// License: GPL. For details, see LICENSE file. package org.openstreetmap.josm.tools; import org.openstreetmap.josm.data.coor.LatLon; import org.openstreetmap.josm.data.osm.BBox; /** * Fast index to look up properties of the earth surface. * * It is expected that there is a relatively slow method to look up the property * for a certain coordinate and that there are larger areas with a uniform * property. * * This index tries to find rectangles with uniform property and caches them. * Rectangles are subdivided, if there are different properties within. * (Up to a maximum level, when the slow method is used again.) * * @param the property (like land/water or nation) */ public class GeoPropertyIndex { /** * A method to look up a property of the earth surface. * (User input for the index.) * @param the property */ public interface GeoProperty { /** * Look up the property for a point. * @param ll the point coordinates * @return property value at that point. Must not be null. */ T get(LatLon ll); /** * Look up the property for a coordinate rectangle. * @param box the rectangle * @return the property, if it is the same in the entire rectangle; * null otherwise */ T get(BBox box); } private final int maxLevel; private final GeoProperty geoProp; private final GPLevel root; private GPLevel lastLevelUsed; private static final boolean DEBUG = false; /** * Create new GeoPropertyIndex. * @param geoProp the input property that should be made faster by this index * @param maxLevel max level */ public GeoPropertyIndex(GeoProperty geoProp, int maxLevel) { this.geoProp = geoProp; this.maxLevel = maxLevel; this.root = new GPLevel<>(0, new BBox(-180, -90, 180, 90), null, this); this.lastLevelUsed = root; } /** * Look up the property for a certain point. * This gives the same result as {@link GeoProperty#get(LatLon)}, but * should be faster. * @param ll the point coordinates * @return property value at that point */ public T get(LatLon ll) { return lastLevelUsed.get(ll); } public static int index(LatLon ll, int level) { long noParts = 1 << level; long x = ((long) ((ll.lon() + 180.0) * noParts / 360.0)) & 1; long y = ((long) ((ll.lat() + 90.0) * noParts / 180.0)) & 1; return (int) (2 * x + y); } protected static class GPLevel { private final T val; private final int level; private final BBox bbox; private final GPLevel parent; private final GeoPropertyIndex owner; // child order by index is sw, nw, se, ne private GPLevel[] children; public GPLevel(int level, BBox bbox, GPLevel parent, GeoPropertyIndex owner) { this.level = level; this.bbox = bbox; this.parent = parent; this.owner = owner; this.val = owner.geoProp.get(bbox); } public T get(LatLon ll) { if (isInside(ll)) return getBounded(ll); if (DEBUG) System.err.print("up["+level+"]"); return parent.get(ll); } private T getBounded(LatLon ll) { if (DEBUG) System.err.print("GPLevel["+level+"]"+bbox+" "); if (!isInside(ll)) { throw new AssertionError("Point "+ll+" should be inside "+bbox); } if (val != null) { if (DEBUG) System.err.println(" hit! "+val); owner.lastLevelUsed = this; return val; } if (level >= owner.maxLevel) { if (DEBUG) System.err.println(" max level reached !"); return owner.geoProp.get(ll); } if (children == null) { @SuppressWarnings("unchecked") GPLevel[] tmp = new GPLevel[4]; this.children = tmp; } int idx = index(ll, level+1); if (children[idx] == null) { double lon1, lat1; switch (idx) { case 0: lon1 = bbox.getTopLeftLon(); lat1 = bbox.getBottomRightLat(); break; case 1: lon1 = bbox.getTopLeftLon(); lat1 = bbox.getTopLeftLat(); break; case 2: lon1 = bbox.getBottomRightLon(); lat1 = bbox.getBottomRightLat(); break; case 3: lon1 = bbox.getBottomRightLon(); lat1 = bbox.getTopLeftLat(); break; default: throw new AssertionError(); } if (DEBUG) System.err.println(" - new with idx "+idx); LatLon center = bbox.getCenter(); BBox b = new BBox(lon1, lat1, center.lon(), center.lat()); children[idx] = new GPLevel<>(level + 1, b, this, owner); } return children[idx].getBounded(ll); } /** * Checks, if a point is inside this tile. * Makes sure, that neighboring tiles do not overlap, i.e. a point exactly * on the border of two tiles must be inside exactly one of the tiles. * @param ll the coordinates of the point * @return true, if it is inside of the box */ boolean isInside(LatLon ll) { return bbox.getTopLeftLon() <= ll.lon() && (ll.lon() < bbox.getBottomRightLon() || (ll.lon() == 180.0 && bbox.getBottomRightLon() == 180.0)) && bbox.getBottomRightLat() <= ll.lat() && (ll.lat() < bbox.getTopLeftLat() || (ll.lat() == 90.0 && bbox.getTopLeftLat() == 90.0)); } } }