package com.bbn.openmap.omGraphics.util; import java.util.LinkedList; import java.util.List; import java.util.logging.Level; import java.util.logging.Logger; import com.bbn.openmap.MoreMath; import com.bbn.openmap.geo.Geo; import com.bbn.openmap.geo.GeoArray; import com.bbn.openmap.geo.Intersection; import com.bbn.openmap.geo.Ribbon; import com.bbn.openmap.geo.RibbonIterator; import com.bbn.openmap.omGraphics.OMAreaList; import com.bbn.openmap.omGraphics.OMGraphic; import com.bbn.openmap.omGraphics.OMPoly; import com.bbn.openmap.proj.Length; /** * The RibbonMaker class takes polygon coordinates and creates another polygon * from it, a buffer region based on ground distance, around the original poly. * It assumes that the polygon coordinates are going in a clockwise direction. * The returned polygon is actually an OMAreaList made up of OMPolys, OMArcs and * OMLines, depending on the shape of the original polygon. * <p> * * The RibbonMaker is created from one of the factory methods that designates * whether the coordinates are radians or decimal degrees. FYI - OMPoly * coordinates retrieved from the getLatLonArray method are in radians. After * the RibbonMaker is created, call the method that creates the appropriate * shape. For now, getOuterRing is your only option. To use: * * <pre> * * llPoints = new double[] { 40.0f, -92.0f, 42.0f, -87.0f, 38.57, -90.825, 37.0f, -89.0f, 35.0f, -94.0f, 40.0f, * -92.0f }; * * OMGraphic omg = RibbonMaker.createFromDecimalDegrees(llPoints).getOuterRing(Length.MILE.toRadians(100)); * * </pre> * * * @author ddietrick */ public class RibbonMaker { protected GeoArray geoCoords; // If a negative distance is passed to getOuterRing, then we'll construct // this with opposite winding protected GeoArray geoCoords_reversed = null; private static final Logger logger = Logger.getLogger("com.bbn.openmap.omGraphics.util.RibbonMaker"); protected final static int STRAIGHT = 0; protected final static int BENDS_LEFT = -1; protected final static int BENDS_RIGHT = 1; protected double bufferLimit = 4.778825E-10; protected RibbonMaker(GeoArray gCoords) { geoCoords = gCoords; } /** * Create a RibbonMaker from decimal degree coordinates. * * @param coords lat,lon,lat,lon in decimal degrees. * @return RibbonMaker */ public static RibbonMaker createFromDecimalDegrees(double[] coords) { return new RibbonMaker(GeoArray.Double.createFromLatLonDegrees(coords)); } /** * Create a RibbonMaker from radian coordinates. * * @param coords lat,lon,lat,lon in radians. * @return RibbonMaker */ public static RibbonMaker createFromRadians(double[] coords) { return new RibbonMaker(GeoArray.Double.createFromLatLonRadians(coords)); } /** * Assumes coords represent a polygon, returns an OMAreaList representing * buffer zone around the outside of a polygon. * * @param dist distance of buffer area, in radians. Use Length to convert. * @return OMAreaList of a polygon that is a distance away from the * coordinate polygon's edges. */ public OMAreaList getOuterRing(double dist) { OMAreaList ret = new OMAreaList(); if (Math.abs(dist) <= bufferLimit) { return ret; } GeoArray localGeoCoords = this.geoCoords; if (dist < 0) { // Construct a version of this shape wound the other way dist = -dist; if (geoCoords_reversed == null) { double[] reversed = new double[2 * geoCoords.getSize()]; for (int i = geoCoords.getSize() - 1; i >= 0; i--) { Geo thisGeo = geoCoords.get(geoCoords.getSize() - i - 1); reversed[2 * i] = thisGeo.getLatitudeRadians(); reversed[2 * i + 1] = thisGeo.getLongitudeRadians(); } geoCoords_reversed = GeoArray.Double.createFromLatLonRadians(reversed); } localGeoCoords = geoCoords_reversed; } int numCoords = localGeoCoords.getSize(); if (numCoords >= 3) { Geo g1 = localGeoCoords.get(0); Geo g2 = localGeoCoords.get(1); Geo g3 = localGeoCoords.get(2); handlePointsForOuterRing(g1, g2, g3, dist, ret); for (int i = 3; i < numCoords; i++) { g1 = g2; g2 = g3; g3 = localGeoCoords.get(i); handlePointsForOuterRing(g1, g2, g3, dist, ret); } // test, and close it off if needed if (!localGeoCoords.get(0).equals(localGeoCoords.get(numCoords - 1))) { g1 = g2; g2 = g3; g3 = localGeoCoords.get(0); handlePointsForOuterRing(g1, g2, g3, dist, ret); } // Now round out the first and last segment, centering on the first // coordinate g1 = g2; g2 = g3; g3 = localGeoCoords.get(1); handlePointsForOuterRing(g1, g2, g3, dist, ret); } return ret; } /** * Takes a corner represented by the three geos, and adds OMGraphics to the * OMAreaList depending on which way the corner bends - for right turns, * it'll add an OMLine, OMArc and OMLine. The OMLines will go from half the * distance of the legs to the rounded corner. The left turn will have a * polygon added. * * @param g1 point 1 * @param g2 point 2 * @param g3 point 3 * @param dist buffer distance in radians * @param ret OMAreaList to add OMGraphics to. */ protected void handlePointsForOuterRing(Geo g1, Geo g2, Geo g3, double dist, OMAreaList ret) { int bend = bends(g1, g2, g3); Geo gret = g3; RibbonIterator leg1 = new RibbonIterator(g1, g2, dist); OMPoly poly1 = getHalfPoly(leg1, Ribbon.LEFT, false); RibbonIterator leg2 = new RibbonIterator(g2, g3, dist); OMPoly poly2 = getHalfPoly(leg2, Ribbon.LEFT, true); // Oh dear... if (poly1 == null || poly2 == null) { return; } if (bend == STRAIGHT || g2.equals(g3)) { ret.add(poly1); ret.add(poly2); } else { if (bend == BENDS_LEFT) { // short, need to find intersection of two legs and remove // points // from polys to only go to intersection double dg12 = g1.distance(g2); double dg23 = g2.distance(g3); double legTestDist = dist * 2; if (dg12 < legTestDist || dg23 < legTestDist) { addShortLegPolyForIntersection(g1, g2, g3, Ribbon.LEFT, dist, ret); } else { addPolyForIntersection(poly1, poly2, dist, ret); } } else { OMGraphic omp = getPushbackPoly(poly1, dist); if (omp != null) { ret.add(omp); } // Add OMArc in the middle, rounding around a corner OMGraphic oma = getArc(g2, poly1, poly2); if (oma != null) { ret.add(oma); } omp = getPushbackPoly(poly2, dist); if (omp != null) { ret.add(omp); } } } } /** * Method that determines which way the angle between the three points * bends. * * @param g1 * @param g2 * @param g3 * @return STRAIGHT if no bend, BENDS_LEFT if bends less than PI, * BENDS_RIGHT if bends more than PI. */ protected int bends(Geo g1, Geo g2, Geo g3) { double bend = g1.crossNormalize(g2).distance(g3) - (Math.PI / 2.0); if (Math.abs(bend) < .0001) { return STRAIGHT; // essentially straight } else { if (bend < 0) { return BENDS_LEFT; } } return BENDS_RIGHT; } /** * Checks to see if a point is too close to any side of the original * polygon. * * @param pnt * @param distance * @return true if is too close and should not be added to the buffer * polygon. */ protected boolean tooClose(Geo pnt, double distance) { return Intersection.isPointNearPoly(pnt, geoCoords, distance - bufferLimit); } /** * Takes a poly that's going to be added to the buffer and removes any * points that may be too close to the original poly. * * @param omp the buffer poly to be added later * @param dist the distance all points should be from the original * @return the OMGraphic with good points. */ protected OMGraphic getPushbackPoly(OMPoly omp, double dist) { double[] coords = omp.getLatLonArray(); List<Geo> results = new LinkedList<Geo>(); for (int i = 0; i < coords.length - 2; i += 2) { Geo g = new Geo(coords[i], coords[i + 1], false); if (!tooClose(g, dist)) { results.add(g); } } if (results.size() == 1) { results.add(new Geo(results.get(0))); } if (results.size() > 1) { return getOMPolyFromGeos(results); } return null; } /** * Called when it's known that one of the legs between the corner is smaller * than the buffer depth. Does some extra work to figure out what points * should be added to the buffer. * * @param g1 point 1 * @param g2 point 2, the corner * @param g3 point 3 * @param ribbonSide which side of the ribbon should be calculated. * @param dist the distance the buffer should be from the legs * @param ret the OMGraphicList to add the resulting poly to. */ protected void addShortLegPolyForIntersection(Geo g1, Geo g2, Geo g3, int ribbonSide, double dist, OMAreaList ret) { /** * We need to do some extra work here. Since one of the legs is shorter * than 2*dist of the buffer, half of the buffer polygon won't reach the * intersection point between the two legs. So, we need to recalculate * the polys so the represent the entire legs, in order to find the * intersection point. * * Once we have that, we can go back to the half polygons, and test for * that intersection point against each little segment. If any of those * points are further away than buffer distance to the opposite poly, * they should be included on the polygon added to the list. If a point * is inside that distance, it should be disregarded. If there is only * one point (i.e. the original intersection point), then a duplicate * intersection point should be added, so the OMAreaList will handle it * properly. */ List<Geo> results = new LinkedList<Geo>(); RibbonIterator leg1 = new RibbonIterator(g1, g2, dist); OMPoly fullPoly1 = getPoly(leg1, ribbonSide); RibbonIterator leg2 = new RibbonIterator(g2, g3, dist); OMPoly fullPoly2 = getPoly(leg2, ribbonSide); if (fullPoly1 == null || fullPoly2 == null) { return; } // Intersection is the point on both polys that is buffer distance away // from corner Geo intersection = getPolyIntersection(fullPoly1, fullPoly2); if (intersection == null) { // GAAH! This shouldn't happen return; } leg1 = new RibbonIterator(g1, g2, dist); OMPoly halfPoly1 = getHalfPoly(leg1, ribbonSide, false); if (halfPoly1 != null) { GeoArray geoPoly2 = GeoArray.Double.createFromLatLonRadians(fullPoly2.getLatLonArray()); double[] leg1Coords = halfPoly1.getLatLonArray(); for (int i = 0; i < leg1Coords.length - 1; i += 2) { Geo pnt = new Geo(leg1Coords[i], leg1Coords[i + 1], false); if (!tooClose(pnt, dist)) { results.add(pnt); } } if (!tooClose(intersection, dist)) { results.add(intersection); } } leg2 = new RibbonIterator(g2, g3, dist); OMPoly halfPoly2 = getHalfPoly(leg2, ribbonSide, true); if (halfPoly2 != null) { GeoArray geoPoly1 = GeoArray.Double.createFromLatLonRadians(fullPoly1.getLatLonArray()); double[] leg2Coords = halfPoly2.getLatLonArray(); for (int i = 0; i < leg2Coords.length - 1; i += 2) { Geo pnt = new Geo(leg2Coords[i], leg2Coords[i + 1], false); if (!tooClose(pnt, dist)) { results.add(pnt); } } } if (results.size() == 1 && !tooClose(intersection, dist)) { results.add(intersection); } if (results.size() > 1) { ret.add(getOMPolyFromGeos(results)); } } /** * Just return the point where the two polygons cross. * * @param poly1 * @param poly2 * @return null if no point found. */ protected Geo getPolyIntersection(OMPoly poly1, OMPoly poly2) { double[] p1Coords = poly1.getLatLonArray(); double[] p2Coords = poly2.getLatLonArray(); Geo a1, a2, b1, b2; Geo intersect = null; int index1 = 0, index2 = 0; for (; index2 + 3 < p2Coords.length; index2 += 2) { b1 = new Geo(p2Coords[index2], p2Coords[index2 + 1], false); b2 = new Geo(p2Coords[index2 + 2], p2Coords[index2 + 3], false); if (intersect == null) { for (; index1 + 3 < p1Coords.length; index1 += 2) { a1 = new Geo(p1Coords[index1], p1Coords[index1 + 1], false); a2 = new Geo(p1Coords[index1 + 2], p1Coords[index1 + 3], false); intersect = Intersection.segmentsIntersect(a1, a2, b1, b2); if (intersect != null) { return intersect; } } } } return intersect; } /** * Converts Vector of Geos to an OMPoly with linetype great_circle. Assumes * that the List has valid coordinates on it. Does not do a closeness check * to the original poly, expected that's been done. * * @param geos a set of coordinates * @return OMPoly */ protected OMPoly getOMPolyFromGeos(List<Geo> geos) { double[] tmpCoords = new double[geos.size() * 2]; int index = 0; for (Geo geo : geos) { tmpCoords[index++] = geo.getLatitudeRadians(); tmpCoords[index++] = geo.getLongitudeRadians(); } return new OMPoly(tmpCoords, OMPoly.RADIANS, OMGraphic.LINETYPE_GREATCIRCLE); } /** * Called to handle BENDS_LEFT, concave corners. RuntimeException is thrown * when one of the legs is much shorter than the other and the mid-point is * still in the buffer of one of the legs. In that special case, the * intersection point is not found. * * @param poly1 * @param poly2 * @param ret */ protected void addPolyForIntersection(OMPoly poly1, OMPoly poly2, double dist, OMAreaList ret) { double[] p1Coords = poly1.getLatLonArray(); double[] p2Coords = poly2.getLatLonArray(); List<Geo> results = new LinkedList<Geo>(); Geo a1, a2, b1, b2; Geo intersect = null; int index1 = 0, index2 = 0; for (; index2 + 3 < p2Coords.length; index2 += 2) { b1 = new Geo(p2Coords[index2], p2Coords[index2 + 1], false); b2 = new Geo(p2Coords[index2 + 2], p2Coords[index2 + 3], false); if (intersect == null) { for (; index1 + 3 < p1Coords.length; index1 += 2) { a1 = new Geo(p1Coords[index1], p1Coords[index1 + 1], false); a2 = new Geo(p1Coords[index1 + 2], p1Coords[index1 + 3], false); intersect = Intersection.segmentsIntersect(a1, a2, b1, b2); if (!tooClose(a1, dist)) { results.add(a1); } if (intersect != null) { if (!tooClose(intersect, dist)) { results.add(intersect); } break; } } } if (intersect != null && !tooClose(b2, dist)) { results.add(b2); } } if (results.size() > 1) { ret.add(getOMPolyFromGeos(results)); } } /** * Given a RibbonIterator created from two Geos, create a poly from half of * that buffer path. Points are not checked for closeness to original poly. * * @param rIterator RibbonIterator for one of the legs of corner * @param side which RibbonIterator side * @param first which half you want, true for first part * @return OMPoly that represents half of the buffered path. */ protected OMPoly getHalfPoly(RibbonIterator rIterator, int side, boolean first) { List<Geo> results = new LinkedList<Geo>(); for (Ribbon rib : rIterator) { Geo g = rib.get(side); results.add(g); } int numCoords = results.size(); if (numCoords > 0) { int startingIndex = 0; int copyLength = numCoords / 2; if (numCoords % 2 == 0) { if (!first) { startingIndex = copyLength; // middle } } else { if (!first) { startingIndex = copyLength; } copyLength++; } List<Geo> newGeoCoords = new LinkedList<Geo>(); for (int index = 0; index < copyLength; index++) { Geo g = results.get(startingIndex + index); newGeoCoords.add(g); } return getOMPolyFromGeos(newGeoCoords); } return null; } /** * Given a RibbonIterator created from two Geos, create a poly from that * buffer path. * * @param rIterator RibbonIterator for one of the legs of corner * @param side which RibbonIterator side * @return OMPoly that represents buffered path between geos. */ protected OMPoly getPoly(RibbonIterator rIterator, int side) { List<Geo> bufferCoords = new LinkedList<Geo>(); for (Ribbon rib : rIterator) { bufferCoords.add(rib.get(side)); } if (bufferCoords.size() > 1) { return getOMPolyFromGeos(bufferCoords); } return null; } /** * Given two polylines, with the end point of poly1 being the same distance * from a point as the starting point of poly2, create an arc that connects * them. * * @param gc point * @param poly1 polyline where the last end point is used * @param poly2 polyline where the first end point is used. * @return OMArc */ public OMGraphic getArc(Geo gc, OMPoly poly1, OMPoly poly2) { double[] poly1Coords = poly1.getLatLonArray(); Geo pt1 = new Geo(poly1Coords[poly1Coords.length - 2], poly1Coords[poly1Coords.length - 1], false); double radAngle1 = gc.azimuth(pt1); double[] poly2Coords = poly2.getLatLonArray(); Geo pt2 = new Geo(poly2Coords[0], poly2Coords[1], false); double radAngle2 = gc.azimuth(pt2); double dist = gc.distance(pt1); if (radAngle2 < radAngle1) { radAngle2 += MoreMath.TWO_PI_D; } if (logger.isLoggable(Level.FINE)) { logger.fine( new StringBuilder("Making arg starting at ").append(Length.DECIMAL_DEGREE.fromRadians(radAngle1)) .append(", ").append(Length.DECIMAL_DEGREE.fromRadians(radAngle2 - radAngle1)).toString()); } List<Geo> points = new LinkedList<Geo>(); double inc = Length.DECIMAL_DEGREE.toRadians(2.0); double angle = radAngle1 + inc; while (angle < radAngle2 - inc) { Geo g = gc.offset(dist, angle); if (!tooClose(g, dist)) { points.add(g); } angle += inc; } return getOMPolyFromGeos(points); // return new OMArc(gc.getLatitude(), gc.getLongitude(), dist, // Length.RADIAN, 100, Length.DECIMAL_DEGREE.fromRadians(radAngle1), // Length.DECIMAL_DEGREE.fromRadians(radAngle2 - radAngle1)); } }