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

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