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

Last change on this file since 15280 was 14338, checked in by Don-vip, 6 years ago

fix #16755 - Cut overlapping GPX layers when merging (patch by Bjoeni, modified)

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