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

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

some more changes and bug fixes related to new projection stuff - GPX should now work also

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