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 ;
1014import org .mapfish .print .map .DistanceUnit ;
1115import org .mapfish .print .map .Scale ;
16+ import org .mapfish .print .PseudoMercatorUtils ;
17+
18+
19+
1220
1321/**
1422 * Represent the map bounds with a bounding box.
@@ -23,12 +31,43 @@ public final class BBoxMapBounds extends MapBounds {
2331 *
2432 * @param projection the projection these bounds are defined in.
2533 * @param envelope the bounds
34+ * @param useGeodeticCalculations force to use geodetic calculations in PseudoMercator projection
2635 */
27- public BBoxMapBounds (final CoordinateReferenceSystem projection , final Envelope envelope ) {
28- super (projection );
36+ public BBoxMapBounds (final CoordinateReferenceSystem projection , final Envelope envelope , 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,26 +164,21 @@ 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 (getProjection (), center .x , center .y , newScale , this . useGeodeticCalculations () );
111168 }
112169
113170 @ Override
114171 public Scale getScale (final Rectangle paintArea , final double dpi ) {
115172 final ReferencedEnvelope bboxAdjustedToScreen = toReferencedEnvelope (paintArea );
116173
117- DistanceUnit projUnit = DistanceUnit .fromProjection (getProjection ());
174+ CoordinateReferenceSystem crs = getProjection ();
175+ DistanceUnit projUnit = DistanceUnit .fromProjection (crs );
118176
119177 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 ());
128-
129- geoWidthInInches = ellipsoidUnit .convertTo (geoWidthInEllipsoidUnits , DistanceUnit .IN );
178+
179+ // If it is geodetic/degress OR it is a special case requiring geodetic calculation (PseudoMercator)
180+ if (projUnit == DistanceUnit .DEGREES || (this .useGeodeticCalculations () && PseudoMercatorUtils .isPseudoMercator (crs ))) {
181+ geoWidthInInches = this .computeGeodeticWidthInInches (bboxAdjustedToScreen );
130182 } else {
131183 // (scale * width ) / dpi = geowidith
132184 geoWidthInInches = projUnit .convertTo (bboxAdjustedToScreen .getWidth (), DistanceUnit .IN );
@@ -135,6 +187,36 @@ public Scale getScale(final Rectangle paintArea, final double dpi) {
135187 return new Scale (geoWidthInInches * (dpi / paintArea .getWidth ()), projUnit , dpi );
136188 }
137189
190+ @ SuppressWarnings ("UseSpecificCatch" )
191+ private double computeGeodeticWidthInInches (final ReferencedEnvelope bbox ) {
192+ try {
193+ CoordinateReferenceSystem crs = bbox .getCoordinateReferenceSystem ();
194+ GeodeticCalculator calculator = new GeodeticCalculator (crs ); // Use the original CRS for the ellipsoid
195+
196+ double centerY = bbox .centre ().y ;
197+ Coordinate start = new Coordinate (bbox .getMinX (), centerY );
198+ Coordinate end = new Coordinate (bbox .getMaxX (), centerY );
199+
200+ if (this .useGeodeticCalculations () && PseudoMercatorUtils .isPseudoMercator (crs )) {
201+ // Needs reprojection
202+ final MathTransform transform = CRS .findMathTransform (crs , GenericMapAttribute .parseProjection ("EPSG:4326" , true ));
203+ start = JTS .transform (start , null , transform );
204+ end = JTS .transform (end , null , transform );
205+ }
206+
207+ // --- Common Logic ---
208+ calculator .setStartingGeographicPoint (start .x , start .y );
209+ calculator .setDestinationGeographicPoint (end .x , end .y );
210+ final double orthodromicWidth = calculator .getOrthodromicDistance ();
211+ final DistanceUnit ellipsoidUnit = DistanceUnit .fromString (calculator .getEllipsoid ().getAxisUnit ().toString ());
212+
213+ return ellipsoidUnit .convertTo (orthodromicWidth , DistanceUnit .IN );
214+
215+ } catch (Exception e ) {
216+ throw new PrintException ("Failed to compute geodetic width" , e );
217+ }
218+ }
219+
138220 @ Override
139221 public MapBounds adjustBoundsToRotation (final double rotation ) {
140222 if (FloatingPointUtil .equals (rotation , 0.0 )) {
@@ -157,7 +239,7 @@ public MapBounds adjustBoundsToRotation(final double rotation) {
157239 final double rotatedMinY = this .bbox .getMinY () - heightDifference ;
158240 final double rotatedMaxY = this .bbox .getMaxY () + heightDifference ;
159241
160- return new BBoxMapBounds (getProjection (), rotatedMinX , rotatedMinY , rotatedMaxX , rotatedMaxY );
242+ return new BBoxMapBounds (getProjection (), rotatedMinX , rotatedMinY , rotatedMaxX , rotatedMaxY , this . useGeodeticCalculations () );
161243 }
162244
163245 private double getRotatedWidth (final double rotation ) {
@@ -197,7 +279,7 @@ public MapBounds zoomOut(final double factor) {
197279 double minGeoY = centerY - destHeight / 2.0f ;
198280 double maxGeoY = centerY + destHeight / 2.0f ;
199281
200- return new BBoxMapBounds (getProjection (), minGeoX , minGeoY , maxGeoX , maxGeoY );
282+ return new BBoxMapBounds (getProjection (), minGeoX , minGeoY , maxGeoX , maxGeoY , this . useGeodeticCalculations () );
201283 }
202284
203285 @ Override
@@ -241,7 +323,7 @@ public MapBounds expand(final int margin, final Rectangle paintArea) {
241323 final double minGeoY = centerY - destHeight / 2.0 ;
242324 final double maxGeoY = centerY + destHeight / 2.0 ;
243325
244- return new BBoxMapBounds (getProjection (), minGeoX , minGeoY , maxGeoX , maxGeoY );
326+ return new BBoxMapBounds (getProjection (), minGeoX , minGeoY , maxGeoX , maxGeoY , this . useGeodeticCalculations () );
245327 }
246328
247329 @ Override
0 commit comments