This is an automated email from the ASF dual-hosted git repository.

desruisseaux pushed a commit to branch geoapi-4.0
in repository https://gitbox.apache.org/repos/asf/sis.git

commit 06e6987fd070a8677370ca3d9ff38fa503c7472b
Author: Martin Desruisseaux <[email protected]>
AuthorDate: Thu Jun 30 18:55:01 2022 +0200

    First draft of a limit applied (at visualisation time) on image 
reprojection.
    This is for preventing exceptions when rendering a world image in Mercator.
    For now we check only the World Mercator projection, but other cases should
    be added progressively in the future.
---
 .../internal/map/coverage/ProjectionLimits.java    | 108 +++++++++++++++++++++
 .../sis/internal/map/coverage/RenderingData.java   |  55 ++++++++++-
 .../main/java/org/apache/sis/referencing/CRS.java  |   4 +-
 3 files changed, 164 insertions(+), 3 deletions(-)

diff --git 
a/core/sis-portrayal/src/main/java/org/apache/sis/internal/map/coverage/ProjectionLimits.java
 
b/core/sis-portrayal/src/main/java/org/apache/sis/internal/map/coverage/ProjectionLimits.java
new file mode 100644
index 0000000000..c955e3ef3a
--- /dev/null
+++ 
b/core/sis-portrayal/src/main/java/org/apache/sis/internal/map/coverage/ProjectionLimits.java
@@ -0,0 +1,108 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sis.internal.map.coverage;
+
+import org.opengis.geometry.Envelope;
+import org.opengis.referencing.operation.MathTransform;
+import org.opengis.referencing.operation.CoordinateOperation;
+import org.opengis.referencing.crs.CoordinateReferenceSystem;
+import org.apache.sis.geometry.GeneralEnvelope;
+import org.apache.sis.referencing.CRS;
+import org.apache.sis.referencing.operation.projection.Mercator;
+import org.apache.sis.referencing.operation.projection.NormalizedProjection;
+import org.apache.sis.referencing.operation.transform.MathTransforms;
+
+
+/**
+ * Map projection for which to apply a limit for avoiding rendering problems.
+ * The most common case is the Mercator projection, for which we need to put
+ * a limit for avoiding to reach the poles.
+ *
+ * <p>This is a first draft to be expanded progressively.</p>
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.3
+ * @since   1.3
+ * @module
+ */
+final class ProjectionLimits {
+    /**
+     * List of rules for which we defines limits.
+     * This list may be expanded in future versions.
+     */
+    private static final ProjectionLimits[] RULES = {
+        new ProjectionLimits(Mercator.class)
+    };
+
+    /**
+     * The type of map projection for which this rule applies.
+     */
+    private final Class<? extends NormalizedProjection> target;
+
+    /**
+     * Creates a new rule for map projection limits.
+     *
+     * @param  target  the type of map projection for which this rule applies.
+     */
+    private ProjectionLimits(final Class<? extends NormalizedProjection> 
target) {
+        this.target = target;
+    }
+
+    /**
+     * Returns the map projection limits for rendering a map in the given 
objective CRS.
+     * The default implementation returns the CRS domain of validity, which is 
okay for
+     * the "World Mercator" projection but is often too conservative for other 
projections.
+     * For example in the case of UTM projection, we needs to allow both 
hemisphere and a larger zone.
+     *
+     * @param  objectiveCRS  the CRS used for rendering the map.
+     * @return limits where to crop the projected image in objective CRS, or 
{@code null} if none.
+     */
+    Envelope limits(final CoordinateReferenceSystem objectiveCRS) {
+        return CRS.getDomainOfValidity(objectiveCRS);
+    }
+
+    /**
+     * Returns the map projection limits for rendering a map after the 
specified "data to objective" transform.
+     *
+     * @param  changeOfCRS  the operation applied on data before rendering in 
objective CRS.
+     * @return limits where to crop the projected image in objective CRS, or 
{@code null} if none.
+     */
+    static Envelope find(final CoordinateOperation changeOfCRS) {
+        Envelope limits = null;
+        if (changeOfCRS != null) {
+            GeneralEnvelope intersection = null;
+            for (final MathTransform step : 
MathTransforms.getSteps(changeOfCRS.getMathTransform())) {
+                for (final ProjectionLimits rule : RULES) {
+                    if (rule.target.isInstance(step)) {
+                        final Envelope e = 
rule.limits(changeOfCRS.getTargetCRS());
+                        if (e != null) {
+                            if (limits == null) {
+                                limits = e;
+                            } else {
+                                if (intersection == null) {
+                                    limits = intersection = new 
GeneralEnvelope(limits);
+                                }
+                                intersection.intersect(e);
+                            }
+                        }
+                    }
+                }
+            }
+        }
+        return limits;
+    }
+}
diff --git 
a/core/sis-portrayal/src/main/java/org/apache/sis/internal/map/coverage/RenderingData.java
 
