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 2f96f80 Improve StatusBar, make it public and make it useable with arbitrary MapCanvas.
2f96f80 is described below
commit 2f96f80af1f9e73aa0fd810e6167f2d3700337a4
Author: Martin Desruisseaux <martin.desruisseaux@geomatys.com>
AuthorDate: Mon Apr 13 12:58:08 2020 +0200
Improve StatusBar, make it public and make it useable with arbitrary MapCanvas.
---
.../org/apache/sis/gui/coverage/CoverageView.java | 104 +---
.../java/org/apache/sis/gui/coverage/GridView.java | 22 +-
.../org/apache/sis/gui/coverage/GridViewSkin.java | 26 +-
.../org/apache/sis/gui/coverage/ImageLoader.java | 2 +-
.../org/apache/sis/gui/coverage/ImageRequest.java | 17 +-
.../org/apache/sis/gui/coverage/StatusBar.java | 336 ------------
.../java/org/apache/sis/gui/map/MapCanvas.java | 85 +++-
.../java/org/apache/sis/gui/map/MapCanvasAWT.java | 12 +-
.../java/org/apache/sis/gui/map/StatusBar.java | 563 +++++++++++++++++++++
.../java/org/apache/sis/internal/gui/Styles.java | 5 +
10 files changed, 704 insertions(+), 468 deletions(-)
diff --git a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/CoverageView.java b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/CoverageView.java
index 2d558aa..ce404d5 100644
--- a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/CoverageView.java
+++ b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/CoverageView.java
@@ -21,7 +21,6 @@ import java.util.EnumMap;
import java.util.Objects;
import java.awt.Graphics2D;
import java.awt.geom.AffineTransform;
-import java.awt.geom.NoninvertibleTransformException;
import java.awt.image.RenderedImage;
import javafx.scene.paint.Color;
import javafx.scene.layout.Region;
@@ -32,7 +31,6 @@ import javafx.beans.value.ObservableValue;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.concurrent.Task;
-import javafx.scene.input.MouseEvent;
import org.opengis.geometry.Envelope;
import org.opengis.referencing.datum.PixelInCell;
import org.opengis.referencing.operation.TransformException;
@@ -40,14 +38,14 @@ import org.apache.sis.coverage.grid.GridCoverage;
import org.apache.sis.coverage.grid.GridExtent;
import org.apache.sis.coverage.grid.GridGeometry;
import org.apache.sis.internal.gui.ImageRenderings;
-import org.apache.sis.util.collection.BackingStoreException;
import org.apache.sis.referencing.operation.matrix.AffineTransforms2D;
import org.apache.sis.referencing.operation.transform.MathTransforms;
import org.apache.sis.gui.map.MapCanvasAWT;
+import org.apache.sis.gui.map.StatusBar;
/**
- * Shows a {@link RenderedImage} produced by a {@link GridCoverage}.
+ * A canvas for {@link RenderedImage} produced by a {@link GridCoverage}.
*
* @author Martin Desruisseaux (Geomatys)
* @version 1.1
@@ -56,8 +54,8 @@ import org.apache.sis.gui.map.MapCanvasAWT;
*/
final class CoverageView extends MapCanvasAWT {
/**
- * The data shown in this view. Note that setting this property to a non-null value may not
- * modify the view content immediately. Instead, a background process will request the tiles.
+ * The data shown in this canvas. Note that setting this property to a non-null value may not
+ * modify the canvas content immediately. Instead, a background process will request the tiles.
*
* <p>Current implementation is restricted to {@link GridCoverage} instances, but a future
* implementation may generalize to {@link org.opengis.coverage.Coverage} instances.</p>
@@ -69,7 +67,7 @@ final class CoverageView extends MapCanvasAWT {
/**
* A subspace of the grid coverage extent where all dimensions except two have a size of 1 cell.
- * May be {@code null} if this grid coverage has only two dimensions with a size greater than 1 cell.
+ * May be {@code null} if the grid coverage has only two dimensions with a size greater than 1 cell.
*
* @see #getSliceExtent()
* @see #setSliceExtent(GridExtent)
@@ -92,10 +90,10 @@ final class CoverageView extends MapCanvasAWT {
private RangeType currentDataAlternative;
/**
- * The data to shown, or {@code null} if not yet specified. This image may be tiled,
+ * The data to show, or {@code null} if not yet specified. This image may be tiled,
* and fetching tiles may require computations to be performed in background thread.
- * The size of this image is not necessarily {@link #buffer} or {@link #image} size.
- * In particular this image way cover a larger area.
+ * The size of this {@code RenderedImage} is not necessarily the {@link #image} size.
+ * In particular {@code data} way cover a larger area.
*/
private RenderedImage data;
@@ -105,26 +103,12 @@ final class CoverageView extends MapCanvasAWT {
*/
private AffineTransform gridToCRS;
- /*
- * The transform from {@link #data} pixel coordinates to pixel coordinates in the widget.
- * This is the concatenation of {@link #gridToCRS} followed by {@link #objectiveToDisplay}.
- * This transform is replaced when the zoom changes or when the viewed area is translated.
- * We create a new transform each time (we do not modify the existing instance) because it
- * may be used by a background thread.
- */
- private AffineTransform gridToDisplay;
-
/**
* The image together with the status bar.
*/
private final BorderPane imageAndStatus;
/**
- * The bar where to format the coordinates below mouse cursor.
- */
- private final StatusBar statusBar;
-
- /**
* Creates a new two-dimensional canvas for {@link RenderedImage}.
*/
public CoverageView() {
@@ -133,14 +117,12 @@ final class CoverageView extends MapCanvasAWT {
sliceExtentProperty = new SimpleObjectProperty<>(this, "sliceExtent");
dataAlternatives = new EnumMap<>(RangeType.class);
currentDataAlternative = RangeType.DECLARED;
- statusBar = new StatusBar(this::toImageCoordinates);
imageAndStatus = new BorderPane(fixedPane);
- imageAndStatus.setBottom(statusBar);
coverageProperty .addListener(this::onImageSpecified);
sliceExtentProperty.addListener(this::onImageSpecified);
- floatingPane.setOnMouseMoved(this::onMouveMoved);
- floatingPane.setOnMouseEntered(statusBar);
- floatingPane.setOnMouseExited (statusBar);
+ final StatusBar statusBar = new StatusBar();
+ statusBar.setCanvas(this);
+ imageAndStatus.setBottom(statusBar.getView());
}
/**
@@ -219,21 +201,6 @@ final class CoverageView extends MapCanvasAWT {
}
/**
- * Starts a background task for loading data, computing slice or rendering the data in a {@link CoverageView}.
- *
- * <p>Tasks need to be careful to not use any {@link CoverageView} field in their {@link Task#call()}
- * method (needed fields shall be copied in the JavaFX thread before the background thread is started).
- * But {@link Task#succeeded()} and similar methods can read and write those fields.</p>
- */
- @Override
- protected final void execute(final Task<?> task) {
- statusBar.setErrorMessage(null);
- task.runningProperty().addListener(statusBar::setRunningState);
- task.setOnFailed((e) -> errorOccurred(e.getSource().getException()));
- super.execute(task);
- }
-
- /**
* Invoked when a new coverage has been specified or when the slice extent changed.
*
* @param property the {@link #coverageProperty} or {@link #sliceExtentProperty} (ignored).
@@ -326,12 +293,11 @@ final class CoverageView extends MapCanvasAWT {
*
* @param image the image to load.
* @param geometry the grid geometry of the coverage that produced the image.
- * @param sliceExtent the extent that were requested.
+ * @param sliceExtent the extent that was requested.
*/
private void setImage(final RenderedImage image, final GridGeometry geometry, final GridExtent sliceExtent) {
setImage(RangeType.DECLARED, image);
setRangeType(currentDataAlternative);
- statusBar.setCoordinateConversion(geometry, sliceExtent);
try {
gridToCRS = AffineTransforms2D.castOrCopy(geometry.getGridToCRS(PixelInCell.CELL_CENTER));
} catch (RuntimeException e) { // Conversion not defined or not affine.
@@ -357,7 +323,7 @@ final class CoverageView extends MapCanvasAWT {
* The rendering will be done later by a call to {@link Renderer#paint(Graphics2D)}.
*/
@Override
- protected Renderer createRenderer(){
+ protected Renderer createRenderer() {
final RenderedImage data = this.data; // Need to copy this reference here before background tasks.
if (data == null) {
return null;
@@ -368,52 +334,14 @@ final class CoverageView extends MapCanvasAWT {
* vary at any time, and also because we need a new `AffineTransform` instance anyway (we can not reuse
* an existing instance, because it needs to be stable for use by the background thread).
*/
- final AffineTransform tr = new AffineTransform(objectiveToDisplay);
+ final AffineTransform gridToDisplay = new AffineTransform(objectiveToDisplay);
if (gridToCRS != null) {
- tr.concatenate(gridToCRS);
+ gridToDisplay.concatenate(gridToCRS);
}
- gridToDisplay = tr;
return new Renderer() {
@Override protected void paint(final Graphics2D gr) {
- gr.drawRenderedImage(data, tr);
+ gr.drawRenderedImage(data, gridToDisplay);
}
};
}
-
- /**
- * Invoked when an error occurred.
- *
- * @param ex the exception that occurred.
- *
- * @todo Should provide a button for getting more details.
- */
- @Override
- protected void errorOccurred(final Throwable ex) {
- String message = ex.getMessage();
- if (message == null) {
- message = ex.toString();
- }
- statusBar.setErrorMessage(message);
- }
-
- /**
- * Invoked when the mouse moved. This method update the coordinates below mouse cursor.
- */
- private void onMouveMoved(final MouseEvent event) {
- statusBar.setCoordinates((int) Math.round(event.getX()),
- (int) Math.round(event.getY()));
- }
-
- /**
- * Converts pixel indices in the window to pixel indices in the image.
- */
- private boolean toImageCoordinates(final double[] indices) {
- if (gridToDisplay != null) try {
- gridToDisplay.inverseTransform(indices, 0, indices, 0, 1);
- return true;
- } catch (NoninvertibleTransformException e) {
- throw new BackingStoreException(e); // Will be unwrapped by the caller
- }
- return false;
- }
}
diff --git a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/GridView.java b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/GridView.java
index 18e9038..fc6fc6e 100644
--- a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/GridView.java
+++ b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/GridView.java
@@ -40,6 +40,7 @@ import javafx.scene.paint.Paint;
import org.apache.sis.util.ArgumentChecks;
import org.apache.sis.coverage.grid.GridCoverage;
import org.apache.sis.internal.gui.BackgroundThreads;
+import org.apache.sis.internal.gui.Styles;
/**
@@ -387,7 +388,7 @@ public class GridView extends Control {
* we take a value close to the vertical scrollbar width as a safety.
*/
final double w = getSizeValue(cellWidth);
- return width * w + getSizeValue(headerWidth) + Math.max(w, GridViewSkin.SCROLLBAR_WIDTH);
+ return width * w + getSizeValue(headerWidth) + Math.max(w, Styles.SCROLLBAR_WIDTH);
}
/**
@@ -411,6 +412,14 @@ public class GridView extends Control {
}
/**
+ * Returns the offset to add for converting cell indices to pixel indices. They are often the same indices,
+ * but may differ if the {@link RenderedImage} uses a coordinate system where coordinates of the upper-left
+ * corner is not (0,0).
+ */
+ final int getImageMinX() {return minX;}
+ final int getImageMinY() {return minY;}
+
+ /**
* Returns the bounds of a single tile in the image. This method is invoked only
* if an error occurred during {@link RenderedImage#getTile(int, int)} invocation.
* The returned bounds are zero-based (may not be the bounds in image coordinates).
@@ -515,17 +524,6 @@ public class GridView extends Control {
}
/**
- * Converts cell indices to pixel indices. They are often the same indices, but may differ if the
- * {@link RenderedImage} uses a coordinate system where the coordinates of the upper-left corner
- * is not (0,0).
- */
- final boolean toImageCoordinates(final double[] indices) {
- indices[0] += minX;
- indices[1] += minY;
- return true;
- }
-
- /**
* Creates a new instance of the skin responsible for rendering this grid view.
* From the perspective of this {@link Control}, the {@link Skin} is a black box.
* It listens and responds to changes in state of this grid view. This method is
diff --git a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/GridViewSkin.java b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/GridViewSkin.java
index 1e2b46d..7b80ede 100644
--- a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/GridViewSkin.java
+++ b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/GridViewSkin.java
@@ -33,6 +33,7 @@ import javafx.scene.text.FontWeight;
import javafx.scene.text.Font;
import javafx.scene.input.MouseEvent;
import javafx.event.EventHandler;
+import org.apache.sis.gui.map.StatusBar;
import org.apache.sis.internal.gui.Styles;
@@ -56,11 +57,6 @@ import org.apache.sis.internal.gui.Styles;
*/
final class GridViewSkin extends VirtualContainerBase<GridView, GridRow> implements EventHandler<MouseEvent> {
/**
- * Approximate size of vertical scroll bar.
- */
- static final int SCROLLBAR_WIDTH = 20;
-
- /**
* The cells that we put in the header row on the top of the view. The children list is initially empty;
* new elements are added or removed when first needed and when the view size changed.
*/
@@ -183,13 +179,14 @@ final class GridViewSkin extends VirtualContainerBase<GridView, GridRow> impleme
* The status bar where to show coordinates of selected cell.
* Mouse exit event is handled by `hideSelection(…)`.
*/
- statusBar = new StatusBar(view::toImageCoordinates);
+ statusBar = new StatusBar();
flow.setOnMouseEntered(statusBar);
/*
* The list of children is initially empty. We need to
* add the virtual flow, otherwise nothing will appear.
*/
- getChildren().addAll(topBackground, leftBackground, selectedColumn, selectedRow, headerRow, selection, statusBar, flow);
+ getChildren().addAll(topBackground, leftBackground, selectedColumn, selectedRow,
+ headerRow, selection, statusBar.getView(), flow);
}
/**
@@ -216,13 +213,18 @@ final class GridViewSkin extends VirtualContainerBase<GridView, GridRow> impleme
selection.relocate(x, y);
selectedRow.setY(y);
selectedColumn.setX(x);
- statusBar.setCoordinates(((int) visibleColumn) + firstVisibleColumn, row.getIndex());
+ final GridView view = getSkinnable();
+ statusBar.setLocalCoordinates(view.getImageMinX() + ((int) visibleColumn) + firstVisibleColumn,
+ view.getImageMinY() + row.getIndex());
}
}
}
selection .setVisible(visible);
selectedRow .setVisible(visible);
selectedColumn.setVisible(visible);
+ if (!visible) {
+ statusBar.handle(null);
+ }
}
/**
@@ -440,7 +442,7 @@ final class GridViewSkin extends VirtualContainerBase<GridView, GridRow> impleme
final Flow flow = (Flow) getVirtualFlow();
final double cellHeight = flow.getFixedCellSize();
final double headerHeight = cellHeight + 2*cellSpacing;
- final double statusHeight = statusBar.getHeight();
+ final double statusHeight = statusBar.getView().getHeight();
final double dataY = y + headerHeight;
final double dataHeight = height - headerHeight - statusHeight;
layoutAll |= (flow.getWidth() != width) || (flow.getHeight() != dataHeight);
@@ -487,8 +489,10 @@ final class GridViewSkin extends VirtualContainerBase<GridView, GridRow> impleme
* may need to be added or removed).
*/
if (layoutAll || oldPos != leftPosition) {
- layoutInArea(headerRow, x, y, width, headerHeight, Node.BASELINE_OFFSET_SAME_AS_HEIGHT, HPos.LEFT, VPos.TOP);
- layoutInArea(statusBar, x, height - statusHeight, width, statusHeight, Node.BASELINE_OFFSET_SAME_AS_HEIGHT, HPos.RIGHT, VPos.BOTTOM);
+ layoutInArea(headerRow, x, y, width, headerHeight,
+ Node.BASELINE_OFFSET_SAME_AS_HEIGHT, HPos.LEFT, VPos.TOP);
+ layoutInArea(statusBar.getView(), x, height - statusHeight, width, statusHeight,
+ Node.BASELINE_OFFSET_SAME_AS_HEIGHT, HPos.RIGHT, VPos.BOTTOM);
final ObservableList<Node> children = headerRow.getChildren();
final int count = children.size();
final int missing = (int) Math.ceil((width - headerWidth) / cellWidth) - count;
diff --git a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/ImageLoader.java b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/ImageLoader.java
index 7ea130c..b6faa39 100644
--- a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/ImageLoader.java
+++ b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/ImageLoader.java
@@ -43,7 +43,7 @@ final class ImageLoader extends Task<RenderedImage> {
/**
* The {@value} value, for identifying code that assume two-dimensional objects.
*/
- public static final int BIDIMENSIONAL = 2;
+ private static final int BIDIMENSIONAL = 2;
/**
* The image source together with optional parameters for reading only a subset.
diff --git a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/ImageRequest.java b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/ImageRequest.java
index e1fae05..9431c33 100644
--- a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/ImageRequest.java
+++ b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/ImageRequest.java
@@ -25,6 +25,8 @@ import org.apache.sis.coverage.grid.GridCoverage;
import org.apache.sis.coverage.grid.GridGeometry;
import org.apache.sis.coverage.grid.GridExtent;
import org.apache.sis.util.ArgumentChecks;
+import org.apache.sis.gui.map.StatusBar;
+import org.apache.sis.referencing.operation.transform.MathTransforms;
/**
@@ -254,6 +256,19 @@ public class ImageRequest {
*/
final void configure(final StatusBar bar) {
final GridCoverage cv = coverage;
- bar.setCoordinateConversion(cv != null ? cv.getGridGeometry() : null, sliceExtent);
+ final GridExtent request = sliceExtent;
+ bar.applyCanvasGeometry(cv != null ? cv.getGridGeometry() : null);
+ /*
+ * By `GridCoverage.render(GridExtent)` contract, the `RenderedImage` pixel coordinates are relative
+ * to the requested `GridExtent`. Consequently we need to translate the image coordinates so that it
+ * become the coordinates of the original `GridGeometry` before to apply `gridToCRS`.
+ */
+ if (request != null) {
+ final double[] origin = new double[request.getDimension()];
+ for (int i=0; i<origin.length; i++) {
+ origin[i] = request.getLow(i);
+ }
+ bar.setLocalToCRS(MathTransforms.concatenate(MathTransforms.translation(origin), bar.getLocalToCRS()));
+ }
}
}
diff --git a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/StatusBar.java b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/StatusBar.java
deleted file mode 100644
index 4a00b6b..0000000
--- a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/StatusBar.java
+++ /dev/null
@@ -1,336 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements. See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package org.apache.sis.gui.coverage;
-
-import java.util.Locale;
-import java.util.function.Predicate;
-import javax.measure.Unit;
-import javafx.geometry.Insets;
-import javafx.scene.paint.Color;
-import javafx.scene.layout.HBox;
-import javafx.scene.layout.Priority;
-import javafx.scene.control.Label;
-import javafx.scene.control.Tooltip;
-import javafx.scene.control.ProgressBar;
-import javafx.scene.input.MouseEvent;
-import javafx.event.EventHandler;
-import javafx.beans.value.ObservableValue;
-import org.opengis.referencing.datum.PixelInCell;
-import org.opengis.referencing.cs.CoordinateSystem;
-import org.opengis.referencing.operation.Matrix;
-import org.opengis.referencing.operation.MathTransform;
-import org.opengis.referencing.operation.TransformException;
-import org.opengis.referencing.crs.CoordinateReferenceSystem;
-import org.apache.sis.referencing.operation.transform.MathTransforms;
-import org.apache.sis.referencing.IdentifiedObjects;
-import org.apache.sis.geometry.GeneralDirectPosition;
-import org.apache.sis.geometry.CoordinateFormat;
-import org.apache.sis.coverage.grid.GridExtent;
-import org.apache.sis.coverage.grid.GridGeometry;
-import org.apache.sis.measure.Units;
-import org.apache.sis.util.Classes;
-import org.apache.sis.util.Exceptions;
-
-
-/**
- * A status bar showing coordinates of a grid cell.
- * The number of fraction digits is adjusted according pixel resolution for each coordinate to format.
- *
- * <p>Callers can register this object to {@link javafx.scene.Node#setOnMouseEntered(EventHandler)} and
- * {@link javafx.scene.Node#setOnMouseExited(EventHandler)} for showing or hiding the coordinate values
- * when the mouse enters or exits the region of interest.</p>
- *
- * @author Martin Desruisseaux (Geomatys)
- * @version 1.1
- * @since 1.1
- * @module
- */
-final class StatusBar extends HBox implements EventHandler<MouseEvent> {
- /**
- * Some spaces to add around the status bar.
- */
- private static final Insets PADDING = new Insets(5, GridViewSkin.SCROLLBAR_WIDTH, 6, 0);
-
- /**
- * The progress bar, hidden by default. This bar is initialized to undetermined state.
- */
- private final ProgressBar progress;
-
- /**
- * Message to write in the middle of the status bar.
- * This component usually has nothing to show; it is used mostly for error messages.
- * It takes all the space between {@link #progress} and {@link #coordinates}.
- */
- private final Label message;
-
- /**
- * A function which take cell indices in input and gives pixel coordinates in output.
- * The input and output coordinates are in the given array, which is updated in-place.
- * This method returns {@code false} if it can not compute the coordinates.
- *
- * @see GridView#toImageCoordinates(double[])
- */
- private final Predicate<double[]> toImageCoordinates;
-
- /**
- * Zero-based cell coordinates currently formatted in the {@link #coordinates} field.
- * This is used for detecting if coordinate values changed since last formatting.
- */
- private int column, row;
-
- /**
- * Conversion from ({@linkplain #column},{@linkplain #row}) cell coordinates
- * to geographic or projected coordinates.
- */
- private MathTransform gridToCRS;
-
- /**
- * The source cell indices before conversion to geospatial coordinates.
- * The number of dimensions should be 2.
- */
- private double[] sourceCoordinates;
-
- /**
- * Coordinates after conversion to the CRS. The number of dimensions depends on
- * the target CRS. This object is reused during each coordinate transformation.
- */
- private GeneralDirectPosition targetCoordinates;
-
- /**
- * The desired precisions for each dimension in the {@link #targetCoordinates} to format.
- * It may vary for each position if the {@link #gridToCRS} transform is non-linear.
- */
- private double[] precisions;
-
- /**
- * A multiplication factory slightly greater than 1 applied on {@link #precisions}.
- * The intent is to avoid that a precision like 0.09999 is interpreted as requiring
- * two decimal digits instead of 1. For avoiding that, we add a small value to the
- * precision: <var>precision</var> += <var>precision</var> × ε, which we compact as
- * <var>precision</var> *= (1 + ε). The ε value is chosen to represent an increase
- * of no more than 0.5 pixel between the lower and upper indices of the grid.
- */
- private double[] inflatePrecisions;
-
- /**
- * The object to use for formatting coordinate values.
- */
- private final CoordinateFormat format;
-
- /**
- * The labels where to format the coordinates.
- */
- private final Label coordinates;
-
- /**
- * Creates a new status bar.
- */
- StatusBar(final Predicate<double[]> toImageCoordinates) {
- this.toImageCoordinates = toImageCoordinates;
- format = new CoordinateFormat();
- coordinates = new Label();
- message = new Label();
- progress = new ProgressBar();
- progress.setVisible(false);
- message.setTextFill(Color.RED);
- message.setMaxWidth(Double.POSITIVE_INFINITY);
- HBox.setHgrow(message, Priority.ALWAYS);
- getChildren().setAll(progress, message, coordinates);
- setPadding(PADDING);
- setSpacing(12);
- /*
- * Following method call will initialize `gridtoCRS` to default value.
- */
- setCoordinateConversion(null, null);
- }
-
- /**
- * Sets the conversion from (column, row) cell indices to geographic or projected coordinates.
- * The conversion is computed from the given grid geometry.
- *
- * @param geometry geometry of the grid coverage shown in {@link GridView}, or {@code null}.
- * @param request sub-region of the coverage which is shown, or {@code null} for the full coverage.
- */
- final void setCoordinateConversion(final GridGeometry geometry, GridExtent request) {
- gridToCRS = MathTransforms.identity(2);
- precisions = null;
- CoordinateReferenceSystem crs = null;
- double resolution = 1;
- Unit<?> unit = Units.PIXEL;
- if (geometry != null) {
- if (geometry.isDefined(GridGeometry.GRID_TO_CRS)) {
- gridToCRS = geometry.getGridToCRS(PixelInCell.CELL_CENTER);
- if (geometry.isDefined(GridGeometry.CRS)) {
- crs = geometry.getCoordinateReferenceSystem();
- }
- }
- if (request == null && geometry.isDefined(GridGeometry.EXTENT)) {
- request = geometry.getExtent();
- }
- /*
- * Computes the precision of coordinates to format. We use the finest resolution,
- * looking only at axes having the same units of measurement than the first axis.
- * This will be used as a fallback if we can not compute the precision specific
- * to a coordinate, for example if we can not compute the derivative.
- */
- if (geometry.isDefined(GridGeometry.RESOLUTION)) {
- double[] resolutions = geometry.getResolution(true);
- if (crs != null && resolutions.length != 0) {
- final CoordinateSystem cs = crs.getCoordinateSystem();
- unit = cs.getAxis(0).getUnit();
- for (int i=0; i<resolutions.length; i++) {
- if (unit.equals(cs.getAxis(i).getUnit())) {
- final double r = resolutions[i];
- if (r < resolution) resolution = r;
- }
- }
- }
- }
- }
- /*
- * By `GridCoverage.render(GridExtent)` contract, the `RenderedImage` pixel coordinates are relative
- * to the requested `GridExtent`. Consequently we need to translate the image coordinates so that it
- * become the coordinates of the original `GridGeometry` before to apply `gridToCRS`.
- */
- if (request != null) {
- final int n = request.getDimension();
- if (inflatePrecisions == null || inflatePrecisions.length != n) {
- inflatePrecisions = new double[n];
- }
- final double[] origin = new double[n];
- for (int i=0; i<n; i++) {
- origin[i] = request.getLow(i);
- inflatePrecisions[i] = (0.5 / request.getSize(i)) + 1;
- }
- gridToCRS = MathTransforms.concatenate(MathTransforms.translation(origin), gridToCRS);
- } else {
- inflatePrecisions = null;
- }
- /*
- * Prepare objects to be reused for each coordinate transformation.
- * Configure the CoordinateFormat with the CRS.
- */
- if (gridToCRS != null) {
- sourceCoordinates = new double[Math.max(gridToCRS.getSourceDimensions(), ImageLoader.BIDIMENSIONAL)];
- targetCoordinates = new GeneralDirectPosition(gridToCRS.getTargetDimensions());
- } else {
- targetCoordinates = new GeneralDirectPosition(ImageLoader.BIDIMENSIONAL);
- sourceCoordinates = targetCoordinates.coordinates;
- }
- format.setDefaultCRS(crs);
- format.setPrecision(resolution, unit);
- Tooltip tp = null;
- if (crs != null) {
- tp = new Tooltip(IdentifiedObjects.getDisplayName(crs, format.getLocale(Locale.Category.DISPLAY)));
- }
- coordinates.setTooltip(tp);
- }
-
- /**
- * Sets the pixel coordinates to show. Those pixel coordinates will be automatically
- * transformed to geographic coordinates if a "grid to CRS" conversion is available.
- */
- final void setCoordinates(final int x, final int y) {
- if (x != column || y != row) {
- sourceCoordinates[0] = column = x;
- sourceCoordinates[1] = row = y;
- String text = null;
- try {
- if (toImageCoordinates.test(sourceCoordinates)) {
- Matrix derivative;
- try {
- derivative = MathTransforms.derivativeAndTransform(gridToCRS,
- sourceCoordinates, 0, targetCoordinates.coordinates, 0);
- } catch (TransformException ignore) {
- /*
- * If above operation failed, it may be because the MathTransform does not support
- * derivative calculation. Try again without derivative (the precision will be set
- * to the default resolution computed in `setCoordinateConversion(…)`).
- */
- gridToCRS.transform(sourceCoordinates, 0, targetCoordinates.coordinates, 0, 1);
- derivative = null;
- }
- if (derivative == null) {
- precisions = null;
- } else {
- if (precisions == null) {
- precisions = new double[targetCoordinates.getDimension()];
- }
- /*
- * Estimate the precision by looking at the maximal displacement in the CRS caused by
- * a displacement of one cell (i.e. when moving by row or one column). We search for
- * maximal displacement instead than minimal because we expect the displacement to be
- * zero along some axes (e.g. one row down does not change longitude value in a Plate
- * Carrée projection).
- */
- for (int j=derivative.getNumRow(); --j >= 0;) {
- double p = 0;
- for (int i=derivative.getNumCol(); --i >= 0;) {
- double e = Math.abs(derivative.getElement(j, i));
- if (inflatePrecisions != null) {
- e *= inflatePrecisions[i];
- }
- if (e > p) p = e;
- }
- precisions[j] = p;
- }
- }
- format.setPrecisions(precisions);
- text = format.format(targetCoordinates);
- }
- } catch (TransformException | RuntimeException e) {
- /*
- * If even the fallback without derivative failed, show the error message.
- */
- Throwable cause = Exceptions.unwrap(e);
- text = cause.getLocalizedMessage();
- if (text == null) {
- text = Classes.getShortClassName(cause);
- }
- }
- coordinates.setText(text);
- }
- }
-
- /**
- * Shows or hide the coordinates depending on whether the mouse entered or exited
- * the region for which this status bar is providing information.
- *
- * <p>For the convenience of {@link GridViewSkin}, a null value is equivalent to
- * a mouse exit event.</p>
- */
- @Override
- public final void handle(final MouseEvent event) {
- coordinates.setVisible(event != null && event.getEventType() != MouseEvent.MOUSE_EXITED);
- }
-
- /**
- * Invoked when the {@link javafx.concurrent.Worker#runningProperty()} state of a task changed.
- * This method shows or hides the progress bar according the {@code newValue} argument.
- */
- final void setRunningState(ObservableValue<? extends Boolean> property, Boolean oldValue, Boolean newValue) {
- progress.setVisible(newValue);
- }
-
- /**
- * Invoked when an error occurred, or for clearing the error message.
- */
- final void setErrorMessage(final String text) {
- message.setVisible(text != null);
- message.setText(text);
- }
-}
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 630d91b..10ce967 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
@@ -17,6 +17,7 @@
package org.apache.sis.gui.map;
import java.util.Locale;
+import java.util.Objects;
import java.awt.geom.AffineTransform;
import javafx.geometry.Bounds;
import javafx.geometry.Point2D;
@@ -31,11 +32,16 @@ import javafx.scene.input.GestureEvent;
import javafx.scene.Cursor;
import javafx.event.EventType;
import javafx.beans.Observable;
+import javafx.beans.property.ReadOnlyBooleanProperty;
+import javafx.beans.property.ReadOnlyBooleanWrapper;
+import javafx.beans.property.ReadOnlyObjectProperty;
+import javafx.beans.property.ReadOnlyObjectWrapper;
import javafx.concurrent.Task;
import javafx.scene.shape.Rectangle;
import javafx.scene.transform.Affine;
import javafx.scene.transform.NonInvertibleTransformException;
import org.opengis.geometry.Envelope;
+import org.opengis.referencing.crs.CoordinateReferenceSystem;
import org.apache.sis.referencing.operation.matrix.Matrices;
import org.apache.sis.referencing.operation.matrix.MatrixSIS;
import org.apache.sis.referencing.operation.transform.MathTransforms;
@@ -46,7 +52,6 @@ import org.apache.sis.util.ArgumentChecks;
import org.apache.sis.util.logging.Logging;
import org.apache.sis.internal.util.Numerics;
import org.apache.sis.internal.gui.BackgroundThreads;
-import org.apache.sis.internal.gui.ExceptionReporter;
import org.apache.sis.internal.map.PlanarCanvas;
import org.apache.sis.internal.map.RenderException;
import org.apache.sis.internal.system.Modules;
@@ -217,6 +222,23 @@ public abstract class MapCanvas extends PlanarCanvas {
private double xPanStart, yPanStart;
/**
+ * Whether a rendering is in progress. This property is set to {@code true} when {@code MapCanvas}
+ * is about to start a background thread for performing a rendering, and is reset to {@code false}
+ * after the {@code MapCanvas} has been updated with new rendering result.
+ *
+ * @see #renderingProperty()
+ */
+ private final ReadOnlyBooleanWrapper isRendering;
+
+ /**
+ * The exception or error that occurred during last rendering operation.
+ * This is reset to {@code null} when a rendering operation completes successfully.
+ *
+ * @see #errorProperty()
+ */
+ private final ReadOnlyObjectWrapper<Throwable> error;
+
+ /**
* Creates a new canvas for JavaFX application.
*
* @param locale the locale to use for labels and some messages, or {@code null} for default.
@@ -257,6 +279,8 @@ public abstract class MapCanvas extends PlanarCanvas {
clip.widthProperty() .bind(fixedPane.widthProperty());
clip.heightProperty().bind(fixedPane.heightProperty());
fixedPane.setClip(clip);
+ isRendering = new ReadOnlyBooleanWrapper(this, "isRendering");
+ error = new ReadOnlyObjectWrapper<>(this, "exception");
}
/**
@@ -446,6 +470,7 @@ public abstract class MapCanvas extends PlanarCanvas {
/**
* Resets the map view to its default zoom level and default position with no rotation.
+ * Contrarily to {@link #clear()}, this method does not remove the map content.
*/
public void reset() {
invalidObjectiveToDisplay = true;
@@ -638,14 +663,18 @@ public abstract class MapCanvas extends PlanarCanvas {
if (invalidObjectiveToDisplay) {
invalidObjectiveToDisplay = false;
LinearTransform tr;
+ final CoordinateReferenceSystem crs;
if (objectiveBounds != null) {
+ crs = objectiveBounds.getCoordinateReferenceSystem();
final Envelope2D target = getDisplayBounds();
final MatrixSIS m = Matrices.createTransform(objectiveBounds, target);
Matrices.forceUniformScale(m, 0, new double[] {target.width / 2, target.height / 2});
tr = MathTransforms.linear(m);
} else {
tr = MathTransforms.identity(BIDIMENSIONAL);
+ crs = null;
}
+ setObjectiveCRS(crs);
setObjectiveToDisplay(tr);
transform.setToIdentity();
}
@@ -661,6 +690,7 @@ public abstract class MapCanvas extends PlanarCanvas {
assert changeInProgress.isIdentity() : changeInProgress;
changeInProgress.setToTransform(transform);
transformOnNewImage.setToIdentity();
+ isRendering.set(true);
if (!transform.isIdentity()) {
transformDisplayCoordinates(new AffineTransform(
transform.getMxx(), transform.getMyx(),
@@ -674,13 +704,16 @@ public abstract class MapCanvas extends PlanarCanvas {
final Renderer context = createRenderer();
if (context != null && context.initialize(floatingPane)) {
executeRendering(createWorker(context));
+ } else {
+ error.set(null);
+ isRendering.set(false);
}
}
/**
* Creates the background task which will invoke {@link Renderer#render()} in a background thread.
- * The tasks must invoke {@link #renderingCompleted()} in JavaFX thread after completion, either
- * successful or not.
+ * The tasks must invoke {@link #renderingCompleted(Task)} in JavaFX thread after completion,
+ * either successful or not.
*/
Task<?> createWorker(final Renderer renderer) {
return new Task<Void>() {
@@ -693,15 +726,15 @@ public abstract class MapCanvas extends PlanarCanvas {
/** Invoked in JavaFX thread on success. */
@Override protected void succeeded() {
final boolean done = renderer.commit();
- renderingCompleted();
+ renderingCompleted(this);
if (!done || contentsChanged()) {
repaint();
}
}
/** Invoked in JavaFX thread on failure. */
- @Override protected void failed() {renderingCompleted();}
- @Override protected void cancelled() {renderingCompleted();}
+ @Override protected void failed() {renderingCompleted(this);}
+ @Override protected void cancelled() {renderingCompleted(this);}
};
}
@@ -711,7 +744,7 @@ public abstract class MapCanvas extends PlanarCanvas {
* which is now integrated in the map. That transform will be removed from {@link #floatingPane} transforms.
* It may be identity if no zoom, rotation or pan gesture has been applied since last rendering.
*/
- final void renderingCompleted() {
+ final void renderingCompleted(final Task<?> task) {
renderingInProgress = null;
floatingPane.setCursor(Cursor.CROSSHAIR);
final Point2D p = changeInProgress.transform(xPanStart, yPanStart);
@@ -719,6 +752,8 @@ public abstract class MapCanvas extends PlanarCanvas {
yPanStart = p.getY();
changeInProgress.setToIdentity();
transform.setToTransform(transformOnNewImage);
+ error.set(task.getException());
+ isRendering.set(false);
}
/**
@@ -782,13 +817,35 @@ public abstract class MapCanvas extends PlanarCanvas {
}
/**
- * Invoked when an error occurred. The default implementation popups a dialog box.
- * Subclasses may override. For example the error messages could be written in a status bar instead.
+ * Returns a property telling whether a rendering is in progress. This property become {@code true}
+ * when this {@code MapCanvas} is about to start a background thread for performing a rendering, and
+ * is reset to {@code false} after this {@code MapCanvas} has been updated with new rendering result.
+ *
+ * @return a property telling whether a rendering is in progress.
+ */
+ public final ReadOnlyBooleanProperty renderingProperty() {
+ return isRendering.getReadOnlyProperty();
+ }
+
+ /**
+ * Returns a property giving the exception or error that occurred during last rendering operation.
+ * The property value is reset to {@code null} when a rendering operation completed successfully.
+ *
+ * @return a property giving the exception or error that occurred during last rendering operation.
+ */
+ public final ReadOnlyObjectProperty<Throwable> errorProperty() {
+ return error.getReadOnlyProperty();
+ }
+
+ /**
+ * Sets the error property to the given value. This method is provided for subclasses that perform
+ * processing outside the {@link Renderer}. It does not need to be invoked if the error occurred
+ * during the rendering process.
*
- * @param ex the exception that occurred.
+ * @param ex the exception that occurred (can not be null).
*/
protected void errorOccurred(final Throwable ex) {
- ExceptionReporter.show(null, null, ex);
+ error.set(Objects.requireNonNull(ex));
}
/**
@@ -799,8 +856,7 @@ public abstract class MapCanvas extends PlanarCanvas {
}
/**
- * Clears the map.
- * Invoking this method may help to release memory when the map is no longer shown.
+ * Removes map content and clears all properties of this canvas.
*
* @see #reset()
*/
@@ -808,5 +864,8 @@ public abstract class MapCanvas extends PlanarCanvas {
transform.setToIdentity();
changeInProgress.setToIdentity();
invalidObjectiveToDisplay = true;
+ objectiveBounds = null;
+ error.set(null);
+ isRendering.set(false);
}
}
diff --git a/application/sis-javafx/src/main/java/org/apache/sis/gui/map/MapCanvasAWT.java b/application/sis-javafx/src/main/java/org/apache/sis/gui/map/MapCanvasAWT.java
index 6ee621a..36a1059 100644
--- a/application/sis-javafx/src/main/java/org/apache/sis/gui/map/MapCanvasAWT.java
+++ b/application/sis-javafx/src/main/java/org/apache/sis/gui/map/MapCanvasAWT.java
@@ -315,14 +315,14 @@ public abstract class MapCanvasAWT extends MapCanvas {
bufferWrapper = wrapper;
bufferConfiguration = configuration;
final boolean done = renderer.commit();
- renderingCompleted();
+ renderingCompleted(this);
if (!done || contentsChanged()) {
repaint();
}
}
- @Override protected void failed() {renderingCompleted();}
- @Override protected void cancelled() {renderingCompleted();}
+ @Override protected void failed() {renderingCompleted(this);}
+ @Override protected void cancelled() {renderingCompleted(this);}
}
/**
@@ -434,14 +434,14 @@ public abstract class MapCanvasAWT extends MapCanvas {
drawTo.flush(); // Release native resources.
}
final boolean done = renderer.commit();
- renderingCompleted();
+ renderingCompleted(this);
if (!done || contentsLost || contentsChanged()) {
repaint();
}
}
- @Override protected void failed() {renderingCompleted();}
- @Override protected void cancelled() {renderingCompleted();}
+ @Override protected void failed() {renderingCompleted(this);}
+ @Override protected void cancelled() {renderingCompleted(this);}
}
/**
diff --git a/application/sis-javafx/src/main/java/org/apache/sis/gui/map/StatusBar.java b/application/sis-javafx/src/main/java/org/apache/sis/gui/map/StatusBar.java
new file mode 100644
index 0000000..4e7c0eb
--- /dev/null
+++ b/application/sis-javafx/src/main/java/org/apache/sis/gui/map/StatusBar.java
@@ -0,0 +1,563 @@
+/*
+ * 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.Locale;
+import java.util.Optional;
+import javax.measure.Unit;
+import javafx.geometry.Insets;
+import javafx.geometry.Point2D;
+import javafx.scene.paint.Color;
+import javafx.scene.layout.HBox;
+import javafx.scene.layout.Region;
+import javafx.scene.layout.Priority;
+import javafx.scene.control.Label;
+import javafx.scene.control.Button;
+import javafx.scene.control.Tooltip;
+import javafx.scene.control.ProgressBar;
+import javafx.scene.input.MouseEvent;
+import javafx.event.EventHandler;
+import javafx.event.EventType;
+import javafx.beans.value.ObservableValue;
+import javafx.beans.property.ObjectProperty;
+import javafx.beans.property.SimpleObjectProperty;
+import javafx.beans.value.ChangeListener;
+import org.opengis.geometry.MismatchedDimensionException;
+import org.opengis.referencing.datum.PixelInCell;
+import org.opengis.referencing.cs.CoordinateSystem;
+import org.opengis.referencing.operation.Matrix;
+import org.opengis.referencing.operation.MathTransform;
+import org.opengis.referencing.operation.TransformException;
+import org.opengis.referencing.crs.CoordinateReferenceSystem;
+import org.apache.sis.referencing.operation.transform.MathTransforms;
+import org.apache.sis.referencing.IdentifiedObjects;
+import org.apache.sis.geometry.GeneralDirectPosition;
+import org.apache.sis.geometry.CoordinateFormat;
+import org.apache.sis.coverage.grid.GridGeometry;
+import org.apache.sis.coverage.grid.GridExtent;
+import org.apache.sis.internal.map.RenderException;
+import org.apache.sis.internal.util.Strings;
+import org.apache.sis.measure.Units;
+import org.apache.sis.util.Classes;
+import org.apache.sis.util.Exceptions;
+import org.apache.sis.util.ArgumentChecks;
+import org.apache.sis.util.resources.Errors;
+import org.apache.sis.gui.Widget;
+import org.apache.sis.internal.gui.ExceptionReporter;
+import org.apache.sis.internal.gui.Resources;
+import org.apache.sis.internal.gui.Styles;
+
+
+/**
+ * A status bar showing geographic or projected coordinates under mouse cursor.
+ * The number of fraction digits is adjusted according pixel resolution for each coordinate to format.
+ * Other components such as progress bar or error message may also be shown.
+ *
+ * <p>Since the main {@code StatusBar} job is to listen to mouse events for updating coordinates,
+ * this class implements {@link EventHandler} directly. {@code StatusBar} can be registered as a listener
+ * using the following methods:</p>
+ *
+ * <ul>
+ * <li>{@link javafx.scene.Node#setOnMouseEntered(EventHandler)} for showing the coordinate values
+ * when the mouse enters the region of interest.</li>
+ * <li>{@link javafx.scene.Node#setOnMouseExited(EventHandler)} for hiding the coordinate values
+ * when the mouse exits the region of interest.</li>
+ * <li>{@link javafx.scene.Node#setOnMouseMoved(EventHandler)} for updating the coordinate values
+ * when the mouse moves inside the region of interest.</li>
+ * </ul>
+ *
+ * Alternatively users can omit some or all above listener registrations and invoke
+ * {@link #setLocalCoordinates(double, double)} explicitly instead.
+ *
+ * @author Martin Desruisseaux (Geomatys)
+ * @version 1.1
+ * @since 1.1
+ * @module
+ */
+public class StatusBar extends Widget implements EventHandler<MouseEvent> {
+ /**
+ * The {@value} value, for identifying code that assume two-dimensional objects.
+ */
+ private static final int BIDIMENSIONAL = 2;
+
+ /**
+ * Some spaces to add around the status bar.
+ */
+ private static final Insets PADDING = new Insets(5, Styles.SCROLLBAR_WIDTH, 6, 0);
+
+ /**
+ * The container of controls making the status bar.
+ */
+ private final HBox view;
+
+ /**
+ * The progress bar, hidden by default. This bar is initialized to undetermined state.
+ */
+ private final ProgressBar progress;
+
+ /**
+ * Message to write in the middle of the status bar.
+ * This component usually has nothing to show; it is used mostly for error messages.
+ * It takes all the space between {@link #progress} and {@link #coordinates}.
+ */
+ private final Label message;
+
+ /**
+ * Local coordinates currently formatted in the {@link #coordinates} field.
+ * This is used for detecting if coordinate values changed since last formatting.
+ * Those coordinates are often integer values.
+ */
+ private double lastX, lastY;
+
+ /**
+ * Conversion from local coordinates to geographic or projected coordinates.
+ * This conversion shall never be null but may be the identity transform.
+ */
+ private MathTransform localToCRS;
+
+ /**
+ * The source local indices before conversion to geospatial coordinates.
+ * The number of dimensions is often {@value #BIDIMENSIONAL}.
+ * Shall never be {@code null}.
+ */
+ private double[] sourceCoordinates;
+
+ /**
+ * Coordinates after conversion to the CRS. The number of dimensions depends on
+ * the target CRS. This object is reused during each coordinate transformation.
+ * Shall never be {@code null}.
+ */
+ private GeneralDirectPosition targetCoordinates;
+
+ /**
+ * The desired precisions for each dimension in the {@link #targetCoordinates} to format.
+ * It may vary for each position if the {@link #localToCRS} transform is non-linear.
+ * This array is initially {@code null} and created when first needed.
+ */
+ private double[] precisions;
+
+ /**
+ * A multiplication factory slightly greater than 1 applied on {@link #precisions}.
+ * The intent is to avoid that a precision like 0.09999 is interpreted as requiring
+ * two decimal digits instead of 1. For avoiding that, we add a small value to the
+ * precision: <var>precision</var> += <var>precision</var> × ε, which we compact as
+ * <var>precision</var> *= (1 + ε). The ε value is chosen to represent an increase
+ * of no more than 0.5 pixel between the lower and upper indices of the grid.
+ * This array may be {@code null} if it has not been computed.
+ */
+ private double[] inflatePrecisions;
+
+ /**
+ * The object to use for formatting coordinate values.
+ */
+ private final CoordinateFormat format;
+
+ /**
+ * The labels where to format the coordinates.
+ */
+ private final Label coordinates;
+
+ /**
+ * The canvas that this status bar is tracking.
+ * The property value is {@code null} if there is none.
+ *
+ * @see #getCanvas()
+ * @see #setCanvas(MapCanvas)
+ */
+ public final ObjectProperty<MapCanvas> canvasProperty;
+
+ /**
+ * The listener registered on {@link MapCanvas#renderingProperty()}.
+ * This reference is stored for allowed removal.
+ *
+ * @see #setCanvas(MapCanvas)
+ */
+ private ChangeListener<Boolean> renderingListener;
+
+ /**
+ * Creates a new status bar.
+ */
+ public StatusBar() {
+ localToCRS = MathTransforms.identity(BIDIMENSIONAL);
+ targetCoordinates = new GeneralDirectPosition(BIDIMENSIONAL);
+ sourceCoordinates = targetCoordinates.coordinates;
+ lastX = lastY = Double.NaN;
+ format = new CoordinateFormat();
+ coordinates = new Label();
+ message = new Label();
+ progress = new ProgressBar();
+ progress.setVisible(false);
+ message.setTextFill(Color.RED);
+ message.setMaxWidth(Double.POSITIVE_INFINITY);
+ HBox.setHgrow(message, Priority.ALWAYS);
+ view = new HBox(12, progress, message, coordinates);
+ view.setPadding(PADDING);
+ canvasProperty = new SimpleObjectProperty<>(this, "canvas");
+ canvasProperty.addListener(this::onCanvasSpecified);
+ }
+
+ /**
+ * Returns the node to add to the scene graph for showing the status bar.
+ */
+ @Override
+ public final Region getView() {
+ return view;
+ }
+
+ /**
+ * Returns the canvas that this status bar is tracking.
+ *
+ * @return canvas that this status bar is tracking, or {@code null} if none.
+ *
+ * @see #canvasProperty
+ */
+ public final MapCanvas getCanvas() {
+ return canvasProperty.get();
+ }
+
+ /**
+ * Sets the canvas that this status bar is tracking.
+ * This method register all necessary listeners.
+ * A value of {@code null} unregister all listeners.
+ *
+ * @param canvas the canvas to track, or {@code null} if none.
+ *
+ * @see #canvasProperty
+ */
+ public final void setCanvas(final MapCanvas canvas) {
+ canvasProperty.set(canvas);
+ }
+
+ /**
+ * Invoked when a new value is set on {@link #canvasProperty}.
+ */
+ private void onCanvasSpecified(final ObservableValue<? extends MapCanvas> property,
+ final MapCanvas previous, final MapCanvas value)
+ {
+ if (previous != null) {
+ previous.floatingPane.removeEventHandler(MouseEvent.MOUSE_ENTERED, this);
+ previous.floatingPane.removeEventHandler(MouseEvent.MOUSE_EXITED, this);
+ previous.floatingPane.removeEventHandler(MouseEvent.MOUSE_MOVED, this);
+ previous.renderingProperty().removeListener(renderingListener);
+ renderingListener = null;
+ }
+ if (value != null) {
+ value.floatingPane.addEventHandler(MouseEvent.MOUSE_ENTERED, this);
+ value.floatingPane.addEventHandler(MouseEvent.MOUSE_EXITED, this);
+ value.floatingPane.addEventHandler(MouseEvent.MOUSE_MOVED, this);
+ value.renderingProperty().addListener(renderingListener = new RenderingListener());
+ }
+ }
+
+ /**
+ * Listener notified when {@link MapCanvas} completed its rendering.
+ * This listener set {@link StatusBar#localToCRS} to the inverse of
+ * {@link MapCanvas#objectiveToDisplay}.
+ */
+ private final class RenderingListener implements ChangeListener<Boolean> {
+ @Override public void changed(final ObservableValue<? extends Boolean> property,
+ final Boolean previous, final Boolean value)
+ {
+ progress.setVisible(value);
+ if (!value) try {
+ applyCanvasGeometry(getCanvas().getGridGeometry());
+ } catch (RenderException e) {
+ setErrorMessage(null, e);
+ }
+ }
+ }
+
+ /**
+ * Configures this status bar for showing coordinates in the CRS and resolution given by the specified
+ * grid geometry. The geometry properties are applied as below:
+ *
+ * <ul>
+ * <li>{@link GridGeometry#getCoordinateReferenceSystem()} defines the CRS of the coordinates to format.</li>
+ * <li>{@link GridGeometry#getGridToCRS(PixelInCell) GridGeometry.getGridToCRS(PixelInCell.CELL_CENTER)}
+ * defines the conversion from coordinate values locale to the canvas to coordinate values in the CRS
+ * (the {@linkplain #getLocalToCRS() local to CRS} conversion).</li>
+ * <li>{@link GridGeometry#getExtent()} provides the view size in pixels, used for estimating a resolution.</li>
+ * <li>{@link GridGeometry#getResolution(boolean)} is also used for estimating a resolution.</li>
+ * </ul>
+ *
+ * All above properties are optional.
+ * The "local to CRS" conversion can be updated after this method call with {@link #setLocalToCRS(MathTransform)}.
+ *
+ * @param geometry geometry of the coverage shown in {@link MapCanvas}, or {@code null}.
+ */
+ public void applyCanvasGeometry(final GridGeometry geometry) {
+ localToCRS = null;
+ precisions = null;
+ inflatePrecisions = null;
+ CoordinateReferenceSystem crs = null;
+ double resolution = 1;
+ Unit<?> unit = Units.PIXEL;
+ if (geometry != null) {
+ if (geometry.isDefined(GridGeometry.GRID_TO_CRS)) {
+ localToCRS = geometry.getGridToCRS(PixelInCell.CELL_CENTER);
+ if (geometry.isDefined(GridGeometry.CRS)) {
+ crs = geometry.getCoordinateReferenceSystem();
+ }
+ }
+ /*
+ * Computes the precision of coordinates to format. We use the finest resolution,
+ * looking only at axes having the same units of measurement than the first axis.
+ * This will be used as a fallback if we can not compute the precision specific
+ * to a coordinate, for example if we can not compute the derivative.
+ */
+ if (geometry.isDefined(GridGeometry.RESOLUTION)) {
+ double[] resolutions = geometry.getResolution(true);
+ if (crs != null && resolutions.length != 0) {
+ final CoordinateSystem cs = crs.getCoordinateSystem();
+ unit = cs.getAxis(0).getUnit();
+ for (int i=0; i<resolutions.length; i++) {
+ if (unit.equals(cs.getAxis(i).getUnit())) {
+ final double r = resolutions[i];
+ if (r < resolution) resolution = r;
+ }
+ }
+ }
+ }
+ /*
+ * Add a tolerance factor of ½ pixel when computing the number of significant
+ * fraction digits to shown in coordinates.
+ */
+ if (geometry.isDefined(GridGeometry.EXTENT)) {
+ final GridExtent extent = geometry.getExtent();
+ final int n = extent.getDimension();
+ inflatePrecisions = new double[n];
+ for (int i=0; i<n; i++) {
+ inflatePrecisions[i] = (0.5 / extent.getSize(i)) + 1;
+ }
+ }
+ }
+ /*
+ * Prepare objects to be reused for each coordinate transformation.
+ * Configure the `CoordinateFormat` with the CRS.
+ */
+ if (localToCRS != null) {
+ sourceCoordinates = new double[Math.max(localToCRS.getSourceDimensions(), BIDIMENSIONAL)];
+ targetCoordinates = new GeneralDirectPosition(localToCRS.getTargetDimensions());
+ } else {
+ localToCRS = MathTransforms.identity(BIDIMENSIONAL);
+ targetCoordinates = new GeneralDirectPosition(BIDIMENSIONAL);
+ sourceCoordinates = targetCoordinates.coordinates; // Okay to share array if same dimension.
+ }
+ format.setDefaultCRS(crs);
+ format.setPrecision(resolution, unit);
+ final String text = IdentifiedObjects.getDisplayName(crs, format.getLocale(Locale.Category.DISPLAY));
+ Tooltip tp = null;
+ if (text != null) {
+ tp = coordinates.getTooltip();
+ if (tp == null) {
+ tp = new Tooltip(text);
+ } else {
+ tp.setText(text);
+ }
+ }
+ coordinates.setTooltip(tp);
+ lastX = lastY = Double.NaN;
+ }
+
+ /**
+ * Returns the conversion from local coordinates to geographic or projected coordinates.
+ * The local coordinates are the coordinates of the view, as given for example in {@link MouseEvent}.
+ * This is initially an identity transform and can be computed by {@link #applyCanvasGeometry(GridGeometry)}.
+ *
+ * @return conversion from local coordinates to "real world" coordinates.
+ */
+ public final MathTransform getLocalToCRS() {
+ return localToCRS;
+ }
+
+ /**
+ * Sets the conversion from local coordinates to geographic or projected coordinates.
+ * The given value must have the same number of source and target dimensions than the
+ * previous value. If a change in the number of dimension is desired,
+ * use {@link #applyCanvasGeometry(GridGeometry)} instead.
+ *
+ * @param conversion the new conversion from local coordinates to "real world" coordinates.
+ * @throws MismatchedDimensionException if the number of dimensions is not the same than previous conversion.
+ */
+ public final void setLocalToCRS(final MathTransform conversion) {
+ ArgumentChecks.ensureNonNull("conversion", conversion);
+ int expected = localToCRS.getSourceDimensions();
+ int actual = conversion.getSourceDimensions();
+ if (expected == actual) {
+ expected = localToCRS.getTargetDimensions();
+ actual = conversion.getTargetDimensions();
+ if (expected == actual) {
+ localToCRS = conversion;
+ return;
+ }
+ }
+ throw new MismatchedDimensionException(Errors.format(
+ Errors.Keys.MismatchedDimension_3, "conversion", expected, actual));
+ }
+
+ /**
+ * Returns the coordinates given to the last call to {@link #setLocalCoordinates(double, double)},
+ * or an empty value if those coordinates are not visible.
+ *
+ * @return the local coordinates currently shown in the status bar.
+ */
+ public Optional<Point2D> getLocalCoordinates() {
+ if (coordinates.isVisible() && !Double.isNaN(lastX) && !Double.isNaN(lastY)) {
+ return Optional.of(new Point2D(lastX, lastY));
+ }
+ return Optional.empty();
+ }
+
+ /**
+ * Converts and formats the given pixel coordinates. Those coordinates will be automatically
+ * converted to geographic or projected coordinates if a "local to CRS" conversion is available.
+ *
+ * @param x the <var>x</var> coordinate local to the view.
+ * @param y the <var>y</var> coordinate local to the view.
+ *
+ * @see #handle(MouseEvent)
+ */
+ public void setLocalCoordinates(final double x, final double y) {
+ if (x != lastX || y != lastY) {
+ sourceCoordinates[0] = lastX = x;
+ sourceCoordinates[1] = lastY = y;
+ String text;
+ try {
+ Matrix derivative;
+ try {
+ derivative = MathTransforms.derivativeAndTransform(localToCRS,
+ sourceCoordinates, 0, targetCoordinates.coordinates, 0);
+ } catch (TransformException ignore) {
+ /*
+ * If above operation failed, it may be because the MathTransform does not support
+ * derivative calculation. Try again without derivative (the precision will be set
+ * to the default resolution computed in `setCanvasGeometry(…)`).
+ */
+ localToCRS.transform(sourceCoordinates, 0, targetCoordinates.coordinates, 0, 1);
+ derivative = null;
+ }
+ if (derivative == null) {
+ precisions = null;
+ } else {
+ if (precisions == null) {
+ precisions = new double[targetCoordinates.getDimension()];
+ }
+ /*
+ * Estimate the precision by looking at the maximal displacement in the CRS caused by
+ * a displacement of one cell (i.e. when moving by one row or column). We search for
+ * maximal displacement instead than minimal because we expect the displacement to be
+ * zero along some axes (e.g. one row down does not change longitude value in a Plate
+ * Carrée projection).
+ */
+ for (int j=derivative.getNumRow(); --j >= 0;) {
+ double p = 0;
+ for (int i=derivative.getNumCol(); --i >= 0;) {
+ double e = Math.abs(derivative.getElement(j, i));
+ if (inflatePrecisions != null) {
+ e *= inflatePrecisions[i];
+ }
+ if (e > p) p = e;
+ }
+ precisions[j] = p;
+ }
+ }
+ format.setPrecisions(precisions);
+ text = format.format(targetCoordinates);
+ } catch (TransformException | RuntimeException e) {
+ /*
+ * If even the fallback without derivative failed, show the error message.
+ */
+ Throwable cause = Exceptions.unwrap(e);
+ text = cause.getLocalizedMessage();
+ if (text == null) {
+ text = Classes.getShortClassName(cause);
+ }
+ }
+ coordinates.setText(text);
+ coordinates.setVisible(true);
+ }
+ }
+
+ /**
+ * Updates the coordinates shown in the status bar with the value given by the mouse event.
+ * This method handles the following events:
+ *
+ * <ul>
+ * <li>{@link MouseEvent#MOUSE_ENTERED}: show the coordinates.</li>
+ * <li>{@link MouseEvent#MOUSE_EXITED}: hide the coordinates.</li>
+ * <li>{@link MouseEvent#MOUSE_MOVED}: delegate to {@link #setLocalCoordinates(double, double)}.</li>
+ * </ul>
+ *
+ * @param event the enter, exit or move event. For the convenience of programmatic calls,
+ * a null value is synonymous to a mouse exit event.
+ */
+ @Override
+ public void handle(final MouseEvent event) {
+ boolean visible = (event != null);
+ if (visible) {
+ final EventType<? extends MouseEvent> type = event.getEventType();
+ final boolean enter = (type == MouseEvent.MOUSE_ENTERED);
+ final boolean moved = (type == MouseEvent.MOUSE_MOVED);
+ if (enter | moved) {
+ setLocalCoordinates(event.getX(), event.getY());
+ if (moved) return;
+ }
+ if (!enter && type != MouseEvent.MOUSE_EXITED) return;
+ }
+ coordinates.setVisible(visible);
+ }
+
+ /**
+ * Returns the error message currently shown.
+ *
+ * @return the current error message, or an empty value if none.
+ */
+ public Optional<String> getErrorMessage() {
+ return Optional.ofNullable(message.getText());
+ }
+
+ /**
+ * Show or hide an error message on the status bar, optionally with a button showing details in a dialog box.
+ * The {@code text} argument specifies the message to show on the status bar.
+ * If {@code text} is null, the message will be taken from the {@code details} if non-null.
+ * If {@code details} is also null, then the error message will be hidden.
+ *
+ * @param text the error message to show, or {@code null} if none.
+ * @param details the exception that caused the error, or {@code null} if none.
+ */
+ public void setErrorMessage(String text, final Throwable details) {
+ text = Strings.trimOrNull(text);
+ Button more = null;
+ if (details != null) {
+ final Locale locale = format.getLocale(Locale.Category.DISPLAY);
+ if (text == null) {
+ text = Exceptions.getLocalizedMessage(details, locale);
+ if (text == null) {
+ text = details.getClass().getSimpleName();
+ }
+ }
+ final String alert = text;
+ more = new Button(Styles.ERROR_DETAILS);
+ more.setOnAction((e) -> ExceptionReporter.show(
+ Resources.forLocale(locale).getString(Resources.Keys.ErrorDetails), alert, details));
+ }
+ message.setVisible(text != null);
+ message.setGraphic(more);
+ message.setText(text);
+ message.setTextFill(Styles.ERROR_TEXT);
+ }
+}
diff --git a/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/Styles.java b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/Styles.java
index a7c1379..0cc704a 100644
--- a/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/Styles.java
+++ b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/Styles.java
@@ -50,6 +50,11 @@ public final class Styles {
public static final double INITIAL_SPLIT = 200;
/**
+ * Approximate size of vertical scroll bar.
+ */
+ public static final int SCROLLBAR_WIDTH = 20;
+
+ /**
* "Standard" height of table rows. Can be approximate.
*/
public static final double ROW_HEIGHT = 30;
|