source: josm/trunk/src/org/openstreetmap/josm/gui/layer/GpxLayer.java@ 16867

Last change on this file since 16867 was 16867, checked in by simon04, 4 years ago

see #10488 - PMD

  • Property svn:eol-style set to native
File size: 22.2 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.gui.layer;
3
4import static org.openstreetmap.josm.tools.I18n.tr;
5import static org.openstreetmap.josm.tools.I18n.trn;
6
7import java.awt.Color;
8import java.awt.Dimension;
9import java.awt.Graphics2D;
10import java.awt.event.ActionEvent;
11import java.io.File;
12import java.text.DateFormat;
13import java.util.ArrayList;
14import java.util.Arrays;
15import java.util.Collections;
16import java.util.Date;
17import java.util.List;
18import java.util.NoSuchElementException;
19import java.util.stream.Collectors;
20
21import javax.swing.AbstractAction;
22import javax.swing.Action;
23import javax.swing.Icon;
24import javax.swing.JScrollPane;
25import javax.swing.SwingUtilities;
26
27import org.openstreetmap.josm.actions.ExpertToggleAction;
28import org.openstreetmap.josm.actions.ExpertToggleAction.ExpertModeChangeListener;
29import org.openstreetmap.josm.actions.RenameLayerAction;
30import org.openstreetmap.josm.actions.SaveActionBase;
31import org.openstreetmap.josm.data.Bounds;
32import org.openstreetmap.josm.data.Data;
33import org.openstreetmap.josm.data.SystemOfMeasurement;
34import org.openstreetmap.josm.data.gpx.GpxConstants;
35import org.openstreetmap.josm.data.gpx.GpxData;
36import org.openstreetmap.josm.data.gpx.GpxData.GpxDataChangeListener;
37import org.openstreetmap.josm.data.gpx.IGpxTrack;
38import org.openstreetmap.josm.data.gpx.IGpxTrackSegment;
39import org.openstreetmap.josm.data.osm.visitor.BoundingXYVisitor;
40import org.openstreetmap.josm.data.projection.Projection;
41import org.openstreetmap.josm.gui.MainApplication;
42import org.openstreetmap.josm.gui.MapView;
43import org.openstreetmap.josm.gui.dialogs.LayerListDialog;
44import org.openstreetmap.josm.gui.dialogs.LayerListPopup;
45import org.openstreetmap.josm.gui.io.importexport.GpxImporter;
46import org.openstreetmap.josm.gui.layer.JumpToMarkerActions.JumpToMarkerLayer;
47import org.openstreetmap.josm.gui.layer.JumpToMarkerActions.JumpToNextMarker;
48import org.openstreetmap.josm.gui.layer.JumpToMarkerActions.JumpToPreviousMarker;
49import org.openstreetmap.josm.gui.layer.gpx.ChooseTrackVisibilityAction;
50import org.openstreetmap.josm.gui.layer.gpx.ConvertFromGpxLayerAction;
51import org.openstreetmap.josm.gui.layer.gpx.CustomizeDrawingAction;
52import org.openstreetmap.josm.gui.layer.gpx.DownloadAlongTrackAction;
53import org.openstreetmap.josm.gui.layer.gpx.DownloadWmsAlongTrackAction;
54import org.openstreetmap.josm.gui.layer.gpx.GpxDrawHelper;
55import org.openstreetmap.josm.gui.layer.gpx.ImportAudioAction;
56import org.openstreetmap.josm.gui.layer.gpx.ImportImagesAction;
57import org.openstreetmap.josm.gui.layer.gpx.MarkersFromNamedPointsAction;
58import org.openstreetmap.josm.gui.layer.markerlayer.MarkerLayer;
59import org.openstreetmap.josm.gui.preferences.display.GPXSettingsPanel;
60import org.openstreetmap.josm.gui.widgets.HtmlPanel;
61import org.openstreetmap.josm.tools.ImageProvider;
62import org.openstreetmap.josm.tools.Logging;
63import org.openstreetmap.josm.tools.Utils;
64import org.openstreetmap.josm.tools.date.DateUtils;
65
66/**
67 * A layer that displays data from a Gpx file / the OSM gpx downloads.
68 */
69public class GpxLayer extends AbstractModifiableLayer implements ExpertModeChangeListener, JumpToMarkerLayer {
70
71 /** GPX data */
72 public GpxData data;
73 private boolean isLocalFile;
74 private boolean isExpertMode;
75
76 /**
77 * used by {@link ChooseTrackVisibilityAction} to determine which tracks to show/hide
78 *
79 * Call {@link #invalidate()} after each change!
80 *
81 * TODO: Make it private, make it respond to track changes.
82 */
83 public boolean[] trackVisibility = new boolean[0];
84 /**
85 * Added as field to be kept as reference.
86 */
87 private final GpxDataChangeListener dataChangeListener = e -> this.invalidate();
88 /**
89 * The MarkerLayer imported from the same file.
90 */
91 private MarkerLayer linkedMarkerLayer;
92
93 /**
94 * Current segment for {@link JumpToMarkerLayer}.
95 */
96 private IGpxTrackSegment currentSegment;
97
98 /**
99 * Constructs a new {@code GpxLayer} without name.
100 * @param d GPX data
101 */
102 public GpxLayer(GpxData d) {
103 this(d, null, false);
104 }
105
106 /**
107 * Constructs a new {@code GpxLayer} with a given name.
108 * @param d GPX data
109 * @param name layer name
110 */
111 public GpxLayer(GpxData d, String name) {
112 this(d, name, false);
113 }
114
115 /**
116 * Constructs a new {@code GpxLayer} with a given name, that can be attached to a local file.
117 * @param d GPX data
118 * @param name layer name
119 * @param isLocal whether data is attached to a local file
120 */
121 public GpxLayer(GpxData d, String name, boolean isLocal) {
122 super(name);
123 data = d;
124 data.addWeakChangeListener(dataChangeListener);
125 trackVisibility = new boolean[data.getTracks().size()];
126 Arrays.fill(trackVisibility, true);
127 isLocalFile = isLocal;
128 ExpertToggleAction.addExpertModeChangeListener(this, true);
129 }
130
131 @Override
132 public Color getColor() {
133 Color[] c = data.getTracks().stream().map(t -> t.getColor()).distinct().toArray(Color[]::new);
134 return c.length == 1 ? c[0] : null; //only return if exactly one distinct color present
135 }
136
137 @Override
138 public void setColor(Color color) {
139 data.beginUpdate();
140 for (IGpxTrack trk : data.getTracks()) {
141 trk.setColor(color);
142 }
143 GPXSettingsPanel.putLayerPrefLocal(this, "colormode", "0");
144 data.endUpdate();
145 }
146
147 @Override
148 public boolean hasColor() {
149 return true;
150 }
151
152 /**
153 * Returns a human readable string that shows the timespan of the given track
154 * @param trk The GPX track for which timespan is displayed
155 * @return The timespan as a string
156 */
157 public static String getTimespanForTrack(IGpxTrack trk) {
158 Date[] bounds = GpxData.getMinMaxTimeForTrack(trk);
159 String ts = "";
160 if (bounds != null) {
161 DateFormat df = DateUtils.getDateFormat(DateFormat.SHORT);
162 String earliestDate = df.format(bounds[0]);
163 String latestDate = df.format(bounds[1]);
164
165 if (earliestDate.equals(latestDate)) {
166 DateFormat tf = DateUtils.getTimeFormat(DateFormat.SHORT);
167 ts += earliestDate + ' ';
168 ts += tf.format(bounds[0]) + " - " + tf.format(bounds[1]);
169 } else {
170 DateFormat dtf = DateUtils.getDateTimeFormat(DateFormat.SHORT, DateFormat.MEDIUM);
171 ts += dtf.format(bounds[0]) + " - " + dtf.format(bounds[1]);
172 }
173
174 int diff = (int) (bounds[1].getTime() - bounds[0].getTime()) / 1000;
175 ts += String.format(" (%d:%02d)", diff / 3600, (diff % 3600) / 60);
176 }
177 return ts;
178 }
179
180 @Override
181 public Icon getIcon() {
182 return ImageProvider.get("layer", "gpx_small");
183 }
184
185 @Override
186 public Object getInfoComponent() {
187 StringBuilder info = new StringBuilder(128)
188 .append("<html><head><style>td { padding: 4px 16px; }</style></head><body>");
189
190 if (data.attr.containsKey("name")) {
191 info.append(tr("Name: {0}", data.get(GpxConstants.META_NAME))).append("<br>");
192 }
193
194 if (data.attr.containsKey("desc")) {
195 info.append(tr("Description: {0}", data.get(GpxConstants.META_DESC))).append("<br>");
196 }
197
198 if (!Utils.isStripEmpty(data.creator)) {
199 info.append(tr("Creator: {0}", data.creator)).append("<br>");
200 }
201
202 if (!data.getTracks().isEmpty()) {
203 info.append("<table><thead align='center'><tr><td colspan='5'>")
204 .append(trn("{0} track, {1} track segments", "{0} tracks, {1} track segments",
205 data.getTrackCount(), data.getTrackCount(),
206 data.getTrackSegsCount(), data.getTrackSegsCount()))
207 .append("</td></tr><tr align='center'><td>").append(tr("Name"))
208 .append("</td><td>").append(tr("Description"))
209 .append("</td><td>").append(tr("Timespan"))
210 .append("</td><td>").append(tr("Length"))
211 .append("</td><td>").append(tr("Number of<br/>Segments"))
212 .append("</td><td>").append(tr("URL"))
213 .append("</td></tr></thead>");
214
215 for (IGpxTrack trk : data.getTracks()) {
216 info.append("<tr><td>");
217 info.append(trk.getAttributes().getOrDefault(GpxConstants.GPX_NAME, ""));
218 info.append("</td><td>");
219 info.append(trk.getAttributes().getOrDefault(GpxConstants.GPX_DESC, ""));
220 info.append("</td><td>");
221 info.append(getTimespanForTrack(trk));
222 info.append("</td><td>");
223 info.append(SystemOfMeasurement.getSystemOfMeasurement().getDistText(trk.length()));
224 info.append("</td><td>");
225 info.append(trk.getSegments().size());
226 info.append("</td><td>");
227 if (trk.getAttributes().containsKey("url")) {
228 info.append(trk.get("url"));
229 }
230 info.append("</td></tr>");
231 }
232 info.append("</table><br><br>");
233 }
234
235 info.append(tr("Length: {0}", SystemOfMeasurement.getSystemOfMeasurement().getDistText(data.length()))).append("<br>")
236 .append(trn("{0} route, ", "{0} routes, ", data.getRoutes().size(), data.getRoutes().size()))
237 .append(trn("{0} waypoint", "{0} waypoints", data.getWaypoints().size(), data.getWaypoints().size()))
238 .append("<br></body></html>");
239
240 final JScrollPane sp = new JScrollPane(new HtmlPanel(info.toString()));
241 sp.setPreferredSize(new Dimension(sp.getPreferredSize().width+20, 370));
242 SwingUtilities.invokeLater(() -> sp.getVerticalScrollBar().setValue(0));
243 return sp;
244 }
245
246 @Override
247 public boolean isInfoResizable() {
248 return true;
249 }
250
251 @Override
252 public Action[] getMenuEntries() {
253 JumpToNextMarker jumpToNext = new JumpToNextMarker(this);
254 jumpToNext.putValue(Action.NAME, tr("Jump to next segment"));
255 JumpToPreviousMarker jumpToPrevious = new JumpToPreviousMarker(this);
256 jumpToPrevious.putValue(Action.NAME, tr("Jump to previous segment"));
257 List<Action> entries = new ArrayList<>(Arrays.asList(
258 LayerListDialog.getInstance().createShowHideLayerAction(),
259 LayerListDialog.getInstance().createDeleteLayerAction(),
260 LayerListDialog.getInstance().createMergeLayerAction(this),
261 SeparatorLayerAction.INSTANCE,
262 new LayerSaveAction(this),
263 new LayerSaveAsAction(this),
264 new CustomizeColor(this),
265 new CustomizeDrawingAction(this),
266 new ImportImagesAction(this),
267 new ImportAudioAction(this),
268 new MarkersFromNamedPointsAction(this),
269 jumpToNext,
270 jumpToPrevious,
271 new ConvertFromGpxLayerAction(this),
272 new DownloadAlongTrackAction(Collections.singleton(data)),
273 new DownloadWmsAlongTrackAction(data),
274 SeparatorLayerAction.INSTANCE,
275 new ChooseTrackVisibilityAction(this),
276 new RenameLayerAction(getAssociatedFile(), this)));
277
278 List<Action> expert = Arrays.asList(
279 new CombineTracksToSegmentedTrackAction(this),
280 new SplitTrackSegmentsToTracksAction(this),
281 new SplitTracksToLayersAction(this));
282
283 if (isExpertMode && expert.stream().anyMatch(Action::isEnabled)) {
284 entries.add(SeparatorLayerAction.INSTANCE);
285 expert.stream().filter(Action::isEnabled).forEach(entries::add);
286 }
287
288 entries.add(SeparatorLayerAction.INSTANCE);
289 entries.add(new LayerListPopup.InfoAction(this));
290 return entries.toArray(new Action[0]);
291 }
292
293 /**
294 * Determines if data is attached to a local file.
295 * @return {@code true} if data is attached to a local file, {@code false} otherwise
296 */
297 public boolean isLocalFile() {
298 return isLocalFile;
299 }
300
301 @Override
302 public String getToolTipText() {
303 StringBuilder info = new StringBuilder(48).append("<html>");
304
305 if (data.attr.containsKey(GpxConstants.META_NAME)) {
306 info.append(tr("Name: {0}", data.get(GpxConstants.META_NAME))).append("<br>");
307 }
308
309 if (data.attr.containsKey(GpxConstants.META_DESC)) {
310 info.append(tr("Description: {0}", data.get(GpxConstants.META_DESC))).append("<br>");
311 }
312
313 info.append(trn("{0} track", "{0} tracks", data.getTrackCount(), data.getTrackCount()))
314 .append(trn(" ({0} segment)", " ({0} segments)", data.getTrackSegsCount(), data.getTrackSegsCount()))
315 .append(", ")
316 .append(trn("{0} route, ", "{0} routes, ", data.getRoutes().size(), data.getRoutes().size()))
317 .append(trn("{0} waypoint", "{0} waypoints", data.getWaypoints().size(), data.getWaypoints().size())).append("<br>")
318 .append(tr("Length: {0}", SystemOfMeasurement.getSystemOfMeasurement().getDistText(data.length())));
319
320 if (Logging.isDebugEnabled() && !data.getLayerPrefs().isEmpty()) {
321 info.append("<br><br>")
322 .append(data.getLayerPrefs().entrySet().stream()
323 .map(e -> e.getKey() + "=" + e.getValue())
324 .collect(Collectors.joining("<br>")));
325 }
326
327 info.append("<br></html>");
328
329 return info.toString();
330 }
331
332 @Override
333 public boolean isMergable(Layer other) {
334 return other instanceof GpxLayer;
335 }
336
337 /**
338 * Shows/hides all tracks of a given date range by setting them to visible/invisible.
339 * @param fromDate The min date
340 * @param toDate The max date
341 * @param showWithoutDate Include tracks that don't have any date set..
342 */
343 public void filterTracksByDate(Date fromDate, Date toDate, boolean showWithoutDate) {
344 int i = 0;
345 long from = fromDate.getTime();
346 long to = toDate.getTime();
347 for (IGpxTrack trk : data.getTracks()) {
348 Date[] t = GpxData.getMinMaxTimeForTrack(trk);
349
350 if (t == null) continue;
351 long tm = t[1].getTime();
352 trackVisibility[i] = (tm == 0 && showWithoutDate) || (from <= tm && tm <= to);
353 i++;
354 }
355 invalidate();
356 }
357
358 @Override
359 public void mergeFrom(Layer from) {
360 if (!(from instanceof GpxLayer))
361 throw new IllegalArgumentException("not a GpxLayer: " + from);
362 mergeFrom((GpxLayer) from, false, false);
363 }
364
365 /**
366 * Merges the given GpxLayer into this layer and can remove timewise overlapping parts of the given track
367 * @param from The GpxLayer that gets merged into this one
368 * @param cutOverlapping whether overlapping parts of the given track should be removed
369 * @param connect whether the tracks should be connected on cuts
370 * @since 14338
371 */
372 public void mergeFrom(GpxLayer from, boolean cutOverlapping, boolean connect) {
373 data.mergeFrom(from.data, cutOverlapping, connect);
374 invalidate();
375 }
376
377 @Override
378 public void visitBoundingBox(BoundingXYVisitor v) {
379 v.visit(data.recalculateBounds());
380 }
381
382 @Override
383 public File getAssociatedFile() {
384 return data.storageFile;
385 }
386
387 @Override
388 public void setAssociatedFile(File file) {
389 data.storageFile = file;
390 }
391
392 /**
393 * Returns the linked MarkerLayer.
394 * @return the linked MarkerLayer (imported from the same file)
395 * @since 15496
396 */
397 public MarkerLayer getLinkedMarkerLayer() {
398 return linkedMarkerLayer;
399 }
400
401 /**
402 * Sets the linked MarkerLayer.
403 * @param linkedMarkerLayer the linked MarkerLayer
404 * @since 15496
405 */
406 public void setLinkedMarkerLayer(MarkerLayer linkedMarkerLayer) {
407 this.linkedMarkerLayer = linkedMarkerLayer;
408 }
409
410 @Override
411 public void projectionChanged(Projection oldValue, Projection newValue) {
412 if (newValue == null) return;
413 data.resetEastNorthCache();
414 }
415
416 @Override
417 public boolean isSavable() {
418 return true; // With GpxExporter
419 }
420
421 @Override
422 public boolean checkSaveConditions() {
423 return data != null;
424 }
425
426 @Override
427 public File createAndOpenSaveFileChooser() {
428 return SaveActionBase.createAndOpenSaveFileChooser(tr("Save GPX file"), GpxImporter.getFileFilter());
429 }
430
431 @Override
432 public LayerPositionStrategy getDefaultLayerPosition() {
433 return LayerPositionStrategy.AFTER_LAST_DATA_LAYER;
434 }
435
436 @Override
437 public void paint(Graphics2D g, MapView mv, Bounds bbox) {
438 // unused - we use a painter so this is not called.
439 }
440
441 @Override
442 protected LayerPainter createMapViewPainter(MapViewEvent event) {
443 return new GpxDrawHelper(this);
444 }
445
446 /**
447 * Action to merge tracks into a single segmented track
448 *
449 * @since 13210
450 */
451 public static class CombineTracksToSegmentedTrackAction extends AbstractAction {
452 private final transient GpxLayer layer;
453
454 /**
455 * Create a new CombineTracksToSegmentedTrackAction
456 * @param layer The layer with the data to work on.
457 */
458 public CombineTracksToSegmentedTrackAction(GpxLayer layer) {
459 // FIXME: icon missing, create a new icon for this action
460 //new ImageProvider(..."gpx_tracks_to_segmented_track").getResource().attachImageIcon(this, true);
461 putValue(SHORT_DESCRIPTION, tr("Collect segments of all tracks and combine in a single track."));
462 putValue(NAME, tr("Combine tracks of this layer"));
463 this.layer = layer;
464 }
465
466 @Override
467 public void actionPerformed(ActionEvent e) {
468 layer.data.combineTracksToSegmentedTrack();
469 layer.invalidate();
470 }
471
472 @Override
473 public boolean isEnabled() {
474 return layer.data.getTrackCount() > 1;
475 }
476 }
477
478 /**
479 * Action to split track segments into a multiple tracks with one segment each
480 *
481 * @since 13210
482 */
483 public static class SplitTrackSegmentsToTracksAction extends AbstractAction {
484 private final transient GpxLayer layer;
485
486 /**
487 * Create a new SplitTrackSegmentsToTracksAction
488 * @param layer The layer with the data to work on.
489 */
490 public SplitTrackSegmentsToTracksAction(GpxLayer layer) {
491 // FIXME: icon missing, create a new icon for this action
492 //new ImageProvider(..."gpx_segmented_track_to_tracks").getResource().attachImageIcon(this, true);
493 putValue(SHORT_DESCRIPTION, tr("Split multiple track segments of one track into multiple tracks."));
494 putValue(NAME, tr("Split track segments to tracks"));
495 this.layer = layer;
496 }
497
498 @Override
499 public void actionPerformed(ActionEvent e) {
500 layer.data.splitTrackSegmentsToTracks(!layer.getName().isEmpty() ? layer.getName() : "GPX split result");
501 layer.invalidate();
502 }
503
504 @Override
505 public boolean isEnabled() {
506 return layer.data.getTrackSegsCount() > layer.data.getTrackCount();
507 }
508 }
509
510 /**
511 * Action to split tracks of one gpx layer into multiple gpx layers,
512 * the result is one GPX track per gpx layer.
513 *
514 * @since 13210
515 */
516 public static class SplitTracksToLayersAction extends AbstractAction {
517 private final transient GpxLayer layer;
518
519 /**
520 * Create a new SplitTrackSegmentsToTracksAction
521 * @param layer The layer with the data to work on.
522 */
523 public SplitTracksToLayersAction(GpxLayer layer) {
524 // FIXME: icon missing, create a new icon for this action
525 //new ImageProvider(..."gpx_split_tracks_to_layers").getResource().attachImageIcon(this, true);
526 putValue(SHORT_DESCRIPTION, tr("Split the tracks of this layer to one new layer each."));
527 putValue(NAME, tr("Split tracks to new layers"));
528 this.layer = layer;
529 }
530
531 @Override
532 public void actionPerformed(ActionEvent e) {
533 layer.data.splitTracksToLayers(!layer.getName().isEmpty() ? layer.getName() : "GPX split result");
534 // layer is not modified by this action
535 }
536
537 @Override
538 public boolean isEnabled() {
539 return layer.data.getTrackCount() > 1;
540 }
541 }
542
543 @Override
544 public void expertChanged(boolean isExpert) {
545 this.isExpertMode = isExpert;
546 }
547
548 @Override
549 public boolean isModified() {
550 return data.isModified();
551 }
552
553 @Override
554 public boolean requiresSaveToFile() {
555 return isModified() && isLocalFile();
556 }
557
558 @Override
559 public void onPostSaveToFile() {
560 isLocalFile = true;
561 data.invalidate();
562 data.setModified(false);
563 }
564
565 @Override
566 public String getChangesetSourceTag() {
567 // no i18n for international values
568 return isLocalFile ? "survey" : null;
569 }
570
571 @Override
572 public Data getData() {
573 return data;
574 }
575
576 /**
577 * Jump (move the viewport) to the next track segment.
578 */
579 @Override
580 public void jumpToNextMarker() {
581 List<IGpxTrackSegment> segments = data.getTrackSegmentsStream().collect(Collectors.toList());
582 jumpToNext(segments);
583 }
584
585 /**
586 * Jump (move the viewport) to the previous track segment.
587 */
588 @Override
589 public void jumpToPreviousMarker() {
590 List<IGpxTrackSegment> segments = data.getTrackSegmentsStream().collect(Collectors.toList());
591 Collections.reverse(segments);
592 jumpToNext(segments);
593 }
594
595 private void jumpToNext(List<IGpxTrackSegment> segments) {
596 if (segments.isEmpty()) {
597 return;
598 } else if (currentSegment == null) {
599 currentSegment = segments.get(0);
600 MainApplication.getMap().mapView.zoomTo(currentSegment.getBounds());
601 } else {
602 try {
603 int index = segments.indexOf(currentSegment);
604 currentSegment = segments.listIterator(index + 1).next();
605 MainApplication.getMap().mapView.zoomTo(currentSegment.getBounds());
606 } catch (IndexOutOfBoundsException | NoSuchElementException ignore) {
607 Logging.trace(ignore);
608 }
609 }
610 }
611}
Note: See TracBrowser for help on using the repository browser.