changeset 1948:8e6f6c376b75

merge translations from weblate
author Fadi Abbud <fadi.abbud@intevation.de>
date Tue, 22 Jan 2019 11:22:17 +0100
parents 4235fa8f59d7 (diff) 99196fbc3897 (current diff)
children fd74afccfdf1
files client/src/locale/ro_RO/LC_MESSAGES/app.po
diffstat 49 files changed, 1794 insertions(+), 584 deletions(-) [+]
line wrap: on
line diff
--- a/.hgtags	Fri Jan 18 06:51:49 2019 +0000
+++ b/.hgtags	Tue Jan 22 11:22:17 2019 +0100
@@ -1,2 +1,4 @@
 532c8392048fe720bf31b7efb475591ab95a74ec v1-uat1
 136aaa7f00af0b500c5c75f9fd9aabfc7fa526ee v1.1.0
+724758455a4e44d25b096f00bcb353ceda0e0d57 v2-preview20190111
+0059aa870a396e2112efae43f822c0c133454d7a v2
--- a/client/.env	Fri Jan 18 06:51:49 2019 +0000
+++ b/client/.env	Tue Jan 22 11:22:17 2019 +0100
@@ -5,8 +5,9 @@
 VUE_APP_API_URL=/api/
 VUE_BACKEND_API_URL=http://gemma_backend:8000
 
-#URL of secondary logo
-VUE_APP_SECONDARY_LOGO_URL=
-
 #Path of vendored images is copied during a webpack build
 VUE_APP_VENDOR_IMG_PATH=
+
+#Logos to be potentially loaded by the SPA. Can be left blank.
+VUE_APP_SECONDARY_LOGO_URL=
+VUE_APP_LOGO_FOR_PDF_URL=
--- a/client/package.json	Fri Jan 18 06:51:49 2019 +0000
+++ b/client/package.json	Tue Jan 22 11:22:17 2019 +0100
@@ -32,6 +32,7 @@
     "debounce": "^1.2.0",
     "font-awesome": "^4.7.0",
     "glob-all": "^3.1.0",
+    "jspdf": "^1.5.3",
     "locale2": "^2.2.0",
     "ol": "^5.3.0",
     "path": "^0.12.7",
--- a/client/src/components/ImportStretches.vue	Fri Jan 18 06:51:49 2019 +0000
+++ b/client/src/components/ImportStretches.vue	Tue Jan 22 11:22:17 2019 +0100
@@ -2,193 +2,231 @@
   <div class="d-flex flex-column mb-3">
     <h6 class="mb-0 py-2 px-3 border-bottom d-flex align-items-center">
       <font-awesome-icon icon="road" class="mr-2"></font-awesome-icon>
-      <translate>Define section and stretches</translate>
+      <translate>Define stretches</translate>
     </h6>
-    <div class="ml-3 mr-3">
-      <div class="d-flex flex-row justify-content-between">
-        <div class="mt-2 w-50 mr-2 text-left">
-          <small class="text-muted"> <translate>ID</translate> </small>
-          <input
-            id="id"
-            type="text"
-            class="form-control"
-            placeholder="AT_Section_12"
-            aria-label="id"
-            v-model="id"
-          />
-          <span class="text-left text-danger">
-            <small v-if="idError && !id">
-              <translate>Please enter an id</translate>
-            </small>
-          </span>
-        </div>
-        <div class="mt-2 w-50 ml-2 text-left">
-          <small class="text-muted"><translate>Function</translate> </small>
-          <select v-model="funktion" class="custom-select">
-            <option value="section"><translate>Section</translate></option>
-            <option v-if="isSysAdmin" value="stretch"
-              ><translate>Stretch</translate></option
-            >
-          </select>
-          <span class="text-left text-danger">
-            <small v-if="funktionError && !funktion">
-              <translate>Please enter a function</translate>
-            </small>
-          </span>
-        </div>
+    <div v-if="!edit" class="mb-3 mr-3 ml-3 text-left">
+      <table v-if="stretches.length > 0" class="table">
+        <thead>
+          <tr>
+            <th class="header"><translate>Name</translate></th>
+            <th class="header"><translate>Datum</translate></th>
+            <th class="header"><translate>Source organization</translate></th>
+            <th class="tools">&nbsp;</th>
+            <th class="tools">&nbsp;</th>
+          </tr>
+        </thead>
+        <tbody>
+          <tr class="small" v-for="(stretch, index) in stretches" :key="index">
+            <td class="">
+              <a @click="moveMapToStretch(index)" href="#">{{
+                stretch.properties.name
+              }}</a>
+            </td>
+            <td class="">
+              {{ formatSurveyDate(stretch.properties["date_info"]) }}
+            </td>
+            <td>{{ stretch.properties["source_organization"] }}</td>
+            <td>
+              <font-awesome-icon
+                @click="editStretch(index)"
+                icon="pencil-alt"
+                fixed-width
+              ></font-awesome-icon>
+            </td>
+            <td>
+              <font-awesome-icon
+                @click="deleteStretch(index)"
+                icon="trash"
+                fixed-width
+              ></font-awesome-icon>
+            </td>
+          </tr>
+        </tbody>
+      </table>
+      <div class="mt-3" v-if="stretches.length == 0">
+        <translate>No results.</translate>
       </div>
-      <div class="d-flex flex-column  justify-content-between">
-        <div class="mt-2 text-left">
-          <small class="text-muted"> <translate>Start rhm</translate> </small>
-          <div class="d-flex flex-row">
+    </div>
+    <div v-if="edit">
+      <div class="ml-3 mr-3">
+        <div class="d-flex flex-row justify-content-between">
+          <div class="mt-2 w-50 mr-2 text-left">
+            <small class="text-muted"> <translate>ID</translate> </small>
             <input
-              id="startrhm"
+              id="id"
               type="text"
               class="form-control"
-              placeholder="e.g. ATXXX00001000000019900"
-              aria-label="startrhm"
-              v-model="startrhm"
+              placeholder="AT_Section_12"
+              aria-label="id"
+              v-model="id"
             />
-            <span class="input-group-text">
-              <font-awesome-icon
-                @click="togglePipette('start')"
-                :class="{ 'text-info': pipetteStart }"
-                icon="bullseye"
-              ></font-awesome-icon>
+            <span class="text-left text-danger">
+              <small v-if="idError && !id">
+                <translate>Please enter an id</translate>
+              </small>
+            </span>
+          </div>
+          <div class="mt-2 w-50 ml-2 text-left">
+            <div>
+              <small class="text-muted">
+                <translate>Countrycode</translate>
+              </small>
+              <input
+                id="countryCode"
+                type="text"
+                class="form-control"
+                placeholder="AT"
+                aria-label="id"
+                v-model="countryCode"
+              />
+              <span class="text-left text-danger">
+                <small v-if="countryCodeError && !countryCode">
+                  <translate>Please enter a countrycode </translate>
+                </small>
+              </span>
+            </div>
+            <div class="w-50 ml-2"></div>
+          </div>
+        </div>
+        <div class="d-flex flex-column  justify-content-between">
+          <div class="mt-2 text-left">
+            <small class="text-muted"> <translate>Start rhm</translate> </small>
+            <div class="d-flex flex-row">
+              <input
+                id="startrhm"
+                type="text"
+                class="form-control"
+                placeholder="e.g. ATXXX00001000000019900"
+                aria-label="startrhm"
+                v-model="startrhm"
+              />
+              <span class="input-group-text">
+                <font-awesome-icon
+                  @click="togglePipette('start')"
+                  :class="{ 'text-info': pipetteStart }"
+                  icon="bullseye"
+                ></font-awesome-icon>
+              </span>
+            </div>
+            <span class="text-left text-danger">
+              <small v-if="startrhmError && !startrhm">
+                <translate>Please enter a start point</translate>
+              </small>
             </span>
           </div>
-          <span class="text-left text-danger">
-            <small v-if="startrhmError && !startrhm">
-              <translate>Please enter a start point</translate>
-            </small>
-          </span>
+          <div class="mt-2 text-left">
+            <small class="text-muted"> <translate>End rhm</translate> </small>
+            <div class="d-flex flex-row">
+              <input
+                id="endrhm"
+                type="text"
+                class="form-control"
+                placeholder="e.g. ATXXX00001000000019900"
+                aria-label="endrhm"
+                v-model="endrhm"
+              />
+              <span class="input-group-text">
+                <font-awesome-icon
+                  @click="togglePipette('end')"
+                  :class="{ 'text-info': pipetteEnd }"
+                  icon="bullseye"
+                ></font-awesome-icon>
+              </span>
+            </div>
+            <span class="text-left text-danger">
+              <small v-if="endrhmError && !endrhm">
+                <translate>Please enter an end point</translate>
+              </small>
+            </span>
+          </div>
         </div>
-        <div class="mt-2 text-left">
-          <small class="text-muted"> <translate>End rhm</translate> </small>
-          <div class="d-flex flex-row">
+        <div class="d-flex flex-row justify-content-between">
+          <div class="mt-2  mr-2 w-50  text-left">
+            <small class="text-muted">
+              <translate>Object name</translate>
+            </small>
             <input
-              id="endrhm"
+              id="objbn"
               type="text"
               class="form-control"
-              placeholder="e.g. ATXXX00001000000019900"
-              aria-label="endrhm"
-              v-model="endrhm"
+              placeholder=""
+              aria-label="objbn"
+              v-model="objbn"
             />
-            <span class="input-group-text">
-              <font-awesome-icon
-                @click="togglePipette('end')"
-                :class="{ 'text-info': pipetteEnd }"
-                icon="bullseye"
-              ></font-awesome-icon>
+            <span class="text-left text-danger">
+              <small v-if="objbnError && !objbn">
+                <translate>Please enter an objectname</translate>
+              </small>
+            </span>
+          </div>
+          <div class="mt-2  ml-2 w-50  text-left">
+            <small class="text-muted">
+              <translate>National Object name</translate>
+            </small>
+            <input
+              id="nobjbn"
+              type="text"
+              class="form-control"
+              placeholder=""
+              aria-label="nobjbn"
+              v-model="nobjbn"
+            />
+            <span class="text-left text-danger">
+              <small v-if="nobjbnError && !nobjbn">
+                <translate>Please enter an objectname</translate>
+              </small>
             </span>
           </div>
-          <span class="text-left text-danger">
-            <small v-if="endrhmError && !endrhm">
-              <translate>Please enter an end point</translate>
-            </small>
-          </span>
+        </div>
+        <div class="d-flex flex-row justify-content-between">
+          <div class="mt-2 mr-2 w-50 text-left">
+            <small class="text-muted"> <translate>Date info</translate> </small>
+            <input
+              id="date_info"
+              type="date"
+              class="form-control"
+              placeholder="date_info"
+              aria-label="date_info"
+              v-model="date_info"
+            />
+            <span class="text-left text-danger">
+              <small v-if="date_infoError && !date_info">
+                <translate>Please enter a date</translate>
+              </small>
+            </span>
+          </div>
+          <div class="mt-2 ml-2 w-50 text-left">
+            <small class="text-muted"> <translate>Source</translate> </small>
+            <input
+              id="source"
+              type="text"
+              class="form-control"
+              placeholder="source"
+              aria-label="source"
+              v-model="source"
+            />
+            <span class="text-left text-danger">
+              <small v-if="sourceError && !source">
+                <translate>Please enter a source</translate>
+              </small>
+            </span>
+          </div>
         </div>
       </div>
-      <div class="d-flex flex-row justify-content-between">
-        <div class="mt-2  mr-2 w-50  text-left">
-          <small class="text-muted"> <translate>Object name</translate> </small>
-          <input
-            id="objbn"
-            type="text"
-            class="form-control"
-            placeholder=""
-            aria-label="objbn"
-            v-model="objbn"
-          />
-          <span class="text-left text-danger">
-            <small v-if="objbnError && !objbn">
-              <translate>Please enter an objectname</translate>
-            </small>
-          </span>
-        </div>
-        <div class="mt-2  ml-2 w-50  text-left">
-          <small class="text-muted">
-            <translate>National Object name</translate>
-          </small>
-          <input
-            id="nobjbn"
-            type="text"
-            class="form-control"
-            placeholder=""
-            aria-label="nobjbn"
-            v-model="nobjbn"
-          />
-          <span class="text-left text-danger">
-            <small v-if="nobjbnError && !nobjbn">
-              <translate>Please enter an objectname</translate>
-            </small>
-          </span>
-        </div>
-      </div>
-      <div class="d-flex flex-row justify-content-between">
-        <div class="mt-2 mr-2 w-50 text-left">
-          <small class="text-muted"> <translate>Date info</translate> </small>
-          <input
-            id="date_info"
-            type="date"
-            class="form-control"
-            placeholder="date_info"
-            aria-label="date_info"
-            v-model="date_info"
-          />
-          <span class="text-left text-danger">
-            <small v-if="date_infoError && !date_info">
-              <translate>Please enter a date</translate>
-            </small>
-          </span>
-        </div>
-        <div class="mt-2 ml-2 w-50 text-left">
-          <small class="text-muted"> <translate>Source</translate> </small>
-          <input
-            id="source"
-            type="text"
-            class="form-control"
-            placeholder="source"
-            aria-label="source"
-            v-model="source"
-          />
-          <span class="text-left text-danger">
-            <small v-if="sourceError && !source">
-              <translate>Please enter a source</translate>
-            </small>
-          </span>
-        </div>
-      </div>
-      <div
-        v-if="funktion === 'stretch'"
-        class="d-flex flex-row justify-content-between"
-      >
-        <div class="mt-2 w-50 mr-2 text-left ">
-          <small class="text-muted"> <translate>Countrycode</translate> </small>
-          <input
-            id="countryCode"
-            type="text"
-            class="form-control"
-            placeholder="AT"
-            aria-label="id"
-            v-model="id"
-          />
-          <span class="text-left text-danger">
-            <small v-if="countryCodeError && !countryCode">
-              <translate>Please enter a countrycode </translate>
-            </small>
-          </span>
-        </div>
-        <div class="w-50 ml-2"></div>
+      <div class="text-right mt-2 mr-3 mb-3">
+        <button @click="edit = false" class="btn btn-warning mr-2">Back</button>
+        <button
+          @click="save"
+          type="submit"
+          class="shadow-sm btn btn-info submit-button"
+        >
+          <translate>Submit</translate>
+        </button>
       </div>
     </div>
-    <div class="text-right mt-2 mr-3 mb-3">
-      <button
-        @click="submit"
-        type="submit"
-        class="shadow-sm btn btn-info submit-button"
-      >
-        <translate>Submit</translate>
+    <div class="text-right mr-3">
+      <button v-if="!edit" @click="startEdit()" class="btn btn-info">
+        <translate>New stretch</translate>
       </button>
     </div>
   </div>
@@ -209,12 +247,15 @@
  * Thomas Junk <thomas.junk@intevation.de>
  */
 import { mapState, mapGetters } from "vuex";
-import { displayInfo } from "@/lib/errors.js";
+import { displayError, displayInfo } from "@/lib/errors.js";
+import { formatSurveyDate } from "@/lib/date.js";
+import center from "@turf/center";
 
 export default {
   name: "importstretches",
   data() {
     return {
+      edit: false,
       id: "",
       funktion: "",
       startrhm: "",
@@ -237,7 +278,74 @@
       countryCodeError: false
     };
   },
+  mounted() {
+    this.edit = false;
+    this.loadStretches();
+  },
   methods: {
+    editStretch(index) {
+      const { date_info, name, objnam, nobjnam } = this.stretches[
+        index
+      ].properties;
+      this.date_info = date_info;
+      this.id = name;
+      this.nobjbn = nobjnam;
+      this.objbn = objnam;
+      this.source = this.stretches[index];
+      this.edit = true;
+    },
+    deleteStretch(index) {
+      displayInfo({
+        title: this.$gettext("Not implemented"),
+        message: this.$gettext("Deleting " + this.stretches[index].id)
+      });
+    },
+    moveMapToStretch(index) {
+      const { coordinates } = center(this.stretches[index]).geometry;
+      this.$store.commit("map/moveMap", {
+        coordinates: coordinates,
+        zoom: 17,
+        preventZoomOut: true
+      });
+    },
+    formatSurveyDate(d) {
+      return formatSurveyDate(d);
+    },
+    loadStretches() {
+      this.$store.dispatch("imports/loadStretches").catch(error => {
+        const { status, data } = error.response;
+        displayError({
+          title: this.$gettext("Backend Error"),
+          message: `${status}: ${data.message || data}`
+        });
+      });
+    },
+    clean() {
+      this.id = "";
+      this.funktion = "";
+      this.startrhm = "";
+      this.endrhm = "";
+      this.objbn = "";
+      this.nobjbn = "";
+      this.countryCode = "";
+      this.date_info = new Date().toISOString().split("T")[0];
+      this.source = "";
+      this.pipetteStart = false;
+      this.pipetteEnd = false;
+      this.idError = false;
+      this.funktionError = false;
+      this.startrhmError = false;
+      this.endrhmError = false;
+      this.objbnError = false;
+      this.nobjbnError = false;
+      this.date_infoError = false;
+      this.sourceError = false;
+      this.countryCodeError = false;
+    },
+    startEdit() {
+      this.clean();
+      this.edit = true;
+    },
     togglePipette(t) {
       if (t === "start") {
         this.pipetteStart = !this.pipetteStart;
@@ -254,7 +362,6 @@
         "startrhm",
         "endrhm",
         "objbn",
-        "nobjbn",
         "date_info",
         "source"
       ];
@@ -266,12 +373,37 @@
         }
       });
     },
