/* * 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; import org.geotools.swing.locale.LocaleUtils; import java.awt.BorderLayout; import java.awt.Point; import java.awt.Rectangle; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.lang.ref.WeakReference; import javax.swing.BorderFactory; import javax.swing.Icon; import javax.swing.JButton; import javax.swing.JComponent; import javax.swing.JOptionPane; import javax.swing.JPanel; import javax.swing.JScrollPane; import javax.swing.event.ListDataEvent; import javax.swing.event.ListDataListener; import org.geotools.map.Layer; import org.geotools.map.MapContent; import org.geotools.map.StyleLayer; import org.geotools.map.event.MapLayerListEvent; import org.geotools.map.event.MapLayerListListener; import org.geotools.styling.Style; import org.geotools.swing.control.DnDList; import org.geotools.swing.control.DnDListModel; import org.geotools.swing.event.MapPaneAdapter; import org.geotools.swing.event.MapPaneEvent; import org.geotools.swing.styling.JSimpleStyleDialog; /** * Displays a list of the map layers in an associated {@linkplain MapPane} and * provides controls to set the visibility, selection and style of each layer. * <p> * Implementation note: DefaultMapContext stores its list of MapLayer objects * in rendering order, ie. the layer at index 0 is rendererd first, followed by * index 1 etc. MapLayerTable stores its layers in the reverse order since it * is more intuitive for the user to think of a layer being 'on top' of other * layers. * * @author Michael Bedward * @since 2.6 * * @source $URL$ * @version $Id$ */ public class MapLayerTable extends JPanel { // used to get localized strings from LocaleUtils class private static final String CLASS_NAME = "MapLayerTable"; private static final String LIST_TITLE = LocaleUtils.getValue(CLASS_NAME, "ListTitle"); private static final String SHOW_HIDE_LAYER = LocaleUtils.getValue(CLASS_NAME, "ShowHideLayer"); private static final String SHOW_ALL_LAYERS = LocaleUtils.getValue(CLASS_NAME, "ShowAllLayers"); private static final String HIDE_ALL_LAYERS = LocaleUtils.getValue(CLASS_NAME, "HideAllLayers"); private static final String SELECT_LAYER = LocaleUtils.getValue(CLASS_NAME, "SelectLayer"); private static final String SELECT_ALL_LAYERS = LocaleUtils.getValue(CLASS_NAME, "SelectAllLayers"); private static final String DESELECT_ALL_LAYERS = LocaleUtils.getValue(CLASS_NAME, "DeselectAllLayers"); private static final String RENAME_LAYER = LocaleUtils.getValue(CLASS_NAME, "RenameLayer"); private static final String RENAME_LAYER_MESSAGE = LocaleUtils.getValue(CLASS_NAME, "RenameLayer_Message"); private static final String REMOVE_LAYER = LocaleUtils.getValue(CLASS_NAME, "RemoveLayer"); private static final String REMOVE_LAYER_MESSAGE = LocaleUtils.getValue(CLASS_NAME, "RemoveLayer_ConfirmMessage"); private static final String REMOVE_LAYER_TITLE = LocaleUtils.getValue(CLASS_NAME, "RemoveLayer_ConfirmTitle"); private static final String STYLE_LAYER = LocaleUtils.getValue(CLASS_NAME, "StyleLayer"); private MapPane mapPane; private DnDListModel<Layer> listModel; private DnDList<Layer> list; private JScrollPane scrollPane; /* For detecting mouse double-clicks */ private static final long DOUBLE_CLICK_TIME = 500; private long lastClickTime = 0; /* * Whether to prompt for confirmation before removing a layer. * @todo introduce a setter or property for this */ private boolean confirmRemove = true; /** * Default constructor. A subsequent call to {@linkplain #setMapPane} * will be required. */ public MapLayerTable() { this(null); } /** * Constructor. * @param mapPane the map pane this MapLayerTable will service. */ public MapLayerTable(MapPane mapPane) { listener = new Listener(this); initComponents(); doSetMapPane(mapPane); } /** * Set the map pane that this MapLayerTable will service. * * @param mapPane the map pane */ public void setMapPane(MapPane mapPane) { doSetMapPane(mapPane); } /** * Helper for {@link #setMapPane(MapPane). This is just defined so that * it can be called from the constructor without a warning from the compiler * about calling a public overridable method. * * @param mapPane the map pane */ private Listener listener; private void doSetMapPane(MapPane newMapPane) { listener.disconnectFromMapPane(); mapPane = newMapPane; listener.connectToMapPane(newMapPane); } /** * Add a new layer to those listed in the table. This method will be called * by the associated map pane automatically as part of the event sequence * when a new MapLayer is added to the pane's MapContext. * * @param layer the map layer */ public void onAddLayer(Layer layer) { listModel.insertItem(0, layer); } /** * Remove a layer from those listed in the table. This method will be called * by the associated map pane automatically as part of the event sequence * when a new MapLayer is removed from the pane's MapContext. * * @param layer the map layer */ void onRemoveLayer(Layer layer) { listModel.removeItem(layer); } /** * Repaint the list item associated with the specified MapLayer object * * @param layer the map layer */ public void repaint(Layer layer) { int index = listModel.indexOf(layer); list.repaint(list.getCellBounds(index, index)); } /** * Removes all items from the table. This is called by the * {@code MapPane} or other clients and is not intended for * general use. */ public void clear() { listModel.clear(); } /** * Called by the constructor. This method lays out the components that * make up the MapLayerTable and registers a mouse listener. */ private void initComponents() { listModel = new DnDListModel<Layer>(); list = new DnDList<Layer>(listModel) { private static final long serialVersionUID = 1289744440656016412L; /* * We override setToolTipText to provide tool tips * for the control labels displayed for each list item */ @Override public String getToolTipText(MouseEvent e) { int item = list.locationToIndex(e.getPoint()); if (item >= 0) { Rectangle r = list.getCellBounds(item, item); if (r.contains(e.getPoint())) { Point p = new Point(e.getPoint().x, e.getPoint().y - r.y); if (MapLayerTableCellRenderer.hitSelectionLabel(p)) { return SELECT_LAYER; } else if (MapLayerTableCellRenderer.hitVisibilityLabel(p)) { return SHOW_HIDE_LAYER; } else if (MapLayerTableCellRenderer.hitStyleLabel(p)) { return STYLE_LAYER; } else if (MapLayerTableCellRenderer.hitRemoveLabel(p)) { return REMOVE_LAYER; } else if (MapLayerTableCellRenderer.hitNameLabel(p)) { return RENAME_LAYER; } } } return null; } }; // Listen for drag-reordering of the list contents which // will be received via the contentsChanged method listModel.addListDataListener(new ListDataListener() { @Override public void intervalAdded(ListDataEvent e) {} @Override public void intervalRemoved(ListDataEvent e) {} @Override public void contentsChanged(ListDataEvent e) { onReorderLayers(e); } }); list.setCellRenderer(new MapLayerTableCellRenderer()); list.setFixedCellHeight(MapLayerTableCellRenderer.getCellHeight()); list.addMouseListener(new MouseAdapter() { @Override public void mouseClicked(MouseEvent e) { long clickTime = System.currentTimeMillis(); boolean doubleClick = clickTime - lastClickTime < DOUBLE_CLICK_TIME; lastClickTime = clickTime; onLayerItemClicked(e, doubleClick); } }); scrollPane = new JScrollPane(list, JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED, JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS); scrollPane.setBorder(BorderFactory.createTitledBorder(LIST_TITLE)); JPanel btnPanel = new JPanel(); Icon showIcon = MapLayerTableCellRenderer.LayerControlItem.VISIBLE.getIcon(); JButton btn = null; btn = new JButton(showIcon); btn.setToolTipText(SHOW_ALL_LAYERS); btn.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { onShowAllLayers(); } }); btnPanel.add(btn); Icon hideIcon = MapLayerTableCellRenderer.LayerControlItem.VISIBLE.getOffIcon(); btn = new JButton(hideIcon); btn.setToolTipText(HIDE_ALL_LAYERS); btn.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { onHideAllLayers(); } }); btnPanel.add(btn); Icon onIcon = MapLayerTableCellRenderer.LayerControlItem.SELECTED.getIcon(); btn = new JButton(onIcon); btn.setToolTipText(SELECT_ALL_LAYERS); btn.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { onSelectAllLayers(); } }); btnPanel.add(btn); Icon offIcon = MapLayerTableCellRenderer.LayerControlItem.SELECTED.getOffIcon(); btn = new JButton(offIcon); btn.setToolTipText(DESELECT_ALL_LAYERS); btn.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { onUnselectAllLayers(); } }); btnPanel.add(btn); setLayout(new BorderLayout()); add(scrollPane, BorderLayout.CENTER); add(btnPanel, BorderLayout.SOUTH); } /** * Handle a mouse click on a cell in the JList that displays * layer names and states. * * @param ev the mouse event * @param doubleClick true if this is the second click of a double-click; false otherwise */ private void onLayerItemClicked(MouseEvent ev, boolean doubleClick) { int item = list.locationToIndex(ev.getPoint()); if (item >= 0) { Rectangle r = list.getCellBounds(item, item); if (r.contains(ev.getPoint())) { Layer layer = listModel.getElementAt(item); Point p = new Point(ev.getPoint().x, ev.getPoint().y - r.y); if (MapLayerTableCellRenderer.hitSelectionLabel(p)) { layer.setSelected(!layer.isSelected()); } else if (MapLayerTableCellRenderer.hitVisibilityLabel(p)) { layer.setVisible(!layer.isVisible()); } else if (MapLayerTableCellRenderer.hitStyleLabel(p)) { doSetStyle(layer); } else if (MapLayerTableCellRenderer.hitRemoveLabel(p)) { doRemoveLayer(layer); } else if (MapLayerTableCellRenderer.hitNameLabel(p)) { if (doubleClick) { doSetLayerName(layer); } } } } } /** * Show a style dialog to create a new Style for the layer * * @param layer the layer to be styled */ private void doSetStyle(Layer layer) { if (layer instanceof StyleLayer) { StyleLayer styleLayer = (StyleLayer) layer; Style style = JSimpleStyleDialog.showDialog(this, styleLayer); if (style != null) { styleLayer.setStyle(style); } } } /** * Prompt for a new title for the layer * * @param layer the layer to be renamed */ private void doSetLayerName(Layer layer) { String name = JOptionPane.showInputDialog(RENAME_LAYER_MESSAGE); if (name != null && name.trim().length() > 0) { layer.setTitle(name.trim()); } } /** * Called when the user has clicked on the remove layer item. * * @param layer the layer to remove */ private void doRemoveLayer(Layer layer) { if (confirmRemove) { int confirm = JOptionPane.showConfirmDialog(null, REMOVE_LAYER_MESSAGE, REMOVE_LAYER_TITLE, JOptionPane.YES_NO_OPTION); if (confirm != JOptionPane.YES_OPTION) { return; } } mapPane.getMapContent().removeLayer(layer); } /** * Handle a ListDataEvent signallying a drag-reordering of the map layers. * The event is published by the list model after the layers have been * reordered there. * * @param ev the event */ private void onReorderLayers(ListDataEvent ev) { ((JComponent) mapPane).setIgnoreRepaint(true); for (int pos = ev.getIndex0(); pos <= ev.getIndex1(); pos++) { Layer layer = listModel.getElementAt(pos); /* * MapLayerTable stores layers in the reverse order to * the MapContent layer list */ int newContextPos = listModel.getSize() - pos - 1; int curContextPos = mapPane.getMapContent().layers().indexOf(layer); if (curContextPos != newContextPos) { mapPane.getMapContent().moveLayer(curContextPos, newContextPos); } } ((JComponent) mapPane).setIgnoreRepaint(false); ((JComponent) mapPane).repaint(); } private void onShowAllLayers() { if (mapPane != null && mapPane.getMapContent() != null) { for (Layer layer : mapPane.getMapContent().layers()) { if (!layer.isVisible()) { layer.setVisible(true); } } } } private void onHideAllLayers() { if (mapPane != null && mapPane.getMapContent() != null) { for (Layer layer : mapPane.getMapContent().layers()) { if (layer.isVisible()) { layer.setVisible(false); } } } } private void onSelectAllLayers() { if (mapPane != null && mapPane.getMapContent() != null) { for (Layer layer : mapPane.getMapContent().layers()) { if (!layer.isSelected()) { layer.setSelected(true); } } } } private void onUnselectAllLayers() { if (mapPane != null && mapPane.getMapContent() != null) { for (Layer layer : mapPane.getMapContent().layers()) { if (layer.isSelected()) { layer.setSelected(false); } } } } private static final class Listener extends MapPaneAdapter implements MapLayerListListener { private final MapLayerTable table; private WeakReference<MapPane> paneRef; private WeakReference<MapContent> contentRef; Listener(MapLayerTable table) { this.table = table; } void connectToMapPane(MapPane newMapPane) { if (newMapPane != null) { paneRef = new WeakReference<MapPane>(newMapPane); newMapPane.addMapPaneListener(this); disconnectFromMapContent(); connectToMapContent(newMapPane.getMapContent()); } } void disconnectFromMapPane() { if (paneRef != null) { MapPane prevMapPane = paneRef.get(); paneRef = null; if (prevMapPane != null) { prevMapPane.removeMapPaneListener(this); } } } void connectToMapContent(MapContent newMapContent) { if (newMapContent != null) { contentRef = new WeakReference<MapContent>(newMapContent); newMapContent.addMapLayerListListener(this); for (Layer layer : newMapContent.layers()) { table.onAddLayer(layer); } } } private void disconnectFromMapContent() { if (contentRef != null) { MapContent prevMapContent = contentRef.get(); contentRef = null; if (prevMapContent != null) { prevMapContent.removeMapLayerListListener(this); } table.clear(); } } @Override public void onNewMapContent(MapPaneEvent ev) { table.clear(); disconnectFromMapContent(); MapContent newMapContent = (MapContent) ev.getData(); connectToMapContent(newMapContent); if (newMapContent != null) { for (Layer layer : newMapContent.layers()) { table.onAddLayer(layer); } } } @Override public void layerAdded(MapLayerListEvent event) { table.onAddLayer(event.getElement()); } @Override public void layerRemoved(MapLayerListEvent event) { table.onRemoveLayer(event.getElement()); } @Override public void layerChanged(MapLayerListEvent event) { table.repaint(event.getElement()); } @Override public void layerMoved(MapLayerListEvent event) { } @Override public void layerPreDispose(MapLayerListEvent event) { } } }