view pkg/geoserver/templates.go @ 3841:8e47d6f12998 sld-colors

Merged default into sld-colors branch.
author Sascha L. Teichmann <sascha.teichmann@intevation.de>
date Mon, 08 Jul 2019 14:34:42 +0200
parents 0ffea636d6b0
children 3fcc4e11fc00
line wrap: on
line source

// 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 <sascha.teichmann@intevation.de>

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
}