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

Last change on this file since 10364 was 10364, checked in by stoecker, 8 years ago

gsoc-core - patch by Michael Zangl - see #12953 - remove deprecation usage

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