Skip to content

Commit

Permalink
Merge pull request #6308 from Jnction/gtfs-text-luminance
Browse files Browse the repository at this point in the history
Use WCAG recommendation to fill in GTFS route text color if it is missing
  • Loading branch information
optionsome authored Dec 12, 2024
2 parents 7aa1baf + d12373f commit 8c965c9
Show file tree
Hide file tree
Showing 5 changed files with 98 additions and 20 deletions.
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package org.opentripplanner.gtfs.graphbuilder;

import static org.opentripplanner.utils.color.ColorUtils.computeBrightness;

import java.awt.Color;
import java.io.IOException;
import java.io.Serializable;
Expand Down Expand Up @@ -52,6 +54,7 @@
import org.opentripplanner.standalone.config.BuildConfig;
import org.opentripplanner.transit.model.framework.FeedScopedId;
import org.opentripplanner.transit.service.TimetableRepository;
import org.opentripplanner.utils.color.Brightness;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

Expand Down Expand Up @@ -415,8 +418,7 @@ private boolean skipEntityClass(Class<?> entityClass) {
* If a route doesn't have color or already has routeColor and routeTextColor nothing is done.
* <p>
* textColor can be black or white. White for dark colors and black for light colors of
* routeColor. If color is light or dark is calculated based on luminance formula: sqrt(
* 0.299*Red^2 + 0.587*Green^2 + 0.114*Blue^2 )
* routeColor.
*/
private void generateRouteColor(Route route) {
String routeColor = route.getColor();
Expand All @@ -431,16 +433,7 @@ private void generateRouteColor(Route route) {
}

Color routeColorColor = Color.decode("#" + routeColor);
//gets float of RED, GREEN, BLUE in range 0...1
float[] colorComponents = routeColorColor.getRGBColorComponents(null);
//Calculates luminance based on https://stackoverflow.com/questions/596216/formula-to-determine-brightness-of-rgb-color
double newRed = 0.299 * Math.pow(colorComponents[0], 2.0);
double newGreen = 0.587 * Math.pow(colorComponents[1], 2.0);
double newBlue = 0.114 * Math.pow(colorComponents[2], 2.0);
double luminance = Math.sqrt(newRed + newGreen + newBlue);

//For brighter colors use black text color and reverse for darker
if (luminance > 0.5) {
if (computeBrightness(routeColorColor) == Brightness.LIGHT) {
textColor = "000000";
} else {
textColor = "FFFFFF";
Expand Down
18 changes: 10 additions & 8 deletions client/src/util/generateTextColor.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,20 @@
/**
* textColor can be black or white. White for dark colors and black for light colors.
* Calculated based on luminance formula:
* sqrt( 0.299*Red^2 + 0.587*Green^2 + 0.114*Blue^2 )
* Calculated according to WCAG 2.1
*/
export function generateTextColor(hexColor: string) {
const color = decodeColor(hexColor);

//Calculates luminance based on https://stackoverflow.com/questions/596216/formula-to-determine-brightness-of-rgb-color
const newRed = 0.299 * Math.pow(color[0] / 255.0, 2.0);
const newGreen = 0.587 * Math.pow(color[1] / 255.0, 2.0);
const newBlue = 0.114 * Math.pow(color[2] / 255.0, 2.0);
const luminance = Math.sqrt(newRed + newGreen + newBlue);
function linearizeColorComponent(srgb: number) {
return srgb <= 0.04045 ? srgb / 12.92 : Math.pow((srgb + 0.055) / 1.055, 2.4);
}

const r = linearizeColorComponent(color[0] / 255.0);
const g = linearizeColorComponent(color[1] / 255.0);
const b = linearizeColorComponent(color[2] / 255.0);
const luminance = 0.2126 * r + 0.7152 * g + 0.0722 * b;

if (luminance > 0.66) {
if (luminance > 0.179) {
return '#000';
} else {
return '#fff';
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package org.opentripplanner.utils.color;

public enum Brightness {
DARK,
LIGHT,
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package org.opentripplanner.utils.color;

import java.awt.Color;

public final class ColorUtils {

private ColorUtils() {}

/**
* Calculates luminance according to
* <a href="https://www.w3.org/TR/WCAG21/#dfn-relative-luminance">W3C Recommendation</a>
*/
public static double computeLuminance(Color color) {
//gets float of RED, GREEN, BLUE in range 0...1
float[] colorComponents = color.getRGBColorComponents(null);
double r = linearizeColorComponent(colorComponents[0]);
double g = linearizeColorComponent(colorComponents[1]);
double b = linearizeColorComponent(colorComponents[2]);
return 0.2126 * r + 0.7152 * g + 0.0722 * b;
}

private static double linearizeColorComponent(double srgb) {
return srgb <= 0.04045 ? srgb / 12.92 : Math.pow((srgb + 0.055) / 1.055, 2.4);
}

/**
* Determine if a color is light or dark
* <p>
* A light color is a color where the contrast ratio with black is larger than with white.
* <p>
* The contrast ratio is defined per Web Content Accessibility Guidelines (WCAG) 2.1.
*/
public static Brightness computeBrightness(Color color) {
// The contrast ratio between two colors is defined as (L1 + 0.05) / (L2 + 0.05)
// where L1 is the lighter of the two colors.
//
// Therefore, the contrast ratio with black is (L + 0.05) / 0.05 and the contrast ratio with
// white is 1.05 / (L + 0.05)
//
// Solving (L + 0.05) / 0.05 > 1.05 / (L + 0.05) gets L > 0.179
return computeLuminance(color) > 0.179 ? Brightness.LIGHT : Brightness.DARK;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package org.opentripplanner.utils.color;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.params.provider.Arguments.arguments;
import static org.opentripplanner.utils.color.ColorUtils.computeBrightness;

import java.awt.Color;
import java.util.stream.Stream;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;

public class ColorUtilsTest {

private static Stream<Arguments> brightnessExpectations() {
return Stream.of(
arguments(Color.black, Brightness.DARK),
arguments(Color.green, Brightness.LIGHT),
arguments(Color.blue, Brightness.DARK),
arguments(Color.red, Brightness.LIGHT),
arguments(Color.yellow, Brightness.LIGHT),
arguments(Color.white, Brightness.LIGHT),
arguments(Color.pink, Brightness.LIGHT),
arguments(Color.orange, Brightness.LIGHT),
arguments(Color.cyan, Brightness.LIGHT)
);
}

@ParameterizedTest
@MethodSource("brightnessExpectations")
void testBrightness(Color color, Brightness brightness) {
assertEquals(computeBrightness(color), brightness);
}
}

0 comments on commit 8c965c9

Please sign in to comment.