blob: 952c9d2b67a846d04671d83b90e4abbf2f352a56 [file] [log] [blame]
/*
* 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.Map;
import java.util.List;
import java.util.ArrayList;
import java.util.Locale;
import java.util.Objects;
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.collections.transformation.FilteredList;
import javafx.scene.control.ChoiceBox;
import javafx.scene.control.MenuItem;
import javafx.scene.control.Menu;
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.DerivedCRS;
import org.opengis.referencing.crs.CRSAuthorityFactory;
import org.opengis.referencing.crs.CoordinateReferenceSystem;
import org.opengis.referencing.operation.TransformException;
import org.opengis.referencing.datum.PixelInCell;
import org.apache.sis.geometry.Envelopes;
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.MilitaryGridReferenceSystem;
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;
import org.apache.sis.util.Utilities;
import org.apache.sis.util.logging.Logging;
import org.apache.sis.util.resources.Vocabulary;
import org.apache.sis.gui.internal.BackgroundThreads;
import org.apache.sis.gui.internal.ExceptionReporter;
import org.apache.sis.gui.internal.GUIUtilities;
import org.apache.sis.gui.internal.NonNullObjectProperty;
import org.apache.sis.gui.internal.RecentChoices;
import org.apache.sis.gui.internal.io.OptionalDataDownloader;
import org.apache.sis.util.privy.Strings;
import org.apache.sis.util.privy.UnmodifiableArrayList;
import static org.apache.sis.gui.internal.LogHandler.LOGGER;
/**
* 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.4
* @since 1.1
*/
public class RecentReferenceSystems {
/**
* 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_ITEMS = 1;
/**
* Number of reference systems to show in {@link ChoiceBox} or {@link MenuItem}s.
* The {@value #NUM_CORE_ITEMS} core systems are included but not {@link #OTHER}.
*/
private static final int NUM_SHOWN_ITEMS = 9;
/**
* Number of reference systems to keep at the end of the list.
*/
private static final int NUM_OTHER_ITEMS = 1;
/**
* Key for use with the {@linkplain Menu#getProperties() property map} for storing the selected item.
* Used for providing the functionality of {@link javafx.scene.control.CheckBox#selectedProperty()}
* on controls that do not have an explicit selected property.
*/
private static final String SELECTED_ITEM_KEY = "SelectedItem";
/**
* 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.
*
* <h4>Maintenance note</h4>
* If this field is changed to a non-null value,
* search also for usages of {@code Object::nonNull} predicate.
*/
static final ReferenceSystem OTHER = null;
/**
* The factory to use for creating a Coordinate Reference System from an authority code.
* If {@code null}, then a 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.
*
* <h4>API note</h4>
* We do not provide getter/setter for this property; use {@link ObjectProperty#set(Object)}
* directly instead. We omit the "Property" suffix for making this operation more natural.
*/
public final ObjectProperty<Envelope> areaOfInterest;
/**
* Area of interest converted to geographic coordinates, or {@code null} if none.
*/
private ImmutableEnvelope geographicAOI;
/**
* 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.
*
* <h4>API note</h4>
* We do not provide getter/setter for this property; use {@link ObjectProperty#set(Object)}
* directly instead. We omit the "Property" suffix for making this operation more natural.
*/
public final ObjectProperty<ComparisonMode> duplicationCriterion;
/**
* Values of controls created by this {@code RecentReferenceSystems} instance. We retain those properties
* because modifying the {@link #referenceSystems} list sometimes 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<WritableValue<ReferenceSystem>> controlValues;
/**
* The preferred locale for displaying object name, or {@code null} for the default locale.
*/
final Locale locale;
/**
* 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. */
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;
}
}
/**
* 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 #filterReferenceSystems(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.
*
* <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 cannot 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.
*
* <h4>Design notes</h4>
* The {@link #OTHER} item needs to exist in the list used internally by this class because those lists
* are used directly by controls like {@code ChoiceBox<ReferenceSystem>}, with the {@link #OTHER} value
* handled in a special way by {@link ObjectStringConverter} for making the "Other…" item present in the
* list of choices. But since {@link #OTHER} is not a real CRS, we want to hide that trick to users.
*
* <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> publicItemList;
/**
* Coordinate reference systems used for computing cell indices of grid coverages.
* Those reference systems are offered in a sub-menu and are not included in {@link #publicItemList}.
* The content of this list depends on the grid coverages shown in the widget.
*
* @see #setGridReferencing(boolean, Map)
*/
private final List<DerivedCRS> cellIndiceSystems;
/**
* {@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 #listModified()
*/
private boolean isModified;
/**
* {@code true} if {@code RecentReferenceSystems} is in the process of modifying {@link #referenceSystems} list.
* In such case we want to temporarily disable the {@link SelectionListener}.
* This field is read and updated in JavaFX thread.
*/
private boolean isAdjusting;
/**
* Creates a builder which will use a default authority factory.
* The factory will be capable to handle at least some EPSG codes.
*/
public RecentReferenceSystems() {
this(null, null);
}
/**
* Creates a builder which will use the specified authority factory.
*
* @param factory the factory to use for building CRS from authority codes, or {@code null} for the default.
* @param locale the preferred locale for displaying object name, or {@code null} for the default locale.
*
* @see org.apache.sis.referencing.CRS#getAuthorityFactory(String)
*/
public RecentReferenceSystems(final CRSAuthorityFactory factory, final Locale locale) {
this.factory = factory;
this.locale = locale;
systemsOrCodes = new ArrayList<>();
cellIndiceSystems = new ArrayList<>();
areaOfInterest = new SimpleObjectProperty<>(this, "areaOfInterest");
duplicationCriterion = new NonNullObjectProperty<>(this, "duplicationCriterion", ComparisonMode.ALLOW_VARIANT);
controlValues = new ArrayList<>();
duplicationCriterion.addListener((e) -> listModified());
areaOfInterest.addListener((e,o,n) -> {
geographicAOI = Utils.toGeographic(RecentReferenceSystems.class, "areaOfInterest", n);
listModified();
});
}
/**
* Sets the reference systems, area of interest and "referencing by grid indices" systems.
* This method performs all the following work:
*
* <ul>
* <li>Invokes {@link #setPreferred(boolean, ReferenceSystem)} with the first CRS in iteration order.</li>
* <li>Invokes {@link #addAlternatives(boolean, ReferenceSystem...)} for all other CRS (single call).</li>
* <li>Sets the {@link #areaOfInterest} to the union of all envelopes.</li>
* <li>Sets the content of "Referencing by cell indices" sub-menu.</li>
* </ul>
*
* @param replaceByAuthoritativeDefinition whether the reference systems should be replaced by authoritative definition.
* @param geometries grid coverage names together with their grid geometry. May be empty.
*
* @since 1.3
*/
public void setGridReferencing(final boolean replaceByAuthoritativeDefinition,
final Map<String,GridGeometry> geometries)
{
/*
* Fetch or compute information needed, but without modifying the state of this object yet.
* All assignments to `this` should be done inside the `try … finally` block.
*/
int countEnv = 0;
int countCRS = 0;
int countCIR = 0;
final Envelope[] envelopes = new Envelope[geometries.size()];
final DerivedCRS[] derived = new DerivedCRS[geometries.size()];
final CoordinateReferenceSystem[] alt = new CoordinateReferenceSystem[Math.max(derived.length - 1, 0)];
CoordinateReferenceSystem preferred = null;
for (final Map.Entry<String,GridGeometry> entry : geometries.entrySet()) {
final GridGeometry gg = entry.getValue();
if (gg.isDefined(GridGeometry.ENVELOPE)) {
envelopes[countEnv++] = gg.getEnvelope();
}
if (gg.isDefined(GridGeometry.CRS)) {
final CoordinateReferenceSystem crs = gg.getCoordinateReferenceSystem();
if (preferred == null) {
preferred = crs;
} else {
alt[countCRS++] = crs;
}
if (gg.isDefined(GridGeometry.GRID_TO_CRS | GridGeometry.EXTENT)) {
derived[countCIR++] = gg.createImageCRS(entry.getKey(), PixelInCell.CELL_CENTER);
}
}
}
Envelope aoi = null;
try {
aoi = Envelopes.union(envelopes); // No need to trim null elements.
} catch (TransformException e) {
errorOccurred("setGridReferencing", e);
}
/*
* Modify now the state of `this` object but with `listModified()` made almost no-op.
* The intent is to have only one effective call to `listModified()` at the end,
* in order to have only one call to `filterReferenceSystems(…)`.
*/
final ObservableList<ReferenceSystem> savedReferenceSystemList = referenceSystems;
try {
referenceSystems = null;
if (preferred != null) {
setPreferred(replaceByAuthoritativeDefinition, preferred);
addAlternatives(replaceByAuthoritativeDefinition, alt); // No need to trim null elements.
cellIndiceSystems.clear();
cellIndiceSystems.addAll(UnmodifiableArrayList.wrap(derived, 0, countCIR));
}
areaOfInterest.set(aoi);
} finally {
referenceSystems = savedReferenceSystemList;
}
listModified();
}
/**
* 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 <q>WGS 84</q> 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 final void setPreferred(final boolean replaceByAuthoritativeDefinition, final ReferenceSystem system) {
// Final because `setGridReferencing(…)` needs to be sure that `referenceSystems` is not rebuilt.
ArgumentChecks.ensureNonNull("system", system);
synchronized (systemsOrCodes) {
systemsOrCodes.add(0, replaceByAuthoritativeDefinition ? new Unverified(system) : system);
listModified();
}
}
/**
* 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);
listModified();
}
}
/**
* Invoked when a new CRS is selected and that CRS has not been found in the list.
* The new CRS is added after the CRS and menu items will be added in background thread.
*/
final void addSelected(final ReferenceSystem system) {
if (isAccepted(system)) {
synchronized (systemsOrCodes) {
systemsOrCodes.add(Math.min(systemsOrCodes.size(), NUM_CORE_ITEMS), system);
listModified();
}
}
}
/**
* 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 <q>WGS 84</q> 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 final void addAlternatives(final boolean replaceByAuthoritativeDefinition, final ReferenceSystem... systems) {
// Final because `setGridReferencing(…)` needs to be sure that `referenceSystems` is not rebuilt.
ArgumentChecks.ensureNonNull("systems", systems);
synchronized (systemsOrCodes) {
for (final ReferenceSystem system : systems) {
if (system != null) {
systemsOrCodes.add(replaceByAuthoritativeDefinition ? new Unverified(system) : system);
}
}
listModified();
}
// Check for duplication will be done in `filterReferenceSystems()` 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);
}
}
listModified();
}
// Parsing will be done in `filterReferenceSystems()` 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 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.
*/
public void addUserPreferences() {
addAlternatives(RecentChoices.getReferenceSystems());
}
/**
* Returns whether the given object is accepted for inclusion in the list of CRS choices.
* In current implementation we accept a CRS if it has an authority code (typically an EPSG code).
*/
private static boolean isAccepted(final IdentifiedObject object) {
return IdentifiedObjects.getIdentifier(object, null) != null;
}
/**
* 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 cannot 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>
*
* @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> 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) {
return null; // Another thread already did the work.
}
boolean noFactoryFound = false;
boolean searchedFinder = false;
IdentifiedObjectFinder finder = null;
for (int i=systemsOrCodes.size(); --i >= 0;) {
final Object item = systemsOrCodes.get(i);
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);
}
} 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 cannot 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);
}
}
/*
* 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 number 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;
}
Object item = systemsOrCodes.get(i);
while (--j > i) {
if (Utilities.deepEquals(item, systemsOrCodes.get(j), mode)) {
final Object removed = systemsOrCodes.remove(j);
if (isAccepted((IdentifiedObject) removed) && !isAccepted((IdentifiedObject) item)) {
/*
* Keep the instance which has an identifier. The instance without identifier
* is typically a CRS with non-standard axis order. It happens when it is the
* CRS associated to an image that has just been read.
*/
systemsOrCodes.set(i, item = removed);
}
}
}
}
/*
* 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 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);
for (int i=0; i<n; i++) {
final ReferenceSystem system = (ReferenceSystem) systemsOrCodes.get(i);
if (i >= NUM_CORE_ITEMS && !Utils.intersects(domain, system)) {
continue;
}
if (Utils.isIgnoreable(system)) { // Ignore "Computer display" CRS.
continue;
}
systems.add(system);
if (systems.size() >= NUM_SHOWN_ITEMS) 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 listModified() {
synchronized (systemsOrCodes) {
isModified = true;
if (referenceSystems != null) {
// ChoiceBox or Menu already created. They will observe the changes in item list.
getReferenceSystems(false);
}
}
}
/**
* 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.
*
* @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> getReferenceSystems(final boolean filtered) {
if (referenceSystems == null) {
referenceSystems = FXCollections.observableArrayList();
}
synchronized (systemsOrCodes) {
/*
* Prepare a temporary list as the concatenation of all items that are currently visible in JavaFX
* controls with all items that were specified by `setPreferred(…)` or `addAlternatives(…)` methods.
* This concatenation creates a lot of duplicated values, but those duplications will be filtered by
* `filterReferenceSystems(…)` method. The intent is to preserve following order:
*
* - NUM_CORE_ITEMS preferred reference systems first.
* - All reference systems that are currently selected by JavaFX controls.
* - All reference systems offered as choice in JavaFX controls.
* - All reference systems specified by `addAlternatives(…)`.
* - NUM_OTHER_ITEMS systems (will be handled in a special way by `filterReferenceSystems(…)`).
*
* The list will be truncated to NUM_SHOWN_ITEMS after duplications are removed and before OTHER
* is added. The first occurrence of duplicated values is kept, which will result in above-cited
* order as the priority order where to insert the CRS.
*/
if (isModified) {
final int insertAt = Math.min(systemsOrCodes.size(), NUM_CORE_ITEMS);
final List<ReferenceSystem> selected = getSelectedItems();
systemsOrCodes.addAll(insertAt, selected);
systemsOrCodes.addAll(insertAt + selected.size(), referenceSystems);
final ImmutableEnvelope domain = geographicAOI;
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 filterReferenceSystems(domain, mode);
}
/** Should never happen. */
@Override protected void failed() {
ExceptionReporter.show(null, this);
}
/** Sets the {@link ChoiceBox} content to the list computed in background thread. */
@Override protected void succeeded() {
setReferenceSystems(getValue(), mode);
}
});
}
}
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
* 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) {
// Nested calls to this method should never happen, but check !isAdjusting anyway as safety.
if (systems != null && !isAdjusting) try {
isAdjusting = true;
/*
* The call to `copyAsDiff(…)` may cause some 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. Note that if `referenceSystems` was empty before
* the copy, `controlValues` should be null before the copy but may become non-null after the
* copy because listeners will have initialized them to the first `ReferenceSystem` available.
* Those non-null values will not be reflected in the `values` array, so we should ignore them.
*/
final ReferenceSystem[] values = controlValues.stream().map(WritableValue::getValue).toArray(ReferenceSystem[]::new);
if (GUIUtilities.copyAsDiff(systems, referenceSystems)) {
notifyChanges();
}
/*
* 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];
if (system != null) { // See comment about empty `referenceSystems` list.
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).setValue(system);
}
}
} finally {
isAdjusting = false;
}
}
/**
* 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.
*/
final class SelectionListener implements ChangeListener<ReferenceSystem> {
/** The user-specified action to execute when a reference system is selected. */
final ChangeListener<ReferenceSystem> action;
/** Creates a new listener of reference system selection. */
private SelectionListener(final ChangeListener<ReferenceSystem> action) {
this.action = action;
}
/** The manager of reference systems to synchronize with. */
final RecentReferenceSystems owner() {
return RecentReferenceSystems.this;
}
/** 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) {
action.changed(property, oldValue, newValue);
return;
}
final ComparisonMode mode = duplicationCriterion.get();
if (newValue == OTHER) {
final CRSChooser chooser = new CRSChooser(factory, geographicAOI, locale);
newValue = chooser.showDialog(GUIUtilities.getWindow(property)).orElse(null);
if (newValue == null) {
newValue = oldValue;
} else {
/*
* If user selected a CRS in the CRSChooser list, verify if her/his selection is a CRS
* already presents in the `referenceSystems` list. We ignore axis order (by default)
* because the previous CRS in the list may be a CRS given by `setPreferred(…)` method,
* which typically come from a DataStore. That previous CRS will be replaced by the CRS
* given by CRSChooser, which is more conform to authoritative definition.
*/
final ObservableList<ReferenceSystem> items = referenceSystems;
int count = items.size() - NUM_OTHER_ITEMS;
boolean found = false;
for (int i=0; i<count; i++) {
if (Utilities.deepEquals(newValue, items.get(i), mode)) {
if (i >= NUM_CORE_ITEMS) {
items.set(i, newValue);
}
found = true;
break;
}
}
/*
* If the selected CRS was not present in the list, we may need to remove the last item
* for making room for the new one. New item must be added before `property.setValue(…)`
* is invoked, otherwise ChoiceBox may add a new item by itself.
*/
if (!found) {
if (count >= NUM_SHOWN_ITEMS) {
final List<ReferenceSystem> selected = getSelectedItems();
for (int i=count; --i >= NUM_CORE_ITEMS;) {
if (!selected.contains(items.get(i))) { // Do not remove selected items.
items.remove(i); // Remove an item before `OTHER`.
if (--count < NUM_SHOWN_ITEMS) break;
}
}
}
items.add(Math.min(items.size(), NUM_CORE_ITEMS), newValue);
notifyChanges();
}
}
/*
* Following cast is safe because this listener is registered only on ObjectProperty
* instances, and the ObjectProperty class implements WritableValue.
* The effect of this method call is to set the selected value.
*/
((WritableValue<ReferenceSystem>) property).setValue(newValue);
}
if (oldValue != newValue) {
/*
* If the selected CRS is already at the beginning of the list, do nothing. The beginning is
* either one of the core items (specified by `setPreferred(…)`) or the first item after the
* core items.
*/
final ObservableList<ReferenceSystem> items = referenceSystems;
final int count = items.size() - NUM_OTHER_ITEMS;
for (int i = Math.min(count, NUM_CORE_ITEMS + 1); --i >= 0;) {
final ReferenceSystem current = items.get(i);
if (Utilities.deepEquals(current, newValue, mode)) {
action.changed(property, oldValue, current);
return;
}
}
/*
* 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.
*/
for (int i = count; --i >= NUM_CORE_ITEMS;) {
if (Utilities.deepEquals(items.get(i), newValue, mode)) {
newValue = items.remove(i);
break;
}
}
items.add(Math.min(items.size(), NUM_CORE_ITEMS), newValue);
/*
* Notify the user-specified listeners. It will typically starts a background process.
* We test (oldValue != newValue) again because `newValue` may have been replaced.
*/
notifyChanges();
if (oldValue != newValue) {
action.changed(property, oldValue, newValue); // Typically starts a background process.
}
RecentChoices.useReferenceSystem(IdentifiedObjects.toString(IdentifiedObjects.getIdentifier(newValue, null)));
}
}
}
/**
* Notifies all {@link MenuSync} that the list of reference systems changed. We send a notification manually
* instead of relying on {@code ListChangeListener} in order to process only one event after we have done
* a bunch of changes instead of an event after each individual add or remove operation.
*/
private void notifyChanges() {
for (final WritableValue<ReferenceSystem> value : controlValues) {
if (value instanceof MenuSync) {
((MenuSync) value).notifyChanges();
}
}
}
/**
* Returns all reference systems in the order they appear in JavaFX controls. The first element
* is the {@link #setPreferred(boolean, ReferenceSystem) preferred} (or native) reference system.
* All other elements are {@linkplain #addAlternatives(boolean, ReferenceSystem...) alternatives}.
*
* @return all reference systems in the order they appear in JavaFX controls.
*/
@SuppressWarnings("ReturnOfCollectionOrArrayField")
public ObservableList<ReferenceSystem> getItems() {
if (publicItemList == null) {
publicItemList = new FilteredList<>(getReferenceSystems(false), Objects::nonNull);
}
return publicItemList;
}
/**
* Returns all currently selected reference systems in the order they appear in JavaFX controls.
* This method collects selected values of all controls created by a {@code createXXX(…)} method.
* The returned list does not contain duplicated values.
*
* @return currently selected values of all controls, without duplicated values and in the order
* they appear in choice lists.
*/
public List<ReferenceSystem> getSelectedItems() {
/*
* Build an array of selected reference systems. This array may contain duplicated elements if two
* or more JavaFX controls have the same selected value. Those duplications will be resolved later.
* Conceptually we will use this array as a java.util.Set, except that its length is so small
* (usually no more than 3 elements) that it is not worth to use HashSet.
*/
int count = 0;
final ReferenceSystem[] selected = new ReferenceSystem[controlValues.size()];
for (final WritableValue<ReferenceSystem> value : controlValues) {
final ReferenceSystem system = value.getValue();
if (system != null) selected[count++] = system;
}
/*
* Now filter the `referenceSystems` list, retaining only elements that are present in `selected`.
* We do that way for having selected elements in the same order as they appear in JavaFX controls.
*/
final List<ReferenceSystem> ordered = new ArrayList<>(count);
if (count != 0) {
// (count > 0) implies (referenceSystems != null).
for (final ReferenceSystem system : referenceSystems) {
if (system != OTHER) {
for (int i=0; i<count; i++) {
if (selected[i] == system) {
ordered.add(system);
if (--count == 0) return ordered;
System.arraycopy(selected, i+1, selected, i, count - i);
break;
}
}
}
}
/*
* If some selected elements were not found in the `referenceSystems` list, add them last.
* It should not happen, unless those remaining elements are duplicated values (i.e. two
* or more controls having the same selection).
*/
next: for (int i=0; i<count; i++) {
final ReferenceSystem system = selected[i];
for (int j=ordered.size(); --j >= 0;) {
if (ordered.get(j) == system) {
continue next; // Skip duplicated value.
}
}
ordered.add(system);
}
}
return ordered;
}
/**
* Creates a box offering choices among the reference systems specified to this {@code RecentReferenceSystems}.
* 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).
*
* <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 boolean filtered, final ChangeListener<ReferenceSystem> action) {
ArgumentChecks.ensureNonNull("action", action);
final ChoiceBox<ReferenceSystem> choices = new ChoiceBox<>(getReferenceSystems(filtered));
choices.setConverter(new ObjectStringConverter<>(choices.getItems(), locale));
choices.valueProperty().addListener(new SelectionListener(action));
controlValues.add(choices.valueProperty());
return choices;
}
/**
* Creates menu items offering choices among the reference systems specified to this {@code RecentReferenceSystems}.
* 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.
*
* <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 boolean filtered, final ChangeListener<ReferenceSystem> action) {
ArgumentChecks.ensureNonNull("action", action);
final List<ReferenceSystem> main = getReferenceSystems(filtered);
final List<DerivedCRS> derived = (filtered) ? null : cellIndiceSystems;
final Menu menu = new Menu(Vocabulary.forLocale(locale).getString(Vocabulary.Keys.ReferenceSystem));
final MenuSync property = new MenuSync(main, !filtered, derived, menu, new SelectionListener(action));
menu.getProperties().put(SELECTED_ITEM_KEY, property);
controlValues.add(property);
return menu;
}
/**
* Returns the property for the selected value in a menu created by {@link #createMenuItems(boolean, ChangeListener)}.
*
* @param menu the menu, or {@code null} if none.
* @return the property for the selected value, or {@code null} if none.
*/
public static ObjectProperty<ReferenceSystem> getSelectedProperty(final Menu menu) {
if (menu != null) {
final Object property = menu.getProperties().get(SELECTED_ITEM_KEY);
if (property instanceof MenuSync) {
return (MenuSync) property;
}
}
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 popups an alert dialog only if the error occurred after the user
* accepted to {@linkplain org.apache.sis.setup.OptionalInstallations download optional dependencies},
* because the error may be caused by a problem related to the download operation.
* Otherwise this method only logs 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) {
OptionalDataDownloader.reportIfInstalling(e);
Logging.recoverableException(LOGGER, RecentReferenceSystems.class, "getReferenceSystems", e);
}
/**
* Invoked when an error other than {@link FactoryException} occurred.
* The error shall be recoverable, e.g. by ignoring a menu item.
*
* @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 Exception e) {
Logging.recoverableException(LOGGER, RecentReferenceSystems.class, caller, e);
}
}