sis-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From desruisse...@apache.org
Subject [sis] branch geoapi-4.0 updated: First draft of Canvas.getGeographicArea() and getSpatialResolution(), and use those information when searching for a CoordinateOperation.
Date Tue, 11 Feb 2020 19:00:12 GMT
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


The following commit(s) were added to refs/heads/geoapi-4.0 by this push:
     new 88b18df  First draft of Canvas.getGeographicArea() and getSpatialResolution(), and
use those information when searching for a CoordinateOperation.
88b18df is described below

commit 88b18df1c73f9a6275131ddc2bf8e09e1ca968d2
Author: Martin Desruisseaux <martin.desruisseaux@geomatys.com>
AuthorDate: Tue Feb 11 19:58:25 2020 +0100

    First draft of Canvas.getGeographicArea() and getSpatialResolution(), and use those information
when searching for a CoordinateOperation.
---
 .../java/org/apache/sis/internal/map/Canvas.java   | 147 ++++++++++----
 .../org/apache/sis/internal/map/CanvasContext.java | 219 +++++++++++++++++++++
 .../org/apache/sis/internal/map/PlanarCanvas.java  |   2 +-
 .../internal/referencing/ReferencingUtilities.java |   4 +-
 4 files changed, 334 insertions(+), 38 deletions(-)

diff --git a/core/sis-portrayal/src/main/java/org/apache/sis/internal/map/Canvas.java b/core/sis-portrayal/src/main/java/org/apache/sis/internal/map/Canvas.java
index b049d2d..59b501c 100644
--- a/core/sis-portrayal/src/main/java/org/apache/sis/internal/map/Canvas.java
+++ b/core/sis-portrayal/src/main/java/org/apache/sis/internal/map/Canvas.java
@@ -16,10 +16,11 @@
  */
 package org.apache.sis.internal.map;
 
+import java.util.ArrayList;
 import java.util.Locale;
 import java.util.Objects;
 import java.util.Optional;
-import java.util.ArrayList;
+import java.util.OptionalDouble;
 import org.opengis.geometry.Envelope;
 import org.opengis.geometry.DirectPosition;
 import org.opengis.geometry.MismatchedDimensionException;
@@ -28,8 +29,10 @@ import org.opengis.metadata.extent.GeographicBoundingBox;
 import org.opengis.metadata.spatial.DimensionNameType;
 import org.opengis.referencing.crs.CoordinateReferenceSystem;
 import org.opengis.referencing.crs.EngineeringCRS;
+import org.opengis.referencing.crs.GeographicCRS;
 import org.opengis.referencing.operation.MathTransform;
 import org.opengis.referencing.operation.TransformException;
+import org.opengis.referencing.operation.CoordinateOperation;
 import org.opengis.referencing.datum.PixelInCell;
 import org.opengis.coverage.CannotEvaluateException;
 import org.opengis.util.FactoryException;
@@ -46,7 +49,6 @@ import org.apache.sis.referencing.operation.matrix.Matrices;
 import org.apache.sis.referencing.operation.transform.MathTransforms;
 import org.apache.sis.referencing.operation.transform.LinearTransform;
 import org.apache.sis.referencing.operation.transform.TransformSeparator;
-import org.apache.sis.referencing.operation.CoordinateOperationContext;
 import org.apache.sis.referencing.operation.DefaultCoordinateOperationFactory;
 import org.apache.sis.internal.referencing.CoordinateOperations;
 import org.apache.sis.internal.referencing.ReferencingUtilities;
@@ -104,7 +106,7 @@ import org.apache.sis.coverage.grid.GridExtent;
  * <h2>Location of data to display</h2>
  * In addition of above-cited Coordinate Reference Systems, a {@code Canvas} contains also
a point of interest.
  * The point of interest is often, but not necessarily, at the center of display area.
