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

org.elasticsearch.common.geo.GeoDistance Maven / Gradle / Ivy

There is a newer version: 8.15.1
Show newest version
/*
 * Licensed to Elasticsearch under one or more contributor
 * license agreements. See the NOTICE file distributed with
 * this work for additional information regarding copyright
 * ownership. Elasticsearch licenses this file to you 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 org.elasticsearch.common.geo;

import org.apache.lucene.util.Bits;
import org.apache.lucene.util.SloppyMath;
import org.elasticsearch.common.io.stream.StreamInput;
import org.elasticsearch.common.io.stream.StreamOutput;
import org.elasticsearch.common.io.stream.Writeable;
import org.elasticsearch.common.unit.DistanceUnit;
import org.elasticsearch.index.fielddata.FieldData;
import org.elasticsearch.index.fielddata.GeoPointValues;
import org.elasticsearch.index.fielddata.MultiGeoPointValues;
import org.elasticsearch.index.fielddata.NumericDoubleValues;
import org.elasticsearch.index.fielddata.SortedNumericDoubleValues;
import org.elasticsearch.index.fielddata.SortingNumericDoubleValues;

import java.io.IOException;
import java.util.Locale;

/**
 * Geo distance calculation.
 */
public enum GeoDistance implements Writeable {
    /**
     * Calculates distance as points on a plane. Faster, but less accurate than {@link #ARC}.
     */
    PLANE {
        @Override
        public double calculate(double sourceLatitude, double sourceLongitude, double targetLatitude, double targetLongitude, DistanceUnit unit) {
            double px = targetLongitude - sourceLongitude;
            double py = targetLatitude - sourceLatitude;
            return Math.sqrt(px * px + py * py) * unit.getDistancePerDegree();
        }

        @Override
        public double normalize(double distance, DistanceUnit unit) {
            return distance;
        }

        @Override
        public FixedSourceDistance fixedSourceDistance(double sourceLatitude, double sourceLongitude, DistanceUnit unit) {
            return new PlaneFixedSourceDistance(sourceLatitude, sourceLongitude, unit);
        }
    },

    /**
     * Calculates distance factor.
     */
    FACTOR {
        @Override
        public double calculate(double sourceLatitude, double sourceLongitude, double targetLatitude, double targetLongitude, DistanceUnit unit) {
            double longitudeDifference = targetLongitude - sourceLongitude;
            double a = Math.toRadians(90D - sourceLatitude);
            double c = Math.toRadians(90D - targetLatitude);
            return (Math.cos(a) * Math.cos(c)) + (Math.sin(a) * Math.sin(c) * Math.cos(Math.toRadians(longitudeDifference)));
        }

        @Override
        public double normalize(double distance, DistanceUnit unit) {
            return Math.cos(distance / unit.getEarthRadius());
        }

        @Override
        public FixedSourceDistance fixedSourceDistance(double sourceLatitude, double sourceLongitude, DistanceUnit unit) {
            return new FactorFixedSourceDistance(sourceLatitude, sourceLongitude);
        }
    },
    /**
     * Calculates distance as points on a globe.
     */
    ARC {
        @Override
        public double calculate(double sourceLatitude, double sourceLongitude, double targetLatitude, double targetLongitude, DistanceUnit unit) {
            double result = SloppyMath.haversinMeters(sourceLatitude, sourceLongitude, targetLatitude, targetLongitude);
            return unit.fromMeters(result);
        }

        @Override
        public double normalize(double distance, DistanceUnit unit) {
            return distance;
        }

        @Override
        public FixedSourceDistance fixedSourceDistance(double sourceLatitude, double sourceLongitude, DistanceUnit unit) {
            return new ArcFixedSourceDistance(sourceLatitude, sourceLongitude, unit);
        }
    },
    /**
     * Calculates distance as points on a globe in a sloppy way. Close to the pole areas the accuracy
     * of this function decreases.
     */
    @Deprecated
    SLOPPY_ARC {

        @Override
        public double normalize(double distance, DistanceUnit unit) {
            return distance;
        }

        @Override
        public double calculate(double sourceLatitude, double sourceLongitude, double targetLatitude, double targetLongitude, DistanceUnit unit) {
            return unit.fromMeters(SloppyMath.haversinMeters(sourceLatitude, sourceLongitude, targetLatitude, targetLongitude));
        }

        @Override
        public FixedSourceDistance fixedSourceDistance(double sourceLatitude, double sourceLongitude, DistanceUnit unit) {
            return new SloppyArcFixedSourceDistance(sourceLatitude, sourceLongitude, unit);
        }
    };

    public static GeoDistance readFromStream(StreamInput in) throws IOException {
        int ord = in.readVInt();
        if (ord < 0 || ord >= values().length) {
            throw new IOException("Unknown GeoDistance ordinal [" + ord + "]");
        }
        return GeoDistance.values()[ord];
    }