-    submit() {
+    save() {
       this.validate();
-      displayInfo({
-        title: this.$gettext("Sections"),
-        message: this.$gettext("Not implemented!")
-      });
+      const data = {
+        name: this.id,
+        from: this.startrhm,
+        to: this.endrhm,
+        "source-organization": this.source,
+        "date-info": this.date_info,
+        objnam: this.objbn,
+        nobjnam: this.nobjbn,
+        countries: this.countryCode.split(",")
+      };
+      this.$store
+        .dispatch("imports/saveStretch", data)
+        .then(() => {
+          displayInfo({
+            title: this.$gettext("Import"),
+            message: this.$gettext("Starting import of stretch")
+          });
+          this.clean();
+          this.edit = false;
+          this.loadStretches();
+        })
+        .catch(error => {
+          console.log(error);
+          const { status, data } = error.response;
+          displayError({
+            title: this.$gettext("Backend Error"),
+            message: `${status}: ${data.message || data}`
+          });
+        });
     }
   },
   watch: {
@@ -293,7 +425,8 @@
   },
   computed: {
     ...mapState("map", ["identifiedFeatures", "currentMeasurement"]),
-    ...mapGetters("user", ["isSysAdmin"])
+    ...mapGetters("user", ["isSysAdmin"]),
+    ...mapState("imports", ["stretches"])
   }
 };
 </script>
--- a/client/src/components/Maplayer.vue	Fri Jan 18 06:51:49 2019 +0000
+++ b/client/src/components/Maplayer.vue	Tue Jan 22 11:22:17 2019 +0100
@@ -14,25 +14,6 @@
 .mapfull {
   height: 100vh;
 }
-
-// the following css part is for browser-printing based pdf generation
-@page {
-  size: A4 landscape !important;
-  margin: 4mm !important;
-  // according to https://www.w3.org/TR/css-page-3/#page-size-prop
-  // we shall now have 210 - 2*4 = 202 mm width and 297 - 2*4 = 289 mm height
-}
-
-@media print {
-  .mapfull {
-    width: 2000px;
-    height: 2828px;
-  }
-  .mapsplit {
-    width: 2000px;
-    height: 2828px;
-  }
-}
 </style>
 
 <script>
@@ -42,7 +23,7 @@
  * SPDX-License-Identifier: AGPL-3.0-or-later
  * License-Filename: LICENSES/AGPL-3.0.txt
  *
- * Copyright (C) 2018 by via donau
+ * Copyright (C) 2018, 2019 by via donau
  *   – Österreichische Wasserstraßen-Gesellschaft mbH
  * Software engineering by Intevation GmbH
  *
@@ -155,34 +136,6 @@
       }
       layer.isVisible = exists;
       layer.data.setVisible(exists);
-    },
-    onBeforePrint(/* evt */) {
-      // console.log("onBeforePrint(", evt ,")");
-      //
-      // the following code shows how to get the current map canvas
-      // and change it, however this does not work well enough, as
-      // another mechanism seems to update the size again before the rendering
-      // for printing is done:
-      // console.log(this.openLayersMap.getViewport());
-      // var canvas = this.openLayersMap.getViewport().getElementsByTagName("canvas")[0];
-      // console.log(canvas);
-      // canvas.width=1000;
-      // canvas.height=1414;
-      //
-      // An experiment which also did not work:
-      // this.openLayersMap.setSize([1000, 1414]); // estimate portait DIN A4
-      //
-      // according to documentation
-      // http://openlayers.org/en/latest/apidoc/module-ol_PluggableMap-PluggableMap.html#updateSize
-      // "Force a recalculation of the map viewport size. This should be called when third-party code changes the size of the map viewport."
-      // but did not help
-      // this.openLayersMap.updateSize();
-    },
-    onAfterPrint(/* evt */) {
-      // could be used to undo changes that have been done for printing
-      // though https://www.tjvantoll.com/2012/06/15/detecting-print-requests-with-javascript/
-      // reported that this was not feasable (back then).
-      // console.log("onAfterPrint(", evt, ")");
     }
   },
   watch: {
@@ -316,16 +269,16 @@
       )
     );
 
-    layer = this.getLayerByName("Waterway Area, named");
+    layer = this.getLayerByName("Stretches");
     layer.data.getSource().setLoader(
       this.buildVectorLoader(
         {
           featureNS: "gemma",
           featurePrefix: "gemma",
-          featureTypes: ["hydro_seaare"],
+          featureTypes: ["stretches_geoserver"],
           geometryName: "geom"
         },
-        "/external/d4d",
+        "/internal/wfs",
         layer.data.getSource()
       )
     );
@@ -373,9 +326,6 @@
         console.log(error);
       });
 
-    window.addEventListener("beforeprint", this.onBeforePrint);
-    window.addEventListener("afterprint", this.onAfterPrint);
-
     // so none is shown
     this.updateBottleneckFilter("does_not_exist", "1999-10-01");
     this.$store.dispatch("map/enableIdentifyTool");
--- a/client/src/components/Pdftool.vue	Fri Jan 18 06:51:49 2019 +0000
+++ b/client/src/components/Pdftool.vue	Tue Jan 22 11:22:17 2019 +0100
@@ -7,8 +7,8 @@
   >
     <div style="width: 20rem">
       <h6 class="mb-0 py-2 px-3 border-bottom d-flex align-items-center">
-        <font-awesome-icon icon="file-pdf" class="mr-2"></font-awesome-icon
-        ><translate>Generate PDF</translate>
+        <font-awesome-icon icon="file-pdf" class="mr-2"></font-awesome-icon>
+        <translate>Generate PDF</translate>
         <font-awesome-icon
           icon="times"
           class="ml-auto text-muted"
@@ -16,15 +16,21 @@
         ></font-awesome-icon>
       </h6>
       <div class="p-3">
-        <b><translate>Chose format:</translate></b>
+        <b><translate>Choose format:</translate></b>
         <select v-model="form.format" class="form-control d-block w-100">
           <option value="landscape"><translate>landscape</translate></option>
           <option value="portrait"><translate>portrait</translate></option>
         </select>
+        <select v-model="form.resolution" class="form-control d-block w-100">
+          <option value="80">80 dpi</option>
+          <option value="120">120 dpi</option>
+          <option value="200">200 dpi</option>
+        </select>
         <select v-model="form.paperSize" class="form-control d-block w-100">
           <option value="a3"><translate>ISO A3</translate></option>
           <option value="a4"><translate>ISO A4</translate></option>
         </select>
+        <!--
         <small class="d-block my-2">
           <input
             type="radio"
@@ -33,22 +39,24 @@
             v-model="form.downloadType"
             selected
           />
-          <label for="pdfexport-downloadtype-download" class="ml-1 mr-2"
-            ><translate>Download</translate></label
-          >
+          <label for="pdfexport-downloadtype-download" class="ml-1 mr-2">
+            <translate>Download</translate>
+          </label>
           <input
             type="radio"
             id="pdfexport-downloadtype-open"
             value="open"
             v-model="form.downloadType"
           />
-          <label for="pdfexport-downloadtype-open" class="ml-1"
-            ><translate>Open in new window</translate></label
-          >
+          <label for="pdfexport-downloadtype-open" class="ml-1">
+            <translate>Open in new window</translate>
+          </label>
         </small>
+        -->
         <button
           @click="download"
           type="button"
+          v-bind:disabled="!readyToGenerate"
           class="btn btn-sm btn-info d-block w-100"
         >
           <translate>Generate PDF</translate>
@@ -65,15 +73,19 @@
  * SPDX-License-Identifier: AGPL-3.0-or-later
  * License-Filename: LICENSES/AGPL-3.0.txt
  *
- * Copyright (C) 2018 by via donau
+ * Copyright (C) 2018, 2019 by via donau
  *   – Österreichische Wasserstraßen-Gesellschaft mbH
  * Software engineering by Intevation GmbH
  *
  * Author(s):
  * * Markus Kottländer <markus.kottlaender@intevation.de>
  * * Bernhard E. Reiter <bernhard@intevation.de>
+ * * Fadi Abbud <fadi.abbud@intevation.de>
  */
-import { mapState } from "vuex";
+import { mapGetters, mapState } from "vuex";
+import jsPDF from "jspdf";
+import { getPointResolution } from "ol/proj.js";
+import locale2 from "locale2";
 
 var paperSizes = {
   // in millimeter, landscape [width, height]
@@ -88,34 +100,120 @@
       form: {
         format: "landscape",
         paperSize: "a4",
-        downloadType: "download"
-      }
+        downloadType: "download",
+        resolution: "120"
+      },
+      logoImageForPDF: null, // a HTMLImageElement instance
+      readyToGenerate: true // if the user is allowed to press the button
     };
   },
   computed: {
-    ...mapState("application", ["showPdfTool"]),
-    ...mapState("bottlenecks", ["selectedSurvey"])
+    ...mapState("application", ["showPdfTool", "logoForPDF"]),
+    ...mapState("bottlenecks", ["selectedSurvey"]),
+    ...mapState("map", ["openLayersMap", "isolinesLegendImgDataURL"]),
+    ...mapGetters("map", ["getLayerByName"]),
+    ...mapState("user", ["user"])
   },
   methods: {
-    isLandscape() {
-      return this.form.format !== "portrait";
-    },
     download() {
-      /* eslint-disable no-unused-vars */
-      const width = this.isLandscape()
-        ? paperSizes[this.form.paperSize][0]
-        : paperSizes[this.form.paperSize][1];
-      const height = this.isLandscape()
-        ? paperSizes[this.form.paperSize][1]
-        : paperSizes[this.form.paperSize][0];
+      // disable button while working on it
+      this.readyToGenerate = false;
+
+      console.log(
+        "will generate pdf with",
+        this.form.paperSize,
+        this.form.format,
+        this.form.resolution
+      );
+      var width, height;
+
+      if (this.form.format !== "portrait") {
+        // landscape, default
+        width = paperSizes[this.form.paperSize][0];
+        height = paperSizes[this.form.paperSize][1];
+      } else {
+        // switch width and height
+        width = paperSizes[this.form.paperSize][1];
+        height = paperSizes[this.form.paperSize][0];
+      }
+
+      // FUTURE: consider margins
+
+      // dots per mm = dots per inch / (25.4 mm/inch)
+      var pixelsPerMapMillimeter = this.form.resolution / 25.4;
+      var mapSizeForPrint = [
+        // in pixel
+        Math.round(width * pixelsPerMapMillimeter),
+        Math.round(height * pixelsPerMapMillimeter)
+      ];
+
+      // generate PDF and open it
+      // our units are milimeters; width 0 x height 0 is left upper corner
+
+      // Step 1 prepare and save current map extend
+      // Then add callback "rendercomplete" for Step 3
+      //    which will generate the pdf and resets the map view
+      // Step 2 which starts rendering a map with the necessary image size
+
+      var map = this.openLayersMap;
+      var mapSize = map.getSize(); // size in pixels of the map in the DOM
+      // Calculate the extent for the current view state and the passed size.
+      // The size is the pixel dimensions of the box into which the calculated
+      // extent should fit.
+      var mapExtent = map.getView().calculateExtent(mapSize);
 
-      // TODO: replace this src with an API reponse after actually generating PDFs
-      let src = !this.isLandscape()
-        ? "/img/PrintTemplate-Var2-Landscape.pdf"
-        : "/img/PrintTemplate-Var2-Portrait.pdf";
+      var pdf = new jsPDF(this.form.format, "mm", this.form.paperSize);
+      var northarrowSize = 3;
+      var self = this;
+
+      // set a callback for after the next complete rendering of the map
+      map.once("rendercomplete", function(event) {
+        let canvas = event.context.canvas;
+
+        // because we are using Web Mercator, a pixel represents
+        // a differently sizes spot depending on the place of the map.
+        // So we use a value calculated from the center of the current view.
+        let view = map.getView();
+        let proj = view.getProjection();
+        let metersPerPixel = // average meters (reality) per pixel (map)
+          getPointResolution(proj, view.getResolution(), view.getCenter()) *
+          proj.getMetersPerUnit();
+        // DEBUG console.log("metersPerPixel = ", metersPerPixel);
+
+        let scaleNominator = Math.round(
+          // the x in 1:x map scale
+          1000 * pixelsPerMapMillimeter * metersPerPixel
+        );
+        console.log("scaleNominator = ", scaleNominator);
 
+        var data = canvas.toDataURL("image/jpeg");
+        pdf.addImage(data, "JPEG", 0, 0, width, height);
+        self.addScaleBar(pdf, width, height, scaleNominator);
+        self.addNorthArrow(pdf, 15, 9, northarrowSize);
+        self.addPageInfo(pdf);
+        self.addAboutBox(pdf, width, height);
+
+        if (self.getLayerByName("Bottleneck isolines").isVisible) {
+          self.addLegend(pdf, width, height);
+        }
+
+        pdf.save("map.pdf");
+        // reset to original size
+        map.setSize(mapSize);
+        map.getView().fit(mapExtent, { size: mapSize });
+
+        // as we are done: re-enable button
+        self.readyToGenerate = true;
+      });
+
+      // trigger rendering
+      this.prepareRendering(function() {
+        map.setSize(mapSizeForPrint);
+        map.getView().fit(mapExtent, { size: mapSizeForPrint });
+
+        /*
       let a = document.createElement("a");
-      a.href = src;
+      a.href = src; // need the generated PDF in here (as dataURL?)
 
       if (this.form.downloadType === "download")
         a.download = src.substr(src.lastIndexOf("/") + 1);
@@ -124,6 +222,208 @@
       document.body.appendChild(a);
       a.click();
       document.body.removeChild(a);
+      */
+      });
+    },
+    prepareRendering(callback) {
+      // call callback() once the preparations are done
+      this.logoImageForPDF = new Image();
+
+      this.logoImageForPDF.onload = function() {
+        callback();
+      };
+
+      if (this.logoForPDF) {
+        this.logoImageForPDF.src = this.logoForPDF;
+      } else {
+        this.logoImageForPDF.src = "/img/gemma-logo-for-pdf.png";
+      }
+    },
+    addRoundedBox(doc, x, y, w, h) {
+      // draws a rounded background box at (x,y) width x height
+      // using jsPDF units
+      doc.setDrawColor(255, 255, 255);
+      doc.setFillColor(255, 255, 255);
+      doc.roundedRect(x, y, w, h, 3, 3, "FD");
+    },
+    addScaleBar(doc, docWidth, docHeight, scaleNominator) {
+      // scaleNominator is the x in 1:x of the map scale
+
+      // hardcode maximal width for now and place in lower right corner
+      let maxWidth = 80; // in mm
+
+      // reduce width until we'll find a nice number for printing
+      // strategy:
+      //           1. check which unit prefix we shall use to get [10:10000[
+      //           2. using a mapping for the leading digit to get [1:10[
+      //           3. select a smaller number which is nicely dividable
+      //           4. scale up again to get length in paper mm and to be shown
+
+      // from https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/log10#Polyfill
+      let log10 =
+        Math.log10 || // more precise, but unsupported by IE
+        function(x) {
+          return Math.log(x) * Math.LOG10E;
+        };
+
+      let maxLength = maxWidth * scaleNominator;
+
+      let unit = "mm";
+      let unitConversionFactor = 1;
+      if (maxLength >= 1e7) {
+        // >= 10 km
+        unit = "km";
+        unitConversionFactor = 1e6;
+      } else if (maxLength >= 1e4) {
+        // >= 10 m
+        unit = "m";
+        unitConversionFactor = 1e3;
+      }
+
+      maxLength /= unitConversionFactor;
+
+      // DEBUG console.log(maxLength, unit);
+      let unroundedLength = maxLength;
+      let numberOfDigits = Math.floor(log10(unroundedLength));
+      let factor = Math.pow(10, numberOfDigits);
+      let mapped = unroundedLength / factor;
+      // DEBUG console.log(mapped);
+
+      var length = Math.floor(maxLength); // just to have an upper limit
+
+      // manually only use numbers that are very nice to devide by 4
+      // note that this is taken into account for rounding later
+      if (mapped > 8) {
+        length = 8 * factor;
+      } else if (mapped > 4) {
+        length = 4 * factor;
+      } else if (mapped > 2) {
+        length = 2 * factor;
+      } else {
+        length = factor;
+      }
+
+      let size = (length * unitConversionFactor) / scaleNominator / 4;
+
+      let x = docWidth - (size * 4 + 8);
+      let y = docHeight - 6;
+
+      this.addRoundedBox(doc, x - 4, y - 4, size * 4 + 12, 10);
+
+      doc.setDrawColor(0, 0, 0);
+      doc.setFillColor(0, 0, 0);
+      doc.rect(x, y, size, 1, "FD");
+      doc.setFillColor(255, 255, 255);
+      doc.setDrawColor(0, 0, 0);
+      doc.rect(x + size, y, size, 1, "FD");
+      doc.setFillColor(0, 0, 0);
+      doc.setDrawColor(0, 0, 0);
+      doc.rect(x + size * 2, y, size * 2, 1, "FD");
+      doc.setFontSize(5);
+      doc.text(x, y + 3, "0");
+      // /4 and could give 2.5. We still round, because of floating point arith
+      doc.text(
+        x + size,
+        y + 3,
+        (Math.round((length * 10) / 4) / 10).toString()
+      );
+      doc.text(x + size * 2, y + 3, Math.round(length / 2).toString());
+      doc.text(x + size * 4, y + 3, Math.round(length).toString() + " " + unit);
+    },
+
+    addNorthArrow(doc, x1, y1, size) {
+      var y2 = y1 + size * 3;
+      var x3 = x1 - size * 2;
+      var y3 = y1 + size * 5;
+      var x4 = x1 + size * 2;
+      //white triangle
+      doc.setFillColor(255, 255, 255);
+      doc.setDrawColor(255, 255, 255);
+      doc.triangle(x3 - 0.8, y3 + 1.2, x1, y1 - 1.2, x1, y2 + 0.6, "F");
+      doc.triangle(x1, y1 - 1.2, x1, y2 + 0.6, x4 + 0.8, y3 + 1.2, "F");
+      //north arrow
+      doc.setDrawColor(0, 0, 0);
+      doc.setFillColor(255, 255, 255);
+      doc.triangle(x3, y3, x1, y1, x1, y2, "FD");
+      doc.setFillColor(0, 0, 0);
+      doc.triangle(x1, y1, x1, y2, x4, y3, "FD");
+      doc.setFontSize(size * 3.1);
+      doc.setTextColor(255, 255, 255);
+      doc.setFontStyle("bold");
+      doc.text(size < 3 ? x1 - 0.5 : x1 - 1.3, y3 + 1, "N");
+      doc.setFontSize(size * 3);
+      doc.setTextColor(0, 0, 0);
+      doc.setFontStyle("normal");
+      doc.text(size < 3 ? x1 - 0.5 : x1 - 1.3, y3 + 1, "N");
+    },
+    // add some text at specific coordinates and determine how many wrolds in single line
+    addText(doc, postitionX, positionY, size, color, lineWidth, text) {
+      // split the incoming string to an array, each element is a string of words in a single line
+      var textLines = doc.splitTextToSize(text, lineWidth);
+      // get the longest line to fit the white backround to it
+      var longestString = "";
+      textLines.forEach(function(element) {
+        if (element.length > longestString.length) longestString = element;
+      });
+      var indexOfMaxString = textLines.indexOf(longestString);
+      // white background (rectangular) around the text
+      doc.setFillColor(255, 255, 255);
+      doc.setDrawColor(255, 255, 255);
+      doc.rect(
+        postitionX - doc.getStringUnitWidth(textLines[indexOfMaxString]) / size,
+        size > 10 ? positionY - size / 1.8 : positionY - size / 2.4,
+        doc.getStringUnitWidth(textLines[indexOfMaxString]) * (size / 2.6),
+        textLines.length * (size / 2),
+        "FD"
+      );
+      //rounded rectangular
+      /* doc.roundedRect(
+        postitionX - doc.getStringUnitWidth(textLines[indexOfMaxString]) / size,
+        size > 10 ? positionY - size / 1.8 : positionY - size / 2.6,
+        doc.getStringUnitWidth(textLines[indexOfMaxString]) * (size / 2.6),
+        textLines.length * (size / 2),
+        3,
+        3,
+        "FD"
+      ); */
+      doc.setTextColor(color);
+      doc.setFontSize(size);
+      doc.text(postitionX, positionY, textLines);
+    },
+    addPageInfo(doc) {
+      this.addRoundedBox(doc, 0, 0, 110, 8);
+      let str =
+        this.$gettext("Date of publication:") +
+        " " +
+        new Date().toLocaleString(locale2) +
+        " " +
+        this.$gettext("– printed by:") +
+        " " +
+        this.user;
+      this.addText(doc, 5, 5, 9, "black", 100, str);
+    },
+    addAboutBox(doc, docWidth, docHeight) {
+      let top = docHeight - 20;
+      this.addRoundedBox(doc, 0, top, 120, 20);
+
+      let logoImage = this.logoImageForPDF;
+      let aspectRatio = logoImage.width / logoImage.height;
+      doc.addImage(logoImage, "PNG", 5, docHeight - 19, 110, 110 / aspectRatio);
+
+      let str =
+        "Dislaimer: Lorem ipsum dolor sit amet, consectetur adipisici elit, sed eiusmod tempor incidunt ut labore et dolore magna aliqua.";
+      this.addText(doc, 5, docHeight - 6, 8, "black", 115, str);
+    },
+    addLegend(doc, docWidth) {
+      // transforming into an HTMLImageElement only to find out
+      // the width x height of the legend image
+      // FUTURE: find a better way to get the width and height
+      let legendImage = new Image();
+      legendImage.src = this.isolinesLegendImgDataURL;
+      let aspectRatio = legendImage.width / legendImage.height;
+
+      this.addRoundedBox(doc, docWidth - 54, 0, 54, 50 / aspectRatio + 4);
+      doc.addImage(legendImage, docWidth - 52, 2, 50, 50 / aspectRatio);
     }
   }
 };
