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

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

Remember selected values in download along track dialog

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