view pkg/controllers/bottlenecks.go @ 3116:ff0b9a94e0e4

Simpified code.
author Sascha L. Teichmann <sascha.teichmann@intevation.de>
date Sun, 28 Apr 2019 21:41:20 +0200
parents 524bc6545b20
children 6b5132fd385e
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>

package controllers

import (
	"context"
	"database/sql"
	"fmt"
	"log"
	"net/http"
	"sort"
	"strconv"
	"strings"
	"time"

	"gemma.intevation.de/gemma/pkg/common"
	"github.com/gorilla/mux"
)

const (
	selectAvailableDepthSQL = `
WITH data AS (
  SELECT
    efa.measure_date,
    efa.available_depth_value,
    efa.water_level_value
  FROM waterway.effective_fairway_availability efa
  JOIN waterway.fairway_availability fa
    ON efa.fairway_availability_id = fa.id
  JOIN waterway.bottlenecks bn
    ON fa.bottleneck_id = bn.id
  WHERE
    bn.objnam = $1 AND
    efa.level_of_service = $2 AND
    efa.measure_type = 'Measured' AND
    efa.available_depth_value IS NOT NULL AND
    efa.water_level_value IS NOT NULL
),
before AS (
  SELECT * FROM data WHERE measure_date < $3
  ORDER BY measure_date DESC LIMIT 1
),
inside AS (
  SELECT * FROM data WHERE measure_date BETWEEN $3 AND $4
),
after AS (
  SELECT * FROM data WHERE measure_date > $4
  ORDER BY measure_date LIMIT 1
)
SELECT * FROM before
UNION ALL
SELECT * FROM inside
UNION ALL
SELECT * FROM after
ORDER BY measure_date
`

	selectGaugeLevelsSQL = `
SELECT
  grwl.depth_reference,
  grwl.value
FROM waterway.gauges_reference_water_levels grwl JOIN 
     waterway.bottlenecks bns
	 ON bns.fk_g_fid = grwl.gauge_id
WHERE bns.objnam = $1 AND (
  grwl.depth_reference like 'HDC%' OR
  grwl.depth_reference like 'LDC%' OR
  grwl.depth_reference like 'MW%'
)
`
)

type (
	referenceValue struct {
		level int
		value float64
	}

	availMeasurement struct {
		when  time.Time
		depth int
		value int
	}

	availMeasurements []availMeasurement
)

func (measurement *availMeasurement) getDepth() float64 {
	return float64(measurement.depth)
}

func (measurement *availMeasurement) getValue() float64 {
	return float64(measurement.value)
}

