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

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

see #15182 - deprecate all Main logging methods and introduce suitable replacements in Logging for most of them

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