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 }