//**********************************************************************
//
//<copyright>
//
//BBN Technologies
//10 Moulton Street
//Cambridge, MA 02138
//(617) 873-8000
//
//Copyright (C) BBNT Solutions LLC. All rights reserved.
//
//</copyright>
//**********************************************************************
//
//$Source:
///cvs/darwars/ambush/aar/src/com/bbn/ambush/mission/MissionHandler.java,v
//$
//$RCSfile: ExtentIndex.java,v $
//$Revision: 1.4 $
//$Date: 2007/02/13 20:02:09 $
//$Author: dietrick $
//
//**********************************************************************
package com.bbn.openmap.geo;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
/**
* A Collection of Regions that supports indexed subsets. That is, in addition
* to acting like a normal collection, it also allows getting an iterator that
* will return a superset of all intersecting regions that is a subset of the
* whole collection.
*
* @author mthome@bbn.com
*/
public interface ExtentIndex extends java.util.Collection {
/**
* report on the maximum horizontalRange supported by this index.
*/
double indexHorizontalRange();
/**
* Add a extent to the index.
*
* @param region
* @return true if Region successfully added, false if not.
*/
boolean addExtent(GeoExtent region);
/**
* Remove a region from the index.
*
* @param region
* @return true if the region was found and removed.
*/
boolean removeExtent(GeoExtent region);
/**
* Resets the index to an empty state.
*/
void clear();
/**
* return an iterator listing a subset of the whole collection that is a
* superset of the actual matches. A valid (but inefficient) implementation
* would return an iterator over the whole collection.
*
* Implementation should match anything that is likely to match - this will
* generally include, for instance, additional space around the actual
* segment to accommodate buffer zones around the segment.
*/
Iterator iterator(GeoExtent extent);
/**
* A basic implementation of ExtentIndex that uses Collection-typed buckets.
* Extending classes must implement #makeBucket(int) to specify an
* alternative Collection implementation.
*/
abstract class AbstractExtentIndex extends java.util.AbstractCollection
implements ExtentIndex {
/**
* Default value for #nbuckets if not specified in the call to the
* constructor.
*/
public static final int D_NBUCKETS = 360;
/**
* Default value for #margin if not specified in the call to the
* constructor.
*/
public static final double D_MARGIN = 0.0;
/**
* how many buckets in the longitudinal index - 360 means 1 bucket per
* degree of longitude. More than 360 doesn't seem to add much search
* speed, less than 180 makes it slower. The sweet spot on current
* datasets is somewhere in between.
*
* If unspecified, defaults to #D_NBUCKETS
*/
public final int nbuckets;
/**
* how much of a margin to put around regions for indexing purposes, in
* nautical miles. This must be at least the largest margin searched for
* by route (currently 50nmiles) - the larger this value, the larger the
* average entries/bucket and so, the slower the search.
*
* If unspecified, defaults to #D_MARGIN
*/
public final double margin;
protected final Collection buckets[];
/** all is a collection of everything successfully indexed. */
protected final Collection all;
/**
* polar is a bucket for anything that is near enough to either pole to
* cover more than 1/2 the buckets.
*/
protected final Collection polar;
protected final Collection discarded;
public AbstractExtentIndex() {
this(D_NBUCKETS, D_MARGIN);
}
public AbstractExtentIndex(int nb) {
this(nb, D_MARGIN);
}
public AbstractExtentIndex(double m) {
this(D_NBUCKETS, m);
}
public AbstractExtentIndex(int nb, double m) {
nbuckets = nb;
margin = m;
buckets = new Collection[nbuckets];
all = makeBucket(2000);
polar = makeBucket();
discarded = makeBucket();
}
protected final Collection makeBucket() {
return makeBucket(0);
}
/**
* implement to specify the factory to use to create Bucket storage.
*
* @param sizeHint a guess at the number of elements that are likely to
* be stored in this bucket or 0 if unknown.
* @return A Collection instance suitable for use as a bucket
*
*/
abstract protected Collection makeBucket(int sizeHint);
/**
* Add an object to the index.
*
* @return true if object is a GeoExtent and was added.
*/
public boolean add(Object o) {
if (o instanceof GeoExtent) {
return addExtent((GeoExtent) o);
} else {
return false;
}
}
/**
* Method to call to add Region object with BoundingCircle to Collection
* and organize it for later retrieval.
*
* @param extent Region to index
* @return true if object added, false if it's been discarded.
*/
public boolean addExtent(GeoExtent extent) {
boolean ret = false;
try {
BoundingCircle bc = extent.getBoundingCircle();
if (bc == null) {
discarded.add(extent);
return false;
}
Geo center = bc.getCenter();
double clon = center.getLongitude();
double clat = center.getLatitude();
double rnm = Geo.nm(bc.getRadius());
if ((clat == 90.0 && clon == -180.0) || rnm >= 90 * 60) {
discarded.add(extent);
} else {
all.add(extent); // add to the everything list
// we need to project the radius away from the
// center at the latitude, NOT at the equator!
double latfactor = Geo.npdAtLat(clat);
if (latfactor == 0) {
polar.add(extent);
ret = true;
} else {
double xd = (rnm + margin) / latfactor;
/*
* margin = xd "extra degrees" at the center's latitude
*/
if (xd >= 45) {
polar.add(extent);
ret = true;
} else {
double[] lons = normalizeLons(new double[] {
clon - xd, clon + xd });
int lb = bucketFor(lons[0]);
int rb = bucketFor(lons[1]);
if (rb < lb)
rb += nbuckets;
for (int i = lb; i <= rb; i++) {
int x = i % nbuckets;
Collection b = buckets[x];
if (b == null) {
b = makeBucket(5);
buckets[x] = b;
}
b.add(extent);
ret = true;
}
}
}
}
} catch (Exception e) {
}
return ret;
}
/** normalize longitude to be at least 0.0 and less than 360 * */
protected static final double normalizeLon(double lon) {
// put it into the range of [-360.0, +360.0]
double n = lon % 360;
return (n < 0.0) ? n + 360.0 : n;
// now n is (0.0,+360]
}
/**
* figure out what bucket a particular longitude goes in.
*/
protected final int bucketFor(double lon) {
return (int) Math.floor(normalizeLon(lon) / 360.0
* (double) nbuckets);
}
/*
* Normalize and sort the argument two element array so that on a
* north-up globe, a great-circle arc between the points is headed
* eastward and is less than half-way around the world. @param lons
* two-element array on longitudes @return the mutated argument.
*/
protected final static double[] normalizeLons(double[] lons) {
double a = normalizeLon(lons[0]);
double b = normalizeLon(lons[1]);
// if wide and east or narrow and west, swap
if ((Math.abs(b - a) > 180.0) == (b > a)) {
lons[0] = b;
lons[1] = a;
} else {
lons[0] = a;
lons[1] = b;
}
return lons;
}
/**
* Called when you want everything in each bucket between the
* coordinates.
*
* @param left left-most (west) bucket value.
* @param right right-most (east) bucket value.
* @return Iterator over regions in buckets that cover range provided.
*/
protected Iterator lookup(double left, double right) {
return lookup(left, right, null);
}
/**
* Called when you want to get the regions in the buckets, but you want
* to further filter on objects that can intersect based on the bounding
* circle provided.
*
* @param left left-most (west) bucket value.
* @param right right-most (east) bucket value.
* @param bc Bounding circle to do another filter check, if null,
* everything in a bucket will be returned.
* @return Iterator over regions in buckets that cover range provided
* that intersect with the BoundingCircle (if one is provided).
*/
protected Iterator lookup(double left, double right, BoundingCircle bc) {
Collection s = null;
int lb = bucketFor(left);
int rb = bucketFor(right);
if (rb < lb)
rb += nbuckets;
for (int i = lb; i <= rb; i++) {
Collection b = buckets[i % nbuckets];
if (b != null) {
if (bc == null) {
if (s == null) {
s = new HashSet();
}
s.addAll(b);
} else {
for (Iterator it = b.iterator(); it.hasNext();) {
GeoExtent region = (GeoExtent) it.next();
if (bc.intersects(region.getBoundingCircle())) {
if (s == null) {
s = new HashSet();
}
s.add(region);
}
}
}
}
}
if (!polar.isEmpty()) {
if (s == null) {
s = new HashSet();
}
s.addAll(polar); // add all the polar regions, just in case
}
if (s == null) {
return Collections.EMPTY_SET.iterator();
} else {
return s.iterator();
}
}
/**
* Method to call to remove a region from the index.
*
* @return true if the region was found and removed.
*/
public boolean removeExtent(GeoExtent region) {
boolean ret = false;
BoundingCircle bc = region.getBoundingCircle();
if (bc == null) {
return discarded.remove(region);
}
Geo center = bc.getCenter();
double clon = center.getLongitude();
double clat = center.getLatitude();
double rnm = Geo.nm(bc.getRadius());
if ((clat == 90.0 && clon == -180.0) || rnm >= 90 * 60) {
discarded.remove(region);
} else {
all.remove(region); // remove from the everything list
// we need to project the radius away from the
// center at the latitude, NOT at the equator!
double latfactor = Geo.npdAtLat(clat);
if (latfactor == 0) {
ret = ret || polar.remove(region);
} else {
double xd = (rnm + margin) / latfactor;
/*
* margin = xd "extra degrees" at the center's latitude
*/
if (xd >= 45) {
ret = ret || polar.remove(region);
} else {
double[] lons = normalizeLons(new double[] { clon - xd,
clon + xd });
int lb = bucketFor(lons[0]);
int rb = bucketFor(lons[1]);
if (rb < lb)
rb += nbuckets;
for (int i = lb; i <= rb; i++) {
int x = i % nbuckets;
Collection b = buckets[x];
if (b != null) {
ret = ret || b.remove(region);
if (b.isEmpty()) {
buckets[x] = null;
}
}
}
}
}
}
return ret;
}
/**
* Method to call to clear out the index.
*/
public void clear() {
all.clear();
polar.clear();
discarded.clear();
for (int i = 0; i < buckets.length; i++) {
if (buckets[i] != null) {
buckets[i].clear();
}
}
}
/**
* RegionIndex parameter method.
*
* @return horizontal range in nautical miles for matches.
*/
public double indexHorizontalRange() {
return margin;
}
public Iterator lookupBySegment(GeoSegment segment) {
Geo[] pts = segment.getSeg();
double[] lons = normalizeLons(new double[] { pts[0].getLongitude(),
pts[1].getLongitude() });
return lookup(lons[0], lons[1], segment.getBoundingCircle());
}
public Iterator lookupByPath(GeoPath path) {
Collection results = null;
GeoPath.SegmentIterator pit = path.segmentIterator();
while (pit.hasNext()) {
GeoSegment seg = pit.nextSegment();
for (Iterator it = lookupBySegment(seg); it.hasNext();) {
if (results == null) {
results = new HashSet();
}
results.add(it.next());
}
}
if (results == null) {
return Collections.EMPTY_SET.iterator();
} else {
return results.iterator();
}
}
public Iterator lookupByBoundingCircle(BoundingCircle bc) {
double cLon = bc.getCenter().getLongitude();
double rNM = Geo.nm(bc.getRadius()); // radius in nm at
// equator
double npd = Geo.npdAtLat(bc.getCenter().getLatitude());
if (npd == 0) { // avoid divide by zero - polar region
return iterator();
} else {
double rdeg = rNM / npd;
if (rdeg >= 180) {
return iterator(); // radius covers the whole world
} else {
return lookup(cLon - rdeg, cLon + rdeg, bc);
}
}
}
/**
* @return an Iterator over BoundingCircle objects in the Collection
* where the GExtent may be related to them.
*/
public Iterator iterator(GeoExtent o) {
if (o instanceof GeoSegment) {
return lookupBySegment((GeoSegment) o);
} else if (o instanceof GeoRegion) {
// It's important that GeoRegion be tested before GeoPath,
// because the GeoPath will catch GeoRegions, and doing the
// lookup by path for a region may cause the lookup to fail for
// extents near the center of the region (outside of the
// bounding circles of the region's segments).
return lookupByBoundingCircle(o.getBoundingCircle());
} else if (o instanceof GeoPath) {
return lookupByPath((GeoPath) o);
} else if (o instanceof GeoPoint) {
return lookupByBoundingCircle(new BoundingCircle.Impl(((GeoPoint) o).getPoint(), 0));
} else {
return lookupByBoundingCircle(o.getBoundingCircle());
}
}
/**
* @return Iterator over all entries in Collection.
*/
public Iterator iterator() {
return all.iterator();
}
/**
* @return number of all entries in Collection.
*/
public int size() {
return all.size();
}
//
// metrics
//
public String toString() {
int entc = 0;
int empties = 0;
for (int i = 0; i < nbuckets; i++) {
Collection l = buckets[i];
if (l != null) {
entc += l.size();
} else {
empties++;
}
}
return this.getClass().getName() + "[" + size() + " -"
+ discarded.size() + " E" + (entc / ((float) nbuckets))
+ "]";
}
}
class HashSetExtentIndexImpl extends AbstractExtentIndex {
public HashSetExtentIndexImpl() {
this(D_NBUCKETS, D_MARGIN);
}
public HashSetExtentIndexImpl(int nb) {
this(nb, D_MARGIN);
}
public HashSetExtentIndexImpl(double m) {
this(D_NBUCKETS, m);
}
public HashSetExtentIndexImpl(int nb, double m) {
super(nb, m);
}
protected Collection makeBucket(int sizeHint) {
if (sizeHint != 0) {
return new HashSet();
} else {
return new HashSet(sizeHint);
}
}
}
class ArrayListExtentIndexImpl extends AbstractExtentIndex {
public ArrayListExtentIndexImpl() {
this(D_NBUCKETS, D_MARGIN);
}
public ArrayListExtentIndexImpl(int nb) {
this(nb, D_MARGIN);
}
public ArrayListExtentIndexImpl(double m) {
this(D_NBUCKETS, m);
}
public ArrayListExtentIndexImpl(int nb, double m) {
super(nb, m);
}
protected Collection makeBucket(int sizeHint) {
if (sizeHint != 0) {
return new ArrayList();
} else {
return new ArrayList(sizeHint);
}
}
}
}