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 d2ef60b78d539de671bec91026748f8352533f89
Author: Martin Desruisseaux <[email protected]>
AuthorDate: Tue Jun 28 18:21:16 2022 +0200

    Add a "Referencing by grid cell indices" menu item.
    The available choices depend on the grid coverages currently shown in the 
widget.
---
 .../apache/sis/gui/coverage/CoverageExplorer.java  |  15 +-
 .../org/apache/sis/gui/dataset/ResourceTree.java   | 105 +------------
 .../org/apache/sis/gui/dataset/WindowHandler.java  |   3 +-
 .../org/apache/sis/gui/referencing/MenuSync.java   | 149 ++++++++++++++-----
 .../gui/referencing/RecentReferenceSystems.java    | 164 +++++++++++++++------
 .../apache/sis/internal/gui/DataStoreOpener.java   | 106 ++++++++++++-
 .../org/apache/sis/internal/gui/Resources.java     |   5 +
 .../apache/sis/internal/gui/Resources.properties   |   1 +
 .../sis/internal/gui/Resources_fr.properties       |   1 +
 9 files changed, 367 insertions(+), 182 deletions(-)

diff --git 
a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/CoverageExplorer.java
 
b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/CoverageExplorer.java
index b6bae9a127..9823132672 100644
--- 
a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/CoverageExplorer.java
+++ 
b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/CoverageExplorer.java
@@ -18,6 +18,7 @@ package org.apache.sis.gui.coverage;
 
 import java.util.EnumMap;
 import java.util.Optional;
+import java.util.Collections;
 import java.awt.image.RenderedImage;
 import javafx.application.Platform;
 import javafx.beans.DefaultProperty;
@@ -31,8 +32,10 @@ import javafx.scene.layout.Region;
 import javafx.event.ActionEvent;
 import javafx.beans.property.ObjectProperty;
 import javafx.beans.property.SimpleObjectProperty;
+import org.apache.sis.storage.DataStoreException;
 import org.apache.sis.storage.GridCoverageResource;
 import org.apache.sis.coverage.grid.GridCoverage;
+import org.apache.sis.internal.gui.DataStoreOpener;
 import org.apache.sis.internal.gui.Resources;
 import org.apache.sis.internal.gui.ToolbarButton;
 import org.apache.sis.internal.gui.NonNullObjectProperty;
