view pkg/models/colors.go @ 4611:b5aa1eb83bb0 geoserver_sql_views

Add possibility to configure SRS for GeoServer SQL view Automatic detection of spatial reference system for SQL views in GeoServer does not always find the correct SRS.
author Tom Gottfried <tom@intevation.de>
date Fri, 06 Sep 2019 11:58:03 +0200
parents 859210ee6440
children 04eba9dc917d
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) 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 ('0' <= r && r <= '9') ||
				('a' <= r && r <= 'f') ||
				('A' <= r && r <= 'F') {
				return r
			}
			return -1
		}, 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
}