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

Last change on this file was 19307, checked in by taylor.smock, 17 months ago

Fix most new PMD issues

It would be better to use the newer switch syntax introduced in Java 14 (JEP 361),
but we currently target Java 11+. When we move to Java 17, this should be
reverted and the newer switch syntax should be used.

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