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