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