/* This file is part of VoltDB.
* Copyright (C) 2008-2017 VoltDB Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with VoltDB. If not, see <http://www.gnu.org/licenses/>.
*/
package org.voltdb.types;
import java.io.IOException;
import java.io.StreamTokenizer;
import java.io.StringReader;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
/**
* The Java class used to represent data with the SQL type GEOGRAPHY.
* For now, this means polygons, but may include other kinds of geospatial
* types in the future.
*/
public class GeographyValue {
// Note that Google S2 refers to each ring of a polygon as a "loop"
// whereas VoltDB uses the term "ring" or "linear ring" in documentation
// and comments on public APIs. In other places in the code, the terms
// "loop" and "ring" are used interchangeably.
/**
* The default length (in bytes) for a column with type GEOGRAPHY, if no
* length is specified.
*/
public static final int DEFAULT_LENGTH = 32768;
/**
* The minimum-allowed length (in bytes) for a column with type GEOGRAPHY.
* This is the length of a polygon with just three vertices.
*/
public static final int MIN_SERIALIZED_LENGTH = 155; // number of bytes needed to store a triangle
/**
* The maximum-allowed length (in bytes) for a column with type GEOGRAPHY.
* This is the usual max column length.
*/
public static final int MAX_SERIALIZED_LENGTH = 1048576;
//
// This is a list of loops. Each loop must be in
// S2Loop format. That is to say, it must have type XYZPoint
// type, it must be in counter-clockwise order and it must not
// be closed. All loops, even holes, are CCW.
//
private List<List<XYZPoint>> m_loops;
/**
* Create a polygon from a list of rings. Each ring is a list of points:
* <ol>
* <li>The first ring in the list is the outer ring, also known as the
* shell.</li>
* <li>Subsequent rings should be inside of the outer ring and represent
* "holes" in the polygon.</li>
* <li>The shell should have its vertices listed in counter-clockwise order,
* so that the area inside the ring is on the left side of the line segments
* formed by adjacent vertices.</li>
* <li>Each hole, or inner ring, should have its vertices listed in clockwise
* order, so that the area inside the ring (the "hole") is on the right side
* of the line segments formed by adjacent vertices.</li>
* </ol>
* Note that this is the same as the order expected by the OGC standard's
* Well-Known Text format.
*
* Note also that the rings here are lists of GeographyPointValues, and that they
* are closed. That is to say, the first vertex and the last
* vertex must be equal.
*
* @param rings A list of lists of points that will form a polygon.
*/
public GeographyValue(List<List<GeographyPointValue>> rings) {
if (rings == null || rings.size() < 1) {
throw new IllegalArgumentException("GeographyValue must be instantiated with at least one ring");
}
// Note that we need to reverse all but the
// first loop, since the EE wants them all in CCW order,
// and the OGC order for holes is CW.
//
m_loops = new ArrayList<List<XYZPoint>>();
boolean firstLoop = true;
for (List<GeographyPointValue> loop : rings) {
diagnoseLoop(loop, "Invalid loop for GeographyValue: ");
List<XYZPoint> oneLoop = new ArrayList<XYZPoint>();
int startIdx;
int endIdx;
int delta;
if (firstLoop) {
startIdx = 1;
// Don't copy the last vertex.
endIdx = loop.size() - 1;
delta = 1;
} else {
// Don't copy the last vertex.
startIdx = loop.size() - 2;
endIdx = 0;
delta = -1;
}
oneLoop.add(XYZPoint.fromGeographyPointValue(loop.get(0)));
for (int i = startIdx; i != endIdx; i += delta) {
oneLoop.add(XYZPoint.fromGeographyPointValue(loop.get(i)));
}
m_loops.add(oneLoop);
firstLoop = false;
}
}
/**
* Create a GeographyValue object from an OGC well-known text-formatted string.
* Currently only polygons can be created via this method.
*
* Well-known text format for polygons is composed of the "POLYGON" keyword
* followed by a list of rings enclosed in parenthesis. For example:
* <p><tt>
* POLYGON((0 0, 20 0, 20 20, 0 20, 0 0),(5 5, 5 15, 15 15, 15 5, 5 5))
* </tt></p>
* Each point in a ring is composed of a coordinate of longitude and a coordinate
* of latitude separated by a space. Note that longitude comes first in this notation.
*
* Additional notes about rings:
* <ol>
* <li>The first ring in the list is the outer ring, also known as the
* shell.</li>
* <li>Subsequent rings should be inside of the outer ring and represent
* "holes" in the polygon.</li>
* <li>The shell should have its vertices listed in counter-clockwise order,
* so that the area inside the ring is on the left side of the line segments
* formed by adjacent vertices.</li>
* <li>Each hole, or inner ring, should have its vertices listed in clockwise
* order, so that the area inside the ring (the "hole") is on the right side
* of the line segments formed by adjacent vertices.</li>
* <li>Each ring must be closed; that is, the last point in the ring must be
* equal to this first.</li>
* </ol>
*
* @param wkt A well-known text-formatted string for a polygon.
*/
public GeographyValue(String wkt) {
if (wkt == null) {
throw new IllegalArgumentException("Argument to GeographyValue WKT constructor was null");
}
m_loops = loopsFromWkt(wkt);
if (m_loops == null || m_loops.isEmpty()) {
throw new IllegalArgumentException("Argument to GeographyValue WKT constructor was invalid");
}
}
/**
* Create a GeographyValue object from a well-known text string.
* This format is described in {@link #GeographyValue(String) the WKT constructor}
* for this class.
*
* @param text A well-known text string
* @return A new instance of GeographyValue
*/
public static GeographyValue fromWKT(String text) {
return new GeographyValue(text);
}
/**
* Return the list of rings of a polygon. The list has the same
* values as the list of rings used to construct the polygon, or
* the sequence of WKT rings used to construct the polygon.
*
* @return A list of rings.
*/
public List<List<GeographyPointValue>> getRings() {
/*
* Gets the loops that make up the polygon, with the outer loop first.
* Note that we need to convert from XYZPoint to GeographyPointValue.
*
* Include the loop back to the first vertex. Also, since WKT wants
* holes oriented Clockwise and S2 wants everything oriented CounterClockWise,
* reverse the order of holes. We take care to leave the first vertex
* the same.
*/
List<List<GeographyPointValue>> llLoops = new ArrayList<List<GeographyPointValue>>();
boolean isShell = true;
for (List<XYZPoint> xyzLoop : m_loops) {
List<GeographyPointValue> llLoop = new ArrayList<GeographyPointValue>();
// Add the first of xyzLoop first.
llLoop.add(xyzLoop.get(0).toGeographyPointValue());
// Add shells left to right, and holes right to left. Make sure
// not to add the first element we just added.
int startIdx = (isShell ? 1 : xyzLoop.size()-1);
int endIdx = (isShell ? xyzLoop.size() : 0);
int delta = (isShell ? 1 : -1);
for (int idx = startIdx; idx != endIdx; idx += delta) {
XYZPoint xyz = xyzLoop.get(idx);
llLoop.add(xyz.toGeographyPointValue());
}
// Close the loop.
llLoop.add(xyzLoop.get(0).toGeographyPointValue());
llLoops.add(llLoop);
isShell = false;
}
return llLoops;
}
/**
* Return a representation of this object as well-known text.
* @return A well-known text string for this object.
*/
@Override
public String toString() {
return toWKT();
}
/**
* Return a representation of this object as well-known text.
* @return A well-known text string for this object.
*/
public String toWKT() {
StringBuffer sb = new StringBuffer();
sb.append("POLYGON (");
boolean isFirstLoop = true;
for (List<XYZPoint> loop : m_loops) {
if (!isFirstLoop) {
sb.append(", ");
}
sb.append("(");
int startIdx = (isFirstLoop ? 1 : loop.size()-1);
int endIdx = (isFirstLoop ? loop.size() : 0);
int increment = (isFirstLoop ? 1 : -1);
sb.append(loop.get(0).toGeographyPointValue().formatLngLat()).append(", ");
for (int idx = startIdx; idx != endIdx; idx += increment) {
XYZPoint xyz = loop.get(idx);
sb.append(xyz.toGeographyPointValue().formatLngLat());
sb.append(", ");
}
// Repeat the start vertex to close the loop as WKT requires.
sb.append(loop.get(0).toGeographyPointValue().formatLngLat());
sb.append(")");
isFirstLoop = false;
}
sb.append(")");
return sb.toString();
}
@Override
public boolean equals(Object o) {
if (!(o instanceof GeographyValue)) {
return false;
}
GeographyValue that = (GeographyValue)o;
if (this == that) {
return true;
}
List<List<GeographyPointValue>> expectedRings = that.getRings();
List<List<GeographyPointValue>> actualRings = getRings();
// check number of rings/loops
if (expectedRings.size() != actualRings.size()) {
return false;
}
Iterator<List<GeographyPointValue>> expectedRingIt = expectedRings.iterator();
for (List<GeographyPointValue> actualRing : actualRings) {
List<GeographyPointValue> expectedRing = expectedRingIt.next();
// check if number of the vertices in loops are equal
if (expectedRing.size() != actualRing.size()) {
return false;
};
Iterator<GeographyPointValue> expectedVertexIt = expectedRing.iterator();
for (GeographyPointValue actualPt : actualRing) {
GeographyPointValue expectedPt = expectedVertexIt.next();
if (!expectedPt.equals(actualPt)) {
return false;
}
}
}
return true;
}
/* Serialization format for polygons.
*
* This is the format used by S2 in the EE. Most of the
* metadata (especially lat/lng rect bounding boxes) are
* ignored here in Java.
*
* 1 byte encoding version
* 1 byte boolean owns_loops
* 1 byte boolean has_holes
* 4 bytes number of loops
* And then for each loop:
* 1 byte encoding version
* 4 bytes number of vertices
* ((number of vertices) * sizeof(double) * 3) bytes vertices as XYZPoints
* 1 byte boolean origin_inside
* 4 bytes depth (nesting level of loop)
* 33 bytes bounding box
* 33 bytes bounding box
*
* We use S2 in the EE for all geometric computation, so polygons sent to
* the EE will be missing bounding box and other info. We indicate this
* by passing INCOMPLETE_ENCODING_FROM_JAVA in the version field. This
* tells the EE to compute bounding boxes and other metadata before storing
* the polygon to memory.
*/
private static final byte INCOMPLETE_ENCODING_FROM_JAVA = 0;
private static final byte COMPLETE_ENCODING = 1;
private static long polygonOverheadInBytes() {
return 7 + boundLengthInBytes();
}
/**
* Return the number of bytes in the serialization for this polygon.
* Returned value does not include the 4-byte length prefix that precedes variable-length types.
* @return The number of bytes in the serialization for this polygon.
*/
public int getLengthInBytes() {
long length = polygonOverheadInBytes();
for (List<XYZPoint> loop : m_loops) {
length += loopLengthInBytes(loop.size());
}
return (int)length;
}
/**
* Given a column of type GEOGRAPHY(nbytes), return an upper bound on the
* number of characters needed to represent any entity of this type in WKT.
* @param numBytes The size of the GEOGRAPHY value in bytes
* @return Upper bound of characters needed for WKT string
*/
public static int getValueDisplaySize(int numBytes) {
if (numBytes < MIN_SERIALIZED_LENGTH) {
throw new IllegalArgumentException("Cannot compute max display size for a GEOGRAPHY value of size "
+ numBytes + " bytes, since minimum allowed size is " + MIN_SERIALIZED_LENGTH);
}
// Vertices will dominate the WKT output, so compute the maximum
// number of vertices given the number of bytes. This will be a polygon
// with just one loop.
int numBytesUsedForVertices = numBytes;
numBytesUsedForVertices -= polygonOverheadInBytes();
numBytesUsedForVertices -= loopOverheadInBytes();
int numVertices = numBytesUsedForVertices / 24;
// display size will be
// "POLYGON (())" [12 bytes]
// plus the number of bytes used by vertices:
// "-180.123456789012 -90.123456789012, " [max of 36 bytes per vertex]
return 12 + 36 * numVertices;
}
/**
* Serialize this object to a ByteBuffer.
* (Assumes that the 4-byte length prefix for variable-length data
* has already been serialized.)
*
* @param buf The ByteBuffer into which the serialization will be placed.
*/
public void flattenToBuffer(ByteBuffer buf) {
buf.put(INCOMPLETE_ENCODING_FROM_JAVA); // encoding version
buf.put((byte)1); // owns_loops_
buf.put((byte)(m_loops.size() > 1 ? 1 : 0)); // has_holes_
buf.putInt(m_loops.size());
for (List<XYZPoint> loop : m_loops) {
flattenLoopToBuffer(loop, buf);
}
flattenEmptyBoundToBuffer(buf);
}
/**
* Deserialize a GeographyValue from a ByteBuffer from an absolute offset.
* (Assumes that the 4-byte length prefix has already been deserialized, and that
* offset points to the start of data just after the prefix.)
* @param inBuffer The ByteBuffer from which to read a GeographyValue
* @param offset The absolute offset in the ByteBuffer from which to read data
* @return A new GeographyValue instance.
*/
public static GeographyValue unflattenFromBuffer(ByteBuffer inBuffer, int offset) {
int origPos = inBuffer.position();
inBuffer.position(offset);
GeographyValue gv = unflattenFromBuffer(inBuffer);
inBuffer.position(origPos);
return gv;
}
/**
* Deserialize a GeographyValue from a ByteBuffer at the ByteBuffer's
* current position.
* (Assumes that the 4-byte length prefix has already been deserialized.)
* @param inBuffer The ByteBuffer from which to read a GeographyValue
* @return A new GeographyValue instance.
*/
public static GeographyValue unflattenFromBuffer(ByteBuffer inBuffer) {
byte version = inBuffer.get(); // encoding version
inBuffer.get(); // owns loops
inBuffer.get(); // has holes
int numLoops = inBuffer.getInt();
List<List<XYZPoint>> loops = new ArrayList<List<XYZPoint>>();
int indexOfOuterRing = 0;
for (int i = 0; i < numLoops; ++i) {
List<XYZPoint> loop = new ArrayList<XYZPoint>();
int depth = unflattenLoopFromBuffer(inBuffer, loop);
if (depth == 0) {
indexOfOuterRing = i;
}
loops.add(loop);
}
// S2 will order loops in depth-first order, which will leave the outer ring last.
// Make it first so it looks right when converted back to WKT.
if (version == COMPLETE_ENCODING && indexOfOuterRing != 0) {
List<XYZPoint> outerRing = loops.get(indexOfOuterRing);
loops.set(indexOfOuterRing, loops.get(0));
loops.set(0, outerRing);
}
unflattenBoundFromBuffer(inBuffer);
return polygonFromXyzPoints(loops);
}
/**
* Google's S2 geometry library uses (x, y, z) representation of polygon vertices,
* But the interface we expose to users is (lat, lng). This class is the
* internal representation for vertices.
*/
static class XYZPoint {
private final double m_x;
private final double m_y;
private final double m_z;
public static XYZPoint fromGeographyPointValue(GeographyPointValue pt) {
double latRadians = pt.getLatitude() * (Math.PI / 180); // AKA phi
double lngRadians = pt.getLongitude() * (Math.PI / 180); // AKA theta
double cosPhi = Math.cos(latRadians);
double x = Math.cos(lngRadians) * cosPhi;
double y = Math.sin(lngRadians) * cosPhi;
double z = Math.sin(latRadians);
return new XYZPoint(x, y, z);
}
public XYZPoint(double x, double y, double z) {
m_x = x;
m_y = y;
m_z = z;
}
public double x() {
return m_x;
}
public double y() {
return m_y;
}
public double z() {
return m_z;
}
public GeographyPointValue toGeographyPointValue() {
double latRadians = Math.atan2(m_z, Math.sqrt(m_x * m_x + m_y * m_y));
double lngRadians = Math.atan2(m_y, m_x);
double latDegrees = latRadians * (180 / Math.PI);
double lngDegrees = lngRadians * (180 / Math.PI);
return new GeographyPointValue(lngDegrees, latDegrees);
}
@Override
public boolean equals(Object other) {
if (!(other instanceof XYZPoint)) {
return false;
}
XYZPoint compareTo = (XYZPoint) other;
if(m_x == compareTo.x() && m_y == compareTo.y() && m_z == compareTo.z()) {
return true;
}
return false;
}
@Override
public String toString() {
return toGeographyPointValue().toString();
}
}
private GeographyValue() {
m_loops = null;
}
static private GeographyValue polygonFromXyzPoints(List<List<XYZPoint>> loops) {
if (loops == null || loops.size() < 1) {
throw new IllegalArgumentException("GeographyValue must be instantiated with at least one loop");
}
GeographyValue geog = new GeographyValue();
geog.m_loops = loops;
return geog;
}
private static long boundLengthInBytes() {
// 1 byte for encoding version
// 32 bytes for lat min, lat max, lng min, lng max as doubles
return 33;
}
private static long loopOverheadInBytes() {
// 1 byte for encoding version
// 4 bytes for number of vertices
// number of vertices * 8 * 3 bytes for vertices as XYZPoints
// 1 byte for origin_inside_
// 4 bytes for depth_
// length of bound
return 10 + boundLengthInBytes();
}
private static long loopLengthInBytes(long numberOfVertices) {
return loopOverheadInBytes() + (numberOfVertices * 24);
}
private static void flattenEmptyBoundToBuffer(ByteBuffer buf) {
buf.put(INCOMPLETE_ENCODING_FROM_JAVA); // for encoding version
buf.putDouble(GeographyPointValue.NULL_COORD);
buf.putDouble(GeographyPointValue.NULL_COORD);
buf.putDouble(GeographyPointValue.NULL_COORD);
buf.putDouble(GeographyPointValue.NULL_COORD);
}
private static void flattenLoopToBuffer(List<XYZPoint> loop, ByteBuffer buf) {
// 1 byte for encoding version
// 4 bytes for number of vertices
// number of vertices * 8 * 3 bytes for vertices as XYZPoints
// 1 byte for origin_inside_
// 4 bytes for depth_
// length of bound
buf.put(INCOMPLETE_ENCODING_FROM_JAVA);
buf.putInt(loop.size());
for (XYZPoint xyz : loop) {
buf.putDouble(xyz.x());
buf.putDouble(xyz.y());
buf.putDouble(xyz.z());
}
buf.put((byte)0); // origin_inside_
buf.putInt(0); // depth_
flattenEmptyBoundToBuffer(buf);
}
private static void unflattenBoundFromBuffer(ByteBuffer inBuffer) {
inBuffer.get(); // for encoding version
inBuffer.getDouble();
inBuffer.getDouble();
inBuffer.getDouble();
inBuffer.getDouble();
}
private static int unflattenLoopFromBuffer(ByteBuffer inBuffer, List<XYZPoint> loop) {
// 1 byte for encoding version
// 4 bytes for number of vertices
// number of vertices * 8 * 3 bytes for vertices as XYZPoints
// 1 byte for origin_inside_
// 4 bytes for depth_
// length of bound
inBuffer.get(); // encoding version
int numVertices = inBuffer.getInt();
for (int i = 0; i < numVertices; ++i) {
double x = inBuffer.getDouble();
double y = inBuffer.getDouble();
double z = inBuffer.getDouble();
loop.add(new XYZPoint(x, y, z));
}
inBuffer.get(); // origin_inside_
int depth = inBuffer.getInt(); // depth
unflattenBoundFromBuffer(inBuffer);
return depth;
}
/**
* A helper function to validate the loop structure
* If loop is invalid, it generates IllegalArgumentException exception
*/
private static <T> void diagnoseLoop(List<T> loop, String excpMsgPrf) throws IllegalArgumentException {
if (loop == null) {
throw new IllegalArgumentException(excpMsgPrf + "a polygon must contain at least one ring " +
"(with each ring at least 4 points, including repeated closing vertex)");
}
// 4 vertices = 3 unique vertices for polygon + 1 end point which is same as start point
if (loop.size() < 4) {
throw new IllegalArgumentException(excpMsgPrf + "a polygon ring must contain at least 4 points " +
"(including repeated closing vertex)");
}
// check if the end points of the loop are equal
if (loop.get(0).equals(loop.get(loop.size() - 1)) == false) {
throw new IllegalArgumentException(excpMsgPrf
+ "closing points of ring are not equal: \""
+ loop.get(0).toString()
+ "\" != \""
+ loop.get(loop.size()-1).toString()
+ "\"");
}
}
/**
* A helper method to parse WKT and produce a list of polygon loops.
* Anything more complicated than this and we probably want a dedicated parser.
*
* Note that we assume that the vertices of the first loop are in counter-clockwise
* order, and that subsequent loops are in clockwise order. This is the OGC format's
* definition. When we send these to the EE we need to put them all into counter-clockwise
* order. So, we need to reverse the order of all but the first loop.
*/
private static List<List<XYZPoint>> loopsFromWkt(String wkt) throws IllegalArgumentException {
final String msgPrefix = "Improperly formatted WKT for polygon: ";
StreamTokenizer tokenizer = new StreamTokenizer(new StringReader(wkt));
tokenizer.lowerCaseMode(true);
tokenizer.eolIsSignificant(false);
List<XYZPoint> currentLoop = null;
List<List<XYZPoint>> loops = new ArrayList<List<XYZPoint>>();
boolean is_shell = true;
try {
int token = tokenizer.nextToken();
if (token != StreamTokenizer.TT_WORD
|| ! tokenizer.sval.equals("polygon")) {
throw new IllegalArgumentException(msgPrefix + "expected WKT to start with POLYGON");
}
token = tokenizer.nextToken();
if (token != '(') {
throw new IllegalArgumentException(msgPrefix + "expected left parenthesis after POLYGON");
}
boolean polygonOpen = true;
while (polygonOpen) {
token = tokenizer.nextToken();
switch (token) {
case '(':
if (currentLoop != null) {
throw new IllegalArgumentException(msgPrefix + "missing closing parenthesis");
}
currentLoop = new ArrayList<XYZPoint>();
break;
case StreamTokenizer.TT_NUMBER:
if (currentLoop == null) {
throw new IllegalArgumentException(msgPrefix + "missing opening parenthesis");
}
double lng = tokenizer.nval;
token = tokenizer.nextToken();
if (token != StreamTokenizer.TT_NUMBER) {
throw new IllegalArgumentException(msgPrefix + "missing latitude in long lat pair");
}
double lat = tokenizer.nval;
currentLoop.add(XYZPoint.fromGeographyPointValue(new GeographyPointValue(lng, lat)));
token = tokenizer.nextToken();
if (token != ',') {
if (token != ')') {
throw new IllegalArgumentException(msgPrefix + "missing comma between long lat pairs");
}
tokenizer.pushBack();
}
break;
case ')':
// perform basic validation of loop
diagnoseLoop(currentLoop, msgPrefix);
// Following the OGC standard, the first loop should be CCW, and subsequent loops
// should be CW. But we will be building the S2 polygon here,
// and S2 wants everything to be CCW. So, we need to
// reverse all but the first loop.
//
// Note also that we don't want to touch the vertex at index 0, and we want
// to remove the vertex at index currentLoop.size() - 1. We want to hold the first
// vertex invariant. The vertex at currentLoop.size() - 1 should be a duplicate
// of the vertex at index 0, and should be removed before pushing it into the
// list of loops.
//
// We are also allowed to swap these out, because they have been
// created and are owned by us.
//
currentLoop.remove(currentLoop.size() - 1);
if (!is_shell) {
for (int fidx = 1, lidx = currentLoop.size() - 1; fidx < lidx; ++fidx, --lidx) {
Collections.swap(currentLoop, fidx, lidx);
}
}
is_shell = false;
loops.add(currentLoop);
currentLoop = null;
token = tokenizer.nextToken();
if (token == ')') {
polygonOpen = false;
}
else if (token != ',') {
throw new IllegalArgumentException(msgPrefix + "unrecognized token in WKT: " + Character.toString((char)token));
}
break;
case StreamTokenizer.TT_EOF:
throw new IllegalArgumentException(msgPrefix + "premature end of input");
default:
throw new IllegalArgumentException(msgPrefix + "unrecognized token in WKT: " + Character.toString((char)token));
}
}
token = tokenizer.nextToken();
if (token != StreamTokenizer.TT_EOF) {
throw new IllegalArgumentException(msgPrefix + "unrecognized input after WKT");
}
} catch (IOException e) {
throw new IllegalArgumentException(msgPrefix + "error tokenizing string");
}
return loops;
}
/**
* Create a new GeographyValue which is offset from this one
* by the given point. The latitude and longitude values
* stay in range because we are using the normalizing operations
* in GeographyPointValue.
*
* @param offset The point by which to translate vertices in this
* @return The resulting GeographyValue.
*/
public GeographyValue add(GeographyPointValue offset) {
List<List<GeographyPointValue>> newLoops = new ArrayList<List<GeographyPointValue>>();
for (List<XYZPoint> oneLoop : m_loops) {
List<GeographyPointValue> loop = new ArrayList<GeographyPointValue>();
for (XYZPoint p : oneLoop) {
loop.add(p.toGeographyPointValue().add(offset));
}
loop.add(oneLoop.get(0).toGeographyPointValue().add(offset));
newLoops.add(loop);
}
return new GeographyValue(newLoops);
}
}