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

Last change on this file since 554 was 554, checked in by david, 16 years ago

new "Add Audio Marker At Play Head" facility; and tidy up Audio Preferences; default for displaying audio trace now true.

File size: 16.0 KB
Line 
1// License: GPL. Copyright 2007 by Immanuel Scholz and others
2package org.openstreetmap.josm.gui.layer.markerlayer;
3
4import static org.openstreetmap.josm.tools.I18n.tr;
5import static org.openstreetmap.josm.tools.I18n.trn;
6
7import java.awt.Color;
8import java.awt.Component;
9import java.awt.Graphics;
10import java.awt.Point;
11import java.awt.Rectangle;
12import java.awt.event.ActionEvent;
13import java.awt.event.ActionListener;
14import java.awt.event.MouseAdapter;
15import java.awt.event.MouseEvent;
16import java.io.File;
17import java.util.ArrayList;
18import java.util.Collection;
19import java.util.Iterator;
20import java.net.URL;
21
22import javax.swing.Icon;
23import javax.swing.JColorChooser;
24import javax.swing.JFileChooser;
25import javax.swing.JMenuItem;
26import javax.swing.JOptionPane;
27import javax.swing.JSeparator;
28import javax.swing.SwingUtilities;
29import javax.swing.Timer;
30import javax.swing.filechooser.FileFilter;
31
32import org.openstreetmap.josm.Main;
33import org.openstreetmap.josm.actions.RenameLayerAction;
34import org.openstreetmap.josm.command.Command;
35import org.openstreetmap.josm.data.coor.EastNorth;
36import org.openstreetmap.josm.data.gpx.GpxData;
37import org.openstreetmap.josm.data.gpx.GpxTrack;
38import org.openstreetmap.josm.data.gpx.WayPoint;
39import org.openstreetmap.josm.data.osm.visitor.BoundingXYVisitor;
40import org.openstreetmap.josm.gui.MapView;
41import org.openstreetmap.josm.gui.dialogs.LayerListDialog;
42import org.openstreetmap.josm.gui.dialogs.LayerListPopup;
43import org.openstreetmap.josm.gui.layer.Layer;
44import org.openstreetmap.josm.gui.layer.GpxLayer;
45import org.openstreetmap.josm.gui.layer.markerlayer.AudioMarker;
46import org.openstreetmap.josm.tools.ColorHelper;
47import org.openstreetmap.josm.tools.ImageProvider;
48import org.openstreetmap.josm.tools.AudioPlayer;
49
50/**
51 * A layer holding markers.
52 *
53 * Markers are GPS points with a name and, optionally, a symbol code attached;
54 * marker layers can be created from waypoints when importing raw GPS data,
55 * but they may also come from other sources.
56 *
57 * The symbol code is for future use.
58 *
59 * The data is read only.
60 */
61public class MarkerLayer extends Layer {
62
63 /**
64 * A list of markers.
65 */
66 public final Collection<Marker> data;
67 private boolean mousePressed = false;
68 public GpxLayer fromLayer = null;
69 private Rectangle audioTracer = null;
70 private Icon audioTracerIcon = null;
71 private EastNorth playheadPosition = null;
72 private static Timer timer = null;
73 private static double audioAnimationInterval = 0.0; // seconds
74 private static double playheadTime = -1.0;
75
76 public MarkerLayer(GpxData indata, String name, File associatedFile, GpxLayer fromLayer) {
77
78 super(name);
79 this.associatedFile = associatedFile;
80 this.data = new ArrayList<Marker>();
81 this.fromLayer = fromLayer;
82 double firstTime = -1.0;
83
84 for (WayPoint wpt : indata.waypoints) {
85 /* calculate time differences in waypoints */
86 double time = wpt.time;
87 if (firstTime < 0)
88 firstTime = time;
89 Marker m = Marker.createMarker(wpt, indata.storageFile, this, time, time - firstTime);
90 if (m != null)
91 data.add(m);
92 }
93
94 SwingUtilities.invokeLater(new Runnable(){
95 public void run() {
96 Main.map.mapView.addMouseListener(new MouseAdapter() {
97 @Override public void mousePressed(MouseEvent e) {
98 if (e.getButton() != MouseEvent.BUTTON1)
99 return;
100 boolean mousePressedInButton = false;
101 if (e.getPoint() != null) {
102 for (Marker mkr : data) {
103 if (mkr.containsPoint(e.getPoint())) {
104 mousePressedInButton = true;
105 break;
106 }
107 }
108 }
109 if (! mousePressedInButton)
110 return;
111 mousePressed = true;
112 if (visible)
113 Main.map.mapView.repaint();
114 }
115 @Override public void mouseReleased(MouseEvent ev) {
116 if (ev.getButton() != MouseEvent.BUTTON1 || ! mousePressed)
117 return;
118 mousePressed = false;
119 if (!visible)
120 return;
121 if (ev.getPoint() != null) {
122 for (Marker mkr : data) {
123 if (mkr.containsPoint(ev.getPoint()))
124 mkr.actionPerformed(new ActionEvent(this, 0, null));
125 }
126 }
127 Main.map.mapView.repaint();
128 }
129 });
130 }
131 });
132 }
133
134 /**
135 * Return a static icon.
136 */
137 @Override public Icon getIcon() {
138 return ImageProvider.get("layer", "marker_small");
139 }
140
141 @Override public void paint(Graphics g, MapView mv) {
142 boolean mousePressedTmp = mousePressed;
143 Point mousePos = mv.getMousePosition();
144 String mkrCol = Main.pref.get("color.gps marker");
145 String mkrColSpecial = Main.pref.get("color.layer "+name);
146 String mkrTextShow = Main.pref.get("marker.show "+name, "show");
147
148 if (!mkrColSpecial.equals(""))
149 g.setColor(ColorHelper.html2color(mkrColSpecial));
150 else if (!mkrCol.equals(""))
151 g.setColor(ColorHelper.html2color(mkrCol));
152 else
153 g.setColor(Color.GRAY);
154
155 for (Marker mkr : data) {
156 if (mousePos != null && mkr.containsPoint(mousePos)) {
157 mkr.paint(g, mv, mousePressedTmp, mkrTextShow);
158 mousePressedTmp = false;
159 } else {
160 mkr.paint(g, mv, false, mkrTextShow);
161 }
162 }
163
164 if (audioTracer != null) {
165 Point screen = Main.map.mapView.getPoint(playheadPosition);
166 audioTracer.setLocation(screen.x, screen.y);
167 audioTracerIcon.paintIcon(Main.map.mapView, g, screen.x, screen.y);
168 }
169 }
170
171 protected void traceAudio() {
172 if (timer == null) {
173 audioAnimationInterval = Double.parseDouble(Main.pref.get("marker.audioanimationinterval", "1")); //milliseconds
174 timer = new Timer((int)(audioAnimationInterval * 1000.0), new ActionListener() {
175 public void actionPerformed(ActionEvent e) {
176 timerAction();
177 }
178 });
179 timer.start();
180 }
181 }
182
183 /**
184 * callback for AudioPlayer when position changes
185 * @param position seconds into the audio stream
186 */
187 public void timerAction() {
188 AudioMarker recentlyPlayedMarker = AudioMarker.recentlyPlayedMarker();
189 if (recentlyPlayedMarker == null)
190 return;
191 double audioTime = recentlyPlayedMarker.time +
192 AudioPlayer.position() -
193 recentlyPlayedMarker.offset -
194 recentlyPlayedMarker.syncOffset;
195 if (Math.abs(audioTime- playheadTime) < audioAnimationInterval)
196 return;
197 if (fromLayer == null)
198 return;
199 /* find the pair of track points for this position (adjusted by the syncOffset)
200 * and interpolate between them
201 */
202 WayPoint w1 = null;
203 WayPoint w2 = null;
204
205 for (GpxTrack track : fromLayer.data.tracks) {
206 for (Collection<WayPoint> trackseg : track.trackSegs) {
207 for (Iterator<WayPoint> it = trackseg.iterator(); it.hasNext();) {
208 WayPoint w = it.next();
209 if (audioTime < w.time) {
210 w2 = w;
211 break;
212 }
213 w1 = w;
214 }
215 if (w2 != null) break;
216 }
217 if (w2 != null) break;
218 }
219
220 if (w1 == null)
221 return;
222 playheadPosition = w2 == null ?
223 w1.eastNorth :
224 w1.eastNorth.interpolate(w2.eastNorth,
225 (audioTime - w1.time)/(w2.time - w1.time));
226
227 if (audioTracer == null) {
228 audioTracerIcon = ImageProvider.getIfAvailable("markers",Main.pref.get("marker.audiotracericon", "audio-tracer"));
229 audioTracer = new Rectangle(0, 0, audioTracerIcon.getIconWidth(), audioTracerIcon.getIconHeight());
230 }
231 playheadTime = audioTime;
232 Main.map.mapView.repaint();
233 }
234
235 @Override public String getToolTipText() {
236 return data.size()+" "+trn("marker", "markers", data.size());
237 }
238
239 @Override public void mergeFrom(Layer from) {
240 MarkerLayer layer = (MarkerLayer)from;
241 data.addAll(layer.data);
242 }
243
244 @Override public boolean isMergable(Layer other) {
245 return other instanceof MarkerLayer;
246 }
247
248 @Override public void visitBoundingBox(BoundingXYVisitor v) {
249 for (Marker mkr : data)
250 v.visit(mkr.eastNorth);
251 }
252
253 public void applyAudio(File wavFile) {
254 String uri = "file:".concat(wavFile.getAbsolutePath());
255 Collection<Marker> markers = new ArrayList<Marker>();
256 for (Marker mkr : data) {
257 AudioMarker audioMarker = mkr.audioMarkerFromMarker(uri);
258 if (audioMarker == null) {
259 markers.add(mkr);
260 } else {
261 markers.add(audioMarker);
262 }
263 }
264 data.clear();
265 data.addAll(markers);
266 }
267
268 @Override public Object getInfoComponent() {
269 return "<html>"+trn("{0} consists of {1} marker", "{0} consists of {1} markers", data.size(), name, data.size()) + "</html>";
270 }
271
272 @Override public Component[] getMenuEntries() {
273 JMenuItem color = new JMenuItem(tr("Customize Color"), ImageProvider.get("colorchooser"));
274 color.addActionListener(new ActionListener(){
275 public void actionPerformed(ActionEvent e) {
276 String col = Main.pref.get("color.layer "+name, Main.pref.get("color.gps marker", ColorHelper.color2html(Color.gray)));
277 JColorChooser c = new JColorChooser(ColorHelper.html2color(col));
278 Object[] options = new Object[]{tr("OK"), tr("Cancel"), tr("Default")};
279 int answer = JOptionPane.showOptionDialog(Main.parent, c, tr("Choose a color"), JOptionPane.OK_CANCEL_OPTION, JOptionPane.PLAIN_MESSAGE, null, options, options[0]);
280 switch (answer) {
281 case 0:
282 Main.pref.put("color.layer "+name, ColorHelper.color2html(c.getColor()));
283 break;
284 case 1:
285 return;
286 case 2:
287 Main.pref.put("color.layer "+name, null);
288 break;
289 }
290 Main.map.repaint();
291 }
292 });
293
294 JMenuItem applyaudio = new JMenuItem(tr("Apply Audio"), ImageProvider.get("applyaudio"));
295 applyaudio.addActionListener(new ActionListener(){
296 public void actionPerformed(ActionEvent e) {
297 JFileChooser fc = new JFileChooser(Main.pref.get("tagimages.lastdirectory"));
298 fc.setFileSelectionMode(JFileChooser.FILES_ONLY);
299 fc.setAcceptAllFileFilterUsed(false);
300 fc.setFileFilter(new FileFilter(){
301 @Override public boolean accept(File f) {
302 return f.isDirectory() || f.getName().toLowerCase().endsWith(".wav");
303 }
304 @Override public String getDescription() {
305 return tr("Wave Audio files (*.wav)");
306 }
307 });
308 fc.showOpenDialog(Main.parent);
309 File sel = fc.getSelectedFile();
310 if (sel == null)
311 return;
312 applyAudio(sel);
313 Main.map.repaint();
314 }
315 });
316
317 JMenuItem syncaudio = new JMenuItem(tr("Synchronize Audio"), ImageProvider.get("audio-sync"));
318 syncaudio.addActionListener(new ActionListener(){
319 public void actionPerformed(ActionEvent e) {
320 adjustOffsetsOnAudioMarkers();
321 }
322 });
323
324 JMenuItem moveaudio = new JMenuItem(tr("Make Audio Marker At Play Head"), ImageProvider.get("addmarkers"));
325 moveaudio.addActionListener(new ActionListener(){
326 public void actionPerformed(ActionEvent e) {
327 makeAudioMarkerAtPlayHead();
328 }
329 });
330
331 Collection<Component> components = new ArrayList<Component>();
332 components.add(new JMenuItem(new LayerListDialog.ShowHideLayerAction(this)));
333 components.add(new JMenuItem(new LayerListDialog.ShowHideMarkerText(this)));
334 components.add(new JMenuItem(new LayerListDialog.DeleteLayerAction(this)));
335 components.add(new JSeparator());
336 components.add(color);
337 components.add(new JSeparator());
338 components.add(syncaudio);
339 components.add(applyaudio);
340 if (Main.pref.getBoolean("marker.traceaudio", true)) {
341 components.add (moveaudio);
342 }
343 components.add(new JMenuItem(new RenameLayerAction(associatedFile, this)));
344 components.add(new JSeparator());
345 components.add(new JMenuItem(new LayerListPopup.InfoAction(this)));
346 return components.toArray(new Component[0]);
347 }
348
349 private void adjustOffsetsOnAudioMarkers() {
350 if (! AudioPlayer.paused()) {
351 JOptionPane.showMessageDialog(Main.parent,tr("You need to pause audio at the moment when you hear your synchronization cue."));
352 return;
353 }
354 Marker startMarker = AudioMarker.recentlyPlayedMarker();
355 boolean explicitMarker = true;
356 if (startMarker != null && ! data.contains(startMarker)) {
357 explicitMarker = false;
358 startMarker = null;
359 }
360 if (startMarker == null) {
361 // find the first audioMarker in this layer
362 for (Marker m : data) {
363 if (m.getClass() == AudioMarker.class) {
364 startMarker = m;
365 break;
366 }
367 }
368 }
369 if (startMarker == null) {
370 // still no marker to work from - message?
371 JOptionPane.showMessageDialog(Main.parent,tr("No audio marker found in the layer to synchronize with."));
372 return;
373 }
374 // apply adjustment to all subsequent audio markers in the layer
375 double adjustment = AudioPlayer.position() - startMarker.offset; // in seconds
376 boolean seenStart = false;
377 URL url = ((AudioMarker)startMarker).url();
378 for (Marker m : data) {
379 if (m == startMarker)
380 seenStart = true;
381 if (seenStart) {
382 AudioMarker ma = (AudioMarker) m; // it must be an AudioMarker
383 if (ma.url().equals(url))
384 ma.adjustOffset(adjustment);
385 }
386 }
387
388 JOptionPane.showMessageDialog(Main.parent, explicitMarker ?
389 tr("Audio synchronized with most recently played marker and subsequent ones (that have the same sound track).") :
390 tr("Audio synchronized with audio markers in the layer (that have the same sound track as the first one)."));
391 }
392
393 private void makeAudioMarkerAtPlayHead() {
394 if (! AudioPlayer.paused()) {
395 JOptionPane.showMessageDialog(Main.parent,tr("You need to pause audio at the point on the track where you want the marker."));
396 return;
397 }
398 // find first audio marker to get absolute start time
399 double offset = 0.0;
400 AudioMarker am = null;
401 for (Marker m : data) {
402 if (m.getClass() == AudioMarker.class) {
403 am = (AudioMarker)m;
404 offset = playheadTime - am.time;
405 break;
406 }
407 }
408 if (am == null) {
409 JOptionPane.showMessageDialog(Main.parent,tr("No existing audio markers in this layer to offset from."));
410 return;
411 }
412
413 // make our new marker
414 AudioMarker newAudioMarker = AudioMarker.create(Main.proj.eastNorth2latlon(playheadPosition),
415 AudioMarker.inventName(offset), AudioPlayer.url().toString(), this, playheadTime, offset);
416
417 // insert it at the right place in a copy the collection
418 Collection<Marker> newData = new ArrayList<Marker>();
419 am = null;
420 for (Marker m : data) {
421 if (m.getClass() == AudioMarker.class) {
422 am = (AudioMarker) m;
423 if (newAudioMarker != null && offset < am.offset) {
424 newAudioMarker.adjustOffset(am.syncOffset()); // i.e. same as predecessor
425 newData.add(newAudioMarker);
426 newAudioMarker = null;
427 }
428 }
429 newData.add(m);
430 }
431 if (newAudioMarker != null) {
432 if (am != null)
433 newAudioMarker.adjustOffset(am.syncOffset()); // i.e. same as predecessor
434 newData.add(newAudioMarker); // insert at end
435 newAudioMarker = null;
436 }
437
438 // replace the collection
439 data.clear();
440 data.addAll(newData);
441 Main.map.mapView.repaint();
442 }
443
444 public static void playAudio() {
445 if (Main.map == null || Main.map.mapView == null)
446 return;
447 for (Layer layer : Main.map.mapView.getAllLayers()) {
448 if (layer.getClass() == MarkerLayer.class) {
449 MarkerLayer markerLayer = (MarkerLayer) layer;
450 for (Marker marker : markerLayer.data) {
451 if (marker.getClass() == AudioMarker.class) {
452 ((AudioMarker)marker).play();
453 break;
454 }
455 }
456 }
457 }
458 }
459
460 public static void playNextMarker() {
461 playAdjacentMarker(true);
462 }
463
464 public static void playPreviousMarker() {
465 playAdjacentMarker(false);
466 }
467
468 private static void playAdjacentMarker(boolean next) {
469 Marker startMarker = AudioMarker.recentlyPlayedMarker();
470 if (startMarker == null) {
471 // message?
472 return;
473 }
474 Marker previousMarker = null;
475 Marker targetMarker = null;
476 boolean nextTime = false;
477 if (Main.map == null || Main.map.mapView == null)
478 return;
479 for (Layer layer : Main.map.mapView.getAllLayers()) {
480 if (layer.getClass() == MarkerLayer.class) {
481 MarkerLayer markerLayer = (MarkerLayer) layer;
482 for (Marker marker : markerLayer.data) {
483 if (marker == startMarker) {
484 if (next) {
485 nextTime = true;
486 } else {
487 if (previousMarker == null)
488 previousMarker = startMarker; // if no previous one, play the first one again
489 ((AudioMarker)previousMarker).play();
490 break;
491 }
492 } else if (nextTime && marker.getClass() == AudioMarker.class) {
493 ((AudioMarker)marker).play();
494 return;
495 }
496 if (marker.getClass() == AudioMarker.class)
497 previousMarker = marker;
498 }
499 if (nextTime) {
500 // there was no next marker in that layer, so play the last one again
501 ((AudioMarker)startMarker).play();
502 return;
503 }
504 }
505 }
506 }
507
508}
Note: See TracBrowser for help on using the repository browser.