changeset 3659:66f2cb789905 single-beam

Merged default into single-beam branch.
author Sascha L. Teichmann <sascha.teichmann@intevation.de>
date Fri, 14 Jun 2019 12:02:58 +0200
parents 1c3df921361d (current diff) 7126fdc5779a (diff)
children b30c0b02e8fb
files pkg/imports/sr.go
diffstat 28 files changed, 732 insertions(+), 247 deletions(-) [+]
line wrap: on
line diff
--- a/client/src/assets/application.scss	Fri Jun 14 11:59:14 2019 +0200
+++ b/client/src/assets/application.scss	Fri Jun 14 12:02:58 2019 +0200
@@ -222,3 +222,8 @@
   width: 50% !important;
   height: 50% !important;
 }
+
+.custom-control-input:checked~.custom-control-label::before {
+    border-color: $color-info;
+    background-color: $color-info;
+}
Binary file client/src/assets/linestring_arrow_grey.png has changed
--- a/client/src/components/Login.vue	Fri Jun 14 11:59:14 2019 +0200
+++ b/client/src/components/Login.vue	Fri Jun 14 12:02:58 2019 +0200
@@ -52,7 +52,7 @@
         </div>
         <button
           v-if="showPasswordReset == false"
-          class="btn btn-primary btn-block shadow-sm"
+          class="btn btn-info btn-block shadow-sm"
           :disabled="submitted || showPasswordReset"
           type="submit"
         >
--- a/client/src/components/fairway/Fairwayprofile.vue	Fri Jun 14 11:59:14 2019 +0200
+++ b/client/src/components/fairway/Fairwayprofile.vue	Fri Jun 14 12:02:58 2019 +0200
@@ -94,8 +94,9 @@
       </DiagramLegend>
       <div
         ref="diagramContainer"
-        class="d-flex flex-fill justify-content-center align-items-center diagram-container"
+        class="d-flex flex-fill justify-content-center align-items-center diagram-container position-relative"
       >
+        <div class="direction-indicator"></div>
         <div v-if="!fairwayData">
           <translate>No data available.</translate>
         </div>
@@ -104,6 +105,30 @@
   </div>
 </template>
 
+<style lang="sass" scoped>
+.direction-indicator
+  width: 70px
+  height: 0
+  border-top: dashed 2px #333
+  position: absolute
+  bottom: 20px
+  left: 115px
+  margin-left: -35px
+  &::after
+    content: ""
+    width: 0
+    height: 0
+    border-width: 10px
+    border-top-width: 5px
+    border-bottom-width: 5px
+    border-style: solid
+    border-color: transparent
+    border-left-color: #333
+    position: absolute
+    right: -20px
+    top: -6px
+</style>
+
 <script>
 /* This is Free Software under GNU Affero General Public License v >= 3.0
  * without warranty, see README.md and license for details.
--- a/client/src/components/map/layers.js	Fri Jun 14 11:59:14 2019 +0200
+++ b/client/src/components/map/layers.js	Fri Jun 14 12:02:58 2019 +0200
@@ -126,8 +126,15 @@
       // linestring
       new Style({
         stroke: new Stroke({
+          color: "#FFFFFF",
+          width: 5,
+          lineDash: [7, 7]
+        })
+      }),
+      new Style({
+        stroke: new Stroke({
           color: "#333333",
-          width: 2,
+          width: 3,
           lineDash: [7, 7]
         })
       })
@@ -230,9 +237,9 @@
         visible: true,
         source: new ImageSource({
           preload: 1,
-          url: "https://service.d4d-portal.info/wms/",
+          url: store.state.application.config.ecdis_wms_url,
           crossOrigin: "anonymous",
-          params: { LAYERS: "d4d", VERSION: "1.1.1", TILED: true }
+          params: JSON.parse(store.state.application.config.ecdis_wms_params)
         })
       }),
       new ImageLayer({
--- a/client/src/components/systemconfiguration/DataAccuracy.vue	Fri Jun 14 11:59:14 2019 +0200
+++ b/client/src/components/systemconfiguration/DataAccuracy.vue	Fri Jun 14 12:02:58 2019 +0200
@@ -330,7 +330,17 @@
   },
   methods: {
     submit() {
-      this.$store.commit("application/config", this.config);
+      this.$store.dispatch("application/saveConfig", {
+        bn_revtime_multiplier: this.config.bn_revtime_multiplier,
+        gm_latest_hours: this.config.gm_latest_hours,
+        gm_min_values_14d: this.config.gm_min_values_14d,
+        gm_forecast_offset_24h: this.config.gm_forecast_offset_24h,
+        gm_forecast_offset_72h: this.config.gm_forecast_offset_72h,
+        gm_forecast_vs_reality_nsc_24h: this.config
+          .gm_forecast_vs_reality_nsc_24h,
+        gm_forecast_vs_reality_nsc_72h: this.config
+          .gm_forecast_vs_reality_nsc_72h
+      });
     }
   }
 };
--- a/client/src/components/systemconfiguration/MapLayers.vue	Fri Jun 14 11:59:14 2019 +0200
+++ b/client/src/components/systemconfiguration/MapLayers.vue	Fri Jun 14 12:02:58 2019 +0200
@@ -5,14 +5,50 @@
       <div class="row">
         <div class="col-sm-6">
           <div class="form-group">
-            <label for="ecdis-url" class="font-weight-bold">ECDIS URL</label>
+            <label for="ecdis-url" class="font-weight-bold">
+              ECDIS WMS URL
+            </label>
             <input
               type="url"
               class="form-control"
               placeholder="https://..."
-              v-model="config.ecdis_url"
+              @input="lookupWMSCapabilities()"
+              v-model="config.ecdis_wms_url"
             />
           </div>
+          <label for="ecdis-layers">
+            <translate>Layers</translate>
+            <transition name="fade"
+              ><font-awesome-icon
+                icon="spinner"
+                spin
+                v-if="availableWMSLayersLoading"
+                class="ml-2"
+            /></transition>
+          </label>
+          <div class="container-fluid">
+            <div class="row">
+              <div
+                class="custom-control custom-checkbox col-sm-4"
+                v-for="layer in availableWMSLayers"
+                :key="'layer-' + layer"
+              >
+                <input
+                  type="checkbox"
+                  class="custom-control-input"
+                  v-model="selectedWMSLayers"
+                  :id="'layer-' + layer"
+                  :value="layer"
+                />
+                <label
+                  class="custom-control-label text-break"
+                  :for="'layer-' + layer"
+                >
+                  {{ layer }}
+                </label>
+              </div>
+            </div>
+          </div>
         </div>
       </div>
     </div>
@@ -39,15 +75,84 @@
  * Markus Kottländer <markus@intevation.de>
  */
 import { mapState } from "vuex";
