// License: GPL. For details, see LICENSE file. package org.openstreetmap.josm.data.osm; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Iterator; import java.util.List; import org.openstreetmap.josm.Main; import org.openstreetmap.josm.data.coor.LatLon; import org.openstreetmap.josm.data.coor.QuadTiling; /** * Note: bbox of primitives added to QuadBuckets has to stay the same. In case of coordinate change, primitive must * be removed and readded. * * This class is (no longer) thread safe. * */ public class QuadBuckets implements Collection { private static final boolean consistency_testing = false; private static final int NW_INDEX = 1; private static final int NE_INDEX = 3; private static final int SE_INDEX = 2; private static final int SW_INDEX = 0; static void abort(String s) { throw new AssertionError(s); } public static final int MAX_OBJECTS_PER_LEVEL = 16; static class QBLevel { private final int level; private final int index; private final BBox bbox; private final long quad; private final QBLevel parent; private boolean isLeaf = true; private List content; // child order by index is sw, nw, se, ne private QBLevel nw, ne, sw, se; private final QuadBuckets buckets; private QBLevel getChild(int index) { switch (index) { case NE_INDEX: if (ne == null) { ne = new QBLevel(this, index, buckets); } return ne; case NW_INDEX: if (nw == null) { nw = new QBLevel(this, index, buckets); } return nw; case SE_INDEX: if (se == null) { se = new QBLevel(this, index, buckets); } return se; case SW_INDEX: if (sw == null) { sw = new QBLevel(this, index, buckets); } return sw; default: return null; } } @SuppressWarnings("unchecked") private QBLevel[] getChildren() { return new QBLevel[] {sw, nw, se, ne}; } @Override public String toString() { return super.toString() + "[" + level + "]: " + bbox(); } /** * Constructor for root node */ public QBLevel(final QuadBuckets buckets) { level = 0; index = 0; quad = 0; parent = null; bbox = new BBox(-180, 90, 180, -90); this.buckets = buckets; } public QBLevel(QBLevel parent, int parent_index, final QuadBuckets buckets) { this.parent = parent; this.level = parent.level + 1; this.index = parent_index; this.buckets = buckets; int shift = (QuadTiling.NR_LEVELS - level) * 2; long mult = 1; // Java blows the big one. It seems to wrap when you shift by > 31 if (shift >= 30) { shift -= 30; mult = 1 << 30; } long this_quadpart = mult * (parent_index << shift); this.quad = parent.quad | this_quadpart; this.bbox = calculateBBox(); // calculateBBox reference quad } private BBox calculateBBox() { LatLon bottom_left = this.coor(); double lat = bottom_left.lat() + parent.height() / 2; double lon = bottom_left.lon() + parent.width() / 2; return new BBox(bottom_left.lon(), bottom_left.lat(), lon, lat); } QBLevel findBucket(BBox bbox) { if (!hasChildren()) return this; else { int index = bbox.getIndex(level); if (index == -1) return this; return getChild(index).findBucket(bbox); } } boolean remove_content(T o) { // If two threads try to remove item at the same time from different buckets of this QBLevel, // it might happen that one thread removes bucket but don't remove parent because it still sees // another bucket set. Second thread do the same. Due to thread memory caching, it's possible that // changes made by threads will show up in children array too late, leading to QBLevel with all children // set to null if (content == null) return false; boolean ret = this.content.remove(o); if (this.content.isEmpty()) { this.content = null; } if (this.canRemove()) { this.remove_from_parent(); } return ret; } /* * There is a race between this and qb.nextContentNode(). * If nextContentNode() runs into this bucket, it may * attempt to null out 'children' because it thinks this * is a dead end. */ void __split() { List tmpcontent = content; content = null; for (T o : tmpcontent) { int index = o.getBBox().getIndex(level); if (index == -1) { __add_content(o); } else { getChild(index).doAdd(o); } } isLeaf = false; // It's not enough to check children because all items could end up in this level (index == -1) } boolean __add_content(T o) { boolean ret = false; // The split_lock will keep two concurrent calls from overwriting content if (content == null) { content = new ArrayList(); } ret = content.add(o); return ret; } boolean matches(final T o, final BBox search_bbox) { if (o instanceof Node){ final LatLon latLon = ((Node)o).getCoor(); // node without coords -> bbox[0,0,0,0] return search_bbox.bounds(latLon != null ? latLon : LatLon.ZERO); } return o.getBBox().intersects(search_bbox); } private void search_contents(BBox search_bbox, List result) { /* * It is possible that this was created in a split * but never got any content populated. */ if (content == null) return; for (T o : content) { if (matches(o, search_bbox)) { result.add(o); } } } /* * This is stupid. I tried to have a QBLeaf and QBBranch * class descending from a QBLevel. It's more than twice * as slow. So, this throws OO out the window, but it * is fast. Runtime type determination must be slow. */ boolean isLeaf() { return isLeaf; } boolean hasChildren() { return nw != null || ne != null || sw != null || se != null; } QBLevel next_sibling() { return (parent == null) ? null : parent.firstSiblingOf(this); } boolean hasContent() { return content != null; } QBLevel nextSibling() { QBLevel next = this; QBLevel sibling = next.next_sibling(); // Walk back up the tree to find the // next sibling node. It may be either // a leaf or branch. while (sibling == null) { next = next.parent; if (next == null) { break; } sibling = next.next_sibling(); } next = sibling; return next; } QBLevel firstChild() { if (sw != null) return sw; if (nw != null) return nw; if (se != null) return se; return ne; } QBLevel firstSiblingOf(final QBLevel child) { switch (child.index) { case SW_INDEX: if (nw != null) return nw; case NW_INDEX: if (se != null) return se; case SE_INDEX: return ne; } return null; } QBLevel nextNode() { if (!this.hasChildren()) return this.nextSibling(); return this.firstChild(); } QBLevel nextContentNode() { QBLevel next = this.nextNode(); if (next == null) return next; if (next.hasContent()) return next; return next.nextContentNode(); } void doAdd(T o) { if (consistency_testing) { if (!matches(o, this.bbox())) { o.getBBox().getIndex(level); o.getBBox().getIndex(level - 1); int nr = 0; abort("\nobject " + o + " does not belong in node at level: " + level + " bbox: " + this.bbox()); } } __add_content(o); if (isLeaf() && content.size() > MAX_OBJECTS_PER_LEVEL && level < QuadTiling.NR_LEVELS) { __split(); } } void add(T o) { findBucket(o.getBBox()).doAdd(o); } private void search(BBox search_bbox, List result) { if (!this.bbox().intersects(search_bbox)) return; else if (bbox().bounds(search_bbox)) { buckets.search_cache = this; } if (this.hasContent()) { search_contents(search_bbox, result); } //TODO Coincidence vector should be calculated here and only buckets that match search_bbox should be checked if (nw != null) { nw.search(search_bbox, result); } if (ne != null) { ne.search(search_bbox, result); } if (se != null) { se.search(search_bbox, result); } if (sw != null) { sw.search(search_bbox, result); } } public String quads() { return Long.toHexString(quad); } int index_of(QBLevel find_this) { QBLevel[] children = getChildren(); for (int i = 0; i < QuadTiling.TILES_PER_LEVEL; i++) { if (children[i] == find_this) return i; } return -1; } double width() { return bbox.width(); } double height() { return bbox.height(); } public BBox bbox() { return bbox; } /* * This gives the coordinate of the bottom-left * corner of the box */ LatLon coor() { return QuadTiling.tile2LatLon(this.quad); } void remove_from_parent() { if (parent == null) return; if (!canRemove()) { abort("attempt to remove non-empty child: " + this.content + " " + Arrays.toString(this.getChildren())); } if (parent.nw == this) { parent.nw = null; } else if (parent.ne == this) { parent.ne = null; } else if (parent.sw == this) { parent.sw = null; } else if (parent.se == this) { parent.se = null; } if (parent.canRemove()) { parent.remove_from_parent(); } } boolean canRemove() { if (content != null && !content.isEmpty()) return false; if (this.hasChildren()) return false; return true; } } private QBLevel root; private QBLevel search_cache; private int size; /** * Constructs a new {@code QuadBuckets}. */ public QuadBuckets() { clear(); } @Override public void clear() { root = new QBLevel(this); search_cache = null; size = 0; } @Override public boolean add(T n) { root.add(n); size++; return true; } @Override public boolean retainAll(Collection objects) { for (T o : this) { if (objects.contains(o)) { continue; } if (!this.remove(o)) return false; } return true; } @Override public boolean removeAll(Collection objects) { boolean changed = false; for (Object o : objects) { changed = changed | remove(o); } return changed; } @Override public boolean addAll(Collection objects) { boolean changed = false; for (T o : objects) { changed = changed | this.add(o); } return changed; } @Override public boolean containsAll(Collection objects) { for (Object o : objects) { if (!this.contains(o)) return false; } return true; } @Override public boolean remove(Object o) { @SuppressWarnings("unchecked") T t = (T) o; search_cache = null; // Search cache might point to one of removed buckets QBLevel bucket = root.findBucket(t.getBBox()); if (bucket.remove_content(t)) { size--; return true; } else return false; } @Override public boolean contains(Object o) { @SuppressWarnings("unchecked") T t = (T) o; QBLevel bucket = root.findBucket(t.getBBox()); return bucket != null && bucket.content != null && bucket.content.contains(t); } public ArrayList toArrayList() { ArrayList a = new ArrayList(); for (T n : this) { a.add(n); } return a; } @Override public Object[] toArray() { return this.toArrayList().toArray(); } @Override public A[] toArray(A[] template) { return this.toArrayList().toArray(template); } class QuadBucketIterator implements Iterator { QBLevel current_node; int content_index; int iterated_over; QBLevel next_content_node(QBLevel q) { if (q == null) return null; QBLevel orig = q; QBLevel next; next = q.nextContentNode(); //if (consistency_testing && (orig == next)) if (orig == next) { abort("got same leaf back leaf: " + q.isLeaf()); } return next; } public QuadBucketIterator(QuadBuckets qb) { if (!qb.root.hasChildren() || qb.root.hasContent()) { current_node = qb.root; } else { current_node = next_content_node(qb.root); } iterated_over = 0; } @Override public boolean hasNext() { if (this.peek() == null) return false; return true; } T peek() { if (current_node == null) return null; while ((current_node.content == null) || (content_index >= current_node.content.size())) { content_index = 0; current_node = next_content_node(current_node); if (current_node == null) { break; } } if (current_node == null || current_node.content == null) return null; return current_node.content.get(content_index); } @Override public T next() { T ret = peek(); content_index++; iterated_over++; return ret; } @Override public void remove() { // two uses // 1. Back up to the thing we just returned // 2. move the index back since we removed // an element content_index--; T object = peek(); current_node.remove_content(object); } } @Override public Iterator iterator() { return new QuadBucketIterator(this); } @Override public int size() { return size; } @Override public boolean isEmpty() { if (this.size() == 0) return true; return false; } public List search(BBox search_bbox) { List ret = new ArrayList(); // Doing this cuts down search cost on a real-life data set by about 25% boolean cache_searches = true; if (cache_searches) { if (search_cache == null) { search_cache = root; } // Walk back up the tree when the last search spot can not cover the current search while (search_cache != null && !search_cache.bbox().bounds(search_bbox)) { search_cache = search_cache.parent; } if (search_cache == null) { search_cache = root; Main.info("bbox: " + search_bbox + " is out of the world"); } } else { search_cache = root; } // Save parent because search_cache might change during search call QBLevel tmp = search_cache.parent; search_cache.search(search_bbox, ret); // A way that spans this bucket may be stored in one // of the nodes which is a parent of the search cache while (tmp != null) { tmp.search_contents(search_bbox, ret); tmp = tmp.parent; } return ret; } public void printTree() { printTreeRecursive(root, 0); } private void printTreeRecursive(QBLevel level, int indent) { if (level == null) { printIndented(indent, ""); return; } printIndented(indent, level); if (level.hasContent()) { for (T o : level.content) { printIndented(indent, o); } } for (QBLevel child : level.getChildren()) { printTreeRecursive(child, indent + 2); } } private void printIndented(int indent, Object msg) { for (int i = 0; i < indent; i++) { System.out.print(' '); } System.out.println(msg); } }