@@ -617,7 +620,17 @@ public class CoverageExplorer extends Widget {
      */
     final void notifyDataChanged(final GridCoverageResource resource, final 
GridCoverage coverage) {
         if (coverage != null) {
-            referenceSystems.configure(coverage.getGridGeometry());
+            String name;
+            try {
+                name = DataStoreOpener.findLabel(resource, getLocale(), true);
+            } catch (DataStoreException e) {
+                name = e.getLocalizedMessage();
+                if (name == null) {
+                    name = e.getClass().getSimpleName();
+                }
+            }
+            referenceSystems.setGridReferencing(true,
+                    Collections.singletonMap(name, 
coverage.getGridGeometry()));
         }
         /*
          * Following calls will NOT forward the new values to the views 
because this `notifyDataChanged(…)`
diff --git 
a/application/sis-javafx/src/main/java/org/apache/sis/gui/dataset/ResourceTree.java
 
b/application/sis-javafx/src/main/java/org/apache/sis/gui/dataset/ResourceTree.java
index 08e22affa6..bab1d75692 100644
--- 
a/application/sis-javafx/src/main/java/org/apache/sis/gui/dataset/ResourceTree.java
+++ 
b/application/sis-javafx/src/main/java/org/apache/sis/gui/dataset/ResourceTree.java
@@ -46,11 +46,7 @@ import javafx.scene.input.Dragboard;
 import javafx.scene.input.TransferMode;
 import javafx.scene.paint.Color;
 import org.opengis.util.GenericName;
-import org.opengis.util.InternationalString;
 import org.opengis.metadata.Metadata;
-import org.opengis.metadata.citation.Citation;
-import org.opengis.metadata.identification.Identification;
-import org.apache.sis.metadata.iso.citation.Citations;
 import org.apache.sis.util.resources.Vocabulary;
 import org.apache.sis.util.Exceptions;
 import org.apache.sis.util.Classes;
@@ -446,90 +442,6 @@ public class ResourceTree extends TreeView<Resource> {
         return null;
     }
 
-    /**
-     * Returns a label for a resource. Current implementation returns the
-     * {@linkplain DataStore#getDisplayName() data store display name} if 
available,
-     * or the title found in {@linkplain Resource#getMetadata() metadata} 
otherwise.
-     * If no label can be found, then this method returns the localized 
"Unnamed" string.
-     *
-     * <p>Identifiers can be very short, for example "1" or "2" meaning first 
or second image in a TIFF file.
-     * If {@code qualified} is {@code true}, then this method tries to return 
a label such as "filename:id".
-     * Generally {@code qualified} should be {@code false} if the label will 
be a node in a tree having the
-     * filename as parent, and {@code true} if the label will be used outside 
the context of a tree.</p>
-     *
-     * <p>This operation may be costly. For example the call to {@link 
Resource#getMetadata()}
-     * may cause the resource to open a connection to the EPSG database.
-     * Consequently his method should be invoked in a background thread.</p>
-     *
-     * @param  resource   the resource for which to get a label, or {@code 
null}.
-     * @param  locale     the locale to use for localizing international 
strings.
-     * @param  qualified  whether to use fully-qualified path of generic names.
-     * @return the resource display name or the citation title, never null.
-     */
-    static String findLabel(final Resource resource, final Locale locale, 
final boolean qualified) throws DataStoreException {
-        if (resource != null) {
-            final Long logID = LogHandler.loadingStart(resource);
-            try {
-                /*
-                 * The data store display name is typically the file name. We 
give precedence to that name
-                 * instead of the citation title because the citation may be 
the same for many files of
-                 * the same product, while the display name have better 
chances to be distinct for each file.
-                 */
-                if (resource instanceof DataStore) {
-                    final String name = Strings.trimOrNull(((DataStore) 
resource).getDisplayName());
-                    if (name != null) return name;
-                }
-                /*
-                 * Search for a title in metadata first because it has better 
chances to be human-readable
-                 * compared to the resource identifier. If the title is the 
same text as the identifier,
-                 * then we will execute the code path for identifier unless 
the caller did not asked for
-                 * qualified name, in which case it would make no difference.
-                 */
-                GenericName name = qualified ? 
resource.getIdentifier().orElse(null) : null;
-                Collection<? extends Identification> identifications = null;
-                final Metadata metadata = resource.getMetadata();
-                if (metadata != null) {
-                    identifications = metadata.getIdentificationInfo();
-                    if (identifications != null) {
-                        for (final Identification identification : 
identifications) {
-                            final Citation citation = 
identification.getCitation();
-                            if (citation != null) {
-                                final String t = string(citation.getTitle(), 
locale);
-                                if (t != null && (name == null || 
!t.equals(name.toString()))) {
-                                    return t;
-                                }
-                            }
-                        }
-                    }
-                }
-                /*
-                 * If we find no title in the metadata, use the resource 
identifier.
-                 * We search for explicitly declared identifier first before 
to fallback on
-                 * metadata identifier, because the latter is more subject to 
interpretation.
-                 */
-                if (!qualified) {
-                    name = resource.getIdentifier().orElse(null);
-                }
-                if (name != null) {
-                    if (qualified) {
-                        name = name.toFullyQualifiedName();
-                    }
-                    final String t = string(name.toInternationalString(), 
locale);
-                    if (t != null) return t;
-                }
-                if (identifications != null) {
-                    for (final Identification identification : 
identifications) {
-                        final String t = 
Citations.getIdentifier(identification.getCitation());
-                        if (t != null) return t;
-                    }
-                }
-            } finally {
-                LogHandler.loadingStop(logID);
-            }
-        }
-        return 
Vocabulary.getResources(locale).getString(Vocabulary.Keys.Unnamed);
-    }
-
     /**
      * Updates {@link Item#label} with the resource label fetched in 
background thread.
      * Caller should invoke this method only if {@link Item#isLoading} is 
{@code true}.
@@ -563,13 +475,6 @@ public class ResourceTree extends TreeView<Resource> {
         return Resources.forLocale(locale);
     }
 
-    /**
-     * Returns the given international string as a non-empty localized string, 
or {@code null} if none.
-     */
-    private static String string(final InternationalString i18n, final Locale 
locale) {
-        return (i18n != null) ? Strings.trimOrNull(i18n.toString(locale)) : 
null;
-    }
-
     /**
      * Returns a localized (if possible) string representation of the given 
exception.
      * This method returns the message if one exist, or the exception class 
name otherwise.
@@ -603,7 +508,7 @@ public class ResourceTree extends TreeView<Resource> {
      * The visual appearance of an {@link Item} in a tree. Cells are initially 
empty;
      * their content will be specified by {@link TreeView} after construction.
      * This class gets the cell text from a resource by a call to
-     * {@link ResourceTree#findLabel(Resource, Locale, boolean)} in a 
background thread.
+     * {@link DataStoreOpener#findLabel(Resource, Locale, boolean)} in a 
background thread.
      * The same call may be recycled many times for different {@link Item} 
data.
      *
      * @see Item
@@ -740,10 +645,10 @@ public class ResourceTree extends TreeView<Resource> {
         Path path;
 
         /**
-         * The text of this node, computed and cached when first needed.
-         * Computation is done by invoking {@link #findLabel(Resource, Locale, 
boolean)} in a background thread.
+         * The text of this node, computed and cached when first needed. 
Computation is done by invoking
+         * {@link DataStoreOpener#findLabel(Resource, Locale, boolean)} in a 
background thread.
          *
-         * @see #fetchLabel(Resource, Locale)
+         * @see #fetchLabel(Item.Completer)
          */
         String label;
 
@@ -830,7 +735,7 @@ public class ResourceTree extends TreeView<Resource> {
             /** Invoked in a background thread for fetching the label. */
             final void fetch(final Locale locale) {
                 try {
-                    result = findLabel(resource, locale, false);
+                    result = DataStoreOpener.findLabel(resource, locale, 
false);
                 } catch (Throwable e) {
                     failure = e;
                 }
diff --git 
a/application/sis-javafx/src/main/java/org/apache/sis/gui/dataset/WindowHandler.java
 
b/application/sis-javafx/src/main/java/org/apache/sis/gui/dataset/WindowHandler.java
index d3b1a46c66..2a7765fbe6 100644
--- 
a/application/sis-javafx/src/main/java/org/apache/sis/gui/dataset/WindowHandler.java
+++ 
b/application/sis-javafx/src/main/java/org/apache/sis/gui/dataset/WindowHandler.java
@@ -37,6 +37,7 @@ import org.apache.sis.storage.event.StoreListener;
 import org.apache.sis.gui.coverage.CoverageExplorer;
 import org.apache.sis.gui.map.MapCanvas;
 import org.apache.sis.internal.gui.BackgroundThreads;
+import org.apache.sis.internal.gui.DataStoreOpener;
 import org.apache.sis.internal.gui.GUIUtilities;
 import org.apache.sis.internal.gui.PrivateAccess;
 import org.apache.sis.internal.gui.Resources;
@@ -110,7 +111,7 @@ public abstract class WindowHandler {
         if (manager.main == this) {
             text = 
Resources.forLocale(manager.locale).getString(Resources.Keys.MainWindow);
         } else try {
-            text = ResourceTree.findLabel(getResource(), manager.locale, true);
+            text = DataStoreOpener.findLabel(getResource(), manager.locale, 
true);
         } catch (DataStoreException | RuntimeException e) {
             text = 
Vocabulary.getResources(manager.locale).getString(Vocabulary.Keys.Unknown);
             
Logging.recoverableException(Logger.getLogger(Modules.APPLICATION), 
WindowHandler.class, "<init>", e);
diff --git 
a/application/sis-javafx/src/main/java/org/apache/sis/gui/referencing/MenuSync.java
 
b/application/sis-javafx/src/main/java/org/apache/sis/gui/referencing/MenuSync.java
index cfa6888b3d..ee9b479a97 100644
--- 
a/application/sis-javafx/src/main/java/org/apache/sis/gui/referencing/MenuSync.java
+++ 
b/application/sis-javafx/src/main/java/org/apache/sis/gui/referencing/MenuSync.java
@@ -16,6 +16,7 @@
  */
 package org.apache.sis.gui.referencing;
 
+import java.util.List;
 import java.util.Arrays;
 import java.util.ArrayList;
 import java.util.IdentityHashMap;
@@ -31,6 +32,7 @@ import javafx.scene.control.MenuItem;
 import javafx.scene.control.RadioMenuItem;
 import javafx.scene.control.ToggleGroup;
 import org.opengis.referencing.ReferenceSystem;
+import org.opengis.referencing.crs.DerivedCRS;
 import org.opengis.referencing.crs.CoordinateReferenceSystem;
 import org.apache.sis.internal.referencing.ReferencingUtilities;
 import org.apache.sis.internal.gui.GUIUtilities;
@@ -70,17 +72,36 @@ final class MenuSync extends 
SimpleObjectProperty<ReferenceSystem> implements Ev
     private static final String CHOOSER = "CHOOSER";
 
     /**
-     * The list of reference systems to show as menu items.
+     * The list of reference systems to show in the root menu, not including 
items in sub-menus.
+     * This is the list of most recently used reference systems, so its 
content may change often.
+     * {@code MenuSync} does not register listeners on this list;
+     * if the content is changed, then {@link #notifyChanges()} should be 
invoked explicitly.
      */
-    private final ObservableList<? extends ReferenceSystem> systems;
+    private final List<ReferenceSystem> recentSystems;
 
     /**
-     * The list of menu items to keep up-to-date with an {@code 
ObservableList<ReferenceSystem>}.
+     * The list of reference systems to show in the "Referencing by cell 
indices" sub-menu.
+     * The content of this list depends on the grid coverages shown in the 
widget.
+     * This is {@code null} if that sub-menu is omitted.
      */
-    private final ObservableList<MenuItem> menus;
+    private final List<DerivedCRS> cellIndicesSystems;
 
     /**
-     * The group of menus.
+     * The list of menu items to keep up-to-date with {@link #recentSystems}.
+     * Contains also non-radio items such as "Other…" menu, and sub-menus for
+     * referencing by identifiers and referencing by cell indices.
+     */
+    private final ObservableList<MenuItem> rootMenus;
+
+    /**
+     * The list of menu items to keep up-to-date with {@link 
#cellIndicesSystems}.
+     * This is {@code null} if that sub-menu is omitted.
+     */
+    private final ObservableList<MenuItem> cellIndicesMenus;
+
+    /**
+     * The group of selectable menu items. Only one items can be selected at a 
time.
+     * The items may be distributed in the root menus and in sub-menus.
      */
     private final ToggleGroup group;
 
@@ -96,36 +117,55 @@ final class MenuSync extends 
SimpleObjectProperty<ReferenceSystem> implements Ev
      * Creates a new synchronization for the given list of menu items.
      *
      * @param  systems  the reference systems for which to build menu items.
+     * @param  byIds    whether to add a sub-menu for "Referencing by 
identifiers".
+     * @param  derived  content of "Referencing by cell indices" sub-menu, or 
{@code null} for omitting that sub-menu.
      * @param  bean     the menu to keep synchronized with the list of 
reference systems.
      * @param  action   the user-specified action to execute when a reference 
system is selected.
      */
-    MenuSync(final ObservableList<ReferenceSystem> systems, final Menu bean, 
final RecentReferenceSystems.SelectionListener action) {
+    MenuSync(final List<ReferenceSystem> systems, final boolean byIds, final 
List<DerivedCRS> derived,
+             final Menu bean, final RecentReferenceSystems.SelectionListener 
action)
+    {
         super(bean, "value");
-        this.systems = systems;
-        this.menus   = bean.getItems();
-        this.group   = new ToggleGroup();
-        this.action  = action;
+        recentSystems      = systems;
+        cellIndicesSystems = derived;
+        rootMenus          = bean.getItems();
+        group              = new ToggleGroup();
+        this.action        = action;
         /*
-         * We do not register listener for `systems` list.
-         * Instead `notifyChanges()` will be invoked directly by 
RecentReferenceSystems.
+         * Root menu. The list of recent reference system is dynamic and will 
change according user actions.
          */
         final MenuItem[] items = new MenuItem[systems.size()];
         final Locale locale = action.owner().locale;
         for (int i=0; i<items.length; i++) {
             items[i] = createItem(systems.get(i), locale);
         }
-        menus.setAll(items);
+        rootMenus.addAll(items);
         initialize();
+        if (byIds) {
+            addReferencingByIdentifiers(locale);
+        }
+        /*
+         * Creates new menu items for referencing by cell indices. Choices are 
offered in a separated sub-menu.
+         * This list of reference systems depends on the coverages shown in 
the widget.
+         */
+        if (derived != null) {
+            final Menu menu = new 
Menu(Resources.forLocale(locale).getString(Resources.Keys.ReferenceByCellIndices));
+            cellIndicesMenus = menu.getItems();
+            updateCellIndicesMenus(locale);
+            rootMenus.add(menu);
+        } else {
+            cellIndicesMenus = null;
+        }
     }
 
     /**
-     * Sets the initial value to the first two-dimensional item in the {@link 
#systems} list, if any.
+     * Sets the initial value to the first two-dimensional item in the {@link 
#recentSystems} list, if any.
      * This method is invoked in JavaFX thread at construction time or, if it 
didn't work,
-     * at some later time when the systems list may contain an element.
+     * at some later time when the list of recent reference systems may 
contain an element.
      * This method should not be invoked anymore after initialization 
succeeded.
      */
     private void initialize() {
-        for (final ReferenceSystem system : systems) {
+        for (final ReferenceSystem system : recentSystems) {
             if (system instanceof CoordinateReferenceSystem) {
                 if 
(ReferencingUtilities.getDimension((CoordinateReferenceSystem) system) == 
BIDIMENSIONAL) {
                     set(system);
@@ -155,10 +195,9 @@ final class MenuSync extends 
SimpleObjectProperty<ReferenceSystem> implements Ev
 
     /**
      * Creates new menu items for references system by identifiers, offered in 
a separated sub-menu.
-     * This list of reference system is fixed; items are not added or removed 
following user's selection.
+     * This list of reference systems is fixed; items are not added or removed 
following user's selection.
      */
-    final void addReferencingByIdentifiers() {
-        final Locale locale = action.owner().locale;
+    private void addReferencingByIdentifiers(final Locale locale) {
         final GazetteerFactory factory = new GazetteerFactory();
         final Resources resources = Resources.forLocale(locale);
         final Menu menu = new 
Menu(resources.getString(Resources.Keys.ReferenceByIdentifiers));
@@ -171,7 +210,38 @@ final class MenuSync extends 
SimpleObjectProperty<ReferenceSystem> implements Ev
         } catch (GazetteerException e) {
             RecentReferenceSystems.errorOccurred("createMenuItems", e);
         }
-        menus.add(menu);
+        rootMenus.add(menu);
+    }
+
+    /**
+     * Updates the {@link #cellIndicesMenus} list with current content of 
{@link #cellIndicesSystems}.
+     * This method recycles existing menu items, creates new ones if needed 
and discards the ones that
+     * are no longer in use.
+     *
+     * @param  systems  all CRS for grid indices to show in the "Referencing 
by cell indices" sub-menu.
+     */
+    private void updateCellIndicesMenus(final Locale locale) {
+        final int n = cellIndicesSystems.size();
+        for (int i=0; i<n; i++) {
+            final DerivedCRS crs = cellIndicesSystems.get(i);
+            final RadioMenuItem item;
+            if (i < cellIndicesMenus.size()) {
+                item = (RadioMenuItem) cellIndicesMenus.get(i);
+            } else {
+                item = new RadioMenuItem();
+                item.setToggleGroup(group);
+                item.setOnAction(this);
+                cellIndicesMenus.add(item);
+            }
+            if (item.getProperties().put(REFERENCE_SYSTEM_KEY, crs) != crs) {
+                item.setText(IdentifiedObjects.getDisplayName(crs, locale));
+            }
+        }
+        for (int i = cellIndicesMenus.size(); --i >= n;) {
+            final RadioMenuItem item = (RadioMenuItem) 
cellIndicesMenus.remove(i);
+            item.setToggleGroup(null);
+            item.setOnAction(null);
+        }
     }
 
     /**
@@ -197,7 +267,7 @@ final class MenuSync extends 
SimpleObjectProperty<ReferenceSystem> implements Ev
          */
         final var subMenus = new ArrayList<Menu>();
         final Map<Object,MenuItem> mapping = new IdentityHashMap<>();
-        for (final Iterator<MenuItem> it = menus.iterator(); it.hasNext();) {
+        for (final Iterator<MenuItem> it = rootMenus.iterator(); 
it.hasNext();) {
             final MenuItem item = it.next();
             if (item instanceof Menu) {
                 subMenus.add((Menu) item);
@@ -211,10 +281,10 @@ final class MenuSync extends 
SimpleObjectProperty<ReferenceSystem> implements Ev
          * Other menu items are left to null for now; those null values may 
appear anywhere in the array. After this
          * loop, the map will contain only menu items for CRS that are no 
longer in the list of CRS to offer.
          */
-        final int newCount = systems.size();
+        final int newCount = recentSystems.size();
         final MenuItem[] items = new MenuItem[newCount + subMenus.size()];
         for (int i=0; i<newCount; i++) {
-            Object key = systems.get(i);
+            Object key = recentSystems.get(i);
             if (key == RecentReferenceSystems.OTHER) key = CHOOSER;
             items[i] = mapping.remove(key);
         }
@@ -229,7 +299,7 @@ final class MenuSync extends 
SimpleObjectProperty<ReferenceSystem> implements Ev
         for (int i=0; i<newCount; i++) {
             if (items[i] == null) {
                 MenuItem item = null;
-                final ReferenceSystem system = systems.get(i);
+                final ReferenceSystem system = recentSystems.get(i);
                 if (system != RecentReferenceSystems.OTHER && 
recycle.hasNext()) {
                     item = recycle.next();
                     recycle.remove();
@@ -258,13 +328,16 @@ final class MenuSync extends 
SimpleObjectProperty<ReferenceSystem> implements Ev
         for (int i=newCount; i<items.length; i++) {
             items[i] = subMenus.get(i - newCount);
         }
-        GUIUtilities.copyAsDiff(Arrays.asList(items), menus);
+        GUIUtilities.copyAsDiff(Arrays.asList(items), rootMenus);
         /*
          * If we had no previously selected item, selects it now.
          */
         if (get() == null) {
             initialize();
         }
+        if (cellIndicesSystems != null) {
+            updateCellIndicesMenus(locale);
+        }
     }
 
     /**
@@ -277,12 +350,23 @@ final class MenuSync extends 
SimpleObjectProperty<ReferenceSystem> implements Ev
      */
     @Override
     public void handle(final ActionEvent event) {
-        // ClassCastException should not happen because this listener is 
registered only on MenuItem.
-        final Object value = ((MenuItem) 
event.getSource()).getProperties().get(REFERENCE_SYSTEM_KEY);
+        /*
+         * ClassCastException should not happen because this listener is 
registered only on MenuItem,
+         * and REFERENCE_SYSTEM_KEY is a property which should be read and 
written only by SIS.
+         */
+        final MenuItem source = (MenuItem) event.getSource();
+        final Object value = source.getProperties().get(REFERENCE_SYSTEM_KEY);
         if (value == CHOOSER) {
             action.changed(this, get(), RecentReferenceSystems.OTHER);
         } else {
-            set((ReferenceSystem) value);
+            final ReferenceSystem system = (ReferenceSystem) value;
+            if (cellIndicesMenus != null && cellIndicesMenus.contains(source)) 
{
+                final ReferenceSystem old = get();
+                super.set(system);                          // Set without 
adding to `recentSystems` list.
+                action.action.changed(this, old, system);   // Skip the work 
done by `action.changed(…)`.
+            } else {
+                set(system);
+            }
         }
     }
 
@@ -298,7 +382,7 @@ final class MenuSync extends 
SimpleObjectProperty<ReferenceSystem> implements Ev
         final ReferenceSystem old = get();
         if (old != system) {
             final ComparisonMode mode = 
action.owner().duplicationCriterion.get();
-            for (final MenuItem item : menus) {
+            for (final MenuItem item : rootMenus) {
                 if (item instanceof RadioMenuItem) {
                     final Object current = 
item.getProperties().get(REFERENCE_SYSTEM_KEY);
                     if (Utilities.deepEquals(current, system, mode)) {
@@ -313,10 +397,9 @@ final class MenuSync extends 
SimpleObjectProperty<ReferenceSystem> implements Ev
             super.set(system);
             group.selectToggle(null);
             action.owner().addSelected(system);
-            /*
-             * Do not invoke action.changed(…) since we have no non-null value 
to provide.
-             * Invoking that method with a null value would cause the 
CRSChooser to popup.
-             */
+            if (system != RecentReferenceSystems.OTHER) {
+                action.changed(this, old, system);
+            }
         }
     }
 }
diff --git 
a/application/sis-javafx/src/main/java/org/apache/sis/gui/referencing/RecentReferenceSystems.java
 
b/application/sis-javafx/src/main/java/org/apache/sis/gui/referencing/RecentReferenceSystems.java
index 04be43ee97..44d450e62b 100644
--- 
a/application/sis-javafx/src/main/java/org/apache/sis/gui/referencing/RecentReferenceSystems.java
+++ 
b/application/sis-javafx/src/main/java/org/apache/sis/gui/referencing/RecentReferenceSystems.java
@@ -16,6 +16,7 @@
  */
 package org.apache.sis.gui.referencing;
 
+import java.util.Map;
 import java.util.List;
 import java.util.ArrayList;
 import java.util.Locale;
@@ -36,8 +37,12 @@ import org.opengis.util.FactoryException;
 import org.opengis.geometry.Envelope;
 import org.opengis.referencing.ReferenceSystem;
 import org.opengis.referencing.IdentifiedObject;
+import org.opengis.referencing.crs.DerivedCRS;
 import org.opengis.referencing.crs.CRSAuthorityFactory;
 import org.opengis.referencing.crs.CoordinateReferenceSystem;
+import org.opengis.referencing.operation.TransformException;
+import org.opengis.referencing.datum.PixelInCell;
+import org.apache.sis.geometry.Envelopes;
 import org.apache.sis.geometry.ImmutableEnvelope;
 import org.apache.sis.referencing.IdentifiedObjects;
 import org.apache.sis.referencing.factory.GeodeticAuthorityFactory;
@@ -58,6 +63,7 @@ import org.apache.sis.internal.gui.OptionalDataDownloader;
 import org.apache.sis.internal.gui.RecentChoices;
 import org.apache.sis.internal.system.Modules;
 import org.apache.sis.internal.util.Strings;
+import org.apache.sis.internal.util.UnmodifiableArrayList;
 
 import static java.util.logging.Logger.getLogger;
 
@@ -231,8 +237,12 @@ public class RecentReferenceSystems {
     /**
      * A filtered view of {@link #referenceSystems} without the {@link #OTHER} 
item.
      * This is the list returned to users by public API, but otherwise it is 
not used by this class.
-     * Instead, the lists used internally by this class contains the {@link 
#OTHER} item because
-     * those lists are used directly by controls like {@code 
ChoiceBox<ReferenceSystem>}.
+     *
+     * <div class="note"><b>Design note:</b>
+     * the {@link #OTHER} item needs to exist in the list used internally by 
this class because those lists
+     * are used directly by controls like {@code ChoiceBox<ReferenceSystem>}, 
with the {@link #OTHER} value
+     * handled in a special way by {@link ObjectStringConverter} for making 
the "Other…" item present in the
+     * list of choices. But since {@link #OTHER} is not a real CRS, we want to 
hide that trick to users.</div>
      *
      * <p>This list is lazily created when first needed,
      * because it depends on {@link #referenceSystems} which is itself lazily 
created.</p>
@@ -241,6 +251,15 @@ public class RecentReferenceSystems {
      */
     private ObservableList<ReferenceSystem> publicItemList;
 
+    /**
+     * Coordinate reference systems used for computing cell indices of grid 
coverages.
+     * Those reference systems are offered in a sub-menu and are not included 
in {@link #publicItemList}.
+     * The content of this list depends on the grid coverages shown in the 
widget.
+     *
+     * @see #setGridReferencing(boolean, Map)
+     */
+    private final List<DerivedCRS> cellIndiceSystems;
+
     /**
      * {@code true} if the {@link #referenceSystems} list needs to be rebuilt 
from {@link #systemsOrCodes} content.
      * This field shall be read and modified in a block synchronized on {@link 
#systemsOrCodes}.
@@ -276,6 +295,7 @@ public class RecentReferenceSystems {
         this.factory         = factory;
         this.locale          = locale;
         systemsOrCodes       = new ArrayList<>();
+        cellIndiceSystems    = new ArrayList<>();
         areaOfInterest       = new SimpleObjectProperty<>(this, 
"areaOfInterest");
         duplicationCriterion = new NonNullObjectProperty<>(this, 
"duplicationCriterion", ComparisonMode.ALLOW_VARIANT);
         controlValues        = new ArrayList<>();
@@ -287,27 +307,77 @@ public class RecentReferenceSystems {
     }
 
     /**
-     * Configures this instance for a grid coverage having the given geometry.
-     * This convenience method sets the {@link #areaOfInterest} and the
-     * {@linkplain #setPreferred(boolean, ReferenceSystem) preferred CRS}
-     * with the information found in the given grid geometry.
-     * The properties for which {@code gg} contains no information are left 
unchanged.
+     * Sets the reference systems, area of interest and "referencing by grid 
indices" systems.
+     * This method performs all the following work:
+     *
+     * <ul>
+     *   <li>Invokes {@link #setPreferred(boolean, ReferenceSystem)} with the 
first CRS in iteration order.</li>
+     *   <li>Invokes {@link #addAlternatives(boolean, ReferenceSystem...)} for 
all other CRS (single call).</li>
+     *   <li>Sets the {@link #areaOfInterest} to the union of all 
envelopes.</li>
+     *   <li>Sets the content of "Referencing by cell indices" sub-menu.</li>
+     * </ul>
      *
-     * @param  gg  the grid geometry, or {@code null} if none.
+     * @param  replaceByAuthoritativeDefinition  whether the reference systems 
should be replaced by authoritative definition.
+     * @param  geometries  grid coverage names together with their grid 
geometry. May be empty.
      *
      * @since 1.3
      */
-    public void configure(final GridGeometry gg) {
-        Envelope aoi = null;
-        if (gg != null) {
+    public void setGridReferencing(final boolean 
replaceByAuthoritativeDefinition,
+            final Map<String,GridGeometry> geometries)
+    {
+        /*
+         * Fetch or compute information needed, but without modifying the 
state of this object yet.
+         * All assignments to `this` should be done inside the `try … finally` 
block.
+         */
+        int countEnv = 0;
+        int countCRS = 0;
+        int countCIR = 0;
+        final Envelope[] envelopes = new Envelope[geometries.size()];
+        final DerivedCRS[] derived = new DerivedCRS[geometries.size()];
+        final CoordinateReferenceSystem[] alt = new 
CoordinateReferenceSystem[Math.max(derived.length - 1, 0)];
+        CoordinateReferenceSystem preferred = null;
+        for (final Map.Entry<String,GridGeometry> entry : 
geometries.entrySet()) {
+            final GridGeometry gg = entry.getValue();
             if (gg.isDefined(GridGeometry.ENVELOPE)) {
-                aoi = gg.getEnvelope();
+                envelopes[countEnv++] = gg.getEnvelope();
             }
             if (gg.isDefined(GridGeometry.CRS)) {
-                setPreferred(true, gg.getCoordinateReferenceSystem());
+                final CoordinateReferenceSystem crs = 
gg.getCoordinateReferenceSystem();
+                if (preferred == null) {
+                    preferred = crs;
+                } else {
+                    alt[countCRS++] = crs;
+                }
+                if (gg.isDefined(GridGeometry.GRID_TO_CRS | 
GridGeometry.EXTENT)) {
+                    derived[countCIR++] = gg.createImageCRS(entry.getKey(), 
PixelInCell.CELL_CENTER);
+                }
             }
         }
-        areaOfInterest.set(aoi);
+        Envelope aoi = null;
+        try {
+            aoi = Envelopes.union(envelopes);       // No need to trim null 
elements.
+        } catch (TransformException e) {
+            errorOccurred("setGridReferencing", e);
+        }
+        /*
+         * Modify now the state of `this` object but with `listModified()` 
made almost no-op.
+         * The intent is to have only one effective call to `listModified()` 
at the end,
+         * in order to have only one call to `filterReferenceSystems(…)`.
+         */
+        final ObservableList<ReferenceSystem> savedReferenceSystemList = 
referenceSystems;
+        try {
+            referenceSystems = null;
+            if (preferred != null) {
+                setPreferred(replaceByAuthoritativeDefinition, preferred);
+                addAlternatives(replaceByAuthoritativeDefinition, alt);        
 // No need to trim null elements.
+                cellIndiceSystems.clear();
+                cellIndiceSystems.addAll(UnmodifiableArrayList.wrap(derived, 
0, countCIR));
+            }
+            areaOfInterest.set(aoi);
+        } finally {
+            referenceSystems = savedReferenceSystemList;
+        }
+        listModified();
     }
 
     /**
@@ -323,7 +393,8 @@ public class RecentReferenceSystems {
      * @param  replaceByAuthoritativeDefinition  whether the given system 
should be replaced by authoritative definition.
      * @param  system  the native or preferred reference system to show as the 
first choice.
      */
-    public void setPreferred(final boolean replaceByAuthoritativeDefinition, 
final ReferenceSystem system) {
+    public final void setPreferred(final boolean 
replaceByAuthoritativeDefinition, final ReferenceSystem system) {
+        // Final because `setGridReferencing(…)` needs to be sure that 
`referenceSystems` is not rebuilt.
         ArgumentChecks.ensureNonNull("system", system);
         synchronized (systemsOrCodes) {
             systemsOrCodes.add(0, replaceByAuthoritativeDefinition ? new 
Unverified(system) : system);
@@ -379,7 +450,8 @@ public class RecentReferenceSystems {
      * @param  replaceByAuthoritativeDefinition  whether the given systems 
should be replaced by authoritative definitions.
      * @param  systems  the reference systems to add as alternative choices. 
Null elements are ignored.
      */
-    public void addAlternatives(final boolean 
replaceByAuthoritativeDefinition, final ReferenceSystem... systems) {
+    public final void addAlternatives(final boolean 
replaceByAuthoritativeDefinition, final ReferenceSystem... systems) {
+        // Final because `setGridReferencing(…)` needs to be sure that 
`referenceSystems` is not rebuilt.
         ArgumentChecks.ensureNonNull("systems", systems);
         synchronized (systemsOrCodes) {
             for (final ReferenceSystem system : systems) {
@@ -432,7 +504,7 @@ public class RecentReferenceSystems {
     }
 
     /**
-     * Returns whether the given object is accepted for inclusion in the list 
of CRS choice.
+     * Returns whether the given object is accepted for inclusion in the list 
of CRS choices.
      * In current implementation we accept a CRS if it has an authority code 
(typically an EPSG code).
      */
     private static boolean isAccepted(final IdentifiedObject object) {
@@ -613,29 +685,30 @@ public class RecentReferenceSystems {
              * is added. The first occurrence of duplicated values is kept, 
which will result in above-cited
              * order as the priority order where to insert the CRS.
              */
-            isModified = true;
-            final int insertAt = Math.min(systemsOrCodes.size(), 
NUM_CORE_ITEMS);
-            final List<ReferenceSystem> selected = getSelectedItems();
-            systemsOrCodes.addAll(insertAt, selected);
-            systemsOrCodes.addAll(insertAt + selected.size(), 
referenceSystems);
-            final ImmutableEnvelope domain = geographicAOI;
-            final ComparisonMode mode = duplicationCriterion.get();
-            BackgroundThreads.execute(new Task<List<ReferenceSystem>>() {
-                /** Filters the {@link ReferenceSystem}s in a background 
thread. */
-                @Override protected List<ReferenceSystem> call() {
-                    return filterReferenceSystems(domain, mode);
-                }
+            if (isModified) {
+                final int insertAt = Math.min(systemsOrCodes.size(), 
NUM_CORE_ITEMS);
+                final List<ReferenceSystem> selected = getSelectedItems();
+                systemsOrCodes.addAll(insertAt, selected);
+                systemsOrCodes.addAll(insertAt + selected.size(), 
referenceSystems);
+                final ImmutableEnvelope domain = geographicAOI;
+                final ComparisonMode mode = duplicationCriterion.get();
+                BackgroundThreads.execute(new Task<List<ReferenceSystem>>() {
+                    /** Filters the {@link ReferenceSystem}s in a background 
thread. */
+                    @Override protected List<ReferenceSystem> call() {
+                        return filterReferenceSystems(domain, mode);
+                    }
 
-                /** Should never happen. */
-                @Override protected void failed() {
-                    ExceptionReporter.show(null, this);
-                }
+                    /** Should never happen. */
+                    @Override protected void failed() {
+                        ExceptionReporter.show(null, this);
+                    }
 
-                /** Sets the {@link ChoiceBox} content to the list computed in 
background thread. */
-                @Override protected void succeeded() {
-                    setReferenceSystems(getValue(), mode);
-                }
-            });
+                    /** Sets the {@link ChoiceBox} content to the list 
computed in background thread. */
+                    @Override protected void succeeded() {
+                        setReferenceSystems(getValue(), mode);
+                    }
+                });
+            }
         }
         if (filtered) {
             if (coordinateReferenceSystems == null) {
@@ -712,7 +785,7 @@ public class RecentReferenceSystems {
      */
     final class SelectionListener implements ChangeListener<ReferenceSystem> {
         /** The user-specified action to execute when a reference system is 
selected. */
-        private final ChangeListener<ReferenceSystem> action;
+        final ChangeListener<ReferenceSystem> action;
 
         /** Creates a new listener of reference system selection. */
         private SelectionListener(final ChangeListener<ReferenceSystem> 
action) {
@@ -963,11 +1036,10 @@ next:       for (int i=0; i<count; i++) {
      */
     public Menu createMenuItems(final boolean filtered, final 
ChangeListener<ReferenceSystem> action) {
         ArgumentChecks.ensureNonNull("action", action);
+        final List<ReferenceSystem> main = getReferenceSystems(filtered);
+        final List<DerivedCRS> derived = (filtered) ? null : cellIndiceSystems;
         final Menu menu = new 
Menu(Vocabulary.getResources(locale).getString(Vocabulary.Keys.ReferenceSystem));
-        final MenuSync property = new MenuSync(getReferenceSystems(filtered), 
menu, new SelectionListener(action));
-        if (!filtered) {
-            property.addReferencingByIdentifiers();
-        }
+        final MenuSync property = new MenuSync(main, !filtered, derived, menu, 
new SelectionListener(action));
         menu.getProperties().put(SELECTED_ITEM_KEY, property);
         controlValues.add(property);
         return menu;
@@ -1026,13 +1098,13 @@ next:       for (int i=0; i<count; i++) {
     }
 
     /**
-     * Invoked when an error occurred while fetching a reference system be 
identifier.
-     * This is the complement of {@link #errorOccurred(FactoryException)} but 
for referencing by identifiers.
+     * Invoked when an error other than {@link FactoryException} occurred.
+     * The error shall be recoverable, e.g. by ignoring a menu item.
      *
      * @param  caller  the method to report as the source the in log record.
      * @param  e  the error that occurred.
      */
-    static void errorOccurred(final String caller, final GazetteerException e) 
{
+    static void errorOccurred(final String caller, final Exception e) {
         Logging.recoverableException(getLogger(Modules.APPLICATION), 
RecentReferenceSystems.class, caller, e);
     }
 }
diff --git 
a/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/DataStoreOpener.java
 
b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/DataStoreOpener.java
index 5dc5a831b2..454d49611a 100644
--- 
a/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/DataStoreOpener.java
+++ 
b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/DataStoreOpener.java
@@ -23,6 +23,8 @@ import java.nio.file.Path;
 import java.nio.file.Paths;
 import java.io.IOException;
 import java.net.URISyntaxException;
+import java.util.Locale;
+import java.util.Collection;
 import java.util.function.Consumer;
 import java.util.function.UnaryOperator;
 import java.util.stream.Stream;
@@ -30,11 +32,19 @@ import javafx.concurrent.Task;
 import javafx.event.EventHandler;
 import javafx.application.Platform;
 import javafx.scene.Node;
+import org.opengis.util.GenericName;
+import org.opengis.util.InternationalString;
+import org.opengis.metadata.Metadata;
+import org.opengis.metadata.citation.Citation;
+import org.opengis.metadata.identification.Identification;
+import org.apache.sis.metadata.iso.citation.Citations;
+import org.apache.sis.storage.Resource;
 import org.apache.sis.storage.StorageConnector;
 import org.apache.sis.storage.DataStoreException;
 import org.apache.sis.storage.DataStores;
 import org.apache.sis.util.collection.Cache;
 import org.apache.sis.util.resources.Vocabulary;
+import org.apache.sis.internal.util.Strings;
 import org.apache.sis.internal.storage.io.IOUtilities;
 import org.apache.sis.internal.storage.io.ChannelFactory;
 import org.apache.sis.internal.storage.io.InternalOptionKey;
@@ -63,7 +73,7 @@ import org.apache.sis.gui.DataViewer;
  * @todo Set title. Add progress listener and cancellation capability.
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.2
+ * @version 1.3
  *
  * @see BackgroundThreads#execute(Runnable)
  *
@@ -211,6 +221,100 @@ public final class DataStoreOpener extends 
Task<DataStore> {
         factoryWrapper = wrapper;
     }
 
+    /**
+     * Returns a label for a resource. Current implementation returns the
+     * {@linkplain DataStore#getDisplayName() data store display name} if 
available,
+     * or the title found in {@linkplain Resource#getMetadata() metadata} 
otherwise.
+     * If no label can be found, then this method returns the localized 
"Unnamed" string.
+     *
+     * <p>Identifiers can be very short, for example "1" or "2" meaning first 
or second image in a TIFF file.
+     * If {@code qualified} is {@code true}, then this method tries to return 
a label such as "filename:id".
+     * Generally {@code qualified} should be {@code false} if the label will 
be a node in a tree having the
+     * filename as parent, and {@code true} if the label will be used outside 
the context of a tree.</p>
+     *
+     * <p>This operation may be costly. For example the call to {@link 
Resource#getMetadata()}
+     * may cause the resource to open a connection to the EPSG database.
+     * Consequently his method should be invoked in a background thread.</p>
+     *
+     * @param  resource   the resource for which to get a label, or {@code 
null}.
+     * @param  locale     the locale to use for localizing international 
strings.
+     * @param  qualified  whether to use fully-qualified path of generic names.
+     * @return the resource display name or the citation title, never null.
+     * @throws DataStoreException if an error occurred while fetching the 
resource identifier or metadata.
+     */
+    public static String findLabel(final Resource resource, final Locale 
locale, final boolean qualified)
+            throws DataStoreException
+    {
+        if (resource != null) {
+            final Long logID = LogHandler.loadingStart(resource);
+            try {
+                /*
+                 * The data store display name is typically the file name. We 
give precedence to that name
+                 * instead of the citation title because the citation may be 
the same for many files of
+                 * the same product, while the display name have better 
chances to be distinct for each file.
+                 */
+                if (resource instanceof DataStore) {
+                    final String name = Strings.trimOrNull(((DataStore) 
resource).getDisplayName());
+                    if (name != null) return name;
+                }
+                /*
+                 * Search for a title in metadata first because it has better 
chances to be human-readable
+                 * compared to the resource identifier. If the title is the 
same text as the identifier,
+                 * then we will execute the code path for identifier unless 
the caller did not asked for
+                 * qualified name, in which case it would make no difference.
+                 */
+                GenericName name = qualified ? 
resource.getIdentifier().orElse(null) : null;
+                Collection<? extends Identification> identifications = null;
+                final Metadata metadata = resource.getMetadata();
+                if (metadata != null) {
+                    identifications = metadata.getIdentificationInfo();
+                    if (identifications != null) {
+                        for (final Identification identification : 
identifications) {
+                            final Citation citation = 
identification.getCitation();
+                            if (citation != null) {
+                                final String t = string(citation.getTitle(), 
locale);
+                                if (t != null && (name == null || 
!t.equals(name.toString()))) {
+                                    return t;
+                                }
+                            }
+                        }
+                    }
+                }
+                /*
+                 * If we find no title in the metadata, use the resource 
identifier.
+                 * We search for explicitly declared identifier first before 
to fallback on
+                 * metadata identifier, because the latter is more subject to 
interpretation.
+                 */
+                if (!qualified) {
+                    name = resource.getIdentifier().orElse(null);
+                }
+                if (name != null) {
+                    if (qualified) {
+                        name = name.toFullyQualifiedName();
+                    }
+                    final String t = string(name.toInternationalString(), 
locale);
+                    if (t != null) return t;
+                }
+                if (identifications != null) {
+                    for (final Identification identification : 
identifications) {
+                        final String t = 
Citations.getIdentifier(identification.getCitation());
+                        if (t != null) return t;
+                    }
+                }
+            } finally {
+                LogHandler.loadingStop(logID);
+            }
+        }
+        return 
Vocabulary.getResources(locale).getString(Vocabulary.Keys.Unnamed);
+    }
+
+    /**
+     * Returns the given international string as a non-empty localized string, 
or {@code null} if none.
+     */
+    private static String string(final InternationalString i18n, final Locale 
locale) {
+        return (i18n != null) ? Strings.trimOrNull(i18n.toString(locale)) : 
null;
+    }
+
     /**
      * Removes the given data store from cache and closes it. It is caller's 
responsibility
      * to ensure that the given data store is not used anymore before to 
invoke this method.
diff --git 
a/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/Resources.java
 
b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/Resources.java
index d1f17be4f3..1b83d23c2a 100644
--- 
a/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/Resources.java
+++ 
b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/Resources.java
@@ -347,6 +347,11 @@ public final class Resources extends IndexedResourceBundle 
{
          */
         public static final short RangeOfValues = 56;
 
+        /**
+         * Reference by cell indices
+         */
+        public static final short ReferenceByCellIndices = 74;
+
         /**
          * Reference system by identifiers
          */
diff --git 
a/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/Resources.properties
 
b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/Resources.properties
index 6da5d3e06e..3081492527 100644
--- 
a/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/Resources.properties
+++ 
b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/Resources.properties
@@ -78,6 +78,7 @@ Orthographic           = Orthographic
 OtherCRS               = Other coordinate reference system\u2026
 PropertyValue          = Property value
 RangeOfValues          = Range of values\u2026
+ReferenceByCellIndices = Reference by cell indices
 ReferenceByIdentifiers = Reference system by identifiers
 SelectCRS              = Select a coordinate reference system
 SelectCrsByContextMenu = For changing the projection, use contextual menu on 
the map.
diff --git 
a/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/Resources_fr.properties
 
b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/Resources_fr.properties
index dc879df892..bbc4e0f7af 100644
--- 
a/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/Resources_fr.properties
+++ 
b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/Resources_fr.properties
@@ -83,6 +83,7 @@ Orthographic           = Orthographique
 OtherCRS               = Autre syst\u00e8me de r\u00e9f\u00e9rence par 
coordonn\u00e9es\u2026
 PropertyValue          = Valeur de la propri\u00e9t\u00e9
 RangeOfValues          = Plage de valeurs\u2026
+ReferenceByCellIndices = R\u00e9f\u00e9rencement par indices de cellules
 ReferenceByIdentifiers = Syst\u00e8me de r\u00e9f\u00e9rence par identifiants
 SelectCRS              = Choisir un syst\u00e8me de r\u00e9f\u00e9rence des 
coordonn\u00e9es
 SelectCrsByContextMenu = Pour changer la projection, utilisez le menu 
contextuel sur la carte.

Reply via email to