| /* |
| * 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.ArrayList; |
| import java.util.Arrays; |
| import java.util.Iterator; |
| import java.util.IdentityHashMap; |
| import java.util.List; |
| import java.util.ListIterator; |
| import java.util.Locale; |
| import java.util.Map; |
| import javafx.application.Platform; |
| import javafx.beans.property.ReadOnlyStringWrapper; |
| import javafx.beans.value.ObservableValue; |
| import javafx.collections.ObservableListBase; |
| import javafx.scene.control.TableColumn; |
| import javafx.scene.control.TableView; |
| import javafx.concurrent.Task; |
| import javafx.util.Callback; |
| import org.opengis.util.FactoryException; |
| import org.opengis.referencing.IdentifiedObject; |
| import org.opengis.referencing.crs.CoordinateReferenceSystem; |
| import org.opengis.referencing.crs.CRSAuthorityFactory; |
| import org.apache.sis.util.Exceptions; |
| import org.apache.sis.util.iso.Types; |
| import org.apache.sis.util.logging.Logging; |
| import org.apache.sis.util.resources.Vocabulary; |
| import org.apache.sis.util.collection.BackingStoreException; |
| import org.apache.sis.util.privy.StandardDateFormat; |
| import org.apache.sis.util.privy.Strings; |
| import org.apache.sis.gui.internal.BackgroundThreads; |
| import static org.apache.sis.gui.internal.LogHandler.LOGGER; |
| |
| |
| /** |
| * A list of authority codes (usually for CRS) which fetch code values in a background thread |
| * and CRS names only when needed. |
| * |
| * @todo {@link org.apache.sis.referencing.factory.sql.EPSGDataAccess} internally uses a {@link java.util.Map} |
| * from codes to descriptions. We could open an access to this map for a little bit more efficiency. |
| * It will be necessary if we want to use {@link AuthorityCodes} for other kinds of objects than CRS |
| * (see {@link #type} field). |
| * |
| * @author Martin Desruisseaux (Geomatys) |
| */ |
| final class AuthorityCodes extends ObservableListBase<Code> |
| implements Callback<TableColumn.CellDataFeatures<Code,String>, ObservableValue<String>> |
| { |
| /** |
| * Delay in nanoseconds before to refresh the list with new content. |
| * Data will be transferred from background threads to JavaFX threads every time this delay is elapsed. |
| * The delay value is a compromise between fast user experience and giving enough time for doing a few |
| * large data transfers instead of many small data transfers. |
| */ |
| private static final long REFRESH_DELAY = StandardDateFormat.NANOS_PER_SECOND / 10; |
| |
| /** |
| * The table view which use this list, or {@code null} if we don't need this information anymore. |
| * See {@link #describedCount} for an explanation about its purpose. |
| */ |
| TableView<Code> owner; |
| |
| /** |
| * The type of object for which we want authority codes. Fixed to {@link CoordinateReferenceSystem} for now, |
| * but could be made configurable in a future version. Making this field configurable would require resolving |
| * the "todo" documented in class javadoc. |
| */ |
| private static final Class<? extends IdentifiedObject> type = CoordinateReferenceSystem.class; |
| |
| /** |
| * The authority codes obtained from the factory. The list elements are provided by a background thread. |
| * Elements are initially {@link String} instances and can be replaced later by {@link Code} instances. |
| */ |
| private final List<Object> codes; |
| |
| /** |
| * Count of the number of {@linkplain #codes} for which we completed the {@link Code#name} information. |
| * This is used for notifying the {@linkplain #owner} when we do not expect more information to be loaded. |
| * This notification is only indicative and may not be fully accurate. Its effect should be only visual |
| * (removing the hour glass icon). |
| */ |
| private int describedCount; |
| |
| /** |
| * The preferred locale of CRS descriptions. |
| */ |
| final Locale locale; |
| |
| /** |
| * The factory to use for creating coordinate reference systems, |
| * or {@code null} if not yet determined. |
| * |
| * @see #getFactory() |
| */ |
| private CRSAuthorityFactory factory; |
| |
| /** |
| * The task where to send requests for CRS descriptions (never {@code null}). |
| * The task is not necessarily running; it may have been created and not yet scheduled, |
| * in which case the task is waiting in {@link Task.State#READY} state for work to arrive. |
| */ |
| private Loader loader; |
| |
| /** |
| * {@code true} if an error occurred. This is used for reporting only one error |
| * for avoiding to flood the logger. |
| * |
| * @see #errorOccurred(Throwable) |
| */ |
| private volatile boolean hasError; |
| |
| /** |
| * Creates a new deferred list and starts a background process for loading CRS codes. |
| * If the given factory is {@code null}, then a |
| * {@linkplain org.apache.sis.referencing.CRS#getAuthorityFactory(String) default factory} |
| * capable to handle at least some EPSG codes will be used. |
| * |
| * @param factory the authority factory, or {@code null} for default factory. |
| * @param locale the preferred locale of CRS descriptions. |
| */ |
| AuthorityCodes(final CRSAuthorityFactory factory, final Locale locale) { |
| this.locale = locale; |
| this.factory = factory; |
| this.codes = new ArrayList<>(); |
| this.loader = new Loader(); |
| loader.start(); |
| } |
| |
| /** |
| * Returns the authority factory. This method may be invoked from any thread. |
| * The factory is not fetched at construction time for giving {@link Loader} |
| * a chance to fetch it in a background thread. |
| */ |
| final synchronized CRSAuthorityFactory getFactory() throws FactoryException { |
| if (factory == null) { |
| factory = Utils.getDefaultFactory(); |
| } |
| return factory; |
| } |
| |
| /** |
| * Returns the number of elements in this list. This method initially returns only the number of |
| * cached elements. This number may increase progressively as the background loading progresses. |
| */ |
| @Override |
| public int size() { |
| return codes.size(); |
| } |
| |
| /** |
| * Returns the authority code at given index, eventually with its name. |
| */ |
| @Override |
| public Code get(final int index) { |
| final Object value = codes.get(index); |
| if (value instanceof Code) { |
| return (Code) value; |
| } |
| // Wraps the String only when first needed. |
| final Code c = new Code((String) value); |
| codes.set(index, c); |
| return c; |
| } |
| |
| /** |
| * Adds a single code. This method should never be invoked except if an error occurred |
| * while loading codes, in which case we add a single pseudo-code with error message. |
| */ |
| @Override |
| public boolean add(final Code code) { |
| final int i = codes.size(); |
| codes.add(code); |
| beginChange(); |
| nextAdd(i, i+1); |
| endChange(); |
| return true; |
| } |
| |
| /** |
| * Invoked when the name or description of an authority code is requested. |
| * If the name is not available, then this method sends to the background thread a |
| * request for fetching that name and update cell property when name become known. |
| */ |
| @Override |
| public ObservableValue<String> call(final TableColumn.CellDataFeatures<Code,String> cell) { |
| return getName(cell.getValue()).getReadOnlyProperty(); |
| } |
| |
| /** |
| * Returns the name (or description) for the given code. |
| * If the name is not available, then this method sends to the background thread a request |
| * for fetching that name and will update the returned property when the name become known. |
| */ |
| final ReadOnlyStringWrapper getName(final Code code) { |
| final ReadOnlyStringWrapper p = code.name(); |
| final String name = p.getValue(); |
| if (name == null) { |
| loader.requestName(code); |
| } |
| return p; |
| } |
| |
| /** |
| * Adds new codes in this list and/or updates existing codes with CRS names. |
| * This method is invoked after the background thread has loaded new codes, |
| * and/or after that thread has fetched names (descriptions) of some codes. |
| * We combine those two tasks in a single method in order to send a single event. |
| * This method must be invoked in JavaFX thread. |
| */ |
| private void update(final PartialResult result) { |
| assert Platform.isFxApplicationThread(); |
| final int s = codes.size(); |
| if (result.codes != null) { |
| codes.addAll(Arrays.asList(result.codes)); |
| } |
| beginChange(); |
| nextAdd(s, codes.size()); |
| if (result.names != null) { |
| final ListIterator<Object> it = codes.listIterator(); |
| while (it.hasNext()) { |
| final Object value = it.next(); |
| final String name = result.names.remove(value); |
| if (name != null) { |
| final int i = it.previousIndex(); |
| if (name.isEmpty()) { |
| it.remove(); // Remove code that we cannot resolve. |
| nextRemove(i, (Code) value); // ClassCastException should never happen here. |
| } else { |
| ((Code) value).name().set(name); // ClassCastException should never happen here. |
| describedCount++; |
| nextUpdate(i); |
| } |
| } |
| } |
| } |
| endChange(); |
| if (describedCount >= codes.size()) { |
| removeHourglass(); |
| } |
| } |
| |
| /** |
| * Removes the hourglass icon which was shown in the table during initial data loading phase. |
| * Removing this icon restores the JavaFX default behavior, which is to show "no data" when the |
| * list is empty. We want this default behavior when we think that there is no more data to load. |
| * This is especially important when the user applies a filter which produces an empty result. |
| * Since the effect is only visual, its okay if the criterion for invoking this method is approximate. |
| */ |
| private void removeHourglass() { |
| if (owner != null) { |
| owner.setPlaceholder(null); |
| owner = null; |
| } |
| } |
| |
| /** |
| * The result of fetching authority codes and/or fetching CRS names in a background thread. |
| */ |
| private static final class PartialResult { |
| /** |
| * New CRS authority codes, or {@code null} if none. |
| */ |
| final Object[] codes; |
| |
| /** |
| * Names for some CRS codes as a modifiable map, or {@code null} if none. |
| * Empty values mean that the code should be removed (because it has an error). |
| */ |
| final Map<Code,String> names; |
| |
| /** |
| * Creates a new partial result. |
| */ |
| PartialResult(final Object[] codes, final Map<Code,String> names) { |
| this.codes = codes; |
| this.names = names; |
| } |
| } |
| |
| /** |
| * Loads CRS authority codes in background thread. The background thread may send tasks to be executed |
| * in JavaFX thread before the final result. The final result returned by {@link #getValue()} contains |
| * only codes that have not been fetched by previous {@code Loader} task executions, or the codes for |
| * which names need to be updated (see {@link #call()} for more information). |
| */ |
| private final class Loader extends Task<PartialResult> { |
| /** |
| * The items for which {@link Code#name} has been requested. |
| * Completing those items have priority over completing {@link AuthorityCodes} because |
| * those completion requests should happen only for cells that are currently visible. |
| * This list is read and written by two different threads; usages must be synchronized. |
| */ |
| private final List<Code> toDescribe; |
| |
| /** |
| * {@code true} for loading authority codes in addition of processing {@link #toDescribe}, |
| * or {@code false} if codes are already loaded. In later case this task will only process |
| * the {@link #toDescribe} list. |
| */ |
| private final boolean loadCodes; |
| |
| /** |
| * Wether this task has been scheduled for execution or is already executing. |
| * This flag shall be read and updated in JavaFX thread only. We cannot rely |
| * on {@link #isRunning()} because that method does not return {@code true} |
| * immediately after {@link BackgroundThreads#execute(Runnable)} invocation. |
| * |
| * @see #start() |
| */ |
| private boolean isRunning; |
| |
| /** |
| * Creates a new loader. |
| */ |
| Loader() { |
| toDescribe = new ArrayList<>(); |
| loadCodes = true; |
| } |
| |
| /** |
| * Invoked after a background thread finished its task. Prepares a new background thread |
| * for loading names (descriptions) for authority codes listed in {@link #toDescribe}. |
| */ |
| private Loader(final Loader previous) { |
| toDescribe = previous.toDescribe; |
| loadCodes = false; |
| } |
| |
| /** |
| * Schedule for execution in a background thread. |
| * This method shall be invoked in JavaFX thread. |
| */ |
| final void start() { |
| isRunning = true; |
| BackgroundThreads.execute(this); |
| } |
| |
| /** |
| * Sends to this background thread a request for fetching the name (description) of given code. |
| * The {@link AuthorityCodes} list will receive an update event after the name has been fetched. |
| * This method must be invoked from JavaFX thread. |
| * |
| * @param code the CRS authority code for which to fetch the name in background thread. |
| */ |
| final void requestName(final Code code) { |
| assert Platform.isFxApplicationThread(); |
| synchronized (toDescribe) { |
| toDescribe.add(code); |
| } |
| /* |
| * This task may be created and ready but not yet started. It happens if `scheduleNewLoader()` |
| * found no code to process in the `toDescribe` list at the time that method has been invoked. |
| */ |
| if (!isRunning) { |
| start(); |
| } |
| } |
| |
| /** |
| * Fetches the names of all objects in the {@link #toDescribe} array and clears that array. |
| * The names are returned as a map with {@link Code} as keys and names (or descriptions) as values. |
| * This method is invoked from a background thread and the returned value will be consumed in JavaFX thread. |
| * Some entries in the returned map be empty strings if the corresponding code should be removed. |
| * |
| * @param factory value of {@link #getFactory()}. |
| * @return the names of CRS authority codes submitted to {@link #requestName(Code)}, or {@code null} if none. |
| */ |
| private Map<Code,String> processNameRequests(final CRSAuthorityFactory factory) { |
| final Code[] snapshot; |
| synchronized (toDescribe) { |
| final int size = toDescribe.size(); |
| if (size == 0) return null; |
| snapshot = toDescribe.toArray(new Code[size]); |
| toDescribe.clear(); |
| } |
| final Map<Code,String> updated = new IdentityHashMap<>(snapshot.length); |
| for (final Code code : snapshot) { |
| String text; |
| try { |
| var i18n = factory.getDescriptionText(CoordinateReferenceSystem.class, code.code).orElse(null); |
| text = Strings.trimOrNull(Types.toString(i18n, locale)); |
| if (text == null) { |
| text = Vocabulary.forLocale(locale).getString(Vocabulary.Keys.Unnamed); |
| } |
| } catch (FactoryException e) { |
| errorOccurred(e); |
| text = ""; // Tells `AuthorityCodes.update(PartialResult)` to remove this code. |
| } |
| updated.put(code, text); // Do not update code in this thread; it will be updated in JavaFX thread. |
| } |
| return updated; |
| } |
| |
| /** |
| * Invoked in background thread for reading authority codes. Intermediate results are sent |
| * to the JavaFX thread every {@value #REFRESH_DELAY} nanoseconds. Requests for code names |
| * are also handled in priority since they are typically required for visible cells. |
| * |
| * @return one or both of the followings: |
| * <ul> |
| * <li>An array of {@code String}s which contains the remaining codes that need |
| * to be sent to {@link AuthorityCodes} list.</li> |
| * <li>A {@code Map<Code,String>} which contains the codes for which the names |
| * or descriptions have been updated.</li> |
| * </ul> |
| * |
| * @throws Exception if an error occurred while fetching the codes or the names/descriptions. |
| */ |
| @Override |
| protected PartialResult call() throws Exception { |
| long lastTime = System.nanoTime(); |
| List<String> codes = List.of(); |
| final CRSAuthorityFactory factory = getFactory(); |
| try { |
| if (loadCodes) { |
| codes = new ArrayList<>(100); |
| final Iterator<String> it = factory.getAuthorityCodes(type).iterator(); |
| while (it.hasNext()) { |
| codes.add(it.next()); |
| if (System.nanoTime() - lastTime > REFRESH_DELAY) { |
| final PartialResult p = new PartialResult(codes.toArray(), processNameRequests(factory)); |
| codes.clear(); |
| Platform.runLater(() -> update(p)); |
| lastTime = System.nanoTime(); |
| } |
| } |
| } |
| /* |
| * At this point we loaded all authority codes. If there is some remaining codes, |
| * returns them immediately for allowing the user interface to be updated quickly. |
| * If there are no more codes to return, wait a little bit for giving a chance to |
| * the `toDescribe` list to be populated with more requests, then process them. |
| */ |
| if (codes.isEmpty()) { |
| Thread.sleep(REFRESH_DELAY / StandardDateFormat.NANOS_PER_MILLISECOND); |
| return new PartialResult(null, processNameRequests(factory)); |
| } |
| } catch (BackingStoreException e) { |
| throw e.unwrapOrRethrow(Exception.class); |
| } |
| return new PartialResult(codes.toArray(), null); |
| } |
| |
| /** |
| * Invoked after the background thread finished to load authority codes. |
| * This method adds the remaining codes to {@link AuthorityCodes} list, |
| * then prepare another background tasks for loading descriptions. |
| */ |
| @Override |
| @SuppressWarnings("unchecked") |
| protected void succeeded() { |
| update(getValue()); |
| prepareNewLoader(); |
| } |
| |
| /** |
| * Invoked if an error occurred while loading the codes. A pseudo-code is added with error message. |
| * A background task is still scheduled for allowing {@link AuthorityCodes} to get descriptions of |
| * codes obtained so far. |
| */ |
| @Override |
| protected void failed() { |
| final Throwable e = getException(); |
| errorOccurred(e); |
| if (loadCodes) { |
| final Code code = new Code(Vocabulary.forLocale(locale).getString(Vocabulary.Keys.Errors)); |
| String message = Exceptions.getLocalizedMessage(e, locale); |
| if (message == null) { |
| message = e.getClass().getSimpleName(); |
| } |
| code.name().set(message); |
| add(code); |
| } |
| removeHourglass(); |
| prepareNewLoader(); |
| } |
| |
| /** |
| * Prepares the next task for loading descriptions. If new description requests were posted |
| * between the end of {@link #call()} execution and the start of the {@link #succeeded()} or |
| * {@link #failed()} execution, starts the new task immediately. |
| */ |
| private void prepareNewLoader() { |
| assert Platform.isFxApplicationThread(); |
| isRunning = false; |
| loader = new Loader(this); |
| final boolean isEmpty; |
| synchronized (toDescribe) { |
| isEmpty = toDescribe.isEmpty(); |
| } |
| if (!isEmpty) { |
| loader.start(); |
| } |
| } |
| } |
| |
| /** |
| * Invoked when an error occurred. This method may be invoked from any thread. |
| * Current implementation logs the first error. |
| */ |
| private void errorOccurred(final Throwable e) { |
| if (!hasError) { |
| hasError = true; // Not a big problem if we have race condition; error will just be logged twice. |
| Logging.unexpectedException(LOGGER, AuthorityCodes.class, "get", e); |
| } |
| } |
| } |