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

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

close #273 - copy GPX name on convert - patch by xeen

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