Ticket #3465: QuadBuckets.patch

File QuadBuckets.patch, 39.0 KB (added by hansendc, 17 years ago)

QuadBuckets patch

  • new file core-dave/src/org/openstreetmap/josm/data/osm/QuadBuckets.java

    I've been hacking on the JOSM validator plugin for a while.  One of the
    repeating "hard problems" that comes up are doing the UnconnectedWays tests.
    You need to do searches for every segment in a way to see if there are any
    nearby nodes.  This generally means that you do a number of searches on the
    same order as the number of nodes that you have.
    
    If you have 100k way nodes, you end up doing ~90k searches (one for each
    segment) each looking at a sizable number of those 100k nodes.  Any way you
    slice it, you end up doing billions of searches since it's an O(n^2) algorithm.
    A single validation run with 50k nodes takes me 183 seconds.
    
    I indexed 500,000 nodes from Oregon in the US.  I then took another 3,000,000
    nodes and searched to find which of those 500,000 were near them.  Each
    iteration over 500,000 nodes for me takes ~0.28 seconds.  If I were to do
    3,000,000 searches, it would take 1.63 days!  So, I attempted to optimize this.
    
    The messages is lengthy one, but feel free to stop and look at the code now.
    If you want to know how I got that 183-second run down to 2 seconds, read
    down. :)
    
    --
    
    My first attempt to optimize was to do binary searches.  I indexed all of the
    nodes in two arrays, one by x coordinate and one by y.  I did a binary search
    for each axis, iterated out from the place that the binary search found
    (representing the radius of the search).  I then combined the two searches.
    Algorithmically, it's still an O(n^2) algorithm since there's an iteration, but
    it was significantly faster since the number of nodes that were touched was
    smaller.
    
    But, I still ran into problems with large data sets because I could still get
    thousands of nodes returned from each axis search.  Optimizing, I instead kept
    just a single index x-axis.  I then sorted the result of the x-axis binary
    search by the y-coordinate and binary searched on that.  That's as fast as I
    ever got that algorithm to go.  It ended up way better than the linear search
    (~600x!) but still takes ~200s for the 3,000,000 node search.
    
    Can we make the binary search better, or is there was still a fundamental
    limitation in that algorithm?  I think I was up against a pretty fundamental
    limitation if I chose to sort by a single coordinate at a time: during the
    search, it touched a *LOT* of virtually random data.  Since I indexed all of
    the coordinates by longitude, a point in Greenwich could be in the binary
    x-axis index next to a point in Algeria and next to another in Antarctica (all
    at 0 longitude).  There's a lot of jumping around to pseudo-random points all
    over the data set.  That thrashes the CPU caches and TLB.  
    
    I was intrigued by the quadtiling algorithm that we currently use on the
    database servers.
    
    	http://wiki.openstreetmap.org/wiki/QuadTiles
    
    If we were to make a data structure that represents the tiles, we could chop up
    the world into much smaller pieces.  Searches would also potentially be faster
    since we could operate on subsets of the data much more easily.  We would not
    be dealing with the whole world of random data.
    
    So, I went and implemented it.  I call it QuadBuckets, and it's basically an
    unbalanced 4-way radix tree structure.  We radix on the bits in the quad index.
    It stores individual data in quad bucket levels (QBLevel)s that can either
    contain pointers to 4 more child QBLevels or may act as a leaf node and store
    arbitrarily long lists of OSM Nodes.
    
    When a leaf node gets too large (my testing shows that 16 or 32 elements is the
    best maximum size for performance.. see todo list), the leaf is split into its
    4 constituent child tiles and its contents are redistributed among them.
    Several splits (and new tree levels) may be required if the stored coordinates
    were very close together.
    
    The quad tiling in the database uses 16 bits of precision.  This means that the
    least-significant quadtile bit represents an area about 610m (40,000km / 2^16)
    across (and half that tall) at the equator.  On real data sets, this means the
    buckets are just too large.  So, I added another 8 bits of precision so that we
    have buckets that are now ~2.4m across.  Making sure buckets don't get too big
    is one of the keys to this algorithm, and these buckets are small enough.
    
    I also implemented a simple "search cache".  It records the last QBLevel node
    from which the last search found results.  When the next search request is
    made, we check to see if this tree node can satisfy the search, and avoid
    walking too far down the tree each time.  If the tree node can not satisfy the
    search, we walk back up the tree until the search can be satisfied.  This
    optimization speeds things up by ~25%.
    
    For testing, I first used random data spanning the globe, basically creating
    nodes with random latitudes and longitudes.  This is a near worst-case for
    QuadBuckets.  Despite this, I got the QuadBuckets approach to be about 30%
    faster than the binary searches.  But, using real OSM data, it really shines.
    
    I threw the 3,000,000 search workload at it.  Remember, using the binary search
    method, it took on average ~200s.  But, using QuadBuckets, the same search was
    9.84s on average.  So, we've gone from 140,000 seconds to 200 to 9.84.  :)
    
    Although it is hard to profile, QuadBuckets seem to outperform the binary
    search even in quite small data sets.  Their worst-case performance happens if
    you try to store an extremely large number of nodes that are within the
    bottom-level tiles (those 2.4m boxes) since it degrades to array storage at
    that point.  It will also degrade if you are working on an area that spans
    several high-level tiles.I don't think this will happen in the real world, much.
    It will probably still be faster than anything else out there.
    
    QuadBuckets do have a higher insertion cost than the other methods. Computing
    the quad tile index means a bunch of Math.floor() function calls, and
    those seem to be at the top of the profiles.  But, we could probably mitigate
    this insertion cost by deferring computing the quad tile coordinates until the
    first search is performed.
    
    TODO:
    * make content buckets larger and do binary searches inside them
    * Do something smarter for Nodes with null getCoor() other than
      sticking in tile 0000000000000000
    * Clean up the code, remove debugging statements
    
    ---
    
     core-dave/src/org/openstreetmap/josm/data/coor/QuadTiling.java |  111 +
     core-dave/src/org/openstreetmap/josm/data/osm/QuadBuckets.java |  881 ++++++++++
     2 files changed, 992 insertions(+)
    
    diff -puN /dev/null src/org/openstreetmap/josm/data/osm/QuadBuckets.java
    - +  
     1package org.openstreetmap.josm.data.osm;
     2import java.util.ArrayList;
     3import java.util.Collection;
     4import java.util.Collections;
     5import java.util.Set;
     6import java.util.SortedMap;
     7import java.util.TreeMap;
     8
     9import org.openstreetmap.josm.data.coor.EastNorth;
     10import org.openstreetmap.josm.data.osm.Node;
     11import org.openstreetmap.josm.tools.Pair;
     12
     13import java.nio.MappedByteBuffer;
     14import java.nio.DoubleBuffer;
     15import java.nio.channels.FileChannel;
     16import java.io.FileOutputStream;
     17import java.io.RandomAccessFile;
     18
     19import java.io.IOException;
     20import java.io.BufferedReader;
     21import java.io.InputStreamReader;
     22
     23import java.util.Map.Entry;
     24import java.awt.geom.Point2D;
     25
     26//import java.util.List;
     27import java.util.*;
     28import java.util.Collection;
     29
     30import org.openstreetmap.josm.data.coor.QuadTiling;
     31import org.openstreetmap.josm.data.coor.CachedLatLon;
     32import org.openstreetmap.josm.data.coor.EastNorth;
     33import org.openstreetmap.josm.data.coor.LatLon;
     34import org.openstreetmap.josm.data.osm.visitor.Visitor;
     35
     36
     37public class QuadBuckets implements Collection<Node>
     38{
     39    public static boolean debug = false;
     40    static boolean consistency_testing = false;
     41
     42    static void abort(String s)
     43    {
     44        out(s);
     45        Object o = null;
     46        o.hashCode();
     47    }
     48    static void out(String s)
     49    {
     50        System.out.println(s);
     51    }
     52    // periodic output
     53    long last_out = -1;
     54    void pout(String s)
     55    {
     56        long now = System.currentTimeMillis();
     57        if (now - last_out < 300)
     58            return;
     59        last_out = now;
     60        System.out.print(s + "\r");
     61    }
     62    void pout(String s, int i, int total)
     63    {
     64        long now = System.currentTimeMillis();
     65        if ((now - last_out < 300) &&
     66            ((i+1) < total))
     67            return;
     68        last_out = now;
     69        // cast to float to keep the output size down
     70        System.out.print(s + " " + (float)((i+1)*100.0/total) + "% done    \r");
     71    }
     72    // 24 levels of buckets gives us bottom-level
     73    // buckets that are about 2 meters on a side.
     74    // That should do. :)
     75    public static int NR_LEVELS = 24;
     76    public static double WORLD_PARTS = (1 << NR_LEVELS);
     77
     78    public static int MAX_OBJECTS_PER_LEVEL = 16;
     79    // has to be a power of 2
     80    public static int TILES_PER_LEVEL_SHIFT = 2;
     81    public static int TILES_PER_LEVEL = 1<<TILES_PER_LEVEL_SHIFT;
     82    class BBox
     83    {
     84        private double xmin;
     85        private double xmax;
     86        private double ymin;
     87        private double ymax;
     88        void sanity()
     89        {
     90            if (xmin < -180.0)
     91                xmin = -180.0;
     92            if (xmax >  180.0)
     93                xmax =  180.0;
     94            if (ymin <  -90.0)
     95                ymin =  -90.0;
     96            if (ymax >   90.0)
     97                ymax =   90.0;
     98            if ((xmin < -180.0) ||
     99                (xmax >  180.0) ||
     100                (ymin <  -90.0) ||
     101                (ymax >   90.0)) {
     102                out("bad BBox: " + this);
     103                Object o = null;
     104                o.hashCode();
     105            }
     106        }
     107        public String toString()
     108        {
     109            return "[ " + xmin + " -> " + xmax + ", " +
     110                         ymin + " -> " + ymax + " ]";
     111        }
     112        double min(double a, double b)
     113        {
     114            if (a < b)
     115                return a;
     116            return b;
     117        }
     118        double max(double a, double b)
     119        {
     120            if (a > b)
     121                return a;
     122            return b;
     123        }
     124        public BBox(LatLon a, LatLon b)
     125        {
     126            xmin = min(a.lon(), b.lon());
     127            xmax = max(a.lon(), b.lon());
     128            ymin = min(a.lat(), b.lat());
     129            ymax = max(a.lat(), b.lat());
     130            sanity();
     131        }
     132        public BBox(double a_x, double a_y, double b_x, double b_y)
     133        {
     134            xmin = min(a_x, b_x);
     135            xmax = max(a_x, b_x);
     136            ymin = min(a_y, b_y);
     137            ymax = max(a_y, b_y);
     138            sanity();
     139        }
     140        public double height()
     141        {
     142            return ymax-ymin;
     143        }
     144        public double width()
     145        {
     146            return xmax-xmin;
     147        }
     148        boolean bounds(BBox b)
     149        {
     150            if (!(xmin <= b.xmin) ||
     151                !(xmax >= b.xmax) ||
     152                !(ymin <= b.ymin) ||
     153                !(ymax >= b.ymax))
     154                return false;
     155            return true;
     156        }
     157        boolean bounds(LatLon c)
     158        {
     159            if ((xmin <= c.lon()) &&
     160                (xmax >= c.lon()) &&
     161                (ymin <= c.lat()) &&
     162                (ymax >= c.lat()))
     163                return true;
     164            return false;
     165        }
     166        boolean inside(BBox b)
     167        {
     168            if (xmin >= b.xmax)
     169                return false;
     170            if (xmax <= b.xmin)
     171                return false;
     172            if (ymin >= b.ymax)
     173                return false;
     174            if (ymax <= b.ymin)
     175                return false;
     176            return true;
     177        }
     178        boolean intersects(BBox b)
     179        {
     180            return this.inside(b) || b.inside(this);
     181        }
     182    }
     183    class QBLevel
     184    {
     185        int level;
     186        long quad;
     187        QBLevel parent;
     188
     189        public List<Node> content;
     190        public QBLevel children[];
     191        public QBLevel(QBLevel parent)
     192        {
     193            init(parent);
     194        }
     195        String quads(Node n)
     196        {
     197            return Long.toHexString(QuadTiling.quadTile(n.getCoor()));
     198        }
     199        void split()
     200        {
     201            if (debug)
     202                out("splitting "+this.bbox()+" level "+level+" with "
     203                        + content.size() + " entries (my dimensions: "
     204                        + this.bbox.width()+", "+this.bbox.height()+")");
     205            if (children != null) {
     206                abort("overwrote children");
     207            }
     208            children = new QBLevel[QuadTiling.TILES_PER_LEVEL];
     209            // deferring allocation of children until use
     210            // seems a bit faster
     211            //for (int i = 0; i < TILES_PER_LEVEL; i++)
     212            //    children[i] = new QBLevel(this, i);
     213            List<Node> tmpcontent = content;
     214            content = null;
     215            for (Node n : tmpcontent) {
     216                int new_index = QuadTiling.index(n, level);
     217                if (children[new_index] == null)
     218                    children[new_index] = new QBLevel(this, new_index);
     219                QBLevel child = children[new_index];
     220                if (debug)
     221                    out("putting "+n+"(q:"+quads(n)+") into ["+new_index+"] " + child.bbox());
     222                child.add(n);
     223            }
     224        }
     225        void add_leaf(Node n)
     226        {
     227            LatLon c = n.getCoor();
     228            QBLevel ret = this;
     229            if (content == null) {
     230                if (debug)
     231                    out("   new content array");
     232                // I chose a LinkedList because we do not do
     233                // any real random access to this.  We generally
     234                // just run through the whole thing all at once
     235                content = new LinkedList<Node>();
     236            }
     237            content.add(n);
     238            if (content.size() > MAX_OBJECTS_PER_LEVEL) {
     239                if (debug)
     240                    out("["+level+"] deciding to split");
     241                if (level >= NR_LEVELS-1) {
     242                    out("can not split, but too deep: " + level + " size: " + content.size());
     243                    return;
     244                }
     245                int before_size = -1;
     246                if (consistency_testing)
     247                    before_size = this.size();
     248                this.split();
     249                if (consistency_testing) {
     250                    int after_size = this.size();
     251                    if (before_size != after_size) {
     252                        abort("["+level+"] split done before: " + before_size + " after: " + after_size);
     253                    }
     254                }
     255                return;
     256            }
     257            if (debug) {
     258                out("   plain content put now have " + content.size());
     259                if (content.contains(c))
     260                    out("   and I already have that one");
     261            }
     262        }
     263
     264        List<Node> search(BBox bbox, LatLon point, double radius)
     265        {
     266            if (isLeaf())
     267                return search_leaf(bbox, point, radius);
     268            return search_branch(bbox, point, radius);
     269        }
     270        private List<Node> search_leaf(BBox bbox, LatLon point, double radius)
     271        {
     272            if (debug)
     273                out("searching contents in tile " + this.bbox() + " for " + bbox);
     274            /*
     275             * It is possible that this was created in a split
     276             * but never got any content populated.
     277             */
     278            if (content == null)
     279                return null;
     280            // We could delay allocating this.  But, the way it is currently
     281            // used, we almost always have a node in the area that we're
     282            // searching since we're searching around other nodes.
     283            //
     284            // the iterator's calls to ArrayList.size() were showing up in
     285            // some profiles, so I'm trying a LinkedList instead
     286            List<Node> ret = new LinkedList<Node>();
     287            for (Node n : content) {
     288                // This supports two modes: one where the bbox is just
     289                // an outline of the point/radius combo, and one where
     290                // it is *just* the bbox.  If it is *just* the bbox,
     291                // don't consider the points below
     292                if (point == null) {
     293                    ret.add(n);
     294                    continue;
     295                }
     296                LatLon c = n.getCoor();
     297                // is greatCircleDistance really necessary?
     298                double d = c.greatCircleDistance(point);
     299                if (debug)
     300                    out("[" + level + "] Checking coor: " + c + " dist: " + d + " vs. " + radius + " " + quads(n));
     301                if (d > radius)
     302                    continue;
     303                if (debug)
     304                    out("hit in quad: "+Long.toHexString(this.quad)+"\n");
     305                //if (ret == null)
     306                //    ret = new LinkedList<Node>();
     307                ret.add(n);
     308            }
     309            if (debug)
     310                out("done searching quad " + Long.toHexString(this.quad));
     311            return ret;
     312        }
     313        /*
     314         * This is stupid.  I tried to have a QBLeaf and QBBranch
     315         * class decending from a QBLevel.  It's more than twice
     316         * as slow.  So, this throws OO out the window, but it
     317         * is fast.  Runtime type determination must be slow.
     318         */
     319        boolean isLeaf()
     320        {
     321            if (children == null)
     322                return true;
     323            return false;
     324        }
     325        QBLevel next_sibling()
     326        {
     327            boolean found_me = false;
     328            if (parent == null)
     329                return null;
     330            int __nr = 0;
     331            for (QBLevel sibling : parent.children) {
     332                __nr++;
     333                int nr = __nr-1;
     334                if (sibling == null) {
     335                    if (debug)
     336                        out("[" + this.level + "] null child nr: " + nr);
     337                    continue;
     338                }
     339                // We're looking for the *next* child
     340                // after us.
     341                if (sibling == this) {
     342                    if (debug)
     343                        out("[" + this.level + "] I was child nr: " + nr);
     344                    found_me = true;
     345                    continue;
     346                }
     347                if (found_me) {
     348                    if (debug)
     349                        out("[" + this.level + "] next sibling was child nr: " + nr);
     350                    return sibling;
     351                }
     352                if (debug)
     353                    out("[" + this.level + "] nr: " + nr + " is before me, ignoring...");
     354            }
     355            return null;
     356        }
     357        QBLevel nextLeaf()
     358        {
     359            QBLevel next = this;
     360            if (this.isLeaf()) {
     361                QBLevel sibling = next.next_sibling();
     362                // Walk back up the tree to find the
     363                // next sibling node.  It may be either
     364                // a leaf or branch.
     365                while (sibling == null) {
     366                    if (debug)
     367                        out("no siblings at level["+next.level+"], moving to parent");
     368                    next = next.parent;
     369                    if (next == null)
     370                        break;
     371                    sibling = next.next_sibling();
     372                }
     373                next = sibling;
     374            }
     375            if (next == null)
     376                return null;
     377            // all branches are guaranteed to have
     378            // at least one leaf.  It may not have
     379            // any contents, but there *will* be a
     380            // leaf.  So, walk back down the tree
     381            // and find the first leaf
     382            while (!next.isLeaf()) {
     383                if (debug)
     384                    out("["+next.level+"] next node is a branch, moving down...");
     385                for (QBLevel child : next.children) {
     386                    if (child == null)
     387                        continue;
     388                    next = child;
     389                    break;
     390                }
     391            }
     392            return next;
     393        }
     394        int size()
     395        {
     396            if (isLeaf())
     397                return size_leaf();
     398            return size_branch();
     399        }
     400        private int size_leaf()
     401        {
     402            if (content == null) {
     403                if (debug)
     404                    out("["+level+"] leaf size: null content, children? " + children);
     405                return 0;
     406            }
     407            if (debug)
     408                out("["+level+"] leaf size: " + content.size());
     409            return content.size();
     410        }
     411        private int size_branch()
     412        {
     413            int ret = 0;
     414            for (QBLevel l : children) {
     415                if (l == null)
     416                    continue;
     417                ret += l.size();
     418            }
     419            if (debug)
     420                out("["+level+"] branch size: " + ret);
     421            return ret;
     422        }
     423        boolean contains(Node n)
     424        {
     425            QBLevel res = find_exact(n);
     426            if (res == null)
     427                return false;
     428            return true;
     429        }
     430        QBLevel find_exact(Node n)
     431        {
     432            if (isLeaf())
     433                return find_exact_leaf(n);
     434            return find_exact_branch(n);
     435        }
     436        QBLevel find_exact_leaf(Node n)
     437        {
     438            QBLevel ret = null;
     439            if (content != null && content.contains(n))
     440                ret = this;
     441            return ret;
     442        }
     443        QBLevel find_exact_branch(Node n)
     444        {
     445            QBLevel ret = null;
     446            for (QBLevel l : children) {
     447                if (l == null)
     448                    continue;
     449                ret = l.find_exact(n);
     450                if (ret != null)
     451                    break;
     452            }
     453            return ret;
     454        }
     455        boolean add(Node n)
     456        {
     457            if (isLeaf())
     458                add_leaf(n);
     459            else
     460                add_branch(n);
     461            return true;
     462        }
     463        QBLevel add_branch(Node n)
     464        {
     465            LatLon c = n.getCoor();
     466            int index = QuadTiling.index(n, level);
     467            if (debug)
     468                out("[" + level + "]: " + n +
     469                    " index " + index + " levelquad:" + this.quads() + " level bbox:" + this.bbox());
     470            if (debug)
     471                out("   put in child["+index+"]");
     472            if (children[index] == null)
     473                children[index] = new QBLevel(this, index);
     474            children[index].add(n);
     475            /* this is broken at the moment because we need to handle the null n.getCoor()
     476            if (consistency_testing && !children[index].bbox().bounds(n.getCoor())) {
     477                out("target child["+index+"] does not bound " + children[index].bbox() + " " + c);
     478                for (int i = 0; i < children.length; i++) {
     479                    QBLevel ch = children[i];
     480                    if (ch == null)
     481                        continue;
     482                    out("child["+i+"] quad: " + ch.quads() + " bounds: " + ch.bbox.bounds(n.getCoor()));
     483                }
     484                out(" coor quad: " + quads(n));
     485                abort("");
     486            }*/
     487
     488            if (consistency_testing) {
     489                for (int i = 0; i < children.length; i++) {
     490                    QBLevel ch = children[i];
     491                    if (ch == null)
     492                        continue;
     493                    if (ch.bbox().bounds(c) && i != index) {
     494                        out("multible bounding?? expected: " + index + " but saw at " + i + " " + ch.quads());
     495                        out("child["+i+"] bbox: " + ch.bbox());
     496                    }
     497                }
     498            }
     499            return this;
     500        }
     501        private List<Node> search_branch(BBox bbox, LatLon point, double radius)
     502        {
     503            List<Node> ret = null;
     504            if (debug)
     505                System.out.print("[" + level + "] qb bbox: " + this.bbox() + " ");
     506            if (!this.bbox().intersects(bbox)) {
     507                if (debug) {
     508                    out("miss " + Long.toHexString(this.quad));
     509                    //QuadTiling.tile2xy(this.quad);
     510                }
     511                return ret;
     512            }
     513            if (debug)
     514                out("hit " + this.quads());
     515
     516            if (debug)
     517                out("[" + level + "] not leaf, moving down");
     518            int child_nr = 0;
     519            for (QBLevel q : children) {
     520                if (q == null)
     521                    continue;
     522                child_nr++;
     523                if (debug)
     524                    System.out.print(child_nr+": ");
     525                List<Node> coors = q.search(bbox, point, radius);
     526                if (coors == null)
     527                    continue;
     528                if (ret == null)
     529                    ret = coors;
     530                else
     531                    ret.addAll(coors);
     532                if (q.bbox().bounds(bbox)) {
     533                    search_cache = q;
     534                    // optimization: do not continue searching
     535                    // other tiles if this one wholly contained
     536                    // what we were looking for.
     537                    if (coors.size() > 0 ) {
     538                        if (debug)
     539                            out("break early");
     540                        break;
     541                    }
     542                }
     543            }
     544            return ret;
     545        }
     546        public String quads()
     547        {
     548            return Long.toHexString(quad);
     549        }
     550        public void init(QBLevel parent)
     551        {
     552                this.parent = parent;
     553                if (parent != null)
     554                    this.level = parent.level + 1;
     555                this.quad = 0;
     556        }
     557        int index_of(QBLevel find_this)
     558        {
     559            if (this.isLeaf())
     560                return -1;
     561
     562            for (int i = 0; i < QuadTiling.TILES_PER_LEVEL; i++) {
     563                if (children[i] == find_this)
     564                    return i;
     565            }
     566            return -1;
     567        }
     568        public QBLevel(QBLevel parent, int parent_index)
     569        {
     570            this.init(parent);
     571            this.level = parent.level+1;
     572            this.parent = parent;
     573            int shift = (QuadBuckets.NR_LEVELS - level) * 2;
     574            long mult = 1;
     575            // Java blows the big one.  It seems to wrap when
     576            // you shift by > 31
     577            if (shift >= 30) {
     578                shift -= 30;
     579                mult = 1<<30;
     580            }
     581            long this_quadpart = mult * (parent_index << shift);
     582            this.quad = parent.quad | this_quadpart;
     583            if (debug)
     584                out("new level["+this.level+"] bbox["+parent_index+"]: " + this.bbox()
     585                        + " coor: " + this.coor()
     586                        + " quadpart: " + Long.toHexString(this_quadpart)
     587                        + " quad: " + Long.toHexString(this.quad));
     588        }
     589        /*
     590         * Surely we can calculate these efficiently instead of storing
     591         */
     592        double width = Double.NEGATIVE_INFINITY;
     593        double width()
     594        {
     595            if (width != Double.NEGATIVE_INFINITY)
     596                return this.width;
     597            if (level == 0) {
     598                width = this.bbox().width();
     599            } else {
     600                width = parent.width()/2;
     601            }
     602            return width;
     603        }
     604        double height()
     605        {
     606            return width()/2;
     607        }
     608        BBox bbox = null;
     609        public BBox bbox()
     610        {
     611            if (bbox != null) {
     612                bbox.sanity();
     613                return bbox;
     614            }
     615            if (level == 0) {
     616                bbox = new BBox(-180, 90, 180, -90);
     617            } else {
     618                LatLon bottom_left = this.coor();
     619                double lat = bottom_left.lat() + this.height();
     620                double lon = bottom_left.lon() + this.width();
     621                LatLon top_right = new LatLon(lat, lon);
     622                bbox = new BBox(bottom_left, top_right);
     623            }
     624            bbox.sanity();
     625            return bbox;
     626        }
     627        /*
     628         * This gives the coordinate of the bottom-left
     629         * corner of the box
     630         */
     631        LatLon coor()
     632        {
     633            return QuadTiling.tile2LatLon(this.quad);
     634        }
     635    }
     636
     637    private QBLevel root;
     638    private QBLevel search_cache;
     639    public QuadBuckets()
     640    {
     641        clear();
     642    }
     643    public void clear()
     644    {
     645        root = new QBLevel(null);
     646        search_cache = null;
     647        if (debug)
     648            out("QuadBuckets() cleared: " + this);
     649    }
     650    public boolean add(Node n)
     651    {
     652        if (debug)
     653            out(this + " QuadBuckets() add: " + n + " size now: " + this.size());
     654        int before_size = -1;
     655        if (consistency_testing)
     656            before_size = root.size();
     657        boolean ret = root.add(n);
     658        if (consistency_testing) {
     659            int end_size = root.size();
     660            if (ret)
     661                end_size--;
     662            if (before_size != end_size)
     663                abort("size inconsistency before add: " + before_size + " after: " + end_size);
     664        }
     665        return ret;
     666    }
     667    public void unsupported()
     668    {
     669        out("unsupported operation");
     670        Object o = null;
     671        o.hashCode();
     672        throw new UnsupportedOperationException();
     673    }
     674    public boolean retainAll(Collection nodes)
     675    {
     676        unsupported();
     677        return false;
     678    }
     679    public boolean removeAll(Collection nodes)
     680    {
     681        unsupported();
     682        return false;
     683    }
     684    public boolean addAll(Collection nodes)
     685    {
     686        unsupported();
     687        return false;
     688    }
     689    public boolean containsAll(Collection nodes)
     690    {
     691        unsupported();
     692        return false;
     693    }
     694    private void check_type(Object o)
     695    {
     696        if (o instanceof Node)
     697            return;
     698        unsupported();
     699    }
     700    public boolean remove(Object o)
     701    {
     702        check_type(o);
     703        return this.remove((Node)o);
     704    }
     705    public boolean remove(Node n)
     706    {
     707        QBLevel bucket = root.find_exact(n);
     708        if (!bucket.isLeaf())
     709            abort("found branch where leaf expected");
     710        return bucket.content.remove(n);
     711    }
     712    public boolean contains(Object o)
     713    {
     714        check_type(o);
     715        QBLevel bucket = root.find_exact((Node)o);
     716        if (bucket == null)
     717            return false;
     718        return true;
     719    }
     720    public ArrayList<Node> toArrayList()
     721    {
     722        ArrayList<Node> a = new ArrayList<Node>();
     723        for (Node n : this)
     724            a.add(n);
     725        out("returning array list with size: " + a.size());
     726        return a;
     727    }
     728    public Object[] toArray()
     729    {
     730        return this.toArrayList().toArray();
     731    }
     732    public <T> T[] toArray(T[] template)
     733    {
     734        return this.toArrayList().toArray(template);
     735    }
     736    class QuadBucketIterator implements Iterator<Node>
     737    {
     738        QBLevel current_leaf;
     739        int index_in_leaf;
     740        int iterated_over;
     741        QBLevel next_leaf(QBLevel q)
     742        {
     743            if (q == null)
     744                return null;
     745            QBLevel orig = q;
     746            QBLevel next = q.nextLeaf();
     747            if (consistency_testing && (orig == next))
     748                abort("got same leaf back leaf: " + q.isLeaf());
     749            return next;
     750        }
     751        public QuadBucketIterator(QuadBuckets qb)
     752        {
     753            if (debug)
     754                out(this + " is a new iterator qb.root: " + qb.root + " size: " + qb.size());
     755            if (qb.root.isLeaf())
     756                current_leaf = qb.root;
     757            else
     758                current_leaf = next_leaf(qb.root);
     759            if (debug)
     760                out("\titerator first leaf: " + current_leaf);
     761            iterated_over = 0;
     762        }
     763        public boolean hasNext()
     764        {
     765            if (this.peek() == null) {
     766                if (debug)
     767                   out(this + " no hasNext(), but iterated over so far: " + iterated_over);
     768                return false;
     769            }
     770            return true;
     771        }
     772        Node peek()
     773        {
     774            if (current_leaf == null) {
     775                if (debug)
     776                    out("null current leaf, nowhere to go");
     777                return null;
     778            }
     779            while((current_leaf.content == null) ||
     780                  (index_in_leaf >= current_leaf.content.size())) {
     781                if (debug)
     782                    out("moving to next leaf");
     783                index_in_leaf = 0;
     784                current_leaf = next_leaf(current_leaf);
     785                if (current_leaf == null)
     786                    break;
     787            }
     788            if (current_leaf == null || current_leaf.content == null) {
     789                if (debug)
     790                    out("late nowhere to go " + current_leaf);
     791                return null;
     792            }
     793            return current_leaf.content.get(index_in_leaf);
     794        }
     795        public Node next()
     796        {
     797            Node ret = peek();
     798            index_in_leaf++;
     799            if (debug)
     800                out("iteration["+iterated_over+"] " + index_in_leaf + " leaf: " + current_leaf);
     801            iterated_over++;
     802            if (ret == null) {
     803                if (debug)
     804                    out(this + " no next node, but iterated over so far: " + iterated_over);
     805            }
     806            return ret;
     807        }
     808        public void remove()
     809        {
     810            Node object = peek();
     811            current_leaf.content.remove(object);
     812        }
     813    }
     814    public Iterator<Node> iterator()
     815    {
     816        return new QuadBucketIterator(this);
     817    }
     818    public int size()
     819    {
     820        // This can certainly by optimized
     821        int ret = root.size();
     822        if (debug)
     823            out(this + " QuadBuckets size: " + ret);
     824        return ret;
     825    }
     826    public boolean isEmpty()
     827    {
     828        if (this.size() == 0)
     829            return true;
     830        return false;
     831    }
     832    public BBox search_to_bbox(LatLon point, double radius)
     833    {
     834        BBox bbox = new BBox(point.lon() - radius, point.lat() - radius,
     835                             point.lon() + radius, point.lat() + radius);
     836        if (debug)
     837            out("search bbox before sanity: " +  bbox);
     838        bbox.sanity();
     839        if (debug)
     840            out("search bbox after sanity: " +  bbox);
     841        return bbox;
     842    }
     843    List<Node> search(LatLon point, double radius)
     844    {
     845        return this.search(search_to_bbox(point, radius), point, radius);
     846    }
     847    List<Node> search(BBox bbox)
     848    {
     849        return this.search(bbox, null, -1);
     850    }
     851    public List<Node> search(LatLon b1, LatLon b2)
     852    {
     853        BBox bbox = new BBox(b1.lon(), b1.lat(), b2.lon(), b2.lat());
     854        bbox.sanity();
     855        return this.search(bbox);
     856    }
     857    List<Node> search(BBox bbox, LatLon point, double radius)
     858    {
     859        if (debug) {
     860            out("qb root search at " + point + " around: " + radius);
     861            out("root bbox: " + root.bbox());
     862        }
     863        List<Node> ret = null;
     864        // Doing this cuts down search cost on a real-life data
     865        // set by about 25%
     866        boolean cache_searches = true;
     867        if (cache_searches) {
     868            if (search_cache == null)
     869                search_cache = root;
     870            // Walk back up the tree when the last
     871            // search spot can not cover the current
     872            // search
     873            while (!search_cache.bbox().bounds(bbox)) {
     874                search_cache = search_cache.parent;
     875            }
     876        } else {
     877            search_cache = root;
     878        }
     879        return search_cache.search(bbox, point, radius);
     880    }
     881}
  • new file core-dave/src/org/openstreetmap/josm/data/coor/QuadTiling.java

    diff -puN /dev/null src/org/openstreetmap/josm/data/coor/QuadTiling.java
    - +  
     1// License: GPL. Copyright 2009 by Dave Hansen, others
     2package org.openstreetmap.josm.data.coor;
     3
     4import org.openstreetmap.josm.data.osm.Node;
     5
     6import org.openstreetmap.josm.Main;
     7import org.openstreetmap.josm.data.projection.Projection;
     8
     9public class QuadTiling
     10{
     11    public static int NR_LEVELS = 24;
     12    public static double WORLD_PARTS = (1 << NR_LEVELS);
     13
     14    public static int MAX_OBJECTS_PER_LEVEL = 16;
     15    // has to be a power of 2
     16    public static int TILES_PER_LEVEL_SHIFT = 2;
     17    public static int TILES_PER_LEVEL = 1<<TILES_PER_LEVEL_SHIFT;
     18    static public int X_PARTS = 360;
     19    static public int X_BIAS = -180;
     20
     21    static public int Y_PARTS = 180;
     22    static public int Y_BIAS = -90;
     23
     24    public static LatLon tile2LatLon(long quad)
     25    {
     26        // The world is divided up into X_PARTS,Y_PARTS.
     27        // The question is how far we move for each bit
     28        // being set.  In the case of the top level, we
     29        // move half of the world.
     30        double x_unit = X_PARTS/2;
     31        double y_unit = Y_PARTS/2;
     32        long shift = (NR_LEVELS*2)-2;
     33
     34        //if (debug)
     35        //    out("tile2xy(0x"+Long.toHexString(quad)+")");
     36        double x = 0;
     37        double y = 0;
     38        for (int i = 0; i < NR_LEVELS; i++) {
     39            long bits = (quad >> shift) & 0x3;
     40            //if (debug)
     41            //    out("shift: " + shift + " bits: " + bits);
     42            // remember x is the MSB
     43            if ((bits & 0x2) != 0)
     44                x += x_unit;
     45            if ((bits & 0x1) != 0)
     46                y += y_unit;
     47            x_unit /= 2;
     48            y_unit /= 2;
     49            shift -= 2;
     50        }
     51        x += X_BIAS;
     52        y += Y_BIAS;
     53        return new LatLon(y, x);
     54    }
     55    static long xy2tile(long x, long y)
     56    {
     57       long tile = 0;
     58       int i;
     59       for (i = NR_LEVELS-1; i >= 0; i--)
     60       {
     61            long xbit = ((x >> i) & 1);
     62            long ybit = ((y >> i) & 1);
     63            tile <<= 2;
     64            // Note that x is the MSB
     65            tile |= (xbit<<1) | ybit;
     66       }
     67       return tile;
     68    }
     69    static long coorToTile(LatLon coor)
     70    {
     71        return quadTile(coor);
     72    }
     73    static long lon2x(double lon)
     74    {
     75       //return Math.round((lon + 180.0) * QuadBuckets.WORLD_PARTS / 360.0)-1;
     76       long ret = (long)Math.floor((lon + 180.0) * WORLD_PARTS / 360.0);
     77       if (ret == WORLD_PARTS)
     78           ret--;
     79       return ret;
     80    }
     81    static long lat2y(double lat)
     82    {
     83       //return Math.round((lat + 90.0) * QuadBuckets.WORLD_PARTS / 180.0)-1;
     84       long ret = (long)Math.floor((lat + 90.0) * WORLD_PARTS / 180.0);
     85       if (ret == WORLD_PARTS)
     86           ret--;
     87       return ret;
     88    }
     89    static public long quadTile(LatLon coor)
     90    {
     91        return xy2tile(lon2x(coor.lon()),
     92                       lat2y(coor.lat()));
     93    }
     94    static public int index(int level, long quad)
     95    {
     96        long mask = 0x00000003;
     97        int total_shift = TILES_PER_LEVEL_SHIFT*(NR_LEVELS-level-1);
     98        return (int)(mask & (quad >> total_shift));
     99    }
     100    static public int index(Node n, int level)
     101    {
     102        LatLon coor = n.getCoor();
     103        // The nodes that don't return coordinates will all get
     104        // stuck in a single tile.  Hopefully there are not too
     105        // many of them
     106        if (coor == null)
     107            return 0;
     108        long quad = coorToTile(n.getCoor());
     109        return index(level, quad);
     110    }
     111}