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