Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Keep M measures when writing to WKB and reading from WKB #734

Merged
merged 4 commits into from
Aug 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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);
kaiwinter marked this conversation as resolved.
Show resolved Hide resolved
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;
kaiwinter marked this conversation as resolved.
Show resolved Hide resolved
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) {
jodygarnett marked this conversation as resolved.
Show resolved Hide resolved

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());
}
}
Loading