+import WMSCapabilities from "ol/format/WMSCapabilities";
+import { HTTP } from "@/lib/http";
+
+const WMSCapabilitiesParser = new WMSCapabilities();
 
 export default {
+  data() {
+    return {
+      selectedWMSLayers: [],
+      availableWMSLayers: [],
+      availableWMSLayersLoading: false,
+      lookupWMSCapabilitiesTimeout: null,
+      wmsVersion: ""
+    };
+  },
   computed: {
     ...mapState("application", ["config"])
   },
   methods: {
+    lookupWMSCapabilities() {
+      if (this.lookupWMSCapabilitiesTimeout) {
+        clearTimeout(this.lookupWMSCapabilitiesTimeout);
+      }
+      this.lookupWMSCapabilitiesTimeout = setTimeout(() => {
+        let url;
+        try {
+          let urlParts = new URL(this.config.ecdis_wms_url);
+          url =
+            urlParts.protocol +
+            "//" +
+            urlParts.host +
+            urlParts.pathname.trim("/") +
+            "/";
+        } catch (e) {
+          url = this.config.ecdis_wms_url;
+        }
+        this.availableWMSLayersLoading = true;
+        HTTP.get(url + "?request=GetCapabilities&service=WMS")
+          .then(response => {
+            let capabilities = WMSCapabilitiesParser.read(response.data);
+            this.wmsVersion = capabilities.version;
+            this.availableWMSLayers = [];
+            this.getLayersRecursive(capabilities.Capability.Layer.Layer);
+          })
+          .catch(() => {
+            this.availableWMSLayers = [];
+          })
+          .finally(() => (this.availableWMSLayersLoading = false));
+      }, 500);
+    },
+    getLayersRecursive(layers) {
+      layers.forEach(l => {
+        if (l.hasOwnProperty("Layer")) {
+          this.getLayersRecursive(l.Layer);
+        } else {
+          this.availableWMSLayers.push(l.Name);
+        }
+      });
+    },
     submit() {
-      this.$store.commit("application/config", this.config);
+      this.$store.dispatch("application/saveConfig", {
+        ecdis_wms_url: this.config.ecdis_wms_url,
+        ecdis_wms_params: JSON.stringify({
+          LAYERS: this.selectedWMSLayers
+            .filter(l => this.availableWMSLayers.find(al => al === l))
+            .join(","),
+          VERSION: this.wmsVersion,
+          TILED: true
+        })
+      });
     }
+  },
+  mounted() {
+    let ecdisWmsParams = JSON.parse(this.config.ecdis_wms_params);
+    if (ecdisWmsParams.LAYERS) {
+      this.selectedWMSLayers = ecdisWmsParams.LAYERS.split(",");
+    }
+    this.lookupWMSCapabilities();
   }
 };
 </script>
--- a/client/src/components/systemconfiguration/MorphologyClassbreaks.vue	Fri Jun 14 11:59:14 2019 +0200
+++ b/client/src/components/systemconfiguration/MorphologyClassbreaks.vue	Fri Jun 14 12:02:58 2019 +0200
@@ -8,11 +8,11 @@
       <div class="d-flex flex-wrap">
         <div
           class="input-group mb-3 mr-2 classbreak"
-          v-for="(value, i) in config.morphology_classbreaks"
+          v-for="(value, i) in morphologyClassbreaks"
           :key="i"
         >
           <input
-            v-model="config.morphology_classbreaks[i]"
+            v-model="morphologyClassbreaks[i]"
             type="number"
             min="0"
             step="0.1"
@@ -22,7 +22,7 @@
             <button
               class="btn btn-sm btn-outline-secondary"
               type="button"
-              @click="config.morphology_classbreaks.splice(i, 1)"
+              @click="morphologyClassbreaks.splice(i, 1)"
             >
               <font-awesome-icon icon="times" />
             </button>
@@ -31,11 +31,9 @@
         <button
           class="btn btn-sm btn-success mb-3"
           @click="
