All Downloads are FREE. Search and download functionalities are using the official Maven repository.

gov.nasa.worldwind.util.BasicQuadTree Maven / Gradle / Ivy

The newest version!
/*
 * Copyright (C) 2012 United States Government as represented by the Administrator of the
 * National Aeronautics and Space Administration.
 * All Rights Reserved.
 */

package gov.nasa.worldwind.util;

import gov.nasa.worldwind.geom.*;
import gov.nasa.worldwind.terrain.*;

import java.util.*;

/**
 * Implements a quadtree backed by a bit-set index. A bit-set provides a minimal-memory index. Each bit identifies one
 * cell in the quadtree.
 * 

* This class provides methods to add and remove items from the quadtree, and to determine the items intersecting * specified regions. *

* Items can be added with an associated name, and can be retrieved and removed by name. * * @author tag * @version $Id: BasicQuadTree.java 1938 2014-04-15 22:34:52Z tgaskins $ */ public class BasicQuadTree extends BitSetQuadTreeFilter implements Iterable { protected ArrayList levelZeroCells; protected Map> items; // the tree's list of items protected T currentItem; // used during add() to pass the added item to doOperation(). protected String currentName; // used during add() to pass the optional name of the added item to doOperation(). protected HashMap nameMap = new HashMap(); // maps names to items protected boolean allowDuplicates = true; /** * Constructs a quadtree of a specified level and spanning a specified region. *

* The number of levels in the quadtree must be specified to the constructor. The more levels there are the more * discriminating searches will be, but at the cost of some performance because more cells are searched. For the * Earth, a level count of 8 provides leaf cells about 75 km along their meridian edges (edges of constant Earth, a * level count of 8 provides leaf cells about 75 km along their meridian edges (edges of constant longitude). * Additional levels successfully halve the distance, fewer levels double that distance. * * @param numLevels the number of levels in the quadtree. The more levels there are the more discriminating searches * will be, but at the cost of some performance. * @param sector the region the tree spans. * @param itemMap a {@link Map} to hold the items added to the quadtree. May be null, in which case a new map is * created. * * @throws IllegalArgumentException if numLevels is less than 1. */ public BasicQuadTree(int numLevels, Sector sector, Map> itemMap) { super(numLevels, null); if (sector == null) { String message = Logging.getMessage("nullValue.SectorIsNull"); Logging.logger().severe(message); throw new IllegalArgumentException(message); } this.makeLevelZeroCells(sector); this.items = itemMap != null ? itemMap : new HashMap>(); } /** * Constructs a quadtree of a specified level and spanning a specified region. *

* The number of levels in the quadtree must be specified to the constructor. The more levels there are the more * discriminating searches will be, but at the cost of some performance because more cells are searched. For the * Earth, a level count of 8 provides leaf cells about 75 km along their meridian edges (edges of constant Earth, a * level count of 8 provides leaf cells about 75 km along their meridian edges (edges of constant longitude). * Additional levels successfully halve the distance, fewer levels double that distance. * * @param numLevels the number of levels in the quadtree. The more levels there are the more discriminating * searches will be, but at the cost of some performance. * @param sector the region the tree spans. * @param itemMap a {@link Map} to hold the items added to the quadtree. May be null, in which case a new * map is created. * @param allowDuplicates Indicates whether the collection held by this quadtree may contain duplicate entries. * Specifying true, which is the default, may cause an individual item to be * associated with multiple quadtree regions if the item's coordinates fall on a region * boundary. In this case that item will be returned multiple times from an iterator created * by this class. Specifying false prevents this. * * @throws IllegalArgumentException if numLevels is less than 1. */ public BasicQuadTree(int numLevels, Sector sector, Map> itemMap, boolean allowDuplicates) { this(numLevels, sector, itemMap); if (sector == null) { String message = Logging.getMessage("nullValue.SectorIsNull"); Logging.logger().severe(message); throw new IllegalArgumentException(message); } this.allowDuplicates = allowDuplicates; this.makeLevelZeroCells(sector); this.items = itemMap != null ? itemMap : new HashMap>(); } /** * Creates the quadtree's level-zero cells. * * @param sector the sector to subdivide to create the cells. */ protected void makeLevelZeroCells(Sector sector) { Sector[] subSectors = sector.subdivide(); this.levelZeroCells = new ArrayList(4); this.levelZeroCells.add(subSectors[0].asDegreesArray()); this.levelZeroCells.add(subSectors[1].asDegreesArray()); this.levelZeroCells.add(subSectors[3].asDegreesArray()); this.levelZeroCells.add(subSectors[2].asDegreesArray()); } /** * Indicates whether the tree contains any items. * * @return true if the tree contains items, otherwise false. */ synchronized public boolean hasItems() { return !this.items.isEmpty(); } /** * Indicates whether an item is contained in the tree. * * @param item the item to check. If null, false is returned. * * @return true if the item is in the tree, otherwise false. */ synchronized public boolean contains(T item) { if (item == null) return false; for (Map.Entry> entry : this.items.entrySet()) { List itemList = entry.getValue(); if (itemList == null) continue; if (itemList.contains(item)) return true; } return false; } /** * Add a named item to the quadtree. Any item duplicates are duplicated in the tree. Any name duplicates replace the * current name association; the name then refers to the item added. * * @param item the item to add. * @param itemCoords an array specifying the region or location of the item. If the array's length is 2 it * represents a location in [latitude, longitude]. If its length is 4 it represents a region, with * the same layout as the nodeRegion argument. * @param itemName the item name. If null, the item is added without a name. * * @throws IllegalArgumentException if either item or itemCoords is null. */ synchronized public void add(T item, double[] itemCoords, String itemName) { this.addItem(item, itemCoords, itemName); } /** * Add an item to the quadtree. Any duplicates are duplicated in the tree. * * @param item the item to add. * @param itemCoords an array specifying the region or location of the item. If the array's length is 2 it * represents a location in [latitude, longitude]. If its length is 4 it represents a region, with * the same layout as the nodeRegion argument. * * @throws IllegalArgumentException if either item or itemCoords is null. */ synchronized public void add(T item, double[] itemCoords) { this.addItem(item, itemCoords, null); } protected void addItem(T item, double[] itemCoords, String name) { if (item == null) { String message = Logging.getMessage("nullValue.ItemIsNull"); Logging.logger().severe(message); throw new IllegalArgumentException(message); } if (itemCoords == null) { String message = Logging.getMessage("nullValue.CoordinatesAreNull"); Logging.logger().severe(message); throw new IllegalArgumentException(message); } this.currentItem = item; this.currentName = name; this.start(); for (int i = 0; i < levelZeroCells.size(); i++) { this.testAndDo(0, i, levelZeroCells.get(i), itemCoords); } } /** * Removes an item from the tree. *

* Note: For large collections, this can be an expensive operation. * * @param item the item to remove. If null, no item is removed. */ synchronized public void remove(T item) { if (item == null) return; List bitsToClear = new ArrayList(); for (Map.Entry> entry : this.items.entrySet()) { List itemList = entry.getValue(); if (itemList == null) continue; if (itemList.contains(item)) itemList.remove(item); if (itemList.size() == 0) bitsToClear.add(entry.getKey()); } for (String bitNum : bitsToClear) { this.bits.clear(Integer.parseInt(bitNum)); } } /** * Removes an item from the tree by name. *

* Note: For large collections, this can be an expensive operation. * * @param name the name of the item to remove. If null, no item is removed. */ synchronized public void removeByName(String name) { T item = this.getByName(name); this.nameMap.remove(name); if (item == null) return; for (Map.Entry> entry : this.items.entrySet()) { List itemList = entry.getValue(); if (itemList == null) continue; if (itemList.contains(item)) itemList.remove(item); } } /** Removes all items from the tree. */ synchronized public void clear() { this.items.clear(); this.bits.clear(); } /** * Returns a named item. * * @param name the item name. If null, null is returned. * * @return the named item, or null if the item is not in the tree or the specified name is null. */ synchronized public T getByName(String name) { return name != null ? this.nameMap.get(name) : null; } /** * Returns an iterator over the items in the tree. There is no specific iteration order and the iterator may return * duplicate entries. *

* Note The {@link java.util.Iterator#remove()} operation is not supported. * * @return an iterator over the items in the tree. */ synchronized public Iterator iterator() { return new Iterator() { // The items are stored in lists associated with each cell (each bit of the bit-set), so two internal // iterators are needed: one for the map of populated cells and one for a cell's list of items. private Iterator> mapIterator; private Iterator listIterator; private T nextItem; { // constructor mapIterator = BasicQuadTree.this.items.values().iterator(); } /** {@inheritDoc} **/ public boolean hasNext() { // This is the only method that causes the list to increment, so call it before every call to next(). if (this.nextItem != null) return true; this.moveToNextItem(); return this.nextItem != null; } /** {@inheritDoc} **/ public T next() { if (!this.hasNext()) throw new NoSuchElementException("Iteration has no more elements."); T lastNext = this.nextItem; this.nextItem = null; return lastNext; } /** * This operation is not supported and will produce a {@link UnsupportedOperationException} if invoked. */ public void remove() { throw new UnsupportedOperationException("The remove() operations is not supported by this Iterator."); } private void moveToNextItem() { // Use the next item in a cell's item list, if there is an item list and it has a next item. if (this.listIterator != null && this.listIterator.hasNext()) { this.nextItem = this.listIterator.next(); return; } // Find the next map entry with a non-null item list. Use the first item in that list. this.listIterator = null; while (this.mapIterator.hasNext()) { if (this.mapIterator.hasNext()) this.listIterator = this.mapIterator.next().iterator(); if (this.listIterator != null && this.listIterator.hasNext()) { this.nextItem = this.listIterator.next(); return; } } } }; } /** * Finds and returns the items within a tree cell containing a specified location. * * @param location the location of interest. * @param outItems a {@link Set} in which to place the items. If null, a new set is created. * * @return the set of intersecting items. The same set passed as the outItems argument is returned, or * a new set if that argument is null. * * @throws IllegalArgumentException if location is null. */ synchronized public Set getItemsAtLocation(LatLon location, Set outItems) { if (location == null) { String message = Logging.getMessage("nullValue.LatLonIsNull"); Logging.logger().severe(message); throw new IllegalArgumentException(message); } FindIntersectingBitsOp op = new FindIntersectingBitsOp(this); List bitIds = op.getOnBits(this.levelZeroCells, location.asDegreesArray(), new ArrayList()); return this.buildItemSet(bitIds, outItems); } /** * Finds and returns the items within tree cells containing specified locations. * * @param locations the locations of interest. * @param outItems a {@link Set} in which to place the items. If null, a new set is created. * * @return the set of intersecting items. The same set passed as the outItems argument is returned, or * a new set if that argument is null. * * @throws IllegalArgumentException if locations is null. */ synchronized public Set getItemsAtLocation(Iterable locations, Set outItems) { if (locations == null) { String message = Logging.getMessage("nullValue.LatLonListIsNull"); Logging.logger().severe(message); throw new IllegalArgumentException(message); } FindIntersectingBitsOp op = new FindIntersectingBitsOp(this); List bitIds = new ArrayList(); for (LatLon location : locations) { if (location != null) bitIds = op.getOnBits(this.levelZeroCells, location.asDegreesArray(), bitIds); } return this.buildItemSet(bitIds, outItems); } /** * Finds and returns the items intersecting a specified sector. * * @param testSector the sector of interest. * @param outItems a {@link Set} in which to place the items. If null, a new set is created. * * @return the set of intersecting items. The same set passed as the outItems argument is returned, or * a new set if that argument is null. * * @throws IllegalArgumentException if testSector is null. */ synchronized public Set getItemsInRegion(Sector testSector, Set outItems) { if (testSector == null) { String message = Logging.getMessage("nullValue.SectorIsNull"); Logging.logger().severe(message); throw new IllegalArgumentException(message); } FindIntersectingBitsOp op = new FindIntersectingBitsOp(this); List bitIds = op.getOnBits(this.levelZeroCells, testSector, new ArrayList()); return this.buildItemSet(bitIds, outItems); } /** * Finds and returns the items intersecting a specified collection of sectors. * * @param testSectors the sectors of interest. * @param outItems a {@link Set} in which to place the items. If null, a new set is created. * * @return the set of intersecting items. The same set passed as the outItems argument is returned, or * a new set if that argument is null. * * @throws IllegalArgumentException if testSectors is null. */ public Set getItemsInRegions(Iterable testSectors, Set outItems) { if (testSectors == null) { String message = Logging.getMessage("nullValue.SectorListIsNull"); Logging.logger().severe(message); throw new IllegalArgumentException(message); } FindIntersectingBitsOp op = new FindIntersectingBitsOp(this); List bitIds = new ArrayList(); for (Sector testSector : testSectors) { if (testSector != null) bitIds = op.getOnBits(this.levelZeroCells, testSector, bitIds); } return this.buildItemSet(bitIds, outItems); } /** * Finds and returns the items intersecting a specified collection of {@link gov.nasa.worldwind.terrain.SectorGeometry}. * This method is a convenience for finding the items intersecting the current visible regions. * * @param geometryList the list of sector geometry. * @param outItems a {@link Set} in which to place the items. If null, a new set is created. * * @return the set of intersecting items. The same set passed as the outItems argument is returned, or * a new set if that argument is null. * * @throws IllegalArgumentException if geometryList is null. */ synchronized public Set getItemsInRegions(SectorGeometryList geometryList, Set outItems) { if (geometryList == null) { String message = Logging.getMessage("nullValue.SectorGeometryListIsNull"); Logging.logger().severe(message); throw new IllegalArgumentException(message); } FindIntersectingBitsOp op = new FindIntersectingBitsOp(this); List bitIds = new ArrayList(); for (SectorGeometry testSector : geometryList) { if (testSector != null) bitIds = op.getOnBits(this.levelZeroCells, testSector.getSector(), bitIds); } return this.buildItemSet(bitIds, outItems); } /** * Adds the items identified by a list of bit IDs to the set returned by the get methods. * * @param bitIds the bit numbers of the cells containing the items to return. * @param outItems a {@link Set} in which to place the items. If null, a new set is created. * * @return the set of items. The value passed as the outItems is returned. */ protected Set buildItemSet(List bitIds, Set outItems) { if (outItems == null) outItems = new HashSet(); if (bitIds == null) return outItems; for (Integer id : bitIds) { List regionItems = this.items.get(id.toString()); if (regionItems == null) continue; for (T item : regionItems) { outItems.add(item); } } return outItems; } /** * Performs the add operation of the quadtree. * * @param level the quadtree level currently being traversed. * @param position the position of the cell in its parent cell, either 0, 1, 2, or 3. Cell positions starts with 0 * at the southwest corner of the parent cell and increment counter-clockwise: cell 1 is SE, cell * 2 is NE and cell 3 is NW. * @param cellRegion an array specifying the coordinates of the cell's region. The first two entries are the minimum * and maximum values on the Y axis (typically latitude). The last two entries are the minimum and * maximum values on the X axis, (typically longitude). * @param itemCoords an array specifying the region or location of the intersecting item. If the array's length is 2 * it represents a location in [latitude, longitude]. If its length is 4 it represents a region, * with the same layout as the nodeRegion argument. * * @return true if traversal should continue to the cell's descendants, false if traversal should not continue to * the cell's descendants. */ protected boolean doOperation(int level, int position, double[] cellRegion, double[] itemCoords) { int bitNum = this.computeBitPosition(level, position); this.bits.set(bitNum); if (level < this.maxLevel) return true; String bitName = Integer.toString(bitNum); List regionItems = this.items.get(bitName); if (regionItems == null) { regionItems = new ArrayList(); this.items.put(bitName, regionItems); } regionItems.add(this.currentItem); if (this.currentName != null) this.nameMap.put(this.currentName, this.currentItem); if (!this.allowDuplicates) this.stop(); return false; } ////////////////////// ONLY TEST CODE BELOW //////////////////////////////////////////// // public static void main(String[] args) // { //// basicTest(); //// iteratorTest(); //// perfTestVisit(); // perfTestFind(); // } // // public abstract static class PerfTestRunner // { // protected abstract void doOp(); // // protected int numIterations; // // protected long run(int numIterations) // { // this.numIterations = numIterations; // long start = System.currentTimeMillis(); // // for (int i = 0; i < numIterations; i++) // { // this.doOp(); // } // // return System.currentTimeMillis() - start; // } // // public void print(long elapsedTime) // { // System.out.printf("Elapsed time for %d iterations = %d milliseconds\n", this.numIterations, elapsedTime); // } // } // // protected static void basicTest() // { // final int treeDepth = 8; // // HashMap> map = new HashMap>(); // final BasicQuadTree tree = new BasicQuadTree(treeDepth, Sector.FULL_SPHERE, map); // // LatLon ll = LatLon.fromDegrees(0, 0); // tree.add(ll.toString(), ll.asDegreesArray()); // // ll = LatLon.fromDegrees(40, 50); // tree.add(ll.toString(), ll.asDegreesArray()); // // ll = LatLon.fromDegrees(-60, -80); // tree.add(ll.toString(), ll.asDegreesArray()); // // Set items = tree.getItemsInRegion(Sector.fromDegrees(0, 45, 0, 55), new HashSet()); // // System.out.printf("Tree depth %d, %d items found\n", treeDepth, items.size()); // // for (String item : items) // { // System.out.println(item); // } // } // // protected static void perfTestVisit() // { // VisitOp filter = new VisitOp(10, Sector.FULL_SPHERE); // // long start = System.currentTimeMillis(); // for (int i = 0; i < 10; i++) // { // filter.visit(Sector.FULL_SPHERE); // } // long end = System.currentTimeMillis(); // // System.out.println( // "DONE " + filter.bits.cardinality() + " bits set in " + (end - start) / 10 + " milliseconds"); // } // // // protected static void perfTestFind() // { // final int treeDepth = 8; // // Map> map = new HashMap>(); // final BasicQuadTree tree = new BasicQuadTree(treeDepth, Sector.FULL_SPHERE, map); // // LatLon ll = LatLon.fromDegrees(0, 0); // tree.add(ll.toString(), ll.asDegreesArray()); // // ll = LatLon.fromDegrees(40, 50); // tree.add(ll.toString(), ll.asDegreesArray()); // // ll = LatLon.fromDegrees(-60, -80); // tree.add(ll.toString(), ll.asDegreesArray()); // // int count = 0; // for (double lat = -45; lat <= 45; lat += 0.25) // { // for (double lon = 6; lon <= 100; lon += 0.25) // { // ll = LatLon.fromDegrees(lat, lon); // tree.add(ll.toString(), ll.asDegreesArray()); // ++count; //// if (lat >= 1 && lat <= 5 && lon >= 1 && lon <= 5) //// System.out.println(ll); // } // } // // final int finalCount = count; // PerfTestRunner runner = new PerfTestRunner() // { // public Set items; // // protected void doOp() // { // this.items = tree.getItemsInRegion(Sector.fromDegrees(-22, 22, 6, 54), new HashSet()); // } // // @Override // public void print(long elapsedMillis) // { // System.out.printf("Tree depth %d, %d locations in tree, %d items found\n", // treeDepth, finalCount, items.size()); // super.print(elapsedMillis); // //// for (String item : this.items) //// { //// System.out.println(item); //// } // } // }; // System.out.println("STARTING"); // runner.print(runner.run(100)); // } // // protected static void iteratorTest() // { // final int treeDepth = 8; // // HashMap> map = new HashMap>(); // final BasicQuadTree tree = new BasicQuadTree(treeDepth, Sector.FULL_SPHERE, map); // // int count = 0; // for (double lat = -5; lat <= 5; lat += 1) // { // for (double lon = 5; lon <= 10; lon += 1) // { // LatLon ll = LatLon.fromDegrees(lat, lon); // tree.add(ll.toString(), ll.asDegreesArray()); // ++count; // } // } // // int actualCount = 0; // for (String s : tree) // { // ++actualCount; // System.out.println(s); // } // // System.out.printf("Actual count (%d) == Total count (%d): %b\n", actualCount, count, (actualCount == count)); // } // // /** // * A test class that prints the set bits in the quadtree. // * @param the item type. // */ // protected static class PrintBitsOp extends BasicQuadTree // { // public PrintBitsOp(int maxLevel) // { // super(maxLevel, Sector.FULL_SPHERE, null); // } // // protected boolean doOperation(int level, int position, double[] nodeRegion, double[] testRegion) // { // int bitNum = this.computeBitPosition(level, position); // // this.bits.set(bitNum); // // System.out.printf("Level %d, %s, Position %d\n", level, this.makePathString(this.path, level), bitNum); // // return level < this.maxLevel; // } // // private String makePathString(int[] path, int endIndex) // { // StringBuilder sb = new StringBuilder(); // // for (int i = 0; i <= endIndex; i++) // { // sb.append(Integer.toString(path[i])); // } // // return sb.toString(); // } // // public void printBits() // { // for (int i = 0; i < levelZeroCells.size(); i++) // { // this.testAndDo(0, i, levelZeroCells.get(i), Sector.FULL_SPHERE.asDegreesArray()); // } // } // } // // /** // * A test class that visits all the cells in the quadtree and sets their bit in the bit-set. // * @param the item type. // */ // protected static class VisitOp extends BasicQuadTree // { // public VisitOp(int maxLevel, Sector region) // { // super(maxLevel, region, null); // } // // protected boolean doOperation(int level, int position, double[] nodeRegion, double[] testRegion) // { // int bitNum = this.computeBitPosition(level, position); // // this.bits.set(bitNum); // // return level < this.maxLevel; // } // // public void visit(Sector sector) // { // for (int i = 0; i < levelZeroCells.size(); i++) // { // this.testAndDo(0, i, levelZeroCells.get(i), sector.asDegreesArray()); // } // } // } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy