001// License: GPL. See LICENSE file for details.
002
003package org.openstreetmap.josm.gui.layer;
004
005import static org.openstreetmap.josm.tools.I18n.marktr;
006import static org.openstreetmap.josm.tools.I18n.tr;
007import static org.openstreetmap.josm.tools.I18n.trn;
008
009import java.awt.BasicStroke;
010import java.awt.Color;
011import java.awt.Dimension;
012import java.awt.Graphics2D;
013import java.awt.Point;
014import java.awt.RenderingHints;
015import java.awt.Stroke;
016import java.io.File;
017import java.text.DateFormat;
018import java.util.ArrayList;
019import java.util.Collection;
020import java.util.Date;
021import java.util.LinkedList;
022import java.util.List;
023
024import javax.swing.Action;
025import javax.swing.Icon;
026import javax.swing.JScrollPane;
027import javax.swing.SwingUtilities;
028
029import org.openstreetmap.josm.Main;
030import org.openstreetmap.josm.actions.RenameLayerAction;
031import org.openstreetmap.josm.actions.SaveActionBase;
032import org.openstreetmap.josm.data.Bounds;
033import org.openstreetmap.josm.data.coor.LatLon;
034import org.openstreetmap.josm.data.gpx.GpxConstants;
035import org.openstreetmap.josm.data.gpx.GpxData;
036import org.openstreetmap.josm.data.gpx.GpxRoute;
037import org.openstreetmap.josm.data.gpx.GpxTrack;
038import org.openstreetmap.josm.data.gpx.GpxTrackSegment;
039import org.openstreetmap.josm.data.gpx.WayPoint;
040import org.openstreetmap.josm.data.osm.visitor.BoundingXYVisitor;
041import org.openstreetmap.josm.data.projection.Projection;
042import org.openstreetmap.josm.gui.MapView;
043import org.openstreetmap.josm.gui.NavigatableComponent;
044import org.openstreetmap.josm.gui.dialogs.LayerListDialog;
045import org.openstreetmap.josm.gui.dialogs.LayerListPopup;
046import org.openstreetmap.josm.gui.layer.gpx.ChooseTrackVisibilityAction;
047import org.openstreetmap.josm.gui.layer.gpx.ConvertToDataLayerAction;
048import org.openstreetmap.josm.gui.layer.gpx.CustomizeDrawingAction;
049import org.openstreetmap.josm.gui.layer.gpx.DownloadAlongTrackAction;
050import org.openstreetmap.josm.gui.layer.gpx.DownloadWmsAlongTrackAction;
051import org.openstreetmap.josm.gui.layer.gpx.ImportAudioAction;
052import org.openstreetmap.josm.gui.layer.gpx.ImportImagesAction;
053import org.openstreetmap.josm.gui.layer.gpx.MarkersFromNamedPointsAction;
054import org.openstreetmap.josm.gui.widgets.HtmlPanel;
055import org.openstreetmap.josm.io.GpxImporter;
056import org.openstreetmap.josm.tools.ImageProvider;
057import org.openstreetmap.josm.tools.Utils;
058
059public class GpxLayer extends Layer {
060
061    public GpxData data;
062    protected static final double PHI = Math.toRadians(15);
063    private boolean computeCacheInSync;
064    private int computeCacheMaxLineLengthUsed;
065    private Color computeCacheColorUsed;
066    private boolean computeCacheColorDynamic;
067    private colorModes computeCacheColored;
068    private int computeCacheColorTracksTune;
069    private boolean isLocalFile;
070    // used by ChooseTrackVisibilityAction to determine which tracks to show/hide
071    public boolean[] trackVisibility = new boolean[0];
072
073    private final List<GpxTrack> lastTracks = new ArrayList<GpxTrack>(); // List of tracks at last paint
074    private int lastUpdateCount;
075
076    public GpxLayer(GpxData d) {
077        super((String) d.attr.get("name"));
078        data = d;
079        computeCacheInSync = false;
080        ensureTrackVisibilityLength();
081    }
082
083    public GpxLayer(GpxData d, String name) {
084        this(d);
085        this.setName(name);
086    }
087
088    public GpxLayer(GpxData d, String name, boolean isLocal) {
089        this(d);
090        this.setName(name);
091        this.isLocalFile = isLocal;
092    }
093
094    /**
095     * returns minimum and maximum timestamps in the track
096     */
097    public static Date[] getMinMaxTimeForTrack(GpxTrack trk) {
098        WayPoint earliest = null, latest = null;
099
100        for (GpxTrackSegment seg : trk.getSegments()) {
101            for (WayPoint pnt : seg.getWayPoints()) {
102                if (latest == null) {
103                    latest = earliest = pnt;
104                } else {
105                    if (pnt.compareTo(earliest) < 0) {
106                        earliest = pnt;
107                    } else {
108                        latest = pnt;
109                    }
110                }
111            }
112        }
113        if (earliest==null || latest==null) return null;
114        return new Date[]{earliest.getTime(), latest.getTime()};
115    }
116
117    /**
118    * Returns minimum and maximum timestamps for all tracks
119    * Warning: there are lot of track with broken timestamps,
120    * so we just ingore points from future and from year before 1970 in this method
121    * works correctly @since 5815
122    */
123    public Date[] getMinMaxTimeForAllTracks() {
124        double min=1e100, max=-1e100, t;
125        double now = System.currentTimeMillis()/1000.0;
126        for (GpxTrack trk: data.tracks) {
127            for (GpxTrackSegment seg : trk.getSegments()) {
128                for (WayPoint pnt : seg.getWayPoints()) {
129                    t = pnt.time;
130                    if (t>0 && t<=now) {
131                        if (t>max) max=t;
132                        if (t<min) min=t;
133                    }
134                }
135            }
136        }
137        if (min==1e100 || max==-1e100) return null;
138        return new Date[]{new Date((long) (min * 1000)), new Date((long) (max * 1000)), };
139    }
140
141
142    /**
143     * returns a human readable string that shows the timespan of the given track
144     */
145    public static String getTimespanForTrack(GpxTrack trk) {
146        Date[] bounds = getMinMaxTimeForTrack(trk);
147        String ts = "";
148        if (bounds != null) {
149            DateFormat df = DateFormat.getDateInstance(DateFormat.SHORT);
150            String earliestDate = df.format(bounds[0]);
151            String latestDate = df.format(bounds[1]);
152
153            if (earliestDate.equals(latestDate)) {
154                DateFormat tf = DateFormat.getTimeInstance(DateFormat.SHORT);
155                ts += earliestDate + " ";
156                ts += tf.format(bounds[0]) + " - " + tf.format(bounds[1]);
157            } else {
158                DateFormat dtf = DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT);
159                ts += dtf.format(bounds[0]) + " - " + dtf.format(bounds[1]);
160            }
161
162            int diff = (int) (bounds[1].getTime() - bounds[0].getTime()) / 1000;
163            ts += String.format(" (%d:%02d)", diff / 3600, (diff % 3600) / 60);
164        }
165        return ts;
166    }
167
168    @Override
169    public Icon getIcon() {
170        return ImageProvider.get("layer", "gpx_small");
171    }
172
173    @Override
174    public Object getInfoComponent() {
175        StringBuilder info = new StringBuilder();
176
177        if (data.attr.containsKey("name")) {
178            info.append(tr("Name: {0}", data.attr.get(GpxConstants.META_NAME))).append("<br>");
179        }
180
181        if (data.attr.containsKey("desc")) {
182            info.append(tr("Description: {0}", data.attr.get(GpxConstants.META_DESC))).append("<br>");
183        }
184
185        if (!data.tracks.isEmpty()) {
186            info.append("<table><thead align='center'><tr><td colspan='5'>"
187                    + trn("{0} track", "{0} tracks", data.tracks.size(), data.tracks.size())
188                    + "</td></tr><tr align='center'><td>" + tr("Name") + "</td><td>"
189                    + tr("Description") + "</td><td>" + tr("Timespan")
190                    + "</td><td>" + tr("Length") + "</td><td>" + tr("URL")
191                    + "</td></tr></thead>");
192
193            for (GpxTrack trk : data.tracks) {
194                info.append("<tr><td>");
195                if (trk.getAttributes().containsKey("name")) {
196                    info.append(trk.getAttributes().get("name"));
197                }
198                info.append("</td><td>");
199                if (trk.getAttributes().containsKey("desc")) {
200                    info.append(" ").append(trk.getAttributes().get("desc"));
201                }
202                info.append("</td><td>");
203                info.append(getTimespanForTrack(trk));
204                info.append("</td><td>");
205                info.append(NavigatableComponent.getSystemOfMeasurement().getDistText(trk.length()));
206                info.append("</td><td>");
207                if (trk.getAttributes().containsKey("url")) {
208                    info.append(trk.getAttributes().get("url"));
209                }
210                info.append("</td></tr>");
211            }
212
213            info.append("</table><br><br>");
214
215        }
216
217        info.append(tr("Length: {0}", NavigatableComponent.getSystemOfMeasurement().getDistText(data.length()))).append("<br>");
218
219        info.append(trn("{0} route, ", "{0} routes, ", data.routes.size(), data.routes.size())).append(
220                trn("{0} waypoint", "{0} waypoints", data.waypoints.size(), data.waypoints.size())).append("<br>");
221
222        final JScrollPane sp = new JScrollPane(new HtmlPanel(info.toString()), JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED, JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED);
223        sp.setPreferredSize(new Dimension(sp.getPreferredSize().width, 350));
224        SwingUtilities.invokeLater(new Runnable() {
225            @Override
226            public void run() {
227                sp.getVerticalScrollBar().setValue(0);
228            }
229        });
230        return sp;
231    }
232
233    @Override
234    public Color getColor(boolean ignoreCustom) {
235        Color c = Main.pref.getColor(marktr("gps point"), "layer " + getName(), Color.gray);
236
237        return ignoreCustom || getColorMode() == colorModes.none ? c : null;
238    }
239
240    public colorModes getColorMode() {
241        try {
242            int i=Main.pref.getInteger("draw.rawgps.colors", "layer " + getName(), 0);
243            return colorModes.values()[i];
244        } catch (Exception e) {
245            Main.warn(e);
246        }
247        return colorModes.none;
248    }
249
250    /* for preferences */
251    static public Color getGenericColor() {
252        return Main.pref.getColor(marktr("gps point"), Color.gray);
253    }
254
255    @Override
256    public Action[] getMenuEntries() {
257        if (Main.applet) {
258            return new Action[] {
259                LayerListDialog.getInstance().createShowHideLayerAction(),
260                LayerListDialog.getInstance().createDeleteLayerAction(),
261                SeparatorLayerAction.INSTANCE,
262                new CustomizeColor(this),
263                new CustomizeDrawingAction(this),
264                new ConvertToDataLayerAction(this),
265                SeparatorLayerAction.INSTANCE,
266                new ChooseTrackVisibilityAction(this),
267                new RenameLayerAction(getAssociatedFile(), this),
268                SeparatorLayerAction.INSTANCE,
269                new LayerListPopup.InfoAction(this) };
270        }
271        return new Action[] {
272                LayerListDialog.getInstance().createShowHideLayerAction(),
273                LayerListDialog.getInstance().createDeleteLayerAction(),
274                SeparatorLayerAction.INSTANCE,
275                new LayerSaveAction(this),
276                new LayerSaveAsAction(this),
277                new CustomizeColor(this),
278                new CustomizeDrawingAction(this),
279                new ImportImagesAction(this),
280                new ImportAudioAction(this),
281                new MarkersFromNamedPointsAction(this),
282                new ConvertToDataLayerAction(this),
283                new DownloadAlongTrackAction(data),
284                new DownloadWmsAlongTrackAction(data),
285                SeparatorLayerAction.INSTANCE,
286                new ChooseTrackVisibilityAction(this),
287                new RenameLayerAction(getAssociatedFile(), this),
288                SeparatorLayerAction.INSTANCE,
289                new LayerListPopup.InfoAction(this) };
290    }
291
292    public boolean isLocalFile() {
293        return isLocalFile;
294    }
295
296    @Override
297    public String getToolTipText() {
298        StringBuilder info = new StringBuilder().append("<html>");
299
300        if (data.attr.containsKey("name")) {
301            info.append(tr("Name: {0}", data.attr.get(GpxConstants.META_NAME))).append("<br>");
302        }
303
304        if (data.attr.containsKey("desc")) {
305            info.append(tr("Description: {0}", data.attr.get(GpxConstants.META_DESC))).append("<br>");
306        }
307
308        info.append(trn("{0} track, ", "{0} tracks, ", data.tracks.size(), data.tracks.size()));
309        info.append(trn("{0} route, ", "{0} routes, ", data.routes.size(), data.routes.size()));
310        info.append(trn("{0} waypoint", "{0} waypoints", data.waypoints.size(), data.waypoints.size())).append("<br>");
311
312        info.append(tr("Length: {0}", NavigatableComponent.getSystemOfMeasurement().getDistText(data.length())));
313        info.append("<br>");
314
315        return info.append("</html>").toString();
316    }
317
318    @Override
319    public boolean isMergable(Layer other) {
320        return other instanceof GpxLayer;
321    }
322
323    private int sumUpdateCount() {
324        int updateCount = 0;
325        for (GpxTrack track: data.tracks) {
326            updateCount += track.getUpdateCount();
327        }
328        return updateCount;
329    }
330
331    @Override
332    public boolean isChanged() {
333        if (data.tracks.equals(lastTracks))
334            return sumUpdateCount() != lastUpdateCount;
335        else
336            return true;
337    }
338
339    public void filterTracksByDate(Date fromDate, Date toDate, boolean showWithoutDate) {
340        int i = 0;
341        long from = fromDate.getTime();
342        long to = toDate.getTime();
343        for (GpxTrack trk : data.tracks) {
344            Date[] t = GpxLayer.getMinMaxTimeForTrack(trk);
345
346            if (t==null) continue;
347            long tm = t[1].getTime();
348            trackVisibility[i]= (tm==0 && showWithoutDate) || (from<=tm && tm <= to);
349            i++;
350        }
351    }
352
353    @Override
354    public void mergeFrom(Layer from) {
355        data.mergeFrom(((GpxLayer) from).data);
356        computeCacheInSync = false;
357    }
358
359    private final static Color[] colors = new Color[256];
360    static {
361        for (int i = 0; i < colors.length; i++) {
362            colors[i] = Color.getHSBColor(i / 300.0f, 1, 1);
363        }
364    }
365
366    private final static Color[] colors_cyclic = new Color[256];
367    static {
368        for (int i = 0; i < colors_cyclic.length; i++) {
369            //                    red   yellow  green   blue    red
370            int[] h = new int[] { 0,    59,     127,    244,    360};
371            int[] s = new int[] { 100,  84,     99,     100 };
372            int[] b = new int[] { 90,   93,     74,     83 };
373
374            float angle = 4 - i / 256f * 4;
375            int quadrant = (int) angle;
376            angle -= quadrant;
377            quadrant = Utils.mod(quadrant+1, 4);
378
379            float vh = h[quadrant] * w(angle) + h[quadrant+1] * (1 - w(angle));
380            float vs = s[quadrant] * w(angle) + s[Utils.mod(quadrant+1, 4)] * (1 - w(angle));
381            float vb = b[quadrant] * w(angle) + b[Utils.mod(quadrant+1, 4)] * (1 - w(angle));
382
383            colors_cyclic[i] = Color.getHSBColor(vh/360f, vs/100f, vb/100f);
384        }
385    }
386
387    /**
388     * transition function:
389     *  w(0)=1, w(1)=0, 0<=w(x)<=1
390     * @param x number: 0<=x<=1
391     * @return the weighted value
392     */
393    private static float w(float x) {
394        if (x < 0.5)
395            return 1 - 2*x*x;
396        else
397            return 2*(1-x)*(1-x);
398    }
399
400    // lookup array to draw arrows without doing any math
401    private final static int ll0 = 9;
402    private final static int sl4 = 5;
403    private final static int sl9 = 3;
404    private final static int[][] dir = { { +sl4, +ll0, +ll0, +sl4 }, { -sl9, +ll0, +sl9, +ll0 }, { -ll0, +sl4, -sl4, +ll0 },
405        { -ll0, -sl9, -ll0, +sl9 }, { -sl4, -ll0, -ll0, -sl4 }, { +sl9, -ll0, -sl9, -ll0 },
406        { +ll0, -sl4, +sl4, -ll0 }, { +ll0, +sl9, +ll0, -sl9 }, { +sl4, +ll0, +ll0, +sl4 },
407        { -sl9, +ll0, +sl9, +ll0 }, { -ll0, +sl4, -sl4, +ll0 }, { -ll0, -sl9, -ll0, +sl9 } };
408
409    // the different color modes
410    enum colorModes {
411        none, velocity, dilution, direction, time
412    }
413
414    @Override
415    public void paint(Graphics2D g, MapView mv, Bounds box) {
416        lastUpdateCount = sumUpdateCount();
417        lastTracks.clear();
418        lastTracks.addAll(data.tracks);
419
420        g.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
421                Main.pref.getBoolean("mappaint.gpx.use-antialiasing", false) ?
422                        RenderingHints.VALUE_ANTIALIAS_ON : RenderingHints.VALUE_ANTIALIAS_OFF);
423
424        /****************************************************************
425         ********** STEP 1 - GET CONFIG VALUES **************************
426         ****************************************************************/
427        Color neutralColor = getColor(true);
428        String spec="layer "+getName();
429
430        // also draw lines between points belonging to different segments
431        boolean forceLines = Main.pref.getBoolean("draw.rawgps.lines.force", spec, false);
432        // draw direction arrows on the lines
433        boolean direction = Main.pref.getBoolean("draw.rawgps.direction", spec, false);
434        // don't draw lines if longer than x meters
435        int lineWidth = Main.pref.getInteger("draw.rawgps.linewidth", spec, 0);
436
437        int maxLineLength;
438        boolean lines;
439        if (!this.data.fromServer) {
440            maxLineLength = Main.pref.getInteger("draw.rawgps.max-line-length.local", spec, -1);
441            lines = Main.pref.getBoolean("draw.rawgps.lines.local", spec, true);
442        } else {
443            maxLineLength = Main.pref.getInteger("draw.rawgps.max-line-length", spec, 200);
444            lines = Main.pref.getBoolean("draw.rawgps.lines", spec, true);
445        }
446        // paint large dots for points
447        boolean large = Main.pref.getBoolean("draw.rawgps.large", spec, false);
448        int largesize = Main.pref.getInteger("draw.rawgps.large.size", spec, 3);
449        boolean hdopcircle = Main.pref.getBoolean("draw.rawgps.hdopcircle", spec, false);
450        // color the lines
451        colorModes colored = getColorMode();
452        // paint direction arrow with alternate math. may be faster
453        boolean alternatedirection = Main.pref.getBoolean("draw.rawgps.alternatedirection", spec, false);
454        // don't draw arrows nearer to each other than this
455        int delta = Main.pref.getInteger("draw.rawgps.min-arrow-distance", spec, 40);
456        // allows to tweak line coloring for different speed levels.
457        int colorTracksTune = Main.pref.getInteger("draw.rawgps.colorTracksTune", spec, 45);
458        boolean colorModeDynamic = Main.pref.getBoolean("draw.rawgps.colors.dynamic", spec, false);
459        int hdopfactor = Main.pref.getInteger("hdop.factor", 25);
460
461        Stroke storedStroke = g.getStroke();
462        if(lineWidth != 0)
463        {
464            g.setStroke(new BasicStroke(lineWidth,BasicStroke.CAP_ROUND,BasicStroke.JOIN_ROUND));
465            largesize += lineWidth;
466        }
467
468        /****************************************************************
469         ********** STEP 2a - CHECK CACHE VALIDITY **********************
470         ****************************************************************/
471        if ((computeCacheMaxLineLengthUsed != maxLineLength) || (!neutralColor.equals(computeCacheColorUsed))
472                || (computeCacheColored != colored) || (computeCacheColorTracksTune != colorTracksTune)
473                || (computeCacheColorDynamic != colorModeDynamic)) {
474            computeCacheMaxLineLengthUsed = maxLineLength;
475            computeCacheInSync = false;
476            computeCacheColorUsed = neutralColor;
477            computeCacheColored = colored;
478            computeCacheColorTracksTune = colorTracksTune;
479            computeCacheColorDynamic = colorModeDynamic;
480        }
481
482        /****************************************************************
483         ********** STEP 2b - RE-COMPUTE CACHE DATA *********************
484         ****************************************************************/
485        if (!computeCacheInSync) { // don't compute if the cache is good
486            double minval = +1e10;
487            double maxval = -1e10;
488            WayPoint oldWp = null;
489            if (colorModeDynamic) {
490                if (colored == colorModes.velocity) {
491                    for (Collection<WayPoint> segment : data.getLinesIterable(null)) {
492                        if(!forceLines) {
493                            oldWp = null;
494                        }
495                        for (WayPoint trkPnt : segment) {
496                            LatLon c = trkPnt.getCoor();
497                            if (Double.isNaN(c.lat()) || Double.isNaN(c.lon())) {
498                                continue;
499                            }
500                            if (oldWp != null && trkPnt.time > oldWp.time) {
501                                double vel = c.greatCircleDistance(oldWp.getCoor())
502                                        / (trkPnt.time - oldWp.time);
503                                if(vel > maxval) {
504                                    maxval = vel;
505                                }
506                                if(vel < minval) {
507                                    minval = vel;
508                                }
509                            }
510                            oldWp = trkPnt;
511                        }
512                    }
513                } else if (colored == colorModes.dilution) {
514                    for (Collection<WayPoint> segment : data.getLinesIterable(null)) {
515                        for (WayPoint trkPnt : segment) {
516                            Object val = trkPnt.attr.get("hdop");
517                            if (val != null) {
518                                double hdop = ((Float) val).doubleValue();
519                                if(hdop > maxval) {
520                                    maxval = hdop;
521                                }
522                                if(hdop < minval) {
523                                    minval = hdop;
524                                }
525                            }
526                        }
527                    }
528                }
529                oldWp = null;
530            }
531            double now = System.currentTimeMillis()/1000.0;
532            if (colored == colorModes.time) {
533                Date[] bounds = getMinMaxTimeForAllTracks();
534                if (bounds!=null) {
535                    minval = bounds[0].getTime()/1000.0;
536                    maxval = bounds[1].getTime()/1000.0;
537                } else {
538                    minval = 0; maxval=now;
539                }
540            }
541
542            for (Collection<WayPoint> segment : data.getLinesIterable(null)) {
543                if (!forceLines) { // don't draw lines between segments, unless forced to
544                    oldWp = null;
545                }
546                for (WayPoint trkPnt : segment) {
547                    LatLon c = trkPnt.getCoor();
548                    if (Double.isNaN(c.lat()) || Double.isNaN(c.lon())) {
549                        continue;
550                    }
551                    trkPnt.customColoring = neutralColor;
552                    if(colored == colorModes.dilution && trkPnt.attr.get("hdop") != null) {
553                        float hdop = ((Float) trkPnt.attr.get("hdop")).floatValue();
554                        int hdoplvl =(int) Math.round(colorModeDynamic ? ((hdop-minval)*255/(maxval-minval))
555                                : (hdop <= 0 ? 0 : hdop * hdopfactor));
556                        // High hdop is bad, but high values in colors are green.
557                        // Therefore inverse the logic
558                        int hdopcolor = 255 - (hdoplvl > 255 ? 255 : hdoplvl);
559                        trkPnt.customColoring = colors[hdopcolor];
560                    }
561                    if (oldWp != null) {
562                        double dist = c.greatCircleDistance(oldWp.getCoor());
563                        boolean noDraw=false;
564                        switch (colored) {
565                        case velocity:
566                            double dtime = trkPnt.time - oldWp.time;
567                            if(dtime > 0) {
568                                float vel = (float) (dist / dtime);
569                                int velColor =(int) Math.round(colorModeDynamic ? ((vel-minval)*255/(maxval-minval))
570                                        : (vel <= 0 ? 0 : vel / colorTracksTune * 255));
571                                trkPnt.customColoring = colors[Math.max(0, Math.min(velColor, 255))];
572                            } else {
573                                trkPnt.customColoring = colors[255];
574                            }
575                            break;
576                        case direction:
577                            double dirColor = oldWp.getCoor().heading(trkPnt.getCoor()) / (2.0 * Math.PI) * 256;
578                            // Bad case first
579                            if (dirColor != dirColor || dirColor < 0.0 || dirColor >= 256.0) {
580                                trkPnt.customColoring = colors_cyclic[0];
581                            } else {
582                                trkPnt.customColoring = colors_cyclic[(int) (dirColor)];
583                            }
584                            break;
585                        case time:
586                            double t=trkPnt.time;
587                            if (t>0 && t<=now){ // skip bad timestamps
588                                int tColor = (int) Math.round((t-minval)*255/(maxval-minval));
589                                trkPnt.customColoring = colors[tColor];
590                            } else {
591                                trkPnt.customColoring = neutralColor;
592                            }
593                            break;
594                        }
595
596                        if (!noDraw && (maxLineLength == -1 || dist <= maxLineLength)) {
597                            trkPnt.drawLine = true;
598                            trkPnt.dir = (int) oldWp.getCoor().heading(trkPnt.getCoor());
599                        } else {
600                            trkPnt.drawLine = false;
601                        }
602                    } else { // make sure we reset outdated data
603                        trkPnt.drawLine = false;
604                    }
605                    oldWp = trkPnt;
606                }
607            }
608            computeCacheInSync = true;
609        }
610
611        LinkedList<WayPoint> visibleSegments = new LinkedList<WayPoint>();
612        WayPoint last = null;
613        ensureTrackVisibilityLength();
614        for (Collection<WayPoint> segment : data.getLinesIterable(trackVisibility)) {
615
616            for(WayPoint pt : segment)
617            {
618                Bounds b = new Bounds(pt.getCoor());
619                // last should never be null when this is true!
620                if(pt.drawLine) {
621                    b.extend(last.getCoor());
622                }
623                if(b.intersects(box))
624                {
625                    if(last != null && (visibleSegments.isEmpty()
626                            || visibleSegments.getLast() != last)) {
627                        if(last.drawLine) {
628                            WayPoint l = new WayPoint(last);
629                            l.drawLine = false;
630                            visibleSegments.add(l);
631                        } else {
632                            visibleSegments.add(last);
633                        }
634                    }
635                    visibleSegments.add(pt);
636                }
637                last = pt;
638            }
639        }
640        if(visibleSegments.isEmpty())
641            return;
642
643        /****************************************************************
644         ********** STEP 3a - DRAW LINES ********************************
645         ****************************************************************/
646        if (lines) {
647            Point old = null;
648            for (WayPoint trkPnt : visibleSegments) {
649                LatLon c = trkPnt.getCoor();
650                if (Double.isNaN(c.lat()) || Double.isNaN(c.lon())) {
651                    continue;
652                }
653                Point screen = mv.getPoint(trkPnt.getEastNorth());
654                if (trkPnt.drawLine) {
655                    // skip points that are on the same screenposition
656                    if (old != null && ((old.x != screen.x) || (old.y != screen.y))) {
657                        g.setColor(trkPnt.customColoring);
658                        g.drawLine(old.x, old.y, screen.x, screen.y);
659                    }
660                }
661                old = screen;
662            } // end for trkpnt
663        } // end if lines
664
665        /****************************************************************
666         ********** STEP 3b - DRAW NICE ARROWS **************************
667         ****************************************************************/
668        if (lines && direction && !alternatedirection) {
669            Point old = null;
670            Point oldA = null; // last arrow painted
671            for (WayPoint trkPnt : visibleSegments) {
672                LatLon c = trkPnt.getCoor();
673                if (Double.isNaN(c.lat()) || Double.isNaN(c.lon())) {
674                    continue;
675                }
676                if (trkPnt.drawLine) {
677                    Point screen = mv.getPoint(trkPnt.getEastNorth());
678                    // skip points that are on the same screenposition
679                    if (old != null
680                            && (oldA == null || screen.x < oldA.x - delta || screen.x > oldA.x + delta
681                            || screen.y < oldA.y - delta || screen.y > oldA.y + delta)) {
682                        g.setColor(trkPnt.customColoring);
683                        double t = Math.atan2(screen.y - old.y, screen.x - old.x) + Math.PI;
684                        g.drawLine(screen.x, screen.y, (int) (screen.x + 10 * Math.cos(t - PHI)),
685                                (int) (screen.y + 10 * Math.sin(t - PHI)));
686                        g.drawLine(screen.x, screen.y, (int) (screen.x + 10 * Math.cos(t + PHI)),
687                                (int) (screen.y + 10 * Math.sin(t + PHI)));
688                        oldA = screen;
689                    }
690                    old = screen;
691                }
692            } // end for trkpnt
693        } // end if lines
694
695        /****************************************************************
696         ********** STEP 3c - DRAW FAST ARROWS **************************
697         ****************************************************************/
698        if (lines && direction && alternatedirection) {
699            Point old = null;
700            Point oldA = null; // last arrow painted
701            for (WayPoint trkPnt : visibleSegments) {
702                LatLon c = trkPnt.getCoor();
703                if (Double.isNaN(c.lat()) || Double.isNaN(c.lon())) {
704                    continue;
705                }
706                if (trkPnt.drawLine) {
707                    Point screen = mv.getPoint(trkPnt.getEastNorth());
708                    // skip points that are on the same screenposition
709                    if (old != null
710                            && (oldA == null || screen.x < oldA.x - delta || screen.x > oldA.x + delta
711                            || screen.y < oldA.y - delta || screen.y > oldA.y + delta)) {
712                        g.setColor(trkPnt.customColoring);
713                        g.drawLine(screen.x, screen.y, screen.x + dir[trkPnt.dir][0], screen.y
714                                + dir[trkPnt.dir][1]);
715                        g.drawLine(screen.x, screen.y, screen.x + dir[trkPnt.dir][2], screen.y
716                                + dir[trkPnt.dir][3]);
717                        oldA = screen;
718                    }
719                    old = screen;
720                }
721            } // end for trkpnt
722        } // end if lines
723
724        /****************************************************************
725         ********** STEP 3d - DRAW LARGE POINTS AND HDOP CIRCLE *********
726         ****************************************************************/
727        if (large || hdopcircle) {
728            g.setColor(neutralColor);
729            for (WayPoint trkPnt : visibleSegments) {
730                LatLon c = trkPnt.getCoor();
731                if (Double.isNaN(c.lat()) || Double.isNaN(c.lon())) {
732                    continue;
733                }
734                Point screen = mv.getPoint(trkPnt.getEastNorth());
735                g.setColor(trkPnt.customColoring);
736                if (hdopcircle && trkPnt.attr.get("hdop") != null) {
737                    // hdop value
738                    float hdop = ((Float)trkPnt.attr.get("hdop")).floatValue();
739                    if (hdop < 0) {
740                        hdop = 0;
741                    }
742                    // hdop pixels
743                    int hdopp = mv.getPoint(new LatLon(trkPnt.getCoor().lat(), trkPnt.getCoor().lon() + 2*6*hdop*360/40000000)).x - screen.x;
744                    g.drawArc(screen.x-hdopp/2, screen.y-hdopp/2, hdopp, hdopp, 0, 360);
745                }
746                if (large) {
747                    g.fillRect(screen.x-1, screen.y-1, largesize, largesize);
748                }
749            } // end for trkpnt
750        } // end if large || hdopcircle
751
752        /****************************************************************
753         ********** STEP 3e - DRAW SMALL POINTS FOR LINES ***************
754         ****************************************************************/
755        if (!large && lines) {
756            g.setColor(neutralColor);
757            for (WayPoint trkPnt : visibleSegments) {
758                LatLon c = trkPnt.getCoor();
759                if (Double.isNaN(c.lat()) || Double.isNaN(c.lon())) {
760                    continue;
761                }
762                if (!trkPnt.drawLine) {
763                    Point screen = mv.getPoint(trkPnt.getEastNorth());
764                    g.drawRect(screen.x, screen.y, 0, 0);
765                }
766            } // end for trkpnt
767        } // end if large
768
769        /****************************************************************
770         ********** STEP 3f - DRAW SMALL POINTS INSTEAD OF LINES ********
771         ****************************************************************/
772        if (!large && !lines) {
773            g.setColor(neutralColor);
774            for (WayPoint trkPnt : visibleSegments) {
775                LatLon c = trkPnt.getCoor();
776                if (Double.isNaN(c.lat()) || Double.isNaN(c.lon())) {
777                    continue;
778                }
779                Point screen = mv.getPoint(trkPnt.getEastNorth());
780                g.setColor(trkPnt.customColoring);
781                g.drawRect(screen.x, screen.y, 0, 0);
782            } // end for trkpnt
783        } // end if large
784
785        if(lineWidth != 0)
786        {
787            g.setStroke(storedStroke);
788        }
789    } // end paint
790
791    @Override
792    public void visitBoundingBox(BoundingXYVisitor v) {
793        v.visit(data.recalculateBounds());
794    }
795
796    @Override
797    public File getAssociatedFile() {
798        return data.storageFile;
799    }
800
801    @Override
802    public void setAssociatedFile(File file) {
803        data.storageFile = file;
804    }
805
806    /** ensures the trackVisibility array has the correct length without losing data.
807     * additional entries are initialized to true;
808     */
809    final private void ensureTrackVisibilityLength() {
810        final int l = data.tracks.size();
811        if(l == trackVisibility.length)
812            return;
813        final boolean[] back = trackVisibility.clone();
814        final int m = Math.min(l, back.length);
815        trackVisibility = new boolean[l];
816        System.arraycopy(back, 0, trackVisibility, 0, m);
817        for(int i=m; i < l; i++) {
818            trackVisibility[i] = true;
819        }
820    }
821
822    @Override
823    public void projectionChanged(Projection oldValue, Projection newValue) {
824        if (newValue == null) return;
825        if (data.waypoints != null) {
826            for (WayPoint wp : data.waypoints){
827                wp.invalidateEastNorthCache();
828            }
829        }
830        if (data.tracks != null){
831            for (GpxTrack track: data.tracks) {
832                for (GpxTrackSegment segment: track.getSegments()) {
833                    for (WayPoint wp: segment.getWayPoints()) {
834                        wp.invalidateEastNorthCache();
835                    }
836                }
837            }
838        }
839        if (data.routes != null) {
840            for (GpxRoute route: data.routes) {
841                if (route.routePoints == null) {
842                    continue;
843                }
844                for (WayPoint wp: route.routePoints) {
845                    wp.invalidateEastNorthCache();
846                }
847            }
848        }
849    }
850
851    @Override
852    public boolean isSavable() {
853        return true; // With GpxExporter
854    }
855
856    @Override
857    public boolean checkSaveConditions() {
858        return data != null;
859    }
860
861    @Override
862    public File createAndOpenSaveFileChooser() {
863        return SaveActionBase.createAndOpenSaveFileChooser(tr("Save GPX file"), GpxImporter.FILE_FILTER);
864    }
865
866}