Mercurial > gemma
diff 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 |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/pkg/models/colors.go Tue Jul 09 11:31:49 2019 +0200 @@ -0,0 +1,209 @@ +// This is Free Software under GNU Affero General Public License v >= 3.0 +// without warranty, see README.md and license for details. +// +// SPDX-License-Identifier: AGPL-3.0-or-later +// License-Filename: LICENSES/AGPL-3.0.txt +// +// Copyright (C) 2019 by via donau +// – Österreichische Wasserstraßen-Gesellschaft mbH +// Software engineering by Intevation GmbH +// +// Author(s): +// * Sascha L. Teichmann <sascha.teichmann@intevation.de> +// * Markus Kottländer <markus.kottlaender@intevation.de> + +package models + +import ( + "fmt" + "image/color" + "math" + "sort" + "strconv" + "strings" +) + +type ( + ColorValue struct { + Value float64 + Color color.RGBA + } + + ColorValues []ColorValue + + ClassBreak struct { + High float64 + HasHigh bool + Low float64 + HasLow bool + Col color.RGBA + } +) + +func (cb *ClassBreak) Color() string { + return fmt.Sprintf("#%02x%02x%02x", + cb.Col.R, + cb.Col.G, + cb.Col.B, + ) +} + +func (cc ColorValues) ClassBreaks() []ClassBreak { + + cbs := make([]ClassBreak, len(cc), len(cc)+1) + for i := range cc { + if i > 0 { + cbs[i].Low = cc[i-1].Value + cbs[i].HasLow = true + } + cbs[i].High = cc[i].Value + cbs[i].HasHigh = true + cbs[i].Col = cc[i].Color + } + if len(cc) > 0 { + cbs = append(cbs, ClassBreak{ + Col: cc[len(cc)-1].Color, + Low: cc[len(cc)-1].Value, + HasLow: true, + }) + } + + return cbs +} + +func (cc ColorValues) Interpolate(v float64) (color.RGBA, bool) { + if len(cc) == 0 || v < cc[0].Value || v > cc[len(cc)-1].Value { + return color.RGBA{}, false + } + + if len(cc) == 1 { + return cc[0].Color, cc[0].Value == v + } + + for i := 0; i < len(cc)-1; i++ { + v1 := cc[i].Value + v2 := cc[i+1].Value + if v1 <= v && v <= v2 { + // f(v1) = 0 + // f(v2) = 1 + // 0 = m*v1 + c <=> c = -m*v1 + // 1 = m*v2 + c + // (1 - 0) = m*(v2 - v1) + // m = 1/(v2 - v1) for v2 != v1 + + if v1 == v2 { + return color.RGBA{ + R: uint8((uint16(cc[i].Color.R) + uint16(cc[i+1].Color.R)) / 2), + G: uint8((uint16(cc[i].Color.G) + uint16(cc[i+1].Color.G)) / 2), + B: uint8((uint16(cc[i].Color.B) + uint16(cc[i+1].Color.B)) / 2), + A: 0xff, + }, true + } + m := 1 / (v2 - v1) + c := -m * v1 + s := v*m + c + + interpolate := func(a, b uint8) uint8 { + v := math.Round(float64(a) + (float64(b)-float64(a))*s) + if v < 0 { + return 0 + } + if v > 255 { + return 255 + } + return uint8(v) + } + + return color.RGBA{ + R: interpolate(cc[i].Color.R, cc[i+1].Color.R), + G: interpolate(cc[i].Color.G, cc[i+1].Color.G), + B: interpolate(cc[i].Color.B, cc[i+1].Color.B), + A: 0xff, + }, true + } + } + + return color.RGBA{}, false +} + +func ParseColorValues(s string) (ColorValues, error) { + + var err error + + parseFloat := func(s string) float64 { + var v float64 + if err == nil { + v, err = strconv.ParseFloat(s, 64) + } + return v + } + + parseColor := func(s string) color.RGBA { + if err != nil { + return color.RGBA{} + } + s = strings.Map(func(r rune) rune { + if ('a' <= r && r <= 'f') || ('0' <= r && r <= '9') { + return r + } + return -1 + }, strings.ToLower(s)) + + var v int64 + v, err = strconv.ParseInt(s, 16, 64) + return color.RGBA{ + R: uint8(v >> 16), + G: uint8(v >> 8), + B: uint8(v >> 0), + A: 0xff, + } + } + + lines := strings.Split(s, ",") + + // first pass: find defined colors + var defined ColorValues + + for _, line := range lines { + // ignore the lines w/o a color. + if !strings.ContainsRune(line, ':') { + continue + } + parts := strings.SplitN(line, ":", 2) + if len(parts) < 2 { + continue + } + value := parseFloat(parts[0]) + color := parseColor(parts[1]) + + defined = append(defined, ColorValue{ + Value: value, + Color: color, + }) + } + + if err != nil { + return nil, err + } + + sort.Slice(defined, func(i, j int) bool { + return defined[i].Value < defined[j].Value + }) + + // second pass: interpolate the rest + var final ColorValues + for _, line := range lines { + if idx := strings.IndexRune(line, ':'); idx >= 0 { + line = line[:idx] + } + value := parseFloat(line) + if color, ok := defined.Interpolate(value); ok { + final = append(final, ColorValue{ + Value: value, + Color: color, + }) + } + } + + return final, err +}