package com.bbn.openmap.proj; import java.awt.Shape; import java.awt.geom.FlatteningPathIterator; import java.awt.geom.GeneralPath; import java.awt.geom.PathIterator; import java.util.ArrayList; import java.util.List; import com.bbn.openmap.proj.coords.LatLonPoint; /** * Generator class that connects simple coordinates with complex lines (great * circle or rhumb). If you provide decimal degrees, the answer will be in * decimal degrees. If coords are provided in radians, the answer will be in * radians. * * <pre> * Usage: * * double[] coords = new double[] { 30.0, -125.0, 30.0, -90.0, 15.0, -90.0, 15, -125.0, 30.0, -125.0 }; * double[] complexCoords = LineCoordinateGenerator.fromDegrees(coords).withSegmentsPerDegrees(10).greatCircleLineDoubles(); * * </pre> * * @author dietrick */ public class LineCoordinateGenerator { public final static double DEFAULT_SEGS_PER_DEG = 10; double segsPerDeg = DEFAULT_SEGS_PER_DEG; final double[] llpts; private boolean returnDegrees = false; /** * Use static methods to create one, designating radians or degrees. * * @param radians */ private LineCoordinateGenerator(double[] radians) { this.llpts = radians; } /** * Create LCG with radian coordinates. * * @param radians array of coordinates in radians, in lat, lon, lat, lon * order. * @return LCG */ public static LineCoordinateGenerator fromRadians(double[] radians) { return new LineCoordinateGenerator(radians); } /** * Create LCG with decimal degree coordinates. * * @param degrees array of coordinates in degrees, in lat, lon, lat, lon * order. * @return LCG */ public static LineCoordinateGenerator fromDegrees(double[] degrees) { double[] radians = new double[degrees.length]; System.arraycopy(degrees, 0, radians, 0, degrees.length); ProjMath.arrayDegToRad(radians); return new LineCoordinateGenerator(radians).fromDegrees(); } private LineCoordinateGenerator fromDegrees() { returnDegrees = true; return this; } /** * Set how complex the line is by setting how many segments per degree are * used to approximate the curve. * * @param spd the default is 10 segments per degree * @return this */ public LineCoordinateGenerator withSegmentsPerDegrees(double spd) { this.segsPerDeg = spd; return this; } /** * Return the source coordinates connected by great circle lines * * @return double[] in lat, lon, lat, lon order. */ public double[] greatCircleLineDoubles() { return toDoubles(greatCircleLineShape()).get(0); } /** * Create a java.awt.Shape object of coordinates connected by great circle * lines. * * @return java.awt.Shape */ public Shape greatCircleLineShape() { GeneralPath path = null; if (llpts != null && llpts.length >= 4 && llpts.length % 2 == 0) { double y1 = llpts[0]; double x1 = llpts[1]; path = new GeneralPath(GeneralPath.WIND_EVEN_ODD, llpts.length / 2); boolean firstCoords = true; for (int i = 2; i < llpts.length; i += 2) { double y2 = llpts[i]; double x2 = llpts[i + 1]; double radDist = GreatCircle.sphericalDistance(y1, x1, y2, x2); int nsegs = (int) (ProjMath.radToDeg(radDist) * segsPerDeg); // segs/degree if (nsegs == 0) { nsegs = 1; } double[] coords = GreatCircle.greatCircle(y1, x1, y2, x2, nsegs, false); if (returnDegrees) { ProjMath.arrayRadToDeg(coords); } for (int j = 0; j <= coords.length - 1; j += 2) { if (firstCoords) { path.moveTo(coords[j + 1], coords[j]); firstCoords = false; } else { path.lineTo(coords[j + 1], coords[j]); } } x1 = x2; y1 = y2; } // End point, since great circle calc doesn't tack on the last // point. if (returnDegrees) { path.lineTo(ProjMath.radToDeg(x1), ProjMath.radToDeg(y1)); } else { path.lineTo(x1, y1); } } return path; } /** * Return the source coordinates connected by rhumb lines * * @return double[] in lat, lon, lat, lon order. */ public double[] rhumbLineDoubles() { return toDoubles(rhumbLineShape()).get(0); } /** * Create a java.awt.Shape object of coordinates connected by rhumb lines. * * @return java.awt.Shape */ public Shape rhumbLineShape() { GeneralPath path = null; if (llpts != null && llpts.length >= 4 && llpts.length % 2 == 0) { LatLonPoint ll1 = new LatLonPoint.Double(llpts[0], llpts[1], true); path = new GeneralPath(GeneralPath.WIND_EVEN_ODD, llpts.length / 2); boolean firstCoords = true; for (int i = 2; i < llpts.length - 1; i += 2) { LatLonPoint ll2 = new LatLonPoint.Double(llpts[i], llpts[i + 1], true); if (firstCoords) { moveTo(path, ll1); firstCoords = false; } else { lineTo(path, ll1); } double radDist = RhumbCalculator.getDistanceBetweenPoints(ll1, ll2); double angle = RhumbCalculator.getAzimuthBetweenPoints(ll1, ll2); double segDistIncrease = radDist / ProjMath.degToRad(ProjMath.radToDeg(radDist) * segsPerDeg); // segs/degree double segDist = segDistIncrease; while (segDist < radDist) { LatLonPoint llp = RhumbCalculator.calculatePointOnRhumbLine(ll1, angle, segDist); lineTo(path, llp); segDist += segDistIncrease; } ll1 = ll2; } // Make sure last coordinate is on return shape lineTo(path, ll1); } return path; } private void moveTo(GeneralPath path, LatLonPoint llp) { if (returnDegrees) { path.moveTo(llp.getX(), llp.getY()); } else { path.moveTo(llp.getRadLon(), llp.getRadLat()); } } private void lineTo(GeneralPath path, LatLonPoint llp) { if (returnDegrees) { path.lineTo(llp.getX(), llp.getY()); } else { path.lineTo(llp.getRadLon(), llp.getRadLat()); } } /** * Creates a Shape object from provided coordinates. * * @return java.awt.Shape */ public Shape straightLineShape() { GeneralPath path = null; if (llpts != null && llpts.length >= 4 && llpts.length % 2 == 0) { double y1 = llpts[0]; double x1 = llpts[1]; path = new GeneralPath(GeneralPath.WIND_EVEN_ODD, llpts.length / 2); if (returnDegrees) { path.moveTo(ProjMath.radToDeg(x1), ProjMath.radToDeg(y1)); } else { path.moveTo(x1, y1); } for (int i = 2; i < llpts.length - 1; i += 2) { x1 = llpts[i + 1]; y1 = llpts[i]; if (returnDegrees) { path.lineTo(ProjMath.radToDeg(x1), ProjMath.radToDeg(y1)); } else { path.lineTo(x1, y1); } } } return path; } /** * Convert a Shape object into a List of double[]. Separate double[] are * created in case there's a moveTo in the order of coordinates in Shape. * * @param s java.awt.Shape * @return a List of double[] */ public static List<double[]> toDoubles(Shape s) { List<double[]> coordLists = new ArrayList<double[]>(); PathIterator pi2 = s.getPathIterator(null); FlatteningPathIterator pi = new FlatteningPathIterator(pi2, .25); double[] coords = new double[6]; double lastMovedToPntX = Double.NaN; double lastMovedToPntY = Double.NaN; List<Double> curCoord = null; while (!pi.isDone()) { int type = pi.currentSegment(coords); if (type == PathIterator.SEG_LINETO) { // Moved to the next point curCoord = assertList(curCoord); // lat/lon order curCoord.add(coords[1]); curCoord.add(coords[0]); } else if (type == PathIterator.SEG_MOVETO) { if (curCoord != null && !curCoord.isEmpty()) { coordLists.add(toArray(curCoord)); curCoord = null; } // Usually the first set of coords lastMovedToPntX = coords[0]; lastMovedToPntY = coords[1]; curCoord = assertList(curCoord); curCoord.add(lastMovedToPntY); curCoord.add(lastMovedToPntX); } else if (type == PathIterator.SEG_CLOSE) { // The last set of coords, if they should go back to the first // set of coords. final double x = coords[0]; final double y = coords[1]; if (x != lastMovedToPntX && y != lastMovedToPntY) { curCoord = assertList(curCoord); curCoord.add(y); curCoord.add(x); } if (curCoord != null && !curCoord.isEmpty()) { coordLists.add(toArray(curCoord)); curCoord = null; } } pi.next(); } if (curCoord != null && !curCoord.isEmpty()) { coordLists.add(toArray(curCoord)); } return coordLists; } private static List<Double> assertList(List<Double> list) { if (list != null) { return list; } return new ArrayList<Double>(); } private static double[] toArray(List<Double> coords) { double[] ret = new double[coords.size()]; int i = 0; for (double d : coords) { ret[i++] = d; } return ret; } }