You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@sis.apache.org by de...@apache.org on 2022/06/27 15:02:56 UTC
[sis] 04/04: Allow the status bar to show coordinates using another system than `CoordinateReferenceSystem`. Current implementation uses `ReferenceSystemUsingIdentifiers` as a proof of work. The intent is to show coverage grid cell coordinates in a next commit.
This is an automated email from the ASF dual-hosted git repository.
desruisseaux pushed a commit to branch geoapi-4.0
in repository https://gitbox.apache.org/repos/asf/sis.git
commit baaa5771321edb6589f441a6e6125cfb2dd952aa
Author: Martin Desruisseaux <ma...@geomatys.com>
AuthorDate: Mon Jun 27 16:49:41 2022 +0200
Allow the status bar to show coordinates using another system than `CoordinateReferenceSystem`.
Current implementation uses `ReferenceSystemUsingIdentifiers` as a proof of work.
The intent is to show coverage grid cell coordinates in a next commit.
---
application/sis-javafx/pom.xml | 5 +
.../apache/sis/gui/coverage/CoverageCanvas.java | 2 +-
.../apache/sis/gui/coverage/CoverageExplorer.java | 2 +-
.../main/java/org/apache/sis/gui/map/MapMenu.java | 4 +-
.../org/apache/sis/gui/map/OperationFinder.java | 4 +-
.../java/org/apache/sis/gui/map/StatusBar.java | 615 +++++++++++++++------
.../org/apache/sis/gui/referencing/MenuSync.java | 131 +++--
.../sis/gui/referencing/ObjectStringConverter.java | 14 +-
.../gui/referencing/RecentReferenceSystems.java | 246 ++++++---
.../org/apache/sis/internal/gui/Resources.java | 10 +
.../apache/sis/internal/gui/Resources.properties | 2 +
.../sis/internal/gui/Resources_fr.properties | 2 +
12 files changed, 759 insertions(+), 278 deletions(-)
diff --git a/application/sis-javafx/pom.xml b/application/sis-javafx/pom.xml
index 463c965cb3..c5219fd2aa 100644
--- a/application/sis-javafx/pom.xml
+++ b/application/sis-javafx/pom.xml
@@ -135,6 +135,11 @@
<artifactId>sis-portrayal</artifactId>
<version>${project.version}</version>
</dependency>
+ <dependency>
+ <groupId>org.apache.sis.core</groupId>
+ <artifactId>sis-referencing-by-identifiers</artifactId>
+ <version>${project.version}</version>
+ </dependency>
<dependency>
<groupId>org.apache.sis.storage</groupId>
<artifactId>sis-xmlstore</artifactId>
diff --git a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/CoverageCanvas.java b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/CoverageCanvas.java
index 21e4bc55ba..daf385014a 100644
--- a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/CoverageCanvas.java
+++ b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/CoverageCanvas.java
@@ -1062,7 +1062,7 @@ public class CoverageCanvas extends MapCanvasAWT {
}
}
}
- controls.status.setLowestAccuracy(accuracy);
+ controls.status.lowestAccuracy.set(accuracy);
}
/*
* If error(s) occurred during calls to `RenderedImage.getTile(tx, ty)`, reports those errors.
diff --git a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/CoverageExplorer.java b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/CoverageExplorer.java
index 03a16c0e5b..b6bae9a127 100644
--- a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/CoverageExplorer.java
+++ b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/CoverageExplorer.java
@@ -252,7 +252,7 @@ public class CoverageExplorer extends Widget {
coverageProperty = new SimpleObjectProperty<> (this, "coverage");
referenceSystems = new RecentReferenceSystems();
referenceSystems.addUserPreferences();
- referenceSystems.addAlternatives("EPSG:4326", "EPSG:3395"); // WGS 84 / World Mercator
+ referenceSystems.addAlternatives("EPSG:4326", "EPSG:3395", "MGRS"); // WGS 84 / World Mercator
viewTypeProperty.addListener((p,o,n) -> onViewTypeSet(n));
resourceProperty.addListener((p,o,n) -> onPropertySet(n, null, coverageProperty));
coverageProperty.addListener((p,o,n) -> onPropertySet(null, n, resourceProperty));
diff --git a/application/sis-javafx/src/main/java/org/apache/sis/gui/map/MapMenu.java b/application/sis-javafx/src/main/java/org/apache/sis/gui/map/MapMenu.java
index 79e3045823..470f075fd6 100644
--- a/application/sis-javafx/src/main/java/org/apache/sis/gui/map/MapMenu.java
+++ b/application/sis-javafx/src/main/java/org/apache/sis/gui/map/MapMenu.java
@@ -132,7 +132,7 @@ public class MapMenu extends ContextMenu {
public void addReferenceSystems(final RecentReferenceSystems preferences) {
ArgumentChecks.ensureNonNull("preferences", preferences);
final MapCanvas.MenuHandler handler = startNewMenuItems(CRS);
- final Menu systemChoices = preferences.createMenuItems(handler);
+ final Menu systemChoices = preferences.createMenuItems(true, handler);
handler.selectedCrsProperty = RecentReferenceSystems.getSelectedProperty(systemChoices);
handler.positionables = new ToggleGroup();
@@ -161,7 +161,7 @@ public class MapMenu extends ContextMenu {
final Resources resources = Resources.forLocale(canvas.getLocale());
final MenuItem coordinates = resources.menu(Resources.Keys.CopyCoordinates, (event) -> {
try {
- final String text = format.formatCoordinates(handler.x, handler.y);
+ final String text = format.formatTabSeparatedCoordinates(handler.x, handler.y);
final ClipboardContent content = new ClipboardContent();
content.putString(text);
Clipboard.getSystemClipboard().setContent(content);
diff --git a/application/sis-javafx/src/main/java/org/apache/sis/gui/map/OperationFinder.java b/application/sis-javafx/src/main/java/org/apache/sis/gui/map/OperationFinder.java
index 0211c55141..dd9219f2c6 100644
--- a/application/sis-javafx/src/main/java/org/apache/sis/gui/map/OperationFinder.java
+++ b/application/sis-javafx/src/main/java/org/apache/sis/gui/map/OperationFinder.java
@@ -157,7 +157,7 @@ abstract class OperationFinder extends Task<MathTransform> {
/**
* If the given CRS is a grid CRS, replaces it by a geospatial CRS if possible.
* If the given CRS is not geospatial, then this method tries to replace it by
- * by the CRS of the coverage shown by the canvas (this is not necessarily the
+ * the CRS of the coverage shown by the canvas (this is not necessarily the
* {@linkplain MapCanvas#getObjectiveCRS() objective CRS}).
*
* @param crs the CRS to eventually replace by a geospatial CRS.
@@ -191,7 +191,7 @@ abstract class OperationFinder extends Task<MathTransform> {
}
/**
- * Returns the target CRS, giving precedence to {@link CoordinateOperation#getTargetCRS()} is suitable.
+ * Returns the target CRS, giving precedence to {@link CoordinateOperation#getTargetCRS()} if suitable.
* That precedence is because the {@link CoordinateOperation} may provide a more complete CRS from EPSG
* database.
*/
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
index f8595e285b..5050a22ab1 100644
--- 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
@@ -18,7 +18,9 @@ package org.apache.sis.gui.map;
import java.util.Arrays;
import java.util.Locale;
+import java.util.Objects;
import java.util.Optional;
+import java.util.logging.Logger;
import java.util.function.Predicate;
import java.awt.image.RenderedImage;
import java.beans.PropertyChangeEvent;
@@ -50,9 +52,12 @@ import javafx.beans.property.ReadOnlyObjectPropertyBase;
import javafx.beans.property.SimpleObjectProperty;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
+import javafx.concurrent.Task;
import javax.measure.Quantity;
import javax.measure.quantity.Length;
+import javax.measure.IncommensurableException;
import org.opengis.geometry.Envelope;
+import org.opengis.geometry.DirectPosition;
import org.opengis.geometry.MismatchedDimensionException;
import org.opengis.referencing.ReferenceSystem;
import org.opengis.referencing.datum.PixelInCell;
@@ -65,6 +70,7 @@ import org.opengis.referencing.operation.CoordinateOperation;
import org.opengis.referencing.crs.CoordinateReferenceSystem;
import org.apache.sis.referencing.operation.transform.MathTransforms;
import org.apache.sis.geometry.GeneralDirectPosition;
+import org.apache.sis.geometry.DirectPosition2D;
import org.apache.sis.geometry.CoordinateFormat;
import org.apache.sis.coverage.grid.GridCoverage;
import org.apache.sis.coverage.grid.GridGeometry;
@@ -80,16 +86,18 @@ import org.apache.sis.util.Exceptions;
import org.apache.sis.util.ArgumentChecks;
import org.apache.sis.util.ComparisonMode;
import org.apache.sis.util.resources.Errors;
+import org.apache.sis.util.logging.Logging;
import org.apache.sis.gui.Widget;
import org.apache.sis.gui.referencing.RecentReferenceSystems;
-import org.apache.sis.internal.referencing.ReferencingUtilities;
import org.apache.sis.internal.gui.BackgroundThreads;
import org.apache.sis.internal.gui.ExceptionReporter;
import org.apache.sis.internal.gui.GUIUtilities;
import org.apache.sis.internal.gui.Resources;
import org.apache.sis.internal.gui.Styles;
+import org.apache.sis.internal.system.Modules;
import org.apache.sis.referencing.CRS;
import org.apache.sis.referencing.IdentifiedObjects;
+import org.apache.sis.referencing.gazetteer.ReferencingByIdentifiers;
/**
@@ -139,6 +147,8 @@ public class StatusBar extends Widget implements EventHandler<MouseEvent> {
/**
* The container of controls making the status bar.
+ *
+ * @see #getView()
*/
private final HBox view;
@@ -146,13 +156,18 @@ public class StatusBar extends Widget implements EventHandler<MouseEvent> {
* 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 before {@link #position}.
+ *
+ * @see #getMessage()
*/
private final Label message;
/**
* Local coordinates currently formatted in the {@link #position} field.
* This is used for detecting if coordinate values changed since last formatting.
- * Those coordinates are often integer values.
+ * If the mouse moved outside the canvas, then those coordinates are set to NaN.
+ * Otherwise those coordinates are usually integer values.
+ *
+ * @see #getLocalCoordinates()
*/
private double lastX, lastY;
@@ -185,7 +200,7 @@ public class StatusBar extends Widget implements EventHandler<MouseEvent> {
/**
* The reference system used for rendering the data for which this status bar is providing cursor coordinates.
* This is the "{@linkplain RecentReferenceSystems#setPreferred(boolean, ReferenceSystem) preferred}" or native
- * data CRS. It may not be the same than the CRS of coordinates actually shown in the status bar.
+ * data CRS. It may be different than the CRS of coordinates actually shown in the status bar.
*
* @see MapCanvas#getObjectiveCRS()
*/
@@ -199,7 +214,8 @@ public class StatusBar extends Widget implements EventHandler<MouseEvent> {
* (in which case {@link #localToPositionCRS} is the same instance than {@link #localToObjectiveCRS})
* or if the target is not a CRS (for example it may be a Military Grid Reference System (MGRS) code).
*
- * @see #updateLocalToPositionCRS()
+ * @see #localToObjectiveCRS
+ * @see #localToPositionCRS
*/
private MathTransform objectiveToPositionCRS;
@@ -250,17 +266,16 @@ public class StatusBar extends Widget implements EventHandler<MouseEvent> {
public final ReadOnlyObjectProperty<ReferenceSystem> positionReferenceSystem;
/**
- * Conversion from local coordinates to geographic or projected coordinates shown in this status bar.
+ * Transform from local coordinates to geographic or projected coordinates shown in this status bar.
* This is the concatenation of {@link #localToObjectiveCRS} with {@link #objectiveToPositionCRS} transform.
* The result is a transform to the user-selected CRS for coordinates shown in the status bar.
- * This conversion shall never be null but may be the identity transform.
+ * That transform target CRS shall correspond to {@link CoordinateFormat#getDefaultCRS()}.
+ * This transform shall never be null but may be the identity transform.
* It is usually non-affine if the display CRS is not the same than the objective CRS.
* This transform may have a {@linkplain CoordinateOperation#getCoordinateOperationAccuracy() limited accuracy}.
*
- * <p>The target CRS can be obtained by {@link CoordinateOperation#getTargetCRS()} on
- * {@link #objectiveToPositionCRS} or by {@link CoordinateFormat#getDefaultCRS()}.</p>
- *
- * @see #updateLocalToPositionCRS()
+ * @see #localToObjectiveCRS
+ * @see #getPositionCRS()
*/
private MathTransform localToPositionCRS;
@@ -290,7 +305,6 @@ public class StatusBar extends Widget implements EventHandler<MouseEvent> {
*
* @see #targetCoordinates
* @see #position
- * @see #setTargetCRS(CoordinateReferenceSystem)
*/
private double[] sourceCoordinates;
@@ -308,6 +322,9 @@ public class StatusBar extends Widget implements EventHandler<MouseEvent> {
* The desired precisions for each dimension in the {@link #targetCoordinates} to format.
* It may vary for each position if the {@link #localToPositionCRS} transform is non-linear.
* This array is initially {@code null} and created when first needed.
+ * It is the argument to be given to {@link CoordinateFormat#setPrecisions(double...)}.
+ *
+ * @see CoordinateFormat#setPrecisions(double...)
*/
private double[] precisions;
@@ -323,18 +340,57 @@ public class StatusBar extends Widget implements EventHandler<MouseEvent> {
private double[] inflatePrecisions;
/**
- * The declared accuracy on ground, or {@code null} if unspecified.
+ * The unit of measurement for {@link #precisions}.
+ * This is the unit of measurement of the first coordinate system axis.
+ */
+ private Unit<?> precisionUnit;
+
+ /**
+ * Number of elements in {@link #precisions} having the same unit of measurement than {@link #precisionUnit}.
+ * This value shall be between 1 and {@code precisions.length} inclusive, or 0 if {@link #precisionUnit} is null.
+ */
+ private int compatiblePrecisionCount;
+
+ /**
+ * Specifies a minimal uncertainty to append as "± <var>accuracy</var>" after the coordinate values.
+ * This uncertainty can be caused for example by a coordinate transformation applied on data before
+ * rendering in the canvas.
+ *
+ * <p>Note that {@code StatusBar} maintains also its own uncertainty, which can be caused by transformation
+ * from objective CRS to the {@linkplain #positionReferenceSystem reference system used in this status bar}.
+ * Such transformations happen when users select a CRS on the status bar (e.g. using the contextual menu)
+ * which is different than the canvas {@linkplain MapCanvas#getObjectiveCRS() objective CRS}.
+ * In such case we have two sources of stochastic errors: one internal to this status bar and one having
+ * causes external to this status bar. This {@code lowestAccuracy} property is for specifying the latter.</p>
+ *
+ * <p>The accuracy actually shown by {@code StatusBar} will be the greatest value between the accuracy
+ * specified in this property and the accuracy computed internally by {@code StatusBar}.
+ * Note that the "± <var>accuracy</var>" text may be shown or hidden depending on the zoom level.
+ * If pixels on screen are larger than the accuracy, then the accuracy text is hidden.</p>
+ *
+ * @see CoordinateFormat#setGroundAccuracy(Quantity)
*
- * @see #getLowestAccuracy()
- * @see #setLowestAccuracy(Quantity)
+ * @since 1.3
*/
- private Quantity<Length> lowestAccuracy;
+ public final ObjectProperty<Quantity<Length>> lowestAccuracy;
/**
* The object to use for formatting coordinate values.
+ * This reference shall not be null because it is the instance to use most of the time.
+ * In the rarer cases where {@link #formatAsIdentifiers} is non-null, the latter has precedence.
*/
private final CoordinateFormat format;
+ /**
+ * The object to use for formatting coordinate values as identifiers (MGRS, GeoHash…).
+ * The null/non-null state tells whether to format coordinates as identifiers or not;
+ * a {@code null} values mean that coordinates shall be formatted using {@link #format} instead.
+ *
+ * <p>If non-null, then {@link #getPositionCRS()} should be the {@link #objectiveCRS}
+ * and {@link #objectiveToPositionCRS} should be null.</p>
+ */
+ private ReferencingByIdentifiers.Coder formatAsIdentifiers;
+
/**
* The label where to format the cursor position, either as coordinate values or other representations.
* The text is usually the result of formatting coordinate values as numerical values,
@@ -392,6 +448,14 @@ public class StatusBar extends Widget implements EventHandler<MouseEvent> {
*/
private boolean isSampleValuesVisible;
+ /**
+ * The background task under execution, or {@code null} if none. This is used for cancellation.
+ *
+ * @see #cancelWorker()
+ * @see #terminated(Task)
+ */
+ private Task<?> worker;
+
/**
* Creates a new status bar for showing coordinates of mouse cursor position in a canvas.
* If {@link #track(Canvas)} is invoked, then this {@code StatusBar} will show coordinates
@@ -419,6 +483,7 @@ public class StatusBar extends Widget implements EventHandler<MouseEvent> {
lastX = lastY = Double.NaN;
yDimension = 1;
format = new CoordinateFormat();
+ lowestAccuracy = new SimpleObjectProperty<>(this, "lowestAccuracy");
message = new Label();
message.setVisible(false); // Waiting for getting a message to display.
@@ -454,8 +519,14 @@ public class StatusBar extends Widget implements EventHandler<MouseEvent> {
if (systemChooser == null) {
selectedSystem = null;
} else {
- final Menu choices = systemChooser.createMenuItems((property, oldValue, newValue) -> {
- setPositionCRS(newValue instanceof CoordinateReferenceSystem ? (CoordinateReferenceSystem) newValue : null);
+ final Menu choices = systemChooser.createMenuItems(false, (property, oldValue, newValue) -> {
+ if (newValue instanceof CoordinateReferenceSystem) {
+ setPositionCRS((CoordinateReferenceSystem) newValue);
+ } else if (newValue instanceof ReferencingByIdentifiers) {
+ setPositionRID((ReferencingByIdentifiers) newValue);
+ } else {
+ setPositionCRS(null); // Default to `objectiveCRS`.
+ }
});
selectedSystem = RecentReferenceSystems.getSelectedProperty(choices);
menu.getItems().add(choices);
@@ -465,14 +536,16 @@ public class StatusBar extends Widget implements EventHandler<MouseEvent> {
* to `applyCanvasGeometry(GridGeometry)` has (λ,φ) axis order but the CRS offered to user have
* (φ,λ) axis order (because we try to comply with definitions following geographers practice).
* In such case we will replace (λ,φ) by (φ,λ). Since we use the list of choices as the source
- * of desired CRS, we have to listen to new elements added to that list. This is necessary since
- * the list of often empty at construction time and filled later after a background thread task.
+ * of desired CRS, we have to listen to new elements added to that list. This is necessary because
+ * the list is often empty at construction time and filled later after a background thread task.
*/
systemChooser.getItems().addListener((ListChangeListener.Change<? extends ReferenceSystem> change) -> {
- while (change.next()) {
- if (change.wasAdded() || change.wasReplaced()) {
- setReplaceablePositionCRS(format.getDefaultCRS());
- break;
+ if (formatAsIdentifiers == null) {
+ while (change.next()) {
+ if (change.wasAdded() || change.wasReplaced()) {
+ setReplaceablePositionCRS(getPositionCRS());
+ break;
+ }
}
}
});
@@ -487,7 +560,7 @@ public class StatusBar extends Widget implements EventHandler<MouseEvent> {
sampleValuesProvider.addListener((p,o,n) -> {
ValuesUnderCursor.update(this, o, n);
if (o != null) items.remove(o.valueChoices);
- if (n != null) items.add(0, n.valueChoices);
+ if (n != null) items.add(1, n.valueChoices);
setSampleValuesVisible(n != null && !n.isEmpty());
});
}
@@ -507,7 +580,7 @@ public class StatusBar extends Widget implements EventHandler<MouseEvent> {
}
/**
- * Registers listeners on the following canvas for track mouse movements.
+ * Registers listeners on the specified canvas for tracking mouse movements.
* After this method call, this {@code StatusBar} will show coordinates (usually geographic or projected)
* of mouse cursor position when the mouse is over that canvas. The {@link #localToObjectiveCRS} property
* value may be overwritten at any time, for example after each gesture event such as pan, zoom or rotation.
@@ -680,10 +753,10 @@ public class StatusBar extends Widget implements EventHandler<MouseEvent> {
*/
MathTransform localToCRS = null;
CoordinateReferenceSystem crs = null;
- sourceCoordinates = ArraysExt.EMPTY_DOUBLE;
- double resolution = 1;
+ double[] pointOfInterest = ArraysExt.EMPTY_DOUBLE;
double[] inflate = null;
- Unit<?> unit = Units.PIXEL;
+ double resolution = 1;
+ Unit<?> unit = Units.PIXEL;
if (geometry != null) {
if (geometry.isDefined(GridGeometry.CRS)) {
crs = geometry.getCoordinateReferenceSystem();
@@ -721,13 +794,21 @@ public class StatusBar extends Widget implements EventHandler<MouseEvent> {
for (int i=0; i<n; i++) {
inflate[i] = (0.5 / extent.getSize(i)) + 1;
}
- sourceCoordinates = extent.getPointOfInterest(PixelInCell.CELL_CENTER);
+ pointOfInterest = extent.getPointOfInterest(PixelInCell.CELL_CENTER);
}
}
- final boolean sameCRS = Utilities.equalsIgnoreMetadata(objectiveCRS, crs);
+ /*
+ * If the objective CRS stay unchanged, then we will try to keep the same position CRS
+ * (which may be different), which implies keeping the same `objectiveToPositionCRS`.
+ */
+ final boolean clear = (fullOperationSearchRequired != null) && fullOperationSearchRequired.test(canvas);
+ final boolean sameCRS = !clear && Utilities.equalsIgnoreMetadata(objectiveCRS, crs);
if (localToCRS == null) {
localToCRS = MathTransforms.identity(BIDIMENSIONAL);
}
+ if (sameCRS && objectiveToPositionCRS != null) {
+ localToCRS = MathTransforms.concatenate(localToCRS, objectiveToPositionCRS);
+ }
final int srcDim = Math.max(localToCRS.getSourceDimensions(), BIDIMENSIONAL);
final int tgtDim = localToCRS.getTargetDimensions();
/*
@@ -740,24 +821,26 @@ public class StatusBar extends Widget implements EventHandler<MouseEvent> {
* Instead we will wait for the next mouse event to provide new local coordinates.
*/
((LocalToObjective) localToObjectiveCRS).setNoCheck(localToCRS);
- sourceCoordinates = Arrays.copyOf(sourceCoordinates, srcDim);
+ sourceCoordinates = Arrays.copyOf(pointOfInterest, srcDim);
targetCoordinates = new GeneralDirectPosition(tgtDim);
objectiveCRS = crs;
- localToPositionCRS = localToCRS; // May be updated again below.
+ localToPositionCRS = localToCRS;
inflatePrecisions = inflate;
precisions = null;
- lastX = lastY = Double.NaN; // Not valid anymove — see above block comment.
+ lastX = lastY = Double.NaN; // Not valid anymove — see above block comment.
+ /*
+ * If the objective CRS is unchanged, keep the same position CRS (the CRS selected
+ * by user for formatting coordinates; it may be different than the objective CRS).
+ * Otherwise we reset the formatter to the CRS specified in the grid geometry.
+ */
if (sameCRS) {
- updateLocalToPositionCRS();
- // Keep the format CRS unchanged since we made `localToPositionCRS` consistent with its value.
- if (fullOperationSearchRequired != null && fullOperationSearchRequired.test(canvas)) {
- setPositionCRS(format.getDefaultCRS());
- }
+ crs = getPositionCRS();
+ targetCoordinates.setCoordinateReferenceSystem(crs);
} else {
objectiveToPositionCRS = null;
- setFormatCRS(crs, null); // Should be invoked before to set precision.
+ setFormatCRS(crs, null); // Should be invoked before to set ground precision.
crs = OperationFinder.toGeospatial(crs, canvas);
- crs = setReplaceablePositionCRS(crs); // May invoke setFormatCRS(…) after background work.
+ crs = setReplaceablePositionCRS(crs); // May invoke setFormatCRS(…) after background work.
}
format.setGroundPrecision(Quantities.create(resolution, unit));
/*
@@ -770,31 +853,6 @@ public class StatusBar extends Widget implements EventHandler<MouseEvent> {
}
}
- /**
- * Computes {@link #localToPositionCRS} after a change of {@link #localToObjectiveCRS}.
- * Other properties, in particular {@link #objectiveToPositionCRS}, must be valid.
- */
- private void updateLocalToPositionCRS() {
- localToPositionCRS = localToObjectiveCRS.get();
- if (objectiveToPositionCRS != null) {
- localToPositionCRS = MathTransforms.concatenate(localToPositionCRS, objectiveToPositionCRS);
- }
- setTargetCRS(format.getDefaultCRS());
- }
-
- /**
- * Sets the CRS of {@link #targetCoordinates}.
- * This method creates a new position if the number of dimensions changed.
- */
- private void setTargetCRS(final CoordinateReferenceSystem crs) {
- final int tgtDim = ReferencingUtilities.getDimension(crs);
- if (tgtDim != 0 && tgtDim != targetCoordinates.getDimension()) {
- precisions = null;
- targetCoordinates = new GeneralDirectPosition(tgtDim);
- }
- targetCoordinates.setCoordinateReferenceSystem(crs);
- }
-
/**
* Sets the CRS of the position shown in this status bar after replacement by one of the available CRS
* if a match is found. This method compares the given CRS with the list of choices before to delegate
@@ -803,6 +861,9 @@ public class StatusBar extends Widget implements EventHandler<MouseEvent> {
* axis order, and this method swapping axes to standard (<var>latitude</var>, <var>longitude</var>)
* axis order for coordinates display purpose.
*
+ * <h4>Prerequisite</h4>
+ * This method should be invoked only when {@link #formatAsIdentifiers} is null. This is not verified.
+ *
* @param crs the new CRS (ignoring axis order), or {@code null} for {@link #objectiveCRS}.
* @return the reference system actually used for formatting coordinates. It may have different axis order
* and units than the specified CRS. This is the CRS that {@link CoordinateFormat#getDefaultCRS()}
@@ -818,7 +879,7 @@ public class StatusBar extends Widget implements EventHandler<MouseEvent> {
}
}
}
- if (crs != format.getDefaultCRS()) {
+ if (crs != getPositionCRS()) {
setPositionCRS(crs);
}
return crs;
@@ -831,8 +892,12 @@ public class StatusBar extends Widget implements EventHandler<MouseEvent> {
* the first time that it is executed, but should be fast on subsequent invocations.
*
* @param crs the new CRS, or {@code null} for {@link #objectiveCRS}.
+ *
+ * @see #setPositionRID(ReferencingByIdentifiers)
+ * @see #getPositionCRS()
*/
private void setPositionCRS(final CoordinateReferenceSystem crs) {
+ cancelWorker();
if (crs != null && objectiveCRS != null && objectiveCRS != crs) {
position.setTextFill(Styles.OUTDATED_TEXT);
/*
@@ -841,10 +906,12 @@ public class StatusBar extends Widget implements EventHandler<MouseEvent> {
* in the middle of changes at any time. All objects are assumed immutable.
*/
final Envelope aoi = (systemChooser != null) ? systemChooser.areaOfInterest.get() : null;
- BackgroundThreads.execute(new OperationFinder(canvas, aoi, objectiveCRS, crs) {
+ BackgroundThreads.execute(worker = new OperationFinder(canvas, aoi, objectiveCRS, crs) {
/**
* The accuracy to show on the status bar, or {@code null} if none.
* This is computed after {@link CoordinateOperation} has been determined.
+ *
+ * @see StatusBar#lowestAccuracy
*/
private Quantity<Length> accuracy;
@@ -860,12 +927,14 @@ public class StatusBar extends Widget implements EventHandler<MouseEvent> {
}
return value;
}
+
/**
* Invoked in JavaFX thread on success. The {@link StatusBar#localToPositionCRS} transform
* is set to the transform that we computed in background and the {@link CoordinateFormat}
* is configured with auxiliary information such as positional accuracy.
*/
@Override protected void succeeded() {
+ terminated(this);
setPositionCRS(this, accuracy);
}
@@ -874,11 +943,8 @@ public class StatusBar extends Widget implements EventHandler<MouseEvent> {
* the coordinates will appear in red for telling user that there is a problem.
*/
@Override protected void failed() {
- final Locale locale = getLocale();
- setErrorMessage(Resources.forLocale(locale).getString(Resources.Keys.CanNotUseRefSys_1,
- IdentifiedObjects.getDisplayName(crs, locale)), getException());
- selectedSystem.set(format.getDefaultCRS());
- resetPositionCRS(Styles.ERROR_TEXT);
+ terminated(this);
+ setReferenceSystemError(crs, getException());
}
/** For logging purpose if a non-fatal error occurs. */
@@ -913,13 +979,29 @@ public class StatusBar extends Widget implements EventHandler<MouseEvent> {
*
* @param finder the completed task with the new {@link #objectiveToPositionCRS}.
* @param accuracy the accuracy to show on the status bar, or {@code null} if none.
+ *
+ * @see #setPositionRID(ReferencingByIdentifiers.Coder, String, DirectPosition)
*/
private void setPositionCRS(final OperationFinder finder, final Quantity<Length> accuracy) {
+ worker = null;
setErrorMessage(null, null);
- setFormatCRS(finder.getTargetCRS(), accuracy);
- objectiveToPositionCRS = finder.getValue();
fullOperationSearchRequired = finder.fullOperationSearchRequired();
- updateLocalToPositionCRS();
+ localToPositionCRS = localToObjectiveCRS.get();
+ objectiveToPositionCRS = finder.getValue();
+ if (objectiveToPositionCRS != null) {
+ localToPositionCRS = MathTransforms.concatenate(localToPositionCRS, objectiveToPositionCRS);
+ }
+ setFormatCRS(finder.getTargetCRS(), accuracy);
+ rewritePosition(null);
+ }
+
+ /**
+ * Invoked after a new reference system has been set. This method rewrites the coordinates
+ * on the assumption that {@link #lastX} and {@link #lastY} are still valid.
+ *
+ * @param current the local coordinates used for current text, or {@code null} if not valid.
+ */
+ private void rewritePosition(final DirectPosition current) {
position.setTextFill(Styles.NORMAL_TEXT);
position.setMinWidth(0);
maximalPositionLength = 0;
@@ -928,64 +1010,92 @@ public class StatusBar extends Widget implements EventHandler<MouseEvent> {
final double y = lastY;
lastX = lastY = Double.NaN;
if (!Double.isNaN(x) && !Double.isNaN(y)) {
- setLocalCoordinates(x, y);
+ if (current == null || current.getOrdinate(0) != x || current.getOrdinate(1) != y) {
+ setLocalCoordinates(x, y);
+ }
}
}
}
+ /**
+ * Invoked in JavaFX thread when a background task finished its work, either successfully or on error.
+ */
+ private void terminated(final Task<?> caller) {
+ if (caller == worker) {
+ worker = null;
+ }
+ }
+
+ /**
+ * If a background task was in progress, cancels it. This is invoked before a new background task is launched.
+ */
+ private void cancelWorker() {
+ if (worker != null) {
+ worker.cancel();
+ worker = null;
+ }
+ }
+
/**
* Sets the {@link CoordinateFormat} default CRS together with the tool tip text.
* Caller is responsible to setup transforms ({@link #localToPositionCRS}, <i>etc</i>).
- * For the method that apply required changes on transforms before to set the format CRS,
+ * For method that applies required changes on transforms before to set the format CRS,
* see {@link #setPositionCRS(CoordinateReferenceSystem)}.
*
* @param crs the new {@link #format} reference system.
- * @param accuracy positional accuracy in the given CRS, or {@code null} if none.
+ * @param accuracy positional accuracy of the transformation from local coordinates to the given CRS,
+ * or {@code null} if none. This is an accuracy computed by this {@code StatusBar} class,
+ * as opposed to {@link #lowestAccuracy} which has causes external to {@code StatusBar}.
*
* @see #positionReferenceSystem
*/
private void setFormatCRS(final CoordinateReferenceSystem crs, final Quantity<Length> accuracy) {
- format.setDefaultCRS(crs);
- format.setGroundAccuracy(Quantities.max(accuracy, lowestAccuracy));
- String text = IdentifiedObjects.getDisplayName(crs, getLocale());
- Tooltip tp = null;
- if (text != null) {
- tp = position.getTooltip();
- if (tp == null) {
- tp = new Tooltip(text);
- } else {
- tp.setText(text);
- }
+ int dimension = localToPositionCRS.getTargetDimensions();
+ GeneralDirectPosition target = targetCoordinates;
+ if (dimension != target.getDimension()) {
+ target = new GeneralDirectPosition(dimension);
+ precisions = null;
}
- position.setTooltip(tp);
+ target.setCoordinateReferenceSystem(crs);
+ format.setDefaultCRS(crs);
+ targetCoordinates = target; // Assign only after abpve succeed.
+ formatAsIdentifiers = null;
+ format.setGroundAccuracy(Quantities.max(accuracy, lowestAccuracy.get()));
+ setTooltip(crs);
/*
* Prepare the text to show when the mouse is outside the canvas area.
* We will write axis abbreviations, for example "(φ, λ)".
+ * Also fetch the unit of measurement of first axes.
*/
- text = null;
+ compatiblePrecisionCount = 0;
+ precisionUnit = null;
+ String text = null;
if (crs != null) {
+ final StringBuilder b = new StringBuilder().append('(');
final CoordinateSystem cs = crs.getCoordinateSystem();
- if (cs != null) { // Paranoiac check (should never be null).
- final int dimension = cs.getDimension();
- if (dimension > 0) { // Paranoiac check (should never be zero).
- final StringBuilder b = new StringBuilder().append('(');
- for (int i=0; i<dimension; i++) {
- if (i != 0) b.append(", ");
- final CoordinateSystemAxis axis = cs.getAxis(i);
- if (axis != null) { // Paranoiac check (should never be null).
- final String abbr = Strings.trimOrNull(axis.getAbbreviation());
- if (abbr != null) {
- b.append(abbr);
- continue;
- }
+ dimension = (cs != null) ? cs.getDimension() : 0; // Paranoiac check (should never be null).
+ for (int i=0; i<dimension; i++) {
+ if (i != 0) b.append(", ");
+ final CoordinateSystemAxis axis = cs.getAxis(i);
+ if (axis != null) { // Paranoiac check (should never be null).
+ if (i == compatiblePrecisionCount) { // Require consecutive axes for unit test.
+ final Unit<?> unit = axis.getUnit();
+ if (i == 0 || Objects.equals(precisionUnit, unit)) {
+ compatiblePrecisionCount = i+1;
+ precisionUnit = unit;
}
- b.append('?');
}
- b.append(')');
- format.getGroundAccuracyText().ifPresent(b::append);
- text = b.toString();
+ final String abbr = Strings.trimOrNull(axis.getAbbreviation());
+ if (abbr != null) {
+ b.append(abbr);
+ continue;
+ }
}
+ b.append('?');
}
+ b.append(')');
+ format.getGroundAccuracyText().ifPresent(b::append);
+ text = b.toString();
}
/*
* If the mouse is already outside canvas area, update the `position` text now.
@@ -995,7 +1105,6 @@ public class StatusBar extends Widget implements EventHandler<MouseEvent> {
position.setText(text);
}
outsideText = text;
- setTargetCRS(crs);
((PositionSystem) positionReferenceSystem).fireValueChangedEvent();
}
@@ -1003,15 +1112,30 @@ public class StatusBar extends Widget implements EventHandler<MouseEvent> {
* Implementation of {@link #positionReferenceSystem} property.
*/
private final class PositionSystem extends ReadOnlyObjectPropertyBase<ReferenceSystem> {
- @Override public Object getBean() {return StatusBar.this;}
- @Override public String getName() {return "positionReferenceSystem";}
- @Override public ReferenceSystem get() {return format.getDefaultCRS();}
+ @Override public Object getBean() {return StatusBar.this;}
+ @Override public String getName() {return "positionReferenceSystem";}
+ @Override public ReferenceSystem get() {
+ final ReferencingByIdentifiers.Coder f = formatAsIdentifiers;
+ return (f != null) ? f.getReferenceSystem() : getPositionCRS();
+ }
@Override protected void fireValueChangedEvent() {super.fireValueChangedEvent();}
}
+ /**
+ * Returns the coordinate reference system of the position shown in this status bar.
+ * This is valid only if {@link #formatAsIdentifiers} is null.
+ *
+ * @see #setPositionCRS(CoordinateReferenceSystem)
+ */
+ private CoordinateReferenceSystem getPositionCRS() {
+ return format.getDefaultCRS();
+ }
+
/**
* Resets {@link #localToPositionCRS} to its default value. This is invoked either when the
* target CRS is {@link #objectiveCRS}, or when an attempt to use another CRS failed.
+ *
+ * @param textFill the color to assign to position text. It depends on the reason why we reset the position.
*/
private void resetPositionCRS(final Color textFill) {
objectiveToPositionCRS = null;
@@ -1037,8 +1161,8 @@ public class StatusBar extends Widget implements EventHandler<MouseEvent> {
/**
* Overwrite previous value without any check. This method is invoked when the {@link #objectiveCRS}
* is changed at the same time that the {@link #localToObjectiveCRS} transform, so the number of dimensions
- * may be temporarily mismatched. This method does not invoke {@link #updateLocalToPositionCRS()};
- * that call must be done by the caller when ready.
+ * may be temporarily mismatched. This method does not update {@link #localToPositionCRS};
+ * that update must be done by the caller when ready.
*/
final void setNoCheck(final MathTransform newValue) {
super.set(newValue);
@@ -1050,17 +1174,113 @@ public class StatusBar extends Widget implements EventHandler<MouseEvent> {
* @param newValue the new conversion from local coordinates to "real world" coordinates of rendered data.
* @throws MismatchedDimensionException if the number of dimensions is not the same than previous conversion.
*/
- @Override public void set(final MathTransform newValue) {
+ @Override public void set(MathTransform newValue) {
ArgumentChecks.ensureNonNull("newValue", newValue);
final MathTransform oldValue = get();
ArgumentChecks.ensureDimensionsMatch("newValue",
oldValue.getSourceDimensions(),
oldValue.getTargetDimensions(), newValue);
+ final MathTransform tr = objectiveToPositionCRS;
+ if (tr != null) {
+ newValue = MathTransforms.concatenate(newValue, tr);
+ }
+ localToPositionCRS = newValue;
super.set(newValue);
- updateLocalToPositionCRS();
}
}
+ /**
+ * Sets the reference system of the position shown in this status bar.
+ * This is similar to {@link #setPositionCRS(CoordinateReferenceSystem)} but for referencing by identifiers.
+ * This method tries to format the current position in a background thread because the first invocation of
+ * {@link ReferencingByIdentifiers.Coder#encode(DirectPosition)} may require an access to the EPSG database.
+ *
+ * @param system the new reference system (shall not be {@code null}).
+ */
+ private void setPositionRID(final ReferencingByIdentifiers system) {
+ resetPositionCRS(Styles.OUTDATED_TEXT);
+ final DirectPosition poi;
+ final MathTransform toObjective;
+ final CoordinateReferenceSystem crs = objectiveCRS;
+ final Quantity<Length> accuracy = lowestAccuracy.get();
+ if (Double.isFinite(lastX) && Double.isFinite(lastY)) {
+ poi = new DirectPosition2D(lastX, lastY);
+ toObjective = localToPositionCRS;
+ } else {
+ poi = (canvas != null) ? canvas.getPointOfInterest(true) : null;
+ toObjective = null;
+ }
+ cancelWorker();
+ BackgroundThreads.execute(worker = new Task<String>() {
+ /**
+ * The object to use for formatting identifiers. This is the value to assign
+ * to {@link StatusBar#formatAsIdentifiers} after successful task completion.
+ */
+ private ReferencingByIdentifiers.Coder coder;
+
+ /**
+ * Invoked in a background thread for formatting the identifier. The point to format is transformed
+ * from local coordinates to objective CRS. The CRS needs to be specified for allowing the coder to
+ * transform again the point to whatever internal CRS it needs for encoding purpose.
+ */
+ @Override protected String call() {
+ coder = system.createCoder();
+ try {
+ DirectPosition p = poi;
+ if (p != null && toObjective != null) {
+ p = toObjective.transform(p, new GeneralDirectPosition(crs));
+ }
+ if (accuracy != null) {
+ coder.setPrecision(accuracy, p);
+ }
+ if (p != null) {
+ return coder.encode(p);
+ }
+ } catch (IncommensurableException | TransformException e) {
+ recoverableException("setPositionRID", e);
+ }
+ return null;
+ }
+
+ /**
+ * Invoked in JavaFX thread for reporting a failure.
+ * The reference system in use stay the previous one.
+ */
+ @Override protected void failed() {
+ terminated(this);
+ setReferenceSystemError(system, getException());
+ }
+
+ /** Invoked in JavaFX thread on success for applying the actual reference system change. */
+ @Override protected void succeeded() {
+ terminated(this);
+ setPositionRID(coder, getValue(), (toObjective != null) ? poi : null);
+ }
+ });
+ }
+
+ /**
+ * Invoked after the background thread prepared the new reference system.
+ * The identifier formatted by the background thread is written, but needs to be rewritten
+ * again if {@link #lastX} or {@link #lastY} changed since background task execution.
+ *
+ * @param coder the coder to use for formatting identifiers.
+ * @param identifier identifier formatted using mouse position.
+ * @param current the local coordinates used for current text, or {@code null} if not valid.
+ *
+ * @see #setPositionCRS(OperationFinder, Quantity)
+ */
+ private void setPositionRID(final ReferencingByIdentifiers.Coder coder, final String identifier, final DirectPosition current) {
+ formatAsIdentifiers = coder;
+ fullOperationSearchRequired = null;
+ outsideText = null;
+ setErrorMessage(null, null);
+ setTooltip(coder.getReferenceSystem());
+ position.setText(identifier);
+ ((PositionSystem) positionReferenceSystem).fireValueChangedEvent();
+ rewritePosition(current);
+ }
+
/**
* Returns the indices of <var>x</var> and <var>y</var> coordinate values in a grid coordinate tuple.
* They are the indices where to assign the values of the <var>x</var> and <var>y</var> arguments in
@@ -1084,9 +1304,12 @@ public class StatusBar extends Widget implements EventHandler<MouseEvent> {
* @return the lowest accuracy to append after the coordinate values, or {@code null} if none.
*
* @see CoordinateFormat#getGroundAccuracy()
+ *
+ * @deprecated Replaced by {@link #lowestAccuracy}.
*/
+ @Deprecated
public Quantity<Length> getLowestAccuracy() {
- return lowestAccuracy;
+ return lowestAccuracy.get();
}
/**
@@ -1101,9 +1324,12 @@ public class StatusBar extends Widget implements EventHandler<MouseEvent> {
* @param accuracy the lowest accuracy to append after the coordinate values, or {@code null} if none.
*
* @see CoordinateFormat#setGroundAccuracy(Quantity)
+ *
+ * @deprecated Replaced by {@link #lowestAccuracy}.
*/
+ @Deprecated
public void setLowestAccuracy(final Quantity<Length> accuracy) {
- lowestAccuracy = accuracy;
+ lowestAccuracy.set(accuracy);
}
/**
@@ -1136,26 +1362,15 @@ public class StatusBar extends Widget implements EventHandler<MouseEvent> {
*/
public void setLocalCoordinates(final double x, final double y) {
if (x != lastX || y != lastY) {
- sourceCoordinates[xDimension] = lastX = x;
- sourceCoordinates[yDimension] = lastY = y;
- String text, values = null;
- try {
- convertCoordinates();
- if (isSampleValuesVisible) {
- values = sampleValuesProvider.get().evaluate(targetCoordinates);
- }
- targetCoordinates.normalize();
- text = format.format(targetCoordinates);
- } catch (TransformException | RuntimeException e) {
- Throwable cause = Exceptions.unwrap(e);
- text = cause.getLocalizedMessage();
- if (text == null) {
- text = Classes.getShortClassName(cause);
- }
- values = null;
- }
+ String text = formatLocalCoordinates(lastX = x, lastY = y);
position.setText(text);
if (isSampleValuesVisible) {
+ String values;
+ try {
+ values = sampleValuesProvider.get().evaluate(targetCoordinates);
+ } catch (RuntimeException e) {
+ values = cause(e);
+ }
sampleValues.setText(values);
}
/*
@@ -1170,17 +1385,17 @@ public class StatusBar extends Widget implements EventHandler<MouseEvent> {
}
/**
- * Converts the local coordinates currently stored in {@link #sourceCoordinates} array.
- * The conversion result is stored in {@link #targetCoordinates} and the {@link #format}
- * is configured with suggested precision. Callers can use this method as below:
+ * Unconditionally converts and formats the given local coordinates, but without modifying any control.
+ * It is caller's responsibility to either change the text shown in the status bar, or to use the returned
+ * text for something else (for example for copying in the clipboard).
*
- * {@preformat java
- * convertCoordinates();
- * targetCoordinates.normalize();
- * String text = format.format(targetCoordinates);
- * }
+ * @param x the <var>x</var> coordinate local to the view.
+ * @param y the <var>y</var> coordinate local to the view.
+ * @return string representation of coordinates or an error message.
*/
- private void convertCoordinates() throws TransformException {
+ private String formatLocalCoordinates(final double x, final double y) {
+ sourceCoordinates[xDimension] = x;
+ sourceCoordinates[yDimension] = y;
Matrix derivative;
try {
derivative = MathTransforms.derivativeAndTransform(localToPositionCRS,
@@ -1191,7 +1406,11 @@ public class StatusBar extends Widget implements EventHandler<MouseEvent> {
* derivative calculation. Try again without derivative (the precision will be set
* to the default resolution computed in `setCanvasGeometry(…)`).
*/
- localToPositionCRS.transform(sourceCoordinates, 0, targetCoordinates.coordinates, 0, 1);
+ try {
+ localToPositionCRS.transform(sourceCoordinates, 0, targetCoordinates.coordinates, 0, 1);
+ } catch (TransformException e) {
+ return cause(e);
+ }
derivative = null;
}
if (derivative == null) {
@@ -1218,23 +1437,43 @@ public class StatusBar extends Widget implements EventHandler<MouseEvent> {
precisions[j] = p;
}
}
- format.setPrecisions(precisions);
+ targetCoordinates.normalize();
+ /*
+ * Format as an identifier or as a coordinate tuple, depending on the type of the reference system.
+ * The precision is determined by the size of a pixel on screen and controls the number of fraction
+ * digits to print. Precision should not be confused with accuracy, which depends on transformation
+ * applied on coordinate values and determines the "± accuracy" text shown after coordinates.
+ */
+ try {
+ if (formatAsIdentifiers != null) {
+ double precision = 0;
+ for (int i = compatiblePrecisionCount; --i >= 0;) {
+ final double p = precisions[i];
+ if (p > precision) precision = p;
+ }
+ return formatAsIdentifiers.encode(targetCoordinates,
+ (precision > 0) ? Quantities.create(precision, precisionUnit) : null);
+ } else {
+ format.setPrecisions(precisions);
+ return format.format(targetCoordinates);
+ }
+ } catch (Exception e) {
+ return cause(e);
+ }
}
/**
* Converts and formats the given local coordinates, but without modifying text shown in this status bar.
+ * This is used for copying the coordinates somewhere else, for example on the clipboard.
*
* @param x the <var>x</var> coordinate local to the view.
* @param y the <var>y</var> coordinate local to the view.
*/
- final String formatCoordinates(final double x, final double y) throws TransformException {
- sourceCoordinates[xDimension] = x;
- sourceCoordinates[yDimension] = y;
+ final String formatTabSeparatedCoordinates(final double x, final double y) throws TransformException {
final String separator = format.getSeparator();
try {
format.setSeparator("\t");
- convertCoordinates();
- return format.format(targetCoordinates);
+ return formatLocalCoordinates(x, y);
} finally {
format.setSeparator(separator);
}
@@ -1353,6 +1592,23 @@ public class StatusBar extends Widget implements EventHandler<MouseEvent> {
return true;
}
+ /**
+ * Sets the tooltip text to show when the mouse cursor is over the coordinate values.
+ */
+ private void setTooltip(final ReferenceSystem crs) {
+ String text = IdentifiedObjects.getDisplayName(crs, getLocale());
+ Tooltip tp = null;
+ if (text != null) {
+ tp = position.getTooltip();
+ if (tp == null) {
+ tp = new Tooltip(text);
+ } else {
+ tp.setText(text);
+ }
+ }
+ position.setTooltip(tp);
+ }
+
/**
* Returns the message currently shown. It may be an error message or an informative message.
*
@@ -1405,17 +1661,10 @@ public class StatusBar extends Widget implements EventHandler<MouseEvent> {
text = Strings.trimOrNull(text);
Button more = null;
if (details != null) {
- final Locale locale = getLocale();
- if (text == null) {
- text = Exceptions.getLocalizedMessage(details, locale);
- if (text == null) {
- text = details.getClass().getSimpleName();
- }
- }
- final String alert = text;
+ final String alert = (text != null) ? text : cause(details);
more = new Button(Styles.ERROR_DETAILS_ICON);
more.setOnAction((e) -> ExceptionReporter.show(getView(),
- Resources.forLocale(locale).getString(Resources.Keys.ErrorDetails), alert, details));
+ Resources.forLocale(getLocale()).getString(Resources.Keys.ErrorDetails), alert, details));
}
message.setVisible(text != null);
message.setGraphic(more);
@@ -1423,6 +1672,24 @@ public class StatusBar extends Widget implements EventHandler<MouseEvent> {
message.setTextFill(Styles.ERROR_TEXT);
}
+ /**
+ * Shows an error message for a reference system that can not be set.
+ * The previous reference system is kept unchanged but the coordinates
+ * will appear in red for telling user that there is a problem.
+ *
+ * @param system the reference system that we failed to set.
+ * @param exception the exception that occurred while attempting to set the CRS.
+ */
+ private void setReferenceSystemError(final ReferenceSystem system, final Throwable exception) {
+ final Locale locale = getLocale();
+ setErrorMessage(Resources.forLocale(locale).getString(Resources.Keys.CanNotUseRefSys_1,
+ IdentifiedObjects.getDisplayName(system, locale)), exception);
+ if (selectedSystem != null) {
+ selectedSystem.set(positionReferenceSystem.get());
+ }
+ resetPositionCRS(Styles.ERROR_TEXT);
+ }
+
/**
* Shown an error message that occurred in the context of rendering the {@link #canvas} content.
* This method should not be invoked for other context like an error during transformation of
@@ -1435,4 +1702,30 @@ public class StatusBar extends Widget implements EventHandler<MouseEvent> {
}
setErrorMessage(text, details);
}
+
+ /**
+ * Returns a string representation of the message of the given exception.
+ * If the exception is a wrapper, the exception cause is taken.
+ * If there is no message, the exception class name is returned.
+ *
+ * @param e the exception.
+ * @return the exception message or class name.
+ */
+ private String cause(Throwable e) {
+ if (e instanceof Exception) {
+ e = Exceptions.unwrap((Exception) e);
+ }
+ String text = Exceptions.getLocalizedMessage(e, getLocale());
+ if (text == null) {
+ text = Classes.getShortClassName(e);
+ }
+ return text;
+ }
+
+ /**
+ * Logs an error considered too minor for reporting on the status bar.
+ */
+ private static void recoverableException(final String caller, final Exception e) {
+ Logging.recoverableException(Logger.getLogger(Modules.APPLICATION), StatusBar.class, caller, e);
+ }
}
diff --git a/application/sis-javafx/src/main/java/org/apache/sis/gui/referencing/MenuSync.java b/application/sis-javafx/src/main/java/org/apache/sis/gui/referencing/MenuSync.java
index 90e1e95642..cfa6888b3d 100644
--- a/application/sis-javafx/src/main/java/org/apache/sis/gui/referencing/MenuSync.java
+++ b/application/sis-javafx/src/main/java/org/apache/sis/gui/referencing/MenuSync.java
@@ -17,6 +17,7 @@
package org.apache.sis.gui.referencing;
import java.util.Arrays;
+import java.util.ArrayList;
import java.util.IdentityHashMap;
import java.util.Iterator;
import java.util.Locale;
@@ -33,8 +34,10 @@ import org.opengis.referencing.ReferenceSystem;
import org.opengis.referencing.crs.CoordinateReferenceSystem;
import org.apache.sis.internal.referencing.ReferencingUtilities;
import org.apache.sis.internal.gui.GUIUtilities;
+import org.apache.sis.internal.gui.Resources;
import org.apache.sis.referencing.IdentifiedObjects;
-import org.apache.sis.util.resources.Vocabulary;
+import org.apache.sis.referencing.gazetteer.GazetteerFactory;
+import org.apache.sis.referencing.gazetteer.GazetteerException;
import org.apache.sis.util.ComparisonMode;
import org.apache.sis.util.Utilities;
@@ -46,13 +49,18 @@ import org.apache.sis.util.Utilities;
* the selected reference system directly.
*
* @author Martin Desruisseaux (Geomatys)
- * @version 1.1
+ * @version 1.3
* @since 1.1
* @module
*/
final class MenuSync extends SimpleObjectProperty<ReferenceSystem> implements EventHandler<ActionEvent> {
/**
- * Keys where to store the reference system in {@link MenuItem}.
+ * The {@value} value, for identifying code that assume two-dimensional objects.
+ */
+ private static final int BIDIMENSIONAL = 2;
+
+ /**
+ * Keys where to store the reference system in {@link MenuItem} properties.
*/
private static final String REFERENCE_SYSTEM_KEY = "ReferenceSystem";
@@ -61,6 +69,11 @@ final class MenuSync extends SimpleObjectProperty<ReferenceSystem> implements Ev
*/
private static final String CHOOSER = "CHOOSER";
+ /**
+ * The list of reference systems to show as menu items.
+ */
+ private final ObservableList<? extends ReferenceSystem> systems;
+
/**
* The list of menu items to keep up-to-date with an {@code ObservableList<ReferenceSystem>}.
*/
@@ -72,12 +85,12 @@ final class MenuSync extends SimpleObjectProperty<ReferenceSystem> implements Ev
private final ToggleGroup group;
/**
- * The action to execute when a reference system is selected. This is not directly the user-specified action, but
- * rather an {@link org.apache.sis.gui.referencing.RecentReferenceSystems.Listener} instance wrapping that action.
- * This listener is invoked explicitly instead of using {@link SimpleObjectProperty} listeners because we do not
- * invoke it in all cases.
+ * The action to execute when a reference system is selected. This is not directly the user-specified action,
+ * but rather a {@link org.apache.sis.gui.referencing.RecentReferenceSystems.SelectionListener} instance wrapping
+ * that action. This listener is invoked explicitly instead of using {@link SimpleObjectProperty} listeners because
+ * we do not invoke it in all cases.
*/
- private final RecentReferenceSystems.Listener action;
+ private final RecentReferenceSystems.SelectionListener action;
/**
* Creates a new synchronization for the given list of menu items.
@@ -86,33 +99,35 @@ final class MenuSync extends SimpleObjectProperty<ReferenceSystem> implements Ev
* @param bean the menu to keep synchronized with the list of reference systems.
* @param action the user-specified action to execute when a reference system is selected.
*/
- MenuSync(final ObservableList<ReferenceSystem> systems, final Menu bean, final RecentReferenceSystems.Listener action) {
+ MenuSync(final ObservableList<ReferenceSystem> systems, final Menu bean, final RecentReferenceSystems.SelectionListener action) {
super(bean, "value");
- this.menus = bean.getItems();
- this.group = new ToggleGroup();
- this.action = action;
+ this.systems = systems;
+ this.menus = bean.getItems();
+ this.group = new ToggleGroup();
+ this.action = action;
/*
* We do not register listener for `systems` list.
- * Instead `notifyChanges(…)` will be invoked directly by RecentReferenceSystems.
+ * Instead `notifyChanges()` will be invoked directly by RecentReferenceSystems.
*/
final MenuItem[] items = new MenuItem[systems.size()];
+ final Locale locale = action.owner().locale;
for (int i=0; i<items.length; i++) {
- items[i] = createItem(systems.get(i));
+ items[i] = createItem(systems.get(i), locale);
}
menus.setAll(items);
- initialize(systems);
+ initialize();
}
/**
- * Sets the initial value to the first two-dimensional item in the {@code systems} list, if any.
+ * Sets the initial value to the first two-dimensional item in the {@link #systems} list, if any.
* This method is invoked in JavaFX thread at construction time or, if it didn't work,
* at some later time when the systems list may contain an element.
* This method should not be invoked anymore after initialization succeeded.
*/
- private void initialize(final ObservableList<? extends ReferenceSystem> systems) {
+ private void initialize() {
for (final ReferenceSystem system : systems) {
if (system instanceof CoordinateReferenceSystem) {
- if (ReferencingUtilities.getDimension((CoordinateReferenceSystem) system) == 2) {
+ if (ReferencingUtilities.getDimension((CoordinateReferenceSystem) system) == BIDIMENSIONAL) {
set(system);
break;
}
@@ -123,20 +138,40 @@ final class MenuSync extends SimpleObjectProperty<ReferenceSystem> implements Ev
/**
* Creates a new menu item for the given reference system.
*/
- private MenuItem createItem(final ReferenceSystem system) {
- final Locale locale = action.owner().locale;
- if (system != RecentReferenceSystems.OTHER) {
+ private MenuItem createItem(final ReferenceSystem system, final Locale locale) {
+ if (system == RecentReferenceSystems.OTHER) {
+ final MenuItem item = new MenuItem(ObjectStringConverter.other(locale));
+ item.getProperties().put(REFERENCE_SYSTEM_KEY, CHOOSER);
+ item.setOnAction(this);
+ return item;
+ } else {
final RadioMenuItem item = new RadioMenuItem(IdentifiedObjects.getDisplayName(system, locale));
item.getProperties().put(REFERENCE_SYSTEM_KEY, system);
item.setToggleGroup(group);
item.setOnAction(this);
return item;
- } else {
- final MenuItem item = new MenuItem(Vocabulary.getResources(locale).getString(Vocabulary.Keys.Others) + '…');
- item.getProperties().put(REFERENCE_SYSTEM_KEY, CHOOSER);
+ }
+ }
+
+ /**
+ * Creates new menu items for references system by identifiers, offered in a separated sub-menu.
+ * This list of reference system is fixed; items are not added or removed following user's selection.
+ */
+ final void addReferencingByIdentifiers() {
+ final Locale locale = action.owner().locale;
+ final GazetteerFactory factory = new GazetteerFactory();
+ final Resources resources = Resources.forLocale(locale);
+ final Menu menu = new Menu(resources.getString(Resources.Keys.ReferenceByIdentifiers));
+ for (final String name : factory.getSupportedNames()) try {
+ final ReferenceSystem system = factory.forName(name);
+ final MenuItem item = new MenuItem(IdentifiedObjects.getDisplayName(system, locale));
+ item.getProperties().put(REFERENCE_SYSTEM_KEY, system);
item.setOnAction(this);
- return item;
+ menu.getItems().add(item);
+ } catch (GazetteerException e) {
+ RecentReferenceSystems.errorOccurred("createMenuItems", e);
}
+ menus.add(menu);
}
/**
@@ -153,15 +188,20 @@ final class MenuSync extends SimpleObjectProperty<ReferenceSystem> implements Ev
* Invoked when the list of reference systems changed. While it would be possible to trace the permutations,
* additions, removals and replacements done on the list, it is easier to recreate the menu items list from
* scratch (with recycling of existing items) and inspect the differences.
+ *
+ * @see RecentReferenceSystems#notifyChanges()
*/
- final void notifyChanges(final ObservableList<? extends ReferenceSystem> systems) {
+ final void notifyChanges() {
/*
* Build a map of current menu items. Key are CRS objects.
*/
+ final var subMenus = new ArrayList<Menu>();
final Map<Object,MenuItem> mapping = new IdentityHashMap<>();
for (final Iterator<MenuItem> it = menus.iterator(); it.hasNext();) {
final MenuItem item = it.next();
- if (mapping.putIfAbsent(item.getProperties().get(REFERENCE_SYSTEM_KEY), item) != null) {
+ if (item instanceof Menu) {
+ subMenus.add((Menu) item);
+ } else if (mapping.putIfAbsent(item.getProperties().get(REFERENCE_SYSTEM_KEY), item) != null) {
it.remove(); // Remove duplicated item. Should never happen, but we are paranoiac.
dispose(item);
}
@@ -171,8 +211,9 @@ final class MenuSync extends SimpleObjectProperty<ReferenceSystem> implements Ev
* Other menu items are left to null for now; those null values may appear anywhere in the array. After this
* loop, the map will contain only menu items for CRS that are no longer in the list of CRS to offer.
*/
- final MenuItem[] items = new MenuItem[systems.size()];
- for (int i=0; i<items.length; i++) {
+ final int newCount = systems.size();
+ final MenuItem[] items = new MenuItem[newCount + subMenus.size()];
+ for (int i=0; i<newCount; i++) {
Object key = systems.get(i);
if (key == RecentReferenceSystems.OTHER) key = CHOOSER;
items[i] = mapping.remove(key);
@@ -184,21 +225,21 @@ final class MenuSync extends SimpleObjectProperty<ReferenceSystem> implements Ev
*/
ReferenceSystem selected = get();
final Iterator<MenuItem> recycle = mapping.values().iterator();
- for (int i=0; i<items.length; i++) {
+ final Locale locale = action.owner().locale;
+ for (int i=0; i<newCount; i++) {
if (items[i] == null) {
- MenuItem item;
+ MenuItem item = null;
final ReferenceSystem system = systems.get(i);
if (system != RecentReferenceSystems.OTHER && recycle.hasNext()) {
item = recycle.next();
recycle.remove();
if (item instanceof RadioMenuItem) {
- item.setText(IdentifiedObjects.getDisplayName(system, action.owner().locale));
+ item.setText(IdentifiedObjects.getDisplayName(system, locale));
item.getProperties().put(REFERENCE_SYSTEM_KEY, system);
- } else {
- item = createItem(system);
}
- } else {
- item = createItem(system);
+ }
+ if (item == null) {
+ item = createItem(system, locale);
}
if (selected != null && system == selected) {
((RadioMenuItem) item).setSelected(true); // ClassCastException should never occur here.
@@ -209,26 +250,30 @@ final class MenuSync extends SimpleObjectProperty<ReferenceSystem> implements Ev
}
/*
* If there is any item left, we must remove them from the ToggleGroup for avoiding memory leak.
+ * The sub-menus (if any) are appended last with no change.
*/
while (recycle.hasNext()) {
dispose(recycle.next());
}
+ for (int i=newCount; i<items.length; i++) {
+ items[i] = subMenus.get(i - newCount);
+ }
GUIUtilities.copyAsDiff(Arrays.asList(items), menus);
/*
* If we had no previously selected item, selects it now.
*/
if (get() == null) {
- initialize(systems);
+ initialize();
}
}
/**
- * Invoked when user selects a menu item. This method gets the old and new values and sends them
- * to {@link org.apache.sis.gui.referencing.RecentReferenceSystems.Listener} as a change event.
- * That {@code Listener} will update the list of reference systems, which may result in a callback
- * to {@link #notifyChanges(ObservableList)}. If the selected menu item is the "Other…" choice,
- * then {@code Listener} will popup {@link CRSChooser} and callback {@link #set(ReferenceSystem)}
- * for storing the result. Otherwise we need to invoke {@link #set(ReferenceSystem)} ourselves.
+ * Invoked when user selects a menu item. This method gets the old and new values and sends them to
+ * {@link org.apache.sis.gui.referencing.RecentReferenceSystems.SelectionListener} as a change event.
+ * That {@code SelectionListener} will update the list of reference systems, which may result
+ * in a callback to {@link #notifyChanges()}. If the selected menu item is the "Other…" choice,
+ * then {@code SelectionListener} will popup {@link CRSChooser} and callback {@link #set(ReferenceSystem)}
+ * for storing the result. Otherwise we need to invoke {@link #set(ReferenceSystem)} ourselves.
*/
@Override
public void handle(final ActionEvent event) {
diff --git a/application/sis-javafx/src/main/java/org/apache/sis/gui/referencing/ObjectStringConverter.java b/application/sis-javafx/src/main/java/org/apache/sis/gui/referencing/ObjectStringConverter.java
index a829270107..3c0be25271 100644
--- a/application/sis-javafx/src/main/java/org/apache/sis/gui/referencing/ObjectStringConverter.java
+++ b/application/sis-javafx/src/main/java/org/apache/sis/gui/referencing/ObjectStringConverter.java
@@ -20,14 +20,14 @@ import java.util.Locale;
import javafx.util.StringConverter;
import org.opengis.referencing.IdentifiedObject;
import org.apache.sis.referencing.IdentifiedObjects;
-import org.apache.sis.util.resources.Vocabulary;
+import org.apache.sis.internal.gui.Resources;
/**
* Converts an {@link IdentifiedObject} to {@link String} representation to show in JavaFX control.
*
* @author Martin Desruisseaux (Geomatys)
- * @version 1.1
+ * @version 1.3
* @since 1.1
* @module
*/
@@ -70,12 +70,20 @@ final class ObjectStringConverter<T extends IdentifiedObject> extends StringConv
return IdentifiedObjects.getDisplayName(object, locale);
} else {
if (other == null) {
- other = Vocabulary.getResources(locale).getString(Vocabulary.Keys.Others) + '…';
+ other = other(locale);
}
return other;
}
}
+ /**
+ * Returns the localized "Other…" text to use for selecting a CRS
+ * which is not in the short list of proposed CRS.
+ */
+ static String other(final Locale locale) {
+ return Resources.forLocale(locale).getString(Resources.Keys.OtherCRS) + '…';
+ }
+
/**
* Returns the object for the given name.
*
diff --git a/application/sis-javafx/src/main/java/org/apache/sis/gui/referencing/RecentReferenceSystems.java b/application/sis-javafx/src/main/java/org/apache/sis/gui/referencing/RecentReferenceSystems.java
index 41124f6201..04be43ee97 100644
--- a/application/sis-javafx/src/main/java/org/apache/sis/gui/referencing/RecentReferenceSystems.java
+++ b/application/sis-javafx/src/main/java/org/apache/sis/gui/referencing/RecentReferenceSystems.java
@@ -42,6 +42,8 @@ import org.apache.sis.geometry.ImmutableEnvelope;
import org.apache.sis.referencing.IdentifiedObjects;
import org.apache.sis.referencing.factory.GeodeticAuthorityFactory;
import org.apache.sis.referencing.factory.IdentifiedObjectFinder;
+import org.apache.sis.referencing.gazetteer.GazetteerException;
+import org.apache.sis.referencing.gazetteer.GazetteerFactory;
import org.apache.sis.coverage.grid.GridGeometry;
import org.apache.sis.util.ArgumentChecks;
import org.apache.sis.util.ComparisonMode;
@@ -166,12 +168,23 @@ public class RecentReferenceSystems {
*/
private static final class Unverified {
/** The reference system to verify. */
- final ReferenceSystem system;
+ private final ReferenceSystem system;
/** Flags the given reference system as unverified. */
Unverified(final ReferenceSystem system) {
this.system = system;
}
+
+ /** Returns the verified (if possible) reference system. */
+ ReferenceSystem find(final IdentifiedObjectFinder finder) throws FactoryException {
+ if (finder != null) {
+ final IdentifiedObject replacement = finder.findSingleton(system);
+ if (replacement instanceof ReferenceSystem) {
+ return (ReferenceSystem) replacement;
+ }
+ }
+ return system;
+ }
}
/**
@@ -194,16 +207,39 @@ public class RecentReferenceSystems {
* instances and duplicated values removed. This is the list given to JavaFX controls that we build.
* This list includes {@link #OTHER} as its last item.
*
- * @see #updateItems()
+ * <p>This list is initially null and created only when first needed. After the list has been created,
+ * this reference is never modified. As long as the reference is null, we can skip the synchronization
+ * of this list content with the {@link #systemsOrCodes} content when the latter changed. Because that
+ * synchronization may involve accesses to the EPSG database, it is potentially costly.</p>
+ *
+ * @see #getReferenceSystems(boolean)
*/
private ObservableList<ReferenceSystem> referenceSystems;
+ /**
+ * A view of {@link #referenceSystems} with only items that are instances of {@link CoordinateReferenceSystem}.
+ * This list includes also {@link #OTHER} as its last item. This list is used for menus shown in contexts where
+ * identifiers can not be used, for example for selecting the CRS to use for displaying a map.
+ *
+ * <p>This list is lazily created when first needed,
+ * because it depends on {@link #referenceSystems} which is itself lazily created.</p>
+ *
+ * @see #getReferenceSystems(boolean)
+ */
+ private ObservableList<ReferenceSystem> coordinateReferenceSystems;
+
/**
* A filtered view of {@link #referenceSystems} without the {@link #OTHER} item.
+ * This is the list returned to users by public API, but otherwise it is not used by this class.
+ * Instead, the lists used internally by this class contains the {@link #OTHER} item because
+ * those lists are used directly by controls like {@code ChoiceBox<ReferenceSystem>}.
+ *
+ * <p>This list is lazily created when first needed,
+ * because it depends on {@link #referenceSystems} which is itself lazily created.</p>
*
* @see #getItems()
*/
- private ObservableList<ReferenceSystem> filteredSystems;
+ private ObservableList<ReferenceSystem> publicItemList;
/**
* {@code true} if the {@link #referenceSystems} list needs to be rebuilt from {@link #systemsOrCodes} content.
@@ -215,7 +251,8 @@ public class RecentReferenceSystems {
/**
* {@code true} if {@code RecentReferenceSystems} is in the process of modifying {@link #referenceSystems} list.
- * In such case we want to temporarily disable the {@link Listener}. This field is read and updated in JavaFX thread.
+ * In such case we want to temporarily disable the {@link SelectionListener}.
+ * This field is read and updated in JavaFX thread.
*/
private boolean isAdjusting;
@@ -261,12 +298,16 @@ public class RecentReferenceSystems {
* @since 1.3
*/
public void configure(final GridGeometry gg) {
+ Envelope aoi = null;
if (gg != null) {
- areaOfInterest.set(gg.isDefined(GridGeometry.ENVELOPE) ? gg.getEnvelope() : null);
+ if (gg.isDefined(GridGeometry.ENVELOPE)) {
+ aoi = gg.getEnvelope();
+ }
if (gg.isDefined(GridGeometry.CRS)) {
setPreferred(true, gg.getCoordinateReferenceSystem());
}
}
+ areaOfInterest.set(aoi);
}
/**
@@ -382,7 +423,7 @@ public class RecentReferenceSystems {
/**
* Adds the coordinate reference systems saved in user preferences. The user preferences are determined
- * from the reference systems observed during current execution or previous execution of JavaFX application.
+ * from the reference systems observed during current execution or previous executions of JavaFX application.
* If an {@linkplain #areaOfInterest area of interest} (AOI) is specified,
* then reference systems that do not intersect the AOI will be ignored.
*/
@@ -402,6 +443,8 @@ public class RecentReferenceSystems {
* Filters the {@link #systemsOrCodes} list by making sure that it contains only {@link ReferenceSystem} instances.
* Authority codes are resolved if possible or removed if they can not be resolved. Unverified CRSs are compared
* with authoritative definitions and replaced when a match is found. Duplications are removed.
+ * Finally reference systems with a domain of validity outside the {@link #geographicAOI} are omitted
+ * from the returned list (but not removed from the original {@link #systemsOrCodes} list).
*
* <p>This method can be invoked from any thread. In practice, it is invoked from a background thread.</p>
*
@@ -411,6 +454,7 @@ public class RecentReferenceSystems {
*/
private List<ReferenceSystem> filterReferenceSystems(final ImmutableEnvelope domain, final ComparisonMode mode) {
final List<ReferenceSystem> systems;
+ final GazetteerFactory gf = new GazetteerFactory(); // Cheap to construct.
synchronized (systemsOrCodes) {
CRSAuthorityFactory factory = this.factory; // Hide volatile field by local field.
if (!isModified) {
@@ -419,54 +463,57 @@ public class RecentReferenceSystems {
boolean noFactoryFound = false;
boolean searchedFinder = false;
IdentifiedObjectFinder finder = null;
- for (int i=systemsOrCodes.size(); --i >= 0;) try {
+ for (int i=systemsOrCodes.size(); --i >= 0;) {
final Object item = systemsOrCodes.get(i);
- if (item == OTHER) {
- systemsOrCodes.remove(i);
- } else if (item instanceof String) {
- /*
- * The current list element is an authority code such as "EPSG::4326".
- * Replace that code by the full `CoordinateReferenceSystem` instance.
- * Note that authority factories are optional, so it is okay if we can
- * not resolve the code. In such case the item will be removed.
- */
- if (!noFactoryFound) {
- if (factory == null) {
- factory = Utils.getDefaultFactory();
- }
- systemsOrCodes.set(i, factory.createCoordinateReferenceSystem((String) item));
- } else {
- systemsOrCodes.remove(i);
- }
- } else if (item instanceof Unverified) {
- /*
- * The current list element is a `ReferenceSystem` instance but maybe not
- * conform to authoritative definition, for example regarding axis order.
- * If we can find an authoritative definition, do the replacement.
- * If this operation can not be done, accept the reference system as-is.
- */
- if (!searchedFinder) {
- searchedFinder = true; // Set now in case an exception is thrown.
- if (factory instanceof GeodeticAuthorityFactory) {
- finder = ((GeodeticAuthorityFactory) factory).newIdentifiedObjectFinder();
- } else {
- finder = IdentifiedObjects.newFinder(null);
+ if (item instanceof ReferenceSystem) {
+ continue;
+ }
+ ReferenceSystem system = null;
+ if (item != OTHER) try {
+ if (item instanceof String) {
+ /*
+ * The current list element is an authority code such as "EPSG::4326".
+ * Replace that code by the full `CoordinateReferenceSystem` instance.
+ * Note that authority factories are optional, so it is okay if we can
+ * not resolve the code. In such case the item will be removed.
+ */
+ system = gf.forNameIfKnown((String) item).orElse(null);
+ if (system == null && !noFactoryFound) {
+ if (factory == null) {
+ factory = Utils.getDefaultFactory();
+ }
+ system = factory.createCoordinateReferenceSystem((String) item);
}
- finder.setIgnoringAxes(true);
- }
- ReferenceSystem system = ((Unverified) item).system;
- if (finder != null) {
- final IdentifiedObject replacement = finder.findSingleton(system);
- if (replacement instanceof ReferenceSystem) {
- system = (ReferenceSystem) replacement;
+ } else if (item instanceof Unverified) {
+ /*
+ * The current list element is a `ReferenceSystem` instance but maybe not
+ * conform to authoritative definition, for example regarding axis order.
+ * If we can find an authoritative definition, do the replacement.
+ * If this operation can not be done, accept the reference system as-is.
+ */
+ if (!searchedFinder) {
+ searchedFinder = true; // Set now in case an exception is thrown.
+ if (factory instanceof GeodeticAuthorityFactory) {
+ finder = ((GeodeticAuthorityFactory) factory).newIdentifiedObjectFinder();
+ } else {
+ finder = IdentifiedObjects.newFinder(null);
+ }
+ finder.setIgnoringAxes(true);
}
+ system = ((Unverified) item).find(finder);
}
+ } catch (FactoryException e) {
+ errorOccurred(e);
+ noFactoryFound = (factory == null);
+ } catch (GazetteerException e) {
+ errorOccurred("getReferenceSystems", e);
+ // Note: `getReferenceSystems(…)` is indirectly the caller of this method.
+ }
+ if (system != null) {
systemsOrCodes.set(i, system);
+ } else {
+ systemsOrCodes.remove(i);
}
- } catch (FactoryException e) {
- errorOccurred(e);
- systemsOrCodes.remove(i);
- noFactoryFound = (factory == null);
}
/*
* Search for duplicated values after we finished filtering. This block is inefficient
@@ -499,7 +546,7 @@ public class RecentReferenceSystems {
* in a separated list as a protection against changes in `systemsOrCodes` list that
* could happen after this method returned, and also for retaining only the reference
* systems that are valid in the area of interest. We do not remove "invalid" CRS
- * because they would become valid later if the area of interest changes.
+ * because they may become valid later if the area of interest changes.
*/
final int n = systemsOrCodes.size();
systems = new ArrayList<>(Math.min(NUM_SHOWN_ITEMS, n) + NUM_OTHER_ITEMS);
@@ -531,7 +578,7 @@ public class RecentReferenceSystems {
isModified = true;
if (referenceSystems != null) {
// ChoiceBox or Menu already created. They will observe the changes in item list.
- updateItems();
+ getReferenceSystems(false);
}
}
}
@@ -541,10 +588,11 @@ public class RecentReferenceSystems {
* The new items may not be added immediately; instead the CRS will be processed in background thread
* and copied to the {@link #referenceSystems} list when ready.
*
+ * @param filtered whether to filter the list for retaining only {@link CoordinateReferenceSystem} instances.
* @return the list of items. May be empty on return and filled later.
*/
@SuppressWarnings("ReturnOfCollectionOrArrayField")
- private ObservableList<ReferenceSystem> updateItems() {
+ private ObservableList<ReferenceSystem> getReferenceSystems(final boolean filtered) {
if (referenceSystems == null) {
referenceSystems = FXCollections.observableArrayList();
}
@@ -589,9 +637,23 @@ public class RecentReferenceSystems {
}
});
}
+ if (filtered) {
+ if (coordinateReferenceSystems == null) {
+ coordinateReferenceSystems = new FilteredList<>(referenceSystems, RecentReferenceSystems::isCRS);
+ }
+ return coordinateReferenceSystems;
+ }
return referenceSystems;
}
+ /**
+ * Returns {@code true} if the given reference system can be included
+ * in the {@link #coordinateReferenceSystems} list.
+ */
+ private static boolean isCRS(final ReferenceSystem system) {
+ return (system == OTHER) || (system instanceof CoordinateReferenceSystem);
+ }
+
/**
* Sets the reference systems to the given content. The given list is often similar to current content,
* for example with only a reference system that moved to a different index. This method compares the
@@ -648,12 +710,12 @@ public class RecentReferenceSystems {
* and the selected reference system is added to the list of choices. If the selected CRS is different than
* the previous one, then {@link RecentChoices} is notified and the user-specified listener is notified.
*/
- final class Listener implements ChangeListener<ReferenceSystem> {
+ final class SelectionListener implements ChangeListener<ReferenceSystem> {
/** The user-specified action to execute when a reference system is selected. */
private final ChangeListener<ReferenceSystem> action;
/** Creates a new listener of reference system selection. */
- private Listener(final ChangeListener<ReferenceSystem> action) {
+ private SelectionListener(final ChangeListener<ReferenceSystem> action) {
this.action = action;
}
@@ -771,7 +833,7 @@ public class RecentReferenceSystems {
private void notifyChanges() {
for (final WritableValue<ReferenceSystem> value : controlValues) {
if (value instanceof MenuSync) {
- ((MenuSync) value).notifyChanges(referenceSystems);
+ ((MenuSync) value).notifyChanges();
}
}
}
@@ -785,10 +847,10 @@ public class RecentReferenceSystems {
*/
@SuppressWarnings("ReturnOfCollectionOrArrayField")
public ObservableList<ReferenceSystem> getItems() {
- if (filteredSystems == null) {
- filteredSystems = new FilteredList<>(updateItems(), Objects::nonNull);
+ if (publicItemList == null) {
+ publicItemList = new FilteredList<>(getReferenceSystems(false), Objects::nonNull);
}
- return filteredSystems;
+ return publicItemList;
}
/**
@@ -854,15 +916,27 @@ next: for (int i=0; i<count; i++) {
* The returned control may be initially empty, in which case its content will be automatically set at
* a later time (after a background thread finished to process the {@link CoordinateReferenceSystem}s).
*
- * @param action the action to execute when a reference system is selected.
+ * <p>If the {@code filtered} argument is {@code true}, then the choice box will contain only reference systems
+ * that can be used for rendering purposes. That filtered list can contain {@link CoordinateReferenceSystem}
+ * instances but not reference systems by identifiers such as {@linkplain MilitaryGridReferenceSystem MGRS}.
+ * The latter are usable only for the purposes of formatting coordinate values as texts.</p>
+ *
+ * <h4>Limitations</h4>
+ * There is currently no mechanism for disposing the returned control. For garbage collecting the
+ * returned {@code ChoiceBox}, this {@code RecentReferenceSystems} must be garbage-collected as well.
+ *
+ * @param filtered whether the choice box should contain only {@link CoordinateReferenceSystem} instances.
+ * @param action the action to execute when a reference system is selected.
* @return a choice box with reference systems specified by {@code setPreferred(…)}
* and {@code addAlternatives(…)} methods.
+ *
+ * @since 1.3
*/
- public ChoiceBox<ReferenceSystem> createChoiceBox(final ChangeListener<ReferenceSystem> action) {
+ public ChoiceBox<ReferenceSystem> createChoiceBox(final boolean filtered, final ChangeListener<ReferenceSystem> action) {
ArgumentChecks.ensureNonNull("action", action);
- final ChoiceBox<ReferenceSystem> choices = new ChoiceBox<>(updateItems());
+ final ChoiceBox<ReferenceSystem> choices = new ChoiceBox<>(getReferenceSystems(filtered));
choices.setConverter(new ObjectStringConverter<>(choices.getItems(), locale));
- choices.valueProperty().addListener(new Listener(action));
+ choices.valueProperty().addListener(new SelectionListener(action));
controlValues.add(choices.valueProperty());
return choices;
}
@@ -872,18 +946,49 @@ next: for (int i=0; i<count; i++) {
* The items will be inserted in the {@linkplain Menu#getItems() menu list}. The content of that list will
* change at any time after this method returned: items will be added or removed as a result of user actions.
*
- * @param action the action to execute when a reference system is selected.
+ * <p>If the {@code filtered} argument is {@code true}, then the menu items will contain only reference systems
+ * that can be used for rendering purposes. That filtered list can contain {@link CoordinateReferenceSystem}
+ * instances but not reference systems by identifiers such as {@linkplain MilitaryGridReferenceSystem MGRS}.
+ * The latter are usable only for the purposes of formatting coordinate values as texts.</p>
+ *
+ * <h4>Limitations</h4>
+ * There is currently no mechanism for disposing the returned control. For garbage collecting the
+ * returned {@code Menu}, this {@code RecentReferenceSystems} must be garbage-collected as well.
+ *
+ * @param filtered whether the menu should contain only {@link CoordinateReferenceSystem} instances.
+ * @param action the action to execute when a reference system is selected.
* @return the menu containing items for reference systems.
+ *
+ * @since 1.3
*/
- public Menu createMenuItems(final ChangeListener<ReferenceSystem> action) {
+ public Menu createMenuItems(final boolean filtered, final ChangeListener<ReferenceSystem> action) {
ArgumentChecks.ensureNonNull("action", action);
final Menu menu = new Menu(Vocabulary.getResources(locale).getString(Vocabulary.Keys.ReferenceSystem));
- final MenuSync property = new MenuSync(updateItems(), menu, new Listener(action));
+ final MenuSync property = new MenuSync(getReferenceSystems(filtered), menu, new SelectionListener(action));
+ if (!filtered) {
+ property.addReferencingByIdentifiers();
+ }
menu.getProperties().put(SELECTED_ITEM_KEY, property);
controlValues.add(property);
return menu;
}
+ /**
+ * @deprecated Replaced by {@link #createChoiceBox(boolean, ChangeListener)}.
+ */
+ @Deprecated
+ public ChoiceBox<ReferenceSystem> createChoiceBox(final ChangeListener<ReferenceSystem> action) {
+ return createChoiceBox(true, action);
+ }
+
+ /**
+ * @deprecated Replaced by {@link #createMenuItems(boolean, ChangeListener)}.
+ */
+ @Deprecated
+ public Menu createMenuItems(final ChangeListener<ReferenceSystem> action) {
+ return createMenuItems(true, action);
+ }
+
/**
* Returns the property for the selected value in a menu created by {@link #createMenuItems(ChangeListener)}.
*
@@ -917,6 +1022,17 @@ next: for (int i=0; i<count; i++) {
*/
protected void errorOccurred(final FactoryException e) {
OptionalDataDownloader.reportIfInstalling(e);
- Logging.recoverableException(getLogger(Modules.APPLICATION), RecentReferenceSystems.class, "updateItems", e);
+ Logging.recoverableException(getLogger(Modules.APPLICATION), RecentReferenceSystems.class, "getReferenceSystems", e);
+ }
+
+ /**
+ * Invoked when an error occurred while fetching a reference system be identifier.
+ * This is the complement of {@link #errorOccurred(FactoryException)} but for referencing by identifiers.
+ *
+ * @param caller the method to report as the source the in log record.
+ * @param e the error that occurred.
+ */
+ static void errorOccurred(final String caller, final GazetteerException e) {
+ Logging.recoverableException(getLogger(Modules.APPLICATION), RecentReferenceSystems.class, caller, e);
}
}
diff --git a/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/Resources.java b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/Resources.java
index 5877c7c3a3..d1f17be4f3 100644
--- a/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/Resources.java
+++ b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/Resources.java
@@ -332,6 +332,11 @@ public final class Resources extends IndexedResourceBundle {
*/
public static final short Orthographic = 52;
+ /**
+ * Other coordinate reference system…
+ */
+ public static final short OtherCRS = 72;
+
/**
* Property value
*/
@@ -342,6 +347,11 @@ public final class Resources extends IndexedResourceBundle {
*/
public static final short RangeOfValues = 56;
+ /**
+ * Reference system by identifiers
+ */
+ public static final short ReferenceByIdentifiers = 73;
+
/**
* Select a coordinate reference system
*/
diff --git a/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/Resources.properties b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/Resources.properties
index fb4077b072..6da5d3e06e 100644
--- a/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/Resources.properties
+++ b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/Resources.properties
@@ -75,8 +75,10 @@ OpenContainingFolder = Open containing folder
OpenDataFile = Open data file
OpenRecentFile = Open recent file
Orthographic = Orthographic
+OtherCRS = Other coordinate reference system\u2026
PropertyValue = Property value
RangeOfValues = Range of values\u2026
+ReferenceByIdentifiers = Reference system by identifiers
SelectCRS = Select a coordinate reference system
SelectCrsByContextMenu = For changing the projection, use contextual menu on the map.
SelectParentLogger = Select parent logger
diff --git a/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/Resources_fr.properties b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/Resources_fr.properties
index 678d1a296e..dc879df892 100644
--- a/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/Resources_fr.properties
+++ b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/Resources_fr.properties
@@ -80,8 +80,10 @@ OpenContainingFolder = Ouvrir le dossier contenant
OpenDataFile = Ouvrir un fichier de donn\u00e9es
OpenRecentFile = Ouvrir un fichier r\u00e9cent
Orthographic = Orthographique
+OtherCRS = Autre syst\u00e8me de r\u00e9f\u00e9rence par coordonn\u00e9es\u2026
PropertyValue = Valeur de la propri\u00e9t\u00e9
RangeOfValues = Plage de valeurs\u2026
+ReferenceByIdentifiers = Syst\u00e8me de r\u00e9f\u00e9rence par identifiants
SelectCRS = Choisir un syst\u00e8me de r\u00e9f\u00e9rence des coordonn\u00e9es
SelectCrsByContextMenu = Pour changer la projection, utilisez le menu contextuel sur la carte.
SelectParentLogger = Choisir le journal parent