Enhance CoordinateConverter with custom central meridian support and refine UTM distance calculations
- Added `toUtmGridAtMeridian` for flexible UTM projections with an arbitrary central meridian. - Updated `DistanceCalculator` to use midpoint-based central meridian for accurate planar distance computations. - Introduced longitude normalization utilities to improve handling of antimeridian crossings.
This commit is contained in:
+27
-11
@@ -194,23 +194,39 @@ public final class DistanceCalculator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static double utmEuclidean(Coordinate from, Coordinate to, MapDatum datum) {
|
private static double utmEuclidean(Coordinate from, Coordinate to, MapDatum datum) {
|
||||||
// Project the source naturally; this fixes the zone (and hemisphere) of
|
// Bring both points onto WGS84 first so they share one ellipsoid.
|
||||||
// the planar frame in which the distance is measured.
|
Coordinate fromWgs84 = CoordinateConverter.transformDatum(from, datum, MapDatum.WGS84);
|
||||||
GridCoordinate fromUtm = CoordinateConverter.convert(from, datum, MapDatum.WGS84, MapGrid.UTM);
|
|
||||||
|
|
||||||
// Project the target into the same zone / hemisphere as the source, so
|
|
||||||
// both points share one central meridian and the wedge-shaped overlap
|
|
||||||
// between zones causes no jump in the planar coordinates.
|
|
||||||
Coordinate toWgs84 = CoordinateConverter.transformDatum(to, datum, MapDatum.WGS84);
|
Coordinate toWgs84 = CoordinateConverter.transformDatum(to, datum, MapDatum.WGS84);
|
||||||
GridCoordinate toUtm = fromUtm.utmZone() != null
|
|
||||||
? CoordinateConverter.toUtmGrid(toWgs84, MapDatum.WGS84,
|
// Pick the central meridian half-way between the two longitudes. This
|
||||||
fromUtm.utmZone(), fromUtm.southernHemisphere())
|
// distributes the Transverse Mercator scale distortion symmetrically
|
||||||
: CoordinateConverter.toGrid(toWgs84, MapDatum.WGS84, MapGrid.UTM);
|
// and roughly halves the maximum planar error compared to using either
|
||||||
|
// point's natural UTM zone — important when the two points are far
|
||||||
|
// apart in longitude or straddle a zone boundary ("Zwickel"). The
|
||||||
|
// shortest longitude arc is used so an antimeridian crossing is fine.
|
||||||
|
double dLon = normalizeLongitudeDelta(toWgs84.longitude() - fromWgs84.longitude());
|
||||||
|
double midLon = normalizeLongitude(fromWgs84.longitude() + dLon / 2.0);
|
||||||
|
boolean southern = (fromWgs84.latitude() + toWgs84.latitude()) / 2.0 < 0.0;
|
||||||
|
|
||||||
|
GridCoordinate fromUtm = CoordinateConverter.toUtmGridAtMeridian(fromWgs84, MapDatum.WGS84, midLon, southern);
|
||||||
|
GridCoordinate toUtm = CoordinateConverter.toUtmGridAtMeridian(toWgs84, MapDatum.WGS84, midLon, southern);
|
||||||
|
|
||||||
return Math.hypot(toUtm.easting() - fromUtm.easting(),
|
return Math.hypot(toUtm.easting() - fromUtm.easting(),
|
||||||
toUtm.northing() - fromUtm.northing());
|
toUtm.northing() - fromUtm.northing());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static double normalizeLongitudeDelta(double dLon) {
|
||||||
|
while (dLon > 180.0) dLon -= 360.0;
|
||||||
|
while (dLon < -180.0) dLon += 360.0;
|
||||||
|
return dLon;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static double normalizeLongitude(double lon) {
|
||||||
|
while (lon > 180.0) lon -= 360.0;
|
||||||
|
while (lon <= -180.0) lon += 360.0;
|
||||||
|
return lon;
|
||||||
|
}
|
||||||
|
|
||||||
// ------------------------------------------------------------------
|
// ------------------------------------------------------------------
|
||||||
// Result assembly
|
// Result assembly
|
||||||
// ------------------------------------------------------------------
|
// ------------------------------------------------------------------
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ public final class CoordinateConverter {
|
|||||||
return new GridCoordinate(coordinate.longitude(), coordinate.latitude(),
|
return new GridCoordinate(coordinate.longitude(), coordinate.latitude(),
|
||||||
coordinate.barometricAltitude(), coordinate.gpsAltitude(), null, null, false);
|
coordinate.barometricAltitude(), coordinate.gpsAltitude(), null, null, false);
|
||||||
case TRANSVERSE_MERCATOR:
|
case TRANSVERSE_MERCATOR:
|
||||||
return transverseMercatorForward(coordinate, datum, grid, null, false);
|
return transverseMercatorForward(coordinate, datum, grid, null, null, false);
|
||||||
case SWISS_OBLIQUE_MERCATOR:
|
case SWISS_OBLIQUE_MERCATOR:
|
||||||
return swissObliqueMercatorForward(coordinate, datum, grid);
|
return swissObliqueMercatorForward(coordinate, datum, grid);
|
||||||
default:
|
default:
|
||||||
@@ -106,7 +106,28 @@ public final class CoordinateConverter {
|
|||||||
if (zone < 1 || zone > 60) {
|
if (zone < 1 || zone > 60) {
|
||||||
throw new IllegalArgumentException("UTM zone must be in [1, 60], was " + zone);
|
throw new IllegalArgumentException("UTM zone must be in [1, 60], was " + zone);
|
||||||
}
|
}
|
||||||
return transverseMercatorForward(coordinate, datum, MapGrid.UTM, zone, southern);
|
return transverseMercatorForward(coordinate, datum, MapGrid.UTM, zone, null, southern);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Projects a {@link Coordinate} (referenced to {@code datum}) onto a
|
||||||
|
* UTM-style Transverse Mercator grid with an arbitrary central meridian
|
||||||
|
* (in decimal degrees). UTM's scale factor (0.9996), false easting
|
||||||
|
* (500 000 m) and hemisphere-dependent false northing are reused;
|
||||||
|
* only the central meridian is overridden.
|
||||||
|
* <p>
|
||||||
|
* This is the primary tool for an accurate planar distance between two
|
||||||
|
* points that are far apart in longitude: picking the mid-longitude as the
|
||||||
|
* central meridian distributes the Transverse Mercator scale distortion
|
||||||
|
* symmetrically and roughly halves the maximum error.
|
||||||
|
* <p>
|
||||||
|
* The returned {@link GridCoordinate} carries no UTM zone number (since
|
||||||
|
* the projection is not a standard UTM zone) but reports the hemisphere
|
||||||
|
* as requested.
|
||||||
|
*/
|
||||||
|
public static GridCoordinate toUtmGridAtMeridian(Coordinate coordinate, MapDatum datum,
|
||||||
|
double centralMeridianDeg, boolean southern) {
|
||||||
|
return transverseMercatorForward(coordinate, datum, MapGrid.UTM, null, centralMeridianDeg, southern);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -288,7 +309,8 @@ public final class CoordinateConverter {
|
|||||||
// ------------------------------------------------------------------
|
// ------------------------------------------------------------------
|
||||||
|
|
||||||
private static GridCoordinate transverseMercatorForward(Coordinate coordinate, MapDatum datum, MapGrid grid,
|
private static GridCoordinate transverseMercatorForward(Coordinate coordinate, MapDatum datum, MapGrid grid,
|
||||||
Integer forcedZone, boolean forcedSouthern) {
|
Integer forcedZone, Double forcedCentralMeridianDeg,
|
||||||
|
boolean forcedSouthern) {
|
||||||
double a = datum.semiMajorAxis();
|
double a = datum.semiMajorAxis();
|
||||||
double f = datum.flattening();
|
double f = datum.flattening();
|
||||||
double e = datum.firstEccentricity();
|
double e = datum.firstEccentricity();
|
||||||
@@ -300,7 +322,11 @@ public final class CoordinateConverter {
|
|||||||
boolean southern = false;
|
boolean southern = false;
|
||||||
double centralMeridian;
|
double centralMeridian;
|
||||||
double falseNorthing = grid.falseNorthing();
|
double falseNorthing = grid.falseNorthing();
|
||||||
if (grid.isZoneDependent()) {
|
if (forcedCentralMeridianDeg != null) {
|
||||||
|
southern = forcedSouthern;
|
||||||
|
centralMeridian = Math.toRadians(forcedCentralMeridianDeg);
|
||||||
|
falseNorthing = southern ? 10_000_000.0 : 0.0;
|
||||||
|
} else if (grid.isZoneDependent()) {
|
||||||
if (forcedZone != null) {
|
if (forcedZone != null) {
|
||||||
zone = forcedZone;
|
zone = forcedZone;
|
||||||
southern = forcedSouthern;
|
southern = forcedSouthern;
|
||||||
|
|||||||
Reference in New Issue
Block a user