eu.esdihumboldt.hale.io.gml.geometry.GMLGeometryUtil.java Source code

Java tutorial

Introduction

Here is the source code for eu.esdihumboldt.hale.io.gml.geometry.GMLGeometryUtil.java

Source

/*
 * Copyright (c) 2012 Data Harmonisation Panel
 * 
 * All rights reserved. This program and the accompanying materials are made
 * available under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation, either version 3 of the License,
 * or (at your option) any later version.
 * 
 * You should have received a copy of the GNU Lesser General Public License
 * along with this distribution. If not, see <http://www.gnu.org/licenses/>.
 * 
 * Contributors:
 *     HUMBOLDT EU Integrated Project #030962
 *     Data Harmonisation Panel <http://www.dhpanel.eu>
 */

package eu.esdihumboldt.hale.io.gml.geometry;

import java.text.DecimalFormat;
import java.text.DecimalFormatSymbols;
import java.text.NumberFormat;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;

import javax.xml.namespace.QName;

import org.springframework.core.convert.ConversionException;

import com.google.common.base.Splitter;
import com.vividsolutions.jts.geom.Coordinate;
import com.vividsolutions.jts.geom.Geometry;

import de.fhg.igd.slf4jplus.ALogger;
import de.fhg.igd.slf4jplus.ALoggerFactory;
import eu.esdihumboldt.hale.common.convert.ConversionUtil;
import eu.esdihumboldt.hale.common.core.io.IOProvider;
import eu.esdihumboldt.hale.common.instance.helper.BreadthFirstInstanceTraverser;
import eu.esdihumboldt.hale.common.instance.helper.PropertyResolver;
import eu.esdihumboldt.hale.common.instance.model.Instance;
import eu.esdihumboldt.hale.common.schema.geometry.CRSDefinition;

/**
 * Utility methods for reading GML geometries from an {@link Instance} model.
 * 
 * @author Simon Templer
 */
@SuppressWarnings("deprecation")
public abstract class GMLGeometryUtil {

    private static final ALogger log = ALoggerFactory.getLogger(GMLGeometryUtil.class);

    /**
     * Name of reader parameter that specifies if composite geometries should be
     * combined to a single geometry object if possible.
     */
    public static final String PARAM_COMBINE_COMPOSITES = "geometry.combineComposites";

    /**
     * Default value for the {@code PARAM_COMBINE_COMPOSITES} parameter.
     */
    public static final boolean PARAM_COMBINE_COMPOSITES_DEFAULT = true;

    /**
     * Parse coordinates from a GML CoordinatesType instance.
     * 
     * @param coordinates the coordinates instance
     * @return the coordinates or <code>null</code> if the instances contains no
     *         coordinates
     * @throws ParseException if parsing the coordinates fails
     */
    public static Coordinate[] parseCoordinates(Instance coordinates) throws ParseException {
        // XXX should the type be checked to match CoordinatesType?

        Object value = coordinates.getValue();

        if (value != null) {
            try {
                String coordinatesString = ConversionUtil.getAs(value, String.class);
                if (coordinatesString.isEmpty()) {
                    return null;
                }

                // determine symbols
                String decimal = getCoordinatesDecimal(coordinates);
                String cs = getCoordinateSeparator(coordinates);
                String ts = getTupleSeparator(coordinates);

                Splitter coordinateSplitter = Splitter.on(cs).trimResults();
                Splitter tupleSplitter = Splitter.on(ts).trimResults();
                NumberFormat format = NumberFormat.getInstance(Locale.US);
                if (format instanceof DecimalFormat) {
                    DecimalFormat decFormat = ((DecimalFormat) format);
                    DecimalFormatSymbols symbols = decFormat.getDecimalFormatSymbols();
                    symbols.setDecimalSeparator(decimal.charAt(0));
                    decFormat.setDecimalFormatSymbols(symbols);
                }

                List<Coordinate> coordList = new ArrayList<Coordinate>();

                // split into tuples
                Iterable<String> tuples = tupleSplitter.split(coordinatesString.trim());
                for (String tuple : tuples) {
                    Coordinate coord = parseTuple(tuple, coordinateSplitter, format);
                    if (coord != null) {
                        coordList.add(coord);
                    }
                }
                return coordList.toArray(new Coordinate[coordList.size()]);
            } catch (ConversionException e) {
                log.error("Error parsing geometry coordinates", e);
            }
        }

        return null;
    }

