Skip to content

Commit

Permalink
Keep M coordinates when writing to WKB and reading from WKB (#734)
Browse files Browse the repository at this point in the history
* Unit test for #733

M coordinate is read as Z coordinate after writing/reading.

* Fixed lost M coordinates (#733)

Referring to WKTReader/WKTWriter I added the EnumSet<Ordinate>
ordinateFlags. The WKBWriter uses these ordinateFlags to set the correct
flags on the byte stream.

Signed-off-by: Kai Winter <[email protected]>

* Fixed Javadoc and exception message

* Fixed handling of XYZM coordinates

Signed-off-by: Kai Winter <[email protected]>
# Conflicts:
#	modules/core/src/main/java/org/locationtech/jts/io/WKBWriter.java

---------

Signed-off-by: Kai Winter <[email protected]>
  • Loading branch information
kaiwinter authored Aug 26, 2024
1 parent b99f94e commit 96805bb
Show file tree
Hide file tree
Showing 6 changed files with 198 additions and 29 deletions.
45 changes: 27 additions & 18 deletions modules/core/src/main/java/org/locationtech/jts/io/WKBReader.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<Ordinate> 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;
Expand All @@ -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);
Expand Down Expand Up @@ -298,31 +307,31 @@ private Geometry setSRID(Geometry g, int SRID)
return g;
}

private Point readPoint() throws IOException, ParseException
private Point readPoint(EnumSet<Ordinate> 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();
}
return factory.createPoint(pts);
}

private LineString readLineString() throws IOException, ParseException
private LineString readLineString(EnumSet<Ordinate> 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<Ordinate> 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<Ordinate> ordinateFlags) throws IOException, ParseException
{
int numRings = readNumField(FIELD_NUMRINGS);
LinearRing[] holes = null;
Expand All @@ -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);
}
Expand Down Expand Up @@ -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<Ordinate> 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;
Expand All @@ -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<Ordinate> 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<Ordinate> 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);
Expand Down
96 changes: 86 additions & 10 deletions modules/core/src/main/java/org/locationtech/jts/io/WKBWriter.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -211,6 +212,7 @@ private static char toHexDigit(int n)
return (char) ('A' + (n - 10));
}

private EnumSet<Ordinate> outputOrdinates;
private int outputDimension = 2;
private int byteOrder;
private boolean includeSRID = false;
Expand Down Expand Up @@ -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:
* <ul>
* <li>If the specified <b>output dimension is 3</b> and the <b>z is measure flag
* is set to true</b>, the Z value of coordinates will be written if it is present
* (i.e. if it is not <code>Double.NaN</code>)</li>
* <li>If the specified <b>output dimension is 3</b> and the <b>z is measure flag
* is set to false</b>, the Measure value of coordinates will be written if it is present
* (i.e. if it is not <code>Double.NaN</code>)</li>
* <li>If the specified <b>output dimension is 4</b>, 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 <code>Double.NaN</code>)</li>
* </ul>
* 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
*/
Expand All @@ -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:
* <ul>
* <li>{@link Ordinate#X}</li>
* <li>{@link Ordinate#Y}</li>
* <li>{@link Ordinate#Z}</li>
* <li>{@link Ordinate#M}</li>
* </ul>
* 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<Ordinate> 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<Ordinate> getOutputOrdinates() {
return this.outputOrdinates;
}

/**
* Writes a {@link Geometry} into a byte array.
*
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -257,10 +257,11 @@ public WKTWriter()
* is set to false</b>, the Measure value of coordinates will be written if it is present
* (i.e. if it is not <code>Double.NaN</code>)</li>
* <li>If the specified <b>output dimension is 4</b>, 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 <code>Double.NaN</code>)</li>
* </ul>
* See also {@link #setOutputOrdinates(EnumSet)}
*
* @param outputDimension the coordinate dimension to output (2 to 4)
*/
Expand Down
44 changes: 44 additions & 0 deletions modules/core/src/test/java/org/locationtech/jts/io/WKBTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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());
}
}

0 comments on commit 96805bb

Please sign in to comment.