changeset 4614:970e90d3d5eb geoserver_sql_views

Merge default into geoserver_sql_views
author Tom Gottfried <tom@intevation.de>
date Mon, 09 Sep 2019 17:24:07 +0200
parents 0c5829ff4dea (current diff) 90b72e811efd (diff)
children 32d3e0cecf4f
files pkg/geoserver/boot.go schema/gemma.sql schema/version.sql
diffstat 14 files changed, 439 insertions(+), 89 deletions(-) [+]
line wrap: on
line diff
--- a/client/docs/developers.md	Mon Sep 09 17:16:41 2019 +0200
+++ b/client/docs/developers.md	Mon Sep 09 17:24:07 2019 +0200
@@ -5,6 +5,23 @@
 
 ## Hints
 
+### svg2pdf
+
+https://github.com/yWorks/svg2pdf.js has known shortcomings,
+which we work around.
+
+Not all layout possibilities of webbrowser are not supported!
+
+Our tactics is to report the problems, but work around them, if we can,
+some examples:
+
+ * https://github.com/yWorks/svg2pdf.js/issues/82 (Unsupported features/Known issues)
+ * https://github.com/yWorks/svg2pdf.js/issues/109 (alignment-baseline attribute does not work)
+ * https://github.com/yWorks/svg2pdf.js/issues/100 (Font-size in `em` unit does not work)
+
+
+### Testing diagram layout and pdf export
+
 When testing the vuex store object can be accessed from the
 webbrowsers console like
 
@@ -17,7 +34,7 @@
 fairwayavailability (tested with Chromium 73 und gemma-2019-09-04):
 
 ```javascript
-data = store.state.fairwayavailability.csv
+data = store.state.fairwayavailability.csv  // see current data
 
 data=`#time,# < LDC (164.0) [h],# >= LDC (164.0) [h],# < 230.0 [h],# >= 230.0 [h],# >= 250.0 [h]
 01-2019,0  ,744,  0,  0,744
