package com.onthegomap.planetiler.geo; import java.util.ArrayList; import java.util.List; import javax.annotation.concurrent.ThreadSafe; import org.locationtech.jts.geom.Geometry; import org.locationtech.jts.geom.GeometryCollection; import org.locationtech.jts.geom.Point; import org.locationtech.jts.geom.Polygon; import org.locationtech.jts.index.strtree.STRtree; /** * Index to efficiently query which polygons contain a point. *

* Writes and reads are thread-safe, but all writes must occur before reads. * * @param the type of value associated with each polygon */ @ThreadSafe public class PolygonIndex { private record GeomWithData(Polygon poly, T data) {} private final STRtree index = new STRtree(); private PolygonIndex() {} public static PolygonIndex create() { return new PolygonIndex<>(); } private volatile boolean built = false; private void build() { if (!built) { synchronized (this) { if (!built) { index.build(); built = true; } } } } /** Returns the data associated with the first polygon containing {@code point}. */ public T getOnlyContaining(Point point) { List result = getContaining(point); return result.isEmpty() ? null : result.getFirst(); } /** Returns the data associated with all polygons containing {@code point}. */ public List getContaining(Point point) { build(); // first pre-filter polygons with envelope that overlaps this point List items = index.query(point.getEnvelopeInternal()); // then post-filter to only polygons that actually contain the point return postFilterContaining(point, items); } /** Returns the data associated with all polygons containing {@code point}. */ public List getIntersecting(Geometry geom) { build(); List items = index.query(geom.getEnvelopeInternal()); return postFilterIntersecting(geom, items); } private List postFilterContaining(Point point, List items) { List result = new ArrayList<>(items.size()); for (Object item : items) { if (item instanceof GeomWithData(var poly,var data) && poly.contains(point)) { @SuppressWarnings("unchecked") T t = (T) data; result.add(t); } } return result; } private List postFilterIntersecting(Geometry geom, List items) { List result = new ArrayList<>(items.size()); for (Object item : items) { if (item instanceof GeomWithData(var poly,var data) && poly.intersects(geom)) { @SuppressWarnings("unchecked") T t = (T) data; result.add(t); } } return result; } /** * Returns the data associated with either the polygons that contain {@code point} or if none are found than the * nearest polygon to {@code point} with an envelope that contains point. */ public List getContainingOrNearest(Point point) { build(); List items = index.query(point.getEnvelopeInternal()); // optimization: if there's only one then skip checking contains/distance if (items.size() == 1) { if (items.getFirst() instanceof GeomWithData value) { @SuppressWarnings("unchecked") T t = (T) value.data; return List.of(t); } } List result = postFilterContaining(point, items); // if none contain, then look for the nearest polygon from potential overlaps if (result.isEmpty()) { double nearest = Double.MAX_VALUE; T nearestValue = null; for (Object item : items) { if (item instanceof GeomWithData value) { double distance = value.poly.distance(point); if (distance < nearest) { @SuppressWarnings("unchecked") T t = (T) value.data; nearestValue = t; nearest = distance; } } } if (nearestValue != null) { result.add(nearestValue); } } return result; } /** Returns the data associated with a polygon that contains {@code point} or nearest polygon if none are found. */ public T get(Point point) { List nearests = getContainingOrNearest(point); return nearests.isEmpty() ? null : nearests.getFirst(); } /** Indexes {@code item} for all polygons contained in {@code geom}. */ public void put(Geometry geom, T item) { if (geom instanceof Polygon poly) { // need to externally synchronize inserts into the STRTree synchronized (this) { index.insert(poly.getEnvelopeInternal(), new GeomWithData<>(poly, item)); } } else if (geom instanceof GeometryCollection geoms) { for (int i = 0; i < geoms.getNumGeometries(); i++) { put(geoms.getGeometryN(i), item); } } } }