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

Last change on this file since 2146 was 2146, checked in by stoecker, 15 years ago

applied #3425 - patch by dmuecke - Import Audio gives excessive warnings

  • Property svn:eol-style set to native
File size: 56.9 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 info.append(trn("{0} track, ", "{0} tracks, ", data.tracks.size(), data.tracks.size())).append(
340 trn("{0} route, ", "{0} routes, ", data.routes.size(), data.routes.size())).append(
341 trn("{0} waypoint", "{0} waypoints", data.waypoints.size(), data.waypoints.size())).append("<br>");
342
343 if (data.attr.containsKey("name")) {
344 info.append(tr("Name: {0}", data.attr.get(GpxData.META_NAME))).append("<br>");
345 }
346
347 if (data.attr.containsKey("desc")) {
348 info.append(tr("Description: {0}", data.attr.get(GpxData.META_DESC))).append("<br>");
349 }
350
351 if (data.tracks.size() > 0) {
352 boolean first = true;
353 WayPoint earliest = null, latest = null;
354
355 for (GpxTrack trk : data.tracks) {
356 for (Collection<WayPoint> seg : trk.trackSegs) {
357 for (WayPoint pnt : seg) {
358 if (first) {
359 latest = earliest = pnt;
360 first = false;
361 } else {
362 if (pnt.compareTo(earliest) < 0) {
363 earliest = pnt;
364 } else {
365 latest = pnt;
366 }
367 }
368 }
369 }
370 }
371 if (earliest != null && latest != null) {
372 DateFormat df = DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.SHORT);
373 info.append(tr("Timespan: ") + df.format(new Date((long) (earliest.time * 1000))) + " - "
374 + df.format(new Date((long) (latest.time * 1000))));
375 int diff = (int) (latest.time - earliest.time);
376 info.append(" (" + (diff / 3600) + ":" + ((diff % 3600) / 60) + ")");
377 info.append("<br>");
378 }
379 }
380 info.append(tr("Length: ") + new DecimalFormat("#0.00").format(data.length() / 1000) + "km");
381 info.append("<br>");
382
383 return info.append("</html>").toString();
384 }
385
386 @Override
387 public boolean isMergable(Layer other) {
388 return other instanceof GpxLayer;
389 }
390
391 @Override
392 public void mergeFrom(Layer from) {
393 data.mergeFrom(((GpxLayer) from).data);
394 computeCacheInSync = false;
395 }
396
397 private static Color[] colors = new Color[256];
398 static {
399 for (int i = 0; i < colors.length; i++) {
400 colors[i] = Color.getHSBColor(i / 300.0f, 1, 1);
401 }
402 }
403
404 // lookup array to draw arrows without doing any math
405 private static int ll0 = 9;
406 private static int sl4 = 5;
407 private static int sl9 = 3;
408 private static int[][] dir = { { +sl4, +ll0, +ll0, +sl4 }, { -sl9, +ll0, +sl9, +ll0 }, { -ll0, +sl4, -sl4, +ll0 },
409 { -ll0, -sl9, -ll0, +sl9 }, { -sl4, -ll0, -ll0, -sl4 }, { +sl9, -ll0, -sl9, -ll0 },
410 { +ll0, -sl4, +sl4, -ll0 }, { +ll0, +sl9, +ll0, -sl9 }, { +sl4, +ll0, +ll0, +sl4 },
411 { -sl9, +ll0, +sl9, +ll0 }, { -ll0, +sl4, -sl4, +ll0 }, { -ll0, -sl9, -ll0, +sl9 } };
412
413 // the different color modes
414 enum colorModes {
415 none, velocity, dilution
416 }
417
418 @Override
419 public void paint(Graphics g, MapView mv) {
420
421 /****************************************************************
422 ********** STEP 1 - GET CONFIG VALUES **************************
423 ****************************************************************/
424 // Long startTime = System.currentTimeMillis();
425 Color neutralColor = getColor(getName());
426 // also draw lines between points belonging to different segments
427 boolean forceLines = Main.pref.getBoolean("draw.rawgps.lines.force");
428 // draw direction arrows on the lines
429 boolean direction = Main.pref.getBoolean("draw.rawgps.direction");
430 // don't draw lines if longer than x meters
431 int lineWidth = Main.pref.getInteger("draw.rawgps.linewidth",0);
432
433 int maxLineLength;
434 if (this.isLocalFile) {
435 maxLineLength = Main.pref.getInteger("draw.rawgps.max-line-length.local", -1);
436 } else {
437 maxLineLength = Main.pref.getInteger("draw.rawgps.max-line-length", 200);
438 }
439 // draw line between points, global setting
440 boolean lines = (Main.pref.getBoolean("draw.rawgps.lines", true) || (Main.pref
441 .getBoolean("draw.rawgps.lines.localfiles") && this.isLocalFile));
442 String linesKey = "draw.rawgps.lines.layer " + getName();
443 // draw lines, per-layer setting
444 if (Main.pref.hasKey(linesKey)) {
445 lines = Main.pref.getBoolean(linesKey);
446 }
447 // paint large dots for points
448 boolean large = Main.pref.getBoolean("draw.rawgps.large");
449 boolean hdopcircle = Main.pref.getBoolean("draw.rawgps.hdopcircle", true);
450 // color the lines
451 colorModes colored = colorModes.none;
452 try {
453 colored = colorModes.values()[Main.pref.getInteger("draw.rawgps.colors", 0)];
454 } catch (Exception e) {
455 }
456 // paint direction arrow with alternate math. may be faster
457 boolean alternatedirection = Main.pref.getBoolean("draw.rawgps.alternatedirection");
458 // don't draw arrows nearer to each other than this
459 int delta = Main.pref.getInteger("draw.rawgps.min-arrow-distance", 0);
460 // allows to tweak line coloring for different speed levels.
461 int colorTracksTune = Main.pref.getInteger("draw.rawgps.colorTracksTune", 45);
462
463 if(lineWidth != 0)
464 {
465 Graphics2D g2d = (Graphics2D)g;
466 g2d.setStroke(new BasicStroke(lineWidth,BasicStroke.CAP_ROUND,BasicStroke.JOIN_ROUND));
467 }
468
469 /****************************************************************
470 ********** STEP 2a - CHECK CACHE VALIDITY **********************
471 ****************************************************************/
472 if (computeCacheInSync
473 && ((computeCacheMaxLineLengthUsed != maxLineLength) || (!neutralColor.equals(computeCacheColorUsed))
474 || (computeCacheColored != colored) || (computeCacheColorTracksTune != colorTracksTune))) {
475 // System.out.println("(re-)computing gpx line styles, reason: CCIS=" +
476 // computeCacheInSync + " CCMLLU=" + (computeCacheMaxLineLengthUsed != maxLineLength) +
477 // " CCCU=" + (!neutralColor.equals(computeCacheColorUsed)) + " CCC=" +
478 // (computeCacheColored != colored));
479 computeCacheMaxLineLengthUsed = maxLineLength;
480 computeCacheInSync = false;
481 computeCacheColorUsed = neutralColor;
482 computeCacheColored = colored;
483 computeCacheColorTracksTune = colorTracksTune;
484 }
485
486 /****************************************************************
487 ********** STEP 2b - RE-COMPUTE CACHE DATA *********************
488 ****************************************************************/
489 if (!computeCacheInSync) { // don't compute if the cache is good
490 WayPoint oldWp = null;
491 for (GpxTrack trk : data.tracks) {
492 if (!forceLines) { // don't draw lines between segments, unless forced to
493 oldWp = null;
494 }
495 for (Collection<WayPoint> segment : trk.trackSegs) {
496 for (WayPoint trkPnt : segment) {
497 LatLon c = trkPnt.getCoor();
498 if (Double.isNaN(c.lat()) || Double.isNaN(c.lon())) {
499 continue;
500 }
501 trkPnt.customColoring = neutralColor;
502 if (oldWp != null) {
503 double dist = c.greatCircleDistance(oldWp.getCoor());
504
505 switch (colored) {
506 case velocity:
507 double dtime = trkPnt.time - oldWp.time;
508 double vel = dist / dtime;
509 double velColor = vel / colorTracksTune * 255;
510 // Bad case first
511 if (dtime <= 0 || vel < 0 || velColor > 255) {
512 trkPnt.customColoring = colors[255];
513 } else {
514 trkPnt.customColoring = colors[(int) (velColor)];
515 }
516 break;
517
518 case dilution:
519 if (trkPnt.attr.get("hdop") != null) {
520 float hdop = ((Float) trkPnt.attr.get("hdop")).floatValue();
521 if (hdop < 0) {
522 hdop = 0;
523 }
524 int hdoplvl = Math.round(hdop * Main.pref.getInteger("hdop.factor", 25));
525 // High hdop is bad, but high values in colors are green.
526 // Therefore inverse the logic
527 int hdopcolor = 255 - (hdoplvl > 255 ? 255 : hdoplvl);
528 trkPnt.customColoring = colors[hdopcolor];
529 }
530 break;
531 }
532
533 if (maxLineLength == -1 || dist <= maxLineLength) {
534 trkPnt.drawLine = true;
535 trkPnt.dir = (int) oldWp.getCoor().heading(trkPnt.getCoor());
536 } else {
537 trkPnt.drawLine = false;
538 }
539 } else { // make sure we reset outdated data
540 trkPnt.drawLine = false;
541 }
542 oldWp = trkPnt;
543 }
544 }
545 }
546 computeCacheInSync = true;
547 }
548
549 /****************************************************************
550 ********** STEP 3a - DRAW LINES ********************************
551 ****************************************************************/
552 if (lines) {
553 Point old = null;
554 for (GpxTrack trk : data.tracks) {
555 for (Collection<WayPoint> segment : trk.trackSegs) {
556 for (WayPoint trkPnt : segment) {
557 LatLon c = trkPnt.getCoor();
558 if (Double.isNaN(c.lat()) || Double.isNaN(c.lon())) {
559 continue;
560 }
561 Point screen = mv.getPoint(trkPnt.getEastNorth());
562 if (trkPnt.drawLine) {
563 // skip points that are on the same screenposition
564 if (old != null && ((old.x != screen.x) || (old.y != screen.y))) {
565 g.setColor(trkPnt.customColoring);
566 g.drawLine(old.x, old.y, screen.x, screen.y);
567 }
568 }
569 old = screen;
570 } // end for trkpnt
571 } // end for segment
572 } // end for trk
573 } // end if lines
574
575 /****************************************************************
576 ********** STEP 3b - DRAW NICE ARROWS **************************
577 ****************************************************************/
578 if (lines && direction && !alternatedirection) {
579 Point old = null;
580 Point oldA = null; // last arrow painted
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 if (trkPnt.drawLine) {
589 Point screen = mv.getPoint(trkPnt.getEastNorth());
590 // skip points that are on the same screenposition
591 if (old != null
592 && (oldA == null || screen.x < oldA.x - delta || screen.x > oldA.x + delta
593 || screen.y < oldA.y - delta || screen.y > oldA.y + delta)) {
594 g.setColor(trkPnt.customColoring);
595 double t = Math.atan2(screen.y - old.y, screen.x - old.x) + Math.PI;
596 g.drawLine(screen.x, screen.y, (int) (screen.x + 10 * Math.cos(t - PHI)),
597 (int) (screen.y + 10 * Math.sin(t - PHI)));
598 g.drawLine(screen.x, screen.y, (int) (screen.x + 10 * Math.cos(t + PHI)),
599 (int) (screen.y + 10 * Math.sin(t + PHI)));
600 oldA = screen;
601 }
602 old = screen;
603 }
604 } // end for trkpnt
605 } // end for segment
606 } // end for trk
607 } // end if lines
608
609 /****************************************************************
610 ********** STEP 3c - DRAW FAST ARROWS **************************
611 ****************************************************************/
612 if (lines && direction && alternatedirection) {
613 Point old = null;
614 Point oldA = null; // last arrow painted
615 for (GpxTrack trk : data.tracks) {
616 for (Collection<WayPoint> segment : trk.trackSegs) {
617 for (WayPoint trkPnt : segment) {
618 LatLon c = trkPnt.getCoor();
619 if (Double.isNaN(c.lat()) || Double.isNaN(c.lon())) {
620 continue;
621 }
622 if (trkPnt.drawLine) {
623 Point screen = mv.getPoint(trkPnt.getEastNorth());
624 // skip points that are on the same screenposition
625 if (old != null
626 && (oldA == null || screen.x < oldA.x - delta || screen.x > oldA.x + delta
627 || screen.y < oldA.y - delta || screen.y > oldA.y + delta)) {
628 g.setColor(trkPnt.customColoring);
629 g.drawLine(screen.x, screen.y, screen.x + dir[trkPnt.dir][0], screen.y
630 + dir[trkPnt.dir][1]);
631 g.drawLine(screen.x, screen.y, screen.x + dir[trkPnt.dir][2], screen.y
632 + dir[trkPnt.dir][3]);
633 oldA = screen;
634 }
635 old = screen;
636 }
637 } // end for trkpnt
638 } // end for segment
639 } // end for trk
640 } // end if lines
641
642
643 /****************************************************************
644 ********** STEP 3d - DRAW LARGE POINTS AND HDOP CIRCLE *********
645 ****************************************************************/
646 if (large || hdopcircle) {
647 g.setColor(neutralColor);
648 for (GpxTrack trk : data.tracks) {
649 for (Collection<WayPoint> segment : trk.trackSegs) {
650 for (WayPoint trkPnt : segment) {
651 LatLon c = trkPnt.getCoor();
652 if (Double.isNaN(c.lat()) || Double.isNaN(c.lon())) {
653 continue;
654 }
655 Point screen = mv.getPoint(trkPnt.getEastNorth());
656 g.setColor(trkPnt.customColoring);
657 if (hdopcircle && trkPnt.attr.get("hdop") != null) {
658 // hdop value
659 float hdop = ((Float)trkPnt.attr.get("hdop")).floatValue();
660 if (hdop < 0) {
661 hdop = 0;
662 }
663 // hdop pixels
664 int hdopp = mv.getPoint(new LatLon(trkPnt.getCoor().lat(), trkPnt.getCoor().lon() + 2*6*hdop*360/40000000)).x - screen.x;
665 g.drawArc(screen.x-hdopp/2, screen.y-hdopp/2, hdopp, hdopp, 0, 360);
666 }
667 if (large) {
668 g.fillRect(screen.x-1, screen.y-1, 3, 3);
669 }
670 } // end for trkpnt
671 } // end for segment
672 } // end for trk
673 } // end if large || hdopcircle
674
675 /****************************************************************
676 ********** STEP 3e - DRAW SMALL POINTS FOR LINES ***************
677 ****************************************************************/
678 if (!large && lines) {
679 g.setColor(neutralColor);
680 for (GpxTrack trk : data.tracks) {
681 for (Collection<WayPoint> segment : trk.trackSegs) {
682 for (WayPoint trkPnt : segment) {
683 LatLon c = trkPnt.getCoor();
684 if (Double.isNaN(c.lat()) || Double.isNaN(c.lon())) {
685 continue;
686 }
687 if (!trkPnt.drawLine) {
688 Point screen = mv.getPoint(trkPnt.getEastNorth());
689 g.drawRect(screen.x, screen.y, 0, 0);
690 }
691 } // end for trkpnt
692 } // end for segment
693 } // end for trk
694 } // end if large
695
696 /****************************************************************
697 ********** STEP 3f - DRAW SMALL POINTS INSTEAD OF LINES ********
698 ****************************************************************/
699 if (!large && !lines) {
700 g.setColor(neutralColor);
701 for (GpxTrack trk : data.tracks) {
702 for (Collection<WayPoint> segment : trk.trackSegs) {
703 for (WayPoint trkPnt : segment) {
704 LatLon c = trkPnt.getCoor();
705 if (Double.isNaN(c.lat()) || Double.isNaN(c.lon())) {
706 continue;
707 }
708 Point screen = mv.getPoint(trkPnt.getEastNorth());
709 g.setColor(trkPnt.customColoring);
710 g.drawRect(screen.x, screen.y, 0, 0);
711 } // end for trkpnt
712 } // end for segment
713 } // end for trk
714 } // end if large
715
716 // Long duration = System.currentTimeMillis() - startTime;
717 // System.out.println(duration);
718 } // end paint
719
720 @Override
721 public void visitBoundingBox(BoundingXYVisitor v) {
722 v.visit(data.recalculateBounds());
723 }
724
725 public class ConvertToDataLayerAction extends AbstractAction {
726 public ConvertToDataLayerAction() {
727 super(tr("Convert to data layer"), ImageProvider.get("converttoosm"));
728 }
729
730 public void actionPerformed(ActionEvent e) {
731 JPanel msg = new JPanel(new GridBagLayout());
732 msg
733 .add(
734 new JLabel(
735 tr("<html>Upload of unprocessed GPS data as map data is considered harmful.<br>If you want to upload traces, look here:")),
736 GBC.eol());
737 msg.add(new UrlLabel(tr("http://www.openstreetmap.org/traces")), GBC.eop());
738 if (!ConditionalOptionPaneUtil.showConfirmationDialog("convert_to_data", Main.parent, msg, tr("Warning"),
739 JOptionPane.OK_CANCEL_OPTION, JOptionPane.WARNING_MESSAGE, JOptionPane.OK_OPTION))
740 return;
741 DataSet ds = new DataSet();
742 for (GpxTrack trk : data.tracks) {
743 for (Collection<WayPoint> segment : trk.trackSegs) {
744 List<Node> nodes = new ArrayList<Node>();
745 for (WayPoint p : segment) {
746 Node n = new Node(p.getCoor());
747 String timestr = p.getString("time");
748 if (timestr != null) {
749 n.setTimestamp(DateUtils.fromString(timestr));
750 }
751 ds.nodes.add(n);
752 nodes.add(n);
753 }
754 Way w = new Way();
755 w.setNodes(nodes);
756 ds.ways.add(w);
757 }
758 }
759 Main.main
760 .addLayer(new OsmDataLayer(ds, tr("Converted from: {0}", GpxLayer.this.getName()), getAssociatedFile()));
761 Main.main.removeLayer(GpxLayer.this);
762 }
763 }
764
765 @Override
766 public File getAssociatedFile() {
767 return data.storageFile;
768 }
769
770 @Override
771 public void setAssociatedFile(File file) {
772 data.storageFile = file;
773 }
774
775 /**
776 * Action that issues a series of download requests to the API, following the GPX track.
777 *
778 * @author fred
779 */
780 public class DownloadAlongTrackAction extends AbstractAction {
781 public DownloadAlongTrackAction() {
782 super(tr("Download from OSM along this track"), ImageProvider.get("downloadalongtrack"));
783 }
784
785 public void actionPerformed(ActionEvent e) {
786 JPanel msg = new JPanel(new GridBagLayout());
787 Integer dist[] = { 5000, 500, 50 };
788 Integer area[] = { 20, 10, 5, 1 };
789
790 msg.add(new JLabel(tr("Download everything within:")), GBC.eol());
791 String s[] = new String[dist.length];
792 for (int i = 0; i < dist.length; ++i) {
793 s[i] = tr("{0} meters", dist[i]);
794 }
795 JList buffer = new JList(s);
796 msg.add(buffer, GBC.eol());
797 msg.add(new JLabel(tr("Maximum area per request:")), GBC.eol());
798 s = new String[area.length];
799 for (int i = 0; i < area.length; ++i) {
800 s[i] = tr("{0} sq km", area[i]);
801 }
802 JList maxRect = new JList(s);
803 msg.add(maxRect, GBC.eol());
804
805 int ret = JOptionPane.showConfirmDialog(
806 Main.parent,
807 msg,
808 tr("Download from OSM along this track"),
809 JOptionPane.OK_CANCEL_OPTION,
810 JOptionPane.QUESTION_MESSAGE
811 );
812 switch(ret) {
813 case JOptionPane.CANCEL_OPTION:
814 case JOptionPane.CLOSED_OPTION:
815 return;
816 default:
817 // continue
818 }
819
820 /*
821 * Find the average latitude for the data we're contemplating, so we can know how many
822 * metres per degree of longitude we have.
823 */
824 double latsum = 0;
825 int latcnt = 0;
826
827 for (GpxTrack trk : data.tracks) {
828 for (Collection<WayPoint> segment : trk.trackSegs) {
829 for (WayPoint p : segment) {
830 latsum += p.getCoor().lat();
831 latcnt++;
832 }
833 }
834 }
835
836 double avglat = latsum / latcnt;
837 double scale = Math.cos(Math.toRadians(avglat));
838
839 /*
840 * Compute buffer zone extents and maximum bounding box size. Note that the maximum we
841 * ever offer is a bbox area of 0.002, while the API theoretically supports 0.25, but as
842 * soon as you touch any built-up area, that kind of bounding box will download forever
843 * and then stop because it has more than 50k nodes.
844 */
845 Integer i = buffer.getSelectedIndex();
846 int buffer_dist = dist[i < 0 ? 0 : i];
847 double buffer_y = buffer_dist / 100000.0;
848 double buffer_x = buffer_y / scale;
849 i = maxRect.getSelectedIndex();
850 double max_area = area[i < 0 ? 0 : i] / 10000.0 / scale;
851 Area a = new Area();
852 Rectangle2D r = new Rectangle2D.Double();
853
854 /*
855 * Collect the combined area of all gpx points plus buffer zones around them. We ignore
856 * points that lie closer to the previous point than the given buffer size because
857 * otherwise this operation takes ages.
858 */
859 LatLon previous = null;
860 for (GpxTrack trk : data.tracks) {
861 for (Collection<WayPoint> segment : trk.trackSegs) {
862 for (WayPoint p : segment) {
863 LatLon c = p.getCoor();
864 if (previous == null || c.greatCircleDistance(previous) > buffer_dist) {
865 // we add a buffer around the point.
866 r.setRect(c.lon() - buffer_x, c.lat() - buffer_y, 2 * buffer_x, 2 * buffer_y);
867 a.add(new Area(r));
868 previous = c;
869 }
870 }
871 }
872 }
873
874 /*
875 * Area "a" now contains the hull that we would like to download data for. however we
876 * can only download rectangles, so the following is an attempt at finding a number of
877 * rectangles to download.
878 *
879 * The idea is simply: Start out with the full bounding box. If it is too large, then
880 * split it in half and repeat recursively for each half until you arrive at something
881 * small enough to download. The algorithm is improved by always using the intersection
882 * between the rectangle and the actual desired area. For example, if you have a track
883 * that goes like this: +----+ | /| | / | | / | |/ | +----+ then we would first look at
884 * downloading the whole rectangle (assume it's too big), after that we split it in half
885 * (upper and lower half), but we donot request the full upper and lower rectangle, only
886 * the part of the upper/lower rectangle that actually has something in it.
887 */
888
889 List<Rectangle2D> toDownload = new ArrayList<Rectangle2D>();
890
891 addToDownload(a, a.getBounds(), toDownload, max_area);
892
893 msg = new JPanel(new GridBagLayout());
894
895 msg
896 .add(
897 new JLabel(
898 tr(
899 "<html>This action will require {0} individual<br>download requests. Do you wish<br>to continue?</html>",
900 toDownload.size())), GBC.eol());
901
902 if (toDownload.size() > 1) {
903 ret = JOptionPane.showConfirmDialog(
904 Main.parent,
905 msg,
906 tr("Download from OSM along this track"),
907 JOptionPane.OK_CANCEL_OPTION,
908 JOptionPane.PLAIN_MESSAGE
909 );
910 switch(ret) {
911 case JOptionPane.CANCEL_OPTION:
912 case JOptionPane.CLOSED_OPTION:
913 return;
914 default:
915 // continue
916 }
917 }
918 new DownloadOsmTaskList().download(false, toDownload, new PleaseWaitProgressMonitor(tr("Download data")));
919 }
920 }
921
922 private static void addToDownload(Area a, Rectangle2D r, Collection<Rectangle2D> results, double max_area) {
923 Area tmp = new Area(r);
924 // intersect with sought-after area
925 tmp.intersect(a);
926 if (tmp.isEmpty())
927 return;
928 Rectangle2D bounds = tmp.getBounds2D();
929 if (bounds.getWidth() * bounds.getHeight() > max_area) {
930 // the rectangle gets too large; split it and make recursive call.
931 Rectangle2D r1;
932 Rectangle2D r2;
933 if (bounds.getWidth() > bounds.getHeight()) {
934 // rectangles that are wider than high are split into a left and right half,
935 r1 = new Rectangle2D.Double(bounds.getX(), bounds.getY(), bounds.getWidth() / 2, bounds.getHeight());
936 r2 = new Rectangle2D.Double(bounds.getX() + bounds.getWidth() / 2, bounds.getY(),
937 bounds.getWidth() / 2, bounds.getHeight());
938 } else {
939 // others into a top and bottom half.
940 r1 = new Rectangle2D.Double(bounds.getX(), bounds.getY(), bounds.getWidth(), bounds.getHeight() / 2);
941 r2 = new Rectangle2D.Double(bounds.getX(), bounds.getY() + bounds.getHeight() / 2, bounds.getWidth(),
942 bounds.getHeight() / 2);
943 }
944 addToDownload(a, r1, results, max_area);
945 addToDownload(a, r2, results, max_area);
946 } else {
947 results.add(bounds);
948 }
949 }
950
951 /**
952 * Makes a new marker layer derived from this GpxLayer containing at least one audio marker
953 * which the given audio file is associated with. Markers are derived from the following (a)
954 * explict waypoints in the GPX layer, or (b) named trackpoints in the GPX layer, or (d)
955 * timestamp on the wav file (e) (in future) voice recognised markers in the sound recording (f)
956 * a single marker at the beginning of the track
957 * @param wavFile : the file to be associated with the markers in the new marker layer
958 * @param markers : keeps track of warning messages to avoid repeated warnings
959 */
960 private void importAudio(File wavFile, MarkerLayer ml, double firstStartTime, Markers markers) {
961 String uri = "file:".concat(wavFile.getAbsolutePath());
962 Collection<WayPoint> waypoints = new ArrayList<WayPoint>();
963 boolean timedMarkersOmitted = false;
964 boolean untimedMarkersOmitted = false;
965 double snapDistance = Main.pref.getDouble("marker.audiofromuntimedwaypoints.distance", 1.0e-3); /*
966 * about
967 * 25
968 * m
969 */
970 WayPoint wayPointFromTimeStamp = null;
971
972 // determine time of first point in track
973 double firstTime = -1.0;
974 if (data.tracks != null && !data.tracks.isEmpty()) {
975 for (GpxTrack track : data.tracks) {
976 if (track.trackSegs == null) {
977 continue;
978 }
979 for (Collection<WayPoint> seg : track.trackSegs) {
980 for (WayPoint w : seg) {
981 firstTime = w.time;
982 break;
983 }
984 if (firstTime >= 0.0) {
985 break;
986 }
987 }
988 if (firstTime >= 0.0) {
989 break;
990 }
991 }
992 }
993 if (firstTime < 0.0) {
994 JOptionPane.showMessageDialog(
995 Main.parent,
996 tr("No GPX track available in layer to associate audio with."),
997 tr("Error"),
998 JOptionPane.ERROR_MESSAGE
999 );
1000 return;
1001 }
1002
1003 // (a) try explicit timestamped waypoints - unless suppressed
1004 if (Main.pref.getBoolean("marker.audiofromexplicitwaypoints", true) && data.waypoints != null
1005 && !data.waypoints.isEmpty()) {
1006 for (WayPoint w : data.waypoints) {
1007 if (w.time > firstTime) {
1008 waypoints.add(w);
1009 } else if (w.time > 0.0) {
1010 timedMarkersOmitted = true;
1011 }
1012 }
1013 }
1014
1015 // (b) try explicit waypoints without timestamps - unless suppressed
1016 if (Main.pref.getBoolean("marker.audiofromuntimedwaypoints", true) && data.waypoints != null
1017 && !data.waypoints.isEmpty()) {
1018 for (WayPoint w : data.waypoints) {
1019 if (waypoints.contains(w)) {
1020 continue;
1021 }
1022 WayPoint wNear = nearestPointOnTrack(w.getEastNorth(), snapDistance);
1023 if (wNear != null) {
1024 WayPoint wc = new WayPoint(w.getCoor());
1025 wc.time = wNear.time;
1026 if (w.attr.containsKey("name")) {
1027 wc.attr.put("name", w.getString("name"));
1028 }
1029 waypoints.add(wc);
1030 } else {
1031 untimedMarkersOmitted = true;
1032 }
1033 }
1034 }
1035
1036 // (c) use explicitly named track points, again unless suppressed
1037 if ((Main.pref.getBoolean("marker.audiofromnamedtrackpoints", false)) && data.tracks != null
1038 && !data.tracks.isEmpty()) {
1039 for (GpxTrack track : data.tracks) {
1040 if (track.trackSegs == null) {
1041 continue;
1042 }
1043 for (Collection<WayPoint> seg : track.trackSegs) {
1044 for (WayPoint w : seg) {
1045 if (w.attr.containsKey("name") || w.attr.containsKey("desc")) {
1046 waypoints.add(w);
1047 }
1048 }
1049 }
1050 }
1051 }
1052
1053 // (d) use timestamp of file as location on track
1054 if ((Main.pref.getBoolean("marker.audiofromwavtimestamps", false)) && data.tracks != null
1055 && !data.tracks.isEmpty()) {
1056 double lastModified = wavFile.lastModified() / 1000.0; // lastModified is in
1057 // milliseconds
1058 double duration = AudioUtil.getCalibratedDuration(wavFile);
1059 double startTime = lastModified - duration;
1060 startTime = firstStartTime + (startTime - firstStartTime)
1061 / Main.pref.getDouble("audio.calibration", "1.0" /* default, ratio */);
1062 WayPoint w1 = null;
1063 WayPoint w2 = null;
1064
1065 for (GpxTrack track : data.tracks) {
1066 if (track.trackSegs == null) {
1067 continue;
1068 }
1069 for (Collection<WayPoint> seg : track.trackSegs) {
1070 for (WayPoint w : seg) {
1071 if (startTime < w.time) {
1072 w2 = w;
1073 break;
1074 }
1075 w1 = w;
1076 }
1077 if (w2 != null) {
1078 break;
1079 }
1080 }
1081 }
1082
1083 if (w1 == null || w2 == null) {
1084 timedMarkersOmitted = true;
1085 } else {
1086 wayPointFromTimeStamp = new WayPoint(w1.getCoor().interpolate(w2.getCoor(),
1087 (startTime - w1.time) / (w2.time - w1.time)));
1088 wayPointFromTimeStamp.time = startTime;
1089 String name = wavFile.getName();
1090 int dot = name.lastIndexOf(".");
1091 if (dot > 0) {
1092 name = name.substring(0, dot);
1093 }
1094 wayPointFromTimeStamp.attr.put("name", name);
1095 waypoints.add(wayPointFromTimeStamp);
1096 }
1097 }
1098
1099 // (e) analyse audio for spoken markers here, in due course
1100
1101 // (f) simply add a single marker at the start of the track
1102 if ((Main.pref.getBoolean("marker.audiofromstart") || waypoints.isEmpty()) && data.tracks != null
1103 && !data.tracks.isEmpty()) {
1104 boolean gotOne = false;
1105 for (GpxTrack track : data.tracks) {
1106 if (track.trackSegs == null) {
1107 continue;
1108 }
1109 for (Collection<WayPoint> seg : track.trackSegs) {
1110 for (WayPoint w : seg) {
1111 WayPoint wStart = new WayPoint(w.getCoor());
1112 wStart.attr.put("name", "start");
1113 wStart.time = w.time;
1114 waypoints.add(wStart);
1115 gotOne = true;
1116 break;
1117 }
1118 if (gotOne) {
1119 break;
1120 }
1121 }
1122 if (gotOne) {
1123 break;
1124 }
1125 }
1126 }
1127
1128 /* we must have got at least one waypoint now */
1129
1130 Collections.sort((ArrayList<WayPoint>) waypoints, new Comparator<WayPoint>() {
1131 public int compare(WayPoint a, WayPoint b) {
1132 return a.time <= b.time ? -1 : 1;
1133 }
1134 });
1135
1136 firstTime = -1.0; /* this time of the first waypoint, not first trackpoint */
1137 for (WayPoint w : waypoints) {
1138 if (firstTime < 0.0) {
1139 firstTime = w.time;
1140 }
1141 double offset = w.time - firstTime;
1142 String name;
1143 if (w.attr.containsKey("name")) {
1144 name = w.getString("name");
1145 } else if (w.attr.containsKey("desc")) {
1146 name = w.getString("desc");
1147 } else {
1148 name = AudioMarker.inventName(offset);
1149 }
1150 AudioMarker am = AudioMarker.create(w.getCoor(), name, uri, ml, w.time, offset);
1151 /*
1152 * timeFromAudio intended for future use to shift markers of this type on
1153 * synchronization
1154 */
1155 if (w == wayPointFromTimeStamp) {
1156 am.timeFromAudio = true;
1157 }
1158 ml.data.add(am);
1159 }
1160
1161 if (timedMarkersOmitted && !markers.timedMarkersOmitted) {
1162 JOptionPane
1163 .showMessageDialog(
1164 Main.parent,
1165 tr("Some waypoints with timestamps from before the start of the track or after the end were omitted or moved to the start."));
1166 markers.timedMarkersOmitted = timedMarkersOmitted;
1167 }
1168 if (untimedMarkersOmitted && !markers.untimedMarkersOmitted) {
1169 JOptionPane
1170 .showMessageDialog(
1171 Main.parent,
1172 tr("Some waypoints which were too far from the track to sensibly estimate their time were omitted."));
1173 markers.untimedMarkersOmitted = untimedMarkersOmitted;
1174 }
1175 }
1176
1177 /**
1178 * Makes a WayPoint at the projection of point P onto the track providing P is less than
1179 * tolerance away from the track
1180 *
1181 * @param P : the point to determine the projection for
1182 * @param tolerance : must be no further than this from the track
1183 * @return the closest point on the track to P, which may be the first or last point if off the
1184 * end of a segment, or may be null if nothing close enough
1185 */
1186 public WayPoint nearestPointOnTrack(EastNorth P, double tolerance) {
1187 /*
1188 * assume the coordinates of P are xp,yp, and those of a section of track between two
1189 * trackpoints are R=xr,yr and S=xs,ys. Let N be the projected point.
1190 *
1191 * The equation of RS is Ax + By + C = 0 where A = ys - yr B = xr - xs C = - Axr - Byr
1192 *
1193 * Also, note that the distance RS^2 is A^2 + B^2
1194 *
1195 * If RS^2 == 0.0 ignore the degenerate section of track
1196 *
1197 * PN^2 = (Axp + Byp + C)^2 / RS^2 that is the distance from P to the line
1198 *
1199 * so if PN^2 is less than PNmin^2 (initialized to tolerance) we can reject the line;
1200 * otherwise... determine if the projected poijnt lies within the bounds of the line: PR^2 -
1201 * PN^2 <= RS^2 and PS^2 - PN^2 <= RS^2
1202 *
1203 * where PR^2 = (xp - xr)^2 + (yp-yr)^2 and PS^2 = (xp - xs)^2 + (yp-ys)^2
1204 *
1205 * If so, calculate N as xn = xr + (RN/RS) B yn = y1 + (RN/RS) A
1206 *
1207 * where RN = sqrt(PR^2 - PN^2)
1208 */
1209
1210 double PNminsq = tolerance * tolerance;
1211 EastNorth bestEN = null;
1212 double bestTime = 0.0;
1213 double px = P.east();
1214 double py = P.north();
1215 double rx = 0.0, ry = 0.0, sx, sy, x, y;
1216 if (data.tracks == null)
1217 return null;
1218 for (GpxTrack track : data.tracks) {
1219 if (track.trackSegs == null) {
1220 continue;
1221 }
1222 for (Collection<WayPoint> seg : track.trackSegs) {
1223 WayPoint R = null;
1224 for (WayPoint S : seg) {
1225 EastNorth c = S.getEastNorth();
1226 if (R == null) {
1227 R = S;
1228 rx = c.east();
1229 ry = c.north();
1230 x = px - rx;
1231 y = py - ry;
1232 double PRsq = x * x + y * y;
1233 if (PRsq < PNminsq) {
1234 PNminsq = PRsq;
1235 bestEN = c;
1236 bestTime = R.time;
1237 }
1238 } else {
1239 sx = c.east();
1240 sy = c.north();
1241 double A = sy - ry;
1242 double B = rx - sx;
1243 double C = -A * rx - B * ry;
1244 double RSsq = A * A + B * B;
1245 if (RSsq == 0.0) {
1246 continue;
1247 }
1248 double PNsq = A * px + B * py + C;
1249 PNsq = PNsq * PNsq / RSsq;
1250 if (PNsq < PNminsq) {
1251 x = px - rx;
1252 y = py - ry;
1253 double PRsq = x * x + y * y;
1254 x = px - sx;
1255 y = py - sy;
1256 double PSsq = x * x + y * y;
1257 if (PRsq - PNsq <= RSsq && PSsq - PNsq <= RSsq) {
1258 double RNoverRS = Math.sqrt((PRsq - PNsq) / RSsq);
1259 double nx = rx - RNoverRS * B;
1260 double ny = ry + RNoverRS * A;
1261 bestEN = new EastNorth(nx, ny);
1262 bestTime = R.time + RNoverRS * (S.time - R.time);
1263 PNminsq = PNsq;
1264 }
1265 }
1266 R = S;
1267 rx = sx;
1268 ry = sy;
1269 }
1270 }
1271 if (R != null) {
1272 EastNorth c = R.getEastNorth();
1273 /* if there is only one point in the seg, it will do this twice, but no matter */
1274 rx = c.east();
1275 ry = c.north();
1276 x = px - rx;
1277 y = py - ry;
1278 double PRsq = x * x + y * y;
1279 if (PRsq < PNminsq) {
1280 PNminsq = PRsq;
1281 bestEN = c;
1282 bestTime = R.time;
1283 }
1284 }
1285 }
1286 }
1287 if (bestEN == null)
1288 return null;
1289 WayPoint best = new WayPoint(Main.proj.eastNorth2latlon(bestEN));
1290 best.time = bestTime;
1291 return best;
1292 }
1293}
Note: See TracBrowser for help on using the repository browser.