22
33import java .awt .Rectangle ;
44import org .geotools .api .referencing .crs .CoordinateReferenceSystem ;
5+ import org .geotools .api .referencing .operation .MathTransform ;
6+ import org .geotools .geometry .jts .JTS ;
57import org .geotools .geometry .jts .ReferencedEnvelope ;
8+ import org .geotools .referencing .CRS ;
69import org .geotools .referencing .GeodeticCalculator ;
710import org .locationtech .jts .geom .Coordinate ;
811import org .locationtech .jts .geom .Envelope ;
912import org .mapfish .print .FloatingPointUtil ;
13+ import org .mapfish .print .PrintException ;
14+ import org .mapfish .print .PseudoMercatorUtils ;
1015import org .mapfish .print .map .DistanceUnit ;
1116import org .mapfish .print .map .Scale ;
1217
@@ -23,12 +28,46 @@ public final class BBoxMapBounds extends MapBounds {
2328 *
2429 * @param projection the projection these bounds are defined in.
2530 * @param envelope the bounds
31+ * @param useGeodeticCalculations force to use geodetic calculations in PseudoMercator projection
2632 */
27- public BBoxMapBounds (final CoordinateReferenceSystem projection , final Envelope envelope ) {
28- super (projection );
33+ public BBoxMapBounds (
34+ final CoordinateReferenceSystem projection ,
35+ final Envelope envelope ,
36+ final boolean useGeodeticCalculations ) {
37+ super (projection , useGeodeticCalculations );
2938 this .bbox = envelope ;
3039 }
3140
41+ /**
42+ * Constructor.
43+ *
44+ * @param projection the projection these bounds are defined in.
45+ * @param envelope the bounds
46+ */
47+ public BBoxMapBounds (final CoordinateReferenceSystem projection , final Envelope envelope ) {
48+ this (projection , envelope , false );
49+ }
50+
51+ /**
52+ * Constructor.
53+ *
54+ * @param projection the projection these bounds are defined in.
55+ * @param minX min X coordinate for the MapBounds
56+ * @param minY min Y coordinate for the MapBounds
57+ * @param maxX max X coordinate for the MapBounds
58+ * @param maxY max Y coordinate for the MapBounds
59+ * @param useGeodeticCalculations force to use geodetic calculations in PseudoMercator projection
60+ */
61+ public BBoxMapBounds (
62+ final CoordinateReferenceSystem projection ,
63+ final double minX ,
64+ final double minY ,
65+ final double maxX ,
66+ final double maxY ,
67+ final boolean useGeodeticCalculations ) {
68+ this (projection , new Envelope (minX , maxX , minY , maxY ), useGeodeticCalculations );
69+ }
70+
3271 /**
3372 * Constructor.
3473 *
@@ -47,6 +86,22 @@ public BBoxMapBounds(
4786 this (projection , new Envelope (minX , maxX , minY , maxY ));
4887 }
4988
89+ /**
90+ * Create from a bbox.
91+ *
92+ * @param bbox the bounds.
93+ * @param useGeodeticCalculations force to use geodetic calculations in PseudoMercator projection
94+ */
95+ public BBoxMapBounds (final ReferencedEnvelope bbox , final boolean useGeodeticCalculations ) {
96+ this (
97+ bbox .getCoordinateReferenceSystem (),
98+ bbox .getMinX (),
99+ bbox .getMinY (),
100+ bbox .getMaxX (),
101+ bbox .getMaxY (),
102+ useGeodeticCalculations );
103+ }
104+
50105 /**
51106 * Create from a bbox.
52107 *
@@ -80,7 +135,8 @@ public MapBounds adjustedEnvelope(final Rectangle paintArea) {
80135 centerX - finalDiff ,
81136 this .bbox .getMinY (),
82137 centerX + finalDiff ,
83- this .bbox .getMaxY ());
138+ this .bbox .getMaxY (),
139+ this .useGeodeticCalculations ());
84140 } else {
85141 double centerY = (this .bbox .getMinY () + this .bbox .getMaxY ()) / 2 ;
86142 double factor = bboxAspectRatio / paintAreaAspectRatio ;
@@ -90,7 +146,8 @@ public MapBounds adjustedEnvelope(final Rectangle paintArea) {
90146 this .bbox .getMinX (),
91147 centerY - finalDiff ,
92148 this .bbox .getMaxX (),
93- centerY + finalDiff );
149+ centerY + finalDiff ,
150+ this .useGeodeticCalculations ());
94151 }
95152 }
96153
@@ -107,34 +164,69 @@ public MapBounds adjustBoundsToNearestScale(
107164 getNearestScale (zoomLevels , tolerance , zoomLevelSnapStrategy , geodetic , paintArea , dpi );
108165
109166 Coordinate center = this .bbox .centre ();
110- return new CenterScaleMapBounds (getProjection (), center .x , center .y , newScale );
167+ return new CenterScaleMapBounds (
168+ getProjection (), center .x , center .y , newScale , this .useGeodeticCalculations ());
111169 }
112170
113171 @ Override
114172 public Scale getScale (final Rectangle paintArea , final double dpi ) {
115173 final ReferencedEnvelope bboxAdjustedToScreen = toReferencedEnvelope (paintArea );
116174
117- DistanceUnit projUnit = DistanceUnit .fromProjection (getProjection ());
175+ CoordinateReferenceSystem crs = getProjection ();
176+ DistanceUnit projUnit = DistanceUnit .fromProjection (crs );
118177
119178 double geoWidthInInches ;
120- if (projUnit == DistanceUnit .DEGREES ) {
121- GeodeticCalculator calculator = new GeodeticCalculator (getProjection ());
122- final double centerY = bboxAdjustedToScreen .centre ().y ;
123- calculator .setStartingGeographicPoint (bboxAdjustedToScreen .getMinX (), centerY );
124- calculator .setDestinationGeographicPoint (bboxAdjustedToScreen .getMaxX (), centerY );
125- double geoWidthInEllipsoidUnits = calculator .getOrthodromicDistance ();
126- DistanceUnit ellipsoidUnit =
127- DistanceUnit .fromString (calculator .getEllipsoid ().getAxisUnit ().toString ());
128179
129- geoWidthInInches = ellipsoidUnit .convertTo (geoWidthInEllipsoidUnits , DistanceUnit .IN );
180+ // If it is geodetic/degrees OR it is a special case requiring geodetic calculation
181+ // (PseudoMercator)
182+ if (projUnit == DistanceUnit .DEGREES
183+ || (this .useGeodeticCalculations () && PseudoMercatorUtils .isPseudoMercator (crs ))) {
184+ geoWidthInInches = this .computeGeodeticWidthInInches (bboxAdjustedToScreen );
130185 } else {
131- // (scale * width ) / dpi = geowidith
186+ // (scale * width ) / dpi = geoWidth
132187 geoWidthInInches = projUnit .convertTo (bboxAdjustedToScreen .getWidth (), DistanceUnit .IN );
133188 }
134189
135190 return new Scale (geoWidthInInches * (dpi / paintArea .getWidth ()), projUnit , dpi );
136191 }
137192
193+ @ SuppressWarnings ("UseSpecificCatch" )
194+ private double computeGeodeticWidthInInches (final ReferencedEnvelope bbox ) {
195+ try {
196+ CoordinateReferenceSystem crs = bbox .getCoordinateReferenceSystem ();
197+ CoordinateReferenceSystem calcCrs = crs ;
198+
199+ double centerY = bbox .centre ().y ;
200+ Coordinate start = new Coordinate (bbox .getMinX (), centerY );
201+ Coordinate end = new Coordinate (bbox .getMaxX (), centerY );
202+
203+ if (this .useGeodeticCalculations () && PseudoMercatorUtils .isPseudoMercator (crs )) {
204+ // Reproject to a geographic CRS (EPSG:4326) for accurate geodetic calculations
205+ final CoordinateReferenceSystem geographicCrs =
206+ GenericMapAttribute .parseProjection ("EPSG:4326" , true );
207+ final MathTransform transform = CRS .findMathTransform (crs , geographicCrs );
208+ start = JTS .transform (start , null , transform );
209+ end = JTS .transform (end , null , transform );
210+ calcCrs = geographicCrs ;
211+ }
212+
213+ // Construct the calculator with the CRS that matches the coordinates
214+ GeodeticCalculator calculator = new GeodeticCalculator (calcCrs );
215+
216+ // --- Common Logic ---
217+ calculator .setStartingGeographicPoint (start .x , start .y );
218+ calculator .setDestinationGeographicPoint (end .x , end .y );
219+ final double orthodromicWidth = calculator .getOrthodromicDistance ();
220+ final DistanceUnit ellipsoidUnit =
221+ DistanceUnit .fromString (calculator .getEllipsoid ().getAxisUnit ().toString ());
222+
223+ return ellipsoidUnit .convertTo (orthodromicWidth , DistanceUnit .IN );
224+
225+ } catch (Exception e ) {
226+ throw new PrintException ("Failed to compute geodetic width" , e );
227+ }
228+ }
229+
138230 @ Override
139231 public MapBounds adjustBoundsToRotation (final double rotation ) {
140232 if (FloatingPointUtil .equals (rotation , 0.0 )) {
@@ -157,7 +249,13 @@ public MapBounds adjustBoundsToRotation(final double rotation) {
157249 final double rotatedMinY = this .bbox .getMinY () - heightDifference ;
158250 final double rotatedMaxY = this .bbox .getMaxY () + heightDifference ;
159251
160- return new BBoxMapBounds (getProjection (), rotatedMinX , rotatedMinY , rotatedMaxX , rotatedMaxY );
252+ return new BBoxMapBounds (
253+ getProjection (),
254+ rotatedMinX ,
255+ rotatedMinY ,
256+ rotatedMaxX ,
257+ rotatedMaxY ,
258+ this .useGeodeticCalculations ());
161259 }
162260
163261 private double getRotatedWidth (final double rotation ) {
@@ -197,13 +295,15 @@ public MapBounds zoomOut(final double factor) {
197295 double minGeoY = centerY - destHeight / 2.0f ;
198296 double maxGeoY = centerY + destHeight / 2.0f ;
199297
200- return new BBoxMapBounds (getProjection (), minGeoX , minGeoY , maxGeoX , maxGeoY );
298+ return new BBoxMapBounds (
299+ getProjection (), minGeoX , minGeoY , maxGeoX , maxGeoY , this .useGeodeticCalculations ());
201300 }
202301
203302 @ Override
204303 public MapBounds zoomToScale (final Scale scale ) {
205304 Coordinate center = this .bbox .centre ();
206- return new CenterScaleMapBounds (getProjection (), center .x , center .y , scale );
305+ return new CenterScaleMapBounds (
306+ getProjection (), center .x , center .y , scale , this .useGeodeticCalculations ());
207307 }
208308
209309 @ Override
@@ -241,7 +341,8 @@ public MapBounds expand(final int margin, final Rectangle paintArea) {
241341 final double minGeoY = centerY - destHeight / 2.0 ;
242342 final double maxGeoY = centerY + destHeight / 2.0 ;
243343
244- return new BBoxMapBounds (getProjection (), minGeoX , minGeoY , maxGeoX , maxGeoY );
344+ return new BBoxMapBounds (
345+ getProjection (), minGeoX , minGeoY , maxGeoX , maxGeoY , this .useGeodeticCalculations ());
245346 }
246347
247348 @ Override
0 commit comments