changeset 4623:30bb2d819d57 geoserver_sql_views

Merge default into geoserver_sql_views
author Tom Gottfried <tom@intevation.de>
date Wed, 09 Oct 2019 16:40:18 +0200
parents b03aa1502736 (current diff) a63df1ac39ac (diff)
children 209b10f7bb2c
files schema/default_sysconfig.sql schema/gemma.sql schema/updates/1300/02.views_to_geoservers.sql schema/version.sql style-templates/sounding_results_contour_lines_geoserver.sld-template
diffstat 39 files changed, 893 insertions(+), 410 deletions(-) [+]
line wrap: on
line diff
--- a/.hgtags	Fri Oct 04 17:26:56 2019 +0200
+++ b/.hgtags	Wed Oct 09 16:40:18 2019 +0200
@@ -14,3 +14,5 @@
 5396581cf20334cbc5e69280e5d9b192640d96b9 v4-preview20190717
 aececbc3d04798d905e65196ac0870d081776ca2 v4-preview20190726
 b5619087e3e909645eeab9e3f198667692895382 v4-preview20190918
+8a6c410f6f03ca8f50022c11f17d0bb2b86215f5 v4-preview20190930
+a92239475590146dc6ad99fc4a3f4ced857c73f6 v4
--- a/client/package.json	Fri Oct 04 17:26:56 2019 +0200
+++ b/client/package.json	Wed Oct 09 16:40:18 2019 +0200
@@ -1,6 +1,6 @@
 {
   "name": "gemmajs",
-  "version": "4.0.0-dev",
+  "version": "4.1.0-dev",
   "license": "AGPL-3.0-or-later",
   "repository": {
     "type": "hg",
--- a/client/src/components/fairway/AvailableFairwayDepth.vue	Fri Oct 04 17:26:56 2019 +0200
+++ b/client/src/components/fairway/AvailableFairwayDepth.vue	Wed Oct 09 16:40:18 2019 +0200
@@ -39,7 +39,7 @@
             :href="dataLink"
             :download="csvFileName"
             class="mt-2 btn btn-sm btn-info w-100"
-            >Download CSV</a
+            ><translate>Download CSV</translate></a
           >
         </div>
         <div class="btn-group-toggle w-100 mt-2">
@@ -50,7 +50,7 @@
               type="checkbox"
               v-model="showNumbers"
               autocomplete="off"
-            />Numbers
+            /><translate>Numbers</translate>
           </label>
         </div>
       </DiagramLegend>
@@ -226,7 +226,7 @@
     },
     title() {
       if (!this.frequencyD) return;
-      return `Available Fairway Depth: ${
+      return `${this.$gettext("Available Fairway Depth:")} ${
         this.featureName
       } (${filters.surveyDate(this.fromDate)} - ${filters.surveyDate(
         this.toDate
@@ -275,7 +275,9 @@
       }
     },
     downloadPDF() {
-      let title = `Available Fairway Depth: ${this.featureName}`;
+      let title = `${this.$gettext("Available Fairway Depth:")} ${
+        this.featureName
+      }`;
       this.generatePDF({
         templateData: this.templateData,
         diagramTitle: title
--- a/client/src/components/fairway/AvailableFairwayDepthLNWL.vue	Fri Oct 04 17:26:56 2019 +0200
+++ b/client/src/components/fairway/AvailableFairwayDepthLNWL.vue	Wed Oct 09 16:40:18 2019 +0200
@@ -39,7 +39,7 @@
             :href="dataLink"
             :download="csvFileName"
             class="mt-2 btn btn-sm btn-info w-100"
-            >Download CSV</a
+            ><translate>Download CSV</translate></a
           >
         </div>
         <div class="btn-group-toggle w-100 mt-2">
@@ -50,7 +50,7 @@
               type="checkbox"
               v-model="showNumbers"
               autocomplete="off"
-            />Numbers
+            /><translate>Numbers</translate>
           </label>
         </div>
       </DiagramLegend>
@@ -217,7 +217,7 @@
     },
     title() {
       if (!this.frequencyD) return;
-      return `Available Fairway Depth vs LNWL: ${
+      return `${this.$gettext("Available Fairway Depth vs LNWL:")} ${
         this.featureName
       } (${filters.surveyDate(this.fromDate)} - ${filters.surveyDate(
         this.toDate
@@ -285,7 +285,9 @@
       }
     },
     downloadPDF() {
-      let title = `Available Fairway Depth vs LNWL: ${this.featureName}`;
+      let title = `${this.$gettext("Available Fairway Depth vs LNWL:")} ${
+        this.featureName
+      }`;
       this.generatePDF({
         templateData: this.templateData,
         diagramTitle: title
--- a/client/src/components/fairway/BottleneckDialogue.vue	Fri Oct 04 17:26:56 2019 +0200
+++ b/client/src/components/fairway/BottleneckDialogue.vue	Wed Oct 09 16:40:18 2019 +0200
@@ -88,6 +88,7 @@
                   >
                 </select>
                 <button
+                  v-if="isAllowedToDelete"
                   class="btn btn-dark btn-xs ml-2"
                   @click="deleteSelectedSurvey"
                 >
@@ -347,6 +348,9 @@
     };
   },
   computed: {
+    ...mapState("user", ["user"]),
+    ...mapGetters("user", ["isWaterwayAdmin", "isSysAdmin"]),
+    ...mapGetters("usermanagement", ["userCountries"]),
     ...mapState("application", ["showProfiles", "paneSetup"]),
     ...mapState("map", ["openLayersMaps", "syncedMaps", "cutToolEnabled"]),
     ...mapState("bottlenecks", [
@@ -354,12 +358,29 @@
       "surveys",
       "surveysLoading"
     ]),
+    isAllowedToDelete() {
+      const userCountryCode = this.userCountries[this.user];
+      const bottleneck = this.bottlenecksList.find(
+        bn => bn.properties.name === this.selectedBottleneck
+      );
+      if (!bottleneck) return;
+      if (this.isWaterwayAdmin || this.isSysAdmin) {
+        if (
+          userCountryCode === "global" ||
+          bottleneck.properties.responsible_country === userCountryCode
+        ) {
+          return true;
+        }
+      }
+      return false;
+    },
     ...mapState("fairwayprofile", [
       "previousCuts",
       "startPoint",
       "endPoint",
       "profileLoading",
       "differencesLoading",
+      "currentDifference",
       "waterLevels",
       "currentProfile"
     ]),
@@ -520,7 +541,7 @@
       if (cut) {
         if (cut.depth) {
           this.depth = cut.depth;
-          this.useCustomDepth = true;
+          this.useCustomDepth = cut.useCustomDepth;
         }
         this.applyCoordinates(cut.coordinates);
       }
@@ -552,7 +573,30 @@
           }
         }
       )
-        .then()
+        .then(response => {
+          this.$store.commit(
+            "fairwayprofile/setCurrentDifference",
+            response.data.id
+          );
+          if (this.openLayersMap(COMPARESURVEYS.compare.id)) {
+            this.openLayersMap(COMPARESURVEYS.compare.id)
+              .getLayer("DIFFERENCES")
+              .getSource()
+              .updateParams({
+                LAYERS: "sounding_differences",
+                VERSION: "1.1.1",
+                TILED: true,
+                CQL_FILTER: "id=" + response.data.id
+              });
+            this.openLayersMap(COMPARESURVEYS.compare.id)
+              .getLayer("DIFFERENCES")
+              .getSource()
+              .refresh();
+            this.openLayersMap(COMPARESURVEYS.compare.id)
+              .getLayer("DIFFERENCES")
+              .setVisible(false);
+          }
+        })
         .catch(error => {
           let status, data, message;
           if (error.response) {
@@ -665,7 +709,8 @@
         bottleneckName: this.selectedBottleneck,
         coordinates: [...this.startPoint, ...this.endPoint],
         timestamp: new Date().getTime(),
-        depth: this.depth
+        depth: this.depth,
+        useCustomDepth: this.useCustomDepth
       };
       const existingEntry = previousCuts.find(cut => {
         return JSON.stringify(cut) === JSON.stringify(newEntry);
@@ -757,6 +802,13 @@
     }
   },
   mounted() {
+    this.$store.dispatch("usermanagement/loadUsers").catch(error => {
+      const { status, data } = error.response;
+      displayError({
+        title: this.$gettext("Backend Error"),
+        message: `${status}: ${data.message || data}`
+      });
+    });
     this.$store.dispatch("bottlenecks/loadBottlenecksList");
   }
 };
--- a/client/src/components/fairway/Fairwayprofile.vue	Fri Oct 04 17:26:56 2019 +0200
+++ b/client/src/components/fairway/Fairwayprofile.vue	Wed Oct 09 16:40:18 2019 +0200
@@ -7,7 +7,9 @@
           <span
             style="background-color: #5995ff; width: 20px; height: 20px;"
           ></span>
-          Water
+          <span class="fix-trans-space" style="display:inline;" v-translate
+            >Water</span
+          >
         </div>
         <div class="legend">
           <span
@@ -20,7 +22,9 @@
                 '; background-clip: padding-box; box-sizing: content-box;'
             "
           ></span>
-          Fairway (LOS 1)
+          <span class="fix-trans-space" style="display:inline;" v-translate
+            >Fairway (LOS 1)</span
+          >
         </div>
         <div class="legend">
           <span
@@ -33,7 +37,9 @@
                 '; background-clip: padding-box; box-sizing: content-box;'
             "
           ></span>
-          Fairway (LOS 2)
+          <span class="fix-trans-space" style="display:inline;" v-translate
+            >Fairway (LOS 2)</span
+          >
         </div>
         <div class="legend">
           <span
@@ -46,19 +52,25 @@
                 '; background-clip: padding-box; box-sizing: content-box;'
             "
           ></span>
-          Fairway (LOS 3)
+          <span class="fix-trans-space" style="display:inline;" v-translate
+            >Fairway (LOS 3)</span
+          >
         </div>
         <div class="legend">
           <span
             style="width: 14px; height: 14px; background-color: #4a2f06; border: solid 3px black; background-clip: padding-box; box-sizing: content-box;"
           ></span>
-          Sediment
+          <span class="fix-trans-space" style="display:inline;" v-translate
+            >Sediment</span
+          >
         </div>
         <div class="legend">
           <span
             style="width: 14px; height: 14px; background-color: rgba(74, 47, 6, 0.6); border: solid 3px #943007; background-clip: padding-box; box-sizing: content-box;"
           ></span>
-          Sediment (Compare)
+          <span class="fix-trans-space" style="display:inline;" v-translate
+            >Sediment (Compare)</span
+          >
         </div>
         <div>
           <select
@@ -284,20 +296,11 @@
       let style = this.openLayersMap()
         .getLayer("FAIRWAYDIMENSIONSLOS" + los)
         .getStyle()()[0];
-
       // use spread operator to clone arrays
       let fillColor = style.getFill().getColor();
-      let fillOpacity =
-        fillColor.length < 8
-          ? 0.8
-          : parseFloat(parseInt(fillColor.slice(7), 16) / 255).toFixed(2);
       let strokeColor = style.getStroke().getColor();
-      let strokeOpacity =
-        strokeColor.length < 8
-          ? 0.8
-          : parseFloat(parseInt(strokeColor.slice(7), 16) / 255).toFixed(2);
       let strokeDash = style.getStroke().getLineDash();
-      return { fillColor, fillOpacity, strokeColor, strokeOpacity, strokeDash };
+      return { fillColor, strokeColor, strokeDash };
     },
     applyChange() {
       if (this.form.template.hasOwnProperty("properties")) {
@@ -358,33 +361,33 @@
       this.pdf.doc.setDrawColor("#5995ff");
       this.pdf.doc.setFillColor("#5995ff");
       this.pdf.doc.circle(x, y, 2, "FD");
-      this.pdf.doc.text(x + 3, y + 1, "Water");
+      this.pdf.doc.text(x + 3, y + 1, this.$gettext("Water"));
 
       this.pdf.doc.setLineDashPattern([0.8], 0);
       this.pdf.doc.setDrawColor("#0000ff");
       this.pdf.doc.setFillColor("#fcfacc");
       this.pdf.doc.circle(x, y + 5, 2, "FD");
-      this.pdf.doc.text(x + 3, y + 6, "Fairway (LOS 1)");
+      this.pdf.doc.text(x + 3, y + 6, this.$gettext("Fairway (LOS 1)"));
 
       this.pdf.doc.setLineDashPattern([1.8], 0);
       this.pdf.doc.setFillColor("#fdfce5");
       this.pdf.doc.circle(x, y + 10, 2, "FD");
-      this.pdf.doc.text(x + 3, y + 11, "Fairway (LOS 2)");
+      this.pdf.doc.text(x + 3, y + 11, this.$gettext("Fairway (LOS 2)"));
 
       this.pdf.doc.setLineDashPattern([], 0);
       this.pdf.doc.setFillColor("#ffffff");
       this.pdf.doc.circle(x, y + 15, 2, "FD");
-      this.pdf.doc.text(x + 3, y + 16, "Fairway (LOS 3)");
+      this.pdf.doc.text(x + 3, y + 16, this.$gettext("Fairway (LOS 3)"));
 
       this.pdf.doc.setDrawColor("black");
       this.pdf.doc.setFillColor("#4a2e06");
       this.pdf.doc.circle(x, y + 20, 2, "FD");
-      this.pdf.doc.text(x + 3, y + 21, "Sediment");
+      this.pdf.doc.text(x + 3, y + 21, this.$gettext("Sediment"));
 
       this.pdf.doc.setDrawColor("#943007");
       this.pdf.doc.setFillColor("#928269");
       this.pdf.doc.circle(x, y + 25, 2, "FD");
-      this.pdf.doc.text(x + 3, y + 26, "Sediment (Compare)");
+      this.pdf.doc.text(x + 3, y + 26, this.$gettext("Sediment (Compare)"));
     },
     getPrintLayout(svgHeight, svgWidth) {
       return {
@@ -479,9 +482,7 @@
               { x: endPoint, y: this.useCustomDepth ? this.depth : depth }
             ])
             .attr("fill", `${this.getLayerStyle(data.los).fillColor}`)
-            .attr("fill-opacity", this.getLayerStyle(data.los).fillOpacity)
             .attr("stroke", `${this.getLayerStyle(data.los).strokeColor}`)
-            .attr("stroke-opacity", this.getLayerStyle(data.los).strokeOpacity)
             .attr("stroke-dasharray", this.getLayerStyle(data.los).strokeDash)
             .attr("d", fairwayArea)
             .attr("transform", `translate(0 ${-offsetY})`);
@@ -496,7 +497,7 @@
         .attr("x", -dimensions.mainHeight / 2)
         .attr("fill", "black")
         .style("text-anchor", "middle")
-        .text("Depth [m]");
+        .text(this.$gettext("Depth [m]"));
       graph
         .append("text")
         .attr("transform", ["rotate(-90)"])
@@ -504,7 +505,7 @@
         .attr("x", -dimensions.mainHeight / 2)
         .attr("fill", "black")
         .style("text-anchor", "middle")
-        .text("Waterlevel [m]");
+        .text(this.$gettext("Waterlevel [m]"));
       graph
         .append("text")
         .attr("y", 0)
@@ -516,7 +517,7 @@
           `translate(${dimensions.width / 2} ${dimensions.mainHeight})`,
           "rotate(0)"
         ])
-        .text("Width [m]");
+        .text(this.$gettext("Width [m]"));
     },
     generateScalesAndGraph({ svg, height, width, dimensions, offsetY }) {
       let xScale = d3
--- a/client/src/components/gauge/HydrologicalConditions.vue	Fri Oct 04 17:26:56 2019 +0200
+++ b/client/src/components/gauge/HydrologicalConditions.vue	Wed Oct 09 16:40:18 2019 +0200
@@ -18,25 +18,33 @@
           <span
             style="background-color: orange; width: 20px; height: 20px;"
           ></span>
-          Q25%
+          <span class="fix-trans-space" style="display:inline;" v-translate
+            >Q25%</span
+          >
         </div>
         <div class="legend">
           <span
             style="background-color: black; width: 20px; height: 20px;"
           ></span>
-          Median
+          <span class="fix-trans-space" style="display:inline;" v-translate
+            >Median</span
+          >
         </div>
         <div class="legend">
           <span
             style="background-color: purple; width: 20px; height: 20px;"
           ></span>
-          Q75%
+          <span class="fix-trans-space" style="display:inline;" v-translate
+            >Q75%</span
+          >
         </div>
         <div class="legend">
           <span
             style="background-color: lightsteelblue; width: 20px; height: 20px;"
           ></span>
-          Long-term Amplitude
+          <span class="fix-trans-space" style="display:inline;" v-translate
+            >Long-term Amplitude</span
+          >
         </div>
         <select
           @change="applyChange"
@@ -282,16 +290,20 @@
       this.pdf.doc.text(x + padding, y + 1, "" + this.yearCompareD);
       this.pdf.doc.setFillColor("orange");
       this.pdf.doc.circle(x, y + 5, 2, "FD");
-      this.pdf.doc.text(x + padding, y + 6, "Q25%");
+      this.pdf.doc.text(x + padding, y + 6, this.$gettext("Q25%"));
       this.pdf.doc.setFillColor("black");
       this.pdf.doc.circle(x, y + 10, 2, "FD");
-      this.pdf.doc.text(x + 3, y + 11, "Median ");
+      this.pdf.doc.text(x + 3, y + 11, this.$gettext("Median"));
       this.pdf.doc.setFillColor("purple");
       this.pdf.doc.circle(x, y + 15, 2, "FD");
-      this.pdf.doc.text(x + padding, y + 16, "Q75%");
+      this.pdf.doc.text(x + padding, y + 16, this.$gettext("Q75%"));
       this.pdf.doc.setFillColor("lightsteelblue");
       this.pdf.doc.circle(x, y + 20, 2, "FD");
-      this.pdf.doc.text(x + padding, y + 21, "Long-term Amplitude");
+      this.pdf.doc.text(
+        x + padding,
+        y + 21,
+        this.$gettext("Long-term Amplitude")
+      );
     },
     getPrintLayout(svgHeight, svgWidth) {
       return {
--- a/client/src/components/gauge/Waterlevel.vue	Fri Oct 04 17:26:56 2019 +0200
+++ b/client/src/components/gauge/Waterlevel.vue	Wed Oct 09 16:40:18 2019 +0200
@@ -12,19 +12,25 @@
           <span
             style="background-color: steelblue; width: 20px; height: 20px;"
           ></span>
-          Waterlevel
+          <span class="fix-trans-space" style="display:inline;" v-translate
+            >Waterlevel</span
+          >
         </div>
         <div class="legend">
           <span
             style="width: 8px; height: 8px; background-color: rgba(70, 130, 180, 0.6); border: solid 7px rgba(70, 130, 180, 0.2); background-clip: padding-box; box-sizing: content-box;"
           ></span>
-          Prediction
+          <span class="fix-trans-space" style="display:inline;" v-translate
+            >Prediction</span
+          >
         </div>
         <div class="legend">
           <span
             style="background-color: rgba(0, 255, 0, 0.1); width: 20px; height: 20px;"
           ></span>
-          Navigable Range
+          <span class="fix-trans-space" style="display:inline;" v-translate
+            >Navigable Range</span
+          >
         </div>
         <div>
           <select
@@ -282,16 +288,16 @@
       this.pdf.doc.setDrawColor("white");
       this.pdf.doc.setFillColor("steelblue");
       this.pdf.doc.circle(x, y, 2, "FD");
-      this.pdf.doc.text(x + padding, y + 1, "Waterlevel");
+      this.pdf.doc.text(x + padding, y + 1, this.$gettext("Waterlevel"));
       this.pdf.doc.setFillColor("#dae6f0");
       this.pdf.doc.circle(x, y + 5, 2, "FD");
       this.pdf.doc.setFillColor("#e5ffe5");
       this.pdf.doc.circle(x, y + 10, 2, "FD");
-      this.pdf.doc.text(x + padding, y + 11, "Navigable Range");
+      this.pdf.doc.text(x + padding, y + 11, this.$gettext("Navigable Range"));
       this.pdf.doc.setDrawColor("#90b4d2");
       this.pdf.doc.setFillColor("#90b4d2");
       this.pdf.doc.circle(x, y + 5, 0.6, "FD");
-      this.pdf.doc.text(x + padding, y + 6, "Prediction");
+      this.pdf.doc.text(x + padding, y + 6, this.$gettext("Prediction"));
     },
     getPrintLayout(svgHeight, svgWidth) {
       return {
--- a/client/src/components/layers/Layerselect.vue	Fri Oct 04 17:26:56 2019 +0200
+++ b/client/src/components/layers/Layerselect.vue	Wed Oct 09 16:40:18 2019 +0200
@@ -78,7 +78,7 @@
     refreshLegend() {
       if (this.layer.get("id") === "BOTTLENECKISOLINE") {
         this.loadLegendImage(
-          "sounding_results_contour_lines_geoserver",
+          "sounding_results_areas_geoserver",
           "isolinesLegendImgDataURL"
         );
       }
@@ -137,7 +137,7 @@
     },
     loadLegendImage(layer, storeTarget) {
       HTTP.get(
-        `/internal/wms?REQUEST=GetLegendGraphic&VERSION=1.0.0&FORMAT=image/png&WIDTH=20&HEIGHT=20&LAYER=${layer}&legend_options=columns:4;fontAntiAliasing:true`,
+        `/internal/wms?REQUEST=GetLegendGraphic&VERSION=1.3.0&FORMAT=image/png&WIDTH=20&HEIGHT=20&LAYER=${layer}&legend_options=columns:4;fontAntiAliasing:true&SCALE=5000`,
         {
           headers: {
             Accept: "image/png",
--- a/client/src/components/map/Map.vue	Fri Oct 04 17:26:56 2019 +0200
+++ b/client/src/components/map/Map.vue	Wed Oct 09 16:40:18 2019 +0200
@@ -201,23 +201,15 @@
     }
   },
   methods: {
-    colorLuminance(hex, lum) {
-      hex = String(hex).replace(/[^0-9a-f]/gi, "");
-      if (hex.length < 6) {
-        hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2];
-      }
-      lum = lum || 0;
-      let opacity = hex.substr(6, 2);
-      var rgb = "#",
-        c,
-        i;
-      for (i = 0; i < 3; i++) {
-        c = parseInt(hex.substr(i * 2, 2), 16);
-        c = Math.round(Math.min(Math.max(0, c + c * lum), 255)).toString(16);
-        rgb += ("00" + c).substr(c.length);
-      }
-
-      return rgb + opacity;
+    colorLuminance(color, lum) {
+      let [r, g, b, a] = color
+        .substring(5, color.length - 1)
+        .split(",")
+        .map(e => Number(e));
+      let [r1, g1, b1] = [r, g, b].map(e =>
+        Math.round(Math.min(Math.max(0, e + e * lum), 255))
+      );
+      return `rgba(${r1},${g1},${b1},${a})`;
     },
     updateBottleneckFilter(bottleneck_id, datestr) {
       if (!bottleneck_id) return;
@@ -327,9 +319,8 @@
           parseInt(color.slice(3, 5), 16) +
           ", " +
           parseInt(color.slice(5, 7), 16) +
-          (color.length > 7
-            ? ", " + parseInt(color.slice(7, 9), 16) / 255
-            : "") +
+          ", " +
+          (color.length > 7 ? parseInt(color.slice(7, 9), 16) / 255 : "1") +
           ")"
         );
       };
--- a/client/src/components/map/layers.js	Fri Oct 04 17:26:56 2019 +0200
+++ b/client/src/components/map/layers.js	Wed Oct 09 16:40:18 2019 +0200
@@ -465,7 +465,7 @@
             projection: "EPSG:3857",
             url: window.location.origin + "/api/internal/wms",
             params: {
-              LAYERS: "sounding_results_contour_lines_geoserver",
+              LAYERS: "sounding_results_areas_geoserver",
               VERSION: "1.1.1",
               TILED: true
             },
@@ -492,7 +492,8 @@
             params: {
               LAYERS: "sounding_differences",
               VERSION: "1.1.1",
-              TILED: true
+              TILED: true,
+              CQL_FILTER: "id=" + store.state.fairwayprofile.currentDifference
             },
             tileLoadFunction: function(tile, src) {
               HTTP.get(src, {
--- a/client/src/store/fairwayprofile.js	Fri Oct 04 17:26:56 2019 +0200
+++ b/client/src/store/fairwayprofile.js	Wed Oct 09 16:40:18 2019 +0200
@@ -36,6 +36,7 @@
     profileLoading: false,
     selectedCut: null,
     differencesLoading: false,
+    currentDifference: null,
     depth: 2.5,
     useCustomDepth: true
   };
@@ -72,6 +73,9 @@
     setDifferencesLoading: (state, value) => {
       state.differencesLoading = value;
     },
+    setCurrentDifference: (state, value) => {
+      state.currentDifference = value;
+    },
     profileLoaded: (state, answer) => {
       const { response, surveyDate } = answer;
       const { data } = response;
--- a/docker/Dockerfile.backend	Fri Oct 04 17:26:56 2019 +0200
+++ b/docker/Dockerfile.backend	Wed Oct 09 16:40:18 2019 +0200
@@ -5,6 +5,7 @@
 
 RUN sed -i 's/\(deb.*\)$/\1 universe/' /etc/apt/sources.list
 
+RUN apt-get update && apt-get install -y software-properties-common
 RUN add-apt-repository ppa:longsleep/golang-backports &&\
     apt-get update &&\
     apt-get -y install --no-install-recommends libxml2-utils\
--- a/docker/Dockerfile.geoserv	Fri Oct 04 17:26:56 2019 +0200
+++ b/docker/Dockerfile.geoserv	Wed Oct 09 16:40:18 2019 +0200
@@ -15,7 +15,7 @@
 
 ENV GS_URL https://downloads.sourceforge.net/project/geoserver/GeoServer
 
-ENV GS_VERSION 2.15.2
+ENV GS_VERSION 2.16.0
 ENV GS_DATADIR /opt/geoserver/data
 
 ENV CATALINA_OPTS="-DGEOSERVER_DATA_DIR=$GS_DATADIR"
--- a/go.mod	Fri Oct 04 17:26:56 2019 +0200
+++ b/go.mod	Wed Oct 09 16:40:18 2019 +0200
@@ -5,6 +5,7 @@
 require (
 	github.com/cockroachdb/apd v1.1.0 // indirect
 	github.com/etcd-io/bbolt v1.3.3
+	github.com/fogleman/contourmap v0.0.0-20190814184649-9f61d36c4199
 	github.com/gofrs/uuid v3.2.0+incompatible // indirect
 	github.com/golang/snappy v0.0.1
 	github.com/gorilla/mux v1.7.3
--- a/go.sum	Fri Oct 04 17:26:56 2019 +0200
+++ b/go.sum	Wed Oct 09 16:40:18 2019 +0200
@@ -2,6 +2,7 @@
 github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
 github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
 github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
+github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af h1:wVe6/Ea46ZMeNkQjjBW6xcqyQA/j5e0D6GytH95g0gQ=
 github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw=
 github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
 github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
@@ -25,6 +26,8 @@
 github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
 github.com/etcd-io/bbolt v1.3.3 h1:gSJmxrs37LgTqR/oyJBWok6k6SvXEUerFTbltIhXkBM=
 github.com/etcd-io/bbolt v1.3.3/go.mod h1:ZF2nL25h33cCyBtcyWeZ2/I3HQOfTP+0PIEvHjkjCrw=
+github.com/fogleman/contourmap v0.0.0-20190814184649-9f61d36c4199 h1:kufr0u0RIG5ACpjFsPRbbuHa0FhMWsS3tnSFZ2hf07s=
+github.com/fogleman/contourmap v0.0.0-20190814184649-9f61d36c4199/go.mod h1:mqaaaP4j7nTF8T/hx5OCljA7BYWHmrH2uh+Q023OchE=
 github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k=
 github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
 github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
@@ -61,8 +64,6 @@
 github.com/jackc/fake v0.0.0-20150926172116-812a484cc733/go.mod h1:WrMFNQdiFJ80sQsxDoMokWK1W5TQtxBFNpzWTD84ibQ=
 github.com/jackc/pgx v3.6.0+incompatible h1:bJeo4JdVbDAW8KB2m8XkFeo8CPipREoG37BwEoKGz+Q=
 github.com/jackc/pgx v3.6.0+incompatible/go.mod h1:0ZGrqGqkRlliWnWB4zKnWtjbSWbGkVEFm4TeybAXq+I=
-github.com/jonas-p/go-shp v0.1.1 h1:LY81nN67DBCz6VNFn2kS64CjmnDo9IP8rmSkTvhO9jE=
-github.com/jonas-p/go-shp v0.1.1/go.mod h1:MRIhyxDQ6VVp0oYeD7yPGr5RSTNScUFKCDsI5DR7PtI=
 github.com/jonas-p/go-shp v0.1.2-0.20190401125246-9fd306ae10a6 h1:h5O7ee4tlSPVjdC75eSLX7jXZiHftthuHio/GtrhaSM=
 github.com/jonas-p/go-shp v0.1.2-0.20190401125246-9fd306ae10a6/go.mod h1:MRIhyxDQ6VVp0oYeD7yPGr5RSTNScUFKCDsI5DR7PtI=
 github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/pkg/common/linear.go	Wed Oct 09 16:40:18 2019 +0200
@@ -0,0 +1,36 @@
+// This is Free Software under GNU Affero General Public License v >= 3.0
+// without warranty, see README.md and license for details.
+//
+// SPDX-License-Identifier: AGPL-3.0-or-later
+// License-Filename: LICENSES/AGPL-3.0.txt
+//
+// Copyright (C) 2019 by via donau
+//   – Österreichische Wasserstraßen-Gesellschaft mbH
+// Software engineering by Intevation GmbH
+//
+// Author(s):
+//  * Sascha L. Teichmann <sascha.teichmann@intevation.de>
+package common
+
+// Linear constructs a function which maps x1 to y1 and x2 to y2.
+// All other values are interpolated linearly.
+func Linear(x1, y1, x2, y2 float64) func(float64) float64 {
+	// f(x1) = y1
+	// f(x2) = y2
+	// y1 = x1*a + b <=> b = y1 - x1*a
+	// y2 = x2*a + b
+
+	// y1 - y2 = a*(x1 - x2)
+	// a = (y1-y2)/(x1 - x2) for x1 != x2
+
+	if x1 == x2 {
+		return func(float64) float64 {
+			return 0.5 * (y1 + y2)
+		}
+	}
+	a := (y1 - y2) / (x1 - x2)
+	b := y1 - x1*a
+	return func(x float64) float64 {
+		return x*a + b
+	}
+}
--- a/pkg/controllers/diff.go	Fri Oct 04 17:26:56 2019 +0200
+++ b/pkg/controllers/diff.go	Wed Oct 09 16:40:18 2019 +0200
@@ -36,6 +36,12 @@
 )
 
 const (
+	// isoCellSize is the side length of a raster cell when tracing
+	// iso areas.
+	isoCellSize = 0.5
+)
+
+const (
 	diffIDSQL = `
 SELECT sd.id FROM
   caching.sounding_differences sd JOIN
@@ -57,24 +63,24 @@
 WHERE m.date_info = $2::date AND s.date_info = $3::date
 RETURNING id
 `
-	insertDiffContourSQL = `
-INSERT INTO caching.sounding_differences_contour_lines (
+	insertDiffIsoAreasQL = `
+INSERT INTO caching.sounding_differences_iso_areas (
   sounding_differences_id,
   height,
-  lines
+  areas
 )
 SELECT
-  $5,
-  $4,
+  $1,
+  $2,
   ST_Transform(
     ST_Multi(
       ST_CollectionExtract(
         ST_SimplifyPreserveTopology(
           ST_Multi(ST_Collectionextract(
-            ST_MakeValid(ST_GeomFromWKB($1, $2::integer)), 2)),
-          $3
+            ST_MakeValid(ST_GeomFromWKB($4, $3::integer)), 3)),
+          $5
         ),
-        2
+        3
       )
     ),
     4326
@@ -273,11 +279,11 @@
 
 	log.Printf("info: num heights: %d\n", len(heights))
 
-	var stmt *sql.Stmt
-	if stmt, err = tx.PrepareContext(ctx, insertDiffContourSQL); err != nil {
+	var isoStmt *sql.Stmt
+	if isoStmt, err = tx.PrepareContext(ctx, insertDiffIsoAreasQL); err != nil {
 		return
 	}
-	defer stmt.Close()
+	defer isoStmt.Close()
 
 	if err = tx.QueryRowContext(
 		ctx,
@@ -291,18 +297,21 @@
 
 	heights = common.DedupFloat64s(heights)
 
-	octree.DoContours(tree, heights, func(res *octree.ContourResult) {
-		if err == nil && len(res.Lines) > 0 {
-			_, err = stmt.ExecContext(
-				ctx,
-				res.Lines.AsWKB2D(),
-				minuendTree.EPSG,
-				contourTolerance,
-				res.Height,
-				id,
-			)
+	areas := tree.TraceAreas(heights, isoCellSize)
+
+	for i, a := range areas {
+		if len(a) == 0 {
+			continue
 		}
-	})
+		if _, err = isoStmt.ExecContext(
+			ctx,
+			id, heights[i], minuendTree.EPSG,
+			a.AsWKB(),
+			contourTolerance,
+		); err != nil {
+			return
+		}
+	}
 
 	log.Printf("info: calculating and storing iso lines took %v\n",
 		time.Since(start))
--- a/pkg/controllers/stretches.go	Fri Oct 04 17:26:56 2019 +0200
+++ b/pkg/controllers/stretches.go	Wed Oct 09 16:40:18 2019 +0200
@@ -31,14 +31,21 @@
 	"gemma.intevation.de/gemma/pkg/middleware"
 )
 
+// The following requests are taking _all_ bottlenecks into account, not only
+// the currently valid ones.  This is neccessary, as we are doing reports on
+// arbitrary time ranges and bottlenecks currently active might have been in the
+// selected time range.
+//
+// FIXME: the better solution would be to limit the bottlenecks to those with:
+//   b.validity && REQUESTED_TIME_RANGE
+
 const (
 	selectSectionBottlenecks = `
 SELECT
   distinct(b.objnam),
   b.limiting
 FROM waterway.sections s, waterway.bottlenecks b
-WHERE b.validity @> current_timestamp
-  AND ST_Intersects(b.area, s.area)
+WHERE ST_Intersects(b.area, s.area)
   AND s.name = $1`
 
 	selectStretchBottlenecks = `
@@ -46,8 +53,7 @@
   distinct(b.objnam),
   b.limiting
 FROM users.stretches s, waterway.bottlenecks b
-WHERE b.validity @> current_timestamp
-  AND ST_Intersects(b.area, s.area)
+WHERE ST_Intersects(b.area, s.area)
   AND s.name = $1`
 )
 
--- a/pkg/controllers/system.go	Fri Oct 04 17:26:56 2019 +0200
+++ b/pkg/controllers/system.go	Wed Oct 09 16:40:18 2019 +0200
@@ -239,7 +239,7 @@
 		func(old sql.NullString, curr string) (func(*http.Request), error) {
 			return reconfigureClassBreaks(
 				old, curr,
-				"sounding_results_contour_lines_geoserver",
+				"sounding_results_areas_geoserver",
 				func(req *http.Request) {
 					if s, ok := auth.GetSession(req); ok {
 						triggerSoundingResultsContoursRecalc(s.User, curr)
--- a/pkg/geoserver/templates.go	Fri Oct 04 17:26:56 2019 +0200
+++ b/pkg/geoserver/templates.go	Wed Oct 09 16:40:18 2019 +0200
@@ -35,7 +35,7 @@
 
 func init() {
 	RegisterStylePreprocessor(
-		"sounding_results_contour_lines_geoserver",
+		"sounding_results_areas_geoserver",
 		templateContourLinesFunc("morphology_classbreaks"))
 	RegisterStylePreprocessor(
 		"sounding_differences",
--- a/pkg/imports/fa.go	Fri Oct 04 17:26:56 2019 +0200
+++ b/pkg/imports/fa.go	Wed Oct 09 16:40:18 2019 +0200
@@ -292,6 +292,14 @@
 	return &summary, nil
 }
 
+// defaultLOS defaults to LOS3 when no expicit LOS is given.
+func defaultLOS(los *ifaf.LosEnum) ifaf.LosEnum {
+	if los == nil {
+		return ifaf.LosEnumLOS3
+	}
+	return *los
+}
+
 func doForFAs(
 	ctx context.Context,
 	bnIds bottlenecks,
@@ -371,7 +379,6 @@
 		if faRes.Effective_fairway_availability != nil {
 			efaCount := 0
 			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{
@@ -382,7 +389,7 @@
 					ctx,
 					faID,
 					efa.Measure_date,
-					string(*los),
+					string(defaultLOS(efa.Level_of_Service)),
 					efa.Available_depth_value,
 					efa.Available_width_value,
 					efa.Water_level_value,
@@ -410,7 +417,7 @@
 				res, err := insertFAVStmt.ExecContext(
 					ctx,
 					faID,
-					fav.Level_of_Service,
+					string(defaultLOS(fav.Level_of_Service)),
 					fav.Fairway_depth,
 					fav.Fairway_width,
 					fav.Fairway_radius,
--- a/pkg/imports/isr.go	Fri Oct 04 17:26:56 2019 +0200
+++ b/pkg/imports/isr.go	Wed Oct 09 16:40:18 2019 +0200
@@ -44,10 +44,7 @@
 }
 
 func (isrJobCreator) Depends() [2][]string {
-	return [2][]string{
-		{"sounding_results", "sounding_results_contour_lines"},
-		{},
-	}
+	return srJobCreator{}.Depends()
 }
 
 const (
@@ -56,8 +53,8 @@
 FROM waterway.sounding_results
 ORDER BY bottleneck_id
 `
-	deleteContourLinesSQL = `
-DELETE FROM waterway.sounding_results_contour_lines
+	deleteIsoAreasSQL = `
+DELETE FROM waterway.sounding_results_iso_areas
 WHERE sounding_result_id = $1
 `
 )
@@ -166,10 +163,11 @@
 	}
 	defer tx.Rollback()
 
-	insertStmt, err := tx.Prepare(insertContourSQL)
+	insertAreasStmt, err := tx.Prepare(insertIsoAreasSQL)
 	if err != nil {
 		return err
 	}
+	defer insertAreasStmt.Close()
 
 	// For all sounding results in bottleneck.
 	for _, sr := range bn.srs {
@@ -180,22 +178,25 @@
 		hs := octree.ExtrapolateClassBreaks(heights, tree.Min.Z, tree.Max.Z)
 		hs = common.DedupFloat64s(hs)
 
-		// Delete the old contour lines.
-		if _, err := tx.ExecContext(ctx, deleteContourLinesSQL, sr); err != nil {
+		// Delete the old iso areas.
+		if _, err := tx.ExecContext(ctx, deleteIsoAreasSQL, sr); err != nil {
 			return err
 		}
 
-		octree.DoContours(tree, hs, func(res *octree.ContourResult) {
-			if err == nil && len(res.Lines) > 0 {
-				_, err = insertStmt.ExecContext(
-					ctx,
-					sr, res.Height, tree.EPSG,
-					res.Lines.AsWKB2D(),
-					contourTolerance)
+		// Calculate and store the iso areas.
+		areas := tree.TraceAreas(hs, isoCellSize)
+		for i, a := range areas {
+			if len(a) == 0 {
+				continue
 			}
-		})
-		if err != nil {
-			return err
+			if _, err := insertAreasStmt.ExecContext(
+				ctx,
+				sr, hs[i], tree.EPSG,
+				a.AsWKB(),
+				contourTolerance,
+			); err != nil {
+				return err
+			}
 		}
 	}
 
--- a/pkg/imports/sr.go	Fri Oct 04 17:26:56 2019 +0200
+++ b/pkg/imports/sr.go	Wed Oct 09 16:40:18 2019 +0200
@@ -37,6 +37,7 @@
 	"gemma.intevation.de/gemma/pkg/common"
 	"gemma.intevation.de/gemma/pkg/models"
 	"gemma.intevation.de/gemma/pkg/octree"
+	"gemma.intevation.de/gemma/pkg/wkb"
 )
 
 // SoundingResult is a Job to import sounding reults
@@ -70,9 +71,17 @@
 )
 
 const (
+	// pointsPerSquareMeter is the average number of points
+	// when generating a artifical height model for single beam scans.
 	pointsPerSquareMeter = 2
 )
 
+const (
+	// isoCellSize is the side length of a raster cell when tracing
+	// iso areas.
+	isoCellSize = 0.5
+)
+
 // SRJobKind is the unique name of this import job type.
 const SRJobKind JobKind = "sr"
 
@@ -88,7 +97,7 @@
 
 func (srJobCreator) Depends() [2][]string {
 	return [2][]string{
-		{"sounding_results", "sounding_results_contour_lines"},
+		{"sounding_results", "sounding_results_iso_areas"},
 		{"bottlenecks"},
 	}
 }
@@ -152,11 +161,11 @@
   ST_AsBinary(ST_Buffer(ST_MakeValid(ST_GeomFromWKB($1, $2::integer)), 0.0)),
   ST_AsBinary(ST_Buffer(ST_MakeValid(ST_GeomFromWKB($1, $2::integer)), 0.1))`
 
-	insertContourSQL = `
-INSERT INTO waterway.sounding_results_contour_lines (
+	insertIsoAreasSQL = `
+INSERT INTO waterway.sounding_results_iso_areas (
   sounding_result_id,
   height,
-  lines
+  areas
 )
 SELECT
   $1,
@@ -166,10 +175,10 @@
       ST_CollectionExtract(
         ST_SimplifyPreserveTopology(
           ST_Multi(ST_Collectionextract(
-            ST_MakeValid(ST_GeomFromWKB($4, $3::integer)), 2)),
+            ST_MakeValid(ST_GeomFromWKB($4, $3::integer)), 3)),
           $5
         ),
-        2
+        3
       )
     ),
     4326
@@ -619,15 +628,10 @@
 		return nil, err
 	}
 	feedback.Info("Storing octree index took %s.", time.Since(start))
-	feedback.Info("Generate contour lines")
-
-	start = time.Now()
-	err = generateContours(ctx, tx, feedback, builder.Tree(), id)
+	err = generateIsos(ctx, tx, feedback, builder.Tree(), id)
 	if err != nil {
 		return nil, err
 	}
-	feedback.Info("Generating contour lines took %s.",
-		time.Since(start))
 
 	// Store for potential later removal.
 	if err = track(ctx, tx, importID, "waterway.sounding_results", id); err != nil {
@@ -827,23 +831,19 @@
 	return shapeToPolygon(s)
 }
 
-func generateContours(
+func generateIsos(
 	ctx context.Context,
 	tx *sql.Tx,
 	feedback Feedback,
 	tree *octree.Tree,
 	id int64,
 ) error {
-	stmt, err := tx.PrepareContext(ctx, insertContourSQL)
-	if err != nil {
-		return err
-	}
-	defer stmt.Close()
 
 	heights, err := octree.LoadClassBreaks(
 		ctx, tx,
 		"morphology_classbreaks",
 	)
+
 	if err != nil {
 		feedback.Warn("Loading class breaks failed: %v", err)
 		feedback.Info("Using default class breaks")
@@ -871,15 +871,66 @@
 
 	heights = common.DedupFloat64s(heights)
 
-	octree.DoContours(tree, heights, func(res *octree.ContourResult) {
-		if err == nil && len(res.Lines) > 0 {
-			_, err = stmt.ExecContext(
-				ctx,
-				id, res.Height, tree.EPSG,
-				res.Lines.AsWKB2D(),
-				contourTolerance)
+	return generateIsoAreas(ctx, tx, feedback, tree, heights, id)
+}
+
+func generateIsoAreas(
+	ctx context.Context,
+	tx *sql.Tx,
+	feedback Feedback,
+	tree *octree.Tree,
+	heights []float64,
+	id int64,
+) error {
+	feedback.Info("Generate iso areas")
+	total := time.Now()
+	defer func() {
+		feedback.Info("Generating iso areas took %s.",
+			time.Since(total))
+	}()
+
+	areas := tree.TraceAreas(heights, isoCellSize)
+
+	return storeAreas(
+		ctx, tx, feedback,
+		areas, tree.EPSG, heights, id)
+}
+
+func storeAreas(
+	ctx context.Context,
+	tx *sql.Tx,
+	feedback Feedback,
+	areas []wkb.MultiPolygonGeom,
+	epsg uint32,
+	heights []float64,
+	id int64,
+) error {
+	feedback.Info("Store iso areas.")
+	total := time.Now()
+	defer func() {
+		feedback.Info("Storing iso areas took %v.",
+			time.Since(total))
+	}()
+
+	stmt, err := tx.PrepareContext(ctx, insertIsoAreasSQL)
+	if err != nil {
+		return err
+	}
+	defer stmt.Close()
+
+	for i, a := range areas {
+		if len(a) == 0 {
+			continue
 		}
-	})
+		if _, err := stmt.ExecContext(
+			ctx,
+			id, heights[i], epsg,
+			a.AsWKB(),
+			contourTolerance,
+		); err != nil {
+			return err
+		}
+	}
 
-	return err
+	return nil
 }
--- a/pkg/imports/st.go	Fri Oct 04 17:26:56 2019 +0200
+++ b/pkg/imports/st.go	Wed Oct 09 16:40:18 2019 +0200
@@ -58,10 +58,10 @@
 const (
 	stDeleteSQL = `
 DELETE FROM users.stretches WHERE
-staging_done AND name = (
+staging_done AND name IN (
   SELECT name
   FROM users.stretches WHERE
-  id = (
+  id IN (
     SELECT key from import.track_imports
     WHERE import_id = $1 AND
       relation = 'users.stretches'::regclass)
--- a/pkg/models/colors.go	Fri Oct 04 17:26:56 2019 +0200
+++ b/pkg/models/colors.go	Wed Oct 09 16:40:18 2019 +0200
@@ -71,6 +71,28 @@
 	return cbs
 }
 
+func (cc ColorValues) Heights() []float64 {
+	heights := make([]float64, len(cc))
+	for i := range cc {
+		heights[i] = cc[i].Value
+	}
+	return heights
+}
+
+func (cc ColorValues) Clip(v float64) color.RGBA {
+	if len(cc) == 0 {
+		return color.RGBA{}
+	}
+	if v < cc[0].Value {
+		return cc[0].Color
+	}
+	if v > cc[len(cc)-1].Value {
+		return cc[len(cc)-1].Color
+	}
+	c, _ := cc.Interpolate(v)
+	return c
+}
+
 func (cc ColorValues) Interpolate(v float64) (color.RGBA, bool) {
 	if len(cc) == 0 || v < cc[0].Value || v > cc[len(cc)-1].Value {
 		return color.RGBA{}, false
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/pkg/octree/areas.go	Wed Oct 09 16:40:18 2019 +0200
@@ -0,0 +1,168 @@
+// 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):
+//  * Sascha L. Teichmann <sascha.teichmann@intevation.de>
+
+package octree
+
+import (
+	"log"
+	"math"
+	"runtime"
+	"sync"
+	"time"
+
+	"github.com/fogleman/contourmap"
+
+	"gemma.intevation.de/gemma/pkg/common"
+	"gemma.intevation.de/gemma/pkg/wkb"
+)
+
+func (tree *Tree) TraceAreas(
+	heights []float64,
+	cellSize float64,
+) []wkb.MultiPolygonGeom {
+	min, max := tree.Min, tree.Max
+
+	width := max.X - min.X
+	height := max.Y - min.Y
+
+	log.Printf("info: Width/Height: %.2f / %.2f\n", width, height)
+
+	xcells := int(math.Ceil(width / cellSize))
+	ycells := int(math.Ceil(height / cellSize))
+
+	log.Printf("info: Raster size: (%d, %d)\n", xcells, ycells)
+
+	start := time.Now()
+
+	// Add border for closing
+	raster := make([]float64, (xcells+2)*(ycells+2))
+
+	// prefill for no data
+	const nodata = -math.MaxFloat64
+	for i := range raster {
+		raster[i] = nodata
+	}
+
+	// rasterize the height model
+
+	var wg sync.WaitGroup
+
+	rows := make(chan int)
+
+	rasterRow := func() {
+		defer wg.Done()
+		quat := 0.25 * cellSize
+		for i := range rows {
+			pos := (i+1)*(xcells+2) + 1
+			row := raster[pos : pos+xcells]
+			py := min.Y + float64(i)*cellSize + cellSize/2
+			px := min.X + cellSize/2
+			y1 := py - quat
+			y2 := py + quat
+			for j := range row {
+				var n int
+				var sum float64
+
+				if v, ok := tree.Value(px-quat, y1); ok {
+					sum = v
+					n = 1
+				}
+				if v, ok := tree.Value(px-quat, y2); ok {
+					sum += v
+					n++
+				}
+				if v, ok := tree.Value(px+quat, y1); ok {
+					sum += v
+					n++
+				}
+				if v, ok := tree.Value(px+quat, y2); ok {
+					sum += v
+					n++
+				}
+
+				if n > 0 {
+					row[j] = sum / float64(n)
+				}
+				px += cellSize
+			}
+		}
+	}
+
+	for n := runtime.NumCPU(); n >= 1; n-- {
+		wg.Add(1)
+		go rasterRow()
+	}
+
+	for i := 0; i < ycells; i++ {
+		rows <- i
+	}
+	close(rows)
+
+	wg.Wait()
+	log.Printf("info: Rastering took %v\n", time.Since(start))
+
+	start = time.Now()
+
+	tracer := contourmap.FromFloat64s(xcells+2, ycells+2, raster)
+
+	areas := make([]wkb.MultiPolygonGeom, len(heights))
+
+	// TODO: Check if this correct!
+	reprojX := common.Linear(0.5, min.X, 1.5, min.X+cellSize)
+	reprojY := common.Linear(0.5, min.Y, 1.5, min.Y+cellSize)
+
+	cnts := make(chan int)
+
+	doContours := func() {
+		defer wg.Done()
+		for hIdx := range cnts {
+			c := tracer.Contours(heights[hIdx])
+
+			if len(c) == 0 {
+				continue
+			}
+
+			// We need to bring it back to the
+			// none raster coordinate system.
+			a := make(wkb.MultiPolygonGeom, len(c))
+
+			for i, pl := range c {
+				shell := make(wkb.LinearRingGeom, len(pl))
+				for j, pt := range pl {
+					shell[j] = wkb.PointGeom{
+						X: reprojX(pt.X),
+						Y: reprojY(pt.Y),
+					}
+				}
+				a[i] = wkb.PolygonGeom{shell}
+			}
+
+			areas[hIdx] = a
+		}
+	}
+
+	for n := runtime.NumCPU(); n >= 1; n-- {
+		wg.Add(1)
+		go doContours()
+	}
+
+	for i := range heights {
+		cnts <- i
+	}
+	close(cnts)
+
+	wg.Wait()
+	log.Printf("info: Tracing areas took %v\n", time.Since(start))
+
+	return areas
+}
--- a/pkg/octree/strtree.go	Fri Oct 04 17:26:56 2019 +0200
+++ b/pkg/octree/strtree.go	Wed Oct 09 16:40:18 2019 +0200
@@ -18,16 +18,21 @@
 	"sort"
 )
 
-const numEntries = 8
+const STRTreeDefaultEntries = 8
 
 type STRTree struct {
-	tin    *Tin
-	index  []int32
-	bboxes []Box2D
+	Entries int
+	tin     *Tin
+	index   []int32
+	bboxes  []Box2D
 }
 
 func (s *STRTree) Build(t *Tin) {
 
+	if s.Entries == 0 {
+		s.Entries = STRTreeDefaultEntries
+	}
+
 	s.tin = t
 
 	all := make([]int32, len(t.Triangles))
@@ -43,6 +48,31 @@
 	s.index[0] = root
 }
 
+func (s *STRTree) BuildWithout(t *Tin, remove map[int32]struct{}) {
+
+	if s.Entries == 0 {
+		s.Entries = STRTreeDefaultEntries
+	}
+
+	s.tin = t
+
+	all := make([]int32, 0, len(t.Triangles)-len(remove))
+
+	for i := range all {
+		idx := int32(i)
+		if _, found := remove[idx]; !found {
+			all = append(all, idx)
+		}
+		all[i] = int32(i)
+	}
+
+	s.index = append(s.index, 0)
+
+	root := s.build(all)
+
+	s.index[0] = root
+}
+
 func (s *STRTree) Clip(p *Polygon) map[int32]struct{} {
 
 	removed := make(map[int32]struct{})
@@ -113,12 +143,12 @@
 		return s.tin.Vertices[ti[0]].X < s.tin.Vertices[tj[0]].X
 	})
 
-	P := int(math.Ceil(float64(len(items)) / float64(numEntries)))
+	P := int(math.Ceil(float64(len(items)) / float64(s.Entries)))
 	S := int(math.Ceil(math.Sqrt(float64(P))))
 
-	slices := strSplit(items, S)
+	slices := s.strSplit(items, S)
 
-	leaves := strJoin(
+	leaves := s.strJoin(
 		slices, S,
 		func(i, j int32) bool {
 			ti := s.tin.Triangles[i]
@@ -133,7 +163,7 @@
 
 func (s *STRTree) buildNodes(items []int32) int32 {
 
-	if len(items) <= numEntries {
+	if len(items) <= s.Entries {
 		return s.allocNode(items)
 	}
 
@@ -141,12 +171,12 @@
 		return s.bbox(items[i]).X1 < s.bbox(items[j]).X1
 	})
 
-	P := int(math.Ceil(float64(len(items)) / float64(numEntries)))
+	P := int(math.Ceil(float64(len(items)) / float64(s.Entries)))
 	S := int(math.Ceil(math.Sqrt(float64(P))))
 
-	slices := strSplit(items, S)
+	slices := s.strSplit(items, S)
 
-	nodes := strJoin(
+	nodes := s.strJoin(
 		slices, S,
 		func(i, j int32) bool { return s.bbox(i).Y1 < s.bbox(j).Y1 },
 		s.allocNode,
@@ -162,8 +192,8 @@
 	return s.bboxes[s.index[idx]]
 }
 
-func strSplit(items []int32, S int) [][]int32 {
-	sm := S * numEntries
+func (s *STRTree) strSplit(items []int32, S int) [][]int32 {
+	sm := S * s.Entries
 	slices := make([][]int32, S)
 	for i := range slices {
 		var n int
@@ -178,7 +208,7 @@
 	return slices
 }
 
-func strJoin(
+func (s *STRTree) strJoin(
 	slices [][]int32, S int,
 	less func(int32, int32) bool,
 	alloc func([]int32) int32,
@@ -192,8 +222,8 @@
 
 		for len(slice) > 0 {
 			var n int
-			if l := len(slice); l >= numEntries {
-				n = numEntries
+			if l := len(slice); l >= s.Entries {
+				n = s.Entries
 			} else {
 				n = l
 			}
--- a/pkg/octree/vertex.go	Fri Oct 04 17:26:56 2019 +0200
+++ b/pkg/octree/vertex.go	Wed Oct 09 16:40:18 2019 +0200
@@ -350,9 +350,9 @@
 }
 
 func (t *Triangle) Contains(x, y float64) bool {
-	v0 := t[2].Sub(t[0])
-	v1 := t[1].Sub(t[0])
-	v2 := Vertex{X: x, Y: y}.Sub(t[0])
+	v0 := t[2].Sub2D(t[0])
+	v1 := t[1].Sub2D(t[0])
+	v2 := Vertex{X: x, Y: y}.Sub2D(t[0])
 
 	dot00 := v0.Dot2(v0)
 	dot01 := v0.Dot2(v1)
@@ -483,6 +483,14 @@
 		math.Abs(v.Y-w.Y) < eps && math.Abs(v.Z-w.Z) < eps
 }
 
+// EpsEquals returns true if v and w are equal component-wise
+// in the X/Y plane with the values within a epsilon range.
+func (v Vertex) EpsEquals2D(w Vertex) bool {
+	const eps = 1e-5
+	return math.Abs(v.X-w.X) < eps &&
+		math.Abs(v.Y-w.Y) < eps
+}
+
 // JoinOnLine joins the the elements of a given multi line string
 // under the assumption that the segments are all on the line segment
 // from (x1, y1) to (x2, y2).
@@ -629,6 +637,15 @@
 	return buf.Bytes()
 }
 
+func (ls LineStringZ) CCW() bool {
+	var sum float64
+	for i, v1 := range ls {
+		v2 := ls[(i+1)%len(ls)]
+		sum += (v2.X - v1.X) * (v2.Y + v1.Y)
+	}
+	return sum > 0
+}
+
 // Join joins two lines leaving the first of the second out.
 func (ls LineStringZ) Join(other LineStringZ) LineStringZ {
 	nline := make(LineStringZ, len(ls)+len(other)-1)
--- a/schema/default_sysconfig.sql	Fri Oct 04 17:26:56 2019 +0200
+++ b/schema/default_sysconfig.sql	Wed Oct 09 16:40:18 2019 +0200
@@ -184,13 +184,13 @@
            (location_code).hectometre
         FROM waterway.distance_marks_virtual
     $$),
-    ('waterway', 'sounding_results_contour_lines_geoserver', 4326, $$
+    ('waterway', 'sounding_results_areas_geoserver', 4326, $$
         SELECT bottleneck_id,
             date_info,
             height,
-            lines
-        FROM waterway.sounding_results_contour_lines cl
-            JOIN waterway.sounding_results sr ON sr.id = cl.sounding_result_id
+            areas
+        FROM waterway.sounding_results_iso_areas ia
+            JOIN waterway.sounding_results sr ON sr.id = ia.sounding_result_id
     $$),
     ('waterway', 'bottlenecks_geoserver', 4326, $$
         SELECT
@@ -257,11 +257,11 @@
             bn.objnam       AS objnam,
             srm.date_info   AS minuend,
             srs.date_info   AS subtrahend,
-            sdcl.height     AS height,
-            sdcl.lines      AS lines
+            sdia.height     AS height,
+            sdia.areas      AS areas
         FROM caching.sounding_differences sd
-            JOIN caching.sounding_differences_contour_lines sdcl
-                ON sd.id = sdcl.sounding_differences_id
+            JOIN caching.sounding_differences_iso_areas sdia
+                ON sd.id = sdia.sounding_differences_id
             JOIN waterway.sounding_results srm
                 ON sd.minuend = srm.id
             JOIN waterway.sounding_results srs
--- a/schema/gemma.sql	Fri Oct 04 17:26:56 2019 +0200
+++ b/schema/gemma.sql	Wed Oct 09 16:40:18 2019 +0200
@@ -720,13 +720,13 @@
         AFTER INSERT OR UPDATE ON sounding_results
         FOR EACH ROW EXECUTE FUNCTION check_sr_in_bn_area()
 
-    CREATE TABLE sounding_results_contour_lines (
+    CREATE TABLE sounding_results_iso_areas (
         sounding_result_id int NOT NULL REFERENCES sounding_results
             ON DELETE CASCADE,
         height numeric NOT NULL,
-        lines geography(multilinestring, 4326) NOT NULL,
+        areas geography(MULTIPOLYGON, 4326) NOT NULL,
         -- TODO: generate valid simple features and add constraint:
-            -- CHECK(ST_IsSimple(CAST(lines AS geometry))),
+            -- CHECK(ST_IsSimple(CAST(areas AS geometry))),
         PRIMARY KEY (sounding_result_id, height)
     )
     --
@@ -914,11 +914,11 @@
         UNIQUE (minuend, subtrahend)
     )
 
-    CREATE TABLE sounding_differences_contour_lines (
+    CREATE TABLE sounding_differences_iso_areas (
         sounding_differences_id int NOT NULL REFERENCES sounding_differences(id)
                                     ON DELETE CASCADE,
         height numeric NOT NULL,
-        lines  geography(multilinestring, 4326) NOT NULL,
+        areas  geography(MULTIPOLYGON, 4326) NOT NULL,
         PRIMARY KEY (sounding_differences_id, height)
     )
 ;
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/schema/updates/1204/01.create-iso-areas.sql	Wed Oct 09 16:40:18 2019 +0200
@@ -0,0 +1,27 @@
+CREATE TABLE waterway.sounding_results_iso_areas (
+    sounding_result_id int NOT NULL REFERENCES waterway.sounding_results
+        ON DELETE CASCADE,
+    height numeric NOT NULL,
+    areas geography(MULTIPOLYGON, 4326) NOT NULL,
+    -- TODO: generate valid simple features and add constraint:
+        -- CHECK(ST_IsSimple(CAST(areas AS geometry))),
+    PRIMARY KEY (sounding_result_id, height)
+);
+
+CREATE TABLE caching.sounding_differences_iso_areas (
+    sounding_differences_id int NOT NULL REFERENCES caching.sounding_differences(id)
+                                ON DELETE CASCADE,
+    height numeric NOT NULL,
+    areas  geography(MULTIPOLYGON, 4326) NOT NULL,
+    PRIMARY KEY (sounding_differences_id, height)
+);
+
+GRANT INSERT, UPDATE, DELETE ON waterway.sounding_results_iso_areas
+    TO waterway_admin;
+
+GRANT SELECT ON waterway.sounding_results_iso_areas
+    TO waterway_user;
+
+GRANT SELECT, UPDATE, DELETE, INSERT ON caching.sounding_differences_iso_areas
+    TO waterway_user;
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/schema/updates/1204/02.delete-contours.sql	Wed Oct 09 16:40:18 2019 +0200
@@ -0,0 +1,8 @@
+DELETE FROM sys_admin.published_services WHERE name = 'waterway.sounding_differences'::regclass;
+DELETE FROM sys_admin.published_services WHERE name = 'waterway.sounding_results_contour_lines_geoserver'::regclass;
+
+DROP VIEW waterway.sounding_results_contour_lines_geoserver;
+DROP VIEW waterway.sounding_differences;
+DROP TABLE caching.sounding_differences_contour_lines;
+DROP TABLE waterway.sounding_results_contour_lines;
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/schema/updates/1204/03.geoserver-views.sql	Wed Oct 09 16:40:18 2019 +0200
@@ -0,0 +1,28 @@
+CREATE OR REPLACE VIEW waterway.sounding_results_areas_geoserver AS
+  SELECT bottleneck_id,
+         date_info,
+         height,
+         CAST(areas AS geometry(multipolygon, 4326)) as areas
+  FROM waterway.sounding_results_iso_areas ia
+  JOIN waterway.sounding_results sr ON sr.id = ia.sounding_result_id;
+
+CREATE OR REPLACE VIEW waterway.sounding_differences AS
+    SELECT
+        sd.id           AS id,
+        bn.objnam       AS objnam,
+        srm.date_info   AS minuend,
+        srs.date_info   AS subtrahend,
+        sdia.height     AS height,
+        CAST(sdia.areas AS geometry(multipolygon, 4326)) AS areas
+    FROM caching.sounding_differences sd
+        JOIN caching.sounding_differences_iso_areas sdia
+            ON sd.id = sdia.sounding_differences_id
+        JOIN waterway.sounding_results srm
+            ON sd.minuend = srm.id
+        JOIN waterway.sounding_results srs
+            ON sd.subtrahend = srs.id
+        JOIN waterway.bottlenecks bn
+            ON srm.bottleneck_id = bn.bottleneck_id
+                AND srm.date_info::timestamptz <@ bn.validity;
+
+GRANT SELECT ON ALL TABLES IN SCHEMA public, users, waterway TO waterway_user;
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/schema/updates/1204/04.publish.sql	Wed Oct 09 16:40:18 2019 +0200
@@ -0,0 +1,6 @@
+INSERT INTO sys_admin.published_services (name)
+   VALUES ('waterway.sounding_results_areas_geoserver'::regclass)
+   ON CONFLICT (name) DO NOTHING;
+INSERT INTO sys_admin.published_services (name)
+   VALUES ('waterway.sounding_differences'::regclass)
+   ON CONFLICT (name) DO NOTHING;
--- a/schema/updates/1300/02.views_to_geoservers.sql	Fri Oct 04 17:26:56 2019 +0200
+++ b/schema/updates/1300/02.views_to_geoservers.sql	Wed Oct 09 16:40:18 2019 +0200
@@ -154,13 +154,13 @@
            (location_code).hectometre
         FROM waterway.distance_marks_virtual
     $$),
-    ('waterway', 'sounding_results_contour_lines_geoserver', 4326, $$
+    ('waterway', 'sounding_results_areas_geoserver', 4326, $$
         SELECT bottleneck_id,
             date_info,
             height,
-            lines
-        FROM waterway.sounding_results_contour_lines cl
-            JOIN waterway.sounding_results sr ON sr.id = cl.sounding_result_id
+            areas
+        FROM waterway.sounding_results_iso_areas ia
+            JOIN waterway.sounding_results sr ON sr.id = ia.sounding_result_id
     $$),
     ('waterway', 'bottlenecks_geoserver', 4326, $$
         SELECT
@@ -227,11 +227,11 @@
             bn.objnam       AS objnam,
             srm.date_info   AS minuend,
             srs.date_info   AS subtrahend,
-            sdcl.height     AS height,
-            sdcl.lines      AS lines
+            sdia.height     AS height,
+            sdia.areas      AS areas
         FROM caching.sounding_differences sd
-            JOIN caching.sounding_differences_contour_lines sdcl
-                ON sd.id = sdcl.sounding_differences_id
+            JOIN caching.sounding_differences_iso_areas sdia
+                ON sd.id = sdia.sounding_differences_id
             JOIN waterway.sounding_results srm
                 ON sd.minuend = srm.id
             JOIN waterway.sounding_results srs
@@ -252,6 +252,6 @@
     waterway.bottlenecks_geoserver,
     waterway.stretches_geoserver,
     waterway.sections_geoserver,
-    waterway.sounding_results_contour_lines_geoserver,
     waterway.bottleneck_overview,
+    waterway.sounding_results_areas_geoserver,
     waterway.sounding_differences;
--- a/style-templates/sounding_differences.sld-template	Fri Oct 04 17:26:56 2019 +0200
+++ b/style-templates/sounding_differences.sld-template	Wed Oct 09 16:40:18 2019 +0200
@@ -50,74 +50,69 @@
             </ogc:And>
           </ogc:Filter>
         {{- end }}
-           <se:LineSymbolizer>
+        <se:MaxScaleDenominator>34e3</se:MaxScaleDenominator>
+          <se:PolygonSymbolizer>
+            <se:Fill>
+              <se:SvgParameter name="fill">{{ .Color }}</se:SvgParameter>
+            </se:Fill>
             <se:Stroke>
-              <se:SvgParameter name="stroke">{{ .Color }}</se:SvgParameter>
+              <se:SvgParameter name="stroke">#404040</se:SvgParameter>
               <se:SvgParameter name="stroke-width">0.5</se:SvgParameter>
             </se:Stroke>
-          </se:LineSymbolizer>
+          </se:PolygonSymbolizer>
+        </se:Rule>
+        <se:Rule>
+        {{- if not .HasLow }}
+          <se:Name>&#8804; {{ printf "%g" .High }}</se:Name>
+          <ogc:Filter>
+            <ogc:PropertyIsLessThanOrEqualTo>
+              <ogc:PropertyName>height</ogc:PropertyName>
+              <ogc:Literal>{{ printf "%f" .High }}</ogc:Literal>
+            </ogc:PropertyIsLessThanOrEqualTo>
+          </ogc:Filter>
+        {{- else if not .HasHigh }}
+          <se:Name>&gt; {{ printf "%g" .Low }}</se:Name>
+          <ogc:Filter>
+            <ogc:PropertyIsGreaterThanOrEqualTo>
+              <ogc:PropertyName>height</ogc:PropertyName>
+              <ogc:Literal>{{ printf "%f" .Low }}</ogc:Literal>
+            </ogc:PropertyIsGreaterThanOrEqualTo>
+          </ogc:Filter>
+        {{- else }}
+          <se:Name>&#8804; {{ printf "%g" .High }}</se:Name>
+          <ogc:Filter>
+            <ogc:And>
+              <ogc:PropertyIsGreaterThan>
+                <ogc:PropertyName>height</ogc:PropertyName>
+                <ogc:Literal>{{ printf "%f" .Low }}</ogc:Literal>
+              </ogc:PropertyIsGreaterThan>
+              <ogc:PropertyIsLessThanOrEqualTo>
+                <ogc:PropertyName>height</ogc:PropertyName>
+                <ogc:Literal>{{ printf "%f" .High }}</ogc:Literal>
+              </ogc:PropertyIsLessThanOrEqualTo>
+            </ogc:And>
+          </ogc:Filter>
+        {{- end }}
+        <se:MinScaleDenominator>34e3</se:MinScaleDenominator>
+          <se:PolygonSymbolizer>
+            <se:Fill>
+              <se:SvgParameter name="fill">{{ .Color }}</se:SvgParameter>
+            </se:Fill>
+          </se:PolygonSymbolizer>
         </se:Rule>
         {{ end }}
       </se:FeatureTypeStyle>
       <se:FeatureTypeStyle>
-        <se:Name>contour_lines_emph</se:Name>
-        <se:Description>
-          <se:Abstract>
-            FeatureTypeStyle for emphasized contour lines
-          </se:Abstract>
-          </se:Description>
-          <se:Rule>
-            <se:LegendGraphic>
-              <se:Graphic>
-            </se:Graphic>
-          </se:LegendGraphic>
-          <ogc:Filter>
-             <ogc:Or>
-              {{ range . -}}
-              {{ if .HasHigh -}}
-                <ogc:PropertyIsEqualTo>
-                <ogc:Function name="numberFormat">
-                  <ogc:Literal>0.000000</ogc:Literal>
-                  <ogc:PropertyName>height</ogc:PropertyName>
-                </ogc:Function>
-                <ogc:Literal>{{ printf "%f" .High }}</ogc:Literal>
-                </ogc:PropertyIsEqualTo>
-              {{ end -}}
-              {{ end }}
-            </ogc:Or>
-          </ogc:Filter>
-          <se:MaxScaleDenominator>5e3</se:MaxScaleDenominator>
-          <se:LineSymbolizer>
-            <se:Stroke>
-              <se:SvgParameter name="stroke-width">1.5</se:SvgParameter>
-              <se:SvgParameter name="stroke">
-                <ogc:Function name="Recode">
-                  <ogc:Function name="numberFormat">
-                    <ogc:Literal>0.000000</ogc:Literal>
-                    <ogc:PropertyName>height</ogc:PropertyName>
-                  </ogc:Function>
-                  {{ range . -}}
-                  {{ if .HasHigh -}}
-                  <ogc:Literal>{{ printf "%f" .High }}</ogc:Literal>
-                  <ogc:Literal>{{ .Color }}</ogc:Literal>
-                  {{ end -}}
-                  {{ end }}
-                </ogc:Function>
-              </se:SvgParameter>
-            </se:Stroke>
-          </se:LineSymbolizer>
-        </se:Rule>
-      </se:FeatureTypeStyle>
-      <se:FeatureTypeStyle>
         <se:Name>contour_lines_label</se:Name>
         <se:Description>
           <se:Abstract>
-            FeatureTypeStyle for labels at contour lines
+            FeatureTypeStyle for labels at color areas
           </se:Abstract>
         </se:Description>
         <se:Rule>
           <se:MaxScaleDenominator>5e3</se:MaxScaleDenominator>
           <se:TextSymbolizer>
+            <se:VendorOption name="spaceAround">50</se:VendorOption>
             <se:Label>
               <ogc:Function name="Recode">
                 <ogc:Function name="numberFormat">
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/style-templates/sounding_results_areas_geoserver.sld-template	Wed Oct 09 16:40:18 2019 +0200
@@ -0,0 +1,153 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<StyledLayerDescriptor
+    xmlns="http://www.opengis.net/sld"
+    xmlns:se="http://www.opengis.net/se"
+    xmlns:ogc="http://www.opengis.net/ogc"
+    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+    xsi:schemaLocation="http://www.opengis.net/sld http://schemas.opengis.net/sld/1.1.0/StyledLayerDescriptor.xsd"
+    version="1.1.0">
+  <NamedLayer>
+    <se:Name>sounding_results_areas</se:Name>
+    <UserStyle>
+      <se:Name>sounding_results_areas</se:Name>
+      <se:FeatureTypeStyle>
+          <se:Name>area_colours</se:Name>
+        <se:Description>
+          <se:Abstract>
+            FeatureTypeStyle defining colour classes for height attribute
+          </se:Abstract>
+        </se:Description>
+        {{ range . -}}
+        <se:Rule>
+        {{- if not .HasLow }}
+          <se:Name>&#8804; {{ printf "%g" .High }}</se:Name>
+          <ogc:Filter>
+            <ogc:PropertyIsLessThanOrEqualTo>
+              <ogc:PropertyName>height</ogc:PropertyName>
+              <ogc:Literal>{{ printf "%f" .High }}</ogc:Literal>
+            </ogc:PropertyIsLessThanOrEqualTo>
+          </ogc:Filter>
+        {{- else if not .HasHigh }}
+          <se:Name>&gt; {{ printf "%g" .Low }}</se:Name>
+          <ogc:Filter>
+            <ogc:PropertyIsGreaterThanOrEqualTo>
+              <ogc:PropertyName>height</ogc:PropertyName>
+              <ogc:Literal>{{ printf "%f" .Low }}</ogc:Literal>
+            </ogc:PropertyIsGreaterThanOrEqualTo>
+          </ogc:Filter>
+        {{- else }}
+          <se:Name>&#8804; {{ printf "%g" .High }}</se:Name>
+          <ogc:Filter>
+            <ogc:And>
+              <ogc:PropertyIsGreaterThan>
+                <ogc:PropertyName>height</ogc:PropertyName>
+                <ogc:Literal>{{ printf "%f" .Low }}</ogc:Literal>
+              </ogc:PropertyIsGreaterThan>
+              <ogc:PropertyIsLessThanOrEqualTo>
+                <ogc:PropertyName>height</ogc:PropertyName>
+                <ogc:Literal>{{ printf "%f" .High }}</ogc:Literal>
+              </ogc:PropertyIsLessThanOrEqualTo>
+            </ogc:And>
+          </ogc:Filter>
+        {{- end }}
+          <se:MaxScaleDenominator>34e3</se:MaxScaleDenominator>
+          <se:PolygonSymbolizer>
+            <se:Fill>
+              <se:SvgParameter name="fill">{{ .Color }}</se:SvgParameter>
+            </se:Fill>
+            <se:Stroke>
+              <se:SvgParameter name="stroke">#404040</se:SvgParameter>
+              <se:SvgParameter name="stroke-width">0.5</se:SvgParameter>
+            </se:Stroke>
+          </se:PolygonSymbolizer>
+        </se:Rule>
+        <se:Rule>
+        {{- if not .HasLow }}
+          <se:Name>&#8804; {{ printf "%g" .High }}</se:Name>
+          <ogc:Filter>
+            <ogc:PropertyIsLessThanOrEqualTo>
+              <ogc:PropertyName>height</ogc:PropertyName>
+              <ogc:Literal>{{ printf "%f" .High }}</ogc:Literal>
+            </ogc:PropertyIsLessThanOrEqualTo>
+          </ogc:Filter>
+        {{- else if not .HasHigh }}
+          <se:Name>&gt; {{ printf "%g" .Low }}</se:Name>
+          <ogc:Filter>
+            <ogc:PropertyIsGreaterThanOrEqualTo>
+              <ogc:PropertyName>height</ogc:PropertyName>
+              <ogc:Literal>{{ printf "%f" .Low }}</ogc:Literal>
+            </ogc:PropertyIsGreaterThanOrEqualTo>
+          </ogc:Filter>
+        {{- else }}
+          <se:Name>&#8804; {{ printf "%g" .High }}</se:Name>
+          <ogc:Filter>
+            <ogc:And>
+              <ogc:PropertyIsGreaterThan>
+                <ogc:PropertyName>height</ogc:PropertyName>
+                <ogc:Literal>{{ printf "%f" .Low }}</ogc:Literal>
+              </ogc:PropertyIsGreaterThan>
+              <ogc:PropertyIsLessThanOrEqualTo>
+                <ogc:PropertyName>height</ogc:PropertyName>
+                <ogc:Literal>{{ printf "%f" .High }}</ogc:Literal>
+              </ogc:PropertyIsLessThanOrEqualTo>
+            </ogc:And>
+          </ogc:Filter>
+        {{- end }}
+          <se:MinScaleDenominator>34e3</se:MinScaleDenominator>
+          <se:PolygonSymbolizer>
+            <se:Fill>
+              <se:SvgParameter name="fill">{{ .Color }}</se:SvgParameter>
+            </se:Fill>
+          </se:PolygonSymbolizer>
+        </se:Rule>
+        <se:VendorOption name="sortBy">height</se:VendorOption>
+        {{ end }}
+      </se:FeatureTypeStyle>
+      <se:FeatureTypeStyle>
+        <se:Name>area_labels</se:Name>
+        <se:Description>
+          <se:Abstract>
+            FeatureTypeStyle for labels at colour areas
+          </se:Abstract>
+        </se:Description>
+        <se:Rule>
+          <se:MaxScaleDenominator>5e3</se:MaxScaleDenominator>
+          <se:TextSymbolizer>
+            <se:VendorOption name="spaceAround">10</se:VendorOption>
+            <se:Label>
+              <ogc:Function name="Recode">
+                <ogc:Function name="numberFormat">
+                  <ogc:Literal>0.000000</ogc:Literal>
+                  <ogc:PropertyName>height</ogc:PropertyName>
+                </ogc:Function>
+                {{ range . -}}
+                {{ if .HasHigh -}}
+                    <ogc:Literal>
+                    {{- printf "%f" .High -}}
+                    </ogc:Literal><ogc:Literal>
+                    {{- printf "%g" .High -}}
+                    </ogc:Literal>
+                {{ end -}}
+                {{ end }}
+              </ogc:Function>
+            </se:Label>
+            <se:Font>
+              <se:SvgParameter name="font-family">Avenir</se:SvgParameter>
+              <se:SvgParameter name="font-family">Helvetica</se:SvgParameter>
+              <se:SvgParameter name="font-family">Arial</se:SvgParameter>
+              <se:SvgParameter name="font-family">sans-serif</se:SvgParameter>
+            </se:Font>
+            <se:LabelPlacement>
+              <se:LinePlacement>
+                <se:PerpendicularOffset>5</se:PerpendicularOffset>
+              </se:LinePlacement>
+            </se:LabelPlacement>
+            <se:Fill>
+              <se:SvgParameter name="fill">#070707</se:SvgParameter>
+            </se:Fill>
+          </se:TextSymbolizer>
+        </se:Rule>
+      </se:FeatureTypeStyle>
+    </UserStyle>
+  </NamedLayer>
+</StyledLayerDescriptor>
--- a/style-templates/sounding_results_contour_lines_geoserver.sld-template	Fri Oct 04 17:26:56 2019 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,157 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<StyledLayerDescriptor
-    xmlns="http://www.opengis.net/sld"
-    xmlns:se="http://www.opengis.net/se"
-    xmlns:ogc="http://www.opengis.net/ogc"
-    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
-    xsi:schemaLocation="http://www.opengis.net/sld http://schemas.opengis.net/sld/1.1.0/StyledLayerDescriptor.xsd"
-    version="1.1.0">
-  <NamedLayer>
-    <se:Name>sounding_results_contour_lines</se:Name>
-    <UserStyle>
-      <se:Name>sounding_results_contour_lines</se:Name>
-      <se:FeatureTypeStyle>
-          <se:Name>contour_line_colours</se:Name>
-        <se:Description>
-          <se:Abstract>
-            FeatureTypeStyle defining colour classes for height attribute
-          </se:Abstract>
-        </se:Description>
-        {{ range . -}}
-        <se:Rule>
-        {{- if not .HasLow }}
-          <se:Name>&#8804; {{ printf "%g" .High }}</se:Name>
-          <ogc:Filter>
-            <ogc:PropertyIsLessThanOrEqualTo>
-              <ogc:PropertyName>height</ogc:PropertyName>
-              <ogc:Literal>{{ printf "%f" .High }}</ogc:Literal>
-            </ogc:PropertyIsLessThanOrEqualTo>
-          </ogc:Filter>
-        {{- else if not .HasHigh }}
-          <se:Name>&gt; {{ printf "%g" .Low }}</se:Name>
-          <ogc:Filter>
-            <ogc:PropertyIsGreaterThanOrEqualTo>
-              <ogc:PropertyName>height</ogc:PropertyName>
-              <ogc:Literal>{{ printf "%f" .Low }}</ogc:Literal>
-            </ogc:PropertyIsGreaterThanOrEqualTo>
-          </ogc:Filter>
-        {{- else }}
-          <se:Name>&#8804; {{ printf "%g" .High }}</se:Name>
-          <ogc:Filter>
-            <ogc:And>
-              <ogc:PropertyIsGreaterThan>
-                <ogc:PropertyName>height</ogc:PropertyName>
-                <ogc:Literal>{{ printf "%f" .Low }}</ogc:Literal>
-              </ogc:PropertyIsGreaterThan>
-              <ogc:PropertyIsLessThanOrEqualTo>
-                <ogc:PropertyName>height</ogc:PropertyName>
-                <ogc:Literal>{{ printf "%f" .High }}</ogc:Literal>
-              </ogc:PropertyIsLessThanOrEqualTo>
-            </ogc:And>
-          </ogc:Filter>
-        {{- end }}
-           <se:LineSymbolizer>
-            <se:Stroke>
-              <se:SvgParameter name="stroke">{{ .Color }}</se:SvgParameter>
-              <se:SvgParameter name="stroke-width">0.5</se:SvgParameter>
-            </se:Stroke>
-          </se:LineSymbolizer>
-        </se:Rule>
-        {{ end }}
-      </se:FeatureTypeStyle>
-      <se:FeatureTypeStyle>
-        <se:Name>contour_lines_emph</se:Name>
-        <se:Description>
-          <se:Abstract>
-            FeatureTypeStyle for emphasized contour lines
-          </se:Abstract>
-          </se:Description>
-          <se:Rule>
-            <se:LegendGraphic>
-              <se:Graphic>
-            </se:Graphic>
-          </se:LegendGraphic>
-          <ogc:Filter>
-             <ogc:Or>
-              {{ range . -}}
-              {{ if .HasHigh -}}
-                <ogc:PropertyIsEqualTo>
-                <ogc:Function name="numberFormat">
-                  <ogc:Literal>0.000000</ogc:Literal>
-                  <ogc:PropertyName>height</ogc:PropertyName>
-                </ogc:Function>
-                <ogc:Literal>{{ printf "%f" .High }}</ogc:Literal>
-                </ogc:PropertyIsEqualTo>
-              {{ end -}}
-              {{ end }}
-            </ogc:Or>
-          </ogc:Filter>
-          <se:MaxScaleDenominator>5e3</se:MaxScaleDenominator>
-          <se:LineSymbolizer>
-            <se:Stroke>
-              <se:SvgParameter name="stroke-width">1.5</se:SvgParameter>
-              <se:SvgParameter name="stroke">
-                <ogc:Function name="Recode">
-                  <ogc:Function name="numberFormat">
-                    <ogc:Literal>0.000000</ogc:Literal>
-                    <ogc:PropertyName>height</ogc:PropertyName>
-                  </ogc:Function>
-                  {{ range . -}}
-                  {{ if .HasHigh -}}
-                  <ogc:Literal>{{ printf "%f" .High }}</ogc:Literal>
-                  <ogc:Literal>{{ .Color }}</ogc:Literal>
-                  {{ end -}}
-                  {{ end }}
-                </ogc:Function>
-              </se:SvgParameter>
-            </se:Stroke>
-          </se:LineSymbolizer>
-        </se:Rule>
-      </se:FeatureTypeStyle>
-      <se:FeatureTypeStyle>
-        <se:Name>contour_lines_label</se:Name>
-        <se:Description>
-          <se:Abstract>
-            FeatureTypeStyle for labels at contour lines
-          </se:Abstract>
-        </se:Description>
-        <se:Rule>
-          <se:MaxScaleDenominator>5e3</se:MaxScaleDenominator>
-          <se:TextSymbolizer>
-            <se:Label>
-              <ogc:Function name="Recode">
-                <ogc:Function name="numberFormat">
-                  <ogc:Literal>0.000000</ogc:Literal>
-                  <ogc:PropertyName>height</ogc:PropertyName>
-                </ogc:Function>
-                {{ range . -}}
-                {{ if .HasHigh -}}
-                    <ogc:Literal>
-                    {{- printf "%f" .High -}}
-                    </ogc:Literal><ogc:Literal>
-                    {{- printf "%g" .High -}}
-                    </ogc:Literal>
-                {{ end -}}
-                {{ end }}
-              </ogc:Function>
-            </se:Label>
-            <se:Font>
-              <se:SvgParameter name="font-family">Avenir</se:SvgParameter>
-              <se:SvgParameter name="font-family">Helvetica</se:SvgParameter>
-              <se:SvgParameter name="font-family">Arial</se:SvgParameter>
-              <se:SvgParameter name="font-family">sans-serif</se:SvgParameter>
-            </se:Font>
-            <se:LabelPlacement>
-              <se:LinePlacement>
-                <se:PerpendicularOffset>5</se:PerpendicularOffset>
-              </se:LinePlacement>
-            </se:LabelPlacement>
-            <se:Fill>
-              <se:SvgParameter name="fill">#070707</se:SvgParameter>
-            </se:Fill>
-          </se:TextSymbolizer>
-        </se:Rule>
-      </se:FeatureTypeStyle>
-    </UserStyle>
-  </NamedLayer>
-</StyledLayerDescriptor>