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.Color; 008import java.awt.Component; 009import java.awt.Dimension; 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.ArrayList; 017import java.util.Arrays; 018import java.util.Comparator; 019import java.util.List; 020import java.util.Map; 021import java.util.Objects; 022import java.util.Optional; 023 024import javax.swing.AbstractAction; 025import javax.swing.JColorChooser; 026import javax.swing.JComponent; 027import javax.swing.JLabel; 028import javax.swing.JOptionPane; 029import javax.swing.JPanel; 030import javax.swing.JScrollPane; 031import javax.swing.JTable; 032import javax.swing.JToggleButton; 033import javax.swing.ListSelectionModel; 034import javax.swing.event.TableModelEvent; 035import javax.swing.table.DefaultTableModel; 036import javax.swing.table.TableCellRenderer; 037import javax.swing.table.TableModel; 038import javax.swing.table.TableRowSorter; 039 040import org.apache.commons.jcs.access.exception.InvalidArgumentException; 041import org.openstreetmap.josm.data.SystemOfMeasurement; 042import org.openstreetmap.josm.data.gpx.GpxConstants; 043import org.openstreetmap.josm.data.gpx.IGpxTrack; 044import org.openstreetmap.josm.gui.ExtendedDialog; 045import org.openstreetmap.josm.gui.MainApplication; 046import org.openstreetmap.josm.gui.layer.GpxLayer; 047import org.openstreetmap.josm.gui.preferences.display.GPXSettingsPanel; 048import org.openstreetmap.josm.gui.util.WindowGeometry; 049import org.openstreetmap.josm.tools.GBC; 050import org.openstreetmap.josm.tools.ImageProvider; 051import org.openstreetmap.josm.tools.OpenBrowser; 052 053/** 054 * allows the user to choose which of the downloaded tracks should be displayed. 055 * they can be chosen from the gpx layer context menu. 056 */ 057public class ChooseTrackVisibilityAction extends AbstractAction { 058 private final transient GpxLayer layer; 059 060 private DateFilterPanel dateFilter; 061 private JTable table; 062 063 /** 064 * Constructs a new {@code ChooseTrackVisibilityAction}. 065 * @param layer The associated GPX layer 066 */ 067 public ChooseTrackVisibilityAction(final GpxLayer layer) { 068 super(tr("Choose track visibility and colors")); 069 new ImageProvider("dialogs/filter").getResource().attachImageIcon(this, true); 070 this.layer = layer; 071 putValue("help", ht("/Action/ChooseTrackVisibility")); 072 } 073 074 /** 075 * Class to format a length according to SystemOfMesurement. 076 */ 077 private static final class TrackLength { 078 private final double value; 079 080 /** 081 * Constructs a new {@code TrackLength} object with a given length. 082 * @param value length of the track 083 */ 084 TrackLength(double value) { 085 this.value = value; 086 } 087 088 /** 089 * Provides string representation. 090 * @return String representation depending of SystemOfMeasurement 091 */ 092 @Override 093 public String toString() { 094 return SystemOfMeasurement.getSystemOfMeasurement().getDistText(value); 095 } 096 } 097 098 /** 099 * Comparator for TrackLength objects 100 */ 101 private static final class LengthContentComparator implements Comparator<TrackLength>, Serializable { 102 103 private static final long serialVersionUID = 1L; 104 105 /** 106 * Compare 2 TrackLength objects relative to the real length 107 */ 108 @Override 109 public int compare(TrackLength l0, TrackLength l1) { 110 return Double.compare(l0.value, l1.value); 111 } 112 } 113 114 /** 115 * Gathers all available data for the tracks and returns them as array of arrays 116 * in the expected column order. 117 * @return table data 118 */ 119 private Object[][] buildTableContents() { 120 Object[][] tracks = new Object[layer.data.tracks.size()][5]; 121 int i = 0; 122 for (IGpxTrack trk : layer.data.tracks) { 123 Map<String, Object> attr = trk.getAttributes(); 124 String name = (String) Optional.ofNullable(attr.get(GpxConstants.GPX_NAME)).orElse(""); 125 String desc = (String) Optional.ofNullable(attr.get(GpxConstants.GPX_DESC)).orElse(""); 126 String time = GpxLayer.getTimespanForTrack(trk); 127 TrackLength length = new TrackLength(trk.length()); 128 String url = (String) Optional.ofNullable(attr.get("url")).orElse(""); 129 tracks[i] = new Object[]{name, desc, time, length, url, trk}; 130 i++; 131 } 132 return tracks; 133 } 134 135 private void showColorDialog(List<IGpxTrack> tracks) { 136 Color cl = tracks.stream().filter(Objects::nonNull) 137 .map(IGpxTrack::getColor).filter(Objects::nonNull) 138 .findAny().orElse(GpxDrawHelper.DEFAULT_COLOR_PROPERTY.get()); 139 JColorChooser c = new JColorChooser(cl); 140 Object[] options = {tr("OK"), tr("Cancel"), tr("Default")}; 141 int answer = JOptionPane.showOptionDialog( 142 MainApplication.getMainFrame(), 143 c, 144 tr("Choose a color"), 145 JOptionPane.OK_CANCEL_OPTION, 146 JOptionPane.PLAIN_MESSAGE, 147 null, 148 options, 149 options[0] 150 ); 151 switch (answer) { 152 case 0: 153 tracks.forEach(t -> t.setColor(c.getColor())); 154 GPXSettingsPanel.putLayerPrefLocal(layer, "colormode", "0"); //set Colormode to none 155 break; 156 case 1: 157 return; 158 case 2: 159 tracks.forEach(t -> t.setColor(null)); 160 break; 161 } 162 table.repaint(); 163 } 164 165 /** 166 * Builds an editable table whose 5th column will open a browser when double clicked. 167 * The table will fill its parent. 168 * @param content table data 169 * @return non-editable table 170 */ 171 private static JTable buildTable(Object[]... content) { 172 final String[] headers = {tr("Name"), tr("Description"), tr("Timespan"), tr("Length"), tr("URL")}; 173 DefaultTableModel model = new DefaultTableModel(content, headers); 174 final GpxTrackTable t = new GpxTrackTable(content, model); 175 // define how to sort row 176 TableRowSorter<DefaultTableModel> rowSorter = new TableRowSorter<>(); 177 t.setRowSorter(rowSorter); 178 rowSorter.setModel(model); 179 rowSorter.setComparator(3, new LengthContentComparator()); 180 // default column widths 181 t.getColumnModel().getColumn(0).setPreferredWidth(220); 182 t.getColumnModel().getColumn(1).setPreferredWidth(300); 183 t.getColumnModel().getColumn(2).setPreferredWidth(200); 184 t.getColumnModel().getColumn(3).setPreferredWidth(50); 185 t.getColumnModel().getColumn(4).setPreferredWidth(100); 186 // make the link clickable 187 final MouseListener urlOpener = new MouseAdapter() { 188 @Override 189 public void mouseClicked(MouseEvent e) { 190 if (e.getClickCount() != 2) { 191 return; 192 } 193 JTable t = (JTable) e.getSource(); 194 int col = t.convertColumnIndexToModel(t.columnAtPoint(e.getPoint())); 195 if (col != 4) { 196 return; 197 } 198 int row = t.rowAtPoint(e.getPoint()); 199 String url = (String) t.getValueAt(row, col); 200 if (url == null || url.isEmpty()) { 201 return; 202 } 203 OpenBrowser.displayUrl(url); 204 } 205 }; 206 t.addMouseListener(urlOpener); 207 t.setFillsViewportHeight(true); 208 t.putClientProperty("terminateEditOnFocusLost", true); 209 return t; 210 } 211 212 private boolean noUpdates; 213 214 /** selects all rows (=tracks) in the table that are currently visible on the layer*/ 215 private void selectVisibleTracksInTable() { 216 // don't select any tracks if the layer is not visible 217 if (!layer.isVisible()) { 218 return; 219 } 220 ListSelectionModel s = table.getSelectionModel(); 221 s.setValueIsAdjusting(true); 222 s.clearSelection(); 223 for (int i = 0; i < layer.trackVisibility.length; i++) { 224 if (layer.trackVisibility[i]) { 225 s.addSelectionInterval(i, i); 226 } 227 } 228 s.setValueIsAdjusting(false); 229 } 230 231 /** listens to selection changes in the table and redraws the map */ 232 private void listenToSelectionChanges() { 233 table.getSelectionModel().addListSelectionListener(e -> { 234 if (noUpdates || !(e.getSource() instanceof ListSelectionModel)) { 235 return; 236 } 237 updateVisibilityFromTable(); 238 }); 239 } 240 241 private void updateVisibilityFromTable() { 242 ListSelectionModel s = table.getSelectionModel(); 243 for (int i = 0; i < layer.trackVisibility.length; i++) { 244 layer.trackVisibility[table.convertRowIndexToModel(i)] = s.isSelectedIndex(i); 245 } 246 layer.invalidate(); 247 } 248 249 @Override 250 public void actionPerformed(ActionEvent ae) { 251 final JPanel msg = new JPanel(new GridBagLayout()); 252 253 dateFilter = new DateFilterPanel(layer, "gpx.traces", false); 254 dateFilter.setFilterAppliedListener(e -> { 255 noUpdates = true; 256 selectVisibleTracksInTable(); 257 noUpdates = false; 258 layer.invalidate(); 259 }); 260 dateFilter.loadFromPrefs(); 261 262 final JToggleButton b = new JToggleButton(new AbstractAction(tr("Select by date")) { 263 @Override public void actionPerformed(ActionEvent e) { 264 if (((JToggleButton) e.getSource()).isSelected()) { 265 dateFilter.setEnabled(true); 266 dateFilter.applyFilter(); 267 } else { 268 dateFilter.setEnabled(false); 269 } 270 } 271 }); 272 dateFilter.setEnabled(false); 273 msg.add(b, GBC.std().insets(0, 0, 5, 0)); 274 msg.add(dateFilter, GBC.eol().insets(0, 0, 10, 0).fill(GBC.HORIZONTAL)); 275 276 msg.add(new JLabel(tr("<html>Select all tracks that you want to be displayed. " + 277 "You can drag select a range of tracks or use CTRL+Click to select specific ones. " + 278 "The map is updated live in the background. Open the URLs by double clicking them, " + 279 "edit name and description by double clicking the cell.</html>")), 280 GBC.eop().fill(GBC.HORIZONTAL)); 281 // build table 282 final boolean[] trackVisibilityBackup = layer.trackVisibility.clone(); 283 Object[][] content = buildTableContents(); 284 table = buildTable(content); 285 selectVisibleTracksInTable(); 286 listenToSelectionChanges(); 287 // make the table scrollable 288 JScrollPane scrollPane = new JScrollPane(table); 289 msg.add(scrollPane, GBC.eol().fill(GBC.BOTH)); 290 291 int v = 1; 292 // build dialog 293 ExtendedDialog ed = new ExtendedDialog(MainApplication.getMainFrame(), 294 tr("Set track visibility for {0}", layer.getName()), 295 tr("Set color for selected tracks..."), tr("Show all"), tr("Show selected only"), tr("Close")) { 296 @Override 297 protected void buttonAction(int buttonIndex, ActionEvent evt) { 298 if (buttonIndex == 0) { 299 List<IGpxTrack> trks = new ArrayList<>(); 300 for (int i : table.getSelectedRows()) { 301 Object trk = content[i][5]; 302 if (trk != null && trk instanceof IGpxTrack) { 303 trks.add((IGpxTrack) trk); 304 } 305 } 306 showColorDialog(trks); 307 } else { 308 super.buttonAction(buttonIndex, evt); 309 } 310 } 311 }; 312 ed.setButtonIcons("colorchooser", "eye", "dialogs/filter", "cancel"); 313 ed.setContent(msg, false); 314 ed.setDefaultButton(2); 315 ed.setCancelButton(3); 316 ed.configureContextsensitiveHelp("/Action/ChooseTrackVisibility", true); 317 ed.setRememberWindowGeometry(getClass().getName() + ".geometry", 318 WindowGeometry.centerInWindow(MainApplication.getMainFrame(), new Dimension(1000, 500))); 319 ed.showDialog(); 320 dateFilter.saveInPrefs(); 321 v = ed.getValue(); 322 // cancel for unknown buttons and copy back original settings 323 if (v != 2 && v != 3) { 324 layer.trackVisibility = Arrays.copyOf(trackVisibilityBackup, layer.trackVisibility.length); 325 MainApplication.getMap().repaint(); 326 return; 327 } 328 // set visibility (2 = show all, 3 = filter). If no tracks are selected 329 // set all of them visible and... 330 ListSelectionModel s = table.getSelectionModel(); 331 final boolean all = v == 2 || s.isSelectionEmpty(); 332 for (int i = 0; i < layer.trackVisibility.length; i++) { 333 layer.trackVisibility[table.convertRowIndexToModel(i)] = all || s.isSelectedIndex(i); 334 } 335 // layer has been changed 336 layer.invalidate(); 337 // ...sync with layer visibility instead to avoid having two ways to hide everything 338 layer.setVisible(v == 2 || !s.isSelectionEmpty()); 339 } 340 341 private static class GpxTrackTable extends JTable { 342 final Object[][] content; 343 344 GpxTrackTable(Object[][] content, TableModel model) { 345 super(model); 346 this.content = content; 347 } 348 349 @Override 350 public Component prepareRenderer(TableCellRenderer renderer, int row, int col) { 351 Component c = super.prepareRenderer(renderer, row, col); 352 if (c instanceof JComponent) { 353 JComponent jc = (JComponent) c; 354 jc.setToolTipText(getValueAt(row, col).toString()); 355 if (content.length > row 356 && content[row].length > 5 357 && content[row][5] instanceof IGpxTrack) { 358 Color color = ((IGpxTrack) content[row][5]).getColor(); 359 if (color != null) { 360 double brightness = Math.sqrt(Math.pow(color.getRed(), 2) * .241 361 + Math.pow(color.getGreen(), 2) * .691 362 + Math.pow(color.getBlue(), 2) * .068); 363 if (brightness > 250) { 364 color = color.darker(); 365 } 366 if (isRowSelected(row)) { 367 jc.setBackground(color); 368 if (brightness <= 130) { 369 jc.setForeground(Color.WHITE); 370 } else { 371 jc.setForeground(Color.BLACK); 372 } 373 } else { 374 if (brightness > 200) { 375 color = color.darker(); //brightness >250 is darkened twice on purpose 376 } 377 jc.setForeground(color); 378 jc.setBackground(Color.WHITE); 379 } 380 } else { 381 jc.setForeground(Color.BLACK); 382 if (isRowSelected(row)) { 383 jc.setBackground(new Color(175, 210, 210)); 384 } else { 385 jc.setBackground(Color.WHITE); 386 } 387 } 388 } 389 } 390 return c; 391 } 392 393 @Override 394 public boolean isCellEditable(int rowIndex, int colIndex) { 395 return colIndex <= 1; 396 } 397 398 @Override 399 public void tableChanged(TableModelEvent e) { 400 super.tableChanged(e); 401 int col = e.getColumn(); 402 int row = e.getFirstRow(); 403 if (row >= 0 && row < content.length && col >= 0 && col <= 1) { 404 Object t = content[row][5]; 405 String val = (String) getValueAt(row, col); 406 if (t != null && t instanceof IGpxTrack) { 407 IGpxTrack trk = (IGpxTrack) t; 408 if (col == 0) { 409 trk.put("name", val); 410 } else { 411 trk.put("desc", val); 412 } 413 } else { 414 throw new InvalidArgumentException("Invalid object in table, must be IGpxTrack."); 415 } 416 } 417 } 418 } 419}