changeset 1662:d8ca44615bfc

Implemented first version of fairway availability import.
author Raimund Renkert <raimund.renkert@intevation.de>
date Fri, 21 Dec 2018 15:56:28 +0100
parents 51a0ba4ede41
children 10e3dd3b9363
files pkg/controllers/faimports.go pkg/controllers/routes.go pkg/imports/fa.go pkg/models/bn.go pkg/models/fa.go pkg/soap/ifaf/service.go schema/gemma.sql
diffstat 7 files changed, 1408 insertions(+), 2 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/pkg/controllers/faimports.go	Fri Dec 21 15:56:28 2018 +0100
@@ -0,0 +1,70 @@
+// 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 by via donau
+//   – Österreichische Wasserstraßen-Gesellschaft mbH
+// Software engineering by Intevation GmbH
+//
+// Author(s):
+//  * Raimund Renkert <raimund.renkert@intevation.de>
+
+package controllers
+
+import (
+	"database/sql"
+	"log"
+	"net/http"
+
+	"gemma.intevation.de/gemma/pkg/auth"
+	"gemma.intevation.de/gemma/pkg/common"
+	"gemma.intevation.de/gemma/pkg/imports"
+	"gemma.intevation.de/gemma/pkg/models"
+)
+
+func importFairwayAvailability(
+	input interface{},
+	req *http.Request,
+	conn *sql.Conn,
+) (jr JSONResult, err error) {
+
+	fai := input.(*models.FairwayAvailabilityImport)
+
+	fa := &imports.FairwayAvailability{
+		URL:      fai.URL,
+		Insecure: fai.Insecure,
+	}
+
+	var serialized string
+	if serialized, err = common.ToJSONString(fa); err != nil {
+		return
+	}
+
+	session, _ := auth.GetSession(req)
+
+	var jobID int64
+	if jobID, err = imports.AddJob(
+		imports.FAJobKind,
+		session.User,
+		fai.SendEmail, true,
+		serialized,
+	); err != nil {
+		return
+	}
+
+	log.Printf("info: added import #%d to queue\n", jobID)
+
+	result := struct {
+		ID int64 `json:"id"`
+	}{
+		ID: jobID,
+	}
+
+	jr = JSONResult{
+		Code:   http.StatusCreated,
+		Result: &result,
+	}
+	return
+}
--- a/pkg/controllers/routes.go	Fri Dec 21 14:15:33 2018 +0100
+++ b/pkg/controllers/routes.go	Fri Dec 21 15:56:28 2018 +0100
@@ -181,6 +181,11 @@
 		Handle: importGaugeMeasurement,
 	})).Methods(http.MethodPost)
 
