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

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

see #13309 - Caching and notifying preferences (patch by michael2402) - gsoc-core

  • 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.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;
72 public GpxLayer fromLayer;
73 private Marker currentMarker;
74 public AudioMarker syncAudioMarker;
75
76 private static final Color DEFAULT_COLOR = Color.magenta;
77 private static final ColorProperty COLOR_PROPERTY = new ColorProperty(marktr("gps marker"), DEFAULT_COLOR);
78
79 /**
80 * Constructs a new {@code MarkerLayer}.
81 * @param indata The GPX data for this layer
82 * @param name The marker layer name
83 * @param associatedFile The associated GPX file
84 * @param fromLayer The associated GPX layer
85 */
86 public MarkerLayer(GpxData indata, String name, File associatedFile, GpxLayer fromLayer) {
87 super(name);
88 this.setAssociatedFile(associatedFile);
89 this.data = new ArrayList<>();
90 this.fromLayer = fromLayer;
91 double firstTime = -1.0;
92 String lastLinkedFile = "";
93
94 for (WayPoint wpt : indata.waypoints) {
95 /* calculate time differences in waypoints */
96 double time = wpt.time;
97 boolean wptHasLink = wpt.attr.containsKey(GpxConstants.META_LINKS);
98 if (firstTime < 0 && wptHasLink) {
99 firstTime = time;
100 for (GpxLink oneLink : wpt.<GpxLink>getCollection(GpxConstants.META_LINKS)) {
101 lastLinkedFile = oneLink.uri;
102 break;
103 }
104 }
105 if (wptHasLink) {
106 for (GpxLink oneLink : wpt.<GpxLink>getCollection(GpxConstants.META_LINKS)) {
107 String uri = oneLink.uri;
108 if (uri != null) {
109 if (!uri.equals(lastLinkedFile)) {
110 firstTime = time;
111 }
112 lastLinkedFile = uri;
113 break;
114 }
115 }
116 }
117 Double offset = null;
118 // If we have an explicit offset, take it.
119 // Otherwise, for a group of markers with the same Link-URI (e.g. an
120 // audio file) calculate the offset relative to the first marker of
121 // that group. This way the user can jump to the corresponding
122 // playback positions in a long audio track.
123 Extensions exts = (Extensions) wpt.get(GpxConstants.META_EXTENSIONS);
124 if (exts != null && exts.containsKey("offset")) {
125 try {
126 offset = Double.valueOf(exts.get("offset"));
127 } catch (NumberFormatException nfe) {
128 Main.warn(nfe);
129 }
130 }
131 if (offset == null) {
132 offset = time - firstTime;
133 }
134 final Collection<Marker> markers = Marker.createMarkers(wpt, indata.storageFile, this, time, offset);
135 if (markers != null) {
136 data.addAll(markers);
137 }
138 }
139 }
140
141 @Override
142 public LayerPainter attachToMapView(MapViewEvent event) {
143 event.getMapView().addMouseListener(new MouseAdapter() {
144 @Override
145 public void mousePressed(MouseEvent e) {
146 if (e.getButton() != MouseEvent.BUTTON1)
147 return;
148 boolean mousePressedInButton = false;
149 if (e.getPoint() != null) {
150 for (Marker mkr : data) {
151 if (mkr.containsPoint(e.getPoint())) {
152 mousePressedInButton = true;
153 break;
154 }
155 }
156 }
157 if (!mousePressedInButton)
158 return;
159 mousePressed = true;
160 if (isVisible()) {
161 invalidate();
162 }
163 }
164
165 @Override
166 public void mouseReleased(MouseEvent ev) {
167 if (ev.getButton() != MouseEvent.BUTTON1 || !mousePressed)
168 return;
169 mousePressed = false;
170 if (!isVisible())
171 return;
172 if (ev.getPoint() != null) {
173 for (Marker mkr : data) {
174 if (mkr.containsPoint(ev.getPoint())) {
175 mkr.actionPerformed(new ActionEvent(this, 0, null));
176 }
177 }
178 }
179 invalidate();
180 }
181 });
182
183 if (event.getMapView().playHeadMarker == null) {
184 event.getMapView().playHeadMarker = PlayHeadMarker.create();
185 }
186
187 return super.attachToMapView(event);
188 }
189
190 /**
191 * Return a static icon.
192 */
193 @Override
194 public Icon getIcon() {
195 return ImageProvider.get("layer", "marker_small");
196 }
197
198 @Override
199 protected ColorProperty getBaseColorProperty() {
200 return COLOR_PROPERTY;
201 }
202
203 /* for preferences */
204 public static Color getGenericColor() {
205 return COLOR_PROPERTY.get();
206 }
207
208 @Override
209 public void paint(Graphics2D g, MapView mv, Bounds box) {
210 boolean showTextOrIcon = isTextOrIconShown();
211 g.setColor(getColorProperty().get());
212
213 if (mousePressed) {
214 boolean mousePressedTmp = mousePressed;
215 Point mousePos = mv.getMousePosition(); // Get mouse position only when necessary (it's the slowest part of marker layer painting)
216 for (Marker mkr : data) {
217 if (mousePos != null && mkr.containsPoint(mousePos)) {
218 mkr.paint(g, mv, mousePressedTmp, showTextOrIcon);
219 mousePressedTmp = false;
220 }
221 }
222 } else {
223 for (Marker mkr : data) {
224 mkr.paint(g, mv, false, showTextOrIcon);
225 }
226 }
227 }
228
229 @Override
230 public String getToolTipText() {
231 return Integer.toString(data.size())+' '+trn("marker", "markers", data.size());
232 }
233
234 @Override
235 public void mergeFrom(Layer from) {
236 if (from instanceof MarkerLayer) {
237 data.addAll(((MarkerLayer) from).data);
238 data.sort(Comparator.comparingDouble(o -> o.time));
239 }
240 }
241
242 @Override public boolean isMergable(Layer other) {
243 return other instanceof MarkerLayer;
244 }
245
246 @Override public void visitBoundingBox(BoundingXYVisitor v) {
247 for (Marker mkr : data) {
248 v.visit(mkr.getEastNorth());
249 }
250 }
251
252 @Override public Object getInfoComponent() {
253 return "<html>"+trn("{0} consists of {1} marker", "{0} consists of {1} markers", data.size(), getName(), data.size()) + "</html>";
254 }
255
256 @Override public Action[] getMenuEntries() {
257 Collection<Action> components = new ArrayList<>();
258 components.add(LayerListDialog.getInstance().createShowHideLayerAction());
259 components.add(new ShowHideMarkerText(this));
260 components.add(LayerListDialog.getInstance().createDeleteLayerAction());
261 components.add(LayerListDialog.getInstance().createMergeLayerAction(this));
262 components.add(SeparatorLayerAction.INSTANCE);
263 components.add(new CustomizeColor(this));
264 components.add(SeparatorLayerAction.INSTANCE);
265 components.add(new SynchronizeAudio());
266 if (Main.pref.getBoolean("marker.traceaudio", true)) {
267 components.add(new MoveAudio());
268 }
269 components.add(new JumpToNextMarker(this));
270 components.add(new JumpToPreviousMarker(this));
271 components.add(new ConvertToDataLayerAction.FromMarkerLayer(this));
272 components.add(new RenameLayerAction(getAssociatedFile(), this));
273 components.add(SeparatorLayerAction.INSTANCE);
274 components.add(new LayerListPopup.InfoAction(this));
275 return components.toArray(new Action[components.size()]);
276 }
277
278 public boolean synchronizeAudioMarkers(final AudioMarker startMarker) {
279 syncAudioMarker = startMarker;
280 if (syncAudioMarker != null && !data.contains(syncAudioMarker)) {
281 syncAudioMarker = null;
282 }
283 if (syncAudioMarker == null) {
284 // find the first audioMarker in this layer
285 for (Marker m : data) {
286 if (m instanceof AudioMarker) {
287 syncAudioMarker = (AudioMarker) m;
288 break;
289 }
290 }
291 }
292 if (syncAudioMarker == null)
293 return false;
294
295 // apply adjustment to all subsequent audio markers in the layer
296 double adjustment = AudioPlayer.position() - syncAudioMarker.offset; // in seconds
297 boolean seenStart = false;
298 try {
299 URI uri = syncAudioMarker.url().toURI();
300 for (Marker m : data) {
301 if (m == syncAudioMarker) {
302 seenStart = true;
303 }
304 if (seenStart && m instanceof AudioMarker) {
305 AudioMarker ma = (AudioMarker) m;
306 // Do not ever call URL.equals but use URI.equals instead to avoid Internet connection
307 // See http://michaelscharf.blogspot.fr/2006/11/javaneturlequals-and-hashcode-make.html for details
308 if (ma.url().toURI().equals(uri)) {
309 ma.adjustOffset(adjustment);
310 }
311 }
312 }
313 } catch (URISyntaxException e) {
314 Main.warn(e);
315 }
316 return true;
317 }
318
319 public AudioMarker addAudioMarker(double time, LatLon coor) {
320 // find first audio marker to get absolute start time
321 double offset = 0.0;
322 AudioMarker am = null;
323 for (Marker m : data) {
324 if (m.getClass() == AudioMarker.class) {
325 am = (AudioMarker) m;
326 offset = time - am.time;
327 break;
328 }
329 }
330 if (am == null) {
331 JOptionPane.showMessageDialog(
332 Main.parent,
333 tr("No existing audio markers in this layer to offset from."),
334 tr("Error"),
335 JOptionPane.ERROR_MESSAGE
336 );
337 return null;
338 }
339
340 // make our new marker
341 AudioMarker newAudioMarker = new AudioMarker(coor,
342 null, AudioPlayer.url(), this, time, offset);
343
344 // insert it at the right place in a copy the collection
345 Collection<Marker> newData = new ArrayList<>();
346 am = null;
347 AudioMarker ret = newAudioMarker; // save to have return value
348 for (Marker m : data) {
349 if (m.getClass() == AudioMarker.class) {
350 am = (AudioMarker) m;
351 if (newAudioMarker != null && offset < am.offset) {
352 newAudioMarker.adjustOffset(am.syncOffset()); // i.e. same as predecessor
353 newData.add(newAudioMarker);
354 newAudioMarker = null;
355 }
356 }
357 newData.add(m);
358 }
359
360 if (newAudioMarker != null) {
361 if (am != null) {
362 newAudioMarker.adjustOffset(am.syncOffset()); // i.e. same as predecessor
363 }
364 newData.add(newAudioMarker); // insert at end
365 }
366
367 // replace the collection
368 data.clear();
369 data.addAll(newData);
370 return ret;
371 }
372
373 @Override
374 public void jumpToNextMarker() {
375 if (currentMarker == null) {
376 currentMarker = data.get(0);
377 } else {
378 boolean foundCurrent = false;
379 for (Marker m: data) {
380 if (foundCurrent) {
381 currentMarker = m;
382 break;
383 } else if (currentMarker == m) {
384 foundCurrent = true;
385 }
386 }
387 }
388 Main.map.mapView.zoomTo(currentMarker.getEastNorth());
389 }
390
391 @Override
392 public void jumpToPreviousMarker() {
393 if (currentMarker == null) {
394 currentMarker = data.get(data.size() - 1);
395 } else {
396 boolean foundCurrent = false;
397 for (int i = data.size() - 1; i >= 0; i--) {
398 Marker m = data.get(i);
399 if (foundCurrent) {
400 currentMarker = m;
401 break;
402 } else if (currentMarker == m) {
403 foundCurrent = true;
404 }
405 }
406 }
407 Main.map.mapView.zoomTo(currentMarker.getEastNorth());
408 }
409
410 public static void playAudio() {
411 playAdjacentMarker(null, true);
412 }
413
414 public static void playNextMarker() {
415 playAdjacentMarker(AudioMarker.recentlyPlayedMarker(), true);
416 }
417
418 public static void playPreviousMarker() {
419 playAdjacentMarker(AudioMarker.recentlyPlayedMarker(), false);
420 }
421
422 private static Marker getAdjacentMarker(Marker startMarker, boolean next, Layer layer) {
423 Marker previousMarker = null;
424 boolean nextTime = false;
425 if (layer.getClass() == MarkerLayer.class) {
426 MarkerLayer markerLayer = (MarkerLayer) layer;
427 for (Marker marker : markerLayer.data) {
428 if (marker == startMarker) {
429 if (next) {
430 nextTime = true;
431 } else {
432 if (previousMarker == null) {
433 previousMarker = startMarker; // if no previous one, play the first one again
434 }
435 return previousMarker;
436 }
437 } else if (marker.getClass() == AudioMarker.class) {
438 if (nextTime || startMarker == null)
439 return marker;
440 previousMarker = marker;
441 }
442 }
443 if (nextTime) // there was no next marker in that layer, so play the last one again
444 return startMarker;
445 }
446 return null;
447 }
448
449 private static void playAdjacentMarker(Marker startMarker, boolean next) {
450 if (!Main.isDisplayingMapView())
451 return;
452 Marker m = null;
453 Layer l = Main.getLayerManager().getActiveLayer();
454 if (l != null) {
455 m = getAdjacentMarker(startMarker, next, l);
456 }
457 if (m == null) {
458 for (Layer layer : Main.getLayerManager().getLayers()) {
459 m = getAdjacentMarker(startMarker, next, layer);
460 if (m != null) {
461 break;
462 }
463 }
464 }
465 if (m != null) {
466 ((AudioMarker) m).play();
467 }
468 }
469
470 /**
471 * Get state of text display.
472 * @return <code>true</code> if text should be shown, <code>false</code> otherwise.
473 */
474 private boolean isTextOrIconShown() {
475 String current = Main.pref.get("marker.show "+getName(), "show");
476 return "show".equalsIgnoreCase(current);
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 Main.map.mapView.repaint();
571 }
572 }
573}
Note: See TracBrowser for help on using the repository browser.