001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.gui.layer.gpx;
003
004import static org.openstreetmap.josm.gui.help.HelpUtil.ht;
005import static org.openstreetmap.josm.tools.I18n.tr;
006
007import java.awt.Component;
008import java.awt.Dimension;
009import java.awt.GridBagLayout;
010import java.awt.event.ActionEvent;
011import java.awt.event.ActionListener;
012import java.awt.event.MouseAdapter;
013import java.awt.event.MouseEvent;
014import java.awt.event.MouseListener;
015import java.util.Map;
016
017import javax.swing.AbstractAction;
018import javax.swing.JComponent;
019import javax.swing.JLabel;
020import javax.swing.JPanel;
021import javax.swing.JScrollPane;
022import javax.swing.JTable;
023import javax.swing.JToggleButton;
024import javax.swing.ListSelectionModel;
025import javax.swing.event.ListSelectionEvent;
026import javax.swing.event.ListSelectionListener;
027import javax.swing.table.TableCellRenderer;
028
029import org.openstreetmap.josm.Main;
030import org.openstreetmap.josm.data.gpx.GpxTrack;
031import org.openstreetmap.josm.gui.ExtendedDialog;
032import org.openstreetmap.josm.gui.NavigatableComponent;
033import org.openstreetmap.josm.gui.layer.GpxLayer;
034import org.openstreetmap.josm.tools.GBC;
035import org.openstreetmap.josm.tools.ImageProvider;
036import org.openstreetmap.josm.tools.OpenBrowser;
037import org.openstreetmap.josm.tools.WindowGeometry;
038
039/**
040 * allows the user to choose which of the downloaded tracks should be displayed.
041 * they can be chosen from the gpx layer context menu.
042 */
043public class ChooseTrackVisibilityAction extends AbstractAction {
044    private final GpxLayer layer;
045
046    DateFilterPanel dateFilter;
047    JTable table;
048
049    /**
050     * Constructs a new {@code ChooseTrackVisibilityAction}.
051     * @param layer The associated GPX layer
052     */
053    public ChooseTrackVisibilityAction(final GpxLayer layer) {
054        super(tr("Choose visible tracks"), ImageProvider.get("dialogs/filter"));
055        this.layer = layer;
056        putValue("help", ht("/Action/ChooseTrackVisibility"));
057    }
058
059    /**
060     * gathers all available data for the tracks and returns them as array of arrays
061     * in the expected column order  */
062    private Object[][] buildTableContents() {
063        Object[][] tracks = new Object[layer.data.tracks.size()][5];
064        int i = 0;
065        for (GpxTrack trk : layer.data.tracks) {
066            Map<String, Object> attr = trk.getAttributes();
067            String name = (String) (attr.containsKey("name") ? attr.get("name") : "");
068            String desc = (String) (attr.containsKey("desc") ? attr.get("desc") : "");
069            String time = GpxLayer.getTimespanForTrack(trk);
070            String length = NavigatableComponent.getSystemOfMeasurement().getDistText(trk.length());
071            String url = (String) (attr.containsKey("url") ? attr.get("url") : "");
072            tracks[i] = new String[]{name, desc, time, length, url};
073            i++;
074        }
075        return tracks;
076    }
077
078    /**
079     * Builds an non-editable table whose 5th column will open a browser when double clicked.
080     * The table will fill its parent. */
081    private JTable buildTable(Object[][] content) {
082        final String[] headers = {tr("Name"), tr("Description"), tr("Timespan"), tr("Length"), tr("URL")};
083        final JTable t = new JTable(content, headers) {
084            @Override
085            public Component prepareRenderer(TableCellRenderer renderer, int row, int col) {
086                Component c = super.prepareRenderer(renderer, row, col);
087                if (c instanceof JComponent) {
088                    JComponent jc = (JComponent) c;
089                    jc.setToolTipText((String) getValueAt(row, col));
090                }
091                return c;
092            }
093
094            @Override
095            public boolean isCellEditable(int rowIndex, int colIndex) {
096                return false;
097            }
098        };
099        // default column widths
100        t.getColumnModel().getColumn(0).setPreferredWidth(220);
101        t.getColumnModel().getColumn(1).setPreferredWidth(300);
102        t.getColumnModel().getColumn(2).setPreferredWidth(200);
103        t.getColumnModel().getColumn(3).setPreferredWidth(50);
104        t.getColumnModel().getColumn(4).setPreferredWidth(100);
105        // make the link clickable
106        final MouseListener urlOpener = new MouseAdapter() {
107            @Override
108            public void mouseClicked(MouseEvent e) {
109                if (e.getClickCount() != 2) {
110                    return;
111                }
112                JTable t = (JTable) e.getSource();
113                int col = t.convertColumnIndexToModel(t.columnAtPoint(e.getPoint()));
114                if (col != 4) {
115                    return;
116                }
117                int row = t.rowAtPoint(e.getPoint());
118                String url = (String) t.getValueAt(row, col);
119                if (url == null || url.isEmpty()) {
120                    return;
121                }
122                OpenBrowser.displayUrl(url);
123            }
124        };
125        t.setAutoCreateRowSorter(true);
126        t.addMouseListener(urlOpener);
127        t.setFillsViewportHeight(true);
128        return t;
129    }
130
131    boolean noUpdates=false;
132
133    /** selects all rows (=tracks) in the table that are currently visible on the layer*/
134    private void selectVisibleTracksInTable() {
135        // don't select any tracks if the layer is not visible
136        if (!layer.isVisible()) {
137            return;
138        }
139        ListSelectionModel s = table.getSelectionModel();
140        s.clearSelection();
141        for (int i = 0; i < layer.trackVisibility.length; i++) {
142            if (layer.trackVisibility[i]) {
143                s.addSelectionInterval(i, i);
144            }
145        }
146    }
147
148    /** listens to selection changes in the table and redraws the map */
149    private void listenToSelectionChanges() {
150        table.getSelectionModel().addListSelectionListener(new ListSelectionListener() {
151            @Override
152            public void valueChanged(ListSelectionEvent e) {
153                if (noUpdates || !(e.getSource() instanceof ListSelectionModel)) {
154                    return;
155                }
156                updateVisibilityFromTable();
157            }
158        });
159    }
160
161    private void updateVisibilityFromTable() {
162        ListSelectionModel s = table.getSelectionModel();
163        for (int i = 0; i < layer.trackVisibility.length; i++) {
164            layer.trackVisibility[table.convertRowIndexToModel(i)] = s.isSelectedIndex(i);
165        }
166        Main.map.mapView.preferenceChanged(null);
167        Main.map.repaint(100);
168    }
169
170    @Override
171    public void actionPerformed(ActionEvent arg0) {
172        final JPanel msg = new JPanel(new GridBagLayout());
173
174        dateFilter = new DateFilterPanel(layer, "gpx.traces", false);
175        dateFilter.setFilterAppliedListener(new ActionListener(){
176            @Override public void actionPerformed(ActionEvent e) {
177                noUpdates = true;
178                selectVisibleTracksInTable();
179                noUpdates = false;
180                Main.map.mapView.preferenceChanged(null);
181                Main.map.repaint(100);
182            }
183        });
184        dateFilter.loadFromPrefs();
185
186        final JToggleButton b = new JToggleButton(new AbstractAction(tr("Select by date")) {
187            @Override public void actionPerformed(ActionEvent e) {
188                if (((JToggleButton) e.getSource()).isSelected()) {
189                    dateFilter.setEnabled(true);
190                    dateFilter.applyFilter();
191                } else {
192                    dateFilter.setEnabled(false);
193                }
194            }
195        });
196        dateFilter.setEnabled(false);
197        msg.add(b, GBC.std().insets(0,0,5,0));
198        msg.add(dateFilter, GBC.eol().insets(0,0,10,0).fill(GBC.HORIZONTAL));
199
200        msg.add(new JLabel(tr("<html>Select all tracks that you want to be displayed. You can drag select a " + "range of tracks or use CTRL+Click to select specific ones. The map is updated live in the " + "background. Open the URLs by double clicking them.</html>")), GBC.eop().fill(GBC.HORIZONTAL));
201        // build table
202        final boolean[] trackVisibilityBackup = layer.trackVisibility.clone();
203        table = buildTable(buildTableContents());
204        selectVisibleTracksInTable();
205        listenToSelectionChanges();
206        // make the table scrollable
207        JScrollPane scrollPane = new JScrollPane(table);
208        msg.add(scrollPane, GBC.eol().fill(GBC.BOTH));
209
210        // build dialog
211        ExtendedDialog ed = new ExtendedDialog(Main.parent, tr("Set track visibility for {0}", layer.getName()), new String[]{tr("Show all"), tr("Show selected only"), tr("Cancel")});
212        ed.setButtonIcons(new String[]{"dialogs/layerlist/eye", "dialogs/filter", "cancel"});
213        ed.setContent(msg, false);
214        ed.setDefaultButton(2);
215        ed.setCancelButton(3);
216        ed.configureContextsensitiveHelp("/Action/ChooseTrackVisibility", true);
217        ed.setRememberWindowGeometry(getClass().getName() + ".geometry", WindowGeometry.centerInWindow(Main.parent, new Dimension(1000, 500)));
218        ed.showDialog();
219        dateFilter.saveInPrefs();
220        int v = ed.getValue();
221        // cancel for unknown buttons and copy back original settings
222        if (v != 1 && v != 2) {
223            System.arraycopy(trackVisibilityBackup, 0, layer.trackVisibility, 0, layer.trackVisibility.length);
224            Main.map.repaint();
225            return;
226        }
227        // set visibility (1 = show all, 2 = filter). If no tracks are selected
228        // set all of them visible and...
229        ListSelectionModel s = table.getSelectionModel();
230        final boolean all = v == 1 || s.isSelectionEmpty();
231        for (int i = 0; i < layer.trackVisibility.length; i++) {
232            layer.trackVisibility[table.convertRowIndexToModel(i)] = all || s.isSelectedIndex(i);
233        }
234        // ...sync with layer visibility instead to avoid having two ways to hide everything
235        layer.setVisible(v == 1 || !s.isSelectionEmpty());
236
237        Main.map.mapView.preferenceChanged(null);
238        Main.map.repaint();
239    }
240
241}