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.Arrays;
016import java.util.Comparator;
017import java.util.Map;
018
019import javax.swing.AbstractAction;
020import javax.swing.JComponent;
021import javax.swing.JLabel;
022import javax.swing.JPanel;
023import javax.swing.JScrollPane;
024import javax.swing.JTable;
025import javax.swing.JToggleButton;
026import javax.swing.ListSelectionModel;
027import javax.swing.event.ListSelectionEvent;
028import javax.swing.event.ListSelectionListener;
029import javax.swing.table.DefaultTableModel;
030import javax.swing.table.TableCellRenderer;
031import javax.swing.table.TableRowSorter;
032
033import org.openstreetmap.josm.Main;
034import org.openstreetmap.josm.data.gpx.GpxConstants;
035import org.openstreetmap.josm.data.gpx.GpxTrack;
036import org.openstreetmap.josm.gui.ExtendedDialog;
037import org.openstreetmap.josm.gui.NavigatableComponent;
038import org.openstreetmap.josm.gui.layer.GpxLayer;
039import org.openstreetmap.josm.tools.GBC;
040import org.openstreetmap.josm.tools.ImageProvider;
041import org.openstreetmap.josm.tools.OpenBrowser;
042import org.openstreetmap.josm.tools.WindowGeometry;
043
044/**
045 * allows the user to choose which of the downloaded tracks should be displayed.
046 * they can be chosen from the gpx layer context menu.
047 */
048public class ChooseTrackVisibilityAction extends AbstractAction {
049    private final GpxLayer layer;
050
051    DateFilterPanel dateFilter;
052    JTable table;
053
054    /**
055     * Constructs a new {@code ChooseTrackVisibilityAction}.
056     * @param layer The associated GPX layer
057     */
058    public ChooseTrackVisibilityAction(final GpxLayer layer) {
059        super(tr("Choose visible tracks"), ImageProvider.get("dialogs/filter"));
060        this.layer = layer;
061        putValue("help", ht("/Action/ChooseTrackVisibility"));
062    }
063
064    /**
065     * Class to format a length according to SystemOfMesurement.
066     */
067    private static final class TrackLength {
068        private double value;
069
070        /**
071         * Constructs a new {@code TrackLength} object with a given length.
072         * @param value length of the track
073         */
074        TrackLength(double value) {
075            this.value = value;
076        }
077
078        /**
079         * Provides string representation.
080         * @return String representation depending of SystemOfMeasurement
081         */
082        @Override
083        public String toString() {
084            return NavigatableComponent.getSystemOfMeasurement().getDistText(value);
085        }
086    }
087
088    /**
089     * Comparator for TrackLength objects
090     */
091    private static final class LengthContentComparator implements Comparator<TrackLength> {
092
093        /**
094         * Compare 2 TrackLength objects relative to the real length
095         */
096        @Override
097        public int compare(TrackLength l0, TrackLength l1) {
098            if(l0.value < l1.value)
099                return -1;
100            else if(l0.value > l1.value)
101                return 1;
102            return 0;
103        }
104    }
105
106    /**
107     * gathers all available data for the tracks and returns them as array of arrays
108     * in the expected column order  */
109    private Object[][] buildTableContents() {
110        Object[][] tracks = new Object[layer.data.tracks.size()][5];
111        int i = 0;
112        for (GpxTrack trk : layer.data.tracks) {
113            Map<String, Object> attr = trk.getAttributes();
114            String name = (String) (attr.containsKey(GpxConstants.GPX_NAME) ? attr.get(GpxConstants.GPX_NAME) : "");
115            String desc = (String) (attr.containsKey(GpxConstants.GPX_DESC) ? attr.get(GpxConstants.GPX_DESC) : "");
116            String time = GpxLayer.getTimespanForTrack(trk);
117            TrackLength length = new TrackLength(trk.length());
118            String url = (String) (attr.containsKey("url") ? attr.get("url") : "");
119            tracks[i] = new Object[]{name, desc, time, length, url};
120            i++;
121        }
122        return tracks;
123    }
124
125    /**
126     * Builds an non-editable table whose 5th column will open a browser when double clicked.
127     * The table will fill its parent. */
128    private JTable buildTable(Object[][] content) {
129        final String[] headers = {tr("Name"), tr("Description"), tr("Timespan"), tr("Length"), tr("URL")};
130        DefaultTableModel model = new DefaultTableModel(content, headers);
131        final JTable t = new JTable(model) {
132            @Override
133            public Component prepareRenderer(TableCellRenderer renderer, int row, int col) {
134                Component c = super.prepareRenderer(renderer, row, col);
135                if (c instanceof JComponent) {
136                    JComponent jc = (JComponent) c;
137                    jc.setToolTipText(getValueAt(row, col).toString());
138                }
139                return c;
140            }
141
142            @Override
143            public boolean isCellEditable(int rowIndex, int colIndex) {
144                return false;
145            }
146        };
147        // define how to sort row
148        TableRowSorter<DefaultTableModel> rowSorter = new TableRowSorter<>();
149        t.setRowSorter(rowSorter);
150        rowSorter.setModel(model);
151        rowSorter.setComparator(3, new LengthContentComparator());
152        // default column widths
153        t.getColumnModel().getColumn(0).setPreferredWidth(220);
154        t.getColumnModel().getColumn(1).setPreferredWidth(300);
155        t.getColumnModel().getColumn(2).setPreferredWidth(200);
156        t.getColumnModel().getColumn(3).setPreferredWidth(50);
157        t.getColumnModel().getColumn(4).setPreferredWidth(100);
158        // make the link clickable
159        final MouseListener urlOpener = new MouseAdapter() {
160            @Override
161            public void mouseClicked(MouseEvent e) {
162                if (e.getClickCount() != 2) {
163                    return;
164                }
165                JTable t = (JTable) e.getSource();
166                int col = t.convertColumnIndexToModel(t.columnAtPoint(e.getPoint()));
167                if (col != 4) {
168                    return;
169                }
170                int row = t.rowAtPoint(e.getPoint());
171                String url = (String) t.getValueAt(row, col);
172                if (url == null || url.isEmpty()) {
173                    return;
174                }
175                OpenBrowser.displayUrl(url);
176            }
177        };
178        t.addMouseListener(urlOpener);
179        t.setFillsViewportHeight(true);
180        return t;
181    }
182
183    boolean noUpdates=false;
184
185    /** selects all rows (=tracks) in the table that are currently visible on the layer*/
186    private void selectVisibleTracksInTable() {
187        // don't select any tracks if the layer is not visible
188        if (!layer.isVisible()) {
189            return;
190        }
191        ListSelectionModel s = table.getSelectionModel();
192        s.clearSelection();
193        for (int i = 0; i < layer.trackVisibility.length; i++) {
194            if (layer.trackVisibility[i]) {
195                s.addSelectionInterval(i, i);
196            }
197        }
198    }
199
200    /** listens to selection changes in the table and redraws the map */
201    private void listenToSelectionChanges() {
202        table.getSelectionModel().addListSelectionListener(new ListSelectionListener() {
203            @Override
204            public void valueChanged(ListSelectionEvent e) {
205                if (noUpdates || !(e.getSource() instanceof ListSelectionModel)) {
206                    return;
207                }
208                updateVisibilityFromTable();
209            }
210        });
211    }
212
213    private void updateVisibilityFromTable() {
214        ListSelectionModel s = table.getSelectionModel();
215        for (int i = 0; i < layer.trackVisibility.length; i++) {
216            layer.trackVisibility[table.convertRowIndexToModel(i)] = s.isSelectedIndex(i);
217        }
218        Main.map.mapView.preferenceChanged(null);
219        Main.map.repaint(100);
220    }
221
222    @Override
223    public void actionPerformed(ActionEvent arg0) {
224        final JPanel msg = new JPanel(new GridBagLayout());
225
226        dateFilter = new DateFilterPanel(layer, "gpx.traces", false);
227        dateFilter.setFilterAppliedListener(new ActionListener(){
228            @Override public void actionPerformed(ActionEvent e) {
229                noUpdates = true;
230                selectVisibleTracksInTable();
231                noUpdates = false;
232                Main.map.mapView.preferenceChanged(null);
233                Main.map.repaint(100);
234            }
235        });
236        dateFilter.loadFromPrefs();
237
238        final JToggleButton b = new JToggleButton(new AbstractAction(tr("Select by date")) {
239            @Override public void actionPerformed(ActionEvent e) {
240                if (((JToggleButton) e.getSource()).isSelected()) {
241                    dateFilter.setEnabled(true);
242                    dateFilter.applyFilter();
243                } else {
244                    dateFilter.setEnabled(false);
245                }
246            }
247        });
248        dateFilter.setEnabled(false);
249        msg.add(b, GBC.std().insets(0,0,5,0));
250        msg.add(dateFilter, GBC.eol().insets(0,0,10,0).fill(GBC.HORIZONTAL));
251
252        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));
253        // build table
254        final boolean[] trackVisibilityBackup = layer.trackVisibility.clone();
255        table = buildTable(buildTableContents());
256        selectVisibleTracksInTable();
257        listenToSelectionChanges();
258        // make the table scrollable
259        JScrollPane scrollPane = new JScrollPane(table);
260        msg.add(scrollPane, GBC.eol().fill(GBC.BOTH));
261
262        // build dialog
263        ExtendedDialog ed = new ExtendedDialog(Main.parent, tr("Set track visibility for {0}", layer.getName()),
264                new String[]{tr("Show all"), tr("Show selected only"), tr("Cancel")});
265        ed.setButtonIcons(new String[]{"eye", "dialogs/filter", "cancel"});
266        ed.setContent(msg, false);
267        ed.setDefaultButton(2);
268        ed.setCancelButton(3);
269        ed.configureContextsensitiveHelp("/Action/ChooseTrackVisibility", true);
270        ed.setRememberWindowGeometry(getClass().getName() + ".geometry", WindowGeometry.centerInWindow(Main.parent, new Dimension(1000, 500)));
271        ed.showDialog();
272        dateFilter.saveInPrefs();
273        int v = ed.getValue();
274        // cancel for unknown buttons and copy back original settings
275        if (v != 1 && v != 2) {
276            layer.trackVisibility = Arrays.copyOf(trackVisibilityBackup, layer.trackVisibility.length);
277            Main.map.repaint();
278            return;
279        }
280        // set visibility (1 = show all, 2 = filter). If no tracks are selected
281        // set all of them visible and...
282        ListSelectionModel s = table.getSelectionModel();
283        final boolean all = v == 1 || s.isSelectionEmpty();
284        for (int i = 0; i < layer.trackVisibility.length; i++) {
285            layer.trackVisibility[table.convertRowIndexToModel(i)] = all || s.isSelectedIndex(i);
286        }
287        // ...sync with layer visibility instead to avoid having two ways to hide everything
288        layer.setVisible(v == 1 || !s.isSelectionEmpty());
289
290        Main.map.mapView.preferenceChanged(null);
291        Main.map.repaint();
292    }
293
294}