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 2020/04/19 21:47:38 UTC
[sis] branch geoapi-4.0 updated: First draft of a `ChoiceBox` for
choosing a CRS in a list of more recently used CRS.
This is an automated email from the ASF dual-hosted git repository.
desruisseaux pushed a commit to branch geoapi-4.0
in repository https://gitbox.apache.org/repos/asf/sis.git
The following commit(s) were added to refs/heads/geoapi-4.0 by this push:
new 1918205 First draft of a `ChoiceBox` for choosing a CRS in a list of more recently used CRS.
1918205 is described below
commit 191820582121738879f6841243ee2e72bb0c2f04
Author: Martin Desruisseaux <ma...@geomatys.com>
AuthorDate: Sun Apr 19 23:43:27 2020 +0200
First draft of a `ChoiceBox` for choosing a CRS in a list of more recently used CRS.
---
.../main/java/org/apache/sis/gui/DataViewer.java | 1 +
.../java/org/apache/sis/gui/coverage/Controls.java | 12 +-
.../apache/sis/gui/coverage/CoverageControls.java | 46 +-
.../apache/sis/gui/coverage/CoverageExplorer.java | 36 +-
.../org/apache/sis/gui/coverage/GridControls.java | 2 +-
.../org/apache/sis/gui/referencing/CRSChooser.java | 2 +-
.../sis/gui/referencing/ObjectStringConverter.java | 110 ++++
.../gui/referencing/RecentReferenceSystems.java | 654 +++++++++++++++++++++
.../org/apache/sis/gui/referencing/WKTPane.java | 2 +-
.../apache/sis/internal/gui/ExceptionReporter.java | 10 +
.../org/apache/sis/internal/gui/GUIUtilities.java | 197 +++++++
.../org/apache/sis/internal/gui/RecentChoices.java | 87 +++
.../java/org/apache/sis/internal/gui/Styles.java | 3 +-
.../apache/sis/internal/gui/GUIUtilitiesTest.java | 45 ++
.../sis/test/suite/ApplicationTestSuite.java | 45 ++
.../java/org/apache/sis/util/CharSequences.java | 2 +-
ide-project/NetBeans/nbproject/build-impl.xml | 22 +-
ide-project/NetBeans/nbproject/genfiles.properties | 4 +-
ide-project/NetBeans/nbproject/project.properties | 1 +
ide-project/NetBeans/nbproject/project.xml | 2 +
20 files changed, 1252 insertions(+), 31 deletions(-)
diff --git a/application/sis-javafx/src/main/java/org/apache/sis/gui/DataViewer.java b/application/sis-javafx/src/main/java/org/apache/sis/gui/DataViewer.java
index b3d28f6..0188c41 100644
--- a/application/sis-javafx/src/main/java/org/apache/sis/gui/DataViewer.java
+++ b/application/sis-javafx/src/main/java/org/apache/sis/gui/DataViewer.java
@@ -229,6 +229,7 @@ public class DataViewer extends Application {
@Override
public void stop() throws Exception {
BackgroundThreads.stop();
+ RecentChoices.saveReferenceSystems();
super.stop();
}
}
diff --git a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/Controls.java b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/Controls.java
index 39e6ee3..00764a8 100644
--- a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/Controls.java
+++ b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/Controls.java
@@ -46,6 +46,11 @@ abstract class Controls {
static final Insets CAPTION_MARGIN = new Insets(12, 0, 9, 0);
/**
+ * Margin to keep around captions after the first one.
+ */
+ static final Insets NEXT_CAPTION_MARGIN = new Insets(30, 0, 9, 0);
+
+ /**
* The border to use for grouping some controls together.
*/
private static final Border GROUP_BORDER = new Border(new BorderStroke(
@@ -110,10 +115,11 @@ abstract class Controls {
abstract Control controls();
/**
- * Invoked after {@link CoverageExplorer#setCoverage(ImageRequest)} for updating the table of
- * sample dimensions when information become available. This method is invoked in JavaFX thread.
+ * Invoked in JavaFX thread after {@link CoverageExplorer#setCoverage(ImageRequest)} completed.
+ * Implementation should update the GUI with new information available, in particular
+ * the coordinate reference system and the list of sample dimensions.
*
* @param data the new coverage, or {@code null} if none.
*/
- abstract void updateBandTable(GridCoverage data);
+ abstract void coverageChanged(GridCoverage data);
}
diff --git a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/CoverageControls.java b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/CoverageControls.java
index f38ddc7..177b944 100644
--- a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/CoverageControls.java
+++ b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/CoverageControls.java
@@ -26,9 +26,14 @@ import javafx.scene.layout.GridPane;
import javafx.scene.layout.Region;
import javafx.scene.layout.VBox;
import javafx.beans.property.ObjectProperty;
+import javafx.beans.value.ObservableValue;
+import javafx.scene.control.ChoiceBox;
import javafx.scene.paint.Color;
-import org.apache.sis.gui.map.StatusBar;
+import org.opengis.referencing.ReferenceSystem;
import org.apache.sis.coverage.grid.GridCoverage;
+import org.apache.sis.gui.referencing.RecentReferenceSystems;
+import org.apache.sis.gui.map.StatusBar;
+import org.apache.sis.internal.gui.Resources;
import org.apache.sis.util.resources.Vocabulary;
@@ -57,12 +62,20 @@ final class CoverageControls extends Controls {
private final BorderPane imageAndStatus;
/**
+ * The coordinate reference system selected in the {@link ChoiceBox}.
+ */
+ private final ObjectProperty<ReferenceSystem> referenceSystem;
+
+ /**
* Creates a new set of coverage controls.
*
+ * @param localized localized GUI resources, provided in argument because often known by the caller.
* @param vocabulary localized set of words, provided in argument because often known by the caller.
* @param coverage property containing the coverage to show.
*/
- CoverageControls(final Vocabulary vocabulary, final ObjectProperty<GridCoverage> coverage) {
+ CoverageControls(final Resources localized, final Vocabulary vocabulary,
+ final ObjectProperty<GridCoverage> coverage, final RecentReferenceSystems referenceSystems)
+ {
final Color background = Color.BLACK;
view = new CoverageCanvas();
view.setBackground(background);
@@ -72,18 +85,25 @@ final class CoverageControls extends Controls {
imageAndStatus.setBottom(statusBar.getView());
/*
* "Display" section with the following controls:
+ * - Coordinate reference system
* - Background color
*/
final VBox displayPane;
{ // Block for making variables locale to this scope.
+ final ChoiceBox<ReferenceSystem> systems = referenceSystems.createChoiceBox(this::onReferenceSystemSelected);
+ systems.setMaxWidth(Double.POSITIVE_INFINITY);
+ referenceSystem = systems.valueProperty();
+ final Label systemLabel = new Label(localized.getString(Resources.Keys.ReferenceSystem));
+ systemLabel.setPadding(CAPTION_MARGIN);
+ systemLabel.setLabelFor(systems);
final GridPane gp = createControlGrid(
label(vocabulary, Vocabulary.Keys.Background, createBackgroundButton(background)),
label(vocabulary, Vocabulary.Keys.ValueRange, RangeType.createButton((p,o,n) -> view.setRangeType(n)))
);
final Label label = new Label(vocabulary.getLabel(Vocabulary.Keys.Image));
- label.setPadding(CAPTION_MARGIN);
+ label.setPadding(NEXT_CAPTION_MARGIN);
label.setLabelFor(gp);
- displayPane = new VBox(label, gp);
+ displayPane = new VBox(systemLabel, systems, label, gp);
}
/*
* Put all sections together and have the first one expanded by default.
@@ -108,13 +128,25 @@ final class CoverageControls extends Controls {
}
/**
- * Invoked after {@link CoverageExplorer#setCoverage(ImageRequest)} for updating the table of
- * sample dimensions when information become available. This method is invoked in JavaFX thread.
+ * Invoked in JavaFX thread after {@link CoverageExplorer#setCoverage(ImageRequest)} completed.
+ * This method updates the GUI with new information available, in particular
+ * the coordinate reference system and the list of sample dimensions.
*
* @param data the new coverage, or {@code null} if none.
*/
@Override
- final void updateBandTable(final GridCoverage data) {
+ final void coverageChanged(final GridCoverage data) {
+ if (data != null) {
+ referenceSystem.set(data.getCoordinateReferenceSystem());
+ }
+ }
+
+ /**
+ * Invoked when a new coordinate reference system is selected.
+ */
+ private void onReferenceSystemSelected(final ObservableValue<? extends ReferenceSystem> property,
+ final ReferenceSystem oldValue, ReferenceSystem newValue)
+ {
}
/**
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 194eab8..987191a 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
@@ -27,10 +27,12 @@ import javafx.beans.value.ObservableValue;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import org.apache.sis.coverage.grid.GridCoverage;
+import org.apache.sis.coverage.grid.GridGeometry;
import org.apache.sis.internal.gui.Resources;
import org.apache.sis.internal.gui.ToolbarButton;
import org.apache.sis.internal.gui.NonNullObjectProperty;
import org.apache.sis.util.resources.Vocabulary;
+import org.apache.sis.gui.referencing.RecentReferenceSystems;
import org.apache.sis.gui.Widget;
@@ -135,6 +137,11 @@ public class CoverageExplorer extends Widget {
private final Controls[] views;
/**
+ * Handles the {@link javafx.scene.control.ChoiceBox} and menu items for selecting a CRS.
+ */
+ private final RecentReferenceSystems referenceSystems;
+
+ /**
* Creates an initially empty explorer.
*/
public CoverageExplorer() {
@@ -142,6 +149,9 @@ public class CoverageExplorer extends Widget {
viewTypeProperty = new NonNullObjectProperty<>(this, "viewType", View.TABLE);
coverageProperty.addListener(this::onCoverageSpecified);
viewTypeProperty.addListener(this::onViewTypeSpecified);
+ referenceSystems = new RecentReferenceSystems();
+ referenceSystems.addUserPreferences();
+ referenceSystems.addAlternatives("EPSG:3395"); // WGS 84 / World Mercator
/*
* Prepare buttons to add on the toolbar. Those buttons are not managed by this class;
* they are managed by org.apache.sis.gui.dataset.DataWindow. We only declare here the
@@ -162,7 +172,7 @@ public class CoverageExplorer extends Widget {
final Controls c;
switch (type) {
case TABLE: c = new GridControls(vocabulary); break;
- case IMAGE: c = new CoverageControls(vocabulary, coverageProperty); break;
+ case IMAGE: c = new CoverageControls(localized, vocabulary, coverageProperty, referenceSystems); break;
default: throw new AssertionError(type);
}
SplitPane.setResizableWithParent(c.controls(), Boolean.FALSE);
@@ -257,7 +267,9 @@ public class CoverageExplorer extends Widget {
}
/**
- * Invoked when a new coverage has been specified.
+ * Invoked when a new coverage has been set on the {@link #coverageProperty}.
+ * This method notifies the GUI controls about the change then starts loading
+ * data in a background thread.
*
* @param property the {@link #coverageProperty} (ignored).
* @param previous ignored.
@@ -268,7 +280,7 @@ public class CoverageExplorer extends Widget {
{
if (!isCoverageAdjusting) {
startLoading(null); // Clear data.
- updateBandTable(coverage);
+ notifyCoverageChange(coverage);
if (coverage != null) {
startLoading(new ImageRequest(coverage, null)); // Start a background thread.
}
@@ -284,7 +296,7 @@ public class CoverageExplorer extends Widget {
* @param coverage the new coverage, or {@code null} if loading failed.
*/
final void onCoverageLoaded(final GridCoverage coverage) {
- updateBandTable(coverage);
+ notifyCoverageChange(coverage);
isCoverageAdjusting = true;
try {
setCoverage(coverage);
@@ -305,14 +317,22 @@ public class CoverageExplorer extends Widget {
}
/**
- * Invoked after {@link #setCoverage(ImageRequest)} for updating the table of sample dimensions
- * with information become available. This method is invoked in JavaFX thread.
+ * Invoked in JavaFX thread after {@link #setCoverage(ImageRequest)} completion for notifying controls
+ * about the coverage change. Controls should update the GUI with new information available,
+ * in particular the coordinate reference system and the list of sample dimensions.
*
* @param data the new coverage, or {@code null} if none.
*/
- private void updateBandTable(final GridCoverage data) {
+ private void notifyCoverageChange(final GridCoverage data) {
+ if (data != null) {
+ final GridGeometry gg = data.getGridGeometry();
+// referenceSystems.areaOfInterest.set(gg.isDefined(GridGeometry.ENVELOPE) ? gg.getEnvelope() : null);
+ if (gg.isDefined(GridGeometry.CRS)) {
+ referenceSystems.setPreferred(true, gg.getCoordinateReferenceSystem());
+ }
+ }
for (final Controls c : views) {
- c.updateBandTable(data);
+ c.coverageChanged(data);
}
}
diff --git a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/GridControls.java b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/GridControls.java
index a4a477b..a1e8098 100644
--- a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/GridControls.java
+++ b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/GridControls.java
@@ -135,7 +135,7 @@ final class GridControls extends Controls {
* @param data the new coverage, or {@code null} if none.
*/
@Override
- final void updateBandTable(final GridCoverage data) {
+ final void coverageChanged(final GridCoverage data) {
final ObservableList<SampleDimension> items = sampleDimensions.getItems();
if (data != null) {
items.setAll(data.getSampleDimensions());
diff --git a/application/sis-javafx/src/main/java/org/apache/sis/gui/referencing/CRSChooser.java b/application/sis-javafx/src/main/java/org/apache/sis/gui/referencing/CRSChooser.java
index e606fc7..e3f98a1 100644
--- a/application/sis-javafx/src/main/java/org/apache/sis/gui/referencing/CRSChooser.java
+++ b/application/sis-javafx/src/main/java/org/apache/sis/gui/referencing/CRSChooser.java
@@ -427,7 +427,7 @@ public class CRSChooser extends Dialog<CoordinateReferenceSystem> {
/**
* Shows a dialog to select a {@link CoordinateReferenceSystem}.
*
- * @param parent parent frame of dialog.
+ * @param parent parent frame of dialog, or {@code null} for an unowned dialog.
* @return the selected {@link CoordinateReferenceSystem}, or empty if none.
*/
public Optional<CoordinateReferenceSystem> showDialog(final Window parent) {
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
new file mode 100644
index 0000000..88f1d71
--- /dev/null
+++ b/application/sis-javafx/src/main/java/org/apache/sis/gui/referencing/ObjectStringConverter.java
@@ -0,0 +1,110 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sis.gui.referencing;
+
+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;
+
+
+/**
+ * Converts an {@link IdentifiedObject} to {@link String} representation to shown in JavaFX control.
+ *
+ * @author Martin Desruisseaux (Geomatys)
+ * @version 1.1
+ * @since 1.1
+ * @module
+ */
+final class ObjectStringConverter<T extends IdentifiedObject> extends StringConverter<T> {
+ /**
+ * The set of items that user can choose.
+ */
+ private final Iterable<? extends T> items;
+
+ /**
+ * The preferred locale for displaying object name, or {@code null} for the default locale.
+ */
+ private final Locale locale;
+
+ /**
+ * The localized "Other…" string.
+ */
+ private String other;
+
+ /**
+ * Creates a new converter.
+ *
+ * @param items the set of items that user can choose.
+ * @param locale the preferred locale for displaying object name, or {@code null} for the default locale.
+ */
+ ObjectStringConverter(final Iterable<? extends T> items, final Locale locale) {
+ this.items = items;
+ this.locale = locale;
+ }
+
+ /**
+ * Returns the display name of the given object.
+ *
+ * @param object the object for which to get a string representation.
+ * @return the display name of the given object, or {@code null} if none.
+ */
+ @Override
+ public String toString(final T object) {
+ if (object != RecentReferenceSystems.OTHER) {
+ return IdentifiedObjects.getDisplayName(object, locale);
+ } else {
+ if (other == null) {
+ other = Vocabulary.getResources(locale).getString(Vocabulary.Keys.Others) + '…';
+ }
+ return other;
+ }
+ }
+
+ /**
+ * Returns the object for the given name.
+ *
+ * @param name name of desired object (may be {@code null}).
+ * @return the desired object, or {@code null} if not found.
+ */
+ @Override
+ public T fromString(final String name) {
+ if (name != null) {
+ T fallback = null;
+ for (final T item : items) {
+ final String candidate = toString(item);
+ if (name.equals(candidate)) {
+ return item;
+ }
+ if (fallback == null && name.equalsIgnoreCase(candidate)) {
+ fallback = item;
+ }
+ }
+ if (fallback != null) {
+ return fallback;
+ }
+ // Check heuristic match only if no exact math was found.
+ for (final T item : items) {
+ if (IdentifiedObjects.isHeuristicMatchForName(item, name)) {
+ return item;
+ }
+ }
+ }
+ return null;
+ }
+}
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
new file mode 100644
index 0000000..6bff9f8
--- /dev/null
+++ b/application/sis-javafx/src/main/java/org/apache/sis/gui/referencing/RecentReferenceSystems.java
@@ -0,0 +1,654 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sis.gui.referencing;
+
+import java.util.List;
+import java.util.ArrayList;
+import javafx.beans.InvalidationListener;
+import javafx.beans.property.ObjectProperty;
+import javafx.beans.property.SimpleObjectProperty;
+import javafx.beans.value.ChangeListener;
+import javafx.beans.value.ObservableValue;
+import javafx.beans.value.WritableValue;
+import javafx.collections.FXCollections;
+import javafx.collections.ObservableList;
+import javafx.scene.control.ChoiceBox;
+import javafx.scene.control.MenuItem;
+import javafx.concurrent.Task;
+import org.opengis.util.FactoryException;
+import org.opengis.geometry.Envelope;
+import org.opengis.referencing.ReferenceSystem;
+import org.opengis.referencing.IdentifiedObject;
+import org.opengis.referencing.crs.CRSAuthorityFactory;
+import org.opengis.referencing.crs.CoordinateReferenceSystem;
+import org.opengis.metadata.extent.GeographicBoundingBox;
+import org.apache.sis.metadata.iso.extent.Extents;
+import org.apache.sis.geometry.ImmutableEnvelope;
+import org.apache.sis.referencing.CRS;
+import org.apache.sis.referencing.IdentifiedObjects;
+import org.apache.sis.referencing.factory.GeodeticAuthorityFactory;
+import org.apache.sis.referencing.factory.IdentifiedObjectFinder;
+import org.apache.sis.util.ArgumentChecks;
+import org.apache.sis.util.ComparisonMode;
+import org.apache.sis.util.Utilities;
+import org.apache.sis.util.logging.Logging;
+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.NonNullObjectProperty;
+import org.apache.sis.internal.gui.RecentChoices;
+import org.apache.sis.internal.system.Modules;
+import org.apache.sis.internal.util.Strings;
+
+
+/**
+ * A short list (~10 items) of most recently used {@link ReferenceSystem}s.
+ * The list can be shown in a {@link ChoiceBox} or in a list of {@link MenuItem} controls.
+ * The last choice is an "Other…" item which, when selected, popups the {@link CRSChooser}.
+ *
+ * <p>The choices are listed in following order:</p>
+ * <ul>
+ * <li>The first choice is the native or preferred reference system of visualized data.
+ * That choice stay always in the first position.</li>
+ * <li>The last choice is "Other…" and stay always in the last position.</li>
+ * <li>All other choices between first and last are ordered with most recently used first.</li>
+ * </ul>
+ *
+ * @author Martin Desruisseaux (Geomatys)
+ * @version 1.1
+ * @since 1.1
+ * @module
+ */
+public class RecentReferenceSystems {
+ /**
+ * The authority of the {@link #factory} (for example "EPSG"),
+ * or {@code null} for all authorities known to SIS.
+ */
+ private static final String AUTHORITY = null;
+
+ /**
+ * Number of reference systems to always show before all other reference systems.
+ * They are the native of preferred reference system for the visualized data.
+ */
+ private static final int NUM_CORE_SYSTEMS = 1;
+
+ /**
+ * Number of reference systems to shown in {@link ChoiceBox} or {@link MenuItem}s.
+ * The {@value #NUM_CORE_SYSTEMS} core systems are included but not {@link #OTHER}.
+ */
+ private static final int NUM_SHOWN_SYSTEMS = 9;
+
+ /**
+ * Number of reference systems to keep at the end of the list.
+ */
+ private static final int NUM_OTHER_SYSTEMS = 1;
+
+ /**
+ * A pseudo-reference system for the "Other…" choice. We use a null value because {@link ChoiceBox}
+ * seems to insist for inserting a null value in the items list when we remove the selected item.
+ */
+ static final ReferenceSystem OTHER = null;
+
+ /**
+ * The factory to use for creating a Coordinate Reference System from an authority code.
+ * If {@code null}, then the {@linkplain CRS#getAuthorityFactory(String) default factory}
+ * will be fetched when first needed.
+ */
+ private volatile CRSAuthorityFactory factory;
+
+ /**
+ * The area of interest, or {@code null} if none. This is used for filtering the reference systems added by
+ * {@code addAlternatives(…)} and for providing some guidance to user when {@link CRSChooser} is shown.
+ */
+ public final ObjectProperty<Envelope> areaOfInterest;
+
+ /**
+ * The comparison criterion for considering two reference systems as a duplication.
+ * The default value is {@link ComparisonMode#ALLOW_VARIANT}, i.e. axis orders are ignored.
+ */
+ public final ObjectProperty<ComparisonMode> duplicationCriterion;
+
+ /**
+ * Values of controls created by this {@code RecentReferenceSystems} instance. We retain those properties
+ * because modifying the {@link #referenceSystems} list sometime causes controls to clear their selection
+ * if we removed the selected item from the list. We use {@code controlValues} for saving currently selected
+ * values before to modify the item list, and restore selections after we finished to modify the list.
+ */
+ private final List<ObjectProperty<ReferenceSystem>> controlValues;
+
+ /**
+ * Wrapper for a {@link ReferenceSystem} which has not yet been compared with authoritative definitions.
+ * Those wrappers are created when {@link ReferenceSystem} instances have been specified to {@code setPreferred(…)}
+ * or {@code addAlternatives(…)} methods with {@code replaceByAuthoritativeDefinition} argument set to {@code true}.
+ *
+ * @see #setPreferred(boolean, ReferenceSystem)
+ * @see #addAlternatives(boolean, ReferenceSystem...)
+ */
+ private static final class Unverified {
+ /** The reference system to verify. */
+ final ReferenceSystem system;
+
+ /** Flags the given reference system as unverified. */
+ Unverified(final ReferenceSystem system) {
+ this.system = system;
+ }
+ }
+
+ /**
+ * The reference systems either as {@link ReferenceSystem} instances, {@link Unverified} wrappers or
+ * {@link String} codes. All {@code String} elements should be authority codes that {@link #factory}
+ * can recognize. The first item in this list should be the native or preferred reference system.
+ * The {@link #OTHER} reference system is <em>not</em> included in this list.
+ *
+ * <p>The list content is specified by calls to {@code setPreferred(…)} and {@code addAlternatives(…)} methods,
+ * then is filtered by {@link #filterSystems(ImmutableEnvelope, ComparisonMode)} for resolving authority codes
+ * and removing duplicated elements.</p>
+ *
+ * <p>All accesses to this field and to {@link #isModified} field shall be done in a block synchronized
+ * on {@code systemsOrCodes}.</p>
+ */
+ private final List<Object> systemsOrCodes;
+
+ /**
+ * The {@link #systemsOrCodes} elements with all codes or wrappers replaced by {@link ReferenceSystem}
+ * 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.
+ */
+ private ObservableList<ReferenceSystem> referenceSystems;
+
+ /**
+ * {@code true} if the {@link #referenceSystems} list needs to be rebuilt from {@link #systemsOrCodes} content.
+ * This field shall be read and modified in a block synchronized on {@link #systemsOrCodes}.
+ *
+ * @see #modified()
+ */
+ private boolean isModified;
+
+ /**
+ * {@code true} if {@code RecentReferenceSystems} is in the process of modifying {@link #referenceSystems} list.
+ * In such we want to temporarily disable the {@link Listener}. This field is read and updated in JavaFX thread.
+ */
+ private boolean isAdjusting;
+
+ /**
+ * Creates a builder which will use the {@linkplain CRS#getAuthorityFactory(String) default authority factory}.
+ */
+ public RecentReferenceSystems() {
+ systemsOrCodes = new ArrayList<>();
+ areaOfInterest = new SimpleObjectProperty<>(this, "areaOfInterest");
+ duplicationCriterion = new NonNullObjectProperty<>(this, "duplicationCriterion", ComparisonMode.ALLOW_VARIANT);
+ controlValues = new ArrayList<>();
+ final InvalidationListener pl = (e) -> modified();
+ areaOfInterest.addListener(pl);
+ duplicationCriterion.addListener(pl);
+ }
+
+ /**
+ * Creates a builder which will use the specified authority factory.
+ *
+ * @param factory the factory to use for building CRS from authority codes.
+ *
+ * @see CRS#getAuthorityFactory(String)
+ */
+ public RecentReferenceSystems(final CRSAuthorityFactory factory) {
+ this();
+ ArgumentChecks.ensureNonNull("factory", factory);
+ this.factory = factory;
+ }
+
+ /**
+ * Sets the native or preferred reference system. This is the system to always show as the first
+ * choice and should typically be the native {@link CoordinateReferenceSystem} of visualized data.
+ * If a previous preferred system existed, the previous system will be moved to alternative choices.
+ *
+ * <p>The {@code replaceByAuthoritativeDefinition} argument specifies whether the given reference system should
+ * be replaced by authoritative definition. If {@code true} then for example a <cite>"WGS 84"</cite> geographic
+ * CRS with (<var>longitude</var>, <var>latitude</var>) axis order may be replaced by "EPSG::4326" definition with
+ * (<var>latitude</var>, <var>longitude</var>) axis order.</p>
+ *
+ * @param replaceByAuthoritativeDefinition whether the given system should be replaced by authoritative definition.
+ * @param system the native or preferred reference system to show as the first choice.
+ */
+ public void setPreferred(final boolean replaceByAuthoritativeDefinition, final ReferenceSystem system) {
+ ArgumentChecks.ensureNonNull("system", system);
+ synchronized (systemsOrCodes) {
+ systemsOrCodes.add(0, replaceByAuthoritativeDefinition ? new Unverified(system) : system);
+ modified();
+ }
+ }
+
+ /**
+ * Sets the native or preferred reference system as an authority code. This is the system to always show as
+ * the first choice and should typically be the native {@link CoordinateReferenceSystem} of visualized data.
+ * If a previous preferred system existed, the previous system will be moved to alternative choices.
+ *
+ * <p>If the given code is not recognized, then the error will be notified at some later time by a call to
+ * {@link #errorOccurred(FactoryException)} in a background thread and the given code will be silently ignored.
+ * This behavior allows the use of codes that depend on whether an optional dependency is present or not,
+ * in particular the <a href="https://sis.apache.org/epsg.html">EPSG dataset</a>.</p>
+ *
+ * @param code authority code of the native of preferred reference system to show as the first choice.
+ */
+ public void setPreferred(final String code) {
+ ArgumentChecks.ensureNonEmpty("code", code);
+ synchronized (systemsOrCodes) {
+ systemsOrCodes.add(0, code);
+ modified();
+ }
+ }
+
+ /**
+ * Adds the given reference systems to the list of alternative choices.
+ * If there is duplicated values in the given list or with previously added systems,
+ * then only the first occurrence of duplicated values is retained.
+ * If an {@linkplain #areaOfInterest area of interest} (AOI) is specified,
+ * then reference systems that do not intersect the AOI will be hidden.
+ *
+ * <p>The {@code replaceByAuthoritativeDefinition} argument specifies whether the given reference systems should
+ * be replaced by authoritative definitions. If {@code true} then for example a <cite>"WGS 84"</cite> geographic
+ * CRS with (<var>longitude</var>, <var>latitude</var>) axis order may be replaced by "EPSG::4326" definition with
+ * (<var>latitude</var>, <var>longitude</var>) axis order.</p>
+ *
+ * @param replaceByAuthoritativeDefinition whether the given systems should be replaced by authoritative definitions.
+ * @param systems the reference systems to add as alternative choices. Null elements are ignored.
+ */
+ public void addAlternatives(final boolean replaceByAuthoritativeDefinition, final ReferenceSystem... systems) {
+ ArgumentChecks.ensureNonNull("systems", systems);
+ synchronized (systemsOrCodes) {
+ for (final ReferenceSystem system : systems) {
+ if (system != null) {
+ systemsOrCodes.add(replaceByAuthoritativeDefinition ? new Unverified(system) : system);
+ }
+ }
+ modified();
+ }
+ // Check for duplication will be done in `filterSystems()` method.
+ }
+
+ /**
+ * Adds the coordinate reference system identified by the given authority codes.
+ * If there is duplicated values in the given list or with previously added systems,
+ * then only the first occurrence of duplicated values is retained.
+ * If an {@linkplain #areaOfInterest area of interest} (AOI) is specified,
+ * then reference systems that do not intersect the AOI will be hidden.
+ *
+ * <p>If a code is not recognized, then the error will be notified at some later time by a call to
+ * {@link #errorOccurred(FactoryException)} in a background thread and the code will be silently ignored.
+ * This behavior allows the use of codes that depend on whether an optional dependency is present or not,
+ * in particular the <a href="https://sis.apache.org/epsg.html">EPSG dataset</a>.</p>
+ *
+ * @param codes authority codes of the coordinate reference systems to add as alternative choices.
+ * Null or empty elements are ignored.
+ */
+ public void addAlternatives(final String... codes) {
+ ArgumentChecks.ensureNonNull("codes", codes);
+ synchronized (systemsOrCodes) {
+ for (String code : codes) {
+ code = Strings.trimOrNull(code);
+ if (code != null) {
+ systemsOrCodes.add(code);
+ }
+ }
+ modified();
+ }
+ // Parsing will be done in `filterSystems()` method.
+ }
+
+ /**
+ * 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.
+ * If an {@linkplain #areaOfInterest area of interest} (AOI) is specified,
+ * then reference systems that do not intersect the AOI will be ignored.
+ */
+ public void addUserPreferences() {
+ addAlternatives(RecentChoices.getReferenceSystems());
+ }
+
+ /**
+ * 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.
+ *
+ * <p>This method can be invoked from any thread.</p>
+ *
+ * @param domain the {@link #areaOfInterest} value read from JavaFX thread, or {@code null} if none.
+ * @param mode the {@link #duplicationCriterion} value read from JavaFX thread.
+ * @return the filtered reference systems, or {@code null} if already filtered.
+ */
+ private List<ReferenceSystem> filterSystems(final ImmutableEnvelope domain, final ComparisonMode mode) {
+ final List<ReferenceSystem> systems;
+ synchronized (systemsOrCodes) {
+ CRSAuthorityFactory factory = this.factory; // Hide volatile field by local field.
+ if (!isModified) {
+ return null; // Another thread already did the work.
+ }
+ boolean noFactoryFound = false;
+ boolean searchedFinder = false;
+ IdentifiedObjectFinder finder = null;
+ for (int i=systemsOrCodes.size(); --i >= 0;) try {
+ 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 = CRS.getAuthorityFactory(AUTHORITY);
+ }
+ 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(AUTHORITY);
+ }
+ finder.setIgnoringAxes(true);
+ }
+ ReferenceSystem system = ((Unverified) item).system;
+ if (finder != null) {
+ final IdentifiedObject replacement = finder.findSingleton(system);
+ if (replacement instanceof ReferenceSystem) {
+ system = (ReferenceSystem) replacement;
+ }
+ }
+ systemsOrCodes.set(i, system);
+ }
+ } catch (FactoryException e) {
+ errorOccurred(e);
+ systemsOrCodes.remove(i);
+ noFactoryFound = (factory == null);
+ }
+ /*
+ * Search for duplicated values after we finished filtering. This block is inefficient
+ * (execution time of O(N²)) but it should not be an issue if this list is short (e.g.
+ * 20 elements). We cut the list if we reach the maximal amount of systems to keep.
+ */
+ for (int i=0,j; i < (j=systemsOrCodes.size()); i++) {
+ if (i >= RecentChoices.MAXIMUM_REFERENCE_SYSTEMS) {
+ systemsOrCodes.subList(i, j).clear();
+ break;
+ }
+ final Object item = systemsOrCodes.get(i);
+ while (--j > i) {
+ if (Utilities.deepEquals(item, systemsOrCodes.get(j), mode)) {
+ systemsOrCodes.remove(j);
+ }
+ }
+ }
+ /*
+ * Finished to filter the `systemsOrCodes` list: all elements are now guaranteed to be
+ * `ReferenceSystem` instances with no duplicated values. Copy those reference systems
+ * 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.
+ */
+ final int n = systemsOrCodes.size();
+ systems = new ArrayList<>(Math.min(NUM_SHOWN_SYSTEMS, n) + NUM_OTHER_SYSTEMS);
+ for (int i=0; i<n; i++) {
+ final ReferenceSystem system = (ReferenceSystem) systemsOrCodes.get(i);
+ if (i >= NUM_CORE_SYSTEMS && domain != null) {
+ final GeographicBoundingBox bbox = Extents.getGeographicBoundingBox(system.getDomainOfValidity());
+ if (bbox != null && !domain.intersects(new ImmutableEnvelope(bbox))) {
+ continue;
+ }
+ }
+ systems.add(system);
+ if (systems.size() >= NUM_SHOWN_SYSTEMS) break;
+ }
+ systems.add(OTHER);
+ isModified = false;
+ this.factory = factory; // Save in volatile field.
+ }
+ return systems;
+ }
+
+ /**
+ * Invoked when {@link #systemsOrCodes} has been modified. If the modification happens after
+ * some controls have been created ({@link ChoiceBox} or {@link MenuItem}s), then this method
+ * updates their list of items. The update may happen at some time after this method returned.
+ */
+ private void modified() {
+ synchronized (systemsOrCodes) {
+ isModified = true;
+ if (referenceSystems != null) {
+ updateItems();
+ }
+ }
+ }
+
+ /**
+ * Updates {@link #referenceSystems} with the reference systems added to {@link #systemsOrCodes} list.
+ * 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.
+ *
+ * @return the list of items. May be empty on return and filled later.
+ */
+ @SuppressWarnings("ReturnOfCollectionOrArrayField")
+ private ObservableList<ReferenceSystem> updateItems() {
+ if (referenceSystems == null) {
+ referenceSystems = FXCollections.observableArrayList();
+ }
+ synchronized (systemsOrCodes) {
+ systemsOrCodes.addAll(Math.min(systemsOrCodes.size(), NUM_CORE_SYSTEMS), referenceSystems);
+ // Duplicated values will be filtered by the background task below.
+ isModified = true;
+ final ImmutableEnvelope domain = ImmutableEnvelope.castOrCopy(areaOfInterest.get());
+ final ComparisonMode mode = duplicationCriterion.get();
+ BackgroundThreads.execute(new Task<List<ReferenceSystem>>() {
+ /** Filters the {@link ReferenceSystem}s in a background thread. */
+ @Override protected List<ReferenceSystem> call() {
+ return filterSystems(domain, mode);
+ }
+
+ /** Should never happen. */
+ @Override protected void failed() {
+ ExceptionReporter.show(this);
+ }
+
+ /** Sets the {@link ChoiceBox} content to the list computed in background thread. */
+ @Override protected void succeeded() {
+ setReferenceSystems(getValue(), mode);
+ }
+ });
+ }
+ return referenceSystems;
+ }
+
+ /**
+ * 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
+ * given list with current one and tries to fire as few change events as possible.
+ *
+ * @param systems the new reference systems, or {@code null} for no changes.
+ * @param mode the value of {@link #duplicationCriterion} at the time the
+ * {@code systems} list has been computed.
+ */
+ private void setReferenceSystems(final List<ReferenceSystem> systems, final ComparisonMode mode) {
+ if (systems != null) {
+ /*
+ * The call to `copyAsDiff(…)` may cause `ChoiceBox` values to be lost if the corresponding item
+ * in the `referenceSystems` list is temporarily removed (before to be inserted elsewhere).
+ * Save the values before to modify the list.
+ */
+ final ReferenceSystem[] values = controlValues.stream().map(ObjectProperty::get).toArray(ReferenceSystem[]::new);
+ try {
+ isAdjusting = true;
+ GUIUtilities.copyAsDiff(systems, referenceSystems);
+ } finally {
+ isAdjusting = false;
+ }
+ /*
+ * Restore the previous selections. This code also serves another purpose: the previous selection
+ * may not be an item in the list. If the value was set by a call to `ChoiceBox.setValue(…)` and
+ * is a `GeographicCRS` with (λ,φ) axis order, it may have been replaced in the list by a CRS with
+ * (φ,λ) axis order. We need to replace the previous value by the instance in the list, otherwise
+ * `ChoiceBox` will not show the CRS as selected.
+ */
+ final int n = referenceSystems.size();
+ for (int j=0; j<values.length; j++) {
+ ReferenceSystem system = values[j];
+ for (int i=0; i<n; i++) {
+ final ReferenceSystem candidate = referenceSystems.get(i);
+ if (Utilities.deepEquals(candidate, system, mode)) {
+ system = candidate;
+ break;
+ }
+ }
+ controlValues.get(j).set(system);
+ }
+ }
+ }
+
+ /**
+ * Invoked when user selects a reference system. If the choice is "Other…", then {@link CRSChooser} popups
+ * 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.
+ */
+ private final class Listener 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. */
+ Listener(final ChangeListener<ReferenceSystem> action) {
+ this.action = action;
+ }
+
+ /** Invoked when the user selects a reference system or the "Other…" item. */
+ @SuppressWarnings("unchecked")
+ @Override public void changed(final ObservableValue<? extends ReferenceSystem> property,
+ final ReferenceSystem oldValue, ReferenceSystem newValue)
+ {
+ if (isAdjusting) {
+ return;
+ }
+ if (newValue == OTHER) {
+ final CRSChooser chooser = new CRSChooser(factory, areaOfInterest.get());
+ newValue = chooser.showDialog(GUIUtilities.getWindow(property)).orElse(null);
+ if (newValue == null) {
+ newValue = oldValue;
+ } else {
+ final ObservableList<ReferenceSystem> items = referenceSystems;
+ final ComparisonMode mode = duplicationCriterion.get();
+ final int count = items.size() - NUM_OTHER_SYSTEMS;
+ boolean found = false;
+ for (int i=0; i<count; i++) {
+ if (Utilities.deepEquals(newValue, items.get(i), mode)) {
+ if (i >= NUM_CORE_SYSTEMS) {
+ items.set(i, newValue);
+ }
+ found = true;
+ break;
+ }
+ }
+ if (!found) {
+ if (count >= NUM_SHOWN_SYSTEMS) {
+ items.remove(count - 1); // Remove the last item before `OTHER`.
+ }
+ items.add(Math.min(count, NUM_CORE_SYSTEMS), newValue);
+ }
+ }
+ /*
+ * Following cast is safe because this listener is registered only on ObjectProperty
+ * instances, and the ObjectProperty class implements WritableValue.
+ */
+ ((WritableValue<ReferenceSystem>) property).setValue(newValue);
+ }
+ if (oldValue != newValue) {
+ /*
+ * Notify the user-specified listener first. It will typically starts a background process.
+ * If an exception occurs in that user code, the list of CRS choices will be left unchanged.
+ */
+ action.changed(property, oldValue, newValue);
+ RecentChoices.useReferenceSystem(IdentifiedObjects.toString(IdentifiedObjects.getIdentifier(newValue, null)));
+ /*
+ * Move the selected reference system as the first choice after the core systems.
+ * We need to remove the old value before to add the new one, otherwise it seems
+ * to confuse the list.
+ */
+ final ObservableList<ReferenceSystem> items = referenceSystems;
+ final int count = items.size() - NUM_OTHER_SYSTEMS;
+ for (int i=Math.min(count, NUM_CORE_SYSTEMS + 1); --i >= 0;) {
+ if (items.get(i) == newValue) {
+ return;
+ }
+ }
+ for (int i=count; --i >= NUM_CORE_SYSTEMS;) {
+ if (items.get(i) == newValue) {
+ items.remove(i);
+ break;
+ }
+ }
+ items.add(Math.max(0, Math.min(count, NUM_CORE_SYSTEMS)), newValue);
+ }
+ }
+ }
+
+ /**
+ * Creates a box offering choices among the reference systems specified to this {@code ShortChoiceList}.
+ * 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.
+ * @return a choice box with reference systems specified by {@code setPreferred(…)}
+ * and {@code addAlternatives(…)} methods.
+ */
+ public ChoiceBox<ReferenceSystem> createChoiceBox(final ChangeListener<ReferenceSystem> action) {
+ ArgumentChecks.ensureNonNull("action", action);
+ final ChoiceBox<ReferenceSystem> choices = new ChoiceBox<>(updateItems());
+ choices.setConverter(new ObjectStringConverter<>(choices.getItems(), null));
+ choices.valueProperty().addListener(new Listener(action));
+ controlValues.add(choices.valueProperty());
+ return choices;
+ }
+
+ public MenuItem[] createMenuItems() {
+ return null;
+ }
+
+ /**
+ * Invoked when an error occurred while filtering a {@link ReferenceSystem} instance.
+ * The error may be a failure to convert an EPSG code to a {@link CoordinateReferenceSystem} instance,
+ * or an error during a CRS verification. Some errors may be normal, for example because EPSG dataset
+ * is not expected to be present in every runtime environments. The consequence of this error is "only"
+ * that the CRS will not be listed among the reference systems that the user can choose.
+ *
+ * <p>The default implementation log the error at {@link java.util.logging.Level#FINE}.
+ * No other processing is done; user is not notified unless (s)he paid attention to loggings.</p>
+ *
+ * @param e the error that occurred.
+ */
+ protected void errorOccurred(final FactoryException e) {
+ Logging.recoverableException(Logging.getLogger(Modules.APPLICATION), RecentReferenceSystems.class, "updateItems", e);
+ }
+}
diff --git a/application/sis-javafx/src/main/java/org/apache/sis/gui/referencing/WKTPane.java b/application/sis-javafx/src/main/java/org/apache/sis/gui/referencing/WKTPane.java
index c09ebac..384f136 100644
--- a/application/sis-javafx/src/main/java/org/apache/sis/gui/referencing/WKTPane.java
+++ b/application/sis-javafx/src/main/java/org/apache/sis/gui/referencing/WKTPane.java
@@ -179,7 +179,7 @@ final class WKTPane extends StringConverter<Convention> implements ChangeListene
}
/**
- * Sets the content to the given coordianate reference system.
+ * Sets the content to the given coordinate reference system.
*/
private void setContent(final CoordinateReferenceSystem newCRS) {
text.setEditable(false); // TODO: make editable if we allow WKT parsing in a future version.
diff --git a/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/ExceptionReporter.java b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/ExceptionReporter.java
index 8e0c617..d8902ad 100644
--- a/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/ExceptionReporter.java
+++ b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/ExceptionReporter.java
@@ -18,6 +18,7 @@ package org.apache.sis.internal.gui;
import java.io.PrintWriter;
import java.io.StringWriter;
+import javafx.concurrent.Task;
import javafx.concurrent.Worker;
import javafx.concurrent.WorkerStateEvent;
import javafx.event.ActionEvent;
@@ -212,6 +213,15 @@ public final class ExceptionReporter {
}
/**
+ * Constructs and shows the exception reporter for the given task.
+ *
+ * @param task the task that failed.
+ */
+ public static void show(final Task<?> task) {
+ show(task.getTitle(), null, task.getException());
+ }
+
+ /**
* Invoked when the user selected the "Copy" action in contextual menu.
*
* @param event ignored.
diff --git a/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/GUIUtilities.java b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/GUIUtilities.java
new file mode 100644
index 0000000..952c784
--- /dev/null
+++ b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/GUIUtilities.java
@@ -0,0 +1,197 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sis.internal.gui;
+
+import java.util.List;
+import java.util.ArrayList;
+import java.util.Collections;
+import javafx.beans.property.ObjectProperty;
+import javafx.beans.value.ObservableValue;
+import javafx.collections.ObservableList;
+import javafx.scene.Node;
+import javafx.scene.Scene;
+import javafx.stage.Window;
+import org.apache.sis.util.Static;
+
+
+/**
+ * Miscellaneous utility methods.
+ *
+ * @author Martin Desruisseaux (Geomatys)
+ * @version 1.1
+ * @since 1.1
+ * @module
+ */
+public final class GUIUtilities extends Static {
+ /**
+ * Do not allow instantiation of this class.
+ */
+ private GUIUtilities() {
+ }
+
+ /**
+ * Returns the window of the bean associated to the given property.
+ *
+ * @param property the property for which to get the window where it appear.
+ * @return the window, or {@code null} if unknown.
+ */
+ public static Window getWindow(final ObservableValue<?> property) {
+ if (property instanceof ObjectProperty<?>) {
+ final Object bean = ((ObjectProperty<?>) property).getBean();
+ if (bean instanceof Node) {
+ final Scene scene = ((Node) bean).getScene();
+ if (scene != null) {
+ return scene.getWindow();
+ }
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Copies all elements from the given source list to the specified target list,
+ * but with the application of insertion and removal operations only.
+ * This method is useful when the two lists should be similar.
+ * The intend is to causes as few change events as possible.
+ *
+ * @param <E> type of elements to copy.
+ * @param source the list of elements to copy in the target.
+ * @param target the list to modify with as few operations as possible.
+ */
+ @SuppressWarnings("empty-statement")
+ public static <E> void copyAsDiff(final List<? extends E> source, final ObservableList<E> target) {
+ if (source.isEmpty()) {
+ target.clear();
+ return;
+ }
+ if (target.isEmpty()) {
+ target.setAll(source);
+ return;
+ }
+ final List<E> lcs = longestCommonSubsequence(source, target);
+ /*
+ * Remove elements before to add new ones, because some listeners
+ * seem to be confused when a list contains duplicated elements
+ * (the removed elements may be inserted elsewhere).
+ */
+ int upper = target.size();
+ for (int i = lcs.size(); --i >= 0;) {
+ final E keep = lcs.get(i);
+ int lower = upper;
+ while (target.get(--lower) != keep); // A negative index here would be a bug in LCS computation.
+ if (lower + 1 < upper) {
+ target.remove(lower + 1, upper);
+ }
+ upper = lower;
+ }
+ if (upper != 0) {
+ target.remove(0, upper);
+ }
+ assert lcs.equals(target); // Because we removed all elements that were not present in LCS.
+ /*
+ * Now insert the new elements. We move forward for reducing the
+ * number of elements that `ObservableList` will have to shift.
+ * (We moved backward in the removal phase for the same reason).
+ */
+ int lower = 0;
+ for (int i=0; i<target.size(); i++) {
+ final E skip = target.get(i);
+ upper = lower;
+ while (source.get(upper) != skip) upper++; // An index out of bounds would be a bug in LCS computation.
+ if (lower < upper) {
+ target.addAll(i, source.subList(lower, upper));
+ i += upper - lower;
+ }
+ lower = upper + 1;
+ }
+ upper = source.size();
+ if (lower < upper) {
+ target.addAll(source.subList(lower, upper));
+ }
+ assert source.equals(target);
+ }
+
+ /**
+ * Returns the longest subsequence common to both specified sequences.
+ * This is known as <cite>longest common subsequence</cite> (LCS) problem.
+ * The LCS elements are not required to occupy consecutive positions within the original sequences.
+ *
+ * <div class="note"><b>Example:</b>
+ * for the two following lists <var>x</var> and <var>y</var>,
+ * the longest common subsequence if given by <var>lcs</var> below:
+ *
+ * {@preformat text
+ * x : 1 2 4 6 7 9
+ * y : 1 2 3 7 8
+ * lcs : 1 2 7
+ * }
+ * </div>
+ *
+ * This algorithm is useful for computing the differences between two sequences.
+ *
+ * @param <E> the type of elements in the sequences.
+ * @param x the first sequence for which to compute LCS.
+ * @param y the second sequence for which to compute LCS.
+ * @return longest common subsequence (LCS) between the two given sequences.
+ *
+ * <a href="https://en.wikipedia.org/wiki/Longest_common_subsequence_problem">Longest common subsequence problem</a>
+ */
+ static <E> List<E> longestCommonSubsequence(final List<? extends E> x, final List<? extends E> y) {
+ /*
+ * This method could be optimized by excluding the common prefix and common suffix before to build the
+ * matrix below. For now we don't do that because the given lists are small. But we should revisit in
+ * the future if this method become used with longer sequences.
+ */
+ int nx = x.size();
+ int ny = y.size();
+ /*
+ * We need a matrix of size (nx x ny) for storing LCS lengths for all (x[i], y[j]) pairs of elements.
+ * The matrix is augmented by one row and one column where all values in the first row and first column
+ * are zero. We could omit that row and that column for saving space, but it would complexify this code.
+ * For now we don't do that, but we may revisit in the future if this code is used for longer sequences.
+ */
+ final int[][] lengths = new int[nx + 1][ny + 1];
+ for (int i=1; i<=nx; i++) {
+ final int im = i - 1;
+ final E xim = x.get(im);
+ for (int j=1; j<=ny; j++) {
+ final int jm = j - 1;
+ lengths[i][j] = (y.get(jm) == xim)
+ ? Math.incrementExact(lengths[im][jm])
+ : Math.max(lengths[i][jm], lengths[im][j]);
+ }
+ }
+ /*
+ * The last cell contains the length of longest subsequence common to both lists.
+ * Following loop is the "traceback" procedure: starting from last cell, follows
+ * the direction where the length decrease.
+ */
+ final List<E> lcs = new ArrayList<>(lengths[nx][ny]);
+ while (nx > 0 && ny > 0) {
+ final int lg = lengths[nx][ny];
+ if (lengths[nx-1][ny] >= lg) {
+ nx--;
+ } else if (lengths[nx][--ny] < lg) {
+ final E ex = x.get(--nx);
+ assert ex == y.get(ny);
+ lcs.add(ex);
+ }
+ }
+ Collections.reverse(lcs);
+ return lcs;
+ }
+}
diff --git a/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/RecentChoices.java b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/RecentChoices.java
index eb0be43..70332e2 100644
--- a/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/RecentChoices.java
+++ b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/RecentChoices.java
@@ -18,9 +18,12 @@ package org.apache.sis.internal.gui;
import java.io.File;
import java.util.List;
+import java.util.Arrays;
import java.util.prefs.Preferences;
import javafx.scene.control.ComboBox;
import javafx.collections.ObservableList;
+import org.apache.sis.util.CharSequences;
+import org.apache.sis.util.collection.FrequencySortedSet;
/**
@@ -33,6 +36,15 @@ import javafx.collections.ObservableList;
*/
public final class RecentChoices {
/**
+ * Maximum number of reference systems to save in the preferences. Note that this is not necessarily
+ * the maximum number of reference systems shown in the GUI (that maximum is lower), because the GUI
+ * will filter out the reference systems that are valid outside the domain of interest.
+ *
+ * @see #useReferenceSystem(String)
+ */
+ public static final int MAXIMUM_REFERENCE_SYSTEMS = 20;
+
+ /**
* The nodes where to store user information (for example last directory opened).
* We want node for the {@code "org.apache.sis.gui"} package, which is the public one.
*/
@@ -44,6 +56,17 @@ public final class RecentChoices {
private static final String OPEN = "Open";
/**
+ * The node where to store authority (usually EPSG) codes of most recently used coordinate reference systems.
+ */
+ private static final String CRS = "ReferenceSystems";
+
+ /**
+ * The coordinate reference systems used in current JVM run, with most frequently used systems first.
+ * The CRS are stored by their authority codes. Access to this set must be synchronized.
+ */
+ private static final FrequencySortedSet<String> CRS_THIS_RUN = new FrequencySortedSet<>(true);
+
+ /**
* Do not allow instantiation of this class.
*/
private RecentChoices() {
@@ -70,6 +93,70 @@ public final class RecentChoices {
}
/**
+ * Returns the authority codes of most recently used reference systems.
+ *
+ * @return authority codes, or an empty array if none.
+ */
+ public static String[] getReferenceSystems() {
+ final String[] codes;
+ synchronized (CRS_THIS_RUN) {
+ final int n = CRS_THIS_RUN.size();
+ if (n != 0) {
+ codes = CRS_THIS_RUN.toArray(new String[n]);
+ } else {
+ final String value = NODE.get(CRS, null);
+ codes = (String[]) CharSequences.split(value, ',');
+ CRS_THIS_RUN.addAll(Arrays.asList(codes));
+ }
+ }
+ return codes;
+ }
+
+ /**
+ * Notifies the preferences that the CRS identified by the given code has been selected.
+ * If the given value is {@code null}, then it is ignored.
+ *
+ * @param code code of the CRS selected by user, or {@code null}.
+ */
+ public static void useReferenceSystem(final String code) {
+ if (code != null) {
+ final String[] codes;
+ synchronized (CRS_THIS_RUN) {
+ if (!CRS_THIS_RUN.add(code.trim())) {
+ return;
+ }
+ codes = CRS_THIS_RUN.toArray(new String[CRS_THIS_RUN.size()]);
+ }
+ saveReferenceSystems(codes);
+ }
+ }
+
+ /**
+ * Saves the authority codes of most recently used reference systems.
+ * This method should be invoked when the application shutdowns.
+ */
+ public static void saveReferenceSystems() {
+ final String[] codes;
+ synchronized (CRS_THIS_RUN) {
+ codes = CRS_THIS_RUN.toArray(new String[CRS_THIS_RUN.size()]);
+ }
+ saveReferenceSystems(codes);
+ }
+
+ /**
+ * Saves the given list of authority codes.
+ * Only the first {@value #MAXIMUM_REFERENCE_SYSTEMS} codes are saved.
+ */
+ private static void saveReferenceSystems(String[] codes) {
+ if (codes.length != 0) {
+ if (codes.length > MAXIMUM_REFERENCE_SYSTEMS) {
+ codes = Arrays.copyOf(codes, MAXIMUM_REFERENCE_SYSTEMS);
+ }
+ NODE.put(CRS, String.join(",", codes));
+ }
+ }
+
+ /**
* Returns the common parent of a list of files.
* This is used for selecting which directory to remember for the next open or save dialog box.
*
diff --git a/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/Styles.java b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/Styles.java
index ab4a0f5..735a77d 100644
--- a/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/Styles.java
+++ b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/Styles.java
@@ -34,6 +34,7 @@ import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import org.apache.sis.util.logging.Logging;
import org.apache.sis.internal.system.Modules;
+import org.apache.sis.util.Static;
/**
@@ -48,7 +49,7 @@ import org.apache.sis.internal.system.Modules;
* @since 1.1
* @module
*/
-public final class Styles {
+public final class Styles extends Static {
/**
* Approximate size of vertical scroll bar.
*/
diff --git a/application/sis-javafx/src/test/java/org/apache/sis/internal/gui/GUIUtilitiesTest.java b/application/sis-javafx/src/test/java/org/apache/sis/internal/gui/GUIUtilitiesTest.java
new file mode 100644
index 0000000..11c14d7
--- /dev/null
+++ b/application/sis-javafx/src/test/java/org/apache/sis/internal/gui/GUIUtilitiesTest.java
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sis.internal.gui;
+
+import java.util.Arrays;
+import java.util.List;
+import org.apache.sis.test.TestCase;
+import org.junit.Test;
+
+import static org.junit.Assert.*;
+
+
+/**
+ * Tests {@link GUIUtilities}.
+ *
+ * @author Martin Desruisseaux (Geomatys)
+ * @version 1.1
+ * @since 1.1
+ * @module
+ */
+public final strictfp class GUIUtilitiesTest extends TestCase {
+ /**
+ * Tests {@link GUIUtilities#longestCommonSubsequence(List, List)}.
+ */
+ @Test
+ public void testLongestCommonSubsequence() {
+ final List<Integer> x = Arrays.asList(1, 2, 4, 6, 7, 9);
+ final List<Integer> y = Arrays.asList(1, 2, 3, 7, 8);
+ assertEquals(Arrays.asList(1, 2, 7), GUIUtilities.longestCommonSubsequence(x, y));
+ }
+}
diff --git a/application/sis-javafx/src/test/java/org/apache/sis/test/suite/ApplicationTestSuite.java b/application/sis-javafx/src/test/java/org/apache/sis/test/suite/ApplicationTestSuite.java
new file mode 100644
index 0000000..c960b54
--- /dev/null
+++ b/application/sis-javafx/src/test/java/org/apache/sis/test/suite/ApplicationTestSuite.java
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sis.test.suite;
+
+import org.apache.sis.test.TestSuite;
+import org.junit.runners.Suite;
+import org.junit.BeforeClass;
+
+
+/**
+ * All tests from the {@code sis-javafx} module, in rough dependency order.
+ *
+ * @author Martin Desruisseaux (Geomatys)
+ * @version 1.1
+ * @since 1.1
+ * @module
+ */
+@Suite.SuiteClasses({
+ org.apache.sis.internal.gui.GUIUtilitiesTest.class
+})
+public final strictfp class ApplicationTestSuite extends TestSuite {
+ /**
+ * Verifies the list of tests before to run the suite.
+ * See {@link #verifyTestList(Class, Class[])} for more information.
+ */
+ @BeforeClass
+ public static void verifyTestList() {
+ assertNoMissingTest(ApplicationTestSuite.class);
+ verifyTestList(ApplicationTestSuite.class);
+ }
+}
diff --git a/core/sis-utility/src/main/java/org/apache/sis/util/CharSequences.java b/core/sis-utility/src/main/java/org/apache/sis/util/CharSequences.java
index 8802403..bab9fe5 100644
--- a/core/sis-utility/src/main/java/org/apache/sis/util/CharSequences.java
+++ b/core/sis-utility/src/main/java/org/apache/sis/util/CharSequences.java
@@ -632,7 +632,7 @@ search: for (; fromIndex <= toIndex; fromIndex++) {
* @param text the text to split, or {@code null}.
* @param separator the delimiting character (typically the coma).
* @return the array of subsequences computed by splitting the given text around the given
- * character, or an empty array if {@code toSplit} was null.
+ * character, or an empty array if {@code text} was null.
*
* @see String#split(String)
*/
diff --git a/ide-project/NetBeans/nbproject/build-impl.xml b/ide-project/NetBeans/nbproject/build-impl.xml
index 3228d8e..c6176df 100644
--- a/ide-project/NetBeans/nbproject/build-impl.xml
+++ b/ide-project/NetBeans/nbproject/build-impl.xml
@@ -180,6 +180,7 @@ is divided into following sections:
</condition>
<condition property="have.tests">
<or>
+ <available file="${test.javafx.dir}"/>
<available file="${test.webapp.dir}"/>
<available file="${test.console.dir}"/>
<available file="${test.portrayal.dir}"/>
@@ -346,6 +347,7 @@ is divided into following sections:
<fail unless="src.jpn-profile.dir">Must set src.jpn-profile.dir</fail>
<fail unless="src.gdal.dir">Must set src.gdal.dir</fail>
<fail unless="src.c.gdal.dir">Must set src.c.gdal.dir</fail>
+ <fail unless="test.javafx.dir">Must set test.javafx.dir</fail>
<fail unless="test.webapp.dir">Must set test.webapp.dir</fail>
<fail unless="test.console.dir">Must set test.console.dir</fail>
<fail unless="test.portrayal.dir">Must set test.portrayal.dir</fail>
@@ -662,6 +664,9 @@ is divided into following sections:
<j2seproject3:junit-prototype>
<customizePrototype>
<batchtest todir="${build.test.results.dir}">
+ <fileset dir="${test.javafx.dir}" excludes="@{excludes},${excludes}" includes="@{includes}">
+ <filename name="@{testincludes}"/>
+ </fileset>
<fileset dir="${test.webapp.dir}" excludes="@{excludes},${excludes}" includes="@{includes}">
<filename name="@{testincludes}"/>
</fileset>
@@ -739,6 +744,9 @@ is divided into following sections:
<isset property="test.method"/>
</condition>
<union id="test.set">
+ <fileset dir="${test.javafx.dir}" excludes="@{excludes},**/*.xml,${excludes}" includes="@{includes}">
+ <filename name="@{testincludes}"/>
+ </fileset>
<fileset dir="${test.webapp.dir}" excludes="@{excludes},**/*.xml,${excludes}" includes="@{includes}">
<filename name="@{testincludes}"/>
</fileset>
@@ -1869,14 +1877,14 @@ is divided into following sections:
<!-- You can override this target in the ../build.xml file. -->
</target>
<target depends="-init-source-module-properties" if="named.module.internal" name="-init-test-javac-module-properties-with-module">
- <j2seproject3:modulename property="test.module.name" sourcepath="${test.webapp.dir}:${test.console.dir}:${test.portrayal.dir}:${test.earth-obs.dir}:${test.geotiff.dir}:${test.netcdf.dir}:${test.shapefile.dir}:${test.sql.dir}:${test.xmlstore.dir}:${test.storage.dir}:${test.feature.dir}:${test.referencing.dir}:${test.ref-by-id.dir}:${test.metadata.dir}:${test.utility.dir}:${test.fra-profile.dir}:${test.jpn-profile.dir}:${test.gdal.dir}"/>
- <condition else="${empty.dir}" property="javac.test.sourcepath" value="${test.webapp.dir}:${test.console.dir}:${test.portrayal.dir}:${test.earth-obs.dir}:${test.geotiff.dir}:${test.netcdf.dir}:${test.shapefile.dir}:${test.sql.dir}:${test.xmlstore.dir}:${test.storage.dir}:${test.feature.dir}:${test.referencing.dir}:${test.ref-by-id.dir}:${test.metadata.dir}:${test.utility.dir}:${test.fra-profile.dir}:${test.jpn-profile.dir}:${test.gdal.dir}">
+ <j2seproject3:modulename property="test.module.name" sourcepath="${test.javafx.dir}:${test.webapp.dir}:${test.console.dir}:${test.portrayal.dir}:${test.earth-obs.dir}:${test.geotiff.dir}:${test.netcdf.dir}:${test.shapefile.dir}:${test.sql.dir}:${test.xmlstore.dir}:${test.storage.dir}:${test.feature.dir}:${test.referencing.dir}:${test.ref-by-id.dir}:${test.metadata.dir}:${test.utility.dir}:${test.fra-profile.dir}:${test.jpn-profile.dir}:${test.gdal.dir}"/>
+ <condition else="${empty.dir}" property="javac.test.sourcepath" value="${test.javafx.dir}:${test.webapp.dir}:${test.console.dir}:${test.portrayal.dir}:${test.earth-obs.dir}:${test.geotiff.dir}:${test.netcdf.dir}:${test.shapefile.dir}:${test.sql.dir}:${test.xmlstore.dir}:${test.storage.dir}:${test.feature.dir}:${test.referencing.dir}:${test.ref-by-id.dir}:${test.metadata.dir}:${test.utility.dir}:${test.fra-profile.dir}:${test.jpn-profile.dir}:${test.gdal.dir}">
<and>
<isset property="test.module.name"/>
<length length="0" string="${test.module.name}" when="greater"/>
</and>
</condition>
- <condition else="--patch-module ${module.name}=${test.webapp.dir}:${test.console.dir}:${test.portrayal.dir}:${test.earth-obs.dir}:${test.geotiff.dir}:${test.netcdf.dir}:${test.shapefile.dir}:${test.sql.dir}:${test.xmlstore.dir}:${test.storage.dir}:${test.feature.dir}:${test.referencing.dir}:${test.ref-by-id.dir}:${test.metadata.dir}:${test.utility.dir}:${test.fra-profile.dir}:${test.jpn-profile.dir}:${test.gdal.dir} --add-reads ${module.name}=ALL-UNNAMED" property="javac.test.com [...]
+ <condition else="--patch-module ${module.name}=${test.javafx.dir}:${test.webapp.dir}:${test.console.dir}:${test.portrayal.dir}:${test.earth-obs.dir}:${test.geotiff.dir}:${test.netcdf.dir}:${test.shapefile.dir}:${test.sql.dir}:${test.xmlstore.dir}:${test.storage.dir}:${test.feature.dir}:${test.referencing.dir}:${test.ref-by-id.dir}:${test.metadata.dir}:${test.utility.dir}:${test.fra-profile.dir}:${test.jpn-profile.dir}:${test.gdal.dir} --add-reads ${module.name}=ALL-UNNAMED" prope [...]
<and>
<isset property="test.module.name"/>
<length length="0" string="${test.module.name}" when="greater"/>
@@ -1917,15 +1925,16 @@ is divided into following sections:
</target>
<target depends="-init-test-javac-module-properties-with-module,-init-test-module-properties-without-module" name="-init-test-module-properties"/>
<target if="do.depend.true" name="-compile-test-depend">
- <j2seproject3:depend classpath="${javac.test.classpath}" destdir="${build.test.classes.dir}" srcdir="${test.webapp.dir}:${test.console.dir}:${test.portrayal.dir}:${test.earth-obs.dir}:${test.geotiff.dir}:${test.netcdf.dir}:${test.shapefile.dir}:${test.sql.dir}:${test.xmlstore.dir}:${test.storage.dir}:${test.feature.dir}:${test.referencing.dir}:${test.ref-by-id.dir}:${test.metadata.dir}:${test.utility.dir}:${test.fra-profile.dir}:${test.jpn-profile.dir}:${test.gdal.dir}"/>
+ <j2seproject3:depend classpath="${javac.test.classpath}" destdir="${build.test.classes.dir}" srcdir="${test.javafx.dir}:${test.webapp.dir}:${test.console.dir}:${test.portrayal.dir}:${test.earth-obs.dir}:${test.geotiff.dir}:${test.netcdf.dir}:${test.shapefile.dir}:${test.sql.dir}:${test.xmlstore.dir}:${test.storage.dir}:${test.feature.dir}:${test.referencing.dir}:${test.ref-by-id.dir}:${test.metadata.dir}:${test.utility.dir}:${test.fra-profile.dir}:${test.jpn-profile.dir}:${test.g [...]
</target>
<target depends="init,deps-jar,compile,-init-test-module-properties,-pre-pre-compile-test,-pre-compile-test,-compile-test-depend" if="have.tests" name="-do-compile-test">
- <j2seproject3:javac apgeneratedsrcdir="${build.test.classes.dir}" classpath="${javac.test.classpath}" debug="true" destdir="${build.test.classes.dir}" modulepath="${javac.test.modulepath}" processorpath="${javac.test.processorpath}" sourcepath="${javac.test.sourcepath}" srcdir="${test.webapp.dir}:${test.console.dir}:${test.portrayal.dir}:${test.earth-obs.dir}:${test.geotiff.dir}:${test.netcdf.dir}:${test.shapefile.dir}:${test.sql.dir}:${test.xmlstore.dir}:${test.storage.dir}:${te [...]
+ <j2seproject3:javac apgeneratedsrcdir="${build.test.classes.dir}" classpath="${javac.test.classpath}" debug="true" destdir="${build.test.classes.dir}" modulepath="${javac.test.modulepath}" processorpath="${javac.test.processorpath}" sourcepath="${javac.test.sourcepath}" srcdir="${test.javafx.dir}:${test.webapp.dir}:${test.console.dir}:${test.portrayal.dir}:${test.earth-obs.dir}:${test.geotiff.dir}:${test.netcdf.dir}:${test.shapefile.dir}:${test.sql.dir}:${test.xmlstore.dir}:${tes [...]
<customize>
<compilerarg line="${javac.test.compilerargs}"/>
</customize>
</j2seproject3:javac>
<copy todir="${build.test.classes.dir}">
+ <fileset dir="${test.javafx.dir}" excludes="${build.classes.excludes},${excludes}" includes="${includes}"/>
<fileset dir="${test.webapp.dir}" excludes="${build.classes.excludes},${excludes}" includes="${includes}"/>
<fileset dir="${test.console.dir}" excludes="${build.classes.excludes},${excludes}" includes="${includes}"/>
<fileset dir="${test.portrayal.dir}" excludes="${build.classes.excludes},${excludes}" includes="${includes}"/>
@@ -1958,12 +1967,13 @@ is divided into following sections:
<target depends="init,deps-jar,compile,-init-test-module-properties,-pre-pre-compile-test,-pre-compile-test-single" if="have.tests" name="-do-compile-test-single">
<fail unless="javac.includes">Must select some files in the IDE or set javac.includes</fail>
<j2seproject3:force-recompile destdir="${build.test.classes.dir}"/>
- <j2seproject3:javac apgeneratedsrcdir="${build.test.classes.dir}" classpath="${javac.test.classpath}" debug="true" destdir="${build.test.classes.dir}" excludes="" includes="${javac.includes}, module-info.java" modulepath="${javac.test.modulepath}" processorpath="${javac.test.processorpath}" sourcepath="${test.webapp.dir}:${test.console.dir}:${test.portrayal.dir}:${test.earth-obs.dir}:${test.geotiff.dir}:${test.netcdf.dir}:${test.shapefile.dir}:${test.sql.dir}:${test.xmlstore.dir} [...]
+ <j2seproject3:javac apgeneratedsrcdir="${build.test.classes.dir}" classpath="${javac.test.classpath}" debug="true" destdir="${build.test.classes.dir}" excludes="" includes="${javac.includes}, module-info.java" modulepath="${javac.test.modulepath}" processorpath="${javac.test.processorpath}" sourcepath="${test.javafx.dir}:${test.webapp.dir}:${test.console.dir}:${test.portrayal.dir}:${test.earth-obs.dir}:${test.geotiff.dir}:${test.netcdf.dir}:${test.shapefile.dir}:${test.sql.dir}:$ [...]
<customize>
<compilerarg line="${javac.test.compilerargs}"/>
</customize>
</j2seproject3:javac>
<copy todir="${build.test.classes.dir}">
+ <fileset dir="${test.javafx.dir}" excludes="${build.classes.excludes},${excludes}" includes="${includes}"/>
<fileset dir="${test.webapp.dir}" excludes="${build.classes.excludes},${excludes}" includes="${includes}"/>
<fileset dir="${test.console.dir}" excludes="${build.classes.excludes},${excludes}" includes="${includes}"/>
<fileset dir="${test.portrayal.dir}" excludes="${build.classes.excludes},${excludes}" includes="${includes}"/>
diff --git a/ide-project/NetBeans/nbproject/genfiles.properties b/ide-project/NetBeans/nbproject/genfiles.properties
index 5330355..df802b9 100644
--- a/ide-project/NetBeans/nbproject/genfiles.properties
+++ b/ide-project/NetBeans/nbproject/genfiles.properties
@@ -3,6 +3,6 @@
build.xml.data.CRC32=58e6b21c
build.xml.script.CRC32=462eaba0
build.xml.stylesheet.CRC32=28e38971@1.53.1.46
-nbproject/build-impl.xml.data.CRC32=84cf0137
-nbproject/build-impl.xml.script.CRC32=27d6eb56
+nbproject/build-impl.xml.data.CRC32=59eb8bb4
+nbproject/build-impl.xml.script.CRC32=b6a6bc73
nbproject/build-impl.xml.stylesheet.CRC32=f89f7d21@1.94.0.48
diff --git a/ide-project/NetBeans/nbproject/project.properties b/ide-project/NetBeans/nbproject/project.properties
index d06b926..82c5377 100644
--- a/ide-project/NetBeans/nbproject/project.properties
+++ b/ide-project/NetBeans/nbproject/project.properties
@@ -54,6 +54,7 @@ run.jvmargs = -enableassertions ${javafx.options} \
project.root = ../..
src.local-src.dir = ../local-src
src.javafx.dir = ${project.root}/application/sis-javafx/doc
+test.javafx.dir = ${project.root}/application/sis-javafx/doc
src.webapp.dir = ${project.root}/application/sis-webapp/src/main/java
test.webapp.dir = ${project.root}/application/sis-webapp/src/test/java
src.console.dir = ${project.root}/application/sis-console/src/main/java
diff --git a/ide-project/NetBeans/nbproject/project.xml b/ide-project/NetBeans/nbproject/project.xml
index 7984b5c..1c0d1e1 100644
--- a/ide-project/NetBeans/nbproject/project.xml
+++ b/ide-project/NetBeans/nbproject/project.xml
@@ -46,6 +46,7 @@
<root id="src.c.gdal.dir" name="GDAL/Proj4 JNI"/>
</source-roots>
<test-roots>
+ <root id="test.javafx.dir" name="Test JavaFX application"/>
<root id="test.webapp.dir" name="Test web application"/>
<root id="test.console.dir" name="Test Console"/>
<root id="test.portrayal.dir" name="Test Portrayal"/>
@@ -89,6 +90,7 @@
<word>classname</word>
<word>classnames</word>
<word>classpath</word>
+ <word>complexify</word>
<word>deserialization</word>
<word>deserialized</word>
<word>endianness</word>