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

Last change on this file since 12105 was 12105, checked in by michael2402, 7 years ago

Audio marker layer: Use invalidate instead of Main.map.mapView.repaint().

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