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;
10  
11  import org.locationtech.spatial4j.context.SpatialContext;
12  import org.locationtech.spatial4j.shape.impl.BBoxCalculator;
13  
14  import java.util.*;
15  
16  import static org.locationtech.spatial4j.shape.SpatialRelation.CONTAINS;
17  import static org.locationtech.spatial4j.shape.SpatialRelation.INTERSECTS;
18  
19  /**
20   * A collection of Shape objects, analogous to an OGC GeometryCollection. The
21   * implementation demands a List (with random access) so that the order can be
22   * retained if an application requires it, although logically it's treated as an
23   * unordered Set, mostly.
24   * <p>
25   * Ideally, {@link #relate(Shape)} should return the same result no matter what
26   * the shape order is, although the default implementation can be order
27   * dependent when the shapes overlap; see {@link #relateContainsShortCircuits()}.
28   *  To improve performance slightly, the caller could order the shapes by
29   * largest first so that relate() will have a greater chance of
30   * short-circuit'ing sooner.  As the Shape contract states; it may return
31   * intersects when the best answer is actually contains or within. If any shape
32   * intersects the provided shape then that is the answer.
33   * <p>
34   * This implementation is not optimized for a large number of shapes; relate is
35   * O(N).  A more sophisticated implementation might do an R-Tree based on
36   * bbox'es, for example.
37   */
38  public class ShapeCollection<S extends Shape> extends AbstractList<S> implements Shape {
39  
40    protected final SpatialContext ctx;
41    protected final List<S> shapes;
42    protected final Rectangle bbox;
43  
44    /**
45     * WARNING: {@code shapes} is copied by reference.
46     * @param shapes Copied by reference! (make a defensive copy if caller modifies)
47     */
48    public ShapeCollection(List<S> shapes, SpatialContext ctx) {
49      if (!(shapes instanceof RandomAccess))
50        throw new IllegalArgumentException("Shapes arg must implement RandomAccess: "+shapes.getClass());
51      this.shapes = shapes;
52      this.ctx = ctx;
53      this.bbox = computeBoundingBox(shapes, ctx);
54    }
55  
56    protected Rectangle computeBoundingBox(Collection<? extends Shape> shapes, SpatialContext ctx) {
57      if (shapes.isEmpty())
58        return ctx.makeRectangle(Double.NaN, Double.NaN, Double.NaN, Double.NaN);
59      BBoxCalculator bboxCalc = new BBoxCalculator(ctx);
60      for (Shape geom : shapes) {
61        bboxCalc.expandRange(geom.getBoundingBox());
62      }
63      return bboxCalc.getBoundary();
64    }
65  
66    public List<S> getShapes() {
67      return shapes;
68    }
69  
70    @Override
71    public S get(int index) {
72      return shapes.get(index);
73    }
74  
75    @Override
76    public int size() {
77      return shapes.size();
78    }
79  
80    @Override
81    public Rectangle getBoundingBox() {
82      return bbox;
83    }
84  
85    @Override
86    public Point getCenter() {
87      return bbox.getCenter();
88    }
89  
90    @Override
91    public boolean hasArea() {
92      for (Shape geom : shapes) {
93        if( geom.hasArea() ) {
94          return true;
95        }
96      }
97      return false;
98    }
99  
100   @Override
101   public ShapeCollection getBuffered(double distance, SpatialContext ctx) {
102     List<Shape> bufColl = new ArrayList<>(size());
103     for (Shape shape : shapes) {
104       bufColl.add(shape.getBuffered(distance, ctx));
105     }
106     return ctx.makeCollection(bufColl);
107   }
108 
109   @Override
110   public SpatialRelation relate(Shape other) {
111     final SpatialRelation bboxSect = bbox.relate(other);
112     if (bboxSect == SpatialRelation.DISJOINT || bboxSect == SpatialRelation.WITHIN)
113       return bboxSect;
114 
115     final boolean containsWillShortCircuit = (other instanceof Point) ||
116         relateContainsShortCircuits();
117     SpatialRelation sect = null;
118     for (Shape shape : shapes) {
119       SpatialRelation nextSect = shape.relate(other);
120 
121       if (sect == null) {//first pass
122         sect = nextSect;
123       } else {
124         sect = sect.combine(nextSect);
125       }
126 
127       if (sect == INTERSECTS)
128         return INTERSECTS;
129 
130       if (sect == CONTAINS && containsWillShortCircuit)
131         return CONTAINS;
132     }
133     return sect;
134   }
135 
136   /**
137    * Called by relate() to determine whether to return early if it finds
138    * CONTAINS, instead of checking the remaining shapes. It will do so without
139    * calling this method if the "other" shape is a Point.  If a remaining shape
140    * finds INTERSECTS, then INTERSECTS will be returned.  The only problem with
141    * this returning true is that if some of the shapes overlap, it's possible
142    * that the result of relate() could be dependent on the order of the shapes,
143    * which could be unexpected / wrong depending on the application. The default
144    * implementation returns true because it probably doesn't matter.  If it
145    * does, a subclass could add a boolean flag that this method could return.
146    * That flag could be initialized to true only if the shapes are mutually
147    * disjoint.
148    *
149    * @see #computeMutualDisjoint(java.util.List) .
150    */
151   protected boolean relateContainsShortCircuits() {
152     return true;
153   }
154 
155   /**
156    * Computes whether the shapes are mutually disjoint. This is a utility method
157    * offered for use by a subclass implementing {@link #relateContainsShortCircuits()}.
158    * <b>Beware: this is an O(N^2) algorithm.</b>.  Consequently, consider safely
159    * assuming non-disjoint if shapes.size() &gt; 10 or something.  And if all shapes
160    * are a Point then the result of this method doesn't ultimately matter.
161    */
162   protected static boolean computeMutualDisjoint(List<? extends Shape> shapes) {
163     //WARNING: this is an O(n^2) algorithm.
164     //loop through each shape and see if it intersects any shape before it
165     for (int i = 1; i < shapes.size(); i++) {
166       Shape shapeI = shapes.get(i);
167       for (int j = 0; j < i; j++) {
168         Shape shapeJ = shapes.get(j);
169         if (shapeJ.relate(shapeI).intersects())
170           return false;
171       }
172     }
173     return true;
174   }
175 
176   @Override
177   public double getArea(SpatialContext ctx) {
178     double MAX_AREA = bbox.getArea(ctx);
179     double sum = 0;
180     for (Shape geom : shapes) {
181       sum += geom.getArea(ctx);
182       if (sum >= MAX_AREA)
183         return MAX_AREA;
184     }
185 
186     return sum;
187   }
188 
189   @Override
190   public String toString() {
191     StringBuilder buf = new StringBuilder(100);
192     buf.append("ShapeCollection(");
193     int i = 0;
194     for (Shape shape : shapes) {
195       if (i++ > 0)
196         buf.append(", ");
197       buf.append(shape);
198       if (buf.length() > 150) {
199         buf.append(" ...").append(shapes.size());
200         break;
201       }
202     }
203     buf.append(")");
204     return buf.toString();
205   }
206 
207   @Override
208   public boolean equals(Object o) {
209     if (this == o) return true;
210     if (o == null || getClass() != o.getClass()) return false;
211 
212     ShapeCollection that = (ShapeCollection) o;
213 
214     if (!shapes.equals(that.shapes)) return false;
215 
216     return true;
217   }
218 
219   @Override
220   public int hashCode() {
221     return shapes.hashCode();
222   }
223 
224   @Override
225   public SpatialContext getContext() {
226     return ctx;
227   }
228 }