package aimax.osm.data.impl; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.List; import aimax.osm.data.BoundingBox; import aimax.osm.data.EntityVisitor; import aimax.osm.data.entities.MapEntity; /** * Implementation of a kd-tree (node) for map entities. Entities are * divided by their two-dimensional geographical coordinates and within each * kd-tree node ordered by ascending minimal visible scale. Note, that * inner nodes can contain entities, e.g. ways, whose nodes do * not completely fit into the bounding box of one of the child nodes. * @author Ruediger Lunde */ public class KDTree { private BoundingBox bb; private KDTree[] children; // null or array of length 2 private int depth; private int maxEntities; private int maxDepth; private ArrayList<DefaultMapEntity> entities; private boolean splitAtLat; private float splitValue; private boolean isSorted; /** * Constructs the root of the tree. * @param bb The bounding box. Only entities within the box can be added. * @param maxEntities Defines, when a leaf node should be split. * @param maxDepth Limits the depth of the tree. */ public KDTree(BoundingBox bb, int maxEntities, int maxDepth) { this.bb = bb; depth = 0; this.maxEntities = maxEntities; this.maxDepth = maxDepth; entities = new ArrayList<DefaultMapEntity>(); } /** Constructor for inner and leaf nodes. */ private KDTree(BoundingBox bb, int maxEntities, int maxDepth, int depth) { this(bb, maxEntities, maxDepth); this.depth = depth; } public BoundingBox getBoundingBox() { return bb; } /** Returns the depth of the tree (longest path length from root to leaf). */ public int depth() { return children == null ? 1 : 1+Math.max(children[0].depth(), children[1].depth()); } /** Must be called after classification of entities has been changed. */ public void setUnsorted() { isSorted = false; if (children != null) { children[0].setUnsorted(); children[1].setUnsorted(); } } /** * Adds an entity at the right position in the tree and extends * the tree if necessary. It is assumed that the entity contains * view information. */ public void insertEntity(DefaultMapEntity entity) { if (children == null) { entities.add(entity); isSorted = false; if (entities.size() > maxEntities && depth < maxDepth) { computeSplitValues(); BoundingBox c1bb; BoundingBox c2bb; if (splitAtLat) { c1bb = new BoundingBox (bb.getLatMin(), bb.getLonMin(), splitValue, bb.getLonMax()); c2bb = new BoundingBox (splitValue, bb.getLonMin(), bb.getLatMax(), bb.getLonMax()); } else { c1bb = new BoundingBox (bb.getLatMin(), bb.getLonMin(), bb.getLatMax(), splitValue); c2bb = new BoundingBox (bb.getLatMin(), splitValue, bb.getLatMax(), bb.getLonMax()); } children = new KDTree[2]; children[0] = new KDTree(c1bb, maxEntities, maxDepth, depth+1); children[1] = new KDTree(c2bb, maxEntities, maxDepth, depth+1); List<DefaultMapEntity> tmp = entities; entities = new ArrayList<DefaultMapEntity>(); for (DefaultMapEntity ne : tmp) insertEntity(ne); } } else { int cr = (splitAtLat ? entity.compareLatitude(splitValue) : entity.compareLongitude(splitValue)); if (cr < 0) children[0].insertEntity(entity); else if (cr > 0) children[1].insertEntity(entity); else { entities.add(entity); isSorted = false; } } } /** * Splits the bounding box, so that the new boxes have equal size * and look square-like as much as possible. */ private void computeSplitValues() { float latMid = (bb.getLatMin()+bb.getLatMax()) / 2f; double width = (bb.getLonMax()-bb.getLonMin()) * Math.cos(latMid * Math.PI / 180.0); double height = bb.getLatMax()-bb.getLatMin(); if (height > width) { splitAtLat = true; splitValue = latMid; } else { splitAtLat = false; splitValue = (bb.getLonMin()+bb.getLonMax()) / 2; } } /** Returns for each split bar the end points (lat1, lon1, lat2, lon2). */ public List<double[]> getSplitCoords() { List<double[]> result; double[] coords; if (splitAtLat) { double lonDiff = (bb.getLonMax()-bb.getLonMin()) / 20; coords = new double[] { splitValue, bb.getLonMin()+lonDiff, splitValue, bb.getLonMax()-lonDiff }; } else { double latDiff = (bb.getLatMax()-bb.getLatMin()) / 20; coords = new double[] { bb.getLatMin()+latDiff, splitValue, bb.getLatMax()-latDiff, splitValue }; } if (children == null) { result = new ArrayList<double[]>(); } else { result = children[0].getSplitCoords(); result.addAll(children[1].getSplitCoords()); } result.add(coords); return result; } /** * Enables to iterate across all contained entities within a given region * in an intelligent manner. Only tree nodes, which have a chance to * meet the location requirement are visited. */ public void visitEntities(EntityVisitor visitor, BoundingBox vbox, float scale) { if (!entities.isEmpty()) { if (!isSorted) { Collections.sort(entities, new EntityComparator()); isSorted = true; } VisibilityTest vtest = new VisibilityTest(bb, vbox); for (DefaultMapEntity entity : entities) { if (entity.getViewInfo().getMinVisibleScale() > scale) break; if (vtest.isVisible(entity)) entity.accept(visitor); } } if (children != null) { float vMin = (splitAtLat ? vbox.getLatMin() : vbox.getLonMin()); float vMax = (splitAtLat ? vbox.getLatMax() : vbox.getLonMax()); if (vMin <= splitValue) children[0].visitEntities(visitor, vbox, scale); if (vMax >= splitValue) children[1].visitEntities(visitor, vbox, scale); } } ///////////////////////////////////////////////////////////////// // some inner classes /** * Compares entities with respect to their minimal visible scale. * Entities which are already visible in small scales are preferred. */ private static class EntityComparator implements Comparator<MapEntity> { @Override public int compare(MapEntity e1, MapEntity e2) { float vs1 = e1.getViewInfo().getMinVisibleScale(); float vs2 = e2.getViewInfo().getMinVisibleScale(); if (vs1 < vs2) return -1; else if (vs1 > vs2) return 1; else return 0; } } /** * If a kd-tree node's bounding box is not completely visible, * this class helps to check which entities are visible and which not. * @author Ruediger Lunde * */ private static class VisibilityTest { private boolean isTrue = true; private float testLatMin = Float.NaN; private float testLonMin = Float.NaN; private float testLatMax = Float.NaN; private float testLonMax = Float.NaN; VisibilityTest(BoundingBox treeBox, BoundingBox visibleBox) { if (treeBox.getLatMin() < visibleBox.getLatMin()) { testLatMin = visibleBox.getLatMin(); isTrue = false; } if (treeBox.getLonMin() < visibleBox.getLonMin()) { testLonMin = visibleBox.getLonMin(); isTrue = false; } if (treeBox.getLatMax() > visibleBox.getLatMax()) { testLatMax = visibleBox.getLatMax(); isTrue = false; } if (treeBox.getLonMax() > visibleBox.getLonMax()) { testLonMax = visibleBox.getLonMax(); isTrue = false; } } boolean isVisible(DefaultMapEntity entity) { if (isTrue) return true; if (testLatMin != Float.NaN && entity.compareLatitude(testLatMin) < 0) return false; // entity below visible area if (testLonMin != Float.NaN && entity.compareLongitude(testLonMin) < 0) return false; // entity to the left of visible area if (testLatMax != Float.NaN && entity.compareLatitude(testLatMax) > 0) return false; // entity above visible area if (testLonMax != Float.NaN && entity.compareLongitude(testLonMax) > 0) return false; // entity to the right of visible area return true; } } }