diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/DDC.java b/src/main/java/com/microsoft/sqlserver/jdbc/DDC.java index 1e90ef726..2509b5e3c 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/DDC.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/DDC.java @@ -612,6 +612,10 @@ static final Object convertStreamToObject(BaseInputStream stream, byte[] byteValue = stream.getBytes(); if (JDBCType.GUID == jdbcType) { return Util.readGUID(byteValue); + } else if (JDBCType.GEOMETRY == jdbcType) { + return Geometry.STGeomFromWKB(byteValue); + } else if (JDBCType.GEOGRAPHY == jdbcType) { + return Geography.STGeomFromWKB(byteValue); } else { String hexString = Util.bytesToHexString(byteValue, byteValue.length); diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/DataTypes.java b/src/main/java/com/microsoft/sqlserver/jdbc/DataTypes.java index 3f6ebdbea..19c6a64a2 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/DataTypes.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/DataTypes.java @@ -148,7 +148,9 @@ enum SSType SQL_VARIANT (Category.SQL_VARIANT, "sql_variant", JDBCType.SQL_VARIANT), UDT (Category.UDT, "udt", JDBCType.VARBINARY), XML (Category.XML, "xml", JDBCType.LONGNVARCHAR), - TIMESTAMP (Category.TIMESTAMP, "timestamp", JDBCType.BINARY); + TIMESTAMP (Category.TIMESTAMP, "timestamp", JDBCType.BINARY), + GEOMETRY (Category.UDT, "geometry", JDBCType.GEOMETRY), + GEOGRAPHY (Category.UDT, "geography", JDBCType.GEOGRAPHY); final Category category; private final String name; @@ -352,7 +354,9 @@ enum GetterConversion EnumSet.of( JDBCType.Category.BINARY, JDBCType.Category.LONG_BINARY, - JDBCType.Category.CHARACTER)), + JDBCType.Category.CHARACTER, + JDBCType.Category.GEOMETRY, + JDBCType.Category.GEOGRAPHY)), GUID ( SSType.Category.GUID, @@ -856,7 +860,9 @@ enum JDBCType DATETIME (Category.TIMESTAMP, microsoft.sql.Types.DATETIME, "java.sql.Timestamp"), SMALLDATETIME (Category.TIMESTAMP, microsoft.sql.Types.SMALLDATETIME, "java.sql.Timestamp"), GUID (Category.CHARACTER, microsoft.sql.Types.GUID, "java.lang.String"), - SQL_VARIANT (Category.SQL_VARIANT, microsoft.sql.Types.SQL_VARIANT, "java.lang.Object"); + SQL_VARIANT (Category.SQL_VARIANT, microsoft.sql.Types.SQL_VARIANT, "java.lang.Object"), + GEOMETRY (Category.GEOMETRY, microsoft.sql.Types.GEOMETRY, "java.lang.Object"), + GEOGRAPHY (Category.GEOGRAPHY, microsoft.sql.Types.GEOGRAPHY, "java.lang.Object"); final Category category; @@ -906,6 +912,8 @@ enum Category { TVP, GUID, SQL_VARIANT, + GEOMETRY, + GEOGRAPHY } // This SetterConversion enum is based on the Category enum diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/Geography.java b/src/main/java/com/microsoft/sqlserver/jdbc/Geography.java new file mode 100644 index 000000000..7c9fcaf8a --- /dev/null +++ b/src/main/java/com/microsoft/sqlserver/jdbc/Geography.java @@ -0,0 +1,332 @@ +/* + * Microsoft JDBC Driver for SQL Server + * + * Copyright(c) Microsoft Corporation All rights reserved. + * + * This program is made available under the terms of the MIT License. See the LICENSE file in the project root for more information. + */ + +package com.microsoft.sqlserver.jdbc; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.Locale; + +public class Geography extends SQLServerSpatialDatatype { + + /** + * Private constructor used for creating a Geography object from WKT and srid. + */ + private Geography(String WellKnownText, int srid) { + this.wkt = WellKnownText; + this.srid = srid; + + try { + parseWKTForSerialization(this, currentWktPos, -1, false); + } + catch (StringIndexOutOfBoundsException e) { + throw new IllegalArgumentException("Reached unexpected end of WKT. Please make sure WKT is valid."); + } + + serializeToWkb(false); + isNull = false; + } + + /** + * Private constructor used for creating a Geography object from WKB. + */ + private Geography(byte[] wkb) { + this.wkb = wkb; + buffer = ByteBuffer.wrap(wkb); + buffer.order(ByteOrder.LITTLE_ENDIAN); + + parseWkb(); + + WKTsb = new StringBuffer(); + WKTsbNoZM = new StringBuffer(); + + constructWKT(this, internalType, numberOfPoints, numberOfFigures, numberOfSegments, numberOfShapes); + + wkt = WKTsb.toString(); + wktNoZM = WKTsbNoZM.toString(); + isNull = false; + } + + public Geography() { + // TODO Auto-generated constructor stub + } + + /** + * Returns a Geography instance from an Open Geospatial Consortium (OGC) Well-Known Text (WKT) + * representation augmented with any Z (elevation) and M (measure) values carried by the instance. + * + * @param wkt WKT + * @param srid SRID + * @return Geography instance + */ + public static Geography STGeomFromText(String wkt, int srid) { + return new Geography(wkt, srid); + } + + /** + * Returns a Geography instance from an Open Geospatial Consortium (OGC) + * Well-Known Binary (WKB) representation. + * + * @param wkb WKB + * @return Geography instance + */ + public static Geography STGeomFromWKB(byte[] wkb) { + return new Geography(wkb); + } + + /** + * Returns a constructed Geography from an internal SQL Server format for spatial data. + * + * @param wkb WKB + * @return Geography instance + */ + public static Geography deserialize(byte[] wkb) { + return new Geography(wkb); + } + + /** + * Returns a Geography instance from an Open Geospatial Consortium (OGC) Well-Known Text (WKT) representation. + * SRID is defaulted to 4326. + * + * @param wkt WKt + * @return Geography instance + */ + public static Geography parse(String wkt) { + return new Geography(wkt, 4326); + } + + /** + * Constructs a Geography instance that represents a Point instance from its X and Y values and an SRID. + * + * @param x x coordinate + * @param y y coordinate + * @param srid SRID + * @return Geography instance + */ + public static Geography point(double x, double y, int srid) { + return new Geography("POINT (" + x + " " + y + ")", srid); + } + + /** + * Returns the Open Geospatial Consortium (OGC) Well-Known Text (WKT) representation of a + * Geography instance. This text will not contain any Z (elevation) or M (measure) values carried by the instance. + * + * @return the WKT representation without the Z and M values. + */ + public String STAsText() { + if (null == wktNoZM) { + buffer = ByteBuffer.wrap(wkb); + buffer.order(ByteOrder.LITTLE_ENDIAN); + + parseWkb(); + + WKTsb = new StringBuffer(); + WKTsbNoZM = new StringBuffer(); + constructWKT(this, internalType, numberOfPoints, numberOfFigures, numberOfSegments, numberOfShapes); + wktNoZM = WKTsbNoZM.toString(); + } + return wktNoZM; + } + + /** + * Returns the Open Geospatial Consortium (OGC) Well-Known Binary (WKB) representation of a + * Geography instance. This value will not contain any Z or M values carried by the instance. + * @return WKB + */ + public byte[] STAsBinary() { + if (null == wkbNoZM) { + serializeToWkb(true); + } + return wkbNoZM; + } + + /** + * Returns the bytes that represent an internal SQL Server format of Geography type. + * + * @return WKB + */ + public byte[] serialize() { + return wkb; + } + + public boolean hasM() { + return hasMvalues; + } + + public boolean hasZ() { + return hasZvalues; + } + + public Double getX() { + if (null != internalType && internalType == InternalSpatialDatatype.POINT && points.length == 2) { + return points[0]; + } + return null; + } + + public Double getY() { + if (null != internalType && internalType == InternalSpatialDatatype.POINT && points.length == 2) { + return points[1]; + } + return null; + } + + public Double getM() { + if (null != internalType && internalType == InternalSpatialDatatype.POINT && hasM()) { + return mValues[0]; + } + return null; + } + + public Double getZ() { + if (null != internalType && internalType == InternalSpatialDatatype.POINT && hasZ()) { + return zValues[0]; + } + return null; + } + + public int getSrid() { + return srid; + } + + public boolean isNull() { + return isNull; + } + + public int STNumPoints() { + return numberOfPoints; + } + + /** + * Returns the Open Geospatial Consortium (OGC) type name represented by a Geography instance. + * + * @return type name + */ + public String STGeographyType() { + if (null != internalType) { + return internalType.getTypeName(); + } + return null; + } + + public String asTextZM() { + return wkt; + } + + public String toString() { + return wkt; + } + + protected void serializeToWkb(boolean noZM) { + ByteBuffer buf = ByteBuffer.allocate(determineWkbCapacity()); + createSerializationProperties(); + + buf.order(ByteOrder.LITTLE_ENDIAN); + buf.putInt(srid); + buf.put(version); + buf.put(serializationProperties); + + if (!isSinglePoint && !isSingleLineSegment) { + buf.putInt(numberOfPoints); + } + + for (int i = 0; i < numberOfPoints; i++) { + buf.putDouble(points[2 * i + 1]); + buf.putDouble(points[2 * i]); + } + + if (!noZM) { + if (hasZvalues) { + for (int i = 0; i < numberOfPoints; i++) { + buf.putDouble(zValues[i]); + } + } + + if (hasMvalues) { + for (int i = 0; i < numberOfPoints; i++) { + buf.putDouble(mValues[i]); + } + } + } + + if (isSinglePoint || isSingleLineSegment) { + wkb = buf.array(); + return; + } + + buf.putInt(numberOfFigures); + for (int i = 0; i < numberOfFigures; i++) { + buf.put(figures[i].getFiguresAttribute()); + buf.putInt(figures[i].getPointOffset()); + } + + buf.putInt(numberOfShapes); + for (int i = 0; i < numberOfShapes; i++) { + buf.putInt(shapes[i].getParentOffset()); + buf.putInt(shapes[i].getFigureOffset()); + buf.put(shapes[i].getOpenGISType()); + } + + if (version == 2 && null != segments) { + buf.putInt(numberOfSegments); + for (int i = 0; i < numberOfSegments; i++) { + buf.put(segments[i].getSegmentType()); + } + } + + if (noZM) { + wkbNoZM = buf.array(); + } else { + wkb = buf.array(); + + } + return; + } + + protected void parseWkb() { + srid = buffer.getInt(); + version = buffer.get(); + serializationProperties = buffer.get(); + + interpretSerializationPropBytes(); + readNumberOfPoints(); + readPoints(); + + if (hasZvalues) { + readZvalues(); + } + + if (hasMvalues) { + readMvalues(); + } + + if (!(isSinglePoint || isSingleLineSegment)) { + readNumberOfFigures(); + readFigures(); + readNumberOfShapes(); + readShapes(); + } + + determineInternalType(); + + if (buffer.hasRemaining()) { + if (version == 2 && internalType.getTypeCode() != 8 && internalType.getTypeCode() != 11) { + readNumberOfSegments(); + readSegments(); + } + } + } + + private void readPoints() { + points = new double[2 * numberOfPoints]; + for (int i = 0; i < numberOfPoints; i++) { + points[2 * i + 1] = buffer.getDouble(); + points[2 * i] = buffer.getDouble(); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/Geometry.java b/src/main/java/com/microsoft/sqlserver/jdbc/Geometry.java new file mode 100644 index 000000000..5966d5fe9 --- /dev/null +++ b/src/main/java/com/microsoft/sqlserver/jdbc/Geometry.java @@ -0,0 +1,332 @@ +/* + * Microsoft JDBC Driver for SQL Server + * + * Copyright(c) Microsoft Corporation All rights reserved. + * + * This program is made available under the terms of the MIT License. See the LICENSE file in the project root for more information. + */ + +package com.microsoft.sqlserver.jdbc; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.Locale; + +public class Geometry extends SQLServerSpatialDatatype { + + /** + * Private constructor used for creating a Geometry object from WKT and srid. + */ + private Geometry(String WellKnownText, int srid) { + this.wkt = WellKnownText; + this.srid = srid; + + try { + parseWKTForSerialization(this, currentWktPos, -1, false); + } + catch (StringIndexOutOfBoundsException e) { + throw new IllegalArgumentException("Reached unexpected end of WKT. Please make sure WKT is valid."); + } + + serializeToWkb(false); + isNull = false; + } + + /** + * Private constructor used for creating a Geometry object from WKB. + */ + private Geometry(byte[] wkb) { + this.wkb = wkb; + buffer = ByteBuffer.wrap(wkb); + buffer.order(ByteOrder.LITTLE_ENDIAN); + + parseWkb(); + + WKTsb = new StringBuffer(); + WKTsbNoZM = new StringBuffer(); + + constructWKT(this, internalType, numberOfPoints, numberOfFigures, numberOfSegments, numberOfShapes); + + wkt = WKTsb.toString(); + wktNoZM = WKTsbNoZM.toString(); + isNull = false; + } + + public Geometry() { + // TODO Auto-generated constructor stub + } + + /** + * Returns a Geometry instance from an Open Geospatial Consortium (OGC) Well-Known Text (WKT) + * representation augmented with any Z (elevation) and M (measure) values carried by the instance. + * + * @param wkt WKT + * @param srid SRID + * @return Geometry instance + */ + public static Geometry STGeomFromText(String wkt, int srid) { + return new Geometry(wkt, srid); + } + + /** + * Returns a Geometry instance from an Open Geospatial Consortium (OGC) + * Well-Known Binary (WKB) representation. + * + * @param wkb WKB + * @return Geometry instance + */ + public static Geometry STGeomFromWKB(byte[] wkb) { + return new Geometry(wkb); + } + + /** + * Returns a constructed Geometry from an internal SQL Server format for spatial data. + * + * @param wkb WKB + * @return Geometry instance + */ + public static Geometry deserialize(byte[] wkb) { + return new Geometry(wkb); + } + + /** + * Returns a Geometry instance from an Open Geospatial Consortium (OGC) Well-Known Text (WKT) representation. + * SRID is defaulted to 0. + * + * @param wkt WKT + * @return Geometry instance + */ + public static Geometry parse(String wkt) { + return new Geometry(wkt, 0); + } + + /** + * Constructs a Geometry instance that represents a Point instance from its X and Y values and an SRID. + * + * @param x x coordinate + * @param y y coordinate + * @param srid SRID + * @return Geometry instance + */ + public static Geometry point(double x, double y, int srid) { + return new Geometry("POINT (" + x + " " + y + ")", srid); + } + + /** + * Returns the Open Geospatial Consortium (OGC) Well-Known Text (WKT) representation of a + * Geometry instance. This text will not contain any Z (elevation) or M (measure) values carried by the instance. + * + * @return the WKT representation without the Z and M values. + */ + public String STAsText() { + if (null == wktNoZM) { + buffer = ByteBuffer.wrap(wkb); + buffer.order(ByteOrder.LITTLE_ENDIAN); + + parseWkb(); + + WKTsb = new StringBuffer(); + WKTsbNoZM = new StringBuffer(); + constructWKT(this, internalType, numberOfPoints, numberOfFigures, numberOfSegments, numberOfShapes); + wktNoZM = WKTsbNoZM.toString(); + } + return wktNoZM; + } + + /** + * Returns the Open Geospatial Consortium (OGC) Well-Known Binary (WKB) representation of a + * Geometry instance. This value will not contain any Z or M values carried by the instance. + * @return WKB + */ + public byte[] STAsBinary() { + if (null == wkbNoZM) { + serializeToWkb(true); + } + return wkbNoZM; + } + + /** + * Returns the bytes that represent an internal SQL Server format of Geometry type. + * + * @return WKB + */ + public byte[] serialize() { + return wkb; + } + + public boolean hasM() { + return hasMvalues; + } + + public boolean hasZ() { + return hasZvalues; + } + + public Double getX() { + if (null != internalType && internalType == InternalSpatialDatatype.POINT && points.length == 2) { + return points[0]; + } + return null; + } + + public Double getY() { + if (null != internalType && internalType == InternalSpatialDatatype.POINT && points.length == 2) { + return points[1]; + } + return null; + } + + public Double getM() { + if (null != internalType && internalType == InternalSpatialDatatype.POINT && hasM()) { + return mValues[0]; + } + return null; + } + + public Double getZ() { + if (null != internalType && internalType == InternalSpatialDatatype.POINT && hasZ()) { + return zValues[0]; + } + return null; + } + + public int getSrid() { + return srid; + } + + public boolean isNull() { + return isNull; + } + + public int STNumPoints() { + return numberOfPoints; + } + + /** + * Returns the Open Geospatial Consortium (OGC) type name represented by a geometry instance. + * + * @return type name + */ + public String STGeometryType() { + if (null != internalType) { + return internalType.getTypeName(); + } + return null; + } + + public String asTextZM() { + return wkt; + } + + public String toString() { + return wkt; + } + + protected void serializeToWkb(boolean noZM) { + ByteBuffer buf = ByteBuffer.allocate(determineWkbCapacity()); + createSerializationProperties(); + + buf.order(ByteOrder.LITTLE_ENDIAN); + buf.putInt(srid); + buf.put(version); + buf.put(serializationProperties); + + if (!isSinglePoint && !isSingleLineSegment) { + buf.putInt(numberOfPoints); + } + + for (int i = 0; i < numberOfPoints; i++) { + buf.putDouble(points[2 * i]); + buf.putDouble(points[2 * i + 1]); + } + + if (!noZM ) { + if (hasZvalues) { + for (int i = 0; i < numberOfPoints; i++) { + buf.putDouble(zValues[i]); + } + } + + if (hasMvalues) { + for (int i = 0; i < numberOfPoints; i++) { + buf.putDouble(mValues[i]); + } + } + } + + if (isSinglePoint || isSingleLineSegment) { + wkb = buf.array(); + return; + } + + buf.putInt(numberOfFigures); + for (int i = 0; i < numberOfFigures; i++) { + buf.put(figures[i].getFiguresAttribute()); + buf.putInt(figures[i].getPointOffset()); + } + + buf.putInt(numberOfShapes); + for (int i = 0; i < numberOfShapes; i++) { + buf.putInt(shapes[i].getParentOffset()); + buf.putInt(shapes[i].getFigureOffset()); + buf.put(shapes[i].getOpenGISType()); + } + + if (version == 2 && null != segments) { + buf.putInt(numberOfSegments); + for (int i = 0; i < numberOfSegments; i++) { + buf.put(segments[i].getSegmentType()); + } + } + + if (noZM) { + wkbNoZM = buf.array(); + } else { + wkb = buf.array(); + + } + return; + } + + protected void parseWkb() { + srid = buffer.getInt(); + version = buffer.get(); + serializationProperties = buffer.get(); + + interpretSerializationPropBytes(); + readNumberOfPoints(); + readPoints(); + + if (hasZvalues) { + readZvalues(); + } + + if (hasMvalues) { + readMvalues(); + } + + if (!(isSinglePoint || isSingleLineSegment)) { + readNumberOfFigures(); + readFigures(); + readNumberOfShapes(); + readShapes(); + } + + determineInternalType(); + + if (buffer.hasRemaining()) { + if (version == 2 && internalType.getTypeCode() != 8 && internalType.getTypeCode() != 11) { + readNumberOfSegments(); + readSegments(); + } + } + } + + private void readPoints() { + points = new double[2 * numberOfPoints]; + for (int i = 0; i < numberOfPoints; i++) { + points[2 * i] = buffer.getDouble(); + points[2 * i + 1] = buffer.getDouble(); + } + } +} diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/InternalSpatialDatatype.java b/src/main/java/com/microsoft/sqlserver/jdbc/InternalSpatialDatatype.java new file mode 100644 index 000000000..a4fc6edfb --- /dev/null +++ b/src/main/java/com/microsoft/sqlserver/jdbc/InternalSpatialDatatype.java @@ -0,0 +1,41 @@ +package com.microsoft.sqlserver.jdbc; + +public enum InternalSpatialDatatype { + POINT((byte)1, "POINT"), + LINESTRING((byte)2, "LINESTRING"), + POLYGON((byte)3, "POLYGON"), + MULTIPOINT((byte)4, "MULTIPOINT"), + MULTILINESTRING((byte)5, "MULTILINESTRING"), + MULTIPOLYGON((byte)6, "MULTIPOLYGON"), + GEOMETRYCOLLECTION((byte)7, "GEOMETRYCOLLECTION"), + CIRCULARSTRING((byte)8, "CIRCULARSTRING"), + COMPOUNDCURVE((byte)9, "COMPOUNDCURVE"), + CURVEPOLYGON((byte)10, "CURVEPOLYGON"), + FULLGLOBE((byte)11, "FULLGLOBE"), + INVALID_TYPE((byte)0, null); + + private byte typeCode; + private String typeName; + + private InternalSpatialDatatype(byte typeCode, String typeName) { + this.typeCode = typeCode; + this.typeName = typeName; + } + + public byte getTypeCode() { + return this.typeCode; + } + + public String getTypeName() { + return this.typeName; + } + + public static InternalSpatialDatatype valueOf(byte typeCode) { + for (InternalSpatialDatatype internalType : values()) { + if (internalType.typeCode == typeCode) { + return internalType; + } + } + return INVALID_TYPE; + } +} diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/Parameter.java b/src/main/java/com/microsoft/sqlserver/jdbc/Parameter.java index e0731eefe..b614ad5fe 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/Parameter.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/Parameter.java @@ -901,6 +901,14 @@ else if ((null != jdbcTypeSetByUser) && ((jdbcTypeSetByUser == JDBCType.NVARCHAR case SQL_VARIANT: param.typeDefinition = SSType.SQL_VARIANT.toString(); break; + + case GEOMETRY: + param.typeDefinition = SSType.GEOMETRY.toString(); + break; + + case GEOGRAPHY: + param.typeDefinition = SSType.GEOGRAPHY.toString(); + break; default: assert false : "Unexpected JDBC type " + dtv.getJdbcType(); break; diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerPreparedStatement.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerPreparedStatement.java index 04c058dcb..7106d9f10 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerPreparedStatement.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerPreparedStatement.java @@ -1585,6 +1585,24 @@ public final void setFloat(int n, setValue(n, JDBCType.REAL, x, JavaType.FLOAT, forceEncrypt); loggerExternal.exiting(getClassNameLogging(), "setFloat"); } + + public final void setGeometry(int n, + Geometry x) throws SQLServerException { + if (loggerExternal.isLoggable(java.util.logging.Level.FINER)) + loggerExternal.entering(getClassNameLogging(), "setGeometry", new Object[] {n, x}); + checkClosed(); + setValue(n, JDBCType.GEOMETRY, x, JavaType.STRING, false); + loggerExternal.exiting(getClassNameLogging(), "setGeometry"); + } + + public final void setGeography(int n, + Geography x) throws SQLServerException { + if (loggerExternal.isLoggable(java.util.logging.Level.FINER)) + loggerExternal.entering(getClassNameLogging(), "setGeography", new Object[] {n, x}); + checkClosed(); + setValue(n, JDBCType.GEOGRAPHY, x, JavaType.STRING, false); + loggerExternal.exiting(getClassNameLogging(), "setGeography"); + } public final void setInt(int n, int value) throws SQLServerException { diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerResultSet.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerResultSet.java index cdd5e8278..b993a6d3c 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerResultSet.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerResultSet.java @@ -2125,6 +2125,38 @@ public float getFloat(String columnName) throws SQLServerException { loggerExternal.exiting(getClassNameLogging(), "getFloat", value); return null != value ? value : 0; } + + public Geometry getGeometry(int columnIndex) throws SQLServerException { + loggerExternal.entering(getClassNameLogging(), "getFloat", columnIndex); + checkClosed(); + Geometry value = (Geometry) getValue(columnIndex, JDBCType.GEOMETRY); + loggerExternal.exiting(getClassNameLogging(), "getFloat", value); + return value; + } + + public Geometry getGeometry(String columnName) throws SQLServerException { + loggerExternal.entering(getClassNameLogging(), "getFloat", columnName); + checkClosed(); + Geometry value = (Geometry) getValue(findColumn(columnName), JDBCType.GEOMETRY); + loggerExternal.exiting(getClassNameLogging(), "getFloat", value); + return value; + } + + public Geography getGeography(int columnIndex) throws SQLServerException { + loggerExternal.entering(getClassNameLogging(), "getFloat", columnIndex); + checkClosed(); + Geography value = (Geography) getValue(columnIndex, JDBCType.GEOGRAPHY); + loggerExternal.exiting(getClassNameLogging(), "getFloat", value); + return value; + } + + public Geography getGeography(String columnName) throws SQLServerException { + loggerExternal.entering(getClassNameLogging(), "getFloat", columnName); + checkClosed(); + Geography value = (Geography) getValue(findColumn(columnName), JDBCType.GEOGRAPHY); + loggerExternal.exiting(getClassNameLogging(), "getFloat", value); + return value; + } public int getInt(int columnIndex) throws SQLServerException { loggerExternal.entering(getClassNameLogging(), "getInt", columnIndex); diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerSpatialDatatype.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerSpatialDatatype.java new file mode 100644 index 000000000..f290959f3 --- /dev/null +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerSpatialDatatype.java @@ -0,0 +1,1661 @@ +/* + * Microsoft JDBC Driver for SQL Server + * + * Copyright(c) Microsoft Corporation All rights reserved. + * + * This program is made available under the terms of the MIT License. See the LICENSE file in the project root for more information. + */ + +package com.microsoft.sqlserver.jdbc; + +import java.math.BigDecimal; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; + +import com.sun.mail.imap.protocol.INTERNALDATE; + +abstract class SQLServerSpatialDatatype { + + /**WKT = Well-Known-Text, WKB = Well-Knwon-Binary */ + /**As a general rule, the ~IndexEnd variables are non-inclusive (i.e. pointIndexEnd = 8 means the shape using it will + * only go up to the 7th index of the array) */ + protected ByteBuffer buffer; + protected InternalSpatialDatatype internalType; + protected String wkt; + protected String wktNoZM; + protected byte[] wkb; + protected byte[] wkbNoZM; + protected int srid; + protected byte version = 1; + protected int numberOfPoints; + protected int numberOfFigures; + protected int numberOfShapes; + protected int numberOfSegments; + protected StringBuffer WKTsb; + protected StringBuffer WKTsbNoZM; + protected int currentPointIndex = 0; + protected int currentFigureIndex = 0; + protected int currentSegmentIndex = 0; + protected int currentShapeIndex = 0; + protected double points[]; + protected double zValues[]; + protected double mValues[]; + protected Figure figures[]; + protected Shape shapes[]; + protected Segment segments[]; + + //serialization properties + protected boolean hasZvalues = false; + protected boolean hasMvalues = false; + protected boolean isValid = false; + protected boolean isSinglePoint = false; + protected boolean isSingleLineSegment = false; + protected boolean isLargerThanHemisphere = false; + protected boolean isNull = true; + + protected final byte FA_INTERIOR_RING = 0; + protected final byte FA_STROKE = 1; + protected final byte FA_EXTERIOR_RING = 2; + + protected final byte FA_POINT = 0; + protected final byte FA_LINE = 1; + protected final byte FA_ARC = 2; + protected final byte FA_COMPOSITE_CURVE = 3; + + // WKT to WKB properties + protected int currentWktPos = 0; + protected List pointList = new ArrayList(); + protected List
figureList = new ArrayList
(); + protected List shapeList = new ArrayList(); + protected List segmentList = new ArrayList(); + protected byte serializationProperties = 0; + + private final byte SEGMENT_LINE = 0; + private final byte SEGMENT_ARC = 1; + private final byte SEGMENT_FIRST_LINE = 2; + private final byte SEGMENT_FIRST_ARC = 3; + + private final byte hasZvaluesMask = 0b00000001; + private final byte hasMvaluesMask = 0b00000010; + private final byte isValidMask = 0b00000100; + private final byte isSinglePointMask = 0b00001000; + private final byte isSingleLineSegmentMask = 0b00010000; + private final byte isLargerThanHemisphereMask = 0b00100000; + + private List version_one_shape_indexes = new ArrayList(); + + /** + * Serializes the Geogemetry/Geography instance to WKB. + * + * @param noZM flag to indicate if Z and M coordinates should be included + */ + protected abstract void serializeToWkb(boolean noZM); + + /** + * Deserialize the buffer (that contains WKB representation of Geometry/Geography data), and stores it + * into multiple corresponding data structures. + * + */ + protected abstract void parseWkb(); + + /** + * Create the WKT representation of Geometry/Geography from the deserialized data. + * + * @param sd the Geometry/Geography instance. + * @param isd internal spatial datatype object + * @param pointIndexEnd upper bound for reading points + * @param figureIndexEnd upper bound for reading figures + * @param segmentIndexEnd upper bound for reading segments + * @param shapeIndexEnd upper bound for reading shapes + */ + protected void constructWKT(SQLServerSpatialDatatype sd, InternalSpatialDatatype isd, int pointIndexEnd, int figureIndexEnd, + int segmentIndexEnd, int shapeIndexEnd) { + if (null == points || numberOfPoints == 0) { + if (isd.getTypeCode() == 11) { // FULLGLOBE + if (sd instanceof Geometry) { + throw new IllegalArgumentException("Fullglobe is not supported for Geometry."); + } else { + appendToWKTBuffers("FULLGLOBE"); + return; + } + } + // handle the case of GeometryCollection having empty objects + if (isd.getTypeCode() == 7 && currentShapeIndex != shapeIndexEnd - 1) { + currentShapeIndex++; + appendToWKTBuffers(isd.getTypeName() + "("); + constructWKT(this, InternalSpatialDatatype.valueOf(shapes[currentShapeIndex].getOpenGISType()), + numberOfPoints, numberOfFigures, numberOfSegments, numberOfShapes); + appendToWKTBuffers(")"); + return; + } + appendToWKTBuffers(isd.getTypeName() + " EMPTY"); + return; + } + + appendToWKTBuffers(isd.getTypeName()); + appendToWKTBuffers("("); + + switch (isd) { + case POINT: + constructPointWKT(currentPointIndex); + break; + case LINESTRING: + case CIRCULARSTRING: + constructLineWKT(currentPointIndex, pointIndexEnd); + break; + case POLYGON: + constructShapeWKT(currentFigureIndex, figureIndexEnd); + break; + case MULTIPOINT: + case MULTILINESTRING: + constructMultiShapeWKT(currentShapeIndex, shapeIndexEnd); + break; + case COMPOUNDCURVE: + constructCompoundcurveWKT(currentSegmentIndex, segmentIndexEnd, pointIndexEnd); + break; + case MULTIPOLYGON: + constructMultipolygonWKT(currentShapeIndex, shapeIndexEnd); + break; + case GEOMETRYCOLLECTION: + constructGeometryCollectionWKT(shapeIndexEnd); + break; + case CURVEPOLYGON: + constructCurvepolygonWKT(currentFigureIndex, figureIndexEnd, currentSegmentIndex, segmentIndexEnd); + break; + default: + throw new IllegalArgumentException("Illegal character at wkt position " + currentWktPos); + } + + appendToWKTBuffers(")"); + } + + /** + * Parses WKT and populates the data structures of the Geometry/Geography instance. + * + * @param sd the Geometry/Geography instance. + * @param startPos The index to start from from the WKT. + * @param parentShapeIndex The index of the parent's Shape in the shapes array. Used to determine this shape's parent. + * @param isGeoCollection flag to indicate if this is part of a GeometryCollection. + */ + protected void parseWKTForSerialization(SQLServerSpatialDatatype sd, int startPos, int parentShapeIndex, boolean isGeoCollection) { + //after every iteration of this while loop, the currentWktPosition will be set to the + //end of the geometry/geography shape, except for the very first iteration of it. + //This means that there has to be comma (that separates the previous shape with the next shape), + //or we expect a ')' that will close the entire shape and exit the method. + + while (hasMoreToken()) { + if (startPos != 0) { + if (wkt.charAt(currentWktPos) == ')') { + return; + } else if (wkt.charAt(currentWktPos) == ',') { + currentWktPos++; + } + } + + String nextToken = getNextStringToken().toUpperCase(Locale.US); + int thisShapeIndex; + InternalSpatialDatatype isd = InternalSpatialDatatype.INVALID_TYPE; + try { + isd = InternalSpatialDatatype.valueOf(nextToken); + } + catch (Exception e) { + throw new IllegalArgumentException("Illegal character at wkt position " + currentWktPos); + } + byte fa = 0; + + if (version == 1 && (nextToken.equals("CIRCULARSTRING") || nextToken.equals("COMPOUNDCURVE") || + nextToken.equals("CURVEPOLYGON"))) { + version = 2; + } + + // check for FULLGLOBE before reading the first open bracket, since FULLGLOBE doesn't have one. + if (nextToken.equals("FULLGLOBE")) { + if (sd instanceof Geometry) { + throw new IllegalArgumentException("Fullglobe is not supported for Geometry."); + } + + if (startPos != 0) { + throw new IllegalArgumentException("Illegal character at wkt position " + currentWktPos); + } + + shapeList.add(new Shape(parentShapeIndex, -1, isd.getTypeCode())); + isLargerThanHemisphere = true; + version = 2; + break; + } + + // if next keyword is empty, continue the loop. + if (checkEmptyKeyword(parentShapeIndex, isd, false)) { + continue; + } + + readOpenBracket(); + + switch (nextToken) { + case "POINT": + if (startPos == 0 && nextToken.toUpperCase().equals("POINT")) { + isSinglePoint = true; + } + + if (isGeoCollection) { + shapeList.add(new Shape(parentShapeIndex, figureList.size(), isd.getTypeCode())); + figureList.add(new Figure(FA_LINE, pointList.size())); + } + + readPointWkt(); + break; + case "LINESTRING": + case "CIRCULARSTRING": + shapeList.add(new Shape(parentShapeIndex, figureList.size(), isd.getTypeCode())); + fa = isd.getTypeCode() == InternalSpatialDatatype.LINESTRING.getTypeCode() ? FA_STROKE : FA_EXTERIOR_RING; + figureList.add(new Figure(fa, pointList.size())); + + readLineWkt(); + + if (startPos == 0 && nextToken.toUpperCase().equals("LINESTRING") && pointList.size() == 2) { + isSingleLineSegment = true; + } + break; + case "POLYGON": + case "MULTIPOINT": + case "MULTILINESTRING": + thisShapeIndex = shapeList.size(); + shapeList.add(new Shape(parentShapeIndex, figureList.size(), isd.getTypeCode())); + + readShapeWkt(thisShapeIndex, nextToken); + + break; + case "MULTIPOLYGON": + thisShapeIndex = shapeList.size(); + shapeList.add(new Shape(parentShapeIndex, figureList.size(), isd.getTypeCode())); + + readMultiPolygonWkt(thisShapeIndex, nextToken); + + break; + case "COMPOUNDCURVE": + shapeList.add(new Shape(parentShapeIndex, figureList.size(), isd.getTypeCode())); + figureList.add(new Figure(FA_COMPOSITE_CURVE, pointList.size())); + + readCompoundCurveWkt(true); + + break; + case "CURVEPOLYGON": + shapeList.add(new Shape(parentShapeIndex, figureList.size(), isd.getTypeCode())); + + readCurvePolygon(); + + break; + case "GEOMETRYCOLLECTION": + thisShapeIndex = shapeList.size(); + shapeList.add(new Shape(parentShapeIndex, figureList.size(), isd.getTypeCode())); + + parseWKTForSerialization(this, currentWktPos, thisShapeIndex, true); + + break; + default: + throw new IllegalArgumentException("Illegal character at wkt position " + currentWktPos); + } + readCloseBracket(); + } + + populateStructures(); + } + + /** + * Constructs and appends a Point type in WKT form to the stringbuffer. + * There are two stringbuffers - WKTsb and WKTsbNoZM. WKTsb contains the X, Y, Z and M coordinates, + * whereas WKTsbNoZM contains only X and Y coordinates. + * + * @param pointIndex indicates which point to append to the stringbuffer. + * + */ + protected void constructPointWKT(int pointIndex) { + int firstPointIndex = pointIndex * 2; + int secondPointIndex = firstPointIndex + 1; + int zValueIndex = pointIndex; + int mValueIndex = pointIndex; + + if (points[firstPointIndex] % 1 == 0) { + appendToWKTBuffers((int) points[firstPointIndex]); + } else { + appendToWKTBuffers(points[firstPointIndex]); + } + appendToWKTBuffers(" "); + + if (points[secondPointIndex] % 1 == 0) { + appendToWKTBuffers((int) points[secondPointIndex]); + } else { + appendToWKTBuffers(points[secondPointIndex]); + } + appendToWKTBuffers(" "); + + if (hasZvalues && !Double.isNaN(zValues[zValueIndex]) && !(zValues[zValueIndex] == 0)) { + if (zValues[zValueIndex] % 1 == 0) { + WKTsb.append((int) zValues[zValueIndex]); + } else { + WKTsb.append(zValues[zValueIndex]); + } + WKTsb.append(" "); + + if (hasMvalues && !Double.isNaN(mValues[mValueIndex]) && !(mValues[mValueIndex] <= 0)) { + if (mValues[mValueIndex] % 1 == 0) { + WKTsb.append((int) mValues[mValueIndex]); + } else { + WKTsb.append(mValues[mValueIndex]); + } + WKTsb.append(" "); + } + } + + currentPointIndex++; + // truncate last space + WKTsb.setLength(WKTsb.length() - 1); + WKTsbNoZM.setLength(WKTsbNoZM.length() - 1); + } + + /** + * Constructs a line in WKT form. + * + * @param pointStartIndex . + * @param pointEndIndex . + */ + protected void constructLineWKT(int pointStartIndex, int pointEndIndex) { + for (int i = pointStartIndex; i < pointEndIndex; i++) { + constructPointWKT(i); + + // add ', ' to separate points, except for the last point + if (i != pointEndIndex - 1) { + appendToWKTBuffers(", "); + } + } + } + + /** + * Constructs a shape (simple Geometry/Geography entities that are contained within a single bracket) in WKT form. + * + * @param figureStartIndex . + * @param figureEndIndex . + */ + protected void constructShapeWKT(int figureStartIndex, int figureEndIndex) { + for (int i = figureStartIndex; i < figureEndIndex; i++) { + appendToWKTBuffers("("); + if (i != numberOfFigures - 1) { //not the last figure + constructLineWKT(figures[i].getPointOffset(), figures[i + 1].getPointOffset()); + } else { + constructLineWKT(figures[i].getPointOffset(), numberOfPoints); + } + + if (i != figureEndIndex - 1) { + appendToWKTBuffers("), "); + } else { + appendToWKTBuffers(")"); + } + } + } + + /** + * Constructs a mutli-shape (MultiPoint / MultiLineString) in WKT form. + * + * @param shapeStartIndex . + * @param shapeEndIndex . + */ + protected void constructMultiShapeWKT(int shapeStartIndex, int shapeEndIndex) { + for (int i = shapeStartIndex + 1; i < shapeEndIndex; i++) { + if (shapes[i].getFigureOffset() == -1) { // EMPTY + appendToWKTBuffers("EMPTY"); + } else { + constructShapeWKT(shapes[i].getFigureOffset(), shapes[i].getFigureOffset() + 1); + } + if (i != shapeEndIndex - 1) { + appendToWKTBuffers(", "); + } + } + } + + /** + * Constructs a CompoundCurve in WKT form. + * + * @param segmentStartIndex . + * @param segmentEndIndex . + * @param pointEndIndex . + */ + protected void constructCompoundcurveWKT(int segmentStartIndex, int segmentEndIndex, int pointEndIndex) { + for (int i = segmentStartIndex; i < segmentEndIndex; i++) { + byte segment = segments[i].getSegmentType(); + constructSegmentWKT(i, segment, pointEndIndex); + + if (i == segmentEndIndex - 1) { + appendToWKTBuffers(")"); + break; + } + + switch (segment) { + case 0: + case 2: + if (segments[i + 1].getSegmentType() != 0) { + appendToWKTBuffers("), "); + } + break; + case 1: + case 3: + if (segments[i + 1].getSegmentType() != 1) { + appendToWKTBuffers("), "); + } + break; + default: + return; + } + } + } + + /** + * Constructs a MultiPolygon in WKT form. + * + * @param shapeStartIndex . + * @param shapeEndIndex . + */ + protected void constructMultipolygonWKT(int shapeStartIndex, int shapeEndIndex) { + int figureStartIndex; + int figureEndIndex; + + for (int i = shapeStartIndex + 1; i < shapeEndIndex; i++) { + figureEndIndex = figures.length; + if (shapes[i].getFigureOffset() == -1) { // EMPTY + appendToWKTBuffers("EMPTY"); + if (!(i == shapeEndIndex - 1)) { // not the last exterior polygon of this multipolygon, add a comma + appendToWKTBuffers(", "); + } + continue; + } + figureStartIndex = shapes[i].getFigureOffset(); + if (i == shapes.length - 1) { // last shape + figureEndIndex = figures.length; + } else { + // look ahead and find the next shape that doesn't have -1 as its figure offset (which signifies EMPTY) + int tempCurrentShapeIndex = i + 1; + // We need to iterate this through until the very end of the shapes list, since if the last shape + // in this MultiPolygon is an EMPTY, it won't know what the correct figureEndIndex would be. + while (tempCurrentShapeIndex < shapes.length) { + if (shapes[tempCurrentShapeIndex].getFigureOffset() == -1) { + tempCurrentShapeIndex++; + continue; + } else { + figureEndIndex = shapes[tempCurrentShapeIndex].getFigureOffset(); + break; + } + } + } + + appendToWKTBuffers("("); + + for (int j = figureStartIndex; j < figureEndIndex; j++) { + appendToWKTBuffers("(");// interior ring + + if (j == figures.length - 1) { // last figure + constructLineWKT(figures[j].getPointOffset(), numberOfPoints); + } else { + constructLineWKT(figures[j].getPointOffset(), figures[j + 1].getPointOffset()); + } + + if (j == figureEndIndex - 1) { // last polygon of this multipolygon, close off the Multipolygon + appendToWKTBuffers(")"); + } else { // not the last polygon, followed by an interior ring + appendToWKTBuffers("), "); + } + } + + appendToWKTBuffers(")"); + + if (!(i == shapeEndIndex - 1)) { // not the last exterior polygon of this multipolygon, add a comma + appendToWKTBuffers(", "); + } + } + } + + /** + * Constructs a CurvePolygon in WKT form. + * + * @param figureStartIndex . + * @param figureEndIndex . + * @param segmentStartIndex . + * @param segmentEndIndex . + */ + protected void constructCurvepolygonWKT(int figureStartIndex, int figureEndIndex, int segmentStartIndex, int segmentEndIndex) { + for (int i = figureStartIndex; i < figureEndIndex; i++) { + switch (figures[i].getFiguresAttribute()) { + case 1: // line + appendToWKTBuffers("("); + + if (i == figures.length - 1) { + constructLineWKT(currentPointIndex, numberOfPoints); + } else { + constructLineWKT(currentPointIndex, figures[i + 1].getPointOffset()); + } + + appendToWKTBuffers(")"); + break; + case 2: // arc + appendToWKTBuffers("CIRCULARSTRING("); + + if (i == figures.length - 1) { + constructLineWKT(currentPointIndex, numberOfPoints); + } else { + constructLineWKT(currentPointIndex, figures[i + 1].getPointOffset()); + } + + appendToWKTBuffers(")"); + + break; + case 3: // composite curve + appendToWKTBuffers("COMPOUNDCURVE("); + + int pointEndIndex = 0; + + if (i == figures.length - 1) { + pointEndIndex = numberOfPoints; + } else { + pointEndIndex = figures[i + 1].getPointOffset(); + } + + while (currentPointIndex < pointEndIndex) { + byte segment = segments[segmentStartIndex].getSegmentType(); + constructSegmentWKT(segmentStartIndex, segment, pointEndIndex); + + if (!(currentPointIndex < pointEndIndex)) { + appendToWKTBuffers("))"); + } else { + switch (segment) { + case 0: + case 2: + if (segments[segmentStartIndex + 1].getSegmentType() != 0) { + appendToWKTBuffers("), "); + } + break; + case 1: + case 3: + if (segments[segmentStartIndex + 1].getSegmentType() != 1) { + appendToWKTBuffers("), "); + } + break; + default: + return; + } + } + + segmentStartIndex++; + } + + break; + default: + return; + } + + //Append a comma if this is not the last figure of the shape. + if (i != figureEndIndex - 1) { + appendToWKTBuffers(", "); + } + } + } + + /** + * Constructs a Segment in WKT form. + * SQL Server re-uses the last point of a segment if the following segment is of type 3 (first arc) or + * type 2 (first line). This makes sense because the last point of a segment and the first point of the next + * segment have to match for a valid curve. This means that the code has to look ahead and decide to decrement + * the currentPointIndex depending on what segment comes next, since it may have been reused (and it's reflected + * in the array of points) + * + * @param currentSegment . + * @param segment . + * @param pointEndIndex . + */ + protected void constructSegmentWKT(int currentSegment, byte segment, int pointEndIndex) { + switch (segment) { + case 0: + appendToWKTBuffers(", "); + constructLineWKT(currentPointIndex, currentPointIndex + 1); + + if (currentSegment == segments.length - 1) { // last segment + break; + } else if (segments[currentSegment + 1].getSegmentType() != 0) { // not being followed by another line, but not the last segment + currentPointIndex = currentPointIndex - 1; + incrementPointNumStartIfPointNotReused(pointEndIndex); + } + break; + + case 1: + appendToWKTBuffers(", "); + constructLineWKT(currentPointIndex, currentPointIndex + 2); + + if (currentSegment == segments.length - 1) { // last segment + break; + } else if (segments[currentSegment + 1].getSegmentType() != 1) { // not being followed by another arc, but not the last segment + currentPointIndex = currentPointIndex - 1; // only increment pointNumStart by one less than what we should be, since the last point will be reused + incrementPointNumStartIfPointNotReused(pointEndIndex); + } + + break; + case 2: + appendToWKTBuffers("("); + constructLineWKT(currentPointIndex, currentPointIndex + 2); + + if (currentSegment == segments.length - 1) { // last segment + break; + } else if (segments[currentSegment + 1].getSegmentType() != 0) { // not being followed by another line, but not the last segment + currentPointIndex = currentPointIndex - 1; // only increment pointNumStart by one less than what we should be, since the last point will be reused + incrementPointNumStartIfPointNotReused(pointEndIndex); + } + + break; + case 3: + appendToWKTBuffers("CIRCULARSTRING("); + constructLineWKT(currentPointIndex, currentPointIndex + 3); + + if (currentSegment == segments.length - 1) { // last segment + break; + } else if (segments[currentSegment + 1].getSegmentType() != 1) { // not being followed by another arc + currentPointIndex = currentPointIndex - 1; // only increment pointNumStart by one less than what we should be, since the last point will be reused + incrementPointNumStartIfPointNotReused(pointEndIndex); + } + + break; + default: + return; + } + } + + /** + * The starting point for constructing a GeometryCollection type in WKT form. + * + * @param shapeEndIndex . + */ + protected void constructGeometryCollectionWKT(int shapeEndIndex) { + currentShapeIndex++; + constructGeometryCollectionWKThelper(shapeEndIndex); + } + + /** + * Reads Point WKT and adds it to the list of points. + * This method will read up until and including the comma that may come at the end of the Point WKT. + */ + protected void readPointWkt() { + int numOfCoordinates = 0; + double sign; + double coords[] = new double[4]; + + while (numOfCoordinates < 4) { + sign = 1; + if (wkt.charAt(currentWktPos) == '-') { + sign = -1; + currentWktPos++; + } + + int startPos = currentWktPos; + + if (wkt.charAt(currentWktPos) == ')') { + break; + } + + while (currentWktPos < wkt.length() && + (Character.isDigit(wkt.charAt(currentWktPos)) + || wkt.charAt(currentWktPos) == '.' + || wkt.charAt(currentWktPos) == 'E' + || wkt.charAt(currentWktPos) == 'e')) { + currentWktPos++; + } + + try { + coords[numOfCoordinates] = sign * + new BigDecimal(wkt.substring(startPos, currentWktPos)).doubleValue(); + } catch (Exception e) { //modify to conversion exception + throw new IllegalArgumentException("Illegal character at wkt position " + currentWktPos); + } + + numOfCoordinates++; + + skipWhiteSpaces(); + + // After skipping white space after the 4th coordinate has been read, the next + // character has to be either a , or ), or the WKT is invalid. + if (numOfCoordinates == 4) { + if (wkt.charAt(currentWktPos) != ',' && wkt.charAt(currentWktPos) != ')') { + throw new IllegalArgumentException("Illegal character at wkt position " + currentWktPos); + } + } + + if (wkt.charAt(currentWktPos) == ',') { + // need at least 2 coordinates + if (numOfCoordinates == 1) { + throw new IllegalArgumentException("Illegal character at wkt position " + currentWktPos); + } + currentWktPos++; + skipWhiteSpaces(); + break; + } + skipWhiteSpaces(); + } + + if (numOfCoordinates == 4) { + hasZvalues = true; + hasMvalues = true; + } else if (numOfCoordinates == 3) { + hasZvalues = true; + } + + pointList.add(new Point(coords[0], coords[1], coords[2], coords[3])); + } + + /** + * Reads a series of Point types. + */ + protected void readLineWkt() { + while (currentWktPos < wkt.length() && wkt.charAt(currentWktPos) != ')') { + readPointWkt(); + } + } + + /** + * Reads a shape (simple Geometry/Geography entities that are contained within a single bracket) WKT. + * + * @param parentShapeIndex shape index of the parent shape that called this method + * @param nextToken next string token + */ + protected void readShapeWkt(int parentShapeIndex, String nextToken) { + byte fa = FA_POINT; + while (currentWktPos < wkt.length() && wkt.charAt(currentWktPos) != ')') { + + // if next keyword is empty, continue the loop. + // Do not check this for polygon. + if (!nextToken.equals("POLYGON") && + checkEmptyKeyword(parentShapeIndex, InternalSpatialDatatype.valueOf(nextToken), true)) { + continue; + } + + if (nextToken.equals("MULTIPOINT")) { + shapeList.add(new Shape(parentShapeIndex, figureList.size(), InternalSpatialDatatype.POINT.getTypeCode())); + } else if (nextToken.equals("MULTILINESTRING")) { + shapeList.add(new Shape(parentShapeIndex, figureList.size(), InternalSpatialDatatype.LINESTRING.getTypeCode())); + } + + if (version == 1) { + if (nextToken.equals("MULTIPOINT")) { + fa = FA_STROKE; + } else if (nextToken.equals("MULTILINESTRING") || nextToken.equals("POLYGON")) { + fa = FA_EXTERIOR_RING; + } + version_one_shape_indexes.add(figureList.size()); + } else if (version == 2) { + if (nextToken.equals("MULTIPOINT") || nextToken.equals("MULTILINESTRING") || + nextToken.equals("POLYGON") || nextToken.equals("MULTIPOLYGON")) { + fa = FA_LINE; + } + } + + figureList.add(new Figure(fa, pointList.size())); + readOpenBracket(); + readLineWkt(); + readCloseBracket(); + + skipWhiteSpaces(); + + if (wkt.charAt(currentWktPos) == ',') { // more rings to follow + readComma(); + } else if (wkt.charAt(currentWktPos) == ')') { // about to exit while loop + continue; + } else { // unexpected input + throw new IllegalArgumentException("Illegal character at wkt position " + currentWktPos); + } + } + } + + /** + * Reads a CurvePolygon WKT + */ + protected void readCurvePolygon() { + while (currentWktPos < wkt.length() && wkt.charAt(currentWktPos) != ')') { + String nextPotentialToken = getNextStringToken().toUpperCase(Locale.US); + if (nextPotentialToken.equals("CIRCULARSTRING")) { + figureList.add(new Figure(FA_ARC, pointList.size())); + readOpenBracket(); + readLineWkt(); + readCloseBracket(); + } else if (nextPotentialToken.equals("COMPOUNDCURVE")) { + figureList.add(new Figure(FA_COMPOSITE_CURVE, pointList.size())); + readOpenBracket(); + readCompoundCurveWkt(true); + readCloseBracket(); + } else if (wkt.charAt(currentWktPos) == '(') { //LineString + figureList.add(new Figure(FA_LINE, pointList.size())); + readOpenBracket(); + readLineWkt(); + readCloseBracket(); + } else { + throw new IllegalArgumentException("Illegal character at wkt position " + currentWktPos); + } + + if (wkt.charAt(currentWktPos) == ',') { // more polygons to follow + readComma(); + } else if (wkt.charAt(currentWktPos) == ')') { // about to exit while loop + continue; + } else { // unexpected input + throw new IllegalArgumentException("Illegal character at wkt position " + currentWktPos); + } + } + } + + /** + * Reads a MultiPolygon WKT + * + * @param thisShapeIndex shape index of current shape + * @param nextToken next string token + */ + protected void readMultiPolygonWkt(int thisShapeIndex, String nextToken) { + while (currentWktPos < wkt.length() && wkt.charAt(currentWktPos) != ')') { + if (checkEmptyKeyword(thisShapeIndex, InternalSpatialDatatype.valueOf(nextToken), true)) { + continue; + } + shapeList.add(new Shape(thisShapeIndex, figureList.size(), InternalSpatialDatatype.POLYGON.getTypeCode())); //exterior polygon + readOpenBracket(); + readShapeWkt(thisShapeIndex, nextToken); + readCloseBracket(); + + if (wkt.charAt(currentWktPos) == ',') { // more polygons to follow + readComma(); + } else if (wkt.charAt(currentWktPos) == ')') { // about to exit while loop + continue; + } else { // unexpected input + throw new IllegalArgumentException("Illegal character at wkt position " + currentWktPos); + } + } + } + + /** + * Reads a Segment WKT + * + * @param segmentType segment type + * @param isFirstIteration flag that indicates if this is the first iteration from the loop outside + */ + protected void readSegmentWkt(int segmentType, boolean isFirstIteration) { + segmentList.add(new Segment((byte) segmentType)); + + int segmentLength = segmentType; + + // under 2 means 0 or 1 (possible values). 0 (line) has 1 point, and 1 (arc) has 2 points, so increment by one + if (segmentLength < 2) { + segmentLength++; + } + + for (int i = 0; i < segmentLength; i++) { + //If a segment type of 2 (first line) or 3 (first arc) is not from the very first iteration of the while loop, + //then the first point has to be a duplicate point from the previous segment, so skip the first point. + if (i == 0 && !isFirstIteration && segmentType >= 2) { + skipFirstPointWkt(); + } else { + readPointWkt(); + } + } + + if (currentWktPos < wkt.length() && wkt.charAt(currentWktPos) != ')') { + if (segmentType == SEGMENT_FIRST_ARC || segmentType == SEGMENT_ARC) { + readSegmentWkt(SEGMENT_ARC, false); + } else if (segmentType == SEGMENT_FIRST_LINE | segmentType == SEGMENT_LINE) { + readSegmentWkt(SEGMENT_LINE, false); + } + } + } + + /** + * Reads a CompoundCurve WKT + * + * @param isFirstIteration flag that indicates if this is the first iteration from the loop outside + */ + protected void readCompoundCurveWkt(boolean isFirstIteration) { + while (currentWktPos < wkt.length() && wkt.charAt(currentWktPos) != ')') { + String nextPotentialToken = getNextStringToken().toUpperCase(Locale.US); + if (nextPotentialToken.equals("CIRCULARSTRING")) { + readOpenBracket(); + readSegmentWkt(SEGMENT_FIRST_ARC, isFirstIteration); + readCloseBracket(); + } else if (wkt.charAt(currentWktPos) == '(') {//LineString + readOpenBracket(); + readSegmentWkt(SEGMENT_FIRST_LINE, isFirstIteration); + readCloseBracket(); + } else { + throw new IllegalArgumentException("Illegal character at wkt position " + currentWktPos); + } + + isFirstIteration = false; + + if (wkt.charAt(currentWktPos) == ',') { // more polygons to follow + readComma(); + } else if (wkt.charAt(currentWktPos) == ')') { // about to exit while loop + continue; + } else { // unexpected input + throw new IllegalArgumentException("Illegal character at wkt position " + currentWktPos); + } + } + } + + /** + * Reads the next string token (usually POINT, LINESTRING, etc.). + * Then increments currentWktPos to the end of the string token. + * + * @return the next string token + */ + protected String getNextStringToken() { + skipWhiteSpaces(); + int endIndex = currentWktPos; + while (endIndex < wkt.length() && Character.isLetter(wkt.charAt(endIndex))) { + endIndex++; + } + int temp = currentWktPos; + currentWktPos = endIndex; + skipWhiteSpaces(); + + return wkt.substring(temp, endIndex); + } + + /** + * Populates the various data structures contained within the Geometry/Geography instace. + */ + protected void populateStructures() { + if (pointList.size() > 0) { + points = new double[pointList.size() * 2]; + + for (int i = 0; i < pointList.size(); i++) { + points[i * 2] = pointList.get(i).getX(); + points[i * 2 + 1] = pointList.get(i).getY(); + } + + if (hasZvalues) { + zValues = new double[pointList.size()]; + for (int i = 0; i < pointList.size(); i++) { + zValues[i] = pointList.get(i).getZ(); + } + } + + if (hasMvalues) { + mValues = new double[pointList.size()]; + for (int i = 0; i < pointList.size(); i++) { + mValues[i] = pointList.get(i).getM(); + } + } + } + + // if version is 2, then we need to check for potential shapes (polygon & multi-shapes) that were + // given their figure attributes as if it was version 1, since we don't know what would be the + // version of the geometry/geography before we parse the entire WKT. + if (version == 2) { + for (int i = 0; i < version_one_shape_indexes.size(); i++) { + figureList.get(version_one_shape_indexes.get(i)).setFiguresAttribute((byte) 1); + } + } + + if (figureList.size() > 0) { + figures = new Figure[figureList.size()]; + + for (int i = 0; i < figureList.size(); i++) { + figures[i] = figureList.get(i); + } + } + + // There is an edge case of empty GeometryCollections being inside other GeometryCollections. In this case, + // the figure offset of the very first shape (GeometryCollections) has to be -1, but this is not possible to know until + // We've parsed through the entire WKT and confirmed that there are 0 points. + // Therefore, if so, we make the figure offset of the first shape to be -1. + if (pointList.size() == 0 && shapeList.size() > 0 && shapeList.get(0).getOpenGISType() == 7) { + shapeList.get(0).setFigureOffset(-1); + } + + if (shapeList.size() > 0) { + shapes = new Shape[shapeList.size()]; + + for (int i = 0; i < shapeList.size(); i++) { + shapes[i] = shapeList.get(i); + } + } + + if (segmentList.size() > 0) { + segments = new Segment[segmentList.size()]; + + for (int i = 0; i < segmentList.size(); i++) { + segments[i] = segmentList.get(i); + } + } + + numberOfPoints = pointList.size(); + numberOfFigures = figureList.size(); + numberOfShapes = shapeList.size(); + numberOfSegments = segmentList.size(); + } + + protected void readOpenBracket() { + skipWhiteSpaces(); + if (wkt.charAt(currentWktPos) == '(') { + currentWktPos++; + skipWhiteSpaces(); + } else { + throw new IllegalArgumentException("Illegal character at wkt position " + currentWktPos); + } + } + + protected void readCloseBracket() { + skipWhiteSpaces(); + if (wkt.charAt(currentWktPos) == ')') { + currentWktPos++; + skipWhiteSpaces(); + } else { + throw new IllegalArgumentException("Illegal character at wkt position " + currentWktPos); + } + } + + protected boolean hasMoreToken() { + skipWhiteSpaces(); + return currentWktPos < wkt.length(); + } + + protected void createSerializationProperties() { + serializationProperties = 0; + if (hasZvalues) { + serializationProperties += hasZvaluesMask; + } + + if (hasMvalues) { + serializationProperties += hasMvaluesMask; + } + + if (isValid) { + serializationProperties += isValidMask; + } + + if (isSinglePoint) { + serializationProperties += isSinglePointMask; + } + + if (isSingleLineSegment) { + serializationProperties += isSingleLineSegmentMask; + } + + if (version == 2) { + if (isLargerThanHemisphere) { + serializationProperties += isLargerThanHemisphereMask; + } + } + } + + protected int determineWkbCapacity() { + int totalSize = 0; + + totalSize+=6; // SRID + version + SerializationPropertiesByte + + if (isSinglePoint || isSingleLineSegment) { + totalSize += 16 * numberOfPoints; + + if (hasZvalues) { + totalSize += 8 * numberOfPoints; + } + + if (hasMvalues) { + totalSize += 8 * numberOfPoints; + } + + return totalSize; + } + + int pointSize = 16; + if (hasZvalues) { + pointSize += 8; + } + + if (hasMvalues) { + pointSize += 8; + } + + totalSize += 12; // 4 bytes for 3 ints, each representing the number of points, shapes and figures + totalSize += numberOfPoints * pointSize; + totalSize += numberOfFigures * 5; + totalSize += numberOfShapes * 9; + + if (version == 2) { + totalSize += 4; // 4 bytes for 1 int, representing the number of segments + totalSize += numberOfSegments; + } + + return totalSize; + } + + /** + * Append the data to both stringbuffers. + * @param o data to append to the stringbuffers. + */ + protected void appendToWKTBuffers(Object o) { + WKTsb.append(o); + WKTsbNoZM.append(o); + } + + protected void interpretSerializationPropBytes() { + hasZvalues = (serializationProperties & hasZvaluesMask) != 0; + hasMvalues = (serializationProperties & hasMvaluesMask) != 0; + isValid = (serializationProperties & isValidMask) != 0; + isSinglePoint = (serializationProperties & isSinglePointMask) != 0; + isSingleLineSegment = (serializationProperties & isSingleLineSegmentMask) != 0; + isLargerThanHemisphere = (serializationProperties & isLargerThanHemisphereMask) != 0; + } + + protected void readNumberOfPoints() { + if (isSinglePoint) { + numberOfPoints = 1; + } else if (isSingleLineSegment) { + numberOfPoints = 2; + } else { + numberOfPoints = buffer.getInt(); + } + } + + protected void readZvalues() { + zValues = new double[numberOfPoints]; + for (int i = 0; i < numberOfPoints; i++) { + zValues[i] = buffer.getDouble(); + } + } + + protected void readMvalues() { + mValues = new double[numberOfPoints]; + for (int i = 0; i < numberOfPoints; i++) { + mValues[i] = buffer.getDouble(); + } + } + + protected void readNumberOfFigures() { + numberOfFigures = buffer.getInt(); + } + + protected void readFigures() { + byte fa; + int po; + figures = new Figure[numberOfFigures]; + for (int i = 0; i < numberOfFigures; i++) { + fa = buffer.get(); + po = buffer.getInt(); + figures[i] = new Figure(fa, po); + } + } + + protected void readNumberOfShapes() { + numberOfShapes = buffer.getInt(); + } + + protected void readShapes() { + int po; + int fo; + byte ogt; + shapes = new Shape[numberOfShapes]; + for (int i = 0; i < numberOfShapes; i++) { + po = buffer.getInt(); + fo = buffer.getInt(); + ogt = buffer.get(); + shapes[i] = new Shape(po, fo, ogt); + } + } + + protected void readNumberOfSegments() { + numberOfSegments = buffer.getInt(); + } + + protected void readSegments() { + byte st; + segments = new Segment[numberOfSegments]; + for (int i = 0; i < numberOfSegments; i++) { + st = buffer.get(); + segments[i] = new Segment(st); + } + } + + protected void determineInternalType() { + if (isSinglePoint) { + internalType = InternalSpatialDatatype.POINT; + } else if (isSingleLineSegment) { + internalType = InternalSpatialDatatype.LINESTRING; + } else { + internalType = InternalSpatialDatatype.valueOf(shapes[0].getOpenGISType()); + } + } + + protected boolean checkEmptyKeyword(int parentShapeIndex, InternalSpatialDatatype isd, boolean isInsideAnotherShape) { + String potentialEmptyKeyword = getNextStringToken().toUpperCase(Locale.US); + if (potentialEmptyKeyword.equals("EMPTY")) { + + byte typeCode = 0; + + if (isInsideAnotherShape) { + byte parentTypeCode = isd.getTypeCode(); + if (parentTypeCode == 4) { // MultiPoint + typeCode = InternalSpatialDatatype.POINT.getTypeCode(); + } else if (parentTypeCode == 5) { // MultiLineString + typeCode = InternalSpatialDatatype.LINESTRING.getTypeCode(); + } else if (parentTypeCode == 6) { // MultiPolygon + typeCode = InternalSpatialDatatype.POLYGON.getTypeCode(); + } else if (parentTypeCode == 7) { // GeometryCollection + typeCode = InternalSpatialDatatype.GEOMETRYCOLLECTION.getTypeCode(); + } else { + throw new IllegalArgumentException("Illegal parentTypeCode."); + } + } else { + typeCode = isd.getTypeCode(); + } + + shapeList.add(new Shape(parentShapeIndex, -1, typeCode)); + skipWhiteSpaces(); + if (currentWktPos < wkt.length() && wkt.charAt(currentWktPos) == ',') { + currentWktPos++; + skipWhiteSpaces(); + } + return true; + } + + if (!potentialEmptyKeyword.equals("")) { + throw new IllegalArgumentException("Illegal character at wkt position " + currentWktPos); + } + return false; + } + + private void incrementPointNumStartIfPointNotReused(int pointEndIndex) { + // We need to increment PointNumStart if the last point was actually not re-used in the points array. + // 0 for pointNumEnd indicates that this check is not applicable. + if (currentPointIndex + 1 >= pointEndIndex) { + currentPointIndex++; + } + } + + /** + * Helper used for resurcive iteration for constructing GeometryCollection in WKT form. + * + * @param shapeEndIndex . + */ + private void constructGeometryCollectionWKThelper(int shapeEndIndex) { + //phase 1: assume that there is no multi - stuff and no geometrycollection + while (currentShapeIndex < shapeEndIndex) { + InternalSpatialDatatype isd = InternalSpatialDatatype.valueOf(shapes[currentShapeIndex].getOpenGISType()); + + int figureIndex = shapes[currentShapeIndex].getFigureOffset(); + int pointIndexEnd = numberOfPoints; + int figureIndexEnd = numberOfFigures; + int segmentIndexEnd = numberOfSegments; + int shapeIndexEnd = numberOfShapes; + int figureIndexIncrement = 0; + int segmentIndexIncrement = 0; + int shapeIndexIncrement = 0; + int localCurrentSegmentIndex = 0; + int localCurrentShapeIndex = 0; + + switch (isd) { + case POINT: + figureIndexIncrement++; + currentShapeIndex++; + break; + case LINESTRING: + case CIRCULARSTRING: + figureIndexIncrement++; + currentShapeIndex++; + pointIndexEnd = figures[figureIndex + 1].getPointOffset(); + break; + case POLYGON: + case CURVEPOLYGON: + if (currentShapeIndex < shapes.length - 1) { + figureIndexEnd = shapes[currentShapeIndex + 1].getFigureOffset(); + } + + figureIndexIncrement = figureIndexEnd - currentFigureIndex; + currentShapeIndex++; + + // Needed to keep track of which segment we are at, inside the for loop + localCurrentSegmentIndex = currentSegmentIndex; + + if (isd.equals(InternalSpatialDatatype.CURVEPOLYGON)) { + // assume Version 2 + + for (int i = currentFigureIndex; i < figureIndexEnd; i++) { + // Only Compoundcurves (with figure attribute 3) can have segments + if (figures[i].getFiguresAttribute() == 3) { + + int pointOffsetEnd; + if (i == figures.length - 1) { + pointOffsetEnd = numberOfPoints; + } else { + pointOffsetEnd = figures[i + 1].getPointOffset(); + } + + int increment = calculateSegmentIncrement(localCurrentSegmentIndex, pointOffsetEnd - figures[i].getPointOffset()); + + segmentIndexIncrement = segmentIndexIncrement + increment; + localCurrentSegmentIndex = localCurrentSegmentIndex + increment; + } + } + } + + segmentIndexEnd = localCurrentSegmentIndex; + + break; + case MULTIPOINT: + case MULTILINESTRING: + case MULTIPOLYGON: + //Multipoint and MultiLineString can go on for multiple Shapes, but eventually + //the parentOffset will signal the end of the object, or it's reached the end of the + //shapes array. + //There is also no possibility that a MultiPoint or MultiLineString would branch + //into another parent. + + int thisShapesParentOffset = shapes[currentShapeIndex].getParentOffset(); + + int tempShapeIndex = currentShapeIndex; + + // Increment shapeStartIndex to account for the shape index that either Multipoint, MultiLineString + // or MultiPolygon takes up + tempShapeIndex++; + while (tempShapeIndex < shapes.length && + shapes[tempShapeIndex].getParentOffset() != thisShapesParentOffset) { + if (!(tempShapeIndex == shapes.length - 1) && // last iteration, don't check for shapes[tempShapeIndex + 1] + !(shapes[tempShapeIndex + 1].getFigureOffset() == -1)) { // disregard EMPTY cases + figureIndexEnd = shapes[tempShapeIndex + 1].getFigureOffset(); + } + tempShapeIndex++; + } + + figureIndexIncrement = figureIndexEnd - currentFigureIndex; + shapeIndexIncrement = tempShapeIndex - currentShapeIndex; + shapeIndexEnd = tempShapeIndex; + break; + case GEOMETRYCOLLECTION: + appendToWKTBuffers(isd.getTypeName()); + + // handle Empty GeometryCollection cases + if (shapes[currentShapeIndex].getFigureOffset() == -1) { + appendToWKTBuffers(" EMPTY"); + currentShapeIndex++; + if (currentShapeIndex < shapeEndIndex) { + appendToWKTBuffers(", "); + } + continue; + } + + appendToWKTBuffers("("); + + int geometryCollectionParentIndex = shapes[currentShapeIndex].getParentOffset(); + + // Needed to keep track of which shape we are at, inside the for loop + localCurrentShapeIndex = currentShapeIndex; + + while (localCurrentShapeIndex < shapes.length - 1 && + shapes[localCurrentShapeIndex + 1].getParentOffset() > geometryCollectionParentIndex) { + localCurrentShapeIndex++; + } + // increment localCurrentShapeIndex one more time since it will be used as a shapeEndIndex parameter + // for constructGeometryCollectionWKT, and the shapeEndIndex parameter is used non-inclusively + localCurrentShapeIndex++; + + currentShapeIndex++; + constructGeometryCollectionWKThelper(localCurrentShapeIndex); + + if (currentShapeIndex < shapeEndIndex) { + appendToWKTBuffers("), "); + } else { + appendToWKTBuffers(")"); + } + + continue; + case COMPOUNDCURVE: + if (currentFigureIndex == figures.length - 1) { + pointIndexEnd = numberOfPoints; + } else { + pointIndexEnd = figures[currentFigureIndex + 1].getPointOffset(); + } + + int increment = calculateSegmentIncrement(currentSegmentIndex, pointIndexEnd - + figures[currentFigureIndex].getPointOffset()); + + segmentIndexIncrement = increment; + segmentIndexEnd = currentSegmentIndex + increment; + figureIndexIncrement++; + currentShapeIndex++; + break; + case FULLGLOBE: + appendToWKTBuffers("FULLGLOBE"); + break; + default: + break; + } + + constructWKT(this, isd, pointIndexEnd, figureIndexEnd, segmentIndexEnd, shapeIndexEnd); + currentFigureIndex = currentFigureIndex + figureIndexIncrement; + currentSegmentIndex = currentSegmentIndex + segmentIndexIncrement; + currentShapeIndex = currentShapeIndex + shapeIndexIncrement; + + if (currentShapeIndex < shapeEndIndex) { + appendToWKTBuffers(", "); + } + } + } + + /** + * Calculates how many segments will be used by this shape. + * Needed to determine when the shape that uses segments (e.g. CompoundCurve) needs to stop reading + * in cases where the CompoundCurve is included as part of GeometryCollection. + * + * @param segmentStart . + * @param pointDifference number of points that were assigned to this segment to be used. + * @return the number of segments that will be used by this shape. + */ + private int calculateSegmentIncrement(int segmentStart, + int pointDifference) { + + int segmentIncrement = 0; + + while (pointDifference > 0) { + switch (segments[segmentStart].getSegmentType()) { + case 0: + pointDifference = pointDifference - 1; + + if (segmentStart == segments.length - 1 || pointDifference < 1) { // last segment + break; + } else if (segments[segmentStart + 1].getSegmentType() != 0) { // one point will be reused + pointDifference = pointDifference + 1; + } + break; + case 1: + pointDifference = pointDifference - 2; + + if (segmentStart == segments.length - 1 || pointDifference < 1) { // last segment + break; + } else if (segments[segmentStart + 1].getSegmentType() != 1) { // one point will be reused + pointDifference = pointDifference + 1; + } + break; + case 2: + pointDifference = pointDifference - 2; + + if (segmentStart == segments.length - 1 || pointDifference < 1) { // last segment + break; + } else if (segments[segmentStart + 1].getSegmentType() != 0) { // one point will be reused + pointDifference = pointDifference + 1; + } + break; + case 3: + pointDifference = pointDifference - 3; + + if (segmentStart == segments.length - 1 || pointDifference < 1) { // last segment + break; + } else if (segments[segmentStart + 1].getSegmentType() != 1) { // one point will be reused + pointDifference = pointDifference + 1; + } + break; + default: + return segmentIncrement; + } + segmentStart++; + segmentIncrement++; + } + + return segmentIncrement; + } + + private void skipFirstPointWkt() { + int numOfCoordinates = 0; + + while (numOfCoordinates < 4) { + if (wkt.charAt(currentWktPos) == '-') { + currentWktPos++; + } + + if (wkt.charAt(currentWktPos) == ')') { + break; + } + + while (currentWktPos < wkt.length() && + (Character.isDigit(wkt.charAt(currentWktPos)) + || wkt.charAt(currentWktPos) == '.' + || wkt.charAt(currentWktPos) == 'E' + || wkt.charAt(currentWktPos) == 'e')) { + currentWktPos++; + } + + skipWhiteSpaces(); + if (wkt.charAt(currentWktPos) == ',') { + currentWktPos++; + skipWhiteSpaces(); + numOfCoordinates++; + break; + } + skipWhiteSpaces(); + + numOfCoordinates++; + } + } + + private void readComma() { + skipWhiteSpaces(); + if (wkt.charAt(currentWktPos) == ',') { + currentWktPos++; + skipWhiteSpaces(); + } else { + throw new IllegalArgumentException("Illegal character at wkt position " + currentWktPos); + } + } + + private void skipWhiteSpaces() { + while (currentWktPos < wkt.length() && Character.isWhitespace(wkt.charAt(currentWktPos))) { + currentWktPos++; + } + } +} + +/** + * Class to hold and represent the internal makings of a Figure. + * + */ +class Figure { + private byte figuresAttribute; + private int pointOffset; + + Figure(byte figuresAttribute, int pointOffset) { + this.figuresAttribute = figuresAttribute; + this.pointOffset = pointOffset; + } + + public byte getFiguresAttribute() { + return figuresAttribute; + } + + public int getPointOffset() { + return pointOffset; + } + + public void setFiguresAttribute(byte fa) { + figuresAttribute = fa; + } +} + +/** + * Class to hold and represent the internal makings of a Shape. + * + */ +class Shape { + private int parentOffset; + private int figureOffset; + private byte openGISType; + + Shape(int parentOffset, int figureOffset, byte openGISType) { + this.parentOffset = parentOffset; + this.figureOffset = figureOffset; + this.openGISType = openGISType; + } + + public int getParentOffset() { + return parentOffset; + } + + public int getFigureOffset() { + return figureOffset; + } + + public byte getOpenGISType() { + return openGISType; + } + + public void setFigureOffset(int fo) { + figureOffset = fo; + } + +} + +/** + * Class to hold and represent the internal makings of a Segment. + * + */ +class Segment { + private byte segmentType; + + Segment(byte segmentType) { + this.segmentType = segmentType; + } + + public byte getSegmentType() { + return segmentType; + } +} + +/** + * Class to hold and represent the internal makings of a Point. + * + */ +class Point { + private final double x; + private final double y; + private final double z; + private final double m; + + Point(double x, double y, double z, double m) { + this.x = x; + this.y = y; + this.z = z; + this.m = m; + } + + public double getX() { + return x; + } + + public double getY() { + return y; + } + + public double getZ() { + return z; + } + + public double getM() { + return m; + } +} \ No newline at end of file diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/dtv.java b/src/main/java/com/microsoft/sqlserver/jdbc/dtv.java index 584cfd681..a0767b6fc 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/dtv.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/dtv.java @@ -1631,6 +1631,12 @@ final void executeOp(DTVExecuteOp op) throws SQLServerException { else if (JDBCType.SQL_VARIANT == jdbcType) { op.execute(this, String.valueOf(value)); } + else if (JDBCType.GEOMETRY == jdbcType) { + op.execute(this, ((Geometry) value).serialize()); + } + else if (JDBCType.GEOGRAPHY == jdbcType) { + op.execute(this, ((Geography) value).serialize()); + } else { if (null != cryptoMeta) { // if streaming types check for allowed data length in AE diff --git a/src/main/java/microsoft/sql/Types.java b/src/main/java/microsoft/sql/Types.java index 9f510c2ec..b2e7cb5d0 100644 --- a/src/main/java/microsoft/sql/Types.java +++ b/src/main/java/microsoft/sql/Types.java @@ -57,4 +57,14 @@ private Types() { * The constant in the Java programming language, sometimes referred to as a type code, that identifies the Microsoft SQL type SQL_VARIANT. */ public static final int SQL_VARIANT = -156; + + /** + * The constant in the Java programming language, sometimes referred to as a type code, that identifies the Microsoft SQL type GEOMETRY. + */ + public static final int GEOMETRY = -157; + + /** + * The constant in the Java programming language, sometimes referred to as a type code, that identifies the Microsoft SQL type GEOGRAPHY. + */ + public static final int GEOGRAPHY = -158; } diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/datatypes/SQLServerSpatialDatatypeTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/datatypes/SQLServerSpatialDatatypeTest.java new file mode 100644 index 000000000..28bd072db --- /dev/null +++ b/src/test/java/com/microsoft/sqlserver/jdbc/datatypes/SQLServerSpatialDatatypeTest.java @@ -0,0 +1,917 @@ +/* + * Microsoft JDBC Driver for SQL Server + * + * Copyright(c) Microsoft Corporation All rights reserved. + * + * This program is made available under the terms of the MIT License. See the LICENSE file in the project root for more information. + */ +package com.microsoft.sqlserver.jdbc.datatypes; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.io.IOException; +import java.sql.DriverManager; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.ArrayList; +import java.util.List; + +import org.apache.commons.codec.DecoderException; +import org.apache.commons.codec.binary.Hex; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.platform.runner.JUnitPlatform; +import org.junit.runner.RunWith; + +import com.microsoft.sqlserver.jdbc.Geography; +import com.microsoft.sqlserver.jdbc.Geometry; +import com.microsoft.sqlserver.jdbc.SQLServerConnection; +import com.microsoft.sqlserver.jdbc.SQLServerPreparedStatement; +import com.microsoft.sqlserver.jdbc.SQLServerResultSet; +import com.microsoft.sqlserver.testframework.AbstractTest; +import com.microsoft.sqlserver.testframework.Utils; + +/** + * Test Geometry / Geography classes + * + */ +@RunWith(JUnitPlatform.class) +public class SQLServerSpatialDatatypeTest extends AbstractTest { + + static SQLServerConnection con = null; + static Statement stmt = null; + static String geomTableName = "geometryTestTable"; + static String geogTableName = "geographyTestTable"; + static String spatialDatatypeTableName = "spatialDatatypeTestTable"; + static SQLServerPreparedStatement pstmt = null; + static SQLServerResultSet rs = null; + static boolean isDenaliOrLater = false; + + @Test + public void testPointWkb() throws DecoderException { + String geoWKT = "POINT(3 40 5 6)"; + byte[] geomWKB = Hex.decodeHex("00000000010F0000000000000840000000000000444000000000000014400000000000001840".toCharArray()); + byte[] geogWKB = Hex.decodeHex("E6100000010F0000000000004440000000000000084000000000000014400000000000001840".toCharArray()); + Geometry geomWKT = Geometry.deserialize(geomWKB); + Geography geogWKT = Geography.deserialize(geogWKB); + assertEquals(geomWKT.asTextZM(), geoWKT); + assertEquals(geogWKT.asTextZM(), geoWKT); + } + + @Test + public void testLineStringWkb() throws DecoderException { + String geoWKT = "LINESTRING(1 0, 0 1, -1 0)"; + byte[] geomWKB = Hex.decodeHex("00000000010403000000000000000000F03F00000000000000000000000000000000000000000000F03F000000000000F0BF000000000000000001000000010000000001000000FFFFFFFF0000000002".toCharArray()); + byte[] geogWKB = Hex.decodeHex("E61000000104030000000000000000000000000000000000F03F000000000000F03F00000000000000000000000000000000000000000000F0BF01000000010000000001000000FFFFFFFF0000000002".toCharArray()); + Geometry geomWKT = Geometry.deserialize(geomWKB); + Geography geogWKT = Geography.deserialize(geogWKB); + assertEquals(geomWKT.asTextZM(), geoWKT); + assertEquals(geogWKT.asTextZM(), geoWKT); + } + + @Test + public void testPolygonWkb() throws DecoderException { + String geoWKT = "POLYGON((0 0, 0 3, 3 3, 3 0, 0 0), (1 1, 1 2, 2 1, 1 1))"; + byte[] geomWKB = Hex.decodeHex("000000000104090000000000000000000000000000000000000000000000000000000000000000000840000000000000084000000000000008400000000000000840000000000000000000000000000000000000000000000000000000000000F03F000000000000F03F000000000000F03F00000000000000400000000000000040000000000000F03F000000000000F03F000000000000F03F020000000200000000000500000001000000FFFFFFFF0000000003".toCharArray()); + byte[] geogWKB = Hex.decodeHex("E61000000200090000000000000000000000000000000000000000000000000008400000000000000000000000000000084000000000000008400000000000000000000000000000084000000000000000000000000000000000000000000000F03F000000000000F03F0000000000000040000000000000F03F000000000000F03F0000000000000040000000000000F03F000000000000F03F020000000100000000010500000001000000FFFFFFFF0000000003".toCharArray()); + Geometry geomWKT = Geometry.deserialize(geomWKB); + Geography geogWKT = Geography.deserialize(geogWKB); + assertEquals(geomWKT.asTextZM(), geoWKT); + assertEquals(geogWKT.asTextZM(), geoWKT); + } + + @Test + public void testMultiPointWkb() throws DecoderException { + String geoWKT = "MULTIPOINT((2 3), (7 8 9.5))"; + byte[] geomWKB = Hex.decodeHex("00000000010502000000000000000000004000000000000008400000000000001C400000000000002040000000000000F8FF0000000000002340020000000100000000010100000003000000FFFFFFFF0000000004000000000000000001000000000100000001".toCharArray()); + byte[] geogWKB = Hex.decodeHex("E61000000105020000000000000000000840000000000000004000000000000020400000000000001C40000000000000F8FF0000000000002340020000000100000000010100000003000000FFFFFFFF0000000004000000000000000001000000000100000001".toCharArray()); + Geometry geomWKT = Geometry.deserialize(geomWKB); + Geography geogWKT = Geography.deserialize(geogWKB); + assertEquals(geomWKT.asTextZM(), geoWKT); + assertEquals(geogWKT.asTextZM(), geoWKT); + } + + @Test + public void testMultiLineStringWkb() throws DecoderException { + String geoWKT = "MULTILINESTRING((0 2, 1 1), (1 0, 1 1))"; + byte[] geomWKB = Hex.decodeHex("0000000001040400000000000000000000000000000000000040000000000000F03F000000000000F03F000000000000F03F0000000000000000000000000000F03F000000000000F03F020000000100000000010200000003000000FFFFFFFF0000000005000000000000000002000000000100000002".toCharArray()); + byte[] geogWKB = Hex.decodeHex("E610000001040400000000000000000000400000000000000000000000000000F03F000000000000F03F0000000000000000000000000000F03F000000000000F03F000000000000F03F020000000100000000010200000003000000FFFFFFFF0000000005000000000000000002000000000100000002".toCharArray()); + Geometry geomWKT = Geometry.deserialize(geomWKB); + Geography geogWKT = Geography.deserialize(geogWKB); + assertEquals(geomWKT.asTextZM(), geoWKT); + assertEquals(geogWKT.asTextZM(), geoWKT); + } + + @Test + public void testMultiPolygonWkb() throws DecoderException { + String geoWKT = "MULTIPOLYGON(((1 1, 1 2, 2 1, 1 1), (0 0, 0 3, 3 3, 3 0, 0 0 7)), ((9 9, 9 10, 10 9, 9 9)))"; + byte[] geomWKB = Hex.decodeHex("0000000001010D000000000000000000F03F000000000000F03F000000000000F03F00000000000000400000000000000040000000000000F03F000000000000F03F000000000000F03F000000000000000000000000000000000000000000000000000000000000084000000000000008400000000000000840000000000000084000000000000000000000000000000000000000000000000000000000000022400000000000002240000000000000224000000000000024400000000000002440000000000000224000000000000022400000000000002240000000000000F8FF000000000000F8FF000000000000F8FF000000000000F8FF000000000000F8FF000000000000F8FF000000000000F8FF000000000000F8FF0000000000001C40000000000000F8FF000000000000F8FF000000000000F8FF000000000000F8FF0300000002000000000004000000020900000003000000FFFFFFFF0000000006000000000000000003000000000200000003".toCharArray()); + byte[] geogWKB = Hex.decodeHex("E610000002010D000000000000000000F03F000000000000F03F0000000000000040000000000000F03F000000000000F03F0000000000000040000000000000F03F000000000000F03F000000000000000000000000000000000000000000000840000000000000000000000000000008400000000000000840000000000000000000000000000008400000000000000000000000000000000000000000000022400000000000002240000000000000244000000000000022400000000000002240000000000000244000000000000022400000000000002240000000000000F8FF000000000000F8FF000000000000F8FF000000000000F8FF000000000000F8FF000000000000F8FF000000000000F8FF000000000000F8FF0000000000001C40000000000000F8FF000000000000F8FF000000000000F8FF000000000000F8FF0300000001000000000104000000010900000003000000FFFFFFFF0000000006000000000000000003000000000200000003".toCharArray()); + Geometry geomWKT = Geometry.deserialize(geomWKB); + Geography geogWKT = Geography.deserialize(geogWKB); + assertEquals(geomWKT.asTextZM(), geoWKT); + assertEquals(geogWKT.asTextZM(), geoWKT); + } + + @Test + public void testGeometryCollectionWkb() throws DecoderException { + String geoWKT = "GEOMETRYCOLLECTION(POINT(3 3 1), LINESTRING(1 0, 0 1, -1 0), CIRCULARSTRING(1 3, 3 5, 4 7, 7 3, 1 3), POLYGON((0 0 2, 1 10 3, 1 0 4, 0 0 2), (0 0 2, 1 10 3, 1 0 4, 0 0 2), (0 0 2, 1 10 3, 1 0 4, 0 0 2)), MULTIPOINT((2 3), (7 8 9.5)), MULTILINESTRING((0 2, 1 1), (1 0, 1 1)), MULTIPOLYGON(((0 0, 0 3, 3 3, 3 0, 0 0), (1 1, 1 2, 2 1, 1 1)), ((9 9, 9 10, 10 9, 9 9))), COMPOUNDCURVE(CIRCULARSTRING(1 0 3, 0 1 3, 9 6 3, 8 7 3, -1 0 3), CIRCULARSTRING(-1 0 3, 7 9 3, -10 2 3), (-10 2 3, 77 77 77, 88 88 88, 2 6 4), (2 6 4, 3 3 6, 7 7 1)), CURVEPOLYGON((0 0, 0 0, 0 0, 0 0), COMPOUNDCURVE((0 -23.43778, 0 23.43778), CIRCULARSTRING(0 23.43778, -45 -23.43778, 0 -23.43778)), COMPOUNDCURVE((0 -23.43778, 7 7, 0 23.43778), CIRCULARSTRING(0 23.43778, 8 8, 8 8, -45 23.43778, -90 23.43778), (-90 23.43778, -90 -23.43778), CIRCULARSTRING(-90 -23.43778, -45 -23.43778, 0 -23.43778))), POLYGON((0 0 2, 1 10 3, 1 0 4, 0 0 2)))"; + byte[] geomWKB = Hex.decodeHex("0100000002014A00000000000000000008400000000000000840000000000000F03F00000000000000000000000000000000000000000000F03F000000000000F0BF0000000000000000000000000000F03F00000000000008400000000000000840000000000000144000000000000010400000000000001C400000000000001C400000000000000840000000000000F03F000000000000084000000000000000000000000000000000000000000000F03F0000000000002440000000000000F03F00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000F03F0000000000002440000000000000F03F00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000F03F0000000000002440000000000000F03F000000000000000000000000000000000000000000000000000000000000004000000000000008400000000000001C40000000000000204000000000000000000000000000000040000000000000F03F000000000000F03F000000000000F03F0000000000000000000000000000F03F000000000000F03F0000000000000000000000000000000000000000000000000000000000000840000000000000084000000000000008400000000000000840000000000000000000000000000000000000000000000000000000000000F03F000000000000F03F000000000000F03F00000000000000400000000000000040000000000000F03F000000000000F03F000000000000F03F00000000000022400000000000002240000000000000224000000000000024400000000000002440000000000000224000000000000022400000000000002240000000000000F03F00000000000000000000000000000000000000000000F03F0000000000002240000000000000184000000000000020400000000000001C40000000000000F0BF00000000000000000000000000001C40000000000000224000000000000024C00000000000000040000000000040534000000000004053400000000000005640000000000000564000000000000000400000000000001840000000000000084000000000000008400000000000001C400000000000001C40000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000C7D79E59127037C00000000000000000C7D79E591270374000000000008046C0C7D79E59127037C00000000000000000C7D79E59127037C00000000000000000C7D79E59127037C00000000000001C400000000000001C400000000000000000C7D79E5912703740000000000000204000000000000020400000000000002040000000000000204000000000008046C0C7D79E591270374000000000008056C0C7D79E591270374000000000008056C0C7D79E59127037C000000000008046C0C7D79E59127037C00000000000000000C7D79E59127037C000000000000000000000000000000000000000000000F03F0000000000002440000000000000F03F000000000000000000000000000000000000000000000000000000000000F03F000000000000F8FF000000000000F8FF000000000000F8FF000000000000F8FF000000000000F8FF000000000000F8FF000000000000F8FF000000000000F8FF000000000000004000000000000008400000000000001040000000000000004000000000000000400000000000000840000000000000104000000000000000400000000000000040000000000000084000000000000010400000000000000040000000000000F8FF0000000000002340000000000000F8FF000000000000F8FF000000000000F8FF000000000000F8FF000000000000F8FF000000000000F8FF000000000000F8FF000000000000F8FF000000000000F8FF000000000000F8FF000000000000F8FF000000000000F8FF000000000000F8FF000000000000F8FF000000000000F8FF000000000000F8FF000000000000F8FF00000000000008400000000000000840000000000000084000000000000008400000000000000840000000000000084000000000000008400000000000405340000000000000564000000000000010400000000000001840000000000000F03F000000000000F8FF000000000000F8FF000000000000F8FF000000000000F8FF000000000000F8FF000000000000F8FF000000000000F8FF000000000000F8FF000000000000F8FF000000000000F8FF000000000000F8FF000000000000F8FF000000000000F8FF000000000000F8FF000000000000F8FF000000000000F8FF000000000000F8FF000000000000F8FF0000000000000040000000000000084000000000000010400000000000000040120000000100000000010100000002040000000109000000010D00000001110000000115000000011600000001170000000119000000011B00000001200000000124000000032800000001340000000338000000033C000000014600000011000000FFFFFFFF0000000007000000000000000001000000000100000002000000000200000008000000000300000003000000000600000004050000000600000001050000000700000001000000000800000005080000000800000002080000000900000002000000000A000000060B0000000A000000030B0000000C00000003000000000D00000009000000000E0000000A0000000011000000031000000003010302000002000203020003010203".toCharArray()); + byte[] geogWKB = Hex.decodeHex("E610000002214A000000000000000000084000000000000008400000000000000000000000000000F03F000000000000F03F00000000000000000000000000000000000000000000F0BF0000000000000840000000000000F03F000000000000144000000000000008400000000000001C40000000000000104000000000000008400000000000001C400000000000000840000000000000F03F000000000000000000000000000000000000000000002440000000000000F03F0000000000000000000000000000F03F00000000000000000000000000000000000000000000000000000000000000000000000000002440000000000000F03F0000000000000000000000000000F03F00000000000000000000000000000000000000000000000000000000000000000000000000002440000000000000F03F0000000000000000000000000000F03F000000000000000000000000000000000000000000000840000000000000004000000000000020400000000000001C4000000000000000400000000000000000000000000000F03F000000000000F03F0000000000000000000000000000F03F000000000000F03F000000000000F03F0000000000000000000000000000000000000000000008400000000000000000000000000000084000000000000008400000000000000000000000000000084000000000000000000000000000000000000000000000F03F000000000000F03F0000000000000040000000000000F03F000000000000F03F0000000000000040000000000000F03F000000000000F03F000000000000224000000000000022400000000000002440000000000000224000000000000022400000000000002440000000000000224000000000000022400000000000000000000000000000F03F000000000000F03F0000000000000000000000000000184000000000000022400000000000001C4000000000000020400000000000000000000000000000F0BF00000000000022400000000000001C40000000000000004000000000000024C0000000000040534000000000004053400000000000005640000000000000564000000000000018400000000000000040000000000000084000000000000008400000000000001C400000000000001C4000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000C7D79E59127037C00000000000000000C7D79E59127037400000000000000000C7D79E59127037C000000000008046C0C7D79E59127037C00000000000000000C7D79E59127037C000000000000000000000000000001C400000000000001C40C7D79E591270374000000000000000000000000000002040000000000000204000000000000020400000000000002040C7D79E591270374000000000008046C0C7D79E591270374000000000008056C0C7D79E59127037C000000000008056C0C7D79E59127037C000000000008046C0C7D79E59127037C00000000000000000000000000000000000000000000000000000000000002440000000000000F03F0000000000000000000000000000F03F00000000000000000000000000000000000000000000F03F000000000000F8FF000000000000F8FF000000000000F8FF000000000000F8FF000000000000F8FF000000000000F8FF000000000000F8FF000000000000F8FF000000000000004000000000000008400000000000001040000000000000004000000000000000400000000000000840000000000000104000000000000000400000000000000040000000000000084000000000000010400000000000000040000000000000F8FF0000000000002340000000000000F8FF000000000000F8FF000000000000F8FF000000000000F8FF000000000000F8FF000000000000F8FF000000000000F8FF000000000000F8FF000000000000F8FF000000000000F8FF000000000000F8FF000000000000F8FF000000000000F8FF000000000000F8FF000000000000F8FF000000000000F8FF000000000000F8FF00000000000008400000000000000840000000000000084000000000000008400000000000000840000000000000084000000000000008400000000000405340000000000000564000000000000010400000000000001840000000000000F03F000000000000F8FF000000000000F8FF000000000000F8FF000000000000F8FF000000000000F8FF000000000000F8FF000000000000F8FF000000000000F8FF000000000000F8FF000000000000F8FF000000000000F8FF000000000000F8FF000000000000F8FF000000000000F8FF000000000000F8FF000000000000F8FF000000000000F8FF000000000000F8FF0000000000000040000000000000084000000000000010400000000000000040120000000100000000010100000002040000000109000000010D00000001110000000115000000011600000001170000000119000000011B00000001200000000124000000032800000001340000000338000000033C000000014600000011000000FFFFFFFF0000000007000000000000000001000000000100000002000000000200000008000000000300000003000000000600000004050000000600000001050000000700000001000000000800000005080000000800000002080000000900000002000000000A000000060B0000000A000000030B0000000C00000003000000000D00000009000000000E0000000A0000000011000000031000000003010302000002000203020003010203".toCharArray()); + Geometry geomWKT = Geometry.deserialize(geomWKB); + Geography geogWKT = Geography.deserialize(geogWKB); + assertEquals(geomWKT.asTextZM(), geoWKT); + assertEquals(geogWKT.asTextZM(), geoWKT); + } + + @Test + public void testCircularStringWkb() throws DecoderException { + String geoWKT = "CIRCULARSTRING(2 1 3 4, 1 2 3, 0 7 3, 1 0 3, 2 1 3)"; + byte[] geomWKB = Hex.decodeHex("000000000207050000000000000000000040000000000000F03F000000000000F03F000000000000004000000000000000000000000000001C40000000000000F03F00000000000000000000000000000040000000000000F03F000000000000084000000000000008400000000000000840000000000000084000000000000008400000000000001040000000000000F8FF000000000000F8FF000000000000F8FF000000000000F8FF01000000020000000001000000FFFFFFFF0000000008".toCharArray()); + byte[] geogWKB = Hex.decodeHex("E6100000020705000000000000000000F03F00000000000000400000000000000040000000000000F03F0000000000001C4000000000000000000000000000000000000000000000F03F000000000000F03F0000000000000040000000000000084000000000000008400000000000000840000000000000084000000000000008400000000000001040000000000000F8FF000000000000F8FF000000000000F8FF000000000000F8FF01000000020000000001000000FFFFFFFF0000000008".toCharArray()); + Geometry geomWKT = Geometry.deserialize(geomWKB); + Geography geogWKT = Geography.deserialize(geogWKB); + assertEquals(geomWKT.asTextZM(), geoWKT); + assertEquals(geogWKT.asTextZM(), geoWKT); + } + + @Test + public void testCompoundCurveWkb() throws DecoderException { + String geoWKT = "COMPOUNDCURVE(CIRCULARSTRING(1 0 3, 0 1 3, 9 6 3, 8 7 3, -1 0 3), CIRCULARSTRING(-1 0 3, 7 9 3, -10 2 3), (-10 2 3, 77 77 77, 88 88 88, 2 6 4), (2 6 4, 3 3 6, 7 7 1))"; + byte[] geomWKB = Hex.decodeHex("0000000002050C000000000000000000F03F00000000000000000000000000000000000000000000F03F0000000000002240000000000000184000000000000020400000000000001C40000000000000F0BF00000000000000000000000000001C40000000000000224000000000000024C00000000000000040000000000040534000000000004053400000000000005640000000000000564000000000000000400000000000001840000000000000084000000000000008400000000000001C400000000000001C4000000000000008400000000000000840000000000000084000000000000008400000000000000840000000000000084000000000000008400000000000405340000000000000564000000000000010400000000000001840000000000000F03F01000000030000000001000000FFFFFFFF0000000009080000000301030200000200".toCharArray()); + byte[] geogWKB = Hex.decodeHex("E610000002050C0000000000000000000000000000000000F03F000000000000F03F0000000000000000000000000000184000000000000022400000000000001C4000000000000020400000000000000000000000000000F0BF00000000000022400000000000001C40000000000000004000000000000024C0000000000040534000000000004053400000000000005640000000000000564000000000000018400000000000000040000000000000084000000000000008400000000000001C400000000000001C4000000000000008400000000000000840000000000000084000000000000008400000000000000840000000000000084000000000000008400000000000405340000000000000564000000000000010400000000000001840000000000000F03F01000000030000000001000000FFFFFFFF0000000009080000000301030200000200".toCharArray()); + Geometry geomWKT = Geometry.deserialize(geomWKB); + Geography geogWKT = Geography.deserialize(geogWKB); + assertEquals(geomWKT.asTextZM(), geoWKT); + assertEquals(geogWKT.asTextZM(), geoWKT); + } + + @Test + public void testCurvePolygonWkb() throws DecoderException { + String geoWKT = "CURVEPOLYGON((0 0, 0 0, 0 0, 0 0), CIRCULARSTRING(1 3, 3 5, 4 7, 7 3, 1 3), COMPOUNDCURVE((0 -23.43778, 0 23.43778), CIRCULARSTRING(0 23.43778, -45 -23.43778, 0 -23.43778)), COMPOUNDCURVE((0 -23.43778, 7 7, 0 23.43778), CIRCULARSTRING(0 23.43778, 8 8, 8 8, -45 23.43778, -90 23.43778), (-90 23.43778, -90 -23.43778), CIRCULARSTRING(-90 -23.43778, -45 -23.43778, 0 -23.43778)))"; + byte[] geomWKB = Hex.decodeHex("0A00000002001700000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000F03F00000000000008400000000000000840000000000000144000000000000010400000000000001C400000000000001C400000000000000840000000000000F03F00000000000008400000000000000000C7D79E59127037C00000000000000000C7D79E591270374000000000008046C0C7D79E59127037C00000000000000000C7D79E59127037C00000000000000000C7D79E59127037C00000000000001C400000000000001C400000000000000000C7D79E5912703740000000000000204000000000000020400000000000002040000000000000204000000000008046C0C7D79E591270374000000000008056C0C7D79E591270374000000000008056C0C7D79E59127037C000000000008046C0C7D79E59127037C00000000000000000C7D79E59127037C004000000010000000002040000000309000000030D00000001000000FFFFFFFF000000000A080000000203020003010203".toCharArray()); + byte[] geogWKB = Hex.decodeHex("E6100000020017000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000840000000000000F03F000000000000144000000000000008400000000000001C40000000000000104000000000000008400000000000001C400000000000000840000000000000F03FC7D79E59127037C00000000000000000C7D79E59127037400000000000000000C7D79E59127037C000000000008046C0C7D79E59127037C00000000000000000C7D79E59127037C000000000000000000000000000001C400000000000001C40C7D79E591270374000000000000000000000000000002040000000000000204000000000000020400000000000002040C7D79E591270374000000000008046C0C7D79E591270374000000000008056C0C7D79E59127037C000000000008056C0C7D79E59127037C000000000008046C0C7D79E59127037C0000000000000000004000000010000000002040000000309000000030D00000001000000FFFFFFFF000000000A080000000203020003010203".toCharArray()); + Geometry geomWKT = Geometry.deserialize(geomWKB); + Geography geogWKT = Geography.deserialize(geogWKB); + assertEquals(geomWKT.asTextZM(), geoWKT); + assertEquals(geogWKT.asTextZM(), geoWKT); + } + + @Test + public void testFullGlobeWkb() throws DecoderException { + String geoWKT = "FULLGLOBE"; + byte[] geogWKB = Hex.decodeHex("E61000000224000000000000000001000000FFFFFFFFFFFFFFFF0B".toCharArray()); + Geography geogWKT = Geography.deserialize(geogWKB); + assertEquals(geogWKT.asTextZM(), geoWKT); + } + + @Test + public void testPointWkt() throws SQLException { + beforeEachSetup(); + String geoWKT = "POINT(3 40 5 6)"; + testWkt(geoWKT); + beforeEachSetup(); + geoWKT = "POINT EMPTY"; + testWkt(geoWKT); + } + + @Test + public void testLineStringWkt() throws SQLException { + beforeEachSetup(); + String geoWKT = "LINESTRING(1 0, 0 1, -1 0)"; + testWkt(geoWKT); + beforeEachSetup(); + geoWKT = "LINESTRING EMPTY"; + testWkt(geoWKT); + } + + @Test + public void testPolygonWkt() throws SQLException { + beforeEachSetup(); + String geoWKT = "POLYGON((0 0, 0 3, 3 3, 3 0, 0 0), (1 1, 1 2, 2 1, 1 1))"; + testWkt(geoWKT); + beforeEachSetup(); + geoWKT = "POLYGON EMPTY"; + testWkt(geoWKT); + } + + @Test + public void testMultiPointWkt() throws SQLException { + beforeEachSetup(); + String geoWKT = "MULTIPOINT((2 3), (7 8 9.5))"; + testWkt(geoWKT); + beforeEachSetup(); + geoWKT = "MULTIPOINT EMPTY"; + testWkt(geoWKT); + } + + @Test + public void testMultiLineStringWkt() throws SQLException { + beforeEachSetup(); + String geoWKT = "MULTILINESTRING((0 2, 1 1), (1 0, 1 1))"; + testWkt(geoWKT); + beforeEachSetup(); + geoWKT = "MULTILINESTRING EMPTY"; + testWkt(geoWKT); + } + + @Test + public void testMultiPolygonWkt() throws SQLException { + beforeEachSetup(); + String geoWKT = "MULTIPOLYGON(((1 1, 1 2, 2 1, 1 1), (0 0, 0 3, 3 3, 3 0, 0 0 7)), ((9 9, 9 10, 10 9, 9 9)))"; + testWkt(geoWKT); + beforeEachSetup(); + geoWKT = "MULTIPOLYGON EMPTY"; + testWkt(geoWKT); + } + + @Test + public void testGeometryCollectionWkt() throws SQLException { + String geoWKT; + if (isDenaliOrLater) { + beforeEachSetup(); + geoWKT = "GEOMETRYCOLLECTION(POINT(3 3 1), LINESTRING(1 0, 0 1, -1 0), CIRCULARSTRING(1 3, 3 5, 4 7, 7 3, 1 3), POLYGON((0 0 2, 1 10 3, 1 0 4, 0 0 2), (0 0 2, 1 10 3, 1 0 4, 0 0 2), (0 0 2, 1 10 3, 1 0 4, 0 0 2)), MULTIPOINT((2 3), (7 8 9.5)), MULTILINESTRING((0 2, 1 1), (1 0, 1 1)), MULTIPOLYGON(((0 0, 0 3, 3 3, 3 0, 0 0), (1 1, 1 2, 2 1, 1 1)), ((9 9, 9 10, 10 9, 9 9))), COMPOUNDCURVE(CIRCULARSTRING(1 0 3, 0 1 3, 9 6 3, 8 7 3, -1 0 3), CIRCULARSTRING(-1 0 3, 7 9 3, -10 2 3), (-10 2 3, 77 77 77, 88 88 88, 2 6 4), (2 6 4, 3 3 6, 7 7 1)), CURVEPOLYGON((0 0, 0 0, 0 0, 0 0), COMPOUNDCURVE((0 -23.43778, 0 23.43778), CIRCULARSTRING(0 23.43778, -45 -23.43778, 0 -23.43778)), COMPOUNDCURVE((0 -23.43778, 7 7, 0 23.43778), CIRCULARSTRING(0 23.43778, 8 8, 8 8, -45 23.43778, -90 23.43778), (-90 23.43778, -90 -23.43778), CIRCULARSTRING(-90 -23.43778, -45 -23.43778, 0 -23.43778))), POLYGON((0 0 2, 1 10 3, 1 0 4, 0 0 2)))"; + testWkt(geoWKT); + } + beforeEachSetup(); + geoWKT = "GEOMETRYCOLLECTION EMPTY"; + testWkt(geoWKT); + beforeEachSetup(); + geoWKT = "GEOMETRYCOLLECTION(GEOMETRYCOLLECTION EMPTY)"; + testWkt(geoWKT); + beforeEachSetup(); + geoWKT = "GEOMETRYCOLLECTION(POINT(3 3 1), GEOMETRYCOLLECTION EMPTY, MULTIPOLYGON(((0 0, 0 3, 3 3, 3 0, 0 0), (1 1, 1 2, 2 1, 1 1)), EMPTY, EMPTY, ((0 0, 1 1, 2 2, 0 0)), EMPTY), GEOMETRYCOLLECTION EMPTY)"; + testWkt(geoWKT); + } + + @Test + public void testCircularStringWkt() throws SQLException { + if (isDenaliOrLater) { + beforeEachSetup(); + String geoWKT = "CIRCULARSTRING(2 1 3 4, 1 2 3, 0 7 3, 1 0 3, 2 1 3)"; + testWkt(geoWKT); + beforeEachSetup(); + geoWKT = "CIRCULARSTRING EMPTY"; + testWkt(geoWKT); + } + } + + @Test + public void testCompoundCurveWkt() throws SQLException { + if (isDenaliOrLater) { + beforeEachSetup(); + String geoWKT = "COMPOUNDCURVE(CIRCULARSTRING(1 0 3, 0 1 3, 9 6 3, 8 7 3, -1 0 3), CIRCULARSTRING(-1 0 3, 7 9 3, -10 2 3), (-10 2 3, 77 77 77, 88 88 88, 2 6 4), (2 6 4, 3 3 6, 7 7 1))"; + testWkt(geoWKT); + beforeEachSetup(); + geoWKT = "COMPOUNDCURVE EMPTY"; + testWkt(geoWKT); + } + } + + @Test + public void testCurvePolygonWkt() throws SQLException { + if (isDenaliOrLater) { + beforeEachSetup(); + String geoWKT = "CURVEPOLYGON((0 0, 0 0, 0 0, 0 0), CIRCULARSTRING(1 3, 3 5, 4 7, 7 3, 1 3), COMPOUNDCURVE((0 -23.43778, 0 23.43778), CIRCULARSTRING(0 23.43778, -45 -23.43778, 0 -23.43778)), COMPOUNDCURVE((0 -23.43778, 7 7, 0 23.43778), CIRCULARSTRING(0 23.43778, 8 8, 8 8, -45 23.43778, -90 23.43778), (-90 23.43778, -90 -23.43778), CIRCULARSTRING(-90 -23.43778, -45 -23.43778, 0 -23.43778)))"; + testWkt(geoWKT); + beforeEachSetup(); + geoWKT = "CURVEPOLYGON EMPTY"; + testWkt(geoWKT); + } + } + + @Test + public void testFullGlobeWkt() throws SQLException { + if (isDenaliOrLater) { + beforeEachSetup(); + + String geoWKT = "FULLGLOBE"; + Geography geogWKT = Geography.STGeomFromText(geoWKT, 4326); + + try { + Geometry.STGeomFromText(geoWKT, 0); + } + catch (IllegalArgumentException e) { + assertEquals(e.getMessage(), "Fullglobe is not supported for Geometry."); + } + + pstmt = (SQLServerPreparedStatement) con.prepareStatement("insert into "+ geogTableName + " values (?)"); + pstmt.setGeography(1, geogWKT); + pstmt.execute(); + + rs = (SQLServerResultSet) stmt.executeQuery("select c1 from " + geogTableName); + rs.next(); + assertEquals(rs.getGeography(1).asTextZM(), geoWKT); + } + } + + @Test + public void testIrregularCases() throws SQLException { + beforeEachSetup(); + + String geoWKT = " GeOMETRyCOLlECTION(POINT( 3e2 2E1 1 ), GEOMETRYCOLLECTION EmPTy , GeometryCollection(GEometryCOLLEction(GEometryCOLLEction Empty)), " + + "POLYGON( (0 0 2, 1 10 3, 1 0 4, 0 0 2)) )"; + String geoWKTSS = "GEOMETRYCOLLECTION(POINT(300 20 1), GEOMETRYCOLLECTION EMPTY, GEOMETRYCOLLECTION(GEOMETRYCOLLECTION(GEOMETRYCOLLECTION EMPTY)), POLYGON((0 0 2, 1 10 3, 1 0 4, 0 0 2)))"; + + testWkt(geoWKT, geoWKTSS); + } + + @Test + public void testIllegalCases() throws SQLException { + //Not enough closing bracket case + String geoWKT = "MULTIPOLYGON(((1 1, 1 2, 2 1, 1 1), (0 0, 0 3, 3 3, 3 0, 0 0 7)), ((9 9, 9 10, 10 9, 9 9))"; + + try { + testWkt(geoWKT); + } + catch (IllegalArgumentException e) { + assertEquals(e.getMessage(), "Reached unexpected end of WKT. Please make sure WKT is valid."); + } + + //Not enough closing and opening bracket case + geoWKT = "MULTIPOLYGON((1 1, 1 2, 2 1, 1 1), (0 0, 0 3, 3 3, 3 0, 0 0 7)), ((9 9, 9 10, 10 9, 9 9))"; + + try { + testWkt(geoWKT); + } + catch (IllegalArgumentException e) { + assertEquals(e.getMessage(), "Illegal character at wkt position 14"); + } + + //Too many closing bracket + geoWKT = "MULTIPOLYGON(((1 1, 1 2, 2 1, 1 1), (0 0, 0 3, 3 3, 3 0, 0 0 7)), ((9 9, 9 10, 10 9, 9 9))))"; + + try { + testWkt(geoWKT); + } + catch (IllegalArgumentException e) { + assertEquals(e.getMessage(), "Illegal character at wkt position 91"); + } + + //Too many opening bracket + geoWKT = "MULTIPOLYGON((((1 1, 1 2, 2 1, 1 1), (0 0, 0 3, 3 3, 3 0, 0 0 7)), ((9 9, 9 10, 10 9, 9 9)))"; + + try { + testWkt(geoWKT); + } + catch (IllegalArgumentException e) { + assertEquals(e.getMessage(), "Illegal character at wkt position 15"); + } + + //Too many coordinates + geoWKT = "MULTIPOLYGON(((1 1 3 4 5, 1 2, 2 1, 1 1), (0 0, 0 3, 3 3, 3 0, 0 0 7)), ((9 9, 9 10, 10 9, 9 9)))"; + + try { + testWkt(geoWKT); + } + catch (IllegalArgumentException e) { + assertEquals(e.getMessage(), "Illegal character at wkt position 23"); + } + + //Too little coordinates + geoWKT = "MULTIPOLYGON(((1 , 1 2, 2 1, 1 1), (0 0, 0 3, 3 3, 3 0, 0 0 7)), ((9 9, 9 10, 10 9, 9 9)))"; + + try { + testWkt(geoWKT); + } + catch (IllegalArgumentException e) { + assertEquals(e.getMessage(), "Illegal character at wkt position 17"); + } + + //Incorrect data type + geoWKT = "IvnalidPolygon(((1 , 1 2, 2 1, 1 1), (0 0, 0 3, 3 3, 3 0, 0 0 7)), ((9 9, 9 10, 10 9, 9 9)))"; + + try { + testWkt(geoWKT); + } + catch (IllegalArgumentException e) { + assertEquals(e.getMessage(), "Illegal character at wkt position 14"); + } + + // too many commas + geoWKT = "MULTIPOLYGON(((1 1, 1 2, 2 1, 1 1),, (0 0, 0 3, 3 3, 3 0, 0 0 7)), ((9 9, 9 10, 10 9, 9 9)))"; + + try { + testWkt(geoWKT); + } + catch (IllegalArgumentException e) { + assertEquals(e.getMessage(), "Illegal character at wkt position 35"); + } + + // too little commas + geoWKT = "MULTIPOLYGON(((1 1, 1 2, 2 1, 1 1) (0 0, 0 3, 3 3, 3 0, 0 0 7)), ((9 9, 9 10, 10 9, 9 9)))"; + + try { + testWkt(geoWKT); + } + catch (IllegalArgumentException e) { + assertEquals(e.getMessage(), "Illegal character at wkt position 35"); + } + } + + @Test + public void testAllTypes() throws SQLException { + if (isDenaliOrLater) { + beforeEachSetup(); + + String geoWKTPoint = "POINT(30 12.12312312 5 6)"; + String geoWKTLineString = "LINESTRING(1 1, 2 4 3, 3 9 123 332)"; + String geoWKTCircularString = "CIRCULARSTRING(1 1, 2 4, 3 9)"; + String geoWKTCompoundCurve = "COMPOUNDCURVE((1 1, 1 3), (1 3, 3 3), (3 3, 3 1), (3 1, 1 1))"; + String geoWKTCurvePolygon = "CURVEPOLYGON(CIRCULARSTRING(2 4, 4 2, 6 4, 4 6, 2 4))"; + String geoWKTPolygon = "POLYGON((0 0, 0 3, 3 3, 3 0, 0 0), (1 1, 1 2, 2 1, 1 1))"; + String geoWKTMultiPoint = "MULTIPOINT((2 3), (7 8 9.5))"; + String geoWKTMultiLineString = "MULTILINESTRING((0 2, 1 1), (1 0, 1 1))"; + String geoWKTMultiPolygon = "MULTIPOLYGON(((0 0, 0 3, 3 3, 3 0, 0 0), (1 1, 1 2, 2 1, 1 1)), ((9 9, 9 10, 10 9, 9 9)))"; + String geoWKTGeometryCollection = "GEOMETRYCOLLECTION(POLYGON((0 0 2, 1 10 3, 1 0 4, 0 0 2)), POINT(3 3 1 2.5), LINESTRING(1 0, 0 1, -1 0), GEOMETRYCOLLECTION(GEOMETRYCOLLECTION(POINT(1 2 3 4))), GEOMETRYCOLLECTION(GEOMETRYCOLLECTION EMPTY), CURVEPOLYGON((0 0, 0 0, 0 0, 0 0), CIRCULARSTRING(1 3, 3 5, 4 7, 7 3, 1 3), COMPOUNDCURVE((0 -23.43778, 0 23.43778), CIRCULARSTRING(0 23.43778, -45 -23.43778, 0 -23.43778)), COMPOUNDCURVE((0 -23.43778, 7 7, 0 23.43778), CIRCULARSTRING(0 23.43778, 8 8, 8 8, -45 23.43778, -90 23.43778), (-90 23.43778, -90 -23.43778), CIRCULARSTRING(-90 -23.43778, -45 -23.43778, 0 -23.43778))))"; + + List geoWKTList = new ArrayList(); + + geoWKTList.add(geoWKTPoint); + geoWKTList.add(geoWKTLineString); + geoWKTList.add(geoWKTCircularString); + geoWKTList.add(geoWKTCompoundCurve); + geoWKTList.add(geoWKTCurvePolygon); + geoWKTList.add(geoWKTPolygon); + geoWKTList.add(geoWKTMultiPoint); + geoWKTList.add(geoWKTMultiLineString); + geoWKTList.add(geoWKTMultiPolygon); + geoWKTList.add(geoWKTGeometryCollection); + + Geometry geomWKT; + Geography geogWKT; + + //Geometry + pstmt = (SQLServerPreparedStatement) con.prepareStatement("insert into "+ geomTableName + " values (?)"); + + geomWKT = Geometry.STGeomFromText(geoWKTPoint, 0); + pstmt.setGeometry(1, geomWKT); + pstmt.executeUpdate(); + + geomWKT = Geometry.STGeomFromText(geoWKTLineString, 0); + pstmt.setGeometry(1, geomWKT); + pstmt.executeUpdate(); + + geomWKT = Geometry.STGeomFromText(geoWKTCircularString, 0); + pstmt.setGeometry(1, geomWKT); + pstmt.executeUpdate(); + + geomWKT = Geometry.STGeomFromText(geoWKTCompoundCurve, 0); + pstmt.setGeometry(1, geomWKT); + pstmt.executeUpdate(); + + geomWKT = Geometry.STGeomFromText(geoWKTCurvePolygon, 0); + pstmt.setGeometry(1, geomWKT); + pstmt.executeUpdate(); + + geomWKT = Geometry.STGeomFromText(geoWKTPolygon, 0); + pstmt.setGeometry(1, geomWKT); + pstmt.executeUpdate(); + + geomWKT = Geometry.STGeomFromText(geoWKTMultiPoint, 0); + pstmt.setGeometry(1, geomWKT); + pstmt.executeUpdate(); + + geomWKT = Geometry.STGeomFromText(geoWKTMultiLineString, 0); + pstmt.setGeometry(1, geomWKT); + pstmt.executeUpdate(); + + geomWKT = Geometry.STGeomFromText(geoWKTMultiPolygon, 0); + pstmt.setGeometry(1, geomWKT); + pstmt.executeUpdate(); + + geomWKT = Geometry.STGeomFromText(geoWKTGeometryCollection, 0); + pstmt.setGeometry(1, geomWKT); + pstmt.executeUpdate(); + + rs = (SQLServerResultSet) stmt.executeQuery("select c1 from " + geomTableName); + for (int i = 0; i < geoWKTList.size(); i++) { + rs.next(); + assertEquals(rs.getGeometry(1).asTextZM(), geoWKTList.get(i)); + } + + //Geography + pstmt = (SQLServerPreparedStatement) con.prepareStatement("insert into "+ geogTableName + " values (?)"); + + geogWKT = Geography.STGeomFromText(geoWKTPoint, 4326); + pstmt.setGeography(1, geogWKT); + + pstmt.executeUpdate(); + + geogWKT = Geography.STGeomFromText(geoWKTLineString, 4326); + pstmt.setGeography(1, geogWKT); + + pstmt.executeUpdate(); + + geogWKT = Geography.STGeomFromText(geoWKTCircularString, 4326); + pstmt.setGeography(1, geogWKT); + + pstmt.executeUpdate(); + + geogWKT = Geography.STGeomFromText(geoWKTCompoundCurve, 4326); + pstmt.setGeography(1, geogWKT); + + pstmt.executeUpdate(); + + geogWKT = Geography.STGeomFromText(geoWKTCurvePolygon, 4326); + pstmt.setGeography(1, geogWKT); + + pstmt.executeUpdate(); + + geogWKT = Geography.STGeomFromText(geoWKTPolygon, 4326); + pstmt.setGeography(1, geogWKT); + + pstmt.executeUpdate(); + + geogWKT = Geography.STGeomFromText(geoWKTMultiPoint, 4326); + pstmt.setGeography(1, geogWKT); + + pstmt.executeUpdate(); + + geogWKT = Geography.STGeomFromText(geoWKTMultiLineString, 4326); + pstmt.setGeography(1, geogWKT); + + pstmt.executeUpdate(); + + geogWKT = Geography.STGeomFromText(geoWKTMultiPolygon, 4326); + pstmt.setGeography(1, geogWKT); + + pstmt.executeUpdate(); + + geogWKT = Geography.STGeomFromText(geoWKTGeometryCollection, 4326); + pstmt.setGeography(1, geogWKT); + + pstmt.executeUpdate(); + + rs = (SQLServerResultSet) stmt.executeQuery("select c1 from " + geogTableName); + for (int i = 0; i < geoWKTList.size(); i++) { + rs.next(); + assertEquals(rs.getGeography(1).asTextZM(), geoWKTList.get(i)); + } + } + } + + @Test + public void testMixedAllTypes() throws SQLException { + if (isDenaliOrLater) { + beforeEachSetupSpatialDatatype(); + + String geoWKTPoint = "POINT(30 12.12312312 5 6)"; + String geoWKTLineString = "LINESTRING(1 1, 2 4 3, 3 9 123 332)"; + String geoWKTCircularString = "CIRCULARSTRING(1 1, 2 4, 3 9)"; + String geoWKTCompoundCurve = "COMPOUNDCURVE((1 1, 1 3), (1 3, 3 3), (3 3, 3 1), (3 1, 1 1))"; + String geoWKTCurvePolygon = "CURVEPOLYGON(CIRCULARSTRING(2 4, 4 2, 6 4, 4 6, 2 4))"; + String geoWKTPolygon = "POLYGON((0 0, 0 3, 3 3, 3 0, 0 0), (1 1, 1 2, 2 1, 1 1))"; + String geoWKTMultiPoint = "MULTIPOINT((2 3), (7 8 9.5))"; + String geoWKTMultiLineString = "MULTILINESTRING((0 2, 1 1), (1 0, 1 1))"; + String geoWKTMultiPolygon = "MULTIPOLYGON(((0 0, 0 3, 3 3, 3 0, 0 0), (1 1, 1 2, 2 1, 1 1)), ((9 9, 9 10, 10 9, 9 9)))"; + String geoWKTGeometryCollection = "GEOMETRYCOLLECTION(POLYGON((0 0 2, 1 10 3, 1 0 4, 0 0 2)), POINT(3 3 1 2.5), LINESTRING(1 0, 0 1, -1 0), GEOMETRYCOLLECTION(GEOMETRYCOLLECTION(POINT(1 2 3 4))), GEOMETRYCOLLECTION(GEOMETRYCOLLECTION EMPTY), CURVEPOLYGON((0 0, 0 0, 0 0, 0 0), CIRCULARSTRING(1 3, 3 5, 4 7, 7 3, 1 3), COMPOUNDCURVE((0 -23.43778, 0 23.43778), CIRCULARSTRING(0 23.43778, -45 -23.43778, 0 -23.43778)), COMPOUNDCURVE((0 -23.43778, 7 7, 0 23.43778), CIRCULARSTRING(0 23.43778, 8 8, 8 8, -45 23.43778, -90 23.43778), (-90 23.43778, -90 -23.43778), CIRCULARSTRING(-90 -23.43778, -45 -23.43778, 0 -23.43778))))"; + + String s = "some string"; + Double d = 31.34; + int i2 = 5; + + List geoWKTList = new ArrayList(); + + geoWKTList.add(geoWKTPoint); + geoWKTList.add(geoWKTLineString); + geoWKTList.add(geoWKTCircularString); + geoWKTList.add(geoWKTCompoundCurve); + geoWKTList.add(geoWKTCurvePolygon); + geoWKTList.add(geoWKTPolygon); + geoWKTList.add(geoWKTMultiPoint); + geoWKTList.add(geoWKTMultiLineString); + geoWKTList.add(geoWKTMultiPolygon); + geoWKTList.add(geoWKTGeometryCollection); + + Geometry geomWKT; + Geography geogWKT; + + pstmt = (SQLServerPreparedStatement) con.prepareStatement("insert into "+ spatialDatatypeTableName + " values (?, ?, ?, ?, ?)"); + + geomWKT = Geometry.STGeomFromText(geoWKTPoint, 0); + geogWKT = Geography.STGeomFromText(geoWKTPoint, 4326); + pstmt.setGeometry(1, geomWKT); + pstmt.setGeography(2, geogWKT); + pstmt.setString(3, s); + pstmt.setDouble(4, d); + pstmt.setInt(5, i2); + + pstmt.executeUpdate(); + + geomWKT = Geometry.STGeomFromText(geoWKTLineString, 0); + geogWKT = Geography.STGeomFromText(geoWKTLineString, 4326); + pstmt.setGeometry(1, geomWKT); + pstmt.setGeography(2, geogWKT); + pstmt.setString(3, s); + pstmt.setDouble(4, d); + pstmt.setInt(5, i2); + + pstmt.executeUpdate(); + + geomWKT = Geometry.STGeomFromText(geoWKTCircularString, 0); + geogWKT = Geography.STGeomFromText(geoWKTCircularString, 4326); + pstmt.setGeometry(1, geomWKT); + pstmt.setGeography(2, geogWKT); + pstmt.setString(3, s); + pstmt.setDouble(4, d); + pstmt.setInt(5, i2); + + pstmt.executeUpdate(); + + geomWKT = Geometry.STGeomFromText(geoWKTCompoundCurve, 0); + geogWKT = Geography.STGeomFromText(geoWKTCompoundCurve, 4326); + pstmt.setGeometry(1, geomWKT); + pstmt.setGeography(2, geogWKT); + pstmt.setString(3, s); + pstmt.setDouble(4, d); + pstmt.setInt(5, i2); + + pstmt.executeUpdate(); + + geomWKT = Geometry.STGeomFromText(geoWKTCurvePolygon, 0); + geogWKT = Geography.STGeomFromText(geoWKTCurvePolygon, 4326); + pstmt.setGeometry(1, geomWKT); + pstmt.setGeography(2, geogWKT); + pstmt.setString(3, s); + pstmt.setDouble(4, d); + pstmt.setInt(5, i2); + + pstmt.executeUpdate(); + + geomWKT = Geometry.STGeomFromText(geoWKTPolygon, 0); + geogWKT = Geography.STGeomFromText(geoWKTPolygon, 4326); + pstmt.setGeometry(1, geomWKT); + pstmt.setGeography(2, geogWKT); + pstmt.setString(3, s); + pstmt.setDouble(4, d); + pstmt.setInt(5, i2); + + pstmt.executeUpdate(); + + geomWKT = Geometry.STGeomFromText(geoWKTMultiPoint, 0); + geogWKT = Geography.STGeomFromText(geoWKTMultiPoint, 4326); + pstmt.setGeometry(1, geomWKT); + pstmt.setGeography(2, geogWKT); + pstmt.setString(3, s); + pstmt.setDouble(4, d); + pstmt.setInt(5, i2); + + pstmt.executeUpdate(); + + geomWKT = Geometry.STGeomFromText(geoWKTMultiLineString, 0); + geogWKT = Geography.STGeomFromText(geoWKTMultiLineString, 4326); + pstmt.setGeometry(1, geomWKT); + pstmt.setGeography(2, geogWKT); + pstmt.setString(3, s); + pstmt.setDouble(4, d); + pstmt.setInt(5, i2); + + pstmt.executeUpdate(); + + geomWKT = Geometry.STGeomFromText(geoWKTMultiPolygon, 0); + geogWKT = Geography.STGeomFromText(geoWKTMultiPolygon, 4326); + pstmt.setGeometry(1, geomWKT); + pstmt.setGeography(2, geogWKT); + pstmt.setString(3, s); + pstmt.setDouble(4, d); + pstmt.setInt(5, i2); + + pstmt.executeUpdate(); + + geomWKT = Geometry.STGeomFromText(geoWKTGeometryCollection, 0); + geogWKT = Geography.STGeomFromText(geoWKTGeometryCollection, 4326); + pstmt.setGeometry(1, geomWKT); + pstmt.setGeography(2, geogWKT); + pstmt.setString(3, s); + pstmt.setDouble(4, d); + pstmt.setInt(5, i2); + + pstmt.executeUpdate(); + + rs = (SQLServerResultSet) stmt.executeQuery("select * from " + spatialDatatypeTableName); + for (int i = 0; i < geoWKTList.size(); i++) { + rs.next(); + assertEquals(rs.getGeometry(1).asTextZM(), geoWKTList.get(i)); + assertEquals(rs.getGeography(2).asTextZM(), geoWKTList.get(i)); + assertEquals(rs.getString(3), s); + assertEquals((Double) rs.getDouble(4), d); + assertEquals(rs.getInt(5), i2); + } + } + } + + @Test + public void testDecimalRounding() throws SQLException { + beforeEachSetup(); + + String geoWKT = "POINT(3 40.7777777777777777777 5 6)"; + String geoWKTSS = "POINT(3 40.77777777777778 5 6)"; + + testWkt(geoWKT, geoWKTSS); + } + + @Test + public void testParse() throws SQLException { + beforeEachSetup(); + + String geoWKT = "GEOMETRYCOLLECTION(POINT(300 20 1), GEOMETRYCOLLECTION EMPTY, GEOMETRYCOLLECTION(GEOMETRYCOLLECTION(GEOMETRYCOLLECTION EMPTY)), POLYGON((0 0 2, 1 10 3, 1 0 4, 0 0 2)))"; + + Geometry geomWKT = Geometry.parse(geoWKT); + Geography geogWKT = Geography.parse(geoWKT); + + pstmt = (SQLServerPreparedStatement) con.prepareStatement("insert into "+ geomTableName + " values (?)"); + pstmt.setGeometry(1, geomWKT); + pstmt.execute(); + + rs = (SQLServerResultSet) stmt.executeQuery("select c1 from " + geomTableName); + rs.next(); + assertEquals(rs.getGeometry(1).asTextZM(), geoWKT); + assertEquals(rs.getGeometry(1).getSrid(), 0); + + pstmt = (SQLServerPreparedStatement) con.prepareStatement("insert into "+ geogTableName + " values (?)"); + pstmt.setGeography(1, geogWKT); + pstmt.execute(); + + rs = (SQLServerResultSet) stmt.executeQuery("select c1 from " + geogTableName); + rs.next(); + assertEquals(rs.getGeography(1).asTextZM(), geoWKT); + assertEquals(rs.getGeography(1).getSrid(), 4326); + } + + @Test + public void testPoint() throws SQLException { + beforeEachSetup(); + + String geoWKT = "POINT(1 2)"; + + Geometry geomWKT = Geometry.point(1, 2, 0); + Geography geogWKT = Geography.point(1, 2, 4326); + + pstmt = (SQLServerPreparedStatement) con.prepareStatement("insert into "+ geomTableName + " values (?)"); + pstmt.setGeometry(1, geomWKT); + pstmt.execute(); + + rs = (SQLServerResultSet) stmt.executeQuery("select c1 from " + geomTableName); + rs.next(); + assertEquals(rs.getGeometry(1).asTextZM(), geoWKT); + assertEquals(rs.getGeometry(1).getSrid(), 0); + + pstmt = (SQLServerPreparedStatement) con.prepareStatement("insert into "+ geogTableName + " values (?)"); + pstmt.setGeography(1, geogWKT); + pstmt.execute(); + + rs = (SQLServerResultSet) stmt.executeQuery("select c1 from " + geogTableName); + rs.next(); + assertEquals(rs.getGeography(1).asTextZM(), geoWKT); + assertEquals(rs.getGeography(1).getSrid(), 4326); + } + + @Test + public void testSTAsText() throws SQLException { + beforeEachSetup(); + + String geoWKT = "GEOMETRYCOLLECTION(POINT(300 20 1), GEOMETRYCOLLECTION EMPTY, GEOMETRYCOLLECTION(GEOMETRYCOLLECTION(GEOMETRYCOLLECTION EMPTY)), POLYGON((0 0 2, 1 10 3, 1 0 4, 0 0 2)))"; + String geoWKTSS = "GEOMETRYCOLLECTION(POINT(300 20), GEOMETRYCOLLECTION EMPTY, GEOMETRYCOLLECTION(GEOMETRYCOLLECTION(GEOMETRYCOLLECTION EMPTY)), POLYGON((0 0, 1 10, 1 0, 0 0)))"; + + Geometry geomWKT = Geometry.STGeomFromText(geoWKT, 0); + Geography geogWKT = Geography.STGeomFromText(geoWKT, 4326); + + pstmt = (SQLServerPreparedStatement) con.prepareStatement("insert into "+ geomTableName + " values (?)"); + pstmt.setGeometry(1, geomWKT); + pstmt.execute(); + + rs = (SQLServerResultSet) stmt.executeQuery("select c1 from " + geomTableName); + rs.next(); + assertEquals(rs.getGeometry(1).STAsText(), geoWKTSS); + + pstmt = (SQLServerPreparedStatement) con.prepareStatement("insert into "+ geogTableName + " values (?)"); + pstmt.setGeography(1, geogWKT); + pstmt.execute(); + + rs = (SQLServerResultSet) stmt.executeQuery("select c1 from " + geogTableName); + rs.next(); + assertEquals(rs.getGeography(1).STAsText(), geoWKTSS); + } + + @Test + public void testSTAsBinary() throws SQLException { + beforeEachSetup(); + + String geoWKT = "POINT(3 40 5 6)"; + String geoWKT2 = "POINT(3 40)"; + + Geometry geomWKT = Geometry.STGeomFromText(geoWKT, 0); + Geography geogWKT = Geography.STGeomFromText(geoWKT, 4326); + + byte[] geomWKB = geomWKT.STAsBinary(); + byte[] geogWKB = geogWKT.STAsBinary(); + + Geometry geomWKT2 = Geometry.STGeomFromText(geoWKT2, 0); + Geography geogWKT2 = Geography.STGeomFromText(geoWKT2, 4326); + + byte[] geomWKB2 = geomWKT2.STAsBinary(); + byte[] geogWKB2 = geogWKT2.STAsBinary(); + + assertEquals(geomWKB, geomWKB2); + assertEquals(geogWKB, geogWKB2); + } + + private void beforeEachSetup() throws SQLException { + Utils.dropTableIfExists(geomTableName, stmt); + Utils.dropTableIfExists(geogTableName, stmt); + stmt.executeUpdate("Create table " + geomTableName + " (c1 geometry)"); + stmt.executeUpdate("Create table " + geogTableName + " (c1 geography)"); + } + + private void beforeEachSetupSpatialDatatype() throws SQLException { + Utils.dropTableIfExists(spatialDatatypeTableName, stmt); + stmt.executeUpdate("Create table " + spatialDatatypeTableName + + " (c1 geometry," + + "c2 geography," + + "c3 nvarchar(512)," + + "c4 decimal(28,4)," + + "c5 int)"); + } + + private void testWkt(String geoWKT) throws SQLException { + testWkt(geoWKT, geoWKT); + } + + private void testWkt(String geoWKT, String geoWKTSS) throws SQLException { + Geometry geomWKT = Geometry.STGeomFromText(geoWKT, 0); + Geography geogWKT = Geography.STGeomFromText(geoWKT, 4326); + + pstmt = (SQLServerPreparedStatement) con.prepareStatement("insert into "+ geomTableName + " values (?)"); + pstmt.setGeometry(1, geomWKT); + pstmt.execute(); + + rs = (SQLServerResultSet) stmt.executeQuery("select c1 from " + geomTableName); + rs.next(); + assertEquals(rs.getGeometry(1).asTextZM(), geoWKTSS); + + pstmt = (SQLServerPreparedStatement) con.prepareStatement("insert into "+ geogTableName + " values (?)"); + pstmt.setGeography(1, geogWKT); + pstmt.execute(); + + rs = (SQLServerResultSet) stmt.executeQuery("select c1 from " + geogTableName); + rs.next(); + assertEquals(rs.getGeography(1).asTextZM(), geoWKTSS); + } + + /** + * Prepare test + * + * @throws SQLException + * @throws SecurityException + * @throws IOException + */ + @BeforeAll + public static void setupHere() throws SQLException, SecurityException, IOException { + con = (SQLServerConnection) DriverManager.getConnection(connectionString); + stmt = con.createStatement(); + + rs = (SQLServerResultSet) stmt.executeQuery("select SERVERPROPERTY ( 'ProductVersion' )"); + + rs.next(); + + try { + int version = Integer.parseInt(rs.getString(1).substring(0, 2)); + + // if major version is greater than or equal to 11, it's SQL Server 2012 or above. + if (version >= 11) { + isDenaliOrLater = true; + } + } catch (Exception e) { + // Do nothing. + } + } + + /** + * drop the tables + * + * @throws SQLException + */ + @AfterAll + public static void afterAll() throws SQLException { + Utils.dropTableIfExists(geomTableName, stmt); + Utils.dropTableIfExists(geogTableName, stmt); + + if (null != stmt) { + stmt.close(); + } + + if (null != pstmt) { + pstmt.close(); + } + + if (null != rs) { + rs.close(); + } + + if (null != con) { + con.close(); + } + } +}