001 // License: GPL. Copyright 2007 by Immanuel Scholz and others
002 package org.openstreetmap.josm.gui.layer.markerlayer;
003
004 import static org.openstreetmap.josm.gui.help.HelpUtil.ht;
005 import static org.openstreetmap.josm.tools.I18n.marktr;
006 import static org.openstreetmap.josm.tools.I18n.tr;
007 import static org.openstreetmap.josm.tools.I18n.trn;
008
009 import java.awt.Color;
010 import java.awt.Component;
011 import java.awt.Graphics2D;
012 import java.awt.Point;
013 import java.awt.event.ActionEvent;
014 import java.awt.event.MouseAdapter;
015 import java.awt.event.MouseEvent;
016 import java.io.File;
017 import java.net.URL;
018 import java.util.ArrayList;
019 import java.util.Collection;
020 import java.util.Collections;
021 import java.util.Comparator;
022 import java.util.List;
023
024 import javax.swing.AbstractAction;
025 import javax.swing.Action;
026 import javax.swing.Icon;
027 import javax.swing.JCheckBoxMenuItem;
028 import javax.swing.JOptionPane;
029
030 import org.openstreetmap.josm.Main;
031 import org.openstreetmap.josm.actions.RenameLayerAction;
032 import org.openstreetmap.josm.data.Bounds;
033 import org.openstreetmap.josm.data.coor.LatLon;
034 import org.openstreetmap.josm.data.gpx.GpxData;
035 import org.openstreetmap.josm.data.gpx.GpxLink;
036 import org.openstreetmap.josm.data.gpx.WayPoint;
037 import org.openstreetmap.josm.data.osm.visitor.BoundingXYVisitor;
038 import org.openstreetmap.josm.gui.MapView;
039 import org.openstreetmap.josm.gui.dialogs.LayerListDialog;
040 import org.openstreetmap.josm.gui.dialogs.LayerListPopup;
041 import org.openstreetmap.josm.gui.layer.CustomizeColor;
042 import org.openstreetmap.josm.gui.layer.GpxLayer;
043 import org.openstreetmap.josm.gui.layer.JumpToMarkerActions.JumpToMarkerLayer;
044 import org.openstreetmap.josm.gui.layer.JumpToMarkerActions.JumpToNextMarker;
045 import org.openstreetmap.josm.gui.layer.JumpToMarkerActions.JumpToPreviousMarker;
046 import org.openstreetmap.josm.gui.layer.Layer;
047 import org.openstreetmap.josm.tools.AudioPlayer;
048 import org.openstreetmap.josm.tools.ImageProvider;
049
050 /**
051 * A layer holding markers.
052 *
053 * Markers are GPS points with a name and, optionally, a symbol code attached;
054 * marker layers can be created from waypoints when importing raw GPS data,
055 * but they may also come from other sources.
056 *
057 * The symbol code is for future use.
058 *
059 * The data is read only.
060 */
061 public class MarkerLayer extends Layer implements JumpToMarkerLayer {
062
063 /**
064 * A list of markers.
065 */
066 public final List<Marker> data;
067 private boolean mousePressed = false;
068 public GpxLayer fromLayer = null;
069 private Marker currentMarker;
070
071 @Deprecated
072 public MarkerLayer(GpxData indata, String name, File associatedFile, GpxLayer fromLayer, boolean addMouseHandlerInConstructor) {
073 this(indata, name, associatedFile, fromLayer);
074 }
075
076 public MarkerLayer(GpxData indata, String name, File associatedFile, GpxLayer fromLayer) {
077 super(name);
078 this.setAssociatedFile(associatedFile);
079 this.data = new ArrayList<Marker>();
080 this.fromLayer = fromLayer;
081 double firstTime = -1.0;
082 String lastLinkedFile = "";
083
084 for (WayPoint wpt : indata.waypoints) {
085 /* calculate time differences in waypoints */
086 double time = wpt.time;
087 boolean wpt_has_link = wpt.attr.containsKey(GpxData.META_LINKS);
088 if (firstTime < 0 && wpt_has_link) {
089 firstTime = time;
090 for (Object oneLink : wpt.getCollection(GpxData.META_LINKS)) {
091 if (oneLink instanceof GpxLink) {
092 lastLinkedFile = ((GpxLink)oneLink).uri;
093 break;
094 }
095 }
096 }
097 if (wpt_has_link) {
098 for (Object oneLink : wpt.getCollection(GpxData.META_LINKS)) {
099 if (oneLink instanceof GpxLink) {
100 String uri = ((GpxLink)oneLink).uri;
101 if (!uri.equals(lastLinkedFile)) {
102 firstTime = time;
103 }
104 lastLinkedFile = uri;
105 break;
106 }
107 }
108 }
109 Marker m = Marker.createMarker(wpt, indata.storageFile, this, time, time - firstTime);
110 if (m != null) {
111 data.add(m);
112 }
113 }
114 }
115
116 @Override
117 public void hookUpMapView() {
118 Main.map.mapView.addMouseListener(new MouseAdapter() {
119 @Override public void mousePressed(MouseEvent e) {
120 if (e.getButton() != MouseEvent.BUTTON1)
121 return;
122 boolean mousePressedInButton = false;
123 if (e.getPoint() != null) {
124 for (Marker mkr : data) {
125 if (mkr.containsPoint(e.getPoint())) {
126 mousePressedInButton = true;
127 break;
128 }
129 }
130 }
131 if (! mousePressedInButton)
132 return;
133 mousePressed = true;
134 if (isVisible()) {
135 Main.map.mapView.repaint();
136 }
137 }
138 @Override public void mouseReleased(MouseEvent ev) {
139 if (ev.getButton() != MouseEvent.BUTTON1 || ! mousePressed)
140 return;
141 mousePressed = false;
142 if (!isVisible())
143 return;
144 if (ev.getPoint() != null) {
145 for (Marker mkr : data) {
146 if (mkr.containsPoint(ev.getPoint())) {
147 mkr.actionPerformed(new ActionEvent(this, 0, null));
148 }
149 }
150 }
151 Main.map.mapView.repaint();
152 }
153 });
154 }
155
156 /**
157 * Return a static icon.
158 */
159 @Override public Icon getIcon() {
160 return ImageProvider.get("layer", "marker_small");
161 }
162
163 @Override
164 public Color getColor(boolean ignoreCustom)
165 {
166 String name = getName();
167 return Main.pref.getColor(marktr("gps marker"), name != null ? "layer "+name : null, Color.gray);
168 }
169
170 /* for preferences */
171 static public Color getGenericColor()
172 {
173 return Main.pref.getColor(marktr("gps marker"), Color.gray);
174 }
175
176 @Override public void paint(Graphics2D g, MapView mv, Bounds box) {
177 boolean showTextOrIcon = isTextOrIconShown();
178 g.setColor(getColor(true));
179
180 if (mousePressed) {
181 boolean mousePressedTmp = mousePressed;
182 Point mousePos = mv.getMousePosition(); // Get mouse position only when necessary (it's the slowest part of marker layer painting)
183 for (Marker mkr : data) {
184 if (mousePos != null && mkr.containsPoint(mousePos)) {
185 mkr.paint(g, mv, mousePressedTmp, showTextOrIcon);
186 mousePressedTmp = false;
187 }
188 }
189 } else {
190 for (Marker mkr : data) {
191 mkr.paint(g, mv, false, showTextOrIcon);
192 }
193 }
194 }
195
196 @Override public String getToolTipText() {
197 return data.size()+" "+trn("marker", "markers", data.size());
198 }
199
200 @Override public void mergeFrom(Layer from) {
201 MarkerLayer layer = (MarkerLayer)from;
202 data.addAll(layer.data);
203 Collections.sort(data, new Comparator<Marker>() {
204 @Override
205 public int compare(Marker o1, Marker o2) {
206 return Double.compare(o1.time, o2.time);
207 }
208 });
209 }
210
211 @Override public boolean isMergable(Layer other) {
212 return other instanceof MarkerLayer;
213 }
214
215 @Override public void visitBoundingBox(BoundingXYVisitor v) {
216 for (Marker mkr : data) {
217 v.visit(mkr.getEastNorth());
218 }
219 }
220
221 @Override public Object getInfoComponent() {
222 return "<html>"+trn("{0} consists of {1} marker", "{0} consists of {1} markers", data.size(), getName(), data.size()) + "</html>";
223 }
224
225 @Override public Action[] getMenuEntries() {
226 Collection<Action> components = new ArrayList<Action>();
227 components.add(LayerListDialog.getInstance().createShowHideLayerAction());
228 components.add(new ShowHideMarkerText(this));
229 components.add(LayerListDialog.getInstance().createDeleteLayerAction());
230 components.add(SeparatorLayerAction.INSTANCE);
231 components.add(new CustomizeColor(this));
232 components.add(SeparatorLayerAction.INSTANCE);
233 components.add(new SynchronizeAudio());
234 if (Main.pref.getBoolean("marker.traceaudio", true)) {
235 components.add (new MoveAudio());
236 }
237 components.add(new JumpToNextMarker(this));
238 components.add(new JumpToPreviousMarker(this));
239 components.add(new RenameLayerAction(getAssociatedFile(), this));
240 components.add(SeparatorLayerAction.INSTANCE);
241 components.add(new LayerListPopup.InfoAction(this));
242 return components.toArray(new Action[0]);
243 }
244
245 public boolean synchronizeAudioMarkers(AudioMarker startMarker) {
246 if (startMarker != null && ! data.contains(startMarker)) {
247 startMarker = null;
248 }
249 if (startMarker == null) {
250 // find the first audioMarker in this layer
251 for (Marker m : data) {
252 if (m instanceof AudioMarker) {
253 startMarker = (AudioMarker) m;
254 break;
255 }
256 }
257 }
258 if (startMarker == null)
259 return false;
260
261 // apply adjustment to all subsequent audio markers in the layer
262 double adjustment = AudioPlayer.position() - startMarker.offset; // in seconds
263 boolean seenStart = false;
264 URL url = startMarker.url();
265 for (Marker m : data) {
266 if (m == startMarker) {
267 seenStart = true;
268 }
269 if (seenStart) {
270 AudioMarker ma = (AudioMarker) m; // it must be an AudioMarker
271 if (ma.url().equals(url)) {
272 ma.adjustOffset(adjustment);
273 }
274 }
275 }
276 return true;
277 }
278
279 public AudioMarker addAudioMarker(double time, LatLon coor) {
280 // find first audio marker to get absolute start time
281 double offset = 0.0;
282 AudioMarker am = null;
283 for (Marker m : data) {
284 if (m.getClass() == AudioMarker.class) {
285 am = (AudioMarker)m;
286 offset = time - am.time;
287 break;
288 }
289 }
290 if (am == null) {
291 JOptionPane.showMessageDialog(
292 Main.parent,
293 tr("No existing audio markers in this layer to offset from."),
294 tr("Error"),
295 JOptionPane.ERROR_MESSAGE
296 );
297 return null;
298 }
299
300 // make our new marker
301 AudioMarker newAudioMarker = new AudioMarker(coor,
302 null, AudioPlayer.url(), this, time, offset);
303
304 // insert it at the right place in a copy the collection
305 Collection<Marker> newData = new ArrayList<Marker>();
306 am = null;
307 AudioMarker ret = newAudioMarker; // save to have return value
308 for (Marker m : data) {
309 if (m.getClass() == AudioMarker.class) {
310 am = (AudioMarker) m;
311 if (newAudioMarker != null && offset < am.offset) {
312 newAudioMarker.adjustOffset(am.syncOffset()); // i.e. same as predecessor
313 newData.add(newAudioMarker);
314 newAudioMarker = null;
315 }
316 }
317 newData.add(m);
318 }
319
320 if (newAudioMarker != null) {
321 if (am != null) {
322 newAudioMarker.adjustOffset(am.syncOffset()); // i.e. same as predecessor
323 }
324 newData.add(newAudioMarker); // insert at end
325 }
326
327 // replace the collection
328 data.clear();
329 data.addAll(newData);
330 return ret;
331 }
332
333 public void jumpToNextMarker() {
334 if (currentMarker == null) {
335 currentMarker = data.get(0);
336 } else {
337 boolean foundCurrent = false;
338 for (Marker m: data) {
339 if (foundCurrent) {
340 currentMarker = m;
341 break;
342 } else if (currentMarker == m) {
343 foundCurrent = true;
344 }
345 }
346 }
347 Main.map.mapView.zoomTo(currentMarker.getEastNorth());
348 }
349
350 public void jumpToPreviousMarker() {
351 if (currentMarker == null) {
352 currentMarker = data.get(data.size() - 1);
353 } else {
354 boolean foundCurrent = false;
355 for (int i=data.size() - 1; i>=0; i--) {
356 Marker m = data.get(i);
357 if (foundCurrent) {
358 currentMarker = m;
359 break;
360 } else if (currentMarker == m) {
361 foundCurrent = true;
362 }
363 }
364 }
365 Main.map.mapView.zoomTo(currentMarker.getEastNorth());
366 }
367
368 public static void playAudio() {
369 playAdjacentMarker(null, true);
370 }
371
372 public static void playNextMarker() {
373 playAdjacentMarker(AudioMarker.recentlyPlayedMarker(), true);
374 }
375
376 public static void playPreviousMarker() {
377 playAdjacentMarker(AudioMarker.recentlyPlayedMarker(), false);
378 }
379
380 private static Marker getAdjacentMarker(Marker startMarker, boolean next, Layer layer) {
381 Marker previousMarker = null;
382 boolean nextTime = false;
383 if (layer.getClass() == MarkerLayer.class) {
384 MarkerLayer markerLayer = (MarkerLayer) layer;
385 for (Marker marker : markerLayer.data) {
386 if (marker == startMarker) {
387 if (next) {
388 nextTime = true;
389 } else {
390 if (previousMarker == null) {
391 previousMarker = startMarker; // if no previous one, play the first one again
392 }
393 return previousMarker;
394 }
395 }
396 else if (marker.getClass() == AudioMarker.class)
397 {
398 if(nextTime || startMarker == null)
399 return marker;
400 previousMarker = marker;
401 }
402 }
403 if (nextTime) // there was no next marker in that layer, so play the last one again
404 return startMarker;
405 }
406 return null;
407 }
408
409 private static void playAdjacentMarker(Marker startMarker, boolean next) {
410 Marker m = null;
411 if (Main.map == null || Main.map.mapView == null)
412 return;
413 Layer l = Main.map.mapView.getActiveLayer();
414 if(l != null) {
415 m = getAdjacentMarker(startMarker, next, l);
416 }
417 if(m == null)
418 {
419 for (Layer layer : Main.map.mapView.getAllLayers())
420 {
421 m = getAdjacentMarker(startMarker, next, layer);
422 if(m != null) {
423 break;
424 }
425 }
426 }
427 if(m != null) {
428 ((AudioMarker)m).play();
429 }
430 }
431
432 /**
433 * Get state of text display.
434 * @return <code>true</code> if text should be shown, <code>false</code> otherwise.
435 */
436 private boolean isTextOrIconShown() {
437 String current = Main.pref.get("marker.show "+getName(),"show");
438 return "show".equalsIgnoreCase(current);
439 }
440
441 public static final class ShowHideMarkerText extends AbstractAction implements LayerAction {
442 private final MarkerLayer layer;
443
444 public ShowHideMarkerText(MarkerLayer layer) {
445 super(tr("Show Text/Icons"), ImageProvider.get("dialogs", "showhide"));
446 putValue(SHORT_DESCRIPTION, tr("Toggle visible state of the marker text and icons."));
447 putValue("help", ht("/Action/ShowHideTextIcons"));
448 this.layer = layer;
449 }
450
451
452 public void actionPerformed(ActionEvent e) {
453 Main.pref.put("marker.show "+layer.getName(), layer.isTextOrIconShown() ? "hide" : "show");
454 Main.map.mapView.repaint();
455 }
456
457
458 @Override
459 public Component createMenuComponent() {
460 JCheckBoxMenuItem showMarkerTextItem = new JCheckBoxMenuItem(this);
461 showMarkerTextItem.setState(layer.isTextOrIconShown());
462 return showMarkerTextItem;
463 }
464
465 @Override
466 public boolean supportLayers(List<Layer> layers) {
467 return layers.size() == 1 && layers.get(0) instanceof MarkerLayer;
468 }
469 }
470
471
472 private class SynchronizeAudio extends AbstractAction {
473
474 public SynchronizeAudio() {
475 super(tr("Synchronize Audio"), ImageProvider.get("audio-sync"));
476 putValue("help", ht("/Action/SynchronizeAudio"));
477 }
478
479 @Override
480 public void actionPerformed(ActionEvent e) {
481 if (! AudioPlayer.paused()) {
482 JOptionPane.showMessageDialog(
483 Main.parent,
484 tr("You need to pause audio at the moment when you hear your synchronization cue."),
485 tr("Warning"),
486 JOptionPane.WARNING_MESSAGE
487 );
488 return;
489 }
490 AudioMarker recent = AudioMarker.recentlyPlayedMarker();
491 if (synchronizeAudioMarkers(recent)) {
492 JOptionPane.showMessageDialog(
493 Main.parent,
494 tr("Audio synchronized at point {0}.", recent.getText()),
495 tr("Information"),
496 JOptionPane.INFORMATION_MESSAGE
497 );
498 } else {
499 JOptionPane.showMessageDialog(
500 Main.parent,
501 tr("Unable to synchronize in layer being played."),
502 tr("Error"),
503 JOptionPane.ERROR_MESSAGE
504 );
505 }
506 }
507 }
508
509 private class MoveAudio extends AbstractAction {
510
511 public MoveAudio() {
512 super(tr("Make Audio Marker at Play Head"), ImageProvider.get("addmarkers"));
513 putValue("help", ht("/Action/MakeAudioMarkerAtPlayHead"));
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 have paused audio at the point on the track where you want the marker."),
522 tr("Warning"),
523 JOptionPane.WARNING_MESSAGE
524 );
525 return;
526 }
527 PlayHeadMarker playHeadMarker = Main.map.mapView.playHeadMarker;
528 if (playHeadMarker == null)
529 return;
530 addAudioMarker(playHeadMarker.time, playHeadMarker.getCoor());
531 Main.map.mapView.repaint();
532 }
533 }
534
535 }