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