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 public AudioMarker syncAudioMarker = null; 074 075 /** 076 * Constructs a new {@code MarkerLayer}. 077 * @param indata The GPX data for this layer 078 * @param name The marker layer name 079 * @param associatedFile The associated GPX file 080 * @param fromLayer The associated GPX layer 081 */ 082 public MarkerLayer(GpxData indata, String name, File associatedFile, GpxLayer fromLayer) { 083 super(name); 084 this.setAssociatedFile(associatedFile); 085 this.data = new ArrayList<>(); 086 this.fromLayer = fromLayer; 087 double firstTime = -1.0; 088 String lastLinkedFile = ""; 089 090 for (WayPoint wpt : indata.waypoints) { 091 /* calculate time differences in waypoints */ 092 double time = wpt.time; 093 boolean wpt_has_link = wpt.attr.containsKey(GpxConstants.META_LINKS); 094 if (firstTime < 0 && wpt_has_link) { 095 firstTime = time; 096 for (GpxLink oneLink : wpt.<GpxLink>getCollection(GpxConstants.META_LINKS)) { 097 lastLinkedFile = oneLink.uri; 098 break; 099 } 100 } 101 if (wpt_has_link) { 102 for (GpxLink oneLink : wpt.<GpxLink>getCollection(GpxConstants.META_LINKS)) { 103 String uri = oneLink.uri; 104 if (!uri.equals(lastLinkedFile)) { 105 firstTime = time; 106 } 107 lastLinkedFile = uri; 108 break; 109 } 110 } 111 Double offset = null; 112 // If we have an explicit offset, take it. 113 // Otherwise, for a group of markers with the same Link-URI (e.g. an 114 // audio file) calculate the offset relative to the first marker of 115 // that group. This way the user can jump to the corresponding 116 // playback positions in a long audio track. 117 Extensions exts = (Extensions) wpt.get(GpxConstants.META_EXTENSIONS); 118 if (exts != null && exts.containsKey("offset")) { 119 try { 120 offset = Double.parseDouble(exts.get("offset")); 121 } catch (NumberFormatException nfe) { 122 Main.warn(nfe); 123 } 124 } 125 if (offset == null) { 126 offset = time - firstTime; 127 } 128 Marker m = Marker.createMarker(wpt, indata.storageFile, this, time, offset); 129 if (m != null) { 130 data.add(m); 131 } 132 } 133 } 134 135 @Override 136 public void hookUpMapView() { 137 Main.map.mapView.addMouseListener(new MouseAdapter() { 138 @Override public void mousePressed(MouseEvent e) { 139 if (e.getButton() != MouseEvent.BUTTON1) 140 return; 141 boolean mousePressedInButton = false; 142 if (e.getPoint() != null) { 143 for (Marker mkr : data) { 144 if (mkr.containsPoint(e.getPoint())) { 145 mousePressedInButton = true; 146 break; 147 } 148 } 149 } 150 if (! mousePressedInButton) 151 return; 152 mousePressed = true; 153 if (isVisible()) { 154 Main.map.mapView.repaint(); 155 } 156 } 157 @Override public void mouseReleased(MouseEvent ev) { 158 if (ev.getButton() != MouseEvent.BUTTON1 || ! mousePressed) 159 return; 160 mousePressed = false; 161 if (!isVisible()) 162 return; 163 if (ev.getPoint() != null) { 164 for (Marker mkr : data) { 165 if (mkr.containsPoint(ev.getPoint())) { 166 mkr.actionPerformed(new ActionEvent(this, 0, null)); 167 } 168 } 169 } 170 Main.map.mapView.repaint(); 171 } 172 }); 173 } 174 175 /** 176 * Return a static icon. 177 */ 178 @Override 179 public Icon getIcon() { 180 return ImageProvider.get("layer", "marker_small"); 181 } 182 183 @Override 184 public Color getColor(boolean ignoreCustom) { 185 String name = getName(); 186 return Main.pref.getColor(marktr("gps marker"), name != null ? "layer "+name : null, Color.gray); 187 } 188 189 /* for preferences */ 190 public static Color getGenericColor() { 191 return Main.pref.getColor(marktr("gps marker"), Color.gray); 192 } 193 194 @Override 195 public void paint(Graphics2D g, MapView mv, Bounds box) { 196 boolean showTextOrIcon = isTextOrIconShown(); 197 g.setColor(getColor(true)); 198 199 if (mousePressed) { 200 boolean mousePressedTmp = mousePressed; 201 Point mousePos = mv.getMousePosition(); // Get mouse position only when necessary (it's the slowest part of marker layer painting) 202 for (Marker mkr : data) { 203 if (mousePos != null && mkr.containsPoint(mousePos)) { 204 mkr.paint(g, mv, mousePressedTmp, showTextOrIcon); 205 mousePressedTmp = false; 206 } 207 } 208 } else { 209 for (Marker mkr : data) { 210 mkr.paint(g, mv, false, showTextOrIcon); 211 } 212 } 213 } 214 215 @Override public String getToolTipText() { 216 return data.size()+" "+trn("marker", "markers", data.size()); 217 } 218 219 @Override public void mergeFrom(Layer from) { 220 MarkerLayer layer = (MarkerLayer)from; 221 data.addAll(layer.data); 222 Collections.sort(data, new Comparator<Marker>() { 223 @Override 224 public int compare(Marker o1, Marker o2) { 225 return Double.compare(o1.time, o2.time); 226 } 227 }); 228 } 229 230 @Override public boolean isMergable(Layer other) { 231 return other instanceof MarkerLayer; 232 } 233 234 @Override public void visitBoundingBox(BoundingXYVisitor v) { 235 for (Marker mkr : data) { 236 v.visit(mkr.getEastNorth()); 237 } 238 } 239 240 @Override public Object getInfoComponent() { 241 return "<html>"+trn("{0} consists of {1} marker", "{0} consists of {1} markers", data.size(), getName(), data.size()) + "</html>"; 242 } 243 244 @Override public Action[] getMenuEntries() { 245 Collection<Action> components = new ArrayList<>(); 246 components.add(LayerListDialog.getInstance().createShowHideLayerAction()); 247 components.add(new ShowHideMarkerText(this)); 248 components.add(LayerListDialog.getInstance().createDeleteLayerAction()); 249 components.add(SeparatorLayerAction.INSTANCE); 250 components.add(new CustomizeColor(this)); 251 components.add(SeparatorLayerAction.INSTANCE); 252 components.add(new SynchronizeAudio()); 253 if (Main.pref.getBoolean("marker.traceaudio", true)) { 254 components.add (new MoveAudio()); 255 } 256 components.add(new JumpToNextMarker(this)); 257 components.add(new JumpToPreviousMarker(this)); 258 components.add(new RenameLayerAction(getAssociatedFile(), this)); 259 components.add(SeparatorLayerAction.INSTANCE); 260 components.add(new LayerListPopup.InfoAction(this)); 261 return components.toArray(new Action[components.size()]); 262 } 263 264 public boolean synchronizeAudioMarkers(final AudioMarker startMarker) { 265 syncAudioMarker = startMarker; 266 if (syncAudioMarker != null && ! data.contains(syncAudioMarker)) { 267 syncAudioMarker = null; 268 } 269 if (syncAudioMarker == null) { 270 // find the first audioMarker in this layer 271 for (Marker m : data) { 272 if (m instanceof AudioMarker) { 273 syncAudioMarker = (AudioMarker) m; 274 break; 275 } 276 } 277 } 278 if (syncAudioMarker == null) 279 return false; 280 281 // apply adjustment to all subsequent audio markers in the layer 282 double adjustment = AudioPlayer.position() - syncAudioMarker.offset; // in seconds 283 boolean seenStart = false; 284 try { 285 URI uri = syncAudioMarker.url().toURI(); 286 for (Marker m : data) { 287 if (m == syncAudioMarker) { 288 seenStart = true; 289 } 290 if (seenStart && m instanceof AudioMarker) { 291 AudioMarker ma = (AudioMarker) m; 292 // Do not ever call URL.equals but use URI.equals instead to avoid Internet connection 293 // See http://michaelscharf.blogspot.fr/2006/11/javaneturlequals-and-hashcode-make.html for details 294 if (ma.url().toURI().equals(uri)) { 295 ma.adjustOffset(adjustment); 296 } 297 } 298 } 299 } catch (URISyntaxException e) { 300 Main.warn(e); 301 } 302 return true; 303 } 304 305 public AudioMarker addAudioMarker(double time, LatLon coor) { 306 // find first audio marker to get absolute start time 307 double offset = 0.0; 308 AudioMarker am = null; 309 for (Marker m : data) { 310 if (m.getClass() == AudioMarker.class) { 311 am = (AudioMarker)m; 312 offset = time - am.time; 313 break; 314 } 315 } 316 if (am == null) { 317 JOptionPane.showMessageDialog( 318 Main.parent, 319 tr("No existing audio markers in this layer to offset from."), 320 tr("Error"), 321 JOptionPane.ERROR_MESSAGE 322 ); 323 return null; 324 } 325 326 // make our new marker 327 AudioMarker newAudioMarker = new AudioMarker(coor, 328 null, AudioPlayer.url(), this, time, offset); 329 330 // insert it at the right place in a copy the collection 331 Collection<Marker> newData = new ArrayList<>(); 332 am = null; 333 AudioMarker ret = newAudioMarker; // save to have return value 334 for (Marker m : data) { 335 if (m.getClass() == AudioMarker.class) { 336 am = (AudioMarker) m; 337 if (newAudioMarker != null && offset < am.offset) { 338 newAudioMarker.adjustOffset(am.syncOffset()); // i.e. same as predecessor 339 newData.add(newAudioMarker); 340 newAudioMarker = null; 341 } 342 } 343 newData.add(m); 344 } 345 346 if (newAudioMarker != null) { 347 if (am != null) { 348 newAudioMarker.adjustOffset(am.syncOffset()); // i.e. same as predecessor 349 } 350 newData.add(newAudioMarker); // insert at end 351 } 352 353 // replace the collection 354 data.clear(); 355 data.addAll(newData); 356 return ret; 357 } 358 359 @Override 360 public void jumpToNextMarker() { 361 if (currentMarker == null) { 362 currentMarker = data.get(0); 363 } else { 364 boolean foundCurrent = false; 365 for (Marker m: data) { 366 if (foundCurrent) { 367 currentMarker = m; 368 break; 369 } else if (currentMarker == m) { 370 foundCurrent = true; 371 } 372 } 373 } 374 Main.map.mapView.zoomTo(currentMarker.getEastNorth()); 375 } 376 377 @Override 378 public void jumpToPreviousMarker() { 379 if (currentMarker == null) { 380 currentMarker = data.get(data.size() - 1); 381 } else { 382 boolean foundCurrent = false; 383 for (int i=data.size() - 1; i>=0; i--) { 384 Marker m = data.get(i); 385 if (foundCurrent) { 386 currentMarker = m; 387 break; 388 } else if (currentMarker == m) { 389 foundCurrent = true; 390 } 391 } 392 } 393 Main.map.mapView.zoomTo(currentMarker.getEastNorth()); 394 } 395 396 public static void playAudio() { 397 playAdjacentMarker(null, true); 398 } 399 400 public static void playNextMarker() { 401 playAdjacentMarker(AudioMarker.recentlyPlayedMarker(), true); 402 } 403 404 public static void playPreviousMarker() { 405 playAdjacentMarker(AudioMarker.recentlyPlayedMarker(), false); 406 } 407 408 private static Marker getAdjacentMarker(Marker startMarker, boolean next, Layer layer) { 409 Marker previousMarker = null; 410 boolean nextTime = false; 411 if (layer.getClass() == MarkerLayer.class) { 412 MarkerLayer markerLayer = (MarkerLayer) layer; 413 for (Marker marker : markerLayer.data) { 414 if (marker == startMarker) { 415 if (next) { 416 nextTime = true; 417 } else { 418 if (previousMarker == null) { 419 previousMarker = startMarker; // if no previous one, play the first one again 420 } 421 return previousMarker; 422 } 423 } 424 else if (marker.getClass() == AudioMarker.class) 425 { 426 if(nextTime || startMarker == null) 427 return marker; 428 previousMarker = marker; 429 } 430 } 431 if (nextTime) // there was no next marker in that layer, so play the last one again 432 return startMarker; 433 } 434 return null; 435 } 436 437 private static void playAdjacentMarker(Marker startMarker, boolean next) { 438 Marker m = null; 439 if (!Main.isDisplayingMapView()) 440 return; 441 Layer l = Main.map.mapView.getActiveLayer(); 442 if(l != null) { 443 m = getAdjacentMarker(startMarker, next, l); 444 } 445 if(m == null) 446 { 447 for (Layer layer : Main.map.mapView.getAllLayers()) 448 { 449 m = getAdjacentMarker(startMarker, next, layer); 450 if(m != null) { 451 break; 452 } 453 } 454 } 455 if(m != null) { 456 ((AudioMarker)m).play(); 457 } 458 } 459 460 /** 461 * Get state of text display. 462 * @return <code>true</code> if text should be shown, <code>false</code> otherwise. 463 */ 464 private boolean isTextOrIconShown() { 465 String current = Main.pref.get("marker.show "+getName(),"show"); 466 return "show".equalsIgnoreCase(current); 467 } 468 469 public static final class ShowHideMarkerText extends AbstractAction implements LayerAction { 470 private final MarkerLayer layer; 471 472 public ShowHideMarkerText(MarkerLayer layer) { 473 super(tr("Show Text/Icons"), ImageProvider.get("dialogs", "showhide")); 474 putValue(SHORT_DESCRIPTION, tr("Toggle visible state of the marker text and icons.")); 475 putValue("help", ht("/Action/ShowHideTextIcons")); 476 this.layer = layer; 477 } 478 479 480 @Override 481 public void actionPerformed(ActionEvent e) { 482 Main.pref.put("marker.show "+layer.getName(), layer.isTextOrIconShown() ? "hide" : "show"); 483 Main.map.mapView.repaint(); 484 } 485 486 487 @Override 488 public Component createMenuComponent() { 489 JCheckBoxMenuItem showMarkerTextItem = new JCheckBoxMenuItem(this); 490 showMarkerTextItem.setState(layer.isTextOrIconShown()); 491 return showMarkerTextItem; 492 } 493 494 @Override 495 public boolean supportLayers(List<Layer> layers) { 496 return layers.size() == 1 && layers.get(0) instanceof MarkerLayer; 497 } 498 } 499 500 501 private class SynchronizeAudio extends AbstractAction { 502 503 public SynchronizeAudio() { 504 super(tr("Synchronize Audio"), ImageProvider.get("audio-sync")); 505 putValue("help", ht("/Action/SynchronizeAudio")); 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 pause audio at the moment when you hear your synchronization cue."), 514 tr("Warning"), 515 JOptionPane.WARNING_MESSAGE 516 ); 517 return; 518 } 519 AudioMarker recent = AudioMarker.recentlyPlayedMarker(); 520 if (synchronizeAudioMarkers(recent)) { 521 JOptionPane.showMessageDialog( 522 Main.parent, 523 tr("Audio synchronized at point {0}.", syncAudioMarker.getText()), 524 tr("Information"), 525 JOptionPane.INFORMATION_MESSAGE 526 ); 527 } else { 528 JOptionPane.showMessageDialog( 529 Main.parent, 530 tr("Unable to synchronize in layer being played."), 531 tr("Error"), 532 JOptionPane.ERROR_MESSAGE 533 ); 534 } 535 } 536 } 537 538 private class MoveAudio extends AbstractAction { 539 540 public MoveAudio() { 541 super(tr("Make Audio Marker at Play Head"), ImageProvider.get("addmarkers")); 542 putValue("help", ht("/Action/MakeAudioMarkerAtPlayHead")); 543 } 544 545 @Override 546 public void actionPerformed(ActionEvent e) { 547 if (! AudioPlayer.paused()) { 548 JOptionPane.showMessageDialog( 549 Main.parent, 550 tr("You need to have paused audio at the point on the track where you want the marker."), 551 tr("Warning"), 552 JOptionPane.WARNING_MESSAGE 553 ); 554 return; 555 } 556 PlayHeadMarker playHeadMarker = Main.map.mapView.playHeadMarker; 557 if (playHeadMarker == null) 558 return; 559 addAudioMarker(playHeadMarker.time, playHeadMarker.getCoor()); 560 Main.map.mapView.repaint(); 561 } 562 } 563 564}