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}.
