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

Last change on this file since 11892 was 11848, checked in by Don-vip, 7 years ago

fix #14613 - Special HTML characters not escaped in GUI error messages

  • Property svn:eol-style set to native
File size: 20.6 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.gui.layer.markerlayer;
3
4import static org.openstreetmap.josm.gui.help.HelpUtil.ht;
5import static org.openstreetmap.josm.tools.I18n.marktr;
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;
11import java.awt.Graphics2D;
12import java.awt.Point;
13import java.awt.event.ActionEvent;
14import java.awt.event.MouseAdapter;
15import java.awt.event.MouseEvent;
16import java.io.File;
17import java.net.URI;
18import java.net.URISyntaxException;
19import java.util.ArrayList;
20import java.util.Collection;
21import java.util.Comparator;
22import java.util.List;
23
24import javax.swing.AbstractAction;
25import javax.swing.Action;
26import javax.swing.Icon;
27import javax.swing.JCheckBoxMenuItem;
28import javax.swing.JOptionPane;
29
30import org.openstreetmap.josm.Main;
31import org.openstreetmap.josm.actions.RenameLayerAction;
32import org.openstreetmap.josm.data.Bounds;
33import org.openstreetmap.josm.data.coor.LatLon;
34import org.openstreetmap.josm.data.gpx.Extensions;
35import org.openstreetmap.josm.data.gpx.GpxConstants;
36import org.openstreetmap.josm.data.gpx.GpxData;
37import org.openstreetmap.josm.data.gpx.GpxLink;
38import org.openstreetmap.josm.data.gpx.WayPoint;
39import org.openstreetmap.josm.data.osm.visitor.BoundingXYVisitor;
40import org.openstreetmap.josm.data.preferences.ColorProperty;
41import org.openstreetmap.josm.gui.MapView;
42import org.openstreetmap.josm.gui.dialogs.LayerListDialog;
43import org.openstreetmap.josm.gui.dialogs.LayerListPopup;
44import org.openstreetmap.josm.gui.layer.CustomizeColor;
45import org.openstreetmap.josm.gui.layer.GpxLayer;
46import org.openstreetmap.josm.gui.layer.JumpToMarkerActions.JumpToMarkerLayer;
47import org.openstreetmap.josm.gui.layer.JumpToMarkerActions.JumpToNextMarker;
48import org.openstreetmap.josm.gui.layer.JumpToMarkerActions.JumpToPreviousMarker;
49import org.openstreetmap.josm.gui.layer.Layer;
50import org.openstreetmap.josm.gui.layer.gpx.ConvertToDataLayerAction;
51import org.openstreetmap.josm.tools.AudioPlayer;
52import org.openstreetmap.josm.tools.ImageProvider;
53import org.openstreetmap.josm.tools.Utils;
54
55/**
56 * A layer holding markers.
57 *
58 * Markers are GPS points with a name and, optionally, a symbol code attached;
59 * marker layers can be created from waypoints when importing raw GPS data,
60 * but they may also come from other sources.
61 *
62 * The symbol code is for future use.
63 *
64 * The data is read only.
65 */
66public class MarkerLayer extends Layer implements JumpToMarkerLayer {
67
68 /**
69 * A list of markers.
70 */
71 public final List<Marker> data;
72 private boolean mousePressed;
73 public GpxLayer fromLayer;
74 private Marker currentMarker;
75 public AudioMarker syncAudioMarker;
76
77 private static final Color DEFAULT_COLOR = Color.magenta;
78 private static final ColorProperty COLOR_PROPERTY = new ColorProperty(marktr("gps marker"), DEFAULT_COLOR);
79
80 /**
81 * Constructs a new {@code MarkerLayer}.
82 * @param indata The GPX data for this layer
83 * @param name The marker layer name
84 * @param associatedFile The associated GPX file
85 * @param fromLayer The associated GPX layer
86 */
87 public MarkerLayer(GpxData indata, String name, File associatedFile, GpxLayer fromLayer) {
88 super(name);
89 this.setAssociatedFile(associatedFile);
90 this.data = new ArrayList<>();
91 this.fromLayer = fromLayer;
92 double firstTime = -1.0;
93 String lastLinkedFile = "";
94
95 for (WayPoint wpt : indata.waypoints) {
96 /* calculate time differences in waypoints */
97 double time = wpt.time;
98 boolean wptHasLink = wpt.attr.containsKey(GpxConstants.META_LINKS);
99 if (firstTime < 0 && wptHasLink) {
100 firstTime = time;
101 for (GpxLink oneLink : wpt.<GpxLink>getCollection(GpxConstants.META_LINKS)) {
102 lastLinkedFile = oneLink.uri;
103 break;
104 }
105 }
106 if (wptHasLink) {
107 for (GpxLink oneLink : wpt.<GpxLink>getCollection(GpxConstants.META_LINKS)) {
108 String uri = oneLink.uri;
109 if (uri != null) {
110 if (!uri.equals(lastLinkedFile)) {
111 firstTime = time;
112 }
113 lastLinkedFile = uri;
114 break;
115 }
116 }
117 }
118 Double offset = null;
119 // If we have an explicit offset, take it.
120 // Otherwise, for a group of markers with the same Link-URI (e.g. an
121 // audio file) calculate the offset relative to the first marker of
122 // that group. This way the user can jump to the corresponding
123 // playback positions in a long audio track.
124 Extensions exts = (Extensions) wpt.get(GpxConstants.META_EXTENSIONS);
125 if (exts != null && exts.containsKey("offset")) {
126 try {
127 offset = Double.valueOf(exts.get("offset"));
128 } catch (NumberFormatException nfe) {
129 Main.warn(nfe);
130 }
131 }
132 if (offset == null) {
133 offset = time - firstTime;
134 }
135 final Collection<Marker> markers = Marker.createMarkers(wpt, indata.storageFile, this, time, offset);
136 if (markers != null) {
137 data.addAll(markers);
138 }
139 }
140 }
141
142 @Override
143 public LayerPainter attachToMapView(MapViewEvent event) {
144 event.getMapView().addMouseListener(new MouseAdapter() {
145 @Override
146 public void mousePressed(MouseEvent e) {
147 if (e.getButton() != MouseEvent.BUTTON1)
148 return;
149 boolean mousePressedInButton = false;
150 for (Marker mkr : data) {
151 if (mkr.containsPoint(e.getPoint())) {
152 mousePressedInButton = true;
153 break;
154 }
155 }
156 if (!mousePressedInButton)
157 return;
158 mousePressed = true;
159 if (isVisible()) {
160 invalidate();
161 }
162 }
163
164 @Override
165 public void mouseReleased(MouseEvent ev) {
166 if (ev.getButton() != MouseEvent.BUTTON1 || !mousePressed)
167 return;
168 mousePressed = false;
169 if (!isVisible())
170 return;
171 for (Marker mkr : data) {
172 if (mkr.containsPoint(ev.getPoint())) {
173 mkr.actionPerformed(new ActionEvent(this, 0, null));
174 }
175 }
176 invalidate();
177 }
178 });
179
180 if (event.getMapView().playHeadMarker == null) {
181 event.getMapView().playHeadMarker = PlayHeadMarker.create();
182 }
183
184 return super.attachToMapView(event);
185 }
186
187 /**
188 * Return a static icon.
189 */
190 @Override
191 public Icon getIcon() {
192 return ImageProvider.get("layer", "marker_small");
193 }
194
195 @Override
196 protected ColorProperty getBaseColorProperty() {
197 return COLOR_PROPERTY;
198 }
199
200 /* for preferences */
201 public static Color getGenericColor() {
202 return COLOR_PROPERTY.get();
203 }
204
205 @Override
206 public void paint(Graphics2D g, MapView mv, Bounds box) {
207 boolean showTextOrIcon = isTextOrIconShown();
208 g.setColor(getColorProperty().get());
209
210 if (mousePressed) {
211 boolean mousePressedTmp = mousePressed;
212 Point mousePos = mv.getMousePosition(); // Get mouse position only when necessary (it's the slowest part of marker layer painting)
213 for (Marker mkr : data) {
214 if (mousePos != null && mkr.containsPoint(mousePos)) {
215 mkr.paint(g, mv, mousePressedTmp, showTextOrIcon);
216 mousePressedTmp = false;
217 }
218 }
219 } else {
220 for (Marker mkr : data) {
221 mkr.paint(g, mv, false, showTextOrIcon);
222 }
223 }
224 }
225
226 @Override
227 public String getToolTipText() {
228 return Integer.toString(data.size())+' '+trn("marker", "markers", data.size());
229 }
230
231 @Override
232 public void mergeFrom(Layer from) {
233 if (from instanceof MarkerLayer) {
234 data.addAll(((MarkerLayer) from).data);
235 data.sort(Comparator.comparingDouble(o -> o.time));
236 }
237 }
238
239 @Override public boolean isMergable(Layer other) {
240 return other instanceof MarkerLayer;
241 }
242
243 @Override public void visitBoundingBox(BoundingXYVisitor v) {
244 for (Marker mkr : data) {
245 v.visit(mkr.getEastNorth());
246 }
247 }
248
249 @Override public Object getInfoComponent() {
250 return "<html>"+trn("{0} consists of {1} marker", "{0} consists of {1} markers",
251 data.size(), Utils.escapeReservedCharactersHTML(getName()), data.size()) + "</html>";
252 }
253
254 @Override public Action[] getMenuEntries() {
255 Collection<Action> components = new ArrayList<>();
256 components.add(LayerListDialog.getInstance().createShowHideLayerAction());
257 components.add(new ShowHideMarkerText(this));
258 components.add(LayerListDialog.getInstance().createDeleteLayerAction());
259 components.add(LayerListDialog.getInstance().createMergeLayerAction(this));
260 components.add(SeparatorLayerAction.INSTANCE);
261 components.add(new CustomizeColor(this));
262 components.add(SeparatorLayerAction.INSTANCE);
263 components.add(new SynchronizeAudio());
264 if (Main.pref.getBoolean("marker.traceaudio", true)) {
265 components.add(new MoveAudio());
266 }
267 components.add(new JumpToNextMarker(this));
268 components.add(new JumpToPreviousMarker(this));
269 components.add(new ConvertToDataLayerAction.FromMarkerLayer(this));
270 components.add(new RenameLayerAction(getAssociatedFile(), this));
271 components.add(SeparatorLayerAction.INSTANCE);
272 components.add(new LayerListPopup.InfoAction(this));
273 return components.toArray(new Action[components.size()]);
274 }
275
276 public boolean synchronizeAudioMarkers(final AudioMarker startMarker) {
277 syncAudioMarker = startMarker;
278 if (syncAudioMarker != null && !data.contains(syncAudioMarker)) {
279 syncAudioMarker = null;
280 }
281 if (syncAudioMarker == null) {
282 // find the first audioMarker in this layer
283 for (Marker m : data) {
284 if (m instanceof AudioMarker) {
285 syncAudioMarker = (AudioMarker) m;
286 break;
287 }
288 }
289 }
290 if (syncAudioMarker == null)
291 return false;
292
293 // apply adjustment to all subsequent audio markers in the layer
294 double adjustment = AudioPlayer.position() - syncAudioMarker.offset; // in seconds
295 boolean seenStart = false;
296 try {
297 URI uri = syncAudioMarker.url().toURI();
298 for (Marker m : data) {
299 if (m == syncAudioMarker) {
300 seenStart = true;
301 }
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 }
310 }
311 } catch (URISyntaxException e) {
312 Main.warn(e);
313 }
314 return true;
315 }
316
317 public AudioMarker addAudioMarker(double time, LatLon coor) {
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) {
323 am = (AudioMarker) m;
324 offset = time - am.time;
325 break;
326 }
327 }
328 if (am == null) {
329 JOptionPane.showMessageDialog(
330 Main.parent,
331 tr("No existing audio markers in this layer to offset from."),
332 tr("Error"),
333 JOptionPane.ERROR_MESSAGE
334 );
335 return null;
336 }
337
338 // make our new marker
339 AudioMarker newAudioMarker = new AudioMarker(coor,
340 null, AudioPlayer.url(), this, time, offset);
341
342 // insert it at the right place in a copy the collection
343 Collection<Marker> newData = new ArrayList<>();
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) {
359 if (am != null) {
360 newAudioMarker.adjustOffset(am.syncOffset()); // i.e. same as predecessor
361 }
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
371 @Override
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
389 @Override
390 public void jumpToPreviousMarker() {
391 if (currentMarker == null) {
392 currentMarker = data.get(data.size() - 1);
393 } else {
394 boolean foundCurrent = false;
395 for (int i = data.size() - 1; i >= 0; i--) {
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
408 public static void playAudio() {
409 playAdjacentMarker(null, true);
410 }
411
412 public static void playNextMarker() {
413 playAdjacentMarker(AudioMarker.recentlyPlayedMarker(), true);
414 }
415
416 public static void playPreviousMarker() {
417 playAdjacentMarker(AudioMarker.recentlyPlayedMarker(), false);
418 }
419
420 private static Marker getAdjacentMarker(Marker startMarker, boolean next, Layer layer) {
421 Marker previousMarker = null;
422 boolean nextTime = false;
423 if (layer.getClass() == MarkerLayer.class) {
424 MarkerLayer markerLayer = (MarkerLayer) layer;
425 for (Marker marker : markerLayer.data) {
426 if (marker == startMarker) {
427 if (next) {
428 nextTime = true;
429 } else {
430 if (previousMarker == null) {
431 previousMarker = startMarker; // if no previous one, play the first one again
432 }
433 return previousMarker;
434 }
435 } else if (marker.getClass() == AudioMarker.class) {
436 if (nextTime || startMarker == null)
437 return marker;
438 previousMarker = marker;
439 }
440 }
441 if (nextTime) // there was no next marker in that layer, so play the last one again
442 return startMarker;
443 }
444 return null;
445 }
446
447 private static void playAdjacentMarker(Marker startMarker, boolean next) {
448 if (!Main.isDisplayingMapView())
449 return;
450 Marker m = null;
451 Layer l = Main.getLayerManager().getActiveLayer();
452 if (l != null) {
453 m = getAdjacentMarker(startMarker, next, l);
454 }
455 if (m == null) {
456 for (Layer layer : Main.getLayerManager().getLayers()) {
457 m = getAdjacentMarker(startMarker, next, layer);
458 if (m != null) {
459 break;
460 }
461 }
462 }
463 if (m != null) {
464 ((AudioMarker) m).play();
465 }
466 }
467
468 /**
469 * Get state of text display.
470 * @return <code>true</code> if text should be shown, <code>false</code> otherwise.
471 */
472 private boolean isTextOrIconShown() {
473 String current = Main.pref.get("marker.show "+getName(), "show");
474 return "show".equalsIgnoreCase(current);
475 }
476
477 public static final class ShowHideMarkerText extends AbstractAction implements LayerAction {
478 private final transient MarkerLayer layer;
479
480 public ShowHideMarkerText(MarkerLayer layer) {
481 super(tr("Show Text/Icons"), ImageProvider.get("dialogs", "showhide"));
482 putValue(SHORT_DESCRIPTION, tr("Toggle visible state of the marker text and icons."));
483 putValue("help", ht("/Action/ShowHideTextIcons"));
484 this.layer = layer;
485 }
486
487 @Override
488 public void actionPerformed(ActionEvent e) {
489 Main.pref.put("marker.show "+layer.getName(), layer.isTextOrIconShown() ? "hide" : "show");
490 Main.map.mapView.repaint();
491 }
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 }
504 }
505
506 private class SynchronizeAudio extends AbstractAction {
507
508 /**
509 * Constructs a new {@code SynchronizeAudio} action.
510 */
511 SynchronizeAudio() {
512 super(tr("Synchronize Audio"), ImageProvider.get("audio-sync"));
513 putValue("help", ht("/Action/SynchronizeAudio"));
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 pause audio at the moment when you hear your synchronization cue."),
522 tr("Warning"),
523 JOptionPane.WARNING_MESSAGE
524 );
525 return;
526 }
527 AudioMarker recent = AudioMarker.recentlyPlayedMarker();
528 if (synchronizeAudioMarkers(recent)) {
529 JOptionPane.showMessageDialog(
530 Main.parent,
531 tr("Audio synchronized at point {0}.", syncAudioMarker.getText()),
532 tr("Information"),
533 JOptionPane.INFORMATION_MESSAGE
534 );
535 } else {
536 JOptionPane.showMessageDialog(
537 Main.parent,
538 tr("Unable to synchronize in layer being played."),
539 tr("Error"),
540 JOptionPane.ERROR_MESSAGE
541 );
542 }
543 }
544 }
545
546 private class MoveAudio extends AbstractAction {
547
548 MoveAudio() {
549 super(tr("Make Audio Marker at Play Head"), ImageProvider.get("addmarkers"));
550 putValue("help", ht("/Action/MakeAudioMarkerAtPlayHead"));
551 }
552
553 @Override
554 public void actionPerformed(ActionEvent e) {
555 if (!AudioPlayer.paused()) {
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
561 );
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 }
571}
Note: See TracBrowser for help on using the repository browser.