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

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

fix #6629 - patch by akks - fix gpx layer settings changes

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