    @Override
    public void writeTo(StreamOutput out) throws IOException {
        out.writeVInt(this.ordinal());
    }

    /**
     * Default {@link GeoDistance} function. This method should be used, If no specific function has been selected.
     * This is an alias for SLOPPY_ARC
     */
    public static final GeoDistance DEFAULT = SLOPPY_ARC;

    public abstract double normalize(double distance, DistanceUnit unit);

    public abstract double calculate(double sourceLatitude, double sourceLongitude, double targetLatitude, double targetLongitude, DistanceUnit unit);

    public abstract FixedSourceDistance fixedSourceDistance(double sourceLatitude, double sourceLongitude, DistanceUnit unit);

    private static final double MIN_LAT = Math.toRadians(-90d);  // -PI/2
    private static final double MAX_LAT = Math.toRadians(90d);   //  PI/2
    private static final double MIN_LON = Math.toRadians(-180d); // -PI
    private static final double MAX_LON = Math.toRadians(180d);  //  PI

    public static DistanceBoundingCheck distanceBoundingCheck(double sourceLatitude, double sourceLongitude, double distance, DistanceUnit unit) {
        // angular distance in radians on a great circle
        // assume worst-case: use the minor axis
        double radDist = unit.toMeters(distance) / GeoUtils.EARTH_SEMI_MINOR_AXIS;

        double radLat = Math.toRadians(sourceLatitude);
        double radLon = Math.toRadians(sourceLongitude);

        double minLat = radLat - radDist;
        double maxLat = radLat + radDist;

        double minLon, maxLon;
        if (minLat > MIN_LAT && maxLat < MAX_LAT) {
            double deltaLon = Math.asin(Math.sin(radDist) / Math.cos(radLat));
            minLon = radLon - deltaLon;
            if (minLon < MIN_LON) minLon += 2d * Math.PI;
            maxLon = radLon + deltaLon;
            if (maxLon > MAX_LON) maxLon -= 2d * Math.PI;
        } else {
            // a pole is within the distance
            minLat = Math.max(minLat, MIN_LAT);
            maxLat = Math.min(maxLat, MAX_LAT);
            minLon = MIN_LON;
            maxLon = MAX_LON;
        }

        GeoPoint topLeft = new GeoPoint(Math.toDegrees(maxLat), Math.toDegrees(minLon));
        GeoPoint bottomRight = new GeoPoint(Math.toDegrees(minLat), Math.toDegrees(maxLon));
        if (minLon > maxLon) {
            return new Meridian180DistanceBoundingCheck(topLeft, bottomRight);
        }
        return new SimpleDistanceBoundingCheck(topLeft, bottomRight);
    }

