view pkg/controllers/fwa.go @ 5203:355195a90298 new-fwa

Start calculting the navigability. TODO: accumulate and do output.
author Sascha L. Teichmann <sascha.teichmann@intevation.de>
date Fri, 08 May 2020 18:59:14 +0200
parents fbc79c8459b4
children 7ca9e6c9a203
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, 2019, 2020 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"
	"time"

	"github.com/gorilla/mux"

	"gemma.intevation.de/gemma/pkg/common"
	"gemma.intevation.de/gemma/pkg/middleware"
)

const (
	selectBottlenecksLimitingSQL = `
SELECT
  lower(validity),
  upper(validity),
  limiting
FROM
  waterway.bottlenecks
WHERE
  bottleneck_id = $1 AND
  validity && tstzrange($2, $3)`

	selectSymbolBottlenecksSQL = `
SELECT
  distinct(b.bottleneck_id)
FROM
  users.%s s, waterway.bottlenecks b
WHERE
  ST_Intersects(b.area, s.area)
  AND s.name = $1
  AND b.validity && tstzrange($2, $3)`

	selectLDCsSQL = `
SELECT
  lower(grwl.validity),
  upper(grwl.validity),
  grwl.value
FROM
  waterway.gauges_reference_water_levels grwl
  JOIN waterway.bottlenecks bns
    ON grwl.location = bns.gauge_location
WHERE
  grwl.depth_reference like 'LDC%'
  AND bns.bottleneck_id = $1
  AND grwl.validity && tstzrange($2, $3)`

	selectMeasurementsSQL = `
WITH data AS (
  SELECT
    efa.measure_date,
    efa.available_depth_value,
    efa.available_width_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.bottleneck_id
  WHERE
    bn.bottleneck_id = $1 AND
    efa.level_of_service = $2 AND
    efa.measure_type = 'Measured' AND
    (efa.available_depth_value IS NOT NULL OR
     efa.available_width_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`
)

type (
	timeRange struct {
		lower time.Time
		upper time.Time
	}

	ldc struct {
		timeRange
		value []float64
	}

	ldcs []*ldc

	limitingValidity struct {
		timeRange
		limiting func(*availMeasurement) float64
		ldcs     ldcs
	}

	limitingValidities []limitingValidity

	availMeasurement struct {
		when  time.Time
		depth int16
		width int16
		value int16
	}

	availMeasurements []availMeasurement

	bottleneck struct {
		id           string
		validities   limitingValidities
		measurements availMeasurements
	}
)

func (ls ldcs) find(from, to time.Time) *ldc {
	for _, l := range ls {
		if l.intersects(from, to) {
			return l
		}
	}
	return nil
}

func fairwayAvailability(rw http.ResponseWriter, req *http.Request) {

	from, to, ok := parseFromTo(rw, req)
	if !ok {
		return
	}

	vars := mux.Vars(req)
	name := vars["name"]
	if name == "" {
		http.Error(rw, "missing 'name' parameter.", http.StatusBadRequest)
		return
	}

	los, ok := parseFormInt(rw, req, "los", 1)
	if !ok {
		return
	}

	ctx := req.Context()
	conn := middleware.GetDBConn(req)

	// Function to extract the bottleneck_id's from the query.
	var extract func(context.Context, *sql.Conn, string, time.Time, time.Time) ([]bottleneck, error)

	switch vars["kind"] {
	case "bottleneck":
		extract = extractBottleneck
	case "stretch":
		extract = extractStretch
	case "section":
		extract = extractSection
	default:
		http.Error(rw, "Invalid kind type.", http.StatusBadRequest)
		return
	}

	bottlenecks, err := extract(ctx, conn, name, from, to)
	if err != nil {
		log.Printf("error: %v\n", err)
		http.Error(rw, "cannot extract bottlenecks", http.StatusBadRequest)
		return
	}

	// load validities and limiting factors
	for i := range bottlenecks {
		if err := bottlenecks[i].loadLimitingValidities(ctx, conn, from, to); err != nil {
			log.Printf("error: %v\n", err)
			http.Error(rw, "cannot load validities", http.StatusInternalServerError)
			return
		}
		// load LCDs
		if err := bottlenecks[i].loadLDCs(ctx, conn, from, to); err != nil {
			log.Printf("error: %v\n", err)
			http.Error(rw, "cannot load LDCs", http.StatusInternalServerError)
			return
		}
		// load values
		if err := bottlenecks[i].loadValues(ctx, conn, from, to, los); err != nil {
			log.Printf("error: %v\n", err)
			http.Error(rw, "cannot load LDCs", http.StatusInternalServerError)
			return
		}

	}

	// For every day on every bottleneck we need to find out if this day is valid.
	validities := make([]func(time.Time, time.Time) *limitingValidity, len(bottlenecks))
	for i := range bottlenecks {
		validities[i] = bottlenecks[i].validities.find()
	}

	var shipableDays int

	// We step through the time in steps of one day.
	for current := from; current.Before(to); {

		next := current.AddDate(0, 0, 1)

		shipable := true

		// over all bottlenecks
		for i, validity := range validities {

			if vs := validity(current, next); vs != nil {

				// Let's see if we have a LDC for this day.
				ldc := vs.ldcs.find(current, next)
				if ldc == nil {
					// TODO: log missing LCD
					continue
				}

				result := bottlenecks[i].measurements.classify(
					current, next,
					ldc.value,
					vs.limiting)

				if result[1] < 12*time.Hour {
					shipable = false
					break
				}
			}
		}

		if shipable {
			shipableDays++
		}
		// TODO: depending on mode write out results.

		current = next
	}
}

