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 20726edec3 Initial support of gesture synchronization between map
canvases in the same mosaic. This initial version only adds the visualization
of the mouse position in other canvases.
20726edec3 is described below
commit 20726edec308e06713bf317255c249599cbccd66
Author: Martin Desruisseaux <[email protected]>
AuthorDate: Wed Apr 8 00:16:56 2026 +0200
Initial support of gesture synchronization between map canvases in the same
mosaic.
This initial version only adds the visualization of the mouse position in
other canvases.
---
.../org/apache/sis/portrayal/CanvasFollower.java | 91 ++++++------
.../org/apache/sis/portrayal/package-info.java | 2 +-
.../apache/sis/gui/coverage/CoverageCanvas.java | 9 +-
.../org/apache/sis/gui/dataset/WindowHandler.java | 16 +-
.../org/apache/sis/gui/map/EvanescentPane.java | 91 ++++++++++++
.../org/apache/sis/gui/map/GestureFollower.java | 47 ++++--
.../main/org/apache/sis/gui/map/MapCanvas.java | 114 ++++++++-------
.../main/org/apache/sis/gui/map/MapCanvasAWT.java | 12 --
.../main/org/apache/sis/gui/map/MapWindows.java | 8 +-
.../main/org/apache/sis/gui/map/MultiCanvas.java | 162 ++++++++++++++++++---
.../main/org/apache/sis/gui/map/RenderingTask.java | 27 +++-
11 files changed, 425 insertions(+), 154 deletions(-)
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 a624916180..0c06d54a41 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
@@ -48,28 +48,28 @@ import
org.apache.sis.referencing.operation.transform.MathTransforms;
*
* <h2>Listeners</h2>
* {@code CanvasFollower} listeners need to be registered explicitly by a call
to the {@link #initialize()} method.
- * The {@link #dispose()} convenience method is provided for unregistering all
those listeners.
+ * Conversely, the {@link #dispose()} method unregisters all those listeners.
* The listeners registered by this class implement an unidirectional binding:
- * changes in source are applied on target, but not the converse.
+ * 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.
*
* @author Martin Desruisseaux (Geomatys)
- * @version 1.3
+ * @version 1.7
* @since 1.3
*/
public class CanvasFollower implements PropertyChangeListener, Disposable {
/**
* The canvas which is the source of zoom, translation or rotation events.
*/
- protected final PlanarCanvas source;
+ public final PlanarCanvas source;
/**
* The canvas on which to apply the change of zoom, translation or
rotation.
*/
- protected final PlanarCanvas target;
+ public final PlanarCanvas target;
/**
* Whether listeners have been registered.
@@ -109,23 +109,21 @@ public class CanvasFollower implements
PropertyChangeListener, Disposable {
/**
* Whether an attempt to compute {@link #displayTransform} has already
been done.
* The {@code displayTransform} field may still be null if the attempt
failed.
- * Value can be {@link #VALID}, {@link #OUTDATED}, {@link #UNKNOWN} or
{@link #ERROR}.
*/
- private byte displayTransformStatus;
+ private Status displayTransformStatus;
/**
* Whether an attempt to compute {@link #objectiveTransform} has already
been done.
* Note that the {@link #objectiveTransform} field can be up-to-date and
{@code null}.
- * Value can be {@link #VALID}, {@link #OUTDATED}, {@link #UNKNOWN} or
{@link #ERROR}.
*
* @see #findObjectiveTransform(String)
*/
- private byte objectiveTransformStatus;
+ private Status objectiveTransformStatus;
/**
* Enumeration values for {@link #displayTransformStatus} and {@link
#objectiveTransformStatus}.
*/
- private static final byte VALID = 0, OUTDATED = 1, UNKNOWN = 2, ERROR = 3;
+ private enum Status {VALID, OUTDATED, UNKNOWN, ERROR}
/**
* Whether a change is in progress. This is for avoiding never-ending loop
@@ -140,17 +138,21 @@ public class CanvasFollower implements
PropertyChangeListener, Disposable {
*
* <p>Caller needs to register listeners by a call to the {@link
#initialize()} method.
* This is not done automatically by this constructor for allowing users
to control
- * when to start listening to changes.</p>
+ * when to start listening to {@code source} change events.</p>
*
* @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.
+ * @throws IllegalArgumentException if {@code source} and {@code target}
are the same map canvas.
*/
public CanvasFollower(final PlanarCanvas source, final PlanarCanvas
target) {
this.source = Objects.requireNonNull(source);
this.target = Objects.requireNonNull(target);
+ if (source == target) {
+ throw new IllegalArgumentException();
+ }
followRealWorld = true;
- displayTransformStatus = OUTDATED;
- objectiveTransformStatus = OUTDATED;
+ displayTransformStatus = Status.OUTDATED;
+ objectiveTransformStatus = Status.OUTDATED;
}
/**
@@ -229,18 +231,18 @@ public class CanvasFollower implements
PropertyChangeListener, Disposable {
followRealWorld = real;
displayTransform = null;
objectiveTransform = null;
- displayTransformStatus = OUTDATED;
- objectiveTransformStatus = OUTDATED;
+ displayTransformStatus = Status.OUTDATED;
+ objectiveTransformStatus = Status.OUTDATED;
}
}
/**
- * Returns the objective coordinates of the Point Of Interest (POI) in
source canvas.
- * This information is used when the source and target canvases do not use
the same CRS.
- * Changes in "real world" coordinates on the {@linkplain #target} canvas
are guaranteed
- * to reflect the changes in "real world" coordinates of the {@linkplain
#source} canvas
- * at that location only. At all other locations, the "real world"
coordinate changes
- * may differ because of map projection deformations.
+ * Returns the objective coordinates of the Point Of Interest
(<abbr>POI</abbr>) in source canvas.
+ * This information is used when the source and target canvases do not use
the same <abbr>CRS</abbr>.
+ * This is the only location where changes in "real world" coordinates on
the {@linkplain #target} canvas
+ * are guaranteed to reflect the changes in "real world" coordinates of
the {@linkplain #source} canvas.
+ * At all other locations, the changes in "real world" coordinates may
differ because of map projection
+ * deformations.
*
* <p>The default implementation computes the value from {@link
#getSourceDisplayPOI()}
* if present, or fallback on {@code source.getPointOfInterest(true)}
otherwise.
@@ -283,20 +285,21 @@ public class CanvasFollower implements
PropertyChangeListener, Disposable {
/**
* Returns the transform from source display coordinates to target display
coordinates.
- * This transform may change every time that a zoom; translation or
rotation is applied
- * on at least one canvas. The transform may be absent if an error prevent
to compute it,
+ * This transform may change every time that a zoom, translation or
rotation is applied
+ * on at least one canvas. The transform may be absent if an error
prevents to compute it,
* for example is no coordinate operation has been found between the
objective CRS of the
* source and target canvases.
*
* @return transform from source display coordinates to target display
coordinates.
*/
public Optional<MathTransform2D> getDisplayTransform() {
- if (displayTransformStatus != VALID) {
- if (displayTransformStatus != OUTDATED) {
+ if (displayTransformStatus != Status.VALID) {
+ if (displayTransformStatus != Status.OUTDATED) {
return Optional.empty();
}
- displayTransformStatus = ERROR; // Set now in case an
exception is thrown below.
- if (objectiveTransformStatus == VALID ||
findObjectiveTransform("getDisplayTransform")) try {
+ displayTransform = null;
+ displayTransformStatus = Status.ERROR; // Set now in case an
exception is thrown below.
+ if (findObjectiveTransform("getDisplayTransform")) try {
/*
* Compute (source display to objective) → (map projection) →
(target objective to display).
* If we can work directly on `AffineTransform` instances, it
should be more efficient than
@@ -317,7 +320,7 @@ public class CanvasFollower implements
PropertyChangeListener, Disposable {
source.getObjectiveToDisplay().inverse(),
objectiveTransform,
target.getObjectiveToDisplay()));
}
- displayTransformStatus = VALID;
+ displayTransformStatus = Status.VALID;
} catch (NoninvertibleTransformException |
org.opengis.referencing.operation.NoninvertibleTransformException e) {
canNotCompute("getDisplayTransform", e);
}
@@ -327,8 +330,8 @@ public class CanvasFollower implements
PropertyChangeListener, Disposable {
/**
* Invoked when the objective CRS, zoom, translation or rotation changed
on a map that we are tracking.
- * If the event is an instance of {@link TransformChangeEvent}, then this
method applies the same change
- * on the {@linkplain #target} canvas.
+ * If the event is an instance of {@link TransformChangeEvent} emitted by
the {@linkplain #source} canvas,
+ * then this method applies the same change in "real world" units on the
{@linkplain #target} canvas.
*
* <p>This method delegates part of its work to the following methods,
* which can be overridden for altering the changes:</p>
@@ -347,13 +350,13 @@ public class CanvasFollower implements
PropertyChangeListener, Disposable {
@Override
public void propertyChange(final PropertyChangeEvent event) {
if (!changing && event instanceof TransformChangeEvent) try {
- final TransformChangeEvent te = (TransformChangeEvent) event;
- displayTransformStatus = OUTDATED;
+ final var te = (TransformChangeEvent) event;
+ displayTransformStatus = Status.OUTDATED;
changing = true;
if (te.isSameSource(source)) {
transformedSource(te);
if (!disabled && filter(te)) {
- if (followRealWorld && (objectiveTransformStatus == VALID
|| findObjectiveTransform("propertyChange"))) {
+ if (followRealWorld &&
findObjectiveTransform("propertyChange")) {
AffineTransform before =
te.getObjectiveChange2D().orElse(null);
if (before != null) try {
/*
@@ -385,8 +388,8 @@ public class CanvasFollower implements
PropertyChangeListener, Disposable {
} else if
(PlanarCanvas.OBJECTIVE_CRS_PROPERTY.equals(event.getPropertyName())) {
displayTransform = null;
objectiveTransform = null;
- displayTransformStatus = OUTDATED;
- objectiveTransformStatus = OUTDATED;
+ displayTransformStatus = Status.OUTDATED;
+ objectiveTransformStatus = Status.OUTDATED;
}
}
@@ -461,18 +464,20 @@ public class CanvasFollower implements
PropertyChangeListener, Disposable {
/**
* Finds the transform to use for converting changes from {@linkplain
#source} canvas to {@linkplain #target} canvas.
- * This method should be invoked only if {@link #objectiveTransformStatus}
is not {@link #VALID}. After this method
- * returned, {@link #objectiveTransform} contains the transform to use,
which may be {@code null} if none.
+ * After this method returned, {@link #objectiveTransform} is the
transform to use, which may be {@code null} if none.
*
* @param caller the public method which is invoked this private method.
Used only for logging purposes.
* @return whether a transform has been computed.
*/
private boolean findObjectiveTransform(final String caller) {
- if (objectiveTransformStatus == OUTDATED) {
+ if (objectiveTransformStatus == Status.VALID) {
+ return true;
+ }
+ if (objectiveTransformStatus == Status.OUTDATED) {
displayTransform = null;
objectiveTransform = null;
- displayTransformStatus = OUTDATED;
- objectiveTransformStatus = ERROR; // If an exception occurs,
use above setting.
+ displayTransformStatus = Status.OUTDATED;
+ objectiveTransformStatus = Status.ERROR; // If an exception
occurs, use above setting.
final CoordinateReferenceSystem sourceCRS =
source.getObjectiveCRS();
final CoordinateReferenceSystem targetCRS =
target.getObjectiveCRS();
if (sourceCRS != null && targetCRS != null) try {
@@ -487,13 +492,13 @@ public class CanvasFollower implements
PropertyChangeListener, Disposable {
if (objectiveTransform.isIdentity()) {
objectiveTransform = null;
}
- objectiveTransformStatus = VALID;
+ objectiveTransformStatus = Status.VALID;
return true;
} catch (FactoryException e) {
canNotCompute(caller, e);
// Stay with "changes in display units" mode.
} else {
- objectiveTransformStatus = UNKNOWN;
+ objectiveTransformStatus = Status.UNKNOWN;
}
}
return false;
@@ -502,8 +507,6 @@ public class CanvasFollower implements
PropertyChangeListener, Disposable {
/**
* Invoked when the {@link #objectiveTransform} transform cannot be
computed,
* or when an optional information required for that transform is missing.
- * This method assumes that the public caller (possibly indirectly) is
- * {@link #propertyChange(PropertyChangeEvent)}.
*
* @param caller the public method which is invoked this private method.
Used only for logging purposes.
* @param e the exception that occurred.
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 40c78c92ce..9a6c67d79e 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
@@ -25,7 +25,7 @@
* Synchronization, if desired, must be done by the caller.
*
* @author Johann Sorel (Geomatys)
- * @version 1.5
+ * @version 1.7
* @since 1.1
*/
package org.apache.sis.portrayal;
diff --git
a/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/coverage/CoverageCanvas.java
b/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/coverage/CoverageCanvas.java
index b7857cf7e7..73045b52f3 100644
---
a/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/coverage/CoverageCanvas.java
+++
b/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/coverage/CoverageCanvas.java
@@ -39,6 +39,7 @@ import javafx.scene.Node;
import javafx.scene.image.Image;
import javafx.scene.paint.Color;
import javafx.scene.shape.Shape;
+import javafx.scene.layout.Pane;
import javafx.scene.layout.Region;
import javafx.scene.layout.Background;
import javafx.scene.layout.BackgroundImage;
@@ -937,7 +938,7 @@ public class CoverageCanvas extends MapCanvasAWT {
/**
* Value of {@link CoverageCanvas#getObjectiveToDisplay()} at the time
this worker has been initialized.
- * This is the conversion from {@link #objectiveCRS} to the canvas
display CRS.
+ * This is the conversion from {@link #objectiveCRS} to the canvas
display <abbr>CRS</abbr>.
* Can be thought as a conversion from "real world" units to pixel
units
* and depends on the zoom and translation events that happened before
rendering.
*/
@@ -1426,7 +1427,7 @@ public class CoverageCanvas extends MapCanvasAWT {
}
}
Platform.runLater(() -> {
- final ObservableList<Node> children =
floatingPane.getChildren();
+ final ObservableList<Node> children =
getEvanescentPane().getChildren();
FadeTransition transition;
while ((transition = tileShapes.poll()) != null) {
children.add(transition.getNode());
@@ -1445,7 +1446,9 @@ public class CoverageCanvas extends MapCanvasAWT {
@Override
public void handle(final ActionEvent event) {
final var transition = (FadeTransition) event.getSource();
- if (floatingPane.getChildren().remove(transition.getNode()) &&
TRACE) {
+ final Node node = transition.getNode();
+ final Pane parent = (Pane) node.getParent();
+ if (parent != null && parent.getChildren().remove(node) && TRACE) {
trace("TileReadListener.removeChild");
}
}
diff --git
a/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/dataset/WindowHandler.java
b/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/dataset/WindowHandler.java
index 6331ab20a9..dc3e4d0b79 100644
---
a/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/dataset/WindowHandler.java
+++
b/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/dataset/WindowHandler.java
@@ -80,7 +80,7 @@ public abstract class WindowHandler {
* to retain a direct reference to {@link #window} in listeners, which
would increase the risk of memory leak.
*/
private static final ChangeListener<String> TITLE_CHANGED = (p,o,n) -> {
- final WindowHandler handler = (WindowHandler) ((StringProperty)
p).getBean();
+ final var handler = (WindowHandler) ((StringProperty) p).getBean();
handler.window.setTitle(n + " — Apache SIS");
};
@@ -263,9 +263,9 @@ public abstract class WindowHandler {
@Override public void eventOccured(final CloseEvent event) {
final Resource resource = event.getSource();
if (Platform.isFxApplicationThread()) {
- close(resource, WindowHandler.this);
+ close(resource);
} else BackgroundThreads.runAndWaitDialog(() -> {
- close(resource, WindowHandler.this);
+ close(resource);
return null;
});
// No need to invoke `resource.removeListener(…)`, that work is
done by `StoreListeners.close()`.
@@ -275,16 +275,16 @@ public abstract class WindowHandler {
/**
* Closes all windows (except the main window) which are showing the given
resource.
* This is invoked when the resource has been closed in the {@link
ResourceTree}.
+ * The handler managed by {@code this} is closed unconditionally
regardless its resource.
*
* @param resource the resource which has been closed.
- * @param force the handler to close unconditionally regardless its
resource.
*/
- private static void close(final Resource resource, final WindowHandler
force) {
- final WindowHandler ignore = force.manager.main;
- final ObservableList<WindowHandler> windows =
force.manager.modifiableWindowList;
+ private void close(final Resource resource) {
+ final WindowHandler ignore = manager.main;
+ final ObservableList<WindowHandler> windows =
manager.modifiableWindowList;
for (int i = windows.size(); --i >= 0;) {
final WindowHandler handler = windows.get(i);
- if (handler != ignore && (handler == force ||
handler.getResource() == resource)) {
+ if (handler != ignore && (handler == this || handler.getResource()
== resource)) {
windows.remove(i);
if (handler.window != null) {
handler.window.setOnHidden(null); // Because we will
invoke `dispose()` ourselves.
diff --git
a/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/map/EvanescentPane.java
b/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/map/EvanescentPane.java
new file mode 100644
index 0000000000..b9e77117d3
--- /dev/null
+++
b/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/map/EvanescentPane.java
@@ -0,0 +1,91 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sis.gui.map;
+
+import java.util.List;
+import javafx.scene.Node;
+import javafx.scene.layout.Pane;
+import javafx.collections.ObservableList;
+import javafx.collections.ListChangeListener;
+
+
+/**
+ * A pane for evanescent shapes shown in a {@link MapCanvas}. The nodes in
this pane should use
+ * an animation such as {@link javafx.animation.FadeTransition} and be removed
after a few seconds.
+ *
+ * <p>This pane is used when it is not worth to update the shapes coordinates
after user's navigation
+ * (zooms, pans, rotations) because those shapes will disappear soon anyway.
Instead, a transform will
+ * be added the whole pane when needed.</p>
+ *
+ * @author Martin Desruisseaux (Geomatys)
+ */
+final class EvanescentPane extends Pane implements ListChangeListener<Node> {
+ /**
+ * Whether the pane had at least one child.
+ */
+ private boolean hadChildren;
+
+ /**
+ * Creates an initially empty pane with no shape and no transform.
+ */
+ private EvanescentPane() {
+ }
+
+ /**
+ * Returns a pane with an identity transform from the given list of
children.
+ * If there is no pane with an identity transform, a new pane is created
and
+ * added to the list of children.
+ *
+ * <p>Note that the returned pane has an identity transform at the time
that
+ * this method is invoked, but that transform may become non-identity later
+ * if the user navigates on the map (e.g. zoom or pan events).</p>
+ *
+ * @param children the children of {@link MapCanvas#floatingPane}.
+ * @return a pane with an identity transform at the time that this method
is invoked.
+ */
+ static EvanescentPane getOrCreate(final List<Node> children) {
+ for (int i = children.size(); --i >= 0;) {
+ if (children.get(i) instanceof EvanescentPane pane) {
+ if (pane.getTransforms().isEmpty()) {
+ return pane;
+ }
+ }
+ }
+ final var pane = new EvanescentPane();
+ pane.getChildren().addListener(pane);
+ children.add(pane);
+ return pane;
+ }
+
+ /**
+ * Invoked when the list of children changed.
+ * If the last child has been removed, removes this pane from the map
canvas.
+ */
+ @Override
+ public void onChanged(Change<? extends Node> change) {
+ final ObservableList<Node> children = getChildren();
+ if (!children.isEmpty()) {
+ hadChildren = true;
+ } else if (hadChildren) {
+ children.removeListener(this);
+ final Pane parent = (Pane) getParent();
+ if (parent != null) {
+ parent.getChildren().remove(this);
+ }
+ }
+ }
+}
diff --git
a/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/map/GestureFollower.java
b/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/map/GestureFollower.java
index 04323ad9ac..ca849101fc 100644
---
a/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/map/GestureFollower.java
+++
b/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/map/GestureFollower.java
@@ -41,18 +41,20 @@ import org.opengis.referencing.operation.TransformException;
import org.apache.sis.portrayal.TransformChangeEvent;
import org.apache.sis.portrayal.CanvasFollower;
import org.apache.sis.util.logging.Logging;
-import static org.apache.sis.gui.internal.LogHandler.LOGGER;
+import org.apache.sis.gui.internal.LogHandler;
/**
* A listener of mouse or keyboard events in a source canvas which can be
reproduced in a target canvas.
* This listener can reproduce the "real world" displacements documented in
{@linkplain CanvasFollower parent class}.
* In addition, this class can also follow mouse movements in source canvas
and move a cursor in the target canvas
- * at the same "real world" position.
+ * at the same "real world" position. These two features are disabled by
default and can be enabled by setting the
+ * {@link #transformEnabled} and/or {@link #cursorEnabled} flags to {@code
true}.
*
* <h2>Listeners</h2>
* {@code GestureFollower} listeners need to be registered explicitly by a
call to the {@link #initialize()} method.
- * The {@link #dispose()} convenience method is provided for unregistering all
those listeners.
+ * Conversely, the {@link #dispose()} method unregisters all those listeners.
The listeners are unidirectional:
+ * changes in the source canvas are applied on the target canvas, but not the
converse.
*
* <h2>Multi-threading</h2>
* This class is <strong>not</strong> thread-safe.
@@ -89,13 +91,16 @@ public class GestureFollower extends CanvasFollower
implements EventHandler<Mous
/**
* Whether changes in the "objective to display" transforms should be
propagated from source to target canvas.
- * The default value is {@code false}; this property needs to be enabled
explicitly by caller if desired.
+ * The default value is {@code false}. This property needs to be enabled
explicitly by caller if desired.
+ *
+ * @see #isDisabled()
+ * @see #setDisabled(boolean)
*/
public final BooleanProperty transformEnabled;
/**
* Whether mouse position in source canvas should be shown by a cursor in
the target canvas.
- * The default value is {@code false}; this property needs to be enabled
explicitly by caller if desired.
+ * The default value is {@code false}. This property needs to be enabled
explicitly by caller if desired.
*/
public final BooleanProperty cursorEnabled;
@@ -124,10 +129,11 @@ public class GestureFollower extends CanvasFollower
implements EventHandler<Mous
*
* <p>Caller needs to register listeners by a call to the {@link
#initialize()} method.
* This is not done automatically by this constructor for allowing users
to control
- * when to start listening to changes.</p>
+ * when to start listening to {@code source} change events.</p>
*
* @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.
+ * @throws IllegalArgumentException if {@code source} and {@code target}
are the same map canvas.
*/
@SuppressWarnings("this-escape") // The invoked method does not store
`this` and is not overrideable.
public GestureFollower(final MapCanvas source, final MapCanvas target) {
@@ -231,28 +237,32 @@ public class GestureFollower extends CanvasFollower
implements EventHandler<Mous
/**
* Sets the cursor location in the target canvas to a position computed
from current value
- * of {@link #cursorSourcePosition}.
+ * of {@link #cursorSourcePosition}. If the position cannot be computed,
the cursor is hidden
+ * for avoiding to mislead the user with a wrong position.
*/
private void updateCursorPosition() {
+ @SuppressWarnings("LocalVariableHidesMemberVariable")
+ final Path cursor = this.cursor;
final MathTransform2D tr = getDisplayTransform().orElse(null);
if (tr != null) try {
- final Point2D p = tr.transform(cursorSourcePosition,
cursorTargetPosition);
+ final Point2D p = tr.transform(cursorSourcePosition,
cursorTargetPosition);
cursor.setTranslateX(p.getX());
cursor.setTranslateY(p.getY());
+ return;
} catch (TransformException e) {
- cursorSourceValid = false;
- cursor.setVisible(false);
- Logging.recoverableException(LOGGER, GestureFollower.class,
"handle", e);
+ canNotCompute("handle", e);
}
+ cursorSourceValid = false;
+ cursor.setVisible(false);
}
/**
* Returns {@code true} if this listener should replicate the following
changes on the target canvas.
* This implementation returns {@code true} if the transform reason is
{@link TransformChangeEvent.Reason#INTERIM}.
- * It allows immediate feedback to users without waiting for the
background thread to complete rendering.
+ * It allows immediate feedback to users without waiting for the
background thread to complete the rendering.
*
* @param event a transform change event that occurred on the source
canvas.
- * @return whether to replicate that change on the target canvas.
+ * @return whether to replicate that change on the target canvas.
*/
@Override
protected boolean filter(final TransformChangeEvent event) {
@@ -285,12 +295,23 @@ public class GestureFollower extends CanvasFollower
implements EventHandler<Mous
change.inverseTransform(cursorSourcePosition,
cursorSourcePosition);
updateCursorPosition();
} catch (NoninvertibleTransformException e) {
+ canNotCompute("transformedSource", e);
cursorSourceValid = false;
cursor.setVisible(false);
}
}
}
+ /**
+ * Invoked when a transform cannot be computed.
+ *
+ * @param caller the public method which is invoked this private method.
Used only for logging purposes.
+ * @param e the exception that occurred.
+ */
+ private static void canNotCompute(final String caller, final Exception e) {
+ Logging.recoverableException(LogHandler.LOGGER, GestureFollower.class,
caller, e);
+ }
+
/**
* Removes all listeners registered by this {@code GestureFollower}
instance.
* This method should be invoked when {@code GestureFollower} is no longer
needed,
diff --git
a/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/map/MapCanvas.java
b/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/map/MapCanvas.java
index a54039f8ff..4e8fc1b150 100644
--- a/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/map/MapCanvas.java
+++ b/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/map/MapCanvas.java
@@ -50,6 +50,7 @@ import javafx.concurrent.Worker;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.ToggleGroup;
import javafx.scene.transform.Affine;
+import javafx.scene.transform.Transform;
import javafx.scene.transform.NonInvertibleTransformException;
import javax.measure.Quantity;
import javax.measure.quantity.Length;
@@ -190,6 +191,8 @@ public abstract class MapCanvas extends PlanarCanvas {
* are temporary; they are applied for producing immediate visual feedback
while the map is recomputed
* in a background thread. Once calculation is completed and the content
of this pane has been updated,
* the {@code floatingPane} {@link Affine} transform is reset to
identity.</p>
+ *
+ * @see #getEvanescentPane()
*/
protected final Pane floatingPane;
@@ -282,12 +285,6 @@ public abstract class MapCanvas extends PlanarCanvas {
*/
private final Affine transform;
- /**
- * The {@link #transform} values at the time the {@link #repaint()} method
has been invoked.
- * This is a change applied on {@link #objectiveToDisplay} but not yet
visible in the map.
- */
- private final Affine changeInProgress;
-
/**
* Cursor position at the time pan event started.
* This is used for computing the {@linkplain #floatingPane} translation
to apply during drag events.
@@ -368,8 +365,7 @@ public abstract class MapCanvas extends PlanarCanvas {
@SuppressWarnings("this-escape")
public MapCanvas(final Locale locale) {
super(locale);
- transform = new Affine();
- changeInProgress = new Affine();
+ transform = new Affine();
final Pane view = new Pane() {
@Override protected void layoutChildren() {
super.layoutChildren();
@@ -441,6 +437,30 @@ public abstract class MapCanvas extends PlanarCanvas {
invalidObjectiveToDisplay = true;
}
+ /**
+ * Returns a pane for evanescent shapes shown in this canvas. The shapes
added in the returned pane should
+ * use an animation effect such as {@link javafx.animation.FadeTransition}
and be removed after a few seconds.
+ * This method allows to show shapes more easily than adding them directly
as {@link #floatingPane} children,
+ * but at the expanse of quality. Evanescent panes are easier to use
because the callers do not need to care
+ * about changes of the {@linkplain #getObjectiveToDisplay() objective to
display} transform.
+ * Instead, callers can add shapes with coordinate values computed using
the current transform
+ * at the time that this method is invoked, and ignore the changes that
may happen afterward.
+ * If the user continues to navigate on the map, the {@linkplain
Pane#getTransforms() list of transforms}
+ * of the returned pane will be updated.
+ *
+ * <p>While this method allows a much easier strategy than adding shapes
into {@link #floatingPane}
+ * and tracking <i>objective to display</i> changes, the result is rougher
(especially after zooms).
+ * For this reason, this method should be used for short-lived shapes such
as animation effects,
+ * when the high-quality strategy is not worth the effort. Since the
returned pane is aimed to be short-lived,
+ * it is automatically removed from this {@code MapPane} when its list of
children become empty.</p>
+ *
+ * @return a pane where to add shapes without the need to track navigation
events after this method call.
+ * @since 1.7
+ */
+ public Pane getEvanescentPane() {
+ return EvanescentPane.getOrCreate(floatingPane.getChildren());
+ }
+
/**
* Returns the bounds of the content in {@link #floatingPane} coordinates,
or {@code null} if unknown.
* Some subclasses may compute a larger image than the widget size for
better visual transition during
@@ -664,23 +684,11 @@ public abstract class MapCanvas extends PlanarCanvas {
event.consume();
}
- /**
- * Removes map content and clears the properties of this canvas.
- * Invoking this method may help to release memory when the map is no
longer shown.
- * The effect of the cleanup action may not be immediate, but may happen
an arbitrary
- * time after this method call.
- *
- * <p>For overriding this method in subclasses, see {@link #clear()}.</p>
- *
- * @since 1.7
- */
- public final void clearLater() {
- runAfterRendering(this::clear);
- }
-
/**
* Resets the map view to its default zoom level and default position with
no rotation.
* Contrarily to {@link #clearLater()}, this method does not remove the
map content.
+ *
+ * @see #clearLater()
*/
public void reset() {
invalidObjectiveToDisplay = true;
@@ -1307,12 +1315,12 @@ public abstract class MapCanvas extends PlanarCanvas {
}
/*
* If a temporary zoom, rotation or translation has been applied using
JavaFX transform API,
- * replace that temporary transform by a "permanent" adjustment of the
`objectiveToDisplay`
- * transform. It allows SIS to get new data for the new visible area
and resolution.
- * Do not reset `transform` to identity now; we need to continue
accumulating gestures
- * that may happen while the rendering is done in a background thread.
+ * replace that temporary transform by an adjustment of the
`objectiveToDisplay` transform.
+ * It allows SIS to get new data for the new visible area and
resolution.
+ *
+ * Note: do not reset `transform` to identity now because we need to
continue accumulating
+ * gestures that may happen while the rendering is done in a
background thread.
*/
- changeInProgress.setToTransform(transform);
if (!transform.isIdentity()) {
super.transformDisplayCoordinates(getInterimTransform(false));
}
@@ -1327,6 +1335,7 @@ public abstract class MapCanvas extends PlanarCanvas {
if (context != null && context.initialize(floatingPane)) {
final RenderingTask<?> worker = createWorker(context);
assert renderingInProgress == null : renderingInProgress;
+ worker.setChangeInProgress(transform);
BackgroundThreads.execute(worker);
renderingInProgress = worker; // Set after we know that the
task has been scheduled.
if (!isCursorChangeScheduled) {
@@ -1377,12 +1386,13 @@ public abstract class MapCanvas extends PlanarCanvas {
/**
* Invoked after the background thread created by {@link #repaint()}
finished to update map content.
* This method should be invoked in all cases: after successful
completion, failure or cancellation.
- * The {@link #changeInProgress} is the JavaFX transform at the time the
repaint event was trigged and
- * which is now integrated in the map. That transform will be removed from
{@link #floatingPane} transforms.
- * The {@link #transform} result is identity if no zoom, rotation or pan
gesture has been applied since last
- * rendering.
+ * The {@link RenderingTask#changeInProgress} transform is the JavaFX
transform at the time that the
+ * repaint event started and which is now integrated in the map.
+ * That transform will be removed from {@link #floatingPane} transforms.
+ * The {@link #transform} result is identity if no zoom, rotation or pan
gesture has been applied
+ * since last rendering.
*
- * <h4>Use case</h4>
+ * <h4>Example</h4>
* <p>Suppose that the {@link RenderingTask} has been started in response
to some user gestures.
* For example, the user has zoomed on the map. The renderer has been
initialized with a snapshot
* of this {@code MapCanvas} state at the time when the {@link Renderer}
has been constructed.
@@ -1414,28 +1424,24 @@ public abstract class MapCanvas extends PlanarCanvas {
* Display coordinates stored in this `MapCanvas` need to be converted
to the
* new display coordinates, as expected by the new "objective to
display" CRS.
*/
+ final Transform changeInProgress = task.getChangeInProgress();
final Point2D p = changeInProgress.transform(xPanStart, yPanStart);
xPanStart = p.getX();
yPanStart = p.getY();
- Affine copyOfChanges = null;
for (final Node child : floatingPane.getChildren()) {
- if (needsPositionUpdateAfterRepaint(child)) {
- if (copyOfChanges == null) {
- copyOfChanges = new Affine(changeInProgress);
- }
- child.getTransforms().add(0, copyOfChanges);
+ if (child instanceof EvanescentPane) {
+ child.getTransforms().addFirst(changeInProgress);
}
}
try {
- changeInProgress.invert();
- transform.append(changeInProgress);
+ transform.append(changeInProgress.createInverse());
/*
* Note: intuitively one may expect `prepend(…)` instead of
`append(…)` above.
* The use of `prepend(…)` would give a `transform` result which
would be as if
* the transform was the identity transform at the time that
rendering started,
* and all operations on it are gesture events that occurred while
the renderer
* was working in background. But actually this is not quite
correct.
- * See the zoom-in discussion in "use case" section in method
javadoc.
+ * See the zoom-in discussion in "example" section of method
javadoc.
*/
} catch (NonInvertibleTransformException e) {
unexpectedException("repaint", e);
@@ -1464,15 +1470,6 @@ public abstract class MapCanvas extends PlanarCanvas {
}
}
- /**
- * Returns whether the given element of the {@link #floatingPane} children
list needs to have its position
- * updated after a repaint event. If {@code true}, the position is updated
with the addition of an affine
- * transform which contains the zoom changes applied by the repaint event.
- */
- boolean needsPositionUpdateAfterRepaint(final Node child) {
- return true;
- }
-
/**
* A pseudo-rendering task which wait for some delay before to perform the
real repaint.
* The intent is to collect some more gesture events (pans, zooms,
<i>etc.</i>) before consuming CPU time.
@@ -1704,11 +1701,11 @@ public abstract class MapCanvas extends PlanarCanvas {
* Implementations in subclasses shall invoke {@code super.clear()}.</p>
*
* @see #clearLater()
+ * @see #reset()
*/
protected void clear() {
assert Platform.isFxApplicationThread();
transform.setToIdentity();
- changeInProgress.setToIdentity();
invalidObjectiveToDisplay = true;
initialState = null;
clearError();
@@ -1719,6 +1716,21 @@ public abstract class MapCanvas extends PlanarCanvas {
requestRepaint();
}
+ /**
+ * Removes map content and clears the properties of this canvas.
+ * Invoking this method may help to release memory when the map is no
longer shown.
+ * The effect of the cleanup action may not be immediate, but may happen
an arbitrary
+ * time after this method call.
+ *
+ * <p>For overriding this method in subclasses, see {@link #clear()}.</p>
+ *
+ * @see #reset()
+ * @since 1.7
+ */
+ public final void clearLater() {
+ runAfterRendering(this::clear);
+ }
+
/**
* Returns a string representation of this canvas for debugging purposes.
* This string spans multiple lines.
diff --git
a/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/map/MapCanvasAWT.java
b/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/map/MapCanvasAWT.java
index a3f7f002f4..72ca3a19b9 100644
---
a/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/map/MapCanvasAWT.java
+++
b/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/map/MapCanvasAWT.java
@@ -33,7 +33,6 @@ import javafx.application.Platform;
import javafx.geometry.Bounds;
import javafx.geometry.Insets;
import javafx.geometry.Rectangle2D;
-import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.image.ImageView;
import javafx.scene.image.PixelBuffer;
@@ -163,17 +162,6 @@ public abstract class MapCanvasAWT extends MapCanvas {
floatingPane.getChildren().add(image);
}
- /**
- * Returns whether the given element of the {@link #floatingPane} children
list needs to have its position
- * updated after a repaint event. If {@code true}, the position is updated
with the addition of an affine
- * transform which contains the zoom changes applied by the repaint event.
- */
- @Override
- final boolean needsPositionUpdateAfterRepaint(final Node child) {
- // Exclude the image because it already contains the zoom changes
applied by the repaint event.
- return child != image;
- }
-
/**
* Returns the image bounds. This is used for determining if a
* repaint is necessary after {@link MapCanvas} size changed.
diff --git
a/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/map/MapWindows.java
b/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/map/MapWindows.java
index e6c7bbc5b9..a410b393cd 100644
---
a/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/map/MapWindows.java
+++
b/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/map/MapWindows.java
@@ -176,10 +176,10 @@ public class MapWindows implements AutoCloseable {
*/
@Override
public void close() {
- for (final Map.Entry<MultiCanvas, Stage> entry : windows.entrySet()) {
- entry.getKey().dispose();
- entry.getValue().close();
- }
+ windows.forEach((canvas, stage) -> {
+ canvas.dispose();
+ stage.close();
+ });
windows.clear();
}
}
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 ca9c820576..70026ff3a6 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
@@ -16,13 +16,16 @@
*/
package org.apache.sis.gui.map;
+import java.util.ArrayList;
import java.util.Collection;
+import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.LinkedHashMap;
import java.util.Locale;
import java.util.NoSuchElementException;
import java.util.Objects;
+import java.util.Set;
import javafx.application.Platform;
import javafx.beans.InvalidationListener;
import javafx.beans.Observable;
@@ -48,6 +51,7 @@ import org.apache.sis.gui.internal.BackgroundThreads;
import org.apache.sis.gui.internal.DataStoreOpener;
import static org.apache.sis.gui.internal.LogHandler.LOGGER;
import org.apache.sis.gui.referencing.RecentReferenceSystems;
+import org.apache.sis.io.TableAppender;
import org.apache.sis.storage.Resource;
import org.apache.sis.storage.GridCoverageResource;
import org.apache.sis.storage.DataStoreException;
@@ -132,6 +136,11 @@ final class MultiCanvas extends Widget implements
Observable {
*/
final StatusBar status;
+ /**
+ * Listeners of mouse displacements and navigation actions such as
zooms and pans.
+ */
+ final List<GestureFollower> followers;
+
/**
* Creates new controls for a new map canvas, then registers the
listeners.
* Note that these listeners create cyclic references: most references
are from {@code canvas} to {@code owner}
@@ -142,6 +151,7 @@ final class MultiCanvas extends Widget implements
Observable {
* @param canvas the new canvas.
*/
Controls(final MultiCanvas owner, final MapCanvas canvas) {
+ followers = new ArrayList<>();
title = new Label();
status = new StatusBar(owner.referenceSystems);
status.track(canvas);
@@ -150,6 +160,45 @@ final class MultiCanvas extends Widget implements
Observable {
menu.addCopyOptions(status);
getView(canvas).addEventHandler(MouseEvent.MOUSE_ENTERED, (event)
-> owner.showStatusBar(canvas));
}
+
+ /**
+ * Creates listeners of navigation events in the source canvas, for
replicating them in the target canvas.
+ * The {@code source} argument <em>shall</em> be the {@code canvas}
argument given to the constructor.
+ *
+ * @param source the {@code canvas} argument given to the
constructor.
+ * @param target the canvas where to replicate the navigation events
applied on {@code source}.
+ */
+ final void addGestureFollower(final MapCanvas source, final MapCanvas
target) {
+ final var follower = new GestureFollower(source, target);
+ follower.initialize();
+ follower.cursorEnabled.set(true);
+ followers.add(follower);
+ }
+
+ /**
+ * Removes the listeners which were replicating navigation events to
the specified target.
+ *
+ * @param target the canvas where the navigation events of {@code
source} were replicated.
+ */
+ final void removeGestureFollower(final MapCanvas target) {
+ for (int i = followers.size(); --i >= 0;) {
+ if (followers.get(i).target == target) {
+ followers.remove(i).dispose();
+ }
+ }
+ }
+
+ /**
+ * Unregisters the listeners which were following the mouse
displacements and navigation events.
+ * Also clears the title for preventing it to contribute to {@link
#getCanvasTitles()}.
+ * Does not unregister the listener for showing the status bar, as
this listener does not depend
+ * on which data are shown in the canvas. This {@code Controls} may be
reused for new data.
+ */
+ final void clear() {
+ title.setText(null);
+ followers.forEach(GestureFollower::dispose);
+ followers.clear();
+ }
}
/**
@@ -225,9 +274,24 @@ final class MultiCanvas extends Widget implements
Observable {
return (Region) ((child instanceof BorderPane pane) ? pane.getCenter()
: child);
}
+ /**
+ * Returns the views of all map canvases that are currently shown in this
{@code MultiCanvas}.
+ * The returned set is modifiable (callers may remove elements) and
elements are in no particular order.
+ *
+ * @param children value of {@code canvasGrid.getChildren()}.
+ * @return all currently visible map canvases views.
+ */
+ private static Set<Region> getCanvasViews(final List<Node> children) {
+ final Set<Region> views = HashSet.newHashSet(children.size());
+ for (final Node child : children) {
+ views.add(getCanvasView(child));
+ }
+ return views;
+ }
+
/**
* Returns the controls of a canvas when only its view is known.
- * This method is inefficient, but is invoked in contextes where the map
has only one element.
+ * This method is inefficient, but is invoked in contexts where the map
has only one element.
*
* @param canvasView the result of a call to {@link
#getCanvasView(Node)}.
* @return the controls for the canvas having the given view.
@@ -258,6 +322,12 @@ final class MultiCanvas extends Widget implements
Observable {
@SuppressWarnings("fallthrough")
private Label addCanvasView(final MapCanvas canvas) {
final Controls controls = canvasPool.computeIfAbsent(canvas, (key) ->
new Controls(this, key));
+ /*
+ * If the canvas will be alone in the window, show the view directly
without title and border.
+ * If more than one canvas will exist, wrap the canvas view in a pane
with a title and a border
+ * for separating this view from other views. It may require adding
the title bar and border to
+ * a previously existing canvas view if the latter was alone before
this method call.
+ */
Region canvasView = getView(canvas);
final List<Node> children = canvasGrid.getChildren();
switch (children.size()) {
@@ -268,6 +338,18 @@ final class MultiCanvas extends Widget implements
Observable {
// Fall through
default: canvasView = addTitleBar(canvasView, controls.title);
}
+ /*
+ * Add listeners for replicating navigation events of `canvas` into
all other visible canvases,
+ * and conversely. Shall be done before the canvas view is added to
the children list.
+ */
+ final Set<Region> visibles = getCanvasViews(children);
+ for (final Map.Entry<MapCanvas, Controls> entry :
canvasPool.entrySet()) {
+ final MapCanvas other = entry.getKey();
+ if (visibles.contains(getView(other))) {
+ controls.addGestureFollower(canvas, other);
+ entry.getValue().addGestureFollower(other, canvas);
+ }
+ }
children.add(canvasView);
return controls.title;
}
@@ -278,10 +360,12 @@ final class MultiCanvas extends Widget implements
Observable {
* instead of trying to remove entries from the {@link #canvasPool} map,
current implementation rather just
* hides unused canvases and may reuse them later.
*
- * @param canvas the canvas for which to remove the view.
+ * @param canvas the canvas for which to remove the view.
+ * @param controls value of {@code canvasPool.get(canvas)} (not
necessarily obtained by that call).
* @return whether the view has been found and removed.
*/
- private boolean removeCanvasView(final MapCanvas canvas) {
+ private boolean removeCanvasView(final MapCanvas canvas, final Controls
controls) {
+ canvasPool.values().forEach((other) ->
other.removeGestureFollower(canvas));
boolean changed = false;
final Region canvasView = getView(canvas);
final List<Node> children = canvasGrid.getChildren();
@@ -296,7 +380,7 @@ final class MultiCanvas extends Widget implements
Observable {
previous = getCanvasView(previous);
children.add(previous); // Hide the title bar and
the border.
}
- clear(canvas);
+ clear(canvas, controls);
return changed;
}
@@ -307,12 +391,12 @@ final class MultiCanvas extends Widget implements
Observable {
*
* @param canvasView view of the canvas to close.
*/
- private void removeCanvasView(final Region canvasView) {
+ private void closeCanvasView(final Region canvasView) {
boolean changed = false;
- for (final MapCanvas canvas : canvasPool.keySet()) {
+ for (final Map.Entry<MapCanvas, Controls> entry :
canvasPool.entrySet()) {
+ final MapCanvas canvas = entry.getKey();
if (getView(canvas) == canvasView) {
- changed |= removeCanvasView(canvas);
- clear(canvas);
+ changed |= removeCanvasView(canvas, entry.getValue());
// Should have only one instance, but continue the loop by
paranoia.
}
}
@@ -330,7 +414,7 @@ final class MultiCanvas extends Widget implements
Observable {
*/
private BorderPane addTitleBar(final Region canvasView, final Label title)
{
final var close = new Button("❌");
- close.setOnAction((event) -> removeCanvasView(canvasView));
+ close.setOnAction((event) -> closeCanvasView(canvasView));
HBox.setHgrow(title, Priority.ALWAYS);
HBox.setHgrow(close, Priority.NEVER);
final var bar = new HBox(title, close);
@@ -415,7 +499,7 @@ final class MultiCanvas extends Widget implements
Observable {
* Get the collection of `MapCanvas` instances which are not currently
used.
* These instances may be recycled for the given resource.
*/
- final var available = LinkedHashMap.<Node,
MapCanvas>newLinkedHashMap(canvasPool.size());
+ final Map<Node, MapCanvas> available =
LinkedHashMap.newLinkedHashMap(canvasPool.size());
canvasPool.keySet().forEach((canvas) -> available.put(getView(canvas),
canvas));
canvasGrid.getChildren().forEach((child) ->
available.remove(getCanvasView(child)));
final MapCanvas canvas = createOrReuseCanvas(resource,
available.values());
@@ -458,9 +542,10 @@ final class MultiCanvas extends Widget implements
Observable {
boolean changed = false;
if (resource != null) {
resource.removeListener(CloseEvent.class, closer);
- for (final MapCanvas canvas : canvasPool.keySet()) {
+ for (final Map.Entry<MapCanvas, Controls> entry :
canvasPool.entrySet()) {
+ final MapCanvas canvas = entry.getKey();
if (getResource(canvas) == resource) {
- changed |= removeCanvasView(canvas);
+ changed |= removeCanvasView(canvas, entry.getValue());
}
}
if (changed) {
@@ -598,16 +683,16 @@ final class MultiCanvas extends Widget implements
Observable {
* Clears the content of the given map canvas.
* It is better to remove the canvas from {@link #canvasGrid} before to
invoke this method.
*
- * @param canvas the map canvas to clear.
+ * @param canvas the map canvas to clear.
+ * @param controls value of {@code canvasPool.get(canvas)} (not
necessarily obtained by that call).
*/
- private void clear(final MapCanvas canvas) {
+ private void clear(final MapCanvas canvas, Controls controls) {
final Resource resource = getResource(canvas);
if (resource != null) {
resource.removeListener(CloseEvent.class, closer);
}
canvas.clearLater();
- final Controls controls = canvasPool.get(canvas); // Should
never be null, but let be safe.
- if (controls != null) controls.title.setText(null);
+ controls.clear();
}
/**
@@ -616,7 +701,7 @@ final class MultiCanvas extends Widget implements
Observable {
*/
final void dispose() {
canvasGrid.getChildren().clear();
- canvasPool.keySet().forEach(this::clear);
+ canvasPool.forEach(this::clear);
canvasPool.clear();
}
@@ -641,4 +726,47 @@ final class MultiCanvas extends Widget implements
Observable {
});
}
}
+
+ /**
+ * Returns a string representation of the state of this {@code
MultiCanvas} for debugging purposes.
+ * The returned string representation contains a consistency check in the
last column.
+ *
+ * @return a string representation of the state of this {@code
MultiCanvas}.
+ */
+ @Override
+ public String toString() {
+ final Set<Region> views = getCanvasViews(canvasGrid.getChildren());
+ final var table = new TableAppender(" │ ");
+ table.appendHorizontalSeparator();
+ table.append("Title").nextColumn();
+ table.append("Shown").nextColumn();
+ table.append("Error").appendHorizontalSeparator();
+ for (final Map.Entry<MapCanvas, Controls> entry :
canvasPool.entrySet()) {
+ final MapCanvas canvas = entry.getKey();
+ final Controls controls = entry.getValue();
+ table.append(controls.title.getText()).nextColumn();
+
table.append(Boolean.toString(views.remove(getView(canvas)))).nextColumn();
+ String error = "";
+ for (final GestureFollower follower : controls.followers) {
+ if (follower.source != canvas) {
+ error = "follower.source";
+ break;
+ }
+ @SuppressWarnings("element-type-mismatch")
+ final Controls target = canvasPool.get(follower.target);
+ if (target == null) {
+ error = "follower.target";
+ break;
+ }
+ }
+ table.append(error).nextLine();
+ }
+ for (final Region orphan : views) {
+ table.append(String.valueOf(orphan)).nextColumn();
+ table.append("true").nextColumn();
+ table.append("orphan").nextLine();
+ }
+ table.appendHorizontalSeparator();
+ return table.toString();
+ }
}
diff --git
a/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/map/RenderingTask.java
b/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/map/RenderingTask.java
index 1d3753513d..6cb9a6a1f2 100644
---
a/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/map/RenderingTask.java
+++
b/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/map/RenderingTask.java
@@ -17,11 +17,12 @@
package org.apache.sis.gui.map;
import javafx.concurrent.Task;
+import javafx.scene.transform.Affine;
+import javafx.scene.transform.Transform;
/**
* Base class of tasks executed in background thread for doing rendering.
- * This is currently used only for type safety.
*
* @author Martin Desruisseaux (Geomatys)
*
@@ -31,9 +32,33 @@ import javafx.concurrent.Task;
* @see MapCanvas#renderingCompleted(RenderingTask)
*/
abstract class RenderingTask<V> extends Task<V> {
+ /**
+ * The {@link MapCanvas#transform} values at the time the {@link
MapCanvas#repaint()} method has been invoked.
+ * This is a change applied on {@link MapCanvas#objectiveToDisplay} but
not yet visible in the map.
+ */
+ private Transform changeInProgress;
+
/**
* Creates a new rendering task.
*/
RenderingTask() {
}
+
+ /**
+ * Takes a copy of the given transform.
+ *
+ * @param transform value of {@link MapCanvas#transform}.
+ */
+ final void setChangeInProgress(final Affine transform) {
+ // TODO: select a simpler transform implementation when possible.
+ changeInProgress = new Affine(transform);
+ }
+
+ /**
+ * Returns the transform specified by the call to {@link
#setChangeInProgress(Affine)}.
+ * The returned value is not copied, caller should not modify it.
+ */
+ final Transform getChangeInProgress() {
+ return changeInProgress;
+ }
}