source: josm/trunk/src/org/openstreetmap/josm/gui/layer/GpxLayer.java@ 3127

Last change on this file since 3127 was 3127, checked in by jttt, 17 years ago

Fix #4728 display of gps-tracks (points, lines) does not refresh, cosmetics

  • Property svn:eol-style set to native
File size: 63.5 KB
Line 
1// License: GPL. See LICENSE file for details.
2
3package org.openstreetmap.josm.gui.layer;
4
5import static org.openstreetmap.josm.gui.help.HelpUtil.ht;
6import static org.openstreetmap.josm.tools.I18n.marktr;
7import static org.openstreetmap.josm.tools.I18n.tr;
8import static org.openstreetmap.josm.tools.I18n.trn;
9
10import java.awt.BasicStroke;
11import java.awt.Color;
12import java.awt.Component;
13import java.awt.Graphics2D;
14import java.awt.GridBagLayout;
15import java.awt.Point;
16import java.awt.event.ActionEvent;
17import java.awt.event.ActionListener;
18import java.awt.geom.Area;
19import java.awt.geom.Rectangle2D;
20import java.io.File;
21import java.text.DateFormat;
22import java.text.DecimalFormat;
23import java.util.ArrayList;
24import java.util.Arrays;
25import java.util.Collection;
26import java.util.Collections;
27import java.util.Comparator;
28import java.util.LinkedList;
29import java.util.List;
30import java.util.concurrent.Future;
31
32import javax.swing.AbstractAction;
33import javax.swing.Box;
34import javax.swing.ButtonGroup;
35import javax.swing.Icon;
36import javax.swing.JColorChooser;
37import javax.swing.JFileChooser;
38import javax.swing.JLabel;
39import javax.swing.JList;
40import javax.swing.JMenuItem;
41import javax.swing.JOptionPane;
42import javax.swing.JPanel;
43import javax.swing.JRadioButton;
44import javax.swing.JSeparator;
45import javax.swing.filechooser.FileFilter;
46
47import org.openstreetmap.josm.Main;
48import org.openstreetmap.josm.actions.RenameLayerAction;
49import org.openstreetmap.josm.actions.downloadtasks.DownloadOsmTaskList;
50import org.openstreetmap.josm.data.Bounds;
51import org.openstreetmap.josm.data.coor.EastNorth;
52import org.openstreetmap.josm.data.coor.LatLon;
53import org.openstreetmap.josm.data.gpx.GpxData;
54import org.openstreetmap.josm.data.gpx.GpxTrack;
55import org.openstreetmap.josm.data.gpx.GpxTrackSegment;
56import org.openstreetmap.josm.data.gpx.WayPoint;
57import org.openstreetmap.josm.data.osm.DataSet;
58import org.openstreetmap.josm.data.osm.Node;
59import org.openstreetmap.josm.data.osm.Way;
60import org.openstreetmap.josm.data.osm.visitor.BoundingXYVisitor;
61import org.openstreetmap.josm.gui.ConditionalOptionPaneUtil;
62import org.openstreetmap.josm.gui.HelpAwareOptionPane;
63import org.openstreetmap.josm.gui.MapView;
64import org.openstreetmap.josm.gui.dialogs.LayerListDialog;
65import org.openstreetmap.josm.gui.dialogs.LayerListPopup;
66import org.openstreetmap.josm.gui.layer.markerlayer.AudioMarker;
67import org.openstreetmap.josm.gui.layer.markerlayer.MarkerLayer;
68import org.openstreetmap.josm.gui.progress.NullProgressMonitor;
69import org.openstreetmap.josm.gui.progress.PleaseWaitProgressMonitor;
70import org.openstreetmap.josm.io.JpgImporter;
71import org.openstreetmap.josm.tools.AudioUtil;
72import org.openstreetmap.josm.tools.DateUtils;
73import org.openstreetmap.josm.tools.GBC;
74import org.openstreetmap.josm.tools.ImageProvider;
75import org.openstreetmap.josm.tools.UrlLabel;
76
77public class GpxLayer extends Layer {
78 public GpxData data;
79 protected static final double PHI = Math.toRadians(15);
80 private boolean computeCacheInSync;
81 private int computeCacheMaxLineLengthUsed;
82 private Color computeCacheColorUsed;
83 private colorModes computeCacheColored;
84 private int computeCacheColorTracksTune;
85 private boolean isLocalFile;
86
87 private final List<GpxTrack> lastTracks = new ArrayList<GpxTrack>(); // List of tracks at last paint
88 private int lastUpdateCount;
89 private PaintSettings lastPaintSettings;
90
91 private static class Markers {
92 public boolean timedMarkersOmitted = false;
93 public boolean untimedMarkersOmitted = false;
94 }
95
96 public GpxLayer(GpxData d) {
97 super((String) d.attr.get("name"));
98 data = d;
99 computeCacheInSync = false;
100 }
101
102 public GpxLayer(GpxData d, String name) {
103 this(d);
104 this.setName(name);
105 }
106
107 public GpxLayer(GpxData d, String name, boolean isLocal) {
108 this(d);
109 this.setName(name);
110 this.isLocalFile = isLocal;
111 }
112
113 @Override
114 public Icon getIcon() {
115 return ImageProvider.get("layer", "gpx_small");
116 }
117
118 @Override
119 public Object getInfoComponent() {
120 return getToolTipText();
121 }
122
123 static public Color getColor(String name) {
124 return Main.pref.getColor(marktr("gps point"), name != null ? "layer " + name : null, Color.gray);
125 }
126
127 @Override
128 public Component[] getMenuEntries() {
129 JMenuItem line = new JMenuItem(tr("Customize line drawing"), ImageProvider.get("mapmode/addsegment"));
130 line.addActionListener(new ActionListener() {
131 public void actionPerformed(ActionEvent e) {
132 JRadioButton[] r = new JRadioButton[3];
133 r[0] = new JRadioButton(tr("Use global settings."));
134 r[1] = new JRadioButton(tr("Draw lines between points for this layer."));
135 r[2] = new JRadioButton(tr("Do not draw lines between points for this layer."));
136 ButtonGroup group = new ButtonGroup();
137 Box panel = Box.createVerticalBox();
138 for (JRadioButton b : r) {
139 group.add(b);
140 panel.add(b);
141 }
142 String propName = "draw.rawgps.lines.layer " + getName();
143 if (Main.pref.hasKey(propName)) {
144 group.setSelected(r[Main.pref.getBoolean(propName) ? 1 : 2].getModel(), true);
145 } else {
146 group.setSelected(r[0].getModel(), true);
147 }
148 int answer = JOptionPane.showConfirmDialog(Main.parent, panel,
149 tr("Select line drawing options"), JOptionPane.OK_CANCEL_OPTION, JOptionPane.QUESTION_MESSAGE);
150 switch (answer) {
151 case JOptionPane.CANCEL_OPTION:
152 case JOptionPane.CLOSED_OPTION:
153 return;
154 default:
155 // continue
156 }
157 if (group.getSelection() == r[0].getModel()) {
158 Main.pref.put(propName, null);
159 } else {
160 Main.pref.put(propName, group.getSelection() == r[1].getModel());
161 }
162 Main.map.repaint();
163 }
164 });
165
166 JMenuItem color = new JMenuItem(tr("Customize Color"), ImageProvider.get("colorchooser"));
167 color.putClientProperty("help", "Action/LayerCustomizeColor");
168 color.addActionListener(new ActionListener() {
169 public void actionPerformed(ActionEvent e) {
170 JColorChooser c = new JColorChooser(getColor(getName()));
171 Object[] options = new Object[] { tr("OK"), tr("Cancel"), tr("Default") };
172 int answer = JOptionPane.showOptionDialog(
173 Main.parent,
174 c,
175 tr("Choose a color"),
176 JOptionPane.OK_CANCEL_OPTION,
177 JOptionPane.PLAIN_MESSAGE,
178 null,
179 options, options[0]
180 );
181 switch (answer) {
182 case 0:
183 Main.pref.putColor("layer " + getName(), c.getColor());
184 break;
185 case 1:
186 return;
187 case 2:
188 Main.pref.putColor("layer " + getName(), null);
189 break;
190 }
191 Main.map.repaint();
192 }
193 });
194
195 JMenuItem markersFromNamedTrackpoints = new JMenuItem(tr("Markers From Named Points"), ImageProvider
196 .get("addmarkers"));
197 markersFromNamedTrackpoints.putClientProperty("help", "Action/MarkersFromNamedPoints");
198 markersFromNamedTrackpoints.addActionListener(new ActionListener() {
199 public void actionPerformed(ActionEvent e) {
200 GpxData namedTrackPoints = new GpxData();
201 for (GpxTrack track : data.tracks) {
202 for (GpxTrackSegment seg : track.getSegments()) {
203 for (WayPoint point : seg.getWayPoints())
204 if (point.attr.containsKey("name") || point.attr.containsKey("desc")) {
205 namedTrackPoints.waypoints.add(point);
206 }
207 }
208 }
209
210 MarkerLayer ml = new MarkerLayer(namedTrackPoints, tr("Named Trackpoints from {0}", getName()),
211 getAssociatedFile(), GpxLayer.this);
212 if (ml.data.size() > 0) {
213 Main.main.addLayer(ml);
214 }
215 }
216 });
217
218 JMenuItem importAudio = new JMenuItem(tr("Import Audio"), ImageProvider.get("importaudio"));
219 importAudio.putClientProperty("help", "ImportAudio");
220 importAudio.addActionListener(new ActionListener() {
221 private void warnCantImportIntoServerLayer(GpxLayer layer) {
222 String msg = tr("<html>The data in the GPX layer ''{0}'' has been downloaded from the server.<br>"
223 + "Because its way points do not include a timestamp we cannot correlate them with audio data.</html>",
224 layer.getName()
225 );
226 HelpAwareOptionPane.showOptionDialog(
227 Main.parent,
228 msg,
229 tr("Import not possible"),
230 JOptionPane.WARNING_MESSAGE,
231 ht("/Action/ImportImages#CantImportIntoGpxLayerFromServer")
232 );
233 }
234 public void actionPerformed(ActionEvent e) {
235 if (GpxLayer.this.data.fromServer) {
236 warnCantImportIntoServerLayer(GpxLayer.this);
237 return;
238 }
239 String dir = Main.pref.get("markers.lastaudiodirectory");
240 JFileChooser fc = new JFileChooser(dir);
241 fc.setFileSelectionMode(JFileChooser.FILES_ONLY);
242 fc.setAcceptAllFileFilterUsed(false);
243 fc.setFileFilter(new FileFilter() {
244 @Override
245 public boolean accept(File f) {
246 return f.isDirectory() || f.getName().toLowerCase().endsWith(".wav");
247 }
248
249 @Override
250 public String getDescription() {
251 return tr("Wave Audio files (*.wav)");
252 }
253 });
254 fc.setMultiSelectionEnabled(true);
255 if (fc.showOpenDialog(Main.parent) == JFileChooser.APPROVE_OPTION) {
256 if (!fc.getCurrentDirectory().getAbsolutePath().equals(dir)) {
257 Main.pref.put("markers.lastaudiodirectory", fc.getCurrentDirectory().getAbsolutePath());
258 }
259
260 File sel[] = fc.getSelectedFiles();
261 // sort files in increasing order of timestamp (this is the end time, but so
262 // long as they don't overlap, that's fine)
263 if (sel.length > 1) {
264 Arrays.sort(sel, new Comparator<File>() {
265 public int compare(File a, File b) {
266 return a.lastModified() <= b.lastModified() ? -1 : 1;
267 }
268 });
269 }
270
271 String names = null;
272 for (int i = 0; i < sel.length; i++) {
273 if (names == null) {
274 names = " (";
275 } else {
276 names += ", ";
277 }
278 names += sel[i].getName();
279 }
280 if (names != null) {
281 names += ")";
282 } else {
283 names = "";
284 }
285 MarkerLayer ml = new MarkerLayer(new GpxData(), tr("Audio markers from {0}", getName()) + names,
286 getAssociatedFile(), GpxLayer.this);
287 double firstStartTime = sel[0].lastModified() / 1000.0 /* ms -> seconds */
288 - AudioUtil.getCalibratedDuration(sel[0]);
289
290 Markers m = new Markers();
291 for (int i = 0; i < sel.length; i++) {
292 importAudio(sel[i], ml, firstStartTime, m);
293 }
294 Main.main.addLayer(ml);
295 Main.map.repaint();
296 }
297 }
298 });
299
300 JMenuItem tagimage = new JMenuItem(tr("Import images"), ImageProvider.get("dialogs/geoimage"));
301 tagimage.putClientProperty("help", ht("/Action/ImportImages"));
302 tagimage.addActionListener(new ActionListener() {
303
304 private void warnCantImportIntoServerLayer(GpxLayer layer) {
305 String msg = tr("<html>The data in the GPX layer ''{0}'' has been downloaded from the server.<br>"
306 + "Because its way points do not include a timestamp we cannot correlate them with images.</html>",
307 layer.getName()
308 );
309 HelpAwareOptionPane.showOptionDialog(
310 Main.parent,
311 msg,
312 tr("Import not possible"),
313 JOptionPane.WARNING_MESSAGE,
314 ht("/Action/ImportImages#CantImportIntoGpxLayerFromServer")
315 );
316 }
317
318 public void actionPerformed(ActionEvent e) {
319 if (GpxLayer.this.data.fromServer) {
320 warnCantImportIntoServerLayer(GpxLayer.this);
321 return;
322 }
323 JFileChooser fc = new JFileChooser(Main.pref.get("geoimage.lastdirectory", Main.pref.get("lastDirectory")));
324 fc.setFileSelectionMode(JFileChooser.FILES_AND_DIRECTORIES);
325 fc.setMultiSelectionEnabled(true);
326 fc.setAcceptAllFileFilterUsed(false);
327 JpgImporter importer = new JpgImporter(GpxLayer.this);
328 fc.setFileFilter(importer.filter);
329 fc.showOpenDialog(Main.parent);
330 LinkedList<File> files = new LinkedList<File>();
331 File[] sel = fc.getSelectedFiles();
332 if (sel == null || sel.length == 0)
333 return;
334 addRecursiveFiles(files, sel);
335 importer.importDataHandleExceptions(files, NullProgressMonitor.INSTANCE);
336 }
337
338 private void addRecursiveFiles(LinkedList<File> files, File[] sel) {
339 for (File f : sel) {
340 if (f.isDirectory()) {
341 addRecursiveFiles(files, f.listFiles());
342 } else if (f.getName().toLowerCase().endsWith(".jpg")) {
343 files.add(f);
344 }
345 }
346 }
347 });
348
349 if (Main.applet)
350 return new Component[] { new JMenuItem(LayerListDialog.getInstance().createShowHideLayerAction(this)),
351 new JMenuItem(LayerListDialog.getInstance().createDeleteLayerAction(this)), new JSeparator(), color, line,
352 new JMenuItem(new ConvertToDataLayerAction()), new JSeparator(),
353 new JMenuItem(new RenameLayerAction(getAssociatedFile(), this)), new JSeparator(),
354 new JMenuItem(new LayerListPopup.InfoAction(this)) };
355 return new Component[] { new JMenuItem(LayerListDialog.getInstance().createShowHideLayerAction(this)),
356 new JMenuItem(LayerListDialog.getInstance().createDeleteLayerAction(this)), new JSeparator(),
357 new JMenuItem(new LayerSaveAction(this)), new JMenuItem(new LayerSaveAsAction(this)), color, line,
358 tagimage, importAudio, markersFromNamedTrackpoints, new JMenuItem(new ConvertToDataLayerAction()),
359 new JMenuItem(new DownloadAlongTrackAction()), new JSeparator(),
360 new JMenuItem(new RenameLayerAction(getAssociatedFile(), this)), new JSeparator(),
361 new JMenuItem(new LayerListPopup.InfoAction(this)) };
362 }
363
364 @Override
365 public String getToolTipText() {
366 StringBuilder info = new StringBuilder().append("<html>");
367
368 if (data.attr.containsKey("name")) {
369 info.append(tr("Name: {0}", data.attr.get(GpxData.META_NAME))).append("<br>");
370 }
371
372 if (data.attr.containsKey("desc")) {
373 info.append(tr("Description: {0}", data.attr.get(GpxData.META_DESC))).append("<br>");
374 }
375
376 if (data.tracks.size() > 0) {
377 info.append("<table><thead align='center'><tr><td colspan='5'>"
378 + trn("{0} track", "{0} tracks", data.tracks.size(), data.tracks.size())
379 + "</td></tr><tr align='center'><td>" + tr("Name") + "</td><td>"
380 + tr("Description") + "</td><td>" + tr("Timespan")
381 + "</td><td>" + tr("Length") + "</td><td>" + tr("URL")
382 + "</td></tr></thead>");
383
384 for (GpxTrack trk : data.tracks) {
385 WayPoint earliest = null, latest = null;
386
387 info.append("<tr><td>");
388 if (trk.getAttributes().containsKey("name")) {
389 info.append(trk.getAttributes().get("name"));
390 }
391 info.append("</td><td>");
392 if (trk.getAttributes().containsKey("desc")) {
393 info.append(" ").append(trk.getAttributes().get("desc"));
394 }
395 info.append("</td><td>");
396
397 for (GpxTrackSegment seg : trk.getSegments()) {
398 for (WayPoint pnt : seg.getWayPoints()) {
399 if (latest == null) {
400 latest = earliest = pnt;
401 } else {
402 if (pnt.compareTo(earliest) < 0) {
403 earliest = pnt;
404 } else {
405 latest = pnt;
406 }
407 }
408 }
409 }
410
411 if (earliest != null && latest != null) {
412 DateFormat df = DateFormat.getDateInstance(DateFormat.SHORT);
413 String earliestDate = df.format(earliest.getTime());
414 String latestDate = df.format(latest.getTime());
415
416 if (earliestDate.equals(latestDate)) {
417 DateFormat tf = DateFormat.getTimeInstance(DateFormat.SHORT);
418 info.append(earliestDate).append(" ");
419 info.append(tf.format(earliest.getTime())).append(" - ").append(tf.format(latest.getTime()));
420 } else {
421 DateFormat dtf = DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT);
422 info.append(dtf.format(earliest.getTime())).append(" - ").append(dtf.format(latest.getTime()));
423 }
424
425 int diff = (int) (latest.time - earliest.time);
426 info.append(" (" + (diff / 3600) + ":" + ((diff % 3600) / 60) + ")");
427 }
428
429 info.append("</td><td>");
430 info.append(new DecimalFormat("#0.00").format(trk.length() / 1000) + "km");
431 info.append("</td><td>");
432 if (trk.getAttributes().containsKey("url")) {
433 info.append(trk.getAttributes().get("url"));
434 }
435 info.append("</td></tr>");
436 }
437
438 info.append("</table><br><br>");
439
440 }
441
442 info.append(tr("Length: ") + new DecimalFormat("#0.00").format(data.length() / 1000) + "km");
443 info.append("<br>");
444
445 info.append(trn("{0} route, ", "{0} routes, ", data.routes.size(), data.routes.size())).append(
446 trn("{0} waypoint", "{0} waypoints", data.waypoints.size(), data.waypoints.size())).append("<br>");
447
448 return info.append("</html>").toString();
449 }
450
451 @Override
452 public boolean isMergable(Layer other) {
453 return other instanceof GpxLayer;
454 }
455
456 private int sumUpdateCount() {
457 int updateCount = 0;
458 for (GpxTrack track: data.tracks) {
459 updateCount += track.getUpdateCount();
460 }
461 return updateCount;
462 }
463
464 @Override
465 public boolean isChanged() {
466 if (data.tracks.equals(lastTracks) && new PaintSettings(isLocalFile).equals(lastPaintSettings))
467 return sumUpdateCount() != lastUpdateCount;
468 else
469 return true;
470 }
471
472 @Override
473 public void mergeFrom(Layer from) {
474 data.mergeFrom(((GpxLayer) from).data);
475 computeCacheInSync = false;
476 }
477
478 private static Color[] colors = new Color[256];
479 static {
480 for (int i = 0; i < colors.length; i++) {
481 colors[i] = Color.getHSBColor(i / 300.0f, 1, 1);
482 }
483 }
484
485 // lookup array to draw arrows without doing any math
486 private static int ll0 = 9;
487 private static int sl4 = 5;
488 private static int sl9 = 3;
489 private static int[][] dir = { { +sl4, +ll0, +ll0, +sl4 }, { -sl9, +ll0, +sl9, +ll0 }, { -ll0, +sl4, -sl4, +ll0 },
490 { -ll0, -sl9, -ll0, +sl9 }, { -sl4, -ll0, -ll0, -sl4 }, { +sl9, -ll0, -sl9, -ll0 },
491 { +ll0, -sl4, +sl4, -ll0 }, { +ll0, +sl9, +ll0, -sl9 }, { +sl4, +ll0, +ll0, +sl4 },
492 { -sl9, +ll0, +sl9, +ll0 }, { -ll0, +sl4, -sl4, +ll0 }, { -ll0, -sl9, -ll0, +sl9 } };
493
494 // the different color modes
495 enum colorModes {
496 none, velocity, dilution
497 }
498
499 private class PaintSettings {
500 final Color neutralColor;
501 final boolean forceLines;
502 final boolean direction;
503 final int lineWidth;
504 final int maxLineLength;
505 final boolean lines;
506 final boolean large;
507 final boolean hdopcircle;
508 final colorModes colored;
509 final boolean alternatedirection;
510 final int delta;
511 final int colorTracksTune;
512
513 public PaintSettings(boolean isLocalFile) {
514 neutralColor = getColor(getName());
515 // also draw lines between points belonging to different segments
516 forceLines = Main.pref.getBoolean("draw.rawgps.lines.force");
517 // draw direction arrows on the lines
518 direction = Main.pref.getBoolean("draw.rawgps.direction");
519 // don't draw lines if longer than x meters
520 lineWidth = Main.pref.getInteger("draw.rawgps.linewidth",0);
521
522 if (isLocalFile) {
523 maxLineLength = Main.pref.getInteger("draw.rawgps.max-line-length.local", -1);
524 } else {
525 maxLineLength = Main.pref.getInteger("draw.rawgps.max-line-length", 200);
526 }
527
528 // draw line between points, global setting
529 boolean lines = (Main.pref.getBoolean("draw.rawgps.lines", true) || (Main.pref
530 .getBoolean("draw.rawgps.lines.localfiles") && isLocalFile));
531 String linesKey = "draw.rawgps.lines.layer " + getName();
532 // draw lines, per-layer setting
533 if (Main.pref.hasKey(linesKey)) {
534 lines = Main.pref.getBoolean(linesKey);
535 }
536 this.lines = lines;
537
538 // paint large dots for points
539 large = Main.pref.getBoolean("draw.rawgps.large");
540 hdopcircle = Main.pref.getBoolean("draw.rawgps.hdopcircle", true);
541 // color the lines
542 int colorIndex = Main.pref.getInteger("draw.rawgps.colors", 0);
543 colored = colorIndex >= 0 && colorIndex < colorModes.values().length?colorModes.values()[colorIndex]:colorModes.none;
544 // paint direction arrow with alternate math. may be faster
545 alternatedirection = Main.pref.getBoolean("draw.rawgps.alternatedirection");
546 // don't draw arrows nearer to each other than this
547 delta = Main.pref.getInteger("draw.rawgps.min-arrow-distance", 0);
548 // allows to tweak line coloring for different speed levels.
549 colorTracksTune = Main.pref.getInteger("draw.rawgps.colorTracksTune", 45);
550 }
551
552 @Override
553 public int hashCode() {
554 final int prime = 31;
555 int result = 1;
556 result = prime * result + getOuterType().hashCode();
557 result = prime * result + (alternatedirection ? 1231 : 1237);
558 result = prime * result + colorTracksTune;
559 result = prime * result + ((colored == null) ? 0 : colored.hashCode());
560 result = prime * result + delta;
561 result = prime * result + (direction ? 1231 : 1237);
562 result = prime * result + (forceLines ? 1231 : 1237);
563 result = prime * result + (hdopcircle ? 1231 : 1237);
564 result = prime * result + (large ? 1231 : 1237);
565 result = prime * result + lineWidth;
566 result = prime * result + (lines ? 1231 : 1237);
567 result = prime * result + maxLineLength;
568 result = prime * result + ((neutralColor == null) ? 0 : neutralColor.hashCode());
569 return result;
570 }
571
572 @Override
573 public boolean equals(Object obj) {
574 if (this == obj)
575 return true;
576 if (obj == null)
577 return false;
578 if (getClass() != obj.getClass())
579 return false;
580 PaintSettings other = (PaintSettings) obj;
581 if (!getOuterType().equals(other.getOuterType()))
582 return false;
583 if (alternatedirection != other.alternatedirection)
584 return false;
585 if (colorTracksTune != other.colorTracksTune)
586 return false;
587 if (colored == null) {
588 if (other.colored != null)
589 return false;
590 } else if (!colored.equals(other.colored))
591 return false;
592 if (delta != other.delta)
593 return false;
594 if (direction != other.direction)
595 return false;
596 if (forceLines != other.forceLines)
597 return false;
598 if (hdopcircle != other.hdopcircle)
599 return false;
600 if (large != other.large)
601 return false;
602 if (lineWidth != other.lineWidth)
603 return false;
604 if (lines != other.lines)
605 return false;
606 if (maxLineLength != other.maxLineLength)
607 return false;
608 if (neutralColor == null) {
609 if (other.neutralColor != null)
610 return false;
611 } else if (!neutralColor.equals(other.neutralColor))
612 return false;
613 return true;
614 }
615
616 private GpxLayer getOuterType() {
617 return GpxLayer.this;
618 }
619 }
620
621 @Override
622 public void paint(Graphics2D g, MapView mv, Bounds box) {
623 lastUpdateCount = sumUpdateCount();
624 lastTracks.clear();
625 lastTracks.addAll(data.tracks);
626 lastPaintSettings = new PaintSettings(isLocalFile);
627
628
629 /****************************************************************
630 ********** STEP 1 - GET CONFIG VALUES **************************
631 ****************************************************************/
632 PaintSettings ps =lastPaintSettings;
633
634 if(ps.lineWidth != 0)
635 {
636 g.setStroke(new BasicStroke(ps.lineWidth,BasicStroke.CAP_ROUND,BasicStroke.JOIN_ROUND));
637 }
638
639 /****************************************************************
640 ********** STEP 2a - CHECK CACHE VALIDITY **********************
641 ****************************************************************/
642 if ((computeCacheMaxLineLengthUsed != ps.maxLineLength) || (!ps.neutralColor.equals(computeCacheColorUsed))
643 || (computeCacheColored != ps.colored) || (computeCacheColorTracksTune != ps.colorTracksTune)) {
644 // System.out.println("(re-)computing gpx line styles, reason: CCIS=" +
645 // computeCacheInSync + " CCMLLU=" + (computeCacheMaxLineLengthUsed != maxLineLength) +
646 // " CCCU=" + (!neutralColor.equals(computeCacheColorUsed)) + " CCC=" +
647 // (computeCacheColored != colored));
648 computeCacheMaxLineLengthUsed = ps.maxLineLength;
649 computeCacheInSync = false;
650 computeCacheColorUsed = ps.neutralColor;
651 computeCacheColored = ps.colored;
652 computeCacheColorTracksTune = ps.colorTracksTune;
653 }
654
655 /****************************************************************
656 ********** STEP 2b - RE-COMPUTE CACHE DATA *********************
657 ****************************************************************/
658 if (!computeCacheInSync) { // don't compute if the cache is good
659 WayPoint oldWp = null;
660 for (GpxTrack trk : data.tracks) {
661 for (GpxTrackSegment segment : trk.getSegments()) {
662 if (!ps.forceLines) { // don't draw lines between segments, unless forced to
663 oldWp = null;
664 }
665 for (WayPoint trkPnt : segment.getWayPoints()) {
666 LatLon c = trkPnt.getCoor();
667 if (Double.isNaN(c.lat()) || Double.isNaN(c.lon())) {
668 continue;
669 }
670 trkPnt.customColoring = ps.neutralColor;
671 if (oldWp != null) {
672 double dist = c.greatCircleDistance(oldWp.getCoor());
673
674 switch (ps.colored) {
675 case velocity:
676 double dtime = trkPnt.time - oldWp.time;
677 double vel = dist / dtime;
678 double velColor = vel / ps.colorTracksTune * 255;
679 // Bad case first
680 if (dtime <= 0 || vel < 0 || velColor > 255) {
681 trkPnt.customColoring = colors[255];
682 } else {
683 trkPnt.customColoring = colors[(int) (velColor)];
684 }
685 break;
686
687 case dilution:
688 if (trkPnt.attr.get("hdop") != null) {
689 float hdop = ((Float) trkPnt.attr.get("hdop")).floatValue();
690 if (hdop < 0) {
691 hdop = 0;
692 }
693 int hdoplvl = Math.round(hdop * Main.pref.getInteger("hdop.factor", 25));
694 // High hdop is bad, but high values in colors are green.
695 // Therefore inverse the logic
696 int hdopcolor = 255 - (hdoplvl > 255 ? 255 : hdoplvl);
697 trkPnt.customColoring = colors[hdopcolor];
698 }
699 break;
700 }
701
702 if (ps.maxLineLength == -1 || dist <= ps.maxLineLength) {
703 trkPnt.drawLine = true;
704 trkPnt.dir = (int) oldWp.getCoor().heading(trkPnt.getCoor());
705 } else {
706 trkPnt.drawLine = false;
707 }
708 } else { // make sure we reset outdated data
709 trkPnt.drawLine = false;
710 }
711 oldWp = trkPnt;
712 }
713 }
714 }
715 computeCacheInSync = true;
716 }
717
718 List<Collection<WayPoint>> visibleSegments = new ArrayList<Collection<WayPoint>>();
719 for (GpxTrack trk: data.tracks) {
720 for (GpxTrackSegment trkSeg: trk.getSegments()) {
721 if (trkSeg.getBounds() != null && trkSeg.getBounds().intersects(box)) {
722 visibleSegments.add(trkSeg.getWayPoints());
723 }
724 }
725 }
726
727 /****************************************************************
728 ********** STEP 3a - DRAW LINES ********************************
729 ****************************************************************/
730 if (ps.lines) {
731 Point old = null;
732 for (Collection<WayPoint> segment : visibleSegments) {
733 for (WayPoint trkPnt : segment) {
734 LatLon c = trkPnt.getCoor();
735 if (Double.isNaN(c.lat()) || Double.isNaN(c.lon())) {
736 continue;
737 }
738 Point screen = mv.getPoint(trkPnt.getEastNorth());
739 if (trkPnt.drawLine) {
740 // skip points that are on the same screenposition
741 if (old != null && ((old.x != screen.x) || (old.y != screen.y))) {
742 g.setColor(trkPnt.customColoring);
743 g.drawLine(old.x, old.y, screen.x, screen.y);
744 }
745 }
746 old = screen;
747 } // end for trkpnt
748 } // end for segment
749 } // end if lines
750
751 /****************************************************************
752 ********** STEP 3b - DRAW NICE ARROWS **************************
753 ****************************************************************/
754 if (ps.lines && ps.direction && !ps.alternatedirection) {
755 Point old = null;
756 Point oldA = null; // last arrow painted
757 for (Collection<WayPoint> segment : visibleSegments) {
758 for (WayPoint trkPnt : segment) {
759 LatLon c = trkPnt.getCoor();
760 if (Double.isNaN(c.lat()) || Double.isNaN(c.lon())) {
761 continue;
762 }
763 if (trkPnt.drawLine) {
764 Point screen = mv.getPoint(trkPnt.getEastNorth());
765 // skip points that are on the same screenposition
766 if (old != null
767 && (oldA == null || screen.x < oldA.x - ps.delta || screen.x > oldA.x + ps.delta
768 || screen.y < oldA.y - ps.delta || screen.y > oldA.y + ps.delta)) {
769 g.setColor(trkPnt.customColoring);
770 double t = Math.atan2(screen.y - old.y, screen.x - old.x) + Math.PI;
771 g.drawLine(screen.x, screen.y, (int) (screen.x + 10 * Math.cos(t - PHI)),
772 (int) (screen.y + 10 * Math.sin(t - PHI)));
773 g.drawLine(screen.x, screen.y, (int) (screen.x + 10 * Math.cos(t + PHI)),
774 (int) (screen.y + 10 * Math.sin(t + PHI)));
775 oldA = screen;
776 }
777 old = screen;
778 }
779 } // end for trkpnt
780 } // end for segment
781 } // end if lines
782
783 /****************************************************************
784 ********** STEP 3c - DRAW FAST ARROWS **************************
785 ****************************************************************/
786 if (ps.lines && ps.direction && ps.alternatedirection) {
787 Point old = null;
788 Point oldA = null; // last arrow painted
789 for (Collection<WayPoint> segment : visibleSegments) {
790 for (WayPoint trkPnt : segment) {
791 LatLon c = trkPnt.getCoor();
792 if (Double.isNaN(c.lat()) || Double.isNaN(c.lon())) {
793 continue;
794 }
795 if (trkPnt.drawLine) {
796 Point screen = mv.getPoint(trkPnt.getEastNorth());
797 // skip points that are on the same screenposition
798 if (old != null
799 && (oldA == null || screen.x < oldA.x - ps.delta || screen.x > oldA.x + ps.delta
800 || screen.y < oldA.y - ps.delta || screen.y > oldA.y + ps.delta)) {
801 g.setColor(trkPnt.customColoring);
802 g.drawLine(screen.x, screen.y, screen.x + dir[trkPnt.dir][0], screen.y
803 + dir[trkPnt.dir][1]);
804 g.drawLine(screen.x, screen.y, screen.x + dir[trkPnt.dir][2], screen.y
805 + dir[trkPnt.dir][3]);
806 oldA = screen;
807 }
808 old = screen;
809 }
810 } // end for trkpnt
811 } // end for segment
812 } // end if lines
813
814 /****************************************************************
815 ********** STEP 3d - DRAW LARGE POINTS AND HDOP CIRCLE *********
816 ****************************************************************/
817 if (ps.large || ps.hdopcircle) {
818 g.setColor(ps.neutralColor);
819 for (Collection<WayPoint> segment : visibleSegments) {
820 for (WayPoint trkPnt : segment) {
821 LatLon c = trkPnt.getCoor();
822 if (Double.isNaN(c.lat()) || Double.isNaN(c.lon())) {
823 continue;
824 }
825 Point screen = mv.getPoint(trkPnt.getEastNorth());
826 g.setColor(trkPnt.customColoring);
827 if (ps.hdopcircle && trkPnt.attr.get("hdop") != null) {
828 // hdop value
829 float hdop = ((Float)trkPnt.attr.get("hdop")).floatValue();
830 if (hdop < 0) {
831 hdop = 0;
832 }
833 // hdop pixels
834 int hdopp = mv.getPoint(new LatLon(trkPnt.getCoor().lat(), trkPnt.getCoor().lon() + 2*6*hdop*360/40000000)).x - screen.x;
835 g.drawArc(screen.x-hdopp/2, screen.y-hdopp/2, hdopp, hdopp, 0, 360);
836 }
837 if (ps.large) {
838 g.fillRect(screen.x-1, screen.y-1, 3, 3);
839 }
840 } // end for trkpnt
841 } // end for segment
842 } // end if large || hdopcircle
843
844 /****************************************************************
845 ********** STEP 3e - DRAW SMALL POINTS FOR LINES ***************
846 ****************************************************************/
847 if (!ps.large && ps.lines) {
848 g.setColor(ps.neutralColor);
849 for (Collection<WayPoint> segment : visibleSegments) {
850 for (WayPoint trkPnt : segment) {
851 LatLon c = trkPnt.getCoor();
852 if (Double.isNaN(c.lat()) || Double.isNaN(c.lon())) {
853 continue;
854 }
855 if (!trkPnt.drawLine) {
856 Point screen = mv.getPoint(trkPnt.getEastNorth());
857 g.drawRect(screen.x, screen.y, 0, 0);
858 }
859 } // end for trkpnt
860 } // end for segment
861 } // end if large
862
863 /****************************************************************
864 ********** STEP 3f - DRAW SMALL POINTS INSTEAD OF LINES ********
865 ****************************************************************/
866 if (!ps.large && !ps.lines) {
867 g.setColor(ps.neutralColor);
868 for (Collection<WayPoint> segment : visibleSegments) {
869 for (WayPoint trkPnt : segment) {
870 LatLon c = trkPnt.getCoor();
871 if (Double.isNaN(c.lat()) || Double.isNaN(c.lon())) {
872 continue;
873 }
874 Point screen = mv.getPoint(trkPnt.getEastNorth());
875 g.setColor(trkPnt.customColoring);
876 g.drawRect(screen.x, screen.y, 0, 0);
877 } // end for trkpnt
878 } // end for segment
879 } // end if large
880
881 // Long duration = System.currentTimeMillis() - startTime;
882 // System.out.println(duration);
883 } // end paint
884
885 @Override
886 public void visitBoundingBox(BoundingXYVisitor v) {
887 v.visit(data.recalculateBounds());
888 }
889
890 public class ConvertToDataLayerAction extends AbstractAction {
891 public ConvertToDataLayerAction() {
892 super(tr("Convert to data layer"), ImageProvider.get("converttoosm"));
893 }
894
895 public void actionPerformed(ActionEvent e) {
896 JPanel msg = new JPanel(new GridBagLayout());
897 msg
898 .add(
899 new JLabel(
900 tr("<html>Upload of unprocessed GPS data as map data is considered harmful.<br>If you want to upload traces, look here:")),
901 GBC.eol());
902 msg.add(new UrlLabel(tr("http://www.openstreetmap.org/traces")), GBC.eop());
903 if (!ConditionalOptionPaneUtil.showConfirmationDialog("convert_to_data", Main.parent, msg, tr("Warning"),
904 JOptionPane.OK_CANCEL_OPTION, JOptionPane.WARNING_MESSAGE, JOptionPane.OK_OPTION))
905 return;
906 DataSet ds = new DataSet();
907 for (GpxTrack trk : data.tracks) {
908 for (GpxTrackSegment segment : trk.getSegments()) {
909 List<Node> nodes = new ArrayList<Node>();
910 for (WayPoint p : segment.getWayPoints()) {
911 Node n = new Node(p.getCoor());
912 String timestr = p.getString("time");
913 if (timestr != null) {
914 n.setTimestamp(DateUtils.fromString(timestr));
915 }
916 ds.addPrimitive(n);
917 nodes.add(n);
918 }
919 Way w = new Way();
920 w.setNodes(nodes);
921 ds.addPrimitive(w);
922 }
923 }
924 Main.main
925 .addLayer(new OsmDataLayer(ds, tr("Converted from: {0}", GpxLayer.this.getName()), getAssociatedFile()));
926 Main.main.removeLayer(GpxLayer.this);
927 }
928 }
929
930 @Override
931 public File getAssociatedFile() {
932 return data.storageFile;
933 }
934
935 @Override
936 public void setAssociatedFile(File file) {
937 data.storageFile = file;
938 }
939
940 /**
941 * Action that issues a series of download requests to the API, following the GPX track.
942 *
943 * @author fred
944 */
945 public class DownloadAlongTrackAction extends AbstractAction {
946 public DownloadAlongTrackAction() {
947 super(tr("Download from OSM along this track"), ImageProvider.get("downloadalongtrack"));
948 }
949
950 public void actionPerformed(ActionEvent e) {
951 JPanel msg = new JPanel(new GridBagLayout());
952 Integer dist[] = { 5000, 500, 50 };
953 Integer area[] = { 20, 10, 5, 1 };
954
955 msg.add(new JLabel(tr("Download everything within:")), GBC.eol());
956 String s[] = new String[dist.length];
957 for (int i = 0; i < dist.length; ++i) {
958 s[i] = tr("{0} meters", dist[i]);
959 }
960 JList buffer = new JList(s);
961 msg.add(buffer, GBC.eol());
962 msg.add(new JLabel(tr("Maximum area per request:")), GBC.eol());
963 s = new String[area.length];
964 for (int i = 0; i < area.length; ++i) {
965 s[i] = tr("{0} sq km", area[i]);
966 }
967 JList maxRect = new JList(s);
968 msg.add(maxRect, GBC.eol());
969
970 int ret = JOptionPane.showConfirmDialog(
971 Main.parent,
972 msg,
973 tr("Download from OSM along this track"),
974 JOptionPane.OK_CANCEL_OPTION,
975 JOptionPane.QUESTION_MESSAGE
976 );
977 switch(ret) {
978 case JOptionPane.CANCEL_OPTION:
979 case JOptionPane.CLOSED_OPTION:
980 return;
981 default:
982 // continue
983 }
984
985 /*
986 * Find the average latitude for the data we're contemplating, so we can know how many
987 * metres per degree of longitude we have.
988 */
989 double latsum = 0;
990 int latcnt = 0;
991
992 for (GpxTrack trk : data.tracks) {
993 for (GpxTrackSegment segment : trk.getSegments()) {
994 for (WayPoint p : segment.getWayPoints()) {
995 latsum += p.getCoor().lat();
996 latcnt++;
997 }
998 }
999 }
1000
1001 double avglat = latsum / latcnt;
1002 double scale = Math.cos(Math.toRadians(avglat));
1003
1004 /*
1005 * Compute buffer zone extents and maximum bounding box size. Note that the maximum we
1006 * ever offer is a bbox area of 0.002, while the API theoretically supports 0.25, but as
1007 * soon as you touch any built-up area, that kind of bounding box will download forever
1008 * and then stop because it has more than 50k nodes.
1009 */
1010 Integer i = buffer.getSelectedIndex();
1011 int buffer_dist = dist[i < 0 ? 0 : i];
1012 double buffer_y = buffer_dist / 100000.0;
1013 double buffer_x = buffer_y / scale;
1014 i = maxRect.getSelectedIndex();
1015 double max_area = area[i < 0 ? 0 : i] / 10000.0 / scale;
1016 Area a = new Area();
1017 Rectangle2D r = new Rectangle2D.Double();
1018
1019 /*
1020 * Collect the combined area of all gpx points plus buffer zones around them. We ignore
1021 * points that lie closer to the previous point than the given buffer size because
1022 * otherwise this operation takes ages.
1023 */
1024 LatLon previous = null;
1025 for (GpxTrack trk : data.tracks) {
1026 for (GpxTrackSegment segment : trk.getSegments()) {
1027 for (WayPoint p : segment.getWayPoints()) {
1028 LatLon c = p.getCoor();
1029 if (previous == null || c.greatCircleDistance(previous) > buffer_dist) {
1030 // we add a buffer around the point.
1031 r.setRect(c.lon() - buffer_x, c.lat() - buffer_y, 2 * buffer_x, 2 * buffer_y);
1032 a.add(new Area(r));
1033 previous = c;
1034 }
1035 }
1036 }
1037 }
1038
1039 /*
1040 * Area "a" now contains the hull that we would like to download data for. however we
1041 * can only download rectangles, so the following is an attempt at finding a number of
1042 * rectangles to download.
1043 *
1044 * The idea is simply: Start out with the full bounding box. If it is too large, then
1045 * split it in half and repeat recursively for each half until you arrive at something
1046 * small enough to download. The algorithm is improved by always using the intersection
1047 * between the rectangle and the actual desired area. For example, if you have a track
1048 * that goes like this: +----+ | /| | / | | / | |/ | +----+ then we would first look at
1049 * downloading the whole rectangle (assume it's too big), after that we split it in half
1050 * (upper and lower half), but we donot request the full upper and lower rectangle, only
1051 * the part of the upper/lower rectangle that actually has something in it.
1052 */
1053
1054 List<Rectangle2D> toDownload = new ArrayList<Rectangle2D>();
1055
1056 addToDownload(a, a.getBounds(), toDownload, max_area);
1057
1058 msg = new JPanel(new GridBagLayout());
1059
1060 msg.add(new JLabel(
1061 tr("<html>This action will require {0} individual<br>"
1062 + "download requests. Do you wish<br>to continue?</html>",
1063 toDownload.size())), GBC.eol());
1064
1065 if (toDownload.size() > 1) {
1066 ret = JOptionPane.showConfirmDialog(
1067 Main.parent,
1068 msg,
1069 tr("Download from OSM along this track"),
1070 JOptionPane.OK_CANCEL_OPTION,
1071 JOptionPane.PLAIN_MESSAGE
1072 );
1073 switch(ret) {
1074 case JOptionPane.CANCEL_OPTION:
1075 case JOptionPane.CLOSED_OPTION:
1076 return;
1077 default:
1078 // continue
1079 }
1080 }
1081 final PleaseWaitProgressMonitor monitor = new PleaseWaitProgressMonitor(tr("Download data"));
1082 final Future<?> future = new DownloadOsmTaskList().download(false, toDownload, monitor);
1083 Main.worker.submit(
1084 new Runnable() {
1085 public void run() {
1086 try {
1087 future.get();
1088 } catch(Exception e) {
1089 e.printStackTrace();
1090 return;
1091 }
1092 monitor.close();
1093 }
1094 }
1095 );
1096 }
1097 }
1098
1099 private static void addToDownload(Area a, Rectangle2D r, Collection<Rectangle2D> results, double max_area) {
1100 Area tmp = new Area(r);
1101 // intersect with sought-after area
1102 tmp.intersect(a);
1103 if (tmp.isEmpty())
1104 return;
1105 Rectangle2D bounds = tmp.getBounds2D();
1106 if (bounds.getWidth() * bounds.getHeight() > max_area) {
1107 // the rectangle gets too large; split it and make recursive call.
1108 Rectangle2D r1;
1109 Rectangle2D r2;
1110 if (bounds.getWidth() > bounds.getHeight()) {
1111 // rectangles that are wider than high are split into a left and right half,
1112 r1 = new Rectangle2D.Double(bounds.getX(), bounds.getY(), bounds.getWidth() / 2, bounds.getHeight());
1113 r2 = new Rectangle2D.Double(bounds.getX() + bounds.getWidth() / 2, bounds.getY(),
1114 bounds.getWidth() / 2, bounds.getHeight());
1115 } else {
1116 // others into a top and bottom half.
1117 r1 = new Rectangle2D.Double(bounds.getX(), bounds.getY(), bounds.getWidth(), bounds.getHeight() / 2);
1118 r2 = new Rectangle2D.Double(bounds.getX(), bounds.getY() + bounds.getHeight() / 2, bounds.getWidth(),
1119 bounds.getHeight() / 2);
1120 }
1121 addToDownload(a, r1, results, max_area);
1122 addToDownload(a, r2, results, max_area);
1123 } else {
1124 results.add(bounds);
1125 }
1126 }
1127
1128 /**
1129 * Makes a new marker layer derived from this GpxLayer containing at least one audio marker
1130 * which the given audio file is associated with. Markers are derived from the following (a)
1131 * explict waypoints in the GPX layer, or (b) named trackpoints in the GPX layer, or (d)
1132 * timestamp on the wav file (e) (in future) voice recognised markers in the sound recording (f)
1133 * a single marker at the beginning of the track
1134 * @param wavFile : the file to be associated with the markers in the new marker layer
1135 * @param markers : keeps track of warning messages to avoid repeated warnings
1136 */
1137 private void importAudio(File wavFile, MarkerLayer ml, double firstStartTime, Markers markers) {
1138 String uri = "file:".concat(wavFile.getAbsolutePath());
1139 Collection<WayPoint> waypoints = new ArrayList<WayPoint>();
1140 boolean timedMarkersOmitted = false;
1141 boolean untimedMarkersOmitted = false;
1142 double snapDistance = Main.pref.getDouble("marker.audiofromuntimedwaypoints.distance", 1.0e-3); /*
1143 * about
1144 * 25
1145 * m
1146 */
1147 WayPoint wayPointFromTimeStamp = null;
1148
1149 // determine time of first point in track
1150 double firstTime = -1.0;
1151 if (data.tracks != null && !data.tracks.isEmpty()) {
1152 for (GpxTrack track : data.tracks) {
1153 for (GpxTrackSegment seg : track.getSegments()) {
1154 for (WayPoint w : seg.getWayPoints()) {
1155 firstTime = w.time;
1156 break;
1157 }
1158 if (firstTime >= 0.0) {
1159 break;
1160 }
1161 }
1162 if (firstTime >= 0.0) {
1163 break;
1164 }
1165 }
1166 }
1167 if (firstTime < 0.0) {
1168 JOptionPane.showMessageDialog(
1169 Main.parent,
1170 tr("No GPX track available in layer to associate audio with."),
1171 tr("Error"),
1172 JOptionPane.ERROR_MESSAGE
1173 );
1174 return;
1175 }
1176
1177 // (a) try explicit timestamped waypoints - unless suppressed
1178 if (Main.pref.getBoolean("marker.audiofromexplicitwaypoints", true) && data.waypoints != null
1179 && !data.waypoints.isEmpty()) {
1180 for (WayPoint w : data.waypoints) {
1181 if (w.time > firstTime) {
1182 waypoints.add(w);
1183 } else if (w.time > 0.0) {
1184 timedMarkersOmitted = true;
1185 }
1186 }
1187 }
1188
1189 // (b) try explicit waypoints without timestamps - unless suppressed
1190 if (Main.pref.getBoolean("marker.audiofromuntimedwaypoints", true) && data.waypoints != null
1191 && !data.waypoints.isEmpty()) {
1192 for (WayPoint w : data.waypoints) {
1193 if (waypoints.contains(w)) {
1194 continue;
1195 }
1196 WayPoint wNear = nearestPointOnTrack(w.getEastNorth(), snapDistance);
1197 if (wNear != null) {
1198 WayPoint wc = new WayPoint(w.getCoor());
1199 wc.time = wNear.time;
1200 if (w.attr.containsKey("name")) {
1201 wc.attr.put("name", w.getString("name"));
1202 }
1203 waypoints.add(wc);
1204 } else {
1205 untimedMarkersOmitted = true;
1206 }
1207 }
1208 }
1209
1210 // (c) use explicitly named track points, again unless suppressed
1211 if ((Main.pref.getBoolean("marker.audiofromnamedtrackpoints", false)) && data.tracks != null
1212 && !data.tracks.isEmpty()) {
1213 for (GpxTrack track : data.tracks) {
1214 for (GpxTrackSegment seg : track.getSegments()) {
1215 for (WayPoint w : seg.getWayPoints()) {
1216 if (w.attr.containsKey("name") || w.attr.containsKey("desc")) {
1217 waypoints.add(w);
1218 }
1219 }
1220 }
1221 }
1222 }
1223
1224 // (d) use timestamp of file as location on track
1225 if ((Main.pref.getBoolean("marker.audiofromwavtimestamps", false)) && data.tracks != null
1226 && !data.tracks.isEmpty()) {
1227 double lastModified = wavFile.lastModified() / 1000.0; // lastModified is in
1228 // milliseconds
1229 double duration = AudioUtil.getCalibratedDuration(wavFile);
1230 double startTime = lastModified - duration;
1231 startTime = firstStartTime + (startTime - firstStartTime)
1232 / Main.pref.getDouble("audio.calibration", "1.0" /* default, ratio */);
1233 WayPoint w1 = null;
1234 WayPoint w2 = null;
1235
1236 for (GpxTrack track : data.tracks) {
1237 for (GpxTrackSegment seg : track.getSegments()) {
1238 for (WayPoint w : seg.getWayPoints()) {
1239 if (startTime < w.time) {
1240 w2 = w;
1241 break;
1242 }
1243 w1 = w;
1244 }
1245 if (w2 != null) {
1246 break;
1247 }
1248 }
1249 }
1250
1251 if (w1 == null || w2 == null) {
1252 timedMarkersOmitted = true;
1253 } else {
1254 wayPointFromTimeStamp = new WayPoint(w1.getCoor().interpolate(w2.getCoor(),
1255 (startTime - w1.time) / (w2.time - w1.time)));
1256 wayPointFromTimeStamp.time = startTime;
1257 String name = wavFile.getName();
1258 int dot = name.lastIndexOf(".");
1259 if (dot > 0) {
1260 name = name.substring(0, dot);
1261 }
1262 wayPointFromTimeStamp.attr.put("name", name);
1263 waypoints.add(wayPointFromTimeStamp);
1264 }
1265 }
1266
1267 // (e) analyse audio for spoken markers here, in due course
1268
1269 // (f) simply add a single marker at the start of the track
1270 if ((Main.pref.getBoolean("marker.audiofromstart") || waypoints.isEmpty()) && data.tracks != null
1271 && !data.tracks.isEmpty()) {
1272 boolean gotOne = false;
1273 for (GpxTrack track : data.tracks) {
1274 for (GpxTrackSegment seg : track.getSegments()) {
1275 for (WayPoint w : seg.getWayPoints()) {
1276 WayPoint wStart = new WayPoint(w.getCoor());
1277 wStart.attr.put("name", "start");
1278 wStart.time = w.time;
1279 waypoints.add(wStart);
1280 gotOne = true;
1281 break;
1282 }
1283 if (gotOne) {
1284 break;
1285 }
1286 }
1287 if (gotOne) {
1288 break;
1289 }
1290 }
1291 }
1292
1293 /* we must have got at least one waypoint now */
1294
1295 Collections.sort((ArrayList<WayPoint>) waypoints, new Comparator<WayPoint>() {
1296 public int compare(WayPoint a, WayPoint b) {
1297 return a.time <= b.time ? -1 : 1;
1298 }
1299 });
1300
1301 firstTime = -1.0; /* this time of the first waypoint, not first trackpoint */
1302 for (WayPoint w : waypoints) {
1303 if (firstTime < 0.0) {
1304 firstTime = w.time;
1305 }
1306 double offset = w.time - firstTime;
1307 String name;
1308 if (w.attr.containsKey("name")) {
1309 name = w.getString("name");
1310 } else if (w.attr.containsKey("desc")) {
1311 name = w.getString("desc");
1312 } else {
1313 name = AudioMarker.inventName(offset);
1314 }
1315 AudioMarker am = AudioMarker.create(w.getCoor(), name, uri, ml, w.time, offset);
1316 /*
1317 * timeFromAudio intended for future use to shift markers of this type on
1318 * synchronization
1319 */
1320 if (w == wayPointFromTimeStamp) {
1321 am.timeFromAudio = true;
1322 }
1323 ml.data.add(am);
1324 }
1325
1326 if (timedMarkersOmitted && !markers.timedMarkersOmitted) {
1327 JOptionPane
1328 .showMessageDialog(
1329 Main.parent,
1330 tr("Some waypoints with timestamps from before the start of the track or after the end were omitted or moved to the start."));
1331 markers.timedMarkersOmitted = timedMarkersOmitted;
1332 }
1333 if (untimedMarkersOmitted && !markers.untimedMarkersOmitted) {
1334 JOptionPane
1335 .showMessageDialog(
1336 Main.parent,
1337 tr("Some waypoints which were too far from the track to sensibly estimate their time were omitted."));
1338 markers.untimedMarkersOmitted = untimedMarkersOmitted;
1339 }
1340 }
1341
1342 /**
1343 * Makes a WayPoint at the projection of point P onto the track providing P is less than
1344 * tolerance away from the track
1345 *
1346 * @param P : the point to determine the projection for
1347 * @param tolerance : must be no further than this from the track
1348 * @return the closest point on the track to P, which may be the first or last point if off the
1349 * end of a segment, or may be null if nothing close enough
1350 */
1351 public WayPoint nearestPointOnTrack(EastNorth P, double tolerance) {
1352 /*
1353 * assume the coordinates of P are xp,yp, and those of a section of track between two
1354 * trackpoints are R=xr,yr and S=xs,ys. Let N be the projected point.
1355 *
1356 * The equation of RS is Ax + By + C = 0 where A = ys - yr B = xr - xs C = - Axr - Byr
1357 *
1358 * Also, note that the distance RS^2 is A^2 + B^2
1359 *
1360 * If RS^2 == 0.0 ignore the degenerate section of track
1361 *
1362 * PN^2 = (Axp + Byp + C)^2 / RS^2 that is the distance from P to the line
1363 *
1364 * so if PN^2 is less than PNmin^2 (initialized to tolerance) we can reject the line;
1365 * otherwise... determine if the projected poijnt lies within the bounds of the line: PR^2 -
1366 * PN^2 <= RS^2 and PS^2 - PN^2 <= RS^2
1367 *
1368 * where PR^2 = (xp - xr)^2 + (yp-yr)^2 and PS^2 = (xp - xs)^2 + (yp-ys)^2
1369 *
1370 * If so, calculate N as xn = xr + (RN/RS) B yn = y1 + (RN/RS) A
1371 *
1372 * where RN = sqrt(PR^2 - PN^2)
1373 */
1374
1375 double PNminsq = tolerance * tolerance;
1376 EastNorth bestEN = null;
1377 double bestTime = 0.0;
1378 double px = P.east();
1379 double py = P.north();
1380 double rx = 0.0, ry = 0.0, sx, sy, x, y;
1381 if (data.tracks == null)
1382 return null;
1383 for (GpxTrack track : data.tracks) {
1384 for (GpxTrackSegment seg : track.getSegments()) {
1385 WayPoint R = null;
1386 for (WayPoint S : seg.getWayPoints()) {
1387 EastNorth c = S.getEastNorth();
1388 if (R == null) {
1389 R = S;
1390 rx = c.east();
1391 ry = c.north();
1392 x = px - rx;
1393 y = py - ry;
1394 double PRsq = x * x + y * y;
1395 if (PRsq < PNminsq) {
1396 PNminsq = PRsq;
1397 bestEN = c;
1398 bestTime = R.time;
1399 }
1400 } else {
1401 sx = c.east();
1402 sy = c.north();
1403 double A = sy - ry;
1404 double B = rx - sx;
1405 double C = -A * rx - B * ry;
1406 double RSsq = A * A + B * B;
1407 if (RSsq == 0.0) {
1408 continue;
1409 }
1410 double PNsq = A * px + B * py + C;
1411 PNsq = PNsq * PNsq / RSsq;
1412 if (PNsq < PNminsq) {
1413 x = px - rx;
1414 y = py - ry;
1415 double PRsq = x * x + y * y;
1416 x = px - sx;
1417 y = py - sy;
1418 double PSsq = x * x + y * y;
1419 if (PRsq - PNsq <= RSsq && PSsq - PNsq <= RSsq) {
1420 double RNoverRS = Math.sqrt((PRsq - PNsq) / RSsq);
1421 double nx = rx - RNoverRS * B;
1422 double ny = ry + RNoverRS * A;
1423 bestEN = new EastNorth(nx, ny);
1424 bestTime = R.time + RNoverRS * (S.time - R.time);
1425 PNminsq = PNsq;
1426 }
1427 }
1428 R = S;
1429 rx = sx;
1430 ry = sy;
1431 }
1432 }
1433 if (R != null) {
1434 EastNorth c = R.getEastNorth();
1435 /* if there is only one point in the seg, it will do this twice, but no matter */
1436 rx = c.east();
1437 ry = c.north();
1438 x = px - rx;
1439 y = py - ry;
1440 double PRsq = x * x + y * y;
1441 if (PRsq < PNminsq) {
1442 PNminsq = PRsq;
1443 bestEN = c;
1444 bestTime = R.time;
1445 }
1446 }
1447 }
1448 }
1449 if (bestEN == null)
1450 return null;
1451 WayPoint best = new WayPoint(Main.proj.eastNorth2latlon(bestEN));
1452 best.time = bestTime;
1453 return best;
1454 }
1455}
Note: See TracBrowser for help on using the repository browser.