func dusk(t time.Time) time.Time {
	return time.Date(
		t.Year(),
		t.Month(),
		t.Day(),
		0, 0, 0, 0,
		t.Location())
}

func dawn(t time.Time) time.Time {
	return time.Date(
		t.Year(),
		t.Month(),
		t.Day(),
		23, 59, 59, 999999999,
		t.Location())
}

func parseFromTo(
	rw http.ResponseWriter,
	req *http.Request,
) (time.Time, time.Time, bool) {
	from, ok := parseFormTime(rw, req, "from", time.Now().AddDate(-1, 0, 0))
	if !ok {
		return time.Time{}, time.Time{}, false
	}

	to, ok := parseFormTime(rw, req, "to", from.AddDate(1, 0, 0))
	if !ok {
		return time.Time{}, time.Time{}, false
	}

	from, to = common.OrderTime(from, to)
	// Operate on daily basis so go to full days.
	return dusk(from), dawn(to), true
}

func (tr *timeRange) intersects(from, to time.Time) bool {
	return !(to.Before(tr.lower) || from.After(tr.upper))
}

func (tr *timeRange) toUTC() {
	tr.lower = tr.lower.UTC()
	tr.upper = tr.upper.UTC()
}

func (lvs limitingValidities) find() func(from, to time.Time) *limitingValidity {

	var last *limitingValidity

	return func(from, to time.Time) *limitingValidity {
		if last != nil && last.intersects(from, to) {
			return last
		}
		for i := range lvs {
			if lv := &lvs[i]; lv.intersects(from, to) {
				last = lv
				return lv
			}
		}
		return nil
	}
}

func limitingFactor(limiting string) func(*availMeasurement) float64 {
	switch limiting {
	case "depth":
		return (*availMeasurement).getDepth
	case "width":
		return (*availMeasurement).getWidth
	default:
		log.Printf("warn: unknown limitation '%s'. default to 'depth'\n", limiting)
		return (*availMeasurement).getDepth
	}
}

func loadLimitingValidities(
	ctx context.Context,
	conn *sql.Conn,
	bottleneckID string,
	from, to time.Time,
) (limitingValidities, error) {

	var lvs limitingValidities

	rows, err := conn.QueryContext(
		ctx,
		selectLimitingSQL,
		from, to)

	if err != nil {
		return nil, err
	}
	defer rows.Close()

	for rows.Next() {
		var lv limitingValidity
		var access string
		if err := rows.Scan(
			&access,
			&lv.lower,
			&lv.upper,
		); err != nil {
			return nil, err
		}
		lv.toUTC()
		lv.limiting = limitingFactor(access)
		lvs = append(lvs, lv)
	}

	return lvs, rows.Err()
}

func loadSymbolBottlenecksFromTo(
	ctx context.Context,
	conn *sql.Conn,
	what, name string,
	from, to time.Time,
) ([]bottleneck, error) {

	rows, err := conn.QueryContext(
		ctx,
		fmt.Sprintf(selectSymbolBottlenecksSQL, what),
		name,
		from, to)
	if err != nil {
		return nil, err
	}
	defer rows.Close()

	var bottlenecks []bottleneck

	for rows.Next() {
		var b bottleneck
		if err := rows.Scan(&b.id); err != nil {
			return nil, err
		}
		bottlenecks = append(bottlenecks, b)
	}

	return bottlenecks, rows.Err()
}

