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 b6f2655fcf2189f7e38a966cd13163565cc152e0 Author: Martin Desruisseaux <martin.desruisse...@geomatys.com> AuthorDate: Mon Nov 15 16:43:06 2021 +0100 Dissociate the process loading data in `GridView` and `CoverageCanvas`. Previous version was systematically loading data in `GridView` (even if it was not visible) then gave the `GridCoverage` reference to `CoverageCanvas` in order to load data only once. This new version loads data independently; we will rely more on `DataStore` resource caching. This separation is needed because `GridView` and `CoverageCanvas` will not load the same data anymore after we take pyramid in account. It also allows to create `GridView` only if needed. --- .../sis/gui/coverage/BandSelectionListener.java | 16 +- .../org/apache/sis/gui/coverage/CellFormat.java | 6 +- .../apache/sis/gui/coverage/CoverageControls.java | 192 ++++++++------------- .../apache/sis/gui/coverage/CoverageExplorer.java | 123 +++++++------ .../org/apache/sis/gui/coverage/GridControls.java | 47 ++--- .../java/org/apache/sis/gui/coverage/GridView.java | 138 +++++++-------- .../org/apache/sis/gui/coverage/ImageRequest.java | 121 +++++++------ .../sis/gui/coverage/InterpolationConverter.java | 112 ++++++++++++ .../sis/gui/coverage/PropertyPaneCreator.java | 65 +++++++ .../apache/sis/gui/coverage/ViewAndControls.java | 39 +++-- .../apache/sis/gui/dataset/ResourceExplorer.java | 185 ++++++++++---------- .../org/apache/sis/gui/dataset/SelectedData.java | 7 +- .../org/apache/sis/gui/dataset/WindowManager.java | 2 +- .../apache/sis/gui/metadata/MetadataSummary.java | 1 - 14 files changed, 607 insertions(+), 447 deletions(-) diff --git a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/BandSelectionListener.java b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/BandSelectionListener.java index 8cc743e..6c394a2 100644 --- a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/BandSelectionListener.java +++ b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/BandSelectionListener.java @@ -19,6 +19,7 @@ package org.apache.sis.gui.coverage; import javafx.beans.property.IntegerProperty; import javafx.beans.value.ChangeListener; import javafx.beans.value.ObservableValue; +import javafx.scene.control.SelectionModel; /** @@ -26,12 +27,23 @@ import javafx.beans.value.ObservableValue; * the selection is forwarded to the {@link GridView#bandProperty}. * * @author Martin Desruisseaux (Geomatys) - * @version 1.1 + * @version 1.2 * @since 1.1 * @module */ final class BandSelectionListener implements ChangeListener<Number> { /** + * Applies a bidirectional binding between a property and the selection in a tabme of sample dimensions. + * + * @param bandProperty the property for currently selected band. + * @param bandSelection the selection in a table of bands or sample dimensions. + */ + static void bind(final IntegerProperty bandProperty, final SelectionModel<?> bandSelection) { + bandSelection.selectedIndexProperty().addListener(new BandSelectionListener(bandProperty)); + bandProperty.addListener((p,o,n) -> bandSelection.clearAndSelect(n.intValue())); + } + + /** * The {@link GridView#bandProperty} to update when a new band is selected. */ private final IntegerProperty bandProperty; @@ -45,7 +57,7 @@ final class BandSelectionListener implements ChangeListener<Number> { /** * Creates a new listener which will modify the given property. */ - BandSelectionListener(final IntegerProperty bandProperty) { + private BandSelectionListener(final IntegerProperty bandProperty) { this.bandProperty = bandProperty; } diff --git a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/CellFormat.java b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/CellFormat.java index 67b67a5..7e416e3 100644 --- a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/CellFormat.java +++ b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/CellFormat.java @@ -96,7 +96,7 @@ final class CellFormat extends SimpleStringProperty { * method to invoke, which {@code NumberFormat.format(…)} method to invoke, and whether to set a format pattern * with fraction digits. */ - boolean dataTypeisInteger; + boolean dataTypeIsInteger; /** * Temporarily set to {@code true} when the user selects or enters a new pattern in a GUI control, then @@ -231,7 +231,7 @@ final class CellFormat extends SimpleStringProperty { * @param band index of the band to show in this grid view. */ final void configure(final RenderedImage image, final int band) { - if (dataTypeisInteger) { + if (dataTypeIsInteger) { cellFormat.setMaximumFractionDigits(0); } else { int n = getFractionDigits(image, band).orElse(1); @@ -285,7 +285,7 @@ final class CellFormat extends SimpleStringProperty { */ final String format(final Raster tile, final int x, final int y, final int b) { buffer.setLength(0); - if (dataTypeisInteger) { + if (dataTypeIsInteger) { final int integer = tile.getSample(x, y, b); final double value = integer; if (Double.doubleToRawLongBits(value) != Double.doubleToRawLongBits(lastValue)) { diff --git a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/CoverageControls.java b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/CoverageControls.java index 161bcc5..1f5d9ca 100644 --- a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/CoverageControls.java +++ b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/CoverageControls.java @@ -16,10 +16,8 @@ */ package org.apache.sis.gui.coverage; -import java.util.List; import java.util.Locale; -import java.util.Objects; -import java.lang.ref.Reference; +import java.lang.ref.WeakReference; import javafx.scene.control.Accordion; import javafx.scene.control.Control; import javafx.scene.control.TitledPane; @@ -27,31 +25,29 @@ import javafx.scene.layout.BorderPane; import javafx.scene.layout.GridPane; import javafx.scene.layout.Region; import javafx.scene.layout.VBox; -import javafx.beans.property.ObjectProperty; -import javafx.beans.value.ChangeListener; -import javafx.beans.value.ObservableValue; -import javafx.scene.control.ChoiceBox; +import javafx.collections.ObservableList; +import javafx.concurrent.Task; import javafx.scene.control.Label; import javafx.scene.control.TableView; import javafx.scene.control.Tooltip; import javafx.scene.paint.Color; -import javafx.util.StringConverter; -import org.apache.sis.storage.Resource; import org.apache.sis.coverage.Category; -import org.apache.sis.coverage.SampleDimension; import org.apache.sis.coverage.grid.GridCoverage; -import org.apache.sis.gui.referencing.RecentReferenceSystems; +import org.apache.sis.storage.DataStoreException; +import org.apache.sis.storage.Resource; import org.apache.sis.gui.map.MapMenu; import org.apache.sis.gui.map.StatusBar; -import org.apache.sis.image.Interpolation; +import org.apache.sis.internal.gui.control.ValueColorMapper; +import org.apache.sis.internal.gui.BackgroundThreads; import org.apache.sis.internal.gui.Styles; import org.apache.sis.internal.gui.Resources; import org.apache.sis.util.resources.Vocabulary; -import org.apache.sis.internal.gui.control.ValueColorMapper; /** * A {@link CoverageCanvas} with associated controls to show in a {@link CoverageExplorer}. + * This class installs bidirectional bindings between {@link CoverageCanvas} and the controls. + * The controls are updated when the coverage shown in {@link CoverageCanvas} is changed. * * @author Martin Desruisseaux (Geomatys) * @version 1.2 @@ -87,23 +83,22 @@ final class CoverageControls extends ViewAndControls { /** * Creates a new set of coverage controls. * - * @param vocabulary localized set of words, provided in argument because often known by the caller. - * @param coverage property containing the coverage to show. + * @param owner the widget which create this view. Can not be null. */ - @SuppressWarnings("ThisEscapedInObjectConstruction") - CoverageControls(final Vocabulary vocabulary, final ObjectProperty<GridCoverage> coverage, - final RecentReferenceSystems referenceSystems) - { - final Resources resources = Resources.forLocale(vocabulary.getLocale()); - view = new CoverageCanvas(vocabulary.getLocale()); + CoverageControls(final CoverageExplorer owner) { + super(owner); + final Locale locale = owner.getLocale(); + final Resources resources = Resources.forLocale(locale); + final Vocabulary vocabulary = Vocabulary.getResources(locale); + + view = new CoverageCanvas(locale); view.setBackground(Color.BLACK); - final StatusBar statusBar = new StatusBar(referenceSystems, view); - view.statusBar = statusBar; + view.statusBar = new StatusBar(owner.referenceSystems, view); imageAndStatus = new BorderPane(view.getView()); - imageAndStatus.setBottom(statusBar.getView()); + imageAndStatus.setBottom(view.statusBar.getView()); final MapMenu menu = new MapMenu(view); - menu.addReferenceSystems(referenceSystems); - menu.addCopyOptions(statusBar); + menu.addReferenceSystems(owner.referenceSystems); + menu.addCopyOptions(view.statusBar); /* * "Display" section with the following controls: * - Current CRS @@ -121,7 +116,7 @@ final class CoverageControls extends ViewAndControls { * - Interpolation */ final GridPane valuesControl = Styles.createControlGrid(0, - label(vocabulary, Vocabulary.Keys.Interpolation, createInterpolationButton(vocabulary.getLocale()))); + label(vocabulary, Vocabulary.Keys.Interpolation, InterpolationConverter.button(view))); final Label valuesHeader = labelOfGroup(vocabulary, Vocabulary.Keys.Values, valuesControl, false); /* * All sections put together. @@ -164,120 +159,75 @@ final class CoverageControls extends ViewAndControls { final TitledPane p4 = new TitledPane(vocabulary.getString(Vocabulary.Keys.Properties), null); controls = new Accordion(p1, p2, p3, p4); controls.setExpandedPane(p1); - view.coverageProperty.bind(coverage); + view.coverageProperty.addListener((p,o,n) -> coverageChanged(null, n)); p4.expandedProperty().addListener(new PropertyPaneCreator(view, p4)); } /** - * Creates the controls for choosing an interpolation method. + * Invoked in JavaFX thread after {@link CoverageCanvas#setCoverage(GridCoverage)}. + * This method updates the GUI with new information available. + * + * @param source the new source of coverage, or {@code null} if none. + * @param data the new coverage, or {@code null} if none. */ - private ChoiceBox<Interpolation> createInterpolationButton(final Locale locale) { - final ChoiceBox<Interpolation> b = new ChoiceBox<>(); - b.setConverter(new InterpolationConverter(locale)); - b.getItems().setAll(InterpolationConverter.INTERPOLATIONS); - b.getSelectionModel().select(view.getInterpolation()); - view.interpolationProperty.bind(b.getSelectionModel().selectedItemProperty()); - return b; + private void coverageChanged(final Resource source, final GridCoverage data) { + final ObservableList<Category> items = categoryTable.getItems(); + if (data == null) { + items.clear(); + } else { + final int visibleBand = 0; // TODO: provide a selector for the band to show. + items.setAll(data.getSampleDimensions().get(visibleBand).getCategories()); + } + owner.coverageChanged(source, data); } /** - * Gives a localized {@link String} instance for a given {@link Interpolation} and conversely. + * Sets the view content to the given coverage. + * This method starts a background thread. + * + * @param request the coverage to set, or {@code null} for clearing the view. */ - private static final class InterpolationConverter extends StringConverter<Interpolation> { - /** The interpolation supported by this converter. */ - static final Interpolation[] INTERPOLATIONS = { - Interpolation.NEAREST, Interpolation.BILINEAR, Interpolation.LANCZOS - }; - - /** Keys of localized names for each {@link #INTERPOLATIONS} element. */ - private static final short[] VOCABULARIES = { - Vocabulary.Keys.NearestNeighbor, Vocabulary.Keys.Bilinear, 0 - }; - - /** The locale to use for string representation. */ - private final Locale locale; - - /** Creates a new converter for the given locale. */ - InterpolationConverter(final Locale locale) { - this.locale = locale; - } - - /** Returns a string representation of the given item. */ - @Override public String toString(final Interpolation item) { - for (int i=0; i<INTERPOLATIONS.length; i++) { - if (INTERPOLATIONS[i].equals(item)) { - final short key = VOCABULARIES[i]; - if (key != 0) { - return Vocabulary.getResources(locale).getString(key); - } else if (item == Interpolation.LANCZOS) { - return "Lanczos"; - } - } - } - return Objects.toString(item); - } - - /** Returns the interpolation for the given text. */ - @Override public Interpolation fromString(final String text) { - final Vocabulary vocabulary = Vocabulary.getResources(locale); - for (int i=0; i<VOCABULARIES.length; i++) { - final short key = VOCABULARIES[i]; - final Interpolation item = INTERPOLATIONS[i]; - if ((key != 0 && vocabulary.getString(key).equalsIgnoreCase(text)) - || item.toString().equalsIgnoreCase(text)) - { - return item; - } - } - return null; + @Override + final void load(final ImageRequest request) { + if (request == null) { + view.setOriginator(null); + view.setCoverage(null); + } else { + view.setOriginator(request.resource != null ? new WeakReference<>(request.resource) : null); + request.getCoverage().ifPresentOrElse(view::setCoverage, + () -> BackgroundThreads.execute(new Loader(request))); } } /** - * Invoked the first time that the "Properties" pane is opened for building the JavaFX visual components. - * We deffer the creation of this pane because it is often not requested at all, since this is more for - * developers than users. + * A task for loading {@link GridCoverage} from a resource in a background thread. + * + * @todo Remove this loader, replace by a {@code resourceProperty} in {@link CoverageCanvas}. */ - private static final class PropertyPaneCreator implements ChangeListener<Boolean> { - /** A copy of {@link CoverageControls#view} reference. */ - private final CoverageCanvas view; + private final class Loader extends Task<GridCoverage> { + /** The coverage resource together with optional parameters for reading only a subset. */ + private final ImageRequest request; - /** The pane where to set the content. */ - private final TitledPane pane; + /** Creates a new task for loading a coverage from the specified resource. */ + Loader(final ImageRequest request) { + this.request = request; + } - /** Creates a new {@link ImagePropertyExplorer} constructor. */ - PropertyPaneCreator(final CoverageCanvas view, final TitledPane pane) { - this.view = view; - this.pane = pane; + /** Invoked in background thread for loading the coverage. */ + @Override protected GridCoverage call() throws DataStoreException { + request.load(this, true, false); + return request.getCoverage().orElse(null); } - /** Creates the {@link ImagePropertyExplorer} when {@link TitledPane#expandedProperty()} changed. */ - @Override public void changed(ObservableValue<? extends Boolean> property, Boolean oldValue, Boolean newValue) { - if (newValue) { - pane.expandedProperty().removeListener(this); - final ImagePropertyExplorer properties = view.createPropertyExplorer(); - properties.updateOnChange.bind(pane.expandedProperty()); - pane.setContent(properties.getView()); - } + /** Invoked in JavaFX thread after successful loading. */ + @Override protected void succeeded() { + view.setCoverage(getValue()); } - } - /** - * Invoked in JavaFX thread after {@link CoverageExplorer#setCoverage(ImageRequest)} completed. - * This method updates the GUI with new information available. - * - * @param data the new coverage, or {@code null} if none. - * @param originator the resource from which the data has been read, or {@code null} if unknown. - */ - @Override - final void coverageChanged(final GridCoverage data, final Reference<Resource> originator) { - view.setOriginator(originator); - if (data != null) { - final int visibleBand = 0; // TODO: provide a selector for the band to show. - final List<SampleDimension> bands = data.getSampleDimensions(); - categoryTable.getItems().setAll(bands.get(visibleBand).getCategories()); - } else { - categoryTable.getItems().clear(); + /** Invoked in JavaFX thread on failure. */ + @Override protected void failed() { + view.setCoverage(null); + request.reportError(imageAndStatus, getException()); } } 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 a06d273..698945a 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 @@ -17,8 +17,7 @@ package org.apache.sis.gui.coverage; import java.util.EnumMap; -import java.lang.ref.Reference; -import java.lang.ref.WeakReference; +import java.awt.image.RenderedImage; import javafx.application.Platform; import javafx.beans.DefaultProperty; import javafx.scene.control.Control; @@ -36,8 +35,6 @@ import org.apache.sis.internal.gui.Resources; import org.apache.sis.internal.gui.ToolbarButton; import org.apache.sis.internal.gui.NonNullObjectProperty; import org.apache.sis.util.ArgumentChecks; -import org.apache.sis.util.resources.Errors; -import org.apache.sis.util.resources.Vocabulary; import org.apache.sis.gui.referencing.RecentReferenceSystems; import org.apache.sis.gui.map.StatusBar; import org.apache.sis.gui.Widget; @@ -49,7 +46,8 @@ import org.apache.sis.storage.Resource; * The class contains two properties: * * <ul> - * <li>A {@link GridCoverage} supplied by user, or an {@link ImageRequest} for loading a coverage.</li> + * <li>A {@link GridCoverage} supplied by user. + * May be specified indirectly with an {@link ImageRequest} for loading the coverage.</li> * <li>A {@link View} type which specify how to show the coverage: * <ul> * <li>using {@link GridView} for showing numerical values in a table, or</li> @@ -151,6 +149,7 @@ public class CoverageExplorer extends Widget { * The type of control may change in any future SIS version. * * @see #getView() + * @see #onViewTypeSpecified(View) */ private SplitPane content; @@ -166,7 +165,7 @@ public class CoverageExplorer extends Widget { /** * Handles the {@link javafx.scene.control.ChoiceBox} and menu items for selecting a CRS. */ - private final RecentReferenceSystems referenceSystems; + final RecentReferenceSystems referenceSystems; /** * Creates an initially empty explorer with default view type. @@ -195,6 +194,7 @@ public class CoverageExplorer extends Widget { ArgumentChecks.ensureNonNull("type", type); coverageProperty = new SimpleObjectProperty<>(this, "coverage"); viewTypeProperty = new NonNullObjectProperty<>(this, "viewType", type); + viewTypeProperty.addListener((p,o,n) -> onViewTypeSpecified(n)); coverageProperty.addListener((p,o,n) -> onCoverageSpecified(n)); referenceSystems = new RecentReferenceSystems(); referenceSystems.addUserPreferences(); @@ -210,19 +210,33 @@ public class CoverageExplorer extends Widget { /** * Returns the view-control pair for the given view type. * The view-control pair is created when first needed. + * + * @param type type of view to obtain. + * @param load whether to force loading of data in the new type. */ - private ViewAndControls getViewAndControls(final View type) { + private ViewAndControls getViewAndControls(final View type, boolean load) { ViewAndControls c = views.get(type); if (c == null) { - final Vocabulary vocabulary = Vocabulary.getResources(getLocale()); switch (type) { - case TABLE: c = new GridControls(referenceSystems, vocabulary); break; - case IMAGE: c = new CoverageControls(vocabulary, coverageProperty, referenceSystems); break; + case TABLE: c = new GridControls(this); break; + case IMAGE: c = new CoverageControls(this); break; default: throw new AssertionError(type); } SplitPane.setResizableWithParent(c.controls(), Boolean.FALSE); SplitPane.setResizableWithParent(c.view(), Boolean.TRUE); views.put(type, c); + load = true; + } + /* + * If this explorer is showing a coverage, load data in the newly created view. + * Data may also be loaded because the view was previously unselected (hidden) + * and became selected (visible). + */ + if (load) { + final GridCoverage coverage = getCoverage(); + if (coverage != null) { + c.load(new ImageRequest(coverage, null)); + } } return c; } @@ -258,11 +272,10 @@ public class CoverageExplorer extends Widget { buttons[1 + type.ordinal()] = new Selector(type).createButton(group, type.icon, localized, type.tooltip); } final View type = getViewType(); - final ViewAndControls c = getViewAndControls(type); + final ViewAndControls c = getViewAndControls(type, false); group.selectToggle(group.getToggles().get(type.ordinal())); content = new SplitPane(c.controls(), c.view()); ToolbarButton.insert(content, buttons); - viewTypeProperty.addListener((p,o,n) -> onViewTypeSpecified(n)); /* * The divider position is supposed to be a fraction between 0 and 1. A value of 1 would mean * to give all the space to controls and no space to data, which is not what we want. However @@ -287,7 +300,7 @@ public class CoverageExplorer extends Widget { public final Region getDataView(final View type) { assert Platform.isFxApplicationThread(); ArgumentChecks.ensureNonNull("type", type); - return getViewAndControls(type).view(); + return getViewAndControls(type, false).view(); } /** @@ -301,7 +314,7 @@ public class CoverageExplorer extends Widget { public final Region getControls(final View type) { assert Platform.isFxApplicationThread(); ArgumentChecks.ensureNonNull("type", type); - return getViewAndControls(type).controls(); + return getViewAndControls(type, false).controls(); } /** @@ -321,7 +334,7 @@ public class CoverageExplorer extends Widget { final Toggle button = (Toggle) event.getSource(); if (button.isSelected()) { setViewType(type); - views.get(type).selector = button; // Should never null null. + views.get(type).selector = button; // Should never be null. } else { button.setSelected(true); // Prevent situation where all buttons are unselected. } @@ -335,6 +348,8 @@ public class CoverageExplorer extends Widget { * @return the coverage shown in this explorer, or {@code null} if none. * * @see #coverageProperty + * @see CoverageCanvas#getCoverage() + * @see GridView#getImage() */ public final GridCoverage getCoverage() { return coverageProperty.get(); @@ -349,9 +364,12 @@ public class CoverageExplorer extends Widget { * @param coverage the data to show in this explorer, or {@code null} if none. * * @see #coverageProperty + * @see CoverageCanvas#setCoverage(GridCoverage) + * @see GridView#setImage(RenderedImage) */ public final void setCoverage(final GridCoverage coverage) { coverageProperty.set(coverage); + // `onCoverageSpecified(…)` is indirectly invoked. } /** @@ -361,18 +379,19 @@ public class CoverageExplorer extends Widget { * the modifications will appear after an undetermined amount of time. * * @param source the coverage or resource to load, or {@code null} if none. + * + * @see GridView#setImage(ImageRequest) */ public final void setCoverage(final ImageRequest source) { assert Platform.isFxApplicationThread(); - if (source == null) { - setCoverage((GridCoverage) null); - } else if (source.listener != null) { - throw new IllegalArgumentException(Errors.getResources(getLocale()) - .getString(Errors.Keys.AlreadyInitialized_1, "listener")); - } else { - source.listener = this; - startLoading(source); + final ViewAndControls current = views.get(getViewType()); + for (final ViewAndControls c : views.values()) { + c.load(c == current ? source : null); + } + if (current == null) { + coverageChanged(null, null); } + // Else `coverageChanged(…)` will be invoked later after background thread finishes its work. } /** @@ -384,50 +403,19 @@ public class CoverageExplorer extends Widget { */ private void onCoverageSpecified(final GridCoverage coverage) { if (!isCoverageAdjusting) { - startLoading(null); // Clear data. - notifyCoverageChange(coverage, null); - if (coverage != null) { - startLoading(new ImageRequest(coverage, null)); // Start a background thread. - } + setCoverage((coverage != null) ? new ImageRequest(coverage, null) : null); } } /** - * Invoked in JavaFX thread by {@link GridView} after the coverage has been read. - * - * @param coverage the new coverage, or {@code null} if loading failed or has been cancelled. - * @param originator resource from which the data has been read, or {@code null} if unknown. - */ - final void onCoverageLoaded(final GridCoverage coverage, final Resource originator) { - notifyCoverageChange(coverage, (originator != null) ? new WeakReference<>(originator) : null); - isCoverageAdjusting = true; - try { - setCoverage(coverage); - } finally { - isCoverageAdjusting = false; - } - } - - /** - * Invoked by {@link #setCoverage(ImageRequest)} for starting data loading in a background thread. - * This method is invoked in JavaFX thread. - * - * @param source the coverage or resource to load, or {@code null} if none. - */ - private void startLoading(final ImageRequest source) { - final GridView main = (GridView) getViewAndControls(View.TABLE).view(); - main.setImage(source); - } - - /** * Invoked in JavaFX thread after {@link #setCoverage(ImageRequest)} completion for notifying controls * about the coverage change. Controls should update the GUI with new information available, * in particular the coordinate reference system and the list of sample dimensions. * - * @param data the new coverage, or {@code null} if none. - * @param originator the resource from which the data has been read, or {@code null} if unknown. + * @param source the new source of coverage, or {@code null} if none. + * @param data the new coverage, or {@code null} if none. */ - private void notifyCoverageChange(final GridCoverage data, final Reference<Resource> originator) { + final void coverageChanged(final Resource source, final GridCoverage data) { if (data != null) { final GridGeometry gg = data.getGridGeometry(); referenceSystems.areaOfInterest.set(gg.isDefined(GridGeometry.ENVELOPE) ? gg.getEnvelope() : null); @@ -435,8 +423,11 @@ public class CoverageExplorer extends Widget { referenceSystems.setPreferred(true, gg.getCoordinateReferenceSystem()); } } - for (final ViewAndControls c : views.values()) { - c.coverageChanged(data, originator); + isCoverageAdjusting = true; + try { + setCoverage(data); + } finally { + isCoverageAdjusting = false; } } @@ -469,11 +460,13 @@ public class CoverageExplorer extends Widget { * @param type the new way to show coverages in this explorer. */ private void onViewTypeSpecified(final View type) { - final ViewAndControls c = getViewAndControls(type); - content.getItems().setAll(c.controls(), c.view()); - final Toggle selector = c.selector; - if (selector != null) { - selector.setSelected(true); + final ViewAndControls c = getViewAndControls(type, true); + if (content != null) { + content.getItems().setAll(c.controls(), c.view()); + final Toggle selector = c.selector; + if (selector != null) { + selector.setSelected(true); + } } } } diff --git a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/GridControls.java b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/GridControls.java index 876eb97..d0d84b0 100644 --- a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/GridControls.java +++ b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/GridControls.java @@ -16,7 +16,6 @@ */ package org.apache.sis.gui.coverage; -import java.lang.ref.Reference; import javafx.beans.property.DoubleProperty; import javafx.collections.ObservableList; import javafx.scene.control.Accordion; @@ -31,13 +30,14 @@ import javafx.scene.layout.VBox; import org.apache.sis.storage.Resource; import org.apache.sis.coverage.SampleDimension; import org.apache.sis.coverage.grid.GridCoverage; -import org.apache.sis.gui.referencing.RecentReferenceSystems; import org.apache.sis.util.resources.Vocabulary; import org.apache.sis.internal.gui.Styles; /** * A {@link GridView} with associated controls to show in a {@link CoverageExplorer}. + * This class installs bidirectional bindings between {@link GridView} and the controls. + * The controls are updated when the image shown in {@link GridView} is changed. * * @author Martin Desruisseaux (Geomatys) * @version 1.2 @@ -63,14 +63,14 @@ final class GridControls extends ViewAndControls { /** * Creates a new set of grid controls. * - * @param referenceSystems the manager of reference systems chosen by the user, or {@code null} if none. - * @param vocabulary localized set of words, provided in argument because often known by the caller. + * @param owner the widget which create this view. Can not be null. */ - GridControls(final RecentReferenceSystems referenceSystems, final Vocabulary vocabulary) { - view = new GridView(referenceSystems); + GridControls(final CoverageExplorer owner) { + super(owner); + view = new GridView(this, owner.referenceSystems); + final Vocabulary vocabulary = Vocabulary.getResources(owner.getLocale()); sampleDimensions = new BandRangeTable(view.cellFormat).create(vocabulary); - sampleDimensions.getSelectionModel().selectedIndexProperty().addListener(new BandSelectionListener(view.bandProperty)); - view.bandProperty.addListener((p,o,n) -> onBandSpecified(n)); + BandSelectionListener.bind(view.bandProperty, sampleDimensions.getSelectionModel()); /* * "Coverage" section with the following controls: * - Coverage domain as a list of CRS dimensions with two of them selected (TODO). @@ -119,22 +119,13 @@ final class GridControls extends ViewAndControls { } /** - * Invoked when the band property changed. This method ensures that the selected row - * in the sample dimension table matches the band which is shown in the grid view. - */ - private void onBandSpecified(final Number band) { - sampleDimensions.getSelectionModel().clearAndSelect(band.intValue()); - } - - /** - * Invoked after {@link CoverageExplorer#setCoverage(ImageRequest)} for updating the table of - * sample dimensions when information become available. This method is invoked in JavaFX thread. + * Invoked after {@link GridView#setImage(ImageRequest)} for updating the table of sample + * dimensions when information become available. This method is invoked in JavaFX thread. * - * @param data the new coverage, or {@code null} if none. - * @param originator the resource from which the data has been read, or {@code null} if unknown. + * @param source the new source of coverage, or {@code null} if none. + * @param data the new coverage, or {@code null} if none. */ - @Override - final void coverageChanged(final GridCoverage data, final Reference<Resource> originator) { + final void coverageChanged(final Resource source, final GridCoverage data) { final ObservableList<SampleDimension> items = sampleDimensions.getItems(); if (data != null) { items.setAll(data.getSampleDimensions()); @@ -142,6 +133,18 @@ final class GridControls extends ViewAndControls { } else { items.clear(); } + owner.coverageChanged(source, data); + } + + /** + * Sets the view content to the given image. + * This method starts a background thread. + * + * @param request the image to set, or {@code null} for clearing the view. + */ + @Override + final void load(final ImageRequest request) { + view.setImage(request); } /** diff --git a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/GridView.java b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/GridView.java index 057c7e7..33ed675 100644 --- a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/GridView.java +++ b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/GridView.java @@ -38,10 +38,7 @@ import javafx.scene.paint.Paint; import org.apache.sis.util.ArgumentChecks; import org.apache.sis.coverage.grid.GridCoverage; import org.apache.sis.storage.DataStoreException; -import org.apache.sis.storage.GridCoverageResource; -import org.apache.sis.storage.event.StoreListeners; import org.apache.sis.internal.gui.BackgroundThreads; -import org.apache.sis.internal.gui.ExceptionReporter; import org.apache.sis.internal.gui.Styles; import org.apache.sis.gui.map.StatusBar; import org.apache.sis.gui.referencing.RecentReferenceSystems; @@ -59,14 +56,12 @@ import org.apache.sis.internal.coverage.j2d.ImageUtilities; * consider using the standard JavaFX {@link javafx.scene.control.TableView} instead.</p> * * @author Martin Desruisseaux (Geomatys) - * @version 1.1 + * @version 1.2 * * @see CoverageExplorer * * @since 1.1 * @module - * - * @todo Allow users to specify a {@link NumberFormat} pattern for writing sample values. */ @DefaultProperty("image") public class GridView extends Control { @@ -124,8 +119,8 @@ public class GridView extends Control { private final GridTileCache tiles; /** - * The most recently used tile. Cached separately because it will be the desired tile in the vast majority - * of cases. + * The most recently used tile. + * Cached separately because it will be the desired tile in the vast majority of cases. */ private GridTile lastTile; @@ -212,20 +207,31 @@ public class GridView extends Control { final StatusBar statusBar; /** + * If this grid view is associated with controls, the controls. Otherwise {@code null}. + * This is used only for notifications; a future version may use a more generic listener. + * We use this specific mechanism because there is no {@code coverageProperty} in this class. + * + * @see GridControls#coverageChanged(GridCoverage) + */ + private final GridControls controls; + + /** * Creates an initially empty grid view. The content can be set after * construction by a call to {@link #setImage(RenderedImage)}. */ public GridView() { - this(null); + this(null, null); } /** * Creates an initially empty grid view. The content can be set after * construction by a call to {@link #setImage(RenderedImage)}. * + * @param controls the controls of this grid view, or {@code null} if none. * @param referenceSystems the manager of reference systems chosen by the user, or {@code null} if none. */ - GridView(final RecentReferenceSystems referenceSystems) { + GridView(final GridControls controls, final RecentReferenceSystems referenceSystems) { + this.controls = controls; bandProperty = new BandProperty(); imageProperty = new SimpleObjectProperty<>(this, "image"); headerWidth = new SimpleDoubleProperty (this, "headerWidth", 60); @@ -299,6 +305,7 @@ public class GridView extends Control { */ public final void setImage(final RenderedImage image) { imageProperty.set(image); + // Above call will cause an invocation of `onImageSpecified(image)`. } /** @@ -308,24 +315,41 @@ public class GridView extends Control { * the modifications will appear after an undetermined amount of time. * * @param source the coverage or resource to load, or {@code null} if none. + * + * @see CoverageExplorer#setCoverage(ImageRequest) */ public void setImage(final ImageRequest source) { if (source == null) { setImage((RenderedImage) null); - } else { - final ImageLoader previous = loader; - loader = null; - if (previous != null) { - previous.cancel(BackgroundThreads.NO_INTERRUPT_DURING_IO); + if (controls != null) { + controls.coverageChanged(null, null); } - loader = new ImageLoader(source, true); + } else { + cancelLoader(); + loader = new ImageLoader(source); BackgroundThreads.execute(loader); } } /** - * A task for loading {@link GridCoverage} from a resource in a background thread, - * then fetching an image from it. + * Invoked after the image has been loaded or after failure. + * + * @param source the coverage or resource to load (never {@code null}). + * @param image the loaded image, or {@code null} on failure. + */ + private void setLoadedImage(final ImageRequest request, final RenderedImage image) { + loader = null; // Must be first for preventing cancellation. + setImage(image); + request.configure(statusBar); + if (controls != null) { + controls.coverageChanged(request.resource, request.getCoverage().orElse(null)); + } + } + + /** + * A task for loading {@link GridCoverage} from a resource in a background thread, then fetching an image from it. + * + * @see #setImage(ImageRequest) */ private final class ImageLoader extends Task<RenderedImage> { /** @@ -334,32 +358,24 @@ public class GridView extends Control { private final ImageRequest request; /** - * Whether the caller wants a grid coverage that contains real values or sample values. - */ - private final boolean converted; - - /** * Creates a new task for loading an image from the specified coverage resource. * - * @param request source of the image to load. - * @param converted {@code true} for a coverage containing converted values, + * @param request source of the image to load. */ - ImageLoader(final ImageRequest request, final boolean converted) { - this.request = request; - this.converted = converted; + ImageLoader(final ImageRequest request) { + this.request = request; } /** - * Loads the image. Current implementation reads the full image. If the coverage has more than 2 dimensions, - * only two of them are taken for the image; for all other dimensions, only the values at lowest index will - * be read. + * Loads the image. If the coverage has more than 2 dimensions, only two of them are taken for the image; + * for all other dimensions, only the values at lowest index will be read. * * @return the image loaded from the source given at construction time. * @throws DataStoreException if an error occurred while loading the grid coverage. */ @Override protected RenderedImage call() throws DataStoreException { - return request.load(this, converted); + return request.load(this, true, true); } /** @@ -368,10 +384,7 @@ public class GridView extends Control { */ @Override protected void succeeded() { - loader = null; - terminated(request.getCoverage().get()); // Should not be empty when the task is successful. - setImage(getValue()); // Must be after the coverage has been set. - request.configure(statusBar); + setLoadedImage(request, getValue()); } /** @@ -380,36 +393,8 @@ public class GridView extends Control { */ @Override protected void failed() { - terminated(null); - setImage((RenderedImage) null); - final GridCoverageResource resource = request.resource; - final GridView owner = GridView.this; - if (resource instanceof StoreListeners) { - ExceptionReporter.canNotReadFile(owner, ((StoreListeners) resource).getSourceName(), getException()); - } else { - ExceptionReporter.canNotUseResource(owner, getException()); - } - } - - /** - * Invoked in JavaFX thread in case of cancellation. - */ - @Override - protected void cancelled() { - terminated(null); - } - - /** - * Notifies listener that the given coverage has been read or failed to be read, - * then discards the listener. This method shall be invoked in JavaFX thread. - * A null argument means that the read operation failed (or has been cancelled. - */ - private void terminated(final GridCoverage result) { - final CoverageExplorer snapshot = request.listener; - request.listener = null; // Clear now in case an error happen. - if (snapshot != null) { - snapshot.onCoverageLoaded(result, request.resource); - } + setLoadedImage(request, null); + request.reportError(GridView.this, getException()); } } @@ -436,6 +421,18 @@ public class GridView extends Control { } /** + * If an image is loaded in a background thread, cancel the loading process. + * This method is invoked when a new image is specified. + */ + private void cancelLoader() { + final ImageLoader previous = loader; + if (previous != null) { + loader = null; + previous.cancel(BackgroundThreads.NO_INTERRUPT_DURING_IO); + } + } + + /** * Invoked (indirectly) when the user sets a new {@link RenderedImage}. * See {@link #setImage(RenderedImage)} for more description. * @@ -443,10 +440,7 @@ public class GridView extends Control { * @throws ArithmeticException if the "tile grid x/y offset" property is too large. */ private void onImageSpecified(final RenderedImage image) { - if (loader != null) { - loader.cancel(BackgroundThreads.NO_INTERRUPT_DURING_IO); - loader = null; - } + cancelLoader(); tiles.clear(); // Let garbage collector dispose the rasters. lastTile = null; width = 0; @@ -461,14 +455,14 @@ public class GridView extends Control { tileHeight = Math.max(1, image.getTileHeight()); tileGridXOffset = Math.subtractExact(image.getTileGridXOffset(), minX); tileGridYOffset = Math.subtractExact(image.getTileGridYOffset(), minY); - cellFormat.dataTypeisInteger = false; // To be kept consistent with `cellFormat` pattern. + cellFormat.dataTypeIsInteger = false; // To be kept consistent with `cellFormat` pattern. final SampleModel sm = image.getSampleModel(); if (sm != null) { // Should never be null, but we are paranoiac. final int numBands = sm.getNumBands(); if (getBand() >= numBands) { ((BandProperty) bandProperty).setNoCheck(numBands - 1); } - cellFormat.dataTypeisInteger = ImageUtilities.isIntegerType(sm); + cellFormat.dataTypeIsInteger = ImageUtilities.isIntegerType(sm); } cellFormat.configure(image, getBand()); } diff --git a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/ImageRequest.java b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/ImageRequest.java index dd3b5ba..df3566d 100644 --- a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/ImageRequest.java +++ b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/ImageRequest.java @@ -19,6 +19,7 @@ package org.apache.sis.gui.coverage; import java.util.Optional; import java.util.concurrent.FutureTask; import java.awt.image.RenderedImage; +import javafx.scene.Node; import org.apache.sis.storage.GridCoverageResource; import org.apache.sis.coverage.grid.GridDerivation; import org.apache.sis.coverage.grid.GridCoverage; @@ -27,8 +28,10 @@ import org.apache.sis.coverage.grid.GridExtent; import org.apache.sis.util.ArgumentChecks; import org.apache.sis.gui.map.StatusBar; import org.apache.sis.internal.gui.LogHandler; +import org.apache.sis.internal.gui.ExceptionReporter; import org.apache.sis.referencing.operation.transform.MathTransforms; import org.apache.sis.storage.DataStoreException; +import org.apache.sis.storage.event.StoreListeners; /** @@ -37,8 +40,12 @@ import org.apache.sis.storage.DataStoreException; * {@linkplain GridCoverage#render(GridExtent) rendering} and image in a background thread. * * @author Martin Desruisseaux (Geomatys) - * @version 1.1 - * @since 1.1 + * @version 1.2 + * + * @see GridView#setImage(ImageRequest) + * @see CoverageExplorer#setCoverage(ImageRequest) + * + * @since 1.1 * @module */ public class ImageRequest { @@ -49,16 +56,18 @@ public class ImageRequest { /** * The source from where to read the image, specified at construction time. - * Can not be {@code null}. + * May be {@code null} if {@link #coverage} instance was specified at construction time. */ final GridCoverageResource resource; /** * The source for rendering the image, specified at construction time. - * After construction, only one of {@link #resource} and {@link #coverage} is non-null. - * But after task execution, this field will be set to the coverage which has been read. + * After construction, only one of {@link #resource} and {@code coverage} fields is non-null. + * But after {@link Loader} task execution, this field will be set to the coverage which has been read. + * + * @see #getCoverage() */ - private GridCoverage coverage; + private volatile GridCoverage coverage; /** * Desired grid extent and resolution, or {@code null} for reading the whole domain. @@ -72,7 +81,7 @@ public class ImageRequest { * 0-based indices of sample dimensions to read, or {@code null} for reading them all. * This is used only if the data source is a {@link GridCoverageResource}. * - * @see #getDomain() + * @see #getRange() */ private final int[] range; @@ -95,13 +104,6 @@ public class ImageRequest { private static final double SLICE_RATIO = 0; /** - * The coverage explorer to inform after loading completed, or {@code null} if none. - * We do not provide a more generic listeners API for now, but we could do that - * in the future if there is a need. - */ - CoverageExplorer listener; - - /** * Creates a new request for loading an image from the specified resource. * If {@code domain} and {@code range} arguments are null, then the full coverage will be loaded. * For loading a smaller amount of data, sub-domain or sub-range can be specified as documented @@ -232,16 +234,18 @@ public class ImageRequest { * argument. This method is provided for the rare cases where it may be useful to specify both the {@code domain} * and the {@code sliceExtent}.</div> * - * @param sliceExtent subspace of the grid coverage extent to render. + * @param sliceExtent subspace of the grid coverage extent to render, or {@code null} for the whole extent. */ public final void setSliceExtent(final GridExtent sliceExtent) { this.sliceExtent = sliceExtent; } /** - * Computes a two dimension slice of the given grid geometry. + * Computes a two dimensional slice of the given grid geometry. * This method selects the two first dimensions having a size greater than 1 cell. * + * @todo Give control to user over which dimensions are selected. + * * @param domain the grid geometry in which to choose a two-dimensional slice. * @return a builder configured for returning the desired two-dimensional slice. */ @@ -260,53 +264,57 @@ public class ImageRequest { } /** - * Loads the image. Current implementation reads the full image. If the coverage has more than - * {@value #BIDIMENSIONAL} dimensions, only two of them are taken for the image; for all other - * dimensions, only the values at lowest index will be read. + * Loads the image. If the coverage has more than {@value #BIDIMENSIONAL} dimensions, + * only two of them are taken for the image; for all other dimensions, only the values + * at lowest index will be read. * * <p>If the {@link #coverage} field was null, it will be initialized as a side-effect. * No other fields will be modified.</p> * - * <p>This class does not need to be thread-safe since it should be used only once in a well-defined - * life cycle. We nevertheless synchronize as a safety (user could give the same {@code ImageRequest} - * to two different {@link CoverageExplorer} instances).</p> + * <h4>Thread safety</h4> + * This class does not need to be thread-safe because it should be used only once in a well-defined life cycle. + * We nevertheless synchronize as a safety (e.g. user could give the same {@code ImageRequest} to two different + * {@link CoverageExplorer} instances). In such case the {@link GridCoverage} will be loaded only once, + * but no caching is done for the {@link RenderedImage}. Image caching is generally not needed because + * {@link CoverageCanvas} does its own image rendering (it invokes this method with {@code render = false}). + * If two image renderings happen anyway, we rely on {@link org.apache.sis.storage.DataStore} caching.</p> * * @param task the task invoking this method (for checking for cancellation). * @param converted {@code true} for a coverage containing converted values, * or {@code false} for a coverage containing packed values. - * @return the image loaded from the source given at construction time, - * or {@code null} if the task has been cancelled. + * @param render {@code false} if only coverage reading is desired. + * @return the image loaded from the source given at construction time, or {@code null} + * if the task has been cancelled or if {@code render} is {@code false}. * @throws DataStoreException if an error occurred while loading the grid coverage. */ - final synchronized RenderedImage load(final FutureTask<?> task, final boolean converted) throws DataStoreException { + final synchronized RenderedImage load(final FutureTask<?> task, final boolean converted, final boolean render) + throws DataStoreException + { + GridCoverage cv = coverage; final Long id = LogHandler.loadingStart(resource); try { - if (coverage == null) { - GridGeometry domain = this.domain; - if (domain == null) { - domain = resource.getGridGeometry(); + if (cv == null) { + GridGeometry gg = domain; + if (gg == null) { + gg = resource.getGridGeometry(); } - if (domain != null && domain.getDimension() > BIDIMENSIONAL) { - domain = slice(domain).build(); + if (gg != null && gg.getDimension() > BIDIMENSIONAL) { + gg = slice(gg).build(); } - /* - * TODO: We restrict loading to a two-dimensional slice for now. - * Future version will need to give user control over slices. - */ - coverage = resource.read(domain, range); // May be long to execute. - coverage = coverage.forConvertedValues(converted); + cv = resource.read(gg, range); } - if (task.isCancelled()) { + coverage = cv = cv.forConvertedValues(converted); + if (!render || task.isCancelled()) { return null; } - GridExtent se = sliceExtent; - if (se == null) { - final GridGeometry cd = coverage.getGridGeometry(); - if (cd != null && cd.getDimension() > BIDIMENSIONAL) { // Should never be null but we are paranoiac. - se = slice(cd).getIntersection(); + GridExtent ex = sliceExtent; + if (ex == null) { + final GridGeometry gg = cv.getGridGeometry(); + if (gg != null && gg.getDimension() > BIDIMENSIONAL) { // Should never be null but we are paranoiac. + ex = slice(gg).getIntersection(); } } - return coverage.render(se); + return cv.render(ex); } finally { LogHandler.loadingStop(id); } @@ -315,13 +323,13 @@ public class ImageRequest { /** * Configures the given status bar with the geometry of the grid coverage we have just read. * This method is invoked in JavaFX thread after {@link GridView#setImage(ImageRequest)} - * successfully loaded in background thread a new image. + * loaded in background thread a new image, successfully or not. */ final void configure(final StatusBar bar) { final Long id = LogHandler.loadingStart(resource); try { final GridCoverage cv = coverage; - final GridExtent request = sliceExtent; + final GridExtent ex = sliceExtent; bar.applyCanvasGeometry(cv != null ? cv.getGridGeometry() : null); /* * By `GridCoverage.render(GridExtent)` contract, the `RenderedImage` pixel coordinates are relative @@ -330,10 +338,10 @@ public class ImageRequest { * modify `StatusBar.localToObjectiveCRS` because we do not associate it to a `MapCanvas`, so it will * not be overwritten by gesture events (zoom, pan, etc). */ - if (request != null) { - final double[] origin = new double[request.getDimension()]; + if (ex != null) { + final double[] origin = new double[ex.getDimension()]; for (int i=0; i<origin.length; i++) { - origin[i] = request.getLow(i); + origin[i] = ex.getLow(i); } bar.localToObjectiveCRS.set(MathTransforms.concatenate( MathTransforms.translation(origin), bar.localToObjectiveCRS.get())); @@ -342,4 +350,19 @@ public class ImageRequest { LogHandler.loadingStop(id); } } + + /** + * Reports an exception in a dialog box. This is a convenience method for + * {@link javafx.concurrent.Task#succeeded()} implementations. + * + * @param owner control in the window which will own the dialog, or {@code null} if unknown. + * @param exception the error that occurred. + */ + final void reportError(final Node owner, final Throwable exception) { + if (resource instanceof StoreListeners) { + ExceptionReporter.canNotReadFile(owner, ((StoreListeners) resource).getSourceName(), exception); + } else { + ExceptionReporter.canNotUseResource(owner, exception); + } + } } diff --git a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/InterpolationConverter.java b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/InterpolationConverter.java new file mode 100644 index 0000000..5fd0f1e --- /dev/null +++ b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/InterpolationConverter.java @@ -0,0 +1,112 @@ +/* + * 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 java.util.Locale; +import java.util.Objects; +import javafx.scene.control.ChoiceBox; +import javafx.util.StringConverter; +import org.apache.sis.image.Interpolation; +import org.apache.sis.util.resources.Vocabulary; + + +/** + * Gives a localized {@link String} instance for a given {@link Interpolation} and conversely. + * + * @author Martin Desruisseaux (Geomatys) + * @version 1.2 + * @since 1.1 + * @module + */ +final class InterpolationConverter extends StringConverter<Interpolation> { + /** + * Creates the controls for choosing an interpolation method on the given canvas. + * + * @param view the canvas for which to create a button for selecting the interpolation method. + * @return a button for applying an interpolation method on the given view. + */ + static ChoiceBox<Interpolation> button(final CoverageCanvas view) { + final ChoiceBox<Interpolation> b = new ChoiceBox<>(); + b.setConverter(new InterpolationConverter(view.getLocale())); + b.getItems().setAll(INTERPOLATIONS); + b.getSelectionModel().select(view.getInterpolation()); + view.interpolationProperty.bind(b.getSelectionModel().selectedItemProperty()); + return b; + } + + /** + * The interpolation supported by this converter. + */ + private static final Interpolation[] INTERPOLATIONS = { + Interpolation.NEAREST, Interpolation.BILINEAR, Interpolation.LANCZOS + }; + + /** + * Keys of localized names for each {@link #INTERPOLATIONS} element. + */ + private static final short[] VOCABULARIES = { + Vocabulary.Keys.NearestNeighbor, Vocabulary.Keys.Bilinear, 0 + }; + + /** + * The locale to use for string representation. + */ + private final Locale locale; + + /** + * Creates a new converter for the given locale. + */ + private InterpolationConverter(final Locale locale) { + this.locale = locale; + } + + /** + * Returns a string representation of the given item. + */ + @Override + public String toString(final Interpolation item) { + for (int i=0; i<INTERPOLATIONS.length; i++) { + if (INTERPOLATIONS[i].equals(item)) { + final short key = VOCABULARIES[i]; + if (key != 0) { + return Vocabulary.getResources(locale).getString(key); + } else if (item == Interpolation.LANCZOS) { + return "Lanczos"; + } + } + } + return Objects.toString(item); + } + + /** + * Returns the interpolation for the given text. + */ + @Override + public Interpolation fromString(final String text) { + final Vocabulary vocabulary = Vocabulary.getResources(locale); + for (int i=0; i<VOCABULARIES.length; i++) { + final short key = VOCABULARIES[i]; + final Interpolation item = INTERPOLATIONS[i]; + if ((key != 0 && vocabulary.getString(key).equalsIgnoreCase(text)) + || item.toString().equalsIgnoreCase(text)) + { + return item; + } + } + return null; + } +} diff --git a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/PropertyPaneCreator.java b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/PropertyPaneCreator.java new file mode 100644 index 0000000..066dd09 --- /dev/null +++ b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/PropertyPaneCreator.java @@ -0,0 +1,65 @@ +/* + * 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.beans.value.ChangeListener; +import javafx.beans.value.ObservableValue; +import javafx.scene.control.TitledPane; + + +/** + * Invoked the first time that the "Properties" pane is opened for building the JavaFX visual components. + * We deffer the creation of this pane because it is often not requested at all, since this is more for + * developers than users. + * + * @author Martin Desruisseaux (Geomatys) + * @version 1.2 + * @since 1.1 + * @module + */ +final class PropertyPaneCreator implements ChangeListener<Boolean> { + /** + * A copy of {@link CoverageControls#view} reference. + */ + private final CoverageCanvas view; + + /** + * The pane where to set the content. + */ + private final TitledPane pane; + + /** + * Creates a new {@link ImagePropertyExplorer} constructor. + */ + PropertyPaneCreator(final CoverageCanvas view, final TitledPane pane) { + this.view = view; + this.pane = pane; + } + + /** + * Creates the {@link ImagePropertyExplorer} when {@link TitledPane#expandedProperty()} changed. + */ + @Override + public void changed(ObservableValue<? extends Boolean> property, Boolean oldValue, Boolean newValue) { + if (newValue) { + pane.expandedProperty().removeListener(this); + final ImagePropertyExplorer properties = view.createPropertyExplorer(); + properties.updateOnChange.bind(pane.expandedProperty()); + pane.setContent(properties.getView()); + } + } +} diff --git a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/ViewAndControls.java b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/ViewAndControls.java index 398bb25..795eaa4 100644 --- a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/ViewAndControls.java +++ b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/ViewAndControls.java @@ -16,7 +16,6 @@ */ package org.apache.sis.gui.coverage; -import java.lang.ref.Reference; import javafx.geometry.Insets; import javafx.scene.control.Control; import javafx.scene.control.Label; @@ -31,8 +30,11 @@ import org.apache.sis.util.resources.IndexedResourceBundle; /** - * A {@link GridView} or {@link CoverageCanvas} together with the controls - * to show in a {@link CoverageExplorer}. + * A {@link GridView} or {@link CoverageCanvas} together with the controls to show in a {@link CoverageExplorer}. + * When the image or coverage is updated in a view, the {@link #coverageChanged(Resource, GridCoverage)} method + * is invoked, which will in turn update the {@link CoverageExplorer} properties. Coverage changes are applied + * on the view then propagated to {@code CoverageExplorer} rather than the opposite direction because loading + * mechanisms are implemented in the view (different views may load a different amount of data). * * @author Martin Desruisseaux (Geomatys) * @version 1.2 @@ -63,9 +65,25 @@ abstract class ViewAndControls { Toggle selector; /** + * The widget which contain this view. This is the widget to inform when the coverage changed. + * Subclasses should define the following method: + * + * {@preformat java + * private void coverageChanged(final Resource source, final GridCoverage data) { + * // Update subclass-specific controls here, before to forward to explorer. + * owner.coverageChanged(source, data); + * } + * } + */ + protected final CoverageExplorer owner; + + /** * Creates a new view-control pair. + * + * @param owner the widget which create this view. Can not be null. */ - ViewAndControls() { + ViewAndControls(final CoverageExplorer owner) { + this.owner = owner; } /** @@ -107,7 +125,7 @@ abstract class ViewAndControls { /** * Returns the font to assign to the label of a group of control. */ - static Font fontOfGroup() { + private static Font fontOfGroup() { return Font.font(null, FontWeight.BOLD, -1); } @@ -124,13 +142,10 @@ abstract class ViewAndControls { abstract Control controls(); /** - * Invoked in JavaFX thread after {@link CoverageExplorer#setCoverage(ImageRequest)} completed. - * Implementation should update the GUI with new information available, in particular - * the coordinate reference system and the list of sample dimensions. + * Sets the view content to the given resource, coverage or image. + * This method may start a background thread. * - * @param data the new coverage, or {@code null} if none. - * @param originator the resource from which the data has been read, or {@code null} if unknown. - * This is used for determining a target window for logging records. + * @param request the resource, coverage or image to set, or {@code null} for clearing the view. */ - abstract void coverageChanged(GridCoverage data, Reference<Resource> originator); + abstract void load(ImageRequest request); } diff --git a/application/sis-javafx/src/main/java/org/apache/sis/gui/dataset/ResourceExplorer.java b/application/sis-javafx/src/main/java/org/apache/sis/gui/dataset/ResourceExplorer.java index 1a42b35..a6b9fbc 100644 --- a/application/sis-javafx/src/main/java/org/apache/sis/gui/dataset/ResourceExplorer.java +++ b/application/sis-javafx/src/main/java/org/apache/sis/gui/dataset/ResourceExplorer.java @@ -132,20 +132,16 @@ public class ResourceExplorer extends WindowManager { * The {@link #features} and {@link #coverage} data will be set only if a data tab is visible, * because the data loading may be costly. * - * @see #isDataTabSet - * @see #isDataTabSelected() - * @see #updateDataTab(Resource, boolean) + * @see #updateDataTab(Resource) */ private final Tab viewTab, tableTab; /** - * Whether the setting of new values in {@link #viewTab} or {@link #tableTab} has been done. - * The new values are set only if a data tab is visible, and otherwise are delayed until one - * of data tab become visible. + * Whether one of the "view" or "table" tab is shown. They are the tabs requiring data loading. * - * @see #updateDataTab(Resource, boolean) + * @see #getCoverageView() */ - private boolean isDataTabSet; + private final BooleanBinding dataShown; /** * Whether one of the standard metadata tab (either "summary" or "metadata") is selected. @@ -177,10 +173,9 @@ public class ResourceExplorer extends WindowManager { final Tab summaryTab = new Tab(vocabulary.getString(Vocabulary.Keys.Summary), metadata.getView()); /* * "Visual" tab showing the raster data as an image. - * - * TODO: add contextual menu for creating a window showing directly the visual. */ viewTab = new Tab(vocabulary.getString(Vocabulary.Keys.Visual)); + viewTab.setContextMenu(new ContextMenu(SelectedData.setTabularView(createNewWindowMenu()))); /* * "Data" tab showing raster data as a table. */ @@ -219,12 +214,16 @@ public class ResourceExplorer extends WindowManager { SplitPane.setResizableWithParent(resources, Boolean.FALSE); SplitPane.setResizableWithParent(tabs, Boolean.TRUE); /* - * Register listeners last, for making sure we don't have undesired event. + * Register listeners last, for making sure we do not have undesired events. * Those listeners trig loading of various objects (data, standard metadata, * native metadata) when the corresponding tab become visible. */ - viewTab .selectedProperty().addListener((p,o,n) -> dataTabShown(n, true)); - tableTab.selectedProperty().addListener((p,o,n) -> dataTabShown(n, false)); + dataShown = viewTab.selectedProperty().or(tableTab.selectedProperty()); + dataShown.addListener((p,o,n) -> { + if (Boolean.FALSE.equals(o) && Boolean.TRUE.equals(n)) { + updateDataTabWithDefault(getSelectedResource()); + } + }); metadataShown = summaryTab.selectedProperty().or(metadataTab.selectedProperty()); metadataShown.addListener((p,o,n) -> { if (Boolean.FALSE.equals(o) && Boolean.TRUE.equals(n)) { @@ -357,15 +356,11 @@ public class ResourceExplorer extends WindowManager { */ selectedResource.set(resource); metadata.setMetadata(metadataShown.get() ? resource : null); - isDataTabSet = viewTab.isSelected() || tableTab.isSelected(); - updateDataTab(isDataTabSet ? resource : null, true); - if (!isDataTabSet) { - setNewWindowDisabled(!(resource instanceof GridCoverageResource || resource instanceof FeatureSet)); - } + updateDataTabWithDefault(dataShown.get() ? resource : null); /* - * Update the label is disabled state of the native metadata tab. We do not have a reliable way + * Update the label and disabled state of the native metadata tab. We do not have a reliable way * to know if metadata are present without trying to fetch them, so current implementation only - * checks if the data store implementation override the `getNativeMetadata()` method. + * checks if the data store implementation overrides the `getNativeMetadata()` method. */ String label = null; boolean disabled = true; @@ -378,7 +373,7 @@ public class ResourceExplorer extends WindowManager { try { disabled = resource.getClass().getMethod("getNativeMetadata").getDeclaringClass() == DataStore.class; } catch (NoSuchMethodException e) { - // Should never happen. + warning("onResourceSelected", resource, e); // Should never happen. } } nativeMetadataTab.setText(Objects.toString(label, defaultNativeTabLabel)); @@ -392,8 +387,9 @@ public class ResourceExplorer extends WindowManager { /** * Loads native metadata in a background thread and shows them in the "native metadata" tab. + * This method is invoked when the tab become visible, or when a new resource is loaded. */ - private final void loadNativeMetadata() { + private void loadNativeMetadata() { final Resource resource = getSelectedResource(); if (resource instanceof DataStore) { final DataStore store = (DataStore) resource; @@ -419,41 +415,54 @@ public class ResourceExplorer extends WindowManager { } /** - * Assigns the given resource into the {@link #viewTab} and {@link #tableTab}. Should be invoked only - * if a data tab is visible because data loading may be costly. It is caller responsibility to invoke - * {@link #setNewWindowDisabled(boolean)} after this method. + * Returns the enumeration value that describe the kind of content to show in {@link CoverageExplorer}. + * The type depends on which tab is visible. If no coverage data tab is visible, then returns null. * - * <p>The {@link #isDataTabSet} flag should be set before to invoke this method. If {@code true}, then - * the given resource is the final content and window menus will be updated accordingly by this method. - * If {@code false}, then the given resource is temporarily null and window menus should be updated by - * the caller instead of this method.</p> + * @see #dataShown + */ + private CoverageExplorer.View getCoverageView() { + if (viewTab.isSelected()) return CoverageExplorer.View.IMAGE; + if (tableTab.isSelected()) return CoverageExplorer.View.TABLE; + return null; + } + + /** + * Assigns the given resource into the {@link #viewTab} or {@link #tableTab}, depending which one is visible. + * Shall be invoked with a non-null resource only if a data tab is visible because data loading may be costly. * * @param resource the resource to set, or {@code null} if none. - * @param fallback whether to allow the search for a default component to show - * if the given resource is an aggregate. - */ - private void updateDataTab(final Resource resource, boolean fallback) { - Region image = null; - Region table = null; - FeatureSet data = null; - ImageRequest grid = null; + * @return {@code true} if the resource has been recognized. + * + * @see #dataShown + * @see #updateDataTabWithDefault(Resource) + */ + private boolean updateDataTab(final Resource resource) { + Region image = null; + Region table = null; + FeatureSet data = null; + ImageRequest grid = null; + Region controlPanel = null; CoverageExplorer.View type = null; if (resource instanceof GridCoverageResource) { + type = getCoverageView(); // A null value here would be a violation of method contract. if (coverage == null) { - coverage = new CoverageExplorer(); + coverage = new CoverageExplorer(type); + } else { + coverage.setViewType(type); + } + final Region view = coverage.getDataView(type); + switch (type) { + case IMAGE: image = view; break; + case TABLE: table = view; break; } - grid = new ImageRequest((GridCoverageResource) resource, null, null); - image = coverage.getDataView(CoverageExplorer.View.IMAGE); - table = coverage.getDataView(CoverageExplorer.View.TABLE); - type = viewTab.isSelected() ? CoverageExplorer.View.IMAGE : CoverageExplorer.View.TABLE; - fallback = false; + grid = new ImageRequest((GridCoverageResource) resource, null, null); + controlPanel = coverage.getControls(type); } else if (resource instanceof FeatureSet) { data = (FeatureSet) resource; if (features == null) { features = new FeatureTable(); } table = features; - fallback = false; } /* * At least one of `grid` or `data` will be null. Invoking the following @@ -463,44 +472,19 @@ public class ResourceExplorer extends WindowManager { if (features != null) features.setFeatures(data); if (image != null) viewTab .setContent(image); if (table != null) tableTab.setContent(table); - if (isDataTabSet) { - setNewWindowDisabled(image == null && table == null); - updateControls(type); - } - if (fallback) { - defaultIfNotViewable(resource); - } - } - - /** - * Invoked when a data tab become selected or unselected. - * This method sets the current resource in the {@link #viewTab} - * or {@link #tableTab} if it has not been already set. - * - * @param selected whether the tab became the selected one. - * @param visual {@code true} for visual, or {@code false} for tabular data. - */ - private void dataTabShown(final Boolean selected, final boolean visual) { - CoverageExplorer.View type = null; - if (selected) { - if (!isDataTabSet) { - isDataTabSet = true; // Must be set before to invoke `updateDataTab(…)`. - updateDataTab(getSelectedResource(), true); - } - if (coverage != null) { // May still be null if the selected resource is not a coverage. - type = visual ? CoverageExplorer.View.IMAGE : CoverageExplorer.View.TABLE; - } - } - updateControls(type); + final boolean isEmpty = (image == null & table == null); + setNewWindowDisabled(isEmpty); + updateControls(controlPanel); + return !isEmpty | (resource == null); } /** * Adds or removes controls for the given view. + * This method is invoked when the visible tab changed. * - * @param type the view for which to provide controls, or {@code null} if none. + * @param controlPanel the controls for the currently selected tab, or {@code null} if none. */ - private void updateControls(final CoverageExplorer.View type) { - final Region controlPanel = (type != null) ? coverage.getControls(type) : null; + private void updateControls(final Region controlPanel) { final ObservableList<Node> items = controls.getItems(); if (items.size() >= 2) { if (controlPanel != null) { @@ -519,6 +503,7 @@ public class ResourceExplorer extends WindowManager { /** * Returns the set of currently selected data, or {@code null} if none. + * This is invoked when the user selects the "New window" menu item. */ @Override final SelectedData getSelectedData() { @@ -540,9 +525,8 @@ public class ResourceExplorer extends WindowManager { * We do that even if the feature table is not currently visible. This will not cause * useless data loading since they share the same `FeatureLoader`. */ - if (features == null || !isDataTabSet) { - isDataTabSet = true; // Must be set before to invoke `updateDataTab(…)`. - updateDataTab(resource, true); // For forcing creation of FeatureTable. + if (features == null) { + updateDataTab(resource); // For forcing creation of FeatureTable. } table = features; } else { @@ -576,14 +560,19 @@ public class ResourceExplorer extends WindowManager { } /** - * If the given resource is not one of the resource that {@link #updateDataTab(Resource, boolean)} - * can handle, searches in a background thread for a default resource to show. The purpose of this - * method is to make navigation easier by allowing users to click on the root node of a resource, + * If the given resource is not one of the resource that {@link #updateDataTab(Resource)} can handle, + * searches in a background thread for a default resource to show. The purpose of this method is to + * make navigation easier by allowing users to click on the root node of a resource, * without requerying them to expand the tree node before to select a resource. * * @param resource the selected resource. + * + * @see #updateDataTab(Resource) */ - private void defaultIfNotViewable(final Resource resource) { + private void updateDataTabWithDefault(final Resource resource) { + if (updateDataTab(resource)) { + return; + } if (resource instanceof Aggregate && !(resource instanceof DataSet)) { BackgroundThreads.execute(new Task<Resource>() { /** Invoked in background thread for fetching the first resource. */ @@ -604,23 +593,33 @@ public class ResourceExplorer extends WindowManager { /** Invoked in JavaFX thread for showing the resource. */ @Override protected void succeeded() { if (getSelectedResource() == resource) { - updateDataTab(getValue(), false); + updateDataTab(getValue()); } } /** Invoked in JavaFX thread if children can not be loaded. */ @Override protected void failed() { - final ObservableList<LogRecord> records = LogHandler.getRecords(resource); - if (records != null) { - final Throwable e = getException(); - final LogRecord record = new LogRecord(Level.WARNING, e.getLocalizedMessage()); - record.setSourceClassName(ResourceExplorer.class.getName()); - record.setSourceMethodName("defaultIfNotViewable"); - record.setThrown(e); - records.add(record); - } + warning("updateDataTabWithDefault", resource, getException()); } }); } } + + /** + * Adds a warning to the logger associated to the resource. + * + * @param caller the method to declare as the source of the warning. + * @param resource the resource for which an exception occurred. + * @param error the exception to log. + */ + private static void warning(final String caller, final Resource resource, final Throwable error) { + final ObservableList<LogRecord> records = LogHandler.getRecords(resource); + if (records != null) { + final LogRecord record = new LogRecord(Level.WARNING, error.getLocalizedMessage()); + record.setSourceClassName(ResourceExplorer.class.getName()); + record.setSourceMethodName(caller); + record.setThrown(error); + records.add(record); + } + } } diff --git a/application/sis-javafx/src/main/java/org/apache/sis/gui/dataset/SelectedData.java b/application/sis-javafx/src/main/java/org/apache/sis/gui/dataset/SelectedData.java index aca933d..13c9c06 100644 --- a/application/sis-javafx/src/main/java/org/apache/sis/gui/dataset/SelectedData.java +++ b/application/sis-javafx/src/main/java/org/apache/sis/gui/dataset/SelectedData.java @@ -109,13 +109,8 @@ final class SelectedData { } } } - final CoverageExplorer ce = new CoverageExplorer(); + final CoverageExplorer ce = new CoverageExplorer(view); ce.setCoverage(coverage); - /* - * TODO: following line is disabled for now because it causes - * the vertical scroll bar of `GridView` to disappear. - */ -// ce.setViewType(view); return ce.getView(); } } diff --git a/application/sis-javafx/src/main/java/org/apache/sis/gui/dataset/WindowManager.java b/application/sis-javafx/src/main/java/org/apache/sis/gui/dataset/WindowManager.java index 4cf5681..47fc08b 100644 --- a/application/sis-javafx/src/main/java/org/apache/sis/gui/dataset/WindowManager.java +++ b/application/sis-javafx/src/main/java/org/apache/sis/gui/dataset/WindowManager.java @@ -84,7 +84,7 @@ abstract class WindowManager extends Widget { * Creates a new manager of windows. */ WindowManager() { - newWindowMenus = new ArrayList<>(2); + newWindowMenus = new ArrayList<>(3); hasWindowsProperty = new WindowsProperty(); } diff --git a/application/sis-javafx/src/main/java/org/apache/sis/gui/metadata/MetadataSummary.java b/application/sis-javafx/src/main/java/org/apache/sis/gui/metadata/MetadataSummary.java index b17445e..7eb7382 100644 --- a/application/sis-javafx/src/main/java/org/apache/sis/gui/metadata/MetadataSummary.java +++ b/application/sis-javafx/src/main/java/org/apache/sis/gui/metadata/MetadataSummary.java @@ -307,7 +307,6 @@ public class MetadataSummary extends Widget { final Metadata oldValue, final Metadata metadata) { final MetadataSummary s = (MetadataSummary) ((SimpleObjectProperty<?>) property).getBean(); - final Getter getter = s.getter; s.getter = null; // In case this method is invoked before `Getter` completed. s.error = null; if (metadata != oldValue) {