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: Improve the support of coordinate display when the image shown on screen does not have an affine transform to a known geospatial CRS. This is the case of netCDF files when the coordinates of each pixel is specified in data arrays (localization grid). This work required the addition of CommonCRS.Engineering enumeration with DISPLAY and GRID values, for making easier to detect when we have such grid CRS. For now we use the EngineeringDatum of CommonCRS.Engineering.GRID as a sentinel value fo [...]
Date Sun, 03 May 2020 23:47:39 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 19abdd5  Improve the support of coordinate display when the image shown on screen does not have an affine transform to a known geospatial CRS. This is the case of netCDF files when the coordinates of each pixel is specified in data arrays (localization grid). This work required the addition of CommonCRS.Engineering enumeration with DISPLAY and GRID values, for making easier to detect when we have such grid CRS. For now we use the EngineeringDatum of CommonCRS.Engineering.GRID as  [...]
19abdd5 is described below

commit 19abdd5a5caefeaf25dc212569ff664ab46118e3
Author: Martin Desruisseaux <martin.desruisseaux@geomatys.com>
AuthorDate: Mon May 4 01:44:32 2020 +0200

    Improve the support of coordinate display when the image shown on screen does not have an affine transform to a known geospatial CRS.
    This is the case of netCDF files when the coordinates of each pixel is specified in data arrays (localization grid).
    This work required the addition of CommonCRS.Engineering enumeration with DISPLAY and GRID values, for making easier to detect when we have such grid CRS.
    For now we use the EngineeringDatum of CommonCRS.Engineering.GRID as a sentinel value for identifying grid CRS.
---
 .../org/apache/sis/gui/coverage/ImageRequest.java  |   5 +-
 .../java/org/apache/sis/gui/map/MapCanvas.java     |  31 +--
 .../org/apache/sis/gui/map/OperationFinder.java    | 238 +++++++++++++++++++++
 .../java/org/apache/sis/gui/map/StatusBar.java     | 109 ++++++----
 .../apache/sis/coverage/grid/GridExtentCRS.java    |  11 +-
 .../org/apache/sis/portrayal/PlanarCanvas.java     |  25 +--
 .../java/org/apache/sis/referencing/CommonCRS.java | 142 +++++++++++-
 .../factory/CommonAuthorityFactory.java            |  38 +---
 .../java/org/apache/sis/internal/jdk9/JDK9.java    |  13 ++
 9 files changed, 483 insertions(+), 129 deletions(-)

