/* * Copyright 2014 * Ubiquitous Knowledge Processing (UKP) Lab and FG Language Technology * Technische Universität Darmstadt * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package de.tudarmstadt.ukp.clarin.webanno.api.annotation.coloring; import static java.util.Arrays.asList; import java.util.ArrayList; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Queue; import de.tudarmstadt.ukp.clarin.webanno.api.AnnotationSchemaService; import de.tudarmstadt.ukp.clarin.webanno.api.WebAnnoConst; import de.tudarmstadt.ukp.clarin.webanno.api.annotation.model.AnnotationPreference; import de.tudarmstadt.ukp.clarin.webanno.model.AnnotationFeature; import de.tudarmstadt.ukp.clarin.webanno.model.AnnotationLayer; import de.tudarmstadt.ukp.clarin.webanno.model.LinkMode; public abstract class ColoringStrategy { public static final ColoringStrategy labelHashBasedColor(final String[] aPalette) { return new ColoringStrategy() { @Override public String getColor(Object aObj, String aLabel) { // If each tag should get a separate color, we currently have no chance other than // to derive the color from the actual label text because at this point, we cannot // access the tagset information. If we could do that, we could calculate a position // within the tag space - at least for those layers that have *only* features with // tagsets. For layers that have features without tagsets, again, we can only use // the actual label value... int colorIndex = Math.abs(aLabel.hashCode()); if (colorIndex == Integer.MIN_VALUE) { colorIndex = 0; } return aPalette[colorIndex % aPalette.length]; } }; } public static ColoringStrategy staticColor(final String aColor) { return new ColoringStrategy() { @Override public String getColor(Object aObj, String aLabel) { return aColor; } }; } public static ColoringStrategy getBestStrategy(AnnotationSchemaService aService, AnnotationLayer aLayer, AnnotationPreference aPreferences, Map<String[], Queue<String>> aColorQueues) { // Decide on coloring strategy for the current layer ColoringStrategy coloringStrategy; if (aLayer.isReadonly()) { coloringStrategy = staticColor(DISABLED); } else if (aPreferences.isStaticColor()) { int threshold; if (WebAnnoConst.SPAN_TYPE.equals(aLayer.getType()) && !hasLinkFeature(aService, aLayer)) { threshold = Integer.MAX_VALUE; // No filtering } else { // Chains and arcs contain relations that are rendered as lines on the light // window background - need to make sure there is some contrast, so we cannot use // the full palette. threshold = LIGHTNESS_FILTER_THRESHOLD; } coloringStrategy = staticColor(nextPaletteEntry(PALETTE_PASTEL, aColorQueues, threshold)); } else { String[] palette; if (WebAnnoConst.SPAN_TYPE.equals(aLayer.getType()) && !hasLinkFeature(aService, aLayer)) { palette = PALETTE_NORMAL; } else { // Chains and arcs contain relations that are rendered as lines on the light // window background - need to make sure there is some contrast, so we cannot use // the full palette. palette = PALETTE_NORMAL_FILTERED; } coloringStrategy = labelHashBasedColor(palette); } return coloringStrategy; } private static boolean hasLinkFeature(AnnotationSchemaService aService, AnnotationLayer aLayer) { for (AnnotationFeature feature : aService.listAnnotationFeature(aLayer)) { if (!LinkMode.NONE.equals(feature.getLinkMode())) { return true; } } return false; } private static String nextPaletteEntry(String[] aPalette, Map<String[], Queue<String>> aPaletteCursors, int aThreshold) { // Initialize the color queue if not already done so Queue<String> colorQueue = aPaletteCursors.get(aPalette); if (colorQueue == null) { colorQueue = new LinkedList<>(asList(aPalette)); aPaletteCursors.put(aPalette, colorQueue); } // Look for a suitable color String color = colorQueue.poll(); String firstColor = color; while (isTooLight(color, aThreshold)) { colorQueue.add(color); color = colorQueue.poll(); // Check if we have seen the same color already (object equality!) if (color == firstColor) { throw new IllegalStateException("Palette out of colors!"); } } colorQueue.add(color); return color; } /** * Filter out too light colors from the palette - those that do not show propely on a ligth * background. The threshold controls what to filter. * * @param aPalette * the palette. * @param aThreshold * the lightness threshold (0 = black, 255 = white) * @return the filtered palette. */ public static String[] filterLightColors(String[] aPalette, int aThreshold) { List<String> filtered = new ArrayList<String>(); for (String color : aPalette) { if (!isTooLight(color, aThreshold)) { filtered.add(color); } } return (String[]) filtered.toArray(new String[filtered.size()]); } public static boolean isTooLight(String aColor, int aThreshold) { // http://24ways.org/2010/calculating-color-contrast/ // http://stackoverflow.com/questions/11867545/change-text-color-based-on-brightness-of-the-covered-background-area int r = Integer.valueOf(aColor.substring(1, 3), 16); int g = Integer.valueOf(aColor.substring(3, 5), 16); int b = Integer.valueOf(aColor.substring(5, 7), 16); int yiq = ((r*299)+(g*587)+(b*114))/1000; return yiq > aThreshold; } private final static int LIGHTNESS_FILTER_THRESHOLD = 180; public final static String DISABLED = "#bebebe"; public final static String[] PALETTE_PASTEL = { "#8dd3c7", "#ffffb3", "#bebada", "#fb8072", "#80b1d3", "#fdb462", "#b3de69", "#fccde5", "#d9d9d9", "#bc80bd", "#ccebc5", "#ffed6f" }; // public final static String[] PALETTE_PASTEL_FILTERED = filterLightColors(PALETTE_PASTEL, // LIGHTNESS_FILTER_THRESHOLD); public final static String[] PALETTE_NORMAL = { "#a6cee3", "#1f78b4", "#b2df8a", "#33a02c", "#fb9a99", "#e31a1c", "#fdbf6f", "#ff7f00", "#cab2d6", "#6a3d9a", "#ffff99", "#b15928" }; public final static String[] PALETTE_NORMAL_FILTERED = filterLightColors(PALETTE_NORMAL, LIGHTNESS_FILTER_THRESHOLD); public abstract String getColor(Object aObj, String aLabel); }