source: josm/trunk/src/org/openstreetmap/josm/gui/layer/markerlayer/MarkerLayer.java@ 8354

Last change on this file since 8354 was 8354, checked in by Don-vip, 9 years ago

fix #11432 - robustness against invalid GPX files

  • Property svn:eol-style set to native
File size: 20.2 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.gui.layer.markerlayer;
3
4import static org.openstreetmap.josm.gui.help.HelpUtil.ht;
5import static org.openstreetmap.josm.tools.I18n.marktr;
6import static org.openstreetmap.josm.tools.I18n.tr;
7import static org.openstreetmap.josm.tools.I18n.trn;
8
9import java.awt.Color;
10import java.awt.Component;
11import java.awt.Graphics2D;
12import java.awt.Point;
13import java.awt.event.ActionEvent;
14import java.awt.event.MouseAdapter;
15import java.awt.event.MouseEvent;
16import java.io.File;
17import java.net.URI;
18import java.net.URISyntaxException;
19import java.util.ArrayList;
20import java.util.Collection;
21import java.util.Collections;
22import java.util.Comparator;
23import java.util.List;
24
25import javax.swing.AbstractAction;
26import javax.swing.Action;
27import javax.swing.Icon;
28import javax.swing.JCheckBoxMenuItem;
29import javax.swing.JOptionPane;
30
31import org.openstreetmap.josm.Main;
32import org.openstreetmap.josm.actions.RenameLayerAction;
33import org.openstreetmap.josm.data.Bounds;
34import org.openstreetmap.josm.data.coor.LatLon;
35import org.openstreetmap.josm.data.gpx.Extensions;
36import org.openstreetmap.josm.data.gpx.GpxConstants;
37import org.openstreetmap.josm.data.gpx.GpxData;
38import org.openstreetmap.josm.data.gpx.GpxLink;
39import org.openstreetmap.josm.data.gpx.WayPoint;
40import org.openstreetmap.josm.data.osm.visitor.BoundingXYVisitor;
41import org.openstreetmap.josm.gui.MapView;
42import org.openstreetmap.josm.gui.dialogs.LayerListDialog;
43import org.openstreetmap.josm.gui.dialogs.LayerListPopup;
44import org.openstreetmap.josm.gui.layer.CustomizeColor;
45import org.openstreetmap.josm.gui.layer.GpxLayer;
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.Layer;
50import org.openstreetmap.josm.tools.AudioPlayer;
51import org.openstreetmap.josm.tools.ImageProvider;
52
53/**
54 * A layer holding markers.
55 *
56 * Markers are GPS points with a name and, optionally, a symbol code attached;
57 * marker layers can be created from waypoints when importing raw GPS data,
58 * but they may also come from other sources.
59 *
60 * The symbol code is for future use.
61 *
62 * The data is read only.
63 */
64public class MarkerLayer extends Layer implements JumpToMarkerLayer {
65
66 /**
67 * A list of markers.
68 */
69 public final List<Marker> data;
70 private boolean mousePressed = false;
71 public GpxLayer fromLayer = null;
72 private Marker currentMarker;
73 public AudioMarker syncAudioMarker = null;
74
75 /**
76 * Constructs a new {@code MarkerLayer}.
77 * @param indata The GPX data for this layer
78 * @param name The marker layer name
79 * @param associatedFile The associated GPX file
80 * @param fromLayer The associated GPX layer
81 */
82 public MarkerLayer(GpxData indata, String name, File associatedFile, GpxLayer fromLayer) {
83 super(name);
84 this.setAssociatedFile(associatedFile);
85 this.data = new ArrayList<>();
86 this.fromLayer = fromLayer;
87 double firstTime = -1.0;
88 String lastLinkedFile = "";
89
90 for (WayPoint wpt : indata.waypoints) {
91 /* calculate time differences in waypoints */
92 double time = wpt.time;
93 boolean wpt_has_link = wpt.attr.containsKey(GpxConstants.META_LINKS);
94 if (firstTime < 0 && wpt_has_link) {
95 firstTime = time;
96 for (GpxLink oneLink : wpt.<GpxLink>getCollection(GpxConstants.META_LINKS)) {
97 lastLinkedFile = oneLink.uri;
98 break;
99 }
100 }
101 if (wpt_has_link) {
102 for (GpxLink oneLink : wpt.<GpxLink>getCollection(GpxConstants.META_LINKS)) {
103 String uri = oneLink.uri;
104 if (uri != null) {
105 if (!uri.equals(lastLinkedFile)) {
106 firstTime = time;
107 }
108 lastLinkedFile = uri;
109 break;
110 }
111 }
112 }
113 Double offset = null;
114 // If we have an explicit offset, take it.
115 // Otherwise, for a group of markers with the same Link-URI (e.g. an
116 // audio file) calculate the offset relative to the first marker of
117 // that group. This way the user can jump to the corresponding
118 // playback positions in a long audio track.
119 Extensions exts = (Extensions) wpt.get(GpxConstants.META_EXTENSIONS);
120 if (exts != null && exts.containsKey("offset")) {
121 try {
122 offset = Double.parseDouble(exts.get("offset"));
123 } catch (NumberFormatException nfe) {
124 Main.warn(nfe);
125 }
126 }
127 if (offset == null) {
128 offset = time - firstTime;
129 }
130 Marker m = Marker.createMarker(wpt, indata.storageFile, this, time, offset);
131 if (m != null) {
132 data.add(m);
133 }
134 }
135 }
136
137 @Override
138 public void hookUpMapView() {
139 Main.map.mapView.addMouseListener(new MouseAdapter() {
140 @Override public void mousePressed(MouseEvent e) {
141 if (e.getButton() != MouseEvent.BUTTON1)
142 return;
143 boolean mousePressedInButton = false;
144 if (e.getPoint() != null) {
145 for (Marker mkr : data) {
146 if (mkr.containsPoint(e.getPoint())) {
147 mousePressedInButton = true;
148 break;
149 }
150 }
151 }
152 if (! mousePressedInButton)
153 return;
154 mousePressed = true;
155 if (isVisible()) {
156 Main.map.mapView.repaint();
157 }
158 }
159 @Override public void mouseReleased(MouseEvent ev) {
160 if (ev.getButton() != MouseEvent.BUTTON1 || ! mousePressed)
161 return;
162 mousePressed = false;
163 if (!isVisible())
164 return;
165 if (ev.getPoint() != null) {
166 for (Marker mkr : data) {
167 if (mkr.containsPoint(ev.getPoint())) {
168 mkr.actionPerformed(new ActionEvent(this, 0, null));
169 }
170 }
171 }
172 Main.map.mapView.repaint();
173 }
174 });
175 }
176
177 /**
178 * Return a static icon.
179 */
180 @Override
181 public Icon getIcon() {
182 return ImageProvider.get("layer", "marker_small");
183 }
184
185 @Override
186 public Color getColor(boolean ignoreCustom) {
187 String name = getName();
188 return Main.pref.getColor(marktr("gps marker"), name != null ? "layer "+name : null, Color.gray);
189 }
190
191 /* for preferences */
192 public static Color getGenericColor() {
193 return Main.pref.getColor(marktr("gps marker"), Color.gray);
194 }
195
196 @Override
197 public void paint(Graphics2D g, MapView mv, Bounds box) {
198 boolean showTextOrIcon = isTextOrIconShown();
199 g.setColor(getColor(true));
200
201 if (mousePressed) {
202 boolean mousePressedTmp = mousePressed;
203 Point mousePos = mv.getMousePosition(); // Get mouse position only when necessary (it's the slowest part of marker layer painting)
204 for (Marker mkr : data) {
205 if (mousePos != null && mkr.containsPoint(mousePos)) {
206 mkr.paint(g, mv, mousePressedTmp, showTextOrIcon);
207 mousePressedTmp = false;
208 }
209 }
210 } else {
211 for (Marker mkr : data) {
212 mkr.paint(g, mv, false, showTextOrIcon);
213 }
214 }
215 }
216
217 @Override public String getToolTipText() {
218 return data.size()+" "+trn("marker", "markers", data.size());
219 }
220
221 @Override public void mergeFrom(Layer from) {
222 MarkerLayer layer = (MarkerLayer)from;
223 data.addAll(layer.data);
224 Collections.sort(data, new Comparator<Marker>() {
225 @Override
226 public int compare(Marker o1, Marker o2) {
227 return Double.compare(o1.time, o2.time);
228 }
229 });
230 }
231
232 @Override public boolean isMergable(Layer other) {
233 return other instanceof MarkerLayer;
234 }
235
236 @Override public void visitBoundingBox(BoundingXYVisitor v) {
237 for (Marker mkr : data) {
238 v.visit(mkr.getEastNorth());
239 }
240 }
241
242 @Override public Object getInfoComponent() {
243 return "<html>"+trn("{0} consists of {1} marker", "{0} consists of {1} markers", data.size(), getName(), data.size()) + "</html>";
244 }
245
246 @Override public Action[] getMenuEntries() {
247 Collection<Action> components = new ArrayList<>();
248 components.add(LayerListDialog.getInstance().createShowHideLayerAction());
249 components.add(new ShowHideMarkerText(this));
250 components.add(LayerListDialog.getInstance().createDeleteLayerAction());
251 components.add(SeparatorLayerAction.INSTANCE);
252 components.add(new CustomizeColor(this));
253 components.add(SeparatorLayerAction.INSTANCE);
254 components.add(new SynchronizeAudio());
255 if (Main.pref.getBoolean("marker.traceaudio", true)) {
256 components.add (new MoveAudio());
257 }
258 components.add(new JumpToNextMarker(this));
259 components.add(new JumpToPreviousMarker(this));
260 components.add(new RenameLayerAction(getAssociatedFile(), this));
261 components.add(SeparatorLayerAction.INSTANCE);
262 components.add(new LayerListPopup.InfoAction(this));
263 return components.toArray(new Action[components.size()]);
264 }
265
266 public boolean synchronizeAudioMarkers(final AudioMarker startMarker) {
267 syncAudioMarker = startMarker;
268 if (syncAudioMarker != null && ! data.contains(syncAudioMarker)) {
269 syncAudioMarker = null;
270 }
271 if (syncAudioMarker == null) {
272 // find the first audioMarker in this layer
273 for (Marker m : data) {
274 if (m instanceof AudioMarker) {
275 syncAudioMarker = (AudioMarker) m;
276 break;
277 }
278 }
279 }
280 if (syncAudioMarker == null)
281 return false;
282
283 // apply adjustment to all subsequent audio markers in the layer
284 double adjustment = AudioPlayer.position() - syncAudioMarker.offset; // in seconds
285 boolean seenStart = false;
286 try {
287 URI uri = syncAudioMarker.url().toURI();
288 for (Marker m : data) {
289 if (m == syncAudioMarker) {
290 seenStart = true;
291 }
292 if (seenStart && m instanceof AudioMarker) {
293 AudioMarker ma = (AudioMarker) m;
294 // Do not ever call URL.equals but use URI.equals instead to avoid Internet connection
295 // See http://michaelscharf.blogspot.fr/2006/11/javaneturlequals-and-hashcode-make.html for details
296 if (ma.url().toURI().equals(uri)) {
297 ma.adjustOffset(adjustment);
298 }
299 }
300 }
301 } catch (URISyntaxException e) {
302 Main.warn(e);
303 }
304 return true;
305 }
306
307 public AudioMarker addAudioMarker(double time, LatLon coor) {
308 // find first audio marker to get absolute start time
309 double offset = 0.0;
310 AudioMarker am = null;
311 for (Marker m : data) {
312 if (m.getClass() == AudioMarker.class) {
313 am = (AudioMarker)m;
314 offset = time - am.time;
315 break;
316 }
317 }
318 if (am == null) {
319 JOptionPane.showMessageDialog(
320 Main.parent,
321 tr("No existing audio markers in this layer to offset from."),
322 tr("Error"),
323 JOptionPane.ERROR_MESSAGE
324 );
325 return null;
326 }
327
328 // make our new marker
329 AudioMarker newAudioMarker = new AudioMarker(coor,
330 null, AudioPlayer.url(), this, time, offset);
331
332 // insert it at the right place in a copy the collection
333 Collection<Marker> newData = new ArrayList<>();
334 am = null;
335 AudioMarker ret = newAudioMarker; // save to have return value
336 for (Marker m : data) {
337 if (m.getClass() == AudioMarker.class) {
338 am = (AudioMarker) m;
339 if (newAudioMarker != null && offset < am.offset) {
340 newAudioMarker.adjustOffset(am.syncOffset()); // i.e. same as predecessor
341 newData.add(newAudioMarker);
342 newAudioMarker = null;
343 }
344 }
345 newData.add(m);
346 }
347
348 if (newAudioMarker != null) {
349 if (am != null) {
350 newAudioMarker.adjustOffset(am.syncOffset()); // i.e. same as predecessor
351 }
352 newData.add(newAudioMarker); // insert at end
353 }
354
355 // replace the collection
356 data.clear();
357 data.addAll(newData);
358 return ret;
359 }
360
361 @Override
362 public void jumpToNextMarker() {
363 if (currentMarker == null) {
364 currentMarker = data.get(0);
365 } else {
366 boolean foundCurrent = false;
367 for (Marker m: data) {
368 if (foundCurrent) {
369 currentMarker = m;
370 break;
371 } else if (currentMarker == m) {
372 foundCurrent = true;
373 }
374 }
375 }
376 Main.map.mapView.zoomTo(currentMarker.getEastNorth());
377 }
378
379 @Override
380 public void jumpToPreviousMarker() {
381 if (currentMarker == null) {
382 currentMarker = data.get(data.size() - 1);
383 } else {
384 boolean foundCurrent = false;
385 for (int i=data.size() - 1; i>=0; i--) {
386 Marker m = data.get(i);
387 if (foundCurrent) {
388 currentMarker = m;
389 break;
390 } else if (currentMarker == m) {
391 foundCurrent = true;
392 }
393 }
394 }
395 Main.map.mapView.zoomTo(currentMarker.getEastNorth());
396 }
397
398 public static void playAudio() {
399 playAdjacentMarker(null, true);
400 }
401
402 public static void playNextMarker() {
403 playAdjacentMarker(AudioMarker.recentlyPlayedMarker(), true);
404 }
405
406 public static void playPreviousMarker() {
407 playAdjacentMarker(AudioMarker.recentlyPlayedMarker(), false);
408 }
409
410 private static Marker getAdjacentMarker(Marker startMarker, boolean next, Layer layer) {
411 Marker previousMarker = null;
412 boolean nextTime = false;
413 if (layer.getClass() == MarkerLayer.class) {
414 MarkerLayer markerLayer = (MarkerLayer) layer;
415 for (Marker marker : markerLayer.data) {
416 if (marker == startMarker) {
417 if (next) {
418 nextTime = true;
419 } else {
420 if (previousMarker == null) {
421 previousMarker = startMarker; // if no previous one, play the first one again
422 }
423 return previousMarker;
424 }
425 } else if (marker.getClass() == AudioMarker.class) {
426 if(nextTime || startMarker == null)
427 return marker;
428 previousMarker = marker;
429 }
430 }
431 if (nextTime) // there was no next marker in that layer, so play the last one again
432 return startMarker;
433 }
434 return null;
435 }
436
437 private static void playAdjacentMarker(Marker startMarker, boolean next) {
438 Marker m = null;
439 if (!Main.isDisplayingMapView())
440 return;
441 Layer l = Main.map.mapView.getActiveLayer();
442 if(l != null) {
443 m = getAdjacentMarker(startMarker, next, l);
444 }
445 if(m == null)
446 {
447 for (Layer layer : Main.map.mapView.getAllLayers())
448 {
449 m = getAdjacentMarker(startMarker, next, layer);
450 if(m != null) {
451 break;
452 }
453 }
454 }
455 if(m != null) {
456 ((AudioMarker)m).play();
457 }
458 }
459
460 /**
461 * Get state of text display.
462 * @return <code>true</code> if text should be shown, <code>false</code> otherwise.
463 */
464 private boolean isTextOrIconShown() {
465 String current = Main.pref.get("marker.show "+getName(),"show");
466 return "show".equalsIgnoreCase(current);
467 }
468
469 public static final class ShowHideMarkerText extends AbstractAction implements LayerAction {
470 private final transient MarkerLayer layer;
471
472 public ShowHideMarkerText(MarkerLayer layer) {
473 super(tr("Show Text/Icons"), ImageProvider.get("dialogs", "showhide"));
474 putValue(SHORT_DESCRIPTION, tr("Toggle visible state of the marker text and icons."));
475 putValue("help", ht("/Action/ShowHideTextIcons"));
476 this.layer = layer;
477 }
478
479
480 @Override
481 public void actionPerformed(ActionEvent e) {
482 Main.pref.put("marker.show "+layer.getName(), layer.isTextOrIconShown() ? "hide" : "show");
483 Main.map.mapView.repaint();
484 }
485
486
487 @Override
488 public Component createMenuComponent() {
489 JCheckBoxMenuItem showMarkerTextItem = new JCheckBoxMenuItem(this);
490 showMarkerTextItem.setState(layer.isTextOrIconShown());
491 return showMarkerTextItem;
492 }
493
494 @Override
495 public boolean supportLayers(List<Layer> layers) {
496 return layers.size() == 1 && layers.get(0) instanceof MarkerLayer;
497 }
498 }
499
500
501 private class SynchronizeAudio extends AbstractAction {
502
503 public SynchronizeAudio() {
504 super(tr("Synchronize Audio"), ImageProvider.get("audio-sync"));
505 putValue("help", ht("/Action/SynchronizeAudio"));
506 }
507
508 @Override
509 public void actionPerformed(ActionEvent e) {
510 if (! AudioPlayer.paused()) {
511 JOptionPane.showMessageDialog(
512 Main.parent,
513 tr("You need to pause audio at the moment when you hear your synchronization cue."),
514 tr("Warning"),
515 JOptionPane.WARNING_MESSAGE
516 );
517 return;
518 }
519 AudioMarker recent = AudioMarker.recentlyPlayedMarker();
520 if (synchronizeAudioMarkers(recent)) {
521 JOptionPane.showMessageDialog(
522 Main.parent,
523 tr("Audio synchronized at point {0}.", syncAudioMarker.getText()),
524 tr("Information"),
525 JOptionPane.INFORMATION_MESSAGE
526 );
527 } else {
528 JOptionPane.showMessageDialog(
529 Main.parent,
530 tr("Unable to synchronize in layer being played."),
531 tr("Error"),
532 JOptionPane.ERROR_MESSAGE
533 );
534 }
535 }
536 }
537
538 private class MoveAudio extends AbstractAction {
539
540 public MoveAudio() {
541 super(tr("Make Audio Marker at Play Head"), ImageProvider.get("addmarkers"));
542 putValue("help", ht("/Action/MakeAudioMarkerAtPlayHead"));
543 }
544
545 @Override
546 public void actionPerformed(ActionEvent e) {
547 if (! AudioPlayer.paused()) {
548 JOptionPane.showMessageDialog(
549 Main.parent,
550 tr("You need to have paused audio at the point on the track where you want the marker."),
551 tr("Warning"),
552 JOptionPane.WARNING_MESSAGE
553 );
554 return;
555 }
556 PlayHeadMarker playHeadMarker = Main.map.mapView.playHeadMarker;
557 if (playHeadMarker == null)
558 return;
559 addAudioMarker(playHeadMarker.time, playHeadMarker.getCoor());
560 Main.map.mapView.repaint();
561 }
562 }
563
564}
Note: See TracBrowser for help on using the repository browser.