/* * Geotoolkit.org - An Open Source Java GIS Toolkit * http://www.geotoolkit.org * * (C) 2005-2012, Open Source Geospatial Foundation (OSGeo) * (C) 2009-2012, Geomatys * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; * version 2.1 of the License. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. */ package org.geotoolkit.gui.swing.referencing; import java.util.Locale; import java.util.regex.Pattern; import java.util.regex.Matcher; import java.util.concurrent.ExecutionException; import java.awt.BorderLayout; import java.awt.CardLayout; import java.awt.Dimension; import java.awt.EventQueue; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.WindowAdapter; import java.awt.event.WindowEvent; import javax.swing.Box; import javax.swing.JPanel; import javax.swing.JLabel; import javax.swing.JButton; import javax.swing.JComboBox; import javax.swing.JComponent; import javax.swing.JTextField; import javax.swing.ComboBoxModel; import javax.swing.DefaultComboBoxModel; import javax.swing.ListCellRenderer; import javax.swing.SwingWorker; import org.opengis.util.FactoryException; import org.opengis.referencing.AuthorityFactory; import org.opengis.referencing.IdentifiedObject; import org.opengis.referencing.crs.CoordinateReferenceSystem; import org.apache.sis.util.Classes; import org.geotoolkit.internal.swing.SwingUtilities; import org.geotoolkit.internal.swing.FastComboBox; import org.geotoolkit.resources.Vocabulary; import org.geotoolkit.factory.AuthorityFactoryFinder; import org.geotoolkit.factory.FactoryRegistryException; import org.geotoolkit.gui.swing.IconFactory; import org.geotoolkit.gui.swing.Window; import org.geotoolkit.gui.swing.WindowCreator; /** * A combo box for selecting a coordinate reference system from a list. This component also * provides a search button (for filtering the CRS name that contain the specified keywords) * and a info button displaying the CRS {@linkplain PropertiesSheet properties sheet}. * * @author Martin Desruisseaux (IRD, Geomatys) * @module */ @SuppressWarnings("serial") public class AuthorityCodesComboBox extends WindowCreator { /** * The key for listening to changes in the combo box selection. This event may occurs as * a result of client selection, or of explicit call to {@link #setSelectedCode(String)}. * The values in the event are authority codes (<strong>not</strong> fully constructed * CRS objects). * <p> * Client can listen for those changes with the following code: * * {@code java * chooser.addPropertyChangeListener(SELECTED_CODE_PROPERTY, listener); * } * * @since 3.16 */ public static final String SELECTED_CODE_PROPERTY = "selectedCode"; /** * Action commands. */ private static final String SEARCH="SEARCH", CONFIRM="CONFIRM", SELECT="SELECT", INFO="INFO"; /** * The authority factory responsible for creating objects from a list of codes. */ private final AuthorityFactory factory; /** * The list of authority codes, as a combo box model. The elements in this * list are instances of {@link AuthorityCode}. */ private final AuthorityCodeList codeList; /** * The list of CRS objects. The elements in this combo box are instances of * {@link AuthorityCode}, and the list model is {@link #codeList}. */ private final JComboBox<AuthorityCode> codeComboBox; /** * The text field for searching item. */ private final JTextField searchField; /** * The {@link #searchField} or {@link #codeComboBox} field. */ private final JPanel searchOrList; /** * The card layout showing either {@link #codeComboBox} or {@link #searchField}. */ private final CardLayout cards; /** * The button to press for showing the search field or the properties. */ private final JButton showSearchField, showProperties; /** * Info about the currently selected item. */ private PropertiesSheet properties; /** * The window that contains {@link #properties}. */ private Window propertiesWindow; /** * The currently selected code. This information is updated by {@link #selectionChanged()}. * * @since 3.16 */ private AuthorityCode selectedCode; /** * Creates a CRS chooser backed by the EPSG authority factory. * * @throws FactoryRegistryException if no EPSG authority factory has been found. */ public AuthorityCodesComboBox() throws FactoryRegistryException { this("EPSG"); } /** * Creates a CRS chooser backed by the specified authority factory. * * @param authority The authority identifier (e.g. {@code "EPSG"}). * @throws FactoryRegistryException if no authority factory has been found. * * @since 2.4 */ public AuthorityCodesComboBox(final String authority) throws FactoryRegistryException { this(AuthorityFactoryFinder.getCRSAuthorityFactory(authority, null)); } /** * Creates a CRS chooser backed by the specified authority factory. * * @param factory The authority factory responsible for creating objects from a list of codes. */ @SuppressWarnings("unchecked") public AuthorityCodesComboBox(final AuthorityFactory factory) { this(factory, CoordinateReferenceSystem.class); } /** * Creates a chooser for objects of the given types backed by the specified authority factory. * * @param factory The authority factory responsible for creating objects from a list of codes. * @param types The types of CRS object to includes in the list. */ @SafeVarargs public AuthorityCodesComboBox(final AuthorityFactory factory, final Class<? extends CoordinateReferenceSystem>... types) { this(factory,"00000000",types); } /** * Creates a chooser for objects of the given types backed by the specified authority factory. * * @param factory The authority factory responsible for creating objects from a list of codes. * @param types The types of CRS object to includes in the list. * @param codePrototype An average String prototype used in code cell renderer */ @SafeVarargs public AuthorityCodesComboBox(final AuthorityFactory factory, final String codePrototype, final Class<? extends CoordinateReferenceSystem>... types) { this.factory = factory; final Locale locale = getLocale(); final Vocabulary resources = Vocabulary.getResources(locale); final Listeners listeners = new Listeners(); setLayout(new BorderLayout()); cards = new CardLayout(); searchOrList = new JPanel(cards); codeList = new AuthorityCodeList(locale, factory, types); codeComboBox = new FastComboBox<>(codeList); searchField = new JTextField(); searchField .setActionCommand(CONFIRM); searchField .addActionListener(listeners); codeComboBox.setActionCommand(SELECT); codeComboBox.addActionListener(listeners); @SuppressWarnings("unchecked") final ListCellRenderer<Object> renderer = (ListCellRenderer) codeComboBox.getRenderer(); if (renderer instanceof JLabel) { // This is the case of typical Swing implementations. codeComboBox.setRenderer(new AuthorityCodeRenderer(renderer,codePrototype)); } codeComboBox.setPrototypeDisplayValue(new AuthorityCode()); Dimension size = codeComboBox.getPreferredSize(); size.width = 400; // Example of text to hold: "Unknown datum based upon the Average Terrestrial System 1977 ellipsoid" codeComboBox.setPreferredSize(size); searchOrList.add(codeComboBox, "List"); searchOrList.add(searchField, "Search"); add(searchOrList, BorderLayout.CENTER); /* * Add the "Info" button. */ JButton button; size = new Dimension(24, 20); final IconFactory icons = IconFactory.DEFAULT; String label = resources.getString(Vocabulary.Keys.Informations); button = icons.getButton("crystalProject/16/actions/info.png", label, label); button.setEnabled(false); button.setFocusable(false); button.setPreferredSize(size); button.setActionCommand(INFO); button.addActionListener(listeners); showProperties = button; /* * Add the "Search" button. */ label = resources.getString(Vocabulary.Keys.Search); button = icons.getButton("crystalProject/16/actions/find.png", label, label); button.setFocusable(false); button.setPreferredSize(size); button.setActionCommand(SEARCH); button.addActionListener(listeners); showSearchField = button; /* * Add the two buttons after the combo box. */ final Box box = Box.createHorizontalBox(); box.add(button); box.add(showProperties); add(box, BorderLayout.LINE_END); } /** * Various listeners used by the enclosing class. */ private final class Listeners implements ActionListener { @Override public void actionPerformed(final ActionEvent event) { final String action = event.getActionCommand(); switch (action) { case SEARCH: search(true); break; case CONFIRM: search(false); break; case INFO: showProperties(true); break; case SELECT: selectionChanged(); break; } } } /** * Invoked when the selection in the combo box changed. This method will enable or * disable the properties panel, and fire a change event. */ final void selectionChanged() { final AuthorityCode oldValue = selectedCode; final AuthorityCode newValue = (AuthorityCode) codeComboBox.getSelectedItem(); selectedCode = newValue; showProperties.setEnabled(newValue != null); firePropertyChange(SELECTED_CODE_PROPERTY, (oldValue != null) ? oldValue.code : null, (newValue != null) ? newValue.code : null); } /** * Returns the authority name. This is useful for example in order to provide a window title. * * {@section Multi-threading} * This method can be safely invoked from any thread - not necessarily the <cite>Swing</cite> * thread. This is assuming that the {@linkplain AuthorityFactory Authority Factory} provided * at construction time is thread-safe, but this is the case of all Geotk implementations. * * @return The current authority name. */ public String getAuthority() { return factory.getAuthority().getTitle().toString(getLocale()); } /** * Returns the code for the selected object, or {@code null} if none. * * {@section Multi-threading} * This method can be safely invoked from any thread - not necessarily the <cite>Swing</cite> * thread. This is a requirement for allowing {@link #getSelectedItem()} to be safely invoked * from a background thread. * * @return The code of the currently selected object. */ public String getSelectedCode() { final AuthorityCode code; if (EventQueue.isDispatchThread()) { code = (AuthorityCode) codeComboBox.getSelectedItem(); } else { final class Delegate implements Runnable { Object code; @Override public void run() { code = codeComboBox.getSelectedItem(); } } final Delegate del = new Delegate(); SwingUtilities.invokeAndWait(del); code = (AuthorityCode) del.code; } return (code != null) ? code.code : null; } /** * Sets the selected object to the one having the given code. If the given object is * {@code null}, then this method clears the selection. * * @param code The authority code of the object to set as the selected index, or {@code null}. * * @since 3.12 */ public void setSelectedCode(final String code) { if (code == null) { codeComboBox.setSelectedItem(null); } else { final ComboBoxModel<AuthorityCode> model = codeComboBox.getModel(); if (model instanceof AuthorityCodeList) { ((AuthorityCodeList) model).setSelectedCode(code); } else { final int size = model.getSize(); for (int i=0; i<size; i++) { final AuthorityCode c = model.getElementAt(i); if (code.equals(c.code)) { model.setSelectedItem(c); break; } } } } } /** * Returns the selected object, usually as a {@link CoordinateReferenceSystem}. The default * implementation {@linkplain AuthorityFactory#createObject(String) creates the object} * identified by the value returned by {@link #getSelectedCode()}. Subclasses can override * any of the {@code getSelectedCode()} or {@code getSelectedItem()} methods if different * objects should be created. * * {@section Multi-threading} * This method can be safely invoked from any thread - not necessarily the <cite>Swing</cite> * thread. This is assuming that the {@linkplain AuthorityFactory Authority Factory} provided * at construction time is thread-safe, but this is the case of all Geotk implementations. * * @return The currently selected object. * @throws FactoryException if the factory can't create the selected object. */ public IdentifiedObject getSelectedItem() throws FactoryException { final String code = getSelectedCode(); return (code != null) ? factory.createObject(code) : null; } /** * Displays information about the currently selected item in a separated window. * This method is invoked automatically when the user press the "Info" button. * <p> * The default implementation invokes {@link #getSelectedItem()} in a background thread, * then shows general information and the object <cite>Well Know Text</cite> in a * {@link PropertiesSheet}. * * @param visible {@code true} for invoking {@link JComponent#setVisible(boolean)} * unconditionally. In some L&F, this bring the focus on the window. */ final void showProperties(final boolean visible) { new SwingWorker<IdentifiedObject,Object>() { private String title, message; /** * Creates the IdentifiedObject in a background thread. */ @Override protected IdentifiedObject doInBackground() { IdentifiedObject item = null; try { item = getSelectedItem(); title = item.getName().getCode(); } catch (FactoryException e) { message = e.getLocalizedMessage(); if (message == null) { message = Classes.getShortClassName(e); } } return item; } /** * Invoked after the IdentifiedObject creation has been completed. * Creates the PropertiesSheet if not already done, updates it and * makes it visible. */ @Override protected void done() { IdentifiedObject item = null; try { item = get(); } catch (InterruptedException | ExecutionException e) { message = e.toString(); } if (title == null) { title = String.valueOf(codeComboBox.getSelectedItem()); } if (properties == null) { properties = new PropertiesSheet(); } if (propertiesWindow == null) { final Window window = getWindowHandler().createWindow(AuthorityCodesComboBox.this, properties, title); final class Listener extends WindowAdapter implements ActionListener { /** Invoked when the user selected a different CRS. */ @Override public void actionPerformed(final ActionEvent event) { if (window.isVisible()) { showProperties(false); } } /** Invoked when the window which contains {@code AuthorityComboBox} has been closed. */ @Override public void windowClosed(final WindowEvent event) { SwingUtilities.removeWindowListener(AuthorityCodesComboBox.this, this); window.dispose(); } } final Listener listener = new Listener(); SwingUtilities.addWindowListener(AuthorityCodesComboBox.this, listener); codeComboBox.addActionListener(listener); window.setDefaultCloseOperation(Window.HIDE_ON_CLOSE); window.setSize(600, 500); propertiesWindow = window; } else { propertiesWindow.setTitle(title); } if (item != null) { properties.setIdentifiedObject(item); } else { properties.setErrorMessage(message); } if (visible || !propertiesWindow.isVisible()) { propertiesWindow.setVisible(true); } } }.execute(); } /** * Enables or disables the search field. */ final void search(final boolean enable) { final JComponent component; final String name; if (enable) { component = searchField; name = "Search"; } else { component = codeComboBox; name = "List"; filter(searchField.getText()); if (propertiesWindow != null && propertiesWindow.isVisible()) { showProperties(false); } } showProperties.setEnabled(!enable); cards.show(searchOrList, name); component.requestFocus(); } /** * Displays only the CRS name that contains the specified keywords. The {@code keywords} * argument is a space-separated list, usually provided by the user after he pressed the * "Search" button. * * @param keywords space-separated list of keywords to look for. */ public void filter(String keywords) { if (keywords == null || ((keywords = keywords.trim()).length()) == 0) { codeComboBox.setModel(codeList); selectionChanged(); return; } /* * Quotes the keywords, except the spaces. Set the 'if' value to * 'false' if the user already provided a regular expression. */ if (true) { final StringBuilder buffer = new StringBuilder(".*"); for (final String token : keywords.split("\\s+")) { buffer.append(Pattern.quote(token)).append(".*"); } keywords = buffer.toString(); } /* * Filters the list in a background thread. * It may take a few seconds when invoked for the first time. */ showSearchField.setEnabled(false); codeComboBox.setEnabled(false); final String words = keywords; new SwingWorker<ComboBoxModel<AuthorityCode>, Object>() { @Override protected ComboBoxModel<AuthorityCode> doInBackground() { Matcher matcher = null; final DefaultComboBoxModel<AuthorityCode> filtered = new DefaultComboBoxModel<>(); final int size = codeList.getSize(); for (int i=0; i<size; i++) { final AuthorityCode code = codeList.getElementAt(i); final String name = code.toString(); if (matcher == null) { matcher = Pattern.compile(words, Pattern.CASE_INSENSITIVE | Pattern.UNICODE_CASE).matcher(name); } else { matcher.reset(name); } if (matcher.matches()) { filtered.addElement(code); } else { matcher.reset(code.code); if (matcher.matches()) { filtered.addElement(code); } } } return filtered; } /** * Invoked in the Swing thread after the filtering has been completed. * This method assigns the new model to the combo box and enable it. */ @Override protected void done() { showSearchField.setEnabled(true); codeComboBox.setEnabled(true); ComboBoxModel<AuthorityCode> model; try { model = get(); } catch (InterruptedException | ExecutionException e) { getWindowHandler().showError(AuthorityCodesComboBox.this, new JLabel(e.toString()), null); return; } codeComboBox.setModel(model); selectionChanged(); } }.execute(); } }