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

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

fixed #2607 - patch by Teemu Koskinen - Don't add splitted ways in the end of relation

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