| 1 | // License: GPL. For details, see LICENSE file.
|
|---|
| 2 | package org.openstreetmap.josm.gui.layer.gpx;
|
|---|
| 3 |
|
|---|
| 4 | import static org.openstreetmap.josm.gui.help.HelpUtil.ht;
|
|---|
| 5 | import static org.openstreetmap.josm.tools.I18n.tr;
|
|---|
| 6 |
|
|---|
| 7 | import java.awt.Color;
|
|---|
| 8 | import java.awt.Component;
|
|---|
| 9 | import java.awt.Dimension;
|
|---|
| 10 | import java.awt.GridBagLayout;
|
|---|
| 11 | import java.awt.event.ActionEvent;
|
|---|
| 12 | import java.awt.event.MouseAdapter;
|
|---|
| 13 | import java.awt.event.MouseEvent;
|
|---|
| 14 | import java.awt.event.MouseListener;
|
|---|
| 15 | import java.time.Instant;
|
|---|
| 16 | import java.util.Arrays;
|
|---|
| 17 | import java.util.Comparator;
|
|---|
| 18 | import java.util.List;
|
|---|
| 19 | import java.util.Map;
|
|---|
| 20 | import java.util.Objects;
|
|---|
| 21 | import java.util.Optional;
|
|---|
| 22 | import java.util.stream.Collectors;
|
|---|
| 23 | import java.util.stream.IntStream;
|
|---|
| 24 |
|
|---|
| 25 | import javax.swing.AbstractAction;
|
|---|
| 26 | import javax.swing.JColorChooser;
|
|---|
| 27 | import javax.swing.JComponent;
|
|---|
| 28 | import javax.swing.JLabel;
|
|---|
| 29 | import javax.swing.JOptionPane;
|
|---|
| 30 | import javax.swing.JPanel;
|
|---|
| 31 | import javax.swing.JScrollPane;
|
|---|
| 32 | import javax.swing.JTable;
|
|---|
| 33 | import javax.swing.JToggleButton;
|
|---|
| 34 | import javax.swing.ListSelectionModel;
|
|---|
| 35 | import javax.swing.event.TableModelEvent;
|
|---|
| 36 | import javax.swing.table.DefaultTableCellRenderer;
|
|---|
| 37 | import javax.swing.table.DefaultTableModel;
|
|---|
| 38 | import javax.swing.table.TableCellRenderer;
|
|---|
| 39 | import javax.swing.table.TableModel;
|
|---|
| 40 | import javax.swing.table.TableRowSorter;
|
|---|
| 41 |
|
|---|
| 42 | import org.apache.commons.jcs3.access.exception.InvalidArgumentException;
|
|---|
| 43 | import org.openstreetmap.josm.data.SystemOfMeasurement;
|
|---|
| 44 | import org.openstreetmap.josm.data.gpx.GpxConstants;
|
|---|
| 45 | import org.openstreetmap.josm.data.gpx.GpxData;
|
|---|
| 46 | import org.openstreetmap.josm.data.gpx.IGpxTrack;
|
|---|
| 47 | import org.openstreetmap.josm.gui.ExtendedDialog;
|
|---|
| 48 | import org.openstreetmap.josm.gui.MainApplication;
|
|---|
| 49 | import org.openstreetmap.josm.gui.layer.GpxLayer;
|
|---|
| 50 | import org.openstreetmap.josm.gui.preferences.display.GPXSettingsPanel;
|
|---|
| 51 | import org.openstreetmap.josm.gui.util.TableHelper;
|
|---|
| 52 | import org.openstreetmap.josm.gui.util.WindowGeometry;
|
|---|
| 53 | import org.openstreetmap.josm.tools.GBC;
|
|---|
| 54 | import org.openstreetmap.josm.tools.ImageProvider;
|
|---|
| 55 | import org.openstreetmap.josm.tools.OpenBrowser;
|
|---|
| 56 | import org.openstreetmap.josm.tools.Utils;
|
|---|
| 57 | import 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 | */
|
|---|
| 63 | public 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 | }
|
|---|