b/core/sis-portrayal/src/main/java/org/apache/sis/internal/map/coverage/RenderingData.java
index 81ffbcdec8..6135be896b 100644
--- 
a/core/sis-portrayal/src/main/java/org/apache/sis/internal/map/coverage/RenderingData.java
+++ 
b/core/sis-portrayal/src/main/java/org/apache/sis/internal/map/coverage/RenderingData.java
@@ -30,6 +30,7 @@ import java.awt.geom.Rectangle2D;
 import java.awt.geom.AffineTransform;
 import java.awt.geom.NoninvertibleTransformException;
 import org.opengis.util.FactoryException;
+import org.opengis.geometry.Envelope;
 import org.opengis.geometry.DirectPosition;
 import org.opengis.metadata.extent.GeographicBoundingBox;
 import org.opengis.referencing.datum.PixelInCell;
@@ -47,6 +48,7 @@ import org.apache.sis.coverage.grid.PixelTranslation;
 import org.apache.sis.coverage.SampleDimension;
 import org.apache.sis.geometry.AbstractEnvelope;
 import org.apache.sis.geometry.Envelope2D;
+import org.apache.sis.geometry.Envelopes;
 import org.apache.sis.geometry.Shapes2D;
 import org.apache.sis.image.PlanarImage;
 import org.apache.sis.image.ErrorHandler;