- * It defines the position where {@linkplain #getResolution() resolutions} will be computed,
and where
+ * It defines the position where {@linkplain #getSpatialResolution() resolutions} will be
computed, and where
  * {@linkplain PlanarCanvas#scale(double, double) scales},
  * {@linkplain PlanarCanvas#translate(double, double) translations} and
  * {@linkplain PlanarCanvas#rotate(double) rotations} will be applied.
@@ -144,14 +146,6 @@ import org.apache.sis.coverage.grid.GridExtent;
  */
 public class Canvas extends Observable implements Localized {
     /**
-     * Desired resolution in display units (usually pixels). This is used for avoiding
-     * the cost of transformations having too much accuracy for the current zoom level.
-     *
-     * @see #findTransform(CoordinateReferenceSystem, CoordinateReferenceSystem)
-     */
-    private static final double DISPLAY_RESOLUTION = 1;
-
-    /**
      * The {@value} property name, used for notifications about changes in objective CRS.
      * The objective CRS is the Coordinate Reference System in which all data are transformed
before displaying.
      * Its number of dimension is the determined by the display device (two for flat screens).
@@ -216,6 +210,24 @@ public class Canvas extends Observable implements Localized {
     private static final String GRID_GEOMETRY_PROPERTY = "gridGeometry";
 
     /**
+     * The {@value} property name. The geographic area is a synthetic property computed
+     * from {@value #DISPLAY_BOUNDS_PROPERTY}, {@value #OBJECTIVE_TO_DISPLAY_PROPERTY}
+     * and {@value #OBJECTIVE_CRS_PROPERTY}. There is no event fired for this property.
+     *
+     * @see #getGeographicArea()
+     */
+    private static final String GEOGRAPHIC_AREA_PROPERTY = "geographicArea";
+
+    /**
+     * The {@value} property name. The resolution is a synthetic property computed from
+     * {@value #POINT_OF_INTEREST_PROPERTY}, {@value #OBJECTIVE_TO_DISPLAY_PROPERTY} and
+     * {@value #OBJECTIVE_CRS_PROPERTY}. There is no event fired for this property.
+     *
+     * @see #getSpatialResolution()
+     */
+    private static final String SPATIAL_RESOLUTION_PROPERTY = "spatialResolution";
+
+    /**
      * The coordinate reference system in which to transform all data before displaying.
      * If {@code null}, then no transformation is applied and data coordinates are used directly
      * as display coordinates, regardless the data CRS (even if different data use different
CRS).
@@ -270,6 +282,7 @@ public class Canvas extends Observable implements Localized {
      * and {@link #objectiveCRS} (indirectly, through {@link #multidimToObjective}) and should
be
      * recomputed when any of those properties changed.</p>
      *
+     * @see #getObjectivePOI()
      * @see #getGridGeometry()
      */
     private DirectPosition objectivePOI;
@@ -343,6 +356,17 @@ public class Canvas extends Observable implements Localized {
     private GridGeometry gridGeometry;
 
     /**
+     * The context (geographic area and desired resolution) for selecting a coordinate operation.
+     * The information contained in this object can opportunistically be used for providing
the
+     * geographic area and spatial resolution of this canvas.
+     *
+     * @see #getGeographicArea()
+     * @see #getSpatialResolution()
+     * @see #findTransform(CoordinateReferenceSystem, CoordinateReferenceSystem)
+     */
+    private final CanvasContext operationContext;
+
+    /**
      * The factory to use for creating coordinate operations. This factory allow us to specify
the area
      * of interest (the geographic region shown by this {@code Canvas}) and the desired resolution.
      *
@@ -374,6 +398,7 @@ public class Canvas extends Observable implements Localized {
         displayBounds = new GeneralEnvelope(displayCRS);
         displayBounds.setToNaN();
         coordinateOperationFactory = CoordinateOperations.factory();
+        operationContext = new CanvasContext();
     }
 
     /**
@@ -479,10 +504,11 @@ public class Canvas extends Observable implements Localized {
         ArgumentChecks.ensureNonNull(OBJECTIVE_CRS_PROPERTY, newValue);
         ArgumentChecks.ensureDimensionMatches(OBJECTIVE_CRS_PROPERTY, getDisplayDimensions(),
newValue);
         final CoordinateReferenceSystem oldValue = objectiveCRS;
-        LinearTransform oldObjectiveToDisplay = null;
-        LinearTransform newObjectiveToDisplay = null;
-        if (!newValue.equals(oldValue)) {
-            if (oldValue != null) try {
+        if (!newValue.equals(oldValue)) try {
+            final CoordinateOperation newToGeo = objectiveToGeographic(newValue);
+            LinearTransform oldObjectiveToDisplay = null;
+            LinearTransform newObjectiveToDisplay = null;
+            if (oldValue != null) {
                 /*
                  * Compute the change unconditionally as a way to verify that the new CRS
is compatible with
                  * data currently shown. Another reason is that checking identity transform
is more reliable
@@ -517,14 +543,15 @@ public class Canvas extends Observable implements Localized {
                     axisTypes             = null;
                     gridGeometry          = null;
                 }
-            } catch (FactoryException | TransformException e) {
-                throw new RenderException(errors().getString(Errors.Keys.CanNotSetPropertyValue_1,
OBJECTIVE_CRS_PROPERTY), e);
             }
-            objectiveCRS = newValue;            // Set only after everything else succeeded.
+            objectiveCRS = newValue;                                // Set only after everything
else succeeded.
+            operationContext.setObjectiveToGeographic(newToGeo);
             firePropertyChange(OBJECTIVE_CRS_PROPERTY, oldValue, newValue);
             if (!Objects.equals(oldObjectiveToDisplay, newObjectiveToDisplay)) {
                 firePropertyChange(OBJECTIVE_TO_DISPLAY_PROPERTY, oldObjectiveToDisplay,
newObjectiveToDisplay);
             }
+        } catch (FactoryException | TransformException e) {
+            throw new RenderException(errors().getString(Errors.Keys.CanNotSetPropertyValue_1,
OBJECTIVE_CRS_PROPERTY), e);
         }
     }
 
@@ -610,6 +637,8 @@ public class Canvas extends Observable implements Localized {
      */
     void updateObjectiveToDisplay(final LinearTransform newValue) {
         objectiveToDisplay = newValue;
+        gridGeometry       = null;
+        operationContext.clear();
     }
 
     /**
@@ -624,6 +653,7 @@ public class Canvas extends Observable implements Localized {
      * @return size and location of the display device.
      *
      * @see #DISPLAY_BOUNDS_PROPERTY
+     * @see #getGeographicArea()
      */
     public Envelope getDisplayBounds() {
         return displayBounds.isAllNaN() ? null : new GeneralEnvelope(displayBounds);
@@ -656,6 +686,7 @@ public class Canvas extends Observable implements Localized {
         }
         if (!oldValue.equals(displayBounds)) {
             gridGeometry = null;
+            operationContext.partialClear(false);                               // Resolution
is still valid.
             firePropertyChange(DISPLAY_BOUNDS_PROPERTY, oldValue, newValue);    // Do not
publish reference to `displayBounds`.
         }
     }
@@ -703,11 +734,13 @@ public class Canvas extends Observable implements Localized {
              * (only the number of dimensions that the display device can show).
              */
             if (objectiveCRS == null) {
-                objectiveCRS = CRS.getComponentAt(crs, 0, getDisplayDimensions());
-                if (objectiveCRS == null) {
+                final CoordinateReferenceSystem newObjectiveCRS = CRS.getComponentAt(crs,
0, getDisplayDimensions());
+                if (newObjectiveCRS == null) {
                     throw new IllegalArgumentException("Can not infer objective CRS.");
                     // Message not localized yet because we should probably try harder.
                 }
+                operationContext.setObjectiveToGeographic(objectiveToGeographic(newObjectiveCRS));
+                objectiveCRS = newObjectiveCRS;                            // Set only on
success.
             }
             /*
              * Transform the Point Of Interest to the objective CRS as a way to test its
validity.
@@ -724,6 +757,7 @@ public class Canvas extends Observable implements Localized {
             augmentedObjectiveCRS = null;                                           // Will
be recomputed when first needed.
             axisTypes             = null;
             gridGeometry          = null;
+            operationContext.partialClear(true);                                    // Geographic
area is still valid.
             firePropertyChange(POINT_OF_INTEREST_PROPERTY, oldValue, newValue);     // Do
not publish reference to `copy`.
         } catch (FactoryException | TransformException e) {
             throw new RenderException(errors().getString(Errors.Keys.CanNotSetPropertyValue_1,
POINT_OF_INTEREST_PROPERTY), e);
@@ -731,6 +765,14 @@ public class Canvas extends Observable implements Localized {
     }
 
     /**
+     * Returns the coordinate values of the Point Of Interest (POI) in objective CRS.
+     * The array length should be equal to {@link #getDisplayDimensions()}.
+     */
+    final double[] getObjectivePOI() {
+        return objectivePOI.getCoordinate();
+    }
+
+    /**
      * Returns canvas properties (CRS, display bounds, conversion) encapsulated in a grid
geometry.
      * This is a convenience method for interoperability with grid coverage API.
      * If {@link #setGridGeometry(GridGeometry)} has been invoked with a non-null value and
no other
@@ -914,8 +956,8 @@ public class Canvas extends Observable implements Localized {
              * "all or nothing" behavior.
              */
             displayBounds.setEnvelope(newBounds);
+            updateObjectiveToDisplay(newObjectiveToDisplay);
             pointOfInterest       = newPOI;
-            objectiveToDisplay    = newObjectiveToDisplay;
             objectiveCRS          = newObjectiveCRS;
             multidimToObjective   = dimensionSelect;
             augmentedObjectiveCRS = null;               // Will be recomputed when first
needed.
@@ -949,21 +991,47 @@ public class Canvas extends Observable implements Localized {
         }
     }
 
-    public Optional<GeographicBoundingBox> getGeographicArea() {
-        return Optional.empty();        // TODO
+    /**
+     * Returns the geographic bounding box encompassing the area shown on the display device.
+     * If the {@linkplain #getObjectiveCRS() objective CRS} is not convertible to a geographic
CRS,
+     * then this method returns an empty value.
+     *
+     * @return geographic bounding box encompassing the viewed area.
+     * @throws RenderException in an error occurred while computing the geographic area.
+     *
+     * @see #getDisplayBounds()
+     */
+    public Optional<GeographicBoundingBox> getGeographicArea() throws RenderException
{
+        try {
+            return operationContext.getGeographicArea(this);
+        } catch (TransformException e) {
+            throw new RenderException(errors().getString(Errors.Keys.CanNotCompute_1, GEOGRAPHIC_AREA_PROPERTY),
e);
+        }
     }
 
-    public double[] getResolution() {
-        return null;
+    /**
+     * Returns an estimation of the resolution (in metres) at the point of interest.
+     * If the {@linkplain #getObjectiveCRS() objective CRS} is not convertible to a
+     * geographic CRS, then this method returns an empty value.
+     *
+     * @return estimation of the resolution in metres at current point of interest.
+     * @throws RenderException in an error occurred while computing the resolution.
+     */
+    public OptionalDouble getSpatialResolution() throws RenderException {
+        try {
+            return operationContext.getSpatialResolution(this);
+        } catch (TransformException e) {
+            throw new RenderException(errors().getString(Errors.Keys.CanNotCompute_1, SPATIAL_RESOLUTION_PROPERTY),
e);
+        }
     }
 
     /**
-     * Allocates a position which can hold a coordinates in objective or display CRS, or
-     * returns {@code null} for letting {@link MathTransform} do the allocation themselves.
-     * May be overridden by subclasses for a little bit more efficiency.
+     * Computes the value for {@link #objectiveToGeographic}. The value is not stored by
this method for
+     * giving caller a chance to validate other properties before to write them in a "all
or nothing" way.
      */
-    DirectPosition allocatePosition() {
-        return null;
+    private CoordinateOperation objectiveToGeographic(final CoordinateReferenceSystem crs)
throws FactoryException {
+        final GeographicCRS geoCRS = ReferencingUtilities.toNormalizedGeographicCRS(crs,
false, false);
+        return (geoCRS != null) ? coordinateOperationFactory.createOperation(crs, geoCRS)
: null;
     }
 
     /**
@@ -972,14 +1040,23 @@ public class Canvas extends Observable implements Localized {
      * CRS may differ depending on which area is currently visible in the canvas. All requests
for a coordinate
      * operation should invoke this method instead than {@link CRS#findOperation(CoordinateReferenceSystem,
      * CoordinateReferenceSystem, GeographicBoundingBox)}.
+     *
+     * @todo verify if bounding box/resolution are up-to-date.
      */
     private MathTransform findTransform(final CoordinateReferenceSystem source,
-                                        final CoordinateReferenceSystem target) throws FactoryException
+                                        final CoordinateReferenceSystem target)
+            throws FactoryException, RenderException
     {
-        final CoordinateOperationContext context = new CoordinateOperationContext();
-        final Optional<GeographicBoundingBox> geographicArea = getGeographicArea();
-        geographicArea.ifPresent(context::setAreaOfInterest);
-        return coordinateOperationFactory.createOperation(source, target, context).getMathTransform();
+        operationContext.refresh();
+        return coordinateOperationFactory.createOperation(source, target, operationContext).getMathTransform();
+    }
+
+    /**
+     * Allocates a position which can hold a coordinates in objective CRS.
+     * May be overridden by subclasses for a little bit more efficiency.
+     */
+    DirectPosition allocatePosition() {
+        return new GeneralDirectPosition(objectiveCRS);
     }
 
     /**
diff --git a/core/sis-portrayal/src/main/java/org/apache/sis/internal/map/CanvasContext.java
b/core/sis-portrayal/src/main/java/org/apache/sis/internal/map/CanvasContext.java
new file mode 100644
index 0000000..5e673bd
--- /dev/null
+++ b/core/sis-portrayal/src/main/java/org/apache/sis/internal/map/CanvasContext.java
@@ -0,0 +1,219 @@
+/*
+ * 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;
+
+import java.util.Optional;
+import java.util.OptionalDouble;
+import org.opengis.metadata.extent.GeographicBoundingBox;
+import org.opengis.referencing.crs.GeographicCRS;
+import org.opengis.referencing.crs.CoordinateReferenceSystem;
+import org.opengis.referencing.datum.Ellipsoid;
+import org.opengis.referencing.operation.Matrix;
+import org.opengis.referencing.operation.MathTransform;
+import org.opengis.referencing.operation.TransformException;
+import org.opengis.referencing.operation.CoordinateOperation;
+import org.apache.sis.referencing.operation.CoordinateOperationContext;
+import org.apache.sis.referencing.operation.transform.MathTransforms;
+import org.apache.sis.metadata.iso.extent.DefaultGeographicBoundingBox;
+import org.apache.sis.math.MathFunctions;
+import org.apache.sis.measure.Units;
+import org.apache.sis.geometry.Envelopes;
+import org.apache.sis.geometry.GeneralEnvelope;
+import org.apache.sis.internal.referencing.Formulas;
+import org.apache.sis.referencing.operation.transform.LinearTransform;
+
+
+/**
+ * Contextual information for allowing {@link Canvas} to select the most appropriate
+ * coordinate operation for the viewed area and resolution.
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.1
+ * @since   1.1
+ * @module
+ */
+@SuppressWarnings("serial")   // Not intended to be serialized.
+final class CanvasContext extends CoordinateOperationContext {
+    /**
+     * Desired resolution in display units (usually pixels). This is used for avoiding
+     * the cost of transformations having too much accuracy for the current zoom level.
+     *
+     * @see Canvas#findTransform(CoordinateReferenceSystem, CoordinateReferenceSystem)
+     */
+    private static final double DISPLAY_RESOLUTION = 1;
+
+    /**
+     * Transformation from {@linkplain Canvas#getObjectiveCRS() objective CRS} to a geographic
CRS, or {@code null} if
+     * none can be found. The geographic CRS (operation target) has (longitude, latitude)
axes in degrees but the same
+     * geodetic datum than the objective CRS, so the prime meridian is not necessarily Greenwich.
This is recomputed
+     * immediately after a change of {@link Canvas#objectiveCRS} or {@link Canvas#pointOfInterest}
because it will
+     * be needed anyway for {@link Canvas#findTransform(CoordinateReferenceSystem, CoordinateReferenceSystem)}.
+     *
+     * @see Canvas#getGeographicArea()
+     * @see Canvas#objectiveToGeographic(CoordinateReferenceSystem)
+     */
+    private CoordinateOperation objectiveToGeographic;
+
+    /**
+     * Authalic radius of the geographic CRS which is the target of {@link #objectiveToGeographic},
in metres.
+     * This is computed in same time than {@link #objectiveToGeographic}.
+     */
+    private double radius;
+
+    /**
+     * The geographic area, computed when first requested and saved for reuse. This is reset
to {@code null} every time
+     * that {@link Canvas#objectiveCRS}, {@link Canvas#objectiveToDisplay} or {@link Canvas#displayBounds}
is modified.
+     *
+     * @see Canvas#GEOGRAPHIC_AREA_PROPERTY
+     * @see #getGeographicArea(Canvas)
+     */
+    private GeographicBoundingBox geographicArea;
+
+    /**
+     * The resolution in metres, computed when first requested and saved for reuse. This
is reset to {@code null}
+     * every time {@link Canvas#objectiveCRS}, {@link Canvas#objectiveToDisplay} or {@link
Canvas#pointOfInterest}
+     * is modified. Value 0 means that the value has not yet been computed.
+     *
+     * @see Canvas#SPATIAL_RESOLUTION_PROPERTY
+     * @see #getSpatialResolution(Canvas)
+     */
+    private double resolution;
+
+    /**
+     * Creates a new context.
+     */
+    CanvasContext() {
+    }
+
+    /**
+     * Sets the operation from {@link Canvas#objectiveCRS} to geographic CRS.
+     */
+    final void setObjectiveToGeographic(final CoordinateOperation op) {
+        final Ellipsoid ellipsoid = ((GeographicCRS) op.getTargetCRS()).getDatum().getEllipsoid();
+        radius = ellipsoid.getAxisUnit().getConverterTo(Units.METRE).convert(Formulas.getAuthalicRadius(ellipsoid));
+        objectiveToGeographic = op;             // Set only if above line succeeded.
+        clear();
+    }
+
+    /**
+     * Clears information that depends on {@link Canvas#objectiveToDisplay}.
+     * This method assumes that {@link Canvas#objectiveCRS} is still valid.
+     */
+    final void clear() {
+        geographicArea = null;
+        resolution     = 0;
+    }
+
+    /**
+     * Clears only some information, depending on whether the modified property is point
of interest
+     * or the display bounds.
+     *
+     * @param  poi  {@code true} if the modified property is the point of interest,
+     *              {@code false} if the modified property is the display bounds.
+     */
+    final void partialClear(final boolean poi) {
+        if (poi) resolution = 0;
+        else geographicArea = null;
+    }
+
+    /**
+     * Returns the geographic area, or an empty value if none.
+     *
+     * @see Canvas#getGeographicArea()
+     */
+    final Optional<GeographicBoundingBox> getGeographicArea(final Canvas canvas) throws
TransformException {
+        if (geographicArea != null) {
+            return Optional.of(geographicArea);
+        }
+        recompute(canvas);
+        return Optional.ofNullable(geographicArea);
+    }
+
+    /**
+     * Returns the spatial resolution, or an empty value if none.
+     *
+     * @see Canvas#getSpatialResolution()
+     */
+    final OptionalDouble getSpatialResolution(final Canvas canvas) throws TransformException
{
+        if (!(resolution > 0)) {
+            recompute(canvas);
+            if (!(resolution > 0)) {
+                return OptionalDouble.empty();
+            }
+        }
+        return OptionalDouble.of(resolution);
+    }
+
+    /**
+     * Recomputes {@link #geographicArea} and {@link #resolution} fields that are not valid.
+     * This method assumes that {@link #objectiveToGeographic} is valid.
+     */
+    @SuppressWarnings("fallthrough")
+    private void recompute(final Canvas canvas) throws TransformException {
+        final LinearTransform objectiveToDisplay = canvas.getObjectiveToDisplay();
+        final MathTransform displayToGeographic = MathTransforms.concatenate(
+                            objectiveToDisplay.inverse(),
+                            objectiveToGeographic.getMathTransform());
+        /*
+         * Compute geographic area using an operation going directly from display CRS
+         * to geographic CRS (do not go to objective CRS as an intermediate step,
+         * because doing 2 envelope transformations increases the errors).
+         */
+        if (geographicArea == null && !canvas.displayBounds.isAllNaN()) {
+            final GeneralEnvelope bounds = Envelopes.transform(displayToGeographic, canvas.displayBounds);
+            bounds.setCoordinateReferenceSystem(objectiveToGeographic.getTargetCRS());
+            final DefaultGeographicBoundingBox bbox = new DefaultGeographicBoundingBox();
+            bbox.setBounds(bounds);     // Will perform longitude rotation to Greenwich if
needed.
+            bbox.transitionTo(DefaultGeographicBoundingBox.State.FINAL);
+            geographicArea = bbox;
+        }
+        /*
+         * Estimate spatial resolution at the point of interest. The calculation is done
in
+         * (longitude, latitude, height) space where the height is optional. The angles are
+         * converted to meters using the authalic radius.
+         */
+        if (!(resolution > 0)) {
+            final double[] poi = canvas.getObjectivePOI();
+            objectiveToDisplay.transform(poi, 0, poi, 0, 1);
+            final Matrix derivative = MathTransforms.derivativeAndTransform(displayToGeographic,
poi, 0, poi, 1);
+            final double[] vector   = new double[derivative.getNumCol()];
+            final double[] combined = new double[derivative.getNumRow()];
+            for (int j=0; j<combined.length; j++) {
+                for (int i=0; i<vector.length; i++) {
+                    vector[i] = derivative.getElement(j,i);
+                }
+                double m = MathFunctions.magnitude(vector);
+                switch (j) {
+                    case 0: m *= Math.cos(Math.toRadians(poi[1]));      // Adjust longitude,
then fall through.
+                    case 1: m  = Math.toRadians(m) * radius; break;     // Latitude (this
case) or Longitude (case 0).
+                    // Other cases: assume value already in metres.
+                }
+                combined[j] = m;
+            }
+            resolution = MathFunctions.magnitude(combined) * radius;
+        }
+    }
+
+    /**
+     * Sets the {@link CoordinateOperationContext} object to the desired area and accuracy
+     * of the coordinate operation to obtain.
+     */
+    final void refresh() {
+        setAreaOfInterest(geographicArea);                          // null for default behavior.
+        setDesiredAccuracy(resolution * DISPLAY_RESOLUTION);        // 0 for default behavior.
+    }
+}
diff --git a/core/sis-portrayal/src/main/java/org/apache/sis/internal/map/PlanarCanvas.java
b/core/sis-portrayal/src/main/java/org/apache/sis/internal/map/PlanarCanvas.java
index e7e8ca2..ec0eed8 100644
--- a/core/sis-portrayal/src/main/java/org/apache/sis/internal/map/PlanarCanvas.java
+++ b/core/sis-portrayal/src/main/java/org/apache/sis/internal/map/PlanarCanvas.java
@@ -116,7 +116,7 @@ public abstract class PlanarCanvas extends Canvas {
      */
     @Override
     final DirectPosition allocatePosition() {
-        return new DirectPosition2D();
+        return new DirectPosition2D(super.getObjectiveCRS());
     }
 
     /**
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/ReferencingUtilities.java
b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/ReferencingUtilities.java
index 7e6489a..35e6745 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/ReferencingUtilities.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/ReferencingUtilities.java
@@ -283,8 +283,8 @@ public final class ReferencingUtilities extends Static {
     }
 
     /**
-     * Derives a geographic CRS with (<var>longitude</var>, <var>latitude</var>)
axis in the specified order and in decimal degrees.
-     * If no such CRS can be obtained or created, returns {@code null}.
+     * Derives a geographic CRS with (<var>longitude</var>, <var>latitude</var>)
axis in the specified
+     * order and in decimal degrees. If no such CRS can be obtained or created, returns {@code
null}.
      *
      * <p>This method does not set the prime meridian to Greenwich.
      * Meridian rotation, if needed, shall be performed by the caller.</p>


Mime
View raw message