Subversion Repositories mkgmap

Rev

Rev 3081 | View as "text/plain" | Blame | Compare with Previous | Last modification | View Log | RSS feed

/*
 * Copyright (C) 2006, 2012.
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License version 3 or
 * version 2 as published by the Free Software Foundation.
 *
 * This program is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * General Public License for more details.
 */

package uk.me.parabola.util;

import java.awt.Rectangle;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;

import uk.me.parabola.imgfmt.app.Area;
import uk.me.parabola.imgfmt.app.Coord;
import uk.me.parabola.log.Logger;
import uk.me.parabola.mkgmap.reader.osm.Element;
import uk.me.parabola.mkgmap.reader.osm.Node;
import uk.me.parabola.mkgmap.reader.osm.Way;

public final class ElementQuadTreeNode {

        private static final Logger log = Logger.getLogger(ElementQuadTreeNode.class);

        /**
         * A static empty list used for node objects. They have one coord only and
         * it is too costly to create a list for each node
         */

        private static final List<Coord> EMPTY_LIST = Collections.emptyList();
       
        /** The maximum number of coords in the quadtree node. */
        private static final int MAX_POINTS = 1000;

        /** Maps elements to its coords located in this quadtree node. */
        private Map<Element, List<Coord>>  elementMap;

        /** The bounds of this quadtree node */
        private final Area bounds;
        private final Rectangle boundsRect;

        /** Flag if this node and all subnodes are empty */
        private Boolean empty;

        /** The subnodes in case this node is not a leaf */
        private ElementQuadTreeNode[] children;

        public static final class ElementQuadTreePolygon {
                private final java.awt.geom.Area javaArea;
                private final Area bbox;

                public ElementQuadTreePolygon(java.awt.geom.Area javaArea) {
                        this.javaArea = javaArea;
                        Rectangle bboxRect = javaArea.getBounds();
                        bbox = new Area(bboxRect.y, bboxRect.x, bboxRect.y
                                        + bboxRect.height, bboxRect.x + bboxRect.width);
                }

                public ElementQuadTreePolygon(List<Coord> points) {
                        this(Java2DConverter.createArea(points));
                }

                public ElementQuadTreePolygon(Collection<List<Coord>> polygonList) {
                        this.javaArea = new java.awt.geom.Area();
                        for (List<Coord> polygon : polygonList) {
                                javaArea.add(Java2DConverter.createArea(polygon));
                        }
                        Rectangle bboxRect = javaArea.getBounds();
                        bbox = new Area(bboxRect.y, bboxRect.x, bboxRect.y
                                        + bboxRect.height, bboxRect.x + bboxRect.width);
                }

                public Area getBbox() {
                        return bbox;
                }

                public java.awt.geom.Area getArea() {
                        return javaArea;
                }
        }

        /**
         * Retrieves if this quadtree node (and all subnodes) contains any elements.
         * @return <code>true</code> this quadtree node does not contain any elements; <code>false</code> else
         */

        public boolean isEmpty() {
                if (empty == null) {
                        if (isLeaf()) {
                                empty = elementMap.isEmpty();
                        } else {
                                empty = true;
                                for (ElementQuadTreeNode child : children) {
                                        if (child.isEmpty()==false) {
                                                empty = false;
                                                break;
                                        }
                                }
                        }
                }
                return empty;
        }
       
       
        /**
         * Retrieves the number of coords hold by this quadtree node and all subnodes.
         * @return the number of coords
         */

        public long getSize() {
                if (isLeaf()) {
                        int items = 0;
                        for (List<Coord> points : elementMap.values()) {
                                if (points == EMPTY_LIST) {
                                        items++;
                                } else {
                                        items += points.size();
                                }
                        }
                        return items;
                } else {
                        int items = 0;
                        for (ElementQuadTreeNode child : children) {
                                        items += child.getSize();
                        }
                        return items;
                }
        }

        /**
         * Retrieves the depth of this quadtree node. Leaves have depth 1.
         * @return the depth of this quadtree node
         */

        public int getDepth() {
                if (isLeaf()) {
                        return 1;
                } else {
                        int maxDepth = 0;
                        for (ElementQuadTreeNode node : children) {
                                maxDepth = Math.max(node.getDepth(), maxDepth);
                        }
                        return maxDepth + 1;
                }
        }

        private ElementQuadTreeNode(Area bounds, Map<Element, List<Coord>> elements) {
                this.bounds = bounds;
                boundsRect = new Rectangle(bounds.getMinLong(), bounds.getMinLat(),
                                bounds.getWidth(), bounds.getHeight());
                this.children = null;
                elementMap =elements;
                empty = elementMap.isEmpty();
               
                checkSplit();          
        }
       
       
        public ElementQuadTreeNode(Area bounds, Collection<Element> elements) {
                this.bounds = bounds;
                boundsRect = new Rectangle(bounds.getMinLong(), bounds.getMinLat(),
                                        bounds.getWidth(), bounds.getHeight());
                this.children = null;

                this.elementMap = new HashMap<Element, List<Coord>>(elements.size()*4/3+10);
               
                for (Element el : elements) {
                        if (el instanceof Way) {
                                List<Coord> points = ((Way) el).getPoints();
                                // no need to create a copy of the points because the list is never changed
                                elementMap.put(el, points);
                        } else if (el instanceof Node) {
                                elementMap.put(el, EMPTY_LIST);
                        }
                }
                empty = elementMap.isEmpty();
                checkSplit();
        }

