# HG changeset patch # User Sascha L. Teichmann # Date 1562594642 -7200 # Node ID 5983eec9436cf632eb2f14168c801b98617d4316 # Parent cc585e068ca0239e9cc5b9b3839588c0881ac75a# Parent 40b28a8c0aaad6ad432ae4f6109105c556eecce5 Merged sld-colors branch back into default branch. diff -r cc585e068ca0 -r 5983eec9436c 3rdpartylibs.sh --- a/3rdpartylibs.sh Mon Jul 08 15:45:42 2019 +0200 +++ b/3rdpartylibs.sh Mon Jul 08 16:04:02 2019 +0200 @@ -44,6 +44,10 @@ go get -u -v gonum.org/v1/gonum/stat # BSD-3-Clause +go get -u -v github.com/sergi/go-diff/diffmatchpatch +# MIT +# Only used in tests. + # Only needed when generating SVG graphics for debugging. # go get -u -v github.com/ajstarks/svgo # Attribution 3.0 United States (CC BY 3.0 US) diff -r cc585e068ca0 -r 5983eec9436c pkg/controllers/system.go --- a/pkg/controllers/system.go Mon Jul 08 15:45:42 2019 +0200 +++ b/pkg/controllers/system.go Mon Jul 08 16:04:02 2019 +0200 @@ -20,10 +20,12 @@ "io/ioutil" "net/http" "strings" + "sync" "github.com/gorilla/mux" "gemma.intevation.de/gemma/pkg/config" + "gemma.intevation.de/gemma/pkg/geoserver" "gemma.intevation.de/gemma/pkg/models" ) @@ -39,6 +41,11 @@ SELECT config_key, config_val FROM sys_admin.system_config` + getConfigSQL = ` +SELECT config_val +FROM sys_admin.system_config +WHERE config_key = $1` + updateSettingSQL = ` INSERT INTO sys_admin.system_config (config_key, config_val) VALUES ($1, $2) @@ -139,6 +146,32 @@ return } +var ( + reconfigureFuncsMu sync.Mutex + reconfigureFuncs = map[string]func(){} +) + +func registerReconfigureFunc(key string, fn func()) { + reconfigureFuncsMu.Lock() + defer reconfigureFuncsMu.Unlock() + reconfigureFuncs[key] = fn +} + +func reconfigureFunc(key string) func() { + reconfigureFuncsMu.Lock() + defer reconfigureFuncsMu.Unlock() + return reconfigureFuncs[key] +} + +func init() { + registerReconfigureFunc("morphology_classbreaks", func() { + geoserver.ReconfigureStyle("sounding_results_contour_lines_geoserver") + }) + registerReconfigureFunc("morphology_classbreaks_compare", func() { + geoserver.ReconfigureStyle("sounding_differences") + }) +} + func setSystemSettings( input interface{}, req *http.Request, @@ -159,8 +192,30 @@ return } defer setStmt.Close() + var getStmt *sql.Stmt + if getStmt, err = tx.PrepareContext(ctx, getConfigSQL); err != nil { + return + } + defer getStmt.Close() + + reconfigure := map[string]func(){} for key, value := range *settings { + var old sql.NullString + err = getStmt.QueryRowContext(ctx, key).Scan(&old) + switch { + case err == sql.ErrNoRows: + old.Valid, err = false, nil + case err != nil: + return + } + + if !old.Valid || old.String != value { + if fn := reconfigureFunc(key); fn != nil { + reconfigure[key] = fn + } + } + if _, err = setStmt.ExecContext(ctx, key, value); err != nil { return } @@ -170,6 +225,10 @@ return } + for _, fn := range reconfigure { + fn() + } + jr = JSONResult{ Code: http.StatusCreated, Result: struct { diff -r cc585e068ca0 -r 5983eec9436c pkg/geoserver/boot.go --- a/pkg/geoserver/boot.go Mon Jul 08 15:45:42 2019 +0200 +++ b/pkg/geoserver/boot.go Mon Jul 08 16:04:02 2019 +0200 @@ -23,6 +23,7 @@ "net/http" "net/url" "strings" + "sync" "golang.org/x/net/html/charset" @@ -391,6 +392,23 @@ return nil } +var ( + stylePreprocessorsMu sync.Mutex + stylePreprocessors = map[string]func(string) (string, error){} +) + +func RegisterStylePreprocessor(name string, processor func(string) (string, error)) { + stylePreprocessorsMu.Lock() + defer stylePreprocessorsMu.Unlock() + stylePreprocessors[name] = processor +} + +func FindStylePreprocessor(name string) func(string) (string, error) { + stylePreprocessorsMu.Lock() + defer stylePreprocessorsMu.Unlock() + return stylePreprocessors[name] +} + func updateStyle(entry *models.IntEntry, create bool) error { log.Printf("info: creating style %s\n", entry.Name) @@ -401,6 +419,12 @@ return err } + if processor := FindStylePreprocessor(entry.Name); processor != nil { + if data, err = processor(data); err != nil { + return err + } + } + var ( geoURL = config.GeoServerURL() user = config.GeoServerUser() diff -r cc585e068ca0 -r 5983eec9436c pkg/geoserver/templates.go --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/pkg/geoserver/templates.go Mon Jul 08 16:04:02 2019 +0200 @@ -0,0 +1,279 @@ +// 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) 2018 by via donau +// – Österreichische Wasserstraßen-Gesellschaft mbH +// Software engineering by Intevation GmbH +// +// Author(s): +// * Sascha L. Teichmann + +package geoserver + +import ( + "context" + "database/sql" + "fmt" + "image/color" + "math" + "sort" + "strconv" + "strings" + "text/template" + + "gemma.intevation.de/gemma/pkg/auth" +) + +const ( + selectClassBreaksSQL = ` +SELECT config_val FROM sys_admin.system_config +WHERE config_key = $1` +) + +func init() { + RegisterStylePreprocessor( + "sounding_results_contour_lines_geoserver", + templateContourLinesFunc("morphology_classbreaks")) + RegisterStylePreprocessor( + "sounding_differences", + 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) + } +} + +func templateContourLines(data, configKey string) (string, error) { + tmpl, err := template.New("template").Parse(data) + if err != nil { + return "", err + } + + var cb []classBreak + + if cb, err = countourLinesClassBreaks(configKey); err != nil { + return "", err + } + + var buf strings.Builder + if err = tmpl.Execute(&buf, cb); err != nil { + return "", err + } + return buf.String(), nil +} + +func countourLinesClassBreaks(configKey string) ([]classBreak, error) { + + var config string + ctx := context.Background() + if err := auth.RunAs( + ctx, + "sys_admin", + func(conn *sql.Conn) error { + return conn.QueryRowContext( + ctx, + selectClassBreaksSQL, + configKey, + ).Scan(&config) + }, + ); err != nil { + return nil, err + } + + cc, err := parseColorClasses(config) + if err != nil { + return nil, err + } + + return cc.toClassBreaks(), nil +} diff -r cc585e068ca0 -r 5983eec9436c pkg/geoserver/templates_test.go --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/pkg/geoserver/templates_test.go Mon Jul 08 16:04:02 2019 +0200 @@ -0,0 +1,907 @@ +// 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) 2018 by via donau +// – Österreichische Wasserstraßen-Gesellschaft mbH +// Software engineering by Intevation GmbH +// +// Author(s): +// * Sascha L. Teichmann + +package geoserver + +import ( + "strings" + "testing" + "text/template" + + "github.com/sergi/go-diff/diffmatchpatch" +) + +const sldTmplTxt = ` + + + sounding_results_contour_lines + + sounding_results_contour_lines + + contour_line_colours + + + FeatureTypeStyle defining colour classes for height attribute + + + {{ range . -}} + + {{- if not .HasLow }} + ≤ {{ printf "%g" .High }} + + + height + {{ printf "%g" .High }} + + + {{- else if not .HasHigh }} + > {{ printf "%g" .Low }} + + + height + {{ printf "%g" .Low }} + + + {{- else }} + ≤ {{ printf "%g" .High }} + + + + height + {{ printf "%g" .Low }} + + + height + {{ printf "%g" .High }} + + + + {{- end }} + + + {{ .Color }} + 0.5 + + + + {{ end }} + + + contour_lines_emph + + + FeatureTypeStyle for emphasized contour lines + + + + + + + + + + {{ range . -}} + {{ if .HasHigh -}} + + + 0.0 + height + + {{ printf "%g" .High }} + + {{ end -}} + {{ end }} + + + 5e3 + + + 1.5 + + + + 0.0 + height + + {{ range . -}} + {{ if .HasHigh -}} + {{ printf "%g" .High }} + {{ .Color }} + {{ end -}} + {{ end }} + + + + + + + + contour_lines_label + + + FeatureTypeStyle for labels at contour lines + + + + 5e3 + + + + + 0.0 + height + + {{ range . -}} + {{ if .HasHigh -}} + + {{- printf "%g" .High -}} + + {{- printf "%g" .High -}} + + {{ end -}} + {{ end }} + + + + + 5 + + + + Avenir + Helvetica + Arial + sans-serif + + + #070707 + + + + + + + +` + +const origSLD = ` + + + sounding_results_contour_lines + + sounding_results_contour_lines + + contour_line_colours + + + FeatureTypeStyle defining colour classes for height attribute + + + + ≤ 1 + + + height + 1 + + + + + #ff00dd + 0.5 + + + + + ≤ 1.5 + + + + height + 1 + + + height + 1.5 + + + + + + #fb209e + 0.5 + + + + + ≤ 1.7 + + + + height + 1.5 + + + height + 1.7 + + + + + + #f92c85 + 0.5 + + + + + ≤ 1.9 + + + + height + 1.7 + + + height + 1.9 + + + + + + #f7396c + 0.5 + + + + + ≤ 2.1 + + + + height + 1.9 + + + height + 2.1 + + + + + + #f54652 + 0.5 + + + + + ≤ 2.3 + + + + height + 2.1 + + + height + 2.3 + + + + + + #f45239 + 0.5 + + + + + ≤ 2.5 + + + + height + 2.3 + + + height + 2.5 + + + + + + #f25f20 + 0.5 + + + + + ≤ 2.7 + + + + height + 2.5 + + + height + 2.7 + + + + + + #e46f1f + 0.5 + + + + + ≤ 2.9 + + + + height + 2.7 + + + height + 2.9 + + + + + + #d67e1e + 0.5 + + + + + ≤ 3.1 + + + + height + 2.9 + + + height + 3.1 + + + + + + #c88e1e + 0.5 + + + + + ≤ 3.3 + + + + height + 3.1 + + + height + 3.3 + + + + + + #bb9e1d + 0.5 + + + + + ≤ 3.5 + + + + height + 3.3 + + + height + 3.5 + + + + + + #adae1c + 0.5 + + + + + ≤ 4 + + + + height + 3.5 + + + height + 4 + + + + + + #8ad51a + 0.5 + + + + + ≤ 4.5 + + + + height + 4 + + + height + 4.5 + + + + + + #76b540 + 0.5 + + + + + ≤ 5 + + + + height + 4.5 + + + height + 5 + + + + + + #639566 + 0.5 + + + + + ≤ 5.5 + + + + height + 5 + + + height + 5.5 + + + + + + #4f758d + 0.5 + + + + + ≤ 6 + + + + height + 5.5 + + + height + 6 + + + + + + #3b54b3 + 0.5 + + + + + ≤ 6.5 + + + + height + 6 + + + height + 6.5 + + + + + + #2834d9 + 0.5 + + + + + ≤ 7 + + + + height + 6.5 + + + height + 7 + + + + + + #1414ff + 0.5 + + + + + > 7 + + + height + 7 + + + + + #1414ff + 0.5 + + + + + + + contour_lines_emph + + + FeatureTypeStyle for emphasized contour lines + + + + + + + + + + + + 0.0 + height + + 1 + + + + 0.0 + height + + 1.5 + + + + 0.0 + height + + 1.7 + + + + 0.0 + height + + 1.9 + + + + 0.0 + height + + 2.1 + + + + 0.0 + height + + 2.3 + + + + 0.0 + height + + 2.5 + + + + 0.0 + height + + 2.7 + + + + 0.0 + height + + 2.9 + + + + 0.0 + height + + 3.1 + + + + 0.0 + height + + 3.3 + + + + 0.0 + height + + 3.5 + + + + 0.0 + height + + 4 + + + + 0.0 + height + + 4.5 + + + + 0.0 + height + + 5 + + + + 0.0 + height + + 5.5 + + + + 0.0 + height + + 6 + + + + 0.0 + height + + 6.5 + + + + 0.0 + height + + 7 + + + + + 5e3 + + + 1.5 + + + + 0.0 + height + + 1 + #ff00dd + 1.5 + #fb209e + 1.7 + #f92c85 + 1.9 + #f7396c + 2.1 + #f54652 + 2.3 + #f45239 + 2.5 + #f25f20 + 2.7 + #e46f1f + 2.9 + #d67e1e + 3.1 + #c88e1e + 3.3 + #bb9e1d + 3.5 + #adae1c + 4 + #8ad51a + 4.5 + #76b540 + 5 + #639566 + 5.5 + #4f758d + 6 + #3b54b3 + 6.5 + #2834d9 + 7 + #1414ff + + + + + + + + + contour_lines_label + + + FeatureTypeStyle for labels at contour lines + + + + 5e3 + + + + + 0.0 + height + + 11 + 1.51.5 + 1.71.7 + 1.91.9 + 2.12.1 + 2.32.3 + 2.52.5 + 2.72.7 + 2.92.9 + 3.13.1 + 3.33.3 + 3.53.5 + 44 + 4.54.5 + 55 + 5.55.5 + 66 + 6.56.5 + 77 + + + + + + 5 + + + + Avenir + Helvetica + Arial + sans-serif + + + #070707 + + + + + + + +` + +const classBreaksConfig = `1:#ff00dd,1.5,1.7,1.9,2.1,2.3,` + + `2.5:#f25f20,2.7,2.9,3.1,3.3,3.5,` + + `4:#8ad51a,4.5,5,5.5,6,6.5,` + + `7:#1414ff` + +func TestTemplate(t *testing.T) { + + ccs, err := parseColorClasses(classBreaksConfig) + if err != nil { + t.Fatalf("parsing color config failed: %v", err) + } + + cbs := ccs.toClassBreaks() + + tmpl, err := template.New("test").Parse(sldTmplTxt) + if err != nil { + t.Fatalf("parsing template failed: %v", err) + } + + var buf strings.Builder + if err := tmpl.Execute(&buf, cbs); err != nil { + t.Fatalf("templating failed: %v", err) + } + + has := buf.String() + if has != origSLD { + dmp := diffmatchpatch.New() + diffs := dmp.DiffMain(has, origSLD, true) + t.Fatalf("Templating results differ: %s", dmp.DiffPrettyText(diffs)) + } +} diff -r cc585e068ca0 -r 5983eec9436c pkg/octree/classbreaks.go --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/pkg/octree/classbreaks.go Mon Jul 08 16:04:02 2019 +0200 @@ -0,0 +1,126 @@ +// 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 + +package octree + +import ( + "context" + "database/sql" + "errors" + "math" + "sort" + "strconv" + "strings" +) + +const ( + selectClassBreaksSQL = ` +SELECT config_val FROM sys_admin.system_config +WHERE config_key = $1` +) + +func SampleDiffHeights(min, max, step float64) []float64 { + var heights []float64 + switch { + case min >= 0: // All values positive. + for v := 0.0; v <= max; v += step { + if v >= min { + heights = append(heights, v) + } + } + case max <= 0: // All values negative. + for v := 0.0; v >= min; v -= step { + if v <= max { + heights = append(heights, v) + } + } + default: // Positive and negative. + for v := step; v <= max; v += step { + heights = append(heights, v) + } + for i, j := 0, len(heights)-1; i < j; i, j = i+1, j-1 { + heights[i], heights[j] = heights[j], heights[i] + } + for v := 0.0; v >= min; v -= step { + heights = append(heights, v) + } + } + return heights +} + +func LoadClassBreaks(ctx context.Context, tx *sql.Tx, key string) ([]float64, error) { + + var config sql.NullString + + err := tx.QueryRowContext(ctx, selectClassBreaksSQL, key).Scan(&config) + + switch { + case err == sql.ErrNoRows: + return nil, nil + case err != nil: + return nil, err + case !config.Valid: + return nil, errors.New("Invalid config string") + } + + parts := strings.Split(config.String, ",") + classes := make([]float64, 0, len(parts)) + for _, part := range parts { + if idx := strings.IndexRune(part, ':'); idx >= 0 { + part = part[idx+1:] + } + if part = strings.TrimSpace(part); part == "" { + continue + } + v, err := strconv.ParseFloat(part, 64) + if err != nil { + return nil, err + } + classes = append(classes, v) + } + + sort.Float64s(classes) + return classes, nil +} + +func InBetweenClassBreaks(cbs []float64, min float64, steps int) []float64 { + if len(cbs) < 2 || steps < 2 { + return cbs + } + + out := make([]float64, 1, len(cbs)*steps) + + out[0] = cbs[0] + + _1steps := 1 / float64(steps) + + for i := 1; i < len(cbs); i++ { + last, curr := cbs[i-1], cbs[i] + + // Gap already too small -> proceed with next gap. + diff := curr - last + if math.Abs(diff) < min { + out = append(out, curr) + continue + } + + delta := diff * _1steps + for p := last + delta; p < curr; p += delta { + out = append(out, p) + } + + out = append(out, curr) + } + + return out +} diff -r cc585e068ca0 -r 5983eec9436c pkg/octree/contours.go --- a/pkg/octree/contours.go Mon Jul 08 15:45:42 2019 +0200 +++ b/pkg/octree/contours.go Mon Jul 08 16:04:02 2019 +0200 @@ -19,35 +19,6 @@ "sync" ) -func SampleDiffHeights(min, max, step float64) []float64 { - var heights []float64 - switch { - case min >= 0: // All values positive. - for v := 0.0; v <= max; v += step { - if v >= min { - heights = append(heights, v) - } - } - case max <= 0: // All values negative. - for v := 0.0; v >= min; v -= step { - if v <= max { - heights = append(heights, v) - } - } - default: // Positive and negative. - for v := step; v <= max; v += step { - heights = append(heights, v) - } - for i, j := 0, len(heights)-1; i < j; i, j = i+1, j-1 { - heights[i], heights[j] = heights[j], heights[i] - } - for v := 0.0; v >= min; v -= step { - heights = append(heights, v) - } - } - return heights -} - // ContourResult stores an calculated iso line for a given height. // Is used as a future variable in the concurrent iso line calculation. type ContourResult struct {