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
The following commit(s) were added to refs/heads/geoapi-4.0 by this push:
new 7b191ca191 feat(Geometry): add EWKT and EWKB support
7b191ca191 is described below
commit 7b191ca1915bc7f238b3523219b5bbfc06c90891
Author: jsorel <[email protected]>
AuthorDate: Wed Sep 16 15:34:57 2026 +0200
feat(Geometry): add EWKT and EWKB support
---
.../org/apache/sis/geometries/adapter/Srid.java | 92 +++++++++++
.../sis/geometries/adapter/WellKnownBinary.java | 182 +++++++++++++++++++--
.../geometries/adapter/WellKnownBinaryParser.java | 102 ++++++++++--
.../sis/geometries/adapter/WellKnownText.java | 96 ++++++++++-
.../geometries/adapter/WellKnownTextParser.java | 95 ++++++++++-
.../geometries/adapter/WellKnownBinaryTest.java | 161 +++++++++++++++++-
.../sis/geometries/adapter/WellKnownTextTest.java | 89 ++++++++++
7 files changed, 768 insertions(+), 49 deletions(-)
diff --git
a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/adapter/Srid.java
b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/adapter/Srid.java
new file mode 100644
index 0000000000..e121324b3e
--- /dev/null
+++
b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/adapter/Srid.java
@@ -0,0 +1,92 @@
+/*
+ * 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.metadata.iso.citation.Citations;
+import org.apache.sis.referencing.CRS;
+import org.apache.sis.referencing.IdentifiedObjects;
+import org.opengis.metadata.Identifier;
+import org.opengis.referencing.crs.CoordinateReferenceSystem;
+import org.opengis.util.FactoryException;
+
+
+/**
+ * Conversions between the spatial reference identifier of the extended
geometry formats and a
+ * {@link CoordinateReferenceSystem}. A <abbr>SRID</abbr> is the numeric part
of an <abbr>EPSG</abbr>
+ * code, and is what {@code EWKT} and {@code EWKB} carry in place of a full
system definition.
+ *
+ * <p>The mapping is the one of the identifier the system already carries,
never a search for an
+ * equivalent definition: {@link #of(CoordinateReferenceSystem)} reads the
identifier and
+ * {@link #forCode(int)} builds the system the authority defines for that
code. Neither method
+ * changes the axis order, so a system read back from a code is the one the
<abbr>EPSG</abbr>
+ * authority defines, which for a geographic system is (<var>latitude</var>,
<var>longitude</var>)
+ * and not the (<var>longitude</var>, <var>latitude</var>) order that some
databases assume.</p>
+ *
+ * @author Johann Sorel (Geomatys)
+ */
+final class Srid {
+ /**
+ * Value meaning that the coordinate reference system has no spatial
reference identifier,
+ * which is how the extended formats say that they carry no system.
+ */
+ static final int UNDEFINED = 0;
+
+ /**
+ * Do not allow instantiation of this holder of static methods.
+ */
+ private Srid() {
+ }
+
+ /**
+ * Returns the spatial reference identifier of the given coordinate
reference system, or
+ * {@link #UNDEFINED} if it has none. Only the identifiers the system
already carries are
+ * examined; the <abbr>EPSG</abbr> geodetic dataset is not searched for an
equivalent
+ * definition, so a system built from axes and a datum has no identifier
whatever it describes.
+ *
+ * @param crs the system whose identifier to return, or {@code null}.
+ * @return the spatial reference identifier, or {@link #UNDEFINED} if none.
+ */
+ static int of(final CoordinateReferenceSystem crs) {
+ if (crs != null) {
+ final Identifier id = IdentifiedObjects.getIdentifier(crs,
Citations.EPSG);
+ if (id != null) try {
+ return Integer.parseInt(id.getCode());
+ } catch (NumberFormatException e) {
+ // An EPSG code which is not a number cannot be written as a
SRID. Fall through.
+ }
+ }
+ return UNDEFINED;
+ }
+
+ /**
+ * Returns the coordinate reference system which the <abbr>EPSG</abbr>
authority defines for
+ * the given spatial reference identifier.
+ *
+ * @param srid the spatial reference identifier to resolve.
+ * @return the coordinate reference system of that identifier.
+ * @throws IllegalArgumentException if the identifier is unknown, or if
the definitions
+ * are not available.
+ */
+ static CoordinateReferenceSystem forCode(final int srid) {
+ try {
+ return CRS.forCode("EPSG:" + srid);
+ } catch (FactoryException e) {
+ throw new IllegalArgumentException("Cannot resolve the spatial
reference identifier "
+ + srid + " to a coordinate reference system: " +
e.getMessage(), e);
+ }
+ }
+}
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
index 2e393f4730..ffe64a5365 100644
---
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
@@ -110,11 +110,12 @@ import
org.opengis.referencing.crs.CoordinateReferenceSystem;
* {@code GeometryCollection}, and an empty point may not appear as a
member of a
* {@code MultiPoint}. Every other type has a genuine empty form, a
count of zero, which
* round-trips unchanged.</li>
- * <li>The coordinate reference system is neither written nor read: the
{@code SRID} field of the
- * extended Well-Known Binary of some databases is not part of the
standard, and neither are
- * the high order type bits it uses for the dimension flags. Unless a
system is given to
+ * <li>In the {@link Flavor#OGC} flavor, the coordinate reference system is
neither written nor
+ * read: the {@code SRID} field is not part of the standard, and neither
are the high order
+ * type bits which carry it. Unless a system is given to
* {@link #decode(byte[], CoordinateReferenceSystem)}, decoded
geometries use
- * {@link
org.apache.sis.geometries.Geometries#getUndefinedCRS(int)}.</li>
+ * {@link org.apache.sis.geometries.Geometries#getUndefinedCRS(int)}. The
+ * {@link Flavor#EWKB} flavor writes and reads that field.</li>
* </ul>
*
* <h2>Thread safety</h2>
@@ -123,11 +124,64 @@ import
org.opengis.referencing.crs.CoordinateReferenceSystem;
* @author Johann Sorel (Geomatys)
*/
public final class WellKnownBinary {
+ /**
+ * The dialects of Well-Known Binary which this class can be asked to read
and write.
+ *
+ * @author Johann Sorel (Geomatys)
+ */
+ public enum Flavor {
+ /**
+ * The Well-Known Binary of <cite>OGC Simple Feature Access
1.2.1</cite>, extended with the
+ * curved and surface-patch types of <cite>ISO 13249-3</cite>. This is
the dialect the
+ * class javadoc describes, and the default. The dimension flags are
the thousands of the
+ * type code, and there is no place for a spatial reference identifier.
+ */
+ OGC,
+
+ /**
+ * The <cite>Extended Well-Known Binary</cite> of PostGIS, which moves
the dimension flags
+ * into the high order bits of the type code and adds a spatial
reference identifier:
+ *
+ * <blockquote><pre>
+ * byte byteOrder;
+ * uint32 type; // geometry type, OR 0x80000000 for Z,
0x40000000 for M, 0x20000000 for a SRID
+ * uint32 srid; // present only if the 0x20000000 bit is set
+ * … // body, unchanged
+ * </pre></blockquote>
+ *
+ * <p>The identifier is written on the outermost geometry only, and
only when the
+ * coordinate reference system carries an <abbr>EPSG</abbr>
identifier. On reading, the
+ * system it names is used unless the caller passed one of their own to
+ * {@link #decode(byte[], CoordinateReferenceSystem)}.</p>
+ *
+ * <p>This dialect is a superset of {@link #OGC} on reading: the
thousands of a type code
+ * are still understood, so a plain Well-Known Binary decodes
unchanged. The converse does
+ * not hold — the {@code OGC} dialect rejects the high order bits.</p>
+ */
+ EWKB,
+
+ /**
+ * The <cite>Tiny Well-Known Binary</cite> of TWKB 1.0.
+ * Not implemented yet: {@link #encode encode(…)} and {@link #decode
decode(…)} throw
+ * an {@link UnsupportedOperationException} for this flavor.
+ */
+ TWKB
+ }
+
/**
* Value of the byte order flag for each of the two orders.
*/
static final byte XDR = 0, NDR = 1;
+ /**
+ * Bits which the {@link Flavor#EWKB} dialect sets in the high order of a
type code, and the
+ * mask which isolates the base code from them.
+ */
+ static final int EWKB_Z = 0x80000000,
+ EWKB_M = 0x40000000,
+ EWKB_SRID = 0x20000000,
+ EWKB_BASE_MASK = 0x1FFFFFFF;
+
/**
* Type codes of the geometries which have one, shared by the encoder and
the parser.
* They are held apart rather than declared on {@code WellKnownBinary}
because a field
@@ -169,6 +223,11 @@ public final class WellKnownBinary {
}
}
+ /**
+ * The dialect to read and write. Never null.
+ */
+ private final Flavor flavor;
+
/**
* Byte order of the written geometries. Never null. Both orders are read
whatever this is.
*/
@@ -179,7 +238,7 @@ public final class WellKnownBinary {
* order of the byte sequences of the standard. Both orders are read.
*/
public WellKnownBinary() {
- byteOrder = ByteOrder.BIG_ENDIAN;
+ this(Flavor.OGC, ByteOrder.BIG_ENDIAN);
}
/**
@@ -189,10 +248,51 @@ public final class WellKnownBinary {
* @param byteOrder order of the multi-byte values to write, not null.
*/
public WellKnownBinary(final ByteOrder byteOrder) {
+ this(Flavor.OGC, byteOrder);
+ }
+
+ /**
+ * Creates a codec for the given dialect, writing geometries in big endian
order.
+ *
+ * @param flavor the dialect to read and write, not null.
+ */
+ public WellKnownBinary(final Flavor flavor) {
+ this(flavor, ByteOrder.BIG_ENDIAN);
+ }
+
+ /**
+ * Creates a codec for the given dialect, writing geometries in the given
byte order.
+ *
+ * @param flavor the dialect to read and write, not null.
+ * @param byteOrder order of the multi-byte values to write, not null.
+ */
+ public WellKnownBinary(final Flavor flavor, final ByteOrder byteOrder) {
+ ArgumentChecks.ensureNonNull("flavor", flavor);
ArgumentChecks.ensureNonNull("byteOrder", byteOrder);
+ this.flavor = flavor;
this.byteOrder = byteOrder;
}
+ /**
+ * Returns the dialect this codec reads and writes.
+ *
+ * @return the dialect given to the constructor, or {@link Flavor#OGC} if
none was.
+ */
+ public Flavor getFlavor() {
+ return flavor;
+ }
+
+ /**
+ * Verifies that the dialect of this codec is implemented.
+ *
+ * @throws UnsupportedOperationException if it is not.
+ */
+ private void ensureImplemented() {
+ if (flavor == Flavor.TWKB) {
+ throw new UnsupportedOperationException("The " + flavor + " flavor
is not implemented yet.");
+ }
+ }
+
/**
* Returns the Well-Known Binary of the given geometry.
*
@@ -204,7 +304,11 @@ public final class WellKnownBinary {
*/
public byte[] encode(final Geometry geom) {
ArgumentChecks.ensureNonNull("geom", geom);
+ ensureImplemented();
final Output out = new Output(byteOrder);
+ if (flavor == Flavor.EWKB) {
+ out.setSrid(Srid.of(geom.getCoordinateReferenceSystem()));
+ }
format(out, geom);
return out.toArray();
}
@@ -223,19 +327,24 @@ public final class WellKnownBinary {
/**
* Returns the geometry described by the given Well-Known Binary, in the
given coordinate
- * reference system. Well-Known Binary carries no system of its own.
+ * reference system. The {@link Flavor#OGC} Well-Known Binary carries no
system of its own;
+ * an {@link Flavor#EWKB} one may carry a spatial reference identifier,
which is used only
+ * when {@code crs} is null. The system which ends up being used, from
either source, must
+ * have as many dimensions as the bytes have ordinates per position.
*
* @param geom the Well-Known Binary to decode, not null.
* @param crs the coordinate reference system of the coordinates in the
bytes, or
- * {@code null}.
+ * {@code null} for the one the bytes name, if any.
* @return the decoded geometry.
* @throws IllegalArgumentException if the bytes are malformed, name a
geometry type which is
- * not in the table of this class javadoc, or have a number of
ordinates which
- * contradicts the dimension of {@code crs}.
+ * not in the table of this class javadoc, carry a spatial
reference identifier which
+ * cannot be resolved, or have a number of ordinates which
contradicts the dimension
+ * of the coordinate reference system.
*/
public Geometry decode(final byte[] geom, final CoordinateReferenceSystem
crs) {
ArgumentChecks.ensureNonNull("geom", geom);
- return new WellKnownBinaryParser(geom, crs).parse();
+ ensureImplemented();
+ return new WellKnownBinaryParser(geom, crs, flavor).parse();
}
// ////////////////////////////////////////////////////////////////////////
@@ -426,7 +535,9 @@ public final class WellKnownBinary {
}
/**
- * Writes the byte order flag and the type code of a geometry, the
dimension flags included.
+ * Writes the byte order flag and the type code of a geometry, the
dimension flags included,
+ * followed by the spatial reference identifier when the dialect carries
one and this is the
+ * outermost geometry.
*
* @return whether the positions carry a measure, which the caller has to
write as the
* ordinate following the position ones.
@@ -441,16 +552,33 @@ public final class WellKnownBinary {
}
final int dimension = crs.getCoordinateSystem().getDimension();
final boolean hasM = hasMeasure(geometry);
- final int flags;
+ final boolean hasZ;
switch (dimension) {
- case 2: flags = hasM ? Codes.M_OFFSET : 0; break;
- case 3: flags = hasM ? Codes.Z_OFFSET + Codes.M_OFFSET :
Codes.Z_OFFSET; break;
+ case 2: hasZ = false; break;
+ case 3: hasZ = true; break;
default: throw new IllegalArgumentException("Cannot write a " +
geometry.getGeometryType()
+ " in Well-Known Binary: its positions have " +
dimension + " dimensions,"
+ " but the format defines only 2 and 3.");
}
+ /*
+ * The identifier is taken at most once per encoding, so only the
outermost geometry
+ * carries it: every nested call gets Srid.UNDEFINED back.
+ */
+ final int srid = out.takeSrid();
+ int type = code;
+ if (flavor == Flavor.EWKB) {
+ if (hasZ) type |= EWKB_Z;
+ if (hasM) type |= EWKB_M;
+ if (srid != Srid.UNDEFINED) type |= EWKB_SRID;
+ } else {
+ if (hasZ) type += Codes.Z_OFFSET;
+ if (hasM) type += Codes.M_OFFSET;
+ }
out.writeByteOrder();
- out.writeInt(code + flags);
+ out.writeInt(type);
+ if (flavor == Flavor.EWKB && srid != Srid.UNDEFINED) {
+ out.writeInt(srid);
+ }
return hasM;
}
@@ -492,6 +620,13 @@ public final class WellKnownBinary {
*/
private final boolean bigEndian;
+ /**
+ * The spatial reference identifier which the next header has to
carry, or
+ * {@link Srid#UNDEFINED} if none has to. Held here rather than passed
down the writing
+ * methods because only the outermost geometry carries it: {@link
#takeSrid()} clears it.
+ */
+ private int srid;
+
/**
* Creates an initially empty output writing in the given byte order.
*/
@@ -499,6 +634,23 @@ public final class WellKnownBinary {
bigEndian = (byteOrder == ByteOrder.BIG_ENDIAN);
}
+ /**
+ * Sets the identifier which the first header written will carry.
+ */
+ void setSrid(final int srid) {
+ this.srid = srid;
+ }
+
+ /**
+ * Returns the identifier which the header being written has to carry,
and clears it so
+ * that the nested geometries do not repeat it.
+ */
+ int takeSrid() {
+ final int value = srid;
+ srid = Srid.UNDEFINED;
+ return value;
+ }
+
/**
* Writes the flag which tells in which order the values after it are
written.
*/
diff --git
a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/adapter/WellKnownBinaryParser.java
b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/adapter/WellKnownBinaryParser.java
index 194ac06624..49cc8d46ed 100644
---
a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/adapter/WellKnownBinaryParser.java
+++
b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/adapter/WellKnownBinaryParser.java
@@ -71,6 +71,11 @@ import org.opengis.referencing.crs.CoordinateReferenceSystem;
* differ, so {@link #bigEndian} is re-read at the start of every element and
applies until the next
* one is read.
*
+ * <h2>Spatial reference identifier</h2>
+ * The identifier belongs to the {@link WellKnownBinary.Flavor#EWKB} dialect
alone, where a bit of
+ * the type code announces it. It describes the whole sequence rather than the
element which
+ * carries it, so the outermost geometry is the one expected to hold it.
+ *
* @author Johann Sorel (Geomatys)
*/
final class WellKnownBinaryParser {
@@ -91,11 +96,29 @@ final class WellKnownBinaryParser {
private final byte[] data;
/**
- * 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.
+ * The coordinate reference system given by the caller, or {@code null}
for deriving one from
+ * the spatial reference identifier, or failing that from the number of
ordinates.
*/
private final CoordinateReferenceSystem userCRS;
+ /**
+ * The dialect being parsed, which decides how the dimension flags are
read and whether a
+ * spatial reference identifier is allowed.
+ */
+ private final WellKnownBinary.Flavor flavor;
+
+ /**
+ * The spatial reference identifier the bytes carry, or {@link
Srid#UNDEFINED} if they carry
+ * none. Always {@link Srid#UNDEFINED} in the {@code OGC} dialect, which
has no field for it.
+ */
+ private int srid;
+
+ /**
+ * The coordinate reference system of {@link #srid}, resolved by the first
call to
+ * {@link #crs()} which needs it. Null as long as it has not been resolved.
+ */
+ private CoordinateReferenceSystem sridCRS;
+
/**
* Index in {@link #data} of the next byte to read.
*/
@@ -119,12 +142,16 @@ final class WellKnownBinaryParser {
/**
* Creates a parser for the given bytes.
*
- * @param data the Well-Known Binary to parse.
- * @param crs the coordinate reference system to give to the
geometries, or {@code null}.
+ * @param data the Well-Known Binary to parse.
+ * @param crs the coordinate reference system to give to the
geometries, or {@code null}.
+ * @param flavor the dialect to parse.
*/
- WellKnownBinaryParser(final byte[] data, final CoordinateReferenceSystem
crs) {
+ WellKnownBinaryParser(final byte[] data, final CoordinateReferenceSystem
crs,
+ final WellKnownBinary.Flavor flavor)
+ {
this.data = data;
this.userCRS = crs;
+ this.flavor = flavor;
}
/**
@@ -149,18 +176,34 @@ final class WellKnownBinaryParser {
*/
private Geometry parseGeometry() {
readByteOrder();
- final int code = readInt();
- if (code < 0) {
+ final int type = readInt();
+ final boolean extended = (flavor == WellKnownBinary.Flavor.EWKB);
+ int code = type;
+ int flags = 0;
+ if (extended) {
+ /*
+ * The high order bits are the dimension flags and the presence of
an identifier.
+ * The base which remains may still carry the thousands of the OGC
dialect, which
+ * this dialect understands as well, so that a plain Well-Known
Binary decodes here.
+ */
+ if ((type & WellKnownBinary.EWKB_Z) != 0) flags |= FLAG_Z;
+ if ((type & WellKnownBinary.EWKB_M) != 0) flags |= FLAG_M;
+ code = type & WellKnownBinary.EWKB_BASE_MASK;
+ } else if (code < 0) {
throw error("Type code " + Integer.toUnsignedString(code) + " is
out of range."
- + " The extended Well-Known Binary of some databases,
which puts the dimension"
- + " flags in the high order bits, is not supported");
+ + " The extended Well-Known Binary, which puts the
dimension flags in the"
+ + " high order bits, is read by the " +
WellKnownBinary.Flavor.EWKB + " flavor");
}
- final int flags = code / 1000;
- if (flags > 3) {
+ final int thousands = code / 1000;
+ if (thousands > 3) {
throw error("Type code " + code + " has no dimension flag: the
thousands must be"
+ " 0 for XY, 1 for Z, 2 for M or 3 for ZM");
}
+ flags |= thousands;
applyFlags(flags);
+ if (extended && (type & WellKnownBinary.EWKB_SRID) != 0) {
+ readSrid();
+ }
switch (code % 1000) {
case WellKnownBinary.Codes.POINT: return
parsePoint();
case WellKnownBinary.Codes.LINESTRING: return
GeometryFactory.createLineString(readPointList());
@@ -181,6 +224,24 @@ final class WellKnownBinaryParser {
}
}
+ /**
+ * Reads the spatial reference identifier which follows a type code whose
+ * {@link WellKnownBinary#EWKB_SRID} bit is set.
+ *
+ * <p>Only the outermost geometry is expected to carry one, but a nested
geometry repeating it
+ * is accepted as long as it repeats the same value: the whole sequence
describes positions in
+ * a single system, so two different identifiers would contradict each
other.</p>
+ */
+ private void readSrid() {
+ final int declared = readInt();
+ if (srid == Srid.UNDEFINED) {
+ srid = declared;
+ } else if (srid != declared) {
+ throw error("A nested geometry declares the spatial reference
identifier " + declared
+ + " while the enclosing one declares " + srid);
+ }
+ }
+
/**
* Parses the single coordinate tuple of a point.
*
@@ -454,15 +515,26 @@ final class WellKnownBinaryParser {
*/
private CoordinateReferenceSystem crs() {
final int dimension = positionDimension();
- if (userCRS == null) {
+ final CoordinateReferenceSystem declared;
+ final String source;
+ if (userCRS != null) {
+ declared = userCRS;
+ source = "The given coordinate reference system";
+ } else if (srid != Srid.UNDEFINED) {
+ if (sridCRS == null) {
+ sridCRS = Srid.forCode(srid);
+ }
+ declared = sridCRS;
+ source = "The coordinate reference system of SRID " + srid;
+ } else {
return Geometries.getUndefinedCRS(dimension);
}
- final int actual = userCRS.getCoordinateSystem().getDimension();
+ final int actual = declared.getCoordinateSystem().getDimension();
if (actual != dimension) {
- throw error("The given coordinate reference system has " + actual
+ " dimensions,"
+ throw error(source + " has " + actual + " dimensions,"
+ " but the bytes have " + dimension + " ordinates per
position");
}
- return userCRS;
+ return declared;
}
/**
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
index 0fe8fde7ca..fedd8c6b5b 100644
---
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
@@ -108,10 +108,11 @@ import
org.opengis.referencing.crs.CoordinateReferenceSystem;
* 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>
+ * <li>In the {@link Flavor#OGC} flavor, the coordinate reference system is
neither written nor
+ * read: the {@code SRID=…;} prefix 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)}. The
+ * {@link Flavor#EWKT} flavor writes and reads that prefix.</li>
* </ul>
*
* <h2>Thread safety</h2>
@@ -121,6 +122,39 @@ import
org.opengis.referencing.crs.CoordinateReferenceSystem;
* @author Johann Sorel (Geomatys)
*/
public final class WellKnownText {
+ /**
+ * The dialects of Well-Known Text which this class can be asked to read
and write.
+ *
+ * @author Johann Sorel (Geomatys)
+ */
+ public enum Flavor {
+ /**
+ * The Well-Known Text of <cite>OGC Simple Feature Access
1.2.1</cite>, extended with the
+ * curved and surface-patch types of <cite>ISO 13249-3</cite>. This is
the dialect the
+ * class javadoc describes, and the default.
+ */
+ OGC,
+
+ /**
+ * The <cite>Extended Well-Known Text</cite> of PostGIS, which is the
{@link #OGC} dialect
+ * preceded by an optional spatial reference identifier:
+ *
+ * <blockquote><pre>SRID=4326;POINT (1 2)</pre></blockquote>
+ *
+ * <p>The prefix is written when the coordinate reference system of
the geometry carries an
+ * <abbr>EPSG</abbr> identifier, and omitted otherwise — an Extended
Well-Known Text without
+ * the prefix is an ordinary Well-Known Text. On reading, the prefix
is optional, and the
+ * system it names is used unless the caller passed one of their own to
+ * {@link #decode(String, CoordinateReferenceSystem)}.</p>
+ */
+ EWKT
+ }
+
+ /**
+ * The dialect to read and write. Never null.
+ */
+ private final Flavor flavor;
+
/**
* 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.
@@ -133,6 +167,18 @@ public final class WellKnownText {
* {@code 2}, not {@code 2.0}.
*/
public WellKnownText() {
+ this(Flavor.OGC);
+ }
+
+ /**
+ * Creates a codec for the given dialect, writing every ordinate as the
shortest decimal text
+ * which parses back to the same {@code double} value.
+ *
+ * @param flavor the dialect to read and write, not null.
+ */
+ public WellKnownText(final Flavor flavor) {
+ ArgumentChecks.ensureNonNull("flavor", flavor);
+ this.flavor = flavor;
format = null;
}
@@ -144,13 +190,35 @@ public final class WellKnownText {
* @param decimalPrecision maximal number of digits after the decimal
separator, 0 or more.
*/
public WellKnownText(final int decimalPrecision) {
+ this(Flavor.OGC, decimalPrecision);
+ }
+
+ /**
+ * Creates a codec for the given dialect, rounding every ordinate to at
most the given number
+ * of fraction digits as {@link #WellKnownText(int)} does.
+ *
+ * @param flavor the dialect to read and write, not null.
+ * @param decimalPrecision maximal number of digits after the decimal
separator, 0 or more.
+ */
+ public WellKnownText(final Flavor flavor, final int decimalPrecision) {
+ ArgumentChecks.ensureNonNull("flavor", flavor);
ArgumentChecks.ensurePositive("decimalPrecision", decimalPrecision);
+ this.flavor = flavor;
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 dialect this codec reads and writes.
+ *
+ * @return the dialect given to the constructor, or {@link Flavor#OGC} if
none was.
+ */
+ public Flavor getFlavor() {
+ return flavor;
+ }
+
/**
* Returns the Well-Known Text of the given geometry.
*
@@ -163,6 +231,12 @@ public final class WellKnownText {
public String encode(final Geometry geom) {
ArgumentChecks.ensureNonNull("geom", geom);
final StringBuilder sb = new StringBuilder();
+ if (flavor == Flavor.EWKT) {
+ final int srid = Srid.of(geom.getCoordinateReferenceSystem());
+ if (srid != Srid.UNDEFINED) {
+ sb.append("SRID=").append(srid).append(';');
+ }
+ }
format(sb, geom);
return sb.toString();
}
@@ -181,19 +255,23 @@ public final class WellKnownText {
/**
* 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.
+ * reference system. The {@link Flavor#OGC} Well-Known Text carries no
system of its own;
+ * an {@link Flavor#EWKT} one may carry a spatial reference identifier,
which is used only
+ * when {@code crs} is null. The system which ends up being used, from
either source, must
+ * have as many dimensions as the text has ordinates per position.
*
* @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}.
+ * {@code null} for the one the text names, if any.
* @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}.
+ * not in the table of this class javadoc, names a spatial
reference identifier which
+ * cannot be resolved, or has a number of ordinates which
contradicts the dimension
+ * of the coordinate reference system.
*/
public Geometry decode(final String geom, final CoordinateReferenceSystem
crs) {
ArgumentChecks.ensureNonNull("geom", geom);
- return new WellKnownTextParser(geom, crs).parse();
+ return new WellKnownTextParser(geom, crs, flavor).parse();
}
// ////////////////////////////////////////////////////////////////////////
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
index 46c933054b..f3b1e9e96f 100644
---
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
@@ -68,6 +68,11 @@ import org.opengis.referencing.crs.CoordinateReferenceSystem;
* 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.
*
+ * <h2>Spatial reference identifier</h2>
+ * The optional {@code SRID=…;} prefix belongs to the {@link
WellKnownText.Flavor#EWKT} dialect
+ * alone, and precedes the whole text rather than any single element. In the
{@code OGC} dialect
+ * the {@code SRID} keyword is read as a geometry type name, and therefore
rejected as unknown.
+ *
* @author Johann Sorel (Geomatys)
*/
final class WellKnownTextParser {
@@ -93,11 +98,29 @@ final class WellKnownTextParser {
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.
+ * The coordinate reference system given by the caller, or {@code null}
for deriving one from
+ * the {@code SRID=…;} prefix, or failing that from the number of
ordinates.
*/
private final CoordinateReferenceSystem userCRS;
+ /**
+ * The dialect being parsed, which decides whether a {@code SRID=…;}
prefix is allowed.
+ */
+ private final WellKnownText.Flavor flavor;
+
+ /**
+ * The spatial reference identifier of the {@code SRID=…;} prefix, or
{@link Srid#UNDEFINED}
+ * if the text has no such prefix. Always {@link Srid#UNDEFINED} in the
{@code OGC} flavor,
+ * where the prefix is not part of the grammar.
+ */
+ private int srid;
+
+ /**
+ * The coordinate reference system of {@link #srid}, resolved by the first
call to
+ * {@link #crs()} which needs it. Null as long as it has not been resolved.
+ */
+ private CoordinateReferenceSystem sridCRS;
+
/**
* Index in {@link #text} of the next character to read.
*/
@@ -117,12 +140,14 @@ final class WellKnownTextParser {
/**
* 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}.
+ * @param text the Well-Known Text to parse.
+ * @param crs the coordinate reference system to give to the
geometries, or {@code null}.
+ * @param flavor the dialect to parse.
*/
- WellKnownTextParser(final String text, final CoordinateReferenceSystem
crs) {
+ WellKnownTextParser(final String text, final CoordinateReferenceSystem
crs, final WellKnownText.Flavor flavor) {
this.text = text;
this.userCRS = crs;
+ this.flavor = flavor;
}
/**
@@ -131,6 +156,9 @@ final class WellKnownTextParser {
* @throws IllegalArgumentException if the text is malformed or names an
unsupported type.
*/
Geometry parse() {
+ if (flavor == WellKnownText.Flavor.EWKT) {
+ readSrid();
+ }
final Geometry geometry = parseGeometry();
skipSpaces();
if (pos < text.length()) {
@@ -139,6 +167,26 @@ final class WellKnownTextParser {
return geometry;
}
+ /**
+ * Consumes the {@code SRID=…;} prefix of an Extended Well-Known Text if
there is one.
+ * The prefix is optional, so a text which does not start with the {@code
SRID} keyword is
+ * left untouched; but a text which does start with it must carry a
complete prefix.
+ */
+ private void readSrid() {
+ final int mark = pos;
+ if (!"SRID".equals(readWord())) {
+ pos = mark;
+ return;
+ }
+ if (!accept('=')) {
+ throw error("Expected '=' after the SRID keyword");
+ }
+ srid = readUnsignedInteger();
+ if (!accept(';')) {
+ throw error("Expected ';' after the spatial reference identifier");
+ }
+ }
+
// ////////////////////////////////////////////////////////////////////////
// Productions ////////////////////////////////////////////////////////////
// ////////////////////////////////////////////////////////////////////////
@@ -494,15 +542,26 @@ final class WellKnownTextParser {
*/
private CoordinateReferenceSystem crs() {
final int dimension = positionDimension();
- if (userCRS == null) {
+ final CoordinateReferenceSystem declared;
+ final String source;
+ if (userCRS != null) {
+ declared = userCRS;
+ source = "The given coordinate reference system";
+ } else if (srid != Srid.UNDEFINED) {
+ if (sridCRS == null) {
+ sridCRS = Srid.forCode(srid);
+ }
+ declared = sridCRS;
+ source = "The coordinate reference system of SRID " + srid;
+ } else {
return Geometries.getUndefinedCRS(dimension);
}
- final int actual = userCRS.getCoordinateSystem().getDimension();
+ final int actual = declared.getCoordinateSystem().getDimension();
if (actual != dimension) {
- throw error("The given coordinate reference system has " + actual
+ " dimensions,"
+ throw error(source + " has " + actual + " dimensions,"
+ " but the text has " + dimension + " ordinates per
position");
}
- return userCRS;
+ return declared;
}
/**
@@ -691,6 +750,24 @@ final class WellKnownTextParser {
}
}
+ /**
+ * Reads a sequence of decimal digits, which the grammar uses only for a
spatial reference
+ * identifier. Unlike {@link #readNumber()} it accepts neither a sign, nor
a fractional part,
+ * nor an exponent, since none of those can appear in an identifier.
+ */
+ private int readUnsignedInteger() {
+ skipSpaces();
+ final int start = pos;
+ while (pos < text.length() && isDigit(text.charAt(pos))) pos++;
+ final String token = text.substring(start, pos);
+ try {
+ return Integer.parseInt(token);
+ } catch (NumberFormatException e) {
+ pos = start;
+ throw error("\"" + token + "\" is not a spatial reference
identifier");
+ }
+ }
+
/**
* 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}.
diff --git
a/incubator/src/org.apache.sis.geometry/test/org/apache/sis/geometries/adapter/WellKnownBinaryTest.java
b/incubator/src/org.apache.sis.geometry/test/org/apache/sis/geometries/adapter/WellKnownBinaryTest.java
index 175f4920eb..99c75693d9 100644
---
a/incubator/src/org.apache.sis.geometry/test/org/apache/sis/geometries/adapter/WellKnownBinaryTest.java
+++
b/incubator/src/org.apache.sis.geometry/test/org/apache/sis/geometries/adapter/WellKnownBinaryTest.java
@@ -41,6 +41,9 @@ 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.apache.sis.metadata.iso.citation.Citations;
+import org.apache.sis.referencing.CommonCRS;
+import org.apache.sis.referencing.IdentifiedObjects;
import org.opengis.referencing.crs.CoordinateReferenceSystem;
// Test dependencies
@@ -258,7 +261,7 @@ public final class WellKnownBinaryTest {
*/
final byte[] mixed = concat(bytes("00" + "00000007" + "00000001"),
littleEndian);
assertEquals("GEOMETRYCOLLECTION (POINT (1 2))",
wkt.encode(wkb.decode(mixed)));
- assertThrows(NullPointerException.class, () -> new
WellKnownBinary(null));
+ assertThrows(NullPointerException.class, () -> new
WellKnownBinary((ByteOrder) null));
}
/**
@@ -354,6 +357,162 @@ public final class WellKnownBinaryTest {
assertThrows(IllegalArgumentException.class, () -> wkb.decode(data),
hex(data));
}
+ /**
+ * Bits of the ordinates used by the extended tests below.
+ */
+ private static final String ZERO = "0000000000000000",
+ ONE = "3FF0000000000000",
+ TWO = "4000000000000000",
+ THREE = "4008000000000000",
+ FOUR = "4010000000000000";
+
+ /**
+ * Tests the spatial reference identifier which the {@link
WellKnownBinary.Flavor#EWKB}
+ * dialect adds, and the high order bits which announce it.
+ */
+ @Test
+ public void testExtendedRoundTrip() {
+ final WellKnownBinary ewkb = new
WellKnownBinary(WellKnownBinary.Flavor.EWKB);
+ assertEquals(WellKnownBinary.Flavor.EWKB, ewkb.getFlavor());
+ assertEquals(WellKnownBinary.Flavor.OGC, wkb.getFlavor());
+ /*
+ * The 0x20000000 bit announces the identifier, which follows the type
code.
+ * EPSG:4326 is 0x10E6.
+ */
+ final Geometry point =
GeometryFactory.createPoint(CommonCRS.WGS84.geographic(), 1, 2);
+ final byte[] bytes = ewkb.encode(point);
+ assertEquals("00" + "20000001" + "000010E6" + ONE + TWO, hex(bytes));
+ /*
+ * Reading gives the geometry its system back, and writing it again
gives the same bytes.
+ */
+ final Geometry back = ewkb.decode(bytes);
+ assertEquals("4326", IdentifiedObjects.getIdentifier(
+ back.getCoordinateReferenceSystem(),
Citations.EPSG).getCode());
+ assertEquals("POINT (1 2)", wkt.encode(back));
+ assertArrayEquals(bytes, ewkb.encode(back));
+ }
+
+ /**
+ * Verifies that only the outermost geometry carries the identifier, the
nested ones being
+ * written with a plain type code.
+ */
+ @Test
+ public void testExtendedIdentifierOnOutermostOnly() {
+ final WellKnownBinary ewkb = new
WellKnownBinary(WellKnownBinary.Flavor.EWKB);
+ final CoordinateReferenceSystem crs = CommonCRS.WGS84.geographic();
+ final byte[] bytes = ewkb.encode(GeometryFactory.createMultiPoint(
+ GeometryFactory.createPoint(crs, 1, 2),
+ GeometryFactory.createPoint(crs, 3, 4)));
+ assertEquals("00" + "20000004" + "000010E6" + "00000002"
+ + "00" + "00000001" + ONE + TWO
+ + "00" + "00000001" + THREE + FOUR, hex(bytes));
+ /*
+ * The identifier read on the outermost geometry reaches the members
as well.
+ */
+ final MultiPoint<?> decoded = assertInstanceOf(MultiPoint.class,
ewkb.decode(bytes));
+ assertEquals("MULTIPOINT ((1 2), (3 4))", wkt.encode(decoded));
+ assertEquals("4326", IdentifiedObjects.getIdentifier(
+ decoded.getGeometryN(0).getCoordinateReferenceSystem(),
Citations.EPSG).getCode());
+ }
+
+ /**
+ * Tests the dimension flags of the extended dialect, which are high order
bits rather than
+ * the thousands of the type code.
+ */
+ @Test
+ public void testExtendedDimensionFlags() {
+ final WellKnownBinary ewkb = new
WellKnownBinary(WellKnownBinary.Flavor.EWKB);
+ assertEquals("00" + "80000001" + ONE + TWO + THREE,
+ hex(ewkb.encode(wkt.decode("POINT Z (1 2 3)"))));
+ assertEquals("00" + "40000001" + ONE + TWO + THREE,
+ hex(ewkb.encode(wkt.decode("POINT M (1 2 3)"))));
+ assertEquals("00" + "C0000001" + ONE + TWO + THREE + FOUR,
+ hex(ewkb.encode(wkt.decode("POINT ZM (1 2 3 4)"))));
+ assertEquals("00" + "80000002" + "00000002" + ZERO + ZERO + ZERO + ONE
+ ONE + ONE,
+ hex(ewkb.encode(wkt.decode("LINESTRING Z (0 0 0, 1 1
1)"))));
+ /*
+ * Every one of them reads back to the geometry it was written from.
+ */
+ for (final String text : new String[] {
+ "POINT Z (1 2 3)", "POINT M (1 2 3)", "POINT ZM (1 2 3 4)",
+ "LINESTRING Z (0 0 0, 1 1 1)", "POLYGON ZM ((0 0 0 5, 1 0 0 6,
1 1 0 7, 0 0 0 5))",
+ "GEOMETRYCOLLECTION Z (POINT Z (1 2 3))"})
+ {
+ assertEquals(text,
wkt.encode(ewkb.decode(ewkb.encode(wkt.decode(text)))), text);
+ }
+ }
+
+ /**
+ * Verifies that the extended dialect is a superset of the standard one on
reading, and that
+ * the converse does not hold.
+ */
+ @Test
+ public void testExtendedReadsPlainBinary() {
+ final WellKnownBinary ewkb = new
WellKnownBinary(WellKnownBinary.Flavor.EWKB);
+ for (final String text : new String[] {
+ "POINT (1 2)", "POINT Z (1 2 3)", "POINT ZM (1 2 3 4)",
+ "POLYGON Z ((0 0 0, 1 0 0, 1 1 0, 0 0 0))",
+ "GEOMETRYCOLLECTION (POINT (1 2), LINESTRING (0 0, 1 1))"})
+ {
+ assertEquals(text,
wkt.encode(ewkb.decode(wkb.encode(wkt.decode(text)))), text);
+ }
+ /*
+ * The standard dialect rejects the high order bits of the extended
one.
+ */
+ assertMalformed(ewkb.encode(wkt.decode("POINT Z (1 2 3)")));
+
assertMalformed(ewkb.encode(GeometryFactory.createPoint(CommonCRS.WGS84.geographic(),
1, 2)));
+ }
+
+ /**
+ * Verifies that the system the bytes name yields to one the caller gives,
and tests the
+ * rejection of the identifiers which cannot be used.
+ */
+ @Test
+ public void testExtendedPrecedenceAndErrors() {
+ final WellKnownBinary ewkb = new
WellKnownBinary(WellKnownBinary.Flavor.EWKB);
+ final byte[] bytes =
ewkb.encode(GeometryFactory.createPoint(CommonCRS.WGS84.geographic(), 1, 2));
+ final CoordinateReferenceSystem crs = Geometries.getUndefinedCRS(2);
+ assertSame(crs, ewkb.decode(bytes,
crs).getCoordinateReferenceSystem());
+ /*
+ * EPSG:4326 is two dimensional, so a Z geometry carrying it
contradicts it.
+ * 0xA0000001 is the point type with the Z bit and the identifier bit.
+ */
+ assertExtendedMalformed("00" + "A0000001" + "000010E6" + ONE + TWO +
THREE);
+ /*
+ * An identifier which no authority defines. 999999 is 0xF423F.
+ */
+ assertExtendedMalformed("00" + "20000001" + "000F423F" + ONE + TWO);
+ /*
+ * Two geometries of the same sequence cannot name two different
systems. 3857 is 0xF11.
+ */
+ assertExtendedMalformed("00" + "20000004" + "000010E6" + "00000001"
+ + "00" + "20000001" + "00000F11" + ONE + TWO);
+ }
+
+ /**
+ * Verifies that the given hexadecimal bytes are rejected by the extended
dialect.
+ */
+ private void assertExtendedMalformed(final String text) {
+ final WellKnownBinary ewkb = new
WellKnownBinary(WellKnownBinary.Flavor.EWKB);
+ final byte[] data = bytes(text);
+ assertThrows(IllegalArgumentException.class, () -> ewkb.decode(data),
text);
+ }
+
+ /**
+ * Verifies that the dialect which is not implemented yet says so instead
of writing
+ * something else.
+ */
+ @Test
+ public void testUnimplementedFlavor() {
+ final WellKnownBinary twkb = new
WellKnownBinary(WellKnownBinary.Flavor.TWKB);
+ assertEquals(WellKnownBinary.Flavor.TWKB, twkb.getFlavor());
+ final Geometry geometry = wkt.decode("POINT (1 2)");
+ final byte[] bytes = wkb.encode(geometry);
+ assertThrows(UnsupportedOperationException.class, () ->
twkb.encode(geometry));
+ assertThrows(UnsupportedOperationException.class, () ->
twkb.decode(bytes));
+ assertThrows(NullPointerException.class, () -> new
WellKnownBinary((WellKnownBinary.Flavor) null));
+ }
+
/**
* Returns a copy of the given geometry with the type code of its
outermost element replaced
* by the given one, the dimension flags left untouched.
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
index 43f4f25ec8..0a3def45dc 100644
---
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
@@ -39,6 +39,8 @@ 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.apache.sis.metadata.iso.citation.Citations;
+import org.apache.sis.referencing.IdentifiedObjects;
import org.opengis.referencing.crs.CoordinateReferenceSystem;
// Test dependencies
@@ -296,4 +298,91 @@ public final class WellKnownTextTest {
private void assertMalformed(final String text) {
assertThrows(IllegalArgumentException.class, () -> wkt.decode(text),
text);
}
+
+ /**
+ * Tests the {@code SRID=…;} prefix which the {@link
WellKnownText.Flavor#EWKT} dialect adds.
+ */
+ @Test
+ public void testExtendedRoundTrip() {
+ final WellKnownText ewkt = new
WellKnownText(WellKnownText.Flavor.EWKT);
+ assertEquals(WellKnownText.Flavor.EWKT, ewkt.getFlavor());
+ assertEquals(WellKnownText.Flavor.OGC, wkt.getFlavor());
+ /*
+ * The prefix is read, kept in the coordinate reference system of the
geometry,
+ * and written back.
+ */
+ final Geometry geometry = ewkt.decode("SRID=4326;POINT (1 2)");
+ assertEquals("4326", IdentifiedObjects.getIdentifier(
+ geometry.getCoordinateReferenceSystem(),
Citations.EPSG).getCode());
+ assertEquals("SRID=4326;POINT (1 2)", ewkt.encode(geometry));
+ /*
+ * The prefix applies to the whole text, not to each member of a
collection.
+ */
+ assertEquals("SRID=4326;GEOMETRYCOLLECTION (POINT (1 2), LINESTRING (0
0, 1 1))",
+ ewkt.encode(ewkt.decode("SRID=4326;GEOMETRYCOLLECTION
(POINT (1 2), LINESTRING (0 0, 1 1))")));
+ assertEquals("SRID=3857;MULTIPOINT ((1 2), (3 4))",
+ ewkt.encode(ewkt.decode("SRID=3857;MULTIPOINT ((1 2), (3
4))")));
+ }
+
+ /**
+ * Verifies that the prefix is optional on both sides: a geometry whose
system has no
+ * <abbr>EPSG</abbr> identifier is written without it, and a text without
it is read.
+ */
+ @Test
+ public void testExtendedWithoutSRID() {
+ final WellKnownText ewkt = new
WellKnownText(WellKnownText.Flavor.EWKT);
+ assertEquals("POINT (1 2)", ewkt.encode(wkt.decode("POINT (1 2)")));
+ assertEquals("POINT (1 2)", ewkt.encode(ewkt.decode("POINT (1 2)")));
+ assertEquals("POINT Z (1 2 3)", ewkt.encode(ewkt.decode("POINT Z (1 2
3)")));
+ }
+
+ /**
+ * Verifies that the prefix belongs to the extended dialect alone, and
that the system it
+ * names yields to one the caller gives.
+ */
+ @Test
+ public void testExtendedPrecedence() {
+ final WellKnownText ewkt = new
WellKnownText(WellKnownText.Flavor.EWKT);
+ /*
+ * The OGC dialect reads SRID as a geometry type name, and rejects it
as unknown.
+ */
+ assertMalformed("SRID=4326;POINT (1 2)");
+ /*
+ * A system given by the caller wins over the one the text names.
+ */
+ final CoordinateReferenceSystem crs = Geometries.getUndefinedCRS(2);
+ assertSame(crs, ewkt.decode("SRID=4326;POINT (1 2)",
crs).getCoordinateReferenceSystem());
+ /*
+ * The system the prefix names must have as many dimensions as the
text has ordinates.
+ * EPSG:4326 is two dimensional, so a Z geometry contradicts it.
+ */
+ assertThrows(IllegalArgumentException.class, () ->
ewkt.decode("SRID=4326;POINT Z (1 2 3)"));
+ }
+
+ /**
+ * Tests the rejection of malformed prefixes.
+ */
+ @Test
+ public void testExtendedMalformed() {
+ final WellKnownText ewkt = new
WellKnownText(WellKnownText.Flavor.EWKT);
+ for (final String text : new String[] {
+ "SRID=;POINT (1 2)", // No identifier.
+ "SRID 4326;POINT (1 2)", // No '='.
+ "SRID=4326 POINT (1 2)", // No ';'.
+ "SRID=-1;POINT (1 2)", // Identifiers are unsigned.
+ "SRID=4326;", // No geometry.
+ "SRID=999999;POINT (1 2)"}) // Unknown identifier.
+ {
+ assertThrows(IllegalArgumentException.class, () ->
ewkt.decode(text), text);
+ }
+ }
+
+ /**
+ * Verifies that a dialect is required when one is asked for.
+ */
+ @Test
+ public void testNullFlavor() {
+ assertThrows(NullPointerException.class, () -> new
WellKnownText(null));
+ assertThrows(NullPointerException.class, () -> new WellKnownText(null,
3));
+ }
}