        public Area getBounds() {
                return this.bounds;
        }

        public Rectangle getBoundsAsRectangle() {
                return boundsRect;
        }

        /**
         * Checks if this quadtree node exceeds the maximum size and splits it in such a case.
         */

        private void checkSplit() {
                if (getSize() > MAX_POINTS) {
                        split();
                }
        }

        /**
         * Removes the element with the given bounding box from this quadtree node and all subnodes.
         * @param elem the element to be removed
         * @param bbox the bounding box of the element
         */

        private void remove(Element elem, Area bbox) {
                if (bbox == null || isEmpty()) {
                        return;
                }
                if (isLeaf()) {
                        elementMap.remove(elem);
                        empty = elementMap.isEmpty();
                } else {
                        for (ElementQuadTreeNode child : children) {
                                if (child.getBounds().intersects(bbox)) {
                                        child.remove(elem, bbox);
                                        if (child.isEmpty()) {
                                                // update the empty flag
                                                empty = null;
                                        }
                                }
                        }
                }
        }

        /**
         * Calculates the bounding box of the given element.
         * @param elem an element
         * @return the bounding box of the element
         */

        private Area getBbox(Element elem) {
                if (elem instanceof Node) {
                        Coord c = ((Node) elem).getLocation();
                        return new Area(c.getLatitude(), c.getLongitude(), c.getLatitude(),c.getLongitude());
                } else if (elem instanceof Way) {
                        List<Coord> points = ((Way) elem).getPoints();
                        if (points.isEmpty()) {
                                return null;
                        }
                        Coord c = points.get(0);
                        int minLat = c.getLatitude();
                        int maxLat = c.getLatitude();
                        int minLong = c.getLongitude();
                        int maxLong = c.getLongitude();
                        for (Coord co : points) {
                                if (co.getLatitude() < minLat) {
                                        minLat = co.getLatitude();
                                } else if (co.getLatitude() > maxLat) {
                                        maxLat = co.getLatitude();
                                }
                                if (co.getLongitude() < minLong) {
                                        minLong = co.getLongitude();
                                } else if (co.getLongitude() > maxLong) {
                                        maxLong = co.getLongitude();
                                }
                        }
                        return new Area(minLat,minLong, maxLat, maxLong);
                }
                return null;
        }
       
        /**
         * Removes the element from this quadtree node and all subnodes.
         * @param elem the element to be removed
         */

        public void remove(Element elem) {
                remove(elem, getBbox(elem));
        }

        /**
         * Retrieves all elements that intersects the given bounding box.
         * @param bbox the bounding box
         * @param resultList results are stored in this collection
         * @return the resultList
         */

        public Set<Element> get(Area bbox, Set<Element> resultList) {
                if (isEmpty()) {
                        return resultList;
                }
                if (isLeaf()) {
                        if (bbox.getMinLat() <= bounds.getMinLat()
                                        && bbox.getMaxLat() >= bounds.getMaxLat()
                                        && bbox.getMinLong() <= bounds.getMinLong()
                                        && bbox.getMaxLong() >= bounds.getMaxLong()) {

                                // the bounding box is contained completely in the bbox
                                // => add all points without further check
                                resultList.addAll(elementMap.keySet());
                        } else {
                                // check each point
                                for (Entry<Element, List<Coord>> elem : elementMap.entrySet()) {
                                        if (elem.getKey() instanceof Node) {
                                                Node n = (Node) elem.getKey();
                                                if (bbox.contains(n.getLocation())) {
                                                        resultList.add(n);
                                                }
                                        } else if (elem.getKey() instanceof Way) {
                                                // no need to check - the element is already in the result list
                                                if (resultList.contains(elem.getKey())) {
                                                        continue;
                                                }
                                                for (Coord c : elem.getValue()) {
                                                        if (bbox.contains(c)) {
                                                                resultList.add(elem.getKey());
                                                                break;
                                                        }
                                                }
                                        }
                                }
                        }
                } else {
                        for (ElementQuadTreeNode child : children) {
                                if (child.isEmpty() == false
                                                && bbox.intersects(child.getBounds())) {
                                        resultList = child.get(bbox, resultList);
                                }
                        }
                }
                return resultList;
        }

        /**
         * Retrieves all elements that intersects the given polygon.
         * @param polygon the polygon
         * @param resultList results are stored in this collection
         * @return the resultList
         */

