Mercurial > gemma
changeset 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 | abc15a3182c7 |
children | 4a2a778f35da |
files | pkg/controllers/system.go pkg/geoserver/templates.go pkg/geoserver/templates_test.go pkg/models/colors.go |
diffstat | 4 files changed, 299 insertions(+), 208 deletions(-) [+] |
line wrap: on
line diff
--- a/pkg/controllers/system.go Tue Jul 09 08:55:00 2019 +0200 +++ b/pkg/controllers/system.go Tue Jul 09 11:31:49 2019 +0200 @@ -18,6 +18,7 @@ "database/sql" "fmt" "io/ioutil" + "log" "net/http" "strings" "sync" @@ -146,30 +147,94 @@ return } +type reconfFunc func(sql.NullString, string) (func(), error) + var ( reconfigureFuncsMu sync.Mutex - reconfigureFuncs = map[string]func(){} + reconfigureFuncs = map[string]reconfFunc{} ) -func registerReconfigureFunc(key string, fn func()) { +func registerReconfigureFunc(key string, fn reconfFunc) { reconfigureFuncsMu.Lock() defer reconfigureFuncsMu.Unlock() reconfigureFuncs[key] = fn } -func reconfigureFunc(key string) func() { +func reconfigureFunc(key string) reconfFunc { reconfigureFuncsMu.Lock() defer reconfigureFuncsMu.Unlock() return reconfigureFuncs[key] } +func reconfigureClassBreaks(old sql.NullString, curr, which string, recalc func()) (func(), error) { + + // If new values are broken, don't proceed. + currCVs, err := models.ParseColorValues(curr) + if err != nil { + return nil, err + } + + doBoth := func() { + log.Printf("info: Trigger re-calculation of %s.", which) + geoserver.ReconfigureStyle(which) + recalc() + } + + if !old.Valid { + return doBoth, nil + } + + oldCVs, err := models.ParseColorValues(old.String) + if err != nil { + log.Printf("warn: old config value is broken: %v\n", err) + return doBoth, nil + } + + if len(currCVs) != len(oldCVs) { + return doBoth, nil + } + + colorChanged := false + + for i := range currCVs { + if currCVs[i].Value != oldCVs[i].Value { + return doBoth, nil + } + if currCVs[i].Color != oldCVs[i].Color { + colorChanged = true + } + } + + // Only the color changed -> no expensive recalc needed. + if colorChanged { + log.Println("info: Only colors changed.") + return func() { geoserver.ReconfigureStyle(which) }, nil + } + + return nil, nil +} + func init() { - registerReconfigureFunc("morphology_classbreaks", func() { - geoserver.ReconfigureStyle("sounding_results_contour_lines_geoserver") - }) - registerReconfigureFunc("morphology_classbreaks_compare", func() { - geoserver.ReconfigureStyle("sounding_differences") - }) + registerReconfigureFunc("morphology_classbreaks", + func(old sql.NullString, curr string) (func(), error) { + return reconfigureClassBreaks( + old, curr, + "sounding_results_contour_lines_geoserver", + func() { + log.Println( + "todo: Trigger expensive recalculation of sounding result contours.") + }) + }) + registerReconfigureFunc("morphology_classbreaks_compare", + func(old sql.NullString, curr string) (func(), error) { + return reconfigureClassBreaks( + old, curr, + "sounding_differences", + func() { + log.Println( + "todo: Trigger expensive recalculation of sounding differences contours.") + }) + }) } func setSystemSettings( @@ -210,8 +275,12 @@ return } - if !old.Valid || old.String != value { - if fn := reconfigureFunc(key); fn != nil { + if cmp := reconfigureFunc(key); cmp != nil { + var fn func() + if fn, err = cmp(old, value); err != nil { + return + } + if fn != nil { reconfigure[key] = fn } }
--- a/pkg/geoserver/templates.go Tue Jul 09 08:55:00 2019 +0200 +++ b/pkg/geoserver/templates.go Tue Jul 09 11:31:49 2019 +0200 @@ -4,27 +4,24 @@ // SPDX-License-Identifier: AGPL-3.0-or-later // License-Filename: LICENSES/AGPL-3.0.txt // -// Copyright (C) 2018 by via donau +// 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 geoserver import ( "context" "database/sql" - "fmt" - "image/color" - "math" - "sort" - "strconv" "strings" "text/template" "gemma.intevation.de/gemma/pkg/auth" + "gemma.intevation.de/gemma/pkg/models" ) const ( @@ -42,191 +39,6 @@ templateContourLinesFunc("morphology_classbreaks_compare")) } -type ( - colorClass struct { - value float64 - color color.RGBA - } - - colorClasses []colorClass - - classBreak struct { - High float64 - HasHigh bool - Low float64 - HasLow bool - color color.RGBA - } -) - -func (cb *classBreak) Color() string { - return fmt.Sprintf("#%02x%02x%02x", - cb.color.R, - cb.color.G, - cb.color.B, - ) -} - -func (cc colorClasses) toClassBreaks() []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].color = cc[i].color - } - if len(cc) > 0 { - cbs = append(cbs, classBreak{ - color: cc[len(cc)-1].color, - Low: cc[len(cc)-1].value, - HasLow: true, - }) - } - - return cbs -} - -func (cc colorClasses) 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 parseColorClasses(s string) (colorClasses, 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 colorClasses - - 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, colorClass{ - 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 colorClasses - 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, colorClass{ - value: value, - color: color, - }) - } - } - - return final, err -} - func templateContourLinesFunc(configKey string) func(string) (string, error) { return func(data string) (string, error) { return templateContourLines(data, configKey) @@ -239,7 +51,7 @@ return "", err } - var cb []classBreak + var cb []models.ClassBreak if cb, err = countourLinesClassBreaks(configKey); err != nil { return "", err @@ -252,7 +64,7 @@ return buf.String(), nil } -func countourLinesClassBreaks(configKey string) ([]classBreak, error) { +func countourLinesClassBreaks(configKey string) ([]models.ClassBreak, error) { var config string ctx := context.Background() @@ -270,10 +82,10 @@ return nil, err } - cc, err := parseColorClasses(config) + cc, err := models.ParseColorValues(config) if err != nil { return nil, err } - return cc.toClassBreaks(), nil + return cc.ClassBreaks(), nil }
--- a/pkg/geoserver/templates_test.go Tue Jul 09 08:55:00 2019 +0200 +++ b/pkg/geoserver/templates_test.go Tue Jul 09 11:31:49 2019 +0200 @@ -18,6 +18,7 @@ "testing" "text/template" + "gemma.intevation.de/gemma/pkg/models" "github.com/sergi/go-diff/diffmatchpatch" ) @@ -881,12 +882,12 @@ func TestTemplate(t *testing.T) { - ccs, err := parseColorClasses(classBreaksConfig) + ccs, err := models.ParseColorValues(classBreaksConfig) if err != nil { t.Fatalf("parsing color config failed: %v", err) } - cbs := ccs.toClassBreaks() + cbs := ccs.ClassBreaks() tmpl, err := template.New("test").Parse(sldTmplTxt) if err != nil {
--- /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 +}