func (measurements availMeasurements) classifyAvailMeasurements(
	from, to time.Time,
	classes []referenceValue,
	access func(*availMeasurement) float64,
) []time.Duration {

	type classValues struct {
		when time.Time
		kind common.ValueRangeKind
	}

	//var invalid time.Duration
	result := make([]time.Duration, len(classes)+1)

	if len(measurements) == 0 ||
		to.Before(measurements[0].when) ||
		from.After(measurements[len(measurements)-1].when) {
		return result
	}

	cvs := make([]classValues, len(classes))

	classify := func(v func(float64) (time.Time, common.ValueRangeKind)) {
		for i := range classes {
			cvs[i].when, cvs[i].kind = v(classes[i].value)
		}
	}

	vbt := func(p1, p2 *availMeasurement) func(time.Time) (float64, common.ValueRangeKind) {
		return common.InterpolateValueByTime(p1.when, access(p1), p2.when, access(p2))
	}

pairs:
	for i := 0; i < len(measurements)-1; i++ {
		p1 := &measurements[i]
		p2 := &measurements[i+1]

		var start, end time.Time

		switch {
		case !p2.when.After(p2.when):
			// Segment invalid
			continue pairs

		case p1.when.After(to) || p2.when.Before(from):
			// Segment complete outside.
			continue pairs

		case p1.when.After(from) && p2.when.Before(to):
			// (from-to) is complete inside segment.
			// invalid += p1.when.Sub(from)
			// invalid += to.Sub(p2.when)
			v := vbt(p1, p2)
			f, _ := v(from)
			t, _ := v(to)
			classify(common.InterpolateTimeByValue(from, f, to, t))
			start, end = from, to

		case p1.when.After(from):
			// from is inside segment
			// invalid += p1.when.Sub(from)
			f, _ := vbt(p1, p2)(from)
			classify(common.InterpolateTimeByValue(
				from, f,
				p2.when, access(p2),
			))
			start, end = from, p2.when

		case p2.when.Before(to):
			// to is inside segment
			// invalid += to.Sub(p2.when)
			t, _ := vbt(p1, p2)(to)
			classify(common.InterpolateTimeByValue(
				p1.when, access(p1),
				to, t,
			))
			start, end = p1.when, to

		case !p1.when.Before(from) && !to.After(p2.when):
			// Segment complete inside.
			classify(common.InterpolateTimeByValue(
				p1.when, access(p1),
				p2.when, access(p2),
			))
			start, end = p1.when, p2.when
		default:
			log.Println("warn: unexpected case. That should not happen.")
			continue pairs
		}

		for i := len(cvs) - 1; i >= 0; i-- {
			switch cvs[i].kind {
			case common.ValueAbove:
				result[i+1] += end.Sub(start)
				continue pairs

			case common.ValueInside:
				// -> split
				if access(p1) < classes[i].value {
					// started below -> second part above
					result[i+1] = end.Sub(cvs[i].when)
					end = cvs[i].when
				} else {
					// started above -> first part above
					result[i+1] = cvs[i].when.Sub(start)
					start = cvs[i].when
				}
			}
		}
		result[0] += end.Sub(start)
	}

	return result
}

func durationsToPercentage(from, to time.Time, classes []time.Duration) []float64 {
	percents := make([]float64, len(classes))
	total := 100 / to.Sub(from).Seconds()
	for i, v := range classes {
		percents[i] = v.Seconds() * total
	}
	return percents
}

func parseTime(s, what string) (time.Time, error) {
	var t time.Time
	var err error
	if t, err = time.Parse(common.TimeFormat, s); err != nil {
		return time.Time{}, JSONError{
			Code: http.StatusBadRequest,
			Message: fmt.Sprintf(
				"Invalid time format for '%s' field: %v", what, err),
		}
	}
	return t.UTC(), nil
}

func loadDepthValues(
	ctx context.Context,
	conn *sql.Conn,
	bottleneck string,
	los int,
	from, to time.Time,
) (availMeasurements, error) {

	rows, err := conn.QueryContext(
		ctx, selectAvailableDepthSQL, bottleneck, los, from, to)
	if err != nil {
		return nil, err
	}
	defer rows.Close()

	var ms availMeasurements

	for rows.Next() {
		var m availMeasurement
		if err := rows.Scan(&m.when, &m.depth, &m.value); err != nil {
			return nil, err
		}
		m.when = m.when.UTC()
		ms = append(ms, m)
	}

	if err := rows.Err(); err != nil {
		return nil, err
	}

	return ms, nil
}

func loadLNWLReferenceValues(
	ctx context.Context,
	conn *sql.Conn,
	bottleneck string,
) ([]referenceValue, error) {
	rows, err := conn.QueryContext(ctx, selectGaugeLevelsSQL, bottleneck)
	if err != nil {
		return nil, err
	}
	defer rows.Close()

	var levels []referenceValue

loop:
	for rows.Next() {
		var what string
		var value int
		if err := rows.Scan(&what, &value); err != nil {
			return nil, err
		}
		var level int
		switch {
		case strings.HasPrefix(what, "LDC"):
			level = 0
		case strings.HasPrefix(what, "MW"):
			level = 1
		case strings.HasPrefix(what, "HDC"):
			level = 2
		default:
			return nil, fmt.Errorf("Unexpected reference level type '%s'", what)
		}
		for i := range levels {
			if levels[i].level == level {
				levels[i].value = float64(value)
				continue loop
			}
		}
		levels = append(levels, referenceValue{
			level: level,
			value: float64(value),
		})
	}

	if err := rows.Err(); err != nil {
		return nil, err
	}

	sort.Slice(levels, func(i, j int) bool { return levels[i].level < levels[j].level })

	return levels, nil
}

