/* 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.nio.ByteBuffer;
import java.text.DecimalFormat;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* The Java class that corresponds to the SQL type GEOGRAPHY_POINT.
* Represents a point as defined by its longitude and latitude.
*/
public class GeographyPointValue {
//
// It's slightly hard to see this in the actual pattern
// definition, but the pattern we want to match, ignoring space, is:
// 1. Some optional space.
// 2. The word "point", case insensitive.
// 3. Some optional space.
// 4. A left parenthesis.
// 5. Some optional space.
// 6. A coordinate, consisting of
// 6.1. An optional sign.
// 6.2. An integer part, consisting of a non-empty sequence of digits.
// 6.3. An optional fractional part, consisting of a
// dot followed by a non-empty sequence of digits.
// 7. Some required space.
// 8. A second coordinate, just like (6) above
// 9. A right parenthesis.
// 10. Some optional space.
// 11. The end of the string.
//
private static final Pattern wktPattern
= Pattern.compile("^\\s*point\\s*[(]\\s*([-]?\\d+)(?:[.](\\d*))?\\s+([-]?\\d+)(?:[.](\\d*))?\\s*[)]\\s*\\z",
Pattern.CASE_INSENSITIVE);
private final double m_latitude;
private final double m_longitude;
private static final int BYTES_IN_A_COORD = Double.SIZE / 8;
static final double EPSILON = 1.0e-12;
// We use this value to represent a null point.
// (Only for sending data over the wire. The client
// should receive a null when retrieving null points from a
// VoltTable.)
static final double NULL_COORD = 360.0;
/**
* Construct a new GeographyPointValue from its coordinates.
* @param longitude in degrees.
* @param latitude in degrees.
*/
public GeographyPointValue(double longitude, double latitude) {
// Add 0.0 to avoid -0.0.
m_latitude = latitude + 0.0;
m_longitude = longitude + 0.0;
if (m_latitude < -90.0 || m_latitude > 90.0) {
throw new IllegalArgumentException("Latitude out of range in GeographyPointValue constructor");
}
if (m_longitude < -180.0 || m_longitude > 180.0) {
throw new IllegalArgumentException("Longitude out of range in GeographyPointValue constructor");
}
}
private static double toDouble(String aInt, String aFrac) {
return Double.parseDouble(aInt + "." + (aFrac == null ? "0" : aFrac));
}
/**
* Create a GeographyPointValue from a well-known text string.
* @param param A well-known text string.
* @return A new instance of GeographyPointValue.
*/
public static GeographyPointValue fromWKT(String param) {
if (param == null) {
throw new IllegalArgumentException("Null well known text argument to GeographyPointValue constructor.");
}
Matcher m = wktPattern.matcher(param);
if (m.find()) {
// Add 0.0 to avoid -0.0.
double longitude = toDouble(m.group(1), m.group(2)) + 0.0;
double latitude = toDouble(m.group(3), m.group(4)) + 0.0;
if (Math.abs(latitude) > 90.0) {
throw new IllegalArgumentException(String.format("Latitude \"%f\" out of bounds.", latitude));
}
if (Math.abs(longitude) > 180.0) {
throw new IllegalArgumentException(String.format("Longitude \"%f\" out of bounds.", longitude));
}
return new GeographyPointValue(longitude, latitude);
} else {
throw new IllegalArgumentException("Cannot construct GeographyPointValue value from \"" + param + "\"");
}
}
/**
* Return the latitude of this point in degrees.
* @return The latitude of this point in degrees.
*/
public double getLatitude() {
return m_latitude;
}
/**
* Return the longitude of this point in degrees.
* @return The longitude of this point in degrees.
*/
public double getLongitude() {
return m_longitude;
}
/**
* Format the coordinates for this point. Use 12 digits of precision after the
* decimal point.
* @return A string containing the longitude and latitude for this point
* separated by one space.
*/
String formatLngLat() {
DecimalFormat df = new DecimalFormat("##0.0###########");
// Explicitly test for differences less than 1.0e-12 and
// force them to be zero. Otherwise you may find a case
// where two points differ in the less significant bits, but
// they format as the same number.
double lng = (Math.abs(m_longitude) < EPSILON) ? 0 : m_longitude;
double lat = (Math.abs(m_latitude) < EPSILON) ? 0 : m_latitude;
return df.format(lng) + " " + df.format(lat);
}
/**
* Return this point as a well-known text string.
* @return This point as a well-known text string.
*/
@Override
public String toString() {
return toWKT();
}
/**
* Return this point as a well-known text string.
* @return This point as a well-known text string.
*/
public String toWKT() {
// This is not GEOGRAPHY_POINT. This is wkt syntax.
return "POINT (" + formatLngLat() + ")";
}
/**
* The largest number of characters needed to represent
* a point value as a string.
* @return number of characters needed for display
*/
public static int getValueDisplaySize() {
return 7 // "POINT ("
+ 1 + 3 + 1 + 12 // lng: sign, whole part, point, fraction digits
+ 1 // space between coordinates
+ 1 + 2 + 1 + 12 // lat: sign, whole part, point fraction digits
+ 1; // ")"
}
/**
* Compare this point with another object. Returns true
* if this point is being compared to another point that
* represents the same location.
* @see java.lang.Object#equals(java.lang.Object)
*/
@Override
public boolean equals(Object o) {
if (!(o instanceof GeographyPointValue)) {
return false;
}
GeographyPointValue that = (GeographyPointValue)o;
if (this == that) {
return true;
}
GeographyPointValue normThis = normalizeLngLat(getLongitude(), getLatitude());
GeographyPointValue normThat = normalizeLngLat(that.getLongitude(), that.getLatitude());
if (Math.abs(normThis.getLongitude() - normThat.getLongitude()) > EPSILON) {
return false;
}
return Math.abs(normThis.getLatitude() - normThat.getLatitude()) < EPSILON;
}
/**
* Returns the number of bytes an instance of this class requires when serialized
* to a ByteBuffer.
* @return The number of bytes an instance of this class requires when serialized
* to a ByteBuffer.
*/
static public int getLengthInBytes() {
return BYTES_IN_A_COORD * 2;
}
/**
* Serialize this point to a ByteBuffer.
* @param buffer The ByteBuffer to which this point will be serialized.
*/
public void flattenToBuffer(ByteBuffer buffer) {
buffer.putDouble(getLongitude());
buffer.putDouble(getLatitude());
}
/**
* Deserializes a point from a ByteBuffer, at an absolute offset.
* @param inBuffer The ByteBuffer from which to read the bytes for a point.
* @param offset Absolute offset of point data in buffer.
* @return A new instance of GeographyPointValue.
*/
public static GeographyPointValue unflattenFromBuffer(ByteBuffer inBuffer, int offset) {
double lng = inBuffer.getDouble(offset);
double lat = inBuffer.getDouble(offset + BYTES_IN_A_COORD);
if (lat == 360.0 && lng == 360.0) {
// This is a null point.
return null;
}
return new GeographyPointValue(lng, lat);
}
/**
* Deserialize a point from a ByteBuffer at the buffer's current position
* @param inBuffer The ByteBuffer from which to read the bytes for a point.
* @return A new instance of GeographyPointValue.
*/
public static GeographyPointValue unflattenFromBuffer(ByteBuffer inBuffer) {
double lng = inBuffer.getDouble();
double lat = inBuffer.getDouble();
if (lat == 360.0 && lng == 360.0) {
// This is a null point.
return null;
}
return new GeographyPointValue(lng, lat);
}
/**
* Serialize the null point (that is, a SQL null value) to a ByteBuffer,
* at the buffer's current position.
* @param buffer The ByteBuffer to which a null point will be serialized.
*/
public static void serializeNull(ByteBuffer buffer) {
buffer.putDouble(NULL_COORD);
buffer.putDouble(NULL_COORD);
}
/**
* Create a GeographyPointValue with normalized coordinates. The longitude and
* latitude inputs may be any real numbers. They are not restricted to be in
* the ranges [-180,180] or [-90,90] respectively. The created instance will
* will have coordinates between (-180,180) and [-90,90].
*
* @param longitude in degrees, not range-restricted.
* @param latitude in degrees, not range-restricted.
* @return A GeographyPointValue with the given coordinates normalized.
*/
public static GeographyPointValue normalizeLngLat(double longitude, double latitude) {
// Now compute the latitude. We compute this in
// the range [-180, 180] and then fiddle with it.
// If it's out of the range [-90, 90], we need to
// flip it and then change the longitude.
double latNorm = normalize(latitude, 360);
double lngNorm = normalize(longitude, 360);
double latFinal = 0.0;
double lngFinal = 0.0;
assert(-180 <= latNorm && latNorm <= 180);
assert(-180 <= lngNorm && lngNorm <= 180);
// Now, latOrig is the latitude in the range [-180,180].
// Let latWant be latitude in the range [-90, 90]. Then
// o If latOrig > 90, we have latOrig + latWant = 180. That
// is, if we rotate our point starting at latOrig through
// the angle latWant, we get +180.</li>
// o If latOrig < 90, then we have latOrig - latWant = -180.
// If we rotate our point, which is the southern hemisphere,
// through the angle latWant, then we will have it at -180.</li>
//
// In either case, if we change the latitude we then need to change
// the longitude. We want to reflect the longitude across the origin.
boolean flipLng = false;
if (latNorm > 90) {
// This is latWant.
latFinal = 180 - latNorm;
flipLng = true;
} else if (latNorm < -90) {
// This is lngWant.
latFinal = -latNorm - 180;
flipLng = true;
} else {
latFinal = latNorm;
lngFinal = lngNorm;
}
if (flipLng) {
if (lngNorm <= 0) {
// Since lngOrig is in [-180,0], we must have that
// lngOrig + 180 is in the range [0, 180].
// So, we are mapping 0 to 180.
lngFinal = lngNorm + 180;
} else {
// Since lngOrig is in the range [-0, 180] we must have
// that lngOrig - 180 is in the range [-180, 0].
// So, we are mapping -0 to -180.
lngFinal = lngNorm - 180;
}
}
assert(-90 <= latFinal && latFinal <= 90);
// For latitude of 90 or -90, canonicalize longitude to
// to 0.
if (90 - Math.abs(latFinal) < EPSILON) {
// We are at one of the poles
lngFinal = 0;
}
// For longitudes within epsilon of the international dateline
// (on the east side), canonicalize to 180.0.
if (lngFinal < 0 && 180 + lngFinal < EPSILON) {
lngFinal = 180.0;
}
assert(-180 < lngFinal && lngFinal <= 180);
// Return the point.
return new GeographyPointValue(lngFinal + 0.0, latFinal + 0.0);
}
// Normalize the value v to be in range [-range, range]
// by subtracting multiples of 360.
private static double normalize(double v, double range) {
double a = v-Math.floor((v + (range/2))/range)*range;
// Make sure that a and v have the same sign
// when abs(v) = 180.
if (Math.abs(a) == 180.0 && (a * v) < 0) {
a *= -1;
}
// The addition of 0.0 is to avoid negative
// zero, which just confuses things.
return a + 0.0;
}
/**
* Return a point which is offset by the given offset point multiplied by alpha.
* That is, the return value is <code> this + alpha*offset</code>, except that
* Java does not allow one to write it in this way.
*
* In particular, <code>this - other</code> is equal to
* <code>add(other, -1)</code>, but fewer temporary objects are
* created.
*
* Normalize the coordinates.
* @param offset A point to add to this.
* @param alpha Coordinates of offset will be scaled by this much.
* @return A new point offset by the scaled offset.
*/
public GeographyPointValue add(GeographyPointValue offset, double alpha) {
// The addition of 0.0 converts
// -0.0 to 0.0.
return GeographyPointValue.normalizeLngLat(getLongitude() + alpha * offset.getLongitude() + 0.0,
getLatitude() + alpha * offset.getLatitude() + 0.0);
}
/**
* Add a point to this, and return the result.
*
* @param offset The offset to add to this.
* @return A new point which is this plus the offset.
*/
public GeographyPointValue add(GeographyPointValue offset) {
return add(offset, 1.0);
}
/**
* Subtract a given offset point from this, and return the result.
*
* @param offset The offset to subtract from this.
* @return A new point translated by -offset.
*/
public GeographyPointValue sub(GeographyPointValue offset) {
return add(offset, -1.0);
}
/**
* Subtract a point from this, and return the result. The point being
* subtracted is computed by scaling a given offset point.
*
* @param offset The offset to subtract from this.
* @param scale The amount by which to scale the offset point.
* @return A new point translated by -offset.
*/
public GeographyPointValue sub(GeographyPointValue offset, double scale) {
return add(offset, -1.0 * scale);
}
/**
* Return a point scaled by the given alpha value.
*
* @param alpha The amount by which to scale this.
* @return The scaled point.
*/
public GeographyPointValue mul(double alpha) {
return GeographyPointValue.normalizeLngLat(getLongitude() * alpha + 0.0,
getLatitude() * alpha + 0.0);
}
/**
* Return a new point which is this point rotated by the angle phi around a given center point.
*
* @param phi The angle to rotate in degrees.
* @param center The center of rotation.
* @return A new, rotated point.
*/
public GeographyPointValue rotate(double phi, GeographyPointValue center) {
double sinphi = Math.sin(2*Math.PI*phi/360.0);
double cosphi = Math.cos(2*Math.PI*phi/360.0);
// Translate to the center.
double longitude = getLongitude() - center.getLongitude();
double latitude = getLatitude() - center.getLatitude();
// Rotate and translate back.
return GeographyPointValue.normalizeLngLat((cosphi * longitude - sinphi * latitude) + center.getLongitude(),
(sinphi * longitude + cosphi * latitude) + center.getLatitude());
}
/**
* Return <code>alpha*(this - center) + center</code>. This is
* used to scale the vector from center to this as an offset by
* alpha. This is equivalent to <code>this.sub(center).mul(alpha).add(center)</code>,
* but with fewer object creations.
*
* @param center The origin of scaling.
* @param alpha The scale factor.
* @return The scaled point.
*/
public GeographyPointValue scale(GeographyPointValue center, double alpha) {
return GeographyPointValue.normalizeLngLat(alpha*(getLongitude()-center.getLongitude()) + center.getLongitude(),
alpha*(getLatitude()-center.getLatitude()) + center.getLatitude());
}
}