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

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

see #11390 - sonar - squid:S1604 - Java 8: Anonymous inner classes containing only one method should become lambdas

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