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
The following commit(s) were added to refs/heads/geoapi-4.0 by this push:
new 6dc984a9b7 Allow to move simultaneously in all canvases shown in the
same window. Contains a bug fix for handling retroaction (moving A moves B
which moves A).
6dc984a9b7 is described below
commit 6dc984a9b79a267e41669c9ac5089a57c379944b
Author: Martin Desruisseaux <[email protected]>
AuthorDate: Fri Apr 10 13:00:53 2026 +0200
Allow to move simultaneously in all canvases shown in the same window.
Contains a bug fix for handling retroaction (moving A moves B which moves
A).
---
.../main/org/apache/sis/portrayal/Canvas.java | 10 +-
.../org/apache/sis/portrayal/CanvasFollower.java | 96 +++++++++------
.../org/apache/sis/portrayal/FollowContext.java | 135 +++++++++++++++++++++
.../main/org/apache/sis/portrayal/Observable.java | 19 ++-
.../org/apache/sis/portrayal/PlanarCanvas.java | 10 +-
.../apache/sis/portrayal/TransformChangeEvent.java | 21 +++-
.../org/apache/sis/portrayal/package-info.java | 8 +-
.../main/org/apache/sis/gui/map/MultiCanvas.java | 36 +++++-
8 files changed, 277 insertions(+), 58 deletions(-)
diff --git
a/endorsed/src/org.apache.sis.portrayal/main/org/apache/sis/portrayal/Canvas.java
b/endorsed/src/org.apache.sis.portrayal/main/org/apache/sis/portrayal/Canvas.java
index f81facfc8e..ea54822a9c 100644
---
a/endorsed/src/org.apache.sis.portrayal/main/org/apache/sis/portrayal/Canvas.java
+++
b/endorsed/src/org.apache.sis.portrayal/main/org/apache/sis/portrayal/Canvas.java
@@ -139,10 +139,12 @@ import
org.opengis.coordinate.MismatchedCoordinateMetadataException;
* The zoom level is given indirectly by the {@link #getObjectiveToDisplay()}
transform.
* The display device may have a wraparound axis, for example in the spherical
coordinate system of a planetarium.
*
- * <h2>Multi-threading</h2>
- * {@code Canvas} is not thread-safe. Synchronization, if desired, must be
done by the caller.
- * Another common strategy is to interact with {@code Canvas} from a single
thread,
- * for example the Swing or JavaFX event queue.
+ * <h2>Thread safety</h2>
+ * {@code Canvas} is not thread-safe.
+ * A single thread should be used for interactions with all instances of
{@code Canvas}
+ * that may be referencing each other through {@linkplain
#addPropertyChangeListener listeners}.
+ * External synchronization is generally not sufficient because listeners may
create a graph of canvases,
+ * and it is difficult to ensure that a lock is kept during all the graph
traversal.
*
* @author Johann Sorel (Geomatys)
* @author Martin Desruisseaux (Geomatys)
diff --git
a/endorsed/src/org.apache.sis.portrayal/main/org/apache/sis/portrayal/CanvasFollower.java
b/endorsed/src/org.apache.sis.portrayal/main/org/apache/sis/portrayal/CanvasFollower.java
index 0c06d54a41..443acaba45 100644
---
a/endorsed/src/org.apache.sis.portrayal/main/org/apache/sis/portrayal/CanvasFollower.java
+++
b/endorsed/src/org.apache.sis.portrayal/main/org/apache/sis/portrayal/CanvasFollower.java
@@ -53,8 +53,9 @@ import
org.apache.sis.referencing.operation.transform.MathTransforms;
* changes in {@linkplain #source} are applied on {@linkplain #target}, but
not the converse.
*
* <h2>Multi-threading</h2>
- * This class is <strong>not</strong> thread-safe.
- * All events should be processed in the same thread.
+ * {@code CanvasFollower} is not thread-safe. The {@linkplain #source} and
{@linkplain #target} canvases,
+ * together with all other canvases that are connected in the same graph of
{@link PlanarCanvas} objects
+ * through any other {@code CanvasFollower} instances, shall be accessed in
the same thread.
*
* @author Martin Desruisseaux (Geomatys)
* @version 1.7
@@ -126,10 +127,18 @@ public class CanvasFollower implements
PropertyChangeListener, Disposable {
private enum Status {VALID, OUTDATED, UNKNOWN, ERROR}
/**
- * Whether a change is in progress. This is for avoiding never-ending loop
- * if a bidirectional mapping or a cycle exists (A → B → C → A).
+ * Contextual information about an "objective to display" transform change
which is progress.
+ * The contextual information is necessary in particular for avoiding
never-ending recursive
+ * loops if a chain of {@link CanvasFollower} result in a cyclic graph of
{@code PlanarCanvas}.
+ *
+ * <h4>Design note</h4>
+ * We cannot store this information as a field in {@link
TransformChangeEvent} because in a chain
+ * such as <var>A</var> → <var>B</var> → <var>C</var>, the {@code
TransformChangeEvent} instance
+ * is not the same between <var>A</var> and <var>B</var> than between
<var>B</var> and <var>C</var>.
+ * The use of thread local requires that all events are managed from a
unique thread.
+ * This advice is documented in the package and {@link Canvas} Javadoc.
*/
- private boolean changing;
+ private static final ThreadLocal<FollowContext> CONTEXT =
ThreadLocal.withInitial(FollowContext::new);
/**
* Creates a new listener for synchronizing "objective to display"
transform changes
@@ -349,42 +358,26 @@ public class CanvasFollower implements
PropertyChangeListener, Disposable {
*/
@Override
public void propertyChange(final PropertyChangeEvent event) {
- if (!changing && event instanceof TransformChangeEvent) try {
+ if (event instanceof TransformChangeEvent) {
final var te = (TransformChangeEvent) event;
displayTransformStatus = Status.OUTDATED;
- changing = true;
- if (te.isSameSource(source)) {
+ if (te.isSameSource(target)) {
+ transformedTarget(te);
+ } else if (te.isSameSource(source)) {
transformedSource(te);
if (!disabled && filter(te)) {
- if (followRealWorld &&
findObjectiveTransform("propertyChange")) {
- AffineTransform before =
te.getObjectiveChange2D().orElse(null);
- if (before != null) try {
- /*
- * Converts a change from units of the source CRS
to units of the target CRS.
- * If that change cannot be computed, fallback on
a change in display units.
- * The POI may be null, but this is okay if the
transform is linear.
- */
- if (objectiveTransform != null) {
- DirectPosition poi = getSourceObjectivePOI();
- AffineTransform t =
AffineTransforms2D.castOrCopy(MathTransforms.tangent(objectiveTransform, poi));
- AffineTransform c = t.createInverse();
- c.preConcatenate(before);
- c.preConcatenate(t);
- before = c;
- }
- transformObjectiveCoordinates(te, before);
- return;
- } catch (NullPointerException | TransformException |
NoninvertibleTransformException e) {
- canNotCompute("propertyChange", e);
- }
+ final FollowContext context = CONTEXT.get();
+ if (context.isPropagating(this)) {
+ context.propagateOrDefer(this, te);
+ } else try {
+ te.deferredListeners = context;
+ context.propagateOrDefer(this, te);
+ } finally {
+ te.deferredListeners = null;
+ context.clear();
}
- te.getDisplayChange2D().ifPresent((after) ->
transformDisplayCoordinates(te, after));
}
- } else if (te.isSameSource(target)) {
- transformedTarget(te);
}
- } finally {
- changing = false;
} else if
(PlanarCanvas.OBJECTIVE_CRS_PROPERTY.equals(event.getPropertyName())) {
displayTransform = null;
objectiveTransform = null;
@@ -393,6 +386,39 @@ public class CanvasFollower implements
PropertyChangeListener, Disposable {
}
}
+ /**
+ * Applies to the {@linkplain #target} canvas the change described by the
given event.
+ * This method shall be invoked only if the change has not already been
applied on the
+ * target canvas.
+ *
+ * @param event the change to apply on the {@linkplain #target} canvas.
+ */
+ final void propagate(final TransformChangeEvent event) {
+ if (followRealWorld && findObjectiveTransform("propertyChange")) {
+ AffineTransform before = event.getObjectiveChange2D().orElse(null);
+ if (before != null) try {
+ /*
+ * Converts a change from units of the source CRS to units of
the target CRS.
+ * If that change cannot be computed, fallback on a change in
display units.
+ * The POI may be null, but this is okay if the transform is
linear.
+ */
+ if (objectiveTransform != null) {
+ DirectPosition poi = getSourceObjectivePOI();
+ AffineTransform t =
AffineTransforms2D.castOrCopy(MathTransforms.tangent(objectiveTransform, poi));
+ AffineTransform c = t.createInverse();
+ c.preConcatenate(before);
+ c.preConcatenate(t);
+ before = c;
+ }
+ transformObjectiveCoordinates(event, before);
+ return;
+ } catch (NullPointerException | TransformException |
NoninvertibleTransformException e) {
+ canNotCompute("propertyChange", e);
+ }
+ }
+ event.getDisplayChange2D().ifPresent((after) ->
transformDisplayCoordinates(event, after));
+ }
+
/**
* Returns {@code true} if this listener should replicate the following
changes on the target canvas.
* The default implementation returns {@code true} if the transform reason
is
@@ -400,7 +426,7 @@ public class CanvasFollower implements
PropertyChangeListener, Disposable {
* {@link TransformChangeEvent.Reason#DISPLAY_NAVIGATION}.
*
* @param event a transform change event that occurred on the
{@linkplain #source} canvas.
- * @return whether to replicate that change on the {@linkplain #target}
canvas.
+ * @return whether to replicate that change on the {@linkplain #target}
canvas.
*/
protected boolean filter(final TransformChangeEvent event) {
return event.getReason().isNavigation();
diff --git
a/endorsed/src/org.apache.sis.portrayal/main/org/apache/sis/portrayal/FollowContext.java
b/endorsed/src/org.apache.sis.portrayal/main/org/apache/sis/portrayal/FollowContext.java
new file mode 100644
index 0000000000..971380dcff
--- /dev/null
+++
b/endorsed/src/org.apache.sis.portrayal/main/org/apache/sis/portrayal/FollowContext.java
@@ -0,0 +1,135 @@
+/*
+ * 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.portrayal;
+
+import java.util.Map;
+import java.util.Queue;
+import java.util.ArrayDeque;
+import java.util.IdentityHashMap;
+
+
+/**
+ * Contextual information about an "objective to display" transform change
which is in progress.
+ * We use one instance per thread on the assumption that events are processed
in the same thread,
+ * at least between {@link PlanarCanvas} instances that are connected in the
same graph.
+ * This condition is documented in the {@link CanvasFollower} class Javadoc.
+ *
+ * @author Martin Desruisseaux (Geomatys)
+ *
+ * @see CanvasFollower#CONTEXT
+ */
+final class FollowContext {
+ /**
+ * Whether to add {@link CanvasFollower} instances to the {@link
#deferred} queue
+ * instead of executing their {@code propagate(…)} method.
+ *
+ * @see CanvasFollower#propagate(TransformChangeEvent)
+ */
+ private boolean propagateLater;
+
+ /**
+ * Listeners for which the call to {@code propagate(…)} has been deferred
to a later time.
+ * This is needed when we have two or more {@link CanvasFollower}
instances registered on
+ * the same source canvas but for different target canvases.
+ *
+ * <h4>Use case</h4>
+ * Consider a case where a change in canvas <var>A</var> is propagated to
canvas <var>B</var>
+ * which in turn propagates its own change to canvas <var>C</var>. If
canvas <var>A</var> has
+ * another {@code CanvasFollower} propagating the same change directly to
<var>C</var>, it is
+ * better to give precedence to the latter because it is more direct (it
avoids to propagate
+ * a transformation of another transformation). But because of the order
in which a tree of
+ * listeners is executed, we need a mechanism if which the execution of a
branch is deferred.
+ *
+ * @see CanvasFollower#propagate(TransformChangeEvent)
+ */
+ private final Queue<CanvasFollower> deferred;
+
+ /**
+ * Canvases in which a change of "objective to display" transform has
already been propagated.
+ * This is used for avoiding never-ending loops if two or more instances
of {@link CanvasFollower}
+ * result in a cyclic graph of {@code PlanarCanvas}.
+ */
+ private final Map<PlanarCanvas, Boolean> propagated;
+
+ /**
+ * Creates an empty context.
+ */
+ FollowContext() {
+ deferred = new ArrayDeque<>(4); // There is usually not many
instances.
+ propagated = new IdentityHashMap<>(4);
+ }
+
+ /**
+ * Resets this {@code FollowContext} to the same state as after
construction.
+ */
+ final void clear() {
+ propagateLater = false;
+ deferred.clear();
+ propagated.clear();
+ }
+
+ /**
+ * Returns whether a {@code TransformChangeEvent} is already in process of
being propagated.
+ * A return value of {@code true} means that the caller is handling events
that were fired as
+ * a consequence of the original event, or are listeners notified after
the first listener.
+ */
+ final boolean isPropagating(final CanvasFollower follower) {
+ if (propagated.isEmpty()) {
+ propagated.put(follower.source, Boolean.TRUE);
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Executes {@code follower.propagate(…)} immediately or adds it to a
queue of methods to be invoked later.
+ * The purpose is to execute {@code propagate(…)} in a different order
than the usual tree traversal order.
+ * We want all siblings to be executed before to traverse the children.
+ *
+ * <p>This method does nothing if the target canvas has already been
notified.</p>
+ *
+ * @param follower the follower for which to execute or defer the call
to {@code propagate(…)}.
+ * @param event the event to propagate if it needs to be done
immediately.
+ */
+ final void propagateOrDefer(final CanvasFollower follower, final
TransformChangeEvent event) {
+ if (propagated.put(follower.target, Boolean.FALSE) == null) {
+ if (propagateLater) {
+ deferred.add(follower);
+ } else {
+ propagateLater = true;
+ follower.propagate(event);
+ propagateLater = false;
+ }
+ }
+ }
+
+ /**
+ * Executes all {@code follower.propagate(…)} calls that were deferred.
+ * This method should be invoked after all siblings have been processed.
+ * Target canvases that have already been processed are ignored.
+ *
+ * @param event the event to propagate.
+ */
+ final void executeDeferred(final TransformChangeEvent event) {
+ CanvasFollower follower;
+ while ((follower = deferred.poll()) != null) {
+ if (propagated.put(follower.target, Boolean.FALSE) == null) {
+ follower.propagate(event);
+ }
+ }
+ }
+}
diff --git
a/endorsed/src/org.apache.sis.portrayal/main/org/apache/sis/portrayal/Observable.java
b/endorsed/src/org.apache.sis.portrayal/main/org/apache/sis/portrayal/Observable.java
index 8554517c21..5cec043ef2 100644
---
a/endorsed/src/org.apache.sis.portrayal/main/org/apache/sis/portrayal/Observable.java
+++
b/endorsed/src/org.apache.sis.portrayal/main/org/apache/sis/portrayal/Observable.java
@@ -30,7 +30,7 @@ import org.apache.sis.util.ArraysExt;
* Parent class of all objects for which it is possible to register listeners.
*
* @author Martin Desruisseaux (Geomatys)
- * @version 1.5
+ * @version 1.7
* @since 1.5
*/
public abstract class Observable {
@@ -45,7 +45,7 @@ public abstract class Observable {
* @see #addPropertyChangeListener(String, PropertyChangeListener)
* @see #removePropertyChangeListener(String, PropertyChangeListener)
*/
- private Map<String,PropertyChangeListener[]> listeners;
+ private Map<String, PropertyChangeListener[]> listeners;
/**
* Creates a new instance.
@@ -88,7 +88,7 @@ public abstract class Observable {
ArgumentChecks.ensureNonNull("listener", listener);
if (listeners != null) {
listeners.computeIfPresent(propertyName, (key, oldList) -> {
- for (int i=oldList.length; --i >= 0;) {
+ for (int i = oldList.length; --i >= 0;) {
if (oldList[i] == listener) {
if (oldList.length != 1) {
return ArraysExt.remove(oldList, i, 1);
@@ -129,7 +129,7 @@ public abstract class Observable {
if (listeners != null) {
final PropertyChangeListener[] list = listeners.get(propertyName);
if (list != null) {
- final PropertyChangeEvent event = new
PropertyChangeEvent(this, propertyName, oldValue, newValue);
+ final var event = new PropertyChangeEvent(this, propertyName,
oldValue, newValue);
for (final PropertyChangeListener listener : list) {
listener.propertyChange(event);
}
@@ -154,6 +154,17 @@ public abstract class Observable {
for (final PropertyChangeListener listener : list) {
listener.propertyChange(event);
}
+ /*
+ * For separation of concerns, following should be managed in
`CanvasFollower`.
+ * But it is difficult to ensure there that it is executed
after all listeners.
+ */
+ if (event instanceof TransformChangeEvent) {
+ final var te = (TransformChangeEvent) event;
+ final FollowContext deferredListeners =
te.deferredListeners;
+ if (deferredListeners != null) {
+ deferredListeners.executeDeferred(te);
+ }
+ }
}
}
}
diff --git
a/endorsed/src/org.apache.sis.portrayal/main/org/apache/sis/portrayal/PlanarCanvas.java
b/endorsed/src/org.apache.sis.portrayal/main/org/apache/sis/portrayal/PlanarCanvas.java
index ef7ea9a38b..6c1a4e4744 100644
---
a/endorsed/src/org.apache.sis.portrayal/main/org/apache/sis/portrayal/PlanarCanvas.java
+++
b/endorsed/src/org.apache.sis.portrayal/main/org/apache/sis/portrayal/PlanarCanvas.java
@@ -34,10 +34,12 @@ import
org.apache.sis.referencing.internal.shared.AffineTransform2D;
* A canvas for two-dimensional display device using a Cartesian coordinate
system.
* Data are reduced to a two-dimensional slice before to be displayed.
*
- * <h2>Multi-threading</h2>
- * {@code PlanarCanvas} is not thread-safe. Synchronization, if desired, must
be done by the caller.
- * Another common strategy is to interact with {@code PlanarCanvas} from a
single thread,
- * for example the Swing or JavaFX event queue.
+ * <h2>Thread safety</h2>
+ * {@code PlanarCanvas} is not thread-safe.
+ * A single thread should be used for interactions with all instances of
{@code PlanarCanvas} that are
+ * linked together by {@link CanvasFollower} or other {@linkplain
#addPropertyChangeListener listeners}.
+ * External synchronization is generally not sufficient because listeners may
create a graph of canvases,
+ * and it is difficult to ensure that a lock is kept during all the graph
traversal.
*
* @author Johann Sorel (Geomatys)
* @author Martin Desruisseaux (Geomatys)
diff --git
a/endorsed/src/org.apache.sis.portrayal/main/org/apache/sis/portrayal/TransformChangeEvent.java
b/endorsed/src/org.apache.sis.portrayal/main/org/apache/sis/portrayal/TransformChangeEvent.java
index 444da8c9bc..891a31077d 100644
---
a/endorsed/src/org.apache.sis.portrayal/main/org/apache/sis/portrayal/TransformChangeEvent.java
+++
b/endorsed/src/org.apache.sis.portrayal/main/org/apache/sis/portrayal/TransformChangeEvent.java
@@ -37,12 +37,15 @@ import org.apache.sis.util.logging.Logging;
* are instances of this class.
* This specialization provides methods for computing the difference between
the old and new state.
*
+ * <p>Instances of {@code TransformChangeEvent} should be short-lived. They
exist the time needed
+ * for processing an event, but should not be retained for a long time and
should not be reused.</p>
+ *
* <h2>Multi-threading</h2>
- * This class is <strong>not</strong> thread-safe.
- * All listeners should process this event in the same thread.
+ * {@code TransformChangeEvent} is not thread-safe. All listeners shall
process this event in the
+ * thread that {@linkplain Observable#firePropertyChange(PropertyChangeEvent)
fired} this event.
*
* @author Martin Desruisseaux (Geomatys)
- * @version 1.3
+ * @version 1.7
*
* @see Canvas#OBJECTIVE_TO_DISPLAY_PROPERTY
*
@@ -144,6 +147,18 @@ public class TransformChangeEvent extends
PropertyChangeEvent {
*/
private transient Exception error;
+ /**
+ * Listeners that have not been fully notified of this event.
+ * Part of the execution of these listeners were deferred.
+ * This is for internal usage by {@link CanvasFollower}.
+ *
+ * <h4>Design note</h4>
+ * For separation of concerns, it would be better to manage deferred
propagation in {@link CanvasFollower}.
+ * But we want to apply deferred changes after {@link
Observable#firePropertyChange(PropertyChangeEvent)}
+ * finished to loop over all listeners, while a {@link CanvasFollower}
instance is only one of those listeners.
+ */
+ transient FollowContext deferredListeners;
+
/**
* Creates a new event for a change of the "objective to display" property.
* The old and new transforms should not be null, except on initialization
or for lazy computation:
diff --git
a/endorsed/src/org.apache.sis.portrayal/main/org/apache/sis/portrayal/package-info.java
b/endorsed/src/org.apache.sis.portrayal/main/org/apache/sis/portrayal/package-info.java
index 9a6c67d79e..8722a340ce 100644
---
a/endorsed/src/org.apache.sis.portrayal/main/org/apache/sis/portrayal/package-info.java
+++
b/endorsed/src/org.apache.sis.portrayal/main/org/apache/sis/portrayal/package-info.java
@@ -20,9 +20,13 @@
* Symbology and map representations, together with a rendering engine for
display.
* This package is currently in early draft stage.
*
- * <h2>Synchronization</h2>
+ * <h2>Thread safety</h2>
* Unless otherwise specified, classes in this package are not thread-safe.
- * Synchronization, if desired, must be done by the caller.
+ * A single thread should be used for interactions with all instances of
+ * {@link org.apache.sis.portrayal.Canvas} that are linked together by
+ * {@link org.apache.sis.portrayal.CanvasFollower} or other listeners.
+ * External synchronization is generally not sufficient because listeners may
create a graph of canvases,
+ * and it is difficult to ensure that a lock is kept during all the graph
traversal.
*
* @author Johann Sorel (Geomatys)
* @version 1.7
diff --git
a/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/map/MultiCanvas.java
b/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/map/MultiCanvas.java
index 924c229236..dcaf62a4bd 100644
---
a/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/map/MultiCanvas.java
+++
b/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/map/MultiCanvas.java
@@ -27,11 +27,14 @@ import java.util.NoSuchElementException;
import java.util.Objects;
import java.util.Set;
import javafx.application.Platform;
-import javafx.beans.InvalidationListener;
import javafx.beans.Observable;
+import javafx.beans.InvalidationListener;
+import javafx.beans.value.ChangeListener;
+import javafx.beans.value.ObservableValue;
import javafx.scene.Node;
import javafx.scene.control.Label;
import javafx.scene.control.Button;
+import javafx.scene.control.ToggleButton;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.Region;
@@ -49,6 +52,7 @@ import org.apache.sis.gui.Widget;
import org.apache.sis.gui.coverage.CoverageCanvas;
import org.apache.sis.gui.internal.BackgroundThreads;
import org.apache.sis.gui.internal.DataStoreOpener;
+import org.apache.sis.gui.internal.FontGIS;
import static org.apache.sis.gui.internal.LogHandler.LOGGER;
import org.apache.sis.gui.referencing.RecentReferenceSystems;
import org.apache.sis.io.TableAppender;
@@ -119,7 +123,7 @@ final class MultiCanvas extends Widget implements
Observable {
/**
* Controls associated to each map canvas.
*/
- private static final class Controls {
+ private static final class Controls implements ChangeListener<Boolean> {
/**
* The title of the associated map canvas. This label is not
necessarily shown.
* If the enclosing {@link MultiCanvas} contains only one {@link
MapCanvas},
@@ -138,6 +142,8 @@ final class MultiCanvas extends Widget implements
Observable {
/**
* Listeners of mouse displacements and navigation actions such as
zooms and pans.
+ * The {@link GestureFollower#source} canvas of all elements shall be
the {@code canvas} argument
+ * given to the constructor.
*/
final List<GestureFollower> followers;
@@ -199,6 +205,18 @@ final class MultiCanvas extends Widget implements
Observable {
followers.forEach(GestureFollower::dispose);
followers.clear();
}
+
+ /**
+ * Invoked when the user pressed the button for enabling or disabling
the propagation of
+ * navigation events from the canvas associated to this {@code
Controls} to other canvases.
+ */
+ @Override
+ public void changed(ObservableValue<? extends Boolean> property,
Boolean oldValue, Boolean newValue) {
+ final boolean enabled = newValue; // Unboxing.
+ for (final GestureFollower follower : followers) {
+ follower.transformEnabled.set(enabled);
+ }
+ }
}
/**
@@ -333,10 +351,10 @@ final class MultiCanvas extends Widget implements
Observable {
switch (children.size()) {
case 0: break;
case 1: var previous = (Region) children.removeLast();
- previous = addTitleBar(previous,
getControls(previous).title);
+ previous = addTitleBar(previous, getControls(previous));
children.add(previous);
// Fall through
- default: canvasView = addTitleBar(canvasView, controls.title);
+ default: canvasView = addTitleBar(canvasView, controls);
}
/*
* Add listeners for replicating navigation events of `canvas` into
all other visible canvases,
@@ -410,14 +428,20 @@ final class MultiCanvas extends Widget implements
Observable {
* Wraps the given {@code MapCanvas} view into a pane with a title.
*
* @param canvasView the {@link MapCanvas} view for which to add a title
bar.
+ * @param controls value of {@code canvasPool.get(canvas)} (not
necessarily obtained by that call).
* @return a wrapper of {@code canvasView} with the addition of a title
bar.
*/
- private BorderPane addTitleBar(final Region canvasView, final Label title)
{
+ private BorderPane addTitleBar(final Region canvasView, final Controls
controls) {
+ final Label title = controls.title;
final var close = new Button("❌");
close.setOnAction((event) -> closeCanvasView(canvasView));
+ final var pin = new ToggleButton();
+ pin.selectedProperty().addListener(controls);
+ FontGIS.setGlyph(pin, FontGIS.Code.MOVE, "⮀", -1, -1);
+ HBox.setHgrow(pin, Priority.NEVER);
HBox.setHgrow(title, Priority.ALWAYS);
HBox.setHgrow(close, Priority.NEVER);
- final var bar = new HBox(title, close);
+ final var bar = new HBox(pin, title, close);
bar.setAlignment(Pos.CENTER);
title.setAlignment(Pos.CENTER);
title.setMaxWidth(Double.MAX_VALUE);