changeset 1558:0ded4c56978e

refac: component filestructure. remove admin/map hierarchy
author Thomas Junk <thomas.junk@intevation.de>
date Wed, 12 Dec 2018 09:22:20 +0100
parents 62171cd9a42b
children 5d84dcb79a54
files client/src/components/App.vue client/src/components/Bottlenecks.vue client/src/components/Contextbox.vue client/src/components/Identify.vue client/src/components/ImportSoundingresults.vue client/src/components/Importqueue.vue client/src/components/Importqueuedetail.vue client/src/components/Logs.vue client/src/components/Main.vue client/src/components/Maplayer.vue client/src/components/Pdftool.vue client/src/components/Search.vue client/src/components/Staging.vue client/src/components/Systemconfiguration.vue client/src/components/Zoom.vue client/src/components/admin/Importqueue.vue client/src/components/admin/Importqueuedetail.vue client/src/components/admin/Logs.vue client/src/components/admin/Systemconfiguration.vue client/src/components/admin/importschedule/Importschedule.vue client/src/components/admin/importschedule/Importscheduledetail.vue client/src/components/admin/usermanagement/Passwordfield.vue client/src/components/admin/usermanagement/Userdetail.vue client/src/components/admin/usermanagement/Usermanagement.vue client/src/components/fairway/Fairwayprofile.vue client/src/components/fairway/Infobar.vue client/src/components/fairway/Profiles.vue client/src/components/importschedule/Importschedule.vue client/src/components/importschedule/Importscheduledetail.vue client/src/components/layers/Layers.vue client/src/components/layers/Layerselect.vue client/src/components/layers/LegendElement.vue client/src/components/map/Identify.vue client/src/components/map/Main.vue client/src/components/map/Maplayer.vue client/src/components/map/Pdftool.vue client/src/components/map/Search.vue client/src/components/map/Zoom.vue client/src/components/map/contextbox/Bottlenecks.vue client/src/components/map/contextbox/Contextbox.vue client/src/components/map/contextbox/ImportSoundingresults.vue client/src/components/map/contextbox/Staging.vue client/src/components/map/fairway/Fairwayprofile.vue client/src/components/map/fairway/Infobar.vue client/src/components/map/fairway/Profiles.vue client/src/components/map/layers/Layers.vue client/src/components/map/layers/Layerselect.vue client/src/components/map/layers/LegendElement.vue client/src/components/map/toolbar/Identify.vue client/src/components/map/toolbar/Layers.vue client/src/components/map/toolbar/Linetool.vue client/src/components/map/toolbar/Pdftool.vue client/src/components/map/toolbar/Polygontool.vue client/src/components/map/toolbar/Profiles.vue client/src/components/map/toolbar/Toolbar.vue client/src/components/toolbar/Identify.vue client/src/components/toolbar/Layers.vue client/src/components/toolbar/Linetool.vue client/src/components/toolbar/Pdftool.vue client/src/components/toolbar/Polygontool.vue client/src/components/toolbar/Profiles.vue client/src/components/toolbar/Toolbar.vue client/src/components/usermanagement/Passwordfield.vue client/src/components/usermanagement/Userdetail.vue client/src/components/usermanagement/Usermanagement.vue client/src/router.js
diffstat 66 files changed, 5829 insertions(+), 5829 deletions(-) [+]
line wrap: on
line diff
--- a/client/src/components/App.vue	Tue Dec 11 22:59:10 2018 +0100
+++ b/client/src/components/App.vue	Wed Dec 12 09:22:20 2018 +0100
@@ -87,16 +87,16 @@
     }
   },
   components: {
-    Profiles: () => import("./map/fairway/Profiles"),
-    Infobar: () => import("./map/fairway/Infobar"),
-    Pdftool: () => import("./map/Pdftool"),
-    Zoom: () => import("./map/Zoom"),
-    Identify: () => import("./map/Identify"),
-    Layers: () => import("./map/layers/Layers"),
+    Profiles: () => import("./fairway/Profiles"),
+    Infobar: () => import("./fairway/Infobar"),
+    Pdftool: () => import("./Pdftool"),
+    Zoom: () => import("./Zoom"),
+    Identify: () => import("./Identify"),
+    Layers: () => import("./layers/Layers"),
     Sidebar: () => import("./Sidebar"),
-    Search: () => import("./map/Search"),
-    Contextbox: () => import("./map/contextbox/Contextbox"),
-    Toolbar: () => import("./map/toolbar/Toolbar")
+    Search: () => import("./Search"),
+    Contextbox: () => import("./Contextbox"),
+    Toolbar: () => import("./toolbar/Toolbar")
   }
 };
 </script>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/client/src/components/Bottlenecks.vue	Wed Dec 12 09:22:20 2018 +0100
