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