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