        public Set<Element> get(ElementQuadTreePolygon polygon,
                        Set<Element> resultList) {
                if (isEmpty()) {
                        return resultList;
                }
                if (polygon.getBbox().intersects(getBounds())) {
                        if (isLeaf()) {
                                for (Entry<Element, List<Coord>> elem : elementMap.entrySet()) {
                                        if (resultList.contains(elem.getKey())) {
                                                continue;
                                        }
                                        if (elem.getKey() instanceof Node) {
                                                Node n = (Node)elem.getKey();
                                                Coord c = n.getLocation();
                                                if (polygon.getArea().contains(c.getLongitude(),
                                                                c.getLatitude())) {
                                                        resultList.add(n);
                                                }
                                        } else if (elem.getKey() instanceof Way) {
                                                for (Coord c : elem.getValue()) {
                                                        if (polygon.getArea().contains(c.getLongitude(),
                                                                        c.getLatitude())) {
                                                                resultList.add(elem.getKey());
                                                                break;
                                                        }
                                                }
                                        }
                                }
                        } else {
                                for (ElementQuadTreeNode child : children) {
                                        if (child.isEmpty()==false
                                                        && polygon.getArea().intersects(
                                                                        child.getBoundsAsRectangle())) {
                                                java.awt.geom.Area subArea = (java.awt.geom.Area) polygon
                                                                .getArea().clone();

                                                subArea.intersect(Java2DConverter.createBoundsArea(new Area(child.getBounds()
                                                                .getMinLat() - 1, child.getBounds()
                                                                .getMinLong() - 1, child.getBounds()
                                                                .getMaxLat() + 1, child.getBounds()
                                                                .getMaxLong() + 1))
                                                                );
                                                child.get(new ElementQuadTreePolygon(subArea),
                                                                        resultList);
                                        }
                                }
                        }
                }
                return resultList;

        }

        /**
         * Retrieves if this quadtree node is a leaf.
         * @return <code>true</code> this node is a leaf
         */

        public boolean isLeaf() {
                return elementMap != null;
        }

        /**
         * Splits this quadtree node into 4 subnodes.
         */

        private void split() {
                if (bounds.getHeight() <= 5 || bounds.getWidth() <= 5) {
                        log.error("Do not split more due to too small bounds: " + bounds);
                        return;
                }

                int halfLat = (bounds.getMinLat() + bounds.getMaxLat()) / 2;
                int halfLong = (bounds.getMinLong() + bounds.getMaxLong()) / 2;
                children = new ElementQuadTreeNode[4];
                Area[] childBounds = new Area[4];
               
                childBounds[0] = new Area(bounds.getMinLat(), bounds.getMinLong(),
                                halfLat, halfLong);
                childBounds[1] = new Area(halfLat, bounds.getMinLong(),
                                bounds.getMaxLat(), halfLong);
                childBounds[2] = new Area(bounds.getMinLat(), halfLong, halfLat,
                                bounds.getMaxLong());
                childBounds[3] = new Area(halfLat, halfLong, bounds.getMaxLat(),
                                bounds.getMaxLong());

                List<Map<Element, List<Coord>>> childElems = new ArrayList<Map<Element, List<Coord>>>(4);
                for (int i = 0; i < 4; i++) {
                        childElems.add(new HashMap<Element, List<Coord>>());
                }
                for (Entry<Element,List<Coord>> elem : elementMap.entrySet()) {
                        if (elem.getKey() instanceof Node) {
                                Node node = (Node) elem.getKey();
                                for (int i = 0; i < childBounds.length; i++) {
                                        if (childBounds[i].contains(node.getLocation())) {
                                                childElems.get(i).put(node, EMPTY_LIST);
                                                break;
                                        }
                                }
                        } else if (elem.getKey() instanceof Way) {
                                List<List<Coord>> points = new ArrayList<List<Coord>>(4);
                                for (int i = 0; i < 4; i++) {
                                        // usually ways are quite local
                                        // therefore there is a high probability that only one child is covered
                                        // dim the new list as the old list
                                        points.add(new ArrayList<Coord>(elem.getValue().size()));
                                }
                                for (Coord c : elem.getValue()) {
                                        for (int i = 0; i < childBounds.length; i++) {
                                                if (childBounds[i].contains(c)) {
                                                        points.get(i).add(c);
                                                        break;
                                                }
                                        }                              
                                }
                                for (int i = 0; i< 4; i++) {
                                        if (points.get(i).isEmpty()==false) {
                                                childElems.get(i).put(elem.getKey(), points.get(i));
                                        }
                                }
                        }
                }
               
                for (int i = 0; i < 4; i++) {
                        children[i] = new ElementQuadTreeNode(childBounds[i], childElems.get(i));
                }
               
                elementMap = null;
        }

        public void clear() {
                this.children = null;
                elementMap = new HashMap<Element, List<Coord>>();
        }
}