1 | // License: GPL. For details, see LICENSE file.
|
---|
2 | package org.openstreetmap.josm.gui.layer;
|
---|
3 |
|
---|
4 | import static org.openstreetmap.josm.tools.I18n.tr;
|
---|
5 | import static org.openstreetmap.josm.tools.I18n.trn;
|
---|
6 |
|
---|
7 | import java.awt.Color;
|
---|
8 | import java.awt.Dimension;
|
---|
9 | import java.awt.Graphics2D;
|
---|
10 | import java.awt.event.ActionEvent;
|
---|
11 | import java.io.File;
|
---|
12 | import java.text.DateFormat;
|
---|
13 | import java.util.ArrayList;
|
---|
14 | import java.util.Arrays;
|
---|
15 | import java.util.Collections;
|
---|
16 | import java.util.Date;
|
---|
17 | import java.util.List;
|
---|
18 | import java.util.NoSuchElementException;
|
---|
19 | import java.util.stream.Collectors;
|
---|
20 |
|
---|
21 | import javax.swing.AbstractAction;
|
---|
22 | import javax.swing.Action;
|
---|
23 | import javax.swing.Icon;
|
---|
24 | import javax.swing.JScrollPane;
|
---|
25 | import javax.swing.SwingUtilities;
|
---|
26 |
|
---|
27 | import org.openstreetmap.josm.actions.ExpertToggleAction;
|
---|
28 | import org.openstreetmap.josm.actions.ExpertToggleAction.ExpertModeChangeListener;
|
---|
29 | import org.openstreetmap.josm.actions.RenameLayerAction;
|
---|
30 | import org.openstreetmap.josm.actions.SaveActionBase;
|
---|
31 | import org.openstreetmap.josm.data.Bounds;
|
---|
32 | import org.openstreetmap.josm.data.Data;
|
---|
33 | import org.openstreetmap.josm.data.SystemOfMeasurement;
|
---|
34 | import org.openstreetmap.josm.data.gpx.GpxConstants;
|
---|
35 | import org.openstreetmap.josm.data.gpx.GpxData;
|
---|
36 | import org.openstreetmap.josm.data.gpx.GpxData.GpxDataChangeListener;
|
---|
37 | import org.openstreetmap.josm.data.gpx.IGpxTrack;
|
---|
38 | import org.openstreetmap.josm.data.gpx.IGpxTrackSegment;
|
---|
39 | import org.openstreetmap.josm.data.osm.visitor.BoundingXYVisitor;
|
---|
40 | import org.openstreetmap.josm.data.projection.Projection;
|
---|
41 | import org.openstreetmap.josm.gui.MainApplication;
|
---|
42 | import org.openstreetmap.josm.gui.MapView;
|
---|
43 | import org.openstreetmap.josm.gui.dialogs.LayerListDialog;
|
---|
44 | import org.openstreetmap.josm.gui.dialogs.LayerListPopup;
|
---|
45 | import org.openstreetmap.josm.gui.io.importexport.GpxImporter;
|
---|
46 | import org.openstreetmap.josm.gui.layer.JumpToMarkerActions.JumpToMarkerLayer;
|
---|
47 | import org.openstreetmap.josm.gui.layer.JumpToMarkerActions.JumpToNextMarker;
|
---|
48 | import org.openstreetmap.josm.gui.layer.JumpToMarkerActions.JumpToPreviousMarker;
|
---|
49 | import org.openstreetmap.josm.gui.layer.gpx.ChooseTrackVisibilityAction;
|
---|
50 | import org.openstreetmap.josm.gui.layer.gpx.ConvertFromGpxLayerAction;
|
---|
51 | import org.openstreetmap.josm.gui.layer.gpx.CustomizeDrawingAction;
|
---|
52 | import org.openstreetmap.josm.gui.layer.gpx.DownloadAlongTrackAction;
|
---|
53 | import org.openstreetmap.josm.gui.layer.gpx.DownloadWmsAlongTrackAction;
|
---|
54 | import org.openstreetmap.josm.gui.layer.gpx.GpxDrawHelper;
|
---|
55 | import org.openstreetmap.josm.gui.layer.gpx.ImportAudioAction;
|
---|
56 | import org.openstreetmap.josm.gui.layer.gpx.ImportImagesAction;
|
---|
57 | import org.openstreetmap.josm.gui.layer.gpx.MarkersFromNamedPointsAction;
|
---|
58 | import org.openstreetmap.josm.gui.layer.markerlayer.MarkerLayer;
|
---|
59 | import org.openstreetmap.josm.gui.preferences.display.GPXSettingsPanel;
|
---|
60 | import org.openstreetmap.josm.gui.widgets.HtmlPanel;
|
---|
61 | import org.openstreetmap.josm.tools.ImageProvider;
|
---|
62 | import org.openstreetmap.josm.tools.Logging;
|
---|
63 | import org.openstreetmap.josm.tools.Utils;
|
---|
64 | import org.openstreetmap.josm.tools.date.DateUtils;
|
---|
65 |
|
---|
66 | /**
|
---|
67 | * A layer that displays data from a Gpx file / the OSM gpx downloads.
|
---|
68 | */
|
---|
69 | public 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 = null;
|
---|
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(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 SplitTrackSegementsToTracksAction(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 SplitTrackSegementsToTracksAction extends AbstractAction {
|
---|
484 | private final transient GpxLayer layer;
|
---|
485 |
|
---|
486 | /**
|
---|
487 | * Create a new SplitTrackSegementsToTracksAction
|
---|
488 | * @param layer The layer with the data to work on.
|
---|
489 | */
|
---|
490 | public SplitTrackSegementsToTracksAction(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 SplitTrackSegementsToTracksAction
|
---|
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 | }
|
---|