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