WKBWriter.java
/*
* Copyright (c) 2016 Vivid Solutions.
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License 2.0
* and Eclipse Distribution License v. 1.0 which accompanies this distribution.
* The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v20.html
* and the Eclipse Distribution License is available at
*
* http://www.eclipse.org/org/documents/edl-v10.php.
*/
package org.locationtech.jts.io;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.EnumSet;
import org.locationtech.jts.geom.Coordinate;
import org.locationtech.jts.geom.CoordinateSequence;
import org.locationtech.jts.geom.Geometry;
import org.locationtech.jts.geom.GeometryCollection;
import org.locationtech.jts.geom.LineString;
import org.locationtech.jts.geom.LinearRing;
import org.locationtech.jts.geom.MultiLineString;
import org.locationtech.jts.geom.MultiPoint;
import org.locationtech.jts.geom.MultiPolygon;
import org.locationtech.jts.geom.Point;
import org.locationtech.jts.geom.Polygon;
import org.locationtech.jts.util.Assert;
/**
* Writes a {@link Geometry} into Well-Known Binary format.
* Supports use of an {@link OutStream}, which allows easy use
* with arbitrary byte stream sinks.
* <p>
* The WKB format is specified in the
* OGC <A HREF="http://portal.opengeospatial.org/files/?artifact_id=829"><i>Simple Features for SQL
* specification</i></a> (section 3.3.2.6).
* <p>
* There are a few cases which are not specified in the standard.
* The implementation uses a representation which is compatible with
* other common spatial systems (notably, PostGIS).
* <ul>
* <li>{@link LinearRing}s are written as {@link LineString}s</li>
* <li>Empty geometries are output as follows:
* <ul>
* <li><b>Point</b>: a <code>WKBPoint</code> with <code>NaN</code> ordinate values</li>
* <li><b>LineString</b>: a <code>WKBLineString</code> with zero points</li>
* <li><b>Polygon</b>: a <code>WKBPolygon</code> with zero rings</li>
* <li><b>Multigeometries</b>: a <code>WKBMulti</code> geometry of appropriate type with zero elements</li>
* <li><b>GeometryCollections</b>: a <code>WKBGeometryCollection</code> with zero elements</li>
* </ul></li>
* </ul>
* <p>
* This implementation supports the <b>Extended WKB</b> standard.
* Extended WKB allows writing 3-dimensional coordinates
* and the geometry SRID value.
* The presence of 3D coordinates is indicated
* by setting the high bit of the <tt>wkbType</tt> word.
* The presence of a SRID is indicated
* by setting the third bit of the <tt>wkbType</tt> word.
* EWKB format is upward-compatible with the original SFS WKB format.
* <p>
* SRID output is optimized, if specified.
* Only the top-level geometry has the SRID included.
* This assumes that all geometries in a collection have the same SRID as
* the collection (which is the JTS convention).
* <p>
* This class supports reuse of a single instance to read multiple
* geometries. This class is not thread-safe; each thread should create its own
* instance.
*
* <h3>Syntax</h3>
* The following syntax specification describes the version of Well-Known Binary
* supported by JTS.
* <p>
* <i>The specification uses a syntax language similar to that used in
* the C language. Bitfields are specified from high-order to low-order bits.</i>
* <p>
* <blockquote><pre>
*
* <b>byte</b> = 1 byte
* <b>uint32</b> = 32 bit unsigned integer (4 bytes)
* <b>double</b> = double precision number (8 bytes)
*
* abstract Point { }
*
* Point2D extends Point {
* <b>double</b> x;
* <b>double</b> y;
* }
*
* Point3D extends Point {
* <b>double</b> x;
* <b>double</b> y;
* <b>double</b> z;
* }
*
* LinearRing {
* <b>uint32</b> numPoints;
* Point points[numPoints];
* }
*
* enum wkbGeometryType {
* wkbPoint = 1,
* wkbLineString = 2,
* wkbPolygon = 3,
* wkbMultiPoint = 4,
* wkbMultiLineString = 5,
* wkbMultiPolygon = 6,
* wkbGeometryCollection = 7
* }
*
* enum byteOrder {
* wkbXDR = 0, // Big Endian
* wkbNDR = 1 // Little Endian
* }
*
* WKBType {
* <b>uint32</b> wkbGeometryType : 8; // values from enum wkbGeometryType
* }
*
* EWKBType {
* <b>uint32</b> is3D : 1; // 0 = 2D, 1 = 3D
* <b>uint32</b> noData1 : 1;
* <b>uint32</b> hasSRID : 1; // 0, no, 1 = yes
* <b>uint32</b> noData2 : 21;
* <b>uint32</b> wkbGeometryType : 8; // values from enum wkbGeometryType
* }
*
* abstract WKBGeometry {
* <b>byte</b> byteOrder; // values from enum byteOrder
* EWKBType wkbType
* [ <b>uint32</b> srid; ] // only if hasSRID = yes
* }
*
* WKBPoint extends WKBGeometry {
* Point point;
* }
*
* WKBLineString extends WKBGeometry {
* <b>uint32</b> numCoords;
* Point points[numCoords];
* }
*
* WKBPolygon extends WKBGeometry {
* <b>uint32</b> numRings;
* LinearRing rings[numRings];
* }
*
* WKBMultiPoint extends WKBGeometry {
* <b>uint32</b> numElems;
* WKBPoint elems[numElems];
* }
*
* WKBMultiLineString extends WKBGeometry {
* <b>uint32</b> numElems;
* WKBLineString elems[numElems];
* }
*
* wkbMultiPolygon extends WKBGeometry {
* <b>uint32</b> numElems;
* WKBPolygon elems[numElems];
* }
*
* WKBGeometryCollection extends WKBGeometry {
* <b>uint32</b> numElems;
* WKBGeometry elems[numElems];
* }
*
* </pre></blockquote>
* @see WKBReader
*/
public class WKBWriter
{
/**
* Converts a byte array to a hexadecimal string.
*
* @param bytes
* @return a string of hexadecimal digits
*
* @deprecated
*/
public static String bytesToHex(byte[] bytes)
{
return toHex(bytes);
}
/**
* Converts a byte array to a hexadecimal string.
*
* @param bytes a byte array
* @return a string of hexadecimal digits
*/
public static String toHex(byte[] bytes)
{
StringBuffer buf = new StringBuffer();
for (int i = 0; i < bytes.length; i++) {
byte b = bytes[i];
buf.append(toHexDigit((b >> 4) & 0x0F));
buf.append(toHexDigit(b & 0x0F));
}
return buf.toString();
}
private static char toHexDigit(int n)
{
if (n < 0 || n > 15)
throw new IllegalArgumentException("Nibble value out of range: " + n);
if (n <= 9)
return (char) ('0' + n);
return (char) ('A' + (n - 10));
}
private EnumSet<Ordinate> outputOrdinates;
private int outputDimension = 2;
private int byteOrder;
private boolean includeSRID = false;
private ByteArrayOutputStream byteArrayOS = new ByteArrayOutputStream();
private OutStream byteArrayOutStream = new OutputStreamOutStream(byteArrayOS);
// holds output data values
private byte[] buf = new byte[8];
/**
* Creates a writer that writes {@link Geometry}s with
* output dimension = 2 and BIG_ENDIAN byte order
*/
public WKBWriter() {
this(2, ByteOrderValues.BIG_ENDIAN);
}
/**
* Creates a writer that writes {@link Geometry}s with
* the given dimension (2 or 3) for output coordinates
* and {@link ByteOrderValues#BIG_ENDIAN} byte order.
* If the input geometry has a small coordinate dimension,
* coordinates will be padded with {@link Coordinate#NULL_ORDINATE}.
*
* @param outputDimension the coordinate dimension to output (2 or 3)
*/
public WKBWriter(int outputDimension) {
this(outputDimension, ByteOrderValues.BIG_ENDIAN);
}
/**
* Creates a writer that writes {@link Geometry}s with
* the given dimension (2 or 3) for output coordinates
* and {@link ByteOrderValues#BIG_ENDIAN} byte order. This constructor also
* takes a flag to control whether srid information will be
* written.
* If the input geometry has a smaller coordinate dimension,
* coordinates will be padded with {@link Coordinate#NULL_ORDINATE}.
*
* @param outputDimension the coordinate dimension to output (2 or 3)
* @param includeSRID indicates whether SRID should be written
*/
public WKBWriter(int outputDimension, boolean includeSRID) {
this(outputDimension, ByteOrderValues.BIG_ENDIAN, includeSRID);
}
/**
* Creates a writer that writes {@link Geometry}s with
* the given dimension (2 or 3) for output coordinates
* and byte order
* If the input geometry has a small coordinate dimension,
* coordinates will be padded with {@link Coordinate#NULL_ORDINATE}.
*
* @param outputDimension the coordinate dimension to output (2 or 3)
* @param byteOrder the byte ordering to use
*/
public WKBWriter(int outputDimension, int byteOrder) {
this(outputDimension, byteOrder, false);
}
/**
* Creates a writer that writes {@link Geometry}s with
* 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 to 4)
* @param byteOrder the byte ordering to use
* @param includeSRID indicates whether SRID should be written
*/
public WKBWriter(int outputDimension, int byteOrder, boolean includeSRID) {
this.outputDimension = outputDimension;
this.byteOrder = byteOrder;
this.includeSRID = includeSRID;
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.
*
* @param geom the geometry to write
* @return the byte array containing the WKB
*/
public byte[] write(Geometry geom)
{
try {
byteArrayOS.reset();
write(geom, byteArrayOutStream);
}
catch (IOException ex) {
throw new RuntimeException("Unexpected IO exception: " + ex.getMessage());
}
return byteArrayOS.toByteArray();
}
/**
* Writes a {@link Geometry} to an {@link OutStream}.
*
* @param geom the geometry to write
* @param os the out stream to write to
* @throws IOException if an I/O error occurs
*/
public void write(Geometry geom, OutStream os) throws IOException
{
if (geom instanceof Point)
writePoint((Point) geom, os);
// LinearRings will be written as LineStrings
else if (geom instanceof LineString)
writeLineString((LineString) geom, os);
else if (geom instanceof Polygon)
writePolygon((Polygon) geom, os);
else if (geom instanceof MultiPoint)
writeGeometryCollection(WKBConstants.wkbMultiPoint,
(MultiPoint) geom, os);
else if (geom instanceof MultiLineString)
writeGeometryCollection(WKBConstants.wkbMultiLineString,
(MultiLineString) geom, os);
else if (geom instanceof MultiPolygon)
writeGeometryCollection(WKBConstants.wkbMultiPolygon,
(MultiPolygon) geom, os);
else if (geom instanceof GeometryCollection)
writeGeometryCollection(WKBConstants.wkbGeometryCollection,
(GeometryCollection) geom, os);
else {
Assert.shouldNeverReachHere("Unknown Geometry type");
}
}
private void writePoint(Point pt, OutStream os) throws IOException
{
writeByteOrder(os);
writeGeometryType(WKBConstants.wkbPoint, pt, os);
if (pt.getCoordinateSequence().size() == 0) {
// write empty point as NaNs (extension to OGC standard)
writeNaNs(outputDimension, os);
} else {
writeCoordinateSequence(pt.getCoordinateSequence(), false, os);
}
}
private void writeLineString(LineString line, OutStream os)
throws IOException
{
writeByteOrder(os);
writeGeometryType(WKBConstants.wkbLineString, line, os);
writeCoordinateSequence(line.getCoordinateSequence(), true, os);
}
private void writePolygon(Polygon poly, OutStream os) throws IOException
{
writeByteOrder(os);
writeGeometryType(WKBConstants.wkbPolygon, poly, os);
//--- write empty polygons with no rings (OCG extension)
if (poly.isEmpty()) {
writeInt(0, os);
return;
}
writeInt(poly.getNumInteriorRing() + 1, os);
writeCoordinateSequence(poly.getExteriorRing().getCoordinateSequence(), true, os);
for (int i = 0; i < poly.getNumInteriorRing(); i++) {
writeCoordinateSequence(poly.getInteriorRingN(i).getCoordinateSequence(), true,
os);
}
}
private void writeGeometryCollection(int geometryType, GeometryCollection gc,
OutStream os) throws IOException
{
writeByteOrder(os);
writeGeometryType(geometryType, gc, os);
writeInt(gc.getNumGeometries(), os);
boolean originalIncludeSRID = this.includeSRID;
this.includeSRID = false;
for (int i = 0; i < gc.getNumGeometries(); i++) {
write(gc.getGeometryN(i), os);
}
this.includeSRID = originalIncludeSRID;
}
private void writeByteOrder(OutStream os) throws IOException
{
if (byteOrder == ByteOrderValues.LITTLE_ENDIAN)
buf[0] = WKBConstants.wkbNDR;
else
buf[0] = WKBConstants.wkbXDR;
os.write(buf, 1);
}
private void writeGeometryType(int geometryType, Geometry g, OutStream os)
throws IOException
{
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);
if (includeSRID) {
writeInt(g.getSRID(), os);
}
}
private void writeInt(int intValue, OutStream os) throws IOException
{
ByteOrderValues.putInt(intValue, buf, byteOrder);
os.write(buf, 4);
}
private void writeCoordinateSequence(CoordinateSequence seq, boolean writeSize, OutStream os)
throws IOException
{
if (writeSize)
writeInt(seq.size(), os);
for (int i = 0; i < seq.size(); i++) {
writeCoordinate(seq, i, os);
}
}
private void writeCoordinate(CoordinateSequence seq, int index, OutStream os)
throws IOException
{
ByteOrderValues.putDouble(seq.getX(index), buf, byteOrder);
os.write(buf, 8);
ByteOrderValues.putDouble(seq.getY(index), buf, byteOrder);
os.write(buf, 8);
// 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 = 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);
}
}
private void writeNaNs(int numNaNs, OutStream os)
throws IOException
{
for (int i = 0; i < numNaNs; i++) {
ByteOrderValues.putDouble(Double.NaN, buf, byteOrder);
os.write(buf, 8);
}
}
}