Skip to content

Commit e87b8f8

Browse files
tulinkryVladimir Kotal
authored andcommitted
improving the color palette for annotations (#2733)
fixes #1361
1 parent 15d77e3 commit e87b8f8

File tree

7 files changed

+356
-17
lines changed

7 files changed

+356
-17
lines changed

opengrok-indexer/src/main/java/org/opengrok/indexer/history/Annotation.java

Lines changed: 63 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,27 +18,32 @@
1818
*/
1919

2020
/*
21-
* Copyright (c) 2007, 2018 Oracle and/or its affiliates. All rights reserved.
21+
* Copyright (c) 2007, 2019 Oracle and/or its affiliates. All rights reserved.
22+
* Portions Copyright (c) 2019, Krystof Tulinger <[email protected]>.
2223
*/
2324

2425
package org.opengrok.indexer.history;
2526

27+
import org.opengrok.indexer.logger.LoggerFactory;
28+
import org.opengrok.indexer.util.Color;
29+
import org.opengrok.indexer.util.LazilyInstantiate;
30+
import org.opengrok.indexer.util.RainbowColorGenerator;
31+
import org.opengrok.indexer.web.Util;
32+
2633
import java.io.IOException;
2734
import java.io.StringWriter;
2835
import java.io.Writer;
2936
import java.util.ArrayList;
37+
import java.util.Comparator;
3038
import java.util.HashMap;
3139
import java.util.HashSet;
3240
import java.util.Iterator;
3341
import java.util.List;
3442
import java.util.Map;
3543
import java.util.Map.Entry;
3644
import java.util.Set;
37-
3845
import java.util.logging.Logger;
39-
40-
import org.opengrok.indexer.logger.LoggerFactory;
41-
import org.opengrok.indexer.web.Util;
46+
import java.util.stream.Collectors;
4247

4348
/**
4449
* Class representing file annotation, i.e., revision and author for the last
@@ -51,6 +56,7 @@ public class Annotation {
5156
private final List<Line> lines = new ArrayList<>();
5257
private final Map<String, String> desc = new HashMap<>();
5358
private final Map<String, Integer> fileVersions = new HashMap<>(); // maps revision to file version
59+
private final LazilyInstantiate<Map<String, String>> colors = LazilyInstantiate.using(this::generateColors);
5460
private int widestRevision;
5561
private int widestAuthor;
5662
private final String filename;
@@ -190,6 +196,58 @@ public int getFileVersionsCount() {
190196
return fileVersions.size();
191197
}
192198

199+
/**
200+
* Return the color palette for the annotated file.
201+
*
202+
* @return map of (revision, css color string) for each revision in {@code getRevisions()}
203+
* @see #generateColors()
204+
*/
205+
public Map<String, String> getColors() {
206+
return colors.get();
207+
}
208+
209+
/**
210+
* Generate the color palette for the annotated revisions.
211+
* <p>
212+
* First, take into account revisions which are tracked in history fields
213+
* and compute their color. Secondly, use all other revisions in order
214+
* which is undefined and generate the rest of the colors for them.
215+
*
216+
* @return map of (revision, css color string) for each revision in {@code getRevisions()}
217+
* @see #getRevisions()
218+
*/
219+
private Map<String, String> generateColors() {
220+
List<Color> colors = RainbowColorGenerator.getOrderedColors();
221+
222+
Map<String, String> colorMap = new HashMap<>();
223+
final List<String> revisions =
224+
getRevisions()
225+
.stream()
226+
/*
227+
* Greater file version means more recent revision.
228+
* 0 file version means unknown revision (untracked by history entries).
229+
*
230+
* The result of this sort is:
231+
* 1) known revisions sorted from most recent to least recent
232+
* 2) all other revisions in non-determined order
233+
*/
234+
.sorted(Comparator.comparingInt(this::getFileVersion).reversed())
235+
.collect(Collectors.toList());
236+
237+
final int nColors = colors.size();
238+
final double colorsPerBucket = (double) nColors / getRevisions().size();
239+
240+
revisions.forEach(revision -> {
241+
final int lineVersion = getRevisions().size() - getFileVersion(revision);
242+
final double bucketTotal = colorsPerBucket * lineVersion;
243+
final int bucketIndex = (int) Math.max(Math.min(Math.floor(bucketTotal), nColors - 1), 0);
244+
Color color = colors.get(bucketIndex);
245+
colorMap.put(revision, String.format("rgb(%d, %d, %d)", color.red, color.green, color.blue));
246+
});
247+
248+
return colorMap;
249+
}
250+
193251
/** Class representing one line in the file. */
194252
private static class Line {
195253
final String revision;
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
/*
2+
* CDDL HEADER START
3+
*
4+
* The contents of this file are subject to the terms of the
5+
* Common Development and Distribution License (the "License").
6+
* You may not use this file except in compliance with the License.
7+
*
8+
* See LICENSE.txt included in this distribution for the specific
9+
* language governing permissions and limitations under the License.
10+
*
11+
* When distributing Covered Code, include this CDDL HEADER in each
12+
* file and include the License file at LICENSE.txt.
13+
* If applicable, add the following below this CDDL HEADER, with the
14+
* fields enclosed by brackets "[]" replaced with your own identifying
15+
* information: Portions Copyright [yyyy] [name of copyright owner]
16+
*
17+
* CDDL HEADER END
18+
*/
19+
20+
/*
21+
* Copyright (c) 2019, Krystof Tulinger <[email protected]>.
22+
*/
23+
24+
package org.opengrok.indexer.util;
25+
26+
public class Color {
27+
public final int red;
28+
public final int green;
29+
public final int blue;
30+
31+
public Color(int red, int green, int blue) {
32+
this.red = red;
33+
this.green = green;
34+
this.blue = blue;
35+
}
36+
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
/*
2+
* CDDL HEADER START
3+
*
4+
* The contents of this file are subject to the terms of the
5+
* Common Development and Distribution License (the "License").
6+
* You may not use this file except in compliance with the License.
7+
*
8+
* See LICENSE.txt included in this distribution for the specific
9+
* language governing permissions and limitations under the License.
10+
*
11+
* When distributing Covered Code, include this CDDL HEADER in each
12+
* file and include the License file at LICENSE.txt.
13+
* If applicable, add the following below this CDDL HEADER, with the
14+
* fields enclosed by brackets "[]" replaced with your own identifying
15+
* information: Portions Copyright [yyyy] [name of copyright owner]
16+
*
17+
* CDDL HEADER END
18+
*/
19+
20+
/*
21+
* Copyright 2000-2016 JetBrains s.r.o.
22+
*
23+
* Licensed under the Apache License, Version 2.0 (the "License");
24+
* you may not use this file except in compliance with the License.
25+
* You may obtain a copy of the License at
26+
*
27+
* http://www.apache.org/licenses/LICENSE-2.0
28+
*
29+
* Unless required by applicable law or agreed to in writing, software
30+
* distributed under the License is distributed on an "AS IS" BASIS,
31+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
32+
* See the License for the specific language governing permissions and
33+
* limitations under the License.
34+
*/
35+
36+
/*
37+
* Portions Copyright (c) 2019, Krystof Tulinger <[email protected]>.
38+
*/
39+
40+
41+
package org.opengrok.indexer.util;
42+
43+
public class ColorUtil {
44+
45+
/**
46+
* Return Color object from string. The following formats are allowed:
47+
* {@code A1B2C3},
48+
* {@code abc123}
49+
*
50+
* @param str hex string
51+
* @return Color object
52+
*/
53+
public static Color fromHex(String str) {
54+
if (str.length() != 6) {
55+
throw new IllegalArgumentException("unsupported length:" + str);
56+
}
57+
58+
return new Color(parseHexNumber(str, 0), parseHexNumber(str, 2), parseHexNumber(str, 4));
59+
}
60+
61+
private static int parseHexNumber(String str, int pos) {
62+
return 16 * convertToDecimal(str, pos) + convertToDecimal(str, pos + 1);
63+
}
64+
65+
private static int convertToDecimal(String str, int pos) {
66+
char ch = str.charAt(pos);
67+
if (ch >= '0' && ch <= '9') {
68+
return ch - '0';
69+
}
70+
if (ch >= 'A' && ch <= 'F') {
71+
return ch - 'A' + 10;
72+
}
73+
if (ch >= 'a' && ch <= 'f') {
74+
return ch - 'a' + 10;
75+
}
76+
throw new IllegalArgumentException("unsupported char at " + pos + ":" + str);
77+
}
78+
}
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
/*
2+
* CDDL HEADER START
3+
*
4+
* The contents of this file are subject to the terms of the
5+
* Common Development and Distribution License (the "License").
6+
* You may not use this file except in compliance with the License.
7+
*
8+
* See LICENSE.txt included in this distribution for the specific
9+
* language governing permissions and limitations under the License.
10+
*
11+
* When distributing Covered Code, include this CDDL HEADER in each
12+
* file and include the License file at LICENSE.txt.
13+
* If applicable, add the following below this CDDL HEADER, with the
14+
* fields enclosed by brackets "[]" replaced with your own identifying
15+
* information: Portions Copyright [yyyy] [name of copyright owner]
16+
*
17+
* CDDL HEADER END
18+
*/
19+
20+
/*
21+
* Copyright 2000-2016 JetBrains s.r.o.
22+
*
23+
* Licensed under the Apache License, Version 2.0 (the "License");
24+
* you may not use this file except in compliance with the License.
25+
* You may obtain a copy of the License at
26+
*
27+
* http://www.apache.org/licenses/LICENSE-2.0
28+
*
29+
* Unless required by applicable law or agreed to in writing, software
30+
* distributed under the License is distributed on an "AS IS" BASIS,
31+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
32+
* See the License for the specific language governing permissions and
33+
* limitations under the License.
34+
*/
35+
36+
/*
37+
* Portions Copyright (c) 2019, Krystof Tulinger <[email protected]>.
38+
*/
39+
40+
41+
package org.opengrok.indexer.util;
42+
43+
import java.util.ArrayList;
44+
import java.util.Arrays;
45+
import java.util.Collections;
46+
import java.util.List;
47+
48+
import static org.opengrok.indexer.util.ColorUtil.fromHex;
49+
50+
public class RainbowColorGenerator {
51+
52+
private static final int COLORS_BETWEEN_ANCHORS = 4;
53+
private static final Color[] STOP_COLORS = new Color[]{
54+
fromHex("eaffe2"),
55+
fromHex("d9e4f9"),
56+
fromHex("d1d1d1"),
57+
fromHex("fffbcf"),
58+
fromHex("ffbfc3"),
59+
};
60+
61+
/**
62+
* Get linear sequence for all stop colors.
63+
*
64+
* @return the list of colors
65+
* @see #STOP_COLORS
66+
* @see #COLORS_BETWEEN_ANCHORS
67+
*/
68+
public static List<Color> getOrderedColors() {
69+
return generateLinearColorSequence(Arrays.asList(STOP_COLORS), COLORS_BETWEEN_ANCHORS);
70+
}
71+
72+
/**
73+
* Generate linear color sequence between given stop colors as {@code anchorColors} and with {@code colorsBetweenAnchors} number of intermediary steps between them.
74+
*
75+
* @param anchorColors the stop colors
76+
* @param colorsBetweenAnchors number of steps between each pair of stop colors
77+
* @return the list of colors
78+
*/
79+
public static List<Color> generateLinearColorSequence(List<? extends Color> anchorColors, int colorsBetweenAnchors) {
80+
assert colorsBetweenAnchors >= 0;
81+
if (anchorColors.size() == 1) {
82+
return Collections.singletonList(anchorColors.get(0));
83+
}
84+
85+
int segmentCount = anchorColors.size() - 1;
86+
List<Color> result = new ArrayList<>(anchorColors.size() + segmentCount * colorsBetweenAnchors);
87+
result.add(anchorColors.get(0));
88+
89+
for (int i = 0; i < segmentCount; i++) {
90+
Color color1 = anchorColors.get(i);
91+
Color color2 = anchorColors.get(i + 1);
92+
93+
List<Color> linearColors = generateLinearColorSequence(color1, color2, colorsBetweenAnchors);
94+
95+
// skip first element from sequence to avoid duplication from connected segments
96+
result.addAll(linearColors.subList(1, linearColors.size()));
97+
}
98+
99+
return result;
100+
}
101+
102+
/**
103+
* Generate linear color sequence between two given colors.
104+
*
105+
* @param color1 the first color (to be included in the sequence)
106+
* @param color2 the last color (to be included in the sequence)
107+
* @param colorsBetweenAnchors number of colors between the two colors
108+
* @return the list of colors
109+
*/
110+
public static List<Color> generateLinearColorSequence(Color color1, Color color2, int colorsBetweenAnchors) {
111+
assert colorsBetweenAnchors >= 0;
112+
113+
List<Color> result = new ArrayList<>(colorsBetweenAnchors + 2);
114+
result.add(color1);
115+
116+
for (int i = 1; i <= colorsBetweenAnchors; i++) {
117+
float ratio = (float) i / (colorsBetweenAnchors + 1);
118+
119+
result.add(new Color(
120+
ratio(color1.red, color2.red, ratio),
121+
ratio(color1.green, color2.green, ratio),
122+
ratio(color1.blue, color2.blue, ratio)
123+
));
124+
}
125+
126+
result.add(color2);
127+
return result;
128+
}
129+
130+
private static int ratio(int val1, int val2, float ratio) {
131+
int value = (int) (val1 + (val2 - val1) * ratio);
132+
return Math.max(Math.min(value, 255), 0);
133+
}
134+
}

opengrok-indexer/src/main/java/org/opengrok/indexer/web/Util.java

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
* Copyright (c) 2005, 2019, Oracle and/or its affiliates. All rights reserved.
2222
* Portions Copyright 2011 Jens Elkner.
2323
* Portions Copyright (c) 2017-2018, Chris Fraire <[email protected]>.
24+
* Portions Copyright (c) 2019, Krystof Tulinger <[email protected]>.
2425
*/
2526

2627
package org.opengrok.indexer.web;
@@ -743,16 +744,8 @@ public static void readableLine(int num, Writer out, Annotation annotation,
743744
if (enabled) {
744745
out.write(anchorClassStart);
745746
out.write("r");
746-
if (annotation.getFileVersion(r) != 0) {
747-
/*
748-
version number, 1 is the most recent
749-
generates css classes version_color_n
750-
*/
751-
int versionNumber = Math.max(1,
752-
annotation.getFileVersionsCount()
753-
- annotation.getFileVersion(r) + 1);
754-
out.write(" version_color_" + versionNumber);
755-
}
747+
out.write("\" style=\"background-color: ");
748+
out.write(annotation.getColors().getOrDefault(r, "inherit"));
756749
out.write("\" href=\"");
757750
out.write(URIEncode(annotation.getFilename()));
758751
out.write("?a=true&amp;r=");
@@ -769,7 +762,16 @@ public static void readableLine(int num, Writer out, Annotation annotation,
769762
out.write(closeQuotedTag);
770763
}
771764
StringBuilder buf = new StringBuilder();
765+
final boolean most_recent_revision = annotation.getFileVersion(r) == annotation.getRevisions().size();
766+
// print an asterisk for the most recent revision
767+
if (most_recent_revision) {
768+
buf.append("<span class=\"most_recent_revision\">");
769+
buf.append('*');
770+
}
772771
htmlize(r, buf);
772+
if (most_recent_revision) {
773+
buf.append("</span>"); // recent revision span
774+
}
773775
out.write(buf.toString());
774776
buf.setLength(0);
775777
if (enabled) {

0 commit comments

Comments
 (0)