source: josm/trunk/src/org/openstreetmap/josm/gui/layer/markerlayer/MarkerLayer.java@ 7326

Last change on this file since 7326 was 7326, checked in by Don-vip, 10 years ago

fix #10292 - allow to load a session with NMEA file + enhance reading/writing unit tests for sessions

  • Property svn:eol-style set to native
File size: 20.3 KB
RevLine 
[6380]1// License: GPL. For details, see LICENSE file.
[626]2package org.openstreetmap.josm.gui.layer.markerlayer;
3
[3754]4import static org.openstreetmap.josm.gui.help.HelpUtil.ht;
[804]5import static org.openstreetmap.josm.tools.I18n.marktr;
[304]6import static org.openstreetmap.josm.tools.I18n.tr;
7import static org.openstreetmap.josm.tools.I18n.trn;
8
9import java.awt.Color;
10import java.awt.Component;
[2450]11import java.awt.Graphics2D;
[304]12import java.awt.Point;
13import java.awt.event.ActionEvent;
14import java.awt.event.MouseAdapter;
15import java.awt.event.MouseEvent;
16import java.io.File;
[6242]17import java.net.URI;
18import java.net.URISyntaxException;
[444]19import java.util.ArrayList;
[304]20import java.util.Collection;
[5564]21import java.util.Collections;
22import java.util.Comparator;
[3408]23import java.util.List;
[304]24
[1890]25import javax.swing.AbstractAction;
[3408]26import javax.swing.Action;
[304]27import javax.swing.Icon;
[3220]28import javax.swing.JCheckBoxMenuItem;
[304]29import javax.swing.JOptionPane;
30
31import org.openstreetmap.josm.Main;
32import org.openstreetmap.josm.actions.RenameLayerAction;
[2450]33import org.openstreetmap.josm.data.Bounds;
[1724]34import org.openstreetmap.josm.data.coor.LatLon;
[5684]35import org.openstreetmap.josm.data.gpx.Extensions;
[5681]36import org.openstreetmap.josm.data.gpx.GpxConstants;
[444]37import org.openstreetmap.josm.data.gpx.GpxData;
[4831]38import org.openstreetmap.josm.data.gpx.GpxLink;
[444]39import org.openstreetmap.josm.data.gpx.WayPoint;
[304]40import org.openstreetmap.josm.data.osm.visitor.BoundingXYVisitor;
41import org.openstreetmap.josm.gui.MapView;
42import org.openstreetmap.josm.gui.dialogs.LayerListDialog;
43import org.openstreetmap.josm.gui.dialogs.LayerListPopup;
[4230]44import org.openstreetmap.josm.gui.layer.CustomizeColor;
[582]45import org.openstreetmap.josm.gui.layer.GpxLayer;
[4751]46import org.openstreetmap.josm.gui.layer.JumpToMarkerActions.JumpToMarkerLayer;
47import org.openstreetmap.josm.gui.layer.JumpToMarkerActions.JumpToNextMarker;
48import org.openstreetmap.josm.gui.layer.JumpToMarkerActions.JumpToPreviousMarker;
[304]49import org.openstreetmap.josm.gui.layer.Layer;
[582]50import org.openstreetmap.josm.tools.AudioPlayer;
[304]51import org.openstreetmap.josm.tools.ImageProvider;
[626]52
53/**
54 * A layer holding markers.
[1169]55 *
[626]56 * Markers are GPS points with a name and, optionally, a symbol code attached;
57 * marker layers can be created from waypoints when importing raw GPS data,
58 * but they may also come from other sources.
[1169]59 *
[626]60 * The symbol code is for future use.
[1169]61 *
[626]62 * The data is read only.
63 */
[4751]64public class MarkerLayer extends Layer implements JumpToMarkerLayer {
[626]65
[1169]66 /**
67 * A list of markers.
68 */
[4595]69 public final List<Marker> data;
[1169]70 private boolean mousePressed = false;
71 public GpxLayer fromLayer = null;
[4595]72 private Marker currentMarker;
[7310]73 public AudioMarker syncAudioMarker = null;
[547]74
[7326]75 /**
76 * Constructs a new {@code MarkerLayer}.
77 * @param indata The GPX data for this layer
78 * @param name The marker layer name
79 * @param associatedFile The associated GPX file
80 * @param fromLayer The associated GPX layer
81 */
[5501]82 public MarkerLayer(GpxData indata, String name, File associatedFile, GpxLayer fromLayer) {
[1169]83 super(name);
[1646]84 this.setAssociatedFile(associatedFile);
[7005]85 this.data = new ArrayList<>();
[1169]86 this.fromLayer = fromLayer;
87 double firstTime = -1.0;
[4831]88 String lastLinkedFile = "";
[1169]89
90 for (WayPoint wpt : indata.waypoints) {
[4831]91 /* calculate time differences in waypoints */
[1169]92 double time = wpt.time;
[5681]93 boolean wpt_has_link = wpt.attr.containsKey(GpxConstants.META_LINKS);
[4831]94 if (firstTime < 0 && wpt_has_link) {
[1169]95 firstTime = time;
[5681]96 for (Object oneLink : wpt.getCollection(GpxConstants.META_LINKS)) {
[5502]97 if (oneLink instanceof GpxLink) {
98 lastLinkedFile = ((GpxLink)oneLink).uri;
99 break;
100 }
[4831]101 }
[1169]102 }
[4831]103 if (wpt_has_link) {
[5681]104 for (Object oneLink : wpt.getCollection(GpxConstants.META_LINKS)) {
[5502]105 if (oneLink instanceof GpxLink) {
106 String uri = ((GpxLink)oneLink).uri;
107 if (!uri.equals(lastLinkedFile)) {
108 firstTime = time;
109 }
110 lastLinkedFile = uri;
111 break;
[4831]112 }
113 }
114 }
[5684]115 Double offset = null;
116 // If we have an explicit offset, take it.
117 // Otherwise, for a group of markers with the same Link-URI (e.g. an
118 // audio file) calculate the offset relative to the first marker of
119 // that group. This way the user can jump to the corresponding
120 // playback positions in a long audio track.
121 Extensions exts = (Extensions) wpt.get(GpxConstants.META_EXTENSIONS);
122 if (exts != null && exts.containsKey("offset")) {
123 try {
124 offset = Double.parseDouble(exts.get("offset"));
[6310]125 } catch (NumberFormatException nfe) {
126 Main.warn(nfe);
127 }
[5684]128 }
129 if (offset == null) {
130 offset = time - firstTime;
131 }
132 Marker m = Marker.createMarker(wpt, indata.storageFile, this, time, offset);
[1865]133 if (m != null) {
[1169]134 data.add(m);
[1865]135 }
[1169]136 }
[4710]137 }
138
[5501]139 @Override
140 public void hookUpMapView() {
[4710]141 Main.map.mapView.addMouseListener(new MouseAdapter() {
142 @Override public void mousePressed(MouseEvent e) {
143 if (e.getButton() != MouseEvent.BUTTON1)
144 return;
145 boolean mousePressedInButton = false;
146 if (e.getPoint() != null) {
147 for (Marker mkr : data) {
148 if (mkr.containsPoint(e.getPoint())) {
149 mousePressedInButton = true;
150 break;
[1169]151 }
152 }
[4710]153 }
154 if (! mousePressedInButton)
155 return;
156 mousePressed = true;
157 if (isVisible()) {
158 Main.map.mapView.repaint();
159 }
160 }
161 @Override public void mouseReleased(MouseEvent ev) {
162 if (ev.getButton() != MouseEvent.BUTTON1 || ! mousePressed)
163 return;
164 mousePressed = false;
165 if (!isVisible())
166 return;
167 if (ev.getPoint() != null) {
168 for (Marker mkr : data) {
169 if (mkr.containsPoint(ev.getPoint())) {
170 mkr.actionPerformed(new ActionEvent(this, 0, null));
[1169]171 }
172 }
[4710]173 }
174 Main.map.mapView.repaint();
[1169]175 }
176 });
177 }
[626]178
[1169]179 /**
180 * Return a static icon.
181 */
[6883]182 @Override
183 public Icon getIcon() {
[1169]184 return ImageProvider.get("layer", "marker_small");
185 }
[626]186
[4230]187 @Override
[6883]188 public Color getColor(boolean ignoreCustom) {
[4230]189 String name = getName();
[1221]190 return Main.pref.getColor(marktr("gps marker"), name != null ? "layer "+name : null, Color.gray);
191 }
192
[4230]193 /* for preferences */
[6883]194 public static Color getGenericColor() {
[4230]195 return Main.pref.getColor(marktr("gps marker"), Color.gray);
196 }
197
[6883]198 @Override
199 public void paint(Graphics2D g, MapView mv, Bounds box) {
[3237]200 boolean showTextOrIcon = isTextOrIconShown();
[4230]201 g.setColor(getColor(true));
[626]202
[2884]203 if (mousePressed) {
204 boolean mousePressedTmp = mousePressed;
205 Point mousePos = mv.getMousePosition(); // Get mouse position only when necessary (it's the slowest part of marker layer painting)
206 for (Marker mkr : data) {
207 if (mousePos != null && mkr.containsPoint(mousePos)) {
[3237]208 mkr.paint(g, mv, mousePressedTmp, showTextOrIcon);
[2884]209 mousePressedTmp = false;
210 }
211 }
212 } else {
213 for (Marker mkr : data) {
[3237]214 mkr.paint(g, mv, false, showTextOrIcon);
[1169]215 }
216 }
217 }
[626]218
[1169]219 @Override public String getToolTipText() {
220 return data.size()+" "+trn("marker", "markers", data.size());
221 }
[626]222
[1169]223 @Override public void mergeFrom(Layer from) {
224 MarkerLayer layer = (MarkerLayer)from;
225 data.addAll(layer.data);
[5564]226 Collections.sort(data, new Comparator<Marker>() {
227 @Override
228 public int compare(Marker o1, Marker o2) {
229 return Double.compare(o1.time, o2.time);
230 }
231 });
[1169]232 }
[547]233
[1169]234 @Override public boolean isMergable(Layer other) {
235 return other instanceof MarkerLayer;
236 }
[626]237
[1169]238 @Override public void visitBoundingBox(BoundingXYVisitor v) {
[1865]239 for (Marker mkr : data) {
[1724]240 v.visit(mkr.getEastNorth());
[1865]241 }
[1169]242 }
[626]243
[1169]244 @Override public Object getInfoComponent() {
[1890]245 return "<html>"+trn("{0} consists of {1} marker", "{0} consists of {1} markers", data.size(), getName(), data.size()) + "</html>";
[1169]246 }
[547]247
[3408]248 @Override public Action[] getMenuEntries() {
[7005]249 Collection<Action> components = new ArrayList<>();
[3408]250 components.add(LayerListDialog.getInstance().createShowHideLayerAction());
251 components.add(new ShowHideMarkerText(this));
252 components.add(LayerListDialog.getInstance().createDeleteLayerAction());
253 components.add(SeparatorLayerAction.INSTANCE);
[4230]254 components.add(new CustomizeColor(this));
[3408]255 components.add(SeparatorLayerAction.INSTANCE);
256 components.add(new SynchronizeAudio());
[1169]257 if (Main.pref.getBoolean("marker.traceaudio", true)) {
[3408]258 components.add (new MoveAudio());
[1169]259 }
[4595]260 components.add(new JumpToNextMarker(this));
261 components.add(new JumpToPreviousMarker(this));
[3408]262 components.add(new RenameLayerAction(getAssociatedFile(), this));
263 components.add(SeparatorLayerAction.INSTANCE);
264 components.add(new LayerListPopup.InfoAction(this));
[6083]265 return components.toArray(new Action[components.size()]);
[1169]266 }
[762]267
[7310]268 public boolean synchronizeAudioMarkers(final AudioMarker startMarker) {
269 syncAudioMarker = startMarker;
270 if (syncAudioMarker != null && ! data.contains(syncAudioMarker)) {
271 syncAudioMarker = null;
[1169]272 }
[7310]273 if (syncAudioMarker == null) {
[1169]274 // find the first audioMarker in this layer
275 for (Marker m : data) {
276 if (m instanceof AudioMarker) {
[7310]277 syncAudioMarker = (AudioMarker) m;
[1169]278 break;
279 }
280 }
281 }
[7310]282 if (syncAudioMarker == null)
[1169]283 return false;
[762]284
[1169]285 // apply adjustment to all subsequent audio markers in the layer
[7310]286 double adjustment = AudioPlayer.position() - syncAudioMarker.offset; // in seconds
[1169]287 boolean seenStart = false;
[6242]288 try {
[7310]289 URI uri = syncAudioMarker.url().toURI();
[6242]290 for (Marker m : data) {
[7310]291 if (m == syncAudioMarker) {
[6242]292 seenStart = true;
[1865]293 }
[6242]294 if (seenStart && m instanceof AudioMarker) {
295 AudioMarker ma = (AudioMarker) m;
296 // Do not ever call URL.equals but use URI.equals instead to avoid Internet connection
297 // See http://michaelscharf.blogspot.fr/2006/11/javaneturlequals-and-hashcode-make.html for details
298 if (ma.url().toURI().equals(uri)) {
299 ma.adjustOffset(adjustment);
300 }
301 }
[1169]302 }
[6242]303 } catch (URISyntaxException e) {
[6248]304 Main.warn(e);
[1169]305 }
306 return true;
307 }
[762]308
[1724]309 public AudioMarker addAudioMarker(double time, LatLon coor) {
[1169]310 // find first audio marker to get absolute start time
311 double offset = 0.0;
312 AudioMarker am = null;
313 for (Marker m : data) {
314 if (m.getClass() == AudioMarker.class) {
315 am = (AudioMarker)m;
316 offset = time - am.time;
317 break;
318 }
319 }
320 if (am == null) {
[2017]321 JOptionPane.showMessageDialog(
[1865]322 Main.parent,
323 tr("No existing audio markers in this layer to offset from."),
324 tr("Error"),
325 JOptionPane.ERROR_MESSAGE
[4282]326 );
[1169]327 return null;
328 }
[547]329
[1169]330 // make our new marker
[4282]331 AudioMarker newAudioMarker = new AudioMarker(coor,
332 null, AudioPlayer.url(), this, time, offset);
[1169]333
334 // insert it at the right place in a copy the collection
[7005]335 Collection<Marker> newData = new ArrayList<>();
[1169]336 am = null;
337 AudioMarker ret = newAudioMarker; // save to have return value
338 for (Marker m : data) {
339 if (m.getClass() == AudioMarker.class) {
340 am = (AudioMarker) m;
341 if (newAudioMarker != null && offset < am.offset) {
342 newAudioMarker.adjustOffset(am.syncOffset()); // i.e. same as predecessor
343 newData.add(newAudioMarker);
344 newAudioMarker = null;
345 }
346 }
347 newData.add(m);
348 }
349
350 if (newAudioMarker != null) {
[1865]351 if (am != null) {
[1169]352 newAudioMarker.adjustOffset(am.syncOffset()); // i.e. same as predecessor
[1865]353 }
[1169]354 newData.add(newAudioMarker); // insert at end
355 }
356
357 // replace the collection
358 data.clear();
359 data.addAll(newData);
360 return ret;
361 }
362
[6084]363 @Override
[4595]364 public void jumpToNextMarker() {
365 if (currentMarker == null) {
366 currentMarker = data.get(0);
367 } else {
368 boolean foundCurrent = false;
369 for (Marker m: data) {
370 if (foundCurrent) {
371 currentMarker = m;
372 break;
373 } else if (currentMarker == m) {
374 foundCurrent = true;
375 }
376 }
377 }
378 Main.map.mapView.zoomTo(currentMarker.getEastNorth());
379 }
380
[6084]381 @Override
[4595]382 public void jumpToPreviousMarker() {
383 if (currentMarker == null) {
384 currentMarker = data.get(data.size() - 1);
385 } else {
386 boolean foundCurrent = false;
387 for (int i=data.size() - 1; i>=0; i--) {
388 Marker m = data.get(i);
389 if (foundCurrent) {
390 currentMarker = m;
391 break;
392 } else if (currentMarker == m) {
393 foundCurrent = true;
394 }
395 }
396 }
397 Main.map.mapView.zoomTo(currentMarker.getEastNorth());
398 }
399
[1169]400 public static void playAudio() {
[1685]401 playAdjacentMarker(null, true);
[1169]402 }
403
404 public static void playNextMarker() {
[1685]405 playAdjacentMarker(AudioMarker.recentlyPlayedMarker(), true);
[1169]406 }
407
408 public static void playPreviousMarker() {
[1685]409 playAdjacentMarker(AudioMarker.recentlyPlayedMarker(), false);
[1169]410 }
411
[1685]412 private static Marker getAdjacentMarker(Marker startMarker, boolean next, Layer layer) {
[1169]413 Marker previousMarker = null;
414 boolean nextTime = false;
[1685]415 if (layer.getClass() == MarkerLayer.class) {
416 MarkerLayer markerLayer = (MarkerLayer) layer;
417 for (Marker marker : markerLayer.data) {
418 if (marker == startMarker) {
[1865]419 if (next) {
[1685]420 nextTime = true;
[1865]421 } else {
422 if (previousMarker == null) {
[1685]423 previousMarker = startMarker; // if no previous one, play the first one again
[1865]424 }
[1685]425 return previousMarker;
[1169]426 }
427 }
[1685]428 else if (marker.getClass() == AudioMarker.class)
429 {
430 if(nextTime || startMarker == null)
431 return marker;
432 previousMarker = marker;
[1169]433 }
434 }
[1685]435 if (nextTime) // there was no next marker in that layer, so play the last one again
436 return startMarker;
[1169]437 }
[1685]438 return null;
[1169]439 }
440
[1685]441 private static void playAdjacentMarker(Marker startMarker, boolean next) {
442 Marker m = null;
[6336]443 if (!Main.isDisplayingMapView())
[1685]444 return;
445 Layer l = Main.map.mapView.getActiveLayer();
[1865]446 if(l != null) {
[1685]447 m = getAdjacentMarker(startMarker, next, l);
[1865]448 }
[1685]449 if(m == null)
450 {
451 for (Layer layer : Main.map.mapView.getAllLayers())
452 {
453 m = getAdjacentMarker(startMarker, next, layer);
[1865]454 if(m != null) {
[1685]455 break;
[1865]456 }
[1685]457 }
458 }
[1865]459 if(m != null) {
[1685]460 ((AudioMarker)m).play();
[1865]461 }
[1685]462 }
463
[5481]464 /**
465 * Get state of text display.
466 * @return <code>true</code> if text should be shown, <code>false</code> otherwise.
467 */
[3237]468 private boolean isTextOrIconShown() {
[3220]469 String current = Main.pref.get("marker.show "+getName(),"show");
[3237]470 return "show".equalsIgnoreCase(current);
[3220]471 }
472
[3408]473 public static final class ShowHideMarkerText extends AbstractAction implements LayerAction {
[3220]474 private final MarkerLayer layer;
[1890]475
[3220]476 public ShowHideMarkerText(MarkerLayer layer) {
477 super(tr("Show Text/Icons"), ImageProvider.get("dialogs", "showhide"));
[1890]478 putValue(SHORT_DESCRIPTION, tr("Toggle visible state of the marker text and icons."));
[3754]479 putValue("help", ht("/Action/ShowHideTextIcons"));
[1890]480 this.layer = layer;
481 }
482
[3220]483
[6084]484 @Override
[1890]485 public void actionPerformed(ActionEvent e) {
[3237]486 Main.pref.put("marker.show "+layer.getName(), layer.isTextOrIconShown() ? "hide" : "show");
[1890]487 Main.map.mapView.repaint();
488 }
[3408]489
490
491 @Override
492 public Component createMenuComponent() {
493 JCheckBoxMenuItem showMarkerTextItem = new JCheckBoxMenuItem(this);
494 showMarkerTextItem.setState(layer.isTextOrIconShown());
495 return showMarkerTextItem;
496 }
497
498 @Override
499 public boolean supportLayers(List<Layer> layers) {
500 return layers.size() == 1 && layers.get(0) instanceof MarkerLayer;
501 }
[1890]502 }
[3408]503
[4595]504
[3408]505 private class SynchronizeAudio extends AbstractAction {
506
507 public SynchronizeAudio() {
508 super(tr("Synchronize Audio"), ImageProvider.get("audio-sync"));
[3754]509 putValue("help", ht("/Action/SynchronizeAudio"));
[3408]510 }
511
512 @Override
513 public void actionPerformed(ActionEvent e) {
514 if (! AudioPlayer.paused()) {
515 JOptionPane.showMessageDialog(
516 Main.parent,
517 tr("You need to pause audio at the moment when you hear your synchronization cue."),
518 tr("Warning"),
519 JOptionPane.WARNING_MESSAGE
[4282]520 );
[3408]521 return;
522 }
523 AudioMarker recent = AudioMarker.recentlyPlayedMarker();
524 if (synchronizeAudioMarkers(recent)) {
525 JOptionPane.showMessageDialog(
526 Main.parent,
[7310]527 tr("Audio synchronized at point {0}.", syncAudioMarker.getText()),
[3408]528 tr("Information"),
529 JOptionPane.INFORMATION_MESSAGE
[4282]530 );
[3408]531 } else {
532 JOptionPane.showMessageDialog(
533 Main.parent,
534 tr("Unable to synchronize in layer being played."),
535 tr("Error"),
536 JOptionPane.ERROR_MESSAGE
[4282]537 );
[3408]538 }
539 }
540 }
541
542 private class MoveAudio extends AbstractAction {
543
544 public MoveAudio() {
545 super(tr("Make Audio Marker at Play Head"), ImageProvider.get("addmarkers"));
[3754]546 putValue("help", ht("/Action/MakeAudioMarkerAtPlayHead"));
[3408]547 }
548
549 @Override
550 public void actionPerformed(ActionEvent e) {
551 if (! AudioPlayer.paused()) {
552 JOptionPane.showMessageDialog(
553 Main.parent,
554 tr("You need to have paused audio at the point on the track where you want the marker."),
555 tr("Warning"),
556 JOptionPane.WARNING_MESSAGE
[4282]557 );
[3408]558 return;
559 }
560 PlayHeadMarker playHeadMarker = Main.map.mapView.playHeadMarker;
561 if (playHeadMarker == null)
562 return;
563 addAudioMarker(playHeadMarker.time, playHeadMarker.getCoor());
564 Main.map.mapView.repaint();
565 }
566 }
[4595]567
[626]568}
Note: See TracBrowser for help on using the repository browser.