view pkg/controllers/fwa.go @ 5199:5001582f2ee1 new-fwa

Prepare to treat stretches and sections, too.
author Sascha L. Teichmann <sascha.teichmann@intevation.de>
date Fri, 08 May 2020 11:43:06 +0200
parents c352dbbf2778
children 5572da077c89
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"
	"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)`
)

type (
	limitingValidity struct {
		limiting string
		lower    time.Time
		upper    time.Time
	}

	limitingValidities []limitingValidity

	bottleneck struct {
		id         string
		validities limitingValidities
	}
)

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
	}

	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.Println("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
		}
	}

	// TODO: Implement me!
}

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 (lv *limitingValidity) intersects(from, to time.Time) bool {
	return !(to.Before(lv.lower) || from.After(lv.upper))
}

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 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
		if err := rows.Scan(
			&lv.limiting,
			&lv.lower,
			&lv.upper,
		); err != nil {
			return nil, err
		}
		lv.lower = lv.lower.UTC()
		lv.upper = lv.upper.UTC()
		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
}