/*******************************************************************************
* Copyright (c) 2015 Voyager Search and MITRE
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Apache License, Version 2.0 which
* accompanies this distribution and is available at
* http://www.apache.org/licenses/LICENSE-2.0.txt
******************************************************************************/
package org.locationtech.spatial4j.shape.impl;
import org.locationtech.spatial4j.context.SpatialContext;
import org.locationtech.spatial4j.shape.BaseShape;
import org.locationtech.spatial4j.shape.Circle;
import org.locationtech.spatial4j.shape.Point;
import org.locationtech.spatial4j.shape.Rectangle;
import org.locationtech.spatial4j.shape.Shape;
import org.locationtech.spatial4j.shape.SpatialRelation;
/**
* A circle, also known as a point-radius, based on a {@link
* org.locationtech.spatial4j.distance.DistanceCalculator} which does all the work. This
* implementation should work for both cartesian 2D and geodetic sphere
* surfaces.
*/
public class CircleImpl extends BaseShape<SpatialContext> implements Circle {
protected final Point point;
protected double radiusDEG;
// calculated & cached
protected Rectangle enclosingBox;
public CircleImpl(Point p, double radiusDEG, SpatialContext ctx) {
super(ctx);
//We assume any validation of params already occurred (including bounding dist)
this.point = p;
this.radiusDEG = point.isEmpty() ? Double.NaN : radiusDEG;
this.enclosingBox = point.isEmpty() ? ctx.makeRectangle(Double.NaN, Double.NaN, Double.NaN, Double.NaN) :
ctx.getDistCalc().calcBoxByDistFromPt(point, this.radiusDEG, ctx, null);
}
@Override
public void reset(double x, double y, double radiusDEG) {
assert ! isEmpty();
point.reset(x, y);
this.radiusDEG = radiusDEG;
this.enclosingBox = ctx.getDistCalc().calcBoxByDistFromPt(point, this.radiusDEG, ctx, enclosingBox);
}
@Override
public boolean isEmpty() {
return point.isEmpty();
}
@Override
public Point getCenter() {
return point;
}
@Override
public double getRadius() {
return radiusDEG;
}
@Override
public double getArea(SpatialContext ctx) {
if (ctx == null) {
return Math.PI * radiusDEG * radiusDEG;
} else {
return ctx.getDistCalc().area(this);
}
}
@Override
public Circle getBuffered(double distance, SpatialContext ctx) {
return ctx.makeCircle(point, distance + radiusDEG);
}
public boolean contains(double x, double y) {
return ctx.getDistCalc().within(point, x, y, radiusDEG);
}
@Override
public boolean hasArea() {
return radiusDEG > 0;
}
/**
* Note that the bounding box might contain a minX that is > maxX, due to WGS84 dateline.
*/
@Override
public Rectangle getBoundingBox() {
return enclosingBox;
}
@Override
public SpatialRelation relate(Shape other) {
//This shortcut was problematic in testing due to distinctions of CONTAINS/WITHIN for no-area shapes (lines, points).
// if (distance == 0) {
// return point.relate(other,ctx).intersects() ? SpatialRelation.WITHIN : SpatialRelation.DISJOINT;
// }
if (isEmpty() || other.isEmpty())
return SpatialRelation.DISJOINT;
if (other instanceof Point) {
return relate((Point) other);
}
if (other instanceof Rectangle) {
return relate((Rectangle) other);
}
if (other instanceof Circle) {
return relate((Circle) other);
}
return other.relate(this).transpose();
}
public SpatialRelation relate(Point point) {
return contains(point.getX(),point.getY()) ? SpatialRelation.CONTAINS : SpatialRelation.DISJOINT;
}
public SpatialRelation relate(Rectangle r) {
//Note: Surprisingly complicated!
//--We start by leveraging the fact we have a calculated bbox that is "cheaper" than use of DistanceCalculator.
final SpatialRelation bboxSect = enclosingBox.relate(r);
if (bboxSect == SpatialRelation.DISJOINT || bboxSect == SpatialRelation.WITHIN)
return bboxSect;
else if (bboxSect == SpatialRelation.CONTAINS && enclosingBox.equals(r))//nasty identity edge-case
return SpatialRelation.WITHIN;
//bboxSect is INTERSECTS or CONTAINS
//The result can be DISJOINT, CONTAINS, or INTERSECTS (not WITHIN)
return relateRectanglePhase2(r, bboxSect);
}
protected SpatialRelation relateRectanglePhase2(final Rectangle r, SpatialRelation bboxSect) {
// DOES NOT WORK WITH GEO CROSSING DATELINE OR WORLD-WRAP. Other methods handle such cases.
//At this point, the only thing we are certain of is that circle is *NOT* WITHIN r, since the
// bounding box of a circle MUST be within r for the circle to be within r.
//Quickly determine if they are DISJOINT or not.
// Find the closest & farthest point to the circle within the rectangle
final double closestX, farthestX;
final double xAxis = getXAxis();
if (xAxis < r.getMinX()) {
closestX = r.getMinX();
farthestX = r.getMaxX();
} else if (xAxis > r.getMaxX()) {
closestX = r.getMaxX();
farthestX = r.getMinX();
} else {
closestX = xAxis; //we don't really use this value but to check this condition
farthestX = r.getMaxX() - xAxis > xAxis - r.getMinX() ? r.getMaxX() : r.getMinX();
}
final double closestY, farthestY;
final double yAxis = getYAxis();
if (yAxis < r.getMinY()) {
closestY = r.getMinY();
farthestY = r.getMaxY();
} else if (yAxis > r.getMaxY()) {
closestY = r.getMaxY();
farthestY = r.getMinY();
} else {
closestY = yAxis; //we don't really use this value but to check this condition
farthestY = r.getMaxY() - yAxis > yAxis - r.getMinY() ? r.getMaxY() : r.getMinY();
}
//If r doesn't overlap an axis, then could be disjoint. Test closestXY
if (xAxis != closestX && yAxis != closestY) {
if (!contains(closestX, closestY))
return SpatialRelation.DISJOINT;
} // else CAN'T be disjoint if spans axis because earlier bbox check ruled that out
//Now, we know it's *NOT* DISJOINT and it's *NOT* WITHIN either.
// Does circle CONTAINS r or simply intersect it?
//If circle contains r, then its bbox MUST also CONTAIN r.
if (bboxSect != SpatialRelation.CONTAINS)
return SpatialRelation.INTERSECTS;
//If the farthest point of r away from the center of the circle is contained, then all of r is
// contained.
if (!contains(farthestX, farthestY))
return SpatialRelation.INTERSECTS;
//geodetic detection of farthest Y when rect crosses x axis can't be reliably determined, so
// check other corner too, which might actually be farthest
if (point.getY() != getYAxis()) {//geodetic
if (yAxis == closestY) {//r crosses north to south over x axis (confusing)
double otherY = (farthestY == r.getMaxY() ? r.getMinY() : r.getMaxY());
if (!contains(farthestX, otherY))
return SpatialRelation.INTERSECTS;
}
}
return SpatialRelation.CONTAINS;
}
/**
* The <code>Y</code> coordinate of where the circle axis intersect.
*/
protected double getYAxis() {
return point.getY();
}
/**
* The <code>X</code> coordinate of where the circle axis intersect.
*/
protected double getXAxis() {
return point.getX();
}
public SpatialRelation relate(Circle circle) {
double crossDist = ctx.getDistCalc().distance(point, circle.getCenter());
double aDist = radiusDEG, bDist = circle.getRadius();
if (crossDist > aDist + bDist)
return SpatialRelation.DISJOINT;
if (crossDist < aDist && crossDist + bDist <= aDist)
return SpatialRelation.CONTAINS;
if (crossDist < bDist && crossDist + aDist <= bDist)
return SpatialRelation.WITHIN;
return SpatialRelation.INTERSECTS;
}
@Override
public String toString() {
return "Circle(" + point + ", d=" + radiusDEG + "°)";
}
@Override
public boolean equals(Object obj) {
return equals(this,obj);
}
/**
* All {@link Circle} implementations should use this definition of {@link Object#equals(Object)}.
*/
public static boolean equals(Circle thiz, Object o) {
assert thiz != null;
if (thiz == o) return true;
if (!(o instanceof Circle)) return false;
Circle circle = (Circle) o;
if (!thiz.getCenter().equals(circle.getCenter())) return false;
if (Double.compare(circle.getRadius(), thiz.getRadius()) != 0) return false;
return true;
}
@Override
public int hashCode() {
return hashCode(this);
}
/**
* All {@link Circle} implementations should use this definition of {@link Object#hashCode()}.
*/
public static int hashCode(Circle thiz) {
int result;
long temp;
result = thiz.getCenter().hashCode();
temp = thiz.getRadius() != +0.0d ? Double.doubleToLongBits(thiz.getRadius()) : 0L;
result = 31 * result + (int) (temp ^ (temp >>> 32));
return result;
}
}