--- a/client/src/components/Sidebar.vue	Fri Jan 18 06:51:49 2019 +0000
+++ b/client/src/components/Sidebar.vue	Tue Jan 22 11:22:17 2019 +0100
@@ -40,6 +40,8 @@
           ></font-awesome-icon>
           <span class="fix-trans-space" v-translate>Staging area</span>
         </a>
+      </div>
+      <div v-if="isSysAdmin">
         <a
           :class="['secondary', { active: isActive('stretches') }]"
           @click="toggleContextBox('stretches')"
@@ -50,10 +52,10 @@
             fixed-width
             icon="road"
           ></font-awesome-icon>
-          <span class="fix-trans-space" v-translate
-            >Define sections and stretches</span
-          >
+          <span class="fix-trans-space" v-translate>Define stretches</span>
         </a>
+      </div>
+      <div v-if="isWaterwayAdmin">
         <small class="text-muted pl-3"> <translate>Import</translate> </small>
         <hr class="m-0" />
         <router-link to="/importsoundingresults">
--- a/client/src/components/importqueue/Importqueuedetail.vue	Fri Jan 18 06:51:49 2019 +0000
+++ b/client/src/components/importqueue/Importqueuedetail.vue	Tue Jan 22 11:22:17 2019 +0100
@@ -8,7 +8,7 @@
         {{ formatDate(job.enqueued) }}
       </div>
       <div @click="showDetails(job.id)" class="kind mt-1 mr-2">
-        {{ job.kind }}
+        {{ job.kind.toUpperCase() }}
       </div>
       <div @click="showDetails(job.id)" class="user mt-1 mr-2">
         {{ job.user }}
@@ -67,7 +67,7 @@
               class="detailsrow"
             >
               <td class="type">
-                <span class="condensed">{{ entry.kind }}</span>
+                <span class="condensed">{{ entry.kind.toUpperCase() }}</span>
               </td>
               <td class="datetime">
                 <span class="condensed">{{ formatDateTime(entry.time) }}</span>
--- a/client/src/components/importschedule/Importschedule.vue	Fri Jan 18 06:51:49 2019 +0000
+++ b/client/src/components/importschedule/Importschedule.vue	Tue Jan 22 11:22:17 2019 +0100
@@ -44,7 +44,7 @@
               <tbody>
                 <tr v-for="schedule in schedules" :key="schedule.id">
                   <td>{{ schedule.id }}</td>
-                  <td>{{ schedule.kind }}</td>
+                  <td>{{ schedule.kind.toUpperCase() }}</td>
                   <td>{{ schedule.user }}</td>
                   <td>{{ schedule.cron }}</td>
                   <td>
--- a/client/src/components/importschedule/Importscheduledetail.vue	Fri Jan 18 06:51:49 2019 +0000
+++ b/client/src/components/importschedule/Importscheduledetail.vue	Tue Jan 22 11:22:17 2019 +0100
@@ -246,7 +246,7 @@
                     class="form-control"
                     type="number"
                   />
-                  <div class="ml-2 my-auto">m</div>
+                  <div class="ml-2 my-auto">&nbsp;m</div>
                 </div>
                 <div v-if="!minWidth" class="d-flex flex-row">
                   <small
@@ -268,7 +268,7 @@
                     class="form-control"
                     type="number"
                   />
-                  <div class="ml-2 my-auto">m</div>
+                  <div class="ml-2 my-auto">&nbsp;m</div>
                 </div>
                 <div v-if="!maxWidth" class="d-flex flex-row">
                   <small
--- a/client/src/components/layers/Layerselect.vue	Fri Jan 18 06:51:49 2019 +0000
+++ b/client/src/components/layers/Layerselect.vue	Tue Jan 22 11:22:17 2019 +0100
@@ -17,7 +17,7 @@
       }}</label>
     </div>
     <div v-if="isVisible && layername == 'Bottleneck isolines'">
-      <img class="rounded my-1 d-block" :src="isolinesLegendImgUrl" />
+      <img class="rounded my-1 d-block" :src="isolinesLegendImgDataURL" />
     </div>
   </div>
 </template>
@@ -38,25 +38,25 @@
  * SPDX-License-Identifier: AGPL-3.0-or-later
  * License-Filename: LICENSES/AGPL-3.0.txt
  *
- * Copyright (C) 2018 by via donau
+ * Copyright (C) 2018, 2019 by via donau
  *   – Österreichische Wasserstraßen-Gesellschaft mbH
  * Software engineering by Intevation GmbH
  *
  * Author(s):
- * Thomas Junk <thomas.junk@intevation.de>
+ * * Thomas Junk <thomas.junk@intevation.de>
+ * * Bernhard Reiter <bernhard.reiter@intevation.de>
  */
 import { HTTP } from "@/lib/http";
+import { mapState } from "vuex";
 export default {
   props: ["layername", "layerindex", "isVisible"],
   name: "layerselect",
-  data() {
-    return {
-      isolinesLegendImgUrl: ""
-    };
-  },
   components: {
     LegendElement: () => import("./LegendElement.vue")
   },
+  computed: {
+    ...mapState("map", ["isolinesLegendImgDataURL"])
+  },
   methods: {
     visibilityToggled() {
       this.$emit("visibilityToggled", this.layerindex);
@@ -64,7 +64,7 @@
   },
   created() {
     // fetch legend image for bottleneck isolines
-    // TODO: move to store
+    // directly read it as dataURL so it is reusable later
     if (this.layername == "Bottleneck isolines") {
       const src =
         "/internal/wms?REQUEST=GetLegendGraphic&VERSION=1.0.0&FORMAT=image/png&WIDTH=20&HEIGHT=20&LAYER=sounding_results_contour_lines_geoserver&legend_options=columns:4;fontAntiAliasing:true";
@@ -75,8 +75,12 @@
         },
         responseType: "blob"
       }).then(response => {
-        var urlCreator = window.URL || window.webkitURL;
-        this.isolinesLegendImgUrl = urlCreator.createObjectURL(response.data);
+        var that = this;
+        const reader = new FileReader();
+        reader.onload = function() {
+          that.$store.commit("map/isolinesLegendImgDataURL", this.result);
+        };
+        reader.readAsDataURL(response.data);
       });
     }
   }
--- a/client/src/components/staging/StagingDetail.vue	Fri Jan 18 06:51:49 2019 +0000
+++ b/client/src/components/staging/StagingDetail.vue	Tue Jan 22 11:22:17 2019 +0100
@@ -3,23 +3,25 @@
     <div class="d-flex flex-row">
       <div class="mt-auto d-flex flex-row mb-auto small name text-left">
         <a
-          v-if="!isBottleneck(data.kind.toUpperCase())"
+          v-if="isSoundingResult(data.kind.toUpperCase())"
           @click="zoomTo()"
           href="#"
           >{{ data.summary.bottleneck }}</a
         >
-        <span v-else class="text-left"
+        <span v-if="isBottleneck(data.kind.toUpperCase())" class="text-left"
           ><translate>Bottlenecks</translate> ({{
             data.summary.bottlenecks.length
           }})</span
         >
+        <span v-if="isFairwayDimension(data.kind.toUpperCase())">-</span>
       </div>
       <div class="mt-auto mb-auto small text-left type">
         {{ data.kind.toUpperCase() }}
       </div>
-      <div class="mt-auto mb-auto small text-left date">
+      <div v-if="data.summary" class="mt-auto mb-auto small text-left date">
         {{ formatSurveyDate(data.summary.date) }}
       </div>
+      <div v-else class="mt-auto mb-auto small text-left date">-</div>
       <div class="mt-auto mb-auto small text-left imported">
         {{ formatSurveyDate(data.enqueued.split("T")[0]) }}
       </div>
@@ -248,9 +250,15 @@
           });
         });
     },
+    isFairwayDimension(kind) {
+      return kind === "FD";
+    },
     isBottleneck(kind) {
       return kind === "BN";
     },
+    isSoundingResult(kind) {
+      return kind === "SR";
+    },
     formatSurveyDate(date) {
       return formatSurveyDate(date);
     },
--- a/client/src/locale/bg_BG/LC_MESSAGES/app.po	Fri Jan 18 06:51:49 2019 +0000
+++ b/client/src/locale/bg_BG/LC_MESSAGES/app.po	Tue Jan 22 11:22:17 2019 +0100
@@ -18,7 +18,11 @@
 "Plural-Forms: nplurals=2; plural=n != 1;\n"
 "X-Generator: Weblate 3.4-dev\n"
 
-#: src/components/importschedule/Importscheduledetail.vue:565
+#: src/components/Pdftool.vue:402
+msgid "– printed by:"
+msgstr ""
+
+#: src/components/importschedule/Importscheduledetail.vue:452
 msgid "15 minutes"
 msgstr ""
 
@@ -91,7 +95,7 @@
 #: src/components/ImportSoundingresults.vue:16
 #, fuzzy
 msgid "Bottleneck"
-msgstr "Критични участъци"
+msgstr ""
 
 #: src/components/Systemconfiguration.vue:19
 msgid "Bottleneck Areas fill-color"
@@ -149,12 +153,7 @@
 #: src/components/usermanagement/Userdetail.vue:33
 #, fuzzy
 msgid "Country"
-msgstr "Държава"
-
-#: src/components/ImportStretches.vue:166
-#, fuzzy
-msgid "Countrycode"
-msgstr "Държава"
+msgstr ""
 
 #: src/components/importschedule/Importscheduledetail.vue:495
 msgid "Cronstring"
@@ -198,11 +197,7 @@
 msgid "Depthreference"
 msgstr ""
 
-#: src/components/importschedule/Importscheduledetail.vue:46
-msgid "Distance Marks Virtual"
-msgstr ""
-
-#: src/components/Pdftool.vue:36
+#: src/components/Pdftool.vue:10
 msgid "Download"
 msgstr ""
 
@@ -410,7 +405,7 @@
 #: src/components/layers/Layers.vue:10
 #, fuzzy
 msgid "Layers"
-msgstr "Слоеве"
+msgstr ""
 
 #: src/components/Login.vue:58
 msgid "Login"
@@ -488,7 +483,7 @@
 #: src/components/Bottlenecks.vue:9 src/components/staging/Staging.vue:11
 #, fuzzy
 msgid "Name"
-msgstr "име"
+msgstr ""
 
 #: src/components/ImportStretches.vue:110
 msgid "National Object name"
@@ -576,7 +571,7 @@
 #: src/components/usermanagement/Userdetail.vue:331
 #, fuzzy
 msgid "Please choose a country"
-msgstr "Моля, изберете държава"
+msgstr ""
 
 #: src/components/usermanagement/Userdetail.vue:336
 msgid "Please choose a role"
@@ -632,10 +627,6 @@
 #: src/components/ImportStretches.vue:156
 #, fuzzy
 msgid "Please enter a source"
-msgstr "Моля, изберете държава"
-
-#: src/components/importschedule/Importscheduledetail.vue:297
-msgid "Please enter a source orgranization"
 msgstr ""
 
 #: src/components/ImportStretches.vue:61
@@ -892,7 +883,7 @@
 #: src/components/staging/Staging.vue:12
 #, fuzzy
 msgid "Type"
-msgstr "Тип"
+msgstr ""
 
 #: src/components/ImportWaterwayProfiles.vue:89
 msgid "under construction"
