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

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

close #2302 - patch by jttt - optimizations and encapsulation

  • 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), null));
672 Main.main.removeLayer(GpxLayer.this);
673 }
674 }
675
676 /**
677 * Action that issues a series of download requests to the API, following the GPX track.
678 *
679 * @author fred
680 */
681 public class DownloadAlongTrackAction extends AbstractAction {
682 public DownloadAlongTrackAction() {
683 super(tr("Download from OSM along this track"), ImageProvider.get("downloadalongtrack"));
684 }
685 public void actionPerformed(ActionEvent e) {
686 JPanel msg = new JPanel(new GridBagLayout());
687 Integer dist[] = {5000, 500, 50};
688 Integer area[] = {20, 10, 5, 1};
689
690 msg.add(new JLabel(tr("Download everything within:")), GBC.eol());
691 String s[] = new String[dist.length];
692 for(int i = 0; i < dist.length; ++i)
693 s[i] = tr("{0} meters", dist[i]);
694 JList buffer = new JList(s);
695 msg.add(buffer, GBC.eol());
696 msg.add(new JLabel(tr("Maximum area per request:")), GBC.eol());
697 s = new String[area.length];
698 for(int i = 0; i < area.length; ++i)
699 s[i] = tr("{0} sq km", area[i]);
700 JList maxRect = new JList(s);
701 msg.add(maxRect, GBC.eol());
702
703 if (JOptionPane.showConfirmDialog(Main.parent, msg,
704 tr("Download from OSM along this track"),
705 JOptionPane.OK_CANCEL_OPTION) == JOptionPane.CANCEL_OPTION) {
706 return;
707 }
708
709 /*
710 * Find the average latitude for the data we're contemplating, so we can
711 * know how many metres per degree of longitude we have.
712 */
713 double latsum = 0;
714 int latcnt = 0;
715
716 for (GpxTrack trk : data.tracks) {
717 for (Collection<WayPoint> segment : trk.trackSegs) {
718 for (WayPoint p : segment) {
719 latsum += p.latlon.lat();
720 latcnt ++;
721 }
722 }
723 }
724
725 double avglat = latsum / latcnt;
726 double scale = Math.cos(Math.toRadians(avglat));
727
728 /*
729 * Compute buffer zone extents and maximum bounding box size. Note that the
730 * maximum we ever offer is a bbox area of 0.002, while the API theoretically
731 * supports 0.25, but as soon as you touch any built-up area, that kind of
732 * bounding box will download forever and then stop because it has more than
733 * 50k nodes.
734 */
735 Integer i = buffer.getSelectedIndex();
736 int buffer_dist = dist[i < 0 ? 0 : i];
737 double buffer_y = buffer_dist / 100000.0;
738 double buffer_x = buffer_y / scale;
739 i = maxRect.getSelectedIndex();
740 double max_area = area[i < 0 ? 0 : i] / 10000.0 / scale;
741 Area a = new Area();
742 Rectangle2D r = new Rectangle2D.Double();
743
744 /*
745 * Collect the combined area of all gpx points plus buffer zones around them.
746 * We ignore points that lie closer to the previous point than the given buffer
747 * size because otherwise this operation takes ages.
748 */
749 LatLon previous = null;
750 for (GpxTrack trk : data.tracks) {
751 for (Collection<WayPoint> segment : trk.trackSegs) {
752 for (WayPoint p : segment) {
753 if (previous == null || p.latlon.greatCircleDistance(previous) > buffer_dist) {
754 // we add a buffer around the point.
755 r.setRect(p.latlon.lon()-buffer_x, p.latlon.lat()-buffer_y, 2*buffer_x, 2*buffer_y);
756 a.add(new Area(r));
757 previous = p.latlon;
758 }
759 }
760 }
761 }
762
763 /*
764 * Area "a" now contains the hull that we would like to download data for.
765 * however we can only download rectangles, so the following is an attempt at
766 * finding a number of rectangles to download.
767 *
768 * The idea is simply: Start out with the full bounding box. If it is too large,
769 * then split it in half and repeat recursively for each half until you arrive
770 * at something small enough to download. The algorithm is improved
771 * by always using the intersection between the rectangle and the actual desired
772 * area. For example, if you have a track that goes like this:
773 * +----+
774 * | /|
775 * | / |
776 * | / |
777 * |/ |
778 * +----+
779 * then we would first look at downloading the whole rectangle (assume it's too big),
780 * after that we split it in half (upper and lower half), but we do *not* request the
781 * full upper and lower rectangle, only the part of the upper/lower rectangle that
782 * actually has something in it.
783 */
784
785 List<Rectangle2D> toDownload = new ArrayList<Rectangle2D>();
786
787 addToDownload(a, a.getBounds(), toDownload, max_area);
788
789 msg = new JPanel(new GridBagLayout());
790
791 msg.add(new JLabel(tr("<html>This action will require {0} individual<br>download requests. Do you wish<br>to continue?</html>",
792 toDownload.size())), GBC.eol());
793
794 if (JOptionPane.showConfirmDialog(Main.parent, msg,
795 tr("Download from OSM along this track"),
796 JOptionPane.OK_CANCEL_OPTION) == JOptionPane.CANCEL_OPTION) {
797 return;
798 }
799
800 new DownloadOsmTaskList().download(false, toDownload);
801 }
802 }
803
804 private static void addToDownload(Area a, Rectangle2D r, Collection<Rectangle2D> results, double max_area) {
805 Area tmp = new Area(r);
806 // intersect with sought-after area
807 tmp.intersect(a);
808 if (tmp.isEmpty()) return;
809 Rectangle2D bounds = tmp.getBounds2D();
810 if (bounds.getWidth() * bounds.getHeight() > max_area) {
811 // the rectangle gets too large; split it and make recursive call.
812 Rectangle2D r1;
813 Rectangle2D r2;
814 if (bounds.getWidth() > bounds.getHeight()) {
815 // rectangles that are wider than high are split into a left and right half,
816 r1 = new Rectangle2D.Double(bounds.getX(), bounds.getY(), bounds.getWidth()/2, bounds.getHeight());
817 r2 = new Rectangle2D.Double(bounds.getX()+bounds.getWidth()/2, bounds.getY(), bounds.getWidth()/2, bounds.getHeight());
818 } else {
819 // others into a top and bottom half.
820 r1 = new Rectangle2D.Double(bounds.getX(), bounds.getY(), bounds.getWidth(), bounds.getHeight()/2);
821 r2 = new Rectangle2D.Double(bounds.getX(), bounds.getY()+bounds.getHeight()/2, bounds.getWidth(), bounds.getHeight()/2);
822 }
823 addToDownload(a, r1, results, max_area);
824 addToDownload(a, r2, results, max_area);
825 } else {
826 results.add(bounds);
827 }
828 }
829
830 /**
831 * Makes a new marker layer derived from this GpxLayer containing at least one
832 * audio marker which the given audio file is associated with.
833 * Markers are derived from the following
834 * (a) explict waypoints in the GPX layer, or
835 * (b) named trackpoints in the GPX layer, or
836 * (d) timestamp on the wav file
837 * (e) (in future) voice recognised markers in the sound recording
838 * (f) a single marker at the beginning of the track
839 * @param wavFile : the file to be associated with the markers in the new marker layer
840 */
841 private void importAudio(File wavFile, MarkerLayer ml, double firstStartTime) {
842 String uri = "file:".concat(wavFile.getAbsolutePath());
843 Collection<WayPoint> waypoints = new ArrayList<WayPoint>();
844 boolean timedMarkersOmitted = false;
845 boolean untimedMarkersOmitted = false;
846 double snapDistance = Main.pref.getDouble("marker.audiofromuntimedwaypoints.distance", 1.0e-3); /* about 25m */
847 WayPoint wayPointFromTimeStamp = null;
848
849 // determine time of first point in track
850 double firstTime = -1.0;
851 if (data.tracks != null && ! data.tracks.isEmpty()) {
852 for (GpxTrack track : data.tracks) {
853 if (track.trackSegs == null) continue;
854 for (Collection<WayPoint> seg : track.trackSegs) {
855 for (WayPoint w : seg) {
856 firstTime = w.time;
857 break;
858 }
859 if (firstTime >= 0.0) break;
860 }
861 if (firstTime >= 0.0) break;
862 }
863 }
864 if (firstTime < 0.0) {
865 JOptionPane.showMessageDialog(Main.parent, tr("No GPX track available in layer to associate audio with."));
866 return;
867 }
868
869 // (a) try explicit timestamped waypoints - unless suppressed
870 if (Main.pref.getBoolean("marker.audiofromexplicitwaypoints", true) &&
871 data.waypoints != null && ! data.waypoints.isEmpty())
872 {
873 for (WayPoint w : data.waypoints) {
874 if (w.time > firstTime) {
875 waypoints.add(w);
876 } else if (w.time > 0.0) {
877 timedMarkersOmitted = true;
878 }
879 }
880 }
881
882 // (b) try explicit waypoints without timestamps - unless suppressed
883 if (Main.pref.getBoolean("marker.audiofromuntimedwaypoints", true) &&
884 data.waypoints != null && ! data.waypoints.isEmpty())
885 {
886 for (WayPoint w : data.waypoints) {
887 if (waypoints.contains(w)) { continue; }
888 WayPoint wNear = nearestPointOnTrack(w.eastNorth, snapDistance);
889 if (wNear != null) {
890 WayPoint wc = new WayPoint(w.latlon);
891 wc.time = wNear.time;
892 if (w.attr.containsKey("name")) wc.attr.put("name", w.getString("name"));
893 waypoints.add(wc);
894 } else {
895 untimedMarkersOmitted = true;
896 }
897 }
898 }
899
900 // (c) use explicitly named track points, again unless suppressed
901 if ((Main.pref.getBoolean("marker.audiofromnamedtrackpoints", false)) &&
902 data.tracks != null && ! data.tracks.isEmpty())
903 {
904 for (GpxTrack track : data.tracks) {
905 if (track.trackSegs == null) continue;
906 for (Collection<WayPoint> seg : track.trackSegs) {
907 for (WayPoint w : seg) {
908 if (w.attr.containsKey("name") || w.attr.containsKey("desc")) {
909 waypoints.add(w);
910 }
911 }
912 }
913 }
914 }
915
916 // (d) use timestamp of file as location on track
917 if ((Main.pref.getBoolean("marker.audiofromwavtimestamps", false)) &&
918 data.tracks != null && ! data.tracks.isEmpty())
919 {
920 double lastModified = wavFile.lastModified() / 1000.0; // lastModified is in milliseconds
921 double duration = AudioUtil.getCalibratedDuration(wavFile);
922 double startTime = lastModified - duration;
923 startTime = firstStartTime + (startTime - firstStartTime) /
924 Main.pref.getDouble("audio.calibration", "1.0" /* default, ratio */);
925 WayPoint w1 = null;
926 WayPoint w2 = null;
927
928 for (GpxTrack track : data.tracks) {
929 if (track.trackSegs == null) continue;
930 for (Collection<WayPoint> seg : track.trackSegs) {
931 for (WayPoint w : seg) {
932 if (startTime < w.time) {
933 w2 = w;
934 break;
935 }
936 w1 = w;
937 }
938 if (w2 != null) break;
939 }
940 }
941
942 if (w1 == null || w2 == null) {
943 timedMarkersOmitted = true;
944 } else {
945 EastNorth eastNorth = w1.eastNorth.interpolate(
946 w2.eastNorth,
947 (startTime - w1.time)/(w2.time - w1.time));
948 wayPointFromTimeStamp = new WayPoint(Main.proj.eastNorth2latlon(eastNorth));
949 wayPointFromTimeStamp.time = startTime;
950 String name = wavFile.getName();
951 int dot = name.lastIndexOf(".");
952 if (dot > 0) { name = name.substring(0, dot); }
953 wayPointFromTimeStamp.attr.put("name", name);
954 waypoints.add(wayPointFromTimeStamp);
955 }
956 }
957
958 // (e) analyse audio for spoken markers here, in due course
959
960 // (f) simply add a single marker at the start of the track
961 if ((Main.pref.getBoolean("marker.audiofromstart") || waypoints.isEmpty()) &&
962 data.tracks != null && ! data.tracks.isEmpty())
963 {
964 boolean gotOne = false;
965 for (GpxTrack track : data.tracks) {
966 if (track.trackSegs == null) continue;
967 for (Collection<WayPoint> seg : track.trackSegs) {
968 for (WayPoint w : seg) {
969 WayPoint wStart = new WayPoint(w.latlon);
970 wStart.attr.put("name", "start");
971 wStart.time = w.time;
972 waypoints.add(wStart);
973 gotOne = true;
974 break;
975 }
976 if (gotOne) break;
977 }
978 if (gotOne) break;
979 }
980 }
981
982 /* we must have got at least one waypoint now */
983
984 Collections.sort((ArrayList<WayPoint>) waypoints, new Comparator<WayPoint>() {
985 public int compare(WayPoint a, WayPoint b) {
986 return a.time <= b.time ? -1 : 1;
987 }
988 });
989
990 firstTime = -1.0; /* this time of the first waypoint, not first trackpoint */
991 for (WayPoint w : waypoints) {
992 if (firstTime < 0.0) firstTime = w.time;
993 double offset = w.time - firstTime;
994 String name;
995 if (w.attr.containsKey("name"))
996 name = w.getString("name");
997 else if (w.attr.containsKey("desc"))
998 name = w.getString("desc");
999 else
1000 name = AudioMarker.inventName(offset);
1001 AudioMarker am = AudioMarker.create(w.latlon,
1002 name, uri, ml, w.time, offset);
1003 /* timeFromAudio intended for future use to shift markers of this type on synchronization */
1004 if (w == wayPointFromTimeStamp) {
1005 am.timeFromAudio = true;
1006 }
1007 ml.data.add(am);
1008 }
1009
1010 if (timedMarkersOmitted) {
1011 JOptionPane.showMessageDialog(Main.parent,
1012 tr("Some waypoints with timestamps from before the start of the track or after the end were omitted or moved to the start."));
1013 }
1014 if (untimedMarkersOmitted) {
1015 JOptionPane.showMessageDialog(Main.parent,
1016 tr("Some waypoints which were too far from the track to sensibly estimate their time were omitted."));
1017 }
1018 }
1019
1020 /**
1021 * Makes a WayPoint at the projection of point P onto the track providing P is
1022 * less than tolerance away from the track
1023
1024 * @param P : the point to determine the projection for
1025 * @param tolerance : must be no further than this from the track
1026 * @return the closest point on the track to P, which may be the
1027 * first or last point if off the end of a segment, or may be null if
1028 * nothing close enough
1029 */
1030 public WayPoint nearestPointOnTrack(EastNorth P, double tolerance) {
1031 /*
1032 * assume the coordinates of P are xp,yp, and those of a section of track
1033 * between two trackpoints are R=xr,yr and S=xs,ys. Let N be the projected point.
1034 *
1035 * The equation of RS is Ax + By + C = 0 where
1036 * A = ys - yr
1037 * B = xr - xs
1038 * C = - Axr - Byr
1039 *
1040 * Also, note that the distance RS^2 is A^2 + B^2
1041 *
1042 * If RS^2 == 0.0 ignore the degenerate section of track
1043 *
1044 * PN^2 = (Axp + Byp + C)^2 / RS^2
1045 * that is the distance from P to the line
1046 *
1047 * so if PN^2 is less than PNmin^2 (initialized to tolerance) we can reject
1048 * the line; otherwise...
1049 * determine if the projected poijnt lies within the bounds of the line:
1050 * PR^2 - PN^2 <= RS^2 and PS^2 - PN^2 <= RS^2
1051 *
1052 * where PR^2 = (xp - xr)^2 + (yp-yr)^2
1053 * and PS^2 = (xp - xs)^2 + (yp-ys)^2
1054 *
1055 * If so, calculate N as
1056 * xn = xr + (RN/RS) B
1057 * yn = y1 + (RN/RS) A
1058 *
1059 * where RN = sqrt(PR^2 - PN^2)
1060 */
1061
1062 double PNminsq = tolerance * tolerance;
1063 EastNorth bestEN = null;
1064 double bestTime = 0.0;
1065 double px = P.east();
1066 double py = P.north();
1067 double rx = 0.0, ry = 0.0, sx, sy, x, y;
1068 if (data.tracks == null) return null;
1069 for (GpxTrack track : data.tracks) {
1070 if (track.trackSegs == null) continue;
1071 for (Collection<WayPoint> seg : track.trackSegs) {
1072 WayPoint R = null;
1073 for (WayPoint S : seg) {
1074 if (R == null) {
1075 R = S;
1076 rx = R.eastNorth.east();
1077 ry = R.eastNorth.north();
1078 x = px - rx;
1079 y = py - ry;
1080 double PRsq = x * x + y * y;
1081 if (PRsq < PNminsq) {
1082 PNminsq = PRsq;
1083 bestEN = R.eastNorth;
1084 bestTime = R.time;
1085 }
1086 } else {
1087 sx = S.eastNorth.east();
1088 sy = S.eastNorth.north();
1089 double A = sy - ry;
1090 double B = rx - sx;
1091 double C = - A * rx - B * ry;
1092 double RSsq = A * A + B * B;
1093 if (RSsq == 0.0) continue;
1094 double PNsq = A * px + B * py + C;
1095 PNsq = PNsq * PNsq / RSsq;
1096 if (PNsq < PNminsq) {
1097 x = px - rx;
1098 y = py - ry;
1099 double PRsq = x * x + y * y;
1100 x = px - sx;
1101 y = py - sy;
1102 double PSsq = x * x + y * y;
1103 if (PRsq - PNsq <= RSsq && PSsq - PNsq <= RSsq) {
1104 double RNoverRS = Math.sqrt((PRsq - PNsq)/RSsq);
1105 double nx = rx - RNoverRS * B;
1106 double ny = ry + RNoverRS * A;
1107 bestEN = new EastNorth(nx, ny);
1108 bestTime = R.time + RNoverRS * (S.time - R.time);
1109 PNminsq = PNsq;
1110 }
1111 }
1112 R = S;
1113 rx = sx;
1114 ry = sy;
1115 }
1116 }
1117 if (R != null) {
1118 /* if there is only one point in the seg, it will do this twice, but no matter */
1119 rx = R.eastNorth.east();
1120 ry = R.eastNorth.north();
1121 x = px - rx;
1122 y = py - ry;
1123 double PRsq = x * x + y * y;
1124 if (PRsq < PNminsq) {
1125 PNminsq = PRsq;
1126 bestEN = R.eastNorth;
1127 bestTime = R.time;
1128 }
1129 }
1130 }
1131 }
1132 if (bestEN == null) return null;
1133 WayPoint best = new WayPoint(Main.proj.eastNorth2latlon(bestEN));
1134 best.time = bestTime;
1135 return best;
1136 }
1137}
Note: See TracBrowser for help on using the repository browser.