/*
* The JTS Topology Suite is a collection of Java classes that
* implement the fundamental operations required to validate a given
* geo-spatial data set to a known topological specification.
*
* Copyright (C) 2001 Vivid Solutions
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*
* For more information, contact:
*
* Vivid Solutions
* Suite #1A
* 2328 Government Street
* Victoria BC V8T 5G5
* Canada
*
* (250)385-6040
* www.vividsolutions.com
*/
package com.revolsys.geometry.wkb;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import com.revolsys.geometry.model.Geometry;
import com.revolsys.geometry.model.GeometryFactory;
import com.revolsys.geometry.model.LineString;
import com.revolsys.geometry.model.Lineal;
import com.revolsys.geometry.model.LinearRing;
import com.revolsys.geometry.model.Point;
import com.revolsys.geometry.model.Polygon;
import com.revolsys.geometry.model.Polygonal;
import com.revolsys.geometry.model.Punctual;
/**
* Reads a {@link Geometry}from a byte stream in Well-Known Binary format.
* Supports use of an {@link InStream}, which allows easy use
* with arbitrary byte stream sources.
* <p>
* This class reads the format describe in {@link WKBWriter}.
* It also partially handles
* the <b>Extended WKB</b> format used by PostGIS,
* by parsing and storing SRID values.
* The reader repairs structurally-invalid input
* (specifically, LineStrings and LinearRings which contain
* too few points have vertices added,
* and non-closed rings are closed).
* <p>
* This class is designed to support reuse of a single instance to read multiple
* geometries. This class is not thread-safe; each thread should create its own
* instance.
*
* @see WKBWriter for a formal format specification
*/
public class WKBReader {
private static final String INVALID_GEOM_TYPE_MSG = "Invalid geometry type encountered in ";
/**
* Converts a hexadecimal string to a byte array.
* The hexadecimal digit symbols are case-insensitive.
*
* @param hex a string containing hex digits
* @return an array of bytes with the value of the hex string
*/
public static byte[] hexToBytes(final String hex) {
final int byteLen = hex.length() / 2;
final byte[] bytes = new byte[byteLen];
for (int i = 0; i < hex.length() / 2; i++) {
final int i2 = 2 * i;
if (i2 + 1 > hex.length()) {
throw new IllegalArgumentException("Hex string has odd length");
}
final int nib1 = hexToInt(hex.charAt(i2));
final int nib0 = hexToInt(hex.charAt(i2 + 1));
final byte b = (byte)((nib1 << 4) + (byte)nib0);
bytes[i] = b;
}
return bytes;
}
private static int hexToInt(final char hex) {
final int nib = Character.digit(hex, 16);
if (nib < 0) {
throw new IllegalArgumentException("Invalid hex digit: '" + hex + "'");
}
return nib;
}
private final ByteOrderDataInStream dis = new ByteOrderDataInStream();
private final GeometryFactory geometryFactory;
public WKBReader() {
this(GeometryFactory.DEFAULT_3D);
}
public WKBReader(final GeometryFactory geometryFactory) {
this.geometryFactory = geometryFactory;
}
/**
* Reads a single {@link Geometry} in WKB format from a byte array.
*
* @param bytes the byte array to read from
* @return the geometry read
* @throws ParseException if the WKB is ill-formed
*/
public Geometry read(final byte[] bytes) throws ParseException {
// possibly reuse the ByteArrayInStream?
// don't throw IOExceptions, since we are not doing any I/O
try {
return read(new ByteArrayInStream(bytes));
} catch (final IOException ex) {
throw new RuntimeException("Unexpected IOException caught: " + ex.getMessage());
}
}
/**
* Reads a {@link Geometry} in binary WKB format from an {@link InStream}.
*
* @param is the stream to read from
* @return the Geometry read
* @throws IOException if the underlying stream creates an error
* @throws ParseException if the WKB is ill-formed
*/
public Geometry read(final InStream is) throws IOException, ParseException {
this.dis.setInStream(is);
final Geometry geometry = readGeometry(this.geometryFactory);
return geometry;
}
private double[] readCoordinates(final int axisCount, final int vertexCount) throws IOException {
final double[] coordinates = new double[vertexCount * axisCount];
int coordinateIndex = 0;
for (int vertexIndex = 0; vertexIndex < vertexCount; vertexIndex++) {
for (int axisIndex = 0; axisIndex < axisCount; axisIndex++) {
final double coordinate = this.dis.readDouble();
final double coordinatePrecise = this.geometryFactory.makePrecise(axisIndex, coordinate);
coordinates[coordinateIndex++] = coordinatePrecise;
}
}
return coordinates;
}
private Geometry readGeometry(GeometryFactory geometryFactory)
throws IOException, ParseException {
// determine byte order
final byte byteOrderWKB = this.dis.readByte();
// always set byte order, since it may change from geometry to geometry
if (byteOrderWKB == WKBConstants.wkbNDR) {
this.dis.setOrder(ByteOrderValues.LITTLE_ENDIAN);
} else if (byteOrderWKB == WKBConstants.wkbXDR) {
this.dis.setOrder(ByteOrderValues.BIG_ENDIAN);
} else {
throw new ParseException("Unknown geometry byte order (not NDR or XDR): " + byteOrderWKB);
}
// if not strict and not XDR or NDR, then we just use the dis default set at
// the
// start of the geometry (if a multi-geometry). This allows WBKReader to
// work
// with Spatialite native BLOB WKB, as well as other WKB variants that might
// just
// specify endian-ness at the start of the multigeometry.
final int typeInt = this.dis.readInt();
final int geometryType = typeInt & 0xff;
// determine if Z values are present
final boolean hasZ = (typeInt & 0x80000000) != 0;
if (hasZ) {
geometryFactory = geometryFactory.convertAxisCount(3);
} else {
geometryFactory = geometryFactory.convertAxisCount(2);
}
// determine if SRIDs are present
final boolean hasSRID = (typeInt & 0x20000000) != 0;
int coordinateSystemId = 0;
if (hasSRID) {
coordinateSystemId = this.dis.readInt();
if (coordinateSystemId != geometryFactory.getCoordinateSystemId()) {
geometryFactory = geometryFactory.convertSrid(coordinateSystemId);
}
}
Geometry geom = null;
switch (geometryType) {
case WKBConstants.wkbPoint:
geom = readPoint(geometryFactory);
break;
case WKBConstants.wkbLineString:
geom = readLineString(geometryFactory);
break;
case WKBConstants.wkbPolygon:
geom = readPolygon(geometryFactory);
break;
case WKBConstants.wkbMultiPoint:
geom = readMultiPoint(geometryFactory);
break;
case WKBConstants.wkbMultiLineString:
geom = readMultiLineString(geometryFactory);
break;
case WKBConstants.wkbMultiPolygon:
geom = readMultiPolygon(geometryFactory);
break;
case WKBConstants.wkbGeometryCollection:
geom = readGeometryCollection(geometryFactory);
break;
default:
throw new ParseException("Unknown WKB type " + geometryType);
}
return geom;
}
private Geometry readGeometryCollection(final GeometryFactory geometryFactory)
throws IOException, ParseException {
final int geometryCount = this.dis.readInt();
final List<Geometry> geometries = new ArrayList<>(geometryCount);
for (int i = 0; i < geometryCount; i++) {
final Geometry geometry = readGeometry(geometryFactory);
geometries.add(geometry);
}
return geometryFactory.geometry(geometries);
}
private LinearRing readLinearRing(final GeometryFactory geometryFactory) throws IOException {
final int vertexCount = this.dis.readInt();
final int axisCount = geometryFactory.getAxisCount();
if (vertexCount == 0) {
return geometryFactory.linearRing();
} else {
final double[] coordinates = readCoordinates(axisCount, vertexCount);
final int lastIndex = (vertexCount - 1) * axisCount;
if (coordinates[0] == coordinates[lastIndex]) {
if (coordinates[1] == coordinates[lastIndex + 1]) {
return geometryFactory.linearRing(axisCount, coordinates);
}
}
final double[] newCoordinates = new double[coordinates.length + axisCount];
System.arraycopy(coordinates, 0, newCoordinates, 0, coordinates.length);
System.arraycopy(coordinates, 0, newCoordinates, lastIndex, axisCount);
return geometryFactory.linearRing(axisCount, newCoordinates);
}
}
private LineString readLineString(final GeometryFactory geometryFactory) throws IOException {
final int vertexCount = this.dis.readInt();
if (vertexCount == 0) {
return geometryFactory.lineString();
} else {
final int axisCount = geometryFactory.getAxisCount();
final double[] coordinates = readCoordinates(axisCount, vertexCount);
return geometryFactory.lineString(axisCount, coordinates);
}
}
private Lineal readMultiLineString(final GeometryFactory geometryFactory)
throws IOException, ParseException {
final int geometryCount = this.dis.readInt();
final LineString[] lines = new LineString[geometryCount];
for (int i = 0; i < geometryCount; i++) {
final Geometry geometry = readGeometry(geometryFactory);
if (!(geometry instanceof LineString)) {
throw new ParseException(INVALID_GEOM_TYPE_MSG + "MultiLineString");
}
lines[i] = (LineString)geometry;
}
return geometryFactory.lineal(lines);
}
private Punctual readMultiPoint(final GeometryFactory geometryFactory)
throws IOException, ParseException {
final int geometryCount = this.dis.readInt();
final Point[] points = new Point[geometryCount];
for (int i = 0; i < geometryCount; i++) {
final Geometry geometry = readGeometry(geometryFactory);
if (!(geometry instanceof Point)) {
throw new ParseException(INVALID_GEOM_TYPE_MSG + "MultiPoint");
}
points[i] = (Point)geometry;
}
return geometryFactory.punctual(points);
}
private Polygonal readMultiPolygon(final GeometryFactory geometryFactory)
throws IOException, ParseException {
final int geometryCount = this.dis.readInt();
final Polygon[] polygons = new Polygon[geometryCount];
for (int i = 0; i < geometryCount; i++) {
final Geometry g = readGeometry(geometryFactory);
if (!(g instanceof Polygon)) {
throw new ParseException(INVALID_GEOM_TYPE_MSG + "MultiPolygon");
}
polygons[i] = (Polygon)g;
}
return geometryFactory.polygonal(polygons);
}
private Point readPoint(final GeometryFactory geometryFactory) throws IOException {
final int axisCount = geometryFactory.getAxisCount();
final double[] coordinates = readCoordinates(axisCount, 1);
return geometryFactory.point(coordinates);
}
private Polygon readPolygon(final GeometryFactory geometryFactory) throws IOException {
final int ringCount = this.dis.readInt();
final List<LinearRing> rings = new ArrayList<>();
for (int i = 0; i < ringCount; i++) {
final LinearRing ring = readLinearRing(geometryFactory);
rings.add(ring);
}
return geometryFactory.polygon(rings);
}
}