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

Last change on this file since 5684 was 5684, checked in by bastiK, 11 years ago

add session support for marker layers (see #4029)

The data is exported to a separate GPX file that contains one waypoint for each marker.
This is not very elegant, because most of the time, all the info is already contained in the original GPX File.
However, when dealing with audio markers, they can be synchronized, or additional markers are added
at certain playback positions. This info must be retained.
Another complication is, that two or more MarkerLayers can be merged to one.

All these problems are avoided by explicitly exporting the markers to a separate file (as done in this commit).

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