//********************************************************************** // //<copyright> // //BBN Technologies, a Verizon Company //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: ConvexHull.java,v $ //$Revision: 1.5 $ //$Date: 2008/04/15 21:51:01 $ //$Author: dietrick $ // //********************************************************************** package com.bbn.openmap.geo; import java.io.Serializable; import java.util.Comparator; import java.util.Iterator; import java.util.Stack; import java.util.TreeSet; /** * This class contains static methods that can be used to create convex hull * GeoRegions from arrays of Geos. The only algorithm implemented is Graham's, * where the highest point is selected (called the pivot), the other points are * sorted according to their relative azimuths from the pivot, and then a path * is created around the other points. Any right turn encountered traversing the * points means that point should be skipped when creating the convex hull. * * @author dietrick */ public class ConvexHull { private ConvexHull() {} /** * Using Graham's scan. * * @param geos * @return GeoRegion outlining the convex hull of the geos */ public static final GeoRegion getRegion(Geo[] geos) { Geo[] regionGeos = hull(geos); return new GeoRegion.Impl(regionGeos); } /** * Using Graham's scan. * * @param geos * @return a convex hull of the geos */ public static final Geo[] hull(Geo[] geos) { return hull(geos, 0); } /** * Using Graham's scan. * * @param geos * @param tolerance the distance between points where they would be * considered equals, in radians. * @return a convex hull of the geos */ public static final Geo[] hull(Geo[] geos, double tolerance) { Geo pivot = findHighest(geos); TreeSet sortedGeos = new TreeSet(new PivotAngleComparator(pivot)); for (int i = 0; i < geos.length; i++) { Geo g = geos[i]; if (g != pivot) { sortedGeos.add(g); } } Stack hullStack = new Stack(); hullStack.push(pivot); Geo gCross, midCross = null; Geo geo = null, endGeo = null, midGeo = null; Iterator sortedGeoIt = sortedGeos.iterator(); if (sortedGeoIt.hasNext()) { midGeo = (Geo) sortedGeoIt.next(); while (midGeo.distance(pivot) == 0 && sortedGeoIt.hasNext()) { midGeo = (Geo) sortedGeoIt.next(); } } Geo lastGeoRead = midGeo; while (sortedGeoIt.hasNext() && midGeo != null) { geo = (Geo) sortedGeoIt.next(); double dist = geo.distance(lastGeoRead); if (dist <= tolerance) { // Debug.output("Skipping duplicate geo"); continue; } endGeo = (Geo) hullStack.peek(); midCross = endGeo.crossNormalize(midGeo); gCross = midGeo.crossNormalize(geo); Geo i = gCross.crossNormalize(midCross).antipode(); // Debug.output("Evaluating:\n\tendGeo: " + endGeo + "\n\tmidGeo: " // + midGeo + "\n\tto " + geo // + "\n ****** intersection point: " + i); if (midGeo.distance(i) < Math.PI / 2) { // Debug.output("+++++++++++++ midGeo to hull"); // left turn, OK for hull hullStack.push(midGeo); endGeo = midGeo; midGeo = geo; } else { // right turn, need to backtrack while (hullStack.size() > 1) { // Debug.output("-------- midGeo dropped"); midGeo = (Geo) hullStack.pop(); endGeo = (Geo) hullStack.peek(); midCross = endGeo.crossNormalize(midGeo); gCross = midGeo.crossNormalize(geo); i = gCross.crossNormalize(midCross).antipode(); // Debug.output("Evaluating:\n\tendGeo: " + endGeo // + "\n\tmidGeo: " + midGeo + "\n\tto " + geo // + "\n ****** intersection point: " + i); if (midGeo.distance(i) < Math.PI / 2) { // Debug.output("+++++++++++++ midGeo to hull"); hullStack.push(midGeo); midGeo = geo; break; } } } lastGeoRead = geo; } if (midGeo != null) { hullStack.push(midGeo); } hullStack.push(pivot); Geo[] regionGeos = new Geo[hullStack.size()]; int i = 0; // Need to reverse order to get inside of poly on the right side of // line. while (!hullStack.isEmpty()) { regionGeos[i++] = (Geo) hullStack.pop(); } return regionGeos; } protected static Geo findHighest(Geo[] geos) { Geo ret = null; double highest = Double.NEGATIVE_INFINITY; for (int i = 0; i < geos.length; i++) { double lat = geos[i].getLatitude(); if (lat > highest) { highest = lat; ret = geos[i]; } } return ret; } // XXX: does this need to be serializable? protected static final class PivotAngleComparator implements Comparator, Serializable { private Geo pivot; public PivotAngleComparator(Geo pivot) { this.pivot = pivot; } public int compare(Object obj1, Object obj2) { double ang1 = Double.MAX_VALUE, ang2 = Double.MAX_VALUE; int ret = 0; if (obj1 instanceof Geo) { ang1 = Math.toDegrees(pivot.azimuth((Geo) obj1)); } if (obj2 instanceof Geo) { ang2 = Math.toDegrees(pivot.azimuth((Geo) obj2)); } // ts1 is the one being tested/added to the TreeSet, so we // want later items with the same time being added after // previous items in the file with the same time. if (ang1 < ang2) { ret = 1; } else if (ang1 >= ang2) { ret = -1; } return ret; } public Geo getPivot() { return pivot; } public boolean equals(Object obj) { if (obj == null) { return false; } if (getClass() != obj.getClass()) { return false; } if (obj instanceof PivotAngleComparator) { return pivot.equals(((PivotAngleComparator) obj).pivot); } else { return false; } } public int hashCode() { return pivot.hashCode(); } } }