@@ -0,0 +1,324 @@
+<template>
+  <div>
+    <h6 class="mb-0 py-2 px-3 border-bottom d-flex align-items-center">
+      <font-awesome-icon icon="ship" class="mr-2"></font-awesome-icon>
+      <translate>Bottlenecks</translate>
+    </h6>
+    <div class="row p-2 text-left small">
+      <div class="col-5">
+        <a href="#" @click="sortBy('name')" class="sort-link">
+          <translate>Name</translate>
+        </a>
+        <font-awesome-icon
+          :icon="sortIcon"
+          class="ml-1"
+          v-if="sortColumn === 'name'"
+        ></font-awesome-icon>
+      </div>
+      <div class="col-2">
+        <a href="#" @click="sortBy('latestMeasurement')" class="sort-link">
+          <translate>Latest</translate> <br />
+          <translate>Measurement</translate>
+        </a>
+        <font-awesome-icon
+          :icon="sortIcon"
+          class="ml-1"
+          v-if="sortColumn === 'latestMeasurement'"
+        ></font-awesome-icon>
+      </div>
+      <div class="col-3">
+        <a href="#" @click="sortBy('chainage')" class="sort-link">
+          <translate>Chainage</translate>
+        </a>
+        <font-awesome-icon
+          :icon="sortIcon"
+          class="ml-1"
+          v-if="sortColumn === 'chainage'"
+        ></font-awesome-icon>
+      </div>
+      <div class="col-2"></div>
+    </div>
+    <div
+      class="bottleneck-list small text-left"
+      :style="'max-height: ' + (showSplitscreen ? 18 : 35) + 'rem'"
+      v-if="filteredAndSortedBottlenecks().length"
+    >
+      <div
+        v-for="bottleneck in filteredAndSortedBottlenecks()"
+        :key="bottleneck.properties.name"
+        class="border-top row bottleneck-row mx-0"
+      >
+        <div class="col-5 py-2 text-left">
+          <a href="#" @click="selectBottleneck(bottleneck)">{{
+            bottleneck.properties.name
+          }}</a>
+        </div>
+        <div class="col-2 py-2">
+          {{ formatSurveyDate(bottleneck.properties.current) }}
+        </div>
+        <div class="col-3 py-2">
+          {{
+            displayCurrentChainage(
+              bottleneck.properties.from,
+              bottleneck.properties.to
+            )
+          }}
+        </div>
+        <div class="col-2 pr-0 text-right">
+          <button
+            type="button"
+            class="btn btn-sm btn-info rounded-0 h-100"
+            @click="loadSurveys(bottleneck.properties.name)"
+            v-if="bottleneck.properties.current"
+          >
+            <font-awesome-icon
+              icon="spinner"
+              fixed-width
+              spin
+              v-if="loading === bottleneck.properties.name"
+            ></font-awesome-icon>
+            <font-awesome-icon
+              icon="angle-down"
+              fixed-width
+              v-if="
+                loading !== bottleneck.properties.name &&
+                  openBottleneck !== bottleneck.properties.name
+              "
+            ></font-awesome-icon>
+            <font-awesome-icon
+              icon="angle-up"
+              fixed-width
+              v-if="
+                loading !== bottleneck.properties.name &&
+                  openBottleneck === bottleneck.properties.name
+              "
+            ></font-awesome-icon>
+          </button>
+        </div>
+        <div
+          :class="[
+            'col-12 p-0',
+            'surveys',
+            { open: openBottleneck === bottleneck.properties.name }
+          ]"
+        >
+          <a
+            href="#"
+            class="d-block px-3 py-2"
+            v-for="(survey, index) in openBottleneckSurveys"
+            :key="index"
+            @click="selectSurvey(survey, bottleneck)"
+            >{{ formatSurveyDate(survey.date_info) }}</a
+          >
+        </div>
+      </div>
+    </div>
+    <div v-else class="small text-center py-3 border-top">
+      <translate>No results.</translate>
+    </div>
+  </div>
+</template>
+
+<script>
+/* 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):
+ * Markus Kottländer <markus.kottlaender@intevation.de>
+ */
+import { mapState } from "vuex";
+import { HTTP } from "../lib/http";
+import { displayError } from "../lib/errors.js";
+import { formatSurveyDate } from "../lib/date.js";
+
+export default {
+  name: "bottlenecks",
+  data() {
+    return {
+      sortColumn: "name",
+      sortDirection: "ASC",
+      openBottleneck: null,
+      openBottleneckSurveys: null,
+      loading: null
+    };
+  },
+  computed: {
+    ...mapState("application", [
+      "searchQuery",
+      "showSearchbarLastState",
+      "showSplitscreen"
+    ]),
+    ...mapState("bottlenecks", ["bottlenecks"]),
+    sortIcon() {
+      return this.sortDirection === "ASC"
+        ? "sort-amount-down"
+        : "sort-amount-up";
+    }
+  },
+  methods: {
+    formatSurveyDate(date) {
+      return formatSurveyDate(date);
+    },
+    filteredAndSortedBottlenecks() {
+      return this.bottlenecks
+        .filter(bn => {
+          return bn.properties.name
+            .toLowerCase()
+            .includes(this.searchQuery.toLowerCase());
+        })
+        .sort((bnA, bnB) => {
+          switch (this.sortColumn) {
+            case "name":
+              if (
+                bnA.properties.name.toLowerCase() <
+                bnB.properties.name.toLowerCase()
+              )
+                return this.sortDirection === "ASC" ? -1 : 1;
+              if (
+                bnA.properties.name.toLowerCase() >
+                bnB.properties.name.toLowerCase()
+              )
+                return this.sortDirection === "ASC" ? 1 : -1;
+              return 0;
+
+            case "latestMeasurement": {
+              if (
+                (bnA.properties.current || "") < (bnB.properties.current || "")
+              )
+                return this.sortDirection === "ASC" ? -1 : 1;
+              if (
+                (bnA.properties.current || "") > (bnB.properties.current || "")
+              )
+                return this.sortDirection === "ASC" ? 1 : -1;
+              return 0;
+            }
+
+            case "chainage":
+              if (bnA.properties.from < bnB.properties.from)
+                return this.sortDirection === "ASC" ? -1 : 1;
+              if (bnA.properties.from > bnB.properties.from)
+                return this.sortDirection === "ASC" ? 1 : -1;
+              return 0;
+
+            default:
+              return 0;
+          }
+        });
+    },
+    selectSurvey(survey, bottleneck) {
+      this.$store
+        .dispatch(
+          "bottlenecks/setSelectedBottleneck",
+          bottleneck.properties.name
+        )
+        .then(() => {
+          this.$store.commit("bottlenecks/selectedSurvey", survey);
+        })
+        .then(() => {
+          this.$store.commit("map/moveMap", {
+            coordinates: bottleneck.geometry.coordinates,
+            zoom: 17,
+            preventZoomOut: true
+          });
+        });
+    },
+    selectBottleneck(bottleneck) {
+      this.$store
+        .dispatch(
+          "bottlenecks/setSelectedBottleneck",
+          bottleneck.properties.name
+        )
+        .then(() => {
+          this.$store.commit("bottlenecks/setFirstSurveySelected");
+        })
+        .then(() => {
+          this.$store.commit("map/moveMap", {
+            coordinates: bottleneck.geometry.coordinates,
+            zoom: 17,
+            preventZoomOut: true
+          });
+        });
+    },
+    sortBy(column) {
+      this.sortColumn = column;
+      this.sortDirection = this.sortDirection === "ASC" ? "DESC" : "ASC";
+    },
+    loadSurveys(name) {
+      this.openBottleneckSurveys = null;
+      if (name === this.openBottleneck) {
+        this.openBottleneck = null;
+      } else {
+        this.openBottleneck = name;
+        this.loading = name;
+
+        HTTP.get("/surveys/" + name, {
+          headers: {
+            "X-Gemma-Auth": localStorage.getItem("token"),
+            "Content-type": "text/xml; charset=UTF-8"
+          }
+        })
+          .then(response => {
+            this.openBottleneckSurveys = response.data.surveys.sort((a, b) => {
+              return a.date_info < b.date_info ? 1 : -1;
+            });
+          })
+          .catch(error => {
+            const { status, data } = error.response;
+            displayError({
+              title: this.$gettext("Backend Error"),
+              message: `${status}: ${data.message || data}`
+            });
+          })
+          .finally(() => (this.loading = null));
+      }
+    },
+    displayCurrentChainage(from, to) {
+      return from / 10 + " - " + to / 10;
+    }
+  },
+  mounted() {
+    this.$store.dispatch("bottlenecks/loadBottlenecks");
+  }
+};
+</script>
+
+<style lang="scss" scoped>
+.bottleneck-list {
+  overflow-y: auto;
+}
+
+.bottleneck-list .bottleneck-row a {
+  text-decoration: none;
+}
+
+.bottleneck-list .bottleneck-row:hover {
+  background: #fbfbfb;
+}
+
+.surveys {
+  max-height: 0;
+  min-height: 0;
+  overflow: hidden;
+}
+
+.surveys a:hover {
+  background: #f3f3f3;
+}
+
+.surveys.open {
+  max-height: 250px;
+  overflow: auto;
+}
+
+.sort-link {
+  color: #444;
+  font-weight: bold;
+}
+</style>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/client/src/components/Contextbox.vue	Wed Dec 12 09:22:20 2018 +0100
@@ -0,0 +1,96 @@
+<template>
+  <div :class="style">
+    <div @click="close" class="ui-element close-contextbox text-muted">
+      <font-awesome-icon icon="times"></font-awesome-icon>
+    </div>
+    <Bottlenecks v-if="contextBoxContent === 'bottlenecks'"></Bottlenecks>
+    <Importsounding v-if="contextBoxContent === 'imports'"></Importsounding>
+    <Staging v-if="contextBoxContent === 'staging'"></Staging>
+  </div>
+</template>
+
+<script>
+/* 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):
+ * Markus Kottländer <markus.kottlaender@intevation.de>
+ */
+import { mapState } from "vuex";
+
+export default {
+  name: "contextbox",
+  components: {
+    Bottlenecks: () => import("./Bottlenecks"),
+    Importsounding: () => import("./ImportSoundingresults.vue"),
+    Staging: () => import("./Staging.vue")
+  },
+  computed: {
+    ...mapState("application", [
+      "showSearchbarLastState",
+      "contextBoxContent",
+      "showContextBox"
+    ]),
+    style() {
+      return [
+        "ui-element shadow-xs contextbox",
+        {
+          contextboxcollapsed: !this.showContextBox,
+          contextboxextended: this.showContextBox,
+          "rounded-bottom": this.contextBoxContent !== "imports",
+          rounded: this.contextBoxContent === "imports"
+        }
+      ];
+    }
+  },
+  methods: {
+    close() {
+      this.$store.commit("application/showContextBox", false);
+      this.$store.commit(
+        "application/showSearchbar",
+        this.showSearchbarLastState
+      );
+    }
+  }
+};
+</script>
+
+<style lang="scss" scoped>
+.contextbox {
+  position: relative;
+  background-color: #ffffff;
+  opacity: $slight-transparent;
+  transition: max-width 0.3s, max-height 0.3s;
+  overflow: hidden;
+  background: #fff;
+}
+.contextbox > div:last-child {
+  width: 600px;
+}
+
+.contextboxcollapsed {
+  max-width: 0;
+  max-height: 0;
+}
+
+.contextboxextended {
+  max-width: 600px;
+  max-height: 640px;
+}
+
+.close-contextbox {
+  position: absolute;
+  z-index: 2;
+  right: 0;
+  top: 7px;
+  height: $icon-width;
+  width: $icon-height;
+}
+</style>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/client/src/components/Identify.vue	Wed Dec 12 09:22:20 2018 +0100
@@ -0,0 +1,124 @@
+<template>
+  <div
+    :class="[
+      'box ui-element rounded bg-white text-nowrap',
+      { expanded: showIdentify }
+    ]"
+  >
+    <div style="width: 20rem">
+      <h6 class="mb-0 py-2 px-3 border-bottom d-flex align-items-center">
+        <font-awesome-icon icon="info" class="mr-2"></font-awesome-icon>
+        <translate>Identified</translate>
+        <font-awesome-icon
+          icon="times"
+          class="ml-auto text-muted"
+          @click="$store.commit('application/showIdentify', false)"
+        ></font-awesome-icon>
+      </h6>
+      <div class="d-flex flex-column features p-3 flex-grow-1 text-left">
+        <div v-if="currentMeasurement">
+          <b>
+            {{ currentMeasurement.quantity }} ({{
+              currentMeasurement.unitSymbol
+            }}):
+          </b>
+          <br />
+          <small>{{ currentMeasurement.value }}</small>
+        </div>
+        <div v-for="(feature, i) of identifiedFeatures" :key="feature.getId()">
+          <div v-if="feature.getId()" :class="{ 'mt-2': i }">
+            <strong>
+              {{
+                feature.getId().replace(/[.][^.]*$/, "")
+                /* cut away everything from the last . to the end */
+              }}:
+            </strong>
+            <small
+              v-for="(value, key) in prepareProperties(feature)"
+              :key="key"
+            >
+              <div v-if="value">{{ key }}:{{ value }}</div>
+            </small>
+          </div>
+        </div>
+        <div
+          v-if="!currentMeasurement && !identifiedFeatures.length"
+          class="text-muted small text-center my-auto"
+        >
+          <translate>No features identified.</translate>
+        </div>
+      </div>
+      <div class="versioninfo border-top p-3 text-left">
+        <span v-translate="{ license: 'AGPL-3.0-or-later' }">
+          This app uses <i>gemma</i>, which is Free Software under <br />
+          %{ license } without warranty, see docs for details.
+        </span>
+        <br />
+        <a href="https://hg.intevation.de/gemma/file/tip">
+          <translate>source-code</translate>
+        </a>
+        {{ versionStr }} <br />© via donau. &#x24D4; Intevation. <br />
+        <span v-translate="{ name: 'OpenSteetMap' }"
+          >Some data ©
+          <a href="https://www.openstreetmap.org/copyright">%{ name }</a>
+          contributors.
+        </span>
+        <p v-translate="{ geoLicense: 'CC-BY-4.0' }">
+          Uses
+          <a href="https://download.geonames.org/export/dump/readme.txt"
+            >GeoNames</a
+          >
+          under %{ geoLicense }.
+        </p>
+      </div>
+    </div>
+  </div>
+</template>
+
+<style lang="scss" scoped>
+.features {
+  max-height: 19rem;
+  overflow-y: auto;
+}
+
+.versioninfo {
+  font-size: 60%;
+  white-space: normal;
+}
+</style>
+
+<script>
+/* 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):
+ * Thomas Junk <thomas.junk@intevation.de>
+ * Bernhard E. Reiter <bernhard.reiter@intevation.de>
+ * Markus Kottländer <markus.kottlaender@intevation.de>
+ */
+import { mapState, mapGetters } from "vuex";
+
+export default {
+  name: "identify",
+  computed: {
+    ...mapGetters("application", ["versionStr"]),
+    ...mapState("application", ["showIdentify"]),
+    ...mapState("map", ["identifiedFeatures", "currentMeasurement"])
+  },
+  methods: {
+    prepareProperties(feature) {
+      // return dict object with propertyname:plainvalue prepared for display
+      var properties = feature.getProperties();
+      delete properties[feature.getGeometryName()];
+      return properties;
+    }
+  }
+};
+</script>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/client/src/components/ImportSoundingresults.vue	Wed Dec 12 09:22:20 2018 +0100
@@ -0,0 +1,379 @@
+<template>
+  <div>
+    <h6 class="mb-0 py-2 px-3 border-bottom d-flex align-items-center">
+      <font-awesome-icon icon="upload" class="mr-2"></font-awesome-icon>
+      <translate>Import Soundingresults</translate>
+    </h6>
+    <div v-if="editState" class="ml-auto mr-auto mt-4 w-95">
+      <div class="d-flex flex-column">
+        <div class="d-flex flex-row">
+          <div class="mt-1 text-left w-50 ml-2 mr-4">
+            <small class="text-muted">
+              <translate>Bottleneck</translate>
+            </small>
+            <select v-model="bottleneck" class="custom-select">
+              <option
+                v-for="bottleneck in availableBottlenecks"
+                :key="bottleneck"
+                >{{ bottleneck }}</option
+              >
+            </select>
+            <span class="text-danger">
+              <small v-if="!bottleneck">
+                <translate>Please select a bottleneck</translate>
+              </small>
+            </span>
+          </div>
+          <div class="d-flex flex-column mt-1 text-left w-50 mr-2">
+            <small class="text-muted">
+              <translate>Projection</translate>&nbsp;(EPSG)
+            </small>
+            <input
+              class="form-control"
+              v-model="projection"
+              value="4326"
+              placeholder="e.g. 4326"
+              type="number"
+            />
+            <span class="text-left text-danger">
+              <small v-if="!projection">
+                <translate>Please enter a projection</translate>
+              </small>
+            </span>
+          </div>
+        </div>
+        <div class="d-flex flex-row">
+          <div class="mt-1 text-left w-50 ml-2 mr-4">
+            <small class="text-muted">
+              <translate>Depthreference</translate>
+            </small>
+            <select
+              v-model="depthReference"
+              class="custom-select"
+              id="depthreference"
+            >
+              <option
+                v-for="option in this.$options.depthReferenceOptions"
+                :key="option"
+                >{{ option }}</option
+              >
+            </select>
+            <span class="text-left text-danger">
+              <small v-if="!depthReference">
+                <translate>Please enter a reference</translate>
+              </small>
+            </span>
+          </div>
+          <div class="mt-1 text-left w-50 mr-2">
+            <small class="text-muted"> <translate>Date</translate> </small>
+            <input
+              id="importdate"
+              type="date"
+              class="form-control"
+              placeholder="Date of import"
+              aria-label="bottleneck"
+              aria-describedby="bottlenecklabel"
+              v-model="importDate"
+            />
+            <span class="text-left text-danger">
+              <small v-if="!importDate">
+                <translate>Please enter a date</translate>
+              </small>
+            </span>
+          </div>
+        </div>
+      </div>
+      <div class="ml-2 mt-2 text-left">
+        <small v-for="(message, index) in messages" :key="index">
+          {{ message }}
+        </small>
+      </div>
+    </div>
+    <div class="w-95 ml-auto mr-auto mt-4 mb-4">
+      <div v-if="uploadState" class="d-flex flex-row input-group mb-4">
+        <div class="custom-file">
+          <input
+            accept=".zip"
+            type="file"
+            @change="fileSelected"
+            class="custom-file-input"
+            id="uploadFile"
+          />
+          <label class="custom-file-label" for="uploadFile">
+            {{ uploadLabel }}
+          </label>
+        </div>
+      </div>
+      <div class="buttons text-right">
+        <a
+          v-if="editState"
+          download="meta.json"
+          :href="dataLink"
+          class="btn btn-outline-info pull-left"
+        >
+          <translate>Download Meta.json</translate>
+        </a>
+        <button
+          v-if="editState"
+          @click="deleteTempData"
+          class="btn btn-danger"
+          type="button"
+        >
+          <translate>Cancel Upload</translate>
+        </button>
+        <button
+          :disabled="disableUploadButton"
+          @click="submit"
+          class="btn btn-info"
+          type="button"
+        >
+          {{ uploadState ? Upload : Confirm }}
+        </button>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+/* 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):
+ * Thomas Junk <thomas.junk@intevation.de>
+ * Markus Kottländer <markus.kottlaender@intevation.de>
+ */
+import { HTTP } from "../lib/http";
+import { displayError, displayInfo } from "../lib/errors.js";
+import { mapState } from "vuex";
+
+const IMPORTSTATE = { UPLOAD: "UPLOAD", EDIT: "EDIT" };
+
+export default {
+  name: "imports",
+  data() {
+    return {
+      importState: IMPORTSTATE.UPLOAD,
+      depthReference: "",
+      bottleneck: "",
+      projection: "",
+      importDate: "",
+      uploadLabel: this.$gettext("choose .zip- file"),
+      uploadFile: null,
+      disableUpload: false,
+      token: null,
+      messages: []
+    };
+  },
+  methods: {
+    initialState() {
+      this.importState = IMPORTSTATE.UPLOAD;
+      this.depthReference = "";
+      this.bottleneck = "";
+      this.projection = "";
+      this.importDate = "";
+      this.uploadLabel = this.$gettext("choose .zip- file");
+      this.uploadFile = null;
+      this.disableUpload = false;
+      this.token = null;
+      this.messages = [];
+    },
+    fileSelected(e) {
+      const files = e.target.files || e.dataTransfer.files;
+      if (!files) return;
+      this.uploadLabel = files[0].name;
+      this.uploadFile = files[0];
+    },
+    deleteTempData() {
+      HTTP.delete("/imports/soundingresult-upload/" + this.token, {
+        headers: {
+          "X-Gemma-Auth": localStorage.getItem("token")
+        }
+      })
+        .then(() => {
+          this.initialState();
+        })
+        .catch(error => {
+          const { status, data } = error.response;
+          displayError({
+            title: this.$gettext("Backend Error"),
+            message: `${status}: ${data.message || data}`
+          });
+        });
+    },
+    submit() {
+      if (!this.uploadFile || this.disableUpload) return;
+      if (this.importState === IMPORTSTATE.UPLOAD) {
+        this.upload();
+      } else {
+        this.confirm();
+      }
+    },
+    upload() {
+      let formData = new FormData();
+      formData.append("soundingresult", this.uploadFile);
+      HTTP.post("/imports/soundingresult-upload", formData, {
+        headers: {
+          "X-Gemma-Auth": localStorage.getItem("token"),
+          "Content-Type": "multipart/form-data"
+        }
+      })
+        .then(response => {
+          if (response.data.meta) {
+            const { bottleneck, date, epsg } = response.data.meta;
+            const depthReference = response.data.meta["depth-reference"];
+            this.bottleneck = bottleneck;
+            this.depthReference = depthReference;
+            this.importDate = new Date(date).toISOString().split("T")[0];
+            this.projection = epsg;
+          }
+          this.importState = IMPORTSTATE.EDIT;
+          this.token = response.data.token;
+          this.messages = response.data.messages;
+        })
+        .catch(error => {
+          const { status, data } = error.response;
+          const messages = data.messages ? data.messages.join(", ") : "";
+          displayError({
+            title: this.$gettext("Backend Error"),
+            message: `${status}: ${messages}`
+          });
+        });
+    },
+    confirm() {
+      let formData = new FormData();
+      formData.append("token", this.token);
+      if (this.bottleneck) formData.append("bottleneck", this.bottleneck);
+      if (this.importDate)
+        formData.append("date", this.importDate.split("T")[0]);
+      if (this.depthReference)
+        formData.append("depth-reference", this.depthReference);
+      if (this.projection) formData.append("", this.projection);
+
+      HTTP.post("/imports/soundingresult", formData, {
+        headers: {
+          "X-Gemma-Auth": localStorage.getItem("token"),
+          "Content-Type": "multipart/form-data"
+        }
+      })
+        .then(() => {
+          displayInfo({
+            title: this.$gettext("Import"),
+            message: this.$gettext("Starting import for ") + this.bottleneck
+          });
+          this.initialState();
+        })
+        .catch(error => {
+          const { status, data } = error.response;
+          displayError({
+            title: this.$gettext("Backend Error"),
+            message: `${status}: ${data.message || data}`
+          });
+        });
+    }
+  },
+  mounted() {
+    this.$store.dispatch("bottlenecks/loadBottlenecks");
+  },
+  watch: {
+    showContextBox() {
+      if (!this.showContextBox && this.token) this.deleteTempData();
+    }
+  },
+  computed: {
+    ...mapState("application", ["showContextBox"]),
+    ...mapState("bottlenecks", ["bottlenecks"]),
+    disableUploadButton() {
+      if (this.importState === IMPORTSTATE.UPLOAD) return this.disableUpload;
+      if (
+        !this.bottleneck ||
+        !this.importDate ||
+        !this.depthReference ||
+        !this.projection
+      )
+        return true;
+      return this.disableUpload;
+    },
+    availableBottlenecks() {
+      return this.bottlenecks.map(x => x.properties.name);
+    },
+    editState() {
+      return this.importState === IMPORTSTATE.EDIT;
+    },
+    uploadState() {
+      return this.importState === IMPORTSTATE.UPLOAD;
+    },
+    Upload() {
+      return this.$gettext("Upload");
+    },
+    Confirm() {
+      return this.$gettext("Confirm");
+    },
+    dataLink() {
+      return (
+        "data:text/json;charset=utf-8," +
+        encodeURIComponent(
+          JSON.stringify({
+            depthReference: this.depthReference,
+            bottleneck: this.bottleneck,
+            date: this.importDate
+          })
+        )
+      );
+    }
+  },
+  depthReferenceOptions: [
+    "",
+    // "NAP",
+    // "KP",
+    // "FZP",
+    // "ADR",
+    // "TAW",
+    // "PUL",
+    // "NGM",
+    // "ETRS",
+    // "POT",
+    // "LDC",
+    // "HDC",
+    // "ZPG",
+    // "GLW",
+    // "HSW",
+    // "LNW",
+    // "HNW",
+    // "IGN",
+    // "WGS",
+    "RN" //,
+    // "HBO"
+  ]
+};
+</script>
+
+<style lang="scss" scoped>
+.projectionLabel {
+  margin-left: $small-offset;
+}
+
+.depthreferencelabel {
+  margin-left: $small-offset;
+}
+
+.offset-r {
+  margin-right: $small-offset;
+}
+
+.buttons button {
+  margin-left: $offset !important;
+}
+
+.label-text {
+  width: 5rem;
+  text-align: left;
+  line-height: 2.25rem;
+}
+</style>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/client/src/components/Importqueue.vue	Wed Dec 12 09:22:20 2018 +0100
@@ -0,0 +1,362 @@
+<template>
+  <div class="d-flex flex-row">
+    <div :class="spacerStyle"></div>
+    <div class="mt-3 importqueuecard flex-grow-1">
+      <div class="card shadow-xs">
+        <h6
+          class="mb-0 py-2 px-3 border-bottom d-flex text-info align-items-center"
+        >
+          <font-awesome-icon icon="tasks" class="mr-2"></font-awesome-icon>
+          <translate class="headline">Importqueue</translate>
+        </h6>
+        <div class="card-body importcardbody">
+          <div class="card-body importcardbody">
+            <div class="searchandfilter d-flex flex-row">
+              <div class="searchgroup input-group">
+                <div class="input-group-prepend">
+                  <span class="input-group-text" id="search">
+                    <font-awesome-icon icon="search"></font-awesome-icon>
+                  </span>
+                </div>
+                <input
+                  v-model="searchQuery"
+                  type="text"
+                  class="form-control"
+                  placeholder
+                  aria-label="Search"
+                  aria-describedby="search"
+                />
+              </div>
+              <div class="filters">
+                <button
+                  @click="setFilter('successful')"
+                  :class="successfulStyle"
+                >
+                  <translate>Successful</translate>
+                </button>
+                <button @click="setFilter('failed')" :class="failedStyle">
+                  <translate>Failed</translate>
+                </button>
+                <button @click="setFilter('pending')" :class="pendingStyle">
+                  <translate>Pending</translate>
+                </button>
+                <button @click="setFilter('rejected')" :class="rejectedStyle">
+                  <translate>Rejected</translate>
+                </button>
+                <button @click="setFilter('accepted')" :class="acceptedStyle">
+                  <translate>Accepted</translate>
+                </button>
+              </div>
+            </div>
+            <div class="text-left d-flex flex-row w-50 border-bottom">
+              <div class="header py-1 jobid mr-2">
+                <translate>Id</translate>
+              </div>
+              <div class="header py-1 enqueued mr-2">
+                <translate>Enqueued</translate>
+              </div>
+              <div class="header py-1 kind mr-2">
+                <translate>Kind</translate>
+              </div>
+              <div class="header py-1 user mr-2">
+                <translate>User</translate>
+              </div>
+              <div class="header py-1 signer mr-2">
+                <translate>Signer</translate>
+              </div>
+              <div class="header py-1 state mr-2">
+                <translate>State</translate>
+              </div>
+            </div>
+            <div class="text-left" v-for="job in filteredImports" :key="job.id">
+              <Importqueuedetail :job="job"></Importqueuedetail>
+            </div>
+            <div>
+              <button @click="refresh" class="btn btn-info refresh">
+                <translate>Refresh</translate>
+              </button>
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+/* 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):
+ * Markus Kottländer <markus@intevation.de>
+ */
+import { displayError } from "../lib/errors.js";
+import { mapState } from "vuex";
+import { HTTP } from "../lib/http.js";
+import Importqueuedetail from "./Importqueuedetail";
+
+export default {
+  name: "importqueue",
+  components: {
+    Importqueuedetail
+  },
+  data() {
+    return {
+      searchQuery: "",
+      successful: false,
+      failed: false,
+      pending: false,
+      rejected: false,
+      accepted: false
+    };
+  },
+  mounted() {
+    this.loadQueue();
+  },
+  methods: {
+    setFilter(name) {
+      this[name] = !this[name];
+      const allSet =
+        this.successful &&
+        this.failed &&
+        this.pending &&
+        this.accepted &&
+        this.rejected;
+      if (allSet) {
+        this.successful = false;
+        this.failed = false;
+        this.pending = false;
+        this.accepted = false;
+        this.rejected = false;
+      }
+    },
+    loadQueue() {
+      this.$store.dispatch("imports/getImports").catch(error => {
+        const { status, data } = error.response;
+        displayError({
+          title: this.$gettext("Backend Error"),
+          message: `${status}: ${data.message || data}`
+        });
+      });
+    },
+    refresh() {
+      this.loadQueue();
+    },
+    showDetails(id) {
+      HTTP.get("/imports/" + id, {
+        headers: { "X-Gemma-Auth": localStorage.getItem("token") }
+      })
+        .then(response => {
+          const { entries } = response.data;
+          this.entries = entries;
+          this.$modal.show("details");
+        })
+        .catch(error => {
+          const { status, data } = error.response;
+          displayError({
+            title: this.$gettext("Backend Error"),
+            message: `${status}: ${data.message || data}`
+          });
+        });
+    },
+    close() {
+      this.$modal.hide("details");
+    }
+  },
+  computed: {
+    ...mapState("imports", ["imports"]),
+    ...mapState("application", ["showSidebar"]),
+    sortIcon() {
+      return this.sortAsc ? "sort-amount-down" : "sort-amount-up";
+    },
+    filteredImports() {
+      const filtered = this.imports
+        .filter(element => {
+          if (!this.searchQuery) return true;
+          return [(element.kind, element.user, element.enqueued)].some(x => {
+            return x.toLowerCase().includes(this.searchQuery.toLowerCase());
+          });
+        })
+        .filter(y => {
+          if (
+            !this.successful &&
+            !this.failed &&
+            !this.pending &&
+            !this.accepted &&
+            !this.rejected
+          )
+            return true;
+          let filterCriteria = [];
+          if (this.successful) filterCriteria.push("successful");
+          if (this.failed) filterCriteria.push("failed");
+          if (this.pending) filterCriteria.push("pending");
+          if (this.accepted) filterCriteria.push("accepted");
+          if (this.rejected) filterCriteria.push("rejected");
+          const result = filterCriteria.map(selectedState => {
+            return y.state === selectedState;
+          });
+          return result.some(x => x);
+        });
+      return filtered;
+    },
+    spacerStyle() {
+      return [
+        "spacer ml-3",
+        {
+          "spacer-expanded": this.showSidebar,
+          "spacer-collapsed": !this.showSidebar
+        }
+      ];
+    },
+    successfulStyle() {
+      return {
+        btn: true,
+        "btn-light": !this.successful,
+        "btn-dark": this.successful
+      };
+    },
+    pendingStyle() {
+      return {
+        btn: true,
+        "btn-light": !this.pending,
+        "btn-dark": this.pending
+      };
+    },
+    failedStyle() {
+      return {
+        btn: true,
+        "btn-light": !this.failed,
+        "btn-dark": this.failed
+      };
+    },
+    rejectedStyle() {
+      return {
+        btn: true,
+        "btn-light": !this.rejected,
+        "btn-dark": this.rejected
+      };
+    },
+    acceptedStyle() {
+      return {
+        btn: true,
+        "btn-light": !this.accepted,
+        "btn-dark": this.accepted
+      };
+    }
+  }
+};
+</script>
+
+<style lang="scss" scoped>
+.jobid {
+  width: 80px;
+}
+
+.enqueued {
+  width: 120px;
+}
+
+.user {
+  width: 80px;
+}
+
+.signer {
+  width: 80px;
+}
+
+.kind {
+  width: 80px;
+}
+
+.state {
+  width: 80px;
+}
+
+.header {
+  font-weight: bold;
+  font-size: 0.9em;
+}
+
+.details thead {
+  display: block;
+}
+.details tbody {
+  display: block;
+}
+
+.details tbody {
+  height: 260px;
+  overflow-y: auto;
+  overflow-x: hidden;
+}
+
+.closebutton {
+  top: $small-offset;
+}
+
+.refresh {
+  position: absolute;
+  right: $offset;
+  bottom: $offset;
+}
+
+.spacer {
+  height: 100vh;
+}
+
+.spacer-collapsed {
+  min-width: $icon-width + $offset;
+  transition: $transition-fast;
+}
+
+.spacer-expanded {
+  min-width: $sidebar-width;
+}
+
+.importqueuecard {
+  width: 97%;
+  margin-left: $offset;
+  margin-right: $offset;
+  min-height: 20rem;
+}
+
+.card-body {
+  width: 100%;
+  margin-left: auto;
+  margin-right: auto;
+}
+
+.searchandfilter {
+  position: relative;
+  margin-bottom: $xx-large-offset;
+}
+
+.filters {
+  position: absolute;
+  right: 0;
+}
+
+.filters button {
+  margin-right: $small-offset;
+}
+
+.table td,
+.table th {
+  border-top: 0 !important;
+  text-align: left;
+  padding: $small-offset !important;
+}
+
+.searchgroup {
+  position: absolute;
+  left: 0;
+  width: 45%;
+}
+</style>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/client/src/components/Importqueuedetail.vue	Wed Dec 12 09:22:20 2018 +0100
@@ -0,0 +1,275 @@
+<template>
+  <div class="entry d-flex flex-column py-1 border-bottom w-50">
+    <div class="d-flex flex-row position-relative">
+      <div @click="showDetails(job.id)" class="jobid ml-2 mt-2 mr-2">
+        {{ job.id }}
+      </div>
+      <div @click="showDetails(job.id)" class="enqueued mt-2  mr-2">
+        {{ formatDate(job.enqueued) }}
+      </div>
+      <div @click="showDetails(job.id)" class="kind mt-2 mr-2">
+        {{ job.kind }}
+      </div>
+      <div @click="showDetails(job.id)" class="user mt-2 mr-2">
+        {{ job.user }}
+      </div>
+      <div @click="showDetails(job.id)" class="signer mt-2 mr-2">
+        {{ job.signer }}
+      </div>
+      <div @click="showDetails(job.id)" class="state mt-2 mr-2">
+        {{ job.state }}
+      </div>
+      <div
+        @click="showDetails(job.id)"
+        class="btn btn-sm h-100 rounded-0 btn-info detailsbutton"
+      >
+        <font-awesome-icon
+          v-if="show"
+          icon="angle-up"
+          fixed-width
+        ></font-awesome-icon>
+        <font-awesome-icon
+          v-else
+          icon="angle-down"
+          fixed-width
+        ></font-awesome-icon>
+      </div>
+    </div>
+    <div class="detailstable d-flex flex-row">
+      <div :class="collapse">
+        <table class="table table-responsive">
+          <thead>
+            <tr>
+              <th class="first pb-0">
+                <small class="condensed"><translate>Kind</translate></small>
+              </th>
+              <th class="second  pb-0">
+                <a href="#" @click="sortAsc = !sortAsc" class="sort-link"
+                  ><small class="condensed"><translate>Date</translate></small>
+                  <small class="condensed"
+                    ><font-awesome-icon
+                      :icon="sortIcon"
+                      class="ml-1"
+                    ></font-awesome-icon></small
+                ></a>
+              </th>
+              <th class="third pb-0">
+                <small class="condensed"><translate>Message</translate></small>
+              </th>
+            </tr>
+          </thead>
+          <tbody>
+            <tr
+              v-for="(entry, index) in sortedEntries"
+              :key="index"
+              class="detailsrow"
+            >
+              <td class="first">
+                <span class="condensed">{{ entry.kind }}</span>
+              </td>
+              <td class="second">
+                <span class="condensed">{{ formatDate(entry.time) }}</span>
+              </td>
+              <td class="third">
+                <span class="condensed">{{ entry.message }}</span>
+              </td>
+            </tr>
+          </tbody>
+        </table>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+/* 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):
+ * Thomas Junk <thomas.junk@intevation.de>
+ */
+
+import { HTTP } from "../lib/http.js";
+import { displayError } from "../lib/errors.js";
+import locale2 from "locale2";
+
+export default {
+  name: "importqueuedetail",
+  props: ["job"],
+  data() {
+    return {
+      show: false,
+      entries: [],
+      sortAsc: true
+    };
+  },
+  methods: {
+    formatDate(date) {
+      return date
+        ? new Date(date).toLocaleDateString(locale2, {
+            day: "2-digit",
+            month: "2-digit",
+            year: "numeric"
+          })
+        : "";
+    },
+    showDetails(id) {
+      if (this.show) {
+        this.show = false;
+        return;
+      }
+      if (this.entries.length === 0) {
+        HTTP.get("/imports/" + id, {
+          headers: { "X-Gemma-Auth": localStorage.getItem("token") }
+        })
+          .then(response => {
+            const { entries } = response.data;
+            this.entries = entries;
+            this.show = true;
+          })
+          .catch(error => {
+            const { status, data } = error.response;
+            displayError({
+              title: this.$gettext("Backend Error"),
+              message: `${status}: ${data.message || data}`
+            });
+          });
+      } else {
+        this.show = true;
+      }
+    }
+  },
+  computed: {
+    sortedEntries() {
+      let sorted = this.entries.slice();
+      sorted.sort((r1, r2) => {
+        let d1 = new Date(r1.time);
+        let d2 = new Date(r2.time);
+        if (d2 < d1) {
+          return !this.sortAsc ? -1 : 1;
+        }
+        if (d2 > d1) {
+          return !this.sortAsc ? 1 : -1;
+        }
+        return 0;
+      });
+      return sorted;
+    },
+    sortIcon() {
+      return this.sortAsc ? "sort-amount-down" : "sort-amount-up";
+    },
+    icon() {
+      return {
+        "angle-up": !this.show,
+        "angle-down": this.show
+      };
+    },
+    collapse() {
+      return {
+        details: true,
+        collapse: true,
+        show: this.show,
+        "w-100": true
+      };
+    }
+  }
+};
+</script>
+
+<style lang="scss" scoped>
+.condensed {
+  font-stretch: condensed;
+}
+
+.entry {
+  background-color: white;
+  cursor: pointer;
+}
+
+.entry:hover {
+  background-color: #f0f0f0;
+  transition: 1s;
+}
+
+.detailstable {
+  margin-left: $offset;
+  margin-right: $large-offset;
+}
+
+.detailsbutton {
+  position: absolute;
+  top: 0;
+  right: 0;
+  height: 100%;
+}
+.jobid {
+  width: 80px;
+}
+
+.enqueued {
+  width: 120px;
+}
+
+.user {
+  width: 80px;
+}
+
+.signer {
+  width: 80px;
+}
+
+.kind {
+  width: 80px;
+}
+
+.state {
+  width: 80px;
+}
+
+.details {
+  width: 50%;
+}
+
+.detailsrow {
+  line-height: 0.1em;
+}
+
+.first {
+  width: 65px;
+  padding-left: 0px;
+  border-top: 0px;
+  padding-bottom: $small-offset;
+}
+
+.second {
+  width: 100px;
+  padding-left: 0px;
+  border-top: 0px;
+  padding-bottom: $small-offset;
+}
+
+.third {
+  width: 600px;
+  padding-left: 0px;
+  border-top: 0px;
+  padding-bottom: $small-offset;
+}
+
+thead,
+tbody {
+  display: block;
+}
+
+tbody {
+  height: 150px;
+  overflow-y: auto;
+  overflow-x: hidden;
+}
+</style>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/client/src/components/Logs.vue	Wed Dec 12 09:22:20 2018 +0100
@@ -0,0 +1,198 @@
+<template>
+  <div class="main d-flex flex-column">
+    <div class="d-flex flex-row">
+      <div :class="spacer"></div>
+      <div class="card logs shadow-xs mt-3 mr-3">
+        <h6
+          class="mb-0 py-2 px-3 border-bottom d-flex text-info align-items-center"
+        >
+          <font-awesome-icon class="mr-2 fa-fw" icon="book"></font-awesome-icon>
+          <translate class="headline">Logs</translate>
+        </h6>
+        <div class="logoutput text-left bg-white">
+          <pre id="code" v-highlightjs="logs">
+          <code class="bash hljs hljs-string"></code>
+          </pre>
+        </div>
+        <div class="logmenu">
+          <div class="d-flex align-self-center">
+            <ul class="nav nav-pills">
+              <li class="nav-item">
+                <a
+                  :class="accesslogStyle"
+                  @click="fetch('system/log/apache2/access.log', 'accesslog')"
+                  href="#"
+                >
+                  <translate>Accesslog</translate>
+                </a>
+              </li>
+              <li class="nav-item">
+                <a
+                  :class="errorlogStyle"
+                  @click="fetch('system/log/apache2/error.log', 'errorlog')"
+                  href="#"
+                >
+                  <translate>Errorlog</translate>
+                </a>
+              </li>
+            </ul>
+          </div>
+          <div class="statuscontainer d-flex flex-row mb-3">
+            <div class="statusline align-self-center">
+              <h3><translate>Last refresh:</translate> {{ refreshed }}</h3>
+            </div>
+            <div class="refresh">
+              <button
+                @click="fetch(currentFile, currentLog)"
+                class="btn btn-dark"
+              >
+                <translate>Refresh</translate>
+              </button>
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<style lang="scss" scoped>
+.statuscontainer {
+  width: 87%;
+  position: relative;
+}
+
+.logmenu {
+  position: relative;
+  margin-left: $offset;
+  margin-top: $offset;
+}
+
+.logs {
+  height: 85vh;
+}
+
+#code {
+  overflow: auto;
+}
+
+.refresh {
+  position: absolute;
+  right: $offset;
+  bottom: 0;
+}
+
+.logoutput {
+  margin-left: $offset;
+  margin-right: $offset;
+  margin-top: $offset;
+  height: 90%;
+  overflow: auto;
+  transition: $transition-fast;
+}
+
+.spacer {
+  height: 90vh;
+}
+
+.spacer-collapsed {
+  min-width: $icon-width + $offset;
+  transition: $transition-fast;
+}
+
+.spacer-expanded {
+  min-width: $sidebar-width + $offset;
+}
+
+.statusline {
+  position: absolute;
+  right: 0;
+  margin-right: 9rem;
+  bottom: -0.5rem;
+}
+
+.statuscontainer {
+  width: 100%;
+}
+</style>
+
+<script>
+/* 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):
+ * Thomas Junk <thomas.junk@intevation.de>
+ */
+import { mapState } from "vuex";
+import { HTTP } from "../lib/http.js";
+import "../../node_modules/highlight.js/styles/paraiso-dark.css";
+import Vue from "vue";
+import VueHighlightJS from "vue-highlightjs";
+Vue.use(VueHighlightJS);
+
+const ACCESSLOG = "accesslog";
+const ERRORLOG = "errorlog";
+
+export default {
+  name: "logs",
+  mounted() {
+    this.fetch("system/log/apache2/access.log", ACCESSLOG);
+  },
+  data() {
+    return {
+      logs: null,
+      currentLog: null,
+      currentFile: null,
+      refreshed: null
+    };
+  },
+  methods: {
+    fetch(file, type) {
+      HTTP.get(file, {
+        headers: { "X-Gemma-Auth": localStorage.getItem("token") }
+      })
+        .then(response => {
+          this.logs = response.data.content;
+          this.currentLog = type;
+          this.refreshed = new Date().toLocaleString();
+          this.currentFile = file;
+        })
+        .catch();
+    },
+    disallow(e) {
+      e.target.blur();
+    }
+  },
+  computed: {
+    ...mapState("application", ["showSidebar"]),
+    accesslogStyle() {
+      return {
+        active: this.currentLog == ACCESSLOG,
+        "nav-link": true
+      };
+    },
+    errorlogStyle() {
+      return {
+        active: this.currentLog == ERRORLOG,
+        "nav-link": true
+      };
+    },
+    spacer() {
+      return [
+        "spacer ml-3",
+        {
+          "spacer-expanded": this.showSidebar,
+          "spacer-collapsed": !this.showSidebar
+        }
+      ];
+    }
+  }
+};
+</script>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/client/src/components/Main.vue	Wed Dec 12 09:22:20 2018 +0100
@@ -0,0 +1,33 @@
+<template>
+  <div class="main d-flex flex-column">
+    <Maplayer></Maplayer>
+    <FairwayProfile></FairwayProfile>
+  </div>
+</template>
+
+<script>
+/* 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):
+ * Thomas Junk <thomas.junk@intevation.de>
+ */
+
+import Maplayer from "./Maplayer";
+import FairwayProfile from "./fairway/Fairwayprofile";
+
+export default {
+  name: "mainview",
+  components: {
+    Maplayer,
+    FairwayProfile
+  }
+};
+</script>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/client/src/components/Maplayer.vue	Wed Dec 12 09:22:20 2018 +0100
@@ -0,0 +1,383 @@
+<template>
+  <div id="map" :class="mapStyle"></div>
+</template>
+
+<style lang="scss" scoped>
+.nocursor {
+  cursor: none;
+}
+
+.mapsplit {
+  height: 50vh;
+}
+
+.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>
+/* 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):
+ * * Thomas Junk <thomas.junk@intevation.de>
+ * * Bernhard E. Reiter <bernhard.reiter@intevation.de>
+ */
+import { HTTP } from "../lib/http";
+import { mapGetters, mapState } from "vuex";
+import "ol/ol.css";
+import { Map, View } from "ol";
+import { WFS, GeoJSON } from "ol/format.js";
+import { Stroke, Style, Fill } from "ol/style.js";
+
+/* for the sake of debugging */
+/* eslint-disable no-console */
+export default {
+  name: "maplayer",
+  data() {
+    return {
+      projection: "EPSG:3857"
+    };
+  },
+  computed: {
+    ...mapGetters("map", ["getLayerByName", "getVSourceByName"]),
+    ...mapState("map", [
+      "extent",
+      "layers",
+      "openLayersMap",
+      "lineTool",
+      "polygonTool",
+      "cutTool"
+    ]),
+    ...mapState("bottlenecks", ["selectedSurvey"]),
+    ...mapState("application", ["showSplitscreen"]),
+    mapStyle() {
+      return {
+        mapfull: !this.showSplitscreen,
+        mapsplit: this.showSplitscreen,
+        nocursor: this.hasActiveInteractions
+      };
+    },
+    hasActiveInteractions() {
+      return (
+        (this.lineTool && this.lineTool.getActive()) ||
+        (this.polygonTool && this.polygonTool.getActive()) ||
+        (this.cutTool && this.cutTool.getActive())
+      );
+    }
+  },
+  methods: {
+    buildVectorLoader(featureRequestOptions, endpoint, vectorSource) {
+      // build a function to be used for VectorSource.setLoader()
+      // make use of WFS().writeGetFeature to build the request
+      // and use our HTTP library to actually do it
+      // NOTE: a) the geometryName has to be given in featureRequestOptions,
+      //          because we want to load depending on the bbox
+      //  b) the VectorSource has to have the option strategy: bbox
+      featureRequestOptions["outputFormat"] = "application/json";
+      var loader = function(extent, resolution, projection) {
+        featureRequestOptions["bbox"] = extent;
+        featureRequestOptions["srsName"] = projection.getCode();
+        var featureRequest = new WFS().writeGetFeature(featureRequestOptions);
+        // DEBUG console.log(featureRequest);
+        HTTP.post(
+          endpoint,
+          new XMLSerializer().serializeToString(featureRequest),
+          {
+            headers: {
+              "X-Gemma-Auth": localStorage.getItem("token"),
+              "Content-type": "text/xml; charset=UTF-8"
+            }
+          }
+        )
+          .then(response => {
+            var features = new GeoJSON().readFeatures(
+              JSON.stringify(response.data)
+            );
+            vectorSource.addFeatures(features);
+            // console.log(
+            //   "loaded",
+            //   features.length,
+            //   featureRequestOptions.featureTypes,
+            //   "features"
+            // );
+            // DEBUG console.log("loaded ", features, "for", vectorSource);
+            // eslint-disable-next-line
+          })
+          .catch(() => {
+            vectorSource.removeLoadedExtent(extent);
+          });
+      };
+      return loader;
+    },
+    updateBottleneckFilter(bottleneck_id, datestr) {
+      console.log("updating filter with", bottleneck_id, datestr);
+      const layer = this.getLayerByName("Bottleneck isolines");
+      const wmsSrc = layer.data.getSource();
+      const exists = bottleneck_id != "does_not_exist";
+
+      if (exists) {
+        wmsSrc.updateParams({
+          cql_filter:
+            "date_info='" +
+            datestr +
+            "' AND bottleneck_id='" +
+            bottleneck_id +
+            "'"
+        });
+      }
+      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: {
+    showSplitscreen() {
+      const map = this.openLayersMap;
+      this.$nextTick(() => {
+        map && map.updateSize();
+      });
+    },
+    selectedSurvey(newSelectedSurvey) {
+      if (newSelectedSurvey) {
+        this.updateBottleneckFilter(
+          newSelectedSurvey.bottleneck_id,
+          newSelectedSurvey.date_info
+        );
+      } else {
+        this.updateBottleneckFilter("does_not_exist", "1999-10-01");
+      }
+    }
+  },
+  mounted() {
+    let map = new Map({
+      layers: [...this.layers.map(x => x.data)],
+      target: "map",
+      controls: [],
+      view: new View({
+        center: [this.extent.lon, this.extent.lat],
+        zoom: this.extent.zoom,
+        projection: this.projection
+      })
+    });
+    map.on("moveend", event => {
+      const center = event.map.getView().getCenter();
+      this.$store.commit("map/extent", {
+        lat: center[1],
+        lon: center[0],
+        zoom: event.map.getView().getZoom()
+      });
+    });
+    this.$store.dispatch("map/openLayersMap", map);
+
+    // TODO make display of layers more dynamic, e.g. from a list
+
+    // loading the full WFS layer, by not setting the loader function
+    // and without bboxStrategy
+    var featureRequest = new WFS().writeGetFeature({
+      srsName: "EPSG:3857",
+      featureNS: "gemma",
+      featurePrefix: "gemma",
+      featureTypes: ["fairway_dimensions"],
+      outputFormat: "application/json"
+    });
+
+    // NOTE: loading the full fairway_dimensions makes sure
+    //       that all are available for the intersection with the profile
+    HTTP.post(
+      "/internal/wfs",
+      new XMLSerializer().serializeToString(featureRequest),
+      {
+        headers: {
+          "X-Gemma-Auth": localStorage.getItem("token"),
+          "Content-type": "text/xml; charset=UTF-8"
+        }
+      }
+    ).then(response => {
+      this.getVSourceByName("Fairway Dimensions").addFeatures(
+        new GeoJSON().readFeatures(JSON.stringify(response.data))
+      );
+      // would scale to the extend of all resulting features
+      // this.openLayersMap.getView().fit(vectorSrc.getExtent());
+    });
+
+    // load following layers with bboxStrategy (using our request builder)
+    var layer = null;
+
+    layer = this.getLayerByName("Waterway Area");
+    layer.data.getSource().setLoader(
+      this.buildVectorLoader(
+        {
+          featurePrefix: "ws-wamos",
+          featureTypes: ["ienc_wtware"],
+          geometryName: "geom"
+        },
+        "/external/d4d",
+        layer.data.getSource()
+      )
+    );
+
+    layer = this.getLayerByName("Waterway Axis");
+    layer.data.getSource().setLoader(
+      this.buildVectorLoader(
+        {
+          featurePrefix: "ws-wamos",
+          featureTypes: ["ienc_wtwaxs"],
+          geometryName: "geom"
+        },
+        "/external/d4d",
+        layer.data.getSource()
+      )
+    );
+
+    layer = this.getLayerByName("Distance marks");
+    layer.data.getSource().setLoader(
+      this.buildVectorLoader(
+        {
+          featurePrefix: "ws-wamos",
+          featureTypes: ["ienc_dismar"],
+          geometryName: "geom" //,
+          /* restrict loading approximately to extend of danube in Austria */
+          // filter: bboxFilter("geom", [13.3, 48.0, 17.1, 48.6], "EPSG:4326")
+        },
+        "/external/d4d",
+        layer.data.getSource()
+      )
+    );
+    layer.data.setVisible(layer.isVisible);
+
+    layer = this.getLayerByName("Distance marks, Axis");
+    layer.data.getSource().setLoader(
+      this.buildVectorLoader(
+        {
+          featureNS: "gemma",
+          featurePrefix: "gemma",
+          featureTypes: ["distance_marks_geoserver"],
+          geometryName: "geom"
+        },
+        "/internal/wfs",
+        layer.data.getSource()
+      )
+    );
+
+    layer = this.getLayerByName("Waterway Area, named");
+    layer.data.getSource().setLoader(
+      this.buildVectorLoader(
+        {
+          featureNS: "gemma",
+          featurePrefix: "gemma",
+          featureTypes: ["hydro_seaare"],
+          geometryName: "geom"
+        },
+        "/external/d4d",
+        layer.data.getSource()
+      )
+    );
+    layer.data.setVisible(layer.isVisible);
+
+    layer = this.getLayerByName("Bottlenecks");
+    layer.data.getSource().setLoader(
+      this.buildVectorLoader(
+        {
+          featureNS: "gemma",
+          featurePrefix: "gemma",
+          featureTypes: ["bottlenecks"],
+          geometryName: "area"
+        },
+        "/internal/wfs",
+        layer.data.getSource()
+      )
+    );
+    HTTP.get("/system/style/Bottlenecks/stroke", {
+      headers: { "X-Gemma-Auth": localStorage.getItem("token") }
+    })
+      .then(response => {
+        this.btlnStrokeC = response.data.code;
+        HTTP.get("/system/style/Bottlenecks/fill", {
+          headers: { "X-Gemma-Auth": localStorage.getItem("token") }
+        })
+          .then(response => {
+            this.btlnFillC = response.data.code;
+            var newstyle = new Style({
+              stroke: new Stroke({
+                color: this.btlnStrokeC,
+                width: 4
+              }),
+              fill: new Fill({
+                color: this.btlnFillC
+              })
+            });
+            layer.data.setStyle(newstyle);
+          })
+          .catch(error => {
+            console.log(error);
+          });
+      })
+      .catch(error => {
+        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");
+    this.$store.dispatch("bottlenecks/loadBottlenecks");
+  }
+};
+</script>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/client/src/components/Pdftool.vue	Wed Dec 12 09:22:20 2018 +0100
@@ -0,0 +1,110 @@
+<template>
+  <div
+    :class="[
+      'box ui-element rounded bg-white text-nowrap',
+      { expanded: showPdfTool }
+    ]"
+  >
+    <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="times"
+          class="ml-auto text-muted"
+          @click="$store.commit('application/showPdfTool', false)"
+        ></font-awesome-icon>
+      </h6>
+      <div class="p-3">
+        <b><translate>Chose format:</translate></b>
+        <select v-model="form.format" class="form-control d-block w-100">
+          <option><translate>landscape</translate></option>
+          <option><translate>portrait</translate></option>
+        </select>
+        <small class="d-block my-2">
+          <input
+            type="radio"
+            id="pdfexport-downloadtype-download"
+            value="download"
+            v-model="form.downloadType"
+            selected
+          />
+          <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
+          >
+        </small>
+        <button
+          @click="download"
+          type="button"
+          class="btn btn-sm btn-info d-block w-100"
+        >
+          <translate>Generate PDF</translate>
+        </button>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+/* 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):
+ * Markus Kottländer <markus.kottlaender@intevation.de>
+ */
+import { mapState } from "vuex";
+//import { HTTP } from "../application/lib/http";
+
+export default {
+  name: "pdftool",
+  data() {
+    return {
+      form: {
+        format: "landscape",
+        downloadType: "download"
+      }
+    };
+  },
+  computed: {
+    ...mapState("application", ["showPdfTool"]),
+    ...mapState("bottlenecks", ["selectedSurvey"])
+  },
+  methods: {
+    download() {
+      // generate PDF and open it
+      // TODO: replace this src with an API reponse after actually generating PDFs
+      let src =
+        this.form.format === "landscape"
+          ? "/img/PrintTemplate-Var2-Landscape.pdf"
+          : "/img/PrintTemplate-Var2-Portrait.pdf";
+
+      let a = document.createElement("a");
+      a.href = src;
+
+      if (this.form.downloadType === "download")
+        a.download = src.substr(src.lastIndexOf("/") + 1);
+      else a.target = "_blank";
+
+      document.body.appendChild(a);
+      a.click();
+      document.body.removeChild(a);
+    }
+  }
+};
+</script>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/client/src/components/Search.vue	Wed Dec 12 09:22:20 2018 +0100
@@ -0,0 +1,292 @@
+<template>
+  <div :class="searchbarContainerStyle">
+    <div class="input-group-prepend m-0 d-print-none">
+      <span @click="toggleSearchbar" :class="searchButtonStyle" for="search">
+        <font-awesome-icon icon="search"></font-awesome-icon>
+      </span>
+    </div>
+    <div
+      :class="[
+        'searchgroup',
+        {
+          'searchgroup-collapsed': !showSearchbar,
+          big:
+            showContextBox &&
+            ['bottlenecks', 'staging'].indexOf(contextBoxContent) !== -1
+        }
+      ]"
+    >
+      <input
+        @keyup.enter="takeFirstSearchresult"
+        id="search"
+        v-model="searchQuery"
+        type="text"
+        :class="searchInputStyle"
+      />
+    </div>
+    <div
+      v-if="showSearchbar && searchResults !== null && !showContextBox"
+      class="searchresults border-top ui-element bg-white rounded-bottom d-print-none position-absolute"
+    >
+      <div
+        v-for="entry of searchResults"
+        :key="entry.name"
+        class="border-top text-left"
+      >
+        <a
+          href="#"
+          @click.prevent="moveToSearchResult(entry)"
+          class="p-2 d-block text-nowrap"
+        >
+          <font-awesome-icon
+            icon="ship"
+            v-if="entry.type === 'bottleneck'"
+            class="mr-1"
+            fixed-width
+          />
+          <font-awesome-icon
+            icon="water"
+            v-if="entry.type === 'rhm'"
+            class="mr-1"
+            fixed-width
+          />
+          <font-awesome-icon
+            icon="city"
+            v-if="entry.type === 'city'"
+            class="mr-1"
+            fixed-width
+          />
+          {{ entry.name }}
+        </a>
+      </div>
+    </div>
+  </div>
+</template>
+
+<style lang="scss" scoped>
+.searchcontainer {
+  opacity: 0.96;
+}
+
+.searchcontainer .searchbar {
+  border-top-left-radius: 0 !important;
+  border-bottom-left-radius: 0 !important;
+}
+
+.searchgroup {
+  margin-left: -3px;
+  transition: width 0.3s;
+  width: 300px;
+  overflow: hidden;
+}
+
+.searchgroup.big {
+  width: 571px;
+}
+
+.searchgroup-collapsed {
+  width: 0;
+}
+
+.searchbar {
+  height: 2rem !important;
+  box-shadow: none !important;
+}
+
+.searchbar.rounded-top-right {
+  border-radius: 0 !important;
+  border-top-right-radius: 0.25rem !important;
+}
+
+.searchlabel.rounded-top-left {
+  border-radius: 0 !important;
+  border-top-left-radius: 0.25rem !important;
+}
+
+.input-group-text {
+  height: 2rem;
+  width: 2rem;
+}
+
+.input-group-prepend svg path {
+  fill: #666;
+}
+
+.searchresults {
+  box-shadow: 0 0.1rem 0.5rem rgba(0, 0, 0, 0.2);
+  top: 2rem;
+  left: 0;
+  right: 0;
+  max-height: 24rem;
+  overflow: auto;
+}
+
+.searchresults > div:first-child {
+  border-top: 0 !important;
+}
+
+.searchresults a {
+  text-decoration: none;
+}
+
+.searchresults a:hover {
+  background: #f8f8f8;
+}
+</style>
+
+<script>
+/* 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):
+ * Markus Kottländer <markus.kottlaender@intevation.de>
+ */
+import debounce from "lodash.debounce";
+import { mapState } from "vuex";
+
+import { displayError } from "../lib/errors.js";
+import { HTTP } from "../lib/http";
+
+const setFocus = () => document.querySelector("#search").focus();
+
+export default {
+  name: "search",
+  data() {
+    return {
+      searchQueryIsDirty: false,
+      searchResults: null,
+      isSearching: false
+    };
+  },
+  computed: {
+    ...mapState("application", [
+      "showSearchbar",
+      "showContextBox",
+      "contextBoxContent"
+    ]),
+    searchQuery: {
+      get() {
+        return this.$store.state.application.searchQuery;
+      },
+      set(value) {
+        this.$store.commit("application/searchQuery", value);
+      }
+    },
+    searchIndicator: function() {
+      if (this.isSearching) {
+        return "⟳";
+      } else if (this.searchQueryIsDirty) {
+        return "";
+      } else {
+        return "✓";
+      }
+    },
+    searchbarContainerStyle() {
+      return [
+        "input-group searchcontainer shadow-xs",
+        {
+          "d-flex": this.contextBoxContent !== "imports",
+          "d-none": this.contextBoxContent === "imports" && this.showContextBox
+        }
+      ];
+    },
+    searchInputStyle() {
+      return [
+        "form-control ui-element search searchbar d-print-none border-0",
+        { "rounded-top-right": this.showContextBox || this.searchResults }
+      ];
+    },
+    searchButtonStyle() {
+      return [
+        "ui-element input-group-text p-0 d-flex border-0 justify-content-center searchlabel bg-white d-print-none",
+        {
+          rounded: !this.showSearchbar,
+          "rounded-left": this.showSearchbar,
+          "rounded-top-left":
+            this.showSearchbar && (this.showContextBox || this.searchResults)
+        }
+      ];
+    }
+  },
+  watch: {
+    searchQuery: function() {
+      this.searchQueryIsDirty = true;
+      this.triggerSearch();
+    }
+  },
+  methods: {
+    takeFirstSearchresult() {
+      if (!this.searchResults || this.searchResults.length != 1) return;
+      this.moveToSearchResult(this.searchResults[0]);
+    },
+    triggerSearch: debounce(function() {
+      this.doSearch();
+    }, 500),
+    doSearch() {
+      this.isCalculating = true;
+      this.searchResults = null;
+
+      if (this.searchQuery == "") {
+        return;
+      }
+
+      HTTP.post(
+        "/search",
+        { string: this.searchQuery },
+        {
+          headers: {
+            "X-Gemma-Auth": localStorage.getItem("token"),
+            "Content-type": "text/xml; charset=UTF-8"
+          }
+        }
+      )
+        .then(response => {
+          // console.log("got:", response.data);
+          this.searchResults = response.data;
+        })
+        .catch(error => {
+          const { status, data } = error.response;
+          displayError({
+            title: this.$gettext("Backend Error"),
+            message: `${status}: ${data.message || data}`
+          });
+        });
+
+      this.isCalculating = false;
+      this.searchQueryIsDirty = false;
+    },
+    moveToSearchResult(resultEntry) {
+      // DEBUG console.log("Moving to", resultEntry);
+      if (resultEntry.geom.type == "Point") {
+        let zoom = 11;
+        if (resultEntry.type === "bottleneck") zoom = 17;
+        if (resultEntry.type === "rhm") zoom = 15;
+        if (resultEntry.type === "city") zoom = 13;
+
+        this.$store.commit("map/moveMap", {
+          coordinates: resultEntry.geom.coordinates,
+          zoom,
+          preventZoomOut: true
+        });
+      }
+      // this.searchQuery = ""; // clear search query again
+      this.toggleSearchbar();
+    },
+    toggleSearchbar() {
+      if (!this.showContextBox) {
+        if (!this.showSearchbar) {
+          setTimeout(setFocus, 300);
+        }
+        this.$store.commit("application/showSearchbar", !this.showSearchbar);
+      }
+    }
+  }
+};
+</script>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/client/src/components/Staging.vue	Wed Dec 12 09:22:20 2018 +0100
@@ -0,0 +1,236 @@
+<template>
+  <div class="w-90 stagingcard">
+    <h6 class="mb-0 py-2 px-3 border-bottom d-flex align-items-center">
+      <font-awesome-icon
+        class="mr-2"
+        icon="clipboard-check"
+      ></font-awesome-icon>
+      <translate>Staging Area</translate>
+    </h6>
+    <table class="table">
+      <thead>
+        <tr>
+          <th><translate>Name</translate></th>
+          <th><translate>Type</translate></th>
+          <th><translate>Date</translate></th>
+          <th><translate>Imported</translate></th>
+          <th><translate>Username</translate></th>
+          <th>&nbsp;</th>
+          <th>&nbsp;</th>
+        </tr>
+      </thead>
+      <tbody v-if="filteredData.length">
+        <tr :key="data.id" v-for="data in filteredData">
+          <td>
+            <a @click="zoomTo(data.id)" href="#">{{
+              data.summary.bottleneck
+            }}</a>
+          </td>
+          <td>{{ data.kind.toUpperCase() }}</td>
+          <td>{{ formatSurveyDate(data.summary.date) }}</td>
+          <td>{{ formatSurveyDate(data.enqueued.split("T")[0]) }}</td>
+          <td>{{ data.user }}</td>
+          <td>
+            <button
+              :class="{
+                btn: true,
+                'btn-sm': true,
+                'btn-outline-success': needsApproval(data) || isRejected(data),
+                'btn-success': isApproved(data)
+              }"
+              @click="toggleApproval(data.id, $options.STATES.APPROVED)"
+            >
+              <font-awesome-icon icon="check"></font-awesome-icon>
+            </button>
+          </td>
+          <td>
+            <button
+              :class="{
+                btn: true,
+                'btn-sm': true,
+                'btn-outline-danger': needsApproval(data) || isApproved(data),
+                'btn-danger': isRejected(data)
+              }"
+              @click="toggleApproval(data.id, $options.STATES.REJECTED)"
+            >
+              <font-awesome-icon icon="times"></font-awesome-icon>
+            </button>
+          </td>
+        </tr>
+      </tbody>
+      <tbody v-else>
+        <tr>
+          <td class="text-center" colspan="6">
+            <translate>No results.</translate>
+          </td>
+        </tr>
+      </tbody>
+    </table>
+    <div class="p-3" v-if="filteredData.length">
+      <button @click="confirmReview" class="confirm-button btn btn-info">
+        <translate>Confirm</translate>
+      </button>
+    </div>
+    <div class="p-3">
+      <button @click="loadData" class="refresh btn btn-dark">Refresh</button>
+    </div>
+  </div>
+</template>
+
+<script>
+/* 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):
+ * Thomas Junk <thomas.junk@intevation.de>
+ * Markus Kottländer <markus@intevation.de>
+ */
+import { mapState } from "vuex";
+import { HTTP } from "../lib/http.js";
+import { STATES } from "../store/imports.js";
+import { displayError, displayInfo } from "../lib/errors.js";
+import { formatSurveyDate } from "../lib/date.js";
+
+export default {
+  data() {
+    return {};
+  },
+  mounted() {
+    this.loadData();
+  },
+  computed: {
+    ...mapState("application", ["searchQuery"]),
+    ...mapState("imports", ["staging"]),
+    filteredData() {
+      return this.staging.filter(data => {
+        const result = [data.id + "", data.enqueued, data.kind, data.user].some(
+          x => x.toLowerCase().includes(this.searchQuery.toLowerCase())
+        );
+        return result;
+      });
+    }
+  },
+  STATES: STATES,
+  methods: {
+    formatSurveyDate(date) {
+      return formatSurveyDate(date);
+    },
+    loadData() {
+      this.$store.dispatch("imports/getStaging").catch(error => {
+        const { status, data } = error.response;
+        displayError({
+          title: "Backend Error",
+          message: `${status}: ${data.message || data}`
+        });
+      });
+    },
+    confirmReview() {
+      const reviewResults = this.staging
+        .filter(x => x.status !== STATES.NEEDSAPPROVAL)
+        .map(r => {
+          return {
+            id: r.id,
+            state: r.status
+          };
+        });
+      if (!reviewResults.length) return;
+      HTTP.patch("/imports", reviewResults, {
+        headers: {
+          "X-Gemma-Auth": localStorage.getItem("token"),
+          "Content-type": "application/json"
+        }
+      })
+        .then(response => {
+          const messages = response.data
+            .map(x => {
+              if (x.message) return x.message;
+              if (x.error) return x.error;
+            })
+            .join("\n\n");
+          displayInfo({
+            title: "Staging Area",
+            message: messages,
+            options: {
+              timeout: 0,
+              buttons: [{ text: "Ok", action: null, bold: true }]
+            }
+          });
+          this.loadData();
+        })
+        .catch(error => {
+          const { status, data } = error.response;
+          displayError({
+            title: "Backend Error",
+            message: `${status}: ${data.message || data}`
+          });
+        });
+    },
+    needsApproval(item) {
+      return item.status === STATES.NEEDSAPPROVAL;
+    },
+    isRejected(item) {
+      return item.status === STATES.REJECTED;
+    },
+    isApproved(item) {
+      return item.status === STATES.APPROVED;
+    },
+    zoomTo(id) {
+      if (!id) return;
+      const soundingResult = this.filteredData.filter(x => x.id == id)[0];
+      const { lat, lon, bottleneck, date } = soundingResult.summary;
+      const coordinates = [lat, lon];
+
+      this.$store.commit("map/moveMap", {
+        coordinates: coordinates,
+        zoom: 17,
+        preventZoomOut: true
+      });
+      this.$store
+        .dispatch("bottlenecks/setSelectedBottleneck", bottleneck)
+        .then(() => {
+          this.$store.commit("bottlenecks/setSelectedSurveyByDate", date);
+        });
+    },
+    toggleApproval(id, newStatus) {
+      this.$store.commit("imports/toggleApproval", {
+        id: id,
+        newStatus: newStatus
+      });
+    }
+  }
+};
+</script>
+
+<style lang="scss" scoped>
+.refresh {
+  position: absolute;
+  left: $offset;
+  bottom: $offset;
+}
+.table th,
+td {
+  font-size: 0.9rem;
+  border-top: 0px !important;
+  border-bottom-width: 1px;
+  text-align: left;
+  padding: 0.5rem !important;
+}
+
+.stagingcard {
+  position: relative;
+  min-height: 150px;
+}
+
+.confirm-button {
+  position: absolute;
+  right: $offset;
+  bottom: $offset;
+}
+</style>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/client/src/components/Systemconfiguration.vue	Wed Dec 12 09:22:20 2018 +0100
@@ -0,0 +1,191 @@
+<template>
+  <div class="d-flex flex-row">
+    <div :class="spacerStyle"></div>
+    <div class="card sysconfig mt-3 shadow-xs">
+      <h6
+        class="mb-0 py-2 px-3 border-bottom d-flex text-info align-items-center"
+      >
+        <font-awesome-icon icon="wrench" class="mr-2"></font-awesome-icon>
+        <translate class="headline">Systemconfiguration</translate>
+      </h6>
+      <div class="card-body config">
+        <section class="configsection">
+          <h4 class="card-title">
+            <translate>Bottleneck Areas stroke-color</translate>
+          </h4>
+          <compact-picker v-model="strokeColor" />
+        </section>
+        <section>
+          <h4 class="card-title">
+            <translate>Bottleneck Areas fill-color</translate>
+          </h4>
+          <chrome-picker v-model="fillColor" />
+        </section>
+        <div class="sendbutton">
+          <a @click.prevent="submit" class="btn btn-info text-white">
+            <translate>Send</translate>
+          </a>
+        </div>
+      </div>
+      <!-- card-body -->
+    </div>
+  </div>
+</template>
+
+<style scoped lang="scss">
+.config {
+  text-align: left;
+}
+
+.configsection {
+  margin-bottom: $large-offset;
+}
+
+.sendbutton {
+  position: absolute;
+  right: $offset;
+  bottom: $offset;
+}
+
+.inputs {
+  margin-left: auto;
+  margin-right: auto;
+}
+
+.sysconfig {
+  margin-right: $offset;
+  width: 100%;
+  height: 100%;
+}
+
+.spacer {
+  height: 100vh;
+}
+
+.spacer-collapsed {
+  min-width: $icon-width + $offset;
+  transition: $transition-fast;
+}
+
+.spacer-expanded {
+  min-width: $sidebar-width + $offset;
+}
+</style>
+
+<script>
+/* 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):
+ * Thomas Junk <thomas.junk@intevation.de>
+ * Bernhard Reiter <bernhard@intevation.de>
+ */
+import { Chrome } from "vue-color";
+import { Compact } from "vue-color";
+
+import { HTTP } from "../lib/http";
+import { displayError } from "../lib/errors.js";
+import { mapState } from "vuex";
+export default {
+  name: "systemconfiguration",
+  data() {
+    return {
+      sent: false,
+      strokeColor: { r: 0, g: 0, b: 0, a: 1.0 },
+      fillColor: { r: 0, g: 0, b: 0, a: 1.0 },
+      currentConfig: null
+    };
+  },
+  components: {
+    "chrome-picker": Chrome,
+    "compact-picker": Compact
+  },
+  computed: {
+    ...mapState("application", ["showSidebar"]),
+    spacerStyle() {
+      return [
+        "spacer ml-3",
+        {
+          "spacer-expanded": this.showSidebar,
+          "spacer-collapsed": !this.showSidebar
+        }
+      ];
+    }
+  },
+  methods: {
+    submit() {
+      HTTP.put("/system/style/Bottlenecks/stroke", this.strokeColor.rgba, {
+        headers: {
+          "X-Gemma-Auth": localStorage.getItem("token"),
+          "Content-type": "application/json"
+        }
+      })
+        .then()
+        .catch(error => {
+          const { status, data } = error.response;
+          displayError({
+            title: this.$gettext("Backend Error"),
+            message: `${status}: ${data.message || data}`
+          });
+        });
+
+      HTTP.put("/system/style/Bottlenecks/fill", this.fillColor.rgba, {
+        headers: {
+          "X-Gemma-Auth": localStorage.getItem("token"),
+          "Content-type": "application/json"
+        }
+      })
+        .then()
+        .catch(error => {
+          const { status, data } = error.response;
+          displayError({
+            title: this.$gettext("Backend Error"),
+            message: `${status}: ${data.message || data}`
+          });
+        });
+    }
+  },
+  mounted() {
+    HTTP.get("/system/style/Bottlenecks/stroke", {
+      headers: {
+        "X-Gemma-Auth": localStorage.getItem("token"),
+        "Content-type": "application/json"
+      }
+    })
+      .then(response => {
+        this.strokeColor = response.data.colour;
+      })
+      .catch(error => {
+        const { status, data } = error.response;
+        displayError({
+          title: this.$gettext("Backend Error"),
+          message: `${status}: ${data.message || data}`
+        });
+      });
+
+    HTTP.get("/system/style/Bottlenecks/fill", {
+      headers: {
+        "X-Gemma-Auth": localStorage.getItem("token"),
+        "Content-type": "application/json"
+      }
+    })
+      .then(response => {
+        this.fillColor = response.data.colour;
+      })
+      .catch(error => {
+        const { status, data } = error.response;
+        displayError({
+          title: this.$gettext("Backend Error"),
+          message: `${status}: ${data.message || data}`
+        });
+      });
+  }
+};
+</script>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/client/src/components/Zoom.vue	Wed Dec 12 09:22:20 2018 +0100
@@ -0,0 +1,76 @@
+<template>
+  <div
+    class="d-flex buttoncontainer shadow-xs mb-3 position-absolute"
+    :style="showSplitscreen ? 'margin-bottom: 51vh !important' : ''"
+  >
+    <button
+      class="zoomButton border-0 bg-white rounded-left ui-element"
+      @click="zoomOut"
+    >
+      <font-awesome-icon icon="minus"></font-awesome-icon>
+    </button>
+    <button
+      class="zoomButton border-0 bg-white rounded-right ui-element border-right"
+      @click="zoomIn"
+    >
+      <font-awesome-icon icon="plus"></font-awesome-icon>
+    </button>
+  </div>
+</template>
+
+<style lang="scss" scoped>
+.buttoncontainer {
+  bottom: 0;
+  left: 50%;
+  margin-left: -$icon-width;
+}
+
+.zoomButton {
+  min-height: $icon-width;
+  min-width: $icon-width;
+  z-index: 1;
+  outline: none;
+  color: #666;
+}
+</style>
+<script>
+/* 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):
+ * Markus Kottländer <markus@intevation.de>
+ * Thomas Junk <thomas.junk@intevation.de>
+ */
+import { mapState } from "vuex";
+
+export default {
+  name: "zoom",
+  computed: {
+    ...mapState("map", ["openLayersMap"]),
+    ...mapState("application", ["showSplitscreen"]),
+    zoomLevel: {
+      get() {
+        return this.openLayersMap.getView().getZoom();
+      },
+      set(value) {
+        this.openLayersMap.getView().animate({ zoom: value, duration: 300 });
+      }
+    }
+  },
+  methods: {
+    zoomIn() {
+      this.zoomLevel = this.zoomLevel + 1;
+    },
+    zoomOut() {
+      this.zoomLevel = this.zoomLevel - 1;
+    }
+  }
+};
+</script>
--- a/client/src/components/admin/Importqueue.vue	Tue Dec 11 22:59:10 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,362 +0,0 @@
-<template>
-  <div class="d-flex flex-row">
-    <div :class="spacerStyle"></div>
-    <div class="mt-3 importqueuecard flex-grow-1">
-      <div class="card shadow-xs">
-        <h6
-          class="mb-0 py-2 px-3 border-bottom d-flex text-info align-items-center"
-        >
-          <font-awesome-icon icon="tasks" class="mr-2"></font-awesome-icon>
-          <translate class="headline">Importqueue</translate>
-        </h6>
-        <div class="card-body importcardbody">
-          <div class="card-body importcardbody">
-            <div class="searchandfilter d-flex flex-row">
-              <div class="searchgroup input-group">
-                <div class="input-group-prepend">
-                  <span class="input-group-text" id="search">
-                    <font-awesome-icon icon="search"></font-awesome-icon>
-                  </span>
-                </div>
-                <input
-                  v-model="searchQuery"
-                  type="text"
-                  class="form-control"
-                  placeholder
-                  aria-label="Search"
-                  aria-describedby="search"
-                />
-              </div>
-              <div class="filters">
-                <button
-                  @click="setFilter('successful')"
-                  :class="successfulStyle"
-                >
-                  <translate>Successful</translate>
-                </button>
-                <button @click="setFilter('failed')" :class="failedStyle">
-                  <translate>Failed</translate>
-                </button>
-                <button @click="setFilter('pending')" :class="pendingStyle">
-                  <translate>Pending</translate>
-                </button>
-                <button @click="setFilter('rejected')" :class="rejectedStyle">
-                  <translate>Rejected</translate>
-                </button>
-                <button @click="setFilter('accepted')" :class="acceptedStyle">
-                  <translate>Accepted</translate>
-                </button>
-              </div>
-            </div>
-            <div class="text-left d-flex flex-row w-50 border-bottom">
-              <div class="header py-1 jobid mr-2">
-                <translate>Id</translate>
-              </div>
-              <div class="header py-1 enqueued mr-2">
-                <translate>Enqueued</translate>
-              </div>
-              <div class="header py-1 kind mr-2">
-                <translate>Kind</translate>
-              </div>
-              <div class="header py-1 user mr-2">
-                <translate>User</translate>
-              </div>
-              <div class="header py-1 signer mr-2">
-                <translate>Signer</translate>
-              </div>
-              <div class="header py-1 state mr-2">
-                <translate>State</translate>
-              </div>
-            </div>
-            <div class="text-left" v-for="job in filteredImports" :key="job.id">
-              <Importqueuedetail :job="job"></Importqueuedetail>
-            </div>
-            <div>
-              <button @click="refresh" class="btn btn-info refresh">
-                <translate>Refresh</translate>
-              </button>
-            </div>
-          </div>
-        </div>
-      </div>
-    </div>
-  </div>
-</template>
-
-<script>
-/* 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):
- * Markus Kottländer <markus@intevation.de>
- */
-import { displayError } from "../../lib/errors.js";
-import { mapState } from "vuex";
-import { HTTP } from "../../lib/http.js";
-import Importqueuedetail from "./Importqueuedetail";
-
-export default {
-  name: "importqueue",
-  components: {
-    Importqueuedetail
-  },
-  data() {
-    return {
-      searchQuery: "",
-      successful: false,
-      failed: false,
-      pending: false,
-      rejected: false,
-      accepted: false
-    };
-  },
-  mounted() {
-    this.loadQueue();
-  },
-  methods: {
-    setFilter(name) {
-      this[name] = !this[name];
-      const allSet =
-        this.successful &&
-        this.failed &&
-        this.pending &&
-        this.accepted &&
-        this.rejected;
-      if (allSet) {
-        this.successful = false;
-        this.failed = false;
-        this.pending = false;
-        this.accepted = false;
-        this.rejected = false;
-      }
-    },
-    loadQueue() {
-      this.$store.dispatch("imports/getImports").catch(error => {
-        const { status, data } = error.response;
-        displayError({
-          title: this.$gettext("Backend Error"),
-          message: `${status}: ${data.message || data}`
-        });
-      });
-    },
-    refresh() {
-      this.loadQueue();
-    },
-    showDetails(id) {
-      HTTP.get("/imports/" + id, {
-        headers: { "X-Gemma-Auth": localStorage.getItem("token") }
-      })
-        .then(response => {
-          const { entries } = response.data;
-          this.entries = entries;
-          this.$modal.show("details");
-        })
-        .catch(error => {
-          const { status, data } = error.response;
-          displayError({
-            title: this.$gettext("Backend Error"),
-            message: `${status}: ${data.message || data}`
-          });
-        });
-    },
-    close() {
-      this.$modal.hide("details");
-    }
-  },
-  computed: {
-    ...mapState("imports", ["imports"]),
-    ...mapState("application", ["showSidebar"]),
-    sortIcon() {
-      return this.sortAsc ? "sort-amount-down" : "sort-amount-up";
-    },
-    filteredImports() {
-      const filtered = this.imports
-        .filter(element => {
-          if (!this.searchQuery) return true;
-          return [(element.kind, element.user, element.enqueued)].some(x => {
-            return x.toLowerCase().includes(this.searchQuery.toLowerCase());
-          });
-        })
-        .filter(y => {
-          if (
-            !this.successful &&
-            !this.failed &&
-            !this.pending &&
-            !this.accepted &&
-            !this.rejected
-          )
-            return true;
-          let filterCriteria = [];
-          if (this.successful) filterCriteria.push("successful");
-          if (this.failed) filterCriteria.push("failed");
-          if (this.pending) filterCriteria.push("pending");
-          if (this.accepted) filterCriteria.push("accepted");
-          if (this.rejected) filterCriteria.push("rejected");
-          const result = filterCriteria.map(selectedState => {
-            return y.state === selectedState;
-          });
-          return result.some(x => x);
-        });
-      return filtered;
-    },
-    spacerStyle() {
-      return [
-        "spacer ml-3",
-        {
-          "spacer-expanded": this.showSidebar,
-          "spacer-collapsed": !this.showSidebar
-        }
-      ];
-    },
-    successfulStyle() {
-      return {
-        btn: true,
-        "btn-light": !this.successful,
-        "btn-dark": this.successful
-      };
-    },
-    pendingStyle() {
-      return {
-        btn: true,
-        "btn-light": !this.pending,
-        "btn-dark": this.pending
-      };
-    },
-    failedStyle() {
-      return {
-        btn: true,
-        "btn-light": !this.failed,
-        "btn-dark": this.failed
-      };
-    },
-    rejectedStyle() {
-      return {
-        btn: true,
-        "btn-light": !this.rejected,
-        "btn-dark": this.rejected
-      };
-    },
-    acceptedStyle() {
-      return {
-        btn: true,
-        "btn-light": !this.accepted,
-        "btn-dark": this.accepted
-      };
-    }
-  }
-};
-</script>
-
-<style lang="scss" scoped>
-.jobid {
-  width: 80px;
-}
-
-.enqueued {
-  width: 120px;
-}
-
-.user {
-  width: 80px;
-}
-
-.signer {
-  width: 80px;
-}
-
-.kind {
-  width: 80px;
-}
-
-.state {
-  width: 80px;
-}
-
-.header {
-  font-weight: bold;
-  font-size: 0.9em;
-}
-
-.details thead {
-  display: block;
-}
-.details tbody {
-  display: block;
-}
-
-.details tbody {
-  height: 260px;
-  overflow-y: auto;
-  overflow-x: hidden;
-}
-
-.closebutton {
-  top: $small-offset;
-}
-
-.refresh {
-  position: absolute;
-  right: $offset;
-  bottom: $offset;
-}
-
-.spacer {
-  height: 100vh;
-}
-
-.spacer-collapsed {
-  min-width: $icon-width + $offset;
-  transition: $transition-fast;
-}
-
-.spacer-expanded {
-  min-width: $sidebar-width;
-}
-
-.importqueuecard {
-  width: 97%;
-  margin-left: $offset;
-  margin-right: $offset;
-  min-height: 20rem;
-}
-
-.card-body {
-  width: 100%;
-  margin-left: auto;
-  margin-right: auto;
-}
-
-.searchandfilter {
-  position: relative;
-  margin-bottom: $xx-large-offset;
-}
-
-.filters {
-  position: absolute;
-  right: 0;
-}
-
-.filters button {
-  margin-right: $small-offset;
-}
-
-.table td,
-.table th {
-  border-top: 0 !important;
-  text-align: left;
-  padding: $small-offset !important;
-}
-
-.searchgroup {
-  position: absolute;
-  left: 0;
-  width: 45%;
-}
-</style>
--- a/client/src/components/admin/Importqueuedetail.vue	Tue Dec 11 22:59:10 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,275 +0,0 @@
-<template>
-  <div class="entry d-flex flex-column py-1 border-bottom w-50">
-    <div class="d-flex flex-row position-relative">
-      <div @click="showDetails(job.id)" class="jobid ml-2 mt-2 mr-2">
-        {{ job.id }}
-      </div>
-      <div @click="showDetails(job.id)" class="enqueued mt-2  mr-2">
-        {{ formatDate(job.enqueued) }}
-      </div>
-      <div @click="showDetails(job.id)" class="kind mt-2 mr-2">
-        {{ job.kind }}
-      </div>
-      <div @click="showDetails(job.id)" class="user mt-2 mr-2">
-        {{ job.user }}
-      </div>
-      <div @click="showDetails(job.id)" class="signer mt-2 mr-2">
-        {{ job.signer }}
-      </div>
-      <div @click="showDetails(job.id)" class="state mt-2 mr-2">
-        {{ job.state }}
-      </div>
-      <div
-        @click="showDetails(job.id)"
-        class="btn btn-sm h-100 rounded-0 btn-info detailsbutton"
-      >
-        <font-awesome-icon
-          v-if="show"
-          icon="angle-up"
-          fixed-width
-        ></font-awesome-icon>
-        <font-awesome-icon
-          v-else
-          icon="angle-down"
-          fixed-width
-        ></font-awesome-icon>
-      </div>
-    </div>
-    <div class="detailstable d-flex flex-row">
-      <div :class="collapse">
-        <table class="table table-responsive">
-          <thead>
-            <tr>
-              <th class="first pb-0">
-                <small class="condensed"><translate>Kind</translate></small>
-              </th>
-              <th class="second  pb-0">
-                <a href="#" @click="sortAsc = !sortAsc" class="sort-link"
-                  ><small class="condensed"><translate>Date</translate></small>
-                  <small class="condensed"
-                    ><font-awesome-icon
-                      :icon="sortIcon"
-                      class="ml-1"
-                    ></font-awesome-icon></small
-                ></a>
-              </th>
-              <th class="third pb-0">
-                <small class="condensed"><translate>Message</translate></small>
-              </th>
-            </tr>
-          </thead>
-          <tbody>
-            <tr
-              v-for="(entry, index) in sortedEntries"
-              :key="index"
-              class="detailsrow"
-            >
-              <td class="first">
-                <span class="condensed">{{ entry.kind }}</span>
-              </td>
-              <td class="second">
-                <span class="condensed">{{ formatDate(entry.time) }}</span>
-              </td>
-              <td class="third">
-                <span class="condensed">{{ entry.message }}</span>
-              </td>
-            </tr>
-          </tbody>
-        </table>
-      </div>
-    </div>
-  </div>
-</template>
-
-<script>
-/* 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):
- * Thomas Junk <thomas.junk@intevation.de>
- */
-
-import { HTTP } from "../../lib/http.js";
-import { displayError } from "../../lib/errors.js";
-import locale2 from "locale2";
-
-export default {
-  name: "importqueuedetail",
-  props: ["job"],
-  data() {
-    return {
-      show: false,
-      entries: [],
-      sortAsc: true
-    };
-  },
-  methods: {
-    formatDate(date) {
-      return date
-        ? new Date(date).toLocaleDateString(locale2, {
-            day: "2-digit",
-            month: "2-digit",
-            year: "numeric"
-          })
-        : "";
-    },
-    showDetails(id) {
-      if (this.show) {
-        this.show = false;
-        return;
-      }
-      if (this.entries.length === 0) {
-        HTTP.get("/imports/" + id, {
-          headers: { "X-Gemma-Auth": localStorage.getItem("token") }
-        })
-          .then(response => {
-            const { entries } = response.data;
-            this.entries = entries;
-            this.show = true;
-          })
-          .catch(error => {
-            const { status, data } = error.response;
-            displayError({
-              title: this.$gettext("Backend Error"),
-              message: `${status}: ${data.message || data}`
-            });
-          });
-      } else {
-        this.show = true;
-      }
-    }
-  },
-  computed: {
-    sortedEntries() {
-      let sorted = this.entries.slice();
-      sorted.sort((r1, r2) => {
-        let d1 = new Date(r1.time);
-        let d2 = new Date(r2.time);
-        if (d2 < d1) {
-          return !this.sortAsc ? -1 : 1;
-        }
-        if (d2 > d1) {
-          return !this.sortAsc ? 1 : -1;
-        }
-        return 0;
-      });
-      return sorted;
-    },
-    sortIcon() {
-      return this.sortAsc ? "sort-amount-down" : "sort-amount-up";
-    },
-    icon() {
-      return {
-        "angle-up": !this.show,
-        "angle-down": this.show
-      };
-    },
-    collapse() {
-      return {
-        details: true,
-        collapse: true,
-        show: this.show,
-        "w-100": true
-      };
-    }
-  }
-};
-</script>
-
-<style lang="scss" scoped>
-.condensed {
-  font-stretch: condensed;
-}
-
-.entry {
-  background-color: white;
-  cursor: pointer;
-}
-
-.entry:hover {
-  background-color: #f0f0f0;
-  transition: 1s;
-}
-
-.detailstable {
-  margin-left: $offset;
-  margin-right: $large-offset;
-}
-
-.detailsbutton {
-  position: absolute;
-  top: 0;
-  right: 0;
-  height: 100%;
-}
-.jobid {
-  width: 80px;
-}
-
-.enqueued {
-  width: 120px;
-}
-
-.user {
-  width: 80px;
-}
-
-.signer {
-  width: 80px;
-}
-
-.kind {
-  width: 80px;
-}
-
-.state {
-  width: 80px;
-}
-
-.details {
-  width: 50%;
-}
-
-.detailsrow {
-  line-height: 0.1em;
-}
-
-.first {
-  width: 65px;
-  padding-left: 0px;
-  border-top: 0px;
-  padding-bottom: $small-offset;
-}
-
-.second {
-  width: 100px;
-  padding-left: 0px;
-  border-top: 0px;
-  padding-bottom: $small-offset;
-}
-
-.third {
-  width: 600px;
-  padding-left: 0px;
-  border-top: 0px;
-  padding-bottom: $small-offset;
-}
-
-thead,
-tbody {
-  display: block;
-}
-
-tbody {
-  height: 150px;
-  overflow-y: auto;
-  overflow-x: hidden;
-}
-</style>
--- a/client/src/components/admin/Logs.vue	Tue Dec 11 22:59:10 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,198 +0,0 @@
-<template>
-  <div class="main d-flex flex-column">
-    <div class="d-flex flex-row">
-      <div :class="spacer"></div>
-      <div class="card logs shadow-xs mt-3 mr-3">
-        <h6
-          class="mb-0 py-2 px-3 border-bottom d-flex text-info align-items-center"
-        >
-          <font-awesome-icon class="mr-2 fa-fw" icon="book"></font-awesome-icon>
-          <translate class="headline">Logs</translate>
-        </h6>
-        <div class="logoutput text-left bg-white">
-          <pre id="code" v-highlightjs="logs">
-          <code class="bash hljs hljs-string"></code>
-          </pre>
-        </div>
-        <div class="logmenu">
-          <div class="d-flex align-self-center">
-            <ul class="nav nav-pills">
-              <li class="nav-item">
-                <a
-                  :class="accesslogStyle"
-                  @click="fetch('system/log/apache2/access.log', 'accesslog')"
-                  href="#"
-                >
-                  <translate>Accesslog</translate>
-                </a>
-              </li>
-              <li class="nav-item">
-                <a
-                  :class="errorlogStyle"
-                  @click="fetch('system/log/apache2/error.log', 'errorlog')"
-                  href="#"
-                >
-                  <translate>Errorlog</translate>
-                </a>
-              </li>
-            </ul>
-          </div>
-          <div class="statuscontainer d-flex flex-row mb-3">
-            <div class="statusline align-self-center">
-              <h3><translate>Last refresh:</translate> {{ refreshed }}</h3>
-            </div>
-            <div class="refresh">
-              <button
-                @click="fetch(currentFile, currentLog)"
-                class="btn btn-dark"
-              >
-                <translate>Refresh</translate>
-              </button>
-            </div>
-          </div>
-        </div>
-      </div>
-    </div>
-  </div>
-</template>
-
-<style lang="scss" scoped>
-.statuscontainer {
-  width: 87%;
-  position: relative;
-}
-
-.logmenu {
-  position: relative;
-  margin-left: $offset;
-  margin-top: $offset;
-}
-
-.logs {
-  height: 85vh;
-}
-
-#code {
-  overflow: auto;
-}
-
-.refresh {
-  position: absolute;
-  right: $offset;
-  bottom: 0;
-}
-
-.logoutput {
-  margin-left: $offset;
-  margin-right: $offset;
-  margin-top: $offset;
-  height: 90%;
-  overflow: auto;
-  transition: $transition-fast;
-}
-
-.spacer {
-  height: 90vh;
-}
-
-.spacer-collapsed {
-  min-width: $icon-width + $offset;
-  transition: $transition-fast;
-}
-
-.spacer-expanded {
-  min-width: $sidebar-width + $offset;
-}
-
-.statusline {
-  position: absolute;
-  right: 0;
-  margin-right: 9rem;
-  bottom: -0.5rem;
-}
-
-.statuscontainer {
-  width: 100%;
-}
-</style>
-
-<script>
-/* 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):
- * Thomas Junk <thomas.junk@intevation.de>
- */
-import { mapState } from "vuex";
-import { HTTP } from "../../lib/http.js";
-import "../../../node_modules/highlight.js/styles/paraiso-dark.css";
-import Vue from "vue";
-import VueHighlightJS from "vue-highlightjs";
-Vue.use(VueHighlightJS);
-
-const ACCESSLOG = "accesslog";
-const ERRORLOG = "errorlog";
-
-export default {
-  name: "logs",
-  mounted() {
-    this.fetch("system/log/apache2/access.log", ACCESSLOG);
-  },
-  data() {
-    return {
-      logs: null,
-      currentLog: null,
-      currentFile: null,
-      refreshed: null
-    };
-  },
-  methods: {
-    fetch(file, type) {
-      HTTP.get(file, {
-        headers: { "X-Gemma-Auth": localStorage.getItem("token") }
-      })
-        .then(response => {
-          this.logs = response.data.content;
-          this.currentLog = type;
-          this.refreshed = new Date().toLocaleString();
-          this.currentFile = file;
-        })
-        .catch();
-    },
-    disallow(e) {
-      e.target.blur();
-    }
-  },
-  computed: {
-    ...mapState("application", ["showSidebar"]),
-    accesslogStyle() {
-      return {
-        active: this.currentLog == ACCESSLOG,
-        "nav-link": true
-      };
-    },
-    errorlogStyle() {
-      return {
-        active: this.currentLog == ERRORLOG,
-        "nav-link": true
-      };
-    },
-    spacer() {
-      return [
-        "spacer ml-3",
-        {
-          "spacer-expanded": this.showSidebar,
-          "spacer-collapsed": !this.showSidebar
-        }
-      ];
-    }
-  }
-};
-</script>
--- a/client/src/components/admin/Systemconfiguration.vue	Tue Dec 11 22:59:10 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,191 +0,0 @@
-<template>
-  <div class="d-flex flex-row">
-    <div :class="spacerStyle"></div>
-    <div class="card sysconfig mt-3 shadow-xs">
-      <h6
-        class="mb-0 py-2 px-3 border-bottom d-flex text-info align-items-center"
-      >
-        <font-awesome-icon icon="wrench" class="mr-2"></font-awesome-icon>
-        <translate class="headline">Systemconfiguration</translate>
-      </h6>
-      <div class="card-body config">
-        <section class="configsection">
-          <h4 class="card-title">
-            <translate>Bottleneck Areas stroke-color</translate>
-          </h4>
-          <compact-picker v-model="strokeColor" />
-        </section>
-        <section>
-          <h4 class="card-title">
-            <translate>Bottleneck Areas fill-color</translate>
-          </h4>
-          <chrome-picker v-model="fillColor" />
-        </section>
-        <div class="sendbutton">
-          <a @click.prevent="submit" class="btn btn-info text-white">
-            <translate>Send</translate>
-          </a>
-        </div>
-      </div>
-      <!-- card-body -->
-    </div>
-  </div>
-</template>
-
-<style scoped lang="scss">
-.config {
-  text-align: left;
-}
-
-.configsection {
-  margin-bottom: $large-offset;
-}
-
-.sendbutton {
-  position: absolute;
-  right: $offset;
-  bottom: $offset;
-}
-
-.inputs {
-  margin-left: auto;
-  margin-right: auto;
-}
-
-.sysconfig {
-  margin-right: $offset;
-  width: 100%;
-  height: 100%;
-}
-
-.spacer {
-  height: 100vh;
-}
-
-.spacer-collapsed {
-  min-width: $icon-width + $offset;
-  transition: $transition-fast;
-}
-
-.spacer-expanded {
-  min-width: $sidebar-width + $offset;
-}
-</style>
-
-<script>
-/* 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):
- * Thomas Junk <thomas.junk@intevation.de>
- * Bernhard Reiter <bernhard@intevation.de>
- */
-import { Chrome } from "vue-color";
-import { Compact } from "vue-color";
-
-import { HTTP } from "../../lib/http";
-import { displayError } from "../../lib/errors.js";
-import { mapState } from "vuex";
-export default {
-  name: "systemconfiguration",
-  data() {
-    return {
-      sent: false,
-      strokeColor: { r: 0, g: 0, b: 0, a: 1.0 },
-      fillColor: { r: 0, g: 0, b: 0, a: 1.0 },
-      currentConfig: null
-    };
-  },
-  components: {
-    "chrome-picker": Chrome,
-    "compact-picker": Compact
-  },
-  computed: {
-    ...mapState("application", ["showSidebar"]),
-    spacerStyle() {
-      return [
-        "spacer ml-3",
-        {
-          "spacer-expanded": this.showSidebar,
-          "spacer-collapsed": !this.showSidebar
-        }
-      ];
-    }
-  },
-  methods: {
-    submit() {
-      HTTP.put("/system/style/Bottlenecks/stroke", this.strokeColor.rgba, {
-        headers: {
-          "X-Gemma-Auth": localStorage.getItem("token"),
-          "Content-type": "application/json"
-        }
-      })
-        .then()
-        .catch(error => {
-          const { status, data } = error.response;
-          displayError({
-            title: this.$gettext("Backend Error"),
-            message: `${status}: ${data.message || data}`
-          });
-        });
-
-      HTTP.put("/system/style/Bottlenecks/fill", this.fillColor.rgba, {
-        headers: {
-          "X-Gemma-Auth": localStorage.getItem("token"),
-          "Content-type": "application/json"
-        }
-      })
-        .then()
-        .catch(error => {
-          const { status, data } = error.response;
-          displayError({
-            title: this.$gettext("Backend Error"),
-            message: `${status}: ${data.message || data}`
-          });
-        });
-    }
-  },
-  mounted() {
-    HTTP.get("/system/style/Bottlenecks/stroke", {
-      headers: {
-        "X-Gemma-Auth": localStorage.getItem("token"),
-        "Content-type": "application/json"
-      }
-    })
-      .then(response => {
-        this.strokeColor = response.data.colour;
-      })
-      .catch(error => {
-        const { status, data } = error.response;
-        displayError({
-          title: this.$gettext("Backend Error"),
-          message: `${status}: ${data.message || data}`
-        });
-      });
-
-    HTTP.get("/system/style/Bottlenecks/fill", {
-      headers: {
-        "X-Gemma-Auth": localStorage.getItem("token"),
-        "Content-type": "application/json"
-      }
-    })
-      .then(response => {
-        this.fillColor = response.data.colour;
-      })
-      .catch(error => {
-        const { status, data } = error.response;
-        displayError({
-          title: this.$gettext("Backend Error"),
-          message: `${status}: ${data.message || data}`
-        });
-      });
-  }
-};
-</script>
--- a/client/src/components/admin/importschedule/Importschedule.vue	Tue Dec 11 22:59:10 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,166 +0,0 @@
-<template>
-  <div class="d-flex flex-row">
-    <div :class="spacerStyle"></div>
-    <div class="mt-3 w-100">
-      <div class="card flex-grow-1 schedulecard shadow-xs">
-        <h6
-          class="mb-0 py-2 px-3 border-bottom d-flex text-info align-items-center"
-        >
-          <font-awesome-icon icon="clock" class="mr-2"></font-awesome-icon>
-          <translate class="headline">Importschedule</translate>
-        </h6>
-        <div class="card-body schedulecardbody">
-          <div class="card-body schedulecardbody">
-            <div class="searchandfilter  w-50 d-flex flex-row">
-              <div class="searchgroup input-group">
-                <div class="input-group-prepend">
-                  <span class="input-group-text" id="search">
-                    <font-awesome-icon icon="search"></font-awesome-icon>
-                  </span>
-                </div>
-                <input
-                  v-model="searchQuery"
-                  type="text"
-                  class="form-control"
-                  placeholder
-                  aria-label="Search"
-                  aria-describedby="search"
-                />
-              </div>
-            </div>
-            <table v-if="schedules.length" class="table">
-              <thead>
-                <tr>
-                  <th><translate>Import</translate></th>
-                  <th><translate>Type</translate></th>
-                  <th><translate>Author</translate></th>
-                  <th><translate>Schedule</translate></th>
-                  <th><translate>Email</translate></th>
-                  <th>&nbsp;</th>
-                  <th>&nbsp;</th>
-                </tr>
-              </thead>
-              <tbody>
-                <tr v-for="(schedule, index) in schedules" :key="index">
-                  <td></td>
-                  <td></td>
-                  <td></td>
-                  <td></td>
-                  <td></td>
-                  <td>
-                    <font-awesome-icon
-                      icon="pencil-alt"
-                      fixed-width
-                    ></font-awesome-icon>
-                  </td>
-                  <td>
-                    <font-awesome-icon
-                      @click="deleteSchedule"
-                      icon="trash"
-                      fixed-width
-                    ></font-awesome-icon>
-                  </td>
-                </tr>
-              </tbody>
-            </table>
-            <div v-else class="mt-4 small text-center py-3">
-              <translate>No schedules</translate>
-            </div>
-            <button
-              @click="newImport"
-              class="btn btn-info position-absolute newbutton"
-            >
-              <translate>New Import</translate>
-            </button>
-          </div>
-        </div>
-      </div>
-    </div>
-    <Importscheduledetail></Importscheduledetail>
-  </div>
-</template>
-
-<script>
-/* 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):
- * Thomas Junk <thomas.junk@intevation.de>
- */
-
-import { mapState } from "vuex";
-import Importscheduledetail from "./Importscheduledetail";
-//import { SCHEDULES } from "../../store/imports.js";
-
-export default {
-  name: "importschedule",
-  components: {
-    Importscheduledetail
-  },
-  data() {
-    return {
-      searchQuery: ""
-    };
-  },
-  methods: {
-    newImport() {
-      this.$store.commit("imports/setImportScheduleDetailVisible");
-    },
-    deleteSchedule(index) {
-      this.$store.commit("imports/deleteSchedule", index);
-    }
-  },
-  computed: {
-    ...mapState("application", ["showSidebar"]),
-    ...mapState("imports", ["schedules"]),
-    spacerStyle() {
-      return [
-        "spacer ml-3",
-        {
-          "spacer-expanded": this.showSidebar,
-          "spacer-collapsed": !this.showSidebar
-        }
-      ];
-    }
-  }
-};
-</script>
-
-<style lang="scss" scoped>
-.spacer {
-  height: 100vh;
-}
-
-.spacer-collapsed {
-  min-width: $icon-width + $offset;
-  transition: $transition-fast;
-}
-
-.spacer-expanded {
-  min-width: $sidebar-width + $offset;
-}
-
-.schedulecard {
-  margin-right: $offset;
-  min-height: 20rem;
-}
-
-.schedulecard-body {
-  width: 100%;
-  margin-left: auto;
-  margin-right: auto;
-}
-
-.newbutton {
-  position: absolute;
-  bottom: $offset;
-  right: $offset;
-}
-</style>
--- a/client/src/components/admin/importschedule/Importscheduledetail.vue	Tue Dec 11 22:59:10 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,146 +0,0 @@
-<template>
-  <div
-    class="importscheduledetails  fadeIn animated"
-    v-if="importScheduleDetailVisible"
-  >
-    <div class="card h-100 shadow-xs">
-      <h6
-        class="mb-0 py-2 px-3 border-bottom d-flex text-info align-items-center"
-      >
-        <translate>New import</translate>
-        <span @click="closeDetailview" class="closebutton">
-          <font-awesome-icon icon="times"></font-awesome-icon>
-        </span>
-      </h6>
-      <div class="card-body">
-        <form @submit.prevent="save" class="ml-3">
-          <div class="d-flex flex-row w-100">
-            <div class="flex-column w-100">
-              <div class="flex-row text-left">
-                <small class="text-muted">
-                  <translate>Imports</translate>
-                </small>
-              </div>
-              <select v-model="import_" class="custom-select" id="import_">
-                <option v-for="option in this.$options.imports" :key="option">{{
-                  option
-                }}</option>
-              </select>
-            </div>
-          </div>
-          <div class="d-flex flex-row mt-3 w-100 justify-content-between">
-            <div class="flex-column w-100 mr-2">
-              <div class="flex-row text-left">
-                <small class="text-muted">
-                  <translate>Importtype</translate>
-                </small>
-              </div>
-              <select v-model="import_" class="custom-select" id="importtype">
-                <option
-                  v-for="option in this.$options.importtype"
-                  :key="option"
-                  >{{ option }}</option
-                >
-              </select>
-            </div>
-            <div class="flex-column w-100 ml-2">
-              <div class="flex-row text-left">
-                <small class="text-muted">
-                  <translate>Schedule</translate>
-                </small>
-              </div>
-              <select v-model="schedule" class="custom-select" id="period">
-                <option v-for="option in this.$options.periods" :key="option">{{
-                  option
-                }}</option>
-              </select>
-            </div>
-          </div>
-          <div class="flex-column mt-3 w-100 mr-2">
-            <div class="flex-row text-left">
-              <small class="text-muted">
-                <translate>Email Notification</translate>
-              </small>
-            </div>
-            <div class="flex-flex-row text-left">
-              <toggle-button
-                v-model="eMailNotification"
-                class="mt-2"
-                :speed="100"
-                :labels="{
-                  checked: this.$options.on,
-                  unchecked: this.$options.off
-                }"
-                :width="50"
-                :height="20"
-              />
-            </div>
-          </div>
-          <div v-if="eMailNotification" class="flex-column w-100 mr-2">
-            <div class="flex-row text-left">
-              <small class="text-muted"> <translate>Email</translate> </small>
-            </div>
-            <input class="form-control" type="text" />
-          </div>
-          <button type="submit" class="shadow-sm btn btn-info submit-button">
-            <translate>Submit</translate>
-          </button>
-        </form>
-      </div>
-    </div>
-  </div>
-</template>
-
-<script>
-import { mapState } from "vuex";
-import { displayInfo } from "../../../lib/errors.js";
-
-export default {
-  name: "importscheduledetail",
-  data() {
-    return {
-      schedule: null,
-      import_: null,
-      eMailNotification: false
-    };
-  },
-  computed: {
-    ...mapState("imports", ["importScheduleDetailVisible"])
-  },
-  methods: {
-    save() {
-      displayInfo({
-        title: "Import",
-        message: "under construction"
-      });
-    },
-    closeDetailview() {
-      this.$store.commit("imports/clearImportScheduleDetail");
-      this.$store.commit("imports/setImportScheduleDetailInvisible");
-    }
-  },
-  imports: [],
-  importtype: [],
-  on: "on",
-  off: "off",
-  periods: {
-    DAILY: "daily",
-    MONTHLY: "monthly"
-  }
-};
-</script>
-
-<style lang="scss" scoped>
-.importscheduledetails {
-  height: 420px;
-  width: 45%;
-  margin-top: $offset;
-  margin-right: $offset;
-}
-
-.submit-button {
-  position: absolute;
-  right: $offset;
-  bottom: $offset;
-}
-</style>
--- a/client/src/components/admin/usermanagement/Passwordfield.vue	Tue Dec 11 22:59:10 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,73 +0,0 @@
-<template>
-  <div class="w-100">
-    <div class="d-flex flex-row">
-      <label for="password">{{ this.label }}</label>
-    </div>
-    <div class="d-flex d-row">
-      <input
-        :type="isPasswordVisible"
-        @change="fieldChanged"
-        class="form-control"
-        :placeholder="placeholder"
-        :required="required"
-      />
-      <span class="input-group-text" @click="showPassword">
-        <font-awesome-icon
-          :icon="readablePassword ? 'eye-slash' : 'eye'"
-        ></font-awesome-icon>
-      </span>
-    </div>
-    <div v-show="passworderrors" class="text-danger">
-      <small>
-        <font-awesome-icon icon="exclamation-triangle"></font-awesome-icon>
-        {{ this.passworderrors }}
-      </small>
-    </div>
-  </div>
-</template>
-
-<style>
-/* FIXME does not work here, unclear why, so added to Login.vue
-input[type="password"]::-ms-reveal {
-  display: none;
-} */
-</style>
-
-<script>
-/* 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):
- * Thomas Junk <thomas.junk@intevation.de>
- */
-export default {
-  name: "passwordfield",
-  props: ["model", "placeholder", "label", "passworderrors", "required"],
-  data() {
-    return {
-      password: "",
-      readablePassword: false
-    };
-  },
-  methods: {
-    showPassword() {
-      this.readablePassword = !this.readablePassword;
-    },
-    fieldChanged(e) {
-      this.$emit("fieldchange", e.target.value);
-    }
-  },
-  computed: {
-    isPasswordVisible() {
-      return this.readablePassword ? "text" : "password";
-    }
-  }
-};
-</script>
--- a/client/src/components/admin/usermanagement/Userdetail.vue	Tue Dec 11 22:59:10 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,393 +0,0 @@
-<template>
-  <div class="userdetails mt-3 shadow fadeIn animated card">
-    <h6
-      class="mb-0 py-2 px-3 border-bottom d-flex text-info align-items-center"
-    >
-      {{ this.cardHeader }}
-      <span @click="closeDetailview" class="closebutton">
-        <font-awesome-icon icon="times"></font-awesome-icon>
-      </span>
-    </h6>
-    <div class="card-body">
-      <form @submit.prevent="save" class="ml-3">
-        <div class="formfields">
-          <div v-if="currentUser.isNew" class="form-group row">
-            <label for="user"> <translate>Username</translate> </label>
-            <input
-              type="user"
-              :placeholder="userNamePlaceholder"
-              class="form-control form-control-sm"
-              id="user"
-              aria-describedby="userHelp"
-              v-model="currentUser.user"
-            />
-            <div v-show="errors.user" class="text-danger">
-              <small>
-                <font-awesome-icon
-                  icon="exclamation-triangle"
-                ></font-awesome-icon>
-                {{ errors.user }}
-              </small>
-            </div>
-          </div>
-          <div class="form-group row">
-            <label for="country"> <translate>Country</translate> </label>
-            <select
-              class="form-control form-control-sm"
-              v-on:change="validateCountry"
-              v-model="currentUser.country"
-            >
-              <option disabled value>
-                <translate>Please select one</translate>
-              </option>
-              <option
-                v-for="country in countries"
-                v-bind:value="country"
-                v-bind:key="country"
-                >{{ country }}</option
-              >
-            </select>
-            <div v-show="errors.country" class="text-danger">
-              <small>
-                <font-awesome-icon
-                  icon="exclamation-triangle"
-                ></font-awesome-icon>
-                {{ errors.country }}
-              </small>
-            </div>
-          </div>
-          <div class="form-group row">
-            <label for="email"> <translate>Email address</translate> </label>
-            <input
-              type="email"
-              v-on:change="validateEmailaddress"
-              class="form-control form-control-sm"
-              id="email"
-              aria-describedby="emailHelp"
-              v-model="currentUser.email"
-            />
-            <div v-show="errors.email" class="text-danger">
-              <small>
-                <font-awesome-icon
-                  icon="exclamation-triangle"
-                ></font-awesome-icon>
-                {{ errors.email }}
-              </small>
-            </div>
-          </div>
-          <div class="form-group row">
-            <label for="role"> <translate>Role</translate> </label>
-            <select
-              class="form-control form-control-sm"
-              v-on:change="validateRole"
-              v-model="currentUser.role"
-            >
-              <option disabled value>
-                <translate>Please select one</translate>
-              </option>
-              <option value="sys_admin">
-                <translate>Sysadmin</translate>
-              </option>
-              <option value="waterway_admin">
-                <translate>Waterway Admin</translate>
-              </option>
-              <option value="waterway_user">
-                <translate>Waterway User</translate>
-              </option>
-            </select>
-            <div v-show="errors.role" class="text-danger">
-              <small>
-                <font-awesome-icon
-                  icon="exclamation-triangle"
-                ></font-awesome-icon>
-                {{ errors.role }}
-              </small>
-            </div>
-          </div>
-          <div class="form-group row">
-            <PasswordField
-              @fieldchange="passwordChanged"
-              :placeholder="passwordPlaceholder"
-              :label="passwordLabel"
-              :passworderrors="errors.password"
-            ></PasswordField>
-          </div>
-          <div class="form-group row">
-            <PasswordField
-              @fieldchange="passwordReChanged"
-              :placeholder="passwordRePlaceholder"
-              :label="passwordReLabel"
-              :passworderrors="errors.passwordre"
-            ></PasswordField>
-          </div>
-        </div>
-        <div>
-          <button
-            type="submit"
-            :disabled="submitted"
-            class="shadow-sm btn btn-info submit-button"
-          >
-            <translate>Submit</translate>
-          </button>
-        </div>
-        <div
-          v-if="currentUser.role != 'waterway_user'"
-          class="form-group row d-flex flex-row justify-content-start mailbutton"
-        >
-          <a @click="sendTestMail" class="btn btn-light">
-            <font-awesome-icon icon="paper-plane"></font-awesome-icon>
-            <translate>Send testmail</translate>
-          </a>
-          <div v-if="mailsent"><translate>Mail was sent</translate></div>
-        </div>
-      </form>
-    </div>
-  </div>
-</template>
-
-<style lang="scss" scoped>
-.submit-button {
-  position: absolute;
-  right: $offset;
-  bottom: $offset;
-}
-.mailbutton {
-  width: 12vw;
-  position: absolute;
-  left: $large-offset;
-  bottom: 0;
-}
-
-.formfields {
-  width: 60%;
-}
-
-.userdetails {
-  height: 600px;
-  margin-top: $offset;
-  margin-left: $offset;
-  margin-right: $offset;
-}
-
-form {
-  font-size: $smaller;
-}
-</style>
-
-<script>
-/* 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):
- * Thomas Junk <thomas.junk@intevation.de>
- */
-import { HTTP } from "../../../lib/http";
-import { displayError } from "../../../lib/errors.js";
-import { mapState } from "vuex";
-import PasswordField from "./Passwordfield";
-
-const emptyErrormessages = () => {
-  return {
-    email: "",
-    country: "",
-    role: "",
-    password: "",
-    passwordre: ""
-  };
-};
-
-const isEmailValid = email => {
-  /**
-   *
-   * For convenience purposes the same regex used as in the go code
-   * cf. types.go
-   *
-   */
-  // eslint-disable-next-line
-  return /(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\[(?:(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9]))\.){3}(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9])|[a-z0-9-]*[a-z0-9]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)\])/.test(
-    email
-  );
-};
-
-const violatedPasswordRules = password => {
-  return (
-    // rules according to issue 70
-    password.length < 7 ||
-    /\W/.test(password) == false ||
-    /\d/.test(password) == false
-  );
-};
-
-export default {
-  name: "userdetail",
-  components: {
-    PasswordField
-  },
-  data() {
-    return {
-      mailsent: false,
-      passwordLabel: this.$gettext("Password"),
-      passwordReLabel: this.$gettext("Repeat Password"),
-      passwordPlaceholder: this.$gettext("password"),
-      passwordRePlaceholder: this.$gettext("password again"),
-      password: "",
-      passwordre: "",
-      currentUser: {},
-      path: null,
-      submitted: false,
-      errors: {
-        email: "",
-        country: "",
-        role: "",
-        password: "",
-        passwordre: ""
-      }
-    };
-  },
-  mounted() {
-    this.currentUser = { ...this.user };
-    this.path = this.user.name;
-  },
-  watch: {
-    user() {
-      this.currentUser = { ...this.user };
-      this.path = this.user.name;
-      this.clearPassword();
-      this.clearErrors();
-    }
-  },
-  computed: {
-    cardHeader() {
-      if (this.currentUser.isNew) return "N.N";
-      return this.currentUser.user;
-    },
-    userNamePlaceholder() {
-      if (this.currentUser.isNew) return "N.N";
-      return "";
-    },
-    ...mapState("application", ["countries"]),
-    user() {
-      return this.$store.getters["usermanagement/currentUser"];
-    },
-    isFormValid() {
-      return (
-        isEmailValid(this.currentUser.email) &&
-        this.currentUser.country &&
-        this.password === this.passwordre &&
-        (this.password === "" || !violatedPasswordRules(this.password))
-      );
-    }
-  },
-  methods: {
-    sendTestMail() {
-      if (this.mailsent) return;
-      HTTP.get("/testmail/" + this.currentUser.user, {
-        headers: {
-          "X-Gemma-Auth": localStorage.getItem("token"),
-          "Content-type": "text/xml; charset=UTF-8"
-        }
-      })
-        .then(() => {
-          this.mailsent = true;
-        })
-        .catch(error => {
-          this.loginFailed = true;
-          this.submitted = false;
-          const { status, data } = error.response;
-          displayError({
-            title: this.$gettext("Backend Error"),
-            message: `${status}: ${data.message || data}`
-          });
-        });
-    },
-    passwordChanged(value) {
-      this.password = value;
-      this.validatePassword();
-    },
-    passwordReChanged(value) {
-      this.passwordre = value;
-      this.validatePassword();
-    },
-    clearErrors() {
-      this.errors = emptyErrormessages();
-    },
-    clearPassword() {
-      this.password = "";
-      this.passwordre = "";
-    },
-    closeDetailview() {
-      this.$store.commit("usermanagement/clearCurrentUser");
-      this.$store.commit("usermanagement/setUserDetailsInvisible");
-    },
-    validateCountry() {
-      this.errors.country = this.currentUser.country
-        ? ""
-        : this.$gettext("Please choose a country");
-    },
-    validateRole() {
-      this.errors.role = this.currentUser.role
-        ? ""
-        : this.$gettext("Please choose a role");
-    },
-    validatePassword() {
-      this.errors.passwordre =
-        this.password === this.passwordre
-          ? ""
-          : this.$gettext("Passwords do not match!");
-      this.errors.password =
-        this.password === "" || !violatedPasswordRules(this.password)
-          ? ""
-          : this.$gettext(
-              "Password should at least be 8 char long including 1 digit and 1 special char like $"
-            );
-    },
-    validateEmailaddress() {
-      this.errors.email = isEmailValid(this.currentUser.email)
-        ? ""
-        : this.$gettext("invalid email");
-    },
-    validate() {
-      this.validateCountry();
-      this.validateRole();
-      this.validatePassword();
-      this.validateEmailaddress();
-    },
-    save() {
-      this.validate();
-      if (!this.isFormValid) return;
-      if (this.password) this.currentUser.password = this.password;
-      this.submitted = true;
-      this.$store
-        .dispatch("usermanagement/saveCurrentUser", {
-          path: this.user.user,
-          user: this.currentUser
-        })
-        .then(() => {
-          this.submitted = false;
-          this.$store.dispatch("usermanagement/loadUsers").catch(error => {
-            const { status, data } = error.response;
-            displayError({
-              title: this.$gettext("Backend Error"),
-              message: `${status}: ${data.message || data}`
-            });
-          });
-        })
-        .catch(error => {
-          this.submitted = false;
-          const { status, data } = error.response;
-          displayError({
-            title: this.$gettext("Error while saving user"),
-            message: `${status}: ${data.message || data}`
-          });
-        });
-    }
-  }
-};
-</script>
--- a/client/src/components/admin/usermanagement/Usermanagement.vue	Tue Dec 11 22:59:10 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,358 +0,0 @@
-<template>
-  <div class="main d-flex flex-row">
-    <div :class="spacerStyle"></div>
-    <div class="d-flex content flex-column">
-      <div class="d-flex flex-row">
-        <div :class="userlistStyle">
-          <div class="card">
-            <h6
-              class="mb-0 py-2 px-3 border-bottom d-flex text-info align-items-center"
-            >
-              <font-awesome-icon
-                icon="users-cog"
-                class="mr-2 fa-fw"
-              ></font-awesome-icon>
-              <translate class="headline">Users</translate>
-            </h6>
-            <div class="card-body">
-              <table id="datatable" :class="tableStyle">
-                <thead>
-                  <tr>
-                    <th scope="col" @click="sortBy('user')">
-                      <span
-                        >Username&nbsp;
-                        <font-awesome-icon
-                          v-if="sortCriterion == 'user'"
-                          icon="angle-down"
-                        ></font-awesome-icon>
-                      </span>
-                    </th>
-                    <th scope="col" @click="sortBy('country')">
-                      <span
-                        >Country&nbsp;
-                        <font-awesome-icon
-                          v-if="sortCriterion == 'country'"
-                          icon="angle-down"
-                        ></font-awesome-icon>
-                      </span>
-                    </th>
-                    <th scope="col" @click="sortBy('email')">
-                      <span
-                        >Email&nbsp;
-                        <font-awesome-icon
-                          v-if="sortCriterion == 'email'"
-                          icon="angle-down"
-                        ></font-awesome-icon>
-                      </span>
-                    </th>
-                    <th scope="col" @click="sortBy('role')">
-                      <span
-                        >Role&nbsp;
-                        <font-awesome-icon
-                          v-if="sortCriterion == 'role'"
-                          icon="angle-down"
-                        ></font-awesome-icon>
-                      </span>
-                    </th>
-                    <th scope="col"></th>
-                  </tr>
-                </thead>
-                <tbody>
-                  <tr
-                    v-for="user in users"
-                    :key="user.user"
-                    @click="selectUser(user.user)"
-                  >
-                    <td>{{ user.user }}</td>
-                    <td>{{ user.country }}</td>
-                    <td>{{ user.email }}</td>
-                    <td>
-                      <font-awesome-icon
-                        :icon="roleIcon(user.role)"
-                        @click="deleteUser(user.user)"
-                      ></font-awesome-icon>
-                    </td>
-                    <td>
-                      <font-awesome-icon
-                        icon="trash"
-                        @click="deleteUser(user.user)"
-                      ></font-awesome-icon>
-                    </td>
-                  </tr>
-                </tbody>
-              </table>
-            </div>
-            <div class="d-flex mx-auto align-items-center">
-              <button
-                @click="prevPage"
-                v-if="this.currentPage !== 1"
-                class="mr-2 btn btn-sm btn-light align-self-center"
-              >
-                <font-awesome-icon icon="angle-left"></font-awesome-icon>
-              </button>
-              {{ this.currentPage }} / {{ this.pages }}
-              <button
-                @click="nextPage"
-                v-if="this.currentPage !== this.pages"
-                class="ml-2 btn btn-sm btn-light align-self-center"
-              >
-                <font-awesome-icon icon="angle-right"></font-awesome-icon>
-              </button>
-            </div>
-            <div class="mr-3 pb-3">
-              <button @click="addUser" class="btn btn-info addbutton shadow-sm">
-                <translate>Add User</translate>
-              </button>
-            </div>
-          </div>
-        </div>
-        <Userdetail
-          class="d-flex userdetails"
-          v-if="isUserDetailsVisible"
-        ></Userdetail>
-      </div>
-    </div>
-  </div>
-</template>
-
-<style scoped lang="scss">
-@import "../../../assets/tooltip.scss";
-
-.addbutton {
-  position: absolute;
-  bottom: $offset;
-  right: $offset;
-}
-
-.content {
-  width: 100%;
-}
-
-.userdetails {
-  width: 50%;
-}
-.spacer {
-  height: 100vh;
-  margin-left: $offset;
-}
-
-.spacer-collapsed {
-  min-width: $icon-width + $offset;
-  transition: $transition-fast;
-}
-
-.spacer-expanded {
-  min-width: $sidebar-width + $offset;
-}
-
-.main {
-  height: 100vh;
-}
-
-.icon {
-  font-size: large;
-}
-
-.userlist {
-  min-width: 520px;
-  height: 100%;
-}
-
-.userlistsmall {
-  width: 100%;
-}
-
-.userlistextended {
-  width: 100%;
-}
-
-.table {
-  width: 90% !important;
-  margin: auto;
-}
-
-.table th {
-  cursor: pointer;
-}
-
-.table th,
-td {
-  font-size: $smaller;
-  border-top: 0px !important;
-  text-align: left;
-  padding: $small-offset !important;
-}
-
-.table td {
-  font-size: $smaller;
-  cursor: pointer;
-}
-
-tr span {
-  display: flex;
-}
-</style>
-
-<script>
-/* 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):
- * Thomas Junk <thomas.junk@intevation.de>
- */
-import Userdetail from "./Userdetail";
-import store from "../../../store";
-import { mapGetters, mapState } from "vuex";
-import { displayError } from "../../../lib/errors.js";
-
-export default {
-  name: "userview",
-  data() {
-    return {
-      sortCriterion: "user",
-      pageSize: 10,
-      currentPage: 1
-    };
-  },
-  components: {
-    Userdetail
-  },
-  computed: {
-    ...mapGetters("usermanagement", ["isUserDetailsVisible"]),
-    ...mapState("application", ["showSidebar"]),
-    spacerStyle() {
-      return [
-        "spacer",
-        {
-          "spacer-expanded": this.showSidebar,
-          "spacer-collapsed": !this.showSidebar
-        }
-      ];
-    },
-    users() {
-      let users = [...this.$store.getters["usermanagement/users"]];
-      users.sort((a, b) => {
-        if (
-          a[this.sortCriterion].toLowerCase() <
-          b[this.sortCriterion].toLowerCase()
-        )
-          return -1;
-        if (
-          a[this.sortCriterion].toLowerCase() >
-          b[this.sortCriterion].toLowerCase()
-        )
-          return 1;
-        return 0;
-      });
-      const start = (this.currentPage - 1) * this.pageSize;
-      return users.slice(start, start + this.pageSize);
-    },
-    pages() {
-      let users = [...this.$store.getters["usermanagement/users"]];
-      return Math.ceil(users.length / this.pageSize);
-    },
-    tableStyle() {
-      return {
-        table: true,
-        "table-hover": true,
-        "table-sm": this.isUserDetailsVisible,
-        fadeIn: true,
-        animated: true
-      };
-    },
-    userlistStyle() {
-      return [
-        "userlist mt-3 mr-3 shadow-xs",
-        {
-          userlistsmall: this.isUserDetailsVisible,
-          userlistextended: !this.isUserDetailsVisible
-        }
-      ];
-    }
-  },
-  methods: {
-    tween() {},
-    nextPage() {
-      if (this.currentPage < this.pages) {
-        document.querySelector("#datatable").classList.add("fadeOut");
-        setTimeout(() => {
-          document.querySelector("#datatable").classList.remove("fadeOut");
-          this.currentPage += 1;
-        }, 10);
-      }
-      return;
-    },
-    prevPage() {
-      if (this.currentPage > 0) {
-        document.querySelector("#datatable").classList.add("fadeOut");
-        setTimeout(() => {
-          document.querySelector("#datatable").classList.remove("fadeOut");
-          this.currentPage -= 1;
-        }, 10);
-      }
-      return;
-    },
-    sortBy(criterion) {
-      this.sortCriterion = criterion;
-    },
-    deleteUser(name) {
-      this.$store
-        .dispatch("usermanagement/deleteUser", { name: name })
-        .then(() => {
-          this.submitted = false;
-          this.$store.dispatch("usermanagement/loadUsers").catch(error => {
-            const { status, data } = error.response;
-            displayError({
-              title: this.$gettext("Backend Error"),
-              message: `${status}: ${data.message || data}`
-            });
-          });
-        })
-        .catch(error => {
-          const { status, data } = error.response;
-          displayError({
-            title: this.$gettext("Backend Error"),
-            message: `${status}: ${data.message || data}`
-          });
-        });
-    },
-    addUser() {
-      this.$store.commit("usermanagement/clearCurrentUser");
-      this.$store.commit("usermanagement/setUserDetailsVisible");
-    },
-    selectUser(name) {
-      const user = this.$store.getters["usermanagement/getUserByName"](name);
-      this.$store.commit("usermanagement/setCurrentUser", user);
-    },
-    roleIcon(role) {
-      if (role === "sys_admin") return "star";
-      if (role === "waterway_admin") return ["fab", "adn"];
-      return "user";
-    }
-  },
-  beforeRouteEnter(to, from, next) {
-    store
-      .dispatch("usermanagement/loadUsers")
-      .then(next)
-      .catch(error => {
-        const { status, data } = error.response;
-        displayError({
-          title: this.$gettext("Backend Error"),
-          message: `${status}: ${data}`
-        });
-      });
-  },
-  beforeRouteLeave(to, from, next) {
-    store.commit("usermanagement/clearCurrentUser");
-    store.commit("usermanagement/setUserDetailsInvisible");
-    next();
-  }
-};
-</script>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/client/src/components/fairway/Fairwayprofile.vue	Wed Dec 12 09:22:20 2018 +0100
@@ -0,0 +1,413 @@
+<template>
+  <div :class="['position-relative', { show: showSplitscreen }]">
+    <button
+      class="rounded-bottom bg-white border-0 position-absolute splitscreen-toggle"
+      @click="$store.commit('application/showSplitscreen', false)"
+      v-if="showSplitscreen"
+    >
+      <font-awesome-icon icon="angle-down" />
+    </button>
+    <button
+      class="rounded-bottom bg-white border-0 position-absolute clear-selection"
+      @click="$store.dispatch('fairwayprofile/clearSelection')"
+      v-if="showSplitscreen"
+    >
+      <font-awesome-icon icon="times" />
+    </button>
+    <div class="profile bg-white position-relative d-flex flex-column">
+      <h5
+        class="headline border-bottom mb-0 py-2"
+        v-if="selectedBottleneck && selectedSurvey"
+      >
+        {{ selectedBottleneck }} ({{ selectedSurvey.date_info }})
+      </h5>
+      <div class="d-flex flex-fill">
+        <div
+          class="loading d-flex justify-content-center align-items-center"
+          v-if="surveysLoading || profileLoading"
+        >
+          <font-awesome-icon icon="spinner" spin />
+        </div>
+        <div class="fairwayprofile m-3 mt-0 bg-white flex-grow-1"></div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<style lang="scss" scoped>
+.profile {
+  width: 100vw;
+  height: 0;
+  overflow: hidden;
+  z-index: 2;
+}
+
+.splitscreen-toggle,
+.clear-selection {
+  width: 2rem;
+  height: 2rem;
+  margin-top: 8px;
+  z-index: 3;
+  outline: none;
+}
+
+.splitscreen-toggle svg path,
+.clear-selection svg path {
+  fill: #666;
+}
+
+.splitscreen-toggle {
+  right: 2.5rem;
+}
+
+.clear-selection {
+  right: 0.5rem;
+}
+
+.show .profile {
+  height: 50vh;
+}
+
+.loading {
+  background: rgba(255, 255, 255, 0.96);
+  position: absolute;
+  z-index: 99;
+  top: 0;
+  right: 0;
+  bottom: 0;
+  left: 0;
+}
+</style>
+
+<script>
+/* 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):
+ * Thomas Junk <thomas.junk@intevation.de>
+ */
+import * as d3 from "d3";
+import { mapState, mapGetters } from "vuex";
+import debounce from "debounce";
+
+const GROUND_COLOR = "#4A2F06";
+
+export default {
+  name: "fairwayprofile",
+  data() {
+    return {
+      coordinatesInput: "",
+      coordinatesSelect: null,
+      cutLabel: "",
+      showLabelInput: false,
+      width: null,
+      height: null,
+      margin: {
+        top: 20,
+        right: 40,
+        bottom: 30,
+        left: 40
+      }
+    };
+  },
+  computed: {
+    ...mapGetters("fairwayprofile", ["totalLength"]),
+    ...mapState("application", ["showSplitscreen"]),
+    ...mapState("fairwayprofile", [
+      "startPoint",
+      "endPoint",
+      "currentProfile",
+      "additionalSurvey",
+      "minAlt",
+      "maxAlt",
+      "fairwayCoordinates",
+      "waterLevels",
+      "selectedWaterLevel",
+      "profileLoading"
+    ]),
+    ...mapState("bottlenecks", [
+      "selectedBottleneck",
+      "selectedSurvey",
+      "surveysLoading"
+    ]),
+    currentData() {
+      if (
+        !this.selectedSurvey ||
+        !this.currentProfile.hasOwnProperty(this.selectedSurvey.date_info)
+      )
+        return [];
+      return this.currentProfile[this.selectedSurvey.date_info].points;
+    },
+    additionalData() {
+      if (
+        !this.additionalSurvey ||
+        !this.currentProfile.hasOwnProperty(this.additionalSurvey.date_info)
+      )
+        return [];
+      return this.currentProfile[this.additionalSurvey.date_info].points;
+    },
+    waterColor() {
+      const result = this.waterLevels.find(
+        x => x.level === this.selectedWaterLevel
+      );
+      return result.color;
+    },
+    xScale() {
+      return [0, this.totalLength];
+    },
+    yScaleLeft() {
+      const hi = Math.max(this.maxAlt, this.selectedWaterLevel);
+      return [this.minAlt, hi];
+    },
+    yScaleRight() {
+      const DELTA = this.maxAlt * 1.1 - this.maxAlt;
+      return [this.maxAlt * 1 + DELTA, -DELTA];
+    }
+  },
+  watch: {
+    currentData() {
+      this.drawDiagram();
+    },
+    additionalData() {
+      this.drawDiagram();
+    },
+    width() {
+      this.drawDiagram();
+    },
+    height() {
+      this.drawDiagram();
+    },
+    waterLevels() {
+      this.drawDiagram();
+    },
+    selectedWaterLevel() {
+      this.drawDiagram();
+    },
+    fairwayCoordinates() {
+      this.drawDiagram();
+    }
+  },
+  methods: {
+    drawDiagram() {
+      this.coordinatesSelect = null;
+      const chartDiv = document.querySelector(".fairwayprofile");
+      d3.select(".fairwayprofile svg").remove();
+      this.scaleFairwayProfile();
+      let svg = d3.select(chartDiv).append("svg");
+      svg.attr("width", this.width);
+      svg.attr("height", this.height);
+      const width = this.width - this.margin.right - 1.5 * this.margin.left;
+      const height = this.height - this.margin.top - 2 * this.margin.bottom;
+      const currentData = this.currentData;
+      const additionalData = this.additionalData;
+      const { xScale, yScaleRight, graph } = this.generateCoordinates(
+        svg,
+        height,
+        width
+      );
+      this.drawWaterlevel({ graph, xScale, yScaleRight, height });
+      this.drawLabels({ graph, height });
+      this.drawFairway({ graph, xScale, yScaleRight });
+      if (currentData) {
+        this.drawProfile({
+          graph,
+          xScale,
+          yScaleRight,
+          currentData,
+          height,
+          color: GROUND_COLOR,
+          strokeColor: "black",
+          opacity: 1
+        });
+      }
+      if (additionalData) {
+        this.drawProfile({
+          graph,
+          xScale,
+          yScaleRight,
+          currentData: additionalData,
+          height,
+          color: GROUND_COLOR,
+          strokeColor: "#943007",
+          opacity: 0.6
+        });
+      }
+    },
+    drawFairway({ graph, xScale, yScaleRight }) {
+      for (let coordinates of this.fairwayCoordinates) {
+        const [startPoint, endPoint, depth] = coordinates;
+        let fairwayArea = d3
+          .area()
+          .x(function(d) {
+            return xScale(d.x);
+          })
+          .y0(yScaleRight(0))
+          .y1(function(d) {
+            return yScaleRight(d.y);
+          });
+        graph
+          .append("path")
+          .datum([{ x: startPoint, y: depth }, { x: endPoint, y: depth }])
+          .attr("fill", "#002AFF")
+          .attr("stroke-opacity", 0.65)
+          .attr("fill-opacity", 0.65)
+          .attr("stroke", "#FFD20D")
+          .attr("d", fairwayArea);
+      }
+    },
+    drawLabels({ graph, height }) {
+      graph
+        .append("text")
+        .attr("transform", ["rotate(-90)"])
+        .attr("y", this.width - 60)
+        .attr("x", -(this.height - this.margin.top - this.margin.bottom) / 2)
+        .attr("dy", "1em")
+        .attr("fill", "black")
+        .style("text-anchor", "middle")
+        .text("Depth [m]");
+      graph
+        .append("text")
+        .attr("y", 0 - this.margin.left)
+        .attr("x", 0 - height / 4)
+        .attr("dy", "1em")
+        .attr("fill", "black")
+        .style("text-anchor", "middle")
+        .attr("transform", [
+          "translate(" + this.width / 2 + "," + this.height + ")",
+          "rotate(0)"
+        ])
+        .text("Width [m]");
+    },
+    generateCoordinates(svg, height, width) {
+      let xScale = d3
+        .scaleLinear()
+        .domain(this.xScale)
+        .rangeRound([0, width]);
+
+      xScale.ticks(5);
+      let yScaleLeft = d3
+        .scaleLinear()
+        .domain(this.yScaleLeft)
+        .rangeRound([height, 0]);
+
+      let yScaleRight = d3
+        .scaleLinear()
+        .domain(this.yScaleRight)
+        .rangeRound([height, 0]);
+
+      let xAxis = d3.axisBottom(xScale);
+      let yAxis2 = d3.axisRight(yScaleRight);
+      let graph = svg
+        .append("g")
+        .attr(
+          "transform",
+          "translate(" + this.margin.left + "," + this.margin.top + ")"
+        );
+      graph
+        .append("g")
+        .attr("transform", "translate(0," + height + ")")
+        .call(xAxis.ticks(5));
+      graph
+        .append("g")
+        .attr("transform", "translate(" + width + ",0)")
+        .call(yAxis2);
+      return { xScale, yScaleLeft, yScaleRight, graph };
+    },
+    drawWaterlevel({ graph, xScale, yScaleRight, height }) {
+      let waterArea = d3
+        .area()
+        .x(function(d) {
+          return xScale(d.x);
+        })
+        .y0(height)
+        .y1(function(d) {
+          return yScaleRight(d.y);
+        });
+      graph
+        .append("path")
+        .datum([{ x: 0, y: 0 }, { x: this.totalLength, y: 0 }])
+        .attr("fill", this.waterColor)
+        .attr("stroke", this.waterColor)
+        .attr("d", waterArea);
+    },
+    drawProfile({
+      graph,
+      xScale,
+      yScaleRight,
+      currentData,
+      height,
+      color,
+      strokeColor,
+      opacity
+    }) {
+      for (let part of currentData) {
+        let profileLine = d3
+          .line()
+          .x(d => {
+            return xScale(d.x);
+          })
+          .y(d => {
+            return yScaleRight(d.y);
+          });
+        let profileArea = d3
+          .area()
+          .x(function(d) {
+            return xScale(d.x);
+          })
+          .y0(height)
+          .y1(function(d) {
+            return yScaleRight(d.y);
+          });
+        graph
+          .append("path")
+          .datum(part)
+          .attr("fill", color)
+          .attr("stroke", color)
+          .attr("stroke-width", 3)
+          .attr("stroke-opacity", opacity)
+          .attr("fill-opacity", opacity)
+          .attr("d", profileArea);
+        graph
+          .append("path")
+          .datum(part)
+          .attr("fill", "none")
+          .attr("stroke", strokeColor)
+          .attr("stroke-linejoin", "round")
+          .attr("stroke-linecap", "round")
+          .attr("stroke-width", 3)
+          .attr("stroke-opacity", opacity)
+          .attr("fill-opacity", opacity)
+          .attr("d", profileLine);
+      }
+    },
+    scaleFairwayProfile() {
+      if (!document.querySelector(".fairwayprofile")) return;
+      const clientHeight = document.querySelector(".fairwayprofile")
+        .clientHeight;
+      const clientWidth = document.querySelector(".fairwayprofile").clientWidth;
+      if (!clientHeight || !clientWidth) return;
+      this.height = clientHeight;
+      this.width = clientWidth;
+    }
+  },
+  created() {
+    window.addEventListener("resize", debounce(this.drawDiagram), 100);
+  },
+  mounted() {
+    this.drawDiagram();
+  },
+  updated() {
+    this.scaleFairwayProfile();
+  },
+  destroyed() {
+    window.removeEventListener("resize", debounce(this.drawDiagram));
+  }
+};
+</script>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/client/src/components/fairway/Infobar.vue	Wed Dec 12 09:22:20 2018 +0100
@@ -0,0 +1,61 @@
+<template>
+  <div
+    v-if="Object.keys(currentProfile).length && !showSplitscreen"
+    class="ui-element shadow-xs infobar rounded bg-white ml-auto mb-3 mr-3"
+  >
+    <div class="d-flex flex-row justify-content-between h-100">
+      <h6 class="my-auto px-2">
+        {{ selectedBottleneck }} ({{ selectedSurvey.date_info }})
+      </h6>
+      <span
+        class="p-2 border-left d-flex align-items-center"
+        @click="$store.commit('application/showSplitscreen', true)"
+      >
+        <font-awesome-icon icon="angle-up"></font-awesome-icon>
+      </span>
+      <span
+        class="p-2 border-left d-flex align-items-center"
+        @click="$store.dispatch('fairwayprofile/clearSelection')"
+      >
+        <font-awesome-icon icon="times"></font-awesome-icon>
+      </span>
+    </div>
+  </div>
+</template>
+
+<style lang="scss" scoped>
+.infobar {
+  height: 2.2rem;
+  z-index: 2;
+}
+
+.infobar svg path {
+  fill: #666;
+}
+</style>
+
+<script>
+/* 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):
+ * Markus Kottländer <markus.kottlaender@intevation.de>
+ */
+import { mapState } from "vuex";
+
+export default {
+  name: "infobar",
+  computed: {
+    ...mapState("application", ["showSplitscreen"]),
+    ...mapState("fairwayprofile", ["currentProfile"]),
+    ...mapState("bottlenecks", ["selectedBottleneck", "selectedSurvey"])
+  }
+};
+</script>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/client/src/components/fairway/Profiles.vue	Wed Dec 12 09:22:20 2018 +0100
@@ -0,0 +1,471 @@
+<template>
+  <div
+    :class="[
+      'box ui-element rounded bg-white text-nowrap',
+      { expanded: showProfiles }
+    ]"
+  >
+    <div>
+      <h6 class="mb-0 py-2 px-3 border-bottom d-flex align-items-center">
+        <font-awesome-icon icon="chart-area" class="mr-2"></font-awesome-icon>
+        <translate>Profiles</translate>
+        <font-awesome-icon
+          icon="times"
+          class="ml-auto text-muted"
+          @click="$store.commit('application/showProfiles', false)"
+        ></font-awesome-icon>
+      </h6>
+      <div
+        class="d-flex flex-column p-3 flex-grow-1 text-left position-relative"
+      >
+        <div
+          class="loading d-flex justify-content-center align-items-center"
+          v-if="surveysLoading || profileLoading"
+        >
+          <font-awesome-icon icon="spinner" spin />
+        </div>
+        <select
+          @click="moveToBottleneck"
+          v-model="selectedBottleneck"
+          class="form-control font-weight-bold"
+        >
+          <option :value="null">
+            <translate>Select Bottleneck</translate>
+          </option>
+          <option
+            v-for="bn in bottlenecks"
+            :key="bn.properties.name"
+            :value="bn.properties.name"
+            >{{ bn.properties.name }}</option
+          >
+        </select>
+        <div v-if="selectedBottleneck">
+          <div class="d-flex mt-2">
+            <div class="flex-fill">
+              <small class="text-muted">
+                <translate>Sounding Result</translate>:
+              </small>
+              <select
+                v-model="selectedSurvey"
+                class="form-control form-control-sm"
+              >
+                <option
+                  v-for="survey in surveys"
+                  :key="survey.date_info"
+                  :value="survey"
+                  >{{ formatSurveyDate(survey.date_info) }}</option
+                >
+              </select>
+            </div>
+            <div
+              class="flex-fill ml-3"
+              v-if="selectedSurvey && surveys.length > 1"
+            >
+              <small class="text-muted mt-1">
+                <translate>Compare with</translate>:
+              </small>
+              <select
+                v-model="additionalSurvey"
+                class="form-control form-control-sm"
+              >
+                <option :value="null">None</option>
+                <option
+                  v-for="survey in additionalSurveys"
+                  :key="survey.date_info"
+                  :value="survey"
+                  >{{ formatSurveyDate(survey.date_info) }}</option
+                >
+              </select>
+            </div>
+          </div>
+          <hr class="w-100 mb-0" />
+          <small class="text-muted d-block mt-2">
+            <translate>Saved cross profiles</translate>:
+          </small>
+          <div class="d-flex">
+            <select
+              :class="[
+                'form-control form-control-sm flex-fill',
+                { 'rounded-left-only': selectedCut }
+              ]"
+              v-model="selectedCut"
+            >
+              <option></option>
+              <option
+                v-for="(cut, index) in previousCuts"
+                :value="cut"
+                :key="index"
+                >{{ cut.label }}</option
+              >
+            </select>
+            <button
+              class="btn btn-sm btn-danger input-button-right"
+              @click="confirmDeleteSelectedCut = true"
+              v-if="selectedCut && !confirmDeleteSelectedCut"
+            >
+              <font-awesome-icon icon="trash" />
+            </button>
+            <button
+              class="btn btn-sm btn-info rounded-0"
+              @click="confirmDeleteSelectedCut = false"
+              v-if="selectedCut && confirmDeleteSelectedCut"
+            >
+              <font-awesome-icon icon="times" />
+            </button>
+            <button
+              class="btn btn-sm btn-danger input-button-right"
+              @click="deleteSelectedCut"
+              v-if="selectedCut && confirmDeleteSelectedCut"
+            >
+              <font-awesome-icon icon="check" />
+            </button>
+          </div>
+          <small class="text-muted d-block mt-2">
+            <translate>Enter coordinates manually</translate>:
+          </small>
+          <div class="position-relative">
+            <input
+              class="form-control form-control-sm pr-5"
+              placeholder="Lat,Lon,Lat,Lon"
+              v-model="coordinatesInput"
+            />
+            <button
+              class="btn btn-sm btn-info position-absolute input-button-right"
+              @click="applyManualCoordinates"
+              style="top: 0; right: 0;"
+              v-if="coordinatesInputIsValid"
+            >
+              <font-awesome-icon icon="check" />
+            </button>
+          </div>
+          <small class="d-flex text-left mt-2" v-if="startPoint && endPoint">
+            <div class="text-nowrap mr-3">
+              <b> <translate>Start</translate>: </b> <br />
+              Lat: {{ startPoint[1] }} <br />
+              Lon: {{ startPoint[0] }}
+            </div>
+            <div class="text-nowrap">
+              <b>End:</b> <br />
+              Lat: {{ endPoint[1] }} <br />
+              Lon: {{ endPoint[0] }}
+            </div>
+            <button
+              v-clipboard:copy="coordinatesForClipboard"
+              v-clipboard:success="onCopyCoordinates"
+              class="btn btn-info btn-sm ml-auto mt-auto"
+            >
+              <font-awesome-icon icon="copy" />
+            </button>
+          </small>
+          <div class="d-flex mt-3">
+            <div
+              class="pr-3 w-50"
+              v-if="startPoint && endPoint && !selectedCut"
+            >
+              <button
+                class="btn btn-info btn-sm w-100"
+                @click="showLabelInput = !showLabelInput"
+              >
+                <font-awesome-icon :icon="showLabelInput ? 'times' : 'check'" />
+                {{ showLabelInput ? "Cancel" : "Save" }}
+              </button>
+            </div>
+            <div
+              :class="startPoint && endPoint && !selectedCut ? 'w-50' : 'w-100'"
+            >
+              <button class="btn btn-info btn-sm w-100" @click="toggleCutTool">
+                <font-awesome-icon
+                  :icon="cutTool && cutTool.getActive() ? 'times' : 'plus'"
+                ></font-awesome-icon>
+                {{ cutTool && cutTool.getActive() ? "Cancel" : "New" }}
+              </button>
+            </div>
+          </div>
+          <div v-if="showLabelInput" class="mt-2">
+            <small class="text-muted">
+              <translate>Enter label for cross profile</translate>:
+            </small>
+            <div class="position-relative">
+              <input
+                class="form-control form-control-sm pr-5"
+                v-model="cutLabel"
+              />
+              <button
+                class="btn btn-sm btn-info position-absolute input-button-right"
+                @click="saveCut"
+                v-if="cutLabel"
+                style="top: 0; right: 0;"
+              >
+                <font-awesome-icon icon="check" />
+              </button>
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<style lang="scss" scoped>
+.loading {
+  background: rgba(255, 255, 255, 0.9);
+  position: absolute;
+  z-index: 99;
+  top: 0;
+  right: 0;
+  bottom: 0;
+  left: 0;
+}
+
+.input-button-right {
+  border-top-right-radius: $border-radius;
+  border-bottom-right-radius: $border-radius;
+  border-top-left-radius: 0 !important;
+  border-bottom-left-radius: 0 !important;
+}
+
+.rounded-left-only {
+  border-top-right-radius: 0 !important;
+  border-bottom-right-radius: 0 !important;
+  border-top-left-radius: $border-radius;
+  border-bottom-left-radius: $border-radius;
+}
+</style>
+
+<script>
+/* 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):
+ * Markus Kottländer <markus.kottlaender@intevation.de>
+ */
+import { mapState, mapGetters } from "vuex";
+import Feature from "ol/Feature";
+import LineString from "ol/geom/LineString";
+import { displayError, displayInfo } from "../../lib/errors.js";
+import { formatSurveyDate } from "../../lib/date.js";
+
+export default {
+  name: "profiles",
+  data() {
+    return {
+      coordinatesInput: "",
+      cutLabel: "",
+      showLabelInput: false,
+      confirmDeleteSelectedCut: false
+    };
+  },
+  computed: {
+    ...mapGetters("map", ["getVSourceByName"]),
+    ...mapState("application", ["showProfiles"]),
+    ...mapState("map", ["lineTool", "polygonTool", "cutTool"]),
+    ...mapState("bottlenecks", ["bottlenecks", "surveys", "surveysLoading"]),
+    ...mapState("fairwayprofile", [
+      "previousCuts",
+      "startPoint",
+      "endPoint",
+      "profileLoading"
+    ]),
+    selectedBottleneck: {
+      get() {
+        return this.$store.state.bottlenecks.selectedBottleneck;
+      },
+      set(name) {
+        this.$store
+          .dispatch("bottlenecks/setSelectedBottleneck", name)
+          .then(() => {
+            this.$store.commit("bottlenecks/setFirstSurveySelected");
+          });
+      }
+    },
+    selectedSurvey: {
+      get() {
+        return this.$store.state.bottlenecks.selectedSurvey;
+      },
+      set(survey) {
+        this.$store.commit("fairwayprofile/additionalSurvey", null);
+        this.$store.commit("bottlenecks/selectedSurvey", survey);
+      }
+    },
+    additionalSurvey: {
+      get() {
+        return this.$store.state.fairwayprofile.additionalSurvey;
+      },
+      set(survey) {
+        this.$store.commit("fairwayprofile/additionalSurvey", survey);
+      }
+    },
+    selectedCut: {
+      get() {
+        return this.$store.state.fairwayprofile.selectedCut;
+      },
+      set(cut) {
+        this.$store.commit("fairwayprofile/selectedCut", cut);
+        if (!cut) {
+          this.$store.commit("fairwayprofile/clearCurrentProfile");
+          this.$store.commit("application/showSplitscreen", false);
+          this.getVSourceByName("Cut Tool").clear();
+        }
+      }
+    },
+    additionalSurveys() {
+      return this.surveys.filter(survey => survey !== this.selectedSurvey);
+    },
+    coordinatesForClipboard() {
+      return (
+        this.startPoint[1] +
+        "," +
+        this.startPoint[0] +
+        "," +
+        this.endPoint[1] +
+        "," +
+        this.endPoint[0]
+      );
+    },
+    coordinatesInputIsValid() {
+      const coordinates = this.coordinatesInput
+        .split(",")
+        .map(coord => parseFloat(coord.trim()))
+        .filter(c => Number(c) === c);
+      return coordinates.length === 4;
+    }
+  },
+  watch: {
+    selectedBottleneck() {
+      this.$store.dispatch("fairwayprofile/previousCuts");
+      this.cutLabel =
+        this.selectedBottleneck + " (" + new Date().toISOString() + ")";
+    },
+    selectedSurvey(survey) {
+      this.loadProfile(survey);
+    },
+    additionalSurvey(survey) {
+      this.loadProfile(survey);
+    },
+    selectedCut(cut) {
+      if (cut) {
+        this.confirmDeleteSelectedCut = false;
+        this.applyCoordinates(cut.coordinates);
+      }
+    }
+  },
+  methods: {
+    formatSurveyDate(date) {
+      return formatSurveyDate(date);
+    },
+    loadProfile(survey) {
+      if (survey) {
+        this.$store.commit("fairwayprofile/profileLoading", true);
+        this.$store
+          .dispatch("fairwayprofile/loadProfile", survey)
+          .finally(() =>
+            this.$store.commit("fairwayprofile/profileLoading", false)
+          );
+      }
+    },
+    toggleCutTool() {
+      this.cutTool.setActive(!this.cutTool.getActive());
+      this.lineTool.setActive(false);
+      this.polygonTool.setActive(false);
+      this.$store.commit("map/setCurrentMeasurement", null);
+    },
+    onCopyCoordinates() {
+      displayInfo({
+        title: this.$gettext("Success"),
+        message: this.$gettext("Coordinates copied to clipboard!")
+      });
+    },
+    applyManualCoordinates() {
+      const coordinates = this.coordinatesInput
+        .split(",")
+        .map(coord => parseFloat(coord.trim()));
+      this.selectedCut = null;
+      this.coordinatesInput = "";
+      this.applyCoordinates([
+        coordinates[1],
+        coordinates[0],
+        coordinates[3],
+        coordinates[2]
+      ]);
+    },
+    applyCoordinates(coordinates) {
+      // allow only numbers
+      coordinates = coordinates.filter(c => Number(c) === c);
+      if (coordinates.length === 4) {
+        // draw line on map
+        this.getVSourceByName("Cut Tool").clear();
+        const cut = new Feature({
+          geometry: new LineString([
+            [coordinates[0], coordinates[1]],
+            [coordinates[2], coordinates[3]]
+          ]).transform("EPSG:4326", "EPSG:3857")
+        });
+        this.getVSourceByName("Cut Tool").addFeature(cut);
+
+        // draw diagram
+        this.$store.dispatch("fairwayprofile/cut", cut);
+      } else {
+        displayError({
+          title: this.$gettext("Invalid input"),
+          message: this.$gettext(
+            "Please enter correct coordinates in the format: Lat,Lon,Lat,Lon"
+          )
+        });
+      }
+    },
+    saveCut() {
+      const previousCuts =
+        JSON.parse(localStorage.getItem("previousCuts")) || [];
+      const newEntry = {
+        label: this.cutLabel,
+        bottleneckName: this.selectedBottleneck,
+        coordinates: [...this.startPoint, ...this.endPoint],
+        timestamp: new Date().getTime()
+      };
+      const existingEntry = previousCuts.find(cut => {
+        return JSON.stringify(cut) === JSON.stringify(newEntry);
+      });
+      if (!existingEntry) previousCuts.push(newEntry);
+      if (previousCuts.length > 100) previousCuts.shift();
+      localStorage.setItem("previousCuts", JSON.stringify(previousCuts));
+      this.$store.dispatch("fairwayprofile/previousCuts");
+
+      this.showLabelInput = false;
+      displayInfo({
+        title: this.$gettext("Profile saved!"),
+        message: this.$gettext(
+          'You can now select these coordinates from the "Saved cross profiles" menu to restore this cross profile.'
+        )
+      });
+    },
+    deleteSelectedCut() {
+      let previousCuts = JSON.parse(localStorage.getItem("previousCuts")) || [];
+      previousCuts = previousCuts.filter(cut => {
+        return JSON.stringify(cut) !== JSON.stringify(this.selectedCut);
+      });
+      localStorage.setItem("previousCuts", JSON.stringify(previousCuts));
+      this.$store.commit("fairwayprofile/selectedCut", null);
+      this.$store.dispatch("fairwayprofile/previousCuts");
+      displayInfo({ title: this.$gettext("Profile deleted!") });
+    },
+    moveToBottleneck() {
+      const bottleneck = this.bottlenecks.find(
+        bn => bn.properties.name === this.selectedBottleneck
+      );
+      if (!bottleneck) return;
+      this.$store.commit("map/moveMap", {
+        coordinates: bottleneck.geometry.coordinates,
+        zoom: 17,
+        preventZoomOut: true
+      });
+    }
+  }
+};
+</script>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/client/src/components/importschedule/Importschedule.vue	Wed Dec 12 09:22:20 2018 +0100
@@ -0,0 +1,166 @@
+<template>
+  <div class="d-flex flex-row">
+    <div :class="spacerStyle"></div>
+    <div class="mt-3 w-100">
+      <div class="card flex-grow-1 schedulecard shadow-xs">
+        <h6
+          class="mb-0 py-2 px-3 border-bottom d-flex text-info align-items-center"
+        >
+          <font-awesome-icon icon="clock" class="mr-2"></font-awesome-icon>
+          <translate class="headline">Importschedule</translate>
+        </h6>
+        <div class="card-body schedulecardbody">
+          <div class="card-body schedulecardbody">
+            <div class="searchandfilter  w-50 d-flex flex-row">
+              <div class="searchgroup input-group">
+                <div class="input-group-prepend">
+                  <span class="input-group-text" id="search">
+                    <font-awesome-icon icon="search"></font-awesome-icon>
+                  </span>
+                </div>
+                <input
+                  v-model="searchQuery"
+                  type="text"
+                  class="form-control"
+                  placeholder
+                  aria-label="Search"
+                  aria-describedby="search"
+                />
+              </div>
+            </div>
+            <table v-if="schedules.length" class="table">
+              <thead>
+                <tr>
+                  <th><translate>Import</translate></th>
+                  <th><translate>Type</translate></th>
+                  <th><translate>Author</translate></th>
+                  <th><translate>Schedule</translate></th>
+                  <th><translate>Email</translate></th>
+                  <th>&nbsp;</th>
+                  <th>&nbsp;</th>
+                </tr>
+              </thead>
+              <tbody>
+                <tr v-for="(schedule, index) in schedules" :key="index">
+                  <td></td>
+                  <td></td>
+                  <td></td>
+                  <td></td>
+                  <td></td>
+                  <td>
+                    <font-awesome-icon
+                      icon="pencil-alt"
+                      fixed-width
+                    ></font-awesome-icon>
+                  </td>
+                  <td>
+                    <font-awesome-icon
+                      @click="deleteSchedule"
+                      icon="trash"
+                      fixed-width
+                    ></font-awesome-icon>
+                  </td>
+                </tr>
+              </tbody>
+            </table>
+            <div v-else class="mt-4 small text-center py-3">
+              <translate>No schedules</translate>
+            </div>
+            <button
+              @click="newImport"
+              class="btn btn-info position-absolute newbutton"
+            >
+              <translate>New Import</translate>
+            </button>
+          </div>
+        </div>
+      </div>
+    </div>
+    <Importscheduledetail></Importscheduledetail>
+  </div>
+</template>
+
+<script>
+/* 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):
+ * Thomas Junk <thomas.junk@intevation.de>
+ */
+
+import { mapState } from "vuex";
+import Importscheduledetail from "./Importscheduledetail";
+//import { SCHEDULES } from "../../store/imports.js";
+
+export default {
+  name: "importschedule",
+  components: {
+    Importscheduledetail
+  },
+  data() {
+    return {
+      searchQuery: ""
+    };
+  },
+  methods: {
+    newImport() {
+      this.$store.commit("imports/setImportScheduleDetailVisible");
+    },
+    deleteSchedule(index) {
+      this.$store.commit("imports/deleteSchedule", index);
+    }
+  },
+  computed: {
+    ...mapState("application", ["showSidebar"]),
+    ...mapState("imports", ["schedules"]),
+    spacerStyle() {
+      return [
+        "spacer ml-3",
+        {
+          "spacer-expanded": this.showSidebar,
+          "spacer-collapsed": !this.showSidebar
+        }
+      ];
+    }
+  }
+};
+</script>
+
+<style lang="scss" scoped>
+.spacer {
+  height: 100vh;
+}
+
+.spacer-collapsed {
+  min-width: $icon-width + $offset;
+  transition: $transition-fast;
+}
+
+.spacer-expanded {
+  min-width: $sidebar-width + $offset;
+}
+
+.schedulecard {
+  margin-right: $offset;
+  min-height: 20rem;
+}
+
+.schedulecard-body {
+  width: 100%;
+  margin-left: auto;
+  margin-right: auto;
+}
+
+.newbutton {
+  position: absolute;
+  bottom: $offset;
+  right: $offset;
+}
+</style>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/client/src/components/importschedule/Importscheduledetail.vue	Wed Dec 12 09:22:20 2018 +0100
@@ -0,0 +1,146 @@
+<template>
+  <div
+    class="importscheduledetails  fadeIn animated"
+    v-if="importScheduleDetailVisible"
+  >
+    <div class="card h-100 shadow-xs">
+      <h6
+        class="mb-0 py-2 px-3 border-bottom d-flex text-info align-items-center"
+      >
+        <translate>New import</translate>
+        <span @click="closeDetailview" class="closebutton">
+          <font-awesome-icon icon="times"></font-awesome-icon>
+        </span>
+      </h6>
+      <div class="card-body">
+        <form @submit.prevent="save" class="ml-3">
+          <div class="d-flex flex-row w-100">
+            <div class="flex-column w-100">
+              <div class="flex-row text-left">
+                <small class="text-muted">
+                  <translate>Imports</translate>
+                </small>
+              </div>
+              <select v-model="import_" class="custom-select" id="import_">
+                <option v-for="option in this.$options.imports" :key="option">{{
+                  option
+                }}</option>
+              </select>
+            </div>
+          </div>
+          <div class="d-flex flex-row mt-3 w-100 justify-content-between">
+            <div class="flex-column w-100 mr-2">
+              <div class="flex-row text-left">
+                <small class="text-muted">
+                  <translate>Importtype</translate>
+                </small>
+              </div>
+              <select v-model="import_" class="custom-select" id="importtype">
+                <option
+                  v-for="option in this.$options.importtype"
+                  :key="option"
+                  >{{ option }}</option
+                >
+              </select>
+            </div>
+            <div class="flex-column w-100 ml-2">
+              <div class="flex-row text-left">
+                <small class="text-muted">
+                  <translate>Schedule</translate>
+                </small>
+              </div>
+              <select v-model="schedule" class="custom-select" id="period">
+                <option v-for="option in this.$options.periods" :key="option">{{
+                  option
+                }}</option>
+              </select>
+            </div>
+          </div>
+          <div class="flex-column mt-3 w-100 mr-2">
+            <div class="flex-row text-left">
+              <small class="text-muted">
+                <translate>Email Notification</translate>
+              </small>
+            </div>
+            <div class="flex-flex-row text-left">
+              <toggle-button
+                v-model="eMailNotification"
+                class="mt-2"
+                :speed="100"
+                :labels="{
+                  checked: this.$options.on,
+                  unchecked: this.$options.off
+                }"
+                :width="50"
+                :height="20"
+              />
+            </div>
+          </div>
+          <div v-if="eMailNotification" class="flex-column w-100 mr-2">
+            <div class="flex-row text-left">
+              <small class="text-muted"> <translate>Email</translate> </small>
+            </div>
+            <input class="form-control" type="text" />
+          </div>
+          <button type="submit" class="shadow-sm btn btn-info submit-button">
+            <translate>Submit</translate>
+          </button>
+        </form>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+import { mapState } from "vuex";
+import { displayInfo } from "../../lib/errors.js";
+
+export default {
+  name: "importscheduledetail",
+  data() {
+    return {
+      schedule: null,
+      import_: null,
+      eMailNotification: false
+    };
+  },
+  computed: {
+    ...mapState("imports", ["importScheduleDetailVisible"])
+  },
+  methods: {
+    save() {
+      displayInfo({
+        title: "Import",
+        message: "under construction"
+      });
+    },
+    closeDetailview() {
+      this.$store.commit("imports/clearImportScheduleDetail");
+      this.$store.commit("imports/setImportScheduleDetailInvisible");
+    }
+  },
+  imports: [],
+  importtype: [],
+  on: "on",
+  off: "off",
+  periods: {
+    DAILY: "daily",
+    MONTHLY: "monthly"
+  }
+};
+</script>
+
+<style lang="scss" scoped>
+.importscheduledetails {
+  height: 420px;
+  width: 45%;
+  margin-top: $offset;
+  margin-right: $offset;
+}
+
+.submit-button {
+  position: absolute;
+  right: $offset;
+  bottom: $offset;
+}
+</style>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/client/src/components/layers/Layers.vue	Wed Dec 12 09:22:20 2018 +0100
@@ -0,0 +1,64 @@
+<template>
+  <div
+    :class="[
+      'box ui-element rounded bg-white text-nowrap',
+      { expanded: showLayers }
+    ]"
+  >
+    <div style="width: 20rem">
+      <h6 class="mb-0 py-2 px-3 border-bottom d-flex align-items-center">
+        <font-awesome-icon icon="layer-group" class="mr-2"></font-awesome-icon
+        ><translate>Layers</translate>
+        <font-awesome-icon
+          icon="times"
+          class="ml-auto text-muted"
+          @click="$store.commit('application/showLayers', false)"
+        ></font-awesome-icon>
+      </h6>
+      <div class="d-flex flex-column p-3 small">
+        <Layerselect
+          v-for="(layer, index) in layersForLegend"
+          :layerindex="index"
+          :layername="layer.name"
+          :key="layer.name"
+          :isVisible="layer.isVisible"
+          @visibilityToggled="visibilityToggled"
+        ></Layerselect>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+/* 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):
+ * Thomas Junk <thomas.junk@intevation.de>
+ * Markus Kottländer <markus.kottlaender@intevation.de>
+ */
+import Layerselect from "./Layerselect";
+import { mapGetters, mapState } from "vuex";
+export default {
+  name: "layers",
+  components: {
+    Layerselect
+  },
+  computed: {
+    ...mapGetters("map", ["layersForLegend"]),
+    ...mapState("application", ["showLayers"])
+  },
+  methods: {
+    visibilityToggled(layer) {
+      this.$store.commit("map/toggleVisibility", layer);
+    }
+  }
+};
+</script>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/client/src/components/layers/Layerselect.vue	Wed Dec 12 09:22:20 2018 +0100
@@ -0,0 +1,85 @@
+<template>
+  <div>
+    <div class="form-check d-flex flex-row flex-start selection">
+      <input
+        class="form-check-input"
+        @change="visibilityToggled"
+        :id="layername"
+        type="checkbox"
+        :checked="isVisible"
+      />
+      <LegendElement
+        :layername="layername"
+        :layerindex="layerindex"
+      ></LegendElement>
+      <label class="layername form-check-label" @click="visibilityToggled">{{
+        layername
+      }}</label>
+    </div>
+    <div v-if="isVisible && layername == 'Bottleneck isolines'">
+      <img class="rounded my-1 d-block" :src="isolinesLegendImgUrl" />
+    </div>
+  </div>
+</template>
+
+<style lang="scss" scoped>
+.selection {
+  text-align: left;
+}
+.layername {
+  margin-left: $small-offset;
+}
+</style>
+
+<script>
+/* 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):
+ * Thomas Junk <thomas.junk@intevation.de>
+ */
+import { HTTP } from "../../lib/http";
+import LegendElement from "./LegendElement.vue";
+export default {
+  props: ["layername", "layerindex", "isVisible"],
+  name: "layerselect",
+  data() {
+    return {
+      isolinesLegendImgUrl: ""
+    };
+  },
+  components: {
+    LegendElement
+  },
+  methods: {
+    visibilityToggled() {
+      this.$emit("visibilityToggled", this.layerindex);
+    }
+  },
+  created() {
+    // fetch legend image for bottleneck isolines
+    // TODO: move to store
+    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";
+      HTTP.get(src, {
+        headers: {
+          Accept: "image/png",
+          "X-Gemma-Auth": localStorage.getItem("token")
+        },
+        responseType: "blob"
+      }).then(response => {
+        var urlCreator = window.URL || window.webkitURL;
+        this.isolinesLegendImgUrl = urlCreator.createObjectURL(response.data);
+      });
+    }
+  }
+};
+</script>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/client/src/components/layers/LegendElement.vue	Wed Dec 12 09:22:20 2018 +0100
@@ -0,0 +1,124 @@
+<template>
+  <div :id="id" class="legendelement"></div>
+</template>
+
+<script>
+/* 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):
+ * Thomas Junk <thomas.junk@intevation.de>
+ */
+import { mapGetters } from "vuex";
+
+import { Map, View } from "ol";
+import Feature from "ol/Feature";
+import { Vector as VectorLayer } from "ol/layer.js";
+import { Vector as VectorSource } from "ol/source.js";
+import LineString from "ol/geom/LineString.js";
+import Point from "ol/geom/Point";
+
+export default {
+  name: "legendelement",
+  props: ["layername", "layerindex"],
+  data: function() {
+    return {
+      myMap: null,
+      mapLayer: null
+    };
+  },
+  computed: {
+    ...mapGetters("map", ["getLayerByName"]),
+    id() {
+      return "legendelement" + this.layerindex;
+    },
+    mstyle() {
+      if (this.mapLayer && this.mapLayer.data.getStyle) {
+        return this.mapLayer.data.getStyle();
+      }
+    }
+  },
+  watch: {
+    mstyle(newStyle, oldStyle) {
+      // only recreate if there already was a style before
+      if (oldStyle) {
+        let vector = this.createVectorLayer();
+
+        this.myMap.removeLayer(this.myMap.getLayers()[0]);
+        this.myMap.addLayer(vector);
+      }
+    }
+  },
+  mounted() {
+    this.mapLayer = this.getLayerByName(this.layername);
+    if (this.mapLayer.data.getType() == "VECTOR") {
+      this.initMap();
+    } else {
+      // TODO other tiles
+    }
+  },
+  methods: {
+    initMap() {
+      let vector = this.createVectorLayer();
+
+      this.myMap = new Map({
+        layers: [vector],
+        target: this.id,
+        controls: [],
+        interactions: [],
+        view: new View({
+          center: [0, 0],
+          zoom: 3,
+          projection: "EPSG:4326"
+        })
+      });
+    },
+    createVectorLayer() {
+      let mapStyle = this.mapLayer.data.getStyle();
+
+      let feature = new Feature({
+        geometry: new LineString([[-1, 0.5], [0, 0], [0.7, 0], [1.3, -0.7]])
+      });
+
+      // special case if we need to call the style function with a special
+      // parameter or to detect a point layer
+      if (this.mapLayer["forLegendStyle"]) {
+        if (this.mapLayer.forLegendStyle.point) {
+          feature.setGeometry(new Point([0, 0]));
+        }
+        mapStyle = this.mapLayer.data.getStyleFunction()(
+          feature,
+          this.mapLayer.forLegendStyle.resolution
+        );
+      }
+
+      // we could add extra properties here, if they are needed for
+      // the styling function in the future. An idea is to extend the
+      // this.mapLayer["forLegendStyle"] for it.
+      // FIXME, this is a special case for the Fairway Dimensions style
+      feature.set("level_of_service", "");
+      return new VectorLayer({
+        source: new VectorSource({
+          features: [feature],
+          wrapX: false
+        }),
+        style: mapStyle
+      });
+    }
+  }
+};
+</script>
+
+<style lang="scss" scoped>
+.legendelement {
+  max-height: 1.5rem;
+  width: 2rem;
+}
+</style>
--- a/client/src/components/map/Identify.vue	Tue Dec 11 22:59:10 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,124 +0,0 @@
-<template>
-  <div
-    :class="[
-      'box ui-element rounded bg-white text-nowrap',
-      { expanded: showIdentify }
-    ]"
-  >
-    <div style="width: 20rem">
-      <h6 class="mb-0 py-2 px-3 border-bottom d-flex align-items-center">
-        <font-awesome-icon icon="info" class="mr-2"></font-awesome-icon>
-        <translate>Identified</translate>
-        <font-awesome-icon
-          icon="times"
-          class="ml-auto text-muted"
-          @click="$store.commit('application/showIdentify', false)"
-        ></font-awesome-icon>
-      </h6>
-      <div class="d-flex flex-column features p-3 flex-grow-1 text-left">
-        <div v-if="currentMeasurement">
-          <b>
-            {{ currentMeasurement.quantity }} ({{
-              currentMeasurement.unitSymbol
-            }}):
-          </b>
-          <br />
-          <small>{{ currentMeasurement.value }}</small>
-        </div>
-        <div v-for="(feature, i) of identifiedFeatures" :key="feature.getId()">
-          <div v-if="feature.getId()" :class="{ 'mt-2': i }">
-            <strong>
-              {{
-                feature.getId().replace(/[.][^.]*$/, "")
-                /* cut away everything from the last . to the end */
-              }}:
-            </strong>
-            <small
-              v-for="(value, key) in prepareProperties(feature)"
-              :key="key"
-            >
-              <div v-if="value">{{ key }}:{{ value }}</div>
-            </small>
-          </div>
-        </div>
-        <div
-          v-if="!currentMeasurement && !identifiedFeatures.length"
-          class="text-muted small text-center my-auto"
-        >
-          <translate>No features identified.</translate>
-        </div>
-      </div>
-      <div class="versioninfo border-top p-3 text-left">
-        <span v-translate="{ license: 'AGPL-3.0-or-later' }">
-          This app uses <i>gemma</i>, which is Free Software under <br />
-          %{ license } without warranty, see docs for details.
-        </span>
-        <br />
-        <a href="https://hg.intevation.de/gemma/file/tip">
-          <translate>source-code</translate>
-        </a>
-        {{ versionStr }} <br />© via donau. &#x24D4; Intevation. <br />
-        <span v-translate="{ name: 'OpenSteetMap' }"
-          >Some data ©
-          <a href="https://www.openstreetmap.org/copyright">%{ name }</a>
-          contributors.
-        </span>
-        <p v-translate="{ geoLicense: 'CC-BY-4.0' }">
-          Uses
-          <a href="https://download.geonames.org/export/dump/readme.txt"
-            >GeoNames</a
-          >
-          under %{ geoLicense }.
-        </p>
-      </div>
-    </div>
-  </div>
-</template>
-
-<style lang="scss" scoped>
-.features {
-  max-height: 19rem;
-  overflow-y: auto;
-}
-
-.versioninfo {
-  font-size: 60%;
-  white-space: normal;
-}
-</style>
-
-<script>
-/* 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):
- * Thomas Junk <thomas.junk@intevation.de>
- * Bernhard E. Reiter <bernhard.reiter@intevation.de>
- * Markus Kottländer <markus.kottlaender@intevation.de>
- */
-import { mapState, mapGetters } from "vuex";
-
-export default {
-  name: "identify",
-  computed: {
-    ...mapGetters("application", ["versionStr"]),
-    ...mapState("application", ["showIdentify"]),
-    ...mapState("map", ["identifiedFeatures", "currentMeasurement"])
-  },
-  methods: {
-    prepareProperties(feature) {
-      // return dict object with propertyname:plainvalue prepared for display
-      var properties = feature.getProperties();
-      delete properties[feature.getGeometryName()];
-      return properties;
-    }
-  }
-};
-</script>
--- a/client/src/components/map/Main.vue	Tue Dec 11 22:59:10 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,33 +0,0 @@
-<template>
-  <div class="main d-flex flex-column">
-    <Maplayer></Maplayer>
-    <FairwayProfile></FairwayProfile>
-  </div>
-</template>
-
-<script>
-/* 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):
- * Thomas Junk <thomas.junk@intevation.de>
- */
-
-import Maplayer from "./Maplayer";
-import FairwayProfile from "./fairway/Fairwayprofile";
-
-export default {
-  name: "mainview",
-  components: {
-    Maplayer,
-    FairwayProfile
-  }
-};
-</script>
--- a/client/src/components/map/Maplayer.vue	Tue Dec 11 22:59:10 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,383 +0,0 @@
-<template>
-  <div id="map" :class="mapStyle"></div>
-</template>
-
-<style lang="scss" scoped>
-.nocursor {
-  cursor: none;
-}
-
-.mapsplit {
-  height: 50vh;
-}
-
-.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>
-/* 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):
- * * Thomas Junk <thomas.junk@intevation.de>
- * * Bernhard E. Reiter <bernhard.reiter@intevation.de>
- */
-import { HTTP } from "../../lib/http";
-import { mapGetters, mapState } from "vuex";
-import "ol/ol.css";
-import { Map, View } from "ol";
-import { WFS, GeoJSON } from "ol/format.js";
-import { Stroke, Style, Fill } from "ol/style.js";
-
-/* for the sake of debugging */
-/* eslint-disable no-console */
-export default {
-  name: "maplayer",
-  data() {
-    return {
-      projection: "EPSG:3857"
-    };
-  },
-  computed: {
-    ...mapGetters("map", ["getLayerByName", "getVSourceByName"]),
-    ...mapState("map", [
-      "extent",
-      "layers",
-      "openLayersMap",
-      "lineTool",
-      "polygonTool",
-      "cutTool"
-    ]),
-    ...mapState("bottlenecks", ["selectedSurvey"]),
-    ...mapState("application", ["showSplitscreen"]),
-    mapStyle() {
-      return {
-        mapfull: !this.showSplitscreen,
-        mapsplit: this.showSplitscreen,
-        nocursor: this.hasActiveInteractions
-      };
-    },
-    hasActiveInteractions() {
-      return (
-        (this.lineTool && this.lineTool.getActive()) ||
-        (this.polygonTool && this.polygonTool.getActive()) ||
-        (this.cutTool && this.cutTool.getActive())
-      );
-    }
-  },
-  methods: {
-    buildVectorLoader(featureRequestOptions, endpoint, vectorSource) {
-      // build a function to be used for VectorSource.setLoader()
-      // make use of WFS().writeGetFeature to build the request
-      // and use our HTTP library to actually do it
-      // NOTE: a) the geometryName has to be given in featureRequestOptions,
-      //          because we want to load depending on the bbox
-      //  b) the VectorSource has to have the option strategy: bbox
-      featureRequestOptions["outputFormat"] = "application/json";
-      var loader = function(extent, resolution, projection) {
-        featureRequestOptions["bbox"] = extent;
-        featureRequestOptions["srsName"] = projection.getCode();
-        var featureRequest = new WFS().writeGetFeature(featureRequestOptions);
-        // DEBUG console.log(featureRequest);
-        HTTP.post(
-          endpoint,
-          new XMLSerializer().serializeToString(featureRequest),
-          {
-            headers: {
-              "X-Gemma-Auth": localStorage.getItem("token"),
-              "Content-type": "text/xml; charset=UTF-8"
-            }
-          }
-        )
-          .then(response => {
-            var features = new GeoJSON().readFeatures(
-              JSON.stringify(response.data)
-            );
-            vectorSource.addFeatures(features);
-            // console.log(
-            //   "loaded",
-            //   features.length,
-            //   featureRequestOptions.featureTypes,
-            //   "features"
-            // );
-            // DEBUG console.log("loaded ", features, "for", vectorSource);
-            // eslint-disable-next-line
-          })
-          .catch(() => {
-            vectorSource.removeLoadedExtent(extent);
-          });
-      };
-      return loader;
-    },
-    updateBottleneckFilter(bottleneck_id, datestr) {
-      console.log("updating filter with", bottleneck_id, datestr);
-      const layer = this.getLayerByName("Bottleneck isolines");
-      const wmsSrc = layer.data.getSource();
-      const exists = bottleneck_id != "does_not_exist";
-
-      if (exists) {
-        wmsSrc.updateParams({
-          cql_filter:
-            "date_info='" +
-            datestr +
-            "' AND bottleneck_id='" +
-            bottleneck_id +
-            "'"
-        });
-      }
-      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: {
-    showSplitscreen() {
-      const map = this.openLayersMap;
-      this.$nextTick(() => {
-        map && map.updateSize();
-      });
-    },
-    selectedSurvey(newSelectedSurvey) {
-      if (newSelectedSurvey) {
-        this.updateBottleneckFilter(
-          newSelectedSurvey.bottleneck_id,
-          newSelectedSurvey.date_info
-        );
-      } else {
-        this.updateBottleneckFilter("does_not_exist", "1999-10-01");
-      }
-    }
-  },
-  mounted() {
-    let map = new Map({
-      layers: [...this.layers.map(x => x.data)],
-      target: "map",
-      controls: [],
-      view: new View({
-        center: [this.extent.lon, this.extent.lat],
-        zoom: this.extent.zoom,
-        projection: this.projection
-      })
-    });
-    map.on("moveend", event => {
-      const center = event.map.getView().getCenter();
-      this.$store.commit("map/extent", {
-        lat: center[1],
-        lon: center[0],
-        zoom: event.map.getView().getZoom()
-      });
-    });
-    this.$store.dispatch("map/openLayersMap", map);
-
-    // TODO make display of layers more dynamic, e.g. from a list
-
-    // loading the full WFS layer, by not setting the loader function
-    // and without bboxStrategy
-    var featureRequest = new WFS().writeGetFeature({
-      srsName: "EPSG:3857",
-      featureNS: "gemma",
-      featurePrefix: "gemma",
-      featureTypes: ["fairway_dimensions"],
-      outputFormat: "application/json"
-    });
-
-    // NOTE: loading the full fairway_dimensions makes sure
-    //       that all are available for the intersection with the profile
-    HTTP.post(
-      "/internal/wfs",
-      new XMLSerializer().serializeToString(featureRequest),
-      {
-        headers: {
-          "X-Gemma-Auth": localStorage.getItem("token"),
-          "Content-type": "text/xml; charset=UTF-8"
-        }
-      }
-    ).then(response => {
-      this.getVSourceByName("Fairway Dimensions").addFeatures(
-        new GeoJSON().readFeatures(JSON.stringify(response.data))
-      );
-      // would scale to the extend of all resulting features
-      // this.openLayersMap.getView().fit(vectorSrc.getExtent());
-    });
-
-    // load following layers with bboxStrategy (using our request builder)
-    var layer = null;
-
-    layer = this.getLayerByName("Waterway Area");
-    layer.data.getSource().setLoader(
-      this.buildVectorLoader(
-        {
-          featurePrefix: "ws-wamos",
-          featureTypes: ["ienc_wtware"],
-          geometryName: "geom"
-        },
-        "/external/d4d",
-        layer.data.getSource()
-      )
-    );
-
-    layer = this.getLayerByName("Waterway Axis");
-    layer.data.getSource().setLoader(
-      this.buildVectorLoader(
-        {
-          featurePrefix: "ws-wamos",
-          featureTypes: ["ienc_wtwaxs"],
-          geometryName: "geom"
-        },
-        "/external/d4d",
-        layer.data.getSource()
-      )
-    );
-
-    layer = this.getLayerByName("Distance marks");
-    layer.data.getSource().setLoader(
-      this.buildVectorLoader(
-        {
-          featurePrefix: "ws-wamos",
-          featureTypes: ["ienc_dismar"],
-          geometryName: "geom" //,
-          /* restrict loading approximately to extend of danube in Austria */
-          // filter: bboxFilter("geom", [13.3, 48.0, 17.1, 48.6], "EPSG:4326")
-        },
-        "/external/d4d",
-        layer.data.getSource()
-      )
-    );
-    layer.data.setVisible(layer.isVisible);
-
-    layer = this.getLayerByName("Distance marks, Axis");
-    layer.data.getSource().setLoader(
-      this.buildVectorLoader(
-        {
-          featureNS: "gemma",
-          featurePrefix: "gemma",
-          featureTypes: ["distance_marks_geoserver"],
-          geometryName: "geom"
-        },
-        "/internal/wfs",
-        layer.data.getSource()
-      )
-    );
-
-    layer = this.getLayerByName("Waterway Area, named");
-    layer.data.getSource().setLoader(
-      this.buildVectorLoader(
-        {
-          featureNS: "gemma",
-          featurePrefix: "gemma",
-          featureTypes: ["hydro_seaare"],
-          geometryName: "geom"
-        },
-        "/external/d4d",
-        layer.data.getSource()
-      )
-    );
-    layer.data.setVisible(layer.isVisible);
-
-    layer = this.getLayerByName("Bottlenecks");
-    layer.data.getSource().setLoader(
-      this.buildVectorLoader(
-        {
-          featureNS: "gemma",
-          featurePrefix: "gemma",
-          featureTypes: ["bottlenecks"],
-          geometryName: "area"
-        },
-        "/internal/wfs",
-        layer.data.getSource()
-      )
-    );
-    HTTP.get("/system/style/Bottlenecks/stroke", {
-      headers: { "X-Gemma-Auth": localStorage.getItem("token") }
-    })
-      .then(response => {
-        this.btlnStrokeC = response.data.code;
-        HTTP.get("/system/style/Bottlenecks/fill", {
-          headers: { "X-Gemma-Auth": localStorage.getItem("token") }
-        })
-          .then(response => {
-            this.btlnFillC = response.data.code;
-            var newstyle = new Style({
-              stroke: new Stroke({
-                color: this.btlnStrokeC,
-                width: 4
-              }),
-              fill: new Fill({
-                color: this.btlnFillC
-              })
-            });
-            layer.data.setStyle(newstyle);
-          })
-          .catch(error => {
-            console.log(error);
-          });
-      })
-      .catch(error => {
-        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");
-    this.$store.dispatch("bottlenecks/loadBottlenecks");
-  }
-};
-</script>
--- a/client/src/components/map/Pdftool.vue	Tue Dec 11 22:59:10 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,110 +0,0 @@
-<template>
-  <div
-    :class="[
-      'box ui-element rounded bg-white text-nowrap',
-      { expanded: showPdfTool }
-    ]"
-  >
-    <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="times"
-          class="ml-auto text-muted"
-          @click="$store.commit('application/showPdfTool', false)"
-        ></font-awesome-icon>
-      </h6>
-      <div class="p-3">
-        <b><translate>Chose format:</translate></b>
-        <select v-model="form.format" class="form-control d-block w-100">
-          <option><translate>landscape</translate></option>
-          <option><translate>portrait</translate></option>
-        </select>
-        <small class="d-block my-2">
-          <input
-            type="radio"
-            id="pdfexport-downloadtype-download"
-            value="download"
-            v-model="form.downloadType"
-            selected
-          />
-          <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
-          >
-        </small>
-        <button
-          @click="download"
-          type="button"
-          class="btn btn-sm btn-info d-block w-100"
-        >
-          <translate>Generate PDF</translate>
-        </button>
-      </div>
-    </div>
-  </div>
-</template>
-
-<script>
-/* 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):
- * Markus Kottländer <markus.kottlaender@intevation.de>
- */
-import { mapState } from "vuex";
-//import { HTTP } from "../application/lib/http";
-
-export default {
-  name: "pdftool",
-  data() {
-    return {
-      form: {
-        format: "landscape",
-        downloadType: "download"
-      }
-    };
-  },
-  computed: {
-    ...mapState("application", ["showPdfTool"]),
-    ...mapState("bottlenecks", ["selectedSurvey"])
-  },
-  methods: {
-    download() {
-      // generate PDF and open it
-      // TODO: replace this src with an API reponse after actually generating PDFs
-      let src =
-        this.form.format === "landscape"
-          ? "/img/PrintTemplate-Var2-Landscape.pdf"
-          : "/img/PrintTemplate-Var2-Portrait.pdf";
-
-      let a = document.createElement("a");
-      a.href = src;
-
-      if (this.form.downloadType === "download")
-        a.download = src.substr(src.lastIndexOf("/") + 1);
-      else a.target = "_blank";
-
-      document.body.appendChild(a);
-      a.click();
-      document.body.removeChild(a);
-    }
-  }
-};
-</script>
--- a/client/src/components/map/Search.vue	Tue Dec 11 22:59:10 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,292 +0,0 @@
-<template>
-  <div :class="searchbarContainerStyle">
-    <div class="input-group-prepend m-0 d-print-none">
-      <span @click="toggleSearchbar" :class="searchButtonStyle" for="search">
-        <font-awesome-icon icon="search"></font-awesome-icon>
-      </span>
-    </div>
-    <div
-      :class="[
-        'searchgroup',
-        {
-          'searchgroup-collapsed': !showSearchbar,
-          big:
-            showContextBox &&
-            ['bottlenecks', 'staging'].indexOf(contextBoxContent) !== -1
-        }
-      ]"
-    >
-      <input
-        @keyup.enter="takeFirstSearchresult"
-        id="search"
-        v-model="searchQuery"
-        type="text"
-        :class="searchInputStyle"
-      />
-    </div>
-    <div
-      v-if="showSearchbar && searchResults !== null && !showContextBox"
-      class="searchresults border-top ui-element bg-white rounded-bottom d-print-none position-absolute"
-    >
-      <div
-        v-for="entry of searchResults"
-        :key="entry.name"
-        class="border-top text-left"
-      >
-        <a
-          href="#"
-          @click.prevent="moveToSearchResult(entry)"
-          class="p-2 d-block text-nowrap"
-        >
-          <font-awesome-icon
-            icon="ship"
-            v-if="entry.type === 'bottleneck'"
-            class="mr-1"
-            fixed-width
-          />
-          <font-awesome-icon
-            icon="water"
-            v-if="entry.type === 'rhm'"
-            class="mr-1"
-            fixed-width
-          />
-          <font-awesome-icon
-            icon="city"
-            v-if="entry.type === 'city'"
-            class="mr-1"
-            fixed-width
-          />
-          {{ entry.name }}
-        </a>
-      </div>
-    </div>
-  </div>
-</template>
-
-<style lang="scss" scoped>
-.searchcontainer {
-  opacity: 0.96;
-}
-
-.searchcontainer .searchbar {
-  border-top-left-radius: 0 !important;
-  border-bottom-left-radius: 0 !important;
-}
-
-.searchgroup {
-  margin-left: -3px;
-  transition: width 0.3s;
-  width: 300px;
-  overflow: hidden;
-}
-
-.searchgroup.big {
-  width: 571px;
-}
-
-.searchgroup-collapsed {
-  width: 0;
-}
-
-.searchbar {
-  height: 2rem !important;
-  box-shadow: none !important;
-}
-
-.searchbar.rounded-top-right {
-  border-radius: 0 !important;
-  border-top-right-radius: 0.25rem !important;
-}
-
-.searchlabel.rounded-top-left {
-  border-radius: 0 !important;
-  border-top-left-radius: 0.25rem !important;
-}
-
-.input-group-text {
-  height: 2rem;
-  width: 2rem;
-}
-
-.input-group-prepend svg path {
-  fill: #666;
-}
-
-.searchresults {
-  box-shadow: 0 0.1rem 0.5rem rgba(0, 0, 0, 0.2);
-  top: 2rem;
-  left: 0;
-  right: 0;
-  max-height: 24rem;
-  overflow: auto;
-}
-
-.searchresults > div:first-child {
-  border-top: 0 !important;
-}
-
-.searchresults a {
-  text-decoration: none;
-}
-
-.searchresults a:hover {
-  background: #f8f8f8;
-}
-</style>
-
-<script>
-/* 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):
- * Markus Kottländer <markus.kottlaender@intevation.de>
- */
-import debounce from "lodash.debounce";
-import { mapState } from "vuex";
-
-import { displayError } from "../../lib/errors.js";
-import { HTTP } from "../../lib/http";
-
-const setFocus = () => document.querySelector("#search").focus();
-
-export default {
-  name: "search",
-  data() {
-    return {
-      searchQueryIsDirty: false,
-      searchResults: null,
-      isSearching: false
-    };
-  },
-  computed: {
-    ...mapState("application", [
-      "showSearchbar",
-      "showContextBox",
-      "contextBoxContent"
-    ]),
-    searchQuery: {
-      get() {
-        return this.$store.state.application.searchQuery;
-      },
-      set(value) {
-        this.$store.commit("application/searchQuery", value);
-      }
-    },
-    searchIndicator: function() {
-      if (this.isSearching) {
-        return "⟳";
-      } else if (this.searchQueryIsDirty) {
-        return "";
-      } else {
-        return "✓";
-      }
-    },
-    searchbarContainerStyle() {
-      return [
-        "input-group searchcontainer shadow-xs",
-        {
-          "d-flex": this.contextBoxContent !== "imports",
-          "d-none": this.contextBoxContent === "imports" && this.showContextBox
-        }
-      ];
-    },
-    searchInputStyle() {
-      return [
-        "form-control ui-element search searchbar d-print-none border-0",
-        { "rounded-top-right": this.showContextBox || this.searchResults }
-      ];
-    },
-    searchButtonStyle() {
-      return [
-        "ui-element input-group-text p-0 d-flex border-0 justify-content-center searchlabel bg-white d-print-none",
-        {
-          rounded: !this.showSearchbar,
-          "rounded-left": this.showSearchbar,
-          "rounded-top-left":
-            this.showSearchbar && (this.showContextBox || this.searchResults)
-        }
-      ];
-    }
-  },
-  watch: {
-    searchQuery: function() {
-      this.searchQueryIsDirty = true;
-      this.triggerSearch();
-    }
-  },
-  methods: {
-    takeFirstSearchresult() {
-      if (!this.searchResults || this.searchResults.length != 1) return;
-      this.moveToSearchResult(this.searchResults[0]);
-    },
-    triggerSearch: debounce(function() {
-      this.doSearch();
-    }, 500),
-    doSearch() {
-      this.isCalculating = true;
-      this.searchResults = null;
-
-      if (this.searchQuery == "") {
-        return;
-      }
-
-      HTTP.post(
-        "/search",
-        { string: this.searchQuery },
-        {
-          headers: {
-            "X-Gemma-Auth": localStorage.getItem("token"),
-            "Content-type": "text/xml; charset=UTF-8"
-          }
-        }
-      )
-        .then(response => {
-          // console.log("got:", response.data);
-          this.searchResults = response.data;
-        })
-        .catch(error => {
-          const { status, data } = error.response;
-          displayError({
-            title: this.$gettext("Backend Error"),
-            message: `${status}: ${data.message || data}`
-          });
-        });
-
-      this.isCalculating = false;
-      this.searchQueryIsDirty = false;
-    },
-    moveToSearchResult(resultEntry) {
-      // DEBUG console.log("Moving to", resultEntry);
-      if (resultEntry.geom.type == "Point") {
-        let zoom = 11;
-        if (resultEntry.type === "bottleneck") zoom = 17;
-        if (resultEntry.type === "rhm") zoom = 15;
-        if (resultEntry.type === "city") zoom = 13;
-
-        this.$store.commit("map/moveMap", {
-          coordinates: resultEntry.geom.coordinates,
-          zoom,
-          preventZoomOut: true
-        });
-      }
-      // this.searchQuery = ""; // clear search query again
-      this.toggleSearchbar();
-    },
-    toggleSearchbar() {
-      if (!this.showContextBox) {
-        if (!this.showSearchbar) {
-          setTimeout(setFocus, 300);
-        }
-        this.$store.commit("application/showSearchbar", !this.showSearchbar);
-      }
-    }
-  }
-};
-</script>
--- a/client/src/components/map/Zoom.vue	Tue Dec 11 22:59:10 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,76 +0,0 @@
-<template>
-  <div
-    class="d-flex buttoncontainer shadow-xs mb-3 position-absolute"
-    :style="showSplitscreen ? 'margin-bottom: 51vh !important' : ''"
-  >
-    <button
-      class="zoomButton border-0 bg-white rounded-left ui-element"
-      @click="zoomOut"
-    >
-      <font-awesome-icon icon="minus"></font-awesome-icon>
-    </button>
-    <button
-      class="zoomButton border-0 bg-white rounded-right ui-element border-right"
-      @click="zoomIn"
-    >
-      <font-awesome-icon icon="plus"></font-awesome-icon>
-    </button>
-  </div>
-</template>
-
-<style lang="scss" scoped>
-.buttoncontainer {
-  bottom: 0;
-  left: 50%;
-  margin-left: -$icon-width;
-}
-
-.zoomButton {
-  min-height: $icon-width;
-  min-width: $icon-width;
-  z-index: 1;
-  outline: none;
-  color: #666;
-}
-</style>
-<script>
-/* 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):
- * Markus Kottländer <markus@intevation.de>
- * Thomas Junk <thomas.junk@intevation.de>
- */
-import { mapState } from "vuex";
-
-export default {
-  name: "zoom",
-  computed: {
-    ...mapState("map", ["openLayersMap"]),
-    ...mapState("application", ["showSplitscreen"]),
-    zoomLevel: {
-      get() {
-        return this.openLayersMap.getView().getZoom();
-      },
-      set(value) {
-        this.openLayersMap.getView().animate({ zoom: value, duration: 300 });
-      }
-    }
-  },
-  methods: {
-    zoomIn() {
-      this.zoomLevel = this.zoomLevel + 1;
-    },
-    zoomOut() {
-      this.zoomLevel = this.zoomLevel - 1;
-    }
-  }
-};
-</script>
--- a/client/src/components/map/contextbox/Bottlenecks.vue	Tue Dec 11 22:59:10 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,324 +0,0 @@
-<template>
-  <div>
-    <h6 class="mb-0 py-2 px-3 border-bottom d-flex align-items-center">
-      <font-awesome-icon icon="ship" class="mr-2"></font-awesome-icon>
-      <translate>Bottlenecks</translate>
-    </h6>
-    <div class="row p-2 text-left small">
-      <div class="col-5">
-        <a href="#" @click="sortBy('name')" class="sort-link">
-          <translate>Name</translate>
-        </a>
-        <font-awesome-icon
-          :icon="sortIcon"
-          class="ml-1"
-          v-if="sortColumn === 'name'"
-        ></font-awesome-icon>
-      </div>
-      <div class="col-2">
-        <a href="#" @click="sortBy('latestMeasurement')" class="sort-link">
-          <translate>Latest</translate> <br />
-          <translate>Measurement</translate>
-        </a>
-        <font-awesome-icon
-          :icon="sortIcon"
-          class="ml-1"
-          v-if="sortColumn === 'latestMeasurement'"
-        ></font-awesome-icon>
-      </div>
-      <div class="col-3">
-        <a href="#" @click="sortBy('chainage')" class="sort-link">
-          <translate>Chainage</translate>
-        </a>
-        <font-awesome-icon
-          :icon="sortIcon"
-          class="ml-1"
-          v-if="sortColumn === 'chainage'"
-        ></font-awesome-icon>
-      </div>
-      <div class="col-2"></div>
-    </div>
-    <div
-      class="bottleneck-list small text-left"
-      :style="'max-height: ' + (showSplitscreen ? 18 : 35) + 'rem'"
-      v-if="filteredAndSortedBottlenecks().length"
-    >
-      <div
-        v-for="bottleneck in filteredAndSortedBottlenecks()"
-        :key="bottleneck.properties.name"
-        class="border-top row bottleneck-row mx-0"
-      >
-        <div class="col-5 py-2 text-left">
-          <a href="#" @click="selectBottleneck(bottleneck)">{{
-            bottleneck.properties.name
-          }}</a>
-        </div>
-        <div class="col-2 py-2">
-          {{ formatSurveyDate(bottleneck.properties.current) }}
-        </div>
-        <div class="col-3 py-2">
-          {{
-            displayCurrentChainage(
-              bottleneck.properties.from,
-              bottleneck.properties.to
-            )
-          }}
-        </div>
-        <div class="col-2 pr-0 text-right">
-          <button
-            type="button"
-            class="btn btn-sm btn-info rounded-0 h-100"
-            @click="loadSurveys(bottleneck.properties.name)"
-            v-if="bottleneck.properties.current"
-          >
-            <font-awesome-icon
-              icon="spinner"
-              fixed-width
-              spin
-              v-if="loading === bottleneck.properties.name"
-            ></font-awesome-icon>
-            <font-awesome-icon
-              icon="angle-down"
-              fixed-width
-              v-if="
-                loading !== bottleneck.properties.name &&
-                  openBottleneck !== bottleneck.properties.name
-              "
-            ></font-awesome-icon>
-            <font-awesome-icon
-              icon="angle-up"
-              fixed-width
-              v-if="
-                loading !== bottleneck.properties.name &&
-                  openBottleneck === bottleneck.properties.name
-              "
-            ></font-awesome-icon>
-          </button>
-        </div>
-        <div
-          :class="[
-            'col-12 p-0',
-            'surveys',
-            { open: openBottleneck === bottleneck.properties.name }
-          ]"
-        >
-          <a
-            href="#"
-            class="d-block px-3 py-2"
-            v-for="(survey, index) in openBottleneckSurveys"
-            :key="index"
-            @click="selectSurvey(survey, bottleneck)"
-            >{{ formatSurveyDate(survey.date_info) }}</a
-          >
-        </div>
-      </div>
-    </div>
-    <div v-else class="small text-center py-3 border-top">
-      <translate>No results.</translate>
-    </div>
-  </div>
-</template>
-
-<script>
-/* 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):
- * Markus Kottländer <markus.kottlaender@intevation.de>
- */
-import { mapState } from "vuex";
-import { HTTP } from "../../../lib/http";
-import { displayError } from "../../../lib/errors.js";
-import { formatSurveyDate } from "../../../lib/date.js";
-
-export default {
-  name: "bottlenecks",
-  data() {
-    return {
-      sortColumn: "name",
-      sortDirection: "ASC",
-      openBottleneck: null,
-      openBottleneckSurveys: null,
-      loading: null
-    };
-  },
-  computed: {
-    ...mapState("application", [
-      "searchQuery",
-      "showSearchbarLastState",
-      "showSplitscreen"
-    ]),
-    ...mapState("bottlenecks", ["bottlenecks"]),
-    sortIcon() {
-      return this.sortDirection === "ASC"
-        ? "sort-amount-down"
-        : "sort-amount-up";
-    }
-  },
-  methods: {
-    formatSurveyDate(date) {
-      return formatSurveyDate(date);
-    },
-    filteredAndSortedBottlenecks() {
-      return this.bottlenecks
-        .filter(bn => {
-          return bn.properties.name
-            .toLowerCase()
-            .includes(this.searchQuery.toLowerCase());
-        })
-        .sort((bnA, bnB) => {
-          switch (this.sortColumn) {
-            case "name":
-              if (
-                bnA.properties.name.toLowerCase() <
-                bnB.properties.name.toLowerCase()
-              )
-                return this.sortDirection === "ASC" ? -1 : 1;
-              if (
-                bnA.properties.name.toLowerCase() >
-                bnB.properties.name.toLowerCase()
-              )
-                return this.sortDirection === "ASC" ? 1 : -1;
-              return 0;
-
-            case "latestMeasurement": {
-              if (
-                (bnA.properties.current || "") < (bnB.properties.current || "")
-              )
-                return this.sortDirection === "ASC" ? -1 : 1;
-              if (
-                (bnA.properties.current || "") > (bnB.properties.current || "")
-              )
-                return this.sortDirection === "ASC" ? 1 : -1;
-              return 0;
-            }
-
-            case "chainage":
-              if (bnA.properties.from < bnB.properties.from)
-                return this.sortDirection === "ASC" ? -1 : 1;
-              if (bnA.properties.from > bnB.properties.from)
-                return this.sortDirection === "ASC" ? 1 : -1;
-              return 0;
-
-            default:
-              return 0;
-          }
-        });
-    },
-    selectSurvey(survey, bottleneck) {
-      this.$store
-        .dispatch(
-          "bottlenecks/setSelectedBottleneck",
-          bottleneck.properties.name
-        )
-        .then(() => {
-          this.$store.commit("bottlenecks/selectedSurvey", survey);
-        })
-        .then(() => {
-          this.$store.commit("map/moveMap", {
-            coordinates: bottleneck.geometry.coordinates,
-            zoom: 17,
-            preventZoomOut: true
-          });
-        });
-    },
-    selectBottleneck(bottleneck) {
-      this.$store
-        .dispatch(
-          "bottlenecks/setSelectedBottleneck",
-          bottleneck.properties.name
-        )
-        .then(() => {
-          this.$store.commit("bottlenecks/setFirstSurveySelected");
-        })
-        .then(() => {
-          this.$store.commit("map/moveMap", {
-            coordinates: bottleneck.geometry.coordinates,
-            zoom: 17,
-            preventZoomOut: true
-          });
-        });
-    },
-    sortBy(column) {
-      this.sortColumn = column;
-      this.sortDirection = this.sortDirection === "ASC" ? "DESC" : "ASC";
-    },
-    loadSurveys(name) {
-      this.openBottleneckSurveys = null;
-      if (name === this.openBottleneck) {
-        this.openBottleneck = null;
-      } else {
-        this.openBottleneck = name;
-        this.loading = name;
-
-        HTTP.get("/surveys/" + name, {
-          headers: {
-            "X-Gemma-Auth": localStorage.getItem("token"),
-            "Content-type": "text/xml; charset=UTF-8"
-          }
-        })
-          .then(response => {
-            this.openBottleneckSurveys = response.data.surveys.sort((a, b) => {
-              return a.date_info < b.date_info ? 1 : -1;
-            });
-          })
-          .catch(error => {
-            const { status, data } = error.response;
-            displayError({
-              title: this.$gettext("Backend Error"),
-              message: `${status}: ${data.message || data}`
-            });
-          })
-          .finally(() => (this.loading = null));
-      }
-    },
-    displayCurrentChainage(from, to) {
-      return from / 10 + " - " + to / 10;
-    }
-  },
-  mounted() {
-    this.$store.dispatch("bottlenecks/loadBottlenecks");
-  }
-};
-</script>
-
-<style lang="scss" scoped>
-.bottleneck-list {
-  overflow-y: auto;
-}
-
-.bottleneck-list .bottleneck-row a {
-  text-decoration: none;
-}
-
-.bottleneck-list .bottleneck-row:hover {
-  background: #fbfbfb;
-}
-
-.surveys {
-  max-height: 0;
-  min-height: 0;
-  overflow: hidden;
-}
-
-.surveys a:hover {
-  background: #f3f3f3;
-}
-
-.surveys.open {
-  max-height: 250px;
-  overflow: auto;
-}
-
-.sort-link {
-  color: #444;
-  font-weight: bold;
-}
-</style>
--- a/client/src/components/map/contextbox/Contextbox.vue	Tue Dec 11 22:59:10 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,96 +0,0 @@
-<template>
-  <div :class="style">
-    <div @click="close" class="ui-element close-contextbox text-muted">
-      <font-awesome-icon icon="times"></font-awesome-icon>
-    </div>
-    <Bottlenecks v-if="contextBoxContent === 'bottlenecks'"></Bottlenecks>
-    <Importsounding v-if="contextBoxContent === 'imports'"></Importsounding>
-    <Staging v-if="contextBoxContent === 'staging'"></Staging>
-  </div>
-</template>
-
-<script>
-/* 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):
- * Markus Kottländer <markus.kottlaender@intevation.de>
- */
-import { mapState } from "vuex";
-
-export default {
-  name: "contextbox",
-  components: {
-    Bottlenecks: () => import("./Bottlenecks"),
-    Importsounding: () => import("./ImportSoundingresults.vue"),
-    Staging: () => import("./Staging.vue")
-  },
-  computed: {
-    ...mapState("application", [
-      "showSearchbarLastState",
-      "contextBoxContent",
-      "showContextBox"
-    ]),
-    style() {
-      return [
-        "ui-element shadow-xs contextbox",
-        {
-          contextboxcollapsed: !this.showContextBox,
-          contextboxextended: this.showContextBox,
-          "rounded-bottom": this.contextBoxContent !== "imports",
-          rounded: this.contextBoxContent === "imports"
-        }
-      ];
-    }
-  },
-  methods: {
-    close() {
-      this.$store.commit("application/showContextBox", false);
-      this.$store.commit(
-        "application/showSearchbar",
-        this.showSearchbarLastState
-      );
-    }
-  }
-};
-</script>
-
-<style lang="scss" scoped>
-.contextbox {
-  position: relative;
-  background-color: #ffffff;
-  opacity: $slight-transparent;
-  transition: max-width 0.3s, max-height 0.3s;
-  overflow: hidden;
-  background: #fff;
-}
-.contextbox > div:last-child {
-  width: 600px;
-}
-
-.contextboxcollapsed {
-  max-width: 0;
-  max-height: 0;
-}
-
-.contextboxextended {
-  max-width: 600px;
-  max-height: 640px;
-}
-
-.close-contextbox {
-  position: absolute;
-  z-index: 2;
-  right: 0;
-  top: 7px;
-  height: $icon-width;
-  width: $icon-height;
-}
-</style>
--- a/client/src/components/map/contextbox/ImportSoundingresults.vue	Tue Dec 11 22:59:10 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,379 +0,0 @@
-<template>
-  <div>
-    <h6 class="mb-0 py-2 px-3 border-bottom d-flex align-items-center">
-      <font-awesome-icon icon="upload" class="mr-2"></font-awesome-icon>
-      <translate>Import Soundingresults</translate>
-    </h6>
-    <div v-if="editState" class="ml-auto mr-auto mt-4 w-95">
-      <div class="d-flex flex-column">
-        <div class="d-flex flex-row">
-          <div class="mt-1 text-left w-50 ml-2 mr-4">
-            <small class="text-muted">
-              <translate>Bottleneck</translate>
-            </small>
-            <select v-model="bottleneck" class="custom-select">
-              <option
-                v-for="bottleneck in availableBottlenecks"
-                :key="bottleneck"
-                >{{ bottleneck }}</option
-              >
-            </select>
-            <span class="text-danger">
-              <small v-if="!bottleneck">
-                <translate>Please select a bottleneck</translate>
-              </small>
-            </span>
-          </div>
-          <div class="d-flex flex-column mt-1 text-left w-50 mr-2">
-            <small class="text-muted">
-              <translate>Projection</translate>&nbsp;(EPSG)
-            </small>
-            <input
-              class="form-control"
-              v-model="projection"
-              value="4326"
-              placeholder="e.g. 4326"
-              type="number"
-            />
-            <span class="text-left text-danger">
-              <small v-if="!projection">
-                <translate>Please enter a projection</translate>
-              </small>
-            </span>
-          </div>
-        </div>
-        <div class="d-flex flex-row">
-          <div class="mt-1 text-left w-50 ml-2 mr-4">
-            <small class="text-muted">
-              <translate>Depthreference</translate>
-            </small>
-            <select
-              v-model="depthReference"
-              class="custom-select"
-              id="depthreference"
-            >
-              <option
-                v-for="option in this.$options.depthReferenceOptions"
-                :key="option"
-                >{{ option }}</option
-              >
-            </select>
-            <span class="text-left text-danger">
-              <small v-if="!depthReference">
-                <translate>Please enter a reference</translate>
-              </small>
-            </span>
-          </div>
-          <div class="mt-1 text-left w-50 mr-2">
-            <small class="text-muted"> <translate>Date</translate> </small>
-            <input
-              id="importdate"
-              type="date"
-              class="form-control"
-              placeholder="Date of import"
-              aria-label="bottleneck"
-              aria-describedby="bottlenecklabel"
-              v-model="importDate"
-            />
-            <span class="text-left text-danger">
-              <small v-if="!importDate">
-                <translate>Please enter a date</translate>
-              </small>
-            </span>
-          </div>
-        </div>
-      </div>
-      <div class="ml-2 mt-2 text-left">
-        <small v-for="(message, index) in messages" :key="index">
-          {{ message }}
-        </small>
-      </div>
-    </div>
-    <div class="w-95 ml-auto mr-auto mt-4 mb-4">
-      <div v-if="uploadState" class="d-flex flex-row input-group mb-4">
-        <div class="custom-file">
-          <input
-            accept=".zip"
-            type="file"
-            @change="fileSelected"
-            class="custom-file-input"
-            id="uploadFile"
-          />
-          <label class="custom-file-label" for="uploadFile">
-            {{ uploadLabel }}
-          </label>
-        </div>
-      </div>
-      <div class="buttons text-right">
-        <a
-          v-if="editState"
-          download="meta.json"
-          :href="dataLink"
-          class="btn btn-outline-info pull-left"
-        >
-          <translate>Download Meta.json</translate>
-        </a>
-        <button
-          v-if="editState"
-          @click="deleteTempData"
-          class="btn btn-danger"
-          type="button"
-        >
-          <translate>Cancel Upload</translate>
-        </button>
-        <button
-          :disabled="disableUploadButton"
-          @click="submit"
-          class="btn btn-info"
-          type="button"
-        >
-          {{ uploadState ? Upload : Confirm }}
-        </button>
-      </div>
-    </div>
-  </div>
-</template>
-
-<script>
-/* 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):
- * Thomas Junk <thomas.junk@intevation.de>
- * Markus Kottländer <markus.kottlaender@intevation.de>
- */
-import { HTTP } from "../../../lib/http";
-import { displayError, displayInfo } from "../../../lib/errors.js";
-import { mapState } from "vuex";
-
-const IMPORTSTATE = { UPLOAD: "UPLOAD", EDIT: "EDIT" };
-
-export default {
-  name: "imports",
-  data() {
-    return {
-      importState: IMPORTSTATE.UPLOAD,
-      depthReference: "",
-      bottleneck: "",
-      projection: "",
-      importDate: "",
-      uploadLabel: this.$gettext("choose .zip- file"),
-      uploadFile: null,
-      disableUpload: false,
-      token: null,
-      messages: []
-    };
-  },
-  methods: {
-    initialState() {
-      this.importState = IMPORTSTATE.UPLOAD;
-      this.depthReference = "";
-      this.bottleneck = "";
-      this.projection = "";
-      this.importDate = "";
-      this.uploadLabel = this.$gettext("choose .zip- file");
-      this.uploadFile = null;
-      this.disableUpload = false;
-      this.token = null;
-      this.messages = [];
-    },
-    fileSelected(e) {
-      const files = e.target.files || e.dataTransfer.files;
-      if (!files) return;
-      this.uploadLabel = files[0].name;
-      this.uploadFile = files[0];
-    },
-    deleteTempData() {
-      HTTP.delete("/imports/soundingresult-upload/" + this.token, {
-        headers: {
-          "X-Gemma-Auth": localStorage.getItem("token")
-        }
-      })
-        .then(() => {
-          this.initialState();
-        })
-        .catch(error => {
-          const { status, data } = error.response;
-          displayError({
-            title: this.$gettext("Backend Error"),
-            message: `${status}: ${data.message || data}`
-          });
-        });
-    },
-    submit() {
-      if (!this.uploadFile || this.disableUpload) return;
-      if (this.importState === IMPORTSTATE.UPLOAD) {
-        this.upload();
-      } else {
-        this.confirm();
-      }
-    },
-    upload() {
-      let formData = new FormData();
-      formData.append("soundingresult", this.uploadFile);
-      HTTP.post("/imports/soundingresult-upload", formData, {
-        headers: {
-          "X-Gemma-Auth": localStorage.getItem("token"),
-          "Content-Type": "multipart/form-data"
-        }
-      })
-        .then(response => {
-          if (response.data.meta) {
-            const { bottleneck, date, epsg } = response.data.meta;
-            const depthReference = response.data.meta["depth-reference"];
-            this.bottleneck = bottleneck;
-            this.depthReference = depthReference;
-            this.importDate = new Date(date).toISOString().split("T")[0];
-            this.projection = epsg;
-          }
-          this.importState = IMPORTSTATE.EDIT;
-          this.token = response.data.token;
-          this.messages = response.data.messages;
-        })
-        .catch(error => {
-          const { status, data } = error.response;
-          const messages = data.messages ? data.messages.join(", ") : "";
-          displayError({
-            title: this.$gettext("Backend Error"),
-            message: `${status}: ${messages}`
-          });
-        });
-    },
-    confirm() {
-      let formData = new FormData();
-      formData.append("token", this.token);
-      if (this.bottleneck) formData.append("bottleneck", this.bottleneck);
-      if (this.importDate)
-        formData.append("date", this.importDate.split("T")[0]);
-      if (this.depthReference)
-        formData.append("depth-reference", this.depthReference);
-      if (this.projection) formData.append("", this.projection);
-
-      HTTP.post("/imports/soundingresult", formData, {
-        headers: {
-          "X-Gemma-Auth": localStorage.getItem("token"),
-          "Content-Type": "multipart/form-data"
-        }
-      })
-        .then(() => {
-          displayInfo({
-            title: this.$gettext("Import"),
-            message: this.$gettext("Starting import for ") + this.bottleneck
-          });
-          this.initialState();
-        })
-        .catch(error => {
-          const { status, data } = error.response;
-          displayError({
-            title: this.$gettext("Backend Error"),
-            message: `${status}: ${data.message || data}`
-          });
-        });
-    }
-  },
-  mounted() {
-    this.$store.dispatch("bottlenecks/loadBottlenecks");
-  },
-  watch: {
-    showContextBox() {
-      if (!this.showContextBox && this.token) this.deleteTempData();
-    }
-  },
-  computed: {
-    ...mapState("application", ["showContextBox"]),
-    ...mapState("bottlenecks", ["bottlenecks"]),
-    disableUploadButton() {
-      if (this.importState === IMPORTSTATE.UPLOAD) return this.disableUpload;
-      if (
-        !this.bottleneck ||
-        !this.importDate ||
-        !this.depthReference ||
-        !this.projection
-      )
-        return true;
-      return this.disableUpload;
-    },
-    availableBottlenecks() {
-      return this.bottlenecks.map(x => x.properties.name);
-    },
-    editState() {
-      return this.importState === IMPORTSTATE.EDIT;
-    },
-    uploadState() {
-      return this.importState === IMPORTSTATE.UPLOAD;
-    },
-    Upload() {
-      return this.$gettext("Upload");
-    },
-    Confirm() {
-      return this.$gettext("Confirm");
-    },
-    dataLink() {
-      return (
-        "data:text/json;charset=utf-8," +
-        encodeURIComponent(
-          JSON.stringify({
-            depthReference: this.depthReference,
-            bottleneck: this.bottleneck,
-            date: this.importDate
-          })
-        )
-      );
-    }
-  },
-  depthReferenceOptions: [
-    "",
-    // "NAP",
-    // "KP",
-    // "FZP",
-    // "ADR",
-    // "TAW",
-    // "PUL",
-    // "NGM",
-    // "ETRS",
-    // "POT",
-    // "LDC",
-    // "HDC",
-    // "ZPG",
-    // "GLW",
-    // "HSW",
-    // "LNW",
-    // "HNW",
-    // "IGN",
-    // "WGS",
-    "RN" //,
-    // "HBO"
-  ]
-};
-</script>
-
-<style lang="scss" scoped>
-.projectionLabel {
-  margin-left: $small-offset;
-}
-
-.depthreferencelabel {
-  margin-left: $small-offset;
-}
-
-.offset-r {
-  margin-right: $small-offset;
-}
-
-.buttons button {
-  margin-left: $offset !important;
-}
-
-.label-text {
-  width: 5rem;
-  text-align: left;
-  line-height: 2.25rem;
-}
-</style>
--- a/client/src/components/map/contextbox/Staging.vue	Tue Dec 11 22:59:10 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,236 +0,0 @@
-<template>
-  <div class="w-90 stagingcard">
-    <h6 class="mb-0 py-2 px-3 border-bottom d-flex align-items-center">
-      <font-awesome-icon
-        class="mr-2"
-        icon="clipboard-check"
-      ></font-awesome-icon>
-      <translate>Staging Area</translate>
-    </h6>
-    <table class="table">
-      <thead>
-        <tr>
-          <th><translate>Name</translate></th>
-          <th><translate>Type</translate></th>
-          <th><translate>Date</translate></th>
-          <th><translate>Imported</translate></th>
-          <th><translate>Username</translate></th>
-          <th>&nbsp;</th>
-          <th>&nbsp;</th>
-        </tr>
-      </thead>
-      <tbody v-if="filteredData.length">
-        <tr :key="data.id" v-for="data in filteredData">
-          <td>
-            <a @click="zoomTo(data.id)" href="#">{{
-              data.summary.bottleneck
-            }}</a>
-          </td>
-          <td>{{ data.kind.toUpperCase() }}</td>
-          <td>{{ formatSurveyDate(data.summary.date) }}</td>
-          <td>{{ formatSurveyDate(data.enqueued.split("T")[0]) }}</td>
-          <td>{{ data.user }}</td>
-          <td>
-            <button
-              :class="{
-                btn: true,
-                'btn-sm': true,
-                'btn-outline-success': needsApproval(data) || isRejected(data),
-                'btn-success': isApproved(data)
-              }"
-              @click="toggleApproval(data.id, $options.STATES.APPROVED)"
-            >
-              <font-awesome-icon icon="check"></font-awesome-icon>
-            </button>
-          </td>
-          <td>
-            <button
-              :class="{
-                btn: true,
-                'btn-sm': true,
-                'btn-outline-danger': needsApproval(data) || isApproved(data),
-                'btn-danger': isRejected(data)
-              }"
-              @click="toggleApproval(data.id, $options.STATES.REJECTED)"
-            >
-              <font-awesome-icon icon="times"></font-awesome-icon>
-            </button>
-          </td>
-        </tr>
-      </tbody>
-      <tbody v-else>
-        <tr>
-          <td class="text-center" colspan="6">
-            <translate>No results.</translate>
-          </td>
-        </tr>
-      </tbody>
-    </table>
-    <div class="p-3" v-if="filteredData.length">
-      <button @click="confirmReview" class="confirm-button btn btn-info">
-        <translate>Confirm</translate>
-      </button>
-    </div>
-    <div class="p-3">
-      <button @click="loadData" class="refresh btn btn-dark">Refresh</button>
-    </div>
-  </div>
-</template>
-
-<script>
-/* 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):
- * Thomas Junk <thomas.junk@intevation.de>
- * Markus Kottländer <markus@intevation.de>
- */
-import { mapState } from "vuex";
-import { HTTP } from "../../../lib/http.js";
-import { STATES } from "../../../store/imports.js";
-import { displayError, displayInfo } from "../../../lib/errors.js";
-import { formatSurveyDate } from "../../../lib/date.js";
-
-export default {
-  data() {
-    return {};
-  },
-  mounted() {
-    this.loadData();
-  },
-  computed: {
-    ...mapState("application", ["searchQuery"]),
-    ...mapState("imports", ["staging"]),
-    filteredData() {
-      return this.staging.filter(data => {
-        const result = [data.id + "", data.enqueued, data.kind, data.user].some(
-          x => x.toLowerCase().includes(this.searchQuery.toLowerCase())
-        );
-        return result;
-      });
-    }
-  },
-  STATES: STATES,
-  methods: {
-    formatSurveyDate(date) {
-      return formatSurveyDate(date);
-    },
-    loadData() {
-      this.$store.dispatch("imports/getStaging").catch(error => {
-        const { status, data } = error.response;
-        displayError({
-          title: "Backend Error",
-          message: `${status}: ${data.message || data}`
-        });
-      });
-    },
-    confirmReview() {
-      const reviewResults = this.staging
-        .filter(x => x.status !== STATES.NEEDSAPPROVAL)
-        .map(r => {
-          return {
-            id: r.id,
-            state: r.status
-          };
-        });
-      if (!reviewResults.length) return;
-      HTTP.patch("/imports", reviewResults, {
-        headers: {
-          "X-Gemma-Auth": localStorage.getItem("token"),
-          "Content-type": "application/json"
-        }
-      })
-        .then(response => {
-          const messages = response.data
-            .map(x => {
-              if (x.message) return x.message;
-              if (x.error) return x.error;
-            })
-            .join("\n\n");
-          displayInfo({
-            title: "Staging Area",
-            message: messages,
-            options: {
-              timeout: 0,
-              buttons: [{ text: "Ok", action: null, bold: true }]
-            }
-          });
-          this.loadData();
-        })
-        .catch(error => {
-          const { status, data } = error.response;
-          displayError({
-            title: "Backend Error",
-            message: `${status}: ${data.message || data}`
-          });
-        });
-    },
-    needsApproval(item) {
-      return item.status === STATES.NEEDSAPPROVAL;
-    },
-    isRejected(item) {
-      return item.status === STATES.REJECTED;
-    },
-    isApproved(item) {
-      return item.status === STATES.APPROVED;
-    },
-    zoomTo(id) {
-      if (!id) return;
-      const soundingResult = this.filteredData.filter(x => x.id == id)[0];
-      const { lat, lon, bottleneck, date } = soundingResult.summary;
-      const coordinates = [lat, lon];
-
-      this.$store.commit("map/moveMap", {
-        coordinates: coordinates,
-        zoom: 17,
-        preventZoomOut: true
-      });
-      this.$store
-        .dispatch("bottlenecks/setSelectedBottleneck", bottleneck)
-        .then(() => {
-          this.$store.commit("bottlenecks/setSelectedSurveyByDate", date);
-        });
-    },
-    toggleApproval(id, newStatus) {
-      this.$store.commit("imports/toggleApproval", {
-        id: id,
-        newStatus: newStatus
-      });
-    }
-  }
-};
-</script>
-
-<style lang="scss" scoped>
-.refresh {
-  position: absolute;
-  left: $offset;
-  bottom: $offset;
-}
-.table th,
-td {
-  font-size: 0.9rem;
-  border-top: 0px !important;
-  border-bottom-width: 1px;
-  text-align: left;
-  padding: 0.5rem !important;
-}
-
-.stagingcard {
-  position: relative;
-  min-height: 150px;
-}
-
-.confirm-button {
-  position: absolute;
-  right: $offset;
-  bottom: $offset;
-}
-</style>
--- a/client/src/components/map/fairway/Fairwayprofile.vue	Tue Dec 11 22:59:10 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,413 +0,0 @@
-<template>
-  <div :class="['position-relative', { show: showSplitscreen }]">
-    <button
-      class="rounded-bottom bg-white border-0 position-absolute splitscreen-toggle"
-      @click="$store.commit('application/showSplitscreen', false)"
-      v-if="showSplitscreen"
-    >
-      <font-awesome-icon icon="angle-down" />
-    </button>
-    <button
-      class="rounded-bottom bg-white border-0 position-absolute clear-selection"
-      @click="$store.dispatch('fairwayprofile/clearSelection')"
-      v-if="showSplitscreen"
-    >
-      <font-awesome-icon icon="times" />
-    </button>
-    <div class="profile bg-white position-relative d-flex flex-column">
-      <h5
-        class="headline border-bottom mb-0 py-2"
-        v-if="selectedBottleneck && selectedSurvey"
-      >
-        {{ selectedBottleneck }} ({{ selectedSurvey.date_info }})
-      </h5>
-      <div class="d-flex flex-fill">
-        <div
-          class="loading d-flex justify-content-center align-items-center"
-          v-if="surveysLoading || profileLoading"
-        >
-          <font-awesome-icon icon="spinner" spin />
-        </div>
-        <div class="fairwayprofile m-3 mt-0 bg-white flex-grow-1"></div>
-      </div>
-    </div>
-  </div>
-</template>
-
-<style lang="scss" scoped>
-.profile {
-  width: 100vw;
-  height: 0;
-  overflow: hidden;
-  z-index: 2;
-}
-
-.splitscreen-toggle,
-.clear-selection {
-  width: 2rem;
-  height: 2rem;
-  margin-top: 8px;
-  z-index: 3;
-  outline: none;
-}
-
-.splitscreen-toggle svg path,
-.clear-selection svg path {
-  fill: #666;
-}
-
-.splitscreen-toggle {
-  right: 2.5rem;
-}
-
-.clear-selection {
-  right: 0.5rem;
-}
-
-.show .profile {
-  height: 50vh;
-}
-
-.loading {
-  background: rgba(255, 255, 255, 0.96);
-  position: absolute;
-  z-index: 99;
-  top: 0;
-  right: 0;
-  bottom: 0;
-  left: 0;
-}
-</style>
-
-<script>
-/* 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):
- * Thomas Junk <thomas.junk@intevation.de>
- */
-import * as d3 from "d3";
-import { mapState, mapGetters } from "vuex";
-import debounce from "debounce";
-
-const GROUND_COLOR = "#4A2F06";
-
-export default {
-  name: "fairwayprofile",
-  data() {
-    return {
-      coordinatesInput: "",
-      coordinatesSelect: null,
-      cutLabel: "",
-      showLabelInput: false,
-      width: null,
-      height: null,
-      margin: {
-        top: 20,
-        right: 40,
-        bottom: 30,
-        left: 40
-      }
-    };
-  },
-  computed: {
-    ...mapGetters("fairwayprofile", ["totalLength"]),
-    ...mapState("application", ["showSplitscreen"]),
-    ...mapState("fairwayprofile", [
-      "startPoint",
-      "endPoint",
-      "currentProfile",
-      "additionalSurvey",
-      "minAlt",
-      "maxAlt",
-      "fairwayCoordinates",
-      "waterLevels",
-      "selectedWaterLevel",
-      "profileLoading"
-    ]),
-    ...mapState("bottlenecks", [
-      "selectedBottleneck",
-      "selectedSurvey",
-      "surveysLoading"
-    ]),
-    currentData() {
-      if (
-        !this.selectedSurvey ||
-        !this.currentProfile.hasOwnProperty(this.selectedSurvey.date_info)
-      )
-        return [];
-      return this.currentProfile[this.selectedSurvey.date_info].points;
-    },
-    additionalData() {
-      if (
-        !this.additionalSurvey ||
-        !this.currentProfile.hasOwnProperty(this.additionalSurvey.date_info)
-      )
-        return [];
-      return this.currentProfile[this.additionalSurvey.date_info].points;
-    },
-    waterColor() {
-      const result = this.waterLevels.find(
-        x => x.level === this.selectedWaterLevel
-      );
-      return result.color;
-    },
-    xScale() {
-      return [0, this.totalLength];
-    },
-    yScaleLeft() {
-      const hi = Math.max(this.maxAlt, this.selectedWaterLevel);
-      return [this.minAlt, hi];
-    },
-    yScaleRight() {
-      const DELTA = this.maxAlt * 1.1 - this.maxAlt;
-      return [this.maxAlt * 1 + DELTA, -DELTA];
-    }
-  },
-  watch: {
-    currentData() {
-      this.drawDiagram();
-    },
-    additionalData() {
-      this.drawDiagram();
-    },
-    width() {
-      this.drawDiagram();
-    },
-    height() {
-      this.drawDiagram();
-    },
-    waterLevels() {
-      this.drawDiagram();
-    },
-    selectedWaterLevel() {
-      this.drawDiagram();
-    },
-    fairwayCoordinates() {
-      this.drawDiagram();
-    }
-  },
-  methods: {
-    drawDiagram() {
-      this.coordinatesSelect = null;
-      const chartDiv = document.querySelector(".fairwayprofile");
-      d3.select(".fairwayprofile svg").remove();
-      this.scaleFairwayProfile();
-      let svg = d3.select(chartDiv).append("svg");
-      svg.attr("width", this.width);
-      svg.attr("height", this.height);
-      const width = this.width - this.margin.right - 1.5 * this.margin.left;
-      const height = this.height - this.margin.top - 2 * this.margin.bottom;
-      const currentData = this.currentData;
-      const additionalData = this.additionalData;
-      const { xScale, yScaleRight, graph } = this.generateCoordinates(
-        svg,
-        height,
-        width
-      );
-      this.drawWaterlevel({ graph, xScale, yScaleRight, height });
-      this.drawLabels({ graph, height });
-      this.drawFairway({ graph, xScale, yScaleRight });
-      if (currentData) {
-        this.drawProfile({
-          graph,
-          xScale,
-          yScaleRight,
-          currentData,
-          height,
-          color: GROUND_COLOR,
-          strokeColor: "black",
-          opacity: 1
-        });
-      }
-      if (additionalData) {
-        this.drawProfile({
-          graph,
-          xScale,
-          yScaleRight,
-          currentData: additionalData,
-          height,
-          color: GROUND_COLOR,
-          strokeColor: "#943007",
-          opacity: 0.6
-        });
-      }
-    },
-    drawFairway({ graph, xScale, yScaleRight }) {
-      for (let coordinates of this.fairwayCoordinates) {
-        const [startPoint, endPoint, depth] = coordinates;
-        let fairwayArea = d3
-          .area()
-          .x(function(d) {
-            return xScale(d.x);
-          })
-          .y0(yScaleRight(0))
-          .y1(function(d) {
-            return yScaleRight(d.y);
-          });
-        graph
-          .append("path")
-          .datum([{ x: startPoint, y: depth }, { x: endPoint, y: depth }])
-          .attr("fill", "#002AFF")
-          .attr("stroke-opacity", 0.65)
-          .attr("fill-opacity", 0.65)
-          .attr("stroke", "#FFD20D")
-          .attr("d", fairwayArea);
-      }
-    },
-    drawLabels({ graph, height }) {
-      graph
-        .append("text")
-        .attr("transform", ["rotate(-90)"])
-        .attr("y", this.width - 60)
-        .attr("x", -(this.height - this.margin.top - this.margin.bottom) / 2)
-        .attr("dy", "1em")
-        .attr("fill", "black")
-        .style("text-anchor", "middle")
-        .text("Depth [m]");
-      graph
-        .append("text")
-        .attr("y", 0 - this.margin.left)
-        .attr("x", 0 - height / 4)
-        .attr("dy", "1em")
-        .attr("fill", "black")
-        .style("text-anchor", "middle")
-        .attr("transform", [
-          "translate(" + this.width / 2 + "," + this.height + ")",
-          "rotate(0)"
-        ])
-        .text("Width [m]");
-    },
-    generateCoordinates(svg, height, width) {
-      let xScale = d3
-        .scaleLinear()
-        .domain(this.xScale)
-        .rangeRound([0, width]);
-
-      xScale.ticks(5);
-      let yScaleLeft = d3
-        .scaleLinear()
-        .domain(this.yScaleLeft)
-        .rangeRound([height, 0]);
-
-      let yScaleRight = d3
-        .scaleLinear()
-        .domain(this.yScaleRight)
-        .rangeRound([height, 0]);
-
-      let xAxis = d3.axisBottom(xScale);
-      let yAxis2 = d3.axisRight(yScaleRight);
-      let graph = svg
-        .append("g")
-        .attr(
-          "transform",
-          "translate(" + this.margin.left + "," + this.margin.top + ")"
-        );
-      graph
-        .append("g")
-        .attr("transform", "translate(0," + height + ")")
-        .call(xAxis.ticks(5));
-      graph
-        .append("g")
-        .attr("transform", "translate(" + width + ",0)")
-        .call(yAxis2);
-      return { xScale, yScaleLeft, yScaleRight, graph };
-    },
-    drawWaterlevel({ graph, xScale, yScaleRight, height }) {
-      let waterArea = d3
-        .area()
-        .x(function(d) {
-          return xScale(d.x);
-        })
-        .y0(height)
-        .y1(function(d) {
-          return yScaleRight(d.y);
-        });
-      graph
-        .append("path")
-        .datum([{ x: 0, y: 0 }, { x: this.totalLength, y: 0 }])
-        .attr("fill", this.waterColor)
-        .attr("stroke", this.waterColor)
-        .attr("d", waterArea);
-    },
-    drawProfile({
-      graph,
-      xScale,
-      yScaleRight,
-      currentData,
-      height,
-      color,
-      strokeColor,
-      opacity
-    }) {
-      for (let part of currentData) {
-        let profileLine = d3
-          .line()
-          .x(d => {
-            return xScale(d.x);
-          })
-          .y(d => {
-            return yScaleRight(d.y);
-          });
-        let profileArea = d3
-          .area()
-          .x(function(d) {
-            return xScale(d.x);
-          })
-          .y0(height)
-          .y1(function(d) {
-            return yScaleRight(d.y);
-          });
-        graph
-          .append("path")
-          .datum(part)
-          .attr("fill", color)
-          .attr("stroke", color)
-          .attr("stroke-width", 3)
-          .attr("stroke-opacity", opacity)
-          .attr("fill-opacity", opacity)
-          .attr("d", profileArea);
-        graph
-          .append("path")
-          .datum(part)
-          .attr("fill", "none")
-          .attr("stroke", strokeColor)
-          .attr("stroke-linejoin", "round")
-          .attr("stroke-linecap", "round")
-          .attr("stroke-width", 3)
-          .attr("stroke-opacity", opacity)
-          .attr("fill-opacity", opacity)
-          .attr("d", profileLine);
-      }
-    },
-    scaleFairwayProfile() {
-      if (!document.querySelector(".fairwayprofile")) return;
-      const clientHeight = document.querySelector(".fairwayprofile")
-        .clientHeight;
-      const clientWidth = document.querySelector(".fairwayprofile").clientWidth;
-      if (!clientHeight || !clientWidth) return;
-      this.height = clientHeight;
-      this.width = clientWidth;
-    }
-  },
-  created() {
-    window.addEventListener("resize", debounce(this.drawDiagram), 100);
-  },
-  mounted() {
-    this.drawDiagram();
-  },
-  updated() {
-    this.scaleFairwayProfile();
-  },
-  destroyed() {
-    window.removeEventListener("resize", debounce(this.drawDiagram));
-  }
-};
-</script>
--- a/client/src/components/map/fairway/Infobar.vue	Tue Dec 11 22:59:10 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,61 +0,0 @@
-<template>
-  <div
-    v-if="Object.keys(currentProfile).length && !showSplitscreen"
-    class="ui-element shadow-xs infobar rounded bg-white ml-auto mb-3 mr-3"
-  >
-    <div class="d-flex flex-row justify-content-between h-100">
-      <h6 class="my-auto px-2">
-        {{ selectedBottleneck }} ({{ selectedSurvey.date_info }})
-      </h6>
-      <span
-        class="p-2 border-left d-flex align-items-center"
-        @click="$store.commit('application/showSplitscreen', true)"
-      >
-        <font-awesome-icon icon="angle-up"></font-awesome-icon>
-      </span>
-      <span
-        class="p-2 border-left d-flex align-items-center"
-        @click="$store.dispatch('fairwayprofile/clearSelection')"
-      >
-        <font-awesome-icon icon="times"></font-awesome-icon>
-      </span>
-    </div>
-  </div>
-</template>
-
-<style lang="scss" scoped>
-.infobar {
-  height: 2.2rem;
-  z-index: 2;
-}
-
-.infobar svg path {
-  fill: #666;
-}
-</style>
-
-<script>
-/* 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):
- * Markus Kottländer <markus.kottlaender@intevation.de>
- */
-import { mapState } from "vuex";
-
-export default {
-  name: "infobar",
-  computed: {
-    ...mapState("application", ["showSplitscreen"]),
-    ...mapState("fairwayprofile", ["currentProfile"]),
-    ...mapState("bottlenecks", ["selectedBottleneck", "selectedSurvey"])
-  }
-};
-</script>
--- a/client/src/components/map/fairway/Profiles.vue	Tue Dec 11 22:59:10 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,471 +0,0 @@
-<template>
-  <div
-    :class="[
-      'box ui-element rounded bg-white text-nowrap',
-      { expanded: showProfiles }
-    ]"
-  >
-    <div>
-      <h6 class="mb-0 py-2 px-3 border-bottom d-flex align-items-center">
-        <font-awesome-icon icon="chart-area" class="mr-2"></font-awesome-icon>
-        <translate>Profiles</translate>
-        <font-awesome-icon
-          icon="times"
-          class="ml-auto text-muted"
-          @click="$store.commit('application/showProfiles', false)"
-        ></font-awesome-icon>
-      </h6>
-      <div
-        class="d-flex flex-column p-3 flex-grow-1 text-left position-relative"
-      >
-        <div
-          class="loading d-flex justify-content-center align-items-center"
-          v-if="surveysLoading || profileLoading"
-        >
-          <font-awesome-icon icon="spinner" spin />
-        </div>
-        <select
-          @click="moveToBottleneck"
-          v-model="selectedBottleneck"
-          class="form-control font-weight-bold"
-        >
-          <option :value="null">
-            <translate>Select Bottleneck</translate>
-          </option>
-          <option
-            v-for="bn in bottlenecks"
-            :key="bn.properties.name"
-            :value="bn.properties.name"
-            >{{ bn.properties.name }}</option
-          >
-        </select>
-        <div v-if="selectedBottleneck">
-          <div class="d-flex mt-2">
-            <div class="flex-fill">
-              <small class="text-muted">
-                <translate>Sounding Result</translate>:
-              </small>
-              <select
-                v-model="selectedSurvey"
-                class="form-control form-control-sm"
-              >
-                <option
-                  v-for="survey in surveys"
-                  :key="survey.date_info"
-                  :value="survey"
-                  >{{ formatSurveyDate(survey.date_info) }}</option
-                >
-              </select>
-            </div>
-            <div
-              class="flex-fill ml-3"
-              v-if="selectedSurvey && surveys.length > 1"
-            >
-              <small class="text-muted mt-1">
-                <translate>Compare with</translate>:
-              </small>
-              <select
-                v-model="additionalSurvey"
-                class="form-control form-control-sm"
-              >
-                <option :value="null">None</option>
-                <option
-                  v-for="survey in additionalSurveys"
-                  :key="survey.date_info"
-                  :value="survey"
-                  >{{ formatSurveyDate(survey.date_info) }}</option
-                >
-              </select>
-            </div>
-          </div>
-          <hr class="w-100 mb-0" />
-          <small class="text-muted d-block mt-2">
-            <translate>Saved cross profiles</translate>:
-          </small>
-          <div class="d-flex">
-            <select
-              :class="[
-                'form-control form-control-sm flex-fill',
-                { 'rounded-left-only': selectedCut }
-              ]"
-              v-model="selectedCut"
-            >
-              <option></option>
-              <option
-                v-for="(cut, index) in previousCuts"
-                :value="cut"
-                :key="index"
-                >{{ cut.label }}</option
-              >
-            </select>
-            <button
-              class="btn btn-sm btn-danger input-button-right"
-              @click="confirmDeleteSelectedCut = true"
-              v-if="selectedCut && !confirmDeleteSelectedCut"
-            >
-              <font-awesome-icon icon="trash" />
-            </button>
-            <button
-              class="btn btn-sm btn-info rounded-0"
-              @click="confirmDeleteSelectedCut = false"
-              v-if="selectedCut && confirmDeleteSelectedCut"
-            >
-              <font-awesome-icon icon="times" />
-            </button>
-            <button
-              class="btn btn-sm btn-danger input-button-right"
-              @click="deleteSelectedCut"
-              v-if="selectedCut && confirmDeleteSelectedCut"
-            >
-              <font-awesome-icon icon="check" />
-            </button>
-          </div>
-          <small class="text-muted d-block mt-2">
-            <translate>Enter coordinates manually</translate>:
-          </small>
-          <div class="position-relative">
-            <input
-              class="form-control form-control-sm pr-5"
-              placeholder="Lat,Lon,Lat,Lon"
-              v-model="coordinatesInput"
-            />
-            <button
-              class="btn btn-sm btn-info position-absolute input-button-right"
-              @click="applyManualCoordinates"
-              style="top: 0; right: 0;"
-              v-if="coordinatesInputIsValid"
-            >
-              <font-awesome-icon icon="check" />
-            </button>
-          </div>
-          <small class="d-flex text-left mt-2" v-if="startPoint && endPoint">
-            <div class="text-nowrap mr-3">
-              <b> <translate>Start</translate>: </b> <br />
-              Lat: {{ startPoint[1] }} <br />
-              Lon: {{ startPoint[0] }}
-            </div>
-            <div class="text-nowrap">
-              <b>End:</b> <br />
-              Lat: {{ endPoint[1] }} <br />
-              Lon: {{ endPoint[0] }}
-            </div>
-            <button
-              v-clipboard:copy="coordinatesForClipboard"
-              v-clipboard:success="onCopyCoordinates"
-              class="btn btn-info btn-sm ml-auto mt-auto"
-            >
-              <font-awesome-icon icon="copy" />
-            </button>
-          </small>
-          <div class="d-flex mt-3">
-            <div
-              class="pr-3 w-50"
-              v-if="startPoint && endPoint && !selectedCut"
-            >
-              <button
-                class="btn btn-info btn-sm w-100"
-                @click="showLabelInput = !showLabelInput"
-              >
-                <font-awesome-icon :icon="showLabelInput ? 'times' : 'check'" />
-                {{ showLabelInput ? "Cancel" : "Save" }}
-              </button>
-            </div>
-            <div
-              :class="startPoint && endPoint && !selectedCut ? 'w-50' : 'w-100'"
-            >
-              <button class="btn btn-info btn-sm w-100" @click="toggleCutTool">
-                <font-awesome-icon
-                  :icon="cutTool && cutTool.getActive() ? 'times' : 'plus'"
-                ></font-awesome-icon>
-                {{ cutTool && cutTool.getActive() ? "Cancel" : "New" }}
-              </button>
-            </div>
-          </div>
-          <div v-if="showLabelInput" class="mt-2">
-            <small class="text-muted">
-              <translate>Enter label for cross profile</translate>:
-            </small>
-            <div class="position-relative">
-              <input
-                class="form-control form-control-sm pr-5"
-                v-model="cutLabel"
-              />
-              <button
-                class="btn btn-sm btn-info position-absolute input-button-right"
-                @click="saveCut"
-                v-if="cutLabel"
-                style="top: 0; right: 0;"
-              >
-                <font-awesome-icon icon="check" />
-              </button>
-            </div>
-          </div>
-        </div>
-      </div>
-    </div>
-  </div>
-</template>
-
-<style lang="scss" scoped>
-.loading {
-  background: rgba(255, 255, 255, 0.9);
-  position: absolute;
-  z-index: 99;
-  top: 0;
-  right: 0;
-  bottom: 0;
-  left: 0;
-}
-
-.input-button-right {
-  border-top-right-radius: $border-radius;
-  border-bottom-right-radius: $border-radius;
-  border-top-left-radius: 0 !important;
-  border-bottom-left-radius: 0 !important;
-}
-
-.rounded-left-only {
-  border-top-right-radius: 0 !important;
-  border-bottom-right-radius: 0 !important;
-  border-top-left-radius: $border-radius;
-  border-bottom-left-radius: $border-radius;
-}
-</style>
-
-<script>
-/* 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):
- * Markus Kottländer <markus.kottlaender@intevation.de>
- */
-import { mapState, mapGetters } from "vuex";
-import Feature from "ol/Feature";
-import LineString from "ol/geom/LineString";
-import { displayError, displayInfo } from "../../../lib/errors.js";
-import { formatSurveyDate } from "../../../lib/date.js";
-
-export default {
-  name: "profiles",
-  data() {
-    return {
-      coordinatesInput: "",
-      cutLabel: "",
-      showLabelInput: false,
-      confirmDeleteSelectedCut: false
-    };
-  },
-  computed: {
-    ...mapGetters("map", ["getVSourceByName"]),
-    ...mapState("application", ["showProfiles"]),
-    ...mapState("map", ["lineTool", "polygonTool", "cutTool"]),
-    ...mapState("bottlenecks", ["bottlenecks", "surveys", "surveysLoading"]),
-    ...mapState("fairwayprofile", [
-      "previousCuts",
-      "startPoint",
-      "endPoint",
-      "profileLoading"
-    ]),
-    selectedBottleneck: {
-      get() {
-        return this.$store.state.bottlenecks.selectedBottleneck;
-      },
-      set(name) {
-        this.$store
-          .dispatch("bottlenecks/setSelectedBottleneck", name)
-          .then(() => {
-            this.$store.commit("bottlenecks/setFirstSurveySelected");
-          });
-      }
-    },
-    selectedSurvey: {
-      get() {
-        return this.$store.state.bottlenecks.selectedSurvey;
-      },
-      set(survey) {
-        this.$store.commit("fairwayprofile/additionalSurvey", null);
-        this.$store.commit("bottlenecks/selectedSurvey", survey);
-      }
-    },
-    additionalSurvey: {
-      get() {
-        return this.$store.state.fairwayprofile.additionalSurvey;
-      },
-      set(survey) {
-        this.$store.commit("fairwayprofile/additionalSurvey", survey);
-      }
-    },
-    selectedCut: {
-      get() {
-        return this.$store.state.fairwayprofile.selectedCut;
-      },
-      set(cut) {
-        this.$store.commit("fairwayprofile/selectedCut", cut);
-        if (!cut) {
-          this.$store.commit("fairwayprofile/clearCurrentProfile");
-          this.$store.commit("application/showSplitscreen", false);
-          this.getVSourceByName("Cut Tool").clear();
-        }
-      }
-    },
-    additionalSurveys() {
-      return this.surveys.filter(survey => survey !== this.selectedSurvey);
-    },
-    coordinatesForClipboard() {
-      return (
-        this.startPoint[1] +
-        "," +
-        this.startPoint[0] +
-        "," +
-        this.endPoint[1] +
-        "," +
-        this.endPoint[0]
-      );
-    },
-    coordinatesInputIsValid() {
-      const coordinates = this.coordinatesInput
-        .split(",")
-        .map(coord => parseFloat(coord.trim()))
-        .filter(c => Number(c) === c);
-      return coordinates.length === 4;
-    }
-  },
-  watch: {
-    selectedBottleneck() {
-      this.$store.dispatch("fairwayprofile/previousCuts");
-      this.cutLabel =
-        this.selectedBottleneck + " (" + new Date().toISOString() + ")";
-    },
-    selectedSurvey(survey) {
-      this.loadProfile(survey);
-    },
-    additionalSurvey(survey) {
-      this.loadProfile(survey);
-    },
-    selectedCut(cut) {
-      if (cut) {
-        this.confirmDeleteSelectedCut = false;
-        this.applyCoordinates(cut.coordinates);
-      }
-    }
-  },
-  methods: {
-    formatSurveyDate(date) {
-      return formatSurveyDate(date);
-    },
-    loadProfile(survey) {
-      if (survey) {
-        this.$store.commit("fairwayprofile/profileLoading", true);
-        this.$store
-          .dispatch("fairwayprofile/loadProfile", survey)
-          .finally(() =>
-            this.$store.commit("fairwayprofile/profileLoading", false)
-          );
-      }
-    },
-    toggleCutTool() {
-      this.cutTool.setActive(!this.cutTool.getActive());
-      this.lineTool.setActive(false);
-      this.polygonTool.setActive(false);
-      this.$store.commit("map/setCurrentMeasurement", null);
-    },
-    onCopyCoordinates() {
-      displayInfo({
-        title: this.$gettext("Success"),
-        message: this.$gettext("Coordinates copied to clipboard!")
-      });
-    },
-    applyManualCoordinates() {
-      const coordinates = this.coordinatesInput
-        .split(",")
-        .map(coord => parseFloat(coord.trim()));
-      this.selectedCut = null;
-      this.coordinatesInput = "";
-      this.applyCoordinates([
-        coordinates[1],
-        coordinates[0],
-        coordinates[3],
-        coordinates[2]
-      ]);
-    },
-    applyCoordinates(coordinates) {
-      // allow only numbers
-      coordinates = coordinates.filter(c => Number(c) === c);
-      if (coordinates.length === 4) {
-        // draw line on map
-        this.getVSourceByName("Cut Tool").clear();
-        const cut = new Feature({
-          geometry: new LineString([
-            [coordinates[0], coordinates[1]],
-            [coordinates[2], coordinates[3]]
-          ]).transform("EPSG:4326", "EPSG:3857")
-        });
-        this.getVSourceByName("Cut Tool").addFeature(cut);
-
-        // draw diagram
-        this.$store.dispatch("fairwayprofile/cut", cut);
-      } else {
-        displayError({
-          title: this.$gettext("Invalid input"),
-          message: this.$gettext(
-            "Please enter correct coordinates in the format: Lat,Lon,Lat,Lon"
-          )
-        });
-      }
-    },
-    saveCut() {
-      const previousCuts =
-        JSON.parse(localStorage.getItem("previousCuts")) || [];
-      const newEntry = {
-        label: this.cutLabel,
-        bottleneckName: this.selectedBottleneck,
-        coordinates: [...this.startPoint, ...this.endPoint],
-        timestamp: new Date().getTime()
-      };
-      const existingEntry = previousCuts.find(cut => {
-        return JSON.stringify(cut) === JSON.stringify(newEntry);
-      });
-      if (!existingEntry) previousCuts.push(newEntry);
-      if (previousCuts.length > 100) previousCuts.shift();
-      localStorage.setItem("previousCuts", JSON.stringify(previousCuts));
-      this.$store.dispatch("fairwayprofile/previousCuts");
-
-      this.showLabelInput = false;
-      displayInfo({
-        title: this.$gettext("Profile saved!"),
-        message: this.$gettext(
-          'You can now select these coordinates from the "Saved cross profiles" menu to restore this cross profile.'
-        )
-      });
-    },
-    deleteSelectedCut() {
-      let previousCuts = JSON.parse(localStorage.getItem("previousCuts")) || [];
-      previousCuts = previousCuts.filter(cut => {
-        return JSON.stringify(cut) !== JSON.stringify(this.selectedCut);
-      });
-      localStorage.setItem("previousCuts", JSON.stringify(previousCuts));
-      this.$store.commit("fairwayprofile/selectedCut", null);
-      this.$store.dispatch("fairwayprofile/previousCuts");
-      displayInfo({ title: this.$gettext("Profile deleted!") });
-    },
-    moveToBottleneck() {
-      const bottleneck = this.bottlenecks.find(
-        bn => bn.properties.name === this.selectedBottleneck
-      );
-      if (!bottleneck) return;
-      this.$store.commit("map/moveMap", {
-        coordinates: bottleneck.geometry.coordinates,
-        zoom: 17,
-        preventZoomOut: true
-      });
-    }
-  }
-};
-</script>
--- a/client/src/components/map/layers/Layers.vue	Tue Dec 11 22:59:10 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,64 +0,0 @@
-<template>
-  <div
-    :class="[
-      'box ui-element rounded bg-white text-nowrap',
-      { expanded: showLayers }
-    ]"
-  >
-    <div style="width: 20rem">
-      <h6 class="mb-0 py-2 px-3 border-bottom d-flex align-items-center">
-        <font-awesome-icon icon="layer-group" class="mr-2"></font-awesome-icon
-        ><translate>Layers</translate>
-        <font-awesome-icon
-          icon="times"
-          class="ml-auto text-muted"
-          @click="$store.commit('application/showLayers', false)"
-        ></font-awesome-icon>
-      </h6>
-      <div class="d-flex flex-column p-3 small">
-        <Layerselect
-          v-for="(layer, index) in layersForLegend"
-          :layerindex="index"
-          :layername="layer.name"
-          :key="layer.name"
-          :isVisible="layer.isVisible"
-          @visibilityToggled="visibilityToggled"
-        ></Layerselect>
-      </div>
-    </div>
-  </div>
-</template>
-
-<script>
-/* 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):
- * Thomas Junk <thomas.junk@intevation.de>
- * Markus Kottländer <markus.kottlaender@intevation.de>
- */
-import Layerselect from "./Layerselect";
-import { mapGetters, mapState } from "vuex";
-export default {
-  name: "layers",
-  components: {
-    Layerselect
-  },
-  computed: {
-    ...mapGetters("map", ["layersForLegend"]),
-    ...mapState("application", ["showLayers"])
-  },
-  methods: {
-    visibilityToggled(layer) {
-      this.$store.commit("map/toggleVisibility", layer);
-    }
-  }
-};
-</script>
--- a/client/src/components/map/layers/Layerselect.vue	Tue Dec 11 22:59:10 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,85 +0,0 @@
-<template>
-  <div>
-    <div class="form-check d-flex flex-row flex-start selection">
-      <input
-        class="form-check-input"
-        @change="visibilityToggled"
-        :id="layername"
-        type="checkbox"
-        :checked="isVisible"
-      />
-      <LegendElement
-        :layername="layername"
-        :layerindex="layerindex"
-      ></LegendElement>
-      <label class="layername form-check-label" @click="visibilityToggled">{{
-        layername
-      }}</label>
-    </div>
-    <div v-if="isVisible && layername == 'Bottleneck isolines'">
-      <img class="rounded my-1 d-block" :src="isolinesLegendImgUrl" />
-    </div>
-  </div>
-</template>
-
-<style lang="scss" scoped>
-.selection {
-  text-align: left;
-}
-.layername {
-  margin-left: $small-offset;
-}
-</style>
-
-<script>
-/* 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):
- * Thomas Junk <thomas.junk@intevation.de>
- */
-import { HTTP } from "../../../lib/http";
-import LegendElement from "./LegendElement.vue";
-export default {
-  props: ["layername", "layerindex", "isVisible"],
-  name: "layerselect",
-  data() {
-    return {
-      isolinesLegendImgUrl: ""
-    };
-  },
-  components: {
-    LegendElement
-  },
-  methods: {
-    visibilityToggled() {
-      this.$emit("visibilityToggled", this.layerindex);
-    }
-  },
-  created() {
-    // fetch legend image for bottleneck isolines
-    // TODO: move to store
-    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";
-      HTTP.get(src, {
-        headers: {
-          Accept: "image/png",
-          "X-Gemma-Auth": localStorage.getItem("token")
-        },
-        responseType: "blob"
-      }).then(response => {
-        var urlCreator = window.URL || window.webkitURL;
-        this.isolinesLegendImgUrl = urlCreator.createObjectURL(response.data);
-      });
-    }
-  }
-};
-</script>
--- a/client/src/components/map/layers/LegendElement.vue	Tue Dec 11 22:59:10 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,124 +0,0 @@
-<template>
-  <div :id="id" class="legendelement"></div>
-</template>
-
-<script>
-/* 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):
- * Thomas Junk <thomas.junk@intevation.de>
- */
-import { mapGetters } from "vuex";
-
-import { Map, View } from "ol";
-import Feature from "ol/Feature";
-import { Vector as VectorLayer } from "ol/layer.js";
-import { Vector as VectorSource } from "ol/source.js";
-import LineString from "ol/geom/LineString.js";
-import Point from "ol/geom/Point";
-
-export default {
-  name: "legendelement",
-  props: ["layername", "layerindex"],
-  data: function() {
-    return {
-      myMap: null,
-      mapLayer: null
-    };
-  },
-  computed: {
-    ...mapGetters("map", ["getLayerByName"]),
-    id() {
-      return "legendelement" + this.layerindex;
-    },
-    mstyle() {
-      if (this.mapLayer && this.mapLayer.data.getStyle) {
-        return this.mapLayer.data.getStyle();
-      }
-    }
-  },
-  watch: {
-    mstyle(newStyle, oldStyle) {
-      // only recreate if there already was a style before
-      if (oldStyle) {
-        let vector = this.createVectorLayer();
-
-        this.myMap.removeLayer(this.myMap.getLayers()[0]);
-        this.myMap.addLayer(vector);
-      }
-    }
-  },
-  mounted() {
-    this.mapLayer = this.getLayerByName(this.layername);
-    if (this.mapLayer.data.getType() == "VECTOR") {
-      this.initMap();
-    } else {
-      // TODO other tiles
-    }
-  },
-  methods: {
-    initMap() {
-      let vector = this.createVectorLayer();
-
-      this.myMap = new Map({
-        layers: [vector],
-        target: this.id,
-        controls: [],
-        interactions: [],
-        view: new View({
-          center: [0, 0],
-          zoom: 3,
-          projection: "EPSG:4326"
-        })
-      });
-    },
-    createVectorLayer() {
-      let mapStyle = this.mapLayer.data.getStyle();
-
-      let feature = new Feature({
-        geometry: new LineString([[-1, 0.5], [0, 0], [0.7, 0], [1.3, -0.7]])
-      });
-
-      // special case if we need to call the style function with a special
-      // parameter or to detect a point layer
-      if (this.mapLayer["forLegendStyle"]) {
-        if (this.mapLayer.forLegendStyle.point) {
-          feature.setGeometry(new Point([0, 0]));
-        }
-        mapStyle = this.mapLayer.data.getStyleFunction()(
-          feature,
-          this.mapLayer.forLegendStyle.resolution
-        );
-      }
-
-      // we could add extra properties here, if they are needed for
-      // the styling function in the future. An idea is to extend the
-      // this.mapLayer["forLegendStyle"] for it.
-      // FIXME, this is a special case for the Fairway Dimensions style
-      feature.set("level_of_service", "");
-      return new VectorLayer({
-        source: new VectorSource({
-          features: [feature],
-          wrapX: false
-        }),
-        style: mapStyle
-      });
-    }
-  }
-};
-</script>
-
-<style lang="scss" scoped>
-.legendelement {
-  max-height: 1.5rem;
-  width: 2rem;
-}
-</style>
--- a/client/src/components/map/toolbar/Identify.vue	Tue Dec 11 22:59:10 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,50 +0,0 @@
-<template>
-  <div
-    @click="$store.commit('application/showIdentify', !showIdentify)"
-    class="toolbar-button"
-  >
-    <font-awesome-icon
-      icon="info"
-      :class="{ 'text-info': showIdentify }"
-    ></font-awesome-icon>
-    <span
-      :class="[
-        'indicator',
-        {
-          show:
-            !showIdentify && (identifiedFeatures.length || currentMeasurement)
-        }
-      ]"
-    >
-      {{ badgeCount }}
-    </span>
-  </div>
-</template>
-
-<script>
-/* 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):
- * Markus Kottländer <markus.kottlaender@intevation.de>
- */
-import { mapState } from "vuex";
-
-export default {
-  name: "identify",
-  computed: {
-    ...mapState("application", ["showIdentify"]),
-    ...mapState("map", ["identifiedFeatures", "currentMeasurement"]),
-    badgeCount() {
-      return this.identifiedFeatures.length + !!this.currentMeasurement;
-    }
-  }
-};
-</script>
--- a/client/src/components/map/toolbar/Layers.vue	Tue Dec 11 22:59:10 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,35 +0,0 @@
-<template>
-  <div
-    @click="$store.commit('application/showLayers', !showLayers)"
-    class="toolbar-button"
-  >
-    <font-awesome-icon
-      icon="layer-group"
-      :class="{ 'text-info': showLayers }"
-    ></font-awesome-icon>
-  </div>
-</template>
-
-<script>
-/* 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):
- * Markus Kottländer <markus.kottlaender@intevation.de>
- */
-import { mapState } from "vuex";
-
-export default {
-  name: "layers",
-  computed: {
-    ...mapState("application", ["showLayers"])
-  }
-};
-</script>
--- a/client/src/components/map/toolbar/Linetool.vue	Tue Dec 11 22:59:10 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,42 +0,0 @@
-<template>
-  <div @click="toggleLineTool" class="toolbar-button">
-    <font-awesome-icon
-      icon="ruler"
-      :class="{ 'text-info': lineTool && lineTool.getActive() }"
-    ></font-awesome-icon>
-  </div>
-</template>
-
-<script>
-/* 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):
- * Markus Kottländer <markus.kottlaender@intevation.de>
- */
-import { mapState, mapGetters } from "vuex";
-
-export default {
-  name: "linetool",
-  computed: {
-    ...mapGetters("map", ["getLayerByName"]),
-    ...mapState("map", ["lineTool", "polygonTool", "cutTool"])
-  },
-  methods: {
-    toggleLineTool() {
-      this.lineTool.setActive(!this.lineTool.getActive());
-      this.polygonTool.setActive(false);
-      this.cutTool.setActive(false);
-      this.$store.commit("map/setCurrentMeasurement", null);
-      this.getVSourceByName("Draw Tool").clear();
-    }
-  }
-};
-</script>
--- a/client/src/components/map/toolbar/Pdftool.vue	Tue Dec 11 22:59:10 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,35 +0,0 @@
-<template>
-  <div
-    @click="$store.commit('application/showPdfTool', !showPdfTool)"
-    class="toolbar-button"
-  >
-    <font-awesome-icon
-      icon="file-pdf"
-      :class="{ 'text-info': showPdfTool }"
-    ></font-awesome-icon>
-  </div>
-</template>
-
-<script>
-/* 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):
- * Markus Kottländer <markus.kottlaender@intevation.de>
- */
-import { mapState } from "vuex";
-
-export default {
-  name: "pdftool",
-  computed: {
-    ...mapState("application", ["showPdfTool"])
-  }
-};
-</script>
--- a/client/src/components/map/toolbar/Polygontool.vue	Tue Dec 11 22:59:10 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,42 +0,0 @@
-<template>
-  <div @click="togglePolygonTool" class="toolbar-button">
-    <font-awesome-icon
-      icon="draw-polygon"
-      :class="{ 'text-info': polygonTool && polygonTool.getActive() }"
-    ></font-awesome-icon>
-  </div>
-</template>
-
-<script>
-/* 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):
- * Markus Kottländer <markus.kottlaender@intevation.de>
- */
-import { mapState, mapGetters } from "vuex";
-
-export default {
-  name: "polygontool",
-  computed: {
-    ...mapGetters("map", ["getLayerByName"]),
-    ...mapState("map", ["lineTool", "polygonTool", "cutTool"])
-  },
-  methods: {
-    togglePolygonTool() {
-      this.polygonTool.setActive(!this.polygonTool.getActive());
-      this.lineTool.setActive(false);
-      this.cutTool.setActive(false);
-      this.$store.commit("map/setCurrentMeasurement", null);
-      this.getVSourceByName("Draw Tool").clear();
-    }
-  }
-};
-</script>
--- a/client/src/components/map/toolbar/Profiles.vue	Tue Dec 11 22:59:10 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,35 +0,0 @@
-<template>
-  <div
-    @click="$store.commit('application/showProfiles', !showProfiles)"
-    class="toolbar-button"
-  >
-    <font-awesome-icon
-      icon="chart-area"
-      :class="{ 'text-info': showProfiles }"
-    ></font-awesome-icon>
-  </div>
-</template>
-
-<script>
-/* 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):
- * Markus Kottländer <markus.kottlaender@intevation.de>
- */
-import { mapState } from "vuex";
-
-export default {
-  name: "profiles",
-  computed: {
-    ...mapState("application", ["showProfiles"])
-  }
-};
-</script>
--- a/client/src/components/map/toolbar/Toolbar.vue	Tue Dec 11 22:59:10 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,142 +0,0 @@
-<template>
-  <div class="ml-2">
-    <div
-      :class="
-        'rounded-top toolbar toolbar-' +
-          (expandToolbar ? 'expanded' : 'collapsed')
-      "
-    >
-      <Identify></Identify>
-      <Layers></Layers>
-      <Profiles></Profiles>
-      <Linetool></Linetool>
-      <Polygontool></Polygontool>
-      <Pdftool></Pdftool>
-    </div>
-    <div
-      @click="$store.commit('application/expandToolbar', !expandToolbar)"
-      class="toolbar-button toolbar-toggle rounded-bottom bg-info text-white"
-    >
-      <font-awesome-icon
-        :icon="expandToolbar ? 'angle-up' : 'angle-down'"
-      ></font-awesome-icon>
-    </div>
-  </div>
-</template>
-
-<style lang="scss">
-// not scoped to affect nested components
-// doen't work when put in application/assets/application.sass... why??? o_O
-.toolbar {
-  box-shadow: 0 0.1rem 0.5rem rgba(0, 0, 0, 0.2);
-  overflow: hidden;
-  transition: max-height 0.4s;
-  margin-bottom: auto;
-}
-
-.toolbar-collapsed {
-  max-height: 6rem;
-}
-
-.toolbar-expanded {
-  max-height: 100%;
-}
-
-.toolbar-button {
-  opacity: 0.96;
-  color: #666;
-  height: 2rem;
-  width: 2rem;
-  align-items: center;
-  justify-content: center;
-  display: flex;
-  background: #fff;
-  border-bottom: 1px solid #dee2e6;
-  z-index: 2;
-  pointer-events: auto;
-  position: relative;
-  overflow: hidden;
-}
-
-.toolbar-button:last-child {
-  border-bottom: none;
-}
-
-.toolbar-button .inverted {
-  color: #17a2b8;
-}
-
-.toolbar-button .grey {
-  color: #ddd;
-}
-
-.toolbar-button .indicator {
-  color: #fff;
-  background: #17a2b8;
-  position: absolute;
-  bottom: -14px;
-  left: -14px;
-  padding: 2px 4px 1px;
-  font-size: 11px;
-  line-height: 11px;
-  border-top-right-radius: 0.25rem;
-  transition: bottom 0.3s, left 0.3s;
-}
-
-.toolbar-button .indicator.show {
-  left: 0;
-  bottom: 0;
-}
-
-.toolbar-toggle {
-  height: 1.2rem;
-  border-bottom: none;
-}
-</style>
-
-<script>
-/* 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):
- * Markus Kottländer <markus.kottlaender@intevation.de>
- */
-import { mapState, mapGetters } from "vuex";
-
-export default {
-  name: "toolbar",
-  components: {
-    Identify: () => import("./Identify.vue"),
-    Layers: () => import("./Layers.vue"),
-    Linetool: () => import("./Linetool.vue"),
-    Polygontool: () => import("./Polygontool.vue"),
-    Profiles: () => import("./Profiles.vue"),
-    Pdftool: () => import("./Pdftool.vue")
-  },
-  computed: {
-    ...mapGetters("map", ["getVSourceByName"]),
-    ...mapState("map", ["lineTool", "polygonTool", "cutTool"]),
-    ...mapState("application", ["expandToolbar"])
-  },
-  mounted() {
-    window.addEventListener("keydown", e => {
-      // Escape
-      if (e.keyCode === 27) {
-        this.lineTool.setActive(false);
-        this.polygonTool.setActive(false);
-        this.cutTool.setActive(false);
-        this.$store.commit("map/setCurrentMeasurement", null);
-        this.$store.dispatch("map/enableIdentifyTool");
-        this.getVSourceByName("Draw Tool").clear();
-      }
-    });
-  }
-};
-</script>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/client/src/components/toolbar/Identify.vue	Wed Dec 12 09:22:20 2018 +0100
@@ -0,0 +1,50 @@
+<template>
+  <div
+    @click="$store.commit('application/showIdentify', !showIdentify)"
+    class="toolbar-button"
+  >
+    <font-awesome-icon
+      icon="info"
+      :class="{ 'text-info': showIdentify }"
+    ></font-awesome-icon>
+    <span
+      :class="[
+        'indicator',
+        {
+          show:
+            !showIdentify && (identifiedFeatures.length || currentMeasurement)
+        }
+      ]"
+    >
+      {{ badgeCount }}
+    </span>
+  </div>
+</template>
+
+<script>
+/* 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):
+ * Markus Kottländer <markus.kottlaender@intevation.de>
+ */
+import { mapState } from "vuex";
+
+export default {
+  name: "identify",
+  computed: {
+    ...mapState("application", ["showIdentify"]),
+    ...mapState("map", ["identifiedFeatures", "currentMeasurement"]),
+    badgeCount() {
+      return this.identifiedFeatures.length + !!this.currentMeasurement;
+    }
+  }
+};
+</script>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/client/src/components/toolbar/Layers.vue	Wed Dec 12 09:22:20 2018 +0100
@@ -0,0 +1,35 @@
+<template>
+  <div
+    @click="$store.commit('application/showLayers', !showLayers)"
+    class="toolbar-button"
+  >
+    <font-awesome-icon
+      icon="layer-group"
+      :class="{ 'text-info': showLayers }"
+    ></font-awesome-icon>
+  </div>
+</template>
+
+<script>
+/* 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):
+ * Markus Kottländer <markus.kottlaender@intevation.de>
+ */
+import { mapState } from "vuex";
+
+export default {
+  name: "layers",
+  computed: {
+    ...mapState("application", ["showLayers"])
+  }
+};
+</script>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/client/src/components/toolbar/Linetool.vue	Wed Dec 12 09:22:20 2018 +0100
@@ -0,0 +1,42 @@
+<template>
+  <div @click="toggleLineTool" class="toolbar-button">
+    <font-awesome-icon
+      icon="ruler"
+      :class="{ 'text-info': lineTool && lineTool.getActive() }"
+    ></font-awesome-icon>
+  </div>
+</template>
+
+<script>
+/* 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):
+ * Markus Kottländer <markus.kottlaender@intevation.de>
+ */
+import { mapState, mapGetters } from "vuex";
+
+export default {
+  name: "linetool",
+  computed: {
+    ...mapGetters("map", ["getLayerByName"]),
+    ...mapState("map", ["lineTool", "polygonTool", "cutTool"])
+  },
+  methods: {
+    toggleLineTool() {
+      this.lineTool.setActive(!this.lineTool.getActive());
+      this.polygonTool.setActive(false);
+      this.cutTool.setActive(false);
+      this.$store.commit("map/setCurrentMeasurement", null);
+      this.getVSourceByName("Draw Tool").clear();
+    }
+  }
+};
+</script>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/client/src/components/toolbar/Pdftool.vue	Wed Dec 12 09:22:20 2018 +0100
@@ -0,0 +1,35 @@
+<template>
+  <div
+    @click="$store.commit('application/showPdfTool', !showPdfTool)"
+    class="toolbar-button"
+  >
+    <font-awesome-icon
+      icon="file-pdf"
+      :class="{ 'text-info': showPdfTool }"
+    ></font-awesome-icon>
+  </div>
+</template>
+
+<script>
+/* 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):
+ * Markus Kottländer <markus.kottlaender@intevation.de>
+ */
+import { mapState } from "vuex";
+
+export default {
+  name: "pdftool",
+  computed: {
+    ...mapState("application", ["showPdfTool"])
+  }
+};
+</script>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/client/src/components/toolbar/Polygontool.vue	Wed Dec 12 09:22:20 2018 +0100
@@ -0,0 +1,42 @@
+<template>
+  <div @click="togglePolygonTool" class="toolbar-button">
+    <font-awesome-icon
+      icon="draw-polygon"
+      :class="{ 'text-info': polygonTool && polygonTool.getActive() }"
+    ></font-awesome-icon>
+  </div>
+</template>
+
+<script>
+/* 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):
+ * Markus Kottländer <markus.kottlaender@intevation.de>
+ */
+import { mapState, mapGetters } from "vuex";
+
+export default {
+  name: "polygontool",
+  computed: {
+    ...mapGetters("map", ["getLayerByName"]),
+    ...mapState("map", ["lineTool", "polygonTool", "cutTool"])
+  },
+  methods: {
+    togglePolygonTool() {
+      this.polygonTool.setActive(!this.polygonTool.getActive());
+      this.lineTool.setActive(false);
+      this.cutTool.setActive(false);
+      this.$store.commit("map/setCurrentMeasurement", null);
+      this.getVSourceByName("Draw Tool").clear();
+    }
+  }
+};
+</script>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/client/src/components/toolbar/Profiles.vue	Wed Dec 12 09:22:20 2018 +0100
@@ -0,0 +1,35 @@
+<template>
+  <div
+    @click="$store.commit('application/showProfiles', !showProfiles)"
+    class="toolbar-button"
+  >
+    <font-awesome-icon
+      icon="chart-area"
+      :class="{ 'text-info': showProfiles }"
+    ></font-awesome-icon>
+  </div>
+</template>
+
+<script>
+/* 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):
+ * Markus Kottländer <markus.kottlaender@intevation.de>
+ */
+import { mapState } from "vuex";
+
+export default {
+  name: "profiles",
+  computed: {
+    ...mapState("application", ["showProfiles"])
+  }
+};
+</script>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/client/src/components/toolbar/Toolbar.vue	Wed Dec 12 09:22:20 2018 +0100
@@ -0,0 +1,142 @@
+<template>
+  <div class="ml-2">
+    <div
+      :class="
+        'rounded-top toolbar toolbar-' +
+          (expandToolbar ? 'expanded' : 'collapsed')
+      "
+    >
+      <Identify></Identify>
+      <Layers></Layers>
+      <Profiles></Profiles>
+      <Linetool></Linetool>
+      <Polygontool></Polygontool>
+      <Pdftool></Pdftool>
+    </div>
+    <div
+      @click="$store.commit('application/expandToolbar', !expandToolbar)"
+      class="toolbar-button toolbar-toggle rounded-bottom bg-info text-white"
+    >
+      <font-awesome-icon
+        :icon="expandToolbar ? 'angle-up' : 'angle-down'"
+      ></font-awesome-icon>
+    </div>
+  </div>
+</template>
+
+<style lang="scss">
+// not scoped to affect nested components
+// doen't work when put in application/assets/application.sass... why??? o_O
+.toolbar {
+  box-shadow: 0 0.1rem 0.5rem rgba(0, 0, 0, 0.2);
+  overflow: hidden;
+  transition: max-height 0.4s;
+  margin-bottom: auto;
+}
+
+.toolbar-collapsed {
+  max-height: 6rem;
+}
+
+.toolbar-expanded {
+  max-height: 100%;
+}
+
+.toolbar-button {
+  opacity: 0.96;
+  color: #666;
+  height: 2rem;
+  width: 2rem;
+  align-items: center;
+  justify-content: center;
+  display: flex;
+  background: #fff;
+  border-bottom: 1px solid #dee2e6;
+  z-index: 2;
+  pointer-events: auto;
+  position: relative;
+  overflow: hidden;
+}
+
+.toolbar-button:last-child {
+  border-bottom: none;
+}
+
+.toolbar-button .inverted {
+  color: #17a2b8;
+}
+
+.toolbar-button .grey {
+  color: #ddd;
+}
+
+.toolbar-button .indicator {
+  color: #fff;
+  background: #17a2b8;
+  position: absolute;
+  bottom: -14px;
+  left: -14px;
+  padding: 2px 4px 1px;
+  font-size: 11px;
+  line-height: 11px;
+  border-top-right-radius: 0.25rem;
+  transition: bottom 0.3s, left 0.3s;
+}
+
+.toolbar-button .indicator.show {
+  left: 0;
+  bottom: 0;
+}
+
+.toolbar-toggle {
+  height: 1.2rem;
+  border-bottom: none;
+}
+</style>
+
+<script>
+/* 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):
+ * Markus Kottländer <markus.kottlaender@intevation.de>
+ */
+import { mapState, mapGetters } from "vuex";
+
+export default {
+  name: "toolbar",
+  components: {
+    Identify: () => import("./Identify.vue"),
+    Layers: () => import("./Layers.vue"),
+    Linetool: () => import("./Linetool.vue"),
+    Polygontool: () => import("./Polygontool.vue"),
+    Profiles: () => import("./Profiles.vue"),
+    Pdftool: () => import("./Pdftool.vue")
+  },
+  computed: {
+    ...mapGetters("map", ["getVSourceByName"]),
+    ...mapState("map", ["lineTool", "polygonTool", "cutTool"]),
+    ...mapState("application", ["expandToolbar"])
+  },
+  mounted() {
+    window.addEventListener("keydown", e => {
+      // Escape
+      if (e.keyCode === 27) {
+        this.lineTool.setActive(false);
+        this.polygonTool.setActive(false);
+        this.cutTool.setActive(false);
+        this.$store.commit("map/setCurrentMeasurement", null);
+        this.$store.dispatch("map/enableIdentifyTool");
+        this.getVSourceByName("Draw Tool").clear();
+      }
+    });
+  }
+};
+</script>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/client/src/components/usermanagement/Passwordfield.vue	Wed Dec 12 09:22:20 2018 +0100
@@ -0,0 +1,73 @@
+<template>
+  <div class="w-100">
+    <div class="d-flex flex-row">
+      <label for="password">{{ this.label }}</label>
+    </div>
+    <div class="d-flex d-row">
+      <input
+        :type="isPasswordVisible"
+        @change="fieldChanged"
+        class="form-control"
+        :placeholder="placeholder"
+        :required="required"
+      />
+      <span class="input-group-text" @click="showPassword">
+        <font-awesome-icon
+          :icon="readablePassword ? 'eye-slash' : 'eye'"
+        ></font-awesome-icon>
+      </span>
+    </div>
+    <div v-show="passworderrors" class="text-danger">
+      <small>
+        <font-awesome-icon icon="exclamation-triangle"></font-awesome-icon>
+        {{ this.passworderrors }}
+      </small>
+    </div>
+  </div>
+</template>
+
+<style>
+/* FIXME does not work here, unclear why, so added to Login.vue
+input[type="password"]::-ms-reveal {
+  display: none;
+} */
+</style>
+
+<script>
+/* 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):
+ * Thomas Junk <thomas.junk@intevation.de>
+ */
+export default {
+  name: "passwordfield",
+  props: ["model", "placeholder", "label", "passworderrors", "required"],
+  data() {
+    return {
+      password: "",
+      readablePassword: false
+    };
+  },
+  methods: {
+    showPassword() {
+      this.readablePassword = !this.readablePassword;
+    },
+    fieldChanged(e) {
+      this.$emit("fieldchange", e.target.value);
+    }
+  },
+  computed: {
+    isPasswordVisible() {
+      return this.readablePassword ? "text" : "password";
+    }
+  }
+};
+</script>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/client/src/components/usermanagement/Userdetail.vue	Wed Dec 12 09:22:20 2018 +0100
@@ -0,0 +1,393 @@
+<template>
+  <div class="userdetails mt-3 shadow fadeIn animated card">
+    <h6
+      class="mb-0 py-2 px-3 border-bottom d-flex text-info align-items-center"
+    >
+      {{ this.cardHeader }}
+      <span @click="closeDetailview" class="closebutton">
+        <font-awesome-icon icon="times"></font-awesome-icon>
+      </span>
+    </h6>
+    <div class="card-body">
+      <form @submit.prevent="save" class="ml-3">
+        <div class="formfields">
+          <div v-if="currentUser.isNew" class="form-group row">
+            <label for="user"> <translate>Username</translate> </label>
+            <input
+              type="user"
+              :placeholder="userNamePlaceholder"
+              class="form-control form-control-sm"
+              id="user"
+              aria-describedby="userHelp"
+              v-model="currentUser.user"
+            />
+            <div v-show="errors.user" class="text-danger">
+              <small>
+                <font-awesome-icon
+                  icon="exclamation-triangle"
+                ></font-awesome-icon>
+                {{ errors.user }}
+              </small>
+            </div>
+          </div>
+          <div class="form-group row">
+            <label for="country"> <translate>Country</translate> </label>
+            <select
+              class="form-control form-control-sm"
+              v-on:change="validateCountry"
+              v-model="currentUser.country"
+            >
+              <option disabled value>
+                <translate>Please select one</translate>
+              </option>
+              <option
+                v-for="country in countries"
+                v-bind:value="country"
+                v-bind:key="country"
+                >{{ country }}</option
+              >
+            </select>
+            <div v-show="errors.country" class="text-danger">
+              <small>
+                <font-awesome-icon
+                  icon="exclamation-triangle"
+                ></font-awesome-icon>
+                {{ errors.country }}
+              </small>
+            </div>
+          </div>
+          <div class="form-group row">
+            <label for="email"> <translate>Email address</translate> </label>
+            <input
+              type="email"
+              v-on:change="validateEmailaddress"
+              class="form-control form-control-sm"
+              id="email"
+              aria-describedby="emailHelp"
+              v-model="currentUser.email"
+            />
+            <div v-show="errors.email" class="text-danger">
+              <small>
+                <font-awesome-icon
+                  icon="exclamation-triangle"
+                ></font-awesome-icon>
+                {{ errors.email }}
+              </small>
+            </div>
+          </div>
+          <div class="form-group row">
+            <label for="role"> <translate>Role</translate> </label>
+            <select
+              class="form-control form-control-sm"
+              v-on:change="validateRole"
+              v-model="currentUser.role"
+            >
+              <option disabled value>
+                <translate>Please select one</translate>
+              </option>
+              <option value="sys_admin">
+                <translate>Sysadmin</translate>
+              </option>
+              <option value="waterway_admin">
+                <translate>Waterway Admin</translate>
+              </option>
+              <option value="waterway_user">
+                <translate>Waterway User</translate>
+              </option>
+            </select>
+            <div v-show="errors.role" class="text-danger">
+              <small>
+                <font-awesome-icon
+                  icon="exclamation-triangle"
+                ></font-awesome-icon>
+                {{ errors.role }}
+              </small>
+            </div>
+          </div>
+          <div class="form-group row">
+            <PasswordField
+              @fieldchange="passwordChanged"
+              :placeholder="passwordPlaceholder"
+              :label="passwordLabel"
+              :passworderrors="errors.password"
+            ></PasswordField>
+          </div>
+          <div class="form-group row">
+            <PasswordField
+              @fieldchange="passwordReChanged"
+              :placeholder="passwordRePlaceholder"
+              :label="passwordReLabel"
+              :passworderrors="errors.passwordre"
+            ></PasswordField>
+          </div>
+        </div>
+        <div>
+          <button
+            type="submit"
+            :disabled="submitted"
+            class="shadow-sm btn btn-info submit-button"
+          >
+            <translate>Submit</translate>
+          </button>
+        </div>
+        <div
+          v-if="currentUser.role != 'waterway_user'"
+          class="form-group row d-flex flex-row justify-content-start mailbutton"
+        >
+          <a @click="sendTestMail" class="btn btn-light">
+            <font-awesome-icon icon="paper-plane"></font-awesome-icon>
+            <translate>Send testmail</translate>
+          </a>
+          <div v-if="mailsent"><translate>Mail was sent</translate></div>
+        </div>
+      </form>
+    </div>
+  </div>
+</template>
+
+<style lang="scss" scoped>
+.submit-button {
+  position: absolute;
+  right: $offset;
+  bottom: $offset;
+}
+.mailbutton {
+  width: 12vw;
+  position: absolute;
+  left: $large-offset;
+  bottom: 0;
+}
+
+.formfields {
+  width: 60%;
+}
+
+.userdetails {
+  height: 600px;
+  margin-top: $offset;
+  margin-left: $offset;
+  margin-right: $offset;
+}
+
+form {
+  font-size: $smaller;
+}
+</style>
+
+<script>
+/* 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):
+ * Thomas Junk <thomas.junk@intevation.de>
+ */
+import { HTTP } from "../../lib/http";
+import { displayError } from "../../lib/errors.js";
+import { mapState } from "vuex";
+import PasswordField from "./Passwordfield";
+
+const emptyErrormessages = () => {
+  return {
+    email: "",
+    country: "",
+    role: "",
+    password: "",
+    passwordre: ""
+  };
+};
+
+const isEmailValid = email => {
+  /**
+   *
+   * For convenience purposes the same regex used as in the go code
+   * cf. types.go
+   *
+   */
+  // eslint-disable-next-line
+  return /(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\[(?:(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9]))\.){3}(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9])|[a-z0-9-]*[a-z0-9]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)\])/.test(
+    email
+  );
+};
+
+const violatedPasswordRules = password => {
+  return (
+    // rules according to issue 70
+    password.length < 7 ||
+    /\W/.test(password) == false ||
+    /\d/.test(password) == false
+  );
+};
+
+export default {
+  name: "userdetail",
+  components: {
+    PasswordField
+  },
+  data() {
+    return {
+      mailsent: false,
+      passwordLabel: this.$gettext("Password"),
+      passwordReLabel: this.$gettext("Repeat Password"),
+      passwordPlaceholder: this.$gettext("password"),
+      passwordRePlaceholder: this.$gettext("password again"),
+      password: "",
+      passwordre: "",
+      currentUser: {},
+      path: null,
+      submitted: false,
+      errors: {
+        email: "",
+        country: "",
+        role: "",
+        password: "",
+        passwordre: ""
+      }
+    };
+  },
+  mounted() {
+    this.currentUser = { ...this.user };
+    this.path = this.user.name;
+  },
+  watch: {
+    user() {
+      this.currentUser = { ...this.user };
+      this.path = this.user.name;
+      this.clearPassword();
+      this.clearErrors();
+    }
+  },
+  computed: {
+    cardHeader() {
+      if (this.currentUser.isNew) return "N.N";
+      return this.currentUser.user;
+    },
+    userNamePlaceholder() {
+      if (this.currentUser.isNew) return "N.N";
+      return "";
+    },
+    ...mapState("application", ["countries"]),
+    user() {
+      return this.$store.getters["usermanagement/currentUser"];
+    },
+    isFormValid() {
+      return (
+        isEmailValid(this.currentUser.email) &&
+        this.currentUser.country &&
+        this.password === this.passwordre &&
+        (this.password === "" || !violatedPasswordRules(this.password))
+      );
+    }
+  },
+  methods: {
+    sendTestMail() {
+      if (this.mailsent) return;
+      HTTP.get("/testmail/" + this.currentUser.user, {
+        headers: {
+          "X-Gemma-Auth": localStorage.getItem("token"),
+          "Content-type": "text/xml; charset=UTF-8"
+        }
+      })
+        .then(() => {
+          this.mailsent = true;
+        })
+        .catch(error => {
+          this.loginFailed = true;
+          this.submitted = false;
+          const { status, data } = error.response;
+          displayError({
+            title: this.$gettext("Backend Error"),
+            message: `${status}: ${data.message || data}`
+          });
+        });
+    },
+    passwordChanged(value) {
+      this.password = value;
+      this.validatePassword();
+    },
+    passwordReChanged(value) {
+      this.passwordre = value;
+      this.validatePassword();
+    },
+    clearErrors() {
+      this.errors = emptyErrormessages();
+    },
+    clearPassword() {
+      this.password = "";
+      this.passwordre = "";
+    },
+    closeDetailview() {
+      this.$store.commit("usermanagement/clearCurrentUser");
+      this.$store.commit("usermanagement/setUserDetailsInvisible");
+    },
+    validateCountry() {
+      this.errors.country = this.currentUser.country
+        ? ""
+        : this.$gettext("Please choose a country");
+    },
+    validateRole() {
+      this.errors.role = this.currentUser.role
+        ? ""
+        : this.$gettext("Please choose a role");
+    },
+    validatePassword() {
+      this.errors.passwordre =
+        this.password === this.passwordre
+          ? ""
+          : this.$gettext("Passwords do not match!");
+      this.errors.password =
+        this.password === "" || !violatedPasswordRules(this.password)
+          ? ""
+          : this.$gettext(
+              "Password should at least be 8 char long including 1 digit and 1 special char like $"
+            );
+    },
+    validateEmailaddress() {
+      this.errors.email = isEmailValid(this.currentUser.email)
+        ? ""
+        : this.$gettext("invalid email");
+    },
+    validate() {
+      this.validateCountry();
+      this.validateRole();
+      this.validatePassword();
+      this.validateEmailaddress();
+    },
+    save() {
+      this.validate();
+      if (!this.isFormValid) return;
+      if (this.password) this.currentUser.password = this.password;
+      this.submitted = true;
+      this.$store
+        .dispatch("usermanagement/saveCurrentUser", {
+          path: this.user.user,
+          user: this.currentUser
+        })
+        .then(() => {
+          this.submitted = false;
+          this.$store.dispatch("usermanagement/loadUsers").catch(error => {
+            const { status, data } = error.response;
+            displayError({
+              title: this.$gettext("Backend Error"),
+              message: `${status}: ${data.message || data}`
+            });
+          });
+        })
+        .catch(error => {
+          this.submitted = false;
+          const { status, data } = error.response;
+          displayError({
+            title: this.$gettext("Error while saving user"),
+            message: `${status}: ${data.message || data}`
+          });
+        });
+    }
+  }
+};
+</script>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/client/src/components/usermanagement/Usermanagement.vue	Wed Dec 12 09:22:20 2018 +0100
@@ -0,0 +1,358 @@
+<template>
+  <div class="main d-flex flex-row">
+    <div :class="spacerStyle"></div>
+    <div class="d-flex content flex-column">
+      <div class="d-flex flex-row">
+        <div :class="userlistStyle">
+          <div class="card">
+            <h6
+              class="mb-0 py-2 px-3 border-bottom d-flex text-info align-items-center"
+            >
+              <font-awesome-icon
+                icon="users-cog"
+                class="mr-2 fa-fw"
+              ></font-awesome-icon>
+              <translate class="headline">Users</translate>
+            </h6>
+            <div class="card-body">
+              <table id="datatable" :class="tableStyle">
+                <thead>
+                  <tr>
+                    <th scope="col" @click="sortBy('user')">
+                      <span
+                        >Username&nbsp;
+                        <font-awesome-icon
+                          v-if="sortCriterion == 'user'"
+                          icon="angle-down"
+                        ></font-awesome-icon>
+                      </span>
+                    </th>
+                    <th scope="col" @click="sortBy('country')">
+                      <span
+                        >Country&nbsp;
+                        <font-awesome-icon
+                          v-if="sortCriterion == 'country'"
+                          icon="angle-down"
+                        ></font-awesome-icon>
+                      </span>
+                    </th>
+                    <th scope="col" @click="sortBy('email')">
+                      <span
+                        >Email&nbsp;
+                        <font-awesome-icon
+                          v-if="sortCriterion == 'email'"
+                          icon="angle-down"
+                        ></font-awesome-icon>
+                      </span>
+                    </th>
+                    <th scope="col" @click="sortBy('role')">
+                      <span
+                        >Role&nbsp;
+                        <font-awesome-icon
+                          v-if="sortCriterion == 'role'"
+                          icon="angle-down"
+                        ></font-awesome-icon>
+                      </span>
+                    </th>
+                    <th scope="col"></th>
+                  </tr>
+                </thead>
+                <tbody>
+                  <tr
+                    v-for="user in users"
+                    :key="user.user"
+                    @click="selectUser(user.user)"
+                  >
+                    <td>{{ user.user }}</td>
+                    <td>{{ user.country }}</td>
+                    <td>{{ user.email }}</td>
+                    <td>
+                      <font-awesome-icon
+                        :icon="roleIcon(user.role)"
+                        @click="deleteUser(user.user)"
+                      ></font-awesome-icon>
+                    </td>
+                    <td>
+                      <font-awesome-icon
+                        icon="trash"
+                        @click="deleteUser(user.user)"
+                      ></font-awesome-icon>
+                    </td>
+                  </tr>
+                </tbody>
+              </table>
+            </div>
+            <div class="d-flex mx-auto align-items-center">
+              <button
+                @click="prevPage"
+                v-if="this.currentPage !== 1"
+                class="mr-2 btn btn-sm btn-light align-self-center"
+              >
+                <font-awesome-icon icon="angle-left"></font-awesome-icon>
+              </button>
+              {{ this.currentPage }} / {{ this.pages }}
+              <button
+                @click="nextPage"
+                v-if="this.currentPage !== this.pages"
+                class="ml-2 btn btn-sm btn-light align-self-center"
+              >
+                <font-awesome-icon icon="angle-right"></font-awesome-icon>
+              </button>
+            </div>
+            <div class="mr-3 pb-3">
+              <button @click="addUser" class="btn btn-info addbutton shadow-sm">
+                <translate>Add User</translate>
+              </button>
+            </div>
+          </div>
+        </div>
+        <Userdetail
+          class="d-flex userdetails"
+          v-if="isUserDetailsVisible"
+        ></Userdetail>
+      </div>
+    </div>
+  </div>
+</template>
+
+<style scoped lang="scss">
+@import "../../assets/tooltip.scss";
+
+.addbutton {
+  position: absolute;
+  bottom: $offset;
+  right: $offset;
+}
+
+.content {
+  width: 100%;
+}
+
+.userdetails {
+  width: 50%;
+}
+.spacer {
+  height: 100vh;
+  margin-left: $offset;
+}
+
+.spacer-collapsed {
+  min-width: $icon-width + $offset;
+  transition: $transition-fast;
+}
+
+.spacer-expanded {
+  min-width: $sidebar-width + $offset;
+}
+
+.main {
+  height: 100vh;
+}
+
+.icon {
+  font-size: large;
+}
+
+.userlist {
+  min-width: 520px;
+  height: 100%;
+}
+
+.userlistsmall {
+  width: 100%;
+}
+
+.userlistextended {
+  width: 100%;
+}
+
+.table {
+  width: 90% !important;
+  margin: auto;
+}
+
+.table th {
+  cursor: pointer;
+}
+
+.table th,
+td {
+  font-size: $smaller;
+  border-top: 0px !important;
+  text-align: left;
+  padding: $small-offset !important;
+}
+
+.table td {
+  font-size: $smaller;
+  cursor: pointer;
+}
+
+tr span {
+  display: flex;
+}
+</style>
+
+<script>
+/* 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):
+ * Thomas Junk <thomas.junk@intevation.de>
+ */
+import Userdetail from "./Userdetail";
+import store from "../../store";
+import { mapGetters, mapState } from "vuex";
+import { displayError } from "../../lib/errors.js";
+
+export default {
+  name: "userview",
+  data() {
+    return {
+      sortCriterion: "user",
+      pageSize: 10,
+      currentPage: 1
+    };
+  },
+  components: {
+    Userdetail
+  },
+  computed: {
+    ...mapGetters("usermanagement", ["isUserDetailsVisible"]),
+    ...mapState("application", ["showSidebar"]),
+    spacerStyle() {
+      return [
+        "spacer",
+        {
+          "spacer-expanded": this.showSidebar,
+          "spacer-collapsed": !this.showSidebar
+        }
+      ];
+    },
+    users() {
+      let users = [...this.$store.getters["usermanagement/users"]];
+      users.sort((a, b) => {
+        if (
+          a[this.sortCriterion].toLowerCase() <
+          b[this.sortCriterion].toLowerCase()
+        )
+          return -1;
+        if (
+          a[this.sortCriterion].toLowerCase() >
+          b[this.sortCriterion].toLowerCase()
+        )
+          return 1;
+        return 0;
+      });
+      const start = (this.currentPage - 1) * this.pageSize;
+      return users.slice(start, start + this.pageSize);
+    },
+    pages() {
+      let users = [...this.$store.getters["usermanagement/users"]];
+      return Math.ceil(users.length / this.pageSize);
+    },
+    tableStyle() {
+      return {
+        table: true,
+        "table-hover": true,
+        "table-sm": this.isUserDetailsVisible,
+        fadeIn: true,
+        animated: true
+      };
+    },
+    userlistStyle() {
+      return [
+        "userlist mt-3 mr-3 shadow-xs",
+        {
+          userlistsmall: this.isUserDetailsVisible,
+          userlistextended: !this.isUserDetailsVisible
+        }
+      ];
+    }
+  },
+  methods: {
+    tween() {},
+    nextPage() {
+      if (this.currentPage < this.pages) {
+        document.querySelector("#datatable").classList.add("fadeOut");
+        setTimeout(() => {
+          document.querySelector("#datatable").classList.remove("fadeOut");
+          this.currentPage += 1;
+        }, 10);
+      }
+      return;
+    },
+    prevPage() {
+      if (this.currentPage > 0) {
+        document.querySelector("#datatable").classList.add("fadeOut");
+        setTimeout(() => {
+          document.querySelector("#datatable").classList.remove("fadeOut");
+          this.currentPage -= 1;
+        }, 10);
+      }
+      return;
+    },
+    sortBy(criterion) {
+      this.sortCriterion = criterion;
+    },
+    deleteUser(name) {
+      this.$store
+        .dispatch("usermanagement/deleteUser", { name: name })
+        .then(() => {
+          this.submitted = false;
+          this.$store.dispatch("usermanagement/loadUsers").catch(error => {
+            const { status, data } = error.response;
+            displayError({
+              title: this.$gettext("Backend Error"),
+              message: `${status}: ${data.message || data}`
+            });
+          });
+        })
+        .catch(error => {
+          const { status, data } = error.response;
+          displayError({
+            title: this.$gettext("Backend Error"),
+            message: `${status}: ${data.message || data}`
+          });
+        });
+    },
+    addUser() {
+      this.$store.commit("usermanagement/clearCurrentUser");
+      this.$store.commit("usermanagement/setUserDetailsVisible");
+    },
+    selectUser(name) {
+      const user = this.$store.getters["usermanagement/getUserByName"](name);
+      this.$store.commit("usermanagement/setCurrentUser", user);
+    },
+    roleIcon(role) {
+      if (role === "sys_admin") return "star";
+      if (role === "waterway_admin") return ["fab", "adn"];
+      return "user";
+    }
+  },
+  beforeRouteEnter(to, from, next) {
+    store
+      .dispatch("usermanagement/loadUsers")
+      .then(next)
+      .catch(error => {
+        const { status, data } = error.response;
+        displayError({
+          title: this.$gettext("Backend Error"),
+          message: `${status}: ${data}`
+        });
+      });
+  },
+  beforeRouteLeave(to, from, next) {
+    store.commit("usermanagement/clearCurrentUser");
+    store.commit("usermanagement/setUserDetailsInvisible");
+    next();
+  }
+};
+</script>
--- a/client/src/router.js	Tue Dec 11 22:59:10 2018 +0100
+++ b/client/src/router.js	Wed Dec 12 09:22:20 2018 +0100
@@ -20,15 +20,15 @@
 
 /*  facilitate codesplitting */
 const Login = () => import("./components/Login.vue");
-const Main = () => import("./components/map/Main.vue");
+const Main = () => import("./components/Main.vue");
 const Usermanagement = () =>
-  import("./components/admin/usermanagement/Usermanagement.vue");
-const Logs = () => import("./components/admin/Logs.vue");
-const Importqueue = () => import("./components/admin/Importqueue.vue");
+  import("./components/usermanagement/Usermanagement.vue");
+const Logs = () => import("./components/Logs.vue");
+const Importqueue = () => import("./components/Importqueue.vue");
 const Importschedule = () =>
-  import("./components/admin/importschedule/Importschedule.vue");
+  import("./components/importschedule/Importschedule.vue");
 const Systemconfiguration = () =>
-  import("./components/admin/Systemconfiguration.vue");
+  import("./components/Systemconfiguration.vue");
 
 Vue.use(Router);