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 }