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.distance.DistanceUtils;
13  import org.locationtech.spatial4j.shape.Point;
14  import org.locationtech.spatial4j.shape.Rectangle;
15  import org.locationtech.spatial4j.shape.SpatialRelation;
16  
17  import java.util.Formatter;
18  import java.util.Locale;
19  
20  /**
21   * A circle as it exists on the surface of a sphere.
22   */
23  public class GeoCircle extends CircleImpl {
24    private GeoCircle inverseCircle;//when distance reaches > 1/2 way around the world, cache the inverse.
25    private double horizAxisY;//see getYAxis
26  
27    public GeoCircle(Point p, double radiusDEG, SpatialContext ctx) {
28      super(p, radiusDEG, ctx);
29      assert ctx.isGeo();
30      init();
31    }
32  
33    @Override
34    public void reset(double x, double y, double radiusDEG) {
35      super.reset(x, y, radiusDEG);
36      init();
37    }
38  
39    private void init() {
40      if (radiusDEG > 90) {
41        //--spans more than half the globe
42        assert enclosingBox.getWidth() == 360;
43        double backDistDEG = 180 - radiusDEG;
44        if (backDistDEG > 0) {
45          double backRadius = 180 - radiusDEG;
46          double backX = DistanceUtils.normLonDEG(getCenter().getX() + 180);
47          double backY = DistanceUtils.normLatDEG(getCenter().getY() + 180);
48          //Shrink inverseCircle as small as possible to avoid accidental overlap.
49          // Note that this is tricky business to come up with a value small enough
50          // but not too small or else numerical conditioning issues become a problem.
51          backRadius -= Math.max(Math.ulp(Math.abs(backY)+backRadius), Math.ulp(Math.abs(backX)+backRadius));
52          if (inverseCircle != null) {
53            inverseCircle.reset(backX, backY, backRadius);
54          } else {
55            inverseCircle = new GeoCircle(ctx.makePoint(backX, backY), backRadius, ctx);
56          }
57        } else {
58          inverseCircle = null;//whole globe
59        }
60        horizAxisY = getCenter().getY();//although probably not used
61      } else {
62        inverseCircle = null;
63        double _horizAxisY = ctx.getDistCalc().calcBoxByDistFromPt_yHorizAxisDEG(getCenter(), radiusDEG, ctx);
64        //some rare numeric conditioning cases can cause this to be barely beyond the box
65        if (_horizAxisY > enclosingBox.getMaxY()) {
66          horizAxisY = enclosingBox.getMaxY();
67        } else if (_horizAxisY < enclosingBox.getMinY()) {
68          horizAxisY = enclosingBox.getMinY();
69        } else {
70          horizAxisY = _horizAxisY;
71        }
72        //assert enclosingBox.relate_yRange(horizAxis,horizAxis,ctx).intersects();
73      }
74    }
75  
76    @Override
77    protected double getYAxis() {
78      return horizAxisY;
79    }
80  
81    /**
82     * Called after bounding box is intersected.
83     * @param bboxSect INTERSECTS or CONTAINS from enclosingBox's intersection
84     * @return DISJOINT, CONTAINS, or INTERSECTS (not WITHIN)
85     */
86    @Override
87    protected SpatialRelation relateRectanglePhase2(Rectangle r, SpatialRelation bboxSect) {
88  
89      if (inverseCircle != null) {
90        return inverseCircle.relate(r).inverse();
91      }
92  
93      //if a pole is wrapped, we have a separate algorithm
94      if (enclosingBox.getWidth() == 360) {
95        return relateRectangleCircleWrapsPole(r, ctx);
96      }
97  
98      //This is an optimization path for when there are no dateline or pole issues.
99      if (!enclosingBox.getCrossesDateLine() && !r.getCrossesDateLine()) {
100       return super.relateRectanglePhase2(r, bboxSect);
101     }
102 
103     //Rectangle wraps around the world longitudinally creating a solid band; there are no corners to test intersection
104     if (r.getWidth() == 360) {
105       return SpatialRelation.INTERSECTS;
106     }
107 
108     //do quick check to see if all corners are within this circle for CONTAINS
109     int cornersIntersect = numCornersIntersect(r);
110     if (cornersIntersect == 4) {
111       //ensure r's x axis is within c's.  If it doesn't, r sneaks around the globe to touch the other side (intersect).
112       SpatialRelation xIntersect = r.relateXRange(enclosingBox.getMinX(), enclosingBox.getMaxX());
113       if (xIntersect == SpatialRelation.WITHIN)
114         return SpatialRelation.CONTAINS;
115       return SpatialRelation.INTERSECTS;
116     }
117 
118     //INTERSECT or DISJOINT ?
119     if (cornersIntersect > 0)
120       return SpatialRelation.INTERSECTS;
121 
122     //Now we check if one of the axis of the circle intersect with r.  If so we have
123     // intersection.
124 
125     /* x axis intersects  */
126     if ( r.relateYRange(getYAxis(), getYAxis()).intersects() // at y vertical
127           && r.relateXRange(enclosingBox.getMinX(), enclosingBox.getMaxX()).intersects() )
128       return SpatialRelation.INTERSECTS;
129 
130     /* y axis intersects */
131     if (r.relateXRange(getXAxis(), getXAxis()).intersects()) { // at x horizontal
132       double yTop = getCenter().getY()+ radiusDEG;
133       assert yTop <= 90;
134       double yBot = getCenter().getY()- radiusDEG;
135       assert yBot >= -90;
136       if (r.relateYRange(yBot, yTop).intersects())//back bottom
137         return SpatialRelation.INTERSECTS;
138     }
139 
140     return SpatialRelation.DISJOINT;
141   }
142 
143   private SpatialRelation relateRectangleCircleWrapsPole(Rectangle r, SpatialContext ctx) {
144     //This method handles the case where the circle wraps ONE pole, but not both.  For both,
145     // there is the inverseCircle case handled before now.  The only exception is for the case where
146     // the circle covers the entire globe, and we'll check that first.
147     if (radiusDEG == 180)//whole globe
148       return SpatialRelation.CONTAINS;
149 
150     //Check if r is within the pole wrap region:
151     double yTop = getCenter().getY() + radiusDEG;
152     if (yTop > 90) {
153       double yTopOverlap = yTop - 90;
154       assert yTopOverlap <= 90;
155       if (r.getMinY() >= 90 - yTopOverlap)
156         return SpatialRelation.CONTAINS;
157     } else {
158       double yBot = point.getY() - radiusDEG;
159       if (yBot < -90) {
160         double yBotOverlap = -90 - yBot;
161         assert yBotOverlap <= 90;
162         if (r.getMaxY() <= -90 + yBotOverlap)
163           return SpatialRelation.CONTAINS;
164       } else {
165         //This point is probably not reachable ??
166         assert yTop == 90 || yBot == -90;//we simply touch a pole
167         //continue
168       }
169     }
170 
171     //If there are no corners to check intersection because r wraps completely...
172     if (r.getWidth() == 360)
173       return SpatialRelation.INTERSECTS;
174 
175     //Check corners:
176     int cornersIntersect = numCornersIntersect(r);
177     // (It might be possible to reduce contains() calls within nCI() to exactly two, but this intersection
178     //  code is complicated enough as it is.)
179     double frontX = getCenter().getX();
180     if (cornersIntersect == 4) {//all
181       double backX = frontX <= 0 ? frontX + 180 : frontX - 180;
182       if (r.relateXRange(backX, backX).intersects())
183         return SpatialRelation.INTERSECTS;
184       else
185         return SpatialRelation.CONTAINS;
186     } else if (cornersIntersect == 0) {//none
187       if (r.relateXRange(frontX, frontX).intersects())
188         return SpatialRelation.INTERSECTS;
189       else
190         return SpatialRelation.DISJOINT;
191     } else//partial
192       return SpatialRelation.INTERSECTS;
193   }
194 
195   /** Returns either 0 for none, 1 for some, or 4 for all. */
196   private int numCornersIntersect(Rectangle r) {
197     //We play some logic games to avoid calling contains() which can be expensive.
198     boolean bool;//if true then all corners intersect, if false then no corners intersect
199     // for partial, we exit early with 1 and ignore bool.
200     bool = (contains(r.getMinX(),r.getMinY()));
201     if (contains(r.getMinX(),r.getMaxY())) {
202       if (!bool)
203         return 1;//partial
204     } else {
205       if (bool)
206         return 1;//partial
207     }
208     if (contains(r.getMaxX(),r.getMinY())) {
209       if (!bool)
210         return 1;//partial
211     } else {
212       if (bool)
213         return 1;//partial
214     }
215     if (contains(r.getMaxX(),r.getMaxY())) {
216       if (!bool)
217         return 1;//partial
218     } else {
219       if (bool)
220         return 1;//partial
221     }
222     return bool?4:0;
223   }
224 
225   @Override
226   public String toString() {
227     //Add distance in km, which may be easier to recognize.
228     double distKm = DistanceUtils.degrees2Dist(radiusDEG,  DistanceUtils.EARTH_MEAN_RADIUS_KM);
229     //instead of String.format() so that we get consistent output no matter the locale
230     String dStr = new Formatter(Locale.ROOT).format("%.1f\u00B0 %.2fkm", radiusDEG, distKm).toString();
231     return "Circle(" + point + ", d=" + dStr + ')';
232   }
233 }