Remove GeographicCoordinate class and refactor CoordinateConverter to simplify altitude handling and extend functionality. Converted all projections and transformations to use Coordinate and GridCoordinate.

This commit is contained in:
Jan Meinl
2026-05-15 10:46:48 +02:00
parent abadc40ed6
commit 91c276349d
4 changed files with 136 additions and 171 deletions
@@ -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!");
*/
}
@@ -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}.
* <p>
* 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:
* <ol>
* <li>inverse projection of the source grid onto geographic coordinates
* (on the source datum's ellipsoid),</li>
* <li>a geocentric 7-parameter Helmert datum transformation
* (source datum &rarr; WGS84 &rarr; target datum), carried out in 3D so
* the ellipsoidal height is transformed rigorously,</li>
* (source datum &rarr; WGS84 &rarr; target datum),</li>
* <li>forward projection onto the target grid (on the target datum's
* ellipsoid).</li>
* </ol>
* <h2>Altitudes</h2>
* 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}
* <i>is</i> a valid altitude and is transformed normally).
* <h2>Accuracy of the formulas</h2>
* <ul>
* <li>Transverse Mercator is evaluated with the Kr&uuml;ger series to order
* {@code n^6} (Karney's algorithm), which is accurate to well below a
* millimetre.</li>
* {@code n^6} (Karney's algorithm).</li>
* <li>The Swiss grid uses the rigorous swisstopo algorithm (ellipsoid
* &rarr; sphere &rarr; oblique cylinder), accurate to a few millimetres.</li>
* &rarr; sphere &rarr; oblique cylinder).</li>
* <li>Geographic &harr; geocentric conversions are exact (forward) and
* iterated to machine precision (inverse).</li>
* </ul>
* 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}.
* <p>
* 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;
@@ -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 + '}';
}
}
@@ -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}.
* <p>
* 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 : "")
+ '}';
}