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

Last change on this file since 4129 was 4129, checked in by stoecker, 13 years ago

speedup GPX drawing due to better clipping

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