Class WKBReader

  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.IOException;
 15  
 16 import org.locationtech.jts.geom.CoordinateSequence;
 17 import org.locationtech.jts.geom.CoordinateSequenceFactory;
 18 import org.locationtech.jts.geom.CoordinateSequences;
 19 import org.locationtech.jts.geom.Geometry;
 20 import org.locationtech.jts.geom.GeometryCollection;
 21 import org.locationtech.jts.geom.GeometryFactory;
 22 import org.locationtech.jts.geom.LineString;
 23 import org.locationtech.jts.geom.LinearRing;
 24 import org.locationtech.jts.geom.MultiLineString;
 25 import org.locationtech.jts.geom.MultiPoint;
 26 import org.locationtech.jts.geom.MultiPolygon;
 27 import org.locationtech.jts.geom.Point;
 28 import org.locationtech.jts.geom.Polygon;
 29 import org.locationtech.jts.geom.PrecisionModel;
 30  
 31 /**
 32  * Reads a {@link Geometry}from a byte stream in Well-Known Binary format.
 33  * Supports use of an {@link InStream}, which allows easy use
 34  * with arbitrary byte stream sources.
 35  * <p>
 36  * This class reads the format describe in {@link WKBWriter}.  
 37  * It partially handles
 38  * the <b>Extended WKB</b> format used by PostGIS, 
 39  * by parsing and storing SRID values.
 40  * Although not defined in the WKB spec, empty points
 41  * are handled if they are represented as a Point with <code>NaN</code> X and Y ordinates.
 42  * <p>
 43  * The reader repairs structurally-invalid input
 44  * (specifically, LineStrings and LinearRings which contain
 45  * too few points have vertices added,
 46  * and non-closed rings are closed).
 47  * <p>
 48  * This class is designed to support reuse of a single instance to read multiple
 49  * geometries. This class is not thread-safe; each thread should create its own
 50  * instance.
 51  * <p>
 52  * As of version 1.15, the reader can read geometries following OGC 06-103r4
 53  * speification used by Spatialite/Geopackage.
 54  * <p>
 55  * The difference between PostGIS EWKB format and the new OGC specification is
 56  * that Z and M coordinates are detected with a bit mask on the higher byte in
 57  * the former case (0x80 for Z and 0x40 for M) while new OGC specification use
 58  * specif int ranges for 2D gemetries, Z geometries (2D code+1000), M geometries
 59  * (2D code+2000) and ZM geometries (2D code+3000).
 60  * <p>
 61  * Note that the {@link WKBWriter} is not changed and still write PostGIS WKB
 62  * geometries
 63  * @see WKBWriter for a formal format specification
 64  */
 65 public class WKBReader
 66 {
 67   /**
 68    * Converts a hexadecimal string to a byte array.
 69    * The hexadecimal digit symbols are case-insensitive.
 70    *
 71    * @param hex a string containing hex digits
 72    * @return an array of bytes with the value of the hex string
 73    */
 74   public static byte[] hexToBytes(String hex)
 75   {
 76     int byteLen = hex.length() / 2;
 77     byte[] bytes = new byte[byteLen];
 78  
 79     for (int i = 0; i < hex.length() / 2; i++) {
 80       int i2 = 2 * i;
 81       if (i2 + 1 > hex.length())
 82         throw new IllegalArgumentException("Hex string has odd length");
 83  
 84       int nib1 = hexToInt(hex.charAt(i2));
 85       int nib0 = hexToInt(hex.charAt(i2 + 1));
 86       byte b = (byte) ((nib1 << 4) + (byte) nib0);
 87       bytes[i] = b;
 88     }
 89     return bytes;
 90   }
 91  
 92   private static int hexToInt(char hex)
 93   {
 94     int nib = Character.digit(hex, 16);
 95     if (nib < 0)
 96       throw new IllegalArgumentException("Invalid hex digit: '" + hex + "'");
 97     return nib;
 98   }
 99  
100   private static final String INVALID_GEOM_TYPE_MSG
101   = "Invalid geometry type encountered in ";
102  
103   private GeometryFactory factory;
104   private CoordinateSequenceFactory csFactory;
105   private PrecisionModel precisionModel;
106   // default dimension - will be set on read
107   private int inputDimension = 2;
108   private boolean hasSRID = false;
109   private int SRID = 0;
110   /**
111    * true if structurally invalid input should be reported rather than repaired.
112    * At some point this could be made client-controllable.
113    */
114   private boolean isStrict = false;
115   private ByteOrderDataInStream dis = new ByteOrderDataInStream();
116   private double[] ordValues;
117  
118   public WKBReader() {
119     this(new GeometryFactory());
120   }
121  
122   public WKBReader(GeometryFactory geometryFactory) {
123     this.factory = geometryFactory;
124     precisionModel = factory.getPrecisionModel();
125     csFactory = factory.getCoordinateSequenceFactory();
126   }
127  
128   /**
129    * Reads a single {@link Geometry} in WKB format from a byte array.
130    *
131    * @param bytes the byte array to read from
132    * @return the geometry read
133    * @throws ParseException if the WKB is ill-formed
134    */
135   public Geometry read(byte[] bytes) throws ParseException
136   {
137     // possibly reuse the ByteArrayInStream?
138     // don't throw IOExceptions, since we are not doing any I/O
139     try {
140       return read(new ByteArrayInStream(bytes));
141     }
142     catch (IOException ex) {
143       throw new RuntimeException("Unexpected IOException caught: " + ex.getMessage());
144     }
145   }
146  
147   /**
148    * Reads a {@link Geometry} in binary WKB format from an {@link InStream}.
149    *
150    * @param is the stream to read from
151    * @return the Geometry read
152    * @throws IOException if the underlying stream creates an error
153    * @throws ParseException if the WKB is ill-formed
154    */
155   public Geometry read(InStream is)
156   throws IOException, ParseException
157   {
158     dis.setInStream(is);
159     Geometry g = readGeometry();
160     return g;
161   }
162  
163   private Geometry readGeometry()
164   throws IOException, ParseException
165   {
166  
167       // determine byte order
168       byte byteOrderWKB = dis.readByte();
169  
170       // always set byte order, since it may change from geometry to geometry
171      if(byteOrderWKB == WKBConstants.wkbNDR)
172      {
173         dis.setOrder(ByteOrderValues.LITTLE_ENDIAN);
174      }
175      else if(byteOrderWKB == WKBConstants.wkbXDR)
176      {
177         dis.setOrder(ByteOrderValues.BIG_ENDIAN);
178      }
179      else if(isStrict)
180      {
181         throw new ParseException("Unknown geometry byte order (not NDR or XDR): " + byteOrderWKB);
182      }
183      //if not strict and not XDR or NDR, then we just use the dis default set at the
184      //start of the geometry (if a multi-geometry).  This  allows WBKReader to work
185      //with Spatialite native BLOB WKB, as well as other WKB variants that might just
186      //specify endian-ness at the start of the multigeometry.
187  
188  
189     int typeInt = dis.readInt();
190     // Adds %1000 to make it compatible with OGC 06-103r4
191     int geometryType = (typeInt & 0xffff)%1000;
192  
193     // handle 3D and 4D WKB geometries
194     // geometries with Z coordinates have the 0x80 flag (postgis EWKB)
195     // or are in the 1000 range (Z) or in the 3000 range (ZM) of geometry type (OGC 06-103r4)
196     boolean hasZ = ((typeInt & 0x80000000) != 0 || (typeInt & 0xffff)/1000 == 1 || (typeInt & 0xffff)/1000 == 3);
197     // geometries with M coordinates have the 0x40 flag (postgis EWKB)
198     // or are in the 1000 range (M) or in the 3000 range (ZM) of geometry type (OGC 06-103r4)
199     boolean hasM = ((typeInt & 0x40000000) != 0 || (typeInt & 0xffff)/1000 == 2 || (typeInt & 0xffff)/1000 == 3);
200     //System.out.println(typeInt + " - " + geometryType + " - hasZ:" + hasZ);
201     inputDimension = 2 + (hasZ?1:0) + (hasM?1:0);
202  
203     // determine if SRIDs are present
204     hasSRID = (typeInt & 0x20000000) != 0;
205     int SRID = 0;
206     if (hasSRID) {
207       SRID = dis.readInt();
208     }
209  
210     // only allocate ordValues buffer if necessary
211     if (ordValues == null || ordValues.length < inputDimension)
212       ordValues = new double[inputDimension];
213  
214     Geometry geom = null;
215     switch (geometryType) {
216       case WKBConstants.wkbPoint :
217         geom = readPoint();
218         break;
219       case WKBConstants.wkbLineString :
220         geom = readLineString();
221         break;
222      case WKBConstants.wkbPolygon :
223        geom = readPolygon();
224         break;
225       case WKBConstants.wkbMultiPoint :
226         geom = readMultiPoint();
227         break;
228       case WKBConstants.wkbMultiLineString :
229         geom = readMultiLineString();
230         break;
231      case WKBConstants.wkbMultiPolygon :
232         geom = readMultiPolygon();
233         break;
234       case WKBConstants.wkbGeometryCollection :
235         geom = readGeometryCollection();
236         break;
237       default: 
238         throw new ParseException("Unknown WKB type " + geometryType);
239     }
240     setSRID(geom, SRID);
241     return geom;
242   }
243  
244   /**
245    * Sets the SRID, if it was specified in the WKB
246    *
247    * @param g the geometry to update
248    * @return the geometry with an updated SRID value, if required
249    */
250   private Geometry setSRID(Geometry g, int SRID)
251   {
252     if (SRID != 0)
253       g.setSRID(SRID);
254     return g;
255   }
256  
257   private Point readPoint() throws IOException
258   {
259     CoordinateSequence pts = readCoordinateSequence(1);
260     // If X and Y are NaN create a empty point
261     if (Double.isNaN(pts.getX(0)) || Double.isNaN(pts.getY(0))) {
262       return factory.createPoint();
263     }
264     return factory.createPoint(pts);
265   }
266  
267   private LineString readLineString() throws IOException
268   {
269     int size = dis.readInt();
270     CoordinateSequence pts = readCoordinateSequenceLineString(size);
271     return factory.createLineString(pts);
272   }
273  
274   private LinearRing readLinearRing() throws IOException
275   {
276     int size = dis.readInt();
277     CoordinateSequence pts = readCoordinateSequenceRing(size);
278     return factory.createLinearRing(pts);
279   }
280  
281   private Polygon readPolygon() throws IOException
282   {
283     int numRings = dis.readInt();
284     LinearRing[] holes = null;
285     if (numRings > 1)
286       holes = new LinearRing[numRings - 1];
287  
288     LinearRing shell = readLinearRing();
289     for (int i = 0; i < numRings - 1; i++) {
290       holes[i] = readLinearRing();
291     }
292     return factory.createPolygon(shell, holes);
293   }
294  
295   private MultiPoint readMultiPoint() throws IOException, ParseException
296   {
297     int numGeom = dis.readInt();
298     Point[] geoms = new Point[numGeom];
299     for (int i = 0; i < numGeom; i++) {
300       Geometry g = readGeometry();
301       if (! (g instanceof Point))
302         throw new ParseException(INVALID_GEOM_TYPE_MSG + "MultiPoint");
303       geoms[i] = (Point) g;
304     }
305     return factory.createMultiPoint(geoms);
306   }
307  
308   private MultiLineString readMultiLineString() throws IOException, ParseException
309   {
310     int numGeom = dis.readInt();
311     LineString[] geoms = new LineString[numGeom];
312     for (int i = 0; i < numGeom; i++) {
313       Geometry g = readGeometry();
314       if (! (g instanceof LineString))
315         throw new ParseException(INVALID_GEOM_TYPE_MSG + "MultiLineString");
316       geoms[i] = (LineString) g;
317     }
318     return factory.createMultiLineString(geoms);
319   }
320  
321   private MultiPolygon readMultiPolygon() throws IOException, ParseException
322   {
323     int numGeom = dis.readInt();
324     Polygon[] geoms = new Polygon[numGeom];
325  
326     for (int i = 0; i < numGeom; i++) {
327       Geometry g = readGeometry();
328       if (! (g instanceof Polygon))
329         throw new ParseException(INVALID_GEOM_TYPE_MSG + "MultiPolygon");
330       geoms[i] = (Polygon) g;
331     }
332     return factory.createMultiPolygon(geoms);
333   }
334  
335   private GeometryCollection readGeometryCollection() throws IOException, ParseException
336   {
337     int numGeom = dis.readInt();
338     Geometry[] geoms = new Geometry[numGeom];
339     for (int i = 0; i < numGeom; i++) {
340       geoms[i] = readGeometry();
341     }
342     return factory.createGeometryCollection(geoms);
343   }
344  
345   private CoordinateSequence readCoordinateSequence(int size) throws IOException
346   {
347     CoordinateSequence seq = csFactory.create(size, inputDimension);
348     int targetDim = seq.getDimension();
349     if (targetDim > inputDimension)
350       targetDim = inputDimension;
351     for (int i = 0; i < size; i++) {
352       readCoordinate();
353       for (int j = 0; j < targetDim; j++) {
354         seq.setOrdinate(i, j, ordValues[j]);
355       }
356     }
357     return seq;
358   }
359  
360   private CoordinateSequence readCoordinateSequenceLineString(int size) throws IOException
361   {
362     CoordinateSequence seq = readCoordinateSequence(size);
363     if (isStrict) return seq;
364     if (seq.size() == 0 || seq.size() >= 2return seq;
365     return CoordinateSequences.extend(csFactory, seq, 2);
366   }
367   
368   private CoordinateSequence readCoordinateSequenceRing(int size) throws IOException
369   {
370     CoordinateSequence seq = readCoordinateSequence(size);
371     if (isStrict) return seq;
372     if (CoordinateSequences.isRing(seq)) return seq;
373     return CoordinateSequences.ensureValidRing(csFactory, seq);
374   }
375  
376   /**
377    * Reads a coordinate value with the specified dimensionality.
378    * Makes the X and Y ordinates precise according to the precision model
379    * in use.
380    */
381   private void readCoordinate() throws IOException
382   {
383     for (int i = 0; i < inputDimension; i++) {
384       if (i <= 1) {
385         ordValues[i] = precisionModel.makePrecise(dis.readDouble());
386       }
387       else {
388         ordValues[i] = dis.readDouble();
389       }
390  
391     }
392   }
393  
394 }
395