blob: 00d4c83f5f6a0635299ea3ade639a6605b547bc0 [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.Locale;
import java.util.Optional;
import javafx.collections.ObservableList;
import javafx.collections.transformation.FilteredList;
import javafx.concurrent.Task;
import javafx.event.ActionEvent;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.ButtonType;
import javafx.scene.control.Control;
import javafx.scene.control.Dialog;
import javafx.scene.control.DialogPane;
import javafx.scene.control.Label;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.control.TextField;
import javafx.scene.control.ToggleButton;
import javafx.scene.control.Tooltip;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.paint.Color;
import javafx.scene.text.Font;
import javafx.stage.Window;
import javafx.util.Duration;
import org.opengis.geometry.Envelope;
import org.opengis.util.FactoryException;
import org.opengis.referencing.crs.GeodeticCRS;
import org.opengis.referencing.crs.GeographicCRS;
import org.opengis.referencing.crs.ProjectedCRS;
import org.opengis.referencing.crs.VerticalCRS;
import org.opengis.referencing.crs.TemporalCRS;
import org.opengis.referencing.crs.CompoundCRS;
import org.opengis.referencing.crs.EngineeringCRS;
import org.opengis.referencing.crs.CRSAuthorityFactory;
import org.opengis.referencing.crs.CoordinateReferenceSystem;
import org.opengis.referencing.operation.Conversion;
import org.opengis.referencing.operation.OperationMethod;
import org.apache.sis.referencing.IdentifiedObjects;
import org.apache.sis.referencing.privy.ReferencingUtilities;
import org.apache.sis.gui.internal.BackgroundThreads;
import org.apache.sis.gui.internal.ExceptionReporter;
import org.apache.sis.gui.internal.IdentityValueFactory;
import org.apache.sis.gui.internal.Resources;
import org.apache.sis.gui.internal.Styles;
import org.apache.sis.geometry.ImmutableEnvelope;
import org.apache.sis.metadata.iso.extent.Extents;
import org.apache.sis.util.Exceptions;
import org.apache.sis.util.resources.Vocabulary;
// Specific to the geoapi-3.1 and geoapi-4.0 branches:
import org.opengis.referencing.ObjectDomain;
// Specific to the geoapi-4.0 branch:
import org.opengis.referencing.crs.DerivedCRS;
/**
* A list of Coordinate Reference Systems (CRS) from which the user can select.
* The CRS choices is built in a background thread from a specified {@link CRSAuthorityFactory}.
*
* @author Johann Sorel (Geomatys)
* @author Martin Desruisseaux (Geomatys)
* @version 1.5
* @since 1.1
*/
public class CRSChooser extends Dialog<CoordinateReferenceSystem> {
/**
* The pane where the controls for this CRS chooser will be put.
* The top part contains the tools bar. The center part contains
* the table or the WKT and change depending on user actions.
*/
private final BorderPane content;
/**
* The tools bar for this pane. Children are in this order:
* <ul>
* <li>A {@link Label} for the second child.</li>
* <li>A text field, combo box or other control.</li>
* <li>An arbitrary number of buttons.</li>
* </ul>
*/
private final HBox tools;
/**
* The text field where user can enter a fragment of the name of the CRS (s)he is looking for.
*/
private final TextField searchField;
/**
* The table showing CRS codes together with their names. Table items are provided by a background thread.
* Items are initially authority codes as {@link Code} instances without {@link Code#name} value.
* Names are completed later when needed.
*/
private final TableView<Code> table;
/**
* A panel showing the type and domain of validity of selected CRS.
*/
private final GridPane summary;
/**
* The label where to write the CRS type and domain of validity.
*/
private final Label type, domain;
/**
* The area of interest, or {@code null} if none.
* Axis order is (<var>longitude</var>, <var>latitude</var>).
*/
private final ImmutableEnvelope areaOfInterest;
/**
* The pane showing the CRS in Well Known Text format.
* Created when first needed.
*/
private WKTPane wktPane;
/**
* Creates a chooser proposing all coordinate reference systems from the default factory.
*/
public CRSChooser() {
this(null, null, null);
}
/**
* Creates a chooser proposing all coordinate reference systems from the given factory.
* 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 factory to use for creating coordinate reference systems, or {@code null} for default.
* @param areaOfInterest geographic area for which to choose a CRS, or {@code null} if no restriction.
* @param locale the preferred locale for displaying object name, or {@code null} for the default locale.
*/
@SuppressWarnings({"unchecked", "this-escape"})
public CRSChooser(final CRSAuthorityFactory factory, final Envelope areaOfInterest, Locale locale) {
this.areaOfInterest = Utils.toGeographic(CRSChooser.class, "<init>", areaOfInterest);
if (locale == null) locale = Locale.getDefault();
final Resources i18n = Resources.forLocale(locale);
final Vocabulary vocabulary = Vocabulary.forLocale(locale);
final AuthorityCodes codeList = new AuthorityCodes(factory, locale);
table = new TableView<>(codeList);
codeList.owner = table;
/*
* Columns to show in CRS table. First column is typically EPSG codes and second
* column is the CRS descriptions. The content is loaded in a background thread.
*/
final TableColumn<Code,Code> codes = new TableColumn<>(vocabulary.getString(Vocabulary.Keys.Code));
final TableColumn<Code,String> names = new TableColumn<>(vocabulary.getString(Vocabulary.Keys.Name));
names.setCellValueFactory(codeList);
codes.setCellValueFactory(IdentityValueFactory.instance());
codes.setCellFactory(Code.Cell::new);
codes.setMinWidth ( 60); // Will be the initial size of this column.
codes.setMaxWidth (120); // Seems to be required for preventing `codes` to be as large as `names`.
table.setPrefWidth(500);
table.getColumns().setAll(codes, names);
table.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY_FLEX_LAST_COLUMN);
final Label clock = new Label("\u23F3"); // Unicode U+23F3: Hourglass With Flowing Sand.
clock.setFont(Font.font(30));
table.setPlaceholder(clock);
/*
* Controls on the top of CRS list. This is either a filter or a combox box
* giving WKT format choices, depending on what is currently shown.
*/
{// block for keeping variable locales.
/*
* Text field for filtering the list of CRS codes using keywords.
* The filtering is applied when the "Enter" key is pressed in that field.
*/
searchField = new TextField();
searchField.setOnAction((ActionEvent event) -> {
CodeFilter.apply(table, searchField.getText());
});
HBox.setHgrow(searchField, Priority.ALWAYS);
final Label label = new Label(vocabulary.getString(Vocabulary.Keys.Filter));
label.setLabelFor(searchField);
/*
* Button for showing the CRS description in Well Known Text (WKT) format.
* The button is enabled only if a row in the table is selected.
*/
final ToggleButton infoButton = new ToggleButton("\uD83D\uDDB9"); // Unicode U+1F5B9: Document With Text.
table.getSelectionModel().selectedItemProperty().addListener((e,o,n) -> {
infoButton.setDisable(n == null);
updateSummary(n);
});
infoButton.setOnAction((ActionEvent event) -> {
setTools(infoButton.isSelected());
});
infoButton.setDisable(true);
/*
* Creates the tools bar to show above the table of codes.
* The tools bar contains the search field and the button for showing the WKT.
*/
tools = new HBox(label, searchField, infoButton);
tools.setSpacing(9);
tools.setAlignment(Pos.BASELINE_LEFT);
BorderPane.setMargin(tools, new Insets(0, 0, 9, 0));
}
/*
* Details about the selected items. This is a form with the following lines:
* - Type (e.g. "Projected — Transverse Mercator").
* - Domain of validity.
*/
{// block for keeping variable locales.
final Label lt = new Label(vocabulary.getLabel(Vocabulary.Keys.Type));
final Label ld = new Label(vocabulary.getLabel(Vocabulary.Keys.Domain));
lt.setLabelFor(type = new Label());
ld.setLabelFor(domain = new Label());
summary = Styles.createControlGrid(0, lt, ld);
final Tooltip tp = new Tooltip();
tp.setShowDelay(Duration.seconds(0.5));
tp.setShowDuration(Duration.minutes(1));
tp.maxWidthProperty().bind(summary.widthProperty());
tp.setWrapText(true);
domain.setTooltip(tp);
}
/*
* Layout table and tools bar inside the dialog content.
* Configure the dialog buttons.
*/
final DialogPane pane = getDialogPane();
content = new BorderPane();
content.setCenter(table);
content.setTop(tools);
content.setBottom(summary);
pane.setContent(content);
pane.getButtonTypes().setAll(ButtonType.OK, ButtonType.CANCEL);
setTitle(i18n.getString(Resources.Keys.SelectCRS));
setResultConverter(this::getSelectedCRS);
setResizable(true);
}
/**
* Sets the tools bar and its content to controls for the given mode.
* If {@code wkt} is {@code true}, then this method set the controls for showing the WKT.
* If {@code wkt} is {@code false} (the default), then this method set the controls to the table of CRS codes.
*/
private void setTools(final boolean wkt) {
final Locale locale = getAuthorityCodes().locale;
final short labelText;
final Control control;
final Control main;
final GridPane info;
if (wkt) {
if (wktPane == null) {
wktPane = new WKTPane(locale);
}
wktPane.setContent(getAuthorityCodes(), table.getSelectionModel().getSelectedItem().code);
labelText = Vocabulary.Keys.Format;
control = wktPane.convention;
main = wktPane.text;
info = null;
} else {
labelText = Vocabulary.Keys.Filter;
control = searchField;
main = table;
info = summary;
}
final ObservableList<Node> children = tools.getChildren();
final Label label = (Label) children.get(0);
final Vocabulary vocabulary = Vocabulary.forLocale(locale);
label.setText(vocabulary.getLabel(labelText));
label.setLabelFor(control);
children.set(1, control);
content.setCenter(main);
content.setBottom(info);
}
/**
* Returns the list of all authority codes. The list may not be complete at the
* time this method returns because codes are loaded in a background thread.
*/
private AuthorityCodes getAuthorityCodes() {
ObservableList<?> items = table.getItems();
if (items instanceof FilteredList<?>) {
items = ((FilteredList<?>) items).getSource();
}
return (AuthorityCodes) items;
}
/**
* Invoked when a new CRS is selected in the table. This method updates
* the {@link #type} and {@link #domain} fields with CRS information.
*/
private void updateSummary(final Code selected) {
if (selected == null) {
clearSummary();
return;
}
final AuthorityCodes source = getAuthorityCodes();
final String code = selected.code;
BackgroundThreads.execute(new Task<CoordinateReferenceSystem>() {
/** Invoked in background thread for fetching the CRS from an authority code. */
@Override protected CoordinateReferenceSystem call() throws FactoryException {
return source.getFactory().createCoordinateReferenceSystem(code);
}
/** Invoked in JavaFX thread on success. */
@Override protected void succeeded() {
final CoordinateReferenceSystem crs = getValue();
type.setTextFill(Styles.NORMAL_TEXT);
type.setText(typeOf(crs, source.locale));
setDomainOfValidity(crs, source.locale);
}
/** Invoked in JavaFX thread on cancellation. */
@Override protected void cancelled() {
clearSummary();
}
/** Invoked in JavaFX thread on failure. */
@Override protected void failed() {
cancelled();
type.setTextFill(Styles.ERROR_TEXT);
type.setText(Exceptions.getLocalizedMessage(getException(), source.locale));
}
});
}
/**
* Clears the {@link #type} and {@link #domain} fields.
*/
private void clearSummary() {
type.setText(null);
domain.setText(null);
}
/**
* Sets the text that describes the domain of validity.
*/
private void setDomainOfValidity(final CoordinateReferenceSystem crs, final Locale locale) {
String extent = null;
for (ObjectDomain c : crs.getDomains()) {
extent = Extents.getDescription(c.getDomainOfValidity(), locale);
if (extent != null) break;
}
String tip = extent;
Color color = Styles.NORMAL_TEXT;
if (!Utils.intersects(areaOfInterest, crs)) {
tip = Resources.forLocale(locale).getString(Resources.Keys.DoesNotCoverAOI);
extent = Styles.WARNING_ICON + " " + (extent != null ? extent : tip);
color = Styles.ERROR_TEXT;
}
domain.setTextFill(color);
domain.setText(extent);
domain.getTooltip().setText(tip);
}
/**
* Returns the text to show of right of the "type" label.
*/
private static String typeOf(CoordinateReferenceSystem crs, final Locale locale) {
while (crs instanceof CompoundCRS) {
crs = ((CompoundCRS) crs).getComponents().get(0);
}
final short key;
final int expected;
if (crs instanceof GeographicCRS) {key = Vocabulary.Keys.Geographic; expected = 2;}
else if (crs instanceof GeodeticCRS) {key = Vocabulary.Keys.Geocentric; expected = 2;}
else if (crs instanceof VerticalCRS) {key = Vocabulary.Keys.Vertical; expected = 1;}
else if (crs instanceof TemporalCRS) {key = Vocabulary.Keys.Temporal; expected = 1;}
else if (crs instanceof ProjectedCRS) {key = Vocabulary.Keys.Projected; expected = 2;}
else if (crs instanceof EngineeringCRS) {key = Vocabulary.Keys.Engineering; expected = 0;}
else {
key = Vocabulary.Keys.NotKnown;
expected = 0;
}
String text = Vocabulary.forLocale(locale).getString(key);
final int dimension = ReferencingUtilities.getDimension(crs);
final boolean addDimension = (dimension != expected && expected != 0);
final boolean isProjection = (crs instanceof DerivedCRS);
if (addDimension | isProjection) {
final StringBuilder buffer = new StringBuilder(text);
if (addDimension) {
buffer.append(" (").append(dimension).append("D)");
}
if (isProjection) {
final Conversion conversion = ((DerivedCRS) crs).getConversionFromBase();
if (conversion != null) {
final OperationMethod method = conversion.getMethod();
if (method != null) {
final String name = IdentifiedObjects.getDisplayName(method, locale);
if (name != null) {
buffer.append(" — ").append(name);
}
}
}
}
text = buffer.toString();
}
return text;
}
/**
* Returns the currently selected CRS, or {@code null} if none.
*
* @return the currently selected CRS, or {@code null}.
*/
private CoordinateReferenceSystem getSelectedCRS(final ButtonType button) {
if (ButtonType.OK.equals(button)) {
final Code code = table.getSelectionModel().getSelectedItem();
if (code != null) try {
return getAuthorityCodes().getFactory().createCoordinateReferenceSystem(code.code);
} catch (FactoryException e) {
ExceptionReporter.canNotCreateCRS(getOwner(), code.code, e);
}
}
return null;
}
/**
* Shows a dialog to select a {@link CoordinateReferenceSystem}.
*
* @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) {
initOwner(parent);
return showAndWait();
}
}