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

Last change on this file since 4718 was 4718, checked in by jttt, 12 years ago

Add posibility to run please wait runnable tasks in background

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