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 57a52ec9345f9d334658d8dda052e3249f226407 Author: Martin Desruisseaux <[email protected]> AuthorDate: Sat Jun 11 16:16:56 2022 +0200 Initial version of a `MapCanvas` capable to follow the displacements of another canvas. Synchronizations are activated by checkbox items in the list of windows. --- .../org/apache/sis/gui/dataset/WindowHandler.java | 73 +++++++++++++------ .../java/org/apache/sis/gui/map/MapCanvas.java | 56 ++++++++++++++- .../sis/internal/gui/control/SyncWindowList.java | 81 ++++++++++++++++++---- 3 files changed, 173 insertions(+), 37 deletions(-) diff --git a/application/sis-javafx/src/main/java/org/apache/sis/gui/dataset/WindowHandler.java b/application/sis-javafx/src/main/java/org/apache/sis/gui/dataset/WindowHandler.java index 07289bfe22..68aa91620f 100644 --- a/application/sis-javafx/src/main/java/org/apache/sis/gui/dataset/WindowHandler.java +++ b/application/sis-javafx/src/main/java/org/apache/sis/gui/dataset/WindowHandler.java @@ -17,6 +17,7 @@ package org.apache.sis.gui.dataset; import java.util.Locale; +import java.util.Optional; import java.util.logging.Logger; import javafx.application.Platform; import javafx.stage.Stage; @@ -34,6 +35,7 @@ import org.apache.sis.storage.DataStoreException; import org.apache.sis.storage.event.CloseEvent; import org.apache.sis.storage.event.StoreListener; import org.apache.sis.gui.coverage.CoverageExplorer; +import org.apache.sis.gui.map.MapCanvas; import org.apache.sis.internal.gui.BackgroundThreads; import org.apache.sis.internal.gui.GUIUtilities; import org.apache.sis.internal.gui.PrivateAccess; @@ -162,11 +164,28 @@ public abstract class WindowHandler { * This is used for identifying which handlers to remove when a resource is closed. * This method is not yet public because a future version may need to return a full * map context instead of a single resource. + * + * @return the resource shown in the window. + * + * @see CoverageExplorer#getResource() + * @see FeatureTable#getFeatures() */ abstract Resource getResource(); /** - * Returns the JavaFX region where the resource is shown. This value shall be stable. + * Returns the canvas (if any) where the resource is shown. + * Canvas exists for some kinds of view such as {@link CoverageExplorer}, but not for every kinds. + * For example tabular data such as {@link FeatureTable} have no canvas. + * + * @return the canvas where the resource is shown. + * + * @see CoverageExplorer#getCanvas() + */ + public abstract Optional<MapCanvas> getCanvas(); + + /** + * Returns the JavaFX region where the resource is shown. + * This value shall be stable. */ abstract Region getView(); @@ -245,15 +264,13 @@ public abstract class WindowHandler { */ @Override public void eventOccured(final CloseEvent event) { final Resource resource = event.getSource(); - if (resource != null) { - resource.removeListener(CloseEvent.class, this); - } if (Platform.isFxApplicationThread()) { close(resource, WindowHandler.this); } else BackgroundThreads.runAndWaitDialog(() -> { close(resource, WindowHandler.this); return null; }); + // No need to invoke `resource.removeListener(…)`, that work is done by `StoreListeners.close()`. } } @@ -319,6 +336,22 @@ public abstract class WindowHandler { this.widget = widget; } + /** + * The resource shown in the {@linkplain #window window}, or {@code null} if unspecified. + */ + @Override + Resource getResource() { + return widget.getResource(); + } + + /** + * Returns the canvas where the map is shown. + */ + @Override + public Optional<MapCanvas> getCanvas() { + return Optional.of(widget.getCanvas()); + } + /** * Returns the JavaFX region where the resource is shown. */ @@ -349,14 +382,6 @@ public abstract class WindowHandler { return handler.finish(); } - /** - * The resource shown in the {@linkplain #window window}, or {@code null} if unspecified. - */ - @Override - Resource getResource() { - return widget.getResource(); - } - /** * Makes a "best effort" for helping the garbage-collector to release resources. */ @@ -393,6 +418,22 @@ public abstract class WindowHandler { this.widget = widget; } + /** + * The resource shown in the {@linkplain #window window}, or {@code null} if unspecified. + */ + @Override + Resource getResource() { + return widget.getFeatures(); + } + + /** + * Returns an empty value since this window does not show map. + */ + @Override + public Optional<MapCanvas> getCanvas() { + return Optional.empty(); + } + /** * Returns the JavaFX region where the resource is shown. */ @@ -409,14 +450,6 @@ public abstract class WindowHandler { return new ForFeatures(this, null, new FeatureTable(widget)).finish(); } - /** - * The resource shown in the {@linkplain #window window}, or {@code null} if unspecified. - */ - @Override - Resource getResource() { - return widget.getFeatures(); - } - /** * Makes a "best effort" for helping the garbage-collector to release resources. */ diff --git a/application/sis-javafx/src/main/java/org/apache/sis/gui/map/MapCanvas.java b/application/sis-javafx/src/main/java/org/apache/sis/gui/map/MapCanvas.java index a4740cec9d..6aee443bbb 100644 --- a/application/sis-javafx/src/main/java/org/apache/sis/gui/map/MapCanvas.java +++ b/application/sis-javafx/src/main/java/org/apache/sis/gui/map/MapCanvas.java @@ -20,6 +20,7 @@ import java.util.Locale; import java.util.Arrays; import java.util.Objects; import java.awt.geom.AffineTransform; +import java.awt.geom.NoninvertibleTransformException; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import javafx.application.Platform; @@ -124,7 +125,7 @@ import static org.apache.sis.internal.util.StandardDateFormat.NANOS_PER_MILLISEC * </ol> * * @author Martin Desruisseaux (Geomatys) - * @version 1.2 + * @version 1.3 * @since 1.1 * @module */ @@ -812,6 +813,57 @@ public abstract class MapCanvas extends PlanarCanvas { return dstAxes; } + /** + * Updates the <cite>objective to display</cite> transform with the given transform in objective coordinates. + * This method must be invoked in the JavaFX thread. The visual is updated immediately by transforming + * the current image, then a more accurate image is prepared in a background thread. + * + * <p>Contrarily to the method defined in the {@link PlanarCanvas} parent class, + * this method does not guarantee that an {@value #OBJECTIVE_TO_DISPLAY_PROPERTY} event is fired immediately. + * The event may be fired at an undetermined amount of time after this method call. + * However the event will always be fired in the JavaFX thread.</p> + * + * @param before coordinate conversion to apply before the current <cite>objective to display</cite> transform. + * + * @since 1.3 + */ + @Override + public void transformObjectiveCoordinates(final AffineTransform before) { + if (!before.isIdentity()) try { + AffineTransform t = objectiveToDisplay.createInverse(); + t.preConcatenate(before); + t.preConcatenate(objectiveToDisplay); + transform.prepend(t.getScaleX(), t.getShearX(), t.getTranslateX(), + t.getShearY(), t.getScaleY(), t.getTranslateY()); + requestRepaint(); + } catch (NoninvertibleTransformException e) { + errorOccurred(e); + } + } + + /** + * Updates the <cite>objective to display</cite> transform with the given transform in pixel coordinates. + * This method must be invoked in the JavaFX thread. The visual is updated immediately by transforming + * the current image, then a more accurate image is prepared in a background thread. + * + * <p>Contrarily to the method defined in the {@link PlanarCanvas} parent class, + * this method does not guarantee that an {@value #OBJECTIVE_TO_DISPLAY_PROPERTY} event is fired immediately. + * The event may be fired at an undetermined amount of time after this method call. + * However the event will always be fired in the JavaFX thread.</p> + * + * @param after coordinate conversion to apply after the current <cite>objective to display</cite> transform. + * + * @since 1.3 + */ + @Override + public void transformDisplayCoordinates(final AffineTransform after) { + if (!after.isIdentity()) { + transform.append(after.getScaleX(), after.getShearX(), after.getTranslateX(), + after.getShearY(), after.getScaleY(), after.getTranslateY()); + requestRepaint(); + } + } + /** * Invoked in JavaFX thread for creating a renderer to be executed in a background thread. * Subclasses shall copy in this method all {@code MapCanvas} properties that the background thread @@ -1032,7 +1084,7 @@ public abstract class MapCanvas extends PlanarCanvas { */ changeInProgress.setToTransform(transform); if (!transform.isIdentity()) { - transformDisplayCoordinates(new AffineTransform( + super.transformDisplayCoordinates(new AffineTransform( transform.getMxx(), transform.getMyx(), transform.getMxy(), transform.getMyy(), transform.getTx(), transform.getTy())); diff --git a/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/control/SyncWindowList.java b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/control/SyncWindowList.java index f909394c9f..a544a6d542 100644 --- a/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/control/SyncWindowList.java +++ b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/control/SyncWindowList.java @@ -19,6 +19,8 @@ package org.apache.sis.internal.gui.control; import java.util.List; import javafx.beans.property.BooleanProperty; import javafx.beans.property.SimpleBooleanProperty; +import javafx.beans.value.ChangeListener; +import javafx.beans.value.ObservableValue; import javafx.collections.ListChangeListener; import javafx.collections.ObservableList; import javafx.scene.control.Button; @@ -28,9 +30,11 @@ import javafx.scene.input.MouseEvent; import javafx.scene.layout.Priority; import javafx.scene.layout.Region; import javafx.scene.layout.VBox; +import javafx.application.Platform; import org.apache.sis.util.resources.Vocabulary; import org.apache.sis.gui.dataset.WindowHandler; import org.apache.sis.gui.map.MapCanvas; +import org.apache.sis.portrayal.CanvasFollower; import org.apache.sis.internal.gui.Resources; import org.apache.sis.internal.util.UnmodifiableArrayList; @@ -49,7 +53,7 @@ public final class SyncWindowList extends TabularWidget implements ListChangeLis * Window containing a {@link MapCanvas} to follow on gesture events. * Gestures are followed only if {@link #linked} is {@code true}. */ - private static final class Link { + private static final class Link extends CanvasFollower implements ChangeListener<Boolean> { /** * Whether the "foreigner" {@linkplain #view} should be followed. */ @@ -63,29 +67,56 @@ public final class SyncWindowList extends TabularWidget implements ListChangeLis /** * Creates a new row for a window to follow. * - * @param view the "foreigner" view for which to follow the gesture. + * @param view the "foreigner" view for which to follow the gesture. + * @param source the canvas which is the source of zoom, pan or rotation events. + * @param target the canvas on which to apply the changes of zoom, pan or rotation. */ - private Link(final WindowHandler view) { + @SuppressWarnings("ThisEscapedInObjectConstruction") + private Link(final WindowHandler view, final MapCanvas source, final MapCanvas target) { + super(source, target); this.view = view; linked = new SimpleBooleanProperty(this, "linked"); + linked.addListener(this); + source.addPropertyChangeListener(MapCanvas.OBJECTIVE_CRS_PROPERTY, this); + target.addPropertyChangeListener(MapCanvas.OBJECTIVE_CRS_PROPERTY, this); } /** * Converts the given list of handled to a list of table rows. * - * @param added list of new items to put in the table. - * @param exclude item to exclude (because the referenced window is itself). + * @param added list of new items to put in the table. + * @param owner item to exclude (because the referenced window is itself). + * @param target the canvas on which to apply the changes of zoom, pan or rotation. */ - static List<Link> wrap(final List<? extends WindowHandler> added, final WindowHandler exclude) { + static List<Link> wrap(final List<? extends WindowHandler> added, final WindowHandler owner, final MapCanvas target) { final Link[] items = new Link[added.size()]; int count = 0; for (final WindowHandler view : added) { - if (view != exclude) { - items[count++] = new Link(view); + if (view != owner) { + final MapCanvas source = view.getCanvas().orElse(null); + if (source != null) { + items[count++] = new Link(view, source, target); + } } } return UnmodifiableArrayList.wrap(items, 0, count); } + + /** + * Invoked when the {@link #linked} property value changed. + * + * @param property equivalent to {@link #link}. + * @param oldValue equivalent to {@code !newValue}. + * @param newValue the new checkbox value. + */ + @Override + public void changed(ObservableValue<? extends Boolean> property, Boolean oldValue, Boolean newValue) { + if (newValue) { + source.addPropertyChangeListener(MapCanvas.OBJECTIVE_TO_DISPLAY_PROPERTY, this); + } else { + source.removePropertyChangeListener(MapCanvas.OBJECTIVE_TO_DISPLAY_PROPERTY, this); + } + } } /** @@ -103,6 +134,13 @@ public final class SyncWindowList extends TabularWidget implements ListChangeLis */ private final WindowHandler owner; + /** + * The canvas on which to apply the change of zoom, pan or rotation. + * Needs to be fetched only when first needed (not at construction time) + * for avoiding a stack overflow. + */ + private MapCanvas target; + /** * The component to be returned by {@link #getView()}. */ @@ -128,7 +166,6 @@ public final class SyncWindowList extends TabularWidget implements ListChangeLis table.getColumns().setAll( newBooleanColumn(/* 🔗 (link symbol) */ "\uD83D\uDD17", (cell) -> cell.getValue().linked), newStringColumn (vocabulary.getString(Vocabulary.Keys.Title), (cell) -> cell.getValue().view.title)); - table.getItems().setAll(Link.wrap(owner.manager.windows, owner)); table.setRowFactory(SyncWindowList::newRow); /* * Build all other widget controls. @@ -138,9 +175,11 @@ public final class SyncWindowList extends TabularWidget implements ListChangeLis VBox.setVgrow(newWindow, Priority.NEVER); content = new VBox(9, table, newWindow); /* - * Add listener last when the everything else is successful - * (because the `this` reference escapes). + * Add listener last when the everything else is successful (because the `this` reference escapes). + * The creation of table item list needs to be done after the caller finished its initialization, + * not now, because invoking`owner.getCanvas()` here create an infinite loop. */ + Platform.runLater(() -> addAll(owner.manager.windows)); owner.manager.windows.addListener(this); } @@ -196,23 +235,35 @@ public final class SyncWindowList extends TabularWidget implements ListChangeLis */ @Override public void onChanged(final Change<? extends WindowHandler> change) { - final ObservableList<Link> items = table.getItems(); while (change.next()) { // Ignore permutations; each table can have its own order. if (change.wasRemoved()) { // List of removed items usually has a single element. - for (final WindowHandler item : change.getRemoved()) { + final ObservableList<Link> items = table.getItems(); + for (final WindowHandler view : change.getRemoved()) { for (int i = items.size(); --i >= 0;) { - if (items.get(i).view == item) { + final Link item = items.get(i); + if (item.view == view) { items.remove(i); + item.dispose(); break; } } } } if (change.wasAdded()) { - items.addAll(Link.wrap(change.getAddedSubList(), owner)); + addAll(change.getAddedSubList()); } } } + + /** + * Adds the given window handlers and items in {@link #table}. + */ + private void addAll(final List<? extends WindowHandler> windows) { + if (target == null) { + target = owner.getCanvas().get(); + } + table.getItems().addAll(Link.wrap(windows, owner, target)); + } }