-            config.morphology_classbreaks.push(
-              config.morphology_classbreaks.length
-                ? config.morphology_classbreaks[
-                    config.morphology_classbreaks.length - 1
-                  ]
+            morphologyClassbreaks.push(
+              morphologyClassbreaks.length
+                ? morphologyClassbreaks[morphologyClassbreaks.length - 1]
                 : 1
             )
           "
@@ -51,11 +49,11 @@
       <div class="d-flex flex-wrap">
         <div
           class="input-group mb-3 mr-2 classbreak"
-          v-for="(value, i) in config.morphology_classbreaks_compare"
+          v-for="(value, i) in morphologyClassbreaksCompare"
           :key="i"
         >
           <input
-            v-model="config.morphology_classbreaks_compare[i]"
+            v-model="morphologyClassbreaksCompare[i]"
             type="number"
             step="0.1"
             class="form-control form-control-sm"
@@ -64,7 +62,7 @@
             <button
               class="btn btn-sm btn-outline-secondary"
               type="button"
-              @click="config.morphology_classbreaks_compare.splice(i, 1)"
+              @click="morphologyClassbreaksCompare.splice(i, 1)"
             >
               <font-awesome-icon icon="times" />
             </button>
@@ -73,10 +71,10 @@
         <button
           class="btn btn-sm btn-success mb-3"
           @click="
-            config.morphology_classbreaks_compare.push(
-              config.morphology_classbreaks_compare.length
-                ? config.morphology_classbreaks_compare[
-                    config.morphology_classbreaks_compare.length - 1
+            morphologyClassbreaksCompare.push(
+              morphologyClassbreaksCompare.length
+                ? morphologyClassbreaksCompare[
+                    morphologyClassbreaksCompare.length - 1
                   ]
                 : 1
             )
@@ -122,13 +120,32 @@
 import { mapState } from "vuex";
 
 export default {
+  data() {
+    return {
+      morphologyClassbreaks: [],
+      morphologyClassbreaksCompare: []
+    };
+  },
   computed: {
     ...mapState("application", ["config"])
   },
   methods: {
     submit() {
-      this.$store.commit("application/config", this.config);
+      this.$store.dispatch("application/saveConfig", {
+        morphology_classbreaks: this.morphologyClassbreaks.join(","),
+        morphology_classbreaks_compare: this.morphologyClassbreaksCompare.join(
+          ","
+        )
+      });
     }
+  },
+  mounted() {
+    this.morphologyClassbreaks = this.config.morphology_classbreaks
+      .split(",")
+      .map(n => Number(n));
+    this.morphologyClassbreaksCompare = this.config.morphology_classbreaks_compare
+      .split(",")
+      .map(n => Number(n));
   }
 };
 </script>
--- a/client/src/components/systemconfiguration/Systemconfiguration.vue	Fri Jun 14 11:59:14 2019 +0200
+++ b/client/src/components/systemconfiguration/Systemconfiguration.vue	Fri Jun 14 12:02:58 2019 +0200
@@ -6,6 +6,9 @@
       <div class="text-left flex-fill" style="overflow: auto">
         <PDFTemplates />
         <ColorSettings v-if="isSysAdmin" />
+        <MapLayers v-if="isSysAdmin" />
+        <DataAccuracy v-if="isSysAdmin" />
+        <MorphologyClassbreaks v-if="isSysAdmin" />
       </div>
       <!-- card-body -->
     </div>
@@ -35,7 +38,10 @@
   components: {
     Spacer: () => import("../Spacer"),
     PDFTemplates: () => import("./PDFTemplates"),
-    ColorSettings: () => import("./ColorSettings")
+    ColorSettings: () => import("./ColorSettings"),
+    MapLayers: () => import("./MapLayers"),
+    DataAccuracy: () => import("./DataAccuracy"),
+    MorphologyClassbreaks: () => import("./MorphologyClassbreaks")
   },
   computed: {
     ...mapGetters("user", ["isSysAdmin"]),
--- a/client/src/store/application.js	Fri Jun 14 11:59:14 2019 +0200
+++ b/client/src/store/application.js	Fri Jun 14 12:02:58 2019 +0200
@@ -14,6 +14,8 @@
  *   Bernhard E. Reiter <bernhard.reiter@intevation.de>
  */
 
+import { HTTP } from "@/lib/http";
+import { displayError, displayInfo } from "@/lib/errors";
 import { version } from "../../package.json";
 
 // initial state
@@ -140,85 +142,32 @@
     }
   },
   actions: {
-    loadConfig({ commit, state }) {
-      if (!Object.keys(state.config).length) {
-        setTimeout(() => {
-          commit("config", {
-            ecdis_url: "https://service.d4d-portal.info/wms/",
-            bn_revtime_multiplier: 1.5,
-            gm_min_values_14d: 1124,
-            gm_latest_hours: 24,
-            gm_forecast_offset_24h: 15,
-            gm_forecast_offset_72h: 15,
-            gm_forecast_vs_reality_nsc_24h: -12.5,
-            gm_forecast_vs_reality_nsc_72h: -12.5,
-            morphology_classbreaks: [
-              1,
-              1.5,
-              1.7,
-              1.9,
-              2.1,
-              2.3,
-              2.5,
-              2.7,
-              2.9,
-              3.1,
-              3.3,
-              3.5,
-              4.0,
-              4.5,
-              5,
-              5.5,
-              6,
-              6.5,
-              7
-            ],
-            morphology_classbreaks_compare: [
-              -2,
-              -1.9,
-              -1.8,
-              -1.7,
-              -1.6,
-              -1.5,
-              -1.4,
-              -1.3,
-              -1.2,
-              -1.1,
-              -1,
-              -0.9,
-              -0.8,
-              -0.7,
-              -0.6,
-              -0.5,
-              -0.4,
-              -0.3,
-              -0.2,
-              -0.1,
-              0,
-              0.1,
-              0.2,
-              0.3,
-              0.4,
-              0.5,
-              0.6,
-              0.7,
-              0.8,
-              0.9,
-              1,
-              1.1,
-              1.2,
-              1.3,
-              1.4,
-              1.5,
-              1.6,
-              1.7,
-              1.8,
-              1.9,
-              2
-            ]
+    loadConfig({ commit }) {
+      HTTP.get("/system/settings", {
+        headers: { "X-Gemma-Auth": localStorage.getItem("token") }
+      }).then(response => {
+        commit("config", response.data);
+      });
+    },
+    saveConfig(context, config) {
+      HTTP.put("/system/settings", config, {
+        headers: {
+          "X-Gemma-Auth": localStorage.getItem("token"),
+          "Content-type": "application/json"
+        }
+      })
+        .then(() => {
+          displayInfo({
+            message: "Configuration saved!"
           });
-        }, 1000);
-      }
+        })
+        .catch(error => {
+          const { status, data } = error.response;
+          displayError({
+            title: "Backend Error",
+            message: `${status}: ${data.message || data}`
+          });
+        });
     }
   }
 };
--- a/pkg/controllers/bottlenecks.go	Fri Jun 14 11:59:14 2019 +0200
+++ b/pkg/controllers/bottlenecks.go	Fri Jun 14 12:02:58 2019 +0200
@@ -33,7 +33,7 @@
 
 const (
 	selectLimitingSQL = `
-SELECT limiting from waterway.bottlenecks WHERE objnam = $1`
+SELECT limiting from waterway.bottlenecks WHERE NOT erased AND objnam = $1`
 
 	selectAvailableDepthSQL = `
 WITH data AS (
@@ -48,6 +48,7 @@
   JOIN waterway.bottlenecks bn
     ON fa.bottleneck_id = bn.id
   WHERE
+    NOT bn.erased AND
     bn.objnam = $1 AND
     efa.level_of_service = $2 AND
     efa.measure_type = 'Measured' AND
@@ -74,20 +75,6 @@
 ORDER BY measure_date
 `
 
-	selectGaugeLevelsSQL = `
-SELECT
-  grwl.depth_reference,
-  grwl.value
-FROM waterway.gauges_reference_water_levels grwl
-  JOIN waterway.bottlenecks bns
-    ON grwl.location = bns.gauge_location
-      AND grwl.validity = bns.gauge_validity
-WHERE bns.objnam = $1 AND (
-  grwl.depth_reference like 'HDC%' OR
-  grwl.depth_reference like 'LDC%' OR
-  grwl.depth_reference like 'MW%'
-)
-`
 	selectGaugeLDCSQL = `
 SELECT
   grwl.value
@@ -95,7 +82,7 @@
   JOIN waterway.bottlenecks bns
     ON grwl.location = bns.gauge_location
       AND grwl.validity = bns.gauge_validity
-WHERE bns.objnam = $1 AND grwl.depth_reference like 'LDC%'
+WHERE NOT bns.erased AND bns.objnam = $1 AND grwl.depth_reference like 'LDC%'
 `
 )
 
--- a/pkg/controllers/diff.go	Fri Jun 14 11:59:14 2019 +0200
+++ b/pkg/controllers/diff.go	Fri Jun 14 12:02:58 2019 +0200
@@ -36,7 +36,7 @@
 const (
 	diffIDSQL = `
 SELECT sd.id FROM
-  caching.sounding_differences sd JOIN 
+  caching.sounding_differences sd JOIN
   waterway.sounding_results srm ON sd.minuend = srm.id JOIN
   waterway.sounding_results srs ON sd.subtrahend = srs.id
 WHERE srm.bottleneck_id = srs.bottleneck_id AND
--- a/pkg/controllers/routes.go	Fri Jun 14 11:59:14 2019 +0200
+++ b/pkg/controllers/routes.go	Fri Jun 14 12:02:58 2019 +0200
@@ -79,6 +79,15 @@
 		NoConn: true,
 	})).Methods(http.MethodGet)
 
+	api.Handle("/system/settings", any(&JSONHandler{
+		Handle: getSystemSettings,
+	})).Methods(http.MethodGet)
+
+	api.Handle("/system/settings", any(&JSONHandler{
+		Input:  func(*http.Request) interface{} { return &map[string]string{} },
+		Handle: setSystemSettings,
+	})).Methods(http.MethodPut)
+
 	api.Handle("/system/style/{feature}/{attr}", any(&JSONHandler{
 		Handle: getFeatureStyle,
 	})).Methods(http.MethodGet)
--- a/pkg/controllers/search.go	Fri Jun 14 11:59:14 2019 +0200
+++ b/pkg/controllers/search.go	Fri Jun 14 12:02:58 2019 +0200
@@ -42,6 +42,7 @@
     ST_AsGeoJSON(ST_Centroid(area))::json AS geom,
     'bottleneck' AS type
   FROM waterway.bottlenecks
+  WHERE NOT erased
 ORDER BY objnam) r
 `
 )
--- a/pkg/controllers/stretches.go	Fri Jun 14 11:59:14 2019 +0200
+++ b/pkg/controllers/stretches.go	Fri Jun 14 12:02:58 2019 +0200
@@ -35,14 +35,14 @@
   distinct(b.objnam),
   b.limiting
 FROM waterway.sections s, waterway.bottlenecks b
-WHERE ST_Intersects(b.area, s.area) AND s.name = $1`
+WHERE NOT b.erased AND ST_Intersects(b.area, s.area) AND s.name = $1`
 
 	selectStretchBottlenecks = `
 SELECT
   distinct(b.objnam),
   b.limiting
 FROM waterway.stretches s, waterway.bottlenecks b
-WHERE ST_Intersects(b.area, s.area) AND s.name = $1`
+WHERE NOT b.erased AND ST_Intersects(b.area, s.area) AND s.name = $1`
 )
 
 type (
--- a/pkg/controllers/surveys.go	Fri Jun 14 11:59:14 2019 +0200
+++ b/pkg/controllers/surveys.go	Fri Jun 14 12:02:58 2019 +0200
@@ -34,9 +34,10 @@
 FROM waterway.bottlenecks AS b
   JOIN waterway.gauges AS g
     ON b.gauge_location = g.location AND b.gauge_validity = g.validity
-  JOIN waterway.sounding_results AS s ON b.id = s.bottleneck_id
+  JOIN waterway.sounding_results AS s ON b.bottleneck_id = s.bottleneck_id
   LEFT JOIN waterway.gauges_reference_water_levels AS r
-    USING (depth_reference, location, validity)
+    ON s.depth_reference = r.depth_reference
+      AND g.location = r.location AND g.validity = r.validity
 WHERE b.objnam = $1`
 )
 
--- a/pkg/controllers/system.go	Fri Jun 14 11:59:14 2019 +0200
+++ b/pkg/controllers/system.go	Fri Jun 14 12:02:58 2019 +0200
@@ -21,9 +21,10 @@
 	"net/http"
 	"strings"
 
+	"github.com/gorilla/mux"
+
 	"gemma.intevation.de/gemma/pkg/config"
 	"gemma.intevation.de/gemma/pkg/models"
-	"github.com/gorilla/mux"
 )
 
 const (
@@ -33,6 +34,15 @@
 	setFeatureColourSQL = `UPDATE systemconf.feature_colours
 SET (r, g, b, a) = ($3, $4, $5, $6)
 WHERE feature_name = $1 AND style_attr = $2`
+
+	getSettingsSQL = `
+SELECT config_key, config_val
+FROM sys_admin.system_config`
+
+	updateSettingSQL = `
+INSERT INTO sys_admin.system_config (config_key, config_val)
+VALUES ($1, $2)
+ON CONFLICT (config_key) DO UPDATE SET config_val = $2`
 )
 
 // System status end points
@@ -100,6 +110,75 @@
 	return
 }
 
+func getSystemSettings(
+	_ interface{},
+	req *http.Request,
+	conn *sql.Conn,
+) (jr JSONResult, err error) {
+
+	var rows *sql.Rows
+	if rows, err = conn.QueryContext(req.Context(), getSettingsSQL); err != nil {
+		return
+	}
+	defer rows.Close()
+
+	settings := map[string]string{}
+
+	for rows.Next() {
+		var key, val string
+		if err = rows.Scan(&key, &val); err != nil {
+			return
+		}
+		settings[key] = val
+	}
+	if err = rows.Err(); err != nil {
+		return
+	}
+
+	jr = JSONResult{Result: settings}
+	return
+}
+
+func setSystemSettings(
+	input interface{},
+	req *http.Request,
+	conn *sql.Conn,
+) (jr JSONResult, err error) {
+
+	settings := input.(*map[string]string)
+
+	ctx := req.Context()
+	var tx *sql.Tx
+	if tx, err = conn.BeginTx(ctx, nil); err != nil {
+		return
+	}
+	defer tx.Rollback()
+
+	var setStmt *sql.Stmt
+	if setStmt, err = tx.PrepareContext(ctx, updateSettingSQL); err != nil {
+		return
+	}
+	defer setStmt.Close()
+
+	for key, value := range *settings {
+		if _, err = setStmt.ExecContext(ctx, key, value); err != nil {
+			return
+		}
+	}
+
+	if err = tx.Commit(); err != nil {
+		return
+	}
+
+	jr = JSONResult{
+		Code: http.StatusCreated,
+		Result: struct {
+			Result string `json:"result"`
+		}{"success"},
+	}
+	return
+}
+
 // Map/Feature style end points
 
 func getFeatureStyle(
--- a/pkg/imports/bn.go	Fri Jun 14 11:59:14 2019 +0200
+++ b/pkg/imports/bn.go	Fri Jun 14 12:02:58 2019 +0200
@@ -24,6 +24,7 @@
 	"time"
 
 	"gemma.intevation.de/gemma/pkg/soap/ifbn"
+	"github.com/jackc/pgx/pgtype"
 )
 
 // Bottleneck is an import job to import
@@ -43,16 +44,29 @@
 
 const (
 	hasBottleneckSQL = `
-SELECT true FROM waterway.bottlenecks WHERE bottleneck_id = $1`
+WITH upd AS (
+  UPDATE waterway.bottlenecks SET
+    erased = true
+  WHERE bottleneck_id = $1
+    AND NOT erased
+    -- Don't touch old entry if new validity contains old: will be updated
+    AND NOT validity <@ $2
+  RETURNING 1
+)
+-- Decide whether a new version will be INSERTed
+SELECT EXISTS(SELECT 1 FROM upd)
+  OR NOT EXISTS(SELECT 1 FROM waterway.bottlenecks WHERE bottleneck_id = $1)
+`
 
 	insertBottleneckSQL = `
 WITH
-bounds (b) AS (VALUES (isrs_fromText($5)), (isrs_fromText($6))),
+bounds (b) AS (VALUES (isrs_fromText($6)), (isrs_fromText($7))),
 r AS (SELECT isrsrange(
     (SELECT b FROM bounds ORDER BY b USING <~ FETCH FIRST ROW ONLY),
     (SELECT b FROM bounds ORDER BY b USING >~ FETCH FIRST ROW ONLY)) AS r)
 INSERT INTO waterway.bottlenecks (
   bottleneck_id,
+  validity,
   gauge_location,
   gauge_validity,
   objnam,
@@ -68,29 +82,89 @@
   source_organization
 ) VALUES (
   $1,
-  isrs_fromText($2),
+  $2,
+  isrs_fromText($3),
   COALESCE(
     (SELECT validity FROM waterway.gauges
-       WHERE location = isrs_fromText($2) AND NOT erased),
+       WHERE location = isrs_fromText($3)
+         AND validity @> lower(CAST($2 AS tstzrange))),
     tstzrange(NULL, NULL)),
-  $3,
   $4,
+  $5,
   (SELECT r FROM r),
   ISRSrange_area(
     ISRSrange_axis((SELECT r FROM r),
-                   $14),
+                   $15),
     (SELECT ST_Collect(CAST(area AS geometry))
         FROM waterway.waterway_area)),
-  $7,
   $8,
   $9,
   $10,
   $11,
   $12,
-  $13
+  $13,
+  $14
 )
 RETURNING id`
 
+	moveSRSQL = `
+UPDATE waterway.sounding_results
+-- Associate measurements to matching bottleneck version
+SET bottleneck_validity = $2
+WHERE bottleneck_id = $1
+  AND CAST(date_info AS timestamptz) <@ CAST($2 AS tstzrange)
+`
+
+	fixBNValiditySQL = `
+UPDATE waterway.bottlenecks SET
+   -- Set enddate of old entry to new startdate in case of overlap:
+  validity = validity - $2
+WHERE bottleneck_id = $1
+  AND validity && $2
+  AND erased
+`
+
+	updateBottleneckSQL = `
+WITH
+bounds (b) AS (VALUES (isrs_fromText($5)), (isrs_fromText($6))),
+r AS (SELECT isrsrange(
+    (SELECT b FROM bounds ORDER BY b USING <~ FETCH FIRST ROW ONLY),
+    (SELECT b FROM bounds ORDER BY b USING >~ FETCH FIRST ROW ONLY)) AS r)
+UPDATE waterway.bottlenecks b SET
+  gauge_location = isrs_fromtext($2),
+  gauge_validity = COALESCE(
+    (SELECT validity FROM waterway.gauges g
+       WHERE g.location = isrs_fromText($2)
+         AND g.validity @> lower(b.validity)),
+    tstzrange(NULL, NULL)),
+  objnam = $3,
+  nobjnm = $4,
+  stretch = (SELECT r FROM r),
+  area = ISRSrange_area(
+    ISRSrange_axis((SELECT r FROM r), $14),
+    (SELECT ST_Collect(CAST(area AS geometry))
+        FROM waterway.waterway_area)),
+  rb = $7,
+  lb = $8,
+  responsible_country = $9,
+  revisiting_time = $10,
+  limiting = $11,
+  date_info = $12,
+  source_organization = $13,
+  validity = $15
+WHERE bottleneck_id = $1
+  AND NOT erased
+  AND $12 > date_info
+RETURNING id
+`
+
+	deleteBottleneckMaterialSQL = `
+DELETE FROM waterway.bottlenecks_riverbed_materials
+WHERE bottleneck_id = $1
+  AND riverbed <> ALL($2)
+RETURNING riverbed
+`
+
 	insertBottleneckMaterialSQL = `
 INSERT INTO waterway.bottlenecks_riverbed_materials (
    bottleneck_id,
@@ -98,7 +172,8 @@
 ) VALUES (
    $1,
    $2
-)`
+) ON CONFLICT (bottleneck_id, riverbed) DO NOTHING
+`
 )
 
 type bnJobCreator struct{}
@@ -198,7 +273,8 @@
 
 	feedback.Info("Found %d bottlenecks for import", len(bns))
 
-	var hasStmt, insertStmt, insertMaterialStmt, trackStmt *sql.Stmt
+	var hasStmt, insertStmt, moveSRStmt, fixValidityStmt, updateStmt,
+		deleteMaterialStmt, insertMaterialStmt, trackStmt *sql.Stmt
 
 	for _, x := range []struct {
 		sql  string
@@ -206,6 +282,10 @@
 	}{
 		{hasBottleneckSQL, &hasStmt},
 		{insertBottleneckSQL, &insertStmt},
+		{moveSRSQL, &moveSRStmt},
+		{fixBNValiditySQL, &fixValidityStmt},
+		{updateBottleneckSQL, &updateStmt},
+		{deleteBottleneckMaterialSQL, &deleteMaterialStmt},
 		{insertBottleneckMaterialSQL, &insertMaterialStmt},
 		{trackImportSQL, &trackStmt},
 	} {
@@ -223,7 +303,8 @@
 	for _, bn := range bns {
 		if err := storeBottleneck(
 			ctx, importID, conn, feedback, bn, &nids, tolerance,
-			hasStmt, insertStmt, insertMaterialStmt, trackStmt); err != nil {
+			hasStmt, insertStmt, moveSRStmt, fixValidityStmt, updateStmt,
+			deleteMaterialStmt, insertMaterialStmt, trackStmt); err != nil {
 			return nil, err
 		}
 	}
@@ -249,28 +330,52 @@
 	bn *ifbn.BottleNeckType,
 	nids *[]string,
 	tolerance float64,
-	hasStmt, insertStmt, insertMaterialStmt, trackStmt *sql.Stmt,
+	hasStmt, insertStmt, moveSRStmt, fixValidityStmt, updateStmt,
+	deleteMaterialStmt, insertMaterialStmt, trackStmt *sql.Stmt,
 ) error {
-
-	tx, err := conn.BeginTx(ctx, nil)
-	if err != nil {
-		return err
-	}
-	defer tx.Rollback()
+	feedback.Info("Processing %s (%s)", bn.OBJNAM, bn.Bottleneck_id)
 
-	var found bool
-	err = tx.StmtContext(ctx, hasStmt).QueryRowContext(ctx,
-		bn.Bottleneck_id).Scan(&found)
-	switch {
-	case err == sql.ErrNoRows:
-		// This is good.
-	case err != nil:
-		return err
-	case found:
-		feedback.Info("'%s' already in database. Skip", bn.OBJNAM)
-		// TODO: Deep comparison database vs. SOAP.
+	if bn.AdditionalData == nil || bn.AdditionalData.KeyValuePair == nil {
+		feedback.Warn("Missing validity information")
 		return nil
 	}
+	const (
+		fromKey = "Valid_from_date"
+		toKey   = "Valid_to_date"
+	)
+	fromTo := make(map[string]time.Time)
+	for _, kv := range bn.AdditionalData.KeyValuePair {
+		k := string(kv.Key)
+		if k == fromKey || k == toKey {
+			if t, err := time.Parse(time.RFC3339, kv.Value); err != nil {
+				return err
+			} else {
+				fromTo[k] = t
+			}
+		}
+	}
+
+	var tfrom, tto pgtype.Timestamptz
+	if t, ok := fromTo[fromKey]; ok {
+		tfrom.Set(t)
+	} else {
+		feedback.Warn("Missing start date")
+		return nil
+	}
+	var uBound pgtype.BoundType
+	if t, ok := fromTo[toKey]; ok {
+		tto.Set(t)
+		uBound = pgtype.Exclusive
+	} else {
+		uBound = pgtype.Unbounded
+	}
+	validity := pgtype.Tstzrange{
+		Lower:     tfrom,
+		Upper:     tto,
+		LowerType: pgtype.Inclusive,
+		UpperType: uBound,
+		Status:    pgtype.Present,
+	}
 
 	rb, lb := splitRBLB(bn.Rb_lb)
 
@@ -297,42 +402,149 @@
 		country = string(*bn.Responsible_country)
 	}
 
-	var nid int64
+	var materials []string
+	if bn.Riverbed != nil {
+		for _, material := range bn.Riverbed.Material {
+			if material != nil {
+				materials = append(materials, string(*material))
+			}
+		}
+	}
 
-	err = tx.StmtContext(ctx, insertStmt).QueryRowContext(
-		ctx,
+	tx, err := conn.BeginTx(ctx, nil)
+	if err != nil {
+		return err
+	}
+	defer tx.Rollback()
+
+	var isNew bool
+	var nid int64
+	err = tx.StmtContext(ctx, hasStmt).QueryRowContext(ctx,
 		bn.Bottleneck_id,
-		bn.Fk_g_fid,
-		bn.OBJNAM,
-		bn.NOBJNM,
-		bn.From_ISRS, bn.To_ISRS,
-		rb,
-		lb,
-		country,
-		revisitingTime,
-		limiting,
-		bn.Date_Info,
-		bn.Source,
-		tolerance,
-	).Scan(&nid)
-	if err != nil {
-		feedback.Warn("Failed to insert '%s' into database", bn.OBJNAM)
+		validity,
+	).Scan(&isNew)
+	switch {
+	case err != nil:
 		feedback.Warn(handleError(err).Error())
+		if err2 := tx.Rollback(); err2 != nil {
+			return err2
+		}
+		return nil
+	case isNew:
+		err = tx.StmtContext(ctx, insertStmt).QueryRowContext(
+			ctx,
+			bn.Bottleneck_id,
+			&validity,
+			bn.Fk_g_fid,
+			bn.OBJNAM,
+			bn.NOBJNM,
+			bn.From_ISRS, bn.To_ISRS,
+			rb,
+			lb,
+			country,
+			revisitingTime,
+			limiting,
+			bn.Date_Info,
+			bn.Source,
+			tolerance,
+		).Scan(&nid)
+		if err != nil {
+			feedback.Warn(handleError(err).Error())
+			return nil
+		}
+		feedback.Info("insert new version")
+	case !isNew:
+		// try to update
+		err := tx.StmtContext(ctx, updateStmt).QueryRowContext(ctx,
+			bn.Bottleneck_id,
+			bn.Fk_g_fid,
+			bn.OBJNAM,
+			bn.NOBJNM,
+			bn.From_ISRS, bn.To_ISRS,
+			rb,
+			lb,
+			country,
+			revisitingTime,
+			limiting,
+			bn.Date_Info,
+			bn.Source,
+			tolerance,
+			&validity,
+		).Scan(&nid)
+		switch {
+		case err == sql.ErrNoRows:
+			feedback.Info("unchanged")
+			if err := tx.Rollback(); err != nil {
+				return err
+			}
+			return nil
+		case err != nil:
+			feedback.Warn(handleError(err).Error())
+			if err := tx.Rollback(); err != nil {
+				return err
+			}
+			return nil
+		default:
+			feedback.Info("update")
+
+			// Remove obsolete riverbed materials
+			var pgMaterials pgtype.VarcharArray
+			pgMaterials.Set(materials)
+			mtls, err := tx.StmtContext(ctx,
+				deleteMaterialStmt).QueryContext(ctx,
+				nid,
+				&pgMaterials,
+			)
+			if err != nil {
+				return err
+			}
+			defer mtls.Close()
+			for mtls.Next() {
+				var delMat string
+				if err := mtls.Scan(&delMat); err != nil {
+					return err
+				}
+				feedback.Warn("Removed riverbed material %s", delMat)
+			}
+			if err := mtls.Err(); err != nil {
+				return err
+			}
+		}
+	}
+
+	// Move sounding results to new matching bottleneck version, if applicable
+	if _, err = tx.StmtContext(ctx, moveSRStmt).ExecContext(ctx,
+		bn.Bottleneck_id,
+		&validity,
+	); err != nil {
+		feedback.Warn(handleError(err).Error())
+		if err2 := tx.Rollback(); err2 != nil {
+			return err2
+		}
 		return nil
 	}
 
-	if bn.Riverbed != nil {
-		for _, material := range bn.Riverbed.Material {
-			if material != nil {
-				mat := string(*material)
-				if _, err := tx.StmtContext(ctx,
-					insertMaterialStmt).ExecContext(
-					ctx, nid, material); err != nil {
-					feedback.Warn(
-						"Failed to insert riverbed material '%s' for bottleneck '%s'.",
-						mat, bn.OBJNAM)
-					feedback.Warn(handleError(err).Error())
-				}
+	// Set end of validity of old version to start of new version
+	// in case of overlap
+	if _, err = tx.StmtContext(ctx, fixValidityStmt).ExecContext(ctx,
+		bn.Bottleneck_id,
+		validity,
+	); err != nil {
+		feedback.Warn(handleError(err).Error())
+		if err2 := tx.Rollback(); err2 != nil {
+			return err2
+		}
+		return nil
+	}
+
+	// Insert riverbed materials
+	if materials != nil {
+		for _, mat := range materials {
+			if _, err := tx.StmtContext(ctx,
+				insertMaterialStmt).ExecContext(
+				ctx, nid, mat); err != nil {
+				feedback.Warn("Failed to insert riverbed material '%s'", mat)
+				feedback.Warn(handleError(err).Error())
 			}
 		}
 	}
@@ -345,7 +557,6 @@
 	if err = tx.Commit(); err != nil {
 		return err
 	}
-	feedback.Info("Inserted '%s' into database", bn.OBJNAM)
 	*nids = append(*nids, bn.Bottleneck_id)
 	return nil
 }
--- a/pkg/imports/fa.go	Fri Jun 14 11:59:14 2019 +0200
+++ b/pkg/imports/fa.go	Fri Jun 14 12:02:58 2019 +0200
@@ -47,7 +47,7 @@
 
 const (
 	listBottlenecksSQL = `
-SELECT
+SELECT DISTINCT
   bottleneck_id
 FROM waterway.bottlenecks
 WHERE responsible_country = users.current_user_country()
@@ -79,7 +79,8 @@
   source_organization
 ) VALUES (
   $1,
-  (SELECT id FROM waterway.bottlenecks WHERE bottleneck_id = $2),
+  (SELECT id FROM waterway.bottlenecks
+     WHERE NOT erased AND bottleneck_id = $2),
   $3,
   $4,
   $5,
--- a/pkg/imports/sr.go	Fri Jun 14 11:59:14 2019 +0200
+++ b/pkg/imports/sr.go	Fri Jun 14 12:02:58 2019 +0200
@@ -114,11 +114,13 @@
 	insertHullSQL = `
 INSERT INTO waterway.sounding_results (
   bottleneck_id,
+  bottleneck_validity,
   date_info,
   depth_reference,
   area
 ) SELECT
-  (SELECT id from waterway.bottlenecks where objnam = $1),
+  bottleneck_id,
+  validity,
   $2::date,
   $3,
   (SELECT
@@ -127,6 +129,8 @@
     ELSE
       ST_MakeValid(ST_Transform(ST_GeomFromWKB($5, $6::integer), 4326))::geography
     END)
+FROM waterway.bottlenecks
+WHERE objnam = $1 AND validity @> CAST($2 AS timestamptz)
 RETURNING
   id,
   ST_X(ST_Centroid(area::geometry)),
@@ -188,7 +192,9 @@
   JOIN waterway.bottlenecks bns
     ON grwl.location = bns.gauge_location
       AND grwl.validity = bns.gauge_validity
-WHERE bns.objnam = $1 AND grwl.depth_reference like 'LDC%'
+WHERE bns.objnam = $1
+  AND bns.validity @> CAST($2 AS timestamptz)
+  AND grwl.depth_reference like 'LDC%'
 `
 
 	reprojectPointsSingleBeamSQL = `
@@ -242,7 +248,11 @@
 		feedback.Info("Found ZPG as reference system -> translating Z values to LDC")
 		var ldc float64
 		var depthReference string
-		err := conn.QueryRowContext(ctx, selectGaugeLDCSQL, m.Bottleneck).Scan(
+		err := conn.QueryRowContext(ctx,
+			selectGaugeLDCSQL,
+			m.Bottleneck,
+			m.Date.Time,
+		).Scan(
 			&ldc,
 			&depthReference,
 		)
@@ -643,7 +653,11 @@
 	)
 	xyz, boundary = nil, nil // not need from now on.
 	feedback.Info("Calculating hull took %s.", time.Since(start))
-	if err != nil {
+	switch {
+	case err == sql.ErrNoRows:
+		return nil, fmt.Errorf(
+			"No bottleneck matching given name and time available")
+	case err != nil:
 		return nil, err
 	}
 	feedback.Info("Best suited UTM EPSG: %d", epsg)
--- a/pkg/imports/wg.go	Fri Jun 14 11:59:14 2019 +0200
+++ b/pkg/imports/wg.go	Fri Jun 14 12:02:58 2019 +0200
@@ -79,8 +79,8 @@
     erased = true
   WHERE isrs_astext(location) = $1
     AND NOT erased
-    -- Don't touch old entry if validity did not change: will be updated
-    AND validity <> $2
+    -- Don't touch old entry if new validity contains old: will be updated
+    AND NOT validity <@ $2
   RETURNING 1
 )
 -- Decide whether a new version will be INSERTed
@@ -124,6 +124,14 @@
   AND measure_date <@ CAST($2 AS tstzrange)
 `
 
+	moveBNSQL = `
+UPDATE waterway.bottlenecks
+-- Associate bottlenecks to matching gauge version
+SET gauge_validity = $2
+WHERE isrs_astext(gauge_location) = $1
+  AND lower(validity) <@ CAST($2 AS tstzrange)
+`
+
 	fixValiditySQL = `
 UPDATE waterway.gauges SET
    -- Set enddate of old entry to new startdate in case of overlap:
@@ -143,7 +151,8 @@
   geodref = $12,
   date_info = $13,
   source_organization = $14,
-  lastupdate = $15
+  lastupdate = $15,
+  validity = $16
 WHERE location = ($1::char(2), $2::char(3), $3::char(5), $4::char(5), $5::int)
   AND NOT erased
   AND $15 > lastupdate
@@ -199,7 +208,8 @@
 		return nil, err
 	}
 
-	var eraseGaugeStmt, insertStmt, moveGMStmt, fixValidityStmt, updateStmt,
+	var eraseGaugeStmt, insertStmt, moveGMStmt, moveBNStmt,
+		fixValidityStmt, updateStmt,
 		deleteReferenceWaterLevelsStmt,
 		isNtSDepthRefStmt, insertWaterLevelStmt *sql.Stmt
 	for _, x := range []struct {
@@ -209,6 +219,7 @@
 		{eraseGaugeSQL, &eraseGaugeStmt},
 		{insertGaugeSQL, &insertStmt},
 		{moveGMSQL, &moveGMStmt},
+		{moveBNSQL, &moveBNStmt},
 		{fixValiditySQL, &fixValidityStmt},
 		{updateGaugeSQL, &updateStmt},
 		{deleteReferenceWaterLevelsSQL, &deleteReferenceWaterLevelsStmt},
@@ -365,33 +376,6 @@
 					unchanged++
 					continue
 				}
-				// Move gauge measurements to new matching gauge version,
-				// if applicable
-				if _, err = tx.StmtContext(ctx, moveGMStmt).ExecContext(ctx,
-					code.String(),
-					&validity,
-				); err != nil {
-					feedback.Warn(handleError(err).Error())
-					if err2 := tx.Rollback(); err2 != nil {
-						return nil, err2
-					}
-					unchanged++
-					continue
-				}
-				// Set end of validity of old version to start of new version
-				// in case of overlap
-				if _, err = tx.StmtContext(ctx, fixValidityStmt).ExecContext(
-					ctx,
-					code.String(),
-					&validity,
-				); err != nil {
-					feedback.Warn(handleError(err).Error())
-					if err2 := tx.Rollback(); err2 != nil {
-						return nil, err2
-					}
-					unchanged++
-					continue
-				}
 				feedback.Info("insert new version")
 			case !isNew:
 				// try to update
@@ -411,6 +395,7 @@
 					&dateInfo,
 					source,
 					time.Time(*dr.Lastupdate),
+					&validity,
 				).Scan(&dummy)
 				switch {
 				case err2 == sql.ErrNoRows:
@@ -461,6 +446,46 @@
 				}
 			}
 
+			// Move gauge measurements and bottlenecks to new matching
+			// gauge version, if applicable
+			if _, err = tx.StmtContext(ctx, moveGMStmt).ExecContext(ctx,
+				code.String(),
+				&validity,
+			); err != nil {
+				feedback.Warn(handleError(err).Error())
+				if err2 := tx.Rollback(); err2 != nil {
+					return nil, err2
+				}
+				unchanged++
+				continue
+			}
+			if _, err = tx.StmtContext(ctx, moveBNStmt).ExecContext(ctx,
+				code.String(),
+				&validity,
+			); err != nil {
+				feedback.Warn(handleError(err).Error())
+				if err2 := tx.Rollback(); err2 != nil {
+					return nil, err2
+				}
+				unchanged++
+				continue
+			}
+
+			// Set end of validity of old version to start of new version
+			// in case of overlap
+			if _, err = tx.StmtContext(ctx, fixValidityStmt).ExecContext(
+				ctx,
+				code.String(),
+				&validity,
+			); err != nil {
+				feedback.Warn(handleError(err).Error())
+				if err2 := tx.Rollback(); err2 != nil {
+					return nil, err2
+				}
+				unchanged++
+				continue
+			}
+
 			// "Upsert" reference water levels
 			for _, wl := range []struct {
 				level **erdms.RisreflevelcodeType
--- a/pkg/models/sr.go	Fri Jun 14 11:59:14 2019 +0200
+++ b/pkg/models/sr.go	Fri Jun 14 12:02:58 2019 +0200
@@ -41,7 +41,8 @@
   FROM waterway.bottlenecks bn
     JOIN waterway.gauges g
       ON bn.gauge_location = g.location AND bn.gauge_validity = g.validity
-    JOIN waterway.gauges_reference_water_levels rl USING (location, validity)
+    JOIN waterway.gauges_reference_water_levels rl
+      ON g.location = rl.location AND g.validity = rl.validity
   WHERE bn.objnam = $1
     AND rl.depth_reference = $2)`
 
@@ -50,7 +51,7 @@
 
 	checkBottleneckDateUniqueSQL = `
 SELECT true FROM waterway.sounding_results sr JOIN
-  waterway.bottlenecks bn ON sr.bottleneck_id = bn.id
+  waterway.bottlenecks bn ON sr.bottleneck_id = bn.bottleneck_id
 WHERE bn.objnam = $1 AND sr.date_info = $2`
 )
 