+	api.Handle("/imports/fairwayavailability", waterwayAdmin(&JSONHandler{
+		Input:  func() interface{} { return new(models.FairwayAvailabilityImport) },
+		Handle: importFairwayAvailability,
+	})).Methods(http.MethodPost)
+
 	// Import scheduler configuration
 	api.Handle("/imports/config/{id:[0-9]+}",
 		waterwayAdmin(&JSONHandler{
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/pkg/imports/fa.go	Fri Dec 21 15:56:28 2018 +0100
@@ -0,0 +1,366 @@
+// 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 by via donau
+//   – Österreichische Wasserstraßen-Gesellschaft mbH
+// Software engineering by Intevation GmbH
+//
+// Author(s):
+//  * Raimund Renkert <raimund.renkert@intevation.de>
+package imports
+
+import (
+	"context"
+	"database/sql"
+	"errors"
+	"time"
+
+	"gemma.intevation.de/gemma/pkg/common"
+	"gemma.intevation.de/gemma/pkg/models"
+	"gemma.intevation.de/gemma/pkg/soap/ifaf"
+	"github.com/jackc/pgx/pgtype"
+)
+
+type FairwayAvailability struct {
+	URL      string `json:"url"`
+	Insecure bool   `json:"insecure"`
+}
+
+const FAJobKind JobKind = "fa"
+
+const (
+	listBottlenecksSQL = `
+SELECT
+	bottleneck_id,
+	responsible_country
+FROM waterway.bottlenecks
+WHERE responsible_country = users.current_user_country()
+	AND staging_done = true
+`
+	insertFASQL = `
+INSERT INTO waterway.fairway_availability (
+  position_code,
+  bottleneck_id,
+  surdat,
+  critical,
+  date_info,
+  source_organization
+) VALUES (
+  $1,
+  (SELECT id FROM waterway.bottlenecks WHERE bottleneck_id = $2),
+  $3,
+  $4,
+  $5,
+  $6
+)
+RETURNING id`
+
+	insertBnPdfsSQL = `
+INSERT INTO waterway.bottleneck_pdfs (
+	fairway_availability_id,
+	profile_pdf_filename,
+	profile_pdf_url,
+	pdf_generation_date,
+	source_organization
+) VALUES (
+	$1,
+	$2,
+	$3,
+	$4,
+	$5
+)`
+	insertEFASQL = `
+INSERT INTO waterway.effective_fairway_availability (
+	fairway_availability_id,
+	measure_date,
+	level_of_service,
+	available_depth_value,
+	available_width_value,
+	water_level_value,
+	measure_type,
+	source_organization,
+	forecast_generation_time,
+	value_lifetime
+) VALUES (
+	$1,
+	$2,
+	(SELECT
+		level_of_service
+	FROM levels_of_service
+	WHERE name = $3),
+	$4,
+	$5,
+	$6,
+	$7,
+	$8,
+	$9,
+	$10
+)`
+	insertFAVSQL = `
+INSERT INTO waterway.fa_reference_values (
+	fairway_availability_id,
+	level_of_service,
+	fairway_depth,
+	fairway_width,
+	fairway_radius,
+	shallowest_spot
+) VALUES (
+	$1,
+	(SELECT
+		level_of_service
+	FROM levels_of_service
+	WHERE name = $2),
+	$3,
+	$4,
+	$5,
+	ST_MakePoint($6, $7)::geography
+)`
+)
+
+type faJobCreator struct{}
+
+func init() {
+	RegisterJobCreator(FAJobKind, faJobCreator{})
+}
+
+func (faJobCreator) Description() string {
+	return "fairway availability"
+}
+
+func (faJobCreator) Create(_ JobKind, data string) (Job, error) {
+	fa := new(FairwayAvailability)
+	if err := common.FromJSONString(data, fa); err != nil {
+		return nil, err
+	}
+	return fa, nil
+}
+
+func (faJobCreator) Depends() []string {
+	return []string{
+		"bottlenecks",
+		"fairway_availability",
+		"bottleneck_pdfs",
+		"effective_fairway_availability",
+		"fa_reference_values",
+		"levels_of_service",
+	}
+}
+
+// StageDone moves the imported fairway availablities out of the staging area.
+// Currently doing nothing.
+func (faJobCreator) StageDone(
+	ctx context.Context,
+	tx *sql.Tx,
+	id int64,
+) error {
+	return nil
+}
+
+// CleanUp of a fairway availablities import is a NOP.
+func (fa *FairwayAvailability) CleanUp() error { return nil }
+
+// Do executes the actual fairway availability import.
+func (fa *FairwayAvailability) Do(
+	ctx context.Context,
+	importID int64,
+	conn *sql.Conn,
+	feedback Feedback,
+) (interface{}, error) {
+
+	// Get available bottlenecks from database for use as filter in SOAP request
+	var rows *sql.Rows
+
+	rows, err := conn.QueryContext(ctx, listBottlenecksSQL)
+	if err != nil {
+		return nil, err
+	}
+	defer rows.Close()
+
+	bottlenecks := []models.Bottleneck{}
+
+	for rows.Next() {
+		var bn models.Bottleneck
+		if err = rows.Scan(
+			&bn.ID,
+			&bn.ResponsibleCountry,
+		); err != nil {
+			return nil, err
+		}
+		bottlenecks = append(bottlenecks, bn)
+	}
+
+	if err = rows.Err(); err != nil {
+		return nil, err
+	}
+
+	faids, err := fa.doForFAs(ctx, bottlenecks, conn, feedback)
+	if err != nil {
+		feedback.Error("Error processing data: %s", err)
+	}
+	if len(faids) == 0 {
+		feedback.Info("No new fairway availablity data found")
+		return nil, nil
+	}
+	feedback.Info("Processed %d of %d bottlenecks", len(faids), len(bottlenecks))
+	// TODO: needs to be filled more useful.
+	summary := struct {
+		FairwayAvailabilities []string `json:"fairwayAvailabilities"`
+	}{
+		FairwayAvailabilities: faids,
+	}
+	return &summary, err
+}
+
+func (fa *FairwayAvailability) doForFAs(
+	ctx context.Context,
+	bottlenecks []models.Bottleneck,
+	conn *sql.Conn,
+	feedback Feedback,
+) ([]string, error) {
+	start := time.Now()
+
+	client := ifaf.NewFairwayAvailabilityService(fa.URL, fa.Insecure, nil)
+
+	var bnIds []string
+	for _, bn := range bottlenecks {
+		bnIds = append(bnIds, bn.ID)
+	}
+
+	ids := ifaf.ArrayOfString{
+		String: bnIds,
+	}
+
+	// TODO: Filter by period. Period should start after latest measurement date.
+	req := &ifaf.Get_bottleneck_fa{
+		Bottleneck_id: &ids,
+	}
+	resp, err := client.Get_bottleneck_fa(req)
+	if err != nil {
+		feedback.Error("%v", err)
+		return nil, err
+	}
+
+	if resp.Get_bottleneck_faResult == nil {
+		err := errors.New("no fairway availabilities found")
+		return nil, err
+	}
+
+	result := resp.Get_bottleneck_faResult
+
+	tx, err := conn.BeginTx(ctx, nil)
+	if err != nil {
+		return nil, err
+	}
+	defer tx.Rollback()
+
+	insertFAStmt, err := tx.PrepareContext(ctx, insertFASQL)
+	if err != nil {
+		return nil, err
+	}
+	defer insertFAStmt.Close()
+	insertBnPdfsStmt, err := tx.PrepareContext(ctx, insertBnPdfsSQL)
+	if err != nil {
+		return nil, err
+	}
+	defer insertBnPdfsStmt.Close()
+	insertEFAStmt, err := tx.PrepareContext(ctx, insertEFASQL)
+	if err != nil {
+		return nil, err
+	}
+	defer insertEFAStmt.Close()
+	insertFAVStmt, err := tx.PrepareContext(ctx, insertFAVSQL)
+	if err != nil {
+		return nil, err
+	}
+	defer insertFAVStmt.Close()
+
+	var faids []string
+	var faId int64
+	feedback.Info("Found %d fairway availabilities", len(result.FairwayAvailability))
+	for _, faRes := range result.FairwayAvailability {
+		// TODO: high frequent requests lead to "duplicate key value violates unique constraint "fairway_availability_bottleneck_id_surdat_key"
+		// in the database. This has to be resolved.
+		// All data subsets can also ocure as duplicates!
+		err = insertFAStmt.QueryRowContext(
+			ctx,
+			faRes.POSITION,
+			faRes.Bottleneck_id,
+			faRes.SURDAT,
+			faRes.Critical,
+			faRes.Date_Info,
+			faRes.Source,
+		).Scan(&faId)
+		if err != nil {
+			return nil, err
+		}
+		feedback.Info("Processing for Bottleneck %s", faRes.Bottleneck_id)
+		faids = append(faids, faRes.Bottleneck_id)
+		for _, bnPdfs := range faRes.Bottleneck_PDFs.PdfInfo {
+			_, err = insertBnPdfsStmt.ExecContext(
+				ctx,
+				faId,
+				bnPdfs.ProfilePdfFilename,
+				bnPdfs.ProfilePdfURL,
+				bnPdfs.PDF_Generation_Date,
+				bnPdfs.Source,
+			)
+			if err != nil {
+				return nil, err
+			}
+			feedback.Info("Add %d Pdfs", len(faRes.Bottleneck_PDFs.PdfInfo))
+		}
+		for _, efa := range faRes.Effective_fairway_availability.EffectiveFairwayAvailability {
+			los := efa.Level_of_Service
+			fgt := efa.Forecast_generation_time
+			if efa.Forecast_generation_time.Status == pgtype.Undefined {
+				fgt = pgtype.Timestamp{
+					Status: pgtype.Null,
+				}
+			}
+			_, err = insertEFAStmt.ExecContext(
+				ctx,
+				faId,
+				efa.Measure_date,
+				string(*los),
+				efa.Available_depth_value,
+				efa.Available_width_value,
+				efa.Water_level_value,
+				efa.Measure_type,
+				efa.Source,
+				fgt,
+				efa.Value_lifetime,
+			)
+			if err != nil {
+				return nil, err
+			}
+			feedback.Info("Add %d Effective Fairway Availability", len(
+				faRes.Effective_fairway_availability.EffectiveFairwayAvailability))
+		}
+		for _, fav := range faRes.Reference_values.ReferenceValue {
+			_, err = insertFAVStmt.ExecContext(
+				ctx,
+				faId,
+				fav.Level_of_Service,
+				fav.Fairway_depth,
+				fav.Fairway_width,
+				fav.Fairway_radius,
+				fav.Shallowest_spot_Lat,
+				fav.Shallowest_spot_Lon,
+			)
+			if err != nil {
+				return nil, err
+			}
+			feedback.Info("Add %d Reference Values",
+				len(faRes.Reference_values.ReferenceValue))
+		}
+	}
+	feedback.Info("Storing fairway availabilities took %s", time.Since(start))
+	if err = tx.Commit(); err == nil {
+		feedback.Info("Import of fairway availabilities was successful")
+	}
+
+	return faids, nil
+}
--- a/pkg/models/bn.go	Fri Dec 21 14:15:33 2018 +0100
+++ b/pkg/models/bn.go	Fri Dec 21 15:56:28 2018 +0100
@@ -18,3 +18,8 @@
 	Insecure  bool   `json:"insecure"`
 	SendEmail bool   `json:"send-email"`
 }
+
+type Bottleneck struct {
+	ID                 string
+	ResponsibleCountry string
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/pkg/models/fa.go	Fri Dec 21 15:56:28 2018 +0100
@@ -0,0 +1,21 @@
+// 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 by via donau
+//   – Österreichische Wasserstraßen-Gesellschaft mbH
+// Software engineering by Intevation GmbH
+//
+// Author(s):
+//  * Raimund Renkert <raimund.renkert@intevation.de>
+
+package models
+
+// FairwayAvailabilityImport contains data used to define the endpoint
+type FairwayAvailabilityImport struct {
+	URL       string `json:"url"`
+	Insecure  bool   `json:"insecure"`
+	SendEmail bool   `json:"send-email"`
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/pkg/soap/ifaf/service.go	Fri Dec 21 15:56:28 2018 +0100
@@ -0,0 +1,935 @@
+// 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 by via donau
+//   – Österreichische Wasserstraßen-Gesellschaft mbH
+// Software engineering by Intevation GmbH
+//
+// Author(s):
+//  * Raimund Renkert <raimund.renkert@intevation.de>
+
+package ifaf
+
+import (
+	"crypto/tls"
+	"encoding/xml"
+	"time"
+
+	"github.com/jackc/pgx/pgtype"
+
+	"gemma.intevation.de/gemma/pkg/soap"
+)
+
+// against "unused imports"
+var _ time.Time
+var _ xml.Name
+
+type Get_bottleneck_fa struct {
+	XMLName xml.Name `xml:"http://www.ris.eu/fairwayavailability/3.0 get_bottleneck_fa"`
+
+	Bottleneck_id *ArrayOfString `xml:"bottleneck_id,omitempty"`
+
+	Period *RequestedPeriod `xml:"period,omitempty"`
+}
+
+type Get_bottleneck_faResponse struct {
+	XMLName xml.Name `xml:"http://www.ris.eu/fairwayavailability/3.0 get_bottleneck_faResponse"`
+
+	Get_bottleneck_faResult *ArrayOfFairwayAvailability `xml:"get_bottleneck_faResult,omitempty"`
+}
+
+type Get_stretch_fa struct {
+	XMLName xml.Name `xml:"http://www.ris.eu/fairwayavailability/3.0 get_stretch_fa"`
+
+	ISRS *ArrayOfISRSPair `xml:"ISRS,omitempty"`
+
+	Period *RequestedPeriod `xml:"period,omitempty"`
+}
+
+type Get_stretch_faResponse struct {
+	XMLName xml.Name `xml:"http://www.ris.eu/fairwayavailability/3.0 get_stretch_faResponse"`
+
+	Get_stretch_faResult *ArrayOfFairwayAvailability `xml:"get_stretch_faResult,omitempty"`
+}
+
+type ArrayOfFairwayAvailability struct {
+	FairwayAvailability []*FairwayAvailability `xml:"FairwayAvailability,omitempty"`
+}
+
+type FairwayAvailability struct {
+	Bottleneck_id string `xml:"bottleneck_id,omitempty"`
+
+	SURDAT time.Time `xml:"SURDAT,omitempty"`
+
+	POSITION *PositionEnum `xml:"POSITION,omitempty"`
+
+	Reference_values *ArrayOfReferenceValue `xml:"Reference_values,omitempty"`
+
+	AdditionalData *ArrayOfKeyValuePair `xml:"AdditionalData,omitempty"`
+
+	Critical bool `xml:"Critical,omitempty"`
+
+	Bottleneck_PDFs *ArrayOfPdfInfo `xml:"Bottleneck_PDFs,omitempty"`
+
+	Effective_fairway_availability *ArrayOfEffectiveFairwayAvailability `xml:"Effective_fairway_availability,omitempty"`
+
+	Date_Info time.Time `xml:"Date_Info,omitempty"`
+
+	Source string `xml:"Source,omitempty"`
+}
+
+type ArrayOfPdfInfo struct {
+	PdfInfo []*PdfInfo `xml:"PdfInfo,omitempty"`
+}
+
+type PdfInfo struct {
+	ProfilePdfFilename string `xml:"ProfilePdfFilename,omitempty"`
+
+	ProfilePdfURL string `xml:"ProfilePdfURL,omitempty"`
+
+	PDF_Generation_Date time.Time `xml:"PDF_Generation_Date,omitempty"`
+
+	Source string `xml:"Source,omitempty"`
+}
+
+type ArrayOfEffectiveFairwayAvailability struct {
+	EffectiveFairwayAvailability []*EffectiveFairwayAvailability `xml:"EffectiveFairwayAvailability,omitempty"`
+}
+
+type EffectiveFairwayAvailability struct {
+	Available_depth_value int32 `xml:"Available_depth_value,omitempty"`
+
+	Available_width_value int32 `xml:"Available_width_value,omitempty"`
+
+	Water_level_value int32 `xml:"Water_level_value,omitempty"`
+
+	Measure_date time.Time `xml:"Measure_date,omitempty"`
+
+	Measure_type *MeasureType `xml:"Measure_type,omitempty"`
+
+	Source string `xml:"Source,omitempty"`
+
+	Level_of_Service *LosEnum `xml:"Level_of_Service,omitempty"`
+
+	Forecast_generation_time pgtype.Timestamp `xml:"Forecast_generation_time,omitempty"`
+
+	Value_lifetime time.Time `xml:"Value_lifetime,omitempty"`
+}
+
+type ArrayOfReferenceValue struct {
+	ReferenceValue []*ReferenceValue `xml:"ReferenceValue,omitempty"`
+}
+
+type ReferenceValue struct {
+	Fairway_depth int32 `xml:"fairway_depth,omitempty"`
+
+	Fairway_width int32 `xml:"fairway_width,omitempty"`
+
+	Fairway_radius int32 `xml:"fairway_radius,omitempty"`
+
+	Shallowest_spot_Lat float64 `xml:"Shallowest_spot_Lat,omitempty"`
+
+	Shallowest_spot_Lon float64 `xml:"Shallowest_spot_Lon,omitempty"`
+
+	Level_of_Service *LosEnum `xml:"Level_of_Service,omitempty"`
+}
+
+type ErrorCode string
+
+const (
+
+	// <summary> Description: message type not supported, Explanation:
+	// web service does not support the requested message type
+	// </summary>
+	ErrorCodeE010 ErrorCode = "e010"
+
+	// <summary> Description: syntax error in request, Explanation:
+	// request violates the schema for requests </summary>
+	ErrorCodeE100 ErrorCode = "e100"
+
+	// <summary> Description: incorrect message type, Explanation: given
+	// message type is not known </summary>
+	ErrorCodeE110 ErrorCode = "e110"
+
+	// <summary> Description: incorrect type-specific parameters,
+	// Explanation: type-specific parameters are erroneous </summary>
+	ErrorCodeE120 ErrorCode = "e120"
+
+	// <summary> Description: operation not known, Explanation: the
+	// requested operation is unknown </summary>
+	ErrorCodeE200 ErrorCode = "e200"
+
+	// <summary> Description: requested method or operation is not
+	// implemented </summary>
+	ErrorCodeE210 ErrorCode = "e210"
+
+	// <summary> Description: data source unavailable, Explanation: data
+	// source of the web service for is temporarily unavailable
+	// </summary>
+	ErrorCodeE300 ErrorCode = "e300"
+
+	// <summary> Description: too many results for request, Explanation:
+	// server is unable to handle number of results </summary>
+	ErrorCodeE310 ErrorCode = "e310"
+
+	// <summary> Description: unexpected or other error
+	// </summary>
+	ErrorCodeE999 ErrorCode = "e999"
+)
+
+type CountryCode string
+
+const (
+	CountryCodeAF CountryCode = "AF"
+
+	CountryCodeAX CountryCode = "AX"
+
+	CountryCodeAL CountryCode = "AL"
+
+	CountryCodeDZ CountryCode = "DZ"
+
+	CountryCodeAS CountryCode = "AS"
+
+	CountryCodeAD CountryCode = "AD"
+
+	CountryCodeAO CountryCode = "AO"
+
+	CountryCodeAI CountryCode = "AI"
+
+	CountryCodeAQ CountryCode = "AQ"
+
+	CountryCodeAG CountryCode = "AG"
+
+	CountryCodeAR CountryCode = "AR"
+
+	CountryCodeAM CountryCode = "AM"
+
+	CountryCodeAW CountryCode = "AW"
+
+	CountryCodeAU CountryCode = "AU"
+
+	CountryCodeAT CountryCode = "AT"
+
+	CountryCodeAZ CountryCode = "AZ"
+
+	CountryCodeBS CountryCode = "BS"
+
+	CountryCodeBH CountryCode = "BH"
+
+	CountryCodeBD CountryCode = "BD"
+
+	CountryCodeBB CountryCode = "BB"
+
+	CountryCodeBY CountryCode = "BY"
+
+	CountryCodeBE CountryCode = "BE"
+
+	CountryCodeBZ CountryCode = "BZ"
+
+	CountryCodeBJ CountryCode = "BJ"
+
+	CountryCodeBM CountryCode = "BM"
+
+	CountryCodeBT CountryCode = "BT"
+
+	CountryCodeBO CountryCode = "BO"
+
+	CountryCodeBQ CountryCode = "BQ"
+
+	CountryCodeBA CountryCode = "BA"
+
+	CountryCodeBW CountryCode = "BW"
+
+	CountryCodeBV CountryCode = "BV"
+
+	CountryCodeBR CountryCode = "BR"
+
+	CountryCodeIO CountryCode = "IO"
+
+	CountryCodeBN CountryCode = "BN"
+
+	CountryCodeBG CountryCode = "BG"
+
+	CountryCodeBF CountryCode = "BF"
+
+	CountryCodeBI CountryCode = "BI"
+
+	CountryCodeCV CountryCode = "CV"
+
+	CountryCodeKH CountryCode = "KH"
+
+	CountryCodeCM CountryCode = "CM"
+
+	CountryCodeCA CountryCode = "CA"
+
+	CountryCodeKY CountryCode = "KY"
+
+	CountryCodeCF CountryCode = "CF"
+
+	CountryCodeTD CountryCode = "TD"
+
+	CountryCodeCL CountryCode = "CL"
+
+	CountryCodeCN CountryCode = "CN"
+
+	CountryCodeCX CountryCode = "CX"
+
+	CountryCodeCC CountryCode = "CC"
+
+	CountryCodeCO CountryCode = "CO"
+
+	CountryCodeKM CountryCode = "KM"
+
+	CountryCodeCG CountryCode = "CG"
+
+	CountryCodeCD CountryCode = "CD"
+
+	CountryCodeCK CountryCode = "CK"
+
+	CountryCodeCR CountryCode = "CR"
+
+	CountryCodeCI CountryCode = "CI"
+
+	CountryCodeHR CountryCode = "HR"
+
+	CountryCodeCU CountryCode = "CU"
+
+	CountryCodeCW CountryCode = "CW"
+
+	CountryCodeCY CountryCode = "CY"
+
+	CountryCodeCZ CountryCode = "CZ"
+
+	CountryCodeDK CountryCode = "DK"
+
+	CountryCodeDJ CountryCode = "DJ"
+
+	CountryCodeDM CountryCode = "DM"
+
+	CountryCodeDO CountryCode = "DO"
+
+	CountryCodeEC CountryCode = "EC"
+
+	CountryCodeEG CountryCode = "EG"
+
+	CountryCodeSV CountryCode = "SV"
+
+	CountryCodeGQ CountryCode = "GQ"
+
+	CountryCodeER CountryCode = "ER"
+
+	CountryCodeEE CountryCode = "EE"
+
+	CountryCodeET CountryCode = "ET"
+
+	CountryCodeFK CountryCode = "FK"
+
+	CountryCodeFO CountryCode = "FO"
+
+	CountryCodeFJ CountryCode = "FJ"
+
+	CountryCodeFI CountryCode = "FI"
+
+	CountryCodeFR CountryCode = "FR"
+
+	CountryCodeGF CountryCode = "GF"
+
+	CountryCodePF CountryCode = "PF"
+
+	CountryCodeTF CountryCode = "TF"
+
+	CountryCodeGA CountryCode = "GA"
+
+	CountryCodeGM CountryCode = "GM"
+
+	CountryCodeGE CountryCode = "GE"
+
+	CountryCodeDE CountryCode = "DE"
+
+	CountryCodeGH CountryCode = "GH"
+
+	CountryCodeGI CountryCode = "GI"
+
+	CountryCodeGR CountryCode = "GR"
+
+	CountryCodeGL CountryCode = "GL"
+
+	CountryCodeGD CountryCode = "GD"
+
+	CountryCodeGP CountryCode = "GP"
+
+	CountryCodeGU CountryCode = "GU"
+
+	CountryCodeGT CountryCode = "GT"
+
+	CountryCodeGG CountryCode = "GG"
+
+	CountryCodeGN CountryCode = "GN"
+
+	CountryCodeGW CountryCode = "GW"
+
+	CountryCodeGY CountryCode = "GY"
+
+	CountryCodeHT CountryCode = "HT"
+
+	CountryCodeHM CountryCode = "HM"
+
+	CountryCodeVA CountryCode = "VA"
+
+	CountryCodeHN CountryCode = "HN"
+
+	CountryCodeHK CountryCode = "HK"
+
+	CountryCodeHU CountryCode = "HU"
+
+	CountryCodeIS CountryCode = "IS"
+
+	CountryCodeIN CountryCode = "IN"
+
+	CountryCodeID CountryCode = "ID"
+
+	CountryCodeIR CountryCode = "IR"
+
+	CountryCodeIQ CountryCode = "IQ"
+
+	CountryCodeIE CountryCode = "IE"
+
+	CountryCodeIM CountryCode = "IM"
+
+	CountryCodeIL CountryCode = "IL"
+
+	CountryCodeIT CountryCode = "IT"
+
+	CountryCodeJM CountryCode = "JM"
+
+	CountryCodeJP CountryCode = "JP"
+
+	CountryCodeJE CountryCode = "JE"
+
+	CountryCodeJO CountryCode = "JO"
+
+	CountryCodeKZ CountryCode = "KZ"
+
+	CountryCodeKE CountryCode = "KE"
+
+	CountryCodeKI CountryCode = "KI"
+
+	CountryCodeKP CountryCode = "KP"
+
+	CountryCodeKR CountryCode = "KR"
+
+	CountryCodeKW CountryCode = "KW"
+
+	CountryCodeKG CountryCode = "KG"
+
+	CountryCodeLA CountryCode = "LA"
+
+	CountryCodeLV CountryCode = "LV"
+
+	CountryCodeLB CountryCode = "LB"
+
+	CountryCodeLS CountryCode = "LS"
+
+	CountryCodeLR CountryCode = "LR"
+
+	CountryCodeLY CountryCode = "LY"
+
+	CountryCodeLI CountryCode = "LI"
+
+	CountryCodeLT CountryCode = "LT"
+
+	CountryCodeLU CountryCode = "LU"
+
+	CountryCodeMO CountryCode = "MO"
+
+	CountryCodeMK CountryCode = "MK"
+
+	CountryCodeMG CountryCode = "MG"
+
+	CountryCodeMW CountryCode = "MW"
+
+	CountryCodeMY CountryCode = "MY"
+
+	CountryCodeMV CountryCode = "MV"
+
+	CountryCodeML CountryCode = "ML"
+
+	CountryCodeMT CountryCode = "MT"
+
+	CountryCodeMH CountryCode = "MH"
+
+	CountryCodeMQ CountryCode = "MQ"
+
+	CountryCodeMR CountryCode = "MR"
+
+	CountryCodeMU CountryCode = "MU"
+
+	CountryCodeYT CountryCode = "YT"
+
+	CountryCodeMX CountryCode = "MX"
+
+	CountryCodeFM CountryCode = "FM"
+
+	CountryCodeMD CountryCode = "MD"
+
+	CountryCodeMC CountryCode = "MC"
+
+	CountryCodeMN CountryCode = "MN"
+
+	CountryCodeME CountryCode = "ME"
+
+	CountryCodeMS CountryCode = "MS"
+
+	CountryCodeMA CountryCode = "MA"
+
+	CountryCodeMZ CountryCode = "MZ"
+
+	CountryCodeMM CountryCode = "MM"
+
+	CountryCodeNA CountryCode = "NA"
+
+	CountryCodeNR CountryCode = "NR"
+
+	CountryCodeNP CountryCode = "NP"
+
+	CountryCodeNL CountryCode = "NL"
+
+	CountryCodeNC CountryCode = "NC"
+
+	CountryCodeNZ CountryCode = "NZ"
+
+	CountryCodeNI CountryCode = "NI"
+
+	CountryCodeNE CountryCode = "NE"
+
+	CountryCodeNG CountryCode = "NG"
+
+	CountryCodeNU CountryCode = "NU"
+
+	CountryCodeNF CountryCode = "NF"
+
+	CountryCodeMP CountryCode = "MP"
+
+	CountryCodeNO CountryCode = "NO"
+
+	CountryCodeOM CountryCode = "OM"
+
+	CountryCodePK CountryCode = "PK"
+
+	CountryCodePW CountryCode = "PW"
+
+	CountryCodePS CountryCode = "PS"
+
+	CountryCodePA CountryCode = "PA"
+
+	CountryCodePG CountryCode = "PG"
+
+	CountryCodePY CountryCode = "PY"
+
+	CountryCodePE CountryCode = "PE"
+
+	CountryCodePH CountryCode = "PH"
+
+	CountryCodePN CountryCode = "PN"
+
+	CountryCodePL CountryCode = "PL"
+
+	CountryCodePT CountryCode = "PT"
+
+	CountryCodePR CountryCode = "PR"
+
+	CountryCodeQA CountryCode = "QA"
+
+	CountryCodeRE CountryCode = "RE"
+
+	CountryCodeRO CountryCode = "RO"
+
+	CountryCodeRU CountryCode = "RU"
+
+	CountryCodeRW CountryCode = "RW"
+
+	CountryCodeBL CountryCode = "BL"
+
+	CountryCodeSH CountryCode = "SH"
+
+	CountryCodeKN CountryCode = "KN"
+
+	CountryCodeLC CountryCode = "LC"
+
+	CountryCodeMF CountryCode = "MF"
+
+	CountryCodePM CountryCode = "PM"
+
+	CountryCodeVC CountryCode = "VC"
+
+	CountryCodeWS CountryCode = "WS"
+
+	CountryCodeSM CountryCode = "SM"
+
+	CountryCodeST CountryCode = "ST"
+
+	CountryCodeSA CountryCode = "SA"
+
+	CountryCodeSN CountryCode = "SN"
+
+	CountryCodeRS CountryCode = "RS"
+
+	CountryCodeSC CountryCode = "SC"
+
+	CountryCodeSL CountryCode = "SL"
+
+	CountryCodeSG CountryCode = "SG"
+
+	CountryCodeSX CountryCode = "SX"
+
+	CountryCodeSK CountryCode = "SK"
+
+	CountryCodeSI CountryCode = "SI"
+
+	CountryCodeSB CountryCode = "SB"
+
+	CountryCodeSO CountryCode = "SO"
+
+	CountryCodeZA CountryCode = "ZA"
+
+	CountryCodeGS CountryCode = "GS"
+
+	CountryCodeSS CountryCode = "SS"
+
+	CountryCodeES CountryCode = "ES"
+
+	CountryCodeLK CountryCode = "LK"
+
+	CountryCodeSD CountryCode = "SD"
+
+	CountryCodeSR CountryCode = "SR"
+
+	CountryCodeSJ CountryCode = "SJ"
+
+	CountryCodeSZ CountryCode = "SZ"
+
+	CountryCodeSE CountryCode = "SE"
+
+	CountryCodeCH CountryCode = "CH"
+
+	CountryCodeSY CountryCode = "SY"
+
+	CountryCodeTW CountryCode = "TW"
+
+	CountryCodeTJ CountryCode = "TJ"
+
+	CountryCodeTZ CountryCode = "TZ"
+
+	CountryCodeTH CountryCode = "TH"
+
+	CountryCodeTL CountryCode = "TL"
+
+	CountryCodeTG CountryCode = "TG"
+
+	CountryCodeTK CountryCode = "TK"
+
+	CountryCodeTO CountryCode = "TO"
+
+	CountryCodeTT CountryCode = "TT"
+
+	CountryCodeTN CountryCode = "TN"
+
+	CountryCodeTR CountryCode = "TR"
+
+	CountryCodeTM CountryCode = "TM"
+
+	CountryCodeTC CountryCode = "TC"
+
+	CountryCodeTV CountryCode = "TV"
+
+	CountryCodeUG CountryCode = "UG"
+
+	CountryCodeUA CountryCode = "UA"
+
+	CountryCodeAE CountryCode = "AE"
+
+	CountryCodeGB CountryCode = "GB"
+
+	CountryCodeUS CountryCode = "US"
+
+	CountryCodeUM CountryCode = "UM"
+
+	CountryCodeUY CountryCode = "UY"
+
+	CountryCodeUZ CountryCode = "UZ"
+
+	CountryCodeVU CountryCode = "VU"
+
+	CountryCodeVE CountryCode = "VE"
+
+	CountryCodeVN CountryCode = "VN"
+
+	CountryCodeVG CountryCode = "VG"
+
+	CountryCodeVI CountryCode = "VI"
+
+	CountryCodeWF CountryCode = "WF"
+
+	CountryCodeEH CountryCode = "EH"
+
+	CountryCodeYE CountryCode = "YE"
+
+	CountryCodeZM CountryCode = "ZM"
+
+	CountryCodeZW CountryCode = "ZW"
+)
+
+type CoverageEnum string
+
+const (
+	CoverageEnumCrossProfiles CoverageEnum = "CrossProfiles"
+
+	CoverageEnumLongitudinalProfiles CoverageEnum = "LongitudinalProfiles"
+
+	CoverageEnumFairway CoverageEnum = "Fairway"
+
+	CoverageEnumRiver CoverageEnum = "River"
+
+	CoverageEnumRiverBanks CoverageEnum = "RiverBanks"
+)
+
+type DepthReferenceEnum string
+
+const (
+	DepthReferenceEnumNAP DepthReferenceEnum = "NAP"
+
+	DepthReferenceEnumKP DepthReferenceEnum = "KP"
+
+	DepthReferenceEnumFZP DepthReferenceEnum = "FZP"
+
+	DepthReferenceEnumADR DepthReferenceEnum = "ADR"
+
+	DepthReferenceEnumTAW DepthReferenceEnum = "TAW"
+
+	DepthReferenceEnumPUL DepthReferenceEnum = "PUL"
+
+	DepthReferenceEnumNGM DepthReferenceEnum = "NGM"
+
+	DepthReferenceEnumETRS DepthReferenceEnum = "ETRS"
+
+	DepthReferenceEnumPOT DepthReferenceEnum = "POT"
+
+	DepthReferenceEnumLDC DepthReferenceEnum = "LDC"
+
+	DepthReferenceEnumHDC DepthReferenceEnum = "HDC"
+
+	DepthReferenceEnumZPG DepthReferenceEnum = "ZPG"
+
+	DepthReferenceEnumGLW DepthReferenceEnum = "GLW"
+
+	DepthReferenceEnumHSW DepthReferenceEnum = "HSW"
+
+	DepthReferenceEnumLNW DepthReferenceEnum = "LNW"
+
+	DepthReferenceEnumHNW DepthReferenceEnum = "HNW"
+
+	DepthReferenceEnumIGN DepthReferenceEnum = "IGN"
+
+	DepthReferenceEnumWGS DepthReferenceEnum = "WGS"
+
+	DepthReferenceEnumRN DepthReferenceEnum = "RN"
+
+	DepthReferenceEnumHBO DepthReferenceEnum = "HBO"
+)
+
+type LimitingFactorEnum string
+
+const (
+	LimitingFactorEnumDepth LimitingFactorEnum = "depth"
+
+	LimitingFactorEnumWidth LimitingFactorEnum = "width"
+
+	LimitingFactorEnumCurveRadius LimitingFactorEnum = "curveRadius"
+)
+
+type LosEnum string
+
+const (
+	LosEnumNotAvailable LosEnum = "NotAvailable"
+
+	LosEnumLOS1 LosEnum = "LOS1"
+
+	LosEnumLOS2 LosEnum = "LOS2"
+
+	LosEnumLOS3 LosEnum = "LOS3"
+)
+
+type MaterialEnum string
+
+const (
+	MaterialEnumGravel MaterialEnum = "Gravel"
+
+	MaterialEnumRocky MaterialEnum = "Rocky"
+
+	MaterialEnumStone MaterialEnum = "Stone"
+
+	MaterialEnumAndesite MaterialEnum = "Andesite"
+
+	MaterialEnumSleazyAndesite MaterialEnum = "SleazyAndesite"
+
+	MaterialEnumSandyGravel MaterialEnum = "SandyGravel"
+
+	MaterialEnumMarl MaterialEnum = "Marl"
+
+	MaterialEnumSand MaterialEnum = "Sand"
+
+	MaterialEnumSarmatianLimestone MaterialEnum = "SarmatianLimestone"
+
+	MaterialEnumSandstonePeaks MaterialEnum = "SandstonePeaks"
+
+	MaterialEnumRoughSandyGravel MaterialEnum = "RoughSandyGravel"
+)
+
+type MeasureType string
+
+const (
+	MeasureTypeMeasured MeasureType = "Measured"
+
+	MeasureTypeForecasted MeasureType = "Forecasted"
+
+	MeasureTypeMinimumGuaranteed MeasureType = "MinimumGuaranteed"
+)
+
+type PositionEnum string
+
+const (
+	PositionEnumRedBuoy PositionEnum = "RedBuoy"
+
+	PositionEnumGreenBuoy PositionEnum = "GreenBuoy"
+
+	PositionEnumRightBank PositionEnum = "RightBank"
+
+	PositionEnumLeftBank PositionEnum = "LeftBank"
+
+	PositionEnumMiddle PositionEnum = "Middle"
+
+	PositionEnumAll PositionEnum = "All"
+)
+
+type SurtypEnum string
+
+const (
+	SurtypEnumMultibeam SurtypEnum = "Multibeam"
+
+	SurtypEnumSinglebeam SurtypEnum = "Singlebeam"
+
+	SurtypEnumADCP SurtypEnum = "ADCP"
+
+	SurtypEnumInspectionTour SurtypEnum = "InspectionTour"
+)
+
+type Error struct {
+	Detail string `xml:"detail,omitempty"`
+
+	Error_code *ErrorCode `xml:"error_code,omitempty"`
+}
+
+type ArrayOfMaterial struct {
+	Material []*MaterialEnum `xml:"Material,omitempty"`
+}
+
+type ArrayOfKeyValuePair struct {
+	KeyValuePair []*KeyValuePair `xml:"KeyValuePair,omitempty"`
+}
+
+type KeyValuePair struct {
+	Key string `xml:"Key,omitempty"`
+
+	Value string `xml:"Value,omitempty"`
+}
+
+type ArrayOfISRSPair struct {
+	ISRSPair []*ISRSPair `xml:"ISRSPair,omitempty"`
+}
+
+type ISRSPair struct {
+	FromISRS string `xml:"fromISRS,omitempty"`
+
+	ToISRS string `xml:"toISRS,omitempty"`
+}
+
+type RequestedPeriod struct {
+	Date_start time.Time `xml:"Date_start,omitempty"`
+
+	Date_end time.Time `xml:"Date_end,omitempty"`
+
+	Value_interval int32 `xml:"Value_interval,omitempty"`
+}
+
+type ArrayOfString struct {
+	String []string `xml:"http://www.ris.eu/wamos/common/3.0 string,omitempty"`
+}
+
+type Char int32
+
+type Duration *Duration
+
+type Guid string
+
+type IFairwayAvailabilityService interface {
+
+	// Error can be either of the following types:
+	//
+	//   - ErrorFault
+
+	Get_bottleneck_fa(request *Get_bottleneck_fa) (*Get_bottleneck_faResponse, error)
+
+	// Error can be either of the following types:
+	//
+	//   - ErrorFault
+
+	Get_stretch_fa(request *Get_stretch_fa) (*Get_stretch_faResponse, error)
+}
+
+type FairwayAvailabilityService struct {
+	client *soap.SOAPClient
+}
+
+func NewFairwayAvailabilityService(url string, tls bool, auth *soap.BasicAuth) *FairwayAvailabilityService {
+	if url == "" {
+		url = ""
+	}
+	client := soap.NewSOAPClient(url, tls, auth)
+	return &FairwayAvailabilityService{
+		client: client,
+	}
+}
+
+func NewFairwayAvailabilityServiceWithTLS(url string, tlsCfg *tls.Config, auth *soap.BasicAuth) *FairwayAvailabilityService {
+	if url == "" {
+		url = ""
+	}
+	client := soap.NewSOAPClientWithTLSConfig(url, tlsCfg, auth)
+
+	return &FairwayAvailabilityService{
+		client: client,
+	}
+}
+
+func (service *FairwayAvailabilityService) Get_bottleneck_fa(request *Get_bottleneck_fa) (*Get_bottleneck_faResponse, error) {
+	response := new(Get_bottleneck_faResponse)
+	err := service.client.Call("http://www.ris.eu/fairwayavailability/3.0/IFairwayAvailabilityService/get_bottleneck_fa", request, response)
+	if err != nil {
+		return nil, err
+	}
+
+	return response, nil
+}
+
+func (service *FairwayAvailabilityService) Get_stretch_fa(request *Get_stretch_fa) (*Get_stretch_faResponse, error) {
+	response := new(Get_stretch_faResponse)
+	err := service.client.Call("http://www.ris.eu/fairwayavailability/3.0/IFairwayAvailabilityService/get_stretch_fa", request, response)
+	if err != nil {
+		return nil, err
+	}
+
+	return response, nil
+}
--- a/schema/gemma.sql	Fri Dec 21 14:15:33 2018 +0100
+++ b/schema/gemma.sql	Fri Dec 21 15:56:28 2018 +0100
@@ -139,9 +139,13 @@
 );
 
 CREATE TABLE levels_of_service (
-    level_of_service smallint PRIMARY KEY
+    level_of_service smallint PRIMARY KEY,
+    name varchar(4)
 );
-INSERT INTO levels_of_service VALUES (1), (2), (3);
+INSERT INTO levels_of_service (
+    level_of_service,
+    name
+) VALUES (1, 'LOS1'), (2, 'LOS2'), (3, 'LOS3');
 
 CREATE TABLE riverbed_materials (
     material varchar PRIMARY KEY