func extractBottleneck(
	_ context.Context,
	_ *sql.Conn,
	name string,
	_, _ time.Time,
) ([]bottleneck, error) {
	return []bottleneck{{id: name}}, nil
}

func extractStretch(
	ctx context.Context,
	conn *sql.Conn,
	name string,
	from, to time.Time,
) ([]bottleneck, error) {
	return loadSymbolBottlenecksFromTo(
		ctx,
		conn,
		"stretches", name,
		from, to)
}

func extractSection(
	ctx context.Context,
	conn *sql.Conn,
	name string,
	from, to time.Time,
) ([]bottleneck, error) {
	return loadSymbolBottlenecksFromTo(
		ctx,
		conn,
		"sections", name,
		from, to)
}

func (bn *bottleneck) loadLimitingValidities(
	ctx context.Context,
	conn *sql.Conn,
	from, to time.Time,
) error {
	vs, err := loadLimitingValidities(
		ctx,
		conn,
		bn.id,
		from, to)
	if err == nil {
		bn.validities = vs
	}
	return err
}

func (bn *bottleneck) loadLDCs(
	ctx context.Context,
	conn *sql.Conn,
	from, to time.Time,
) error {
	rows, err := conn.QueryContext(
		ctx, selectLDCsSQL,
		bn.id,
		from, to)
	if err != nil {
		return err
	}
	defer rows.Close()
	for rows.Next() {
		l := ldc{value: []float64{0}}
		if err := rows.Scan(&l.lower, &l.upper, &l.value[0]); err != nil {
			return err
		}
		l.toUTC()
		for i := range bn.validities {
			vs := bn.validities[i]
			if vs.intersects(l.lower, l.upper) {
				vs.ldcs = append(vs.ldcs, &l)
			}
		}
	}
	return rows.Err()
}

func (bn *bottleneck) loadValues(
	ctx context.Context,
	conn *sql.Conn,
	from, to time.Time,
	los int,
) error {
	rows, err := conn.QueryContext(
		ctx, selectMeasurementsSQL,
		bn.id,
		from, to,
		los)
	if err != nil {
		return err
	}
	defer rows.Close()

	var ms availMeasurements

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

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

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

func (measurement *availMeasurement) getWidth() float64 {
	return float64(measurement.width)
}

func (measurements availMeasurements) classify(
	from, to time.Time,
	breaks []float64,
	access func(*availMeasurement) float64,
) []time.Duration {

	if len(breaks) == 0 {
		return []time.Duration{}
	}

	result := make([]time.Duration, len(breaks)+1)
	classes := make([]float64, len(breaks)+2)
	values := make([]time.Time, len(classes))

	// Add sentinels
	classes[0] = breaks[0] - 9999
	classes[len(classes)-1] = breaks[len(breaks)-1] + 9999
	for i := range breaks {
		classes[i+1] = breaks[i]
	}

	idx := sort.Search(len(measurements), func(i int) bool {
		// All values before from can be ignored.
		return !measurements[i].when.Before(from)
	})

	if idx >= len(measurements) {
		return result
	}

	// Be safe for interpolation.
	if idx > 0 {
		idx--
	}

	measurements = measurements[idx:]

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

		if p1.when.After(to) {
			return result
		}

		if p2.when.Before(from) {
			continue
		}

		if p2.when.Sub(p1.when).Hours() > 1.5 {
			// Don't interpolate ranges bigger then one and a half hour
			continue
		}

		lo, hi := common.MaxTime(p1.when, from), common.MinTime(p2.when, to)

		m1, m2 := access(p1), access(p2)
		if m1 == m2 { // The whole interval is in only one class.
			for j := 0; j < len(classes)-1; j++ {
				if classes[j] <= m1 && m1 <= classes[j+1] {
					result[j] += hi.Sub(lo)
					break
				}
			}
			continue
		}

		f := common.InterpolateTime(
			p1.when, m1,
			p2.when, m2,
		)

		for j, c := range classes {
			values[j] = f(c)
		}

		for j := 0; j < len(values)-1; j++ {
			start, end := common.OrderTime(values[j], values[j+1])

			if start.After(hi) || end.Before(lo) {
				continue
			}

			start, end = common.MaxTime(start, lo), common.MinTime(end, hi)
			result[j] += end.Sub(start)
		}
	}

	return result
}