view pkg/controllers/gauges.go @ 2741:87aed4f9b1b8

Added calculation of Nash Sutcliffe efficiency coefficents. GET /api/data/nash-sutcliffe/{gauge}?when={WHEN} 'when' is optional in form of "2006-01-02T15:04:05.000" and defaults to current server time. curl -H "X-Gemma-Auth:$KEY" http://${server}:${port}/api/data/nash-sutcliffe/${gauge} | jq . { "when": "2019-03-20T10:38:05.687", "coeffs": [ { "value": 0, "samples": 0, "hours": 24 }, { "value": 0, "samples": 0, "hours": 48 }, { "value": 0, "samples": 0, "hours": 72 } ] }
author Sascha L. Teichmann <sascha.teichmann@intevation.de>
date Wed, 20 Mar 2019 10:47:01 +0100
parents 0d7a4fdb9e12
children f95ec0bb565c
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 (
	"database/sql"
	"encoding/csv"
	"fmt"
	"log"
	"net/http"
	"strconv"
	"time"

	"github.com/gorilla/mux"

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

const (
	selectPredictedObserveredSQL = `
SELECT
  a.measure_date AS measure_date,
  a.water_level  AS predicted,
  b.water_level  AS observed
FROM waterway.gauge_measurements a JOIN waterway.gauge_measurements b
  ON a.fk_gauge_id  = b.fk_gauge_id AND
     a.measure_date = b.measure_date AND
     a.predicted AND NOT b.predicted
WHERE
  a.fk_gauge_id = (
    $1::char(1),
	$2::char(2),
	$3::char(3),
	$4::char(4),
	$5::int
  ) AND
  a.measure_date BETWEEN
    $6::timestamp AND $6::timestamp - '72hours'::interval
ORDER BY a.measure_date
`

	selectWaterlevelsSQL = `
SELECT
  measure_date,
  water_level,
  predicted
FROM waterway.gauge_measurements
WHERE
`
)

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

	var isrs *models.Isrs
	if isrs, err = models.IsrsFromString(gauge); err != nil {
		err = JSONError{
			Code:    http.StatusBadRequest,
			Message: fmt.Sprintf("error: Invalid ISRS code: %v", err),
		}
		return
	}

	var when time.Time
	if w := req.FormValue("when"); w != "" {
		if when, err = time.Parse(models.ImportTimeFormat, w); err != nil {
			err = JSONError{
				Code:    http.StatusBadRequest,
				Message: fmt.Sprintf("error: wrong time format: %v", err),
			}
			return
		}
	} else {
		when = time.Now()
	}

	ctx := req.Context()

	var rows *sql.Rows
	if rows, err = conn.QueryContext(
		ctx,
		selectPredictedObserveredSQL,
		isrs.CountryCode,
		isrs.LoCode,
		isrs.FairwaySection,
		isrs.Orc,
		isrs.Hectometre,
		when,
	); err != nil {
		return
	}
	defer rows.Close()

	var measurements []common.NSMeasurement

	for rows.Next() {
		var m common.NSMeasurement
		if err = rows.Scan(
			&m.When,
			&m.Predicted,
			&m.Observed,
		); err != nil {
			return
		}
		measurements = append(measurements, m)
	}
	if err = rows.Err(); err != nil {
		return
	}

	type coeff struct {
		Value   float64 `json:"value"`
		Samples int     `json:"samples"`
		Hours   int     `json:"hours"`
	}

	type coeffs struct {
		When   models.ImportTime `json:"when"`
		Coeffs []coeff           `json:"coeffs"`
	}

	cs := make([]coeff, 3)
	for i := range cs {
		cs[i].Hours = (i + 1) * 24
		cs[i].Value, cs[i].Samples = common.NashSutcliffe(
			measurements,
			when,
			when.Add(time.Duration(-cs[i].Hours)*time.Hour),
		)
	}

	jr = JSONResult{
		Result: &coeffs{
			When:   models.ImportTime{when},
			Coeffs: cs,
		},
	}
	return
}

func waterlevels(rw http.ResponseWriter, req *http.Request) {
	gauge := mux.Vars(req)["gauge"]

	isrs, err := models.IsrsFromString(gauge)
	if err != nil {
		http.Error(
			rw, fmt.Sprintf("error: Invalid ISRS code: %v", err),
			http.StatusBadRequest)
		return
	}

	var fb filterBuilder
	fb.stmt.WriteString(selectWaterlevelsSQL)

	fb.cond(
		" fk_gauge_id = ($%d::char(2), $%d::char(3), $%d::char(5), $%d::char(5), $%d::int) ",
		isrs.CountryCode,
		isrs.LoCode,
		isrs.FairwaySection,
		isrs.Orc,
		isrs.Hectometre,
	)

	if from := req.FormValue("from"); from != "" {
		fromTime, err := time.Parse(models.ImportTimeFormat, from)
		if err != nil {
			http.Error(
				rw, fmt.Sprintf("error: Invalid from time: %v", err),
				http.StatusBadRequest)
			return
		}
		fb.cond("measure_date >= $%d", fromTime)
	}

	if to := req.FormValue("to"); to != "" {
		toTime, err := time.Parse(models.ImportTimeFormat, to)
		if err != nil {
			http.Error(
				rw, fmt.Sprintf("error: Invalid from time: %v", err),
				http.StatusBadRequest)
			return
		}
		fb.cond("measure_date <= $%d", toTime)
	}

	conn := middleware.GetDBConn(req)

	ctx := req.Context()

	rows, err := conn.QueryContext(ctx, fb.stmt.String(), fb.args...)
	if err != nil {
		http.Error(
			rw, fmt.Sprintf("error: %v", err),
			http.StatusInternalServerError)
		return
	}
	defer rows.Close()

	rw.Header().Add("Content-Type", "text/csv")

	out := csv.NewWriter(rw)

	record := make([]string, 3)

	for rows.Next() {
		var (
			measureDate time.Time
			waterlevel  float64
			predicted   bool
		)
		if err := rows.Scan(&measureDate, &waterlevel, &predicted); err != nil {
			log.Printf("error: %v\n", err)
			// Too late for an HTTP error code.
			return
		}
		record[0] = measureDate.Format(models.ImportTimeFormat)
		record[1] = strconv.FormatFloat(waterlevel, 'f', -1, 64)
		if predicted {
			record[2] = "t"
		} else {
			record[2] = "f"
		}
		if err := out.Write(record); err != nil {
			log.Printf("error: %v", err)
			// Too late for an HTTP error code.
			return
		}
	}

	if err := rows.Err(); err != nil {
		log.Printf("error: %v", err)
		// Too late for an HTTP error code.
		return
	}

	out.Flush()
	if err := out.Error(); err != nil {
		log.Printf("error: %v", err)
		// Too late for an HTTP error code.
		return
	}
}