This is an automated email from the ASF dual-hosted git repository. asf-gitbox-commits pushed a commit to branch geoapi-4.0 in repository https://gitbox.apache.org/repos/asf/sis.git
commit 57b466cbb6c6dc5cb4c4be2f2996fa9673a0dc07 Author: jsorel <[email protected]> AuthorDate: Fri Sep 11 11:11:42 2026 +0200 feat(Geometry): add WKT encoding --- .../org.apache.sis.geometry/main/module-info.java | 1 + .../org/apache/sis/geometries/AttributesType.java | 10 + .../main/org/apache/sis/geometries/Geometry.java | 8 +- .../org/apache/sis/geometries/GeometryFactory.java | 2 +- .../main/org/apache/sis/geometries/Point.java | 10 - .../sis/geometries/adapter/WellKnownBinary.java | 26 + .../sis/geometries/adapter/WellKnownText.java | 512 ++++++++++++++ .../geometries/adapter/WellKnownTextParser.java | 786 +++++++++++++++++++++ .../org/apache/sis/geometries/curve/Geodesic.java | 10 - .../apache/sis/geometries/curve/LineString.java | 9 - .../apache/sis/geometries/curve/LinearRing.java | 15 - .../sis/geometries/curve/MultiLineString.java | 21 - .../org/apache/sis/geometries/curve/NurbCurve.java | 10 - .../org/apache/sis/geometries/curve/Rhumb.java | 10 - .../internal/shared/AbstractGeometry.java | 36 - .../internal/shared/DefaultArcByBulge.java | 9 - .../internal/shared/DefaultArcByCenterPoint.java | 11 - .../internal/shared/DefaultCircularString.java | 7 - .../internal/shared/DefaultCompoundCurve.java | 10 - .../internal/shared/DefaultCurvePolygon.java | 8 - .../internal/shared/DefaultPolyhedralSurface.java | 9 - .../internal/shared/DefaultPolyhedron.java | 14 - .../internal/shared/DefaultReversedCurve.java | 5 - .../internal/shared/DefaultReversedSurface.java | 4 - .../internal/shared/DefaultTriangle.java | 5 - .../shared/DefaultTriangulatedSurface.java | 8 - .../operation/triangulate/delaunay/Delaunay.java | 1 - .../apache/sis/geometries/point/MultiPoint.java | 14 - .../sis/geometries/surface/CurvePolygon.java | 16 - .../sis/geometries/surface/MultiPolygon.java | 21 - .../org/apache/sis/geometries/surface/Polygon.java | 16 - .../org/apache/sis/geometries/surface/TIN.java | 44 +- .../apache/sis/geometries/surface/Triangle.java | 26 +- .../org/apache/sis/geometries/PreparedTINTest.java | 8 +- .../sis/geometries/adapter/WellKnownTextTest.java | 299 ++++++++ .../sis/geometries/mesh/PrimitiveVisitorTest.java | 140 ++-- .../org/apache/sis/referencing/dggs/s2/S2Test.java | 2 +- 37 files changed, 1723 insertions(+), 420 deletions(-) diff --git a/incubator/src/org.apache.sis.geometry/main/module-info.java b/incubator/src/org.apache.sis.geometry/main/module-info.java index db680f0f4c..aea12d70e0 100644 --- a/incubator/src/org.apache.sis.geometry/main/module-info.java +++ b/incubator/src/org.apache.sis.geometry/main/module-info.java @@ -31,6 +31,7 @@ module org.apache.sis.geometry { exports org.apache.sis.images; exports org.apache.sis.geometries; + exports org.apache.sis.geometries.adapter; exports org.apache.sis.geometries.curve; exports org.apache.sis.geometries.operation; exports org.apache.sis.geometries.point; diff --git a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/AttributesType.java b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/AttributesType.java index c5939bc4b9..29361b6272 100644 --- a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/AttributesType.java +++ b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/AttributesType.java @@ -38,6 +38,16 @@ import org.apache.sis.maths.SampleSystem; public interface AttributesType { static final String ATT_POSITION = "POSITION"; + + /** + * The single ordinate that common GIS formats call <cite>M</cite>, the measure. + * It is a one dimensional attribute, kept apart from {@link #ATT_POSITION} because + * it is not a spatial ordinate: it takes part in no distance, area or transform. + * This is the attribute that the {@code M} and {@code ZM} flavors of Well-Known Text + * are read into and written from. + */ + static final String ATT_M = "M"; + static final String ATT_NORMAL = "NORMAL"; static final String ATT_TANGENT = "TANGENT"; static final String ATT_TEXCOORD_0 = "TEXCOORD_0"; diff --git a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/Geometry.java b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/Geometry.java index f5f19cf5a6..bbcf9ae742 100644 --- a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/Geometry.java +++ b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/Geometry.java @@ -19,6 +19,7 @@ package org.apache.sis.geometries; import java.util.List; import java.util.Map; import javax.measure.quantity.Length; +import org.apache.sis.geometries.adapter.WellKnownText; import org.apache.sis.geometries.internal.shared.AbstractGeometry; import org.apache.sis.geometries.mesh.MeshPrimitive; import org.apache.sis.geometries.operation.GeometryProcessor; @@ -385,15 +386,18 @@ public sealed interface Geometry * - this method is located on Encoding sub interface in the standard, it is placed * on Geometry to match OGC SFA.</p> * + * <p>The encoding is delegated to {@link WellKnownText}, which documents the supported + * geometry types and the deviations from the format.</p> + * * @return this geometry in Well-known Text + * @throws IllegalArgumentException if this geometry has no Well-known Text representation. * * @see OGC Simple Feature Access 1.2.1 - 6.1.2.2 * @see ISO 19107:2019 - 6.4.4.5, 6.4.7 */ @UML(identifier="asText", specification=ISO_19107) default String asText() { - //TODO remove this method default when all classes implement it. - return this.getClass().getSimpleName(); + return new WellKnownText().encode(this); } /** diff --git a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/GeometryFactory.java b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/GeometryFactory.java index 3423f52207..11b8342531 100644 --- a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/GeometryFactory.java +++ b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/GeometryFactory.java @@ -414,7 +414,7 @@ public final class GeometryFactory extends org.apache.sis.geometry.wrapper.Geome } else { marray = NDArrays.of(SampleSystem.ofSize(1), coordinates.get(dimensions.hasZ ? 3 : 2)); } - points.setAttribute("m", marray); + points.setAttribute(AttributesType.ATT_M, marray); } return new DefaultPoint(points); diff --git a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/Point.java b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/Point.java index 4026b590c5..444fb9c3af 100644 --- a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/Point.java +++ b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/Point.java @@ -17,7 +17,6 @@ package org.apache.sis.geometries; import java.util.List; -import org.apache.sis.geometries.internal.shared.AbstractGeometry; import org.apache.sis.geometries.internal.shared.ArrayDataPoints; import org.apache.sis.geometries.internal.shared.DefaultPoint; import org.apache.sis.geometries.internal.shared.IndexedPoint; @@ -233,15 +232,6 @@ public sealed interface Point extends Primitive return env; } - @Override - default String asText() { - final Tuple crd = getPosition(); - final StringBuilder sb = new StringBuilder("POINT ("); - AbstractGeometry.toText(sb, crd); - sb.append(')'); - return sb.toString(); - } - /** * Returns the vector, in the tangent space at this point, whose direction determines the * geodesic curve reaching the given position and whose length is the distance to it. diff --git a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/adapter/WellKnownBinary.java b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/adapter/WellKnownBinary.java new file mode 100644 index 0000000000..50df291e6f --- /dev/null +++ b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/adapter/WellKnownBinary.java @@ -0,0 +1,26 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.sis.geometries.adapter; + +/** + * TODO + * + * @author Johann Sorel (Geomatys) + */ +public final class WellKnownBinary { + +} diff --git a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/adapter/WellKnownText.java b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/adapter/WellKnownText.java new file mode 100644 index 0000000000..0fe8fde7ca --- /dev/null +++ b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/adapter/WellKnownText.java @@ -0,0 +1,512 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.sis.geometries.adapter; + +import java.math.RoundingMode; +import java.text.NumberFormat; +import java.util.Locale; +import java.util.function.IntFunction; +import org.apache.sis.geometries.AttributesType; +import org.apache.sis.geometries.Curve; +import org.apache.sis.geometries.DataPoints; +import org.apache.sis.geometries.Empty; +import org.apache.sis.geometries.Geometry; +import org.apache.sis.geometries.GeometryCollection; +import org.apache.sis.geometries.Orientable; +import org.apache.sis.geometries.Point; +import org.apache.sis.geometries.curve.CircularString; +import org.apache.sis.geometries.curve.CompoundCurve; +import org.apache.sis.geometries.curve.LineString; +import org.apache.sis.geometries.curve.MultiCurve; +import org.apache.sis.geometries.curve.MultiLineString; +import org.apache.sis.geometries.point.MultiPoint; +import org.apache.sis.geometries.surface.CurvePolygon; +import org.apache.sis.geometries.surface.MultiPolygon; +import org.apache.sis.geometries.surface.MultiSurface; +import org.apache.sis.geometries.surface.Polygon; +import org.apache.sis.geometries.surface.PolyhedralSurface; +import org.apache.sis.geometries.surface.TIN; +import org.apache.sis.geometries.surface.Triangle; +import org.apache.sis.maths.Tuple; +import org.apache.sis.util.ArgumentChecks; +import org.apache.sis.util.StringBuilders; +import org.opengis.referencing.crs.CoordinateReferenceSystem; + + +/** + * Encoder and decoder for the Well-Known Text representation of geometries. + * + * <p>The grammar is the one of <cite>OGC Simple Feature Access 1.2.1</cite> extended with the + * curved and surface-patch types of <cite>ISO 13249-3</cite> (SQL/MM Part 3):</p> + * + * <blockquote><pre> + * <keyword> [Z | M | ZM] ( … ) + * <keyword> [Z | M | ZM] EMPTY + * </pre></blockquote> + * + * <p>The supported keywords, and the geometry type each one maps to, are:</p> + * + * <table class="sis"> + * <caption>Supported Well-Known Text keywords</caption> + * <tr><th>Keyword</th> <th>Geometry type</th></tr> + * <tr><td>{@code POINT}</td> <td>{@link Point}</td></tr> + * <tr><td>{@code LINESTRING}</td> <td>{@link LineString}</td></tr> + * <tr><td>{@code CIRCULARSTRING}</td> <td>{@link CircularString}</td></tr> + * <tr><td>{@code COMPOUNDCURVE}</td> <td>{@link CompoundCurve}</td></tr> + * <tr><td>{@code POLYGON}</td> <td>{@link Polygon}</td></tr> + * <tr><td>{@code TRIANGLE}</td> <td>{@link Triangle}</td></tr> + * <tr><td>{@code CURVEPOLYGON}</td> <td>{@link CurvePolygon}</td></tr> + * <tr><td>{@code POLYHEDRALSURFACE}</td> <td>{@link PolyhedralSurface}</td></tr> + * <tr><td>{@code TIN}</td> <td>{@link TIN}</td></tr> + * <tr><td>{@code MULTIPOINT}</td> <td>{@link MultiPoint}</td></tr> + * <tr><td>{@code MULTILINESTRING}</td> <td>{@link MultiLineString}</td></tr> + * <tr><td>{@code MULTICURVE}</td> <td>{@link MultiCurve}</td></tr> + * <tr><td>{@code MULTIPOLYGON}</td> <td>{@link MultiPolygon}</td></tr> + * <tr><td>{@code MULTISURFACE}</td> <td>{@link MultiSurface}</td></tr> + * <tr><td>{@code GEOMETRYCOLLECTION}</td> <td>{@link GeometryCollection}</td></tr> + * </table> + * + * <p>Any other geometry of the Apache SIS hierarchy + * is rejected by {@link #encode encode(…)} with an {@link IllegalArgumentException}.</p> + * + * <h2>Dimensions and measures</h2> + * The {@code Z} flag is the third ordinate of the {@linkplain AttributesType#ATT_POSITION position} + * attribute, and the {@code M} flag is the separate {@linkplain AttributesType#ATT_M measure} + * attribute. A geometry whose position has neither 2 nor 3 dimensions cannot be written. + * When the text carries no flag, the number of ordinates of the first coordinate tuple decides: + * two of them are read as <var>x y</var> and three as <var>x y z</var>. Four ordinates without a + * flag are ambiguous — {@code ZM} and a 4-dimensional position would be written the same way — + * and are therefore rejected. + * + * <h2>Deviations</h2> + * <ul> + * <li>A {@link org.apache.sis.geometries.curve.LinearRing} is written as a {@code LINESTRING}: + * it is a {@code LineString} and Well-Known Text has no standalone ring type. Consequently + * {@code decode(…)} never returns a {@code LinearRing} at the top level.</li> + * <li>An {@linkplain Orientable#getOrientationSign() orientation} of + * {@link Orientable.Sign#NEGATIVE} is written as the underlying + * {@linkplain Orientable#getReverse() reverse} primitive. Well-Known Text has no notion + * of orientation, so that information is lost.</li> + * <li>An {@link Empty} geometry is written as {@code GEOMETRYCOLLECTION EMPTY}, the only + * type-less empty form the grammar offers.</li> + * <li>{@code POINT EMPTY} is read as an {@link Empty} geometry rather than as a {@code Point}, + * because the model has no empty point: a point holds exactly one position. It is therefore + * written back as {@code GEOMETRYCOLLECTION EMPTY}, and an empty point may not appear as a + * member of a {@code MULTIPOINT}. Every other type has a genuine empty form which + * round-trips unchanged.</li> + * <li>The coordinate reference system is neither written nor read: the {@code SRID=…;} prefix + * of the extended Well-Known Text of some databases is not part of the standard. Unless a + * system is given to {@link #decode(String, CoordinateReferenceSystem)}, decoded geometries + * use {@link org.apache.sis.geometries.Geometries#getUndefinedCRS(int)}.</li> + * </ul> + * + * <h2>Thread safety</h2> + * Instances are cheap to create but are <strong>not</strong> thread-safe, because the number + * format they may hold is not. Use one instance per thread. + * + * @author Johann Sorel (Geomatys) + */ +public final class WellKnownText { + /** + * Format of the ordinates, or {@code null} for writing the shortest text which parses back + * to the same {@code double}. Not thread-safe, hence the warning in the class javadoc. + */ + private final NumberFormat format; + + /** + * Creates an encoder writing every ordinate as the shortest decimal text which parses back to + * the same {@code double} value. A whole number is written without its fractional part: + * {@code 2}, not {@code 2.0}. + */ + public WellKnownText() { + format = null; + } + + /** + * Creates an encoder rounding every ordinate to at most the given number of fraction digits, + * half away from zero. Trailing zeros are not written, so a precision of 3 writes {@code 1.5} + * rather than {@code 1.500}. The precision has no effect on {@link #decode decoding}. + * + * @param decimalPrecision maximal number of digits after the decimal separator, 0 or more. + */ + public WellKnownText(final int decimalPrecision) { + ArgumentChecks.ensurePositive("decimalPrecision", decimalPrecision); + format = NumberFormat.getNumberInstance(Locale.ROOT); + format.setGroupingUsed(false); + format.setMaximumFractionDigits(decimalPrecision); + format.setRoundingMode(RoundingMode.HALF_UP); // Not the HALF_EVEN default, which surprises. + } + + /** + * Returns the Well-Known Text of the given geometry. + * + * @param geom the geometry to encode, not null. + * @return the geometry in Well-Known Text. + * @throws IllegalArgumentException if the geometry, or one of the geometries it contains, + * has no Well-Known Text representation, or if its positions are neither 2 + * nor 3 dimensional. + */ + public String encode(final Geometry geom) { + ArgumentChecks.ensureNonNull("geom", geom); + final StringBuilder sb = new StringBuilder(); + format(sb, geom); + return sb.toString(); + } + + /** + * Returns the geometry described by the given Well-Known Text. + * + * @param geom the Well-Known Text to decode, not null. + * @return the decoded geometry. + * @throws IllegalArgumentException if the text is malformed, or names a geometry type which + * is not in the table of this class javadoc. + */ + public Geometry decode(final String geom) { + return decode(geom, null); + } + + /** + * Returns the geometry described by the given Well-Known Text, in the given coordinate + * reference system. Well-Known Text carries no system of its own. + * + * @param geom the Well-Known Text to decode, not null. + * @param crs the coordinate reference system of the coordinates in the text, or + * {@code null}. + * @return the decoded geometry. + * @throws IllegalArgumentException if the text is malformed, names a geometry type which is + * not in the table of this class javadoc, or has a number of ordinates which + * contradicts the dimension of {@code crs}. + */ + public Geometry decode(final String geom, final CoordinateReferenceSystem crs) { + ArgumentChecks.ensureNonNull("geom", geom); + return new WellKnownTextParser(geom, crs).parse(); + } + + // //////////////////////////////////////////////////////////////////////// + // Encoding /////////////////////////////////////////////////////////////// + // //////////////////////////////////////////////////////////////////////// + + /** + * Appends the Well-Known Text of the given geometry, keyword included. + * + * <p>The order of the tests below is significant: the geometry interfaces form a hierarchy, + * so every type must be tested before its supertypes. It mirrors the dispatch of + * {@code GML3Writer.writeGeometry(…)}.</p> + */ + private void format(final StringBuilder sb, final Geometry geometry) { + if (geometry instanceof Orientable o && o.getOrientationSign() == Orientable.Sign.NEGATIVE) { + format(sb, o.getReverse()); // Orientation is not representable. + } else if (geometry instanceof Empty) { + sb.append(GeometryCollection.TYPE).append(" EMPTY"); + } else if (geometry instanceof Point g) { + formatPoint(sb, g); + } else if (geometry instanceof CircularString g) { // Before Curve. + formatPointList(sb, CircularString.TYPE, g, g.getDataPoints()); + } else if (geometry instanceof CompoundCurve g) { // Before Curve. + formatCompoundCurve(sb, g); + } else if (geometry instanceof LineString g) { // Also matches LinearRing. + formatPointList(sb, LineString.TYPE, g, g.getDataPoints()); + } else if (geometry instanceof Triangle g) { // Before Polygon. + formatPolygon(sb, Triangle.TYPE, g); + } else if (geometry instanceof Polygon g) { // Before Surface. + formatPolygon(sb, Polygon.TYPE, g); + } else if (geometry instanceof CurvePolygon g) { // Before Surface. + formatCurvePolygon(sb, g); + } else if (geometry instanceof TIN g) { // Before PolyhedralSurface. + formatPatches(sb, TIN.TYPE, g.getNumPatches(), g::getPatchN, g); + } else if (geometry instanceof PolyhedralSurface<?> g) { + formatPatches(sb, PolyhedralSurface.TYPE, g.getNumPatches(), g::getPatchN, g); + } else if (geometry instanceof MultiPoint<?> g) { // Before GeometryCollection. + formatMultiPoint(sb, g); + } else if (geometry instanceof MultiLineString g) { // Before MultiCurve. + formatMultiLineString(sb, g); + } else if (geometry instanceof MultiPolygon g) { // Before MultiSurface. + formatMultiPolygon(sb, g); + } else if (geometry instanceof MultiCurve<?> g) { // Before GeometryCollection. + formatMembers(sb, MultiCurve.TYPE, g); + } else if (geometry instanceof MultiSurface<?> g) { // Before GeometryCollection. + formatMembers(sb, MultiSurface.TYPE, g); + } else if (geometry instanceof GeometryCollection<?> g) { + formatMembers(sb, GeometryCollection.TYPE, g); + } else { + throw unsupported(geometry); + } + } + + /** + * Appends {@code "POINT Z (1 2 3)"} or {@code "POINT Z EMPTY"}. + */ + private void formatPoint(final StringBuilder sb, final Point geometry) { + if (appendHeader(sb, Point.TYPE, geometry)) return; + sb.append('('); + appendPosition(sb, geometry.getPosition(), hasMeasure(geometry) ? geometry.getAttribute(AttributesType.ATT_M) : null); + sb.append(')'); + } + + /** + * Appends {@code "LINESTRING (1 2, 3 4)"}: a keyword followed by a single parenthesized + * list of coordinate tuples. + */ + private void formatPointList(final StringBuilder sb, final String keyword, + final Geometry geometry, final DataPoints points) + { + if (appendHeader(sb, keyword, geometry)) return; + appendPointList(sb, points, hasMeasure(geometry)); + } + + /** + * Appends {@code "COMPOUNDCURVE ((1 2, 3 4), CIRCULARSTRING (3 4, 5 6, 7 8))"}. + * A {@code LINESTRING} component may drop its keyword, and does so here; any other + * component keeps it. + */ + private void formatCompoundCurve(final StringBuilder sb, final CompoundCurve geometry) { + if (appendHeader(sb, CompoundCurve.TYPE, geometry)) return; + final boolean hasM = hasMeasure(geometry); + sb.append('('); + for (int i = 0, n = geometry.getNumCurves(); i < n; i++) { + if (i != 0) sb.append(", "); + appendCurveComponent(sb, geometry.getCurveN(i), hasM); + } + sb.append(')'); + } + + /** + * Appends {@code "POLYGON ((0 0, 1 0, 1 1, 0 0), (…))"}, or the same with the + * {@code TRIANGLE} keyword, in which case {@link Triangle#getInteriorRings()} is empty + * and only the exterior ring is written. + */ + private void formatPolygon(final StringBuilder sb, final String keyword, final Polygon geometry) { + if (appendHeader(sb, keyword, geometry)) return; + appendRings(sb, geometry, hasMeasure(geometry)); + } + + /** + * Appends {@code "CURVEPOLYGON (CIRCULARSTRING (…), (…))"}. As in a compound curve, a ring + * which is a {@code LINESTRING} drops its keyword. + */ + private void formatCurvePolygon(final StringBuilder sb, final CurvePolygon geometry) { + if (appendHeader(sb, CurvePolygon.TYPE, geometry)) return; + final boolean hasM = hasMeasure(geometry); + sb.append('('); + appendCurveComponent(sb, geometry.getExteriorRing(), hasM); + for (int i = 0, n = geometry.getNumInteriorRing(); i < n; i++) { + sb.append(", "); + appendCurveComponent(sb, geometry.getInteriorRingN(i), hasM); + } + sb.append(')'); + } + + /** + * Appends {@code "POLYHEDRALSURFACE (((…)), ((…)))"} or the same with the {@code TIN} + * keyword: a list of polygon patches, each written without its keyword. + */ + private void formatPatches(final StringBuilder sb, final String keyword, final int count, + final IntFunction<? extends Polygon> patches, + final Geometry geometry) + { + if (appendHeader(sb, keyword, geometry)) return; + final boolean hasM = hasMeasure(geometry); + sb.append('('); + for (int i = 0; i < count; i++) { + if (i != 0) sb.append(", "); + appendRings(sb, patches.apply(i), hasM); + } + sb.append(')'); + } + + /** + * Appends {@code "MULTIPOINT ((1 2), (3 4))"}. The grammar also allows the parentheses around + * each point to be omitted; they are written here because that form is unambiguous. + */ + private void formatMultiPoint(final StringBuilder sb, final MultiPoint<?> geometry) { + if (appendHeader(sb, MultiPoint.TYPE, geometry)) return; + final boolean hasM = hasMeasure(geometry); + sb.append('('); + for (int i = 0, n = geometry.getNumGeometries(); i < n; i++) { + if (i != 0) sb.append(", "); + final Point point = geometry.getGeometryN(i); + if (point.isEmpty()) { + sb.append("EMPTY"); + } else { + sb.append('('); + appendPosition(sb, point.getPosition(), hasM ? point.getAttribute(AttributesType.ATT_M) : null); + sb.append(')'); + } + } + sb.append(')'); + } + + /** + * Appends {@code "MULTILINESTRING ((1 2, 3 4), (5 6, 7 8))"}: components keep no keyword. + */ + private void formatMultiLineString(final StringBuilder sb, final MultiLineString geometry) { + if (appendHeader(sb, MultiLineString.TYPE, geometry)) return; + final boolean hasM = hasMeasure(geometry); + sb.append('('); + for (int i = 0, n = geometry.getNumGeometries(); i < n; i++) { + if (i != 0) sb.append(", "); + appendPointList(sb, geometry.getGeometryN(i).getDataPoints(), hasM); + } + sb.append(')'); + } + + /** + * Appends {@code "MULTIPOLYGON (((…), (…)), ((…)))"}: components keep no keyword. + */ + private void formatMultiPolygon(final StringBuilder sb, final MultiPolygon geometry) { + if (appendHeader(sb, MultiPolygon.TYPE, geometry)) return; + final boolean hasM = hasMeasure(geometry); + sb.append('('); + for (int i = 0, n = geometry.getNumGeometries(); i < n; i++) { + if (i != 0) sb.append(", "); + appendRings(sb, geometry.getGeometryN(i), hasM); + } + sb.append(')'); + } + + /** + * Appends a collection whose members are written in full, with their own keyword: this is + * mandatory for {@code GEOMETRYCOLLECTION}, and is what makes {@code MULTICURVE} and + * {@code MULTISURFACE} able to hold the curved types. + */ + private void formatMembers(final StringBuilder sb, final String keyword, final GeometryCollection<?> geometry) { + if (appendHeader(sb, keyword, geometry)) return; + sb.append('('); + for (int i = 0, n = geometry.getNumGeometries(); i < n; i++) { + if (i != 0) sb.append(", "); + format(sb, geometry.getGeometryN(i)); + } + sb.append(')'); + } + + /** + * Appends a curve used as a component of a compound curve or as a ring of a curve polygon. + * A {@code LineString} is written as a bare parenthesized list of coordinates, which the + * grammar defines as meaning a line string; anything else is written in full. + */ + private void appendCurveComponent(final StringBuilder sb, final Curve curve, final boolean hasM) { + if (curve instanceof LineString g) { + appendPointList(sb, g.getDataPoints(), hasM); + } else { + format(sb, curve); + } + } + + /** + * Appends the rings of a polygon as {@code "((0 0, 1 0, 1 1, 0 0), (…))"}, without keyword. + * A triangle is written the same way: its exterior ring already closes on its first corner. + */ + private void appendRings(final StringBuilder sb, final Polygon polygon, final boolean hasM) { + if (polygon.isEmpty()) { + sb.append("EMPTY"); + return; + } + sb.append('('); + appendPointList(sb, polygon.getExteriorRing().getDataPoints(), hasM); + for (int i = 0, n = polygon.getNumInteriorRing(); i < n; i++) { + sb.append(", "); + appendPointList(sb, polygon.getInteriorRingN(i).getDataPoints(), hasM); + } + sb.append(')'); + } + + /** + * Appends {@code "(1 2, 3 4)"}: a parenthesized list of coordinate tuples. + */ + private void appendPointList(final StringBuilder sb, final DataPoints points, final boolean hasM) { + sb.append('('); + for (int i = 0, n = points.size(); i < n; i++) { + if (i != 0) sb.append(", "); + appendPosition(sb, points.getPosition(i), hasM ? points.getAttribute(i, AttributesType.ATT_M) : null); + } + sb.append(')'); + } + + /** + * Appends {@code "1 2 3 4"}: the ordinates of one position, followed by its measure if any. + */ + private void appendPosition(final StringBuilder sb, final Tuple<?> position, final Tuple<?> measure) { + for (int i = 0, n = position.getDimension(); i < n; i++) { + if (i != 0) sb.append(' '); + appendNumber(sb, position.get(i)); + } + if (measure != null) { + sb.append(' '); + appendNumber(sb, measure.get(0)); + } + } + + /** + * Appends one ordinate, rounded to the precision given to the constructor if there was one. + * Values which are not finite have no decimal representation to round, and are written + * as {@code NaN} or {@code Infinity} — neither of which the grammar allows, but refusing + * to write them would make the geometry impossible to inspect at all. + */ + private void appendNumber(final StringBuilder sb, final double value) { + if (format == null || !Double.isFinite(value)) { + sb.append(value); + StringBuilders.trimFractionalPart(sb); + } else { + sb.append(format.format(value)); + } + } + + /** + * Appends the keyword and the dimension flag of a geometry, then {@code "EMPTY"} if the + * geometry is empty. Returns {@code true} if the caller has nothing more to write. + */ + private static boolean appendHeader(final StringBuilder sb, final String keyword, final Geometry geometry) { + sb.append(keyword); + final CoordinateReferenceSystem crs = geometry.getCoordinateReferenceSystem(); + if (crs == null) { + throw new IllegalArgumentException("Cannot write a " + keyword + " in Well-Known Text:" + + " it has no coordinate reference system, therefore no known number of dimensions." + + " An empty collection takes one from the factory method which creates it."); + } + final int dimension = crs.getCoordinateSystem().getDimension(); + final boolean hasM = hasMeasure(geometry); + switch (dimension) { + case 2: sb.append(hasM ? " M " : " "); break; + case 3: sb.append(hasM ? " ZM " : " Z "); break; + default: throw new IllegalArgumentException("Cannot write a " + keyword + " in Well-Known Text:" + + " its positions have " + dimension + " dimensions," + + " but the format defines only 2 and 3."); + } + if (geometry.isEmpty()) { + sb.append("EMPTY"); + return true; + } + return false; + } + + /** + * Returns whether the given geometry carries the {@linkplain AttributesType#ATT_M measure} + * attribute, which is what the {@code M} and {@code ZM} flags stand for. + */ + private static boolean hasMeasure(final Geometry geometry) { + final AttributesType type = geometry.getAttributesType(); + return (type != null) && type.getAttributeNames().contains(AttributesType.ATT_M); + } + + /** + * Returns the exception to throw for a geometry which the format cannot represent. + */ + private static IllegalArgumentException unsupported(final Geometry geometry) { + return new IllegalArgumentException("Well-Known Text defines no representation for " + + geometry.getClass().getSimpleName() + '.'); + } +} diff --git a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/adapter/WellKnownTextParser.java b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/adapter/WellKnownTextParser.java new file mode 100644 index 0000000000..46c933054b --- /dev/null +++ b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/adapter/WellKnownTextParser.java @@ -0,0 +1,786 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.sis.geometries.adapter; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import org.apache.sis.geometries.AttributesType; +import org.apache.sis.geometries.Curve; +import org.apache.sis.geometries.DataPoints; +import org.apache.sis.geometries.Empty; +import org.apache.sis.geometries.Geometries; +import org.apache.sis.geometries.Geometry; +import org.apache.sis.geometries.GeometryCollection; +import org.apache.sis.geometries.GeometryFactory; +import org.apache.sis.geometries.Point; +import org.apache.sis.geometries.Surface; +import org.apache.sis.geometries.curve.CircularString; +import org.apache.sis.geometries.curve.CompoundCurve; +import org.apache.sis.geometries.curve.LinearRing; +import org.apache.sis.geometries.curve.LineString; +import org.apache.sis.geometries.curve.MultiCurve; +import org.apache.sis.geometries.curve.MultiLineString; +import org.apache.sis.geometries.point.MultiPoint; +import org.apache.sis.geometries.surface.CurvePolygon; +import org.apache.sis.geometries.surface.MultiPolygon; +import org.apache.sis.geometries.surface.MultiSurface; +import org.apache.sis.geometries.surface.Polygon; +import org.apache.sis.geometries.surface.PolyhedralSurface; +import org.apache.sis.geometries.surface.TIN; +import org.apache.sis.geometries.surface.Triangle; +import org.apache.sis.maths.Array; +import org.apache.sis.maths.DataType; +import org.apache.sis.maths.NDArrays; +import org.apache.sis.maths.SampleSystem; +import org.opengis.referencing.crs.CoordinateReferenceSystem; + + +/** + * Recursive descent parser of the Well-Known Text representation of geometries. + * One instance parses one text and is then discarded. + * + * <p>The grammar, the set of accepted keywords and the handling of the {@code Z}, {@code M} and + * {@code ZM} flags are documented on {@link WellKnownText}, which is the public face of this + * class. Everything below is that grammar written as one method per production.</p> + * + * <h2>Dimensions</h2> + * A Well-Known Text is homogeneous: a single geometry cannot mix 2- and 3-dimensional positions, + * because an Apache SIS {@link DataPoints} reports one dimension only. The flags are therefore + * parser state rather than per-element state: whichever element states them first fixes them, a + * later element contradicting them is an error, and if no element states them at all they are + * inferred from the width of the first coordinate tuple. + * + * @author Johann Sorel (Geomatys) + */ +final class WellKnownTextParser { + /** + * Value returned by {@link #readFlags()} when the text carries no dimension flag. + */ + private static final int UNSPECIFIED = -1; + + /** + * Bits of the value returned by {@link #readFlags()}. + */ + private static final int FLAG_Z = 1, FLAG_M = 2; + + /** + * Sample system of the {@linkplain AttributesType#ATT_M measure} attribute, which is a single + * value with no coordinate reference system of its own. + */ + private static final SampleSystem MEASURE_SYSTEM = SampleSystem.ofSize(1); + + /** + * The text being parsed. + */ + private final String text; + + /** + * The coordinate reference system given by the caller, or {@code null} for deriving an + * {@linkplain Geometries#getUndefinedCRS(int) undefined} one from the number of ordinates. + */ + private final CoordinateReferenceSystem userCRS; + + /** + * Index in {@link #text} of the next character to read. + */ + private int pos; + + /** + * Whether {@link #hasZ} and {@link #hasM} have been established, either by a dimension flag + * or by the width of a coordinate tuple. + */ + private boolean flagsKnown; + + /** + * Whether positions have a third ordinate, and whether they carry a measure. + */ + private boolean hasZ, hasM; + + /** + * Creates a parser for the given text. + * + * @param text the Well-Known Text to parse. + * @param crs the coordinate reference system to give to the geometries, or {@code null}. + */ + WellKnownTextParser(final String text, final CoordinateReferenceSystem crs) { + this.text = text; + this.userCRS = crs; + } + + /** + * Parses the whole text as a single geometry. + * + * @throws IllegalArgumentException if the text is malformed or names an unsupported type. + */ + Geometry parse() { + final Geometry geometry = parseGeometry(); + skipSpaces(); + if (pos < text.length()) { + throw error("Unexpected text after the end of the geometry"); + } + return geometry; + } + + // //////////////////////////////////////////////////////////////////////// + // Productions //////////////////////////////////////////////////////////// + // //////////////////////////////////////////////////////////////////////// + + /** + * Parses {@code <keyword> [Z|M|ZM] <body>}. + */ + private Geometry parseGeometry() { + final String keyword = readWord(); + if (keyword.isEmpty()) { + throw error("Expected a geometry type keyword"); + } + applyFlags(readFlags()); + switch (keyword) { + case Point.TYPE: return parsePoint(); + case LineString.TYPE: return GeometryFactory.createLineString(readPointList()); + case CircularString.TYPE: return GeometryFactory.createCircularString(readPointList()); + case CompoundCurve.TYPE: return parseCompoundCurve(); + case Polygon.TYPE: return readPolygonBody(); + case Triangle.TYPE: return readTriangleBody(); + case CurvePolygon.TYPE: return parseCurvePolygon(); + case PolyhedralSurface.TYPE: return parsePolyhedralSurface(); + case TIN.TYPE: return parseTIN(); + case MultiPoint.TYPE: return parseMultiPoint(); + case MultiLineString.TYPE: return parseMultiLineString(); + case MultiCurve.TYPE: return parseMultiCurve(); + case MultiPolygon.TYPE: return parseMultiPolygon(); + case MultiSurface.TYPE: return parseMultiSurface(); + case GeometryCollection.TYPE: + case "GEOMCOLLECTION": return parseGeometryCollection(); // Alias in use in some databases. + default: throw error("Well-Known Text defines no geometry type named \"" + keyword + '"'); + } + } + + /** + * Parses {@code (1 2)} or {@code EMPTY}. + * + * <p>{@code POINT EMPTY} does not map to a {@link Point}: the Apache SIS model has no empty + * point, since a point sequence of length 0 is not a valid position of a point and + * {@link GeometryFactory#createPoint(CoordinateReferenceSystem)} builds a point at the origin + * rather than an absent one. It maps to {@link Empty} instead, the geometry which stands for + * the empty point set whatever its type, and which is written back as + * {@code GEOMETRYCOLLECTION EMPTY}.</p> + */ + private Geometry parsePoint() { + if (readEmpty()) { + return GeometryFactory.createEmpty(crs()); + } + return GeometryFactory.createPoint(readSinglePosition()); + } + + /** + * Parses {@code ((1 2, 3 4), CIRCULARSTRING (3 4, 5 6, 7 8))} or {@code EMPTY}. + */ + private CompoundCurve parseCompoundCurve() { + if (readEmpty()) { + return GeometryFactory.createCompoundCurve(crs()); + } + final List<Curve> curves = new ArrayList<>(); + expect('('); + do { + curves.add(readCurve(false)); + } while (accept(',')); + expect(')'); + return GeometryFactory.createCompoundCurve(curves.toArray(Curve[]::new)); + } + + /** + * Parses {@code (CIRCULARSTRING (…), (…))} or {@code EMPTY}. + */ + private CurvePolygon parseCurvePolygon() { + if (readEmpty()) { + return GeometryFactory.createCurvePolygon(emptyRing(), List.of()); + } + final List<Curve> rings = new ArrayList<>(); + expect('('); + do { + rings.add(readCurve(true)); + } while (accept(',')); + expect(')'); + return GeometryFactory.createCurvePolygon(rings.get(0), new ArrayList<>(rings.subList(1, rings.size()))); + } + + /** + * Parses {@code (((…)), ((…)))} or {@code EMPTY}: a list of polygon patches written + * without their keyword. + */ + private PolyhedralSurface<Polygon> parsePolyhedralSurface() { + if (readEmpty()) { + return GeometryFactory.createPolyhedralSurface(crs(), new Polygon[0]); + } + final List<Polygon> patches = new ArrayList<>(); + expect('('); + do { + patches.add(readPolygonBody()); + } while (accept(',')); + expect(')'); + return GeometryFactory.createPolyhedralSurface(patches.toArray(Polygon[]::new)); + } + + /** + * Parses {@code (((…)), ((…)))} or {@code EMPTY}, where every patch is a triangle. + */ + private TIN parseTIN() { + if (readEmpty()) { + return GeometryFactory.createTIN(crs(), new Triangle[0]); + } + final List<Triangle> patches = new ArrayList<>(); + expect('('); + do { + patches.add(readTriangleBody()); + } while (accept(',')); + expect(')'); + return GeometryFactory.createTIN(patches.toArray(Triangle[]::new)); + } + + /** + * Parses {@code ((1 2), (3 4))} or {@code (1 2, 3 4)} or {@code EMPTY}. The grammar allows + * the parentheses around each point to be omitted, and both forms are accepted here. + */ + private MultiPoint<?> parseMultiPoint() { + if (readEmpty()) { + return GeometryFactory.createMultiPoint(crs()); + } + final List<Point> points = new ArrayList<>(); + expect('('); + do { + if (readEmpty()) { + // See parsePoint(): there is no empty point to put in the collection. + throw error("A " + MultiPoint.TYPE + " cannot hold an empty point"); + } else if (peek() == '(') { + points.add(GeometryFactory.createPoint(readSinglePosition())); + } else { + final Coordinates c = new Coordinates(); + c.add(readTuple()); + points.add(GeometryFactory.createPoint(c.build())); + } + } while (accept(',')); + expect(')'); + return GeometryFactory.createMultiPoint(points.toArray(Point[]::new)); + } + + /** + * Parses {@code ((1 2, 3 4), (5 6, 7 8))} or {@code EMPTY}. + */ + private MultiLineString parseMultiLineString() { + if (readEmpty()) { + return GeometryFactory.createMultiLineString(crs()); + } + final List<LineString> members = new ArrayList<>(); + expect('('); + do { + members.add(GeometryFactory.createLineString(readPointList())); + } while (accept(',')); + expect(')'); + return GeometryFactory.createMultiLineString(members.toArray(LineString[]::new)); + } + + /** + * Parses {@code (((…), (…)), ((…)))} or {@code EMPTY}. + */ + private MultiPolygon parseMultiPolygon() { + if (readEmpty()) { + return GeometryFactory.createMultiPolygon(crs()); + } + final List<Polygon> members = new ArrayList<>(); + expect('('); + do { + members.add(readPolygonBody()); + } while (accept(',')); + expect(')'); + return GeometryFactory.createMultiPolygon(members.toArray(Polygon[]::new)); + } + + /** + * Parses {@code (CIRCULARSTRING (…), (…))} or {@code EMPTY}: unlike a + * {@code MULTILINESTRING}, the members may be of any curve type and therefore keep their + * keyword, except a line string which may be written as a bare coordinate list. + */ + private MultiCurve<Curve> parseMultiCurve() { + if (readEmpty()) { + return GeometryFactory.<Curve>createMultiCurve(crs()); + } + final List<Curve> members = new ArrayList<>(); + expect('('); + do { + members.add(readCurve(false)); + } while (accept(',')); + expect(')'); + return GeometryFactory.createMultiCurve(members.toArray(Curve[]::new)); + } + + /** + * Parses {@code (CURVEPOLYGON (…), ((…)))} or {@code EMPTY}: as in a {@code MULTICURVE}, + * the members keep their keyword except a polygon, which may be written as a bare ring list. + */ + private MultiSurface<Surface> parseMultiSurface() { + if (readEmpty()) { + return GeometryFactory.<Surface>createMultiSurface(crs()); + } + final List<Surface> members = new ArrayList<>(); + expect('('); + do { + if (peek() == '(') { + members.add(readPolygonBody()); + } else { + members.add(expectType(parseGeometry(), Surface.class, MultiSurface.TYPE)); + } + } while (accept(',')); + expect(')'); + return GeometryFactory.createMultiSurface(members.toArray(Surface[]::new)); + } + + /** + * Parses {@code (POINT (1 2), LINESTRING (3 4, 5 6))} or {@code EMPTY}. Every member keeps + * its keyword, since a collection puts no constraint on the type of its members. + */ + private GeometryCollection<Geometry> parseGeometryCollection() { + if (readEmpty()) { + return GeometryFactory.<Geometry>createGeometryCollection(crs()); + } + final List<Geometry> members = new ArrayList<>(); + expect('('); + do { + members.add(parseGeometry()); + } while (accept(',')); + expect(')'); + return GeometryFactory.createGeometryCollection(members.toArray(Geometry[]::new)); + } + + // //////////////////////////////////////////////////////////////////////// + // Shared productions ///////////////////////////////////////////////////// + // //////////////////////////////////////////////////////////////////////// + + /** + * Parses a curve written either as a bare coordinate list or with its own keyword. + * A bare list is a line string, and is closed into a {@link LinearRing} when it is used as + * the boundary of a {@link CurvePolygon}. + * + * @param asRing whether a bare coordinate list is a ring rather than a line string. + */ + private Curve readCurve(final boolean asRing) { + if (peek() == '(') { + final DataPoints points = readPointList(); + return asRing ? GeometryFactory.createLinearRing(points) + : GeometryFactory.createLineString(points); + } + return expectType(parseGeometry(), Curve.class, asRing ? CurvePolygon.TYPE : CompoundCurve.TYPE); + } + + /** + * Parses {@code ((…), (…))} or {@code EMPTY}: the rings of a polygon, without keyword. + */ + private Polygon readPolygonBody() { + if (readEmpty()) { + return GeometryFactory.createPolygon(emptyRing(), List.of()); + } + final List<LinearRing> rings = readRings(); + return GeometryFactory.createPolygon(rings.get(0), new ArrayList<>(rings.subList(1, rings.size()))); + } + + /** + * Parses {@code ((…))} or {@code EMPTY}: the single ring of a triangle, without keyword. + */ + private Triangle readTriangleBody() { + if (readEmpty()) { + return GeometryFactory.createTriangle(emptyRing()); + } + final List<LinearRing> rings = readRings(); + if (rings.size() != 1) { + throw error("A " + Triangle.TYPE + " patch has no interior ring, but " + (rings.size() - 1) + " were given"); + } + return GeometryFactory.createTriangle(rings.get(0)); + } + + /** + * Parses {@code ((0 0, 1 0, 0 0), (…))}: one or more parenthesized coordinate lists. + */ + private List<LinearRing> readRings() { + final List<LinearRing> rings = new ArrayList<>(); + expect('('); + do { + rings.add(GeometryFactory.createLinearRing(readPointList())); + } while (accept(',')); + expect(')'); + return rings; + } + + /** + * Parses {@code (1 2, 3 4)} or {@code EMPTY}: one parenthesized list of coordinate tuples. + */ + private DataPoints readPointList() { + if (readEmpty()) { + return new Coordinates().build(); + } + final Coordinates coordinates = new Coordinates(); + expect('('); + do { + coordinates.add(readTuple()); + } while (accept(',')); + expect(')'); + return coordinates.build(); + } + + /** + * Parses {@code (1 2)}: exactly one parenthesized coordinate tuple, as in a point. + */ + private DataPoints readSinglePosition() { + final Coordinates coordinates = new Coordinates(); + expect('('); + coordinates.add(readTuple()); + expect(')'); + return coordinates.build(); + } + + /** + * Returns an empty ring, for the {@code EMPTY} form of the types which are made of rings. + */ + private LinearRing emptyRing() { + return GeometryFactory.createLinearRing(new Coordinates().build()); + } + + /** + * Casts a parsed member to the type its container requires. + */ + private <T> T expectType(final Geometry geometry, final Class<T> type, final String container) { + if (type.isInstance(geometry)) { + return type.cast(geometry); + } + throw error("A " + container + " cannot contain a " + geometry.getGeometryType()); + } + + // //////////////////////////////////////////////////////////////////////// + // Dimensions ///////////////////////////////////////////////////////////// + // //////////////////////////////////////////////////////////////////////// + + /** + * Returns the number of ordinates of a position, which is 2 unless the text has established + * that positions carry a <var>z</var> ordinate. A text made only of {@code EMPTY} establishes + * nothing, in which case the 2-dimensional default applies. + */ + private int positionDimension() { + return (flagsKnown && hasZ) ? 3 : 2; + } + + /** + * Returns the coordinate reference system to give to the geometries, which is the one the + * caller supplied if any, and an {@linkplain Geometries#getUndefinedCRS(int) undefined} one + * of the right dimension otherwise. + * + * @throws IllegalArgumentException if the caller supplied a system whose dimension + * contradicts the number of ordinates found in the text. + */ + private CoordinateReferenceSystem crs() { + final int dimension = positionDimension(); + if (userCRS == null) { + return Geometries.getUndefinedCRS(dimension); + } + final int actual = userCRS.getCoordinateSystem().getDimension(); + if (actual != dimension) { + throw error("The given coordinate reference system has " + actual + " dimensions," + + " but the text has " + dimension + " ordinates per position"); + } + return userCRS; + } + + /** + * Records the dimension flag of an element. The first element to state the flags fixes them + * for the whole text; a later element may repeat them but not contradict them. + */ + private void applyFlags(final int flags) { + if (flags == UNSPECIFIED) { + return; + } + final boolean z = (flags & FLAG_Z) != 0; + final boolean m = (flags & FLAG_M) != 0; + if (flagsKnown) { + if (z != hasZ || m != hasM) { + throw error("Dimension flag " + flagName(z, m) + " contradicts the " + + flagName(hasZ, hasM) + " established earlier in the text"); + } + } else { + hasZ = z; + hasM = m; + flagsKnown = true; + } + } + + /** + * Establishes or verifies the dimension flags against the width of a coordinate tuple. + * Called by {@link Coordinates} when it receives its first tuple. + */ + private void inferFlags(final int width) { + if (flagsKnown) { + final int expected = positionDimension() + (hasM ? 1 : 0); + if (width != expected) { + throw error("A coordinate tuple of " + width + " ordinates contradicts the " + + flagName(hasZ, hasM) + " dimension flag, which requires " + expected); + } + return; + } + switch (width) { + case 2: hasZ = false; break; + case 3: hasZ = true; break; + default: throw error("A coordinate tuple of " + width + " ordinates is ambiguous" + + " without a Z, M or ZM flag"); + } + hasM = false; + flagsKnown = true; + } + + /** + * Returns {@code "Z"}, {@code "M"}, {@code "ZM"} or {@code "XY"} for an error message. + */ + private static String flagName(final boolean z, final boolean m) { + if (z) return m ? "ZM" : "Z"; + return m ? "M" : "XY"; + } + + /** + * Accumulator of the coordinate tuples of one element, and factory of the + * {@link DataPoints} the Apache SIS geometry model is built upon. + * + * <p>Ordinates are accumulated in a single flat array in row-major order, which is the layout + * {@link NDArrays#of(SampleSystem, double...)} expects. The measure, when there is one, is the + * last ordinate of each tuple in the text but a separate attribute in the model, so + * {@link #build()} splits the two apart.</p> + */ + private final class Coordinates { + /** + * The accumulated ordinates. Only the first {@link #count} elements are meaningful. + */ + private double[] values = new double[12]; + + /** + * Number of meaningful ordinates in {@link #values}, always a multiple of {@link #width}. + */ + private int count; + + /** + * Number of ordinates per tuple, or 0 if no tuple has been added yet. + */ + private int width; + + /** + * Appends one coordinate tuple. The first one fixes the width for all the others, + * and settles the dimension flags if the text did not state them. + */ + void add(final double[] tuple) { + if (width == 0) { + width = tuple.length; + inferFlags(width); + } else if (tuple.length != width) { + throw error("A coordinate tuple of " + tuple.length + " ordinates follows tuples of " + width); + } + if (count + width > values.length) { + values = Arrays.copyOf(values, Math.max(count + width, values.length * 2)); + } + System.arraycopy(tuple, 0, values, count, width); + count += width; + } + + /** + * Builds the point sequence of the accumulated tuples, possibly empty. + */ + DataPoints build() { + final int posWidth = positionDimension(); + final SampleSystem posSystem = SampleSystem.of(crs()); + final int size = (width != 0) ? count / width : 0; + if (size == 0) { + final Array empty = NDArrays.of(posSystem, DataType.DOUBLE, 0); + if (!hasM) { + return GeometryFactory.createSequence(empty); + } + return createSequence(empty, NDArrays.of(MEASURE_SYSTEM, DataType.DOUBLE, 0)); + } + if (!hasM) { + return GeometryFactory.createSequence(NDArrays.of(posSystem, Arrays.copyOf(values, count))); + } + /* + * The measure is the ordinate following the position ones in each tuple of the text, + * but a one dimensional attribute of its own in the model. + */ + final double[] p = new double[size * posWidth]; + final double[] m = new double[size]; + for (int i = 0; i < size; i++) { + System.arraycopy(values, i * width, p, i * posWidth, posWidth); + m[i] = values[i * width + posWidth]; + } + return createSequence(NDArrays.of(posSystem, p), NDArrays.of(MEASURE_SYSTEM, m)); + } + + /** + * Returns a sequence holding both the positions and the measures. + */ + private DataPoints createSequence(final Array positions, final Array measures) { + final Map<String,Array> attributes = new LinkedHashMap<>(4); + attributes.put(AttributesType.ATT_POSITION, positions); + attributes.put(AttributesType.ATT_M, measures); + return GeometryFactory.createSequence(attributes); + } + } + + // //////////////////////////////////////////////////////////////////////// + // Tokens ///////////////////////////////////////////////////////////////// + // //////////////////////////////////////////////////////////////////////// + + /** + * Reads the numbers of one coordinate tuple, which are separated by spaces only. + */ + private double[] readTuple() { + double[] values = new double[4]; + int n = 0; + for (;;) { + skipSpaces(); + if (pos >= text.length() || !isNumberStart(text.charAt(pos))) { + break; + } + if (n == values.length) { + values = Arrays.copyOf(values, n * 2); + } + values[n++] = readNumber(); + } + if (n == 0) { + throw error("Expected a coordinate tuple"); + } + return Arrays.copyOf(values, n); + } + + /** + * Reads one number. The lexical form is the one of {@link Double#parseDouble(String)} + * restricted to decimal notation, and is therefore independent of the default locale. + */ + private double readNumber() { + final int start = pos; + final int length = text.length(); + if (pos < length && isSign(text.charAt(pos))) pos++; + while (pos < length && (isDigit(text.charAt(pos)) || text.charAt(pos) == '.')) pos++; + if (pos < length && (text.charAt(pos) == 'e' || text.charAt(pos) == 'E')) { + pos++; + if (pos < length && isSign(text.charAt(pos))) pos++; + while (pos < length && isDigit(text.charAt(pos))) pos++; + } + final String token = text.substring(start, pos); + try { + return Double.parseDouble(token); + } catch (NumberFormatException e) { + pos = start; + throw error("\"" + token + "\" is not a number"); + } + } + + /** + * Reads the next word, in upper case, or an empty string if the next character is not a + * letter. Words are the keywords, the dimension flags and {@code EMPTY}. + */ + private String readWord() { + skipSpaces(); + final int start = pos; + while (pos < text.length() && Character.isLetter(text.charAt(pos))) pos++; + return text.substring(start, pos).toUpperCase(Locale.ROOT); + } + + /** + * Reads the optional dimension flag which may follow a keyword. + * + * @return {@link #FLAG_Z} and/or {@link #FLAG_M}, or {@link #UNSPECIFIED} if there is none. + */ + private int readFlags() { + final int mark = pos; + switch (readWord()) { + case "Z": return FLAG_Z; + case "M": return FLAG_M; + case "ZM": return FLAG_Z | FLAG_M; + default: pos = mark; return UNSPECIFIED; + } + } + + /** + * Consumes the {@code EMPTY} keyword if it is the next word, and returns whether it was. + */ + private boolean readEmpty() { + final int mark = pos; + if ("EMPTY".equals(readWord())) { + return true; + } + pos = mark; + return false; + } + + /** + * Consumes the given character, which must be the next one. + */ + private void expect(final char c) { + if (!accept(c)) { + throw error("Expected '" + c + '\''); + } + } + + /** + * Consumes the given character if it is the next one, and returns whether it was. + */ + private boolean accept(final char c) { + skipSpaces(); + if (pos < text.length() && text.charAt(pos) == c) { + pos++; + return true; + } + return false; + } + + /** + * Returns the next character without consuming it, or {@code 0} at the end of the text. + */ + private char peek() { + skipSpaces(); + return (pos < text.length()) ? text.charAt(pos) : 0; + } + + /** + * Advances past any whitespace. + */ + private void skipSpaces() { + while (pos < text.length() && Character.isWhitespace(text.charAt(pos))) pos++; + } + + private static boolean isDigit(final char c) { + return c >= '0' && c <= '9'; + } + + private static boolean isSign(final char c) { + return c == '+' || c == '-'; + } + + private static boolean isNumberStart(final char c) { + return isDigit(c) || isSign(c) || c == '.'; + } + + /** + * Returns the exception to throw for a malformed text, pointing at the current position. + */ + private IllegalArgumentException error(final String message) { + return new IllegalArgumentException(message + ", at offset " + pos + " of \"" + text + '"'); + } +} diff --git a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/curve/Geodesic.java b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/curve/Geodesic.java index 42575abe8c..f3035a7a62 100644 --- a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/curve/Geodesic.java +++ b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/curve/Geodesic.java @@ -19,9 +19,7 @@ package org.apache.sis.geometries.curve; import org.apache.sis.geometries.AttributesType; import org.apache.sis.geometries.Curve; import org.apache.sis.geometries.CurveInterpolation; -import org.apache.sis.geometries.DataPoints; import org.apache.sis.geometries.Point; -import org.apache.sis.geometries.internal.shared.AbstractGeometry; import org.apache.sis.geometries.internal.shared.DefaultGeodesic; import org.apache.sis.maths.Array; import static org.opengis.annotation.Specification.ISO_19107; @@ -129,12 +127,4 @@ public sealed interface Geodesic extends Curve return getDataPoints().getAttributesType(); } - @Override - default String asText() { - final StringBuilder sb = new StringBuilder("GEODESIC ("); - final DataPoints points = getDataPoints(); - AbstractGeometry.toText(sb, points); - sb.append(')'); - return sb.toString(); - } } diff --git a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/curve/LineString.java b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/curve/LineString.java index 02388c94bd..fef79f59f4 100644 --- a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/curve/LineString.java +++ b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/curve/LineString.java @@ -22,7 +22,6 @@ import org.apache.sis.geometries.Curve; import org.apache.sis.geometries.CurveInterpolation; import org.apache.sis.geometries.DataPoints; import org.apache.sis.geometries.Point; -import org.apache.sis.geometries.internal.shared.AbstractGeometry; import org.apache.sis.geometries.internal.shared.DefaultLineString; import org.apache.sis.geometries.mesh.MeshPrimitive; import org.apache.sis.geometries.operation.triangulate.delaunay.OrientedEdge; @@ -162,12 +161,4 @@ public sealed interface LineString extends Curve return points.getEnvelope(); } - @Override - default String asText() { - final StringBuilder sb = new StringBuilder("LINESTRING ("); - final DataPoints points = getDataPoints(); - AbstractGeometry.toText(sb, points); - sb.append(')'); - return sb.toString(); - } } diff --git a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/curve/LinearRing.java b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/curve/LinearRing.java index a49ed90fc0..91ac52b456 100644 --- a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/curve/LinearRing.java +++ b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/curve/LinearRing.java @@ -16,10 +16,7 @@ */ package org.apache.sis.geometries.curve; -import org.apache.sis.geometries.DataPoints; -import org.apache.sis.geometries.internal.shared.AbstractGeometry; import org.apache.sis.geometries.internal.shared.DefaultLinearRing; -import org.apache.sis.maths.Tuple; /** @@ -68,16 +65,4 @@ public sealed interface LinearRing extends LineString return true; } - @Override - default String asText() { - final StringBuilder sb = new StringBuilder("LINEARRING ("); - final DataPoints points = getDataPoints(); - for (int i = 0, n = points.size() ; i < n; i++) { - final Tuple pt = points.getPosition(i); - if (i > 0) sb.append(','); - AbstractGeometry.toText(sb, pt); - } - sb.append(')'); - return sb.toString(); - } } diff --git a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/curve/MultiLineString.java b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/curve/MultiLineString.java index ba7990bbfe..b0b6fe9725 100644 --- a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/curve/MultiLineString.java +++ b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/curve/MultiLineString.java @@ -17,12 +17,9 @@ package org.apache.sis.geometries.curve; import java.util.Set; -import org.apache.sis.geometries.DataPoints; import org.apache.sis.geometries.GeometryType; -import org.apache.sis.geometries.internal.shared.AbstractGeometry; import org.apache.sis.geometries.internal.shared.DefaultMultiLineString; import org.apache.sis.geometries.mesh.MeshPrimitive; -import org.apache.sis.maths.Tuple; /** @@ -52,22 +49,4 @@ public sealed interface MultiLineString extends MultiCurve<LineString> return Set.of(GeometryType.LINE); } - @Override - default String asText() { - final StringBuilder sb = new StringBuilder("MULTILINESTRING ("); - for (int k = 0, kn = getNumGeometries(); k < kn; k++){ - if (k > 0) sb.append(','); - sb.append('('); - final LineString line = getGeometryN(k); - final DataPoints points = line.getDataPoints(); - for (int i = 0, n = points.size() ; i < n; i++) { - final Tuple pos = points.getPosition(i); - if (i > 0) sb.append(','); - AbstractGeometry.toText(sb, pos); - } - sb.append(')'); - } - sb.append(')'); - return sb.toString(); - } } diff --git a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/curve/NurbCurve.java b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/curve/NurbCurve.java index a0519d288f..02c4ef07aa 100644 --- a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/curve/NurbCurve.java +++ b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/curve/NurbCurve.java @@ -18,8 +18,6 @@ package org.apache.sis.geometries.curve; import org.apache.sis.geometries.AttributesType; import org.apache.sis.geometries.CurveInterpolation; -import org.apache.sis.geometries.DataPoints; -import org.apache.sis.geometries.internal.shared.AbstractGeometry; import org.apache.sis.geometries.internal.shared.DefaultNurbCurve; import static org.opengis.annotation.Specification.ISO_19107; import org.opengis.annotation.UML; @@ -137,12 +135,4 @@ public sealed interface NurbCurve extends BSplineCurve throw new UnsupportedOperationException("Not supported yet."); } - @Override - default String asText() { - final StringBuilder sb = new StringBuilder("NURBS ("); - final DataPoints points = getDataPoints(); - AbstractGeometry.toText(sb, points); - sb.append(')'); - return sb.toString(); - } } diff --git a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/curve/Rhumb.java b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/curve/Rhumb.java index 958b55d612..6b2b13a5da 100644 --- a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/curve/Rhumb.java +++ b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/curve/Rhumb.java @@ -20,9 +20,7 @@ import org.apache.sis.geometries.AttributesType; import org.apache.sis.geometries.Bearing; import org.apache.sis.geometries.Curve; import org.apache.sis.geometries.CurveInterpolation; -import org.apache.sis.geometries.DataPoints; import org.apache.sis.geometries.Point; -import org.apache.sis.geometries.internal.shared.AbstractGeometry; import org.apache.sis.geometries.internal.shared.DefaultRhumb; import org.apache.sis.maths.Array; import static org.opengis.annotation.Specification.ISO_19107; @@ -139,12 +137,4 @@ public sealed interface Rhumb extends Curve return getDataPoints().getAttributesType(); } - @Override - default String asText() { - final StringBuilder sb = new StringBuilder("RHUMB ("); - final DataPoints points = getDataPoints(); - AbstractGeometry.toText(sb, points); - sb.append(')'); - return sb.toString(); - } } diff --git a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/AbstractGeometry.java b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/AbstractGeometry.java index c2be631961..d60d869e56 100644 --- a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/AbstractGeometry.java +++ b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/AbstractGeometry.java @@ -18,12 +18,8 @@ package org.apache.sis.geometries.internal.shared; import java.util.HashMap; import java.util.Map; -import org.apache.sis.geometries.DataPoints; import org.apache.sis.geometries.Geometry; import org.apache.sis.geometry.GeneralEnvelope; -import org.apache.sis.maths.Array; -import org.apache.sis.maths.Cursor; -import org.apache.sis.maths.Tuple; import org.opengis.geometry.Envelope; @@ -49,38 +45,6 @@ public abstract non-sealed class AbstractGeometry implements Geometry { return asText(); } - public static void toText(StringBuilder sb, Tuple tuple) { - sb.append(tuple.get(0)); - for (int i = 1, n = tuple.getDimension(); i < n; i++) { - sb.append(' '); - sb.append(tuple.get(i)); - } - } - - public static void toText(StringBuilder sb, Array array) { - final Cursor cursor = array.cursor(); - boolean first = true; - while (cursor.next()) { - if (!first) { - sb.append(", "); - } - toText(sb, cursor.samples()); - first = false; - } - } - - public static void toText(StringBuilder sb, DataPoints array) { - final int size = array.size(); - if (size == 0) return; - - for (int i = 0; i < size; i++) { - if (i != 0) { - sb.append(", "); - } - toText(sb, array.getPosition(i)); - } - } - /** * Returns the union of the envelopes of the given geometries, * or {@code null} if there is nothing to compute a union of. diff --git a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/DefaultArcByBulge.java b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/DefaultArcByBulge.java index 543edfa4bb..645043be66 100644 --- a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/DefaultArcByBulge.java +++ b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/DefaultArcByBulge.java @@ -78,13 +78,4 @@ public non-sealed class DefaultArcByBulge extends AbstractGeometry implements Ar throw new UnsupportedOperationException("Not supported yet."); } - @Override - public String asText() { - final StringBuilder sb = new StringBuilder(TYPE).append(" ("); - toText(sb, points); - sb.append(", BULGE ").append(bulge); - sb.append(", NORMAL "); - toText(sb, normal); - return sb.append(')').toString(); - } } diff --git a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/DefaultArcByCenterPoint.java b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/DefaultArcByCenterPoint.java index a4f7d61106..6dbf90ff41 100644 --- a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/DefaultArcByCenterPoint.java +++ b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/DefaultArcByCenterPoint.java @@ -97,15 +97,4 @@ public non-sealed class DefaultArcByCenterPoint extends AbstractGeometry impleme throw new UnsupportedOperationException("Not supported yet."); } - @Override - public String asText() { - final StringBuilder sb = new StringBuilder(TYPE).append(" ("); - toText(sb, center.getPosition()); - sb.append(", RADIUS ").append(radius); - if (radiusUnit != null) { - sb.append(' ').append(radiusUnit); - } - sb.append(", ANGLES ").append(startAngle).append(' ').append(endAngle); - return sb.append(')').toString(); - } } diff --git a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/DefaultCircularString.java b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/DefaultCircularString.java index 298507c309..30ab8e0273 100644 --- a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/DefaultCircularString.java +++ b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/DefaultCircularString.java @@ -55,11 +55,4 @@ public non-sealed class DefaultCircularString extends AbstractGeometry implement throw new UnsupportedOperationException("Not supported yet."); } - @Override - public String asText() { - final StringBuilder sb = new StringBuilder(TYPE).append(" ("); - toText(sb, points); - return sb.append(')').toString(); - } - } diff --git a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/DefaultCompoundCurve.java b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/DefaultCompoundCurve.java index ea319daec8..37e10aadaf 100644 --- a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/DefaultCompoundCurve.java +++ b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/DefaultCompoundCurve.java @@ -96,16 +96,6 @@ public non-sealed class DefaultCompoundCurve extends AbstractGeometry implements return envUnion(curves); } - @Override - public String asText() { - final StringBuilder sb = new StringBuilder(TYPE).append(" ("); - for (int i = 0; i < curves.length; i++) { - if (i != 0) sb.append(", "); - sb.append(curves[i].asText()); - } - return sb.append(')').toString(); - } - @Override public Array getControlPoints() { throw new UnsupportedOperationException("Not supported yet."); diff --git a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/DefaultCurvePolygon.java b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/DefaultCurvePolygon.java index dd808cbac7..c86cc90970 100644 --- a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/DefaultCurvePolygon.java +++ b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/DefaultCurvePolygon.java @@ -87,12 +87,4 @@ public non-sealed class DefaultCurvePolygon extends AbstractGeometry implements throw new UnsupportedOperationException("Not supported yet."); } - @Override - public String asText() { - final StringBuilder sb = new StringBuilder(TYPE).append(" (").append(exterior.asText()); - for (final Curve interior : interiors) { - sb.append(", ").append(interior.asText()); - } - return sb.append(')').toString(); - } } diff --git a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/DefaultPolyhedralSurface.java b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/DefaultPolyhedralSurface.java index d307336018..6d82f4c9f0 100644 --- a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/DefaultPolyhedralSurface.java +++ b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/DefaultPolyhedralSurface.java @@ -95,13 +95,4 @@ public non-sealed class DefaultPolyhedralSurface<T extends Polygon> extends Abst return envUnion(patches); } - @Override - public String asText() { - final StringBuilder sb = new StringBuilder(PolyhedralSurface.TYPE).append(" ("); - for (int i = 0; i < patches.length; i++) { - if (i != 0) sb.append(", "); - sb.append(patches[i].asText()); - } - return sb.append(')').toString(); - } } diff --git a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/DefaultPolyhedron.java b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/DefaultPolyhedron.java index 14cc4bb18d..b603651aca 100644 --- a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/DefaultPolyhedron.java +++ b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/DefaultPolyhedron.java @@ -16,7 +16,6 @@ */ package org.apache.sis.geometries.internal.shared; -import java.util.ArrayList; import java.util.List; import java.util.Objects; import javax.measure.quantity.Area; @@ -86,19 +85,6 @@ public non-sealed class DefaultPolyhedron extends AbstractGeometry implements Po return exterior.getEnvelope(); } - @Override - public String asText() { - final List<MultiPolygon> shells = new ArrayList<>(interiors.size() + 1); - shells.add(exterior); - shells.addAll(interiors); - final StringBuilder sb = new StringBuilder(TYPE).append(" ("); - for (int i = 0; i < shells.size(); i++) { - if (i != 0) sb.append(", "); - sb.append(shells.get(i).asText()); - } - return sb.append(')').toString(); - } - @Override public Geometry getBoundary() { throw new UnsupportedOperationException("Not supported yet."); diff --git a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/DefaultReversedCurve.java b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/DefaultReversedCurve.java index d00b5d1488..efcddf5bce 100644 --- a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/DefaultReversedCurve.java +++ b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/DefaultReversedCurve.java @@ -128,11 +128,6 @@ public non-sealed class DefaultReversedCurve extends AbstractGeometry implements return base.getEnvelope(); } - @Override - public String asText() { - return base.asText(); - } - @Override public Array getControlPoints() { return base.getControlPoints(); diff --git a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/DefaultReversedSurface.java b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/DefaultReversedSurface.java index b89cca5147..fa6730f8c1 100644 --- a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/DefaultReversedSurface.java +++ b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/DefaultReversedSurface.java @@ -101,8 +101,4 @@ public non-sealed class DefaultReversedSurface extends AbstractGeometry implemen return base.getEnvelope(); } - @Override - public String asText() { - return base.asText(); - } } diff --git a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/DefaultTriangle.java b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/DefaultTriangle.java index 257544b97a..acb9c10426 100644 --- a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/DefaultTriangle.java +++ b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/DefaultTriangle.java @@ -73,9 +73,4 @@ public non-sealed class DefaultTriangle extends DefaultPolygon implements Triang return 7 * exterior.hashCode(); } - @Override - public String asText() { - return Triangle.super.asText(); - } - } diff --git a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/DefaultTriangulatedSurface.java b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/DefaultTriangulatedSurface.java index 4991032ff5..f581ab4cf0 100644 --- a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/DefaultTriangulatedSurface.java +++ b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/DefaultTriangulatedSurface.java @@ -43,12 +43,4 @@ public non-sealed class DefaultTriangulatedSurface extends DefaultPolyhedralSurf public String getGeometryType() { return TIN.TYPE; } - - /** - * Delegates to {@link TIN#asText()}, which formats the triangles as a WKT {@code TIN}. - */ - @Override - public String asText() { - return TIN.super.asText(); - } } diff --git a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/operation/triangulate/delaunay/Delaunay.java b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/operation/triangulate/delaunay/Delaunay.java index 192ae45310..b6bee56335 100644 --- a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/operation/triangulate/delaunay/Delaunay.java +++ b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/operation/triangulate/delaunay/Delaunay.java @@ -427,7 +427,6 @@ public final class Delaunay { * @throws OperationException if an algorithm exception occurs */ private OrientedEdge insertEdge(OrientedEdge A_B, Vertex Z, boolean isContraint) throws OperationException { -// System.out.println("ee " + A_B.getTriangle().asTextPolygon() +" LINESTRING(" + A_B.getStart().getPosition().get(0) +" "+ A_B.getStart().getPosition().get(1) + ", " + Z.getPosition().get(0) +" "+ Z.getPosition().get(1) + ")"); //find the starting triangle /* diff --git a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/point/MultiPoint.java b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/point/MultiPoint.java index 764c6095b4..516c80ae84 100644 --- a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/point/MultiPoint.java +++ b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/point/MultiPoint.java @@ -22,7 +22,6 @@ import java.util.Set; import org.apache.sis.geometries.GeometryCollection; import org.apache.sis.geometries.GeometryType; import org.apache.sis.geometries.Point; -import org.apache.sis.geometries.internal.shared.AbstractGeometry; import org.apache.sis.geometries.internal.shared.DefaultMultiPoint; import org.apache.sis.geometries.internal.shared.DefaultRawMultiPoint; import org.apache.sis.geometries.mesh.MeshPrimitive; @@ -115,17 +114,4 @@ public sealed interface MultiPoint<T extends Point> extends GeometryCollection<T }; } - @Override - default String asText() { int dimension = getDimension(); - final StringBuilder sb = new StringBuilder(TYPE); - sb.append('('); - for (int i = 0, n = getNumGeometries(); i < n; i++) { - T point = getGeometryN(i); - if (i > 0) sb.append(','); - AbstractGeometry.toText(sb, point.getPosition()); - } - sb.append(')'); - return sb.toString(); - } - } diff --git a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/surface/CurvePolygon.java b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/surface/CurvePolygon.java index 9b5151bc55..3bf17c75f7 100644 --- a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/surface/CurvePolygon.java +++ b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/surface/CurvePolygon.java @@ -20,7 +20,6 @@ import java.util.List; import org.apache.sis.geometries.AttributesType; import org.apache.sis.geometries.Curve; import org.apache.sis.geometries.Surface; -import org.apache.sis.geometries.internal.shared.AbstractGeometry; import org.apache.sis.geometries.internal.shared.DefaultCurvePolygon; import static org.opengis.annotation.Specification.ISO_19107; import org.opengis.annotation.UML; @@ -152,19 +151,4 @@ public sealed interface CurvePolygon extends Surface throw new UnsupportedOperationException(); } - @Override - default String asText() { - final StringBuilder sb = new StringBuilder("POLYGON (("); - AbstractGeometry.toText(sb, getExteriorRing().asLine(null, null).getDataPoints()); - sb.append(')'); - for (int i = 0, n = getNumInteriorRing(); i < n; i++) { - if (i != 0) sb.append(','); - sb.append('('); - AbstractGeometry.toText(sb, getInteriorRingN(i).asLine(null, null).getDataPoints()); - sb.append(')'); - } - sb.append(')'); - return sb.toString(); - } - } diff --git a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/surface/MultiPolygon.java b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/surface/MultiPolygon.java index 7008210971..a6828f1581 100644 --- a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/surface/MultiPolygon.java +++ b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/surface/MultiPolygon.java @@ -18,7 +18,6 @@ package org.apache.sis.geometries.surface; import java.util.Set; import org.apache.sis.geometries.GeometryType; -import org.apache.sis.geometries.internal.shared.AbstractGeometry; import org.apache.sis.geometries.internal.shared.DefaultMultiPolygon; @@ -58,24 +57,4 @@ public sealed interface MultiPolygon extends MultiSurface<Polygon> return Set.of(GeometryType.POLYGON); } - @Override - default String asText() { - final StringBuilder sb = new StringBuilder("MULTIPOLYGON ("); - for (int k = 0, kn = getNumGeometries(); k < kn; k++){ - if (k > 0) sb.append(','); - final Polygon polygon = getGeometryN(k); - sb.append("(("); - AbstractGeometry.toText(sb, polygon.getExteriorRing().asLine(null, null).getDataPoints()); - sb.append(')'); - for (int i = 0, n = polygon.getNumInteriorRing(); i < n; i++) { - if (i != 0) sb.append(','); - sb.append('('); - AbstractGeometry.toText(sb, polygon.getInteriorRingN(i).getDataPoints()); - sb.append(')'); - } - sb.append(')'); - } - sb.append(')'); - return sb.toString(); - } } diff --git a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/surface/Polygon.java b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/surface/Polygon.java index af99ba0fb9..cf4dd562d9 100644 --- a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/surface/Polygon.java +++ b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/surface/Polygon.java @@ -21,7 +21,6 @@ import org.apache.sis.geometries.AttributesType; import org.apache.sis.geometries.Surface; import org.apache.sis.geometries.SurfaceInterpolation; import org.apache.sis.geometries.curve.LinearRing; -import org.apache.sis.geometries.internal.shared.AbstractGeometry; import org.apache.sis.geometries.internal.shared.DefaultPolygon; import static org.opengis.annotation.Specification.ISO_19107; import org.opengis.annotation.UML; @@ -172,19 +171,4 @@ public sealed interface Polygon extends Surface throw new UnsupportedOperationException(); } - @Override - default String asText() { - final StringBuilder sb = new StringBuilder("POLYGON (("); - AbstractGeometry.toText(sb, getExteriorRing().asLine(null, null).getDataPoints()); - sb.append(')'); - for (int i = 0, n = getNumInteriorRing(); i < n; i++) { - if (i != 0) sb.append(','); - sb.append('('); - AbstractGeometry.toText(sb, getInteriorRingN(i).getDataPoints()); - sb.append(')'); - } - sb.append(')'); - return sb.toString(); - } - } diff --git a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/surface/TIN.java b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/surface/TIN.java index 837b8c47bc..98dba1422d 100644 --- a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/surface/TIN.java +++ b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/surface/TIN.java @@ -17,12 +17,9 @@ package org.apache.sis.geometries.surface; import java.util.List; -import org.apache.sis.geometries.DataPoints; import org.apache.sis.geometries.SurfaceInterpolation; -import org.apache.sis.geometries.internal.shared.AbstractGeometry; import org.apache.sis.geometries.internal.shared.DefaultTriangulatedSurface; import org.apache.sis.geometries.mesh.MeshPrimitive; -import org.apache.sis.maths.Tuple; /** @@ -40,6 +37,9 @@ public sealed interface TIN extends TriangulatedSurface<Triangle> static final String TYPE = "TIN"; + /** + * Returns {@value #TYPE}. + */ @Override default String getGeometryType() { return TYPE; @@ -56,42 +56,4 @@ public sealed interface TIN extends TriangulatedSurface<Triangle> return List.of(SurfaceInterpolation.TIN); } - /** - * Produce a Well Known Text representation of this TIN. - * - * @return WKT string - */ - default String asText() { - final StringBuilder sb = new StringBuilder("TIN("); - boolean first = true; - Tuple corner; - for (int i = 0, n = getNumPatches(); i < n; i++) { - Triangle triangle = getPatchN(i); - if (first) { - first = false; - } else { - sb.append(','); - } - sb.append("(("); - final DataPoints points = triangle.getExteriorRing().getDataPoints(); - corner = points.getPosition(0); - AbstractGeometry.toText(sb, corner); - - sb.append(','); - corner = points.getPosition(1); - AbstractGeometry.toText(sb, corner); - - sb.append(','); - corner = points.getPosition(2); - AbstractGeometry.toText(sb, corner); - - sb.append(','); - corner = points.getPosition(0); - AbstractGeometry.toText(sb, corner); - - sb.append("))"); - } - sb.append(')'); - return sb.toString(); - } } diff --git a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/surface/Triangle.java b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/surface/Triangle.java index 7465fe162f..d071af9728 100644 --- a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/surface/Triangle.java +++ b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/surface/Triangle.java @@ -24,7 +24,6 @@ import org.apache.sis.geometries.BBox; import org.apache.sis.geometries.DataPoints; import org.apache.sis.geometries.Point; import org.apache.sis.geometries.curve.LinearRing; -import org.apache.sis.geometries.internal.shared.AbstractGeometry; import org.apache.sis.geometries.internal.shared.DefaultTriangle; import org.apache.sis.geometries.operation.simplify.greedyinsert.WTriangle; import org.apache.sis.geometries.operation.triangulate.delaunay.OrientedTriangle; @@ -72,14 +71,16 @@ public sealed interface Triangle extends Polygon OrientedTriangle { + static final String TYPE = "TRIANGLE"; + /** - * Returns {@code "TRIANGLE"}. + * Returns {@value #TYPE}. * * @see ISO 19107:2019 - 6.4.4.23 */ @Override default String getGeometryType() { - return "TRIANGLE"; + return TYPE; } /** @@ -255,25 +256,6 @@ public sealed interface Triangle extends Polygon return null; } - @Override - default String asText() { - final DataPoints exterior = getExteriorRing().getDataPoints(); - final StringBuilder sb = new StringBuilder("TRIANGLE (("); - AbstractGeometry.toText(sb, exterior.getPosition(0)); - sb.append(','); - AbstractGeometry.toText(sb, exterior.getPosition(1)); - sb.append(','); - AbstractGeometry.toText(sb, exterior.getPosition(2)); - sb.append("))"); - return sb.toString(); - } - - /** - */ - default String asTextPolygon() { - return Polygon.super.asText(); - } - public static final class InterpolatedPoint implements Point { private static final double[][] CORNERS = new double[][]{ diff --git a/incubator/src/org.apache.sis.geometry/test/org/apache/sis/geometries/PreparedTINTest.java b/incubator/src/org.apache.sis.geometry/test/org/apache/sis/geometries/PreparedTINTest.java index 4301893b99..4efcec33dc 100644 --- a/incubator/src/org.apache.sis.geometry/test/org/apache/sis/geometries/PreparedTINTest.java +++ b/incubator/src/org.apache.sis.geometry/test/org/apache/sis/geometries/PreparedTINTest.java @@ -93,9 +93,9 @@ public class PreparedTINTest { assertEquals(2, tin.getPatches(env).count()); assertEquals(4, tin.getNumPatches()); - assertEquals("TRIANGLE ((0.0 1.0 2.0,3.0 4.0 5.0,6.0 7.0 8.0))", tin.getPatchN(0).asText()); - assertEquals("TRIANGLE ((6.0 7.0 8.0,9.0 10.0 11.0,3.0 4.0 5.0))", tin.getPatchN(1).asText()); - assertEquals("TRIANGLE ((0.0 1.0 2.0,3.0 4.0 5.0,6.0 7.0 8.0))", tin.getPatchN(2).asText()); - assertEquals("TRIANGLE ((6.0 7.0 8.0,9.0 10.0 11.0,3.0 4.0 5.0))", tin.getPatchN(3).asText()); + assertEquals("TRIANGLE Z ((0 1 2, 3 4 5, 6 7 8, 0 1 2))", tin.getPatchN(0).asText()); + assertEquals("TRIANGLE Z ((6 7 8, 9 10 11, 3 4 5, 6 7 8))", tin.getPatchN(1).asText()); + assertEquals("TRIANGLE Z ((0 1 2, 3 4 5, 6 7 8, 0 1 2))", tin.getPatchN(2).asText()); + assertEquals("TRIANGLE Z ((6 7 8, 9 10 11, 3 4 5, 6 7 8))", tin.getPatchN(3).asText()); } } diff --git a/incubator/src/org.apache.sis.geometry/test/org/apache/sis/geometries/adapter/WellKnownTextTest.java b/incubator/src/org.apache.sis.geometry/test/org/apache/sis/geometries/adapter/WellKnownTextTest.java new file mode 100644 index 0000000000..43f4f25ec8 --- /dev/null +++ b/incubator/src/org.apache.sis.geometry/test/org/apache/sis/geometries/adapter/WellKnownTextTest.java @@ -0,0 +1,299 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.sis.geometries.adapter; + +import org.apache.sis.geometries.AttributesType; +import org.apache.sis.geometries.DataPoints; +import org.apache.sis.geometries.Empty; +import org.apache.sis.geometries.Geometries; +import org.apache.sis.geometries.Geometry; +import org.apache.sis.geometries.GeometryCollection; +import org.apache.sis.geometries.GeometryFactory; +import org.apache.sis.geometries.Point; +import org.apache.sis.geometries.curve.CircularString; +import org.apache.sis.geometries.curve.CompoundCurve; +import org.apache.sis.geometries.curve.LineString; +import org.apache.sis.geometries.curve.MultiCurve; +import org.apache.sis.geometries.curve.MultiLineString; +import org.apache.sis.geometries.point.MultiPoint; +import org.apache.sis.geometries.surface.CurvePolygon; +import org.apache.sis.geometries.surface.MultiPolygon; +import org.apache.sis.geometries.surface.MultiSurface; +import org.apache.sis.geometries.surface.Polygon; +import org.apache.sis.geometries.surface.PolyhedralSurface; +import org.apache.sis.geometries.surface.TIN; +import org.apache.sis.geometries.surface.Triangle; +import org.apache.sis.maths.NDArrays; +import org.apache.sis.maths.SampleSystem; +import org.opengis.referencing.crs.CoordinateReferenceSystem; + +// Test dependencies +import static org.junit.jupiter.api.Assertions.*; +import org.junit.jupiter.api.Test; + + +/** + * Tests {@link WellKnownText}. + * + * @author Johann Sorel (Geomatys) + */ +public final class WellKnownTextTest { + /** + * The codec under test, writing the shortest representation of every ordinate. + */ + private final WellKnownText wkt = new WellKnownText(); + + /** + * Creates a new test case. + */ + public WellKnownTextTest() { + } + + /** + * Verifies that the given text decodes to a geometry of the expected type, and that encoding + * that geometry gives the text back. Every text given here is in the canonical form the + * encoder writes, so the round trip is an equality and not merely an equivalence. + */ + private void assertRoundTrip(final Class<? extends Geometry> type, final String text) { + final Geometry geometry = wkt.decode(text); + assertInstanceOf(type, geometry, text); + assertEquals(text, wkt.encode(geometry), "Round trip of " + text); + assertEquals(text, geometry.asText(), "Geometry.asText() of " + text); + } + + /** + * Tests the two dimensional form of every supported geometry type. + */ + @Test + public void testRoundTrip2D() { + assertRoundTrip(Point.class, "POINT (1 2)"); + assertRoundTrip(LineString.class, "LINESTRING (0 0, 1 1, 2 0)"); + assertRoundTrip(CircularString.class, "CIRCULARSTRING (0 0, 1 1, 2 0)"); + assertRoundTrip(CompoundCurve.class, "COMPOUNDCURVE ((0 0, 1 1), CIRCULARSTRING (1 1, 2 2, 3 1))"); + assertRoundTrip(Polygon.class, "POLYGON ((0 0, 4 0, 4 4, 0 4, 0 0), (1 1, 2 1, 2 2, 1 2, 1 1))"); + assertRoundTrip(Triangle.class, "TRIANGLE ((0 0, 1 0, 0 1, 0 0))"); + assertRoundTrip(CurvePolygon.class, "CURVEPOLYGON (CIRCULARSTRING (0 0, 2 0, 2 2, 0 2, 0 0))"); + assertRoundTrip(MultiPoint.class, "MULTIPOINT ((1 2), (3 4))"); + assertRoundTrip(MultiLineString.class, "MULTILINESTRING ((0 0, 1 1), (2 2, 3 3))"); + assertRoundTrip(MultiPolygon.class, "MULTIPOLYGON (((0 0, 1 0, 1 1, 0 0)), ((2 2, 3 2, 3 3, 2 2)))"); + assertRoundTrip(GeometryCollection.class, "GEOMETRYCOLLECTION (POINT (1 2), LINESTRING (0 0, 1 1))"); + } + + /** + * Tests the types whose members keep their own keyword, and the surface patch types. + * Those are the extensions of ISO 13249-3 over OGC Simple Feature Access. + */ + @Test + public void testRoundTripCurvedAndPatches() { + assertRoundTrip(MultiCurve.class, + "MULTICURVE (LINESTRING (0 0, 1 1), CIRCULARSTRING (1 1, 2 2, 3 1))"); + assertRoundTrip(MultiSurface.class, + "MULTISURFACE (POLYGON ((0 0, 1 0, 1 1, 0 0)), CURVEPOLYGON (CIRCULARSTRING (0 0, 2 0, 2 2, 0 2, 0 0)))"); + assertRoundTrip(PolyhedralSurface.class, + "POLYHEDRALSURFACE Z (((0 0 0, 1 0 0, 1 1 0, 0 0 0)), ((0 0 0, 1 1 0, 0 1 0, 0 0 0)))"); + assertRoundTrip(TIN.class, + "TIN Z (((0 0 0, 1 0 0, 1 1 0, 0 0 0)), ((0 0 0, 1 1 0, 0 1 0, 0 0 0)))"); + } + + /** + * Tests the {@code Z}, {@code M} and {@code ZM} flags. + */ + @Test + public void testDimensionFlags() { + assertRoundTrip(Point.class, "POINT Z (1 2 3)"); + assertRoundTrip(Point.class, "POINT M (1 2 3)"); + assertRoundTrip(Point.class, "POINT ZM (1 2 3 4)"); + assertRoundTrip(LineString.class, "LINESTRING Z (0 0 0, 1 1 1)"); + assertRoundTrip(LineString.class, "LINESTRING M (0 0 5, 1 1 6)"); + assertRoundTrip(LineString.class, "LINESTRING ZM (0 0 0 5, 1 1 1 6)"); + assertRoundTrip(MultiPoint.class, "MULTIPOINT ZM ((1 2 3 4), (5 6 7 8))"); + assertRoundTrip(Polygon.class, "POLYGON Z ((0 0 0, 1 0 0, 1 1 0, 0 0 0))"); + /* + * A geometry collection repeats the flag on each of its members, since each member + * carries its own keyword. + */ + assertRoundTrip(GeometryCollection.class, "GEOMETRYCOLLECTION Z (POINT Z (1 2 3))"); + } + + /** + * Verifies that the measure is read into the dedicated attribute rather than into the + * position, and that the position keeps the number of dimensions the flag announces. + */ + @Test + public void testMeasureIsASeparateAttribute() { + final LineString line = assertInstanceOf(LineString.class, wkt.decode("LINESTRING ZM (0 1 2 5, 3 4 5 6)")); + final DataPoints points = line.getDataPoints(); + assertEquals(3, points.getDimension(), "Position dimension"); + assertEquals(2, points.size()); + assertArrayEquals(new double[] {0, 1, 2}, points.getPosition(0).toArrayDouble()); + assertArrayEquals(new double[] {3, 4, 5}, points.getPosition(1).toArrayDouble()); + assertTrue(points.getAttributesType().getAttributeNames().contains(AttributesType.ATT_M)); + assertEquals(5.0, points.getAttribute(0, AttributesType.ATT_M).get(0)); + assertEquals(6.0, points.getAttribute(1, AttributesType.ATT_M).get(0)); + /* + * Without the flag, a 2-dimensional text carries no measure at all. + */ + final LineString plain = assertInstanceOf(LineString.class, wkt.decode("LINESTRING (0 1, 3 4)")); + assertFalse(plain.getDataPoints().getAttributesType().getAttributeNames().contains(AttributesType.ATT_M)); + } + + /** + * Tests the {@code EMPTY} form of every supported geometry type. + */ + @Test + public void testEmpty() { + assertRoundTrip(LineString.class, "LINESTRING EMPTY"); + assertRoundTrip(CircularString.class, "CIRCULARSTRING EMPTY"); + assertRoundTrip(CompoundCurve.class, "COMPOUNDCURVE EMPTY"); + assertRoundTrip(Polygon.class, "POLYGON EMPTY"); + assertRoundTrip(Triangle.class, "TRIANGLE EMPTY"); + assertRoundTrip(CurvePolygon.class, "CURVEPOLYGON EMPTY"); + assertRoundTrip(TIN.class, "TIN EMPTY"); + assertRoundTrip(MultiPoint.class, "MULTIPOINT EMPTY"); + assertRoundTrip(MultiLineString.class, "MULTILINESTRING EMPTY"); + assertRoundTrip(MultiPolygon.class, "MULTIPOLYGON EMPTY"); + assertRoundTrip(GeometryCollection.class, "GEOMETRYCOLLECTION EMPTY"); + } + + /** + * Verifies that an {@link Empty} geometry, which has no type of its own, is written as the + * type-less empty form of the grammar. + */ + @Test + public void testEmptyGeometry() { + final Geometry empty = GeometryFactory.createEmpty(Geometries.getUndefinedCRS(2)); + assertEquals("GEOMETRYCOLLECTION EMPTY", wkt.encode(empty)); + } + + /** + * Verifies the one empty form which does not round-trip: the model has no empty point, since + * a point holds exactly one position, so {@code POINT EMPTY} becomes an {@link Empty}. + */ + @Test + public void testEmptyPoint() { + assertInstanceOf(Empty.class, wkt.decode("POINT EMPTY")); + assertInstanceOf(Empty.class, wkt.decode("POINT Z EMPTY")); + assertEquals("GEOMETRYCOLLECTION EMPTY", wkt.encode(wkt.decode("POINT EMPTY"))); + assertMalformed("MULTIPOINT (EMPTY, (1 2))"); + } + + /** + * Tests the forms which the grammar allows but the encoder does not write: + * lower case keywords, loose whitespace, a multi point without parentheses around its points, + * a bare coordinate list as a line string, and the {@code GEOMCOLLECTION} alias. + */ + @Test + public void testAlternateForms() { + assertEquals("POINT (1 2)", wkt.encode(wkt.decode("point(1 2)"))); + assertEquals("POINT (1 2)", wkt.encode(wkt.decode(" Point ( 1 2 ) "))); + assertEquals("MULTIPOINT ((1 2), (3 4))", wkt.encode(wkt.decode("MULTIPOINT (1 2, 3 4)"))); + assertEquals("MULTICURVE (LINESTRING (0 0, 1 1))", wkt.encode(wkt.decode("MULTICURVE ((0 0, 1 1))"))); + assertEquals("MULTISURFACE (POLYGON ((0 0, 1 0, 1 1, 0 0)))", + wkt.encode(wkt.decode("MULTISURFACE (((0 0, 1 0, 1 1, 0 0)))"))); + assertEquals("GEOMETRYCOLLECTION (POINT (1 2))", wkt.encode(wkt.decode("GEOMCOLLECTION (POINT (1 2))"))); + /* + * Exponents and explicit signs are part of the lexical form of a number. + */ + assertEquals("POINT (1000 -2)", wkt.encode(wkt.decode("POINT (1e3 -2)"))); + assertEquals("POINT (1.5 2)", wkt.encode(wkt.decode("POINT (+1.5 +2.0)"))); + } + + /** + * Tests the number of decimal digits given to the constructor. + */ + @Test + public void testDecimalPrecision() { + final Geometry geometry = wkt.decode("POINT (1.23456 2.5)"); + assertEquals("POINT (1.23456 2.5)", new WellKnownText().encode(geometry)); + assertEquals("POINT (1.235 2.5)", new WellKnownText(3).encode(geometry)); + assertEquals("POINT (1.2 2.5)", new WellKnownText(1).encode(geometry)); + assertEquals("POINT (1 3)", new WellKnownText(0).encode(geometry)); + assertThrows(IllegalArgumentException.class, () -> new WellKnownText(-1)); + } + + /** + * Verifies that a whole ordinate is written without a fractional part, + * which the model would otherwise expose as {@code 1.0}. + */ + @Test + public void testWholeNumbers() { + final Point point = GeometryFactory.createPoint(Geometries.getUndefinedCRS(3), 1, 2, 3); + assertEquals("POINT Z (1 2 3)", wkt.encode(point)); + } + + /** + * Tests decoding in a coordinate reference system given by the caller. + */ + @Test + public void testDecodeWithCRS() { + final CoordinateReferenceSystem crs = Geometries.getUndefinedCRS(3); + final Geometry geometry = wkt.decode("POINT Z (1 2 3)", crs); + assertSame(crs, geometry.getCoordinateReferenceSystem()); + /* + * The number of ordinates in the text and the dimension of the system must agree. + */ + assertThrows(IllegalArgumentException.class, () -> wkt.decode("POINT (1 2)", crs)); + } + + /** + * Verifies that the geometry types which the format does not define are rejected + * rather than written in an invented syntax. + */ + @Test + public void testUnsupportedType() { + final DataPoints points = GeometryFactory.createSequence( + NDArrays.of(SampleSystem.of(Geometries.getUndefinedCRS(2)), 0, 0, 1, 1)); + assertThrows(IllegalArgumentException.class, () -> wkt.encode(GeometryFactory.createGeodesic(points))); + assertThrows(IllegalArgumentException.class, () -> wkt.encode(GeometryFactory.createRhumb(points))); + } + + /** + * Verifies that a geometry whose positions have more than three dimensions is rejected, + * since the format has no way to tell them from a measure. + */ + @Test + public void testTooManyDimensions() { + final Point point = GeometryFactory.createPoint(Geometries.getUndefinedCRS(4), 1, 2, 3, 4); + assertThrows(IllegalArgumentException.class, () -> wkt.encode(point)); + } + + /** + * Tests the rejection of malformed texts. + */ + @Test + public void testMalformed() { + assertMalformed(""); // No keyword at all. + assertMalformed("FOO (1 2)"); // Unknown keyword. + assertMalformed("POINT (1 2"); // Unbalanced parenthesis. + assertMalformed("POINT 1 2)"); // Missing opening parenthesis. + assertMalformed("POINT ()"); // No coordinate. + assertMalformed("POINT (1 2) LINESTRING (0 0, 1 1)"); // Trailing text. + assertMalformed("POINT (1 2 3 4)"); // Ambiguous without a flag. + assertMalformed("LINESTRING (0 0, 1 1 1)"); // Inconsistent tuple width. + assertMalformed("POINT Z (1 2)"); // Flag contradicted by the tuple. + assertMalformed("GEOMETRYCOLLECTION Z (POINT (1 2))"); // Flag contradicted by a member. + assertMalformed("TRIANGLE ((0 0, 1 0, 0 1, 0 0), (0 0, 1 0, 0 1, 0 0))"); // Interior ring. + assertMalformed("MULTICURVE (POLYGON ((0 0, 1 0, 1 1, 0 0)))"); // Member is not a curve. + } + + /** + * Verifies that the given text is rejected as malformed. + */ + private void assertMalformed(final String text) { + assertThrows(IllegalArgumentException.class, () -> wkt.decode(text), text); + } +} diff --git a/incubator/src/org.apache.sis.geometry/test/org/apache/sis/geometries/mesh/PrimitiveVisitorTest.java b/incubator/src/org.apache.sis.geometry/test/org/apache/sis/geometries/mesh/PrimitiveVisitorTest.java index 5f095c0ba0..9c74f14681 100644 --- a/incubator/src/org.apache.sis.geometry/test/org/apache/sis/geometries/mesh/PrimitiveVisitorTest.java +++ b/incubator/src/org.apache.sis.geometry/test/org/apache/sis/geometries/mesh/PrimitiveVisitorTest.java @@ -117,10 +117,10 @@ public class PrimitiveVisitorTest { 9,10,11)); final List<String> expected = Arrays.asList( - "LINESTRING (0.0 1.0 2.0, 3.0 4.0 5.0)", + "LINESTRING Z (0 1 2, 3 4 5)", "V:0 POSITION[0.0, 1.0, 2.0]", "V:1 POSITION[3.0, 4.0, 5.0]", - "LINESTRING (6.0 7.0 8.0, 9.0 10.0 11.0)", + "LINESTRING Z (6 7 8, 9 10 11)", "V:2 POSITION[6.0, 7.0, 8.0]", "V:3 POSITION[9.0, 10.0, 11.0]" ); @@ -129,8 +129,8 @@ public class PrimitiveVisitorTest { //test geometry collection assertEquals(2, geometry.getNumGeometries()); - assertEquals("LINESTRING (0.0 1.0 2.0, 3.0 4.0 5.0)", geometry.getGeometryN(0).asText()); - assertEquals("LINESTRING (6.0 7.0 8.0, 9.0 10.0 11.0)", geometry.getGeometryN(1).asText()); + assertEquals("LINESTRING Z (0 1 2, 3 4 5)", geometry.getGeometryN(0).asText()); + assertEquals("LINESTRING Z (6 7 8, 9 10 11)", geometry.getGeometryN(1).asText()); } /** @@ -148,13 +148,13 @@ public class PrimitiveVisitorTest { geometry.setIndex(NDArrays.ofUnsigned(1, 0, 1, 2, 3, 1, 3)); final List<String> expected = Arrays.asList( - "LINESTRING (0.0 1.0 2.0, 3.0 4.0 5.0)", + "LINESTRING Z (0 1 2, 3 4 5)", "V:0 POSITION[0.0, 1.0, 2.0]", "V:1 POSITION[3.0, 4.0, 5.0]", - "LINESTRING (6.0 7.0 8.0, 9.0 10.0 11.0)", + "LINESTRING Z (6 7 8, 9 10 11)", "V:2 POSITION[6.0, 7.0, 8.0]", "V:3 POSITION[9.0, 10.0, 11.0]", - "LINESTRING (3.0 4.0 5.0, 9.0 10.0 11.0)", + "LINESTRING Z (3 4 5, 9 10 11)", "V:1 POSITION[3.0, 4.0, 5.0]", "V:3 POSITION[9.0, 10.0, 11.0]" ); @@ -163,9 +163,9 @@ public class PrimitiveVisitorTest { //test geometry collection assertEquals(3, geometry.getNumGeometries()); - assertEquals("LINESTRING (0.0 1.0 2.0, 3.0 4.0 5.0)", geometry.getGeometryN(0).asText()); - assertEquals("LINESTRING (6.0 7.0 8.0, 9.0 10.0 11.0)", geometry.getGeometryN(1).asText()); - assertEquals("LINESTRING (3.0 4.0 5.0, 9.0 10.0 11.0)", geometry.getGeometryN(2).asText()); + assertEquals("LINESTRING Z (0 1 2, 3 4 5)", geometry.getGeometryN(0).asText()); + assertEquals("LINESTRING Z (6 7 8, 9 10 11)", geometry.getGeometryN(1).asText()); + assertEquals("LINESTRING Z (3 4 5, 9 10 11)", geometry.getGeometryN(2).asText()); } /** @@ -182,13 +182,13 @@ public class PrimitiveVisitorTest { 9,10,11)); final List<String> expected = Arrays.asList( - "LINESTRING (0.0 1.0 2.0, 3.0 4.0 5.0)", + "LINESTRING Z (0 1 2, 3 4 5)", "V:0 POSITION[0.0, 1.0, 2.0]", "V:1 POSITION[3.0, 4.0, 5.0]", - "LINESTRING (3.0 4.0 5.0, 6.0 7.0 8.0)", + "LINESTRING Z (3 4 5, 6 7 8)", "V:1 POSITION[3.0, 4.0, 5.0]", "V:2 POSITION[6.0, 7.0, 8.0]", - "LINESTRING (6.0 7.0 8.0, 9.0 10.0 11.0)", + "LINESTRING Z (6 7 8, 9 10 11)", "V:2 POSITION[6.0, 7.0, 8.0]", "V:3 POSITION[9.0, 10.0, 11.0]" ); @@ -196,7 +196,7 @@ public class PrimitiveVisitorTest { //test geometry as LineString assertTrue(geometry instanceof LineString); - assertEquals("LINESTRING (0.0 1.0 2.0, 3.0 4.0 5.0, 6.0 7.0 8.0, 9.0 10.0 11.0)", geometry.asText()); + assertEquals("LINESTRING Z (0 1 2, 3 4 5, 6 7 8, 9 10 11)", geometry.asText()); } /** @@ -214,19 +214,19 @@ public class PrimitiveVisitorTest { geometry.setIndex(NDArrays.ofUnsigned(1, 0, 1, 2, 3, 0, 2)); final List<String> expected = Arrays.asList( - "LINESTRING (0.0 1.0 2.0, 3.0 4.0 5.0)", + "LINESTRING Z (0 1 2, 3 4 5)", "V:0 POSITION[0.0, 1.0, 2.0]", "V:1 POSITION[3.0, 4.0, 5.0]", - "LINESTRING (3.0 4.0 5.0, 6.0 7.0 8.0)", + "LINESTRING Z (3 4 5, 6 7 8)", "V:1 POSITION[3.0, 4.0, 5.0]", "V:2 POSITION[6.0, 7.0, 8.0]", - "LINESTRING (6.0 7.0 8.0, 9.0 10.0 11.0)", + "LINESTRING Z (6 7 8, 9 10 11)", "V:2 POSITION[6.0, 7.0, 8.0]", "V:3 POSITION[9.0, 10.0, 11.0]", - "LINESTRING (9.0 10.0 11.0, 0.0 1.0 2.0)", + "LINESTRING Z (9 10 11, 0 1 2)", "V:3 POSITION[9.0, 10.0, 11.0]", "V:0 POSITION[0.0, 1.0, 2.0]", - "LINESTRING (0.0 1.0 2.0, 6.0 7.0 8.0)", + "LINESTRING Z (0 1 2, 6 7 8)", "V:0 POSITION[0.0, 1.0, 2.0]", "V:2 POSITION[6.0, 7.0, 8.0]" ); @@ -234,7 +234,7 @@ public class PrimitiveVisitorTest { //test geometry as LineString assertTrue(geometry instanceof LineString); - assertEquals("LINESTRING (0.0 1.0 2.0, 3.0 4.0 5.0, 6.0 7.0 8.0, 9.0 10.0 11.0, 0.0 1.0 2.0, 6.0 7.0 8.0)", geometry.asText()); + assertEquals("LINESTRING Z (0 1 2, 3 4 5, 6 7 8, 9 10 11, 0 1 2, 6 7 8)", geometry.asText()); } /** @@ -253,11 +253,11 @@ public class PrimitiveVisitorTest { 15,16,17)); final List<String> expected = Arrays.asList( - "TRIANGLE ((0.0 1.0 2.0,3.0 4.0 5.0,6.0 7.0 8.0))", + "TRIANGLE Z ((0 1 2, 3 4 5, 6 7 8, 0 1 2))", "V:0 POSITION[0.0, 1.0, 2.0]", "V:1 POSITION[3.0, 4.0, 5.0]", "V:2 POSITION[6.0, 7.0, 8.0]", - "TRIANGLE ((9.0 10.0 11.0,12.0 13.0 14.0,15.0 16.0 17.0))", + "TRIANGLE Z ((9 10 11, 12 13 14, 15 16 17, 9 10 11))", "V:3 POSITION[9.0, 10.0, 11.0]", "V:4 POSITION[12.0, 13.0, 14.0]", "V:5 POSITION[15.0, 16.0, 17.0]" @@ -268,8 +268,8 @@ public class PrimitiveVisitorTest { //test geometry as TIN assertTrue(geometry instanceof TIN); assertEquals(2, geometry.getNumPatches()); - assertEquals("TRIANGLE ((0.0 1.0 2.0,3.0 4.0 5.0,6.0 7.0 8.0))", geometry.getPatchN(0).asText()); - assertEquals("TRIANGLE ((9.0 10.0 11.0,12.0 13.0 14.0,15.0 16.0 17.0))", geometry.getPatchN(1).asText()); + assertEquals("TRIANGLE Z ((0 1 2, 3 4 5, 6 7 8, 0 1 2))", geometry.getPatchN(0).asText()); + assertEquals("TRIANGLE Z ((9 10 11, 12 13 14, 15 16 17, 9 10 11))", geometry.getPatchN(1).asText()); } /** @@ -287,11 +287,11 @@ public class PrimitiveVisitorTest { geometry.setIndex(NDArrays.ofUnsigned(1, 0, 1, 2, 2, 3, 1)); final List<String> expected = Arrays.asList( - "TRIANGLE ((0.0 1.0 2.0,3.0 4.0 5.0,6.0 7.0 8.0))", + "TRIANGLE Z ((0 1 2, 3 4 5, 6 7 8, 0 1 2))", "V:0 POSITION[0.0, 1.0, 2.0]", "V:1 POSITION[3.0, 4.0, 5.0]", "V:2 POSITION[6.0, 7.0, 8.0]", - "TRIANGLE ((6.0 7.0 8.0,9.0 10.0 11.0,3.0 4.0 5.0))", + "TRIANGLE Z ((6 7 8, 9 10 11, 3 4 5, 6 7 8))", "V:2 POSITION[6.0, 7.0, 8.0]", "V:3 POSITION[9.0, 10.0, 11.0]", "V:1 POSITION[3.0, 4.0, 5.0]" @@ -302,8 +302,8 @@ public class PrimitiveVisitorTest { //test geometry as TIN assertTrue(geometry instanceof TIN); assertEquals(2, geometry.getNumPatches()); - assertEquals("TRIANGLE ((0.0 1.0 2.0,3.0 4.0 5.0,6.0 7.0 8.0))", geometry.getPatchN(0).asText()); - assertEquals("TRIANGLE ((6.0 7.0 8.0,9.0 10.0 11.0,3.0 4.0 5.0))", geometry.getPatchN(1).asText()); + assertEquals("TRIANGLE Z ((0 1 2, 3 4 5, 6 7 8, 0 1 2))", geometry.getPatchN(0).asText()); + assertEquals("TRIANGLE Z ((6 7 8, 9 10 11, 3 4 5, 6 7 8))", geometry.getPatchN(1).asText()); } /** @@ -333,19 +333,19 @@ public class PrimitiveVisitorTest { 0,-1,0)); final List<String> expected = Arrays.asList( - "TRIANGLE ((0.0 0.0 0.0,0.0 1.0 0.0,1.0 1.0 0.0))", + "TRIANGLE Z ((0 0 0, 0 1 0, 1 1 0, 0 0 0))", "V:0 POSITION[0.0, 0.0, 0.0]", "V:1 POSITION[0.0, 1.0, 0.0]", "V:2 POSITION[1.0, 1.0, 0.0]", - "TRIANGLE ((0.0 0.0 0.0,1.0 1.0 0.0,1.0 0.0 0.0))", + "TRIANGLE Z ((0 0 0, 1 1 0, 1 0 0, 0 0 0))", "V:0 POSITION[0.0, 0.0, 0.0]", "V:2 POSITION[1.0, 1.0, 0.0]", "V:3 POSITION[1.0, 0.0, 0.0]", - "TRIANGLE ((0.0 0.0 0.0,1.0 0.0 0.0,1.0 -1.0 0.0))", + "TRIANGLE Z ((0 0 0, 1 0 0, 1 -1 0, 0 0 0))", "V:0 POSITION[0.0, 0.0, 0.0]", "V:3 POSITION[1.0, 0.0, 0.0]", "V:4 POSITION[1.0, -1.0, 0.0]", - "TRIANGLE ((0.0 0.0 0.0,1.0 -1.0 0.0,0.0 -1.0 0.0))", + "TRIANGLE Z ((0 0 0, 1 -1 0, 0 -1 0, 0 0 0))", "V:0 POSITION[0.0, 0.0, 0.0]", "V:4 POSITION[1.0, -1.0, 0.0]", "V:5 POSITION[0.0, -1.0, 0.0]" @@ -356,10 +356,10 @@ public class PrimitiveVisitorTest { //test geometry as TIN assertTrue(geometry instanceof TIN); assertEquals(4, geometry.getNumPatches()); - assertEquals("TRIANGLE ((0.0 0.0 0.0,0.0 1.0 0.0,1.0 1.0 0.0))", geometry.getPatchN(0).asText()); - assertEquals("TRIANGLE ((0.0 0.0 0.0,1.0 1.0 0.0,1.0 0.0 0.0))", geometry.getPatchN(1).asText()); - assertEquals("TRIANGLE ((0.0 0.0 0.0,1.0 0.0 0.0,1.0 -1.0 0.0))", geometry.getPatchN(2).asText()); - assertEquals("TRIANGLE ((0.0 0.0 0.0,1.0 -1.0 0.0,0.0 -1.0 0.0))", geometry.getPatchN(3).asText()); + assertEquals("TRIANGLE Z ((0 0 0, 0 1 0, 1 1 0, 0 0 0))", geometry.getPatchN(0).asText()); + assertEquals("TRIANGLE Z ((0 0 0, 1 1 0, 1 0 0, 0 0 0))", geometry.getPatchN(1).asText()); + assertEquals("TRIANGLE Z ((0 0 0, 1 0 0, 1 -1 0, 0 0 0))", geometry.getPatchN(2).asText()); + assertEquals("TRIANGLE Z ((0 0 0, 1 -1 0, 0 -1 0, 0 0 0))", geometry.getPatchN(3).asText()); } /** @@ -391,23 +391,23 @@ public class PrimitiveVisitorTest { 0, 1, 2, 3, 4, 5, 2)); final List<String> expected = Arrays.asList( - "TRIANGLE ((0.0 0.0 0.0,0.0 1.0 0.0,1.0 1.0 0.0))", + "TRIANGLE Z ((0 0 0, 0 1 0, 1 1 0, 0 0 0))", "V:0 POSITION[0.0, 0.0, 0.0]", "V:1 POSITION[0.0, 1.0, 0.0]", "V:2 POSITION[1.0, 1.0, 0.0]", - "TRIANGLE ((0.0 0.0 0.0,1.0 1.0 0.0,1.0 0.0 0.0))", + "TRIANGLE Z ((0 0 0, 1 1 0, 1 0 0, 0 0 0))", "V:0 POSITION[0.0, 0.0, 0.0]", "V:2 POSITION[1.0, 1.0, 0.0]", "V:3 POSITION[1.0, 0.0, 0.0]", - "TRIANGLE ((0.0 0.0 0.0,1.0 0.0 0.0,1.0 -1.0 0.0))", + "TRIANGLE Z ((0 0 0, 1 0 0, 1 -1 0, 0 0 0))", "V:0 POSITION[0.0, 0.0, 0.0]", "V:3 POSITION[1.0, 0.0, 0.0]", "V:4 POSITION[1.0, -1.0, 0.0]", - "TRIANGLE ((0.0 0.0 0.0,1.0 -1.0 0.0,0.0 -1.0 0.0))", + "TRIANGLE Z ((0 0 0, 1 -1 0, 0 -1 0, 0 0 0))", "V:0 POSITION[0.0, 0.0, 0.0]", "V:4 POSITION[1.0, -1.0, 0.0]", "V:5 POSITION[0.0, -1.0, 0.0]", - "TRIANGLE ((0.0 0.0 0.0,0.0 -1.0 0.0,1.0 1.0 0.0))", + "TRIANGLE Z ((0 0 0, 0 -1 0, 1 1 0, 0 0 0))", "V:0 POSITION[0.0, 0.0, 0.0]", "V:5 POSITION[0.0, -1.0, 0.0]", "V:2 POSITION[1.0, 1.0, 0.0]" @@ -418,11 +418,11 @@ public class PrimitiveVisitorTest { //test geometry as TIN assertTrue(geometry instanceof TIN); assertEquals(5, geometry.getNumPatches()); - assertEquals("TRIANGLE ((0.0 0.0 0.0,0.0 1.0 0.0,1.0 1.0 0.0))", geometry.getPatchN(0).asText()); - assertEquals("TRIANGLE ((0.0 0.0 0.0,1.0 1.0 0.0,1.0 0.0 0.0))", geometry.getPatchN(1).asText()); - assertEquals("TRIANGLE ((0.0 0.0 0.0,1.0 0.0 0.0,1.0 -1.0 0.0))", geometry.getPatchN(2).asText()); - assertEquals("TRIANGLE ((0.0 0.0 0.0,1.0 -1.0 0.0,0.0 -1.0 0.0))", geometry.getPatchN(3).asText()); - assertEquals("TRIANGLE ((0.0 0.0 0.0,0.0 -1.0 0.0,1.0 1.0 0.0))", geometry.getPatchN(4).asText()); + assertEquals("TRIANGLE Z ((0 0 0, 0 1 0, 1 1 0, 0 0 0))", geometry.getPatchN(0).asText()); + assertEquals("TRIANGLE Z ((0 0 0, 1 1 0, 1 0 0, 0 0 0))", geometry.getPatchN(1).asText()); + assertEquals("TRIANGLE Z ((0 0 0, 1 0 0, 1 -1 0, 0 0 0))", geometry.getPatchN(2).asText()); + assertEquals("TRIANGLE Z ((0 0 0, 1 -1 0, 0 -1 0, 0 0 0))", geometry.getPatchN(3).asText()); + assertEquals("TRIANGLE Z ((0 0 0, 0 -1 0, 1 1 0, 0 0 0))", geometry.getPatchN(4).asText()); } /** @@ -451,27 +451,27 @@ public class PrimitiveVisitorTest { 3,1,0)); final List<String> expected = Arrays.asList( - "TRIANGLE ((0.0 0.0 0.0,0.0 1.0 0.0,1.0 0.0 0.0))", + "TRIANGLE Z ((0 0 0, 0 1 0, 1 0 0, 0 0 0))", "V:0 POSITION[0.0, 0.0, 0.0]", "V:1 POSITION[0.0, 1.0, 0.0]", "V:2 POSITION[1.0, 0.0, 0.0]", - "TRIANGLE ((1.0 0.0 0.0,0.0 1.0 0.0,1.0 1.0 0.0))", + "TRIANGLE Z ((1 0 0, 0 1 0, 1 1 0, 1 0 0))", "V:2 POSITION[1.0, 0.0, 0.0]", "V:1 POSITION[0.0, 1.0, 0.0]", "V:3 POSITION[1.0, 1.0, 0.0]", - "TRIANGLE ((1.0 0.0 0.0,1.0 1.0 0.0,2.0 0.0 0.0))", + "TRIANGLE Z ((1 0 0, 1 1 0, 2 0 0, 1 0 0))", "V:2 POSITION[1.0, 0.0, 0.0]", "V:3 POSITION[1.0, 1.0, 0.0]", "V:4 POSITION[2.0, 0.0, 0.0]", - "TRIANGLE ((2.0 0.0 0.0,1.0 1.0 0.0,2.0 1.0 0.0))", + "TRIANGLE Z ((2 0 0, 1 1 0, 2 1 0, 2 0 0))", "V:4 POSITION[2.0, 0.0, 0.0]", "V:3 POSITION[1.0, 1.0, 0.0]", "V:5 POSITION[2.0, 1.0, 0.0]", - "TRIANGLE ((2.0 0.0 0.0,2.0 1.0 0.0,3.0 0.0 0.0))", + "TRIANGLE Z ((2 0 0, 2 1 0, 3 0 0, 2 0 0))", "V:4 POSITION[2.0, 0.0, 0.0]", "V:5 POSITION[2.0, 1.0, 0.0]", "V:6 POSITION[3.0, 0.0, 0.0]", - "TRIANGLE ((3.0 0.0 0.0,2.0 1.0 0.0,3.0 1.0 0.0))", + "TRIANGLE Z ((3 0 0, 2 1 0, 3 1 0, 3 0 0))", "V:6 POSITION[3.0, 0.0, 0.0]", "V:5 POSITION[2.0, 1.0, 0.0]", "V:7 POSITION[3.0, 1.0, 0.0]" @@ -482,12 +482,12 @@ public class PrimitiveVisitorTest { //test geometry as TIN assertTrue(geometry instanceof TIN); assertEquals(6, geometry.getNumPatches()); - assertEquals("TRIANGLE ((0.0 0.0 0.0,0.0 1.0 0.0,1.0 0.0 0.0))", geometry.getPatchN(0).asText()); - assertEquals("TRIANGLE ((1.0 0.0 0.0,0.0 1.0 0.0,1.0 1.0 0.0))", geometry.getPatchN(1).asText()); - assertEquals("TRIANGLE ((1.0 0.0 0.0,1.0 1.0 0.0,2.0 0.0 0.0))", geometry.getPatchN(2).asText()); - assertEquals("TRIANGLE ((2.0 0.0 0.0,1.0 1.0 0.0,2.0 1.0 0.0))", geometry.getPatchN(3).asText()); - assertEquals("TRIANGLE ((2.0 0.0 0.0,2.0 1.0 0.0,3.0 0.0 0.0))", geometry.getPatchN(4).asText()); - assertEquals("TRIANGLE ((3.0 0.0 0.0,2.0 1.0 0.0,3.0 1.0 0.0))", geometry.getPatchN(5).asText()); + assertEquals("TRIANGLE Z ((0 0 0, 0 1 0, 1 0 0, 0 0 0))", geometry.getPatchN(0).asText()); + assertEquals("TRIANGLE Z ((1 0 0, 0 1 0, 1 1 0, 1 0 0))", geometry.getPatchN(1).asText()); + assertEquals("TRIANGLE Z ((1 0 0, 1 1 0, 2 0 0, 1 0 0))", geometry.getPatchN(2).asText()); + assertEquals("TRIANGLE Z ((2 0 0, 1 1 0, 2 1 0, 2 0 0))", geometry.getPatchN(3).asText()); + assertEquals("TRIANGLE Z ((2 0 0, 2 1 0, 3 0 0, 2 0 0))", geometry.getPatchN(4).asText()); + assertEquals("TRIANGLE Z ((3 0 0, 2 1 0, 3 1 0, 3 0 0))", geometry.getPatchN(5).asText()); } /** @@ -518,27 +518,27 @@ public class PrimitiveVisitorTest { 0, 1, 2, 3, 4, 5, 6, 3)); final List<String> expected = Arrays.asList( - "TRIANGLE ((0.0 0.0 0.0,0.0 1.0 0.0,1.0 0.0 0.0))", + "TRIANGLE Z ((0 0 0, 0 1 0, 1 0 0, 0 0 0))", "V:0 POSITION[0.0, 0.0, 0.0]", "V:1 POSITION[0.0, 1.0, 0.0]", "V:2 POSITION[1.0, 0.0, 0.0]", - "TRIANGLE ((1.0 0.0 0.0,0.0 1.0 0.0,1.0 1.0 0.0))", + "TRIANGLE Z ((1 0 0, 0 1 0, 1 1 0, 1 0 0))", "V:2 POSITION[1.0, 0.0, 0.0]", "V:1 POSITION[0.0, 1.0, 0.0]", "V:3 POSITION[1.0, 1.0, 0.0]", - "TRIANGLE ((1.0 0.0 0.0,1.0 1.0 0.0,2.0 0.0 0.0))", + "TRIANGLE Z ((1 0 0, 1 1 0, 2 0 0, 1 0 0))", "V:2 POSITION[1.0, 0.0, 0.0]", "V:3 POSITION[1.0, 1.0, 0.0]", "V:4 POSITION[2.0, 0.0, 0.0]", - "TRIANGLE ((2.0 0.0 0.0,1.0 1.0 0.0,2.0 1.0 0.0))", + "TRIANGLE Z ((2 0 0, 1 1 0, 2 1 0, 2 0 0))", "V:4 POSITION[2.0, 0.0, 0.0]", "V:3 POSITION[1.0, 1.0, 0.0]", "V:5 POSITION[2.0, 1.0, 0.0]", - "TRIANGLE ((2.0 0.0 0.0,2.0 1.0 0.0,3.0 0.0 0.0))", + "TRIANGLE Z ((2 0 0, 2 1 0, 3 0 0, 2 0 0))", "V:4 POSITION[2.0, 0.0, 0.0]", "V:5 POSITION[2.0, 1.0, 0.0]", "V:6 POSITION[3.0, 0.0, 0.0]", - "TRIANGLE ((3.0 0.0 0.0,2.0 1.0 0.0,1.0 1.0 0.0))", + "TRIANGLE Z ((3 0 0, 2 1 0, 1 1 0, 3 0 0))", "V:6 POSITION[3.0, 0.0, 0.0]", "V:5 POSITION[2.0, 1.0, 0.0]", "V:3 POSITION[1.0, 1.0, 0.0]" @@ -549,12 +549,12 @@ public class PrimitiveVisitorTest { //test geometry as TIN assertTrue(geometry instanceof TIN); assertEquals(6, geometry.getNumPatches()); - assertEquals("TRIANGLE ((0.0 0.0 0.0,0.0 1.0 0.0,1.0 0.0 0.0))", geometry.getPatchN(0).asText()); - assertEquals("TRIANGLE ((1.0 0.0 0.0,0.0 1.0 0.0,1.0 1.0 0.0))", geometry.getPatchN(1).asText()); - assertEquals("TRIANGLE ((1.0 0.0 0.0,1.0 1.0 0.0,2.0 0.0 0.0))", geometry.getPatchN(2).asText()); - assertEquals("TRIANGLE ((2.0 0.0 0.0,1.0 1.0 0.0,2.0 1.0 0.0))", geometry.getPatchN(3).asText()); - assertEquals("TRIANGLE ((2.0 0.0 0.0,2.0 1.0 0.0,3.0 0.0 0.0))", geometry.getPatchN(4).asText()); - assertEquals("TRIANGLE ((3.0 0.0 0.0,2.0 1.0 0.0,1.0 1.0 0.0))", geometry.getPatchN(5).asText()); + assertEquals("TRIANGLE Z ((0 0 0, 0 1 0, 1 0 0, 0 0 0))", geometry.getPatchN(0).asText()); + assertEquals("TRIANGLE Z ((1 0 0, 0 1 0, 1 1 0, 1 0 0))", geometry.getPatchN(1).asText()); + assertEquals("TRIANGLE Z ((1 0 0, 1 1 0, 2 0 0, 1 0 0))", geometry.getPatchN(2).asText()); + assertEquals("TRIANGLE Z ((2 0 0, 1 1 0, 2 1 0, 2 0 0))", geometry.getPatchN(3).asText()); + assertEquals("TRIANGLE Z ((2 0 0, 2 1 0, 3 0 0, 2 0 0))", geometry.getPatchN(4).asText()); + assertEquals("TRIANGLE Z ((3 0 0, 2 1 0, 1 1 0, 3 0 0))", geometry.getPatchN(5).asText()); } private void visit(MeshPrimitive geometry, List<String> exp) { diff --git a/incubator/src/org.apache.sis.referencing.dggs/test/org/apache/sis/referencing/dggs/s2/S2Test.java b/incubator/src/org.apache.sis.referencing.dggs/test/org/apache/sis/referencing/dggs/s2/S2Test.java index 7f7c5714a4..efcd471308 100644 --- a/incubator/src/org.apache.sis.referencing.dggs/test/org/apache/sis/referencing/dggs/s2/S2Test.java +++ b/incubator/src/org.apache.sis.referencing.dggs/test/org/apache/sis/referencing/dggs/s2/S2Test.java @@ -50,7 +50,7 @@ public class S2Test extends AbstractDggrsTest { final GeographicExtent extent = zone.getGeographicExtent(); final Polygon polygon = DiscreteGlobalGridSystems.toSISPolygon(extent); - assertEquals("POLYGON ((-180.0 34.50852298766839, -180.0 22.619864948040426, -169.38034472384487 22.270575488008195, -169.38034472384487 34.04786296943431, -180.0 34.50852298766839))", polygon.asText()); + assertEquals("POLYGON ((-180 34.50852298766839, -180 22.619864948040426, -169.38034472384487 22.270575488008195, -169.38034472384487 34.04786296943431, -180 34.50852298766839))", polygon.asText()); final Envelope envelope = zone.getEnvelope(); assertEquals(-180, envelope.getMinimum(0), 0.0);
