Ticket #14343: josm_ticket_1434_gps_heat_map _point_cloud_v1_by_kidelo.patch

File josm_ticket_1434_gps_heat_map _point_cloud_v1_by_kidelo.patch, 26.2 KB (added by kidelo, 8 years ago)
  • src/org/openstreetmap/josm/gui/layer/gpx/GpxDrawHelper.java

     
    2727import java.util.Collections;
    2828import java.util.Date;
    2929import java.util.List;
     30import java.util.Random;
    3031
    3132import javax.swing.ImageIcon;
    3233
     
    9596    private ColorMode computeCacheColored;
    9697    private int computeCacheColorTracksTune;
    9798    private int computeCacheHeatMapDrawColorTableIdx;
     99    private boolean computeCacheHeatMapDrawPointMode;
     100    private int computeCacheHeatMapDrawGain;
     101    private int computeCacheHeatMapDrawLowerLimit;
    98102
    99103    //// Color-related fields
    100104    /** Mode of the line coloring **/
     
    134138    private boolean heatMapDrawExtraLine;
    135139    // used index for color table (parameter)
    136140    private int heatMapDrawColorTableIdx;
     141    // use point or line draw mode
     142    private boolean heatMapDrawPointMode;
     143    // extra gain > 0 or < 0 attenuation, 0 = default
     144    private int heatMapDrawGain;
     145    // do not draw elements with value lower than this limit
     146    private int heatMapDrawLowerLimit;
    137147
    138148    // normal buffered image and draw object (cached)
    139149    private BufferedImage heatMapImgGray;
     
    155165    private static Color[] heatMapLutColorJosmRed2Blue = createColorFromResource("red2blue");
    156166
    157167    // user defined heatmap color
    158     private Color[] heatMapLutColor = createColorLut(Color.BLACK, Color.WHITE);
     168    private Color[] heatMapLutColor = createColorLut(0, Color.BLACK, Color.WHITE);
    159169
    160170    private void setupColors() {
    161171        hdopAlpha = Main.pref.getInteger("hdop.color.alpha", -1);
     
    277287        // get heatmap parameters
    278288        heatMapEnabled = Main.pref.getBoolean("draw.rawgps.heatmap.enabled", spec, false);
    279289        heatMapDrawExtraLine = Main.pref.getBoolean("draw.rawgps.heatmap.line-extra", spec, false);
    280         heatMapDrawColorTableIdx = Main.pref.getInteger("draw.rawgps.heatmap.colormap", specName(layerName), 0);
     290        heatMapDrawColorTableIdx = Main.pref.getInteger("draw.rawgps.heatmap.colormap", spec, 0);
     291        heatMapDrawPointMode = Main.pref.getBoolean("draw.rawgps.heatmap.use-points", spec, false);
     292        heatMapDrawGain = Main.pref.getInteger("draw.rawgps.heatmap.gain", spec, 0);
     293        heatMapDrawLowerLimit = Main.pref.getInteger("draw.rawgps.heatmap.lower-limit", spec, 0);
    281294
     295        // shrink to range
     296        heatMapDrawGain = Math.min(Math.max(-10, heatMapDrawGain), 10);
     297
    282298        neutralColor = getColor(layerName, true);
    283299        velocityScale.setNoDataColor(neutralColor);
    284300        dateScale.setNoDataColor(neutralColor);
     
    513529        // heat mode
    514530        if (ColorMode.HEATMAP == colored) {
    515531
    516             // generate and get new user color map
    517             heatMapLutColor = selectColorMap(neutralColor != null ? neutralColor : Color.WHITE, heatMapDrawColorTableIdx);
     532            // get new user color map and refresh visibility level
     533            heatMapLutColor = createColorLut(heatMapDrawLowerLimit,
     534                                             selectColorMap(neutralColor != null ? neutralColor : Color.WHITE, heatMapDrawColorTableIdx));
    518535
    519536            // force redraw of image
    520537            heatMapMapViewState = null;
     
    794811    }
    795812
    796813    /**
    797      * Creates a linear distributed colormap by linear blending between colors
     814     * Creates a distributed colormap by linear blending between colors
     815     * @param lowerLimit lower limit for first visible color
    798816     * @param colors 1..n colors
    799817     * @return array of Color objects
    800818     */
    801     protected static Color[] createColorLut(Color... colors) {
     819    protected static Color[] createColorLut(int lowerLimit, Color... colors) {
    802820
    803821        // number of lookup entries
    804822        final int tableSize = 256;
     
    824842            Color c = new Color(pixel[0]);
    825843
    826844            // smooth alpha like sin curve
    827             int alpha = (int) (Math.sin(i * mapTo90Deg) * 255);
     845            int alpha = (i > lowerLimit) ? (int) (Math.sin((i-lowerLimit) * mapTo90Deg) * 255) : 0;
    828846
    829847            // alpha with pre-offset, first color -> full transparent
    830             alpha = i > 0 ? (10 + alpha) : 0;
     848            alpha = alpha > 0 ? (20 + alpha) : 0;
    831849
    832850            // shrink to maximum bound
    833851            if (alpha > 255) {
     
    915933            colorList.add(darkerColor(lastColor, 0.950f));
    916934        }
    917935
    918         return createColorLut(colorList.toArray(new Color[ colorList.size() ]));
     936        return createColorLut(0, colorList.toArray(new Color[ colorList.size() ]));
    919937    }
    920938
    921939    /**
     
    927945     */
    928946    protected static Color[] selectColorMap(Color userColor, int tableIdx) {
    929947
     948        // generate new user color map ( dark, user color, white )
     949        Color[] userColor1 = createColorLut(0, userColor.darker(), userColor, userColor.brighter(), Color.WHITE);
     950
     951        // generate new user color map ( white -> color )
     952        Color[] userColor2 = createColorLut(0, Color.WHITE, Color.WHITE, userColor);
     953
    930954        // generate new user color map
    931         Color[] nextUserColor = createColorLut(Color.BLACK, userColor.darker(),
    932                                                userColor, userColor.brighter(), Color.WHITE);
     955        Color[] ColorTrafficLights = createColorLut(0, Color.WHITE, Color.GREEN.darker(), Color.YELLOW, Color.RED);
    933956
    934957        // decide what, keep order is sync with setting on GUI
    935958        Color[][] lut = {
    936                 nextUserColor,
     959                userColor1,
     960                userColor2,
     961                ColorTrafficLights,
    937962                heatMapLutColorJosmInferno,
    938963                heatMapLutColorJosmViridis,
    939964                heatMapLutColorJosmBrown2Green,
     
    940965                heatMapLutColorJosmRed2Blue
    941966        };
    942967
     968        // default case
     969        Color[] nextUserColor = userColor1;
     970
    943971        // select by index
    944972        if (tableIdx < lut.length) {
    945973            nextUserColor = lut[ tableIdx ];
    946974        }
    947975
     976        // adjust color map
    948977        return nextUserColor;
    949978    }
    950979
     
    970999     * @param backComp        composite use to draw background objects
    9711000     * @param backStroke      stroke use to draw background objects
    9721001     */
    973     private void drawHeatGrayMap(Graphics2D gB, MapView mv, List<WayPoint> listSegm,
    974                                  Composite foreComp, Stroke foreStroke,
    975                                  Composite backComp, Stroke backStroke) {
     1002    private void drawHeatGrayLineMap(Graphics2D gB, MapView mv, List<WayPoint> listSegm,
     1003                                     Composite foreComp, Stroke foreStroke,
     1004                                     Composite backComp, Stroke backStroke) {
    9761005
    9771006        // draw foreground
    9781007        boolean drawForeground = foreComp != null && foreStroke != null;
     
    10361065        final int maxPixelY = imgGray.getHeight();
    10371066
    10381067        // always full or outlines at big samples rasters
    1039         final boolean drawOutlines = (outlineWidth > 0) && ((0 == sampleRaster) || (sampleRaster > 8));
     1068        final boolean drawOutlines = (outlineWidth > 0) && ((0 == sampleRaster) || (sampleRaster > 10));
    10401069
    10411070        // backup stroke
    10421071        final Stroke oldStroke = g.getStroke();
     
    11421171            heatMapGraph2d.setBackground(new Color(0, 0, 0, 255));
    11431172            heatMapGraph2d.setColor(Color.WHITE);
    11441173
     1174            // fast draw ( maybe help or not )
     1175            heatMapGraph2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_OFF);
     1176            heatMapGraph2d.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_SPEED);
     1177            heatMapGraph2d.setRenderingHint(RenderingHints.KEY_ALPHA_INTERPOLATION, RenderingHints.VALUE_ALPHA_INTERPOLATION_SPEED);
     1178            heatMapGraph2d.setRenderingHint(RenderingHints.KEY_DITHERING, RenderingHints.VALUE_DITHER_DISABLE);
     1179            heatMapGraph2d.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR);
     1180            heatMapGraph2d.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, RenderingHints.VALUE_STROKE_PURE);
     1181            heatMapGraph2d.setRenderingHint(RenderingHints.KEY_COLOR_RENDERING, RenderingHints.VALUE_COLOR_RENDER_SPEED);
     1182
    11451183            // cache it
    11461184            heatMapCacheScreenBounds = screenBounds;
    11471185        }
     
    11491187        // 2nd. determine current scale factors -------------------------------
    11501188
    11511189        // the line width (foreground: draw extra small footprint line of track)
    1152         final int lineWidthB = (int) Math.max(1.5f * (globalLineWidth / zoomScale) + 1, 2);
    1153         final int lineWidthF = lineWidthB > 2 ? (globalLineWidth - 1) : 0;
     1190        int lineWidthB = (int) Math.max(1.5f * (globalLineWidth / zoomScale) + 1, 2);
     1191        int lineWidthF = lineWidthB > 2 ? (globalLineWidth - 1) : 0;
    11541192
     1193        // global alpha adjustment
     1194        float lineAlpha = Math.min(Math.max((0.40f/(float) zoomScale)/(globalLineWidth+1), 0.01f), 0.40f);
     1195
     1196        // adjust 0.15 .. 1.85
     1197        float scaleAlpha = 1.0f + ((heatMapDrawGain/10.0f) * 0.85f);
     1198
     1199        // add to calculated values
     1200        float lineAlphaBPoint = Math.max(Math.min(((lineAlpha * 0.65f) * scaleAlpha), 0.90f), 0.001f);
     1201        float lineAlphaBLine = Math.max(Math.min(((lineAlpha * 1.00f) * scaleAlpha), 0.90f), 0.001f);
     1202        float lineAlphaFLine = Math.max(Math.min(((lineAlpha / 1.50f) * scaleAlpha), 0.90f), 0.001f);
     1203
     1204        // 3rd Calculate the heat map data by draw GPX traces with alpha value ----------
     1205
    11551206        // recalculation of image needed
    11561207        final boolean imageRecalc = !mapViewState.equalsInWindow(heatMapMapViewState) ||
    11571208                                    heatMapCacheLineWith != globalLineWidth;
    11581209
    1159         // 3rd Calculate the heat map data by draw GPX traces with alpha value ----------
    1160 
    11611210        // need re-generation of gray image ?
    11621211        if (imageSetup || imageRecalc) {
    11631212
     
    11641213            // clear background
    11651214            heatMapGraph2d.clearRect(0, 0, heatMapImgGray.getWidth(), heatMapImgGray.getHeight());
    11661215
    1167             // alpha combines both values, therefore the foreground shall be lighter
    1168             final float lineAlphaB = Math.min(Math.max((0.40f/(float) zoomScale)/(globalLineWidth + 1), 0.01f), 0.40f);
    1169             final float lineAlphaF = lineAlphaB / 1.5f;
     1216            // point or line blending
     1217            if (heatMapDrawPointMode) {
     1218                heatMapGraph2d.setComposite(AlphaComposite.SrcOver.derive(lineAlphaBPoint));
     1219                drawHeatGrayDotMap(heatMapGraph2d, mv, visibleSegments, lineWidthB);
    11701220
    1171             // derive draw parameters and draw
    1172             drawHeatGrayMap(heatMapGraph2d, mv, visibleSegments,
    1173                             lineWidthF > 1 ? AlphaComposite.SrcOver.derive(lineAlphaF) : null,
    1174                             new BasicStroke(lineWidthF, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND),
    1175                             AlphaComposite.SrcOver.derive(lineAlphaB),
    1176                             new BasicStroke(lineWidthB, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND));
     1221            } else {
     1222                drawHeatGrayLineMap(heatMapGraph2d, mv, visibleSegments,
     1223                                    lineWidthF > 1 ? AlphaComposite.SrcOver.derive(lineAlphaFLine) : null,
     1224                                    new BasicStroke(lineWidthF, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND),
     1225                                    AlphaComposite.SrcOver.derive(lineAlphaBLine),
     1226                                    new BasicStroke(lineWidthB, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND));
     1227            }
    11771228
    1178             // remember draw parameters
     1229            // remember draw parameter
    11791230            heatMapMapViewState = mapViewState;
    11801231            heatMapCacheLineWith = globalLineWidth;
    11811232        }
    11821233
    11831234        // 4th. Draw data on target layer, map data via color lookup table --------------
    1184         drawHeatMapGrayMap(g, heatMapImgGray, lineWidthB > 2 ? (lineWidthB / 2) : 1, lineWidth > 2 ? (lineWidth - 2) : 1);
     1235        drawHeatMapGrayMap(g, heatMapImgGray, lineWidthB > 2 ? (int) (lineWidthB*1.25f) : 1, lineWidth > 2 ? (lineWidth - 2) : 1);
    11851236    }
    11861237
     1238
    11871239    /**
     1240     * Draw a dotted heat map
     1241     *
     1242     * @param gB              the common draw object to use
     1243     * @param mv              the meta data to current displayed area
     1244     * @param listSegm        segments visible in the current scope of mv
     1245     * @param drawSize        draw size of draw element
     1246     */
     1247    private void drawHeatGrayDotMap(Graphics2D gB, MapView mv, List<WayPoint> listSegm, int drawSize) {
     1248
     1249        // typical rendering rate -> use realtime preview instead of accurate display
     1250        final double maxSegm = 25000, nrSegms = listSegm.size();
     1251
     1252        // determine random drop rate
     1253        final double randomDrop = Math.min(nrSegms > maxSegm ? (nrSegms - maxSegm) / nrSegms : 0, 0.70f);
     1254
     1255        // http://www.nstb.tc.faa.gov/reports/PAN94_0716.pdf#page=22
     1256        // Global Average Position Domain Accuracy, typical -> not worst case !
     1257        // < 4.218 m Vertical
     1258        // < 2.168 m Horizontal
     1259        final double pixelRmsX = (100 / mv.getDist100Pixel()) * 2.168;
     1260        final double pixelRmsY = (100 / mv.getDist100Pixel()) * 4.218;
     1261
     1262        Point lastPnt = null;
     1263
     1264        // for all points, draw single lines
     1265        for (WayPoint trkPnt : listSegm) {
     1266
     1267            // get transformed coordinates
     1268            final Point paintPnt = mv.getPoint(trkPnt.getEastNorth());
     1269
     1270            // end of line segment or end of list reached
     1271            if (trkPnt.drawLine && null != lastPnt) {
     1272                drawHeatSurfaceLine(gB, paintPnt, lastPnt, drawSize, pixelRmsX, pixelRmsY, randomDrop);
     1273            }
     1274
     1275            // remember
     1276            lastPnt = paintPnt;
     1277        }
     1278    }
     1279
     1280    /**
     1281     * Draw a dotted surface line
     1282     *
     1283     * @param g                 the common draw object to use
     1284     * @param fromPnt           start point
     1285     * @param toPnt             end point
     1286     * @param drawSize          size of draw elements
     1287     * @param rmsSizeX          RMS size of circle for X (width)
     1288     * @param rmsSizeY          RMS size of circle for Y (height)
     1289     * @param dropRate          Pixel render drop rate
     1290     */
     1291    private void drawHeatSurfaceLine(Graphics2D g, Point fromPnt, Point toPnt, int drawSize, double rmsSizeX, double rmsSizeY, double dropRate) {
     1292
     1293        // collect frequently used items
     1294        final int fromX = (int) fromPnt.getX(); final int deltaX = (int) (toPnt.getX() - fromX);
     1295        final int fromY = (int) fromPnt.getY(); final int deltaY = (int) (toPnt.getY() - fromY);
     1296
     1297        // use same random values for each point
     1298        final Random heatMapRandom = new Random(fromX+fromY+deltaX+deltaY);
     1299
     1300        // cache distance between start and end point
     1301        final int dist = (int) Math.abs(fromPnt.distance(toPnt));
     1302
     1303        // number of increment ( fill wide distance tracks )
     1304        double scaleStep = Math.max(1.0f / dist, dist > 100 ? 0.10f : 0.20f);
     1305
     1306        // number of additional random points
     1307        int rounds = Math.min(drawSize/2, 1)+1;
     1308
     1309        // decrease random noise at high drop rate ( more accurate draw of fewer points )
     1310        rmsSizeX *= (1.0d - dropRate);
     1311        rmsSizeY *= (1.0d - dropRate);
     1312
     1313        double scaleVal = 0;
     1314
     1315        // interpolate line draw ( needs separate point instead of line )
     1316        while (scaleVal < (1.0d-0.0001d)) {
     1317
     1318            // get position
     1319            final double pntX = fromX + scaleVal * deltaX;
     1320            final double pntY = fromY + scaleVal * deltaY;
     1321
     1322            // add random distribution around sampled point
     1323            for (int k = 0; k < rounds; k++) {
     1324
     1325                // add error distribution, first point with less error
     1326                int x = (int) (pntX + heatMapRandom.nextGaussian() * (k > 0 ? rmsSizeX : rmsSizeX/4));
     1327                int y = (int) (pntY + heatMapRandom.nextGaussian() * (k > 0 ? rmsSizeY : rmsSizeY/4));
     1328
     1329                // draw it, even drop is requested
     1330                if (heatMapRandom.nextDouble() >= dropRate) {
     1331                    g.fillRect(x-drawSize, y-drawSize, drawSize, drawSize);
     1332                }
     1333            }
     1334            scaleVal += scaleStep;
     1335        }
     1336    }
     1337
     1338    /**
    11881339     * Apply default color configuration to way segments
    11891340     * @param visibleSegments segments visible in the current scope of mv
    11901341     */
     
    12051356                || (computeCacheColorTracksTune != colorTracksTune)
    12061357                || (computeCacheColorDynamic != colorModeDynamic)
    12071358                || (computeCacheHeatMapDrawColorTableIdx != heatMapDrawColorTableIdx)
    1208                 || (!neutralColor.equals(computeCacheColorUsed))
     1359                || (!neutralColor.equals(computeCacheColorUsed)
     1360                || (computeCacheHeatMapDrawPointMode != heatMapDrawPointMode)
     1361                || (computeCacheHeatMapDrawGain != heatMapDrawGain))
     1362                || (computeCacheHeatMapDrawLowerLimit != heatMapDrawLowerLimit)
    12091363      ) {
    12101364            computeCacheMaxLineLengthUsed = maxLineLength;
    12111365            computeCacheInSync = false;
     
    12141368            computeCacheColorTracksTune = colorTracksTune;
    12151369            computeCacheColorDynamic = colorModeDynamic;
    12161370            computeCacheHeatMapDrawColorTableIdx = heatMapDrawColorTableIdx;
     1371            computeCacheHeatMapDrawPointMode = heatMapDrawPointMode;
     1372            computeCacheHeatMapDrawGain = heatMapDrawGain;
     1373            computeCacheHeatMapDrawLowerLimit = heatMapDrawLowerLimit;
    12171374        }
    12181375    }
    12191376
  • src/org/openstreetmap/josm/gui/preferences/display/GPXSettingsPanel.java

     
    99import java.awt.Dimension;
    1010import java.awt.GridBagLayout;
    1111import java.awt.event.ActionListener;
     12import java.util.Enumeration;
    1213
     14import javax.swing.AbstractButton;
    1315import javax.swing.BorderFactory;
    1416import javax.swing.Box;
    1517import javax.swing.ButtonGroup;
     
    1820import javax.swing.JOptionPane;
    1921import javax.swing.JPanel;
    2022import javax.swing.JRadioButton;
     23import javax.swing.JSlider;
    2124
    2225import org.openstreetmap.josm.Main;
    2326import org.openstreetmap.josm.actions.ExpertToggleAction;
     
    6366    private final JRadioButton colorTypeGlobal = new JRadioButton(tr("Use global settings"));
    6467    private final JosmComboBox<String> colorTypeVelocityTune = new JosmComboBox<>(new String[] {tr("Car"), tr("Bicycle"), tr("Foot")});
    6568    private final JosmComboBox<String> colorTypeHeatMapTune = new JosmComboBox<>(new String[] {
    66         trc("Heat map", "User"),
     69        trc("Heat map", "User Normal"),
     70        trc("Heat map", "User Light"),
     71        trc("Heat map", "Traffic Lights"),
    6772        trc("Heat map", "Inferno"),
    6873        trc("Heat map", "Viridis"),
    6974        trc("Heat map", "Wood"),
    7075        trc("Heat map", "Heat")});
     76    private final JCheckBox colorTypeHeatMapPoints = new JCheckBox(tr("Use points instead of lines for heat map"));
     77    private final JSlider colorTypeHeatMapGain = new JSlider();
     78    private final JSlider colorTypeHeatMapLowerLimit = new JSlider();
     79    private JLabel colorTypeHeatMapLowerLimitLabel;
     80    private JLabel colorTypeHeatMapGainLabel;
    7181    private final JCheckBox makeAutoMarkers = new JCheckBox(tr("Create markers when reading GPX"));
    7282    private final JCheckBox drawGpsArrows = new JCheckBox(tr("Draw Direction Arrows"));
    7383    private final JCheckBox drawGpsArrowsFast = new JCheckBox(tr("Fast drawing (looks uglier)"));
     
    228238
    229239        // colorTracks
    230240        ButtonGroup colorGroup = new ButtonGroup();
     241
    231242        if (layerName != null) {
    232243            colorGroup.add(colorTypeGlobal);
    233244        }
     
    238249        colorGroup.add(colorTypeTime);
    239250        colorGroup.add(colorTypeHeatMap);
    240251
    241         colorTypeVelocity.addChangeListener(e -> {
    242             colorTypeVelocityTune.setEnabled(colorTypeVelocity.isSelected());
    243             colorDynamic.setEnabled(colorTypeVelocity.isSelected() || colorTypeDilution.isSelected());
    244         });
    245 
    246         colorTypeHeatMap.addChangeListener(e -> {
    247             colorTypeHeatMapTune.setEnabled(colorTypeHeatMap.isSelected());
    248             colorDynamic.setEnabled(false);
    249         });
    250 
    251         colorTypeDilution.addChangeListener(e -> colorDynamic.setEnabled(colorTypeVelocity.isSelected() || colorTypeDilution.isSelected()));
    252 
    253252        colorTypeNone.setToolTipText(tr("All points and track segments will have the same color. Can be customized in Layer Manager."));
    254253        colorTypeVelocity.setToolTipText(tr("Colors points and track segments by velocity."));
    255254        colorTypeDirection.setToolTipText(tr("Colors points and track segments by direction."));
     
    280279        add(colorTypeHeatIconLabel, GBC.std().insets(5, 0, 0, 5));
    281280        add(colorTypeHeatMapTune, GBC.eol().fill(GBC.HORIZONTAL).insets(5, 0, 0, 5));
    282281
     282        colorTypeHeatMapGainLabel = new JLabel(tr("Overlay gain adjustment"));
     283        colorTypeHeatMapLowerLimitLabel = new JLabel(tr("Lower limit of visibility"));
     284        add(colorTypeHeatMapGainLabel, GBC.std().insets(80, 0, 0, 0));
     285        add(colorTypeHeatMapGain, GBC.eol().fill(GBC.HORIZONTAL).insets(5, 0, 0, 5));
     286        add(colorTypeHeatMapLowerLimitLabel, GBC.std().insets(80, 0, 0, 0));
     287        add(colorTypeHeatMapLowerLimit, GBC.eol().fill(GBC.HORIZONTAL).insets(5, 0, 0, 5));
     288        add(colorTypeHeatMapPoints, GBC.eol().insets(60, 0, 0, 0));
     289
     290        colorTypeHeatMapGain.setToolTipText(tr("Adjust the gain of overlay blending."));
     291        colorTypeHeatMapGain.setOrientation(JSlider.HORIZONTAL);
     292        colorTypeHeatMapGain.setPaintLabels(true);
     293        colorTypeHeatMapGain.setMinimum(-10);
     294        colorTypeHeatMapGain.setMaximum(+10);
     295        colorTypeHeatMapGain.setMinorTickSpacing(1);
     296        colorTypeHeatMapGain.setMajorTickSpacing(5);
     297
     298        colorTypeHeatMapLowerLimit.setToolTipText(tr("Draw all GPX traces that exceed this threshold."));
     299        colorTypeHeatMapLowerLimit.setOrientation(JSlider.HORIZONTAL);
     300        colorTypeHeatMapLowerLimit.setMinimum(0);
     301        colorTypeHeatMapLowerLimit.setMaximum(254);
     302        colorTypeHeatMapLowerLimit.setPaintLabels(true);
     303        colorTypeHeatMapLowerLimit.setMinorTickSpacing(10);
     304        colorTypeHeatMapLowerLimit.setMajorTickSpacing(100);
     305
     306        colorTypeHeatMapPoints.setToolTipText(tr("Render engine uses points with simulated position error instead of lines. "));
     307
     308        // iterate over the buttons, add change listener to any change event
     309        for (Enumeration<AbstractButton> button = colorGroup.getElements(); button.hasMoreElements();) {
     310            (button.nextElement()).addChangeListener(e -> {
     311                colorTypeVelocityTune.setEnabled(colorTypeVelocity.isSelected());
     312                colorTypeHeatMapTune.setEnabled(colorTypeHeatMap.isSelected());
     313                colorTypeHeatMapPoints.setEnabled(colorTypeHeatMap.isSelected());
     314                colorTypeHeatMapGain.setEnabled(colorTypeHeatMap.isSelected());
     315                colorTypeHeatMapLowerLimit.setEnabled(colorTypeHeatMap.isSelected());
     316                colorTypeHeatMapGainLabel.setEnabled(colorTypeHeatMap.isSelected());
     317                colorTypeHeatMapLowerLimitLabel.setEnabled(colorTypeHeatMap.isSelected());
     318                colorDynamic.setEnabled(colorTypeVelocity.isSelected() || colorTypeDilution.isSelected());
     319            });
     320        }
     321
    283322        colorTypeHeatMapTune.addPropertyChangeListener(e -> {
    284323            final Dimension dim = colorTypeHeatMapTune.getPreferredSize();
    285324            if (null != dim) {
     
    298337
    299338        ExpertToggleAction.addVisibilitySwitcher(colorTypeDirection);
    300339        ExpertToggleAction.addVisibilitySwitcher(colorTypeDilution);
     340        ExpertToggleAction.addVisibilitySwitcher(colorTypeHeatMapLowerLimit);
     341        ExpertToggleAction.addVisibilitySwitcher(colorTypeHeatMapLowerLimitLabel);
    301342
    302343        colorDynamic.setToolTipText(tr("Colors points and track segments by data limits."));
    303344        add(colorDynamic, GBC.eop().insets(40, 0, 0, 0));
     
    377418            colorTypeGlobal.setSelected(true);
    378419            colorDynamic.setSelected(false);
    379420            colorDynamic.setEnabled(false);
     421            colorTypeHeatMapPoints.setSelected(false);
     422            colorTypeHeatMapGain.setValue(0);
     423            colorTypeHeatMapLowerLimit.setValue(0);
    380424        } else {
    381425            int colorType = Main.pref.getInteger("draw.rawgps.colors", layerName, 0);
    382426            switch (colorType) {
     
    390434            }
    391435            int ccts = Main.pref.getInteger("draw.rawgps.colorTracksTune", layerName, 45);
    392436            colorTypeVelocityTune.setSelectedIndex(ccts == 10 ? 2 : (ccts == 20 ? 1 : 0));
    393             colorTypeVelocityTune.setEnabled(colorTypeVelocity.isSelected() && colorTypeVelocity.isEnabled());
    394 
    395437            colorTypeHeatMapTune.setSelectedIndex(Main.pref.getInteger("draw.rawgps.heatmap.colormap", layerName, 0));
    396             colorTypeHeatMapTune.setEnabled(colorTypeHeatMap.isSelected() && colorTypeHeatMap.isEnabled());
    397 
    398438            colorDynamic.setSelected(Main.pref.getBoolean("draw.rawgps.colors.dynamic", layerName, false));
    399             colorDynamic.setEnabled(colorTypeVelocity.isSelected() || colorTypeDilution.isSelected());
     439            colorTypeHeatMapPoints.setSelected(Main.pref.getBoolean("draw.rawgps.heatmap.use-points", layerName, false));
     440            colorTypeHeatMapGain.setValue(Main.pref.getInteger("draw.rawgps.heatmap.gain", layerName, 0));
     441            colorTypeHeatMapLowerLimit.setValue(Main.pref.getInteger("draw.rawgps.heatmap.lower-limit", layerName, 0));
    400442        }
    401443    }
    402444
     
    469511        Main.pref.putInteger("draw.rawgps.colorTracksTune"+layerNameDot, ccti == 2 ? 10 : (ccti == 1 ? 20 : 45));
    470512
    471513        Main.pref.putInteger("draw.rawgps.heatmap.colormap"+layerNameDot, colorTypeHeatMapTune.getSelectedIndex());
     514        Main.pref.put("draw.rawgps.heatmap.use-points"+layerNameDot, colorTypeHeatMapPoints.isSelected());
     515        Main.pref.putInteger("draw.rawgps.heatmap.gain"+layerNameDot, colorTypeHeatMapGain.getValue());
     516        Main.pref.putInteger("draw.rawgps.heatmap.lower-limit"+layerNameDot, colorTypeHeatMapLowerLimit.getValue());
    472517
    473518        return false;
    474519    }