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    }