-
Notifications
You must be signed in to change notification settings - Fork 414
Introduce EPSG3857Utils for accurate Web Mercator handling #3827
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Conversation
|
Shouldn't we provide a generic solution? |
|
I don't quite understand what you're referring to with a "generic solution". The problem appears to be exclusively with the EPSG:3857 projection, or at least that's the one with which we have detected it. The reason for the specific check for EPSG:3857 is due to the nature of this projection and how distances are calculated in it:
The distinction between Geodetic and Planar projections was already being made in some methods, such as the |
Finally, please add tests ;-) and rebase your code to clean the history. |
|
Allow me to try to explain the differences between the 3 cases to get to the point of why the Pseudo-Mercator projection is a special case:
I fully understand the concern about changing the default behavior and trying to find a generic solution that doesn't depend on a specific However, we would like to insist that the current handling of Pseudo-Mercator projections is not a simple inaccuracy, but a fundamental error in calculation. The result it produces for maps far from the equator (such as Greenland) is not a matter of preference, but is objectively incorrect and can lead to the creation of maps with completely misleading scales. We are aware that the calculations we propose do not fix the problem, but they do mitigate it significantly. For maps that are not very wide in latitude, the mitigation can even solve it, whereas for maps that are wide in latitude, differences in measurements at the edges will still be observed when compared to the scale bar (since the measurements would only be correct at the center of the map, and they lose precision as they move north or south). The problem is tied to the Pseudo-Mercator projection with the "WGS 84" datum, but right now I only know of one non-deprecated CRS associated with it, "EPSG:3857". In fact, in the I also understand that introducing the change to improve measurement accuracy on the map when using that specific CRS may cause users who are currently generating maps to suddenly get different maps, and this could cause them some problems. I imagine that's why the suggestion was made to introduce something like We are going to spend some time trying to introduce the |
|
It should be in the Can you also clarify the @sebr72 point 4 (differences between cases 1 and 3 except for the additional reprojection)? |
|
Regarding the clarification of point 4, about the "differences between cases 1 and 3 except for the additional reprojection," we have been reviewing the code that we have introduced in:
We will discuss each one. BBoxMapBounds, getScale methodWe proposed something like: ...
if (projUnit == DistanceUnit.DEGREES) {
GeodeticCalculator calculator = new GeodeticCalculator(getProjection());
final double centerY = bboxAdjustedToScreen.centre().y;
calculator.setStartingGeographicPoint(bboxAdjustedToScreen.getMinX(), centerY);
calculator.setDestinationGeographicPoint(bboxAdjustedToScreen.getMaxX(), centerY);
DistanceUnit.fromString(calculator.getEllipsoid().getAxisUnit().toString());
geoWidthInInches = ellipsoidUnit.convertTo(geoWidthInEllipsoidUnits, DistanceUnit.IN);
} else if (EPSG3857Utils.is3857(crs)){
geoWidthInInches = EPSG3857Utils.computeOrthodromicWidthInInches(bboxAdjustedToScreen);
...Reviewing it more carefully, the code in private double computeGeodeticWidthInInches(final ReferencedEnvelope bbox) {
try {
CoordinateReferenceSystem crs = bbox.getCoordinateReferenceSystem();
GeodeticCalculator calculator = new GeodeticCalculator(crs); // Use the original CRS for the ellipsoid
Coordinate startDegrees;
Coordinate endDegrees;
if (DistanceUnit.fromProjection(crs) == DistanceUnit.DEGREES) {
// Already in degrees, use directly
startDegrees = new Coordinate(bbox.getMinX(), bbox.centre().y);
endDegrees = new Coordinate(bbox.getMaxX(), bbox.centre().y);
} else { // Assuming it is is3857() from the if statement above
// Needs reprojection
final MathTransform transform = CRS.findMathTransform(crs, GenericMapAttribute.parseProjection("EPSG:4326", true));
startDegrees = JTS.transform(new Coordinate(bbox.getMinX(), bbox.centre().y), null, transform);
endDegrees = JTS.transform(new Coordinate(bbox.getMaxX(), bbox.centre().y), null, transform);
}
// --- Common Logic ---
calculator.setStartingGeographicPoint(startDegrees.x, startDegrees.y);
calculator.setDestinationGeographicPoint(endDegrees.x, endDegrees.y);
final double orthodromicWidth = calculator.getOrthodromicDistance();
final DistanceUnit ellipsoidUnit = DistanceUnit.fromString(calculator.getEllipsoid().getAxisUnit().toString());
return ellipsoidUnit.convertTo(orthodromicWidth, DistanceUnit.IN);
} catch (Exception e) {
throw new PrintException("Failed to compute geodetic width", e);
}
}And leaving in public Scale getScale(final Rectangle paintArea, final double dpi) {
final ReferencedEnvelope bboxAdjustedToScreen = toReferencedEnvelope(paintArea);
DistanceUnit projUnit = DistanceUnit.fromProjection(getProjection());
double geoWidthInInches;
CoordinateReferenceSystem crs = getProjection();
// If it is geodetic OR it is a special case requiring geodetic calculation
if (projUnit == DistanceUnit.DEGREES || EPSG3857Utils.is3857(crs)) {
geoWidthInInches = computeGeodeticWidthInInches(bboxAdjustedToScreen);
} else {
// Original logic for planar projections
geoWidthInInches = projUnit.convertTo(bboxAdjustedToScreen.getWidth(), DistanceUnit.IN);
}
// ... rest of the getScale method ...
}This way, it would be integrated into the Constructor of "WMTSLayer.WMTSTileCacheInfo"In this constructor, an "extra" scale factor is simply being calculated to apply to the image when dealing with the "Pseudo-Mercator" projection. To calculate it, the CenterScaleMapBounds, toReferencedEnvelope methodThis is the most conflicting case. In the 1. Different Concepts of Scale and Input Parameters The two methods are triggered by different scale definitions, which determines their starting units.
This initial difference in intent (physical scale vs. cartographic scale) means we cannot simply reuse the first method, as the input parameters are conceptually and numerically different. 2. Fundamentally Different Algorithm for the Y-Axis This is the most critical algorithmic difference.
3. Different Final BBox Construction (Output Processing) Finally, the construction of the
In summary: The two methods differ in their initial inputs (physical vs. cartographic scale), their main algorithm for the Y-axis (geodetic destination vs. correction with a linear offset), and their final processing of the result (angular normalization vs. direct planar coordinates). For these reasons, we conclude that creating a dedicated and specialized method for Pseudo-Mercator is the clearest and safest design choice. It correctly encapsulates the unique handling that this projection requires and avoids complicating the existing and proven We could integrate the method we created, I don't know if this resolves the doubts about point 4, "differences between cases 1 and 3 except for the additional reprojection," or if they were related to something else. On the other hand, if we make these changes to integrate the Pseudo-Mercator related calculations where they are needed, only the Regarding the introduction of the new parameter in |
|
@fdiazcarsi Thanks for all the explanations. We will get back to you shortly. |
28c3fe1 to
20ba37e
Compare
|
We have made the changes we proposed to integrate our code with the project's base code. We added the useGeodeticCalculations attribute to the map parameters, added unit tests for the new methods, and included an example of how to use this attribute (use_geodetic_calculations_EPSG_3857). We also performed the rebase and verified that the example works correctly. |
This commit introduces the useGeodeticCalculations parameter for maps. When set to true for Pseudo-Mercator projections, it ensures more accurate scale and bounding box computations by accounting for Mercator projection distortions, especially away from the equator. This change affects map bounds, scale calculation, and WMTS layer scaling. It also includes new unit tests and an example to demonstrate the feature.
81fc36c to
e5f2808
Compare
Related with issue #327.
This pull request introduces a new utility class,
EPSG3857Utils, and refactors existing map bounds and tile caching logic to ensure accurate handling of the EPSG:3857 (Web Mercator) projection.The EPSG:3857 projection presents distance and area distortions, which vary with latitude. Traditional linear calculations for map scale and bounding boxes are inaccurate in this projection, especially at higher latitudes. This change addresses these inaccuracies by centralizing and applying geodetic correction methods.
Key changes and their impact:
New Utility: EPSG3857Utils.java: A new utility class has been added to encapsulate specific calculations required for EPSG:3857. This includes methods for:
Identifying if a CRS is EPSG:3857.
Computing a scaling factor to compensate for Mercator distortion.
Calculating the true orthodromic width of a bounding box.
Computing a
ReferencedEnvelopefrom a center, scale, and paint area, accounting for distortion.WMTSLayer.java (WMTSTileCacheInfo constructor): Modified to use
EPSG3857Utils.computeScalingFactor()when the map projection is EPSG:3857. This ensures that theimageBufferScalingcorrectly accounts for Mercator distortion, leading to more accurate tile rendering.BBoxMapBounds.java (getScale method): Refactored to use
EPSG3857Utils.computeOrthodromicWidthInInches()for EPSG:3857 projections. This replaces a potentially inaccurate linear calculation with a geodetically correct one, ensuring the computed map scale is precise in the center of image.CenterScaleMapBounds.java (toReferencedEnvelope method): Updated to utilize
EPSG3857Utils.computeReferencedEnvelope()for EPSG:3857. This ensures that the bounding box is accurately derived from the center and scale, correctly accounting for the projection's distortions.These changes improve the accuracy of map rendering and scale calculations for EPSG:3857.
See JIRA issue: EPSG-3857.