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

Last change on this file since 2025 was 2025, checked in by Gubaer, 15 years ago

new: improved dialog for uploading/saving modified layers on exit
new: improved dialog for uploading/saving modified layers if layers are deleted
new: new progress monitor which can delegate rendering to any Swing component
more setters/getters for properties in OSM data classes (fields are @deprecated); started to update references in the code base

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