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 <[email protected]> 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
