Java tutorial
/* 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 dk.dma.enav.model.geometry; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; import dk.dma.enav.model.dto.PositionDTO; import dk.dma.enav.model.geometry.CoordinateSystem.VincentyCalculationType; import static java.lang.Math.PI; import static java.lang.Math.abs; import static java.lang.Math.asin; import static java.lang.Math.atan2; import static java.lang.Math.cos; import static java.lang.Math.floor; import static java.lang.Math.log; import static java.lang.Math.round; import static java.lang.Math.sin; import static java.lang.Math.tan; import static java.lang.Math.toDegrees; import static java.lang.Math.toRadians; import static java.util.Objects.requireNonNull; /** * Representation of a WGS84 position and methods for calculating range and bearing between positions. */ @JsonIgnoreProperties(ignoreUnknown = true) public class Position implements Element { /** The mean radius of the earth in kilometers. */ static final double EARTH_RADIUS = 6371; /** 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) { verifyLatitude(latitude); verifyLongitude(longitude); // We want simple equals and hashCode implementation. So we make sure // that // positions are never constructed with -0.0 as latitude or longitude. this.latitude = latitude == -0.0 ? 0.0 : latitude; this.longitude = longitude == -0.0 ? 0.0 : longitude; } /** * Distance in meters */ public double distanceTo(Element other, CoordinateSystem system) { return requireNonNull(system) == CoordinateSystem.CARTESIAN ? rhumbLineDistanceTo(other) : geodesicDistanceTo(other); } /** * 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; } /** * Get great circle distance to location * * @param other * @return distance in meters */ public double geodesicDistanceTo(Element other) { if (other instanceof Position) { return CoordinateSystem.GEODETIC.distanceBetween(this, (Position) other); } return other.geodesicDistanceTo(this); } /** * Calculate final bearing for great circle route to location using Thaddeus Vincenty's</a> inverse formula. * * @param location * second location * @return bearing in degrees */ public double geodesicFinalBearingTo(Position location) { return CoordinateSystem.vincentyFormula(getLatitude(), getLongitude(), location.getLatitude(), location.getLongitude(), VincentyCalculationType.FINAL_BEARING); } /** * Calculate initial bearing for great circle route to location using Thaddeus Vincenty's</a> inverse formula. * * @param location * second location * @return bearing in degrees */ public double geodesicInitialBearingTo(Position location) { return CoordinateSystem.vincentyFormula(getLatitude(), getLongitude(), location.getLatitude(), location.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) (floor(getLatitude() / degress) * (360.0 / degress)) + (long) ((360.0 + getLongitude()) / degress) - (long) (360L / degress); } public int getCellInt(double degrees) { // bigger cellsize than 0.01 cannot be supported. unless we change the cellsize to long if (degrees < 0.01) { throw new IllegalArgumentException("degrees = " + degrees); } return (int) getCell(degrees); } /** * 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) 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) round(1000 * (lon - (int) lon)))); longitudeAsString.append(longitude < 0 ? "W" : "E"); return longitudeAsString.toString(); } /** * Format the given integer value as a String of length 3 with leading zeros. * @param value * @return */ private static String format000(int value) { if (value < 10) { return "00" + value; } else if (value < 100) { return "0" + value; } return Integer.toString(value); } /** * Format the given integer value as a String of length 2 with leading zeros. * @param value * @return */ private static String format00(int value) { if (value < 10) { return "0" + value; } return Integer.toString(value); } /** * 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); } /** * 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 = toRadians(latitude); double lat2 = toRadians(position.latitude); double dPhi = log(tan(lat2 / 2 + PI / 4) / tan(lat1 / 2 + PI / 4)); double dLon = toRadians(position.longitude - longitude); if (abs(dLon) > PI) { dLon = dLon > 0 ? -(2 * PI - dLon) : 2 * PI + dLon; } double brng = atan2(dLon, dPhi); return (toDegrees(brng) + 360) % 360; } public double rhumbLineDistanceTo(Element other) { if (other instanceof Position) { return CoordinateSystem.CARTESIAN.distanceBetween(this, (Position) other); } return other.rhumbLineDistanceTo(this); } /** * Calculates the position following a rhumb line with the given bearing for the specified distance. * * @param bearing * the bearing (in compass degrees) * @param distance * the distance (in meters) * @return the position */ public Position positionAt(double bearing, double distance) { final double d = distance / (EARTH_RADIUS * 1e3); final double bearingRad = toRadians(bearing); final double lat1Rad = toRadians(this.latitude); final double lon1Rad = toRadians(this.longitude); final double lat2Rad = asin(sin(lat1Rad) * cos(d) + cos(lat1Rad) * sin(d) * cos(bearingRad)); final double a = atan2(sin(bearingRad) * sin(d) * cos(lat1Rad), cos(d) - sin(lat1Rad) * sin(lat2Rad)); final double lon2Rad = (lon1Rad + a + 3 * PI) % (2 * PI) - PI; final double lat2 = toDegrees(lat2Rad); final double lon2 = toDegrees(lon2Rad); return create(lat2, lon2); } /** * 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 "(" + getLatitudeAsString() + ", " + getLongitudeAsString() + ")"; } public Position withLatitude(double latitude) { return new Position(latitude, longitude); } // we lose some pression public Position withLongitude(double longitude) { return new Position(latitude, longitude); } public PositionTime withTime(long time) { return PositionTime.create(this, time); } /** * 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 */ @JsonCreator public static Position create(@JsonProperty("latitude") double latitude, @JsonProperty("longitude") double longitude) { return new Position(latitude, longitude); } public static Position fromPackedLong(long l) { return new Position(Float.intBitsToFloat((int) (l >> 32)), Float.intBitsToFloat((int) l)); } public static boolean isValid(double latitude, double longitude) { return latitude <= 90 && latitude >= -90 && longitude <= 180 && longitude >= -180; } /** * Verify that latitude is within the interval [-90:90]. * * @param latitude * @throws IllegalArgumentException * When latitude is invalid */ public static void verifyLatitude(double latitude) { if (latitude > 90 || latitude < -90) { throw new IllegalArgumentException("Illegal latitude must be between -90 and 90, was " + latitude); } } /** * Verify that longitude is within the interval [-180:180]. * * @param longitude * @throws IllegalArgumentException * When longitude is invalid */ public static void verifyLongitude(double longitude) { if (longitude > 180 || longitude < -180) { throw new IllegalArgumentException("Longitude must be between -180 and 180, was " + longitude); } } public PositionDTO getDTO() { return new PositionDTO(this.latitude, this.longitude); } }