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) {
|
||||
// Project the source naturally; this fixes the zone (and hemisphere) of
|
||||
// the planar frame in which the distance is measured.
|
||||
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.
|
||||
// Bring both points onto WGS84 first so they share one ellipsoid.
|
||||
Coordinate fromWgs84 = CoordinateConverter.transformDatum(from, datum, MapDatum.WGS84);
|
||||
Coordinate toWgs84 = CoordinateConverter.transformDatum(to, datum, MapDatum.WGS84);
|
||||
GridCoordinate toUtm = fromUtm.utmZone() != null
|
||||
? CoordinateConverter.toUtmGrid(toWgs84, MapDatum.WGS84,
|
||||
fromUtm.utmZone(), fromUtm.southernHemisphere())
|
||||
: CoordinateConverter.toGrid(toWgs84, MapDatum.WGS84, MapGrid.UTM);
|
||||
|
||||
// Pick the central meridian half-way between the two longitudes. This
|
||||
// distributes the Transverse Mercator scale distortion symmetrically
|
||||
// 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(),
|
||||
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
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
@@ -82,7 +82,7 @@ public final class CoordinateConverter {
|
||||
return new GridCoordinate(coordinate.longitude(), coordinate.latitude(),
|
||||
coordinate.barometricAltitude(), coordinate.gpsAltitude(), null, null, false);
|
||||
case TRANSVERSE_MERCATOR:
|
||||
return transverseMercatorForward(coordinate, datum, grid, null, false);
|
||||
return transverseMercatorForward(coordinate, datum, grid, null, null, false);
|
||||
case SWISS_OBLIQUE_MERCATOR:
|
||||
return swissObliqueMercatorForward(coordinate, datum, grid);
|
||||
default:
|
||||
@@ -106,7 +106,28 @@ public final class CoordinateConverter {
|
||||
if (zone < 1 || zone > 60) {
|
||||
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,
|
||||
Integer forcedZone, boolean forcedSouthern) {
|
||||
Integer forcedZone, Double forcedCentralMeridianDeg,
|
||||
boolean forcedSouthern) {
|
||||
double a = datum.semiMajorAxis();
|
||||
double f = datum.flattening();
|
||||
double e = datum.firstEccentricity();
|
||||
@@ -300,7 +322,11 @@ public final class CoordinateConverter {
|
||||
boolean southern = false;
|
||||
double centralMeridian;
|
||||
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) {
|
||||
zone = forcedZone;
|
||||
southern = forcedSouthern;
|
||||
|
||||
Reference in New Issue
Block a user