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

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

fix #16796 - Rework of GPX track colors / layer preferences (patch by Bjoeni)

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