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

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

sonar - Local variable and method parameter names should comply with a naming convention

  • 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.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.gui.layer.gpx.ConvertToDataLayerAction;
51import org.openstreetmap.josm.tools.AudioPlayer;
52import org.openstreetmap.josm.tools.ImageProvider;
53
54/**
55 * A layer holding markers.
56 *
57 * Markers are GPS points with a name and, optionally, a symbol code attached;
58 * marker layers can be created from waypoints when importing raw GPS data,
59 * but they may also come from other sources.
60 *
61 * The symbol code is for future use.
62 *
63 * The data is read only.
64 */
65public class MarkerLayer extends Layer implements JumpToMarkerLayer {
66
67 /**
68 * A list of markers.
69 */
70 public final List<Marker> data;
71 private boolean mousePressed;
72 public GpxLayer fromLayer;
73 private Marker currentMarker;
74 public AudioMarker syncAudioMarker;
75
76 private static final Color DEFAULT_COLOR = Color.magenta;
77
78 /**
79 * Constructs a new {@code MarkerLayer}.
80 * @param indata The GPX data for this layer
81 * @param name The marker layer name
82 * @param associatedFile The associated GPX file
83 * @param fromLayer The associated GPX layer
84 */
85 public MarkerLayer(GpxData indata, String name, File associatedFile, GpxLayer fromLayer) {
86 super(name);
87 this.setAssociatedFile(associatedFile);
88 this.data = new ArrayList<>();
89 this.fromLayer = fromLayer;
90 double firstTime = -1.0;
91 String lastLinkedFile = "";
92
93 for (WayPoint wpt : indata.waypoints) {
94 /* calculate time differences in waypoints */
95 double time = wpt.time;
96 boolean wptHasLink = wpt.attr.containsKey(GpxConstants.META_LINKS);
97 if (firstTime < 0 && wptHasLink) {
98 firstTime = time;
99 for (GpxLink oneLink : wpt.<GpxLink>getCollection(GpxConstants.META_LINKS)) {
100 lastLinkedFile = oneLink.uri;
101 break;
102 }
103 }
104 if (wptHasLink) {
105 for (GpxLink oneLink : wpt.<GpxLink>getCollection(GpxConstants.META_LINKS)) {
106 String uri = oneLink.uri;
107 if (uri != null) {
108 if (!uri.equals(lastLinkedFile)) {
109 firstTime = time;
110 }
111 lastLinkedFile = uri;
112 break;
113 }
114 }
115 }
116 Double offset = null;
117 // If we have an explicit offset, take it.
118 // Otherwise, for a group of markers with the same Link-URI (e.g. an
119 // audio file) calculate the offset relative to the first marker of
120 // that group. This way the user can jump to the corresponding
121 // playback positions in a long audio track.
122 Extensions exts = (Extensions) wpt.get(GpxConstants.META_EXTENSIONS);
123 if (exts != null && exts.containsKey("offset")) {
124 try {
125 offset = Double.valueOf(exts.get("offset"));
126 } catch (NumberFormatException nfe) {
127 Main.warn(nfe);
128 }
129 }
130 if (offset == null) {
131 offset = time - firstTime;
132 }
133 final Collection<Marker> markers = Marker.createMarkers(wpt, indata.storageFile, this, time, offset);
134 if (markers != null) {
135 data.addAll(markers);
136 }
137 }
138 }
139
140 @Override
141 public void hookUpMapView() {
142 Main.map.mapView.addMouseListener(new MouseAdapter() {
143 @Override
144 public void mousePressed(MouseEvent e) {
145 if (e.getButton() != MouseEvent.BUTTON1)
146 return;
147 boolean mousePressedInButton = false;
148 if (e.getPoint() != null) {
149 for (Marker mkr : data) {
150 if (mkr.containsPoint(e.getPoint())) {
151 mousePressedInButton = true;
152 break;
153 }
154 }
155 }
156 if (!mousePressedInButton)
157 return;
158 mousePressed = true;
159 if (isVisible()) {
160 Main.map.mapView.repaint();
161 }
162 }
163
164 @Override
165 public void mouseReleased(MouseEvent ev) {
166 if (ev.getButton() != MouseEvent.BUTTON1 || !mousePressed)
167 return;
168 mousePressed = false;
169 if (!isVisible())
170 return;
171 if (ev.getPoint() != null) {
172 for (Marker mkr : data) {
173 if (mkr.containsPoint(ev.getPoint())) {
174 mkr.actionPerformed(new ActionEvent(this, 0, null));
175 }
176 }
177 }
178 Main.map.mapView.repaint();
179 }
180 });
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 Color getColor(boolean ignoreCustom) {
193 String name = getName();
194 return Main.pref.getColor(marktr("gps marker"), name != null ? "layer "+name : null, DEFAULT_COLOR);
195 }
196
197 /* for preferences */
198 public static Color getGenericColor() {
199 return Main.pref.getColor(marktr("gps marker"), DEFAULT_COLOR);
200 }
201
202 @Override
203 public void paint(Graphics2D g, MapView mv, Bounds box) {
204 boolean showTextOrIcon = isTextOrIconShown();
205 g.setColor(getColor(true));
206
207 if (mousePressed) {
208 boolean mousePressedTmp = mousePressed;
209 Point mousePos = mv.getMousePosition(); // Get mouse position only when necessary (it's the slowest part of marker layer painting)
210 for (Marker mkr : data) {
211 if (mousePos != null && mkr.containsPoint(mousePos)) {
212 mkr.paint(g, mv, mousePressedTmp, showTextOrIcon);
213 mousePressedTmp = false;
214 }
215 }
216 } else {
217 for (Marker mkr : data) {
218 mkr.paint(g, mv, false, showTextOrIcon);
219 }
220 }
221 }
222
223 @Override
224 public String getToolTipText() {
225 return data.size()+' '+trn("marker", "markers", data.size());
226 }
227
228 @Override
229 public void mergeFrom(Layer from) {
230 MarkerLayer layer = (MarkerLayer) from;
231 data.addAll(layer.data);
232 Collections.sort(data, new Comparator<Marker>() {
233 @Override
234 public int compare(Marker o1, Marker o2) {
235 return Double.compare(o1.time, o2.time);
236 }
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.map.mapView.getActiveLayer();
452 if (l != null) {
453 m = getAdjacentMarker(startMarker, next, l);
454 }
455 if (m == null) {
456 for (Layer layer : Main.map.mapView.getAllLayers()) {
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.