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

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

added link to presets, fix #1675

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