/* * 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>>(); } }