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

Last change on this file since 2635 was 2635, checked in by bastiK, 14 years ago

fixed gpx-rendering error: Tracksegments should not be connected by a line (unless explicitly demanded by the user)

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