diff --git a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/ImageRequest.java b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/ImageRequest.java
index 17df35b..3ee0826 100644
--- a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/ImageRequest.java
+++ b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/ImageRequest.java
@@ -218,8 +218,9 @@ public class ImageRequest {
         /*
          * By `GridCoverage.render(GridExtent)` contract, the `RenderedImage` pixel coordinates are relative
          * to the requested `GridExtent`. Consequently we need to translate the image coordinates so that it
-         * become the coordinates of the original `GridGeometry` before to apply `gridToCRS`. It is okay to
-         * modify `StatusBar.localToObjectiveCRS` because we do not associate it to a `MapCanvas`.
+         * become the coordinates of the original `GridGeometry` before to apply `gridToCRS`.  It is okay to
+         * modify `StatusBar.localToObjectiveCRS` because we do not associate it to a `MapCanvas`, so it will
+         * not be overwritten by gesture events (zoom, pan, etc).
          */
         if (request != null) {
             final double[] origin = new double[request.getDimension()];
diff --git a/application/sis-javafx/src/main/java/org/apache/sis/gui/map/MapCanvas.java b/application/sis-javafx/src/main/java/org/apache/sis/gui/map/MapCanvas.java
index db377f0..e3ed2b0 100644
--- a/application/sis-javafx/src/main/java/org/apache/sis/gui/map/MapCanvas.java
+++ b/application/sis-javafx/src/main/java/org/apache/sis/gui/map/MapCanvas.java
@@ -43,7 +43,7 @@ import javafx.scene.transform.NonInvertibleTransformException;
 import org.opengis.geometry.Envelope;
 import org.opengis.referencing.datum.PixelInCell;
 import org.opengis.referencing.crs.CoordinateReferenceSystem;
-import org.opengis.referencing.operation.NoninvertibleTransformException;
+import org.opengis.referencing.operation.TransformException;
 import org.apache.sis.referencing.operation.matrix.Matrices;
 import org.apache.sis.referencing.operation.matrix.MatrixSIS;
 import org.apache.sis.referencing.operation.transform.MathTransforms;
@@ -666,29 +666,30 @@ public abstract class MapCanvas extends PlanarCanvas {
              */
             if (invalidObjectiveToDisplay) {
                 invalidObjectiveToDisplay = false;
-                final LinearTransform tr;
-                CoordinateReferenceSystem crs;
                 final Envelope2D target = getDisplayBounds();
+                final GridExtent extent = new GridExtent(null,
+                        new long[] {Math.round(target.getMinX()), Math.round(target.getMinY())},
+                        new long[] {Math.round(target.getMaxX()), Math.round(target.getMaxY())}, false);
+
+                final LinearTransform crsToDisplay;
+                CoordinateReferenceSystem crs;
                 if (objectiveBounds != null) {
-                    crs = objectiveBounds.getCoordinateReferenceSystem();
                     final MatrixSIS m = Matrices.createTransform(objectiveBounds, target);
                     Matrices.forceUniformScale(m, 0, new double[] {target.width / 2, target.height / 2});
-                    tr = MathTransforms.linear(m);
+                    crsToDisplay = MathTransforms.linear(m);
+                    crs = objectiveBounds.getCoordinateReferenceSystem();
+                    if (crs == null) {
+                        crs = extent.toEnvelope(crsToDisplay.inverse()).getCoordinateReferenceSystem();
+                        // CRS computed above should not be null.
+                    }
                 } else {
-                    tr = MathTransforms.identity(BIDIMENSIONAL);
-                    crs = null;
-                }
-                if (crs == null) {
-                    // TODO: build an EngineeringCRS reflecting better the data.
+                    crsToDisplay = MathTransforms.identity(BIDIMENSIONAL);
                     crs = getDisplayCRS();
                 }
-                final GridExtent extent = new GridExtent(null,
-                        new long[] {Math.round(target.getMinX()), Math.round(target.getMinY())},
-                        new long[] {Math.round(target.getMaxX()), Math.round(target.getMaxY())}, false);
-                setGridGeometry(new GridGeometry(extent, PixelInCell.CELL_CORNER, tr.inverse(), crs));
+                setGridGeometry(new GridGeometry(extent, PixelInCell.CELL_CORNER, crsToDisplay.inverse(), crs));
                 transform.setToIdentity();
             }
-        } catch (NoninvertibleTransformException | RenderException ex) {
+        } catch (TransformException | RenderException ex) {
             floatingPane.setCursor(Cursor.CROSSHAIR);
             errorOccurred(ex);
             return;
diff --git a/application/sis-javafx/src/main/java/org/apache/sis/gui/map/OperationFinder.java b/application/sis-javafx/src/main/java/org/apache/sis/gui/map/OperationFinder.java
new file mode 100644
index 0000000..95e6552
--- /dev/null
+++ b/application/sis-javafx/src/main/java/org/apache/sis/gui/map/OperationFinder.java
@@ -0,0 +1,238 @@
+/*
+ * 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.gui.map;
+
+import java.util.function.Predicate;
+import javafx.beans.property.ReadOnlyProperty;
+import javafx.concurrent.Task;
+import org.opengis.geometry.Envelope;
+import org.opengis.referencing.crs.CoordinateReferenceSystem;
+import org.opengis.referencing.crs.SingleCRS;
+import org.opengis.referencing.datum.PixelInCell;
+import org.opengis.referencing.operation.MathTransform;
+import org.opengis.referencing.operation.CoordinateOperation;
+import org.opengis.referencing.operation.TransformException;
+import org.apache.sis.referencing.CRS;
+import org.apache.sis.referencing.CommonCRS;
+import org.apache.sis.coverage.grid.GridCoverage;
+import org.apache.sis.coverage.grid.GridGeometry;
+import org.apache.sis.gui.coverage.CoverageCanvas;
+import org.apache.sis.metadata.iso.extent.DefaultGeographicBoundingBox;
+import org.apache.sis.referencing.operation.transform.MathTransforms;
+import org.apache.sis.util.logging.Logging;
+import org.apache.sis.internal.system.Modules;
+
+
+/**
+ * Finds a coordinate operation between two CRS in the context of a {@link MapCanvas}.
+ * The operation may depend on the region visible in the canvas and the resolution.
+ * Computing the coordinate operation may be costly, and for this reason should be
+ * done in a background thread.
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.1
+ * @since   1.1
+ * @module
+ */
+abstract class OperationFinder extends Task<MathTransform> {
+    /**
+     * The grid geometry of data, or {@code null} if none or unknown. This is used for getting the operation between
+     * two CRSs when one of the source or target CRS is {@link org.apache.sis.referencing.CommonCRS.Engineering#GRID}.
+     * Because the relationship from {@code GRID} CRS to a geospatial CRS is unknown to {@code CRS.findOperation(…)},
+     * the operation can not be found without the help of this {@code dataGeometry} field.
+     *
+     * Actually this information is rarely needed. It is needed only if there is no known affine transform from grid
+     * coordinates to some geospatial coordinates that we can use as a starting point (before to apply map projection
+     * to other CRSs if desired). For example some netCDF files provides the coordinates of each pixel in data arrays.
+     * Those data arrays can be stored (indirectly) in this {@code dataGeometry} object.
+     */
+    private final GridGeometry dataGeometry;
+
+    /**
+     * Region visible in the map canvas, or {@code null} if none. May be in any CRS.
+     */
+    private final Envelope areaOfInterest;
+
+    /**
+     * The source and target CRS requested by the user. This is usually the {@link #operation} source
+     * and target CRS, unless a CRS was a {@linkplain #isGridCRS(CoordinateReferenceSystem) grid CRS}.
+     */
+    private final CoordinateReferenceSystem sourceCRS, targetCRS;
+
+    /**
+     * {@code true} if {@link #sourceCRS} or {@link #targetCRS} is a
+     * {@linkplain #isGridCRS(CoordinateReferenceSystem) grid CRS}.
+     */
+    private boolean sourceIsGrid, targetIsGrid;
+
+    /**
+     * The coordinate operation from {@link #sourceCRS} to {@link #targetCRS}, computed in background thread.
+     * The {@link CoordinateOperation#getMathTransform()} value may not be the complete transform returned by
+     * {@link #getValue()} because the later may include transform from/to {@linkplain #isGridCRS grid CRS}.
+     */
+    private CoordinateOperation operation;
+
+    /**
+     * Creates a new task for finding the coordinate operation between two CRS.
+     *
+     * @param canvas          the canvas for which we are searching a coordinate operation, or {@code null}.
+     * @param areaOfInterest  region visible in the map canvas, or {@code null}. May be in any CRS.
+     * @param sourceCRS       source CRS of the transform to compute.
+     * @param targetCRS       target CRS of the transform to compute.
+     */
+    protected OperationFinder(final MapCanvas canvas,
+                              final Envelope  areaOfInterest,
+                              final CoordinateReferenceSystem sourceCRS,
+                              final CoordinateReferenceSystem targetCRS)
+    {
+        this.dataGeometry   = dataGeometry(canvas);
+        this.sourceCRS      = sourceCRS;
+        this.targetCRS      = targetCRS;
+        this.areaOfInterest = areaOfInterest;
+    }
+
+    /**
+     * Returns the <em>data</em> (not canvas) grid geometry, or {@code null} if none.
+     */
+    private static GridGeometry dataGeometry(final MapCanvas canvas) {
+        if (canvas instanceof CoverageCanvas) {
+            final GridCoverage coverage = ((CoverageCanvas) canvas).getCoverage();
+            if (coverage != null) {
+                return coverage.getGridGeometry();
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Computes the transform from the source CRS to the target CRS specified at construction time.
+     * This method is invoked in a background thread and does not need to be invoked explicitly.
+     */
+    @Override
+    protected MathTransform call() throws Exception {
+        DefaultGeographicBoundingBox bbox = null;
+        if (areaOfInterest != null) try {
+            bbox = new DefaultGeographicBoundingBox();
+            bbox.setBounds(areaOfInterest);
+        } catch (TransformException e) {
+            bbox = null;
+            Logging.recoverableException(Logging.getLogger(Modules.APPLICATION), getCallerClass(), getCallerMethod(), e);
+        }
+        MathTransform before = null;
+        MathTransform after  = null;
+        CoordinateReferenceSystem source = sourceCRS;
+        CoordinateReferenceSystem target = targetCRS;
+        if (dataGeometry != null && dataGeometry.isDefined(GridGeometry.CRS | GridGeometry.GRID_TO_CRS)) {
+            if (sourceIsGrid = isGridCRS(source)) {
+                before = dataGeometry.getGridToCRS(PixelInCell.CELL_CENTER);
+                source = dataGeometry.getCoordinateReferenceSystem();
+            }
+            if (targetIsGrid = isGridCRS(target)) {
+                after  = dataGeometry.getGridToCRS(PixelInCell.CELL_CENTER).inverse();
+                target = dataGeometry.getCoordinateReferenceSystem();
+            }
+        }
+        operation = CRS.findOperation(source, target, bbox);
+        MathTransform transform = operation.getMathTransform();
+        if (before != null) transform = MathTransforms.concatenate(before, transform);
+        if (after  != null) transform = MathTransforms.concatenate(transform, after);
+        return transform;
+    }
+
+    /**
+     * If the given CRS is a grid CRS, replaces it by a geospatial CRS if possible.
+     */
+    static CoordinateReferenceSystem toGeospatial(CoordinateReferenceSystem crs, final ReadOnlyProperty<MapCanvas> canvas) {
+        if (isGridCRS(crs)) {
+            final GridGeometry dataGeometry = dataGeometry(canvas.getValue());
+            if (dataGeometry != null && dataGeometry.isDefined(GridGeometry.CRS)) {
+                return dataGeometry.getCoordinateReferenceSystem();
+            }
+        }
+        return crs;
+    }
+
+    /**
+     * Returns {@code true} if the given coordinate reference system is the CRS of the grid.
+     * We use the {@link org.apache.sis.referencing.CommonCRS.Engineering#GRID} datum as a signature.
+     */
+    private static boolean isGridCRS(final CoordinateReferenceSystem crs) {
+        return (crs instanceof SingleCRS) && CommonCRS.Engineering.GRID.datum().equals(((SingleCRS) crs).getDatum());
+    }
+
+    /**
+     * Returns the coordinate operation computed by the {@link #call()} method. The associated transform can be
+     * obtained by {@link #getValue()}. The {@link CoordinateOperation#getMathTransform()} method should not be
+     * used because it may be incomplete if the source or target CRS was a grid CRS.
+     */
+    public final CoordinateOperation getOperation() {
+        return operation;
+    }
+
+    /**
+     * Returns the target CRS, giving precedence to {@link CoordinateOperation#getTargetCRS()} is suitable.
+     * That precedence is because the {@link CoordinateOperation} may provide a more complete CRS from EPSG
+     * database.
+     */
+    public final CoordinateReferenceSystem getTargetCRS() {
+        if (!targetIsGrid) {
+            final CoordinateReferenceSystem crs = operation.getTargetCRS();
+            if (crs != null) return crs;
+        }
+        return targetCRS;
+    }
+
+    /**
+     * Returns a predicate for determining if {@link OperationFinder} task need to be executed again.
+     * If there is no need to perform such check, returns {@code null}.
+     *
+     * <p><b>Note:</b> actually recomputing everything is a bit overly aggresive.  We could keep the
+     * {@link CoordinateOperation} found by {@link #call()} and just update the {@link MathTransform}
+     * before or after the operation. But the use of a grid CRS should be rare enough that it is not
+     * worth to do this optimization.</p>
+     *
+     * @see StatusBar#fullOperationSearchRequired
+     */
+    final Predicate<MapCanvas> fullOperationSearchRequired() {
+        return (sourceIsGrid | targetIsGrid) ? new UpdateCheck(dataGeometry) : null;
+    }
+
+    /**
+     * The predicate for determining if {@link OperationFinder} task needs to be executed again.
+     */
+    private static final class UpdateCheck implements Predicate<MapCanvas> {
+        private final GridGeometry dataGeometry;
+
+        UpdateCheck(final GridGeometry dataGeometry) {
+            this.dataGeometry = dataGeometry;
+        }
+
+        @Override public boolean test(final MapCanvas canvas) {
+            return !dataGeometry.equals(dataGeometry(canvas));
+        }
+    }
+
+    /**
+     * Returns the class to report as the caller in case of non-fatal error. This is used for logging.
+     */
+    protected abstract Class<?> getCallerClass();
+
+    /**
+     * Returns the method to report as the caller in case of non-fatal error. This is used for logging.
+     */
+    protected abstract String getCallerMethod();
+}
diff --git a/application/sis-javafx/src/main/java/org/apache/sis/gui/map/StatusBar.java b/application/sis-javafx/src/main/java/org/apache/sis/gui/map/StatusBar.java
index 75e5d65..afc538a 100644
--- a/application/sis-javafx/src/main/java/org/apache/sis/gui/map/StatusBar.java
+++ b/application/sis-javafx/src/main/java/org/apache/sis/gui/map/StatusBar.java
@@ -18,6 +18,7 @@ package org.apache.sis.gui.map;
 
 import java.util.Locale;
 import java.util.Optional;
+import java.util.function.Predicate;
 import javax.measure.Unit;
 import javafx.geometry.Pos;
 import javafx.geometry.Insets;
@@ -43,11 +44,9 @@ import javafx.beans.property.ReadOnlyObjectPropertyBase;
 import javafx.beans.property.SimpleObjectProperty;
 import javafx.beans.value.ChangeListener;
 import javafx.collections.ListChangeListener;
-import javafx.concurrent.Task;
 import javax.measure.quantity.Length;
 import org.opengis.geometry.Envelope;
 import org.opengis.geometry.MismatchedDimensionException;
-import org.opengis.util.FactoryException;
 import org.opengis.referencing.ReferenceSystem;
 import org.opengis.referencing.datum.PixelInCell;
 import org.opengis.referencing.cs.CoordinateSystem;
@@ -58,7 +57,6 @@ import org.opengis.referencing.operation.TransformException;
 import org.opengis.referencing.operation.CoordinateOperation;
 import org.opengis.referencing.crs.CoordinateReferenceSystem;
 import org.apache.sis.referencing.operation.transform.MathTransforms;
-import org.apache.sis.metadata.iso.extent.DefaultGeographicBoundingBox;
 import org.apache.sis.geometry.GeneralDirectPosition;
 import org.apache.sis.geometry.CoordinateFormat;
 import org.apache.sis.coverage.grid.GridGeometry;
@@ -72,7 +70,6 @@ import org.apache.sis.util.Utilities;
 import org.apache.sis.util.Exceptions;
 import org.apache.sis.util.ArgumentChecks;
 import org.apache.sis.util.ComparisonMode;
-import org.apache.sis.util.logging.Logging;
 import org.apache.sis.util.resources.Errors;
 import org.apache.sis.gui.Widget;
 import org.apache.sis.gui.referencing.RecentReferenceSystems;
@@ -81,7 +78,6 @@ import org.apache.sis.internal.gui.BackgroundThreads;
 import org.apache.sis.internal.gui.ExceptionReporter;
 import org.apache.sis.internal.gui.Resources;
 import org.apache.sis.internal.gui.Styles;
-import org.apache.sis.internal.system.Modules;
 import org.apache.sis.referencing.CRS;
 import org.apache.sis.referencing.IdentifiedObjects;
 
@@ -194,7 +190,7 @@ public class StatusBar extends Widget implements EventHandler<MouseEvent> {
      *
      * @see #updateLocalToPositionCRS()
      */
-    private CoordinateOperation objectiveToPositionCRS;
+    private MathTransform objectiveToPositionCRS;
 
     /**
      * Conversion from local coordinates to geographic or projected coordinates of rendered data.
@@ -247,10 +243,22 @@ public class StatusBar extends Widget implements EventHandler<MouseEvent> {
      *
      * <p>The target CRS can be obtained by {@link CoordinateOperation#getTargetCRS()} on
      * {@link #objectiveToPositionCRS} or by {@link CoordinateFormat#getDefaultCRS()}.</p>
+     *
+     * @see #updateLocalToPositionCRS()
      */
     private MathTransform localToPositionCRS;
 
     /**
+     * If non-null, determines if {@link #apply(GridGeometry)} needs to update {@link #localToPositionCRS} with a
+     * potentially costly search for coordinate operation even in context where it would normally not be required.
+     * An explanation of the context when it may happen is given in {@link OperationFinder#dataGeometry}.
+     * This is rarely needed for most data (i.e. this field is almost always {@code null}).
+     *
+     * @see OperationFinder#fullOperationSearchRequired()
+     */
+    private Predicate<MapCanvas> fullOperationSearchRequired;
+
+    /**
      * The source local indices before conversion to geospatial coordinates.
      * The number of dimensions is often {@value #BIDIMENSIONAL}.
      * Shall never be {@code null}.
@@ -434,11 +442,18 @@ public class StatusBar extends Widget implements EventHandler<MouseEvent> {
         }
         position.setText(null);
         registerMouseListeners(value);
-        try {
-            apply(value != null ? value.getGridGeometry() : null);
+        /*
+         * Configure this status bar for showing coordinates in the CRS and with the resolution given by
+         * the canvas grid geometry. This is the same operation than the one executed every time that a
+         * new rendering occurred.
+         */
+        GridGeometry geometry = null;
+        if (value != null) try {
+            geometry = value.getGridGeometry();
         } catch (RenderException e) {
-            setErrorMessage(null, e);
+            setRenderingError(e);
         }
+        applyCanvasGeometry(geometry);
     }
 
     /**
@@ -596,10 +611,14 @@ public class StatusBar extends Widget implements EventHandler<MouseEvent> {
         if (sameCRS) {
             updateLocalToPositionCRS();
             // Keep the format CRS unchanged since we made `localToPositionCRS` consistent with its value.
+            if (fullOperationSearchRequired != null && fullOperationSearchRequired.test(canvas.get())) {
+                setPositionCRS(format.getDefaultCRS());
+            }
         } else {
             objectiveToPositionCRS = null;
             setFormatCRS(crs, null);                                // Should be invoked before to set precision.
-            crs = setReplaceablePositionCRS(crs);                   // May invoke later setFormatCRS(…) again.
+            crs = OperationFinder.toGeospatial(crs, canvas);
+            crs = setReplaceablePositionCRS(crs);                   // May invoke setFormatCRS(…) after background work.
         }
         format.setGroundPrecision(Quantities.create(resolution, unit));
         /*
@@ -627,8 +646,7 @@ public class StatusBar extends Widget implements EventHandler<MouseEvent> {
      */
     private void updateLocalToPositionCRS() {
         if (objectiveToPositionCRS != null) {
-            localToPositionCRS = MathTransforms.concatenate(
-                    localToObjectiveCRS.get(), objectiveToPositionCRS.getMathTransform());
+            localToPositionCRS = MathTransforms.concatenate(localToObjectiveCRS.get(), objectiveToPositionCRS);
         }
     }
 
@@ -672,34 +690,43 @@ public class StatusBar extends Widget implements EventHandler<MouseEvent> {
     private void setPositionCRS(final CoordinateReferenceSystem crs) {
         if (crs != null && objectiveCRS != null && objectiveCRS != crs) {
             position.setTextFill(Styles.OUTDATED_TEXT);
+            /*
+             * Take snapshots of references to all objects that the background thread will use.
+             * The background thread shall not read StatusBar fields directly since they may be
+             * in the middle of changes at any time. All objects are assumed immutable.
+             */
             final Envelope aoi = (systemChooser != null) ? systemChooser.areaOfInterest.get() : null;
-            BackgroundThreads.execute(new Task<CoordinateOperation>() {
+            BackgroundThreads.execute(new OperationFinder(canvas.get(), aoi, objectiveCRS, crs) {
+                /**
+                 * The accuracy to show on the status bar, or {@code null} if none.
+                 * This is computed after {@link CoordinateOperation} has been determined.
+                 */
+                private Length accuracy;
+
                 /**
                  * Invoked in a background thread for fetching transformation to target CRS.
-                 * The potentially costly part is {@code CRS.findOperation(…)}.
+                 * The potentially costly part is {@code CRS.findOperation(…)} in super.call().
                  */
-                @Override protected CoordinateOperation call() throws FactoryException {
-                    DefaultGeographicBoundingBox bbox = null;
-                    if (aoi != null) try {
-                        bbox = new DefaultGeographicBoundingBox();
-                        bbox.setBounds(aoi);
-                    } catch (TransformException e) {
-                        bbox = null;
-                        Logging.recoverableException(Logging.getLogger(Modules.APPLICATION),
-                                StatusBar.class, "setPositionCRS", e);
+                @Override protected MathTransform call() throws Exception {
+                    final MathTransform value = super.call();
+                    double a = CRS.getLinearAccuracy(getOperation());
+                    if (a > 0) {
+                        final Unit<Length> unit;
+                        if      (a < 1)    unit = Units.CENTIMETRE;
+                        else if (a < 1000) unit = Units.METRE;
+                        else               unit = Units.KILOMETRE;
+                        a = Units.METRE.getConverterTo(unit).convert(Math.max(a, Formulas.LINEAR_TOLERANCE));
+                        accuracy = Quantities.create(a, unit);
                     }
-                    return CRS.findOperation(objectiveCRS, crs, bbox);
+                    return value;
                 }
-
                 /**
                  * Invoked in JavaFX thread on success. The {@link StatusBar#localToPositionCRS} transform
                  * is set to the transform that we computed in background and the {@link CoordinateFormat}
                  * is configured with auxiliary information such as positional accuracy.
                  */
                 @Override protected void succeeded() {
-                    final CoordinateOperation operation = getValue();
-                    final CoordinateReferenceSystem targetCRS = operation.getTargetCRS();
-                    setPositionCRS(targetCRS != null ? targetCRS : crs, operation);
+                    setPositionCRS(this, accuracy);
                 }
 
                 /**
@@ -713,6 +740,10 @@ public class StatusBar extends Widget implements EventHandler<MouseEvent> {
                     selectedSystem.set(format.getDefaultCRS());
                     resetPositionCRS(Styles.ERROR_TEXT);
                 }
+
+                /** For logging purpose if a non-fatal error occurs. */
+                @Override protected Class<?> getCallerClass()  {return StatusBar.class;}
+                @Override protected String   getCallerMethod() {return "setPositionCRS";}
             });
         } else {
             /*
@@ -739,27 +770,17 @@ public class StatusBar extends Widget implements EventHandler<MouseEvent> {
      * and {@link #lastY} are still valid. This assumption should be correct when
      * only the format CRS has been updated and not {@link #localToObjectiveCRS}.
      *
-     * @param  crs        the new CRS. Should not be {@code null}.
-     * @param  operation  the new value to assign to {@link #objectiveToPositionCRS}
+     * @param  finder    the completed task with the new {@link #objectiveToPositionCRS}.
+     * @param  accuracy  the accuracy to show on the status bar, or {@code null} if none.
      */
-    private void setPositionCRS(final CoordinateReferenceSystem crs, final CoordinateOperation operation) {
+    private void setPositionCRS(final OperationFinder finder, final Length accuracy) {
         setErrorMessage(null, null);
-        Length accuracy = null;
-        double a = CRS.getLinearAccuracy(operation);
-        if (a > 0) {
-            final Unit<Length> unit;
-            if      (a < 1)    unit = Units.CENTIMETRE;
-            else if (a < 1000) unit = Units.METRE;
-            else               unit = Units.KILOMETRE;
-            a = Units.METRE.getConverterTo(unit).convert(Math.max(a, Formulas.LINEAR_TOLERANCE));
-            accuracy = Quantities.create(a, unit);
-        }
-        setFormatCRS(crs, accuracy);
-        objectiveToPositionCRS = operation;
+        setFormatCRS(finder.getTargetCRS(), accuracy);
+        objectiveToPositionCRS = finder.getValue();
+        fullOperationSearchRequired = finder.fullOperationSearchRequired();
         updateLocalToPositionCRS();
         position.setTextFill(Styles.NORMAL_TEXT);
         position.setMinWidth(0);
-        setErrorMessage(null, null);
         if (isPositionVisible()) {
             final double x = lastX;
             final double y = lastY;
diff --git a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridExtentCRS.java b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridExtentCRS.java
index 597e7b8..dbd5b26 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridExtentCRS.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridExtentCRS.java
@@ -28,15 +28,14 @@ import org.opengis.referencing.cs.CoordinateSystemAxis;
 import org.opengis.referencing.crs.CRSFactory;
 import org.opengis.referencing.crs.EngineeringCRS;
 import org.opengis.referencing.crs.CoordinateReferenceSystem;
-import org.opengis.referencing.datum.EngineeringDatum;
 import org.opengis.referencing.operation.Matrix;
 import org.apache.sis.referencing.cs.AbstractCS;
+import org.apache.sis.referencing.CommonCRS;
 import org.apache.sis.internal.system.DefaultFactories;
 import org.apache.sis.internal.referencing.AxisDirections;
 import org.apache.sis.util.resources.Vocabulary;
 import org.apache.sis.util.iso.Types;
 import org.apache.sis.measure.Units;
-import org.apache.sis.referencing.datum.DefaultEngineeringDatum;
 import org.apache.sis.util.Characters;
 
 
@@ -53,11 +52,6 @@ import org.apache.sis.util.Characters;
  */
 final class GridExtentCRS {
     /**
-     * The datum for grid.
-     */
-    private static final EngineeringDatum DATUM = new DefaultEngineeringDatum(properties("Grid"));
-
-    /**
      * Do not allow instantiation of this class.
      */
     private GridExtentCRS() {
@@ -191,6 +185,7 @@ final class GridExtentCRS {
             case 3:  cs = csFactory.createAffineCS(properties, axes[0], axes[1], axes[2]); break;
             default: cs = new AbstractCS(properties, axes); break;
         }
-        return DefaultFactories.forBuildin(CRSFactory.class).createEngineeringCRS(properties(cs.getName()), DATUM, cs);
+        return DefaultFactories.forBuildin(CRSFactory.class).createEngineeringCRS(
+                properties(cs.getName()), CommonCRS.Engineering.GRID.datum(), cs);
     }
 }
diff --git a/core/sis-portrayal/src/main/java/org/apache/sis/portrayal/PlanarCanvas.java b/core/sis-portrayal/src/main/java/org/apache/sis/portrayal/PlanarCanvas.java
index ca558e4..ffac6e0 100644
--- a/core/sis-portrayal/src/main/java/org/apache/sis/portrayal/PlanarCanvas.java
+++ b/core/sis-portrayal/src/main/java/org/apache/sis/portrayal/PlanarCanvas.java
@@ -16,27 +16,19 @@
  */
 package org.apache.sis.portrayal;
 
-import java.util.Map;
 import java.util.Locale;
 import java.awt.geom.AffineTransform;
 import org.opengis.geometry.Envelope;
 import org.opengis.geometry.DirectPosition;
-import org.opengis.referencing.cs.AxisDirection;
 import org.opengis.metadata.spatial.DimensionNameType;
 import org.apache.sis.measure.Units;
 import org.apache.sis.geometry.Envelope2D;
 import org.apache.sis.geometry.DirectPosition2D;
-import org.apache.sis.referencing.cs.DefaultCartesianCS;
-import org.apache.sis.referencing.cs.DefaultCoordinateSystemAxis;
-import org.apache.sis.referencing.datum.DefaultEngineeringDatum;
-import org.apache.sis.referencing.crs.DefaultEngineeringCRS;
+import org.apache.sis.referencing.CommonCRS;
 import org.apache.sis.referencing.operation.matrix.AffineTransforms2D;
 import org.apache.sis.referencing.operation.transform.LinearTransform;
 import org.apache.sis.internal.referencing.j2d.AffineTransform2D;
 
-import static java.util.Collections.singletonMap;
-import static org.opengis.referencing.IdentifiedObject.NAME_KEY;
-
 
 /**
  * A canvas for two-dimensional display device using a Cartesian coordinate system.
@@ -60,19 +52,6 @@ public abstract class PlanarCanvas extends Canvas {
     protected static final int BIDIMENSIONAL = 2;
 
     /**
-     * The display Coordinate Reference System used by all {@code PlanarCanvas} instances.
-     */
-    private static final DefaultEngineeringCRS DISPLAY_CRS;
-    static {
-        Map<String,?> property = singletonMap(NAME_KEY, "Display on two-dimensional Cartesian coordinate system");
-        DefaultCartesianCS cs = new DefaultCartesianCS(property,
-                new DefaultCoordinateSystemAxis(singletonMap(NAME_KEY, "Column"), "x", AxisDirection.DISPLAY_RIGHT, Units.PIXEL),
-                new DefaultCoordinateSystemAxis(singletonMap(NAME_KEY, "Row"),    "y", AxisDirection.DISPLAY_DOWN,  Units.PIXEL));
-        property = singletonMap(NAME_KEY, cs.getName());        // Reuse the same Identifier instance.
-        DISPLAY_CRS = new DefaultEngineeringCRS(property, new DefaultEngineeringDatum(property), cs);
-    }
-
-    /**
      * The conversion from {@linkplain #getObjectiveCRS() objective CRS} to the display coordinate system as a
      * Java2D affine transform. This transform will be modified in-place when user applies zoom, translation or
      * rotation on the view area. Subclasses should generally not modify this affine transform directly; invoke
@@ -90,7 +69,7 @@ public abstract class PlanarCanvas extends Canvas {
      * @param  locale  the locale to use for labels and some messages, or {@code null} for default.
      */
     protected PlanarCanvas(final Locale locale) {
-        super(DISPLAY_CRS, locale);
+        super(CommonCRS.Engineering.DISPLAY.crs(), locale);
         objectiveToDisplay = new AffineTransform();
     }
 
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/referencing/CommonCRS.java b/core/sis-referencing/src/main/java/org/apache/sis/referencing/CommonCRS.java
index a8df281..f779f13 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/referencing/CommonCRS.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/referencing/CommonCRS.java
@@ -35,6 +35,7 @@ import org.opengis.referencing.crs.TemporalCRS;
 import org.opengis.referencing.crs.GeographicCRS;
 import org.opengis.referencing.crs.GeocentricCRS;
 import org.opengis.referencing.crs.ProjectedCRS;
+import org.opengis.referencing.crs.EngineeringCRS;
 import org.opengis.referencing.crs.SingleCRS;
 import org.opengis.referencing.cs.TimeCS;
 import org.opengis.referencing.cs.VerticalCS;
@@ -49,16 +50,20 @@ import org.opengis.referencing.datum.PrimeMeridian;
 import org.opengis.referencing.datum.VerticalDatum;
 import org.opengis.referencing.datum.VerticalDatumType;
 import org.opengis.referencing.datum.TemporalDatum;
+import org.opengis.referencing.datum.EngineeringDatum;
 import org.apache.sis.referencing.datum.DefaultVerticalDatum;
 import org.apache.sis.referencing.datum.DefaultTemporalDatum;
+import org.apache.sis.referencing.datum.DefaultEngineeringDatum;
 import org.apache.sis.referencing.cs.AxesConvention;
 import org.apache.sis.referencing.cs.DefaultTimeCS;
 import org.apache.sis.referencing.cs.DefaultVerticalCS;
+import org.apache.sis.referencing.cs.DefaultCartesianCS;
 import org.apache.sis.referencing.cs.DefaultCoordinateSystemAxis;
 import org.apache.sis.referencing.crs.DefaultTemporalCRS;
 import org.apache.sis.referencing.crs.DefaultVerticalCRS;
 import org.apache.sis.referencing.crs.DefaultGeographicCRS;
 import org.apache.sis.referencing.crs.DefaultGeocentricCRS;
+import org.apache.sis.referencing.crs.DefaultEngineeringCRS;
 import org.apache.sis.referencing.factory.GeodeticAuthorityFactory;
 import org.apache.sis.referencing.factory.UnavailableFactoryException;
 import org.apache.sis.metadata.iso.citation.Citations;
@@ -69,6 +74,7 @@ import org.apache.sis.internal.system.SystemListener;
 import org.apache.sis.internal.system.Modules;
 import org.apache.sis.internal.system.Loggers;
 import org.apache.sis.internal.util.Constants;
+import org.apache.sis.internal.jdk9.JDK9;
 import org.apache.sis.util.resources.Vocabulary;
 import org.apache.sis.util.resources.Errors;
 import org.apache.sis.util.logging.Logging;
@@ -125,7 +131,7 @@ import static org.apache.sis.internal.util.StandardDateFormat.MILLISECONDS_PER_D
  * </table></blockquote>
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.0
+ * @version 1.1
  *
  * @see org.apache.sis.referencing.factory.CommonAuthorityFactory
  *
@@ -1773,6 +1779,140 @@ public enum CommonCRS {
         }
     }
 
+
+
+
+    /**
+     * Frequently-used engineering CRS and datum that are guaranteed to be available in SIS.
+     * Below is an alphabetical list of object names available in this enumeration:
+     *
+     * <blockquote><table class="sis">
+     *   <caption>Temporal objects accessible by enumeration constants</caption>
+     *   <tr><th>Name or alias</th>    <th>Object type</th> <th>Enumeration value</th></tr>
+     *   <tr><td>Computer display</td> <td>CRS, Datum</td>  <td>{@link #GEODISPLAY}</td></tr>
+     *   <tr><td>Computer display</td> <td>CRS, Datum</td>  <td>{@link #DISPLAY}</td></tr>
+     * </table></blockquote>
+     *
+     * @author  Martin Desruisseaux (Geomatys)
+     * @version 1.1
+     * @since   1.1
+     * @module
+     */
+    public enum Engineering {
+        /**
+         * Cartesian coordinate system with (east, south) oriented axes in pixel units.
+         * Axis directions are {@link AxisDirection#EAST EAST} and {@link AxisDirection#SOUTH SOUTH},
+         * which implies that the coordinate system can be related to a geospatial system in some way.
+         * The CRS is defined by the <cite>OGC Web Map Service Interface</cite> specification.
+         *
+         * <blockquote><table class="compact">
+         * <caption>Computer display properties</caption>
+         *   <tr><th>WMS identifier:</th> <td>CRS:1</td></tr>
+         *   <tr><th>Primary names:</th>  <td>"Computer display"</td></tr>
+         *   <tr><th>Direction:</th>
+         *     <td>{@link AxisDirection#EAST},
+         *         {@link AxisDirection#SOUTH SOUTH}</td></tr>
+         *   <tr><th>Unit:</th> <td>{@link Units#PIXEL}</td></tr>
+         * </table></blockquote>
+         */
+        GEODISPLAY(new DefaultEngineeringDatum(JDK9.mapOf(
+                EngineeringDatum.NAME_KEY, "Computer display",
+                EngineeringDatum.ANCHOR_POINT_KEY, "Origin is in upper left."))),
+
+        /**
+         * Cartesian coordinate system with (right, down) oriented axes in pixel units.
+         * This definition does not require the data to be geospatial.
+         *
+         * <blockquote><table class="compact">
+         * <caption>Computer display properties</caption>
+         *   <tr><th>Primary names:</th> <td>"Computer display"</td></tr>
+         *   <tr><th>Direction:</th>
+         *     <td>{@link AxisDirection#DISPLAY_RIGHT},
+         *         {@link AxisDirection#DISPLAY_DOWN DISPLAY_DOWN}</td></tr>
+         *   <tr><th>Unit:</th> <td>{@link Units#PIXEL}</td></tr>
+         * </table></blockquote>
+         */
+        DISPLAY(GEODISPLAY.datum),
+
+        /**
+         * Cartesian coordinate system with (column, row) oriented axes in unity units.
+         * This definition does not require the data to be geospatial.
+         *
+         * <blockquote><table class="compact">
+         * <caption>Grid properties</caption>
+         *   <tr><th>Primary names:</th> <td>"Cell indices"</td></tr>
+         *   <tr><th>Direction:</th>
+         *     <td>{@link AxisDirection#COLUMN_POSITIVE},
+         *         {@link AxisDirection#ROW_POSITIVE ROW_POSITIVE}</td></tr>
+         *   <tr><th>Unit:</th> <td>{@link Units#UNITY}</td></tr>
+         * </table></blockquote>
+         */
+        GRID(new DefaultEngineeringDatum(singletonMap(EngineeringDatum.NAME_KEY, "Cell indices")));
+
+        /**
+         * The datum.
+         */
+        private final EngineeringDatum datum;
+
+        /**
+         * The CRS, built when first needed.
+         */
+        private EngineeringCRS crs;
+
+        /**
+         * Creates a new enumeration value with the specified datum.
+         */
+        private Engineering(final EngineeringDatum datum) {
+            this.datum = datum;
+        }
+
+        /**
+         * Returns the coordinate reference system associated to this engineering object.
+         *
+         * @return the CRS associated to this enum.
+         */
+        public synchronized EngineeringCRS crs() {
+            if (crs == null) {
+                final String x, y;
+                final AxisDirection dx, dy;
+                final Map<String,Object> cs = singletonMap(CartesianCS.NAME_KEY, datum.getName());
+                final Map<String,Object> properties = new HashMap<>(cs);
+                switch (this) {
+                    case GEODISPLAY: {
+                        x = "i"; dx = AxisDirection.EAST;
+                        y = "j"; dy = AxisDirection.SOUTH;
+                        properties.put(EngineeringCRS.NAME_KEY, new NamedIdentifier(Citations.WMS, "1"));
+                        break;
+                    }
+                    case DISPLAY: {
+                        x = "x"; dx = AxisDirection.DISPLAY_RIGHT;
+                        y = "y"; dy = AxisDirection.DISPLAY_DOWN;
+                        break;
+                    }
+                    case GRID: {
+                        x = "i"; dx = AxisDirection.COLUMN_POSITIVE;
+                        y = "j"; dy = AxisDirection.ROW_POSITIVE;
+                        break;
+                    }
+                    default: throw new AssertionError(this);
+                }
+                crs = new DefaultEngineeringCRS(properties, datum, new DefaultCartesianCS(cs,
+                        new DefaultCoordinateSystemAxis(singletonMap(CartesianCS.NAME_KEY, x), x, dx, Units.PIXEL),
+                        new DefaultCoordinateSystemAxis(singletonMap(CartesianCS.NAME_KEY, y), y, dy, Units.PIXEL)));
+            }
+            return crs;
+        }
+
+        /**
+         * Returns the datum associated to this engineering object.
+         *
+         * @return the datum associated to this enum.
+         */
+        public EngineeringDatum datum() {
+            return datum;
+        }
+    }
+
     /**
      * Puts the name for the given key in a map of properties to be given to object constructors.
      *
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/referencing/factory/CommonAuthorityFactory.java b/core/sis-referencing/src/main/java/org/apache/sis/referencing/factory/CommonAuthorityFactory.java
index bb9a9c3..1c7cbe7 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/referencing/factory/CommonAuthorityFactory.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/referencing/factory/CommonAuthorityFactory.java
@@ -18,7 +18,6 @@ package org.apache.sis.referencing.factory;
 
 import java.util.Map;
 import java.util.Set;
-import java.util.HashMap;
 import java.util.LinkedHashMap;
 import java.util.LinkedHashSet;
 import java.util.Collections;
@@ -31,23 +30,17 @@ import org.opengis.metadata.citation.Citation;
 import org.opengis.referencing.IdentifiedObject;
 import org.opengis.referencing.NoSuchAuthorityCodeException;
 import org.opengis.referencing.crs.CoordinateReferenceSystem;
-import org.opengis.referencing.crs.CRSFactory;
 import org.opengis.referencing.crs.CRSAuthorityFactory;
 import org.opengis.referencing.crs.EngineeringCRS;
 import org.opengis.referencing.crs.GeographicCRS;
 import org.opengis.referencing.crs.ProjectedCRS;
 import org.opengis.referencing.crs.VerticalCRS;
 import org.opengis.referencing.crs.SingleCRS;
-import org.opengis.referencing.cs.CSFactory;
 import org.opengis.referencing.cs.CartesianCS;
-import org.opengis.referencing.cs.AxisDirection;
-import org.opengis.referencing.datum.DatumFactory;
-import org.opengis.referencing.datum.EngineeringDatum;
 import org.apache.sis.internal.referencing.provider.TransverseMercator.Zoner;
 import org.apache.sis.internal.referencing.GeodeticObjectBuilder;
 import org.apache.sis.internal.referencing.Resources;
 import org.apache.sis.metadata.iso.citation.Citations;
-import org.apache.sis.internal.system.DefaultFactories;
 import org.apache.sis.internal.system.Loggers;
 import org.apache.sis.internal.util.Constants;
 import org.apache.sis.measure.Units;
@@ -189,7 +182,7 @@ import org.apache.sis.util.iso.SimpleInternationalString;
  * switching to polar stereographic projections for high latitudes.</p>
  *
  * @author  Martin Desruisseaux (IRD, Geomatys)
- * @version 0.8
+ * @version 1.1
  *
  * @see CommonCRS
  *
@@ -248,13 +241,6 @@ public class CommonAuthorityFactory extends GeodeticAuthorityFactory implements
     private final Map<String,Class<?>> codes;
 
     /**
-     * The "Computer display" reference system (CRS:1). Created when first needed.
-     *
-     * @see #displayCRS()
-     */
-    private CoordinateReferenceSystem displayCRS;
-
-    /**
      * The coordinate system for map projection in metres, created when first needed.
      */
     private volatile CartesianCS projectedCS;
@@ -553,7 +539,7 @@ public class CommonAuthorityFactory extends GeodeticAuthorityFactory implements
         }
         final CommonCRS crs;
         switch (codeValue) {
-            case Constants.CRS1:  return displayCRS();
+            case Constants.CRS1:  return CommonCRS.Engineering.GEODISPLAY.crs();
             case Constants.CRS84: crs = CommonCRS.WGS84; break;
             case Constants.CRS83: crs = CommonCRS.NAD83; break;
             case Constants.CRS27: crs = CommonCRS.NAD27; break;
@@ -690,26 +676,6 @@ public class CommonAuthorityFactory extends GeodeticAuthorityFactory implements
     }
 
     /**
-     * Returns the "Computer display" reference system (CRS:1). This is rarely used.
-     */
-    private synchronized CoordinateReferenceSystem displayCRS() throws FactoryException {
-        if (displayCRS == null) {
-            final CSFactory csFactory = DefaultFactories.forBuildin(CSFactory.class);
-            final CartesianCS cs = csFactory.createCartesianCS(
-                    Collections.singletonMap(CartesianCS.NAME_KEY, "Computer display"),
-                    csFactory.createCoordinateSystemAxis(Collections.singletonMap(CartesianCS.NAME_KEY, "i"), "i", AxisDirection.EAST,  Units.PIXEL),
-                    csFactory.createCoordinateSystemAxis(Collections.singletonMap(CartesianCS.NAME_KEY, "j"), "j", AxisDirection.SOUTH, Units.PIXEL));
-
-            final Map<String,Object> properties = new HashMap<>(4);
-            properties.put(EngineeringDatum.NAME_KEY, cs.getName());
-            properties.put(EngineeringDatum.ANCHOR_POINT_KEY, "Origin is in upper left.");
-            displayCRS = DefaultFactories.forBuildin(CRSFactory.class).createEngineeringCRS(properties,
-                         DefaultFactories.forBuildin(DatumFactory.class).createEngineeringDatum(properties), cs);
-        }
-        return displayCRS;
-    }
-
-    /**
      * Creates an exception for an unknown authority code.
      *
      * @param  localCode  the unknown authority code, without namespace.
diff --git a/core/sis-utility/src/main/java/org/apache/sis/internal/jdk9/JDK9.java b/core/sis-utility/src/main/java/org/apache/sis/internal/jdk9/JDK9.java
index 1e3c7bb..cc2d3ed 100644
--- a/core/sis-utility/src/main/java/org/apache/sis/internal/jdk9/JDK9.java
+++ b/core/sis-utility/src/main/java/org/apache/sis/internal/jdk9/JDK9.java
@@ -27,7 +27,9 @@ import java.util.Arrays;
 import java.util.Set;
 import java.util.List;
 import java.util.Collections;
+import java.util.HashMap;
 import java.util.LinkedHashSet;
+import java.util.Map;
 import org.apache.sis.internal.util.UnmodifiableArrayList;
 
 
@@ -80,6 +82,17 @@ public final class JDK9 {
     }
 
     /**
+     * Placeholder for {@code Map.of(...)}.
+     */
+    public static <K,V> Map<K,V> mapOf(final Object... entries) {
+        final Map map = new HashMap();
+        for (int i=0; i<entries.length;) {
+            map.put(entries[i++], entries[i++]);
+        }
+        return map;
+    }
+
+    /**
      * Place holder for {@code Buffer.slice()}.
      *
      * @param  b the buffer to slice.


Mime
View raw message