package com.opendoorlogistics.core.geometry.operations; import gnu.trove.set.hash.TLongHashSet; import java.util.HashMap; import java.util.HashSet; import java.util.function.BiConsumer; import com.vividsolutions.jts.geom.Coordinate; import com.vividsolutions.jts.geom.Envelope; import com.vividsolutions.jts.geom.Geometry; import com.vividsolutions.jts.geom.GeometryFactory; /* * Specialised quadtree designed for fast lookups of points with an id contained within a polygon */ public class FastContainedPointsQuadtree { private final static int MAX_POINTS_PER_NODE = 10; private final CacheKey cacheKey; private final Node root; private static class PointsMap extends HashMap<Coordinate, TLongHashSet> { void add(Coordinate c, long ...ids){ if(ids.length==0){ return; } TLongHashSet set = get(c); if(set==null){ set = new TLongHashSet(ids.length); put(new Coordinate(c), set); } set.addAll(ids); } long getEstimatedSizeInBytes(){ int sz = size(); class Ret{ long val=0; } Ret ret = new Ret(); ret.val += 8; forEach(new BiConsumer<Coordinate, TLongHashSet>() { @Override public void accept(Coordinate t, TLongHashSet u) { ret.val += 8; // object itself ret.val += 4*8; // coord ret.val += 8 * u.size() + 8; // longs } }); return ret.val; } } private static class Node{ final Envelope envelope; final Coordinate centre; final GeometryFactory factory; final Geometry polygonEnvelope; Node [] children; PointsMap points; Node(Envelope e, GeometryFactory factory) { this.factory = factory; this.envelope = e; this.centre = e.centre(); this.polygonEnvelope = factory.toGeometry(envelope); } @Override public String toString(){ return "Node with " + countCoords() + " coords"; } long getEstimatedSizeInBytes(){ long ret=0; // envelope ret += 4*8 + 8; // centre ret += 3*8 + 8; // factory is held elsewhere.. // polygon envelope ret += 4*3*8 + 8; // child array pointer ret+= 8; // points map pointer ret +=8; if(points!=null){ ret += points.getEstimatedSizeInBytes(); } if(children!=null){ ret += 4*8; for(Node c : children){ if(c!=null){ ret += c.getEstimatedSizeInBytes(); } } } return ret; } void split(){ if(children==null){ children = new Node[4]; PointsMap tmpPoints = points; points = null; tmpPoints.forEach(new BiConsumer<Coordinate, TLongHashSet>() { @Override public void accept(Coordinate t, TLongHashSet u) { insert(t, u.toArray()); } }); } } void insert(Coordinate c, long ...ids){ // check for no ids (should probably never happen) if(ids.length==0){ return; } // test if we already have this coordinate if(points!=null){ TLongHashSet set = points.get(c); if(set!=null){ set.addAll(ids); return; } } // if we haven't already split, test if we need split if(children==null){ int sz = points!=null ? points.size() : 0; int newSz = sz + 1; if(newSz >= MAX_POINTS_PER_NODE){ split(); } } // insert into children if we're split if(children!=null){ boolean isRight = c.x >= centre.x; boolean isTop = c.y >= centre.y; // indices are top left, top right, bottom left, bottom right int indx; if(isTop){ if(!isRight){ // top left indx=0; }else{ // top right indx = 1; } }else{ if(!isRight){ // top left indx=2; }else{ // top right indx = 3; } } // create child node if not already existing if(children[indx]==null){ double x1, x2; if(!isRight){ x1 = envelope.getMinX(); x2 = centre.x; }else{ x1 = centre.x; x2 = envelope.getMaxX(); } double y1, y2; if(isTop){ y1 = centre.y; y2 = envelope.getMaxY(); }else{ y1 = envelope.getMinY(); y2 = centre.y; } children[indx] = new Node(new Envelope(x1, x2, y1, y2), factory); } // insert into it children[indx].insert(c, ids); }else{ // Otherwise insert into this node if(points==null){ points = new PointsMap(); } points.add(c,ids); } } void fetchIds(TLongHashSet ids){ if(points!=null){ for(TLongHashSet set : points.values()){ ids.addAll(set); } } if(children!=null){ for(Node c : children){ if(c!=null){ c.fetchIds(ids); } } } } long countCoords(){ long ret = 0; if(points!=null){ ret+=points.size(); } if(children!=null){ for(Node child:children){ if(child!=null){ ret += child.countCoords(); } } } return ret; } NodeQueryResult getRelationToGeometry(Geometry g, QueryStats stats){ if(g==null){ // everything is outside of null geometry return NodeQueryResult.OUTSIDE; } // check for empty node (should probably never happen) if(polygonEnvelope==null){ return NodeQueryResult.OUTSIDE; } stats.nbQuadIntersectionTests++; // check for no intersection boolean intersects = g.intersects(polygonEnvelope); if(!intersects){ stats.nbOutsideQuads++; return NodeQueryResult.OUTSIDE; } // check for completely contained boolean contains = g.contains(polygonEnvelope); if(contains){ stats.nbContainedQuads++; return NodeQueryResult.INSIDE; } // or partially contained stats.nbIntersectingQuads++; return NodeQueryResult.INTERSECTING; } int nbNonNullChildren(){ int ret=0; if(children!=null){ for(Node c : children){ if(c!=null){ ret++; } } } return ret; } void query(Geometry g, TLongHashSet ids, QueryStats stats){ // Check for the case where we only have one non-null child and descend straight away. // This can happen for highly-concentrated points where we may have to descend many levels until finding them if(nbNonNullChildren()==1){ for(Node c : children){ if(c!=null){ c.query(g, ids, stats); return; } } } NodeQueryResult relation = getRelationToGeometry(g,stats); switch(relation){ case INSIDE: // The node is totally inside the geometry, so fetch all its ids int currentCount=ids.size(); fetchIds(ids); stats.nbIdsIdentifiedFromContainedQuads += ids.size() - currentCount; break; case OUTSIDE: // The node is totally outside, so don't do anything else. break; case INTERSECTING: // The node is partially inside, so we have to either (a) recurse if we have children // or (b) test all the contained points. if(children!=null){ for(Node c : children){ if(c!=null){ c.query(g, ids, stats); } } }else{ // test each individually... if(points!=null){ points.forEach(new BiConsumer<Coordinate, TLongHashSet>() { @Override public void accept(Coordinate c, TLongHashSet s) { stats.nbPointIntersectionTests++; if(GeomContains.containsPoint(g, c)){ ids.addAll(s); } } }); } } break; } } } private enum NodeQueryResult{ OUTSIDE, INSIDE, INTERSECTING, } public QueryStats query(Geometry g, TLongHashSet ids){ QueryStats stats = new QueryStats(); if(root!=null){ root.query(g, ids, stats); } return stats; } public Object getCacheKey(){ return cacheKey; } private FastContainedPointsQuadtree(CacheKey key, Node root){ this.cacheKey = key; this.root = root; } public static class Builder{ private Envelope e; private PointsMap points = new PointsMap(); private TLongHashSet testids = new TLongHashSet(); private HashSet<Coordinate> testCoords = new HashSet<Coordinate>(); public void add(Coordinate coordinate , long id){ if(e==null){ e = new Envelope(coordinate); }else{ e.expandToInclude(coordinate); } points.add(coordinate, id); testids.add(id); testCoords.add(coordinate); } public FastContainedPointsQuadtree build(GeometryFactory factory){ return build(factory, null); } public interface InsertedListener{ void inserted(Coordinate c, long count); } public FastContainedPointsQuadtree build(GeometryFactory factory, InsertedListener listener){ if(e==null){ // no points ... return dummy empty tree which will return nothing from all queries return new FastContainedPointsQuadtree(new CacheKey(points), null); } Node root = new Node(e, factory); // small object to count number added class Counter{ long count=0; } Counter counter = new Counter(); points.forEach(new BiConsumer<Coordinate, TLongHashSet>() { @Override public void accept(Coordinate t, TLongHashSet u) { root.insert(t, u.toArray()); if(listener!=null){ listener.inserted(t, counter.count); } counter.count++; } }); // check root ok - all should be contained TLongHashSet allContained = new TLongHashSet(); root.fetchIds(allContained); if(allContained.equals(testids)==false){ throw new RuntimeException("Error building points lookup quadtree"); } if(root.countCoords()!=testCoords.size()){ throw new RuntimeException("Error building points lookup quadtree"); } return new FastContainedPointsQuadtree(new CacheKey(points), root); } public Object buildCacheKey(){ return new CacheKey(points); } } private static class CacheKey{ private final PointsMap points; private final int hashcode; public CacheKey(PointsMap points) { this.points = points; this.hashcode = points.hashCode(); } @Override public int hashCode() { return hashcode; } @Override public boolean equals(Object obj) { if (this == obj) return true; if (obj == null) return false; if (getClass() != obj.getClass()) return false; CacheKey other = (CacheKey) obj; if (points == null) { if (other.points != null) return false; } else if (!points.equals(other.points)) return false; return true; } } public long getEstimatedSizeInBytes(){ long ret = 0; ret += cacheKey.points.getEstimatedSizeInBytes(); if(root!=null){ ret += root.getEstimatedSizeInBytes(); } return ret; } public static class QueryStats{ int nbQuadIntersectionTests; int nbPointIntersectionTests; int nbIdsIdentifiedFromContainedQuads; int nbOutsideQuads; int nbContainedQuads; int nbIntersectingQuads; @Override public String toString() { StringBuilder builder = new StringBuilder(); builder.append("QueryStats [nbQuadIntersectionTests="); builder.append(nbQuadIntersectionTests); builder.append(", nbPointIntersectionTests="); builder.append(nbPointIntersectionTests); builder.append(", nbIdsIdentifiedFromContainedQuads="); builder.append(nbIdsIdentifiedFromContainedQuads); builder.append(", nbOutsideQuads="); builder.append(nbOutsideQuads); builder.append(", nbContainedQuads="); builder.append(nbContainedQuads); builder.append(", nbIntersectingQuads="); builder.append(nbIntersectingQuads); builder.append("]"); return builder.toString(); } } }