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

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

FindBugs - fix blocker issue (Performance - The equals and hashCode methods of URL are blocking)

  • Property svn:eol-style set to native
File size: 20.0 KB
Line 
1// License: GPL. Copyright 2007 by Immanuel Scholz and others
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<Marker>();
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 }
119 if (offset == null) {
120 offset = time - firstTime;
121 }
122 Marker m = Marker.createMarker(wpt, indata.storageFile, this, time, offset);
123 if (m != null) {
124 data.add(m);
125 }
126 }
127 }
128
129 @Override
130 public void hookUpMapView() {
131 Main.map.mapView.addMouseListener(new MouseAdapter() {
132 @Override public void mousePressed(MouseEvent e) {
133 if (e.getButton() != MouseEvent.BUTTON1)
134 return;
135 boolean mousePressedInButton = false;
136 if (e.getPoint() != null) {
137 for (Marker mkr : data) {
138 if (mkr.containsPoint(e.getPoint())) {
139 mousePressedInButton = true;
140 break;
141 }
142 }
143 }
144 if (! mousePressedInButton)
145 return;
146 mousePressed = true;
147 if (isVisible()) {
148 Main.map.mapView.repaint();
149 }
150 }
151 @Override public void mouseReleased(MouseEvent ev) {
152 if (ev.getButton() != MouseEvent.BUTTON1 || ! mousePressed)
153 return;
154 mousePressed = false;
155 if (!isVisible())
156 return;
157 if (ev.getPoint() != null) {
158 for (Marker mkr : data) {
159 if (mkr.containsPoint(ev.getPoint())) {
160 mkr.actionPerformed(new ActionEvent(this, 0, null));
161 }
162 }
163 }
164 Main.map.mapView.repaint();
165 }
166 });
167 }
168
169 /**
170 * Return a static icon.
171 */
172 @Override public Icon getIcon() {
173 return ImageProvider.get("layer", "marker_small");
174 }
175
176 @Override
177 public Color getColor(boolean ignoreCustom)
178 {
179 String name = getName();
180 return Main.pref.getColor(marktr("gps marker"), name != null ? "layer "+name : null, Color.gray);
181 }
182
183 /* for preferences */
184 static public Color getGenericColor()
185 {
186 return Main.pref.getColor(marktr("gps marker"), Color.gray);
187 }
188
189 @Override public void paint(Graphics2D g, MapView mv, Bounds box) {
190 boolean showTextOrIcon = isTextOrIconShown();
191 g.setColor(getColor(true));
192
193 if (mousePressed) {
194 boolean mousePressedTmp = mousePressed;
195 Point mousePos = mv.getMousePosition(); // Get mouse position only when necessary (it's the slowest part of marker layer painting)
196 for (Marker mkr : data) {
197 if (mousePos != null && mkr.containsPoint(mousePos)) {
198 mkr.paint(g, mv, mousePressedTmp, showTextOrIcon);
199 mousePressedTmp = false;
200 }
201 }
202 } else {
203 for (Marker mkr : data) {
204 mkr.paint(g, mv, false, showTextOrIcon);
205 }
206 }
207 }
208
209 @Override public String getToolTipText() {
210 return data.size()+" "+trn("marker", "markers", data.size());
211 }
212
213 @Override public void mergeFrom(Layer from) {
214 MarkerLayer layer = (MarkerLayer)from;
215 data.addAll(layer.data);
216 Collections.sort(data, new Comparator<Marker>() {
217 @Override
218 public int compare(Marker o1, Marker o2) {
219 return Double.compare(o1.time, o2.time);
220 }
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.getEastNorth());
231 }
232 }
233
234 @Override public Object getInfoComponent() {
235 return "<html>"+trn("{0} consists of {1} marker", "{0} consists of {1} markers", data.size(), getName(), data.size()) + "</html>";
236 }
237
238 @Override public Action[] getMenuEntries() {
239 Collection<Action> components = new ArrayList<Action>();
240 components.add(LayerListDialog.getInstance().createShowHideLayerAction());
241 components.add(new ShowHideMarkerText(this));
242 components.add(LayerListDialog.getInstance().createDeleteLayerAction());
243 components.add(SeparatorLayerAction.INSTANCE);
244 components.add(new CustomizeColor(this));
245 components.add(SeparatorLayerAction.INSTANCE);
246 components.add(new SynchronizeAudio());
247 if (Main.pref.getBoolean("marker.traceaudio", true)) {
248 components.add (new MoveAudio());
249 }
250 components.add(new JumpToNextMarker(this));
251 components.add(new JumpToPreviousMarker(this));
252 components.add(new RenameLayerAction(getAssociatedFile(), this));
253 components.add(SeparatorLayerAction.INSTANCE);
254 components.add(new LayerListPopup.InfoAction(this));
255 return components.toArray(new Action[components.size()]);
256 }
257
258 public boolean synchronizeAudioMarkers(AudioMarker startMarker) {
259 if (startMarker != null && ! data.contains(startMarker)) {
260 startMarker = null;
261 }
262 if (startMarker == null) {
263 // find the first audioMarker in this layer
264 for (Marker m : data) {
265 if (m instanceof AudioMarker) {
266 startMarker = (AudioMarker) m;
267 break;
268 }
269 }
270 }
271 if (startMarker == null)
272 return false;
273
274 // apply adjustment to all subsequent audio markers in the layer
275 double adjustment = AudioPlayer.position() - startMarker.offset; // in seconds
276 boolean seenStart = false;
277 try {
278 URI uri = startMarker.url().toURI();
279 for (Marker m : data) {
280 if (m == startMarker) {
281 seenStart = true;
282 }
283 if (seenStart && m instanceof AudioMarker) {
284 AudioMarker ma = (AudioMarker) m;
285 // Do not ever call URL.equals but use URI.equals instead to avoid Internet connection
286 // See http://michaelscharf.blogspot.fr/2006/11/javaneturlequals-and-hashcode-make.html for details
287 if (ma.url().toURI().equals(uri)) {
288 ma.adjustOffset(adjustment);
289 }
290 }
291 }
292 } catch (URISyntaxException e) {
293 Main.warn("URISyntaxException: "+e.getMessage());
294 }
295 return true;
296 }
297
298 public AudioMarker addAudioMarker(double time, LatLon coor) {
299 // find first audio marker to get absolute start time
300 double offset = 0.0;
301 AudioMarker am = null;
302 for (Marker m : data) {
303 if (m.getClass() == AudioMarker.class) {
304 am = (AudioMarker)m;
305 offset = time - am.time;
306 break;
307 }
308 }
309 if (am == null) {
310 JOptionPane.showMessageDialog(
311 Main.parent,
312 tr("No existing audio markers in this layer to offset from."),
313 tr("Error"),
314 JOptionPane.ERROR_MESSAGE
315 );
316 return null;
317 }
318
319 // make our new marker
320 AudioMarker newAudioMarker = new AudioMarker(coor,
321 null, AudioPlayer.url(), this, time, offset);
322
323 // insert it at the right place in a copy the collection
324 Collection<Marker> newData = new ArrayList<Marker>();
325 am = null;
326 AudioMarker ret = newAudioMarker; // save to have return value
327 for (Marker m : data) {
328 if (m.getClass() == AudioMarker.class) {
329 am = (AudioMarker) m;
330 if (newAudioMarker != null && offset < am.offset) {
331 newAudioMarker.adjustOffset(am.syncOffset()); // i.e. same as predecessor
332 newData.add(newAudioMarker);
333 newAudioMarker = null;
334 }
335 }
336 newData.add(m);
337 }
338
339 if (newAudioMarker != null) {
340 if (am != null) {
341 newAudioMarker.adjustOffset(am.syncOffset()); // i.e. same as predecessor
342 }
343 newData.add(newAudioMarker); // insert at end
344 }
345
346 // replace the collection
347 data.clear();
348 data.addAll(newData);
349 return ret;
350 }
351
352 @Override
353 public void jumpToNextMarker() {
354 if (currentMarker == null) {
355 currentMarker = data.get(0);
356 } else {
357 boolean foundCurrent = false;
358 for (Marker m: data) {
359 if (foundCurrent) {
360 currentMarker = m;
361 break;
362 } else if (currentMarker == m) {
363 foundCurrent = true;
364 }
365 }
366 }
367 Main.map.mapView.zoomTo(currentMarker.getEastNorth());
368 }
369
370 @Override
371 public void jumpToPreviousMarker() {
372 if (currentMarker == null) {
373 currentMarker = data.get(data.size() - 1);
374 } else {
375 boolean foundCurrent = false;
376 for (int i=data.size() - 1; i>=0; i--) {
377 Marker m = data.get(i);
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 public static void playAudio() {
390 playAdjacentMarker(null, true);
391 }
392
393 public static void playNextMarker() {
394 playAdjacentMarker(AudioMarker.recentlyPlayedMarker(), true);
395 }
396
397 public static void playPreviousMarker() {
398 playAdjacentMarker(AudioMarker.recentlyPlayedMarker(), false);
399 }
400
401 private static Marker getAdjacentMarker(Marker startMarker, boolean next, Layer layer) {
402 Marker previousMarker = null;
403 boolean nextTime = false;
404 if (layer.getClass() == MarkerLayer.class) {
405 MarkerLayer markerLayer = (MarkerLayer) layer;
406 for (Marker marker : markerLayer.data) {
407 if (marker == startMarker) {
408 if (next) {
409 nextTime = true;
410 } else {
411 if (previousMarker == null) {
412 previousMarker = startMarker; // if no previous one, play the first one again
413 }
414 return previousMarker;
415 }
416 }
417 else if (marker.getClass() == AudioMarker.class)
418 {
419 if(nextTime || startMarker == null)
420 return marker;
421 previousMarker = marker;
422 }
423 }
424 if (nextTime) // there was no next marker in that layer, so play the last one again
425 return startMarker;
426 }
427 return null;
428 }
429
430 private static void playAdjacentMarker(Marker startMarker, boolean next) {
431 Marker m = null;
432 if (Main.map == null || Main.map.mapView == null)
433 return;
434 Layer l = Main.map.mapView.getActiveLayer();
435 if(l != null) {
436 m = getAdjacentMarker(startMarker, next, l);
437 }
438 if(m == null)
439 {
440 for (Layer layer : Main.map.mapView.getAllLayers())
441 {
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 String current = Main.pref.get("marker.show "+getName(),"show");
459 return "show".equalsIgnoreCase(current);
460 }
461
462 public static final class ShowHideMarkerText extends AbstractAction implements LayerAction {
463 private final MarkerLayer layer;
464
465 public ShowHideMarkerText(MarkerLayer layer) {
466 super(tr("Show Text/Icons"), ImageProvider.get("dialogs", "showhide"));
467 putValue(SHORT_DESCRIPTION, tr("Toggle visible state of the marker text and icons."));
468 putValue("help", ht("/Action/ShowHideTextIcons"));
469 this.layer = layer;
470 }
471
472
473 @Override
474 public void actionPerformed(ActionEvent e) {
475 Main.pref.put("marker.show "+layer.getName(), layer.isTextOrIconShown() ? "hide" : "show");
476 Main.map.mapView.repaint();
477 }
478
479
480 @Override
481 public Component createMenuComponent() {
482 JCheckBoxMenuItem showMarkerTextItem = new JCheckBoxMenuItem(this);
483 showMarkerTextItem.setState(layer.isTextOrIconShown());
484 return showMarkerTextItem;
485 }
486
487 @Override
488 public boolean supportLayers(List<Layer> layers) {
489 return layers.size() == 1 && layers.get(0) instanceof MarkerLayer;
490 }
491 }
492
493
494 private class SynchronizeAudio extends AbstractAction {
495
496 public SynchronizeAudio() {
497 super(tr("Synchronize Audio"), ImageProvider.get("audio-sync"));
498 putValue("help", ht("/Action/SynchronizeAudio"));
499 }
500
501 @Override
502 public void actionPerformed(ActionEvent e) {
503 if (! AudioPlayer.paused()) {
504 JOptionPane.showMessageDialog(
505 Main.parent,
506 tr("You need to pause audio at the moment when you hear your synchronization cue."),
507 tr("Warning"),
508 JOptionPane.WARNING_MESSAGE
509 );
510 return;
511 }
512 AudioMarker recent = AudioMarker.recentlyPlayedMarker();
513 if (synchronizeAudioMarkers(recent)) {
514 JOptionPane.showMessageDialog(
515 Main.parent,
516 tr("Audio synchronized at point {0}.", recent.getText()),
517 tr("Information"),
518 JOptionPane.INFORMATION_MESSAGE
519 );
520 } else {
521 JOptionPane.showMessageDialog(
522 Main.parent,
523 tr("Unable to synchronize in layer being played."),
524 tr("Error"),
525 JOptionPane.ERROR_MESSAGE
526 );
527 }
528 }
529 }
530
531 private class MoveAudio extends AbstractAction {
532
533 public MoveAudio() {
534 super(tr("Make Audio Marker at Play Head"), ImageProvider.get("addmarkers"));
535 putValue("help", ht("/Action/MakeAudioMarkerAtPlayHead"));
536 }
537
538 @Override
539 public void actionPerformed(ActionEvent e) {
540 if (! AudioPlayer.paused()) {
541 JOptionPane.showMessageDialog(
542 Main.parent,
543 tr("You need to have paused audio at the point on the track where you want the marker."),
544 tr("Warning"),
545 JOptionPane.WARNING_MESSAGE
546 );
547 return;
548 }
549 PlayHeadMarker playHeadMarker = Main.map.mapView.playHeadMarker;
550 if (playHeadMarker == null)
551 return;
552 addAudioMarker(playHeadMarker.time, playHeadMarker.getCoor());
553 Main.map.mapView.repaint();
554 }
555 }
556
557}
Note: See TracBrowser for help on using the repository browser.