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