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>
+ * &lt;keyword&gt; [Z | M | ZM] ( … )
+ * &lt;keyword&gt; [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);

Reply via email to