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 9fd2e0d706 feat(Geometry): implement WKB Reader and Writer
9fd2e0d706 is described below
commit 9fd2e0d706f72e874b016079ecfe29c80adaa77c
Author: jsorel <[email protected]>
AuthorDate: Wed Sep 16 14:37:37 2026 +0200
feat(Geometry): implement WKB Reader and Writer
---
.../main/org/apache/sis/geometries/Geometry.java | 8 +-
.../sis/geometries/adapter/WellKnownBinary.java | 525 +++++++++++++++-
.../geometries/adapter/WellKnownBinaryParser.java | 663 +++++++++++++++++++++
.../geometries/adapter/WellKnownBinaryTest.java | 414 +++++++++++++
4 files changed, 1607 insertions(+), 3 deletions(-)
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 70e5938beb..3997f5d419 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.WellKnownBinary;
import org.apache.sis.geometries.adapter.WellKnownText;
import org.apache.sis.geometries.internal.shared.AbstractGeometry;
import org.apache.sis.geometries.mesh.MeshPrimitive;
@@ -428,15 +429,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 WellKnownBinary}, which
documents the supported
+ * geometry types and the deviations from the format.</p>
+ *
* @return this geometry in Well-known Binary
+ * @throws IllegalArgumentException if this geometry has no Well-known
Binary representation.
*
* @see OGC Simple Feature Access 1.2.1 - 6.1.2.2
* @see ISO 19107:2019 - 6.4.4.3, 6.4.7
*/
@UML(identifier="asBinary", specification=ISO_19107)
default byte[] asBinary() {
- //TODO remove this method default when all classes implement it.
- throw new UnsupportedOperationException();
+ return new WellKnownBinary().encode(this);
}
/**
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 50df291e6f..2e393f4730 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
@@ -16,11 +16,534 @@
*/
package org.apache.sis.geometries.adapter;
+import java.nio.ByteOrder;
+import java.util.Arrays;
+import java.util.function.IntFunction;
+import org.apache.sis.geometries.AttributesType;
+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.opengis.referencing.crs.CoordinateReferenceSystem;
+
+
/**
- * TODO
+ * Encoder and decoder for the Well-Known Binary representation of geometries.
+ *
+ * <p>This is the binary counterpart of {@link WellKnownText}, and covers the
same geometry types:
+ * those 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). Every geometry is a byte
order flag, a type
+ * code and a body:</p>
+ *
+ * <blockquote><pre>
+ * byte byteOrder; // 0 = big endian (XDR), 1 = little endian (NDR)
+ * uint32 type; // geometry type, plus 1000 for Z, 2000 for M, 3000
for ZM
+ * … // body, which depends on the type
+ * </pre></blockquote>
+ *
+ * <p>The supported type codes, and the geometry type each one maps to,
are:</p>
+ *
+ * <table class="sis">
+ * <caption>Supported Well-Known Binary type codes</caption>
+ * <tr><th>Code</th> <th>Geometry type</th></tr>
+ * <tr><td>1</td> <td>{@link Point}</td></tr>
+ * <tr><td>2</td> <td>{@link LineString}</td></tr>
+ * <tr><td>3</td> <td>{@link Polygon}</td></tr>
+ * <tr><td>4</td> <td>{@link MultiPoint}</td></tr>
+ * <tr><td>5</td> <td>{@link MultiLineString}</td></tr>
+ * <tr><td>6</td> <td>{@link MultiPolygon}</td></tr>
+ * <tr><td>7</td> <td>{@link GeometryCollection}</td></tr>
+ * <tr><td>8</td> <td>{@link CircularString}</td></tr>
+ * <tr><td>9</td> <td>{@link CompoundCurve}</td></tr>
+ * <tr><td>10</td> <td>{@link CurvePolygon}</td></tr>
+ * <tr><td>11</td> <td>{@link MultiCurve}</td></tr>
+ * <tr><td>12</td> <td>{@link MultiSurface}</td></tr>
+ * <tr><td>15</td> <td>{@link PolyhedralSurface}</td></tr>
+ * <tr><td>16</td> <td>{@link TIN}</td></tr>
+ * <tr><td>17</td> <td>{@link Triangle}</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>
+ * As in {@link WellKnownText}, 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, written as
the ordinate following
+ * the position ones. A geometry whose position has neither 2 nor 3 dimensions
cannot be written.
+ * Unlike the text form, the binary form always states the flags, so nothing
is ever inferred from
+ * the width of a tuple.
+ *
+ * <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 Binary 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
Binary has no notion
+ * of orientation, so that information is lost.</li>
+ * <li>An {@link Empty} geometry is written as an empty {@code
GeometryCollection}, the only
+ * type-less empty form the format offers.</li>
+ * <li>The format has no empty form for a point, since a point is a bare
coordinate tuple with
+ * no count in front of it. The widespread convention of writing {@link
Double#NaN} ordinates
+ * is followed: an empty point is written that way, and a point whose
ordinates are all
+ * {@code NaN} is read as an {@link Empty} geometry rather than as a
{@code Point}, because
+ * the model has no empty point. It is therefore written back as an empty
+ * {@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
+ * {@link #decode(byte[], CoordinateReferenceSystem)}, decoded
geometries use
+ * {@link
org.apache.sis.geometries.Geometries#getUndefinedCRS(int)}.</li>
+ * </ul>
+ *
+ * <h2>Thread safety</h2>
+ * Instances are cheap to create, immutable, and safe for use by multiple
threads.
*
* @author Johann Sorel (Geomatys)
*/
public final class WellKnownBinary {
+ /**
+ * Value of the byte order flag for each of the two orders.
+ */
+ static final byte XDR = 0, NDR = 1;
+
+ /**
+ * 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
+ * named after a geometry type would obscure the type of the same name.
+ */
+ static final class Codes {
+ /**
+ * The codes of <cite>OGC Simple Feature Access 1.2.1</cite> and
<cite>ISO 13249-3</cite>.
+ * Codes 13 and 14, which stand for the abstract {@code Curve} and
{@code Surface} types,
+ * are deliberately absent: no geometry can be of an abstract type.
+ */
+ static final int
+ POINT = 1,
+ LINESTRING = 2,
+ POLYGON = 3,
+ MULTIPOINT = 4,
+ MULTILINESTRING = 5,
+ MULTIPOLYGON = 6,
+ GEOMETRYCOLLECTION = 7,
+ CIRCULARSTRING = 8,
+ COMPOUNDCURVE = 9,
+ CURVEPOLYGON = 10,
+ MULTICURVE = 11,
+ MULTISURFACE = 12,
+ POLYHEDRALSURFACE = 15,
+ TIN = 16,
+ TRIANGLE = 17;
+
+ /**
+ * Value added to a type code for the {@code Z}, {@code M} and {@code
ZM} flags.
+ * The {@code ZM} flag is the sum of the two others.
+ */
+ static final int Z_OFFSET = 1000, M_OFFSET = 2000;
+
+ /**
+ * Do not allow instantiation of this holder of constants.
+ */
+ private Codes() {
+ }
+ }
+
+ /**
+ * Byte order of the written geometries. Never null. Both orders are read
whatever this is.
+ */
+ private final ByteOrder byteOrder;
+
+ /**
+ * Creates a codec writing geometries in big endian order, also known as
XDR, which is the
+ * order of the byte sequences of the standard. Both orders are read.
+ */
+ public WellKnownBinary() {
+ byteOrder = ByteOrder.BIG_ENDIAN;
+ }
+
+ /**
+ * Creates a codec writing geometries in the given byte order. Both orders
are read whatever
+ * the order given here.
+ *
+ * @param byteOrder order of the multi-byte values to write, not null.
+ */
+ public WellKnownBinary(final ByteOrder byteOrder) {
+ ArgumentChecks.ensureNonNull("byteOrder", byteOrder);
+ this.byteOrder = byteOrder;
+ }
+
+ /**
+ * Returns the Well-Known Binary of the given geometry.
+ *
+ * @param geom the geometry to encode, not null.
+ * @return the geometry in Well-Known Binary.
+ * @throws IllegalArgumentException if the geometry, or one of the
geometries it contains,
+ * has no Well-Known Binary representation, or if its positions
are neither 2
+ * nor 3 dimensional.
+ */
+ public byte[] encode(final Geometry geom) {
+ ArgumentChecks.ensureNonNull("geom", geom);
+ final Output out = new Output(byteOrder);
+ format(out, geom);
+ return out.toArray();
+ }
+
+ /**
+ * Returns the geometry described by the given Well-Known Binary.
+ *
+ * @param geom the Well-Known Binary to decode, not null.
+ * @return the decoded geometry.
+ * @throws IllegalArgumentException if the bytes are malformed, or name a
geometry type which
+ * is not in the table of this class javadoc.
+ */
+ public Geometry decode(final byte[] geom) {
+ return decode(geom, null);
+ }
+
+ /**
+ * 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.
+ *
+ * @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}.
+ * @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}.
+ */
+ public Geometry decode(final byte[] geom, final CoordinateReferenceSystem
crs) {
+ ArgumentChecks.ensureNonNull("geom", geom);
+ return new WellKnownBinaryParser(geom, crs).parse();
+ }
+
+ // ////////////////////////////////////////////////////////////////////////
+ // Encoding ///////////////////////////////////////////////////////////////
+ // ////////////////////////////////////////////////////////////////////////
+
+ /**
+ * Writes the Well-Known Binary of the given geometry, header 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 WellKnownText.format(…)}.</p>
+ */
+ private void format(final Output out, final Geometry geometry) {
+ if (geometry instanceof Orientable o && o.getOrientationSign() ==
Orientable.Sign.NEGATIVE) {
+ format(out, o.getReverse()); // Orientation
is not representable.
+ } else if (geometry instanceof Empty g) {
+ writeHeader(out, Codes.GEOMETRYCOLLECTION, g);
+ out.writeInt(0);
+ } else if (geometry instanceof Point g) {
+ formatPoint(out, g);
+ } else if (geometry instanceof CircularString g) { // Before
Curve.
+ formatPointList(out, Codes.CIRCULARSTRING, g, g.getDataPoints());
+ } else if (geometry instanceof CompoundCurve g) { // Before
Curve.
+ formatCompoundCurve(out, g);
+ } else if (geometry instanceof LineString g) { // Also
matches LinearRing.
+ formatPointList(out, Codes.LINESTRING, g, g.getDataPoints());
+ } else if (geometry instanceof Triangle g) { // Before
Polygon.
+ formatPolygon(out, Codes.TRIANGLE, g);
+ } else if (geometry instanceof Polygon g) { // Before
Surface.
+ formatPolygon(out, Codes.POLYGON, g);
+ } else if (geometry instanceof CurvePolygon g) { // Before
Surface.
+ formatCurvePolygon(out, g);
+ } else if (geometry instanceof TIN g) { // Before
PolyhedralSurface.
+ formatPatches(out, Codes.TIN, g.getNumPatches(), g::getPatchN, g);
+ } else if (geometry instanceof PolyhedralSurface<?> g) {
+ formatPatches(out, Codes.POLYHEDRALSURFACE, g.getNumPatches(),
g::getPatchN, g);
+ } else if (geometry instanceof MultiPoint<?> g) { // Before
GeometryCollection.
+ formatMembers(out, Codes.MULTIPOINT, g);
+ } else if (geometry instanceof MultiLineString g) { // Before
MultiCurve.
+ formatMembers(out, Codes.MULTILINESTRING, g);
+ } else if (geometry instanceof MultiPolygon g) { // Before
MultiSurface.
+ formatMembers(out, Codes.MULTIPOLYGON, g);
+ } else if (geometry instanceof MultiCurve<?> g) { // Before
GeometryCollection.
+ formatMembers(out, Codes.MULTICURVE, g);
+ } else if (geometry instanceof MultiSurface<?> g) { // Before
GeometryCollection.
+ formatMembers(out, Codes.MULTISURFACE, g);
+ } else if (geometry instanceof GeometryCollection<?> g) {
+ formatMembers(out, Codes.GEOMETRYCOLLECTION, g);
+ } else {
+ throw unsupported(geometry);
+ }
+ }
+
+ /**
+ * Writes a point as a single coordinate tuple. An empty point has no form
of its own in this
+ * format and is written as a tuple of {@link Double#NaN} ordinates, the
usual convention.
+ */
+ private void formatPoint(final Output out, final Point geometry) {
+ final boolean hasM = writeHeader(out, Codes.POINT, geometry);
+ if (geometry.isEmpty()) {
+ final int dimension =
geometry.getCoordinateReferenceSystem().getCoordinateSystem().getDimension();
+ for (int i = hasM ? dimension + 1 : dimension; --i >= 0;) {
+ out.writeDouble(Double.NaN);
+ }
+ } else {
+ writePosition(out, geometry.getPosition(), hasM ?
geometry.getAttribute(AttributesType.ATT_M) : null);
+ }
+ }
+
+ /**
+ * Writes a type whose body is a single count of coordinate tuples
followed by those tuples.
+ */
+ private void formatPointList(final Output out, final int code,
+ final Geometry geometry, final DataPoints
points)
+ {
+ final boolean hasM = writeHeader(out, code, geometry);
+ writePointList(out, points, hasM);
+ }
+
+ /**
+ * Writes a compound curve as a count of components followed by those
components, each of them
+ * a complete geometry. Unlike the text form, a {@code LineString}
component keeps its header.
+ */
+ private void formatCompoundCurve(final Output out, final CompoundCurve
geometry) {
+ writeHeader(out, Codes.COMPOUNDCURVE, geometry);
+ final int n = geometry.getNumCurves();
+ out.writeInt(n);
+ for (int i = 0; i < n; i++) {
+ format(out, geometry.getCurveN(i));
+ }
+ }
+
+ /**
+ * Writes a polygon, or a triangle, as a count of rings followed by those
rings.
+ * A triangle has no interior ring, so its count is 1 unless it is empty.
+ */
+ private void formatPolygon(final Output out, final int code, final Polygon
geometry) {
+ final boolean hasM = writeHeader(out, code, geometry);
+ writeRings(out, geometry, hasM);
+ }
+
+ /**
+ * Writes a curve polygon as a count of rings followed by those rings,
each of them a complete
+ * geometry. This is what lets the rings be of any curve type.
+ */
+ private void formatCurvePolygon(final Output out, final CurvePolygon
geometry) {
+ writeHeader(out, Codes.CURVEPOLYGON, geometry);
+ if (geometry.isEmpty()) {
+ out.writeInt(0);
+ return;
+ }
+ final int n = geometry.getNumInteriorRing();
+ out.writeInt(n + 1);
+ format(out, geometry.getExteriorRing());
+ for (int i = 0; i < n; i++) {
+ format(out, geometry.getInteriorRingN(i));
+ }
+ }
+
+ /**
+ * Writes a polyhedral surface, or a TIN, as a count of patches followed
by those patches,
+ * each of them a complete {@code POLYGON} or {@code TRIANGLE} geometry.
+ */
+ private void formatPatches(final Output out, final int code, final int
count,
+ final IntFunction<? extends Polygon> patches,
+ final Geometry geometry)
+ {
+ writeHeader(out, code, geometry);
+ out.writeInt(count);
+ for (int i = 0; i < count; i++) {
+ format(out, patches.apply(i));
+ }
+ }
+
+ /**
+ * Writes a collection as a count of members followed by those members,
each of them a
+ * complete geometry. Every collection type shares this body, the type
code alone saying
+ * what the members are allowed to be.
+ */
+ private void formatMembers(final Output out, final int code, final
GeometryCollection<?> geometry) {
+ writeHeader(out, code, geometry);
+ final int n = geometry.getNumGeometries();
+ out.writeInt(n);
+ for (int i = 0; i < n; i++) {
+ format(out, geometry.getGeometryN(i));
+ }
+ }
+
+ /**
+ * Writes the rings of a polygon: a count followed by one coordinate list
per ring.
+ * A triangle is written the same way: its exterior ring already closes on
its first corner.
+ */
+ private void writeRings(final Output out, final Polygon polygon, final
boolean hasM) {
+ if (polygon.isEmpty()) {
+ out.writeInt(0);
+ return;
+ }
+ final int n = polygon.getNumInteriorRing();
+ out.writeInt(n + 1);
+ writePointList(out, polygon.getExteriorRing().getDataPoints(), hasM);
+ for (int i = 0; i < n; i++) {
+ writePointList(out, polygon.getInteriorRingN(i).getDataPoints(),
hasM);
+ }
+ }
+
+ /**
+ * Writes a count of coordinate tuples followed by those tuples.
+ */
+ private void writePointList(final Output out, final DataPoints points,
final boolean hasM) {
+ final int n = points.size();
+ out.writeInt(n);
+ for (int i = 0; i < n; i++) {
+ writePosition(out, points.getPosition(i), hasM ?
points.getAttribute(i, AttributesType.ATT_M) : null);
+ }
+ }
+
+ /**
+ * Writes the ordinates of one position, followed by its measure if any.
+ */
+ private void writePosition(final Output out, final Tuple<?> position,
final Tuple<?> measure) {
+ for (int i = 0, n = position.getDimension(); i < n; i++) {
+ out.writeDouble(position.get(i));
+ }
+ if (measure != null) {
+ out.writeDouble(measure.get(0));
+ }
+ }
+
+ /**
+ * Writes the byte order flag and the type code of a geometry, the
dimension flags included.
+ *
+ * @return whether the positions carry a measure, which the caller has to
write as the
+ * ordinate following the position ones.
+ */
+ private boolean writeHeader(final Output out, final int code, final
Geometry geometry) {
+ final CoordinateReferenceSystem crs =
geometry.getCoordinateReferenceSystem();
+ if (crs == null) {
+ throw new IllegalArgumentException("Cannot write a " +
geometry.getGeometryType()
+ + " in Well-Known Binary:"
+ + " 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);
+ final int flags;
+ 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;
+ 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.");
+ }
+ out.writeByteOrder();
+ out.writeInt(code + flags);
+ return hasM;
+ }
+
+ /**
+ * 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 Binary defines no
representation for "
+ + geometry.getClass().getSimpleName() + '.');
+ }
+
+ /**
+ * Growable sequence of bytes, writing the multi-byte values in the order
given to the
+ * constructor. A {@code ByteBuffer} is not used because the length of the
result is not
+ * known before the geometry has been walked.
+ */
+ private static final class Output {
+ /**
+ * The bytes written so far. Only the first {@link #length} elements
are meaningful.
+ */
+ private byte[] array = new byte[64];
+
+ /**
+ * Number of meaningful bytes in {@link #array}.
+ */
+ private int length;
+
+ /**
+ * Whether the multi-byte values are written most significant byte
first.
+ */
+ private final boolean bigEndian;
+
+ /**
+ * Creates an initially empty output writing in the given byte order.
+ */
+ Output(final ByteOrder byteOrder) {
+ bigEndian = (byteOrder == ByteOrder.BIG_ENDIAN);
+ }
+
+ /**
+ * Writes the flag which tells in which order the values after it are
written.
+ */
+ void writeByteOrder() {
+ ensure(1);
+ array[length++] = bigEndian ? XDR : NDR;
+ }
+
+ /**
+ * Writes a 32 bits integer, which the format uses for the type codes
and the counts.
+ */
+ void writeInt(final int value) {
+ ensure(Integer.BYTES);
+ for (int i = 0; i < Integer.BYTES; i++) {
+ final int shift = 8 * (bigEndian ? Integer.BYTES - 1 - i : i);
+ array[length++] = (byte) (value >>> shift);
+ }
+ }
+
+ /**
+ * Writes an IEEE 754 double precision number, which the format uses
for the ordinates.
+ */
+ void writeDouble(final double value) {
+ final long bits = Double.doubleToLongBits(value);
+ ensure(Double.BYTES);
+ for (int i = 0; i < Double.BYTES; i++) {
+ final int shift = 8 * (bigEndian ? Double.BYTES - 1 - i : i);
+ array[length++] = (byte) (bits >>> shift);
+ }
+ }
+
+ /**
+ * Makes room for the given number of additional bytes.
+ */
+ private void ensure(final int count) {
+ if (length + count > array.length) {
+ array = Arrays.copyOf(array, Math.max(length + count,
array.length * 2));
+ }
+ }
+ /**
+ * Returns the bytes written so far, in a array of exactly the right
length.
+ */
+ byte[] toArray() {
+ return Arrays.copyOf(array, length);
+ }
+ }
}
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
new file mode 100644
index 0000000000..194ac06624
--- /dev/null
+++
b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/adapter/WellKnownBinaryParser.java
@@ -0,0 +1,663 @@
+/*
+ * 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.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.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 Binary representation of
geometries.
+ * One instance parses one byte sequence and is then discarded.
+ *
+ * <p>The structure of the bodies, the set of accepted type codes and the
handling of the
+ * {@code Z}, {@code M} and {@code ZM} flags are documented on {@link
WellKnownBinary}, which is
+ * the public face of this class. Everything below is that structure written
as one method per
+ * type.</p>
+ *
+ * <h2>Dimensions</h2>
+ * A Well-Known Binary is homogeneous: a single geometry cannot mix 2- and
3-dimensional positions,
+ * because an Apache SIS {@link DataPoints} reports one dimension only. Unlike
the text form, every
+ * element states its flags, so nothing has to be inferred; but the flags of
the outermost element
+ * still fix them for the whole sequence, and a nested element contradicting
them is an error.
+ *
+ * <h2>Byte order</h2>
+ * The order is not parser state: each element carries its own flag and the
standard allows them to
+ * differ, so {@link #bigEndian} is re-read at the start of every element and
applies until the next
+ * one is read.
+ *
+ * @author Johann Sorel (Geomatys)
+ */
+final class WellKnownBinaryParser {
+ /**
+ * Bits of the dimension flags, which are the thousands of a type code.
+ */
+ 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 bytes being parsed.
+ */
+ 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.
+ */
+ private final CoordinateReferenceSystem userCRS;
+
+ /**
+ * Index in {@link #data} of the next byte to read.
+ */
+ private int pos;
+
+ /**
+ * Whether the multi-byte values of the element being read are most
significant byte first.
+ */
+ private boolean bigEndian;
+
+ /**
+ * Whether {@link #hasZ} and {@link #hasM} have been established by the
flags of an element.
+ */
+ 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 bytes.
+ *
+ * @param data the Well-Known Binary to parse.
+ * @param crs the coordinate reference system to give to the
geometries, or {@code null}.
+ */
+ WellKnownBinaryParser(final byte[] data, final CoordinateReferenceSystem
crs) {
+ this.data = data;
+ this.userCRS = crs;
+ }
+
+ /**
+ * Parses the whole byte sequence as a single geometry.
+ *
+ * @throws IllegalArgumentException if the bytes are malformed or name an
unsupported type.
+ */
+ Geometry parse() {
+ final Geometry geometry = parseGeometry();
+ if (pos < data.length) {
+ throw error("Unexpected bytes after the end of the geometry");
+ }
+ return geometry;
+ }
+
+ // ////////////////////////////////////////////////////////////////////////
+ // Productions ////////////////////////////////////////////////////////////
+ // ////////////////////////////////////////////////////////////////////////
+
+ /**
+ * Parses one complete geometry: a byte order flag, a type code and a body.
+ */
+ private Geometry parseGeometry() {
+ readByteOrder();
+ final int code = readInt();
+ 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");
+ }
+ final int flags = code / 1000;
+ if (flags > 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");
+ }
+ applyFlags(flags);
+ switch (code % 1000) {
+ case WellKnownBinary.Codes.POINT: return
parsePoint();
+ case WellKnownBinary.Codes.LINESTRING: return
GeometryFactory.createLineString(readPointList());
+ case WellKnownBinary.Codes.CIRCULARSTRING: return
GeometryFactory.createCircularString(readPointList());
+ case WellKnownBinary.Codes.COMPOUNDCURVE: return
parseCompoundCurve();
+ case WellKnownBinary.Codes.POLYGON: return
readPolygonBody();
+ case WellKnownBinary.Codes.TRIANGLE: return
readTriangleBody();
+ case WellKnownBinary.Codes.CURVEPOLYGON: return
parseCurvePolygon();
+ case WellKnownBinary.Codes.POLYHEDRALSURFACE: return
parsePolyhedralSurface();
+ case WellKnownBinary.Codes.TIN: return parseTIN();
+ case WellKnownBinary.Codes.MULTIPOINT: return
parseMultiPoint();
+ case WellKnownBinary.Codes.MULTILINESTRING: return
parseMultiLineString();
+ case WellKnownBinary.Codes.MULTICURVE: return
parseMultiCurve();
+ case WellKnownBinary.Codes.MULTIPOLYGON: return
parseMultiPolygon();
+ case WellKnownBinary.Codes.MULTISURFACE: return
parseMultiSurface();
+ case WellKnownBinary.Codes.GEOMETRYCOLLECTION: return
parseGeometryCollection();
+ default: throw error("Well-Known Binary defines no geometry type
of code " + code);
+ }
+ }
+
+ /**
+ * Parses the single coordinate tuple of a point.
+ *
+ * <p>A tuple of {@link Double#NaN} ordinates is the usual way of writing
an empty point, and
+ * 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. It maps to
{@link Empty} instead,
+ * the geometry which stands for the empty point set whatever its type,
and which is written
+ * back as an empty {@code GEOMETRYCOLLECTION}.</p>
+ */
+ private Geometry parsePoint() {
+ final double[] tuple = readTuple();
+ for (final double ordinate : tuple) {
+ if (!Double.isNaN(ordinate)) {
+ final Coordinates c = new Coordinates();
+ c.add(tuple);
+ return GeometryFactory.createPoint(c.build());
+ }
+ }
+ return GeometryFactory.createEmpty(crs());
+ }
+
+ /**
+ * Parses a count of components followed by that many complete curves.
+ */
+ private CompoundCurve parseCompoundCurve() {
+ final int n = readCount();
+ if (n == 0) {
+ return GeometryFactory.createCompoundCurve(crs());
+ }
+ final Curve[] curves = new Curve[n];
+ for (int i = 0; i < n; i++) {
+ curves[i] = readMember(Curve.class, CompoundCurve.TYPE);
+ }
+ return GeometryFactory.createCompoundCurve(curves);
+ }
+
+ /**
+ * Parses a count of rings followed by that many complete curves, the
first one being the
+ * exterior ring. A count of zero is the empty curve polygon.
+ */
+ private CurvePolygon parseCurvePolygon() {
+ final int n = readCount();
+ if (n == 0) {
+ return GeometryFactory.createCurvePolygon(emptyRing(), List.of());
+ }
+ final Curve exterior = readMember(Curve.class, CurvePolygon.TYPE);
+ final List<Curve> interiors = new ArrayList<>(n - 1);
+ for (int i = 1; i < n; i++) {
+ interiors.add(readMember(Curve.class, CurvePolygon.TYPE));
+ }
+ return GeometryFactory.createCurvePolygon(exterior, interiors);
+ }
+
+ /**
+ * Parses a count of patches followed by that many complete polygons.
+ */
+ private PolyhedralSurface<Polygon> parsePolyhedralSurface() {
+ final int n = readCount();
+ if (n == 0) {
+ return GeometryFactory.createPolyhedralSurface(crs(), new
Polygon[0]);
+ }
+ final Polygon[] patches = new Polygon[n];
+ for (int i = 0; i < n; i++) {
+ patches[i] = readMember(Polygon.class, PolyhedralSurface.TYPE);
+ }
+ return GeometryFactory.createPolyhedralSurface(patches);
+ }
+
+ /**
+ * Parses a count of patches followed by that many complete triangles.
+ */
+ private TIN parseTIN() {
+ final int n = readCount();
+ if (n == 0) {
+ return GeometryFactory.createTIN(crs(), new Triangle[0]);
+ }
+ final Triangle[] patches = new Triangle[n];
+ for (int i = 0; i < n; i++) {
+ patches[i] = readMember(Triangle.class, TIN.TYPE);
+ }
+ return GeometryFactory.createTIN(patches);
+ }
+
+ /**
+ * Parses a count of members followed by that many complete points.
+ */
+ private MultiPoint<?> parseMultiPoint() {
+ final int n = readCount();
+ if (n == 0) {
+ return GeometryFactory.createMultiPoint(crs());
+ }
+ final Point[] members = new Point[n];
+ for (int i = 0; i < n; i++) {
+ // See parsePoint(): an empty point decodes to Empty, which is not
a Point.
+ members[i] = readMember(Point.class, MultiPoint.TYPE);
+ }
+ return GeometryFactory.createMultiPoint(members);
+ }
+
+ /**
+ * Parses a count of members followed by that many complete line strings.
+ */
+ private MultiLineString parseMultiLineString() {
+ final int n = readCount();
+ if (n == 0) {
+ return GeometryFactory.createMultiLineString(crs());
+ }
+ final LineString[] members = new LineString[n];
+ for (int i = 0; i < n; i++) {
+ members[i] = readMember(LineString.class, MultiLineString.TYPE);
+ }
+ return GeometryFactory.createMultiLineString(members);
+ }
+
+ /**
+ * Parses a count of members followed by that many complete polygons.
+ */
+ private MultiPolygon parseMultiPolygon() {
+ final int n = readCount();
+ if (n == 0) {
+ return GeometryFactory.createMultiPolygon(crs());
+ }
+ final Polygon[] members = new Polygon[n];
+ for (int i = 0; i < n; i++) {
+ members[i] = readMember(Polygon.class, MultiPolygon.TYPE);
+ }
+ return GeometryFactory.createMultiPolygon(members);
+ }
+
+ /**
+ * Parses a count of members followed by that many complete curves, which
unlike those of a
+ * {@code MULTILINESTRING} may be of any curve type.
+ */
+ private MultiCurve<Curve> parseMultiCurve() {
+ final int n = readCount();
+ if (n == 0) {
+ return GeometryFactory.<Curve>createMultiCurve(crs());
+ }
+ final Curve[] members = new Curve[n];
+ for (int i = 0; i < n; i++) {
+ members[i] = readMember(Curve.class, MultiCurve.TYPE);
+ }
+ return GeometryFactory.createMultiCurve(members);
+ }
+
+ /**
+ * Parses a count of members followed by that many complete surfaces,
which unlike those of a
+ * {@code MULTIPOLYGON} may be of any surface type.
+ */
+ private MultiSurface<Surface> parseMultiSurface() {
+ final int n = readCount();
+ if (n == 0) {
+ return GeometryFactory.<Surface>createMultiSurface(crs());
+ }
+ final Surface[] members = new Surface[n];
+ for (int i = 0; i < n; i++) {
+ members[i] = readMember(Surface.class, MultiSurface.TYPE);
+ }
+ return GeometryFactory.createMultiSurface(members);
+ }
+
+ /**
+ * Parses a count of members followed by that many complete geometries of
any type.
+ */
+ private GeometryCollection<Geometry> parseGeometryCollection() {
+ final int n = readCount();
+ if (n == 0) {
+ return GeometryFactory.<Geometry>createGeometryCollection(crs());
+ }
+ final Geometry[] members = new Geometry[n];
+ for (int i = 0; i < n; i++) {
+ members[i] = parseGeometry();
+ }
+ return GeometryFactory.createGeometryCollection(members);
+ }
+
+ // ////////////////////////////////////////////////////////////////////////
+ // Shared productions /////////////////////////////////////////////////////
+ // ////////////////////////////////////////////////////////////////////////
+
+ /**
+ * Parses a count of rings followed by that many coordinate lists, without
header.
+ * A count of zero is the empty polygon.
+ */
+ private Polygon readPolygonBody() {
+ final List<LinearRing> rings = readRings();
+ if (rings.isEmpty()) {
+ return GeometryFactory.createPolygon(emptyRing(), List.of());
+ }
+ return GeometryFactory.createPolygon(rings.get(0), new
ArrayList<>(rings.subList(1, rings.size())));
+ }
+
+ /**
+ * Parses the single ring of a triangle, in the same form as the rings of
a polygon.
+ */
+ private Triangle readTriangleBody() {
+ final List<LinearRing> rings = readRings();
+ if (rings.isEmpty()) {
+ return GeometryFactory.createTriangle(emptyRing());
+ }
+ 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 a count of rings followed by that many coordinate lists.
+ */
+ private List<LinearRing> readRings() {
+ final int n = readCount();
+ final List<LinearRing> rings = new ArrayList<>(n);
+ for (int i = 0; i < n; i++) {
+ rings.add(GeometryFactory.createLinearRing(readPointList()));
+ }
+ return rings;
+ }
+
+ /**
+ * Parses a count of coordinate tuples followed by that many tuples.
+ */
+ private DataPoints readPointList() {
+ final int n = readCount();
+ final Coordinates coordinates = new Coordinates();
+ for (int i = 0; i < n; i++) {
+ coordinates.add(readTuple());
+ }
+ return coordinates.build();
+ }
+
+ /**
+ * Returns an empty ring, for the zero counts of the types which are made
of rings.
+ */
+ private LinearRing emptyRing() {
+ return GeometryFactory.createLinearRing(new Coordinates().build());
+ }
+
+ /**
+ * Parses one complete geometry and casts it to the type its container
requires.
+ *
+ * @param type the type the container accepts.
+ * @param container name of the container type, for the error message.
+ */
+ private <T> T readMember(final Class<T> type, final String container) {
+ final Geometry geometry = parseGeometry();
+ 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
bytes have established
+ * that positions carry a <var>z</var> ordinate.
+ */
+ 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 announced by the flags.
+ */
+ 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 bytes have " + dimension + " ordinates per
position");
+ }
+ return userCRS;
+ }
+
+ /**
+ * Records the dimension flags of an element. The outermost element fixes
them for the whole
+ * sequence; a nested element may repeat them but not contradict them.
+ */
+ private void applyFlags(final int flags) {
+ 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 by the
enclosing geometry");
+ }
+ } else {
+ hasZ = z;
+ hasM = m;
+ 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 bytes 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. Every tuple has the width the
dimension flags announce,
+ * since {@link WellKnownBinaryParser#readTuple()} reads exactly that
many ordinates.
+ */
+ void add(final double[] tuple) {
+ width = tuple.length;
+ 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 bytes,
+ * 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);
+ }
+ }
+
+ // ////////////////////////////////////////////////////////////////////////
+ // Primitives /////////////////////////////////////////////////////////////
+ // ////////////////////////////////////////////////////////////////////////
+
+ /**
+ * Reads the ordinates of one coordinate tuple. Their number is not
written anywhere:
+ * it is the one the dimension flags of the element announce.
+ */
+ private double[] readTuple() {
+ final double[] tuple = new double[positionDimension() + (hasM ? 1 :
0)];
+ for (int i = 0; i < tuple.length; i++) {
+ tuple[i] = readDouble();
+ }
+ return tuple;
+ }
+
+ /**
+ * Reads a count of elements, and verifies that the bytes which remain
could hold that many.
+ * The check costs nothing and keeps a malformed count from asking for a
huge allocation:
+ * no element of any type is written in less than one byte.
+ */
+ private int readCount() {
+ final int count = readInt();
+ if (count < 0 || count > data.length - pos) {
+ throw error("A count of " + Integer.toUnsignedString(count) + "
elements exceeds the "
+ + (data.length - pos) + " bytes which remain");
+ }
+ return count;
+ }
+
+ /**
+ * Reads the flag which tells in which order the values after it are
written.
+ */
+ private void readByteOrder() {
+ final byte flag = readByte();
+ switch (flag) {
+ case WellKnownBinary.XDR: bigEndian = true; break;
+ case WellKnownBinary.NDR: bigEndian = false; break;
+ default: throw error("Byte order flag " + flag + " is neither 0
for big endian nor 1 for little endian");
+ }
+ }
+
+ /**
+ * Reads one byte.
+ */
+ private byte readByte() {
+ if (pos >= data.length) {
+ throw error("Unexpected end of the geometry");
+ }
+ return data[pos++];
+ }
+
+ /**
+ * Reads a 32 bits integer in the byte order of the element being read.
+ */
+ private int readInt() {
+ if (pos + Integer.BYTES > data.length) {
+ throw error("Unexpected end of the geometry");
+ }
+ int value = 0;
+ for (int i = 0; i < Integer.BYTES; i++) {
+ final int shift = 8 * (bigEndian ? Integer.BYTES - 1 - i : i);
+ value |= (data[pos++] & 0xFF) << shift;
+ }
+ return value;
+ }
+
+ /**
+ * Reads an IEEE 754 double precision number in the byte order of the
element being read.
+ */
+ private double readDouble() {
+ if (pos + Double.BYTES > data.length) {
+ throw error("Unexpected end of the geometry");
+ }
+ long bits = 0;
+ for (int i = 0; i < Double.BYTES; i++) {
+ final int shift = 8 * (bigEndian ? Double.BYTES - 1 - i : i);
+ bits |= (long) (data[pos++] & 0xFF) << shift;
+ }
+ return Double.longBitsToDouble(bits);
+ }
+
+ /**
+ * Returns the exception to throw for malformed bytes, pointing at the
current position.
+ */
+ private IllegalArgumentException error(final String message) {
+ return new IllegalArgumentException(message + ", at offset " + pos + "
of a Well-Known Binary of "
+ + data.length + " bytes");
+ }
+}
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
new file mode 100644
index 0000000000..175f4920eb
--- /dev/null
+++
b/incubator/src/org.apache.sis.geometry/test/org/apache/sis/geometries/adapter/WellKnownBinaryTest.java
@@ -0,0 +1,414 @@
+/*
+ * 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.nio.ByteOrder;
+import java.util.Arrays;
+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 WellKnownBinary}.
+ *
+ * <p>Geometries are built from their Well-Known Text rather than from the
factory, so that a test
+ * case says in one readable line what it encodes. {@link WellKnownTextTest}
is what verifies that
+ * those texts mean what they look like.</p>
+ *
+ * @author Johann Sorel (Geomatys)
+ */
+public final class WellKnownBinaryTest {
+ /**
+ * Bits of {@link Double#NaN}, which is how an empty point is written.
+ */
+ private static final String NAN = "7FF8000000000000";
+
+ /**
+ * The codec under test, writing in the big endian order of the standard.
+ */
+ private final WellKnownBinary wkb = new WellKnownBinary();
+
+ /**
+ * The text codec, used for building the geometries to encode and for
reading back the
+ * geometries decoded.
+ */
+ private final WellKnownText wkt = new WellKnownText();
+
+ /**
+ * Creates a new test case.
+ */
+ public WellKnownBinaryTest() {
+ }
+
+ /**
+ * Verifies that the geometry of the given text encodes to bytes which
decode back to a
+ * geometry of the expected type and of the same text. The text is the
canonical form of the
+ * encoder of {@link WellKnownText}, 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 source = wkt.decode(text);
+ final byte[] bytes = wkb.encode(source);
+ final Geometry target = wkb.decode(bytes);
+ assertInstanceOf(type, target, text);
+ assertEquals(text, wkt.encode(target), "Round trip of " + text);
+ assertArrayEquals(bytes, source.asBinary(), "Geometry.asBinary() 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 header, 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, which are the
thousands of a type code.
+ */
+ @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 collection repeats the flag in the type code of each of its
members,
+ * since each member carries its own header.
+ */
+ assertRoundTrip(GeometryCollection.class, "GEOMETRYCOLLECTION Z (POINT
Z (1 2 3))");
+ /*
+ * The flags are added to the type code: 1000 for Z, 2000 for M, 3000
for ZM.
+ */
+
assertEquals("00000003E93FF000000000000040000000000000004008000000000000",
+ hex(wkb.encode(wkt.decode("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 byte[] bytes = wkb.encode(wkt.decode("LINESTRING ZM (0 1 2 5, 3
4 5 6)"));
+ final LineString line = assertInstanceOf(LineString.class,
wkb.decode(bytes));
+ 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 geometry carries no measure at
all.
+ */
+ final LineString plain = assertInstanceOf(LineString.class,
+ wkb.decode(wkb.encode(wkt.decode("LINESTRING (0 1, 3 4)"))));
+
assertFalse(plain.getDataPoints().getAttributesType().getAttributeNames().contains(AttributesType.ATT_M));
+ }
+
+ /**
+ * Tests the empty form, a count of zero, 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");
+ /*
+ * An empty geometry is a header and a count of zero, nothing more.
+ */
+ assertEquals("000000000700000000",
hex(wkb.encode(wkt.decode("GEOMETRYCOLLECTION EMPTY"))));
+ assertEquals("000000000200000000",
hex(wkb.encode(wkt.decode("LINESTRING EMPTY"))));
+ assertEquals("000000000300000000", hex(wkb.encode(wkt.decode("POLYGON
EMPTY"))));
+ }
+
+ /**
+ * Verifies that an {@link Empty} geometry, which has no type of its own,
is written as the
+ * type-less empty form of the format.
+ */
+ @Test
+ public void testEmptyGeometry() {
+ final Geometry empty =
GeometryFactory.createEmpty(Geometries.getUndefinedCRS(2));
+ assertEquals("000000000700000000", hex(wkb.encode(empty)));
+ assertEquals("000000000700000000", hex(empty.asBinary()));
+ }
+
+ /**
+ * Verifies the one empty form which does not round-trip: the format
writes a point as a bare
+ * coordinate tuple, so an empty point is a tuple of {@link Double#NaN},
and the model has no
+ * empty point to decode it into.
+ */
+ @Test
+ public void testEmptyPoint() {
+ final byte[] empty2D = bytes("00" + "00000001" + NAN + NAN);
+ final byte[] empty3D = bytes("00" + "000003E9" + NAN + NAN + NAN);
+ assertInstanceOf(Empty.class, wkb.decode(empty2D));
+ assertInstanceOf(Empty.class, wkb.decode(empty3D));
+ assertEquals("000000000700000000",
hex(wkb.encode(wkb.decode(empty2D))));
+ /*
+ * A single NaN ordinate is a legitimate coordinate, not an empty
point.
+ */
+ assertInstanceOf(Point.class, wkb.decode(bytes("00" + "00000001" + NAN
+ "4000000000000000")));
+ /*
+ * There is therefore no way to put an empty point in a multi point.
+ */
+ assertMalformed(bytes("00" + "00000004" + "00000001" + "00" +
"00000001" + NAN + NAN));
+ }
+
+ /**
+ * Tests the byte order flag: both orders are written on demand, both are
read whatever the
+ * order the codec writes, and the order may change from one nested
geometry to the next.
+ */
+ @Test
+ public void testByteOrder() {
+ final Geometry point = wkt.decode("POINT (1 2)");
+ final byte[] bigEndian = new
WellKnownBinary(ByteOrder.BIG_ENDIAN).encode(point);
+ final byte[] littleEndian = new
WellKnownBinary(ByteOrder.LITTLE_ENDIAN).encode(point);
+ assertEquals("00000000013FF00000000000004000000000000000",
hex(bigEndian));
+ assertEquals("0101000000000000000000F03F0000000000000040",
hex(littleEndian));
+ assertArrayEquals(bigEndian, wkb.encode(point), "Big endian is the
default");
+ /*
+ * Whichever order the codec writes, it reads both.
+ */
+ final WellKnownBinary reader = new
WellKnownBinary(ByteOrder.LITTLE_ENDIAN);
+ assertEquals("POINT (1 2)", wkt.encode(reader.decode(bigEndian)));
+ assertEquals("POINT (1 2)", wkt.encode(reader.decode(littleEndian)));
+ /*
+ * A collection written in one order may hold a member written in the
other.
+ */
+ 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));
+ }
+
+ /**
+ * Tests decoding in a coordinate reference system given by the caller.
+ */
+ @Test
+ public void testDecodeWithCRS() {
+ final CoordinateReferenceSystem crs = Geometries.getUndefinedCRS(3);
+ final byte[] bytes = wkb.encode(wkt.decode("POINT Z (1 2 3)"));
+ final Geometry geometry = wkb.decode(bytes, crs);
+ assertSame(crs, geometry.getCoordinateReferenceSystem());
+ /*
+ * The number of ordinates the flags announce and the dimension of the
system must agree.
+ */
+ final byte[] flat = wkb.encode(wkt.decode("POINT (1 2)"));
+ assertThrows(IllegalArgumentException.class, () -> wkb.decode(flat,
crs));
+ }
+
+ /**
+ * Verifies that the geometry types which the format does not define are
rejected
+ * rather than written in an invented structure.
+ */
+ @Test
+ public void testUnsupportedType() {
+ final DataPoints points = GeometryFactory.createSequence(
+ NDArrays.of(SampleSystem.of(Geometries.getUndefinedCRS(2)), 0,
0, 1, 1));
+ assertThrows(IllegalArgumentException.class, () ->
wkb.encode(GeometryFactory.createGeodesic(points)));
+ assertThrows(IllegalArgumentException.class, () ->
wkb.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, () -> wkb.encode(point));
+ }
+
+ /**
+ * Tests the rejection of malformed byte sequences built by altering a
valid one.
+ */
+ @Test
+ public void testTruncatedAndTrailing() {
+ final byte[] point = wkb.encode(wkt.decode("POINT (1 2)"));
+ assertMalformed(new byte[0]); // Not
even a byte order flag.
+ assertMalformed(Arrays.copyOf(point, 1)); // No
type code.
+ assertMalformed(Arrays.copyOf(point, point.length - 1)); //
Truncated ordinate.
+ assertMalformed(Arrays.copyOf(point, point.length + 1)); //
Trailing byte.
+ final byte[] badOrder = point.clone();
+ badOrder[0] = 2; //
Neither 0 nor 1.
+ assertMalformed(badOrder);
+ }
+
+ /**
+ * Tests the rejection of type codes which the format does not define.
+ */
+ @Test
+ public void testMalformedTypeCode() {
+ assertMalformed(bytes("00" + "00000000" + "00000000")); //
Code 0.
+ assertMalformed(bytes("00" + "0000000D" + "00000000")); //
Abstract Curve type.
+ assertMalformed(bytes("00" + "000000FF" + "00000000")); //
Unknown code.
+ assertMalformed(bytes("00" + "00001389" + "00000000")); //
Flags of 5000.
+ assertMalformed(bytes("00" + "80000001" + "00000000")); //
Extended WKB flags.
+ }
+
+ /**
+ * Tests the rejection of the structural errors which the counts and the
type codes can carry.
+ */
+ @Test
+ public void testMalformedStructure() {
+ // A count larger than the bytes which remain.
+ assertMalformed(bytes("00" + "00000002" + "7FFFFFFF"));
+ // A geometry collection announcing Z, holding a member which does not.
+ assertMalformed(concat(bytes("00" + "000003EF" + "00000001"),
+ wkb.encode(wkt.decode("POINT (1 2)"))));
+ // A triangle with an interior ring.
+ assertMalformed(swapTypeCode(wkb.encode(wkt.decode(
+ "POLYGON ((0 0, 4 0, 4 4, 0 0), (1 1, 2 1, 2 2, 1 1))")), 17));
+ // A multi curve whose member is a polygon.
+ assertMalformed(concat(bytes("00" + "0000000B" + "00000001"),
+ wkb.encode(wkt.decode("POLYGON ((0 0, 1 0, 1 1,
0 0))"))));
+ // A multi line string whose member is a circular string.
+ assertMalformed(concat(bytes("00" + "00000005" + "00000001"),
+ wkb.encode(wkt.decode("CIRCULARSTRING (0 0, 1
1, 2 0)"))));
+ }
+
+ /**
+ * Verifies that the given bytes are rejected as malformed.
+ */
+ private void assertMalformed(final byte[] data) {
+ assertThrows(IllegalArgumentException.class, () -> wkb.decode(data),
hex(data));
+ }
+
+ /**
+ * 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.
+ */
+ private static byte[] swapTypeCode(final byte[] data, final int code) {
+ final byte[] copy = data.clone();
+ final int flags = ((data[1] & 0xFF) << 24 | (data[2] & 0xFF) << 16
+ | (data[3] & 0xFF) << 8 | (data[4] & 0xFF)) / 1000 *
1000;
+ final int value = flags + code;
+ copy[1] = (byte) (value >>> 24);
+ copy[2] = (byte) (value >>> 16);
+ copy[3] = (byte) (value >>> 8);
+ copy[4] = (byte) value;
+ return copy;
+ }
+
+ /**
+ * Returns the concatenation of the given byte sequences.
+ */
+ private static byte[] concat(final byte[]... parts) {
+ int length = 0;
+ for (final byte[] part : parts) {
+ length += part.length;
+ }
+ final byte[] result = new byte[length];
+ int offset = 0;
+ for (final byte[] part : parts) {
+ System.arraycopy(part, 0, result, offset, part.length);
+ offset += part.length;
+ }
+ return result;
+ }
+
+ /**
+ * Returns the bytes of the given hexadecimal text, which must have an
even number of digits.
+ */
+ private static byte[] bytes(final String text) {
+ assertEquals(0, text.length() % 2, text);
+ final byte[] data = new byte[text.length() / 2];
+ for (int i = 0; i < data.length; i++) {
+ data[i] = (byte) Integer.parseInt(text.substring(i * 2, i * 2 +
2), 16);
+ }
+ return data;
+ }
+
+ /**
+ * Returns the given bytes as upper case hexadecimal digits, which is how
the expected values
+ * of this class are written.
+ */
+ private static String hex(final byte[] data) {
+ final String digits = "0123456789ABCDEF";
+ final StringBuilder sb = new StringBuilder(data.length * 2);
+ for (final byte b : data) {
+ sb.append(digits.charAt((b >>> 4) & 0xF)).append(digits.charAt(b &
0xF));
+ }
+ return sb.toString();
+ }
+}