@@ -34,4 +51,29 @@
 store.commit("fairwayavailability/setAvailableFairwayDepthData", data)
 ```
 
+Or for the AvailableFairwayDepthLNWLDiagram:
+```javascript
+data = store.state.fairwayavailability.csv  // see current data
+
+data=`
+#time,# < LDC (162.0) [h],# >= LDC (162.0) [h],#d < 230.0 [%],#d >= 230.0 [%],#d >= 250.0 [%]
+09-2018,100,  0,100  ,    0,  0
+10-2018,100,  0, 80  ,   20,  0
+11-2018,100,  0,  0  ,  100,  0
+12-2018, 80, 20, 50  ,   50,  0
+01-2019, 70, 30, 15  ,   60, 25
+02-2019, 65, 35, 40  ,   40, 20
+03-2019, 55, 45, 42  ,   42, 16
+04-2019, 42, 58, 40  , 20  , 40
+05-2019, 30, 70, 2   , 30  , 68
+06-2019, 30, 70, 0   , 25  , 75
+07-2019, 20, 80, 25  ,  0  , 75
+08-2019,  0,100, 10.1,  1.5, 88.4
+09-2019,  0,100, 23.5,  2.4, 74.1
+`
+
+store.commit("fairwayavailability/setAvailableFairwayDepthLNWLData", data)
+```
+
+
 (Depends on the code structure in store/fairwayavailability.js.)
--- a/client/src/components/App.vue	Mon Sep 09 17:16:41 2019 +0200
+++ b/client/src/components/App.vue	Mon Sep 09 17:24:07 2019 +0200
@@ -17,7 +17,9 @@
             <AvailableFairwayDepthDialogue v-if="isMapVisible" />
           </div>
           <div class="d-flex flex-column align-items-end">
-            <Identify v-if="isMapVisible" />
+            <keep-alive>
+              <Identify v-if="isMapVisible" />
+            </keep-alive>
             <Layers v-if="isMapVisible" />
           </div>
           <Toolbar v-if="isMapVisible" />
--- a/client/src/components/fairway/AvailableFairwayDepthLNWL.vue	Mon Sep 09 17:16:41 2019 +0200
+++ b/client/src/components/fairway/AvailableFairwayDepthLNWL.vue	Mon Sep 09 17:24:07 2019 +0200
@@ -42,6 +42,17 @@
             >Download CSV</a
           >
         </div>
+        <div class="btn-group-toggle w-100 mt-2">
+          <label
+            class="btn btn-outline-secondary btn-sm"
+            :class="{ active: showNumbers }"
+            ><input
+              type="checkbox"
+              v-model="showNumbers"
+              autocomplete="off"
+            />Numbers
+          </label>
+        </div>
       </DiagramLegend>
       <div
         ref="diagramContainer"
@@ -69,6 +80,7 @@
  * * Thomas Junk <thomas.junk@intevation.de>
  * * Markus Kottländer <markus.kottlaender@intevation.de>
  * * Fadi Abbud <fadi.abbud@intevation.de>
+ * * Bernhard Reiter <bernhard.reiter@intevation.de>
  */
 import * as d3 from "d3";
 import app from "@/main";
@@ -103,7 +115,8 @@
       },
       templateData: null,
       templates: [],
-      defaultTemplate: defaultDiagramTemplate
+      defaultTemplate: defaultDiagramTemplate,
+      showNumbers: false
     };
   },
   created() {
@@ -477,6 +490,28 @@
         .attr("fill", (d, i) => {
           return this.$options.AFDCOLORS[i];
         });
+      if (this.showNumbers) {
+        afd
+          .selectAll("text")
+          .data([data.above, data.between, data.below])
+          .enter()
+          .append("text")
+          .attr("fill", "black")
+          .attr("text-anchor", "middle")
+          .attr("x", ldcWidth / 2)
+          .attr("y", (d, i) => {
+            let h = d; // i == 0
+            if (i > 0) {
+              h += data.above;
+            }
+            if (i > 1) {
+              h += data.between;
+            }
+            return yScale(h + 0.8) + this.paddingTop;
+          })
+          .text(d => (d > 0 ? Math.round(d) : ""))
+          .attr("font-size", "8");
+      }
     },
     drawLNWL(data, i, diagram, spaceBetween, widthPerItem, ldcWidth, yScale) {
       let lnwl = diagram
@@ -510,17 +545,24 @@
           //d3.event.pageX gives coordinates relative to SVG
           //dy gives offset of svg on page
         })
-        .attr("height", d => {
-          return yScale(0) - yScale(d);
-        })
-        .attr("y", d => {
-          return yScale(d);
-        })
+        .attr("height", d => yScale(0) - yScale(d))
+        .attr("y", d => yScale(d))
         .attr("transform", `translate(0 ${this.paddingTop})`)
         .attr("width", ldcWidth)
         .attr("fill", () => {
           return this.$options.LWNLCOLORS.LDC;
         });
+      if (this.showNumbers) {
+        // we do not need to bind data or a datum as the forEach in drawBars
+        // already brings us only one datum in data
+        lnwl
+          .append("text")
+          .attr("y", yScale(data.ldc + 0.8) + this.paddingTop)
+          .text(data.ldc)
+          .attr("text-anchor", "left")
+          .attr("fill", "black")
+          .attr("font-size", "8");
+      }
     },
     drawScaleLabel({ diagram, dimensions }) {
       diagram
@@ -530,6 +572,7 @@
         .attr("x", 0)
         .attr("y", 0)
         .attr("dy", "20")
+        .attr("fill", "black")
         // translate a few mm to the right to allow for slightly higher letters
         .attr(
           "transform",
@@ -603,6 +646,9 @@
   watch: {
     fwLNWLData() {
       this.drawDiagram();
+    },
+    showNumbers() {
+      this.drawDiagram();
     }
   },
   LEGEND: app.$gettext("Percent"),
--- a/client/src/components/identify/Identify.vue	Mon Sep 09 17:16:41 2019 +0200
+++ b/client/src/components/identify/Identify.vue	Mon Sep 09 17:24:07 2019 +0200
@@ -64,15 +64,29 @@
             v-if="showGaugeMeta(feature)"
             class="ml-2 mb-1 text-left d-flex flex-column"
           >
-            <div>
-              <font-awesome-icon
-                icon="caret-up"
-                fixed-width
-                :style="{
-                  color: gmAvailabilityColor(feature),
-                  'font-size': 'x-large'
-                }"
-              />
+            <div class="d-flex">
+              <div class="d-flex flex-column">
+                <font-awesome-icon
+                  icon="caret-up"
+                  fixed-width
+                  :style="{
+                    color: gmAvailabilityColor(feature),
+                    'font-size': 'x-large',
+                    position: 'relative',
+                    top: '0.28em'
+                  }"
+                />
+                <font-awesome-icon
+                  icon="caret-down"
+                  fixed-width
+                  :style="{
+                    color: gmAvailabilityColor(feature),
+                    'font-size': 'x-large',
+                    position: 'relative',
+                    top: '-0.45em'
+                  }"
+                />
+              </div>
               <small class="my-auto">
                 {{ gmAvailability(feature) }}
               </small>
@@ -227,7 +241,9 @@
   data() {
     return {
       refGaugeStatus: "",
-      gaugeStatus: ""
+      gaugeStatus: "",
+      gaugeCoeffs: null,
+      refGaugeCoeffs: null
     };
   },
   computed: {
@@ -248,10 +264,17 @@
     gaugeStatusText() {
       const nsc24 = this.config.gm_forecast_vs_reality_nsc_24h;
       const nsc72 = this.config.gm_forecast_vs_reality_nsc_72h;
+      const coeffWarn = this.gaugeCoeffs ? this.gaugeCoeffs[2].value : "";
+      const coeffDanger = this.gaugeCoeffs ? this.gaugeCoeffs[0].value : "";
       const messagesPerState = {
-        OK: this.$gettext("Nash-Sutcliffe") + `>${nsc24} /24h >${nsc72} / 72h`,
-        WARNING: this.$gettext("Nash-Sutcliffe") + ` < ${nsc72}`,
-        DANGER: this.$gettext("Nash-Sutcliffe") + ` < ${nsc24}`,
+        OK:
+          this.$gettext("Nash-Sutcliffe") +
+          `(${coeffDanger} >${nsc24} /24h ${coeffWarn} >${nsc72} / 72h)`,
+        WARNING:
+          this.$gettext("Nash-Sutcliffe") + ` (${coeffWarn} < ${nsc72} / 72h)`,
+        DANGER:
+          this.$gettext("Nash-Sutcliffe") +
+          ` (${coeffDanger} < ${nsc24} / 72h)`,
         NEUTRAL: this.$gettext("Nash-Sutcliffe not available")
       };
       return messagesPerState[this.gaugeStatus];
@@ -262,10 +285,19 @@
     refGaugeStatusText() {
       const nsc24 = this.config.gm_forecast_vs_reality_nsc_24h;
       const nsc72 = this.config.gm_forecast_vs_reality_nsc_72h;
+      const coeffWarn = this.refGaugeCoeffs ? this.refGaugeCoeffs[2].value : "";
+      const coeffDanger = this.refGaugeCoeffs
+        ? this.refGaugeCoeffs[0].value
+        : "";
       const messagesPerState = {
-        OK: this.$gettext("Nash-Sutcliffe") + `>${nsc24} /24h >${nsc72} / 72h`,
-        WARNING: this.$gettext("Nash-Sutcliffe") + ` < ${nsc72}`,
-        DANGER: this.$gettext("Nash-Sutcliffe") + ` < ${nsc24}`,
+        OK:
+          this.$gettext("Nash-Sutcliffe") +
+          `(${coeffDanger} >${nsc24} /24h ${coeffWarn} >${nsc72} / 72h)`,
+        WARNING:
+          this.$gettext("Nash-Sutcliffe") + ` (${coeffWarn} < ${nsc72} / 72h)`,
+        DANGER:
+          this.$gettext("Nash-Sutcliffe") +
+          ` (${coeffDanger} < ${nsc24} / 72h)`,
         NEUTRAL: this.$gettext("Nash-Sutcliffe not available")
       };
       return messagesPerState[this.refGaugeStatus];
@@ -284,6 +316,7 @@
         this.$store
           .dispatch("gauges/getNashSutcliffeForISRS", isrs)
           .then(response => {
+            this.gaugeCoeffs = response.coeffs;
             this.gaugeStatus = classifications.calcForecastVsRealityForNSC(
               response
             );
@@ -297,6 +330,7 @@
         this.$store
           .dispatch("gauges/getNashSutcliffeForISRS", isrs)
           .then(response => {
+            this.refGaugeCoeffs = response.coeffs;
             this.refGaugeStatus = classifications.calcForecastVsRealityForNSC(
               response
             );
@@ -335,12 +369,17 @@
     forecastAccuracy(feature) {
       const offset24 = this.config.gm_forecast_offset_24h;
       const offset72 = this.config.gm_forecast_offset_72h;
+      const fa3d = feature.get("forecast_accuracy_3d");
+      const fa1d = feature.get("forecast_accuracy_1d");
       const messagesPerState = {
         OK:
           this.$gettext("Highest confidence") +
           ` <${offset24} cm/24h, <${offset72} cm/72h`,
-        WARNING: this.$gettext("Confidence per 72h") + ` > ${offset72} cm`,
-        DANGER: this.$gettext("Confidence per 24h") + ` > ${offset24} cm`
+        WARNING:
+          this.$gettext("Confidence per 72h") +
+          ` (${fa3d} cm > ${offset72} cm)`,
+        DANGER:
+          this.$gettext("Confidence per 24h") + ` (${fa1d} cm > ${offset24} cm)`
       };
       return messagesPerState[[classifications.forecastAccuracy(feature)]];
     },
@@ -351,13 +390,17 @@
     },
     recency(feature) {
       const revisitingFactor = this.config.bn_revtime_multiplier;
+      const revisitingTime = feature.get("revisiting_time");
       const messagesPerState = {
-        OK: this.$gettext("Data within the revisiting time"),
+        OK:
+          this.$gettext("Data within the revisiting time") +
+          ` (${revisitingTime}d)`,
         WARNING:
           this.$gettext("Data within revisiting treshold") +
-          ` (${revisitingFactor})`,
+          ` (${revisitingTime} * ${revisitingFactor})`,
         DANGER:
-          this.$gettext("Data too old. Treshold:") + ` (${revisitingFactor})`
+          this.$gettext("Data too old. Treshold:") +
+          ` (${revisitingTime}d * ${revisitingFactor})`
       };
       return messagesPerState[classifications.surveyRecency(feature)];
     },
--- a/client/src/components/layers/LegendElement.vue	Mon Sep 09 17:16:41 2019 +0200
+++ b/client/src/components/layers/LegendElement.vue	Mon Sep 09 17:24:07 2019 +0200
@@ -29,9 +29,11 @@
 import Feature from "ol/Feature";
 import { Vector as VectorLayer } from "ol/layer";
 import { Vector as VectorSource } from "ol/source";
+import Polygon from "ol/geom/Polygon";
 import LineString from "ol/geom/LineString";
 import Point from "ol/geom/Point";
 import { HTTP } from "@/lib/http";
+import { Stroke, Style, Fill } from "ol/style";
 
 export default {
   props: ["layer"],
@@ -135,7 +137,8 @@
     recreateLayers() {
       let vector = this.createVectorLayer();
 
-      this.myMap.removeLayer(this.myMap.getLayers()[0]);
+      this.myMap.removeLayer(this.myMap.getLayers().getArray()[0]);
+
       this.myMap.addLayer(vector);
     },
     initMap() {
@@ -155,11 +158,29 @@
     },
     createVectorLayer() {
       let mapStyle = this.layer.getStyle();
-
-      let feature = new Feature({
-        geometry: new LineString([[-1, -1], [0, 0], [1, 1]])
-      });
-
+      let feature;
+      // show bottleneck legend as polygon
+      if (this.layer.get("id") === "BOTTLENECKS") {
+        feature = new Feature({
+          geometry: new Polygon([
+            [[-1.7, -1.2], [-1.7, 0.5], [1.7, 1.2], [1.7, -0.5]]
+          ])
+        });
+        mapStyle = new Style({
+          stroke: new Stroke({
+            color: mapStyle.getStroke().getColor(),
+            // reduce the stroke width for better layout in map legend.
+            width: 2
+          }),
+          fill: new Fill({
+            color: mapStyle.getFill().getColor()
+          })
+        });
+      } else {
+        feature = new Feature({
+          geometry: new LineString([[-1, -1], [0, 0], [1, 1]])
+        });
+      }
       // special case if we need to call the style function with a special
       // parameter or to detect a point layer
       let legendStyle = this.layer.get("forLegendStyle");
--- a/client/src/lib/classifications.js	Mon Sep 09 17:16:41 2019 +0200
+++ b/client/src/lib/classifications.js	Mon Sep 09 17:24:07 2019 +0200
@@ -12,6 +12,9 @@
  * Raimund Renkert <raimund.renkert@intevation.de>
  * Markus Kottländer <markus.kottlaender@intevation.de>
  */
+
+/*eslint no-unused-vars: ["error", { "varsIgnorePattern": "_" }]*/
+
 import store from "@/store/index";
 
 const getGauge = f => {
@@ -32,18 +35,21 @@
 };
 
 const calcForecastVsRealityForNSC = nsc => {
-  if (nsc && nsc.coeffs.reduce((sum, coeff) => sum + coeff.samples, 0)) {
+  const hasSamples =
+    nsc && nsc.coeffs.reduce((sum, coeff) => sum + coeff.samples, 0);
+  if (hasSamples) {
+    const [nsc24h, _, nsc72h] = nsc.coeffs;
     // 24h < configured value
     if (
-      nsc.coeffs[0].samples &&
-      nsc.coeffs[0].value <
+      nsc24h.samples &&
+      nsc24h.value <
         store.state.application.config.gm_forecast_vs_reality_nsc_24h
     )
       return "DANGER";
     // 72h < configured value
     if (
-      nsc.coeffs[2].samples &&
-      nsc.coeffs[2].value <
+      nsc72h.samples &&
+      nsc72h.value <
         store.state.application.config.gm_forecast_vs_reality_nsc_72h
     )
       return "WARNING";
--- a/client/src/main.js	Mon Sep 09 17:16:41 2019 +0200
+++ b/client/src/main.js	Mon Sep 09 17:24:07 2019 +0200
@@ -53,6 +53,7 @@
   faBars,
   faBook,
   faCaretUp,
+  faCaretDown,
   faChartArea,
   faChartBar,
   faChartLine,
@@ -124,6 +125,7 @@
   faBars,
   faBook,
   faCaretUp,
+  faCaretDown,
   faChartArea,
   faChartBar,
   faChartLine,
--- a/client/src/store/fairwayavailability.js	Mon Sep 09 17:16:41 2019 +0200
+++ b/client/src/store/fairwayavailability.js	Mon Sep 09 17:24:07 2019 +0200
@@ -178,12 +178,6 @@
     setSelectedFairwayAvailability: (state, feature) => {
       state.selectedFairwayAvailabilityFeature = feature;
     },
-    setFwLNWLData: (state, fwLNWLData) => {
-      state.fwLNWLData = fwLNWLData;
-    },
-    setCSV: (state, csv) => {
-      state.csv = csv;
-    },
     addFwLNWLOverviewData: (state, data) => {
       let existingIndex = state.fwLNWLOverviewData.findIndex(
         d => d.feature.get("id") === data.feature.get("id")
@@ -212,6 +206,37 @@
       });
 
       state.fwData = transformAFD(csv);
+    },
+    // See docs/developers.md for an example how to directly
+    // call this method for testing.
+    setAvailableFairwayDepthLNWLData: (state, data) => {
+      state.csv = data;
+
+      data = data.split("\n").filter(d => d);
+      data.shift(); // remove header line
+      data = data.map(d => {
+        let columns = d.split(",");
+        let result;
+        if (columns.length === 6) {
+          result = {
+            date: columns[0],
+            ldc: Number(columns[2]),
+            below: Number(columns[3]),
+            between: Number(columns[4]),
+            above: Number(columns[5])
+          };
+        } else {
+          result = {
+            date: columns[0],
+            ldc: Number(columns[2]),
+            below: Number(columns[3]),
+            between: null,
+            above: Number(columns[4])
+          };
+        }
+        return result;
+      });
+      state.fwLNWLData = data;
     }
   },
   actions: {
@@ -306,34 +331,8 @@
       });
     },
     loadAvailableFairwayDepthLNWLDiagram: ({ commit, dispatch }, options) => {
-      dispatch("loadAvailableFairwayDepthLNWL", options).then(response => {
-        commit("setCSV", response);
-        let data = response.split("\n").filter(d => d);
-        data.shift(); // remove header line
-        data = data.map(d => {
-          let columns = d.split(",");
-          let result;
-          if (columns.length === 6) {
-            result = {
-              date: columns[0],
-              ldc: Number(columns[2]),
-              below: Number(columns[3]),
-              between: Number(columns[4]),
-              above: Number(columns[5])
-            };
-          } else {
-            result = {
-              date: columns[0],
-              ldc: Number(columns[2]),
-              below: Number(columns[3]),
-              between: null,
-              above: Number(columns[4])
-            };
-          }
-          return result;
-        });
-        commit("setFwLNWLData", data);
-        return data;
+      dispatch("loadAvailableFairwayDepthLNWL", options).then(data => {
+        commit("setAvailableFairwayDepthLNWLData", data);
       });
     },
     loadAvailableFairwayDepthLNWLForMap: ({ dispatch }, options) => {
--- a/pkg/controllers/bottlenecks.go	Mon Sep 09 17:16:41 2019 +0200
+++ b/pkg/controllers/bottlenecks.go	Mon Sep 09 17:24:07 2019 +0200
@@ -10,6 +10,7 @@
 //
 // Author(s):
 //  * Sascha L. Teichmann <sascha.teichmann@intevation.de>
+//  * Sascha Wilde <wilde@intevation.de>
 
 package controllers
 
@@ -131,6 +132,43 @@
 	}
 }
 
+// According to clarification, it has to be assumed, that at times
+// with no data, the best case (which by convention is the highest
+// class created by classify()) should be assumed.  That is due to the
+// fact, that at times where bottlenecks are not a limiting factor on
+// the waterway, services don't provide any data for the bottleneck in
+// question.
+//
+// FIXME: A potential improvement could be to intersect the time
+// ranges with the time ranges where bottlenecks were "active" (this
+// _might_ be derivable from the validity periods in the bottleneck
+// data.  So it _might_ be possible do detect actual missing data (BN
+// valid, but no data from FA service).  Anyway, this is left out for
+// now, as many clarification regarding the base assumtions would be
+// needed and the results still might be unrelyable.
+func optimisticPadClassification(
+	from, to time.Time,
+	classified []time.Duration,
+) []time.Duration {
+
+	var actualDuration time.Duration
+	for _, v := range classified {
+		actualDuration += v
+	}
+
+	// If the actual duration is smaller than the length
+	// of the classifaction interval extend the
+	// time spend in the highest class by the difference.
+	if delta := to.Sub(from) - actualDuration; delta > 0 {
+		log.Printf("info: time interval: (%v - %v)\n", from, to)
+		log.Printf("info: found only data for %.2f hours, padding by %.2f hours\n",
+			actualDuration.Hours(), delta.Hours())
+		classified[len(classified)-1] += delta
+	}
+
+	return classified
+}
+
 func (measurements availMeasurements) classify(
 	from, to time.Time,
 	breaks []float64,
@@ -158,7 +196,7 @@
 	})
 
 	if idx >= len(measurements) {
-		return result
+		return optimisticPadClassification(from, to, result)
 	}
 
 	// Be safe for interpolation.
@@ -173,7 +211,7 @@
 		p2 := &measurements[i+1]
 
 		if p1.when.After(to) {
-			return result
+			return optimisticPadClassification(from, to, result)
 		}
 
 		if p2.when.Before(from) {
@@ -214,7 +252,7 @@
 		}
 	}
 
-	return result
+	return optimisticPadClassification(from, to, result)
 }
 
 func orderTime(a, b time.Time) (time.Time, time.Time) {
@@ -482,7 +520,13 @@
 
 	interval := intervals[mode](from, to)
 
+	now := time.Now()
 	for pfrom, pto, label := interval(); label != ""; pfrom, pto, label = interval() {
+		// Don't interpolate for the future
+		if now.Sub(pto) < 0 {
+			pto = now
+		}
+
 		lnwl := ms.classify(
 			pfrom, pto,
 			ldcRefs,
@@ -638,7 +682,12 @@
 
 	interval := intervals[mode](from, to)
 
+	now := time.Now()
 	for pfrom, pto, label := interval(); label != ""; pfrom, pto, label = interval() {
+		// Don't interpolate for the future
+		if now.Sub(pto) < 0 {
+			pto = now
+		}
 
 		ldc := ms.classify(
 			pfrom, pto,
--- a/pkg/controllers/stretches.go	Mon Sep 09 17:16:41 2019 +0200
+++ b/pkg/controllers/stretches.go	Mon Sep 09 17:24:07 2019 +0200
@@ -279,7 +279,13 @@
 
 				var ldc, breaks []time.Duration
 
+				now := time.Now()
 				for _, bn := range loaded {
+					// Don't interpolate for the future
+					if now.Sub(res.to) < 0 {
+						res.to = now
+					}
+
 					l := bn.measurements.classify(
 						res.from, res.to,
 						bn.ldc,
@@ -539,7 +545,13 @@
 			for res := range jobCh {
 				var ldc, breaks []time.Duration
 
+				now := time.Now()
 				for _, bn := range loaded {
+					// Don't interpolate for the future
+					if now.Sub(res.to) < 0 {
+						res.to = now
+					}
+
 					l := bn.measurements.classify(
 						res.from, res.to,
 						bn.ldc,
--- a/pkg/geoserver/boot.go	Mon Sep 09 17:16:41 2019 +0200
+++ b/pkg/geoserver/boot.go	Mon Sep 09 17:24:07 2019 +0200
@@ -114,7 +114,7 @@
 	// Probe  workspace.
 	req, err := http.NewRequest(
 		http.MethodGet,
-		geoURL+"/rest/workspaces/"+workspaceName+".json",
+		geoURL+"/rest/workspaces/"+workspaceName+".json?quietOnNotFound=true",
 		nil)
 	if err != nil {
 		return err
@@ -170,7 +170,7 @@
 	req, err := http.NewRequest(
 		http.MethodGet,
 		geoURL+"/rest/workspaces/"+workspaceName+
-			"/datastores/"+datastoreName+".json",
+			"/datastores/"+datastoreName+".json?quietOnNotFound=true",
 		nil)
 	if err != nil {
 		return err
--- a/schema/gemma.sql	Mon Sep 09 17:16:41 2019 +0200
+++ b/schema/gemma.sql	Mon Sep 09 17:24:07 2019 +0200
@@ -807,8 +807,8 @@
 
 -- Configure primary keys for geoserver views
 INSERT INTO waterway.gt_pk_metadata VALUES
-  ('waterway', 'gauges_geoserver', 'location'),
-  ('waterway', 'distance_marks_geoserver', 'location_code'),
+  ('waterway', 'gauges_geoserver', 'isrs_code'),
+  ('waterway', 'distance_marks_geoserver', 'location'),
   ('waterway', 'distance_marks_ashore_geoserver', 'id'),
   ('waterway', 'bottlenecks_geoserver', 'id'),
   ('waterway', 'stretches_geoserver', 'id'),
--- a/schema/geoserver_views.sql	Mon Sep 09 17:16:41 2019 +0200
+++ b/schema/geoserver_views.sql	Mon Sep 09 17:24:07 2019 +0200
@@ -55,13 +55,11 @@
 
 CREATE OR REPLACE VIEW waterway.gauges_geoserver AS
     SELECT
-        location,
         isrs_code,
         objname,
         geom,
         applicability_from_km,
         applicability_to_km,
-        validity,
         zero_point,
         geodref,
         date_info,
@@ -76,7 +74,7 @@
     WHERE NOT erased;
 
 CREATE OR REPLACE VIEW waterway.distance_marks_geoserver AS
-    SELECT location_code,
+    SELECT
        isrs_asText(location_code) AS location,
        geom::Geometry(POINT, 4326),
        related_enc,
@@ -99,7 +97,6 @@
         b.bottleneck_id,
         b.objnam,
         b.nobjnm,
-        b.stretch,
         b.area,
         b.rb,
         b.lb,
@@ -108,7 +105,6 @@
         b.limiting,
         b.date_info,
         b.source_organization,
-        g.location AS gauge_isrs_code,
         g.objname AS gauge_objname,
         g.reference_water_levels,
         fal.date_info AS fa_date_info,
@@ -155,7 +151,8 @@
         max(g.forecast_accuracy_3d) AS forecast_accuracy_3d,
         max(g.forecast_accuracy_1d) AS forecast_accuracy_1d
     FROM waterway.stretches s
-        LEFT JOIN waterway.gauges_geoserver g ON g.location <@ s.stretch
+        LEFT JOIN waterway.gauges_geoserver g
+            ON isrs_fromtext(g.isrs_code) <@ s.stretch
     GROUP BY s.id;
 
 CREATE OR REPLACE VIEW waterway.sections_geoserver AS
@@ -175,7 +172,8 @@
         max(g.forecast_accuracy_3d) AS forecast_accuracy_3d,
         max(g.forecast_accuracy_1d) AS forecast_accuracy_1d
     FROM waterway.sections s
-        LEFT JOIN waterway.gauges_geoserver g ON g.location <@ s.section
+        LEFT JOIN waterway.gauges_geoserver g
+            ON isrs_fromtext(g.isrs_code) <@ s.section
     GROUP BY s.id;
 
 CREATE OR REPLACE VIEW waterway.sounding_results_contour_lines_geoserver AS
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/schema/updates/1112/01.cleanup_views.sql	Mon Sep 09 17:24:07 2019 +0200
@@ -0,0 +1,130 @@
+DROP VIEW waterway.gauges_geoserver CASCADE;
+CREATE VIEW waterway.gauges_geoserver AS
+    SELECT
+        isrs_code,
+        objname,
+        geom,
+        applicability_from_km,
+        applicability_to_km,
+        zero_point,
+        geodref,
+        date_info,
+        source_organization,
+        reference_water_levels,
+        gm_measuredate,
+        gm_waterlevel,
+        gm_n_14d,
+        forecast_accuracy_3d,
+        forecast_accuracy_1d
+    FROM waterway.gauges_base_view
+    WHERE NOT erased;
+
+DROP VIEW waterway.distance_marks_geoserver;
+CREATE VIEW waterway.distance_marks_geoserver AS
+    SELECT
+       isrs_asText(location_code) AS location,
+       geom::Geometry(POINT, 4326),
+       related_enc,
+       (location_code).hectometre
+    FROM waterway.distance_marks_virtual;
+
+DROP VIEW waterway.bottlenecks_geoserver;
+CREATE OR REPLACE VIEW waterway.bottlenecks_geoserver AS
+    SELECT
+        b.id,
+        b.bottleneck_id,
+        b.objnam,
+        b.nobjnm,
+        b.area,
+        b.rb,
+        b.lb,
+        b.responsible_country,
+        b.revisiting_time,
+        b.limiting,
+        b.date_info,
+        b.source_organization,
+        g.objname AS gauge_objname,
+        g.reference_water_levels,
+        fal.date_info AS fa_date_info,
+        fal.critical AS fa_critical,
+        g.gm_measuredate,
+        g.gm_waterlevel,
+        g.gm_n_14d,
+        srl.date_max,
+        g.forecast_accuracy_3d,
+        g.forecast_accuracy_1d
+    FROM waterway.bottlenecks b
+        LEFT JOIN waterway.gauges_base_view g
+            ON b.gauge_location = g.location AND g.validity @> current_timestamp
+        LEFT JOIN (SELECT DISTINCT ON (bottleneck_id)
+                    bottleneck_id, date_info, critical
+                FROM waterway.fairway_availability
+                ORDER BY bottleneck_id, date_info DESC) AS fal
+            ON b.bottleneck_id = fal.bottleneck_id
+        LEFT JOIN (SELECT DISTINCT ON (bottleneck_id)
+                    bottleneck_id, max(date_info) AS date_max
+                FROM waterway.sounding_results
+                GROUP BY bottleneck_id
+                ORDER BY bottleneck_id DESC) AS srl
+            ON b.bottleneck_id = srl.bottleneck_id
+    WHERE b.validity @> current_timestamp;
+
+CREATE VIEW waterway.stretches_geoserver AS
+    SELECT
+        s.id,
+        s.name,
+        (s.stretch).lower::varchar as lower,
+        (s.stretch).upper::varchar as upper,
+        s.area::Geometry(MULTIPOLYGON, 4326),
+        s.objnam,
+        s.nobjnam,
+        s.date_info,
+        s.source_organization,
+        (SELECT string_agg(country_code, ', ')
+            FROM waterway.stretch_countries
+            WHERE stretches_id = s.id) AS countries,
+        s.staging_done,
+        min(g.gm_measuredate) AS gm_measuredate,
+        min(g.gm_n_14d) AS gm_n_14d,
+        max(g.forecast_accuracy_3d) AS forecast_accuracy_3d,
+        max(g.forecast_accuracy_1d) AS forecast_accuracy_1d
+    FROM waterway.stretches s
+        LEFT JOIN waterway.gauges_geoserver g
+            ON isrs_fromtext(g.isrs_code) <@ s.stretch
+    GROUP BY s.id;
+
+CREATE VIEW waterway.sections_geoserver AS
+    SELECT
+        s.id,
+        s.name,
+        (s.section).lower::varchar as lower,
+        (s.section).upper::varchar as upper,
+        s.area::Geometry(MULTIPOLYGON, 4326),
+        s.objnam,
+        s.nobjnam,
+        s.date_info,
+        s.source_organization,
+        s.staging_done,
+        min(g.gm_measuredate) AS gm_measuredate,
+        min(g.gm_n_14d) AS gm_n_14d,
+        max(g.forecast_accuracy_3d) AS forecast_accuracy_3d,
+        max(g.forecast_accuracy_1d) AS forecast_accuracy_1d
+    FROM waterway.sections s
+        LEFT JOIN waterway.gauges_geoserver g
+            ON isrs_fromtext(g.isrs_code) <@ s.section
+    GROUP BY s.id;
+
+UPDATE waterway.gt_pk_metadata SET pk_column = 'isrs_code'
+    WHERE table_schema = 'waterway'
+        AND table_name = 'gauges_geoserver';
+UPDATE waterway.gt_pk_metadata SET pk_column = 'location'
+    WHERE table_schema = 'waterway'
+        AND table_name = 'distance_marks_geoserver';
+
+GRANT SELECT ON
+        waterway.gauges_geoserver,
+        waterway.distance_marks_geoserver,
+        waterway.bottlenecks_geoserver,
+        waterway.stretches_geoserver,
+        waterway.sections_geoserver
+    TO waterway_user;