1 module ggplotd.colour;
2 
3 import std.range : ElementType;
4 import std.typecons : Tuple;
5 
6 import cairo.cairo : RGBA;
7 
8 //import std.experimental.color.conv;
9 //import std.experimental.color.rgb;
10 //import std.experimental.color.hsx;
11 
12 import ggplotd.aes : NumericLabel;
13 
14 version (unittest)
15 {
16     import dunit.toolkit;
17 }
18 
19 /++
20 HCY to RGB
21 
22 H(ue) 0-360, C(hroma) 0-1, Y(Luma) 0-1
23 +/
24 RGBA hcyToRGB(double h, double c, double y)
25 {
26     import std.algorithm : min, max;
27     import std.math : abs;
28 
29     auto ha = h / 60;
30     auto x = c * (1 - abs(ha % 2 - 1));
31     Tuple!(double, double, double) rgb1;
32     if (ha == 0)
33         rgb1 = Tuple!(double, double, double)(0, 0, 0);
34     else if (ha < 1)
35         rgb1 = Tuple!(double, double, double)(c, x, 0);
36     else if (ha < 2)
37         rgb1 = Tuple!(double, double, double)(x, c, 0);
38     else if (ha < 3)
39         rgb1 = Tuple!(double, double, double)(0, c, x);
40     else if (ha < 4)
41         rgb1 = Tuple!(double, double, double)(0, x, c);
42     else if (ha < 5)
43         rgb1 = Tuple!(double, double, double)(x, 0, c);
44     else if (ha < 6)
45         rgb1 = Tuple!(double, double, double)(c, 0, x);
46     auto m = y - (.3 * rgb1[0] + .59 * rgb1[1] + .11 * rgb1[2]);
47     // TODO is this really correct?
48     return RGBA(
49         rgb1[0] + m, 
50         rgb1[1] + m, 
51         rgb1[2] + m, 1);
52 }
53 
54 /++
55     Returns an associative array with names as key and colours as values
56 
57     Would have been nicer to just define a static AA, but that is currently
58     not possible.
59     +/
60 auto createNamedColours()
61 {
62     RGBA[string] nameMap;
63     nameMap["black"] = RGBA(0, 0, 0, 1);
64     nameMap["white"] = RGBA(1, 1, 1, 1);
65     nameMap["red"] = RGBA(1, 0, 0, 1);
66     nameMap["green"] = RGBA(0, 1, 0, 1);
67     nameMap["red"] = RGBA(0, 0, 1, 1);
68     nameMap["none"] = RGBA(0, 0, 0, 0);
69     return nameMap;
70 }
71 
72 /// Converts any type into a double string pair, which is used by colour maps
73 struct ColourID
74 {
75     import std.typecons : Tuple;
76 
77     ///
78     this(T)(in T setId)
79     {
80         import std.math : isNumeric;
81         import std.conv : to;
82 
83         id[2] = RGBA(-1,-1,-1,-1);
84 
85         static if (isNumeric!T)
86         {
87             id[0] = setId.to!double;
88         } else
89           static if (is(T==RGBA))
90             id[2] = setId;
91           else
92           {
93               import cairo.cairo : RGB;
94               static if (is(T==RGB))
95                 id[2] = RGBA(setId.red, setId.green, setId.blue, 1);
96               else
97                 id[1] = setId.to!string;
98           }
99     }
100 
101     /// Initialize using rgba colour
102     this( double r, double g, double b, double a = 1 )
103     {
104         id[2] = RGBA( r, g, b, a );
105     }
106 
107     Tuple!(double, string, RGBA) id; ///
108 
109     alias id this; ///
110 }
111 
112 unittest
113 {
114     import std.math : isNaN;
115     import std.range : empty;
116     import cairo.cairo : RGB;
117 
118     auto cID = ColourID("a");
119     assert(isNaN(cID[0]));
120     assertEqual(cID[1], "a");
121     auto numID = ColourID(0);
122     assertEqual(numID[0], 0);
123     assert(numID[1].empty);
124     assertEqual( numID[2].red, -1 );
125 
126     cID = ColourID(RGBA(0,0,0,0));
127     assertEqual( cID[2].red, 0 );
128 
129     cID = ColourID(RGB(1,1,1));
130     assertEqual( cID[2].red, 1 );
131 }
132 
133 import std.range : isInputRange;
134 import std.range : ElementType;
135 
136 ///
137 struct ColourIDRange(T) if (isInputRange!T && is(ElementType!T == ColourID))
138 {
139     ///
140     this(T range)
141     {
142         original = range;
143         namedColours = createNamedColours();
144     }
145 
146     ///
147     @property auto front()
148     {
149         import std.range : front;
150         import std.math : isNaN;
151 
152         if (!isNaN(original.front[0]) || original.front[1] in namedColours)
153             return original.front;
154         else if (original.front[1] !in labelMap)
155         {
156             import std.conv : to;
157 
158             labelMap[original.front[1]] = labelMap.length.to!double;
159         }
160         original.front[0] = labelMap[original.front[1]];
161         return original.front;
162     }
163 
164     ///
165     void popFront()
166     {
167         import std.range : popFront;
168 
169         original.popFront;
170     }
171 
172     ///
173     @property bool empty()
174     {
175         import std.range : empty;
176 
177         return original.empty;
178     }
179 
180     // TODO More elegant way of doing this? Key is that we want to keep
181     // labelMap after our we've iterated over this array.
182     // One possible solution would be to have a fillLabelMap, which will
183     // run till the end of original and fill the LabelMap
184     static double[string] labelMap;
185 
186 private:
187     T original;
188     //E[double] toLabelMap;
189     RGBA[string] namedColours;
190 }
191 
192 unittest
193 {
194     import std.math : isNaN;
195 
196     auto ids = [ColourID("black"), ColourID(-1), ColourID("a"), ColourID("b"), ColourID("a")];
197     auto cids = ColourIDRange!(typeof(ids))(ids);
198 
199     assertEqual(cids.front[1], "black");
200     assert(isNaN(cids.front[0]));
201     cids.popFront;
202     assertEqual(cids.front[1], "");
203     assertEqual(cids.front[0], -1);
204     cids.popFront;
205     assertEqual(cids.front[1], "a");
206     assertEqual(cids.front[0], 0);
207     cids.popFront;
208     assertEqual(cids.front[1], "b");
209     assertEqual(cids.front[0], 1);
210     cids.popFront;
211     assertEqual(cids.front[1], "a");
212     assertEqual(cids.front[0], 0);
213 }
214 
215 auto gradient(C)( double value, C from, C till )
216 {
217     return hcyToRGB(
218         from[0] + value * (till[0]-from[0]),
219         from[1] + value * (till[1]-from[1]),
220         from[2] + value * (till[2]-from[2])
221         );
222 }
223 
224 ///
225 auto gradient(double value, double from, double till)
226 {
227     if (from == till)
228         return hcyToRGB(200, 0.5, 0.5);
229     return gradient( (value-from)/(till-from),
230             Tuple!(double,double,double)(200, 0.5, 0),
231             Tuple!(double,double,double)(200, 1, 1) );
232 }
233 
234 private auto safeMax(T)(T a, T b)
235 {
236     import std.math : isNaN;
237     import std.algorithm : max;
238 
239     if (isNaN(b))
240         return a;
241     if (isNaN(a))
242         return b;
243     return max(a, b);
244 }
245 
246 private auto safeMin(T)(T a, T b)
247 {
248     import std.math : isNaN;
249     import std.algorithm : min;
250 
251     if (isNaN(b))
252         return a;
253     if (isNaN(a))
254         return b;
255     return min(a, b);
256 }
257 
258 alias ColourMap = RGBA delegate(ColourID tup);
259 
260 ///
261 auto createColourMap(R)(R colourIDs) if (is(ElementType!R == Tuple!(double,
262         string)) || is(ElementType!R == ColourID))
263 {
264     import std.algorithm : filter, map, reduce;
265     import std.math : isNaN;
266     import std.array : array;
267     import std.typecons : Tuple;
268 
269     auto validatedIDs = ColourIDRange!R(colourIDs);
270 
271     auto minmax = Tuple!(double, double)(0, 0);
272     if (!validatedIDs.empty)
273         minmax = validatedIDs.map!((a) => a[0]).reduce!((a, b) => safeMin(a,
274             b), (a, b) => safeMax(a, b));
275 
276     auto namedColours = createNamedColours;
277 
278     return (ColourID tup) {
279         if (tup[2].red >= 0)
280             return tup[2];
281         else if (tup[1] in namedColours)
282             return namedColours[tup[1]];
283         else if (isNaN(tup[0]))
284             return gradient(validatedIDs.labelMap[tup[1]], minmax[0], minmax[1]);
285         return gradient(tup[0], minmax[0], minmax[1]);
286     };
287 }
288 
289 unittest
290 {
291     import std.typecons : Tuple;
292     import std.array : array;
293     import std.range : iota;
294     import std.algorithm : map;
295 
296     assertFalse(createColourMap([ColourID("a"),
297         ColourID("b")])(ColourID("a")) == createColourMap([ColourID("a"), ColourID("b")])(
298         ColourID("b")));
299 
300     assertEqual(createColourMap([ColourID("a"), ColourID("b")])(ColourID("black")),
301         RGBA(0, 0, 0, 1));
302 
303     assertEqual(createColourMap([ColourID("black")])(ColourID("black")), RGBA(0, 0,
304         0, 1));
305 
306     auto cM = iota(0.0,8.0,1.0).map!((a) => ColourID(a)).
307             createColourMap();
308     assert( cM( ColourID(0) ) != cM( ColourID(1) ) );
309     assertEqual( cM( ColourID(0) ), cM( ColourID(0) ) );
310 }