source: josm/trunk/src/org/openstreetmap/josm/gui/layer/gpx/ChooseTrackVisibilityAction.java@ 16601

Last change on this file since 16601 was 16601, checked in by simon04, 4 years ago

Add TableHelper.setSelectedIndices

  • Property svn:eol-style set to native
File size: 16.2 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.gui.layer.gpx;
3
4import static org.openstreetmap.josm.gui.help.HelpUtil.ht;
5import static org.openstreetmap.josm.tools.I18n.tr;
6
7import java.awt.Color;
8import java.awt.Component;
9import java.awt.Dimension;
10import java.awt.GridBagLayout;
11import java.awt.event.ActionEvent;
12import java.awt.event.MouseAdapter;
13import java.awt.event.MouseEvent;
14import java.awt.event.MouseListener;
15import java.io.Serializable;
16import java.util.Arrays;
17import java.util.Comparator;
18import java.util.List;
19import java.util.Map;
20import java.util.Objects;
21import java.util.Optional;
22import java.util.stream.Collectors;
23import java.util.stream.IntStream;
24
25import javax.swing.AbstractAction;
26import javax.swing.JColorChooser;
27import javax.swing.JComponent;
28import javax.swing.JLabel;
29import javax.swing.JOptionPane;
30import javax.swing.JPanel;
31import javax.swing.JScrollPane;
32import javax.swing.JTable;
33import javax.swing.JToggleButton;
34import javax.swing.ListSelectionModel;
35import javax.swing.event.TableModelEvent;
36import javax.swing.table.DefaultTableModel;
37import javax.swing.table.TableCellRenderer;
38import javax.swing.table.TableModel;
39import javax.swing.table.TableRowSorter;
40
41import org.apache.commons.jcs3.access.exception.InvalidArgumentException;
42import org.openstreetmap.josm.data.SystemOfMeasurement;
43import org.openstreetmap.josm.data.gpx.GpxConstants;
44import org.openstreetmap.josm.data.gpx.IGpxTrack;
45import org.openstreetmap.josm.gui.ExtendedDialog;
46import org.openstreetmap.josm.gui.MainApplication;
47import org.openstreetmap.josm.gui.layer.GpxLayer;
48import org.openstreetmap.josm.gui.preferences.display.GPXSettingsPanel;
49import org.openstreetmap.josm.gui.util.TableHelper;
50import org.openstreetmap.josm.gui.util.WindowGeometry;
51import org.openstreetmap.josm.tools.GBC;
52import org.openstreetmap.josm.tools.ImageProvider;
53import org.openstreetmap.josm.tools.OpenBrowser;
54
55/**
56 * allows the user to choose which of the downloaded tracks should be displayed.
57 * they can be chosen from the gpx layer context menu.
58 */
59public class ChooseTrackVisibilityAction extends AbstractAction {
60 private final transient GpxLayer layer;
61
62 private DateFilterPanel dateFilter;
63 private JTable table;
64
65 /**
66 * Constructs a new {@code ChooseTrackVisibilityAction}.
67 * @param layer The associated GPX layer
68 */
69 public ChooseTrackVisibilityAction(final GpxLayer layer) {
70 super(tr("Choose track visibility and colors"));
71 new ImageProvider("dialogs/filter").getResource().attachImageIcon(this, true);
72 this.layer = layer;
73 putValue("help", ht("/Action/ChooseTrackVisibility"));
74 }
75
76 /**
77 * Class to format a length according to SystemOfMesurement.
78 */
79 private static final class TrackLength {
80 private final double value;
81
82 /**
83 * Constructs a new {@code TrackLength} object with a given length.
84 * @param value length of the track
85 */
86 TrackLength(double value) {
87 this.value = value;
88 }
89
90 /**
91 * Provides string representation.
92 * @return String representation depending of SystemOfMeasurement
93 */
94 @Override
95 public String toString() {
96 return SystemOfMeasurement.getSystemOfMeasurement().getDistText(value);
97 }
98 }
99
100 /**
101 * Comparator for TrackLength objects
102 */
103 private static final class LengthContentComparator implements Comparator<TrackLength>, Serializable {
104
105 private static final long serialVersionUID = 1L;
106
107 /**
108 * Compare 2 TrackLength objects relative to the real length
109 */
110 @Override
111 public int compare(TrackLength l0, TrackLength l1) {
112 return Double.compare(l0.value, l1.value);
113 }
114 }
115
116 /**
117 * Gathers all available data for the tracks and returns them as array of arrays
118 * in the expected column order.
119 * @return table data
120 */
121 private Object[][] buildTableContents() {
122 Object[][] tracks = new Object[layer.data.tracks.size()][5];
123 int i = 0;
124 for (IGpxTrack trk : layer.data.tracks) {
125 Map<String, Object> attr = trk.getAttributes();
126 String name = (String) Optional.ofNullable(attr.get(GpxConstants.GPX_NAME)).orElse("");
127 String desc = (String) Optional.ofNullable(attr.get(GpxConstants.GPX_DESC)).orElse("");
128 String time = GpxLayer.getTimespanForTrack(trk);
129 TrackLength length = new TrackLength(trk.length());
130 String url = (String) Optional.ofNullable(attr.get("url")).orElse("");
131 tracks[i] = new Object[]{name, desc, time, length, url, trk};
132 i++;
133 }
134 return tracks;
135 }
136
137 private void showColorDialog(List<IGpxTrack> tracks) {
138 Color cl = tracks.stream().filter(Objects::nonNull)
139 .map(IGpxTrack::getColor).filter(Objects::nonNull)
140 .findAny().orElse(GpxDrawHelper.DEFAULT_COLOR_PROPERTY.get());
141 JColorChooser c = new JColorChooser(cl);
142 Object[] options = {tr("OK"), tr("Cancel"), tr("Default")};
143 int answer = JOptionPane.showOptionDialog(
144 MainApplication.getMainFrame(),
145 c,
146 tr("Choose a color"),
147 JOptionPane.OK_CANCEL_OPTION,
148 JOptionPane.PLAIN_MESSAGE,
149 null,
150 options,
151 options[0]
152 );
153 switch (answer) {
154 case 0:
155 tracks.forEach(t -> t.setColor(c.getColor()));
156 GPXSettingsPanel.putLayerPrefLocal(layer, "colormode", "0"); //set Colormode to none
157 break;
158 case 1:
159 return;
160 case 2:
161 tracks.forEach(t -> t.setColor(null));
162 break;
163 }
164 table.repaint();
165 }
166
167 /**
168 * Builds an editable table whose 5th column will open a browser when double clicked.
169 * The table will fill its parent.
170 * @param content table data
171 * @return non-editable table
172 */
173 private static JTable buildTable(Object[]... content) {
174 final String[] headers = {tr("Name"), tr("Description"), tr("Timespan"), tr("Length"), tr("URL")};
175 DefaultTableModel model = new DefaultTableModel(content, headers);
176 final GpxTrackTable t = new GpxTrackTable(content, model);
177 // define how to sort row
178 TableRowSorter<DefaultTableModel> rowSorter = new TableRowSorter<>();
179 t.setRowSorter(rowSorter);
180 rowSorter.setModel(model);
181 rowSorter.setComparator(3, new LengthContentComparator());
182 // default column widths
183 t.getColumnModel().getColumn(0).setPreferredWidth(220);
184 t.getColumnModel().getColumn(1).setPreferredWidth(300);
185 t.getColumnModel().getColumn(2).setPreferredWidth(200);
186 t.getColumnModel().getColumn(3).setPreferredWidth(50);
187 t.getColumnModel().getColumn(4).setPreferredWidth(100);
188 // make the link clickable
189 final MouseListener urlOpener = new MouseAdapter() {
190 @Override
191 public void mouseClicked(MouseEvent e) {
192 if (e.getClickCount() != 2) {
193 return;
194 }
195 JTable t = (JTable) e.getSource();
196 int col = t.convertColumnIndexToModel(t.columnAtPoint(e.getPoint()));
197 if (col != 4) {
198 return;
199 }
200 int row = t.rowAtPoint(e.getPoint());
201 String url = (String) t.getValueAt(row, col);
202 if (url == null || url.isEmpty()) {
203 return;
204 }
205 OpenBrowser.displayUrl(url);
206 }
207 };
208 t.addMouseListener(urlOpener);
209 t.setFillsViewportHeight(true);
210 t.putClientProperty("terminateEditOnFocusLost", true);
211 return t;
212 }
213
214 private boolean noUpdates;
215
216 /** selects all rows (=tracks) in the table that are currently visible on the layer*/
217 private void selectVisibleTracksInTable() {
218 // don't select any tracks if the layer is not visible
219 if (!layer.isVisible()) {
220 return;
221 }
222 ListSelectionModel s = table.getSelectionModel();
223 TableHelper.setSelectedIndices(s,
224 IntStream.range(0, layer.trackVisibility.length).filter(i -> layer.trackVisibility[i]));
225 }
226
227 /** listens to selection changes in the table and redraws the map */
228 private void listenToSelectionChanges() {
229 table.getSelectionModel().addListSelectionListener(e -> {
230 if (noUpdates || !(e.getSource() instanceof ListSelectionModel)) {
231 return;
232 }
233 updateVisibilityFromTable();
234 });
235 }
236
237 private void updateVisibilityFromTable() {
238 ListSelectionModel s = table.getSelectionModel();
239 for (int i = 0; i < layer.trackVisibility.length; i++) {
240 layer.trackVisibility[table.convertRowIndexToModel(i)] = s.isSelectedIndex(i);
241 }
242 layer.invalidate();
243 }
244
245 @Override
246 public void actionPerformed(ActionEvent ae) {
247 final JPanel msg = new JPanel(new GridBagLayout());
248
249 dateFilter = new DateFilterPanel(layer, "gpx.traces", false);
250 dateFilter.setFilterAppliedListener(e -> {
251 noUpdates = true;
252 selectVisibleTracksInTable();
253 noUpdates = false;
254 layer.invalidate();
255 });
256 dateFilter.loadFromPrefs();
257
258 final JToggleButton b = new JToggleButton(new AbstractAction(tr("Select by date")) {
259 @Override public void actionPerformed(ActionEvent e) {
260 if (((JToggleButton) e.getSource()).isSelected()) {
261 dateFilter.setEnabled(true);
262 dateFilter.applyFilter();
263 } else {
264 dateFilter.setEnabled(false);
265 }
266 }
267 });
268 dateFilter.setEnabled(false);
269 msg.add(b, GBC.std().insets(0, 0, 5, 0));
270 msg.add(dateFilter, GBC.eol().insets(0, 0, 10, 0).fill(GBC.HORIZONTAL));
271
272 msg.add(new JLabel(tr("<html>Select all tracks that you want to be displayed. " +
273 "You can drag select a range of tracks or use CTRL+Click to select specific ones. " +
274 "The map is updated live in the background. Open the URLs by double clicking them, " +
275 "edit name and description by double clicking the cell.</html>")),
276 GBC.eop().fill(GBC.HORIZONTAL));
277 // build table
278 final boolean[] trackVisibilityBackup = layer.trackVisibility.clone();
279 Object[][] content = buildTableContents();
280 table = buildTable(content);
281 selectVisibleTracksInTable();
282 listenToSelectionChanges();
283 // make the table scrollable
284 JScrollPane scrollPane = new JScrollPane(table);
285 msg.add(scrollPane, GBC.eol().fill(GBC.BOTH));
286
287 int v = 1;
288 // build dialog
289 ExtendedDialog ed = new ExtendedDialog(MainApplication.getMainFrame(),
290 tr("Set track visibility for {0}", layer.getName()),
291 tr("Set color for selected tracks..."), tr("Show all"), tr("Show selected only"), tr("Close")) {
292 @Override
293 protected void buttonAction(int buttonIndex, ActionEvent evt) {
294 if (buttonIndex == 0) {
295 List<IGpxTrack> trks = Arrays.stream(table.getSelectedRows())
296 .mapToObj(i -> content[i][5])
297 .filter(trk -> trk instanceof IGpxTrack)
298 .map(IGpxTrack.class::cast)
299 .collect(Collectors.toList());
300 showColorDialog(trks);
301 } else {
302 super.buttonAction(buttonIndex, evt);
303 }
304 }
305 };
306 ed.setButtonIcons("colorchooser", "eye", "dialogs/filter", "cancel");
307 ed.setContent(msg, false);
308 ed.setDefaultButton(2);
309 ed.setCancelButton(3);
310 ed.configureContextsensitiveHelp("/Action/ChooseTrackVisibility", true);
311 ed.setRememberWindowGeometry(getClass().getName() + ".geometry",
312 WindowGeometry.centerInWindow(MainApplication.getMainFrame(), new Dimension(1000, 500)));
313 ed.showDialog();
314 dateFilter.saveInPrefs();
315 v = ed.getValue();
316 // cancel for unknown buttons and copy back original settings
317 if (v != 2 && v != 3) {
318 layer.trackVisibility = Arrays.copyOf(trackVisibilityBackup, layer.trackVisibility.length);
319 MainApplication.getMap().repaint();
320 return;
321 }
322 // set visibility (2 = show all, 3 = filter). If no tracks are selected
323 // set all of them visible and...
324 ListSelectionModel s = table.getSelectionModel();
325 final boolean all = v == 2 || s.isSelectionEmpty();
326 for (int i = 0; i < layer.trackVisibility.length; i++) {
327 layer.trackVisibility[table.convertRowIndexToModel(i)] = all || s.isSelectedIndex(i);
328 }
329 // layer has been changed
330 layer.invalidate();
331 // ...sync with layer visibility instead to avoid having two ways to hide everything
332 layer.setVisible(v == 2 || !s.isSelectionEmpty());
333 }
334
335 private static class GpxTrackTable extends JTable {
336 final Object[][] content;
337
338 GpxTrackTable(Object[][] content, TableModel model) {
339 super(model);
340 this.content = content;
341 }
342
343 @Override
344 public Component prepareRenderer(TableCellRenderer renderer, int row, int col) {
345 Component c = super.prepareRenderer(renderer, row, col);
346 if (c instanceof JComponent) {
347 JComponent jc = (JComponent) c;
348 jc.setToolTipText(getValueAt(row, col).toString());
349 if (content.length > row
350 && content[row].length > 5
351 && content[row][5] instanceof IGpxTrack) {
352 Color color = ((IGpxTrack) content[row][5]).getColor();
353 if (color != null) {
354 double brightness = Math.sqrt(Math.pow(color.getRed(), 2) * .241
355 + Math.pow(color.getGreen(), 2) * .691
356 + Math.pow(color.getBlue(), 2) * .068);
357 if (brightness > 250) {
358 color = color.darker();
359 }
360 if (isRowSelected(row)) {
361 jc.setBackground(color);
362 if (brightness <= 130) {
363 jc.setForeground(Color.WHITE);
364 } else {
365 jc.setForeground(Color.BLACK);
366 }
367 } else {
368 if (brightness > 200) {
369 color = color.darker(); //brightness >250 is darkened twice on purpose
370 }
371 jc.setForeground(color);
372 jc.setBackground(Color.WHITE);
373 }
374 } else {
375 jc.setForeground(Color.BLACK);
376 if (isRowSelected(row)) {
377 jc.setBackground(new Color(175, 210, 210));
378 } else {
379 jc.setBackground(Color.WHITE);
380 }
381 }
382 }
383 }
384 return c;
385 }
386
387 @Override
388 public boolean isCellEditable(int rowIndex, int colIndex) {
389 return colIndex <= 1;
390 }
391
392 @Override
393 public void tableChanged(TableModelEvent e) {
394 super.tableChanged(e);
395 int col = e.getColumn();
396 int row = e.getFirstRow();
397 if (row >= 0 && row < content.length && col >= 0 && col <= 1) {
398 Object t = content[row][5];
399 String val = (String) getValueAt(row, col);
400 if (t != null && t instanceof IGpxTrack) {
401 IGpxTrack trk = (IGpxTrack) t;
402 if (col == 0) {
403 trk.put("name", val);
404 } else {
405 trk.put("desc", val);
406 }
407 } else {
408 throw new InvalidArgumentException("Invalid object in table, must be IGpxTrack.");
409 }
410 }
411 }
412 }
413}
Note: See TracBrowser for help on using the repository browser.