    /**
     * Get a {@link GeoDistance} according to a given name. Valid values are
     *
     * 
    *
  • plane for GeoDistance.PLANE
  • *
  • sloppy_arc for GeoDistance.SLOPPY_ARC
  • *
  • factor for GeoDistance.FACTOR
  • *
  • arc for GeoDistance.ARC
  • *
* * @param name name of the {@link GeoDistance} * @return a {@link GeoDistance} */ public static GeoDistance fromString(String name) { name = name.toLowerCase(Locale.ROOT); if ("plane".equals(name)) { return PLANE; } else if ("arc".equals(name)) { return ARC; } else if ("sloppy_arc".equals(name)) { return SLOPPY_ARC; } else if ("factor".equals(name)) { return FACTOR; } throw new IllegalArgumentException("No geo distance for [" + name + "]"); } public interface FixedSourceDistance { double calculate(double targetLatitude, double targetLongitude); } public interface DistanceBoundingCheck { boolean isWithin(double targetLatitude, double targetLongitude); GeoPoint topLeft(); GeoPoint bottomRight(); } public static final AlwaysDistanceBoundingCheck ALWAYS_INSTANCE = new AlwaysDistanceBoundingCheck(); private static class AlwaysDistanceBoundingCheck implements DistanceBoundingCheck { @Override public boolean isWithin(double targetLatitude, double targetLongitude) { return true; } @Override public GeoPoint topLeft() { return null; } @Override public GeoPoint bottomRight() { return null; } } public static class Meridian180DistanceBoundingCheck implements DistanceBoundingCheck { private final GeoPoint topLeft; private final GeoPoint bottomRight; public Meridian180DistanceBoundingCheck(GeoPoint topLeft, GeoPoint bottomRight) { this.topLeft = topLeft; this.bottomRight = bottomRight; } @Override public boolean isWithin(double targetLatitude, double targetLongitude) { return (targetLatitude >= bottomRight.lat() && targetLatitude <= topLeft.lat()) && (targetLongitude >= topLeft.lon() || targetLongitude <= bottomRight.lon()); } @Override public GeoPoint topLeft() { return topLeft; } @Override public GeoPoint bottomRight() { return bottomRight; } } public static class SimpleDistanceBoundingCheck implements DistanceBoundingCheck { private final GeoPoint topLeft; private final GeoPoint bottomRight; public SimpleDistanceBoundingCheck(GeoPoint topLeft, GeoPoint bottomRight) { this.topLeft = topLeft; this.bottomRight = bottomRight; } @Override public boolean isWithin(double targetLatitude, double targetLongitude) { return (targetLatitude >= bottomRight.lat() && targetLatitude <= topLeft.lat()) && (targetLongitude >= topLeft.lon() && targetLongitude <= bottomRight.lon()); } @Override public GeoPoint topLeft() { return topLeft; } @Override public GeoPoint bottomRight() { return bottomRight; } } public static class PlaneFixedSourceDistance implements FixedSourceDistance { private final double sourceLatitude; private final double sourceLongitude; private final double distancePerDegree; public PlaneFixedSourceDistance(double sourceLatitude, double sourceLongitude, DistanceUnit unit) { this.sourceLatitude = sourceLatitude; this.sourceLongitude = sourceLongitude; this.distancePerDegree = unit.getDistancePerDegree(); } @Override public double calculate(double targetLatitude, double targetLongitude) { double px = targetLongitude - sourceLongitude; double py = targetLatitude - sourceLatitude; return Math.sqrt(px * px + py * py) * distancePerDegree; } } public static class FactorFixedSourceDistance implements FixedSourceDistance { private final double sourceLongitude; private final double a; private final double sinA; private final double cosA; public FactorFixedSourceDistance(double sourceLatitude, double sourceLongitude) { this.sourceLongitude = sourceLongitude; this.a = Math.toRadians(90D - sourceLatitude); this.sinA = Math.sin(a); this.cosA = Math.cos(a); } @Override public double calculate(double targetLatitude, double targetLongitude) { double longitudeDifference = targetLongitude - sourceLongitude; double c = Math.toRadians(90D - targetLatitude); return (cosA * Math.cos(c)) + (sinA * Math.sin(c) * Math.cos(Math.toRadians(longitudeDifference))); } } /** * Basic implementation of {@link FixedSourceDistance}. This class keeps the basic parameters for a distance * functions based on a fixed source. Namely latitude, longitude and unit. */ public abstract static class FixedSourceDistanceBase implements FixedSourceDistance { protected final double sourceLatitude; protected final double sourceLongitude; protected final DistanceUnit unit; public FixedSourceDistanceBase(double sourceLatitude, double sourceLongitude, DistanceUnit unit) { this.sourceLatitude = sourceLatitude; this.sourceLongitude = sourceLongitude; this.unit = unit; } } public static class ArcFixedSourceDistance extends FixedSourceDistanceBase { public ArcFixedSourceDistance(double sourceLatitude, double sourceLongitude, DistanceUnit unit) { super(sourceLatitude, sourceLongitude, unit); } @Override public double calculate(double targetLatitude, double targetLongitude) { return ARC.calculate(sourceLatitude, sourceLongitude, targetLatitude, targetLongitude, unit); } } public static class SloppyArcFixedSourceDistance extends FixedSourceDistanceBase { public SloppyArcFixedSourceDistance(double sourceLatitude, double sourceLongitude, DistanceUnit unit) { super(sourceLatitude, sourceLongitude, unit); } @Override public double calculate(double targetLatitude, double targetLongitude) { return SLOPPY_ARC.calculate(sourceLatitude, sourceLongitude, targetLatitude, targetLongitude, unit); } } /** * Return a {@link SortedNumericDoubleValues} instance that returns the distances to a list of geo-points for each document. */ public static SortedNumericDoubleValues distanceValues(final MultiGeoPointValues geoPointValues, final FixedSourceDistance... distances) { final GeoPointValues singleValues = FieldData.unwrapSingleton(geoPointValues); if (singleValues != null && distances.length == 1) { final Bits docsWithField = FieldData.unwrapSingletonBits(geoPointValues); return FieldData.singleton(new NumericDoubleValues() { @Override public double get(int docID) { if (docsWithField != null && !docsWithField.get(docID)) { return 0d; } final GeoPoint point = singleValues.get(docID); return distances[0].calculate(point.lat(), point.lon()); } }, docsWithField); } else { return new SortingNumericDoubleValues() { @Override public void setDocument(int doc) { geoPointValues.setDocument(doc); resize(geoPointValues.count() * distances.length); int valueCounter = 0; for (FixedSourceDistance distance : distances) { for (int i = 0; i < geoPointValues.count(); ++i) { final GeoPoint point = geoPointValues.valueAt(i); values[valueCounter] = distance.calculate(point.lat(), point.lon()); valueCounter++; } } sort(); } }; } } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy