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 48633f037761e8b77ee14b8d3aacb23615d93802 Author: Martin Desruisseaux <[email protected]> AuthorDate: Mon Mar 30 11:07:38 2026 +0200 Adds a "Layers" panel and move the "Visual indication of tile loading" checkbox as a map item. This is the beginning of a `MapContextView` widget as a tree view with a configuration panel. --- .../org/apache/sis/util/resources/Vocabulary.java | 5 + .../sis/util/resources/Vocabulary.properties | 1 + .../sis/util/resources/Vocabulary_fr.properties | 1 + .../apache/sis/gui/coverage/CoverageCanvas.java | 42 ++++++- .../apache/sis/gui/coverage/CoverageControls.java | 50 +++++++-- .../apache/sis/gui/coverage/CoverageExplorer.java | 22 ++-- .../apache/sis/gui/coverage/StyleController.java | 77 +++++++++++++ .../org/apache/sis/gui/dataset/WindowHandler.java | 27 +++-- .../apache/sis/gui/internal/DataStoreOpener.java | 56 +++++++--- .../apache/sis/gui/map/style/ItemController.java | 50 +++++++++ .../apache/sis/gui/map/style/MapContextView.java | 122 +++++++++++++++++++++ .../package-info.java => map/style/MapItem.java} | 27 ++++- .../package-info.java => map/style/MapLayer.java} | 29 ++++- .../{referencing => map/style}/package-info.java | 10 +- .../gui/referencing/RecentReferenceSystems.java | 73 ++++++++---- .../apache/sis/gui/referencing/package-info.java | 2 +- 16 files changed, 502 insertions(+), 92 deletions(-) diff --git a/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/resources/Vocabulary.java b/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/resources/Vocabulary.java index 2bf1d664eb..9fea4b3345 100644 --- a/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/resources/Vocabulary.java +++ b/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/resources/Vocabulary.java @@ -699,6 +699,11 @@ public class Vocabulary extends IndexedResourceBundle { */ public static final short Latitude = 112; + /** + * Layers + */ + public static final short Layers = 285; + /** * Layout */ diff --git a/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/resources/Vocabulary.properties b/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/resources/Vocabulary.properties index da9cf8472e..b7e9cbf0e0 100644 --- a/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/resources/Vocabulary.properties +++ b/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/resources/Vocabulary.properties @@ -145,6 +145,7 @@ JavaHome = Java home directory Julian = Julian Latitude = Latitude Longitude = Longitude +Layers = Layers Layout = Layout Legend = Legend Level = Level diff --git a/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/resources/Vocabulary_fr.properties b/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/resources/Vocabulary_fr.properties index 67fa5590ea..98b196c92d 100644 --- a/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/resources/Vocabulary_fr.properties +++ b/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/resources/Vocabulary_fr.properties @@ -152,6 +152,7 @@ JavaHome = R\u00e9pertoire du Java Julian = Julien Latitude = Latitude Longitude = Longitude +Layers = Couches Layout = Disposition Legend = L\u00e9gende Level = Niveau diff --git a/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/coverage/CoverageCanvas.java b/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/coverage/CoverageCanvas.java index 20ceee15a7..7433c89b84 100644 --- a/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/coverage/CoverageCanvas.java +++ b/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/coverage/CoverageCanvas.java @@ -207,6 +207,14 @@ public class CoverageCanvas extends MapCanvasAWT { */ private boolean hasCoverageOrResource; + /** + * Whether to skip the rendering of the coverage. + * OTher features such as the isolines may still be rendered. + * + * @see #setCoverageHidden(boolean) + */ + private boolean isCoverageHidden; + /** * A subspace of the grid coverage extent where all dimensions except two have a size of 1 cell. * May be {@code null} if the grid coverage has only two dimensions with a size greater than 1 cell. @@ -488,6 +496,19 @@ public class CoverageCanvas extends MapCanvasAWT { } } + /** + * Set whether to skip the rendering of the coverage. + * OTher features such as the isolines may still be rendered. + * + * @param hidden whether to skip the rendering of the coverage. + */ + final void setCoverageHidden(final boolean hidden) { + if (isCoverageHidden != hidden) { + isCoverageHidden = hidden; + requestRepaint(); + } + } + /** * Invoked when image colors changed. Derived features such are isolines are assumed unchanged. * This method should be invoked explicitly when the {@link Colorizer} changes its internal state. @@ -945,13 +966,13 @@ public class CoverageCanvas extends MapCanvasAWT { /** * The {@link #recoloredImage} after resampling is applied. * May be {@code null} if not yet computed, in which case it will be computed by {@link #render()}. + * This image should not be cached after rendering operation is completed. */ private RenderedImage resampledImage; /** - * The resampled image with tiles computed in advance. The set of prefetched - * tiles may differ at each rendering event. This image should not be cached - * after rendering operation is completed. + * The resampled image with tiles computed in advance. + * The set of prefetched tiles may differ at each rendering event. */ private RenderedImage prefetchedImage; @@ -965,6 +986,12 @@ public class CoverageCanvas extends MapCanvasAWT { */ private AffineTransform resampledToDisplay; + /** + * Whether to skip the rendering of the coverage. + * OTher features such as the isolines may still be rendered. + */ + private final boolean isCoverageHidden; + /** * Snapshot of information required for rendering isolines, or {@code null} if none. */ @@ -983,6 +1010,7 @@ public class CoverageCanvas extends MapCanvasAWT { displayBounds = canvas.getDisplayBounds(); objectivePOI = canvas.getPointOfInterest(true); recoloredImage = canvas.derivedImages.get(data.selectedDerivative); + isCoverageHidden = canvas.isCoverageHidden; if (data.validateCRS(objectiveCRS)) { resampledImage = canvas.resampledImage; } @@ -1092,7 +1120,9 @@ public class CoverageCanvas extends MapCanvasAWT { * We cannot invoke it sooner because it needs some `resampleAndConvert(…)` results. */ final Future<Isolines[]> newIsolines = data.generate(isolines); - prefetchedImage = data.prefetch(resampledImage, resampledToDisplay, displayBounds); + if (!isCoverageHidden) { + prefetchedImage = data.prefetch(resampledImage, resampledToDisplay, displayBounds); + } if (newIsolines != null) { IsolineRenderer.complete(isolines, newIsolines); } @@ -1112,7 +1142,7 @@ public class CoverageCanvas extends MapCanvasAWT { ((TileErrorHandler.Executor) prefetchedImage).execute( () -> gr.drawRenderedImage(RenderingWorkaround.wrap(prefetchedImage), resampledToDisplay), new TileErrorHandler(data.processor.getErrorHandler(), CoverageCanvas.class, "paint")); - } else { + } else if (!isCoverageHidden) { gr.drawRenderedImage(RenderingWorkaround.wrap(prefetchedImage), resampledToDisplay); } if (isolines != null) { @@ -1399,7 +1429,7 @@ public class CoverageCanvas extends MapCanvasAWT { } }); if (error != null) { - unexpectedException(error); + Logging.recoverableException(LOGGER, TileReadListener.class, "run", error); } } diff --git a/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/coverage/CoverageControls.java b/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/coverage/CoverageControls.java index 9e39751247..2daf592fc9 100644 --- a/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/coverage/CoverageControls.java +++ b/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/coverage/CoverageControls.java @@ -18,9 +18,9 @@ package org.apache.sis.gui.coverage; import java.util.List; import java.util.Locale; +import javafx.application.Platform; import javafx.scene.control.TitledPane; import javafx.scene.control.ChoiceBox; -import javafx.scene.control.CheckBox; import javafx.scene.control.Label; import javafx.scene.control.TableView; import javafx.scene.control.Tooltip; @@ -29,18 +29,25 @@ import javafx.scene.layout.Region; import javafx.scene.layout.VBox; import javafx.scene.layout.Priority; import javafx.collections.ObservableList; +import org.apache.sis.image.Interpolation; import org.apache.sis.coverage.Category; import org.apache.sis.coverage.grid.GridCoverage; import org.apache.sis.storage.GridCoverageResource; +import org.apache.sis.storage.DataStoreException; import org.apache.sis.gui.dataset.WindowHandler; import org.apache.sis.gui.map.MapMenu; -import org.apache.sis.image.Interpolation; +import org.apache.sis.gui.map.style.MapLayer; +import org.apache.sis.gui.map.style.MapContextView; import org.apache.sis.gui.internal.GUIUtilities; import org.apache.sis.gui.internal.Styles; import org.apache.sis.gui.internal.Resources; +import org.apache.sis.gui.internal.DataStoreOpener; +import org.apache.sis.gui.internal.BackgroundThreads; +import static org.apache.sis.gui.internal.LogHandler.LOGGER; import org.apache.sis.gui.controls.ValueColorMapper; import org.apache.sis.gui.controls.SyncWindowList; import org.apache.sis.util.resources.Vocabulary; +import org.apache.sis.util.logging.Logging; /** @@ -58,6 +65,12 @@ final class CoverageControls extends ViewAndControls { */ final CoverageCanvas view; + /** + * Provides widget for controlling the rendering of the coverage. + * This is the root of the {@link MapContextView} tree. + */ + private final StyleController style; + /** * The control showing categories and their colors for the current coverage. */ @@ -100,6 +113,13 @@ final class CoverageControls extends ViewAndControls { final MapMenu menu = new MapMenu(view); menu.addReferenceSystems(owner.referenceSystems); menu.addCopyOptions(status); + /* + * "Layers" section with the following controls: + * - Tree of layers associated to the coverage (styling, isolines, visual indication of loaded tiles). + */ + final var layers = new MapContextView(resources); + style = new StyleController(view, resources); + layers.setRootItem(style); /* * "Display" section with the following controls: * - Current CRS @@ -129,19 +149,13 @@ final class CoverageControls extends ViewAndControls { styling = new CoverageStyling(view); categoryTable = styling.createCategoryTable(resources, vocabulary); VBox.setVgrow(categoryTable, Priority.ALWAYS); - /* - * Whether to show a visual indication of which tiles are read. - */ - final var showTileReads = new CheckBox(resources.getString(Resources.Keys.ShowTileReadEvents)); - showTileReads.selectedProperty().addListener((p,o,n) -> view.showTileReads(n)); /* * All sections put together. */ displayPane = new VBox( labelOfGroup(vocabulary, Vocabulary.Keys.ReferenceSystem, crsControl, true), crsControl, labelOfGroup(vocabulary, Vocabulary.Keys.Values, valuesControl, false), valuesControl, - labelOfGroup(vocabulary, Vocabulary.Keys.Categories, categoryTable, false), categoryTable, - showTileReads); + labelOfGroup(vocabulary, Vocabulary.Keys.Categories, categoryTable, false), categoryTable); } /* * "Isolines" section with the following controls: @@ -168,6 +182,7 @@ final class CoverageControls extends ViewAndControls { */ final TitledPane deferred; // Control to be built only if requested. controlPanes = new TitledPane[] { + new TitledPane(vocabulary.getString(Vocabulary.Keys.Layers), layers.getView()), new TitledPane(vocabulary.getString(Vocabulary.Keys.Display), displayPane), new TitledPane(vocabulary.getString(Vocabulary.Keys.Isolines), isolinesPane), new TitledPane(resources.getString(Resources.Keys.Windows), windows.getView()), @@ -196,6 +211,23 @@ final class CoverageControls extends ViewAndControls { if (isAdjustingSlice) { return; } + BackgroundThreads.execute(() -> { + final Locale locale = owner.getLocale(); + String name; + try { + name = DataStoreOpener.findLabel(resource, locale, true); + } catch (DataStoreException | RuntimeException e) { + // Declare `setResource` as the public method invoking (indirectly) this method. + Logging.recoverableException(LOGGER, CoverageExplorer.class, "setResource", e); + name = DataStoreOpener.fallbackLabel(resource, locale); + } + final var layer = new MapLayer<>(resource, name); + Platform.runLater(() -> { + if (owner.getResource() == resource) { // Verify that the resource did not changed concurrently. + style.setData(layer); + } + }); + }); final ObservableList<Category> items = categoryTable.getItems(); if (coverage == null) { items.clear(); diff --git a/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/coverage/CoverageExplorer.java b/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/coverage/CoverageExplorer.java index 71ac26f3b2..3379cc7228 100644 --- a/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/coverage/CoverageExplorer.java +++ b/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/coverage/CoverageExplorer.java @@ -33,6 +33,7 @@ import javafx.scene.layout.Region; import javafx.event.ActionEvent; import javafx.beans.property.ObjectProperty; import javafx.beans.property.SimpleObjectProperty; +import org.apache.sis.util.logging.Logging; import org.apache.sis.storage.DataStoreException; import org.apache.sis.storage.GridCoverageResource; import org.apache.sis.coverage.grid.GridCoverage; @@ -44,6 +45,8 @@ import org.apache.sis.gui.internal.Resources; import org.apache.sis.gui.internal.ToolbarButton; import org.apache.sis.gui.internal.NonNullObjectProperty; import org.apache.sis.gui.internal.PrivateAccess; +import org.apache.sis.gui.internal.BackgroundThreads; +import static org.apache.sis.gui.internal.LogHandler.LOGGER; import org.apache.sis.gui.referencing.RecentReferenceSystems; import org.apache.sis.gui.dataset.WindowHandler; import org.apache.sis.gui.map.StatusBar; @@ -608,16 +611,17 @@ public class CoverageExplorer extends Widget { */ final void notifyDataChanged(final GridCoverageResource resource, final GridCoverage coverage) { if (coverage != null) { - String name; - try { - name = DataStoreOpener.findLabel(resource, getLocale(), true); - } catch (DataStoreException e) { - name = e.getLocalizedMessage(); - if (name == null) { - name = e.getClass().getSimpleName(); + BackgroundThreads.execute(() -> { + String name; + try { + name = DataStoreOpener.findLabel(resource, getLocale(), true); + } catch (DataStoreException | RuntimeException e) { + // Declare `setResource` as the public method invoking (indirectly) this method. + Logging.recoverableException(LOGGER, CoverageExplorer.class, "setResource", e); + name = DataStoreOpener.fallbackLabel(resource, getLocale()); } - } - referenceSystems.setGridReferencing(true, Map.of(name, coverage.getGridGeometry())); + referenceSystems.setGridReferencing(true, Map.of(name, coverage.getGridGeometry())); + }); } /* * Following calls will NOT forward the new values to the views because this `notifyDataChanged(…)` diff --git a/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/coverage/StyleController.java b/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/coverage/StyleController.java new file mode 100644 index 0000000000..18548647a8 --- /dev/null +++ b/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/coverage/StyleController.java @@ -0,0 +1,77 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.sis.gui.coverage; + +import javafx.scene.control.TreeItem; +import javafx.collections.ObservableList; +import org.apache.sis.gui.internal.Resources; +import org.apache.sis.gui.map.style.MapItem; +import org.apache.sis.gui.map.style.ItemController; +import org.apache.sis.gui.map.style.MapLayer; +import org.apache.sis.storage.GridCoverageResource; +import org.apache.sis.storage.tiling.TiledGridCoverageResource; + + +/** + * A node shown in a tree of map items for controlling the rendering of a single grid coverage. + * + * @author Martin Desruisseaux (Geomatys) + */ +final class StyleController extends ItemController { + /** + * Whether to show a visual indication of which tiles are read. + */ + private final ItemController showTileReads; + + /** + * Creates a controller for a map layer. + * The controller is initially selected. + * + * @param view where the coverage will be rendered. + * @param resources resources for localized <abbr>GUI</abbr> elements. + */ + StyleController(final CoverageCanvas view, final Resources resources) { + setSelected(true); + setIndependent(true); + selectedProperty().addListener((p,o,n) -> view.setCoverageHidden(!n)); + showTileReads = new ItemController(new MapItem(resources.getString(Resources.Keys.ShowTileReadEvents))); + showTileReads.selectedProperty().addListener((p,o,n) -> view.showTileReads(n)); + showTileReads.setIndependent(true); + getChildren().add(showTileReads); + } + + /** + * Sets the item to show. + * + * @param item the new item, or {@code null} if none. + */ + final void setData(final MapLayer<GridCoverageResource> item) { + setValue(item); + final boolean isTiled = (item.resource instanceof TiledGridCoverageResource); + final ObservableList<TreeItem<MapItem>> children = getChildren(); + final int last = children.size() - 1; + if (last >= 0 && children.get(last) == showTileReads) { + if (!isTiled) { + children.remove(last); + } + } else { + if (isTiled) { + children.add(showTileReads); + } + } + } +} diff --git a/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/dataset/WindowHandler.java b/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/dataset/WindowHandler.java index 9b2cb5a22e..c6226552bb 100644 --- a/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/dataset/WindowHandler.java +++ b/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/dataset/WindowHandler.java @@ -28,7 +28,6 @@ import javafx.beans.value.ChangeListener; import javafx.beans.property.StringProperty; import javafx.beans.property.SimpleStringProperty; import javafx.collections.ObservableList; -import org.apache.sis.util.resources.Vocabulary; import org.apache.sis.util.resources.Errors; import org.apache.sis.storage.Resource; import org.apache.sis.storage.DataStoreException; @@ -52,7 +51,7 @@ import static org.apache.sis.gui.internal.LogHandler.LOGGER; * It may also be a tile in a mosaic of windows. * * @author Martin Desruisseaux (Geomatys) - * @version 1.4 + * @version 1.7 * @since 1.3 */ public abstract class WindowHandler { @@ -104,16 +103,20 @@ public abstract class WindowHandler { * @return {@code this} for method call chaining. */ final WindowHandler finish() { - String text; - if (manager.main == this) { - text = Resources.forLocale(manager.locale).getString(Resources.Keys.MainWindow); - } else try { - text = DataStoreOpener.findLabel(getResource(), manager.locale, true); - } catch (DataStoreException | RuntimeException e) { - text = Vocabulary.forLocale(manager.locale).getString(Vocabulary.Keys.NotKnown); - Logging.recoverableException(LOGGER, WindowHandler.class, "<init>", e); - } - title.set(text); + BackgroundThreads.execute(() -> { + String text; + final Locale locale = manager.locale; + if (manager.main == this) { + text = Resources.forLocale(locale).getString(Resources.Keys.MainWindow); + } else try { + text = DataStoreOpener.findLabel(getResource(), locale, true); + } catch (DataStoreException | RuntimeException e) { + text = DataStoreOpener.fallbackLabel(getResource(), locale); + Logging.recoverableException(LOGGER, WindowHandler.class, "<init>", e); + } + final String label = text; // Because lambda functions want final variable. + Platform.runLater(() -> title.set(label)); + }); manager.modifiableWindowList.add(this); return this; } diff --git a/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/internal/DataStoreOpener.java b/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/internal/DataStoreOpener.java index 8e2a3317ce..8ac65413e9 100644 --- a/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/internal/DataStoreOpener.java +++ b/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/internal/DataStoreOpener.java @@ -43,13 +43,13 @@ import org.apache.sis.storage.StorageConnector; import org.apache.sis.storage.DataStoreException; import org.apache.sis.storage.DataStores; import org.apache.sis.storage.DataStore; +import org.apache.sis.storage.folder.ConcurrentCloser; import org.apache.sis.util.collection.Cache; import org.apache.sis.util.resources.Vocabulary; import org.apache.sis.util.internal.shared.Strings; import org.apache.sis.io.stream.IOUtilities; import org.apache.sis.io.stream.ChannelFactory; import org.apache.sis.io.stream.InternalOptionKey; -import org.apache.sis.storage.folder.ConcurrentCloser; import org.apache.sis.gui.DataViewer; @@ -212,7 +212,7 @@ public final class DataStoreOpener extends Task<DataStore> { } /** - * Set the provider of wrappers around channels used for reading data. + * Sets the provider of wrappers around channels used for reading data. * Those wrappers can be used for listening to file accesses. * * @param wrapper the wrapper, or {@code null} if none. @@ -248,21 +248,20 @@ public final class DataStoreOpener extends Task<DataStore> { 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; + GenericName identifier = resource.getIdentifier().orElse(null); + if (identifier != null) { + if (qualified) { + String t = string(identifier.toFullyQualifiedName().toInternationalString(), locale); + if (t != null) return t; // Should never be null, but we are paranoiac. + } else { + identifier = identifier.tip(); + } } /* * 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 execute the code path for identifier (i.e. try to find a more informative text). */ - GenericName name = resource.getIdentifier().orElse(null); Collection<? extends Identification> identifications = null; final Metadata metadata = resource.getMetadata(); if (metadata != null) { @@ -272,7 +271,7 @@ public final class DataStoreOpener extends Task<DataStore> { final Citation citation = identification.getCitation(); if (citation != null) { final String t = string(citation.getTitle(), locale); - if (t != null && (name == null || !t.equals(name.tip().toString()))) { + if (t != null && (identifier == null || !t.equals(identifier.tip().toString()))) { return t; } } @@ -284,17 +283,25 @@ public final class DataStoreOpener extends Task<DataStore> { * We search for explicitly declared identifier first before to fallback on * metadata identifier, because the latter is more subject to interpretation. */ - if (name != null) { - name = qualified ? name.toFullyQualifiedName() : name.tip(); - final String t = string(name.toInternationalString(), locale); + if (identifier != null) { + String t = string(identifier.toInternationalString(), locale); if (t != null) return t; } if (identifications != null) { for (final Identification identification : identifications) { - final String t = Citations.getIdentifier(identification.getCitation()); + String t = Citations.getIdentifier(identification.getCitation()); if (t != null) return t; } } + /* + * Check for data store display name in last resort, because it depends on the input object. + * It may be a filename if the resource was opened from a `java.nio.Path`, but may also be a + * class name if the resource was opened from a stream. + */ + if (resource instanceof DataStore) { + String t = Strings.trimOrNull(((DataStore) resource).getDisplayName()); + if (t != null) return t; + } } finally { LogHandler.loadingStop(logID); } @@ -302,6 +309,23 @@ public final class DataStoreOpener extends Task<DataStore> { return Vocabulary.forLocale(locale).getString(Vocabulary.Keys.Unnamed); } + /** + * Returns the label to use as a fallback if {@link #findLabel(Resource, Locale, boolean)} threw an exception. + * The fallback may be the "Not known" text. Note that we do not use the "Unnamed" text because we don't know + * if the resource is really unnamed. This method should be quick enough for invocation from the JavaFX thread. + * + * @param resource the resource for which to get a label, or {@code null}. + * @param locale the locale to use for localizing international strings. + * @return a label to use as a fallback (never {@code null}). + */ + public static String fallbackLabel(final Resource resource, final Locale locale) { + if (resource instanceof DataStore) { + final String text = Strings.trimOrNull(((DataStore) resource).getDisplayName()); + if (text != null) return text; + } + return Vocabulary.forLocale(locale).getString(Vocabulary.Keys.NotKnown); + } + /** * Returns the given international string as a non-empty localized string, or {@code null} if none. */ diff --git a/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/map/style/ItemController.java b/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/map/style/ItemController.java new file mode 100644 index 0000000000..86e14a75ff --- /dev/null +++ b/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/map/style/ItemController.java @@ -0,0 +1,50 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.sis.gui.map.style; + +import javafx.scene.control.CheckBoxTreeItem; + + +/** + * Base class of controller for configuring a map item (data and style). + * An {@code ItemController} contains indirectly the following properties: + * + * <ul> + * <li>A human-readable {@linkplain MapItem#title title} to show in the <abbr>GUI</abbr>.</li> + * <li>A narrative {@linkplain MapItem#description description} providing more details.</li> + * <li>Whether the map item {@linkplain #selectedProperty() should be shown} on the map.</li> + * </ul> + * + * @author Johann Sorel (Geomatys) + * @author Martin Desruisseaux (Geomatys) + */ +public class ItemController extends CheckBoxTreeItem<MapItem> { + /** + * Creates an initially empty controller. + */ + public ItemController() { + } + + /** + * Creates a controller for the given map item. + * + * @param item the map item, or {@code null} if none. + */ + public ItemController(final MapItem item) { + super(item); + } +} diff --git a/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/map/style/MapContextView.java b/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/map/style/MapContextView.java new file mode 100644 index 0000000000..0c3d8d7cef --- /dev/null +++ b/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/map/style/MapContextView.java @@ -0,0 +1,122 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.sis.gui.map.style; + +import javafx.util.Callback; +import javafx.util.StringConverter; +import javafx.beans.value.ObservableValue; +import javafx.geometry.Orientation; +import javafx.scene.control.SplitPane; +import javafx.scene.control.TreeView; +import javafx.scene.control.TreeCell; +import javafx.scene.control.TreeItem; +import javafx.scene.control.CheckBoxTreeItem; +import javafx.scene.control.cell.CheckBoxTreeCell; +import javafx.scene.layout.Region; +import org.apache.sis.gui.Widget; +import org.apache.sis.gui.internal.Resources; + + +/** + * Tree of portrayal objects such as map layers, together with controls for configuring their appearance. + * When an item in the tree is selected, controls for configuring that item are shown. + * + * @author Johann Sorel (Geomatys) + * @author Martin Desruisseaux (Geomatys) + */ +public class MapContextView extends Widget { + /** + * The method to invoke for creating tree cells for a map item. + */ + private static final class CellFactory extends StringConverter<TreeItem<MapItem>> + implements Callback<TreeView<MapItem>, TreeCell<MapItem>> + { + /** The unique instance. */ + static final CellFactory INSTANCE = new CellFactory(); + + /** The function to invoke for getting the property describing whether the item is selected. */ + private final Callback<TreeItem<MapItem>, ObservableValue<Boolean>> selectedProperty; + + /** Creates the unique instance. */ + private CellFactory() { + selectedProperty = (item) -> item instanceof CheckBoxTreeItem<?> ? ((CheckBoxTreeItem<?>)item).selectedProperty() : null; + } + + /** Creates an initially empty tree cell for the given tree view. */ + @Override public TreeCell<MapItem> call(final TreeView<MapItem> item) { + return new CheckBoxTreeCell<>(selectedProperty, this); + } + + /** Returns the text to show in the tree node. */ + @Override public String toString(final TreeItem<MapItem> item) { + if (item != null) { + final MapItem value = item.getValue(); + if (value != null) return value.title; + } + return null; + } + + /** Returns a new item with the given text. Defined as a matter of principle, but should not be invoked. */ + @Override public TreeItem<MapItem> fromString(final String text) { + return new ItemController(new MapItem(text)); + } + } + + /** + * The map items to show in a tree. + */ + private final TreeView<MapItem> items; + + /** + * The tree of map items in the top part, + * together with configuration options for the selected item in the bottom part. + */ + private final SplitPane itemsAndConfiguration; + + /** + * Creates an initially empty tree. + * + * @param resources the resource to use for localized labels. + * Should not be in public API, appears in argument for now only for convenience. + */ + public MapContextView(final Resources resources) { + items = new TreeView<>(); + items.setCellFactory(CellFactory.INSTANCE); + itemsAndConfiguration = new SplitPane(items); + itemsAndConfiguration.setOrientation(Orientation.VERTICAL); + } + + /** + * Returns the encapsulated JavaFX component to add in a scene graph for making the tree visible. + * The {@code Region} subclass is implementation dependent and may change in any future SIS version. + * + * @return the JavaFX component to insert in a scene graph. + */ + @Override + public Region getView() { + return itemsAndConfiguration; + } + + /** + * Sets the root item. + * + * @param root the new root item, or {@code null} if none. + */ + public void setRootItem(final ItemController root) { + items.setRoot(root); + } +} diff --git a/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/referencing/package-info.java b/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/map/style/MapItem.java similarity index 52% copy from optional/src/org.apache.sis.gui/main/org/apache/sis/gui/referencing/package-info.java copy to optional/src/org.apache.sis.gui/main/org/apache/sis/gui/map/style/MapItem.java index e6ff89407e..c4f207de58 100644 --- a/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/referencing/package-info.java +++ b/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/map/style/MapItem.java @@ -14,13 +14,28 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +package org.apache.sis.gui.map.style; + /** - * Widgets about coordinate reference systems. + * Placeholder for {@code org.apache.sis.map.MapItem}. + * We use this temporary class because {@code org.apache.sis.map.MapItem} is in incubator. * - * @author Johann Sorel (Geomatys) - * @author Martin Desruisseaux (Geomatys) - * @version 1.6 - * @since 1.1 + * @todo Replace by {@link org.apache.sis.map.MapItem}. */ -package org.apache.sis.gui.referencing; +public class MapItem { + /** + * A human-readable short description for labeling the map item in a tree view. + * This title should be user friendly. It shall not be used as an identifier. + */ + public final String title; + + /** + * Creates a new map item with the given text. + * + * @param title a human-readable short description for labeling the map item in a tree view. + */ + public MapItem(final String title) { + this.title = title; + } +} diff --git a/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/referencing/package-info.java b/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/map/style/MapLayer.java similarity index 53% copy from optional/src/org.apache.sis.gui/main/org/apache/sis/gui/referencing/package-info.java copy to optional/src/org.apache.sis.gui/main/org/apache/sis/gui/map/style/MapLayer.java index e6ff89407e..055981d4cc 100644 --- a/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/referencing/package-info.java +++ b/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/map/style/MapLayer.java @@ -14,13 +14,30 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +package org.apache.sis.gui.map.style; + +import org.apache.sis.storage.Resource; + /** - * Widgets about coordinate reference systems. + * Placeholder for {@code org.apache.sis.map.MapLayer}. + * We use this temporary class because {@code org.apache.sis.map.MapLayer} is in incubator. * - * @author Johann Sorel (Geomatys) - * @author Martin Desruisseaux (Geomatys) - * @version 1.6 - * @since 1.1 + * @todo Replace by {@link org.apache.sis.map.MapLayer}. */ -package org.apache.sis.gui.referencing; +public final class MapLayer<R extends Resource> extends MapItem { + /** + * The resource managed by this map layer. + */ + public final R resource; + + /** + * Creates a new map item with the given resource. + * + * @param resource the resource managed by this map layer. + */ + public MapLayer(final R resource, final String title) { + super(title); + this.resource = resource; + } +} diff --git a/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/referencing/package-info.java b/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/map/style/package-info.java similarity index 83% copy from optional/src/org.apache.sis.gui/main/org/apache/sis/gui/referencing/package-info.java copy to optional/src/org.apache.sis.gui/main/org/apache/sis/gui/map/style/package-info.java index e6ff89407e..7d10bdd35a 100644 --- a/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/referencing/package-info.java +++ b/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/map/style/package-info.java @@ -16,11 +16,13 @@ */ /** - * Widgets about coordinate reference systems. + * Controls for styling a map. + * + * <STRONG>Do not use!</STRONG> + * + * This is an experimental package not yet ready for public usage. * * @author Johann Sorel (Geomatys) * @author Martin Desruisseaux (Geomatys) - * @version 1.6 - * @since 1.1 */ -package org.apache.sis.gui.referencing; +package org.apache.sis.gui.map.style; diff --git a/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/referencing/RecentReferenceSystems.java b/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/referencing/RecentReferenceSystems.java index c80476dc0e..26ff43c839 100644 --- a/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/referencing/RecentReferenceSystems.java +++ b/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/referencing/RecentReferenceSystems.java @@ -21,6 +21,8 @@ import java.util.List; import java.util.ArrayList; import java.util.Locale; import java.util.Objects; +import java.util.concurrent.atomic.AtomicInteger; +import javafx.application.Platform; import javafx.beans.property.ObjectProperty; import javafx.beans.property.SimpleObjectProperty; import javafx.beans.value.ChangeListener; @@ -81,7 +83,7 @@ import static org.apache.sis.gui.internal.LogHandler.LOGGER; * </ul> * * @author Martin Desruisseaux (Geomatys) - * @version 1.6 + * @version 1.7 * @since 1.1 */ public class RecentReferenceSystems { @@ -202,8 +204,8 @@ public class RecentReferenceSystems { * then is filtered by {@link #filterReferenceSystems(ImmutableEnvelope, ComparisonMode)} for resolving authority * codes and removing duplicated elements.</p> * - * <p>All accesses to this field and to {@link #isModified} field shall be done in a block synchronized - * on {@code systemsOrCodes}.</p> + * <p>All accesses to this field and all accesses to the {@link #isModified} + * field shall be done in a block synchronized on {@code systemsOrCodes}.</p> */ private final List<Object> systemsOrCodes; @@ -274,6 +276,12 @@ public class RecentReferenceSystems { */ private boolean isAdjusting; + /** + * For detecting when the {@code RecentReferenceSystems} state has been modified concurrently. + * Used only in contexts where the state is computed by a background thread. + */ + private final AtomicInteger modificationCount; + /** * Creates a builder which will use a default authority factory. * The factory will be capable to handle at least some EPSG codes. @@ -294,11 +302,12 @@ public class RecentReferenceSystems { public RecentReferenceSystems(final CRSAuthorityFactory factory, final Locale locale) { this.factory = factory; this.locale = locale; + controlValues = new ArrayList<>(); systemsOrCodes = new ArrayList<>(); cellIndiceSystems = new ArrayList<>(); + modificationCount = new AtomicInteger(); areaOfInterest = new SimpleObjectProperty<>(this, "areaOfInterest"); duplicationCriterion = new NonNullObjectProperty<>(this, "duplicationCriterion", ComparisonMode.ALLOW_VARIANT); - controlValues = new ArrayList<>(); duplicationCriterion.addListener((e) -> listModified()); areaOfInterest.addListener((e,o,n) -> { geographicAOI = Utils.toGeographic(RecentReferenceSystems.class, "areaOfInterest", n); @@ -317,6 +326,15 @@ public class RecentReferenceSystems { * <li>Sets the content of "Referencing by cell indices" sub-menu.</li> * </ul> * + * Above information are derived from the values of the {@code geometries} map. + * For each entry, the map key should be the {@link org.apache.sis.storage.Resource#getIdentifier() identifier} of + * the resource that provided the {@link GridGeometry} value, or other text allowing the user to identify the resource. + * Those keys are used for naming the <abbr>CRS</abbr>s of cell coordinates, which are different for each grid coverage. + * + * <p>This method can be invoked from any thread. The reference systems are collected in the current thread, + * then the state of this {@code RecentReferenceSystems} is updated in the JavaFX thread after all reference + * systems are ready.</p> + * * @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. * @@ -334,8 +352,8 @@ public class RecentReferenceSystems { 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; + final var alt = new CoordinateReferenceSystem[Math.max(derived.length - 1, 0)]; + CoordinateReferenceSystem firstCRS = null; for (final Map.Entry<String,GridGeometry> entry : geometries.entrySet()) { final GridGeometry gg = entry.getValue(); if (gg.isDefined(GridGeometry.ENVELOPE)) { @@ -343,8 +361,8 @@ public class RecentReferenceSystems { } if (gg.isDefined(GridGeometry.CRS)) { final CoordinateReferenceSystem crs = gg.getCoordinateReferenceSystem(); - if (preferred == null) { - preferred = crs; + if (firstCRS == null) { + firstCRS = crs; } else { alt[countCRS++] = crs; } @@ -353,31 +371,40 @@ public class RecentReferenceSystems { } } } - Envelope aoi = null; + Envelope union; try { - aoi = Envelopes.union(envelopes); // No need to trim null elements. + union = Envelopes.union(envelopes); // No need to trim null elements. } catch (TransformException e) { errorOccurred("setGridReferencing", e); + union = null; } /* * 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(Containers.viewAsUnmodifiableList(derived, 0, countCIR)); + final Envelope aoi = union; // Because lambda functions want a final variable. + final CoordinateReferenceSystem preferred = firstCRS; + final List<DerivedCRS> cellCRS = Containers.viewAsUnmodifiableList(derived, 0, countCIR); + final int stamp = modificationCount.incrementAndGet(); + Platform.runLater(() -> { + if (modificationCount.get() == stamp) { + 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(cellCRS); + } + areaOfInterest.set(aoi); + } finally { + referenceSystems = savedReferenceSystemList; + } + listModified(); } - areaOfInterest.set(aoi); - } finally { - referenceSystems = savedReferenceSystemList; - } - listModified(); + }); } /** diff --git a/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/referencing/package-info.java b/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/referencing/package-info.java index e6ff89407e..a7a22fcb04 100644 --- a/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/referencing/package-info.java +++ b/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/referencing/package-info.java @@ -20,7 +20,7 @@ * * @author Johann Sorel (Geomatys) * @author Martin Desruisseaux (Geomatys) - * @version 1.6 + * @version 1.7 * @since 1.1 */ package org.apache.sis.gui.referencing;
