Mercurial > gemma
comparison pkg/models/colors.go @ 3854:3fcc4e11fc00
Validate the config values of the morpho classes when saving. Also don't trigger the expensive re-calculation of the contour lines if only the colors changed.
author | Sascha L. Teichmann <sascha.teichmann@intevation.de> |
---|---|
date | Tue, 09 Jul 2019 11:31:49 +0200 |
parents | |
children | 859210ee6440 |
comparison
equal
deleted
inserted
replaced
3853:abc15a3182c7 | 3854:3fcc4e11fc00 |
---|---|
1 // This is Free Software under GNU Affero General Public License v >= 3.0 | |
2 // without warranty, see README.md and license for details. | |
3 // | |
4 // SPDX-License-Identifier: AGPL-3.0-or-later | |
5 // License-Filename: LICENSES/AGPL-3.0.txt | |
6 // | |
7 // Copyright (C) 2019 by via donau | |
8 // – Österreichische Wasserstraßen-Gesellschaft mbH | |
9 // Software engineering by Intevation GmbH | |
10 // | |
11 // Author(s): | |
12 // * Sascha L. Teichmann <sascha.teichmann@intevation.de> | |
13 // * Markus Kottländer <markus.kottlaender@intevation.de> | |
14 | |
15 package models | |
16 | |
17 import ( | |
18 "fmt" | |
19 "image/color" | |
20 "math" | |
21 "sort" | |
22 "strconv" | |
23 "strings" | |
24 ) | |
25 | |
26 type ( | |
27 ColorValue struct { | |
28 Value float64 | |
29 Color color.RGBA | |
30 } | |
31 | |
32 ColorValues []ColorValue | |
33 | |
34 ClassBreak struct { | |
35 High float64 | |
36 HasHigh bool | |
37 Low float64 | |
38 HasLow bool | |
39 Col color.RGBA | |
40 } | |
41 ) | |
42 | |
43 func (cb *ClassBreak) Color() string { | |
44 return fmt.Sprintf("#%02x%02x%02x", | |
45 cb.Col.R, | |
46 cb.Col.G, | |
47 cb.Col.B, | |
48 ) | |
49 } | |
50 | |
51 func (cc ColorValues) ClassBreaks() []ClassBreak { | |
52 | |
53 cbs := make([]ClassBreak, len(cc), len(cc)+1) | |
54 for i := range cc { | |
55 if i > 0 { | |
56 cbs[i].Low = cc[i-1].Value | |
57 cbs[i].HasLow = true | |
58 } | |
59 cbs[i].High = cc[i].Value | |
60 cbs[i].HasHigh = true | |
61 cbs[i].Col = cc[i].Color | |
62 } | |
63 if len(cc) > 0 { | |
64 cbs = append(cbs, ClassBreak{ | |
65 Col: cc[len(cc)-1].Color, | |
66 Low: cc[len(cc)-1].Value, | |
67 HasLow: true, | |
68 }) | |
69 } | |
70 | |
71 return cbs | |
72 } | |
73 | |
74 func (cc ColorValues) Interpolate(v float64) (color.RGBA, bool) { | |
75 if len(cc) == 0 || v < cc[0].Value || v > cc[len(cc)-1].Value { | |
76 return color.RGBA{}, false | |
77 } | |
78 | |
79 if len(cc) == 1 { | |
80 return cc[0].Color, cc[0].Value == v | |
81 } | |
82 | |
83 for i := 0; i < len(cc)-1; i++ { | |
84 v1 := cc[i].Value | |
85 v2 := cc[i+1].Value | |
86 if v1 <= v && v <= v2 { | |
87 // f(v1) = 0 | |
88 // f(v2) = 1 | |
89 // 0 = m*v1 + c <=> c = -m*v1 | |
90 // 1 = m*v2 + c | |
91 // (1 - 0) = m*(v2 - v1) | |
92 // m = 1/(v2 - v1) for v2 != v1 | |
93 | |
94 if v1 == v2 { | |
95 return color.RGBA{ | |
96 R: uint8((uint16(cc[i].Color.R) + uint16(cc[i+1].Color.R)) / 2), | |
97 G: uint8((uint16(cc[i].Color.G) + uint16(cc[i+1].Color.G)) / 2), | |
98 B: uint8((uint16(cc[i].Color.B) + uint16(cc[i+1].Color.B)) / 2), | |
99 A: 0xff, | |
100 }, true | |
101 } | |
102 m := 1 / (v2 - v1) | |
103 c := -m * v1 | |
104 s := v*m + c | |
105 | |
106 interpolate := func(a, b uint8) uint8 { | |
107 v := math.Round(float64(a) + (float64(b)-float64(a))*s) | |
108 if v < 0 { | |
109 return 0 | |
110 } | |
111 if v > 255 { | |
112 return 255 | |
113 } | |
114 return uint8(v) | |
115 } | |
116 | |
117 return color.RGBA{ | |
118 R: interpolate(cc[i].Color.R, cc[i+1].Color.R), | |
119 G: interpolate(cc[i].Color.G, cc[i+1].Color.G), | |
120 B: interpolate(cc[i].Color.B, cc[i+1].Color.B), | |
121 A: 0xff, | |
122 }, true | |
123 } | |
124 } | |
125 | |
126 return color.RGBA{}, false | |
127 } | |
128 | |
129 func ParseColorValues(s string) (ColorValues, error) { | |
130 | |
131 var err error | |
132 | |
133 parseFloat := func(s string) float64 { | |
134 var v float64 | |
135 if err == nil { | |
136 v, err = strconv.ParseFloat(s, 64) | |
137 } | |
138 return v | |
139 } | |
140 | |
141 parseColor := func(s string) color.RGBA { | |
142 if err != nil { | |
143 return color.RGBA{} | |
144 } | |
145 s = strings.Map(func(r rune) rune { | |
146 if ('a' <= r && r <= 'f') || ('0' <= r && r <= '9') { | |
147 return r | |
148 } | |
149 return -1 | |
150 }, strings.ToLower(s)) | |
151 | |
152 var v int64 | |
153 v, err = strconv.ParseInt(s, 16, 64) | |
154 return color.RGBA{ | |
155 R: uint8(v >> 16), | |
156 G: uint8(v >> 8), | |
157 B: uint8(v >> 0), | |
158 A: 0xff, | |
159 } | |
160 } | |
161 | |
162 lines := strings.Split(s, ",") | |
163 | |
164 // first pass: find defined colors | |
165 var defined ColorValues | |
166 | |
167 for _, line := range lines { | |
168 // ignore the lines w/o a color. | |
169 if !strings.ContainsRune(line, ':') { | |
170 continue | |
171 } | |
172 parts := strings.SplitN(line, ":", 2) | |
173 if len(parts) < 2 { | |
174 continue | |
175 } | |
176 value := parseFloat(parts[0]) | |
177 color := parseColor(parts[1]) | |
178 | |
179 defined = append(defined, ColorValue{ | |
180 Value: value, | |
181 Color: color, | |
182 }) | |
183 } | |
184 | |
185 if err != nil { | |
186 return nil, err | |
187 } | |
188 | |
189 sort.Slice(defined, func(i, j int) bool { | |
190 return defined[i].Value < defined[j].Value | |
191 }) | |
192 | |
193 // second pass: interpolate the rest | |
194 var final ColorValues | |
195 for _, line := range lines { | |
196 if idx := strings.IndexRune(line, ':'); idx >= 0 { | |
197 line = line[:idx] | |
198 } | |
199 value := parseFloat(line) | |
200 if color, ok := defined.Interpolate(value); ok { | |
201 final = append(final, ColorValue{ | |
202 Value: value, | |
203 Color: color, | |
204 }) | |
205 } | |
206 } | |
207 | |
208 return final, err | |
209 } |