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

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

code refactoring

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