Changeset 19236 in josm


Ignore:
Timestamp:
2024-10-08T19:56:20+02:00 (3 months ago)
Author:
taylor.smock
Message:

Fix #23926: Extend GPS legend for time information, improve design (patch by Pauline, modified)

Modifications are as follows:

  • Reduction of code duplication
  • Addition of functions in ColorHelper to calculate contrast ratios
Location:
trunk/src/org/openstreetmap/josm
Files:
3 edited

Legend:

Unmodified
Added
Removed
  • trunk/src/org/openstreetmap/josm/gui/layer/gpx/GpxDrawHelper.java

    r19106 r19236  
    237237    private boolean gpxLayerInvalidated;
    238238
     239    /** minTime saves the start time of the track as epoch seconds */
     240    private double minTime;
     241    /** maxTime saves the end time of the track as epoch seconds */
     242    private double maxTime;
     243
     244
    239245    private void setupColors() {
    240246        hdopAlpha = Config.getPref().getInt("hdop.color.alpha", -1);
     
    243249        hdopScale = ColorScale.createHSBScale(256).makeReversed().addTitle(tr("HDOP"));
    244250        qualityScale = ColorScale.createFixedScale(rtkLibQualityColors).addTitle(tr("Quality")).addColorBarTitles(rtkLibQualityNames);
    245         fixScale = ColorScale.createFixedScale(gpsFixQualityColors).addTitle(tr("GPS fix")).addColorBarTitles(gpsFixQualityNames);
    246         refScale = ColorScale.createCyclicScale(1).addTitle(tr("GPS ref"));
    247         dateScale = ColorScale.createHSBScale(256).addTitle(tr("Time"));
    248         directionScale = ColorScale.createCyclicScale(256).setIntervalCount(4).addTitle(tr("Direction"));
     251        fixScale = ColorScale.createFixedScale(gpsFixQualityColors).addTitle(tr("GPS fix value")).addColorBarTitles(gpsFixQualityNames);
     252        refScale = ColorScale.createCyclicScale(1).addTitle(tr("GPS Ref-ID"));
     253        dateScale = ColorScale.createHSBScale(256).addTitle(tr("Track date"));
     254        directionScale = ColorScale.createCyclicScale(256).setIntervalCount(4).addTitle(tr("Direction [°]"));
    249255
    250256        systemOfMeasurementChanged(null, null);
     
    254260    public void systemOfMeasurementChanged(String oldSoM, String newSoM) {
    255261        SystemOfMeasurement som = SystemOfMeasurement.getSystemOfMeasurement();
    256         velocityScale.addTitle(tr("Velocity, {0}", som.speedName));
     262        velocityScale.addTitle(tr("Velocity [{0}]", som.speedName));
    257263        layer.invalidate();
    258264    }
     
    623629            minval = interval.getStart().getEpochSecond();
    624630            maxval = interval.getEnd().getEpochSecond();
     631            this.minTime = minval;
     632            this.maxTime = maxval;
     633
    625634            dateScale.setRange(minval, maxval);
    626635        }
     
    642651                Collections.sort(refs);
    643652                String[] a = {};
    644                 refScale = ColorScale.createCyclicScale(refs.size()).addTitle(tr("GPS ref")).addColorBarTitles(refs.toArray(a));
     653                refScale = ColorScale.createCyclicScale(refs.size()).addTitle(tr("GPS ref ID")).addColorBarTitles(refs.toArray(a));
    645654                refScale.setRange(0, refs.size());
    646655            }
     
    16191628
    16201629        if (colored == ColorMode.HDOP) {
    1621             hdopScale.drawColorBar(g, w-30, 50, 20, 100, 1.0);
     1630            hdopScale.drawColorBar(g, w-10, 50, 20, 100, 1.0);
    16221631        } else if (colored == ColorMode.QUALITY) {
    1623             qualityScale.drawColorBar(g, w-30, 50, 20, 100, 1.0);
     1632            qualityScale.drawColorBar(g, w-10, 50, 20, 100, 1.0);
    16241633        } else if (colored == ColorMode.FIX) {
    1625             fixScale.drawColorBar(g, w-30, 50, 20, 175, 1.0);
     1634            fixScale.drawColorBar(g, w-10, 50, 20, 175, 1.0);
    16261635        } else if (colored == ColorMode.REF) {
    1627             refScale.drawColorBar(g, w-30, 50, 20, 175, 1.0);
     1636            refScale.drawColorBar(g, w-10, 50, 20, 175, 1.0);
    16281637        } else if (colored == ColorMode.VELOCITY) {
    16291638            SystemOfMeasurement som = SystemOfMeasurement.getSystemOfMeasurement();
    1630             velocityScale.drawColorBar(g, w-30, 50, 20, 100, som.speedValue);
     1639            velocityScale.drawColorBar(g, w-10, 50, 20, 100, som.speedValue);
    16311640        } else if (colored == ColorMode.DIRECTION) {
    1632             directionScale.drawColorBar(g, w-30, 50, 20, 100, 180.0/Math.PI);
     1641            directionScale.drawColorBar(g, w-10, 50, 20, 100, 180.0/Math.PI);
     1642        } else if (colored == ColorMode.TIME) {
     1643            dateScale.drawColorBarTime(g, w-10, 50, 20, 100, this.minTime, this.maxTime);
    16331644        }
    16341645    }
  • trunk/src/org/openstreetmap/josm/tools/ColorHelper.java

    r16319 r19236  
    7474    public static Color getForegroundColor(Color bg) {
    7575        // http://stackoverflow.com/a/3943023/2257172
    76         return bg == null ? null :
    77               (bg.getRed()*0.299 + bg.getGreen()*0.587 + bg.getBlue()*0.114) > 186 ?
    78                   Color.BLACK : Color.WHITE;
     76        if (bg == null) {
     77            return null;
     78        }
     79        if (calculateContrastRatio(Color.WHITE, bg) > calculateContrastRatio(Color.BLACK, bg)) {
     80            return Color.WHITE;
     81        }
     82        return Color.BLACK;
    7983    }
    8084
     
    129133        return new Color(255 - clr.getRed(), 255 - clr.getGreen(), 255 - clr.getBlue(), clr.getAlpha());
    130134    }
     135
     136    /**
     137     * Calculate the relative "luminance" of a color. This is mostly useful for choosing background/foreground colours
     138     * @see <a href="https://stackoverflow.com/questions/9733288/how-to-programmatically-calculate-the-contrast-ratio-between-two-colors">
     139     *     constrast ratio</a>
     140     */
     141    private static double calculateLuminance(Color color) {
     142        final double rs = color.getRed() / 255.0;
     143        final double gs = color.getGreen() / 255.0;
     144        final double bs = color.getBlue() / 255.0;
     145        final double r = calculateLuminanceStepFunction(rs);
     146        final double g = calculateLuminanceStepFunction(gs);
     147        final double b = calculateLuminanceStepFunction(bs);
     148        return 0.2126 * r + 0.7152 * g + 0.0722 * b;
     149    }
     150
     151    /**
     152     * This is a step function for {@link #calculateLuminance(Color)}
     153     * @param color The color to get the values for
     154     * @return The value to use when calculating relative luminance
     155     */
     156    private static double calculateLuminanceStepFunction(double color) {
     157        if (color <= 0.03928) {
     158            return color / 12.92;
     159        }
     160        return Math.pow((color + 0.055) / 1.055, 2.4);
     161    }
     162
     163    /**
     164     * Calculate the contrast between two colors (e.g. {@link Color#black} and {@link Color#white}).
     165     * @param first The first color to use
     166     * @param second The second color to use
     167     * @return The contrast ratio ((L1 + 0.05)/(L2 + 0.05))
     168     * @since 19236
     169     */
     170    public static double calculateContrastRatio(Color first, Color second) {
     171        final double fL = calculateLuminance(first);
     172        final double sL = calculateLuminance(second);
     173        return (Math.max(fL, sL) + 0.05) / (Math.min(fL, sL) + 0.05);
     174    }
    131175}
  • trunk/src/org/openstreetmap/josm/tools/ColorScale.java

    r18801 r19236  
    11// License: GPL. For details, see LICENSE file.
    22package org.openstreetmap.josm.tools;
     3
     4import static org.openstreetmap.josm.tools.I18n.marktr;
    35
    46import java.awt.Color;
    57import java.awt.FontMetrics;
    68import java.awt.Graphics2D;
     9import java.awt.Font;
    710import java.util.Arrays;
     11import java.util.Date;
     12import java.time.ZoneId;
     13import java.time.Instant;
     14import java.time.ZonedDateTime;
     15import java.time.format.DateTimeFormatter;
     16
     17import org.openstreetmap.josm.data.preferences.NamedColorProperty;
     18
     19import javax.swing.UIManager;
    820
    921/**
     
    1224 */
    1325public final class ColorScale {
     26    private static final Color LEGEND_BACKGROUND = new NamedColorProperty(marktr("gpx legend background"), new Color(180, 180, 180, 160)).get();
     27    private static final Color LEGEND_TEXT_OUTLINE_DARK = new NamedColorProperty(marktr("gpx legend text outline dark"),
     28            new Color(102, 102, 102)).get();
     29    private static final Color LEGEND_TEXT_OUTLINE_BRIGHT = new NamedColorProperty(marktr("gpx legend text outline bright"),
     30            new Color(204, 204, 204)).get();
     31    private static final Color LEGEND_TITLE = new NamedColorProperty(marktr("gpx legend title color"), new Color(0, 0, 0)).get();
     32
     33    private static final String DAY_TIME_FORMAT = "yyyy-MM-dd      HH:mm";
     34    private static final String TIME_FORMAT = "HH:mm:ss";
     35    /** Padding for the legend (from the text to the edge of the rectangle) */
     36    private static final byte PADDING = 19;
     37
    1438    private double min, max;
    1539    private Color noDataColor;
     
    226250
    227251    /**
     252     * draws an outline for the legend texts
     253     * @param g The graphics to draw on
     254     * @param txt The text to draw the outline
     255     * @param x Text x
     256     * @param y Text y
     257     * @param color The color of the text
     258     */
     259    private void drawOutline(final Graphics2D g, final String txt, final int x, final int y, final Color color) {
     260        if (ColorHelper.calculateContrastRatio(color, LEGEND_TEXT_OUTLINE_DARK) >=
     261            ColorHelper.calculateContrastRatio(color, LEGEND_TEXT_OUTLINE_BRIGHT)) {
     262            g.setColor(LEGEND_TEXT_OUTLINE_DARK);
     263        } else {
     264            g.setColor(LEGEND_TEXT_OUTLINE_BRIGHT);
     265        }
     266
     267        g.drawString(txt, x -1, y -1);
     268        g.drawString(txt, x +1, y -1);
     269        g.drawString(txt, x -1, y +1);
     270        g.drawString(txt, x +1, y +1);
     271        g.setColor(color);
     272    }
     273
     274    /**
    228275     * Draws a color bar representing this scale on the given graphics
    229276     * @param g The graphics to draw on
     
    234281     * @param valueScale The scale factor of the values
    235282     */
    236     public void drawColorBar(Graphics2D g, int x, int y, int w, int h, double valueScale) {
    237         int n = colors.length;
    238         for (int i = 0; i < n; i++) {
    239             g.setColor(colors[i]);
    240             if (w < h) {
    241                 g.fillRect(x, y+i*h/n, w, h/n+1);
    242             } else {
    243                 g.fillRect(x+i*w/n, y, w/n+1, h);
    244             }
    245         }
    246 
    247         int fw, fh;
    248         FontMetrics fm = g.getFontMetrics();
    249         fh = fm.getHeight()/2;
     283    public void drawColorBar(final Graphics2D g, final int x, final int y, final int w, final int h, final double valueScale) {
     284        final int n = colors.length;
     285
     286        final FontMetrics fm = calculateFontMetrics(g);
     287
     288        g.setColor(LEGEND_BACKGROUND);
     289
     290        // color bar texts width & height
     291        final int fw;
     292        final int fh = fm.getHeight() / 2;
     293
     294        // calculates the width of the color bar texts
    250295        if (colorBarTitles != null && colorBarTitles.length > 0) {
    251              fw = Arrays.stream(colorBarTitles).mapToInt(fm::stringWidth).max().orElse(50);
     296            fw = Arrays.stream(colorBarTitles).mapToInt(fm::stringWidth).max().orElse(50);
    252297        } else {
    253298            fw = fm.stringWidth(
     
    255300                    + fm.stringWidth("0.123");
    256301        }
     302
     303        // background rectangle
     304        final int[] t = drawBackgroundRectangle(g, x, y, w, h, fw, fh, fm.stringWidth(title));
     305        final int xRect = t[0];
     306        final int rectWidth = t[1];
     307        final int xText = t[2];
     308        final int titleWidth = t[3];
     309
     310        // colorbar
     311        for (int i = 0; i < n; i++) {
     312            g.setColor(colors[i]);
     313            if (w < h) {
     314                double factor = n == 6 ? 1.2 : 1.07 + (0.045 * Math.log(n));
     315                if (n < 200) {
     316                    g.fillRect(xText + fw + PADDING / 3, y - PADDING / 2 + i * (int) ((double) h / n * factor),
     317                            w, (int) ((double) h / n * factor));
     318                } else {
     319                    g.fillRect(xText + fw + PADDING / 3, y - PADDING / 2 + i * h / (int) (n * 0.875), w, (h / n + 1));
     320                }
     321            } else {
     322                g.fillRect(xText + fw + 7 + i * w / n, y, w / n, h + 1);
     323            }
     324        }
     325
     326        // legend title
     327        if (title != null) {
     328            g.setColor(LEGEND_TITLE);
     329            g.drawString(title, xRect + rectWidth / 2 - titleWidth / 2, y - fh * 3 / 2 - 10);
     330        }
     331
     332        // legend texts
     333        drawLegend(g, y, w, h, valueScale, fh, fw, xText);
     334
    257335        g.setColor(noDataColor);
     336    }
     337
     338    /**
     339     * Draws a color bar representing the time scale on the given graphics
     340     * @param g The graphics to draw on
     341     * @param x Rect x
     342     * @param y Rect y
     343     * @param w Color bar width
     344     * @param h Color bar height
     345     * @param minVal start time of the track
     346     * @param maxVal end time of the track
     347     */
     348    public void drawColorBarTime(final Graphics2D g, final int x, final int y, final int w, final int h,
     349                                 final double minVal, final double maxVal) {
     350        final int n = colors.length;
     351
     352        final FontMetrics fm = calculateFontMetrics(g);
     353
     354        g.setColor(LEGEND_BACKGROUND);
     355
     356        final int padding = PADDING;
     357
     358        // color bar texts width & height
     359        final int fw;
     360        final int fh = fm.getHeight() / 2;
     361
     362        // calculates the width of the colorbar texts
     363        if (maxVal - minVal > 86400) {
     364            fw = fm.stringWidth(DAY_TIME_FORMAT);
     365        } else {
     366            fw = fm.stringWidth(TIME_FORMAT);
     367        }
     368
     369        // background rectangle
     370        final int[] t = drawBackgroundRectangle(g, x, y, w, h, fw, fh, fm.stringWidth(title));
     371        final int xRect = t[0];
     372        final int rectWidth = t[1];
     373        final int xText = t[2];
     374        final int titleWidth = t[3];
     375
     376        // colorbar
     377        for (int i = 0; i < n; i++) {
     378            g.setColor(colors[i]);
     379            if (w < h) {
     380                g.fillRect(xText + fw + padding / 3, y - padding / 2 + i * h / (int) (n * 0.875), w, (h / n + 1));
     381            } else {
     382                g.fillRect(xText + fw + padding / 3 + i * w / n, y, w / n + 1, h);
     383            }
     384        }
     385
     386        // legend title
    258387        if (title != null) {
    259             g.drawString(title, x-fw-3, y-fh*3/2);
    260         }
     388            g.setColor(LEGEND_TITLE);
     389            g.drawString(title, xRect + rectWidth / 2 - titleWidth / 2, y - fh * 3 / 2 - padding / 2);
     390        }
     391
     392        // legend texts
     393        drawTimeLegend(g, y, x, h, minVal, maxVal, fh, fw, xText);
     394
     395        g.setColor(noDataColor);
     396    }
     397
     398    private static FontMetrics calculateFontMetrics(final Graphics2D g) {
     399        final Font newFont = UIManager.getFont("PopupMenu.font");
     400        g.setFont(newFont);
     401        return g.getFontMetrics();
     402    }
     403
     404    /**
     405     * Draw the background rectangle
     406     * @param g The graphics to draw on
     407     * @param x Rect x
     408     * @param y Rect y
     409     * @param w Color bar width
     410     * @param h Color bar height
     411     * @param fw The font width
     412     * @param fh The font height
     413     * @param titleWidth The width of the title
     414     * @return an @{code int[]} of [xRect, rectWidth, xText, titleWidth] TODO investigate using records in Java 17
     415     */
     416    private int[] drawBackgroundRectangle(final Graphics2D g, final int x, final int y,
     417                                          final int w, final int h, final int fw, final int fh,
     418                                          int titleWidth) {
     419        final int xRect;
     420        final int rectWidth;
     421        final int xText;
     422        final int arcWidth = 20;
     423        final int arcHeight = 20;
     424        if (fw + w > titleWidth) {
     425            rectWidth = w + fw + PADDING * 2;
     426            xRect = x - rectWidth;
     427            xText = xRect + (int) (PADDING / 1.2);
     428            g.fillRoundRect(xRect, (fh * 3 / 2), rectWidth, h + y - (fh * 3 / 2) + (int) (PADDING / 1.5), arcWidth, arcHeight);
     429        } else {
     430            if (titleWidth >= 120) {
     431                titleWidth = 120;
     432            }
     433            rectWidth = w + titleWidth + PADDING + PADDING / 2;
     434            xRect = x - rectWidth;
     435            xText = xRect + PADDING / 2 + rectWidth / 2 - fw;
     436            g.fillRoundRect(xRect, (fh * 3 / 2), rectWidth, h + y - (fh * 3 / 2) + (int) (PADDING / 1.5), arcWidth, arcHeight);
     437        }
     438        return new int[] {xRect, rectWidth, xText, titleWidth};
     439    }
     440
     441    /**
     442     * Draws the legend for the color bar representing the time scale on the given graphics
     443     * @param g The graphics to draw on
     444     * @param y Rect y
     445     * @param w Color bar width
     446     * @param h Color bar height
     447     * @param fw The font width
     448     * @param fh The font height
     449     * @param valueScale The scale factor of the values
     450     * @param xText The location to start drawing the text (x-axis)
     451     */
     452    private void drawLegend(final Graphics2D g, final int y, final int w, final int h, final double valueScale,
     453                            final int fh, final int fw, final int xText) {
    261454        for (int i = 0; i <= intervalCount; i++) {
    262             g.setColor(colors[(int) (1.0*i*n/intervalCount-1e-10)]);
    263             String txt;
     455            final String txt;
     456            final Color color = colors[(int) (1.0 * i * colors.length / intervalCount - 1e-10)];
     457            g.setColor(color);
     458
    264459            if (colorBarTitles != null && i < colorBarTitles.length) {
    265460                txt = colorBarTitles[i];
     
    268463                txt = String.format("%.3f", val*valueScale);
    269464            }
    270             if (intervalCount == 0) {
    271                 g.drawString(txt, x-fw-3, y+h/2+fh/2);
    272             } else if (w < h) {
    273                 g.drawString(txt, x-fw-3, y+i*h/intervalCount+fh/2);
     465            drawLegendText(g, y, w, h, fh, fw, xText, i, color, txt);
     466        }
     467    }
     468
     469    /**
     470     * Draws the legend for the color bar representing the time scale on the given graphics
     471     * @param g The graphics to draw on
     472     * @param y Rect y
     473     * @param w Color bar width
     474     * @param h Color bar height
     475     * @param minVal start time of the track
     476     * @param maxVal end time of the track
     477     * @param fw The font width
     478     * @param fh The font height
     479     * @param xText The location to start drawing the text (x-axis)
     480     */
     481    private void drawTimeLegend(final Graphics2D g, final int y, final int w, final int h,
     482                                final double minVal, final double maxVal,
     483                                final int fh, final int fw, final int xText) {
     484        for (int i = 0; i <= intervalCount; i++) {
     485            final String txt;
     486            final Color color = colors[(int) (1.0 * i * colors.length / intervalCount - 1e-10)];
     487            g.setColor(color);
     488
     489            if (colorBarTitles != null && i < colorBarTitles.length) {
     490                txt = colorBarTitles[i];
    274491            } else {
    275                 g.drawString(txt, x+i*w/intervalCount-fw/2, y+fh-3);
     492                final double val = minVal + i * (maxVal - minVal) / intervalCount;
     493                final long longval = (long) val;
     494
     495                final Date date = new Date(longval * 1000L);
     496                final Instant dateInst = date.toInstant();
     497
     498                final ZoneId gmt = ZoneId.of("GMT");
     499                final ZonedDateTime zonedDateTime = dateInst.atZone(gmt);
     500
     501                String formatted;
     502
     503                if (maxVal-minVal > 86400) {
     504                    final DateTimeFormatter day = DateTimeFormatter.ofPattern(DAY_TIME_FORMAT);
     505                    formatted = zonedDateTime.format(day);
     506                } else {
     507                    final DateTimeFormatter time = DateTimeFormatter.ofPattern(TIME_FORMAT);
     508                    formatted = zonedDateTime.format(time);
     509                }
     510
     511                txt = formatted;
    276512            }
     513            drawLegendText(g, y, w, h, fh, fw, xText, i, color, txt);
     514        }
     515    }
     516
     517    /**
     518     * Draws the legend for the color bar representing the time scale on the given graphics
     519     * @param g The graphics to draw on
     520     * @param y Rect y
     521     * @param w Color bar width
     522     * @param h Color bar height
     523     * @param fw The font width
     524     * @param fh The font height
     525     * @param xText The location to start drawing the text (x-axis)
     526     * @param color The color of the text to draw
     527     * @param txt The text string to draw
     528     * @param i The index of the legend (so we can calculate the y location)
     529     */
     530    private void drawLegendText(Graphics2D g, int y, int w, int h, int fh, int fw, int xText,
     531                                int i, Color color, String txt) {
     532
     533        if (intervalCount == 0) {
     534            drawOutline(g, txt, xText, y + h / 2 + fh / 2, color);
     535            g.drawString(txt, xText, y + h / 2 + fh / 2);
     536        } else if (w < h) {
     537            drawOutline(g, txt, xText, y + i * h / intervalCount + fh / 2, color);
     538            g.drawString(txt, xText, y + i * h / intervalCount + fh / 2);
     539        } else {
     540            final int xLoc = xText + i * w / intervalCount - fw / 2 - (int) (PADDING / 1.3);
     541            final int yLoc = y + fh - 5;
     542            drawOutline(g, txt, xLoc, yLoc, color);
     543            g.drawString(txt, xLoc, yLoc);
    277544        }
    278545    }
Note: See TracChangeset for help on using the changeset viewer.