LUCENE-9238: Add new XYPointField, queries and sorting capabilities (#1272)

New XYPointField field and Queries for indexing, searching and sorting cartesian points.
This commit is contained in:
Ignacio Vera 2020-02-21 11:26:30 +01:00 committed by GitHub
parent cb68d7d2c5
commit 88dd1c3f3d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 2869 additions and 8 deletions

View File

@ -121,6 +121,9 @@ New Features
* LUCENE-8707: Add LatLonShape and XYShape distance query. (Ignacio Vera)
* LUCENE-9238: New XYPointField field and Queries for indexing, searching and sorting
cartesian points. (Ignacio Vera)
Improvements
---------------------

View File

@ -0,0 +1,178 @@
/*
* 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.document;
import org.apache.lucene.geo.XYCircle;
import org.apache.lucene.geo.XYEncodingUtils;
import org.apache.lucene.geo.XYPolygon;
import org.apache.lucene.geo.XYRectangle;
import org.apache.lucene.index.DocValuesType;
import org.apache.lucene.index.FieldInfo;
import org.apache.lucene.search.FieldDoc;
import org.apache.lucene.search.IndexOrDocValuesQuery;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.SortField;
/**
* An per-document location field.
* <p>
* Sorting by distance is efficient. Multiple values for the same field in one document
* is allowed.
* <p>
* This field defines static factory methods for common operations:
* <ul>
* <li>{@link #newDistanceSort newDistanceSort()} for ordering documents by distance from a specified location.
* </ul>
* <p>
* If you also need query operations, you should add a separate {@link XYPointField} instance.
* If you also need to store the value, you should add a separate {@link StoredField} instance.
*
* @see XYPointField
*/
public class XYDocValuesField extends Field {
/**
* Type for a XYDocValuesField
* <p>
* Each value stores a 64-bit long where the upper 32 bits are the encoded x value,
* and the lower 32 bits are the encoded y value.
* @see org.apache.lucene.geo.XYEncodingUtils#decode(int)
*/
public static final FieldType TYPE = new FieldType();
static {
TYPE.setDocValuesType(DocValuesType.SORTED_NUMERIC);
TYPE.freeze();
}
/**
* Creates a new XYDocValuesField with the specified x and y
* @param name field name
* @param x x value.
* @param y y values.
* @throws IllegalArgumentException if the field name is null or x or y are infinite or NaN.
*/
public XYDocValuesField(String name, float x, float y) {
super(name, TYPE);
setLocationValue(x, y);
}
/**
* Change the values of this field
* @param x x value.
* @param y y value.
* @throws IllegalArgumentException if x or y are infinite or NaN.
*/
public void setLocationValue(float x, float y) {
int xEncoded = XYEncodingUtils.encode(x);
int yEncoded = XYEncodingUtils.encode(y);
fieldsData = Long.valueOf((((long) xEncoded) << 32) | (yEncoded & 0xFFFFFFFFL));
}
/** helper: checks a fieldinfo and throws exception if its definitely not a XYDocValuesField */
static void checkCompatible(FieldInfo fieldInfo) {
// dv properties could be "unset", if you e.g. used only StoredField with this same name in the segment.
if (fieldInfo.getDocValuesType() != DocValuesType.NONE && fieldInfo.getDocValuesType() != TYPE.docValuesType()) {
throw new IllegalArgumentException("field=\"" + fieldInfo.name + "\" was indexed with docValuesType=" + fieldInfo.getDocValuesType() +
" but this type has docValuesType=" + TYPE.docValuesType() +
", is the field really a XYDocValuesField?");
}
}
@Override
public String toString() {
StringBuilder result = new StringBuilder();
result.append(getClass().getSimpleName());
result.append(" <");
result.append(name);
result.append(':');
long currentValue = (Long)fieldsData;
result.append(XYEncodingUtils.decode((int)(currentValue >> 32)));
result.append(',');
result.append(XYEncodingUtils.decode((int)(currentValue & 0xFFFFFFFF)));
result.append('>');
return result.toString();
}
/**
* Creates a SortField for sorting by distance from a location.
* <p>
* 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.
* <p>
* If a document is missing the field, then by default it is treated as having {@link Double#POSITIVE_INFINITY} distance
* (missing values sort last).
* <p>
* If a document contains multiple values for the field, the <i>closest</i> distance to the location is used.
*
* @param field field name. must not be null.
* @param x x at the center.
* @param y y at the center.
* @return SortField ordering documents by distance
* @throws IllegalArgumentException if {@code field} is null or location has invalid coordinates.
*/
public static SortField newDistanceSort(String field, float x, float y) {
return new XYPointSortField(field, x, y);
}
/**
* Create a query for matching a bounding box using doc values.
* This query is usually slow as it does not use an index structure and needs
* to verify documents one-by-one in order to know whether they match. It is
* best used wrapped in an {@link IndexOrDocValuesQuery} alongside a
* {@link XYPointField#newBoxQuery}.
*/
public static Query newSlowBoxQuery(String field, float minX, float maxX, float minY, float maxY) {
XYRectangle rectangle = new XYRectangle(minX, maxX, minY, maxY);
return new XYDocValuesPointInGeometryQuery(field, rectangle);
}
/**
* Create a query for matching points within the specified distance of the supplied location.
* This query is usually slow as it does not use an index structure and needs
* to verify documents one-by-one in order to know whether they match. It is
* best used wrapped in an {@link IndexOrDocValuesQuery} alongside a
* {@link XYPointField#newDistanceQuery}.
* @param field field name. must not be null.
* @param x x at the center.
* @param y y at the center: must be within standard +/-180 coordinate bounds.
* @param radius maximum distance from the center in cartesian distance: must be non-negative and finite.
* @return query matching points within this distance
* @throws IllegalArgumentException if {@code field} is null, location has invalid coordinates, or radius is invalid.
*/
public static Query newSlowDistanceQuery(String field, float x, float y, float radius) {
XYCircle circle = new XYCircle(x, y, radius);
return new XYDocValuesPointInGeometryQuery(field, circle);
}
/**
* Create a query for matching points within the supplied polygons.
* This query is usually slow as it does not use an index structure and needs
* to verify documents one-by-one in order to know whether they match. It is
* best used wrapped in an {@link IndexOrDocValuesQuery} alongside a
* {@link XYPointField#newPolygonQuery(String, XYPolygon...)}.
* @param field field name. must not be null.
* @param polygons array of polygons. must not be null or empty.
* @return query matching points within the given polygons.
* @throws IllegalArgumentException if {@code field} is null or polygons is empty or contain a null polygon.
*/
public static Query newSlowPolygonQuery(String field, XYPolygon... polygons) {
return new XYDocValuesPointInGeometryQuery(field, polygons);
}
}

View File

@ -0,0 +1,143 @@
/*
* 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.document;
import java.io.IOException;
import java.util.Arrays;
import org.apache.lucene.geo.Component2D;
import org.apache.lucene.geo.XYEncodingUtils;
import org.apache.lucene.geo.XYGeometry;
import org.apache.lucene.index.DocValues;
import org.apache.lucene.index.LeafReaderContext;
import org.apache.lucene.index.SortedNumericDocValues;
import org.apache.lucene.search.ConstantScoreScorer;
import org.apache.lucene.search.ConstantScoreWeight;
import org.apache.lucene.search.IndexSearcher;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.QueryVisitor;
import org.apache.lucene.search.ScoreMode;
import org.apache.lucene.search.Scorer;
import org.apache.lucene.search.TwoPhaseIterator;
import org.apache.lucene.search.Weight;
/** XYGeometry query for {@link XYDocValuesField}. */
public class XYDocValuesPointInGeometryQuery extends Query {
private final String field;
private final XYGeometry[] geometries;
XYDocValuesPointInGeometryQuery(String field, XYGeometry... geometries) {
if (field == null) {
throw new IllegalArgumentException("field must not be null");
}
if (geometries == null) {
throw new IllegalArgumentException("geometries must not be null");
}
if (geometries.length == 0) {
throw new IllegalArgumentException("geometries must not be empty");
}
for (int i = 0; i < geometries.length; i++) {
if (geometries[i] == null) {
throw new IllegalArgumentException("geometries[" + i + "] must not be null");
}
}
this.field = field;
this.geometries = geometries.clone();
}
@Override
public String toString(String field) {
StringBuilder sb = new StringBuilder();
if (!this.field.equals(field)) {
sb.append(this.field);
sb.append(':');
}
sb.append("geometries(").append(Arrays.toString(geometries));
return sb.append(")").toString();
}
@Override
public boolean equals(Object obj) {
if (sameClassAs(obj) == false) {
return false;
}
XYDocValuesPointInGeometryQuery other = (XYDocValuesPointInGeometryQuery) obj;
return field.equals(other.field) &&
Arrays.equals(geometries, other.geometries);
}
@Override
public int hashCode() {
int h = classHash();
h = 31 * h + field.hashCode();
h = 31 * h + Arrays.hashCode(geometries);
return h;
}
@Override
public void visit(QueryVisitor visitor) {
if (visitor.acceptField(field)) {
visitor.visitLeaf(this);
}
}
@Override
public Weight createWeight(IndexSearcher searcher, ScoreMode scoreMode, float boost) throws IOException {
return new ConstantScoreWeight(this, boost) {
final Component2D component2D = XYGeometry.create(geometries);
@Override
public Scorer scorer(LeafReaderContext context) throws IOException {
final SortedNumericDocValues values = context.reader().getSortedNumericDocValues(field);
if (values == null) {
return null;
}
final TwoPhaseIterator iterator = new TwoPhaseIterator(values) {
@Override
public boolean matches() throws IOException {
for (int i = 0, count = values.docValueCount(); i < count; ++i) {
final long value = values.nextValue();
final double x = XYEncodingUtils.decode((int) (value >>> 32));
final double y = XYEncodingUtils.decode((int) (value & 0xFFFFFFFF));
if (component2D.contains(x, y)) {
return true;
}
}
return false;
}
@Override
public float matchCost() {
return 1000f; // TODO: what should it be?
}
};
return new ConstantScoreScorer(this, boost, scoreMode, iterator);
}
@Override
public boolean isCacheable(LeafReaderContext ctx) {
return DocValues.isCacheable(ctx, field);
}
};
}
}

View File

@ -0,0 +1,207 @@
/*
* 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.document;
import java.io.IOException;
import org.apache.lucene.geo.XYEncodingUtils;
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.Scorable;
import org.apache.lucene.util.ArrayUtil;
/**
* Compares documents by distance from an origin point
* <p>
* When the least competitive item on the priority queue changes (setBottom), we recompute
* a bounding box representing competitive distance to the top-N. Then in compareBottom, we can
* quickly reject hits based on bounding box alone without computing distance for every element.
*/
class XYPointDistanceComparator extends FieldComparator<Double> implements LeafFieldComparator {
final String field;
final double x;
final double y;
// distances needs to be calculated with square root to
// avoid numerical issues (square distances are different but
// actual distances are equal)
final double[] values;
double bottom;
double topValue;
SortedNumericDocValues currentDocs;
// current bounding box(es) for the bottom distance on the PQ.
// these are pre-encoded with XYPoint's encoding and
// used to exclude uncompetitive hits faster.
int minX = Integer.MIN_VALUE;
int maxX = Integer.MAX_VALUE;
int minY = Integer.MIN_VALUE;
int maxY = Integer.MAX_VALUE;
// the number of times setBottom has been called (adversary protection)
int setBottomCounter = 0;
private long[] currentValues = new long[4];
private int valuesDocID = -1;
public XYPointDistanceComparator(String field, float x, float y, int numHits) {
this.field = field;
this.x = x;
this.y = y;
this.values = new double[numHits];
}
@Override
public void setScorer(Scorable scorer) {}
@Override
public int compare(int slot1, int slot2) {
return Double.compare(values[slot1], values[slot2]);
}
@Override
public void setBottom(int slot) {
bottom = values[slot];
// make bounding box(es) to exclude non-competitive hits, but start
// sampling if we get called way too much: don't make gobs of bounding
// boxes if comparator hits a worst case order (e.g. backwards distance order)
if (setBottomCounter < 1024 || (setBottomCounter & 0x3F) == 0x3F) {
// pre-encode our box to our integer encoding, so we don't have to decode
// to double values for uncompetitive hits. This has some cost!
this.minX = XYEncodingUtils.encode((float) Math.max(-Float.MAX_VALUE, x - bottom));
this.maxX = XYEncodingUtils.encode((float) Math.min(Float.MAX_VALUE, x + bottom));
this.minY = XYEncodingUtils.encode((float) Math.max(-Float.MAX_VALUE, y - bottom));
this.maxY = XYEncodingUtils.encode((float) Math.min(Float.MAX_VALUE, y + bottom));
}
setBottomCounter++;
}
@Override
public void setTopValue(Double value) {
topValue = value.doubleValue();
}
private void setValues() throws IOException {
if (valuesDocID != currentDocs.docID()) {
assert valuesDocID < currentDocs.docID(): " valuesDocID=" + valuesDocID + " vs " + currentDocs.docID();
valuesDocID = currentDocs.docID();
int count = currentDocs.docValueCount();
if (count > currentValues.length) {
currentValues = new long[ArrayUtil.oversize(count, Long.BYTES)];
}
for(int i=0;i<count;i++) {
currentValues[i] = currentDocs.nextValue();
}
}
}
@Override
public int compareBottom(int doc) throws IOException {
if (doc > currentDocs.docID()) {
currentDocs.advance(doc);
}
if (doc < currentDocs.docID()) {
return Double.compare(bottom, Double.POSITIVE_INFINITY);
}
setValues();
int numValues = currentDocs.docValueCount();
int cmp = -1;
for (int i = 0; i < numValues; i++) {
long encoded = currentValues[i];
// test bounding box
int xBits = (int)(encoded >> 32);
if (xBits < minX || xBits > maxX) {
continue;
}
int yBits = (int)(encoded & 0xFFFFFFFF);
if (yBits < minY || yBits > maxY) {
continue;
}
// only compute actual distance if its inside "competitive bounding box"
double docX = XYEncodingUtils.decode(xBits);
double docY = XYEncodingUtils.decode(yBits);
final double diffX = x - docX;
final double diffY = y - docY;
double distance = Math.sqrt(diffX * diffX + diffY * diffY);
cmp = Math.max(cmp, Double.compare(bottom, distance));
// once we compete in the PQ, no need to continue.
if (cmp > 0) {
return cmp;
}
}
return cmp;
}
@Override
public void copy(int slot, int doc) throws IOException {
values[slot] = sortKey(doc);
}
@Override
public LeafFieldComparator getLeafComparator(LeafReaderContext context) throws IOException {
LeafReader reader = context.reader();
FieldInfo info = reader.getFieldInfos().fieldInfo(field);
if (info != null) {
XYDocValuesField.checkCompatible(info);
}
currentDocs = DocValues.getSortedNumeric(reader, field);
valuesDocID = -1;
return this;
}
@Override
public Double value(int slot) {
return values[slot];
}
@Override
public int compareTop(int doc) throws IOException {
return Double.compare(topValue, sortKey(doc));
}
double sortKey(int doc) throws IOException {
if (doc > currentDocs.docID()) {
currentDocs.advance(doc);
}
double minValue = Double.POSITIVE_INFINITY;
if (doc == currentDocs.docID()) {
setValues();
int numValues = currentDocs.docValueCount();
for (int i = 0; i < numValues; i++) {
long encoded = currentValues[i];
double docX = XYEncodingUtils.decode((int)(encoded >> 32));
double docY = XYEncodingUtils.decode((int)(encoded & 0xFFFFFFFF));
final double diffX = x - docX;
final double diffY = y - docY;
double distance = Math.sqrt(diffX * diffX + diffY * diffY);
minValue = Math.min(minValue, distance);
}
}
return minValue;
}
}

View File

@ -0,0 +1,172 @@
/*
* 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.document;
import org.apache.lucene.geo.Polygon;
import org.apache.lucene.geo.XYCircle;
import org.apache.lucene.geo.XYEncodingUtils;
import org.apache.lucene.geo.XYPolygon;
import org.apache.lucene.geo.XYRectangle;
import org.apache.lucene.index.FieldInfo;
import org.apache.lucene.index.PointValues;
import org.apache.lucene.search.Query;
import org.apache.lucene.util.BytesRef;
import org.apache.lucene.util.NumericUtils;
/**
* An indexed XY position field.
* <p>
* Finding all documents within a range at search time is
* efficient. Multiple values for the same field in one document
* is allowed.
* <p>
* This field defines static factory methods for common operations:
* <ul>
* <li>{@link #newBoxQuery newBoxQuery()} for matching points within a bounding box.
* <li>{@link #newDistanceQuery newDistanceQuery()} for matching points within a specified distance.
* <li>{@link #newPolygonQuery newPolygonQuery()} for matching points within an arbitrary polygon.
* </ul>
* <p>
* If you also need per-document operations such as sort by distance, add a separate {@link XYDocValuesField} instance.
* If you also need to store the value, you should add a separate {@link StoredField} instance.
*
* @see PointValues
* @see XYDocValuesField
*/
public class XYPointField extends Field {
/** XYPoint is encoded as integer values so number of bytes is 4 */
public static final int BYTES = Integer.BYTES;
/**
* Type for an indexed XYPoint
* <p>
* Each point stores two dimensions with 4 bytes per dimension.
*/
public static final FieldType TYPE = new FieldType();
static {
TYPE.setDimensions(2, Integer.BYTES);
TYPE.freeze();
}
/**
* Change the values of this field
* @param x x value.
* @param y y value.
*/
public void setLocationValue(float x, float y) {
final byte[] bytes;
if (fieldsData == null) {
bytes = new byte[8];
fieldsData = new BytesRef(bytes);
} else {
bytes = ((BytesRef) fieldsData).bytes;
}
int xEncoded = XYEncodingUtils.encode(x);
int yEncoded = XYEncodingUtils.encode(y);
NumericUtils.intToSortableBytes(xEncoded, bytes, 0);
NumericUtils.intToSortableBytes(yEncoded, bytes, Integer.BYTES);
}
/**
* Creates a new XYPoint with the specified x and y
* @param name field name
* @param x x value.
* @param y y value.
*/
public XYPointField(String name, float x, float y) {
super(name, TYPE);
setLocationValue(x, y);
}
@Override
public String toString() {
StringBuilder result = new StringBuilder();
result.append(getClass().getSimpleName());
result.append(" <");
result.append(name);
result.append(':');
byte bytes[] = ((BytesRef) fieldsData).bytes;
result.append(XYEncodingUtils.decode(bytes, 0));
result.append(',');
result.append(XYEncodingUtils.decode(bytes, Integer.BYTES));
result.append('>');
return result.toString();
}
/** helper: checks a fieldinfo and throws exception if its definitely not a XYPoint */
static void checkCompatible(FieldInfo fieldInfo) {
// point/dv properties could be "unset", if you e.g. used only StoredField with this same name in the segment.
if (fieldInfo.getPointDimensionCount() != 0 && fieldInfo.getPointDimensionCount() != TYPE.pointDimensionCount()) {
throw new IllegalArgumentException("field=\"" + fieldInfo.name + "\" was indexed with numDims=" + fieldInfo.getPointDimensionCount() +
" but this point type has numDims=" + TYPE.pointDimensionCount() +
", is the field really a XYPoint?");
}
if (fieldInfo.getPointNumBytes() != 0 && fieldInfo.getPointNumBytes() != TYPE.pointNumBytes()) {
throw new IllegalArgumentException("field=\"" + fieldInfo.name + "\" was indexed with bytesPerDim=" + fieldInfo.getPointNumBytes() +
" but this point type has bytesPerDim=" + TYPE.pointNumBytes() +
", is the field really a XYPoint?");
}
}
// static methods for generating queries
/**
* Create a query for matching a bounding box.
* <p>
* @param field field name. must not be null.
* @param minX x lower bound.
* @param maxX x upper bound.
* @param minY y lower bound.
* @param maxY y upper bound.
* @return query matching points within this box
* @throws IllegalArgumentException if {@code field} is null, or the box has invalid coordinates.
*/
public static Query newBoxQuery(String field, float minX, float maxX, float minY, float maxY) {
XYRectangle rectangle = new XYRectangle(minX, maxX, minY, maxY);
return new XYPointInGeometryQuery(field, rectangle);
}
/**
* Create a query for matching points within the specified distance of the supplied location.
* @param field field name. must not be null.
* @param x x at the center.
* @param y y at the center.
* @param radius maximum distance from the center in cartesian units: must be non-negative and finite.
* @return query matching points within this distance
* @throws IllegalArgumentException if {@code field} is null, location has invalid coordinates, or radius is invalid.
*/
public static Query newDistanceQuery(String field, float x, float y, float radius) {
XYCircle circle = new XYCircle(x, y, radius);
return new XYPointInGeometryQuery(field, circle);
}
/**
* Create a query for matching one or more polygons.
* @param field field name. must not be null.
* @param polygons array of polygons. must not be null or empty
* @return query matching points within this polygon
* @throws IllegalArgumentException if {@code field} is null, {@code polygons} is null or empty
* @see Polygon
*/
public static Query newPolygonQuery(String field, XYPolygon... polygons) {
return new XYPointInGeometryQuery(field, polygons);
}
}

View File

@ -0,0 +1,227 @@
/*
* 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.document;
import java.io.IOException;
import java.util.Arrays;
import org.apache.lucene.geo.Component2D;
import org.apache.lucene.geo.XYEncodingUtils;
import org.apache.lucene.geo.XYGeometry;
import org.apache.lucene.index.FieldInfo;
import org.apache.lucene.index.LeafReader;
import org.apache.lucene.index.LeafReaderContext;
import org.apache.lucene.index.PointValues;
import org.apache.lucene.index.PointValues.IntersectVisitor;
import org.apache.lucene.index.PointValues.Relation;
import org.apache.lucene.search.ConstantScoreScorer;
import org.apache.lucene.search.ConstantScoreWeight;
import org.apache.lucene.search.DocIdSetIterator;
import org.apache.lucene.search.IndexSearcher;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.QueryVisitor;
import org.apache.lucene.search.ScoreMode;
import org.apache.lucene.search.Scorer;
import org.apache.lucene.search.ScorerSupplier;
import org.apache.lucene.search.Weight;
import org.apache.lucene.util.DocIdSetBuilder;
/** Finds all previously indexed points that fall within the specified XY geometries.
*
* <p>The field must be indexed with using {@link XYPointField} added per document.
*
* @lucene.experimental */
final class XYPointInGeometryQuery extends Query {
final String field;
final XYGeometry[] xyGeometries;
XYPointInGeometryQuery(String field, XYGeometry... xyGeometries) {
if (field == null) {
throw new IllegalArgumentException("field must not be null");
}
if (xyGeometries == null) {
throw new IllegalArgumentException("geometries must not be null");
}
if (xyGeometries.length == 0) {
throw new IllegalArgumentException("geometries must not be empty");
}
this.field = field;
this.xyGeometries = xyGeometries.clone();
}
@Override
public void visit(QueryVisitor visitor) {
if (visitor.acceptField(field)) {
visitor.visitLeaf(this);
}
}
private IntersectVisitor getIntersectVisitor(DocIdSetBuilder result, Component2D tree) {
return new IntersectVisitor() {
DocIdSetBuilder.BulkAdder adder;
@Override
public void grow(int count) {
adder = result.grow(count);
}
@Override
public void visit(int docID) {
adder.add(docID);
}
@Override
public void visit(int docID, byte[] packedValue) {
double x = XYEncodingUtils.decode(packedValue, 0);
double y = XYEncodingUtils.decode(packedValue, Integer.BYTES);
if (tree.contains(x, y)) {
visit(docID);
}
}
@Override
public void visit(DocIdSetIterator iterator, byte[] packedValue) throws IOException {
double x = XYEncodingUtils.decode(packedValue, 0);
double y = XYEncodingUtils.decode(packedValue, Integer.BYTES);
if (tree.contains(x, y)) {
int docID;
while ((docID = iterator.nextDoc()) != DocIdSetIterator.NO_MORE_DOCS) {
visit(docID);
}
}
}
@Override
public Relation compare(byte[] minPackedValue, byte[] maxPackedValue) {
double cellMinX = XYEncodingUtils.decode(minPackedValue, 0);
double cellMinY = XYEncodingUtils.decode(minPackedValue, Integer.BYTES);
double cellMaxX = XYEncodingUtils.decode(maxPackedValue, 0);
double cellMaxY = XYEncodingUtils.decode(maxPackedValue, Integer.BYTES);
return tree.relate(cellMinX, cellMaxX, cellMinY, cellMaxY);
}
};
}
@Override
public Weight createWeight(IndexSearcher searcher, ScoreMode scoreMode, float boost) throws IOException {
final Component2D tree = XYGeometry.create(xyGeometries);
return new ConstantScoreWeight(this, boost) {
@Override
public ScorerSupplier scorerSupplier(LeafReaderContext context) throws IOException {
LeafReader reader = context.reader();
PointValues values = reader.getPointValues(field);
if (values == null) {
// No docs in this segment had any points fields
return null;
}
FieldInfo fieldInfo = reader.getFieldInfos().fieldInfo(field);
if (fieldInfo == null) {
// No docs in this segment indexed this field at all
return null;
}
XYPointField.checkCompatible(fieldInfo);
final Weight weight = this;
return new ScorerSupplier() {
long cost = -1;
DocIdSetBuilder result = new DocIdSetBuilder(reader.maxDoc(), values, field);
final IntersectVisitor visitor = getIntersectVisitor(result, tree);
@Override
public Scorer get(long leadCost) throws IOException {
values.intersect(visitor);
return new ConstantScoreScorer(weight, score(), scoreMode, result.build().iterator());
}
@Override
public long cost() {
if (cost == -1) {
// Computing the cost may be expensive, so only do it if necessary
cost = values.estimateDocCount(visitor);
assert cost >= 0;
}
return cost;
}
};
}
@Override
public Scorer scorer(LeafReaderContext context) throws IOException {
ScorerSupplier scorerSupplier = scorerSupplier(context);
if (scorerSupplier == null) {
return null;
}
return scorerSupplier.get(Long.MAX_VALUE);
}
@Override
public boolean isCacheable(LeafReaderContext ctx) {
return true;
}
};
}
/** Returns the query field */
public String getField() {
return field;
}
/** Returns a copy of the internal geometries array */
public XYGeometry[] getGeometries() {
return xyGeometries.clone();
}
@Override
public int hashCode() {
final int prime = 31;
int result = classHash();
result = prime * result + field.hashCode();
result = prime * result + Arrays.hashCode(xyGeometries);
return result;
}
@Override
public boolean equals(Object other) {
return sameClassAs(other) &&
equalsTo(getClass().cast(other));
}
private boolean equalsTo(XYPointInGeometryQuery other) {
return field.equals(other.field) &&
Arrays.equals(xyGeometries, other.xyGeometries);
}
@Override
public String toString(String field) {
final StringBuilder sb = new StringBuilder();
sb.append(getClass().getSimpleName());
sb.append(':');
if (this.field.equals(field) == false) {
sb.append(" field=");
sb.append(this.field);
sb.append(':');
}
sb.append(Arrays.toString(xyGeometries));
return sb.toString();
}
}

View File

@ -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.document;
import org.apache.lucene.search.FieldComparator;
import org.apache.lucene.search.SortField;
/**
* Sorts by distance from an origin location.
*/
final class XYPointSortField extends SortField {
final float x;
final float y;
XYPointSortField(String field, float x, float y) {
super(field, Type.CUSTOM);
if (field == null) {
throw new IllegalArgumentException("field must not be null");
}
this.x = x;
this.y = y;
setMissingValue(Double.POSITIVE_INFINITY);
}
@Override
public FieldComparator<?> getComparator(int numHits, int sortPos) {
return new XYPointDistanceComparator(getField(), x, y, 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 = Float.floatToIntBits(x);
result = prime * result + (int) (temp ^ (temp >>> 32));
temp = Float.floatToIntBits(y);
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;
XYPointSortField other = (XYPointSortField) obj;
if (x != other.x || y != other.y) return false;
return true;
}
@Override
public String toString() {
StringBuilder builder = new StringBuilder();
builder.append("<distance:");
builder.append('"');
builder.append(getField());
builder.append('"');
builder.append(" x=");
builder.append(x);
builder.append(" y=");
builder.append(y);
if (Double.POSITIVE_INFINITY != getMissingValue()) {
builder.append(" missingValue=").append(getMissingValue());
}
builder.append('>');
return builder.toString();
}
}

View File

@ -0,0 +1,45 @@
/*
* 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.search;
import org.apache.lucene.document.Document;
import org.apache.lucene.document.XYDocValuesField;
import org.apache.lucene.geo.BaseXYPointTestCase;
import org.apache.lucene.geo.XYPolygon;
public class TestXYDocValuesQueries extends BaseXYPointTestCase {
@Override
protected void addPointToDoc(String field, Document doc, float x, float y) {
doc.add(new XYDocValuesField(field, x, y));
}
@Override
protected Query newRectQuery(String field, float minX, float maxX, float minY, float maxY) {
return XYDocValuesField.newSlowBoxQuery(field, minX, maxX, minY, maxY);
}
@Override
protected Query newDistanceQuery(String field, float centerX, float centerY, float radius) {
return XYDocValuesField.newSlowDistanceQuery(field, centerX, centerY, radius);
}
@Override
protected Query newPolygonQuery(String field, XYPolygon... polygons) {
return XYDocValuesField.newSlowPolygonQuery(field, polygons);
}
}

View File

@ -0,0 +1,265 @@
/*
* 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.search;
import java.io.IOException;
import java.util.Arrays;
import org.apache.lucene.document.Document;
import org.apache.lucene.document.NumericDocValuesField;
import org.apache.lucene.document.StoredField;
import org.apache.lucene.document.XYDocValuesField;
import org.apache.lucene.geo.ShapeTestUtil;
import org.apache.lucene.index.IndexReader;
import org.apache.lucene.index.IndexWriterConfig;
import org.apache.lucene.index.RandomIndexWriter;
import org.apache.lucene.index.SerialMergeScheduler;
import org.apache.lucene.store.Directory;
import org.apache.lucene.util.LuceneTestCase;
import org.apache.lucene.util.TestUtil;
/** Simple tests for {@link XYDocValuesField#newDistanceSort} */
public class TestXYPointDistanceSort extends LuceneTestCase {
private double cartesianDistance(double x1, double y1, double x2, double y2) {
final double diffX = x1 - x2;
final double diffY = y1 - y2;
return Math.sqrt(diffX * diffX + diffY * diffY);
}
/** Add three points and sort by distance */
public void testDistanceSort() throws Exception {
Directory dir = newDirectory();
RandomIndexWriter iw = new RandomIndexWriter(random(), dir);
// add some docs
Document doc = new Document();
doc.add(new XYDocValuesField("location", 40.759011f, -73.9844722f));
iw.addDocument(doc);
double d1 = cartesianDistance(40.759011f, -73.9844722f, 40.7143528f, -74.0059731f);
doc = new Document();
doc.add(new XYDocValuesField("location", 40.718266f, -74.007819f));
iw.addDocument(doc);
double d2 = cartesianDistance(40.718266f, -74.007819f, 40.7143528f, -74.0059731f);
doc = new Document();
doc.add(new XYDocValuesField("location", 40.7051157f, -74.0088305f));
iw.addDocument(doc);
double d3 = cartesianDistance(40.7051157f, -74.0088305f, 40.7143528f, -74.0059731f);
IndexReader reader = iw.getReader();
IndexSearcher searcher = newSearcher(reader);
iw.close();
Sort sort = new Sort(XYDocValuesField.newDistanceSort("location", 40.7143528f, -74.0059731f));
TopDocs td = searcher.search(new MatchAllDocsQuery(), 3, sort);
FieldDoc d = (FieldDoc) td.scoreDocs[0];
assertEquals(d2, (Double)d.fields[0], 0.0D);
d = (FieldDoc) td.scoreDocs[1];
assertEquals(d3, (Double)d.fields[0], 0.0D);
d = (FieldDoc) td.scoreDocs[2];
assertEquals(d1, (Double)d.fields[0], 0.0D);
reader.close();
dir.close();
}
/** Add two points (one doc missing) and sort by distance */
public void testMissingLast() throws Exception {
Directory dir = newDirectory();
RandomIndexWriter iw = new RandomIndexWriter(random(), dir);
// missing
Document doc = new Document();
iw.addDocument(doc);
doc = new Document();
doc.add(new XYDocValuesField("location", 40.718266f, -74.007819f));
iw.addDocument(doc);
double d2 = cartesianDistance(40.718266f, -74.007819f, 40.7143528f, -74.0059731f);
doc = new Document();
doc.add(new XYDocValuesField("location", 40.7051157f, -74.0088305f));
iw.addDocument(doc);
double d3 = cartesianDistance(40.7051157f, -74.0088305f, 40.7143528f, -74.0059731f);
IndexReader reader = iw.getReader();
IndexSearcher searcher = newSearcher(reader);
iw.close();
Sort sort = new Sort(XYDocValuesField.newDistanceSort("location", 40.7143528f, -74.0059731f));
TopDocs td = searcher.search(new MatchAllDocsQuery(), 3, sort);
FieldDoc d = (FieldDoc) td.scoreDocs[0];
assertEquals(d2, (Double)d.fields[0], 0.0D);
d = (FieldDoc) td.scoreDocs[1];
assertEquals(d3, (Double)d.fields[0], 0.0D);
d = (FieldDoc) td.scoreDocs[2];
assertEquals(Double.POSITIVE_INFINITY, (Double)d.fields[0], 0.0D);
reader.close();
dir.close();
}
/** Run a few iterations with just 10 docs, hopefully easy to debug */
public void testRandom() throws Exception {
for (int iters = 0; iters < 100; iters++) {
doRandomTest(10, 100);
}
}
/** Runs with thousands of docs */
@Nightly
public void testRandomHuge() throws Exception {
for (int iters = 0; iters < 10; iters++) {
doRandomTest(2000, 100);
}
}
// result class used for testing. holds an id+distance.
// we sort these with Arrays.sort and compare with lucene's results
static class Result implements Comparable<Result> {
int id;
double distance;
Result(int id, double distance) {
this.id = id;
this.distance = distance;
}
@Override
public int compareTo(Result o) {
int cmp = Double.compare(distance, o.distance);
if (cmp == 0) {
return Integer.compare(id, o.id);
}
return cmp;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
long temp;
temp = Double.doubleToLongBits(distance);
result = prime * result + (int) (temp ^ (temp >>> 32));
result = prime * result + id;
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null) return false;
if (getClass() != obj.getClass()) return false;
Result other = (Result) obj;
if (Double.doubleToLongBits(distance) != Double.doubleToLongBits(other.distance)) return false;
if (id != other.id) return false;
return true;
}
@Override
public String toString() {
return "Result [id=" + id + ", distance=" + distance + "]";
}
}
private void doRandomTest(int numDocs, int numQueries) throws IOException {
Directory dir = newDirectory();
IndexWriterConfig iwc = newIndexWriterConfig();
// else seeds may not to reproduce:
iwc.setMergeScheduler(new SerialMergeScheduler());
RandomIndexWriter writer = new RandomIndexWriter(random(), dir, iwc);
for (int i = 0; i < numDocs; i++) {
Document doc = new Document();
doc.add(new StoredField("id", i));
doc.add(new NumericDocValuesField("id", i));
if (random().nextInt(10) > 7) {
float x = ShapeTestUtil.nextFloat(random());
float y = ShapeTestUtil.nextFloat(random());
doc.add(new XYDocValuesField("field", x, y));
doc.add(new StoredField("x", x));
doc.add(new StoredField("y", y));
} // otherwise "missing"
writer.addDocument(doc);
}
IndexReader reader = writer.getReader();
IndexSearcher searcher = newSearcher(reader);
for (int i = 0; i < numQueries; i++) {
float x = ShapeTestUtil.nextFloat(random());
float y = ShapeTestUtil.nextFloat(random());
double missingValue = Double.POSITIVE_INFINITY;
Result expected[] = new Result[reader.maxDoc()];
for (int doc = 0; doc < reader.maxDoc(); doc++) {
Document targetDoc = reader.document(doc);
final double distance;
if (targetDoc.getField("x") == null) {
distance = missingValue; // missing
} else {
double docX = targetDoc.getField("x").numericValue().floatValue();
double docY = targetDoc.getField("y").numericValue().floatValue();
distance = cartesianDistance(x, y, docX, docY);
}
int id = targetDoc.getField("id").numericValue().intValue();
expected[doc] = new Result(id, distance);
}
Arrays.sort(expected);
// randomize the topN a bit
int topN = TestUtil.nextInt(random(), 1, reader.maxDoc());
// sort by distance, then ID
SortField distanceSort = XYDocValuesField.newDistanceSort("field", x, y);
distanceSort.setMissingValue(missingValue);
Sort sort = new Sort(distanceSort,
new SortField("id", SortField.Type.INT));
TopDocs topDocs = searcher.search(new MatchAllDocsQuery(), topN, sort);
for (int resultNumber = 0; resultNumber < topN; resultNumber++) {
FieldDoc fieldDoc = (FieldDoc) topDocs.scoreDocs[resultNumber];
Result actual = new Result((Integer) fieldDoc.fields[1], (Double) fieldDoc.fields[0]);
assertEquals(expected[resultNumber], actual);
}
// get page2 with searchAfter()
if (topN < reader.maxDoc()) {
int page2 = TestUtil.nextInt(random(), 1, reader.maxDoc() - topN);
TopDocs topDocs2 = searcher.searchAfter(topDocs.scoreDocs[topN - 1], new MatchAllDocsQuery(), page2, sort);
for (int resultNumber = 0; resultNumber < page2; resultNumber++) {
FieldDoc fieldDoc = (FieldDoc) topDocs2.scoreDocs[resultNumber];
Result actual = new Result((Integer) fieldDoc.fields[1], (Double) fieldDoc.fields[0]);
assertEquals(expected[topN + resultNumber], actual);
}
}
}
reader.close();
writer.close();
dir.close();
}
}

View File

@ -0,0 +1,45 @@
/*
* 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.search;
import org.apache.lucene.document.Document;
import org.apache.lucene.document.XYPointField;
import org.apache.lucene.geo.BaseXYPointTestCase;
import org.apache.lucene.geo.XYPolygon;
public class TestXYPointQueries extends BaseXYPointTestCase {
@Override
protected void addPointToDoc(String field, Document doc, float x, float y) {
doc.add(new XYPointField(field, x, y));
}
@Override
protected Query newRectQuery(String field, float minX, float maxX, float minY, float maxY) {
return XYPointField.newBoxQuery(field, minX, maxX, minY, maxY);
}
@Override
protected Query newDistanceQuery(String field, float centerX, float centerY, float radius) {
return XYPointField.newDistanceQuery(field, centerX, centerY, radius);
}
@Override
protected Query newPolygonQuery(String field, XYPolygon... polygons) {
return XYPointField.newPolygonQuery(field, polygons);
}
}

File diff suppressed because it is too large Load Diff

View File

@ -21,7 +21,6 @@ import java.util.Random;
import com.carrotsearch.randomizedtesting.RandomizedContext;
import com.carrotsearch.randomizedtesting.generators.BiasedNumbers;
import org.apache.lucene.util.LuceneTestCase;
import org.apache.lucene.util.TestUtil;
@ -42,7 +41,7 @@ public class ShapeTestUtil {
try {
return createRegularPolygon(nextFloat(random), nextFloat(random), radius, gons);
} catch (IllegalArgumentException iae) {
// we tried to cross dateline or pole ... try again
// something went wrong, try again
}
}
}
@ -136,9 +135,7 @@ public class ShapeTestUtil {
}
private static XYPolygon surpriseMePolygon(Random random) {
// repeat until we get a poly that doesn't cross dateline:
while (true) {
//System.out.println("\nPOLY ITER");
float centerX = nextFloat(random);
float centerY = nextFloat(random);
double radius = 0.1 + 20 * random.nextDouble();
@ -149,7 +146,6 @@ public class ShapeTestUtil {
double angle = 0.0;
while (true) {
angle += random.nextDouble()*40.0;
//System.out.println(" angle " + angle);
if (angle > 360) {
break;
}
@ -159,14 +155,11 @@ public class ShapeTestUtil {
len = StrictMath.min(len, StrictMath.min(maxX, maxY));
//System.out.println(" len=" + len);
float x = (float)(centerX + len * Math.cos(Math.toRadians(angle)));
float y = (float)(centerY + len * Math.sin(Math.toRadians(angle)));
xList.add(x);
yList.add(y);
//System.out.println(" lat=" + lats.get(lats.size()-1) + " lon=" + lons.get(lons.size()-1));
}
// close it
@ -222,4 +215,67 @@ public class ShapeTestUtil {
private static Random random() {
return RandomizedContext.current().getRandom();
}
/**
* Simple slow point in polygon check (for testing)
*/
// direct port of PNPOLY C code (https://www.ecse.rpi.edu/~wrf/Research/Short_Notes/pnpoly.html)
// this allows us to improve the code yet still ensure we have its properties
// it is 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.
public static boolean containsSlowly(XYPolygon polygon, double x, double y) {
if (polygon.getHoles().length > 0) {
throw new UnsupportedOperationException("this testing method does not support holes");
}
double polyXs[] = XYEncodingUtils.floatArrayToDoubleArray(polygon.getPolyX());
double polyYs[] =XYEncodingUtils.floatArrayToDoubleArray(polygon.getPolyY());
// bounding box check required due to rounding errors (we don't solve that problem)
if (x < polygon.minX || x > polygon.maxX || y < polygon.minY || y > polygon.maxY) {
return false;
}
boolean c = false;
int i, j;
int nvert = polyYs.length;
double verty[] = polyYs;
double vertx[] = polyXs;
double testy = y;
double testx = x;
for (i = 0, j = 1; j < nvert; ++i, ++j) {
if (testy == verty[j] && testy == verty[i] ||
((testy <= verty[j] && testy >= verty[i]) != (testy >= verty[j] && testy <= verty[i]))) {
if ((testx == vertx[j] && testx == vertx[i]) ||
((testx <= vertx[j] && testx >= vertx[i]) != (testx >= vertx[j] && testx <= vertx[i]) &&
GeoUtils.orient(vertx[i], verty[i], vertx[j], verty[j], testx, testy) == 0)) {
// return true if point is on boundary
return true;
} else if ( ((verty[i] > testy) != (verty[j] > testy)) &&
(testx < (vertx[j]-vertx[i]) * (testy-verty[i]) / (verty[j]-verty[i]) + vertx[i]) ) {
c = !c;
}
}
}
return c;
}
}