view pkg/models/colors.go @ 5415:4ad68ab239b7 marking-single-beam

Factored creation of default class breaks in SR import to be reused with markings, too.
author Sascha L. Teichmann <sascha.teichmann@intevation.de>
date Wed, 07 Jul 2021 12:01:28 +0200
parents 18d5461bec5d
children
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 models a tuple of a floating point value and a color.
	ColorValue struct {
		Value float64
		Color color.RGBA
	}

	// ColorValues are a list of color values.
	// Colors between the given ones are linear interpolated.
	ColorValues []ColorValue

	// ClassBreak is a potential open interval of values associated
	// with a color.
	ClassBreak struct {
		High    float64
		HasHigh bool
		Low     float64
		HasLow  bool
		Col     color.RGBA
	}
)

// Color gives a HTML compatible string representation of the color
// of the class break.
func (cb *ClassBreak) Color() string {
	return fmt.Sprintf("#%02x%02x%02x",
		cb.Col.R,
		cb.Col.G,
		cb.Col.B,
	)
}

// ClassBreaks converts a list of colors to a list of class breaks.
// The first an the last class break are open on the respective end.
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
}

// Heights extracts the values friom the color value pairs.
func (cc ColorValues) Heights() []float64 {
	heights := make([]float64, len(cc))
	for i := range cc {
		heights[i] = cc[i].Value
	}
	return heights
}

// Clip does the same as Interpolate
// but if the value is out of bounds the value of the nearest border
// is returned.
func (cc ColorValues) Clip(v float64) color.RGBA {
	if len(cc) == 0 {
		return color.RGBA{}
	}
	if v < cc[0].Value {
		return cc[0].Color
	}
	if v > cc[len(cc)-1].Value {
		return cc[len(cc)-1].Color
	}
	c, _ := cc.Interpolate(v)
	return c
}

// Interpolate interpolates the color linearly between the
// given values of the color values.
// If the value is out of bounds false is returned.
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
}

// ParseColorValues parses a string to a list of color values.
// The value/color pairs are separated by ','.
// A color/value pair is separated with ':'.
// f ':' is missing it is assumed that only the value is given.
// The missing color is interpolated from fore and aftermost
// value/colors pairs.
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
}