diff --git a/worldwind-examples/src/main/AndroidManifest.xml b/worldwind-examples/src/main/AndroidManifest.xml index 7db09c69f..c37ad91f4 100644 --- a/worldwind-examples/src/main/AndroidManifest.xml +++ b/worldwind-examples/src/main/AndroidManifest.xml @@ -56,6 +56,14 @@ android:noHistory="true" android:theme="@style/AppTheme.NoActionBar"> + + = 0.0 ? "E" : "W")); } + protected String formatElevaton(double elevation) { + return String.format("Alt: %,.0f %s", + (elevation < 100000 ? elevation : elevation / 1000), + (elevation < 100000 ? "m" : "km")); + } + protected String formatAltitude(double altitude) { return String.format("Eye: %,.0f %s", (altitude < 100000 ? altitude : altitude / 1000), diff --git a/worldwind-examples/src/main/java/gov/nasa/worldwindx/MGRSGraticuleActivity.java b/worldwind-examples/src/main/java/gov/nasa/worldwindx/MGRSGraticuleActivity.java new file mode 100644 index 000000000..4b1c597ec --- /dev/null +++ b/worldwind-examples/src/main/java/gov/nasa/worldwindx/MGRSGraticuleActivity.java @@ -0,0 +1,16 @@ +package gov.nasa.worldwindx; + +import android.os.Bundle; + +import gov.nasa.worldwind.layer.graticule.MGRSGraticuleLayer; + +public class MGRSGraticuleActivity extends GeneralGlobeActivity { + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + this.wwd.getLayers().addLayer(new MGRSGraticuleLayer()); + } + +} diff --git a/worldwind-examples/src/main/res/layout/globe_content.xml b/worldwind-examples/src/main/res/layout/globe_content.xml index 2ca2f77d6..5cdd48b10 100644 --- a/worldwind-examples/src/main/res/layout/globe_content.xml +++ b/worldwind-examples/src/main/res/layout/globe_content.xml @@ -82,6 +82,23 @@ android:padding="10dp" android:text="@string/spacer"/> + + + + + Basic Stress Test Day and Night Cycle General Purpose Globe + MGRS graticule Demonstration Multi-Globe Demonstration Movable Line of Sight Paths Example diff --git a/worldwind/src/main/java/gov/nasa/worldwind/BasicFrameController.java b/worldwind/src/main/java/gov/nasa/worldwind/BasicFrameController.java index e01d4049b..3320ffb33 100644 --- a/worldwind/src/main/java/gov/nasa/worldwind/BasicFrameController.java +++ b/worldwind/src/main/java/gov/nasa/worldwind/BasicFrameController.java @@ -65,7 +65,6 @@ protected void renderTerrainPickedObject(RenderContext rc) { // picked object ID and the intersection position. if (rc.pickRay != null && rc.terrain.intersect(rc.pickRay, this.pickPoint)) { rc.globe.cartesianToGeographic(this.pickPoint.x, this.pickPoint.y, this.pickPoint.z, this.pickPos); - this.pickPos.altitude = 0; // report the actual altitude, which may not lie on the terrain's surface rc.offerPickedObject(PickedObject.fromTerrain(pickedObjectId, this.pickPos)); } } diff --git a/worldwind/src/main/java/gov/nasa/worldwind/Navigator.java b/worldwind/src/main/java/gov/nasa/worldwind/Navigator.java index bff820e0f..5c1748819 100644 --- a/worldwind/src/main/java/gov/nasa/worldwind/Navigator.java +++ b/worldwind/src/main/java/gov/nasa/worldwind/Navigator.java @@ -28,19 +28,29 @@ public class Navigator { protected double roll; - private Camera scratchCamera = new Camera(); + private final Camera scratchCamera = new Camera(); - private Matrix4 modelview = new Matrix4(); + private final Matrix4 modelview = new Matrix4(); - private Matrix4 origin = new Matrix4(); + private final Matrix4 origin = new Matrix4(); - private Vec3 originPoint = new Vec3(); + private final Vec3 originPoint = new Vec3(); - private Position originPos = new Position(); + private final Position originPos = new Position(); - private Line forwardRay = new Line(); + private final Line forwardRay = new Line(); - public Navigator() { + private final WorldWindow wwd; + + private final static double COLLISION_THRESHOLD = 10.0; // 10m above surface + + public Navigator(WorldWindow wwd) { + if (wwd == null) { + throw new IllegalArgumentException( + Logger.logMessage(Logger.ERROR, "Navigator", "constructor", "missingWorldWindow")); + } + + this.wwd = wwd; } public double getLatitude() { @@ -193,16 +203,23 @@ public Matrix4 getAsViewingMatrix(Globe globe, Matrix4 result) { protected LookAt cameraToLookAt(Globe globe, Camera camera, LookAt result) { this.cameraToViewingMatrix(globe, camera, this.modelview); - this.modelview.extractEyePoint(this.forwardRay.origin); - this.modelview.extractForwardVector(this.forwardRay.direction); - if (!globe.intersect(this.forwardRay, this.originPoint)) { - double horizon = globe.horizonDistance(camera.altitude); - this.forwardRay.pointAt(horizon, this.originPoint); + // Pick terrain located behind the viewport center point + PickedObject terrainPickedObject = wwd.pick(wwd.getViewport().width / 2f, wwd.getViewport().height / 2f).terrainPickedObject(); + if (terrainPickedObject != null) { + // Use picked terrain position including approximate rendered altitude + this.originPos.set(terrainPickedObject.getTerrainPosition()); + globe.geographicToCartesian(this.originPos.latitude, this.originPos.longitude, this.originPos.altitude, this.originPoint); + globe.cartesianToLocalTransform(this.originPoint.x, this.originPoint.y, this.originPoint.z, this.origin); + } else { + // Center is outside the globe - use point on horizon + this.modelview.extractEyePoint(this.forwardRay.origin); + this.modelview.extractForwardVector(this.forwardRay.direction); + this.forwardRay.pointAt(globe.horizonDistance(camera.altitude), this.originPoint); + globe.cartesianToGeographic(this.originPoint.x, this.originPoint.y, this.originPoint.z, this.originPos); + globe.cartesianToLocalTransform(this.originPoint.x, this.originPoint.y, this.originPoint.z, this.origin); } - globe.cartesianToGeographic(this.originPoint.x, this.originPoint.y, this.originPoint.z, this.originPos); - globe.cartesianToLocalTransform(this.originPoint.x, this.originPoint.y, this.originPoint.z, this.origin); this.modelview.multiplyByMatrix(this.origin); result.latitude = this.originPos.latitude; @@ -247,6 +264,25 @@ protected Camera lookAtToCamera(Globe globe, LookAt lookAt, Camera result) { result.tilt = this.modelview.extractTilt(); result.roll = lookAt.roll; // roll passes straight through + // Check if camera altitude is not under the surface + double elevation = globe.getElevationAtLocation(result.latitude, result.longitude) * wwd.getVerticalExaggeration() + COLLISION_THRESHOLD; + if(elevation > result.altitude) { + // Set camera altitude above the surface + result.altitude = elevation; + // Compute new camera point + globe.geographicToCartesian(result.latitude, result.longitude, result.altitude, originPoint); + // Compute look at point + globe.geographicToCartesian(lookAt.latitude, lookAt.longitude, lookAt.altitude, forwardRay.origin); + // Compute normal to globe in look at point + globe.geographicToCartesianNormal(lookAt.latitude, lookAt.longitude, forwardRay.direction); + // Calculate tilt angle between new camera point and look at point + originPoint.subtract(forwardRay.origin).normalize(); + double dot = forwardRay.direction.dot(originPoint); + if (dot >= -1 || dot <= 1) { + result.tilt = Math.toDegrees(Math.acos(dot)); + } + } + return result; } diff --git a/worldwind/src/main/java/gov/nasa/worldwind/NavigatorEventSupport.java b/worldwind/src/main/java/gov/nasa/worldwind/NavigatorEventSupport.java index c4fb95f52..b2bf9f1b7 100644 --- a/worldwind/src/main/java/gov/nasa/worldwind/NavigatorEventSupport.java +++ b/worldwind/src/main/java/gov/nasa/worldwind/NavigatorEventSupport.java @@ -41,6 +41,14 @@ public boolean handleMessage(Message msg) { } }); + protected Handler moveHandler = new Handler(Looper.getMainLooper(), new Handler.Callback() { + @Override + public boolean handleMessage(Message msg) { + onNavigatorMoved(); + return false; + } + }); + public NavigatorEventSupport(WorldWindow wwd) { if (wwd == null) { throw new IllegalArgumentException( @@ -53,6 +61,7 @@ public NavigatorEventSupport(WorldWindow wwd) { public void reset() { this.lastModelview = null; this.stopHandler.removeMessages(0 /*what*/); + this.moveHandler.removeMessages(0 /*what*/); if (this.lastTouchEvent != null) { this.lastTouchEvent.recycle(); @@ -113,10 +122,14 @@ public void onFrameRendered(RenderContext rc) { if (this.lastModelview == null) { // this is the first frame; copy the frame's modelview this.lastModelview = new Matrix4(rc.modelview); + // Notify listeners with stopped event on first frame + this.stopHandler.removeMessages(0 /*what*/); + this.stopHandler.sendEmptyMessage(0 /*what*/); } else if (!this.lastModelview.equals(rc.modelview)) { // the frame's modelview has changed this.lastModelview.set(rc.modelview); // Notify the listeners of a navigator moved event. - this.onNavigatorMoved(); + this.moveHandler.removeMessages(0 /*what*/); + this.moveHandler.sendEmptyMessage(0/*what*/); // Schedule a navigator stopped event after a specified delay in milliseconds. this.stopHandler.removeMessages(0 /*what*/); this.stopHandler.sendEmptyMessageDelayed(0 /*what*/, this.stoppedEventDelay); diff --git a/worldwind/src/main/java/gov/nasa/worldwind/WorldWindow.java b/worldwind/src/main/java/gov/nasa/worldwind/WorldWindow.java index 6b50a0613..5dddaa419 100644 --- a/worldwind/src/main/java/gov/nasa/worldwind/WorldWindow.java +++ b/worldwind/src/main/java/gov/nasa/worldwind/WorldWindow.java @@ -75,7 +75,7 @@ public class WorldWindow extends GLSurfaceView implements Choreographer.FrameCal protected double fieldOfView = 45; - protected Navigator navigator = new Navigator(); + protected Navigator navigator = new Navigator(this); protected NavigatorEventSupport navigatorEvents = new NavigatorEventSupport(this); @@ -389,6 +389,10 @@ public void setRenderResourceCache(RenderResourceCache cache) { this.renderResourceCache = cache; } + public Viewport getViewport() { + return this.viewport; + } + /** * Determines the WorldWind objects displayed at a screen point. The screen point is interpreted as coordinates in * Android screen pixels relative to this View. @@ -1039,21 +1043,26 @@ protected void computeViewingTransform(Matrix4 projection, Matrix4 modelview) { double eyeAltitude = this.navigator.getAltitude(); double eyeHorizon = this.globe.horizonDistance(eyeAltitude); double atmosphereHorizon = this.globe.horizonDistance(160000); - double near = eyeAltitude * 0.5; - double far = eyeHorizon + atmosphereHorizon; - // Computes the near clip distance that provides a minimum resolution at the far clip plane, based on the OpenGL - // context's depth buffer precision. - if (this.depthBits != 0) { - double maxDepthValue = (1 << this.depthBits) - 1; - double farResolution = 10.0; - double nearDistance = far / (maxDepthValue / (1 - farResolution / far) - maxDepthValue + 1); - // Use the computed near distance only when it's less than our default distance. - if (near > nearDistance) { - near = nearDistance; - } + // The far distance is set to the smallest value that does not clip the atmosphere. + double far = eyeHorizon + atmosphereHorizon; + if (far < 1e3) far = 1e3; + + //The near distance is set to a large value that does not clip the globe's surface. + double maxDepthValue = (1L << this.depthBits) - 1L; + double farResolution = 10.0; + double near = far / (maxDepthValue / (1 - farResolution / far) - maxDepthValue + 1); + + // Prevent the near clip plane from intersecting the terrain. + double distanceToSurface = this.navigator.getAltitude() - this.globe.getElevationAtLocation(this.navigator.getLatitude(), this.navigator.getLongitude()) * this.getVerticalExaggeration(); + if (distanceToSurface > 0) { + double tanHalfFov = Math.tan(0.5 * Math.toRadians(this.fieldOfView)); + double maxNearDistance = distanceToSurface / (2 * Math.sqrt(2 * tanHalfFov * tanHalfFov + 1)); + if (near > maxNearDistance) near = maxNearDistance; } + if (near < 1) near = 1; + // Compute a perspective projection matrix given the WorldWindow's viewport, field of view, and clip distances. projection.setToPerspectiveProjection(this.viewport.width, this.viewport.height, this.fieldOfView, near, far); diff --git a/worldwind/src/main/java/gov/nasa/worldwind/geom/coords/Hemisphere.java b/worldwind/src/main/java/gov/nasa/worldwind/geom/coords/Hemisphere.java new file mode 100644 index 000000000..67b035103 --- /dev/null +++ b/worldwind/src/main/java/gov/nasa/worldwind/geom/coords/Hemisphere.java @@ -0,0 +1,5 @@ +package gov.nasa.worldwind.geom.coords; + +public enum Hemisphere { + N, S +} diff --git a/worldwind/src/main/java/gov/nasa/worldwind/geom/coords/MGRSCoord.java b/worldwind/src/main/java/gov/nasa/worldwind/geom/coords/MGRSCoord.java new file mode 100644 index 000000000..7fbba7691 --- /dev/null +++ b/worldwind/src/main/java/gov/nasa/worldwind/geom/coords/MGRSCoord.java @@ -0,0 +1,117 @@ +/* + * Copyright (C) 2011 United States Government as represented by the Administrator of the + * National Aeronautics and Space Administration. + * All Rights Reserved. + */ +package gov.nasa.worldwind.geom.coords; + +import android.support.annotation.NonNull; + +/** + * This class holds an immutable MGRS coordinate string along with + * the corresponding latitude and longitude. + * + * @author Patrick Murris + * @version $Id$ + */ + +public class MGRSCoord { + private final String MGRSString; + private final double latitude; + private final double longitude; + + /** + * Create a WGS84 MGRS coordinate from a pair of latitude and longitude double + * with the maximum precision of five digits (one meter). + * + * @param latitude the latitude double. + * @param longitude the longitude double. + * @return the corresponding MGRSCoord. + * @throws IllegalArgumentException if latitude or longitude is null, + * or the conversion to MGRS coordinates fails. + */ + public static MGRSCoord fromLatLon(double latitude, double longitude) { + return fromLatLon(latitude, longitude, 5); + } + + /** + * Create a MGRS coordinate from a pair of latitude and longitude double + * with the given precision or number of digits (1 to 5). + * + * @param latitude the latitude double. + * @param longitude the longitude double. + * @param precision the number of digits used for easting and northing (1 to 5). + * @return the corresponding MGRSCoord. + * @throws IllegalArgumentException if latitude or longitude is null, + * or the conversion to MGRS coordinates fails. + */ + public static MGRSCoord fromLatLon(double latitude, double longitude, int precision) { + final MGRSCoordConverter converter = new MGRSCoordConverter(); + long err = converter.convertGeodeticToMGRS(Math.toRadians(latitude), Math.toRadians(longitude), precision); + + if (err != MGRSCoordConverter.MGRS_NO_ERROR) { + throw new IllegalArgumentException("MGRS Conversion Error"); + } + + return new MGRSCoord(latitude, longitude, converter.getMGRSString()); + } + + /** + * Create a MGRS coordinate from a standard MGRS coordinate text string. + *

+ * The string will be converted to uppercase and stripped of all spaces before being evaluated. + *

+ *

Valid examples:
+ * 32TLP5626635418
+ * 32 T LP 56266 35418
+ * 11S KU 528 111
+ *

+ * @param MGRSString the MGRS coordinate text string. + * @return the corresponding MGRSCoord. + * @throws IllegalArgumentException if the MGRSString is null or empty, + * the globe is null, or the conversion to geodetic coordinates fails (invalid coordinate string). + */ + public static MGRSCoord fromString(String MGRSString) { + MGRSString = MGRSString.toUpperCase().replaceAll(" ", ""); + + final MGRSCoordConverter converter = new MGRSCoordConverter(); + long err = converter.convertMGRSToGeodetic(MGRSString); + + if (err != MGRSCoordConverter.MGRS_NO_ERROR) { + throw new IllegalArgumentException("MGRS Conversion Error"); + } + + return new MGRSCoord(Math.toDegrees(converter.getLatitude()), Math.toDegrees(converter.getLongitude()), MGRSString); + } + + /** + * Create an arbitrary MGRS coordinate from a pair of latitude-longitude double + * and the corresponding MGRS coordinate string. + * + * @param latitude the latitude double. + * @param longitude the longitude double. + * @param MGRSString the corresponding MGRS coordinate string. + * @throws IllegalArgumentException if latitude or longitude is null, + * or the MGRSString is null or empty. + */ + public MGRSCoord(double latitude, double longitude, String MGRSString) { + this.latitude = latitude; + this.longitude = longitude; + this.MGRSString = MGRSString; + } + + public double getLatitude() { + return this.latitude; + } + + public double getLongitude() { + return this.longitude; + } + + @NonNull + @Override + public String toString() { + return this.MGRSString; + } + +} diff --git a/worldwind/src/main/java/gov/nasa/worldwind/geom/coords/MGRSCoordConverter.java b/worldwind/src/main/java/gov/nasa/worldwind/geom/coords/MGRSCoordConverter.java new file mode 100644 index 000000000..97ce5ce88 --- /dev/null +++ b/worldwind/src/main/java/gov/nasa/worldwind/geom/coords/MGRSCoordConverter.java @@ -0,0 +1,937 @@ +/* + * Copyright (C) 2011 United States Government as represented by the Administrator of the + * National Aeronautics and Space Administration. + * All Rights Reserved. + */ +package gov.nasa.worldwind.geom.coords; + +/** + * Converter used to translate MGRS coordinate strings to and from geodetic latitude and longitude. + * + * @author Patrick Murris + * @version $Id$ + * @see MGRSCoord + */ + +import android.support.annotation.NonNull; + +/** + * Ported to Java from the NGA GeoTrans mgrs.c and mgrs.h code. Contains routines to convert from Geodetic to MGRS and + * the other direction. + * + * @author Garrett Headley, Patrick Murris + */ +class MGRSCoordConverter { + + public static final int MGRS_NO_ERROR = 0; + private static final int MGRS_LAT_ERROR = 0x0001; + private static final int MGRS_LON_ERROR = 0x0002; + public static final int MGRS_STRING_ERROR = 0x0004; + private static final int MGRS_PRECISION_ERROR = 0x0008; + private static final int MGRS_EASTING_ERROR = 0x0040; + private static final int MGRS_NORTHING_ERROR = 0x0080; + private static final int MGRS_ZONE_ERROR = 0x0100; + private static final int MGRS_HEMISPHERE_ERROR = 0x0200; + private static final int MGRS_LAT_WARNING = 0x0400; + private static final int MGRS_UTM_ERROR = 0x1000; + private static final int MGRS_UPS_ERROR = 0x2000; + + private static final double PI = 3.14159265358979323; + private static final double PI_OVER_2 = (PI / 2.0e0); + private static final int MAX_PRECISION = 5; + private static final double MIN_UTM_LAT = (-80 * PI) / 180.0; // -80 degrees in radians + private static final double MAX_UTM_LAT = (84 * PI) / 180.0; // 84 degrees in radians + public static final double DEG_TO_RAD = 0.017453292519943295; // PI/180 + private static final double RAD_TO_DEG = 57.29577951308232087; // 180/PI + + private static final double MIN_EAST_NORTH = 0; + private static final double MAX_EAST_NORTH = 4000000; + private static final double TWOMIL = 2000000; + private static final double ONEHT = 100000; + + private static final String CLARKE_1866 = "CC"; + private static final String CLARKE_1880 = "CD"; + private static final String BESSEL_1841 = "BR"; + private static final String BESSEL_1841_NAMIBIA = "BN"; + + private String MGRS_Ellipsoid_Code = "WE"; + + private String MGRSString = ""; + private long ltr2_low_value; + private long ltr2_high_value; // this is only used for doing MGRS to xxx conversions. + private double false_northing; + private long lastLetter; + private long last_error = MGRS_NO_ERROR; + private double north, south, min_northing, northing_offset; //smithjl added north_offset + private double latitude; + private double longitude; + + private static final int LETTER_A = 0; /* ARRAY INDEX FOR LETTER A */ + private static final int LETTER_B = 1; /* ARRAY INDEX FOR LETTER B */ + private static final int LETTER_C = 2; /* ARRAY INDEX FOR LETTER C */ + private static final int LETTER_D = 3; /* ARRAY INDEX FOR LETTER D */ + private static final int LETTER_E = 4; /* ARRAY INDEX FOR LETTER E */ + private static final int LETTER_F = 5; /* ARRAY INDEX FOR LETTER E */ + private static final int LETTER_G = 6; /* ARRAY INDEX FOR LETTER H */ + private static final int LETTER_H = 7; /* ARRAY INDEX FOR LETTER H */ + private static final int LETTER_I = 8; /* ARRAY INDEX FOR LETTER I */ + private static final int LETTER_J = 9; /* ARRAY INDEX FOR LETTER J */ + private static final int LETTER_K = 10; /* ARRAY INDEX FOR LETTER J */ + private static final int LETTER_L = 11; /* ARRAY INDEX FOR LETTER L */ + private static final int LETTER_M = 12; /* ARRAY INDEX FOR LETTER M */ + private static final int LETTER_N = 13; /* ARRAY INDEX FOR LETTER N */ + private static final int LETTER_O = 14; /* ARRAY INDEX FOR LETTER O */ + private static final int LETTER_P = 15; /* ARRAY INDEX FOR LETTER P */ + private static final int LETTER_Q = 16; /* ARRAY INDEX FOR LETTER Q */ + private static final int LETTER_R = 17; /* ARRAY INDEX FOR LETTER R */ + private static final int LETTER_S = 18; /* ARRAY INDEX FOR LETTER S */ + private static final int LETTER_T = 19; /* ARRAY INDEX FOR LETTER S */ + private static final int LETTER_U = 20; /* ARRAY INDEX FOR LETTER U */ + private static final int LETTER_V = 21; /* ARRAY INDEX FOR LETTER V */ + private static final int LETTER_W = 22; /* ARRAY INDEX FOR LETTER W */ + private static final int LETTER_X = 23; /* ARRAY INDEX FOR LETTER X */ + private static final int LETTER_Y = 24; /* ARRAY INDEX FOR LETTER Y */ + private static final int LETTER_Z = 25; /* ARRAY INDEX FOR LETTER Z */ + private static final int MGRS_LETTERS = 3; /* NUMBER OF LETTERS IN MGRS */ + + private static final String alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; + + // UPS Constants are in the following order: + // long letter; /* letter representing latitude band */ + // long ltr2_low_value; /* 2nd letter range - high number */ + // long ltr2_high_value; /* 2nd letter range - low number */ + // long ltr3_high_value; /* 3rd letter range - high number (UPS) */ + // double false_easting; /* False easting based on 2nd letter */ + // double false_northing; /* False northing based on 3rd letter */ + private static final long[][] upsConstants = { + {LETTER_A, LETTER_J, LETTER_Z, LETTER_Z, 800000, 800000}, + {LETTER_B, LETTER_A, LETTER_R, LETTER_Z, 2000000, 800000}, + {LETTER_Y, LETTER_J, LETTER_Z, LETTER_P, 800000, 1300000}, + {LETTER_Z, LETTER_A, LETTER_J, LETTER_P, 2000000, 1300000}}; + + // Latitude Band Constants are in the following order: + // long letter; /* letter representing latitude band */ + // double min_northing; /* minimum northing for latitude band */ + // double north; /* upper latitude for latitude band */ + // double south; /* lower latitude for latitude band */ + private static final double[][] latitudeBandConstants = { + {LETTER_C, 1100000.0, -72.0, -80.5, 0.0}, + {LETTER_D, 2000000.0, -64.0, -72.0, 2000000.0}, + {LETTER_E, 2800000.0, -56.0, -64.0, 2000000.0}, + {LETTER_F, 3700000.0, -48.0, -56.0, 2000000.0}, + {LETTER_G, 4600000.0, -40.0, -48.0, 4000000.0}, + {LETTER_H, 5500000.0, -32.0, -40.0, 4000000.0}, //smithjl last column to table + {LETTER_J, 6400000.0, -24.0, -32.0, 6000000.0}, + {LETTER_K, 7300000.0, -16.0, -24.0, 6000000.0}, + {LETTER_L, 8200000.0, -8.0, -16.0, 8000000.0}, + {LETTER_M, 9100000.0, 0.0, -8.0, 8000000.0}, + {LETTER_N, 0.0, 8.0, 0.0, 0.0}, + {LETTER_P, 800000.0, 16.0, 8.0, 0.0}, + {LETTER_Q, 1700000.0, 24.0, 16.0, 0.0}, + {LETTER_R, 2600000.0, 32.0, 24.0, 2000000.0}, + {LETTER_S, 3500000.0, 40.0, 32.0, 2000000.0}, + {LETTER_T, 4400000.0, 48.0, 40.0, 4000000.0}, + {LETTER_U, 5300000.0, 56.0, 48.0, 4000000.0}, + {LETTER_V, 6200000.0, 64.0, 56.0, 6000000.0}, + {LETTER_W, 7000000.0, 72.0, 64.0, 6000000.0}, + {LETTER_X, 7900000.0, 84.5, 72.0, 6000000.0}}; + + private class MGRSComponents { + private final int zone; + private final int latitudeBand; + private final int squareLetter1; + private final int squareLetter2; + private final double easting; + private final double northing; + private final int precision; + + public MGRSComponents(int zone, int latitudeBand, int squareLetter1, int squareLetter2, + double easting, double northing, int precision) { + this.zone = zone; + this.latitudeBand = latitudeBand; + this.squareLetter1 = squareLetter1; + this.squareLetter2 = squareLetter2; + this.easting = easting; + this.northing = northing; + this.precision = precision; + } + + @Override + @NonNull + public String toString() { + return "MGRS: " + zone + " " + + alphabet.charAt(latitudeBand) + " " + + alphabet.charAt(squareLetter1) + alphabet.charAt(squareLetter2) + " " + + easting + " " + + northing + " " + + "(" + precision + ")"; + } + } + + MGRSCoordConverter(){} + + /** @return Latitude band letter */ + private long getLastLetter() { + return lastLetter; + } + + /** + * The function ConvertMGRSToGeodetic converts an MGRS coordinate string to Geodetic (latitude and longitude) + * coordinates according to the current ellipsoid parameters. If any errors occur, the error code(s) are returned + * by the function, otherwise UTM_NO_ERROR is returned. + * + * @param MGRSString MGRS coordinate string. + * + * @return the error code. + */ + public long convertMGRSToGeodetic(String MGRSString) { + latitude = 0; + longitude = 0; + MGRSComponents mgrs = breakMGRSString(MGRSString); + if (mgrs == null) { + return last_error; + } + long error_code = MGRS_NO_ERROR; + if (mgrs.zone != 0) { + UTMCoord UTM = convertMGRSToUTM(MGRSString); + if (UTM != null) { + latitude = Math.toRadians(UTM.getLatitude()); + longitude = Math.toRadians(UTM.getLongitude()); + } else + error_code = MGRS_UTM_ERROR; + } else { + UPSCoord UPS = convertMGRSToUPS(MGRSString); + if (UPS != null) { + latitude = Math.toRadians(UPS.getLatitude()); + longitude = Math.toRadians(UPS.getLongitude()); + } else + error_code = MGRS_UPS_ERROR; + } + return (error_code); + } + + public double getLatitude() { + return latitude; + } + + public double getLongitude() { + return longitude; + } + + /** + * The function Break_MGRS_String breaks down an MGRS coordinate string into its component parts. Updates + * last_error. + * + * @param MGRSString the MGRS coordinate string + * + * @return the corresponding MGRSComponents or null. + */ + private MGRSComponents breakMGRSString(String MGRSString) { + int num_digits; + int num_letters; + int i = 0; + int j = 0; + long error_code = MGRS_NO_ERROR; + + int zone = 0; + int[] letters = new int[3]; + long easting = 0; + long northing = 0; + int precision = 0; + + MGRSString = MGRSString.toUpperCase().replaceAll("\\s", ""); + j = i; + while (i < MGRSString.length() && Character.isDigit(MGRSString.charAt(i))) { + i++; + } + num_digits = i - j; + if (num_digits <= 2) { + if (num_digits > 0) { + /* get zone */ + zone = Integer.parseInt(MGRSString.substring(j, i)); + if ((zone < 1) || (zone > 60)) { + error_code |= MGRS_STRING_ERROR; + } + } else { + zone = 0; + } + } + j = i; + + while (i < MGRSString.length() && Character.isLetter(MGRSString.charAt(i))) { + i++; + } + num_letters = i - j; + if (num_letters == 3) { + /* get letters */ + letters[0] = alphabet.indexOf(Character.toUpperCase(MGRSString.charAt(j))); + if ((letters[0] == LETTER_I) || (letters[0] == LETTER_O)) + error_code |= MGRS_STRING_ERROR; + letters[1] = alphabet.indexOf(Character.toUpperCase(MGRSString.charAt(j + 1))); + if ((letters[1] == LETTER_I) || (letters[1] == LETTER_O)) + error_code |= MGRS_STRING_ERROR; + letters[2] = alphabet.indexOf(Character.toUpperCase(MGRSString.charAt(j + 2))); + if ((letters[2] == LETTER_I) || (letters[2] == LETTER_O)) + error_code |= MGRS_STRING_ERROR; + } else + error_code |= MGRS_STRING_ERROR; + j = i; + while (i < MGRSString.length() && Character.isDigit(MGRSString.charAt(i))) { + i++; + } + num_digits = i - j; + if ((num_digits <= 10) && (num_digits % 2 == 0)) { + /* get easting, northing and precision */ + int n; + double multiplier; + /* get easting & northing */ + n = num_digits / 2; + precision = n; + if (n > 0) { + easting = Integer.parseInt(MGRSString.substring(j, j + n)); + northing = Integer.parseInt(MGRSString.substring(j + n, j + n + n)); + multiplier = Math.pow(10.0, 5 - n); + easting *= multiplier; + northing *= multiplier; + } else { + easting = 0; + northing = 0; + } + } else + error_code |= MGRS_STRING_ERROR; + + last_error = error_code; + if (error_code == MGRS_NO_ERROR) + return new MGRSComponents(zone, letters[0], letters[1], letters[2], easting, northing, precision); + + return null; + } + + /** + * The function Get_Latitude_Band_Min_Northing receives a latitude band letter and uses the Latitude_Band_Table to + * determine the minimum northing for that latitude band letter. Updates min_northing. + * + * @param letter Latitude band letter. + * + * @return the error code. + */ + private long getLatitudeBandMinNorthing(int letter) { + long error_code = MGRS_NO_ERROR; + + if ((letter >= LETTER_C) && (letter <= LETTER_H)) { + min_northing = latitudeBandConstants[letter - 2][1]; + northing_offset = latitudeBandConstants[letter - 2][4]; //smithjl + } else if ((letter >= LETTER_J) && (letter <= LETTER_N)) { + min_northing = latitudeBandConstants[letter - 3][1]; + northing_offset = latitudeBandConstants[letter - 3][4]; //smithjl + } else if ((letter >= LETTER_P) && (letter <= LETTER_X)) { + min_northing = latitudeBandConstants[letter - 4][1]; + northing_offset = latitudeBandConstants[letter - 4][4]; //smithjl + } else + error_code |= MGRS_STRING_ERROR; + return error_code; + } + + /** + * The function Get_Latitude_Range receives a latitude band letter and uses the Latitude_Band_Table to determine the + * latitude band boundaries for that latitude band letter. Updates north and south. + * + * @param letter the Latitude band letter + * + * @return the error code. + */ + private long getLatitudeRange(int letter) { + long error_code = MGRS_NO_ERROR; + + if ((letter >= LETTER_C) && (letter <= LETTER_H)) { + north = latitudeBandConstants[letter - 2][2] * DEG_TO_RAD; + south = latitudeBandConstants[letter - 2][3] * DEG_TO_RAD; + } else if ((letter >= LETTER_J) && (letter <= LETTER_N)) { + north = latitudeBandConstants[letter - 3][2] * DEG_TO_RAD; + south = latitudeBandConstants[letter - 3][3] * DEG_TO_RAD; + } else if ((letter >= LETTER_P) && (letter <= LETTER_X)) { + north = latitudeBandConstants[letter - 4][2] * DEG_TO_RAD; + south = latitudeBandConstants[letter - 4][3] * DEG_TO_RAD; + } else + error_code |= MGRS_STRING_ERROR; + + return error_code; + } + + /** + * The function convertMGRSToUTM converts an MGRS coordinate string to UTM projection (zone, hemisphere, easting and + * northing) coordinates according to the current ellipsoid parameters. Updates last_error if any errors occured. + * + * @param MGRSString the MGRS coordinate string + * + * @return the corresponding UTMComponents or null. + */ + private UTMCoord convertMGRSToUTM(String MGRSString) { + double grid_easting; /* Easting for 100,000 meter grid square */ + double grid_northing; /* Northing for 100,000 meter grid square */ + double latitude; + double divisor; + long error_code = MGRS_NO_ERROR; + + Hemisphere hemisphere; + double easting; + double northing; + UTMCoord UTM = null; + + MGRSComponents MGRS = breakMGRSString(MGRSString); + if (MGRS == null) + error_code |= MGRS_STRING_ERROR; + else { + if ((MGRS.latitudeBand == LETTER_X) && ((MGRS.zone == 32) || (MGRS.zone == 34) || (MGRS.zone == 36))) + error_code |= MGRS_STRING_ERROR; + else { + if (MGRS.latitudeBand < LETTER_N) + hemisphere = Hemisphere.S; + else + hemisphere = Hemisphere.N; + + getGridValues(MGRS.zone); + + // Check that the second letter of the MGRS string is within + // the range of valid second letter values + // Also check that the third letter is valid + if ((MGRS.squareLetter1 < ltr2_low_value) || (MGRS.squareLetter1 > ltr2_high_value) || + (MGRS.squareLetter2 > LETTER_V)) + error_code |= MGRS_STRING_ERROR; + + if (error_code == MGRS_NO_ERROR) { + grid_northing = (MGRS.squareLetter2) * ONEHT; // smithjl commented out + false_northing; + grid_easting = ((MGRS.squareLetter1) - ltr2_low_value + 1) * ONEHT; + if ((ltr2_low_value == LETTER_J) && (MGRS.squareLetter1 > LETTER_O)) + grid_easting = grid_easting - ONEHT; + + if (MGRS.squareLetter2 > LETTER_O) + grid_northing = grid_northing - ONEHT; + + if (MGRS.squareLetter2 > LETTER_I) + grid_northing = grid_northing - ONEHT; + + if (grid_northing >= TWOMIL) + grid_northing = grid_northing - TWOMIL; + + error_code = getLatitudeBandMinNorthing(MGRS.latitudeBand); + if (error_code == MGRS_NO_ERROR) { + /*smithjl Deleted code here and added this*/ + grid_northing = grid_northing - false_northing; + + if (grid_northing < 0.0) + grid_northing += TWOMIL; + + grid_northing += northing_offset; + + if (grid_northing < min_northing) + grid_northing += TWOMIL; + + /* smithjl End of added code */ + + easting = grid_easting + MGRS.easting; + northing = grid_northing + MGRS.northing; + + try { + UTM = UTMCoord.fromUTM(MGRS.zone, hemisphere, easting, northing); + latitude = Math.toRadians(UTM.getLatitude()); + divisor = Math.pow(10.0, MGRS.precision); + error_code = getLatitudeRange(MGRS.latitudeBand); + if (error_code == MGRS_NO_ERROR) { + if (!(((south - DEG_TO_RAD / divisor) <= latitude) + && (latitude <= (north + DEG_TO_RAD / divisor)))) + error_code |= MGRS_LAT_WARNING; + } + } catch (Exception e) { + error_code = MGRS_UTM_ERROR; + } + } + } + } + } + + last_error = error_code; + if (error_code == MGRS_NO_ERROR || error_code == MGRS_LAT_WARNING) + return UTM; + + return null; + } /* Convert_MGRS_To_UTM */ + + /** + * The function convertGeodeticToMGRS converts Geodetic (latitude and longitude) coordinates to an MGRS coordinate + * string, according to the current ellipsoid parameters. If any errors occur, the error code(s) are returned by + * the function, otherwise MGRS_NO_ERROR is returned. + * + * @param latitude Latitude in radians + * @param longitude Longitude in radian + * @param precision Precision level of MGRS string + * + * @return error code + */ + public long convertGeodeticToMGRS(double latitude, double longitude, int precision) { + MGRSString = ""; + + long error_code = MGRS_NO_ERROR; + if ((latitude < -PI_OVER_2) || (latitude > PI_OVER_2)) { /* Latitude out of range */ + error_code = MGRS_LAT_ERROR; + } + + if ((longitude < -PI) || (longitude > (2 * PI))) { /* Longitude out of range */ + error_code = MGRS_LON_ERROR; + } + + if ((precision < 0) || (precision > MAX_PRECISION)) + error_code = MGRS_PRECISION_ERROR; + + if (error_code == MGRS_NO_ERROR) { + if ((latitude < MIN_UTM_LAT) || (latitude > MAX_UTM_LAT)) { + try { + UPSCoord UPS = + UPSCoord.fromLatLon(Math.toDegrees(latitude), Math.toDegrees(longitude)); + error_code |= convertUPSToMGRS(UPS.getHemisphere(), UPS.getEasting(), + UPS.getNorthing(), precision); + } catch (Exception e) { + error_code = MGRS_UPS_ERROR; + } + } else { + try { + UTMCoord UTM = + UTMCoord.fromLatLon(Math.toDegrees(latitude), Math.toDegrees(longitude)); + error_code |= convertUTMToMGRS(UTM.getZone(), latitude, UTM.getEasting(), + UTM.getNorthing(), precision); + } catch (Exception e) { + error_code = MGRS_UTM_ERROR; + } + } + } + + return error_code; + } + + /** @return converted MGRS string */ + public String getMGRSString() { + return MGRSString; + } + + /** + * The function Convert_UPS_To_MGRS converts UPS (hemisphere, easting, and northing) coordinates to an MGRS + * coordinate string according to the current ellipsoid parameters. If any errors occur, the error code(s) are + * returned by the function, otherwise MGRS_NO_ERROR is returned. + * + * @param hemisphere hemisphere either {@link Hemisphere#N} of {@link Hemisphere#S}. + * @param easting easting/X in meters + * @param northing northing/Y in meters + * @param precision precision level of MGRS string + * + * @return error value + */ + private long convertUPSToMGRS(Hemisphere hemisphere, Double easting, Double northing, long precision) { + double false_easting; /* False easting for 2nd letter */ + double false_northing; /* False northing for 3rd letter */ + double grid_easting; /* easting used to derive 2nd letter of MGRS */ + double grid_northing; /* northing used to derive 3rd letter of MGRS */ + int ltr2_low_value; /* 2nd letter range - low number */ + long[] letters = new long[MGRS_LETTERS]; /* Number location of 3 letters in alphabet */ + double divisor; + int index; + long error_code = MGRS_NO_ERROR; + + if (!Hemisphere.N.equals(hemisphere) && !Hemisphere.S.equals(hemisphere)) + error_code |= MGRS_HEMISPHERE_ERROR; + if ((easting < MIN_EAST_NORTH) || (easting > MAX_EAST_NORTH)) + error_code |= MGRS_EASTING_ERROR; + if ((northing < MIN_EAST_NORTH) || (northing > MAX_EAST_NORTH)) + error_code |= MGRS_NORTHING_ERROR; + if ((precision < 0) || (precision > MAX_PRECISION)) + error_code |= MGRS_PRECISION_ERROR; + + if (error_code == MGRS_NO_ERROR) { + divisor = Math.pow(10.0, (5 - precision)); + easting = roundMGRS(easting / divisor) * divisor; + northing = roundMGRS(northing / divisor) * divisor; + + if (Hemisphere.N.equals(hemisphere)) { + if (easting >= TWOMIL) + letters[0] = LETTER_Z; + else + letters[0] = LETTER_Y; + + index = (int) letters[0] - 22; +// ltr2_low_value = UPS_Constant_Table.get(index).ltr2_low_value; +// false_easting = UPS_Constant_Table.get(index).false_easting; +// false_northing = UPS_Constant_Table.get(index).false_northing; + ltr2_low_value = (int) upsConstants[index][1]; + false_easting = upsConstants[index][4]; + false_northing = upsConstants[index][5]; + } else { + if (easting >= TWOMIL) + letters[0] = LETTER_B; + else + letters[0] = LETTER_A; + +// ltr2_low_value = UPS_Constant_Table.get((int) letters[0]).ltr2_low_value; +// false_easting = UPS_Constant_Table.get((int) letters[0]).false_easting; +// false_northing = UPS_Constant_Table.get((int) letters[0]).false_northing; + ltr2_low_value = (int) upsConstants[(int) letters[0]][1]; + false_easting = upsConstants[(int) letters[0]][4]; + false_northing = upsConstants[(int) letters[0]][5]; + } + + grid_northing = northing; + grid_northing = grid_northing - false_northing; + letters[2] = (int) (grid_northing / ONEHT); + + if (letters[2] > LETTER_H) + letters[2] = letters[2] + 1; + + if (letters[2] > LETTER_N) + letters[2] = letters[2] + 1; + + grid_easting = easting; + grid_easting = grid_easting - false_easting; + letters[1] = ltr2_low_value + ((int) (grid_easting / ONEHT)); + + if (easting < TWOMIL) { + if (letters[1] > LETTER_L) + letters[1] = letters[1] + 3; + + if (letters[1] > LETTER_U) + letters[1] = letters[1] + 2; + } else { + if (letters[1] > LETTER_C) + letters[1] = letters[1] + 2; + + if (letters[1] > LETTER_H) + letters[1] = letters[1] + 1; + + if (letters[1] > LETTER_L) + letters[1] = letters[1] + 3; + } + + makeMGRSString(0, letters, easting, northing, precision); + } + return (error_code); + } + + /** + * The function UTM_To_MGRS calculates an MGRS coordinate string based on the zone, latitude, easting and northing. + * + * @param Zone Zone number + * @param Latitude Latitude in radians + * @param Easting Easting + * @param Northing Northing + * @param Precision Precision + * + * @return error code + */ + private long convertUTMToMGRS(long Zone, double Latitude, double Easting, double Northing, long Precision) { + double grid_easting; /* Easting used to derive 2nd letter of MGRS */ + double grid_northing; /* Northing used to derive 3rd letter of MGRS */ + long[] letters = new long[MGRS_LETTERS]; /* Number location of 3 letters in alphabet */ + double divisor; + long error_code; + + /* Round easting and northing values */ + divisor = Math.pow(10.0, (5 - Precision)); + Easting = roundMGRS(Easting / divisor) * divisor; + Northing = roundMGRS(Northing / divisor) * divisor; + + getGridValues(Zone); + + error_code = getLatitudeLetter(Latitude); + letters[0] = getLastLetter(); + + if (error_code == MGRS_NO_ERROR) { + grid_northing = Northing; + if (grid_northing == 1.e7) + grid_northing = grid_northing - 1.0; + + while (grid_northing >= TWOMIL) { + grid_northing = grid_northing - TWOMIL; + } + grid_northing = grid_northing + false_northing; //smithjl + + if (grid_northing >= TWOMIL) //smithjl + grid_northing = grid_northing - TWOMIL; //smithjl + + letters[2] = (long) (grid_northing / ONEHT); + if (letters[2] > LETTER_H) + letters[2] = letters[2] + 1; + + if (letters[2] > LETTER_N) + letters[2] = letters[2] + 1; + + grid_easting = Easting; + if (((letters[0] == LETTER_V) && (Zone == 31)) && (grid_easting == 500000.0)) + grid_easting = grid_easting - 1.0; /* SUBTRACT 1 METER */ + + letters[1] = ltr2_low_value + ((long) (grid_easting / ONEHT) - 1); + if ((ltr2_low_value == LETTER_J) && (letters[1] > LETTER_N)) + letters[1] = letters[1] + 1; + + makeMGRSString(Zone, letters, Easting, Northing, Precision); + } + return error_code; + } + + /** + * The function Get_Grid_Values sets the letter range used for the 2nd letter in the MGRS coordinate string, based + * on the set number of the utm zone. It also sets the false northing using a value of A for the second letter of + * the grid square, based on the grid pattern and set number of the utm zone. + *

+ * Key values that are set in this function include: ltr2_low_value, ltr2_high_value, and false_northing. + * + * @param zone Zone number + */ + private void getGridValues(long zone) { + long set_number; /* Set number (1-6) based on UTM zone number */ + long aa_pattern; /* Pattern based on ellipsoid code */ + + set_number = zone % 6; + + if (set_number == 0) + set_number = 6; + + if (MGRS_Ellipsoid_Code.compareTo(CLARKE_1866) == 0 || MGRS_Ellipsoid_Code.compareTo(CLARKE_1880) == 0 || + MGRS_Ellipsoid_Code.compareTo(BESSEL_1841) == 0 || MGRS_Ellipsoid_Code.compareTo(BESSEL_1841_NAMIBIA) == 0) + aa_pattern = 0L; + else + aa_pattern = 1L; + + if ((set_number == 1) || (set_number == 4)) { + ltr2_low_value = LETTER_A; + ltr2_high_value = LETTER_H; + } else if ((set_number == 2) || (set_number == 5)) { + ltr2_low_value = LETTER_J; + ltr2_high_value = LETTER_R; + } else if ((set_number == 3) || (set_number == 6)) { + ltr2_low_value = LETTER_S; + ltr2_high_value = LETTER_Z; + } + + /* False northing at A for second letter of grid square */ + if (aa_pattern == 1L) { + if ((set_number % 2) == 0) + false_northing = 500000.0; //smithjl was 1500000 + else + false_northing = 0.0; + } else { + if ((set_number % 2) == 0) + false_northing = 1500000.0; //smithjl was 500000 + else + false_northing = 1000000.00; + } + } + + /** + * The function Get_Latitude_Letter receives a latitude value and uses the Latitude_Band_Table to determine the + * latitude band letter for that latitude. + * + * @param latitude latitude to turn into code + * + * @return error code + */ + private long getLatitudeLetter(double latitude) { + double temp; + long error_code = MGRS_NO_ERROR; + double lat_deg = latitude * RAD_TO_DEG; + + if (lat_deg >= 72 && lat_deg < 84.5) + lastLetter = LETTER_X; + else if (lat_deg > -80.5 && lat_deg < 72) { + temp = ((latitude + (80.0 * DEG_TO_RAD)) / (8.0 * DEG_TO_RAD)) + 1.0e-12; + // lastLetter = Latitude_Band_Table.get((int) temp).letter; + lastLetter = (long) latitudeBandConstants[(int) temp][0]; + } else + error_code |= MGRS_LAT_ERROR; + + return error_code; + } + + /** + * The function Round_MGRS rounds the input value to the nearest integer, using the standard engineering rule. The + * rounded integer value is then returned. + * + * @param value Value to be rounded + * + * @return rounded double value + */ + private double roundMGRS(double value) { + double ivalue = Math.floor(value); + long ival; + double fraction = value - ivalue; + // double fraction = modf (value, &ivalue); + + ival = (long) (ivalue); + if ((fraction > 0.5) || ((fraction == 0.5) && (ival % 2 == 1))) + ival++; + return ival; + } + + /** + * The function Make_MGRS_String constructs an MGRS string from its component parts. + * + * @param Zone UTM Zone + * @param Letters MGRS coordinate string letters + * @param Easting Easting value + * @param Northing Northing value + * @param Precision Precision level of MGRS string + */ + private void makeMGRSString(long Zone, long[] Letters, double Easting, double Northing, long Precision) { + int j; + double divisor; + long east; + long north; + + if (Zone != 0) + MGRSString = String.format("%02d", Zone); + else + MGRSString = " "; + + for (j = 0; j < 3; j++) { + if (Letters[j] < 0 || Letters[j] > 26) + return; + MGRSString = MGRSString + alphabet.charAt((int) Letters[j]); + } + + divisor = Math.pow(10.0, (5 - Precision)); + Easting = Easting % 100000.0; + if (Easting >= 99999.5) + Easting = 99999.0; + east = (long) (Easting / divisor); + + // Here we need to only use the number requesting in the precision + Integer iEast = (int) east; + StringBuilder sEast = new StringBuilder(iEast.toString()); + if (sEast.length() > Precision) + sEast = new StringBuilder(sEast.substring(0, (int) Precision - 1)); + else { + int i; + int length = sEast.length(); + for (i = 0; i < Precision - length; i++) { + sEast.insert(0, "0"); + } + } + MGRSString = MGRSString + " " + sEast; + + Northing = Northing % 100000.0; + if (Northing >= 99999.5) + Northing = 99999.0; + north = (long) (Northing / divisor); + + Integer iNorth = (int) north; + StringBuilder sNorth = new StringBuilder(iNorth.toString()); + if (sNorth.length() > Precision) + sNorth = new StringBuilder(sNorth.substring(0, (int) Precision - 1)); + else + { + int i; + int length = sNorth.length(); + for (i = 0; i < Precision - length; i++) + { + sNorth.insert(0, "0"); + } + } + MGRSString = MGRSString + " " + sNorth; + } + + /** + * The function Convert_MGRS_To_UPS converts an MGRS coordinate string to UPS (hemisphere, easting, and northing) + * coordinates, according to the current ellipsoid parameters. If any errors occur, the error code(s) are returned + * by the function, otherwise UPS_NO_ERROR is returned. + * + * @param MGRS the MGRS coordinate string. + * + * @return a corresponding {@link UPSCoord} instance. + */ + private UPSCoord convertMGRSToUPS(String MGRS) { + long ltr2_high_value; /* 2nd letter range - high number */ + long ltr3_high_value; /* 3rd letter range - high number (UPS) */ + long ltr2_low_value; /* 2nd letter range - low number */ + double false_easting; /* False easting for 2nd letter */ + double false_northing; /* False northing for 3rd letter */ + double grid_easting; /* easting for 100,000 meter grid square */ + double grid_northing; /* northing for 100,000 meter grid square */ + int index = 0; + long error_code = MGRS_NO_ERROR; + + Hemisphere hemisphere; + double easting, northing; + + MGRSComponents mgrs = breakMGRSString(MGRS); + if (mgrs == null) { + error_code = this.last_error; + } else { + if (mgrs.zone > 0) { + error_code |= MGRS_STRING_ERROR; + } + + if (error_code == MGRS_NO_ERROR) { + easting = mgrs.easting; + northing = mgrs.northing; + + if (mgrs.latitudeBand >= LETTER_Y) { + hemisphere = Hemisphere.N; + + index = mgrs.latitudeBand - 22; + ltr2_low_value = upsConstants[index][1]; //.ltr2_low_value; + ltr2_high_value = upsConstants[index][2]; //.ltr2_high_value; + ltr3_high_value = upsConstants[index][3]; //.ltr3_high_value; + false_easting = upsConstants[index][4]; //.false_easting; + false_northing = upsConstants[index][5]; //.false_northing; + } else { + hemisphere = Hemisphere.S; + + ltr2_low_value = upsConstants[mgrs.latitudeBand][1]; //.ltr2_low_value; + ltr2_high_value = upsConstants[mgrs.latitudeBand][2]; //.ltr2_high_value; + ltr3_high_value = upsConstants[mgrs.latitudeBand][3]; //.ltr3_high_value; + false_easting = upsConstants[mgrs.latitudeBand][4]; //.false_easting; + false_northing = upsConstants[mgrs.latitudeBand][5]; //.false_northing; + } + + // Check that the second letter of the MGRS string is within + // the range of valid second letter values + // Also check that the third letter is valid + if ((mgrs.squareLetter1 < ltr2_low_value) || (mgrs.squareLetter1 > ltr2_high_value) || + ((mgrs.squareLetter1 == LETTER_D) || (mgrs.squareLetter1 == LETTER_E) || + (mgrs.squareLetter1 == LETTER_M) || (mgrs.squareLetter1 == LETTER_N) || + (mgrs.squareLetter1 == LETTER_V) || (mgrs.squareLetter1 == LETTER_W)) || + (mgrs.squareLetter2 > ltr3_high_value)) + error_code = MGRS_STRING_ERROR; + + if (error_code == MGRS_NO_ERROR) { + grid_northing = mgrs.squareLetter2 * ONEHT + false_northing; + if (mgrs.squareLetter2 > LETTER_I) + grid_northing = grid_northing - ONEHT; + + if (mgrs.squareLetter2 > LETTER_O) + grid_northing = grid_northing - ONEHT; + + grid_easting = ((mgrs.squareLetter1) - ltr2_low_value) * ONEHT + false_easting; + if (ltr2_low_value != LETTER_A) { + if (mgrs.squareLetter1 > LETTER_L) + grid_easting = grid_easting - 300000.0; + + if (mgrs.squareLetter1 > LETTER_U) + grid_easting = grid_easting - 200000.0; + } else { + if (mgrs.squareLetter1 > LETTER_C) + grid_easting = grid_easting - 200000.0; + + if (mgrs.squareLetter1 > LETTER_I) + grid_easting = grid_easting - ONEHT; + + if (mgrs.squareLetter1 > LETTER_L) + grid_easting = grid_easting - 300000.0; + } + + easting = grid_easting + easting; + northing = grid_northing + northing; + return UPSCoord.fromUPS(hemisphere, easting, northing); + } + } + } + + return null; + } +} diff --git a/worldwind/src/main/java/gov/nasa/worldwind/geom/coords/PolarCoordConverter.java b/worldwind/src/main/java/gov/nasa/worldwind/geom/coords/PolarCoordConverter.java new file mode 100644 index 000000000..4cc47f92e --- /dev/null +++ b/worldwind/src/main/java/gov/nasa/worldwind/geom/coords/PolarCoordConverter.java @@ -0,0 +1,432 @@ +/* + * Copyright (C) 2011 United States Government as represented by the Administrator of the + * National Aeronautics and Space Administration. + * All Rights Reserved. + */ + +/* RSC IDENTIFIER: POLAR STEREOGRAPHIC + * + * + * ABSTRACT + * + * This component provides conversions between geodetic (latitude and + * longitude) coordinates and Polar Stereographic (easting and northing) + * coordinates. + * + * ERROR HANDLING + * + * This component checks parameters for valid values. If an invalid + * value is found the error code is combined with the current error code + * using the bitwise or. This combining allows multiple error codes to + * be returned. The possible error codes are: + * + * POLAR_NO_ERROR : No errors occurred in function + * POLAR_LAT_ERROR : Latitude outside of valid range + * (-90 to 90 degrees) + * POLAR_LON_ERROR : Longitude outside of valid range + * (-180 to 360 degrees) + * POLAR_ORIGIN_LAT_ERROR : Latitude of true scale outside of valid + * range (-90 to 90 degrees) + * POLAR_ORIGIN_LON_ERROR : Longitude down from pole outside of valid + * range (-180 to 360 degrees) + * POLAR_EASTING_ERROR : Easting outside of valid range, + * depending on ellipsoid and + * projection parameters + * POLAR_NORTHING_ERROR : Northing outside of valid range, + * depending on ellipsoid and + * projection parameters + * POLAR_RADIUS_ERROR : Coordinates too far from pole, + * depending on ellipsoid and + * projection parameters + * POLAR_A_ERROR : Semi-major axis less than or equal to zero + * POLAR_INV_F_ERROR : Inverse flattening outside of valid range + * (250 to 350) + * + * + * REUSE NOTES + * + * POLAR STEREOGRAPHIC is intended for reuse by any application that + * performs a Polar Stereographic projection. + * + * + * REFERENCES + * + * Further information on POLAR STEREOGRAPHIC can be found in the + * Reuse Manual. + * + * + * POLAR STEREOGRAPHIC originated from : + * U.S. Army Topographic Engineering Center + * Geospatial Information Division + * 7701 Telegraph Road + * Alexandria, VA 22310-3864 + * + * + * LICENSES + * + * None apply to this component. + * + * + * RESTRICTIONS + * + * POLAR STEREOGRAPHIC has no restrictions. + * + * + * ENVIRONMENT + * + * POLAR STEREOGRAPHIC was tested and certified in the following + * environments: + * + * 1. Solaris 2.5 with GCC, version 2.8.1 + * 2. Window 95 with MS Visual C++, version 6 + * + * + * MODIFICATIONS + * + * Date Description + * ---- ----------- + * 06-11-95 Original Code + * 03-01-97 Original Code + * + * + */ + +package gov.nasa.worldwind.geom.coords; + +/** + * Ported to Java from the NGA GeoTrans polarst.c and polarst.h code. + * + * @author Garrett Headley - Feb 12, 2007 4:48:11 PM + * @version $Id$ + */ +public class PolarCoordConverter { + + private static final long POLAR_NO_ERROR = 0x0000; + private static final long POLAR_LAT_ERROR = 0x0001; + private static final long POLAR_LON_ERROR = 0x0002; + private static final long POLAR_ORIGIN_LAT_ERROR = 0x0004; + private static final long POLAR_ORIGIN_LON_ERROR = 0x0008; + public static final long POLAR_EASTING_ERROR = 0x0010; + public static final long POLAR_NORTHING_ERROR = 0x0020; + private static final long POLAR_A_ERROR = 0x0040; + private static final long POLAR_INV_F_ERROR = 0x0080; + public static final long POLAR_RADIUS_ERROR = 0x0100; + + private static final double PI = 3.14159265358979323; + private static final double PI_OVER_2 = PI / 2.0; + private static final double PI_Over_4 = PI / 4.0; + private static final double TWO_PI = 2.0 * PI; + + /* Ellipsoid Parameters, default to WGS 84 */ + private double Polar_a = 6378137.0; /* Semi-major axis of ellipsoid in meters */ + private double Polar_f = 1 / 298.257223563; /* Flattening of ellipsoid */ + private double es = 0.08181919084262188000; /* Eccentricity of ellipsoid */ + private double es_OVER_2 = .040909595421311; /* es / 2.0 */ + private double Southern_Hemisphere = 0; /* Flag variable */ + private double mc = 1.0; + private double tc = 1.0; + private double e4 = 1.0033565552493; + private double Polar_a_mc = 6378137.0; /* Polar_a * mc */ + private double two_Polar_a = 12756274.0; /* 2.0 * Polar_a */ + + /* Polar Stereographic projection Parameters */ + private double Polar_Origin_Lat = ((PI * 90) / 180); /* Latitude of origin in radians */ + private double Polar_Origin_Long = 0.0; /* Longitude of origin in radians */ + private double Polar_False_Easting = 0.0; /* False easting in meters */ + private double Polar_False_Northing = 0.0; /* False northing in meters */ + + /* Maximum variance for easting and northing values for WGS 84. */ + private double Polar_Delta_Easting = 12713601.0; + private double Polar_Delta_Northing = 12713601.0; + + private double Easting; + private double Northing; + private double Latitude; + private double Longitude; + + PolarCoordConverter() + { + } + + /** + * The function setPolarStereographicParameters receives the ellipsoid parameters and Polar Stereograpic projection + * parameters as inputs, and sets the corresponding state variables. If any errors occur, error code(s) are + * returned by the function, otherwise POLAR_NO_ERROR is returned. + * + * @param a Semi-major axis of ellipsoid, in meters + * @param f Flattening of ellipsoid + * @param Latitude_of_True_Scale Latitude of true scale, in radians + * @param Longitude_Down_from_Pole Longitude down from pole, in radians + * @param False_Easting Easting (X) at center of projection, in meters + * @param False_Northing Northing (Y) at center of projection, in meters + * @return error code + */ + public long setPolarStereographicParameters(double a, double f, double Latitude_of_True_Scale, + double Longitude_Down_from_Pole, double False_Easting, double False_Northing) { + double es2; + double slat, clat; + double essin; + double one_PLUS_es, one_MINUS_es; + double pow_es; + double inv_f = 1 / f; + final double epsilon = 1.0e-2; + long Error_Code = POLAR_NO_ERROR; + + if (a <= 0.0) { /* Semi-major axis must be greater than zero */ + Error_Code |= POLAR_A_ERROR; + } + if ((inv_f < 250) || (inv_f > 350)) { /* Inverse flattening must be between 250 and 350 */ + Error_Code |= POLAR_INV_F_ERROR; + } + if ((Latitude_of_True_Scale < -PI_OVER_2) || (Latitude_of_True_Scale > PI_OVER_2)) { /* Origin Latitude out of range */ + Error_Code |= POLAR_ORIGIN_LAT_ERROR; + } + if ((Longitude_Down_from_Pole < -PI) || (Longitude_Down_from_Pole > TWO_PI)) { /* Origin Longitude out of range */ + Error_Code |= POLAR_ORIGIN_LON_ERROR; + } + + if (Error_Code == POLAR_NO_ERROR) { /* no errors */ + Polar_a = a; + two_Polar_a = 2.0 * Polar_a; + Polar_f = f; + + if (Longitude_Down_from_Pole > PI) + Longitude_Down_from_Pole -= TWO_PI; + if (Latitude_of_True_Scale < 0) { + Southern_Hemisphere = 1; + Polar_Origin_Lat = -Latitude_of_True_Scale; + Polar_Origin_Long = -Longitude_Down_from_Pole; + } else { + Southern_Hemisphere = 0; + Polar_Origin_Lat = Latitude_of_True_Scale; + Polar_Origin_Long = Longitude_Down_from_Pole; + } + Polar_False_Easting = False_Easting; + Polar_False_Northing = False_Northing; + + es2 = 2 * Polar_f - Polar_f * Polar_f; + es = Math.sqrt(es2); + es_OVER_2 = es / 2.0; + + if (Math.abs(Math.abs(Polar_Origin_Lat) - PI_OVER_2) > 1.0e-10) { + slat = Math.sin(Polar_Origin_Lat); + essin = es * slat; + pow_es = Math.pow((1.0 - essin) / (1.0 + essin), es_OVER_2); + clat = Math.cos(Polar_Origin_Lat); + mc = clat / Math.sqrt(1.0 - essin * essin); + Polar_a_mc = Polar_a * mc; + tc = Math.tan(PI_Over_4 - Polar_Origin_Lat / 2.0) / pow_es; + } else { + one_PLUS_es = 1.0 + es; + one_MINUS_es = 1.0 - es; + e4 = Math.sqrt(Math.pow(one_PLUS_es, one_PLUS_es) * Math.pow(one_MINUS_es, one_MINUS_es)); + } + } + + /* Calculate Radius */ + convertGeodeticToPolarStereographic(0, Polar_Origin_Long); + + Polar_Delta_Northing = Northing * 2; // Increased range for accepted easting and northing values + Polar_Delta_Northing = Math.abs(Polar_Delta_Northing) + epsilon; + Polar_Delta_Easting = Polar_Delta_Northing; + + return (Error_Code); + } + + /** + * The function Convert_Geodetic_To_Polar_Stereographic converts geodetic coordinates (latitude and longitude) to + * Polar Stereographic coordinates (easting and northing), according to the current ellipsoid and Polar + * Stereographic projection parameters. If any errors occur, error code(s) are returned by the function, otherwise + * POLAR_NO_ERROR is returned. + * + * @param Latitude latitude, in radians + * @param Longitude Longitude, in radians + * @return error code + */ + public long convertGeodeticToPolarStereographic(double Latitude, double Longitude) { + double dlam; + double slat; + double essin; + double t; + double rho; + double pow_es; + long Error_Code = POLAR_NO_ERROR; + + if ((Latitude < -PI_OVER_2) || (Latitude > PI_OVER_2)) { /* Latitude out of range */ + Error_Code |= POLAR_LAT_ERROR; + } + if ((Latitude < 0) && (Southern_Hemisphere == 0)) { /* Latitude and Origin Latitude in different hemispheres */ + Error_Code |= POLAR_LAT_ERROR; + } + if ((Latitude > 0) && (Southern_Hemisphere == 1)) { /* Latitude and Origin Latitude in different hemispheres */ + Error_Code |= POLAR_LAT_ERROR; + } + if ((Longitude < -PI) || (Longitude > TWO_PI)) { /* Longitude out of range */ + Error_Code |= POLAR_LON_ERROR; + } + + if (Error_Code == POLAR_NO_ERROR) { /* no errors */ + + if (Math.abs(Math.abs(Latitude) - PI_OVER_2) < 1.0e-10) { + Easting = 0.0; + Northing = 0.0; + } else { + if (Southern_Hemisphere != 0) { + Longitude *= -1.0; + Latitude *= -1.0; + } + dlam = Longitude - Polar_Origin_Long; + if (dlam > PI) { + dlam -= TWO_PI; + } + if (dlam < -PI) { + dlam += TWO_PI; + } + slat = Math.sin(Latitude); + essin = es * slat; + pow_es = Math.pow((1.0 - essin) / (1.0 + essin), es_OVER_2); + t = Math.tan(PI_Over_4 - Latitude / 2.0) / pow_es; + + if (Math.abs(Math.abs(Polar_Origin_Lat) - PI_OVER_2) > 1.0e-10) + rho = Polar_a_mc * t / tc; + else + rho = two_Polar_a * t / e4; + + + if (Southern_Hemisphere != 0) { + Easting = -(rho * Math.sin(dlam) - Polar_False_Easting); + //Easting *= -1.0; + Northing = rho * Math.cos(dlam) + Polar_False_Northing; + } else + Easting = rho * Math.sin(dlam) + Polar_False_Easting; + Northing = -rho * Math.cos(dlam) + Polar_False_Northing; + } + } + return (Error_Code); + } + + public double getEasting() { + return Easting; + } + + public double getNorthing() { + return Northing; + } + + /** + * The function Convert_Polar_Stereographic_To_Geodetic converts Polar + * Stereographic coordinates (easting and northing) to geodetic + * coordinates (latitude and longitude) according to the current ellipsoid + * and Polar Stereographic projection Parameters. If any errors occur, the + * code(s) are returned by the function, otherwise POLAR_NO_ERROR + * is returned. + * + * @param Easting Easting (X), in meters + * @param Northing Northing (Y), in meters + * @return error code + */ + public long convertPolarStereographicToGeodetic (double Easting, double Northing) { + double dy = 0, dx = 0; + double rho = 0; + double t; + double PHI, sin_PHI; + double tempPHI = 0.0; + double essin; + double pow_es; + double delta_radius; + long Error_Code = POLAR_NO_ERROR; + double min_easting = Polar_False_Easting - Polar_Delta_Easting; + double max_easting = Polar_False_Easting + Polar_Delta_Easting; + double min_northing = Polar_False_Northing - Polar_Delta_Northing; + double max_northing = Polar_False_Northing + Polar_Delta_Northing; + + if (Easting > max_easting || Easting < min_easting) { /* Easting out of range */ + Error_Code |= POLAR_EASTING_ERROR; + } + if (Northing > max_northing || Northing < min_northing) { /* Northing out of range */ + Error_Code |= POLAR_NORTHING_ERROR; + } + + if (Error_Code == POLAR_NO_ERROR) { + dy = Northing - Polar_False_Northing; + dx = Easting - Polar_False_Easting; + + /* Radius of point with origin of false easting, false northing */ + rho = Math.sqrt(dx * dx + dy * dy); + + delta_radius = Math.sqrt(Polar_Delta_Easting * Polar_Delta_Easting + Polar_Delta_Northing * Polar_Delta_Northing); + + if(rho > delta_radius) + { /* Point is outside of projection area */ + Error_Code |= POLAR_RADIUS_ERROR; + } + } + + if (Error_Code == POLAR_NO_ERROR) { /* no errors */ + if ((dy == 0.0) && (dx == 0.0)) { + Latitude = PI_OVER_2; + Longitude = Polar_Origin_Long; + + } else { + if (Southern_Hemisphere != 0) { + dy *= -1.0; + dx *= -1.0; + } + + if (Math.abs(Math.abs(Polar_Origin_Lat) - PI_OVER_2) > 1.0e-10) + t = rho * tc / (Polar_a_mc); + else + t = rho * e4 / (two_Polar_a); + PHI = PI_OVER_2 - 2.0 * Math.atan(t); + while (Math.abs(PHI - tempPHI) > 1.0e-10) { + tempPHI = PHI; + sin_PHI = Math.sin(PHI); + essin = es * sin_PHI; + pow_es = Math.pow((1.0 - essin) / (1.0 + essin), es_OVER_2); + PHI = PI_OVER_2 - 2.0 * Math.atan(t * pow_es); + } + Latitude = PHI; + Longitude = Polar_Origin_Long + Math.atan2(dx, -dy); + + if (Longitude > PI) + Longitude -= TWO_PI; + else if (Longitude < -PI) + Longitude += TWO_PI; + + + if (Latitude > PI_OVER_2) /* force distorted values to 90, -90 degrees */ + Latitude = PI_OVER_2; + else if (Latitude < -PI_OVER_2) + Latitude = -PI_OVER_2; + + if (Longitude > PI) /* force distorted values to 180, -180 degrees */ + Longitude = PI; + else if (Longitude < -PI) + Longitude = -PI; + + } + if (Southern_Hemisphere != 0) { + Latitude *= -1.0; + Longitude *= -1.0; + } + + } + return (Error_Code); + } + + /** + * @return Latitude in radians. + */ + public double getLatitude() { + return Latitude; + } + + /** + * @return Longitude in radians. + */ + public double getLongitude() { + return Longitude; + } + +} + diff --git a/worldwind/src/main/java/gov/nasa/worldwind/geom/coords/TMCoord.java b/worldwind/src/main/java/gov/nasa/worldwind/geom/coords/TMCoord.java new file mode 100644 index 000000000..863396dde --- /dev/null +++ b/worldwind/src/main/java/gov/nasa/worldwind/geom/coords/TMCoord.java @@ -0,0 +1,138 @@ +/* + * Copyright (C) 2011 United States Government as represented by the Administrator of the + * National Aeronautics and Space Administration. + * All Rights Reserved. + */ +package gov.nasa.worldwind.geom.coords; + +/** + * This class holds a set of Transverse Mercator coordinates along with the + * corresponding latitude and longitude. + * + * @author Patrick Murris + * @version $Id$ + * @see TMCoordConverter + */ +public class TMCoord { + + private final double latitude; + private final double longitude; + private final double easting; + private final double northing; + + /** + * Create a set of Transverse Mercator coordinates from a pair of latitude and longitude, + * for the given Globe and projection parameters. + * + * @param latitude the latitude double. + * @param longitude the longitude double. + * @param a semi-major ellipsoid radius. If this and argument f are non-null and globe is null, will use the specfied a and f. + * @param f ellipsoid flattening. If this and argument a are non-null and globe is null, will use the specfied a and f. + * @param originLatitude the origin latitude double. + * @param centralMeridian the central meridian longitude double. + * @param falseEasting easting value at the center of the projection in meters. + * @param falseNorthing northing value at the center of the projection in meters. + * @param scale scaling factor. + * @return the corresponding TMCoord. + * @throws IllegalArgumentException if latitude or longitude is null, + * or the conversion to TM coordinates fails. If the globe is null conversion will default + * to using WGS84. + */ + public static TMCoord fromLatLon(double latitude, double longitude, Double a, Double f, + double originLatitude, double centralMeridian, + double falseEasting, double falseNorthing, + double scale) { + + final TMCoordConverter converter = new TMCoordConverter(); + if (a == null || f == null) { + a = converter.getA(); + f = converter.getF(); + } + long err = converter.setTransverseMercatorParameters(a, f, Math.toRadians(originLatitude), Math.toRadians(centralMeridian), + falseEasting, falseNorthing, scale); + if (err == TMCoordConverter.TRANMERC_NO_ERROR) + err = converter.convertGeodeticToTransverseMercator(Math.toRadians(latitude), Math.toRadians(longitude)); + + if (err != TMCoordConverter.TRANMERC_NO_ERROR && err != TMCoordConverter.TRANMERC_LON_WARNING) { + throw new IllegalArgumentException("TM Conversion Error"); + } + + return new TMCoord(latitude, longitude, converter.getEasting(), converter.getNorthing(), + originLatitude, centralMeridian); + } + + /** + * Create a set of Transverse Mercator coordinates for the given Globe, + * easting, northing and projection parameters. + * + * @param easting the easting distance value in meters. + * @param northing the northing distance value in meters. + * @param originLatitude the origin latitude double. + * @param centralMeridian the central meridian longitude double. + * @param falseEasting easting value at the center of the projection in meters. + * @param falseNorthing northing value at the center of the projection in meters. + * @param scale scaling factor. + * @return the corresponding TMCoord. + * @throws IllegalArgumentException if originLatitude or centralMeridian + * is null, or the conversion to geodetic coordinates fails. If the globe is null conversion will default + * to using WGS84. + */ + public static TMCoord fromTM(double easting, double northing, + double originLatitude, double centralMeridian, + double falseEasting, double falseNorthing, + double scale) { + + final TMCoordConverter converter = new TMCoordConverter(); + + double a = converter.getA(); + double f = converter.getF(); + long err = converter.setTransverseMercatorParameters(a, f, Math.toRadians(originLatitude), Math.toRadians(centralMeridian), + falseEasting, falseNorthing, scale); + if (err == TMCoordConverter.TRANMERC_NO_ERROR) + err = converter.convertTransverseMercatorToGeodetic(easting, northing); + + if (err != TMCoordConverter.TRANMERC_NO_ERROR && err != TMCoordConverter.TRANMERC_LON_WARNING) { + throw new IllegalArgumentException("TM Conversion Error"); + } + + return new TMCoord(Math.toDegrees(converter.getLatitude()), Math.toDegrees(converter.getLongitude()), + easting, northing, originLatitude, centralMeridian); + } + + /** + * Create an arbitrary set of Transverse Mercator coordinates with the given values. + * + * @param latitude the latitude double. + * @param longitude the longitude double. + * @param easting the easting distance value in meters. + * @param northing the northing distance value in meters. + * @param originLatitude the origin latitude double. + * @param centralMeridian the central meridian longitude double. + * @throws IllegalArgumentException if latitude, longitude, originLatitude + * or centralMeridian is null. + */ + public TMCoord(double latitude, double longitude, double easting, double northing, + double originLatitude, double centralMeridian) { + this.latitude = latitude; + this.longitude = longitude; + this.easting = easting; + this.northing = northing; + } + + public double getLatitude() { + return this.latitude; + } + + public double getLongitude() { + return this.longitude; + } + + public double getEasting() { + return this.easting; + } + + public double getNorthing() { + return this.northing; + } + +} diff --git a/worldwind/src/main/java/gov/nasa/worldwind/geom/coords/TMCoordConverter.java b/worldwind/src/main/java/gov/nasa/worldwind/geom/coords/TMCoordConverter.java new file mode 100644 index 000000000..60259b031 --- /dev/null +++ b/worldwind/src/main/java/gov/nasa/worldwind/geom/coords/TMCoordConverter.java @@ -0,0 +1,505 @@ +/* + * Copyright (C) 2011 United States Government as represented by the Administrator of the + * National Aeronautics and Space Administration. + * All Rights Reserved. + */ +package gov.nasa.worldwind.geom.coords; + +/* + * Converter used to translate Transverse Mercator coordinates to and from geodetic latitude and longitude. + * + * @author Patrick Murris + * @version $Id$ + * @see TMCoord, UTMCoordConverter, MGRSCoordConverter + */ + +/* + * Ported to Java from the NGA GeoTrans code tranmerc.c and tranmerc.h + * + * @author Garrett Headley, Patrick Murris + */ +class TMCoordConverter { + + public final static int TRANMERC_NO_ERROR = 0x0000; + private final static int TRANMERC_LAT_ERROR = 0x0001; + private final static int TRANMERC_LON_ERROR = 0x0002; + public final static int TRANMERC_EASTING_ERROR = 0x0004; + public final static int TRANMERC_NORTHING_ERROR = 0x0008; + private final static int TRANMERC_ORIGIN_LAT_ERROR = 0x0010; + private final static int TRANMERC_CENT_MER_ERROR = 0x0020; + private final static int TRANMERC_A_ERROR = 0x0040; + private final static int TRANMERC_INV_F_ERROR = 0x0080; + private final static int TRANMERC_SCALE_FACTOR_ERROR = 0x0100; + public final static int TRANMERC_LON_WARNING = 0x0200; + + private final static double PI = 3.14159265358979323; /* PI */ + private final static double MAX_LAT = ((PI * 89.99) / 180.0); /* 90 degrees in radians */ + private final static double MAX_DELTA_LONG = ((PI * 90) / 180.0); /* 90 degrees in radians */ + private final static double MIN_SCALE_FACTOR = 0.3; + private final static double MAX_SCALE_FACTOR = 3.0; + + /* Ellipsoid Parameters, default to WGS 84 */ + private double TranMerc_a = 6378137.0; /* Semi-major axis of ellipsoid i meters */ + private double TranMerc_f = 1 / 298.257223563; /* Flattening of ellipsoid */ + private double TranMerc_es = 0.0066943799901413800; /* Eccentricity (0.08181919084262188000) squared */ + private double TranMerc_ebs = 0.0067394967565869; /* Second Eccentricity squared */ + + /* Transverse_Mercator projection Parameters */ + private double TranMerc_Origin_Lat = 0.0; /* Latitude of origin in radians */ + private double TranMerc_Origin_Long = 0.0; /* Longitude of origin in radians */ + private double TranMerc_False_Northing = 0.0; /* False northing in meters */ + private double TranMerc_False_Easting = 0.0; /* False easting in meters */ + private double TranMerc_Scale_Factor = 1.0; /* Scale factor */ + + /* Isometeric to geodetic latitude parameters, default to WGS 84 */ + private double TranMerc_ap = 6367449.1458008; + private double TranMerc_bp = 16038.508696861; + private double TranMerc_cp = 16.832613334334; + private double TranMerc_dp = 0.021984404273757; + private double TranMerc_ep = 3.1148371319283e-005; + + /* Maximum variance for easting and northing values for WGS 84. */ + private double TranMerc_Delta_Easting = 40000000.0; + private double TranMerc_Delta_Northing = 40000000.0; + + private double Easting; + private double Northing; + private double Longitude; + private double Latitude; + + TMCoordConverter() { } + + public double getA() { + return TranMerc_a; + } + + public double getF() { + return TranMerc_f; + } + + /** + * The function Set_Tranverse_Mercator_Parameters receives the ellipsoid parameters and Tranverse Mercator + * projection parameters as inputs, and sets the corresponding state variables. If any errors occur, the error + * code(s) are returned by the function, otherwise TRANMERC_NO_ERROR is returned. + * + * @param a Semi-major axis of ellipsoid, in meters + * @param f Flattening of ellipsoid + * @param Origin_Latitude Latitude in radians at the origin of the projection + * @param Central_Meridian Longitude in radians at the center of the projection + * @param False_Easting Easting/X at the center of the projection + * @param False_Northing Northing/Y at the center of the projection + * @param Scale_Factor Projection scale factor + * + * @return error code + */ + public long setTransverseMercatorParameters(double a, double f, double Origin_Latitude, + double Central_Meridian, double False_Easting, double False_Northing, double Scale_Factor) { + + double tn; /* True Meridianal distance constant */ + double tn2; + double tn3; + double tn4; + double tn5; + double TranMerc_b; /* Semi-minor axis of ellipsoid, in meters */ + double inv_f = 1 / f; + long Error_Code = TRANMERC_NO_ERROR; + + if (a <= 0.0) { /* Semi-major axis must be greater than zero */ + Error_Code |= TRANMERC_A_ERROR; + } + if ((inv_f < 250) || (inv_f > 350)) { /* Inverse flattening must be between 250 and 350 */ + Error_Code |= TRANMERC_INV_F_ERROR; + } + if ((Origin_Latitude < -MAX_LAT) || (Origin_Latitude > MAX_LAT)) { /* origin latitude out of range */ + Error_Code |= TRANMERC_ORIGIN_LAT_ERROR; + } + if ((Central_Meridian < -PI) || (Central_Meridian > (2 * PI))) { /* origin longitude out of range */ + Error_Code |= TRANMERC_CENT_MER_ERROR; + } + if ((Scale_Factor < MIN_SCALE_FACTOR) || (Scale_Factor > MAX_SCALE_FACTOR)) { + Error_Code |= TRANMERC_SCALE_FACTOR_ERROR; + } + if (Error_Code == TRANMERC_NO_ERROR) { /* no errors */ + TranMerc_a = a; + TranMerc_f = f; + TranMerc_Origin_Lat = 0; + TranMerc_Origin_Long = 0; + TranMerc_False_Northing = 0; + TranMerc_False_Easting = 0; + TranMerc_Scale_Factor = 1; + + /* Eccentricity Squared */ + TranMerc_es = 2 * TranMerc_f - TranMerc_f * TranMerc_f; + /* Second Eccentricity Squared */ + TranMerc_ebs = (1 / (1 - TranMerc_es)) - 1; + + TranMerc_b = TranMerc_a * (1 - TranMerc_f); + /*True meridianal constants */ + tn = (TranMerc_a - TranMerc_b) / (TranMerc_a + TranMerc_b); + tn2 = tn * tn; + tn3 = tn2 * tn; + tn4 = tn3 * tn; + tn5 = tn4 * tn; + + TranMerc_ap = TranMerc_a * (1.e0 - tn + 5.e0 * (tn2 - tn3) / 4.e0 + + 81.e0 * (tn4 - tn5) / 64.e0); + TranMerc_bp = 3.e0 * TranMerc_a * (tn - tn2 + 7.e0 * (tn3 - tn4) + / 8.e0 + 55.e0 * tn5 / 64.e0) / 2.e0; + TranMerc_cp = 15.e0 * TranMerc_a * (tn2 - tn3 + 3.e0 * (tn4 - tn5) / 4.e0) / 16.0; + TranMerc_dp = 35.e0 * TranMerc_a * (tn3 - tn4 + 11.e0 * tn5 / 16.e0) / 48.e0; + TranMerc_ep = 315.e0 * TranMerc_a * (tn4 - tn5) / 512.e0; + + convertGeodeticToTransverseMercator(MAX_LAT, MAX_DELTA_LONG); + + TranMerc_Delta_Easting = getEasting(); + TranMerc_Delta_Northing = getNorthing(); + + convertGeodeticToTransverseMercator(0, MAX_DELTA_LONG); + TranMerc_Delta_Easting = getEasting(); + + TranMerc_Origin_Lat = Origin_Latitude; + if (Central_Meridian > PI) + Central_Meridian -= (2 * PI); + TranMerc_Origin_Long = Central_Meridian; + TranMerc_False_Northing = False_Northing; + TranMerc_False_Easting = False_Easting; + TranMerc_Scale_Factor = Scale_Factor; + } + return (Error_Code); + } + + /** + * The function Convert_Geodetic_To_Transverse_Mercator converts geodetic (latitude and longitude) coordinates to + * Transverse Mercator projection (easting and northing) coordinates, according to the current ellipsoid and + * Transverse Mercator projection coordinates. If any errors occur, the error code(s) are returned by the function, + * otherwise TRANMERC_NO_ERROR is returned. + * + * @param Latitude Latitude in radians + * @param Longitude Longitude in radians + * + * @return error code + */ + public long convertGeodeticToTransverseMercator(double Latitude, double Longitude) { + + double c; /* Cosine of latitude */ + double c2; + double c3; + double c5; + double c7; + double dlam; /* Delta longitude - Difference in Longitude */ + double eta; /* constant - TranMerc_ebs *c *c */ + double eta2; + double eta3; + double eta4; + double s; /* Sine of latitude */ + double sn; /* Radius of curvature in the prime vertical */ + double t; /* Tangent of latitude */ + double tan2; + double tan3; + double tan4; + double tan5; + double tan6; + double t1; /* Term in coordinate conversion formula - GP to Y */ + double t2; /* Term in coordinate conversion formula - GP to Y */ + double t3; /* Term in coordinate conversion formula - GP to Y */ + double t4; /* Term in coordinate conversion formula - GP to Y */ + double t5; /* Term in coordinate conversion formula - GP to Y */ + double t6; /* Term in coordinate conversion formula - GP to Y */ + double t7; /* Term in coordinate conversion formula - GP to Y */ + double t8; /* Term in coordinate conversion formula - GP to Y */ + double t9; /* Term in coordinate conversion formula - GP to Y */ + double tmd; /* True Meridional distance */ + double tmdo; /* True Meridional distance for latitude of origin */ + long Error_Code = TRANMERC_NO_ERROR; + double temp_Origin; + double temp_Long; + + if ((Latitude < -MAX_LAT) || (Latitude > MAX_LAT)) { /* Latitude out of range */ + Error_Code |= TRANMERC_LAT_ERROR; + } + if (Longitude > PI) + Longitude -= (2 * PI); + if ((Longitude < (TranMerc_Origin_Long - MAX_DELTA_LONG)) + || (Longitude > (TranMerc_Origin_Long + MAX_DELTA_LONG))) { + if (Longitude < 0) + temp_Long = Longitude + 2 * PI; + else + temp_Long = Longitude; + if (TranMerc_Origin_Long < 0) + temp_Origin = TranMerc_Origin_Long + 2 * PI; + else + temp_Origin = TranMerc_Origin_Long; + if ((temp_Long < (temp_Origin - MAX_DELTA_LONG)) + || (temp_Long > (temp_Origin + MAX_DELTA_LONG))) + Error_Code |= TRANMERC_LON_ERROR; + } + if (Error_Code == TRANMERC_NO_ERROR) { /* no errors */ + /* + * Delta Longitude + */ + dlam = Longitude - TranMerc_Origin_Long; + + if (Math.abs(dlam) > (9.0 * PI / 180)) { /* Distortion will result if Longitude is more than 9 degrees from the Central Meridian */ + Error_Code |= TRANMERC_LON_WARNING; + } + + if (dlam > PI) + dlam -= (2 * PI); + if (dlam < -PI) + dlam += (2 * PI); + if (Math.abs(dlam) < 2.e-10) + dlam = 0.0; + + s = Math.sin(Latitude); + c = Math.cos(Latitude); + c2 = c * c; + c3 = c2 * c; + c5 = c3 * c2; + c7 = c5 * c2; + t = Math.tan(Latitude); + tan2 = t * t; + tan3 = tan2 * t; + tan4 = tan3 * t; + tan5 = tan4 * t; + tan6 = tan5 * t; + eta = TranMerc_ebs * c2; + eta2 = eta * eta; + eta3 = eta2 * eta; + eta4 = eta3 * eta; + + /* radius of curvature in prime vertical */ + // sn = SPHSN(Latitude); + sn = TranMerc_a / Math.sqrt(1 - TranMerc_es * Math.pow(Math.sin(Latitude), 2)); + + /* True Meridianal Distances */ + // tmd = SPHTMD(Latitude); + tmd = TranMerc_ap * Latitude + - TranMerc_bp * Math.sin(2.0 * Latitude) + + TranMerc_cp * Math.sin(4.0 * Latitude) + - TranMerc_dp * Math.sin(6.0 * Latitude) + + TranMerc_ep * Math.sin(8.0 * Latitude); + /* Origin */ + + // tmdo = SPHTMD (TranMerc_Origin_Lat); + tmdo = TranMerc_ap * TranMerc_Origin_Lat + - TranMerc_bp * Math.sin(2.0 * TranMerc_Origin_Lat) + + TranMerc_cp * Math.sin(4.0 * TranMerc_Origin_Lat) + - TranMerc_dp * Math.sin(6.0 * TranMerc_Origin_Lat) + + TranMerc_ep * Math.sin(8.0 * TranMerc_Origin_Lat); + + /* northing */ + t1 = (tmd - tmdo) * TranMerc_Scale_Factor; + t2 = sn * s * c * TranMerc_Scale_Factor / 2.e0; + t3 = sn * s * c3 * TranMerc_Scale_Factor * (5.e0 - tan2 + 9.e0 * eta + + 4.e0 * eta2) / 24.e0; + + t4 = sn * s * c5 * TranMerc_Scale_Factor * (61.e0 - 58.e0 * tan2 + + tan4 + 270.e0 * eta - 330.e0 * tan2 * eta + 445.e0 * eta2 + + 324.e0 * eta3 - 680.e0 * tan2 * eta2 + 88.e0 * eta4 + - 600.e0 * tan2 * eta3 - 192.e0 * tan2 * eta4) / 720.e0; + + t5 = sn * s * c7 * TranMerc_Scale_Factor * (1385.e0 - 3111.e0 * + tan2 + 543.e0 * tan4 - tan6) / 40320.e0; + + Northing = TranMerc_False_Northing + t1 + Math.pow(dlam, 2.e0) * t2 + + Math.pow(dlam, 4.e0) * t3 + Math.pow(dlam, 6.e0) * t4 + + Math.pow(dlam, 8.e0) * t5; + + /* Easting */ + t6 = sn * c * TranMerc_Scale_Factor; + t7 = sn * c3 * TranMerc_Scale_Factor * (1.e0 - tan2 + eta) / 6.e0; + t8 = sn * c5 * TranMerc_Scale_Factor * (5.e0 - 18.e0 * tan2 + tan4 + + 14.e0 * eta - 58.e0 * tan2 * eta + 13.e0 * eta2 + 4.e0 * eta3 + - 64.e0 * tan2 * eta2 - 24.e0 * tan2 * eta3) / 120.e0; + t9 = sn * c7 * TranMerc_Scale_Factor * (61.e0 - 479.e0 * tan2 + + 179.e0 * tan4 - tan6) / 5040.e0; + + Easting = TranMerc_False_Easting + dlam * t6 + Math.pow(dlam, 3.e0) * t7 + + Math.pow(dlam, 5.e0) * t8 + Math.pow(dlam, 7.e0) * t9; + } + return (Error_Code); + } + + /** @return Easting/X at the center of the projection */ + public double getEasting() { + return Easting; + } + + /** @return Northing/Y at the center of the projection */ + public double getNorthing() { + return Northing; + } + + /** + * The function Convert_Transverse_Mercator_To_Geodetic converts Transverse Mercator projection (easting and + * northing) coordinates to geodetic (latitude and longitude) coordinates, according to the current ellipsoid and + * Transverse Mercator projection parameters. If any errors occur, the error code(s) are returned by the function, + * otherwise TRANMERC_NO_ERROR is returned. + * + * @param Easting Easting/X in meters + * @param Northing Northing/Y in meters + * + * @return error code + */ + public long convertTransverseMercatorToGeodetic(double Easting, double Northing) { + double c; /* Cosine of latitude */ + double de; /* Delta easting - Difference in Easting (Easting-Fe) */ + double dlam; /* Delta longitude - Difference in Longitude */ + double eta; /* constant - TranMerc_ebs *c *c */ + double eta2; + double eta3; + double eta4; + double ftphi; /* Footpoint latitude */ + int i; /* Loop iterator */ + //double s; /* Sine of latitude */ + double sn; /* Radius of curvature in the prime vertical */ + double sr; /* Radius of curvature in the meridian */ + double t; /* Tangent of latitude */ + double tan2; + double tan4; + double t10; /* Term in coordinate conversion formula - GP to Y */ + double t11; /* Term in coordinate conversion formula - GP to Y */ + double t12; /* Term in coordinate conversion formula - GP to Y */ + double t13; /* Term in coordinate conversion formula - GP to Y */ + double t14; /* Term in coordinate conversion formula - GP to Y */ + double t15; /* Term in coordinate conversion formula - GP to Y */ + double t16; /* Term in coordinate conversion formula - GP to Y */ + double t17; /* Term in coordinate conversion formula - GP to Y */ + double tmd; /* True Meridional distance */ + double tmdo; /* True Meridional distance for latitude of origin */ + long Error_Code = TRANMERC_NO_ERROR; + + if ((Easting < (TranMerc_False_Easting - TranMerc_Delta_Easting)) + || (Easting > (TranMerc_False_Easting + TranMerc_Delta_Easting))) { /* Easting out of range */ + Error_Code |= TRANMERC_EASTING_ERROR; + } + if ((Northing < (TranMerc_False_Northing - TranMerc_Delta_Northing)) + || (Northing > (TranMerc_False_Northing + TranMerc_Delta_Northing))) { /* Northing out of range */ + Error_Code |= TRANMERC_NORTHING_ERROR; + } + + if (Error_Code == TRANMERC_NO_ERROR) { + /* True Meridional Distances for latitude of origin */ + // tmdo = SPHTMD(TranMerc_Origin_Lat); + tmdo = TranMerc_ap * TranMerc_Origin_Lat + - TranMerc_bp * Math.sin(2.0 * TranMerc_Origin_Lat) + + TranMerc_cp * Math.sin(4.0 * TranMerc_Origin_Lat) + - TranMerc_dp * Math.sin(6.0 * TranMerc_Origin_Lat) + + TranMerc_ep * Math.sin(8.0 * TranMerc_Origin_Lat); + + /* Origin */ + tmd = tmdo + (Northing - TranMerc_False_Northing) / TranMerc_Scale_Factor; + + /* First Estimate */ + //sr = SPHSR(0.e0); + sr = TranMerc_a * (1.e0 - TranMerc_es) / + Math.pow(Math.sqrt(1.e0 - TranMerc_es * Math.pow(Math.sin(0.e0), 2)), 3); + + ftphi = tmd / sr; + + for (i = 0; i < 5; i++) { + // t10 = SPHTMD (ftphi); + t10 = TranMerc_ap * ftphi + - TranMerc_bp * Math.sin(2.0 * ftphi) + + TranMerc_cp * Math.sin(4.0 * ftphi) + - TranMerc_dp * Math.sin(6.0 * ftphi) + + TranMerc_ep * Math.sin(8.0 * ftphi); + // sr = SPHSR(ftphi); + sr = TranMerc_a * (1.e0 - TranMerc_es) / + Math.pow(Math.sqrt(1.e0 - TranMerc_es * Math.pow(Math.sin(ftphi), 2)), 3); + ftphi = ftphi + (tmd - t10) / sr; + } + + /* Radius of Curvature in the meridian */ + // sr = SPHSR(ftphi); + sr = TranMerc_a * (1.e0 - TranMerc_es) / + Math.pow(Math.sqrt(1.e0 - TranMerc_es * Math.pow(Math.sin(ftphi), 2)), 3); + + /* Radius of Curvature in the meridian */ + // sn = SPHSN(ftphi); + sn = TranMerc_a / Math.sqrt(1.e0 - TranMerc_es * Math.pow(Math.sin(ftphi), 2)); + + /* Sine Cosine terms */ + //s = Math.sin(ftphi); + c = Math.cos(ftphi); + + /* Tangent Value */ + t = Math.tan(ftphi); + tan2 = t * t; + tan4 = tan2 * tan2; + eta = TranMerc_ebs * Math.pow(c, 2); + eta2 = eta * eta; + eta3 = eta2 * eta; + eta4 = eta3 * eta; + de = Easting - TranMerc_False_Easting; + if (Math.abs(de) < 0.0001) + de = 0.0; + + /* Latitude */ + t10 = t / (2.e0 * sr * sn * Math.pow(TranMerc_Scale_Factor, 2)); + t11 = t * (5.e0 + 3.e0 * tan2 + eta - 4.e0 * Math.pow(eta, 2) + - 9.e0 * tan2 * eta) / (24.e0 * sr * Math.pow(sn, 3) + * Math.pow(TranMerc_Scale_Factor, 4)); + t12 = t * (61.e0 + 90.e0 * tan2 + 46.e0 * eta + 45.E0 * tan4 + - 252.e0 * tan2 * eta - 3.e0 * eta2 + 100.e0 + * eta3 - 66.e0 * tan2 * eta2 - 90.e0 * tan4 + * eta + 88.e0 * eta4 + 225.e0 * tan4 * eta2 + + 84.e0 * tan2 * eta3 - 192.e0 * tan2 * eta4) + / (720.e0 * sr * Math.pow(sn, 5) * Math.pow(TranMerc_Scale_Factor, 6)); + t13 = t * (1385.e0 + 3633.e0 * tan2 + 4095.e0 * tan4 + 1575.e0 + * Math.pow(t, 6)) / (40320.e0 * sr * Math.pow(sn, 7) * Math.pow(TranMerc_Scale_Factor, 8)); + Latitude = ftphi - Math.pow(de, 2) * t10 + Math.pow(de, 4) * t11 - Math.pow(de, 6) * t12 + + Math.pow(de, 8) * t13; + + t14 = 1.e0 / (sn * c * TranMerc_Scale_Factor); + + t15 = (1.e0 + 2.e0 * tan2 + eta) / (6.e0 * Math.pow(sn, 3) * c * + Math.pow(TranMerc_Scale_Factor, 3)); + + t16 = (5.e0 + 6.e0 * eta + 28.e0 * tan2 - 3.e0 * eta2 + + 8.e0 * tan2 * eta + 24.e0 * tan4 - 4.e0 + * eta3 + 4.e0 * tan2 * eta2 + 24.e0 + * tan2 * eta3) / (120.e0 * Math.pow(sn, 5) * c + * Math.pow(TranMerc_Scale_Factor, 5)); + + t17 = (61.e0 + 662.e0 * tan2 + 1320.e0 * tan4 + 720.e0 + * Math.pow(t, 6)) / (5040.e0 * Math.pow(sn, 7) * c + * Math.pow(TranMerc_Scale_Factor, 7)); + + /* Difference in Longitude */ + dlam = de * t14 - Math.pow(de, 3) * t15 + Math.pow(de, 5) * t16 - Math.pow(de, 7) * t17; + + /* Longitude */ + Longitude = TranMerc_Origin_Long + dlam; + + if (Math.abs(Latitude) > (90.0 * PI / 180.0)) + Error_Code |= TRANMERC_NORTHING_ERROR; + + if ((Longitude) > (PI)) { + Longitude -= (2 * PI); + if (Math.abs(Longitude) > PI) + Error_Code |= TRANMERC_EASTING_ERROR; + } + + if (Math.abs(dlam) > (9.0 * PI / 180) * Math.cos(Latitude)) { /* Distortion will result if Longitude is more than 9 degrees from the Central Meridian at the equator */ + /* and decreases to 0 degrees at the poles */ + /* As you move towards the poles, distortion will become more significant */ + Error_Code |= TRANMERC_LON_WARNING; + } + + if (Latitude > 1.0e10) + Error_Code |= TRANMERC_LON_WARNING; + } + return (Error_Code); + } + + /** @return Latitude in radians. */ + public double getLatitude() { + return Latitude; + } + + /** @return Longitude in radians. */ + public double getLongitude() { + return Longitude; + } +} // end TMConverter class diff --git a/worldwind/src/main/java/gov/nasa/worldwind/geom/coords/UPSCoord.java b/worldwind/src/main/java/gov/nasa/worldwind/geom/coords/UPSCoord.java new file mode 100644 index 000000000..881391be7 --- /dev/null +++ b/worldwind/src/main/java/gov/nasa/worldwind/geom/coords/UPSCoord.java @@ -0,0 +1,116 @@ +/* + * Copyright (C) 2011 United States Government as represented by the Administrator of the + * National Aeronautics and Space Administration. + * All Rights Reserved. + */ +package gov.nasa.worldwind.geom.coords; + +import android.support.annotation.NonNull; + +/** + * This immutable class holds a set of UPS coordinates along with it's corresponding latitude and longitude. + * + * @author Patrick Murris + * @version $Id$ + */ + +public class UPSCoord { + private final double latitude; + private final double longitude; + private final Hemisphere hemisphere; + private final double easting; + private final double northing; + + /** + * Create a set of UPS coordinates from a pair of latitude and longitude for the given Globe. + * + * @param latitude the latitude double. + * @param longitude the longitude double. + * + * @return the corresponding UPSCoord. + * + * @throws IllegalArgumentException if latitude or longitude is null, or the conversion to + * UPS coordinates fails. + */ + public static UPSCoord fromLatLon(double latitude, double longitude) { + final UPSCoordConverter converter = new UPSCoordConverter(); + long err = converter.convertGeodeticToUPS(Math.toRadians(latitude), Math.toRadians(longitude)); + + if (err != UPSCoordConverter.UPS_NO_ERROR) { + throw new IllegalArgumentException("UPS Conversion Error"); + } + + return new UPSCoord(latitude, longitude, converter.getHemisphere(), + converter.getEasting(), converter.getNorthing()); + } + + /** + * Create a set of UPS coordinates for the given Globe. + * + * @param hemisphere the hemisphere, either {@link Hemisphere#N} of {@link Hemisphere#S}. + * @param easting the easting distance in meters + * @param northing the northing distance in meters. + * + * @return the corresponding UPSCoord. + * + * @throws IllegalArgumentException if the conversion to UPS coordinates fails. + */ + public static UPSCoord fromUPS(Hemisphere hemisphere, double easting, double northing) { + final UPSCoordConverter converter = new UPSCoordConverter(); + long err = converter.convertUPSToGeodetic(hemisphere, easting, northing); + + if (err != UTMCoordConverter.UTM_NO_ERROR) { + throw new IllegalArgumentException("UTM Conversion Error"); + } + + return new UPSCoord(Math.toDegrees(converter.getLatitude()), + Math.toDegrees(converter.getLongitude()), + hemisphere, easting, northing); + } + + /** + * Create an arbitrary set of UPS coordinates with the given values. + * + * @param latitude the latitude double. + * @param longitude the longitude double. + * @param hemisphere the hemisphere, either {@link Hemisphere#N} of {@link Hemisphere#S}. + * @param easting the easting distance in meters + * @param northing the northing distance in meters. + * + * @throws IllegalArgumentException if latitude, longitude, or hemisphere is + * null. + */ + public UPSCoord(double latitude, double longitude, Hemisphere hemisphere, double easting, double northing) { + this.latitude = latitude; + this.longitude = longitude; + this.hemisphere = hemisphere; + this.easting = easting; + this.northing = northing; + } + + public double getLatitude() { + return this.latitude; + } + + public double getLongitude() { + return this.longitude; + } + + public Hemisphere getHemisphere() { + return this.hemisphere; + } + + public double getEasting() { + return this.easting; + } + + public double getNorthing() { + return this.northing; + } + + @Override + @NonNull + public String toString() { + return hemisphere + " " + easting + "E" + " " + northing + "N"; + } +} diff --git a/worldwind/src/main/java/gov/nasa/worldwind/geom/coords/UPSCoordConverter.java b/worldwind/src/main/java/gov/nasa/worldwind/geom/coords/UPSCoordConverter.java new file mode 100644 index 000000000..4e5f66024 --- /dev/null +++ b/worldwind/src/main/java/gov/nasa/worldwind/geom/coords/UPSCoordConverter.java @@ -0,0 +1,258 @@ +/* + * Copyright (C) 2011 United States Government as represented by the Administrator of the + * National Aeronautics and Space Administration. + * All Rights Reserved. + */ + +/********************************************************************/ +/* RSC IDENTIFIER: UPS + * + * + * ABSTRACT + * + * This component provides conversions between geodetic (latitude + * and longitude) coordinates and Universal Polar Stereographic (UPS) + * projection (hemisphere, easting, and northing) coordinates. + * + * + * ERROR HANDLING + * + * This component checks parameters for valid values. If an + * invalid value is found the error code is combined with the + * current error code using the bitwise or. This combining allows + * multiple error codes to be returned. The possible error codes + * are: + * + * UPS_NO_ERROR : No errors occurred in function + * UPS_LAT_ERROR : latitude outside of valid range + * (North Pole: 83.5 to 90, + * South Pole: -79.5 to -90) + * UPS_LON_ERROR : longitude outside of valid range + * (-180 to 360 degrees) + * UPS_HEMISPHERE_ERROR : Invalid hemisphere ('N' or 'S') + * UPS_EASTING_ERROR : easting outside of valid range, + * (0 to 4,000,000m) + * UPS_NORTHING_ERROR : northing outside of valid range, + * (0 to 4,000,000m) + * UPS_A_ERROR : Semi-major axis less than or equal to zero + * UPS_INV_F_ERROR : Inverse flattening outside of valid range + * (250 to 350) + * + * + * REUSE NOTES + * + * UPS is intended for reuse by any application that performs a Universal + * Polar Stereographic (UPS) projection. + * + * + * REFERENCES + * + * Further information on UPS can be found in the Reuse Manual. + * + * UPS originated from : U.S. Army Topographic Engineering Center + * Geospatial Information Division + * 7701 Telegraph Road + * Alexandria, VA 22310-3864 + * + * + * LICENSES + * + * None apply to this component. + * + * + * RESTRICTIONS + * + * UPS has no restrictions. + * + * + * ENVIRONMENT + * + * UPS was tested and certified in the following environments: + * + * 1. Solaris 2.5 with GCC version 2.8.1 + * 2. Windows 95 with MS Visual C++ version 6 + * + * + * MODIFICATIONS + * + * Date Description + * ---- ----------- + * 06-11-95 Original Code + * 03-01-97 Original Code + * + * + */ + +package gov.nasa.worldwind.geom.coords; + +/** + * Ported to Java from the NGA GeoTrans ups.c and ups.h code - Feb 12, 2007 4:52:59 PM + * + * @author Garrett Headley, Patrick Murris + * @version $Id$ + */ +public class UPSCoordConverter { + + public static final int UPS_NO_ERROR = 0x0000; + private static final int UPS_LAT_ERROR = 0x0001; + private static final int UPS_LON_ERROR = 0x0002; + public static final int UPS_HEMISPHERE_ERROR = 0x0004; + public static final int UPS_EASTING_ERROR = 0x0008; + public static final int UPS_NORTHING_ERROR = 0x0010; + + private static final double PI = 3.14159265358979323; + private static final double MAX_LAT = (PI * 90) / 180.0; // 90 degrees in radians + // Min and max latitude values accepted + private static final double MIN_NORTH_LAT = 72 * PI / 180.0; // 83.5 + private static final double MIN_SOUTH_LAT = -72 * PI / 180.0; // -79.5 + + private static final double MAX_ORIGIN_LAT = (81.114528 * PI) / 180.0; + private static final double MIN_EAST_NORTH = 0; + private static final double MAX_EAST_NORTH = 4000000; + + private double UPS_Origin_Latitude = MAX_ORIGIN_LAT; /*set default = North hemisphere */ + private double UPS_Origin_Longitude = 0.0; + + /* Ellipsoid Parameters, default to WGS 84 */ + private double UPS_a = 6378137.0; /* Semi-major axis of ellipsoid in meters */ + private double UPS_f = 1 / 298.257223563; /* Flattening of ellipsoid */ + private double UPS_False_Easting = 2000000.0; + private double UPS_False_Northing = 2000000.0; + private double false_easting = 0.0; + private double false_northing = 0.0; + private double UPS_Easting = 0.0; + private double UPS_Northing = 0.0; + + private double easting = 0.0; + private double northing = 0.0; + private Hemisphere hemisphere = Hemisphere.N; + private double latitude = 0.0; + private double longitude = 0.0; + + private PolarCoordConverter polarConverter = new PolarCoordConverter(); + + UPSCoordConverter(){} + + /** + * The function convertGeodeticToUPS converts geodetic (latitude and longitude) coordinates to UPS (hemisphere, + * easting, and northing) coordinates, according to the current ellipsoid parameters. If any errors occur, the error + * code(s) are returned by the function, otherwide UPS_NO_ERROR is returned. + * + * @param latitude latitude in radians + * @param longitude longitude in radians + * + * @return error code + */ + public long convertGeodeticToUPS(double latitude, double longitude) { + if ((latitude < -MAX_LAT) || (latitude > MAX_LAT)) { /* latitude out of range */ + return UPS_LAT_ERROR; + } + if ((latitude < 0) && (latitude > MIN_SOUTH_LAT)) + return UPS_LAT_ERROR; + if ((latitude >= 0) && (latitude < MIN_NORTH_LAT)) + return UPS_LAT_ERROR; + + if ((longitude < -PI) || (longitude > (2 * PI))) { /* slam out of range */ + return UPS_LON_ERROR; + } + + if (latitude < 0) { + UPS_Origin_Latitude = -MAX_ORIGIN_LAT; + hemisphere = Hemisphere.S; + } else { + UPS_Origin_Latitude = MAX_ORIGIN_LAT; + hemisphere = Hemisphere.N; + } + + polarConverter.setPolarStereographicParameters(UPS_a, UPS_f, + UPS_Origin_Latitude, UPS_Origin_Longitude, + false_easting, false_northing); + + polarConverter.convertGeodeticToPolarStereographic(latitude, longitude); + + UPS_Easting = UPS_False_Easting + polarConverter.getEasting(); + UPS_Northing = UPS_False_Northing + polarConverter.getNorthing(); + if (Hemisphere.S.equals(hemisphere)) + UPS_Northing = UPS_False_Northing - polarConverter.getNorthing(); + + easting = UPS_Easting; + northing = UPS_Northing; + + return UPS_NO_ERROR; + } + + /** @return easting/X in meters */ + public double getEasting() { + return easting; + } + + /** @return northing/Y in meters */ + public double getNorthing() { + return northing; + } + + /** + * @return hemisphere, either {@link Hemisphere#N} of {@link Hemisphere#S}. + */ + public Hemisphere getHemisphere() { + return hemisphere; + } + + /** + * The function Convert_UPS_To_Geodetic converts UPS (hemisphere, easting, and northing) coordinates to geodetic + * (latitude and longitude) coordinates according to the current ellipsoid parameters. If any errors occur, the + * error code(s) are returned by the function, otherwise UPS_NO_ERROR is returned. + * + * @param hemisphere hemisphere, either {@link Hemisphere#N} of {@link Hemisphere#S}. + * @param easting easting/X in meters + * @param northing northing/Y in meters + * + * @return error code + */ + public long convertUPSToGeodetic(Hemisphere hemisphere, double easting, double northing) { + long Error_Code = UPS_NO_ERROR; + + if (!Hemisphere.N.equals(hemisphere) && !Hemisphere.S.equals(hemisphere)) + Error_Code |= UPS_HEMISPHERE_ERROR; + if ((easting < MIN_EAST_NORTH) || (easting > MAX_EAST_NORTH)) + Error_Code |= UPS_EASTING_ERROR; + if ((northing < MIN_EAST_NORTH) || (northing > MAX_EAST_NORTH)) + Error_Code |= UPS_NORTHING_ERROR; + + if (Hemisphere.N.equals(hemisphere)) + UPS_Origin_Latitude = MAX_ORIGIN_LAT; + if (Hemisphere.S.equals(hemisphere)) + UPS_Origin_Latitude = -MAX_ORIGIN_LAT; + + if (Error_Code == UPS_NO_ERROR) { /* no errors */ + polarConverter.setPolarStereographicParameters(UPS_a, + UPS_f, + UPS_Origin_Latitude, + UPS_Origin_Longitude, + UPS_False_Easting, + UPS_False_Northing); + + polarConverter.convertPolarStereographicToGeodetic(easting, northing); + latitude = polarConverter.getLatitude(); + longitude = polarConverter.getLongitude(); + + if ((latitude < 0) && (latitude > MIN_SOUTH_LAT)) + Error_Code |= UPS_LAT_ERROR; + if ((latitude >= 0) && (latitude < MIN_NORTH_LAT)) + Error_Code |= UPS_LAT_ERROR; + } + return Error_Code; + } + + /** @return latitude in radians. */ + public double getLatitude() { + return latitude; + } + + /** @return longitude in radians. */ + public double getLongitude() { + return longitude; + } +} + + diff --git a/worldwind/src/main/java/gov/nasa/worldwind/geom/coords/UTMCoord.java b/worldwind/src/main/java/gov/nasa/worldwind/geom/coords/UTMCoord.java new file mode 100644 index 000000000..06989c8b6 --- /dev/null +++ b/worldwind/src/main/java/gov/nasa/worldwind/geom/coords/UTMCoord.java @@ -0,0 +1,123 @@ +/* + * Copyright (C) 2011 United States Government as represented by the Administrator of the + * National Aeronautics and Space Administration. + * All Rights Reserved. + */ +package gov.nasa.worldwind.geom.coords; + +import android.support.annotation.NonNull; + +/** + * This immutable class holds a set of UTM coordinates along with it's corresponding latitude and longitude. + * + * @author Patrick Murris + * @version $Id$ + */ + +public class UTMCoord { + + private final double latitude; + private final double longitude; + private final Hemisphere hemisphere; + private final int zone; + private final double easting; + private final double northing; + + /** + * Create a set of UTM coordinates from a pair of latitude and longitude for the given Globe. + * + * @param latitude the latitude double. + * @param longitude the longitude double. + * + * @return the corresponding UTMCoord. + * + * @throws IllegalArgumentException if latitude or longitude is null, or the conversion to + * UTM coordinates fails. + */ + public static UTMCoord fromLatLon(double latitude, double longitude) { + final UTMCoordConverter converter = new UTMCoordConverter(); + long err = converter.convertGeodeticToUTM(Math.toRadians(latitude), Math.toRadians(longitude)); + + if (err != UTMCoordConverter.UTM_NO_ERROR) { + throw new IllegalArgumentException("UTM Conversion Error"); + } + + return new UTMCoord(latitude, longitude, converter.getZone(), converter.getHemisphere(), + converter.getEasting(), converter.getNorthing()); + } + + /** + * Create a set of UTM coordinates for the given Globe. + * + * @param zone the UTM zone - 1 to 60. + * @param hemisphere the hemisphere, either {@link Hemisphere#N} of {@link Hemisphere#S}. + * @param easting the easting distance in meters + * @param northing the northing distance in meters. + * + * @return the corresponding UTMCoord. + * + * @throws IllegalArgumentException if the conversion to UTM coordinates fails. + */ + public static UTMCoord fromUTM(int zone, Hemisphere hemisphere, double easting, double northing) { + final UTMCoordConverter converter = new UTMCoordConverter(); + long err = converter.convertUTMToGeodetic(zone, hemisphere, easting, northing); + + if (err != UTMCoordConverter.UTM_NO_ERROR) { + throw new IllegalArgumentException("UTM Conversion Error"); + } + + return new UTMCoord(Math.toDegrees(converter.getLatitude()), + Math.toDegrees(converter.getLongitude()), + zone, hemisphere, easting, northing); + } + + /** + * Create an arbitrary set of UTM coordinates with the given values. + * + * @param latitude the latitude double. + * @param longitude the longitude double. + * @param zone the UTM zone - 1 to 60. + * @param hemisphere the hemisphere, either {@link Hemisphere#N} of {@link Hemisphere#S}. + * @param easting the easting distance in meters + * @param northing the northing distance in meters. + * @throws IllegalArgumentException if latitude or longitude is null. + */ + public UTMCoord(double latitude, double longitude, int zone, Hemisphere hemisphere, double easting, double northing) { + this.latitude = latitude; + this.longitude = longitude; + this.hemisphere = hemisphere; + this.zone = zone; + this.easting = easting; + this.northing = northing; + } + + public double getLatitude() { + return this.latitude; + } + + public double getLongitude() { + return this.longitude; + } + + public int getZone() { + return this.zone; + } + + public Hemisphere getHemisphere() { + return this.hemisphere; + } + + public double getEasting() { + return this.easting; + } + + public double getNorthing() { + return this.northing; + } + + @Override + @NonNull + public String toString() { + return String.valueOf(zone) + " " + hemisphere + " " + easting + "E" + " " + northing + "N"; + } +} diff --git a/worldwind/src/main/java/gov/nasa/worldwind/geom/coords/UTMCoordConverter.java b/worldwind/src/main/java/gov/nasa/worldwind/geom/coords/UTMCoordConverter.java new file mode 100644 index 000000000..1008ced1f --- /dev/null +++ b/worldwind/src/main/java/gov/nasa/worldwind/geom/coords/UTMCoordConverter.java @@ -0,0 +1,238 @@ +/* + * Copyright (C) 2011 United States Government as represented by the Administrator of the + * National Aeronautics and Space Administration. + * All Rights Reserved. + */ +package gov.nasa.worldwind.geom.coords; + +/** + * Converter used to translate UTM coordinates to and from geodetic latitude and longitude. + * + * @author Patrick Murris + * @version $Id$ + * @see UTMCoord, TMCoordConverter + */ + +/** + * Ported to Java from the NGA GeoTrans utm.c and utm.h + * + * @author Garrett Headley, Patrick Murris + */ +class UTMCoordConverter { + + public final static int UTM_NO_ERROR = 0x0000; + public final static int UTM_LAT_ERROR = 0x0001; + public final static int UTM_LON_ERROR = 0x0002; + public final static int UTM_EASTING_ERROR = 0x0004; + public final static int UTM_NORTHING_ERROR = 0x0008; + public final static int UTM_ZONE_ERROR = 0x0010; + public final static int UTM_HEMISPHERE_ERROR = 0x0020; + public final static int UTM_ZONE_OVERRIDE_ERROR = 0x0040; + public final static int UTM_TM_ERROR = 0x0200; + + private final static double PI = 3.14159265358979323; + //private final static double MIN_LAT = ((-80.5 * PI) / 180.0); /* -80.5 degrees in radians */ + //private final static double MAX_LAT = ((84.5 * PI) / 180.0); /* 84.5 degrees in radians */ + private final static double MIN_LAT = ((-82 * PI) / 180.0); /* -82 degrees in radians */ + private final static double MAX_LAT = ((86 * PI) / 180.0); /* 86 degrees in radians */ + + private final static int MIN_EASTING = 100000; + private final static int MAX_EASTING = 900000; + private final static int MIN_NORTHING = 0; + private final static int MAX_NORTHING = 10000000; + + private double UTM_a = 6378137.0; /* Semi-major axis of ellipsoid in meters */ + private double UTM_f = 1 / 298.257223563; /* Flattening of ellipsoid */ + private long UTM_Override = 0; /* Zone override flag */ + + private double Easting; + private double Northing; + private Hemisphere hemisphere; + private int Zone; + private double Latitude; + private double Longitude; + private double Central_Meridian; + + UTMCoordConverter(){} + + /** + * The function Convert_Geodetic_To_UTM converts geodetic (latitude and longitude) coordinates to UTM projection + * (zone, hemisphere, easting and northing) coordinates according to the current ellipsoid and UTM zone override + * parameters. If any errors occur, the error code(s) are returned by the function, otherwise UTM_NO_ERROR is + * returned. + * + * @param Latitude Latitude in radians + * @param Longitude Longitude in radians + * + * @return error code + */ + public long convertGeodeticToUTM(double Latitude, double Longitude) { + long Lat_Degrees; + long Long_Degrees; + long temp_zone; + long Error_Code = UTM_NO_ERROR; + double Origin_Latitude = 0; + double False_Easting = 500000; + double False_Northing = 0; + double Scale = 0.9996; + + if ((Latitude < MIN_LAT) || (Latitude > MAX_LAT)) { /* Latitude out of range */ + Error_Code |= UTM_LAT_ERROR; + } + if ((Longitude < -PI) || (Longitude > (2 * PI))) { /* Longitude out of range */ + Error_Code |= UTM_LON_ERROR; + } + if (Error_Code == UTM_NO_ERROR) { /* no errors */ + if (Longitude < 0) + Longitude += (2 * PI) + 1.0e-10; + Lat_Degrees = (long) (Latitude * 180.0 / PI); + Long_Degrees = (long) (Longitude * 180.0 / PI); + + if (Longitude < PI) + temp_zone = (long) (31 + ((Longitude * 180.0 / PI) / 6.0)); + else + temp_zone = (long) (((Longitude * 180.0 / PI) / 6.0) - 29); + if (temp_zone > 60) + temp_zone = 1; + /* UTM special cases */ + if ((Lat_Degrees > 55) && (Lat_Degrees < 64) && (Long_Degrees > -1) && (Long_Degrees < 3)) + temp_zone = 31; + if ((Lat_Degrees > 55) && (Lat_Degrees < 64) && (Long_Degrees > 2) && (Long_Degrees < 12)) + temp_zone = 32; + if ((Lat_Degrees > 71) && (Long_Degrees > -1) && (Long_Degrees < 9)) + temp_zone = 31; + if ((Lat_Degrees > 71) && (Long_Degrees > 8) && (Long_Degrees < 21)) + temp_zone = 33; + if ((Lat_Degrees > 71) && (Long_Degrees > 20) && (Long_Degrees < 33)) + temp_zone = 35; + if ((Lat_Degrees > 71) && (Long_Degrees > 32) && (Long_Degrees < 42)) + temp_zone = 37; + + if (UTM_Override != 0) { + if ((temp_zone == 1) && (UTM_Override == 60)) + temp_zone = UTM_Override; + else if ((temp_zone == 60) && (UTM_Override == 1)) + temp_zone = UTM_Override; + else if (((temp_zone - 1) <= UTM_Override) && (UTM_Override <= (temp_zone + 1))) + temp_zone = UTM_Override; + else + Error_Code = UTM_ZONE_OVERRIDE_ERROR; + } + if (Error_Code == UTM_NO_ERROR) { + if (temp_zone >= 31) + Central_Meridian = (6 * temp_zone - 183) * PI / 180.0; + else + Central_Meridian = (6 * temp_zone + 177) * PI / 180.0; + Zone = (int) temp_zone; + if (Latitude < 0) { + False_Northing = 10000000; + hemisphere = Hemisphere.S; + } + else + hemisphere = Hemisphere.N; + + try { + TMCoord TM = TMCoord.fromLatLon(Math.toDegrees(Latitude), Math.toDegrees(Longitude), + this.UTM_a, this.UTM_f, Math.toDegrees(Origin_Latitude), + Math.toDegrees(Central_Meridian), False_Easting, False_Northing, Scale); + Easting = TM.getEasting(); + Northing = TM.getNorthing(); + + if ((Easting < MIN_EASTING) || (Easting > MAX_EASTING)) + Error_Code = UTM_EASTING_ERROR; + if ((Northing < MIN_NORTHING) || (Northing > MAX_NORTHING)) + Error_Code |= UTM_NORTHING_ERROR; + } catch (Exception e) { + Error_Code = UTM_TM_ERROR; + } + } + } + return (Error_Code); + } + + /** @return Easting (X) in meters */ + public double getEasting() { + return Easting; + } + + /** @return Northing (Y) in meters */ + public double getNorthing() { + return Northing; + } + + /** + * @return The coordinate hemisphere, either {@link Hemisphere#N} of {@link Hemisphere#S}. + */ + public Hemisphere getHemisphere() { + return hemisphere; + } + + /** @return UTM zone */ + public int getZone() { + return Zone; + } + + /** + * The function Convert_UTM_To_Geodetic converts UTM projection (zone, hemisphere, easting and northing) coordinates + * to geodetic(latitude and longitude) coordinates, according to the current ellipsoid parameters. If any errors + * occur, the error code(s) are returned by the function, otherwise UTM_NO_ERROR is returned. + * + * @param zone UTM zone. + * @param hemisphere The coordinate hemisphere, either {@link Hemisphere#N} of {@link Hemisphere#S}. + * @param easting easting (X) in meters. + * @param Northing Northing (Y) in meters. + * + * @return error code. + */ + public long convertUTMToGeodetic(long zone, Hemisphere hemisphere, double easting, double Northing) { + // TODO: arg checking + long Error_Code = UTM_NO_ERROR; + double Origin_Latitude = 0; + double False_Easting = 500000; + double False_Northing = 0; + double Scale = 0.9996; + + if ((zone < 1) || (zone > 60)) + Error_Code |= UTM_ZONE_ERROR; + if (!hemisphere.equals(Hemisphere.S) && !hemisphere.equals(Hemisphere.N)) + Error_Code |= UTM_HEMISPHERE_ERROR; +// if ((easting < MIN_EASTING) || (easting > MAX_EASTING)) //removed check to enable reprojecting images +// Error_Code |= UTM_EASTING_ERROR; //that extend into another zone + if ((Northing < MIN_NORTHING) || (Northing > MAX_NORTHING)) + Error_Code |= UTM_NORTHING_ERROR; + + if (Error_Code == UTM_NO_ERROR) { /* no errors */ + if (zone >= 31) + Central_Meridian = ((6 * zone - 183) * PI / 180.0 /*+ 0.00000005*/); + else + Central_Meridian = ((6 * zone + 177) * PI / 180.0 /*+ 0.00000005*/); + if (hemisphere.equals(Hemisphere.S)) + False_Northing = 10000000; + try { + TMCoord TM = TMCoord.fromTM(easting, Northing, + Math.toDegrees(Origin_Latitude), Math.toDegrees(Central_Meridian), + False_Easting, False_Northing, Scale); + Latitude = Math.toRadians(TM.getLatitude()); + Longitude = Math.toRadians(TM.getLongitude()); + + if ((Latitude < MIN_LAT) || (Latitude > MAX_LAT)) { /* Latitude out of range */ + Error_Code |= UTM_NORTHING_ERROR; + } + } catch (Exception e) { + Error_Code = UTM_TM_ERROR; + } + } + return (Error_Code); + } + + /** @return Latitude in radians. */ + public double getLatitude() { + return Latitude; + } + + /** @return Longitude in radians. */ + public double getLongitude() { + return Longitude; + } + +} diff --git a/worldwind/src/main/java/gov/nasa/worldwind/globe/Globe.java b/worldwind/src/main/java/gov/nasa/worldwind/globe/Globe.java index 958409124..0bd48b459 100644 --- a/worldwind/src/main/java/gov/nasa/worldwind/globe/Globe.java +++ b/worldwind/src/main/java/gov/nasa/worldwind/globe/Globe.java @@ -32,6 +32,10 @@ public class Globe { */ protected GeographicProjection projection; + private final float[] scratchHeights = new float[1]; + + private final Sector scratchSector = new Sector(); + /** * Constructs a globe with a specified reference ellipsoid and projection. * @@ -320,4 +324,19 @@ public boolean intersect(Line line, Vec3 result) { return this.projection.intersect(this, line, result); } + + /** + * Determine terrain altitude in specified geographic point from elevation model + * + * @param latitude location latitude + * @param longitude location longitude + * + * @return Elevation in meters in specified location + */ + public double getElevationAtLocation(double latitude, double longitude) { + // Use 1E-15 below because sector can not have zero deltas + this.scratchSector.set(latitude, longitude, 1E-15, 1E-15); + this.getElevationModel().getHeightGrid(this.scratchSector, 1, 1, this.scratchHeights); + return this.scratchHeights[0]; + } } diff --git a/worldwind/src/main/java/gov/nasa/worldwind/layer/graticule/AbstractGraticuleLayer.java b/worldwind/src/main/java/gov/nasa/worldwind/layer/graticule/AbstractGraticuleLayer.java new file mode 100644 index 000000000..637cafc63 --- /dev/null +++ b/worldwind/src/main/java/gov/nasa/worldwind/layer/graticule/AbstractGraticuleLayer.java @@ -0,0 +1,610 @@ +/* + * 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.layer.graticule; + +import android.graphics.Typeface; + +import java.util.List; + +import gov.nasa.worldwind.WorldWind; +import gov.nasa.worldwind.geom.Line; +import gov.nasa.worldwind.geom.Location; +import gov.nasa.worldwind.geom.Position; +import gov.nasa.worldwind.geom.Sector; +import gov.nasa.worldwind.geom.Vec3; +import gov.nasa.worldwind.layer.AbstractLayer; +import gov.nasa.worldwind.render.Color; +import gov.nasa.worldwind.render.RenderContext; +import gov.nasa.worldwind.render.Renderable; +import gov.nasa.worldwind.shape.Label; +import gov.nasa.worldwind.shape.Path; +import gov.nasa.worldwind.util.WWMath; + +/** + * Displays a graticule. + * + * @author Patrick Murris + * @version $Id: AbstractGraticuleLayer.java 2153 2014-07-17 17:33:13Z tgaskins $ + */ +public abstract class AbstractGraticuleLayer extends AbstractLayer { + +// /** +// * Solid line rendering style. This style specifies that a line will be drawn without any breaks.
+// *

_________
+// *
is an example of a solid line. +// */ +// public static final String LINE_STYLE_SOLID = GraticuleRenderingParams.VALUE_LINE_STYLE_SOLID; +// /** +// * Dashed line rendering style. This style specifies that a line will be drawn as a series of long strokes, with +// * space in between.
+// *
- - - - -
+// *
is an example of a dashed line. +// */ +// public static final String LINE_STYLE_DASHED = GraticuleRenderingParams.VALUE_LINE_STYLE_DASHED; +// /** +// * Dotted line rendering style. This style specifies that a line will be drawn as a series of evenly spaced "square" +// * dots.
+// *
. . . . .
+// * is an example of a dotted line. +// */ +// public static final String LINE_STYLE_DOTTED = GraticuleRenderingParams.VALUE_LINE_STYLE_DOTTED; + + private static final String LOOK_AT_LATITUDE_PROPERTY = "look_at_latitude"; + private static final String LOOK_AT_LONGITUDE_PROPERTY = "look_at_longitude"; + private static final String GRATICULE_PIXEL_SIZE_PROPERTY = "graticule_pixel_size"; + private static final String GRATICULE_LABEL_OFFSET_PROPERTY = "graticule_label_offset"; + + // Helper variables to avoid memory leaks + private final Vec3 surfacePoint = new Vec3(); + private final Line forwardRay = new Line(); + private final Vec3 lookAtPoint = new Vec3(); + private final Position lookAtPos = new Position(); + + private final GraticuleSupport graticuleSupport = new GraticuleSupport(); + + // Update reference states + private final Vec3 lastCameraPoint = new Vec3(); + private double lastCameraHeading; + private double lastCameraTilt; + private double lastFOV; + private double lastVerticalExaggeration; +// private Globe lastGlobe; +// private GeographicProjection lastProjection; +// private long frameTimeStamp; // used only for 2D continuous globes to determine whether render is in same frame +// private double terrainConformance = 50; + + AbstractGraticuleLayer(String name) { + this.setDisplayName(name); + this.setPickEnabled(false); + this.initRenderingParams(); + } + + protected abstract void initRenderingParams(); + + /** + * Returns whether or not graticule lines will be rendered. + * + * @param key the rendering parameters key. + * + * @return true if graticule lines will be rendered; false otherwise. + */ + public boolean isDrawGraticule(String key) { + return this.getRenderingParams(key).isDrawLines(); + } + + /** + * Sets whether or not graticule lines will be rendered. + * + * @param drawGraticule true to render graticule lines; false to disable rendering. + * @param key the rendering parameters key. + */ + public void setDrawGraticule(boolean drawGraticule, String key) { + this.getRenderingParams(key).setDrawLines(drawGraticule); + } + + /** + * Returns the graticule line Color. + * + * @param key the rendering parameters key. + * + * @return Color used to render graticule lines. + */ + public Color getGraticuleLineColor(String key) { + return this.getRenderingParams(key).getLineColor(); + } + + /** + * Sets the graticule line Color. + * + * @param color Color that will be used to render graticule lines. + * @param key the rendering parameters key. + */ + public void setGraticuleLineColor(Color color, String key) { + this.getRenderingParams(key).setLineColor(color); + } + + /** + * Returns the graticule line width. + * + * @param key the rendering parameters key. + * + * @return width of the graticule lines. + */ + public double getGraticuleLineWidth(String key) { + return this.getRenderingParams(key).getLineWidth(); + } + + /** + * Sets the graticule line width. + * + * @param lineWidth width of the graticule lines. + * @param key the rendering parameters key. + */ + public void setGraticuleLineWidth(double lineWidth, String key) { + this.getRenderingParams(key).setLineWidth(lineWidth); + } + +// /** +// * Returns the graticule line rendering style. +// * +// * @param key the rendering parameters key. +// * +// * @return rendering style of the graticule lines. +// */ +// public String getGraticuleLineStyle(String key) { +// return this.getRenderingParams(key).getLineStyle(); +// } +// +// /** +// * Sets the graticule line rendering style. +// * +// * @param lineStyle rendering style of the graticule lines. One of LINE_STYLE_SOLID, LINE_STYLE_DASHED, or +// * LINE_STYLE_DOTTED. +// * @param key the rendering parameters key. +// */ +// public void setGraticuleLineStyle(String lineStyle, String key) { +// this.getRenderingParams(key).setLineStyle(lineStyle); +// } + + /** + * Returns whether or not graticule labels will be rendered. + * + * @param key the rendering parameters key. + * + * @return true if graticule labels will be rendered; false otherwise. + */ + public boolean isDrawLabels(String key) { + return this.getRenderingParams(key).isDrawLabels(); + } + + /** + * Sets whether or not graticule labels will be rendered. + * + * @param drawLabels true to render graticule labels; false to disable rendering. + * @param key the rendering parameters key. + */ + public void setDrawLabels(boolean drawLabels, String key) { + this.getRenderingParams(key).setDrawLabels(drawLabels); + } + + /** + * Returns the graticule label Color. + * + * @param key the rendering parameters key. + * + * @return Color used to render graticule labels. + */ + public Color getLabelColor(String key) { + return this.getRenderingParams(key).getLabelColor(); + } + + /** + * Sets the graticule label Color. + * + * @param color Color that will be used to render graticule labels. + * @param key the rendering parameters key. + */ + public void setLabelColor(Color color, String key) { + this.getRenderingParams(key).setLabelColor(color); + } + + /** + * Returns the Typeface used for graticule labels. + * + * @param key the rendering parameters key. + * + * @return Typeface used to render graticule labels. + */ + public Typeface getLabelTypeface(String key) { + return this.getRenderingParams(key).getLabelTypeface(); + } + + /** + * Sets the Typeface used for graticule labels. + * + * @param typeface Typeface that will be used to render graticule labels. + * @param key the rendering parameters key. + */ + public void setLabelTypeface(Typeface typeface, String key) { + this.getRenderingParams(key).setLabelTypeface(typeface); + } + + /** + * Returns the Size used for graticule labels. + * + * @param key the rendering parameters key. + * + * @return Size used to render graticule labels. + */ + public Float getLabelSize(String key) { + return this.getRenderingParams(key).getLabelSize(); + } + + /** + * Sets the Size used for graticule labels. + * + * @param size Size that will be used to render graticule labels. + * @param key the rendering parameters key. + */ + public void setLabelSize(Float size, String key) { + this.getRenderingParams(key).setLabelSize(size); + } + + GraticuleRenderingParams getRenderingParams(String key) { + return this.graticuleSupport.getRenderingParams(key); + } + + void setRenderingParams(String key, GraticuleRenderingParams renderingParams) { + this.graticuleSupport.setRenderingParams(key, renderingParams); + } + + void addRenderable(Renderable renderable, String paramsKey) { + this.graticuleSupport.addRenderable(renderable, paramsKey); + } + + private void removeAllRenderables() { + this.graticuleSupport.removeAllRenderables(); + } + + @Override + public void doRender(RenderContext rc) { +// if (rc.isContinuous2DGlobe()) { +// if (this.needsToUpdate(rc)) { +// this.clear(rc); +// this.selectRenderables(rc); +// } +// +// // If the frame time stamp is the same, then this is the second or third pass of the same frame. We continue +// // selecting renderables in these passes. +// if (rc.getFrameTimeStamp() == this.frameTimeStamp) +// this.selectRenderables(rc); +// +// this.frameTimeStamp = rc.getFrameTimeStamp(); +// } else { + if (this.needsToUpdate(rc)) { + this.clear(rc); + this.selectRenderables(rc); + } +// } + + // Render + this.graticuleSupport.render(rc, this.getOpacity()); + } + + /** + * Select the visible grid elements + * + * @param rc the current RenderContext. + */ + protected abstract void selectRenderables(RenderContext rc); + + protected abstract List getOrderedTypes(); + + protected abstract String getTypeFor(double resolution); + + /** + * Determines whether the grid should be updated. It returns true if:
  • the eye has moved more than 1% of its + * altitude above ground
  • the view FOV, heading or pitch have changed more than 1 degree
  • vertical + * exaggeration has changed
RenderContext. + * + * @return true if the graticule should be updated. + */ + @SuppressWarnings({"RedundantIfStatement"}) + private boolean needsToUpdate(RenderContext rc) { + if (this.lastVerticalExaggeration != rc.verticalExaggeration) + return true; + + if (Math.abs(this.lastCameraHeading - rc.camera.heading) > 1) + return true; + + if (Math.abs(this.lastCameraTilt - rc.camera.tilt) > 1) + return true; + + if (Math.abs(this.lastFOV - rc.fieldOfView) > 1) + return true; + + if (rc.cameraPoint.distanceTo(this.lastCameraPoint) > computeAltitudeAboveGround(rc) / 100) // 1% of AAG + return true; + + // We must test the globe and its projection to see if either changed. We can't simply use the globe state + // key for this because we don't want a 2D globe offset change to cause an update. Offset changes don't + // invalidate the current set of renderables. + +// if (rc.globe != this.lastGlobe) +// return true; + +// if (rc.is2DGlobe()) +// if (((Globe2D) rc.getGlobe()).getProjection() != this.lastProjection) +// return true; + + return false; + } + + protected void clear(RenderContext rc) { + this.removeAllRenderables(); + this.lastCameraPoint.set(rc.cameraPoint); + this.lastFOV = rc.fieldOfView; + this.lastCameraHeading = rc.camera.heading; + this.lastCameraTilt = rc.camera.tilt; + this.lastVerticalExaggeration = rc.verticalExaggeration; +// this.lastGlobe = rc.globe; +// if (rc.is2DGlobe()) +// this.lastProjection = ((Globe2D) rc.getGlobe()).getProjection(); +// this.terrainConformance = this.computeTerrainConformance(rc); +// this.applyTerrainConformance(); + } + +// private double computeTerrainConformance(RenderContext rc) { +// int value = 100; +// double alt = rc.camera.altitude; +// if (alt < 10e3) +// value = 20; +// else if (alt < 50e3) +// value = 30; +// else if (alt < 100e3) +// value = 40; +// else if (alt < 1000e3) +// value = 60; +// +// return value; +// } +// +// private void applyTerrainConformance() { +// String[] graticuleType = getOrderedTypes(); +// for (String type : graticuleType) { +// getRenderingParams(type).put( +// GraticuleRenderingParams.KEY_LINE_CONFORMANCE, this.terrainConformance); +// } +// } + + Location computeLabelOffset(RenderContext rc) { + if(this.hasLookAtPos(rc)) { + double labelOffsetDegrees = this.getLabelOffset(rc); + Location labelPos = Location.fromDegrees(this.getLookAtLatitude(rc) - labelOffsetDegrees, + this.getLookAtLongitude(rc) - labelOffsetDegrees); + labelPos.set(WWMath.clamp(Location.normalizeLatitude(labelPos.latitude), -70, 70), + Location.normalizeLongitude(labelPos.longitude)); + return labelPos; + } else { + return Location.fromDegrees(rc.camera.latitude, rc.camera.longitude); + } + } + + Renderable createLineRenderable(List positions, int pathType) { + Path path = new Path(positions); + path.setPathType(pathType); + path.setFollowTerrain(true); + // path.setTerrainConformance(1); // WTF Why not this.terrainConformance? + path.setAltitudeMode(WorldWind.CLAMP_TO_GROUND); + return path; + } + + Renderable createTextRenderable(Position position, String label, double resolution) { + Label text = new Label(position, label).setAltitudeMode(WorldWind.CLAMP_TO_GROUND); + //text.setPriority(resolution * 1e6); + return text; + } + + boolean hasLookAtPos(RenderContext rc) { + calculateLookAtProperties(rc); + return rc.getUserProperty(LOOK_AT_LATITUDE_PROPERTY) != null && rc.getUserProperty(LOOK_AT_LONGITUDE_PROPERTY) != null; + } + + double getLookAtLatitude(RenderContext rc) { + calculateLookAtProperties(rc); + return (double) rc.getUserProperty(LOOK_AT_LATITUDE_PROPERTY); + } + + double getLookAtLongitude(RenderContext rc) { + calculateLookAtProperties(rc); + return (double) rc.getUserProperty(LOOK_AT_LONGITUDE_PROPERTY); + } + + double getPixelSize(RenderContext rc) { + calculateLookAtProperties(rc); + return (double) rc.getUserProperty(GRATICULE_PIXEL_SIZE_PROPERTY); + } + + private double getLabelOffset(RenderContext rc) { + calculateLookAtProperties(rc); + return (double) rc.getUserProperty(GRATICULE_LABEL_OFFSET_PROPERTY); + } + + Vec3 getSurfacePoint(RenderContext rc, double latitude, double longitude) { + if (!rc.terrain.surfacePoint(latitude, longitude, surfacePoint)) + rc.globe.geographicToCartesian(latitude, longitude, + rc.globe.getElevationAtLocation(latitude, longitude), surfacePoint); + + return surfacePoint; + } + + double computeAltitudeAboveGround(RenderContext rc) { + Vec3 surfacePoint = getSurfacePoint(rc, rc.camera.latitude, rc.camera.longitude); + return rc.cameraPoint.distanceTo(surfacePoint); + } + + void computeTruncatedSegment(Position p1, Position p2, Sector sector, List positions) { + if (p1 == null || p2 == null) + return; + + boolean p1In = sector.contains(p1.latitude, p1.longitude); + boolean p2In = sector.contains(p2.latitude, p2.longitude); + if (!p1In && !p2In) { + // whole segment is (likely) outside + return; + } + if (p1In && p2In) { + // whole segment is (likely) inside + positions.add(p1); + positions.add(p2); + } else { + // segment does cross the boundary + Position outPoint = !p1In ? p1 : p2; + Position inPoint = p1In ? p1 : p2; + for (int i = 1; i <= 2; i++) { // there may be two intersections + Location intersection = null; + if (outPoint.longitude > sector.maxLongitude() + || (sector.maxLongitude() == 180 && outPoint.longitude < 0)) + { + // intersect with east meridian + intersection = this.greatCircleIntersectionAtLongitude( + inPoint, outPoint, sector.maxLongitude()); + } else if (outPoint.longitude < sector.minLongitude() + || (sector.minLongitude() == -180 && outPoint.longitude > 0)) { + // intersect with west meridian + intersection = this.greatCircleIntersectionAtLongitude( + inPoint, outPoint, sector.minLongitude()); + } else if (outPoint.latitude > sector.maxLatitude()) { + // intersect with top parallel + intersection = this.greatCircleIntersectionAtLatitude( + inPoint, outPoint, sector.maxLatitude()); + } else if (outPoint.latitude < sector.minLatitude()) { + // intersect with bottom parallel + intersection = this.greatCircleIntersectionAtLatitude( + inPoint, outPoint, sector.minLatitude()); + } + if (intersection != null) + outPoint = new Position(intersection.latitude, intersection.longitude, outPoint.altitude); + else + break; + } + positions.add(inPoint); + positions.add(outPoint); + } + } + + /** + * Computes the intersection point position between a great circle segment and a meridian. + * + * @param p1 the great circle segment start position. + * @param p2 the great circle segment end position. + * @param longitude the meridian longitude Angle + * + * @return the intersection Position or null if there was no intersection found. + */ + private Location greatCircleIntersectionAtLongitude(Location p1, Location p2, double longitude) { + if (p1.longitude == longitude) + return p1; + if (p2.longitude == longitude) + return p2; + Location pos = null; + double deltaLon = this.getDeltaLongitude(p1, p2.longitude); + if (this.getDeltaLongitude(p1, longitude) < deltaLon && this.getDeltaLongitude(p2, longitude) < deltaLon) { + int count = 0; + double precision = 1d / 6378137d; // 1m angle in radians + Location a = p1; + Location b = p2; + Location midPoint = this.greatCircleMidPoint(a, b); + while (Math.toRadians(this.getDeltaLongitude(midPoint, longitude)) > precision && count <= 20) { + count++; + if (this.getDeltaLongitude(a, longitude) < this.getDeltaLongitude(b, longitude)) + b = midPoint; + else + a = midPoint; + midPoint = this.greatCircleMidPoint(a, b); + } + pos = midPoint; + } + // Adjust final longitude for an exact match + if (pos != null) + pos = new Location(pos.latitude, longitude); + return pos; + } + + /** + * Computes the intersection point position between a great circle segment and a parallel. + * + * @param p1 the great circle segment start position. + * @param p2 the great circle segment end position. + * @param latitude the parallel latitude Angle + * + * @return the intersection Position or null if there was no intersection found. + */ + private Location greatCircleIntersectionAtLatitude(Location p1, Location p2, double latitude) { + Location pos = null; + if (Math.signum(p1.latitude - latitude) != Math.signum(p2.latitude - latitude)) { + int count = 0; + double precision = 1d / 6378137d; // 1m angle in radians + Location a = p1; + Location b = p2; + Location midPoint = this.greatCircleMidPoint(a, b); + while (Math.abs(Math.toRadians(midPoint.latitude) - Math.toRadians(latitude)) > precision && count <= 20) { + count++; + if (Math.signum(a.latitude - latitude) + != Math.signum(midPoint.latitude - latitude)) + b = midPoint; + else + a = midPoint; + midPoint = this.greatCircleMidPoint(a, b); + } + pos = midPoint; + } + // Adjust final latitude for an exact match + if (pos != null) + pos = new Location(latitude, pos.longitude); + return pos; + } + + private Location greatCircleMidPoint(Location p1, Location p2) { + double azimuth = p1.greatCircleAzimuth(p2); + double distance = p1.greatCircleDistance(p2); + return p1.greatCircleLocation(azimuth, distance / 2, new Location()); + } + + private double getDeltaLongitude(Location p1, double longitude) { + double deltaLon = Math.abs(p1.longitude - longitude); + return deltaLon < 180 ? deltaLon : 360 - deltaLon; + } + + private void calculateLookAtProperties(RenderContext rc) { + if (!rc.hasUserProperty(LOOK_AT_LATITUDE_PROPERTY) || !rc.hasUserProperty(LOOK_AT_LONGITUDE_PROPERTY)) { + //rc.modelview.extractEyePoint(forwardRay.origin); + forwardRay.origin.set(rc.cameraPoint); + rc.modelview.extractForwardVector(forwardRay.direction); + + double range; + if (rc.globe.intersect(forwardRay, lookAtPoint)) { + rc.globe.cartesianToGeographic(lookAtPoint.x, lookAtPoint.y, lookAtPoint.z, lookAtPos); + rc.putUserProperty(LOOK_AT_LATITUDE_PROPERTY, lookAtPos.latitude); + rc.putUserProperty(LOOK_AT_LONGITUDE_PROPERTY, lookAtPos.longitude); + range = lookAtPoint.distanceTo(rc.cameraPoint); + } else { + rc.putUserProperty(LOOK_AT_LATITUDE_PROPERTY, null); + rc.putUserProperty(LOOK_AT_LONGITUDE_PROPERTY, null); + range = rc.horizonDistance; + } + + double pixelSizeMeters = rc.pixelSizeAtDistance(range); + rc.putUserProperty(GRATICULE_PIXEL_SIZE_PROPERTY, pixelSizeMeters); + + double pixelSizeDegrees = Math.toDegrees(pixelSizeMeters / rc.globe.getEquatorialRadius()); + rc.putUserProperty(GRATICULE_LABEL_OFFSET_PROPERTY, pixelSizeDegrees * rc.viewport.width / 4); + } + } + +} diff --git a/worldwind/src/main/java/gov/nasa/worldwind/layer/graticule/AbstractGraticuleTile.java b/worldwind/src/main/java/gov/nasa/worldwind/layer/graticule/AbstractGraticuleTile.java new file mode 100644 index 000000000..774737d8b --- /dev/null +++ b/worldwind/src/main/java/gov/nasa/worldwind/layer/graticule/AbstractGraticuleTile.java @@ -0,0 +1,119 @@ +package gov.nasa.worldwind.layer.graticule; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import gov.nasa.worldwind.geom.BoundingBox; +import gov.nasa.worldwind.geom.Sector; +import gov.nasa.worldwind.geom.Vec3; +import gov.nasa.worldwind.render.RenderContext; + +abstract class AbstractGraticuleTile { + + private final AbstractGraticuleLayer layer; + private final Sector sector; + + private List gridElements; + + private BoundingBox extent; + private float[] heightLimits; + private long heightLimitsTimestamp; + private double extentExaggeration; + + AbstractGraticuleTile(AbstractGraticuleLayer layer, Sector sector) { + this.layer = layer; + this.sector = sector; + } + + AbstractGraticuleLayer getLayer() { + return this.layer; + } + + Sector getSector() { + return this.sector; + } + + List getGridElements() { + return this.gridElements; + } + + boolean isInView(RenderContext rc) { + return this.getExtent(rc).intersectsFrustum(rc.frustum); + } + + double getSizeInPixels(RenderContext rc) { + Vec3 centerPoint = layer.getSurfacePoint(rc, this.sector.centroidLatitude(), this.sector.centroidLongitude()); + double distance = rc.cameraPoint.distanceTo(centerPoint); + double tileSizeMeter = Math.toRadians(this.sector.deltaLatitude()) * rc.globe.getEquatorialRadius(); + return tileSizeMeter / rc.pixelSizeAtDistance(distance) / rc.resources.getDisplayMetrics().density; + } + + void selectRenderables(RenderContext rc) { + if (this.gridElements == null) + this.createRenderables(); + } + + void clearRenderables() { + if (this.gridElements != null) { + this.gridElements.clear(); + this.gridElements = null; + } + } + + void createRenderables() { + this.gridElements = new ArrayList<>(); + } + + Sector[] subdivide(int div) { + double dLat = this.getSector().deltaLatitude() / div; + double dLon = this.getSector().deltaLongitude() / div; + + Sector[] sectors = new Sector[div * div]; + int idx = 0; + for (int row = 0; row < div; row++) { + for (int col = 0; col < div; col++) { + sectors[idx++] = Sector.fromDegrees(this.getSector().minLatitude() + dLat * row, + this.getSector().minLongitude() + dLon * col, dLat, dLon); + } + } + + return sectors; + } + + private BoundingBox getExtent(RenderContext rc) { + if (this.heightLimits == null) { + this.heightLimits = new float[2]; + } + + if (this.extent == null) { + this.extent = new BoundingBox(); + } + + long elevationTimestamp = rc.globe.getElevationModel().getTimestamp(); + if (elevationTimestamp != this.heightLimitsTimestamp) { + // initialize the heights for elevation model scan + this.heightLimits[0] = Float.MAX_VALUE; + this.heightLimits[1] = -Float.MAX_VALUE; + rc.globe.getElevationModel().getHeightLimits(this.sector, this.heightLimits); + // check for valid height limits + if (this.heightLimits[0] > this.heightLimits[1]) { + Arrays.fill(this.heightLimits, 0f); + } + } + + double verticalExaggeration = rc.verticalExaggeration; + if (verticalExaggeration != this.extentExaggeration || + elevationTimestamp != this.heightLimitsTimestamp) { + float minHeight = (float) (this.heightLimits[0] * verticalExaggeration); + float maxHeight = (float) (this.heightLimits[1] * verticalExaggeration); + this.extent.setToSector(this.sector, rc.globe, minHeight, maxHeight); + } + + this.heightLimitsTimestamp = elevationTimestamp; + this.extentExaggeration = verticalExaggeration; + + return this.extent; + } + +} diff --git a/worldwind/src/main/java/gov/nasa/worldwind/layer/graticule/AbstractLatLonGraticuleLayer.java b/worldwind/src/main/java/gov/nasa/worldwind/layer/graticule/AbstractLatLonGraticuleLayer.java new file mode 100644 index 000000000..96e13d1b9 --- /dev/null +++ b/worldwind/src/main/java/gov/nasa/worldwind/layer/graticule/AbstractLatLonGraticuleLayer.java @@ -0,0 +1,219 @@ +/* + * 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.layer.graticule; + +import java.util.ArrayList; +import java.util.List; + +import gov.nasa.worldwind.geom.Location; +import gov.nasa.worldwind.geom.Position; +import gov.nasa.worldwind.geom.Sector; +import gov.nasa.worldwind.render.RenderContext; + +abstract class AbstractLatLonGraticuleLayer extends AbstractGraticuleLayer implements GridTilesSupport.Callback { + + public enum AngleFormat { + DD, DM, DMS + } + + private final GridTilesSupport gridTilesSupport; + private final List latitudeLabels = new ArrayList<>(); + private final List longitudeLabels = new ArrayList<>(); + private AngleFormat angleFormat = AngleFormat.DMS; + + AbstractLatLonGraticuleLayer(String name) { + super(name); + this.gridTilesSupport = new GridTilesSupport(this, 18, 36); + } + + /** + * Get the graticule division and angular display format. Can be one of {@link AngleFormat#DD} + * or {@link AngleFormat#DMS}. + * + * @return the graticule division and angular display format. + */ + public AngleFormat getAngleFormat() { + return this.angleFormat; + } + + /** + * Sets the graticule division and angular display format. Can be one of {@link AngleFormat#DD}, + * {@link AngleFormat#DMS} of {@link AngleFormat#DM}. + * + * @param format the graticule division and angular display format. + */ + public void setAngleFormat(AngleFormat format) { + if (this.angleFormat.equals(format)) + return; + + this.angleFormat = format; + this.gridTilesSupport.clearTiles(); + } + + @Override + protected void clear(RenderContext rc) { + super.clear(rc); + this.latitudeLabels.clear(); + this.longitudeLabels.clear(); + } + + @Override + protected void selectRenderables(RenderContext rc) { + this.gridTilesSupport.selectRenderables(rc); + } + + @Override + public Sector getGridSector(int row, int col) { + int minLat = -90 + row * 10; + int maxLat = minLat + 10; + int minLon = -180 + col * 10; + int maxLon = minLon + 10; + return Sector.fromDegrees(minLat, minLon, maxLat - minLat, maxLon - minLon); + } + + @Override + public int getGridColumn(double longitude) { + return Math.min((int) Math.floor((longitude + 180) / 10d), 35); + } + + @Override + public int getGridRow(double latitude) { + return Math.min((int) Math.floor((latitude + 90) / 10d), 17); + } + + void addLabel(double value, String labelType, String graticuleType, double resolution, Location labelOffset) { + Position position = null; + if (labelType.equals(GridElement.TYPE_LATITUDE_LABEL)) { + if (!this.latitudeLabels.contains(value)) { + this.latitudeLabels.add(value); + position = Position.fromDegrees(value, labelOffset.longitude, 0); + } + } else if (labelType.equals(GridElement.TYPE_LONGITUDE_LABEL)) { + if (!this.longitudeLabels.contains(value)) { + this.longitudeLabels.add(value); + position = Position.fromDegrees(labelOffset.latitude, value, 0); + } + } + if (position != null) { + String label = makeAngleLabel(value, resolution); + this.addRenderable(this.createTextRenderable(position, label, resolution), graticuleType); + } + } + + private String toDecimalDegreesString(double angle, int digits) { + return String.format("%." + digits + "f\u00B0", angle); + } + + private String toDMSString(double angle) { + int sign = (int) Math.signum(angle); + angle *= sign; + int d = (int) Math.floor(angle); + angle = (angle - d) * 60d; + int m = (int) Math.floor(angle); + angle = (angle - m) * 60d; + int s = (int) Math.round(angle); + + if (s == 60) { + m++; + s = 0; + } + if (m == 60) { + d++; + m = 0; + } + + return (sign == -1 ? "-" : "") + d + '\u00B0' + ' ' + m + '\u2019' + ' ' + s + '\u201d'; + } + + private String toDMString(double angle) { + int sign = (int) Math.signum(angle); + angle *= sign; + int d = (int) Math.floor(angle); + angle = (angle - d) * 60d; + int m = (int) Math.floor(angle); + angle = (angle - m) * 60d; + int s = (int) Math.round(angle); + + if (s == 60) { + m++; + s = 0; + } + if (m == 60) { + d++; + m = 0; + } + + double mf = s == 0 ? m : m + s / 60.0; + + return (sign == -1 ? "-" : "") + d + '\u00B0' + ' ' + String.format("%5.2f", mf) + '\u2019'; + } + + private double[] toDMS(double angle) { + int sign = (int) Math.signum(angle); + + angle *= sign; + int d = (int) Math.floor(angle); + angle = (angle - d) * 60d; + int m = (int) Math.floor(angle); + angle = (angle - m) * 60d; + double s = Math.rint(angle * 100) / 100; // keep two decimals for seconds + + if (s == 60) { + m++; + s = 0; + } + if (m == 60) { + d++; + m = 0; + } + + return new double[] {sign * d, m, s}; + } + + private String makeAngleLabel(double angle, double resolution) { + double epsilon = .000000001; + String label; + if (this.getAngleFormat().equals(AngleFormat.DMS)) { + if (resolution >= 1) + label = toDecimalDegreesString(angle, 0); + else { + double[] dms = toDMS(angle); + if (dms[1] < epsilon && dms[2] < epsilon) + label = String.format("%4d\u00B0", (int) dms[0]); + else if (dms[2] < epsilon) + label = String.format("%4d\u00B0 %2d\u2019", (int) dms[0], (int) dms[1]); + else + label = toDMSString(angle); + } + } else if (this.getAngleFormat().equals(AngleFormat.DM)) { + if (resolution >= 1) + label = toDecimalDegreesString(angle,0); + else { + double[] dms = toDMS(angle); + if (dms[1] < epsilon && dms[2] < epsilon) + label = String.format("%4d\u00B0", (int) dms[0]); + else if (dms[2] < epsilon) + label = String.format("%4d\u00B0 %2d\u2019", (int) dms[0], (int) dms[1]); + else + label = toDMString(angle); + } + } else { // default to decimal degrees + if (resolution >= 1) + label = toDecimalDegreesString(angle, 0); + else if (resolution >= .1) + label = toDecimalDegreesString(angle, 1); + else if (resolution >= .01) + label = toDecimalDegreesString(angle, 2); + else if (resolution >= .001) + label = toDecimalDegreesString(angle, 3); + else + label = toDecimalDegreesString(angle, 4); + } + + return label; + } + +} diff --git a/worldwind/src/main/java/gov/nasa/worldwind/layer/graticule/AbstractUTMGraticuleLayer.java b/worldwind/src/main/java/gov/nasa/worldwind/layer/graticule/AbstractUTMGraticuleLayer.java new file mode 100644 index 000000000..5d25aa4de --- /dev/null +++ b/worldwind/src/main/java/gov/nasa/worldwind/layer/graticule/AbstractUTMGraticuleLayer.java @@ -0,0 +1,194 @@ +/* + * 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.layer.graticule; + +import android.content.res.Resources; +import android.graphics.Typeface; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import gov.nasa.worldwind.geom.Position; +import gov.nasa.worldwind.geom.Sector; +import gov.nasa.worldwind.geom.coords.Hemisphere; +import gov.nasa.worldwind.geom.coords.UPSCoord; +import gov.nasa.worldwind.geom.coords.UTMCoord; +import gov.nasa.worldwind.render.Color; +import gov.nasa.worldwind.render.RenderContext; + +/** + * Displays the UTM graticule metric scale. + * + * @author Patrick Murris + * @version $Id: UTMBaseGraticuleLayer.java 2153 2014-07-17 17:33:13Z tgaskins $ + */ +public abstract class AbstractUTMGraticuleLayer extends AbstractGraticuleLayer { + + static final int UTM_MIN_LATITUDE = -80; + static final int UTM_MAX_LATITUDE = 84; + + /** Graticule for the 100,000 meter grid. */ + private static final String GRATICULE_UTM_100000M = "Graticule.UTM.100000m"; + /** Graticule for the 10,000 meter grid. */ + private static final String GRATICULE_UTM_10000M = "Graticule.UTM.10000m"; + /** Graticule for the 1,000 meter grid. */ + private static final String GRATICULE_UTM_1000M = "Graticule.UTM.1000m"; + /** Graticule for the 100 meter grid. */ + private static final String GRATICULE_UTM_100M = "Graticule.UTM.100m"; + /** Graticule for the 10 meter grid. */ + private static final String GRATICULE_UTM_10M = "Graticule.UTM.10m"; + /** Graticule for the 1 meter grid. */ + private static final String GRATICULE_UTM_1M = "Graticule.UTM.1m"; + + private static final double ONEHT = 100e3; + + private final UTMMetricScaleSupport metricScaleSupport; + + AbstractUTMGraticuleLayer(String name, int scaleModulo, double maxResolution) { + super(name); + this.metricScaleSupport = new UTMMetricScaleSupport(this); + this.metricScaleSupport.setScaleModulo(scaleModulo); + this.metricScaleSupport.setMaxResolution(maxResolution); + } + + @Override + protected void initRenderingParams() { + GraticuleRenderingParams params; + // 100,000 meter graticule + params = new GraticuleRenderingParams(); + params.put(GraticuleRenderingParams.KEY_LINE_COLOR, new Color(android.graphics.Color.GREEN)); + params.put(GraticuleRenderingParams.KEY_LABEL_COLOR, new Color(android.graphics.Color.GREEN)); + params.put(GraticuleRenderingParams.KEY_LABEL_TYPEFACE, Typeface.create("arial", Typeface.BOLD)); + params.put(GraticuleRenderingParams.KEY_LABEL_SIZE, 14f * Resources.getSystem().getDisplayMetrics().scaledDensity); + setRenderingParams(GRATICULE_UTM_100000M, params); + // 10,000 meter graticule + params = new GraticuleRenderingParams(); + params.put(GraticuleRenderingParams.KEY_LINE_COLOR, new Color(android.graphics.Color.rgb(0, 102, 255))); + params.put(GraticuleRenderingParams.KEY_LABEL_COLOR, new Color(android.graphics.Color.rgb(0, 102, 255))); + setRenderingParams(GRATICULE_UTM_10000M, params); + // 1,000 meter graticule + params = new GraticuleRenderingParams(); + params.put(GraticuleRenderingParams.KEY_LINE_COLOR, new Color(android.graphics.Color.CYAN)); + params.put(GraticuleRenderingParams.KEY_LABEL_COLOR, new Color(android.graphics.Color.CYAN)); + setRenderingParams(GRATICULE_UTM_1000M, params); + // 100 meter graticule + params = new GraticuleRenderingParams(); + params.put(GraticuleRenderingParams.KEY_LINE_COLOR, new Color(android.graphics.Color.rgb(0, 153, 153))); + params.put(GraticuleRenderingParams.KEY_LABEL_COLOR, new Color(android.graphics.Color.rgb(0, 153, 153))); + setRenderingParams(GRATICULE_UTM_100M, params); + // 10 meter graticule + params = new GraticuleRenderingParams(); + params.put(GraticuleRenderingParams.KEY_LINE_COLOR, new Color(android.graphics.Color.rgb(102, 255, 204))); + params.put(GraticuleRenderingParams.KEY_LABEL_COLOR, new Color(android.graphics.Color.rgb(102, 255, 204))); + setRenderingParams(GRATICULE_UTM_10M, params); + // 1 meter graticule + params = new GraticuleRenderingParams(); + params.put(GraticuleRenderingParams.KEY_LINE_COLOR, new Color(android.graphics.Color.rgb(153, 153, 255))); + params.put(GraticuleRenderingParams.KEY_LABEL_COLOR, new Color(android.graphics.Color.rgb(153, 153, 255))); + setRenderingParams(GRATICULE_UTM_1M, params); + } + + @Override + protected List getOrderedTypes() { + return Arrays.asList( + GRATICULE_UTM_100000M, + GRATICULE_UTM_10000M, + GRATICULE_UTM_1000M, + GRATICULE_UTM_100M, + GRATICULE_UTM_10M, + GRATICULE_UTM_1M); + } + + @Override + protected String getTypeFor(double resolution) { + if (resolution >= 100000) + return GRATICULE_UTM_100000M; + else if (resolution >= 10000) + return GRATICULE_UTM_10000M; + else if (resolution >= 1000) + return GRATICULE_UTM_1000M; + else if (resolution >= 100) + return GRATICULE_UTM_100M; + else if (resolution >= 10) + return GRATICULE_UTM_10M; + else if (resolution >= 1) + return GRATICULE_UTM_1M; + else + return null; + } + + @Override + protected void clear(RenderContext rc) { + super.clear(rc); + this.metricScaleSupport.clear(); + this.metricScaleSupport.computeZone(rc); + } + + @Override + protected void selectRenderables(RenderContext rc) { + this.metricScaleSupport.selectRenderables(rc); + } + + void computeMetricScaleExtremes(int UTMZone, Hemisphere hemisphere, GridElement ge, double size) { + this.metricScaleSupport.computeMetricScaleExtremes(UTMZone, hemisphere, ge, size); + } + + Position computePosition(int zone, Hemisphere hemisphere, double easting, double northing) { + return zone > 0 ? + computePositionFromUTM(zone, hemisphere, easting, northing) : + computePositionFromUPS(hemisphere, easting, northing); + } + + private Position computePositionFromUTM(int zone, Hemisphere hemisphere, double easting, double northing) { + UTMCoord UTM = UTMCoord.fromUTM(zone, hemisphere, easting, northing); + return Position.fromDegrees(Position.clampLatitude(UTM.getLatitude()), + Position.clampLongitude(UTM.getLongitude()), 10e3); + } + + private Position computePositionFromUPS(Hemisphere hemisphere, double easting, double northing) { + UPSCoord UPS = UPSCoord.fromUPS(hemisphere, easting, northing); + return Position.fromDegrees(Position.clampLatitude(UPS.getLatitude()), + Position.clampLongitude(UPS.getLongitude()), 10e3); + } + + List createSquaresGrid(int UTMZone, Hemisphere hemisphere, Sector UTMZoneSector, + double minEasting, double maxEasting, double minNorthing, double maxNorthing) { + List squares = new ArrayList<>(); + double startEasting = Math.floor(minEasting / ONEHT) * ONEHT; + double startNorthing = Math.floor(minNorthing / ONEHT) * ONEHT; + int cols = (int) Math.ceil((maxEasting - startEasting) / ONEHT); + int rows = (int) Math.ceil((maxNorthing - startNorthing) / ONEHT); + UTMSquareZone[][] squaresArray = new UTMSquareZone[rows][cols]; + int col = 0; + for (double easting = startEasting; easting < maxEasting; easting += ONEHT) { + int row = 0; + for (double northing = startNorthing; northing < maxNorthing; northing += ONEHT) { + UTMSquareZone sz = new UTMSquareZone(this, UTMZone, hemisphere, UTMZoneSector, easting, northing, ONEHT); + if (sz.boundingSector != null && !sz.isOutsideGridZone()) { + squares.add(sz); + squaresArray[row][col] = sz; + } + row++; + } + col++; + } + + // Keep track of neighbors + for (col = 0; col < cols; col++) { + for (int row = 0; row < rows; row++) { + UTMSquareZone sz = squaresArray[row][col]; + if (sz != null) { + sz.setNorthNeighbor(row + 1 < rows ? squaresArray[row + 1][col] : null); + sz.setEastNeighbor(col + 1 < cols ? squaresArray[row][col + 1] : null); + } + } + } + + return squares; + } + +} diff --git a/worldwind/src/main/java/gov/nasa/worldwind/layer/graticule/GARSGraticuleLayer.java b/worldwind/src/main/java/gov/nasa/worldwind/layer/graticule/GARSGraticuleLayer.java new file mode 100644 index 000000000..2a85821e2 --- /dev/null +++ b/worldwind/src/main/java/gov/nasa/worldwind/layer/graticule/GARSGraticuleLayer.java @@ -0,0 +1,99 @@ +/* + * Copyright (C) 2014 United States Government as represented by the Administrator of the + * National Aeronautics and Space Administration. + * All Rights Reserved. + */ +package gov.nasa.worldwind.layer.graticule; + +import android.content.res.Resources; +import android.graphics.Typeface; + +import java.util.Arrays; +import java.util.List; + +import gov.nasa.worldwind.geom.Sector; +import gov.nasa.worldwind.render.Color; + +/** + * Displays the geographic Global Area Reference System (GARS) graticule. The graticule has four levels. The first level + * displays lines of latitude and longitude. The second level displays 30 minute square grid cells. The third level + * displays 15 minute grid cells. The fourth and final level displays 5 minute grid cells. + * + * This graticule is intended to be used on 2D globes because it is so dense. + * + * @version $Id: GARSGraticuleLayer.java 2384 2014-10-14 21:55:10Z tgaskins $ + */ +public class GARSGraticuleLayer extends AbstractLatLonGraticuleLayer { + + private static final String GRATICULE_GARS_LEVEL_0 = "Graticule.GARSLevel0"; + private static final String GRATICULE_GARS_LEVEL_1 = "Graticule.GARSLevel1"; + private static final String GRATICULE_GARS_LEVEL_2 = "Graticule.GARSLevel2"; + private static final String GRATICULE_GARS_LEVEL_3 = "Graticule.GARSLevel3"; + + public GARSGraticuleLayer() { + super("GARS Graticule"); + } + + @Override + protected void initRenderingParams() { + GraticuleRenderingParams params; + // Ten degrees grid + params = new GraticuleRenderingParams(); + params.put(GraticuleRenderingParams.KEY_LINE_COLOR, new Color(android.graphics.Color.WHITE)); + params.put(GraticuleRenderingParams.KEY_LABEL_COLOR,new Color(android.graphics.Color.WHITE)); + params.put(GraticuleRenderingParams.KEY_LABEL_TYPEFACE, Typeface.create("arial", Typeface.BOLD)); + params.put(GraticuleRenderingParams.KEY_LABEL_SIZE, 16f * Resources.getSystem().getDisplayMetrics().scaledDensity); + setRenderingParams(GRATICULE_GARS_LEVEL_0, params); + // One degree + params = new GraticuleRenderingParams(); + params.put(GraticuleRenderingParams.KEY_LINE_COLOR, new Color(android.graphics.Color.YELLOW)); + params.put(GraticuleRenderingParams.KEY_LABEL_COLOR, new Color(android.graphics.Color.YELLOW)); + params.put(GraticuleRenderingParams.KEY_LABEL_TYPEFACE, Typeface.create("arial", Typeface.BOLD)); + params.put(GraticuleRenderingParams.KEY_LABEL_SIZE, 14f * Resources.getSystem().getDisplayMetrics().scaledDensity); + setRenderingParams(GRATICULE_GARS_LEVEL_1, params); + // 1/10th degree - 1/6th (10 minutes) + params = new GraticuleRenderingParams(); + params.put(GraticuleRenderingParams.KEY_LINE_COLOR, new Color(android.graphics.Color.GREEN)); + params.put(GraticuleRenderingParams.KEY_LABEL_COLOR, new Color(android.graphics.Color.GREEN)); + setRenderingParams(GRATICULE_GARS_LEVEL_2, params); + // 1/100th degree - 1/60th (one minutes) + params = new GraticuleRenderingParams(); + params.put(GraticuleRenderingParams.KEY_LINE_COLOR, new Color(android.graphics.Color.CYAN)); + params.put(GraticuleRenderingParams.KEY_LABEL_COLOR, new Color(android.graphics.Color.CYAN)); + setRenderingParams(GRATICULE_GARS_LEVEL_3, params); + } + + @Override + protected List getOrderedTypes() { + return Arrays.asList( + GRATICULE_GARS_LEVEL_0, + GRATICULE_GARS_LEVEL_1, + GRATICULE_GARS_LEVEL_2, + GRATICULE_GARS_LEVEL_3); + } + + @Override + protected String getTypeFor(double resolution) { + if (resolution >= 10) + return GRATICULE_GARS_LEVEL_0; + else if (resolution >= 0.5) + return GRATICULE_GARS_LEVEL_1; + else if (resolution >= .25) + return GRATICULE_GARS_LEVEL_2; + else if (resolution >= 5.0 / 60.0) + return GRATICULE_GARS_LEVEL_3; + + return null; + } + + @Override + public AbstractGraticuleTile[][] initGridTiles(int rows, int cols) { + return new GARSGraticuleTile[rows][cols]; + } + + @Override + public AbstractGraticuleTile createGridTile(Sector sector) { + return new GARSGraticuleTile(this, sector, 20, 0); + } + +} diff --git a/worldwind/src/main/java/gov/nasa/worldwind/layer/graticule/GARSGraticuleTile.java b/worldwind/src/main/java/gov/nasa/worldwind/layer/graticule/GARSGraticuleTile.java new file mode 100644 index 000000000..d78ecfd33 --- /dev/null +++ b/worldwind/src/main/java/gov/nasa/worldwind/layer/graticule/GARSGraticuleTile.java @@ -0,0 +1,313 @@ +package gov.nasa.worldwind.layer.graticule; + +import java.util.ArrayList; +import java.util.List; + +import gov.nasa.worldwind.WorldWind; +import gov.nasa.worldwind.geom.Location; +import gov.nasa.worldwind.geom.Position; +import gov.nasa.worldwind.geom.Sector; +import gov.nasa.worldwind.render.RenderContext; +import gov.nasa.worldwind.render.Renderable; + +class GARSGraticuleTile extends AbstractGraticuleTile { + + /** + * Indicates the eye altitudes in meters below which each level should be displayed. + */ + private static final double[] THRESHOLDS = new double[] {600e3, 300e3, 90e3}; // 30 min, 15 min, 5 min + + /** + * Specifies the eye altitude below which the 30 minute grid is displayed. + * + * @param altitude the eye altitude in meters below which the 30 minute grid is displayed. + */ + public static void set30MinuteThreshold(double altitude) { + THRESHOLDS[0] = altitude; + } + + /** + * Indicates the eye altitude below which the 30 minute grid is displayed. + * + * @return the eye altitude in meters below which the 30 minute grid is displayed. + */ + public static double get30MinuteThreshold() { + return THRESHOLDS[0]; + } + + /** + * Specifies the eye altitude below which the 15 minute grid is displayed. + * + * @param altitude the eye altitude in meters below which the 15 minute grid is displayed. + */ + public static void set15MinuteThreshold(double altitude) { + THRESHOLDS[1] = altitude; + } + + /** + * Indicates the eye altitude below which the 15 minute grid is displayed. + * + * @return the eye altitude in meters below which the 15 minute grid is displayed. + */ + public static double get15MinuteThreshold() { + return THRESHOLDS[1]; + } + + /** + * Specifies the eye altitude below which the 5 minute grid is displayed. + * + * @param altitude the eye altitude in meters below which the 5 minute grid is displayed. + */ + public static void set5MinuteThreshold(double altitude) { + THRESHOLDS[2] = altitude; + } + + /** + * Indicates the eye altitude below which the 5 minute grid is displayed. + * + * @return the eye altitude in meters below which the 5 minute grid is displayed. + */ + public static double get5MinuteThreshold() { + return THRESHOLDS[2]; + } + + private static final List LAT_LABELS = new ArrayList<>(360); + private static final List LON_LABELS = new ArrayList<>(720); + private static final String CHARS = "ABCDEFGHJKLMNPQRSTUVWXYZ"; + private static final String[][] LEVEL_2_LABELS = new String[][] {{"3", "4"}, {"1", "2"}}; + + static { + for (int i = 1; i <= 720; i++) { + LON_LABELS.add(String.format("%03d", i)); + } + + for (int i = 0; i < 360; i++) { + int length = CHARS.length(); + int i1 = i / length; + int i2 = i % length; + LAT_LABELS.add(String.format("%c%c", CHARS.charAt(i1), CHARS.charAt(i2))); + } + } + + private static String makeLabelLevel1(Sector sector) { + int iLat = (int) ((90 + sector.centroidLatitude()) * 60 / 30); + int iLon = (int) ((180 + sector.centroidLongitude()) * 60 / 30); + + return LON_LABELS.get(iLon) + LAT_LABELS.get(iLat); + } + + private static String makeLabelLevel2(Sector sector) { + int minutesLat = (int) ((90 + sector.minLatitude()) * 60); + int j = (minutesLat % 30) / 15; + int minutesLon = (int) ((180 + sector.minLongitude()) * 60); + int i = (minutesLon % 30) / 15; + + return LEVEL_2_LABELS[j][i]; + } + + private final int divisions; + private final int level; + + private List subTiles; + + GARSGraticuleTile(GARSGraticuleLayer layer, Sector sector, int divisions, int level) { + super(layer, sector); + this.divisions = divisions; + this.level = level; + } + + @Override + GARSGraticuleLayer getLayer() { + return (GARSGraticuleLayer) super.getLayer(); + } + + @Override + boolean isInView(RenderContext rc) { + return super.isInView(rc) && (this.level == 0 || rc.camera.altitude <= THRESHOLDS[this.level - 1]); + } + + @Override + void selectRenderables(RenderContext rc) { + super.selectRenderables(rc); + + String graticuleType = getLayer().getTypeFor(this.getSector().deltaLatitude()); + if (this.level == 0 && rc.camera.altitude > THRESHOLDS[0]) { + Location labelOffset = getLayer().computeLabelOffset(rc); + + for (GridElement ge : this.getGridElements()) { + if (ge.isInView(rc)) { + // Add level zero bounding lines and labels + if (ge.type.equals(GridElement.TYPE_LINE_SOUTH) || ge.type.equals(GridElement.TYPE_LINE_NORTH) + || ge.type.equals(GridElement.TYPE_LINE_WEST)) { + getLayer().addRenderable(ge.renderable, graticuleType); + String labelType = ge.type.equals(GridElement.TYPE_LINE_SOUTH) + || ge.type.equals(GridElement.TYPE_LINE_NORTH) ? + GridElement.TYPE_LATITUDE_LABEL : GridElement.TYPE_LONGITUDE_LABEL; + getLayer().addLabel(ge.value, labelType, graticuleType, + this.getSector().deltaLatitude(), labelOffset); + } + } + } + + if (rc.camera.altitude > THRESHOLDS[0]) + return; + } + + // Select tile grid elements + double eyeDistance = rc.camera.altitude; + + if (this.level == 0 && eyeDistance <= THRESHOLDS[0] + || this.level == 1 && eyeDistance <= THRESHOLDS[1] + || this.level == 2) { + double resolution = this.getSector().deltaLatitude() / this.divisions; + graticuleType = getLayer().getTypeFor(resolution); + for (GridElement ge : this.getGridElements()) { + if (ge.isInView(rc)) { + getLayer().addRenderable(ge.renderable, graticuleType); + } + } + } + + if (this.level == 0 && eyeDistance > THRESHOLDS[1]) + return; + else if (this.level == 1 && eyeDistance > THRESHOLDS[2]) + return; + else if (this.level == 2) + return; + + // Select child elements + if (this.subTiles == null) + createSubTiles(); + for (GARSGraticuleTile gt : this.subTiles) { + if (gt.isInView(rc)) { + gt.selectRenderables(rc); + } else + gt.clearRenderables(); + } + } + + @Override + void clearRenderables() { + super.clearRenderables(); + if (this.subTiles != null) { + for (GARSGraticuleTile gt : this.subTiles) { + gt.clearRenderables(); + } + this.subTiles.clear(); + this.subTiles = null; + } + } + + private void createSubTiles() { + this.subTiles = new ArrayList<>(); + Sector[] sectors = this.subdivide(this.divisions); + int nextLevel = this.level + 1; + int subDivisions = 10; + if (nextLevel == 1) + subDivisions = 2; + else if (nextLevel == 2) + subDivisions = 3; + for (Sector s : sectors) { + this.subTiles.add(new GARSGraticuleTile(getLayer(), s, subDivisions, nextLevel)); + } + } + + @Override + void createRenderables() { + super.createRenderables(); + + double step = getSector().deltaLatitude() / this.divisions; + + // Generate meridians with labels + double lon = getSector().minLongitude() + (this.level == 0 ? 0 : step); + while (lon < getSector().maxLongitude() - step / 2) { + double longitude = lon; + // Meridian + List positions = new ArrayList<>(2); + positions.add(new Position(this.getSector().minLatitude(), longitude, 0)); + positions.add(new Position(this.getSector().maxLatitude(), longitude, 0)); + + Renderable line = getLayer().createLineRenderable(positions, WorldWind.LINEAR); + Sector sector = Sector.fromDegrees( + this.getSector().minLatitude(), lon, this.getSector().deltaLatitude(), 1E-15); + String lineType = lon == this.getSector().minLongitude() ? + GridElement.TYPE_LINE_WEST : GridElement.TYPE_LINE; + this.getGridElements().add(new GridElement(sector, line, lineType, lon)); + + // Increase longitude + lon += step; + } + + // Generate parallels + double lat = this.getSector().minLatitude() + (this.level == 0 ? 0 : step); + while (lat < this.getSector().maxLatitude() - step / 2) { + double latitude = lat; + List positions = new ArrayList<>(2); + positions.add(new Position(latitude, this.getSector().minLongitude(), 0)); + positions.add(new Position(latitude, this.getSector().maxLongitude(), 0)); + + Renderable line = getLayer().createLineRenderable(positions, WorldWind.LINEAR); + Sector sector = Sector.fromDegrees( + lat, this.getSector().minLongitude(), 1E-15, this.getSector().deltaLongitude()); + String lineType = lat == this.getSector().minLatitude() ? + GridElement.TYPE_LINE_SOUTH : GridElement.TYPE_LINE; + this.getGridElements().add(new GridElement(sector, line, lineType, lat)); + + // Increase latitude + lat += step; + } + + // Draw and label a parallel at the top of the graticule. The line is apparent only on 2D globes. + if (this.getSector().maxLatitude() == 90) { + List positions = new ArrayList<>(2); + positions.add(new Position(90, this.getSector().minLongitude(), 0)); + positions.add(new Position(90, this.getSector().maxLongitude(), 0)); + + Renderable line = getLayer().createLineRenderable(positions, WorldWind.LINEAR); + Sector sector = Sector.fromDegrees( + 90, this.getSector().minLongitude(), 1E-15, this.getSector().deltaLongitude()); + this.getGridElements().add(new GridElement(sector, line, GridElement.TYPE_LINE_NORTH, 90)); + } + + double resolution = this.getSector().deltaLatitude() / this.divisions; + if (this.level == 0) { + Sector[] sectors = this.subdivide(20); + for (int j = 0; j < 20; j++) { + for (int i = 0; i < 20; i++) { + Sector sector = sectors[j * 20 + i]; + String label = makeLabelLevel1(sector); + addLabel(label, sectors[j * 20 + i], resolution); + } + } + } else if (this.level == 1) { + String label = makeLabelLevel1(this.getSector()); + + Sector[] sectors = this.subdivide(2); + addLabel(label + "3", sectors[0], resolution); + addLabel(label + "4", sectors[1], resolution); + addLabel(label + "1", sectors[2], resolution); + addLabel(label + "2", sectors[3], resolution); + } else if (this.level == 2) { + String label = makeLabelLevel1(this.getSector()); + label += makeLabelLevel2(this.getSector()); + + resolution = 0.26; // make label priority a little higher than level 2's + Sector[] sectors = this.subdivide(3); + addLabel(label + "7", sectors[0], resolution); + addLabel(label + "8", sectors[1], resolution); + addLabel(label + "9", sectors[2], resolution); + addLabel(label + "4", sectors[3], resolution); + addLabel(label + "5", sectors[4], resolution); + addLabel(label + "6", sectors[5], resolution); + addLabel(label + "1", sectors[6], resolution); + addLabel(label + "2", sectors[7], resolution); + addLabel(label + "3", sectors[8], resolution); + } + } + + private void addLabel(String label, Sector sector, double resolution) { + Renderable text = this.getLayer().createTextRenderable(new Position(sector.centroidLatitude(), sector.centroidLongitude(), 0), label, resolution); + this.getGridElements().add(new GridElement(sector, text, GridElement.TYPE_GRIDZONE_LABEL)); + } + +} diff --git a/worldwind/src/main/java/gov/nasa/worldwind/layer/graticule/GraticuleRenderingParams.java b/worldwind/src/main/java/gov/nasa/worldwind/layer/graticule/GraticuleRenderingParams.java new file mode 100644 index 000000000..daf2982d3 --- /dev/null +++ b/worldwind/src/main/java/gov/nasa/worldwind/layer/graticule/GraticuleRenderingParams.java @@ -0,0 +1,120 @@ +/* + * 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.layer.graticule; + +import android.graphics.Typeface; + +import java.util.HashMap; + +import gov.nasa.worldwind.render.Color; + +/** + * @author dcollins + * @version $Id: GraticuleRenderingParams.java 1171 2013-02-11 21:45:02Z dcollins $ + */ +class GraticuleRenderingParams extends HashMap { + static final String KEY_DRAW_LINES = "DrawGraticule"; + static final String KEY_LINE_COLOR = "GraticuleLineColor"; + static final String KEY_LINE_WIDTH = "GraticuleLineWidth"; +// static final String KEY_LINE_STYLE = "GraticuleLineStyle"; +// static final String KEY_LINE_CONFORMANCE = "GraticuleLineConformance"; + static final String KEY_DRAW_LABELS = "DrawLabels"; + static final String KEY_LABEL_COLOR = "LabelColor"; + static final String KEY_LABEL_TYPEFACE = "LabelTypeface"; + static final String KEY_LABEL_SIZE = "LabelSize"; +// static final String VALUE_LINE_STYLE_SOLID = "LineStyleSolid"; +// static final String VALUE_LINE_STYLE_DASHED = "LineStyleDashed"; +// static final String VALUE_LINE_STYLE_DOTTED = "LineStyleDotted"; + + boolean isDrawLines() { + Object value = get(KEY_DRAW_LINES); + return value instanceof Boolean ? (Boolean) value : false; + } + + void setDrawLines(boolean drawLines) { + put(KEY_DRAW_LINES, drawLines); + } + + Color getLineColor() { + Object value = get(KEY_LINE_COLOR); + return value instanceof Color ? (Color) value : null; + } + + void setLineColor(Color color) { + put(KEY_LINE_COLOR, color); + } + + double getLineWidth() { + Object value = get(KEY_LINE_WIDTH); + return value instanceof Double ? (Double) value : 0; + } + + void setLineWidth(double lineWidth) { + put(KEY_LINE_WIDTH, lineWidth); + } + +// String getLineStyle() { +// Object value = get(KEY_LINE_STYLE); +// return value instanceof String ? (String) value : null; +// } +// +// void setLineStyle(String lineStyle) { +// put(KEY_LINE_STYLE, lineStyle); +// } + + boolean isDrawLabels() { + Object value = get(KEY_DRAW_LABELS); + return value instanceof Boolean ? (Boolean) value : false; + } + + void setDrawLabels(boolean drawLabels) { + put(KEY_DRAW_LABELS, drawLabels); + } + + Color getLabelColor() { + Object value = get(KEY_LABEL_COLOR); + return value instanceof Color ? (Color) value : null; + } + + void setLabelColor(Color color) { + put(KEY_LABEL_COLOR, color); + } + + Typeface getLabelTypeface() { + Object value = get(KEY_LABEL_TYPEFACE); + return value instanceof Typeface ? (Typeface) value : null; + } + + void setLabelTypeface(Typeface font) { + put(KEY_LABEL_TYPEFACE, font); + } + + Float getLabelSize() { + Object value = get(KEY_LABEL_SIZE); + return value instanceof Float ? (Float) value : null; + } + + void setLabelSize(Float size) { + put(KEY_LABEL_SIZE, size); + } + + String getStringValue(String key) { + Object value = this.get(key); + return value != null ? value.toString() : null; + } + + Float getFloatValue(String key) { + Object o = get(key); + if (o == null) return null; + + if (o instanceof Float) return (Float) o; + + String v = getStringValue(key); + + if (v == null) return null; + else return Float.parseFloat(v); + } +} diff --git a/worldwind/src/main/java/gov/nasa/worldwind/layer/graticule/GraticuleSupport.java b/worldwind/src/main/java/gov/nasa/worldwind/layer/graticule/GraticuleSupport.java new file mode 100644 index 000000000..8e19ecb16 --- /dev/null +++ b/worldwind/src/main/java/gov/nasa/worldwind/layer/graticule/GraticuleSupport.java @@ -0,0 +1,202 @@ +/* + * 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.layer.graticule; + +import android.content.res.Resources; +import android.graphics.Typeface; + +import java.util.HashMap; +import java.util.Map; + +import gov.nasa.worldwind.render.Color; +import gov.nasa.worldwind.render.RenderContext; +import gov.nasa.worldwind.render.Renderable; +import gov.nasa.worldwind.shape.Label; +import gov.nasa.worldwind.shape.Path; +import gov.nasa.worldwind.shape.ShapeAttributes; + +/** + * @author dcollins + * @version $Id: GraticuleSupport.java 2372 2014-10-10 18:32:15Z tgaskins $ + */ +public class GraticuleSupport { + + private Map renderables = new HashMap<>(); + private Map namedParams = new HashMap<>(); + private Map namedShapeAttributes = new HashMap<>(); + private GraticuleRenderingParams defaultParams; + + public void addRenderable(Renderable renderable, String paramsKey) { + this.renderables.put(renderable, paramsKey); + } + + void removeAllRenderables() { + this.renderables.clear(); + } + + public void render(RenderContext rc) { + this.render(rc, 1); + } + + public void render(RenderContext rc, double opacity) { + + this.namedShapeAttributes.clear(); + + // Render lines and collect text labels + for (Map.Entry entry : this.renderables.entrySet()) { + Renderable renderable = entry.getKey(); + String paramsKey = entry.getValue(); + GraticuleRenderingParams renderingParams = paramsKey != null ? this.namedParams.get(paramsKey) : null; + + if (renderable instanceof Path) { + if (renderingParams == null || renderingParams.isDrawLines()) { + applyRenderingParams(paramsKey, renderingParams, (Path) renderable, opacity); + renderable.render(rc); + } + } else if (renderable instanceof Label) { + if (renderingParams == null || renderingParams.isDrawLabels()) { + applyRenderingParams(renderingParams, (Label) renderable, opacity); + renderable.render(rc); + } + } + } + } + + GraticuleRenderingParams getRenderingParams(String key) { + GraticuleRenderingParams value = this.namedParams.get(key); + if (value == null) { + value = new GraticuleRenderingParams(); + initRenderingParams(value); + if (this.defaultParams != null) + value.putAll(this.defaultParams); + + this.namedParams.put(key, value); + } + + return value; + } + + void setRenderingParams(String key, GraticuleRenderingParams renderingParams) { + initRenderingParams(renderingParams); + this.namedParams.put(key, renderingParams); + } + + public GraticuleRenderingParams getDefaultParams() { + return this.defaultParams; + } + + public void setDefaultParams(GraticuleRenderingParams defaultParams) { + this.defaultParams = defaultParams; + } + + private void initRenderingParams(GraticuleRenderingParams params) { + if (params.get(GraticuleRenderingParams.KEY_DRAW_LINES) == null) + params.put(GraticuleRenderingParams.KEY_DRAW_LINES, Boolean.TRUE); + + if (params.get(GraticuleRenderingParams.KEY_LINE_COLOR) == null) + params.put(GraticuleRenderingParams.KEY_LINE_COLOR, new Color(android.graphics.Color.WHITE)); + + if (params.get(GraticuleRenderingParams.KEY_LINE_WIDTH) == null) + params.put(GraticuleRenderingParams.KEY_LINE_WIDTH, .5f * Resources.getSystem().getDisplayMetrics().density); + +// if (params.get(GraticuleRenderingParams.KEY_LINE_STYLE) == null) +// params.put(GraticuleRenderingParams.KEY_LINE_STYLE, GraticuleRenderingParams.VALUE_LINE_STYLE_SOLID); + + if (params.get(GraticuleRenderingParams.KEY_DRAW_LABELS) == null) + params.put(GraticuleRenderingParams.KEY_DRAW_LABELS, Boolean.TRUE); + + if (params.get(GraticuleRenderingParams.KEY_LABEL_COLOR) == null) + params.put(GraticuleRenderingParams.KEY_LABEL_COLOR, new Color(android.graphics.Color.WHITE)); + + if (params.get(GraticuleRenderingParams.KEY_LABEL_TYPEFACE) == null) + params.put(GraticuleRenderingParams.KEY_LABEL_TYPEFACE, Typeface.create("arial", Typeface.BOLD)); + + if (params.get(GraticuleRenderingParams.KEY_LABEL_SIZE) == null) + params.put(GraticuleRenderingParams.KEY_LABEL_SIZE, 12f * Resources.getSystem().getDisplayMetrics().scaledDensity); + } + + private void applyRenderingParams(GraticuleRenderingParams params, Label text, double opacity) { + if (params != null && text != null) { + // Apply "label" properties to the Label. + Object o = params.get(GraticuleRenderingParams.KEY_LABEL_COLOR); + if (o instanceof Color) { + Color color = applyOpacity((Color) o, opacity); + float[] compArray = new float[3]; + android.graphics.Color.colorToHSV(color.toColorInt(), compArray); + float colorValue = compArray[2] < .5f ? 1f : 0f; + text.getAttributes().setTextColor(color); + text.getAttributes().setOutlineColor(new Color(colorValue, colorValue, colorValue, color.alpha)); + } + + o = params.get(GraticuleRenderingParams.KEY_LABEL_TYPEFACE); + if (o instanceof Typeface) { + text.getAttributes().setTypeface((Typeface) o); + } + + o = params.get(GraticuleRenderingParams.KEY_LABEL_SIZE); + if (o instanceof Float) { + text.getAttributes().setTextSize((Float) o); + } + } + } + + private void applyRenderingParams(String key, GraticuleRenderingParams params, Path path, double opacity) { + if (key != null && params != null && path != null) { + path.setAttributes(this.getLineShapeAttributes(key, params, opacity)); + } + } + + private ShapeAttributes getLineShapeAttributes(String key, GraticuleRenderingParams params, double opacity) { + ShapeAttributes attrs = this.namedShapeAttributes.get(key); + if (attrs == null) { + attrs = createLineShapeAttributes(params, opacity); + this.namedShapeAttributes.put(key, attrs); + } + return attrs; + } + + private ShapeAttributes createLineShapeAttributes(GraticuleRenderingParams params, double opacity) { + ShapeAttributes attrs = new ShapeAttributes(); + attrs.setDrawInterior(false); + attrs.setDrawOutline(true); + if (params != null) { + // Apply "line" properties. + Object o = params.get(GraticuleRenderingParams.KEY_LINE_COLOR); + if (o instanceof Color) { + attrs.setOutlineColor(applyOpacity((Color) o, opacity)); + } + + Float lineWidth = params.getFloatValue(GraticuleRenderingParams.KEY_LINE_WIDTH); + if (lineWidth != null) { + attrs.setOutlineWidth(lineWidth); + } + +// String s = params.getStringValue(GraticuleRenderingParams.KEY_LINE_STYLE); +// // Draw a solid line. +// if (GraticuleRenderingParams.VALUE_LINE_STYLE_SOLID.equalsIgnoreCase(s)) { +// attrs.setOutlineStipplePattern((short) 0xAAAA); +// attrs.setOutlineStippleFactor(0); +// } +// // Draw the line as longer strokes with space in between. +// else if (GraticuleRenderingParams.VALUE_LINE_STYLE_DASHED.equalsIgnoreCase(s)) { +// int baseFactor = (int) (lineWidth != null ? Math.round(lineWidth) : 1.0); +// attrs.setOutlineStipplePattern((short) 0xAAAA); +// attrs.setOutlineStippleFactor(3 * baseFactor); +// } +// // Draw the line as a evenly spaced "square" dots. +// else if (GraticuleRenderingParams.VALUE_LINE_STYLE_DOTTED.equalsIgnoreCase(s)) { +// int baseFactor = (int) (lineWidth != null ? Math.round(lineWidth) : 1.0); +// attrs.setOutlineStipplePattern((short) 0xAAAA); +// attrs.setOutlineStippleFactor(baseFactor); +// } + } + return attrs; + } + + private Color applyOpacity(Color color, double opacity) { + return opacity >= 1 ? color : new Color(color.red, color.green, color.blue, color.alpha * (float) opacity); + } +} diff --git a/worldwind/src/main/java/gov/nasa/worldwind/layer/graticule/GridElement.java b/worldwind/src/main/java/gov/nasa/worldwind/layer/graticule/GridElement.java new file mode 100644 index 000000000..385a29862 --- /dev/null +++ b/worldwind/src/main/java/gov/nasa/worldwind/layer/graticule/GridElement.java @@ -0,0 +1,39 @@ +package gov.nasa.worldwind.layer.graticule; + +import gov.nasa.worldwind.geom.Sector; +import gov.nasa.worldwind.render.RenderContext; +import gov.nasa.worldwind.render.Renderable; + +class GridElement { + final static String TYPE_LINE = "GridElement_Line"; + final static String TYPE_LINE_NORTH = "GridElement_LineNorth"; + final static String TYPE_LINE_SOUTH = "GridElement_LineSouth"; + final static String TYPE_LINE_WEST = "GridElement_LineWest"; + final static String TYPE_LINE_EAST = "GridElement_LineEast"; + final static String TYPE_LINE_NORTHING = "GridElement_LineNorthing"; + final static String TYPE_LINE_EASTING = "GridElement_LineEasting"; + final static String TYPE_GRIDZONE_LABEL = "GridElement_GridZoneLabel"; + final static String TYPE_LONGITUDE_LABEL = "GridElement_LongitudeLabel"; + final static String TYPE_LATITUDE_LABEL = "GridElement_LatitudeLabel"; + + public final Sector sector; + public final Renderable renderable; + public final String type; + public final double value; + + GridElement(Sector sector, Renderable renderable, String type, double value) { + this.sector = sector; + this.renderable = renderable; + this.type = type; + this.value = value; + } + + GridElement(Sector sector, Renderable renderable, String type) { + this(sector, renderable, type, 0); + } + + boolean isInView(RenderContext rc) { + return this.sector.intersectsOrNextTo(rc.terrain.getSector()); + } + +} diff --git a/worldwind/src/main/java/gov/nasa/worldwind/layer/graticule/GridTilesSupport.java b/worldwind/src/main/java/gov/nasa/worldwind/layer/graticule/GridTilesSupport.java new file mode 100644 index 000000000..926850430 --- /dev/null +++ b/worldwind/src/main/java/gov/nasa/worldwind/layer/graticule/GridTilesSupport.java @@ -0,0 +1,86 @@ +package gov.nasa.worldwind.layer.graticule; + +import android.graphics.Rect; + +import java.util.ArrayList; +import java.util.List; + +import gov.nasa.worldwind.geom.Sector; +import gov.nasa.worldwind.render.RenderContext; + +class GridTilesSupport { + + interface Callback { + AbstractGraticuleTile[][] initGridTiles(int rows, int cols); + AbstractGraticuleTile createGridTile(Sector sector); + Sector getGridSector(int row, int col); + int getGridColumn(double longitude); + int getGridRow(double latitude); + } + + private final Callback callback; + private final int rows; + private final int cols; + private final AbstractGraticuleTile[][] gridTiles; + + GridTilesSupport(Callback callback, int rows, int cols) { + this.callback = callback; + this.rows = rows; + this.cols = cols; + this.gridTiles = callback.initGridTiles(rows, cols); + } + + void clearTiles() { + for (int row = 0; row < this.rows; row++) { + for (int col = 0; col < this.cols; col++) { + if (this.gridTiles[row][col] != null) { + this.gridTiles[row][col].clearRenderables(); + this.gridTiles[row][col] = null; + } + } + } + } + + /** + * Select the visible grid elements + * + * @param rc the current RenderContext. + */ + void selectRenderables(RenderContext rc) { + List tileList = getVisibleTiles(rc); + if (tileList.size() > 0) { + for (AbstractGraticuleTile gt : tileList) { + // Select tile visible elements + gt.selectRenderables(rc); + } + } + } + + private List getVisibleTiles(RenderContext rc) { + List tileList = new ArrayList<>(); + Sector vs = rc.terrain.getSector(); + if (vs != null) { + Rect gridRectangle = getGridRectangleForSector(vs); + for (int row = gridRectangle.top; row <= gridRectangle.bottom; row++) { + for (int col = gridRectangle.left; col <= gridRectangle.right; col++) { + if (gridTiles[row][col] == null) + gridTiles[row][col] = callback.createGridTile(callback.getGridSector(row, col)); + if (gridTiles[row][col].isInView(rc)) + tileList.add(gridTiles[row][col]); + else + gridTiles[row][col].clearRenderables(); + } + } + } + return tileList; + } + + private Rect getGridRectangleForSector(Sector sector) { + int x1 = callback.getGridColumn(sector.minLongitude()); + int x2 = callback.getGridColumn(sector.maxLongitude()); + int y1 = callback.getGridRow(sector.minLatitude()); + int y2 = callback.getGridRow(sector.maxLatitude()); + return new Rect(x1, y1, x2, y2); + } + +} diff --git a/worldwind/src/main/java/gov/nasa/worldwind/layer/graticule/LatLonGraticuleLayer.java b/worldwind/src/main/java/gov/nasa/worldwind/layer/graticule/LatLonGraticuleLayer.java new file mode 100644 index 000000000..4496cb1bf --- /dev/null +++ b/worldwind/src/main/java/gov/nasa/worldwind/layer/graticule/LatLonGraticuleLayer.java @@ -0,0 +1,114 @@ +/* + * 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.layer.graticule; + +import android.content.res.Resources; +import android.graphics.Typeface; + +import java.util.Arrays; +import java.util.List; + +import gov.nasa.worldwind.geom.Sector; +import gov.nasa.worldwind.render.Color; + +/** + * Displays the geographic latitude/longitude graticule. + * + * @author Patrick Murris + * @version $Id: LatLonGraticuleLayer.java 2153 2014-07-17 17:33:13Z tgaskins $ + */ +public class LatLonGraticuleLayer extends AbstractLatLonGraticuleLayer { + + private static final String GRATICULE_LATLON_LEVEL_0 = "Graticule.LatLonLevel0"; + private static final String GRATICULE_LATLON_LEVEL_1 = "Graticule.LatLonLevel1"; + private static final String GRATICULE_LATLON_LEVEL_2 = "Graticule.LatLonLevel2"; + private static final String GRATICULE_LATLON_LEVEL_3 = "Graticule.LatLonLevel3"; + private static final String GRATICULE_LATLON_LEVEL_4 = "Graticule.LatLonLevel4"; + private static final String GRATICULE_LATLON_LEVEL_5 = "Graticule.LatLonLevel5"; + + public LatLonGraticuleLayer() { + super("LatLon Graticule"); + } + + @Override + protected void initRenderingParams() { + GraticuleRenderingParams params; + // Ten degrees grid + params = new GraticuleRenderingParams(); + params.put(GraticuleRenderingParams.KEY_LINE_COLOR, new Color(android.graphics.Color.WHITE)); + params.put(GraticuleRenderingParams.KEY_LABEL_COLOR,new Color(android.graphics.Color.WHITE)); + params.put(GraticuleRenderingParams.KEY_LABEL_TYPEFACE, Typeface.create("arial", Typeface.BOLD)); + params.put(GraticuleRenderingParams.KEY_LABEL_SIZE, 16f * Resources.getSystem().getDisplayMetrics().scaledDensity); + setRenderingParams(GRATICULE_LATLON_LEVEL_0, params); + // One degree + params = new GraticuleRenderingParams(); + params.put(GraticuleRenderingParams.KEY_LINE_COLOR, new Color(android.graphics.Color.GREEN)); + params.put(GraticuleRenderingParams.KEY_LABEL_COLOR, new Color(android.graphics.Color.GREEN)); + params.put(GraticuleRenderingParams.KEY_LABEL_TYPEFACE, Typeface.create("arial", Typeface.BOLD)); + params.put(GraticuleRenderingParams.KEY_LABEL_SIZE, 14f * Resources.getSystem().getDisplayMetrics().scaledDensity); + setRenderingParams(GRATICULE_LATLON_LEVEL_1, params); + // 1/10th degree - 1/6th (10 minutes) + params = new GraticuleRenderingParams(); + params.put(GraticuleRenderingParams.KEY_LINE_COLOR, new Color(android.graphics.Color.rgb(0, 102, 255))); + params.put(GraticuleRenderingParams.KEY_LABEL_COLOR, new Color(android.graphics.Color.rgb(0, 102, 255))); + setRenderingParams(GRATICULE_LATLON_LEVEL_2, params); + // 1/100th degree - 1/60th (one minutes) + params = new GraticuleRenderingParams(); + params.put(GraticuleRenderingParams.KEY_LINE_COLOR, new Color(android.graphics.Color.CYAN)); + params.put(GraticuleRenderingParams.KEY_LABEL_COLOR, new Color(android.graphics.Color.CYAN)); + setRenderingParams(GRATICULE_LATLON_LEVEL_3, params); + // 1/1000 degree - 1/360th (10 seconds) + params = new GraticuleRenderingParams(); + params.put(GraticuleRenderingParams.KEY_LINE_COLOR, new Color(android.graphics.Color.rgb(0, 153, 153))); + params.put(GraticuleRenderingParams.KEY_LABEL_COLOR, new Color(android.graphics.Color.rgb(0, 153, 153))); + setRenderingParams(GRATICULE_LATLON_LEVEL_4, params); + // 1/10000 degree - 1/3600th (one second) + params = new GraticuleRenderingParams(); + params.put(GraticuleRenderingParams.KEY_LINE_COLOR, new Color(android.graphics.Color.rgb(102, 255, 204))); + params.put(GraticuleRenderingParams.KEY_LABEL_COLOR, new Color(android.graphics.Color.rgb(102, 255, 204))); + setRenderingParams(GRATICULE_LATLON_LEVEL_5, params); + } + + @Override + protected List getOrderedTypes() { + return Arrays.asList( + GRATICULE_LATLON_LEVEL_0, + GRATICULE_LATLON_LEVEL_1, + GRATICULE_LATLON_LEVEL_2, + GRATICULE_LATLON_LEVEL_3, + GRATICULE_LATLON_LEVEL_4, + GRATICULE_LATLON_LEVEL_5); + } + + @Override + protected String getTypeFor(double resolution) { + if (resolution >= 10) + return GRATICULE_LATLON_LEVEL_0; + else if (resolution >= 1) + return GRATICULE_LATLON_LEVEL_1; + else if (resolution >= .1) + return GRATICULE_LATLON_LEVEL_2; + else if (resolution >= .01) + return GRATICULE_LATLON_LEVEL_3; + else if (resolution >= .001) + return GRATICULE_LATLON_LEVEL_4; + else if (resolution >= .0001) + return GRATICULE_LATLON_LEVEL_5; + + return null; + } + + @Override + public AbstractGraticuleTile[][] initGridTiles(int rows, int cols) { + return new LatLonGraticuleTile[rows][cols]; + } + + @Override + public AbstractGraticuleTile createGridTile(Sector sector) { + return new LatLonGraticuleTile(this, sector, 10, 0); + } + +} diff --git a/worldwind/src/main/java/gov/nasa/worldwind/layer/graticule/LatLonGraticuleTile.java b/worldwind/src/main/java/gov/nasa/worldwind/layer/graticule/LatLonGraticuleTile.java new file mode 100644 index 000000000..c072a1afe --- /dev/null +++ b/worldwind/src/main/java/gov/nasa/worldwind/layer/graticule/LatLonGraticuleTile.java @@ -0,0 +1,173 @@ +package gov.nasa.worldwind.layer.graticule; + +import java.util.ArrayList; +import java.util.List; + +import gov.nasa.worldwind.WorldWind; +import gov.nasa.worldwind.geom.Location; +import gov.nasa.worldwind.geom.Position; +import gov.nasa.worldwind.geom.Sector; +import gov.nasa.worldwind.render.RenderContext; +import gov.nasa.worldwind.render.Renderable; + +class LatLonGraticuleTile extends AbstractGraticuleTile { + + private static final int MIN_CELL_SIZE_PIXELS = 40; // TODO: make settable + + private final int divisions; + private final int level; + + private List subTiles; + + LatLonGraticuleTile(LatLonGraticuleLayer layer, Sector sector, int divisions, int level) { + super(layer, sector); + this.divisions = divisions; + this.level = level; + } + + @Override + LatLonGraticuleLayer getLayer() { + return (LatLonGraticuleLayer) super.getLayer(); + } + + @Override + boolean isInView(RenderContext rc) { + return super.isInView(rc) && (this.level == 0 || getSizeInPixels(rc) / this.divisions >= MIN_CELL_SIZE_PIXELS); + } + + @Override + void selectRenderables(RenderContext rc) { + super.selectRenderables(rc); + + Location labelOffset = getLayer().computeLabelOffset(rc); + String graticuleType = getLayer().getTypeFor(this.getSector().deltaLatitude()); + if (this.level == 0) { + for (GridElement ge : this.getGridElements()) { + if (ge.isInView(rc)) { + // Add level zero bounding lines and labels + if (ge.type.equals(GridElement.TYPE_LINE_SOUTH) || ge.type.equals(GridElement.TYPE_LINE_NORTH) + || ge.type.equals(GridElement.TYPE_LINE_WEST)) { + getLayer().addRenderable(ge.renderable, graticuleType); + String labelType = ge.type.equals(GridElement.TYPE_LINE_SOUTH) + || ge.type.equals(GridElement.TYPE_LINE_NORTH) ? + GridElement.TYPE_LATITUDE_LABEL : GridElement.TYPE_LONGITUDE_LABEL; + getLayer().addLabel(ge.value, labelType, graticuleType, this.getSector().deltaLatitude(), labelOffset); + } + } + } + if (getSizeInPixels(rc) / this.divisions < MIN_CELL_SIZE_PIXELS) + return; + } + + // Select tile grid elements + double resolution = this.getSector().deltaLatitude() / this.divisions; + graticuleType = getLayer().getTypeFor(resolution); + for (GridElement ge : this.getGridElements()) { + if (ge.isInView(rc)) { + if (ge.type.equals(GridElement.TYPE_LINE)) { + getLayer().addRenderable(ge.renderable, graticuleType); + String labelType = ge.sector.deltaLatitude() < 1E-14 ? + GridElement.TYPE_LATITUDE_LABEL : GridElement.TYPE_LONGITUDE_LABEL; + getLayer().addLabel(ge.value, labelType, graticuleType, resolution, labelOffset); + } + } + } + + if (getSizeInPixels(rc) / this.divisions < MIN_CELL_SIZE_PIXELS * 2) + return; + + // Select child elements + if (this.subTiles == null) + createSubTiles(); + for (LatLonGraticuleTile gt : this.subTiles) { + if (gt.isInView(rc)) { + gt.selectRenderables(rc); + } else + gt.clearRenderables(); + } + } + + @Override + void clearRenderables() { + super.clearRenderables(); + if (this.subTiles != null) { + for (LatLonGraticuleTile gt : this.subTiles) { + gt.clearRenderables(); + } + this.subTiles.clear(); + this.subTiles = null; + } + } + + private void createSubTiles() { + this.subTiles = new ArrayList<>(); + Sector[] sectors = this.subdivide(this.divisions); + int subDivisions = 10; + if ((getLayer().getAngleFormat().equals(LatLonGraticuleLayer.AngleFormat.DMS) + || getLayer().getAngleFormat().equals(LatLonGraticuleLayer.AngleFormat.DM)) + && (this.level == 0 || this.level == 2)) + subDivisions = 6; + for (Sector s : sectors) { + this.subTiles.add(new LatLonGraticuleTile(getLayer(), s, subDivisions, this.level + 1)); + } + } + + @Override + void createRenderables() { + super.createRenderables(); + + double step = this.getSector().deltaLatitude() / this.divisions; + + // Generate meridians with labels + double lon = this.getSector().minLongitude() + (this.level == 0 ? 0 : step); + while (lon < this.getSector().maxLongitude() - step / 2) { + double longitude = lon; + // Meridian + List positions = new ArrayList<>(2); + positions.add(new Position(this.getSector().minLatitude(), longitude, 0)); + positions.add(new Position(this.getSector().maxLatitude(), longitude, 0)); + + Renderable line = getLayer().createLineRenderable(positions, WorldWind.LINEAR); + Sector sector = Sector.fromDegrees( + this.getSector().minLatitude(), lon, this.getSector().deltaLatitude(), 1E-15); + String lineType = lon == this.getSector().minLongitude() ? + GridElement.TYPE_LINE_WEST : GridElement.TYPE_LINE; + this.getGridElements().add(new GridElement(sector, line, lineType, lon)); + + // Increase longitude + lon += step; + } + + // Generate parallels + double lat = this.getSector().minLatitude() + (this.level == 0 ? 0 : step); + while (lat < this.getSector().maxLatitude() - step / 2) { + double latitude = lat; + List positions = new ArrayList<>(2); + positions.add(new Position(latitude, this.getSector().minLongitude(), 0)); + positions.add(new Position(latitude, this.getSector().maxLongitude(), 0)); + + Renderable line = getLayer().createLineRenderable(positions, WorldWind.LINEAR); + Sector sector = Sector.fromDegrees( + lat, this.getSector().minLongitude(), 1E-15, this.getSector().deltaLongitude()); + String lineType = lat == this.getSector().minLatitude() ? + GridElement.TYPE_LINE_SOUTH : GridElement.TYPE_LINE; + this.getGridElements().add(new GridElement(sector, line, lineType, lat)); + + // Increase latitude + lat += step; + } + + // Draw and label a parallel at the top of the graticule. The line is apparent only on 2D globes. + if (this.getSector().maxLatitude() == 90) { + List positions = new ArrayList<>(2); + positions.add(new Position(90, this.getSector().minLongitude(), 0)); + positions.add(new Position(90, this.getSector().maxLongitude(), 0)); + + Renderable line = getLayer().createLineRenderable(positions, WorldWind.LINEAR); + Sector sector = Sector.fromDegrees( + 90, this.getSector().minLongitude(), 1E-15, this.getSector().deltaLongitude()); + this.getGridElements().add(new GridElement(sector, line, GridElement.TYPE_LINE_NORTH, 90)); + } + } + +} diff --git a/worldwind/src/main/java/gov/nasa/worldwind/layer/graticule/MGRSGraticuleLayer.java b/worldwind/src/main/java/gov/nasa/worldwind/layer/graticule/MGRSGraticuleLayer.java new file mode 100644 index 000000000..4b75b8864 --- /dev/null +++ b/worldwind/src/main/java/gov/nasa/worldwind/layer/graticule/MGRSGraticuleLayer.java @@ -0,0 +1,277 @@ +/* + * 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.layer.graticule; + +import android.content.res.Resources; +import android.graphics.Rect; +import android.graphics.Typeface; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import gov.nasa.worldwind.geom.Sector; +import gov.nasa.worldwind.render.Color; +import gov.nasa.worldwind.render.RenderContext; + +/** + * @author Patrick Murris + * @version $Id: MGRSGraticuleLayer.java 2153 2014-07-17 17:33:13Z tgaskins $ + */ + +public class MGRSGraticuleLayer extends AbstractUTMGraticuleLayer { + + static final int MGRS_OVERVIEW_RESOLUTION = 1000000; + static final int MGRS_GRID_ZONE_RESOLUTION = 500000; + + /** Graticule for the MGRS overview. */ + private static final String GRATICULE_MGRS_OVERVIEW = "Graticule.MGRS.Overview"; + /** Graticule for the MGRS grid zone. */ + private static final String GRATICULE_MGRS_GRID_ZONE = "Graticule.MGRS.GridZone"; + + private static final double GRID_ZONE_MAX_ALTITUDE = 5000e3; + + private final MGRSGridZone[][] gridZones = new MGRSGridZone[20][60]; // row/col + private final MGRSGridZone[] poleZones = new MGRSGridZone[4]; // North x2 + South x2 + private final MGRSOverview overview = new MGRSOverview(this); + + /** Creates a new MGRSGraticuleLayer, with default graticule attributes. */ + public MGRSGraticuleLayer() { + super("MGRS graticule", (int) 100e3, 1e5); + } + + /** + * Returns the maxiumum resolution graticule that will be rendered, or null if no graticules will be rendered. By + * default, all graticules are rendered, and this will return GRATICULE_1M. + * + * @return maximum resolution rendered. + */ + public String getMaximumGraticuleResolution() { + String maxTypeDrawn = null; + for (String type : getOrderedTypes()) { + GraticuleRenderingParams params = getRenderingParams(type); + if (params.isDrawLines()) { + maxTypeDrawn = type; + } + } + return maxTypeDrawn; + } + + /** + * Sets the maxiumum resolution graticule that will be rendered. + * + * @param graticuleType one of GRATICULE_MGRS_OVERVIEW, GRATICULE_MGRS_GRID_ZONE, GRATICULE_100000M, GRATICULE_10000M, + * GRATICULE_1000M, GRATICULE_100M, GRATICULE_10M, or GRATICULE_1M. + */ + public void setMaximumGraticuleResolution(String graticuleType) { + boolean pastTarget = false; + for (String type : getOrderedTypes()) { + // Enable all graticulte BEFORE and INCLUDING the target. + // Disable all graticules AFTER the target. + GraticuleRenderingParams params = getRenderingParams(type); + params.setDrawLines(!pastTarget); + params.setDrawLabels(!pastTarget); + if (!pastTarget && type.equals(graticuleType)) { + pastTarget = true; + } + } + } + + @Override + protected void initRenderingParams() { + super.initRenderingParams(); + + GraticuleRenderingParams params; + // MGRS Overview graticule + params = new GraticuleRenderingParams(); + params.put(GraticuleRenderingParams.KEY_LINE_COLOR, new Color(.8f, .8f, .8f, .5f)); + params.put(GraticuleRenderingParams.KEY_LABEL_COLOR, new Color(1f, 1f, 1f, .8f)); + params.put(GraticuleRenderingParams.KEY_LABEL_TYPEFACE, Typeface.create("arial", Typeface.BOLD)); + params.put(GraticuleRenderingParams.KEY_LABEL_SIZE, 14f * Resources.getSystem().getDisplayMetrics().scaledDensity); + params.put(GraticuleRenderingParams.KEY_DRAW_LABELS, Boolean.TRUE); + setRenderingParams(GRATICULE_MGRS_OVERVIEW, params); + // MGRS GridZone graticule + params = new GraticuleRenderingParams(); + params.put(GraticuleRenderingParams.KEY_LINE_COLOR, new Color(android.graphics.Color.YELLOW)); + params.put(GraticuleRenderingParams.KEY_LABEL_COLOR, new Color(android.graphics.Color.YELLOW)); + params.put(GraticuleRenderingParams.KEY_LABEL_TYPEFACE, Typeface.create("arial", Typeface.BOLD)); + params.put(GraticuleRenderingParams.KEY_LABEL_SIZE, 16f * Resources.getSystem().getDisplayMetrics().scaledDensity); + setRenderingParams(GRATICULE_MGRS_GRID_ZONE, params); + } + + @Override + protected List getOrderedTypes() { + List orderedTypes = Arrays.asList(GRATICULE_MGRS_OVERVIEW, GRATICULE_MGRS_GRID_ZONE); + orderedTypes.addAll(super.getOrderedTypes()); + return orderedTypes; + } + + @Override + protected String getTypeFor(double resolution) { + switch ((int) resolution) { + case MGRS_OVERVIEW_RESOLUTION: return GRATICULE_MGRS_OVERVIEW; + case MGRS_GRID_ZONE_RESOLUTION: return GRATICULE_MGRS_GRID_ZONE; + default: return super.getTypeFor(resolution); + } + } + + @Override + protected void selectRenderables(RenderContext rc) { + if (rc.camera.altitude <= GRID_ZONE_MAX_ALTITUDE) { + this.selectMGRSRenderables(rc); + super.selectRenderables(rc); + } else { + this.overview.selectRenderables(rc); + } + } + + private void selectMGRSRenderables(RenderContext rc) { + List zoneList = getVisibleZones(rc); + if (zoneList.size() > 0) { + for (MGRSGridZone gz : zoneList) { + // Select visible grid zones elements + gz.selectRenderables(rc); + } + } + } + + private List getVisibleZones(RenderContext rc) { + List zoneList = new ArrayList<>(); + Sector vs = rc.terrain.getSector(); + if (vs != null) { + // UTM Grid + Rect gridRectangle = getGridRectangleForSector(vs); + if (gridRectangle != null) { + for (int row = gridRectangle.top; row <= gridRectangle.bottom; row++) { + for (int col = gridRectangle.left; col <= gridRectangle.right; col++) { + if (row != 19 || (col != 31 && col != 33 && col != 35)) { // ignore X32, 34 and 36 + if (gridZones[row][col] == null) + gridZones[row][col] = new MGRSGridZone(this, getGridSector(row, col)); + if (gridZones[row][col].isInView(rc)) + zoneList.add(gridZones[row][col]); + else + gridZones[row][col].clearRenderables(); + } + } + } + } + // Poles + if (vs.maxLatitude() > 84) { + // North pole + if (poleZones[2] == null) + poleZones[2] = new MGRSGridZone(this, Sector.fromDegrees(84, -180, 6,180)); // Y + if (poleZones[3] == null) + poleZones[3] = new MGRSGridZone(this, Sector.fromDegrees(84, 0, 6,180)); // Z + zoneList.add(poleZones[2]); + zoneList.add(poleZones[3]); + } + if (vs.minLatitude() < -80) { + // South pole + if (poleZones[0] == null) + poleZones[0] = new MGRSGridZone(this, Sector.fromDegrees(-90, -180, 10,180)); // B + if (poleZones[1] == null) + poleZones[1] = new MGRSGridZone(this, Sector.fromDegrees(-90, 0, 10,180)); // A + zoneList.add(poleZones[0]); + zoneList.add(poleZones[1]); + } + } + return zoneList; + } + + private Rect getGridRectangleForSector(Sector sector) { + Rect rectangle = null; + if (sector.minLatitude() < 84 && sector.maxLatitude() > -80) { + double minLat = Math.max(sector.minLatitude(), -80); + double maxLat = Math.min(sector.maxLatitude(), 84); + Sector gridSector = Sector.fromDegrees(minLat, sector.minLongitude(), + maxLat - minLat, sector.deltaLongitude()); + int x1 = getGridColumn(gridSector.minLongitude()); + int x2 = getGridColumn(gridSector.maxLongitude()); + int y1 = getGridRow(gridSector.minLatitude()); + int y2 = getGridRow(gridSector.maxLatitude()); + // Adjust rectangle to include special zones + if (y1 <= 17 && y2 >= 17 && x2 == 30) // 32V Norway + x2 = 31; + if (y1 <= 19 && y2 >= 19) { // X band + if (x1 == 31) // 31X + x1 = 30; + if (x2 == 31) // 33X + x2 = 32; + if (x1 == 33) // 33X + x1 = 32; + if (x2 == 33) // 35X + x2 = 34; + if (x1 == 35) // 35X + x1 = 34; + if (x2 == 35) // 37X + x2 = 36; + } + rectangle = new Rect(x1, y1, x2, y2); + } + return rectangle; + } + + private int getGridColumn(double longitude) { + return Math.min((int) Math.floor((longitude + 180) / 6d), 59); + } + + private int getGridRow(double latitude) { + return Math.min((int) Math.floor((latitude + 80) / 8d), 19); + } + + private Sector getGridSector(int row, int col) { + int minLat = -80 + row * 8; + int maxLat = minLat + (minLat != 72 ? 8 : 12); + int minLon = -180 + col * 6; + int maxLon = minLon + 6; + // Special sectors + if (row == 17 && col == 30) // 31V + maxLon -= 3; + else if (row == 17 && col == 31) // 32V + minLon -= 3; + else if (row == 19 && col == 30) // 31X + maxLon += 3; + else if (row == 19 && col == 31) { // 32X does not exist + minLon += 3; + maxLon -= 3; + } else if (row == 19 && col == 32) { // 33X + minLon -= 3; + maxLon += 3; + } else if (row == 19 && col == 33) { // 34X does not exist + minLon += 3; + maxLon -= 3; + } else if (row == 19 && col == 34) { // 35X + minLon -= 3; + maxLon += 3; + } else if (row == 19 && col == 35) { // 36X does not exist + minLon += 3; + maxLon -= 3; + } else if (row == 19 && col == 36) // 37X + minLon -= 3; + return Sector.fromDegrees(minLat, minLon, maxLat - minLat, maxLon - minLon); + } + + boolean isNorthNeighborInView(MGRSGridZone gz, RenderContext rc) { + if (gz.isUPS()) + return true; + + int row = getGridRow(gz.getSector().centroidLatitude()); + int col = getGridColumn(gz.getSector().centroidLongitude()); + MGRSGridZone neighbor = row + 1 <= 19 ? this.gridZones[row + 1][col] : null; + return neighbor != null && neighbor.isInView(rc); + } + + boolean isEastNeighborInView(MGRSGridZone gz, RenderContext rc) { + if (gz.isUPS()) + return true; + + int row = getGridRow(gz.getSector().centroidLatitude()); + int col = getGridColumn(gz.getSector().centroidLongitude()); + MGRSGridZone neighbor = col + 1 <= 59 ? this.gridZones[row][col + 1] : null; + return neighbor != null && neighbor.isInView(rc); + } + +} diff --git a/worldwind/src/main/java/gov/nasa/worldwind/layer/graticule/MGRSGridZone.java b/worldwind/src/main/java/gov/nasa/worldwind/layer/graticule/MGRSGridZone.java new file mode 100644 index 000000000..eea4191bb --- /dev/null +++ b/worldwind/src/main/java/gov/nasa/worldwind/layer/graticule/MGRSGridZone.java @@ -0,0 +1,229 @@ +package gov.nasa.worldwind.layer.graticule; + +import java.util.ArrayList; +import java.util.List; + +import gov.nasa.worldwind.WorldWind; +import gov.nasa.worldwind.geom.Position; +import gov.nasa.worldwind.geom.Sector; +import gov.nasa.worldwind.geom.coords.Hemisphere; +import gov.nasa.worldwind.geom.coords.MGRSCoord; +import gov.nasa.worldwind.geom.coords.UTMCoord; +import gov.nasa.worldwind.render.RenderContext; +import gov.nasa.worldwind.render.Renderable; + +/** Represent a UTM zone / latitude band intersection */ +class MGRSGridZone extends AbstractGraticuleTile { + + private static final double ONEHT = 100e3; + private static final double TWOMIL = 2e6; + private static final double SQUARE_MAX_ALTITUDE = 3000e3; + + private final boolean ups; + private final String name; + private final Hemisphere hemisphere; + private final int zone; + + private List squares; + + MGRSGridZone(MGRSGraticuleLayer layer, Sector sector) { + super(layer, sector); + this.ups = (sector.maxLatitude() > MGRSGraticuleLayer.UTM_MAX_LATITUDE + || sector.minLatitude() < MGRSGraticuleLayer.UTM_MIN_LATITUDE); + MGRSCoord MGRS = MGRSCoord.fromLatLon(sector.centroidLatitude(), sector.centroidLongitude()); + if (this.ups) { + this.name = MGRS.toString().substring(2, 3); + this.hemisphere = sector.minLatitude() > 0 ? Hemisphere.N : Hemisphere.S; + this.zone = 0; + } else { + this.name = MGRS.toString().substring(0, 3); + UTMCoord UTM = UTMCoord.fromLatLon(sector.centroidLatitude(), sector.centroidLongitude()); + this.hemisphere = UTM.getHemisphere(); + this.zone = UTM.getZone(); + } + } + + @Override + MGRSGraticuleLayer getLayer() { + return (MGRSGraticuleLayer) super.getLayer(); + } + + @Override + void selectRenderables(RenderContext rc) { + super.selectRenderables(rc); + + String graticuleType = getLayer().getTypeFor(MGRSGraticuleLayer.MGRS_GRID_ZONE_RESOLUTION); + for (GridElement ge : this.getGridElements()) + if (ge.isInView(rc)) { + if (ge.type.equals(GridElement.TYPE_LINE_NORTH) && this.getLayer().isNorthNeighborInView(this, rc)) + continue; + if (ge.type.equals(GridElement.TYPE_LINE_EAST) && this.getLayer().isEastNeighborInView(this, rc)) + continue; + + getLayer().addRenderable(ge.renderable, graticuleType); + } + + if (rc.camera.altitude > SQUARE_MAX_ALTITUDE) + return; + + // Select 100km squares elements + if (this.squares == null) + if (this.ups) + createSquaresUPS(); + else + createSquaresUTM(); + + for (UTMSquareZone sz : this.squares) + if (sz.isInView(rc)) + sz.selectRenderables(rc); + else + sz.clearRenderables(); + } + + @Override + void clearRenderables() { + super.clearRenderables(); + if (this.squares != null) { + for (UTMSquareZone sz : this.squares) + sz.clearRenderables(); + this.squares.clear(); + this.squares = null; + } + } + + @Override + void createRenderables() { + super.createRenderables(); + + List positions = new ArrayList<>(); + + // left meridian segment + positions.clear(); + positions.add(Position.fromDegrees(this.getSector().minLatitude(), this.getSector().minLongitude(), 10e3)); + positions.add(Position.fromDegrees(this.getSector().maxLatitude(), this.getSector().minLongitude(), 10e3)); + Renderable polyline = getLayer().createLineRenderable(new ArrayList<>(positions), WorldWind.LINEAR); + Sector lineSector = Sector.fromDegrees(this.getSector().minLatitude(), this.getSector().minLongitude(), + this.getSector().deltaLatitude(), 1E-15); + this.getGridElements().add(new GridElement(lineSector, polyline, GridElement.TYPE_LINE_WEST)); + + if (!this.ups) { + // right meridian segment + positions.clear(); + positions.add(Position.fromDegrees(this.getSector().minLatitude(), this.getSector().maxLongitude(), 10e3)); + positions.add(Position.fromDegrees(this.getSector().maxLatitude(), this.getSector().maxLongitude(), 10e3)); + polyline = getLayer().createLineRenderable(new ArrayList<>(positions), WorldWind.LINEAR); + lineSector = Sector.fromDegrees(this.getSector().minLatitude(), this.getSector().maxLongitude(), + this.getSector().deltaLatitude(), 1E-15); + this.getGridElements().add(new GridElement(lineSector, polyline, GridElement.TYPE_LINE_EAST)); + + // bottom parallel segment + positions.clear(); + positions.add(Position.fromDegrees(this.getSector().minLatitude(), this.getSector().minLongitude(), 10e3)); + positions.add(Position.fromDegrees(this.getSector().minLatitude(), this.getSector().maxLongitude(), 10e3)); + polyline = getLayer().createLineRenderable(new ArrayList<>(positions), WorldWind.LINEAR); + lineSector = Sector.fromDegrees(this.getSector().minLatitude(), this.getSector().minLongitude(), + 1E-15, this.getSector().deltaLongitude()); + this.getGridElements().add(new GridElement(lineSector, polyline, GridElement.TYPE_LINE_SOUTH)); + + // top parallel segment + positions.clear(); + positions.add(Position.fromDegrees(this.getSector().maxLatitude(), this.getSector().minLongitude(), 10e3)); + positions.add(Position.fromDegrees(this.getSector().maxLatitude(), this.getSector().maxLongitude(), 10e3)); + polyline = getLayer().createLineRenderable(new ArrayList<>(positions), WorldWind.LINEAR); + lineSector = Sector.fromDegrees(this.getSector().maxLatitude(), this.getSector().minLongitude(), + 1E-15, this.getSector().deltaLongitude()); + this.getGridElements().add(new GridElement(lineSector, polyline, GridElement.TYPE_LINE_NORTH)); + } + + // Label + Renderable text = this.getLayer().createTextRenderable(Position.fromDegrees(this.getSector().centroidLatitude(), this.getSector().centroidLongitude(), 0), this.name, 10e6); + this.getGridElements().add(new GridElement(this.getSector(), text, GridElement.TYPE_GRIDZONE_LABEL)); + } + + + boolean isUPS() { + return this.ups; + } + + private void createSquaresUTM() { + // Find grid zone easting and northing boundaries + UTMCoord UTM; + UTM = UTMCoord.fromLatLon(this.getSector().minLatitude(), this.getSector().centroidLongitude()); + double minNorthing = UTM.getNorthing(); + UTM = UTMCoord.fromLatLon(this.getSector().maxLatitude(), this.getSector().centroidLongitude()); + double maxNorthing = UTM.getNorthing(); + maxNorthing = maxNorthing == 0 ? 10e6 : maxNorthing; + UTM = UTMCoord.fromLatLon(this.getSector().minLatitude(), this.getSector().minLongitude()); + double minEasting = UTM.getEasting(); + UTM = UTMCoord.fromLatLon(this.getSector().maxLatitude(), this.getSector().minLongitude()); + minEasting = UTM.getEasting() < minEasting ? UTM.getEasting() : minEasting; + double maxEasting = 1e6 - minEasting; + + // Compensate for some distorted zones + if (this.name.equals("32V")) // catch KS and LS in 32V + maxNorthing += 20e3; + if (this.name.equals("31X")) // catch GA and GV in 31X + maxEasting += ONEHT; + + // Create squares + this.squares = getLayer().createSquaresGrid(this.zone, this.hemisphere, this.getSector(), minEasting, maxEasting, + minNorthing, maxNorthing); + this.setSquareNames(); + } + + private void createSquaresUPS() { + this.squares = new ArrayList<>(); + double minEasting, maxEasting, minNorthing, maxNorthing; + + if (Hemisphere.N.equals(this.hemisphere)) { + minNorthing = TWOMIL - ONEHT * 7; + maxNorthing = TWOMIL + ONEHT * 7; + minEasting = this.name.equals("Y") ? TWOMIL - ONEHT * 7 : TWOMIL; + maxEasting = this.name.equals("Y") ? TWOMIL : TWOMIL + ONEHT * 7; + } else { + minNorthing = TWOMIL - ONEHT * 12; + maxNorthing = TWOMIL + ONEHT * 12; + minEasting = this.name.equals("A") ? TWOMIL - ONEHT * 12 : TWOMIL; + maxEasting = this.name.equals("A") ? TWOMIL : TWOMIL + ONEHT * 12; + } + + // Create squares + this.squares = getLayer().createSquaresGrid(this.zone, this.hemisphere, this.getSector(), minEasting, maxEasting, + minNorthing, maxNorthing); + this.setSquareNames(); + } + + private void setSquareNames() { + for (UTMSquareZone sz : this.squares) { + this.setSquareName(sz); + } + } + + private void setSquareName(UTMSquareZone sz) { + // Find out MGRS 100Km square name + double tenMeterDegree = Math.toDegrees(10d / 6378137d); + MGRSCoord MGRS = null; + if (sz.centroid != null && sz.isPositionInside(Position.fromDegrees(sz.centroid.latitude, sz.centroid.longitude, 0))) + MGRS = MGRSCoord.fromLatLon(sz.centroid.latitude, sz.centroid.longitude); + else if (sz.isPositionInside(sz.sw)) + MGRS = MGRSCoord.fromLatLon( + Position.clampLatitude(sz.sw.latitude + tenMeterDegree), + Position.clampLongitude(sz.sw.longitude + tenMeterDegree)); + else if (sz.isPositionInside(sz.se)) + MGRS = MGRSCoord.fromLatLon( + Position.clampLatitude(sz.se.latitude + tenMeterDegree), + Position.clampLongitude(sz.se.longitude - tenMeterDegree)); + else if (sz.isPositionInside(sz.nw)) + MGRS = MGRSCoord.fromLatLon( + Position.clampLatitude(sz.nw.latitude - tenMeterDegree), + Position.clampLongitude(sz.nw.longitude + tenMeterDegree)); + else if (sz.isPositionInside(sz.ne)) + MGRS = MGRSCoord.fromLatLon( + Position.clampLatitude(sz.ne.latitude - tenMeterDegree), + Position.clampLongitude(sz.ne.longitude - tenMeterDegree)); + // Set square zone name + if (MGRS != null) + sz.setName(MGRS.toString().substring(3, 5)); + } + +} diff --git a/worldwind/src/main/java/gov/nasa/worldwind/layer/graticule/MGRSOverview.java b/worldwind/src/main/java/gov/nasa/worldwind/layer/graticule/MGRSOverview.java new file mode 100644 index 000000000..0a93d3649 --- /dev/null +++ b/worldwind/src/main/java/gov/nasa/worldwind/layer/graticule/MGRSOverview.java @@ -0,0 +1,141 @@ +package gov.nasa.worldwind.layer.graticule; + +import java.util.ArrayList; +import java.util.List; + +import gov.nasa.worldwind.WorldWind; +import gov.nasa.worldwind.geom.Location; +import gov.nasa.worldwind.geom.Position; +import gov.nasa.worldwind.geom.Sector; +import gov.nasa.worldwind.render.RenderContext; +import gov.nasa.worldwind.render.Renderable; +import gov.nasa.worldwind.shape.Label; + +class MGRSOverview extends AbstractGraticuleTile { + + // Exceptions for some meridians. Values: longitude, min latitude, max latitude + private static final int[][] SPECIAL_MERIDIANS = {{3, 56, 64}, {6, 64, 72}, {9, 72, 84}, {21, 72, 84}, {33, 72, 84}}; + // Latitude bands letters - from south to north + private static final String LAT_BANDS = "CDEFGHJKLMNPQRSTUVWX"; + + MGRSOverview(MGRSGraticuleLayer layer) { + super(layer, null); + } + + @Override + void selectRenderables(RenderContext rc) { + super.selectRenderables(rc); + + Location labelPos = getLayer().computeLabelOffset(rc); + for (GridElement ge : this.getGridElements()) { + if (ge.isInView(rc)) { + if (ge.renderable instanceof Label) { + Label gt = (Label) ge.renderable; + if (labelPos.latitude < 72 || !"*32*34*36*".contains("*" + gt.getText() + "*")) { + // Adjust label position according to eye position + Position pos = gt.getPosition(); + if (ge.type.equals(GridElement.TYPE_LATITUDE_LABEL)) + pos = Position.fromDegrees(pos.latitude, + labelPos.longitude, pos.altitude); + else if (ge.type.equals(GridElement.TYPE_LONGITUDE_LABEL)) + pos = Position.fromDegrees(labelPos.latitude, + pos.longitude, pos.altitude); + + gt.setPosition(pos); + } + } + + getLayer().addRenderable(ge.renderable, getLayer().getTypeFor(MGRSGraticuleLayer.MGRS_OVERVIEW_RESOLUTION)); + } + } + } + + @Override + void createRenderables() { + super.createRenderables(); + + List positions = new ArrayList<>(); + + // Generate meridians and zone labels + int lon = -180; + int zoneNumber = 1; + int maxLat; + for (int i = 0; i < 60; i++) { + double longitude = lon; + // Meridian + positions.clear(); + positions.add(Position.fromDegrees(-80, longitude, 10e3)); + positions.add(Position.fromDegrees(-60, longitude, 10e3)); + positions.add(Position.fromDegrees(-30, longitude, 10e3)); + positions.add(Position.fromDegrees(0, longitude, 10e3)); + positions.add(Position.fromDegrees(30, longitude, 10e3)); + if (lon < 6 || lon > 36) { + // 'regular' UTM meridians + maxLat = 84; + positions.add(Position.fromDegrees(60, longitude, 10e3)); + positions.add(Position.fromDegrees(maxLat, longitude, 10e3)); + } else { + // Exceptions: shorter meridians around and north-east of Norway + if (lon == 6) { + maxLat = 56; + positions.add(Position.fromDegrees(maxLat, longitude, 10e3)); + } else { + maxLat = 72; + positions.add(Position.fromDegrees(60, longitude, 10e3)); + positions.add(Position.fromDegrees(maxLat, longitude, 10e3)); + } + } + Renderable polyline = getLayer().createLineRenderable(new ArrayList<>(positions), WorldWind.GREAT_CIRCLE); + Sector sector = Sector.fromDegrees(-80, lon, maxLat + 80, 1E-15); + this.getGridElements().add(new GridElement(sector, polyline, GridElement.TYPE_LINE)); + + // Zone label + Renderable text = getLayer().createTextRenderable(Position.fromDegrees(0, lon + 3, 0), zoneNumber + "", 10e6); + sector = Sector.fromDegrees(-90, lon + 3, 180, 1E-15); + this.getGridElements().add(new GridElement(sector, text, GridElement.TYPE_LONGITUDE_LABEL)); + + // Increase longitude and zone number + lon += 6; + zoneNumber++; + } + + // Generate special meridian segments for exceptions around and north-east of Norway + for (int i = 0; i < 5; i++) { + positions.clear(); + lon = SPECIAL_MERIDIANS[i][0]; + positions.add(Position.fromDegrees(SPECIAL_MERIDIANS[i][1], lon, 10e3)); + positions.add(Position.fromDegrees(SPECIAL_MERIDIANS[i][2], lon, 10e3)); + Renderable polyline = getLayer().createLineRenderable(new ArrayList<>(positions), WorldWind.GREAT_CIRCLE); + Sector sector = Sector.fromDegrees(SPECIAL_MERIDIANS[i][1], lon, SPECIAL_MERIDIANS[i][2] - SPECIAL_MERIDIANS[i][1], 1E-15); + this.getGridElements().add(new GridElement(sector, polyline, GridElement.TYPE_LINE)); + } + + // Generate parallels - no exceptions + int lat = -80; + for (int i = 0; i < 21; i++) { + double latitude = lat; + for (int j = 0; j < 4; j++) { + // Each prallel is divided into four 90 degrees segments + positions.clear(); + lon = -180 + j * 90; + positions.add(Position.fromDegrees(latitude, lon, 10e3)); + positions.add(Position.fromDegrees(latitude, lon + 30, 10e3)); + positions.add(Position.fromDegrees(latitude, lon + 60, 10e3)); + positions.add(Position.fromDegrees(latitude, lon + 90, 10e3)); + Renderable polyline = getLayer().createLineRenderable(new ArrayList<>(positions), WorldWind.LINEAR); + Sector sector = Sector.fromDegrees(lat, lon, 1E-15, 90); + this.getGridElements().add(new GridElement(sector, polyline, GridElement.TYPE_LINE)); + } + // Latitude band label + if (i < 20) { + Renderable text = getLayer().createTextRenderable(Position.fromDegrees(lat + 4, 0, 0), LAT_BANDS.charAt(i) + "", 10e6); + Sector sector = Sector.fromDegrees(lat + 4, -180, 1E-15,360); + this.getGridElements().add(new GridElement(sector, text, GridElement.TYPE_LATITUDE_LABEL)); + } + + // Increase latitude + lat += lat < 72 ? 8 : 12; + } + } + +} diff --git a/worldwind/src/main/java/gov/nasa/worldwind/layer/graticule/UTMGraticuleLayer.java b/worldwind/src/main/java/gov/nasa/worldwind/layer/graticule/UTMGraticuleLayer.java new file mode 100644 index 000000000..f52069a54 --- /dev/null +++ b/worldwind/src/main/java/gov/nasa/worldwind/layer/graticule/UTMGraticuleLayer.java @@ -0,0 +1,111 @@ +/* + * 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.layer.graticule; + +import android.content.res.Resources; +import android.graphics.Typeface; + +import java.util.Arrays; +import java.util.List; + +import gov.nasa.worldwind.geom.Sector; +import gov.nasa.worldwind.render.Color; +import gov.nasa.worldwind.render.RenderContext; + +/** + * Displays the UTM graticule. + * + * @author Patrick Murris + * @version $Id: UTMGraticuleLayer.java 2153 2014-07-17 17:33:13Z tgaskins $ + */ +public class UTMGraticuleLayer extends AbstractUTMGraticuleLayer implements GridTilesSupport.Callback { + + static final int UTM_ZONE_RESOLUTION = 500000; + + /** Graticule for the UTM zone grid. */ + private static final String GRATICULE_UTM_ZONE = "Graticule.UTM.Zone"; + + private static final int GRID_ROWS = 2; + private static final int GRID_COLS = 60; + + private final GridTilesSupport gridTilesSupport; + + public UTMGraticuleLayer() { + super("UTM Graticule", (int) 10e6, 1e6); + this.gridTilesSupport = new GridTilesSupport(this, GRID_ROWS, GRID_COLS); + } + + @Override + protected void initRenderingParams() { + super.initRenderingParams(); + + GraticuleRenderingParams params; + // UTM zone grid + params = new GraticuleRenderingParams(); + params.put(GraticuleRenderingParams.KEY_LINE_COLOR, new Color(android.graphics.Color.WHITE)); + params.put(GraticuleRenderingParams.KEY_LABEL_COLOR, new Color(android.graphics.Color.WHITE)); + params.put(GraticuleRenderingParams.KEY_LABEL_TYPEFACE, Typeface.create("arial", Typeface.BOLD)); + params.put(GraticuleRenderingParams.KEY_LABEL_SIZE, 16f * Resources.getSystem().getDisplayMetrics().scaledDensity); + setRenderingParams(GRATICULE_UTM_ZONE, params); + } + + @Override + protected List getOrderedTypes() { + List orderedTypes = Arrays.asList(GRATICULE_UTM_ZONE); + orderedTypes.addAll(super.getOrderedTypes()); + return orderedTypes; + } + + @Override + protected String getTypeFor(double resolution) { + if (resolution >= UTM_ZONE_RESOLUTION) + return GRATICULE_UTM_ZONE; + else + return super.getTypeFor(resolution); + } + + @Override + protected void selectRenderables(RenderContext rc) { + this.gridTilesSupport.selectRenderables(rc); + super.selectRenderables(rc); + } + + @Override + public AbstractGraticuleTile[][] initGridTiles(int rows, int cols) { + return new UTMGraticuleTile[rows][cols]; + } + + @Override + public AbstractGraticuleTile createGridTile(Sector sector) { + return new UTMGraticuleTile(this, sector, getGridColumn(sector.centroidLongitude()) + 1); + } + + @Override + public Sector getGridSector(int row, int col) { + double deltaLat = UTM_MAX_LATITUDE * 2d / GRID_ROWS; + double deltaLon = 360d / GRID_COLS; + double minLat = row == 0 ? UTM_MIN_LATITUDE : -UTM_MAX_LATITUDE + deltaLat * row; + double maxLat = -UTM_MAX_LATITUDE + deltaLat * (row + 1); + double minLon = -180 + deltaLon * col; + double maxLon = minLon + deltaLon; + return Sector.fromDegrees(minLat, minLon, maxLat - minLat, maxLon - minLon); + } + + @Override + public int getGridColumn(double longitude) { + double deltaLon = 360d / GRID_COLS; + int col = (int) Math.floor((longitude + 180) / deltaLon); + return Math.min(col, GRID_COLS - 1); + } + + @Override + public int getGridRow(double latitude) { + double deltaLat = UTM_MAX_LATITUDE * 2d / GRID_ROWS; + int row = (int) Math.floor((latitude + UTM_MAX_LATITUDE) / deltaLat); + return Math.max(0, Math.min(row, GRID_ROWS - 1)); + } + +} \ No newline at end of file diff --git a/worldwind/src/main/java/gov/nasa/worldwind/layer/graticule/UTMGraticuleTile.java b/worldwind/src/main/java/gov/nasa/worldwind/layer/graticule/UTMGraticuleTile.java new file mode 100644 index 000000000..8998b1368 --- /dev/null +++ b/worldwind/src/main/java/gov/nasa/worldwind/layer/graticule/UTMGraticuleTile.java @@ -0,0 +1,145 @@ +package gov.nasa.worldwind.layer.graticule; + +import java.util.ArrayList; +import java.util.List; + +import gov.nasa.worldwind.WorldWind; +import gov.nasa.worldwind.geom.Position; +import gov.nasa.worldwind.geom.Sector; +import gov.nasa.worldwind.geom.coords.Hemisphere; +import gov.nasa.worldwind.geom.coords.UTMCoord; +import gov.nasa.worldwind.render.RenderContext; +import gov.nasa.worldwind.render.Renderable; + +class UTMGraticuleTile extends AbstractGraticuleTile { + + private static final int MIN_CELL_SIZE_PIXELS = 40; // TODO: make settable + + private final int zone; + private final Hemisphere hemisphere; + + private List squares; + + UTMGraticuleTile(UTMGraticuleLayer layer, Sector sector, int zone) { + super(layer, sector); + this.zone = zone; + this.hemisphere = sector.centroidLatitude() > 0 ? Hemisphere.N : Hemisphere.S; + } + + @Override + UTMGraticuleLayer getLayer() { + return (UTMGraticuleLayer) super.getLayer(); + } + + @Override + void selectRenderables(RenderContext rc) { + super.selectRenderables(rc); + + // Select tile grid elements + String graticuleType = getLayer().getTypeFor(UTMGraticuleLayer.UTM_ZONE_RESOLUTION); + for (GridElement ge : this.getGridElements()) + if (ge.isInView(rc)) + getLayer().addRenderable(ge.renderable, graticuleType); + + if (getSizeInPixels(rc) / 10 < MIN_CELL_SIZE_PIXELS * 2) + return; + + // Select child elements + if (this.squares == null) + createSquares(); + + for (UTMSquareZone sz : this.squares) + if (sz.isInView(rc)) + sz.selectRenderables(rc); + else + sz.clearRenderables(); + } + + @Override + void clearRenderables() { + super.clearRenderables(); + if (this.squares != null) { + for (UTMSquareZone sz : this.squares) + sz.clearRenderables(); + this.squares.clear(); + this.squares = null; + } + } + + private void createSquares() { + // Find grid zone easting and northing boundaries + UTMCoord UTM; + UTM = UTMCoord.fromLatLon(this.getSector().minLatitude(), this.getSector().centroidLongitude()); + double minNorthing = UTM.getNorthing(); + UTM = UTMCoord.fromLatLon(this.getSector().maxLatitude(), this.getSector().centroidLongitude()); + double maxNorthing = UTM.getNorthing(); + maxNorthing = maxNorthing == 0 ? 10e6 : maxNorthing; + UTM = UTMCoord.fromLatLon(this.getSector().minLatitude(), this.getSector().minLongitude()); + double minEasting = UTM.getEasting(); + UTM = UTMCoord.fromLatLon(this.getSector().maxLatitude(), this.getSector().minLongitude()); + minEasting = UTM.getEasting() < minEasting ? UTM.getEasting() : minEasting; + double maxEasting = 1e6 - minEasting; + + // Create squares + this.squares = getLayer().createSquaresGrid(this.zone, this.hemisphere, this.getSector(), + minEasting, maxEasting, minNorthing, maxNorthing); + } + + @Override + void createRenderables() { + super.createRenderables(); + + List positions = new ArrayList<>(); + + // Generate west meridian + positions.clear(); + positions.add(Position.fromDegrees(this.getSector().minLatitude(), this.getSector().minLongitude(), 0)); + positions.add(Position.fromDegrees(this.getSector().maxLatitude(), this.getSector().minLongitude(), 0)); + Renderable polyline = getLayer().createLineRenderable(new ArrayList<>(positions), WorldWind.LINEAR); + Sector lineSector = Sector.fromDegrees(this.getSector().minLatitude(), this.getSector().minLongitude(), + this.getSector().deltaLatitude(), 1E-15); + this.getGridElements().add(new GridElement(lineSector, polyline, GridElement.TYPE_LINE, this.getSector().minLongitude())); + + // Generate south parallel at south pole and equator + if (this.getSector().minLatitude() == UTMGraticuleLayer.UTM_MIN_LATITUDE || this.getSector().minLatitude() == 0) { + positions.clear(); + positions.add(Position.fromDegrees(this.getSector().minLatitude(), this.getSector().minLongitude(), 0)); + positions.add(Position.fromDegrees(this.getSector().minLatitude(), this.getSector().maxLongitude(), 0)); + polyline = getLayer().createLineRenderable(new ArrayList<>(positions), WorldWind.LINEAR); + lineSector = Sector.fromDegrees(this.getSector().minLatitude(), this.getSector().minLongitude(), + 1E-15, this.getSector().deltaLongitude()); + this.getGridElements().add(new GridElement(lineSector, polyline, GridElement.TYPE_LINE, this.getSector().minLatitude())); + } + + // Generate north parallel at north pole + if (this.getSector().maxLatitude() == UTMGraticuleLayer.UTM_MAX_LATITUDE) { + positions.clear(); + positions.add(Position.fromDegrees(this.getSector().maxLatitude(), this.getSector().minLongitude(), 0)); + positions.add(Position.fromDegrees(this.getSector().maxLatitude(), this.getSector().maxLongitude(), 0)); + polyline = getLayer().createLineRenderable(new ArrayList<>(positions), WorldWind.LINEAR); + lineSector = Sector.fromDegrees(this.getSector().maxLatitude(), this.getSector().minLongitude(), + 1E-15, this.getSector().deltaLongitude()); + this.getGridElements().add(new GridElement(lineSector, polyline, GridElement.TYPE_LINE, this.getSector().maxLatitude())); + } + + // Add label + if (this.hasLabel()) { + Renderable text = this.getLayer().createTextRenderable(Position.fromDegrees(this.getSector().centroidLatitude(), this.getSector().centroidLongitude(), 0), String.valueOf(this.zone) + this.hemisphere, 10e6); + this.getGridElements().add(new GridElement(this.getSector(), text, GridElement.TYPE_GRIDZONE_LABEL)); + } + } + + private boolean hasLabel() { + // Has label if it contains hemisphere mid latitude + double southLat = UTMGraticuleLayer.UTM_MIN_LATITUDE / 2d; + boolean southLabel = this.getSector().minLatitude() < southLat + && southLat <= this.getSector().maxLatitude(); + + double northLat = UTMGraticuleLayer.UTM_MAX_LATITUDE / 2d; + boolean northLabel = this.getSector().minLatitude() < northLat + && northLat <= this.getSector().maxLatitude(); + + return southLabel || northLabel; + } + +} diff --git a/worldwind/src/main/java/gov/nasa/worldwind/layer/graticule/UTMMetricScaleSupport.java b/worldwind/src/main/java/gov/nasa/worldwind/layer/graticule/UTMMetricScaleSupport.java new file mode 100644 index 000000000..005ede490 --- /dev/null +++ b/worldwind/src/main/java/gov/nasa/worldwind/layer/graticule/UTMMetricScaleSupport.java @@ -0,0 +1,241 @@ +package gov.nasa.worldwind.layer.graticule; + +import android.support.annotation.NonNull; + +import gov.nasa.worldwind.geom.Frustum; +import gov.nasa.worldwind.geom.Position; +import gov.nasa.worldwind.geom.Vec3; +import gov.nasa.worldwind.geom.coords.Hemisphere; +import gov.nasa.worldwind.geom.coords.UPSCoord; +import gov.nasa.worldwind.geom.coords.UTMCoord; +import gov.nasa.worldwind.render.RenderContext; +import gov.nasa.worldwind.render.Renderable; + +class UTMMetricScaleSupport { + + private class UTMExtremes { + double minX, maxX, minY, maxY; + Hemisphere minYHemisphere, maxYHemisphere; + + UTMExtremes() { + this.clear(); + } + + void clear() { + minX = 1e6; + maxX = 0; + minY = 10e6; + maxY = 0; + minYHemisphere = Hemisphere.N; + maxYHemisphere = Hemisphere.S; + } + } + + private static final double OFFSET_FACTOR_X = -.5; + private static final double OFFSET_FACTOR_Y = -.5; + private static final double VISIBLE_DISTANCE_FACTOR = 10; + + private final AbstractUTMGraticuleLayer layer; + + private int scaleModulo = (int) 10e6; + private double maxResolution = 1e5; + private int zone; + + // 5 levels 100km to 10m + private UTMExtremes[] extremes; + + UTMMetricScaleSupport(AbstractUTMGraticuleLayer layer) { + this.layer = layer; + } + + void setScaleModulo(int modulo) { + this.scaleModulo = modulo; + } + + void setMaxResolution(double maxResolution) { + this.maxResolution = maxResolution; + this.clear(); + } + + int getZone() { + return this.zone; + } + + void computeZone(RenderContext rc) { + try { + if(layer.hasLookAtPos(rc)) { + double latitude = layer.getLookAtLatitude(rc); + double longitude = layer.getLookAtLongitude(rc); + if (latitude <= AbstractUTMGraticuleLayer.UTM_MAX_LATITUDE + && latitude >= AbstractUTMGraticuleLayer.UTM_MIN_LATITUDE) { + UTMCoord UTM = UTMCoord.fromLatLon(latitude, longitude); + this.zone = UTM.getZone(); + } else + this.zone = 0; + } + } catch (Exception ex) { + this.zone = 0; + } + } + + void clear() { + int numLevels = (int) Math.log10(this.maxResolution); + this.extremes = new UTMExtremes[numLevels]; + for (int i = 0; i < numLevels; i++) { + this.extremes[i] = new UTMExtremes(); + this.extremes[i].clear(); + } + } + + void computeMetricScaleExtremes(int UTMZone, Hemisphere hemisphere, GridElement ge, double size) { + if (UTMZone != this.zone) + return; + if (size < 1 || size > this.maxResolution) + return; + + UTMExtremes levelExtremes = this.extremes[(int) Math.log10(size) - 1]; + + if (ge.type.equals(GridElement.TYPE_LINE_EASTING) + || ge.type.equals(GridElement.TYPE_LINE_EAST) + || ge.type.equals(GridElement.TYPE_LINE_WEST)) { + levelExtremes.minX = ge.value < levelExtremes.minX ? ge.value : levelExtremes.minX; + levelExtremes.maxX = ge.value > levelExtremes.maxX ? ge.value : levelExtremes.maxX; + } else if (ge.type.equals(GridElement.TYPE_LINE_NORTHING) + || ge.type.equals(GridElement.TYPE_LINE_SOUTH) + || ge.type.equals(GridElement.TYPE_LINE_NORTH)) { + if (hemisphere.equals(levelExtremes.minYHemisphere)) + levelExtremes.minY = ge.value < levelExtremes.minY ? ge.value : levelExtremes.minY; + else if (hemisphere.equals(Hemisphere.S)) { + levelExtremes.minY = ge.value; + levelExtremes.minYHemisphere = hemisphere; + } + if (hemisphere.equals(levelExtremes.maxYHemisphere)) + levelExtremes.maxY = ge.value > levelExtremes.maxY ? ge.value : levelExtremes.maxY; + else if (hemisphere.equals(Hemisphere.N)) { + levelExtremes.maxY = ge.value; + levelExtremes.maxYHemisphere = hemisphere; + } + } + } + + void selectRenderables(RenderContext rc) { + if(!layer.hasLookAtPos(rc)) { + return; + } + + // Compute easting and northing label offsets + double pixelSize = layer.getPixelSize(rc); + double eastingOffset = rc.viewport.width * pixelSize * OFFSET_FACTOR_X / 2; + double northingOffset = rc.viewport.height * pixelSize * OFFSET_FACTOR_Y / 2; + // Derive labels center pos from the view center + double labelEasting; + double labelNorthing; + Hemisphere labelHemisphere; + if (this.zone > 0) { + UTMCoord UTM = UTMCoord.fromLatLon(layer.getLookAtLatitude(rc), layer.getLookAtLongitude(rc)); + labelEasting = UTM.getEasting() + eastingOffset; + labelNorthing = UTM.getNorthing() + northingOffset; + labelHemisphere = UTM.getHemisphere(); + if (labelNorthing < 0) { + labelNorthing = 10e6 + labelNorthing; + labelHemisphere = Hemisphere.S; + } + } else { + UPSCoord UPS = UPSCoord.fromLatLon(layer.getLookAtLatitude(rc), layer.getLookAtLongitude(rc)); + labelEasting = UPS.getEasting() + eastingOffset; + labelNorthing = UPS.getNorthing() + northingOffset; + labelHemisphere = UPS.getHemisphere(); + } + + Frustum viewFrustum = rc.frustum; + + Position labelPos; + for (int i = 0; i < this.extremes.length; i++) { + UTMExtremes levelExtremes = this.extremes[i]; + double gridStep = Math.pow(10, i); + double gridStepTimesTen = gridStep * 10; + String graticuleType = layer.getTypeFor(gridStep); + if (levelExtremes.minX <= levelExtremes.maxX) { + // Process easting scale labels for this level + for (double easting = levelExtremes.minX; easting <= levelExtremes.maxX; easting += gridStep) { + // Skip multiples of ten grid steps except for last (higher) level + if (i == this.extremes.length - 1 || easting % gridStepTimesTen != 0) { + labelPos = layer.computePosition(this.zone, labelHemisphere, easting, labelNorthing); + if (labelPos == null) + continue; + double lat = labelPos.latitude; + double lon = labelPos.longitude; + Vec3 surfacePoint = layer.getSurfacePoint(rc, lat, lon); + if (viewFrustum.containsPoint(surfacePoint) && isPointInRange(rc, surfacePoint)) { + String text = String.valueOf((int) (easting % this.scaleModulo)); + Renderable gt = this.layer.createTextRenderable(Position.fromDegrees(lat, lon, 0), text, gridStepTimesTen); + layer.addRenderable(gt, graticuleType); + } + } + } + } + if (!(levelExtremes.maxYHemisphere.equals(Hemisphere.S) && levelExtremes.maxY == 0)) { + // Process northing scale labels for this level + Hemisphere currentHemisphere = levelExtremes.minYHemisphere; + for (double northing = levelExtremes.minY; (northing <= levelExtremes.maxY) + || !currentHemisphere.equals(levelExtremes.maxYHemisphere); northing += gridStep) { + // Skip multiples of ten grid steps except for last (higher) level + if (i == this.extremes.length - 1 || northing % gridStepTimesTen != 0) { + labelPos = layer.computePosition(this.zone, currentHemisphere, labelEasting, northing); + if (labelPos == null) + continue; + double lat = labelPos.latitude; + double lon = labelPos.longitude; + Vec3 surfacePoint = layer.getSurfacePoint(rc, lat, lon); + if (viewFrustum.containsPoint(surfacePoint) && isPointInRange(rc, surfacePoint)) { + String text = String.valueOf((int) (northing % this.scaleModulo)); + Renderable gt = this.layer.createTextRenderable(Position.fromDegrees(lat, lon, 0), text, gridStepTimesTen); + layer.addRenderable(gt, graticuleType); + } + + if (!currentHemisphere.equals(levelExtremes.maxYHemisphere) + && northing >= 10e6 - gridStep) { + // Switch hemisphere + currentHemisphere = levelExtremes.maxYHemisphere; + northing = -gridStep; + } + } + } + } // end northing + } // for levels + } + + private boolean isPointInRange(RenderContext rc, Vec3 point) { + double altitudeAboveGround = layer.computeAltitudeAboveGround(rc); + return rc.cameraPoint.distanceTo(point) < altitudeAboveGround * VISIBLE_DISTANCE_FACTOR; + } + + @Override + @NonNull + public String toString() { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < 5; i++) { + sb.append("level "); + sb.append(String.valueOf(i)); + sb.append(" : "); + UTMExtremes levelExtremes = this.extremes[i]; + if (levelExtremes.minX < levelExtremes.maxX || + !(levelExtremes.maxYHemisphere.equals(Hemisphere.S) && levelExtremes.maxY == 0)) { + sb.append(levelExtremes.minX); + sb.append(", "); + sb.append(levelExtremes.maxX); + sb.append(" - "); + sb.append(levelExtremes.minY); + sb.append(levelExtremes.minYHemisphere); + sb.append(", "); + sb.append(levelExtremes.maxY); + sb.append(levelExtremes.maxYHemisphere); + } else { + sb.append("empty"); + } + sb.append("\n"); + } + return sb.toString(); + } + +} diff --git a/worldwind/src/main/java/gov/nasa/worldwind/layer/graticule/UTMSquareGrid.java b/worldwind/src/main/java/gov/nasa/worldwind/layer/graticule/UTMSquareGrid.java new file mode 100644 index 000000000..a6de0ee2d --- /dev/null +++ b/worldwind/src/main/java/gov/nasa/worldwind/layer/graticule/UTMSquareGrid.java @@ -0,0 +1,134 @@ +package gov.nasa.worldwind.layer.graticule; + +import java.util.ArrayList; +import java.util.List; + +import gov.nasa.worldwind.WorldWind; +import gov.nasa.worldwind.geom.Position; +import gov.nasa.worldwind.geom.Sector; +import gov.nasa.worldwind.geom.coords.Hemisphere; +import gov.nasa.worldwind.render.RenderContext; +import gov.nasa.worldwind.render.Renderable; + +/** Represent a square 10x10 grid and recursive tree in easting/northing coordinates */ +class UTMSquareGrid extends UTMSquareSector { + + private List subGrids; + + UTMSquareGrid(AbstractUTMGraticuleLayer layer, int UTMZone, Hemisphere hemisphere, Sector UTMZoneSector, + double SWEasting, double SWNorthing, double size) { + super(layer, UTMZone, hemisphere, UTMZoneSector, SWEasting, SWNorthing, size); + } + + @Override + boolean isInView(RenderContext rc) { + return super.isInView(rc) && getSizeInPixels(rc) > MIN_CELL_SIZE_PIXELS * 4; + } + + @Override + void selectRenderables(RenderContext rc) { + super.selectRenderables(rc); + + boolean drawMetricLabels = getSizeInPixels(rc) > MIN_CELL_SIZE_PIXELS * 4 * 1.7; + String graticuleType = getLayer().getTypeFor(this.size / 10); + + for (GridElement ge : this.getGridElements()) { + if (ge.isInView(rc)) { + if (drawMetricLabels) + getLayer().computeMetricScaleExtremes(this.UTMZone, this.hemisphere, ge, this.size); + + getLayer().addRenderable(ge.renderable, graticuleType); + } + } + + if (getSizeInPixels(rc) <= MIN_CELL_SIZE_PIXELS * 4 * 2) + return; + + // Select sub grids renderables + if (this.subGrids == null) + createSubGrids(); + + for (UTMSquareGrid sg : this.subGrids) { + if (sg.isInView(rc)) + sg.selectRenderables(rc); + else + sg.clearRenderables(); + } + } + + @Override + void clearRenderables() { + super.clearRenderables(); + if (this.subGrids != null) { + for (UTMSquareGrid sg : this.subGrids) + sg.clearRenderables(); + this.subGrids.clear(); + this.subGrids = null; + } + } + + private void createSubGrids() { + this.subGrids = new ArrayList<>(); + double gridStep = this.size / 10; + for (int i = 0; i < 10; i++) { + double easting = this.SWEasting + gridStep * i; + for (int j = 0; j < 10; j++) { + double northing = this.SWNorthing + gridStep * j; + UTMSquareGrid sg = new UTMSquareGrid(this.getLayer(), this.UTMZone, this.hemisphere, this.UTMZoneSector, + easting, northing, gridStep); + if (!sg.isOutsideGridZone()) + this.subGrids.add(sg); + } + } + } + + @Override + void createRenderables() { + super.createRenderables(); + double gridStep = this.size / 10; + Position p1, p2; + List positions = new ArrayList<>(); + + // South-North lines + for (int i = 1; i <= 9; i++) { + double easting = this.SWEasting + gridStep * i; + positions.clear(); + p1 = getLayer().computePosition(this.UTMZone, this.hemisphere, easting, SWNorthing); + p2 = getLayer().computePosition(this.UTMZone, this.hemisphere, easting, SWNorthing + this.size); + if (this.isTruncated) { + getLayer().computeTruncatedSegment(p1, p2, this.UTMZoneSector, positions); + } else { + positions.add(p1); + positions.add(p2); + } + if (positions.size() > 0) { + p1 = positions.get(0); + p2 = positions.get(1); + Renderable polyline = getLayer().createLineRenderable(new ArrayList<>(positions), WorldWind.GREAT_CIRCLE); + Sector lineSector = boundingSector(p1, p2); + this.getGridElements().add(new GridElement(lineSector, polyline, GridElement.TYPE_LINE_EASTING, easting)); + } + } + // West-East lines + for (int i = 1; i <= 9; i++) { + double northing = this.SWNorthing + gridStep * i; + positions.clear(); + p1 = getLayer().computePosition(this.UTMZone, this.hemisphere, SWEasting, northing); + p2 = getLayer().computePosition(this.UTMZone, this.hemisphere, SWEasting + this.size, northing); + if (this.isTruncated) { + getLayer().computeTruncatedSegment(p1, p2, this.UTMZoneSector, positions); + } else { + positions.add(p1); + positions.add(p2); + } + if (positions.size() > 0) { + p1 = positions.get(0); + p2 = positions.get(1); + Renderable polyline = getLayer().createLineRenderable(new ArrayList<>(positions), WorldWind.GREAT_CIRCLE); + Sector lineSector = boundingSector(p1, p2); + this.getGridElements().add(new GridElement(lineSector, polyline, GridElement.TYPE_LINE_NORTHING, northing)); + } + } + } + +} diff --git a/worldwind/src/main/java/gov/nasa/worldwind/layer/graticule/UTMSquareSector.java b/worldwind/src/main/java/gov/nasa/worldwind/layer/graticule/UTMSquareSector.java new file mode 100644 index 000000000..2f6026e4d --- /dev/null +++ b/worldwind/src/main/java/gov/nasa/worldwind/layer/graticule/UTMSquareSector.java @@ -0,0 +1,188 @@ +package gov.nasa.worldwind.layer.graticule; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import gov.nasa.worldwind.geom.Location; +import gov.nasa.worldwind.geom.Position; +import gov.nasa.worldwind.geom.Sector; +import gov.nasa.worldwind.geom.Vec3; +import gov.nasa.worldwind.geom.coords.Hemisphere; +import gov.nasa.worldwind.render.RenderContext; + +/** Represent a generic UTM/UPS square area */ +abstract class UTMSquareSector extends AbstractGraticuleTile { + + static final int MIN_CELL_SIZE_PIXELS = 50; + + final int UTMZone; + final Hemisphere hemisphere; + final Sector UTMZoneSector; + final double SWEasting; + final double SWNorthing; + final double size; + + Position sw, se, nw, ne; // Four corners position + Sector boundingSector; + Location centroid; + final Location squareCenter; + final boolean isTruncated; + + UTMSquareSector(AbstractUTMGraticuleLayer layer, int UTMZone, Hemisphere hemisphere, Sector UTMZoneSector, + double SWEasting, double SWNorthing, double size) { + super(layer, new Sector()); + this.UTMZone = UTMZone; + this.hemisphere = hemisphere; + this.UTMZoneSector = UTMZoneSector; + this.SWEasting = SWEasting; + this.SWNorthing = SWNorthing; + this.size = size; + + // Compute corners positions + this.sw = layer.computePosition(this.UTMZone, this.hemisphere, SWEasting, SWNorthing); + this.se = layer.computePosition(this.UTMZone, this.hemisphere, SWEasting + size, SWNorthing); + this.nw = layer.computePosition(this.UTMZone, this.hemisphere, SWEasting, SWNorthing + size); + this.ne = layer.computePosition(this.UTMZone, this.hemisphere, SWEasting + size, SWNorthing + size); + this.squareCenter = layer.computePosition(this.UTMZone, this.hemisphere, + SWEasting + size / 2, SWNorthing + size / 2); + + // Compute approximate bounding sector and center point + if (this.sw != null && this.se != null && this.nw != null && this.ne != null) { + adjustDateLineCrossingPoints(); + this.boundingSector = boundingSector(Arrays.asList(sw, se, nw, ne)); + if (!isInsideGridZone()) + this.boundingSector.intersect(this.UTMZoneSector); + + this.centroid = this.boundingSector != null ? this.boundingSector.centroid(new Location()) : this.squareCenter; + + if(this.boundingSector != null) { + this.getSector().set(this.boundingSector); + } + } + + // Check whether this square is truncated by the grid zone boundary + this.isTruncated = !isInsideGridZone(); + } + + @Override + AbstractUTMGraticuleLayer getLayer() { + return (AbstractUTMGraticuleLayer) super.getLayer(); + } + + Sector boundingSector(Location pA, Location pB) { + double minLat = pA.latitude; + double minLon = pA.longitude; + double maxLat = pA.latitude; + double maxLon = pA.longitude; + + if (pB.latitude < minLat) + minLat = pB.latitude; + else if (pB.latitude > maxLat) + maxLat = pB.latitude; + + if (pB.longitude < minLon) + minLon = pB.longitude; + else if (pB.longitude > maxLon) + maxLon = pB.longitude; + + return Sector.fromDegrees(minLat, minLon, maxLat - minLat + 1E-15, maxLon - minLon + 1E-15); + } + + private Sector boundingSector(Iterable locations) { + double minLat = 90; + double minLon = 180; + double maxLat = -90; + double maxLon = -180; + + for (Location p : locations) { + double lat = p.latitude; + if (lat < minLat) + minLat = lat; + if (lat > maxLat) + maxLat = lat; + + double lon = p.longitude; + if (lon < minLon) + minLon = lon; + if (lon > maxLon) + maxLon = lon; + } + + return Sector.fromDegrees(minLat, minLon, maxLat - minLat + 1E-15, maxLon - minLon + 1E-15); + } + + private void adjustDateLineCrossingPoints() { + List corners = new ArrayList<>(Arrays.asList(sw, se, nw, ne)); + if (!locationsCrossDateLine(corners)) + return; + + double lonSign = 0; + for (Location corner : corners) { + if (Math.abs(corner.longitude) != 180) + lonSign = Math.signum(corner.longitude); + } + + if (lonSign == 0) + return; + + if (Math.abs(sw.longitude) == 180 && Math.signum(sw.longitude) != lonSign) + sw = Position.fromDegrees(sw.latitude, sw.longitude * -1, sw.altitude); + if (Math.abs(se.longitude) == 180 && Math.signum(se.longitude) != lonSign) + se = Position.fromDegrees(se.latitude, se.longitude * -1, se.altitude); + if (Math.abs(nw.longitude) == 180 && Math.signum(nw.longitude) != lonSign) + nw = Position.fromDegrees(nw.latitude, nw.longitude * -1, nw.altitude); + if (Math.abs(ne.longitude) == 180 && Math.signum(ne.longitude) != lonSign) + ne = Position.fromDegrees(ne.latitude, ne.longitude * -1, ne.altitude); + } + + private boolean locationsCrossDateLine(Iterable locations) { + Location pos = null; + for (Location posNext : locations) { + if (pos != null) { + // A segment cross the line if end pos have different longitude signs + // and are more than 180 degrees longitude apart + if (Math.signum(pos.longitude) != Math.signum(posNext.longitude)) { + double delta = Math.abs(pos.longitude - posNext.longitude); + if (delta > 180 && delta < 360) + return true; + } + } + pos = posNext; + } + + return false; + } + + /** + * Determines whether this square is fully inside its parent grid zone. + * + * @return true if this square is totaly inside its parent grid zone. + */ + private boolean isInsideGridZone() { + return this.isPositionInside(this.nw) && this.isPositionInside(this.ne) + && this.isPositionInside(this.sw) && this.isPositionInside(this.se); + } + + /** + * Determines whether this square is fully outside its parent grid zone. + * + * @return true if this square is totaly outside its parent grid zone. + */ + boolean isOutsideGridZone() { + return !this.isPositionInside(this.nw) && !this.isPositionInside(this.ne) + && !this.isPositionInside(this.sw) && !this.isPositionInside(this.se); + } + + boolean isPositionInside(Location position) { + return position != null && this.UTMZoneSector.contains(position.latitude, position.longitude); + } + + @Override + double getSizeInPixels(RenderContext rc) { + Vec3 centerPoint = getLayer().getSurfacePoint(rc, this.centroid.latitude, this.centroid.longitude); + double distance = rc.cameraPoint.distanceTo(centerPoint); + return this.size / rc.pixelSizeAtDistance(distance) / rc.resources.getDisplayMetrics().density; + } + +} diff --git a/worldwind/src/main/java/gov/nasa/worldwind/layer/graticule/UTMSquareZone.java b/worldwind/src/main/java/gov/nasa/worldwind/layer/graticule/UTMSquareZone.java new file mode 100644 index 000000000..2b28c0025 --- /dev/null +++ b/worldwind/src/main/java/gov/nasa/worldwind/layer/graticule/UTMSquareZone.java @@ -0,0 +1,189 @@ +package gov.nasa.worldwind.layer.graticule; + +import java.util.ArrayList; +import java.util.List; + +import gov.nasa.worldwind.WorldWind; +import gov.nasa.worldwind.geom.Location; +import gov.nasa.worldwind.geom.Position; +import gov.nasa.worldwind.geom.Sector; +import gov.nasa.worldwind.geom.coords.Hemisphere; +import gov.nasa.worldwind.render.RenderContext; +import gov.nasa.worldwind.render.Renderable; + +/** Represent a 100km square zone inside an UTM zone. */ +class UTMSquareZone extends UTMSquareSector { + + private String name; + private UTMSquareGrid squareGrid; + private UTMSquareZone northNeighbor, eastNeighbor; + + UTMSquareZone(AbstractUTMGraticuleLayer layer, int UTMZone, Hemisphere hemisphere, Sector UTMZoneSector, + double SWEasting, double SWNorthing, double size) { + super(layer, UTMZone, hemisphere, UTMZoneSector, SWEasting, SWNorthing, size); + } + + @Override + boolean isInView(RenderContext rc) { + return super.isInView(rc) && getSizeInPixels(rc) > MIN_CELL_SIZE_PIXELS; + } + + void setName(String name) { + this.name = name; + } + + void setNorthNeighbor(UTMSquareZone sz) { + this.northNeighbor = sz; + } + + void setEastNeighbor(UTMSquareZone sz) { + this.eastNeighbor = sz; + } + + @Override + void selectRenderables(RenderContext rc) { + super.selectRenderables(rc); + + boolean drawMetricLabels = getSizeInPixels(rc) > MIN_CELL_SIZE_PIXELS * 2; + String graticuleType = getLayer().getTypeFor(this.size); + for (GridElement ge : this.getGridElements()) { + if (ge.isInView(rc)) { + if (ge.type.equals(GridElement.TYPE_LINE_NORTH) && this.isNorthNeighborInView(rc)) + continue; + if (ge.type.equals(GridElement.TYPE_LINE_EAST) && this.isEastNeighborInView(rc)) + continue; + + if (drawMetricLabels) + getLayer().computeMetricScaleExtremes(this.UTMZone, this.hemisphere, ge, + this.size * 10); + getLayer().addRenderable(ge.renderable, graticuleType); + } + } + + if (getSizeInPixels(rc) <= MIN_CELL_SIZE_PIXELS * 2) + return; + + // Select grid renderables + if (this.squareGrid == null) + this.squareGrid = new UTMSquareGrid(getLayer(), this.UTMZone, this.hemisphere, this.UTMZoneSector, this.SWEasting, + this.SWNorthing, this.size); + + if (this.squareGrid.isInView(rc)) + this.squareGrid.selectRenderables(rc); + else + this.squareGrid.clearRenderables(); + } + + private boolean isNorthNeighborInView(RenderContext rc) { + return this.northNeighbor != null && this.northNeighbor.isInView(rc); + } + + private boolean isEastNeighborInView(RenderContext rc) { + return this.eastNeighbor != null && this.eastNeighbor.isInView(rc); + } + + @Override + void clearRenderables() { + super.clearRenderables(); + if (this.squareGrid != null) { + this.squareGrid.clearRenderables(); + this.squareGrid = null; + } + } + + @Override + void createRenderables() { + super.createRenderables(); + + List positions = new ArrayList<>(); + Position p1, p2; + Renderable polyline; + Sector lineSector; + + // left segment + positions.clear(); + if (this.isTruncated) { + getLayer().computeTruncatedSegment(sw, nw, this.UTMZoneSector, positions); + } else { + positions.add(sw); + positions.add(nw); + } + if (positions.size() > 0) { + p1 = positions.get(0); + p2 = positions.get(1); + polyline = getLayer().createLineRenderable(new ArrayList<>(positions), WorldWind.GREAT_CIRCLE); + lineSector = boundingSector(p1, p2); + this.getGridElements().add(new GridElement(lineSector, polyline, GridElement.TYPE_LINE_WEST, this.SWEasting)); + } + + // right segment + positions.clear(); + if (this.isTruncated) { + getLayer().computeTruncatedSegment(se, ne, this.UTMZoneSector, positions); + } else { + positions.add(se); + positions.add(ne); + } + if (positions.size() > 0) { + p1 = positions.get(0); + p2 = positions.get(1); + polyline = getLayer().createLineRenderable(new ArrayList<>(positions), WorldWind.GREAT_CIRCLE); + lineSector = boundingSector(p1, p2); + this.getGridElements().add(new GridElement(lineSector, polyline, GridElement.TYPE_LINE_EAST, this.SWEasting + this.size)); + } + + // bottom segment + positions.clear(); + if (this.isTruncated) { + getLayer().computeTruncatedSegment(sw, se, this.UTMZoneSector, positions); + } else { + positions.add(sw); + positions.add(se); + } + if (positions.size() > 0) { + p1 = positions.get(0); + p2 = positions.get(1); + polyline = getLayer().createLineRenderable(new ArrayList<>(positions), WorldWind.GREAT_CIRCLE); + lineSector = boundingSector(p1, p2); + this.getGridElements().add(new GridElement(lineSector, polyline, GridElement.TYPE_LINE_SOUTH, this.SWNorthing)); + } + + // top segment + positions.clear(); + if (this.isTruncated) { + getLayer().computeTruncatedSegment(nw, ne, this.UTMZoneSector, positions); + } else { + positions.add(nw); + positions.add(ne); + } + if (positions.size() > 0) { + p1 = positions.get(0); + p2 = positions.get(1); + polyline = getLayer().createLineRenderable(new ArrayList<>(positions), WorldWind.GREAT_CIRCLE); + lineSector = boundingSector(p1, p2); + this.getGridElements().add(new GridElement(lineSector, polyline, GridElement.TYPE_LINE_NORTH, this.SWNorthing + this.size)); + } + + // Label + if (this.name != null) { + // Only add a label to squares above some dimension + if (this.boundingSector.deltaLongitude() * Math.cos(Math.toRadians(this.centroid.latitude)) > .2 + && this.boundingSector.deltaLatitude() > .2) { + Location labelPos = null; + if (this.UTMZone != 0) { // Not at poles + labelPos = this.centroid; + } else if (this.isPositionInside(Position.fromDegrees(this.squareCenter.latitude, this.squareCenter.longitude, 0))) { + labelPos = this.squareCenter; + } else if (this.squareCenter.latitude <= this.UTMZoneSector.maxLatitude() + && this.squareCenter.latitude >= this.UTMZoneSector.minLatitude()) { + labelPos = this.centroid; + } + if (labelPos != null) { + Renderable text = this.getLayer().createTextRenderable(Position.fromDegrees(labelPos.latitude, labelPos.longitude, 0), this.name, this.size * 10); + this.getGridElements().add(new GridElement(this.boundingSector, text, GridElement.TYPE_GRIDZONE_LABEL)); + } + } + } + } + +} diff --git a/worldwind/src/test/java/gov/nasa/worldwind/geom/FrustumTest.java b/worldwind/src/test/java/gov/nasa/worldwind/geom/FrustumTest.java index 58cb5d661..5cc4a1084 100644 --- a/worldwind/src/test/java/gov/nasa/worldwind/geom/FrustumTest.java +++ b/worldwind/src/test/java/gov/nasa/worldwind/geom/FrustumTest.java @@ -177,100 +177,102 @@ public void testIntersectsSegment() throws Exception { assertFalse("outside far", frustum.intersectsSegment(new Vec3(0, 0, 2), new Vec3(0, 0, 1.0000001))); } - @Test - public void testSetToModelviewProjection() throws Exception { - // The expected test values were obtained via SystemOut on Frustum object - // at a time in the development cycle when the setToModelviewProjection - // was known to be working correctly (via observed runtime behavior). - // This unit test simply tests for changes in the behavior since that time. - - // Create a Frustum similar to the way the WorldWindow does it. - - // Setup a Navigator, looking near Oxnard Airport. - LookAt lookAt = new LookAt().set(34.15, -119.15, 0, WorldWind.ABSOLUTE, 2e4 /*range*/, 0 /*heading*/, 45 /*tilt*/, 0 /*roll*/); - Navigator navigator = new Navigator(); - navigator.setAsLookAt(globe, lookAt); - - // Compute a perspective projection matrix given the viewport, field of view, and clip distances. - Viewport viewport = new Viewport(0, 0, 100, 100); // screen coordinates - double nearDistance = navigator.getAltitude() * 0.75; - double farDistance = globe.horizonDistance(navigator.getAltitude()) + globe.horizonDistance(160000); - Matrix4 projection = new Matrix4(); - projection.setToPerspectiveProjection(viewport.width, viewport.height, 45d /*fovy*/, nearDistance, farDistance); - - // Compute a Cartesian viewing matrix using this Navigator's properties as a Camera. - Matrix4 modelview = new Matrix4(); - navigator.getAsViewingMatrix(globe, modelview); - - // Compute the Frustum - Frustum frustum = new Frustum(); - frustum.setToModelviewProjection(projection, modelview, viewport); - - // Evaluate the results with known values captured on 07/19/2016 - //System.out.println(frustumToString(frustum)); - Plane bottom = new Plane(0.17635740224291638, 0.9793994030381801, 0.09836094754823524, -2412232.453445458); - Plane left = new Plane(-0.12177864151960982, 0.07203573632653165, 0.9899398038070459, 1737116.8972521012); - Plane right = new Plane(0.7782605589154529, 0.07203573632653174, -0.6237959242640989, 1737116.8972521003); - Plane top = new Plane(0.48012451515292665, -0.8353279303851167, 0.2677829319947119, 5886466.24794966); - Plane near = new Plane(0.8577349603804412, 0.1882384504636923, 0.4783900328269719, 4528686.830908618); - Plane far = new Plane(-0.8577349603804412, -0.1882384504636923, -0.4783900328269719, -2676528.6881595235); - - assertEquals("left", left, frustum.left); - assertEquals("right", right, frustum.right); - assertEquals("bottom", bottom, frustum.bottom); - assertEquals("top", top, frustum.top); - assertEquals("near", near, frustum.near); - assertEquals("far", far, frustum.far); - assertEquals("viewport", viewport, frustum.viewport); - } - - @Test - public void testSetToModelviewProjection_SubViewport() throws Exception { - // The expected test values were obtained via SystemOut on Frustum object - // at a time in the development cycle when the setToModelviewProjection - // was known to be working correctly (via observed runtime behavior). - // This unit test simply tests for changes in the behavior since that time. - - // Create a Frustum similar to the way the WorldWindow does it when picking - - // Setup a Navigator, looking near Oxnard Airport. - LookAt lookAt = new LookAt().set(34.15, -119.15, 0, WorldWind.ABSOLUTE, 2e4 /*range*/, 0 /*heading*/, 45 /*tilt*/, 0 /*roll*/); - Navigator navigator = new Navigator(); - navigator.setAsLookAt(globe, lookAt); - - // Compute a perspective projection matrix given the viewport, field of view, and clip distances. - Viewport viewport = new Viewport(0, 0, 100, 100); // screen coordinates - Viewport pickViewport = new Viewport(49, 49, 3, 3); // 3x3 viewport centered on a pick point - double nearDistance = navigator.getAltitude() * 0.75; - double farDistance = globe.horizonDistance(navigator.getAltitude()) + globe.horizonDistance(160000); - Matrix4 projection = new Matrix4(); - projection.setToPerspectiveProjection(viewport.width, viewport.height, 45d /*fovy*/, nearDistance, farDistance); - - // Compute a Cartesian viewing matrix using this Navigator's properties as a Camera. - Matrix4 modelview = new Matrix4(); - navigator.getAsViewingMatrix(globe, modelview); - - // Compute the Frustum - Frustum frustum = new Frustum(); - frustum.setToModelviewProjection(projection, modelview, viewport, pickViewport); - - // Evaluate the results with known values captured on 06/03/2016 - //System.out.println(frustumToString(frustum)); - Plane bottom = new Plane(-0.15728647066358287, 0.9836490211411795, -0.0877243942936819, -4453465.7217097925); - Plane left = new Plane(-0.4799755263103557, 0.001559364875310035, 0.8772804925018466, 37603.54528193692); - Plane right = new Plane(0.5012403287200531, 0.003118408767628064, -0.8653024953109584, 75199.35019616158); - Plane top = new Plane(0.17858448447919384, -0.9788701700756626, 0.09960307243927863, 4565806.392885632); - Plane near = new Plane(0.8577349603809148, 0.18823845046641746, 0.4783900328250505, 4528686.830896157); - Plane far = new Plane(-0.8577349603804465, -0.1882384504638284, -0.4783900328269087, -2676528.6881588553); - - assertEquals("left", left, frustum.left); - assertEquals("right", right, frustum.right); - assertEquals("bottom", bottom, frustum.bottom); - assertEquals("top", top, frustum.top); - assertEquals("near", near, frustum.near); - assertEquals("far", far, frustum.far); - assertEquals("viewport", pickViewport, frustum.viewport); - } +// NOTE Navigator is now dependent on WorldWindow instance which is dependent on Android Context. +// Move these tests to androidTest section? +// @Test +// public void testSetToModelviewProjection() throws Exception { +// // The expected test values were obtained via SystemOut on Frustum object +// // at a time in the development cycle when the setToModelviewProjection +// // was known to be working correctly (via observed runtime behavior). +// // This unit test simply tests for changes in the behavior since that time. +// +// // Create a Frustum similar to the way the WorldWindow does it. +// +// // Setup a Navigator, looking near Oxnard Airport. +// LookAt lookAt = new LookAt().set(34.15, -119.15, 0, WorldWind.ABSOLUTE, 2e4 /*range*/, 0 /*heading*/, 45 /*tilt*/, 0 /*roll*/); +// Navigator navigator = new Navigator(); +// navigator.setAsLookAt(globe, lookAt); +// +// // Compute a perspective projection matrix given the viewport, field of view, and clip distances. +// Viewport viewport = new Viewport(0, 0, 100, 100); // screen coordinates +// double nearDistance = navigator.getAltitude() * 0.75; +// double farDistance = globe.horizonDistance(navigator.getAltitude()) + globe.horizonDistance(160000); +// Matrix4 projection = new Matrix4(); +// projection.setToPerspectiveProjection(viewport.width, viewport.height, 45d /*fovy*/, nearDistance, farDistance); +// +// // Compute a Cartesian viewing matrix using this Navigator's properties as a Camera. +// Matrix4 modelview = new Matrix4(); +// navigator.getAsViewingMatrix(globe, modelview); +// +// // Compute the Frustum +// Frustum frustum = new Frustum(); +// frustum.setToModelviewProjection(projection, modelview, viewport); +// +// // Evaluate the results with known values captured on 07/19/2016 +// //System.out.println(frustumToString(frustum)); +// Plane bottom = new Plane(0.17635740224291638, 0.9793994030381801, 0.09836094754823524, -2412232.453445458); +// Plane left = new Plane(-0.12177864151960982, 0.07203573632653165, 0.9899398038070459, 1737116.8972521012); +// Plane right = new Plane(0.7782605589154529, 0.07203573632653174, -0.6237959242640989, 1737116.8972521003); +// Plane top = new Plane(0.48012451515292665, -0.8353279303851167, 0.2677829319947119, 5886466.24794966); +// Plane near = new Plane(0.8577349603804412, 0.1882384504636923, 0.4783900328269719, 4528686.830908618); +// Plane far = new Plane(-0.8577349603804412, -0.1882384504636923, -0.4783900328269719, -2676528.6881595235); +// +// assertEquals("left", left, frustum.left); +// assertEquals("right", right, frustum.right); +// assertEquals("bottom", bottom, frustum.bottom); +// assertEquals("top", top, frustum.top); +// assertEquals("near", near, frustum.near); +// assertEquals("far", far, frustum.far); +// assertEquals("viewport", viewport, frustum.viewport); +// } +// +// @Test +// public void testSetToModelviewProjection_SubViewport() throws Exception { +// // The expected test values were obtained via SystemOut on Frustum object +// // at a time in the development cycle when the setToModelviewProjection +// // was known to be working correctly (via observed runtime behavior). +// // This unit test simply tests for changes in the behavior since that time. +// +// // Create a Frustum similar to the way the WorldWindow does it when picking +// +// // Setup a Navigator, looking near Oxnard Airport. +// LookAt lookAt = new LookAt().set(34.15, -119.15, 0, WorldWind.ABSOLUTE, 2e4 /*range*/, 0 /*heading*/, 45 /*tilt*/, 0 /*roll*/); +// Navigator navigator = new Navigator(); +// navigator.setAsLookAt(globe, lookAt); +// +// // Compute a perspective projection matrix given the viewport, field of view, and clip distances. +// Viewport viewport = new Viewport(0, 0, 100, 100); // screen coordinates +// Viewport pickViewport = new Viewport(49, 49, 3, 3); // 3x3 viewport centered on a pick point +// double nearDistance = navigator.getAltitude() * 0.75; +// double farDistance = globe.horizonDistance(navigator.getAltitude()) + globe.horizonDistance(160000); +// Matrix4 projection = new Matrix4(); +// projection.setToPerspectiveProjection(viewport.width, viewport.height, 45d /*fovy*/, nearDistance, farDistance); +// +// // Compute a Cartesian viewing matrix using this Navigator's properties as a Camera. +// Matrix4 modelview = new Matrix4(); +// navigator.getAsViewingMatrix(globe, modelview); +// +// // Compute the Frustum +// Frustum frustum = new Frustum(); +// frustum.setToModelviewProjection(projection, modelview, viewport, pickViewport); +// +// // Evaluate the results with known values captured on 06/03/2016 +// //System.out.println(frustumToString(frustum)); +// Plane bottom = new Plane(-0.15728647066358287, 0.9836490211411795, -0.0877243942936819, -4453465.7217097925); +// Plane left = new Plane(-0.4799755263103557, 0.001559364875310035, 0.8772804925018466, 37603.54528193692); +// Plane right = new Plane(0.5012403287200531, 0.003118408767628064, -0.8653024953109584, 75199.35019616158); +// Plane top = new Plane(0.17858448447919384, -0.9788701700756626, 0.09960307243927863, 4565806.392885632); +// Plane near = new Plane(0.8577349603809148, 0.18823845046641746, 0.4783900328250505, 4528686.830896157); +// Plane far = new Plane(-0.8577349603804465, -0.1882384504638284, -0.4783900328269087, -2676528.6881588553); +// +// assertEquals("left", left, frustum.left); +// assertEquals("right", right, frustum.right); +// assertEquals("bottom", bottom, frustum.bottom); +// assertEquals("top", top, frustum.top); +// assertEquals("near", near, frustum.near); +// assertEquals("far", far, frustum.far); +// assertEquals("viewport", pickViewport, frustum.viewport); +// } @Test public void testIntersectsViewport() throws Exception { diff --git a/worldwind/src/test/java/gov/nasa/worldwind/geom/coords/CoordTest.java b/worldwind/src/test/java/gov/nasa/worldwind/geom/coords/CoordTest.java new file mode 100644 index 000000000..077e3f6be --- /dev/null +++ b/worldwind/src/test/java/gov/nasa/worldwind/geom/coords/CoordTest.java @@ -0,0 +1,93 @@ +/* + * Copyright (C) 2019 United States Government as represented by the Administrator of the + * National Aeronautics and Space Administration. + * All Rights Reserved. + */ +package gov.nasa.worldwind.geom.coords; + +import gov.nasa.worldwind.geom.Location; +import org.junit.Test; + +import static org.junit.Assert.*; + +public class CoordTest { + + private static boolean isClose(double x, double y, double limit) { + return (Math.abs(x - y) < limit); + } + + private static boolean isClose(Location a, Location b) { + double epsilonRad = Math.toRadians(9.0e-6); + return isClose(a, b, epsilonRad); + } + + private static boolean isClose(Location a, Location b, double limit) { + return isClose(Math.toRadians(a.latitude), Math.toRadians(b.latitude), limit) + && isClose(Math.toRadians(a.longitude), Math.toRadians(b.longitude), limit); + } + + private static final Location[] TEST_POSITIONS = { + Location.fromDegrees(-74.37916, 155.02235), + Location.fromDegrees(0, 0), + Location.fromDegrees(0.13, -0.2324), + Location.fromDegrees(-45.6456, 23.3545), + Location.fromDegrees(-12.7650, -33.8765), + Location.fromDegrees(23.4578, -135.4545), + Location.fromDegrees(77.3450, 156.9876) + }; + + @Test + public void utmConstructionTest() { + for (Location input : TEST_POSITIONS) { + UTMCoord fromLocation = UTMCoord.fromLatLon(input.latitude, input.longitude); + UTMCoord utmCoord = UTMCoord.fromUTM(fromLocation.getZone(), fromLocation.getHemisphere(), fromLocation.getEasting(), fromLocation.getNorthing()); + Location position = Location.fromDegrees(utmCoord.getLatitude(), utmCoord.getLongitude()); + assertTrue(isClose(input, position)); + } + } + + @Test + public void mgrsConstructionTest() { + for (Location input : TEST_POSITIONS) { + MGRSCoord fromLocation = MGRSCoord.fromLatLon(input.latitude, input.longitude); + MGRSCoord fromString = MGRSCoord.fromString(fromLocation.toString()); + Location position = Location.fromDegrees(fromString.getLatitude(), fromString.getLongitude()); + assertTrue(isClose(input, position, 0.0002)); + } + } + + private static final Location[] MGRS_ONLY_POSITIONS = { + Location.fromDegrees(-89.3454, -48.9306), + Location.fromDegrees(-80.5434, -170.6540), + }; + + @Test + public void mgrsOnlyConstructionTest() { + for (Location input : MGRS_ONLY_POSITIONS) { + MGRSCoord fromLocation = MGRSCoord.fromLatLon(input.latitude, input.longitude); + MGRSCoord fromString = MGRSCoord.fromString(fromLocation.toString()); + Location position = Location.fromDegrees(fromString.getLatitude(), fromString.getLongitude()); + assertTrue(isClose(input, position, 0.0002)); + } + } + + private static final Location[] NO_INVERSE_POSITIONS = { + Location.fromDegrees(90.0, 177.0), + Location.fromDegrees(-90.0, -177.0), + Location.fromDegrees(90.0, 3.0) + }; + + private static final String[] NO_INVERSE_TO_MGRS = { + "ZAH 00000 00000", "BAN 00000 00000", "ZAH 00000 00000" + }; + + @Test + public void noInverseToMGRSTest() { + for (int i = 0; i < NO_INVERSE_POSITIONS.length; i++) { + Location input = NO_INVERSE_POSITIONS[i]; + MGRSCoord fromLocation = MGRSCoord.fromLatLon(input.latitude, input.longitude); + String mgrsString = fromLocation.toString().trim(); + assertEquals(mgrsString, NO_INVERSE_TO_MGRS[i]); + } + } +} \ No newline at end of file