diff --git a/xtraplatform-features-gml/src/main/java/de/ii/xtraplatform/features/gml/domain/GeometryEncoderGml.java b/xtraplatform-features-gml/src/main/java/de/ii/xtraplatform/features/gml/domain/GeometryEncoderGml.java index 64a715f52..5753006fd 100644 --- a/xtraplatform-features-gml/src/main/java/de/ii/xtraplatform/features/gml/domain/GeometryEncoderGml.java +++ b/xtraplatform-features-gml/src/main/java/de/ii/xtraplatform/features/gml/domain/GeometryEncoderGml.java @@ -8,6 +8,7 @@ package de.ii.xtraplatform.features.gml.domain; import com.google.common.collect.ImmutableSet; +import de.ii.xtraplatform.crs.domain.EpsgCrs; import de.ii.xtraplatform.crs.domain.OgcCrs; import de.ii.xtraplatform.geometries.domain.Axes; import de.ii.xtraplatform.geometries.domain.CircularString; @@ -33,15 +34,13 @@ import java.util.Map; import java.util.Optional; import java.util.Set; +import java.util.function.Function; +import javax.xml.stream.XMLStreamException; +import javax.xml.stream.XMLStreamWriter; public class GeometryEncoderGml implements GeometryVisitor { public static final String SPACE = " "; - public static final String OPEN = "<"; - public static final String CLOSE = ">"; - public static final String EQUALS = "="; - public static final String QUOTE = "\""; - public static final String SLASH = "/"; public static final String COMMA = ","; private static final String POINT = "Point"; @@ -54,6 +53,7 @@ public class GeometryEncoderGml implements GeometryVisitor { private static final String MULTI_CURVE = "MultiCurve"; private static final String MULTI_LINE_STRING = "MultiLineString"; private static final String POLYGON = "Polygon"; + private static final String SURFACE = "Surface"; private static final String POLYGON_PATCH = "PolygonPatch"; private static final String POLYHEDRAL_SURFACE = "PolyhedralSurface"; private static final String MULTI_SURFACE = "MultiSurface"; @@ -121,10 +121,11 @@ public enum Options { WITH_SRS_NAME, WITH_SRS_DIMENSION, LINE_STRING_AS_SEGMENT, - POLYGON_AS_PATCH + POLYGON_AS_PATCH, + USE_SURFACE_RING_CURVE } - private final StringBuilder builder; + private final XMLStreamWriter xmlWriter; private final Optional gmlPrefix; private final String gmlIdPrefix; private final Set options; @@ -132,50 +133,71 @@ public enum Options { private final Optional encodeAsSegmentOrPatch; private final Optional encodeAsEmbeddedGeometry; private final GmlVersion version; + private final Function srsNameMapper; private int nextGmlId = 0; private String srsName; - public GeometryEncoderGml(StringBuilder builder) { - this.builder = builder; + public GeometryEncoderGml(XMLStreamWriter xmlWriter) { + this.xmlWriter = xmlWriter; this.gmlPrefix = Optional.of("gml"); this.gmlIdPrefix = "geom_"; this.options = Set.of(); this.precision = null; + this.srsNameMapper = EpsgCrs::toUriString; this.encodeAsSegmentOrPatch = Optional.of( new GeometryEncoderGml( - builder, + xmlWriter, GmlVersion.GML32, Set.of(Options.LINE_STRING_AS_SEGMENT, Options.POLYGON_AS_PATCH), this.gmlPrefix, Optional.empty(), - List.of())); + List.of(), + this.srsNameMapper)); this.encodeAsEmbeddedGeometry = Optional.of( new GeometryEncoderGml( - builder, GmlVersion.GML32, Set.of(), this.gmlPrefix, Optional.empty(), List.of())); + xmlWriter, + GmlVersion.GML32, + Set.of(), + this.gmlPrefix, + Optional.empty(), + List.of(), + this.srsNameMapper)); this.srsName = null; this.version = GmlVersion.GML32; } public GeometryEncoderGml( - StringBuilder builder, + XMLStreamWriter xmlWriter, GmlVersion version, Set options, Optional gmlPrefix, Optional gmlIdPrefix, List precision) { - this.builder = builder; + this(xmlWriter, version, options, gmlPrefix, gmlIdPrefix, precision, EpsgCrs::toUriString); + } + + public GeometryEncoderGml( + XMLStreamWriter xmlWriter, + GmlVersion version, + Set options, + Optional gmlPrefix, + Optional gmlIdPrefix, + List precision, + Function srsNameMapper) { + this.xmlWriter = xmlWriter; this.gmlPrefix = gmlPrefix; this.gmlIdPrefix = gmlIdPrefix.orElse("geom_"); this.options = options; + this.srsNameMapper = srsNameMapper; this.encodeAsSegmentOrPatch = options.contains(Options.LINE_STRING_AS_SEGMENT) && options.contains(Options.POLYGON_AS_PATCH) ? Optional.empty() : Optional.of( new GeometryEncoderGml( - builder, + xmlWriter, version, ImmutableSet.builder() .addAll(options) @@ -183,12 +205,13 @@ public GeometryEncoderGml( .build(), gmlPrefix, Optional.of(this.gmlIdPrefix + "seg_"), - precision)); + precision, + srsNameMapper)); this.encodeAsEmbeddedGeometry = options.contains(Options.WITH_SRS_NAME) ? Optional.of( new GeometryEncoderGml( - builder, + xmlWriter, version, ImmutableSet.builder() .addAll( @@ -198,7 +221,8 @@ public GeometryEncoderGml( .build(), gmlPrefix, Optional.of(this.gmlIdPrefix + "embed_"), - precision)) + precision, + srsNameMapper)) : Optional.empty(); this.precision = precision.stream().anyMatch(v -> v > 0) @@ -212,36 +236,48 @@ public GeometryEncoderGml( public Optional initAndCheckGeometry(Geometry geometry) { if (srsName == null) { srsName = - geometry - .getCrs() - .orElse( - geometry.getAxes() == Axes.XY || geometry.getAxes() == Axes.XYM - ? OgcCrs.CRS84 - : OgcCrs.CRS84h) - .toUriString(); + srsNameMapper.apply( + geometry + .getCrs() + .orElse( + geometry.getAxes() == Axes.XY || geometry.getAxes() == Axes.XYM + ? OgcCrs.CRS84 + : OgcCrs.CRS84h)); } return Optional.empty(); } private void write(String s) { - builder.append(s); + try { + xmlWriter.writeCharacters(s); + } catch (XMLStreamException e) { + throw new IllegalStateException(e); + } } private void write(double d) { - builder.append(d); + try { + xmlWriter.writeCharacters(Double.toString(d)); + } catch (XMLStreamException e) { + throw new IllegalStateException(e); + } } private void write(BigDecimal d) { - builder.append(d.toString()); + try { + xmlWriter.writeCharacters(d.toString()); + } catch (XMLStreamException e) { + throw new IllegalStateException(e); + } } private void writeAttribute(String name, String value) { - write(name); - write(EQUALS); - write(QUOTE); - write(value); - write(QUOTE); + try { + xmlWriter.writeAttribute(name, value); + } catch (XMLStreamException e) { + throw new IllegalStateException(e); + } } private void writeStartTagObject(String tagName, boolean suppressSrsName) { @@ -262,33 +298,33 @@ private void writeStartTagProperty(String tagName, Map attribute private void writeStartTag( String tagName, Map attributes, boolean isObject, boolean suppressSrsName) { - write(OPEN); - gmlPrefix.ifPresent(pre -> write(pre + ':')); - write(tagName); - if (isObject) { - if (options.contains(Options.WITH_GML_ID)) { - write(SPACE); - writeAttribute(gmlPrefix.map(pre -> pre + ':' + ID).orElse(ID), gmlIdPrefix + nextGmlId++); + try { + if (gmlPrefix.isPresent()) { + xmlWriter.writeStartElement(gmlPrefix.get(), tagName, null); + } else { + xmlWriter.writeStartElement(tagName); } - if (options.contains(Options.WITH_SRS_NAME) && !suppressSrsName) { - write(SPACE); - writeAttribute(SRS_NAME, srsName); + if (isObject) { + if (options.contains(Options.WITH_GML_ID)) { + writeAttribute( + gmlPrefix.map(pre -> pre + ':' + ID).orElse(ID), gmlIdPrefix + nextGmlId++); + } + if (options.contains(Options.WITH_SRS_NAME) && !suppressSrsName) { + writeAttribute(SRS_NAME, srsName); + } } + attributes.forEach(this::writeAttribute); + } catch (XMLStreamException e) { + throw new IllegalStateException(e); } - attributes.forEach( - (key, value) -> { - write(SPACE); - writeAttribute(key, value); - }); - write(CLOSE); } - private void writeEndTag(String tagName) { - write(OPEN); - write(SLASH); - gmlPrefix.ifPresent(pre -> write(pre + ':')); - write(tagName); - write(CLOSE); + private void writeEndTag() { + try { + xmlWriter.writeEndElement(); + } catch (XMLStreamException e) { + throw new IllegalStateException(e); + } } private void writeCoordinates(double[] coordinates, Axes axes) { @@ -326,7 +362,7 @@ private void writePosition(double[] coordinates, Axes axes) { writeStartTagProperty(COORDINATES); } writeCoordinates(coordinates, axes); - writeEndTag(version != GmlVersion.GML21 ? POS : COORDINATES); + writeEndTag(); } private void writePositionList(double[] coordinates, Axes axes) { @@ -340,14 +376,14 @@ private void writePositionList(double[] coordinates, Axes axes) { writeStartTagProperty(COORDINATES); } writeCoordinates(coordinates, axes); - writeEndTag(version != GmlVersion.GML21 ? POS_LIST : COORDINATES); + writeEndTag(); } @Override public Void visit(Point geometry) { writeStartTagObject(POINT, false); writePosition(geometry.getValue().getCoordinates(), geometry.getAxes()); - writeEndTag(POINT); + writeEndTag(); return null; } @@ -358,9 +394,9 @@ public Void visit(MultiPoint geometry) { Point point = geometry.getValue().get(i); writeStartTagProperty(POINT_MEMBER); point.accept(encodeAsEmbeddedGeometry.orElse(this)); - writeEndTag(POINT_MEMBER); + writeEndTag(); } - writeEndTag(MULTI_POINT); + writeEndTag(); return null; } @@ -375,16 +411,24 @@ public Void visit(SingleCurve geometry) { } private void writeLineString(LineString geometry, boolean asSegment) { - String tagName; + boolean useSurfaceAndCurve = options.contains(Options.USE_SURFACE_RING_CURVE); if (asSegment) { - tagName = LINE_STRING_SEGMENT; - writeStartTagDataType(tagName); + writeStartTagDataType(LINE_STRING_SEGMENT); + writePositionList(geometry.getValue().getCoordinates(), geometry.getAxes()); + writeEndTag(); + } else if (useSurfaceAndCurve) { + writeStartTagObject(CURVE, false); + writeStartTagProperty(SEGMENTS); + writeStartTagDataType(LINE_STRING_SEGMENT); + writePositionList(geometry.getValue().getCoordinates(), geometry.getAxes()); + writeEndTag(); + writeEndTag(); + writeEndTag(); } else { - tagName = LINE_STRING; - writeStartTagObject(tagName, false); + writeStartTagObject(LINE_STRING, false); + writePositionList(geometry.getValue().getCoordinates(), geometry.getAxes()); + writeEndTag(); } - writePositionList(geometry.getValue().getCoordinates(), geometry.getAxes()); - writeEndTag(tagName); } private void writeCircularString(CircularString geometry, boolean asSegment) { @@ -395,10 +439,10 @@ private void writeCircularString(CircularString geometry, boolean asSegment) { String tagName = geometry.getValue().getNumPositions() == 3 ? ARC : ARC_STRING; writeStartTagDataType(tagName); writePositionList(geometry.getValue().getCoordinates(), geometry.getAxes()); - writeEndTag(tagName); + writeEndTag(); if (!asSegment) { - writeEndTag(SEGMENTS); - writeEndTag(CURVE); + writeEndTag(); + writeEndTag(); } } @@ -409,16 +453,21 @@ public Void visit(MultiLineString geometry) { LineString lineString = geometry.getValue().get(i); writeStartTagProperty(version != GmlVersion.GML21 ? CURVE_MEMBER : LINE_STRING_MEMBER); lineString.accept(encodeAsEmbeddedGeometry.orElse(this)); - writeEndTag(version != GmlVersion.GML21 ? CURVE_MEMBER : LINE_STRING_MEMBER); + writeEndTag(); } - writeEndTag(version != GmlVersion.GML21 ? MULTI_CURVE : MULTI_LINE_STRING); + writeEndTag(); return null; } @Override public Void visit(Polygon geometry) { boolean asPatch = options.contains(Options.POLYGON_AS_PATCH); - if (asPatch) { + boolean useSurfaceAndCurve = options.contains(Options.USE_SURFACE_RING_CURVE); + if (useSurfaceAndCurve) { + writeStartTagObject(SURFACE, false); + writeStartTagProperty(PATCHES); + writeStartTagDataType(POLYGON_PATCH); + } else if (asPatch) { writeStartTagDataType(POLYGON_PATCH); } else { writeStartTagObject(POLYGON, false); @@ -430,20 +479,25 @@ public Void visit(Polygon geometry) { } else { writeStartTagProperty(version != GmlVersion.GML21 ? INTERIOR : INNER_BOUNDARY_IS); } - writeStartTagObject(LINEAR_RING, true); - writePositionList(ring.getValue().getCoordinates(), geometry.getAxes()); - writeEndTag(LINEAR_RING); - if (i == 0) { - writeEndTag(version != GmlVersion.GML21 ? EXTERIOR : OUTER_BOUNDARY_IS); + if (useSurfaceAndCurve) { + writeStartTagObject(RING, true); + writeStartTagProperty(CURVE_MEMBER); + writeStartTagDataType(LINE_STRING_SEGMENT); + writePositionList(ring.getValue().getCoordinates(), geometry.getAxes()); + writeEndTag(); + writeEndTag(); } else { - writeEndTag(version != GmlVersion.GML21 ? INTERIOR : INNER_BOUNDARY_IS); + writeStartTagObject(LINEAR_RING, true); + writePositionList(ring.getValue().getCoordinates(), geometry.getAxes()); } + writeEndTag(); + writeEndTag(); } - if (asPatch) { - writeEndTag(POLYGON_PATCH); - } else { - writeEndTag(POLYGON); + if (useSurfaceAndCurve) { + writeEndTag(); + writeEndTag(); } + writeEndTag(); return null; } @@ -454,9 +508,9 @@ public Void visit(MultiPolygon geometry) { Polygon polygon = geometry.getValue().get(i); writeStartTagProperty(version != GmlVersion.GML21 ? SURFACE_MEMBER : POLYGON_MEMBER); polygon.accept(encodeAsEmbeddedGeometry.orElse(this)); - writeEndTag(version != GmlVersion.GML21 ? SURFACE_MEMBER : POLYGON_MEMBER); + writeEndTag(); } - writeEndTag(version != GmlVersion.GML21 ? MULTI_SURFACE : MULTI_POLYGON); + writeEndTag(); return null; } @@ -467,9 +521,9 @@ public Void visit(MultiCurve geometry) { Geometry geometry2 = geometry.getValue().get(i); writeStartTagProperty(CURVE_MEMBER); geometry2.accept(encodeAsEmbeddedGeometry.orElse(this)); - writeEndTag(CURVE_MEMBER); + writeEndTag(); } - writeEndTag(MULTI_CURVE); + writeEndTag(); return null; } @@ -480,9 +534,9 @@ public Void visit(MultiSurface geometry) { Geometry geometry2 = geometry.getValue().get(i); writeStartTagProperty(SURFACE_MEMBER); geometry2.accept(encodeAsEmbeddedGeometry.orElse(this)); - writeEndTag(SURFACE_MEMBER); + writeEndTag(); } - writeEndTag(MULTI_SURFACE); + writeEndTag(); return null; } @@ -493,47 +547,75 @@ public Void visit(GeometryCollection geometry) { Geometry geometry2 = geometry.getValue().get(i); writeStartTagProperty(GEOMETRY_MEMBER); geometry2.accept(encodeAsEmbeddedGeometry.orElse(this)); - writeEndTag(GEOMETRY_MEMBER); + writeEndTag(); } - writeEndTag(MULTI_GEOMETRY); + writeEndTag(); return null; } @Override public Void visit(CompoundCurve geometry) { - writeStartTagObject(CURVE, false); - writeStartTagProperty(SEGMENTS); - for (int i = 0; i < geometry.getNumGeometries(); i++) { - SingleCurve curve = geometry.getValue().get(i); - curve.accept(encodeAsSegmentOrPatch.orElse(this)); + boolean useSurfaceAndCurve = options.contains(Options.USE_SURFACE_RING_CURVE); + if (useSurfaceAndCurve) { + writeStartTagObject(COMPOSITE_CURVE, false); + for (int i = 0; i < geometry.getNumGeometries(); i++) { + SingleCurve curve = geometry.getValue().get(i); + writeStartTagProperty(CURVE_MEMBER); + curve.accept(encodeAsEmbeddedGeometry.orElse(this)); + writeEndTag(); + } + writeEndTag(); + } else { + writeStartTagObject(CURVE, false); + writeStartTagProperty(SEGMENTS); + for (int i = 0; i < geometry.getNumGeometries(); i++) { + SingleCurve curve = geometry.getValue().get(i); + curve.accept(encodeAsSegmentOrPatch.orElse(this)); + } + writeEndTag(); + writeEndTag(); } - writeEndTag(SEGMENTS); - writeEndTag(CURVE); return null; } @Override public Void visit(CurvePolygon geometry) { - writeStartTagObject(POLYGON, false); + boolean useSurfaceAndCurve = options.contains(Options.USE_SURFACE_RING_CURVE); + if (useSurfaceAndCurve) { + writeStartTagObject(SURFACE, false); + writeStartTagProperty(PATCHES); + writeStartTagDataType(POLYGON_PATCH); + } else { + writeStartTagObject(POLYGON, false); + } for (int i = 0; i < geometry.getNumRings(); i++) { Curve ring = geometry.getValue().get(i); if (i == 0) { - writeStartTagProperty(EXTERIOR); + writeStartTagProperty(version != GmlVersion.GML21 ? EXTERIOR : OUTER_BOUNDARY_IS); } else { - writeStartTagProperty(INTERIOR); + writeStartTagProperty(version != GmlVersion.GML21 ? INTERIOR : INNER_BOUNDARY_IS); } - writeStartTagObject(RING, false); - writeStartTagProperty(CURVE_MEMBER); - ring.accept(encodeAsEmbeddedGeometry.orElse(this)); - writeEndTag(CURVE_MEMBER); - writeEndTag(RING); - if (i == 0) { - writeEndTag(EXTERIOR); + writeStartTagObject(RING, true); + if (useSurfaceAndCurve && ring instanceof CompoundCurve compoundCurve) { + for (int j = 0; j < compoundCurve.getNumGeometries(); j++) { + SingleCurve segment = compoundCurve.getValue().get(j); + writeStartTagProperty(CURVE_MEMBER); + segment.accept(encodeAsEmbeddedGeometry.orElse(this)); + writeEndTag(); + } } else { - writeEndTag(INTERIOR); + writeStartTagProperty(CURVE_MEMBER); + ring.accept(encodeAsEmbeddedGeometry.orElse(this)); + writeEndTag(); } + writeEndTag(); + writeEndTag(); + } + if (useSurfaceAndCurve) { + writeEndTag(); + writeEndTag(); } - writeEndTag(POLYGON); + writeEndTag(); return null; } @@ -547,11 +629,11 @@ public Void visit(PolyhedralSurface geometry) { writeStartTagProperty(SURFACE_MEMBER); Polygon polygon = geometry.getValue().get(i); polygon.accept(encodeAsEmbeddedGeometry.orElse(this)); - writeEndTag(SURFACE_MEMBER); + writeEndTag(); } - writeEndTag(SHELL); - writeEndTag(EXTERIOR); - writeEndTag(SOLID); + writeEndTag(); + writeEndTag(); + writeEndTag(); } else { writeStartTagObject(POLYHEDRAL_SURFACE, false); writeStartTagProperty(PATCHES); @@ -559,8 +641,8 @@ public Void visit(PolyhedralSurface geometry) { Polygon polygon = geometry.getValue().get(i); polygon.accept(encodeAsSegmentOrPatch.orElse(this)); } - writeEndTag(PATCHES); - writeEndTag(POLYHEDRAL_SURFACE); + writeEndTag(); + writeEndTag(); } return null; } diff --git a/xtraplatform-features-gml/src/test/groovy/de/ii/xtraplatform/features/gml/domain/GeometrySpec.groovy b/xtraplatform-features-gml/src/test/groovy/de/ii/xtraplatform/features/gml/domain/GeometrySpec.groovy index 6d8f076cc..bc5acd08a 100644 --- a/xtraplatform-features-gml/src/test/groovy/de/ii/xtraplatform/features/gml/domain/GeometrySpec.groovy +++ b/xtraplatform-features-gml/src/test/groovy/de/ii/xtraplatform/features/gml/domain/GeometrySpec.groovy @@ -26,28 +26,37 @@ import de.ii.xtraplatform.geometries.domain.Position import de.ii.xtraplatform.geometries.domain.PositionList import spock.lang.Specification +import javax.xml.stream.XMLOutputFactory + class GeometrySpec extends Specification { - StringBuilder sb = new StringBuilder() - GeometryEncoderGml gmlEncoderWith = new GeometryEncoderGml(sb, GmlVersion.GML32, Set.of(GeometryEncoderGml.Options.WITH_GML_ID, GeometryEncoderGml.Options.WITH_SRS_NAME), Optional.of("gml"), Optional.of("g_"), List.of(1,1)) - GeometryEncoderGml gmlEncoderWithout = new GeometryEncoderGml(sb, GmlVersion.GML32, Set.of(), Optional.of("gml"), Optional.empty(), List.of()) - GeometryEncoderGml gmlEncoderGml21 = new GeometryEncoderGml(sb, GmlVersion.GML21, Set.of(), Optional.of("gml"), Optional.empty(), List.of(1,1)) + // ... def 'POINT XY'() { - given: Geometry geometry = Point.of(10.81, 10.37) when: - sb.setLength(0) + def sw1 = new StringWriter() + def xmlWriter1 = XMLOutputFactory.newInstance().createXMLStreamWriter(sw1) + def gmlEncoderWithout = new GeometryEncoderGml(xmlWriter1, GmlVersion.GML32, Set.of(), Optional.of("gml"), Optional.empty(), List.of()) geometry.accept(gmlEncoderWithout) - String gmlOut1 = sb.toString() - sb.setLength(0) + xmlWriter1.flush() + String gmlOut1 = sw1.toString() + + def sw2 = new StringWriter() + def xmlWriter2 = XMLOutputFactory.newInstance().createXMLStreamWriter(sw2) + def gmlEncoderWith = new GeometryEncoderGml(xmlWriter2, GmlVersion.GML32, Set.of(GeometryEncoderGml.Options.WITH_GML_ID, GeometryEncoderGml.Options.WITH_SRS_NAME), Optional.of("gml"), Optional.of("g_"), List.of(1,1)) geometry.accept(gmlEncoderWith) - String gmlOut2 = sb.toString() - sb.setLength(0) + xmlWriter2.flush() + String gmlOut2 = sw2.toString() + + def sw3 = new StringWriter() + def xmlWriter3 = XMLOutputFactory.newInstance().createXMLStreamWriter(sw3) + def gmlEncoderGml21 = new GeometryEncoderGml(xmlWriter3, GmlVersion.GML21, Set.of(), Optional.of("gml"), Optional.empty(), List.of(1,1)) geometry.accept(gmlEncoderGml21) - String gmlOut21 = sb.toString() + xmlWriter3.flush() + String gmlOut21 = sw3.toString() then: gmlOut1 == "10.81 10.37" @@ -60,12 +69,19 @@ class GeometrySpec extends Specification { Geometry geometry = Point.of(Position.ofXYZ(10.81, 10.37, 5.00)) when: - sb.setLength(0) + def sw1 = new StringWriter() + def xmlWriter1 = XMLOutputFactory.newInstance().createXMLStreamWriter(sw1) + def gmlEncoderWithout = new GeometryEncoderGml(xmlWriter1, GmlVersion.GML32, Set.of(), Optional.of("gml"), Optional.empty(), List.of()) geometry.accept(gmlEncoderWithout) - String gmlOut = sb.toString() - sb.setLength(0) + xmlWriter1.flush() + String gmlOut = sw1.toString() + + def sw2 = new StringWriter() + def xmlWriter2 = XMLOutputFactory.newInstance().createXMLStreamWriter(sw2) + def gmlEncoderGml21 = new GeometryEncoderGml(xmlWriter2, GmlVersion.GML21, Set.of(), Optional.of("gml"), Optional.empty(), List.of(1,1)) geometry.accept(gmlEncoderGml21) - String gmlOut21 = sb.toString() + xmlWriter2.flush() + String gmlOut21 = sw2.toString() then: gmlOut == "10.81 10.37 5.0" @@ -77,9 +93,12 @@ class GeometrySpec extends Specification { Geometry geometry = Point.of(Position.ofXYM(10.81, 10.37, 5.00)) when: - sb.setLength(0) + def sw = new StringWriter() + def xmlWriter = XMLOutputFactory.newInstance().createXMLStreamWriter(sw) + def gmlEncoderWithout = new GeometryEncoderGml(xmlWriter, GmlVersion.GML32, Set.of(), Optional.of("gml"), Optional.empty(), List.of()) geometry.accept(gmlEncoderWithout) - String gmlOut = sb.toString() + xmlWriter.flush() + String gmlOut = sw.toString() then: gmlOut == "10.81 10.37" @@ -90,9 +109,12 @@ class GeometrySpec extends Specification { Geometry geometry = Point.of(Position.ofXYZM(10.81, 10.37, 5.00, 7.50)) when: - sb.setLength(0) + def sw = new StringWriter() + def xmlWriter = XMLOutputFactory.newInstance().createXMLStreamWriter(sw) + def gmlEncoderWithout = new GeometryEncoderGml(xmlWriter, GmlVersion.GML32, Set.of(), Optional.of("gml"), Optional.empty(), List.of()) geometry.accept(gmlEncoderWithout) - String gmlOut = sb.toString() + xmlWriter.flush() + String gmlOut = sw.toString() then: gmlOut == "10.81 10.37 5.0" @@ -100,12 +122,15 @@ class GeometrySpec extends Specification { def 'POINT XY EMPTY'() { given: - Geometry geometry = Point.empty(Axes.XY); + Geometry geometry = Point.empty(Axes.XY) when: - sb.setLength(0) + def sw = new StringWriter() + def xmlWriter = XMLOutputFactory.newInstance().createXMLStreamWriter(sw) + def gmlEncoderWithout = new GeometryEncoderGml(xmlWriter, GmlVersion.GML32, Set.of(), Optional.of("gml"), Optional.empty(), List.of()) geometry.accept(gmlEncoderWithout) - String gmlOut = sb.toString() + xmlWriter.flush() + String gmlOut = sw.toString() then: gmlOut == "NaN NaN" @@ -113,12 +138,15 @@ class GeometrySpec extends Specification { def 'POINT XYZ EMPTY'() { given: - Geometry geometry = Point.empty(Axes.XYZ); + Geometry geometry = Point.empty(Axes.XYZ) when: - sb.setLength(0) + def sw = new StringWriter() + def xmlWriter = XMLOutputFactory.newInstance().createXMLStreamWriter(sw) + def gmlEncoderWithout = new GeometryEncoderGml(xmlWriter, GmlVersion.GML32, Set.of(), Optional.of("gml"), Optional.empty(), List.of()) geometry.accept(gmlEncoderWithout) - String gmlOut = sb.toString() + xmlWriter.flush() + String gmlOut = sw.toString() then: gmlOut == "NaN NaN NaN" @@ -126,15 +154,22 @@ class GeometrySpec extends Specification { def 'LINESTRING XY'() { given: - Geometry geometry = LineString.of(new double[]{10.0,10.0,20.0,20.0,30.0,40.0}); + Geometry geometry = LineString.of(new double[]{10.0,10.0,20.0,20.0,30.0,40.0}) when: - sb.setLength(0) + def sw1 = new StringWriter() + def xmlWriter1 = XMLOutputFactory.newInstance().createXMLStreamWriter(sw1) + def gmlEncoderWithout = new GeometryEncoderGml(xmlWriter1, GmlVersion.GML32, Set.of(), Optional.of("gml"), Optional.empty(), List.of()) geometry.accept(gmlEncoderWithout) - String gmlOut = sb.toString() - sb.setLength(0) + xmlWriter1.flush() + String gmlOut = sw1.toString() + + def sw2 = new StringWriter() + def xmlWriter2 = XMLOutputFactory.newInstance().createXMLStreamWriter(sw2) + def gmlEncoderGml21 = new GeometryEncoderGml(xmlWriter2, GmlVersion.GML21, Set.of(), Optional.of("gml"), Optional.empty(), List.of(1,1)) geometry.accept(gmlEncoderGml21) - String gmlOut21 = sb.toString() + xmlWriter2.flush() + String gmlOut21 = sw2.toString() then: gmlOut == "10.0 10.0 20.0 20.0 30.0 40.0" @@ -143,15 +178,22 @@ class GeometrySpec extends Specification { def 'LINESTRING XYZ'() { given: - Geometry geometry = LineString.of(PositionList.of(Axes.XYZ, new double[]{10.0,10.0,1.0,20.0,20.0,2.0,30.0,40.0,3.0})); + Geometry geometry = LineString.of(PositionList.of(Axes.XYZ, new double[]{10.0,10.0,1.0,20.0,20.0,2.0,30.0,40.0,3.0})) when: - sb.setLength(0) + def sw1 = new StringWriter() + def xmlWriter1 = XMLOutputFactory.newInstance().createXMLStreamWriter(sw1) + def gmlEncoderWithout = new GeometryEncoderGml(xmlWriter1, GmlVersion.GML32, Set.of(), Optional.of("gml"), Optional.empty(), List.of()) geometry.accept(gmlEncoderWithout) - String gmlOut = sb.toString() - sb.setLength(0) + xmlWriter1.flush() + String gmlOut = sw1.toString() + + def sw2 = new StringWriter() + def xmlWriter2 = XMLOutputFactory.newInstance().createXMLStreamWriter(sw2) + def gmlEncoderGml21 = new GeometryEncoderGml(xmlWriter2, GmlVersion.GML21, Set.of(), Optional.of("gml"), Optional.empty(), List.of(1,1)) geometry.accept(gmlEncoderGml21) - String gmlOut21 = sb.toString() + xmlWriter2.flush() + String gmlOut21 = sw2.toString() then: gmlOut == "10.0 10.0 1.0 20.0 20.0 2.0 30.0 40.0 3.0" @@ -163,12 +205,18 @@ class GeometrySpec extends Specification { Geometry geometry = MultiPoint.of(List.of(Point.of(10,10),Point.of(20,20))) when: - sb.setLength(0) + def sw = new StringWriter() + def xmlWriter = XMLOutputFactory.newInstance().createXMLStreamWriter(sw) + def gmlEncoderWithout = new GeometryEncoderGml(xmlWriter, GmlVersion.GML32, Set.of(), Optional.of("gml"), Optional.empty(), List.of()) geometry.accept(gmlEncoderWithout) - String gmlOut = sb.toString() - sb.setLength(0) + xmlWriter.flush() + String gmlOut = sw.toString() + def sw2 = new StringWriter() + def xmlWriter2 = XMLOutputFactory.newInstance().createXMLStreamWriter(sw2) + def gmlEncoderGml21 = new GeometryEncoderGml(xmlWriter2, GmlVersion.GML21, Set.of(), Optional.of("gml"), Optional.empty(), List.of(1,1)) geometry.accept(gmlEncoderGml21) - String gmlOut21 = sb.toString() + xmlWriter2.flush() + String gmlOut21 = sw2.toString() then: gmlOut == "10.0 10.020.0 20.0" @@ -180,12 +228,18 @@ class GeometrySpec extends Specification { Geometry geometry = Polygon.of(List.of(PositionList.of(Axes.XY,new double[]{10.0,10.0,20.0,20.0,30.0,40.0,10.0,10.0}))) when: - sb.setLength(0) + def sw = new StringWriter() + def xmlWriter = XMLOutputFactory.newInstance().createXMLStreamWriter(sw) + def gmlEncoderWithout = new GeometryEncoderGml(xmlWriter, GmlVersion.GML32, Set.of(), Optional.of("gml"), Optional.empty(), List.of()) geometry.accept(gmlEncoderWithout) - String gmlOut = sb.toString() - sb.setLength(0) + xmlWriter.flush() + String gmlOut = sw.toString() + def sw2 = new StringWriter() + def xmlWriter2 = XMLOutputFactory.newInstance().createXMLStreamWriter(sw2) + def gmlEncoderGml21 = new GeometryEncoderGml(xmlWriter2, GmlVersion.GML21, Set.of(), Optional.of("gml"), Optional.empty(), List.of(1,1)) geometry.accept(gmlEncoderGml21) - String gmlOut21 = sb.toString() + xmlWriter2.flush() + String gmlOut21 = sw2.toString() then: gmlOut == "10.0 10.0 20.0 20.0 30.0 40.0 10.0 10.0" @@ -200,12 +254,18 @@ class GeometrySpec extends Specification { )) when: - sb.setLength(0) + def sw = new StringWriter() + def xmlWriter = XMLOutputFactory.newInstance().createXMLStreamWriter(sw) + def gmlEncoderWithout = new GeometryEncoderGml(xmlWriter, GmlVersion.GML32, Set.of(), Optional.of("gml"), Optional.empty(), List.of()) geometry.accept(gmlEncoderWithout) - String gmlOut = sb.toString() - sb.setLength(0) + xmlWriter.flush() + String gmlOut = sw.toString() + def sw2 = new StringWriter() + def xmlWriter2 = XMLOutputFactory.newInstance().createXMLStreamWriter(sw2) + def gmlEncoderGml21 = new GeometryEncoderGml(xmlWriter2, GmlVersion.GML21, Set.of(), Optional.of("gml"), Optional.empty(), List.of(1,1)) geometry.accept(gmlEncoderGml21) - String gmlOut21 = sb.toString() + xmlWriter2.flush() + String gmlOut21 = sw2.toString() then: gmlOut == "10.0 10.0 20.0 20.030.0 40.0 50.0 60.0" @@ -217,12 +277,18 @@ class GeometrySpec extends Specification { Geometry geometry = MultiPolygon.of(List.of(Polygon.of(List.of(PositionList.of(Axes.XY,new double[]{10.0,10.0,20.0,20.0,30.0,40.0,10.0,10.0}))), Polygon.of(List.of(PositionList.of(Axes.XY,new double[]{50.0,50.0,60.0,60.0,70.0,80.0,50.0,50.0}))))) when: - sb.setLength(0) + def sw = new StringWriter() + def xmlWriter = XMLOutputFactory.newInstance().createXMLStreamWriter(sw) + def gmlEncoderWithout = new GeometryEncoderGml(xmlWriter, GmlVersion.GML32, Set.of(), Optional.of("gml"), Optional.empty(), List.of()) geometry.accept(gmlEncoderWithout) - String gmlOut = sb.toString() - sb.setLength(0) + xmlWriter.flush() + String gmlOut = sw.toString() + def sw2 = new StringWriter() + def xmlWriter2 = XMLOutputFactory.newInstance().createXMLStreamWriter(sw2) + def gmlEncoderGml21 = new GeometryEncoderGml(xmlWriter2, GmlVersion.GML21, Set.of(), Optional.of("gml"), Optional.empty(), List.of(1,1)) geometry.accept(gmlEncoderGml21) - String gmlOut21 = sb.toString() + xmlWriter2.flush() + String gmlOut21 = sw2.toString() then: gmlOut == "10.0 10.0 20.0 20.0 30.0 40.0 10.0 10.050.0 50.0 60.0 60.0 70.0 80.0 50.0 50.0" @@ -234,12 +300,18 @@ class GeometrySpec extends Specification { Geometry geometry = GeometryCollection.of(List.of(Point.of(10,10),LineString.of(new double[]{20.0,20.0,30.0,30.0}))) when: - sb.setLength(0) + def sw = new StringWriter() + def xmlWriter = XMLOutputFactory.newInstance().createXMLStreamWriter(sw) + def gmlEncoderWithout = new GeometryEncoderGml(xmlWriter, GmlVersion.GML32, Set.of(), Optional.of("gml"), Optional.empty(), List.of()) geometry.accept(gmlEncoderWithout) - String gmlOut = sb.toString() - sb.setLength(0) + xmlWriter.flush() + String gmlOut = sw.toString() + def sw2 = new StringWriter() + def xmlWriter2 = XMLOutputFactory.newInstance().createXMLStreamWriter(sw2) + def gmlEncoderGml21 = new GeometryEncoderGml(xmlWriter2, GmlVersion.GML21, Set.of(), Optional.of("gml"), Optional.empty(), List.of(1,1)) geometry.accept(gmlEncoderGml21) - String gmlOut21 = sb.toString() + xmlWriter2.flush() + String gmlOut21 = sw2.toString() then: gmlOut == "10.0 10.020.0 20.0 30.0 30.0" @@ -253,12 +325,18 @@ class GeometrySpec extends Specification { Geometry geometry = GeometryCollection.of(List.of(geometryCollection, multiPoint)) when: - sb.setLength(0) + def sw = new StringWriter() + def xmlWriter = XMLOutputFactory.newInstance().createXMLStreamWriter(sw) + def gmlEncoderWithout = new GeometryEncoderGml(xmlWriter, GmlVersion.GML32, Set.of(), Optional.of("gml"), Optional.empty(), List.of()) geometry.accept(gmlEncoderWithout) - String gmlOut = sb.toString() - sb.setLength(0) + xmlWriter.flush() + String gmlOut = sw.toString() + def sw2 = new StringWriter() + def xmlWriter2 = XMLOutputFactory.newInstance().createXMLStreamWriter(sw2) + def gmlEncoderGml21 = new GeometryEncoderGml(xmlWriter2, GmlVersion.GML21, Set.of(), Optional.of("gml"), Optional.empty(), List.of(1,1)) geometry.accept(gmlEncoderGml21) - String gmlOut21 = sb.toString() + xmlWriter2.flush() + String gmlOut21 = sw2.toString() then: gmlOut == "10.0 10.020.0 20.0 30.0 30.010.0 10.020.0 20.0" @@ -272,9 +350,12 @@ class GeometrySpec extends Specification { )) when: - sb.setLength(0) + def sw = new StringWriter() + def xmlWriter = XMLOutputFactory.newInstance().createXMLStreamWriter(sw) + def gmlEncoderWithout = new GeometryEncoderGml(xmlWriter, GmlVersion.GML32, Set.of(), Optional.of("gml"), Optional.empty(), List.of()) geometry.accept(gmlEncoderWithout) - String gmlOut = sb.toString() + xmlWriter.flush() + String gmlOut = sw.toString() then: gmlOut == "0.0 0.0 0.0 1.0 1.0 1.0 0.0 0.00.0 0.0 1.0 1.0 1.0 0.0 0.0 0.0" @@ -285,9 +366,12 @@ class GeometrySpec extends Specification { Geometry geometry = CircularString.of(PositionList.of(Axes.XY, new double[]{0.0, 0.0, 1.0, 1.0, 2.0, 0.0})) when: - sb.setLength(0) + def sw = new StringWriter() + def xmlWriter = XMLOutputFactory.newInstance().createXMLStreamWriter(sw) + def gmlEncoderWithout = new GeometryEncoderGml(xmlWriter, GmlVersion.GML32, Set.of(), Optional.of("gml"), Optional.empty(), List.of()) geometry.accept(gmlEncoderWithout) - String gmlOut = sb.toString() + xmlWriter.flush() + String gmlOut = sw.toString() then: gmlOut == "0.0 0.0 1.0 1.0 2.0 0.0" @@ -301,9 +385,12 @@ class GeometrySpec extends Specification { )) when: - sb.setLength(0) + def sw = new StringWriter() + def xmlWriter = XMLOutputFactory.newInstance().createXMLStreamWriter(sw) + def gmlEncoderWithout = new GeometryEncoderGml(xmlWriter, GmlVersion.GML32, Set.of(), Optional.of("gml"), Optional.empty(), List.of()) geometry.accept(gmlEncoderWithout) - String gmlOut = sb.toString() + xmlWriter.flush() + String gmlOut = sw.toString() then: gmlOut == "0.0 0.0 1.0 1.01.0 1.0 2.0 0.0" @@ -316,14 +403,110 @@ class GeometrySpec extends Specification { )) when: - sb.setLength(0) + def sw = new StringWriter() + def xmlWriter = XMLOutputFactory.newInstance().createXMLStreamWriter(sw) + def gmlEncoderWithout = new GeometryEncoderGml(xmlWriter, GmlVersion.GML32, Set.of(), Optional.of("gml"), Optional.empty(), List.of()) geometry.accept(gmlEncoderWithout) - String gmlOut = sb.toString() + xmlWriter.flush() + String gmlOut = sw.toString() then: gmlOut == "0.0 0.0 1.0 1.0 0.0 2.0 -1.0 -1.0 0.0 0.0" } + def 'LINESTRING XY with USE_SURFACE_RING_CURVE'() { + given: + Geometry geometry = LineString.of(new double[]{0.0, 0.0, 1.0, 1.0, 2.0, 0.0}) + + when: + def sw = new StringWriter() + def xmlWriter = XMLOutputFactory.newInstance().createXMLStreamWriter(sw) + def gmlEncoder = new GeometryEncoderGml(xmlWriter, GmlVersion.GML32, Set.of(GeometryEncoderGml.Options.USE_SURFACE_RING_CURVE), Optional.of("gml"), Optional.empty(), List.of()) + geometry.accept(gmlEncoder) + xmlWriter.flush() + String gmlOut = sw.toString() + + then: + gmlOut == "0.0 0.0 1.0 1.0 2.0 0.0" + } + + def 'POLYGON XY with USE_SURFACE_RING_CURVE'() { + given: + Geometry geometry = Polygon.of(List.of( + PositionList.of(Axes.XY, new double[]{0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 0.0, 0.0}) + )) + + when: + def sw = new StringWriter() + def xmlWriter = XMLOutputFactory.newInstance().createXMLStreamWriter(sw) + def gmlEncoder = new GeometryEncoderGml(xmlWriter, GmlVersion.GML32, Set.of(GeometryEncoderGml.Options.USE_SURFACE_RING_CURVE), Optional.of("gml"), Optional.empty(), List.of()) + geometry.accept(gmlEncoder) + xmlWriter.flush() + String gmlOut = sw.toString() + + then: + gmlOut == "0.0 0.0 0.0 1.0 1.0 1.0 0.0 0.0" + } + + def 'CURVEPOLYGON XY with USE_SURFACE_RING_CURVE'() { + given: + Geometry geometry = CurvePolygon.of(List.of( + CircularString.of(PositionList.of(Axes.XY, new double[]{0.0, 0.0, 1.0, 1.0, 0.0, 2.0, -1.0, -1.0, 0.0, 0.0})) + )) + + when: + def sw = new StringWriter() + def xmlWriter = XMLOutputFactory.newInstance().createXMLStreamWriter(sw) + def gmlEncoder = new GeometryEncoderGml(xmlWriter, GmlVersion.GML32, Set.of(GeometryEncoderGml.Options.USE_SURFACE_RING_CURVE), Optional.of("gml"), Optional.empty(), List.of()) + geometry.accept(gmlEncoder) + xmlWriter.flush() + String gmlOut = sw.toString() + + then: + gmlOut == "0.0 0.0 1.0 1.0 0.0 2.0 -1.0 -1.0 0.0 0.0" + } + + def 'CURVEPOLYGON with CompoundCurve ring XY with USE_SURFACE_RING_CURVE'() { + given: + Geometry geometry = CurvePolygon.of(List.of( + CompoundCurve.of(List.of( + LineString.of(new double[]{0.0, 0.0, 1.0, 1.0}), + CircularString.of(PositionList.of(Axes.XY, new double[]{1.0, 1.0, 2.0, 0.0, 3.0, 1.0})), + LineString.of(new double[]{3.0, 1.0, 0.0, 0.0}) + )) + )) + + when: + def sw = new StringWriter() + def xmlWriter = XMLOutputFactory.newInstance().createXMLStreamWriter(sw) + def gmlEncoder = new GeometryEncoderGml(xmlWriter, GmlVersion.GML32, Set.of(GeometryEncoderGml.Options.USE_SURFACE_RING_CURVE), Optional.of("gml"), Optional.empty(), List.of()) + geometry.accept(gmlEncoder) + xmlWriter.flush() + String gmlOut = sw.toString() + + then: + gmlOut == "0.0 0.0 1.0 1.01.0 1.0 2.0 0.0 3.0 1.03.0 1.0 0.0 0.0" + } + + def 'COMPOUNDCURVE XY with USE_SURFACE_RING_CURVE'() { + given: + Geometry geometry = CompoundCurve.of(List.of( + LineString.of(new double[]{0.0, 0.0, 1.0, 1.0}), + CircularString.of(PositionList.of(Axes.XY, new double[]{1.0, 1.0, 2.0, 0.0, 3.0, 1.0})) + )) + + when: + def sw = new StringWriter() + def xmlWriter = XMLOutputFactory.newInstance().createXMLStreamWriter(sw) + def gmlEncoder = new GeometryEncoderGml(xmlWriter, GmlVersion.GML32, Set.of(GeometryEncoderGml.Options.USE_SURFACE_RING_CURVE), Optional.of("gml"), Optional.empty(), List.of()) + geometry.accept(gmlEncoder) + xmlWriter.flush() + String gmlOut = sw.toString() + + then: + gmlOut == "0.0 0.0 1.0 1.01.0 1.0 2.0 0.0 3.0 1.0" + } + def 'MULTICURVE XY'() { given: Geometry geometry = MultiCurve.of(List.of( @@ -332,9 +515,12 @@ class GeometrySpec extends Specification { )) when: - sb.setLength(0) + def sw = new StringWriter() + def xmlWriter = XMLOutputFactory.newInstance().createXMLStreamWriter(sw) + def gmlEncoderWithout = new GeometryEncoderGml(xmlWriter, GmlVersion.GML32, Set.of(), Optional.of("gml"), Optional.empty(), List.of()) geometry.accept(gmlEncoderWithout) - String gmlOut = sb.toString() + xmlWriter.flush() + String gmlOut = sw.toString() then: gmlOut == "0.0 0.0 1.0 1.01.0 1.0 2.0 0.0 3.0 1.0" @@ -348,9 +534,12 @@ class GeometrySpec extends Specification { )) when: - sb.setLength(0) + def sw = new StringWriter() + def xmlWriter = XMLOutputFactory.newInstance().createXMLStreamWriter(sw) + def gmlEncoderWithout = new GeometryEncoderGml(xmlWriter, GmlVersion.GML32, Set.of(), Optional.of("gml"), Optional.empty(), List.of()) geometry.accept(gmlEncoderWithout) - String gmlOut = sb.toString() + xmlWriter.flush() + String gmlOut = sw.toString() then: gmlOut == "0.0 0.0 0.0 1.0 1.0 1.0 0.0 0.01.0 1.0 2.0 2.0 3.0 2.0 2.0 1.0 1.0 1.0" @@ -371,9 +560,12 @@ class GeometrySpec extends Specification { ), true) when: - sb.setLength(0) + def sw = new StringWriter() + def xmlWriter = XMLOutputFactory.newInstance().createXMLStreamWriter(sw) + def gmlEncoderWithout = new GeometryEncoderGml(xmlWriter, GmlVersion.GML32, Set.of(), Optional.of("gml"), Optional.empty(), List.of()) geometry.accept(gmlEncoderWithout) - String gmlOut = sb.toString() + xmlWriter.flush() + String gmlOut = sw.toString() then: gmlOut == "280414.631 5660090.756 40.255 280414.631 5660088.454 40.255 280414.631 5660088.454 32.967 280414.631 5660090.756 32.967 280414.631 5660090.756 40.255280414.631 5660088.454 40.255 280405.623 5660088.454 33.256 280405.623 5660088.454 32.967 280414.631 5660088.454 32.967 280414.631 5660088.454 40.255280405.623 5660088.454 33.256 280405.623 5660090.756 33.256 280405.623 5660090.756 32.967 280405.623 5660088.454 32.967 280405.623 5660088.454 33.256280405.623 5660090.756 33.256 280414.631 5660090.756 40.255 280414.631 5660090.756 32.967 280405.623 5660090.756 32.967 280405.623 5660090.756 33.256280405.623 5660088.454 33.256 280414.631 5660088.454 40.255 280411.722 5660088.454 41.63 280405.623 5660088.454 33.256280414.631 5660090.756 40.255 280405.623 5660090.756 33.256 280411.722 5660090.756 41.63 280414.631 5660090.756 40.255280414.631 5660088.454 40.255 280414.631 5660090.756 40.255 280411.722 5660090.756 41.63 280411.722 5660088.454 41.63 280414.631 5660088.454 40.255280405.623 5660090.756 33.256 280405.623 5660088.454 33.256 280411.722 5660088.454 41.63 280411.722 5660090.756 41.63 280405.623 5660090.756 33.256280414.631 5660090.756 32.967 280414.631 5660088.454 32.967 280405.623 5660088.454 32.967 280405.623 5660090.756 32.967 280414.631 5660090.756 32.967" diff --git a/xtraplatform-features-gml/src/test/groovy/de/ii/xtraplatform/features/gml/domain/GeometrySrsNameMapperSpec.groovy b/xtraplatform-features-gml/src/test/groovy/de/ii/xtraplatform/features/gml/domain/GeometrySrsNameMapperSpec.groovy new file mode 100644 index 000000000..ceee40a85 --- /dev/null +++ b/xtraplatform-features-gml/src/test/groovy/de/ii/xtraplatform/features/gml/domain/GeometrySrsNameMapperSpec.groovy @@ -0,0 +1,89 @@ +/* + * Copyright 2026 interactive instruments GmbH + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ +package de.ii.xtraplatform.features.gml.domain + +import de.ii.xtraplatform.crs.domain.EpsgCrs +import de.ii.xtraplatform.geometries.domain.Geometry +import de.ii.xtraplatform.geometries.domain.Point +import spock.lang.Specification + +import javax.xml.stream.XMLOutputFactory + +class GeometrySrsNameMapperSpec extends Specification { + + def 'TEMPLATE mapper rewrites srsName for matching CRS'() { + given: + EpsgCrs etrs89Utm32 = EpsgCrs.of(25832) + Geometry geometry = Point.of(389000.0, 5705000.0).withCrs(etrs89Utm32) + def sw = new StringWriter() + def xmlWriter = XMLOutputFactory.newInstance().createXMLStreamWriter(sw) + def encoder = new GeometryEncoderGml( + xmlWriter, + GmlVersion.GML32, + Set.of(GeometryEncoderGml.Options.WITH_SRS_NAME), + Optional.of("gml"), + Optional.empty(), + List.of(), + { EpsgCrs crs -> etrs89Utm32 == crs ? 'urn:adv:crs:ETRS89_UTM32' : crs.toUriString() } as java.util.function.Function) + + when: + geometry.accept(encoder) + xmlWriter.flush() + String gmlOut = sw.toString() + + then: + gmlOut.contains('srsName="urn:adv:crs:ETRS89_UTM32"') + } + + def 'TEMPLATE mapper falls back to OGC URI for unmapped CRS'() { + given: + EpsgCrs etrs89Utm32 = EpsgCrs.of(25832) + EpsgCrs wgs84Utm32n = EpsgCrs.of(32632) + Geometry geometry = Point.of(389000.0, 5705000.0).withCrs(wgs84Utm32n) + def sw = new StringWriter() + def xmlWriter = XMLOutputFactory.newInstance().createXMLStreamWriter(sw) + def encoder = new GeometryEncoderGml( + xmlWriter, + GmlVersion.GML32, + Set.of(GeometryEncoderGml.Options.WITH_SRS_NAME), + Optional.of("gml"), + Optional.empty(), + List.of(), + { EpsgCrs crs -> etrs89Utm32 == crs ? 'urn:adv:crs:ETRS89_UTM32' : crs.toUriString() } as java.util.function.Function) + + when: + geometry.accept(encoder) + xmlWriter.flush() + String gmlOut = sw.toString() + + then: + gmlOut.contains('srsName="http://www.opengis.net/def/crs/EPSG/0/32632"') + } + + def 'OGC default constructor preserves toUriString behavior'() { + given: + Geometry geometry = Point.of(389000.0, 5705000.0).withCrs(EpsgCrs.of(25832)) + def sw = new StringWriter() + def xmlWriter = XMLOutputFactory.newInstance().createXMLStreamWriter(sw) + def encoder = new GeometryEncoderGml( + xmlWriter, + GmlVersion.GML32, + Set.of(GeometryEncoderGml.Options.WITH_SRS_NAME), + Optional.of("gml"), + Optional.empty(), + List.of()) + + when: + geometry.accept(encoder) + xmlWriter.flush() + String gmlOut = sw.toString() + + then: + gmlOut.contains('srsName="http://www.opengis.net/def/crs/EPSG/0/25832"') + } +} diff --git a/xtraplatform-features/src/main/java/de/ii/xtraplatform/features/domain/FeatureSchema.java b/xtraplatform-features/src/main/java/de/ii/xtraplatform/features/domain/FeatureSchema.java index eb28bab64..93cba3fe9 100644 --- a/xtraplatform-features/src/main/java/de/ii/xtraplatform/features/domain/FeatureSchema.java +++ b/xtraplatform-features/src/main/java/de/ii/xtraplatform/features/domain/FeatureSchema.java @@ -53,6 +53,7 @@ "geometryType", "objectType", "label", + "alias", "description", "unit", "format", @@ -214,6 +215,23 @@ default Type getType() { */ Optional getLabel(); + /** + * @langEn An alternative property name used by feature encodings that opt in to alias mode (for + * example, GML with `useAlias: true`). Unlike `label` (which is free-text for display), the + * alias must satisfy the encoding's identifier constraints (e.g. an XML element name or a + * JSON property name). When alias mode is active and an alias is set, the encoded property + * name is the alias instead of the schema name; an explicit `rename` transformation still + * takes precedence over the alias. + * @langDe Ein alternativer Eigenschaftsname, der von Feature-Kodierungen verwendet wird, die den + * Alias-Modus aktiviert haben (z.B. GML mit `useAlias: true`). Anders als `label` (ein freier + * Anzeigetext) muss der Alias den Identifier-Regeln der Kodierung entsprechen (z.B. + * XML-Elementname oder JSON-Eigenschaftsname). Bei aktivem Alias-Modus und gesetztem Alias + * wird der Alias anstelle des Schemanamens als Eigenschaftsname kodiert; eine explizite + * `rename`-Transformation hat weiterhin Vorrang vor dem Alias. + * @default null + */ + Optional getAlias(); + /** * @langEn Description for the schema object, used for example in HTML representations or JSON * Schema. diff --git a/xtraplatform-features/src/main/java/de/ii/xtraplatform/features/domain/FeatureSchemaAliases.java b/xtraplatform-features/src/main/java/de/ii/xtraplatform/features/domain/FeatureSchemaAliases.java new file mode 100644 index 000000000..dbd35535e --- /dev/null +++ b/xtraplatform-features/src/main/java/de/ii/xtraplatform/features/domain/FeatureSchemaAliases.java @@ -0,0 +1,43 @@ +/* + * Copyright 2026 interactive instruments GmbH + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ +package de.ii.xtraplatform.features.domain; + +import de.ii.xtraplatform.features.domain.transform.ImmutablePropertyTransformation; +import de.ii.xtraplatform.features.domain.transform.PropertyTransformation; +import de.ii.xtraplatform.features.domain.transform.PropertyTransformations; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public final class FeatureSchemaAliases { + + private FeatureSchemaAliases() {} + + public static PropertyTransformations injectAliasRenames( + PropertyTransformations propertyTransformations, FeatureSchema schema) { + Map> aliasRenames = new LinkedHashMap<>(); + collectAliasRenames(schema, aliasRenames); + if (aliasRenames.isEmpty()) { + return propertyTransformations; + } + return propertyTransformations.mergeInto(() -> aliasRenames); + } + + private static void collectAliasRenames( + FeatureSchema schema, Map> aliasRenames) { + schema + .getAlias() + .filter(alias -> !schema.getFullPath().isEmpty()) + .ifPresent( + alias -> + aliasRenames.put( + schema.getFullPathAsString(), + List.of(new ImmutablePropertyTransformation.Builder().rename(alias).build()))); + schema.getProperties().forEach(child -> collectAliasRenames(child, aliasRenames)); + } +} diff --git a/xtraplatform-features/src/main/java/de/ii/xtraplatform/features/domain/FeatureStreamImpl.java b/xtraplatform-features/src/main/java/de/ii/xtraplatform/features/domain/FeatureStreamImpl.java index 65400a1a9..8464b52ff 100644 --- a/xtraplatform-features/src/main/java/de/ii/xtraplatform/features/domain/FeatureStreamImpl.java +++ b/xtraplatform-features/src/main/java/de/ii/xtraplatform/features/domain/FeatureStreamImpl.java @@ -25,7 +25,6 @@ import de.ii.xtraplatform.streams.domain.Reactive.Stream; import java.time.ZoneId; import java.util.AbstractMap.SimpleImmutableEntry; -import java.util.Collection; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -334,43 +333,54 @@ static PropertyTransformations getPropertyTransformations( private static PropertyTransformations applyRename( PropertyTransformations propertyTransformations) { - if (propertyTransformations.getTransformations().values().stream() - .flatMap(Collection::stream) - .anyMatch(propertyTransformation -> propertyTransformation.getRename().isPresent())) { - Map> renamed = new LinkedHashMap<>(); - - propertyTransformations - .getTransformations() - .forEach( - (key, value) -> { - Optional rename = - value.stream() - .filter( - propertyTransformation -> - propertyTransformation.getRename().isPresent()) - .map(propertyTransformation -> propertyTransformation.getRename().get()) - .findFirst(); - - if (rename.isPresent()) { - renamed.put(rename.get(), value); - - String prefix = key + "."; - - propertyTransformations - .getTransformations() - .forEach( - (key2, value2) -> { - if (key2.startsWith(prefix)) { - renamed.put(key2.replace(key, rename.get()), value2); - } - }); - } - }); - - return propertyTransformations.mergeInto(() -> renamed); + // Collect every rename keyed by its original full path. + Map renames = new LinkedHashMap<>(); + propertyTransformations + .getTransformations() + .forEach( + (key, value) -> + value.stream() + .filter(pt -> pt.getRename().isPresent()) + .map(pt -> pt.getRename().get()) + .findFirst() + .ifPresent(rename -> renames.put(key, rename))); + + if (renames.isEmpty()) { + return propertyTransformations; } - return propertyTransformations; + // Re-key every transformation by the cumulative renamed full path so that lookups + // by the renamed target path (e.g. "qualitaetsangaben.herkunft.gmd:processStep.gmd:dateTime") + // still find the right transformation (auto DATETIME formatter, value transformers, + // wrap transformers, ...). + Map> renamed = new LinkedHashMap<>(); + propertyTransformations + .getTransformations() + .forEach( + (key, value) -> { + String newKey = renameFullPath(key, renames); + renamed.put(newKey, value); + }); + + return propertyTransformations.mergeInto(() -> renamed); + } + + private static String renameFullPath(String path, Map renames) { + String[] segments = path.split("\\."); + StringBuilder result = new StringBuilder(); + StringBuilder running = new StringBuilder(); + for (int i = 0; i < segments.length; i++) { + if (i > 0) { + running.append("."); + } + running.append(segments[i]); + String renamedSegment = renames.getOrDefault(running.toString(), segments[i]); + if (i > 0) { + result.append("."); + } + result.append(renamedSegment); + } + return result.toString(); } private static Map> getProviderTransformations( diff --git a/xtraplatform-features/src/test/groovy/de/ii/xtraplatform/features/domain/FeatureSchemaAliasesSpec.groovy b/xtraplatform-features/src/test/groovy/de/ii/xtraplatform/features/domain/FeatureSchemaAliasesSpec.groovy new file mode 100644 index 000000000..f79ed4d0d --- /dev/null +++ b/xtraplatform-features/src/test/groovy/de/ii/xtraplatform/features/domain/FeatureSchemaAliasesSpec.groovy @@ -0,0 +1,127 @@ +/* + * Copyright 2026 interactive instruments GmbH + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ +package de.ii.xtraplatform.features.domain + +import de.ii.xtraplatform.features.domain.transform.PropertyTransformations +import spock.lang.Specification + +class FeatureSchemaAliasesSpec extends Specification { + + static FeatureSchema property(String name, String alias = null) { + def b = new ImmutableFeatureSchema.Builder() + .name(name) + .type(SchemaBase.Type.STRING) + .sourcePath(name) + if (alias != null) { + b.alias(alias) + } + return b.build() + } + + static FeatureSchema object(String name, String alias, FeatureSchema... children) { + def b = new ImmutableFeatureSchema.Builder() + .name(name) + .type(SchemaBase.Type.OBJECT) + if (alias != null) { + b.alias(alias) + } + children.each { b.putPropertyMap(it.getName(), it) } + return b.build() + } + + static FeatureSchema feature(FeatureSchema... properties) { + def b = new ImmutableFeatureSchema.Builder() + .name("test") + .type(SchemaBase.Type.OBJECT) + .sourcePath("/test") + properties.each { b.putPropertyMap(it.getName(), it) } + return b.build() + } + + static PropertyTransformations base(Map transformations = [:]) { + return { -> transformations as Map } as PropertyTransformations + } + + static String renameAt(PropertyTransformations pt, String path) { + def entries = pt.transformations.get(path) + return entries == null ? null : entries.find { it.rename.present }?.rename?.orElse(null) + } + + def "schema with no aliases: transformations map unchanged"() { + given: + def schema = feature(property("anl")) + def input = base() + + when: + def result = FeatureSchemaAliases.injectAliasRenames(input, schema) + + then: + result.is(input) + } + + def "property alias: rename entry is added at the property's full path"() { + given: + def schema = feature(property("anl", "anlass")) + def input = base() + + when: + def result = FeatureSchemaAliases.injectAliasRenames(input, schema) + + then: + renameAt(result, "anl") == "anlass" + } + + def "nested aliases: rename entries are added at each level's full path"() { + given: + def schema = feature( + object("qag", "qualitaetsangaben", + object("dpl", "herkunft", + property("prs", "gmd:processStep")))) + def input = base() + + when: + def result = FeatureSchemaAliases.injectAliasRenames(input, schema) + + then: + renameAt(result, "qag") == "qualitaetsangaben" + renameAt(result, "qag.dpl") == "herkunft" + renameAt(result, "qag.dpl.prs") == "gmd:processStep" + } + + def "existing transformations are preserved alongside injected aliases"() { + given: + def schema = feature(property("anl", "anlass")) + def input = base(["other.path": []]) + + when: + def result = FeatureSchemaAliases.injectAliasRenames(input, schema) + + then: + result.transformations.containsKey("other.path") + renameAt(result, "anl") == "anlass" + } + + def "feature type's own alias is not injected (only properties)"() { + given: + def schema = new ImmutableFeatureSchema.Builder() + .name("test") + .type(SchemaBase.Type.OBJECT) + .sourcePath("/test") + .alias("ignored") + .putPropertyMap("anl", property("anl", "anlass")) + .build() + def input = base() + + when: + def result = FeatureSchemaAliases.injectAliasRenames(input, schema) + + then: + !result.transformations.containsKey("") + renameAt(result, "anl") == "anlass" + } +}