From 91c276349dbe9ededd5e6e8729e95bd9f7bdf46e Mon Sep 17 00:00:00 2001 From: Jan Meinl Date: Fri, 15 May 2026 10:46:48 +0200 Subject: [PATCH] Remove `GeographicCoordinate` class and refactor `CoordinateConverter` to simplify altitude handling and extend functionality. Converted all projections and transformations to use `Coordinate` and `GridCoordinate`. --- .../dev/coph/flightscore/backend/Main.java | 13 + .../backend/map/CoordinateConverter.java | 222 ++++++++---------- .../backend/map/GeographicCoordinate.java | 36 --- .../backend/map/GridCoordinate.java | 36 +-- 4 files changed, 136 insertions(+), 171 deletions(-) delete mode 100644 src/main/java/dev/coph/flightscore/backend/map/GeographicCoordinate.java diff --git a/src/main/java/dev/coph/flightscore/backend/Main.java b/src/main/java/dev/coph/flightscore/backend/Main.java index 3168043..ef839f7 100644 --- a/src/main/java/dev/coph/flightscore/backend/Main.java +++ b/src/main/java/dev/coph/flightscore/backend/Main.java @@ -1,5 +1,11 @@ package dev.coph.flightscore.backend; +import dev.coph.flightscore.backend.coordinate.Altitude; +import dev.coph.flightscore.backend.coordinate.Coordinate; +import dev.coph.flightscore.backend.map.CoordinateConverter; +import dev.coph.flightscore.backend.map.GridCoordinate; +import dev.coph.flightscore.backend.map.MapDatum; +import dev.coph.flightscore.backend.map.MapGrid; import dev.coph.simplelogger.GenericLogger; import dev.coph.simplelogger.LogLevel; @@ -10,6 +16,12 @@ public class Main { GenericLogger.instance().consoleLogLevel(LogLevel.DEBUG); GenericLogger.instance().fileLogLevel(LogLevel.INFO); + Coordinate coordinate = new Coordinate(47.280509, 7.837391, Altitude.fromMeters(400)); + GridCoordinate convertToSwiss = CoordinateConverter.convert(coordinate, MapDatum.WGS84, MapDatum.CH1903, MapGrid.SWISS_GRID_LV03); + GridCoordinate convertToUTMETRS = CoordinateConverter.convert(coordinate, MapDatum.WGS84, MapDatum.ETRS89, MapGrid.UTM); + System.out.println(convertToSwiss); + System.out.println(convertToUTMETRS); + /* Runtime.getRuntime().addShutdownHook(new Thread(() -> { if (backend != null) backend.onDisable(); })); @@ -20,6 +32,7 @@ public class Main { GenericLogger.info("Starting backend..."); backend.onEnable(); GenericLogger.success("Backend started!"); + */ } diff --git a/src/main/java/dev/coph/flightscore/backend/map/CoordinateConverter.java b/src/main/java/dev/coph/flightscore/backend/map/CoordinateConverter.java index de20207..c92e89d 100644 --- a/src/main/java/dev/coph/flightscore/backend/map/CoordinateConverter.java +++ b/src/main/java/dev/coph/flightscore/backend/map/CoordinateConverter.java @@ -1,6 +1,5 @@ package dev.coph.flightscore.backend.map; -import dev.coph.flightscore.backend.competition.AltitudeSource; import dev.coph.flightscore.backend.coordinate.Altitude; import dev.coph.flightscore.backend.coordinate.Coordinate; @@ -8,37 +7,39 @@ import dev.coph.flightscore.backend.coordinate.Coordinate; * High precision conversion utility between any combination of {@link MapDatum} * and {@link MapGrid}. *

