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