--- a/schema/auth.sql	Fri Jun 14 11:59:14 2019 +0200
+++ b/schema/auth.sql	Fri Jun 14 12:02:58 2019 +0200
@@ -25,10 +25,11 @@
 --
 -- Privileges for waterway_user
 --
-GRANT USAGE ON SCHEMA public, users, waterway, systemconf, caching TO waterway_user;
+GRANT USAGE ON SCHEMA public, users, waterway, systemconf, sys_admin, caching TO waterway_user;
 GRANT SELECT ON ALL TABLES IN SCHEMA public, users, waterway TO waterway_user;
 GRANT SELECT, UPDATE, DELETE, INSERT ON ALL TABLES IN SCHEMA caching TO waterway_user;
 GRANT SELECT ON systemconf.feature_colours TO waterway_user;
+GRANT SELECT ON sys_admin.system_config TO waterway_user;
 GRANT UPDATE (pw, map_extent, email_address) ON users.list_users
     TO waterway_user;
 
@@ -65,7 +66,7 @@
     ON users.list_users, users.responsibility_areas TO sys_admin;
 GRANT USAGE ON SCHEMA sys_admin TO sys_admin;
 GRANT SELECT ON ALL TABLES IN SCHEMA sys_admin TO sys_admin;
-GRANT UPDATE ON sys_admin.system_config TO sys_admin;
+GRANT INSERT, UPDATE ON sys_admin.system_config TO sys_admin;
 GRANT UPDATE ON systemconf.feature_colours TO sys_admin;
 GRANT UPDATE ON sys_admin.published_services TO sys_admin;
 GRANT INSERT, DELETE ON sys_admin.password_reset_requests TO sys_admin;
