/* * 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.writer.internal.geometry; import java.text.DecimalFormat; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import javax.annotation.Nullable; import javax.xml.namespace.QName; import javax.xml.stream.XMLStreamException; import javax.xml.stream.XMLStreamWriter; import com.vividsolutions.jts.geom.Coordinate; import com.vividsolutions.jts.geom.Geometry; import com.vividsolutions.jts.geom.GeometryCollection; import de.fhg.igd.slf4jplus.ALogger; import de.fhg.igd.slf4jplus.ALoggerFactory; import eu.esdihumboldt.hale.common.core.io.report.IOReporter; import eu.esdihumboldt.hale.common.core.io.report.impl.IOMessageImpl; import eu.esdihumboldt.hale.common.schema.model.ChildDefinition; import eu.esdihumboldt.hale.common.schema.model.DefinitionUtil; import eu.esdihumboldt.hale.common.schema.model.PropertyDefinition; import eu.esdihumboldt.hale.common.schema.model.TypeDefinition; import eu.esdihumboldt.hale.common.schema.model.constraint.property.Cardinality; import eu.esdihumboldt.hale.common.schema.model.constraint.type.GeometryType; import eu.esdihumboldt.hale.io.gml.writer.internal.GmlWriterUtil; import eu.esdihumboldt.hale.io.gml.writer.internal.geometry.GeometryConverterRegistry.ConversionLadder; import eu.esdihumboldt.hale.io.gml.writer.internal.geometry.writers.CompositeSurfaceWriter; import eu.esdihumboldt.hale.io.gml.writer.internal.geometry.writers.CurveSingleSegmentWriter; import eu.esdihumboldt.hale.io.gml.writer.internal.geometry.writers.CurveWriter; import eu.esdihumboldt.hale.io.gml.writer.internal.geometry.writers.EnvelopeWriter; import eu.esdihumboldt.hale.io.gml.writer.internal.geometry.writers.LegacyMultiPolygonWriter; import eu.esdihumboldt.hale.io.gml.writer.internal.geometry.writers.LegacyPolygonWriter; import eu.esdihumboldt.hale.io.gml.writer.internal.geometry.writers.LineStringWriter; import eu.esdihumboldt.hale.io.gml.writer.internal.geometry.writers.MultiCurveWriter; import eu.esdihumboldt.hale.io.gml.writer.internal.geometry.writers.MultiLineStringWriter; import eu.esdihumboldt.hale.io.gml.writer.internal.geometry.writers.MultiPointWriter; import eu.esdihumboldt.hale.io.gml.writer.internal.geometry.writers.MultiPolygonWriter; import eu.esdihumboldt.hale.io.gml.writer.internal.geometry.writers.Pattern; import eu.esdihumboldt.hale.io.gml.writer.internal.geometry.writers.PointWriter; import eu.esdihumboldt.hale.io.gml.writer.internal.geometry.writers.PolygonWriter; import eu.esdihumboldt.hale.io.xsd.constraint.XmlAttributeFlag; /** * Write geometries for a GML document. * * @author Simon Templer * @partner 01 / Fraunhofer Institute for Computer Graphics Research */ public class StreamGeometryWriter extends AbstractTypeMatcher<Class<? extends Geometry>> { private static final ALogger log = ALoggerFactory.getLogger(StreamGeometryWriter.class); /** * Get a geometry writer instance with a default configuration. * * @param gmlNs the GML namespace * @param simplifyGeometry if geometries should be simplified before writing * them if possible (e.g. a MultiGeometry with only one geometry * is reduced to the contained geometry) * @return the geometry writer */ public static StreamGeometryWriter getDefaultInstance(String gmlNs, boolean simplifyGeometry) { StreamGeometryWriter sgm = new StreamGeometryWriter(gmlNs, simplifyGeometry); // TODO configure via extension? sgm.registerGeometryWriter(new CurveWriter()); sgm.registerGeometryWriter(new CurveSingleSegmentWriter()); sgm.registerGeometryWriter(new MultiCurveWriter()); sgm.registerGeometryWriter(new PointWriter()); sgm.registerGeometryWriter(new PolygonWriter()); sgm.registerGeometryWriter(new LineStringWriter()); sgm.registerGeometryWriter(new MultiPolygonWriter()); sgm.registerGeometryWriter(new CompositeSurfaceWriter()); sgm.registerGeometryWriter(new MultiPointWriter()); sgm.registerGeometryWriter(new MultiLineStringWriter()); sgm.registerGeometryWriter(new LegacyPolygonWriter()); sgm.registerGeometryWriter(new LegacyMultiPolygonWriter()); sgm.registerGeometryWriter(new EnvelopeWriter()); return sgm; } /** * The GML namespace */ private final String gmlNs; /** * Geometry types mapped to compatible writers */ private final Map<Class<? extends Geometry>, Set<GeometryWriter<?>>> geometryWriters = new HashMap<Class<? extends Geometry>, Set<GeometryWriter<?>>>(); /** * Types mapped to geometry types mapped to matched definition paths */ // XXX stored paths instead per attribute definition? private final Map<TypeDefinition, Map<Class<? extends Geometry>, List<DefinitionPath>>> storedPaths = new HashMap<TypeDefinition, Map<Class<? extends Geometry>, List<DefinitionPath>>>(); private final boolean simplifyGeometry; /** * Constructor * * @param gmlNs the GML namespace * @param simplifyGeometry if geometries should be simplified before writing * them if possible (e.g. a MultiGeometry with only one geometry * is reduced to the contained geometry) */ public StreamGeometryWriter(String gmlNs, boolean simplifyGeometry) { super(); this.gmlNs = gmlNs; this.simplifyGeometry = simplifyGeometry; } /** * Register a geometry writer * * @param writer the geometry writer */ public void registerGeometryWriter(GeometryWriter<?> writer) { Class<? extends Geometry> geomType = writer.getGeometryType(); Set<GeometryWriter<?>> writers = geometryWriters.get(geomType); if (writers == null) { writers = new HashSet<GeometryWriter<?>>(); geometryWriters.put(geomType, writers); } writers.add(writer); } /** * Write a geometry to a stream for a GML document * * @param writer the XML stream writer * @param geometry the geometry * @param property the geometry property * @param srsName the SRS name of a common SRS for the whole document, may * be <code>null</code> * @param report the reporter * @param decimalFormatter a decimal formatter to format geometry * coordinates * @throws XMLStreamException if any error occurs writing the geometry */ public void write(XMLStreamWriter writer, Geometry geometry, PropertyDefinition property, String srsName, IOReporter report, DecimalFormat decimalFormatter) throws XMLStreamException { // write eventual required id GmlWriterUtil.writeRequiredID(writer, property.getPropertyType(), null, false); // write any srsName attribute on the parent element writeSrsName(writer, property.getPropertyType(), geometry, srsName); // write any srsDimension attribute on the parent element writeSrsDimension(writer, property.getPropertyType(), geometry); if (simplifyGeometry) { // if geometry collection containing only one geometry, // reduce to internal geometry if (geometry instanceof GeometryCollection && ((GeometryCollection) geometry).getNumGeometries() == 1) { geometry = geometry.getGeometryN(0); } } Class<? extends Geometry> geomType = geometry.getClass(); // remember if we already found a solution to this problem List<DefinitionPath> preferredPaths = restoreCandidate(property.getPropertyType(), geomType); if (preferredPaths == null) { // find candidates List<DefinitionPath> candidates = findCandidates(property, geomType); // if no candidate found, try with compatible geometries Class<? extends Geometry> originalType = geomType; Geometry originalGeometry = geometry; ConversionLadder ladder = GeometryConverterRegistry.getInstance() .createLadder(geometry); while (candidates.isEmpty() && ladder.hasNext()) { geometry = ladder.next(); geomType = geometry.getClass(); log.info("Possible structure for writing " + originalType.getSimpleName() + //$NON-NLS-1$ " not found, trying " + geomType.getSimpleName() + " instead"); //$NON-NLS-1$ //$NON-NLS-2$ List<DefinitionPath> candPaths = restoreCandidate(property.getPropertyType(), geomType); if (candPaths != null) { // use stored candidate candidates = new ArrayList<>(candPaths); } else { candidates = findCandidates(property, geomType); } } if (candidates.isEmpty()) { // also try the generic geometry type geometry = originalGeometry; geomType = Geometry.class; log.info("Possible structure for writing " + originalType.getSimpleName() + //$NON-NLS-1$ " not found, trying the generic geometry type instead"); //$NON-NLS-1$ //$NON-NLS-2$ List<DefinitionPath> candPaths = restoreCandidate(property.getPropertyType(), geomType); if (candPaths != null) { // use stored candidate candidates = new ArrayList<>(candidates); } else { candidates = findCandidates(property, geomType); } // remember generic match for later storeCandidate(property.getPropertyType(), originalType, sortPreferredCandidates(candidates, geomType)); } for (DefinitionPath candidate : candidates) { log.info("Geometry structure match: " + geomType.getSimpleName() + " - " //$NON-NLS-1$ //$NON-NLS-2$ + candidate); } if (candidates.isEmpty()) { log.error("No geometry structure match for " + //$NON-NLS-1$ originalType.getSimpleName() + " found, writing WKT " + //$NON-NLS-1$ "representation instead"); //$NON-NLS-1$ writer.writeCharacters(originalGeometry.toText()); return; } // determine preferred candidate preferredPaths = sortPreferredCandidates(candidates, geomType); // remember for later storeCandidate(property.getPropertyType(), geomType, preferredPaths); } DefinitionPath path = selectValidPath(preferredPaths, geometry); if (path != null) { // write geometry writeGeometry(writer, geometry, path, srsName, decimalFormatter); } else { report.error(new IOMessageImpl( "No valid path found for encoding geometry, geometry is skipped.", null)); } } /** * Select the first valid path that is applicable for the given geometry. * * @param preferredPaths the path candidates in order * @param geometry the geometry to write * @return the selected path */ @Nullable private DefinitionPath selectValidPath(List<DefinitionPath> preferredPaths, Geometry geometry) { for (DefinitionPath candidate : preferredPaths) { GeometryWriter<?> writer = candidate.getGeometryWriter(); if (writer.accepts(geometry)) { return candidate; } } return null; } /** * Sort the given candidates by preference. * * @param candidates the path candidates * @param geomType the geometry type * @return the sorted list of candidates */ private List<DefinitionPath> sortPreferredCandidates(List<DefinitionPath> candidates, Class<? extends Geometry> geomType) { List<DefinitionPath> preferred = new ArrayList<>(candidates); // sort candidates by preference Collections.sort(preferred, new Comparator<DefinitionPath>() { @Override public int compare(DefinitionPath o1, DefinitionPath o2) { int p1 = priority(o1); int p2 = priority(o2); if (p1 > p2) return -1; else if (p2 < p1) return 1; return 0; } public int priority(DefinitionPath path) { if (path != null && path.getGeometryCompatibleType() != null) { /* * Policies: * * General - prefer simple types over composite types * * MultiPolygons - prefer MultiSurface over * CompositeSurface, because it is more flexible * * MultiLineString - rely on validity check */ switch (path.getGeometryCompatibleType().getName().getLocalPart()) { case "LineStringType": return 50; case "CurveType": return 40; case "CompositeCurveType": return 30; case "MultiCurveType": case "MultiSurfaceType": return 10; case "MultiPolygonType": return 5; case "CompositeSurfaceType": return -10; } } return 0; } }); return preferred; } /** * Find candidates for a possible path to use for writing the geometry * * @param property the start property * @param geomType the geometry type * * @return the path candidates */ public List<DefinitionPath> findCandidates(PropertyDefinition property, Class<? extends Geometry> geomType) { Set<GeometryWriter<?>> writers = findWriters(geomType); if (writers == null || writers.isEmpty()) { // if no writer is present, we can cancel right here return new ArrayList<DefinitionPath>(); } long max = property.getConstraint(Cardinality.class).getMaxOccurs(); return super.findCandidates(property.getPropertyType(), property.getName(), max != Cardinality.UNBOUNDED && max <= 1, geomType); } /** * Write the geometry using the given path * * @param writer the XML stream writer * @param geometry the geometry * @param path the definition path to use * @param srsName the SRS name of a common SRS for the whole document, may * be <code>null</code> * @param decimalFormatter a decimal formatter for geometry values * @throws XMLStreamException if writing the geometry fails */ @SuppressWarnings("unchecked") private void writeGeometry(XMLStreamWriter writer, Geometry geometry, DefinitionPath path, String srsName, DecimalFormat decimalFormatter) throws XMLStreamException { @SuppressWarnings("rawtypes") GeometryWriter geomWriter = path.getGeometryWriter(); QName name = path.getLastName(); if (path.isEmpty()) { // directly write geometry geomWriter.write(writer, geometry, path.getLastType(), name, gmlNs, decimalFormatter); } else { for (PathElement step : path.getSteps()) { if (!step.isTransient()) { // start elements name = step.getName(); GmlWriterUtil.writeStartPathElement(writer, step, false); // write eventual required ID GmlWriterUtil.writeRequiredID(writer, step.getType(), null, false); // write eventual srsName writeSrsName(writer, step.getType(), geometry, srsName); // write eventual srsDimension writeSrsDimension(writer, step.getType(), geometry); } } // write geometry geomWriter.write(writer, geometry, path.getLastType(), name, gmlNs, decimalFormatter); for (int i = 0; i < path.getSteps().size(); i++) { PathElement step = path.getSteps().get(path.getSteps().size() - 1 - i); if (!step.isTransient()) { // end elements writer.writeEndElement(); } } } } /** * Write the SRS name if a corresponding attribute is present * * @param writer the XML stream writer * @param type the element type definition * @param geometry the geometry * @param srsName the common SRS name, may be <code>null</code> * @throws XMLStreamException if writing the SRS name fails */ private void writeSrsName(XMLStreamWriter writer, TypeDefinition type, Geometry geometry, String srsName) throws XMLStreamException { // TODO can SRS be extracted from geometry? if (srsName != null) { PropertyDefinition srsAtt = null; for (ChildDefinition<?> att : DefinitionUtil.getAllProperties(type)) { // XXX is this enough? or should groups be handled explicitly? if (att.asProperty() != null // if we write an attribute, it must be an attribute ;) && att.asProperty().getConstraint(XmlAttributeFlag.class).isEnabled() && att.getName().getLocalPart().equals("srsName") // TODO //$NON-NLS-1$ // improve // condition? && (att.getName().getNamespaceURI() == null || att.getName().getNamespaceURI().equals(gmlNs) || att.getName().getNamespaceURI().isEmpty())) { srsAtt = att.asProperty(); break; } } if (srsAtt != null) { GmlWriterUtil.writeAttribute(writer, srsName, srsAtt); } } } /** * Write the SRS dimension if a corresponding attribute is present * * @param writer the XML stream writer * @param type the element type definition * @param geometry the geometry * @throws XMLStreamException if writing the SRS dimension fails */ private void writeSrsDimension(XMLStreamWriter writer, TypeDefinition type, Geometry geometry) throws XMLStreamException { // detect geometry dimension if possible Integer dimension = detectDimension(geometry); if (dimension != null) { PropertyDefinition dimensionAtt = null; for (ChildDefinition<?> att : DefinitionUtil.getAllProperties(type)) { // XXX is this enough? or should groups be handled explicitly? if (att.asProperty() != null // if we write an attribute, it must be an attribute ;) && att.asProperty().getConstraint(XmlAttributeFlag.class).isEnabled() && att.getName().getLocalPart().equals("srsDimension") //$NON-NLS-1$ && (att.getName().getNamespaceURI() == null || att.getName().getNamespaceURI().equals(gmlNs) || att.getName().getNamespaceURI().isEmpty())) { dimensionAtt = att.asProperty(); break; } } if (dimensionAtt != null) { GmlWriterUtil.writeAttribute(writer, dimension, dimensionAtt); } } } @Nullable private Integer detectDimension(Geometry geometry) { // test the coordinate dimension Coordinate coord = geometry.getCoordinate(); if (coord != null) { int dimension = 2; if (!Double.isNaN(coord.z)) { dimension = 3; } return dimension; } // not able to detect return null; } /** * Store the candidate for later use * * @param type the attribute type definition * @param geomType the geometry type * @param path the definition path */ private void storeCandidate(TypeDefinition type, Class<? extends Geometry> geomType, List<DefinitionPath> path) { Map<Class<? extends Geometry>, List<DefinitionPath>> paths = storedPaths.get(type); if (paths == null) { paths = new HashMap<>(); storedPaths.put(type, paths); } paths.put(geomType, path); } /** * Restore the candidate matching the given types * * @param type the attribute type definition * @param geomType the geometry type * * @return a previously found path or <code>null</code> */ private List<DefinitionPath> restoreCandidate(TypeDefinition type, Class<? extends Geometry> geomType) { Map<Class<? extends Geometry>, List<DefinitionPath>> paths = storedPaths.get(type); if (paths != null) { return paths.get(geomType); } return null; } /** * Determines if a type definition is compatible to a geometry type * * @param type the type definition * @param geomType the geometry type * @param path the current definition path * * @return the (eventually updated) definition path if a match is found, * otherwise <code>null</code> */ @Override protected DefinitionPath matchPath(TypeDefinition type, Class<? extends Geometry> geomType, DefinitionPath path) { // check compatibility list Set<GeometryWriter<?>> writers = findWriters(geomType); if (writers != null) { for (GeometryWriter<?> writer : writers) { boolean compatible = false; Set<QName> names = writer.getCompatibleTypes(); if (names != null) { if (names.contains(type.getName())) { // check type name compatible = true; } if (!compatible && type.getName().getNamespaceURI().equals(gmlNs)) { // check GML type name compatible = names.contains(new QName(Pattern.GML_NAMESPACE_PLACEHOLDER, type.getName().getLocalPart())); // the GML_NAMESPACE_PLACEHOLDER namespace references // the GML namespace } if (compatible) { // check structure / match writer DefinitionPath candidate = writer.match(type, path, gmlNs); if (candidate != null) { // set appropriate writer for path and return it candidate.setGeometryWriter(writer, type); return candidate; } } } } } // fall back to binding test // check for equality because we don't want a match for the property // types Class<? extends Geometry> geomBinding = type.getConstraint(GeometryType.class).getBinding(); boolean compatible = geomType.equals(geomBinding); if (compatible) { // check structure / match writers if (writers != null) { for (GeometryWriter<?> writer : writers) { DefinitionPath candidate = writer.match(type, path, gmlNs); if (candidate != null) { // set appropriate writer for path and return it candidate.setGeometryWriter(writer, type); return candidate; } } } } return null; } private Set<GeometryWriter<?>> findWriters(Class<? extends Geometry> geomType) { Set<GeometryWriter<?>> writers = geometryWriters.get(geomType); if (writers == null) { writers = new HashSet<>(); } else { writers = new HashSet<>(writers); } // also handle super class writers, e.g. LineString for LinearRing Class<?> geomAlt = geomType; while (geomAlt.getSuperclass() != null && !Geometry.class.equals(geomAlt.getSuperclass()) && Geometry.class.isAssignableFrom(geomAlt.getSuperclass())) { geomAlt = geomAlt.getSuperclass(); Set<GeometryWriter<?>> moreWriters = geometryWriters.get(geomAlt); if (moreWriters != null) { writers.addAll(moreWriters); } } return writers; } }