--- a/client/src/locale/de_AT/LC_MESSAGES/app.po	Fri Jan 18 06:51:49 2019 +0000
+++ b/client/src/locale/de_AT/LC_MESSAGES/app.po	Tue Jan 22 11:22:17 2019 +0100
@@ -196,11 +196,7 @@
 msgid "Depthreference"
 msgstr "Tiefenreferenz"
 
-#: src/components/importschedule/Importscheduledetail.vue:46
-msgid "Distance Marks Virtual"
-msgstr ""
-
-#: src/components/Pdftool.vue:36
+#: src/components/Pdftool.vue:10
 msgid "Download"
 msgstr "Herunterladen"
 
@@ -265,7 +261,7 @@
 msgid "Fairwaydimension"
 msgstr "Waterway-Admin"
 
-#: src/components/importschedule/Importscheduledetail.vue:158
+#: src/components/importschedule/Importscheduledetail.vue:155
 msgid "Featuretype"
 msgstr ""
 
--- a/client/src/locale/en_GB/LC_MESSAGES/app.po	Fri Jan 18 06:51:49 2019 +0000
+++ b/client/src/locale/en_GB/LC_MESSAGES/app.po	Tue Jan 22 11:22:17 2019 +0100
@@ -194,11 +194,7 @@
 msgid "Depthreference"
 msgstr ""
 
-#: src/components/importschedule/Importscheduledetail.vue:46
-msgid "Distance Marks Virtual"
-msgstr ""
-
-#: src/components/Pdftool.vue:36
+#: src/components/Pdftool.vue:10
 msgid "Download"
 msgstr ""
 
--- a/client/src/locale/hr_HR/LC_MESSAGES/app.po	Fri Jan 18 06:51:49 2019 +0000
+++ b/client/src/locale/hr_HR/LC_MESSAGES/app.po	Tue Jan 22 11:22:17 2019 +0100
@@ -18,7 +18,11 @@
 "Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n"
 "X-Generator: Weblate 3.4-dev\n"
 
-#: src/components/importschedule/Importscheduledetail.vue:565
+#: src/components/Pdftool.vue:402
+msgid "– printed by:"
+msgstr ""
+
+#: src/components/importschedule/Importscheduledetail.vue:452
 msgid "15 minutes"
 msgstr ""
 
@@ -91,7 +95,7 @@
 #: src/components/ImportSoundingresults.vue:16
 #, fuzzy
 msgid "Bottleneck"
-msgstr "Kritični sektor"
+msgstr ""
 
 #: src/components/Systemconfiguration.vue:19
 msgid "Bottleneck Areas fill-color"
@@ -197,11 +201,7 @@
 msgid "Depthreference"
 msgstr ""
 
-#: src/components/importschedule/Importscheduledetail.vue:46
-msgid "Distance Marks Virtual"
-msgstr ""
-
-#: src/components/Pdftool.vue:36
+#: src/components/Pdftool.vue:10
 msgid "Download"
 msgstr ""
 
@@ -220,7 +220,7 @@
 #: src/components/importschedule/Importscheduledetail.vue:53
 #, fuzzy
 msgid "Email Notification"
-msgstr "E-mail Obavijest"
+msgstr ""
 
 #: src/components/ImportStretches.vue:66
 msgid "End rhm"
@@ -630,10 +630,6 @@
 #: src/components/ImportStretches.vue:156
 #, fuzzy
 msgid "Please enter a source"
-msgstr "Odaberite zemlju"
-
-#: src/components/importschedule/Importscheduledetail.vue:297
-msgid "Please enter a source orgranization"
 msgstr ""
 
 #: src/components/ImportStretches.vue:61
--- a/client/src/locale/hu_HU/LC_MESSAGES/app.po	Fri Jan 18 06:51:49 2019 +0000
+++ b/client/src/locale/hu_HU/LC_MESSAGES/app.po	Tue Jan 22 11:22:17 2019 +0100
@@ -194,11 +194,7 @@
 msgid "Depthreference"
 msgstr ""
 
-#: src/components/importschedule/Importscheduledetail.vue:46
-msgid "Distance Marks Virtual"
-msgstr ""
-
-#: src/components/Pdftool.vue:36
+#: src/components/Pdftool.vue:10
 msgid "Download"
 msgstr ""
 
--- a/client/src/locale/sk_SK/LC_MESSAGES/app.po	Fri Jan 18 06:51:49 2019 +0000
+++ b/client/src/locale/sk_SK/LC_MESSAGES/app.po	Tue Jan 22 11:22:17 2019 +0100
@@ -13,7 +13,11 @@
 "Plural-Forms: nplurals=2; plural=(n != 1);\n"
 "X-Generator: Weblate 3.4-dev\n"
 
-#: src/components/importschedule/Importscheduledetail.vue:565
+#: src/components/Pdftool.vue:402
+msgid "– printed by:"
+msgstr ""
+
+#: src/components/importschedule/Importscheduledetail.vue:452
 msgid "15 minutes"
 msgstr ""
 
@@ -191,11 +195,7 @@
 msgid "Depthreference"
 msgstr ""
 
-#: src/components/importschedule/Importscheduledetail.vue:46
-msgid "Distance Marks Virtual"
-msgstr ""
-
-#: src/components/Pdftool.vue:36
+#: src/components/Pdftool.vue:10
 msgid "Download"
 msgstr ""
 
@@ -214,7 +214,7 @@
 #: src/components/importschedule/Importscheduledetail.vue:53
 #, fuzzy
 msgid "Email Notification"
-msgstr "Email Oznámenia"
+msgstr ""
 
 #: src/components/ImportStretches.vue:66
 msgid "End rhm"
@@ -437,7 +437,7 @@
 #: src/components/Sidebar.vue:15
 #, fuzzy
 msgid "Map"
-msgstr "Mapa"
+msgstr ""
 
 #: src/components/importschedule/Importscheduledetail.vue:584
 msgid "March"
@@ -625,10 +625,6 @@
 #: src/components/ImportStretches.vue:156
 #, fuzzy
 msgid "Please enter a source"
-msgstr "Vyberte krajinu"
-
-#: src/components/importschedule/Importscheduledetail.vue:297
-msgid "Please enter a source orgranization"
 msgstr ""
 
 #: src/components/ImportStretches.vue:61
