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

Revision 5050, 86.4 KB checked in by akks, 3 months ago (diff)

UrlLabel class simplification by Zverik, better label layout in VersionInfoPanel (see #7450, #7326)

  • Property svn:eol-style set to native
Line 
1// License: GPL. See LICENSE file for details.
2
3package org.openstreetmap.josm.gui.layer;
4
5import static org.openstreetmap.josm.gui.help.HelpUtil.ht;
6import static org.openstreetmap.josm.tools.I18n.marktr;
7import static org.openstreetmap.josm.tools.I18n.tr;
8import static org.openstreetmap.josm.tools.I18n.trn;
9
10import java.awt.BasicStroke;
11import java.awt.Color;
12import java.awt.Component;
13import java.awt.Dimension;
14import java.awt.Graphics2D;
15import java.awt.GridBagLayout;
16import java.awt.Point;
17import java.awt.RenderingHints;
18import java.awt.Stroke;
19import java.awt.Toolkit;
20import java.awt.event.ActionEvent;
21import java.awt.event.MouseAdapter;
22import java.awt.event.MouseEvent;
23import java.awt.event.MouseListener;
24import java.awt.geom.Area;
25import java.awt.geom.Rectangle2D;
26import java.io.File;
27import java.io.IOException;
28import java.net.MalformedURLException;
29import java.net.URL;
30import java.text.DateFormat;
31import java.util.ArrayList;
32import java.util.Arrays;
33import java.util.Collection;
34import java.util.Collections;
35import java.util.Comparator;
36import java.util.LinkedList;
37import java.util.List;
38import java.util.Map;
39import java.util.concurrent.Future;
40
41import javax.swing.AbstractAction;
42import javax.swing.Action;
43import javax.swing.BorderFactory;
44import javax.swing.DefaultComboBoxModel;
45import javax.swing.Icon;
46import javax.swing.JComboBox;
47import javax.swing.JComponent;
48import javax.swing.JFileChooser;
49import javax.swing.JLabel;
50import javax.swing.JList;
51import javax.swing.JMenuItem;
52import javax.swing.JOptionPane;
53import javax.swing.JPanel;
54import javax.swing.JScrollPane;
55import javax.swing.JTable;
56import javax.swing.ListSelectionModel;
57import javax.swing.SwingUtilities;
58import javax.swing.event.ListSelectionEvent;
59import javax.swing.event.ListSelectionListener;
60import javax.swing.filechooser.FileFilter;
61import javax.swing.table.TableCellRenderer;
62
63import org.openstreetmap.josm.Main;
64import org.openstreetmap.josm.actions.AbstractMergeAction.LayerListCellRenderer;
65import org.openstreetmap.josm.actions.RenameLayerAction;
66import org.openstreetmap.josm.actions.downloadtasks.DownloadOsmTaskList;
67import org.openstreetmap.josm.data.Bounds;
68import org.openstreetmap.josm.data.coor.EastNorth;
69import org.openstreetmap.josm.data.coor.LatLon;
70import org.openstreetmap.josm.data.gpx.GpxData;
71import org.openstreetmap.josm.data.gpx.GpxRoute;
72import org.openstreetmap.josm.data.gpx.GpxTrack;
73import org.openstreetmap.josm.data.gpx.GpxTrackSegment;
74import org.openstreetmap.josm.data.gpx.WayPoint;
75import org.openstreetmap.josm.data.osm.DataSet;
76import org.openstreetmap.josm.data.osm.Node;
77import org.openstreetmap.josm.data.osm.Way;
78import org.openstreetmap.josm.data.osm.visitor.BoundingXYVisitor;
79import org.openstreetmap.josm.data.projection.Projection;
80import org.openstreetmap.josm.gui.ConditionalOptionPaneUtil;
81import org.openstreetmap.josm.gui.ExtendedDialog;
82import org.openstreetmap.josm.gui.HelpAwareOptionPane;
83import org.openstreetmap.josm.gui.MapView;
84import org.openstreetmap.josm.gui.NavigatableComponent;
85import org.openstreetmap.josm.gui.PleaseWaitRunnable;
86import org.openstreetmap.josm.gui.dialogs.LayerListDialog;
87import org.openstreetmap.josm.gui.dialogs.LayerListPopup;
88import org.openstreetmap.josm.gui.layer.WMSLayer.PrecacheTask;
89import org.openstreetmap.josm.gui.layer.markerlayer.AudioMarker;
90import org.openstreetmap.josm.gui.layer.markerlayer.MarkerLayer;
91import org.openstreetmap.josm.gui.preferences.display.GPXSettingsPanel;
92import org.openstreetmap.josm.gui.progress.NullProgressMonitor;
93import org.openstreetmap.josm.gui.progress.PleaseWaitProgressMonitor;
94import org.openstreetmap.josm.gui.progress.ProgressMonitor;
95import org.openstreetmap.josm.gui.progress.ProgressTaskId;
96import org.openstreetmap.josm.gui.progress.ProgressTaskIds;
97import org.openstreetmap.josm.gui.widgets.HtmlPanel;
98import org.openstreetmap.josm.io.JpgImporter;
99import org.openstreetmap.josm.io.OsmTransferException;
100import org.openstreetmap.josm.tools.AudioUtil;
101import org.openstreetmap.josm.tools.DateUtils;
102import org.openstreetmap.josm.tools.GBC;
103import org.openstreetmap.josm.tools.ImageProvider;
104import org.openstreetmap.josm.tools.OpenBrowser;
105import org.openstreetmap.josm.tools.UrlLabel;
106import org.openstreetmap.josm.tools.Utils;
107import org.openstreetmap.josm.tools.WindowGeometry;
108import org.xml.sax.SAXException;
109
110public class GpxLayer extends Layer {
111
112    private static final String PREF_DOWNLOAD_ALONG_TRACK_DISTANCE = "gpxLayer.downloadAlongTrack.distance";
113    private static final String PREF_DOWNLOAD_ALONG_TRACK_AREA = "gpxLayer.downloadAlongTrack.area";
114    private static final String PREF_DOWNLOAD_ALONG_TRACK_NEAR = "gpxLayer.downloadAlongTrack.near";
115
116    public GpxData data;
117    protected static final double PHI = Math.toRadians(15);
118    private boolean computeCacheInSync;
119    private int computeCacheMaxLineLengthUsed;
120    private Color computeCacheColorUsed;
121    private boolean computeCacheColorDynamic;
122    private colorModes computeCacheColored;
123    private int computeCacheColorTracksTune;
124    private boolean isLocalFile;
125    // used by ChooseTrackVisibilityAction to determine which tracks to show/hide
126    private boolean[] trackVisibility = new boolean[0];
127
128    private final List<GpxTrack> lastTracks = new ArrayList<GpxTrack>(); // List of tracks at last paint
129    private int lastUpdateCount;
130
131    private static class Markers {
132        public boolean timedMarkersOmitted = false;
133        public boolean untimedMarkersOmitted = false;
134    }
135
136    public GpxLayer(GpxData d) {
137        super((String) d.attr.get("name"));
138        data = d;
139        computeCacheInSync = false;
140        ensureTrackVisibilityLength();
141    }
142
143    public GpxLayer(GpxData d, String name) {
144        this(d);
145        this.setName(name);
146    }
147
148    public GpxLayer(GpxData d, String name, boolean isLocal) {
149        this(d);
150        this.setName(name);
151        this.isLocalFile = isLocal;
152    }
153
154    /**
155     * returns a human readable string that shows the timespan of the given track
156     */
157    private static String getTimespanForTrack(GpxTrack trk) {
158        WayPoint earliest = null, latest = null;
159
160        for (GpxTrackSegment seg : trk.getSegments()) {
161            for (WayPoint pnt : seg.getWayPoints()) {
162                if (latest == null) {
163                    latest = earliest = pnt;
164                } else {
165                    if (pnt.compareTo(earliest) < 0) {
166                        earliest = pnt;
167                    } else {
168                        latest = pnt;
169                    }
170                }
171            }
172        }
173
174        String ts = "";
175
176        if (earliest != null && latest != null) {
177            DateFormat df = DateFormat.getDateInstance(DateFormat.SHORT);
178            String earliestDate = df.format(earliest.getTime());
179            String latestDate = df.format(latest.getTime());
180
181            if (earliestDate.equals(latestDate)) {
182                DateFormat tf = DateFormat.getTimeInstance(DateFormat.SHORT);
183                ts += earliestDate + " ";
184                ts += tf.format(earliest.getTime()) + " - " + tf.format(latest.getTime());
185            } else {
186                DateFormat dtf = DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT);
187                ts += dtf.format(earliest.getTime()) + " - " + dtf.format(latest.getTime());
188            }
189
190            int diff = (int) (latest.time - earliest.time);
191            ts += String.format(" (%d:%02d)", diff / 3600, (diff % 3600) / 60);
192        }
193        return ts;
194    }
195
196    @Override
197    public Icon getIcon() {
198        return ImageProvider.get("layer", "gpx_small");
199    }
200
201    @Override
202    public Object getInfoComponent() {
203        StringBuilder info = new StringBuilder();
204
205        if (data.attr.containsKey("name")) {
206            info.append(tr("Name: {0}", data.attr.get(GpxData.META_NAME))).append("<br>");
207        }
208
209        if (data.attr.containsKey("desc")) {
210            info.append(tr("Description: {0}", data.attr.get(GpxData.META_DESC))).append("<br>");
211        }
212
213        if (data.tracks.size() > 0) {
214            info.append("<table><thead align='center'><tr><td colspan='5'>"
215                    + trn("{0} track", "{0} tracks", data.tracks.size(), data.tracks.size())
216                    + "</td></tr><tr align='center'><td>" + tr("Name") + "</td><td>"
217                    + tr("Description") + "</td><td>" + tr("Timespan")
218                    + "</td><td>" + tr("Length") + "</td><td>" + tr("URL")
219                    + "</td></tr></thead>");
220
221            for (GpxTrack trk : data.tracks) {
222                info.append("<tr><td>");
223                if (trk.getAttributes().containsKey("name")) {
224                    info.append(trk.getAttributes().get("name"));
225                }
226                info.append("</td><td>");
227                if (trk.getAttributes().containsKey("desc")) {
228                    info.append(" ").append(trk.getAttributes().get("desc"));
229                }
230                info.append("</td><td>");
231                info.append(getTimespanForTrack(trk));
232                info.append("</td><td>");
233                info.append(NavigatableComponent.getSystemOfMeasurement().getDistText(trk.length()));
234                info.append("</td><td>");
235                if (trk.getAttributes().containsKey("url")) {
236                    info.append(trk.getAttributes().get("url"));
237                }
238                info.append("</td></tr>");
239            }
240
241            info.append("</table><br><br>");
242
243        }
244
245        info.append(tr("Length: {0}", NavigatableComponent.getSystemOfMeasurement().getDistText(data.length()))).append("<br>");
246
247        info.append(trn("{0} route, ", "{0} routes, ", data.routes.size(), data.routes.size())).append(
248                trn("{0} waypoint", "{0} waypoints", data.waypoints.size(), data.waypoints.size())).append("<br>");
249
250        final JScrollPane sp = new JScrollPane(new HtmlPanel(info.toString()), JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED, JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED);
251        sp.setPreferredSize(new Dimension(sp.getPreferredSize().width, 350));
252        SwingUtilities.invokeLater(new Runnable() {
253            @Override
254            public void run() {
255                sp.getVerticalScrollBar().setValue(0);
256            }
257        });
258        return sp;
259    }
260
261    @Override
262    public Color getColor(boolean ignoreCustom) {
263        Color c = Main.pref.getColor(marktr("gps point"), "layer " + getName(), Color.gray);
264
265        return ignoreCustom || getColorMode() == colorModes.none ? c : null;
266    }
267
268    public colorModes getColorMode() {
269        try {
270            int i=Main.pref.getInteger("draw.rawgps.colors", "layer " + getName(), 0);
271            return colorModes.values()[i];
272        } catch (Exception e) {
273        }
274        return colorModes.none;
275    }
276
277    /* for preferences */
278    static public Color getGenericColor() {
279        return Main.pref.getColor(marktr("gps point"), Color.gray);
280    }
281
282    @Override
283    public Action[] getMenuEntries() {
284        if (Main.applet)
285            return new Action[] {
286                LayerListDialog.getInstance().createShowHideLayerAction(),
287                LayerListDialog.getInstance().createDeleteLayerAction(),
288                SeparatorLayerAction.INSTANCE,
289                new CustomizeColor(this),
290                new CustomizeDrawing(this),
291                new ConvertToDataLayerAction(),
292                SeparatorLayerAction.INSTANCE,
293                new ChooseTrackVisibilityAction(),
294                new RenameLayerAction(getAssociatedFile(), this),
295                SeparatorLayerAction.INSTANCE,
296                new LayerListPopup.InfoAction(this) };
297        return new Action[] {
298                LayerListDialog.getInstance().createShowHideLayerAction(),
299                LayerListDialog.getInstance().createDeleteLayerAction(),
300                SeparatorLayerAction.INSTANCE,
301                new LayerSaveAction(this),
302                new LayerSaveAsAction(this),
303                new CustomizeColor(this),
304                new CustomizeDrawing(this),
305                new ImportImages(),
306                new ImportAudio(),
307                new MarkersFromNamedPoins(),
308                new ConvertToDataLayerAction(),
309                new DownloadAlongTrackAction(),
310                new DownloadWmsAlongTrackAction(),
311                SeparatorLayerAction.INSTANCE,
312                new ChooseTrackVisibilityAction(),
313                new RenameLayerAction(getAssociatedFile(), this),
314                SeparatorLayerAction.INSTANCE,
315                new LayerListPopup.InfoAction(this) };
316    }
317
318    @Override
319    public String getToolTipText() {
320        StringBuilder info = new StringBuilder().append("<html>");
321
322        if (data.attr.containsKey("name")) {
323            info.append(tr("Name: {0}", data.attr.get(GpxData.META_NAME))).append("<br>");
324        }
325
326        if (data.attr.containsKey("desc")) {
327            info.append(tr("Description: {0}", data.attr.get(GpxData.META_DESC))).append("<br>");
328        }
329
330        info.append(trn("{0} track, ", "{0} tracks, ", data.tracks.size(), data.tracks.size()));
331        info.append(trn("{0} route, ", "{0} routes, ", data.routes.size(), data.routes.size()));
332        info.append(trn("{0} waypoint", "{0} waypoints", data.waypoints.size(), data.waypoints.size())).append("<br>");
333
334        info.append(tr("Length: {0}", NavigatableComponent.getSystemOfMeasurement().getDistText(data.length())));
335        info.append("<br>");
336
337        return info.append("</html>").toString();
338    }
339
340    @Override
341    public boolean isMergable(Layer other) {
342        return other instanceof GpxLayer;
343    }
344
345    private int sumUpdateCount() {
346        int updateCount = 0;
347        for (GpxTrack track: data.tracks) {
348            updateCount += track.getUpdateCount();
349        }
350        return updateCount;
351    }
352
353    @Override
354    public boolean isChanged() {
355        if (data.tracks.equals(lastTracks))
356            return sumUpdateCount() != lastUpdateCount;
357        else
358            return true;
359    }
360
361    @Override
362    public void mergeFrom(Layer from) {
363        data.mergeFrom(((GpxLayer) from).data);
364        computeCacheInSync = false;
365    }
366
367    private final static Color[] colors = new Color[256];
368    static {
369        for (int i = 0; i < colors.length; i++) {
370            colors[i] = Color.getHSBColor(i / 300.0f, 1, 1);
371        }
372    }
373
374    private final static Color[] colors_cyclic = new Color[256];
375    static {
376        for (int i = 0; i < colors_cyclic.length; i++) {
377            //                    red   yellow  green   blue    red
378            int[] h = new int[] { 0,    59,     127,    244,    360};
379            int[] s = new int[] { 100,  84,     99,     100 };
380            int[] b = new int[] { 90,   93,     74,     83 };
381
382            float angle = 4 - i / 256f * 4;
383            int quadrant = (int) angle;
384            angle -= quadrant;
385            quadrant = Utils.mod(quadrant+1, 4);
386
387            float vh = h[quadrant] * w(angle) + h[quadrant+1] * (1 - w(angle));
388            float vs = s[quadrant] * w(angle) + s[Utils.mod(quadrant+1, 4)] * (1 - w(angle));
389            float vb = b[quadrant] * w(angle) + b[Utils.mod(quadrant+1, 4)] * (1 - w(angle));
390
391            colors_cyclic[i] = Color.getHSBColor(vh/360f, vs/100f, vb/100f);
392        }
393    }
394
395    /**
396     * transition function:
397     *  w(0)=1, w(1)=0, 0<=w(x)<=1
398     * @param x number: 0<=x<=1
399     * @return the weighted value
400     */
401    private static float w(float x) {
402        if (x < 0.5)
403            return 1 - 2*x*x;
404        else
405            return 2*(1-x)*(1-x);
406    }
407
408    // lookup array to draw arrows without doing any math
409    private final static int ll0 = 9;
410    private final static int sl4 = 5;
411    private final static int sl9 = 3;
412    private final static int[][] dir = { { +sl4, +ll0, +ll0, +sl4 }, { -sl9, +ll0, +sl9, +ll0 }, { -ll0, +sl4, -sl4, +ll0 },
413        { -ll0, -sl9, -ll0, +sl9 }, { -sl4, -ll0, -ll0, -sl4 }, { +sl9, -ll0, -sl9, -ll0 },
414        { +ll0, -sl4, +sl4, -ll0 }, { +ll0, +sl9, +ll0, -sl9 }, { +sl4, +ll0, +ll0, +sl4 },
415        { -sl9, +ll0, +sl9, +ll0 }, { -ll0, +sl4, -sl4, +ll0 }, { -ll0, -sl9, -ll0, +sl9 } };
416
417    // the different color modes
418    enum colorModes {
419        none, velocity, dilution, direction, time
420    }
421
422    @Override
423    public void paint(Graphics2D g, MapView mv, Bounds box) {
424        lastUpdateCount = sumUpdateCount();
425        lastTracks.clear();
426        lastTracks.addAll(data.tracks);
427
428        g.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
429                Main.pref.getBoolean("mappaint.gpx.use-antialiasing", false) ?
430                        RenderingHints.VALUE_ANTIALIAS_ON : RenderingHints.VALUE_ANTIALIAS_OFF);
431
432        /****************************************************************
433         ********** STEP 1 - GET CONFIG VALUES **************************
434         ****************************************************************/
435        // Long startTime = System.currentTimeMillis();
436        Color neutralColor = getColor(true);
437        String spec="layer "+getName();
438
439        // also draw lines between points belonging to different segments
440        boolean forceLines = Main.pref.getBoolean("draw.rawgps.lines.force", spec, false);
441        // draw direction arrows on the lines
442        boolean direction = Main.pref.getBoolean("draw.rawgps.direction", spec, false);
443        // don't draw lines if longer than x meters
444        int lineWidth = Main.pref.getInteger("draw.rawgps.linewidth", spec, 0);
445
446        int maxLineLength;
447        boolean lines;
448        if (this.isLocalFile) {
449            maxLineLength = Main.pref.getInteger("draw.rawgps.max-line-length.local", spec, -1);
450            lines = Main.pref.getBoolean("draw.rawgps.lines.local", spec, true);
451        } else {
452            maxLineLength = Main.pref.getInteger("draw.rawgps.max-line-length", spec, 200);
453            lines = Main.pref.getBoolean("draw.rawgps.lines", spec, true);
454        }
455        // paint large dots for points
456        boolean large = Main.pref.getBoolean("draw.rawgps.large", spec, false);
457        int largesize = Main.pref.getInteger("draw.rawgps.large.size", spec, 3);
458        boolean hdopcircle = Main.pref.getBoolean("draw.rawgps.hdopcircle", spec, false);
459        // color the lines
460        colorModes colored = getColorMode();
461        // paint direction arrow with alternate math. may be faster
462        boolean alternatedirection = Main.pref.getBoolean("draw.rawgps.alternatedirection", spec, false);
463        // don't draw arrows nearer to each other than this
464        int delta = Main.pref.getInteger("draw.rawgps.min-arrow-distance", spec, 40);
465        // allows to tweak line coloring for different speed levels.
466        int colorTracksTune = Main.pref.getInteger("draw.rawgps.colorTracksTune", spec, 45);
467        boolean colorModeDynamic = Main.pref.getBoolean("draw.rawgps.colors.dynamic", spec, false);
468        int hdopfactor = Main.pref.getInteger("hdop.factor", 25);
469
470        Stroke storedStroke = g.getStroke();
471        if(lineWidth != 0)
472        {
473            g.setStroke(new BasicStroke(lineWidth,BasicStroke.CAP_ROUND,BasicStroke.JOIN_ROUND));
474            largesize += lineWidth;
475        }
476
477        /****************************************************************
478         ********** STEP 2a - CHECK CACHE VALIDITY **********************
479         ****************************************************************/
480        if ((computeCacheMaxLineLengthUsed != maxLineLength) || (!neutralColor.equals(computeCacheColorUsed))
481                || (computeCacheColored != colored) || (computeCacheColorTracksTune != colorTracksTune)
482                || (computeCacheColorDynamic != colorModeDynamic)) {
483            computeCacheMaxLineLengthUsed = maxLineLength;
484            computeCacheInSync = false;
485            computeCacheColorUsed = neutralColor;
486            computeCacheColored = colored;
487            computeCacheColorTracksTune = colorTracksTune;
488            computeCacheColorDynamic = colorModeDynamic;
489        }
490
491        /****************************************************************
492         ********** STEP 2b - RE-COMPUTE CACHE DATA *********************
493         ****************************************************************/
494        if (!computeCacheInSync) { // don't compute if the cache is good
495            double minval = +1e10;
496            double maxval = -1e10;
497            WayPoint oldWp = null;
498            if (colorModeDynamic) {
499                if (colored == colorModes.velocity) {
500                    for (GpxTrack trk : data.tracks) {
501                        for (GpxTrackSegment segment : trk.getSegments()) {
502                            if(!forceLines) {
503                                oldWp = null;
504                            }
505                            for (WayPoint trkPnt : segment.getWayPoints()) {
506                                LatLon c = trkPnt.getCoor();
507                                if (Double.isNaN(c.lat()) || Double.isNaN(c.lon())) {
508                                    continue;
509                                }
510                                if (oldWp != null && trkPnt.time > oldWp.time) {
511                                    double vel = c.greatCircleDistance(oldWp.getCoor())
512                                            / (trkPnt.time - oldWp.time);
513                                    if(vel > maxval) {
514                                        maxval = vel;
515                                    }
516                                    if(vel < minval) {
517                                        minval = vel;
518                                    }
519                                }
520                                oldWp = trkPnt;
521                            }
522                        }
523                    }
524                } else if (colored == colorModes.dilution) {
525                    for (GpxTrack trk : data.tracks) {
526                        for (GpxTrackSegment segment : trk.getSegments()) {
527                            for (WayPoint trkPnt : segment.getWayPoints()) {
528                                Object val = trkPnt.attr.get("hdop");
529                                if (val != null) {
530                                    double hdop = ((Float) val).doubleValue();
531                                    if(hdop > maxval) {
532                                        maxval = hdop;
533                                    }
534                                    if(hdop < minval) {
535                                        minval = hdop;
536                                    }
537                                }
538                            }
539                        }
540                    }
541                }
542                oldWp = null;
543            }
544            if (colored == colorModes.time) {
545                for (GpxTrack trk : data.tracks) {
546                    for (GpxTrackSegment segment : trk.getSegments()) {
547                        for (WayPoint trkPnt : segment.getWayPoints()) {
548                            double t=trkPnt.time;
549                            if (t==0) {
550                                continue; // skip non-dated trackpoints
551                            }
552                            if(t > maxval) {
553                                maxval = t;
554                            }
555                            if(t < minval) {
556                                minval = t;
557                            }
558                        }
559                    }
560                }
561            }
562
563            for (GpxTrack trk : data.tracks) {
564                for (GpxTrackSegment segment : trk.getSegments()) {
565                    if (!forceLines) { // don't draw lines between segments, unless forced to
566                        oldWp = null;
567                    }
568                    for (WayPoint trkPnt : segment.getWayPoints()) {
569                        LatLon c = trkPnt.getCoor();
570                        if (Double.isNaN(c.lat()) || Double.isNaN(c.lon())) {
571                            continue;
572                        }
573                        trkPnt.customColoring = neutralColor;
574                        if(colored == colorModes.dilution && trkPnt.attr.get("hdop") != null) {
575                            float hdop = ((Float) trkPnt.attr.get("hdop")).floatValue();
576                            int hdoplvl =(int) Math.round(colorModeDynamic ? ((hdop-minval)*255/(maxval-minval))
577                                    : (hdop <= 0 ? 0 : hdop * hdopfactor));
578                            // High hdop is bad, but high values in colors are green.
579                            // Therefore inverse the logic
580                            int hdopcolor = 255 - (hdoplvl > 255 ? 255 : hdoplvl);
581                            trkPnt.customColoring = colors[hdopcolor];
582                        }
583                        if (oldWp != null) {
584                            double dist = c.greatCircleDistance(oldWp.getCoor());
585                            boolean noDraw=false;
586                            switch (colored) {
587                            case velocity:
588                                double dtime = trkPnt.time - oldWp.time;
589                                if(dtime > 0) {
590                                    float vel = (float) (dist / dtime);
591                                    int velColor =(int) Math.round(colorModeDynamic ? ((vel-minval)*255/(maxval-minval))
592                                            : (vel <= 0 ? 0 : vel / colorTracksTune * 255));
593                                    trkPnt.customColoring = colors[Math.max(0, Math.min(velColor, 255))];
594                                } else {
595                                    trkPnt.customColoring = colors[255];
596                                }
597                                break;
598                            case direction:
599                                double dirColor = oldWp.getCoor().heading(trkPnt.getCoor()) / (2.0 * Math.PI) * 256;
600                                // Bad case first
601                                if (dirColor != dirColor || dirColor < 0.0 || dirColor >= 256.0) {
602                                    trkPnt.customColoring = colors_cyclic[0];
603                                } else {
604                                    trkPnt.customColoring = colors_cyclic[(int) (dirColor)];
605                                }
606                                break;
607                            case time:
608                                if (trkPnt.time>0){
609                                    int tColor = (int) Math.round((trkPnt.time-minval)*255/(maxval-minval));
610                                    trkPnt.customColoring = colors[tColor];
611                                } else {
612                                    trkPnt.customColoring = neutralColor;
613                                }
614                                break;
615                            }
616
617                            if (!noDraw && (maxLineLength == -1 || dist <= maxLineLength)) {
618                                trkPnt.drawLine = true;
619                                trkPnt.dir = (int) oldWp.getCoor().heading(trkPnt.getCoor());
620                            } else {
621                                trkPnt.drawLine = false;
622                            }
623                        } else { // make sure we reset outdated data
624                            trkPnt.drawLine = false;
625                        }
626                        oldWp = trkPnt;
627                    }
628                }
629            }
630            computeCacheInSync = true;
631        }
632
633        LinkedList<WayPoint> visibleSegments = new LinkedList<WayPoint>();
634        WayPoint last = null;
635        int i = 0;
636        ensureTrackVisibilityLength();
637        for (GpxTrack trk: data.tracks) {
638            // hide tracks that were de-selected in ChooseTrackVisibilityAction
639            if(!trackVisibility[i++]) {
640                continue;
641            }
642
643            for (GpxTrackSegment trkSeg: trk.getSegments()) {
644                for(WayPoint pt : trkSeg.getWayPoints())
645                {
646                    Bounds b = new Bounds(pt.getCoor());
647                    // last should never be null when this is true!
648                    if(pt.drawLine) {
649                        b.extend(last.getCoor());
650                    }
651                    if(b.intersects(box))
652                    {
653                        if(last != null && (visibleSegments.isEmpty()
654                                || visibleSegments.getLast() != last)) {
655                            if(last.drawLine) {
656                                WayPoint l = new WayPoint(last);
657                                l.drawLine = false;
658                                visibleSegments.add(l);
659                            } else {
660                                visibleSegments.add(last);
661                            }
662                        }
663                        visibleSegments.add(pt);
664                    }
665                    last = pt;
666                }
667            }
668        }
669        if(visibleSegments.isEmpty())
670            return;
671
672        /****************************************************************
673         ********** STEP 3a - DRAW LINES ********************************
674         ****************************************************************/
675        if (lines) {
676            Point old = null;
677            for (WayPoint trkPnt : visibleSegments) {
678                LatLon c = trkPnt.getCoor();
679                if (Double.isNaN(c.lat()) || Double.isNaN(c.lon())) {
680                    continue;
681                }
682                Point screen = mv.getPoint(trkPnt.getEastNorth());
683                if (trkPnt.drawLine) {
684                    // skip points that are on the same screenposition
685                    if (old != null && ((old.x != screen.x) || (old.y != screen.y))) {
686                        g.setColor(trkPnt.customColoring);
687                        g.drawLine(old.x, old.y, screen.x, screen.y);
688                    }
689                }
690                old = screen;
691            } // end for trkpnt
692        } // end if lines
693
694        /****************************************************************
695         ********** STEP 3b - DRAW NICE ARROWS **************************
696         ****************************************************************/
697        if (lines && direction && !alternatedirection) {
698            Point old = null;
699            Point oldA = null; // last arrow painted
700            for (WayPoint trkPnt : visibleSegments) {
701                LatLon c = trkPnt.getCoor();
702                if (Double.isNaN(c.lat()) || Double.isNaN(c.lon())) {
703                    continue;
704                }
705                if (trkPnt.drawLine) {
706                    Point screen = mv.getPoint(trkPnt.getEastNorth());
707                    // skip points that are on the same screenposition
708                    if (old != null
709                            && (oldA == null || screen.x < oldA.x - delta || screen.x > oldA.x + delta
710                            || screen.y < oldA.y - delta || screen.y > oldA.y + delta)) {
711                        g.setColor(trkPnt.customColoring);
712                        double t = Math.atan2(screen.y - old.y, screen.x - old.x) + Math.PI;
713                        g.drawLine(screen.x, screen.y, (int) (screen.x + 10 * Math.cos(t - PHI)),
714                                (int) (screen.y + 10 * Math.sin(t - PHI)));
715                        g.drawLine(screen.x, screen.y, (int) (screen.x + 10 * Math.cos(t + PHI)),
716                                (int) (screen.y + 10 * Math.sin(t + PHI)));
717                        oldA = screen;
718                    }
719                    old = screen;
720                }
721            } // end for trkpnt
722        } // end if lines
723
724        /****************************************************************
725         ********** STEP 3c - DRAW FAST ARROWS **************************
726         ****************************************************************/
727        if (lines && direction && alternatedirection) {
728            Point old = null;
729            Point oldA = null; // last arrow painted
730            for (WayPoint trkPnt : visibleSegments) {
731                LatLon c = trkPnt.getCoor();
732                if (Double.isNaN(c.lat()) || Double.isNaN(c.lon())) {
733                    continue;
734                }
735                if (trkPnt.drawLine) {
736                    Point screen = mv.getPoint(trkPnt.getEastNorth());
737                    // skip points that are on the same screenposition
738                    if (old != null
739                            && (oldA == null || screen.x < oldA.x - delta || screen.x > oldA.x + delta
740                            || screen.y < oldA.y - delta || screen.y > oldA.y + delta)) {
741                        g.setColor(trkPnt.customColoring);
742                        g.drawLine(screen.x, screen.y, screen.x + dir[trkPnt.dir][0], screen.y
743                                + dir[trkPnt.dir][1]);
744                        g.drawLine(screen.x, screen.y, screen.x + dir[trkPnt.dir][2], screen.y
745                                + dir[trkPnt.dir][3]);
746                        oldA = screen;
747                    }
748                    old = screen;
749                }
750            } // end for trkpnt
751        } // end if lines
752
753        /****************************************************************
754         ********** STEP 3d - DRAW LARGE POINTS AND HDOP CIRCLE *********
755         ****************************************************************/
756        if (large || hdopcircle) {
757            g.setColor(neutralColor);
758            for (WayPoint trkPnt : visibleSegments) {
759                LatLon c = trkPnt.getCoor();
760                if (Double.isNaN(c.lat()) || Double.isNaN(c.lon())) {
761                    continue;
762                }
763                Point screen = mv.getPoint(trkPnt.getEastNorth());
764                g.setColor(trkPnt.customColoring);
765                if (hdopcircle && trkPnt.attr.get("hdop") != null) {
766                    // hdop value
767                    float hdop = ((Float)trkPnt.attr.get("hdop")).floatValue();
768                    if (hdop < 0) {
769                        hdop = 0;
770                    }
771                    // hdop pixels
772                    int hdopp = mv.getPoint(new LatLon(trkPnt.getCoor().lat(), trkPnt.getCoor().lon() + 2*6*hdop*360/40000000)).x - screen.x;
773                    g.drawArc(screen.x-hdopp/2, screen.y-hdopp/2, hdopp, hdopp, 0, 360);
774                }
775                if (large) {
776                    g.fillRect(screen.x-1, screen.y-1, largesize, largesize);
777                }
778            } // end for trkpnt
779        } // end if large || hdopcircle
780
781        /****************************************************************
782         ********** STEP 3e - DRAW SMALL POINTS FOR LINES ***************
783         ****************************************************************/
784        if (!large && lines) {
785            g.setColor(neutralColor);
786            for (WayPoint trkPnt : visibleSegments) {
787                LatLon c = trkPnt.getCoor();
788                if (Double.isNaN(c.lat()) || Double.isNaN(c.lon())) {
789                    continue;
790                }
791                if (!trkPnt.drawLine) {
792                    Point screen = mv.getPoint(trkPnt.getEastNorth());
793                    g.drawRect(screen.x, screen.y, 0, 0);
794                }
795            } // end for trkpnt
796        } // end if large
797
798        /****************************************************************
799         ********** STEP 3f - DRAW SMALL POINTS INSTEAD OF LINES ********
800         ****************************************************************/
801        if (!large && !lines) {
802            g.setColor(neutralColor);
803            for (WayPoint trkPnt : visibleSegments) {
804                LatLon c = trkPnt.getCoor();
805                if (Double.isNaN(c.lat()) || Double.isNaN(c.lon())) {
806                    continue;
807                }
808                Point screen = mv.getPoint(trkPnt.getEastNorth());
809                g.setColor(trkPnt.customColoring);
810                g.drawRect(screen.x, screen.y, 0, 0);
811            } // end for trkpnt
812        } // end if large
813
814        if(lineWidth != 0)
815        {
816            g.setStroke(storedStroke);
817        }
818        // Long duration = System.currentTimeMillis() - startTime;
819        // System.out.println(duration);
820    } // end paint
821
822    @Override
823    public void visitBoundingBox(BoundingXYVisitor v) {
824        v.visit(data.recalculateBounds());
825    }
826
827    public class ConvertToDataLayerAction extends AbstractAction {
828        public ConvertToDataLayerAction() {
829            super(tr("Convert to data layer"), ImageProvider.get("converttoosm"));
830            putValue("help", ht("/Action/ConvertToDataLayer"));
831        }
832
833        @Override
834        public void actionPerformed(ActionEvent e) {
835            JPanel msg = new JPanel(new GridBagLayout());
836            msg
837            .add(
838                    new JLabel(
839                            tr("<html>Upload of unprocessed GPS data as map data is considered harmful.<br>If you want to upload traces, look here:</html>")),
840                            GBC.eol());
841            msg.add(new UrlLabel(tr("http://www.openstreetmap.org/traces"),2), GBC.eop());
842            if (!ConditionalOptionPaneUtil.showConfirmationDialog("convert_to_data", Main.parent, msg, tr("Warning"),
843                    JOptionPane.OK_CANCEL_OPTION, JOptionPane.WARNING_MESSAGE, JOptionPane.OK_OPTION))
844                return;
845            DataSet ds = new DataSet();
846            for (GpxTrack trk : data.tracks) {
847                for (GpxTrackSegment segment : trk.getSegments()) {
848                    List<Node> nodes = new ArrayList<Node>();
849                    for (WayPoint p : segment.getWayPoints()) {
850                        Node n = new Node(p.getCoor());
851                        String timestr = p.getString("time");
852                        if (timestr != null) {
853                            n.setTimestamp(DateUtils.fromString(timestr));
854                        }
855                        ds.addPrimitive(n);
856                        nodes.add(n);
857                    }
858                    Way w = new Way();
859                    w.setNodes(nodes);
860                    ds.addPrimitive(w);
861                }
862            }
863            Main.main
864            .addLayer(new OsmDataLayer(ds, tr("Converted from: {0}", GpxLayer.this.getName()), getAssociatedFile()));
865            Main.main.removeLayer(GpxLayer.this);
866        }
867    }
868
869    @Override
870    public File getAssociatedFile() {
871        return data.storageFile;
872    }
873
874    @Override
875    public void setAssociatedFile(File file) {
876        data.storageFile = file;
877    }
878
879    /** ensures the trackVisibility array has the correct length without losing data.
880     * additional entries are initialized to true;
881     */
882    final private void ensureTrackVisibilityLength() {
883        final int l = data.tracks.size();
884        if(l == trackVisibility.length)
885            return;
886        final boolean[] back = trackVisibility.clone();
887        final int m = Math.min(l, back.length);
888        trackVisibility = new boolean[l];
889        for(int i=0; i < m; i++) {
890            trackVisibility[i] = back[i];
891        }
892        for(int i=m; i < l; i++) {
893            trackVisibility[i] = true;
894        }
895    }
896
897    /**
898     * allows the user to choose which of the downloaded tracks should be displayed.
899     * they can be chosen from the gpx layer context menu.
900     */
901    public class ChooseTrackVisibilityAction extends AbstractAction {
902        public ChooseTrackVisibilityAction() {
903            super(tr("Choose visible tracks"), ImageProvider.get("dialogs/filter"));
904            putValue("help", ht("/Action/ChooseTrackVisibility"));
905        }
906
907        /**
908         * gathers all available data for the tracks and returns them as array of arrays
909         * in the expected column order  */
910        private Object[][] buildTableContents() {
911            Object[][] tracks = new Object[data.tracks.size()][5];
912            int i = 0;
913            for (GpxTrack trk : data.tracks) {
914                Map<String, Object> attr = trk.getAttributes();
915                String name = (String) (attr.containsKey("name") ? attr.get("name") : "");
916                String desc = (String) (attr.containsKey("desc") ? attr.get("desc") : "");
917                String time = getTimespanForTrack(trk);
918                String length = NavigatableComponent.getSystemOfMeasurement().getDistText(trk.length());
919                String url = (String) (attr.containsKey("url") ? attr.get("url") : "");
920                tracks[i] = new String[] {name, desc, time, length, url};
921                i++;
922            }
923            return tracks;
924        }
925
926        /**
927         * Builds an non-editable table whose 5th column will open a browser when double clicked.
928         * The table will fill its parent. */
929        private JTable buildTable(String[] headers, Object[][] content) {
930            final JTable t = new JTable(content, headers) {
931                @Override
932                public Component prepareRenderer(TableCellRenderer renderer, int row, int col) {
933                    Component c = super.prepareRenderer(renderer, row, col);
934                    if (c instanceof JComponent) {
935                        JComponent jc = (JComponent)c;
936                        jc.setToolTipText((String)getValueAt(row, col));
937                    }
938                    return c;
939                }
940
941                @Override
942                public boolean isCellEditable(int rowIndex, int colIndex) {
943                    return false;
944                }
945            };
946            // default column widths
947            t.getColumnModel().getColumn(0).setPreferredWidth(220);
948            t.getColumnModel().getColumn(1).setPreferredWidth(300);
949            t.getColumnModel().getColumn(2).setPreferredWidth(200);
950            t.getColumnModel().getColumn(3).setPreferredWidth(50);
951            t.getColumnModel().getColumn(4).setPreferredWidth(100);
952            // make the link clickable
953            final MouseListener urlOpener = new MouseAdapter() {
954                @Override
955                public void mouseClicked(MouseEvent e) {
956                    if (e.getClickCount() != 2)
957                        return;
958                    JTable t = (JTable)e.getSource();
959                    int col = t.convertColumnIndexToModel(t.columnAtPoint(e.getPoint()));
960                    if(col != 4) // only accept clicks on the URL column
961                        return;
962                    int row = t.rowAtPoint(e.getPoint());
963                    String url = (String) t.getValueAt(row, col);
964                    if(url == "")
965                        return;
966                    OpenBrowser.displayUrl(url);
967                }
968            };
969            t.addMouseListener(urlOpener);
970            t.setFillsViewportHeight(true);
971            return t;
972        }
973
974        /** selects all rows (=tracks) in the table that are currently visible */
975        private void selectVisibleTracksInTable(JTable table) {
976            // don't select any tracks if the layer is not visible
977            if(!isVisible())
978                return;
979            ListSelectionModel s = table.getSelectionModel();
980            s.clearSelection();
981            for(int i=0; i < trackVisibility.length; i++)
982                if(trackVisibility[i]) {
983                    s.addSelectionInterval(i, i);
984                }
985        }
986
987        /** listens to selection changes in the table and redraws the map */
988        private void listenToSelectionChanges(JTable table) {
989            table.getSelectionModel().addListSelectionListener(new ListSelectionListener(){
990                public void valueChanged(ListSelectionEvent e) {
991                    if(!(e.getSource() instanceof ListSelectionModel))
992                        return;
993
994                    ListSelectionModel s =  (ListSelectionModel) e.getSource();
995                    for(int i = 0; i < data.tracks.size(); i++) {
996                        trackVisibility[i] = s.isSelectedIndex(i);
997                    }
998                    Main.map.mapView.preferenceChanged(null);
999                    Main.map.repaint(100);
1000                }
1001            });
1002        }
1003
1004        @Override
1005        public void actionPerformed(ActionEvent arg0) {
1006            final JPanel msg = new JPanel(new GridBagLayout());
1007            msg.add(new JLabel(tr("<html>Select all tracks that you want to be displayed. You can drag select a "
1008                    + "range of tracks or use CTRL+Click to select specific ones. The map is updated live in the "
1009                    + "background. Open the URLs by double clicking them.</html>")),
1010                    GBC.eol().fill(GBC.HORIZONTAL));
1011
1012            // build table
1013            final boolean[] trackVisibilityBackup = trackVisibility.clone();
1014            final String[] headers = {tr("Name"), tr("Description"), tr("Timespan"), tr("Length"), tr("URL")};
1015            final JTable table = buildTable(headers, buildTableContents());
1016            selectVisibleTracksInTable(table);
1017            listenToSelectionChanges(table);
1018
1019            // make the table scrollable
1020            JScrollPane scrollPane = new JScrollPane(table);
1021            msg.add(scrollPane, GBC.eol().fill(GBC.BOTH));
1022
1023            // build dialog
1024            ExtendedDialog ed = new ExtendedDialog(
1025                    Main.parent, tr("Set track visibility for {0}", getName()),
1026                    new String[] {tr("Show all"), tr("Show selected only"), tr("Cancel")});
1027            ed.setButtonIcons(new String[] {"dialogs/layerlist/eye", "dialogs/filter", "cancel"});
1028            ed.setContent(msg, false);
1029            ed.setDefaultButton(2);
1030            ed.setCancelButton(3);
1031            ed.configureContextsensitiveHelp("/Action/ChooseTrackVisibility", true);
1032            ed.setRememberWindowGeometry(
1033                    getClass().getName() + ".geometry",
1034                    WindowGeometry.centerInWindow(Main.parent, new Dimension(1000, 500))
1035                    );
1036            ed.showDialog();
1037            int v = ed.getValue();
1038            // cancel for unknown buttons and copy back original settings
1039            if(v != 1 && v != 2) {
1040                for(int i = 0; i < data.tracks.size(); i++) {
1041                    trackVisibility[i] = trackVisibilityBackup[i];
1042                }
1043                Main.map.repaint();
1044                return;
1045            }
1046
1047            // set visibility (1 = show all, 2 = filter). If no tracks are selected
1048            // set all of them visible and...
1049            ListSelectionModel s = table.getSelectionModel();
1050            final boolean all = v == 1 || s.isSelectionEmpty();
1051            for(int i = 0; i < data.tracks.size(); i++) {
1052                trackVisibility[i] = all || s.isSelectedIndex(i);
1053            }
1054            // ...sync with layer visibility instead to avoid having two ways to hide everything
1055            setVisible(v == 1 || !s.isSelectionEmpty());
1056            Main.map.repaint();
1057        }
1058    }
1059
1060    /**
1061     * Action that issues a series of download requests to the API, following the GPX track.
1062     *
1063     * @author fred
1064     */
1065    public class DownloadAlongTrackAction extends AbstractAction {
1066        final static int NEAR_TRACK=0;
1067        final static int NEAR_WAYPOINTS=1;
1068        final static int NEAR_BOTH=2;
1069        final Integer dist[] = { 5000, 500, 50 };
1070        final Integer area[] = { 20, 10, 5, 1 };
1071
1072        public DownloadAlongTrackAction() {
1073            super(tr("Download from OSM along this track"), ImageProvider.get("downloadalongtrack"));
1074        }
1075
1076        @Override
1077        public void actionPerformed(ActionEvent e) {
1078            /*
1079             * build selection dialog
1080             */
1081            JPanel msg = new JPanel(new GridBagLayout());
1082
1083            msg.add(new JLabel(tr("Download everything within:")), GBC.eol());
1084            String s[] = new String[dist.length];
1085            for (int i = 0; i < dist.length; ++i) {
1086                s[i] = tr("{0} meters", dist[i]);
1087            }
1088            JList buffer = new JList(s);
1089            buffer.setSelectedIndex(Main.pref.getInteger(PREF_DOWNLOAD_ALONG_TRACK_DISTANCE, 0));
1090            msg.add(buffer, GBC.eol());
1091
1092            msg.add(new JLabel(tr("Maximum area per request:")), GBC.eol());
1093            s = new String[area.length];
1094            for (int i = 0; i < area.length; ++i) {
1095                s[i] = tr("{0} sq km", area[i]);
1096            }
1097            JList maxRect = new JList(s);
1098            maxRect.setSelectedIndex(Main.pref.getInteger(PREF_DOWNLOAD_ALONG_TRACK_AREA, 0));
1099            msg.add(maxRect, GBC.eol());
1100
1101            msg.add(new JLabel(tr("Download near:")), GBC.eol());
1102            JList downloadNear = new JList(new String[] { tr("track only"), tr("waypoints only"), tr("track and waypoints") });
1103
1104            downloadNear.setSelectedIndex(Main.pref.getInteger(PREF_DOWNLOAD_ALONG_TRACK_NEAR, 0));
1105            msg.add(downloadNear, GBC.eol());
1106
1107            int ret = JOptionPane.showConfirmDialog(
1108                    Main.parent,
1109                    msg,
1110                    tr("Download from OSM along this track"),
1111                    JOptionPane.OK_CANCEL_OPTION,
1112                    JOptionPane.QUESTION_MESSAGE
1113                    );
1114            switch(ret) {
1115            case JOptionPane.CANCEL_OPTION:
1116            case JOptionPane.CLOSED_OPTION:
1117                return;
1118            default:
1119                // continue
1120            }
1121
1122            Main.pref.putInteger(PREF_DOWNLOAD_ALONG_TRACK_DISTANCE, buffer.getSelectedIndex());
1123            Main.pref.putInteger(PREF_DOWNLOAD_ALONG_TRACK_AREA, maxRect.getSelectedIndex());
1124            final int near = downloadNear.getSelectedIndex();
1125            Main.pref.putInteger(PREF_DOWNLOAD_ALONG_TRACK_NEAR, near);
1126
1127            /*
1128             * Find the average latitude for the data we're contemplating, so we can know how many
1129             * metres per degree of longitude we have.
1130             */
1131            double latsum = 0;
1132            int latcnt = 0;
1133
1134            if (near == NEAR_TRACK || near == NEAR_BOTH) {
1135                for (GpxTrack trk : data.tracks) {
1136                    for (GpxTrackSegment segment : trk.getSegments()) {
1137                        for (WayPoint p : segment.getWayPoints()) {
1138                            latsum += p.getCoor().lat();
1139                            latcnt++;
1140                        }
1141                    }
1142                }
1143            }
1144
1145            if (near == NEAR_WAYPOINTS || near == NEAR_BOTH) {
1146                for (WayPoint p : data.waypoints) {
1147                    latsum += p.getCoor().lat();
1148                    latcnt++;
1149                }
1150            }
1151
1152            double avglat = latsum / latcnt;
1153            double scale = Math.cos(Math.toRadians(avglat));
1154
1155            /*
1156             * Compute buffer zone extents and maximum bounding box size. Note that the maximum we
1157             * ever offer is a bbox area of 0.002, while the API theoretically supports 0.25, but as
1158             * soon as you touch any built-up area, that kind of bounding box will download forever
1159             * and then stop because it has more than 50k nodes.
1160             */
1161            Integer i = buffer.getSelectedIndex();
1162            final int buffer_dist = dist[i < 0 ? 0 : i];
1163            i = maxRect.getSelectedIndex();
1164            final double max_area = area[i < 0 ? 0 : i] / 10000.0 / scale;
1165            final double buffer_y = buffer_dist / 100000.0;
1166            final double buffer_x = buffer_y / scale;
1167
1168            final int totalTicks = latcnt;
1169            // guess if a progress bar might be useful.
1170            final boolean displayProgress = totalTicks > 2000 && buffer_y < 0.01;
1171
1172            class CalculateDownloadArea extends PleaseWaitRunnable {
1173                private Area a = new Area();
1174                private boolean cancel = false;
1175                private int ticks = 0;
1176                private Rectangle2D r = new Rectangle2D.Double();
1177
1178                public CalculateDownloadArea() {
1179                    super(tr("Calculating Download Area"),
1180                            (displayProgress ? null : NullProgressMonitor.INSTANCE),
1181                            false);
1182                }
1183
1184                @Override
1185                protected void cancel() {
1186                    cancel = true;
1187                }
1188
1189                @Override
1190                protected void finish() {
1191                }
1192
1193                @Override
1194                protected void afterFinish() {
1195                    if(cancel)
1196                        return;
1197                    confirmAndDownloadAreas(a, max_area, progressMonitor);
1198                }
1199
1200                /**
1201                 * increase tick count by one, report progress every 100 ticks
1202                 */
1203                private void tick() {
1204                    ticks++;
1205                    if(ticks % 100 == 0) {
1206                        progressMonitor.worked(100);
1207                    }
1208                }
1209
1210                /**
1211                 * calculate area for single, given way point and return new LatLon if the
1212                 * way point has been used to modify the area.
1213                 */
1214                private LatLon calcAreaForWayPoint(WayPoint p, LatLon previous) {
1215                    tick();
1216                    LatLon c = p.getCoor();
1217                    if (previous == null || c.greatCircleDistance(previous) > buffer_dist) {
1218                        // we add a buffer around the point.
1219                        r.setRect(c.lon() - buffer_x, c.lat() - buffer_y, 2 * buffer_x, 2 * buffer_y);
1220                        a.add(new Area(r));
1221                        return c;
1222                    }
1223                    return previous;
1224                }
1225
1226                @Override
1227                protected void realRun() {
1228                    progressMonitor.setTicksCount(totalTicks);
1229                    /*
1230                     * Collect the combined area of all gpx points plus buffer zones around them. We ignore
1231                     * points that lie closer to the previous point than the given buffer size because
1232                     * otherwise this operation takes ages.
1233                     */
1234                    LatLon previous = null;
1235                    if (near == NEAR_TRACK || near == NEAR_BOTH) {
1236                        for (GpxTrack trk : data.tracks) {
1237                            for (GpxTrackSegment segment : trk.getSegments()) {
1238                                for (WayPoint p : segment.getWayPoints()) {
1239                                    if(cancel)
1240                                        return;
1241                                    previous = calcAreaForWayPoint(p, previous);
1242                                }
1243                            }
1244                        }
1245                    }
1246                    if (near == NEAR_WAYPOINTS || near == NEAR_BOTH) {
1247                        for (WayPoint p : data.waypoints) {
1248                            if(cancel)
1249                                return;
1250                            previous = calcAreaForWayPoint(p, previous);
1251                        }
1252                    }
1253                }
1254            }
1255
1256            Main.worker.submit(new CalculateDownloadArea());
1257        }
1258
1259
1260        /**
1261         * Area "a" contains the hull that we would like to download data for. however we
1262         * can only download rectangles, so the following is an attempt at finding a number of
1263         * rectangles to download.
1264         *
1265         * The idea is simply: Start out with the full bounding box. If it is too large, then
1266         * split it in half and repeat recursively for each half until you arrive at something
1267         * small enough to download. The algorithm is improved by always using the intersection
1268         * between the rectangle and the actual desired area. For example, if you have a track
1269         * that goes like this: +----+ | /| | / | | / | |/ | +----+ then we would first look at
1270         * downloading the whole rectangle (assume it's too big), after that we split it in half
1271         * (upper and lower half), but we donot request the full upper and lower rectangle, only
1272         * the part of the upper/lower rectangle that actually has something in it.
1273         *
1274         * This functions calculates the rectangles, asks the user to continue and downloads
1275         * the areas if applicable.
1276         */
1277        private void confirmAndDownloadAreas(Area a, double max_area, ProgressMonitor progressMonitor) {
1278            List<Rectangle2D> toDownload = new ArrayList<Rectangle2D>();
1279
1280            addToDownload(a, a.getBounds(), toDownload, max_area);
1281
1282            if(toDownload.size() == 0)
1283                return;
1284
1285            JPanel msg = new JPanel(new GridBagLayout());
1286
1287            msg.add(new JLabel(
1288                    tr("<html>This action will require {0} individual<br>"
1289                            + "download requests. Do you wish<br>to continue?</html>",
1290                            toDownload.size())), GBC.eol());
1291
1292            if (toDownload.size() > 1) {
1293                int ret = JOptionPane.showConfirmDialog(
1294                        Main.parent,
1295                        msg,
1296                        tr("Download from OSM along this track"),
1297                        JOptionPane.OK_CANCEL_OPTION,
1298                        JOptionPane.PLAIN_MESSAGE
1299                        );
1300                switch(ret) {
1301                case JOptionPane.CANCEL_OPTION:
1302                case JOptionPane.CLOSED_OPTION:
1303                    return;
1304                default:
1305                    // continue
1306                }
1307            }
1308            final PleaseWaitProgressMonitor monitor = new PleaseWaitProgressMonitor(tr("Download data"));
1309            final Future<?> future = new DownloadOsmTaskList().download(false, toDownload, monitor);
1310            Main.worker.submit(
1311                    new Runnable() {
1312                        @Override
1313                        public void run() {
1314                            try {
1315                                future.get();
1316                            } catch(Exception e) {
1317                                e.printStackTrace();
1318                                return;
1319                            }
1320                            monitor.close();
1321                        }
1322                    }
1323                    );
1324        }
1325    }
1326
1327
1328    public class DownloadWmsAlongTrackAction extends AbstractAction {
1329        public DownloadWmsAlongTrackAction() {
1330            super(tr("Precache imagery tiles along this track"), ImageProvider.get("downloadalongtrack"));
1331        }
1332
1333        public void actionPerformed(ActionEvent e) {
1334
1335            final List<LatLon> points = new ArrayList<LatLon>();
1336
1337            for (GpxTrack trk : data.tracks) {
1338                for (GpxTrackSegment segment : trk.getSegments()) {
1339                    for (WayPoint p : segment.getWayPoints()) {
1340                        points.add(p.getCoor());
1341                    }
1342                }
1343            }
1344            for (WayPoint p : data.waypoints) {
1345                points.add(p.getCoor());
1346            }
1347
1348
1349            final WMSLayer layer = askWMSLayer();
1350            if (layer != null) {
1351                PleaseWaitRunnable task = new PleaseWaitRunnable(tr("Precaching WMS")) {
1352
1353                    private PrecacheTask precacheTask;
1354
1355                    @Override
1356                    protected void realRun() throws SAXException, IOException, OsmTransferException {
1357                        precacheTask = new PrecacheTask(progressMonitor);
1358                        layer.downloadAreaToCache(precacheTask, points, 0, 0);
1359                        while (!precacheTask.isFinished() && !progressMonitor.isCanceled()) {
1360                            synchronized (this) {
1361                                try {
1362                                    wait(200);
1363                                } catch (InterruptedException e) {
1364                                    e.printStackTrace();
1365                                }
1366                            }
1367                        }
1368                    }
1369
1370                    @Override
1371                    protected void finish() {
1372                    }
1373
1374                    @Override
1375                    protected void cancel() {
1376                        precacheTask.cancel();
1377                    }
1378
1379                    @Override
1380                    public ProgressTaskId canRunInBackground() {
1381                        return ProgressTaskIds.PRECACHE_WMS;
1382                    }
1383                };
1384                Main.worker.execute(task);
1385            }
1386
1387
1388        }
1389
1390        protected WMSLayer askWMSLayer() {
1391            List<WMSLayer> targetLayers = Main.map.mapView.getLayersOfType(WMSLayer.class);
1392
1393            if (targetLayers.isEmpty()) {
1394                warnNoImageryLayers();
1395                return null;
1396            }
1397
1398            JComboBox layerList = new JComboBox();
1399            layerList.setRenderer(new LayerListCellRenderer());
1400            layerList.setModel(new DefaultComboBoxModel(targetLayers.toArray()));
1401            layerList.setSelectedIndex(0);
1402
1403            JPanel pnl = new JPanel();
1404            pnl.setLayout(new GridBagLayout());
1405            pnl.add(new JLabel(tr("Please select the imagery layer.")), GBC.eol());
1406            pnl.add(layerList, GBC.eol());
1407
1408            ExtendedDialog ed = new ExtendedDialog(Main.parent,
1409                    tr("Select imagery layer"),
1410                    new String[] { tr("Download"), tr("Cancel") });
1411            ed.setButtonIcons(new String[] { "dialogs/down", "cancel" });
1412            ed.setContent(pnl);
1413            ed.showDialog();
1414            if (ed.getValue() != 1)
1415                return null;
1416
1417            WMSLayer targetLayer = (WMSLayer) layerList.getSelectedItem();
1418            return targetLayer;
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            String dir = Main.pref.get("markers.lastaudiodirectory");
1912            JFileChooser fc = new JFileChooser(dir);
1913            fc.setFileSelectionMode(JFileChooser.FILES_ONLY);
1914            fc.setAcceptAllFileFilterUsed(false);
1915            fc.setFileFilter(new FileFilter() {
1916                @Override
1917                public boolean accept(File f) {
1918                    return f.isDirectory() || f.getName().toLowerCase().endsWith(".wav");
1919                }
1920
1921                @Override
1922                public String getDescription() {
1923                    return tr("Wave Audio files (*.wav)");
1924                }
1925            });
1926            fc.setMultiSelectionEnabled(true);
1927            if (fc.showOpenDialog(Main.parent) == JFileChooser.APPROVE_OPTION) {
1928                if (!fc.getCurrentDirectory().getAbsolutePath().equals(dir)) {
1929                    Main.pref.put("markers.lastaudiodirectory", fc.getCurrentDirectory().getAbsolutePath());
1930                }
1931
1932                File sel[] = fc.getSelectedFiles();
1933                // sort files in increasing order of timestamp (this is the end time, but so
1934                // long as they don't overlap, that's fine)
1935                if (sel.length > 1) {
1936                    Arrays.sort(sel, new Comparator<File>() {
1937                        @Override
1938                        public int compare(File a, File b) {
1939                            return a.lastModified() <= b.lastModified() ? -1 : 1;
1940                        }
1941                    });
1942                }
1943
1944                String names = null;
1945                for (int i = 0; i < sel.length; i++) {
1946                    if (names == null) {
1947                        names = " (";
1948                    } else {
1949                        names += ", ";
1950                    }
1951                    names += sel[i].getName();
1952                }
1953                if (names != null) {
1954                    names += ")";
1955                } else {
1956                    names = "";
1957                }
1958                MarkerLayer ml = new MarkerLayer(new GpxData(), tr("Audio markers from {0}", getName()) + names,
1959                        getAssociatedFile(), GpxLayer.this);
1960                double firstStartTime = sel[0].lastModified() / 1000.0 /* ms -> seconds */
1961                        - AudioUtil.getCalibratedDuration(sel[0]);
1962
1963                Markers m = new Markers();
1964                for (int i = 0; i < sel.length; i++) {
1965                    importAudio(sel[i], ml, firstStartTime, m);
1966                }
1967                Main.main.addLayer(ml);
1968                Main.map.repaint();
1969            }
1970
1971        }
1972    }
1973
1974    private class ImportImages extends AbstractAction {
1975
1976        public ImportImages() {
1977            super(tr("Import images"), ImageProvider.get("dialogs/geoimage"));
1978            putValue("help", ht("/Action/ImportImages"));
1979        }
1980
1981        private void warnCantImportIntoServerLayer(GpxLayer layer) {
1982            String msg = tr("<html>The data in the GPX layer ''{0}'' has been downloaded from the server.<br>"
1983                    + "Because its way points do not include a timestamp we cannot correlate them with images.</html>",
1984                    layer.getName()
1985                    );
1986            HelpAwareOptionPane.showOptionDialog(
1987                    Main.parent,
1988                    msg,
1989                    tr("Import not possible"),
1990                    JOptionPane.WARNING_MESSAGE,
1991                    ht("/Action/ImportImages#CantImportIntoGpxLayerFromServer")
1992                    );
1993        }
1994
1995        private void addRecursiveFiles(LinkedList<File> files, File[] sel) {
1996            for (File f : sel) {
1997                if (f.isDirectory()) {
1998                    addRecursiveFiles(files, f.listFiles());
1999                } else if (f.getName().toLowerCase().endsWith(".jpg")) {
2000                    files.add(f);
2001                }
2002            }
2003        }
2004
2005        @Override
2006        public void actionPerformed(ActionEvent e) {
2007
2008            if (GpxLayer.this.data.fromServer) {
2009                warnCantImportIntoServerLayer(GpxLayer.this);
2010                return;
2011            }
2012            String curDir = Main.pref.get("geoimage.lastdirectory", Main.pref.get("lastDirectory"));
2013            if (curDir.equals("")) {
2014                curDir = ".";
2015            }
2016            JFileChooser fc = new JFileChooser(new File(curDir));
2017
2018            fc.setFileSelectionMode(JFileChooser.FILES_AND_DIRECTORIES);
2019            fc.setMultiSelectionEnabled(true);
2020            fc.setAcceptAllFileFilterUsed(false);
2021            JpgImporter importer = new JpgImporter(GpxLayer.this);
2022            fc.setFileFilter(importer.filter);
2023            fc.showOpenDialog(Main.parent);
2024            LinkedList<File> files = new LinkedList<File>();
2025            File[] sel = fc.getSelectedFiles();
2026            if (sel == null || sel.length == 0)
2027                return;
2028            if (!fc.getCurrentDirectory().getAbsolutePath().equals(curDir)) {
2029                Main.pref.put("geoimage.lastdirectory", fc.getCurrentDirectory().getAbsolutePath());
2030            }
2031            addRecursiveFiles(files, sel);
2032            importer.importDataHandleExceptions(files, NullProgressMonitor.INSTANCE);
2033        }
2034    }
2035
2036    @Override
2037    public void projectionChanged(Projection oldValue, Projection newValue) {
2038        if (newValue == null) return;
2039        if (data.waypoints != null) {
2040            for (WayPoint wp : data.waypoints){
2041                wp.invalidateEastNorthCache();
2042            }
2043        }
2044        if (data.tracks != null){
2045            for (GpxTrack track: data.tracks) {
2046                for (GpxTrackSegment segment: track.getSegments()) {
2047                    for (WayPoint wp: segment.getWayPoints()) {
2048                        wp.invalidateEastNorthCache();
2049                    }
2050                }
2051            }
2052        }
2053        if (data.routes != null) {
2054            for (GpxRoute route: data.routes) {
2055                if (route.routePoints == null) {
2056                    continue;
2057                }
2058                for (WayPoint wp: route.routePoints) {
2059                    wp.invalidateEastNorthCache();
2060                }
2061            }
2062        }
2063    }
2064}
Note: See TracBrowser for help on using the repository browser.