View Javadoc
1   /*******************************************************************************
2    * Copyright (c) 2015 Voyager Search and MITRE
3    * All rights reserved. This program and the accompanying materials
4    * are made available under the terms of the Apache License, Version 2.0 which
5    * accompanies this distribution and is available at
6    *    http://www.apache.org/licenses/LICENSE-2.0.txt
7    ******************************************************************************/
8   
9   package org.locationtech.spatial4j.shape.impl;
10  
11  import org.locationtech.spatial4j.context.SpatialContext;
12  import org.locationtech.spatial4j.shape.BaseShape;
13  import org.locationtech.spatial4j.shape.Circle;
14  import org.locationtech.spatial4j.shape.Point;
15  import org.locationtech.spatial4j.shape.Rectangle;
16  import org.locationtech.spatial4j.shape.Shape;
17  import org.locationtech.spatial4j.shape.SpatialRelation;
18  
19  /**
20   * A circle, also known as a point-radius, based on a {@link
21   * org.locationtech.spatial4j.distance.DistanceCalculator} which does all the work. This
22   * implementation should work for both cartesian 2D and geodetic sphere
23   * surfaces.
24   */
25  public class CircleImpl extends BaseShape<SpatialContext> implements Circle {
26  
27    protected final Point point;
28    protected double radiusDEG;
29  
30    // calculated & cached
31    protected Rectangle enclosingBox;
32  
33    public CircleImpl(Point p, double radiusDEG, SpatialContext ctx) {
34      super(ctx);
35      //We assume any validation of params already occurred (including bounding dist)
36      this.point = p;
37      this.radiusDEG = point.isEmpty() ? Double.NaN : radiusDEG;
38      this.enclosingBox = point.isEmpty() ? ctx.makeRectangle(Double.NaN, Double.NaN, Double.NaN, Double.NaN) :
39        ctx.getDistCalc().calcBoxByDistFromPt(point, this.radiusDEG, ctx, null);
40    }
41  
42    @Override
43    public void reset(double x, double y, double radiusDEG) {
44      assert ! isEmpty();
45      point.reset(x, y);
46      this.radiusDEG = radiusDEG;
47      this.enclosingBox = ctx.getDistCalc().calcBoxByDistFromPt(point, this.radiusDEG, ctx, enclosingBox);
48    }
49  
50    @Override
51    public boolean isEmpty() {
52      return point.isEmpty();
53    }
54  
55    @Override
56    public Point getCenter() {
57      return point;
58    }
59  
60    @Override
61    public double getRadius() {
62      return radiusDEG;
63    }
64  
65    @Override
66    public double getArea(SpatialContext ctx) {
67      if (ctx == null) {
68        return Math.PI * radiusDEG * radiusDEG;
69      } else {
70        return ctx.getDistCalc().area(this);
71      }
72    }
73  
74    @Override
75    public Circle getBuffered(double distance, SpatialContext ctx) {
76      return ctx.makeCircle(point, distance + radiusDEG);
77    }
78  
79    public boolean contains(double x, double y) {
80      return ctx.getDistCalc().within(point, x, y, radiusDEG);
81    }
82  
83    @Override
84    public boolean hasArea() {
85      return radiusDEG > 0;
86    }
87  
88    /**
89     * Note that the bounding box might contain a minX that is &gt; maxX, due to WGS84 anti-meridian.
90     */
91    @Override
92    public Rectangle getBoundingBox() {
93      return enclosingBox;
94    }
95  
96    @Override
97    public SpatialRelation relate(Shape other) {
98  //This shortcut was problematic in testing due to distinctions of CONTAINS/WITHIN for no-area shapes (lines, points).
99  //    if (distance == 0) {
100 //      return point.relate(other,ctx).intersects() ? SpatialRelation.WITHIN : SpatialRelation.DISJOINT;
101 //    }
102     if (isEmpty() || other.isEmpty())
103       return SpatialRelation.DISJOINT;
104     if (other instanceof Point) {
105       return relate((Point) other);
106     }
107     if (other instanceof Rectangle) {
108       return relate((Rectangle) other);
109     }
110     if (other instanceof Circle) {
111       return relate((Circle) other);
112     }
113     return other.relate(this).transpose();
114   }
115 
116   public SpatialRelation relate(Point point) {
117     return contains(point.getX(),point.getY()) ? SpatialRelation.CONTAINS : SpatialRelation.DISJOINT;
118   }
119 
120   public SpatialRelation relate(Rectangle r) {
121     //Note: Surprisingly complicated!
122 
123     //--We start by leveraging the fact we have a calculated bbox that is "cheaper" than use of DistanceCalculator.
124     final SpatialRelation bboxSect = enclosingBox.relate(r);
125     if (bboxSect == SpatialRelation.DISJOINT || bboxSect == SpatialRelation.WITHIN)
126       return bboxSect;
127     else if (bboxSect == SpatialRelation.CONTAINS && enclosingBox.equals(r))//nasty identity edge-case
128       return SpatialRelation.WITHIN;
129     //bboxSect is INTERSECTS or CONTAINS
130     //The result can be DISJOINT, CONTAINS, or INTERSECTS (not WITHIN)
131 
132     return relateRectanglePhase2(r, bboxSect);
133   }
134 
135   protected SpatialRelation relateRectanglePhase2(final Rectangle r, SpatialRelation bboxSect) {
136     // DOES NOT WORK WITH GEO CROSSING DATELINE OR WORLD-WRAP. Other methods handle such cases.
137 
138     //At this point, the only thing we are certain of is that circle is *NOT* WITHIN r, since the
139     // bounding box of a circle MUST be within r for the circle to be within r.
140 
141     //Quickly determine if they are DISJOINT or not.
142     // Find the closest & farthest point to the circle within the rectangle
143     final double closestX, farthestX;
144     final double xAxis = getXAxis();
145     if (xAxis < r.getMinX()) {
146       closestX = r.getMinX();
147       farthestX = r.getMaxX();
148     } else if (xAxis > r.getMaxX()) {
149       closestX = r.getMaxX();
150       farthestX = r.getMinX();
151     } else {
152       closestX = xAxis; //we don't really use this value but to check this condition
153       farthestX = r.getMaxX() - xAxis > xAxis - r.getMinX() ? r.getMaxX() : r.getMinX();
154     }
155 
156     final double closestY, farthestY;
157     final double yAxis = getYAxis();
158     if (yAxis < r.getMinY()) {
159       closestY = r.getMinY();
160       farthestY = r.getMaxY();
161     } else if (yAxis > r.getMaxY()) {
162       closestY = r.getMaxY();
163       farthestY = r.getMinY();
164     } else {
165       closestY = yAxis; //we don't really use this value but to check this condition
166       farthestY = r.getMaxY() - yAxis > yAxis - r.getMinY() ? r.getMaxY() : r.getMinY();
167     }
168 
169     //If r doesn't overlap an axis, then could be disjoint. Test closestXY
170     if (xAxis != closestX && yAxis != closestY) {
171       if (!contains(closestX, closestY))
172         return SpatialRelation.DISJOINT;
173     } // else CAN'T be disjoint if spans axis because earlier bbox check ruled that out
174 
175     //Now, we know it's *NOT* DISJOINT and it's *NOT* WITHIN either.
176     // Does circle CONTAINS r or simply intersect it?
177 
178     //If circle contains r, then its bbox MUST also CONTAIN r.
179     if (bboxSect != SpatialRelation.CONTAINS)
180       return SpatialRelation.INTERSECTS;
181 
182     //If the farthest point of r away from the center of the circle is contained, then all of r is
183     // contained.
184     if (!contains(farthestX, farthestY))
185       return SpatialRelation.INTERSECTS;
186 
187     //geodetic detection of farthest Y when rect crosses x axis can't be reliably determined, so
188     // check other corner too, which might actually be farthest
189     if (point.getY() != getYAxis()) {//geodetic
190       if (yAxis == closestY) {//r crosses north to south over x axis (confusing)
191         double otherY = (farthestY == r.getMaxY() ? r.getMinY() : r.getMaxY());
192         if (!contains(farthestX, otherY))
193           return SpatialRelation.INTERSECTS;
194       }
195     }
196    
197     return SpatialRelation.CONTAINS;
198   }
199 
200   /**
201    * The <code>Y</code> coordinate of where the circle axis intersect.
202    */
203   protected double getYAxis() {
204     return point.getY();
205   }
206 
207   /**
208    * The <code>X</code> coordinate of where the circle axis intersect.
209    */
210   protected double getXAxis() {
211     return point.getX();
212   }
213 
214   public SpatialRelation relate(Circle circle) {
215     double crossDist = ctx.getDistCalc().distance(point, circle.getCenter());
216     double aDist = radiusDEG, bDist = circle.getRadius();
217     if (crossDist > aDist + bDist)
218       return SpatialRelation.DISJOINT;
219     if (crossDist < aDist && crossDist + bDist <= aDist)
220       return SpatialRelation.CONTAINS;
221     if (crossDist < bDist && crossDist + aDist <= bDist)
222       return SpatialRelation.WITHIN;
223 
224     return SpatialRelation.INTERSECTS;
225   }
226 
227   @Override
228   public String toString() {
229     return "Circle(" + point + ", d=" + radiusDEG + "°)";
230   }
231 
232   @Override
233   public boolean equals(Object obj) {
234     return equals(this,obj);
235   }
236 
237   /**
238    * All {@link Circle} implementations should use this definition of {@link Object#equals(Object)}.
239    */
240   public static boolean equals(Circle thiz, Object o) {
241     assert thiz != null;
242     if (thiz == o) return true;
243     if (!(o instanceof Circle)) return false;
244 
245     Circle circle = (Circle) o;
246 
247     if (!thiz.getCenter().equals(circle.getCenter())) return false;
248     if (Double.compare(circle.getRadius(), thiz.getRadius()) != 0) return false;
249 
250     return true;
251   }
252 
253   @Override
254   public int hashCode() {
255     return hashCode(this);
256   }
257 
258   /**
259    * All {@link Circle} implementations should use this definition of {@link Object#hashCode()}.
260    */
261   public static int hashCode(Circle thiz) {
262     int result;
263     long temp;
264     result = thiz.getCenter().hashCode();
265     temp = thiz.getRadius() != +0.0d ? Double.doubleToLongBits(thiz.getRadius()) : 0L;
266     result = 31 * result + (int) (temp ^ (temp >>> 32));
267     return result;
268   }
269 }