    /**
     * Parse a tuple in a GML CoordinatesType string.
     * 
     * @param tuple the tuple
     * @param coordinateSplitter the coordinate splitter
     * @param format the number format
     * @return the coordinate or <code>null</code>
     * @throws ParseException if parsing the coordinates fails
     */
    private static Coordinate parseTuple(String tuple, Splitter coordinateSplitter, NumberFormat format)
            throws ParseException {
        if (tuple == null || tuple.isEmpty()) {
            return null;
        }

        double x = Double.NaN;
        double y = Double.NaN;
        double z = Double.NaN;

        Iterable<String> coordinates = coordinateSplitter.split(tuple);
        Iterator<String> itCoordinates = coordinates.iterator();
        int index = 0;
        while (index <= 2 && itCoordinates.hasNext()) {
            String coord = itCoordinates.next();

            // parse coordinate value
            Number value = format.parse(coord);
            switch (index) {
            case 0:
                x = value.doubleValue();
                break;
            case 1:
                y = value.doubleValue();
                break;
            case 2:
                z = value.doubleValue();
                break;
            }

            index++;
        }

        return new Coordinate(x, y, z);
    }

    private static String getTupleSeparator(Instance coordinates) {
        return getAttributeValue(coordinates, new QName("ts"), " "); // default
        // separator
        // within
        // a
        // tuple
        // is a
        // space
    }

    private static String getCoordinateSeparator(Instance coordinates) {
        return getAttributeValue(coordinates, new QName("cs"), ","); // default
        // separator
        // within
        // a
        // tuple
        // is a
        // comma
    }

    private static String getCoordinatesDecimal(Instance coordinates) {
        return getAttributeValue(coordinates, new QName("decimal"), "."); // default
        // decimal
        // point
        // is
        // a
        // dot
    }

    private static String getAttributeValue(Instance coordinates, QName propertyName, String def) {
        Object[] values = coordinates.getProperty(propertyName);

        if (values != null && values.length > 0) {
            Object value = values[0];
            try {
                String decimal = ConversionUtil.getAs(value, String.class);
                if (decimal != null && !decimal.isEmpty()) { // don't accept
                    // empty values
                    return decimal;
                }
            } catch (ConversionException e) {
                // ignore, just use the default then
            }
        }

        return def;
    }

    /**
     * Parse a coordinate from a GML DirectPositionType instance.
     * 
     * @param directPosition the direct position instance
     * @return the coordinate or <code>null</code> if the instance contains not
     *         direct position
     * @throws GeometryNotSupportedException if no valid coordinate could be
     *             created from the direct position
     */
    public static Coordinate parseDirectPosition(Instance directPosition) throws GeometryNotSupportedException {
        // XXX should the type be checked to match CoordinatesType?

        Object value = directPosition.getValue();

        if (value != null) {
            // binding for DirectPositionType is Collection/Double
            try {
                List<Double> values = ConversionUtil.getAsList(value, Double.class, true);
                if (values.size() == 2) {
                    return new Coordinate(values.get(0), values.get(1));
                } else if (values.size() >= 3) {
                    return new Coordinate(values.get(0), values.get(1), values.get(2));
                } else {
                    throw new GeometryNotSupportedException(
                            "DirectPosition with invalid number of coordinates: " + values.size());
                }
            } catch (ConversionException e) {
                throw new GeometryNotSupportedException(e);
            }
        }

        return null;
    }

