001// License: GPL. See LICENSE file for details.
002package org.openstreetmap.josm.gui.layer.gpx;
003
004import static org.openstreetmap.josm.tools.I18n.marktr;
005import static org.openstreetmap.josm.tools.I18n.tr;
006
007import java.awt.BasicStroke;
008import java.awt.Color;
009import java.awt.Graphics2D;
010import java.awt.Point;
011import java.awt.RenderingHints;
012import java.awt.Stroke;
013import java.util.Collection;
014import java.util.Date;
015import java.util.List;
016
017import org.openstreetmap.josm.Main;
018import org.openstreetmap.josm.data.coor.LatLon;
019import org.openstreetmap.josm.data.gpx.GpxData;
020import org.openstreetmap.josm.data.gpx.WayPoint;
021import org.openstreetmap.josm.gui.MapView;
022import org.openstreetmap.josm.tools.ColorScale;
023
024/**
025 * Class that helps to draw large set of GPS tracks with different colors and options
026 * @since 7319
027 */
028public class GpxDrawHelper {
029    private GpxData data;
030
031    // draw lines between points belonging to different segments
032    private boolean forceLines;
033    // draw direction arrows on the lines
034    private boolean direction;
035    /** don't draw lines if longer than x meters **/
036    private int lineWidth;
037    private int maxLineLength;
038    private boolean lines;
039    /** paint large dots for points **/
040    private boolean large;
041    private int largesize;
042    private boolean hdopCircle;
043    /** paint direction arrow with alternate math. may be faster **/
044    private boolean alternateDirection;
045    /** don't draw arrows nearer to each other than this **/
046    private int delta;
047    private double minTrackDurationForTimeColoring;
048
049    private int hdopfactor;
050
051    private static final double PHI = Math.toRadians(15);
052
053    //// Variables used only to check cache validity
054    private boolean computeCacheInSync = false;
055    private int computeCacheMaxLineLengthUsed;
056    private Color computeCacheColorUsed;
057    private boolean computeCacheColorDynamic;
058    private ColorMode computeCacheColored;
059    private int computeCacheColorTracksTune;
060
061    //// Color-related fields
062    /** Mode of the line coloring **/
063    private ColorMode colored;
064    /** max speed for coloring - allows to tweak line coloring for different speed levels. **/
065    private int colorTracksTune;
066    private boolean colorModeDynamic;
067    private Color neutralColor;
068    private int largePointAlpha;
069
070    // default access is used to allow changing from plugins
071    ColorScale velocityScale;
072    /** Colors (without custom alpha channel, if given) for HDOP painting. **/
073    ColorScale hdopScale;
074    ColorScale dateScale;
075    ColorScale directionScale;
076
077    /** Opacity for hdop points **/
078    private int hdopAlpha;
079
080
081    // lookup array to draw arrows without doing any math
082    private static final int ll0 = 9;
083    private static final int sl4 = 5;
084    private static final int sl9 = 3;
085    private static final int[][] dir = { { +sl4, +ll0, +ll0, +sl4 }, { -sl9, +ll0, +sl9, +ll0 }, { -ll0, +sl4, -sl4, +ll0 },
086        { -ll0, -sl9, -ll0, +sl9 }, { -sl4, -ll0, -ll0, -sl4 }, { +sl9, -ll0, -sl9, -ll0 },
087        { +ll0, -sl4, +sl4, -ll0 }, { +ll0, +sl9, +ll0, -sl9 }, { +sl4, +ll0, +ll0, +sl4 },
088        { -sl9, +ll0, +sl9, +ll0 }, { -ll0, +sl4, -sl4, +ll0 }, { -ll0, -sl9, -ll0, +sl9 } };
089
090    private void setupColors() {
091        hdopAlpha = Main.pref.getInteger("hdop.color.alpha", -1);
092        velocityScale = ColorScale.createHSBScale(256).addTitle(tr("Velocity, km/h"));
093        /** Colors (without custom alpha channel, if given) for HDOP painting. **/
094        hdopScale = ColorScale.createHSBScale(256).makeReversed().addTitle(tr("HDOP, m"));
095        dateScale = ColorScale.createHSBScale(256).addTitle(tr("Time"));
096        directionScale = ColorScale.createCyclicScale(256).setIntervalCount(4).addTitle(tr("Direction"));
097    }
098
099    /**
100     * Different color modes
101     */
102    public enum ColorMode {
103        NONE, VELOCITY, HDOP, DIRECTION, TIME
104    }
105
106    /**
107     * Constructs a new {@code GpxDrawHelper}.
108     * @param gpxData GPX data
109     */
110    public GpxDrawHelper(GpxData gpxData) {
111        data = gpxData;
112        setupColors();
113    }
114
115    private static String specName(String layerName) {
116        return "layer " + layerName;
117    }
118
119    /**
120     * Get the default color for gps tracks for specified layer
121     * @param layerName name of the GpxLayer
122     * @param ignoreCustom do not use preferences
123     * @return the color or null if the color is not constant
124     */
125    public Color getColor(String layerName, boolean ignoreCustom) {
126        Color c = Main.pref.getColor(marktr("gps point"), specName(layerName), Color.gray);
127        return ignoreCustom || getColorMode(layerName) == ColorMode.NONE ? c : null;
128    }
129
130    /**
131     * Read coloring mode for specified layer from preferences
132     * @param layerName name of the GpxLayer
133     * @return coloting mode
134     */
135    public ColorMode getColorMode(String layerName) {
136        try {
137            int i = Main.pref.getInteger("draw.rawgps.colors", specName(layerName), 0);
138            return ColorMode.values()[i];
139        } catch (Exception e) {
140            Main.warn(e);
141        }
142        return ColorMode.NONE;
143    }
144
145    /** Reads generic color from preferences (usually gray)
146     * @return the color
147     **/
148    public static Color getGenericColor() {
149        return Main.pref.getColor(marktr("gps point"), Color.gray);
150    }
151
152    /**
153     * Read all drawing-related settings from preferences
154     * @param layerName layer name used to access its specific preferences
155     **/
156    public void readPreferences(String layerName) {
157        String spec = specName(layerName);
158        forceLines = Main.pref.getBoolean("draw.rawgps.lines.force", spec, false);
159        direction = Main.pref.getBoolean("draw.rawgps.direction", spec, false);
160        lineWidth = Main.pref.getInteger("draw.rawgps.linewidth", spec, 0);
161
162        if (!data.fromServer) {
163            maxLineLength = Main.pref.getInteger("draw.rawgps.max-line-length.local", spec, -1);
164            lines = Main.pref.getBoolean("draw.rawgps.lines.local", spec, true);
165        } else {
166            maxLineLength = Main.pref.getInteger("draw.rawgps.max-line-length", spec, 200);
167            lines = Main.pref.getBoolean("draw.rawgps.lines", spec, true);
168        }
169        large = Main.pref.getBoolean("draw.rawgps.large", spec, false);
170        largesize = Main.pref.getInteger("draw.rawgps.large.size", spec, 3);
171        hdopCircle = Main.pref.getBoolean("draw.rawgps.hdopcircle", spec, false);
172        colored = getColorMode(layerName);
173        alternateDirection = Main.pref.getBoolean("draw.rawgps.alternatedirection", spec, false);
174        delta = Main.pref.getInteger("draw.rawgps.min-arrow-distance", spec, 40);
175        colorTracksTune = Main.pref.getInteger("draw.rawgps.colorTracksTune", spec, 45);
176        colorModeDynamic = Main.pref.getBoolean("draw.rawgps.colors.dynamic", spec, false);
177        hdopfactor = Main.pref.getInteger("hdop.factor", 25);
178        minTrackDurationForTimeColoring = Main.pref.getInteger("draw.rawgps.date-coloring-min-dt", 60);
179        largePointAlpha = Main.pref.getInteger("draw.rawgps.large.alpha", -1) & 0xFF;
180
181        neutralColor = getColor(layerName, true);
182        velocityScale.setNoDataColor(neutralColor);
183        dateScale.setNoDataColor(neutralColor);
184        hdopScale.setNoDataColor(neutralColor);
185        directionScale.setNoDataColor(neutralColor);
186
187        largesize += lineWidth;
188    }
189
190
191    public void drawAll(Graphics2D g, MapView mv, List<WayPoint> visibleSegments) {
192
193        checkCache();
194
195        // STEP 2b - RE-COMPUTE CACHE DATA *********************
196        if (!computeCacheInSync) { // don't compute if the cache is good
197            calculateColors();
198        }
199
200        Stroke storedStroke = g.getStroke();
201
202        g.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
203            Main.pref.getBoolean("mappaint.gpx.use-antialiasing", false) ?
204                    RenderingHints.VALUE_ANTIALIAS_ON : RenderingHints.VALUE_ANTIALIAS_OFF);
205
206        if (lineWidth != 0) {
207            g.setStroke(new BasicStroke(lineWidth,BasicStroke.CAP_ROUND,BasicStroke.JOIN_ROUND));
208        }
209        fixColors(visibleSegments);
210        drawLines(g, mv, visibleSegments);
211        drawArrows(g, mv, visibleSegments);
212        drawPoints(g, mv, visibleSegments);
213        if (lineWidth != 0) {
214            g.setStroke(storedStroke);
215        }
216    }
217
218    public void calculateColors() {
219        double minval = +1e10;
220        double maxval = -1e10;
221        WayPoint oldWp = null;
222
223        if (colorModeDynamic) {
224            if (colored == ColorMode.VELOCITY) {
225                for (Collection<WayPoint> segment : data.getLinesIterable(null)) {
226                    if(!forceLines) {
227                        oldWp = null;
228                    }
229                    for (WayPoint trkPnt : segment) {
230                        LatLon c = trkPnt.getCoor();
231                        if (Double.isNaN(c.lat()) || Double.isNaN(c.lon())) {
232                            continue;
233                        }
234                        if (oldWp != null && trkPnt.time > oldWp.time) {
235                            double vel = c.greatCircleDistance(oldWp.getCoor())
236                                    / (trkPnt.time - oldWp.time);
237                            if(vel > maxval) {
238                                maxval = vel;
239                            }
240                            if(vel < minval) {
241                                minval = vel;
242                            }
243                        }
244                        oldWp = trkPnt;
245                    }
246                }
247                if (minval >= maxval) {
248                    velocityScale.setRange(0, 120/3.6);
249                } else {
250                    velocityScale.setRange(minval, maxval);
251                }
252            } else if (colored == ColorMode.HDOP) {
253                for (Collection<WayPoint> segment : data.getLinesIterable(null)) {
254                    for (WayPoint trkPnt : segment) {
255                        Object val = trkPnt.attr.get("hdop");
256                        if (val != null) {
257                            double hdop = ((Float) val).doubleValue();
258                            if(hdop > maxval) {
259                                maxval = hdop;
260                            }
261                            if(hdop < minval) {
262                                minval = hdop;
263                            }
264                        }
265                    }
266                }
267                if (minval >= maxval) {
268                    hdopScale.setRange(0, 100);
269                } else {
270                    hdopScale.setRange(minval, maxval);
271                }
272            }
273            oldWp = null;
274        } else { // color mode not dynamic
275            velocityScale.setRange(0, colorTracksTune);
276            hdopScale.setRange(0, 1.0/hdopfactor);
277        }
278        double now = System.currentTimeMillis()/1000.0;
279        if (colored == ColorMode.TIME) {
280            Date[] bounds = data.getMinMaxTimeForAllTracks();
281            if (bounds!=null) {
282                minval = bounds[0].getTime()/1000.0;
283                maxval = bounds[1].getTime()/1000.0;
284            } else {
285                minval = 0; maxval=now;
286            }
287            dateScale.setRange(minval, maxval);
288        }
289
290
291        // Now the colors for all the points will be assigned
292        for (Collection<WayPoint> segment : data.getLinesIterable(null)) {
293            if (!forceLines) { // don't draw lines between segments, unless forced to
294                oldWp = null;
295            }
296            for (WayPoint trkPnt : segment) {
297                LatLon c = trkPnt.getCoor();
298                trkPnt.customColoring = neutralColor;
299                if (Double.isNaN(c.lat()) || Double.isNaN(c.lon())) {
300                    continue;
301                }
302                 // now we are sure some color will be assigned
303                Color color = null;
304
305                if (colored == ColorMode.HDOP) {
306                    Float hdop = (Float) trkPnt.attr.get("hdop");
307                    color = hdopScale.getColor(hdop);
308                }
309                if (oldWp != null) { // other coloring modes need segment for calcuation
310                    double dist = c.greatCircleDistance(oldWp.getCoor());
311                    boolean noDraw=false;
312                    switch (colored) {
313                    case VELOCITY:
314                        double dtime = trkPnt.time - oldWp.time;
315                        if(dtime > 0) {
316                            color = velocityScale.getColor(dist / dtime);
317                        } else {
318                            color = velocityScale.getNoDataColor();
319                        }
320                        break;
321                    case DIRECTION:
322                        double dirColor = oldWp.getCoor().heading(trkPnt.getCoor());
323                        color = directionScale.getColor(dirColor);
324                        break;
325                    case TIME:
326                        double t=trkPnt.time;
327                        if (t > 0 && t <= now && maxval - minval > minTrackDurationForTimeColoring) { // skip bad timestamps and very short tracks
328                            color = dateScale.getColor(t);
329                        } else {
330                            color = dateScale.getNoDataColor();
331                        }
332                        break;
333                    }
334                    if (!noDraw && (maxLineLength == -1 || dist <= maxLineLength)) {
335                        trkPnt.drawLine = true;
336                        trkPnt.dir = (int) oldWp.getCoor().heading(trkPnt.getCoor());
337                    } else {
338                        trkPnt.drawLine = false;
339                    }
340                } else { // make sure we reset outdated data
341                    trkPnt.drawLine = false;
342                    color = neutralColor;
343                }
344                if (color!=null) {
345                    trkPnt.customColoring = color;
346                }
347                oldWp = trkPnt;
348            }
349        }
350
351        computeCacheInSync = true;
352    }
353
354    private void drawLines(Graphics2D g, MapView mv, List<WayPoint> visibleSegments) {
355        if (lines) {
356            Point old = null;
357            for (WayPoint trkPnt : visibleSegments) {
358                LatLon c = trkPnt.getCoor();
359                if (Double.isNaN(c.lat()) || Double.isNaN(c.lon())) {
360                    continue;
361                }
362                Point screen = mv.getPoint(trkPnt.getEastNorth());
363                // skip points that are on the same screenposition
364                if (trkPnt.drawLine && old != null && ((old.x != screen.x) || (old.y != screen.y))) {
365                    g.setColor(trkPnt.customColoring);
366                    g.drawLine(old.x, old.y, screen.x, screen.y);
367                }
368                old = screen;
369            }
370        }
371    }
372
373    private void drawArrows(Graphics2D g, MapView mv, List<WayPoint> visibleSegments) {
374        /****************************************************************
375         ********** STEP 3b - DRAW NICE ARROWS **************************
376         ****************************************************************/
377        if (lines && direction && !alternateDirection) {
378            Point old = null;
379            Point oldA = null; // last arrow painted
380            for (WayPoint trkPnt : visibleSegments) {
381                LatLon c = trkPnt.getCoor();
382                if (Double.isNaN(c.lat()) || Double.isNaN(c.lon())) {
383                    continue;
384                }
385                if (trkPnt.drawLine) {
386                    Point screen = mv.getPoint(trkPnt.getEastNorth());
387                    // skip points that are on the same screenposition
388                    if (old != null
389                            && (oldA == null || screen.x < oldA.x - delta || screen.x > oldA.x + delta
390                            || screen.y < oldA.y - delta || screen.y > oldA.y + delta)) {
391                        g.setColor(trkPnt.customColoring);
392                        double t = Math.atan2(screen.y - old.y, screen.x - old.x) + Math.PI;
393                        g.drawLine(screen.x, screen.y, (int) (screen.x + 10 * Math.cos(t - PHI)),
394                                (int) (screen.y + 10 * Math.sin(t - PHI)));
395                        g.drawLine(screen.x, screen.y, (int) (screen.x + 10 * Math.cos(t + PHI)),
396                                (int) (screen.y + 10 * Math.sin(t + PHI)));
397                        oldA = screen;
398                    }
399                    old = screen;
400                }
401            } // end for trkpnt
402        }
403
404        /****************************************************************
405         ********** STEP 3c - DRAW FAST ARROWS **************************
406         ****************************************************************/
407        if (lines && direction && alternateDirection) {
408            Point old = null;
409            Point oldA = null; // last arrow painted
410            for (WayPoint trkPnt : visibleSegments) {
411                LatLon c = trkPnt.getCoor();
412                if (Double.isNaN(c.lat()) || Double.isNaN(c.lon())) {
413                    continue;
414                }
415                if (trkPnt.drawLine) {
416                    Point screen = mv.getPoint(trkPnt.getEastNorth());
417                    // skip points that are on the same screenposition
418                    if (old != null
419                            && (oldA == null || screen.x < oldA.x - delta || screen.x > oldA.x + delta
420                            || screen.y < oldA.y - delta || screen.y > oldA.y + delta)) {
421                        g.setColor(trkPnt.customColoring);
422                        g.drawLine(screen.x, screen.y, screen.x + dir[trkPnt.dir][0], screen.y
423                                + dir[trkPnt.dir][1]);
424                        g.drawLine(screen.x, screen.y, screen.x + dir[trkPnt.dir][2], screen.y
425                                + dir[trkPnt.dir][3]);
426                        oldA = screen;
427                    }
428                    old = screen;
429                }
430            } // end for trkpnt
431        }
432    }
433
434    private void drawPoints(Graphics2D g, MapView mv, List<WayPoint> visibleSegments) {
435        /****************************************************************
436         ********** STEP 3d - DRAW LARGE POINTS AND HDOP CIRCLE *********
437         ****************************************************************/
438        if (large || hdopCircle) {
439            final int halfSize = largesize/2;
440            for (WayPoint trkPnt : visibleSegments) {
441                LatLon c = trkPnt.getCoor();
442                if (Double.isNaN(c.lat()) || Double.isNaN(c.lon())) {
443                    continue;
444                }
445                Point screen = mv.getPoint(trkPnt.getEastNorth());
446
447
448                if (hdopCircle && trkPnt.attr.get("hdop") != null) {
449                    // hdop value
450                    float hdop = (Float)trkPnt.attr.get("hdop");
451                    if (hdop < 0) {
452                        hdop = 0;
453                    }
454                    Color customColoringTransparent = hdopAlpha<0 ? trkPnt.customColoring:
455                        new Color(trkPnt.customColoring.getRGB() & 0x00ffffff | hdopAlpha<<24, true);
456                    g.setColor(customColoringTransparent);
457                    // hdop cirles
458                    int hdopp = mv.getPoint(new LatLon(trkPnt.getCoor().lat(), trkPnt.getCoor().lon() + 2*6*hdop*360/40000000)).x - screen.x;
459                    g.drawArc(screen.x-hdopp/2, screen.y-hdopp/2, hdopp, hdopp, 0, 360);
460                }
461                if (large) {
462                    // color the large GPS points like the gps lines
463                    if (trkPnt.customColoring != null) {
464                        Color customColoringTransparent = largePointAlpha<0 ? trkPnt.customColoring:
465                            new Color(trkPnt.customColoring.getRGB() & 0x00ffffff | largePointAlpha<<24, true);
466
467                        g.setColor(customColoringTransparent);
468                    }
469                    g.fillRect(screen.x-halfSize, screen.y-halfSize, largesize, largesize);
470                }
471            } // end for trkpnt
472        } // end if large || hdopcircle
473
474        /****************************************************************
475         ********** STEP 3e - DRAW SMALL POINTS FOR LINES ***************
476         ****************************************************************/
477        if (!large && lines) {
478            g.setColor(neutralColor);
479            for (WayPoint trkPnt : visibleSegments) {
480                LatLon c = trkPnt.getCoor();
481                if (Double.isNaN(c.lat()) || Double.isNaN(c.lon())) {
482                    continue;
483                }
484                if (!trkPnt.drawLine) {
485                    Point screen = mv.getPoint(trkPnt.getEastNorth());
486                    g.drawRect(screen.x, screen.y, 0, 0);
487                }
488            } // end for trkpnt
489        } // end if large
490
491        /****************************************************************
492         ********** STEP 3f - DRAW SMALL POINTS INSTEAD OF LINES ********
493         ****************************************************************/
494        if (!large && !lines) {
495            g.setColor(neutralColor);
496            for (WayPoint trkPnt : visibleSegments) {
497                LatLon c = trkPnt.getCoor();
498                if (Double.isNaN(c.lat()) || Double.isNaN(c.lon())) {
499                    continue;
500                }
501                Point screen = mv.getPoint(trkPnt.getEastNorth());
502                g.setColor(trkPnt.customColoring);
503                g.drawRect(screen.x, screen.y, 0, 0);
504            } // end for trkpnt
505        } // end if large
506    }
507
508    private void fixColors(List<WayPoint> visibleSegments) {
509        for (WayPoint trkPnt : visibleSegments) {
510            if (trkPnt.customColoring == null) {
511                trkPnt.customColoring = neutralColor;
512            }
513        }
514    }
515
516    /**
517     * Check cache validity set necessary flags
518     */
519    private void checkCache() {
520        if ((computeCacheMaxLineLengthUsed != maxLineLength) || (!neutralColor.equals(computeCacheColorUsed))
521                || (computeCacheColored != colored) || (computeCacheColorTracksTune != colorTracksTune)
522                || (computeCacheColorDynamic != colorModeDynamic)) {
523            computeCacheMaxLineLengthUsed = maxLineLength;
524            computeCacheInSync = false;
525            computeCacheColorUsed = neutralColor;
526            computeCacheColored = colored;
527            computeCacheColorTracksTune = colorTracksTune;
528            computeCacheColorDynamic = colorModeDynamic;
529        }
530    }
531
532    public void dataChanged() {
533        computeCacheInSync = false;
534    }
535
536    public void drawColorBar(Graphics2D g, MapView mv) {
537        int w = mv.getWidth();
538        if (colored == ColorMode.HDOP) {
539            hdopScale.drawColorBar(g, w-30, 50, 20, 100, 1.0);
540        } else if (colored == ColorMode.VELOCITY) {
541            velocityScale.drawColorBar(g, w-30, 50, 20, 100, 3.6);
542        } else if (colored == ColorMode.DIRECTION) {
543            directionScale.drawColorBar(g, w-30, 50, 20, 100, 180.0/Math.PI);
544        }
545    }
546}