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

Last change on this file since 8804 was 8804, checked in by simon04, 9 years ago

see #6968 - Allow converting gpx marker layer to osm data layer

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