--- a/schema/auth_tests.sql	Fri Jun 14 11:59:14 2019 +0200
+++ b/schema/auth_tests.sql	Fri Jun 14 12:02:58 2019 +0200
@@ -75,16 +75,16 @@
 
 PREPARE bn_insert (varchar, geometry(MULTIPOLYGON, 4326)) AS
     INSERT INTO waterway.bottlenecks (
-        gauge_location, gauge_validity,
+        gauge_location, gauge_validity, validity,
         bottleneck_id, stretch, area, rb, lb, responsible_country,
-        revisiting_time, limiting, source_organization)
+        revisiting_time, limiting, date_info, source_organization)
         SELECT
-            location, validity,
+            location, validity, validity,
             $1,
             isrsrange(('AT', 'XXX', '00001', '00000', 0)::isrs,
                 ('AT', 'XXX', '00001', '00000', 2)::isrs),
             $2, 'AT', 'AT', 'AT',
-            1, 'depth', 'testorganization'
+            1, 'depth', current_timestamp, 'testorganization'
         FROM waterway.gauges
         WHERE location = ('AT', 'XXX', '00001', 'G0001', 1)::isrs;
 SELECT lives_ok($$
--- a/schema/default_sysconfig.sql	Fri Jun 14 11:59:14 2019 +0200
+++ b/schema/default_sysconfig.sql	Fri Jun 14 12:02:58 2019 +0200
@@ -23,4 +23,19 @@
 INSERT INTO systemconf.feature_colours VALUES ('Bottlenecks', 'stroke', 250, 40, 255, 1);
 INSERT INTO systemconf.feature_colours VALUES ('Bottlenecks', 'fill', 255, 37, 196, 0.14);
 
+--
+-- Settings
+--
+INSERT INTO sys_admin.system_config VALUES ('ecdis_wms_url', 'https://service.d4d-portal.info/wms/');
+INSERT INTO sys_admin.system_config VALUES ('ecdis_wms_params', '{"LAYERS": "d4d", "VERSION": "1.1.1", "TILED": true}');
+INSERT INTO sys_admin.system_config VALUES ('bn_revtime_multiplier', 1.5);
+INSERT INTO sys_admin.system_config VALUES ('gm_min_values_14d', 1224);
+INSERT INTO sys_admin.system_config VALUES ('gm_latest_hours', 24);
+INSERT INTO sys_admin.system_config VALUES ('gm_forecast_offset_24h', 15);
+INSERT INTO sys_admin.system_config VALUES ('gm_forecast_offset_72h', 15);
+INSERT INTO sys_admin.system_config VALUES ('gm_forecast_vs_reality_nsc_24h', -12.5);
+INSERT INTO sys_admin.system_config VALUES ('gm_forecast_vs_reality_nsc_72h', -12.5);
+INSERT INTO sys_admin.system_config VALUES ('morphology_classbreaks', '1,1.5,1.7,1.9,2.1,2.3,2.5,2.7,2.9,3.1,3.3,3.5,4.0,4.5,5,5.5,6,6.5,7');
+INSERT INTO sys_admin.system_config VALUES ('morphology_classbreaks_compare', '-2,-1.9,-1.8,-1.7,-1.6,-1.5,-1.4,-1.3,-1.2,-1.1,-1,-0.9,-0.8,-0.7,-0.6,-0.5,-0.4,-0.3,-0.2,-0.1,0,0.1,0.2,0.3,0.4,0.5,0.6,0.7,0.8,0.9,1,1.1,1.2,1.3,1.4,1.5,1.6,1.7,1.8,1.9,2');
+
 COMMIT;
--- a/schema/gemma.sql	Fri Jun 14 11:59:14 2019 +0200
+++ b/schema/gemma.sql	Fri Jun 14 12:02:58 2019 +0200
@@ -280,7 +280,7 @@
         geom geography(POINT, 4326) NOT NULL,
         applicability_from_km int8,
         applicability_to_km int8,
-        validity tstzrange NOT NULL,
+        validity tstzrange NOT NULL CHECK (NOT isempty(validity)),
         zero_point double precision NOT NULL,
         geodref varchar,
         date_info timestamp with time zone NOT NULL,
@@ -288,7 +288,6 @@
         lastupdate timestamp with time zone NOT NULL,
         -- entry removed from external data source (RIS-Index)/historicised:
         erased boolean NOT NULL DEFAULT false,
-        CHECK (erased OR NOT isempty(validity)),
         PRIMARY KEY (location, validity),
         EXCLUDE USING GiST (isrs_astext(location) WITH =, validity WITH &&)
             DEFERRABLE INITIALLY DEFERRED
@@ -488,9 +487,14 @@
     --
     CREATE TABLE bottlenecks (
         id int PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY,
-        bottleneck_id varchar UNIQUE NOT NULL,
+        bottleneck_id varchar NOT NULL,
+        validity tstzrange NOT NULL CHECK (NOT isempty(validity)),
+        UNIQUE (bottleneck_id, validity),
+        EXCLUDE USING GiST (bottleneck_id WITH =, validity WITH &&)
+            DEFERRABLE INITIALLY DEFERRED,
         gauge_location isrs NOT NULL,
         gauge_validity tstzrange NOT NULL,
+        CHECK(lower(validity) <@ gauge_validity),
         CONSTRAINT gauge_key
             FOREIGN KEY (gauge_location, gauge_validity) REFERENCES gauges
                 ON UPDATE CASCADE,
@@ -512,13 +516,15 @@
         -- different model approach?
         -- depth_reference char(3) NOT NULL REFERENCES depth_references,
         -- XXX: Also an attribut of sounding result?
-        date_info timestamp with time zone NOT NULL DEFAULT CURRENT_TIMESTAMP,
+        date_info timestamp with time zone NOT NULL,
         source_organization varchar NOT NULL,
-        -- additional_data xml -- Currently not relevant for GEMMA
+        erased boolean NOT NULL DEFAULT false,
         staging_done boolean NOT NULL DEFAULT false
     )
-    CREATE TRIGGER bottleneck_date_info BEFORE UPDATE ON bottlenecks
-        FOR EACH ROW EXECUTE PROCEDURE update_date_info()
+    -- Allow only one non-erased entry per bottleneck
+    CREATE UNIQUE INDEX bottlenecks_erased_unique_constraint
+        ON bottlenecks (bottleneck_id)
+        WHERE NOT erased
 
     CREATE TABLE bottlenecks_riverbed_materials (
         bottleneck_id int NOT NULL REFERENCES bottlenecks(id)
@@ -530,8 +536,15 @@
 
     CREATE TABLE sounding_results (
         id int PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY,
-        bottleneck_id int NOT NULL REFERENCES bottlenecks(id),
+        bottleneck_id varchar NOT NULL,
+        bottleneck_validity tstzrange NOT NULL,
+        CONSTRAINT bottleneck_key
+            FOREIGN KEY (bottleneck_id, bottleneck_validity)
+                REFERENCES bottlenecks (bottleneck_id, validity)
+                ON UPDATE CASCADE,
         date_info date NOT NULL,
+        CHECK (tstzrange(date_info::timestamptz,
+            date_info::timestamptz + '1 d'::interval) && bottleneck_validity),
         UNIQUE (bottleneck_id, date_info),
         area geography(POLYGON, 4326) NOT NULL
             CHECK(ST_IsValid(CAST(area AS geometry))),
--- a/schema/geoserver_views.sql	Fri Jun 14 11:59:14 2019 +0200
+++ b/schema/geoserver_views.sql	Fri Jun 14 12:02:58 2019 +0200
@@ -135,7 +135,8 @@
         LEFT JOIN fairway_availability_latest fal
             ON b.id = fal.bottleneck_id
         LEFT JOIN sounding_result_latest srl
-            ON b.id = srl.bottleneck_id;
+            ON b.bottleneck_id = srl.bottleneck_id
+    WHERE NOT b.erased;
 
 CREATE OR REPLACE VIEW waterway.stretches_geoserver AS
     SELECT
@@ -199,7 +200,8 @@
     FROM waterway.bottlenecks bn LEFT JOIN (
         SELECT bottleneck_id, max(date_info) AS current
             FROM waterway.sounding_results
-            GROUP BY bottleneck_id) sr ON sr.bottleneck_id = bn.id
+            GROUP BY bottleneck_id) sr ON sr.bottleneck_id = bn.bottleneck_id
+    WHERE NOT bn.erased
     ORDER BY objnam;
 
 CREATE OR REPLACE VIEW waterway.sounding_differences AS
@@ -218,4 +220,5 @@
         JOIN waterway.sounding_results srs
             ON sd.subtrahend = srs.id
         JOIN waterway.bottlenecks bn
-            ON srm.bottleneck_id = bn.id;
+            ON srm.bottleneck_id = bn.bottleneck_id
+                AND srm.bottleneck_validity = bn.validity;
--- a/schema/tap_tests_data.sql	Fri Jun 14 11:59:14 2019 +0200
+++ b/schema/tap_tests_data.sql	Fri Jun 14 12:02:58 2019 +0200
@@ -57,7 +57,7 @@
         current_timestamp,
         'testorganization',
         current_timestamp)
-    RETURNING location, validity),
+    RETURNING location, validity, validity),
 bns AS (
     VALUES (
         'testbottleneck1',
@@ -65,19 +65,19 @@
             ('AT', 'XXX', '00001', '00000', 2)::isrs),
         ST_geomfromtext('MULTIPOLYGON(((0 0, 0 1, 1 1, 1 0, 0 0)))', 4326),
         'AT', 'AT', 'AT',
-        1, 'depth', 'testorganization', false
+        1, 'depth', current_timestamp, 'testorganization', false
     ), (
         'testbottleneck2',
         isrsrange(('AT', 'XXX', '00001', '00000', 0)::isrs,
             ('AT', 'XXX', '00001', '00000', 2)::isrs),
         ST_geomfromtext('MULTIPOLYGON(((0 0, 0 1, 1 1, 1 0, 0 0)))', 4326),
         'AT', 'AT', 'AT',
-        1, 'depth', 'testorganization', true
+        1, 'depth', current_timestamp, 'testorganization', true
     ))
 INSERT INTO waterway.bottlenecks (
-    gauge_location, gauge_validity,
+    gauge_location, gauge_validity, validity,
     bottleneck_id, stretch, area, rb, lb, responsible_country,
-    revisiting_time, limiting, source_organization, staging_done)
+    revisiting_time, limiting, date_info, source_organization, staging_done)
     SELECT * FROM gs, bns;
 
 INSERT INTO waterway.distance_marks_virtual VALUES (