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

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

GpxLayer: use Map.getOrDefault (Java 8)

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