    /**
     * Parse a coordinate from a GML PosList instance.
     * 
     * @param posList the PosList instance
     * @param srsDimension the Dimension of the instance
     * @return the array of the coordinates or <code>null</code> if the instance
     *         contains not a PosList
     * @throws GeometryNotSupportedException if no valid coordinate could be
     *             created from the PosList
     */
    public static Coordinate[] parsePosList(Instance posList, int srsDimension)
            throws GeometryNotSupportedException {

        Object value = posList.getValue();
        Coordinate[] coordinates = null;

        // XXX Coordinate support only 2D and 3D coordinates

        if (value != null) {
            try {
                List<Double> values = ConversionUtil.getAsList(value, Double.class, true);

                /*
                 * Filter null values that may have been created because of
                 * whitespace, e.g. at the end or beginning of the list.
                 * 
                 * XXX An alternative would be trimming the list string before
                 * splitting it (in SimpleTypeUtil.convertFromXml), though I am
                 * not sure what the behavior actually should be according to
                 * XML Schema (is whitespace at the beginning/end just ignored
                 * or not?)
                 */
                values.removeAll(Collections.singleton(null));

                List<Coordinate> cs = new ArrayList<Coordinate>();

                // validate dimension
                if (values.size() % srsDimension != 0) {
                    // try alternative dimension
                    int alternative = (srsDimension == 2) ? (3) : (2);

                    if (values.size() % alternative != 0) {
                        // still not valid
                        throw new GeometryNotSupportedException(
                                "Value count in posList not compatible to given dimension.");
                    } else {
                        log.debug("Assuming " + alternative
                                + "-dimensional coordinates, as value count doesn't match " + srsDimension
                                + " dimensions.");
                        srsDimension = alternative;
                    }
                }

                if (srsDimension == 2) {
                    for (int i = 0; i < values.size(); i++) {
                        cs.add(new Coordinate(values.get(i), values.get(++i)));
                    }
                    coordinates = cs.toArray(new Coordinate[values.size() / 2]);
                } else if (srsDimension == 3) {
                    for (int i = 0; i < values.size(); i++) {
                        cs.add(new Coordinate(values.get(i), values.get(++i), values.get(++i)));
                    }
                    coordinates = cs.toArray(new Coordinate[values.size() / 3]);
                } else {
                    throw new GeometryNotSupportedException(
                            "DirectPosition with invalid number of coordinates: " + values.size());
                }

            } catch (ConversionException e) {
                throw new GeometryNotSupportedException(e);
            }
        }

        return coordinates;
    }

    /**
     * Parse a coordinate from a GML CoordType instance.
     * 
     * @param instance the coord instance
     * @return the coordinate
     * @throws GeometryNotSupportedException if a valid coordinate can't be
     *             created
     */
    public static Coordinate parseCoord(Instance instance) throws GeometryNotSupportedException {
        double x = Double.NaN;
        double y = Double.NaN;
        double z = Double.NaN;

        Collection<Object> values = PropertyResolver.getValues(instance, "X", false);
        if (values == null || values.isEmpty()) {
            throw new GeometryNotSupportedException("Missing X coordinate");
        }
        x = ConversionUtil.getAs(values.iterator().next(), Double.class);

        values = PropertyResolver.getValues(instance, "Y", false);
        if (values == null || values.isEmpty()) {
            throw new GeometryNotSupportedException("Missing Y coordinate");
        }
        y = ConversionUtil.getAs(values.iterator().next(), Double.class);

        values = PropertyResolver.getValues(instance, "Z", false);
        if (values != null && !values.isEmpty()) {
            z = ConversionUtil.getAs(values.iterator().next(), Double.class);
        }

        return new Coordinate(x, y, z);
    }

    /**
     * Find the CRS definition to be associated with the geometry contained in
     * the given instance.
     * 
     * @param instance the given instance
     * @return the CRS definition or <code>null</code> if none could be
     *         identified
     */
    public static CRSDefinition findCRS(Instance instance) {
        BreadthFirstInstanceTraverser traverser = new BreadthFirstInstanceTraverser();

        CRSFinder finder = new CRSFinder();
        traverser.traverse(instance, finder);

        return finder.getDefinition();
    }

    /**
     * Determines if the given geometries are all 2D.
     * 
     * @param geometries the geometries
     * @return if the geometries are 2D
     */
    @SafeVarargs
    public static <T extends Geometry> boolean is2D(T... geometries) {
        for (Geometry geom : geometries) {
            if (!is2D(geom.getCoordinates())) {
                return false;
            }
        }

        // all polygons were 2D
        return true;
    }

    private static boolean is2D(Coordinate[] coordinates) {
        for (Coordinate coord : coordinates) {
            if (!Double.isNaN(coord.z)) {
                return false;
            }
        }
        // all coordinates are 2D
        return true;
    }

    /**
     * Determine if combining composite (2D) geometries is enabled for a given
     * reader.
     * 
     * @param reader the reader
     * @return if combining composite geometries is enabled
     */
    public static boolean isCombineCompositesEnabled(IOProvider reader) {
        return reader.getParameter(PARAM_COMBINE_COMPOSITES).as(Boolean.class, PARAM_COMBINE_COMPOSITES_DEFAULT);
    }

}