/******************************************************************************* * 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; import org.locationtech.spatial4j.context.SpatialContext; import org.locationtech.spatial4j.shape.impl.BBoxCalculator; import java.util.*; import static org.locationtech.spatial4j.shape.SpatialRelation.CONTAINS; import static org.locationtech.spatial4j.shape.SpatialRelation.INTERSECTS; /** * A collection of Shape objects, analogous to an OGC GeometryCollection. The * implementation demands a List (with random access) so that the order can be * retained if an application requires it, although logically it's treated as an * unordered Set, mostly. * <p> * Ideally, {@link #relate(Shape)} should return the same result no matter what * the shape order is, although the default implementation can be order * dependent when the shapes overlap; see {@link #relateContainsShortCircuits()}. * To improve performance slightly, the caller could order the shapes by * largest first so that relate() will have a greater chance of * short-circuit'ing sooner. As the Shape contract states; it may return * intersects when the best answer is actually contains or within. If any shape * intersects the provided shape then that is the answer. * <p> * This implementation is not optimized for a large number of shapes; relate is * O(N). A more sophisticated implementation might do an R-Tree based on * bbox'es, for example. */ public class ShapeCollection<S extends Shape> extends AbstractList<S> implements Shape { protected final SpatialContext ctx; protected final List<S> shapes; protected final Rectangle bbox; /** * WARNING: {@code shapes} is copied by reference. * @param shapes Copied by reference! (make a defensive copy if caller modifies) * @param ctx */ public ShapeCollection(List<S> shapes, SpatialContext ctx) { if (!(shapes instanceof RandomAccess)) throw new IllegalArgumentException("Shapes arg must implement RandomAccess: "+shapes.getClass()); this.shapes = shapes; this.ctx = ctx; this.bbox = computeBoundingBox(shapes, ctx); } protected Rectangle computeBoundingBox(Collection<? extends Shape> shapes, SpatialContext ctx) { if (shapes.isEmpty()) return ctx.makeRectangle(Double.NaN, Double.NaN, Double.NaN, Double.NaN); BBoxCalculator bboxCalc = new BBoxCalculator(ctx); for (Shape geom : shapes) { bboxCalc.expandRange(geom.getBoundingBox()); } return bboxCalc.getBoundary(); } public List<S> getShapes() { return shapes; } @Override public S get(int index) { return shapes.get(index); } @Override public int size() { return shapes.size(); } @Override public Rectangle getBoundingBox() { return bbox; } @Override public Point getCenter() { return bbox.getCenter(); } @Override public boolean hasArea() { for (Shape geom : shapes) { if( geom.hasArea() ) { return true; } } return false; } @Override public ShapeCollection getBuffered(double distance, SpatialContext ctx) { List<Shape> bufColl = new ArrayList<Shape>(size()); for (Shape shape : shapes) { bufColl.add(shape.getBuffered(distance, ctx)); } return ctx.makeCollection(bufColl); } @Override public SpatialRelation relate(Shape other) { final SpatialRelation bboxSect = bbox.relate(other); if (bboxSect == SpatialRelation.DISJOINT || bboxSect == SpatialRelation.WITHIN) return bboxSect; final boolean containsWillShortCircuit = (other instanceof Point) || relateContainsShortCircuits(); SpatialRelation sect = null; for (Shape shape : shapes) { SpatialRelation nextSect = shape.relate(other); if (sect == null) {//first pass sect = nextSect; } else { sect = sect.combine(nextSect); } if (sect == INTERSECTS) return INTERSECTS; if (sect == CONTAINS && containsWillShortCircuit) return CONTAINS; } return sect; } /** * Called by relate() to determine whether to return early if it finds * CONTAINS, instead of checking the remaining shapes. It will do so without * calling this method if the "other" shape is a Point. If a remaining shape * finds INTERSECTS, then INTERSECTS will be returned. The only problem with * this returning true is that if some of the shapes overlap, it's possible * that the result of relate() could be dependent on the order of the shapes, * which could be unexpected / wrong depending on the application. The default * implementation returns true because it probably doesn't matter. If it * does, a subclass could add a boolean flag that this method could return. * That flag could be initialized to true only if the shapes are mutually * disjoint. * * @see #computeMutualDisjoint(java.util.List) . */ protected boolean relateContainsShortCircuits() { return true; } /** * Computes whether the shapes are mutually disjoint. This is a utility method * offered for use by a subclass implementing {@link #relateContainsShortCircuits()}. * <b>Beware: this is an O(N^2) algorithm.</b>. Consequently, consider safely * assuming non-disjoint if shapes.size() > 10 or something. And if all shapes * are a Point then the result of this method doesn't ultimately matter. */ protected static boolean computeMutualDisjoint(List<? extends Shape> shapes) { //WARNING: this is an O(n^2) algorithm. //loop through each shape and see if it intersects any shape before it for (int i = 1; i < shapes.size(); i++) { Shape shapeI = shapes.get(i); for (int j = 0; j < i; j++) { Shape shapeJ = shapes.get(j); if (shapeJ.relate(shapeI).intersects()) return false; } } return true; } @Override public double getArea(SpatialContext ctx) { double MAX_AREA = bbox.getArea(ctx); double sum = 0; for (Shape geom : shapes) { sum += geom.getArea(ctx); if (sum >= MAX_AREA) return MAX_AREA; } return sum; } @Override public String toString() { StringBuilder buf = new StringBuilder(100); buf.append("ShapeCollection("); int i = 0; for (Shape shape : shapes) { if (i++ > 0) buf.append(", "); buf.append(shape); if (buf.length() > 150) { buf.append(" ...").append(shapes.size()); break; } } buf.append(")"); return buf.toString(); } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; ShapeCollection that = (ShapeCollection) o; if (!shapes.equals(that.shapes)) return false; return true; } @Override public int hashCode() { return shapes.hashCode(); } @Override public SpatialContext getContext() { return ctx; } }