Class WKBWriter

  1 /*
  2  * Copyright (c) 2016 Vivid Solutions.
  3  *
  4  * All rights reserved. This program and the accompanying materials
  5  * are made available under the terms of the Eclipse Public License 2.0
  6  * and Eclipse Distribution License v. 1.0 which accompanies this distribution.
  7  * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v20.html
  8  * and the Eclipse Distribution License is available at
  9  *
 10  * http://www.eclipse.org/org/documents/edl-v10.php.
 11  */
 12 package org.locationtech.jts.io;
 13  
 14 import java.io.ByteArrayOutputStream;
 15 import java.io.IOException;
 16  
 17 import org.locationtech.jts.geom.Coordinate;
 18 import org.locationtech.jts.geom.CoordinateSequence;
 19 import org.locationtech.jts.geom.Geometry;
 20 import org.locationtech.jts.geom.GeometryCollection;
 21 import org.locationtech.jts.geom.LineString;
 22 import org.locationtech.jts.geom.LinearRing;
 23 import org.locationtech.jts.geom.MultiLineString;
 24 import org.locationtech.jts.geom.MultiPoint;
 25 import org.locationtech.jts.geom.MultiPolygon;
 26 import org.locationtech.jts.geom.Point;
 27 import org.locationtech.jts.geom.Polygon;
 28 import org.locationtech.jts.util.Assert;
 29  
 30 /**
 31  * Writes a {@link Geometry} into Well-Known Binary format.
 32  * Supports use of an {@link OutStream}, which allows easy use
 33  * with arbitrary byte stream sinks.
 34  * <p>
 35  * The WKB format is specified in the 
 36  * OGC <A HREF="http://www.opengis.org/techno/specs.htm"><i>Simple Features for SQL</i></a>
 37  * specification.
 38  * This implementation also supports the <b>Extended WKB</b> 
 39  * standard. Extended WKB allows writing 3-dimensional coordinates
 40  * and including the geometry SRID value.  
 41  * The presence of 3D coordinates is signified
 42  * by setting the high bit of the <tt>wkbType</tt> word.
 43  * The presence of an SRID is signified 
 44  * by setting the third bit of the <tt>wkbType</tt> word.
 45  * EWKB format is upward compatible with the original SFS WKB format.
 46  * <p>
 47  * Empty Points are output as a Point with <code>NaN</code> X and Y ordinate values. 
 48  * <p>
 49  * The WKB specification does not support representing {@link LinearRing}s;
 50  * they will be written as {@link LineString}s.
 51  * <p>
 52  * This class is designed to support reuse of a single instance to read multiple
 53  * geometries. This class is not thread-safe; each thread should create its own
 54  * instance.
 55  * 
 56  * <h3>Syntax</h3>
 57  * The following syntax specification describes the version of Well-Known Binary
 58  * supported by JTS.
 59  * <p>
 60  * <i>The specification uses a syntax language similar to that used in
 61  * the C language.  Bitfields are specified from hi-order to lo-order bits.</i>
 62  * <p>
 63  * <blockquote><pre>
 64  * 
 65  * <b>byte</b> = 1 byte
 66  * <b>uint32</b> = 32 bit unsigned integer (4 bytes)
 67  * <b>double</b> = double precision number (8 bytes)
 68  * 
 69  * abstract Point { }
 70  * 
 71  * Point2D extends Point {
 72  *     <b>double</b> x;
 73  *     <b>double</b> y;
 74  * }
 75  * 
 76  * Point3D extends Point {
 77  *     <b>double</b> x;
 78  *     <b>double</b> y;
 79  *     <b>double</b> z;
 80  * }
 81  * 
 82  * LinearRing {
 83  *     <b>uint32</b> numPoints;
 84  *     Point points[numPoints];
 85  * }
 86  * 
 87  * enum wkbGeometryType {
 88  *     wkbPoint = 1,
 89  *     wkbLineString = 2,
 90  *     wkbPolygon = 3,
 91  *     wkbMultiPoint = 4,
 92  *     wkbMultiLineString = 5,
 93  *     wkbMultiPolygon = 6,
 94  *     wkbGeometryCollection = 7
 95  * }
 96  * 
 97  * enum byteOrder {
 98  *     wkbXDR = 0,    // Big Endian
 99  *     wkbNDR = 1     // Little Endian
100  * }
101  * 
102  * WKBType {
103  *     <b>uint32</b> wkbGeometryType : 8; // values from enum wkbGeometryType
104  * }
105  * 
106  * EWKBType {
107  *     <b>uint32</b> is3D : 1;     // 0 = 2D, 1 = 3D
108  *     <b>uint32</b> noData1 : 1; 
109  *     <b>uint32</b> hasSRID : 1;      // 0, no, 1 = yes
110  *     <b>uint32</b> noData2 : 21; 
111  *     <b>uint32</b> wkbGeometryType : 8; // values from enum wkbGeometryType
112  * }
113  * 
114  * abstract WKBGeometry {
115  *     <b>byte</b> byteOrder;        // values from enum byteOrder
116  *     EWKBType wkbType
117  *     [ <b>uint32</b> srid; ]     // only if hasSRID = yes
118  * }
119  * 
120  * WKBPoint extends WKBGeometry {
121  *     Point point;
122  * }
123  * 
124  * WKBLineString extends WKBGeometry {
125  *     <b>uint32</b> numCoords;
126  *     Point points[numCoords];
127  * }
128  * 
129  * WKBPolygon extends WKBGeometry {
130  *     <b>uint32</b> numRings;
131  *     LinearRing rings[numRings];
132  * }
133  * 
134  * WKBMultiPoint extends WKBGeometry {
135  *     <b>uint32</b> numElems;
136  *     WKBPoint elems[numElems];
137  * }
138  * 
139  * WKBMultiLineString extends WKBGeometry {
140  *     <b>uint32</b> numElems;
141  *     WKBLineString elems[numElems];
142  * }
143  * 
144  * wkbMultiPolygon extends WKBGeometry {
145  *     <b>uint32</b> numElems;
146  *     WKBPolygon elems[numElems];
147  * }
148  * 
149  * WKBGeometryCollection extends WKBGeometry {
150  *     <b>uint32</b> numElems;
151  *     WKBGeometry elems[numElems];
152  * }
153  * 
154  * </pre></blockquote> 
155  * @see WKBReader
156  */
157 public class WKBWriter
158 {
159   /**
160    * Converts a byte array to a hexadecimal string.
161    * 
162    * @param bytes
163    * @return a string of hexadecimal digits
164    * 
165    * @deprecated
166    */
167   public static String bytesToHex(byte[] bytes)
168   {
169     return toHex(bytes);
170   }
171  
172   /**
173    * Converts a byte array to a hexadecimal string.
174    * 
175    * @param bytes a byte array
176    * @return a string of hexadecimal digits
177    */
178   public static String toHex(byte[] bytes)
179   {
180     StringBuffer buf = new StringBuffer();
181     for (int i = 0; i < bytes.length; i++) {
182       byte b = bytes[i];
183       buf.append(toHexDigit((b >> 4) & 0x0F));
184       buf.append(toHexDigit(b & 0x0F));
185     }
186     return buf.toString();
187   }
188  
189   private static char toHexDigit(int n)
190   {
191     if (n < 0 || n > 15)
192       throw new IllegalArgumentException("Nibble value out of range: " + n);
193     if (n <= 9)
194       return (char) ('0' + n);
195     return (char) ('A' + (n - 10));
196   }
197  
198   private int outputDimension = 2;
199   private int byteOrder;
200   private boolean includeSRID = false;
201   private ByteArrayOutputStream byteArrayOS = new ByteArrayOutputStream();
202   private OutStream byteArrayOutStream = new OutputStreamOutStream(byteArrayOS);
203   // holds output data values
204   private byte[] buf = new byte[8];
205  
206   /**
207    * Creates a writer that writes {@link Geometry}s with
208    * output dimension = 2 and BIG_ENDIAN byte order
209    */
210   public WKBWriter() {
211     this(2, ByteOrderValues.BIG_ENDIAN);
212   }
213  
214   /**
215    * Creates a writer that writes {@link Geometry}s with
216    * the given dimension (2 or 3) for output coordinates
217    * and {@link ByteOrderValues#BIG_ENDIAN} byte order.
218    * If the input geometry has a small coordinate dimension,
219    * coordinates will be padded with {@link Coordinate#NULL_ORDINATE}.
220    *
221    * @param outputDimension the coordinate dimension to output (2 or 3)
222    */
223   public WKBWriter(int outputDimension) {
224     this(outputDimension, ByteOrderValues.BIG_ENDIAN);
225   }
226  
227   /**
228    * Creates a writer that writes {@link Geometry}s with
229    * the given dimension (2 or 3) for output coordinates
230    * and {@link ByteOrderValues#BIG_ENDIAN} byte order. This constructor also
231    * takes a flag to control whether srid information will be
232    * written.
233    * If the input geometry has a smaller coordinate dimension,
234    * coordinates will be padded with {@link Coordinate#NULL_ORDINATE}.
235    *
236    * @param outputDimension the coordinate dimension to output (2 or 3)
237    * @param includeSRID indicates whether SRID should be written
238    */
239   public WKBWriter(int outputDimension, boolean includeSRID) {
240     this(outputDimension, ByteOrderValues.BIG_ENDIAN, includeSRID);
241   }
242   
243   /**
244    * Creates a writer that writes {@link Geometry}s with
245    * the given dimension (2 or 3) for output coordinates
246    * and byte order
247    * If the input geometry has a small coordinate dimension,
248    * coordinates will be padded with {@link Coordinate#NULL_ORDINATE}.
249    *
250    * @param outputDimension the coordinate dimension to output (2 or 3)
251    * @param byteOrder the byte ordering to use
252    */
253   public WKBWriter(int outputDimension, int byteOrder) {
254       this(outputDimension, byteOrder, false);
255   }
256   
257   /**
258    * Creates a writer that writes {@link Geometry}s with
259    * the given dimension (2 or 3) for output coordinates
260    * and byte order. This constructor also takes a flag to 
261    * control whether srid information will be written.
262    * If the input geometry has a small coordinate dimension,
263    * coordinates will be padded with {@link Coordinate#NULL_ORDINATE}.
264    *
265    * @param outputDimension the coordinate dimension to output (2 or 3)
266    * @param byteOrder the byte ordering to use
267    * @param includeSRID indicates whether SRID should be written
268    */
269   public WKBWriter(int outputDimension, int byteOrder, boolean includeSRID) {
270       this.outputDimension = outputDimension;
271       this.byteOrder = byteOrder;
272       this.includeSRID = includeSRID;
273       
274       if (outputDimension < 2 || outputDimension > 3)
275         throw new IllegalArgumentException("Output dimension must be 2 or 3");
276   }
277   
278   /**
279    * Writes a {@link Geometry} into a byte array.
280    *
281    * @param geom the geometry to write
282    * @return the byte array containing the WKB
283    */
284   public byte[] write(Geometry geom)
285   {
286     try {
287       byteArrayOS.reset();
288       write(geom, byteArrayOutStream);
289     }
290     catch (IOException ex) {
291       throw new RuntimeException("Unexpected IO exception: " + ex.getMessage());
292     }
293     return byteArrayOS.toByteArray();
294   }
295  
296   /**
297    * Writes a {@link Geometry} to an {@link OutStream}.
298    *
299    * @param geom the geometry to write
300    * @param os the out stream to write to
301    * @throws IOException if an I/O error occurs
302    */
303   public void write(Geometry geom, OutStream os) throws IOException
304   {
305     if (geom instanceof Point)
306       writePoint((Point) geom, os);
307     // LinearRings will be written as LineStrings
308     else if (geom instanceof LineString)
309       writeLineString((LineString) geom, os);
310     else if (geom instanceof Polygon)
311       writePolygon((Polygon) geom, os);
312     else if (geom instanceof MultiPoint)
313       writeGeometryCollection(WKBConstants.wkbMultiPoint, 
314           (MultiPoint) geom, os);
315     else if (geom instanceof MultiLineString)
316       writeGeometryCollection(WKBConstants.wkbMultiLineString,
317           (MultiLineString) geom, os);
318     else if (geom instanceof MultiPolygon)
319       writeGeometryCollection(WKBConstants.wkbMultiPolygon,
320           (MultiPolygon) geom, os);
321     else if (geom instanceof GeometryCollection)
322       writeGeometryCollection(WKBConstants.wkbGeometryCollection,
323           (GeometryCollection) geom, os);
324     else {
325       Assert.shouldNeverReachHere("Unknown Geometry type");
326     }
327   }
328  
329   private void writePoint(Point pt, OutStream os) throws IOException
330   {
331     writeByteOrder(os);
332     writeGeometryType(WKBConstants.wkbPoint, pt, os);
333     if (pt.getCoordinateSequence().size() == 0) {
334       writeNaNs(2, os);
335     } else {
336       writeCoordinateSequence(pt.getCoordinateSequence(), false, os);
337     }
338   }
339  
340   private void writeLineString(LineString line, OutStream os)
341       throws IOException
342   {
343     writeByteOrder(os);
344     writeGeometryType(WKBConstants.wkbLineString, line, os);
345     writeCoordinateSequence(line.getCoordinateSequence(), true, os);
346   }
347  
348   private void writePolygon(Polygon poly, OutStream os) throws IOException
349   {
350     writeByteOrder(os);
351     writeGeometryType(WKBConstants.wkbPolygon, poly, os);
352     writeInt(poly.getNumInteriorRing() + 1, os);
353     writeCoordinateSequence(poly.getExteriorRing().getCoordinateSequence(), true, os);
354     for (int i = 0; i < poly.getNumInteriorRing(); i++) {
355       writeCoordinateSequence(poly.getInteriorRingN(i).getCoordinateSequence(), true,
356           os);
357     }
358   }
359  
360   private void writeGeometryCollection(int geometryType, GeometryCollection gc,
361       OutStream os) throws IOException
362   {
363     writeByteOrder(os);
364     writeGeometryType(geometryType, gc, os);
365     writeInt(gc.getNumGeometries(), os);
366     for (int i = 0; i < gc.getNumGeometries(); i++) {
367       write(gc.getGeometryN(i), os);
368     }
369   }
370  
371   private void writeByteOrder(OutStream os) throws IOException
372   {
373     if (byteOrder == ByteOrderValues.LITTLE_ENDIAN)
374       buf[0] = WKBConstants.wkbNDR;
375     else
376       buf[0] = WKBConstants.wkbXDR;
377     os.write(buf, 1);
378   }
379  
380   private void writeGeometryType(int geometryType, Geometry g, OutStream os)
381       throws IOException
382   {
383     int flag3D = (outputDimension == 3) ? 0x80000000 : 0;
384     int typeInt = geometryType | flag3D;
385     typeInt |= includeSRID ? 0x20000000 : 0;
386     writeInt(typeInt, os);
387     if (includeSRID) {
388         writeInt(g.getSRID(), os);
389     }
390   }
391  
392   private void writeInt(int intValue, OutStream os) throws IOException
393   {
394     ByteOrderValues.putInt(intValue, buf, byteOrder);
395     os.write(buf, 4);
396   }
397  
398   private void writeCoordinateSequence(CoordinateSequence seq, boolean writeSize, OutStream os)
399       throws IOException
400   {
401     if (writeSize)
402       writeInt(seq.size(), os);
403  
404     for (int i = 0; i < seq.size(); i++) {
405       writeCoordinate(seq, i, os);
406     }
407   }
408  
409   private void writeCoordinate(CoordinateSequence seq, int index, OutStream os)
410   throws IOException
411   {
412     ByteOrderValues.putDouble(seq.getX(index), buf, byteOrder);
413     os.write(buf, 8);
414     ByteOrderValues.putDouble(seq.getY(index), buf, byteOrder);
415     os.write(buf, 8);
416     
417     // only write 3rd dim if caller has requested it for this writer
418     if (outputDimension >= 3) {
419       // if 3rd dim is requested, only write it if the CoordinateSequence provides it
420         double ordVal = Coordinate.NULL_ORDINATE;
421         if (seq.getDimension() >= 3)
422             ordVal = seq.getOrdinate(index, 2);
423       ByteOrderValues.putDouble(ordVal, buf, byteOrder);
424       os.write(buf, 8);
425     }
426   }
427   
428   private void writeNaNs(int numNaNs, OutStream os)
429       throws IOException
430   {
431     for (int i = 0; i < numNaNs; i++) {
432       ByteOrderValues.putDouble(Double.NaN, buf, byteOrder);
433       os.write(buf, 8);
434     }
435   }
436 }
437