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