/* * 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); } }