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
+}