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.ActionListener;
013import java.awt.event.MouseAdapter;
014import java.awt.event.MouseEvent;
015import java.awt.event.MouseListener;
016import java.io.Serializable;
017import java.util.Arrays;
018import java.util.Comparator;
019import java.util.Map;
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.event.ListSelectionEvent;
030import javax.swing.event.ListSelectionListener;
031import javax.swing.table.DefaultTableModel;
032import javax.swing.table.TableCellRenderer;
033import javax.swing.table.TableRowSorter;
034
035import org.openstreetmap.josm.Main;
036import org.openstreetmap.josm.data.SystemOfMeasurement;
037import org.openstreetmap.josm.data.gpx.GpxConstants;
038import org.openstreetmap.josm.data.gpx.GpxTrack;
039import org.openstreetmap.josm.gui.ExtendedDialog;
040import org.openstreetmap.josm.gui.layer.GpxLayer;
041import org.openstreetmap.josm.tools.GBC;
042import org.openstreetmap.josm.tools.ImageProvider;
043import org.openstreetmap.josm.tools.OpenBrowser;
044import org.openstreetmap.josm.tools.WindowGeometry;
045
046/**
047 * allows the user to choose which of the downloaded tracks should be displayed.
048 * they can be chosen from the gpx layer context menu.
049 */
050public class ChooseTrackVisibilityAction extends AbstractAction {
051    private final transient GpxLayer layer;
052
053    private DateFilterPanel dateFilter;
054    private JTable table;
055
056    /**
057     * Constructs a new {@code ChooseTrackVisibilityAction}.
058     * @param layer The associated GPX layer
059     */
060    public ChooseTrackVisibilityAction(final GpxLayer layer) {
061        super(tr("Choose visible tracks"), ImageProvider.get("dialogs/filter"));
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) (attr.containsKey(GpxConstants.GPX_NAME) ? attr.get(GpxConstants.GPX_NAME) : "");
117            String desc = (String) (attr.containsKey(GpxConstants.GPX_DESC) ? attr.get(GpxConstants.GPX_DESC) : "");
118            String time = GpxLayer.getTimespanForTrack(trk);
119            TrackLength length = new TrackLength(trk.length());
120            String url = (String) (attr.containsKey("url") ? attr.get("url") : "");
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 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.clearSelection();
198        for (int i = 0; i < layer.trackVisibility.length; i++) {
199            if (layer.trackVisibility[i]) {
200                s.addSelectionInterval(i, i);
201            }
202        }
203    }
204
205    /** listens to selection changes in the table and redraws the map */
206    private void listenToSelectionChanges() {
207        table.getSelectionModel().addListSelectionListener(new ListSelectionListener() {
208            @Override
209            public void valueChanged(ListSelectionEvent e) {
210                if (noUpdates || !(e.getSource() instanceof ListSelectionModel)) {
211                    return;
212                }
213                updateVisibilityFromTable();
214            }
215        });
216    }
217
218    private void updateVisibilityFromTable() {
219        ListSelectionModel s = table.getSelectionModel();
220        for (int i = 0; i < layer.trackVisibility.length; i++) {
221            layer.trackVisibility[table.convertRowIndexToModel(i)] = s.isSelectedIndex(i);
222        }
223        Main.map.mapView.preferenceChanged(null);
224        Main.map.repaint(100);
225    }
226
227    @Override
228    public void actionPerformed(ActionEvent arg0) {
229        final JPanel msg = new JPanel(new GridBagLayout());
230
231        dateFilter = new DateFilterPanel(layer, "gpx.traces", false);
232        dateFilter.setFilterAppliedListener(new ActionListener() {
233            @Override public void actionPerformed(ActionEvent e) {
234                noUpdates = true;
235                selectVisibleTracksInTable();
236                noUpdates = false;
237                Main.map.mapView.preferenceChanged(null);
238                Main.map.repaint(100);
239            }
240        });
241        dateFilter.loadFromPrefs();
242
243        final JToggleButton b = new JToggleButton(new AbstractAction(tr("Select by date")) {
244            @Override public void actionPerformed(ActionEvent e) {
245                if (((JToggleButton) e.getSource()).isSelected()) {
246                    dateFilter.setEnabled(true);
247                    dateFilter.applyFilter();
248                } else {
249                    dateFilter.setEnabled(false);
250                }
251            }
252        });
253        dateFilter.setEnabled(false);
254        msg.add(b, GBC.std().insets(0, 0, 5, 0));
255        msg.add(dateFilter, GBC.eol().insets(0, 0, 10, 0).fill(GBC.HORIZONTAL));
256
257        msg.add(new JLabel(tr("<html>Select all tracks that you want to be displayed. " +
258                "You can drag select a range of tracks or use CTRL+Click to select specific ones. " +
259                "The map is updated live in the background. Open the URLs by double clicking them.</html>")),
260                GBC.eop().fill(GBC.HORIZONTAL));
261        // build table
262        final boolean[] trackVisibilityBackup = layer.trackVisibility.clone();
263        table = buildTable(buildTableContents());
264        selectVisibleTracksInTable();
265        listenToSelectionChanges();
266        // make the table scrollable
267        JScrollPane scrollPane = new JScrollPane(table);
268        msg.add(scrollPane, GBC.eol().fill(GBC.BOTH));
269
270        int v = 1;
271        if (!GraphicsEnvironment.isHeadless()) {
272            // build dialog
273            ExtendedDialog ed = new ExtendedDialog(Main.parent, tr("Set track visibility for {0}", layer.getName()),
274                    new String[]{tr("Show all"), tr("Show selected only"), tr("Cancel")});
275            ed.setButtonIcons(new String[]{"eye", "dialogs/filter", "cancel"});
276            ed.setContent(msg, false);
277            ed.setDefaultButton(2);
278            ed.setCancelButton(3);
279            ed.configureContextsensitiveHelp("/Action/ChooseTrackVisibility", true);
280            ed.setRememberWindowGeometry(getClass().getName() + ".geometry",
281                    WindowGeometry.centerInWindow(Main.parent, new Dimension(1000, 500)));
282            ed.showDialog();
283            dateFilter.saveInPrefs();
284            v = ed.getValue();
285            // cancel for unknown buttons and copy back original settings
286            if (v != 1 && v != 2) {
287                layer.trackVisibility = Arrays.copyOf(trackVisibilityBackup, layer.trackVisibility.length);
288                Main.map.repaint();
289                return;
290            }
291        }
292        // set visibility (1 = show all, 2 = filter). If no tracks are selected
293        // set all of them visible and...
294        ListSelectionModel s = table.getSelectionModel();
295        final boolean all = v == 1 || s.isSelectionEmpty();
296        for (int i = 0; i < layer.trackVisibility.length; i++) {
297            layer.trackVisibility[table.convertRowIndexToModel(i)] = all || s.isSelectedIndex(i);
298        }
299        // ...sync with layer visibility instead to avoid having two ways to hide everything
300        layer.setVisible(v == 1 || !s.isSelectionEmpty());
301
302        if (Main.isDisplayingMapView()) {
303            Main.map.mapView.preferenceChanged(null);
304        }
305        if (Main.map != null) {
306            Main.map.repaint();
307        }
308    }
309}