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

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

see #8465 - use diamond operator where applicable

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