/* Copyright (c) 2011 Danish Maritime Authority.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.maritimecloud.util.geometry;
import java.io.IOException;
import java.io.Serializable;
import java.util.Random;
import java.util.concurrent.ThreadLocalRandom;
import net.maritimecloud.internal.message.BinaryUtil;
import net.maritimecloud.message.Message;
import net.maritimecloud.message.MessageReader;
import net.maritimecloud.message.MessageSerializer;
import net.maritimecloud.message.MessageWriter;
import net.maritimecloud.util.Binary;
import net.maritimecloud.util.geometry.CoordinateSystem.VincentyCalculationType;
/**
* Representation of a WGS84 position and methods for calculating range and bearing between positions.
*/
public class Position implements Message, Serializable {
static final double POS_INT_SCALE = 10_000_000d;
public static final MessageSerializer<Position> SERIALIZER = new MessageSerializer<Position>() {
/** {@inheritDoc} */
@Override
public Position read(MessageReader reader) throws IOException {
double lat = reader.readDouble(1, "latitude");
double lon = reader.readDouble(2, "longitude");
return Position.create(lat, lon);
}
public void write(Position message, MessageWriter writer) throws IOException {
writer.writeDouble(1, "latitude", message.latitude);
writer.writeDouble(2, "longitude", message.longitude);
}
};
//
// static Position readFrom(MessageReader r) throws IOException {
// // if (r.isCompact()) {
// // int lat = r.readInt32(1, "latitude");
// // int lon = r.readInt32(2, "longitude");
// // return Position.create(lat / 10_000_000d, lon / 10_000_000d);
// // } else {
// double lat = r.readDouble(1, "latitude");
// double lon = r.readDouble(2, "longitude");
// return Position.create(lat, lon);
// // }
// }
//
// static Position readFromPacked(MessageReader r, int latId, String latName, int lonId, String lonName)
// throws IOException {
// int lat = r.readInt(latId, latName);
// int lon = r.readInt(lonId, lonName);
// return Position.create(lat / 10_000_000d, lon / 10_000_000d);
// }
/** serialVersionUID. */
private static final long serialVersionUID = 1L;
/** The latitude part of the position. */
final double latitude;
/** The longitude part of the position. */
final double longitude;
/**
* Constructor given position and timezone
*
* @param latitude
* Negative south of equator
* @param longitude
* Negative east of Prime Meridian
*/
Position(double latitude, double longitude) {
this.latitude = verifyLatitude(latitude);
this.longitude = verifyLongitude(longitude);
}
/**
* Equals method
*/
@Override
public boolean equals(Object other) {
return other instanceof Position && equals((Position) other);
}
// We probably want another function that also takes a precision.
public boolean equals(Position other) {
// id longitude 180 == - 180???
return other == this || other != null && latitude == other.latitude && longitude == other.longitude;
}
/**
* Returns the great circle distance to the specified position.
*
* @param other
* the position to calculate the distance from
* @return distance in meters
*/
public double geodesicDistanceTo(Position other) {
return CoordinateSystem.GEODETIC.distanceBetween(this, other);
}
/**
* Calculate final bearing for great circle route to location using Thaddeus Vincenty's inverse formula.
*
* @param other
* the position to calculate the bearing from
* @return bearing in degrees
*/
public double geodesicFinalBearingTo(Position other) {
return CoordinateSystem.vincentyFormula(getLatitude(), getLongitude(), other.getLatitude(),
other.getLongitude(), VincentyCalculationType.FINAL_BEARING);
}
/**
* Calculate initial bearing for great circle route to location using Thaddeus Vincenty's inverse formula.
*
* @param other
* the position to calculate the bearing from
* @return bearing in degrees
*/
public double geodesicInitialBearingTo(Position other) {
return CoordinateSystem.vincentyFormula(getLatitude(), getLongitude(), other.getLatitude(),
other.getLongitude(), VincentyCalculationType.INITIAL_BEARING);
}
public long getCell(double degress) {
if (degress < 0.0001) {
throw new IllegalArgumentException("degress = " + degress);
} else if (degress > 100) {
throw new IllegalArgumentException("degress = " + degress);
}
return (long) (Math.floor(getLatitude() / degress) * (360.0 / degress))
+ (long) ((360.0 + getLongitude()) / degress) - (long) (360L / degress);
}
public int getCellInt(double degress) {
// bigger cellsize than 0.01 cannot be supported. unless we change the cellsize to long
if (degress < 0.01) {
throw new IllegalArgumentException("degress = " + degress);
}
return (int) getCell(degress);
}
/**
* Returns the latitude part of this position.
*
* @return the latitude part of this position
*/
public double getLatitude() {
return latitude;
}
public String getLatitudeAsString() {
double lat = latitude;
if (lat < 0) {
lat *= -1;
}
int hours = (int) lat;
lat -= hours;
lat *= 60;
StringBuilder latitudeAsString = new StringBuilder(16);
latitudeAsString.append(format00(hours));
latitudeAsString.append(" ");
latitudeAsString.append(format00((int) lat));
latitudeAsString.append(".");
latitudeAsString.append(format000((int) Math.round(1000 * (lat - (int) lat))));
latitudeAsString.append(latitude < 0 ? "S" : "N");
return latitudeAsString.toString();
}
/**
* Returns the longitude part of this position.
*
* @return the longitude part of this position
*/
public double getLongitude() {
return longitude;
}
public String getLongitudeAsString() {
double lon = longitude;
if (lon < 0) {
lon *= -1;
}
int hours = (int) lon;
lon -= hours;
lon *= 60;
StringBuilder longitudeAsString = new StringBuilder(16);
longitudeAsString.append(format000(hours));
longitudeAsString.append(" ");
longitudeAsString.append(format00((int) lon));
longitudeAsString.append(".");
longitudeAsString.append(format000((int) Math.round(1000 * (lon - (int) lon))));
longitudeAsString.append(longitude < 0 ? "W" : "E");
return longitudeAsString.toString();
}
/**
* Hash code for the location
*/
@Override
public int hashCode() {
// If we need to use this as a key somewhere we can use the same hash
// code technique as java.lang.String
long latLong = Double.doubleToLongBits(latitude);
long lonLong = Double.doubleToLongBits(longitude);
return (int) (latLong ^ latLong >>> 32) ^ (int) (lonLong ^ lonLong >>> 32);
}
/** {@inheritDoc} */
@Override
public Position immutable() {
return this;
}
/**
* Calculates the rhumb line bearing to the specified position
*
* @param position
* the position
* @return the rhumb line bearing in degrees
*/
public double rhumbLineBearingTo(Position position) {
double lat1 = Math.toRadians(latitude);
double lat2 = Math.toRadians(position.latitude);
double dPhi = Math.log(Math.tan(lat2 / 2 + Math.PI / 4) / Math.tan(lat1 / 2 + Math.PI / 4));
double dLon = Math.toRadians(position.longitude - longitude);
if (Math.abs(dLon) > Math.PI) {
dLon = dLon > 0 ? -(2 * Math.PI - dLon) : 2 * Math.PI + dLon;
}
double brng = Math.atan2(dLon, dPhi);
return (Math.toDegrees(brng) + 360) % 360;
}
public double rhumbLineDistanceTo(Position other) {
return CoordinateSystem.CARTESIAN.distanceBetween(this, other);
}
/** Returns a JSON representation of this message */
public String toJSON() {
return MessageSerializer.writeToJSON(this, SERIALIZER);
}
/**
* Packs the position into a long (losing some precision). Can be read later by {@link #fromPackedLong(long)}
*
* @return the packet long
*/
public long toPackedLong() {
float lat = (float) getLatitude();
float lon = (float) getLongitude();
return ((long) Float.floatToRawIntBits(lat) << 32) + Float.floatToRawIntBits(lon);
}
@Override
public String toString() {
return "(" + getLatitude() + ", " + getLongitude() + ")";
}
public String toStringDegrees() {
return "(" + getLatitudeAsString() + ", " + getLongitudeAsString() + ")";
}
/**
* Returns a new position with the same longitude as this position but with the specified latitude.
*
* @param latitude
* the new latitude for the new position
* @return the new position
*/
public Position withLatitude(double latitude) {
return new Position(latitude, longitude);
}
/**
* Returns a new position with the same latitude as this position but with the specified longitude.
*
* @param longitude
* the new longitude for the new position
* @return the new position
*/
public Position withLongitude(double longitude) {
return new Position(latitude, longitude);
}
/**
* Returns a new position time with this position added with the current time.
*
* @param time
* the time of the new position time object
* @return the new position time object
*/
public PositionTime withTime(long time) {
return PositionTime.create(this, time);
}
void writeToPacked(MessageWriter w, int latId, String latName, int lonId, String lonName) throws IOException {
w.writeInt(latId, latName, getLatitudeAsInt());
w.writeInt(lonId, lonName, getLongitudeAsInt());
}
int getLatitudeAsInt() {
return (int) (latitude * POS_INT_SCALE);
}
int getLongitudeAsInt() {
return (int) (longitude * POS_INT_SCALE);
}
/**
* Creates a new position from the specified latitude and longitude.
*
* @param latitude
* the latitude
* @param longitude
* the longitude
* @return the new position
* @throws IllegalArgumentException
* if the
*/
public static Position create(double latitude, double longitude) {
return new Position(latitude, longitude);
}
/**
* Format the given integer value as a String of length 2 with leading zeros.
*
* @param value
* the value to format
* @return the formatted string
*/
private static String format00(int value) {
if (value < 10) {
return "0" + value;
}
return Integer.toString(value);
}
/**
* Returns a position from a 64 bit byte array encoded as decimal degress with 7 decimal places.
*
* @return the position from a binary array
*/
public static Position fromBinary(Binary b) {
byte[] bytes = b.toByteArray();
Position pos = Position.create(
BinaryUtil.readInt(bytes, 0) / POS_INT_SCALE,
BinaryUtil.readInt(bytes, 4) / POS_INT_SCALE
);
return pos;
}
/**
* Returns a 64 bit representation of this position. Encoded as decimal degress with 7 decimal places.
*
* @return the position as a binary
*/
public Binary toBinary() {
byte[] b = new byte[4 + 4];
BinaryUtil.writeInt(getLatitudeAsInt(), b, 0);
BinaryUtil.writeInt(getLongitudeAsInt(), b, 4);
return Binary.copyFrom(b);
}
/**
* Format the given integer value as a String of length 3 with leading zeros.
*
* @param value
* the value to format
* @return the formatted string
*/
private static String format000(int value) {
if (value < 10) {
return "00" + value;
} else if (value < 100) {
return "0" + value;
}
return Integer.toString(value);
}
public static Position fromPackedLong(long l) {
return new Position(Float.intBitsToFloat((int) (l >> 32)), Float.intBitsToFloat((int) l));
}
/**
* @param latitude
* the latitude
* @param longitude
* the longitude
* @return whether or not the specified latitude and longitude is valid
*/
public static boolean isValid(double latitude, double longitude) {
return latitude <= 90 && latitude >= -90 && longitude <= 180 && longitude >= -180;
}
/**
* Returns a random valid position.
*
* @return the random position
*/
public static Position random() {
return random(ThreadLocalRandom.current());
}
/**
* Returns a random valid position.
*
* @param rnd
* the source of randomness
* @return the random position
*/
public static Position random(Random rnd) {
return Position.create(rnd.nextDouble() * 180 - 90, rnd.nextDouble() * 360 - 180);
}
/**
* Verify that latitude is within the interval [-90:90].
*
* @param latitude
* the latitude to verify
* @throws IllegalArgumentException
* When latitude is invalid
* @return the specified latitude
*/
public static double verifyLatitude(double latitude) {
if (latitude > 90 || latitude < -90) {
throw new IllegalArgumentException("Illegal latitude must be between -90 and 90, was " + latitude);
}
// We want simple equals and hashCode implementation. So we make sure
// that positions are never constructed with -0.0 as latitude or longitude.
return latitude == -0.0 ? 0.0 : latitude;
}
/**
* Verify that longitude is within the interval [-180:180].
*
* @param longitude
* the longitude to verify
* @throws IllegalArgumentException
* When longitude is invalid
* @return the specified longitude
*/
public static double verifyLongitude(double longitude) {
if (longitude > 180 || longitude < -180) {
throw new IllegalArgumentException("Longitude must be between -180 and 180, was " + longitude);
}
// We want simple equals and hashCode implementation. So we make sure
// that positions are never constructed with -0.0 as latitude or longitude.
return longitude == -0.0 ? 0.0 : longitude;
}
}
//
// public static Position createFromPacked(int latitude, int longitude) {
// return new Position(latitude / 10_000_000, longitude / 10_000_000);
// }