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