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

Last change on this file since 587 was 587, checked in by framm, 17 years ago
  • new boolean config option draw.rawgps.direction enables arrowheads on lines between GPS points (only if lines are drawn). can currently only be set manually or through expert mode. Fixes #650.
  • new integer config option draw.rawgps.max-line-length can be set to suppress line drawing between GPS points further apart than the configured value (in metres). Beware: Using this option will SLOW THINGS DOWN because distance computation is expensive. can currently only be set manually or through expert mode. Fixes #494.
File size: 26.6 KB
Line 
1//License: GPL. Copyright 2007 by Immanuel Scholz, Raphael Mack and others
2
3package org.openstreetmap.josm.gui.layer;
4
5import static org.openstreetmap.josm.tools.I18n.tr;
6import static org.openstreetmap.josm.tools.I18n.trn;
7
8import java.awt.Color;
9import java.awt.Component;
10import java.awt.Graphics;
11import java.awt.GridBagLayout;
12import java.awt.Point;
13import java.awt.event.ActionEvent;
14import java.awt.event.ActionListener;
15import java.io.BufferedReader;
16import java.io.File;
17import java.io.FileInputStream;
18import java.io.FileOutputStream;
19import java.io.InputStreamReader;
20import java.net.URL;
21import java.net.URLConnection;
22import java.net.UnknownHostException;
23import java.util.ArrayList;
24import java.util.Collection;
25import java.util.Collections;
26import java.util.Comparator;
27import java.util.LinkedList;
28
29import javax.swing.AbstractAction;
30import javax.swing.Box;
31import javax.swing.ButtonGroup;
32import javax.swing.Icon;
33import javax.swing.JCheckBox;
34import javax.swing.JColorChooser;
35import javax.swing.JFileChooser;
36import javax.swing.JLabel;
37import javax.swing.JMenuItem;
38import javax.swing.JOptionPane;
39import javax.swing.JPanel;
40import javax.swing.JRadioButton;
41import javax.swing.JSeparator;
42import javax.swing.JTextField;
43import javax.swing.filechooser.FileFilter;
44
45import org.openstreetmap.josm.Main;
46import org.openstreetmap.josm.actions.RenameLayerAction;
47import org.openstreetmap.josm.actions.SaveAction;
48import org.openstreetmap.josm.actions.SaveAsAction;
49import org.openstreetmap.josm.data.coor.EastNorth;
50import org.openstreetmap.josm.data.gpx.GpxData;
51import org.openstreetmap.josm.data.gpx.GpxRoute;
52import org.openstreetmap.josm.data.gpx.GpxTrack;
53import org.openstreetmap.josm.data.gpx.WayPoint;
54import org.openstreetmap.josm.data.osm.DataSet;
55import org.openstreetmap.josm.data.osm.Node;
56import org.openstreetmap.josm.data.osm.Way;
57import org.openstreetmap.josm.data.osm.visitor.BoundingXYVisitor;
58import org.openstreetmap.josm.gui.MapView;
59import org.openstreetmap.josm.gui.dialogs.LayerListDialog;
60import org.openstreetmap.josm.gui.dialogs.LayerListPopup;
61import org.openstreetmap.josm.gui.layer.markerlayer.AudioMarker;
62import org.openstreetmap.josm.gui.layer.markerlayer.MarkerLayer;
63import org.openstreetmap.josm.io.GpxWriter;
64import org.openstreetmap.josm.io.MultiPartFormOutputStream;
65import org.openstreetmap.josm.tools.ColorHelper;
66import org.openstreetmap.josm.tools.DontShowAgainInfo;
67import org.openstreetmap.josm.tools.GBC;
68import org.openstreetmap.josm.tools.ImageProvider;
69import org.openstreetmap.josm.tools.UrlLabel;
70
71public class GpxLayer extends Layer {
72 public GpxData data;
73 private final GpxLayer me;
74 protected static final double PHI = Math.toRadians(15);
75
76 public GpxLayer(GpxData d) {
77 super((String) d.attr.get("name"));
78 data = d;
79 me = this;
80 }
81
82 public GpxLayer(GpxData d, String name) {
83 this(d);
84 this.name = name;
85 }
86
87 @Override public Icon getIcon() {
88 return ImageProvider.get("layer", "gpx_small");
89 }
90
91 @Override public Object getInfoComponent() {
92 return getToolTipText();
93 }
94
95 @Override public Component[] getMenuEntries() {
96 JMenuItem line = new JMenuItem(tr("Customize line drawing"), ImageProvider.get("mapmode/addsegment"));
97 line.addActionListener(new ActionListener() {
98 public void actionPerformed(ActionEvent e) {
99 JRadioButton[] r = new JRadioButton[3];
100 r[0] = new JRadioButton(tr("Use global settings."));
101 r[1] = new JRadioButton(tr("Draw lines between points for this layer."));
102 r[2] = new JRadioButton(tr("Do not draw lines between points for this layer."));
103 ButtonGroup group = new ButtonGroup();
104 Box panel = Box.createVerticalBox();
105 for (JRadioButton b : r) {
106 group.add(b);
107 panel.add(b);
108 }
109 String propName = "draw.rawgps.lines.layer "+name;
110 if (Main.pref.hasKey(propName))
111 group.setSelected(r[Main.pref.getBoolean(propName) ? 1:2].getModel(), true);
112 else
113 group.setSelected(r[0].getModel(), true);
114 int answer = JOptionPane.showConfirmDialog(Main.parent, panel, tr("Select line drawing options"), JOptionPane.OK_CANCEL_OPTION);
115 if (answer == JOptionPane.CANCEL_OPTION)
116 return;
117 if (group.getSelection() == r[0].getModel())
118 Main.pref.put(propName, null);
119 else
120 Main.pref.put(propName, group.getSelection() == r[1].getModel());
121 Main.map.repaint();
122 }
123 });
124
125 JMenuItem color = new JMenuItem(tr("Customize Color"), ImageProvider.get("colorchooser"));
126 color.putClientProperty("help", "Action/LayerCustomizeColor");
127 color.addActionListener(new ActionListener() {
128 public void actionPerformed(ActionEvent e) {
129 String col = Main.pref.get("color.layer "+name, Main.pref.get("color.gps point", ColorHelper.color2html(Color.gray)));
130 JColorChooser c = new JColorChooser(ColorHelper.html2color(col));
131 Object[] options = new Object[]{tr("OK"), tr("Cancel"), tr("Default")};
132 int answer = JOptionPane.showOptionDialog(Main.parent, c, tr("Choose a color"), JOptionPane.OK_CANCEL_OPTION, JOptionPane.PLAIN_MESSAGE, null, options, options[0]);
133 switch (answer) {
134 case 0:
135 Main.pref.put("color.layer "+name, ColorHelper.color2html(c.getColor()));
136 break;
137 case 1:
138 return;
139 case 2:
140 Main.pref.put("color.layer "+name, null);
141 break;
142 }
143 Main.map.repaint();
144 }
145 });
146
147 JMenuItem markersFromNamedTrackpoints = new JMenuItem(tr("Markers From Named Points"), ImageProvider.get("addmarkers"));
148 markersFromNamedTrackpoints.putClientProperty("help", "Action/MarkersFromNamedPoints");
149 markersFromNamedTrackpoints.addActionListener(new ActionListener() {
150 public void actionPerformed(ActionEvent e) {
151 GpxData namedTrackPoints = new GpxData();
152 for (GpxTrack track : data.tracks)
153 for (Collection<WayPoint> seg : track.trackSegs)
154 for (WayPoint point : seg)
155 if (point.attr.containsKey("name") || point.attr.containsKey("desc"))
156 namedTrackPoints.waypoints.add(point);
157
158 MarkerLayer ml = new MarkerLayer(namedTrackPoints, tr("Named Trackpoints from {0}", name), associatedFile, me);
159 if (ml.data.size() > 0) {
160 Main.main.addLayer(ml);
161 }
162 }
163 });
164
165 JMenuItem importAudio = new JMenuItem(tr("Import Audio"), ImageProvider.get("importaudio"));
166 importAudio.putClientProperty("help", "ImportAudio");
167 importAudio.addActionListener(new ActionListener() {
168 public void actionPerformed(ActionEvent e) {
169 String dir = Main.pref.get("markers.lastaudiodirectory");
170 JFileChooser fc = new JFileChooser(dir);
171 fc.setFileSelectionMode(JFileChooser.FILES_ONLY);
172 fc.setAcceptAllFileFilterUsed(false);
173 fc.setFileFilter(new FileFilter(){
174 @Override public boolean accept(File f) {
175 return f.isDirectory() || f.getName().toLowerCase().endsWith(".wav");
176 }
177 @Override public String getDescription() {
178 return tr("Wave Audio files (*.wav)");
179 }
180 });
181 fc.showOpenDialog(Main.parent);
182 File sel = fc.getSelectedFile();
183 if (!fc.getCurrentDirectory().getAbsolutePath().equals(dir))
184 Main.pref.put("markers.lastaudiodirectory", fc.getCurrentDirectory().getAbsolutePath());
185 if (sel == null)
186 return;
187 importAudio(sel);
188 Main.map.repaint();
189 }
190 });
191
192 JMenuItem tagimage = new JMenuItem(tr("Import images"), ImageProvider.get("tagimages"));
193 tagimage.putClientProperty("help", "Action/ImportImages");
194 tagimage.addActionListener(new ActionListener() {
195 public void actionPerformed(ActionEvent e) {
196 JFileChooser fc = new JFileChooser(Main.pref.get("tagimages.lastdirectory"));
197 fc.setFileSelectionMode(JFileChooser.FILES_AND_DIRECTORIES);
198 fc.setMultiSelectionEnabled(true);
199 fc.setAcceptAllFileFilterUsed(false);
200 fc.setFileFilter(new FileFilter() {
201 @Override public boolean accept(File f) {
202 return f.isDirectory() || f.getName().toLowerCase().endsWith(".jpg");
203 }
204 @Override public String getDescription() {
205 return tr("JPEG images (*.jpg)");
206 }
207 });
208 fc.showOpenDialog(Main.parent);
209 File[] sel = fc.getSelectedFiles();
210 if (sel == null || sel.length == 0)
211 return;
212 LinkedList<File> files = new LinkedList<File>();
213 addRecursiveFiles(files, sel);
214 Main.pref.put("tagimages.lastdirectory", fc.getCurrentDirectory().getPath());
215 GeoImageLayer.create(files, GpxLayer.this);
216 }
217
218 private void addRecursiveFiles(LinkedList<File> files, File[] sel) {
219 for (File f : sel) {
220 if (f.isDirectory())
221 addRecursiveFiles(files, f.listFiles());
222 else if (f.getName().toLowerCase().endsWith(".jpg"))
223 files.add(f);
224 }
225 }
226 });
227
228 if (Main.applet)
229 return new Component[] {
230 new JMenuItem(new LayerListDialog.ShowHideLayerAction(this)),
231 new JMenuItem(new LayerListDialog.DeleteLayerAction(this)),
232 new JSeparator(),
233 color,
234 line,
235 new JMenuItem(new ConvertToDataLayerAction()),
236 new JSeparator(),
237 new JMenuItem(new RenameLayerAction(associatedFile, this)),
238 new JSeparator(),
239 new JMenuItem(new LayerListPopup.InfoAction(this))};
240 return new Component[] {
241 new JMenuItem(new LayerListDialog.ShowHideLayerAction(this)),
242 new JMenuItem(new LayerListDialog.DeleteLayerAction(this)),
243 new JSeparator(),
244 new JMenuItem(new SaveAction(this)),
245 new JMenuItem(new SaveAsAction(this)),
246 // new JMenuItem(new UploadTraceAction()),
247 color,
248 line,
249 tagimage,
250 importAudio,
251 markersFromNamedTrackpoints,
252 new JMenuItem(new ConvertToDataLayerAction()),
253 new JSeparator(),
254 new JMenuItem(new RenameLayerAction(associatedFile, this)),
255 new JSeparator(),
256 new JMenuItem(new LayerListPopup.InfoAction(this))};
257 }
258
259 @Override public String getToolTipText() {
260 StringBuilder info = new StringBuilder().append("<html>");
261
262 info.append(trn("{0} track, ", "{0} tracks, ",
263 data.tracks.size(), data.tracks.size()))
264 .append(trn("{0} route, ", "{0} routes, ",
265 data.routes.size(), data.routes.size()))
266 .append(trn("{0} waypoint", "{0} waypoints",
267 data.waypoints.size(), data.waypoints.size()))
268 .append("<br />");
269
270 if (data.attr.containsKey("name"))
271 info.append(tr("Name: {0}", data.attr.get("name")))
272 .append("<br />");
273
274 if (data.attr.containsKey("desc"))
275 info.append(tr("Description: {0}", data.attr.get("desc")))
276 .append("<br />");
277
278 return info.append("</html>").toString();
279 }
280
281 @Override public boolean isMergable(Layer other) {
282 return other instanceof GpxLayer;
283 }
284
285 @Override public void mergeFrom(Layer from) {
286 data.mergeFrom(((GpxLayer)from).data);
287 }
288
289 @Override public void paint(Graphics g, MapView mv) {
290 String gpsCol = Main.pref.get("color.gps point");
291 String gpsColSpecial = Main.pref.get("color.layer "+name);
292 if (!gpsColSpecial.equals("")) {
293 g.setColor(ColorHelper.html2color(gpsColSpecial));
294 } else if (!gpsCol.equals("")) {
295 g.setColor(ColorHelper.html2color(gpsCol));
296 } else{
297 g.setColor(Color.GRAY);
298 }
299
300 boolean forceLines = Main.pref.getBoolean("draw.rawgps.lines.force");
301 boolean direction = Main.pref.getBoolean("draw.rawgps.direction");
302 int maxLineLength = Integer.parseInt(Main.pref.get("draw.rawgps.max-line-length", "-1"));
303 boolean lines = Main.pref.getBoolean("draw.rawgps.lines");
304 String linesKey = "draw.rawgps.lines.layer "+name;
305 if (Main.pref.hasKey(linesKey))
306 lines = Main.pref.getBoolean(linesKey);
307 boolean large = Main.pref.getBoolean("draw.rawgps.large");
308
309 Point old = null;
310 WayPoint oldWp = null;
311 for (GpxTrack trk : data.tracks) {
312 if (!forceLines) {
313 old = null;
314 }
315 for (Collection<WayPoint> segment : trk.trackSegs) {
316 for (WayPoint trkPnt : segment) {
317 if (Double.isNaN(trkPnt.latlon.lat()) || Double.isNaN(trkPnt.latlon.lon()))
318 continue;
319 Point screen = mv.getPoint(trkPnt.eastNorth);
320 if (lines && old != null) {
321
322 // break out if a maxLineLength is set and the line is longer.
323 if (maxLineLength > -1)
324 if (trkPnt.latlon.distance(oldWp.latlon) > maxLineLength) continue;
325 g.drawLine(old.x, old.y, screen.x, screen.y);
326
327 if (direction) {
328 double t = Math.atan2(screen.y-old.y, screen.x-old.x) + Math.PI;
329 g.drawLine(screen.x,screen.y, (int)(screen.x + 10*Math.cos(t-PHI)), (int)(screen.y + 10*Math.sin(t-PHI)));
330 g.drawLine(screen.x,screen.y, (int)(screen.x + 10*Math.cos(t+PHI)), (int)(screen.y + 10*Math.sin(t+PHI)));
331 }
332
333 } else if (!large) {
334 g.drawRect(screen.x, screen.y, 0, 0);
335 }
336 if (large)
337 g.fillRect(screen.x-1, screen.y-1, 3, 3);
338 old = screen;
339 oldWp = trkPnt;
340 }
341 }
342 }
343 }
344
345 @Override public void visitBoundingBox(BoundingXYVisitor v) {
346 for (WayPoint p : data.waypoints)
347 v.visit(p.eastNorth);
348
349 for (GpxRoute rte : data.routes) {
350 Collection<WayPoint> r = rte.routePoints;
351 for (WayPoint p : r) {
352 v.visit(p.eastNorth);
353 }
354 }
355
356 for (GpxTrack trk : data.tracks) {
357 for (Collection<WayPoint> seg : trk.trackSegs) {
358 for (WayPoint p : seg) {
359 v.visit(p.eastNorth);
360 }
361 }
362 }
363 }
364
365 public class UploadTraceAction extends AbstractAction {
366 public UploadTraceAction() {
367 super(tr("Upload this trace..."), ImageProvider.get("uploadtrace"));
368 }
369 public void actionPerformed(ActionEvent e) {
370 JPanel msg = new JPanel(new GridBagLayout());
371 msg.add(new JLabel(tr("<html>This functionality has been added only recently. Please<br>"+
372 "use with care and check if it works as expected.</html>")), GBC.eop());
373 ButtonGroup bg = new ButtonGroup();
374 JRadioButton c1 = null;
375 JRadioButton c2 = null;
376
377 //TODO
378 //check whether data comes from server
379 //check whether data changed sind last save/open
380
381 c1 = new JRadioButton(tr("Upload track filtered by JOSM"), true);
382 c2 = new JRadioButton(tr("Upload raw file: "), false);
383 c2.setEnabled(false);
384 c1.setEnabled(false);
385 bg.add(c1);
386 bg.add(c2);
387
388 msg.add(c1, GBC.eol());
389 msg.add(c2, GBC.eop());
390
391
392 JLabel description = new JLabel((String) data.attr.get("desc"));
393 JTextField tags = new JTextField();
394 tags.setText((String) data.attr.get("keywords"));
395 msg.add(new JLabel(tr("Description:")), GBC.std());
396 msg.add(description, GBC.eol().fill(GBC.HORIZONTAL));
397 msg.add(new JLabel(tr("Tags (keywords in GPX):")), GBC.std());
398 msg.add(tags, GBC.eol().fill(GBC.HORIZONTAL));
399 JCheckBox c3 = new JCheckBox("public");
400 msg.add(c3, GBC.eop());
401 msg.add(new JLabel("Please ensure that you don't upload your traces twice."), GBC.eop());
402
403 int answer = JOptionPane.showConfirmDialog(Main.parent, msg, tr("GPX-Upload"), JOptionPane.OK_CANCEL_OPTION);
404 if (answer == JOptionPane.OK_OPTION)
405 {
406 try {
407 String version = Main.pref.get("osm-server.version", "0.5");
408 URL url = new URL(Main.pref.get("osm-server.url") +
409 "/" + version + "/gpx/create");
410
411 // create a boundary string
412 String boundary = MultiPartFormOutputStream.createBoundary();
413 URLConnection urlConn = MultiPartFormOutputStream.createConnection(url);
414 urlConn.setRequestProperty("Accept", "*/*");
415 urlConn.setRequestProperty("Content-Type",
416 MultiPartFormOutputStream.getContentType(boundary));
417 // set some other request headers...
418 urlConn.setRequestProperty("Connection", "Keep-Alive");
419 urlConn.setRequestProperty("Cache-Control", "no-cache");
420 // no need to connect cuz getOutputStream() does it
421 MultiPartFormOutputStream out =
422 new MultiPartFormOutputStream(urlConn.getOutputStream(), boundary);
423 out.writeField("description", description.getText());
424 out.writeField("tags", tags.getText());
425 out.writeField("public", (c3.getSelectedObjects() != null) ? "1" : "0");
426 // upload a file
427 // out.writeFile("gpx_file", "text/xml", associatedFile);
428 // can also write bytes directly
429 // out.writeFile("myFile", "text/plain", "C:\\test.txt",
430 // "This is some file text.".getBytes("ASCII"));
431 File tmp = File.createTempFile("josm", "tmp.gpx");
432 FileOutputStream outs = new FileOutputStream(tmp);
433 new GpxWriter(outs).write(data);
434 outs.close();
435 FileInputStream ins = new FileInputStream(tmp);
436 new GpxWriter(System.out).write(data);
437 out.writeFile("gpx_file", "text/xml", data.storageFile.getName(), ins);
438 out.close();
439 tmp.delete();
440 // read response from server
441 BufferedReader in = new BufferedReader(
442 new InputStreamReader(urlConn.getInputStream()));
443 String line = "";
444 while((line = in.readLine()) != null) {
445 System.out.println(line);
446 }
447 in.close();
448
449 //TODO check response
450 /* int retCode = urlConn.getResponseCode();
451 System.out.println("got return: " + retCode);
452 String retMsg = urlConn.getResponseMessage();
453 urlConn.disconnect();
454 if (retCode != 200) {
455 // Look for a detailed error message from the server
456 if (urlConn.getHeaderField("Error") != null)
457 retMsg += "\n" + urlConn.getHeaderField("Error");
458
459 // Report our error
460 ByteArrayOutputStream o = new ByteArrayOutputStream();
461 System.out.println(new String(o.toByteArray(), "UTF-8").toString());
462 throw new RuntimeException(retCode+" "+retMsg);
463 }
464 */
465 } catch (UnknownHostException ex) {
466 throw new RuntimeException(tr("Unknown host")+": "+ex.getMessage(), ex);
467 } catch (Exception ex) {
468 //if (cancel)
469 // return; // assume cancel
470 if (ex instanceof RuntimeException)
471 throw (RuntimeException)ex;
472 throw new RuntimeException(ex.getMessage(), ex);
473 }
474 }
475 }
476 }
477
478 public class ConvertToDataLayerAction extends AbstractAction {
479 public ConvertToDataLayerAction() {
480 super(tr("Convert to data layer"), ImageProvider.get("converttoosm"));
481 }
482 public void actionPerformed(ActionEvent e) {
483 JPanel msg = new JPanel(new GridBagLayout());
484 msg.add(new JLabel(tr("<html>Upload of unprocessed GPS data as map data is considered harmful.<br>If you want to upload traces, look here:")), GBC.eol());
485 msg.add(new UrlLabel(tr("http://www.openstreetmap.org/traces")), GBC.eop());
486 if (!DontShowAgainInfo.show("convert_to_data", msg))
487 return;
488 DataSet ds = new DataSet();
489 for (GpxTrack trk : data.tracks) {
490 for (Collection<WayPoint> segment : trk.trackSegs) {
491 Way w = new Way();
492 for (WayPoint p : segment) {
493 Node n = new Node(p.latlon);
494 ds.nodes.add(n);
495 w.nodes.add(n);
496 }
497 ds.ways.add(w);
498 }
499 }
500 Main.main.addLayer(new OsmDataLayer(ds, tr("Converted from: {0}", GpxLayer.this.name), null));
501 Main.main.removeLayer(GpxLayer.this);
502 }
503 }
504
505 /**
506 * Makes a new marker layer derived from this GpxLayer containing at least one
507 * audio marker which the given audio file is associated with.
508 * Markers are derived from the following
509 * (a) explict waypoints in the GPX layer, or
510 * (b) named trackpoints in the GPX layer, or
511 * (c) (in future) voice recognised markers in the sound recording
512 * (d) a single marker at the beginning of the track
513 * @param wavFile : the file to be associated with the markers in the new marker layer
514 */
515 private void importAudio(File wavFile) {
516 String uri = "file:".concat(wavFile.getAbsolutePath());
517 MarkerLayer ml = new MarkerLayer(new GpxData(), tr("Audio markers from {0}", name), associatedFile, me);
518
519 Collection<WayPoint> waypoints = new ArrayList<WayPoint>();
520 boolean timedMarkersOmitted = false;
521 boolean untimedMarkersOmitted = false;
522
523 // determine time of first point in track
524 double firstTime = -1.0;
525 if (data.tracks != null && ! data.tracks.isEmpty()) {
526 for (GpxTrack track : data.tracks) {
527 if (track.trackSegs == null) continue;
528 for (Collection<WayPoint> seg : track.trackSegs) {
529 for (WayPoint w : seg) {
530 firstTime = w.time;
531 break;
532 }
533 if (firstTime >= 0.0) break;
534 }
535 if (firstTime >= 0.0) break;
536 }
537 }
538 if (firstTime < 0.0) {
539 JOptionPane.showMessageDialog(Main.parent, tr("No GPX track available in layer to associate audio with."));
540 return;
541 }
542
543 // (a) try explicit timestamped waypoints - unless suppressed
544 if (Main.pref.getBoolean("marker.audiofromexplicitwaypoints", true) &&
545 data.waypoints != null && ! data.waypoints.isEmpty())
546 {
547 for (WayPoint w : data.waypoints) {
548 if (w.time > firstTime) {
549 waypoints.add(w);
550 } else if (w.time > 0.0) {
551 timedMarkersOmitted = true;
552 }
553 }
554 }
555
556 // (b) try explicit waypoints without timestamps - unless suppressed
557 if (Main.pref.getBoolean("marker.audiofromuntimedwaypoints", true) &&
558 data.waypoints != null && ! data.waypoints.isEmpty())
559 {
560 for (WayPoint w : data.waypoints) {
561 if (waypoints.contains(w)) { continue; }
562 WayPoint wNear = nearestPointOnTrack(w.eastNorth, 10.0e-7 /* about 25m */);
563 if (wNear != null) {
564 WayPoint wc = new WayPoint(w.latlon);
565 wc.time = wNear.time;
566 if (w.attr.containsKey("name")) wc.attr.put("name", w.getString("name"));
567 waypoints.add(wc);
568 } else {
569 untimedMarkersOmitted = true;
570 }
571 }
572 }
573
574 // (c) use explicitly named track points, again unless suppressed
575 if ((Main.pref.getBoolean("marker.audiofromnamedtrackpoints", Main.pref.getBoolean("marker.namedtrackpoints") /* old name */)) &&
576 data.tracks != null && ! data.tracks.isEmpty())
577 {
578 for (GpxTrack track : data.tracks) {
579 if (track.trackSegs == null) continue;
580 for (Collection<WayPoint> seg : track.trackSegs) {
581 for (WayPoint w : seg) {
582 if (w.attr.containsKey("name") || w.attr.containsKey("desc")) {
583 waypoints.add(w);
584 }
585 }
586 }
587 }
588 }
589
590 // (d) analyse audio for spoken markers here, in due course
591
592 // (e) simply add a single marker at the start of the track
593 if ((Main.pref.getBoolean("marker.audiofromstart") || waypoints.isEmpty()) &&
594 data.tracks != null && ! data.tracks.isEmpty())
595 {
596 boolean gotOne = false;
597 for (GpxTrack track : data.tracks) {
598 if (track.trackSegs == null) continue;
599 for (Collection<WayPoint> seg : track.trackSegs) {
600 for (WayPoint w : seg) {
601 WayPoint wStart = new WayPoint(w.latlon);
602 wStart.attr.put("name", "start");
603 wStart.time = w.time;
604 waypoints.add(wStart);
605 gotOne = true;
606 break;
607 }
608 if (gotOne) break;
609 }
610 if (gotOne) break;
611 }
612 }
613
614 /* we must have got at least one waypoint now */
615
616 Collections.sort((ArrayList<WayPoint>) waypoints, new Comparator<WayPoint>() {
617 public int compare(WayPoint a, WayPoint b) {
618 return a.time <= b.time ? -1 : 1;
619 }
620 });
621
622 firstTime = -1.0; /* this time of the first waypoint, not first trackpoint */
623 for (WayPoint w : waypoints) {
624 if (firstTime < 0.0) firstTime = w.time;
625 double offset = w.time - firstTime;
626 String name;
627 if (w.attr.containsKey("name"))
628 name = w.getString("name");
629 else if (w.attr.containsKey("desc"))
630 name = w.getString("desc");
631 else
632 name = AudioMarker.inventName(offset);
633 AudioMarker am = AudioMarker.create(w.latlon,
634 name, uri, ml, w.time, offset);
635 ml.data.add(am);
636 }
637 Main.main.addLayer(ml);
638
639 if (timedMarkersOmitted) {
640 JOptionPane.showMessageDialog(Main.parent, tr("Some waypoints with timestamps from before the start of the track were omitted."));
641 }
642 if (untimedMarkersOmitted) {
643 JOptionPane.showMessageDialog(Main.parent, tr("Some waypoints which were too far from the track to sensibly estimate their time were omitted."));
644 }
645 }
646
647 /**
648 * Makes a WayPoint at the projection of point P onto the track providing P is
649 * less than tolerance away from the track
650
651 * @param P : the point to determine the projection for
652 * @param tolerance : must be no further than this from the track
653 * @return the closest point on the track to P, which may be the
654 * first or last point if off the end of a segment, or may be null if
655 * nothing close enough
656 */
657 public WayPoint nearestPointOnTrack(EastNorth P, double tolerance) {
658 /*
659 * assume the coordinates of P are xp,yp, and those of a section of track
660 * between two trackpoints are R=xr,yr and S=xs,ys. Let N be the projected point.
661 *
662 * The equation of RS is Ax + By + C = 0 where
663 * A = ys - yr
664 * B = xr - xs
665 * C = - Axr - Byr
666 *
667 * Also, note that the distance RS^2 is A^2 + B^2
668 *
669 * If RS^2 == 0.0 ignore the degenerate section of track
670 *
671 * PN^2 = (Axp + Byp + C)^2 / RS^2
672 * that is the distance from P to the line
673 *
674 * so if PN^2 is less than PNmin^2 (initialized to tolerance) we can reject
675 * the line; otherwise...
676 * determine if the projected poijnt lies within the bounds of the line:
677 * PR^2 - PN^2 <= RS^2 and PS^2 - PN^2 <= RS^2
678 *
679 * where PR^2 = (xp - xr)^2 + (yp-yr)^2
680 * and PS^2 = (xp - xs)^2 + (yp-ys)^2
681 *
682 * If so, calculate N as
683 * xn = xr + (RN/RS) B
684 * yn = y1 + (RN/RS) A
685 *
686 * where RN = sqrt(PR^2 - PN^2)
687 */
688
689 double PNminsq = tolerance * tolerance;
690 EastNorth bestEN = null;
691 double bestTime = 0.0;
692 double px = P.east();
693 double py = P.north();
694 double rx = 0.0, ry = 0.0, sx, sy, x, y;
695 if (data.tracks == null) return null;
696 for (GpxTrack track : data.tracks) {
697 if (track.trackSegs == null) continue;
698 for (Collection<WayPoint> seg : track.trackSegs) {
699 WayPoint R = null;
700 for (WayPoint S : seg) {
701 if (R == null) {
702 R = S;
703 rx = R.eastNorth.east();
704 ry = R.eastNorth.north();
705 x = px - rx;
706 y = py - ry;
707 double PRsq = x * x + y * y;
708 if (PRsq < PNminsq) {
709 PNminsq = PRsq;
710 bestEN = R.eastNorth;
711 bestTime = R.time;
712 }
713 } else {
714 sx = S.eastNorth.east();
715 sy = S.eastNorth.north();
716 double A = sy - ry;
717 double B = rx - sx;
718 double C = - A * rx - B * ry;
719 double RSsq = A * A + B * B;
720 if (RSsq == 0.0) continue;
721 double PNsq = A * px + B * py + C;
722 PNsq = PNsq * PNsq / RSsq;
723 if (PNsq < PNminsq) {
724 x = px - rx;
725 y = py - ry;
726 double PRsq = x * x + y * y;
727 x = px - sx;
728 y = py - sy;
729 double PSsq = x * x + y * y;
730 if (PRsq - PNsq <= RSsq && PSsq - PNsq <= RSsq) {
731 double RNoverRS = Math.sqrt((PRsq - PNsq)/RSsq);
732 double nx = rx - RNoverRS * B;
733 double ny = ry + RNoverRS * A;
734 bestEN = new EastNorth(nx, ny);
735 bestTime = R.time + RNoverRS * (S.time - R.time);
736 PNminsq = PNsq;
737 }
738 }
739 R = S;
740 rx = sx;
741 ry = sy;
742 }
743 }
744 if (R != null) {
745 /* if there is only one point in the seg, it will do this twice, but no matter */
746 rx = R.eastNorth.east();
747 ry = R.eastNorth.north();
748 x = px - rx;
749 y = py - ry;
750 double PRsq = x * x + y * y;
751 if (PRsq < PNminsq) {
752 PNminsq = PRsq;
753 bestEN = R.eastNorth;
754 bestTime = R.time;
755 }
756
757 }
758 }
759 }
760 if (bestEN == null) return null;
761 WayPoint best = new WayPoint(Main.proj.eastNorth2latlon(bestEN));
762 best.time = bestTime;
763 return best;
764 }
765}
Note: See TracBrowser for help on using the repository browser.