diff --git a/lucene/spatial3d/src/java/org/apache/lucene/spatial3d/Geo3DDocValuesField.java b/lucene/spatial3d/src/java/org/apache/lucene/spatial3d/Geo3DDocValuesField.java index 3fdefb55240..dd171ec1ae0 100644 --- a/lucene/spatial3d/src/java/org/apache/lucene/spatial3d/Geo3DDocValuesField.java +++ b/lucene/spatial3d/src/java/org/apache/lucene/spatial3d/Geo3DDocValuesField.java @@ -22,10 +22,12 @@ import org.apache.lucene.search.FieldDoc; import org.apache.lucene.search.SortField; import org.apache.lucene.document.Field; import org.apache.lucene.document.FieldType; +import org.apache.lucene.geo.Polygon; import org.apache.lucene.spatial3d.geom.PlanetModel; import org.apache.lucene.spatial3d.geom.GeoPoint; import org.apache.lucene.spatial3d.geom.GeoDistanceShape; +import org.apache.lucene.spatial3d.geom.GeoOutsideDistance; /** * An per-document 3D location field. @@ -256,7 +258,7 @@ public class Geo3DDocValuesField extends Field { } /** - * Creates a SortField for sorting by distance from a point. + * Creates a SortField for sorting by distance within a circle. *

* This sort orders documents by ascending distance from the location. The value returned in {@link FieldDoc} for * the hits contains a Double instance with the distance in meters. @@ -264,14 +266,14 @@ public class Geo3DDocValuesField extends Field { * If a document is missing the field, then by default it is treated as having {@link Double#POSITIVE_INFINITY} distance * (missing values sort last). *

- * If a document contains multiple values for the field, the closest distance to the location is used. + * If a document contains multiple values for the field, the closest distance from the circle center is used. * * @param field field name. must not be null. * @param latitude latitude at the center: must be within standard +/-90 coordinate bounds. * @param longitude longitude at the center: must be within standard +/-180 coordinate bounds. * @param maxRadiusMeters is the maximum radius in meters. * @return SortField ordering documents by distance - * @throws IllegalArgumentException if {@code field} is null or location has invalid coordinates. + * @throws IllegalArgumentException if {@code field} is null or circle has invalid coordinates. */ public static SortField newDistanceSort(final String field, final double latitude, final double longitude, final double maxRadiusMeters) { final GeoDistanceShape shape = Geo3DUtil.fromDistance(latitude, longitude, maxRadiusMeters); @@ -287,18 +289,138 @@ public class Geo3DDocValuesField extends Field { * If a document is missing the field, then by default it is treated as having {@link Double#POSITIVE_INFINITY} distance * (missing values sort last). *

- * If a document contains multiple values for the field, the closest distance to the location is used. + * If a document contains multiple values for the field, the closest distance along the path is used. * * @param field field name. must not be null. * @param pathLatitudes latitude values for points of the path: must be within standard +/-90 coordinate bounds. * @param pathLongitudes longitude values for points of the path: must be within standard +/-180 coordinate bounds. * @param pathWidthMeters width of the path in meters. * @return SortField ordering documents by distance - * @throws IllegalArgumentException if {@code field} is null or location has invalid coordinates. + * @throws IllegalArgumentException if {@code field} is null or path has invalid coordinates. */ public static SortField newPathSort(final String field, final double[] pathLatitudes, final double[] pathLongitudes, final double pathWidthMeters) { final GeoDistanceShape shape = Geo3DUtil.fromPath(pathLatitudes, pathLongitudes, pathWidthMeters); return new Geo3DPointSortField(field, shape); } + // Outside distances + + /** + * Creates a SortField for sorting by outside distance from a circle. + *

+ * This sort orders documents by ascending outside distance from the circle. Points within the circle have distance 0.0. + * The value returned in {@link FieldDoc} for + * the hits contains a Double instance with the distance in meters. + *

+ * If a document is missing the field, then by default it is treated as having {@link Double#POSITIVE_INFINITY} distance + * (missing values sort last). + *

+ * If a document contains multiple values for the field, the closest distance to the circle is used. + * + * @param field field name. must not be null. + * @param latitude latitude at the center: must be within standard +/-90 coordinate bounds. + * @param longitude longitude at the center: must be within standard +/-180 coordinate bounds. + * @param maxRadiusMeters is the maximum radius in meters. + * @return SortField ordering documents by distance + * @throws IllegalArgumentException if {@code field} is null or location has invalid coordinates. + */ + public static SortField newOutsideDistanceSort(final String field, final double latitude, final double longitude, final double maxRadiusMeters) { + final GeoOutsideDistance shape = Geo3DUtil.fromDistance(latitude, longitude, maxRadiusMeters); + return new Geo3DPointOutsideSortField(field, shape); + } + + /** + * Creates a SortField for sorting by outside distance from a box. + *

+ * This sort orders documents by ascending outside distance from the box. Points within the box have distance 0.0. + * The value returned in {@link FieldDoc} for + * the hits contains a Double instance with the distance in meters. + *

+ * If a document is missing the field, then by default it is treated as having {@link Double#POSITIVE_INFINITY} distance + * (missing values sort last). + *

+ * If a document contains multiple values for the field, the closest distance to the box is used. + * + * @param field field name. must not be null. + * @param minLatitude latitude lower bound: must be within standard +/-90 coordinate bounds. + * @param maxLatitude latitude upper bound: must be within standard +/-90 coordinate bounds. + * @param minLongitude longitude lower bound: must be within standard +/-180 coordinate bounds. + * @param maxLongitude longitude upper bound: must be within standard +/-180 coordinate bounds. + * @return SortField ordering documents by distance + * @throws IllegalArgumentException if {@code field} is null or box has invalid coordinates. + */ + public static SortField newOutsideBoxSort(final String field, final double minLatitude, final double maxLatitude, final double minLongitude, final double maxLongitude) { + final GeoOutsideDistance shape = Geo3DUtil.fromBox(minLatitude, maxLatitude, minLongitude, maxLongitude); + return new Geo3DPointOutsideSortField(field, shape); + } + + /** + * Creates a SortField for sorting by outside distance from a polygon. + *

+ * This sort orders documents by ascending outside distance from the polygon. Points within the polygon have distance 0.0. + * The value returned in {@link FieldDoc} for + * the hits contains a Double instance with the distance in meters. + *

+ * If a document is missing the field, then by default it is treated as having {@link Double#POSITIVE_INFINITY} distance + * (missing values sort last). + *

+ * If a document contains multiple values for the field, the closest distance to the polygon is used. + * + * @param field field name. must not be null. + * @param polygons is the list of polygons to use to construct the query; must be at least one. + * @return SortField ordering documents by distance + * @throws IllegalArgumentException if {@code field} is null or polygon has invalid coordinates. + */ + public static SortField newOutsidePolygonSort(final String field, final Polygon... polygons) { + final GeoOutsideDistance shape = Geo3DUtil.fromPolygon(polygons); + return new Geo3DPointOutsideSortField(field, shape); + } + + /** + * Creates a SortField for sorting by outside distance from a large polygon. This differs from the related newOutsideLargePolygonSort in that it + * does little or no legality checking and is optimized for very large numbers of polygon edges. + *

+ * This sort orders documents by ascending outside distance from the polygon. Points within the polygon have distance 0.0. + * The value returned in {@link FieldDoc} for + * the hits contains a Double instance with the distance in meters. + *

+ * If a document is missing the field, then by default it is treated as having {@link Double#POSITIVE_INFINITY} distance + * (missing values sort last). + *

+ * If a document contains multiple values for the field, the closest distance to the polygon is used. + * + * @param field field name. must not be null. + * @param polygons is the list of polygons to use to construct the query; must be at least one. + * @return SortField ordering documents by distance + * @throws IllegalArgumentException if {@code field} is null or polygon has invalid coordinates. + */ + public static SortField newOutsideLargePolygonSort(final String field, final Polygon... polygons) { + final GeoOutsideDistance shape = Geo3DUtil.fromLargePolygon(polygons); + return new Geo3DPointOutsideSortField(field, shape); + } + + /** + * Creates a SortField for sorting by outside distance from a path. + *

+ * This sort orders documents by ascending outside distance from the described path. Points within the path + * are given the distance of 0.0. The value returned in {@link FieldDoc} for + * the hits contains a Double instance with the distance in meters. + *

+ * If a document is missing the field, then by default it is treated as having {@link Double#POSITIVE_INFINITY} distance + * (missing values sort last). + *

+ * If a document contains multiple values for the field, the closest distance from the path is used. + * + * @param field field name. must not be null. + * @param pathLatitudes latitude values for points of the path: must be within standard +/-90 coordinate bounds. + * @param pathLongitudes longitude values for points of the path: must be within standard +/-180 coordinate bounds. + * @param pathWidthMeters width of the path in meters. + * @return SortField ordering documents by distance + * @throws IllegalArgumentException if {@code field} is null or path has invalid coordinates. + */ + public static SortField newOutsidePathSort(final String field, final double[] pathLatitudes, final double[] pathLongitudes, final double pathWidthMeters) { + final GeoOutsideDistance shape = Geo3DUtil.fromPath(pathLatitudes, pathLongitudes, pathWidthMeters); + return new Geo3DPointOutsideSortField(field, shape); + } + } diff --git a/lucene/spatial3d/src/java/org/apache/lucene/spatial3d/Geo3DPoint.java b/lucene/spatial3d/src/java/org/apache/lucene/spatial3d/Geo3DPoint.java index 2ff1286d434..15426e68952 100644 --- a/lucene/spatial3d/src/java/org/apache/lucene/spatial3d/Geo3DPoint.java +++ b/lucene/spatial3d/src/java/org/apache/lucene/spatial3d/Geo3DPoint.java @@ -129,9 +129,6 @@ public final class Geo3DPoint extends Field { * @return query matching points within this polygon */ public static Query newLargePolygonQuery(final String field, final Polygon... polygons) { - if (polygons.length < 1) { - throw new IllegalArgumentException("need at least one polygon"); - } final GeoShape shape = Geo3DUtil.fromLargePolygon(polygons); return newShapeQuery(field, shape); } diff --git a/lucene/spatial3d/src/java/org/apache/lucene/spatial3d/Geo3DPointOutsideDistanceComparator.java b/lucene/spatial3d/src/java/org/apache/lucene/spatial3d/Geo3DPointOutsideDistanceComparator.java new file mode 100644 index 00000000000..10e001066f6 --- /dev/null +++ b/lucene/spatial3d/src/java/org/apache/lucene/spatial3d/Geo3DPointOutsideDistanceComparator.java @@ -0,0 +1,137 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF 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.apache.lucene.spatial3d; + +import java.io.IOException; + +import org.apache.lucene.index.DocValues; +import org.apache.lucene.index.FieldInfo; +import org.apache.lucene.index.LeafReader; +import org.apache.lucene.index.LeafReaderContext; +import org.apache.lucene.index.SortedNumericDocValues; +import org.apache.lucene.search.FieldComparator; +import org.apache.lucene.search.LeafFieldComparator; +import org.apache.lucene.search.Scorer; + +import org.apache.lucene.spatial3d.geom.GeoOutsideDistance; +import org.apache.lucene.spatial3d.geom.DistanceStyle; +import org.apache.lucene.spatial3d.geom.PlanetModel; + +/** + * Compares documents by outside distance, using a GeoOutsideDistance to compute the distance + */ +class Geo3DPointOutsideDistanceComparator extends FieldComparator implements LeafFieldComparator { + final String field; + + final GeoOutsideDistance distanceShape; + + final double[] values; + double bottomDistance; + double topValue; + SortedNumericDocValues currentDocs; + + public Geo3DPointOutsideDistanceComparator(String field, final GeoOutsideDistance distanceShape, int numHits) { + this.field = field; + this.distanceShape = distanceShape; + this.values = new double[numHits]; + } + + @Override + public void setScorer(Scorer scorer) {} + + @Override + public int compare(int slot1, int slot2) { + return Double.compare(values[slot1], values[slot2]); + } + + @Override + public void setBottom(int slot) { + bottomDistance = values[slot]; + } + + @Override + public void setTopValue(Double value) { + topValue = value.doubleValue(); + } + + @Override + public int compareBottom(int doc) throws IOException { + currentDocs.setDocument(doc); + + int numValues = currentDocs.count(); + if (numValues == 0) { + return Double.compare(bottomDistance, Double.POSITIVE_INFINITY); + } + + int cmp = -1; + for (int i = 0; i < numValues; i++) { + long encoded = currentDocs.valueAt(i); + + // Test against bounds. + // First we need to decode... + final double x = Geo3DDocValuesField.decodeXValue(encoded); + final double y = Geo3DDocValuesField.decodeYValue(encoded); + final double z = Geo3DDocValuesField.decodeZValue(encoded); + + cmp = Math.max(cmp, Double.compare(bottomDistance, distanceShape.computeOutsideDistance(DistanceStyle.ARC, x, y, z))); + } + return cmp; + } + + @Override + public void copy(int slot, int doc) throws IOException { + values[slot] = computeMinimumDistance(doc); + } + + @Override + public LeafFieldComparator getLeafComparator(LeafReaderContext context) throws IOException { + LeafReader reader = context.reader(); + FieldInfo info = reader.getFieldInfos().fieldInfo(field); + if (info != null) { + Geo3DDocValuesField.checkCompatible(info); + } + currentDocs = DocValues.getSortedNumeric(reader, field); + return this; + } + + @Override + public Double value(int slot) { + // Return the arc distance + return Double.valueOf(values[slot] * PlanetModel.WGS84_MEAN); + } + + @Override + public int compareTop(int doc) throws IOException { + return Double.compare(topValue, computeMinimumDistance(doc)); + } + + double computeMinimumDistance(final int doc) { + currentDocs.setDocument(doc); + double minValue = Double.POSITIVE_INFINITY; + final int numValues = currentDocs.count(); + for (int i = 0; i < numValues; i++) { + final long encoded = currentDocs.valueAt(i); + final double distance = distanceShape.computeOutsideDistance(DistanceStyle.ARC, + Geo3DDocValuesField.decodeXValue(encoded), + Geo3DDocValuesField.decodeYValue(encoded), + Geo3DDocValuesField.decodeZValue(encoded)); + minValue = Math.min(minValue, distance); + } + return minValue; + } + +} diff --git a/lucene/spatial3d/src/java/org/apache/lucene/spatial3d/Geo3DPointOutsideSortField.java b/lucene/spatial3d/src/java/org/apache/lucene/spatial3d/Geo3DPointOutsideSortField.java new file mode 100644 index 00000000000..b48984cc404 --- /dev/null +++ b/lucene/spatial3d/src/java/org/apache/lucene/spatial3d/Geo3DPointOutsideSortField.java @@ -0,0 +1,96 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF 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.apache.lucene.spatial3d; + +import java.io.IOException; + +import org.apache.lucene.search.FieldComparator; +import org.apache.lucene.search.SortField; + +import org.apache.lucene.spatial3d.geom.GeoOutsideDistance; + +/** + * Sorts by outside distance from an origin location. + */ +final class Geo3DPointOutsideSortField extends SortField { + final GeoOutsideDistance distanceShape; + + Geo3DPointOutsideSortField(final String field, final GeoOutsideDistance distanceShape) { + super(field, SortField.Type.CUSTOM); + if (field == null) { + throw new IllegalArgumentException("field must not be null"); + } + if (distanceShape == null) { + throw new IllegalArgumentException("distanceShape must not be null"); + } + this.distanceShape = distanceShape; + setMissingValue(Double.POSITIVE_INFINITY); + } + + @Override + public FieldComparator getComparator(int numHits, int sortPos) throws IOException { + return new Geo3DPointOutsideDistanceComparator(getField(), distanceShape, numHits); + } + + @Override + public Double getMissingValue() { + return (Double) super.getMissingValue(); + } + + @Override + public void setMissingValue(Object missingValue) { + if (Double.valueOf(Double.POSITIVE_INFINITY).equals(missingValue) == false) { + throw new IllegalArgumentException("Missing value can only be Double.POSITIVE_INFINITY (missing values last), but got " + missingValue); + } + this.missingValue = missingValue; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = super.hashCode(); + long temp; + temp = distanceShape.hashCode(); + result = prime * result + (int) (temp ^ (temp >>> 32)); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (!super.equals(obj)) return false; + if (getClass() != obj.getClass()) return false; + final Geo3DPointSortField other = (Geo3DPointSortField) obj; + return distanceShape.equals(other.distanceShape); + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + builder.append("