/*
* GeoTools - The Open Source Java GIS Toolkit
* http://geotools.org
*
* (C) 2008-2011, Open Source Geospatial Foundation (OSGeo)
*
* 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.geotools.swing.dialog;
import java.awt.Dimension;
import java.awt.Point;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.util.concurrent.SynchronousQueue;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.swing.BorderFactory;
import javax.swing.JButton;
import javax.swing.JLabel;
import javax.swing.JList;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTextField;
import javax.swing.JViewport;
import javax.swing.ListSelectionModel;
import javax.swing.SwingUtilities;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import javax.swing.event.ListDataEvent;
import javax.swing.event.ListDataListener;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;
import net.miginfocom.swing.MigLayout;
import org.geotools.referencing.CRS;
import org.geotools.util.logging.Logging;
import org.opengis.referencing.crs.CoordinateReferenceSystem;
/**
* This class has a single static method that shows a dialog to prompt
* the user to choose a coordinate reference system.
* <p>
* Example of use:
* <pre><code>
* CoordinateReferenceSystem crs = JCRSChooser.showDialog();
* if (crs != null) {
* // use the CRS...
* }
* </code></pre>
*
* @author Michael Bedward
* @since 2.6
*
* @source $URL$
* @version $Id$
*/
public class JCRSChooser {
private static final Logger LOGGER = Logging.getLogger("org.geotools.swing");
/** Default authority name (EPSG). */
public static final String DEFAULT_AUTHORITY = "EPSG";
/** Default dialog title */
public static final String DEFAULT_TITLE = "Choose a projection";
private CRSDialog dialog;
private CoordinateReferenceSystem crs;
/**
* Constructor is hidden.
*/
private JCRSChooser() {}
/**
* Displays a dialog with a list of coordinate reference systems in the EPSG
* database.
* <p>
* This method can be called safely from any thread.
*
* @return a {@code CoordinateReferenceSystem} object or {@code null} if the user
* cancelled the dialog
*/
public static CoordinateReferenceSystem showDialog() {
return showDialog(null);
}
/**
* Displays a dialog with a list of coordinate reference systems in the EPSG
* database.
* <p>
* This method can be called safely from any thread.
*
* @param title optional non-default title
*
* @return a {@code CoordinateReferenceSystem} object or {@code null} if the user
* cancelled the dialog
*/
public static CoordinateReferenceSystem showDialog(final String title) {
return showDialog(title, null);
}
/**
* Displays a dialog with a list of coordinate reference systems in the EPSG
* database and with the specified initial code highlighted.
* <p>
* This method can be called safely from any thread.
*
* @param title optional non-default title
* @param initialCode optional initial EPSG code
*
* @return a {@code CoordinateReferenceSystem} object or {@code null} if the user
* cancelled the dialog
*/
public static CoordinateReferenceSystem showDialog(final String title,
final String initialCode) {
return showDialog(title, initialCode, null);
}
/**
* Displays a dialog with a list of coordinate reference systems provided by
* the given authority (e.g. "EPSG"), and with the specified initial code
* highlighted.
* <p>
* This method can be called safely from any thread.
*
* @param title optional non-default title
* @param initialCode an optional initial code in appropriate form for the authority
* @param authority optional non-default authority (defaults to "EPSG")
*
* @return a {@code CoordinateReferenceSystem} object or {@code null} if the user
* cancelled the dialog
*/
public static CoordinateReferenceSystem showDialog(final String title,
final String initialCode,
final String authority) {
CoordinateReferenceSystem selected = null;
if (SwingUtilities.isEventDispatchThread()) {
selected = doShow(title, initialCode, authority);
} else {
final SynchronousQueue<CoordinateReferenceSystem> sq =
new SynchronousQueue<CoordinateReferenceSystem>();
final Thread currentThread = Thread.currentThread();
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
try {
CoordinateReferenceSystem crs =
doShow(title, initialCode, authority);
if (crs == null) {
currentThread.interrupt();
} else {
sq.put(crs);
}
} catch (InterruptedException ex) {
throw new RuntimeException(ex);
}
}
});
try {
selected = sq.take();
} catch (InterruptedException ex) {
// dialog was cancelled
}
}
return selected;
}
/**
* Creates and displays the modal dialog.
*
* @param title optional non-default title
* @param initialCode an optional initial code in appropriate form for the authority
* @param authority optional non-default authority (defaults to "EPSG")
*
* @return the selected coordinate reference system or {@code null} if the dialog
* is cancelled by the user
*/
private static CoordinateReferenceSystem doShow(String title,
String initialCode, String authority) {
CRSDialog dialog = new CRSDialog(title, initialCode, authority);
DialogUtils.showCentred(dialog);
CoordinateReferenceSystem crs = dialog.getCoordinateReferenceSystem();
dialog.dispose();
return crs;
}
/**
* A modal dialog which displays a list of projections for the user to choose from.
* <p>
* This class is package-private, rather than private, for unit testing
* purposes.
*/
static class CRSDialog extends AbstractSimpleDialog {
private static final int CONTROL_WIDTH = 400;
private final String authority;
private final String initialCode;
private CRSListModel model;
private JList listBox;
private JButton okButton;
private CoordinateReferenceSystem crs;
/**
* Creates the dialog.
*
* @param title optional non-default title
* @param initialCode an optional initial code in appropriate form for the authority
* @param authority optional non-default authority (defaults to "EPSG")
*/
public CRSDialog(String title, String initialCode, String authority) {
super(DialogUtils.getString(title, DEFAULT_TITLE));
this.authority = DialogUtils.getString(authority, DEFAULT_AUTHORITY);
this.initialCode = initialCode;
initComponents();
}
@Override
public JPanel createControlPanel() {
JPanel panel = new JPanel(new MigLayout("", "[left]"));
model = new CRSListModel(authority);
panel.add(new JLabel("Enter sub-string to filter list"), "growx, wrap");
final JTextField filterFld = new JTextField();
filterFld.setPreferredSize(new Dimension(CONTROL_WIDTH, 20));
filterFld.getDocument().addDocumentListener(new DocumentListener() {
@Override
public void insertUpdate(DocumentEvent e) {
model.setFilter(filterFld.getText());
}
@Override
public void removeUpdate(DocumentEvent e) {
model.setFilter(filterFld.getText());
}
@Override
public void changedUpdate(DocumentEvent e) {
model.setFilter(filterFld.getText());
}
});
panel.add(filterFld, "wrap");
listBox = new JList(model);
listBox.addMouseListener(new MouseAdapter() {
@Override
public void mouseClicked(MouseEvent e) {
if (e.getClickCount() == 2) {
selectCRS(listBox.getSelectedIndex());
}
}
});
listBox.addListSelectionListener(new ListSelectionListener() {
@Override
public void valueChanged(ListSelectionEvent e) {
setOKButtonState();
}
});
model.addListDataListener(new CRSListModelListener() {
@Override
public void process() {
setOKButtonState();
}
});
JScrollPane listPane = new JScrollPane(listBox);
listPane.setPreferredSize(new Dimension(CONTROL_WIDTH, 300));
listBox.setBorder(BorderFactory.createEtchedBorder());
listBox.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
int initialIndex = model.findCode(initialCode);
if (initialIndex >= 0) {
listBox.setSelectedIndex(initialIndex);
Point p = listBox.indexToLocation(initialIndex);
JViewport port = listPane.getViewport();
port.setViewPosition(p);
}
panel.add(listPane, "gaptop 10, wrap");
return panel;
}
/**
* Overridden to get a reference to the OK button created by the
* super-class method.
*
* @return the button panel
*/
@Override
protected JPanel createButtonPanel() {
JPanel panel = super.createButtonPanel();
for (JButton btn : DialogUtils.getChildComponents(JButton.class, panel, false)) {
if ("OK".equals(btn.getText())) {
okButton = btn;
break;
}
}
if (okButton == null) {
throw new IllegalStateException("Failed to initialize the OK button correctly");
}
okButton.setEnabled(false);
return panel;
}
/**
* Records the selected coordinate reference system, if one exists,
* and hides the dialog.
*/
@Override
public void onOK() {
if (model.getSize() > 0 && listBox.getSelectedIndex() >= 0) {
if (model.getSize() == 1) {
selectCRS(0);
} else {
int index = listBox.getSelectedIndex();
if (index >= 0) {
selectCRS(index);
}
}
setVisible(false);
}
}
/**
* Sets the selection coordinate reference system to {@code null}
* and hides the dialog.
*/
@Override
public void onCancel() {
crs = null;
setVisible(false);
}
/**
* Helper method for the list box and {@linkplain #onOK()} method
* which records the selected coordinate reference system.
*
* @param index selected item index in the list box
*/
private void selectCRS(int index) {
String code = model.getCodeAt(index);
try {
crs = CRS.decode(DEFAULT_AUTHORITY + ":" + code, true);
} catch (Exception ex) {
LOGGER.log(Level.SEVERE,
"Failed to get coordinate reference system for code {0}",
code);
} finally {
closeDialog();
}
}
/**
* Enables or disables the OK button based on the state
* of the CRS list.
*/
private void setOKButtonState() {
boolean b = model.getSize() == 1 || listBox.getSelectedIndex() >= 0;
okButton.setEnabled(b);
}
/**
* Gets the selected coordinate reference system.
*
* @return selected coordinate reference system (may be {@code null}).
*/
CoordinateReferenceSystem getCoordinateReferenceSystem() {
return crs;
}
}
/**
* Simple listener used by the dialog to detect when the list model has
* changed.
*/
private static abstract class CRSListModelListener implements ListDataListener {
public abstract void process();
@Override
public void intervalAdded(ListDataEvent e) {
process();
}
@Override
public void intervalRemoved(ListDataEvent e) {
process();
}
@Override
public void contentsChanged(ListDataEvent e) {
process();
}
}
}