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

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

see #15182 - deprecate Main.map and Main.isDisplayingMapView(). Replacements: gui.MainApplication.getMap() / gui.MainApplication.isDisplayingMapView()

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