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

Last change on this file since 4207 was 4207, checked in by bastiK, 13 years ago

applied #6540 (anonymous contribution) - New gps-trace coloring method: direction

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