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.List; 021 022 import javax.swing.AbstractAction; 023 import javax.swing.Action; 024 import javax.swing.Icon; 025 import javax.swing.JCheckBoxMenuItem; 026 import javax.swing.JOptionPane; 027 import javax.swing.SwingUtilities; 028 029 import org.openstreetmap.josm.Main; 030 import org.openstreetmap.josm.actions.RenameLayerAction; 031 import org.openstreetmap.josm.data.Bounds; 032 import org.openstreetmap.josm.data.coor.LatLon; 033 import org.openstreetmap.josm.data.gpx.GpxData; 034 import org.openstreetmap.josm.data.gpx.GpxLink; 035 import org.openstreetmap.josm.data.gpx.WayPoint; 036 import org.openstreetmap.josm.data.osm.visitor.BoundingXYVisitor; 037 import org.openstreetmap.josm.gui.MapView; 038 import org.openstreetmap.josm.gui.dialogs.LayerListDialog; 039 import org.openstreetmap.josm.gui.dialogs.LayerListPopup; 040 import org.openstreetmap.josm.gui.layer.CustomizeColor; 041 import org.openstreetmap.josm.gui.layer.GpxLayer; 042 import org.openstreetmap.josm.gui.layer.JumpToMarkerActions.JumpToMarkerLayer; 043 import org.openstreetmap.josm.gui.layer.JumpToMarkerActions.JumpToNextMarker; 044 import org.openstreetmap.josm.gui.layer.JumpToMarkerActions.JumpToPreviousMarker; 045 import org.openstreetmap.josm.gui.layer.Layer; 046 import org.openstreetmap.josm.tools.AudioPlayer; 047 import org.openstreetmap.josm.tools.ImageProvider; 048 049 /** 050 * A layer holding markers. 051 * 052 * Markers are GPS points with a name and, optionally, a symbol code attached; 053 * marker layers can be created from waypoints when importing raw GPS data, 054 * but they may also come from other sources. 055 * 056 * The symbol code is for future use. 057 * 058 * The data is read only. 059 */ 060 public class MarkerLayer extends Layer implements JumpToMarkerLayer { 061 062 /** 063 * A list of markers. 064 */ 065 public final List<Marker> data; 066 private boolean mousePressed = false; 067 public GpxLayer fromLayer = null; 068 private Marker currentMarker; 069 070 public MarkerLayer(GpxData indata, String name, File associatedFile, GpxLayer fromLayer) { 071 this(indata, name, associatedFile, fromLayer, true); 072 } 073 074 public MarkerLayer(GpxData indata, String name, File associatedFile, GpxLayer fromLayer, boolean addMouseHandlerInConstructor) { 075 super(name); 076 this.setAssociatedFile(associatedFile); 077 this.data = new ArrayList<Marker>(); 078 this.fromLayer = fromLayer; 079 double firstTime = -1.0; 080 String lastLinkedFile = ""; 081 082 for (WayPoint wpt : indata.waypoints) { 083 /* calculate time differences in waypoints */ 084 double time = wpt.time; 085 boolean wpt_has_link = wpt.attr.containsKey(GpxData.META_LINKS); 086 if (firstTime < 0 && wpt_has_link) { 087 firstTime = time; 088 for (GpxLink oneLink : (Collection<GpxLink>) wpt.attr.get(GpxData.META_LINKS)) { 089 lastLinkedFile = oneLink.uri; 090 break; 091 } 092 } 093 if (wpt_has_link) { 094 for (GpxLink oneLink : (Collection<GpxLink>) wpt.attr.get(GpxData.META_LINKS)) { 095 if (!oneLink.uri.equals(lastLinkedFile)) { 096 firstTime = time; 097 } 098 lastLinkedFile = oneLink.uri; 099 break; 100 } 101 } 102 Marker m = Marker.createMarker(wpt, indata.storageFile, this, time, time - firstTime); 103 if (m != null) { 104 data.add(m); 105 } 106 } 107 108 if (addMouseHandlerInConstructor) { 109 SwingUtilities.invokeLater(new Runnable(){ 110 public void run() { 111 addMouseHandler(); 112 } 113 }); 114 } 115 } 116 117 public void addMouseHandler() { 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 } 204 205 @Override public boolean isMergable(Layer other) { 206 return other instanceof MarkerLayer; 207 } 208 209 @Override public void visitBoundingBox(BoundingXYVisitor v) { 210 for (Marker mkr : data) { 211 v.visit(mkr.getEastNorth()); 212 } 213 } 214 215 @Override public Object getInfoComponent() { 216 return "<html>"+trn("{0} consists of {1} marker", "{0} consists of {1} markers", data.size(), getName(), data.size()) + "</html>"; 217 } 218 219 @Override public Action[] getMenuEntries() { 220 Collection<Action> components = new ArrayList<Action>(); 221 components.add(LayerListDialog.getInstance().createShowHideLayerAction()); 222 components.add(new ShowHideMarkerText(this)); 223 components.add(LayerListDialog.getInstance().createDeleteLayerAction()); 224 components.add(SeparatorLayerAction.INSTANCE); 225 components.add(new CustomizeColor(this)); 226 components.add(SeparatorLayerAction.INSTANCE); 227 components.add(new SynchronizeAudio()); 228 if (Main.pref.getBoolean("marker.traceaudio", true)) { 229 components.add (new MoveAudio()); 230 } 231 components.add(new JumpToNextMarker(this)); 232 components.add(new JumpToPreviousMarker(this)); 233 components.add(new RenameLayerAction(getAssociatedFile(), this)); 234 components.add(SeparatorLayerAction.INSTANCE); 235 components.add(new LayerListPopup.InfoAction(this)); 236 return components.toArray(new Action[0]); 237 } 238 239 public boolean synchronizeAudioMarkers(AudioMarker startMarker) { 240 if (startMarker != null && ! data.contains(startMarker)) { 241 startMarker = null; 242 } 243 if (startMarker == null) { 244 // find the first audioMarker in this layer 245 for (Marker m : data) { 246 if (m instanceof AudioMarker) { 247 startMarker = (AudioMarker) m; 248 break; 249 } 250 } 251 } 252 if (startMarker == null) 253 return false; 254 255 // apply adjustment to all subsequent audio markers in the layer 256 double adjustment = AudioPlayer.position() - startMarker.offset; // in seconds 257 boolean seenStart = false; 258 URL url = startMarker.url(); 259 for (Marker m : data) { 260 if (m == startMarker) { 261 seenStart = true; 262 } 263 if (seenStart) { 264 AudioMarker ma = (AudioMarker) m; // it must be an AudioMarker 265 if (ma.url().equals(url)) { 266 ma.adjustOffset(adjustment); 267 } 268 } 269 } 270 return true; 271 } 272 273 public AudioMarker addAudioMarker(double time, LatLon coor) { 274 // find first audio marker to get absolute start time 275 double offset = 0.0; 276 AudioMarker am = null; 277 for (Marker m : data) { 278 if (m.getClass() == AudioMarker.class) { 279 am = (AudioMarker)m; 280 offset = time - am.time; 281 break; 282 } 283 } 284 if (am == null) { 285 JOptionPane.showMessageDialog( 286 Main.parent, 287 tr("No existing audio markers in this layer to offset from."), 288 tr("Error"), 289 JOptionPane.ERROR_MESSAGE 290 ); 291 return null; 292 } 293 294 // make our new marker 295 AudioMarker newAudioMarker = new AudioMarker(coor, 296 null, AudioPlayer.url(), this, time, offset); 297 298 // insert it at the right place in a copy the collection 299 Collection<Marker> newData = new ArrayList<Marker>(); 300 am = null; 301 AudioMarker ret = newAudioMarker; // save to have return value 302 for (Marker m : data) { 303 if (m.getClass() == AudioMarker.class) { 304 am = (AudioMarker) m; 305 if (newAudioMarker != null && offset < am.offset) { 306 newAudioMarker.adjustOffset(am.syncOffset()); // i.e. same as predecessor 307 newData.add(newAudioMarker); 308 newAudioMarker = null; 309 } 310 } 311 newData.add(m); 312 } 313 314 if (newAudioMarker != null) { 315 if (am != null) { 316 newAudioMarker.adjustOffset(am.syncOffset()); // i.e. same as predecessor 317 } 318 newData.add(newAudioMarker); // insert at end 319 } 320 321 // replace the collection 322 data.clear(); 323 data.addAll(newData); 324 return ret; 325 } 326 327 public void jumpToNextMarker() { 328 if (currentMarker == null) { 329 currentMarker = data.get(0); 330 } else { 331 boolean foundCurrent = false; 332 for (Marker m: data) { 333 if (foundCurrent) { 334 currentMarker = m; 335 break; 336 } else if (currentMarker == m) { 337 foundCurrent = true; 338 } 339 } 340 } 341 Main.map.mapView.zoomTo(currentMarker.getEastNorth()); 342 } 343 344 public void jumpToPreviousMarker() { 345 if (currentMarker == null) { 346 currentMarker = data.get(data.size() - 1); 347 } else { 348 boolean foundCurrent = false; 349 for (int i=data.size() - 1; i>=0; i--) { 350 Marker m = data.get(i); 351 if (foundCurrent) { 352 currentMarker = m; 353 break; 354 } else if (currentMarker == m) { 355 foundCurrent = true; 356 } 357 } 358 } 359 Main.map.mapView.zoomTo(currentMarker.getEastNorth()); 360 } 361 362 public static void playAudio() { 363 playAdjacentMarker(null, true); 364 } 365 366 public static void playNextMarker() { 367 playAdjacentMarker(AudioMarker.recentlyPlayedMarker(), true); 368 } 369 370 public static void playPreviousMarker() { 371 playAdjacentMarker(AudioMarker.recentlyPlayedMarker(), false); 372 } 373 374 private static Marker getAdjacentMarker(Marker startMarker, boolean next, Layer layer) { 375 Marker previousMarker = null; 376 boolean nextTime = false; 377 if (layer.getClass() == MarkerLayer.class) { 378 MarkerLayer markerLayer = (MarkerLayer) layer; 379 for (Marker marker : markerLayer.data) { 380 if (marker == startMarker) { 381 if (next) { 382 nextTime = true; 383 } else { 384 if (previousMarker == null) { 385 previousMarker = startMarker; // if no previous one, play the first one again 386 } 387 return previousMarker; 388 } 389 } 390 else if (marker.getClass() == AudioMarker.class) 391 { 392 if(nextTime || startMarker == null) 393 return marker; 394 previousMarker = marker; 395 } 396 } 397 if (nextTime) // there was no next marker in that layer, so play the last one again 398 return startMarker; 399 } 400 return null; 401 } 402 403 private static void playAdjacentMarker(Marker startMarker, boolean next) { 404 Marker m = null; 405 if (Main.map == null || Main.map.mapView == null) 406 return; 407 Layer l = Main.map.mapView.getActiveLayer(); 408 if(l != null) { 409 m = getAdjacentMarker(startMarker, next, l); 410 } 411 if(m == null) 412 { 413 for (Layer layer : Main.map.mapView.getAllLayers()) 414 { 415 m = getAdjacentMarker(startMarker, next, layer); 416 if(m != null) { 417 break; 418 } 419 } 420 } 421 if(m != null) { 422 ((AudioMarker)m).play(); 423 } 424 } 425 426 /** 427 * Get state of text display. 428 * @return <code>true</code> if text should be shown, <code>false</code> otherwise. 429 */ 430 private boolean isTextOrIconShown() { 431 String current = Main.pref.get("marker.show "+getName(),"show"); 432 return "show".equalsIgnoreCase(current); 433 } 434 435 public static final class ShowHideMarkerText extends AbstractAction implements LayerAction { 436 private final MarkerLayer layer; 437 438 public ShowHideMarkerText(MarkerLayer layer) { 439 super(tr("Show Text/Icons"), ImageProvider.get("dialogs", "showhide")); 440 putValue(SHORT_DESCRIPTION, tr("Toggle visible state of the marker text and icons.")); 441 putValue("help", ht("/Action/ShowHideTextIcons")); 442 this.layer = layer; 443 } 444 445 446 public void actionPerformed(ActionEvent e) { 447 Main.pref.put("marker.show "+layer.getName(), layer.isTextOrIconShown() ? "hide" : "show"); 448 Main.map.mapView.repaint(); 449 } 450 451 452 @Override 453 public Component createMenuComponent() { 454 JCheckBoxMenuItem showMarkerTextItem = new JCheckBoxMenuItem(this); 455 showMarkerTextItem.setState(layer.isTextOrIconShown()); 456 return showMarkerTextItem; 457 } 458 459 @Override 460 public boolean supportLayers(List<Layer> layers) { 461 return layers.size() == 1 && layers.get(0) instanceof MarkerLayer; 462 } 463 } 464 465 466 private class SynchronizeAudio extends AbstractAction { 467 468 public SynchronizeAudio() { 469 super(tr("Synchronize Audio"), ImageProvider.get("audio-sync")); 470 putValue("help", ht("/Action/SynchronizeAudio")); 471 } 472 473 @Override 474 public void actionPerformed(ActionEvent e) { 475 if (! AudioPlayer.paused()) { 476 JOptionPane.showMessageDialog( 477 Main.parent, 478 tr("You need to pause audio at the moment when you hear your synchronization cue."), 479 tr("Warning"), 480 JOptionPane.WARNING_MESSAGE 481 ); 482 return; 483 } 484 AudioMarker recent = AudioMarker.recentlyPlayedMarker(); 485 if (synchronizeAudioMarkers(recent)) { 486 JOptionPane.showMessageDialog( 487 Main.parent, 488 tr("Audio synchronized at point {0}.", recent.getText()), 489 tr("Information"), 490 JOptionPane.INFORMATION_MESSAGE 491 ); 492 } else { 493 JOptionPane.showMessageDialog( 494 Main.parent, 495 tr("Unable to synchronize in layer being played."), 496 tr("Error"), 497 JOptionPane.ERROR_MESSAGE 498 ); 499 } 500 } 501 } 502 503 private class MoveAudio extends AbstractAction { 504 505 public MoveAudio() { 506 super(tr("Make Audio Marker at Play Head"), ImageProvider.get("addmarkers")); 507 putValue("help", ht("/Action/MakeAudioMarkerAtPlayHead")); 508 } 509 510 @Override 511 public void actionPerformed(ActionEvent e) { 512 if (! AudioPlayer.paused()) { 513 JOptionPane.showMessageDialog( 514 Main.parent, 515 tr("You need to have paused audio at the point on the track where you want the marker."), 516 tr("Warning"), 517 JOptionPane.WARNING_MESSAGE 518 ); 519 return; 520 } 521 PlayHeadMarker playHeadMarker = Main.map.mapView.playHeadMarker; 522 if (playHeadMarker == null) 523 return; 524 addAudioMarker(playHeadMarker.time, playHeadMarker.getCoor()); 525 Main.map.mapView.repaint(); 526 } 527 } 528 529 }