--- a/client/src/store/application.js	Fri Jan 18 06:51:49 2019 +0000
+++ b/client/src/store/application.js	Tue Jan 22 11:22:17 2019 +0100
@@ -21,6 +21,7 @@
   return {
     appTitle: process.env.VUE_APP_TITLE,
     secondaryLogo: process.env.VUE_APP_SECONDARY_LOGO_URL,
+    logoForPDF: process.env.VUE_APP_LOGO_FOR_PDF_URL,
     showSidebar: false,
     showUsermenu: false,
     showSplitscreen: false,
--- a/client/src/store/imports.js	Fri Jan 18 06:51:49 2019 +0000
+++ b/client/src/store/imports.js	Tue Jan 22 11:22:17 2019 +0100
@@ -14,6 +14,7 @@
 
 import { HTTP } from "@/lib/http";
 import Vue from "vue";
+import { WFS } from "ol/format.js";
 
 /* eslint-disable no-unused-vars */
 /* eslint-disable no-unreachable */
@@ -97,6 +98,7 @@
 // initial state
 const init = () => {
   return {
+    stretches: [],
     imports: [],
     staging: [],
     schedules: [],
@@ -111,6 +113,9 @@
   namespaced: true,
   state: init(),
   mutations: {
+    setStretches: (state, stretches) => {
+      state.stretches = stretches;
+    },
     clearCurrentSchedule: state => {
       state.currentSchedule = initializeCurrentSchedule();
     },
@@ -210,6 +215,53 @@
     }
   },
   actions: {
+    loadStretches({ commit }) {
+      return new Promise((resolve, reject) => {
+        var stretchesFeatureCollectionRequest = new WFS().writeGetFeature({
+          srsName: "EPSG:4326",
+          featureNS: "gemma",
+          featurePrefix: "gemma",
+          featureTypes: ["stretches_geoserver"],
+          outputFormat: "application/json"
+        });
+        HTTP.post(
+          "/internal/wfs",
+          new XMLSerializer().serializeToString(
+            stretchesFeatureCollectionRequest
+          ),
+          {
+            headers: {
+              "X-Gemma-Auth": localStorage.getItem("token"),
+              "Content-type": "text/xml; charset=UTF-8"
+            }
+          }
+        )
+          .then(response => {
+            if (response.data.features) {
+              commit("setStretches", response.data.features);
+            } else {
+              commit("setStretches", []);
+            }
+            resolve(response);
+          })
+          .catch(error => {
+            reject(error);
+          });
+      });
+    },
+    saveStretch({ commit }, stretch) {
+      return new Promise((resolve, reject) => {
+        HTTP.post("/imports/stretch", stretch, {
+          headers: { "X-Gemma-Auth": localStorage.getItem("token") }
+        })
+          .then(response => {
+            resolve(response);
+          })
+          .catch(error => {
+            reject(error);
+          });
+      });
+    },
     loadSchedule({ commit }, id) {
       return new Promise((resolve, reject) => {
         HTTP.get("/imports/config/" + id, {
--- a/client/src/store/map.js	Fri Jan 18 06:51:49 2019 +0000
+++ b/client/src/store/map.js	Tue Jan 22 11:22:17 2019 +0100
@@ -4,13 +4,14 @@
  * SPDX-License-Identifier: AGPL-3.0-or-later
  * License-Filename: LICENSES/AGPL-3.0.txt
  *
- * Copyright (C) 2018 by via donau
+ * Copyright (C) 2018, 2019 by via donau
  *   – Österreichische Wasserstraßen-Gesellschaft mbH
  * Software engineering by Intevation GmbH
  *
  * Author(s):
- * Markus Kottländer <markus@intevation.de>
- * Thomas Junk <thomas.junk@intevation.de>
+ * * Bernhard Reiter <bernhard.reiter@intevation.de>
+ * * Markus Kottländer <markus@intevation.de>
+ * * Thomas Junk <thomas.junk@intevation.de>
  */
 
 //import { HTTP } from "../lib/http";
@@ -45,6 +46,7 @@
     lineTool: null, // open layers interaction object (Draw)
     polygonTool: null, // open layers interaction object (Draw)
     cutTool: null, // open layers interaction object (Draw)
+    isolinesLegendImgDataURL: "",
     layers: [
       {
         name: "Open Streetmap",
@@ -60,6 +62,7 @@
           source: new TileWMS({
             preload: 1,
             url: "https://service.d4d-portal.info/wms/",
+            crossOrigin: "anonymous",
             params: { LAYERS: "d4d", VERSION: "1.1.1", TILED: true }
           })
         }),
@@ -67,6 +70,41 @@
         showInLegend: true
       },
       {
+        name: "Waterway Area",
+        data: new VectorLayer({
+          source: new VectorSource({
+            strategy: bboxStrategy
+          }),
+          style: new Style({
+            stroke: new Stroke({
+              color: "rgba(0, 102, 0, 1)",
+              width: 2
+            })
+          })
+        }),
+        isVisible: true,
+        showInLegend: true
+      },
+      {
+        name: "Stretches",
+        data: new VectorLayer({
+          source: new VectorSource({
+            strategy: bboxStrategy
+          }),
+          style: new Style({
+            stroke: new Stroke({
+              color: "rgba(250, 200, 0, .8)",
+              width: 2
+            }),
+            fill: new Fill({
+              color: "rgba(250, 200, 10, .3)"
+            })
+          })
+        }),
+        isVisible: false,
+        showInLegend: true
+      },
+      {
         name: "Fairway Dimensions",
         data: new VectorLayer({
           source: new VectorSource(),
@@ -96,38 +134,6 @@
         showInLegend: true
       },
       {
-        name: "Waterway Area, named",
-        data: new VectorLayer({
-          source: new VectorSource({
-            strategy: bboxStrategy
-          }),
-          style: new Style({
-            stroke: new Stroke({
-              color: "rgba(0, 132, 0, 1)",
-              width: 2
-            })
-          })
-        }),
-        isVisible: false,
-        showInLegend: true
-      },
-      {
-        name: "Waterway Area",
-        data: new VectorLayer({
-          source: new VectorSource({
-            strategy: bboxStrategy
-          }),
-          style: new Style({
-            stroke: new Stroke({
-              color: "rgba(0, 102, 0, 1)",
-              width: 2
-            })
-          })
-        }),
-        isVisible: true,
-        showInLegend: true
-      },
-      {
         name: "Waterway Axis",
         data: new VectorLayer({
           source: new VectorSource({
@@ -145,17 +151,6 @@
         showInLegend: true
       },
       {
-        name: "Distance marks",
-        forLegendStyle: { point: true, resolution: 8 },
-        data: new VectorLayer({
-          source: new VectorSource({
-            strategy: bboxStrategy
-          })
-        }),
-        isVisible: false,
-        showInLegend: true
-      },
-      {
         name: "Bottlenecks",
         data: new VectorLayer({
           source: new VectorSource({
@@ -203,6 +198,17 @@
         showInLegend: true
       },
       {
+        name: "Distance marks",
+        forLegendStyle: { point: true, resolution: 8 },
+        data: new VectorLayer({
+          source: new VectorSource({
+            strategy: bboxStrategy
+          })
+        }),
+        isVisible: false,
+        showInLegend: true
+      },
+      {
         name: "Distance marks, Axis",
         forLegendStyle: { point: true, resolution: 8 },
         data: new VectorLayer({
@@ -392,6 +398,9 @@
         center: fromLonLat(coordinates, view.getProjection()),
         duration: 700
       });
+    },
+    isolinesLegendImgDataURL: (state, isolinesLegendImgDataURL) => {
+      state.isolinesLegendImgDataURL = isolinesLegendImgDataURL;
     }
   },
   actions: {
--- a/client/yarn.lock	Fri Jan 18 06:51:49 2019 +0000
+++ b/client/yarn.lock	Tue Jan 22 11:22:17 2019 +0100
@@ -1157,6 +1157,11 @@
   resolved "https://registry.yarnpkg.com/@xtuc/long/-/long-4.2.1.tgz#5c85d662f76fa1d34575766c5dcd6615abcd30d8"
   integrity sha512-FZdkNBDqBRHKQ2MEbSC17xnPFOhZxeJ2YGSfr2BKf3sujG49Qe3bB+rGCwQfIaA7WHnGeGkSijX4FuBCdrzW/g==
 
+abab@^1.0.0:
+  version "1.0.4"
+  resolved "https://registry.yarnpkg.com/abab/-/abab-1.0.4.tgz#5faad9c2c07f60dd76770f71cf025b62a63cfd4e"
+  integrity sha1-X6rZwsB/YN12dw9xzwJbYqY8/U4=
+
 abab@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.0.tgz#aba0ab4c5eee2d4c79d3487d85450fb2376ebb0f"
@@ -1196,6 +1201,13 @@
   dependencies:
     acorn "^5.0.0"
 
+acorn-globals@^1.0.4:
+  version "1.0.9"
+  resolved "https://registry.yarnpkg.com/acorn-globals/-/acorn-globals-1.0.9.tgz#55bb5e98691507b74579d0513413217c380c54cf"
+  integrity sha1-VbtemGkVB7dFedBRNBMhfDgMVM8=
+  dependencies:
+    acorn "^2.1.0"
+
 acorn-globals@^3.0.0:
   version "3.1.0"
   resolved "https://registry.yarnpkg.com/acorn-globals/-/acorn-globals-3.1.0.tgz#fd8270f71fbb4996b004fa880ee5d46573a731bf"
@@ -1273,6 +1285,11 @@
   resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-6.1.1.tgz#d363b66f5fac5f018ff9c3a1e7b6f8e310cc3913"
   integrity sha512-OtUw6JUTgxA2QoqqmrmQ7F2NYqiBPi/L2jqHyFtllhOUvXYQXf0Z1CYUinIfyT4bTCGmrA7gX9FvHA81uzCoVw==
 
+acorn@^2.1.0, acorn@^2.4.0:
+  version "2.7.0"
+  resolved "https://registry.yarnpkg.com/acorn/-/acorn-2.7.0.tgz#ab6e7d9d886aaca8b085bc3312b79a198433f0e7"
+  integrity sha1-q259nYhqrKiwhbwzEreaGYQz8Oc=
+
 acorn@^3.0.4, acorn@^3.1.0:
   version "3.3.0"
   resolved "https://registry.yarnpkg.com/acorn/-/acorn-3.3.0.tgz#45e37fb39e8da3f25baee3ff5369e2bb5f22017a"
@@ -1331,7 +1348,7 @@
     fast-json-stable-stringify "^2.0.0"
     json-schema-traverse "^0.3.0"
 
-ajv@^6.1.0, ajv@^6.5.5:
+ajv@^6.1.0:
   version "6.6.1"
   resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.6.1.tgz#6360f5ed0d80f232cc2b294c362d5dc2e538dd61"
   integrity sha512-ZoJjft5B+EJBjUyu9C9Hc0OZyPZSSlOF+plzouTrg6UlA8f+e/n8NIgBFG/9tppJtpPWfthHakK7juJdNDODww==
@@ -1341,6 +1358,16 @@
     json-schema-traverse "^0.4.1"
     uri-js "^4.2.2"
 
+ajv@^6.5.5:
+  version "6.7.0"
+  resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.7.0.tgz#e3ce7bb372d6577bb1839f1dfdfcbf5ad2948d96"
+  integrity sha512-RZXPviBTtfmtka9n9sy1N5M5b82CbxWIR6HIis4s3WQTXDJamc/0gpCWNGz6EWdWp4DOfjzJfhz/AS9zVPjjWg==
+  dependencies:
+    fast-deep-equal "^2.0.1"
+    fast-json-stable-stringify "^2.0.0"
+    json-schema-traverse "^0.4.1"
+    uri-js "^4.2.2"
+
 align-text@^0.1.1, align-text@^0.1.3:
   version "0.1.4"
   resolved "https://registry.yarnpkg.com/align-text/-/align-text-0.1.4.tgz#0cd90a561093f35d0a99256c22b7069433fad117"
@@ -1876,6 +1903,11 @@
   resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767"
   integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c=
 
+base64-arraybuffer@^0.1.5:
+  version "0.1.5"
+  resolved "https://registry.yarnpkg.com/base64-arraybuffer/-/base64-arraybuffer-0.1.5.tgz#73926771923b5a19747ad666aa5cd4bf9c6e9ce8"
+  integrity sha1-c5JncZI7Whl0etZmqlzUv5xunOg=
+
 base64-js@^1.0.2:
   version "1.3.0"
   resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.3.0.tgz#cab1e6118f051095e58b5281aea8c1cd22bfc0e3"
@@ -2314,6 +2346,16 @@
   resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30000913.tgz#560311ecf242eaf12159b720e64b11ebd759b5e4"
   integrity sha512-PP7Ypc35XY1mNduHqweGNOp0qfNUCmaQauGOYDByvirlFjrzRyY72pBRx7jnBidOB8zclg00DAzsy2H475BouQ==
 
+canvg@1.5.3:
+  version "1.5.3"
+  resolved "https://registry.yarnpkg.com/canvg/-/canvg-1.5.3.tgz#aad17915f33368bf8eb80b25d129e3ae922ddc5f"
+  integrity sha512-7Gn2IuQzvUQWPIuZuFHrzsTM0gkPz2RRT9OcbdmA03jeKk8kltrD8gqUzNX15ghY/4PV5bbe5lmD6yDLDY6Ybg==
+  dependencies:
+    jsdom "^8.1.0"
+    rgbcolor "^1.0.1"
+    stackblur-canvas "^1.4.1"
+    xmldom "^0.1.22"
+
 capture-exit@^1.2.0:
   version "1.2.0"
   resolved "https://registry.yarnpkg.com/capture-exit/-/capture-exit-1.2.0.tgz#1c5fcc489fd0ab00d4f1ac7ae1072e3173fbab6f"
@@ -2945,6 +2987,13 @@
     postcss "^7.0.1"
     timsort "^0.3.0"
 
+css-line-break@1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/css-line-break/-/css-line-break-1.0.1.tgz#19f2063a33e95fb2831b86446c0b80c188af450a"
+  integrity sha1-GfIGOjPpX7KDG4ZEbAuAwYivRQo=
+  dependencies:
+    base64-arraybuffer "^0.1.5"
+
 css-loader@^1.0.1:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-1.0.1.tgz#6885bb5233b35ec47b006057da01cc640b6b79fe"
@@ -3123,11 +3172,18 @@
   dependencies:
     css-tree "1.0.0-alpha.29"
 
-cssom@0.3.x, "cssom@>= 0.3.2 < 0.4.0":
+cssom@0.3.x, "cssom@>= 0.3.0 < 0.4.0", "cssom@>= 0.3.2 < 0.4.0":
   version "0.3.4"
   resolved "https://registry.yarnpkg.com/cssom/-/cssom-0.3.4.tgz#8cd52e8a3acfd68d3aed38ee0a640177d2f9d797"
   integrity sha512-+7prCSORpXNeR4/fUP3rL+TzqtiFfhMvTd7uEqMdgPvLPt4+uzFUeufx5RHjGTACCargg/DiEt/moMQmvnfkog==
 
+"cssstyle@>= 0.2.34 < 0.3.0":
+  version "0.2.37"
+  resolved "https://registry.yarnpkg.com/cssstyle/-/cssstyle-0.2.37.tgz#541097234cb2513c83ceed3acddc27ff27987d54"
+  integrity sha1-VBCXI0yyUTyDzu06zdwn/yeYfVQ=
+  dependencies:
+    cssom "0.3.x"
+
 cssstyle@^1.0.0:
   version "1.1.1"
   resolved "https://registry.yarnpkg.com/cssstyle/-/cssstyle-1.1.1.tgz#18b038a9c44d65f7a8e428a653b9f6fe42faf5fb"
@@ -3966,7 +4022,7 @@
   resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4"
   integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=
 
-escodegen@1.x.x, escodegen@^1.9.1:
+escodegen@1.x.x, escodegen@^1.6.1, escodegen@^1.9.1:
   version "1.11.0"
   resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-1.11.0.tgz#b27a9389481d5bfd5bec76f7bb1eb3f8f4556589"
   integrity sha512-IeMV45ReixHS53K/OmfKAIztN/igDHzTJUhZM3k1jMhIZWjk45SMwAtBsEXiJp3vSPmTcu6CXn7mDvFHRN66fw==
@@ -4474,6 +4530,10 @@
     loader-utils "^1.0.2"
     schema-utils "^1.0.0"
 
+file-saver@eligrey/FileSaver.js#1.3.8:
+  version "1.3.8"
+  resolved "https://codeload.github.com/eligrey/FileSaver.js/tar.gz/e865e37af9f9947ddcced76b549e27dc45c1cb2e"
+
 file-uri-to-path@1:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz#553a7b8446ff6f684359c445f1e37a05dacc33dd"
@@ -5229,6 +5289,13 @@
     toposort "^1.0.0"
     util.promisify "1.0.0"
 
+html2canvas@1.0.0-alpha.12:
+  version "1.0.0-alpha.12"
+  resolved "https://registry.yarnpkg.com/html2canvas/-/html2canvas-1.0.0-alpha.12.tgz#3b1992e3c9b3f56063c35fd620494f37eba88513"
+  integrity sha1-OxmS48mz9WBjw1/WIElPN+uohRM=
+  dependencies:
+    css-line-break "1.0.1"
+
 htmlparser2@^3.9.1:
   version "3.10.0"
   resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-3.10.0.tgz#5f5e422dcf6119c0d983ed36260ce9ded0bee464"
@@ -5322,7 +5389,7 @@
     debug "2"
     extend "3"
 
-iconv-lite@0.4, iconv-lite@0.4.24, iconv-lite@^0.4.17, iconv-lite@^0.4.4:
+iconv-lite@0.4, iconv-lite@0.4.24, iconv-lite@^0.4.13, iconv-lite@^0.4.17, iconv-lite@^0.4.4:
   version "0.4.24"
   resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b"
   integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==
@@ -6413,6 +6480,29 @@
     ws "^5.2.0"
     xml-name-validator "^3.0.0"
 
+jsdom@^8.1.0:
+  version "8.5.0"
+  resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-8.5.0.tgz#d4d8f5dbf2768635b62a62823b947cf7071ebc98"
+  integrity sha1-1Nj12/J2hjW2KmKCO5R89wcevJg=
+  dependencies:
+    abab "^1.0.0"
+    acorn "^2.4.0"
+    acorn-globals "^1.0.4"
+    array-equal "^1.0.0"
+    cssom ">= 0.3.0 < 0.4.0"
+    cssstyle ">= 0.2.34 < 0.3.0"
+    escodegen "^1.6.1"
+    iconv-lite "^0.4.13"
+    nwmatcher ">= 1.3.7 < 2.0.0"
+    parse5 "^1.5.1"
+    request "^2.55.0"
+    sax "^1.1.4"
+    symbol-tree ">= 3.1.0 < 4.0.0"
+    tough-cookie "^2.2.0"
+    webidl-conversions "^3.0.1"
+    whatwg-url "^2.0.1"
+    xml-name-validator ">= 2.0.1 < 3.0.0"
+
 jsesc@^1.3.0:
   version "1.3.0"
   resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-1.3.0.tgz#46c3fec8c1892b12b0833db9bc7622176dbab34b"
@@ -6487,6 +6577,18 @@
   resolved "https://registry.yarnpkg.com/jsonify/-/jsonify-0.0.0.tgz#2c74b6ee41d93ca51b7b5aaee8f503631d252a73"
   integrity sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM=
 
+jspdf@^1.5.3:
+  version "1.5.3"
+  resolved "https://registry.yarnpkg.com/jspdf/-/jspdf-1.5.3.tgz#5a12c011479defabef5735de55c913060ed219f2"
+  integrity sha512-J9X76xnncMw+wIqb15HeWfPMqPwYxSpPY8yWPJ7rAZN/ZDzFkjCSZObryCyUe8zbrVRNiuCnIeQteCzMn7GnWw==
+  dependencies:
+    canvg "1.5.3"
+    file-saver eligrey/FileSaver.js#1.3.8
+    html2canvas "1.0.0-alpha.12"
+    omggif "1.0.7"
+    promise-polyfill "8.1.0"
+    stackblur-canvas "2.2.0"
+
 jsprim@^1.2.2:
   version "1.4.1"
   resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.1.tgz#313e66bc1e5cc06e438bc1b7499c2e5c56acb6a2"
@@ -7623,6 +7725,11 @@
   resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d"
   integrity sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=
 
+"nwmatcher@>= 1.3.7 < 2.0.0":
+  version "1.4.4"
+  resolved "https://registry.yarnpkg.com/nwmatcher/-/nwmatcher-1.4.4.tgz#2285631f34a95f0d0395cd900c96ed39b58f346e"
+  integrity sha512-3iuY4N5dhgMpCUrOVnuAdGrgxVqV2cJpM+XNccjR2DKOB1RUP0aA+wGXEiNziG/UKboFyGBIoKOaNlJxx8bciQ==
+
 nwsapi@^2.0.7:
   version "2.0.9"
   resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.0.9.tgz#77ac0cdfdcad52b6a1151a84e73254edc33ed016"
@@ -7721,6 +7828,11 @@
     pixelworks "1.1.0"
     rbush "2.0.2"
 
+omggif@1.0.7:
+  version "1.0.7"
+  resolved "https://registry.yarnpkg.com/omggif/-/omggif-1.0.7.tgz#59d2eecb0263de84635b3feb887c0c9973f1e49d"
+  integrity sha1-WdLuywJj3oRjWz/riHwMmXPx5J0=
+
 on-finished@~2.3.0:
   version "2.3.0"
   resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947"
@@ -8004,6 +8116,11 @@
   resolved "https://registry.yarnpkg.com/parse5/-/parse5-4.0.0.tgz#6d78656e3da8d78b4ec0b906f7c08ef1dfe3f608"
   integrity sha512-VrZ7eOd3T1Fk4XWNXMgiGBK/z0MG48BWG2uQNU4I72fkQuKUTZpl+u9k+CxEG0twMVzSmXEEz12z5Fnw1jIQFA==
 
+parse5@^1.5.1:
+  version "1.5.1"
+  resolved "https://registry.yarnpkg.com/parse5/-/parse5-1.5.1.tgz#9b7f3b0de32be78dc2401b17573ccaf0f6f59d94"
+  integrity sha1-m387DeMr543CQBsXVzzK8Pb1nZQ=
+
 parse5@^3.0.1:
   version "3.0.3"
   resolved "https://registry.yarnpkg.com/parse5/-/parse5-3.0.3.tgz#042f792ffdd36851551cf4e9e066b3874ab45b5c"
@@ -8629,6 +8746,11 @@
   resolved "https://registry.yarnpkg.com/promise-inflight/-/promise-inflight-1.0.1.tgz#98472870bf228132fcbdd868129bad12c3c029e3"
   integrity sha1-mEcocL8igTL8vdhoEputEsPAKeM=
 
+promise-polyfill@8.1.0:
+  version "8.1.0"
+  resolved "https://registry.yarnpkg.com/promise-polyfill/-/promise-polyfill-8.1.0.tgz#30059da54d1358ce905ac581f287e184aedf995d"
+  integrity sha512-OzSf6gcCUQ01byV4BgwyUCswlaQQ6gzXc23aLQWhicvfX9kfsUiUhgt3CCQej8jDnl8/PhGF31JdHX2/MzF3WA==
+
 promise@^7.0.1:
   version "7.3.1"
   resolved "https://registry.yarnpkg.com/promise/-/promise-7.3.1.tgz#064b72602b18f90f29192b8b1bc418ffd1ebd3bf"
@@ -8687,9 +8809,9 @@
   integrity sha1-8FKijacOYYkX7wqKw0wa5aaChrM=
 
 psl@^1.1.24, psl@^1.1.28:
-  version "1.1.29"
-  resolved "https://registry.yarnpkg.com/psl/-/psl-1.1.29.tgz#60f580d360170bb722a797cc704411e6da850c67"
-  integrity sha512-AeUmQ0oLN02flVHXWh9sSJF7mcdFq0ppid/JkErufc3hGIV/AMa8Fo9VgDo/cT2jFdOWoFvHp90qqBH54W+gjQ==
+  version "1.1.31"
+  resolved "https://registry.yarnpkg.com/psl/-/psl-1.1.31.tgz#e9aa86d0101b5b105cbe93ac6b784cd547276184"
+  integrity sha512-/6pt4+C+T+wZUieKR620OpzN/LlnNKuWjy1iFLQ/UG35JqHlR/89MP1d96dUfkf6Dne3TuLQzOYEYshJ+Hx8mw==
 
 public-encrypt@^4.0.0:
   version "4.0.3"
@@ -9197,7 +9319,7 @@
     stealthy-require "^1.1.0"
     tough-cookie ">=2.3.3"
 
-request@^2.87.0, request@^2.88.0:
+request@^2.55.0, request@^2.87.0, request@^2.88.0:
   version "2.88.0"
   resolved "https://registry.yarnpkg.com/request/-/request-2.88.0.tgz#9c2fca4f7d35b592efe57c7f0a55e81052124fef"
   integrity sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg==
@@ -9315,6 +9437,11 @@
   resolved "https://registry.yarnpkg.com/rgba-regex/-/rgba-regex-1.0.0.tgz#43374e2e2ca0968b0ef1523460b7d730ff22eeb3"
   integrity sha1-QzdOLiyglosO8VI0YLfXMP8i7rM=
 
+rgbcolor@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/rgbcolor/-/rgbcolor-1.0.1.tgz#d6505ecdb304a6595da26fa4b43307306775945d"
+  integrity sha1-1lBezbMEplldom+ktDMHMGd1lF0=
+
 right-align@^0.1.1:
   version "0.1.3"
   resolved "https://registry.yarnpkg.com/right-align/-/right-align-0.1.3.tgz#61339b722fe6a3515689210d24e14c96148613ef"
@@ -9435,7 +9562,7 @@
     pify "^3.0.0"
     semver "^5.5.0"
 
-sax@^1.2.4, sax@~1.2.4:
+sax@^1.1.4, sax@^1.2.4, sax@~1.2.4:
   version "1.2.4"
   resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9"
   integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==
@@ -9861,9 +9988,9 @@
   integrity sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=
 
 sshpk@^1.7.0:
-  version "1.15.2"
-  resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.15.2.tgz#c946d6bd9b1a39d0e8635763f5242d6ed6dcb629"
-  integrity sha512-Ra/OXQtuh0/enyl4ETZAfTaeksa6BXks5ZcjpSUNrjBr0DvrJKX+1fsKDPpT9TBXgHAFsa4510aNVgI8g/+SzA==
+  version "1.16.0"
+  resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.16.0.tgz#1d4963a2fbffe58050aa9084ca20be81741c07de"
+  integrity sha512-Zhev35/y7hRMcID/upReIvRse+I9SVhyVre/KTJSJQWMz3C3+G+HpO7m1wK/yckEtujKZ7dS4hkVxAnmHaIGVQ==
   dependencies:
     asn1 "~0.2.3"
     assert-plus "^1.0.0"
@@ -9899,6 +10026,16 @@
   resolved "https://registry.yarnpkg.com/stack-utils/-/stack-utils-1.0.2.tgz#33eba3897788558bebfc2db059dc158ec36cebb8"
   integrity sha512-MTX+MeG5U994cazkjd/9KNAapsHnibjMLnfXodlkXw76JEea0UiNzrqidzo1emMwk7w5Qhc9jd4Bn9TBb1MFwA==
 
+stackblur-canvas@2.2.0:
+  version "2.2.0"
+  resolved "https://registry.yarnpkg.com/stackblur-canvas/-/stackblur-canvas-2.2.0.tgz#cacc5924a0744b3e183eb2e6c1d8559c1a17c26e"
+  integrity sha512-5Gf8dtlf8k6NbLzuly2NkGrkS/Ahh+I5VUjO7TnFizdJtgpfpLLEdQlLe9umbcnZlitU84kfYjXE67xlSXfhfQ==
+
+stackblur-canvas@^1.4.1:
+  version "1.4.1"
+  resolved "https://registry.yarnpkg.com/stackblur-canvas/-/stackblur-canvas-1.4.1.tgz#849aa6f94b272ff26f6471fa4130ed1f7e47955b"
+  integrity sha1-hJqm+UsnL/JvZHH6QTDtH35HlVs=
+
 stackframe@^1.0.4:
   version "1.0.4"
   resolved "https://registry.yarnpkg.com/stackframe/-/stackframe-1.0.4.tgz#357b24a992f9427cba6b545d96a14ed2cbca187b"
@@ -10138,7 +10275,7 @@
     unquote "~1.1.1"
     util.promisify "~1.0.0"
 
-symbol-tree@^3.2.2:
+"symbol-tree@>= 3.1.0 < 4.0.0", symbol-tree@^3.2.2:
   version "3.2.2"
   resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.2.tgz#ae27db38f660a7ae2e1c3b7d1bc290819b8519e6"
   integrity sha1-rifbOPZgp64uHDt9G8KQgZuFGeY=
@@ -10357,7 +10494,7 @@
   resolved "https://registry.yarnpkg.com/toposort/-/toposort-1.0.7.tgz#2e68442d9f64ec720b8cc89e6443ac6caa950029"
   integrity sha1-LmhELZ9k7HILjMieZEOsbKqVACk=
 
-tough-cookie@>=2.3.3, tough-cookie@^2.3.4:
+tough-cookie@>=2.3.3, tough-cookie@^2.2.0, tough-cookie@^2.3.4:
   version "2.5.0"
   resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.5.0.tgz#cd9fb2a0aa1d5a12b473bd9fb96fa3dcff65ade2"
   integrity sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==
@@ -10380,6 +10517,11 @@
   dependencies:
     punycode "^2.1.0"
 
+tr46@~0.0.3:
+  version "0.0.3"
+  resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a"
+  integrity sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o=
+
 tree-kill@^1.1.0:
   version "1.2.1"
   resolved "https://registry.yarnpkg.com/tree-kill/-/tree-kill-1.2.1.tgz#5398f374e2f292b9dcc7b2e71e30a5c3bb6c743a"
@@ -10883,6 +11025,11 @@
   dependencies:
     defaults "^1.0.3"
 
+webidl-conversions@^3.0.0, webidl-conversions@^3.0.1:
+  version "3.0.1"
+  resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871"
+  integrity sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE=
+
 webidl-conversions@^4.0.2:
   version "4.0.2"
   resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-4.0.2.tgz#a855980b1f0b6b359ba1d5d9fb39ae941faa63ad"
@@ -11052,6 +11199,14 @@
   resolved "https://registry.yarnpkg.com/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz#3d4b1e0312d2079879f826aff18dbeeca5960fbf"
   integrity sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g==
 
+whatwg-url@^2.0.1:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-2.0.1.tgz#5396b2043f020ee6f704d9c45ea8519e724de659"
+  integrity sha1-U5ayBD8CDub3BNnEXqhRnnJN5lk=
+  dependencies:
+    tr46 "~0.0.3"
+    webidl-conversions "^3.0.0"
+
 whatwg-url@^6.4.1:
   version "6.5.0"
   resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-6.5.0.tgz#f2df02bff176fd65070df74ad5ccbb5a199965a8"
@@ -11172,11 +11327,21 @@
   dependencies:
     async-limiter "~1.0.0"
 
+"xml-name-validator@>= 2.0.1 < 3.0.0":
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-2.0.1.tgz#4d8b8f1eccd3419aa362061becef515e1e559635"
+  integrity sha1-TYuPHszTQZqjYgYb7O9RXh5VljU=
+
 xml-name-validator@^3.0.0:
   version "3.0.0"
   resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-3.0.0.tgz#6ae73e06de4d8c6e47f9fb181f78d648ad457c6a"
   integrity sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw==
 
+xmldom@^0.1.22:
+  version "0.1.27"
+  resolved "https://registry.yarnpkg.com/xmldom/-/xmldom-0.1.27.tgz#d501f97b3bdb403af8ef9ecc20573187aadac0e9"
+  integrity sha1-1QH5ezvbQDr4757MIFcxh6rawOk=
+
 xregexp@2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/xregexp/-/xregexp-2.0.0.tgz#52a63e56ca0b84a7f3a5f3d61872f126ad7a5943"
--- a/docker/Dockerfile.geoserv	Fri Jan 18 06:51:49 2019 +0000
+++ b/docker/Dockerfile.geoserv	Tue Jan 22 11:22:17 2019 +0100
@@ -14,7 +14,7 @@
 ENV CATALINA_TMPDIR /tmp/tomcat8-tmp
 
 ENV GS_URL https://downloads.sourceforge.net/project/geoserver/GeoServer
-ENV GS_VERSION 2.14.1
+ENV GS_VERSION 2.14.2
 ENV GS_DATADIR /opt/geoserver/data
 
 ENV CATALINA_OPTS="-DGEOSERVER_DATA_DIR=$GS_DATADIR"
--- a/pkg/controllers/manualimports.go	Fri Jan 18 06:51:49 2019 +0000
+++ b/pkg/controllers/manualimports.go	Tue Jan 22 11:22:17 2019 +0100
@@ -144,6 +144,22 @@
 	return dma, due, retries, dmai.SendEmail
 }
 
+func importStretch(input interface{}) (interface{}, time.Time, int, bool) {
+	sti := input.(*models.StretchImport)
+	st := &imports.Stretch{
+		Name:      sti.Name,
+		From:      sti.From,
+		To:        sti.To,
+		ObjNam:    sti.ObjNam,
+		NObjNam:   sti.NObjNam,
+		Source:    sti.Source,
+		Date:      sti.Date,
+		Countries: sti.Countries,
+	}
+	due, retries := retry(sti.Attributes)
+	return st, due, retries, sti.SendEmail
+}
+
 func manualImport(
 	kind imports.JobKind,
 	setup func(interface{}) (interface{}, time.Time, int, bool),
--- a/pkg/controllers/routes.go	Fri Jan 18 06:51:49 2019 +0000
+++ b/pkg/controllers/routes.go	Tue Jan 22 11:22:17 2019 +0100
@@ -229,6 +229,12 @@
 		NoConn: true,
 	})).Methods(http.MethodPost)
 
+	api.Handle("/imports/stretch", sysAdmin(&JSONHandler{
+		Input:  func() interface{} { return new(models.StretchImport) },
+		Handle: manualImport(imports.STJobKind, importStretch),
+		NoConn: true,
+	})).Methods(http.MethodPost)
+
 	// Import scheduler configuration
 	api.Handle("/imports/config/{id:[0-9]+}/run",
 		waterwayAdmin(&JSONHandler{
--- a/pkg/controllers/srimports.go	Fri Jan 18 06:51:49 2019 +0000
+++ b/pkg/controllers/srimports.go	Tue Jan 22 11:22:17 2019 +0100
@@ -116,11 +116,11 @@
 	}
 
 	if v := req.FormValue("date"); v != "" {
-		date, err := time.Parse(models.SoundingResultDateFormat, v)
+		date, err := time.Parse(models.DateFormat, v)
 		if err != nil {
 			return err
 		}
-		sr.Date = &models.SoundingResultDate{Time: date}
+		sr.Date = &models.Date{Time: date}
 	}
 
 	if v := req.FormValue("depth-reference"); v != "" {
--- a/pkg/imports/agm.go	Fri Jan 18 06:51:49 2019 +0000
+++ b/pkg/imports/agm.go	Tue Jan 22 11:22:17 2019 +0100
@@ -27,6 +27,7 @@
 	"time"
 
 	"gemma.intevation.de/gemma/pkg/common"
+	"gemma.intevation.de/gemma/pkg/misc"
 	"gemma.intevation.de/gemma/pkg/models"
 )
 
@@ -109,19 +110,10 @@
 	return os.RemoveAll(agm.Dir)
 }
 
-func guessDate(s string) (time.Time, error) {
-	var err error
-	var t time.Time
-	for _, layout := range [...]string{
-		"02.01.2006 15:04",
-		"2006-01-02T15:04:05-07:00",
-	} {
-		if t, err = time.Parse(layout, s); err == nil {
-			break
-		}
-	}
-	return t, err
-}
+var guessDate = misc.TimeGuesser([]string{
+	"02.01.2006 15:04",
+	"2006-01-02T15:04:05-07:00",
+}).Guess
 
 // Do executes the actual approved gauge measurements import.
 func (agm *ApprovedGaugeMeasurements) Do(
--- a/pkg/imports/bn.go	Fri Jan 18 06:51:49 2019 +0000
+++ b/pkg/imports/bn.go	Tue Jan 22 11:22:17 2019 +0100
@@ -66,8 +66,7 @@
   ISRSrange_area(
     isrsrange(isrs_fromText($5), isrs_fromText($6)),
     (SELECT ST_Union(CAST(area AS geometry))
-        FROM waterway.fairway_dimensions
-        WHERE level_of_service = 3)),
+        FROM waterway.waterway_area)),
   $7,
   $8,
   $9,
@@ -258,14 +257,14 @@
 	}
 
 	feedback.Info("Storing %d bottlenecks took %s", len(nids), time.Since(start))
-	if err = tx.Commit(); err == nil {
-		feedback.Info("Import of bottlenecks was successful")
+	if err := tx.Commit(); err != nil {
+		return nil, err
 	}
-
+	feedback.Info("Import of bottlenecks was successful")
 	summary := struct {
 		Bottlenecks []string `json:"bottlenecks"`
 	}{
 		Bottlenecks: nids,
 	}
-	return &summary, err
+	return &summary, nil
 }
--- a/pkg/imports/dmv.go	Fri Jan 18 06:51:49 2019 +0000
+++ b/pkg/imports/dmv.go	Tue Jan 22 11:22:17 2019 +0100
@@ -188,7 +188,7 @@
 			code.FairwaySection,
 			code.Orc,
 			code.Hectometre,
-			float64(*dr.Lat), float64(*dr.Lon),
+			float64(*dr.Lon), float64(*dr.Lat),
 			string(*dr.Relenc),
 		); err != nil {
 			return nil, err
--- a/pkg/imports/fd.go	Fri Jan 18 06:51:49 2019 +0000
+++ b/pkg/imports/fd.go	Tue Jan 22 11:22:17 2019 +0100
@@ -23,6 +23,7 @@
 	"time"
 
 	"gemma.intevation.de/gemma/pkg/common"
+	"gemma.intevation.de/gemma/pkg/misc"
 	"gemma.intevation.de/gemma/pkg/wfs"
 )
 
@@ -44,12 +45,18 @@
 
 type fdTime struct{ time.Time }
 
+var guessFDTime = misc.TimeGuesser([]string{
+	"20060102",
+	"2006",
+	"",
+}).Guess
+
 func (fdt *fdTime) UnmarshalJSON(data []byte) error {
 	var s string
 	if err := json.Unmarshal(data, &s); err != nil {
 		return err
 	}
-	t, err := time.Parse("20060102", s)
+	t, err := guessFDTime(s)
 	if err == nil {
 		*fdt = fdTime{t}
 	}
@@ -67,7 +74,7 @@
 
 func (fdJobCreator) Description() string { return "fairway dimension" }
 
-func (fdJobCreator) AutoAccept() bool { return true }
+func (fdJobCreator) AutoAccept() bool { return false }
 
 func (fdJobCreator) Create(_ JobKind, data string) (Job, error) {
 	fd := new(FairwayDimension)
@@ -83,19 +90,41 @@
 	}
 }
 
-// StageDone is a NOP for fairway dimensions imports.
-func (fdJobCreator) StageDone(context.Context, *sql.Tx, int64) error {
-	return nil
+func (fdJobCreator) StageDone(
+	ctx context.Context,
+	tx *sql.Tx,
+	id int64,
+) error {
+	// Delete the old features.
+	if _, err := tx.ExecContext(ctx, deleteFairwayDimensionSQL); err != nil {
+		return err
+	}
+
+	_, err := tx.ExecContext(ctx, fdStageDoneSQL, id)
+	return err
 }
 
 // CleanUp for fairway dimension imports is a NOP.
 func (*FairwayDimension) CleanUp() error { return nil }
 
 type fairwayDimensionProperties struct {
-	HydroSorDat fdTime `json:"hydro_sordat"`
+	HydroSorDat *fdTime `json:"hydro_sordat"`
+}
+
+type fdSummary struct {
+	Lat float64 `json:"lat"`
+	Lon float64 `json:"lon"`
+	ID  int64   `json:"id"`
 }
 
 const (
+	fdStageDoneSQL = `
+UPDATE waterway.fairway_dimensions SET staging_done = true
+WHERE id IN (
+  SELECT key from waterway.track_imports
+  WHERE import_id = $1 AND
+		relation = 'waterway.fairway_dimensions'::regclass)`
+
 	deleteFairwayDimensionSQL = `
 WITH resp AS (
   SELECT best_utm(area::geometry) AS t,
@@ -106,7 +135,7 @@
 DELETE FROM waterway.fairway_dimensions
 WHERE ST_Covers(
   (SELECT a FROM resp),
-  ST_Transform(area::geometry, (SELECT t FROM resp)))
+  ST_Transform(area::geometry, (SELECT t FROM resp))) AND staging_done = true
 `
 
 	// The ST_MakeValid (line125) and ST_Buffer (line124) are a workarround to
@@ -131,7 +160,10 @@
      )).geom AS geom
   ) AS clipped
   WHERE clipped.geom IS NOT NULL
-`
+RETURNING id,
+  ST_X(ST_Centroid(area::geometry)),
+  ST_Y(ST_Centroid(area::geometry))
+ `
 )
 
 // Do executes the actual fairway dimension import.
@@ -185,16 +217,13 @@
 	}
 	defer insertStmt.Close()
 
-	// Delete the old features.
-	if _, err := tx.ExecContext(ctx, deleteFairwayDimensionSQL); err != nil {
-		return nil, err
-	}
-
 	var (
 		unsupported       = stringCounter{}
 		missingProperties int
 		badProperties     int
 		features          int
+		outside           int
+		fds               []fdSummary
 	)
 
 	if err := wfs.DownloadURLs(urls, func(r io.Reader) error {
@@ -217,6 +246,7 @@
 
 		feedback.Info("Using EPSG: %d", epsg)
 
+	features:
 		for _, feature := range rfc.Features {
 			if feature.Geometry.Coordinates == nil {
 				missingProperties++
@@ -226,16 +256,25 @@
 			var props fairwayDimensionProperties
 
 			if err := json.Unmarshal(*feature.Properties, &props); err != nil {
+				feedback.Warn("bad property: %v", err)
 				badProperties++
 				continue
 			}
+			var dateInfo time.Time
+			if props.HydroSorDat == nil || props.HydroSorDat.IsZero() {
+				dateInfo = start
+			} else {
+				dateInfo = (*props.HydroSorDat).Time
+			}
 			switch feature.Geometry.Type {
 			case "Polygon":
 				var p polygonSlice
 				if err := json.Unmarshal(*feature.Geometry.Coordinates, &p); err != nil {
 					return err
 				}
-				if _, err := insertStmt.ExecContext(
+				var fdid int64
+				var lat, lon float64
+				err = insertStmt.QueryRowContext(
 					ctx,
 					p.asWKB(),
 					epsg,
@@ -243,12 +282,23 @@
 					fd.MinWidth,
 					fd.MaxWidth,
 					fd.Depth,
-					props.HydroSorDat.Time,
+					dateInfo,
 					fd.SourceOrganization,
-				); err != nil {
-					feedback.Error("error: %s", err)
+				).Scan(&fdid, &lat, &lon)
+				switch {
+				case err == sql.ErrNoRows:
+					outside++
+					// ignore -> filtered by responsibility_areas
+					continue features
+				case err != nil:
 					return err
 				}
+				// Store for potential later removal.
+				if err = track(ctx, tx, importID, "waterway.fairway_dimensions", fdid); err != nil {
+					return err
+				}
+				fds = append(fds, fdSummary{ID: fdid, Lat: lat, Lon: lon})
+
 				features++
 			default:
 				unsupported[feature.Geometry.Type]++
@@ -272,6 +322,10 @@
 		feedback.Warn("Unsupported types found: %s", unsupported)
 	}
 
+	if outside > 0 {
+		feedback.Info("Features outside responsibility areas: %d", outside)
+	}
+
 	if features == 0 {
 		err := errors.New("No features found")
 		feedback.Error("%v", err)
@@ -283,5 +337,16 @@
 			features, time.Since(start))
 	}
 
-	return nil, err
+	summary := struct {
+		Date               time.Time   `json:"date"`
+		LOS                int         `json:"los"`
+		SourceOrganization string      `json:"source-organization"`
+		FdArea             []fdSummary `json:"fd-area"`
+	}{
+		Date:               time.Now(),
+		LOS:                fd.LOS,
+		SourceOrganization: fd.SourceOrganization,
+		FdArea:             fds,
+	}
+	return &summary, err
 }
--- a/pkg/imports/sr.go	Fri Jan 18 06:51:49 2019 +0000
+++ b/pkg/imports/sr.go	Tue Jan 22 11:22:17 2019 +0100
@@ -48,7 +48,7 @@
 
 	// Override data
 	// Date if given overrides the date value from the meta.json.
-	Date *models.SoundingResultDate `json:"date,omitempty"`
+	Date *models.Date `json:"date,omitempty"`
 	// Date if given overrides the name of the bottleneck from the meta.json.
 	Bottleneck *string `json:"bottleneck,omitempty"`
 	// EPSG if given overrides the EPSG code from the meta.json.
@@ -311,10 +311,10 @@
 	}
 
 	summary := struct {
-		Bottleneck string                    `json:"bottleneck"`
-		Date       models.SoundingResultDate `json:"date"`
-		Lat        float64                   `json:"lat"`
-		Lon        float64                   `json:"lon"`
+		Bottleneck string      `json:"bottleneck"`
+		Date       models.Date `json:"date"`
+		Lat        float64     `json:"lat"`
+		Lon        float64     `json:"lon"`
 	}{
 		Bottleneck: m.Bottleneck,
 		Date:       m.Date,
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/pkg/imports/st.go	Tue Jan 22 11:22:17 2019 +0100
@@ -0,0 +1,227 @@
+// 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 imports
+
+import (
+	"context"
+	"database/sql"
+	"errors"
+	"time"
+
+	"gemma.intevation.de/gemma/pkg/common"
+	"gemma.intevation.de/gemma/pkg/models"
+)
+
+type Stretch struct {
+	Name      string                 `json:"name"`
+	From      models.Isrs            `json:"from"`
+	To        models.Isrs            `json:"to"`
+	ObjNam    string                 `json:"objnam"`
+	NObjNam   *string                `json:"nobjnam"`
+	Source    string                 `json:"source-organization"`
+	Date      models.Date            `json:"date-info"`
+	Countries models.UniqueCountries `json:"countries"`
+}
+
+const STJobKind JobKind = "st"
+
+type stJobCreator struct{}
+
+func init() {
+	RegisterJobCreator(STJobKind, stJobCreator{})
+}
+
+func (stJobCreator) Description() string { return "stretch" }
+
+func (stJobCreator) AutoAccept() bool { return false }
+
+func (stJobCreator) Create(_ JobKind, data string) (Job, error) {
+	st := new(Stretch)
+	if err := common.FromJSONString(data, st); err != nil {
+		return nil, err
+	}
+	return st, nil
+}
+
+func (stJobCreator) Depends() []string {
+	return []string{
+		"stretches",
+	}
+}
+
+const (
+	stDeleteSQL = `
+DELETE FROM waterway.stretches WHERE
+staging_done AND name = (
+  SELECT name
+  FROM waterway.stretches WHERE
+  id = (
+    SELECT key from waterway.track_imports
+    WHERE import_id = $1 AND
+      relation = 'waterway.stretches'::regclass)
+  AND NOT staging_done
+)`
+
+	stStageDoneSQL = `
+UPDATE waterway.stretches SET staging_done = true
+WHERE id IN (
+  SELECT key from waterway.track_imports
+  WHERE import_id = $1 AND
+        relation = 'waterway.stretches'::regclass)`
+
+	stInsertSQL = `
+WITH r AS (
+  SELECT isrsrange(
+	( $1::char(2),
+	  $2::char(3),
+	  $3::char(5),
+	  $4::char(5),
+	  $5::int),
+	( $6::char(2),
+	  $7::char(3),
+	  $8::char(5),
+	  $9::char(5),
+	  $10::int)) AS r
+)
+INSERT INTO waterway.stretches (
+  name,
+  stretch,
+  geom,
+  objnam,
+  nobjnam,
+  date_info,
+  source_organization
+) VALUES (
+  $11,
+  (SELECT r FROM r),
+  ISRSrange_area(
+    (SELECT r FROM r),
+    (SELECT ST_Union(CAST(area AS geometry))
+      FROM waterway.waterway_area)),
+  $12,
+  $13,
+  $14,
+  $15)
+RETURNING id`
+
+	stInsertCountrySQL = `
+INSERT INTO waterway.stretch_countries (
+  stretches_id,
+  country_code
+) VALUES (
+  $1,
+  $2
+)`
+)
+
+// StageDone moves the imported stretch out of the staging area.
+func (stJobCreator) StageDone(
+	ctx context.Context,
+	tx *sql.Tx,
+	id int64,
+) error {
+	if _, err := tx.ExecContext(ctx, stDeleteSQL, id); err != nil {
+		return err
+	}
+	_, err := tx.ExecContext(ctx, stStageDoneSQL, id)
+	return err
+}
+
+// CleanUp of a stretch import is a NOP.
+func (*Stretch) CleanUp() error { return nil }
+
+// Do executes the actual stretch import.
+func (st *Stretch) Do(
+	ctx context.Context,
+	importID int64,
+	conn *sql.Conn,
+	feedback Feedback,
+) (interface{}, error) {
+
+	start := time.Now()
+
+	if st.Date.Time.IsZero() {
+		st.Date = models.Date{Time: start}
+	}
+
+	feedback.Info("Storing stretch '%s'", st.Name)
+
+	if len(st.Countries) == 0 {
+		return nil, errors.New("List of countries is empty")
+	}
+
+	tx, err := conn.BeginTx(ctx, nil)
+	if err != nil {
+		return nil, err
+	}
+	defer tx.Rollback()
+
+	insertCountryStmt, err := tx.PrepareContext(ctx, stInsertCountrySQL)
+	if err != nil {
+		return nil, err
+	}
+
+	var nobjnm sql.NullString
+	if st.NObjNam != nil {
+		nobjnm = sql.NullString{String: *st.NObjNam, Valid: true}
+	}
+
+	var id int64
+	if err := tx.QueryRowContext(
+		ctx,
+		stInsertSQL,
+		st.From.CountryCode,
+		st.From.LoCode,
+		st.From.FairwaySection,
+		st.From.Orc,
+		st.From.Hectometre,
+		st.To.CountryCode,
+		st.To.LoCode,
+		st.To.FairwaySection,
+		st.To.Orc,
+		st.To.Hectometre,
+		st.Name,
+		st.ObjNam,
+		nobjnm,
+		st.Date.Time,
+		st.Source,
+	).Scan(&id); err != nil {
+		return nil, err
+	}
+
+	// store the associated countries.
+	for _, c := range st.Countries {
+		if _, err := insertCountryStmt.ExecContext(ctx, id, c); err != nil {
+			return nil, err
+		}
+	}
+
+	if err := track(ctx, tx, importID, "waterway.stretches", id); err != nil {
+		return nil, err
+	}
+
+	feedback.Info("Storing stretch '%s' took %s", st.Name, time.Since(start))
+	if err := tx.Commit(); err != nil {
+		return nil, err
+	}
+	feedback.Info("Import of stretch was successful")
+
+	summary := struct {
+		Stretch string `json:"stretch"`
+	}{
+		Stretch: st.Name,
+	}
+
+	return &summary, nil
+}
--- a/pkg/imports/wa.go	Fri Jan 18 06:51:49 2019 +0000
+++ b/pkg/imports/wa.go	Tue Jan 22 11:22:17 2019 +0100
@@ -104,11 +104,11 @@
 SELECT ST_Transform(clipped.geom, 4326)::geography, $3, $4 FROM (
     SELECT (ST_Dump(
        ST_Intersection(
-         (SELECT a FROM resp),
-         ST_Transform(
+         (SELECT ST_Buffer(a, 0.0001) FROM resp),
+         ST_CollectionExtract(ST_MakeValid(ST_Transform(
            ST_GeomFromWKB($1, $2::integer),
            (SELECT t FROM resp)
-         )
+         )),3)
        )
      )).geom AS geom
   ) AS clipped
--- a/pkg/imports/wg.go	Fri Jan 18 06:51:49 2019 +0000
+++ b/pkg/imports/wg.go	Tue Jan 22 11:22:17 2019 +0100
@@ -382,7 +382,7 @@
 			ic.code.Orc,
 			ic.code.Hectometre,
 			string(*dr.Objname.Loc),
-			int64(*dr.Lat), int64(*dr.Lon),
+			int64(*dr.Lon), int64(*dr.Lat),
 			from,
 			to,
 			&validity,
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/pkg/misc/time.go	Tue Jan 22 11:22:17 2019 +0100
@@ -0,0 +1,34 @@
+// This is Free Software under GNU Affero General Public License v >= 3.0.Reader.
+// 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 misc
+
+import "time"
+
+// TimeGuesser is a list of time formats.
+type TimeGuesser []string
+
+// Guess tries to parse a given string by the entries of the layout
+// list one after another. The first matching time is returned.
+// If no layout matches the last error is returned or time zero
+// if the layout list is empty.
+func (tg TimeGuesser) Guess(s string) (time.Time, error) {
+	var err error
+	var t time.Time
+	for _, layout := range tg {
+		if t, err = time.Parse(layout, s); err == nil {
+			break
+		}
+	}
+	return t, err
+}
--- a/pkg/models/common.go	Fri Jan 18 06:51:49 2019 +0000
+++ b/pkg/models/common.go	Tue Jan 22 11:22:17 2019 +0100
@@ -13,7 +13,14 @@
 
 package models
 
-import "errors"
+import (
+	"database/sql/driver"
+	"encoding/json"
+	"errors"
+	"fmt"
+	"strings"
+	"time"
+)
 
 var (
 	errNoString    = errors.New("Not a string")
@@ -22,3 +29,85 @@
 
 // WGS84 is the EPSG of the World Geodetic System 1984.
 const WGS84 = 4326
+
+const DateFormat = "2006-01-02"
+
+type (
+	Date struct{ time.Time }
+	// Country is a valid country 2 letter code.
+	Country string
+	// UniqueCountries is a list of unique countries.
+	UniqueCountries []Country
+)
+
+func (srd Date) MarshalJSON() ([]byte, error) {
+	return json.Marshal(srd.Format(DateFormat))
+}
+
+func (srd *Date) UnmarshalJSON(data []byte) error {
+	var s string
+	if err := json.Unmarshal(data, &s); err != nil {
+		return err
+	}
+	d, err := time.Parse(DateFormat, s)
+	if err == nil {
+		*srd = Date{d}
+	}
+	return err
+}
+
+var (
+	validCountries = []string{
+		"AT", "BG", "DE", "HU", "HR",
+		"MD", "RO", "RS", "SK", "UA",
+	}
+	errNoValidCountry = errors.New("Not a valid country")
+)
+
+// UnmarshalJSON ensures that the given string forms a valid
+// two letter country code.
+func (c *Country) UnmarshalJSON(data []byte) error {
+	var s string
+	if err := json.Unmarshal(data, &s); err != nil {
+		return err
+	}
+	s = strings.ToUpper(s)
+	for _, v := range validCountries {
+		if v == s {
+			*c = Country(v)
+			return nil
+		}
+	}
+	return errNoValidCountry
+}
+
+// Value implements the driver.Valuer interface.
+func (c Country) Value() (driver.Value, error) {
+	return string(c), nil
+}
+
+// Scan implements the sql.Scanner interfaces.
+func (c *Country) Scan(src interface{}) (err error) {
+	if s, ok := src.(string); ok {
+		*c = Country(s)
+	} else {
+		err = errNoString
+	}
+	return
+}
+
+func (uc *UniqueCountries) UnmarshalJSON(data []byte) error {
+	var countries []Country
+	if err := json.Unmarshal(data, &countries); err != nil {
+		return err
+	}
+	unique := map[Country]struct{}{}
+	for _, c := range countries {
+		if _, found := unique[c]; found {
+			return fmt.Errorf("country '%s' is not unique", string(c))
+		}
+		unique[c] = struct{}{}
+	}
+	*uc = countries
+	return nil
+}
--- a/pkg/models/gauge.go	Fri Jan 18 06:51:49 2019 +0000
+++ b/pkg/models/gauge.go	Tue Jan 22 11:22:17 2019 +0100
@@ -14,9 +14,6 @@
 package models
 
 import (
-	"errors"
-	"fmt"
-	"strconv"
 	"time"
 
 	"gemma.intevation.de/gemma/pkg/common"
@@ -36,41 +33,3 @@
 	Gauge           Isrs
 	LatestDateIssue time.Time
 }
-
-// Isrs represents the gauge identification data structure
-type Isrs struct {
-	CountryCode    string
-	LoCode         string
-	FairwaySection string
-	Orc            string
-	Hectometre     int
-}
-
-// IsrsFromString converts string representation of isrs code to type Isrs
-func IsrsFromString(isrsCode string) (*Isrs, error) {
-	if len(isrsCode) < 20 {
-		return nil, errors.New("ISRS code too short")
-	}
-	hm, err := strconv.Atoi(isrsCode[15:20])
-	if err != nil {
-		return nil, err
-	}
-	isrs := Isrs{
-		CountryCode:    isrsCode[0:2],
-		LoCode:         isrsCode[2:5],
-		FairwaySection: isrsCode[5:10],
-		Orc:            isrsCode[10:15],
-		Hectometre:     hm,
-	}
-	return &isrs, nil
-}
-
-// String creates a isrs code string from Isrs
-func (isrs *Isrs) String() string {
-	return fmt.Sprintf("%s%s%s%s%05d",
-		isrs.CountryCode,
-		isrs.LoCode,
-		isrs.FairwaySection,
-		isrs.Orc,
-		isrs.Hectometre)
-}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/pkg/models/isrs.go	Tue Jan 22 11:22:17 2019 +0100
@@ -0,0 +1,80 @@
+// This is Free Software under GNU Affero General Public License v >= 3.0
+// without warranty, see README.md and license for details.
+//
+// SPDX-License-Identifier: AGPL-3.0-or-later
+// License-Filename: LICENSES/AGPL-3.0.txt
+//
+// Copyright (C) 2018 by via donau
+//   – Österreichische Wasserstraßen-Gesellschaft mbH
+// Software engineering by Intevation GmbH
+//
+// Author(s):
+//  * Raimund Renkert <raimund.renkert@intevation.de>
+//  * Sascha L. Teichmann <sascha.teichmann@intevation.de>
+
+package models
+
+import (
+	"encoding/json"
+	"errors"
+	"fmt"
+	"strconv"
+)
+
+// Isrs represents the gauge identification data structure
+type Isrs struct {
+	CountryCode    string
+	LoCode         string
+	FairwaySection string
+	Orc            string
+	Hectometre     int
+}
+
+func (isrs *Isrs) UnmarshalJSON(data []byte) error {
+	var s string
+	if err := json.Unmarshal(data, &s); err != nil {
+		return err
+	}
+	in, err := IsrsFromString(s)
+	if err != nil {
+		return err
+	}
+	*isrs = *in
+	return nil
+}
+
+func (isrs *Isrs) MarshalJSON() ([]byte, error) {
+	if isrs == nil {
+		return nil, nil
+	}
+	return json.Marshal(isrs.String())
+}
+
+// IsrsFromString converts string representation of isrs code to type Isrs
+func IsrsFromString(isrsCode string) (*Isrs, error) {
+	if len(isrsCode) < 20 {
+		return nil, errors.New("ISRS code too short")
+	}
+	hm, err := strconv.Atoi(isrsCode[15:20])
+	if err != nil {
+		return nil, err
+	}
+	isrs := Isrs{
+		CountryCode:    isrsCode[0:2],
+		LoCode:         isrsCode[2:5],
+		FairwaySection: isrsCode[5:10],
+		Orc:            isrsCode[10:15],
+		Hectometre:     hm,
+	}
+	return &isrs, nil
+}
+
+// String creates a isrs code string from Isrs
+func (isrs *Isrs) String() string {
+	return fmt.Sprintf("%s%s%s%s%05d",
+		isrs.CountryCode,
+		isrs.LoCode,
+		isrs.FairwaySection,
+		isrs.Orc,
+		isrs.Hectometre)
+}
--- a/pkg/models/sr.go	Fri Jan 18 06:51:49 2019 +0000
+++ b/pkg/models/sr.go	Tue Jan 22 11:22:17 2019 +0100
@@ -21,19 +21,14 @@
 	"errors"
 	"fmt"
 	"io"
-	"time"
 )
 
-const SoundingResultDateFormat = "2006-01-02"
-
 type (
-	SoundingResultDate struct{ time.Time }
-
 	SoundingResultMeta struct {
-		Date           SoundingResultDate `json:"date"`
-		Bottleneck     string             `json:"bottleneck"`
-		EPSG           uint               `json:"epsg"`
-		DepthReference string             `json:"depth-reference"`
+		Date           Date   `json:"date"`
+		Bottleneck     string `json:"bottleneck"`
+		EPSG           uint   `json:"epsg"`
+		DepthReference string `json:"depth-reference"`
 	}
 )
 
@@ -50,22 +45,6 @@
 WHERE bn.objnam = $1 AND sr.date_info = $2`
 )
 
-func (srd SoundingResultDate) MarshalJSON() ([]byte, error) {
-	return json.Marshal(srd.Format(SoundingResultDateFormat))
-}
-
-func (srd *SoundingResultDate) UnmarshalJSON(data []byte) error {
-	var s string
-	if err := json.Unmarshal(data, &s); err != nil {
-		return err
-	}
-	d, err := time.Parse(SoundingResultDateFormat, s)
-	if err == nil {
-		*srd = SoundingResultDate{d}
-	}
-	return err
-}
-
 func (m *SoundingResultMeta) Decode(r io.Reader) error {
 	err := json.NewDecoder(r).Decode(m)
 	if err == nil && m.EPSG == 0 {
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/pkg/models/stretch.go	Tue Jan 22 11:22:17 2019 +0100
@@ -0,0 +1,30 @@
+// 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 models
+
+import "gemma.intevation.de/gemma/pkg/common"
+
+type StretchImport struct {
+	Name      string          `json:"name"`
+	From      Isrs            `json:"from"`
+	To        Isrs            `json:"to"`
+	ObjNam    string          `json:"objnam"`
+	NObjNam   *string         `json:"nobjnam"`
+	Source    string          `json:"source-organization"`
+	Date      Date            `json:"date-info"`
+	Countries UniqueCountries `json:"countries"`
+
+	SendEmail  bool              `json:"send-email"`
+	Attributes common.Attributes `json:"attributes,omitempty"`
+}
--- a/pkg/models/user.go	Fri Jan 18 06:51:49 2019 +0000
+++ b/pkg/models/user.go	Tue Jan 22 11:22:17 2019 +0100
@@ -25,8 +25,6 @@
 type (
 	// Email is a string formed by a valid EMail address.
 	Email string
-	// Country is a valid country 2 letter code.
-	Country string
 	// Role is a string with a valid gemma role.
 	Role string
 	// UserName is a string forming a valid user name.
@@ -132,46 +130,6 @@
 }
 
 var (
-	validCountries = []string{
-		"AT", "BG", "DE", "HU", "HR",
-		"MD", "RO", "RS", "SK", "UA",
-	}
-	errNoValidCountry = errors.New("Not a valid country")
-)
-
-// UnmarshalJSON ensures that the given string forms a valid
-// two letter country code.
-func (c *Country) UnmarshalJSON(data []byte) error {
-	var s string
-	if err := json.Unmarshal(data, &s); err != nil {
-		return err
-	}
-	s = strings.ToUpper(s)
-	for _, v := range validCountries {
-		if v == s {
-			*c = Country(v)
-			return nil
-		}
-	}
-	return errNoValidCountry
-}
-
-// Value implements the driver.Valuer interface.
-func (c Country) Value() (driver.Value, error) {
-	return string(c), nil
-}
-
-// Scan implements the sql.Scanner interfaces.
-func (c *Country) Scan(src interface{}) (err error) {
-	if s, ok := src.(string); ok {
-		*c = Country(s)
-	} else {
-		err = errNoString
-	}
-	return
-}
-
-var (
 	validRoles = []string{
 		"waterway_user",
 		"waterway_admin",
--- a/pkg/wfs/download.go	Fri Jan 18 06:51:49 2019 +0000
+++ b/pkg/wfs/download.go	Tue Jan 22 11:22:17 2019 +0100
@@ -264,7 +264,7 @@
 func DownloadURLs(urls []string, handler func(io.Reader) error) error {
 	for _, url := range urls {
 		if err := downloadURL(url, handler); err != nil {
-			return nil
+			return err
 		}
 	}
 	return nil
--- a/schema/auth.sql	Fri Jan 18 06:51:49 2019 +0000
+++ b/schema/auth.sql	Tue Jan 22 11:22:17 2019 +0100
@@ -78,7 +78,7 @@
 BEGIN
     FOREACH the_table IN ARRAY ARRAY[
         'gauge_measurements',
-        'sections_stretches',
+        'stretches',
         'waterway_profiles',
         'fairway_dimensions',
         'bottlenecks',
@@ -125,6 +125,9 @@
     FOR ALL TO waterway_admin
     USING (utm_covers(area));
 
+CREATE POLICY responsibility_area ON waterway.stretches
+    FOR ALL TO sys_admin
+    USING (utm_covers(geom));
 
 --
 -- RLS policies for imports and import config
@@ -138,22 +141,48 @@
 ALTER table waterway.imports ENABLE ROW LEVEL SECURITY;
 
 -- The job running the import queue is running as sys_admin and login users
--- with that role should see all imports anyhow
-CREATE POLICY read_all ON waterway.imports
-    FOR SELECT TO sys_admin
+-- with that role should be able to run imports without restrictions anyhow
+CREATE POLICY import_all ON waterway.imports
+    FOR ALL TO sys_admin
     USING (true);
-CREATE POLICY update_all ON waterway.imports
-    FOR UPDATE TO sys_admin
-    USING (true);
+
+-- For the given table, check whether the given value is used as primary key,
+-- bypassing row level security.
+CREATE OR REPLACE FUNCTION waterway.is_new_key(
+        tablename varchar,
+        kv anyelement)
+    RETURNS boolean
+AS $$
+DECLARE columnname varchar;
+DECLARE ret boolean;
+BEGIN
+    columnname = (SELECT column_name
+        FROM information_schema.key_column_usage k
+        JOIN information_schema.table_constraints USING (constraint_name)
+        WHERE k.table_name = tablename and constraint_type = 'PRIMARY KEY');
+    EXECUTE format('SELECT NOT $1 = ANY(SELECT %I FROM waterway.%I)',
+        columnname, tablename)
+        INTO ret
+        USING kv;
+    RETURN ret;
+END;
+$$
+    LANGUAGE plpgsql
+    SECURITY DEFINER
+    STABLE PARALLEL SAFE;
 
 CREATE POLICY parent_allowed ON waterway.import_logs
     FOR ALL TO waterway_admin
-    USING (import_id IN (SELECT id FROM waterway.imports));
+    USING (import_id IN (SELECT id FROM waterway.imports))
+    WITH CHECK (waterway.is_new_key('imports', import_id)
+        OR import_id IN (SELECT id FROM waterway.imports));
 ALTER table waterway.import_logs ENABLE ROW LEVEL SECURITY;
 
 CREATE POLICY parent_allowed ON waterway.track_imports
     FOR ALL TO waterway_admin
-    USING (import_id IN (SELECT id FROM waterway.imports));
+    USING (import_id IN (SELECT id FROM waterway.imports))
+    WITH CHECK (waterway.is_new_key('imports', import_id)
+        OR import_id IN (SELECT id FROM waterway.imports));
 ALTER table waterway.track_imports ENABLE ROW LEVEL SECURITY;
 
 CREATE POLICY import_configuration_policy ON waterway.import_configuration
@@ -172,7 +201,11 @@
 CREATE POLICY parent_allowed ON waterway.import_configuration_attributes
     FOR ALL TO waterway_admin
     USING (import_configuration_id IN (
-        SELECT id FROM waterway.import_configuration));
+        SELECT id FROM waterway.import_configuration))
+    WITH CHECK (
+        waterway.is_new_key('import_configuration', import_configuration_id)
+        OR import_configuration_id IN (
+            SELECT id FROM waterway.import_configuration));
 ALTER table waterway.import_configuration_attributes ENABLE ROW LEVEL SECURITY;
 
 COMMIT;
--- a/schema/auth_tests.sql	Fri Jan 18 06:51:49 2019 +0000
+++ b/schema/auth_tests.sql	Tue Jan 22 11:22:17 2019 +0100
@@ -121,3 +121,94 @@
     DELETE FROM users.templates WHERE template_name = 'RO' RETURNING *
     $$,
     'Waterway admin cannot delete templates for other country');
+
+-- import management
+SELECT lives_ok($$
+    WITH
+    job AS (
+        INSERT INTO waterway.imports (kind, username, data) VALUES (
+            'test', current_user, 'test') RETURNING id),
+    log AS (
+        INSERT INTO waterway.import_logs (import_id, msg)
+            SELECT id, 'test' FROM job)
+    INSERT INTO waterway.track_imports
+        SELECT id, 'waterway.bottlenecks', 0 FROM job
+    $$,
+    'Waterway admin can add import job and related data');
+
+SET SESSION AUTHORIZATION test_admin_at2;
+SELECT bag_has($$
+    SELECT username FROM users.list_users
+    $$,
+    $$
+    WITH job AS (
+        UPDATE waterway.imports SET state = 'accepted'
+            RETURNING id, username),
+    log AS (
+        INSERT INTO waterway.import_logs (import_id, msg)
+            SELECT id, 'test continued' FROM job)
+    SELECT username FROM job
+    $$,
+    'Waterway admin can edit import jobs from his country only');
+
+SELECT lives_ok($$
+    WITH
+    config AS (
+        INSERT INTO waterway.import_configuration (kind, username) VALUES (
+            'test', current_user) RETURNING id)
+    INSERT INTO waterway.import_configuration_attributes
+        SELECT id, 'test key', 'test value' FROM config
+    $$,
+    'Waterway admin can add import config and related data');
+
+SET SESSION AUTHORIZATION test_admin_at;
+SELECT bag_has($$
+    SELECT username FROM users.list_users
+    $$,
+    $$
+    WITH config AS (
+        UPDATE waterway.import_configuration SET send_email = true
+            RETURNING id, username),
+    attrib AS (
+        INSERT INTO waterway.import_configuration_attributes
+            SELECT id, 'test continued', 'test value' FROM config),
+    attrib_upd AS (
+        UPDATE waterway.import_configuration_attributes SET v = 'test v'
+            WHERE import_configuration_id = (SELECT id FROM config))
+    SELECT username FROM config
+    $$,
+    'Waterway admin can edit import config from his country only');
+
+SET SESSION AUTHORIZATION test_admin_ro;
+SELECT throws_ok($$
+    INSERT INTO waterway.import_logs (import_id, msg)
+        VALUES (currval(pg_get_serial_sequence('waterway.imports', 'id')),
+            'test')
+    $$,
+    42501, NULL,
+    'Waterway admin cannot add log messages to other countries imports');
+
+SELECT throws_ok($$
+    DELETE FROM waterway.track_imports
+        WHERE import_id = currval(
+            pg_get_serial_sequence('waterway.imports', 'id'))
+    $$,
+    42501, NULL,
+    'Waterway admin cannot delete tracking data of other countries imports');
+
+SELECT throws_ok($$
+    INSERT INTO waterway.import_configuration_attributes
+        VALUES (currval(pg_get_serial_sequence(
+                'waterway.import_configuration', 'id')),
+            'test', 'test value')
+    $$,
+    42501, NULL,
+    'Waterway admin cannot add attributes to other countries import config');
+
+SELECT throws_ok($$
+    UPDATE waterway.import_configuration_attributes SET v = 'evil'
+        WHERE import_configuration_id = currval(
+            pg_get_serial_sequence('waterway.import_configuration', 'id'))
+    $$,
+    42501, NULL,
+    'Waterway admin cannot overwrite attributes of other countries config');
--- a/schema/demo-data/published_services.sql	Fri Jan 18 06:51:49 2019 +0000
+++ b/schema/demo-data/published_services.sql	Tue Jan 22 11:22:17 2019 +0100
@@ -12,6 +12,7 @@
 --  * Tom Gottfried <tom@intevation.de>
 
 INSERT INTO sys_admin.published_services (name) VALUES
+    ('waterway.stretches_geoserver'),
     ('waterway.fairway_dimensions'),
     ('waterway.distance_marks_geoserver'),
     ('waterway.sounding_results_contour_lines_geoserver'),
--- a/schema/gemma.sql	Fri Jan 18 06:51:49 2019 +0000
+++ b/schema/gemma.sql	Tue Jan 22 11:22:17 2019 +0100
@@ -347,18 +347,49 @@
         check (pk_policy in ('sequence', 'assigned', 'autogenerated'))
     )
 
-    CREATE TABLE sections_stretches (
-        id varchar PRIMARY KEY,
-        is_section boolean NOT NULL, -- maps 'function' from interface
-        stretch isrsrange,
+    CREATE TABLE stretches (
+        id int PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY,
+        name varchar NOT NULL,
+        stretch isrsrange NOT NULL,
+        -- TODO: make it a (MULTI)-LINESTRING.
+        -- POLYGON is chosen for the convinience
+        -- of re-using ISRSrange_area.
+        geom geography(POLYGON, 4326) NOT NULL,
         objnam varchar NOT NULL,
         nobjnam varchar,
         date_info timestamp with time zone NOT NULL DEFAULT CURRENT_TIMESTAMP,
         source_organization varchar NOT NULL,
-        staging_done boolean NOT NULL DEFAULT false
+        staging_done boolean NOT NULL DEFAULT false,
+        UNIQUE(name, staging_done)
+    )
+
+    CREATE TABLE stretch_countries (
+        stretches_id int NOT NULL REFERENCES stretches(id)
+            ON DELETE CASCADE,
+        country_code char(2) NOT NULL REFERENCES countries(country_code),
+        UNIQUE(stretches_id, country_code)
     )
+
+    -- Published view for GeoServer
+    CREATE VIEW stretches_geoserver AS SELECT
+        id,
+        name,
+        (stretch).lower::varchar as lower,
+        (stretch).upper::varchar as upper,
+        geom::Geometry(POLYGON, 4326),
+        objnam,
+        nobjnam,
+        date_info,
+        source_organization,
+        (SELECT string_agg(country_code, ', ')
+            FROM stretch_countries
+            WHERE stretches_id = id) AS countries,
+        staging_done
+    FROM stretches
+
+
     CREATE TRIGGER sections_stretches_date_info
-        BEFORE UPDATE ON sections_stretches
+        BEFORE UPDATE ON stretches
         FOR EACH ROW EXECUTE PROCEDURE update_date_info()
 
     CREATE TABLE waterway_profiles (
--- a/schema/manage_users_tests.sql	Fri Jan 18 06:51:49 2019 +0000
+++ b/schema/manage_users_tests.sql	Tue Jan 22 11:22:17 2019 +0100
@@ -40,7 +40,7 @@
 SELECT set_eq($$
     SELECT count(*) FROM users.list_users
     $$,
-    ARRAY[4],
+    ARRAY[6],
     'System admin can see all users');
 
 --
--- a/schema/run_tests.sh	Fri Jan 18 06:51:49 2019 +0000
+++ b/schema/run_tests.sh	Tue Jan 22 11:22:17 2019 +0100
@@ -28,7 +28,7 @@
     -c 'SET client_min_messages TO WARNING' \
     -c "DROP ROLE IF EXISTS $TEST_ROLES" \
     -f tap_tests_data.sql \
-    -c 'SELECT plan(47)' \
+    -c 'SELECT plan(55)' \
     -f isrs_tests.sql \
     -f auth_tests.sql \
     -f manage_users_tests.sql \
--- a/schema/tap_tests_data.sql	Fri Jan 18 06:51:49 2019 +0000
+++ b/schema/tap_tests_data.sql	Tue Jan 22 11:22:17 2019 +0100
@@ -29,6 +29,10 @@
 INSERT INTO users.list_users VALUES (
     'waterway_admin', 'test_admin_at', 'admin_at1$', 'AT', NULL, 'yyy');
 INSERT INTO users.list_users VALUES (
+    'waterway_admin', 'test_admin_at2', 'admin_at2$', 'AT', NULL, 'yyy');
+INSERT INTO users.list_users VALUES (
+    'waterway_admin', 'test_admin_ro', 'admin_ro1$', 'RO', NULL, 'yyx');
+INSERT INTO users.list_users VALUES (
     'sys_admin', 'test_sys_admin1', 'sys_admin1$', 'AT', NULL, 'zzz');
 
 INSERT INTO limiting_factors VALUES ('depth'), ('width');
@@ -88,3 +92,20 @@
     VALUES ('AT', '\x'), ('RO', '\x');
 INSERT INTO users.user_templates
     VALUES ('test_user_at', 'AT'), ('test_user_ro', 'RO');
+
+WITH
+job AS (
+    INSERT INTO waterway.imports (kind, username, data) VALUES (
+        'test', 'test_admin_ro', 'test') RETURNING id),
+log AS (
+    INSERT INTO waterway.import_logs (import_id, msg)
+        SELECT id, 'test' FROM job)
+INSERT INTO waterway.track_imports
+    SELECT id, 'waterway.bottlenecks', 1 FROM job;
+
+WITH
+config AS (
+    INSERT INTO waterway.import_configuration (kind, username) VALUES (
+        'test', 'test_admin_ro') RETURNING id)
+INSERT INTO waterway.import_configuration_attributes
+    SELECT id, 'test key', 'test value' FROM config;