diff --git a/modules/core/src/main/java/org/locationtech/jts/io/WKBReader.java b/modules/core/src/main/java/org/locationtech/jts/io/WKBReader.java index ff8f6a09c0..c4bb54647d 100644 --- a/modules/core/src/main/java/org/locationtech/jts/io/WKBReader.java +++ b/modules/core/src/main/java/org/locationtech/jts/io/WKBReader.java @@ -12,6 +12,7 @@ package org.locationtech.jts.io; import java.io.IOException; +import java.util.EnumSet; import org.locationtech.jts.geom.CoordinateSequence; import org.locationtech.jts.geom.CoordinateSequenceFactory; @@ -244,6 +245,14 @@ else if(isStrict) boolean hasM = ((typeInt & 0x40000000) != 0 || (typeInt & 0xffff)/1000 == 2 || (typeInt & 0xffff)/1000 == 3); //System.out.println(typeInt + " - " + geometryType + " - hasZ:" + hasZ); inputDimension = 2 + (hasZ ? 1 : 0) + (hasM ? 1 : 0); + + EnumSet ordinateFlags = EnumSet.of(Ordinate.X, Ordinate.Y); + if (hasZ) { + ordinateFlags.add(Ordinate.Z); + } + if (hasM) { + ordinateFlags.add(Ordinate.M); + } // determine if SRIDs are present (EWKB only) boolean hasSRID = (typeInt & 0x20000000) != 0; @@ -258,13 +267,13 @@ else if(isStrict) Geometry geom = null; switch (geometryType) { case WKBConstants.wkbPoint : - geom = readPoint(); + geom = readPoint(ordinateFlags); break; case WKBConstants.wkbLineString : - geom = readLineString(); + geom = readLineString(ordinateFlags); break; case WKBConstants.wkbPolygon : - geom = readPolygon(); + geom = readPolygon(ordinateFlags); break; case WKBConstants.wkbMultiPoint : geom = readMultiPoint(SRID); @@ -298,9 +307,9 @@ private Geometry setSRID(Geometry g, int SRID) return g; } - private Point readPoint() throws IOException, ParseException + private Point readPoint(EnumSet ordinateFlags) throws IOException, ParseException { - CoordinateSequence pts = readCoordinateSequence(1); + CoordinateSequence pts = readCoordinateSequence(1, ordinateFlags); // If X and Y are NaN create a empty point if (Double.isNaN(pts.getX(0)) || Double.isNaN(pts.getY(0))) { return factory.createPoint(); @@ -308,21 +317,21 @@ private Point readPoint() throws IOException, ParseException return factory.createPoint(pts); } - private LineString readLineString() throws IOException, ParseException + private LineString readLineString(EnumSet ordinateFlags) throws IOException, ParseException { int size = readNumField(FIELD_NUMCOORDS); - CoordinateSequence pts = readCoordinateSequenceLineString(size); + CoordinateSequence pts = readCoordinateSequenceLineString(size, ordinateFlags); return factory.createLineString(pts); } - private LinearRing readLinearRing() throws IOException, ParseException + private LinearRing readLinearRing(EnumSet ordinateFlags) throws IOException, ParseException { int size = readNumField(FIELD_NUMCOORDS); - CoordinateSequence pts = readCoordinateSequenceRing(size); + CoordinateSequence pts = readCoordinateSequenceRing(size, ordinateFlags); return factory.createLinearRing(pts); } - private Polygon readPolygon() throws IOException, ParseException + private Polygon readPolygon(EnumSet ordinateFlags) throws IOException, ParseException { int numRings = readNumField(FIELD_NUMRINGS); LinearRing[] holes = null; @@ -333,9 +342,9 @@ private Polygon readPolygon() throws IOException, ParseException if (numRings <= 0) return factory.createPolygon(); - LinearRing shell = readLinearRing(); + LinearRing shell = readLinearRing(ordinateFlags); for (int i = 0; i < numRings - 1; i++) { - holes[i] = readLinearRing(); + holes[i] = readLinearRing(ordinateFlags); } return factory.createPolygon(shell, holes); } @@ -390,9 +399,9 @@ private GeometryCollection readGeometryCollection(int SRID) throws IOException, return factory.createGeometryCollection(geoms); } - private CoordinateSequence readCoordinateSequence(int size) throws IOException, ParseException + private CoordinateSequence readCoordinateSequence(int size, EnumSet ordinateFlags) throws IOException, ParseException { - CoordinateSequence seq = csFactory.create(size, inputDimension); + CoordinateSequence seq = csFactory.create(size, inputDimension, ordinateFlags.contains(Ordinate.M) ? 1 : 0); int targetDim = seq.getDimension(); if (targetDim > inputDimension) targetDim = inputDimension; @@ -405,17 +414,17 @@ private CoordinateSequence readCoordinateSequence(int size) throws IOException, return seq; } - private CoordinateSequence readCoordinateSequenceLineString(int size) throws IOException, ParseException + private CoordinateSequence readCoordinateSequenceLineString(int size, EnumSet ordinateFlags) throws IOException, ParseException { - CoordinateSequence seq = readCoordinateSequence(size); + CoordinateSequence seq = readCoordinateSequence(size, ordinateFlags); if (isStrict) return seq; if (seq.size() == 0 || seq.size() >= 2) return seq; return CoordinateSequences.extend(csFactory, seq, 2); } - private CoordinateSequence readCoordinateSequenceRing(int size) throws IOException, ParseException + private CoordinateSequence readCoordinateSequenceRing(int size, EnumSet ordinateFlags) throws IOException, ParseException { - CoordinateSequence seq = readCoordinateSequence(size); + CoordinateSequence seq = readCoordinateSequence(size, ordinateFlags); if (isStrict) return seq; if (CoordinateSequences.isRing(seq)) return seq; return CoordinateSequences.ensureValidRing(csFactory, seq); diff --git a/modules/core/src/main/java/org/locationtech/jts/io/WKBWriter.java b/modules/core/src/main/java/org/locationtech/jts/io/WKBWriter.java index 5fe9e840f6..38fd9b1c5e 100644 --- a/modules/core/src/main/java/org/locationtech/jts/io/WKBWriter.java +++ b/modules/core/src/main/java/org/locationtech/jts/io/WKBWriter.java @@ -13,6 +13,7 @@ import java.io.ByteArrayOutputStream; import java.io.IOException; +import java.util.EnumSet; import org.locationtech.jts.geom.Coordinate; import org.locationtech.jts.geom.CoordinateSequence; @@ -211,6 +212,7 @@ private static char toHexDigit(int n) return (char) ('A' + (n - 10)); } + private EnumSet outputOrdinates; private int outputDimension = 2; private int byteOrder; private boolean includeSRID = false; @@ -272,13 +274,27 @@ public WKBWriter(int outputDimension, int byteOrder) { /** * Creates a writer that writes {@link Geometry}s with - * the given dimension (2 or 3) for output coordinates + * the given dimension (2 to 4) for output coordinates * and byte order. This constructor also takes a flag to * control whether srid information will be written. * If the input geometry has a small coordinate dimension, * coordinates will be padded with {@link Coordinate#NULL_ORDINATE}. + * The output follows the following rules: + *
    + *
  • If the specified output dimension is 3 and the z is measure flag + * is set to true, the Z value of coordinates will be written if it is present + * (i.e. if it is not Double.NaN)
  • + *
  • If the specified output dimension is 3 and the z is measure flag + * is set to false, the Measure value of coordinates will be written if it is present + * (i.e. if it is not Double.NaN)
  • + *
  • If the specified output dimension is 4, the Z value of coordinates will + * be written even if it is not present when the Measure value is present. The Measure + * value of coordinates will be written if it is present + * (i.e. if it is not Double.NaN)
  • + *
