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

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

memory optimizations for Node & WayPoint (Patch by Gubaer, modified)

The field 'proj' in CachedLatLon is a waste of memory. For the 2 classes where this has the greatest impact, the cache for the projected coordinates is replaced by 2 simple double fields (east & north). On projection change, they have to be invalidated explicitly. This is handled by the DataSet & the GpxLayer.

  • 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 List<Collection<WayPoint>> visibleSegments = new ArrayList<Collection<WayPoint>>();
464 for (GpxTrack trk: data.tracks) {
465 for (GpxTrackSegment trkSeg: trk.getSegments()) {
466 if (trkSeg.getBounds() != null && trkSeg.getBounds().intersects(box)) {
467 visibleSegments.add(trkSeg.getWayPoints());
468 }
469 }
470 }
471
472 /****************************************************************
473 ********** STEP 3a - DRAW LINES ********************************
474 ****************************************************************/
475 if (lines) {
476 Point old = null;
477 for (Collection<WayPoint> segment : visibleSegments) {
478 for (WayPoint trkPnt : segment) {
479 LatLon c = trkPnt.getCoor();
480 if (Double.isNaN(c.lat()) || Double.isNaN(c.lon())) {
481 continue;
482 }
483 Point screen = mv.getPoint(trkPnt.getEastNorth());
484 if (trkPnt.drawLine) {
485 // skip points that are on the same screenposition
486 if (old != null && ((old.x != screen.x) || (old.y != screen.y))) {
487 g.setColor(trkPnt.customColoring);
488 g.drawLine(old.x, old.y, screen.x, screen.y);
489 }
490 }
491 old = screen;
492 } // end for trkpnt
493 } // end for segment
494 } // end if lines
495
496 /****************************************************************
497 ********** STEP 3b - DRAW NICE ARROWS **************************
498 ****************************************************************/
499 if (lines && direction && !alternatedirection) {
500 Point old = null;
501 Point oldA = null; // last arrow painted
502 for (Collection<WayPoint> segment : visibleSegments) {
503 for (WayPoint trkPnt : segment) {
504 LatLon c = trkPnt.getCoor();
505 if (Double.isNaN(c.lat()) || Double.isNaN(c.lon())) {
506 continue;
507 }
508 if (trkPnt.drawLine) {
509 Point screen = mv.getPoint(trkPnt.getEastNorth());
510 // skip points that are on the same screenposition
511 if (old != null
512 && (oldA == null || screen.x < oldA.x - delta || screen.x > oldA.x + delta
513 || screen.y < oldA.y - delta || screen.y > oldA.y + delta)) {
514 g.setColor(trkPnt.customColoring);
515 double t = Math.atan2(screen.y - old.y, screen.x - old.x) + Math.PI;
516 g.drawLine(screen.x, screen.y, (int) (screen.x + 10 * Math.cos(t - PHI)),
517 (int) (screen.y + 10 * Math.sin(t - PHI)));
518 g.drawLine(screen.x, screen.y, (int) (screen.x + 10 * Math.cos(t + PHI)),
519 (int) (screen.y + 10 * Math.sin(t + PHI)));
520 oldA = screen;
521 }
522 old = screen;
523 }
524 } // end for trkpnt
525 } // end for segment
526 } // end if lines
527
528 /****************************************************************
529 ********** STEP 3c - DRAW FAST ARROWS **************************
530 ****************************************************************/
531 if (lines && direction && alternatedirection) {
532 Point old = null;
533 Point oldA = null; // last arrow painted
534 for (Collection<WayPoint> segment : visibleSegments) {
535 for (WayPoint trkPnt : segment) {
536 LatLon c = trkPnt.getCoor();
537 if (Double.isNaN(c.lat()) || Double.isNaN(c.lon())) {
538 continue;
539 }
540 if (trkPnt.drawLine) {
541 Point screen = mv.getPoint(trkPnt.getEastNorth());
542 // skip points that are on the same screenposition
543 if (old != null
544 && (oldA == null || screen.x < oldA.x - delta || screen.x > oldA.x + delta
545 || screen.y < oldA.y - delta || screen.y > oldA.y + delta)) {
546 g.setColor(trkPnt.customColoring);
547 g.drawLine(screen.x, screen.y, screen.x + dir[trkPnt.dir][0], screen.y
548 + dir[trkPnt.dir][1]);
549 g.drawLine(screen.x, screen.y, screen.x + dir[trkPnt.dir][2], screen.y
550 + dir[trkPnt.dir][3]);
551 oldA = screen;
552 }
553 old = screen;
554 }
555 } // end for trkpnt
556 } // end for segment
557 } // end if lines
558
559 /****************************************************************
560 ********** STEP 3d - DRAW LARGE POINTS AND HDOP CIRCLE *********
561 ****************************************************************/
562 if (large || hdopcircle) {
563 g.setColor(neutralColor);
564 for (Collection<WayPoint> segment : visibleSegments) {
565 for (WayPoint trkPnt : segment) {
566 LatLon c = trkPnt.getCoor();
567 if (Double.isNaN(c.lat()) || Double.isNaN(c.lon())) {
568 continue;
569 }
570 Point screen = mv.getPoint(trkPnt.getEastNorth());
571 g.setColor(trkPnt.customColoring);
572 if (hdopcircle && trkPnt.attr.get("hdop") != null) {
573 // hdop value
574 float hdop = ((Float)trkPnt.attr.get("hdop")).floatValue();
575 if (hdop < 0) {
576 hdop = 0;
577 }
578 // hdop pixels
579 int hdopp = mv.getPoint(new LatLon(trkPnt.getCoor().lat(), trkPnt.getCoor().lon() + 2*6*hdop*360/40000000)).x - screen.x;
580 g.drawArc(screen.x-hdopp/2, screen.y-hdopp/2, hdopp, hdopp, 0, 360);
581 }
582 if (large) {
583 g.fillRect(screen.x-1, screen.y-1, 3, 3);
584 }
585 } // end for trkpnt
586 } // end for segment
587 } // end if large || hdopcircle
588
589 /****************************************************************
590 ********** STEP 3e - DRAW SMALL POINTS FOR LINES ***************
591 ****************************************************************/
592 if (!large && lines) {
593 g.setColor(neutralColor);
594 for (Collection<WayPoint> segment : visibleSegments) {
595 for (WayPoint trkPnt : segment) {
596 LatLon c = trkPnt.getCoor();
597 if (Double.isNaN(c.lat()) || Double.isNaN(c.lon())) {
598 continue;
599 }
600 if (!trkPnt.drawLine) {
601 Point screen = mv.getPoint(trkPnt.getEastNorth());
602 g.drawRect(screen.x, screen.y, 0, 0);
603 }
604 } // end for trkpnt
605 } // end for segment
606 } // end if large
607
608 /****************************************************************
609 ********** STEP 3f - DRAW SMALL POINTS INSTEAD OF LINES ********
610 ****************************************************************/
611 if (!large && !lines) {
612 g.setColor(neutralColor);
613 for (Collection<WayPoint> segment : visibleSegments) {
614 for (WayPoint trkPnt : segment) {
615 LatLon c = trkPnt.getCoor();
616 if (Double.isNaN(c.lat()) || Double.isNaN(c.lon())) {
617 continue;
618 }
619 Point screen = mv.getPoint(trkPnt.getEastNorth());
620 g.setColor(trkPnt.customColoring);
621 g.drawRect(screen.x, screen.y, 0, 0);
622 } // end for trkpnt
623 } // end for segment
624 } // end if large
625
626 // Long duration = System.currentTimeMillis() - startTime;
627 // System.out.println(duration);
628 } // end paint
629
630 @Override
631 public void visitBoundingBox(BoundingXYVisitor v) {
632 v.visit(data.recalculateBounds());
633 }
634
635 public class ConvertToDataLayerAction extends AbstractAction {
636 public ConvertToDataLayerAction() {
637 super(tr("Convert to data layer"), ImageProvider.get("converttoosm"));
638 }
639
640 public void actionPerformed(ActionEvent e) {
641 JPanel msg = new JPanel(new GridBagLayout());
642 msg
643 .add(
644 new JLabel(
645 tr("<html>Upload of unprocessed GPS data as map data is considered harmful.<br>If you want to upload traces, look here:</html>")),
646 GBC.eol());
647 msg.add(new UrlLabel(tr("http://www.openstreetmap.org/traces")), GBC.eop());
648 if (!ConditionalOptionPaneUtil.showConfirmationDialog("convert_to_data", Main.parent, msg, tr("Warning"),
649 JOptionPane.OK_CANCEL_OPTION, JOptionPane.WARNING_MESSAGE, JOptionPane.OK_OPTION))
650 return;
651 DataSet ds = new DataSet();
652 for (GpxTrack trk : data.tracks) {
653 for (GpxTrackSegment segment : trk.getSegments()) {
654 List<Node> nodes = new ArrayList<Node>();
655 for (WayPoint p : segment.getWayPoints()) {
656 Node n = new Node(p.getCoor());
657 String timestr = p.getString("time");
658 if (timestr != null) {
659 n.setTimestamp(DateUtils.fromString(timestr));
660 }
661 ds.addPrimitive(n);
662 nodes.add(n);
663 }
664 Way w = new Way();
665 w.setNodes(nodes);
666 ds.addPrimitive(w);
667 }
668 }
669 Main.main
670 .addLayer(new OsmDataLayer(ds, tr("Converted from: {0}", GpxLayer.this.getName()), getAssociatedFile()));
671 Main.main.removeLayer(GpxLayer.this);
672 }
673 }
674
675 @Override
676 public File getAssociatedFile() {
677 return data.storageFile;
678 }
679
680 @Override
681 public void setAssociatedFile(File file) {
682 data.storageFile = file;
683 }
684
685 /**
686 * Action that issues a series of download requests to the API, following the GPX track.
687 *
688 * @author fred
689 */
690 public class DownloadAlongTrackAction extends AbstractAction {
691 public DownloadAlongTrackAction() {
692 super(tr("Download from OSM along this track"), ImageProvider.get("downloadalongtrack"));
693 }
694
695 public void actionPerformed(ActionEvent e) {
696 JPanel msg = new JPanel(new GridBagLayout());
697 Integer dist[] = { 5000, 500, 50 };
698 Integer area[] = { 20, 10, 5, 1 };
699
700 msg.add(new JLabel(tr("Download everything within:")), GBC.eol());
701 String s[] = new String[dist.length];
702 for (int i = 0; i < dist.length; ++i) {
703 s[i] = tr("{0} meters", dist[i]);
704 }
705 JList buffer = new JList(s);
706 buffer.setSelectedIndex(Main.pref.getInteger(PREF_DOWNLOAD_ALONG_TRACK_DISTANCE, 0));
707 msg.add(buffer, GBC.eol());
708
709 msg.add(new JLabel(tr("Maximum area per request:")), GBC.eol());
710 s = new String[area.length];
711 for (int i = 0; i < area.length; ++i) {
712 s[i] = tr("{0} sq km", area[i]);
713 }
714 JList maxRect = new JList(s);
715 maxRect.setSelectedIndex(Main.pref.getInteger(PREF_DOWNLOAD_ALONG_TRACK_AREA, 0));
716 msg.add(maxRect, GBC.eol());
717
718 msg.add(new JLabel(tr("Download near:")), GBC.eol());
719 JList downloadNear = new JList(new String[] { tr("track only"), tr("waypoints only"), tr("track and waypoints") });
720 int NEAR_TRACK=0;
721 int NEAR_WAYPOINTS=1;
722 int NEAR_BOTH=2;
723
724 downloadNear.setSelectedIndex(Main.pref.getInteger(PREF_DOWNLOAD_ALONG_TRACK_NEAR, 0));
725 msg.add(downloadNear, GBC.eol());
726
727 int ret = JOptionPane.showConfirmDialog(
728 Main.parent,
729 msg,
730 tr("Download from OSM along this track"),
731 JOptionPane.OK_CANCEL_OPTION,
732 JOptionPane.QUESTION_MESSAGE
733 );
734 switch(ret) {
735 case JOptionPane.CANCEL_OPTION:
736 case JOptionPane.CLOSED_OPTION:
737 return;
738 default:
739 // continue
740 }
741
742 Main.pref.putInteger(PREF_DOWNLOAD_ALONG_TRACK_DISTANCE, buffer.getSelectedIndex());
743 Main.pref.putInteger(PREF_DOWNLOAD_ALONG_TRACK_AREA, maxRect.getSelectedIndex());
744 int near = downloadNear.getSelectedIndex();
745 Main.pref.putInteger(PREF_DOWNLOAD_ALONG_TRACK_NEAR, near);
746
747 /*
748 * Find the average latitude for the data we're contemplating, so we can know how many
749 * metres per degree of longitude we have.
750 */
751 double latsum = 0;
752 int latcnt = 0;
753
754 if (near == NEAR_TRACK || near == NEAR_BOTH) {
755 for (GpxTrack trk : data.tracks) {
756 for (GpxTrackSegment segment : trk.getSegments()) {
757 for (WayPoint p : segment.getWayPoints()) {
758 latsum += p.getCoor().lat();
759 latcnt++;
760 }
761 }
762 }
763 }
764
765 if (near == NEAR_WAYPOINTS || near == NEAR_BOTH) {
766 for (WayPoint p : data.waypoints) {
767 latsum += p.getCoor().lat();
768 latcnt++;
769 }
770 }
771
772 double avglat = latsum / latcnt;
773 double scale = Math.cos(Math.toRadians(avglat));
774
775 /*
776 * Compute buffer zone extents and maximum bounding box size. Note that the maximum we
777 * ever offer is a bbox area of 0.002, while the API theoretically supports 0.25, but as
778 * soon as you touch any built-up area, that kind of bounding box will download forever
779 * and then stop because it has more than 50k nodes.
780 */
781 Integer i = buffer.getSelectedIndex();
782 int buffer_dist = dist[i < 0 ? 0 : i];
783 double buffer_y = buffer_dist / 100000.0;
784 double buffer_x = buffer_y / scale;
785 i = maxRect.getSelectedIndex();
786 double max_area = area[i < 0 ? 0 : i] / 10000.0 / scale;
787 Area a = new Area();
788 Rectangle2D r = new Rectangle2D.Double();
789
790 /*
791 * Collect the combined area of all gpx points plus buffer zones around them. We ignore
792 * points that lie closer to the previous point than the given buffer size because
793 * otherwise this operation takes ages.
794 */
795 LatLon previous = null;
796 if (near == NEAR_TRACK || near == NEAR_BOTH) {
797 for (GpxTrack trk : data.tracks) {
798 for (GpxTrackSegment segment : trk.getSegments()) {
799 for (WayPoint p : segment.getWayPoints()) {
800 LatLon c = p.getCoor();
801 if (previous == null || c.greatCircleDistance(previous) > buffer_dist) {
802 // we add a buffer around the point.
803 r.setRect(c.lon() - buffer_x, c.lat() - buffer_y, 2 * buffer_x, 2 * buffer_y);
804 a.add(new Area(r));
805 previous = c;
806 }
807 }
808 }
809 }
810 }
811 if (near == NEAR_WAYPOINTS || near == NEAR_BOTH) {
812 for (WayPoint p : data.waypoints) {
813 LatLon c = p.getCoor();
814 if (previous == null || c.greatCircleDistance(previous) > buffer_dist) {
815 // we add a buffer around the point.
816 r.setRect(c.lon() - buffer_x, c.lat() - buffer_y, 2 * buffer_x, 2 * buffer_y);
817 a.add(new Area(r));
818 previous = c;
819 }
820 }
821 }
822
823 /*
824 * Area "a" now contains the hull that we would like to download data for. however we
825 * can only download rectangles, so the following is an attempt at finding a number of
826 * rectangles to download.
827 *
828 * The idea is simply: Start out with the full bounding box. If it is too large, then
829 * split it in half and repeat recursively for each half until you arrive at something
830 * small enough to download. The algorithm is improved by always using the intersection
831 * between the rectangle and the actual desired area. For example, if you have a track
832 * that goes like this: +----+ | /| | / | | / | |/ | +----+ then we would first look at
833 * downloading the whole rectangle (assume it's too big), after that we split it in half
834 * (upper and lower half), but we donot request the full upper and lower rectangle, only
835 * the part of the upper/lower rectangle that actually has something in it.
836 */
837
838 List<Rectangle2D> toDownload = new ArrayList<Rectangle2D>();
839
840 addToDownload(a, a.getBounds(), toDownload, max_area);
841
842 msg = new JPanel(new GridBagLayout());
843
844 msg.add(new JLabel(
845 tr("<html>This action will require {0} individual<br>"
846 + "download requests. Do you wish<br>to continue?</html>",
847 toDownload.size())), GBC.eol());
848
849 if (toDownload.size() > 1) {
850 ret = JOptionPane.showConfirmDialog(
851 Main.parent,
852 msg,
853 tr("Download from OSM along this track"),
854 JOptionPane.OK_CANCEL_OPTION,
855 JOptionPane.PLAIN_MESSAGE
856 );
857 switch(ret) {
858 case JOptionPane.CANCEL_OPTION:
859 case JOptionPane.CLOSED_OPTION:
860 return;
861 default:
862 // continue
863 }
864 }
865 final PleaseWaitProgressMonitor monitor = new PleaseWaitProgressMonitor(tr("Download data"));
866 final Future<?> future = new DownloadOsmTaskList().download(false, toDownload, monitor);
867 Main.worker.submit(
868 new Runnable() {
869 public void run() {
870 try {
871 future.get();
872 } catch(Exception e) {
873 e.printStackTrace();
874 return;
875 }
876 monitor.close();
877 }
878 }
879 );
880 }
881 }
882
883 private static void addToDownload(Area a, Rectangle2D r, Collection<Rectangle2D> results, double max_area) {
884 Area tmp = new Area(r);
885 // intersect with sought-after area
886 tmp.intersect(a);
887 if (tmp.isEmpty())
888 return;
889 Rectangle2D bounds = tmp.getBounds2D();
890 if (bounds.getWidth() * bounds.getHeight() > max_area) {
891 // the rectangle gets too large; split it and make recursive call.
892 Rectangle2D r1;
893 Rectangle2D r2;
894 if (bounds.getWidth() > bounds.getHeight()) {
895 // rectangles that are wider than high are split into a left and right half,
896 r1 = new Rectangle2D.Double(bounds.getX(), bounds.getY(), bounds.getWidth() / 2, bounds.getHeight());
897 r2 = new Rectangle2D.Double(bounds.getX() + bounds.getWidth() / 2, bounds.getY(),
898 bounds.getWidth() / 2, bounds.getHeight());
899 } else {
900 // others into a top and bottom half.
901 r1 = new Rectangle2D.Double(bounds.getX(), bounds.getY(), bounds.getWidth(), bounds.getHeight() / 2);
902 r2 = new Rectangle2D.Double(bounds.getX(), bounds.getY() + bounds.getHeight() / 2, bounds.getWidth(),
903 bounds.getHeight() / 2);
904 }
905 addToDownload(a, r1, results, max_area);
906 addToDownload(a, r2, results, max_area);
907 } else {
908 results.add(bounds);
909 }
910 }
911
912 /**
913 * Makes a new marker layer derived from this GpxLayer containing at least one audio marker
914 * which the given audio file is associated with. Markers are derived from the following (a)
915 * explict waypoints in the GPX layer, or (b) named trackpoints in the GPX layer, or (d)
916 * timestamp on the wav file (e) (in future) voice recognised markers in the sound recording (f)
917 * a single marker at the beginning of the track
918 * @param wavFile : the file to be associated with the markers in the new marker layer
919 * @param markers : keeps track of warning messages to avoid repeated warnings
920 */
921 private void importAudio(File wavFile, MarkerLayer ml, double firstStartTime, Markers markers) {
922 String uri = "file:".concat(wavFile.getAbsolutePath());
923 Collection<WayPoint> waypoints = new ArrayList<WayPoint>();
924 boolean timedMarkersOmitted = false;
925 boolean untimedMarkersOmitted = false;
926 double snapDistance = Main.pref.getDouble("marker.audiofromuntimedwaypoints.distance", 1.0e-3); /*
927 * about
928 * 25
929 * m
930 */
931 WayPoint wayPointFromTimeStamp = null;
932
933 // determine time of first point in track
934 double firstTime = -1.0;
935 if (data.tracks != null && !data.tracks.isEmpty()) {
936 for (GpxTrack track : data.tracks) {
937 for (GpxTrackSegment seg : track.getSegments()) {
938 for (WayPoint w : seg.getWayPoints()) {
939 firstTime = w.time;
940 break;
941 }
942 if (firstTime >= 0.0) {
943 break;
944 }
945 }
946 if (firstTime >= 0.0) {
947 break;
948 }
949 }
950 }
951 if (firstTime < 0.0) {
952 JOptionPane.showMessageDialog(
953 Main.parent,
954 tr("No GPX track available in layer to associate audio with."),
955 tr("Error"),
956 JOptionPane.ERROR_MESSAGE
957 );
958 return;
959 }
960
961 // (a) try explicit timestamped waypoints - unless suppressed
962 if (Main.pref.getBoolean("marker.audiofromexplicitwaypoints", true) && data.waypoints != null
963 && !data.waypoints.isEmpty()) {
964 for (WayPoint w : data.waypoints) {
965 if (w.time > firstTime) {
966 waypoints.add(w);
967 } else if (w.time > 0.0) {
968 timedMarkersOmitted = true;
969 }
970 }
971 }
972
973 // (b) try explicit waypoints without timestamps - unless suppressed
974 if (Main.pref.getBoolean("marker.audiofromuntimedwaypoints", true) && data.waypoints != null
975 && !data.waypoints.isEmpty()) {
976 for (WayPoint w : data.waypoints) {
977 if (waypoints.contains(w)) {
978 continue;
979 }
980 WayPoint wNear = nearestPointOnTrack(w.getEastNorth(), snapDistance);
981 if (wNear != null) {
982 WayPoint wc = new WayPoint(w.getCoor());
983 wc.time = wNear.time;
984 if (w.attr.containsKey("name")) {
985 wc.attr.put("name", w.getString("name"));
986 }
987 waypoints.add(wc);
988 } else {
989 untimedMarkersOmitted = true;
990 }
991 }
992 }
993
994 // (c) use explicitly named track points, again unless suppressed
995 if ((Main.pref.getBoolean("marker.audiofromnamedtrackpoints", false)) && data.tracks != null
996 && !data.tracks.isEmpty()) {
997 for (GpxTrack track : data.tracks) {
998 for (GpxTrackSegment seg : track.getSegments()) {
999 for (WayPoint w : seg.getWayPoints()) {
1000 if (w.attr.containsKey("name") || w.attr.containsKey("desc")) {
1001 waypoints.add(w);
1002 }
1003 }
1004 }
1005 }
1006 }
1007
1008 // (d) use timestamp of file as location on track
1009 if ((Main.pref.getBoolean("marker.audiofromwavtimestamps", false)) && data.tracks != null
1010 && !data.tracks.isEmpty()) {
1011 double lastModified = wavFile.lastModified() / 1000.0; // lastModified is in
1012 // milliseconds
1013 double duration = AudioUtil.getCalibratedDuration(wavFile);
1014 double startTime = lastModified - duration;
1015 startTime = firstStartTime + (startTime - firstStartTime)
1016 / Main.pref.getDouble("audio.calibration", "1.0" /* default, ratio */);
1017 WayPoint w1 = null;
1018 WayPoint w2 = null;
1019
1020 for (GpxTrack track : data.tracks) {
1021 for (GpxTrackSegment seg : track.getSegments()) {
1022 for (WayPoint w : seg.getWayPoints()) {
1023 if (startTime < w.time) {
1024 w2 = w;
1025 break;
1026 }
1027 w1 = w;
1028 }
1029 if (w2 != null) {
1030 break;
1031 }
1032 }
1033 }
1034
1035 if (w1 == null || w2 == null) {
1036 timedMarkersOmitted = true;
1037 } else {
1038 wayPointFromTimeStamp = new WayPoint(w1.getCoor().interpolate(w2.getCoor(),
1039 (startTime - w1.time) / (w2.time - w1.time)));
1040 wayPointFromTimeStamp.time = startTime;
1041 String name = wavFile.getName();
1042 int dot = name.lastIndexOf(".");
1043 if (dot > 0) {
1044 name = name.substring(0, dot);
1045 }
1046 wayPointFromTimeStamp.attr.put("name", name);
1047 waypoints.add(wayPointFromTimeStamp);
1048 }
1049 }
1050
1051 // (e) analyse audio for spoken markers here, in due course
1052
1053 // (f) simply add a single marker at the start of the track
1054 if ((Main.pref.getBoolean("marker.audiofromstart") || waypoints.isEmpty()) && data.tracks != null
1055 && !data.tracks.isEmpty()) {
1056 boolean gotOne = false;
1057 for (GpxTrack track : data.tracks) {
1058 for (GpxTrackSegment seg : track.getSegments()) {
1059 for (WayPoint w : seg.getWayPoints()) {
1060 WayPoint wStart = new WayPoint(w.getCoor());
1061 wStart.attr.put("name", "start");
1062 wStart.time = w.time;
1063 waypoints.add(wStart);
1064 gotOne = true;
1065 break;
1066 }
1067 if (gotOne) {
1068 break;
1069 }
1070 }
1071 if (gotOne) {
1072 break;
1073 }
1074 }
1075 }
1076
1077 /* we must have got at least one waypoint now */
1078
1079 Collections.sort((ArrayList<WayPoint>) waypoints, new Comparator<WayPoint>() {
1080 public int compare(WayPoint a, WayPoint b) {
1081 return a.time <= b.time ? -1 : 1;
1082 }
1083 });
1084
1085 firstTime = -1.0; /* this time of the first waypoint, not first trackpoint */
1086 for (WayPoint w : waypoints) {
1087 if (firstTime < 0.0) {
1088 firstTime = w.time;
1089 }
1090 double offset = w.time - firstTime;
1091 String name;
1092 if (w.attr.containsKey("name")) {
1093 name = w.getString("name");
1094 } else if (w.attr.containsKey("desc")) {
1095 name = w.getString("desc");
1096 } else {
1097 name = AudioMarker.inventName(offset);
1098 }
1099 AudioMarker am = AudioMarker.create(w.getCoor(), name, uri, ml, w.time, offset);
1100 /*
1101 * timeFromAudio intended for future use to shift markers of this type on
1102 * synchronization
1103 */
1104 if (w == wayPointFromTimeStamp) {
1105 am.timeFromAudio = true;
1106 }
1107 ml.data.add(am);
1108 }
1109
1110 if (timedMarkersOmitted && !markers.timedMarkersOmitted) {
1111 JOptionPane
1112 .showMessageDialog(
1113 Main.parent,
1114 tr("Some waypoints with timestamps from before the start of the track or after the end were omitted or moved to the start."));
1115 markers.timedMarkersOmitted = timedMarkersOmitted;
1116 }
1117 if (untimedMarkersOmitted && !markers.untimedMarkersOmitted) {
1118 JOptionPane
1119 .showMessageDialog(
1120 Main.parent,
1121 tr("Some waypoints which were too far from the track to sensibly estimate their time were omitted."));
1122 markers.untimedMarkersOmitted = untimedMarkersOmitted;
1123 }
1124 }
1125
1126 /**
1127 * Makes a WayPoint at the projection of point P onto the track providing P is less than
1128 * tolerance away from the track
1129 *
1130 * @param P : the point to determine the projection for
1131 * @param tolerance : must be no further than this from the track
1132 * @return the closest point on the track to P, which may be the first or last point if off the
1133 * end of a segment, or may be null if nothing close enough
1134 */
1135 public WayPoint nearestPointOnTrack(EastNorth P, double tolerance) {
1136 /*
1137 * assume the coordinates of P are xp,yp, and those of a section of track between two
1138 * trackpoints are R=xr,yr and S=xs,ys. Let N be the projected point.
1139 *
1140 * The equation of RS is Ax + By + C = 0 where A = ys - yr B = xr - xs C = - Axr - Byr
1141 *
1142 * Also, note that the distance RS^2 is A^2 + B^2
1143 *
1144 * If RS^2 == 0.0 ignore the degenerate section of track
1145 *
1146 * PN^2 = (Axp + Byp + C)^2 / RS^2 that is the distance from P to the line
1147 *
1148 * so if PN^2 is less than PNmin^2 (initialized to tolerance) we can reject the line;
1149 * otherwise... determine if the projected poijnt lies within the bounds of the line: PR^2 -
1150 * PN^2 <= RS^2 and PS^2 - PN^2 <= RS^2
1151 *
1152 * where PR^2 = (xp - xr)^2 + (yp-yr)^2 and PS^2 = (xp - xs)^2 + (yp-ys)^2
1153 *
1154 * If so, calculate N as xn = xr + (RN/RS) B yn = y1 + (RN/RS) A
1155 *
1156 * where RN = sqrt(PR^2 - PN^2)
1157 */
1158
1159 double PNminsq = tolerance * tolerance;
1160 EastNorth bestEN = null;
1161 double bestTime = 0.0;
1162 double px = P.east();
1163 double py = P.north();
1164 double rx = 0.0, ry = 0.0, sx, sy, x, y;
1165 if (data.tracks == null)
1166 return null;
1167 for (GpxTrack track : data.tracks) {
1168 for (GpxTrackSegment seg : track.getSegments()) {
1169 WayPoint R = null;
1170 for (WayPoint S : seg.getWayPoints()) {
1171 EastNorth c = S.getEastNorth();
1172 if (R == null) {
1173 R = S;
1174 rx = c.east();
1175 ry = c.north();
1176 x = px - rx;
1177 y = py - ry;
1178 double PRsq = x * x + y * y;
1179 if (PRsq < PNminsq) {
1180 PNminsq = PRsq;
1181 bestEN = c;
1182 bestTime = R.time;
1183 }
1184 } else {
1185 sx = c.east();
1186 sy = c.north();
1187 double A = sy - ry;
1188 double B = rx - sx;
1189 double C = -A * rx - B * ry;
1190 double RSsq = A * A + B * B;
1191 if (RSsq == 0.0) {
1192 continue;
1193 }
1194 double PNsq = A * px + B * py + C;
1195 PNsq = PNsq * PNsq / RSsq;
1196 if (PNsq < PNminsq) {
1197 x = px - rx;
1198 y = py - ry;
1199 double PRsq = x * x + y * y;
1200 x = px - sx;
1201 y = py - sy;
1202 double PSsq = x * x + y * y;
1203 if (PRsq - PNsq <= RSsq && PSsq - PNsq <= RSsq) {
1204 double RNoverRS = Math.sqrt((PRsq - PNsq) / RSsq);
1205 double nx = rx - RNoverRS * B;
1206 double ny = ry + RNoverRS * A;
1207 bestEN = new EastNorth(nx, ny);
1208 bestTime = R.time + RNoverRS * (S.time - R.time);
1209 PNminsq = PNsq;
1210 }
1211 }
1212 R = S;
1213 rx = sx;
1214 ry = sy;
1215 }
1216 }
1217 if (R != null) {
1218 EastNorth c = R.getEastNorth();
1219 /* if there is only one point in the seg, it will do this twice, but no matter */
1220 rx = c.east();
1221 ry = c.north();
1222 x = px - rx;
1223 y = py - ry;
1224 double PRsq = x * x + y * y;
1225 if (PRsq < PNminsq) {
1226 PNminsq = PRsq;
1227 bestEN = c;
1228 bestTime = R.time;
1229 }
1230 }
1231 }
1232 }
1233 if (bestEN == null)
1234 return null;
1235 WayPoint best = new WayPoint(Main.getProjection().eastNorth2latlon(bestEN));
1236 best.time = bestTime;
1237 return best;
1238 }
1239
1240 private class CustomizeLineDrawing extends AbstractAction {
1241
1242 CustomizeLineDrawing() {
1243 super(tr("Customize line drawing"), ImageProvider.get("mapmode/addsegment"));
1244 }
1245
1246 @Override
1247 public void actionPerformed(ActionEvent e) {
1248 JRadioButton[] r = new JRadioButton[3];
1249 r[0] = new JRadioButton(tr("Use global settings."));
1250 r[1] = new JRadioButton(tr("Draw lines between points for this layer."));
1251 r[2] = new JRadioButton(tr("Do not draw lines between points for this layer."));
1252 ButtonGroup group = new ButtonGroup();
1253 Box panel = Box.createVerticalBox();
1254 for (JRadioButton b : r) {
1255 group.add(b);
1256 panel.add(b);
1257 }
1258 String propName = "draw.rawgps.lines.layer " + getName();
1259 if (Main.pref.hasKey(propName)) {
1260 group.setSelected(r[Main.pref.getBoolean(propName) ? 1 : 2].getModel(), true);
1261 } else {
1262 group.setSelected(r[0].getModel(), true);
1263 }
1264 int answer = JOptionPane.showConfirmDialog(Main.parent, panel,
1265 tr("Select line drawing options"), JOptionPane.OK_CANCEL_OPTION, JOptionPane.QUESTION_MESSAGE);
1266 switch (answer) {
1267 case JOptionPane.CANCEL_OPTION:
1268 case JOptionPane.CLOSED_OPTION:
1269 return;
1270 default:
1271 // continue
1272 }
1273 if (group.getSelection() == r[0].getModel()) {
1274 Main.pref.put(propName, null);
1275 } else {
1276 Main.pref.put(propName, group.getSelection() == r[1].getModel());
1277 }
1278 Main.map.repaint();
1279 }
1280 }
1281
1282 private class CustomizeColor extends AbstractAction {
1283
1284 public CustomizeColor() {
1285 super(tr("Customize Color"), ImageProvider.get("colorchooser"));
1286 putValue("help", ht("/Action/LayerCustomizeColor"));
1287 }
1288
1289 @Override
1290 public void actionPerformed(ActionEvent e) {
1291 JColorChooser c = new JColorChooser(getColor(getName()));
1292 Object[] options = new Object[] { tr("OK"), tr("Cancel"), tr("Default") };
1293 int answer = JOptionPane.showOptionDialog(
1294 Main.parent,
1295 c,
1296 tr("Choose a color"),
1297 JOptionPane.OK_CANCEL_OPTION,
1298 JOptionPane.PLAIN_MESSAGE,
1299 null,
1300 options, options[0]
1301 );
1302 switch (answer) {
1303 case 0:
1304 Main.pref.putColor("layer " + getName(), c.getColor());
1305 break;
1306 case 1:
1307 return;
1308 case 2:
1309 Main.pref.putColor("layer " + getName(), null);
1310 break;
1311 }
1312 Main.map.repaint();
1313 }
1314
1315 }
1316
1317 private class MarkersFromNamedPoins extends AbstractAction {
1318
1319 public MarkersFromNamedPoins() {
1320 super(tr("Markers From Named Points"), ImageProvider.get("addmarkers"));
1321 putValue("help", ht("/Action/MarkersFromNamedPoints"));
1322 }
1323
1324 @Override
1325 public void actionPerformed(ActionEvent e) {
1326 GpxData namedTrackPoints = new GpxData();
1327 for (GpxTrack track : data.tracks) {
1328 for (GpxTrackSegment seg : track.getSegments()) {
1329 for (WayPoint point : seg.getWayPoints())
1330 if (point.attr.containsKey("name") || point.attr.containsKey("desc")) {
1331 namedTrackPoints.waypoints.add(point);
1332 }
1333 }
1334 }
1335
1336 MarkerLayer ml = new MarkerLayer(namedTrackPoints, tr("Named Trackpoints from {0}", getName()),
1337 getAssociatedFile(), GpxLayer.this);
1338 if (ml.data.size() > 0) {
1339 Main.main.addLayer(ml);
1340 }
1341
1342 }
1343 }
1344
1345 private class ImportAudio extends AbstractAction {
1346
1347 public ImportAudio() {
1348 super(tr("Import Audio"), ImageProvider.get("importaudio"));
1349 putValue("help", ht("/Action/ImportAudio"));
1350 }
1351
1352 private void warnCantImportIntoServerLayer(GpxLayer layer) {
1353 String msg = tr("<html>The data in the GPX layer ''{0}'' has been downloaded from the server.<br>"
1354 + "Because its way points do not include a timestamp we cannot correlate them with audio data.</html>",
1355 layer.getName()
1356 );
1357 HelpAwareOptionPane.showOptionDialog(
1358 Main.parent,
1359 msg,
1360 tr("Import not possible"),
1361 JOptionPane.WARNING_MESSAGE,
1362 ht("/Action/ImportAudio#CantImportIntoGpxLayerFromServer")
1363 );
1364 }
1365
1366 @Override
1367 public void actionPerformed(ActionEvent e) {
1368 if (GpxLayer.this.data.fromServer) {
1369 warnCantImportIntoServerLayer(GpxLayer.this);
1370 return;
1371 }
1372 String dir = Main.pref.get("markers.lastaudiodirectory");
1373 JFileChooser fc = new JFileChooser(dir);
1374 fc.setFileSelectionMode(JFileChooser.FILES_ONLY);
1375 fc.setAcceptAllFileFilterUsed(false);
1376 fc.setFileFilter(new FileFilter() {
1377 @Override
1378 public boolean accept(File f) {
1379 return f.isDirectory() || f.getName().toLowerCase().endsWith(".wav");
1380 }
1381
1382 @Override
1383 public String getDescription() {
1384 return tr("Wave Audio files (*.wav)");
1385 }
1386 });
1387 fc.setMultiSelectionEnabled(true);
1388 if (fc.showOpenDialog(Main.parent) == JFileChooser.APPROVE_OPTION) {
1389 if (!fc.getCurrentDirectory().getAbsolutePath().equals(dir)) {
1390 Main.pref.put("markers.lastaudiodirectory", fc.getCurrentDirectory().getAbsolutePath());
1391 }
1392
1393 File sel[] = fc.getSelectedFiles();
1394 // sort files in increasing order of timestamp (this is the end time, but so
1395 // long as they don't overlap, that's fine)
1396 if (sel.length > 1) {
1397 Arrays.sort(sel, new Comparator<File>() {
1398 public int compare(File a, File b) {
1399 return a.lastModified() <= b.lastModified() ? -1 : 1;
1400 }
1401 });
1402 }
1403
1404 String names = null;
1405 for (int i = 0; i < sel.length; i++) {
1406 if (names == null) {
1407 names = " (";
1408 } else {
1409 names += ", ";
1410 }
1411 names += sel[i].getName();
1412 }
1413 if (names != null) {
1414 names += ")";
1415 } else {
1416 names = "";
1417 }
1418 MarkerLayer ml = new MarkerLayer(new GpxData(), tr("Audio markers from {0}", getName()) + names,
1419 getAssociatedFile(), GpxLayer.this);
1420 double firstStartTime = sel[0].lastModified() / 1000.0 /* ms -> seconds */
1421 - AudioUtil.getCalibratedDuration(sel[0]);
1422
1423 Markers m = new Markers();
1424 for (int i = 0; i < sel.length; i++) {
1425 importAudio(sel[i], ml, firstStartTime, m);
1426 }
1427 Main.main.addLayer(ml);
1428 Main.map.repaint();
1429 }
1430
1431 }
1432 }
1433
1434 private class ImportImages extends AbstractAction {
1435
1436 public ImportImages() {
1437 super(tr("Import images"), ImageProvider.get("dialogs/geoimage"));
1438 putValue("help", ht("/Action/ImportImages"));
1439 }
1440
1441 private void warnCantImportIntoServerLayer(GpxLayer layer) {
1442 String msg = tr("<html>The data in the GPX layer ''{0}'' has been downloaded from the server.<br>"
1443 + "Because its way points do not include a timestamp we cannot correlate them with images.</html>",
1444 layer.getName()
1445 );
1446 HelpAwareOptionPane.showOptionDialog(
1447 Main.parent,
1448 msg,
1449 tr("Import not possible"),
1450 JOptionPane.WARNING_MESSAGE,
1451 ht("/Action/ImportImages#CantImportIntoGpxLayerFromServer")
1452 );
1453 }
1454
1455 private void addRecursiveFiles(LinkedList<File> files, File[] sel) {
1456 for (File f : sel) {
1457 if (f.isDirectory()) {
1458 addRecursiveFiles(files, f.listFiles());
1459 } else if (f.getName().toLowerCase().endsWith(".jpg")) {
1460 files.add(f);
1461 }
1462 }
1463 }
1464
1465 @Override
1466 public void actionPerformed(ActionEvent e) {
1467
1468 if (GpxLayer.this.data.fromServer) {
1469 warnCantImportIntoServerLayer(GpxLayer.this);
1470 return;
1471 }
1472 String curDir = Main.pref.get("geoimage.lastdirectory", Main.pref.get("lastDirectory"));
1473 if (curDir.equals("")) {
1474 curDir = ".";
1475 }
1476 JFileChooser fc = new JFileChooser(new File(curDir));
1477
1478 fc.setFileSelectionMode(JFileChooser.FILES_AND_DIRECTORIES);
1479 fc.setMultiSelectionEnabled(true);
1480 fc.setAcceptAllFileFilterUsed(false);
1481 JpgImporter importer = new JpgImporter(GpxLayer.this);
1482 fc.setFileFilter(importer.filter);
1483 fc.showOpenDialog(Main.parent);
1484 LinkedList<File> files = new LinkedList<File>();
1485 File[] sel = fc.getSelectedFiles();
1486 if (sel == null || sel.length == 0)
1487 return;
1488 if (!fc.getCurrentDirectory().getAbsolutePath().equals(curDir)) {
1489 Main.pref.put("geoimage.lastdirectory", fc.getCurrentDirectory().getAbsolutePath());
1490 }
1491 addRecursiveFiles(files, sel);
1492 importer.importDataHandleExceptions(files, NullProgressMonitor.INSTANCE);
1493 }
1494 }
1495
1496 @Override
1497 public void projectionChanged(Projection oldValue, Projection newValue) {
1498 if (newValue == null) return;
1499 if (data.waypoints != null) {
1500 for (WayPoint wp : data.waypoints){
1501 wp.invalidateEastNorthCache();
1502 }
1503 }
1504 if (data.tracks != null){
1505 for (GpxTrack track: data.tracks) {
1506 for (GpxTrackSegment segment: track.getSegments()) {
1507 for (WayPoint wp: segment.getWayPoints()) {
1508 wp.invalidateEastNorthCache();
1509 }
1510 }
1511 }
1512 }
1513 if (data.routes != null) {
1514 for (GpxRoute route: data.routes) {
1515 if (route.routePoints == null) {
1516 continue;
1517 }
1518 for (WayPoint wp: route.routePoints) {
1519 wp.invalidateEastNorthCache();
1520 }
1521 }
1522 }
1523 }
1524}
Note: See TracBrowser for help on using the repository browser.