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

Last change on this file since 2151 was 2151, checked in by Gubaer, 15 years ago

applied #3526: patch by glebius@...: be more verbose about available traces in GPX layer

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