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

Last change on this file since 3128 was 3128, checked in by jttt, 14 years ago

Different fix for #4728 - discard offscreen buffer for all layer in MapView

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