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

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

fixed #4100 - unable to simply load already referenced images
Added 'jpg' to the list of available formats for 'File' > 'Open...'

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