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

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

Added canceling of DownloadOsmTaskLists
Removed error remembering in the progress dialog

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