blob: 0d0cb4dabbf8d0002d3082ae12520d0f5e858773 [file] [log] [blame]
/*
* 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.geo;
import org.apache.lucene.index.PointValues.Relation;
import org.apache.lucene.util.SloppyMath;
/**
* 2D circle implementation containing spatial logic.
*/
class Circle2D implements Component2D {
private final DistanceCalculator calculator;
private Circle2D(DistanceCalculator calculator) {
this.calculator = calculator;
}
@Override
public double getMinX() {
return calculator.getMinX();
}
@Override
public double getMaxX() {
return calculator.getMaxX();
}
@Override
public double getMinY() {
return calculator.getMinY();
}
@Override
public double getMaxY() {
return calculator.getMaxY();
}
@Override
public boolean contains(double x, double y) {
return calculator.contains(x, y);
}
@Override
public Relation relate(double minX, double maxX, double minY, double maxY) {
if (calculator.disjoint(minX, maxX, minY, maxY)) {
return Relation.CELL_OUTSIDE_QUERY;
}
if (calculator.within(minX, maxX, minY, maxY)) {
return Relation.CELL_CROSSES_QUERY;
}
return calculator.relate(minX, maxX, minY, maxY);
}
@Override
public boolean intersectsLine(double minX, double maxX, double minY, double maxY,
double aX, double aY, double bX, double bY) {
if (calculator.disjoint(minX, maxX, minY, maxY)) {
return false;
}
return contains(aX, aY) || contains(bX, bY) ||
calculator.intersectsLine(aX, aY, bX, bY);
}
@Override
public boolean intersectsTriangle(double minX, double maxX, double minY, double maxY,
double aX, double aY, double bX, double bY, double cX, double cY) {
if (calculator.disjoint(minX, maxX, minY, maxY)) {
return false;
}
return contains(aX, aY) || contains(bX, bY) || contains(cX, cY) ||
Component2D.pointInTriangle(minX, maxX, minY, maxY, calculator.geX(), calculator.getY(), aX, aY, bX, bY, cX, cY) ||
calculator.intersectsLine(aX, aY, bX, bY) ||
calculator.intersectsLine(bX, bY, cX, cY) ||
calculator.intersectsLine(cX, cY, aX, aY);
}
@Override
public boolean containsLine(double minX, double maxX, double minY, double maxY,
double aX, double aY, double bX, double bY) {
if (calculator.disjoint(minX, maxX, minY, maxY)) {
return false;
}
return contains(aX, aY) && contains(bX, bY);
}
@Override
public boolean containsTriangle(double minX, double maxX, double minY, double maxY,
double aX, double aY, double bX, double bY, double cX, double cY) {
if (calculator.disjoint(minX, maxX, minY, maxY)) {
return false;
}
return contains(aX, aY) && contains(bX, bY) && contains(cX, cY);
}
@Override
public WithinRelation withinPoint(double x, double y) {
return contains(x, y) ? WithinRelation.NOTWITHIN : WithinRelation.DISJOINT;
}
@Override
public WithinRelation withinLine(double minX, double maxX, double minY, double maxY,
double aX, double aY, boolean ab, double bX, double bY) {
if (calculator.disjoint(minX, maxX, minY, maxY)) {
return WithinRelation.DISJOINT;
}
if (ab == true && calculator.intersectsLine(aX, aY, bX, bY)) {
return WithinRelation.NOTWITHIN;
}
return WithinRelation.DISJOINT;
}
@Override
public WithinRelation withinTriangle(double minX, double maxX, double minY, double maxY,
double aX, double aY, boolean ab, double bX, double bY, boolean bc, double cX, double cY, boolean ca) {
if (calculator.disjoint(minX, maxX, minY, maxY)) {
return WithinRelation.DISJOINT;
}
// if any of the points is inside the circle then we cannot be within this
// indexed shape
if (contains(aX, aY) || contains(bX, bY) || contains(cX, cY)) {
return WithinRelation.NOTWITHIN;
}
// we only check edges that belong to the original polygon. If we intersect any of them, then
// we are not within.
if (ab == true && calculator.intersectsLine(aX, aY, bX, bY)) {
return WithinRelation.NOTWITHIN;
}
if (bc == true && calculator.intersectsLine(bX, bY, cX, cY)) {
return WithinRelation.NOTWITHIN;
}
if (ca == true && calculator.intersectsLine(cX, cY, aX, aY)) {
return WithinRelation.NOTWITHIN;
}
// check if center is within the triangle. This is the only check that returns this circle as a candidate but that is ol
// is fine as the center must be inside to be one of the triangles.
if (Component2D.pointInTriangle(minX, maxX, minY, maxY, calculator.geX(), calculator.getY(), aX, aY, bX, bY, cX, cY) == true) {
return WithinRelation.CANDIDATE;
}
return WithinRelation.DISJOINT;
}
private static boolean intersectsLine(double centerX, double centerY, double aX, double aY, double bX, double bY, DistanceCalculator calculator) {
//Algorithm based on this thread : https://stackoverflow.com/questions/3120357/get-closest-point-to-a-line
final double vectorAPX = centerX - aX;
final double vectorAPY = centerY - aY;
final double vectorABX = bX - aX;
final double vectorABY = bY - aY;
final double magnitudeAB = vectorABX * vectorABX + vectorABY * vectorABY;
final double dotProduct = vectorAPX * vectorABX + vectorAPY * vectorABY;
final double distance = dotProduct / magnitudeAB;
if (distance < 0 || distance > 1) {
return false;
}
final double pX = aX + vectorABX * distance;
final double pY = aY + vectorABY * distance;
final double minX = StrictMath.min(aX, bX);
final double minY = StrictMath.min(aY, bY);
final double maxX = StrictMath.max(aX, bX);
final double maxY = StrictMath.max(aY, bY);
if (pX >= minX && pX <= maxX && pY >= minY && pY <= maxY) {
return calculator.contains(pX, pY);
}
return false;
}
private interface DistanceCalculator {
/** check if the point is within a distance */
boolean contains(double x, double y);
/** check if the line is within a distance */
boolean intersectsLine(double aX, double aY, double bX, double bY);
/** Relates this calculator to the provided bounding box */
Relation relate(double minX, double maxX, double minY, double maxY);
/** check if the bounding box is disjoint with this calculator bounding box */
boolean disjoint(double minX, double maxX, double minY, double maxY);
/** check if the bounding box is contains this calculator bounding box */
boolean within(double minX, double maxX, double minY, double maxY);
/** get min X of this calculator */
double getMinX();
/** get max X of this calculator */
double getMaxX();
/** get min Y of this calculator */
double getMinY();
/** get max Y of this calculator */
double getMaxY();
/** get center X */
double geX();
/** get center Y */
double getY();
}
private static class CartesianDistance implements DistanceCalculator {
private final double centerX;
private final double centerY;
private final double radiusSquared;
private final XYRectangle rectangle;
public CartesianDistance(float centerX, float centerY, float radius) {
this.centerX = centerX;
this.centerY = centerY;
this.rectangle = XYRectangle.fromPointDistance(centerX, centerY, radius);
// product performed with doubles
this.radiusSquared = (double) radius * radius;
}
@Override
public Relation relate(double minX, double maxX, double minY, double maxY) {
if (Component2D.containsPoint(centerX, centerY, minX, maxX, minY, maxY)) {
if (contains(minX, minY) && contains(maxX, minY) && contains(maxX, maxY) && contains(minX, maxY)) {
// we are fully enclosed, collect everything within this subtree
return Relation.CELL_INSIDE_QUERY;
}
} else {
// circle not fully inside, compute closest distance
double sumOfSquaredDiffs = 0.0d;
if (centerX < minX) {
double diff = minX - centerX;
sumOfSquaredDiffs += diff * diff;
} else if (centerX > maxX) {
double diff = maxX - centerX;
sumOfSquaredDiffs += diff * diff;
}
if (centerY < minY) {
double diff = minY - centerY;
sumOfSquaredDiffs += diff * diff;
} else if (centerY > maxY) {
double diff = maxY - centerY;
sumOfSquaredDiffs += diff * diff;
}
if (sumOfSquaredDiffs > radiusSquared) {
// disjoint
return Relation.CELL_OUTSIDE_QUERY;
}
}
return Relation.CELL_CROSSES_QUERY;
}
@Override
public boolean contains(double x, double y) {
if (Component2D.containsPoint(x, y, rectangle.minX, rectangle.maxX, rectangle.minY, rectangle.maxY)) {
final double diffX = x - this.centerX;
final double diffY = y - this.centerY;
return diffX * diffX + diffY * diffY <= radiusSquared;
}
return false;
}
@Override
public boolean intersectsLine(double aX, double aY, double bX, double bY) {
return Circle2D.intersectsLine(centerX, centerY, aX, aY, bX, bY, this);
}
@Override
public boolean disjoint(double minX, double maxX, double minY, double maxY) {
return Component2D.disjoint(rectangle.minX, rectangle.maxX, rectangle.minY, rectangle.maxY, minX, maxX, minY, maxY);
}
@Override
public boolean within(double minX, double maxX, double minY, double maxY) {
return Component2D.within(rectangle.minX, rectangle.maxX, rectangle.minY, rectangle.maxY, minX, maxX, minY, maxY);
}
@Override
public double getMinX() {
return rectangle.minX;
}
@Override
public double getMaxX() {
return rectangle.maxX;
}
@Override
public double getMinY() {
return rectangle.minY;
}
@Override
public double getMaxY() {
return rectangle.maxY;
}
@Override
public double geX() {
return centerX;
}
@Override
public double getY() {
return centerY;
}
}
private static class HaversinDistance implements DistanceCalculator {
final double centerLat;
final double centerLon;
final double sortKey;
final double axisLat;
final Rectangle rectangle;
final boolean crossesDateline;
public HaversinDistance(double centerLon, double centerLat, double radius) {
this.centerLat = centerLat;
this.centerLon = centerLon;
this.sortKey = GeoUtils.distanceQuerySortKey(radius);
this.axisLat = Rectangle.axisLat(centerLat, radius);
this.rectangle = Rectangle.fromPointDistance(centerLat, centerLon, radius);
this.crossesDateline = rectangle.minLon > rectangle.maxLon;
}
@Override
public Relation relate(double minX, double maxX, double minY, double maxY) {
return GeoUtils.relate(minY, maxY, minX, maxX, centerLat, centerLon, sortKey, axisLat);
}
@Override
public boolean contains(double x, double y) {
if (crossesDateline) {
if (Component2D.containsPoint(x, y, rectangle.minLon, GeoUtils.MAX_LON_INCL, rectangle.minLat, rectangle.maxLat) ||
Component2D.containsPoint(x, y, GeoUtils.MIN_LON_INCL, rectangle.maxLon, rectangle.minLat, rectangle.maxLat)) {
return SloppyMath.haversinSortKey(y, x, this.centerLat, this.centerLon) <= sortKey;
}
} else {
if (Component2D.containsPoint(x, y, rectangle.minLon, rectangle.maxLon, rectangle.minLat, rectangle.maxLat)) {
return SloppyMath.haversinSortKey(y, x, this.centerLat, this.centerLon) <= sortKey;
}
}
return false;
}
@Override
public boolean intersectsLine(double aX, double aY, double bX, double bY) {
if (Circle2D.intersectsLine(centerLon, centerLat, aX, aY, bX, bY, this)) {
return true;
}
if (crossesDateline) {
double newCenterLon = (centerLon > 0) ? centerLon - 360 : centerLon + 360;
return Circle2D.intersectsLine(newCenterLon, centerLat, aX, aY, bX, bY, this);
}
return false;
}
@Override
public boolean disjoint(double minX, double maxX, double minY, double maxY) {
if (crossesDateline) {
return Component2D.disjoint(rectangle.minLon, GeoUtils.MAX_LON_INCL, rectangle.minLat, rectangle.maxLat, minX, maxX, minY, maxY)
&& Component2D.disjoint(GeoUtils.MIN_LON_INCL, rectangle.maxLon, rectangle.minLat, rectangle.maxLat, minX, maxX, minY, maxY);
} else {
return Component2D.disjoint(rectangle.minLon, rectangle.maxLon, rectangle.minLat, rectangle.maxLat, minX, maxX, minY, maxY);
}
}
@Override
public boolean within(double minX, double maxX, double minY, double maxY) {
if (crossesDateline) {
return Component2D.within(rectangle.minLon, GeoUtils.MAX_LON_INCL, rectangle.minLat, rectangle.maxLat, minX, maxX, minY, maxY)
|| Component2D.within(GeoUtils.MIN_LON_INCL, rectangle.maxLon, rectangle.minLat, rectangle.maxLat, minX, maxX, minY, maxY);
} else {
return Component2D.within(rectangle.minLon, rectangle.maxLon, rectangle.minLat, rectangle.maxLat, minX, maxX, minY, maxY);
}
}
@Override
public double getMinX() {
if (crossesDateline) {
// Component2D does not support boxes that crosses the dateline
return GeoUtils.MIN_LON_INCL;
}
return rectangle.minLon;
}
@Override
public double getMaxX() {
if (crossesDateline) {
// Component2D does not support boxes that crosses the dateline
return GeoUtils.MAX_LON_INCL;
}
return rectangle.maxLon;
}
@Override
public double getMinY() {
return rectangle.minLat;
}
@Override
public double getMaxY() {
return rectangle.maxLat;
}
@Override
public double geX() {
return centerLon;
}
@Override
public double getY() {
return centerLat;
}
}
/** Builds a XYCircle2D from XYCircle. Distance calculations are performed using cartesian distance.*/
static Component2D create(XYCircle circle) {
DistanceCalculator calculator = new CartesianDistance(circle.getX(), circle.getY(), circle.getRadius());
return new Circle2D(calculator);
}
/** Builds a Circle2D from Circle. Distance calculations are performed using haversin distance. */
static Component2D create(Circle circle) {
DistanceCalculator calculator = new HaversinDistance(circle.getLon(), circle.getLat(), circle.getRadius());
return new Circle2D(calculator);
}
}