+ * See also {@link #setOutputOrdinates(EnumSet)} * - * @param outputDimension the coordinate dimension to output (2 or 3) + * @param outputDimension the coordinate dimension to output (2 to 4) * @param byteOrder the byte ordering to use * @param includeSRID indicates whether SRID should be written */ @@ -287,10 +303,57 @@ public WKBWriter(int outputDimension, int byteOrder, boolean includeSRID) { this.byteOrder = byteOrder; this.includeSRID = includeSRID; - if (outputDimension < 2 || outputDimension > 3) - throw new IllegalArgumentException("Output dimension must be 2 or 3"); + if (outputDimension < 2 || outputDimension > 4) + throw new IllegalArgumentException("Output dimension must be 2 to 4"); + + this.outputOrdinates = EnumSet.of(Ordinate.X, Ordinate.Y); + if (outputDimension > 2) + outputOrdinates.add(Ordinate.Z); + if (outputDimension > 3) + outputOrdinates.add(Ordinate.M); } - + + /** + * Sets the {@link Ordinate} that are to be written. Possible members are: + *
    + *
  • {@link Ordinate#X}
  • + *
  • {@link Ordinate#Y}
  • + *
  • {@link Ordinate#Z}
  • + *
  • {@link Ordinate#M}
  • + *
+ * Values of {@link Ordinate#X} and {@link Ordinate#Y} are always assumed and not + * particularly checked for. + * + * @param outputOrdinates A set of {@link Ordinate} values + */ + public void setOutputOrdinates(EnumSet outputOrdinates) { + + this.outputOrdinates.remove(Ordinate.Z); + this.outputOrdinates.remove(Ordinate.M); + + if (this.outputDimension == 3) { + if (outputOrdinates.contains(Ordinate.Z)) + this.outputOrdinates.add(Ordinate.Z); + else if (outputOrdinates.contains(Ordinate.M)) + this.outputOrdinates.add(Ordinate.M); + } + if (this.outputDimension == 4) { + if (outputOrdinates.contains(Ordinate.Z)) + this.outputOrdinates.add(Ordinate.Z); + if (outputOrdinates.contains(Ordinate.M)) + this.outputOrdinates.add(Ordinate.M); + } + } + + /** + * Gets a bit-pattern defining which ordinates should be + * @return an ordinate bit-pattern + * @see #setOutputOrdinates(EnumSet) + */ + public EnumSet getOutputOrdinates() { + return this.outputOrdinates; + } + /** * Writes a {@link Geometry} into a byte array. * @@ -405,7 +468,16 @@ private void writeByteOrder(OutStream os) throws IOException private void writeGeometryType(int geometryType, Geometry g, OutStream os) throws IOException { - int flag3D = (outputDimension == 3) ? 0x80000000 : 0; + int ordinals = 0; + if (outputOrdinates.contains(Ordinate.Z)) { + ordinals = ordinals | 0x80000000; + } + + if (outputOrdinates.contains(Ordinate.M)) { + ordinals = ordinals | 0x40000000; + } + + int flag3D = (outputDimension > 2) ? ordinals : 0; int typeInt = geometryType | flag3D; typeInt |= includeSRID ? 0x20000000 : 0; writeInt(typeInt, os); @@ -442,10 +514,14 @@ private void writeCoordinate(CoordinateSequence seq, int index, OutStream os) // only write 3rd dim if caller has requested it for this writer if (outputDimension >= 3) { // if 3rd dim is requested, only write it if the CoordinateSequence provides it - double ordVal = Coordinate.NULL_ORDINATE; - if (seq.getDimension() >= 3) { - ordVal = seq.getOrdinate(index, 2); - } + double ordVal = seq.getOrdinate(index, 2); + ByteOrderValues.putDouble(ordVal, buf, byteOrder); + os.write(buf, 8); + } + // only write 4th dim if caller has requested it for this writer + if (outputDimension == 4) { + // if 4th dim is requested, only write it if the CoordinateSequence provides it + double ordVal = seq.getOrdinate(index, 3); ByteOrderValues.putDouble(ordVal, buf, byteOrder); os.write(buf, 8); } diff --git a/modules/core/src/main/java/org/locationtech/jts/io/WKTWriter.java b/modules/core/src/main/java/org/locationtech/jts/io/WKTWriter.java index 530a5686ed..4cb2b28790 100644 --- a/modules/core/src/main/java/org/locationtech/jts/io/WKTWriter.java +++ b/modules/core/src/main/java/org/locationtech/jts/io/WKTWriter.java @@ -257,10 +257,11 @@ public WKTWriter() * is set to false, the Measure value of coordinates will be written if it is present * (i.e. if it is not Double.NaN) *
  • If the specified output dimension is 4, the Z value of coordinates will - * be written even if it is not present when the Measure value is present.The Measrue + * be written even if it is not present when the Measure value is present. The Measure * value of coordinates will be written if it is present * (i.e. if it is not Double.NaN)
  • * + * See also {@link #setOutputOrdinates(EnumSet)} * * @param outputDimension the coordinate dimension to output (2 to 4) */ diff --git a/modules/core/src/test/java/org/locationtech/jts/io/WKBTest.java b/modules/core/src/test/java/org/locationtech/jts/io/WKBTest.java index 6928f4184a..95025daca7 100644 --- a/modules/core/src/test/java/org/locationtech/jts/io/WKBTest.java +++ b/modules/core/src/test/java/org/locationtech/jts/io/WKBTest.java @@ -12,6 +12,7 @@ package org.locationtech.jts.io; import java.io.IOException; +import java.util.EnumSet; import org.locationtech.jts.geom.Coordinate; import org.locationtech.jts.geom.CoordinateFilter; @@ -152,6 +153,49 @@ public void testGeometryCollectionEmpty() runWKBTest("GEOMETRYCOLLECTION EMPTY"); } + /** + * Tests if a previously written WKB with M-coordinates can be read as expected. + */ + public void testWriteAndReadM() throws ParseException + { + String wkt = "MULTILINESTRING M((1 1 1, 2 2 2))"; + WKTReader wktReader = new WKTReader(); + Geometry geometryBefore = wktReader.read(wkt); + + WKBWriter wkbWriter = new WKBWriter(3); + wkbWriter.setOutputOrdinates(EnumSet.of(Ordinate.X, Ordinate.Y, Ordinate.M)); + byte[] write = wkbWriter.write(geometryBefore); + + WKBReader wkbReader = new WKBReader(); + Geometry geometryAfter = wkbReader.read(write); + + assertEquals(1.0, geometryAfter.getCoordinates()[0].getX()); + assertEquals(1.0, geometryAfter.getCoordinates()[0].getY()); + assertEquals(Double.NaN, geometryAfter.getCoordinates()[0].getZ()); + assertEquals(1.0, geometryAfter.getCoordinates()[0].getM()); + } + + /** + * Tests if a previously written WKB with Z-coordinates can be read as expected. + */ + public void testWriteAndReadZ() throws ParseException + { + String wkt = "MULTILINESTRING ((1 1 1, 2 2 2))"; + WKTReader wktReader = new WKTReader(); + Geometry geometryBefore = wktReader.read(wkt); + + WKBWriter wkbWriter = new WKBWriter(3); + byte[] write = wkbWriter.write(geometryBefore); + + WKBReader wkbReader = new WKBReader(); + Geometry geometryAfter = wkbReader.read(write); + + assertEquals(1.0, geometryAfter.getCoordinates()[0].getX()); + assertEquals(1.0, geometryAfter.getCoordinates()[0].getY()); + assertEquals(1.0, geometryAfter.getCoordinates()[0].getZ()); + assertEquals(Double.NaN, geometryAfter.getCoordinates()[0].getM()); + } + private void runWKBTest(String wkt) throws IOException, ParseException { runWKBTestCoordinateArray(wkt); diff --git a/modules/core/src/test/java/org/locationtech/jts/io/WKBWriterTest.java b/modules/core/src/test/java/org/locationtech/jts/io/WKBWriterTest.java index 683cb7dace..d70276c5b5 100644 --- a/modules/core/src/test/java/org/locationtech/jts/io/WKBWriterTest.java +++ b/modules/core/src/test/java/org/locationtech/jts/io/WKBWriterTest.java @@ -12,8 +12,10 @@ package org.locationtech.jts.io; import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.CoordinateXYZM; import org.locationtech.jts.geom.Geometry; import org.locationtech.jts.geom.GeometryFactory; +import org.locationtech.jts.geom.LineString; import org.locationtech.jts.geom.Point; import junit.textui.TestRunner; @@ -125,6 +127,25 @@ public void testGeometryCollection() { 4326, "0107000020E61000000900000001010000000000000000000000000000000000F03F01010000000000000000000000000000000000F03F01010000000000000000000040000000000000084001020000000200000000000000000000400000000000000840000000000000104000000000000014400102000000020000000000000000000000000000000000F03F000000000000004000000000000008400102000000020000000000000000001040000000000000144000000000000018400000000000001C4001030000000200000005000000000000000000000000000000000000000000000000000000000000000000244000000000000024400000000000002440000000000000244000000000000000000000000000000000000000000000000005000000000000000000F03F000000000000F03F000000000000F03F0000000000002240000000000000224000000000000022400000000000002240000000000000F03F000000000000F03F000000000000F03F01030000000200000005000000000000000000000000000000000000000000000000000000000000000000244000000000000024400000000000002440000000000000244000000000000000000000000000000000000000000000000005000000000000000000F03F000000000000F03F000000000000F03F0000000000002240000000000000224000000000000022400000000000002240000000000000F03F000000000000F03F000000000000F03F0103000000010000000500000000000000000022C0000000000000000000000000000022C00000000000002440000000000000F0BF0000000000002440000000000000F0BF000000000000000000000000000022C00000000000000000"); } + + public void testWkbLineStringZM() throws ParseException { + LineString lineZM = new GeometryFactory().createLineString(new Coordinate[]{new CoordinateXYZM(1,2,3,4), new CoordinateXYZM(5,6,7,8)}); + byte[] write = new WKBWriter(4).write(lineZM); + + LineString deserialisiert = (LineString) new WKBReader().read(write); + + assertEquals(lineZM, deserialisiert); + + assertEquals(1.0, lineZM.getPointN(0).getCoordinate().getX()); + assertEquals(2.0, lineZM.getPointN(0).getCoordinate().getY()); + assertEquals(3.0, lineZM.getPointN(0).getCoordinate().getZ()); + assertEquals(4.0, lineZM.getPointN(0).getCoordinate().getM()); + + assertEquals(5.0, lineZM.getPointN(1).getCoordinate().getX()); + assertEquals(6.0, lineZM.getPointN(1).getCoordinate().getY()); + assertEquals(7.0, lineZM.getPointN(1).getCoordinate().getZ()); + assertEquals(8.0, lineZM.getPointN(1).getCoordinate().getM()); + } void checkWKB(String wkt, int dimension, String expectedWKBHex) { checkWKB(wkt, dimension, ByteOrderValues.LITTLE_ENDIAN, -1, expectedWKBHex); diff --git a/modules/core/src/test/java/org/locationtech/jts/io/WKTWriterTest.java b/modules/core/src/test/java/org/locationtech/jts/io/WKTWriterTest.java index 66e573e390..f2d4eb4577 100644 --- a/modules/core/src/test/java/org/locationtech/jts/io/WKTWriterTest.java +++ b/modules/core/src/test/java/org/locationtech/jts/io/WKTWriterTest.java @@ -181,4 +181,22 @@ public void testWrite3D_withNaN() { assertEquals("LINESTRING (1 1, 2 2)", wkt); } + public void testWktLineStringZM() throws ParseException { + LineString lineZM = new GeometryFactory().createLineString(new Coordinate[]{new CoordinateXYZM(1,2,3,4), new CoordinateXYZM(5,6,7,8)}); + String write = new WKTWriter(4).write(lineZM); + + LineString deserialisiert = (LineString) new WKTReader().read(write); + + assertEquals(lineZM, deserialisiert); + + assertEquals(1.0, lineZM.getPointN(0).getCoordinate().getX()); + assertEquals(2.0, lineZM.getPointN(0).getCoordinate().getY()); + assertEquals(3.0, lineZM.getPointN(0).getCoordinate().getZ()); + assertEquals(4.0, lineZM.getPointN(0).getCoordinate().getM()); + + assertEquals(5.0, lineZM.getPointN(1).getCoordinate().getX()); + assertEquals(6.0, lineZM.getPointN(1).getCoordinate().getY()); + assertEquals(7.0, lineZM.getPointN(1).getCoordinate().getZ()); + assertEquals(8.0, lineZM.getPointN(1).getCoordinate().getM()); + } }