@@ -183,6 +185,21 @@ public class RenderingData implements Cloneable {
      */
     private GridGeometry dataGeometry;
 
+    /**
+     * The domain of validity of the objective CRS in units of the {@link 
#data} image.
+     * This value needs to be recomputed when the objective CRS or the {@link 
#dataGeometry} changes.
+     * It is used for avoiding failure to project a part of the image too far 
from what the projection
+     * can handle, for example polar areas with a Mercator projection.
+     */
+    private Rectangle domainOfValidity;
+
+    /**
+     * A value for {@link #domainOfValidity} meaning that there is no limits. 
Should not be modified.
+     */
+    private static final Rectangle NO_LIMITS = new Rectangle(
+            Integer.MIN_VALUE, Integer.MIN_VALUE,
+            Integer.MAX_VALUE, Integer.MAX_VALUE);
+
     /**
      * Ranges of sample values in each band of {@link #data}. This is used for 
determining on which sample values
      * to apply colors when user asked to apply a color ramp. May be {@code 
null}.
@@ -272,6 +289,7 @@ public class RenderingData implements Cloneable {
         changeOfCRS       = null;
         cornerToObjective = null;
         objectiveToCenter = null;
+        domainOfValidity  = null;
     }
 
     /**
@@ -421,6 +439,7 @@ public class RenderingData implements Cloneable {
             final MathTransform inverse = concatenate(PixelInCell.CELL_CENTER, 
old, dataGeometry, toNew);
             cornerToObjective = MathTransforms.concatenate(forward, 
cornerToObjective);
             objectiveToCenter = MathTransforms.concatenate(objectiveToCenter, 
inverse);
+            domainOfValidity  = null;       // Will be recomputed when needed.
         }
         return true;
     }
@@ -549,6 +568,40 @@ public class RenderingData implements Cloneable {
         }
     }
 
+    /**
+     * Returns the bounds of the given image clipped to the domain of validity 
of the objective CRS.
+     * The intent is to avoid a failure to reproject the image, for example if 
the image reaches the
+     * poles and the objective CRS is a Mercator projection.
+     */
+    private Rectangle getValidImageBounds(final RenderedImage image) throws 
TransformException {
+        Rectangle bounds = ImageUtilities.getBounds(image);
+        if (domainOfValidity == NO_LIMITS) {        // Identity comparison is 
okay here (quick check).
+            return bounds;
+        }
+        if (domainOfValidity == null) {
+            Envelope domain = ProjectionLimits.find(changeOfCRS);
+            if (domain == null) {
+                domainOfValidity = NO_LIMITS;
+                return bounds;
+            }
+            domain = Envelopes.transform(cornerToObjective.inverse(), domain);
+            double x = domain.getMinimum(0);
+            double y = domain.getMinimum(1);
+            double w = domain.getSpan(0);
+            double h = domain.getSpan(1);
+            if (!(x >= Integer.MIN_VALUE)) x = Integer.MIN_VALUE;       // Use 
`!` for catching NaN.
+            if (!(y >= Integer.MIN_VALUE)) y = Integer.MIN_VALUE;
+            if (!(h <= Integer.MAX_VALUE)) h = Integer.MAX_VALUE;
+            if (!(w <= Integer.MAX_VALUE)) w = Integer.MAX_VALUE;
+            domainOfValidity = new Rectangle(
+                    (int) Math.min(x, Integer.MAX_VALUE),
+                    (int) Math.min(y, Integer.MAX_VALUE),
+                    (int) Math.max(w, 0),
+                    (int) Math.max(h, 0));
+        }
+        return bounds.intersection(domainOfValidity);
+    }
+
     /**
      * Creates the resampled image, then optionally applies an index color 
model.
      * This method will compute the {@link MathTransform} steps from image 
coordinate system
@@ -608,7 +661,7 @@ public class RenderingData implements Cloneable {
         MathTransform displayToCenter = MathTransforms.concatenate(inverse, 
objectiveToCenter);
         final Rectangle bounds = (Rectangle) Shapes2D.transform(
                 MathTransforms.bidimensional(cornerToDisplay),
-                ImageUtilities.getBounds(recoloredImage), new Rectangle());
+                getValidImageBounds(recoloredImage), new Rectangle());
         /*
          * Verify if wraparound is really necessary. We do this check because 
the `displayToCenter` transform
          * may be used for every pixels, so it is worth to make that transform 
more efficient if possible.
diff --git 
a/core/sis-referencing/src/main/java/org/apache/sis/referencing/CRS.java 
b/core/sis-referencing/src/main/java/org/apache/sis/referencing/CRS.java
index 56dec908d8..acb27f0868 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/referencing/CRS.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/referencing/CRS.java
@@ -791,7 +791,7 @@ public final class CRS extends Static {
 
     /**
      * Returns the domain of validity of the specified coordinate reference 
system, or {@code null} if unknown.
-     * If non-null, then the returned envelope will use the same coordinate 
reference system them the given CRS
+     * If non-null, then the returned envelope will use the same coordinate 
reference system than the given CRS
      * argument.
      *
      * <p>This method looks in two places:</p>
@@ -801,7 +801,7 @@ public final class CRS extends Static {
      *       {@link BoundingPolygon} associated to the given CRS are taken in 
account for this first step.</li>
      *   <li>If the above step did not found found any bounding polygon, then 
the
      *       {@linkplain #getGeographicBoundingBox(CoordinateReferenceSystem) 
geographic bounding boxes}
-     *       are used as a fallback and tranformed to the given CRS.</li>
+     *       are used as a fallback and transformed to the given CRS.</li>
      * </ol>
      *
      * @param  crs  the coordinate reference system, or {@code null}.

Reply via email to