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.GridBagLayout; 010import java.awt.event.ActionEvent; 011import java.awt.event.ActionListener; 012import java.awt.event.MouseAdapter; 013import java.awt.event.MouseEvent; 014import java.awt.event.MouseListener; 015import java.io.Serializable; 016import java.util.Arrays; 017import java.util.Comparator; 018import java.util.Map; 019 020import javax.swing.AbstractAction; 021import javax.swing.JComponent; 022import javax.swing.JLabel; 023import javax.swing.JPanel; 024import javax.swing.JScrollPane; 025import javax.swing.JTable; 026import javax.swing.JToggleButton; 027import javax.swing.ListSelectionModel; 028import javax.swing.event.ListSelectionEvent; 029import javax.swing.event.ListSelectionListener; 030import javax.swing.table.DefaultTableModel; 031import javax.swing.table.TableCellRenderer; 032import javax.swing.table.TableRowSorter; 033 034import org.openstreetmap.josm.Main; 035import org.openstreetmap.josm.data.SystemOfMeasurement; 036import org.openstreetmap.josm.data.gpx.GpxConstants; 037import org.openstreetmap.josm.data.gpx.GpxTrack; 038import org.openstreetmap.josm.gui.ExtendedDialog; 039import org.openstreetmap.josm.gui.layer.GpxLayer; 040import org.openstreetmap.josm.tools.GBC; 041import org.openstreetmap.josm.tools.ImageProvider; 042import org.openstreetmap.josm.tools.OpenBrowser; 043import org.openstreetmap.josm.tools.WindowGeometry; 044 045/** 046 * allows the user to choose which of the downloaded tracks should be displayed. 047 * they can be chosen from the gpx layer context menu. 048 */ 049public class ChooseTrackVisibilityAction extends AbstractAction { 050 private final transient GpxLayer layer; 051 052 private DateFilterPanel dateFilter; 053 private JTable table; 054 055 /** 056 * Constructs a new {@code ChooseTrackVisibilityAction}. 057 * @param layer The associated GPX layer 058 */ 059 public ChooseTrackVisibilityAction(final GpxLayer layer) { 060 super(tr("Choose visible tracks"), ImageProvider.get("dialogs/filter")); 061 this.layer = layer; 062 putValue("help", ht("/Action/ChooseTrackVisibility")); 063 } 064 065 /** 066 * Class to format a length according to SystemOfMesurement. 067 */ 068 private static final class TrackLength { 069 private final double value; 070 071 /** 072 * Constructs a new {@code TrackLength} object with a given length. 073 * @param value length of the track 074 */ 075 TrackLength(double value) { 076 this.value = value; 077 } 078 079 /** 080 * Provides string representation. 081 * @return String representation depending of SystemOfMeasurement 082 */ 083 @Override 084 public String toString() { 085 return SystemOfMeasurement.getSystemOfMeasurement().getDistText(value); 086 } 087 } 088 089 /** 090 * Comparator for TrackLength objects 091 */ 092 private static final class LengthContentComparator implements Comparator<TrackLength>, Serializable { 093 094 private static final long serialVersionUID = 1L; 095 096 /** 097 * Compare 2 TrackLength objects relative to the real length 098 */ 099 @Override 100 public int compare(TrackLength l0, TrackLength l1) { 101 if (l0.value < l1.value) 102 return -1; 103 else if (l0.value > l1.value) 104 return 1; 105 return 0; 106 } 107 } 108 109 /** 110 * Gathers all available data for the tracks and returns them as array of arrays 111 * in the expected column order. 112 * @return table data 113 */ 114 private Object[][] buildTableContents() { 115 Object[][] tracks = new Object[layer.data.tracks.size()][5]; 116 int i = 0; 117 for (GpxTrack trk : layer.data.tracks) { 118 Map<String, Object> attr = trk.getAttributes(); 119 String name = (String) (attr.containsKey(GpxConstants.GPX_NAME) ? attr.get(GpxConstants.GPX_NAME) : ""); 120 String desc = (String) (attr.containsKey(GpxConstants.GPX_DESC) ? attr.get(GpxConstants.GPX_DESC) : ""); 121 String time = GpxLayer.getTimespanForTrack(trk); 122 TrackLength length = new TrackLength(trk.length()); 123 String url = (String) (attr.containsKey("url") ? attr.get("url") : ""); 124 tracks[i] = new Object[]{name, desc, time, length, url}; 125 i++; 126 } 127 return tracks; 128 } 129 130 /** 131 * Builds an non-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 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 JTable t = new JTable(model) { 140 @Override 141 public Component prepareRenderer(TableCellRenderer renderer, int row, int col) { 142 Component c = super.prepareRenderer(renderer, row, col); 143 if (c instanceof JComponent) { 144 JComponent jc = (JComponent) c; 145 jc.setToolTipText(getValueAt(row, col).toString()); 146 } 147 return c; 148 } 149 150 @Override 151 public boolean isCellEditable(int rowIndex, int colIndex) { 152 return false; 153 } 154 }; 155 // define how to sort row 156 TableRowSorter<DefaultTableModel> rowSorter = new TableRowSorter<>(); 157 t.setRowSorter(rowSorter); 158 rowSorter.setModel(model); 159 rowSorter.setComparator(3, new LengthContentComparator()); 160 // default column widths 161 t.getColumnModel().getColumn(0).setPreferredWidth(220); 162 t.getColumnModel().getColumn(1).setPreferredWidth(300); 163 t.getColumnModel().getColumn(2).setPreferredWidth(200); 164 t.getColumnModel().getColumn(3).setPreferredWidth(50); 165 t.getColumnModel().getColumn(4).setPreferredWidth(100); 166 // make the link clickable 167 final MouseListener urlOpener = new MouseAdapter() { 168 @Override 169 public void mouseClicked(MouseEvent e) { 170 if (e.getClickCount() != 2) { 171 return; 172 } 173 JTable t = (JTable) e.getSource(); 174 int col = t.convertColumnIndexToModel(t.columnAtPoint(e.getPoint())); 175 if (col != 4) { 176 return; 177 } 178 int row = t.rowAtPoint(e.getPoint()); 179 String url = (String) t.getValueAt(row, col); 180 if (url == null || url.isEmpty()) { 181 return; 182 } 183 OpenBrowser.displayUrl(url); 184 } 185 }; 186 t.addMouseListener(urlOpener); 187 t.setFillsViewportHeight(true); 188 return t; 189 } 190 191 private boolean noUpdates; 192 193 /** selects all rows (=tracks) in the table that are currently visible on the layer*/ 194 private void selectVisibleTracksInTable() { 195 // don't select any tracks if the layer is not visible 196 if (!layer.isVisible()) { 197 return; 198 } 199 ListSelectionModel s = table.getSelectionModel(); 200 s.clearSelection(); 201 for (int i = 0; i < layer.trackVisibility.length; i++) { 202 if (layer.trackVisibility[i]) { 203 s.addSelectionInterval(i, i); 204 } 205 } 206 } 207 208 /** listens to selection changes in the table and redraws the map */ 209 private void listenToSelectionChanges() { 210 table.getSelectionModel().addListSelectionListener(new ListSelectionListener() { 211 @Override 212 public void valueChanged(ListSelectionEvent e) { 213 if (noUpdates || !(e.getSource() instanceof ListSelectionModel)) { 214 return; 215 } 216 updateVisibilityFromTable(); 217 } 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 Main.map.mapView.preferenceChanged(null); 227 Main.map.repaint(100); 228 } 229 230 @Override 231 public void actionPerformed(ActionEvent arg0) { 232 final JPanel msg = new JPanel(new GridBagLayout()); 233 234 dateFilter = new DateFilterPanel(layer, "gpx.traces", false); 235 dateFilter.setFilterAppliedListener(new ActionListener() { 236 @Override public void actionPerformed(ActionEvent e) { 237 noUpdates = true; 238 selectVisibleTracksInTable(); 239 noUpdates = false; 240 Main.map.mapView.preferenceChanged(null); 241 Main.map.repaint(100); 242 } 243 }); 244 dateFilter.loadFromPrefs(); 245 246 final JToggleButton b = new JToggleButton(new AbstractAction(tr("Select by date")) { 247 @Override public void actionPerformed(ActionEvent e) { 248 if (((JToggleButton) e.getSource()).isSelected()) { 249 dateFilter.setEnabled(true); 250 dateFilter.applyFilter(); 251 } else { 252 dateFilter.setEnabled(false); 253 } 254 } 255 }); 256 dateFilter.setEnabled(false); 257 msg.add(b, GBC.std().insets(0, 0, 5, 0)); 258 msg.add(dateFilter, GBC.eol().insets(0, 0, 10, 0).fill(GBC.HORIZONTAL)); 259 260 msg.add(new JLabel(tr("<html>Select all tracks that you want to be displayed. " + 261 "You can drag select a range of tracks or use CTRL+Click to select specific ones. " + 262 "The map is updated live in the background. Open the URLs by double clicking them.</html>")), 263 GBC.eop().fill(GBC.HORIZONTAL)); 264 // build table 265 final boolean[] trackVisibilityBackup = layer.trackVisibility.clone(); 266 table = buildTable(buildTableContents()); 267 selectVisibleTracksInTable(); 268 listenToSelectionChanges(); 269 // make the table scrollable 270 JScrollPane scrollPane = new JScrollPane(table); 271 msg.add(scrollPane, GBC.eol().fill(GBC.BOTH)); 272 273 // build dialog 274 ExtendedDialog ed = new ExtendedDialog(Main.parent, tr("Set track visibility for {0}", layer.getName()), 275 new String[]{tr("Show all"), tr("Show selected only"), tr("Cancel")}); 276 ed.setButtonIcons(new String[]{"eye", "dialogs/filter", "cancel"}); 277 ed.setContent(msg, false); 278 ed.setDefaultButton(2); 279 ed.setCancelButton(3); 280 ed.configureContextsensitiveHelp("/Action/ChooseTrackVisibility", true); 281 ed.setRememberWindowGeometry(getClass().getName() + ".geometry", WindowGeometry.centerInWindow(Main.parent, new Dimension(1000, 500))); 282 ed.showDialog(); 283 dateFilter.saveInPrefs(); 284 int 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 // set visibility (1 = show all, 2 = filter). If no tracks are selected 292 // set all of them visible and... 293 ListSelectionModel s = table.getSelectionModel(); 294 final boolean all = v == 1 || s.isSelectionEmpty(); 295 for (int i = 0; i < layer.trackVisibility.length; i++) { 296 layer.trackVisibility[table.convertRowIndexToModel(i)] = all || s.isSelectedIndex(i); 297 } 298 // ...sync with layer visibility instead to avoid having two ways to hide everything 299 layer.setVisible(v == 1 || !s.isSelectionEmpty()); 300 301 Main.map.mapView.preferenceChanged(null); 302 Main.map.repaint(); 303 } 304 305}