- * The conversion of a full {@code (datum, grid)} pair into any other + * The full conversion of a {@code (datum, grid)} pair into any other * {@code (datum, grid)} pair runs through three stages: *

    *
  1. inverse projection of the source grid onto geographic coordinates * (on the source datum's ellipsoid),
  2. *
  3. a geocentric 7-parameter Helmert datum transformation - * (source datum → WGS84 → target datum), carried out in 3D so - * the ellipsoidal height is transformed rigorously,
  4. + * (source datum → WGS84 → target datum), *
  5. forward projection onto the target grid (on the target datum's * ellipsoid).
  6. *
+ *

Altitudes

+ * The {@link Coordinate} class carries two independent altitude readings + * (barometric and GPS). Pure projections ({@link #toGrid} / {@link #toGeographic}) + * leave both untouched. {@link #transformDatum} and the variants of + * {@link #convert} that include a datum change run the Helmert transform once + * per altitude that is actually set; {@code null} altitudes stay {@code null} + * and the height is excluded from the geocentric step (a height of {@code 0} + * is a valid altitude and is transformed normally). *

Accuracy of the formulas

* * The overall accuracy is therefore limited only by the published - * {@link MapDatum} Helmert parameters (roughly metre level for the mean - * parameter sets stored in the enum). + * {@link MapDatum} Helmert parameters. */ public final class CoordinateConverter { private static final double ARCSEC_TO_RAD = Math.PI / (180.0 * 3600.0); - /** - * Iteration tolerance for the Newton/fixed-point solvers (radians). - */ private static final double TOLERANCE = 1.0e-14; private static final int MAX_ITERATIONS = 100; @@ -52,74 +53,38 @@ public final class CoordinateConverter { /** * Converts a grid coordinate from one {@code (datum, grid)} combination into * any other {@code (datum, grid)} combination. - * - * @param source the coordinate in the source grid - * @param sourceDatum datum the source coordinate is referenced to - * @param sourceGrid grid the source coordinate is projected with - * @param targetDatum datum the result shall be referenced to - * @param targetGrid grid the result shall be projected with - * @return the coordinate expressed in the target {@code (datum, grid)} combination */ public static GridCoordinate convert(GridCoordinate source, MapDatum sourceDatum, MapGrid sourceGrid, MapDatum targetDatum, MapGrid targetGrid) { - GeographicCoordinate sourceGeographic = toGeographic(source, sourceDatum, sourceGrid); - GeographicCoordinate targetGeographic = transformDatum(sourceGeographic, sourceDatum, targetDatum); - return toGrid(targetGeographic, targetDatum, targetGrid); + Coordinate geographic = toGeographic(source, sourceDatum, sourceGrid); + Coordinate shifted = transformDatum(geographic, sourceDatum, targetDatum); + return toGrid(shifted, targetDatum, targetGrid); } /** - * Converts a geographic coordinate from its datum onto any target - * {@code (datum, grid)} combination. This is the natural entry point when - * the input is plain longitude/latitude (e.g. a GPS track point) and avoids - * having to wrap it into a {@link MapGrid#LAT_LONG} {@link GridCoordinate}. + * Converts a geographic {@link Coordinate} from its datum onto any target + * {@code (datum, grid)} combination. Natural entry point for plain GPS + * track points (WGS84 lon/lat). */ - public static GridCoordinate convert(GeographicCoordinate source, MapDatum sourceDatum, + public static GridCoordinate convert(Coordinate source, MapDatum sourceDatum, MapDatum targetDatum, MapGrid targetGrid) { return toGrid(transformDatum(source, sourceDatum, targetDatum), targetDatum, targetGrid); } /** * Projects a {@link Coordinate} (referenced to {@code datum}) onto {@code grid}. - *

- * The {@link Coordinate#gpsAltitude() GPS altitude} is taken as the ellipsoidal - * height since it is the GNSS-derived height and therefore the closest match; - * the barometric altitude is used as a fallback and {@code 0} if neither is set. + * Both altitudes are carried through unchanged. */ - public static GridCoordinate toGrid(Coordinate coordinate, AltitudeSource altitudeSource, MapDatum datum, MapGrid grid) { - return toGrid(toGeographicCoordinate(coordinate, altitudeSource), datum, grid); - } - - /** - * Inverse projection of a grid coordinate onto a {@link Coordinate}. The - * resulting ellipsoidal height is stored as the coordinate's altitude. - */ - public static Coordinate toCoordinate(GridCoordinate grid, MapDatum datum, MapGrid mapGrid) { - GeographicCoordinate geographic = toGeographic(grid, datum, mapGrid); - return new Coordinate(geographic.latitude(), geographic.longitude(), - Altitude.fromMeters(geographic.ellipsoidalHeight())); - } - - private static GeographicCoordinate toGeographicCoordinate(Coordinate coordinate, AltitudeSource source) { - Altitude altitude = switch (source) { - case GPS -> coordinate.gpsAltitude(); - case BAROMETRIC -> coordinate.barometricAltitude(); - }; - double height = altitude != null ? altitude.meters() : 0.0; - return new GeographicCoordinate(coordinate.latitude(), coordinate.longitude(), height); - } - - /** - * Projects a geographic coordinate (referenced to {@code datum}) onto {@code grid}. - */ - public static GridCoordinate toGrid(GeographicCoordinate geographic, MapDatum datum, MapGrid grid) { + public static GridCoordinate toGrid(Coordinate coordinate, MapDatum datum, MapGrid grid) { switch (grid.projectionMethod()) { case NONE: - return new GridCoordinate(geographic.longitude(), geographic.latitude(), geographic.ellipsoidalHeight()); + return new GridCoordinate(coordinate.longitude(), coordinate.latitude(), + coordinate.barometricAltitude(), coordinate.gpsAltitude(), null, false); case TRANSVERSE_MERCATOR: - return transverseMercatorForward(geographic, datum, grid); + return transverseMercatorForward(coordinate, datum, grid); case SWISS_OBLIQUE_MERCATOR: - return swissObliqueMercatorForward(geographic, datum, grid); + return swissObliqueMercatorForward(coordinate, datum, grid); default: throw new IllegalArgumentException("Unsupported projection method: " + grid.projectionMethod()); } @@ -127,12 +92,14 @@ public final class CoordinateConverter { /** * Inverse projection of a grid coordinate (projected with {@code grid}, - * referenced to {@code datum}) onto geographic coordinates. + * referenced to {@code datum}) onto a geographic {@link Coordinate}. Both + * altitudes are carried through unchanged. */ - public static GeographicCoordinate toGeographic(GridCoordinate grid, MapDatum datum, MapGrid mapGrid) { + public static Coordinate toGeographic(GridCoordinate grid, MapDatum datum, MapGrid mapGrid) { switch (mapGrid.projectionMethod()) { case NONE: - return new GeographicCoordinate(grid.northing(), grid.easting(), grid.ellipsoidalHeight()); + return new Coordinate(grid.northing(), grid.easting(), + grid.barometricAltitude(), grid.gpsAltitude()); case TRANSVERSE_MERCATOR: return transverseMercatorInverse(grid, datum, mapGrid); case SWISS_OBLIQUE_MERCATOR: @@ -143,41 +110,73 @@ public final class CoordinateConverter { } /** - * Transforms a geographic coordinate from one datum to another using a - * geocentric 7-parameter Helmert transformation via WGS84. + * Transforms a {@link Coordinate} from one datum to another using a + * geocentric 7-parameter Helmert transformation via WGS84. Each set + * altitude is transformed independently; {@code null} altitudes remain + * {@code null}. */ - public static GeographicCoordinate transformDatum(GeographicCoordinate geographic, MapDatum from, MapDatum to) { + public static Coordinate transformDatum(Coordinate coordinate, MapDatum from, MapDatum to) { if (from == to) { - return geographic; + return coordinate; } - double[] geocentric = geographicToGeocentric(geographic, from); - geocentric = helmertToWgs84(geocentric, from); - geocentric = helmertFromWgs84(geocentric, to); - return geocentricToGeographic(geocentric, to); + Altitude baro = coordinate.barometricAltitude(); + Altitude gps = coordinate.gpsAltitude(); + + // Use any present altitude to compute the (essentially h-independent) horizontal result. + double referenceHeight = gps != null ? gps.meters() : (baro != null ? baro.meters() : 0.0); + double[] reference = transformGeographicPosition(coordinate.latitude(), coordinate.longitude(), + referenceHeight, from, to); + + Altitude newBaro = null; + Altitude newGps = null; + if (gps != null && baro != null && gps != baro) { + // Both set, with different values: run the second transform for the other altitude. + newGps = Altitude.fromMeters(reference[2]); + double[] secondary = transformGeographicPosition(coordinate.latitude(), coordinate.longitude(), + baro.meters(), from, to); + newBaro = Altitude.fromMeters(secondary[2]); + } else if (gps != null) { + newGps = Altitude.fromMeters(reference[2]); + if (baro != null) { + newBaro = newGps; // shared instance, both pointed to the same value originally + } + } else if (baro != null) { + newBaro = Altitude.fromMeters(reference[2]); + } + return new Coordinate(reference[0], reference[1], newBaro, newGps); } // ------------------------------------------------------------------ // Datum transformation (geocentric Helmert) // ------------------------------------------------------------------ - private static double[] geographicToGeocentric(GeographicCoordinate geographic, MapDatum datum) { + /** Returns {@code {latDeg, lonDeg, height}}. */ + private static double[] transformGeographicPosition(double latDeg, double lonDeg, double height, + MapDatum from, MapDatum to) { + double[] xyz = geographicToGeocentric(latDeg, lonDeg, height, from); + xyz = helmertToWgs84(xyz, from); + xyz = helmertFromWgs84(xyz, to); + return geocentricToGeographic(xyz, to); + } + + private static double[] geographicToGeocentric(double latDeg, double lonDeg, double height, MapDatum datum) { double a = datum.semiMajorAxis(); double e2 = datum.firstEccentricitySquared(); - double phi = Math.toRadians(geographic.latitude()); - double lambda = Math.toRadians(geographic.longitude()); - double h = geographic.ellipsoidalHeight(); + double phi = Math.toRadians(latDeg); + double lambda = Math.toRadians(lonDeg); double sinPhi = Math.sin(phi); double cosPhi = Math.cos(phi); double primeVerticalRadius = a / Math.sqrt(1.0 - e2 * sinPhi * sinPhi); - double x = (primeVerticalRadius + h) * cosPhi * Math.cos(lambda); - double y = (primeVerticalRadius + h) * cosPhi * Math.sin(lambda); - double z = (primeVerticalRadius * (1.0 - e2) + h) * sinPhi; + double x = (primeVerticalRadius + height) * cosPhi * Math.cos(lambda); + double y = (primeVerticalRadius + height) * cosPhi * Math.sin(lambda); + double z = (primeVerticalRadius * (1.0 - e2) + height) * sinPhi; return new double[]{x, y, z}; } - private static GeographicCoordinate geocentricToGeographic(double[] xyz, MapDatum datum) { + /** Returns {@code {latDeg, lonDeg, height}}. */ + private static double[] geocentricToGeographic(double[] xyz, MapDatum datum) { double a = datum.semiMajorAxis(); double e2 = datum.firstEccentricitySquared(); double x = xyz[0]; @@ -187,11 +186,11 @@ public final class CoordinateConverter { double lambda = Math.atan2(y, x); double p = Math.hypot(x, y); - if (p < 1.0e-11) { // on or extremely close to the polar axis + if (p < 1.0e-11) { double phiPole = z >= 0.0 ? Math.PI / 2.0 : -Math.PI / 2.0; double semiMinor = a * Math.sqrt(1.0 - e2); double height = Math.abs(z) - semiMinor; - return new GeographicCoordinate(Math.toDegrees(phiPole), Math.toDegrees(lambda), height); + return new double[]{Math.toDegrees(phiPole), Math.toDegrees(lambda), height}; } double phi = Math.atan2(z, p * (1.0 - e2)); @@ -207,7 +206,7 @@ public final class CoordinateConverter { } phi = phiNext; } - return new GeographicCoordinate(Math.toDegrees(phi), Math.toDegrees(lambda), height); + return new double[]{Math.toDegrees(phi), Math.toDegrees(lambda), height}; } private static double[] helmertToWgs84(double[] xyz, MapDatum datum) { @@ -234,10 +233,6 @@ public final class CoordinateConverter { }; } - /** - * Builds the {@code (1 + s) * R} matrix of the Helmert transformation - * (Position Vector rotation convention). - */ private static double[][] helmertMatrix(MapDatum datum) { double scale = 1.0 + datum.scaleDifference() * 1.0e-6; double rx = datum.rotationX() * ARCSEC_TO_RAD; @@ -273,21 +268,21 @@ public final class CoordinateConverter { // Transverse Mercator (Krueger series, Karney's algorithm) // ------------------------------------------------------------------ - private static GridCoordinate transverseMercatorForward(GeographicCoordinate geographic, MapDatum datum, MapGrid grid) { + private static GridCoordinate transverseMercatorForward(Coordinate coordinate, MapDatum datum, MapGrid grid) { double a = datum.semiMajorAxis(); double f = datum.flattening(); double e = datum.firstEccentricity(); double n = f / (2.0 - f); - double phi = Math.toRadians(geographic.latitude()); + double phi = Math.toRadians(coordinate.latitude()); Integer zone = null; boolean southern = false; double centralMeridian; double falseNorthing = grid.falseNorthing(); if (grid.isZoneDependent()) { - zone = utmZone(geographic.longitude()); - southern = geographic.latitude() < 0.0; + zone = utmZone(coordinate.longitude()); + southern = coordinate.latitude() < 0.0; centralMeridian = Math.toRadians(zone * 6.0 - 183.0); falseNorthing = southern ? 10_000_000.0 : 0.0; } else { @@ -295,22 +290,20 @@ public final class CoordinateConverter { } double k0 = grid.scaleFactor(); double falseEasting = grid.falseEasting(); - double deltaLambda = Math.toRadians(geographic.longitude()) - centralMeridian; + double deltaLambda = Math.toRadians(coordinate.longitude()) - centralMeridian; double rectifyingRadius = rectifyingRadius(a, n); double[] alpha = kruegerAlpha(n); double xiOrigin = tmForwardSeries(Math.toRadians(grid.latitudeOfOrigin()), 0.0, e, alpha)[0]; double[] series = tmForwardSeries(phi, deltaLambda, e, alpha); - double xi = series[0]; - double eta = series[1]; - - double easting = falseEasting + k0 * rectifyingRadius * eta; - double northing = falseNorthing + k0 * rectifyingRadius * (xi - xiOrigin); - return new GridCoordinate(easting, northing, geographic.ellipsoidalHeight(), zone, southern); + double easting = falseEasting + k0 * rectifyingRadius * series[1]; + double northing = falseNorthing + k0 * rectifyingRadius * (series[0] - xiOrigin); + return new GridCoordinate(easting, northing, + coordinate.barometricAltitude(), coordinate.gpsAltitude(), zone, southern); } - private static GeographicCoordinate transverseMercatorInverse(GridCoordinate grid, MapDatum datum, MapGrid mapGrid) { + private static Coordinate transverseMercatorInverse(GridCoordinate grid, MapDatum datum, MapGrid mapGrid) { double a = datum.semiMajorAxis(); double f = datum.flattening(); double e = datum.firstEccentricity(); @@ -351,13 +344,10 @@ public final class CoordinateConverter { double phi = Math.atan(tau); double lambda = centralMeridian + Math.atan2(Math.sinh(etaPrime), Math.cos(xiPrime)); - return new GeographicCoordinate(Math.toDegrees(phi), Math.toDegrees(lambda), grid.ellipsoidalHeight()); + return new Coordinate(Math.toDegrees(phi), Math.toDegrees(lambda), + grid.barometricAltitude(), grid.gpsAltitude()); } - /** - * Evaluates the Krueger forward series and returns {@code {xi, eta}} for a - * given geodetic latitude and longitude offset from the central meridian. - */ private static double[] tmForwardSeries(double phi, double deltaLambda, double e, double[] alpha) { double tau = Math.tan(phi); double sigma = Math.sinh(e * atanh(e * tau / Math.sqrt(1.0 + tau * tau))); @@ -375,10 +365,6 @@ public final class CoordinateConverter { return new double[]{xi, eta}; } - /** - * Recovers the geodetic latitude tangent {@code tan(phi)} from the conformal - * latitude tangent using Karney's Newton iteration. - */ private static double solveGeodeticTangent(double conformalTangent, double e, double e2) { double tau = conformalTangent; for (int i = 0; i < MAX_ITERATIONS; i++) { @@ -442,11 +428,11 @@ public final class CoordinateConverter { // Swiss oblique Mercator (rigorous swisstopo algorithm) // ------------------------------------------------------------------ - private static GridCoordinate swissObliqueMercatorForward(GeographicCoordinate geographic, MapDatum datum, MapGrid grid) { + private static GridCoordinate swissObliqueMercatorForward(Coordinate coordinate, MapDatum datum, MapGrid grid) { SwissConstants c = new SwissConstants(datum, grid); - double phi = Math.toRadians(geographic.latitude()); - double lambda = Math.toRadians(geographic.longitude()); + double phi = Math.toRadians(coordinate.latitude()); + double lambda = Math.toRadians(coordinate.longitude()); double s = c.alpha * Math.log(Math.tan(Math.PI / 4.0 + phi / 2.0)) - c.alpha * c.e / 2.0 * Math.log((1.0 + c.e * Math.sin(phi)) / (1.0 - c.e * Math.sin(phi))) @@ -462,10 +448,11 @@ public final class CoordinateConverter { double easting = c.r * pseudoLongitude + grid.falseEasting(); double northing = c.r * atanh(sinPseudoLatitude) + grid.falseNorthing(); - return new GridCoordinate(easting, northing, geographic.ellipsoidalHeight()); + return new GridCoordinate(easting, northing, + coordinate.barometricAltitude(), coordinate.gpsAltitude(), null, false); } - private static GeographicCoordinate swissObliqueMercatorInverse(GridCoordinate grid, MapDatum datum, MapGrid mapGrid) { + private static Coordinate swissObliqueMercatorInverse(GridCoordinate grid, MapDatum datum, MapGrid mapGrid) { SwissConstants c = new SwissConstants(datum, mapGrid); double pseudoLongitude = (grid.easting() - mapGrid.falseEasting()) / c.r; @@ -490,15 +477,10 @@ public final class CoordinateConverter { } phi = phiNext; } - return new GeographicCoordinate(Math.toDegrees(phi), Math.toDegrees(lambda), grid.ellipsoidalHeight()); + return new Coordinate(Math.toDegrees(phi), Math.toDegrees(lambda), + grid.barometricAltitude(), grid.gpsAltitude()); } - /** - * Derived constants of the Swiss oblique Mercator projection (projection - * sphere radius, sphere distortion factor, fundamental point on the sphere - * and the integration constant), computed from the datum's ellipsoid and - * the grid's fundamental point. - */ private static final class SwissConstants { private final double e; private final double r; diff --git a/src/main/java/dev/coph/flightscore/backend/map/GeographicCoordinate.java b/src/main/java/dev/coph/flightscore/backend/map/GeographicCoordinate.java deleted file mode 100644 index 032c7b0..0000000 --- a/src/main/java/dev/coph/flightscore/backend/map/GeographicCoordinate.java +++ /dev/null @@ -1,36 +0,0 @@ -package dev.coph.flightscore.backend.map; - -import lombok.Getter; -import lombok.experimental.Accessors; - -/** - * A geographic coordinate (longitude / latitude) with an ellipsoidal height, - * expressed on a specific {@link MapDatum}. - */ -@Getter -@Accessors(fluent = true) -public class GeographicCoordinate { - - /** Latitude in decimal degrees, positive north. */ - private final double latitude; - /** Longitude in decimal degrees, positive east. */ - private final double longitude; - /** Height above the reference ellipsoid in metres. */ - private final double ellipsoidalHeight; - - public GeographicCoordinate(double latitude, double longitude, double ellipsoidalHeight) { - this.latitude = latitude; - this.longitude = longitude; - this.ellipsoidalHeight = ellipsoidalHeight; - } - - public GeographicCoordinate(double latitude, double longitude) { - this(latitude, longitude, 0.0); - } - - @Override - public String toString() { - return "GeographicCoordinate{latitude=" + latitude + ", longitude=" + longitude - + ", ellipsoidalHeight=" + ellipsoidalHeight + '}'; - } -} diff --git a/src/main/java/dev/coph/flightscore/backend/map/GridCoordinate.java b/src/main/java/dev/coph/flightscore/backend/map/GridCoordinate.java index 24089a9..3a35bb5 100644 --- a/src/main/java/dev/coph/flightscore/backend/map/GridCoordinate.java +++ b/src/main/java/dev/coph/flightscore/backend/map/GridCoordinate.java @@ -1,10 +1,12 @@ package dev.coph.flightscore.backend.map; +import dev.coph.flightscore.backend.coordinate.Altitude; import lombok.Getter; import lombok.experimental.Accessors; /** - * A projected grid coordinate (easting / northing) with an ellipsoidal height. + * A projected grid coordinate (easting / northing) together with the same pair + * of altitudes as {@link dev.coph.flightscore.backend.coordinate.Coordinate}. *

* For zone dependent grids (UTM) {@link #utmZone()} and {@link #southernHemisphere()} * carry the information that cannot be recovered from easting/northing alone. @@ -22,40 +24,44 @@ public class GridCoordinate { private final double easting; /** Northing in metres (or latitude in degrees for {@link MapGrid#LAT_LONG}). */ private final double northing; - /** Height above the reference ellipsoid in metres. */ - private final double ellipsoidalHeight; + /** Barometric altitude, or {@code null} if not set. */ + private final Altitude barometricAltitude; + /** GPS / GNSS altitude, or {@code null} if not set. */ + private final Altitude gpsAltitude; /** UTM zone number (1..60) for zone dependent grids, otherwise {@code null}. */ private final Integer utmZone; /** Whether the coordinate lies on the southern hemisphere (relevant for UTM false northing). */ private final boolean southernHemisphere; - public GridCoordinate(double easting, double northing, double ellipsoidalHeight, + public GridCoordinate(double easting, double northing, + Altitude barometricAltitude, Altitude gpsAltitude, Integer utmZone, boolean southernHemisphere) { this.easting = easting; this.northing = northing; - this.ellipsoidalHeight = ellipsoidalHeight; + this.barometricAltitude = barometricAltitude; + this.gpsAltitude = gpsAltitude; this.utmZone = utmZone; this.southernHemisphere = southernHemisphere; } - public GridCoordinate(double easting, double northing, double ellipsoidalHeight) { - this(easting, northing, ellipsoidalHeight, null, false); + public GridCoordinate(double easting, double northing, + Altitude barometricAltitude, Altitude gpsAltitude) { + this(easting, northing, barometricAltitude, gpsAltitude, null, false); + } + + public GridCoordinate(double easting, double northing, Altitude altitude) { + this(easting, northing, altitude, altitude, null, false); } public GridCoordinate(double easting, double northing) { - this(easting, northing, 0.0, null, false); - } - - /** UTM grid coordinate carrying its zone and hemisphere. */ - public static GridCoordinate utm(double easting, double northing, double ellipsoidalHeight, - int zone, boolean southernHemisphere) { - return new GridCoordinate(easting, northing, ellipsoidalHeight, zone, southernHemisphere); + this(easting, northing, null, null, null, false); } @Override public String toString() { return "GridCoordinate{easting=" + easting + ", northing=" + northing - + ", ellipsoidalHeight=" + ellipsoidalHeight + + ", barometricAltitude=" + barometricAltitude + + ", gpsAltitude=" + gpsAltitude + (utmZone != null ? ", utmZone=" + utmZone + ", southernHemisphere=" + southernHemisphere : "") + '}'; }