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.
