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.BaseShape;
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 simple Rectangle implementation that also supports a longitudinal
21   * wrap-around. When minX > maxX, this will assume it is world coordinates that
22   * cross the date line using degrees. Immutable & threadsafe.
23   */
24  public class RectangleImpl extends BaseShape<SpatialContext> implements Rectangle {
25  
26    private double minX;
27    private double maxX;
28    private double minY;
29    private double maxY;
30  
31    /** A simple constructor without normalization / validation. */
32    public RectangleImpl(double minX, double maxX, double minY, double maxY, SpatialContext ctx) {
33      super(ctx);
34      //TODO change to West South East North to be more consistent with OGC?
35      reset(minX, maxX, minY, maxY);
36    }
37  
38    /** A convenience constructor which pulls out the coordinates. */
39    public RectangleImpl(Point lowerLeft, Point upperRight, SpatialContext ctx) {
40      this(lowerLeft.getX(), upperRight.getX(),
41          lowerLeft.getY(), upperRight.getY(), ctx);
42    }
43  
44    /** Copy constructor. */
45    public RectangleImpl(Rectangle r, SpatialContext ctx) {
46      this(r.getMinX(), r.getMaxX(), r.getMinY(), r.getMaxY(), ctx);
47    }
48  
49    @Override
50    public void reset(double minX, double maxX, double minY, double maxY) {
51      assert ! isEmpty();
52      this.minX = minX;
53      this.maxX = maxX;
54      this.minY = minY;
55      this.maxY = maxY;
56      assert minY <= maxY || Double.isNaN(minY) : "minY, maxY: "+minY+", "+maxY;
57    }
58  
59    @Override
60    public boolean isEmpty() {
61      return Double.isNaN(minX);
62    }
63  
64    @Override
65    public Rectangle getBuffered(double distance, SpatialContext ctx) {
66      if (ctx.isGeo()) {
67        //first check pole touching, triggering a world-wrap rect
68        if (maxY + distance >= 90) {
69          return ctx.makeRectangle(-180, 180, Math.max(-90, minY - distance), 90);
70        } else if (minY - distance <= -90) {
71          return ctx.makeRectangle(-180, 180, -90, Math.min(90, maxY + distance));
72        } else {
73          //doesn't touch pole
74          double latDistance = distance;
75          double closestToPoleY = Math.abs(maxY) > Math.abs(minY) ? maxY : minY;
76          double lonDistance = DistanceUtils.calcBoxByDistFromPt_deltaLonDEG(
77              closestToPoleY, minX, distance);//lat,lon order
78          //could still wrap the world though...
79          if (lonDistance * 2 + getWidth() >= 360)
80            return ctx.makeRectangle(-180, 180, minY - latDistance, maxY + latDistance);
81          return ctx.makeRectangle(
82              DistanceUtils.normLonDEG(minX - lonDistance),
83              DistanceUtils.normLonDEG(maxX + lonDistance),
84              minY - latDistance, maxY + latDistance);
85        }
86      } else {
87        Rectangle worldBounds = ctx.getWorldBounds();
88        double newMinX = Math.max(worldBounds.getMinX(), minX - distance);
89        double newMaxX = Math.min(worldBounds.getMaxX(), maxX + distance);
90        double newMinY = Math.max(worldBounds.getMinY(), minY - distance);
91        double newMaxY = Math.min(worldBounds.getMaxY(), maxY + distance);
92        return ctx.makeRectangle(newMinX, newMaxX, newMinY, newMaxY);
93      }
94    }
95  
96    @Override
97    public boolean hasArea() {
98      return maxX != minX && maxY != minY;
99    }
100 
101   @Override
102   public double getArea(SpatialContext ctx) {
103     if (ctx == null) {
104       return getWidth() * getHeight();
105     } else {
106       return ctx.getDistCalc().area(this);
107     }
108   }
109 
110   @Override
111   public boolean getCrossesDateLine() {
112     return (minX > maxX);
113   }
114 
115   @Override
116   public double getHeight() {
117     return maxY - minY;
118   }
119 
120   @Override
121   public double getWidth() {
122     double w = maxX - minX;
123     if (w < 0) {//only true when minX > maxX (WGS84 assumed)
124       w += 360;
125       assert w >= 0;
126     }
127     return w;
128   }
129 
130   @Override
131   public double getMaxX() {
132     return maxX;
133   }
134 
135   @Override
136   public double getMaxY() {
137     return maxY;
138   }
139 
140   @Override
141   public double getMinX() {
142     return minX;
143   }
144 
145   @Override
146   public double getMinY() {
147     return minY;
148   }
149 
150   @Override
151   public Rectangle getBoundingBox() {
152     return this;
153   }
154 
155   @Override
156   public SpatialRelation relate(Shape other) {
157     if (isEmpty() || other.isEmpty())
158       return SpatialRelation.DISJOINT;
159     if (other instanceof Point) {
160       return relate((Point) other);
161     }
162     if (other instanceof Rectangle) {
163       return relate((Rectangle) other);
164     }
165     return other.relate(this).transpose();
166   }
167 
168   public SpatialRelation relate(Point point) {
169     if (point.getY() > getMaxY() || point.getY() < getMinY())
170       return SpatialRelation.DISJOINT;
171     //  all the below logic is rather unfortunate but some dateline cases demand it
172     double minX = this.minX;
173     double maxX = this.maxX;
174     double pX = point.getX();
175     if (ctx.isGeo()) {
176       //unwrap dateline and normalize +180 to become -180
177       double rawWidth = maxX - minX;
178       if (rawWidth < 0) {
179         maxX = minX + (rawWidth + 360);
180       }
181       //shift to potentially overlap
182       if (pX < minX) {
183         pX += 360;
184       } else if (pX > maxX) {
185         pX -= 360;
186       } else {
187         return SpatialRelation.CONTAINS;//short-circuit
188       }
189     }
190     if (pX < minX || pX > maxX)
191       return SpatialRelation.DISJOINT;
192     return SpatialRelation.CONTAINS;
193   }
194 
195   public SpatialRelation relate(Rectangle rect) {
196     SpatialRelation yIntersect = relateYRange(rect.getMinY(), rect.getMaxY());
197     if (yIntersect == SpatialRelation.DISJOINT)
198       return SpatialRelation.DISJOINT;
199 
200     SpatialRelation xIntersect = relateXRange(rect.getMinX(), rect.getMaxX());
201     if (xIntersect == SpatialRelation.DISJOINT)
202       return SpatialRelation.DISJOINT;
203 
204     if (xIntersect == yIntersect)//in agreement
205       return xIntersect;
206 
207     //if one side is equal, return the other
208     if (getMinY() == rect.getMinY() && getMaxY() == rect.getMaxY())
209       return xIntersect;
210     if (getMinX() == rect.getMinX() && getMaxX() == rect.getMaxX()
211             || (ctx.isGeo() && verticalAtDateline(this, rect))) {
212       return yIntersect;
213     }
214 
215     return SpatialRelation.INTERSECTS;
216   }
217 
218   //note: if vertical lines at the dateline were normalized (say to -180.0) then this method wouldn't be necessary.
219   private static boolean verticalAtDateline(RectangleImpl rect1, Rectangle rect2) {
220     if (rect1.getMinX() == rect1.getMaxX() && rect2.getMinX() == rect2.getMaxX()) {
221       if (rect1.getMinX() == -180) {
222         return rect2.getMinX() == +180;
223       } else if (rect1.getMinX() == +180) {
224         return rect2.getMinX() == -180;
225       }
226     }
227     return false;
228   }
229 
230   //TODO might this utility move to SpatialRelation ?
231   private static SpatialRelation relate_range(double int_min, double int_max, double ext_min, double ext_max) {
232     if (ext_min > int_max || ext_max < int_min) {
233       return SpatialRelation.DISJOINT;
234     }
235 
236     if (ext_min >= int_min && ext_max <= int_max) {
237       return SpatialRelation.CONTAINS;
238     }
239 
240     if (ext_min <= int_min && ext_max >= int_max) {
241       return SpatialRelation.WITHIN;
242     }
243     return SpatialRelation.INTERSECTS;
244   }
245 
246   @Override
247   public SpatialRelation relateYRange(double ext_minY, double ext_maxY) {
248     return relate_range(minY, maxY, ext_minY, ext_maxY);
249   }
250 
251   @Override
252   public SpatialRelation relateXRange(double ext_minX, double ext_maxX) {
253     //For ext & this we have local minX and maxX variable pairs. We rotate them so that minX <= maxX
254     double minX = this.minX;
255     double maxX = this.maxX;
256     if (ctx.isGeo()) {
257       //unwrap dateline, plus do world-wrap short circuit
258       double rawWidth = maxX - minX;
259       if (rawWidth == 360)
260         return SpatialRelation.CONTAINS;
261       if (rawWidth < 0) {
262         maxX = minX + (rawWidth + 360);
263       }
264       double ext_rawWidth = ext_maxX - ext_minX;
265       if (ext_rawWidth == 360)
266         return SpatialRelation.WITHIN;
267       if (ext_rawWidth < 0) {
268         ext_maxX = ext_minX + (ext_rawWidth + 360);
269       }
270       //shift to potentially overlap
271       if (maxX < ext_minX) {
272         minX += 360;
273         maxX += 360;
274       } else if (ext_maxX < minX) {
275         ext_minX += 360;
276         ext_maxX += 360;
277       }
278     }
279 
280     return relate_range(minX, maxX, ext_minX, ext_maxX);
281   }
282 
283   @Override
284   public String toString() {
285     return "Rect(minX=" + minX + ",maxX=" + maxX + ",minY=" + minY + ",maxY=" + maxY + ")";
286   }
287 
288   @Override
289   public Point getCenter() {
290     if (Double.isNaN(minX))
291       return ctx.makePoint(Double.NaN, Double.NaN);
292     final double y = getHeight() / 2 + minY;
293     double x = getWidth() / 2 + minX;
294     if (minX > maxX)//WGS84
295       x = DistanceUtils.normLonDEG(x);//in case falls outside the standard range
296     return new PointImpl(x, y, ctx);
297   }
298 
299   @Override
300   public boolean equals(Object obj) {
301     return equals(this,obj);
302   }
303 
304   /**
305    * All {@link Rectangle} implementations should use this definition of {@link Object#equals(Object)}.
306    */
307   public static boolean equals(Rectangle thiz, Object o) {
308     assert thiz != null;
309     if (thiz == o) return true;
310     if (!(o instanceof Rectangle)) return false;
311 
312     RectangleImpl rectangle = (RectangleImpl) o;
313 
314     if (Double.compare(rectangle.getMaxX(), thiz.getMaxX()) != 0) return false;
315     if (Double.compare(rectangle.getMaxY(), thiz.getMaxY()) != 0) return false;
316     if (Double.compare(rectangle.getMinX(), thiz.getMinX()) != 0) return false;
317     if (Double.compare(rectangle.getMinY(), thiz.getMinY()) != 0) return false;
318 
319     return true;
320   }
321 
322   @Override
323   public int hashCode() {
324     return hashCode(this);
325   }
326 
327   /**
328    * All {@link Rectangle} implementations should use this definition of {@link Object#hashCode()}.
329    */
330   public static int hashCode(Rectangle thiz) {
331     int result;
332     long temp;
333     temp = thiz.getMinX() != +0.0d ? Double.doubleToLongBits(thiz.getMinX()) : 0L;
334     result = (int) (temp ^ (temp >>> 32));
335     temp = thiz.getMaxX() != +0.0d ? Double.doubleToLongBits(thiz.getMaxX()) : 0L;
336     result = 31 * result + (int) (temp ^ (temp >>> 32));
337     temp = thiz.getMinY() != +0.0d ? Double.doubleToLongBits(thiz.getMinY()) : 0L;
338     result = 31 * result + (int) (temp ^ (temp >>> 32));
339     temp = thiz.getMaxY() != +0.0d ? Double.doubleToLongBits(thiz.getMaxY()) : 0L;
340     result = 31 * result + (int) (temp ^ (temp >>> 32));
341     return result;
342   }
343 }