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));
+    }
 }

Reply via email to