From 104d4e5bd34b912e7ff1324a8e8cc52052cf95d6 Mon Sep 17 00:00:00 2001 From: Nicholas Knize Date: Wed, 28 Nov 2018 15:04:42 -0500 Subject: [PATCH 01/10] Add initial GeoShape hierachy WIP --- .../elasticsearch/geo/GeoEncodingUtils.java | 158 ++++ .../java/org/elasticsearch/geo/GeoUtils.java | 253 ++++++ .../elasticsearch/geo/geometry/Circle.java | 134 +++ .../elasticsearch/geo/geometry/EdgeTree.java | 823 ++++++++++++++++++ .../elasticsearch/geo/geometry/GeoShape.java | 168 ++++ .../geo/geometry/GeoShapeCollection.java | 98 +++ .../org/elasticsearch/geo/geometry/Line.java | 97 +++ .../elasticsearch/geo/geometry/MultiLine.java | 150 ++++ .../geo/geometry/MultiPoint.java | 216 +++++ .../geo/geometry/MultiPolygon.java | 143 +++ .../org/elasticsearch/geo/geometry/Point.java | 142 +++ .../elasticsearch/geo/geometry/Polygon.java | 242 +++++ .../elasticsearch/geo/geometry/Predicate.java | 254 ++++++ .../elasticsearch/geo/geometry/Rectangle.java | 415 +++++++++ .../elasticsearch/geo/geometry/ShapeType.java | 83 ++ .../org/elasticsearch/geo/package-info.java | 24 + .../parsers/SimpleGeoJSONPolygonParser.java | 445 ++++++++++ .../elasticsearch/geo/parsers/WKBParser.java | 49 ++ .../elasticsearch/geo/parsers/WKTParser.java | 326 +++++++ .../geo/TestGeoEncodingUtils.java | 155 ++++ .../elasticsearch/geo/TestGeoJSONParsing.java | 266 ++++++ .../org/elasticsearch/geo/TestGeoUtils.java | 315 +++++++ .../geo/geometry/BaseGeometryTestCase.java | 100 +++ .../geo/geometry/TestEdgeTree.java | 312 +++++++ .../elasticsearch/geo/geometry/TestLine.java | 114 +++ .../geo/geometry/TestMultiLine.java | 115 +++ .../geo/geometry/TestMultiPoint.java | 117 +++ .../geo/geometry/TestMultiPolygon.java | 136 +++ .../elasticsearch/geo/geometry/TestPoint.java | 105 +++ .../geo/geometry/TestPolygon.java | 185 ++++ 30 files changed, 6140 insertions(+) create mode 100644 libs/geo/src/main/java/org/elasticsearch/geo/GeoEncodingUtils.java create mode 100644 libs/geo/src/main/java/org/elasticsearch/geo/GeoUtils.java create mode 100644 libs/geo/src/main/java/org/elasticsearch/geo/geometry/Circle.java create mode 100644 libs/geo/src/main/java/org/elasticsearch/geo/geometry/EdgeTree.java create mode 100644 libs/geo/src/main/java/org/elasticsearch/geo/geometry/GeoShape.java create mode 100644 libs/geo/src/main/java/org/elasticsearch/geo/geometry/GeoShapeCollection.java create mode 100644 libs/geo/src/main/java/org/elasticsearch/geo/geometry/Line.java create mode 100644 libs/geo/src/main/java/org/elasticsearch/geo/geometry/MultiLine.java create mode 100644 libs/geo/src/main/java/org/elasticsearch/geo/geometry/MultiPoint.java create mode 100644 libs/geo/src/main/java/org/elasticsearch/geo/geometry/MultiPolygon.java create mode 100644 libs/geo/src/main/java/org/elasticsearch/geo/geometry/Point.java create mode 100644 libs/geo/src/main/java/org/elasticsearch/geo/geometry/Polygon.java create mode 100644 libs/geo/src/main/java/org/elasticsearch/geo/geometry/Predicate.java create mode 100644 libs/geo/src/main/java/org/elasticsearch/geo/geometry/Rectangle.java create mode 100644 libs/geo/src/main/java/org/elasticsearch/geo/geometry/ShapeType.java create mode 100644 libs/geo/src/main/java/org/elasticsearch/geo/package-info.java create mode 100644 libs/geo/src/main/java/org/elasticsearch/geo/parsers/SimpleGeoJSONPolygonParser.java create mode 100644 libs/geo/src/main/java/org/elasticsearch/geo/parsers/WKBParser.java create mode 100644 libs/geo/src/main/java/org/elasticsearch/geo/parsers/WKTParser.java create mode 100644 libs/geo/src/test/java/org/elasticsearch/geo/TestGeoEncodingUtils.java create mode 100644 libs/geo/src/test/java/org/elasticsearch/geo/TestGeoJSONParsing.java create mode 100644 libs/geo/src/test/java/org/elasticsearch/geo/TestGeoUtils.java create mode 100644 libs/geo/src/test/java/org/elasticsearch/geo/geometry/BaseGeometryTestCase.java create mode 100644 libs/geo/src/test/java/org/elasticsearch/geo/geometry/TestEdgeTree.java create mode 100644 libs/geo/src/test/java/org/elasticsearch/geo/geometry/TestLine.java create mode 100644 libs/geo/src/test/java/org/elasticsearch/geo/geometry/TestMultiLine.java create mode 100644 libs/geo/src/test/java/org/elasticsearch/geo/geometry/TestMultiPoint.java create mode 100644 libs/geo/src/test/java/org/elasticsearch/geo/geometry/TestMultiPolygon.java create mode 100644 libs/geo/src/test/java/org/elasticsearch/geo/geometry/TestPoint.java create mode 100644 libs/geo/src/test/java/org/elasticsearch/geo/geometry/TestPolygon.java diff --git a/libs/geo/src/main/java/org/elasticsearch/geo/GeoEncodingUtils.java b/libs/geo/src/main/java/org/elasticsearch/geo/GeoEncodingUtils.java new file mode 100644 index 0000000000000..b4fbbfad256b5 --- /dev/null +++ b/libs/geo/src/main/java/org/elasticsearch/geo/GeoEncodingUtils.java @@ -0,0 +1,158 @@ +/* + * 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.geo; + +import org.apache.lucene.util.NumericUtils; + +import static org.elasticsearch.geo.GeoUtils.MAX_LAT_INCL; +import static org.elasticsearch.geo.GeoUtils.MAX_LON_INCL; +import static org.elasticsearch.geo.GeoUtils.MIN_LON_INCL; +import static org.elasticsearch.geo.GeoUtils.MIN_LAT_INCL; +import static org.elasticsearch.geo.GeoUtils.checkLatitude; +import static org.elasticsearch.geo.GeoUtils.checkLongitude; + +/** + * reusable geopoint encoding methods + */ +public final class GeoEncodingUtils { + /** + * number of bits used for quantizing latitude and longitude values + */ + public static final short BITS = 32; + + private static final double LAT_SCALE = (0x1L << BITS) / 180.0D; + private static final double LAT_DECODE = 1 / LAT_SCALE; + private static final double LON_SCALE = (0x1L << BITS) / 360.0D; + private static final double LON_DECODE = 1 / LON_SCALE; + + // No instance: + private GeoEncodingUtils() { + } + + /** + * Quantizes double (64 bit) latitude into 32 bits (rounding down: in the direction of -90) + * + * @param latitude latitude value: must be within standard +/-90 coordinate bounds. + * @return encoded value as a 32-bit {@code int} + * @throws IllegalArgumentException if latitude is out of bounds + */ + public static int encodeLatitude(double latitude) { + checkLatitude(latitude); + // the maximum possible value cannot be encoded without overflow + if (latitude == 90.0D) { + latitude = Math.nextDown(latitude); + } + return (int) Math.floor(latitude / LAT_DECODE); + } + + /** + * Quantizes double (64 bit) latitude into 32 bits (rounding up: in the direction of +90) + * + * @param latitude latitude value: must be within standard +/-90 coordinate bounds. + * @return encoded value as a 32-bit {@code int} + * @throws IllegalArgumentException if latitude is out of bounds + */ + public static int encodeLatitudeCeil(double latitude) { + GeoUtils.checkLatitude(latitude); + // the maximum possible value cannot be encoded without overflow + if (latitude == 90.0D) { + latitude = Math.nextDown(latitude); + } + return (int) Math.ceil(latitude / LAT_DECODE); + } + + /** + * Quantizes double (64 bit) longitude into 32 bits (rounding down: in the direction of -180) + * + * @param longitude longitude value: must be within standard +/-180 coordinate bounds. + * @return encoded value as a 32-bit {@code int} + * @throws IllegalArgumentException if longitude is out of bounds + */ + public static int encodeLongitude(double longitude) { + checkLongitude(longitude); + // the maximum possible value cannot be encoded without overflow + if (longitude == 180.0D) { + longitude = Math.nextDown(longitude); + } + return (int) Math.floor(longitude / LON_DECODE); + } + + /** + * Quantizes double (64 bit) longitude into 32 bits (rounding up: in the direction of +180) + * + * @param longitude longitude value: must be within standard +/-180 coordinate bounds. + * @return encoded value as a 32-bit {@code int} + * @throws IllegalArgumentException if longitude is out of bounds + */ + public static int encodeLongitudeCeil(double longitude) { + GeoUtils.checkLongitude(longitude); + // the maximum possible value cannot be encoded without overflow + if (longitude == 180.0D) { + longitude = Math.nextDown(longitude); + } + return (int) Math.ceil(longitude / LON_DECODE); + } + + /** + * Turns quantized value from {@link #encodeLatitude} back into a double. + * + * @param encoded encoded value: 32-bit quantized value. + * @return decoded latitude value. + */ + public static double decodeLatitude(int encoded) { + double result = encoded * LAT_DECODE; + assert result >= MIN_LAT_INCL && result < MAX_LAT_INCL; + return result; + } + + /** + * Turns quantized value from byte array back into a double. + * + * @param src byte array containing 4 bytes to decode at {@code offset} + * @param offset offset into {@code src} to decode from. + * @return decoded latitude value. + */ + public static double decodeLatitude(byte[] src, int offset) { + return decodeLatitude(NumericUtils.sortableBytesToInt(src, offset)); + } + + /** + * Turns quantized value from {@link #encodeLongitude} back into a double. + * + * @param encoded encoded value: 32-bit quantized value. + * @return decoded longitude value. + */ + public static double decodeLongitude(int encoded) { + double result = encoded * LON_DECODE; + assert result >= MIN_LON_INCL && result < MAX_LON_INCL; + return result; + } + + /** + * Turns quantized value from byte array back into a double. + * + * @param src byte array containing 4 bytes to decode at {@code offset} + * @param offset offset into {@code src} to decode from. + * @return decoded longitude value. + */ + public static double decodeLongitude(byte[] src, int offset) { + return decodeLongitude(NumericUtils.sortableBytesToInt(src, offset)); + } +} diff --git a/libs/geo/src/main/java/org/elasticsearch/geo/GeoUtils.java b/libs/geo/src/main/java/org/elasticsearch/geo/GeoUtils.java new file mode 100644 index 0000000000000..5920146782a98 --- /dev/null +++ b/libs/geo/src/main/java/org/elasticsearch/geo/GeoUtils.java @@ -0,0 +1,253 @@ +/* + * 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.geo; + +import static org.apache.lucene.util.SloppyMath.TO_RADIANS; +import static org.apache.lucene.util.SloppyMath.cos; +import static org.apache.lucene.util.SloppyMath.haversinMeters; + +import org.elasticsearch.geo.geometry.GeoShape.Relation; +import org.elasticsearch.geo.geometry.Rectangle; +import org.apache.lucene.util.SloppyMath; + +/** + * Basic reusable geo-spatial utility methods + */ +public final class GeoUtils { + /** + * Minimum longitude value. + */ + public static final double MIN_LON_INCL = -180.0D; + + /** + * Maximum longitude value. + */ + public static final double MAX_LON_INCL = 180.0D; + + /** + * Minimum latitude value. + */ + public static final double MIN_LAT_INCL = -90.0D; + + /** + * Maximum latitude value. + */ + public static final double MAX_LAT_INCL = 90.0D; + + /** + * min longitude value in radians + */ + public static final double MIN_LON_RADIANS = TO_RADIANS * MIN_LON_INCL; + /** + * min latitude value in radians + */ + public static final double MIN_LAT_RADIANS = TO_RADIANS * MIN_LAT_INCL; + /** + * max longitude value in radians + */ + public static final double MAX_LON_RADIANS = TO_RADIANS * MAX_LON_INCL; + /** + * max latitude value in radians + */ + public static final double MAX_LAT_RADIANS = TO_RADIANS * MAX_LAT_INCL; + + // WGS84 earth-ellipsoid parameters + /** + * mean earth axis in meters + */ + // see http://earth-info.nga.mil/GandG/publications/tr8350.2/wgs84fin.pdf + public static final double EARTH_MEAN_RADIUS_METERS = 6_371_008.7714; + /** + * conversion factor for degree to kilometers + */ + public static final double DEG_TO_METERS = 111195.07973436875D; + + + // No instance: + private GeoUtils() { + } + + /** + * validates latitude value is within standard +/-90 coordinate bounds + */ + public static void checkLatitude(double latitude) { + if (Double.isNaN(latitude) || latitude < MIN_LAT_INCL || latitude > MAX_LAT_INCL) { + throw new IllegalArgumentException("invalid latitude " + latitude + "; must be between " + MIN_LAT_INCL + " and " + MAX_LAT_INCL); + } + } + + /** + * validates longitude value is within standard +/-180 coordinate bounds + */ + public static void checkLongitude(double longitude) { + if (Double.isNaN(longitude) || longitude < MIN_LON_INCL || longitude > MAX_LON_INCL) { + throw new IllegalArgumentException("invalid longitude " + longitude + "; must be between " + MIN_LON_INCL + " and " + MAX_LON_INCL); + } + } + + // some sloppyish stuff, do we really need this to be done in a sloppy way? + // unless it is performance sensitive, we should try to remove. + private static final double PIO2 = Math.PI / 2D; + + /** + * Returns the trigonometric sine of an angle converted as a cos operation. + *

+ * Note that this is not quite right... e.g. sin(0) != 0 + *

+ * Special cases: + *

+ * + * @param a an angle, in radians. + * @return the sine of the argument. + * @see Math#sin(double) + */ + // TODO: deprecate/remove this? at least its no longer public. + public static double sloppySin(double a) { + return cos(a - PIO2); + } + + /** + * binary search to find the exact sortKey needed to match the specified radius + * any sort key lte this is a query match. + */ + public static double distanceQuerySortKey(double radius) { + // effectively infinite + if (radius >= haversinMeters(Double.MAX_VALUE)) { + return haversinMeters(Double.MAX_VALUE); + } + + // this is a search through non-negative long space only + long lo = 0; + long hi = Double.doubleToRawLongBits(Double.MAX_VALUE); + while (lo <= hi) { + long mid = (lo + hi) >>> 1; + double sortKey = Double.longBitsToDouble(mid); + double midRadius = haversinMeters(sortKey); + if (midRadius == radius) { + return sortKey; + } else if (midRadius > radius) { + hi = mid - 1; + } else { + lo = mid + 1; + } + } + + // not found: this is because a user can supply an arbitrary radius, one that we will never + // calculate exactly via our haversin method. + double ceil = Double.longBitsToDouble(lo); + assert haversinMeters(ceil) > radius; + return ceil; + } + + /** + * Compute the relation between the provided box and distance query. + * This only works for boxes that do not cross the dateline. + */ + public static Relation relate( + double minLat, double maxLat, double minLon, double maxLon, + double lat, double lon, double distanceSortKey, double axisLat) { + + if (minLon > maxLon) { + throw new IllegalArgumentException("Box crosses the dateline"); + } + + if ((lon < minLon || lon > maxLon) && (axisLat + Rectangle.AXISLAT_ERROR < minLat || axisLat - Rectangle.AXISLAT_ERROR > maxLat)) { + // circle not fully inside / crossing axis + if (SloppyMath.haversinSortKey(lat, lon, minLat, minLon) > distanceSortKey && + SloppyMath.haversinSortKey(lat, lon, minLat, maxLon) > distanceSortKey && + SloppyMath.haversinSortKey(lat, lon, maxLat, minLon) > distanceSortKey && + SloppyMath.haversinSortKey(lat, lon, maxLat, maxLon) > distanceSortKey) { + // no points inside + return Relation.DISJOINT; + } + } + + if (within90LonDegrees(lon, minLon, maxLon) && + SloppyMath.haversinSortKey(lat, lon, minLat, minLon) <= distanceSortKey && + SloppyMath.haversinSortKey(lat, lon, minLat, maxLon) <= distanceSortKey && + SloppyMath.haversinSortKey(lat, lon, maxLat, minLon) <= distanceSortKey && + SloppyMath.haversinSortKey(lat, lon, maxLat, maxLon) <= distanceSortKey) { + // we are fully enclosed, collect everything within this subtree + return Relation.WITHIN; + } + + return Relation.CROSSES; + } + + /** + * Return whether all points of {@code [minLon,maxLon]} are within 90 degrees of {@code lon}. + */ + static boolean within90LonDegrees(double lon, double minLon, double maxLon) { + if (maxLon <= lon - 180) { + lon -= 360; + } else if (minLon >= lon + 180) { + lon += 360; + } + return maxLon - lon < 90 && lon - minLon < 90; + } + + /** + * computes longitude in range -180 <= lon_deg <= +180. + */ + public static double normalizeLonDegrees(double lonDegrees) { + if (lonDegrees >= -180 && lonDegrees <= 180) + return lonDegrees;//common case, and avoids slight double precision shifting + double off = (lonDegrees + 180) % 360; + if (off < 0) + return 180 + off; + else if (off == 0 && lonDegrees > 0) + return 180; + else + return -180 + off; + } + + public static double distanceToDegrees(double dist, double radius) { + return StrictMath.toDegrees(distanceToRadians(dist, radius)); + } + + public static double degreesToDistance(double degrees, double radius) { + return radiansToDistance(StrictMath.toRadians(degrees), radius); + } + + public static double distanceToRadians(double dist, double radius) { + return dist / radius; + } + + public static double radiansToDistance(double radians, double radius) { + return radians * radius; + } + + public static double computeOrientation(final double[] xVals, final double[] yVals) { + if (xVals == null || yVals == null || xVals.length < 3 || xVals.length != yVals.length) { + throw new IllegalArgumentException("xVals and yVals must be the same length and include three or more values"); + } + double windingSum = 0d; + final int numPts = yVals.length - 1; + for (int i = 1, j = 0; i < numPts; j = i++) { + // compute signed area for orientation + windingSum += (xVals[j] - xVals[numPts]) * (yVals[i] - yVals[numPts]) + - (yVals[j] - yVals[numPts]) * (xVals[i] - xVals[numPts]); + } + return windingSum; + } + +} diff --git a/libs/geo/src/main/java/org/elasticsearch/geo/geometry/Circle.java b/libs/geo/src/main/java/org/elasticsearch/geo/geometry/Circle.java new file mode 100644 index 0000000000000..485287253e9ce --- /dev/null +++ b/libs/geo/src/main/java/org/elasticsearch/geo/geometry/Circle.java @@ -0,0 +1,134 @@ +/* + * 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.geo.geometry; + +import java.io.IOException; + +import org.elasticsearch.geo.geometry.Predicate.DistancePredicate; +import org.apache.lucene.store.OutputStreamDataOutput; + +/** + * Created by nknize on 9/25/17. + */ +public class Circle extends GeoShape { + private final double lat; + private final double lon; + private final double radiusMeters; + private DistancePredicate predicate; + + public Circle(final double lat, final double lon, final double radiusMeters) { + this.lat = lat; + this.lon = lon; + this.radiusMeters = radiusMeters; + this.boundingBox = Rectangle.fromPointDistance(lat, lon, radiusMeters); + } + + public double getCenterLat() { + return lat; + } + + public double getCenterLon() { + return lon; + } + + public double getRadiusMeters() { + return radiusMeters; + } + + protected double computeArea() { + return radiusMeters * radiusMeters * StrictMath.PI; + } + + @Override + public ShapeType type() { + return ShapeType.CIRCLE; + } + + @Override + public boolean hasArea() { + return true; + } + + @Override + public Relation relate(double minLat, double maxLat, double minLon, double maxLon) { + return predicate().relate(minLat, maxLat, minLon, maxLon); + } + + @Override + public Relation relate(GeoShape shape) { + throw new UnsupportedOperationException("not yet able to relate other GeoShape types to circles"); + } + + public boolean pointInside(final int encodedLat, final int encodedLon) { + return predicate().test(encodedLat, encodedLon); + } + + private DistancePredicate predicate() { + if (predicate == null) { + predicate = Predicate.DistancePredicate.create(lat, lon, radiusMeters); + } + return predicate; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + if (!super.equals(o)) return false; + + Circle circle = (Circle) o; + if (Double.compare(circle.lat, lat) != 0) return false; + if (Double.compare(circle.lon, lon) != 0) return false; + if (Double.compare(circle.radiusMeters, radiusMeters) != 0) return false; + return predicate.equals(circle.predicate); + } + + @Override + public int hashCode() { + int result = super.hashCode(); + long temp; + temp = Double.doubleToLongBits(lat); + result = 31 * result + (int) (temp ^ (temp >>> 32)); + temp = Double.doubleToLongBits(lon); + result = 31 * result + (int) (temp ^ (temp >>> 32)); + temp = Double.doubleToLongBits(radiusMeters); + result = 31 * result + (int) (temp ^ (temp >>> 32)); + result = 31 * result + predicate.hashCode(); + return result; + } + + @Override + public String toWKT() { + throw new UnsupportedOperationException("The WKT spec does not support CIRCLE geometry"); + } + + + @Override + protected StringBuilder contentToWKT() { + throw new UnsupportedOperationException("The WKT spec does not support CIRCLE geometry"); + } + + @Override + protected void appendWKBContent(OutputStreamDataOutput out) throws IOException { + out.writeVLong(Double.doubleToRawLongBits(lat)); + out.writeVLong(Double.doubleToRawLongBits(lon)); + out.writeVLong(Double.doubleToRawLongBits(radiusMeters)); + } +} diff --git a/libs/geo/src/main/java/org/elasticsearch/geo/geometry/EdgeTree.java b/libs/geo/src/main/java/org/elasticsearch/geo/geometry/EdgeTree.java new file mode 100644 index 0000000000000..277f482b4d7fb --- /dev/null +++ b/libs/geo/src/main/java/org/elasticsearch/geo/geometry/EdgeTree.java @@ -0,0 +1,823 @@ +/* + * 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.geo.geometry; + +import java.util.Arrays; +import java.util.Comparator; + +import org.elasticsearch.geo.geometry.GeoShape.Relation; +import org.apache.lucene.util.ArrayUtil; + +/** + * 2D polygon implementation represented as a balanced interval tree of edges. + *

+ * Construction takes {@code O(n log n)} time for sorting and tree construction. + * {@link #contains contains()} and {@link #relate relate()} are {@code O(n)}, but for most + * practical polygons are much faster than brute force. + *

+ * Loosely based on the algorithm described in + * http://www-ma2.upc.es/geoc/Schirra-pointPolygon.pdf. + * + * @lucene.internal + */ +// Both Polygon.contains() and Polygon.crossesSlowly() loop all edges, and first check that the edge is within a range. +// we just organize the edges to do the same computations on the same subset of edges more efficiently. +final class EdgeTree { + /** + * minimum latitude of this polygon's bounding box area + */ + public final double minLat; + /** + * maximum latitude of this polygon's bounding box area + */ + public final double maxLat; + /** + * minimum longitude of this polygon's bounding box area + */ + public final double minLon; + /** + * maximum longitude of this polygon's bounding box area + */ + public final double maxLon; + + // each component/hole is a node in an augmented 2d kd-tree: we alternate splitting between latitude/longitude, + // and pull up max values for both dimensions to each parent node (regardless of split). + + /** + * maximum latitude of this component or any of its children + */ + private double maxY; + /** + * maximum longitude of this component or any of its children + */ + private double maxX; + /** + * which dimension was this node split on + */ + // TODO: its implicit based on level, but boolean keeps code simple + private boolean splitX; + + // child components, or null + private EdgeTree left; + private EdgeTree right; + + /** + * tree of holes, or null + */ + private final EdgeTree holes; + + /** + * root node of edge tree + */ + private final Edge tree; + + /** + * area (in sq meters) of shape represented by the tree + */ + private double areaSqDegrees = Double.NaN; + // NOCOMMIT +// private final boolean isCyclic; + + EdgeTree(Line boundary) { + this(boundary, null); + } + + EdgeTree(Line boundary, EdgeTree holes) { + this.holes = holes; + this.minLat = boundary.minLat(); + this.maxLat = boundary.maxLat(); + this.minLon = boundary.minLon(); + this.maxLon = boundary.maxLon(); + this.maxY = maxLat; + this.maxX = maxLon; +// this.isCyclic = boundary instanceof Polygon; + + // create interval tree of edges + this.tree = createTree(boundary.getLats(), boundary.getLons()); + if (holes != null) { + this.areaSqDegrees -= holes.areaSqDegrees; + } + } + + public double getArea() { + return areaSqDegrees; + } + + /** + * Returns true if the point is contained within this polygon. + *

+ * See + * https://www.ecse.rpi.edu/~wrf/Research/Short_Notes/pnpoly.html for more information. + */ + public boolean contains(double latitude, double longitude) { + if (latitude <= maxY && longitude <= maxX) { + if (componentContains(latitude, longitude)) { + return true; + } + if (left != null) { + if (left.contains(latitude, longitude)) { + return true; + } + } + if (right != null && ((splitX == false && latitude >= minLat) || (splitX && longitude >= minLon))) { + if (right.contains(latitude, longitude)) { + return true; + } + } + } + return false; + } + + /** + * Returns true if the point is contained within this polygon component. + */ + private boolean componentContains(double latitude, double longitude) { + // check bounding box + if (latitude < minLat || latitude > maxLat || longitude < minLon || longitude > maxLon) { + return false; + } + + if (tree.contains(latitude, longitude)) { + if (holes != null && holes.contains(latitude, longitude)) { + return false; + } + return true; + } + + return false; + } + + /** + * Returns relation to the provided rectangle + */ + public Relation relate(double minLat, double maxLat, double minLon, double maxLon) { + if (minLat <= maxY && minLon <= maxX) { + Relation relation = componentRelate(minLat, maxLat, minLon, maxLon); + if (relation != Relation.DISJOINT) { + return relation; + } + if (left != null) { + relation = left.relate(minLat, maxLat, minLon, maxLon); + if (relation != Relation.DISJOINT) { + return relation; + } + } + if (right != null && ((splitX == false && maxLat >= this.minLat) || (splitX && maxLon >= this.minLon))) { + relation = right.relate(minLat, maxLat, minLon, maxLon); + if (relation != Relation.DISJOINT) { + return relation; + } + } + } + return Relation.DISJOINT; + } + + /** + * Returns relation to the provided line + */ + public Relation relateLine(double lat1, double lon1, double lat2, double lon2) { + double minLat = lat1, maxLat = lat2; + if (lat2 < lat1) { + minLat = lat2; + maxLat = lat1; + } + if (minLat <= maxY && minLon <= maxX) { + Relation relation = componentRelateLine(lat1, lon1, lat2, lon2); + if (relation != Relation.DISJOINT) { + return relation; + } + if (left != null) { + relation = left.relateLine(lat1, lon1, lat2, lon2); + if (relation != Relation.DISJOINT) { + return relation; + } + } + if (right != null && ((splitX == false && maxLat >= this.minLat) || (splitX && Math.max(lon1, lon2) >= this.minLon))) { + relation = right.relateLine(lat1, lon1, lat2, lon2); + if (relation != Relation.DISJOINT) { + return relation; + } + } + } + return Relation.DISJOINT; + } + +// /** Traverse 2 EdgeTrees to find */ +// public Relation relate(EdgeTree o) { +// // if the bounding boxes are disjoint then the trees are disjoint +// if (o.maxLon < this.minLon || o.minLon > this.maxLon || o.maxLat < this.minLat || o.minLat > this.maxLat) { +// return Relation.DISJOINT; +// } +// +// // check any holes +// if (holes != null) { +// Relation holeRelation = holes.relate(o); +// if (holeRelation == Relation.CROSSES) { +// return Relation.CROSSES; +// } else if (holeRelation == Relation.CONTAINS) { +// return Relation.DISJOINT; +// } +// } +// +// boolean crosses = tree.crosses(o.tree); +// +// if (crosses == false) { +// // iff other is a closed shape; check one point is in the shape (if so its contained): +// if (o.isCyclic) { +// if (o.componentContains(tree.lat1, tree.lon1)) { +// return Relation.WITHIN; +// } else if (this.isCyclic && componentContains(o.tree.lat1, o.tree.lat2)) { +// return Relation.CONTAINS; +// } +// } else if (this.isCyclic && componentContains(o.tree.lat1, o.tree.lon1)) { +// return Relation.CONTAINS; +// } +// return Relation.DISJOINT; +// } +// +// return Relation.CROSSES; +// } + + /** + * Returns relation to the provided rectangle for this component + */ + private Relation componentRelate(double minLat, double maxLat, double minLon, double maxLon) { + // if the bounding boxes are disjoint then the shape does not cross + if (maxLon < this.minLon || minLon > this.maxLon || maxLat < this.minLat || minLat > this.maxLat) { + return Relation.DISJOINT; + } + // if the rectangle fully encloses us, we cross. + if (minLat <= this.minLat && maxLat >= this.maxLat && minLon <= this.minLon && maxLon >= this.maxLon) { + return Relation.INTERSECTS; + } + // check any holes + if (holes != null) { + Relation holeRelation = holes.relate(minLat, maxLat, minLon, maxLon); + if (holeRelation == Relation.INTERSECTS) { + return Relation.INTERSECTS; + } else if (holeRelation == Relation.WITHIN) { + return Relation.DISJOINT; + } + } + // check each corner: if < 4 are present, its cheaper than crossesSlowly + int numCorners = numberOfCorners(minLat, maxLat, minLon, maxLon); + if (numCorners == 4) { + if (tree.intersects(minLat, maxLat, minLon, maxLon)) { + return Relation.INTERSECTS; + } + return Relation.WITHIN; + } else if (numCorners > 0) { + return Relation.INTERSECTS; + } + + // we cross + if (tree.intersects(minLat, maxLat, minLon, maxLon)) { + return Relation.INTERSECTS; + } + + return Relation.DISJOINT; + } + + /** + * Returns relation to the provided line for this component + */ + private Relation componentRelateLine(double lat1, double lon1, double lat2, double lon2) { + if (lineDisjointWithBBox(lat1, lon1, lat2, lon2)) { + return Relation.DISJOINT; + } + // check any holes + if (holes != null) { + Relation holeRelation = holes.relateLine(lat1, lon1, lat2, lon2); + if (holeRelation == Relation.INTERSECTS) { + return Relation.INTERSECTS; + } else if (holeRelation == Relation.WITHIN) { + return Relation.DISJOINT; + } + } + // we intersects + if (tree.intersectsLine(lat1, lon1, lat2, lon2)) { + return Relation.INTERSECTS; + } + return Relation.DISJOINT; + } + + /** + * checks if provided line is disjoint with the component's bounding box + */ + private boolean lineDisjointWithBBox(double lat1, double lon1, double lat2, double lon2) { + double minLat = lat1, maxLat = lat2; + if (lat2 < lat1) { + minLat = lat2; + maxLat = lat1; + } + if (maxLat < this.minLat || minLat > this.maxLat) { + return true; + } + double minLon = lon1, maxLon = lon2; + if (lon2 < lon1) { + minLon = lon2; + maxLon = lon1; + } + // if the bounding boxes are disjoint then the shape does not cross + if (maxLon < this.minLon || minLon > this.maxLon) { + return true; + } + return false; + } + + // returns 0, 4, or something in between + private int numberOfCorners(double minLat, double maxLat, double minLon, double maxLon) { + int containsCount = 0; + if (componentContains(minLat, minLon)) { + containsCount++; + } + if (componentContains(minLat, maxLon)) { + containsCount++; + } + if (containsCount == 1) { + return containsCount; + } + if (componentContains(maxLat, maxLon)) { + containsCount++; + } + if (containsCount == 2) { + return containsCount; + } + if (componentContains(maxLat, minLon)) { + containsCount++; + } + return containsCount; + } + + /** + * Creates tree from sorted components (with range low and high inclusive) + */ + protected static EdgeTree createTree(EdgeTree components[], int low, int high, boolean splitX) { + if (low > high) { + return null; + } + final int mid = (low + high) >>> 1; + if (low < high) { + Comparator comparator; + if (splitX) { + comparator = (left, right) -> { + int ret = Double.compare(left.minLon, right.minLon); + if (ret == 0) { + ret = Double.compare(left.maxX, right.maxX); + } + return ret; + }; + } else { + comparator = (left, right) -> { + int ret = Double.compare(left.minLat, right.minLat); + if (ret == 0) { + ret = Double.compare(left.maxY, right.maxY); + } + return ret; + }; + } + ArrayUtil.select(components, low, high + 1, mid, comparator); + } + // add midpoint + EdgeTree newNode = components[mid]; + newNode.splitX = splitX; + // add children + newNode.left = createTree(components, low, mid - 1, !splitX); + newNode.right = createTree(components, mid + 1, high, !splitX); + // pull up max values to this node + if (newNode.left != null) { + newNode.maxX = Math.max(newNode.maxX, newNode.left.maxX); + newNode.maxY = Math.max(newNode.maxY, newNode.left.maxY); + newNode.areaSqDegrees += newNode.left.areaSqDegrees; + } + if (newNode.right != null) { + newNode.maxX = Math.max(newNode.maxX, newNode.right.maxX); + newNode.maxY = Math.max(newNode.maxY, newNode.right.maxY); + newNode.areaSqDegrees += newNode.right.areaSqDegrees; + } + return newNode; + } + + /** + * Builds a EdgeTree from multipolygon + */ + public static EdgeTree create(Polygon... polygons) { + EdgeTree components[] = new EdgeTree[polygons.length]; + for (int i = 0; i < components.length; i++) { + Polygon gon = polygons[i]; + Polygon gonHoles[] = gon.getHoles(); + EdgeTree holes = null; + if (gonHoles.length > 0) { + holes = create(gonHoles); + } + components[i] = new EdgeTree(gon, holes); + } + return createTree(components, 0, components.length - 1, false); + } + + /** + * Internal tree node: represents polygon edge from lat1,lon1 to lat2,lon2. + * The sort value is {@code low}, which is the minimum latitude of the edge. + * {@code max} stores the maximum latitude of this edge or any children. + */ + static final class Edge { + // lat-lon pair (in original order) of the two vertices + final double lat1, lat2; + final double lon1, lon2; + /** + * min of this edge + */ + final double low; + /** + * max latitude of this edge or any children + */ + double max; + + /** + * left child edge, or null + */ + Edge left; + /** + * right child edge, or null + */ + Edge right; + + Edge(double lat1, double lon1, double lat2, double lon2, double low, double max) { + this.lat1 = lat1; + this.lon1 = lon1; + this.lat2 = lat2; + this.lon2 = lon2; + this.low = low; + this.max = max; + } + + /** + * Returns true if the point crosses this edge subtree an odd number of times + *

+ * See + * https://www.ecse.rpi.edu/~wrf/Research/Short_Notes/pnpoly.html for more information. + */ + // ported to java from https://www.ecse.rpi.edu/~wrf/Research/Short_Notes/pnpoly.html + // original code under the BSD license (https://www.ecse.rpi.edu/~wrf/Research/Short_Notes/pnpoly.html#License%20to%20Use) + // + // Copyright (c) 1970-2003, Wm. Randolph Franklin + // + // Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated + // documentation files (the "Software"), to deal in the Software without restriction, including without limitation + // the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and + // to permit persons to whom the Software is furnished to do so, subject to the following conditions: + // + // 1. Redistributions of source code must retain the above copyright + // notice, this list of conditions and the following disclaimers. + // 2. Redistributions in binary form must reproduce the above copyright + // notice in the documentation and/or other materials provided with + // the distribution. + // 3. The name of W. Randolph Franklin may not be used to endorse or + // promote products derived from this Software without specific + // prior written permission. + // + // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED + // TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + // THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF + // CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + // IN THE SOFTWARE. + boolean contains(double latitude, double longitude) { + // crossings algorithm is an odd-even algorithm, so we descend the tree xor'ing results along our path + boolean res = false; + if (latitude <= max) { + if (lat1 > latitude != lat2 > latitude) { + if (longitude < (lon1 - lon2) * (latitude - lat2) / (lat1 - lat2) + lon2) { + res = true; + } + } + if (left != null) { + res ^= left.contains(latitude, longitude); + } + if (right != null && latitude >= low) { + res ^= right.contains(latitude, longitude); + } + } + return res; + } + + /** + * Returns true if the box crosses any edge in this edge subtree + */ + boolean intersects(double minLat, double maxLat, double minLon, double maxLon) { + // we just have to cross one edge to answer the question, so we descend the tree and return when we do. + if (minLat <= max) { + // we compute line intersections of every polygon edge with every box line. + // if we find one, return true. + // for each box line (AB): + // for each poly line (CD): + // intersects = orient(C,D,A) * orient(C,D,B) <= 0 && orient(A,B,C) * orient(A,B,D) <= 0 + double cy = lat1; + double dy = lat2; + double cx = lon1; + double dx = lon2; + + // optimization: see if the rectangle is outside of the "bounding box" of the polyline at all + // if so, don't waste our time trying more complicated stuff + boolean outside = (cy < minLat && dy < minLat) || + (cy > maxLat && dy > maxLat) || + (cx < minLon && dx < minLon) || + (cx > maxLon && dx > maxLon); + if (outside == false) { + // does box's top edge intersect polyline? + // ax = minLon, bx = maxLon, ay = maxLat, by = maxLat + if (orient(cx, cy, dx, dy, minLon, maxLat) * orient(cx, cy, dx, dy, maxLon, maxLat) <= 0 && + orient(minLon, maxLat, maxLon, maxLat, cx, cy) * orient(minLon, maxLat, maxLon, maxLat, dx, dy) <= 0) { + return true; + } + + // does box's right edge intersect polyline? + // ax = maxLon, bx = maxLon, ay = maxLat, by = minLat + if (orient(cx, cy, dx, dy, maxLon, maxLat) * orient(cx, cy, dx, dy, maxLon, minLat) <= 0 && + orient(maxLon, maxLat, maxLon, minLat, cx, cy) * orient(maxLon, maxLat, maxLon, minLat, dx, dy) <= 0) { + return true; + } + + // does box's bottom edge intersect polyline? + // ax = maxLon, bx = minLon, ay = minLat, by = minLat + if (orient(cx, cy, dx, dy, maxLon, minLat) * orient(cx, cy, dx, dy, minLon, minLat) <= 0 && + orient(maxLon, minLat, minLon, minLat, cx, cy) * orient(maxLon, minLat, minLon, minLat, dx, dy) <= 0) { + return true; + } + + // does box's left edge intersect polyline? + // ax = minLon, bx = minLon, ay = minLat, by = maxLat + if (orient(cx, cy, dx, dy, minLon, minLat) * orient(cx, cy, dx, dy, minLon, maxLat) <= 0 && + orient(minLon, minLat, minLon, maxLat, cx, cy) * orient(minLon, minLat, minLon, maxLat, dx, dy) <= 0) { + return true; + } + } + + if (left != null) { + if (left.intersects(minLat, maxLat, minLon, maxLon)) { + return true; + } + } + + if (right != null && maxLat >= low) { + if (right.intersects(minLat, maxLat, minLon, maxLon)) { + return true; + } + } + } + return false; + } + +// /** Returns the relation between the two DAGs */ +// boolean crosses(Edge o) { +// return crossesLine(o.lat1, o.lon1, o.lat2, o.lon2) +// || (o.left != null && crosses(o.left)) +// || (o.right != null && crosses(o.right)); +// } + + boolean intersectsLine(double lineLat1, double lineLon1, double lineLat2, double lineLon2) { + // we just have to cross one edge to answer the question, so we descend the tree and return when we do. + double minLat, maxLat; + if (lineLat1 < lineLat2) { + minLat = lineLat1; + maxLat = lineLat2; + } else { + minLat = lineLat2; + maxLat = lineLat1; + } + + if (minLat <= max) { + // we compute line intersections of every polygon edge with every box line. + // if we find one, return true. + // for each box line (AB): + // for each poly line (CD): + // intersects = orient(C,D,A) * orient(C,D,B) <= 0 && orient(A,B,C) * orient(A,B,D) <= 0 + double cy = lat1; + double dy = lat2; + double cx = lon1; + double dx = lon2; + double minLon, maxLon; + if (lineLon1 < lineLon2) { + minLon = lineLon1; + maxLon = lineLon2; + } else { + minLon = lineLon2; + maxLon = lineLon1; + } + + // optimization: see if the rectangle is outside of the "bounding box" of the polyline at all + // if so, don't waste our time trying more complicated stuff + boolean outside = (cy < minLat && dy < minLat) || + (cy > maxLat && dy > maxLat) || + (cx < minLon && dx < minLon) || + (cx > maxLon && dx > maxLon); + if (outside == false) { + // does provided edge intersect polyline? + // ax = minLon, bx = maxLon, ay = maxLat, by = maxLat + if (orient(cx, cy, dx, dy, minLon, maxLat) * orient(cx, cy, dx, dy, maxLon, maxLat) <= 0 && + orient(minLon, maxLat, maxLon, maxLat, cx, cy) * orient(minLon, maxLat, maxLon, maxLat, dx, dy) <= 0) { + return true; + } + } + + if (left != null) { + if (left.intersectsLine(minLat, maxLat, minLon, maxLon)) { + return true; + } + } + + if (right != null && maxLat >= low) { + if (right.intersectsLine(minLat, maxLat, minLon, maxLon)) { + return true; + } + } + } + return false; + } + + @Override + public boolean equals(Object other) { + if (other == null || getClass() != other.getClass()) return false; + Edge o = getClass().cast(other); + if (left == null && o.left != null) return false; + if (left != null && left.equals(o.left) == false) return false; + if (right == null && o.right != null) return false; + if (right != null && right.equals(o.right) == false) return false; + return lat1 != o.lat1 + && lat2 != o.lat2 + && lon1 != o.lon1 + && lon2 != o.lon2 + && low != o.low + && max != o.max; + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + toString(sb, "(ROOT)", 0); + return sb.append("\n\n").toString(); + } + + public void toString(StringBuilder sb, String branch, int level) { + sb.append("(" + lat1 + ", " + lon1 + ") "); + sb.append("(" + lat2 + ", " + lon2 + ") "); + sb.append(branch + " - " + level++ + "\r\t"); + if (left != null) left.toString(sb, "[L]", level); + if (right != null) right.toString(sb, "[R]", level); + } + } + + /** + * Creates an edge interval tree from a set of polygon vertices. + * + * @return root node of the tree. + */ + private Edge createTree(double polyLats[], double polyLons[]) { + final Edge edges[] = new Edge[polyLats.length - 1]; + double area = 0; + for (int i = 1; i < polyLats.length; i++) { + double lat1 = polyLats[i - 1]; + double lon1 = polyLons[i - 1]; + double lat2 = polyLats[i]; + double lon2 = polyLons[i]; + edges[i - 1] = new Edge(lat1, lon1, lat2, lon2, Math.min(lat1, lat2), Math.max(lat1, lat2)); + area += lon1 * lat2 - lon2 * lat1; + } + this.areaSqDegrees = StrictMath.abs(area) * 0.5d; + // sort the edges then build a balanced tree from them + Arrays.sort(edges, (left, right) -> { + int ret = Double.compare(left.low, right.low); + if (ret == 0) { + ret = Double.compare(left.max, right.max); + } + return ret; + }); + return createTree(edges, 0, edges.length - 1); + } + + /** + * Creates tree from sorted edges (with range low and high inclusive) + */ + private static Edge createTree(Edge edges[], int low, int high) { + if (low > high) { + return null; + } + // add midpoint + int mid = (low + high) >>> 1; + Edge newNode = edges[mid]; + // add children + newNode.left = createTree(edges, low, mid - 1); + newNode.right = createTree(edges, mid + 1, high); + // pull up max values to this node + if (newNode.left != null) { + newNode.max = Math.max(newNode.max, newNode.left.max); + } + if (newNode.right != null) { + newNode.max = Math.max(newNode.max, newNode.right.max); + } + return newNode; + } + + /** + * Returns a positive value if points a, b, and c are arranged in counter-clockwise order, + * negative value if clockwise, zero if collinear. + */ + // see the "Orient2D" method described here: + // http://www.cs.berkeley.edu/~jrs/meshpapers/robnotes.pdf + // https://www.cs.cmu.edu/~quake/robust.html + // Note that this one does not yet have the floating point tricks to be exact! + private static int orient(double ax, double ay, double bx, double by, double cx, double cy) { + double v1 = (bx - ax) * (cy - ay); + double v2 = (cx - ax) * (by - ay); + if (v1 > v2) { + return 1; + } else if (v1 < v2) { + return -1; + } else { + return 0; + } + } + + @Override + public boolean equals(Object other) { + if (other == null || getClass() != other.getClass()) return false; + EdgeTree o = getClass().cast(other); + if (left == null && o.left != null) return false; + if (left != null && left.equals(o.left) == false) return false; + if (right == null && o.right != null) return false; + if (right != null && right.equals(o.right) == false) return false; + if (holes == null && o.holes != null) return false; + if (holes != null && holes.equals(o.holes) == false) return false; + if (tree == null && o.tree != null) return false; + if (tree != null && tree.equals(o.tree) == false) return false; + return maxLat == o.maxLat + && maxLon == o.maxLon + && maxX == o.maxX + && maxY == o.maxY + && minLat == o.minLat + && minLon == o.minLon + && splitX == o.splitX; + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + toString(sb); + return sb.toString(); + } + + public void toString(StringBuilder sb) { + sb.append(tree.toString()); + if (left != null) left.toString(sb); + if (right != null) right.toString(sb); + } + + public static void main(String[] args) { + Polygon p = new Polygon( + new double[]{10, 9, 7, 4, 2, 6, 10}, + new double[]{8, 10, 15, 11, 8, 3, 8}, + new Polygon( + new double[]{5, 7, 5, 4, 5}, + new double[]{9, 10, 11, 9, 9} + ) + ); + MultiPolygon mp = new MultiPolygon(new Polygon[]{p}); + + Relation r = mp.relate(-5, 5, -5, 5); + System.out.println(mp.tree); + System.out.println(mp.tree.holes); + + Polygon p2 = new Polygon( + new double[]{5, 9, 6, 9, 10, 7, 5}, + new double[]{16, 20, 21, 22, 20, 15, 16} + ); + r = p2.relate(-5, 5, -5, 5); + + //System.out.println(mp.tree.relate(p2.tree)); + + // +// Polygon p2 = new Polygon( +// new double[] {7, 7, 6, 6, 7}, +// new double[] {7, 8, 8, 7, 7} +// ); +// Relation r2 = p.relate(p2); +// +// assert r == Relation.CELL_CROSSES_QUERY; + } +} diff --git a/libs/geo/src/main/java/org/elasticsearch/geo/geometry/GeoShape.java b/libs/geo/src/main/java/org/elasticsearch/geo/geometry/GeoShape.java new file mode 100644 index 0000000000000..84ffc16a88a0e --- /dev/null +++ b/libs/geo/src/main/java/org/elasticsearch/geo/geometry/GeoShape.java @@ -0,0 +1,168 @@ +/* + * 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.geo.geometry; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.Objects; + +import org.elasticsearch.geo.parsers.WKBParser; +import org.elasticsearch.geo.parsers.WKBParser.ByteOrder; +import org.elasticsearch.geo.parsers.WKTParser; +import org.apache.lucene.index.PointValues; +import org.apache.lucene.store.OutputStreamDataOutput; + +/** + */ +public abstract class GeoShape { + protected Rectangle boundingBox; + protected double area = Double.NaN; + + public Rectangle getBoundingBox() { + return boundingBox; + } + + public double minLat() { + return boundingBox.minLat; + } + + public double maxLat() { + return boundingBox.maxLat; + } + + public double minLon() { + return boundingBox.minLon; + } + + public double maxLon() { + return boundingBox.maxLon; + } + + + public Point getCenter() { + return boundingBox.getCenter(); + } + + public double getArea() { + if (hasArea()) { + if (Double.isNaN(area)) { + area = computeArea(); + } + return area; + } + throw new UnsupportedOperationException(type() + " does not have an area"); + } + + protected double computeArea() { + throw new UnsupportedOperationException(type() + " does not have an area"); + } + + public abstract boolean hasArea(); + + public abstract ShapeType type(); + + public abstract Relation relate(double minLat, double maxLat, double minLon, double maxLon); + + public abstract Relation relate(GeoShape shape); + + interface ConnectedComponent { + EdgeTree createEdgeTree(); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof GeoShape)) return false; + GeoShape geoShape = (GeoShape) o; + return Double.compare(geoShape.area, area) == 0 && + Objects.equals(boundingBox, geoShape.boundingBox); + } + + @Override + public int hashCode() { + return Objects.hash(boundingBox, area); + } + + public String toWKT() { + StringBuilder sb = new StringBuilder(); + sb.append(type().wktName()); + sb.append(WKTParser.SPACE); + sb.append(contentToWKT()); + return sb.toString(); + } + + public ByteArrayOutputStream toWKB() { + return toWKB(null); + } + + public ByteArrayOutputStream toWKB(ByteArrayOutputStream reuse) { + if (reuse == null) { + reuse = new ByteArrayOutputStream(); + } + try (OutputStreamDataOutput out = new OutputStreamDataOutput(reuse)) { + appendWKB(out); + } catch (IOException e) { + throw new RuntimeException(e); // not possible + } + return reuse; + } + + protected abstract StringBuilder contentToWKT(); + + private void appendWKB(OutputStreamDataOutput out) throws IOException { + out.writeVInt(WKBParser.ByteOrder.XDR.ordinal()); // byteOrder + out.writeVInt(type().wkbOrdinal()); // shapeType ordinal + appendWKBContent(out); + } + + protected abstract void appendWKBContent(OutputStreamDataOutput out) throws IOException; + + public enum Relation { + DISJOINT(PointValues.Relation.CELL_OUTSIDE_QUERY), + INTERSECTS(PointValues.Relation.CELL_CROSSES_QUERY), + CONTAINS(PointValues.Relation.CELL_CROSSES_QUERY), + WITHIN(PointValues.Relation.CELL_INSIDE_QUERY), + CROSSES(PointValues.Relation.CELL_CROSSES_QUERY); + + // used to translate between PointValues.Relation and full geo relations + private final PointValues.Relation pointsRelation; + + Relation(PointValues.Relation pointsRelation) { + this.pointsRelation = pointsRelation; + } + + public PointValues.Relation toPointsRelation() { + return pointsRelation; + } + + public boolean intersects() { + return this != DISJOINT; + } + + public Relation transpose() { + if (this == CONTAINS) { + return WITHIN; + } else if (this == WITHIN) { + return CONTAINS; + } + return this; + } + } +} diff --git a/libs/geo/src/main/java/org/elasticsearch/geo/geometry/GeoShapeCollection.java b/libs/geo/src/main/java/org/elasticsearch/geo/geometry/GeoShapeCollection.java new file mode 100644 index 0000000000000..605905bf2a985 --- /dev/null +++ b/libs/geo/src/main/java/org/elasticsearch/geo/geometry/GeoShapeCollection.java @@ -0,0 +1,98 @@ +/* + * 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.geo.geometry; + +import java.io.IOException; +import java.io.UnsupportedEncodingException; + +import org.apache.lucene.store.OutputStreamDataOutput; + +public class GeoShapeCollection extends GeoShape { + protected GeoShape[] shapes; + private final boolean hasArea; + + public GeoShapeCollection(GeoShape... shapes) { + if (shapes.length < 1) { + throw new IllegalArgumentException("must have at least one shape to create a " + type()); + } + // nocommit - CHECK THIS + this.shapes = shapes.clone(); + + double minLat = Double.POSITIVE_INFINITY; + double maxLat = Double.NEGATIVE_INFINITY; + double minLon = Double.POSITIVE_INFINITY; + double maxLon = Double.NEGATIVE_INFINITY; + + Rectangle bbox; + boolean hasArea = false; + for (GeoShape shape : shapes) { + if (hasArea == false && shape.hasArea()) { + hasArea = true; + } + bbox = shape.getBoundingBox(); + minLat = StrictMath.min(minLat, bbox.minLat()); + maxLat = StrictMath.max(maxLat, bbox.maxLat()); + minLon = StrictMath.min(minLon, bbox.minLon()); + maxLon = StrictMath.max(maxLon, bbox.maxLon()); + } + this.hasArea = hasArea; + this.boundingBox = new Rectangle(minLat, maxLat, minLon, maxLon); + } + + @Override + public boolean hasArea() { + return hasArea; + } + + @Override + protected double computeArea() { + double area = 0; + for (GeoShape shape : shapes) { + if (shape.hasArea()) { + area += shape.getArea(); + } + } + return area; + } + + @Override + public ShapeType type() { + return ShapeType.GEOMETRYCOLLECTION; + } + + @Override + public Relation relate(double minLat, double maxLat, double minLon, double maxLon) { + throw new UnsupportedOperationException("not yet implemented"); + } + + @Override + public Relation relate(GeoShape shape) { + throw new UnsupportedOperationException("not yet implemented"); + } + + @Override + protected StringBuilder contentToWKT() { + throw new UnsupportedOperationException("not yet implemented"); + } + + @Override + protected void appendWKBContent(OutputStreamDataOutput out) throws IOException { + throw new UnsupportedEncodingException("not yet implemented"); + } +} diff --git a/libs/geo/src/main/java/org/elasticsearch/geo/geometry/Line.java b/libs/geo/src/main/java/org/elasticsearch/geo/geometry/Line.java new file mode 100644 index 0000000000000..5c461bd16d8c0 --- /dev/null +++ b/libs/geo/src/main/java/org/elasticsearch/geo/geometry/Line.java @@ -0,0 +1,97 @@ +/* + * 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.geo.geometry; + +import java.io.IOException; + +import org.elasticsearch.geo.geometry.GeoShape.ConnectedComponent; +import org.elasticsearch.geo.parsers.WKBParser; +import org.apache.lucene.store.OutputStreamDataOutput; + +/** + * Represents a Line on the earth's surface in lat/lon decimal degrees. + */ +public class Line extends MultiPoint implements ConnectedComponent { + EdgeTree tree; + + public Line(double[] lats, double[] lons) { + super(lats, lons); + } + + @Override + public Relation relate(double minLat, double maxLat, double minLon, double maxLon) { + if (tree == null) { + tree = createEdgeTree(); + } + return tree.relate(minLat, maxLat, minLon, maxLon); + } + + @Override + public ShapeType type() { + return ShapeType.LINESTRING; + } + + public Relation relate(GeoShape other) { + // not yet implemented + throw new UnsupportedOperationException("not yet able to relate other GeoShape types to linestrings"); + } + + @Override + public EdgeTree createEdgeTree() { + return new EdgeTree(this); + + // NOCOMMIT +// EdgeTree components[] = new EdgeTree[lines.length]; +// for (int i = 0; i < components.length; i++) { +// Line gon = lines[i]; +// components[i] = new EdgeTree(gon); +// } +// return EdgeTree.createTree(components, 0, components.length - 1, false); + } + + @Override + public boolean equals(Object other) { + if (super.equals(other) == false) return false; + Line o = getClass().cast(other); + if ((tree == null) != (o.tree == null)) return false; + return tree != null ? tree.equals(o.tree) : true; + } + + @Override + public int hashCode() { + int result = super.hashCode(); + result = 31 * result + (tree != null ? tree.hashCode() : 0); + return result; + } + + @Override + protected void appendWKBContent(OutputStreamDataOutput out) throws IOException { + lineToWKB(lats, lons, out, false); + } + + public static void lineToWKB(final double[] lats, final double[] lons, OutputStreamDataOutput out, boolean writeHeader) throws IOException { + if (writeHeader == true) { + out.writeVInt(WKBParser.ByteOrder.XDR.ordinal()); + out.writeVInt(ShapeType.LINESTRING.wkbOrdinal()); + } + out.writeVInt(lats.length); // number of points + pointsToWKB(lats, lons, out, false); + } +} diff --git a/libs/geo/src/main/java/org/elasticsearch/geo/geometry/MultiLine.java b/libs/geo/src/main/java/org/elasticsearch/geo/geometry/MultiLine.java new file mode 100644 index 0000000000000..997f1cda1dae7 --- /dev/null +++ b/libs/geo/src/main/java/org/elasticsearch/geo/geometry/MultiLine.java @@ -0,0 +1,150 @@ +/* + * 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.geo.geometry; + +import java.io.IOException; +import java.util.Arrays; + +import org.elasticsearch.geo.geometry.GeoShape.ConnectedComponent; +import org.elasticsearch.geo.parsers.WKBParser; +import org.elasticsearch.geo.parsers.WKTParser; +import org.apache.lucene.index.PointValues.Relation; +import org.apache.lucene.store.OutputStreamDataOutput; + +/** + * Represents a MultiLine geometry object on the earth's surface. + */ +public class MultiLine extends GeoShape implements ConnectedComponent { + EdgeTree tree; + Line[] lines; + + public MultiLine(Line... lines) { + this.lines = lines.clone(); + // compute bounding box + double minLat = Double.POSITIVE_INFINITY; + double maxLat = Double.NEGATIVE_INFINITY; + double minLon = Double.POSITIVE_INFINITY; + double maxLon = Double.NEGATIVE_INFINITY; + for (Line l : lines) { + minLat = Math.min(l.minLat(), minLat); + maxLat = Math.max(l.maxLat(), maxLat); + minLon = Math.min(l.minLon(), minLon); + maxLon = Math.max(l.maxLon(), maxLon); + } + boundingBox = new Rectangle(minLat, maxLat, minLon, maxLon); + } + + public int length() { + return lines.length; + } + + public Line get(int index) { + checkVertexIndex(index); + return lines[index]; + } + + protected void checkVertexIndex(final int i) { + if (i >= lines.length) { + throw new IllegalArgumentException("Index " + i + " is outside the bounds of the " + lines.length + " shapes"); + } + } + + @Override + public ShapeType type() { + return ShapeType.MULTILINESTRING; + } + + @Override + public boolean hasArea() { + return false; + } + + @Override + public EdgeTree createEdgeTree() { + EdgeTree components[] = new EdgeTree[lines.length]; + for (int i = 0; i < components.length; i++) { + Line line = lines[i]; + components[i] = new EdgeTree(line); + } + return EdgeTree.createTree(components, 0, components.length - 1, false); + } + + @Override + public Relation relate(double minLat, double maxLat, double minLon, double maxLon) { + if (tree == null) { + tree = createEdgeTree(); + } + return tree.relate(minLat, maxLat, minLon, maxLon).transpose(); + } + + public Relation relate(GeoShape shape) { + return null; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + if (!super.equals(o)) return false; + + MultiLine multiLine = (MultiLine) o; + + if (!tree.equals(multiLine.tree)) return false; + // Probably incorrect - comparing Object[] arrays with Arrays.equals + return Arrays.equals(lines, multiLine.lines); + } + + @Override + public int hashCode() { + int result = super.hashCode(); + result = 31 * result + tree.hashCode(); + result = 31 * result + Arrays.hashCode(lines); + return result; + } + + @Override + protected StringBuilder contentToWKT() { + final StringBuilder sb = new StringBuilder(); + if (lines.length == 0) { + sb.append(WKTParser.EMPTY); + } else { + sb.append(WKTParser.LPAREN); + if (lines.length > 0) { + sb.append(MultiPoint.coordinatesToWKT(lines[0].lats, lines[0].lons)); + } + for (int i = 1; i < lines.length; ++i) { + sb.append(WKTParser.COMMA); + sb.append(MultiPoint.coordinatesToWKT(lines[i].lats, lines[i].lons)); + } + sb.append(WKTParser.RPAREN); + } + return sb; + } + + @Override + protected void appendWKBContent(OutputStreamDataOutput out) throws IOException { + int numLines = length(); + out.writeVInt(numLines); + for (int i = 0; i < numLines; ++i) { + Line line = lines[i]; + Line.lineToWKB(line.lats, line.lons, out, true); + } + } +} diff --git a/libs/geo/src/main/java/org/elasticsearch/geo/geometry/MultiPoint.java b/libs/geo/src/main/java/org/elasticsearch/geo/geometry/MultiPoint.java new file mode 100644 index 0000000000000..ae745d6f90c49 --- /dev/null +++ b/libs/geo/src/main/java/org/elasticsearch/geo/geometry/MultiPoint.java @@ -0,0 +1,216 @@ +/* + * 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.geo.geometry; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Iterator; + +import org.elasticsearch.geo.GeoUtils; +import org.elasticsearch.geo.parsers.WKBParser; +import org.elasticsearch.geo.parsers.WKTParser; +import org.apache.lucene.store.OutputStreamDataOutput; + +/** + * Represents a MultiPoint object on the earth's surface in decimal degrees. + */ +public class MultiPoint extends GeoShape implements Iterable { + protected final double[] lats; + protected final double[] lons; + + public MultiPoint(double[] lats, double[] lons) { + checkLatArgs(lats); + checkLonArgs(lons); + if (lats.length != lons.length) { + throw new IllegalArgumentException("lats and lons must be equal length"); + } + for (int i = 0; i < lats.length; i++) { + GeoUtils.checkLatitude(lats[i]); + GeoUtils.checkLongitude(lons[i]); + } + this.lats = lats.clone(); + this.lons = lons.clone(); + + // compute bounding box + double minLat = Math.min(lats[0], lats[lats.length - 1]); + double maxLat = Math.max(lats[0], lats[lats.length - 1]); + double minLon = Math.min(lons[0], lons[lats.length - 1]); + double maxLon = Math.max(lons[0], lons[lats.length - 1]); + + double windingSum = 0d; + final int numPts = lats.length - 1; + for (int i = 1, j = 0; i < numPts; j = i++) { + minLat = Math.min(lats[i], minLat); + maxLat = Math.max(lats[i], maxLat); + minLon = Math.min(lons[i], minLon); + maxLon = Math.max(lons[i], maxLon); + // compute signed area for orientation + windingSum += (lons[j] - lons[numPts]) * (lats[i] - lats[numPts]) + - (lats[j] - lats[numPts]) * (lons[i] - lons[numPts]); + } + this.boundingBox = new Rectangle(minLat, maxLat, minLon, maxLon); + } + + @Override + public ShapeType type() { + return ShapeType.MULTIPOINT; + } + + protected void checkLatArgs(double[] lats) { + if (lats == null) { + throw new IllegalArgumentException("lats must not be null"); + } + if (lats.length < 2) { + throw new IllegalArgumentException("at least 2 points are required"); + } + } + + protected void checkLonArgs(double[] lons) { + if (lons == null) { + throw new IllegalArgumentException("lons must not be null"); + } + } + + public int numPoints() { + return lats.length; + } + + private void checkVertexIndex(final int i) { + if (i >= lats.length) { + throw new IllegalArgumentException("Index " + i + " is outside the bounds of the " + lats.length + " vertices "); + } + } + + public double getLat(int vertex) { + checkVertexIndex(vertex); + return lats[vertex]; + } + + public double getLon(int vertex) { + checkVertexIndex(vertex); + return lons[vertex]; + } + + /** + * Returns a copy of the internal latitude array + */ + public double[] getLats() { + return lats.clone(); + } + + /** + * Returns a copy of the internal longitude array + */ + public double[] getLons() { + return lons.clone(); + } + + public Relation relate(double minLat, double maxLat, double minLon, double maxLon) { + // note: this relate is not used; points are indexed as separate POINT types + // note: if needed, we could build an in-memory BKD for each MultiPoint type + throw new UnsupportedOperationException("use Point.relate instead"); + } + + public Relation relate(GeoShape shape) { + return null; + } + + @Override + public boolean hasArea() { + return false; + } + + @Override + public boolean equals(Object other) { + if (super.equals(other) == false) return false; + MultiPoint o = getClass().cast(other); + return Arrays.equals(lats, o.lats) && Arrays.equals(lons, o.lons); + } + + @Override + public int hashCode() { + int result = super.hashCode(); + result = 31 * result + Arrays.hashCode(lats); + result = 31 * result + Arrays.hashCode(lons); + return result; + } + + @Override + protected StringBuilder contentToWKT() { + return coordinatesToWKT(lats, lons); + } + + protected static StringBuilder coordinatesToWKT(final double[] lats, final double[] lons) { + StringBuilder sb = new StringBuilder(); + if (lats.length == 0) { + sb.append(WKTParser.EMPTY); + } else { + // walk through coordinates: + sb.append(WKTParser.LPAREN); + sb.append(Point.coordinateToWKT(lats[0], lons[0])); + for (int i = 1; i < lats.length; ++i) { + sb.append(WKTParser.COMMA); + sb.append(WKTParser.SPACE); + sb.append(Point.coordinateToWKT(lats[i], lons[i])); + } + sb.append(WKTParser.RPAREN); + } + + return sb; + } + + @Override + protected void appendWKBContent(OutputStreamDataOutput out) throws IOException { + out.writeVInt(numPoints()); + pointsToWKB(lats, lons, out, true); + } + + protected static OutputStreamDataOutput pointsToWKB(final double[] lats, final double[] lons, + OutputStreamDataOutput out, boolean writeHeader) throws IOException { + final int numPoints = lats.length; + for (int i = 0; i < numPoints; ++i) { + if (writeHeader == true) { + // write header for each coordinate (req. as part of spec for MultiPoints but not LineStrings) + out.writeVInt(WKBParser.ByteOrder.XDR.ordinal()); + out.writeVInt(ShapeType.POINT.wkbOrdinal()); + } + // write coordinates + Point.coordinateToWKB(lats[i], lons[i], out); + } + return out; + } + + @Override + public Iterator iterator() { + return new Iterator() { + int i = 0; + + @Override + public boolean hasNext() { + return i < numPoints(); + } + + @Override + public Point next() { + return new Point(lats[i], lons[i]); + } + }; + } +} diff --git a/libs/geo/src/main/java/org/elasticsearch/geo/geometry/MultiPolygon.java b/libs/geo/src/main/java/org/elasticsearch/geo/geometry/MultiPolygon.java new file mode 100644 index 0000000000000..77510b7c2e652 --- /dev/null +++ b/libs/geo/src/main/java/org/elasticsearch/geo/geometry/MultiPolygon.java @@ -0,0 +1,143 @@ +/* + * 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.geo.geometry; + +import java.io.IOException; + +import org.elasticsearch.geo.parsers.WKTParser; +import org.apache.lucene.store.OutputStreamDataOutput; + +/** + * Created by nknize on 2/27/17. + */ +public class MultiPolygon extends MultiLine { + Predicate.PolygonPredicate predicate; + + public MultiPolygon(Polygon... polygons) { + super(polygons); + } + + @Override + public ShapeType type() { + return ShapeType.MULTIPOLYGON; + } + + @Override + public int length() { + return lines.length; + } + + @Override + public Polygon get(int index) { + checkVertexIndex(index); + return (Polygon) (lines[index]); + } + + @Override + public EdgeTree createEdgeTree() { + Polygon[] polygons = (Polygon[]) this.lines; + this.tree = Polygon.createEdgeTree(polygons); + predicate = Predicate.PolygonPredicate.create(this.boundingBox, tree); + return predicate.tree; + } + + public boolean pointInside(int encodedLat, int encodedLon) { + return predicate.test(encodedLat, encodedLon); + } + + @Override + public boolean hasArea() { + return true; + } + + @Override + public double computeArea() { + assertEdgeTree(); + return this.tree.getArea(); + } + + protected void assertEdgeTree() { + if (this.tree == null) { + final Polygon[] polygons = (Polygon[]) this.lines; + tree = Polygon.createEdgeTree(polygons); + } + } + + // private EdgeTree createEdgeTree(Polygon... polygons) { +// EdgeTree components[] = new EdgeTree[polygons.length]; +// for (int i = 0; i < components.length; i++) { +// Polygon gon = polygons[i]; +// Polygon gonHoles[] = gon.getHoles(); +// EdgeTree holes = null; +// if (gonHoles.length > 0) { +// holes = createEdgeTree(gonHoles); +// } +// components[i] = new EdgeTree(gon, holes); +// } +// return EdgeTree.createTree(components, 0, components.length - 1, false); +// } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + if (!super.equals(o)) return false; + + MultiPolygon that = (MultiPolygon) o; + return predicate.equals(that.predicate); + } + + @Override + public int hashCode() { + int result = super.hashCode(); + result = 31 * result + predicate.hashCode(); + return result; + } + + @Override + protected StringBuilder contentToWKT() { + final StringBuilder sb = new StringBuilder(); + Polygon[] polygons = (Polygon[]) lines; + if (polygons.length == 0) { + sb.append(WKTParser.EMPTY); + } else { + sb.append(WKTParser.LPAREN); + if (polygons.length > 0) { + sb.append(Polygon.polygonToWKT(polygons[0])); + } + for (int i = 1; i < polygons.length; ++i) { + sb.append(WKTParser.COMMA); + sb.append(Polygon.polygonToWKT(polygons[i])); + } + sb.append(WKTParser.RPAREN); + } + return sb; + } + + @Override + protected void appendWKBContent(OutputStreamDataOutput out) throws IOException { + int numPolys = length(); + out.writeVInt(numPolys); + for (int i = 0; i < numPolys; ++i) { + Polygon polygon = this.get(i); + Polygon.polygonToWKB(polygon, out, true); + } + } +} diff --git a/libs/geo/src/main/java/org/elasticsearch/geo/geometry/Point.java b/libs/geo/src/main/java/org/elasticsearch/geo/geometry/Point.java new file mode 100644 index 0000000000000..8aa4a67e964ac --- /dev/null +++ b/libs/geo/src/main/java/org/elasticsearch/geo/geometry/Point.java @@ -0,0 +1,142 @@ +/* + * 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.geo.geometry; + +import java.io.IOException; + +import org.elasticsearch.geo.GeoUtils; +import org.elasticsearch.geo.parsers.WKTParser; +import org.apache.lucene.store.OutputStreamDataOutput; + +/** + * Represents a Point on the earth's surface in decimal degrees. + */ +public class Point extends GeoShape { + protected final double lat; + protected final double lon; + + public Point(double lat, double lon) { + GeoUtils.checkLatitude(lat); + GeoUtils.checkLongitude(lon); + this.lat = lat; + this.lon = lon; + this.boundingBox = null; + } + + @Override + public ShapeType type() { + return ShapeType.POINT; + } + + public double lat() { + return lat; + } + + public double lon() { + return lon; + } + + public double minLat() { + return lat; + } + + public double maxLat() { + return lat; + } + + public double minLon() { + return lon; + } + + public double maxLon() { + return lon; + } + + @Override + public Rectangle getBoundingBox() { + throw new UnsupportedOperationException("Points do not have a bounding box"); + } + + @Override + public Point getCenter() { + return this; + } + + @Override + public boolean hasArea() { + return false; + } + + public Relation relate(double minLat, double maxLat, double minLon, double maxLon) { + if (lat < minLat || lat > maxLat || lon < minLon || lon > maxLon) { + return Relation.DISJOINT; + } + return Relation.WITHIN; + } + + public Relation relate(GeoShape shape) { + return null; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + if (!super.equals(o)) return false; + + Point point = (Point) o; + + if (Double.compare(point.lat, lat) != 0) return false; + return Double.compare(point.lon, lon) == 0; + } + + @Override + public int hashCode() { + int result = super.hashCode(); + long temp; + temp = Double.doubleToLongBits(lat); + result = 31 * result + (int) (temp ^ (temp >>> 32)); + temp = Double.doubleToLongBits(lon); + result = 31 * result + (int) (temp ^ (temp >>> 32)); + return result; + } + + @Override + protected StringBuilder contentToWKT() { + return coordinateToWKT(lat, lon); + } + + protected static StringBuilder coordinateToWKT(final double lat, final double lon) { + final StringBuilder sb = new StringBuilder(); + sb.append(lon + WKTParser.SPACE + lat); + return sb; + } + + @Override + protected void appendWKBContent(OutputStreamDataOutput out) throws IOException { + coordinateToWKB(lat, lon, out); + } + + public static OutputStreamDataOutput coordinateToWKB(double lat, double lon, OutputStreamDataOutput out) throws IOException { + out.writeVLong(Double.doubleToRawLongBits(lon)); // lon + out.writeVLong(Double.doubleToRawLongBits(lat)); // lat + return out; + } +} diff --git a/libs/geo/src/main/java/org/elasticsearch/geo/geometry/Polygon.java b/libs/geo/src/main/java/org/elasticsearch/geo/geometry/Polygon.java new file mode 100644 index 0000000000000..135e7a78f3edb --- /dev/null +++ b/libs/geo/src/main/java/org/elasticsearch/geo/geometry/Polygon.java @@ -0,0 +1,242 @@ +/* + * 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.geo.geometry; + +import java.io.IOException; +import java.text.ParseException; +import java.util.Arrays; + +import org.elasticsearch.geo.parsers.SimpleGeoJSONPolygonParser; +import org.elasticsearch.geo.parsers.WKBParser; +import org.apache.lucene.store.OutputStreamDataOutput; + +/** + * Represents a closed polygon on the earth's surface. You can either construct the Polygon directly yourself with {@code double[]} + * coordinates, or use {@link Polygon#fromGeoJSON} if you have a polygon already encoded as a + * GeoJSON string. + *

+ * NOTES: + *

    + *
  1. Coordinates must be in clockwise order, except for holes. Holes must be in counter-clockwise order. + *
  2. The polygon must be closed: the first and last coordinates need to have the same values. + *
  3. The polygon must not be self-crossing, otherwise may result in unexpected behavior. + *
  4. All latitude/longitude values must be in decimal degrees. + *
  5. Polygons cannot cross the 180th meridian. Instead, use two polygons: one on each side. + *
  6. For more advanced GeoSpatial indexing and query operations see the {@code spatial-extras} module + *
+ */ +public final class Polygon extends Line { + private final Polygon[] holes; + + /** + * Creates a new Polygon from the supplied latitude/longitude array, and optionally any holes. + */ + public Polygon(double[] polyLats, double[] polyLons, Polygon... holes) { + super(polyLats, polyLons); + if (holes == null) { + throw new IllegalArgumentException("holes must not be null"); + } + if (polyLats[0] != polyLats[polyLats.length - 1]) { + throw new IllegalArgumentException("first and last points of the polygon must be the same (it must close itself): polyLats[0]=" + polyLats[0] + " polyLats[" + (polyLats.length - 1) + "]=" + polyLats[polyLats.length - 1]); + } + if (polyLons[0] != polyLons[polyLons.length - 1]) { + throw new IllegalArgumentException("first and last points of the polygon must be the same (it must close itself): polyLons[0]=" + polyLons[0] + " polyLons[" + (polyLons.length - 1) + "]=" + polyLons[polyLons.length - 1]); + } + for (int i = 0; i < holes.length; i++) { + Polygon inner = holes[i]; + if (inner.holes.length > 0) { + throw new IllegalArgumentException("holes may not contain holes: polygons may not nest."); + } + } + this.holes = holes.clone(); + } + + @Override + public ShapeType type() { + return ShapeType.POLYGON; + } + + @Override + protected void checkLatArgs(final double[] lats) { + super.checkLatArgs(lats); + if (lats.length < 4) { + throw new IllegalArgumentException("at least 4 polygon points required"); + } + } + + @Override + protected void checkLonArgs(final double[] lons) { + super.checkLonArgs(lons); + if (lons.length < 4) { + // being pedantic; the order of operations preclude this check, but we should do it anyway + throw new IllegalArgumentException("at least 4 polygon points required"); + } + } + + /** + * Returns a copy of the internal latitude array + */ + public double[] getPolyLats() { + return getLats(); + } + + /** + * Returns a copy of the internal longitude array + */ + public double[] getPolyLons() { + return getLons(); + } + + /** + * Returns a copy of the internal holes array + */ + public Polygon[] getHoles() { + return holes.clone(); + } + + public int numHoles() { + return holes.length; + } + + public Polygon getHole(int i) { + if (i >= holes.length) { + throw new IllegalArgumentException("Index " + i + " is outside the bounds of the " + holes.length + " polygon holes"); + } + return holes[i]; + } + + /** + * Lazily builds an EdgeTree from multipolygon + */ + public static EdgeTree createEdgeTree(Polygon... polygons) { + EdgeTree components[] = new EdgeTree[polygons.length]; + for (int i = 0; i < components.length; i++) { + Polygon gon = polygons[i]; + Polygon gonHoles[] = gon.getHoles(); + EdgeTree holes = null; + if (gonHoles.length > 0) { + holes = createEdgeTree(gonHoles); + } + components[i] = new EdgeTree(gon, holes); + } + return EdgeTree.createTree(components, 0, components.length - 1, false); + } + + @Override + public boolean hasArea() { + return true; + } + + @Override + protected double computeArea() { + assertEdgeTree(); + return this.tree.getArea(); + } + + @Override + public Relation relate(double minLat, double maxLat, double minLon, double maxLon) { + assertEdgeTree(); + Relation r = tree.relate(minLat, maxLat, minLon, maxLon); + return r.transpose(); + } + + protected void assertEdgeTree() { + if (this.tree == null) { + tree = createEdgeTree(this); + } + } + + @Override + public int hashCode() { + final int prime = 31; + int result = super.hashCode(); + result = prime * result + Arrays.hashCode(holes); + return result; + } + + @Override + public boolean equals(Object obj) { + if (super.equals(obj) == false) return false; + Polygon other = (Polygon) obj; + if (!Arrays.equals(holes, other.holes)) return false; + return true; + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(super.toString()); + if (holes.length > 0) { + sb.append(", holes="); + sb.append(Arrays.toString(holes)); + } + return sb.toString(); + } + + protected static StringBuilder polygonToWKT(final Polygon polygon) { + StringBuilder sb = new StringBuilder(); + sb.append('('); + sb.append(MultiPoint.coordinatesToWKT(polygon.lats, polygon.lons)); + Polygon[] holes = polygon.getHoles(); + for (int i = 0; i < holes.length; ++i) { + sb.append(", "); + sb.append(MultiPoint.coordinatesToWKT(holes[i].lats, holes[i].lons)); + } + sb.append(')'); + return sb; + } + + @Override + protected StringBuilder contentToWKT() { + return polygonToWKT(this); + } + + /** + * Parses a standard GeoJSON polygon string. The type of the incoming GeoJSON object must be a Polygon or MultiPolygon, optionally + * embedded under a "type: Feature". A Polygon will return as a length 1 array, while a MultiPolygon will be 1 or more in length. + * + *

See the GeoJSON specification. + */ + public static Polygon[] fromGeoJSON(String geojson) throws ParseException { + return new SimpleGeoJSONPolygonParser(geojson).parse(); + } + + @Override + protected void appendWKBContent(OutputStreamDataOutput out) throws IOException { + polygonToWKB(this, out, false); + } + + public static void polygonToWKB(final Polygon polygon, OutputStreamDataOutput out, + final boolean writeHeader) throws IOException { + if (writeHeader == true) { + out.writeVInt(WKBParser.ByteOrder.XDR.ordinal()); + out.writeVInt(ShapeType.POLYGON.wkbOrdinal()); + } + int numHoles = polygon.numHoles(); + out.writeVInt(numHoles + 1); // number rings + // write shell + Line.lineToWKB(polygon.lats, polygon.lons, out, false); + // write holes + Polygon hole; + for (int i = 0; i < numHoles; ++i) { + hole = polygon.getHole(i); + Line.lineToWKB(hole.lats, hole.lons, out, false); + } + } +} diff --git a/libs/geo/src/main/java/org/elasticsearch/geo/geometry/Predicate.java b/libs/geo/src/main/java/org/elasticsearch/geo/geometry/Predicate.java new file mode 100644 index 0000000000000..59822fe7ec1fc --- /dev/null +++ b/libs/geo/src/main/java/org/elasticsearch/geo/geometry/Predicate.java @@ -0,0 +1,254 @@ +/* + * 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.geo.geometry; + +import java.util.function.Function; + +import org.elasticsearch.geo.GeoUtils; +import org.elasticsearch.geo.geometry.GeoShape.Relation; +import org.apache.lucene.util.SloppyMath; + +import static org.elasticsearch.geo.GeoEncodingUtils.decodeLatitude; +import static org.elasticsearch.geo.GeoEncodingUtils.decodeLongitude; +import static org.elasticsearch.geo.GeoEncodingUtils.encodeLatitude; +import static org.elasticsearch.geo.GeoEncodingUtils.encodeLatitudeCeil; +import static org.elasticsearch.geo.GeoEncodingUtils.encodeLongitude; +import static org.elasticsearch.geo.GeoEncodingUtils.encodeLongitudeCeil; + +/** + * Used to speed up point-in-polygon and point-distance computations + */ +abstract class Predicate { + static final int ARITY = 64; + + final int latShift, lonShift; + final int latBase, lonBase; + final int maxLatDelta, maxLonDelta; + final byte[] relations; + + protected Predicate(Rectangle boundingBox, Function boxToRelation) { + final int minLat = encodeLatitudeCeil(boundingBox.minLat); + final int maxLat = encodeLatitude(boundingBox.maxLat); + final int minLon = encodeLongitudeCeil(boundingBox.minLon); + final int maxLon = encodeLongitude(boundingBox.maxLon); + + int latShift = 1; + int lonShift = 1; + int latBase = 0; + int lonBase = 0; + int maxLatDelta = 0; + int maxLonDelta = 0; + byte[] relations; + + if (maxLat < minLat || (boundingBox.crossesDateline() == false && maxLon < minLon)) { + // the box cannot match any quantized point + relations = new byte[0]; + } else { + { + long minLat2 = (long) minLat - Integer.MIN_VALUE; + long maxLat2 = (long) maxLat - Integer.MIN_VALUE; + latShift = computeShift(minLat2, maxLat2); + latBase = (int) (minLat2 >>> latShift); + maxLatDelta = (int) (maxLat2 >>> latShift) - latBase + 1; + assert maxLatDelta > 0; + } + { + long minLon2 = (long) minLon - Integer.MIN_VALUE; + long maxLon2 = (long) maxLon - Integer.MIN_VALUE; + if (boundingBox.crossesDateline()) { + maxLon2 += 1L << 32; // wrap + } + lonShift = computeShift(minLon2, maxLon2); + lonBase = (int) (minLon2 >>> lonShift); + maxLonDelta = (int) (maxLon2 >>> lonShift) - lonBase + 1; + assert maxLonDelta > 0; + } + + relations = new byte[maxLatDelta * maxLonDelta]; + for (int i = 0; i < maxLatDelta; ++i) { + for (int j = 0; j < maxLonDelta; ++j) { + final int boxMinLat = ((latBase + i) << latShift) + Integer.MIN_VALUE; + final int boxMinLon = ((lonBase + j) << lonShift) + Integer.MIN_VALUE; + final int boxMaxLat = boxMinLat + (1 << latShift) - 1; + final int boxMaxLon = boxMinLon + (1 << lonShift) - 1; + + relations[i * maxLonDelta + j] = (byte) boxToRelation.apply(new Rectangle( + decodeLatitude(boxMinLat), decodeLatitude(boxMaxLat), + decodeLongitude(boxMinLon), decodeLongitude(boxMaxLon))).ordinal(); + } + } + } + this.latShift = latShift; + this.lonShift = lonShift; + this.latBase = latBase; + this.lonBase = lonBase; + this.maxLatDelta = maxLatDelta; + this.maxLonDelta = maxLonDelta; + this.relations = relations; + } + + /** + * A predicate that checks whether a given point is within a distance of another point. + */ + final static class DistancePredicate extends Predicate { + + private final double lat, lon; + private final double distanceKey; + private final double axisLat; + + private DistancePredicate(double lat, double lon, double distanceKey, double axisLat, Rectangle boundingBox, + Function boxToRelation) { + super(boundingBox, boxToRelation); + this.lat = lat; + this.lon = lon; + this.distanceKey = distanceKey; + this.axisLat = axisLat; + } + + /** + * Create a predicate that checks whether points are within a distance of a given point. + * It works by computing the bounding box around the circle that is defined + * by the given points/distance and splitting it into between 1024 and 4096 + * smaller boxes (4096*0.75^2=2304 on average). Then for each sub box, it + * computes the relation between this box and the distance query. Finally at + * search time, it first computes the sub box that the point belongs to, + * most of the time, no distance computation will need to be performed since + * all points from the sub box will either be in or out of the circle. + * + * @lucene.internal + */ + static DistancePredicate create(double lat, double lon, double radiusMeters) { + final Rectangle boundingBox = Rectangle.fromPointDistance(lat, lon, radiusMeters); + final double axisLat = Rectangle.axisLat(lat, radiusMeters); + final double distanceSortKey = GeoUtils.distanceQuerySortKey(radiusMeters); + final Function boxToRelation = box -> GeoUtils.relate( + box.minLat, box.maxLat, box.minLon, box.maxLon, lat, lon, distanceSortKey, axisLat); + return new DistancePredicate(lat, lon, distanceSortKey, axisLat, boundingBox, boxToRelation); + } + + public Relation relate(double minLat, double maxLat, double minLon, double maxLon) { + return GeoUtils.relate(minLat, maxLat, minLon, maxLon, lat, lon, distanceKey, axisLat); + } + + /** + * Check whether the given point is within a distance of another point. + * NOTE: this operates directly on the encoded representation of points. + */ + public boolean test(int lat, int lon) { + final int lat2 = ((lat - Integer.MIN_VALUE) >>> latShift); + if (lat2 < latBase || lat2 >= latBase + maxLatDelta) { + return false; + } + int lon2 = ((lon - Integer.MIN_VALUE) >>> lonShift); + if (lon2 < lonBase) { // wrap + lon2 += 1 << (32 - lonShift); + } + assert Integer.toUnsignedLong(lon2) >= lonBase; + assert lon2 - lonBase >= 0; + if (lon2 - lonBase >= maxLonDelta) { + return false; + } + + final int relation = relations[(lat2 - latBase) * maxLonDelta + (lon2 - lonBase)]; + if (relation == Relation.CROSSES.ordinal()) { + return SloppyMath.haversinSortKey( + decodeLatitude(lat), decodeLongitude(lon), + this.lat, this.lon) <= distanceKey; + } else { + return relation == Relation.WITHIN.ordinal(); + } + } + } + + /** + * A predicate that checks whether a given point is within a polygon. + */ + final static class PolygonPredicate extends Predicate { + + final EdgeTree tree; + + private PolygonPredicate(EdgeTree tree, Rectangle boundingBox, Function boxToRelation) { + super(boundingBox, boxToRelation); + this.tree = tree; + } + + /** + * Create a predicate that checks whether points are within a polygon. + * It works the same way as {@code DistancePredicate.create}. + * + * @lucene.internal + */ + public static PolygonPredicate create(Rectangle boundingBox, EdgeTree tree) { + final Function boxToRelation = box -> tree.relate( + box.minLat, box.maxLat, box.minLon, box.maxLon); + return new PolygonPredicate(tree, boundingBox, boxToRelation); + } + + + public Relation relate(final double minLat, final double maxLat, final double minLon, final double maxLon) { + return tree.relate(minLat, maxLat, minLon, maxLon); + } + + /** + * Check whether the given point is within the considered polygon. + * NOTE: this operates directly on the encoded representation of points. + */ + public boolean test(int lat, int lon) { + final int lat2 = ((lat - Integer.MIN_VALUE) >>> latShift); + if (lat2 < latBase || lat2 >= latBase + maxLatDelta) { + return false; + } + int lon2 = ((lon - Integer.MIN_VALUE) >>> lonShift); + if (lon2 < lonBase) { // wrap + lon2 += 1 << (32 - lonShift); + } + assert Integer.toUnsignedLong(lon2) >= lonBase; + assert lon2 - lonBase >= 0; + if (lon2 - lonBase >= maxLonDelta) { + return false; + } + + final int relation = relations[(lat2 - latBase) * maxLonDelta + (lon2 - lonBase)]; + if (relation == Relation.CROSSES.ordinal()) { + return tree.contains(decodeLatitude(lat), decodeLongitude(lon)); + } else { + return relation == Relation.WITHIN.ordinal(); + } + } + } + + /** + * Compute the minimum shift value so that + * {@code (b>>>shift)-(a>>>shift)} is less that {@code ARITY}. + */ + private static int computeShift(long a, long b) { + assert a <= b; + // We enforce a shift of at least 1 so that when we work with unsigned ints + // by doing (lat - MIN_VALUE), the result of the shift (lat - MIN_VALUE) >>> shift + // can be used for comparisons without particular care: the sign bit has + // been cleared so comparisons work the same for signed and unsigned ints + for (int shift = 1; ; ++shift) { + final long delta = (b >>> shift) - (a >>> shift); + if (delta >= 0 && delta < ARITY) { + return shift; + } + } + } +} diff --git a/libs/geo/src/main/java/org/elasticsearch/geo/geometry/Rectangle.java b/libs/geo/src/main/java/org/elasticsearch/geo/geometry/Rectangle.java new file mode 100644 index 0000000000000..9a0d0bd62a278 --- /dev/null +++ b/libs/geo/src/main/java/org/elasticsearch/geo/geometry/Rectangle.java @@ -0,0 +1,415 @@ +/* + * 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.geo.geometry; + +import java.io.IOException; + +import org.elasticsearch.geo.GeoUtils; +import org.elasticsearch.geo.parsers.WKTParser; +import org.apache.lucene.store.OutputStreamDataOutput; + +import static java.lang.Math.PI; +import static java.lang.Math.max; +import static java.lang.Math.min; +import static org.elasticsearch.geo.GeoUtils.checkLatitude; +import static org.elasticsearch.geo.GeoUtils.checkLongitude; +import static org.elasticsearch.geo.GeoUtils.MAX_LAT_INCL; +import static org.elasticsearch.geo.GeoUtils.MIN_LAT_INCL; +import static org.elasticsearch.geo.GeoUtils.MAX_LAT_RADIANS; +import static org.elasticsearch.geo.GeoUtils.MAX_LON_RADIANS; +import static org.elasticsearch.geo.GeoUtils.MIN_LAT_RADIANS; +import static org.elasticsearch.geo.GeoUtils.MIN_LON_RADIANS; +import static org.elasticsearch.geo.GeoUtils.EARTH_MEAN_RADIUS_METERS; +import static org.elasticsearch.geo.GeoUtils.sloppySin; +import static org.apache.lucene.util.SloppyMath.TO_DEGREES; +import static org.apache.lucene.util.SloppyMath.asin; +import static org.apache.lucene.util.SloppyMath.cos; +import static org.apache.lucene.util.SloppyMath.toDegrees; +import static org.apache.lucene.util.SloppyMath.toRadians; + +/** + * Represents a lat/lon rectangle in decimal degrees. + */ +public class Rectangle extends GeoShape { + /** + * maximum longitude value (in degrees) + */ + public final double minLat; + /** + * minimum longitude value (in degrees) + */ + public final double minLon; + /** + * maximum latitude value (in degrees) + */ + public final double maxLat; + /** + * minimum latitude value (in degrees) + */ + public final double maxLon; + /** + * center of rectangle (in lat/lon degrees) + */ + private final Point center; + + /** + * Constructs a bounding box by first validating the provided latitude and longitude coordinates + */ + public Rectangle(double minLat, double maxLat, double minLon, double maxLon) { + GeoUtils.checkLatitude(minLat); + GeoUtils.checkLatitude(maxLat); + GeoUtils.checkLongitude(minLon); + GeoUtils.checkLongitude(maxLon); + this.minLon = minLon; + this.maxLon = maxLon; + this.minLat = minLat; + this.maxLat = maxLat; + assert maxLat >= minLat; + + // NOTE: cannot assert maxLon >= minLon since this rect could cross the dateline + this.boundingBox = this; + + // compute the center of the rectangle + final double cntrLat = getHeight() / 2 + minLat; + double cntrLon = getWidth() / 2 + minLon; + if (crossesDateline()) { + cntrLon = GeoUtils.normalizeLonDegrees(cntrLon); + } + this.center = new Point(cntrLat, cntrLon); + } + + @Override + public boolean hasArea() { + return minLat != maxLat && minLon != maxLon; + } + + public double getWidth() { + if (crossesDateline()) { + return GeoUtils.MAX_LON_INCL - minLon + maxLon - GeoUtils.MIN_LON_INCL; + } + return maxLon - minLon; + } + + public double getHeight() { + return maxLat - minLat; + } + + public Point getCenter() { + return this.center; + } + + @Override + public ShapeType type() { + return ShapeType.ENVELOPE; + } + + @Override + public Relation relate(double minLat, double maxLat, double minLon, double maxLon) { + if (minLat == GeoUtils.MIN_LAT_INCL && maxLat == GeoUtils.MAX_LAT_INCL + && minLon == GeoUtils.MIN_LON_INCL && maxLon == GeoUtils.MAX_LON_INCL) { + return Relation.WITHIN; + } else if (this.minLat == GeoUtils.MIN_LAT_INCL && this.maxLat == GeoUtils.MAX_LAT_INCL + && this.minLon == GeoUtils.MIN_LON_INCL && this.maxLon == GeoUtils.MAX_LON_INCL) { + return Relation.CONTAINS; + } else if (crossesDateline() == true) { + return relateXDL(minLat, maxLat, minLon, maxLon); + } else if (minLon > maxLon) { + if (rectDisjoint(this.minLat, this.maxLat, this.minLon, this.maxLon, minLat, maxLat, minLon, GeoUtils.MAX_LON_INCL) + && rectDisjoint(this.minLat, this.maxLat, this.minLon, this.maxLon, minLat, maxLat, GeoUtils.MIN_LON_INCL, maxLon)) { + return Relation.DISJOINT; + } else if (rectWithin(this.minLat, this.maxLat, this.minLon, this.maxLon, minLat, maxLat, minLon, GeoUtils.MAX_LON_INCL) + || rectWithin(this.minLat, this.maxLat, this.minLon, this.maxLon, minLat, maxLat, GeoUtils.MIN_LON_INCL, maxLon)) { + return Relation.WITHIN; + } // can't contain + } else if (rectDisjoint(this.minLat, this.maxLat, this.minLon, this.maxLon, minLat, maxLat, minLon, maxLon)) { + return Relation.DISJOINT; + } else if (rectWithin(this.minLat, this.maxLat, this.minLon, this.maxLon, minLat, maxLat, minLon, maxLon)) { + return Relation.WITHIN; + } else if (rectWithin(minLat, maxLat, minLon, maxLon, this.minLat, this.maxLat, this.minLon, this.maxLon)) { + return Relation.CONTAINS; + } + return Relation.INTERSECTS; + } + + /** + * compute relation for this Rectangle crossing the dateline + */ + private Relation relateXDL(double minLat, double maxLat, double minLon, double maxLon) { + if (minLon > maxLon) { + // incoming rectangle crosses dateline; just check latitude for disjoint + if (this.minLat > maxLat || this.maxLat < minLat) { + return Relation.DISJOINT; + } else if (rectWithin(this.minLat, this.maxLat, this.minLon, this.maxLon, minLat, maxLat, minLon, maxLon)) { + return Relation.WITHIN; + } else if (rectWithin(minLat, maxLat, minLon, maxLon, this.minLat, this.maxLat, this.minLon, this.maxLon)) { + return Relation.CONTAINS; + } + } else { + if (rectDisjoint(this.minLat, this.maxLat, this.minLon, GeoUtils.MAX_LON_INCL, minLat, maxLat, minLon, maxLon) + && rectDisjoint(this.minLat, this.maxLat, GeoUtils.MIN_LON_INCL, this.maxLon, minLat, maxLat, minLon, maxLon)) { + return Relation.DISJOINT; + // WITHIN not possible; *this* rectangle crosses the dateline but *that* rectangle does not + } else if (rectWithin(minLat, maxLat, minLon, maxLon, this.minLat, this.maxLat, this.minLon, GeoUtils.MAX_LON_INCL) + || rectWithin(minLat, maxLat, minLon, maxLon, this.minLat, this.maxLat, GeoUtils.MIN_LON_INCL, this.maxLon)) { + return Relation.CONTAINS; + } + } + return Relation.INTERSECTS; + } + + public Relation relate(double lat, double lon) { + if (lat > maxLat || lat < minLat || lon < minLon || lon > maxLon) { + return Relation.DISJOINT; + } + return Relation.INTERSECTS; + } + + @Override + public Relation relate(GeoShape shape) { + Relation r = shape.relate(this.minLat, this.maxLat, this.minLon, this.maxLon); + if (r == Relation.WITHIN) { + return Relation.CONTAINS; + } else if (r == Relation.CONTAINS) { + return Relation.WITHIN; + } + return r; + } + + /** + * Computes whether two rectangles are disjoint + */ + private static boolean rectDisjoint(final double aMinLat, final double aMaxLat, final double aMinLon, final double aMaxLon, + final double bMinLat, final double bMaxLat, final double bMinLon, final double bMaxLon) { + assert aMinLon <= aMaxLon : "dateline crossing not supported"; + assert bMinLon <= bMaxLon : "dateline crossing not supported"; + // fail quickly: test latitude + if (aMaxLat < bMinLat || aMinLat > bMaxLat) { + return true; + } + + // check sharing dateline + if ((aMinLon == GeoUtils.MIN_LON_INCL && bMaxLon == GeoUtils.MAX_LON_INCL) + || (bMinLon == GeoUtils.MIN_LON_INCL && aMaxLon == GeoUtils.MAX_LON_INCL)) { + return false; + } + + // check longitude + return aMaxLon < bMinLon || aMinLon > bMaxLon; + } + + /** + * Computes whether the first (a) rectangle is wholly within another (b) rectangle (shared boundaries allowed) + */ + private static boolean rectWithin(final double aMinLat, final double aMaxLat, final double aMinLon, final double aMaxLon, + final double bMinLat, final double bMaxLat, final double bMinLon, final double bMaxLon) { + return !(aMinLon < bMinLon || aMinLat < bMinLat || aMaxLon > bMaxLon || aMaxLat > bMaxLat); + } + + @Override + public String toString() { + StringBuilder b = new StringBuilder(); + b.append("Rectangle(lat="); + b.append(minLat); + b.append(" TO "); + b.append(maxLat); + b.append(" lon="); + b.append(minLon); + b.append(" TO "); + b.append(maxLon); + if (maxLon < minLon) { + b.append(" [crosses dateline!]"); + } + b.append(")"); + + return b.toString(); + } + + /** + * Returns true if this bounding box crosses the dateline + */ + public boolean crossesDateline() { + return maxLon < minLon; + } + + /** + * Compute Bounding Box for a circle using WGS-84 parameters + */ + public static Rectangle fromPointDistance(final double centerLat, final double centerLon, final double radiusMeters) { + checkLatitude(centerLat); + checkLongitude(centerLon); + final double radLat = toRadians(centerLat); + final double radLon = toRadians(centerLon); + // LUCENE-7143 + double radDistance = (radiusMeters + 7E-2) / EARTH_MEAN_RADIUS_METERS; + double minLat = radLat - radDistance; + double maxLat = radLat + radDistance; + double minLon; + double maxLon; + + if (minLat > MIN_LAT_RADIANS && maxLat < MAX_LAT_RADIANS) { + double deltaLon = asin(sloppySin(radDistance) / cos(radLat)); + minLon = radLon - deltaLon; + if (minLon < MIN_LON_RADIANS) { + minLon += 2d * PI; + } + maxLon = radLon + deltaLon; + if (maxLon > MAX_LON_RADIANS) { + maxLon -= 2d * PI; + } + } else { + // a pole is within the distance + minLat = max(minLat, MIN_LAT_RADIANS); + maxLat = min(maxLat, MAX_LAT_RADIANS); + minLon = MIN_LON_RADIANS; + maxLon = MAX_LON_RADIANS; + } + + return new Rectangle(toDegrees(minLat), toDegrees(maxLat), toDegrees(minLon), toDegrees(maxLon)); + } + + /** + * maximum error from {@link #axisLat(double, double)}. logic must be prepared to handle this + */ + public static final double AXISLAT_ERROR = 0.1D / EARTH_MEAN_RADIUS_METERS * TO_DEGREES; + + /** + * Calculate the latitude of a circle's intersections with its bbox meridians. + *

+ * NOTE: the returned value will be +/- {@link #AXISLAT_ERROR} of the actual value. + * + * @param centerLat The latitude of the circle center + * @param radiusMeters The radius of the circle in meters + * @return A latitude + */ + public static double axisLat(double centerLat, double radiusMeters) { + // A spherical triangle with: + // r is the radius of the circle in radians + // l1 is the latitude of the circle center + // l2 is the latitude of the point at which the circle intersect's its bbox longitudes + // We know r is tangent to the bbox meridians at l2, therefore it is a right angle. + // So from the law of cosines, with the angle of l1 being 90, we have: + // cos(l1) = cos(r) * cos(l2) + sin(r) * sin(l2) * cos(90) + // The second part cancels out because cos(90) == 0, so we have: + // cos(l1) = cos(r) * cos(l2) + // Solving for l2, we get: + // l2 = acos( cos(l1) / cos(r) ) + // We ensure r is in the range (0, PI/2) and l1 in the range (0, PI/2]. This means we + // cannot divide by 0, and we will always get a positive value in the range [0, 1) as + // the argument to arc cosine, resulting in a range (0, PI/2]. + final double PIO2 = Math.PI / 2D; + double l1 = toRadians(centerLat); + double r = (radiusMeters + 7E-2) / EARTH_MEAN_RADIUS_METERS; + + // if we are within radius range of a pole, the lat is the pole itself + if (Math.abs(l1) + r >= MAX_LAT_RADIANS) { + return centerLat >= 0 ? MAX_LAT_INCL : MIN_LAT_INCL; + } + + // adjust l1 as distance from closest pole, to form a right triangle with bbox meridians + // and ensure it is in the range (0, PI/2] + l1 = centerLat >= 0 ? PIO2 - l1 : l1 + PIO2; + + double l2 = Math.acos(Math.cos(l1) / Math.cos(r)); + assert !Double.isNaN(l2); + + // now adjust back to range [-pi/2, pi/2], ie latitude in radians + l2 = centerLat >= 0 ? PIO2 - l2 : l2 - PIO2; + + return toDegrees(l2); + } + + /** + * Returns the bounding box over an array of polygons + */ + public static Rectangle fromPolygon(Polygon[] polygons) { + // compute bounding box + double minLat = Double.POSITIVE_INFINITY; + double maxLat = Double.NEGATIVE_INFINITY; + double minLon = Double.POSITIVE_INFINITY; + double maxLon = Double.NEGATIVE_INFINITY; + + for (int i = 0; i < polygons.length; i++) { + minLat = min(polygons[i].minLat(), minLat); + maxLat = max(polygons[i].maxLat(), maxLat); + minLon = min(polygons[i].minLon(), minLon); + maxLon = max(polygons[i].maxLon(), maxLon); + } + + return new Rectangle(minLat, maxLat, minLon, maxLon); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + Rectangle rectangle = (Rectangle) o; + + if (Double.compare(rectangle.minLat, minLat) != 0) return false; + if (Double.compare(rectangle.minLon, minLon) != 0) return false; + if (Double.compare(rectangle.maxLat, maxLat) != 0) return false; + return Double.compare(rectangle.maxLon, maxLon) == 0; + + } + + @Override + public int hashCode() { + int result; + long temp; + temp = Double.doubleToLongBits(minLat); + result = (int) (temp ^ (temp >>> 32)); + temp = Double.doubleToLongBits(minLon); + result = 31 * result + (int) (temp ^ (temp >>> 32)); + temp = Double.doubleToLongBits(maxLat); + result = 31 * result + (int) (temp ^ (temp >>> 32)); + temp = Double.doubleToLongBits(maxLon); + result = 31 * result + (int) (temp ^ (temp >>> 32)); + return result; + } + + @Override + protected StringBuilder contentToWKT() { + StringBuilder sb = new StringBuilder(); + + sb.append(WKTParser.LPAREN); + // minX, maxX, maxY, minY + sb.append(minLon); + sb.append(WKTParser.COMMA); + sb.append(WKTParser.SPACE); + sb.append(maxLon); + sb.append(WKTParser.COMMA); + sb.append(WKTParser.SPACE); + sb.append(maxLat); + sb.append(WKTParser.COMMA); + sb.append(WKTParser.SPACE); + sb.append(minLat); + sb.append(WKTParser.RPAREN); + + return sb; + } + + @Override + protected void appendWKBContent(OutputStreamDataOutput out) throws IOException { + out.writeVLong(Double.doubleToRawLongBits(minLon)); + out.writeVLong(Double.doubleToRawLongBits(maxLon)); + out.writeVLong(Double.doubleToRawLongBits(maxLat)); + out.writeVLong(Double.doubleToRawLongBits(minLat)); + } +} diff --git a/libs/geo/src/main/java/org/elasticsearch/geo/geometry/ShapeType.java b/libs/geo/src/main/java/org/elasticsearch/geo/geometry/ShapeType.java new file mode 100644 index 0000000000000..5e0e75cab2c54 --- /dev/null +++ b/libs/geo/src/main/java/org/elasticsearch/geo/geometry/ShapeType.java @@ -0,0 +1,83 @@ +/* + * 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.geo.geometry; + +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; + +/** + * Created by nknize on 9/22/17. + */ +public enum ShapeType { + POINT("point", 1), + MULTIPOINT("multipoint", 4), + LINESTRING("linestring", 2), + MULTILINESTRING("multilinestring", 5), + POLYGON("polygon", 3), + MULTIPOLYGON("multipolygon", 6), + GEOMETRYCOLLECTION("geometrycollection", 7), + ENVELOPE("envelope", 8), // not part of the actual WKB spec + CIRCLE("circle", 9); // not part of the actual WKB spec + + private final String shapeName; + private final int wkbOrdinal; + private static Map shapeTypeMap = new HashMap<>(); + private static Map wkbTypeMap = new HashMap<>(); + private static final String BBOX = "BBOX"; + + static { + for (ShapeType type : values()) { + shapeTypeMap.put(type.shapeName, type); + wkbTypeMap.put(type.wkbOrdinal, type); + } + shapeTypeMap.put(ENVELOPE.wktName().toLowerCase(Locale.ROOT), ENVELOPE); + } + + ShapeType(String shapeName, int wkbOrdinal) { + this.shapeName = shapeName; + this.wkbOrdinal = wkbOrdinal; + } + + protected String typename() { + return shapeName; + } + + /** + * wkt shape name + */ + public String wktName() { + return this == ENVELOPE ? BBOX : this.shapeName; + } + + public int wkbOrdinal() { + return this.wkbOrdinal; + } + + public static ShapeType forName(String shapename) { + String typename = shapename.toLowerCase(Locale.ROOT); + for (ShapeType type : values()) { + if (type.shapeName.equals(typename)) { + return type; + } + } + throw new IllegalArgumentException("unknown geo_shape [" + shapename + "]"); + } +} diff --git a/libs/geo/src/main/java/org/elasticsearch/geo/package-info.java b/libs/geo/src/main/java/org/elasticsearch/geo/package-info.java new file mode 100644 index 0000000000000..3b59d36b46ae7 --- /dev/null +++ b/libs/geo/src/main/java/org/elasticsearch/geo/package-info.java @@ -0,0 +1,24 @@ +/* + * 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. + */ + + +/** + * Common Geo classes + */ +package org.elasticsearch.geo; diff --git a/libs/geo/src/main/java/org/elasticsearch/geo/parsers/SimpleGeoJSONPolygonParser.java b/libs/geo/src/main/java/org/elasticsearch/geo/parsers/SimpleGeoJSONPolygonParser.java new file mode 100644 index 0000000000000..40d6c16377bb8 --- /dev/null +++ b/libs/geo/src/main/java/org/elasticsearch/geo/parsers/SimpleGeoJSONPolygonParser.java @@ -0,0 +1,445 @@ +/* + * 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.geo.parsers; + +import java.text.ParseException; +import java.util.ArrayList; +import java.util.List; + +import org.elasticsearch.geo.geometry.Polygon; + +/* + We accept either a whole type: Feature, like this: + + { "type": "Feature", + "geometry": { + "type": "Polygon", + "coordinates": [ + [ [100.0, 0.0], [101.0, 0.0], [101.0, 1.0], + [100.0, 1.0], [100.0, 0.0] ] + ] + }, + "properties": { + "prop0": "value0", + "prop1": {"this": "that"} + } + } + + Or the inner object with type: Multi/Polygon. + + Or a type: FeatureCollection, if it has only one Feature which is a Polygon or MultiPolyon. + + type: MultiPolygon (union of polygons) is also accepted. +*/ + +/** Does minimal parsing of a GeoJSON object, to extract either Polygon or MultiPolygon, either directly as a the top-level type, or if + * the top-level type is Feature, as the geometry of that feature. */ + +@SuppressWarnings("unchecked") +public class SimpleGeoJSONPolygonParser { + final String input; + private int upto; + private String polyType; + private List coordinates; + + public SimpleGeoJSONPolygonParser(String input) { + this.input = input; + } + + public Polygon[] parse() throws ParseException { + // parse entire object + parseObject(""); + + // make sure there's nothing left: + readEnd(); + + // The order of JSON object keys (type, geometry, coordinates in our case) can be arbitrary, so we wait until we are done parsing to + // put the pieces together here: + + if (coordinates == null) { + throw newParseException("did not see any polygon coordinates"); + } + + if (polyType == null) { + throw newParseException("did not see type: Polygon or MultiPolygon"); + } + + if (polyType.equals("Polygon")) { + return new Polygon[] {parsePolygon(coordinates)}; + } else { + List polygons = new ArrayList<>(); + for(int i=0;i) o)); + } + + return polygons.toArray(new Polygon[polygons.size()]); + } + } + + /** path is the "address" by keys of where we are, e.g. geometry.coordinates */ + private void parseObject(String path) throws ParseException { + scan('{'); + boolean first = true; + while (true) { + char ch = peek(); + if (ch == '}') { + break; + } else if (first == false) { + if (ch == ',') { + // ok + upto++; + ch = peek(); + if (ch == '}') { + break; + } + } else { + throw newParseException("expected , but got " + ch); + } + } + + first = false; + + int uptoStart = upto; + String key = parseString(); + + if (path.equals("crs.properties") && key.equals("href")) { + upto = uptoStart; + throw newParseException("cannot handle linked crs"); + } + + scan(':'); + + Object o; + + ch = peek(); + + uptoStart = upto; + + if (ch == '[') { + String newPath; + if (path.length() == 0) { + newPath = key; + } else { + newPath = path + "." + key; + } + o = parseArray(newPath); + } else if (ch == '{') { + String newPath; + if (path.length() == 0) { + newPath = key; + } else { + newPath = path + "." + key; + } + parseObject(newPath); + o = null; + } else if (ch == '"') { + o = parseString(); + } else if (ch == 't') { + scan("true"); + o = Boolean.TRUE; + } else if (ch == 'f') { + scan("false"); + o = Boolean.FALSE; + } else if (ch == 'n') { + scan("null"); + o = null; + } else if (ch == '-' || ch == '.' || (ch >= '0' && ch <= '9')) { + o = parseNumber(); + } else if (ch == '}') { + break; + } else { + throw newParseException("expected array, object, string or literal value, but got: " + ch); + } + + if (path.equals("crs.properties") && key.equals("name")) { + if (o instanceof String == false) { + upto = uptoStart; + throw newParseException("crs.properties.name should be a string, but saw: " + o); + } + String crs = (String) o; + if (crs.startsWith("urn:ogc:def:crs:OGC") == false || crs.endsWith(":CRS84") == false) { + upto = uptoStart; + throw newParseException("crs must be CRS84 from OGC, but saw: " + o); + } + } + + if (key.equals("type") && path.startsWith("crs") == false) { + if (o instanceof String == false) { + upto = uptoStart; + throw newParseException("type should be a string, but got: " + o); + } + String type = (String) o; + if (type.equals("Polygon") && isValidGeometryPath(path)) { + polyType = "Polygon"; + } else if (type.equals("MultiPolygon") && isValidGeometryPath(path)) { + polyType = "MultiPolygon"; + } else if ((type.equals("FeatureCollection") || type.equals("Feature")) && (path.equals("features.[]") || path.equals(""))) { + // OK, we recurse + } else { + upto = uptoStart; + throw newParseException("can only handle type FeatureCollection (if it has a single polygon geometry), Feature, Polygon or MutiPolygon, but got " + type); + } + } else if (key.equals("coordinates") && isValidGeometryPath(path)) { + if (o instanceof List == false) { + upto = uptoStart; + throw newParseException("coordinates should be an array, but got: " + o.getClass()); + } + if (coordinates != null) { + upto = uptoStart; + throw newParseException("only one Polygon or MultiPolygon is supported"); + } + coordinates = (List) o; + } + } + + scan('}'); + } + + /** Returns true if the object path is a valid location to see a Multi/Polygon geometry */ + private boolean isValidGeometryPath(String path) { + return path.equals("") || path.equals("geometry") || path.equals("features.[].geometry"); + } + + private Polygon parsePolygon(List coordinates) throws ParseException { + List holes = new ArrayList<>(); + Object o = coordinates.get(0); + if (o instanceof List == false) { + throw newParseException("first element of polygon array must be an array [[lat, lon], [lat, lon] ...] but got: " + o); + } + double[][] polyPoints = parsePoints((List) o); + for(int i=1;i) o); + holes.add(new Polygon(holePoints[0], holePoints[1])); + } + return new Polygon(polyPoints[0], polyPoints[1], holes.toArray(new Polygon[holes.size()])); + } + + /** Parses [[lat, lon], [lat, lon] ...] into 2d double array */ + private double[][] parsePoints(List o) throws ParseException { + double[] lats = new double[o.size()]; + double[] lons = new double[o.size()]; + for(int i=0;i pointList = (List) point; + if (pointList.size() != 2) { + throw newParseException("elements of coordinates array must [lat, lon] array, but got wrong element count: " + pointList); + } + if (pointList.get(0) instanceof Double == false) { + throw newParseException("elements of coordinates array must [lat, lon] array, but first element is not a Double: " + pointList.get(0)); + } + if (pointList.get(1) instanceof Double == false) { + throw newParseException("elements of coordinates array must [lat, lon] array, but second element is not a Double: " + pointList.get(1)); + } + + // lon, lat ordering in GeoJSON! + lons[i] = ((Double) pointList.get(0)).doubleValue(); + lats[i] = ((Double) pointList.get(1)).doubleValue(); + } + + return new double[][] {lats, lons}; + } + + private List parseArray(String path) throws ParseException { + List result = new ArrayList<>(); + scan('['); + while (upto < input.length()) { + char ch = peek(); + if (ch == ']') { + scan(']'); + return result; + } + + if (result.size() > 0) { + if (ch != ',') { + throw newParseException("expected ',' separating list items, but got '" + ch + "'"); + } + + // skip the , + upto++; + + if (upto == input.length()) { + throw newParseException("hit EOF while parsing array"); + } + ch = peek(); + } + + Object o; + if (ch == '[') { + o = parseArray(path + ".[]"); + } else if (ch == '{') { + // This is only used when parsing the "features" in type: FeatureCollection + parseObject(path + ".[]"); + o = null; + } else if (ch == '-' || ch == '.' || (ch >= '0' && ch <= '9')) { + o = parseNumber(); + } else { + throw newParseException("expected another array or number while parsing array, not '" + ch + "'"); + } + + result.add(o); + } + + throw newParseException("hit EOF while reading array"); + } + + private Number parseNumber() throws ParseException { + StringBuilder b = new StringBuilder(); + int uptoStart = upto; + while (upto < input.length()) { + char ch = input.charAt(upto); + if (ch == '-' || ch == '.' || (ch >= '0' && ch <= '9') || ch == 'e' || ch == 'E') { + upto++; + b.append(ch); + } else { + break; + } + } + + // we only handle doubles + try { + return Double.parseDouble(b.toString()); + } catch (NumberFormatException nfe) { + upto = uptoStart; + throw newParseException("could not parse number as double"); + } + } + + private String parseString() throws ParseException { + scan('"'); + StringBuilder b = new StringBuilder(); + while (upto < input.length()) { + char ch = input.charAt(upto); + if (ch == '"') { + upto++; + return b.toString(); + } + if (ch == '\\') { + // an escaped character + upto++; + if (upto == input.length()) { + throw newParseException("hit EOF inside string literal"); + } + ch = input.charAt(upto); + if (ch == 'u') { + // 4 hex digit unicode BMP escape + upto++; + if (upto + 4 > input.length()) { + throw newParseException("hit EOF inside string literal"); + } + b.append(Integer.parseInt(input.substring(upto, upto+4), 16)); + } else if (ch == '\\') { + b.append('\\'); + upto++; + } else { + // TODO: allow \n, \t, etc.??? + throw newParseException("unsupported string escape character \\" + ch); + } + } else { + b.append(ch); + upto++; + } + } + + throw newParseException("hit EOF inside string literal"); + } + + private char peek() throws ParseException { + while (upto < input.length()) { + char ch = input.charAt(upto); + if (isJSONWhitespace(ch)) { + upto++; + continue; + } + return ch; + } + + throw newParseException("unexpected EOF"); + } + + /** Scans across whitespace and consumes the expected character, or throws {@code ParseException} if the character is wrong */ + private void scan(char expected) throws ParseException { + while (upto < input.length()) { + char ch = input.charAt(upto); + if (isJSONWhitespace(ch)) { + upto++; + continue; + } + if (ch != expected) { + throw newParseException("expected '" + expected + "' but got '" + ch + "'"); + } + upto++; + return; + } + throw newParseException("expected '" + expected + "' but got EOF"); + } + + private void readEnd() throws ParseException { + while (upto < input.length()) { + char ch = input.charAt(upto); + if (isJSONWhitespace(ch) == false) { + throw newParseException("unexpected character '" + ch + "' after end of GeoJSON object"); + } + upto++; + } + } + + /** Scans the expected string, or throws {@code ParseException} */ + private void scan(String expected) throws ParseException { + if (upto + expected.length() > input.length()) { + throw newParseException("expected \"" + expected + "\" but hit EOF"); + } + String subString = input.substring(upto, upto+expected.length()); + if (subString.equals(expected) == false) { + throw newParseException("expected \"" + expected + "\" but got \"" + subString + "\""); + } + upto += expected.length(); + } + + private static boolean isJSONWhitespace(char ch) { + // JSON doesn't accept allow unicode whitespace? + return ch == 0x20 || // space + ch == 0x09 || // tab + ch == 0x0a || // line feed + ch == 0x0d; // newline + } + + /** When calling this, upto should be at the position of the incorrect character! */ + private ParseException newParseException(String details) throws ParseException { + String fragment; + int end = Math.min(input.length(), upto+1); + if (upto < 50) { + fragment = input.substring(0, end); + } else { + fragment = "..." + input.substring(upto-50, end); + } + return new ParseException(details + " at character offset " + upto + "; fragment leading to this:\n" + fragment, upto); + } +} diff --git a/libs/geo/src/main/java/org/elasticsearch/geo/parsers/WKBParser.java b/libs/geo/src/main/java/org/elasticsearch/geo/parsers/WKBParser.java new file mode 100644 index 0000000000000..946df5a2d2b84 --- /dev/null +++ b/libs/geo/src/main/java/org/elasticsearch/geo/parsers/WKBParser.java @@ -0,0 +1,49 @@ +/* + * 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.geo.parsers; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; + +import org.elasticsearch.geo.geometry.GeoShape; +import org.apache.lucene.store.OutputStreamDataOutput; +import org.apache.lucene.util.BytesRef; + +public class WKBParser { + + // no instance: + private WKBParser() { + } + + public enum ByteOrder { + XDR, // big endian + NDR; // little endian + + public BytesRef toWKB() { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + try (OutputStreamDataOutput out = new OutputStreamDataOutput(baos)) { + out.writeVInt(this.ordinal()); + } catch (IOException e) { + throw new RuntimeException(e); // not possible + } + return new BytesRef(baos.toByteArray()); + } + } +} diff --git a/libs/geo/src/main/java/org/elasticsearch/geo/parsers/WKTParser.java b/libs/geo/src/main/java/org/elasticsearch/geo/parsers/WKTParser.java new file mode 100644 index 0000000000000..667965d9954bb --- /dev/null +++ b/libs/geo/src/main/java/org/elasticsearch/geo/parsers/WKTParser.java @@ -0,0 +1,326 @@ +/* + * 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.geo.parsers; + +import java.io.IOException; +import java.io.StreamTokenizer; +import java.io.StringReader; +import java.text.ParseException; +import java.util.ArrayList; + +import org.elasticsearch.geo.geometry.Point; +import org.elasticsearch.geo.geometry.GeoShape; +import org.elasticsearch.geo.geometry.Rectangle; +import org.elasticsearch.geo.geometry.ShapeType; +import org.elasticsearch.geo.geometry.Line; +import org.elasticsearch.geo.geometry.MultiLine; +import org.elasticsearch.geo.geometry.MultiPoint; +import org.elasticsearch.geo.geometry.MultiPolygon; +import org.elasticsearch.geo.geometry.Polygon; + +/** + * Created by nknize on 3/12/17. + */ +public class WKTParser { + public static final String EMPTY = "EMPTY"; + public static final String SPACE = " "; + public static final String LPAREN = "("; + public static final String RPAREN = ")"; + public static final String COMMA = ","; + public static final String NAN = "NaN"; + + private static final String NUMBER = ""; + private static final String EOF = "END-OF-STREAM"; + private static final String EOL = "END-OF-LINE"; + + // no instance + private WKTParser() { + } + + public static GeoShape parse(String wkt) throws IOException, ParseException { + StringReader reader = new StringReader(wkt); + try { + // setup the tokenizer; configured to read words w/o numbers + StreamTokenizer tokenizer = new StreamTokenizer(reader); + tokenizer.resetSyntax(); + tokenizer.wordChars('a', 'z'); + tokenizer.wordChars('A', 'Z'); + tokenizer.wordChars(128 + 32, 255); + tokenizer.wordChars('0', '9'); + tokenizer.wordChars('-', '-'); + tokenizer.wordChars('+', '+'); + tokenizer.wordChars('.', '.'); + tokenizer.whitespaceChars(0, ' '); + tokenizer.commentChar('#'); + return parseGeometry(tokenizer); + } finally { + reader.close(); + } + } + + /** + * parse geometry from the stream tokenizer + */ + private static GeoShape parseGeometry(StreamTokenizer stream) throws IOException, ParseException { + final ShapeType type = ShapeType.forName(nextWord(stream)); + switch (type) { + case POINT: + return parsePoint(stream); + case MULTIPOINT: + return parseMultiPoint(stream); + case LINESTRING: + return parseLine(stream); + case MULTILINESTRING: + return parseMultiLine(stream); + case POLYGON: + return parsePolygon(stream); + case MULTIPOLYGON: + return parseMultiPolygon(stream); + case ENVELOPE: + return parseBBox(stream); + } + throw new IllegalArgumentException("Unknown geometry type: " + type); + } + + private static Point parsePoint(StreamTokenizer stream) throws IOException, ParseException { + if (nextEmptyOrOpen(stream).equals(EMPTY)) { + return new Point(0, 0); + } + Point pt = new Point(nextNumber(stream), nextNumber(stream)); + if (isNumberNext(stream) == true) { + nextNumber(stream); + } + nextCloser(stream); + return pt; + } + + private static void parseCoordinates(StreamTokenizer stream, ArrayList lats, ArrayList lons) + throws IOException, ParseException { + parseCoordinate(stream, lats, lons); + while (nextCloserOrComma(stream).equals(COMMA)) { + parseCoordinate(stream, lats, lons); + } + } + + private static void parseCoordinate(StreamTokenizer stream, ArrayList lats, ArrayList lons) + throws IOException, ParseException { + lats.add(nextNumber(stream)); + lons.add(nextNumber(stream)); + if (isNumberNext(stream)) { + nextNumber(stream); + } + } + + private static MultiPoint parseMultiPoint(StreamTokenizer stream) throws IOException, ParseException { + String token = nextEmptyOrOpen(stream); + if (token.equals(EMPTY)) { + return new MultiPoint(new double[]{0.0}, new double[]{0.0}); + } + ArrayList lats = new ArrayList(); + ArrayList lons = new ArrayList(); + parseCoordinates(stream, lats, lons); + return new MultiPoint(lats.stream().mapToDouble(i -> i).toArray(), lons.stream().mapToDouble(i -> i).toArray()); + } + + private static Line parseLine(StreamTokenizer stream) throws IOException, ParseException { + String token = nextEmptyOrOpen(stream); + if (token.equals(EMPTY)) { + return new Line(new double[]{0.0}, new double[]{0.0}); + } + ArrayList lats = new ArrayList(); + ArrayList lons = new ArrayList(); + parseCoordinates(stream, lats, lons); + return new Line(lats.stream().mapToDouble(i -> i).toArray(), lons.stream().mapToDouble(i -> i).toArray()); + } + + private static MultiLine parseMultiLine(StreamTokenizer stream) throws IOException, ParseException { + String token = nextEmptyOrOpen(stream); + if (token.equals(EMPTY)) { + return new MultiLine(null); + } + ArrayList lines = new ArrayList(); + lines.add(parseLine(stream)); + while (nextCloserOrComma(stream).equals(COMMA)) { + lines.add(parseLine(stream)); + } + Line[] l = lines.toArray(new Line[lines.size()]); + return new MultiLine(l); + } + + private static Polygon parsePolygonHole(StreamTokenizer stream) throws IOException, ParseException { + ArrayList lats = new ArrayList(); + ArrayList lons = new ArrayList(); + parseCoordinates(stream, lats, lons); + return new Polygon(lats.stream().mapToDouble(i -> i).toArray(), lons.stream().mapToDouble(i -> i).toArray()); + } + + private static Polygon parsePolygon(StreamTokenizer stream) throws IOException, ParseException { + if (nextEmptyOrOpen(stream).equals(EMPTY)) { + return new Polygon(new double[0], new double[0]); + } + nextOpener(stream); + ArrayList lats = new ArrayList(); + ArrayList lons = new ArrayList(); + parseCoordinates(stream, lats, lons); + ArrayList holes = null; + if (nextWord(stream).equals(LPAREN)) { + while (nextCloserOrComma(stream).equals(COMMA)) { + holes.add(parsePolygonHole(stream)); + } + } + if (holes != null) { + Polygon[] h = null; + holes.toArray(new Polygon[holes.size()]); + return new Polygon(lats.stream().mapToDouble(i -> i).toArray(), lons.stream().mapToDouble(i -> i).toArray(), h); + } + return new Polygon(lats.stream().mapToDouble(i -> i).toArray(), lons.stream().mapToDouble(i -> i).toArray()); + } + + private static MultiPolygon parseMultiPolygon(StreamTokenizer stream) throws IOException, ParseException { + String token = nextEmptyOrOpen(stream); + if (token.equals(EMPTY)) { + return new MultiPolygon(null); + } + ArrayList polygons = new ArrayList(); + polygons.add(parsePolygon(stream)); + while (nextCloserOrComma(stream).equals(COMMA)) { + polygons.add(parsePolygon(stream)); + } + Polygon[] p = polygons.toArray(new Polygon[polygons.size()]); + return new MultiPolygon(p); + } + + private static Rectangle parseBBox(StreamTokenizer stream) throws IOException, ParseException { + if (nextEmptyOrOpen(stream).equals(EMPTY)) { + return null; + } + double minLon = nextNumber(stream); + nextComma(stream); + double maxLon = nextNumber(stream); + nextComma(stream); + double maxLat = nextNumber(stream); + nextComma(stream); + double minLat = nextNumber(stream); + nextCloser(stream); + return new Rectangle(minLat, maxLat, minLon, maxLon); + } + + /** + * next word in the stream + */ + private static String nextWord(StreamTokenizer stream) throws ParseException, IOException { + switch (stream.nextToken()) { + case StreamTokenizer.TT_WORD: + final String word = stream.sval; + return word.equalsIgnoreCase(EMPTY) ? EMPTY : word; + case '(': + return LPAREN; + case ')': + return RPAREN; + case ',': + return COMMA; + } + throw new ParseException("expected word but found: " + tokenString(stream), stream.lineno()); + } + + private static double nextNumber(StreamTokenizer stream) throws IOException, ParseException { + if (stream.nextToken() == StreamTokenizer.TT_WORD) { + if (stream.sval.equalsIgnoreCase(NAN)) { + return Double.NaN; + } else { + try { + return Double.parseDouble(stream.sval); + } catch (NumberFormatException e) { + throw new ParseException("invalid number found: " + stream.sval, stream.lineno()); + } + } + } + throw new ParseException("expected number but found: " + tokenString(stream), stream.lineno()); + } + + private static String tokenString(StreamTokenizer stream) { + switch (stream.ttype) { + case StreamTokenizer.TT_WORD: + return stream.sval; + case StreamTokenizer.TT_EOF: + return EOF; + case StreamTokenizer.TT_EOL: + return EOL; + case StreamTokenizer.TT_NUMBER: + return NUMBER; + } + return "'" + (char) stream.ttype + "'"; + } + + private static boolean isNumberNext(StreamTokenizer stream) throws IOException { + final int type = stream.nextToken(); + stream.pushBack(); + return type == StreamTokenizer.TT_WORD; + } + + private static String nextEmptyOrOpen(StreamTokenizer stream) throws IOException, ParseException { + final String next = nextWord(stream); + if (next.equals(EMPTY) || next.equals(LPAREN)) { + return next; + } + throw new ParseException("expected " + EMPTY + " or " + LPAREN + + " but found: " + tokenString(stream), stream.lineno()); + } + + private static String nextCloser(StreamTokenizer stream) throws IOException, ParseException { + if (nextWord(stream).equals(RPAREN)) { + return RPAREN; + } + throw new ParseException("expected " + RPAREN + " but found: " + tokenString(stream), stream.lineno()); + } + + private static String nextComma(StreamTokenizer stream) throws IOException, ParseException { + if (nextWord(stream).equals(COMMA) == true) { + return COMMA; + } + throw new ParseException("expected " + COMMA + " but found: " + tokenString(stream), stream.lineno()); + } + + private static String nextOpener(StreamTokenizer stream) throws IOException, ParseException { + if (nextWord(stream).equals(LPAREN)) { + return LPAREN; + } + throw new ParseException("expected " + LPAREN + " but found: " + tokenString(stream), stream.lineno()); + } + + private static String nextCloserOrComma(StreamTokenizer stream) throws IOException, ParseException { + String token = nextWord(stream); + if (token.equals(COMMA) || token.equals(RPAREN)) { + return token; + } + throw new ParseException("expected " + COMMA + " or " + RPAREN + + " but found: " + tokenString(stream), stream.lineno()); + } + + public static void main(String[] args) { + try { + String wkt = "MULTIPOLYGON (((10 40, 40 30, 20 20, 30 10, 10 40)))"; + GeoShape shape = WKTParser.parse(wkt); + assert shape instanceof Point; + } catch (Exception e) { + e.printStackTrace(); + } + } +} diff --git a/libs/geo/src/test/java/org/elasticsearch/geo/TestGeoEncodingUtils.java b/libs/geo/src/test/java/org/elasticsearch/geo/TestGeoEncodingUtils.java new file mode 100644 index 0000000000000..be3df4418c67d --- /dev/null +++ b/libs/geo/src/test/java/org/elasticsearch/geo/TestGeoEncodingUtils.java @@ -0,0 +1,155 @@ +/* + * 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.geo; + +import org.apache.lucene.geo.GeoEncodingUtils; +import org.apache.lucene.util.LuceneTestCase; +import org.apache.lucene.util.NumericUtils; +import org.apache.lucene.util.TestUtil; + +import java.util.Random; + +import static org.apache.lucene.geo.GeoEncodingUtils.decodeLatitude; +import static org.apache.lucene.geo.GeoEncodingUtils.decodeLongitude; +import static org.apache.lucene.geo.GeoEncodingUtils.encodeLatitude; +import static org.apache.lucene.geo.GeoEncodingUtils.encodeLatitudeCeil; +import static org.apache.lucene.geo.GeoEncodingUtils.encodeLongitude; +import static org.apache.lucene.geo.GeoEncodingUtils.encodeLongitudeCeil; +import static org.apache.lucene.geo.GeoUtils.MAX_LAT_INCL; +import static org.apache.lucene.geo.GeoUtils.MAX_LON_INCL; +import static org.apache.lucene.geo.GeoUtils.MIN_LAT_INCL; +import static org.apache.lucene.geo.GeoUtils.MIN_LON_INCL; + +/** + * Tests methods in {@link GeoEncodingUtils} + */ +public class TestGeoEncodingUtils extends LuceneTestCase { + + /** + * step through some integers, ensuring they decode to their expected double values. + * double values start at -90 and increase by LATITUDE_DECODE for each integer. + * check edge cases within the double range and random doubles within the range too. + */ + public void testLatitudeQuantization() throws Exception { + final double LATITUDE_DECODE = 180.0D / (0x1L << 32); + Random random = random(); + for (int i = 0; i < 10000; i++) { + int encoded = random.nextInt(); + double min = MIN_LAT_INCL + (encoded - (long) Integer.MIN_VALUE) * LATITUDE_DECODE; + double decoded = decodeLatitude(encoded); + // should exactly equal expected value + assertEquals(min, decoded, 0.0D); + // should round-trip + assertEquals(encoded, encodeLatitude(decoded)); + assertEquals(encoded, encodeLatitudeCeil(decoded)); + // test within the range + if (encoded != Integer.MAX_VALUE) { + // this is the next representable value + // all double values between [min .. max) should encode to the current integer + // all double values between (min .. max] should encodeCeil to the next integer. + double max = min + LATITUDE_DECODE; + assertEquals(max, decodeLatitude(encoded + 1), 0.0D); + assertEquals(encoded + 1, encodeLatitude(max)); + assertEquals(encoded + 1, encodeLatitudeCeil(max)); + + // first and last doubles in range that will be quantized + double minEdge = Math.nextUp(min); + double maxEdge = Math.nextDown(max); + assertEquals(encoded, encodeLatitude(minEdge)); + assertEquals(encoded + 1, encodeLatitudeCeil(minEdge)); + assertEquals(encoded, encodeLatitude(maxEdge)); + assertEquals(encoded + 1, encodeLatitudeCeil(maxEdge)); + + // check random values within the double range + long minBits = NumericUtils.doubleToSortableLong(minEdge); + long maxBits = NumericUtils.doubleToSortableLong(maxEdge); + for (int j = 0; j < 100; j++) { + double value = NumericUtils.sortableLongToDouble(TestUtil.nextLong(random, minBits, maxBits)); + // round down + assertEquals(encoded, encodeLatitude(value)); + // round up + assertEquals(encoded + 1, encodeLatitudeCeil(value)); + } + } + } + } + + /** + * step through some integers, ensuring they decode to their expected double values. + * double values start at -180 and increase by LONGITUDE_DECODE for each integer. + * check edge cases within the double range and a random doubles within the range too. + */ + public void testLongitudeQuantization() throws Exception { + final double LONGITUDE_DECODE = 360.0D / (0x1L << 32); + Random random = random(); + for (int i = 0; i < 10000; i++) { + int encoded = random.nextInt(); + double min = MIN_LON_INCL + (encoded - (long) Integer.MIN_VALUE) * LONGITUDE_DECODE; + double decoded = decodeLongitude(encoded); + // should exactly equal expected value + assertEquals(min, decoded, 0.0D); + // should round-trip + assertEquals(encoded, encodeLongitude(decoded)); + assertEquals(encoded, encodeLongitudeCeil(decoded)); + // test within the range + if (encoded != Integer.MAX_VALUE) { + // this is the next representable value + // all double values between [min .. max) should encode to the current integer + // all double values between (min .. max] should encodeCeil to the next integer. + double max = min + LONGITUDE_DECODE; + assertEquals(max, decodeLongitude(encoded + 1), 0.0D); + assertEquals(encoded + 1, encodeLongitude(max)); + assertEquals(encoded + 1, encodeLongitudeCeil(max)); + + // first and last doubles in range that will be quantized + double minEdge = Math.nextUp(min); + double maxEdge = Math.nextDown(max); + assertEquals(encoded, encodeLongitude(minEdge)); + assertEquals(encoded + 1, encodeLongitudeCeil(minEdge)); + assertEquals(encoded, encodeLongitude(maxEdge)); + assertEquals(encoded + 1, encodeLongitudeCeil(maxEdge)); + + // check random values within the double range + long minBits = NumericUtils.doubleToSortableLong(minEdge); + long maxBits = NumericUtils.doubleToSortableLong(maxEdge); + for (int j = 0; j < 100; j++) { + double value = NumericUtils.sortableLongToDouble(TestUtil.nextLong(random, minBits, maxBits)); + // round down + assertEquals(encoded, encodeLongitude(value)); + // round up + assertEquals(encoded + 1, encodeLongitudeCeil(value)); + } + } + } + } + + // check edge/interesting cases explicitly + public void testEncodeEdgeCases() { + assertEquals(Integer.MIN_VALUE, encodeLatitude(MIN_LAT_INCL)); + assertEquals(Integer.MIN_VALUE, encodeLatitudeCeil(MIN_LAT_INCL)); + assertEquals(Integer.MAX_VALUE, encodeLatitude(MAX_LAT_INCL)); + assertEquals(Integer.MAX_VALUE, encodeLatitudeCeil(MAX_LAT_INCL)); + + assertEquals(Integer.MIN_VALUE, encodeLongitude(MIN_LON_INCL)); + assertEquals(Integer.MIN_VALUE, encodeLongitudeCeil(MIN_LON_INCL)); + assertEquals(Integer.MAX_VALUE, encodeLongitude(MAX_LON_INCL)); + assertEquals(Integer.MAX_VALUE, encodeLongitudeCeil(MAX_LON_INCL)); + } +} diff --git a/libs/geo/src/test/java/org/elasticsearch/geo/TestGeoJSONParsing.java b/libs/geo/src/test/java/org/elasticsearch/geo/TestGeoJSONParsing.java new file mode 100644 index 0000000000000..26dd48e4ea21f --- /dev/null +++ b/libs/geo/src/test/java/org/elasticsearch/geo/TestGeoJSONParsing.java @@ -0,0 +1,266 @@ +/* + * 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.geo; + +import org.apache.lucene.geo.Polygon; +import org.apache.lucene.util.LuceneTestCase; + +import java.text.ParseException; + +public class TestGeoJSONParsing extends LuceneTestCase { + public void testGeoJSONPolygon() throws Exception { + StringBuilder b = new StringBuilder(); + b.append("{\n"); + b.append(" \"type\": \"Polygon\",\n"); + b.append(" \"coordinates\": [\n"); + b.append(" [ [100.0, 0.0], [101.0, 0.0], [101.0, 1.0],\n"); + b.append(" [100.0, 1.0], [100.0, 0.0] ]\n"); + b.append(" ]\n"); + b.append("}\n"); + + Polygon[] polygons = Polygon.fromGeoJSON(b.toString()); + assertEquals(1, polygons.length); + assertEquals(new Polygon(new double[]{0.0, 0.0, 1.0, 1.0, 0.0}, + new double[]{100.0, 101.0, 101.0, 100.0, 100.0}), polygons[0]); + } + + public void testGeoJSONPolygonWithHole() throws Exception { + StringBuilder b = new StringBuilder(); + b.append("{\n"); + b.append(" \"type\": \"Polygon\",\n"); + b.append(" \"coordinates\": [\n"); + b.append(" [ [100.0, 0.0], [101.0, 0.0], [101.0, 1.0],\n"); + b.append(" [100.0, 1.0], [100.0, 0.0] ],\n"); + b.append(" [ [100.5, 0.5], [100.5, 0.75], [100.75, 0.75], [100.75, 0.5], [100.5, 0.5]]\n"); + b.append(" ]\n"); + b.append("}\n"); + + Polygon hole = new Polygon(new double[]{0.5, 0.75, 0.75, 0.5, 0.5}, + new double[]{100.5, 100.5, 100.75, 100.75, 100.5}); + Polygon expected = new Polygon(new double[]{0.0, 0.0, 1.0, 1.0, 0.0}, + new double[]{100.0, 101.0, 101.0, 100.0, 100.0}, hole); + Polygon[] polygons = Polygon.fromGeoJSON(b.toString()); + + assertEquals(1, polygons.length); + assertEquals(expected, polygons[0]); + } + + // a MultiPolygon returns multiple Polygons + public void testGeoJSONMultiPolygon() throws Exception { + StringBuilder b = new StringBuilder(); + b.append("{\n"); + b.append(" \"type\": \"MultiPolygon\",\n"); + b.append(" \"coordinates\": [\n"); + b.append(" [\n"); + b.append(" [ [100.0, 0.0], [101.0, 0.0], [101.0, 1.0],\n"); + b.append(" [100.0, 1.0], [100.0, 0.0] ]\n"); + b.append(" ],\n"); + b.append(" [\n"); + b.append(" [ [10.0, 2.0], [11.0, 2.0], [11.0, 3.0],\n"); + b.append(" [10.0, 3.0], [10.0, 2.0] ]\n"); + b.append(" ]\n"); + b.append(" ],\n"); + b.append("}\n"); + + Polygon[] polygons = Polygon.fromGeoJSON(b.toString()); + assertEquals(2, polygons.length); + assertEquals(new Polygon(new double[]{0.0, 0.0, 1.0, 1.0, 0.0}, + new double[]{100.0, 101.0, 101.0, 100.0, 100.0}), polygons[0]); + assertEquals(new Polygon(new double[]{2.0, 2.0, 3.0, 3.0, 2.0}, + new double[]{10.0, 11.0, 11.0, 10.0, 10.0}), polygons[1]); + } + + // make sure type can appear last (JSON allows arbitrary key/value order for objects) + public void testGeoJSONTypeComesLast() throws Exception { + StringBuilder b = new StringBuilder(); + b.append("{\n"); + b.append(" \"coordinates\": [\n"); + b.append(" [ [100.0, 0.0], [101.0, 0.0], [101.0, 1.0],\n"); + b.append(" [100.0, 1.0], [100.0, 0.0] ]\n"); + b.append(" ],\n"); + b.append(" \"type\": \"Polygon\",\n"); + b.append("}\n"); + + Polygon[] polygons = Polygon.fromGeoJSON(b.toString()); + assertEquals(1, polygons.length); + assertEquals(new Polygon(new double[]{0.0, 0.0, 1.0, 1.0, 0.0}, + new double[]{100.0, 101.0, 101.0, 100.0, 100.0}), polygons[0]); + } + + // make sure Polygon inside a type: Feature also works + public void testGeoJSONPolygonFeature() throws Exception { + StringBuilder b = new StringBuilder(); + b.append("{ \"type\": \"Feature\",\n"); + b.append(" \"geometry\": {\n"); + b.append(" \"type\": \"Polygon\",\n"); + b.append(" \"coordinates\": [\n"); + b.append(" [ [100.0, 0.0], [101.0, 0.0], [101.0, 1.0],\n"); + b.append(" [100.0, 1.0], [100.0, 0.0] ]\n"); + b.append(" ]\n"); + b.append(" },\n"); + b.append(" \"properties\": {\n"); + b.append(" \"prop0\": \"value0\",\n"); + b.append(" \"prop1\": {\"this\": \"that\"}\n"); + b.append(" }\n"); + b.append("}\n"); + + Polygon[] polygons = Polygon.fromGeoJSON(b.toString()); + assertEquals(1, polygons.length); + assertEquals(new Polygon(new double[]{0.0, 0.0, 1.0, 1.0, 0.0}, + new double[]{100.0, 101.0, 101.0, 100.0, 100.0}), polygons[0]); + } + + // make sure MultiPolygon inside a type: Feature also works + public void testGeoJSONMultiPolygonFeature() throws Exception { + StringBuilder b = new StringBuilder(); + b.append("{ \"type\": \"Feature\",\n"); + b.append(" \"geometry\": {\n"); + b.append(" \"type\": \"MultiPolygon\",\n"); + b.append(" \"coordinates\": [\n"); + b.append(" [\n"); + b.append(" [ [100.0, 0.0], [101.0, 0.0], [101.0, 1.0],\n"); + b.append(" [100.0, 1.0], [100.0, 0.0] ]\n"); + b.append(" ],\n"); + b.append(" [\n"); + b.append(" [ [10.0, 2.0], [11.0, 2.0], [11.0, 3.0],\n"); + b.append(" [10.0, 3.0], [10.0, 2.0] ]\n"); + b.append(" ]\n"); + b.append(" ]\n"); + b.append(" },\n"); + b.append(" \"properties\": {\n"); + b.append(" \"prop0\": \"value0\",\n"); + b.append(" \"prop1\": {\"this\": \"that\"}\n"); + b.append(" }\n"); + b.append("}\n"); + + Polygon[] polygons = Polygon.fromGeoJSON(b.toString()); + assertEquals(2, polygons.length); + assertEquals(new Polygon(new double[]{0.0, 0.0, 1.0, 1.0, 0.0}, + new double[]{100.0, 101.0, 101.0, 100.0, 100.0}), polygons[0]); + assertEquals(new Polygon(new double[]{2.0, 2.0, 3.0, 3.0, 2.0}, + new double[]{10.0, 11.0, 11.0, 10.0, 10.0}), polygons[1]); + } + + // FeatureCollection with one geometry is allowed: + public void testGeoJSONFeatureCollectionWithSinglePolygon() throws Exception { + StringBuilder b = new StringBuilder(); + b.append("{ \"type\": \"FeatureCollection\",\n"); + b.append(" \"features\": [\n"); + b.append(" { \"type\": \"Feature\",\n"); + b.append(" \"geometry\": {\n"); + b.append(" \"type\": \"Polygon\",\n"); + b.append(" \"coordinates\": [\n"); + b.append(" [ [100.0, 0.0], [101.0, 0.0], [101.0, 1.0],\n"); + b.append(" [100.0, 1.0], [100.0, 0.0] ]\n"); + b.append(" ]\n"); + b.append(" },\n"); + b.append(" \"properties\": {\n"); + b.append(" \"prop0\": \"value0\",\n"); + b.append(" \"prop1\": {\"this\": \"that\"}\n"); + b.append(" }\n"); + b.append(" }\n"); + b.append(" ]\n"); + b.append("} \n"); + + Polygon expected = new Polygon(new double[]{0.0, 0.0, 1.0, 1.0, 0.0}, + new double[]{100.0, 101.0, 101.0, 100.0, 100.0}); + Polygon[] actual = Polygon.fromGeoJSON(b.toString()); + assertEquals(1, actual.length); + assertEquals(expected, actual[0]); + } + + // stuff after the object is not allowed + public void testIllegalGeoJSONExtraCrapAtEnd() throws Exception { + StringBuilder b = new StringBuilder(); + b.append("{\n"); + b.append(" \"type\": \"Polygon\",\n"); + b.append(" \"coordinates\": [\n"); + b.append(" [ [100.0, 0.0], [101.0, 0.0], [101.0, 1.0],\n"); + b.append(" [100.0, 1.0], [100.0, 0.0] ]\n"); + b.append(" ]\n"); + b.append("}\n"); + b.append("foo\n"); + + Exception e = expectThrows(ParseException.class, () -> Polygon.fromGeoJSON(b.toString())); + assertTrue(e.getMessage().contains("unexpected character 'f' after end of GeoJSON object")); + } + + public void testIllegalGeoJSONLinkedCRS() throws Exception { + + StringBuilder b = new StringBuilder(); + b.append("{\n"); + b.append(" \"type\": \"Polygon\",\n"); + b.append(" \"coordinates\": [\n"); + b.append(" [ [100.0, 0.0], [101.0, 0.0], [101.0, 1.0],\n"); + b.append(" [100.0, 1.0], [100.0, 0.0] ]\n"); + b.append(" ],\n"); + b.append(" \"crs\": {\n"); + b.append(" \"type\": \"link\",\n"); + b.append(" \"properties\": {\n"); + b.append(" \"href\": \"http://example.com/crs/42\",\n"); + b.append(" \"type\": \"proj4\"\n"); + b.append(" }\n"); + b.append(" } \n"); + b.append("}\n"); + Exception e = expectThrows(ParseException.class, () -> Polygon.fromGeoJSON(b.toString())); + assertTrue(e.getMessage().contains("cannot handle linked crs")); + } + + // FeatureCollection with more than one geometry is not supported: + public void testIllegalGeoJSONMultipleFeatures() throws Exception { + StringBuilder b = new StringBuilder(); + b.append("{ \"type\": \"FeatureCollection\",\n"); + b.append(" \"features\": [\n"); + b.append(" { \"type\": \"Feature\",\n"); + b.append(" \"geometry\": {\"type\": \"Point\", \"coordinates\": [102.0, 0.5]},\n"); + b.append(" \"properties\": {\"prop0\": \"value0\"}\n"); + b.append(" },\n"); + b.append(" { \"type\": \"Feature\",\n"); + b.append(" \"geometry\": {\n"); + b.append(" \"type\": \"LineString\",\n"); + b.append(" \"coordinates\": [\n"); + b.append(" [102.0, 0.0], [103.0, 1.0], [104.0, 0.0], [105.0, 1.0]\n"); + b.append(" ]\n"); + b.append(" },\n"); + b.append(" \"properties\": {\n"); + b.append(" \"prop0\": \"value0\",\n"); + b.append(" \"prop1\": 0.0\n"); + b.append(" }\n"); + b.append(" },\n"); + b.append(" { \"type\": \"Feature\",\n"); + b.append(" \"geometry\": {\n"); + b.append(" \"type\": \"Polygon\",\n"); + b.append(" \"coordinates\": [\n"); + b.append(" [ [100.0, 0.0], [101.0, 0.0], [101.0, 1.0],\n"); + b.append(" [100.0, 1.0], [100.0, 0.0] ]\n"); + b.append(" ]\n"); + b.append(" },\n"); + b.append(" \"properties\": {\n"); + b.append(" \"prop0\": \"value0\",\n"); + b.append(" \"prop1\": {\"this\": \"that\"}\n"); + b.append(" }\n"); + b.append(" }\n"); + b.append(" ]\n"); + b.append("} \n"); + + Exception e = expectThrows(ParseException.class, () -> Polygon.fromGeoJSON(b.toString())); + assertTrue(e.getMessage().contains("can only handle type FeatureCollection (if it has a single polygon geometry), Feature, Polygon or MutiPolygon, but got Point")); + } +} diff --git a/libs/geo/src/test/java/org/elasticsearch/geo/TestGeoUtils.java b/libs/geo/src/test/java/org/elasticsearch/geo/TestGeoUtils.java new file mode 100644 index 0000000000000..5f51b796e2248 --- /dev/null +++ b/libs/geo/src/test/java/org/elasticsearch/geo/TestGeoUtils.java @@ -0,0 +1,315 @@ +/* + * 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.geo; + +import org.apache.lucene.geo.EarthDebugger; +import org.apache.lucene.geo.GeoTestUtil; +import org.apache.lucene.geo.GeoUtils; +import org.apache.lucene.geo.Rectangle; +import org.apache.lucene.util.LuceneTestCase; +import org.apache.lucene.util.SloppyMath; + +import java.util.Locale; + +/** + * Tests class for methods in GeoUtils + */ +public class TestGeoUtils extends LuceneTestCase { + + // We rely heavily on GeoUtils.circleToBBox so we test it here: + public void testRandomCircleToBBox() throws Exception { + int iters = atLeast(100); + for (int iter = 0; iter < iters; iter++) { + + double centerLat = GeoTestUtil.nextLatitude(); + double centerLon = GeoTestUtil.nextLongitude(); + + final double radiusMeters; + if (random().nextBoolean()) { + // Approx 4 degrees lon at the equator: + radiusMeters = random().nextDouble() * 444000; + } else { + radiusMeters = random().nextDouble() * 50000000; + } + + // TODO: randomly quantize radius too, to provoke exact math errors? + + Rectangle bbox = Rectangle.fromPointDistance(centerLat, centerLon, radiusMeters); + + int numPointsToTry = 1000; + for (int i = 0; i < numPointsToTry; i++) { + + double point[] = GeoTestUtil.nextPointNear(bbox); + double lat = point[0]; + double lon = point[1]; + + double distanceMeters = SloppyMath.haversinMeters(centerLat, centerLon, lat, lon); + + // Haversin says it's within the circle: + boolean haversinSays = distanceMeters <= radiusMeters; + + // BBox says its within the box: + boolean bboxSays; + if (bbox.crossesDateline()) { + if (lat >= bbox.minLat && lat <= bbox.maxLat) { + bboxSays = lon <= bbox.maxLon || lon >= bbox.minLon; + } else { + bboxSays = false; + } + } else { + bboxSays = lat >= bbox.minLat && lat <= bbox.maxLat && lon >= bbox.minLon && lon <= bbox.maxLon; + } + + if (haversinSays) { + if (bboxSays == false) { + System.out.println("centerLat=" + centerLat + " centerLon=" + centerLon + " radiusMeters=" + radiusMeters); + System.out.println(" bbox: lat=" + bbox.minLat + " to " + bbox.maxLat + " lon=" + bbox.minLon + " to " + bbox.maxLon); + System.out.println(" point: lat=" + lat + " lon=" + lon); + System.out.println(" haversin: " + distanceMeters); + fail("point was within the distance according to haversin, but the bbox doesn't contain it"); + } + } else { + // it's fine if haversin said it was outside the radius and bbox said it was inside the box + } + } + } + } + + // similar to testRandomCircleToBBox, but different, less evil, maybe simpler + public void testBoundingBoxOpto() { + int iters = atLeast(100); + for (int i = 0; i < iters; i++) { + double lat = GeoTestUtil.nextLatitude(); + double lon = GeoTestUtil.nextLongitude(); + double radius = 50000000 * random().nextDouble(); + Rectangle box = Rectangle.fromPointDistance(lat, lon, radius); + final Rectangle box1; + final Rectangle box2; + if (box.crossesDateline()) { + box1 = new Rectangle(box.minLat, box.maxLat, -180, box.maxLon); + box2 = new Rectangle(box.minLat, box.maxLat, box.minLon, 180); + } else { + box1 = box; + box2 = null; + } + + for (int j = 0; j < 1000; j++) { + double point[] = GeoTestUtil.nextPointNear(box); + double lat2 = point[0]; + double lon2 = point[1]; + // if the point is within radius, then it should be in our bounding box + if (SloppyMath.haversinMeters(lat, lon, lat2, lon2) <= radius) { + assertTrue(lat >= box.minLat && lat <= box.maxLat); + assertTrue(lon >= box1.minLon && lon <= box1.maxLon || (box2 != null && lon >= box2.minLon && lon <= box2.maxLon)); + } + } + } + } + + // test we can use haversinSortKey() for distance queries. + public void testHaversinOpto() { + int iters = atLeast(100); + for (int i = 0; i < iters; i++) { + double lat = GeoTestUtil.nextLatitude(); + double lon = GeoTestUtil.nextLongitude(); + double radius = 50000000 * random().nextDouble(); + Rectangle box = Rectangle.fromPointDistance(lat, lon, radius); + + if (box.maxLon - lon < 90 && lon - box.minLon < 90) { + double minPartialDistance = Math.max(SloppyMath.haversinSortKey(lat, lon, lat, box.maxLon), + SloppyMath.haversinSortKey(lat, lon, box.maxLat, lon)); + + for (int j = 0; j < 10000; j++) { + double point[] = GeoTestUtil.nextPointNear(box); + double lat2 = point[0]; + double lon2 = point[1]; + // if the point is within radius, then it should be <= our sort key + if (SloppyMath.haversinMeters(lat, lon, lat2, lon2) <= radius) { + assertTrue(SloppyMath.haversinSortKey(lat, lon, lat2, lon2) <= minPartialDistance); + } + } + } + } + } + + /** + * Test infinite radius covers whole earth + */ + public void testInfiniteRect() { + for (int i = 0; i < 1000; i++) { + double centerLat = GeoTestUtil.nextLatitude(); + double centerLon = GeoTestUtil.nextLongitude(); + Rectangle rect = Rectangle.fromPointDistance(centerLat, centerLon, Double.POSITIVE_INFINITY); + assertEquals(-180.0, rect.minLon, 0.0D); + assertEquals(180.0, rect.maxLon, 0.0D); + assertEquals(-90.0, rect.minLat, 0.0D); + assertEquals(90.0, rect.maxLat, 0.0D); + assertFalse(rect.crossesDateline()); + } + } + + public void testAxisLat() { + double earthCircumference = 2D * Math.PI * GeoUtils.EARTH_MEAN_RADIUS_METERS; + assertEquals(90, Rectangle.axisLat(0, earthCircumference / 4), 0.0D); + + for (int i = 0; i < 100; ++i) { + boolean reallyBig = random().nextInt(10) == 0; + final double maxRadius = reallyBig ? 1.1 * earthCircumference : earthCircumference / 8; + final double radius = maxRadius * random().nextDouble(); + double prevAxisLat = Rectangle.axisLat(0.0D, radius); + for (double lat = 0.1D; lat < 90D; lat += 0.1D) { + double nextAxisLat = Rectangle.axisLat(lat, radius); + Rectangle bbox = Rectangle.fromPointDistance(lat, 180D, radius); + double dist = SloppyMath.haversinMeters(lat, 180D, nextAxisLat, bbox.maxLon); + if (nextAxisLat < GeoUtils.MAX_LAT_INCL) { + assertEquals("lat = " + lat, dist, radius, 0.1D); + } + assertTrue("lat = " + lat, prevAxisLat <= nextAxisLat); + prevAxisLat = nextAxisLat; + } + + prevAxisLat = Rectangle.axisLat(-0.0D, radius); + for (double lat = -0.1D; lat > -90D; lat -= 0.1D) { + double nextAxisLat = Rectangle.axisLat(lat, radius); + Rectangle bbox = Rectangle.fromPointDistance(lat, 180D, radius); + double dist = SloppyMath.haversinMeters(lat, 180D, nextAxisLat, bbox.maxLon); + if (nextAxisLat > GeoUtils.MIN_LAT_INCL) { + assertEquals("lat = " + lat, dist, radius, 0.1D); + } + assertTrue("lat = " + lat, prevAxisLat >= nextAxisLat); + prevAxisLat = nextAxisLat; + } + } + } + + // TODO: does not really belong here, but we test it like this for now + // we can make a fake IndexReader to send boxes directly to Point visitors instead? + public void testCircleOpto() throws Exception { + int iters = atLeast(20); + for (int i = 0; i < iters; i++) { + // circle + final double centerLat = -90 + 180.0 * random().nextDouble(); + final double centerLon = -180 + 360.0 * random().nextDouble(); + final double radius = 50_000_000D * random().nextDouble(); + final Rectangle box = Rectangle.fromPointDistance(centerLat, centerLon, radius); + // TODO: remove this leniency! + if (box.crossesDateline()) { + --i; // try again... + continue; + } + final double axisLat = Rectangle.axisLat(centerLat, radius); + + for (int k = 0; k < 1000; ++k) { + + double[] latBounds = {-90, box.minLat, axisLat, box.maxLat, 90}; + double[] lonBounds = {-180, box.minLon, centerLon, box.maxLon, 180}; + // first choose an upper left corner + int maxLatRow = random().nextInt(4); + double latMax = randomInRange(latBounds[maxLatRow], latBounds[maxLatRow + 1]); + int minLonCol = random().nextInt(4); + double lonMin = randomInRange(lonBounds[minLonCol], lonBounds[minLonCol + 1]); + // now choose a lower right corner + int minLatMaxRow = maxLatRow == 3 ? 3 : maxLatRow + 1; // make sure it will at least cross into the bbox + int minLatRow = random().nextInt(minLatMaxRow); + double latMin = randomInRange(latBounds[minLatRow], Math.min(latBounds[minLatRow + 1], latMax)); + int maxLonMinCol = Math.max(minLonCol, 1); // make sure it will at least cross into the bbox + int maxLonCol = maxLonMinCol + random().nextInt(4 - maxLonMinCol); + double lonMax = randomInRange(Math.max(lonBounds[maxLonCol], lonMin), lonBounds[maxLonCol + 1]); + + assert latMax >= latMin; + assert lonMax >= lonMin; + + if (isDisjoint(centerLat, centerLon, radius, axisLat, latMin, latMax, lonMin, lonMax)) { + // intersects says false: test a ton of points + for (int j = 0; j < 200; j++) { + double lat = latMin + (latMax - latMin) * random().nextDouble(); + double lon = lonMin + (lonMax - lonMin) * random().nextDouble(); + + if (random().nextBoolean()) { + // explicitly test an edge + int edge = random().nextInt(4); + if (edge == 0) { + lat = latMin; + } else if (edge == 1) { + lat = latMax; + } else if (edge == 2) { + lon = lonMin; + } else if (edge == 3) { + lon = lonMax; + } + } + double distance = SloppyMath.haversinMeters(centerLat, centerLon, lat, lon); + try { + assertTrue(String.format(Locale.ROOT, "\nisDisjoint(\n" + + "centerLat=%s\n" + + "centerLon=%s\n" + + "radius=%s\n" + + "latMin=%s\n" + + "latMax=%s\n" + + "lonMin=%s\n" + + "lonMax=%s) == false BUT\n" + + "haversin(%s, %s, %s, %s) = %s\nbbox=%s", + centerLat, centerLon, radius, latMin, latMax, lonMin, lonMax, + centerLat, centerLon, lat, lon, distance, Rectangle.fromPointDistance(centerLat, centerLon, radius)), + distance > radius); + } catch (AssertionError e) { + EarthDebugger ed = new EarthDebugger(); + ed.addRect(latMin, latMax, lonMin, lonMax); + ed.addCircle(centerLat, centerLon, radius, true); + System.out.println(ed.finish()); + throw e; + } + } + } + } + } + } + + static double randomInRange(double min, double max) { + return min + (max - min) * random().nextDouble(); + } + + static boolean isDisjoint(double centerLat, double centerLon, double radius, double axisLat, double latMin, double latMax, double lonMin, double lonMax) { + if ((centerLon < lonMin || centerLon > lonMax) && (axisLat + Rectangle.AXISLAT_ERROR < latMin || axisLat - Rectangle.AXISLAT_ERROR > latMax)) { + // circle not fully inside / crossing axis + if (SloppyMath.haversinMeters(centerLat, centerLon, latMin, lonMin) > radius && + SloppyMath.haversinMeters(centerLat, centerLon, latMin, lonMax) > radius && + SloppyMath.haversinMeters(centerLat, centerLon, latMax, lonMin) > radius && + SloppyMath.haversinMeters(centerLat, centerLon, latMax, lonMax) > radius) { + // no points inside + return true; + } + } + + return false; + } + + public void testWithin90LonDegrees() { + assertTrue(GeoUtils.within90LonDegrees(0, -80, 80)); + assertFalse(GeoUtils.within90LonDegrees(0, -100, 80)); + assertFalse(GeoUtils.within90LonDegrees(0, -80, 100)); + + assertTrue(GeoUtils.within90LonDegrees(-150, 140, 170)); + assertFalse(GeoUtils.within90LonDegrees(-150, 120, 150)); + + assertTrue(GeoUtils.within90LonDegrees(150, -170, -140)); + assertFalse(GeoUtils.within90LonDegrees(150, -150, -120)); + } +} diff --git a/libs/geo/src/test/java/org/elasticsearch/geo/geometry/BaseGeometryTestCase.java b/libs/geo/src/test/java/org/elasticsearch/geo/geometry/BaseGeometryTestCase.java new file mode 100644 index 0000000000000..d003c26de86c9 --- /dev/null +++ b/libs/geo/src/test/java/org/elasticsearch/geo/geometry/BaseGeometryTestCase.java @@ -0,0 +1,100 @@ +/* + * 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.geo.geometry; + +import org.apache.lucene.geo.GeoUtils; +import org.apache.lucene.util.LuceneTestCase; + +abstract class BaseGeometryTestCase extends LuceneTestCase { + abstract public T getShape(); + + public void testArea() { + expectThrows(UnsupportedOperationException.class, () -> getShape().getArea()); + } + + /** + * tests bounding box of shape + */ + abstract public void testBoundingBox(); + + /** + * tests WITHIN relation + */ + abstract public void testWithin(); + + /** + * tests CONTAINS relation + */ + abstract public void testContains(); + + /** + * tests DISJOINT relation + */ + abstract public void testDisjoint(); + + /** + * tests INTERSECTS relation + */ + abstract public void testIntersects(); + + /** + * tests center of shape + */ + public void testCenter() { + GeoShape shape = getShape(); + Rectangle bbox = shape.getBoundingBox(); + double centerLat = StrictMath.abs(bbox.maxLat() - bbox.minLat()) * 0.5 + bbox.minLat(); + double centerLon; + if (bbox.crossesDateline()) { + centerLon = GeoUtils.MAX_LON_INCL - bbox.minLon() + bbox.maxLon() - GeoUtils.MIN_LON_INCL; + centerLon = GeoUtils.normalizeLonDegrees(centerLon * 0.5 + bbox.minLon()); + } else { + centerLon = StrictMath.abs(bbox.maxLon() - bbox.minLon()) * 0.5 + bbox.minLon(); + } + assertEquals(shape.getCenter(), new Point(centerLat, centerLon)); + } + + /** + * helper method for semi-random relation testing + */ + protected void relationTest(GeoShape points, GeoShape.Relation r) { + Rectangle bbox = points.getBoundingBox(); + double minLat = bbox.minLat(); + double maxLat = bbox.maxLat(); + double minLon = bbox.minLon(); + double maxLon = bbox.maxLon(); + + if (r == GeoShape.Relation.WITHIN) { + return; + } else if (r == GeoShape.Relation.DISJOINT) { + // shrink test box + minLat -= 20D; + maxLat = minLat - 1D; + minLon -= 20D; + maxLon = minLon - 1D; + } else if (r == GeoShape.Relation.INTERSECTS) { + // intersects (note: MultiPoint does not support CONTAINS) + minLat -= 10D; + maxLat = minLat + 10D; + } + + assertEquals(r, points.relate(minLat, maxLat, minLon, maxLon)); + } +} diff --git a/libs/geo/src/test/java/org/elasticsearch/geo/geometry/TestEdgeTree.java b/libs/geo/src/test/java/org/elasticsearch/geo/geometry/TestEdgeTree.java new file mode 100644 index 0000000000000..970598ef213f9 --- /dev/null +++ b/libs/geo/src/test/java/org/elasticsearch/geo/geometry/TestEdgeTree.java @@ -0,0 +1,312 @@ +/* + * 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.geo.geometry; + +import org.apache.lucene.geo.GeoTestUtil; +import org.apache.lucene.index.PointValues.Relation; +import org.apache.lucene.util.LuceneTestCase; + +import static org.apache.lucene.geo.GeoTestUtil.nextLatitude; +import static org.apache.lucene.geo.GeoTestUtil.nextLongitude; +import static org.apache.lucene.geo.GeoTestUtil.nextPolygon; + +/** + * Test EdgeTree impl + */ +public class TestEdgeTree extends LuceneTestCase { + + /** + * Three boxes, an island inside a hole inside a shape + */ + public void testMultiPolygon() { + Polygon hole = new Polygon(new double[]{-10, -10, 10, 10, -10}, new double[]{-10, 10, 10, -10, -10}); + Polygon outer = new Polygon(new double[]{-50, -50, 50, 50, -50}, new double[]{-50, 50, 50, -50, -50}, hole); + Polygon island = new Polygon(new double[]{-5, -5, 5, 5, -5}, new double[]{-5, 5, 5, -5, -5}); + EdgeTree polygon = EdgeTree.create(outer, island); + + // contains(point) + assertTrue(polygon.contains(-2, 2)); // on the island + assertFalse(polygon.contains(-6, 6)); // in the hole + assertTrue(polygon.contains(-25, 25)); // on the mainland + assertFalse(polygon.contains(-51, 51)); // in the ocean + + // relate(box): this can conservatively return CELL_CROSSES_QUERY + assertEquals(Relation.CELL_INSIDE_QUERY, polygon.relate(-2, 2, -2, 2)); // on the island + assertEquals(Relation.CELL_OUTSIDE_QUERY, polygon.relate(6, 7, 6, 7)); // in the hole + assertEquals(Relation.CELL_INSIDE_QUERY, polygon.relate(24, 25, 24, 25)); // on the mainland + assertEquals(Relation.CELL_OUTSIDE_QUERY, polygon.relate(51, 52, 51, 52)); // in the ocean + assertEquals(Relation.CELL_CROSSES_QUERY, polygon.relate(-60, 60, -60, 60)); // enclosing us completely + assertEquals(Relation.CELL_CROSSES_QUERY, polygon.relate(49, 51, 49, 51)); // overlapping the mainland + assertEquals(Relation.CELL_CROSSES_QUERY, polygon.relate(9, 11, 9, 11)); // overlapping the hole + assertEquals(Relation.CELL_CROSSES_QUERY, polygon.relate(5, 6, 5, 6)); // overlapping the island + } + + public void testPacMan() throws Exception { + // pacman + double[] px = {0, 10, 10, 0, -8, -10, -8, 0, 10, 10, 0}; + double[] py = {0, 5, 9, 10, 9, 0, -9, -10, -9, -5, 0}; + + // candidate crosses cell + double xMin = 2;//-5; + double xMax = 11;//0.000001; + double yMin = -1;//0; + double yMax = 1;//5; + + // test cell crossing poly + EdgeTree polygon = EdgeTree.create(new Polygon(py, px)); + assertEquals(Relation.CELL_CROSSES_QUERY, polygon.relate(yMin, yMax, xMin, xMax)); + } + + public void testBoundingBox() throws Exception { + for (int i = 0; i < 100; i++) { + EdgeTree polygon = EdgeTree.create(nextPolygon()); + + for (int j = 0; j < 100; j++) { + double latitude = nextLatitude(); + double longitude = nextLongitude(); + // if the point is within poly, then it should be in our bounding box + if (polygon.contains(latitude, longitude)) { + assertTrue(latitude >= polygon.minLat && latitude <= polygon.maxLat); + assertTrue(longitude >= polygon.minLon && longitude <= polygon.maxLon); + } + } + } + } + + // targets the bounding box directly + public void testBoundingBoxEdgeCases() throws Exception { + for (int i = 0; i < 100; i++) { + Polygon polygon = nextPolygon(); + EdgeTree impl = EdgeTree.create(polygon); + + for (int j = 0; j < 100; j++) { + double point[] = GeoTestUtil.nextPointNear(polygon); + double latitude = point[0]; + double longitude = point[1]; + // if the point is within poly, then it should be in our bounding box + if (impl.contains(latitude, longitude)) { + assertTrue(latitude >= polygon.minLat() && latitude <= polygon.maxLat()); + assertTrue(longitude >= polygon.minLon() && longitude <= polygon.maxLon()); + } + } + } + } + + /** + * If polygon.contains(box) returns true, then any point in that box should return true as well + */ + public void testContainsRandom() throws Exception { + int iters = atLeast(50); + for (int i = 0; i < iters; i++) { + Polygon polygon = nextPolygon(); + EdgeTree impl = EdgeTree.create(polygon); + + for (int j = 0; j < 100; j++) { + Rectangle rectangle = GeoTestUtil.nextBoxNear(polygon); + // allowed to conservatively return false + if (impl.relate(rectangle.minLat, rectangle.maxLat, rectangle.minLon, rectangle.maxLon) == GeoShape.Relation.WITHIN) { + for (int k = 0; k < 500; k++) { + // this tests in our range but sometimes outside! so we have to double-check its really in other box + double point[] = GeoTestUtil.nextPointNear(rectangle); + double latitude = point[0]; + double longitude = point[1]; + // check for sure its in our box + if (latitude >= rectangle.minLat && latitude <= rectangle.maxLat && longitude >= rectangle.minLon && longitude <= rectangle.maxLon) { + assertTrue(impl.contains(latitude, longitude)); + } + } + for (int k = 0; k < 100; k++) { + // this tests in our range but sometimes outside! so we have to double-check its really in other box + double point[] = GeoTestUtil.nextPointNear(polygon); + double latitude = point[0]; + double longitude = point[1]; + // check for sure its in our box + if (latitude >= rectangle.minLat && latitude <= rectangle.maxLat && longitude >= rectangle.minLon && longitude <= rectangle.maxLon) { + assertTrue(impl.contains(latitude, longitude)); + } + } + } + } + } + } + + /** + * If polygon.contains(box) returns true, then any point in that box should return true as well + */ + // different from testContainsRandom in that its not a purely random test. we iterate the vertices of the polygon + // and generate boxes near each one of those to try to be more efficient. + public void testContainsEdgeCases() throws Exception { + for (int i = 0; i < 1000; i++) { + Polygon polygon = nextPolygon(); + EdgeTree impl = EdgeTree.create(polygon); + + for (int j = 0; j < 10; j++) { + Rectangle rectangle = GeoTestUtil.nextBoxNear(polygon); + // allowed to conservatively return false + if (impl.relate(rectangle.minLat, rectangle.maxLat, rectangle.minLon, rectangle.maxLon) == GeoShape.Relation.WITHIN) { + for (int k = 0; k < 100; k++) { + // this tests in our range but sometimes outside! so we have to double-check its really in other box + double point[] = GeoTestUtil.nextPointNear(rectangle); + double latitude = point[0]; + double longitude = point[1]; + // check for sure its in our box + if (latitude >= rectangle.minLat && latitude <= rectangle.maxLat && longitude >= rectangle.minLon && longitude <= rectangle.maxLon) { + assertTrue(impl.contains(latitude, longitude)); + } + } + for (int k = 0; k < 20; k++) { + // this tests in our range but sometimes outside! so we have to double-check its really in other box + double point[] = GeoTestUtil.nextPointNear(polygon); + double latitude = point[0]; + double longitude = point[1]; + // check for sure its in our box + if (latitude >= rectangle.minLat && latitude <= rectangle.maxLat && longitude >= rectangle.minLon && longitude <= rectangle.maxLon) { + assertTrue(impl.contains(latitude, longitude)); + } + } + } + } + } + } + + /** + * If polygon.intersects(box) returns false, then any point in that box should return false as well + */ + public void testIntersectRandom() { + int iters = atLeast(10); + for (int i = 0; i < iters; i++) { + Polygon polygon = nextPolygon(); + EdgeTree impl = EdgeTree.create(polygon); + + for (int j = 0; j < 100; j++) { + Rectangle rectangle = GeoTestUtil.nextBoxNear(polygon); + // allowed to conservatively return true. + if (impl.relate(rectangle.minLat, rectangle.maxLat, rectangle.minLon, rectangle.maxLon) == GeoShape.Relation.DISJOINT) { + for (int k = 0; k < 1000; k++) { + double point[] = GeoTestUtil.nextPointNear(rectangle); + // this tests in our range but sometimes outside! so we have to double-check its really in other box + double latitude = point[0]; + double longitude = point[1]; + // check for sure its in our box + if (latitude >= rectangle.minLat && latitude <= rectangle.maxLat && longitude >= rectangle.minLon && longitude <= rectangle.maxLon) { + assertFalse(impl.contains(latitude, longitude)); + } + } + for (int k = 0; k < 100; k++) { + double point[] = GeoTestUtil.nextPointNear(polygon); + // this tests in our range but sometimes outside! so we have to double-check its really in other box + double latitude = point[0]; + double longitude = point[1]; + // check for sure its in our box + if (latitude >= rectangle.minLat && latitude <= rectangle.maxLat && longitude >= rectangle.minLon && longitude <= rectangle.maxLon) { + assertFalse(impl.contains(latitude, longitude)); + } + } + } + } + } + } + + /** + * If polygon.intersects(box) returns false, then any point in that box should return false as well + */ + // different from testIntersectsRandom in that its not a purely random test. we iterate the vertices of the polygon + // and generate boxes near each one of those to try to be more efficient. + public void testIntersectEdgeCases() { + for (int i = 0; i < 100; i++) { + Polygon polygon = nextPolygon(); + EdgeTree impl = EdgeTree.create(polygon); + + for (int j = 0; j < 10; j++) { + Rectangle rectangle = GeoTestUtil.nextBoxNear(polygon); + // allowed to conservatively return false. + if (impl.relate(rectangle.minLat, rectangle.maxLat, rectangle.minLon, rectangle.maxLon) == GeoShape.Relation.DISJOINT) { + for (int k = 0; k < 100; k++) { + // this tests in our range but sometimes outside! so we have to double-check its really in other box + double point[] = GeoTestUtil.nextPointNear(rectangle); + double latitude = point[0]; + double longitude = point[1]; + // check for sure its in our box + if (latitude >= rectangle.minLat && latitude <= rectangle.maxLat && longitude >= rectangle.minLon && longitude <= rectangle.maxLon) { + assertFalse(impl.contains(latitude, longitude)); + } + } + for (int k = 0; k < 50; k++) { + // this tests in our range but sometimes outside! so we have to double-check its really in other box + double point[] = GeoTestUtil.nextPointNear(polygon); + double latitude = point[0]; + double longitude = point[1]; + // check for sure its in our box + if (latitude >= rectangle.minLat && latitude <= rectangle.maxLat && longitude >= rectangle.minLon && longitude <= rectangle.maxLon) { + assertFalse(impl.contains(latitude, longitude)); + } + } + } + } + } + } + + /** + * Tests edge case behavior with respect to insideness + */ + public void testEdgeInsideness() { + EdgeTree poly = EdgeTree.create(new Polygon(new double[]{-2, -2, 2, 2, -2}, new double[]{-2, 2, 2, -2, -2})); + assertTrue(poly.contains(-2, -2)); // bottom left corner: true + assertFalse(poly.contains(-2, 2)); // bottom right corner: false + assertFalse(poly.contains(2, -2)); // top left corner: false + assertFalse(poly.contains(2, 2)); // top right corner: false + assertTrue(poly.contains(-2, -1)); // bottom side: true + assertTrue(poly.contains(-2, 0)); // bottom side: true + assertTrue(poly.contains(-2, 1)); // bottom side: true + assertFalse(poly.contains(2, -1)); // top side: false + assertFalse(poly.contains(2, 0)); // top side: false + assertFalse(poly.contains(2, 1)); // top side: false + assertFalse(poly.contains(-1, 2)); // right side: false + assertFalse(poly.contains(0, 2)); // right side: false + assertFalse(poly.contains(1, 2)); // right side: false + assertTrue(poly.contains(-1, -2)); // left side: true + assertTrue(poly.contains(0, -2)); // left side: true + assertTrue(poly.contains(1, -2)); // left side: true + } + + /** + * Tests current impl against original algorithm + */ + public void testContainsAgainstOriginal() { + int iters = atLeast(100); + for (int i = 0; i < iters; i++) { + Polygon polygon = nextPolygon(); + // currently we don't generate these, but this test does not want holes. + while (polygon.getHoles().length > 0) { + polygon = nextPolygon(); + } + EdgeTree impl = EdgeTree.create(polygon); + + // random lat/lons against polygon + for (int j = 0; j < 1000; j++) { + double point[] = GeoTestUtil.nextPointNear(polygon); + double latitude = point[0]; + double longitude = point[1]; + boolean expected = GeoTestUtil.containsSlowly(polygon, latitude, longitude); + assertEquals(expected, impl.contains(latitude, longitude)); + } + } + } +} diff --git a/libs/geo/src/test/java/org/elasticsearch/geo/geometry/TestLine.java b/libs/geo/src/test/java/org/elasticsearch/geo/geometry/TestLine.java new file mode 100644 index 0000000000000..02d01394aee94 --- /dev/null +++ b/libs/geo/src/test/java/org/elasticsearch/geo/geometry/TestLine.java @@ -0,0 +1,114 @@ +/* + * 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.geo.geometry; + +import org.apache.lucene.geo.GeoTestUtil; +import org.apache.lucene.geo.GeoUtils; +import org.apache.lucene.geo.geometry.GeoShape.Relation; +import org.junit.Ignore; + +import java.util.Arrays; + +import static com.carrotsearch.randomizedtesting.generators.RandomNumbers.randomIntBetween; +import static org.apache.lucene.geo.GeoTestUtil.nextLatitudeIn; +import static org.apache.lucene.geo.GeoTestUtil.nextLongitudeIn; + +/** + * Tests relations and features of Line types + */ +public class TestLine extends TestMultiPoint { + @Override + public Line getShape() { + return getShape(false); + } + + @Override + public Line getShape(boolean padded) { + double minFactor = padded == true ? 20D : 0D; + double maxFactor = padded == true ? -20D : 0D; + + // we can't have self crossing lines. + // since polygons share this contract we create an unclosed polygon + Polygon poly = GeoTestUtil.createRegularPolygon( + nextLatitudeIn(GeoUtils.MIN_LAT_INCL + minFactor, GeoUtils.MAX_LAT_INCL + maxFactor), + nextLongitudeIn(GeoUtils.MIN_LON_INCL + minFactor, GeoUtils.MAX_LON_INCL + maxFactor), + 100D, randomIntBetween(random(), 4, 100)); + final double[] lats = poly.getPolyLats(); + final double[] lons = poly.getPolyLons(); + return new Line(Arrays.copyOfRange(lats, 0, lats.length - 1), + Arrays.copyOfRange(lons, 0, lons.length - 1)); + } + + /** + * tests the bounding box of the line + */ + @Override + public void testBoundingBox() { + // lines are just MultiPoint with an edge tree; bounding box logic is the same + super.testBoundingBox(); + } + + /** + * tests the "center" of the line + */ + @Override + public void testCenter() { + // lines are just MultiPoint with an edge tree; center logic is the same + super.testCenter(); + } + + /** + * tests MultiPoints are within a box + */ + @Override + public void testWithin() { + relationTest(getShape(true), Relation.WITHIN); + } + + /** + * tests box is disjoint with a MultiPoint shape + */ + @Override + public void testDisjoint() { + relationTest(getShape(true), Relation.DISJOINT); + } + + /** + * IGNORED: Line does not contain other shapes + */ + @Ignore + @Override + public void testContains() { + } + + /** + * tests box intersection with a MultiPoint shape + */ + @Override + public void testIntersects() { + MultiPoint points = getShape(true); + Line line = new Line(points.getLats(), points.getLons()); + double minLat = StrictMath.min(line.getLat(0), line.getLat(1)); + double maxLat = StrictMath.max(line.getLat(0), line.getLat(1)); + double minLon = StrictMath.min(line.getLon(0), line.getLon(1)); + double maxLon = StrictMath.max(line.getLon(0), line.getLon(1)); + assertEquals(Relation.INTERSECTS, line.relate(minLat, maxLat, minLon, maxLon)); + } +} diff --git a/libs/geo/src/test/java/org/elasticsearch/geo/geometry/TestMultiLine.java b/libs/geo/src/test/java/org/elasticsearch/geo/geometry/TestMultiLine.java new file mode 100644 index 0000000000000..8861dced5e560 --- /dev/null +++ b/libs/geo/src/test/java/org/elasticsearch/geo/geometry/TestMultiLine.java @@ -0,0 +1,115 @@ +/* + * 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.geo.geometry; + +import org.apache.lucene.geo.GeoTestUtil; +import org.apache.lucene.geo.GeoUtils; +import org.apache.lucene.geo.geometry.GeoShape.Relation; +import org.junit.Ignore; + +import java.util.Arrays; + +import static com.carrotsearch.randomizedtesting.generators.RandomNumbers.randomIntBetween; +import static org.apache.lucene.geo.GeoTestUtil.nextLatitudeIn; +import static org.apache.lucene.geo.GeoTestUtil.nextLongitudeIn; + +/** + * Tests relations and features of MultiLine types + */ +public class TestMultiLine extends BaseGeometryTestCase { + @Override + public MultiLine getShape() { + return getShape(false); + } + + public MultiLine getShape(boolean padded) { + double minFactor = padded == true ? 20D : 0D; + double maxFactor = padded == true ? -20D : 0D; + int numLines = randomIntBetween(random(), 1, 10); + Line[] lines = new Line[numLines]; + + // we can't have self crossing lines. + // since polygons share this contract we create an unclosed polygon + Polygon poly; + double[] lats; + double[] lons; + + for (int i = 0; i < numLines; ++i) { + poly = GeoTestUtil.createRegularPolygon( + nextLatitudeIn(GeoUtils.MIN_LAT_INCL + minFactor, GeoUtils.MAX_LAT_INCL + maxFactor), + nextLongitudeIn(GeoUtils.MIN_LON_INCL + minFactor, GeoUtils.MAX_LON_INCL + maxFactor), + 100D, randomIntBetween(random(), 4, 100)); + lats = poly.getPolyLats(); + lons = poly.getPolyLons(); + lines[i] = new Line(Arrays.copyOfRange(lats, 0, lats.length - 1), + Arrays.copyOfRange(lons, 0, lons.length - 1)); + } + + return new MultiLine(lines); + } + + @Override + public void testBoundingBox() { + MultiLine lines = getShape(true); + double minLat = Double.POSITIVE_INFINITY; + double maxLat = Double.NEGATIVE_INFINITY; + double minLon = Double.POSITIVE_INFINITY; + double maxLon = Double.NEGATIVE_INFINITY; + for (int l = 0; l < lines.length(); ++l) { + Line line = lines.get(l); + for (int j = 0; j < line.numPoints(); ++j) { + minLat = StrictMath.min(minLat, line.getLat(j)); + maxLat = StrictMath.max(maxLat, line.getLat(j)); + minLon = StrictMath.min(minLon, line.getLon(j)); + maxLon = StrictMath.max(maxLon, line.getLon(j)); + } + } + + Rectangle bbox = new Rectangle(minLat, maxLat, minLon, maxLon); + assertEquals(bbox, lines.getBoundingBox()); + } + + @Override + public void testWithin() { + relationTest(getShape(true), Relation.WITHIN); + } + + @Ignore + @Override + public void testContains() { + // CONTAINS not supported with MultiLine + } + + @Override + public void testDisjoint() { + relationTest(getShape(true), Relation.DISJOINT); + } + + @Override + public void testIntersects() { + MultiLine lines = getShape(true); + Line line = lines.get(0); + double minLat = StrictMath.min(line.getLat(0), line.getLat(1)); + double maxLat = StrictMath.max(line.getLat(0), line.getLat(1)); + double minLon = StrictMath.min(line.getLon(0), line.getLon(1)); + double maxLon = StrictMath.max(line.getLon(0), line.getLon(1)); + assertEquals(Relation.INTERSECTS, lines.relate(minLat, maxLat, minLon, maxLon)); + } +} diff --git a/libs/geo/src/test/java/org/elasticsearch/geo/geometry/TestMultiPoint.java b/libs/geo/src/test/java/org/elasticsearch/geo/geometry/TestMultiPoint.java new file mode 100644 index 0000000000000..2d3f2fa112370 --- /dev/null +++ b/libs/geo/src/test/java/org/elasticsearch/geo/geometry/TestMultiPoint.java @@ -0,0 +1,117 @@ +/* + * 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.geo.geometry; + +import org.apache.lucene.geo.GeoTestUtil; +import org.apache.lucene.geo.GeoUtils; +import org.apache.lucene.geo.geometry.GeoShape.Relation; +import org.junit.Ignore; + +import static com.carrotsearch.randomizedtesting.generators.RandomNumbers.randomIntBetween; + +/** + * Tests relations and features of MultiPoint types + */ +public class TestMultiPoint extends BaseGeometryTestCase { + /** + * returns a MultiPoint shape + */ + @Override + public MultiPoint getShape() { + return getShape(false); + } + + /** + * returns a MultiPoint shape within a small area; + * ensures MultiPoints do not cover entire globe. + * Used for testing INTERSECTS and DISJOINT queries + */ + protected MultiPoint getShape(boolean padded) { + int numPoints = randomIntBetween(random(), 2, 100); + double[] lats = new double[numPoints]; + double[] lons = new double[numPoints]; + for (int i = 0; i < numPoints; ++i) { + if (padded == true) { + lats[i] = GeoTestUtil.nextLatitudeIn(GeoUtils.MIN_LAT_INCL + 20D, GeoUtils.MAX_LAT_INCL - 20D); + lons[i] = GeoTestUtil.nextLongitudeIn(GeoUtils.MIN_LON_INCL + 20D, GeoUtils.MAX_LON_INCL - 20D); + } else { + lats[i] = GeoTestUtil.nextLatitude(); + lons[i] = GeoTestUtil.nextLongitude(); + } + } + return new MultiPoint(lats, lons); + } + + @Override + public void testWithin() { + relationTest(getShape(true), Relation.WITHIN); + } + + @Ignore + @Override + public void testContains() { + // MultiPoint does not support CONTAINS + } + + @Override + public void testDisjoint() { + // this is a simple test where we build a bounding box that is disjoint + // from the MultiPoint bounding box + // note: we should add a test where a box is between points + relationTest(getShape(true), Relation.DISJOINT); + } + + @Override + public void testIntersects() { + MultiPoint points = getShape(true); + double minLat = StrictMath.min(points.getLat(0), points.getLat(1)); + double maxLat = StrictMath.max(points.getLat(0), points.getLat(1)); + double minLon = StrictMath.min(points.getLon(0), points.getLon(1)); + double maxLon = StrictMath.max(points.getLon(0), points.getLon(1)); + Relation r = Relation.DISJOINT; + for (Point p : points) { + if ((r = p.relate(minLat, maxLat, minLon, maxLon)) == Relation.INTERSECTS) { + break; + } + } + assertEquals(Relation.INTERSECTS, r); + } + + /** + * tests bounding box of MultiPoint shape type + */ + @Override + public void testBoundingBox() { + MultiPoint points = getShape(true); + double minLat = Double.POSITIVE_INFINITY; + double maxLat = Double.NEGATIVE_INFINITY; + double minLon = Double.POSITIVE_INFINITY; + double maxLon = Double.NEGATIVE_INFINITY; + for (int i = 0; i < points.numPoints(); ++i) { + minLat = StrictMath.min(minLat, points.getLat(i)); + maxLat = StrictMath.max(maxLat, points.getLat(i)); + minLon = StrictMath.min(minLon, points.getLon(i)); + maxLon = StrictMath.max(maxLon, points.getLon(i)); + } + + Rectangle bbox = new Rectangle(minLat, maxLat, minLon, maxLon); + assertEquals(bbox, points.getBoundingBox()); + } +} diff --git a/libs/geo/src/test/java/org/elasticsearch/geo/geometry/TestMultiPolygon.java b/libs/geo/src/test/java/org/elasticsearch/geo/geometry/TestMultiPolygon.java new file mode 100644 index 0000000000000..fcf24bb29bf2e --- /dev/null +++ b/libs/geo/src/test/java/org/elasticsearch/geo/geometry/TestMultiPolygon.java @@ -0,0 +1,136 @@ +/* + * 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.geo.geometry; + +import org.apache.lucene.geo.GeoTestUtil; +import org.apache.lucene.geo.GeoUtils; +import org.apache.lucene.geo.geometry.GeoShape.Relation; + +import static com.carrotsearch.randomizedtesting.generators.RandomNumbers.randomIntBetween; +import static org.apache.lucene.geo.GeoTestUtil.nextBoxNotCrossingDateline; +import static org.apache.lucene.geo.GeoTestUtil.nextLatitudeIn; +import static org.apache.lucene.geo.GeoTestUtil.nextLongitudeIn; +import static org.apache.lucene.geo.GeoTestUtil.nextPolygon; + +/** + * Tests relations and features of MultiPolygon types + */ +public class TestMultiPolygon extends BaseGeometryTestCase { + @Override + public MultiPolygon getShape() { + int numPolys = randomIntBetween(random(), 2, 100); + Polygon[] polygons = new Polygon[numPolys]; + for (int i = 0; i < numPolys; ++i) { + polygons[i] = nextPolygon(); + } + return new MultiPolygon(polygons); + } + + protected MultiPolygon getShape(boolean padded) { + double minFactor = padded == true ? 20D : 0D; + double maxFactor = padded == true ? -20D : 0D; + int numPolys = randomIntBetween(random(), 1, 10); + Polygon[] polygons = new Polygon[numPolys]; + + // we can't have self crossing lines. + // since polygons share this contract we create an unclosed polygon + for (int i = 0; i < numPolys; ++i) { + polygons[i] = GeoTestUtil.createRegularPolygon( + nextLatitudeIn(GeoUtils.MIN_LAT_INCL + minFactor, GeoUtils.MAX_LAT_INCL + maxFactor), + nextLongitudeIn(GeoUtils.MIN_LON_INCL + minFactor, GeoUtils.MAX_LON_INCL + maxFactor), + 10000D, randomIntBetween(random(), 4, 100)); + } + + return new MultiPolygon(polygons); + } + + /** + * tests area of MultiPolygon using simple random boxes + */ + @Override + public void testArea() { + int numPolys = randomIntBetween(random(), 2, 10); + Rectangle box; + Polygon[] polygon = new Polygon[numPolys]; + double width, height; + double area = 0; + for (int i = 0; i < numPolys; ++i) { + box = nextBoxNotCrossingDateline(); + polygon[i] = new Polygon( + new double[]{box.minLat(), box.minLat(), box.maxLat(), box.maxLat(), box.minLat()}, + new double[]{box.minLon(), box.maxLon(), box.maxLon(), box.minLon(), box.minLon()}); + width = box.maxLon() - box.minLon(); + height = box.maxLat() - box.minLat(); + area += width * height; + } + MultiPolygon polygons = new MultiPolygon(polygon); + assertEquals(area, polygons.getArea(), 1E-10D); + } + + @Override + public void testBoundingBox() { + MultiPolygon polygons = getShape(true); + double minLat = Double.POSITIVE_INFINITY; + double maxLat = Double.NEGATIVE_INFINITY; + double minLon = Double.POSITIVE_INFINITY; + double maxLon = Double.NEGATIVE_INFINITY; + Polygon p; + for (int i = 0; i < polygons.length(); ++i) { + p = polygons.get(i); + for (int j = 0; j < p.numPoints(); ++j) { + minLat = Math.min(p.getLat(j), minLat); + maxLat = Math.max(p.getLat(j), maxLat); + minLon = Math.min(p.getLon(j), minLon); + maxLon = Math.max(p.getLon(j), maxLon); + } + } + Rectangle bbox = new Rectangle(minLat, maxLat, minLon, maxLon); + assertEquals(bbox, polygons.getBoundingBox()); + } + + @Override + public void testWithin() { + relationTest(getShape(true), Relation.WITHIN); + } + + @Override + public void testContains() { + MultiPolygon polygons = getShape(true); + Point center = polygons.get(0).getCenter(); + Rectangle box = new Rectangle(center.lat() - 1E-3D, center.lat() + 1E-3D, center.lon() - 1E-3D, center.lon() + 1E-3D); + assertEquals(Relation.CONTAINS, polygons.relate(box.minLat(), box.maxLat(), box.minLon(), box.maxLon())); + } + + @Override + public void testDisjoint() { + relationTest(getShape(true), Relation.DISJOINT); + } + + @Override + public void testIntersects() { + MultiPolygon polygons = getShape(true); + Polygon polygon = polygons.get(0); + double minLat = StrictMath.min(polygon.getLat(0), polygon.getLat(1)); + double maxLat = StrictMath.max(polygon.getLat(0), polygon.getLat(1)); + double minLon = StrictMath.min(polygon.getLon(0), polygon.getLon(1)); + double maxLon = StrictMath.max(polygon.getLon(0), polygon.getLon(1)); + assertEquals(Relation.INTERSECTS, polygon.relate(minLat, maxLat, minLon, maxLon)); + } +} diff --git a/libs/geo/src/test/java/org/elasticsearch/geo/geometry/TestPoint.java b/libs/geo/src/test/java/org/elasticsearch/geo/geometry/TestPoint.java new file mode 100644 index 0000000000000..68233f3a72579 --- /dev/null +++ b/libs/geo/src/test/java/org/elasticsearch/geo/geometry/TestPoint.java @@ -0,0 +1,105 @@ +/* + * 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.geo.geometry; + +import org.apache.lucene.geo.GeoTestUtil; +import org.apache.lucene.geo.GeoUtils; +import org.apache.lucene.geo.geometry.GeoShape.Relation; +import org.junit.Ignore; + +import static org.apache.lucene.geo.GeoTestUtil.nextLatitudeIn; +import static org.apache.lucene.geo.GeoTestUtil.nextLongitudeIn; + +/** + * Tests relations and features of simple Point types + */ +public class TestPoint extends BaseGeometryTestCase { + @Override + public Point getShape() { + return new Point(GeoTestUtil.nextLatitude(), GeoTestUtil.nextLongitude()); + } + + @Ignore + @Override + public void testWithin() { + // for Point types WITHIN == INTERSECTS; so ingore this test + } + + @Ignore + @Override + public void testContains() { + // points do not contain other shapes; ignore this test + } + + @Override + public void testDisjoint() { + Point pt = getShape(); + + double minLat = pt.lat(); + double maxLat = pt.lat(); + double minLon = pt.lon(); + double maxLon = pt.lon(); + + // ensure point latitude is outside of box (with pole as boundary) + if (pt.lat() <= GeoUtils.MIN_LAT_INCL + 1D) { + minLat = GeoUtils.MIN_LAT_INCL + 2D; + maxLat = nextLatitudeIn(minLat, GeoUtils.MAX_LAT_INCL); + } else if (pt.lat() >= GeoUtils.MAX_LAT_INCL - 1D) { + maxLat -= 2D; + minLat = nextLatitudeIn(GeoUtils.MIN_LAT_INCL, maxLat); + } else { + minLat += 1D; + maxLat = nextLatitudeIn(minLat, GeoUtils.MAX_LAT_INCL); + } + + // ensure point longitude is disjoint with box (with dateline as boundary) + if (pt.lon() <= GeoUtils.MIN_LON_INCL + 1D) { + minLon = GeoUtils.MIN_LON_INCL + 2D; + maxLon = nextLongitudeIn(minLon, GeoUtils.MAX_LON_INCL); + } else if (pt.lon() >= GeoUtils.MAX_LON_INCL - 1D) { + maxLon -= 2D; + minLon = nextLongitudeIn(GeoUtils.MIN_LON_INCL, maxLon); + } else { + minLon += 1D; + maxLon = nextLongitudeIn(minLon, GeoUtils.MAX_LON_INCL); + } + + Rectangle box = new Rectangle(minLat, maxLat, minLon, maxLon); + assertEquals(Relation.DISJOINT, pt.relate(box.minLat(), box.maxLat(), box.minLon(), box.maxLon())); + } + + @Override + public void testIntersects() { + Rectangle box = GeoTestUtil.nextBoxNotCrossingDateline(); + Point pt = new Point(nextLatitudeIn(box.minLat(), box.maxLat()), nextLongitudeIn(box.minLon(), box.maxLon())); + assertEquals(Relation.INTERSECTS, pt.relate(box.minLat(), box.maxLat(), box.minLon(), box.maxLon())); + } + + @Override + public void testBoundingBox() { + expectThrows(UnsupportedOperationException.class, () -> getShape().getBoundingBox()); + } + + @Override + public void testCenter() { + Point pt = getShape(); + assertEquals(pt, new Point(pt.lat(), pt.lon())); + } +} diff --git a/libs/geo/src/test/java/org/elasticsearch/geo/geometry/TestPolygon.java b/libs/geo/src/test/java/org/elasticsearch/geo/geometry/TestPolygon.java new file mode 100644 index 0000000000000..8763bc5271c1c --- /dev/null +++ b/libs/geo/src/test/java/org/elasticsearch/geo/geometry/TestPolygon.java @@ -0,0 +1,185 @@ +/* + * 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.geo.geometry; + +import org.apache.lucene.geo.GeoTestUtil; +import org.apache.lucene.geo.GeoUtils; +import org.apache.lucene.geo.geometry.GeoShape.Relation; + +import static com.carrotsearch.randomizedtesting.generators.RandomNumbers.randomIntBetween; +import static org.apache.lucene.geo.GeoTestUtil.nextBoxNotCrossingDateline; +import static org.apache.lucene.geo.GeoTestUtil.nextLatitudeIn; +import static org.apache.lucene.geo.GeoTestUtil.nextLongitudeIn; + +/** + * Tests relations and features of Polygon types + */ +public class TestPolygon extends BaseGeometryTestCase { + + @Override + public Polygon getShape() { + return GeoTestUtil.nextPolygon(); + } + + public Polygon getShape(boolean padded) { + double minFactor = padded == true ? 20D : 0D; + double maxFactor = padded == true ? -20D : 0D; + + // we can't have self crossing lines. + // since polygons share this contract we create an unclosed polygon + Polygon poly = GeoTestUtil.createRegularPolygon( + nextLatitudeIn(GeoUtils.MIN_LAT_INCL + minFactor, GeoUtils.MAX_LAT_INCL + maxFactor), + nextLongitudeIn(GeoUtils.MIN_LON_INCL + minFactor, GeoUtils.MAX_LON_INCL + maxFactor), + 10000D, randomIntBetween(random(), 4, 100)); + return poly; + } + + /** + * tests area of a Polygon type using a simple random box + */ + @Override + public void testArea() { + // test with simple random box + Rectangle box = nextBoxNotCrossingDateline(); + Polygon polygon = new Polygon( + new double[]{box.minLat(), box.minLat(), box.maxLat(), box.maxLat(), box.minLat()}, + new double[]{box.minLon(), box.maxLon(), box.maxLon(), box.minLon(), box.minLon()}); + double width = box.maxLon() - box.minLon(); + double height = box.maxLat() - box.minLat(); + double area = width * height; + + assertEquals(area, polygon.getArea(), 1E-10D); + } + + @Override + public void testBoundingBox() { + Polygon polygon = getShape(); + double minLat = Double.POSITIVE_INFINITY; + double maxLat = Double.NEGATIVE_INFINITY; + double minLon = Double.POSITIVE_INFINITY; + double maxLon = Double.NEGATIVE_INFINITY; + // shell of polygon + Line shell = new Line(polygon.getPolyLats(), polygon.getPolyLons()); + for (int i = 0; i < shell.numPoints(); ++i) { + minLat = StrictMath.min(minLat, shell.getLat(i)); + maxLat = StrictMath.max(maxLat, shell.getLat(i)); + minLon = StrictMath.min(minLon, shell.getLon(i)); + maxLon = StrictMath.max(maxLon, shell.getLon(i)); + } + + Rectangle bbox = new Rectangle(minLat, maxLat, minLon, maxLon); + assertEquals(bbox, polygon.getBoundingBox()); + } + + @Override + public void testWithin() { + relationTest(getShape(true), Relation.WITHIN); + } + + @Override + public void testContains() { + Polygon polygon = getShape(true); + Point center = polygon.getCenter(); + Rectangle box = new Rectangle(center.lat() - 1E-3D, center.lat() + 1E-3D, center.lon() - 1E-3D, center.lon() + 1E-3D); + assertEquals(Relation.CONTAINS, polygon.relate(box.minLat(), box.maxLat(), box.minLon(), box.maxLon())); + } + + @Override + public void testDisjoint() { + relationTest(getShape(true), Relation.DISJOINT); + } + + @Override + public void testIntersects() { + Polygon polygon = getShape(true); + double minLat = StrictMath.min(polygon.getLat(0), polygon.getLat(1)); + double maxLat = StrictMath.max(polygon.getLat(0), polygon.getLat(1)); + double minLon = StrictMath.min(polygon.getLon(0), polygon.getLon(1)); + double maxLon = StrictMath.max(polygon.getLon(0), polygon.getLon(1)); + assertEquals(Relation.INTERSECTS, polygon.relate(minLat, maxLat, minLon, maxLon)); + } + + public void testPacMan() { + // pacman + double[] px = {0, 10, 10, 0, -8, -10, -8, 0, 10, 10, 0}; + double[] py = {0, 5, 9, 10, 9, 0, -9, -10, -9, -5, 0}; + + Polygon polygon = new Polygon(py, px); + + // candidate crosses cell + double minLon = 2; + double maxLon = 11; + double minLat = -1; + double maxLat = 1; + + // test cell crossing poly + assertEquals(Relation.INTERSECTS, polygon.relate(minLat, maxLat, minLon, maxLon)); + } + + /** + * null polyLats not allowed + */ + public void testPolygonNullPolyLats() { + IllegalArgumentException expected = expectThrows(IllegalArgumentException.class, () -> { + new Polygon(null, new double[]{-66, -65, -65, -66, -66}); + }); + assertTrue(expected.getMessage().contains("lats must not be null")); + } + + /** + * null polyLons not allowed + */ + public void testPolygonNullPolyLons() { + IllegalArgumentException expected = expectThrows(IllegalArgumentException.class, () -> { + new Polygon(new double[]{18, 18, 19, 19, 18}, null); + }); + assertTrue(expected.getMessage().contains("lons must not be null")); + } + + /** + * polygon needs at least 3 vertices + */ + public void testPolygonLine() { + IllegalArgumentException expected = expectThrows(IllegalArgumentException.class, () -> { + new Polygon(new double[]{18, 18, 18}, new double[]{-66, -65, -66}); + }); + assertTrue(expected.getMessage().contains("at least 4 polygon points required")); + } + + /** + * polygon needs same number of latitudes as longitudes + */ + public void testPolygonBogus() { + IllegalArgumentException expected = expectThrows(IllegalArgumentException.class, () -> { + new Polygon(new double[]{18, 18, 19, 19}, new double[]{-66, -65, -65, -66, -66}); + }); + assertTrue(expected.getMessage().contains("must be equal length")); + } + + /** + * polygon must be closed + */ + public void testPolygonNotClosed() { + IllegalArgumentException expected = expectThrows(IllegalArgumentException.class, () -> { + new Polygon(new double[]{18, 18, 19, 19, 19}, new double[]{-66, -65, -65, -66, -67}); + }); + assertTrue(expected.getMessage(), expected.getMessage().contains("it must close itself")); + } +} From 9b784e24520d385ceac533673be28a9fc4c4650d Mon Sep 17 00:00:00 2001 From: Igor Motov Date: Fri, 7 Dec 2018 14:35:07 -0500 Subject: [PATCH 02/10] Geo: Clean up geo hierarchy Removes all math and WKB parser and refactors WKT parser --- libs/geo/build.gradle | 69 ++ .../elasticsearch/geo/GeoEncodingUtils.java | 158 ---- .../java/org/elasticsearch/geo/GeoUtils.java | 191 +--- .../elasticsearch/geo/geometry/Circle.java | 90 +- .../elasticsearch/geo/geometry/EdgeTree.java | 823 ------------------ .../elasticsearch/geo/geometry/GeoShape.java | 168 ---- .../geo/geometry/GeoShapeCollection.java | 98 --- .../elasticsearch/geo/geometry/Geometry.java | 32 + .../geo/geometry/GeometryCollection.java | 85 ++ .../geo/geometry/GeometryVisitor.java | 47 + .../org/elasticsearch/geo/geometry/Line.java | 99 ++- .../geo/geometry/LinearRing.java | 55 ++ .../elasticsearch/geo/geometry/MultiLine.java | 119 +-- .../geo/geometry/MultiPoint.java | 187 +--- .../geo/geometry/MultiPolygon.java | 118 +-- .../org/elasticsearch/geo/geometry/Point.java | 91 +- .../elasticsearch/geo/geometry/Polygon.java | 224 ++--- .../elasticsearch/geo/geometry/Predicate.java | 254 ------ .../elasticsearch/geo/geometry/Rectangle.java | 311 +------ .../elasticsearch/geo/geometry/ShapeType.java | 69 +- .../parsers/SimpleGeoJSONPolygonParser.java | 445 ---------- .../elasticsearch/geo/parsers/WKBParser.java | 49 -- .../elasticsearch/geo/parsers/WKTParser.java | 326 ------- .../geo/utils/WellKnownText.java | 557 ++++++++++++ .../geo/TestGeoEncodingUtils.java | 155 ---- .../elasticsearch/geo/TestGeoJSONParsing.java | 266 ------ .../org/elasticsearch/geo/TestGeoUtils.java | 315 ------- .../geo/geometry/BaseGeometryTestCase.java | 170 ++-- .../geo/geometry/CircleTests.java | 51 ++ .../geo/geometry/GeometryCollectionTests.java | 54 ++ .../elasticsearch/geo/geometry/LineTests.java | 51 ++ .../geo/geometry/LinearRingTests.java | 48 + .../geo/geometry/MultiLineTests.java | 51 ++ .../geo/geometry/MultiPointTests.java | 51 ++ .../geo/geometry/MultiPolygonTests.java | 53 ++ .../geo/geometry/PointTests.java | 48 + .../geo/geometry/PolygonTests.java | 52 ++ .../geo/geometry/RectangleTests.java | 51 ++ .../geo/geometry/TestEdgeTree.java | 312 ------- .../elasticsearch/geo/geometry/TestLine.java | 114 --- .../geo/geometry/TestMultiLine.java | 115 --- .../geo/geometry/TestMultiPoint.java | 117 --- .../geo/geometry/TestMultiPolygon.java | 136 --- .../elasticsearch/geo/geometry/TestPoint.java | 105 --- .../geo/geometry/TestPolygon.java | 185 ---- 45 files changed, 1694 insertions(+), 5471 deletions(-) create mode 100644 libs/geo/build.gradle delete mode 100644 libs/geo/src/main/java/org/elasticsearch/geo/GeoEncodingUtils.java delete mode 100644 libs/geo/src/main/java/org/elasticsearch/geo/geometry/EdgeTree.java delete mode 100644 libs/geo/src/main/java/org/elasticsearch/geo/geometry/GeoShape.java delete mode 100644 libs/geo/src/main/java/org/elasticsearch/geo/geometry/GeoShapeCollection.java create mode 100644 libs/geo/src/main/java/org/elasticsearch/geo/geometry/Geometry.java create mode 100644 libs/geo/src/main/java/org/elasticsearch/geo/geometry/GeometryCollection.java create mode 100644 libs/geo/src/main/java/org/elasticsearch/geo/geometry/GeometryVisitor.java create mode 100644 libs/geo/src/main/java/org/elasticsearch/geo/geometry/LinearRing.java delete mode 100644 libs/geo/src/main/java/org/elasticsearch/geo/geometry/Predicate.java delete mode 100644 libs/geo/src/main/java/org/elasticsearch/geo/parsers/SimpleGeoJSONPolygonParser.java delete mode 100644 libs/geo/src/main/java/org/elasticsearch/geo/parsers/WKBParser.java delete mode 100644 libs/geo/src/main/java/org/elasticsearch/geo/parsers/WKTParser.java create mode 100644 libs/geo/src/main/java/org/elasticsearch/geo/utils/WellKnownText.java delete mode 100644 libs/geo/src/test/java/org/elasticsearch/geo/TestGeoEncodingUtils.java delete mode 100644 libs/geo/src/test/java/org/elasticsearch/geo/TestGeoJSONParsing.java delete mode 100644 libs/geo/src/test/java/org/elasticsearch/geo/TestGeoUtils.java create mode 100644 libs/geo/src/test/java/org/elasticsearch/geo/geometry/CircleTests.java create mode 100644 libs/geo/src/test/java/org/elasticsearch/geo/geometry/GeometryCollectionTests.java create mode 100644 libs/geo/src/test/java/org/elasticsearch/geo/geometry/LineTests.java create mode 100644 libs/geo/src/test/java/org/elasticsearch/geo/geometry/LinearRingTests.java create mode 100644 libs/geo/src/test/java/org/elasticsearch/geo/geometry/MultiLineTests.java create mode 100644 libs/geo/src/test/java/org/elasticsearch/geo/geometry/MultiPointTests.java create mode 100644 libs/geo/src/test/java/org/elasticsearch/geo/geometry/MultiPolygonTests.java create mode 100644 libs/geo/src/test/java/org/elasticsearch/geo/geometry/PointTests.java create mode 100644 libs/geo/src/test/java/org/elasticsearch/geo/geometry/PolygonTests.java create mode 100644 libs/geo/src/test/java/org/elasticsearch/geo/geometry/RectangleTests.java delete mode 100644 libs/geo/src/test/java/org/elasticsearch/geo/geometry/TestEdgeTree.java delete mode 100644 libs/geo/src/test/java/org/elasticsearch/geo/geometry/TestLine.java delete mode 100644 libs/geo/src/test/java/org/elasticsearch/geo/geometry/TestMultiLine.java delete mode 100644 libs/geo/src/test/java/org/elasticsearch/geo/geometry/TestMultiPoint.java delete mode 100644 libs/geo/src/test/java/org/elasticsearch/geo/geometry/TestMultiPolygon.java delete mode 100644 libs/geo/src/test/java/org/elasticsearch/geo/geometry/TestPoint.java delete mode 100644 libs/geo/src/test/java/org/elasticsearch/geo/geometry/TestPolygon.java diff --git a/libs/geo/build.gradle b/libs/geo/build.gradle new file mode 100644 index 0000000000000..29df20678012a --- /dev/null +++ b/libs/geo/build.gradle @@ -0,0 +1,69 @@ +/* + * 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. + */ + +apply plugin: 'elasticsearch.build' +apply plugin: 'nebula.maven-base-publish' +apply plugin: 'nebula.maven-scm' + +archivesBaseName = 'elasticsearch-geo' + +publishing { + publications { + nebula { + artifactId = archivesBaseName + } + } +} + +dependencies { + compile "org.elasticsearch:elasticsearch-core:${version}" + + + testCompile "com.carrotsearch.randomizedtesting:randomizedtesting-runner:${versions.randomizedrunner}" + testCompile "junit:junit:${versions.junit}" + testCompile "org.hamcrest:hamcrest-all:${versions.hamcrest}" + + if (isEclipse == false || project.path == ":libs:x-content-tests") { + testCompile("org.elasticsearch.test:framework:${version}") { + exclude group: 'org.elasticsearch', module: 'elasticsearch-geo' + } + } + +} + +forbiddenApisMain { + // geo does not depend on server + // TODO: Need to decide how we want to handle for forbidden signatures with the changes to core + replaceSignatureFiles 'jdk-signatures' +} + +if (isEclipse) { + // in eclipse the project is under a fake root, we need to change around the source sets + sourceSets { + if (project.path == ":libs:geo") { + main.java.srcDirs = ['java'] + main.resources.srcDirs = ['resources'] + } else { + test.java.srcDirs = ['java'] + test.resources.srcDirs = ['resources'] + } + } +} + +jarHell.enabled = false diff --git a/libs/geo/src/main/java/org/elasticsearch/geo/GeoEncodingUtils.java b/libs/geo/src/main/java/org/elasticsearch/geo/GeoEncodingUtils.java deleted file mode 100644 index b4fbbfad256b5..0000000000000 --- a/libs/geo/src/main/java/org/elasticsearch/geo/GeoEncodingUtils.java +++ /dev/null @@ -1,158 +0,0 @@ -/* - * 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.geo; - -import org.apache.lucene.util.NumericUtils; - -import static org.elasticsearch.geo.GeoUtils.MAX_LAT_INCL; -import static org.elasticsearch.geo.GeoUtils.MAX_LON_INCL; -import static org.elasticsearch.geo.GeoUtils.MIN_LON_INCL; -import static org.elasticsearch.geo.GeoUtils.MIN_LAT_INCL; -import static org.elasticsearch.geo.GeoUtils.checkLatitude; -import static org.elasticsearch.geo.GeoUtils.checkLongitude; - -/** - * reusable geopoint encoding methods - */ -public final class GeoEncodingUtils { - /** - * number of bits used for quantizing latitude and longitude values - */ - public static final short BITS = 32; - - private static final double LAT_SCALE = (0x1L << BITS) / 180.0D; - private static final double LAT_DECODE = 1 / LAT_SCALE; - private static final double LON_SCALE = (0x1L << BITS) / 360.0D; - private static final double LON_DECODE = 1 / LON_SCALE; - - // No instance: - private GeoEncodingUtils() { - } - - /** - * Quantizes double (64 bit) latitude into 32 bits (rounding down: in the direction of -90) - * - * @param latitude latitude value: must be within standard +/-90 coordinate bounds. - * @return encoded value as a 32-bit {@code int} - * @throws IllegalArgumentException if latitude is out of bounds - */ - public static int encodeLatitude(double latitude) { - checkLatitude(latitude); - // the maximum possible value cannot be encoded without overflow - if (latitude == 90.0D) { - latitude = Math.nextDown(latitude); - } - return (int) Math.floor(latitude / LAT_DECODE); - } - - /** - * Quantizes double (64 bit) latitude into 32 bits (rounding up: in the direction of +90) - * - * @param latitude latitude value: must be within standard +/-90 coordinate bounds. - * @return encoded value as a 32-bit {@code int} - * @throws IllegalArgumentException if latitude is out of bounds - */ - public static int encodeLatitudeCeil(double latitude) { - GeoUtils.checkLatitude(latitude); - // the maximum possible value cannot be encoded without overflow - if (latitude == 90.0D) { - latitude = Math.nextDown(latitude); - } - return (int) Math.ceil(latitude / LAT_DECODE); - } - - /** - * Quantizes double (64 bit) longitude into 32 bits (rounding down: in the direction of -180) - * - * @param longitude longitude value: must be within standard +/-180 coordinate bounds. - * @return encoded value as a 32-bit {@code int} - * @throws IllegalArgumentException if longitude is out of bounds - */ - public static int encodeLongitude(double longitude) { - checkLongitude(longitude); - // the maximum possible value cannot be encoded without overflow - if (longitude == 180.0D) { - longitude = Math.nextDown(longitude); - } - return (int) Math.floor(longitude / LON_DECODE); - } - - /** - * Quantizes double (64 bit) longitude into 32 bits (rounding up: in the direction of +180) - * - * @param longitude longitude value: must be within standard +/-180 coordinate bounds. - * @return encoded value as a 32-bit {@code int} - * @throws IllegalArgumentException if longitude is out of bounds - */ - public static int encodeLongitudeCeil(double longitude) { - GeoUtils.checkLongitude(longitude); - // the maximum possible value cannot be encoded without overflow - if (longitude == 180.0D) { - longitude = Math.nextDown(longitude); - } - return (int) Math.ceil(longitude / LON_DECODE); - } - - /** - * Turns quantized value from {@link #encodeLatitude} back into a double. - * - * @param encoded encoded value: 32-bit quantized value. - * @return decoded latitude value. - */ - public static double decodeLatitude(int encoded) { - double result = encoded * LAT_DECODE; - assert result >= MIN_LAT_INCL && result < MAX_LAT_INCL; - return result; - } - - /** - * Turns quantized value from byte array back into a double. - * - * @param src byte array containing 4 bytes to decode at {@code offset} - * @param offset offset into {@code src} to decode from. - * @return decoded latitude value. - */ - public static double decodeLatitude(byte[] src, int offset) { - return decodeLatitude(NumericUtils.sortableBytesToInt(src, offset)); - } - - /** - * Turns quantized value from {@link #encodeLongitude} back into a double. - * - * @param encoded encoded value: 32-bit quantized value. - * @return decoded longitude value. - */ - public static double decodeLongitude(int encoded) { - double result = encoded * LON_DECODE; - assert result >= MIN_LON_INCL && result < MAX_LON_INCL; - return result; - } - - /** - * Turns quantized value from byte array back into a double. - * - * @param src byte array containing 4 bytes to decode at {@code offset} - * @param offset offset into {@code src} to decode from. - * @return decoded longitude value. - */ - public static double decodeLongitude(byte[] src, int offset) { - return decodeLongitude(NumericUtils.sortableBytesToInt(src, offset)); - } -} diff --git a/libs/geo/src/main/java/org/elasticsearch/geo/GeoUtils.java b/libs/geo/src/main/java/org/elasticsearch/geo/GeoUtils.java index 5920146782a98..a47aeec33344e 100644 --- a/libs/geo/src/main/java/org/elasticsearch/geo/GeoUtils.java +++ b/libs/geo/src/main/java/org/elasticsearch/geo/GeoUtils.java @@ -19,14 +19,6 @@ package org.elasticsearch.geo; -import static org.apache.lucene.util.SloppyMath.TO_RADIANS; -import static org.apache.lucene.util.SloppyMath.cos; -import static org.apache.lucene.util.SloppyMath.haversinMeters; - -import org.elasticsearch.geo.geometry.GeoShape.Relation; -import org.elasticsearch.geo.geometry.Rectangle; -import org.apache.lucene.util.SloppyMath; - /** * Basic reusable geo-spatial utility methods */ @@ -51,35 +43,6 @@ public final class GeoUtils { */ public static final double MAX_LAT_INCL = 90.0D; - /** - * min longitude value in radians - */ - public static final double MIN_LON_RADIANS = TO_RADIANS * MIN_LON_INCL; - /** - * min latitude value in radians - */ - public static final double MIN_LAT_RADIANS = TO_RADIANS * MIN_LAT_INCL; - /** - * max longitude value in radians - */ - public static final double MAX_LON_RADIANS = TO_RADIANS * MAX_LON_INCL; - /** - * max latitude value in radians - */ - public static final double MAX_LAT_RADIANS = TO_RADIANS * MAX_LAT_INCL; - - // WGS84 earth-ellipsoid parameters - /** - * mean earth axis in meters - */ - // see http://earth-info.nga.mil/GandG/publications/tr8350.2/wgs84fin.pdf - public static final double EARTH_MEAN_RADIUS_METERS = 6_371_008.7714; - /** - * conversion factor for degree to kilometers - */ - public static final double DEG_TO_METERS = 111195.07973436875D; - - // No instance: private GeoUtils() { } @@ -89,7 +52,8 @@ private GeoUtils() { */ public static void checkLatitude(double latitude) { if (Double.isNaN(latitude) || latitude < MIN_LAT_INCL || latitude > MAX_LAT_INCL) { - throw new IllegalArgumentException("invalid latitude " + latitude + "; must be between " + MIN_LAT_INCL + " and " + MAX_LAT_INCL); + throw new IllegalArgumentException( + "invalid latitude " + latitude + "; must be between " + MIN_LAT_INCL + " and " + MAX_LAT_INCL); } } @@ -98,156 +62,9 @@ public static void checkLatitude(double latitude) { */ public static void checkLongitude(double longitude) { if (Double.isNaN(longitude) || longitude < MIN_LON_INCL || longitude > MAX_LON_INCL) { - throw new IllegalArgumentException("invalid longitude " + longitude + "; must be between " + MIN_LON_INCL + " and " + MAX_LON_INCL); - } - } - - // some sloppyish stuff, do we really need this to be done in a sloppy way? - // unless it is performance sensitive, we should try to remove. - private static final double PIO2 = Math.PI / 2D; - - /** - * Returns the trigonometric sine of an angle converted as a cos operation. - *

- * Note that this is not quite right... e.g. sin(0) != 0 - *

- * Special cases: - *

    - *
  • If the argument is {@code NaN} or an infinity, then the result is {@code NaN}. - *
- * - * @param a an angle, in radians. - * @return the sine of the argument. - * @see Math#sin(double) - */ - // TODO: deprecate/remove this? at least its no longer public. - public static double sloppySin(double a) { - return cos(a - PIO2); - } - - /** - * binary search to find the exact sortKey needed to match the specified radius - * any sort key lte this is a query match. - */ - public static double distanceQuerySortKey(double radius) { - // effectively infinite - if (radius >= haversinMeters(Double.MAX_VALUE)) { - return haversinMeters(Double.MAX_VALUE); - } - - // this is a search through non-negative long space only - long lo = 0; - long hi = Double.doubleToRawLongBits(Double.MAX_VALUE); - while (lo <= hi) { - long mid = (lo + hi) >>> 1; - double sortKey = Double.longBitsToDouble(mid); - double midRadius = haversinMeters(sortKey); - if (midRadius == radius) { - return sortKey; - } else if (midRadius > radius) { - hi = mid - 1; - } else { - lo = mid + 1; - } - } - - // not found: this is because a user can supply an arbitrary radius, one that we will never - // calculate exactly via our haversin method. - double ceil = Double.longBitsToDouble(lo); - assert haversinMeters(ceil) > radius; - return ceil; - } - - /** - * Compute the relation between the provided box and distance query. - * This only works for boxes that do not cross the dateline. - */ - public static Relation relate( - double minLat, double maxLat, double minLon, double maxLon, - double lat, double lon, double distanceSortKey, double axisLat) { - - if (minLon > maxLon) { - throw new IllegalArgumentException("Box crosses the dateline"); - } - - if ((lon < minLon || lon > maxLon) && (axisLat + Rectangle.AXISLAT_ERROR < minLat || axisLat - Rectangle.AXISLAT_ERROR > maxLat)) { - // circle not fully inside / crossing axis - if (SloppyMath.haversinSortKey(lat, lon, minLat, minLon) > distanceSortKey && - SloppyMath.haversinSortKey(lat, lon, minLat, maxLon) > distanceSortKey && - SloppyMath.haversinSortKey(lat, lon, maxLat, minLon) > distanceSortKey && - SloppyMath.haversinSortKey(lat, lon, maxLat, maxLon) > distanceSortKey) { - // no points inside - return Relation.DISJOINT; - } - } - - if (within90LonDegrees(lon, minLon, maxLon) && - SloppyMath.haversinSortKey(lat, lon, minLat, minLon) <= distanceSortKey && - SloppyMath.haversinSortKey(lat, lon, minLat, maxLon) <= distanceSortKey && - SloppyMath.haversinSortKey(lat, lon, maxLat, minLon) <= distanceSortKey && - SloppyMath.haversinSortKey(lat, lon, maxLat, maxLon) <= distanceSortKey) { - // we are fully enclosed, collect everything within this subtree - return Relation.WITHIN; - } - - return Relation.CROSSES; - } - - /** - * Return whether all points of {@code [minLon,maxLon]} are within 90 degrees of {@code lon}. - */ - static boolean within90LonDegrees(double lon, double minLon, double maxLon) { - if (maxLon <= lon - 180) { - lon -= 360; - } else if (minLon >= lon + 180) { - lon += 360; - } - return maxLon - lon < 90 && lon - minLon < 90; - } - - /** - * computes longitude in range -180 <= lon_deg <= +180. - */ - public static double normalizeLonDegrees(double lonDegrees) { - if (lonDegrees >= -180 && lonDegrees <= 180) - return lonDegrees;//common case, and avoids slight double precision shifting - double off = (lonDegrees + 180) % 360; - if (off < 0) - return 180 + off; - else if (off == 0 && lonDegrees > 0) - return 180; - else - return -180 + off; - } - - public static double distanceToDegrees(double dist, double radius) { - return StrictMath.toDegrees(distanceToRadians(dist, radius)); - } - - public static double degreesToDistance(double degrees, double radius) { - return radiansToDistance(StrictMath.toRadians(degrees), radius); - } - - public static double distanceToRadians(double dist, double radius) { - return dist / radius; - } - - public static double radiansToDistance(double radians, double radius) { - return radians * radius; - } - - public static double computeOrientation(final double[] xVals, final double[] yVals) { - if (xVals == null || yVals == null || xVals.length < 3 || xVals.length != yVals.length) { - throw new IllegalArgumentException("xVals and yVals must be the same length and include three or more values"); - } - double windingSum = 0d; - final int numPts = yVals.length - 1; - for (int i = 1, j = 0; i < numPts; j = i++) { - // compute signed area for orientation - windingSum += (xVals[j] - xVals[numPts]) * (yVals[i] - yVals[numPts]) - - (yVals[j] - yVals[numPts]) * (xVals[i] - xVals[numPts]); + throw new IllegalArgumentException( + "invalid longitude " + longitude + "; must be between " + MIN_LON_INCL + " and " + MAX_LON_INCL); } - return windingSum; } } diff --git a/libs/geo/src/main/java/org/elasticsearch/geo/geometry/Circle.java b/libs/geo/src/main/java/org/elasticsearch/geo/geometry/Circle.java index 485287253e9ce..87a6eba2341e3 100644 --- a/libs/geo/src/main/java/org/elasticsearch/geo/geometry/Circle.java +++ b/libs/geo/src/main/java/org/elasticsearch/geo/geometry/Circle.java @@ -19,41 +19,32 @@ package org.elasticsearch.geo.geometry; -import java.io.IOException; - -import org.elasticsearch.geo.geometry.Predicate.DistancePredicate; -import org.apache.lucene.store.OutputStreamDataOutput; +import org.elasticsearch.geo.GeoUtils; /** - * Created by nknize on 9/25/17. + * Circle geometry (not part of WKT standard, but used in elasticsearch) */ -public class Circle extends GeoShape { +public class Circle implements Geometry { + public static final Circle EMPTY = new Circle(); private final double lat; private final double lon; private final double radiusMeters; - private DistancePredicate predicate; + + private Circle() { + lat = 0; + lon = 0; + radiusMeters = -1; + } public Circle(final double lat, final double lon, final double radiusMeters) { this.lat = lat; this.lon = lon; this.radiusMeters = radiusMeters; - this.boundingBox = Rectangle.fromPointDistance(lat, lon, radiusMeters); - } - - public double getCenterLat() { - return lat; - } - - public double getCenterLon() { - return lon; - } - - public double getRadiusMeters() { - return radiusMeters; - } - - protected double computeArea() { - return radiusMeters * radiusMeters * StrictMath.PI; + if (radiusMeters < 0 ) { + throw new IllegalArgumentException("Circle radius [" + radiusMeters + "] cannot be negative"); + } + GeoUtils.checkLatitude(lat); + GeoUtils.checkLongitude(lon); } @Override @@ -61,74 +52,55 @@ public ShapeType type() { return ShapeType.CIRCLE; } - @Override - public boolean hasArea() { - return true; - } - - @Override - public Relation relate(double minLat, double maxLat, double minLon, double maxLon) { - return predicate().relate(minLat, maxLat, minLon, maxLon); - } - - @Override - public Relation relate(GeoShape shape) { - throw new UnsupportedOperationException("not yet able to relate other GeoShape types to circles"); + public double getLat() { + return lat; } - public boolean pointInside(final int encodedLat, final int encodedLon) { - return predicate().test(encodedLat, encodedLon); + public double getLon() { + return lon; } - private DistancePredicate predicate() { - if (predicate == null) { - predicate = Predicate.DistancePredicate.create(lat, lon, radiusMeters); - } - return predicate; + public double getRadiusMeters() { + return radiusMeters; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; - if (!super.equals(o)) return false; Circle circle = (Circle) o; if (Double.compare(circle.lat, lat) != 0) return false; if (Double.compare(circle.lon, lon) != 0) return false; - if (Double.compare(circle.radiusMeters, radiusMeters) != 0) return false; - return predicate.equals(circle.predicate); + return (Double.compare(circle.radiusMeters, radiusMeters) == 0); } @Override public int hashCode() { - int result = super.hashCode(); + int result; long temp; temp = Double.doubleToLongBits(lat); - result = 31 * result + (int) (temp ^ (temp >>> 32)); + result = (int) (temp ^ (temp >>> 32)); temp = Double.doubleToLongBits(lon); result = 31 * result + (int) (temp ^ (temp >>> 32)); temp = Double.doubleToLongBits(radiusMeters); result = 31 * result + (int) (temp ^ (temp >>> 32)); - result = 31 * result + predicate.hashCode(); return result; } @Override - public String toWKT() { - throw new UnsupportedOperationException("The WKT spec does not support CIRCLE geometry"); + public T visit(GeometryVisitor visitor) { + return visitor.visit(this); } - @Override - protected StringBuilder contentToWKT() { - throw new UnsupportedOperationException("The WKT spec does not support CIRCLE geometry"); + public boolean isEmpty() { + return radiusMeters < 0; } @Override - protected void appendWKBContent(OutputStreamDataOutput out) throws IOException { - out.writeVLong(Double.doubleToRawLongBits(lat)); - out.writeVLong(Double.doubleToRawLongBits(lon)); - out.writeVLong(Double.doubleToRawLongBits(radiusMeters)); + public String toString() { + return "lat=" + lat + ", lon=" + lon + ", radius=" + radiusMeters; } + } diff --git a/libs/geo/src/main/java/org/elasticsearch/geo/geometry/EdgeTree.java b/libs/geo/src/main/java/org/elasticsearch/geo/geometry/EdgeTree.java deleted file mode 100644 index 277f482b4d7fb..0000000000000 --- a/libs/geo/src/main/java/org/elasticsearch/geo/geometry/EdgeTree.java +++ /dev/null @@ -1,823 +0,0 @@ -/* - * 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.geo.geometry; - -import java.util.Arrays; -import java.util.Comparator; - -import org.elasticsearch.geo.geometry.GeoShape.Relation; -import org.apache.lucene.util.ArrayUtil; - -/** - * 2D polygon implementation represented as a balanced interval tree of edges. - *

- * Construction takes {@code O(n log n)} time for sorting and tree construction. - * {@link #contains contains()} and {@link #relate relate()} are {@code O(n)}, but for most - * practical polygons are much faster than brute force. - *

- * Loosely based on the algorithm described in - * http://www-ma2.upc.es/geoc/Schirra-pointPolygon.pdf. - * - * @lucene.internal - */ -// Both Polygon.contains() and Polygon.crossesSlowly() loop all edges, and first check that the edge is within a range. -// we just organize the edges to do the same computations on the same subset of edges more efficiently. -final class EdgeTree { - /** - * minimum latitude of this polygon's bounding box area - */ - public final double minLat; - /** - * maximum latitude of this polygon's bounding box area - */ - public final double maxLat; - /** - * minimum longitude of this polygon's bounding box area - */ - public final double minLon; - /** - * maximum longitude of this polygon's bounding box area - */ - public final double maxLon; - - // each component/hole is a node in an augmented 2d kd-tree: we alternate splitting between latitude/longitude, - // and pull up max values for both dimensions to each parent node (regardless of split). - - /** - * maximum latitude of this component or any of its children - */ - private double maxY; - /** - * maximum longitude of this component or any of its children - */ - private double maxX; - /** - * which dimension was this node split on - */ - // TODO: its implicit based on level, but boolean keeps code simple - private boolean splitX; - - // child components, or null - private EdgeTree left; - private EdgeTree right; - - /** - * tree of holes, or null - */ - private final EdgeTree holes; - - /** - * root node of edge tree - */ - private final Edge tree; - - /** - * area (in sq meters) of shape represented by the tree - */ - private double areaSqDegrees = Double.NaN; - // NOCOMMIT -// private final boolean isCyclic; - - EdgeTree(Line boundary) { - this(boundary, null); - } - - EdgeTree(Line boundary, EdgeTree holes) { - this.holes = holes; - this.minLat = boundary.minLat(); - this.maxLat = boundary.maxLat(); - this.minLon = boundary.minLon(); - this.maxLon = boundary.maxLon(); - this.maxY = maxLat; - this.maxX = maxLon; -// this.isCyclic = boundary instanceof Polygon; - - // create interval tree of edges - this.tree = createTree(boundary.getLats(), boundary.getLons()); - if (holes != null) { - this.areaSqDegrees -= holes.areaSqDegrees; - } - } - - public double getArea() { - return areaSqDegrees; - } - - /** - * Returns true if the point is contained within this polygon. - *

- * See - * https://www.ecse.rpi.edu/~wrf/Research/Short_Notes/pnpoly.html for more information. - */ - public boolean contains(double latitude, double longitude) { - if (latitude <= maxY && longitude <= maxX) { - if (componentContains(latitude, longitude)) { - return true; - } - if (left != null) { - if (left.contains(latitude, longitude)) { - return true; - } - } - if (right != null && ((splitX == false && latitude >= minLat) || (splitX && longitude >= minLon))) { - if (right.contains(latitude, longitude)) { - return true; - } - } - } - return false; - } - - /** - * Returns true if the point is contained within this polygon component. - */ - private boolean componentContains(double latitude, double longitude) { - // check bounding box - if (latitude < minLat || latitude > maxLat || longitude < minLon || longitude > maxLon) { - return false; - } - - if (tree.contains(latitude, longitude)) { - if (holes != null && holes.contains(latitude, longitude)) { - return false; - } - return true; - } - - return false; - } - - /** - * Returns relation to the provided rectangle - */ - public Relation relate(double minLat, double maxLat, double minLon, double maxLon) { - if (minLat <= maxY && minLon <= maxX) { - Relation relation = componentRelate(minLat, maxLat, minLon, maxLon); - if (relation != Relation.DISJOINT) { - return relation; - } - if (left != null) { - relation = left.relate(minLat, maxLat, minLon, maxLon); - if (relation != Relation.DISJOINT) { - return relation; - } - } - if (right != null && ((splitX == false && maxLat >= this.minLat) || (splitX && maxLon >= this.minLon))) { - relation = right.relate(minLat, maxLat, minLon, maxLon); - if (relation != Relation.DISJOINT) { - return relation; - } - } - } - return Relation.DISJOINT; - } - - /** - * Returns relation to the provided line - */ - public Relation relateLine(double lat1, double lon1, double lat2, double lon2) { - double minLat = lat1, maxLat = lat2; - if (lat2 < lat1) { - minLat = lat2; - maxLat = lat1; - } - if (minLat <= maxY && minLon <= maxX) { - Relation relation = componentRelateLine(lat1, lon1, lat2, lon2); - if (relation != Relation.DISJOINT) { - return relation; - } - if (left != null) { - relation = left.relateLine(lat1, lon1, lat2, lon2); - if (relation != Relation.DISJOINT) { - return relation; - } - } - if (right != null && ((splitX == false && maxLat >= this.minLat) || (splitX && Math.max(lon1, lon2) >= this.minLon))) { - relation = right.relateLine(lat1, lon1, lat2, lon2); - if (relation != Relation.DISJOINT) { - return relation; - } - } - } - return Relation.DISJOINT; - } - -// /** Traverse 2 EdgeTrees to find */ -// public Relation relate(EdgeTree o) { -// // if the bounding boxes are disjoint then the trees are disjoint -// if (o.maxLon < this.minLon || o.minLon > this.maxLon || o.maxLat < this.minLat || o.minLat > this.maxLat) { -// return Relation.DISJOINT; -// } -// -// // check any holes -// if (holes != null) { -// Relation holeRelation = holes.relate(o); -// if (holeRelation == Relation.CROSSES) { -// return Relation.CROSSES; -// } else if (holeRelation == Relation.CONTAINS) { -// return Relation.DISJOINT; -// } -// } -// -// boolean crosses = tree.crosses(o.tree); -// -// if (crosses == false) { -// // iff other is a closed shape; check one point is in the shape (if so its contained): -// if (o.isCyclic) { -// if (o.componentContains(tree.lat1, tree.lon1)) { -// return Relation.WITHIN; -// } else if (this.isCyclic && componentContains(o.tree.lat1, o.tree.lat2)) { -// return Relation.CONTAINS; -// } -// } else if (this.isCyclic && componentContains(o.tree.lat1, o.tree.lon1)) { -// return Relation.CONTAINS; -// } -// return Relation.DISJOINT; -// } -// -// return Relation.CROSSES; -// } - - /** - * Returns relation to the provided rectangle for this component - */ - private Relation componentRelate(double minLat, double maxLat, double minLon, double maxLon) { - // if the bounding boxes are disjoint then the shape does not cross - if (maxLon < this.minLon || minLon > this.maxLon || maxLat < this.minLat || minLat > this.maxLat) { - return Relation.DISJOINT; - } - // if the rectangle fully encloses us, we cross. - if (minLat <= this.minLat && maxLat >= this.maxLat && minLon <= this.minLon && maxLon >= this.maxLon) { - return Relation.INTERSECTS; - } - // check any holes - if (holes != null) { - Relation holeRelation = holes.relate(minLat, maxLat, minLon, maxLon); - if (holeRelation == Relation.INTERSECTS) { - return Relation.INTERSECTS; - } else if (holeRelation == Relation.WITHIN) { - return Relation.DISJOINT; - } - } - // check each corner: if < 4 are present, its cheaper than crossesSlowly - int numCorners = numberOfCorners(minLat, maxLat, minLon, maxLon); - if (numCorners == 4) { - if (tree.intersects(minLat, maxLat, minLon, maxLon)) { - return Relation.INTERSECTS; - } - return Relation.WITHIN; - } else if (numCorners > 0) { - return Relation.INTERSECTS; - } - - // we cross - if (tree.intersects(minLat, maxLat, minLon, maxLon)) { - return Relation.INTERSECTS; - } - - return Relation.DISJOINT; - } - - /** - * Returns relation to the provided line for this component - */ - private Relation componentRelateLine(double lat1, double lon1, double lat2, double lon2) { - if (lineDisjointWithBBox(lat1, lon1, lat2, lon2)) { - return Relation.DISJOINT; - } - // check any holes - if (holes != null) { - Relation holeRelation = holes.relateLine(lat1, lon1, lat2, lon2); - if (holeRelation == Relation.INTERSECTS) { - return Relation.INTERSECTS; - } else if (holeRelation == Relation.WITHIN) { - return Relation.DISJOINT; - } - } - // we intersects - if (tree.intersectsLine(lat1, lon1, lat2, lon2)) { - return Relation.INTERSECTS; - } - return Relation.DISJOINT; - } - - /** - * checks if provided line is disjoint with the component's bounding box - */ - private boolean lineDisjointWithBBox(double lat1, double lon1, double lat2, double lon2) { - double minLat = lat1, maxLat = lat2; - if (lat2 < lat1) { - minLat = lat2; - maxLat = lat1; - } - if (maxLat < this.minLat || minLat > this.maxLat) { - return true; - } - double minLon = lon1, maxLon = lon2; - if (lon2 < lon1) { - minLon = lon2; - maxLon = lon1; - } - // if the bounding boxes are disjoint then the shape does not cross - if (maxLon < this.minLon || minLon > this.maxLon) { - return true; - } - return false; - } - - // returns 0, 4, or something in between - private int numberOfCorners(double minLat, double maxLat, double minLon, double maxLon) { - int containsCount = 0; - if (componentContains(minLat, minLon)) { - containsCount++; - } - if (componentContains(minLat, maxLon)) { - containsCount++; - } - if (containsCount == 1) { - return containsCount; - } - if (componentContains(maxLat, maxLon)) { - containsCount++; - } - if (containsCount == 2) { - return containsCount; - } - if (componentContains(maxLat, minLon)) { - containsCount++; - } - return containsCount; - } - - /** - * Creates tree from sorted components (with range low and high inclusive) - */ - protected static EdgeTree createTree(EdgeTree components[], int low, int high, boolean splitX) { - if (low > high) { - return null; - } - final int mid = (low + high) >>> 1; - if (low < high) { - Comparator comparator; - if (splitX) { - comparator = (left, right) -> { - int ret = Double.compare(left.minLon, right.minLon); - if (ret == 0) { - ret = Double.compare(left.maxX, right.maxX); - } - return ret; - }; - } else { - comparator = (left, right) -> { - int ret = Double.compare(left.minLat, right.minLat); - if (ret == 0) { - ret = Double.compare(left.maxY, right.maxY); - } - return ret; - }; - } - ArrayUtil.select(components, low, high + 1, mid, comparator); - } - // add midpoint - EdgeTree newNode = components[mid]; - newNode.splitX = splitX; - // add children - newNode.left = createTree(components, low, mid - 1, !splitX); - newNode.right = createTree(components, mid + 1, high, !splitX); - // pull up max values to this node - if (newNode.left != null) { - newNode.maxX = Math.max(newNode.maxX, newNode.left.maxX); - newNode.maxY = Math.max(newNode.maxY, newNode.left.maxY); - newNode.areaSqDegrees += newNode.left.areaSqDegrees; - } - if (newNode.right != null) { - newNode.maxX = Math.max(newNode.maxX, newNode.right.maxX); - newNode.maxY = Math.max(newNode.maxY, newNode.right.maxY); - newNode.areaSqDegrees += newNode.right.areaSqDegrees; - } - return newNode; - } - - /** - * Builds a EdgeTree from multipolygon - */ - public static EdgeTree create(Polygon... polygons) { - EdgeTree components[] = new EdgeTree[polygons.length]; - for (int i = 0; i < components.length; i++) { - Polygon gon = polygons[i]; - Polygon gonHoles[] = gon.getHoles(); - EdgeTree holes = null; - if (gonHoles.length > 0) { - holes = create(gonHoles); - } - components[i] = new EdgeTree(gon, holes); - } - return createTree(components, 0, components.length - 1, false); - } - - /** - * Internal tree node: represents polygon edge from lat1,lon1 to lat2,lon2. - * The sort value is {@code low}, which is the minimum latitude of the edge. - * {@code max} stores the maximum latitude of this edge or any children. - */ - static final class Edge { - // lat-lon pair (in original order) of the two vertices - final double lat1, lat2; - final double lon1, lon2; - /** - * min of this edge - */ - final double low; - /** - * max latitude of this edge or any children - */ - double max; - - /** - * left child edge, or null - */ - Edge left; - /** - * right child edge, or null - */ - Edge right; - - Edge(double lat1, double lon1, double lat2, double lon2, double low, double max) { - this.lat1 = lat1; - this.lon1 = lon1; - this.lat2 = lat2; - this.lon2 = lon2; - this.low = low; - this.max = max; - } - - /** - * Returns true if the point crosses this edge subtree an odd number of times - *

- * See - * https://www.ecse.rpi.edu/~wrf/Research/Short_Notes/pnpoly.html for more information. - */ - // ported to java from https://www.ecse.rpi.edu/~wrf/Research/Short_Notes/pnpoly.html - // original code under the BSD license (https://www.ecse.rpi.edu/~wrf/Research/Short_Notes/pnpoly.html#License%20to%20Use) - // - // Copyright (c) 1970-2003, Wm. Randolph Franklin - // - // Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated - // documentation files (the "Software"), to deal in the Software without restriction, including without limitation - // the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and - // to permit persons to whom the Software is furnished to do so, subject to the following conditions: - // - // 1. Redistributions of source code must retain the above copyright - // notice, this list of conditions and the following disclaimers. - // 2. Redistributions in binary form must reproduce the above copyright - // notice in the documentation and/or other materials provided with - // the distribution. - // 3. The name of W. Randolph Franklin may not be used to endorse or - // promote products derived from this Software without specific - // prior written permission. - // - // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED - // TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL - // THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF - // CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS - // IN THE SOFTWARE. - boolean contains(double latitude, double longitude) { - // crossings algorithm is an odd-even algorithm, so we descend the tree xor'ing results along our path - boolean res = false; - if (latitude <= max) { - if (lat1 > latitude != lat2 > latitude) { - if (longitude < (lon1 - lon2) * (latitude - lat2) / (lat1 - lat2) + lon2) { - res = true; - } - } - if (left != null) { - res ^= left.contains(latitude, longitude); - } - if (right != null && latitude >= low) { - res ^= right.contains(latitude, longitude); - } - } - return res; - } - - /** - * Returns true if the box crosses any edge in this edge subtree - */ - boolean intersects(double minLat, double maxLat, double minLon, double maxLon) { - // we just have to cross one edge to answer the question, so we descend the tree and return when we do. - if (minLat <= max) { - // we compute line intersections of every polygon edge with every box line. - // if we find one, return true. - // for each box line (AB): - // for each poly line (CD): - // intersects = orient(C,D,A) * orient(C,D,B) <= 0 && orient(A,B,C) * orient(A,B,D) <= 0 - double cy = lat1; - double dy = lat2; - double cx = lon1; - double dx = lon2; - - // optimization: see if the rectangle is outside of the "bounding box" of the polyline at all - // if so, don't waste our time trying more complicated stuff - boolean outside = (cy < minLat && dy < minLat) || - (cy > maxLat && dy > maxLat) || - (cx < minLon && dx < minLon) || - (cx > maxLon && dx > maxLon); - if (outside == false) { - // does box's top edge intersect polyline? - // ax = minLon, bx = maxLon, ay = maxLat, by = maxLat - if (orient(cx, cy, dx, dy, minLon, maxLat) * orient(cx, cy, dx, dy, maxLon, maxLat) <= 0 && - orient(minLon, maxLat, maxLon, maxLat, cx, cy) * orient(minLon, maxLat, maxLon, maxLat, dx, dy) <= 0) { - return true; - } - - // does box's right edge intersect polyline? - // ax = maxLon, bx = maxLon, ay = maxLat, by = minLat - if (orient(cx, cy, dx, dy, maxLon, maxLat) * orient(cx, cy, dx, dy, maxLon, minLat) <= 0 && - orient(maxLon, maxLat, maxLon, minLat, cx, cy) * orient(maxLon, maxLat, maxLon, minLat, dx, dy) <= 0) { - return true; - } - - // does box's bottom edge intersect polyline? - // ax = maxLon, bx = minLon, ay = minLat, by = minLat - if (orient(cx, cy, dx, dy, maxLon, minLat) * orient(cx, cy, dx, dy, minLon, minLat) <= 0 && - orient(maxLon, minLat, minLon, minLat, cx, cy) * orient(maxLon, minLat, minLon, minLat, dx, dy) <= 0) { - return true; - } - - // does box's left edge intersect polyline? - // ax = minLon, bx = minLon, ay = minLat, by = maxLat - if (orient(cx, cy, dx, dy, minLon, minLat) * orient(cx, cy, dx, dy, minLon, maxLat) <= 0 && - orient(minLon, minLat, minLon, maxLat, cx, cy) * orient(minLon, minLat, minLon, maxLat, dx, dy) <= 0) { - return true; - } - } - - if (left != null) { - if (left.intersects(minLat, maxLat, minLon, maxLon)) { - return true; - } - } - - if (right != null && maxLat >= low) { - if (right.intersects(minLat, maxLat, minLon, maxLon)) { - return true; - } - } - } - return false; - } - -// /** Returns the relation between the two DAGs */ -// boolean crosses(Edge o) { -// return crossesLine(o.lat1, o.lon1, o.lat2, o.lon2) -// || (o.left != null && crosses(o.left)) -// || (o.right != null && crosses(o.right)); -// } - - boolean intersectsLine(double lineLat1, double lineLon1, double lineLat2, double lineLon2) { - // we just have to cross one edge to answer the question, so we descend the tree and return when we do. - double minLat, maxLat; - if (lineLat1 < lineLat2) { - minLat = lineLat1; - maxLat = lineLat2; - } else { - minLat = lineLat2; - maxLat = lineLat1; - } - - if (minLat <= max) { - // we compute line intersections of every polygon edge with every box line. - // if we find one, return true. - // for each box line (AB): - // for each poly line (CD): - // intersects = orient(C,D,A) * orient(C,D,B) <= 0 && orient(A,B,C) * orient(A,B,D) <= 0 - double cy = lat1; - double dy = lat2; - double cx = lon1; - double dx = lon2; - double minLon, maxLon; - if (lineLon1 < lineLon2) { - minLon = lineLon1; - maxLon = lineLon2; - } else { - minLon = lineLon2; - maxLon = lineLon1; - } - - // optimization: see if the rectangle is outside of the "bounding box" of the polyline at all - // if so, don't waste our time trying more complicated stuff - boolean outside = (cy < minLat && dy < minLat) || - (cy > maxLat && dy > maxLat) || - (cx < minLon && dx < minLon) || - (cx > maxLon && dx > maxLon); - if (outside == false) { - // does provided edge intersect polyline? - // ax = minLon, bx = maxLon, ay = maxLat, by = maxLat - if (orient(cx, cy, dx, dy, minLon, maxLat) * orient(cx, cy, dx, dy, maxLon, maxLat) <= 0 && - orient(minLon, maxLat, maxLon, maxLat, cx, cy) * orient(minLon, maxLat, maxLon, maxLat, dx, dy) <= 0) { - return true; - } - } - - if (left != null) { - if (left.intersectsLine(minLat, maxLat, minLon, maxLon)) { - return true; - } - } - - if (right != null && maxLat >= low) { - if (right.intersectsLine(minLat, maxLat, minLon, maxLon)) { - return true; - } - } - } - return false; - } - - @Override - public boolean equals(Object other) { - if (other == null || getClass() != other.getClass()) return false; - Edge o = getClass().cast(other); - if (left == null && o.left != null) return false; - if (left != null && left.equals(o.left) == false) return false; - if (right == null && o.right != null) return false; - if (right != null && right.equals(o.right) == false) return false; - return lat1 != o.lat1 - && lat2 != o.lat2 - && lon1 != o.lon1 - && lon2 != o.lon2 - && low != o.low - && max != o.max; - } - - @Override - public String toString() { - StringBuilder sb = new StringBuilder(); - toString(sb, "(ROOT)", 0); - return sb.append("\n\n").toString(); - } - - public void toString(StringBuilder sb, String branch, int level) { - sb.append("(" + lat1 + ", " + lon1 + ") "); - sb.append("(" + lat2 + ", " + lon2 + ") "); - sb.append(branch + " - " + level++ + "\r\t"); - if (left != null) left.toString(sb, "[L]", level); - if (right != null) right.toString(sb, "[R]", level); - } - } - - /** - * Creates an edge interval tree from a set of polygon vertices. - * - * @return root node of the tree. - */ - private Edge createTree(double polyLats[], double polyLons[]) { - final Edge edges[] = new Edge[polyLats.length - 1]; - double area = 0; - for (int i = 1; i < polyLats.length; i++) { - double lat1 = polyLats[i - 1]; - double lon1 = polyLons[i - 1]; - double lat2 = polyLats[i]; - double lon2 = polyLons[i]; - edges[i - 1] = new Edge(lat1, lon1, lat2, lon2, Math.min(lat1, lat2), Math.max(lat1, lat2)); - area += lon1 * lat2 - lon2 * lat1; - } - this.areaSqDegrees = StrictMath.abs(area) * 0.5d; - // sort the edges then build a balanced tree from them - Arrays.sort(edges, (left, right) -> { - int ret = Double.compare(left.low, right.low); - if (ret == 0) { - ret = Double.compare(left.max, right.max); - } - return ret; - }); - return createTree(edges, 0, edges.length - 1); - } - - /** - * Creates tree from sorted edges (with range low and high inclusive) - */ - private static Edge createTree(Edge edges[], int low, int high) { - if (low > high) { - return null; - } - // add midpoint - int mid = (low + high) >>> 1; - Edge newNode = edges[mid]; - // add children - newNode.left = createTree(edges, low, mid - 1); - newNode.right = createTree(edges, mid + 1, high); - // pull up max values to this node - if (newNode.left != null) { - newNode.max = Math.max(newNode.max, newNode.left.max); - } - if (newNode.right != null) { - newNode.max = Math.max(newNode.max, newNode.right.max); - } - return newNode; - } - - /** - * Returns a positive value if points a, b, and c are arranged in counter-clockwise order, - * negative value if clockwise, zero if collinear. - */ - // see the "Orient2D" method described here: - // http://www.cs.berkeley.edu/~jrs/meshpapers/robnotes.pdf - // https://www.cs.cmu.edu/~quake/robust.html - // Note that this one does not yet have the floating point tricks to be exact! - private static int orient(double ax, double ay, double bx, double by, double cx, double cy) { - double v1 = (bx - ax) * (cy - ay); - double v2 = (cx - ax) * (by - ay); - if (v1 > v2) { - return 1; - } else if (v1 < v2) { - return -1; - } else { - return 0; - } - } - - @Override - public boolean equals(Object other) { - if (other == null || getClass() != other.getClass()) return false; - EdgeTree o = getClass().cast(other); - if (left == null && o.left != null) return false; - if (left != null && left.equals(o.left) == false) return false; - if (right == null && o.right != null) return false; - if (right != null && right.equals(o.right) == false) return false; - if (holes == null && o.holes != null) return false; - if (holes != null && holes.equals(o.holes) == false) return false; - if (tree == null && o.tree != null) return false; - if (tree != null && tree.equals(o.tree) == false) return false; - return maxLat == o.maxLat - && maxLon == o.maxLon - && maxX == o.maxX - && maxY == o.maxY - && minLat == o.minLat - && minLon == o.minLon - && splitX == o.splitX; - } - - @Override - public String toString() { - StringBuilder sb = new StringBuilder(); - toString(sb); - return sb.toString(); - } - - public void toString(StringBuilder sb) { - sb.append(tree.toString()); - if (left != null) left.toString(sb); - if (right != null) right.toString(sb); - } - - public static void main(String[] args) { - Polygon p = new Polygon( - new double[]{10, 9, 7, 4, 2, 6, 10}, - new double[]{8, 10, 15, 11, 8, 3, 8}, - new Polygon( - new double[]{5, 7, 5, 4, 5}, - new double[]{9, 10, 11, 9, 9} - ) - ); - MultiPolygon mp = new MultiPolygon(new Polygon[]{p}); - - Relation r = mp.relate(-5, 5, -5, 5); - System.out.println(mp.tree); - System.out.println(mp.tree.holes); - - Polygon p2 = new Polygon( - new double[]{5, 9, 6, 9, 10, 7, 5}, - new double[]{16, 20, 21, 22, 20, 15, 16} - ); - r = p2.relate(-5, 5, -5, 5); - - //System.out.println(mp.tree.relate(p2.tree)); - - // -// Polygon p2 = new Polygon( -// new double[] {7, 7, 6, 6, 7}, -// new double[] {7, 8, 8, 7, 7} -// ); -// Relation r2 = p.relate(p2); -// -// assert r == Relation.CELL_CROSSES_QUERY; - } -} diff --git a/libs/geo/src/main/java/org/elasticsearch/geo/geometry/GeoShape.java b/libs/geo/src/main/java/org/elasticsearch/geo/geometry/GeoShape.java deleted file mode 100644 index 84ffc16a88a0e..0000000000000 --- a/libs/geo/src/main/java/org/elasticsearch/geo/geometry/GeoShape.java +++ /dev/null @@ -1,168 +0,0 @@ -/* - * 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.geo.geometry; - -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.util.Objects; - -import org.elasticsearch.geo.parsers.WKBParser; -import org.elasticsearch.geo.parsers.WKBParser.ByteOrder; -import org.elasticsearch.geo.parsers.WKTParser; -import org.apache.lucene.index.PointValues; -import org.apache.lucene.store.OutputStreamDataOutput; - -/** - */ -public abstract class GeoShape { - protected Rectangle boundingBox; - protected double area = Double.NaN; - - public Rectangle getBoundingBox() { - return boundingBox; - } - - public double minLat() { - return boundingBox.minLat; - } - - public double maxLat() { - return boundingBox.maxLat; - } - - public double minLon() { - return boundingBox.minLon; - } - - public double maxLon() { - return boundingBox.maxLon; - } - - - public Point getCenter() { - return boundingBox.getCenter(); - } - - public double getArea() { - if (hasArea()) { - if (Double.isNaN(area)) { - area = computeArea(); - } - return area; - } - throw new UnsupportedOperationException(type() + " does not have an area"); - } - - protected double computeArea() { - throw new UnsupportedOperationException(type() + " does not have an area"); - } - - public abstract boolean hasArea(); - - public abstract ShapeType type(); - - public abstract Relation relate(double minLat, double maxLat, double minLon, double maxLon); - - public abstract Relation relate(GeoShape shape); - - interface ConnectedComponent { - EdgeTree createEdgeTree(); - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (!(o instanceof GeoShape)) return false; - GeoShape geoShape = (GeoShape) o; - return Double.compare(geoShape.area, area) == 0 && - Objects.equals(boundingBox, geoShape.boundingBox); - } - - @Override - public int hashCode() { - return Objects.hash(boundingBox, area); - } - - public String toWKT() { - StringBuilder sb = new StringBuilder(); - sb.append(type().wktName()); - sb.append(WKTParser.SPACE); - sb.append(contentToWKT()); - return sb.toString(); - } - - public ByteArrayOutputStream toWKB() { - return toWKB(null); - } - - public ByteArrayOutputStream toWKB(ByteArrayOutputStream reuse) { - if (reuse == null) { - reuse = new ByteArrayOutputStream(); - } - try (OutputStreamDataOutput out = new OutputStreamDataOutput(reuse)) { - appendWKB(out); - } catch (IOException e) { - throw new RuntimeException(e); // not possible - } - return reuse; - } - - protected abstract StringBuilder contentToWKT(); - - private void appendWKB(OutputStreamDataOutput out) throws IOException { - out.writeVInt(WKBParser.ByteOrder.XDR.ordinal()); // byteOrder - out.writeVInt(type().wkbOrdinal()); // shapeType ordinal - appendWKBContent(out); - } - - protected abstract void appendWKBContent(OutputStreamDataOutput out) throws IOException; - - public enum Relation { - DISJOINT(PointValues.Relation.CELL_OUTSIDE_QUERY), - INTERSECTS(PointValues.Relation.CELL_CROSSES_QUERY), - CONTAINS(PointValues.Relation.CELL_CROSSES_QUERY), - WITHIN(PointValues.Relation.CELL_INSIDE_QUERY), - CROSSES(PointValues.Relation.CELL_CROSSES_QUERY); - - // used to translate between PointValues.Relation and full geo relations - private final PointValues.Relation pointsRelation; - - Relation(PointValues.Relation pointsRelation) { - this.pointsRelation = pointsRelation; - } - - public PointValues.Relation toPointsRelation() { - return pointsRelation; - } - - public boolean intersects() { - return this != DISJOINT; - } - - public Relation transpose() { - if (this == CONTAINS) { - return WITHIN; - } else if (this == WITHIN) { - return CONTAINS; - } - return this; - } - } -} diff --git a/libs/geo/src/main/java/org/elasticsearch/geo/geometry/GeoShapeCollection.java b/libs/geo/src/main/java/org/elasticsearch/geo/geometry/GeoShapeCollection.java deleted file mode 100644 index 605905bf2a985..0000000000000 --- a/libs/geo/src/main/java/org/elasticsearch/geo/geometry/GeoShapeCollection.java +++ /dev/null @@ -1,98 +0,0 @@ -/* - * 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.geo.geometry; - -import java.io.IOException; -import java.io.UnsupportedEncodingException; - -import org.apache.lucene.store.OutputStreamDataOutput; - -public class GeoShapeCollection extends GeoShape { - protected GeoShape[] shapes; - private final boolean hasArea; - - public GeoShapeCollection(GeoShape... shapes) { - if (shapes.length < 1) { - throw new IllegalArgumentException("must have at least one shape to create a " + type()); - } - // nocommit - CHECK THIS - this.shapes = shapes.clone(); - - double minLat = Double.POSITIVE_INFINITY; - double maxLat = Double.NEGATIVE_INFINITY; - double minLon = Double.POSITIVE_INFINITY; - double maxLon = Double.NEGATIVE_INFINITY; - - Rectangle bbox; - boolean hasArea = false; - for (GeoShape shape : shapes) { - if (hasArea == false && shape.hasArea()) { - hasArea = true; - } - bbox = shape.getBoundingBox(); - minLat = StrictMath.min(minLat, bbox.minLat()); - maxLat = StrictMath.max(maxLat, bbox.maxLat()); - minLon = StrictMath.min(minLon, bbox.minLon()); - maxLon = StrictMath.max(maxLon, bbox.maxLon()); - } - this.hasArea = hasArea; - this.boundingBox = new Rectangle(minLat, maxLat, minLon, maxLon); - } - - @Override - public boolean hasArea() { - return hasArea; - } - - @Override - protected double computeArea() { - double area = 0; - for (GeoShape shape : shapes) { - if (shape.hasArea()) { - area += shape.getArea(); - } - } - return area; - } - - @Override - public ShapeType type() { - return ShapeType.GEOMETRYCOLLECTION; - } - - @Override - public Relation relate(double minLat, double maxLat, double minLon, double maxLon) { - throw new UnsupportedOperationException("not yet implemented"); - } - - @Override - public Relation relate(GeoShape shape) { - throw new UnsupportedOperationException("not yet implemented"); - } - - @Override - protected StringBuilder contentToWKT() { - throw new UnsupportedOperationException("not yet implemented"); - } - - @Override - protected void appendWKBContent(OutputStreamDataOutput out) throws IOException { - throw new UnsupportedEncodingException("not yet implemented"); - } -} diff --git a/libs/geo/src/main/java/org/elasticsearch/geo/geometry/Geometry.java b/libs/geo/src/main/java/org/elasticsearch/geo/geometry/Geometry.java new file mode 100644 index 0000000000000..4557780effcad --- /dev/null +++ b/libs/geo/src/main/java/org/elasticsearch/geo/geometry/Geometry.java @@ -0,0 +1,32 @@ +/* + * 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.geo.geometry; + +/** + * Base class for all Geometry objects supported by elasticsearch + */ +public interface Geometry { + + ShapeType type(); + + T visit(GeometryVisitor visitor); + + boolean isEmpty(); +} diff --git a/libs/geo/src/main/java/org/elasticsearch/geo/geometry/GeometryCollection.java b/libs/geo/src/main/java/org/elasticsearch/geo/geometry/GeometryCollection.java new file mode 100644 index 0000000000000..a6bad62efad30 --- /dev/null +++ b/libs/geo/src/main/java/org/elasticsearch/geo/geometry/GeometryCollection.java @@ -0,0 +1,85 @@ +/* + * 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.geo.geometry; + +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.Objects; + +/** + * Collection of arbitrary geometry classes + */ +public class GeometryCollection implements Geometry, Iterable { + public static final GeometryCollection EMPTY = new GeometryCollection<>(); + + private final List shapes; + + public GeometryCollection() { + shapes = Collections.emptyList(); + } + + public GeometryCollection(List shapes) { + if (shapes == null || shapes.isEmpty()) { + throw new IllegalArgumentException("the list of shapes cannot be null or empty"); + } + this.shapes = shapes; + } + + @Override + public ShapeType type() { + return ShapeType.GEOMETRYCOLLECTION; + } + + @Override + public T visit(GeometryVisitor visitor) { + return visitor.visit(this); + } + + @Override + public boolean isEmpty() { + return shapes.isEmpty(); + } + + public int size() { + return shapes.size(); + } + + public G get(int i) { + return shapes.get(i); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + GeometryCollection that = (GeometryCollection) o; + return Objects.equals(shapes, that.shapes); + } + + @Override + public int hashCode() { + return Objects.hash(shapes); + } + + @Override + public Iterator iterator() { + return shapes.iterator(); + } +} diff --git a/libs/geo/src/main/java/org/elasticsearch/geo/geometry/GeometryVisitor.java b/libs/geo/src/main/java/org/elasticsearch/geo/geometry/GeometryVisitor.java new file mode 100644 index 0000000000000..b71763212a8e2 --- /dev/null +++ b/libs/geo/src/main/java/org/elasticsearch/geo/geometry/GeometryVisitor.java @@ -0,0 +1,47 @@ +/* + * 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.geo.geometry; + +/** + * Support class for creating of geometry Visitors + */ +public interface GeometryVisitor { + + T visit(Circle circle); + + T visit(GeometryCollection collection); + + T visit(Line line); + + T visit(LinearRing ring); + + T visit(MultiLine multiLine); + + T visit(MultiPoint multiPoint); + + T visit(MultiPolygon multiPolygon); + + T visit(Point point); + + T visit(Polygon polygon); + + T visit(Rectangle rectangle); + +} diff --git a/libs/geo/src/main/java/org/elasticsearch/geo/geometry/Line.java b/libs/geo/src/main/java/org/elasticsearch/geo/geometry/Line.java index 5c461bd16d8c0..aa1cf06867fd3 100644 --- a/libs/geo/src/main/java/org/elasticsearch/geo/geometry/Line.java +++ b/libs/geo/src/main/java/org/elasticsearch/geo/geometry/Line.java @@ -19,28 +19,54 @@ package org.elasticsearch.geo.geometry; -import java.io.IOException; +import org.elasticsearch.geo.GeoUtils; -import org.elasticsearch.geo.geometry.GeoShape.ConnectedComponent; -import org.elasticsearch.geo.parsers.WKBParser; -import org.apache.lucene.store.OutputStreamDataOutput; +import java.util.Arrays; /** * Represents a Line on the earth's surface in lat/lon decimal degrees. */ -public class Line extends MultiPoint implements ConnectedComponent { - EdgeTree tree; +public class Line implements Geometry { + public static final Line EMPTY = new Line(); + private final double[] lats; + private final double[] lons; - public Line(double[] lats, double[] lons) { - super(lats, lons); + protected Line() { + lats = new double[0]; + lons = new double[0]; } - @Override - public Relation relate(double minLat, double maxLat, double minLon, double maxLon) { - if (tree == null) { - tree = createEdgeTree(); + public Line(double[] lats, double[] lons) { + this.lats = lats; + this.lons = lons; + if (lats == null) { + throw new IllegalArgumentException("lats must not be null"); + } + if (lons == null) { + throw new IllegalArgumentException("lons must not be null"); + } + if (lats.length != lons.length) { + throw new IllegalArgumentException("lats and lons must be equal length"); } - return tree.relate(minLat, maxLat, minLon, maxLon); + if (lats.length < 2) { + throw new IllegalArgumentException("at least two points in the line is required"); + } + for (int i = 0; i < lats.length; i++) { + GeoUtils.checkLatitude(lats[i]); + GeoUtils.checkLongitude(lons[i]); + } + } + + public int length() { + return lats.length; + } + + public double getLat(int i) { + return lats[i]; + } + + public double getLon(int i) { + return lons[i]; } @Override @@ -48,50 +74,35 @@ public ShapeType type() { return ShapeType.LINESTRING; } - public Relation relate(GeoShape other) { - // not yet implemented - throw new UnsupportedOperationException("not yet able to relate other GeoShape types to linestrings"); + @Override + public T visit(GeometryVisitor visitor) { + return visitor.visit(this); } @Override - public EdgeTree createEdgeTree() { - return new EdgeTree(this); - - // NOCOMMIT -// EdgeTree components[] = new EdgeTree[lines.length]; -// for (int i = 0; i < components.length; i++) { -// Line gon = lines[i]; -// components[i] = new EdgeTree(gon); -// } -// return EdgeTree.createTree(components, 0, components.length - 1, false); + public boolean isEmpty() { + return lats.length == 0; } @Override - public boolean equals(Object other) { - if (super.equals(other) == false) return false; - Line o = getClass().cast(other); - if ((tree == null) != (o.tree == null)) return false; - return tree != null ? tree.equals(o.tree) : true; + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Line line = (Line) o; + return Arrays.equals(lats, line.lats) && + Arrays.equals(lons, line.lons); } @Override public int hashCode() { - int result = super.hashCode(); - result = 31 * result + (tree != null ? tree.hashCode() : 0); + int result = Arrays.hashCode(lats); + result = 31 * result + Arrays.hashCode(lons); return result; } @Override - protected void appendWKBContent(OutputStreamDataOutput out) throws IOException { - lineToWKB(lats, lons, out, false); - } - - public static void lineToWKB(final double[] lats, final double[] lons, OutputStreamDataOutput out, boolean writeHeader) throws IOException { - if (writeHeader == true) { - out.writeVInt(WKBParser.ByteOrder.XDR.ordinal()); - out.writeVInt(ShapeType.LINESTRING.wkbOrdinal()); - } - out.writeVInt(lats.length); // number of points - pointsToWKB(lats, lons, out, false); + public String toString() { + return "lats=" + Arrays.toString(lats) + + ", lons=" + Arrays.toString(lons); } } diff --git a/libs/geo/src/main/java/org/elasticsearch/geo/geometry/LinearRing.java b/libs/geo/src/main/java/org/elasticsearch/geo/geometry/LinearRing.java new file mode 100644 index 0000000000000..6a36ca280bef6 --- /dev/null +++ b/libs/geo/src/main/java/org/elasticsearch/geo/geometry/LinearRing.java @@ -0,0 +1,55 @@ +/* + * 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.geo.geometry; + +/** + * Represents a closed line on the earth's surface in lat/lon decimal degrees. + *

+ * Cannot be serialized by WKT directly but used as a part of polygon + */ +public class LinearRing extends Line { + public static final LinearRing EMPTY = new LinearRing(); + + + private LinearRing() { + + } + + public LinearRing(double[] lats, double[] lons) { + super(lats, lons); + if (lats.length < 2) { + throw new IllegalArgumentException("linear ring cannot contain less than 2 points, found " + lats.length); + } + if (lats[0] != lats[lats.length - 1] || lons[0] != lons[lons.length - 1]) { + throw new IllegalArgumentException("first and last points of the linear ring must be the same (it must close itself): lats[0]=" + + lats[0] + " lats[" + (lats.length - 1) + "]=" + lats[lats.length - 1]); + } + } + + @Override + public ShapeType type() { + return ShapeType.LINEARRING; + } + + @Override + public T visit(GeometryVisitor visitor) { + return visitor.visit(this); + } +} diff --git a/libs/geo/src/main/java/org/elasticsearch/geo/geometry/MultiLine.java b/libs/geo/src/main/java/org/elasticsearch/geo/geometry/MultiLine.java index 997f1cda1dae7..f43f6d2fd2488 100644 --- a/libs/geo/src/main/java/org/elasticsearch/geo/geometry/MultiLine.java +++ b/libs/geo/src/main/java/org/elasticsearch/geo/geometry/MultiLine.java @@ -19,51 +19,20 @@ package org.elasticsearch.geo.geometry; -import java.io.IOException; -import java.util.Arrays; - -import org.elasticsearch.geo.geometry.GeoShape.ConnectedComponent; -import org.elasticsearch.geo.parsers.WKBParser; -import org.elasticsearch.geo.parsers.WKTParser; -import org.apache.lucene.index.PointValues.Relation; -import org.apache.lucene.store.OutputStreamDataOutput; +import java.util.List; /** * Represents a MultiLine geometry object on the earth's surface. */ -public class MultiLine extends GeoShape implements ConnectedComponent { - EdgeTree tree; - Line[] lines; - - public MultiLine(Line... lines) { - this.lines = lines.clone(); - // compute bounding box - double minLat = Double.POSITIVE_INFINITY; - double maxLat = Double.NEGATIVE_INFINITY; - double minLon = Double.POSITIVE_INFINITY; - double maxLon = Double.NEGATIVE_INFINITY; - for (Line l : lines) { - minLat = Math.min(l.minLat(), minLat); - maxLat = Math.max(l.maxLat(), maxLat); - minLon = Math.min(l.minLon(), minLon); - maxLon = Math.max(l.maxLon(), maxLon); - } - boundingBox = new Rectangle(minLat, maxLat, minLon, maxLon); - } +public class MultiLine extends GeometryCollection { + public static final MultiLine EMPTY = new MultiLine(); - public int length() { - return lines.length; - } + private MultiLine() { - public Line get(int index) { - checkVertexIndex(index); - return lines[index]; } - protected void checkVertexIndex(final int i) { - if (i >= lines.length) { - throw new IllegalArgumentException("Index " + i + " is outside the bounds of the " + lines.length + " shapes"); - } + public MultiLine(List lines) { + super(lines); } @Override @@ -72,79 +41,7 @@ public ShapeType type() { } @Override - public boolean hasArea() { - return false; - } - - @Override - public EdgeTree createEdgeTree() { - EdgeTree components[] = new EdgeTree[lines.length]; - for (int i = 0; i < components.length; i++) { - Line line = lines[i]; - components[i] = new EdgeTree(line); - } - return EdgeTree.createTree(components, 0, components.length - 1, false); - } - - @Override - public Relation relate(double minLat, double maxLat, double minLon, double maxLon) { - if (tree == null) { - tree = createEdgeTree(); - } - return tree.relate(minLat, maxLat, minLon, maxLon).transpose(); - } - - public Relation relate(GeoShape shape) { - return null; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - if (!super.equals(o)) return false; - - MultiLine multiLine = (MultiLine) o; - - if (!tree.equals(multiLine.tree)) return false; - // Probably incorrect - comparing Object[] arrays with Arrays.equals - return Arrays.equals(lines, multiLine.lines); - } - - @Override - public int hashCode() { - int result = super.hashCode(); - result = 31 * result + tree.hashCode(); - result = 31 * result + Arrays.hashCode(lines); - return result; - } - - @Override - protected StringBuilder contentToWKT() { - final StringBuilder sb = new StringBuilder(); - if (lines.length == 0) { - sb.append(WKTParser.EMPTY); - } else { - sb.append(WKTParser.LPAREN); - if (lines.length > 0) { - sb.append(MultiPoint.coordinatesToWKT(lines[0].lats, lines[0].lons)); - } - for (int i = 1; i < lines.length; ++i) { - sb.append(WKTParser.COMMA); - sb.append(MultiPoint.coordinatesToWKT(lines[i].lats, lines[i].lons)); - } - sb.append(WKTParser.RPAREN); - } - return sb; - } - - @Override - protected void appendWKBContent(OutputStreamDataOutput out) throws IOException { - int numLines = length(); - out.writeVInt(numLines); - for (int i = 0; i < numLines; ++i) { - Line line = lines[i]; - Line.lineToWKB(line.lats, line.lons, out, true); - } + public T visit(GeometryVisitor visitor) { + return visitor.visit(this); } } diff --git a/libs/geo/src/main/java/org/elasticsearch/geo/geometry/MultiPoint.java b/libs/geo/src/main/java/org/elasticsearch/geo/geometry/MultiPoint.java index ae745d6f90c49..383fef81219aa 100644 --- a/libs/geo/src/main/java/org/elasticsearch/geo/geometry/MultiPoint.java +++ b/libs/geo/src/main/java/org/elasticsearch/geo/geometry/MultiPoint.java @@ -19,53 +19,19 @@ package org.elasticsearch.geo.geometry; -import java.io.IOException; -import java.util.Arrays; -import java.util.Iterator; - -import org.elasticsearch.geo.GeoUtils; -import org.elasticsearch.geo.parsers.WKBParser; -import org.elasticsearch.geo.parsers.WKTParser; -import org.apache.lucene.store.OutputStreamDataOutput; +import java.util.List; /** * Represents a MultiPoint object on the earth's surface in decimal degrees. */ -public class MultiPoint extends GeoShape implements Iterable { - protected final double[] lats; - protected final double[] lons; - - public MultiPoint(double[] lats, double[] lons) { - checkLatArgs(lats); - checkLonArgs(lons); - if (lats.length != lons.length) { - throw new IllegalArgumentException("lats and lons must be equal length"); - } - for (int i = 0; i < lats.length; i++) { - GeoUtils.checkLatitude(lats[i]); - GeoUtils.checkLongitude(lons[i]); - } - this.lats = lats.clone(); - this.lons = lons.clone(); +public class MultiPoint extends GeometryCollection { + public static final MultiPoint EMPTY = new MultiPoint(); - // compute bounding box - double minLat = Math.min(lats[0], lats[lats.length - 1]); - double maxLat = Math.max(lats[0], lats[lats.length - 1]); - double minLon = Math.min(lons[0], lons[lats.length - 1]); - double maxLon = Math.max(lons[0], lons[lats.length - 1]); + private MultiPoint() { + } - double windingSum = 0d; - final int numPts = lats.length - 1; - for (int i = 1, j = 0; i < numPts; j = i++) { - minLat = Math.min(lats[i], minLat); - maxLat = Math.max(lats[i], maxLat); - minLon = Math.min(lons[i], minLon); - maxLon = Math.max(lons[i], maxLon); - // compute signed area for orientation - windingSum += (lons[j] - lons[numPts]) * (lats[i] - lats[numPts]) - - (lats[j] - lats[numPts]) * (lons[i] - lons[numPts]); - } - this.boundingBox = new Rectangle(minLat, maxLat, minLon, maxLon); + public MultiPoint(List points) { + super(points); } @Override @@ -73,144 +39,9 @@ public ShapeType type() { return ShapeType.MULTIPOINT; } - protected void checkLatArgs(double[] lats) { - if (lats == null) { - throw new IllegalArgumentException("lats must not be null"); - } - if (lats.length < 2) { - throw new IllegalArgumentException("at least 2 points are required"); - } - } - - protected void checkLonArgs(double[] lons) { - if (lons == null) { - throw new IllegalArgumentException("lons must not be null"); - } - } - - public int numPoints() { - return lats.length; - } - - private void checkVertexIndex(final int i) { - if (i >= lats.length) { - throw new IllegalArgumentException("Index " + i + " is outside the bounds of the " + lats.length + " vertices "); - } - } - - public double getLat(int vertex) { - checkVertexIndex(vertex); - return lats[vertex]; - } - - public double getLon(int vertex) { - checkVertexIndex(vertex); - return lons[vertex]; - } - - /** - * Returns a copy of the internal latitude array - */ - public double[] getLats() { - return lats.clone(); - } - - /** - * Returns a copy of the internal longitude array - */ - public double[] getLons() { - return lons.clone(); - } - - public Relation relate(double minLat, double maxLat, double minLon, double maxLon) { - // note: this relate is not used; points are indexed as separate POINT types - // note: if needed, we could build an in-memory BKD for each MultiPoint type - throw new UnsupportedOperationException("use Point.relate instead"); - } - - public Relation relate(GeoShape shape) { - return null; - } - - @Override - public boolean hasArea() { - return false; - } - - @Override - public boolean equals(Object other) { - if (super.equals(other) == false) return false; - MultiPoint o = getClass().cast(other); - return Arrays.equals(lats, o.lats) && Arrays.equals(lons, o.lons); - } - - @Override - public int hashCode() { - int result = super.hashCode(); - result = 31 * result + Arrays.hashCode(lats); - result = 31 * result + Arrays.hashCode(lons); - return result; - } - - @Override - protected StringBuilder contentToWKT() { - return coordinatesToWKT(lats, lons); - } - - protected static StringBuilder coordinatesToWKT(final double[] lats, final double[] lons) { - StringBuilder sb = new StringBuilder(); - if (lats.length == 0) { - sb.append(WKTParser.EMPTY); - } else { - // walk through coordinates: - sb.append(WKTParser.LPAREN); - sb.append(Point.coordinateToWKT(lats[0], lons[0])); - for (int i = 1; i < lats.length; ++i) { - sb.append(WKTParser.COMMA); - sb.append(WKTParser.SPACE); - sb.append(Point.coordinateToWKT(lats[i], lons[i])); - } - sb.append(WKTParser.RPAREN); - } - - return sb; - } - @Override - protected void appendWKBContent(OutputStreamDataOutput out) throws IOException { - out.writeVInt(numPoints()); - pointsToWKB(lats, lons, out, true); + public T visit(GeometryVisitor visitor) { + return visitor.visit(this); } - protected static OutputStreamDataOutput pointsToWKB(final double[] lats, final double[] lons, - OutputStreamDataOutput out, boolean writeHeader) throws IOException { - final int numPoints = lats.length; - for (int i = 0; i < numPoints; ++i) { - if (writeHeader == true) { - // write header for each coordinate (req. as part of spec for MultiPoints but not LineStrings) - out.writeVInt(WKBParser.ByteOrder.XDR.ordinal()); - out.writeVInt(ShapeType.POINT.wkbOrdinal()); - } - // write coordinates - Point.coordinateToWKB(lats[i], lons[i], out); - } - return out; - } - - @Override - public Iterator iterator() { - return new Iterator() { - int i = 0; - - @Override - public boolean hasNext() { - return i < numPoints(); - } - - @Override - public Point next() { - return new Point(lats[i], lons[i]); - } - }; - } } diff --git a/libs/geo/src/main/java/org/elasticsearch/geo/geometry/MultiPolygon.java b/libs/geo/src/main/java/org/elasticsearch/geo/geometry/MultiPolygon.java index 77510b7c2e652..3a289f6b4a793 100644 --- a/libs/geo/src/main/java/org/elasticsearch/geo/geometry/MultiPolygon.java +++ b/libs/geo/src/main/java/org/elasticsearch/geo/geometry/MultiPolygon.java @@ -19,125 +19,29 @@ package org.elasticsearch.geo.geometry; -import java.io.IOException; - -import org.elasticsearch.geo.parsers.WKTParser; -import org.apache.lucene.store.OutputStreamDataOutput; +import java.util.List; /** - * Created by nknize on 2/27/17. + * Collection of polygons */ -public class MultiPolygon extends MultiLine { - Predicate.PolygonPredicate predicate; - - public MultiPolygon(Polygon... polygons) { - super(polygons); - } - - @Override - public ShapeType type() { - return ShapeType.MULTIPOLYGON; - } - - @Override - public int length() { - return lines.length; - } - - @Override - public Polygon get(int index) { - checkVertexIndex(index); - return (Polygon) (lines[index]); - } - - @Override - public EdgeTree createEdgeTree() { - Polygon[] polygons = (Polygon[]) this.lines; - this.tree = Polygon.createEdgeTree(polygons); - predicate = Predicate.PolygonPredicate.create(this.boundingBox, tree); - return predicate.tree; - } +public class MultiPolygon extends GeometryCollection { + public static final MultiPolygon EMPTY = new MultiPolygon(); - public boolean pointInside(int encodedLat, int encodedLon) { - return predicate.test(encodedLat, encodedLon); - } + private MultiPolygon() { - @Override - public boolean hasArea() { - return true; } - @Override - public double computeArea() { - assertEdgeTree(); - return this.tree.getArea(); - } - - protected void assertEdgeTree() { - if (this.tree == null) { - final Polygon[] polygons = (Polygon[]) this.lines; - tree = Polygon.createEdgeTree(polygons); - } - } - - // private EdgeTree createEdgeTree(Polygon... polygons) { -// EdgeTree components[] = new EdgeTree[polygons.length]; -// for (int i = 0; i < components.length; i++) { -// Polygon gon = polygons[i]; -// Polygon gonHoles[] = gon.getHoles(); -// EdgeTree holes = null; -// if (gonHoles.length > 0) { -// holes = createEdgeTree(gonHoles); -// } -// components[i] = new EdgeTree(gon, holes); -// } -// return EdgeTree.createTree(components, 0, components.length - 1, false); -// } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - if (!super.equals(o)) return false; - - MultiPolygon that = (MultiPolygon) o; - return predicate.equals(that.predicate); - } - - @Override - public int hashCode() { - int result = super.hashCode(); - result = 31 * result + predicate.hashCode(); - return result; + public MultiPolygon(List polygons) { + super(polygons); } @Override - protected StringBuilder contentToWKT() { - final StringBuilder sb = new StringBuilder(); - Polygon[] polygons = (Polygon[]) lines; - if (polygons.length == 0) { - sb.append(WKTParser.EMPTY); - } else { - sb.append(WKTParser.LPAREN); - if (polygons.length > 0) { - sb.append(Polygon.polygonToWKT(polygons[0])); - } - for (int i = 1; i < polygons.length; ++i) { - sb.append(WKTParser.COMMA); - sb.append(Polygon.polygonToWKT(polygons[i])); - } - sb.append(WKTParser.RPAREN); - } - return sb; + public ShapeType type() { + return ShapeType.MULTIPOLYGON; } @Override - protected void appendWKBContent(OutputStreamDataOutput out) throws IOException { - int numPolys = length(); - out.writeVInt(numPolys); - for (int i = 0; i < numPolys; ++i) { - Polygon polygon = this.get(i); - Polygon.polygonToWKB(polygon, out, true); - } + public T visit(GeometryVisitor visitor) { + return visitor.visit(this); } } diff --git a/libs/geo/src/main/java/org/elasticsearch/geo/geometry/Point.java b/libs/geo/src/main/java/org/elasticsearch/geo/geometry/Point.java index 8aa4a67e964ac..9c71e91811219 100644 --- a/libs/geo/src/main/java/org/elasticsearch/geo/geometry/Point.java +++ b/libs/geo/src/main/java/org/elasticsearch/geo/geometry/Point.java @@ -19,25 +19,30 @@ package org.elasticsearch.geo.geometry; -import java.io.IOException; - import org.elasticsearch.geo.GeoUtils; -import org.elasticsearch.geo.parsers.WKTParser; -import org.apache.lucene.store.OutputStreamDataOutput; /** * Represents a Point on the earth's surface in decimal degrees. */ -public class Point extends GeoShape { - protected final double lat; - protected final double lon; +public class Point implements Geometry { + public static final Point EMPTY = new Point(); + + private final double lat; + private final double lon; + private final boolean empty; + + private Point() { + lat = 0; + lon = 0; + empty = true; + } public Point(double lat, double lon) { GeoUtils.checkLatitude(lat); GeoUtils.checkLongitude(lon); this.lat = lat; this.lon = lon; - this.boundingBox = null; + this.empty = false; } @Override @@ -53,90 +58,40 @@ public double lon() { return lon; } - public double minLat() { - return lat; - } - - public double maxLat() { - return lat; - } - - public double minLon() { - return lon; - } - - public double maxLon() { - return lon; - } - - @Override - public Rectangle getBoundingBox() { - throw new UnsupportedOperationException("Points do not have a bounding box"); - } - - @Override - public Point getCenter() { - return this; - } - - @Override - public boolean hasArea() { - return false; - } - - public Relation relate(double minLat, double maxLat, double minLon, double maxLon) { - if (lat < minLat || lat > maxLat || lon < minLon || lon > maxLon) { - return Relation.DISJOINT; - } - return Relation.WITHIN; - } - - public Relation relate(GeoShape shape) { - return null; - } - @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; - if (!super.equals(o)) return false; Point point = (Point) o; - + if (point.empty != empty) return false; if (Double.compare(point.lat, lat) != 0) return false; return Double.compare(point.lon, lon) == 0; } @Override public int hashCode() { - int result = super.hashCode(); + int result; long temp; temp = Double.doubleToLongBits(lat); - result = 31 * result + (int) (temp ^ (temp >>> 32)); + result = (int) (temp ^ (temp >>> 32)); temp = Double.doubleToLongBits(lon); result = 31 * result + (int) (temp ^ (temp >>> 32)); return result; } @Override - protected StringBuilder contentToWKT() { - return coordinateToWKT(lat, lon); - } - - protected static StringBuilder coordinateToWKT(final double lat, final double lon) { - final StringBuilder sb = new StringBuilder(); - sb.append(lon + WKTParser.SPACE + lat); - return sb; + public T visit(GeometryVisitor visitor) { + return visitor.visit(this); } @Override - protected void appendWKBContent(OutputStreamDataOutput out) throws IOException { - coordinateToWKB(lat, lon, out); + public boolean isEmpty() { + return empty; } - public static OutputStreamDataOutput coordinateToWKB(double lat, double lon, OutputStreamDataOutput out) throws IOException { - out.writeVLong(Double.doubleToRawLongBits(lon)); // lon - out.writeVLong(Double.doubleToRawLongBits(lat)); // lat - return out; + @Override + public String toString() { + return "lat=" + lat + ", lon=" + lon; } } diff --git a/libs/geo/src/main/java/org/elasticsearch/geo/geometry/Polygon.java b/libs/geo/src/main/java/org/elasticsearch/geo/geometry/Polygon.java index 135e7a78f3edb..9f28c4b81b6a2 100644 --- a/libs/geo/src/main/java/org/elasticsearch/geo/geometry/Polygon.java +++ b/libs/geo/src/main/java/org/elasticsearch/geo/geometry/Polygon.java @@ -19,53 +19,43 @@ package org.elasticsearch.geo.geometry; -import java.io.IOException; -import java.text.ParseException; -import java.util.Arrays; - -import org.elasticsearch.geo.parsers.SimpleGeoJSONPolygonParser; -import org.elasticsearch.geo.parsers.WKBParser; -import org.apache.lucene.store.OutputStreamDataOutput; +import java.util.Collections; +import java.util.List; +import java.util.Objects; /** - * Represents a closed polygon on the earth's surface. You can either construct the Polygon directly yourself with {@code double[]} - * coordinates, or use {@link Polygon#fromGeoJSON} if you have a polygon already encoded as a - * GeoJSON string. - *

- * NOTES: - *

    - *
  1. Coordinates must be in clockwise order, except for holes. Holes must be in counter-clockwise order. - *
  2. The polygon must be closed: the first and last coordinates need to have the same values. - *
  3. The polygon must not be self-crossing, otherwise may result in unexpected behavior. - *
  4. All latitude/longitude values must be in decimal degrees. - *
  5. Polygons cannot cross the 180th meridian. Instead, use two polygons: one on each side. - *
  6. For more advanced GeoSpatial indexing and query operations see the {@code spatial-extras} module - *
+ * Represents a closed polygon on the earth's surface with optional holes */ -public final class Polygon extends Line { - private final Polygon[] holes; +public final class Polygon implements Geometry { + public static final Polygon EMPTY = new Polygon(); + private final LinearRing polygon; + private final List holes; + + private Polygon() { + polygon = LinearRing.EMPTY; + holes = Collections.emptyList(); + } /** * Creates a new Polygon from the supplied latitude/longitude array, and optionally any holes. */ - public Polygon(double[] polyLats, double[] polyLons, Polygon... holes) { - super(polyLats, polyLons); + public Polygon(LinearRing polygon, List holes) { + this.polygon = polygon; + this.holes = holes; if (holes == null) { throw new IllegalArgumentException("holes must not be null"); } - if (polyLats[0] != polyLats[polyLats.length - 1]) { - throw new IllegalArgumentException("first and last points of the polygon must be the same (it must close itself): polyLats[0]=" + polyLats[0] + " polyLats[" + (polyLats.length - 1) + "]=" + polyLats[polyLats.length - 1]); - } - if (polyLons[0] != polyLons[polyLons.length - 1]) { - throw new IllegalArgumentException("first and last points of the polygon must be the same (it must close itself): polyLons[0]=" + polyLons[0] + " polyLons[" + (polyLons.length - 1) + "]=" + polyLons[polyLons.length - 1]); + checkRing(polygon); + for (LinearRing hole : holes) { + checkRing(hole); } - for (int i = 0; i < holes.length; i++) { - Polygon inner = holes[i]; - if (inner.holes.length > 0) { - throw new IllegalArgumentException("holes may not contain holes: polygons may not nest."); - } - } - this.holes = holes.clone(); + } + + /** + * Creates a new Polygon from the supplied latitude/longitude array, and optionally any holes. + */ + public Polygon(LinearRing polygon) { + this(polygon, Collections.emptyList()); } @Override @@ -73,170 +63,60 @@ public ShapeType type() { return ShapeType.POLYGON; } - @Override - protected void checkLatArgs(final double[] lats) { - super.checkLatArgs(lats); - if (lats.length < 4) { + private void checkRing(LinearRing ring) { + if (ring.length() < 4) { throw new IllegalArgumentException("at least 4 polygon points required"); } } - @Override - protected void checkLonArgs(final double[] lons) { - super.checkLonArgs(lons); - if (lons.length < 4) { - // being pedantic; the order of operations preclude this check, but we should do it anyway - throw new IllegalArgumentException("at least 4 polygon points required"); - } + public int getNumberOfHoles() { + return holes.size(); } - /** - * Returns a copy of the internal latitude array - */ - public double[] getPolyLats() { - return getLats(); - } - - /** - * Returns a copy of the internal longitude array - */ - public double[] getPolyLons() { - return getLons(); - } - - /** - * Returns a copy of the internal holes array - */ - public Polygon[] getHoles() { - return holes.clone(); - } - - public int numHoles() { - return holes.length; + public LinearRing getPolygon() { + return polygon; } - public Polygon getHole(int i) { - if (i >= holes.length) { - throw new IllegalArgumentException("Index " + i + " is outside the bounds of the " + holes.length + " polygon holes"); + public LinearRing getHole(int i) { + if (i >= holes.size()) { + throw new IllegalArgumentException("Index " + i + " is outside the bounds of the " + holes.size() + " polygon holes"); } - return holes[i]; - } - - /** - * Lazily builds an EdgeTree from multipolygon - */ - public static EdgeTree createEdgeTree(Polygon... polygons) { - EdgeTree components[] = new EdgeTree[polygons.length]; - for (int i = 0; i < components.length; i++) { - Polygon gon = polygons[i]; - Polygon gonHoles[] = gon.getHoles(); - EdgeTree holes = null; - if (gonHoles.length > 0) { - holes = createEdgeTree(gonHoles); - } - components[i] = new EdgeTree(gon, holes); - } - return EdgeTree.createTree(components, 0, components.length - 1, false); - } - - @Override - public boolean hasArea() { - return true; - } - - @Override - protected double computeArea() { - assertEdgeTree(); - return this.tree.getArea(); + return holes.get(i); } @Override - public Relation relate(double minLat, double maxLat, double minLon, double maxLon) { - assertEdgeTree(); - Relation r = tree.relate(minLat, maxLat, minLon, maxLon); - return r.transpose(); - } - - protected void assertEdgeTree() { - if (this.tree == null) { - tree = createEdgeTree(this); - } + public T visit(GeometryVisitor visitor) { + return visitor.visit(this); } @Override - public int hashCode() { - final int prime = 31; - int result = super.hashCode(); - result = prime * result + Arrays.hashCode(holes); - return result; + public boolean isEmpty() { + return polygon.isEmpty(); } - @Override - public boolean equals(Object obj) { - if (super.equals(obj) == false) return false; - Polygon other = (Polygon) obj; - if (!Arrays.equals(holes, other.holes)) return false; - return true; - } @Override public String toString() { - StringBuilder sb = new StringBuilder(super.toString()); - if (holes.length > 0) { + StringBuilder sb = new StringBuilder(); + sb.append("polygon=").append(polygon); + if (holes.size() > 0) { sb.append(", holes="); - sb.append(Arrays.toString(holes)); + sb.append(holes); } return sb.toString(); } - protected static StringBuilder polygonToWKT(final Polygon polygon) { - StringBuilder sb = new StringBuilder(); - sb.append('('); - sb.append(MultiPoint.coordinatesToWKT(polygon.lats, polygon.lons)); - Polygon[] holes = polygon.getHoles(); - for (int i = 0; i < holes.length; ++i) { - sb.append(", "); - sb.append(MultiPoint.coordinatesToWKT(holes[i].lats, holes[i].lons)); - } - sb.append(')'); - return sb; - } - @Override - protected StringBuilder contentToWKT() { - return polygonToWKT(this); - } - - /** - * Parses a standard GeoJSON polygon string. The type of the incoming GeoJSON object must be a Polygon or MultiPolygon, optionally - * embedded under a "type: Feature". A Polygon will return as a length 1 array, while a MultiPolygon will be 1 or more in length. - * - *

See the GeoJSON specification. - */ - public static Polygon[] fromGeoJSON(String geojson) throws ParseException { - return new SimpleGeoJSONPolygonParser(geojson).parse(); + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Polygon polygon1 = (Polygon) o; + return Objects.equals(polygon, polygon1.polygon) && + Objects.equals(holes, polygon1.holes); } @Override - protected void appendWKBContent(OutputStreamDataOutput out) throws IOException { - polygonToWKB(this, out, false); - } - - public static void polygonToWKB(final Polygon polygon, OutputStreamDataOutput out, - final boolean writeHeader) throws IOException { - if (writeHeader == true) { - out.writeVInt(WKBParser.ByteOrder.XDR.ordinal()); - out.writeVInt(ShapeType.POLYGON.wkbOrdinal()); - } - int numHoles = polygon.numHoles(); - out.writeVInt(numHoles + 1); // number rings - // write shell - Line.lineToWKB(polygon.lats, polygon.lons, out, false); - // write holes - Polygon hole; - for (int i = 0; i < numHoles; ++i) { - hole = polygon.getHole(i); - Line.lineToWKB(hole.lats, hole.lons, out, false); - } + public int hashCode() { + return Objects.hash(polygon, holes); } } diff --git a/libs/geo/src/main/java/org/elasticsearch/geo/geometry/Predicate.java b/libs/geo/src/main/java/org/elasticsearch/geo/geometry/Predicate.java deleted file mode 100644 index 59822fe7ec1fc..0000000000000 --- a/libs/geo/src/main/java/org/elasticsearch/geo/geometry/Predicate.java +++ /dev/null @@ -1,254 +0,0 @@ -/* - * 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.geo.geometry; - -import java.util.function.Function; - -import org.elasticsearch.geo.GeoUtils; -import org.elasticsearch.geo.geometry.GeoShape.Relation; -import org.apache.lucene.util.SloppyMath; - -import static org.elasticsearch.geo.GeoEncodingUtils.decodeLatitude; -import static org.elasticsearch.geo.GeoEncodingUtils.decodeLongitude; -import static org.elasticsearch.geo.GeoEncodingUtils.encodeLatitude; -import static org.elasticsearch.geo.GeoEncodingUtils.encodeLatitudeCeil; -import static org.elasticsearch.geo.GeoEncodingUtils.encodeLongitude; -import static org.elasticsearch.geo.GeoEncodingUtils.encodeLongitudeCeil; - -/** - * Used to speed up point-in-polygon and point-distance computations - */ -abstract class Predicate { - static final int ARITY = 64; - - final int latShift, lonShift; - final int latBase, lonBase; - final int maxLatDelta, maxLonDelta; - final byte[] relations; - - protected Predicate(Rectangle boundingBox, Function boxToRelation) { - final int minLat = encodeLatitudeCeil(boundingBox.minLat); - final int maxLat = encodeLatitude(boundingBox.maxLat); - final int minLon = encodeLongitudeCeil(boundingBox.minLon); - final int maxLon = encodeLongitude(boundingBox.maxLon); - - int latShift = 1; - int lonShift = 1; - int latBase = 0; - int lonBase = 0; - int maxLatDelta = 0; - int maxLonDelta = 0; - byte[] relations; - - if (maxLat < minLat || (boundingBox.crossesDateline() == false && maxLon < minLon)) { - // the box cannot match any quantized point - relations = new byte[0]; - } else { - { - long minLat2 = (long) minLat - Integer.MIN_VALUE; - long maxLat2 = (long) maxLat - Integer.MIN_VALUE; - latShift = computeShift(minLat2, maxLat2); - latBase = (int) (minLat2 >>> latShift); - maxLatDelta = (int) (maxLat2 >>> latShift) - latBase + 1; - assert maxLatDelta > 0; - } - { - long minLon2 = (long) minLon - Integer.MIN_VALUE; - long maxLon2 = (long) maxLon - Integer.MIN_VALUE; - if (boundingBox.crossesDateline()) { - maxLon2 += 1L << 32; // wrap - } - lonShift = computeShift(minLon2, maxLon2); - lonBase = (int) (minLon2 >>> lonShift); - maxLonDelta = (int) (maxLon2 >>> lonShift) - lonBase + 1; - assert maxLonDelta > 0; - } - - relations = new byte[maxLatDelta * maxLonDelta]; - for (int i = 0; i < maxLatDelta; ++i) { - for (int j = 0; j < maxLonDelta; ++j) { - final int boxMinLat = ((latBase + i) << latShift) + Integer.MIN_VALUE; - final int boxMinLon = ((lonBase + j) << lonShift) + Integer.MIN_VALUE; - final int boxMaxLat = boxMinLat + (1 << latShift) - 1; - final int boxMaxLon = boxMinLon + (1 << lonShift) - 1; - - relations[i * maxLonDelta + j] = (byte) boxToRelation.apply(new Rectangle( - decodeLatitude(boxMinLat), decodeLatitude(boxMaxLat), - decodeLongitude(boxMinLon), decodeLongitude(boxMaxLon))).ordinal(); - } - } - } - this.latShift = latShift; - this.lonShift = lonShift; - this.latBase = latBase; - this.lonBase = lonBase; - this.maxLatDelta = maxLatDelta; - this.maxLonDelta = maxLonDelta; - this.relations = relations; - } - - /** - * A predicate that checks whether a given point is within a distance of another point. - */ - final static class DistancePredicate extends Predicate { - - private final double lat, lon; - private final double distanceKey; - private final double axisLat; - - private DistancePredicate(double lat, double lon, double distanceKey, double axisLat, Rectangle boundingBox, - Function boxToRelation) { - super(boundingBox, boxToRelation); - this.lat = lat; - this.lon = lon; - this.distanceKey = distanceKey; - this.axisLat = axisLat; - } - - /** - * Create a predicate that checks whether points are within a distance of a given point. - * It works by computing the bounding box around the circle that is defined - * by the given points/distance and splitting it into between 1024 and 4096 - * smaller boxes (4096*0.75^2=2304 on average). Then for each sub box, it - * computes the relation between this box and the distance query. Finally at - * search time, it first computes the sub box that the point belongs to, - * most of the time, no distance computation will need to be performed since - * all points from the sub box will either be in or out of the circle. - * - * @lucene.internal - */ - static DistancePredicate create(double lat, double lon, double radiusMeters) { - final Rectangle boundingBox = Rectangle.fromPointDistance(lat, lon, radiusMeters); - final double axisLat = Rectangle.axisLat(lat, radiusMeters); - final double distanceSortKey = GeoUtils.distanceQuerySortKey(radiusMeters); - final Function boxToRelation = box -> GeoUtils.relate( - box.minLat, box.maxLat, box.minLon, box.maxLon, lat, lon, distanceSortKey, axisLat); - return new DistancePredicate(lat, lon, distanceSortKey, axisLat, boundingBox, boxToRelation); - } - - public Relation relate(double minLat, double maxLat, double minLon, double maxLon) { - return GeoUtils.relate(minLat, maxLat, minLon, maxLon, lat, lon, distanceKey, axisLat); - } - - /** - * Check whether the given point is within a distance of another point. - * NOTE: this operates directly on the encoded representation of points. - */ - public boolean test(int lat, int lon) { - final int lat2 = ((lat - Integer.MIN_VALUE) >>> latShift); - if (lat2 < latBase || lat2 >= latBase + maxLatDelta) { - return false; - } - int lon2 = ((lon - Integer.MIN_VALUE) >>> lonShift); - if (lon2 < lonBase) { // wrap - lon2 += 1 << (32 - lonShift); - } - assert Integer.toUnsignedLong(lon2) >= lonBase; - assert lon2 - lonBase >= 0; - if (lon2 - lonBase >= maxLonDelta) { - return false; - } - - final int relation = relations[(lat2 - latBase) * maxLonDelta + (lon2 - lonBase)]; - if (relation == Relation.CROSSES.ordinal()) { - return SloppyMath.haversinSortKey( - decodeLatitude(lat), decodeLongitude(lon), - this.lat, this.lon) <= distanceKey; - } else { - return relation == Relation.WITHIN.ordinal(); - } - } - } - - /** - * A predicate that checks whether a given point is within a polygon. - */ - final static class PolygonPredicate extends Predicate { - - final EdgeTree tree; - - private PolygonPredicate(EdgeTree tree, Rectangle boundingBox, Function boxToRelation) { - super(boundingBox, boxToRelation); - this.tree = tree; - } - - /** - * Create a predicate that checks whether points are within a polygon. - * It works the same way as {@code DistancePredicate.create}. - * - * @lucene.internal - */ - public static PolygonPredicate create(Rectangle boundingBox, EdgeTree tree) { - final Function boxToRelation = box -> tree.relate( - box.minLat, box.maxLat, box.minLon, box.maxLon); - return new PolygonPredicate(tree, boundingBox, boxToRelation); - } - - - public Relation relate(final double minLat, final double maxLat, final double minLon, final double maxLon) { - return tree.relate(minLat, maxLat, minLon, maxLon); - } - - /** - * Check whether the given point is within the considered polygon. - * NOTE: this operates directly on the encoded representation of points. - */ - public boolean test(int lat, int lon) { - final int lat2 = ((lat - Integer.MIN_VALUE) >>> latShift); - if (lat2 < latBase || lat2 >= latBase + maxLatDelta) { - return false; - } - int lon2 = ((lon - Integer.MIN_VALUE) >>> lonShift); - if (lon2 < lonBase) { // wrap - lon2 += 1 << (32 - lonShift); - } - assert Integer.toUnsignedLong(lon2) >= lonBase; - assert lon2 - lonBase >= 0; - if (lon2 - lonBase >= maxLonDelta) { - return false; - } - - final int relation = relations[(lat2 - latBase) * maxLonDelta + (lon2 - lonBase)]; - if (relation == Relation.CROSSES.ordinal()) { - return tree.contains(decodeLatitude(lat), decodeLongitude(lon)); - } else { - return relation == Relation.WITHIN.ordinal(); - } - } - } - - /** - * Compute the minimum shift value so that - * {@code (b>>>shift)-(a>>>shift)} is less that {@code ARITY}. - */ - private static int computeShift(long a, long b) { - assert a <= b; - // We enforce a shift of at least 1 so that when we work with unsigned ints - // by doing (lat - MIN_VALUE), the result of the shift (lat - MIN_VALUE) >>> shift - // can be used for comparisons without particular care: the sign bit has - // been cleared so comparisons work the same for signed and unsigned ints - for (int shift = 1; ; ++shift) { - final long delta = (b >>> shift) - (a >>> shift); - if (delta >= 0 && delta < ARITY) { - return shift; - } - } - } -} diff --git a/libs/geo/src/main/java/org/elasticsearch/geo/geometry/Rectangle.java b/libs/geo/src/main/java/org/elasticsearch/geo/geometry/Rectangle.java index 9a0d0bd62a278..37d9fd337562c 100644 --- a/libs/geo/src/main/java/org/elasticsearch/geo/geometry/Rectangle.java +++ b/libs/geo/src/main/java/org/elasticsearch/geo/geometry/Rectangle.java @@ -19,55 +19,39 @@ package org.elasticsearch.geo.geometry; -import java.io.IOException; - import org.elasticsearch.geo.GeoUtils; -import org.elasticsearch.geo.parsers.WKTParser; -import org.apache.lucene.store.OutputStreamDataOutput; - -import static java.lang.Math.PI; -import static java.lang.Math.max; -import static java.lang.Math.min; -import static org.elasticsearch.geo.GeoUtils.checkLatitude; -import static org.elasticsearch.geo.GeoUtils.checkLongitude; -import static org.elasticsearch.geo.GeoUtils.MAX_LAT_INCL; -import static org.elasticsearch.geo.GeoUtils.MIN_LAT_INCL; -import static org.elasticsearch.geo.GeoUtils.MAX_LAT_RADIANS; -import static org.elasticsearch.geo.GeoUtils.MAX_LON_RADIANS; -import static org.elasticsearch.geo.GeoUtils.MIN_LAT_RADIANS; -import static org.elasticsearch.geo.GeoUtils.MIN_LON_RADIANS; -import static org.elasticsearch.geo.GeoUtils.EARTH_MEAN_RADIUS_METERS; -import static org.elasticsearch.geo.GeoUtils.sloppySin; -import static org.apache.lucene.util.SloppyMath.TO_DEGREES; -import static org.apache.lucene.util.SloppyMath.asin; -import static org.apache.lucene.util.SloppyMath.cos; -import static org.apache.lucene.util.SloppyMath.toDegrees; -import static org.apache.lucene.util.SloppyMath.toRadians; /** * Represents a lat/lon rectangle in decimal degrees. */ -public class Rectangle extends GeoShape { +public class Rectangle implements Geometry { + public static final Rectangle EMPTY = new Rectangle(); /** * maximum longitude value (in degrees) */ - public final double minLat; + private final double minLat; /** * minimum longitude value (in degrees) */ - public final double minLon; + private final double minLon; /** * maximum latitude value (in degrees) */ - public final double maxLat; + private final double maxLat; /** * minimum latitude value (in degrees) */ - public final double maxLon; - /** - * center of rectangle (in lat/lon degrees) - */ - private final Point center; + private final double maxLon; + + private final boolean empty; + + private Rectangle() { + minLat = 0; + minLon = 0; + maxLat = 0; + maxLon = 0; + empty = true; + } /** * Constructs a bounding box by first validating the provided latitude and longitude coordinates @@ -81,23 +65,10 @@ public Rectangle(double minLat, double maxLat, double minLon, double maxLon) { this.maxLon = maxLon; this.minLat = minLat; this.maxLat = maxLat; - assert maxLat >= minLat; - - // NOTE: cannot assert maxLon >= minLon since this rect could cross the dateline - this.boundingBox = this; - - // compute the center of the rectangle - final double cntrLat = getHeight() / 2 + minLat; - double cntrLon = getWidth() / 2 + minLon; - if (crossesDateline()) { - cntrLon = GeoUtils.normalizeLonDegrees(cntrLon); + empty = false; + if (maxLat < minLat) { + throw new IllegalArgumentException("max lat cannot be less than min lat"); } - this.center = new Point(cntrLat, cntrLon); - } - - @Override - public boolean hasArea() { - return minLat != maxLat && minLon != maxLon; } public double getWidth() { @@ -111,115 +82,25 @@ public double getHeight() { return maxLat - minLat; } - public Point getCenter() { - return this.center; - } - - @Override - public ShapeType type() { - return ShapeType.ENVELOPE; + public double getMinLat() { + return minLat; } - @Override - public Relation relate(double minLat, double maxLat, double minLon, double maxLon) { - if (minLat == GeoUtils.MIN_LAT_INCL && maxLat == GeoUtils.MAX_LAT_INCL - && minLon == GeoUtils.MIN_LON_INCL && maxLon == GeoUtils.MAX_LON_INCL) { - return Relation.WITHIN; - } else if (this.minLat == GeoUtils.MIN_LAT_INCL && this.maxLat == GeoUtils.MAX_LAT_INCL - && this.minLon == GeoUtils.MIN_LON_INCL && this.maxLon == GeoUtils.MAX_LON_INCL) { - return Relation.CONTAINS; - } else if (crossesDateline() == true) { - return relateXDL(minLat, maxLat, minLon, maxLon); - } else if (minLon > maxLon) { - if (rectDisjoint(this.minLat, this.maxLat, this.minLon, this.maxLon, minLat, maxLat, minLon, GeoUtils.MAX_LON_INCL) - && rectDisjoint(this.minLat, this.maxLat, this.minLon, this.maxLon, minLat, maxLat, GeoUtils.MIN_LON_INCL, maxLon)) { - return Relation.DISJOINT; - } else if (rectWithin(this.minLat, this.maxLat, this.minLon, this.maxLon, minLat, maxLat, minLon, GeoUtils.MAX_LON_INCL) - || rectWithin(this.minLat, this.maxLat, this.minLon, this.maxLon, minLat, maxLat, GeoUtils.MIN_LON_INCL, maxLon)) { - return Relation.WITHIN; - } // can't contain - } else if (rectDisjoint(this.minLat, this.maxLat, this.minLon, this.maxLon, minLat, maxLat, minLon, maxLon)) { - return Relation.DISJOINT; - } else if (rectWithin(this.minLat, this.maxLat, this.minLon, this.maxLon, minLat, maxLat, minLon, maxLon)) { - return Relation.WITHIN; - } else if (rectWithin(minLat, maxLat, minLon, maxLon, this.minLat, this.maxLat, this.minLon, this.maxLon)) { - return Relation.CONTAINS; - } - return Relation.INTERSECTS; + public double getMinLon() { + return minLon; } - /** - * compute relation for this Rectangle crossing the dateline - */ - private Relation relateXDL(double minLat, double maxLat, double minLon, double maxLon) { - if (minLon > maxLon) { - // incoming rectangle crosses dateline; just check latitude for disjoint - if (this.minLat > maxLat || this.maxLat < minLat) { - return Relation.DISJOINT; - } else if (rectWithin(this.minLat, this.maxLat, this.minLon, this.maxLon, minLat, maxLat, minLon, maxLon)) { - return Relation.WITHIN; - } else if (rectWithin(minLat, maxLat, minLon, maxLon, this.minLat, this.maxLat, this.minLon, this.maxLon)) { - return Relation.CONTAINS; - } - } else { - if (rectDisjoint(this.minLat, this.maxLat, this.minLon, GeoUtils.MAX_LON_INCL, minLat, maxLat, minLon, maxLon) - && rectDisjoint(this.minLat, this.maxLat, GeoUtils.MIN_LON_INCL, this.maxLon, minLat, maxLat, minLon, maxLon)) { - return Relation.DISJOINT; - // WITHIN not possible; *this* rectangle crosses the dateline but *that* rectangle does not - } else if (rectWithin(minLat, maxLat, minLon, maxLon, this.minLat, this.maxLat, this.minLon, GeoUtils.MAX_LON_INCL) - || rectWithin(minLat, maxLat, minLon, maxLon, this.minLat, this.maxLat, GeoUtils.MIN_LON_INCL, this.maxLon)) { - return Relation.CONTAINS; - } - } - return Relation.INTERSECTS; + public double getMaxLat() { + return maxLat; } - public Relation relate(double lat, double lon) { - if (lat > maxLat || lat < minLat || lon < minLon || lon > maxLon) { - return Relation.DISJOINT; - } - return Relation.INTERSECTS; + public double getMaxLon() { + return maxLon; } @Override - public Relation relate(GeoShape shape) { - Relation r = shape.relate(this.minLat, this.maxLat, this.minLon, this.maxLon); - if (r == Relation.WITHIN) { - return Relation.CONTAINS; - } else if (r == Relation.CONTAINS) { - return Relation.WITHIN; - } - return r; - } - - /** - * Computes whether two rectangles are disjoint - */ - private static boolean rectDisjoint(final double aMinLat, final double aMaxLat, final double aMinLon, final double aMaxLon, - final double bMinLat, final double bMaxLat, final double bMinLon, final double bMaxLon) { - assert aMinLon <= aMaxLon : "dateline crossing not supported"; - assert bMinLon <= bMaxLon : "dateline crossing not supported"; - // fail quickly: test latitude - if (aMaxLat < bMinLat || aMinLat > bMaxLat) { - return true; - } - - // check sharing dateline - if ((aMinLon == GeoUtils.MIN_LON_INCL && bMaxLon == GeoUtils.MAX_LON_INCL) - || (bMinLon == GeoUtils.MIN_LON_INCL && aMaxLon == GeoUtils.MAX_LON_INCL)) { - return false; - } - - // check longitude - return aMaxLon < bMinLon || aMinLon > bMaxLon; - } - - /** - * Computes whether the first (a) rectangle is wholly within another (b) rectangle (shared boundaries allowed) - */ - private static boolean rectWithin(final double aMinLat, final double aMaxLat, final double aMinLon, final double aMaxLon, - final double bMinLat, final double bMaxLat, final double bMinLon, final double bMaxLon) { - return !(aMinLon < bMinLon || aMinLat < bMinLat || aMaxLon > bMaxLon || aMaxLat > bMaxLat); + public ShapeType type() { + return ShapeType.ENVELOPE; } @Override @@ -248,113 +129,6 @@ public boolean crossesDateline() { return maxLon < minLon; } - /** - * Compute Bounding Box for a circle using WGS-84 parameters - */ - public static Rectangle fromPointDistance(final double centerLat, final double centerLon, final double radiusMeters) { - checkLatitude(centerLat); - checkLongitude(centerLon); - final double radLat = toRadians(centerLat); - final double radLon = toRadians(centerLon); - // LUCENE-7143 - double radDistance = (radiusMeters + 7E-2) / EARTH_MEAN_RADIUS_METERS; - double minLat = radLat - radDistance; - double maxLat = radLat + radDistance; - double minLon; - double maxLon; - - if (minLat > MIN_LAT_RADIANS && maxLat < MAX_LAT_RADIANS) { - double deltaLon = asin(sloppySin(radDistance) / cos(radLat)); - minLon = radLon - deltaLon; - if (minLon < MIN_LON_RADIANS) { - minLon += 2d * PI; - } - maxLon = radLon + deltaLon; - if (maxLon > MAX_LON_RADIANS) { - maxLon -= 2d * PI; - } - } else { - // a pole is within the distance - minLat = max(minLat, MIN_LAT_RADIANS); - maxLat = min(maxLat, MAX_LAT_RADIANS); - minLon = MIN_LON_RADIANS; - maxLon = MAX_LON_RADIANS; - } - - return new Rectangle(toDegrees(minLat), toDegrees(maxLat), toDegrees(minLon), toDegrees(maxLon)); - } - - /** - * maximum error from {@link #axisLat(double, double)}. logic must be prepared to handle this - */ - public static final double AXISLAT_ERROR = 0.1D / EARTH_MEAN_RADIUS_METERS * TO_DEGREES; - - /** - * Calculate the latitude of a circle's intersections with its bbox meridians. - *

- * NOTE: the returned value will be +/- {@link #AXISLAT_ERROR} of the actual value. - * - * @param centerLat The latitude of the circle center - * @param radiusMeters The radius of the circle in meters - * @return A latitude - */ - public static double axisLat(double centerLat, double radiusMeters) { - // A spherical triangle with: - // r is the radius of the circle in radians - // l1 is the latitude of the circle center - // l2 is the latitude of the point at which the circle intersect's its bbox longitudes - // We know r is tangent to the bbox meridians at l2, therefore it is a right angle. - // So from the law of cosines, with the angle of l1 being 90, we have: - // cos(l1) = cos(r) * cos(l2) + sin(r) * sin(l2) * cos(90) - // The second part cancels out because cos(90) == 0, so we have: - // cos(l1) = cos(r) * cos(l2) - // Solving for l2, we get: - // l2 = acos( cos(l1) / cos(r) ) - // We ensure r is in the range (0, PI/2) and l1 in the range (0, PI/2]. This means we - // cannot divide by 0, and we will always get a positive value in the range [0, 1) as - // the argument to arc cosine, resulting in a range (0, PI/2]. - final double PIO2 = Math.PI / 2D; - double l1 = toRadians(centerLat); - double r = (radiusMeters + 7E-2) / EARTH_MEAN_RADIUS_METERS; - - // if we are within radius range of a pole, the lat is the pole itself - if (Math.abs(l1) + r >= MAX_LAT_RADIANS) { - return centerLat >= 0 ? MAX_LAT_INCL : MIN_LAT_INCL; - } - - // adjust l1 as distance from closest pole, to form a right triangle with bbox meridians - // and ensure it is in the range (0, PI/2] - l1 = centerLat >= 0 ? PIO2 - l1 : l1 + PIO2; - - double l2 = Math.acos(Math.cos(l1) / Math.cos(r)); - assert !Double.isNaN(l2); - - // now adjust back to range [-pi/2, pi/2], ie latitude in radians - l2 = centerLat >= 0 ? PIO2 - l2 : l2 - PIO2; - - return toDegrees(l2); - } - - /** - * Returns the bounding box over an array of polygons - */ - public static Rectangle fromPolygon(Polygon[] polygons) { - // compute bounding box - double minLat = Double.POSITIVE_INFINITY; - double maxLat = Double.NEGATIVE_INFINITY; - double minLon = Double.POSITIVE_INFINITY; - double maxLon = Double.NEGATIVE_INFINITY; - - for (int i = 0; i < polygons.length; i++) { - minLat = min(polygons[i].minLat(), minLat); - maxLat = max(polygons[i].maxLat(), maxLat); - minLon = min(polygons[i].minLon(), minLon); - maxLon = max(polygons[i].maxLon(), maxLon); - } - - return new Rectangle(minLat, maxLat, minLon, maxLon); - } - @Override public boolean equals(Object o) { if (this == o) return true; @@ -385,31 +159,12 @@ public int hashCode() { } @Override - protected StringBuilder contentToWKT() { - StringBuilder sb = new StringBuilder(); - - sb.append(WKTParser.LPAREN); - // minX, maxX, maxY, minY - sb.append(minLon); - sb.append(WKTParser.COMMA); - sb.append(WKTParser.SPACE); - sb.append(maxLon); - sb.append(WKTParser.COMMA); - sb.append(WKTParser.SPACE); - sb.append(maxLat); - sb.append(WKTParser.COMMA); - sb.append(WKTParser.SPACE); - sb.append(minLat); - sb.append(WKTParser.RPAREN); - - return sb; + public T visit(GeometryVisitor visitor) { + return visitor.visit(this); } @Override - protected void appendWKBContent(OutputStreamDataOutput out) throws IOException { - out.writeVLong(Double.doubleToRawLongBits(minLon)); - out.writeVLong(Double.doubleToRawLongBits(maxLon)); - out.writeVLong(Double.doubleToRawLongBits(maxLat)); - out.writeVLong(Double.doubleToRawLongBits(minLat)); + public boolean isEmpty() { + return empty; } } diff --git a/libs/geo/src/main/java/org/elasticsearch/geo/geometry/ShapeType.java b/libs/geo/src/main/java/org/elasticsearch/geo/geometry/ShapeType.java index 5e0e75cab2c54..2272f1ad89410 100644 --- a/libs/geo/src/main/java/org/elasticsearch/geo/geometry/ShapeType.java +++ b/libs/geo/src/main/java/org/elasticsearch/geo/geometry/ShapeType.java @@ -19,65 +19,18 @@ package org.elasticsearch.geo.geometry; -import java.util.HashMap; -import java.util.Locale; -import java.util.Map; - /** - * Created by nknize on 9/22/17. + * Shape types supported by elasticsearch */ public enum ShapeType { - POINT("point", 1), - MULTIPOINT("multipoint", 4), - LINESTRING("linestring", 2), - MULTILINESTRING("multilinestring", 5), - POLYGON("polygon", 3), - MULTIPOLYGON("multipolygon", 6), - GEOMETRYCOLLECTION("geometrycollection", 7), - ENVELOPE("envelope", 8), // not part of the actual WKB spec - CIRCLE("circle", 9); // not part of the actual WKB spec - - private final String shapeName; - private final int wkbOrdinal; - private static Map shapeTypeMap = new HashMap<>(); - private static Map wkbTypeMap = new HashMap<>(); - private static final String BBOX = "BBOX"; - - static { - for (ShapeType type : values()) { - shapeTypeMap.put(type.shapeName, type); - wkbTypeMap.put(type.wkbOrdinal, type); - } - shapeTypeMap.put(ENVELOPE.wktName().toLowerCase(Locale.ROOT), ENVELOPE); - } - - ShapeType(String shapeName, int wkbOrdinal) { - this.shapeName = shapeName; - this.wkbOrdinal = wkbOrdinal; - } - - protected String typename() { - return shapeName; - } - - /** - * wkt shape name - */ - public String wktName() { - return this == ENVELOPE ? BBOX : this.shapeName; - } - - public int wkbOrdinal() { - return this.wkbOrdinal; - } - - public static ShapeType forName(String shapename) { - String typename = shapename.toLowerCase(Locale.ROOT); - for (ShapeType type : values()) { - if (type.shapeName.equals(typename)) { - return type; - } - } - throw new IllegalArgumentException("unknown geo_shape [" + shapename + "]"); - } + POINT, + MULTIPOINT, + LINESTRING, + MULTILINESTRING, + POLYGON, + MULTIPOLYGON, + GEOMETRYCOLLECTION, + LINEARRING, // not serialized by itself in WKT or WKB + ENVELOPE, // not part of the actual WKB spec + CIRCLE; // not part of the actual WKB spec } diff --git a/libs/geo/src/main/java/org/elasticsearch/geo/parsers/SimpleGeoJSONPolygonParser.java b/libs/geo/src/main/java/org/elasticsearch/geo/parsers/SimpleGeoJSONPolygonParser.java deleted file mode 100644 index 40d6c16377bb8..0000000000000 --- a/libs/geo/src/main/java/org/elasticsearch/geo/parsers/SimpleGeoJSONPolygonParser.java +++ /dev/null @@ -1,445 +0,0 @@ -/* - * 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.geo.parsers; - -import java.text.ParseException; -import java.util.ArrayList; -import java.util.List; - -import org.elasticsearch.geo.geometry.Polygon; - -/* - We accept either a whole type: Feature, like this: - - { "type": "Feature", - "geometry": { - "type": "Polygon", - "coordinates": [ - [ [100.0, 0.0], [101.0, 0.0], [101.0, 1.0], - [100.0, 1.0], [100.0, 0.0] ] - ] - }, - "properties": { - "prop0": "value0", - "prop1": {"this": "that"} - } - } - - Or the inner object with type: Multi/Polygon. - - Or a type: FeatureCollection, if it has only one Feature which is a Polygon or MultiPolyon. - - type: MultiPolygon (union of polygons) is also accepted. -*/ - -/** Does minimal parsing of a GeoJSON object, to extract either Polygon or MultiPolygon, either directly as a the top-level type, or if - * the top-level type is Feature, as the geometry of that feature. */ - -@SuppressWarnings("unchecked") -public class SimpleGeoJSONPolygonParser { - final String input; - private int upto; - private String polyType; - private List coordinates; - - public SimpleGeoJSONPolygonParser(String input) { - this.input = input; - } - - public Polygon[] parse() throws ParseException { - // parse entire object - parseObject(""); - - // make sure there's nothing left: - readEnd(); - - // The order of JSON object keys (type, geometry, coordinates in our case) can be arbitrary, so we wait until we are done parsing to - // put the pieces together here: - - if (coordinates == null) { - throw newParseException("did not see any polygon coordinates"); - } - - if (polyType == null) { - throw newParseException("did not see type: Polygon or MultiPolygon"); - } - - if (polyType.equals("Polygon")) { - return new Polygon[] {parsePolygon(coordinates)}; - } else { - List polygons = new ArrayList<>(); - for(int i=0;i) o)); - } - - return polygons.toArray(new Polygon[polygons.size()]); - } - } - - /** path is the "address" by keys of where we are, e.g. geometry.coordinates */ - private void parseObject(String path) throws ParseException { - scan('{'); - boolean first = true; - while (true) { - char ch = peek(); - if (ch == '}') { - break; - } else if (first == false) { - if (ch == ',') { - // ok - upto++; - ch = peek(); - if (ch == '}') { - break; - } - } else { - throw newParseException("expected , but got " + ch); - } - } - - first = false; - - int uptoStart = upto; - String key = parseString(); - - if (path.equals("crs.properties") && key.equals("href")) { - upto = uptoStart; - throw newParseException("cannot handle linked crs"); - } - - scan(':'); - - Object o; - - ch = peek(); - - uptoStart = upto; - - if (ch == '[') { - String newPath; - if (path.length() == 0) { - newPath = key; - } else { - newPath = path + "." + key; - } - o = parseArray(newPath); - } else if (ch == '{') { - String newPath; - if (path.length() == 0) { - newPath = key; - } else { - newPath = path + "." + key; - } - parseObject(newPath); - o = null; - } else if (ch == '"') { - o = parseString(); - } else if (ch == 't') { - scan("true"); - o = Boolean.TRUE; - } else if (ch == 'f') { - scan("false"); - o = Boolean.FALSE; - } else if (ch == 'n') { - scan("null"); - o = null; - } else if (ch == '-' || ch == '.' || (ch >= '0' && ch <= '9')) { - o = parseNumber(); - } else if (ch == '}') { - break; - } else { - throw newParseException("expected array, object, string or literal value, but got: " + ch); - } - - if (path.equals("crs.properties") && key.equals("name")) { - if (o instanceof String == false) { - upto = uptoStart; - throw newParseException("crs.properties.name should be a string, but saw: " + o); - } - String crs = (String) o; - if (crs.startsWith("urn:ogc:def:crs:OGC") == false || crs.endsWith(":CRS84") == false) { - upto = uptoStart; - throw newParseException("crs must be CRS84 from OGC, but saw: " + o); - } - } - - if (key.equals("type") && path.startsWith("crs") == false) { - if (o instanceof String == false) { - upto = uptoStart; - throw newParseException("type should be a string, but got: " + o); - } - String type = (String) o; - if (type.equals("Polygon") && isValidGeometryPath(path)) { - polyType = "Polygon"; - } else if (type.equals("MultiPolygon") && isValidGeometryPath(path)) { - polyType = "MultiPolygon"; - } else if ((type.equals("FeatureCollection") || type.equals("Feature")) && (path.equals("features.[]") || path.equals(""))) { - // OK, we recurse - } else { - upto = uptoStart; - throw newParseException("can only handle type FeatureCollection (if it has a single polygon geometry), Feature, Polygon or MutiPolygon, but got " + type); - } - } else if (key.equals("coordinates") && isValidGeometryPath(path)) { - if (o instanceof List == false) { - upto = uptoStart; - throw newParseException("coordinates should be an array, but got: " + o.getClass()); - } - if (coordinates != null) { - upto = uptoStart; - throw newParseException("only one Polygon or MultiPolygon is supported"); - } - coordinates = (List) o; - } - } - - scan('}'); - } - - /** Returns true if the object path is a valid location to see a Multi/Polygon geometry */ - private boolean isValidGeometryPath(String path) { - return path.equals("") || path.equals("geometry") || path.equals("features.[].geometry"); - } - - private Polygon parsePolygon(List coordinates) throws ParseException { - List holes = new ArrayList<>(); - Object o = coordinates.get(0); - if (o instanceof List == false) { - throw newParseException("first element of polygon array must be an array [[lat, lon], [lat, lon] ...] but got: " + o); - } - double[][] polyPoints = parsePoints((List) o); - for(int i=1;i) o); - holes.add(new Polygon(holePoints[0], holePoints[1])); - } - return new Polygon(polyPoints[0], polyPoints[1], holes.toArray(new Polygon[holes.size()])); - } - - /** Parses [[lat, lon], [lat, lon] ...] into 2d double array */ - private double[][] parsePoints(List o) throws ParseException { - double[] lats = new double[o.size()]; - double[] lons = new double[o.size()]; - for(int i=0;i pointList = (List) point; - if (pointList.size() != 2) { - throw newParseException("elements of coordinates array must [lat, lon] array, but got wrong element count: " + pointList); - } - if (pointList.get(0) instanceof Double == false) { - throw newParseException("elements of coordinates array must [lat, lon] array, but first element is not a Double: " + pointList.get(0)); - } - if (pointList.get(1) instanceof Double == false) { - throw newParseException("elements of coordinates array must [lat, lon] array, but second element is not a Double: " + pointList.get(1)); - } - - // lon, lat ordering in GeoJSON! - lons[i] = ((Double) pointList.get(0)).doubleValue(); - lats[i] = ((Double) pointList.get(1)).doubleValue(); - } - - return new double[][] {lats, lons}; - } - - private List parseArray(String path) throws ParseException { - List result = new ArrayList<>(); - scan('['); - while (upto < input.length()) { - char ch = peek(); - if (ch == ']') { - scan(']'); - return result; - } - - if (result.size() > 0) { - if (ch != ',') { - throw newParseException("expected ',' separating list items, but got '" + ch + "'"); - } - - // skip the , - upto++; - - if (upto == input.length()) { - throw newParseException("hit EOF while parsing array"); - } - ch = peek(); - } - - Object o; - if (ch == '[') { - o = parseArray(path + ".[]"); - } else if (ch == '{') { - // This is only used when parsing the "features" in type: FeatureCollection - parseObject(path + ".[]"); - o = null; - } else if (ch == '-' || ch == '.' || (ch >= '0' && ch <= '9')) { - o = parseNumber(); - } else { - throw newParseException("expected another array or number while parsing array, not '" + ch + "'"); - } - - result.add(o); - } - - throw newParseException("hit EOF while reading array"); - } - - private Number parseNumber() throws ParseException { - StringBuilder b = new StringBuilder(); - int uptoStart = upto; - while (upto < input.length()) { - char ch = input.charAt(upto); - if (ch == '-' || ch == '.' || (ch >= '0' && ch <= '9') || ch == 'e' || ch == 'E') { - upto++; - b.append(ch); - } else { - break; - } - } - - // we only handle doubles - try { - return Double.parseDouble(b.toString()); - } catch (NumberFormatException nfe) { - upto = uptoStart; - throw newParseException("could not parse number as double"); - } - } - - private String parseString() throws ParseException { - scan('"'); - StringBuilder b = new StringBuilder(); - while (upto < input.length()) { - char ch = input.charAt(upto); - if (ch == '"') { - upto++; - return b.toString(); - } - if (ch == '\\') { - // an escaped character - upto++; - if (upto == input.length()) { - throw newParseException("hit EOF inside string literal"); - } - ch = input.charAt(upto); - if (ch == 'u') { - // 4 hex digit unicode BMP escape - upto++; - if (upto + 4 > input.length()) { - throw newParseException("hit EOF inside string literal"); - } - b.append(Integer.parseInt(input.substring(upto, upto+4), 16)); - } else if (ch == '\\') { - b.append('\\'); - upto++; - } else { - // TODO: allow \n, \t, etc.??? - throw newParseException("unsupported string escape character \\" + ch); - } - } else { - b.append(ch); - upto++; - } - } - - throw newParseException("hit EOF inside string literal"); - } - - private char peek() throws ParseException { - while (upto < input.length()) { - char ch = input.charAt(upto); - if (isJSONWhitespace(ch)) { - upto++; - continue; - } - return ch; - } - - throw newParseException("unexpected EOF"); - } - - /** Scans across whitespace and consumes the expected character, or throws {@code ParseException} if the character is wrong */ - private void scan(char expected) throws ParseException { - while (upto < input.length()) { - char ch = input.charAt(upto); - if (isJSONWhitespace(ch)) { - upto++; - continue; - } - if (ch != expected) { - throw newParseException("expected '" + expected + "' but got '" + ch + "'"); - } - upto++; - return; - } - throw newParseException("expected '" + expected + "' but got EOF"); - } - - private void readEnd() throws ParseException { - while (upto < input.length()) { - char ch = input.charAt(upto); - if (isJSONWhitespace(ch) == false) { - throw newParseException("unexpected character '" + ch + "' after end of GeoJSON object"); - } - upto++; - } - } - - /** Scans the expected string, or throws {@code ParseException} */ - private void scan(String expected) throws ParseException { - if (upto + expected.length() > input.length()) { - throw newParseException("expected \"" + expected + "\" but hit EOF"); - } - String subString = input.substring(upto, upto+expected.length()); - if (subString.equals(expected) == false) { - throw newParseException("expected \"" + expected + "\" but got \"" + subString + "\""); - } - upto += expected.length(); - } - - private static boolean isJSONWhitespace(char ch) { - // JSON doesn't accept allow unicode whitespace? - return ch == 0x20 || // space - ch == 0x09 || // tab - ch == 0x0a || // line feed - ch == 0x0d; // newline - } - - /** When calling this, upto should be at the position of the incorrect character! */ - private ParseException newParseException(String details) throws ParseException { - String fragment; - int end = Math.min(input.length(), upto+1); - if (upto < 50) { - fragment = input.substring(0, end); - } else { - fragment = "..." + input.substring(upto-50, end); - } - return new ParseException(details + " at character offset " + upto + "; fragment leading to this:\n" + fragment, upto); - } -} diff --git a/libs/geo/src/main/java/org/elasticsearch/geo/parsers/WKBParser.java b/libs/geo/src/main/java/org/elasticsearch/geo/parsers/WKBParser.java deleted file mode 100644 index 946df5a2d2b84..0000000000000 --- a/libs/geo/src/main/java/org/elasticsearch/geo/parsers/WKBParser.java +++ /dev/null @@ -1,49 +0,0 @@ -/* - * 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.geo.parsers; - -import java.io.ByteArrayOutputStream; -import java.io.IOException; - -import org.elasticsearch.geo.geometry.GeoShape; -import org.apache.lucene.store.OutputStreamDataOutput; -import org.apache.lucene.util.BytesRef; - -public class WKBParser { - - // no instance: - private WKBParser() { - } - - public enum ByteOrder { - XDR, // big endian - NDR; // little endian - - public BytesRef toWKB() { - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - try (OutputStreamDataOutput out = new OutputStreamDataOutput(baos)) { - out.writeVInt(this.ordinal()); - } catch (IOException e) { - throw new RuntimeException(e); // not possible - } - return new BytesRef(baos.toByteArray()); - } - } -} diff --git a/libs/geo/src/main/java/org/elasticsearch/geo/parsers/WKTParser.java b/libs/geo/src/main/java/org/elasticsearch/geo/parsers/WKTParser.java deleted file mode 100644 index 667965d9954bb..0000000000000 --- a/libs/geo/src/main/java/org/elasticsearch/geo/parsers/WKTParser.java +++ /dev/null @@ -1,326 +0,0 @@ -/* - * 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.geo.parsers; - -import java.io.IOException; -import java.io.StreamTokenizer; -import java.io.StringReader; -import java.text.ParseException; -import java.util.ArrayList; - -import org.elasticsearch.geo.geometry.Point; -import org.elasticsearch.geo.geometry.GeoShape; -import org.elasticsearch.geo.geometry.Rectangle; -import org.elasticsearch.geo.geometry.ShapeType; -import org.elasticsearch.geo.geometry.Line; -import org.elasticsearch.geo.geometry.MultiLine; -import org.elasticsearch.geo.geometry.MultiPoint; -import org.elasticsearch.geo.geometry.MultiPolygon; -import org.elasticsearch.geo.geometry.Polygon; - -/** - * Created by nknize on 3/12/17. - */ -public class WKTParser { - public static final String EMPTY = "EMPTY"; - public static final String SPACE = " "; - public static final String LPAREN = "("; - public static final String RPAREN = ")"; - public static final String COMMA = ","; - public static final String NAN = "NaN"; - - private static final String NUMBER = ""; - private static final String EOF = "END-OF-STREAM"; - private static final String EOL = "END-OF-LINE"; - - // no instance - private WKTParser() { - } - - public static GeoShape parse(String wkt) throws IOException, ParseException { - StringReader reader = new StringReader(wkt); - try { - // setup the tokenizer; configured to read words w/o numbers - StreamTokenizer tokenizer = new StreamTokenizer(reader); - tokenizer.resetSyntax(); - tokenizer.wordChars('a', 'z'); - tokenizer.wordChars('A', 'Z'); - tokenizer.wordChars(128 + 32, 255); - tokenizer.wordChars('0', '9'); - tokenizer.wordChars('-', '-'); - tokenizer.wordChars('+', '+'); - tokenizer.wordChars('.', '.'); - tokenizer.whitespaceChars(0, ' '); - tokenizer.commentChar('#'); - return parseGeometry(tokenizer); - } finally { - reader.close(); - } - } - - /** - * parse geometry from the stream tokenizer - */ - private static GeoShape parseGeometry(StreamTokenizer stream) throws IOException, ParseException { - final ShapeType type = ShapeType.forName(nextWord(stream)); - switch (type) { - case POINT: - return parsePoint(stream); - case MULTIPOINT: - return parseMultiPoint(stream); - case LINESTRING: - return parseLine(stream); - case MULTILINESTRING: - return parseMultiLine(stream); - case POLYGON: - return parsePolygon(stream); - case MULTIPOLYGON: - return parseMultiPolygon(stream); - case ENVELOPE: - return parseBBox(stream); - } - throw new IllegalArgumentException("Unknown geometry type: " + type); - } - - private static Point parsePoint(StreamTokenizer stream) throws IOException, ParseException { - if (nextEmptyOrOpen(stream).equals(EMPTY)) { - return new Point(0, 0); - } - Point pt = new Point(nextNumber(stream), nextNumber(stream)); - if (isNumberNext(stream) == true) { - nextNumber(stream); - } - nextCloser(stream); - return pt; - } - - private static void parseCoordinates(StreamTokenizer stream, ArrayList lats, ArrayList lons) - throws IOException, ParseException { - parseCoordinate(stream, lats, lons); - while (nextCloserOrComma(stream).equals(COMMA)) { - parseCoordinate(stream, lats, lons); - } - } - - private static void parseCoordinate(StreamTokenizer stream, ArrayList lats, ArrayList lons) - throws IOException, ParseException { - lats.add(nextNumber(stream)); - lons.add(nextNumber(stream)); - if (isNumberNext(stream)) { - nextNumber(stream); - } - } - - private static MultiPoint parseMultiPoint(StreamTokenizer stream) throws IOException, ParseException { - String token = nextEmptyOrOpen(stream); - if (token.equals(EMPTY)) { - return new MultiPoint(new double[]{0.0}, new double[]{0.0}); - } - ArrayList lats = new ArrayList(); - ArrayList lons = new ArrayList(); - parseCoordinates(stream, lats, lons); - return new MultiPoint(lats.stream().mapToDouble(i -> i).toArray(), lons.stream().mapToDouble(i -> i).toArray()); - } - - private static Line parseLine(StreamTokenizer stream) throws IOException, ParseException { - String token = nextEmptyOrOpen(stream); - if (token.equals(EMPTY)) { - return new Line(new double[]{0.0}, new double[]{0.0}); - } - ArrayList lats = new ArrayList(); - ArrayList lons = new ArrayList(); - parseCoordinates(stream, lats, lons); - return new Line(lats.stream().mapToDouble(i -> i).toArray(), lons.stream().mapToDouble(i -> i).toArray()); - } - - private static MultiLine parseMultiLine(StreamTokenizer stream) throws IOException, ParseException { - String token = nextEmptyOrOpen(stream); - if (token.equals(EMPTY)) { - return new MultiLine(null); - } - ArrayList lines = new ArrayList(); - lines.add(parseLine(stream)); - while (nextCloserOrComma(stream).equals(COMMA)) { - lines.add(parseLine(stream)); - } - Line[] l = lines.toArray(new Line[lines.size()]); - return new MultiLine(l); - } - - private static Polygon parsePolygonHole(StreamTokenizer stream) throws IOException, ParseException { - ArrayList lats = new ArrayList(); - ArrayList lons = new ArrayList(); - parseCoordinates(stream, lats, lons); - return new Polygon(lats.stream().mapToDouble(i -> i).toArray(), lons.stream().mapToDouble(i -> i).toArray()); - } - - private static Polygon parsePolygon(StreamTokenizer stream) throws IOException, ParseException { - if (nextEmptyOrOpen(stream).equals(EMPTY)) { - return new Polygon(new double[0], new double[0]); - } - nextOpener(stream); - ArrayList lats = new ArrayList(); - ArrayList lons = new ArrayList(); - parseCoordinates(stream, lats, lons); - ArrayList holes = null; - if (nextWord(stream).equals(LPAREN)) { - while (nextCloserOrComma(stream).equals(COMMA)) { - holes.add(parsePolygonHole(stream)); - } - } - if (holes != null) { - Polygon[] h = null; - holes.toArray(new Polygon[holes.size()]); - return new Polygon(lats.stream().mapToDouble(i -> i).toArray(), lons.stream().mapToDouble(i -> i).toArray(), h); - } - return new Polygon(lats.stream().mapToDouble(i -> i).toArray(), lons.stream().mapToDouble(i -> i).toArray()); - } - - private static MultiPolygon parseMultiPolygon(StreamTokenizer stream) throws IOException, ParseException { - String token = nextEmptyOrOpen(stream); - if (token.equals(EMPTY)) { - return new MultiPolygon(null); - } - ArrayList polygons = new ArrayList(); - polygons.add(parsePolygon(stream)); - while (nextCloserOrComma(stream).equals(COMMA)) { - polygons.add(parsePolygon(stream)); - } - Polygon[] p = polygons.toArray(new Polygon[polygons.size()]); - return new MultiPolygon(p); - } - - private static Rectangle parseBBox(StreamTokenizer stream) throws IOException, ParseException { - if (nextEmptyOrOpen(stream).equals(EMPTY)) { - return null; - } - double minLon = nextNumber(stream); - nextComma(stream); - double maxLon = nextNumber(stream); - nextComma(stream); - double maxLat = nextNumber(stream); - nextComma(stream); - double minLat = nextNumber(stream); - nextCloser(stream); - return new Rectangle(minLat, maxLat, minLon, maxLon); - } - - /** - * next word in the stream - */ - private static String nextWord(StreamTokenizer stream) throws ParseException, IOException { - switch (stream.nextToken()) { - case StreamTokenizer.TT_WORD: - final String word = stream.sval; - return word.equalsIgnoreCase(EMPTY) ? EMPTY : word; - case '(': - return LPAREN; - case ')': - return RPAREN; - case ',': - return COMMA; - } - throw new ParseException("expected word but found: " + tokenString(stream), stream.lineno()); - } - - private static double nextNumber(StreamTokenizer stream) throws IOException, ParseException { - if (stream.nextToken() == StreamTokenizer.TT_WORD) { - if (stream.sval.equalsIgnoreCase(NAN)) { - return Double.NaN; - } else { - try { - return Double.parseDouble(stream.sval); - } catch (NumberFormatException e) { - throw new ParseException("invalid number found: " + stream.sval, stream.lineno()); - } - } - } - throw new ParseException("expected number but found: " + tokenString(stream), stream.lineno()); - } - - private static String tokenString(StreamTokenizer stream) { - switch (stream.ttype) { - case StreamTokenizer.TT_WORD: - return stream.sval; - case StreamTokenizer.TT_EOF: - return EOF; - case StreamTokenizer.TT_EOL: - return EOL; - case StreamTokenizer.TT_NUMBER: - return NUMBER; - } - return "'" + (char) stream.ttype + "'"; - } - - private static boolean isNumberNext(StreamTokenizer stream) throws IOException { - final int type = stream.nextToken(); - stream.pushBack(); - return type == StreamTokenizer.TT_WORD; - } - - private static String nextEmptyOrOpen(StreamTokenizer stream) throws IOException, ParseException { - final String next = nextWord(stream); - if (next.equals(EMPTY) || next.equals(LPAREN)) { - return next; - } - throw new ParseException("expected " + EMPTY + " or " + LPAREN - + " but found: " + tokenString(stream), stream.lineno()); - } - - private static String nextCloser(StreamTokenizer stream) throws IOException, ParseException { - if (nextWord(stream).equals(RPAREN)) { - return RPAREN; - } - throw new ParseException("expected " + RPAREN + " but found: " + tokenString(stream), stream.lineno()); - } - - private static String nextComma(StreamTokenizer stream) throws IOException, ParseException { - if (nextWord(stream).equals(COMMA) == true) { - return COMMA; - } - throw new ParseException("expected " + COMMA + " but found: " + tokenString(stream), stream.lineno()); - } - - private static String nextOpener(StreamTokenizer stream) throws IOException, ParseException { - if (nextWord(stream).equals(LPAREN)) { - return LPAREN; - } - throw new ParseException("expected " + LPAREN + " but found: " + tokenString(stream), stream.lineno()); - } - - private static String nextCloserOrComma(StreamTokenizer stream) throws IOException, ParseException { - String token = nextWord(stream); - if (token.equals(COMMA) || token.equals(RPAREN)) { - return token; - } - throw new ParseException("expected " + COMMA + " or " + RPAREN - + " but found: " + tokenString(stream), stream.lineno()); - } - - public static void main(String[] args) { - try { - String wkt = "MULTIPOLYGON (((10 40, 40 30, 20 20, 30 10, 10 40)))"; - GeoShape shape = WKTParser.parse(wkt); - assert shape instanceof Point; - } catch (Exception e) { - e.printStackTrace(); - } - } -} diff --git a/libs/geo/src/main/java/org/elasticsearch/geo/utils/WellKnownText.java b/libs/geo/src/main/java/org/elasticsearch/geo/utils/WellKnownText.java new file mode 100644 index 0000000000000..c80fdb571e90b --- /dev/null +++ b/libs/geo/src/main/java/org/elasticsearch/geo/utils/WellKnownText.java @@ -0,0 +1,557 @@ +/* + * 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.geo.utils; + +import org.elasticsearch.geo.geometry.Circle; +import org.elasticsearch.geo.geometry.Geometry; +import org.elasticsearch.geo.geometry.GeometryCollection; +import org.elasticsearch.geo.geometry.GeometryVisitor; +import org.elasticsearch.geo.geometry.Line; +import org.elasticsearch.geo.geometry.LinearRing; +import org.elasticsearch.geo.geometry.MultiLine; +import org.elasticsearch.geo.geometry.MultiPoint; +import org.elasticsearch.geo.geometry.MultiPolygon; +import org.elasticsearch.geo.geometry.Point; +import org.elasticsearch.geo.geometry.Polygon; +import org.elasticsearch.geo.geometry.Rectangle; + +import java.io.IOException; +import java.io.StreamTokenizer; +import java.io.StringReader; +import java.text.ParseException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Locale; + +/** + * Utility class for converting to and from WKT + */ +public class WellKnownText { + public static final String EMPTY = "EMPTY"; + public static final String SPACE = " "; + public static final String LPAREN = "("; + public static final String RPAREN = ")"; + public static final String COMMA = ","; + public static final String NAN = "NaN"; + + private static final String NUMBER = ""; + private static final String EOF = "END-OF-STREAM"; + private static final String EOL = "END-OF-LINE"; + + public static String toWKT(Geometry geometry) { + StringBuilder builder = new StringBuilder(); + toWKT(geometry, builder); + return builder.toString(); + } + + public static void toWKT(Geometry geometry, StringBuilder sb) { + sb.append(getWKTName(geometry)); + sb.append(SPACE); + if (geometry.isEmpty()) { + sb.append(EMPTY); + } else { + geometry.visit(new GeometryVisitor() { + @Override + public Void visit(Circle circle) { + sb.append(LPAREN); + visitPoint(circle.getLon(), circle.getLat()); + sb.append(SPACE); + sb.append(circle.getRadiusMeters()); + sb.append(RPAREN); + return null; + } + + @Override + public Void visit(GeometryCollection collection) { + if (collection.size() == 0) { + sb.append(EMPTY); + } else { + sb.append(LPAREN); + toWKT(collection.get(0), sb); + for (int i = 1; i < collection.size(); ++i) { + sb.append(COMMA); + toWKT(collection.get(i), sb); + } + sb.append(RPAREN); + } + return null; + } + + @Override + public Void visit(Line line) { + sb.append(LPAREN); + visitPoint(line.getLon(0), line.getLat(0)); + for (int i = 1; i < line.length(); ++i) { + sb.append(COMMA); + sb.append(SPACE); + visitPoint(line.getLon(i), line.getLat(i)); + } + sb.append(RPAREN); + return null; + } + + @Override + public Void visit(LinearRing ring) { + throw new IllegalArgumentException("Linear ring is not supported by WKT"); + } + + @Override + public Void visit(MultiLine multiLine) { + visitCollection(multiLine); + return null; + } + + @Override + public Void visit(MultiPoint multiPoint) { + // walk through coordinates: + sb.append(LPAREN); + visitPoint(multiPoint.get(0).lon(), multiPoint.get(0).lat()); + for (int i = 1; i < multiPoint.size(); ++i) { + sb.append(COMMA); + sb.append(SPACE); + Point point = multiPoint.get(i); + visitPoint(point.lon(), point.lat()); + } + sb.append(RPAREN); + return null; + } + + @Override + public Void visit(MultiPolygon multiPolygon) { + visitCollection(multiPolygon); + return null; + } + + @Override + public Void visit(Point point) { + if (point.isEmpty()) { + sb.append(EMPTY); + } else { + sb.append(LPAREN); + visitPoint(point.lon(), point.lat()); + sb.append(RPAREN); + } + return null; + } + + private void visitPoint(double lon, double lat) { + sb.append(lon).append(SPACE).append(lat); + } + + private void visitCollection(GeometryCollection collection) { + if (collection.size() == 0) { + sb.append(EMPTY); + } else { + sb.append(LPAREN); + collection.get(0).visit(this); + for (int i = 1; i < collection.size(); ++i) { + sb.append(COMMA); + collection.get(i).visit(this); + } + sb.append(RPAREN); + } + } + + @Override + public Void visit(Polygon polygon) { + sb.append(LPAREN); + visit((Line) polygon.getPolygon()); + int numberOfHoles = polygon.getNumberOfHoles(); + for (int i = 0; i < numberOfHoles; ++i) { + sb.append(", "); + visit((Line) polygon.getHole(i)); + } + sb.append(RPAREN); + return null; + } + + @Override + public Void visit(Rectangle rectangle) { + sb.append(LPAREN); + // minX, maxX, maxY, minY + sb.append(rectangle.getMinLon()); + sb.append(COMMA); + sb.append(SPACE); + sb.append(rectangle.getMaxLon()); + sb.append(COMMA); + sb.append(SPACE); + sb.append(rectangle.getMaxLat()); + sb.append(COMMA); + sb.append(SPACE); + sb.append(rectangle.getMinLat()); + sb.append(RPAREN); + return null; + } + }); + } + } + + public static Geometry fromWKT(String wkt) throws IOException, ParseException { + StringReader reader = new StringReader(wkt); + try { + // setup the tokenizer; configured to read words w/o numbers + StreamTokenizer tokenizer = new StreamTokenizer(reader); + tokenizer.resetSyntax(); + tokenizer.wordChars('a', 'z'); + tokenizer.wordChars('A', 'Z'); + tokenizer.wordChars(128 + 32, 255); + tokenizer.wordChars('0', '9'); + tokenizer.wordChars('-', '-'); + tokenizer.wordChars('+', '+'); + tokenizer.wordChars('.', '.'); + tokenizer.whitespaceChars(0, ' '); + tokenizer.commentChar('#'); + return parseGeometry(tokenizer); + } finally { + reader.close(); + } + } + + /** + * parse geometry from the stream tokenizer + */ + private static Geometry parseGeometry(StreamTokenizer stream) throws IOException, ParseException { + final String type = nextWord(stream).toLowerCase(Locale.ROOT); + switch (type) { + case "point": + return parsePoint(stream); + case "multipoint": + return parseMultiPoint(stream); + case "linestring": + return parseLine(stream); + case "multilinestring": + return parseMultiLine(stream); + case "polygon": + return parsePolygon(stream); + case "multipolygon": + return parseMultiPolygon(stream); + case "bbox": + return parseBBox(stream); + case "geometrycollection": + return parseGeometryCollection(stream); + case "circle": // Not part of the standard, but we need it for internal serialization + return parseCircle(stream); + } + throw new IllegalArgumentException("Unknown geometry type: " + type); + } + + private static GeometryCollection parseGeometryCollection(StreamTokenizer stream) throws IOException, ParseException { + if (nextEmptyOrOpen(stream).equals(EMPTY)) { + return GeometryCollection.EMPTY; + } + List shapes = new ArrayList<>(); + shapes.add(parseGeometry(stream)); + while (nextCloserOrComma(stream).equals(COMMA)) { + shapes.add(parseGeometry(stream)); + } + return new GeometryCollection<>(shapes); + } + + private static Point parsePoint(StreamTokenizer stream) throws IOException, ParseException { + if (nextEmptyOrOpen(stream).equals(EMPTY)) { + return Point.EMPTY; + } + double lon = nextNumber(stream); + double lat = nextNumber(stream); + Point pt = new Point(lat, lon); + if (isNumberNext(stream) == true) { + nextNumber(stream); + } + nextCloser(stream); + return pt; + } + + private static void parseCoordinates(StreamTokenizer stream, ArrayList lats, ArrayList lons) + throws IOException, ParseException { + parseCoordinate(stream, lats, lons); + while (nextCloserOrComma(stream).equals(COMMA)) { + parseCoordinate(stream, lats, lons); + } + } + + private static void parseCoordinate(StreamTokenizer stream, ArrayList lats, ArrayList lons) + throws IOException, ParseException { + lons.add(nextNumber(stream)); + lats.add(nextNumber(stream)); + if (isNumberNext(stream)) { + nextNumber(stream); + } + } + + private static MultiPoint parseMultiPoint(StreamTokenizer stream) throws IOException, ParseException { + String token = nextEmptyOrOpen(stream); + if (token.equals(EMPTY)) { + return MultiPoint.EMPTY; + } + ArrayList lats = new ArrayList<>(); + ArrayList lons = new ArrayList<>(); + ArrayList points = new ArrayList<>(); + parseCoordinates(stream, lats, lons); + for (int i = 0; i < lats.size(); i++) { + points.add(new Point(lats.get(i), lons.get(i))); + } + return new MultiPoint(Collections.unmodifiableList(points)); + } + + private static Line parseLine(StreamTokenizer stream) throws IOException, ParseException { + String token = nextEmptyOrOpen(stream); + if (token.equals(EMPTY)) { + return Line.EMPTY; + } + ArrayList lats = new ArrayList<>(); + ArrayList lons = new ArrayList<>(); + parseCoordinates(stream, lats, lons); + return new Line(lats.stream().mapToDouble(i -> i).toArray(), lons.stream().mapToDouble(i -> i).toArray()); + } + + private static MultiLine parseMultiLine(StreamTokenizer stream) throws IOException, ParseException { + String token = nextEmptyOrOpen(stream); + if (token.equals(EMPTY)) { + return MultiLine.EMPTY; + } + ArrayList lines = new ArrayList<>(); + lines.add(parseLine(stream)); + while (nextCloserOrComma(stream).equals(COMMA)) { + lines.add(parseLine(stream)); + } + return new MultiLine(Collections.unmodifiableList(lines)); + } + + private static LinearRing parsePolygonHole(StreamTokenizer stream) throws IOException, ParseException { + nextOpener(stream); + ArrayList lats = new ArrayList<>(); + ArrayList lons = new ArrayList<>(); + parseCoordinates(stream, lats, lons); + return new LinearRing(lats.stream().mapToDouble(i -> i).toArray(), lons.stream().mapToDouble(i -> i).toArray()); + } + + private static Polygon parsePolygon(StreamTokenizer stream) throws IOException, ParseException { + if (nextEmptyOrOpen(stream).equals(EMPTY)) { + return Polygon.EMPTY; + } + nextOpener(stream); + ArrayList lats = new ArrayList<>(); + ArrayList lons = new ArrayList<>(); + parseCoordinates(stream, lats, lons); + ArrayList holes = new ArrayList<>(); + while (nextCloserOrComma(stream).equals(COMMA)) { + holes.add(parsePolygonHole(stream)); + } + if (holes.isEmpty()) { + return new Polygon(new LinearRing(lats.stream().mapToDouble(i -> i).toArray(), lons.stream().mapToDouble(i -> i).toArray())); + } else { + return new Polygon( + new LinearRing(lats.stream().mapToDouble(i -> i).toArray(), lons.stream().mapToDouble(i -> i).toArray()), + Collections.unmodifiableList(holes)); + } + } + + private static MultiPolygon parseMultiPolygon(StreamTokenizer stream) throws IOException, ParseException { + String token = nextEmptyOrOpen(stream); + if (token.equals(EMPTY)) { + return MultiPolygon.EMPTY; + } + ArrayList polygons = new ArrayList<>(); + polygons.add(parsePolygon(stream)); + while (nextCloserOrComma(stream).equals(COMMA)) { + polygons.add(parsePolygon(stream)); + } + return new MultiPolygon(Collections.unmodifiableList(polygons)); + } + + private static Rectangle parseBBox(StreamTokenizer stream) throws IOException, ParseException { + if (nextEmptyOrOpen(stream).equals(EMPTY)) { + return Rectangle.EMPTY; + } + double minLon = nextNumber(stream); + nextComma(stream); + double maxLon = nextNumber(stream); + nextComma(stream); + double maxLat = nextNumber(stream); + nextComma(stream); + double minLat = nextNumber(stream); + nextCloser(stream); + return new Rectangle(minLat, maxLat, minLon, maxLon); + } + + + private static Circle parseCircle(StreamTokenizer stream) throws IOException, ParseException { + if (nextEmptyOrOpen(stream).equals(EMPTY)) { + return Circle.EMPTY; + } + double lon = nextNumber(stream); + double lat = nextNumber(stream); + double radius = nextNumber(stream); + Circle circle = new Circle(lat, lon, radius); + if (isNumberNext(stream) == true) { + nextNumber(stream); + } + nextCloser(stream); + return circle; + } + + /** + * next word in the stream + */ + private static String nextWord(StreamTokenizer stream) throws ParseException, IOException { + switch (stream.nextToken()) { + case StreamTokenizer.TT_WORD: + final String word = stream.sval; + return word.equalsIgnoreCase(EMPTY) ? EMPTY : word; + case '(': + return LPAREN; + case ')': + return RPAREN; + case ',': + return COMMA; + } + throw new ParseException("expected word but found: " + tokenString(stream), stream.lineno()); + } + + private static double nextNumber(StreamTokenizer stream) throws IOException, ParseException { + if (stream.nextToken() == StreamTokenizer.TT_WORD) { + if (stream.sval.equalsIgnoreCase(NAN)) { + return Double.NaN; + } else { + try { + return Double.parseDouble(stream.sval); + } catch (NumberFormatException e) { + throw new ParseException("invalid number found: " + stream.sval, stream.lineno()); + } + } + } + throw new ParseException("expected number but found: " + tokenString(stream), stream.lineno()); + } + + private static String tokenString(StreamTokenizer stream) { + switch (stream.ttype) { + case StreamTokenizer.TT_WORD: + return stream.sval; + case StreamTokenizer.TT_EOF: + return EOF; + case StreamTokenizer.TT_EOL: + return EOL; + case StreamTokenizer.TT_NUMBER: + return NUMBER; + } + return "'" + (char) stream.ttype + "'"; + } + + private static boolean isNumberNext(StreamTokenizer stream) throws IOException { + final int type = stream.nextToken(); + stream.pushBack(); + return type == StreamTokenizer.TT_WORD; + } + + private static String nextEmptyOrOpen(StreamTokenizer stream) throws IOException, ParseException { + final String next = nextWord(stream); + if (next.equals(EMPTY) || next.equals(LPAREN)) { + return next; + } + throw new ParseException("expected " + EMPTY + " or " + LPAREN + + " but found: " + tokenString(stream), stream.lineno()); + } + + private static String nextCloser(StreamTokenizer stream) throws IOException, ParseException { + if (nextWord(stream).equals(RPAREN)) { + return RPAREN; + } + throw new ParseException("expected " + RPAREN + " but found: " + tokenString(stream), stream.lineno()); + } + + private static String nextComma(StreamTokenizer stream) throws IOException, ParseException { + if (nextWord(stream).equals(COMMA) == true) { + return COMMA; + } + throw new ParseException("expected " + COMMA + " but found: " + tokenString(stream), stream.lineno()); + } + + private static String nextOpener(StreamTokenizer stream) throws IOException, ParseException { + if (nextWord(stream).equals(LPAREN)) { + return LPAREN; + } + throw new ParseException("expected " + LPAREN + " but found: " + tokenString(stream), stream.lineno()); + } + + private static String nextCloserOrComma(StreamTokenizer stream) throws IOException, ParseException { + String token = nextWord(stream); + if (token.equals(COMMA) || token.equals(RPAREN)) { + return token; + } + throw new ParseException("expected " + COMMA + " or " + RPAREN + + " but found: " + tokenString(stream), stream.lineno()); + } + + public static String getWKTName(Geometry geometry) { + return geometry.visit(new GeometryVisitor() { + @Override + public String visit(Circle circle) { + return "circle"; + } + + @Override + public String visit(GeometryCollection collection) { + return "geometrycollection"; + } + + @Override + public String visit(Line line) { + return "linestring"; + } + + @Override + public String visit(LinearRing ring) { + throw new UnsupportedOperationException("line ring cannot be serialized using WKT"); + } + + @Override + public String visit(MultiLine multiLine) { + return "multilinestring"; + } + + @Override + public String visit(MultiPoint multiPoint) { + return "multipoint"; + } + + @Override + public String visit(MultiPolygon multiPolygon) { + return "multipolygon"; + } + + @Override + public String visit(Point point) { + return "point"; + } + + @Override + public String visit(Polygon polygon) { + return "polygon"; + } + + @Override + public String visit(Rectangle rectangle) { + return "bbox"; + } + }); + } + +} diff --git a/libs/geo/src/test/java/org/elasticsearch/geo/TestGeoEncodingUtils.java b/libs/geo/src/test/java/org/elasticsearch/geo/TestGeoEncodingUtils.java deleted file mode 100644 index be3df4418c67d..0000000000000 --- a/libs/geo/src/test/java/org/elasticsearch/geo/TestGeoEncodingUtils.java +++ /dev/null @@ -1,155 +0,0 @@ -/* - * 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.geo; - -import org.apache.lucene.geo.GeoEncodingUtils; -import org.apache.lucene.util.LuceneTestCase; -import org.apache.lucene.util.NumericUtils; -import org.apache.lucene.util.TestUtil; - -import java.util.Random; - -import static org.apache.lucene.geo.GeoEncodingUtils.decodeLatitude; -import static org.apache.lucene.geo.GeoEncodingUtils.decodeLongitude; -import static org.apache.lucene.geo.GeoEncodingUtils.encodeLatitude; -import static org.apache.lucene.geo.GeoEncodingUtils.encodeLatitudeCeil; -import static org.apache.lucene.geo.GeoEncodingUtils.encodeLongitude; -import static org.apache.lucene.geo.GeoEncodingUtils.encodeLongitudeCeil; -import static org.apache.lucene.geo.GeoUtils.MAX_LAT_INCL; -import static org.apache.lucene.geo.GeoUtils.MAX_LON_INCL; -import static org.apache.lucene.geo.GeoUtils.MIN_LAT_INCL; -import static org.apache.lucene.geo.GeoUtils.MIN_LON_INCL; - -/** - * Tests methods in {@link GeoEncodingUtils} - */ -public class TestGeoEncodingUtils extends LuceneTestCase { - - /** - * step through some integers, ensuring they decode to their expected double values. - * double values start at -90 and increase by LATITUDE_DECODE for each integer. - * check edge cases within the double range and random doubles within the range too. - */ - public void testLatitudeQuantization() throws Exception { - final double LATITUDE_DECODE = 180.0D / (0x1L << 32); - Random random = random(); - for (int i = 0; i < 10000; i++) { - int encoded = random.nextInt(); - double min = MIN_LAT_INCL + (encoded - (long) Integer.MIN_VALUE) * LATITUDE_DECODE; - double decoded = decodeLatitude(encoded); - // should exactly equal expected value - assertEquals(min, decoded, 0.0D); - // should round-trip - assertEquals(encoded, encodeLatitude(decoded)); - assertEquals(encoded, encodeLatitudeCeil(decoded)); - // test within the range - if (encoded != Integer.MAX_VALUE) { - // this is the next representable value - // all double values between [min .. max) should encode to the current integer - // all double values between (min .. max] should encodeCeil to the next integer. - double max = min + LATITUDE_DECODE; - assertEquals(max, decodeLatitude(encoded + 1), 0.0D); - assertEquals(encoded + 1, encodeLatitude(max)); - assertEquals(encoded + 1, encodeLatitudeCeil(max)); - - // first and last doubles in range that will be quantized - double minEdge = Math.nextUp(min); - double maxEdge = Math.nextDown(max); - assertEquals(encoded, encodeLatitude(minEdge)); - assertEquals(encoded + 1, encodeLatitudeCeil(minEdge)); - assertEquals(encoded, encodeLatitude(maxEdge)); - assertEquals(encoded + 1, encodeLatitudeCeil(maxEdge)); - - // check random values within the double range - long minBits = NumericUtils.doubleToSortableLong(minEdge); - long maxBits = NumericUtils.doubleToSortableLong(maxEdge); - for (int j = 0; j < 100; j++) { - double value = NumericUtils.sortableLongToDouble(TestUtil.nextLong(random, minBits, maxBits)); - // round down - assertEquals(encoded, encodeLatitude(value)); - // round up - assertEquals(encoded + 1, encodeLatitudeCeil(value)); - } - } - } - } - - /** - * step through some integers, ensuring they decode to their expected double values. - * double values start at -180 and increase by LONGITUDE_DECODE for each integer. - * check edge cases within the double range and a random doubles within the range too. - */ - public void testLongitudeQuantization() throws Exception { - final double LONGITUDE_DECODE = 360.0D / (0x1L << 32); - Random random = random(); - for (int i = 0; i < 10000; i++) { - int encoded = random.nextInt(); - double min = MIN_LON_INCL + (encoded - (long) Integer.MIN_VALUE) * LONGITUDE_DECODE; - double decoded = decodeLongitude(encoded); - // should exactly equal expected value - assertEquals(min, decoded, 0.0D); - // should round-trip - assertEquals(encoded, encodeLongitude(decoded)); - assertEquals(encoded, encodeLongitudeCeil(decoded)); - // test within the range - if (encoded != Integer.MAX_VALUE) { - // this is the next representable value - // all double values between [min .. max) should encode to the current integer - // all double values between (min .. max] should encodeCeil to the next integer. - double max = min + LONGITUDE_DECODE; - assertEquals(max, decodeLongitude(encoded + 1), 0.0D); - assertEquals(encoded + 1, encodeLongitude(max)); - assertEquals(encoded + 1, encodeLongitudeCeil(max)); - - // first and last doubles in range that will be quantized - double minEdge = Math.nextUp(min); - double maxEdge = Math.nextDown(max); - assertEquals(encoded, encodeLongitude(minEdge)); - assertEquals(encoded + 1, encodeLongitudeCeil(minEdge)); - assertEquals(encoded, encodeLongitude(maxEdge)); - assertEquals(encoded + 1, encodeLongitudeCeil(maxEdge)); - - // check random values within the double range - long minBits = NumericUtils.doubleToSortableLong(minEdge); - long maxBits = NumericUtils.doubleToSortableLong(maxEdge); - for (int j = 0; j < 100; j++) { - double value = NumericUtils.sortableLongToDouble(TestUtil.nextLong(random, minBits, maxBits)); - // round down - assertEquals(encoded, encodeLongitude(value)); - // round up - assertEquals(encoded + 1, encodeLongitudeCeil(value)); - } - } - } - } - - // check edge/interesting cases explicitly - public void testEncodeEdgeCases() { - assertEquals(Integer.MIN_VALUE, encodeLatitude(MIN_LAT_INCL)); - assertEquals(Integer.MIN_VALUE, encodeLatitudeCeil(MIN_LAT_INCL)); - assertEquals(Integer.MAX_VALUE, encodeLatitude(MAX_LAT_INCL)); - assertEquals(Integer.MAX_VALUE, encodeLatitudeCeil(MAX_LAT_INCL)); - - assertEquals(Integer.MIN_VALUE, encodeLongitude(MIN_LON_INCL)); - assertEquals(Integer.MIN_VALUE, encodeLongitudeCeil(MIN_LON_INCL)); - assertEquals(Integer.MAX_VALUE, encodeLongitude(MAX_LON_INCL)); - assertEquals(Integer.MAX_VALUE, encodeLongitudeCeil(MAX_LON_INCL)); - } -} diff --git a/libs/geo/src/test/java/org/elasticsearch/geo/TestGeoJSONParsing.java b/libs/geo/src/test/java/org/elasticsearch/geo/TestGeoJSONParsing.java deleted file mode 100644 index 26dd48e4ea21f..0000000000000 --- a/libs/geo/src/test/java/org/elasticsearch/geo/TestGeoJSONParsing.java +++ /dev/null @@ -1,266 +0,0 @@ -/* - * 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.geo; - -import org.apache.lucene.geo.Polygon; -import org.apache.lucene.util.LuceneTestCase; - -import java.text.ParseException; - -public class TestGeoJSONParsing extends LuceneTestCase { - public void testGeoJSONPolygon() throws Exception { - StringBuilder b = new StringBuilder(); - b.append("{\n"); - b.append(" \"type\": \"Polygon\",\n"); - b.append(" \"coordinates\": [\n"); - b.append(" [ [100.0, 0.0], [101.0, 0.0], [101.0, 1.0],\n"); - b.append(" [100.0, 1.0], [100.0, 0.0] ]\n"); - b.append(" ]\n"); - b.append("}\n"); - - Polygon[] polygons = Polygon.fromGeoJSON(b.toString()); - assertEquals(1, polygons.length); - assertEquals(new Polygon(new double[]{0.0, 0.0, 1.0, 1.0, 0.0}, - new double[]{100.0, 101.0, 101.0, 100.0, 100.0}), polygons[0]); - } - - public void testGeoJSONPolygonWithHole() throws Exception { - StringBuilder b = new StringBuilder(); - b.append("{\n"); - b.append(" \"type\": \"Polygon\",\n"); - b.append(" \"coordinates\": [\n"); - b.append(" [ [100.0, 0.0], [101.0, 0.0], [101.0, 1.0],\n"); - b.append(" [100.0, 1.0], [100.0, 0.0] ],\n"); - b.append(" [ [100.5, 0.5], [100.5, 0.75], [100.75, 0.75], [100.75, 0.5], [100.5, 0.5]]\n"); - b.append(" ]\n"); - b.append("}\n"); - - Polygon hole = new Polygon(new double[]{0.5, 0.75, 0.75, 0.5, 0.5}, - new double[]{100.5, 100.5, 100.75, 100.75, 100.5}); - Polygon expected = new Polygon(new double[]{0.0, 0.0, 1.0, 1.0, 0.0}, - new double[]{100.0, 101.0, 101.0, 100.0, 100.0}, hole); - Polygon[] polygons = Polygon.fromGeoJSON(b.toString()); - - assertEquals(1, polygons.length); - assertEquals(expected, polygons[0]); - } - - // a MultiPolygon returns multiple Polygons - public void testGeoJSONMultiPolygon() throws Exception { - StringBuilder b = new StringBuilder(); - b.append("{\n"); - b.append(" \"type\": \"MultiPolygon\",\n"); - b.append(" \"coordinates\": [\n"); - b.append(" [\n"); - b.append(" [ [100.0, 0.0], [101.0, 0.0], [101.0, 1.0],\n"); - b.append(" [100.0, 1.0], [100.0, 0.0] ]\n"); - b.append(" ],\n"); - b.append(" [\n"); - b.append(" [ [10.0, 2.0], [11.0, 2.0], [11.0, 3.0],\n"); - b.append(" [10.0, 3.0], [10.0, 2.0] ]\n"); - b.append(" ]\n"); - b.append(" ],\n"); - b.append("}\n"); - - Polygon[] polygons = Polygon.fromGeoJSON(b.toString()); - assertEquals(2, polygons.length); - assertEquals(new Polygon(new double[]{0.0, 0.0, 1.0, 1.0, 0.0}, - new double[]{100.0, 101.0, 101.0, 100.0, 100.0}), polygons[0]); - assertEquals(new Polygon(new double[]{2.0, 2.0, 3.0, 3.0, 2.0}, - new double[]{10.0, 11.0, 11.0, 10.0, 10.0}), polygons[1]); - } - - // make sure type can appear last (JSON allows arbitrary key/value order for objects) - public void testGeoJSONTypeComesLast() throws Exception { - StringBuilder b = new StringBuilder(); - b.append("{\n"); - b.append(" \"coordinates\": [\n"); - b.append(" [ [100.0, 0.0], [101.0, 0.0], [101.0, 1.0],\n"); - b.append(" [100.0, 1.0], [100.0, 0.0] ]\n"); - b.append(" ],\n"); - b.append(" \"type\": \"Polygon\",\n"); - b.append("}\n"); - - Polygon[] polygons = Polygon.fromGeoJSON(b.toString()); - assertEquals(1, polygons.length); - assertEquals(new Polygon(new double[]{0.0, 0.0, 1.0, 1.0, 0.0}, - new double[]{100.0, 101.0, 101.0, 100.0, 100.0}), polygons[0]); - } - - // make sure Polygon inside a type: Feature also works - public void testGeoJSONPolygonFeature() throws Exception { - StringBuilder b = new StringBuilder(); - b.append("{ \"type\": \"Feature\",\n"); - b.append(" \"geometry\": {\n"); - b.append(" \"type\": \"Polygon\",\n"); - b.append(" \"coordinates\": [\n"); - b.append(" [ [100.0, 0.0], [101.0, 0.0], [101.0, 1.0],\n"); - b.append(" [100.0, 1.0], [100.0, 0.0] ]\n"); - b.append(" ]\n"); - b.append(" },\n"); - b.append(" \"properties\": {\n"); - b.append(" \"prop0\": \"value0\",\n"); - b.append(" \"prop1\": {\"this\": \"that\"}\n"); - b.append(" }\n"); - b.append("}\n"); - - Polygon[] polygons = Polygon.fromGeoJSON(b.toString()); - assertEquals(1, polygons.length); - assertEquals(new Polygon(new double[]{0.0, 0.0, 1.0, 1.0, 0.0}, - new double[]{100.0, 101.0, 101.0, 100.0, 100.0}), polygons[0]); - } - - // make sure MultiPolygon inside a type: Feature also works - public void testGeoJSONMultiPolygonFeature() throws Exception { - StringBuilder b = new StringBuilder(); - b.append("{ \"type\": \"Feature\",\n"); - b.append(" \"geometry\": {\n"); - b.append(" \"type\": \"MultiPolygon\",\n"); - b.append(" \"coordinates\": [\n"); - b.append(" [\n"); - b.append(" [ [100.0, 0.0], [101.0, 0.0], [101.0, 1.0],\n"); - b.append(" [100.0, 1.0], [100.0, 0.0] ]\n"); - b.append(" ],\n"); - b.append(" [\n"); - b.append(" [ [10.0, 2.0], [11.0, 2.0], [11.0, 3.0],\n"); - b.append(" [10.0, 3.0], [10.0, 2.0] ]\n"); - b.append(" ]\n"); - b.append(" ]\n"); - b.append(" },\n"); - b.append(" \"properties\": {\n"); - b.append(" \"prop0\": \"value0\",\n"); - b.append(" \"prop1\": {\"this\": \"that\"}\n"); - b.append(" }\n"); - b.append("}\n"); - - Polygon[] polygons = Polygon.fromGeoJSON(b.toString()); - assertEquals(2, polygons.length); - assertEquals(new Polygon(new double[]{0.0, 0.0, 1.0, 1.0, 0.0}, - new double[]{100.0, 101.0, 101.0, 100.0, 100.0}), polygons[0]); - assertEquals(new Polygon(new double[]{2.0, 2.0, 3.0, 3.0, 2.0}, - new double[]{10.0, 11.0, 11.0, 10.0, 10.0}), polygons[1]); - } - - // FeatureCollection with one geometry is allowed: - public void testGeoJSONFeatureCollectionWithSinglePolygon() throws Exception { - StringBuilder b = new StringBuilder(); - b.append("{ \"type\": \"FeatureCollection\",\n"); - b.append(" \"features\": [\n"); - b.append(" { \"type\": \"Feature\",\n"); - b.append(" \"geometry\": {\n"); - b.append(" \"type\": \"Polygon\",\n"); - b.append(" \"coordinates\": [\n"); - b.append(" [ [100.0, 0.0], [101.0, 0.0], [101.0, 1.0],\n"); - b.append(" [100.0, 1.0], [100.0, 0.0] ]\n"); - b.append(" ]\n"); - b.append(" },\n"); - b.append(" \"properties\": {\n"); - b.append(" \"prop0\": \"value0\",\n"); - b.append(" \"prop1\": {\"this\": \"that\"}\n"); - b.append(" }\n"); - b.append(" }\n"); - b.append(" ]\n"); - b.append("} \n"); - - Polygon expected = new Polygon(new double[]{0.0, 0.0, 1.0, 1.0, 0.0}, - new double[]{100.0, 101.0, 101.0, 100.0, 100.0}); - Polygon[] actual = Polygon.fromGeoJSON(b.toString()); - assertEquals(1, actual.length); - assertEquals(expected, actual[0]); - } - - // stuff after the object is not allowed - public void testIllegalGeoJSONExtraCrapAtEnd() throws Exception { - StringBuilder b = new StringBuilder(); - b.append("{\n"); - b.append(" \"type\": \"Polygon\",\n"); - b.append(" \"coordinates\": [\n"); - b.append(" [ [100.0, 0.0], [101.0, 0.0], [101.0, 1.0],\n"); - b.append(" [100.0, 1.0], [100.0, 0.0] ]\n"); - b.append(" ]\n"); - b.append("}\n"); - b.append("foo\n"); - - Exception e = expectThrows(ParseException.class, () -> Polygon.fromGeoJSON(b.toString())); - assertTrue(e.getMessage().contains("unexpected character 'f' after end of GeoJSON object")); - } - - public void testIllegalGeoJSONLinkedCRS() throws Exception { - - StringBuilder b = new StringBuilder(); - b.append("{\n"); - b.append(" \"type\": \"Polygon\",\n"); - b.append(" \"coordinates\": [\n"); - b.append(" [ [100.0, 0.0], [101.0, 0.0], [101.0, 1.0],\n"); - b.append(" [100.0, 1.0], [100.0, 0.0] ]\n"); - b.append(" ],\n"); - b.append(" \"crs\": {\n"); - b.append(" \"type\": \"link\",\n"); - b.append(" \"properties\": {\n"); - b.append(" \"href\": \"http://example.com/crs/42\",\n"); - b.append(" \"type\": \"proj4\"\n"); - b.append(" }\n"); - b.append(" } \n"); - b.append("}\n"); - Exception e = expectThrows(ParseException.class, () -> Polygon.fromGeoJSON(b.toString())); - assertTrue(e.getMessage().contains("cannot handle linked crs")); - } - - // FeatureCollection with more than one geometry is not supported: - public void testIllegalGeoJSONMultipleFeatures() throws Exception { - StringBuilder b = new StringBuilder(); - b.append("{ \"type\": \"FeatureCollection\",\n"); - b.append(" \"features\": [\n"); - b.append(" { \"type\": \"Feature\",\n"); - b.append(" \"geometry\": {\"type\": \"Point\", \"coordinates\": [102.0, 0.5]},\n"); - b.append(" \"properties\": {\"prop0\": \"value0\"}\n"); - b.append(" },\n"); - b.append(" { \"type\": \"Feature\",\n"); - b.append(" \"geometry\": {\n"); - b.append(" \"type\": \"LineString\",\n"); - b.append(" \"coordinates\": [\n"); - b.append(" [102.0, 0.0], [103.0, 1.0], [104.0, 0.0], [105.0, 1.0]\n"); - b.append(" ]\n"); - b.append(" },\n"); - b.append(" \"properties\": {\n"); - b.append(" \"prop0\": \"value0\",\n"); - b.append(" \"prop1\": 0.0\n"); - b.append(" }\n"); - b.append(" },\n"); - b.append(" { \"type\": \"Feature\",\n"); - b.append(" \"geometry\": {\n"); - b.append(" \"type\": \"Polygon\",\n"); - b.append(" \"coordinates\": [\n"); - b.append(" [ [100.0, 0.0], [101.0, 0.0], [101.0, 1.0],\n"); - b.append(" [100.0, 1.0], [100.0, 0.0] ]\n"); - b.append(" ]\n"); - b.append(" },\n"); - b.append(" \"properties\": {\n"); - b.append(" \"prop0\": \"value0\",\n"); - b.append(" \"prop1\": {\"this\": \"that\"}\n"); - b.append(" }\n"); - b.append(" }\n"); - b.append(" ]\n"); - b.append("} \n"); - - Exception e = expectThrows(ParseException.class, () -> Polygon.fromGeoJSON(b.toString())); - assertTrue(e.getMessage().contains("can only handle type FeatureCollection (if it has a single polygon geometry), Feature, Polygon or MutiPolygon, but got Point")); - } -} diff --git a/libs/geo/src/test/java/org/elasticsearch/geo/TestGeoUtils.java b/libs/geo/src/test/java/org/elasticsearch/geo/TestGeoUtils.java deleted file mode 100644 index 5f51b796e2248..0000000000000 --- a/libs/geo/src/test/java/org/elasticsearch/geo/TestGeoUtils.java +++ /dev/null @@ -1,315 +0,0 @@ -/* - * 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.geo; - -import org.apache.lucene.geo.EarthDebugger; -import org.apache.lucene.geo.GeoTestUtil; -import org.apache.lucene.geo.GeoUtils; -import org.apache.lucene.geo.Rectangle; -import org.apache.lucene.util.LuceneTestCase; -import org.apache.lucene.util.SloppyMath; - -import java.util.Locale; - -/** - * Tests class for methods in GeoUtils - */ -public class TestGeoUtils extends LuceneTestCase { - - // We rely heavily on GeoUtils.circleToBBox so we test it here: - public void testRandomCircleToBBox() throws Exception { - int iters = atLeast(100); - for (int iter = 0; iter < iters; iter++) { - - double centerLat = GeoTestUtil.nextLatitude(); - double centerLon = GeoTestUtil.nextLongitude(); - - final double radiusMeters; - if (random().nextBoolean()) { - // Approx 4 degrees lon at the equator: - radiusMeters = random().nextDouble() * 444000; - } else { - radiusMeters = random().nextDouble() * 50000000; - } - - // TODO: randomly quantize radius too, to provoke exact math errors? - - Rectangle bbox = Rectangle.fromPointDistance(centerLat, centerLon, radiusMeters); - - int numPointsToTry = 1000; - for (int i = 0; i < numPointsToTry; i++) { - - double point[] = GeoTestUtil.nextPointNear(bbox); - double lat = point[0]; - double lon = point[1]; - - double distanceMeters = SloppyMath.haversinMeters(centerLat, centerLon, lat, lon); - - // Haversin says it's within the circle: - boolean haversinSays = distanceMeters <= radiusMeters; - - // BBox says its within the box: - boolean bboxSays; - if (bbox.crossesDateline()) { - if (lat >= bbox.minLat && lat <= bbox.maxLat) { - bboxSays = lon <= bbox.maxLon || lon >= bbox.minLon; - } else { - bboxSays = false; - } - } else { - bboxSays = lat >= bbox.minLat && lat <= bbox.maxLat && lon >= bbox.minLon && lon <= bbox.maxLon; - } - - if (haversinSays) { - if (bboxSays == false) { - System.out.println("centerLat=" + centerLat + " centerLon=" + centerLon + " radiusMeters=" + radiusMeters); - System.out.println(" bbox: lat=" + bbox.minLat + " to " + bbox.maxLat + " lon=" + bbox.minLon + " to " + bbox.maxLon); - System.out.println(" point: lat=" + lat + " lon=" + lon); - System.out.println(" haversin: " + distanceMeters); - fail("point was within the distance according to haversin, but the bbox doesn't contain it"); - } - } else { - // it's fine if haversin said it was outside the radius and bbox said it was inside the box - } - } - } - } - - // similar to testRandomCircleToBBox, but different, less evil, maybe simpler - public void testBoundingBoxOpto() { - int iters = atLeast(100); - for (int i = 0; i < iters; i++) { - double lat = GeoTestUtil.nextLatitude(); - double lon = GeoTestUtil.nextLongitude(); - double radius = 50000000 * random().nextDouble(); - Rectangle box = Rectangle.fromPointDistance(lat, lon, radius); - final Rectangle box1; - final Rectangle box2; - if (box.crossesDateline()) { - box1 = new Rectangle(box.minLat, box.maxLat, -180, box.maxLon); - box2 = new Rectangle(box.minLat, box.maxLat, box.minLon, 180); - } else { - box1 = box; - box2 = null; - } - - for (int j = 0; j < 1000; j++) { - double point[] = GeoTestUtil.nextPointNear(box); - double lat2 = point[0]; - double lon2 = point[1]; - // if the point is within radius, then it should be in our bounding box - if (SloppyMath.haversinMeters(lat, lon, lat2, lon2) <= radius) { - assertTrue(lat >= box.minLat && lat <= box.maxLat); - assertTrue(lon >= box1.minLon && lon <= box1.maxLon || (box2 != null && lon >= box2.minLon && lon <= box2.maxLon)); - } - } - } - } - - // test we can use haversinSortKey() for distance queries. - public void testHaversinOpto() { - int iters = atLeast(100); - for (int i = 0; i < iters; i++) { - double lat = GeoTestUtil.nextLatitude(); - double lon = GeoTestUtil.nextLongitude(); - double radius = 50000000 * random().nextDouble(); - Rectangle box = Rectangle.fromPointDistance(lat, lon, radius); - - if (box.maxLon - lon < 90 && lon - box.minLon < 90) { - double minPartialDistance = Math.max(SloppyMath.haversinSortKey(lat, lon, lat, box.maxLon), - SloppyMath.haversinSortKey(lat, lon, box.maxLat, lon)); - - for (int j = 0; j < 10000; j++) { - double point[] = GeoTestUtil.nextPointNear(box); - double lat2 = point[0]; - double lon2 = point[1]; - // if the point is within radius, then it should be <= our sort key - if (SloppyMath.haversinMeters(lat, lon, lat2, lon2) <= radius) { - assertTrue(SloppyMath.haversinSortKey(lat, lon, lat2, lon2) <= minPartialDistance); - } - } - } - } - } - - /** - * Test infinite radius covers whole earth - */ - public void testInfiniteRect() { - for (int i = 0; i < 1000; i++) { - double centerLat = GeoTestUtil.nextLatitude(); - double centerLon = GeoTestUtil.nextLongitude(); - Rectangle rect = Rectangle.fromPointDistance(centerLat, centerLon, Double.POSITIVE_INFINITY); - assertEquals(-180.0, rect.minLon, 0.0D); - assertEquals(180.0, rect.maxLon, 0.0D); - assertEquals(-90.0, rect.minLat, 0.0D); - assertEquals(90.0, rect.maxLat, 0.0D); - assertFalse(rect.crossesDateline()); - } - } - - public void testAxisLat() { - double earthCircumference = 2D * Math.PI * GeoUtils.EARTH_MEAN_RADIUS_METERS; - assertEquals(90, Rectangle.axisLat(0, earthCircumference / 4), 0.0D); - - for (int i = 0; i < 100; ++i) { - boolean reallyBig = random().nextInt(10) == 0; - final double maxRadius = reallyBig ? 1.1 * earthCircumference : earthCircumference / 8; - final double radius = maxRadius * random().nextDouble(); - double prevAxisLat = Rectangle.axisLat(0.0D, radius); - for (double lat = 0.1D; lat < 90D; lat += 0.1D) { - double nextAxisLat = Rectangle.axisLat(lat, radius); - Rectangle bbox = Rectangle.fromPointDistance(lat, 180D, radius); - double dist = SloppyMath.haversinMeters(lat, 180D, nextAxisLat, bbox.maxLon); - if (nextAxisLat < GeoUtils.MAX_LAT_INCL) { - assertEquals("lat = " + lat, dist, radius, 0.1D); - } - assertTrue("lat = " + lat, prevAxisLat <= nextAxisLat); - prevAxisLat = nextAxisLat; - } - - prevAxisLat = Rectangle.axisLat(-0.0D, radius); - for (double lat = -0.1D; lat > -90D; lat -= 0.1D) { - double nextAxisLat = Rectangle.axisLat(lat, radius); - Rectangle bbox = Rectangle.fromPointDistance(lat, 180D, radius); - double dist = SloppyMath.haversinMeters(lat, 180D, nextAxisLat, bbox.maxLon); - if (nextAxisLat > GeoUtils.MIN_LAT_INCL) { - assertEquals("lat = " + lat, dist, radius, 0.1D); - } - assertTrue("lat = " + lat, prevAxisLat >= nextAxisLat); - prevAxisLat = nextAxisLat; - } - } - } - - // TODO: does not really belong here, but we test it like this for now - // we can make a fake IndexReader to send boxes directly to Point visitors instead? - public void testCircleOpto() throws Exception { - int iters = atLeast(20); - for (int i = 0; i < iters; i++) { - // circle - final double centerLat = -90 + 180.0 * random().nextDouble(); - final double centerLon = -180 + 360.0 * random().nextDouble(); - final double radius = 50_000_000D * random().nextDouble(); - final Rectangle box = Rectangle.fromPointDistance(centerLat, centerLon, radius); - // TODO: remove this leniency! - if (box.crossesDateline()) { - --i; // try again... - continue; - } - final double axisLat = Rectangle.axisLat(centerLat, radius); - - for (int k = 0; k < 1000; ++k) { - - double[] latBounds = {-90, box.minLat, axisLat, box.maxLat, 90}; - double[] lonBounds = {-180, box.minLon, centerLon, box.maxLon, 180}; - // first choose an upper left corner - int maxLatRow = random().nextInt(4); - double latMax = randomInRange(latBounds[maxLatRow], latBounds[maxLatRow + 1]); - int minLonCol = random().nextInt(4); - double lonMin = randomInRange(lonBounds[minLonCol], lonBounds[minLonCol + 1]); - // now choose a lower right corner - int minLatMaxRow = maxLatRow == 3 ? 3 : maxLatRow + 1; // make sure it will at least cross into the bbox - int minLatRow = random().nextInt(minLatMaxRow); - double latMin = randomInRange(latBounds[minLatRow], Math.min(latBounds[minLatRow + 1], latMax)); - int maxLonMinCol = Math.max(minLonCol, 1); // make sure it will at least cross into the bbox - int maxLonCol = maxLonMinCol + random().nextInt(4 - maxLonMinCol); - double lonMax = randomInRange(Math.max(lonBounds[maxLonCol], lonMin), lonBounds[maxLonCol + 1]); - - assert latMax >= latMin; - assert lonMax >= lonMin; - - if (isDisjoint(centerLat, centerLon, radius, axisLat, latMin, latMax, lonMin, lonMax)) { - // intersects says false: test a ton of points - for (int j = 0; j < 200; j++) { - double lat = latMin + (latMax - latMin) * random().nextDouble(); - double lon = lonMin + (lonMax - lonMin) * random().nextDouble(); - - if (random().nextBoolean()) { - // explicitly test an edge - int edge = random().nextInt(4); - if (edge == 0) { - lat = latMin; - } else if (edge == 1) { - lat = latMax; - } else if (edge == 2) { - lon = lonMin; - } else if (edge == 3) { - lon = lonMax; - } - } - double distance = SloppyMath.haversinMeters(centerLat, centerLon, lat, lon); - try { - assertTrue(String.format(Locale.ROOT, "\nisDisjoint(\n" + - "centerLat=%s\n" + - "centerLon=%s\n" + - "radius=%s\n" + - "latMin=%s\n" + - "latMax=%s\n" + - "lonMin=%s\n" + - "lonMax=%s) == false BUT\n" + - "haversin(%s, %s, %s, %s) = %s\nbbox=%s", - centerLat, centerLon, radius, latMin, latMax, lonMin, lonMax, - centerLat, centerLon, lat, lon, distance, Rectangle.fromPointDistance(centerLat, centerLon, radius)), - distance > radius); - } catch (AssertionError e) { - EarthDebugger ed = new EarthDebugger(); - ed.addRect(latMin, latMax, lonMin, lonMax); - ed.addCircle(centerLat, centerLon, radius, true); - System.out.println(ed.finish()); - throw e; - } - } - } - } - } - } - - static double randomInRange(double min, double max) { - return min + (max - min) * random().nextDouble(); - } - - static boolean isDisjoint(double centerLat, double centerLon, double radius, double axisLat, double latMin, double latMax, double lonMin, double lonMax) { - if ((centerLon < lonMin || centerLon > lonMax) && (axisLat + Rectangle.AXISLAT_ERROR < latMin || axisLat - Rectangle.AXISLAT_ERROR > latMax)) { - // circle not fully inside / crossing axis - if (SloppyMath.haversinMeters(centerLat, centerLon, latMin, lonMin) > radius && - SloppyMath.haversinMeters(centerLat, centerLon, latMin, lonMax) > radius && - SloppyMath.haversinMeters(centerLat, centerLon, latMax, lonMin) > radius && - SloppyMath.haversinMeters(centerLat, centerLon, latMax, lonMax) > radius) { - // no points inside - return true; - } - } - - return false; - } - - public void testWithin90LonDegrees() { - assertTrue(GeoUtils.within90LonDegrees(0, -80, 80)); - assertFalse(GeoUtils.within90LonDegrees(0, -100, 80)); - assertFalse(GeoUtils.within90LonDegrees(0, -80, 100)); - - assertTrue(GeoUtils.within90LonDegrees(-150, 140, 170)); - assertFalse(GeoUtils.within90LonDegrees(-150, 120, 150)); - - assertTrue(GeoUtils.within90LonDegrees(150, -170, -140)); - assertFalse(GeoUtils.within90LonDegrees(150, -150, -120)); - } -} diff --git a/libs/geo/src/test/java/org/elasticsearch/geo/geometry/BaseGeometryTestCase.java b/libs/geo/src/test/java/org/elasticsearch/geo/geometry/BaseGeometryTestCase.java index d003c26de86c9..7dc1938a67c01 100644 --- a/libs/geo/src/test/java/org/elasticsearch/geo/geometry/BaseGeometryTestCase.java +++ b/libs/geo/src/test/java/org/elasticsearch/geo/geometry/BaseGeometryTestCase.java @@ -19,82 +19,116 @@ package org.elasticsearch.geo.geometry; -import org.apache.lucene.geo.GeoUtils; -import org.apache.lucene.util.LuceneTestCase; +import org.elasticsearch.ElasticsearchException; +import org.elasticsearch.Version; +import org.elasticsearch.common.io.stream.Writeable; +import org.elasticsearch.geo.utils.WellKnownText; +import org.elasticsearch.test.AbstractWireTestCase; -abstract class BaseGeometryTestCase extends LuceneTestCase { - abstract public T getShape(); +import java.io.IOException; +import java.text.ParseException; +import java.util.ArrayList; +import java.util.List; +import java.util.function.Supplier; - public void testArea() { - expectThrows(UnsupportedOperationException.class, () -> getShape().getArea()); +abstract class BaseGeometryTestCase extends AbstractWireTestCase { + + @Override + protected Writeable.Reader instanceReader() { + throw new IllegalStateException("shouldn't be called in this test"); } - /** - * tests bounding box of shape - */ - abstract public void testBoundingBox(); - - /** - * tests WITHIN relation - */ - abstract public void testWithin(); - - /** - * tests CONTAINS relation - */ - abstract public void testContains(); - - /** - * tests DISJOINT relation - */ - abstract public void testDisjoint(); - - /** - * tests INTERSECTS relation - */ - abstract public void testIntersects(); - - /** - * tests center of shape - */ - public void testCenter() { - GeoShape shape = getShape(); - Rectangle bbox = shape.getBoundingBox(); - double centerLat = StrictMath.abs(bbox.maxLat() - bbox.minLat()) * 0.5 + bbox.minLat(); - double centerLon; - if (bbox.crossesDateline()) { - centerLon = GeoUtils.MAX_LON_INCL - bbox.minLon() + bbox.maxLon() - GeoUtils.MIN_LON_INCL; - centerLon = GeoUtils.normalizeLonDegrees(centerLon * 0.5 + bbox.minLon()); - } else { - centerLon = StrictMath.abs(bbox.maxLon() - bbox.minLon()) * 0.5 + bbox.minLon(); + + @SuppressWarnings("unchecked") + @Override + protected T copyInstance(T instance, Version version) throws IOException { + String text = WellKnownText.toWKT(instance); + try { + return (T) WellKnownText.fromWKT(text); + } catch (ParseException e) { + throw new ElasticsearchException(e); } - assertEquals(shape.getCenter(), new Point(centerLat, centerLon)); } - /** - * helper method for semi-random relation testing - */ - protected void relationTest(GeoShape points, GeoShape.Relation r) { - Rectangle bbox = points.getBoundingBox(); - double minLat = bbox.minLat(); - double maxLat = bbox.maxLat(); - double minLon = bbox.minLon(); - double maxLon = bbox.maxLon(); - - if (r == GeoShape.Relation.WITHIN) { - return; - } else if (r == GeoShape.Relation.DISJOINT) { - // shrink test box - minLat -= 20D; - maxLat = minLat - 1D; - minLon -= 20D; - maxLon = minLon - 1D; - } else if (r == GeoShape.Relation.INTERSECTS) { - // intersects (note: MultiPoint does not support CONTAINS) - minLat -= 10D; - maxLat = minLat + 10D; + public static double randomLat() { + return randomDoubleBetween(-90, 90, true); + } + + public static double randomLon() { + return randomDoubleBetween(-180, 180, true); + } + + public static Circle randomCircle() { + return new Circle(randomDoubleBetween(-90, 90, true), randomDoubleBetween(-180, 180, true), randomDoubleBetween(0, 100, false)); + } + + public static Line randomLine() { + int size = randomIntBetween(2, 10); + double[] lats = new double[size]; + double[] lons = new double[size]; + for (int i = 0; i < size; i++) { + lats[i] = randomLat(); + lons[i] = randomLon(); } + return new Line(lats, lons); + } - assertEquals(r, points.relate(minLat, maxLat, minLon, maxLon)); + public static Point randomPoint() { + return new Point(randomLat(), randomLon()); + } + + public static LinearRing randomLinearRing() { + int size = randomIntBetween(3, 10); + double[] lats = new double[size + 1]; + double[] lons = new double[size + 1]; + for (int i = 0; i < size; i++) { + lats[i] = randomLat(); + lons[i] = randomLon(); + } + lats[size] = lats[0]; + lons[size] = lons[0]; + return new LinearRing(lats, lons); + } + + public static Polygon randomPolygon() { + int size = randomIntBetween(0, 10); + List holes = new ArrayList<>(); + for (int i = 0; i < size; i++) { + holes.add(randomLinearRing()); + } + if (holes.size() > 0) { + return new Polygon(randomLinearRing(), holes); + } else { + return new Polygon(randomLinearRing()); + } + } + + public static Rectangle randomRectangle() { + double lat1 = randomLat(); + double lat2 = randomLat(); + double minLon = randomLon(); + double maxLon = randomLon(); + return new Rectangle(Math.min(lat1, lat2), Math.max(lat1, lat2), minLon, maxLon); + } + + public static GeometryCollection randomGeometryCollection() { + return randomGeometryCollection(0); + } + + private static GeometryCollection randomGeometryCollection(int level) { + int size = randomIntBetween(1, 10); + List shapes = new ArrayList<>(); + for (int i = 0; i < size; i++) { + @SuppressWarnings("unchecked") Supplier geometry = randomFrom( + BaseGeometryTestCase::randomCircle, + BaseGeometryTestCase::randomLine, + BaseGeometryTestCase::randomPoint, + BaseGeometryTestCase::randomPolygon, + BaseGeometryTestCase::randomRectangle, + level < 3 ? () -> randomGeometryCollection(level + 1) : BaseGeometryTestCase::randomPoint // don't build too deep + ); + shapes.add(geometry.get()); + } + return new GeometryCollection<>(shapes); } } diff --git a/libs/geo/src/test/java/org/elasticsearch/geo/geometry/CircleTests.java b/libs/geo/src/test/java/org/elasticsearch/geo/geometry/CircleTests.java new file mode 100644 index 0000000000000..0f2292792a743 --- /dev/null +++ b/libs/geo/src/test/java/org/elasticsearch/geo/geometry/CircleTests.java @@ -0,0 +1,51 @@ +/* + * 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.geo.geometry; + +import org.elasticsearch.geo.utils.WellKnownText; + +import java.io.IOException; +import java.text.ParseException; + +public class CircleTests extends BaseGeometryTestCase { + @Override + protected Circle createTestInstance() { + return new Circle(randomDoubleBetween(-90, 90, true), randomDoubleBetween(-180, 180, true), randomDoubleBetween(0, 100, false)); + } + + public void testBasicSerialization() throws IOException, ParseException { + assertEquals("circle (20.0 10.0 15.0)", WellKnownText.toWKT(new Circle(10, 20, 15))); + assertEquals(new Circle(10, 20, 15), WellKnownText.fromWKT("circle (20.0 10.0 15.0)")); + + assertEquals("circle EMPTY", WellKnownText.toWKT(Circle.EMPTY)); + assertEquals(Circle.EMPTY, WellKnownText.fromWKT("circle EMPTY)")); + } + + public void testInitValidation() { + IllegalArgumentException ex = expectThrows(IllegalArgumentException.class, () -> new Circle(10, 20, -1)); + assertEquals("Circle radius [-1.0] cannot be negative", ex.getMessage()); + + ex = expectThrows(IllegalArgumentException.class, () -> new Circle(100, 20, 1)); + assertEquals("invalid latitude 100.0; must be between -90.0 and 90.0", ex.getMessage()); + + ex = expectThrows(IllegalArgumentException.class, () -> new Circle(10, 200, 1)); + assertEquals("invalid longitude 200.0; must be between -180.0 and 180.0", ex.getMessage()); + } +} diff --git a/libs/geo/src/test/java/org/elasticsearch/geo/geometry/GeometryCollectionTests.java b/libs/geo/src/test/java/org/elasticsearch/geo/geometry/GeometryCollectionTests.java new file mode 100644 index 0000000000000..21b3c65410372 --- /dev/null +++ b/libs/geo/src/test/java/org/elasticsearch/geo/geometry/GeometryCollectionTests.java @@ -0,0 +1,54 @@ +/* + * 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.geo.geometry; + +import org.elasticsearch.geo.utils.WellKnownText; + +import java.io.IOException; +import java.text.ParseException; +import java.util.Arrays; +import java.util.Collections; + +public class GeometryCollectionTests extends BaseGeometryTestCase> { + @Override + protected GeometryCollection createTestInstance() { + return randomGeometryCollection(); + } + + public void testBasicSerialization() throws IOException, ParseException { + assertEquals("geometrycollection (point (20.0 10.0),point EMPTY)", + WellKnownText.toWKT(new GeometryCollection(Arrays.asList(new Point(10, 20), Point.EMPTY)))); + + assertEquals(new GeometryCollection(Arrays.asList(new Point(10, 20), Point.EMPTY)), + WellKnownText.fromWKT("geometrycollection (point (20.0 10.0),point EMPTY)")); + + assertEquals("geometrycollection EMPTY", WellKnownText.toWKT(GeometryCollection.EMPTY)); + assertEquals(GeometryCollection.EMPTY, WellKnownText.fromWKT("geometrycollection EMPTY)")); + } + + @SuppressWarnings("ConstantConditions") + public void testInitValidation() { + IllegalArgumentException ex = expectThrows(IllegalArgumentException.class, () -> new GeometryCollection<>(Collections.emptyList())); + assertEquals("the list of shapes cannot be null or empty", ex.getMessage()); + + ex = expectThrows(IllegalArgumentException.class, () -> new GeometryCollection<>(null)); + assertEquals("the list of shapes cannot be null or empty", ex.getMessage()); + } +} diff --git a/libs/geo/src/test/java/org/elasticsearch/geo/geometry/LineTests.java b/libs/geo/src/test/java/org/elasticsearch/geo/geometry/LineTests.java new file mode 100644 index 0000000000000..9914481df44e1 --- /dev/null +++ b/libs/geo/src/test/java/org/elasticsearch/geo/geometry/LineTests.java @@ -0,0 +1,51 @@ +/* + * 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.geo.geometry; + +import org.elasticsearch.geo.utils.WellKnownText; + +import java.io.IOException; +import java.text.ParseException; + +public class LineTests extends BaseGeometryTestCase { + @Override + protected Line createTestInstance() { + return randomLine(); + } + + public void testBasicSerialization() throws IOException, ParseException { + assertEquals("linestring (3.0 1.0, 4.0 2.0)", WellKnownText.toWKT(new Line(new double[]{1, 2}, new double[]{3, 4}))); + assertEquals(new Line(new double[]{1, 2}, new double[]{3, 4}), WellKnownText.fromWKT("linestring (3 1, 4 2)")); + + assertEquals("linestring EMPTY", WellKnownText.toWKT(Line.EMPTY)); + assertEquals(Line.EMPTY, WellKnownText.fromWKT("linestring EMPTY)")); + } + + public void testInitValidation() { + IllegalArgumentException ex = expectThrows(IllegalArgumentException.class, () -> new Line(new double[]{1}, new double[]{3})); + assertEquals("at least two points in the line is required", ex.getMessage()); + + ex = expectThrows(IllegalArgumentException.class, () -> new Line(new double[]{1, 2, 3, 1}, new double[]{3, 4, 500, 3})); + assertEquals("invalid longitude 500.0; must be between -180.0 and 180.0", ex.getMessage()); + + ex = expectThrows(IllegalArgumentException.class, () -> new Line(new double[]{1, 100, 3, 1}, new double[]{3, 4, 5, 3})); + assertEquals("invalid latitude 100.0; must be between -90.0 and 90.0", ex.getMessage()); + } +} diff --git a/libs/geo/src/test/java/org/elasticsearch/geo/geometry/LinearRingTests.java b/libs/geo/src/test/java/org/elasticsearch/geo/geometry/LinearRingTests.java new file mode 100644 index 0000000000000..94d6449845359 --- /dev/null +++ b/libs/geo/src/test/java/org/elasticsearch/geo/geometry/LinearRingTests.java @@ -0,0 +1,48 @@ +/* + * 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.geo.geometry; + +import org.elasticsearch.geo.utils.WellKnownText; +import org.elasticsearch.test.ESTestCase; + +public class LinearRingTests extends ESTestCase { + + public void testBasicSerialization() { + UnsupportedOperationException ex = expectThrows(UnsupportedOperationException.class, + () -> WellKnownText.toWKT(new LinearRing(new double[]{1, 2, 3, 1}, new double[]{3, 4, 5, 3}))); + assertEquals("line ring cannot be serialized using WKT", ex.getMessage()); + } + + public void testInitValidation() { + IllegalArgumentException ex = expectThrows(IllegalArgumentException.class, + () -> new LinearRing(new double[]{1, 2, 3}, new double[]{3, 4, 5})); + assertEquals("first and last points of the linear ring must be the same (it must close itself): lats[0]=1.0 lats[2]=3.0", + ex.getMessage()); + + ex = expectThrows(IllegalArgumentException.class, () -> new LinearRing(new double[]{1}, new double[]{3})); + assertEquals("at least two points in the line is required", ex.getMessage()); + + ex = expectThrows(IllegalArgumentException.class, () -> new LinearRing(new double[]{1, 2, 3, 1}, new double[]{3, 4, 500, 3})); + assertEquals("invalid longitude 500.0; must be between -180.0 and 180.0", ex.getMessage()); + + ex = expectThrows(IllegalArgumentException.class, () -> new LinearRing(new double[]{1, 100, 3, 1}, new double[]{3, 4, 5, 3})); + assertEquals("invalid latitude 100.0; must be between -90.0 and 90.0", ex.getMessage()); + } +} diff --git a/libs/geo/src/test/java/org/elasticsearch/geo/geometry/MultiLineTests.java b/libs/geo/src/test/java/org/elasticsearch/geo/geometry/MultiLineTests.java new file mode 100644 index 0000000000000..ec5a8c2dd3860 --- /dev/null +++ b/libs/geo/src/test/java/org/elasticsearch/geo/geometry/MultiLineTests.java @@ -0,0 +1,51 @@ +/* + * 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.geo.geometry; + +import org.elasticsearch.geo.utils.WellKnownText; + +import java.io.IOException; +import java.text.ParseException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class MultiLineTests extends BaseGeometryTestCase { + + @Override + protected MultiLine createTestInstance() { + int size = randomIntBetween(1, 10); + List arr = new ArrayList(); + for (int i = 0; i < size; i++) { + arr.add(randomLine()); + } + return new MultiLine(arr); + } + + public void testBasicSerialization() throws IOException, ParseException { + assertEquals("multilinestring ((3.0 1.0, 4.0 2.0))", WellKnownText.toWKT( + new MultiLine(Collections.singletonList(new Line(new double[]{1, 2}, new double[]{3, 4}))))); + assertEquals(new MultiLine(Collections.singletonList(new Line(new double[]{1, 2}, new double[]{3, 4}))), + WellKnownText.fromWKT("multilinestring ((3 1, 4 2))")); + + assertEquals("multilinestring EMPTY", WellKnownText.toWKT(MultiLine.EMPTY)); + assertEquals(MultiLine.EMPTY, WellKnownText.fromWKT("multilinestring EMPTY)")); + } +} diff --git a/libs/geo/src/test/java/org/elasticsearch/geo/geometry/MultiPointTests.java b/libs/geo/src/test/java/org/elasticsearch/geo/geometry/MultiPointTests.java new file mode 100644 index 0000000000000..81c8c6f3facab --- /dev/null +++ b/libs/geo/src/test/java/org/elasticsearch/geo/geometry/MultiPointTests.java @@ -0,0 +1,51 @@ +/* + * 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.geo.geometry; + +import org.elasticsearch.geo.utils.WellKnownText; + +import java.io.IOException; +import java.text.ParseException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class MultiPointTests extends BaseGeometryTestCase { + + @Override + protected MultiPoint createTestInstance() { + int size = randomIntBetween(1, 10); + List arr = new ArrayList<>(); + for (int i = 0; i < size; i++) { + arr.add(randomPoint()); + } + return new MultiPoint(arr); + } + + public void testBasicSerialization() throws IOException, ParseException { + assertEquals("multipoint (2.0 1.0)", WellKnownText.toWKT( + new MultiPoint(Collections.singletonList(new Point(1, 2))))); + assertEquals(new MultiPoint(Collections.singletonList(new Point(1 ,2))), + WellKnownText.fromWKT("multipoint (2 1)")); + + assertEquals("multipoint EMPTY", WellKnownText.toWKT(MultiPoint.EMPTY)); + assertEquals(MultiPoint.EMPTY, WellKnownText.fromWKT("multipoint EMPTY)")); + } +} diff --git a/libs/geo/src/test/java/org/elasticsearch/geo/geometry/MultiPolygonTests.java b/libs/geo/src/test/java/org/elasticsearch/geo/geometry/MultiPolygonTests.java new file mode 100644 index 0000000000000..382572456032c --- /dev/null +++ b/libs/geo/src/test/java/org/elasticsearch/geo/geometry/MultiPolygonTests.java @@ -0,0 +1,53 @@ +/* + * 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.geo.geometry; + +import org.elasticsearch.geo.utils.WellKnownText; + +import java.io.IOException; +import java.text.ParseException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class MultiPolygonTests extends BaseGeometryTestCase { + + @Override + protected MultiPolygon createTestInstance() { + int size = randomIntBetween(1, 10); + List arr = new ArrayList<>(); + for (int i = 0; i < size; i++) { + arr.add(randomPolygon()); + } + return new MultiPolygon(arr); + } + + public void testBasicSerialization() throws IOException, ParseException { + assertEquals("multipolygon (((3.0 1.0, 4.0 2.0, 5.0 3.0, 3.0 1.0)))", + WellKnownText.toWKT(new MultiPolygon(Collections.singletonList( + new Polygon(new LinearRing(new double[]{1, 2, 3, 1}, new double[]{3, 4, 5, 3})))))); + assertEquals(new MultiPolygon(Collections.singletonList( + new Polygon(new LinearRing(new double[]{1, 2, 3, 1}, new double[]{3, 4, 5, 3})))), + WellKnownText.fromWKT("multipolygon (((3.0 1.0, 4.0 2.0, 5.0 3.0, 3.0 1.0)))")); + + assertEquals("multipolygon EMPTY", WellKnownText.toWKT(MultiPolygon.EMPTY)); + assertEquals(MultiPolygon.EMPTY, WellKnownText.fromWKT("multipolygon EMPTY)")); + } +} diff --git a/libs/geo/src/test/java/org/elasticsearch/geo/geometry/PointTests.java b/libs/geo/src/test/java/org/elasticsearch/geo/geometry/PointTests.java new file mode 100644 index 0000000000000..bfdb369d7a839 --- /dev/null +++ b/libs/geo/src/test/java/org/elasticsearch/geo/geometry/PointTests.java @@ -0,0 +1,48 @@ +/* + * 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.geo.geometry; + +import org.elasticsearch.geo.utils.WellKnownText; + +import java.io.IOException; +import java.text.ParseException; + +public class PointTests extends BaseGeometryTestCase { + @Override + protected Point createTestInstance() { + return randomPoint(); + } + + public void testBasicSerialization() throws IOException, ParseException { + assertEquals("point (20.0 10.0)", WellKnownText.toWKT(new Point(10, 20))); + assertEquals(new Point(10, 20), WellKnownText.fromWKT("point (20.0 10.0)")); + + assertEquals("point EMPTY", WellKnownText.toWKT(Point.EMPTY)); + assertEquals(Point.EMPTY, WellKnownText.fromWKT("point EMPTY)")); + } + + public void testInitValidation() { + IllegalArgumentException ex = expectThrows(IllegalArgumentException.class, () -> new Point(100, 10)); + assertEquals("invalid latitude 100.0; must be between -90.0 and 90.0", ex.getMessage()); + + ex = expectThrows(IllegalArgumentException.class, () -> new Point(10, 500)); + assertEquals("invalid longitude 500.0; must be between -180.0 and 180.0", ex.getMessage()); + } +} diff --git a/libs/geo/src/test/java/org/elasticsearch/geo/geometry/PolygonTests.java b/libs/geo/src/test/java/org/elasticsearch/geo/geometry/PolygonTests.java new file mode 100644 index 0000000000000..69a6d232083fe --- /dev/null +++ b/libs/geo/src/test/java/org/elasticsearch/geo/geometry/PolygonTests.java @@ -0,0 +1,52 @@ +/* + * 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.geo.geometry; + +import org.elasticsearch.geo.utils.WellKnownText; + +import java.io.IOException; +import java.text.ParseException; + +public class PolygonTests extends BaseGeometryTestCase { + @Override + protected Polygon createTestInstance() { + return randomPolygon(); + } + + public void testBasicSerialization() throws IOException, ParseException { + assertEquals("polygon ((3.0 1.0, 4.0 2.0, 5.0 3.0, 3.0 1.0))", + WellKnownText.toWKT(new Polygon(new LinearRing(new double[]{1, 2, 3, 1}, new double[]{3, 4, 5, 3})))); + assertEquals(new Polygon(new LinearRing(new double[]{1, 2, 3, 1}, new double[]{3, 4, 5, 3})), + WellKnownText.fromWKT("polygon ((3 1, 4 2, 5 3, 3 1))")); + + assertEquals("polygon EMPTY", WellKnownText.toWKT(Polygon.EMPTY)); + assertEquals(Polygon.EMPTY, WellKnownText.fromWKT("polygon EMPTY)")); + } + + public void testInitValidation() { + IllegalArgumentException ex = expectThrows(IllegalArgumentException.class, + () -> new Polygon(new LinearRing(new double[]{1, 2, 1}, new double[]{3, 4, 3}))); + assertEquals("at least 4 polygon points required", ex.getMessage()); + + ex = expectThrows(IllegalArgumentException.class, + () -> new Polygon(new LinearRing(new double[]{1, 2, 3, 1}, new double[]{3, 4, 5, 3}), null)); + assertEquals("holes must not be null", ex.getMessage()); + } +} diff --git a/libs/geo/src/test/java/org/elasticsearch/geo/geometry/RectangleTests.java b/libs/geo/src/test/java/org/elasticsearch/geo/geometry/RectangleTests.java new file mode 100644 index 0000000000000..fa5cbcd0a8f05 --- /dev/null +++ b/libs/geo/src/test/java/org/elasticsearch/geo/geometry/RectangleTests.java @@ -0,0 +1,51 @@ +/* + * 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.geo.geometry; + +import org.elasticsearch.geo.utils.WellKnownText; + +import java.io.IOException; +import java.text.ParseException; + +public class RectangleTests extends BaseGeometryTestCase { + @Override + protected Rectangle createTestInstance() { + return randomRectangle(); + } + + public void testBasicSerialization() throws IOException, ParseException { + assertEquals("bbox (10.0, 20.0, 40.0, 30.0)", WellKnownText.toWKT(new Rectangle(30, 40, 10, 20))); + assertEquals(new Rectangle(30, 40, 10, 20), WellKnownText.fromWKT("bbox (10.0, 20.0, 40.0, 30.0)")); + + assertEquals("bbox EMPTY", WellKnownText.toWKT(Rectangle.EMPTY)); + assertEquals(Rectangle.EMPTY, WellKnownText.fromWKT("bbox EMPTY)")); + } + + public void testInitValidation() { + IllegalArgumentException ex = expectThrows(IllegalArgumentException.class, () -> new Rectangle(100, 1, 2, 3)); + assertEquals("invalid latitude 100.0; must be between -90.0 and 90.0", ex.getMessage()); + + ex = expectThrows(IllegalArgumentException.class, () -> new Rectangle(1, 2, 200, 3)); + assertEquals("invalid longitude 200.0; must be between -180.0 and 180.0", ex.getMessage()); + + ex = expectThrows(IllegalArgumentException.class, () -> new Rectangle(2, 1, 2, 3)); + assertEquals("max lat cannot be less than min lat", ex.getMessage()); + } +} diff --git a/libs/geo/src/test/java/org/elasticsearch/geo/geometry/TestEdgeTree.java b/libs/geo/src/test/java/org/elasticsearch/geo/geometry/TestEdgeTree.java deleted file mode 100644 index 970598ef213f9..0000000000000 --- a/libs/geo/src/test/java/org/elasticsearch/geo/geometry/TestEdgeTree.java +++ /dev/null @@ -1,312 +0,0 @@ -/* - * 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.geo.geometry; - -import org.apache.lucene.geo.GeoTestUtil; -import org.apache.lucene.index.PointValues.Relation; -import org.apache.lucene.util.LuceneTestCase; - -import static org.apache.lucene.geo.GeoTestUtil.nextLatitude; -import static org.apache.lucene.geo.GeoTestUtil.nextLongitude; -import static org.apache.lucene.geo.GeoTestUtil.nextPolygon; - -/** - * Test EdgeTree impl - */ -public class TestEdgeTree extends LuceneTestCase { - - /** - * Three boxes, an island inside a hole inside a shape - */ - public void testMultiPolygon() { - Polygon hole = new Polygon(new double[]{-10, -10, 10, 10, -10}, new double[]{-10, 10, 10, -10, -10}); - Polygon outer = new Polygon(new double[]{-50, -50, 50, 50, -50}, new double[]{-50, 50, 50, -50, -50}, hole); - Polygon island = new Polygon(new double[]{-5, -5, 5, 5, -5}, new double[]{-5, 5, 5, -5, -5}); - EdgeTree polygon = EdgeTree.create(outer, island); - - // contains(point) - assertTrue(polygon.contains(-2, 2)); // on the island - assertFalse(polygon.contains(-6, 6)); // in the hole - assertTrue(polygon.contains(-25, 25)); // on the mainland - assertFalse(polygon.contains(-51, 51)); // in the ocean - - // relate(box): this can conservatively return CELL_CROSSES_QUERY - assertEquals(Relation.CELL_INSIDE_QUERY, polygon.relate(-2, 2, -2, 2)); // on the island - assertEquals(Relation.CELL_OUTSIDE_QUERY, polygon.relate(6, 7, 6, 7)); // in the hole - assertEquals(Relation.CELL_INSIDE_QUERY, polygon.relate(24, 25, 24, 25)); // on the mainland - assertEquals(Relation.CELL_OUTSIDE_QUERY, polygon.relate(51, 52, 51, 52)); // in the ocean - assertEquals(Relation.CELL_CROSSES_QUERY, polygon.relate(-60, 60, -60, 60)); // enclosing us completely - assertEquals(Relation.CELL_CROSSES_QUERY, polygon.relate(49, 51, 49, 51)); // overlapping the mainland - assertEquals(Relation.CELL_CROSSES_QUERY, polygon.relate(9, 11, 9, 11)); // overlapping the hole - assertEquals(Relation.CELL_CROSSES_QUERY, polygon.relate(5, 6, 5, 6)); // overlapping the island - } - - public void testPacMan() throws Exception { - // pacman - double[] px = {0, 10, 10, 0, -8, -10, -8, 0, 10, 10, 0}; - double[] py = {0, 5, 9, 10, 9, 0, -9, -10, -9, -5, 0}; - - // candidate crosses cell - double xMin = 2;//-5; - double xMax = 11;//0.000001; - double yMin = -1;//0; - double yMax = 1;//5; - - // test cell crossing poly - EdgeTree polygon = EdgeTree.create(new Polygon(py, px)); - assertEquals(Relation.CELL_CROSSES_QUERY, polygon.relate(yMin, yMax, xMin, xMax)); - } - - public void testBoundingBox() throws Exception { - for (int i = 0; i < 100; i++) { - EdgeTree polygon = EdgeTree.create(nextPolygon()); - - for (int j = 0; j < 100; j++) { - double latitude = nextLatitude(); - double longitude = nextLongitude(); - // if the point is within poly, then it should be in our bounding box - if (polygon.contains(latitude, longitude)) { - assertTrue(latitude >= polygon.minLat && latitude <= polygon.maxLat); - assertTrue(longitude >= polygon.minLon && longitude <= polygon.maxLon); - } - } - } - } - - // targets the bounding box directly - public void testBoundingBoxEdgeCases() throws Exception { - for (int i = 0; i < 100; i++) { - Polygon polygon = nextPolygon(); - EdgeTree impl = EdgeTree.create(polygon); - - for (int j = 0; j < 100; j++) { - double point[] = GeoTestUtil.nextPointNear(polygon); - double latitude = point[0]; - double longitude = point[1]; - // if the point is within poly, then it should be in our bounding box - if (impl.contains(latitude, longitude)) { - assertTrue(latitude >= polygon.minLat() && latitude <= polygon.maxLat()); - assertTrue(longitude >= polygon.minLon() && longitude <= polygon.maxLon()); - } - } - } - } - - /** - * If polygon.contains(box) returns true, then any point in that box should return true as well - */ - public void testContainsRandom() throws Exception { - int iters = atLeast(50); - for (int i = 0; i < iters; i++) { - Polygon polygon = nextPolygon(); - EdgeTree impl = EdgeTree.create(polygon); - - for (int j = 0; j < 100; j++) { - Rectangle rectangle = GeoTestUtil.nextBoxNear(polygon); - // allowed to conservatively return false - if (impl.relate(rectangle.minLat, rectangle.maxLat, rectangle.minLon, rectangle.maxLon) == GeoShape.Relation.WITHIN) { - for (int k = 0; k < 500; k++) { - // this tests in our range but sometimes outside! so we have to double-check its really in other box - double point[] = GeoTestUtil.nextPointNear(rectangle); - double latitude = point[0]; - double longitude = point[1]; - // check for sure its in our box - if (latitude >= rectangle.minLat && latitude <= rectangle.maxLat && longitude >= rectangle.minLon && longitude <= rectangle.maxLon) { - assertTrue(impl.contains(latitude, longitude)); - } - } - for (int k = 0; k < 100; k++) { - // this tests in our range but sometimes outside! so we have to double-check its really in other box - double point[] = GeoTestUtil.nextPointNear(polygon); - double latitude = point[0]; - double longitude = point[1]; - // check for sure its in our box - if (latitude >= rectangle.minLat && latitude <= rectangle.maxLat && longitude >= rectangle.minLon && longitude <= rectangle.maxLon) { - assertTrue(impl.contains(latitude, longitude)); - } - } - } - } - } - } - - /** - * If polygon.contains(box) returns true, then any point in that box should return true as well - */ - // different from testContainsRandom in that its not a purely random test. we iterate the vertices of the polygon - // and generate boxes near each one of those to try to be more efficient. - public void testContainsEdgeCases() throws Exception { - for (int i = 0; i < 1000; i++) { - Polygon polygon = nextPolygon(); - EdgeTree impl = EdgeTree.create(polygon); - - for (int j = 0; j < 10; j++) { - Rectangle rectangle = GeoTestUtil.nextBoxNear(polygon); - // allowed to conservatively return false - if (impl.relate(rectangle.minLat, rectangle.maxLat, rectangle.minLon, rectangle.maxLon) == GeoShape.Relation.WITHIN) { - for (int k = 0; k < 100; k++) { - // this tests in our range but sometimes outside! so we have to double-check its really in other box - double point[] = GeoTestUtil.nextPointNear(rectangle); - double latitude = point[0]; - double longitude = point[1]; - // check for sure its in our box - if (latitude >= rectangle.minLat && latitude <= rectangle.maxLat && longitude >= rectangle.minLon && longitude <= rectangle.maxLon) { - assertTrue(impl.contains(latitude, longitude)); - } - } - for (int k = 0; k < 20; k++) { - // this tests in our range but sometimes outside! so we have to double-check its really in other box - double point[] = GeoTestUtil.nextPointNear(polygon); - double latitude = point[0]; - double longitude = point[1]; - // check for sure its in our box - if (latitude >= rectangle.minLat && latitude <= rectangle.maxLat && longitude >= rectangle.minLon && longitude <= rectangle.maxLon) { - assertTrue(impl.contains(latitude, longitude)); - } - } - } - } - } - } - - /** - * If polygon.intersects(box) returns false, then any point in that box should return false as well - */ - public void testIntersectRandom() { - int iters = atLeast(10); - for (int i = 0; i < iters; i++) { - Polygon polygon = nextPolygon(); - EdgeTree impl = EdgeTree.create(polygon); - - for (int j = 0; j < 100; j++) { - Rectangle rectangle = GeoTestUtil.nextBoxNear(polygon); - // allowed to conservatively return true. - if (impl.relate(rectangle.minLat, rectangle.maxLat, rectangle.minLon, rectangle.maxLon) == GeoShape.Relation.DISJOINT) { - for (int k = 0; k < 1000; k++) { - double point[] = GeoTestUtil.nextPointNear(rectangle); - // this tests in our range but sometimes outside! so we have to double-check its really in other box - double latitude = point[0]; - double longitude = point[1]; - // check for sure its in our box - if (latitude >= rectangle.minLat && latitude <= rectangle.maxLat && longitude >= rectangle.minLon && longitude <= rectangle.maxLon) { - assertFalse(impl.contains(latitude, longitude)); - } - } - for (int k = 0; k < 100; k++) { - double point[] = GeoTestUtil.nextPointNear(polygon); - // this tests in our range but sometimes outside! so we have to double-check its really in other box - double latitude = point[0]; - double longitude = point[1]; - // check for sure its in our box - if (latitude >= rectangle.minLat && latitude <= rectangle.maxLat && longitude >= rectangle.minLon && longitude <= rectangle.maxLon) { - assertFalse(impl.contains(latitude, longitude)); - } - } - } - } - } - } - - /** - * If polygon.intersects(box) returns false, then any point in that box should return false as well - */ - // different from testIntersectsRandom in that its not a purely random test. we iterate the vertices of the polygon - // and generate boxes near each one of those to try to be more efficient. - public void testIntersectEdgeCases() { - for (int i = 0; i < 100; i++) { - Polygon polygon = nextPolygon(); - EdgeTree impl = EdgeTree.create(polygon); - - for (int j = 0; j < 10; j++) { - Rectangle rectangle = GeoTestUtil.nextBoxNear(polygon); - // allowed to conservatively return false. - if (impl.relate(rectangle.minLat, rectangle.maxLat, rectangle.minLon, rectangle.maxLon) == GeoShape.Relation.DISJOINT) { - for (int k = 0; k < 100; k++) { - // this tests in our range but sometimes outside! so we have to double-check its really in other box - double point[] = GeoTestUtil.nextPointNear(rectangle); - double latitude = point[0]; - double longitude = point[1]; - // check for sure its in our box - if (latitude >= rectangle.minLat && latitude <= rectangle.maxLat && longitude >= rectangle.minLon && longitude <= rectangle.maxLon) { - assertFalse(impl.contains(latitude, longitude)); - } - } - for (int k = 0; k < 50; k++) { - // this tests in our range but sometimes outside! so we have to double-check its really in other box - double point[] = GeoTestUtil.nextPointNear(polygon); - double latitude = point[0]; - double longitude = point[1]; - // check for sure its in our box - if (latitude >= rectangle.minLat && latitude <= rectangle.maxLat && longitude >= rectangle.minLon && longitude <= rectangle.maxLon) { - assertFalse(impl.contains(latitude, longitude)); - } - } - } - } - } - } - - /** - * Tests edge case behavior with respect to insideness - */ - public void testEdgeInsideness() { - EdgeTree poly = EdgeTree.create(new Polygon(new double[]{-2, -2, 2, 2, -2}, new double[]{-2, 2, 2, -2, -2})); - assertTrue(poly.contains(-2, -2)); // bottom left corner: true - assertFalse(poly.contains(-2, 2)); // bottom right corner: false - assertFalse(poly.contains(2, -2)); // top left corner: false - assertFalse(poly.contains(2, 2)); // top right corner: false - assertTrue(poly.contains(-2, -1)); // bottom side: true - assertTrue(poly.contains(-2, 0)); // bottom side: true - assertTrue(poly.contains(-2, 1)); // bottom side: true - assertFalse(poly.contains(2, -1)); // top side: false - assertFalse(poly.contains(2, 0)); // top side: false - assertFalse(poly.contains(2, 1)); // top side: false - assertFalse(poly.contains(-1, 2)); // right side: false - assertFalse(poly.contains(0, 2)); // right side: false - assertFalse(poly.contains(1, 2)); // right side: false - assertTrue(poly.contains(-1, -2)); // left side: true - assertTrue(poly.contains(0, -2)); // left side: true - assertTrue(poly.contains(1, -2)); // left side: true - } - - /** - * Tests current impl against original algorithm - */ - public void testContainsAgainstOriginal() { - int iters = atLeast(100); - for (int i = 0; i < iters; i++) { - Polygon polygon = nextPolygon(); - // currently we don't generate these, but this test does not want holes. - while (polygon.getHoles().length > 0) { - polygon = nextPolygon(); - } - EdgeTree impl = EdgeTree.create(polygon); - - // random lat/lons against polygon - for (int j = 0; j < 1000; j++) { - double point[] = GeoTestUtil.nextPointNear(polygon); - double latitude = point[0]; - double longitude = point[1]; - boolean expected = GeoTestUtil.containsSlowly(polygon, latitude, longitude); - assertEquals(expected, impl.contains(latitude, longitude)); - } - } - } -} diff --git a/libs/geo/src/test/java/org/elasticsearch/geo/geometry/TestLine.java b/libs/geo/src/test/java/org/elasticsearch/geo/geometry/TestLine.java deleted file mode 100644 index 02d01394aee94..0000000000000 --- a/libs/geo/src/test/java/org/elasticsearch/geo/geometry/TestLine.java +++ /dev/null @@ -1,114 +0,0 @@ -/* - * 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.geo.geometry; - -import org.apache.lucene.geo.GeoTestUtil; -import org.apache.lucene.geo.GeoUtils; -import org.apache.lucene.geo.geometry.GeoShape.Relation; -import org.junit.Ignore; - -import java.util.Arrays; - -import static com.carrotsearch.randomizedtesting.generators.RandomNumbers.randomIntBetween; -import static org.apache.lucene.geo.GeoTestUtil.nextLatitudeIn; -import static org.apache.lucene.geo.GeoTestUtil.nextLongitudeIn; - -/** - * Tests relations and features of Line types - */ -public class TestLine extends TestMultiPoint { - @Override - public Line getShape() { - return getShape(false); - } - - @Override - public Line getShape(boolean padded) { - double minFactor = padded == true ? 20D : 0D; - double maxFactor = padded == true ? -20D : 0D; - - // we can't have self crossing lines. - // since polygons share this contract we create an unclosed polygon - Polygon poly = GeoTestUtil.createRegularPolygon( - nextLatitudeIn(GeoUtils.MIN_LAT_INCL + minFactor, GeoUtils.MAX_LAT_INCL + maxFactor), - nextLongitudeIn(GeoUtils.MIN_LON_INCL + minFactor, GeoUtils.MAX_LON_INCL + maxFactor), - 100D, randomIntBetween(random(), 4, 100)); - final double[] lats = poly.getPolyLats(); - final double[] lons = poly.getPolyLons(); - return new Line(Arrays.copyOfRange(lats, 0, lats.length - 1), - Arrays.copyOfRange(lons, 0, lons.length - 1)); - } - - /** - * tests the bounding box of the line - */ - @Override - public void testBoundingBox() { - // lines are just MultiPoint with an edge tree; bounding box logic is the same - super.testBoundingBox(); - } - - /** - * tests the "center" of the line - */ - @Override - public void testCenter() { - // lines are just MultiPoint with an edge tree; center logic is the same - super.testCenter(); - } - - /** - * tests MultiPoints are within a box - */ - @Override - public void testWithin() { - relationTest(getShape(true), Relation.WITHIN); - } - - /** - * tests box is disjoint with a MultiPoint shape - */ - @Override - public void testDisjoint() { - relationTest(getShape(true), Relation.DISJOINT); - } - - /** - * IGNORED: Line does not contain other shapes - */ - @Ignore - @Override - public void testContains() { - } - - /** - * tests box intersection with a MultiPoint shape - */ - @Override - public void testIntersects() { - MultiPoint points = getShape(true); - Line line = new Line(points.getLats(), points.getLons()); - double minLat = StrictMath.min(line.getLat(0), line.getLat(1)); - double maxLat = StrictMath.max(line.getLat(0), line.getLat(1)); - double minLon = StrictMath.min(line.getLon(0), line.getLon(1)); - double maxLon = StrictMath.max(line.getLon(0), line.getLon(1)); - assertEquals(Relation.INTERSECTS, line.relate(minLat, maxLat, minLon, maxLon)); - } -} diff --git a/libs/geo/src/test/java/org/elasticsearch/geo/geometry/TestMultiLine.java b/libs/geo/src/test/java/org/elasticsearch/geo/geometry/TestMultiLine.java deleted file mode 100644 index 8861dced5e560..0000000000000 --- a/libs/geo/src/test/java/org/elasticsearch/geo/geometry/TestMultiLine.java +++ /dev/null @@ -1,115 +0,0 @@ -/* - * 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.geo.geometry; - -import org.apache.lucene.geo.GeoTestUtil; -import org.apache.lucene.geo.GeoUtils; -import org.apache.lucene.geo.geometry.GeoShape.Relation; -import org.junit.Ignore; - -import java.util.Arrays; - -import static com.carrotsearch.randomizedtesting.generators.RandomNumbers.randomIntBetween; -import static org.apache.lucene.geo.GeoTestUtil.nextLatitudeIn; -import static org.apache.lucene.geo.GeoTestUtil.nextLongitudeIn; - -/** - * Tests relations and features of MultiLine types - */ -public class TestMultiLine extends BaseGeometryTestCase { - @Override - public MultiLine getShape() { - return getShape(false); - } - - public MultiLine getShape(boolean padded) { - double minFactor = padded == true ? 20D : 0D; - double maxFactor = padded == true ? -20D : 0D; - int numLines = randomIntBetween(random(), 1, 10); - Line[] lines = new Line[numLines]; - - // we can't have self crossing lines. - // since polygons share this contract we create an unclosed polygon - Polygon poly; - double[] lats; - double[] lons; - - for (int i = 0; i < numLines; ++i) { - poly = GeoTestUtil.createRegularPolygon( - nextLatitudeIn(GeoUtils.MIN_LAT_INCL + minFactor, GeoUtils.MAX_LAT_INCL + maxFactor), - nextLongitudeIn(GeoUtils.MIN_LON_INCL + minFactor, GeoUtils.MAX_LON_INCL + maxFactor), - 100D, randomIntBetween(random(), 4, 100)); - lats = poly.getPolyLats(); - lons = poly.getPolyLons(); - lines[i] = new Line(Arrays.copyOfRange(lats, 0, lats.length - 1), - Arrays.copyOfRange(lons, 0, lons.length - 1)); - } - - return new MultiLine(lines); - } - - @Override - public void testBoundingBox() { - MultiLine lines = getShape(true); - double minLat = Double.POSITIVE_INFINITY; - double maxLat = Double.NEGATIVE_INFINITY; - double minLon = Double.POSITIVE_INFINITY; - double maxLon = Double.NEGATIVE_INFINITY; - for (int l = 0; l < lines.length(); ++l) { - Line line = lines.get(l); - for (int j = 0; j < line.numPoints(); ++j) { - minLat = StrictMath.min(minLat, line.getLat(j)); - maxLat = StrictMath.max(maxLat, line.getLat(j)); - minLon = StrictMath.min(minLon, line.getLon(j)); - maxLon = StrictMath.max(maxLon, line.getLon(j)); - } - } - - Rectangle bbox = new Rectangle(minLat, maxLat, minLon, maxLon); - assertEquals(bbox, lines.getBoundingBox()); - } - - @Override - public void testWithin() { - relationTest(getShape(true), Relation.WITHIN); - } - - @Ignore - @Override - public void testContains() { - // CONTAINS not supported with MultiLine - } - - @Override - public void testDisjoint() { - relationTest(getShape(true), Relation.DISJOINT); - } - - @Override - public void testIntersects() { - MultiLine lines = getShape(true); - Line line = lines.get(0); - double minLat = StrictMath.min(line.getLat(0), line.getLat(1)); - double maxLat = StrictMath.max(line.getLat(0), line.getLat(1)); - double minLon = StrictMath.min(line.getLon(0), line.getLon(1)); - double maxLon = StrictMath.max(line.getLon(0), line.getLon(1)); - assertEquals(Relation.INTERSECTS, lines.relate(minLat, maxLat, minLon, maxLon)); - } -} diff --git a/libs/geo/src/test/java/org/elasticsearch/geo/geometry/TestMultiPoint.java b/libs/geo/src/test/java/org/elasticsearch/geo/geometry/TestMultiPoint.java deleted file mode 100644 index 2d3f2fa112370..0000000000000 --- a/libs/geo/src/test/java/org/elasticsearch/geo/geometry/TestMultiPoint.java +++ /dev/null @@ -1,117 +0,0 @@ -/* - * 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.geo.geometry; - -import org.apache.lucene.geo.GeoTestUtil; -import org.apache.lucene.geo.GeoUtils; -import org.apache.lucene.geo.geometry.GeoShape.Relation; -import org.junit.Ignore; - -import static com.carrotsearch.randomizedtesting.generators.RandomNumbers.randomIntBetween; - -/** - * Tests relations and features of MultiPoint types - */ -public class TestMultiPoint extends BaseGeometryTestCase { - /** - * returns a MultiPoint shape - */ - @Override - public MultiPoint getShape() { - return getShape(false); - } - - /** - * returns a MultiPoint shape within a small area; - * ensures MultiPoints do not cover entire globe. - * Used for testing INTERSECTS and DISJOINT queries - */ - protected MultiPoint getShape(boolean padded) { - int numPoints = randomIntBetween(random(), 2, 100); - double[] lats = new double[numPoints]; - double[] lons = new double[numPoints]; - for (int i = 0; i < numPoints; ++i) { - if (padded == true) { - lats[i] = GeoTestUtil.nextLatitudeIn(GeoUtils.MIN_LAT_INCL + 20D, GeoUtils.MAX_LAT_INCL - 20D); - lons[i] = GeoTestUtil.nextLongitudeIn(GeoUtils.MIN_LON_INCL + 20D, GeoUtils.MAX_LON_INCL - 20D); - } else { - lats[i] = GeoTestUtil.nextLatitude(); - lons[i] = GeoTestUtil.nextLongitude(); - } - } - return new MultiPoint(lats, lons); - } - - @Override - public void testWithin() { - relationTest(getShape(true), Relation.WITHIN); - } - - @Ignore - @Override - public void testContains() { - // MultiPoint does not support CONTAINS - } - - @Override - public void testDisjoint() { - // this is a simple test where we build a bounding box that is disjoint - // from the MultiPoint bounding box - // note: we should add a test where a box is between points - relationTest(getShape(true), Relation.DISJOINT); - } - - @Override - public void testIntersects() { - MultiPoint points = getShape(true); - double minLat = StrictMath.min(points.getLat(0), points.getLat(1)); - double maxLat = StrictMath.max(points.getLat(0), points.getLat(1)); - double minLon = StrictMath.min(points.getLon(0), points.getLon(1)); - double maxLon = StrictMath.max(points.getLon(0), points.getLon(1)); - Relation r = Relation.DISJOINT; - for (Point p : points) { - if ((r = p.relate(minLat, maxLat, minLon, maxLon)) == Relation.INTERSECTS) { - break; - } - } - assertEquals(Relation.INTERSECTS, r); - } - - /** - * tests bounding box of MultiPoint shape type - */ - @Override - public void testBoundingBox() { - MultiPoint points = getShape(true); - double minLat = Double.POSITIVE_INFINITY; - double maxLat = Double.NEGATIVE_INFINITY; - double minLon = Double.POSITIVE_INFINITY; - double maxLon = Double.NEGATIVE_INFINITY; - for (int i = 0; i < points.numPoints(); ++i) { - minLat = StrictMath.min(minLat, points.getLat(i)); - maxLat = StrictMath.max(maxLat, points.getLat(i)); - minLon = StrictMath.min(minLon, points.getLon(i)); - maxLon = StrictMath.max(maxLon, points.getLon(i)); - } - - Rectangle bbox = new Rectangle(minLat, maxLat, minLon, maxLon); - assertEquals(bbox, points.getBoundingBox()); - } -} diff --git a/libs/geo/src/test/java/org/elasticsearch/geo/geometry/TestMultiPolygon.java b/libs/geo/src/test/java/org/elasticsearch/geo/geometry/TestMultiPolygon.java deleted file mode 100644 index fcf24bb29bf2e..0000000000000 --- a/libs/geo/src/test/java/org/elasticsearch/geo/geometry/TestMultiPolygon.java +++ /dev/null @@ -1,136 +0,0 @@ -/* - * 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.geo.geometry; - -import org.apache.lucene.geo.GeoTestUtil; -import org.apache.lucene.geo.GeoUtils; -import org.apache.lucene.geo.geometry.GeoShape.Relation; - -import static com.carrotsearch.randomizedtesting.generators.RandomNumbers.randomIntBetween; -import static org.apache.lucene.geo.GeoTestUtil.nextBoxNotCrossingDateline; -import static org.apache.lucene.geo.GeoTestUtil.nextLatitudeIn; -import static org.apache.lucene.geo.GeoTestUtil.nextLongitudeIn; -import static org.apache.lucene.geo.GeoTestUtil.nextPolygon; - -/** - * Tests relations and features of MultiPolygon types - */ -public class TestMultiPolygon extends BaseGeometryTestCase { - @Override - public MultiPolygon getShape() { - int numPolys = randomIntBetween(random(), 2, 100); - Polygon[] polygons = new Polygon[numPolys]; - for (int i = 0; i < numPolys; ++i) { - polygons[i] = nextPolygon(); - } - return new MultiPolygon(polygons); - } - - protected MultiPolygon getShape(boolean padded) { - double minFactor = padded == true ? 20D : 0D; - double maxFactor = padded == true ? -20D : 0D; - int numPolys = randomIntBetween(random(), 1, 10); - Polygon[] polygons = new Polygon[numPolys]; - - // we can't have self crossing lines. - // since polygons share this contract we create an unclosed polygon - for (int i = 0; i < numPolys; ++i) { - polygons[i] = GeoTestUtil.createRegularPolygon( - nextLatitudeIn(GeoUtils.MIN_LAT_INCL + minFactor, GeoUtils.MAX_LAT_INCL + maxFactor), - nextLongitudeIn(GeoUtils.MIN_LON_INCL + minFactor, GeoUtils.MAX_LON_INCL + maxFactor), - 10000D, randomIntBetween(random(), 4, 100)); - } - - return new MultiPolygon(polygons); - } - - /** - * tests area of MultiPolygon using simple random boxes - */ - @Override - public void testArea() { - int numPolys = randomIntBetween(random(), 2, 10); - Rectangle box; - Polygon[] polygon = new Polygon[numPolys]; - double width, height; - double area = 0; - for (int i = 0; i < numPolys; ++i) { - box = nextBoxNotCrossingDateline(); - polygon[i] = new Polygon( - new double[]{box.minLat(), box.minLat(), box.maxLat(), box.maxLat(), box.minLat()}, - new double[]{box.minLon(), box.maxLon(), box.maxLon(), box.minLon(), box.minLon()}); - width = box.maxLon() - box.minLon(); - height = box.maxLat() - box.minLat(); - area += width * height; - } - MultiPolygon polygons = new MultiPolygon(polygon); - assertEquals(area, polygons.getArea(), 1E-10D); - } - - @Override - public void testBoundingBox() { - MultiPolygon polygons = getShape(true); - double minLat = Double.POSITIVE_INFINITY; - double maxLat = Double.NEGATIVE_INFINITY; - double minLon = Double.POSITIVE_INFINITY; - double maxLon = Double.NEGATIVE_INFINITY; - Polygon p; - for (int i = 0; i < polygons.length(); ++i) { - p = polygons.get(i); - for (int j = 0; j < p.numPoints(); ++j) { - minLat = Math.min(p.getLat(j), minLat); - maxLat = Math.max(p.getLat(j), maxLat); - minLon = Math.min(p.getLon(j), minLon); - maxLon = Math.max(p.getLon(j), maxLon); - } - } - Rectangle bbox = new Rectangle(minLat, maxLat, minLon, maxLon); - assertEquals(bbox, polygons.getBoundingBox()); - } - - @Override - public void testWithin() { - relationTest(getShape(true), Relation.WITHIN); - } - - @Override - public void testContains() { - MultiPolygon polygons = getShape(true); - Point center = polygons.get(0).getCenter(); - Rectangle box = new Rectangle(center.lat() - 1E-3D, center.lat() + 1E-3D, center.lon() - 1E-3D, center.lon() + 1E-3D); - assertEquals(Relation.CONTAINS, polygons.relate(box.minLat(), box.maxLat(), box.minLon(), box.maxLon())); - } - - @Override - public void testDisjoint() { - relationTest(getShape(true), Relation.DISJOINT); - } - - @Override - public void testIntersects() { - MultiPolygon polygons = getShape(true); - Polygon polygon = polygons.get(0); - double minLat = StrictMath.min(polygon.getLat(0), polygon.getLat(1)); - double maxLat = StrictMath.max(polygon.getLat(0), polygon.getLat(1)); - double minLon = StrictMath.min(polygon.getLon(0), polygon.getLon(1)); - double maxLon = StrictMath.max(polygon.getLon(0), polygon.getLon(1)); - assertEquals(Relation.INTERSECTS, polygon.relate(minLat, maxLat, minLon, maxLon)); - } -} diff --git a/libs/geo/src/test/java/org/elasticsearch/geo/geometry/TestPoint.java b/libs/geo/src/test/java/org/elasticsearch/geo/geometry/TestPoint.java deleted file mode 100644 index 68233f3a72579..0000000000000 --- a/libs/geo/src/test/java/org/elasticsearch/geo/geometry/TestPoint.java +++ /dev/null @@ -1,105 +0,0 @@ -/* - * 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.geo.geometry; - -import org.apache.lucene.geo.GeoTestUtil; -import org.apache.lucene.geo.GeoUtils; -import org.apache.lucene.geo.geometry.GeoShape.Relation; -import org.junit.Ignore; - -import static org.apache.lucene.geo.GeoTestUtil.nextLatitudeIn; -import static org.apache.lucene.geo.GeoTestUtil.nextLongitudeIn; - -/** - * Tests relations and features of simple Point types - */ -public class TestPoint extends BaseGeometryTestCase { - @Override - public Point getShape() { - return new Point(GeoTestUtil.nextLatitude(), GeoTestUtil.nextLongitude()); - } - - @Ignore - @Override - public void testWithin() { - // for Point types WITHIN == INTERSECTS; so ingore this test - } - - @Ignore - @Override - public void testContains() { - // points do not contain other shapes; ignore this test - } - - @Override - public void testDisjoint() { - Point pt = getShape(); - - double minLat = pt.lat(); - double maxLat = pt.lat(); - double minLon = pt.lon(); - double maxLon = pt.lon(); - - // ensure point latitude is outside of box (with pole as boundary) - if (pt.lat() <= GeoUtils.MIN_LAT_INCL + 1D) { - minLat = GeoUtils.MIN_LAT_INCL + 2D; - maxLat = nextLatitudeIn(minLat, GeoUtils.MAX_LAT_INCL); - } else if (pt.lat() >= GeoUtils.MAX_LAT_INCL - 1D) { - maxLat -= 2D; - minLat = nextLatitudeIn(GeoUtils.MIN_LAT_INCL, maxLat); - } else { - minLat += 1D; - maxLat = nextLatitudeIn(minLat, GeoUtils.MAX_LAT_INCL); - } - - // ensure point longitude is disjoint with box (with dateline as boundary) - if (pt.lon() <= GeoUtils.MIN_LON_INCL + 1D) { - minLon = GeoUtils.MIN_LON_INCL + 2D; - maxLon = nextLongitudeIn(minLon, GeoUtils.MAX_LON_INCL); - } else if (pt.lon() >= GeoUtils.MAX_LON_INCL - 1D) { - maxLon -= 2D; - minLon = nextLongitudeIn(GeoUtils.MIN_LON_INCL, maxLon); - } else { - minLon += 1D; - maxLon = nextLongitudeIn(minLon, GeoUtils.MAX_LON_INCL); - } - - Rectangle box = new Rectangle(minLat, maxLat, minLon, maxLon); - assertEquals(Relation.DISJOINT, pt.relate(box.minLat(), box.maxLat(), box.minLon(), box.maxLon())); - } - - @Override - public void testIntersects() { - Rectangle box = GeoTestUtil.nextBoxNotCrossingDateline(); - Point pt = new Point(nextLatitudeIn(box.minLat(), box.maxLat()), nextLongitudeIn(box.minLon(), box.maxLon())); - assertEquals(Relation.INTERSECTS, pt.relate(box.minLat(), box.maxLat(), box.minLon(), box.maxLon())); - } - - @Override - public void testBoundingBox() { - expectThrows(UnsupportedOperationException.class, () -> getShape().getBoundingBox()); - } - - @Override - public void testCenter() { - Point pt = getShape(); - assertEquals(pt, new Point(pt.lat(), pt.lon())); - } -} diff --git a/libs/geo/src/test/java/org/elasticsearch/geo/geometry/TestPolygon.java b/libs/geo/src/test/java/org/elasticsearch/geo/geometry/TestPolygon.java deleted file mode 100644 index 8763bc5271c1c..0000000000000 --- a/libs/geo/src/test/java/org/elasticsearch/geo/geometry/TestPolygon.java +++ /dev/null @@ -1,185 +0,0 @@ -/* - * 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.geo.geometry; - -import org.apache.lucene.geo.GeoTestUtil; -import org.apache.lucene.geo.GeoUtils; -import org.apache.lucene.geo.geometry.GeoShape.Relation; - -import static com.carrotsearch.randomizedtesting.generators.RandomNumbers.randomIntBetween; -import static org.apache.lucene.geo.GeoTestUtil.nextBoxNotCrossingDateline; -import static org.apache.lucene.geo.GeoTestUtil.nextLatitudeIn; -import static org.apache.lucene.geo.GeoTestUtil.nextLongitudeIn; - -/** - * Tests relations and features of Polygon types - */ -public class TestPolygon extends BaseGeometryTestCase { - - @Override - public Polygon getShape() { - return GeoTestUtil.nextPolygon(); - } - - public Polygon getShape(boolean padded) { - double minFactor = padded == true ? 20D : 0D; - double maxFactor = padded == true ? -20D : 0D; - - // we can't have self crossing lines. - // since polygons share this contract we create an unclosed polygon - Polygon poly = GeoTestUtil.createRegularPolygon( - nextLatitudeIn(GeoUtils.MIN_LAT_INCL + minFactor, GeoUtils.MAX_LAT_INCL + maxFactor), - nextLongitudeIn(GeoUtils.MIN_LON_INCL + minFactor, GeoUtils.MAX_LON_INCL + maxFactor), - 10000D, randomIntBetween(random(), 4, 100)); - return poly; - } - - /** - * tests area of a Polygon type using a simple random box - */ - @Override - public void testArea() { - // test with simple random box - Rectangle box = nextBoxNotCrossingDateline(); - Polygon polygon = new Polygon( - new double[]{box.minLat(), box.minLat(), box.maxLat(), box.maxLat(), box.minLat()}, - new double[]{box.minLon(), box.maxLon(), box.maxLon(), box.minLon(), box.minLon()}); - double width = box.maxLon() - box.minLon(); - double height = box.maxLat() - box.minLat(); - double area = width * height; - - assertEquals(area, polygon.getArea(), 1E-10D); - } - - @Override - public void testBoundingBox() { - Polygon polygon = getShape(); - double minLat = Double.POSITIVE_INFINITY; - double maxLat = Double.NEGATIVE_INFINITY; - double minLon = Double.POSITIVE_INFINITY; - double maxLon = Double.NEGATIVE_INFINITY; - // shell of polygon - Line shell = new Line(polygon.getPolyLats(), polygon.getPolyLons()); - for (int i = 0; i < shell.numPoints(); ++i) { - minLat = StrictMath.min(minLat, shell.getLat(i)); - maxLat = StrictMath.max(maxLat, shell.getLat(i)); - minLon = StrictMath.min(minLon, shell.getLon(i)); - maxLon = StrictMath.max(maxLon, shell.getLon(i)); - } - - Rectangle bbox = new Rectangle(minLat, maxLat, minLon, maxLon); - assertEquals(bbox, polygon.getBoundingBox()); - } - - @Override - public void testWithin() { - relationTest(getShape(true), Relation.WITHIN); - } - - @Override - public void testContains() { - Polygon polygon = getShape(true); - Point center = polygon.getCenter(); - Rectangle box = new Rectangle(center.lat() - 1E-3D, center.lat() + 1E-3D, center.lon() - 1E-3D, center.lon() + 1E-3D); - assertEquals(Relation.CONTAINS, polygon.relate(box.minLat(), box.maxLat(), box.minLon(), box.maxLon())); - } - - @Override - public void testDisjoint() { - relationTest(getShape(true), Relation.DISJOINT); - } - - @Override - public void testIntersects() { - Polygon polygon = getShape(true); - double minLat = StrictMath.min(polygon.getLat(0), polygon.getLat(1)); - double maxLat = StrictMath.max(polygon.getLat(0), polygon.getLat(1)); - double minLon = StrictMath.min(polygon.getLon(0), polygon.getLon(1)); - double maxLon = StrictMath.max(polygon.getLon(0), polygon.getLon(1)); - assertEquals(Relation.INTERSECTS, polygon.relate(minLat, maxLat, minLon, maxLon)); - } - - public void testPacMan() { - // pacman - double[] px = {0, 10, 10, 0, -8, -10, -8, 0, 10, 10, 0}; - double[] py = {0, 5, 9, 10, 9, 0, -9, -10, -9, -5, 0}; - - Polygon polygon = new Polygon(py, px); - - // candidate crosses cell - double minLon = 2; - double maxLon = 11; - double minLat = -1; - double maxLat = 1; - - // test cell crossing poly - assertEquals(Relation.INTERSECTS, polygon.relate(minLat, maxLat, minLon, maxLon)); - } - - /** - * null polyLats not allowed - */ - public void testPolygonNullPolyLats() { - IllegalArgumentException expected = expectThrows(IllegalArgumentException.class, () -> { - new Polygon(null, new double[]{-66, -65, -65, -66, -66}); - }); - assertTrue(expected.getMessage().contains("lats must not be null")); - } - - /** - * null polyLons not allowed - */ - public void testPolygonNullPolyLons() { - IllegalArgumentException expected = expectThrows(IllegalArgumentException.class, () -> { - new Polygon(new double[]{18, 18, 19, 19, 18}, null); - }); - assertTrue(expected.getMessage().contains("lons must not be null")); - } - - /** - * polygon needs at least 3 vertices - */ - public void testPolygonLine() { - IllegalArgumentException expected = expectThrows(IllegalArgumentException.class, () -> { - new Polygon(new double[]{18, 18, 18}, new double[]{-66, -65, -66}); - }); - assertTrue(expected.getMessage().contains("at least 4 polygon points required")); - } - - /** - * polygon needs same number of latitudes as longitudes - */ - public void testPolygonBogus() { - IllegalArgumentException expected = expectThrows(IllegalArgumentException.class, () -> { - new Polygon(new double[]{18, 18, 19, 19}, new double[]{-66, -65, -65, -66, -66}); - }); - assertTrue(expected.getMessage().contains("must be equal length")); - } - - /** - * polygon must be closed - */ - public void testPolygonNotClosed() { - IllegalArgumentException expected = expectThrows(IllegalArgumentException.class, () -> { - new Polygon(new double[]{18, 18, 19, 19, 19}, new double[]{-66, -65, -65, -66, -67}); - }); - assertTrue(expected.getMessage(), expected.getMessage().contains("it must close itself")); - } -} From 937e289b50122d3fa4b3825f4d48e7375a455fc0 Mon Sep 17 00:00:00 2001 From: Igor Motov Date: Wed, 12 Dec 2018 12:25:55 +0400 Subject: [PATCH 03/10] Address @atorok's review comments --- libs/geo/build.gradle | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/libs/geo/build.gradle b/libs/geo/build.gradle index 29df20678012a..3ee562c48dbf4 100644 --- a/libs/geo/build.gradle +++ b/libs/geo/build.gradle @@ -32,14 +32,7 @@ publishing { } dependencies { - compile "org.elasticsearch:elasticsearch-core:${version}" - - - testCompile "com.carrotsearch.randomizedtesting:randomizedtesting-runner:${versions.randomizedrunner}" - testCompile "junit:junit:${versions.junit}" - testCompile "org.hamcrest:hamcrest-all:${versions.hamcrest}" - - if (isEclipse == false || project.path == ":libs:x-content-tests") { + if (isEclipse == false || project.path == ":libs:x-geo-tests") { testCompile("org.elasticsearch.test:framework:${version}") { exclude group: 'org.elasticsearch', module: 'elasticsearch-geo' } @@ -65,5 +58,3 @@ if (isEclipse) { } } } - -jarHell.enabled = false From 01cae2005789d523686cee96f72887df1d05f802 Mon Sep 17 00:00:00 2001 From: Igor Motov Date: Thu, 13 Dec 2018 15:33:24 +0400 Subject: [PATCH 04/10] Address @atorok's 2nd review --- libs/geo/build.gradle | 2 +- settings.gradle | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/libs/geo/build.gradle b/libs/geo/build.gradle index 3ee562c48dbf4..f32c19c8eddea 100644 --- a/libs/geo/build.gradle +++ b/libs/geo/build.gradle @@ -32,7 +32,7 @@ publishing { } dependencies { - if (isEclipse == false || project.path == ":libs:x-geo-tests") { + if (isEclipse == false || project.path == ":libs:geo-tests") { testCompile("org.elasticsearch.test:framework:${version}") { exclude group: 'org.elasticsearch', module: 'elasticsearch-geo' } diff --git a/settings.gradle b/settings.gradle index 43313f7236cb7..c6865041d9278 100644 --- a/settings.gradle +++ b/settings.gradle @@ -95,6 +95,7 @@ if (isEclipse) { projects << 'libs:x-content-tests' projects << 'libs:secure-sm-tests' projects << 'libs:grok-tests' + projects << 'libs:geo-tests' } include projects.toArray(new String[0]) From a5acea29915499cae348d280470f7f53c7825863 Mon Sep 17 00:00:00 2001 From: Igor Motov Date: Fri, 14 Dec 2018 22:44:43 +0400 Subject: [PATCH 05/10] Fix geo-test configuration for eclipse --- settings.gradle | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/settings.gradle b/settings.gradle index c6865041d9278..2b98502cfe7e1 100644 --- a/settings.gradle +++ b/settings.gradle @@ -131,7 +131,8 @@ if (isEclipse) { project(":libs:grok").buildFileName = 'eclipse-build.gradle' project(":libs:grok-tests").projectDir = new File(rootProject.projectDir, 'libs/grok/src/test') project(":libs:grok-tests").buildFileName = 'eclipse-build.gradle' -} + project(":libs:geo-tests").projectDir = new File(rootProject.projectDir, 'libs/geo/src/test') + project(":libs:geo-tests").buildFileName = 'eclipse-build.gradle'} // look for extra plugins for elasticsearch File extraProjects = new File(rootProject.projectDir.parentFile, "${dirName}-extra") From bb491d8bf81da918ede8d4f73faeebeb19b50059 Mon Sep 17 00:00:00 2001 From: Igor Motov Date: Wed, 2 Jan 2019 09:40:53 -0500 Subject: [PATCH 06/10] Move project name setting into settings.gradle --- libs/geo/build.gradle | 11 ----------- settings.gradle | 3 +++ 2 files changed, 3 insertions(+), 11 deletions(-) diff --git a/libs/geo/build.gradle b/libs/geo/build.gradle index f32c19c8eddea..ab3419b93b9b8 100644 --- a/libs/geo/build.gradle +++ b/libs/geo/build.gradle @@ -21,23 +21,12 @@ apply plugin: 'elasticsearch.build' apply plugin: 'nebula.maven-base-publish' apply plugin: 'nebula.maven-scm' -archivesBaseName = 'elasticsearch-geo' - -publishing { - publications { - nebula { - artifactId = archivesBaseName - } - } -} - dependencies { if (isEclipse == false || project.path == ":libs:geo-tests") { testCompile("org.elasticsearch.test:framework:${version}") { exclude group: 'org.elasticsearch', module: 'elasticsearch-geo' } } - } forbiddenApisMain { diff --git a/settings.gradle b/settings.gradle index 2b98502cfe7e1..40fb419b0bf3c 100644 --- a/settings.gradle +++ b/settings.gradle @@ -131,6 +131,8 @@ if (isEclipse) { project(":libs:grok").buildFileName = 'eclipse-build.gradle' project(":libs:grok-tests").projectDir = new File(rootProject.projectDir, 'libs/grok/src/test') project(":libs:grok-tests").buildFileName = 'eclipse-build.gradle' + project(":libs:geo").projectDir = new File(rootProject.projectDir, 'libs/geo/src/main') + project(":libs:geo").buildFileName = 'eclipse-build.gradle' project(":libs:geo-tests").projectDir = new File(rootProject.projectDir, 'libs/geo/src/test') project(":libs:geo-tests").buildFileName = 'eclipse-build.gradle'} @@ -143,3 +145,4 @@ if (extraProjects.exists()) { } project(":libs:cli").name = 'elasticsearch-cli' +project(":libs:geo").name = 'elasticsearch-geo' From 7fdddb621150d877f0dc5a26ee2299b19c83150a Mon Sep 17 00:00:00 2001 From: Igor Motov Date: Mon, 7 Jan 2019 14:03:26 -0500 Subject: [PATCH 07/10] Add test for Geometry Visitor --- .../geo/geometry/BaseGeometryTestCase.java | 69 +++++++++++++++++++ .../geo/geometry/LinearRingTests.java | 4 ++ 2 files changed, 73 insertions(+) diff --git a/libs/geo/src/test/java/org/elasticsearch/geo/geometry/BaseGeometryTestCase.java b/libs/geo/src/test/java/org/elasticsearch/geo/geometry/BaseGeometryTestCase.java index 7dc1938a67c01..3ffabd2134323 100644 --- a/libs/geo/src/test/java/org/elasticsearch/geo/geometry/BaseGeometryTestCase.java +++ b/libs/geo/src/test/java/org/elasticsearch/geo/geometry/BaseGeometryTestCase.java @@ -29,6 +29,7 @@ import java.text.ParseException; import java.util.ArrayList; import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.Supplier; abstract class BaseGeometryTestCase extends AbstractWireTestCase { @@ -50,6 +51,74 @@ protected T copyInstance(T instance, Version version) throws IOException { } } + public void testVisitor() { + testVisitor(createTestInstance()); + } + + public static void testVisitor(Geometry geom) { + AtomicBoolean called = new AtomicBoolean(false); + Object result = geom.visit(new GeometryVisitor() { + private Object verify(Geometry geometry, String expectedClass) { + assertFalse("Visitor should be called only once", called.getAndSet(true)); + assertSame(geom, geometry); + assertEquals(geometry.getClass().getName(), "org.elasticsearch.geo.geometry." + expectedClass); + return "result"; + } + + @Override + public Object visit(Circle circle) { + return verify(circle, "Circle"); + } + + @Override + public Object visit(GeometryCollection collection) { + return verify(collection, "GeometryCollection"); } + + @Override + public Object visit(Line line) { + return verify(line, "Line"); + } + + @Override + public Object visit(LinearRing ring) { + return verify(ring, "LinearRing"); + } + + @Override + public Object visit(MultiLine multiLine) { + return verify(multiLine, "MultiLine"); + } + + @Override + public Object visit(MultiPoint multiPoint) { + return verify(multiPoint, "MultiPoint"); + } + + @Override + public Object visit(MultiPolygon multiPolygon) { + return verify(multiPolygon, "MultiPolygon"); + } + + @Override + public Object visit(Point point) { + return verify(point, "Point"); + } + + @Override + public Object visit(Polygon polygon) { + return verify(polygon, "Polygon"); + } + + @Override + public Object visit(Rectangle rectangle) { + return verify(rectangle, "Rectangle"); + } + }); + + assertTrue("visitor wasn't called", called.get()); + assertEquals("result", result); + } + public static double randomLat() { return randomDoubleBetween(-90, 90, true); } diff --git a/libs/geo/src/test/java/org/elasticsearch/geo/geometry/LinearRingTests.java b/libs/geo/src/test/java/org/elasticsearch/geo/geometry/LinearRingTests.java index 94d6449845359..73f6ac9a2f97a 100644 --- a/libs/geo/src/test/java/org/elasticsearch/geo/geometry/LinearRingTests.java +++ b/libs/geo/src/test/java/org/elasticsearch/geo/geometry/LinearRingTests.java @@ -45,4 +45,8 @@ public void testInitValidation() { ex = expectThrows(IllegalArgumentException.class, () -> new LinearRing(new double[]{1, 100, 3, 1}, new double[]{3, 4, 5, 3})); assertEquals("invalid latitude 100.0; must be between -90.0 and 90.0", ex.getMessage()); } + + public void testVisitor() { + BaseGeometryTestCase.testVisitor(new LinearRing(new double[]{1, 2, 3, 1}, new double[]{3, 4, 5, 3})); + } } From f31d10cabbd5e8b915ebddf24ebd47b1ecadf859 Mon Sep 17 00:00:00 2001 From: Igor Motov Date: Mon, 7 Jan 2019 17:01:23 -0500 Subject: [PATCH 08/10] Make GeomUtils package private --- .../elasticsearch/geo/geometry/Circle.java | 6 ++---- .../GeometryUtils.java} | 20 +++++++++---------- .../org/elasticsearch/geo/geometry/Line.java | 6 ++---- .../org/elasticsearch/geo/geometry/Point.java | 6 ++---- .../elasticsearch/geo/geometry/Rectangle.java | 12 +++++------ 5 files changed, 21 insertions(+), 29 deletions(-) rename libs/geo/src/main/java/org/elasticsearch/geo/{GeoUtils.java => geometry/GeometryUtils.java} (79%) diff --git a/libs/geo/src/main/java/org/elasticsearch/geo/geometry/Circle.java b/libs/geo/src/main/java/org/elasticsearch/geo/geometry/Circle.java index 87a6eba2341e3..fea582e07b3e2 100644 --- a/libs/geo/src/main/java/org/elasticsearch/geo/geometry/Circle.java +++ b/libs/geo/src/main/java/org/elasticsearch/geo/geometry/Circle.java @@ -19,8 +19,6 @@ package org.elasticsearch.geo.geometry; -import org.elasticsearch.geo.GeoUtils; - /** * Circle geometry (not part of WKT standard, but used in elasticsearch) */ @@ -43,8 +41,8 @@ public Circle(final double lat, final double lon, final double radiusMeters) { if (radiusMeters < 0 ) { throw new IllegalArgumentException("Circle radius [" + radiusMeters + "] cannot be negative"); } - GeoUtils.checkLatitude(lat); - GeoUtils.checkLongitude(lon); + GeometryUtils.checkLatitude(lat); + GeometryUtils.checkLongitude(lon); } @Override diff --git a/libs/geo/src/main/java/org/elasticsearch/geo/GeoUtils.java b/libs/geo/src/main/java/org/elasticsearch/geo/geometry/GeometryUtils.java similarity index 79% rename from libs/geo/src/main/java/org/elasticsearch/geo/GeoUtils.java rename to libs/geo/src/main/java/org/elasticsearch/geo/geometry/GeometryUtils.java index a47aeec33344e..9a7d4b99d3e53 100644 --- a/libs/geo/src/main/java/org/elasticsearch/geo/GeoUtils.java +++ b/libs/geo/src/main/java/org/elasticsearch/geo/geometry/GeometryUtils.java @@ -17,40 +17,40 @@ * under the License. */ -package org.elasticsearch.geo; +package org.elasticsearch.geo.geometry; /** - * Basic reusable geo-spatial utility methods + * Geometry-related utility methods */ -public final class GeoUtils { +final class GeometryUtils { /** * Minimum longitude value. */ - public static final double MIN_LON_INCL = -180.0D; + static final double MIN_LON_INCL = -180.0D; /** * Maximum longitude value. */ - public static final double MAX_LON_INCL = 180.0D; + static final double MAX_LON_INCL = 180.0D; /** * Minimum latitude value. */ - public static final double MIN_LAT_INCL = -90.0D; + static final double MIN_LAT_INCL = -90.0D; /** * Maximum latitude value. */ - public static final double MAX_LAT_INCL = 90.0D; + static final double MAX_LAT_INCL = 90.0D; // No instance: - private GeoUtils() { + private GeometryUtils() { } /** * validates latitude value is within standard +/-90 coordinate bounds */ - public static void checkLatitude(double latitude) { + static void checkLatitude(double latitude) { if (Double.isNaN(latitude) || latitude < MIN_LAT_INCL || latitude > MAX_LAT_INCL) { throw new IllegalArgumentException( "invalid latitude " + latitude + "; must be between " + MIN_LAT_INCL + " and " + MAX_LAT_INCL); @@ -60,7 +60,7 @@ public static void checkLatitude(double latitude) { /** * validates longitude value is within standard +/-180 coordinate bounds */ - public static void checkLongitude(double longitude) { + static void checkLongitude(double longitude) { if (Double.isNaN(longitude) || longitude < MIN_LON_INCL || longitude > MAX_LON_INCL) { throw new IllegalArgumentException( "invalid longitude " + longitude + "; must be between " + MIN_LON_INCL + " and " + MAX_LON_INCL); diff --git a/libs/geo/src/main/java/org/elasticsearch/geo/geometry/Line.java b/libs/geo/src/main/java/org/elasticsearch/geo/geometry/Line.java index aa1cf06867fd3..415dacfce9b3c 100644 --- a/libs/geo/src/main/java/org/elasticsearch/geo/geometry/Line.java +++ b/libs/geo/src/main/java/org/elasticsearch/geo/geometry/Line.java @@ -19,8 +19,6 @@ package org.elasticsearch.geo.geometry; -import org.elasticsearch.geo.GeoUtils; - import java.util.Arrays; /** @@ -52,8 +50,8 @@ public Line(double[] lats, double[] lons) { throw new IllegalArgumentException("at least two points in the line is required"); } for (int i = 0; i < lats.length; i++) { - GeoUtils.checkLatitude(lats[i]); - GeoUtils.checkLongitude(lons[i]); + GeometryUtils.checkLatitude(lats[i]); + GeometryUtils.checkLongitude(lons[i]); } } diff --git a/libs/geo/src/main/java/org/elasticsearch/geo/geometry/Point.java b/libs/geo/src/main/java/org/elasticsearch/geo/geometry/Point.java index 9c71e91811219..d85d40c8dc789 100644 --- a/libs/geo/src/main/java/org/elasticsearch/geo/geometry/Point.java +++ b/libs/geo/src/main/java/org/elasticsearch/geo/geometry/Point.java @@ -19,8 +19,6 @@ package org.elasticsearch.geo.geometry; -import org.elasticsearch.geo.GeoUtils; - /** * Represents a Point on the earth's surface in decimal degrees. */ @@ -38,8 +36,8 @@ private Point() { } public Point(double lat, double lon) { - GeoUtils.checkLatitude(lat); - GeoUtils.checkLongitude(lon); + GeometryUtils.checkLatitude(lat); + GeometryUtils.checkLongitude(lon); this.lat = lat; this.lon = lon; this.empty = false; diff --git a/libs/geo/src/main/java/org/elasticsearch/geo/geometry/Rectangle.java b/libs/geo/src/main/java/org/elasticsearch/geo/geometry/Rectangle.java index 37d9fd337562c..8170bf95a6e2d 100644 --- a/libs/geo/src/main/java/org/elasticsearch/geo/geometry/Rectangle.java +++ b/libs/geo/src/main/java/org/elasticsearch/geo/geometry/Rectangle.java @@ -19,8 +19,6 @@ package org.elasticsearch.geo.geometry; -import org.elasticsearch.geo.GeoUtils; - /** * Represents a lat/lon rectangle in decimal degrees. */ @@ -57,10 +55,10 @@ private Rectangle() { * Constructs a bounding box by first validating the provided latitude and longitude coordinates */ public Rectangle(double minLat, double maxLat, double minLon, double maxLon) { - GeoUtils.checkLatitude(minLat); - GeoUtils.checkLatitude(maxLat); - GeoUtils.checkLongitude(minLon); - GeoUtils.checkLongitude(maxLon); + GeometryUtils.checkLatitude(minLat); + GeometryUtils.checkLatitude(maxLat); + GeometryUtils.checkLongitude(minLon); + GeometryUtils.checkLongitude(maxLon); this.minLon = minLon; this.maxLon = maxLon; this.minLat = minLat; @@ -73,7 +71,7 @@ public Rectangle(double minLat, double maxLat, double minLon, double maxLon) { public double getWidth() { if (crossesDateline()) { - return GeoUtils.MAX_LON_INCL - minLon + maxLon - GeoUtils.MIN_LON_INCL; + return GeometryUtils.MAX_LON_INCL - minLon + maxLon - GeometryUtils.MIN_LON_INCL; } return maxLon - minLon; } From 9408af723036bcf0b23e91cf901bb66ee0ad1d8e Mon Sep 17 00:00:00 2001 From: Igor Motov Date: Wed, 9 Jan 2019 15:32:30 -0500 Subject: [PATCH 09/10] Add javadoc with explanation of the Visitor Pattern --- .../geo/geometry/GeometryVisitor.java | 24 ++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/libs/geo/src/main/java/org/elasticsearch/geo/geometry/GeometryVisitor.java b/libs/geo/src/main/java/org/elasticsearch/geo/geometry/GeometryVisitor.java index b71763212a8e2..9712e82a425d9 100644 --- a/libs/geo/src/main/java/org/elasticsearch/geo/geometry/GeometryVisitor.java +++ b/libs/geo/src/main/java/org/elasticsearch/geo/geometry/GeometryVisitor.java @@ -20,7 +20,29 @@ package org.elasticsearch.geo.geometry; /** - * Support class for creating of geometry Visitors + * Support class for creating of geometry Visitors. + *

+ * This is an implementation of the Visitor pattern. The basic idea is to simplify adding new operations on Geometries, without + * constantly modifying and adding new functionality to the Geometry hierarchy and keeping it as lightweight as possible. + *

+ * It is a more object-oriented alternative to structures like this: + *

+ * if (obj instanceof This) {
+ *   doThis((This) obj);
+ * } elseif (obj instanceof That) {
+ *   doThat((That) obj);
+ * ...
+ * } else {
+ *   throw new IllegalArgumentException("Unknown object " + obj);
+ * }
+ * 
+ *

+ * The Visitor Pattern replaces this structure with Interface inheritance making it easier to identify all places that are using this + * structure, and making a shape a compile-time failure instead of runtime. + *

+ * See {@link org.elasticsearch.geo.utils.WellKnownText#toWKT(Geometry, StringBuilder)} for an example of how this interface is used. + * + * @see Visitor Pattern */ public interface GeometryVisitor { From 4929333b0fdd7d7661e07b2199bdbc4a1d129291 Mon Sep 17 00:00:00 2001 From: Igor Motov Date: Thu, 10 Jan 2019 10:52:32 -0500 Subject: [PATCH 10/10] Address review comments --- .../java/org/elasticsearch/geo/geometry/GeometryVisitor.java | 2 +- .../main/java/org/elasticsearch/geo/geometry/LinearRing.java | 2 -- .../main/java/org/elasticsearch/geo/geometry/MultiLine.java | 1 - .../java/org/elasticsearch/geo/geometry/MultiPolygon.java | 1 - .../main/java/org/elasticsearch/geo/utils/WellKnownText.java | 5 ++++- 5 files changed, 5 insertions(+), 6 deletions(-) diff --git a/libs/geo/src/main/java/org/elasticsearch/geo/geometry/GeometryVisitor.java b/libs/geo/src/main/java/org/elasticsearch/geo/geometry/GeometryVisitor.java index 9712e82a425d9..8317b23d1feca 100644 --- a/libs/geo/src/main/java/org/elasticsearch/geo/geometry/GeometryVisitor.java +++ b/libs/geo/src/main/java/org/elasticsearch/geo/geometry/GeometryVisitor.java @@ -20,7 +20,7 @@ package org.elasticsearch.geo.geometry; /** - * Support class for creating of geometry Visitors. + * Support class for creating Geometry Visitors. *

* This is an implementation of the Visitor pattern. The basic idea is to simplify adding new operations on Geometries, without * constantly modifying and adding new functionality to the Geometry hierarchy and keeping it as lightweight as possible. diff --git a/libs/geo/src/main/java/org/elasticsearch/geo/geometry/LinearRing.java b/libs/geo/src/main/java/org/elasticsearch/geo/geometry/LinearRing.java index 6a36ca280bef6..20b1a46dd9d31 100644 --- a/libs/geo/src/main/java/org/elasticsearch/geo/geometry/LinearRing.java +++ b/libs/geo/src/main/java/org/elasticsearch/geo/geometry/LinearRing.java @@ -27,9 +27,7 @@ public class LinearRing extends Line { public static final LinearRing EMPTY = new LinearRing(); - private LinearRing() { - } public LinearRing(double[] lats, double[] lons) { diff --git a/libs/geo/src/main/java/org/elasticsearch/geo/geometry/MultiLine.java b/libs/geo/src/main/java/org/elasticsearch/geo/geometry/MultiLine.java index f43f6d2fd2488..995c43d0c1c80 100644 --- a/libs/geo/src/main/java/org/elasticsearch/geo/geometry/MultiLine.java +++ b/libs/geo/src/main/java/org/elasticsearch/geo/geometry/MultiLine.java @@ -28,7 +28,6 @@ public class MultiLine extends GeometryCollection { public static final MultiLine EMPTY = new MultiLine(); private MultiLine() { - } public MultiLine(List lines) { diff --git a/libs/geo/src/main/java/org/elasticsearch/geo/geometry/MultiPolygon.java b/libs/geo/src/main/java/org/elasticsearch/geo/geometry/MultiPolygon.java index 3a289f6b4a793..01c68d6dd0b32 100644 --- a/libs/geo/src/main/java/org/elasticsearch/geo/geometry/MultiPolygon.java +++ b/libs/geo/src/main/java/org/elasticsearch/geo/geometry/MultiPolygon.java @@ -28,7 +28,6 @@ public class MultiPolygon extends GeometryCollection { public static final MultiPolygon EMPTY = new MultiPolygon(); private MultiPolygon() { - } public MultiPolygon(List polygons) { diff --git a/libs/geo/src/main/java/org/elasticsearch/geo/utils/WellKnownText.java b/libs/geo/src/main/java/org/elasticsearch/geo/utils/WellKnownText.java index c80fdb571e90b..5cf29065b006a 100644 --- a/libs/geo/src/main/java/org/elasticsearch/geo/utils/WellKnownText.java +++ b/libs/geo/src/main/java/org/elasticsearch/geo/utils/WellKnownText.java @@ -217,7 +217,10 @@ public static Geometry fromWKT(String wkt) throws IOException, ParseException { tokenizer.wordChars('-', '-'); tokenizer.wordChars('+', '+'); tokenizer.wordChars('.', '.'); - tokenizer.whitespaceChars(0, ' '); + tokenizer.whitespaceChars(' ', ' '); + tokenizer.whitespaceChars('\t', '\t'); + tokenizer.whitespaceChars('\r', '\r'); + tokenizer.whitespaceChars('\n', '\n'); tokenizer.commentChar('#'); return parseGeometry(tokenizer); } finally {