All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.mapcode.Point Maven / Gradle / Ivy

Go to download

This library offers Java support for mapcodes. For more info: http://mapcode.com

There is a newer version: 2.4.18
Show newest version
/*
 * Copyright (C) 2016-2020, Stichting Mapcode Foundation (http://www.mapcode.com)
 *
 * 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 com.mapcode;

import javax.annotation.Nonnull;
import java.util.Arrays;
import java.util.Random;

import static com.mapcode.CheckArgs.checkNonnull;

/**
 * This class defines a class for lat/lon points.
 *
 * Internally, the class implements a fixed-point representation where a coordinate is expressed in
 * "fractions", of 1/3.240,000,000,000th of a degree. A double (an IEEE 754-1985 binary64) is just
 * sufficient to represent coordinates between -180 and +180 degrees in such fractions.
 * However, for applications that use micro-degrees a lot, the implementation below is more efficient.
 * It represent the fractions in pairs of integers, the first integer
 * representing 1/1,000,000th of degrees, the second representing the remainder.
 */
@SuppressWarnings("MagicNumber")
public final class Point {

    // Latitude and longitude ranges.
    @SuppressWarnings("unused")
    public static final double LON_DEG_MIN = -180.0;
    @SuppressWarnings("unused")
    public static final double LON_DEG_MAX = 180.0;
    @SuppressWarnings("unused")
    public static final double LAT_DEG_MIN = -90.0;
    @SuppressWarnings("unused")
    public static final double LAT_DEG_MAX = 90.0;

    // Conversion constants.
    public static final int DEG_TO_MICRO_DEG = 1000000;
    public static final int MICRO_DEG_90 = 90 * DEG_TO_MICRO_DEG;
    public static final int MICRO_DEG_180 = 180 * DEG_TO_MICRO_DEG;
    public static final int MICRO_DEG_360 = 360 * DEG_TO_MICRO_DEG;

    // Radius of Earth.
    public static final double EARTH_RADIUS_X_METERS = 6378137.0;
    public static final double EARTH_RADIUS_Y_METERS = 6356752.3;

    // Circumference of Earth.
    public static final double EARTH_CIRCUMFERENCE_X = EARTH_RADIUS_X_METERS * 2.0 * Math.PI;
    public static final double EARTH_CIRCUMFERENCE_Y = EARTH_RADIUS_Y_METERS * 2.0 * Math.PI;

    // Meters per degree latitude is fixed. For longitude: use factor * cos(midpoint of two degree latitudes).
    public static final double METERS_PER_DEGREE_LAT = EARTH_CIRCUMFERENCE_Y / 360.0;
    public static final double METERS_PER_DEGREE_LON_EQUATOR = EARTH_CIRCUMFERENCE_X / 360.0; // * cos(deg(lat)).

    /**
     * Create a point from lat/lon in degrees (may be precision!)
     *
     * @param latDeg Latitude in degrees. Range: [-90, 90].
     * @param lonDeg Longitude in degrees. Range: [-180, 180).
     * @return A defined point.
     */
    @Nonnull
    public static Point fromDeg(final double latDeg, final double lonDeg) {
        return new Point(latDeg, lonDeg);
    }

    /**
     * Public construction, from integer microdegrees (no loss of precision).
     *
     * @param latMicroDeg Latitude, in microdegrees.
     * @param lonMicroDeg Longitude, in microdegrees.
     * @return A defined point.
     */
    @Nonnull
    public static Point fromMicroDeg(final int latMicroDeg, final int lonMicroDeg) {
        final Point p = new Point();
        p.latMicroDeg = latMicroDeg;
        p.latFractionOnlyDeg = 0;
        p.lonMicroDeg = lonMicroDeg;
        p.lonFractionOnlyDeg = 0;
        p.defined = true;
        return p.wrap();
    }

    /**
     * Get the latitude in degrees (may lose precision).
     *
     * @return Latitude in degrees. No range is enforced.
     */
    public double getLatDeg() {
        assert defined;
        return (latMicroDeg / MICRODEG_TO_DEG_FACTOR) + (latFractionOnlyDeg / LAT_TO_FRACTIONS_FACTOR);
    }

    /**
     * Get the longitude in degrees (may lose precision).
     *
     * @return Longitude in degrees. No range is enforced.
     */
    public double getLonDeg() {
        assert defined;
        return (lonMicroDeg / MICRODEG_TO_DEG_FACTOR) + (lonFractionOnlyDeg / LON_TO_FRACTIONS_FACTOR);
    }

    /**
     * Get latitude as micro-degrees. Note that this looses precision beyond microdegrees!
     *
     * @return floor(Latitude in microdegrees)
     */
    public int getLatMicroDeg() {
        assert defined;
        return latMicroDeg;
    }

    /**
     * Get longitude as micro-degrees. Note that this looses precision beyond microdegrees!
     *
     * @return floor(Longitude in microdegrees)
     */
    public int getLonMicroDeg() {
        assert defined;
        return lonMicroDeg;
    }

    /**
     * Create a random point, uniformly distributed over the surface of the Earth.
     *
     * @param randomGenerator Random generator used to create a point.
     * @return Random point with uniform distribution over the sphere.
     */
    @Nonnull
    public static Point fromUniformlyDistributedRandomPoints(@Nonnull final Random randomGenerator) {
        checkNonnull("randomGenerator", randomGenerator);

        // Calculate uniformly distributed 3D point on sphere (radius = 1.0):
        // http://mathproofs.blogspot.co.il/2005/04/uniform-random-distribution-on-sphere.html
        final double unitRand1 = randomGenerator.nextDouble();
        final double unitRand2 = randomGenerator.nextDouble();
        final double theta0 = (2.0 * Math.PI) * unitRand1;
        final double theta1 = Math.acos(1.0 - (2.0 * unitRand2));
        final double x = Math.sin(theta0) * Math.sin(theta1);
        final double y = Math.cos(theta0) * Math.sin(theta1);
        final double z = Math.cos(theta1);

        // Convert Carthesian 3D point into lat/lon (radius = 1.0):
        // http://stackoverflow.com/questions/1185408/converting-from-longitude-latitude-to-cartesian-coordinates
        final double latRad = Math.asin(z);
        final double lonRad = Math.atan2(y, x);

        // Convert radians to degrees.
        assert !Double.isNaN(latRad);
        assert !Double.isNaN(lonRad);
        final double lat = latRad * (180.0 / Math.PI);
        final double lon = lonRad * (180.0 / Math.PI);
        return fromDeg(lat, lon);
    }

    /**
     * Calculate the distance between two points. This algorithm does not take the curvature of the Earth into
     * account, so it only works for small distance up to, say 200 km, and not too close to the poles.
     *
     * @param p1 Point 1.
     * @param p2 Point 2.
     * @return Straight distance between p1 and p2. Only accurate for small distances up to 200 km.
     */
    public static double distanceInMeters(@Nonnull final Point p1, @Nonnull final Point p2) {
        checkNonnull("p1", p1);
        checkNonnull("p2", p2);

        final Point from;
        final Point to;
        if (p1.getLonDeg() <= p2.getLonDeg()) {
            from = p1;
            to = p2;
        } else {
            from = p2;
            to = p1;
        }

        // Calculate mid point of 2 latitudes.
        final double avgLat = (from.getLatDeg() + to.getLatDeg()) / 2.0;

        final double deltaLatDeg = Math.abs(to.getLatDeg() - from.getLatDeg());
        final double deltaLonDeg360 = Math.abs(to.getLonDeg() - from.getLonDeg());
        final double deltaLonDeg = ((deltaLonDeg360 <= 180.0) ? deltaLonDeg360 : (360.0 - deltaLonDeg360));

        // Meters per longitude is fixed; per latitude requires * cos(avg(lat)).
        final double deltaXMeters = degreesLonToMetersAtLat(deltaLonDeg, avgLat);
        final double deltaYMeters = degreesLatToMeters(deltaLatDeg);


        // Calculate length through Earth. This is an approximation, but works fine for short distances.
        return Math.sqrt((deltaXMeters * deltaXMeters) + (deltaYMeters * deltaYMeters));
    }

    public static double degreesLatToMeters(final double latDegrees) {
        return latDegrees * METERS_PER_DEGREE_LAT;
    }

    public static double degreesLonToMetersAtLat(final double lonDegrees, final double lat) {
        return lonDegrees * METERS_PER_DEGREE_LON_EQUATOR * Math.cos(Math.toRadians(lat));
    }

    public static double metersToDegreesLonAtLat(final double eastMeters, final double lat) {
        return (eastMeters / METERS_PER_DEGREE_LON_EQUATOR) / Math.cos(Math.toRadians(lat));
    }

    @Nonnull
    @Override
    public String toString() {
        return defined ? ("(" + getLatDeg() + ", " + getLonDeg() + ')') : "undefined";
    }

    @SuppressWarnings("NonFinalFieldReferencedInHashCode")
    @Override
    public int hashCode() {
        return Arrays.hashCode(new Object[]{latMicroDeg, lonMicroDeg, latFractionOnlyDeg, lonFractionOnlyDeg, defined});
    }

    @SuppressWarnings("NonFinalFieldReferenceInEquals")
    @Override
    public boolean equals(final Object obj) {
        if (this == obj) {
            return true;
        }
        if (!(obj instanceof Point)) {
            return false;
        }
        final Point that = (Point) obj;
        return (this.latMicroDeg == that.latMicroDeg) &&
                (this.lonMicroDeg == that.lonMicroDeg) &&
                (this.latFractionOnlyDeg == that.latFractionOnlyDeg) &&
                (this.lonFractionOnlyDeg == that.lonFractionOnlyDeg) &&
                (this.defined == that.defined);
    }

    // -----------------------------------------------------------------------
    // (Package) private data and methods.
    // -----------------------------------------------------------------------

    // Constants to convert between Degrees, MicroDegrees and Fractions
    static final double MICRODEG_TO_DEG_FACTOR = 1000000.0;
    static final double MAX_PRECISION_FACTOR = 810000.0;
    static final double LAT_MICRODEG_TO_FRACTIONS_FACTOR = MAX_PRECISION_FACTOR;
    static final double LON_MICRODEG_TO_FRACTIONS_FACTOR = MAX_PRECISION_FACTOR * 4;
    static final double LAT_TO_FRACTIONS_FACTOR = MICRODEG_TO_DEG_FACTOR * LAT_MICRODEG_TO_FRACTIONS_FACTOR;
    static final double LON_TO_FRACTIONS_FACTOR = MICRODEG_TO_DEG_FACTOR * LON_MICRODEG_TO_FRACTIONS_FACTOR;

    private int latMicroDeg;            // Whole nr of MICRODEG_TO_DEG_FACTOR.
    private int lonMicroDeg;            // Whole nr of MICRODEG_TO_DEG_FACTOR.
    private int latFractionOnlyDeg;     // Whole nr of LAT_TO_FRACTIONS_FACTOR, relative to latMicroDeg.
    private int lonFractionOnlyDeg;     // Whole nr of LON_TO_FRACTIONS_FACTOR, relative to lonMicroDeg.

    /**
     * Points can be "undefined" within the mapcode implementation, but never outside of that.
     * Any methods creating or setting undefined points must be package private and external
     * interfaces must never pass undefined points to callers.
     */
    private boolean defined;

    /**
     * Private constructors.
     */
    private Point() {
        defined = false;
    }

    /**
     * Public construction, from floating point degrees (potentially lossy).
     */
    @SuppressWarnings("NumericCastThatLosesPrecision")
    private Point(final double latDeg, final double lonDeg) {

        double lat = latDeg + 90;
        if (lat < 0) {
            lat = 0;
        } else if (lat > 180) {
            lat = 180;
        }

        // Rounding factor.
        final double fractionRounding = 0.1;

        // Lat now [0..180].
        lat = lat * LAT_TO_FRACTIONS_FACTOR;
        double latFractionOnly = Math.floor(lat + fractionRounding);
        latMicroDeg = (int) (latFractionOnly / LAT_MICRODEG_TO_FRACTIONS_FACTOR);
        latFractionOnly = latFractionOnly - ((double) latMicroDeg * LAT_MICRODEG_TO_FRACTIONS_FACTOR);
        latFractionOnlyDeg = (int) latFractionOnly;
        latMicroDeg = latMicroDeg - MICRO_DEG_90;

        // Math.floor has limited precision for really large values, so we need to limit the lon explicitly.
        double lon = Math.min(360.0, Math.max(0.0, lonDeg - (360.0 * Math.floor(lonDeg / 360.0))));
        if (Double.compare(lon, 360.0) == 0) {
            lon = 0.0;
        }

        // Lon now in [0..360>.
        lon = lon * LON_TO_FRACTIONS_FACTOR;
        double lonFractionOnly = Math.floor(lon + fractionRounding);
        lonMicroDeg = (int) (lonFractionOnly / LON_MICRODEG_TO_FRACTIONS_FACTOR);
        lonFractionOnly = lonFractionOnly - ((double) lonMicroDeg * LON_MICRODEG_TO_FRACTIONS_FACTOR);
        lonFractionOnlyDeg = (int) lonFractionOnly;

        // Wrap lonMicroDeg from [0..360> to [-180..180).
        if (lonMicroDeg >= MICRO_DEG_180) {
            lonMicroDeg = lonMicroDeg - MICRO_DEG_360;
        }

        defined = true;
    }

