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