func bottleneckAvailabilty(
	_ interface{},
	req *http.Request,
	conn *sql.Conn,
) (jr JSONResult, err error) {
	bn := mux.Vars(req)["objnam"]

	if bn == "" {
		err = JSONError{
			Code:    http.StatusBadRequest,
			Message: "Missing objnam of bottleneck",
		}
		return
	}

	var from, to time.Time

	if f := req.FormValue("from"); f != "" {
		if from, err = parseTime(f, "from"); err != nil {
			return
		}
	} else {
		from = time.Now().AddDate(-1, 0, 0).UTC()
	}

	if t := req.FormValue("to"); t != "" {
		if to, err = parseTime(t, "to"); err != nil {
			return
		}
	} else {
		to = from.AddDate(1, 0, 0).UTC()
	}

	if to.Before(from) {
		to, from = from, to
	}

	log.Printf("info: time interval: (%v - %v)\n", from, to)

	var los int
	if l := req.FormValue("los"); l != "" {
		if los, err = strconv.Atoi(l); err != nil {
			err = JSONError{
				Code:    http.StatusBadRequest,
				Message: fmt.Sprintf("Invalid value for field 'los': %v", err),
			}
			return
		}
	} else {
		los = 1
	}

	ctx := req.Context()

	var lnwlRefs []referenceValue
	if lnwlRefs, err = loadLNWLReferenceValues(ctx, conn, bn); err != nil {
		return
	}

	if len(lnwlRefs) == 0 {
		err = JSONError{
			Code:    http.StatusNotFound,
			Message: "No gauge reference values found for bottleneck",
		}
		return
	}

	var ms availMeasurements
	if ms, err = loadDepthValues(ctx, conn, bn, los, from, to); err != nil {
		return
	}

	if len(ms) == 0 {
		err = JSONError{
			Code:    http.StatusNotFound,
			Message: "No available fairway depth values found",
		}
		return
	}

	lnwl := ms.classifyAvailMeasurements(
		from, to,
		lnwlRefs,
		(*availMeasurement).getValue,
	)

	afdRefs := []referenceValue{
		{0, 200},
		{1, 230},
		{2, 250},
	}

	afd := ms.classifyAvailMeasurements(
		from, to,
		afdRefs,
		(*availMeasurement).getDepth,
	)

	lnwlPercents := durationsToPercentage(from, to, lnwl)
	afdPercents := durationsToPercentage(from, to, afd)

	type lnwlOutput struct {
		Level   string  `json:"level"`
		Value   float64 `json:"value"`
		Percent float64 `json:"percent"`
	}

	type afdOutput struct {
		Value   float64 `json:"value"`
		Percent float64 `json:"percent"`
	}

	type output struct {
		LNWL []lnwlOutput `json:"lnwl"`
		AFD  []afdOutput  `json:"afd"`
	}

	out := output{}

	for i := range lnwlRefs {
		var level string
		switch lnwlRefs[i].level {
		case 0:
			level = "LDC"
		case 1:
			level = "MW"
		case 2:
			level = "HDC"
		}
		out.LNWL = append(out.LNWL, lnwlOutput{
			Level:   level,
			Value:   lnwlRefs[i].value,
			Percent: lnwlPercents[i],
		})
	}

	for i := range afdRefs {
		out.AFD = append(out.AFD, afdOutput{
			Value:   afdRefs[i].value,
			Percent: afdPercents[i],
		})
	}

	jr = JSONResult{Result: &out}

	return
}