    /**
     * Get the the longitude "fractions", which is a whole number of 1/LON_TO_FRACTIONS_FACTOR-th
     * degrees versus the millionths of degrees.
     */
    int getLonFraction() {
        assert defined;
        return lonFractionOnlyDeg;
    }

    /**
     * Get the the latitude "fractions", which is a whole number of 1/LAT_TO_FRACTIONS_FACTOR-th
     * degrees versus the millionths of degrees
     */
    int getLatFraction() {
        assert defined;
        return latFractionOnlyDeg;
    }

    /**
     * Package private construction, from integer fractions (no loss of precision).
     */
    @SuppressWarnings("NumericCastThatLosesPrecision")
    @Nonnull
    static Point fromLatLonFractions(final double latFraction, final double lonFraction) {
        final Point p = new Point();
        p.latMicroDeg = (int) Math.floor(latFraction / LAT_MICRODEG_TO_FRACTIONS_FACTOR);
        p.latFractionOnlyDeg = (int) (latFraction - (LAT_MICRODEG_TO_FRACTIONS_FACTOR * p.latMicroDeg));
        p.lonMicroDeg = (int) Math.floor(lonFraction / LON_MICRODEG_TO_FRACTIONS_FACTOR);
        p.lonFractionOnlyDeg = (int) (lonFraction - (LON_MICRODEG_TO_FRACTIONS_FACTOR * p.lonMicroDeg));
        p.defined = true;
        return p.wrap();
    }

    static int degToMicroDeg(final double deg) {
        //noinspection NumericCastThatLosesPrecision
        return (int) Math.floor(deg * MICRODEG_TO_DEG_FACTOR);
    }

    static double microDegToDeg(final int microDeg) {
        return ((double) microDeg) / MICRODEG_TO_DEG_FACTOR;
    }

    @Nonnull
    Point wrap() {
        if (defined) {
            // Cut latitude to [-90, 90].
            if (latMicroDeg < -MICRO_DEG_90) {
                latMicroDeg = -MICRO_DEG_90;
                latFractionOnlyDeg = 0;
            }
            if (latMicroDeg > MICRO_DEG_90) {
                latMicroDeg = MICRO_DEG_90;
                latFractionOnlyDeg = 0;
            }
            // Map longitude to [-180, 180). Values outside this range are wrapped to this range.
            lonMicroDeg %= MICRO_DEG_360;
            if (lonMicroDeg >= MICRO_DEG_180) {
                lonMicroDeg -= MICRO_DEG_360;
            } else if (lonMicroDeg < -MICRO_DEG_180) {
                lonMicroDeg += MICRO_DEG_360;
            }
        }
        return this;
    }

    /**
     * Create an undefined points. No latitude or longitude can be obtained from it.
     * Only within the mapcode implementation points can be undefined, so this methods is package private.
     *
     * @return Undefined points.
     */
    @Nonnull
    static Point undefined() {
        return new Point();
    }

    /**
     * Set a point to be undefined, invalidating the latitude and longitude.
     * Only within the mapcode implementation points can be undefined, so this methods is package private.
     */
    void setUndefined() {
        defined = false;
    }

    /**
     * Return whether the point is defined or not.
     * Only within the mapcode implementation points can be undefined, so this methods is package private.
     *
     * @return True if defined. If false, no lat/lon is available.
     */
    boolean isDefined() {
        return defined;
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy