changeset 1273:a76a06dd2f1e

merge
author Markus Kottlaender <markus@intevation.de>
date Thu, 22 Nov 2018 07:08:38 +0100
parents bc55ffaeb639 (diff) 717c58fea5c1 (current diff)
children dc3fb8ad8f86
files
diffstat 100 files changed, 5508 insertions(+), 5510 deletions(-) [+]
line wrap: on
line diff
--- a/client/src/App.vue	Wed Nov 21 16:47:02 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,96 +0,0 @@
-<template>
-    <div id="app" class="main">
-        <div v-if="isAuthenticated" class="d-flex flex-column userinterface">
-            <div class="topbar d-flex pt-3 mx-3">
-                <div class="mr-auto d-flex">
-                    <Sidebar :routeName="routeName"></Sidebar>
-                    <div class="d-flex flex-column" style="max-width: 600px;">
-                        <Search v-if="routeName == 'mainview'"></Search>
-                        <Contextbox v-if="routeName == 'mainview'"></Contextbox>
-                    </div>
-                </div>
-                <div class="ml-auto d-flex">
-                    <Pdftool v-if="routeName == 'mainview'"></Pdftool>
-                    <Layers v-if="routeName == 'mainview'"></Layers>
-                    <Identify v-if="routeName == 'mainview'"></Identify>
-                    <Toolbar v-if="routeName == 'mainview'"></Toolbar>
-                </div>
-            </div>
-            <div class="flex-fill"></div>
-            <div class="d-flex flex-row align-items-end">
-                <Surveys v-if="routeName == 'mainview'"></Surveys>
-                <Infobar v-if="routeName == 'mainview'"></Infobar>
-            </div>
-            <Zoom v-if="routeName == 'mainview'"></Zoom>
-        </div>
-        <div class="d-flex flex-column">
-            <router-view/>
-        </div>
-    </div>
-</template>
-
-<style lang="sass" scoped>
-.userinterface
-  position: absolute
-  top: 0
-  left: 0
-  height: 100vh
-  width: 100vw
-  z-index: 4
-  pointer-events: none
-
-.topbar
-  position: relative
-  z-index: 2
-
-#app
-  height: 100vh
-  width: 100vw
-  font-family: "Avenir", Helvetica, Arial, sans-serif
-  -webkit-font-smoothing: antialiased
-  -moz-osx-font-smoothing: grayscale
-  text-align: center
-  color: #2c3e50
-</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>
- * Markus Kottländer <markus.kottlaender@intevation.de>
- */
-import { mapState } from "vuex";
-
-export default {
-  name: "app",
-  computed: {
-    ...mapState("user", ["isAuthenticated"]),
-    routeName() {
-      const routeName = this.$route.name;
-      return routeName;
-    }
-  },
-  components: {
-    Surveys: () => import("./fairway/Surveys"),
-    Infobar: () => import("./fairway/Infobar"),
-    Pdftool: () => import("./pdftool/Pdftool"),
-    Zoom: () => import("./zoom/zoom"),
-    Identify: () => import("./identify/Identify"),
-    Layers: () => import("./layers/Layers"),
-    Sidebar: () => import("./application/Sidebar"),
-    Search: () => import("./application/Search"),
-    Contextbox: () => import("./application/Contextbox"),
-    Toolbar: () => import("./toolbar/Toolbar")
-  }
-};
-</script>
--- a/client/src/application/Contextbox.vue	Wed Nov 21 16:47:02 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,91 +0,0 @@
-<template>
-    <div :class="style">
-        <div @click="close" class="ui-element close-contextbox">
-            <i class="fa fa-close"></i>
-        </div>
-        <Bottlenecks v-if="showInContextBox === 'bottlenecks'"></Bottlenecks>
-        <Imports v-if="showInContextBox === 'imports'"></Imports>
-        <Staging v-if="showInContextBox === '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/Bottlenecks"),
-    Imports: () => import("../imports/Imports.vue"),
-    Staging: () => import("../staging/Staging.vue")
-  },
-  computed: {
-    ...mapState("application", ["showSearchbarLastState", "showInContextBox"]),
-    style() {
-      return [
-        "ui-element shadow-xs contextbox ml-3",
-        {
-          contextboxcollapsed: !this.showInContextBox,
-          contextboxextended: this.showInContextBox,
-          "rounded-bottom": this.showInContextBox !== "imports",
-          rounded: this.showInContextBox === "imports"
-        }
-      ];
-    }
-  },
-  methods: {
-    close() {
-      this.$store.commit("application/showInContextBox", null);
-      this.$store.commit(
-        "application/showSearchbar",
-        this.showSearchbarLastState
-      );
-    }
-  }
-};
-</script>
-
-<style lang="sass" scoped>
-.contextbox
-  position: relative
-  background-color: #ffffff
-  opacity: $slight-transparent
-  transition: left 0.3s ease
-  overflow: hidden
-  background: #fff
-
-.contextboxcollapsed
-  width: 0
-  height: 0
-  transition: $transition-fast
-
-.contextboxextended
-  min-width: 600px
-
-.close-contextbox
-  position: absolute
-  z-index: 2
-  right: 0
-  top: 7px
-  height: $icon-width
-  width: $icon-height
-  display: none
-  color: #fff
-
-.contextboxextended .close-contextbox
-  display: block
-</style>
--- a/client/src/application/Main.vue	Wed Nov 21 16:47:02 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,126 +0,0 @@
-<template>
-    <div class="main d-flex flex-column">
-        <Maplayer :split="showSplitscreen" :lat="6155376" :long="1819178" :zoom="11"></Maplayer>
-        <FairwayProfile
-            :additionalSurveys="additionalSurveys"
-            :height="height"
-            :width="width"
-            :xScale="xAxis"
-            :yScaleLeft="yAxisLeft"
-            :yScaleRight="yAxisRight"
-            :margin="margins"
-        ></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 "../map/Maplayer";
-import FairwayProfile from "../fairway/Fairwayprofile";
-import { mapState } from "vuex";
-import debounce from "debounce";
-
-export default {
-  name: "mainview",
-  components: {
-    Maplayer,
-    FairwayProfile
-  },
-  data() {
-    return {
-      width: null,
-      height: null,
-      margin: {
-        top: 20,
-        right: 40,
-        bottom: 30,
-        left: 40
-      }
-    };
-  },
-  computed: {
-    ...mapState("application", ["showSplitscreen"]),
-    ...mapState("fairwayprofile", [
-      "currentProfile",
-      "minAlt",
-      "maxAlt",
-      "totalLength",
-      "waterLevels",
-      "fairwayCoordinates",
-      "selectedWaterLevel"
-    ]),
-    ...mapState("bottlenecks", ["surveys", "selectedSurvey"]),
-    additionalSurveys() {
-      if (!this.surveys) return [];
-      if (!this.selectedSurvey) return this.surveys;
-      return this.surveys.filter(survey => {
-        return survey.date_info !== this.selectedSurvey.date_info;
-      });
-    },
-    xAxis() {
-      return [this.xScale.x, this.xScale.y];
-    },
-    yAxisLeft() {
-      const hi = Math.max(this.maxAlt, this.selectedWaterLevel);
-      return [this.yScaleLeft.lo, hi];
-    },
-    yAxisRight() {
-      const DELTA = this.maxAlt * 1.1 - this.maxAlt;
-      return [this.maxAlt * 1 + DELTA, -DELTA];
-    },
-    margins() {
-      return this.margin;
-    },
-    yScaleLeft() {
-      return {
-        lo: this.minAlt,
-        hi: this.maxAlt
-      };
-    },
-    xScale() {
-      return {
-        x: 0,
-        y: this.totalLength
-      };
-    }
-  },
-  created() {
-    window.addEventListener("resize", debounce(this.scaleFairwayProfile), 100);
-    window.addEventListener("onbeforeprint", this.test);
-  },
-  updated() {
-    this.scaleFairwayProfile();
-  },
-  destroyed() {
-    window.removeEventListener("resize", debounce(this.scaleFairwayProfile));
-  },
-  methods: {
-    test(evt) {
-      console.log("test: ", evt);
-    },
-    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;
-    }
-  }
-};
-</script>
--- a/client/src/application/Search.vue	Wed Nov 21 16:47:02 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,223 +0,0 @@
-<template>
-    <div :class="searchbarContainerStyle">
-        <div class="input-group-prepend">
-            <span @click="toggleSearchbar" :class="searchButtonStyle" for="search">
-                <i class="fa fa-search d-print-none"></i>
-            </span>
-        </div>
-        <div class="searchgroup flex-fill">
-            <input
-                @keyup.enter="takeFirstSearchresult"
-                v-if="showSearchbar"
-                id="search"
-                v-model="searchQuery"
-                type="text"
-                :class="searchInputStyle"
-            >
-            <div v-if="showSearchbar && searchResults !== null && !showInContextBox" class="searchresults border-top ui-element bg-white rounded-bottom d-print-none">
-                <div v-for="entry of searchResults" :key="entry.name" class="border-top py-2">
-                    <a href="#" @click.prevent="moveToSearchResult(entry)">{{ entry.name }}</a>
-                </div>
-            </div>
-        </div>
-    </div>
-</template>
-
-<style lang="sass" scoped>
-  .searchcontainer
-    height: $icon-height
-    opacity: $slight-transparent
-
-  .searchbar-expanded
-    min-width: 600px
-    .searchbar
-      border-top-left-radius: 0 !important
-      border-bottom-left-radius: 0 !important
-
-  .searchbar-collapsed
-    width: $icon-width !important
-    transition: $transition-fast
-
-  .searchbar
-    height: $icon-height !important
-    box-shadow: none !important
-    &.rounded-top-right
-      border-radius: 0 !important
-      border-top-right-radius: $border-radius !important
-
-  .searchlabel
-    &.rounded-top-left
-      border-radius: 0 !important
-      border-top-left-radius: $border-radius !important
-
-  .input-group-text
-    height: $icon-height
-    width: $icon-width
-
-  .input-group-prepend
-    .fa-search
-      color: #666
-
-  .searchresults
-    margin-left: -31px
-    max-height: 20rem
-    overflow: auto
-    > div:first-child
-      border-top: 0 !important
-</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 "../application/lib/errors.js";
-import { HTTP } from "../application/lib/http";
-
-const setFocus = () => document.querySelector("#search").focus();
-
-export default {
-  name: "search",
-  data() {
-    return {
-      searchQueryIsDirty: false,
-      searchResults: null,
-      isSearching: false
-    };
-  },
-  computed: {
-    ...mapState("application", ["showSearchbar", "showInContextBox"]),
-    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 ml-3 shadow-xs",
-        {
-          "searchbar-collapsed": !this.showSearchbar,
-          "searchbar-expanded": this.showSearchbar,
-          "d-flex": this.showInContextBox !== "imports",
-          "d-none": this.showInContextBox === "imports"
-        }
-      ];
-    },
-    searchInputStyle() {
-      return [
-        "form-control ui-element search searchbar d-print-none border-0",
-        { "rounded-top-right": this.showInContextBox || 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.showInContextBox || 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: "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.showInContextBox) {
-        if (!this.showSearchbar) {
-          setTimeout(setFocus, 300);
-        }
-        this.$store.commit("application/showSearchbar", !this.showSearchbar);
-      }
-    }
-  }
-};
-</script>
--- a/client/src/application/Sidebar.vue	Wed Nov 21 16:47:02 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,186 +0,0 @@
-<template>
-    <div :class="sidebarStyle">
-        <div
-            @click="$store.commit('application/showSidebar', !showSidebar)"
-            class="menubutton p-2 bg-white rounded position-absolute d-flex justify-content-center"
-        >
-            <i class="ui-element d-print-none fa fa-bars"></i>
-        </div>
-        <div :class="menuStyle">
-            <div class="menupoints" v-if="this.showSidebar">
-                <router-link to="/" class="text-body d-flex flex-row nav-link">
-                    <i class="fa fa-map-o align-self-center navicon"></i>Map
-                </router-link>
-                <div class="d-flex flex-row nav-link">
-                    <i class="fa fa-ship align-self-center navicon"></i>
-                    <a
-                        class="text-body d-flex flex-row"
-                        href="#"
-                        @click="toggleContextBox('bottlenecks')"
-                    >Bottlenecks</a>
-                </div>
-                <div v-if="isSysAdmin">
-                    <hr>
-                    <div class="nav-link d-flex menupadding text-muted">Administration</div>
-                </div>
-                <div v-if="isWaterwayAdmin">
-                    <div class="d-flex flex-row nav-link">
-                        <i class="fa fa-upload align-self-center navicon"></i>
-                        <a
-                            href="#"
-                            class="text-body"
-                            @click="toggleContextBox('imports')"
-                        >Import soundingresults</a>
-                    </div>
-                    <div class="d-flex flex-row nav-link">
-                        <i class="fa fa-list-ol align-self-center navicon"></i>
-                        <a
-                            href="#"
-                            class="text-body"
-                            @click="toggleContextBox('staging')"
-                        >Staging area</a>
-                    </div>
-                    <div class="nav-link d-flex menupadding text-muted">Systemadministration</div>
-                    <router-link class="text-body d-flex flex-row nav-link" to="usermanagement">
-                        <i class="fa fa-address-card-o align-self-center navicon"></i>Users
-                    </router-link>
-                </div>
-                <div v-if="isSysAdmin">
-                    <router-link
-                        class="text-body d-flex flex-row nav-link"
-                        to="systemconfiguration"
-                    >
-                        <i class="fa fa-wrench align-self-center navicon"></i>Systemconfiguration
-                    </router-link>
-                    <router-link class="text-body d-flex flex-row nav-link" to="logs">
-                        <i class="fa fa-book align-self-center navicon"></i>Logs
-                    </router-link>
-                    <router-link class="text-body d-flex flex-row nav-link" to="importqueue">
-                        <i class="fa fa-exchange align-self-center navicon"></i>Importqueue
-                    </router-link>
-                </div>
-                <hr>
-                <a href="#" @click="logoff" class="text-body d-flex flex-row nav-link">
-                    <i class="fa fa-power-off align-self-center navicon"></i>
-                    Logout {{ user }}
-                </a>
-            </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 { mapGetters, mapState } from "vuex";
-
-export default {
-  name: "sidebar",
-  props: ["routeName"],
-  computed: {
-    ...mapGetters("user", ["isSysAdmin", "isWaterwayAdmin"]),
-    ...mapState("user", ["user"]),
-    ...mapState("application", [
-      "showSidebar",
-      "showSearchbarLastState",
-      "showInContextBox"
-    ]),
-    menuStyle() {
-      return {
-        menu: true,
-        nav: true,
-        "flex-column": true
-      };
-    },
-    sidebarStyle() {
-      return [
-        "ui-element position-relative sidebar rounded shadow-xs d-print-none mb-auto",
-        {
-          sidebarcollapsed: !this.showSidebar,
-          sidebarextended: this.showSidebar
-        }
-      ];
-    }
-  },
-  methods: {
-    logoff() {
-      this.$store.commit("user/clearAuth");
-      this.$store.commit("application/showSidebar", false);
-      this.$store.commit("application/showUsermenu", false);
-      this.$store.commit("application/showSplitscreen", false);
-      this.$router.push("/login");
-    },
-    toggleContextBox(context) {
-      const SHOW = context;
-      const HIDE = null;
-      const isElementAlreadyShown = this.showInContextBox === context;
-      let toggleState =
-        isElementAlreadyShown && this.routeName === "mainview" ? HIDE : SHOW;
-      this.$router.push("/");
-      this.$store.commit("application/showInContextBox", toggleState);
-      if (this.showInContextBox === context) {
-        this.$store.commit("application/showSearchbar", true);
-      } else {
-        this.$store.commit(
-          "application/showSearchbar",
-          this.showSearchbarLastState
-        );
-      }
-    }
-  }
-};
-</script>
-
-<style lang="sass" scoped>
-
-a:hover
-  text-decoration: none
-
-.menupoints
-  text-align: left
-
-.menubutton
-  height: $icon-height
-  width: $icon-width
-  top: 0
-  left: 0
-  color: #666
-
-.router-link-exact-active
-  background-color: #f2f2f2
-
-.navicon
-  margin-right: $small-offset
-  color: #666
-
-.menu
-  padding-top: $small-offset
-
-.sidebar
-  background-color: #ffffff
-  padding-top: $large-offset
-  opacity: $slight-transparent
-
-.sidebarcollapsed
-  height: 30px
-  width: 30px
-  transition: $transition-fast
-
-.sidebarextended
-  height: 35rem
-  width: $sidebar-width
-  min-width: $sidebar-width
-</style>
--- a/client/src/application/assets/application.sass	Wed Nov 21 16:47:02 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,66 +0,0 @@
-/*
- * 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>
- */
-$shadow-xs: 0 0.1rem 0.5rem rgba(0, 0, 0, 0.2)
-$border-radius: 0.25rem
-$icon-height: 2rem
-$icon-width: 2rem
-$large-offset: 2rem
-$offset: 1rem
-$sidebar-width: 15rem
-$slight-transparent: 0.96
-$small-offset: 0.5rem
-$smaller: 0.9rem
-$transition-fast: 0.3s
-$x-large-offset: 3rem
-$xx-large-offset: 5rem
-$color-info: #17a2b8
-
-a
-  color: $color-info
-
-.w-90
-  width: 90%
-
-.debug
-  border: 1px solid red
-
-%fully-centered
-  position: absolute
-  top: 50%
-  left: 50%
-  transform: translate(-50%, -50%)
-
-.ui-element
-  pointer-events: auto
-
-.shadow-xs
-  box-shadow: $shadow-xs
-
-.box
-  opacity: $slight-transparent
-  max-height: 0
-  max-width: 0
-  overflow: hidden
-  margin-left: 0
-  margin-right: 0
-  box-shadow: $shadow-xs
-  transition: max-width .4s, max-height .4s, margin-left .4s, margin-right .4s
-
-.expanded
-  max-height: 999px
-  max-width: 999px
-  margin-left: 0.5rem
-  margin-right: 0.5rem
Binary file client/src/application/assets/legend_contour_lines.png has changed
Binary file client/src/application/assets/linestring_arrow.png has changed
Binary file client/src/application/assets/linestring_arrow_grey.png has changed
Binary file client/src/application/assets/logo.png has changed
--- a/client/src/application/assets/tooltip.sass	Wed Nov 21 16:47:02 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,107 +0,0 @@
-/*
- * 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>
- */
-.tooltip
-  display: block !important
-  z-index: 10000
-
-  .tooltip-inner
-    background: black
-    color: white
-    border-radius: 16px
-    padding: 5px 10px 4px
-
-  .tooltip-arrow
-    width: 0
-    height: 0
-    border-style: solid
-    position: absolute
-    margin: 5px
-    border-color: black
-    z-index: 1
-
-  &[x-placement^="top"]
-    margin-bottom: 5px
-
-    .tooltip-arrow
-      border-width: 5px 5px 0 5px
-      border-left-color: transparent !important
-      border-right-color: transparent !important
-      border-bottom-color: transparent !important
-      bottom: -5px
-      left: calc(50% - 5px)
-      margin-top: 0
-      margin-bottom: 0
-
-  &[x-placement^="bottom"]
-    margin-top: 5px
-
-    .tooltip-arrow
-      border-width: 0 5px 5px 5px
-      border-left-color: transparent !important
-      border-right-color: transparent !important
-      border-top-color: transparent !important
-      top: -5px
-      left: calc(50% - 5px)
-      margin-top: 0
-      margin-bottom: 0
-
-  &[x-placement^="right"]
-    margin-left: 5px
-
-    .tooltip-arrow
-      border-width: 5px 5px 5px 0
-      border-left-color: transparent !important
-      border-top-color: transparent !important
-      border-bottom-color: transparent !important
-      left: -5px
-      top: calc(50% - 5px)
-      margin-left: 0
-      margin-right: 0
-
-  &[x-placement^="left"]
-    margin-right: 5px
-
-    .tooltip-arrow
-      border-width: 5px 0 5px 5px
-      border-top-color: transparent !important
-      border-right-color: transparent !important
-      border-bottom-color: transparent !important
-      right: -5px
-      top: calc(50% - 5px)
-      margin-left: 0
-      margin-right: 0
-
-  &.popover
-    $color: #f9f9f9
-
-    .popover-inner
-      background: $color
-      color: black
-      padding: 24px
-      border-radius: 5px
-      box-shadow: 0 5px 30px rgba(black, 0.1)
-
-    .popover-arrow
-      border-color: $color
-
-  &[aria-hidden="true"]
-    visibility: hidden
-    opacity: 0
-    transition: opacity 0.15s, visibility 0.15s
-
-  &[aria-hidden="false"]
-    visibility: visible
-    opacity: 1
-    transition: opacity 0.15s
Binary file client/src/application/assets/user.png has changed
--- a/client/src/application/lib/errors.js	Wed Nov 21 16:47:02 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,33 +0,0 @@
-/*
- * 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>
- */
-
-/** facade to wrap calls to vue2-toastr */
-import app from "../../main";
-
-const displayError = ({ title, message }) => {
-  app.$toast.error({
-    title: title,
-    message: message
-  });
-};
-
-const displayInfo = ({ title, message }) => {
-  app.$toast.info({
-    title: title,
-    message: message
-  });
-};
-
-export { displayError, displayInfo };
--- a/client/src/application/lib/geo.js	Wed Nov 21 16:47:02 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,208 +0,0 @@
-/*
- * 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>
- */
-
-/**
- *
- * Distance calculations
- * JS transposition of cross.go functions
- *
- */
-
-import { GeoJSON } from "ol/format.js";
-import Feature from "ol/Feature";
-import distance from "@turf/distance";
-import {
-  lineString as turfLineString,
-  polygon as turfPolygon
-} from "@turf/helpers";
-import lineIntersect from "@turf/line-intersect";
-
-const EARTHRADIUS = 6378137.0;
-
-/**
- * Converts to radiant
- * @param {degree} d
- */
-const deg2rad = d => {
-  return (d * Math.PI) / 180.0;
-};
-
-/**
- * Calculates the difference between two points in m
- *
- * Points are given with {lat: $lat, lon: $lon}
- *
- * @param {object} P1
- * @param {object} P2
- */
-const distanceBetween = (P1, P2) => {
-  const dLat = deg2rad(P2.lat - P1.lat);
-  let dLng = Math.abs(deg2rad(P2.lon - P1.lon));
-  if (dLng > Math.PI) {
-    dLng = 2 * Math.PI - dLng;
-  }
-  const x = dLng * Math.cos(deg2rad((P1.lat + P2.lat) / 2.0));
-  return Math.sqrt(dLat * dLat + x * x) * EARTHRADIUS;
-};
-
-/**
- * Takes a triple of [lat, long, alt] and generates
- * an object with according attributes
- * {
- *  lat: $lat,
- *  lon: $lon,
- *  alt: $alt
- * }
- *
- * @param {array} coords
- */
-const Point = coords => {
-  return {
-    lon: coords[0],
-    lat: coords[1],
-    alt: coords[2]
-  };
-};
-
-/**
- * Has geoJSON as its input and transforms
- * given coordinates into points representing
- * distance from startpoint / altitude information
- *
- * a) extracting the minimum altitude
- * b) extracting the maximum altitude
- * c) calculating the total length of the given profile
- * d) transposes the datapoints relative to a given start point
- *
- * The calculation of total equals the sum of partial distances between points
- *
- * The x-value of a point is equal to the total distance up to this point
- *
- * The distance between the last point of the last segment and the end point is added
- * to the total
- *
- * @param {object} geoJSON, startPoint, endPoint
- */
-const transform = ({ geoJSON, startPoint, endPoint }) => {
-  const lineSegments = geoJSON.geometry.coordinates;
-  let segmentPoints = [];
-  let lengthPolyLine = 0;
-  let referencePoint = Point(startPoint);
-  let minAlt = Math.abs(lineSegments[0][0][2]);
-  let maxAlt = Math.abs(lineSegments[0][0][2]);
-  let currentPoint = null;
-  for (let segment of lineSegments) {
-    let points = [];
-    for (let coordinateTriplet of segment) {
-      currentPoint = Point(coordinateTriplet);
-      lengthPolyLine += distanceBetween(referencePoint, currentPoint);
-      let y = Math.abs(currentPoint.alt);
-      points.push({
-        x: lengthPolyLine,
-        y: y
-      });
-      if (y < minAlt) minAlt = y;
-      if (y > maxAlt) maxAlt = y;
-      referencePoint = currentPoint;
-    }
-    segmentPoints.push(points);
-  }
-  lengthPolyLine += distanceBetween(currentPoint, Point(endPoint));
-  return { segmentPoints, lengthPolyLine, minAlt, maxAlt };
-};
-
-/**
- * Prepare profile takes geoJSON data in form of
- * a MultiLineString, e.g.
- *
- * {
- *   type: "Feature",
- *   geometry: {
- *   type: "MultiLineString",
- *   coordinates: [
- *                 [
- *                  [16.53593398, 48.14694085, -146.52392755]
- *                  ...
- *                  ]]
- *
- * and transforms it to a structure representing the number of sections
- * where data is present with according lengths and the points
- *
- * {
- *  { points:
- *           [
- *            [ { x: 0.005798201616417183, y: -146.52419461 },
- *              { x: 0, y: -146.52394016 }
- *              ...
- *            ]
- *           ]
- *   lengthPolyLine: 160.06814078495722,
- *   minAlt: -146.73122231,
- *   maxAlt: -145.65155866
- *  }
- *
- * @param {object} geoJSON
- */
-const prepareProfile = ({ geoJSON, startPoint, endPoint }) => {
-  const { segmentPoints, lengthPolyLine, minAlt, maxAlt } = transform({
-    geoJSON,
-    startPoint,
-    endPoint
-  });
-  return {
-    points: segmentPoints,
-    lengthPolyLine: lengthPolyLine,
-    minAlt: minAlt,
-    maxAlt: maxAlt
-  };
-};
-
-const generateFeatureRequest = (profileLine, bottleneck_id, date_info) => {
-  const feature = new Feature({
-    geometry: profileLine,
-    bottleneck: bottleneck_id,
-    date: date_info
-  });
-  return new GeoJSON({ geometryName: "geometry" }).writeFeature(feature);
-};
-
-const calculateFairwayCoordinates = (profileLine, fairwayGeometry, depth) => {
-  // both geometries have to be in EPSG:4326
-  // uses turfjs distance() function
-  let fairwayCoordinates = [];
-  var line = turfLineString(profileLine.getCoordinates());
-  var polygon = turfPolygon(fairwayGeometry.getCoordinates());
-  var intersects = lineIntersect(line, polygon);
-  var l = intersects.features.length;
-  if (l % 2 != 0) {
-    console.log("Ignoring fairway because profile only intersects once.");
-  } else {
-    for (let i = 0; i < l; i += 2) {
-      let pStartPoint = profileLine.getCoordinates()[0];
-      let fStartPoint = intersects.features[i].geometry.coordinates;
-      let fEndPoint = intersects.features[i + 1].geometry.coordinates;
-      let opts = { units: "kilometers" };
-
-      fairwayCoordinates.push([
-        distance(pStartPoint, fStartPoint, opts) * 1000,
-        distance(pStartPoint, fEndPoint, opts) * 1000,
-        depth
-      ]);
-    }
-  }
-  return fairwayCoordinates;
-};
-
-export { generateFeatureRequest, prepareProfile, calculateFairwayCoordinates };
--- a/client/src/application/lib/http.js	Wed Nov 21 16:47:02 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,23 +0,0 @@
-/*
- * 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 axios from "axios";
-
-export const HTTP = axios.create({
-  baseURL: process.env.VUE_APP_API_URL || "/api"
-  /* headers: {
-    Authorization: 'Bearer {token}'
-  }*/
-});
--- a/client/src/application/lib/session.js	Wed Nov 21 16:47:02 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,38 +0,0 @@
-/*
- * 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>
- */
-
-/**
- * Compares whether session is current
- * based on the expiry information and the
- * current date
- *
- * @param  {number} expiresFromPastSession
- */
-function sessionStillActive(expiresFromPastSession) {
-  if (!expiresFromPastSession) return false;
-  const now = Date.now();
-  const stillActive = now < expiresFromPastSession;
-  return stillActive;
-}
-/**
- * Converts a given unix time to Milliseconds
- *
- * @param  {string} timestring
- */
-function toMillisFromString(timestring) {
-  return timestring * 1000;
-}
-
-export { sessionStillActive, toMillisFromString };
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/client/src/assets/application.sass	Thu Nov 22 07:08:38 2018 +0100
@@ -0,0 +1,66 @@
+/*
+ * 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>
+ */
+$shadow-xs: 0 0.1rem 0.5rem rgba(0, 0, 0, 0.2)
+$border-radius: 0.25rem
+$icon-height: 2rem
+$icon-width: 2rem
+$large-offset: 2rem
+$offset: 1rem
+$sidebar-width: 15rem
+$slight-transparent: 0.96
+$small-offset: 0.5rem
+$smaller: 0.9rem
+$transition-fast: 0.3s
+$x-large-offset: 3rem
+$xx-large-offset: 5rem
+$color-info: #17a2b8
+
+a
+  color: $color-info
+
+.w-90
+  width: 90%
+
+.debug
+  border: 1px solid red
+
+%fully-centered
+  position: absolute
+  top: 50%
+  left: 50%
+  transform: translate(-50%, -50%)
+
+.ui-element
+  pointer-events: auto
+
+.shadow-xs
+  box-shadow: $shadow-xs
+
+.box
+  opacity: $slight-transparent
+  max-height: 0
+  max-width: 0
+  overflow: hidden
+  margin-left: 0
+  margin-right: 0
+  box-shadow: $shadow-xs
+  transition: max-width .4s, max-height .4s, margin-left .4s, margin-right .4s
+
+.expanded
+  max-height: 999px
+  max-width: 999px
+  margin-left: 0.5rem
+  margin-right: 0.5rem
Binary file client/src/assets/linestring_arrow.png has changed
Binary file client/src/assets/linestring_arrow_grey.png has changed
Binary file client/src/assets/logo.png has changed
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/client/src/assets/tooltip.sass	Thu Nov 22 07:08:38 2018 +0100
@@ -0,0 +1,107 @@
+/*
+ * 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>
+ */
+.tooltip
+  display: block !important
+  z-index: 10000
+
+  .tooltip-inner
+    background: black
+    color: white
+    border-radius: 16px
+    padding: 5px 10px 4px
+
+  .tooltip-arrow
+    width: 0
+    height: 0
+    border-style: solid
+    position: absolute
+    margin: 5px
+    border-color: black
+    z-index: 1
+
+  &[x-placement^="top"]
+    margin-bottom: 5px
+
+    .tooltip-arrow
+      border-width: 5px 5px 0 5px
+      border-left-color: transparent !important
+      border-right-color: transparent !important
+      border-bottom-color: transparent !important
+      bottom: -5px
+      left: calc(50% - 5px)
+      margin-top: 0
+      margin-bottom: 0
+
+  &[x-placement^="bottom"]
+    margin-top: 5px
+
+    .tooltip-arrow
+      border-width: 0 5px 5px 5px
+      border-left-color: transparent !important
+      border-right-color: transparent !important
+      border-top-color: transparent !important
+      top: -5px
+      left: calc(50% - 5px)
+      margin-top: 0
+      margin-bottom: 0
+
+  &[x-placement^="right"]
+    margin-left: 5px
+
+    .tooltip-arrow
+      border-width: 5px 5px 5px 0
+      border-left-color: transparent !important
+      border-top-color: transparent !important
+      border-bottom-color: transparent !important
+      left: -5px
+      top: calc(50% - 5px)
+      margin-left: 0
+      margin-right: 0
+
+  &[x-placement^="left"]
+    margin-right: 5px
+
+    .tooltip-arrow
+      border-width: 5px 0 5px 5px
+      border-top-color: transparent !important
+      border-right-color: transparent !important
+      border-bottom-color: transparent !important
+      right: -5px
+      top: calc(50% - 5px)
+      margin-left: 0
+      margin-right: 0
+
+  &.popover
+    $color: #f9f9f9
+
+    .popover-inner
+      background: $color
+      color: black
+      padding: 24px
+      border-radius: 5px
+      box-shadow: 0 5px 30px rgba(black, 0.1)
+
+    .popover-arrow
+      border-color: $color
+
+  &[aria-hidden="true"]
+    visibility: hidden
+    opacity: 0
+    transition: opacity 0.15s, visibility 0.15s
+
+  &[aria-hidden="false"]
+    visibility: visible
+    opacity: 1
+    transition: opacity 0.15s
Binary file client/src/assets/user.png has changed
--- a/client/src/bottlenecks/Bottlenecks.vue	Wed Nov 21 16:47:02 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,233 +0,0 @@
-<template>
-    <div>
-        <h6 class="mb-0 py-2 px-3 border-bottom d-flex align-items-center bg-info text-white">
-          <i class="fa fa-ship mr-2"></i>
-          Bottlenecks
-        </h6>
-        <div class="row p-2 text-left small">
-            <div class="col-5">
-                <a href="#" @click="sortBy('name')" class="sort-link">Name</a>
-                <i :class="sortClass" v-if="sortColumn === 'name'"></i>
-            </div>
-            <div class="col-2">
-                <a
-                    href="#"
-                    @click="sortBy('latestMeasurement')"
-                    class="sort-link"
-                >Latest Measurement</a>
-                <i :class="sortClass" v-if="sortColumn === 'latestMeasurement'"></i>
-            </div>
-            <div class="col-3">
-                <a href="#" @click="sortBy('chainage')" class="sort-link">Chainage</a>
-                <i :class="sortClass" v-if="sortColumn === 'chainage'"></i>
-            </div>
-            <div class="col-2"></div>
-        </div>
-        <div class="bottleneck-list small text-left" v-if="filteredAndSortedBottlenecks().length">
-            <div
-                v-for="bottleneck in filteredAndSortedBottlenecks()"
-                :key="bottleneck.properties.name"
-                class="border-top row mx-0 py-2"
-            >
-                <div class="col-5 text-left">
-                    <a
-                        href="#"
-                        class="d-block"
-                        @click="moveToBottleneck(bottleneck)"
-                    >{{ bottleneck.properties.name }}</a>
-                </div>
-                <div class="col-2">{{ displayCurrentSurvey(bottleneck.properties.current) }}</div>
-                <div
-                    class="col-3"
-                >{{ displayCurrentChainage(bottleneck.properties.from, bottleneck.properties.from) }}</div>
-                <div class="col-2 text-right">
-                    <button
-                        type="button"
-                        class="btn btn-sm btn-outline-secondary"
-                        @click="toggleBottleneck(bottleneck.properties.name)"
-                    >
-                        <i class="fa fa-angle-down"></i>
-                    </button>
-                </div>
-                <div
-                    :class="['col-12', 'surveys', {open: openBottleneck === bottleneck.properties.name}]"
-                >
-                    <a
-                        href="#"
-                        class="d-block p-2"
-                        v-for="(survey, index) in openBottleneckSurveys"
-                        :key="index"
-                        @click="selectSurvey(survey, bottleneck)"
-                    >{{ survey.date_info }}</a>
-                </div>
-            </div>
-        </div>
-        <div v-else class="small text-center py-3 border-top">
-            No results.
-        </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";
-import { displayError } from "../application/lib/errors.js";
-
-export default {
-  name: "bottlenecks",
-  data() {
-    return {
-      sortColumn: "name",
-      sortDirection: "ASC",
-      openBottleneck: null,
-      openBottleneckSurveys: null
-    };
-  },
-  computed: {
-    ...mapState("application", ["searchQuery", "showSearchbarLastState"]),
-    ...mapState("bottlenecks", ["bottlenecks"]),
-    sortClass() {
-      return [
-        "fa ml-1",
-        {
-          "fa-sort-amount-asc": this.sortDirection === "ASC",
-          "fa-sort-amount-desc": this.sortDirection === "DESC"
-        }
-      ];
-    }
-  },
-  methods: {
-    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
-      );
-      this.$store.commit("bottlenecks/setSelectedSurvey", survey);
-      this.moveToBottleneck(bottleneck);
-    },
-    moveToBottleneck(bottleneck) {
-      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";
-    },
-    toggleBottleneck(name) {
-      this.openBottleneckSurveys = null;
-      if (name === this.openBottleneck) {
-        this.openBottleneck = null;
-      } else {
-        this.openBottleneck = 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;
-          })
-          .catch(error => {
-            const { status, data } = error.response;
-            displayError({
-              title: "Backend Error",
-              message: `${status}: ${data.message || data}`
-            });
-          });
-      }
-    },
-    displayCurrentSurvey(current) {
-      return current ? current.substr(0, current.length - 1) : "";
-    },
-    displayCurrentChainage(from, to) {
-      return from / 10 + " - " + to / 10;
-    }
-  },
-  mounted() {
-    this.$store.dispatch("bottlenecks/loadBottlenecks");
-  }
-};
-</script>
-
-<style lang="sass" scoped>
-.bottleneck-list
-  overflow-y: auto
-  max-height: 500px
-
-.surveys
-  max-height: 0
-  overflow: hidden
-  transition: max-height 0.3s ease
-
-.surveys.open
-  max-height: 999px
-
-.sort-link
-  color: #444
-  font-weight: bold
-</style>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/client/src/components/App.vue	Thu Nov 22 07:08:38 2018 +0100
@@ -0,0 +1,96 @@
+<template>
+    <div id="app" class="main">
+        <div v-if="isAuthenticated" class="d-flex flex-column userinterface">
+            <div class="topbar d-flex pt-3 mx-3">
+                <div class="mr-auto d-flex">
+                    <Sidebar :routeName="routeName"></Sidebar>
+                    <div class="d-flex flex-column" style="max-width: 600px;">
+                        <Search v-if="routeName == 'mainview'"></Search>
+                        <Contextbox v-if="routeName == 'mainview'"></Contextbox>
+                    </div>
+                </div>
+                <div class="ml-auto d-flex">
+                    <Pdftool v-if="routeName == 'mainview'"></Pdftool>
+                    <Layers v-if="routeName == 'mainview'"></Layers>
+                    <Identify v-if="routeName == 'mainview'"></Identify>
+                    <Toolbar v-if="routeName == 'mainview'"></Toolbar>
+                </div>
+            </div>
+            <div class="flex-fill"></div>
+            <div class="d-flex flex-row align-items-end">
+                <Surveys v-if="routeName == 'mainview'"></Surveys>
+                <Infobar v-if="routeName == 'mainview'"></Infobar>
+            </div>
+            <Zoom v-if="routeName == 'mainview'"></Zoom>
+        </div>
+        <div class="d-flex flex-column">
+            <router-view/>
+        </div>
+    </div>
+</template>
+
+<style lang="sass" scoped>
+.userinterface
+  position: absolute
+  top: 0
+  left: 0
+  height: 100vh
+  width: 100vw
+  z-index: 4
+  pointer-events: none
+
+.topbar
+  position: relative
+  z-index: 2
+
+#app
+  height: 100vh
+  width: 100vw
+  font-family: "Avenir", Helvetica, Arial, sans-serif
+  -webkit-font-smoothing: antialiased
+  -moz-osx-font-smoothing: grayscale
+  text-align: center
+  color: #2c3e50
+</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>
+ * Markus Kottländer <markus.kottlaender@intevation.de>
+ */
+import { mapState } from "vuex";
+
+export default {
+  name: "app",
+  computed: {
+    ...mapState("user", ["isAuthenticated"]),
+    routeName() {
+      const routeName = this.$route.name;
+      return routeName;
+    }
+  },
+  components: {
+    Surveys: () => import("./map/fairway/Surveys"),
+    Infobar: () => import("./map/fairway/Infobar"),
+    Pdftool: () => import("./map/Pdftool"),
+    Zoom: () => import("./map/Zoom"),
+    Identify: () => import("./map/Identify"),
+    Layers: () => import("./map/layers/Layers"),
+    Sidebar: () => import("./Sidebar"),
+    Search: () => import("./map/Search"),
+    Contextbox: () => import("./map/Contextbox"),
+    Toolbar: () => import("./map/toolbar/Toolbar")
+  }
+};
+</script>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/client/src/components/Login.vue	Thu Nov 22 07:08:38 2018 +0100
@@ -0,0 +1,182 @@
+(<template>
+    <div class="d-flex flex-column login bg-white shadow">
+        <div class="m-5">
+            <!-- logo section -->
+            <div class="d-flex flex-row justify-content-center mb-3">
+                <div class="logo mr-3"><img src="../assets/logo.png"></div>
+                <div class="title">
+                    <h1>{{ appTitle }}</h1>
+                </div>
+            </div>
+            <!-- end logo section -->
+            <form class="loginform mx-auto" @submit.prevent="login">
+                <div id="alert" :style="errorMessageStyle" :class="errorMessageClass" role="alert">
+                    <span>{{ errorMessage }}</span>
+                </div>
+                <div class="input-group mb-3">
+                    <input type="text" v-model="user" id="inputUsername" class="form-control shadow-sm" :placeholder="usernameLabel" required autofocus>
+                </div>
+                <div class="input-group mb-3">
+                    <input :type="isPasswordVisible" v-model="password" id="inputPassword" class="form-control shadow-sm" :placeholder='passwordLabel' :required='!showPasswordReset' :disabled='showPasswordReset'>
+                    <div class="input-group-append">
+                        <span class="input-group-text disabled" id="basic-addon2" @click="showPassword">
+                            <font-awesome-icon icon="eye" v-if="!readablePassword" />
+                            <font-awesome-icon icon="eye-slash" v-if="readablePassword" />
+                        </span>
+                    </div>
+                </div>
+                <button v-if="showPasswordReset==false" class="btn btn-primary btn-block shadow-sm" :disabled="submitted || showPasswordReset" type="submit">
+                    <translate>Login</translate>
+                </button>
+                <div v-if="showPasswordReset" class="passwordreset">
+                    <button class="btn btn-block btn-info" type="button" @click="resetPassword">
+                        <translate>Request password reset!</translate>
+                    </button>
+                    <div class="pull-right">
+                        <a href="#" @click.prevent="togglePasswordReset">
+                            <translate>back to login</translate>
+                        </a>
+                    </div>
+                </div>
+                <div v-else class="pull-right">
+                    <a href="#" @click.prevent="togglePasswordReset">
+                        <translate>Forgot password</translate>
+                    </a>
+                </div>
+            </form>
+
+            <!-- bottom logo section -->
+            <div class="mb-3 secondary-logo mx-auto mb-auto"><img :src="secondaryLogo"></div>
+        </div>
+    </div>
+</template>)
+
+<style lang="sass" scoped>
+.login
+  min-width: 375px
+  min-height: 500px
+  @extend %fully-centered
+
+.loginform
+  max-width: 375px
+
+.alert
+  padding: 0.5rem
+
+.secondary-logo
+  max-width: 375px
+
+/* avoid IE and Edge show a password reveal as we do our own */
+input[type="password"]::-ms-reveal
+  display: none
+</style>
+
+<script>
+import { mapState } from "vuex";
+import { HTTP } from "../lib/http.js";
+import { displayError } from "../lib/errors.js";
+
+const UNAUTHORIZED = 401;
+
+export default {
+  name: "login",
+  data() {
+    return {
+      user: "",
+      password: "",
+      submitted: false,
+      loginFailed: false,
+      passwordJustResetted: false,
+      readablePassword: false,
+      showPasswordReset: false,
+      usernameToReset: ""
+    };
+  },
+  computed: {
+    errorMessage() {
+      if (this.loginFailed) return this.$gettext("Login failed");
+      if (this.passwordJustResetted)
+        return this.$gettext("Password reset requested!");
+      return "&npsp;";
+    },
+    passwordLabel() {
+      return this.$gettext("Enter passphrase");
+    },
+    usernameLabel() {
+      return this.$gettext("Enter username");
+    },
+    isPasswordVisible() {
+      return this.readablePassword ? "text" : "password";
+    },
+    errorMessageStyle() {
+      if (this.loginFailed || this.passwordJustResetted) {
+        return "visibility:visible";
+      }
+      return "visibility:hidden";
+    },
+    errorMessageClass() {
+      let result = {
+        "mb-3": true,
+        errormessage: true,
+        alert: true
+      };
+      if (this.loginFailed) {
+        result["alert-danger"] = true;
+      }
+      if (this.passwordJustResetted) {
+        result["alert-info"] = true;
+      }
+      return result;
+    },
+    ...mapState("application", ["appTitle", "secondaryLogo"])
+  },
+  methods: {
+    login() {
+      this.submitted = true;
+      this.passwordJustResetted = false;
+      const { user, password } = this;
+      this.$store
+        .dispatch("user/login", { user, password })
+        .then(() => {
+          this.loginFailed = false;
+          this.$router.push("/");
+        })
+        .catch(error => {
+          this.loginFailed = true;
+          this.submitted = false;
+          const { status, data } = error.response;
+          if (status !== UNAUTHORIZED) {
+            //Unauthorized is handled in alert-div
+            displayError({
+              title: "Backend Error",
+              message: `${status}: ${data.message || data}`
+            });
+          }
+        });
+    },
+    showPassword() {
+      // disallowing toggle when in reset mode
+      if (this.showPasswordReset) return;
+      this.readablePassword = !this.readablePassword;
+    },
+    togglePasswordReset() {
+      this.passwordJustResetted = false;
+      this.showPasswordReset = !this.showPasswordReset;
+      this.loginFailed = false;
+    },
+    resetPassword() {
+      if (this.user) {
+        HTTP.post("/users/passwordreset", { user: this.user }).catch(error => {
+          const { status, data } = error.response;
+          displayError({
+            title: "Backend Error",
+            message: `${status}: ${data.message || data}`
+          });
+        });
+        this.togglePasswordReset();
+        this.passwordJustResetted = true;
+      }
+    }
+  }
+};
+</script>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/client/src/components/Sidebar.vue	Thu Nov 22 07:08:38 2018 +0100
@@ -0,0 +1,186 @@
+<template>
+    <div :class="sidebarStyle">
+        <div
+            @click="$store.commit('application/showSidebar', !showSidebar)"
+            class="menubutton p-2 bg-white rounded position-absolute d-flex justify-content-center"
+        >
+            <i class="ui-element d-print-none fa fa-bars"></i>
+        </div>
+        <div :class="menuStyle">
+            <div class="menupoints" v-if="this.showSidebar">
+                <router-link to="/" class="text-body d-flex flex-row nav-link">
+                    <i class="fa fa-map-o align-self-center navicon"></i>Map
+                </router-link>
+                <div class="d-flex flex-row nav-link">
+                    <i class="fa fa-ship align-self-center navicon"></i>
+                    <a
+                        class="text-body d-flex flex-row"
+                        href="#"
+                        @click="toggleContextBox('bottlenecks')"
+                    >Bottlenecks</a>
+                </div>
+                <div v-if="isSysAdmin">
+                    <hr>
+                    <div class="nav-link d-flex menupadding text-muted">Administration</div>
+                </div>
+                <div v-if="isWaterwayAdmin">
+                    <div class="d-flex flex-row nav-link">
+                        <i class="fa fa-upload align-self-center navicon"></i>
+                        <a
+                            href="#"
+                            class="text-body"
+                            @click="toggleContextBox('imports')"
+                        >Import soundingresults</a>
+                    </div>
+                    <div class="d-flex flex-row nav-link">
+                        <i class="fa fa-list-ol align-self-center navicon"></i>
+                        <a
+                            href="#"
+                            class="text-body"
+                            @click="toggleContextBox('staging')"
+                        >Staging area</a>
+                    </div>
+                    <div class="nav-link d-flex menupadding text-muted">Systemadministration</div>
+                    <router-link class="text-body d-flex flex-row nav-link" to="usermanagement">
+                        <i class="fa fa-address-card-o align-self-center navicon"></i>Users
+                    </router-link>
+                </div>
+                <div v-if="isSysAdmin">
+                    <router-link
+                        class="text-body d-flex flex-row nav-link"
+                        to="systemconfiguration"
+                    >
+                        <i class="fa fa-wrench align-self-center navicon"></i>Systemconfiguration
+                    </router-link>
+                    <router-link class="text-body d-flex flex-row nav-link" to="logs">
+                        <i class="fa fa-book align-self-center navicon"></i>Logs
+                    </router-link>
+                    <router-link class="text-body d-flex flex-row nav-link" to="importqueue">
+                        <i class="fa fa-exchange align-self-center navicon"></i>Importqueue
+                    </router-link>
+                </div>
+                <hr>
+                <a href="#" @click="logoff" class="text-body d-flex flex-row nav-link">
+                    <i class="fa fa-power-off align-self-center navicon"></i>
+                    Logout {{ user }}
+                </a>
+            </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 { mapGetters, mapState } from "vuex";
+
+export default {
+  name: "sidebar",
+  props: ["routeName"],
+  computed: {
+    ...mapGetters("user", ["isSysAdmin", "isWaterwayAdmin"]),
+    ...mapState("user", ["user"]),
+    ...mapState("application", [
+      "showSidebar",
+      "showSearchbarLastState",
+      "showInContextBox"
+    ]),
+    menuStyle() {
+      return {
+        menu: true,
+        nav: true,
+        "flex-column": true
+      };
+    },
+    sidebarStyle() {
+      return [
+        "ui-element position-relative sidebar rounded shadow-xs d-print-none mb-auto",
+        {
+          sidebarcollapsed: !this.showSidebar,
+          sidebarextended: this.showSidebar
+        }
+      ];
+    }
+  },
+  methods: {
+    logoff() {
+      this.$store.commit("user/clearAuth");
+      this.$store.commit("application/showSidebar", false);
+      this.$store.commit("application/showUsermenu", false);
+      this.$store.commit("application/showSplitscreen", false);
+      this.$router.push("/login");
+    },
+    toggleContextBox(context) {
+      const SHOW = context;
+      const HIDE = null;
+      const isElementAlreadyShown = this.showInContextBox === context;
+      let toggleState =
+        isElementAlreadyShown && this.routeName === "mainview" ? HIDE : SHOW;
+      this.$router.push("/");
+      this.$store.commit("application/showInContextBox", toggleState);
+      if (this.showInContextBox === context) {
+        this.$store.commit("application/showSearchbar", true);
+      } else {
+        this.$store.commit(
+          "application/showSearchbar",
+          this.showSearchbarLastState
+        );
+      }
+    }
+  }
+};
+</script>
+
+<style lang="sass" scoped>
+
+a:hover
+  text-decoration: none
+
+.menupoints
+  text-align: left
+
+.menubutton
+  height: $icon-height
+  width: $icon-width
+  top: 0
+  left: 0
+  color: #666
+
+.router-link-exact-active
+  background-color: #f2f2f2
+
+.navicon
+  margin-right: $small-offset
+  color: #666
+
+.menu
+  padding-top: $small-offset
+
+.sidebar
+  background-color: #ffffff
+  padding-top: $large-offset
+  opacity: $slight-transparent
+
+.sidebarcollapsed
+  height: 30px
+  width: 30px
+  transition: $transition-fast
+
+.sidebarextended
+  height: 35rem
+  width: $sidebar-width
+  min-width: $sidebar-width
+</style>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/client/src/components/admin/logs.vue	Thu Nov 22 07:08:38 2018 +0100
@@ -0,0 +1,158 @@
+<template>
+    <div class="main d-flex flex-column">
+        <div class="d-flex flex-row">
+            <div :class="spacer"></div>
+            <div class="logoutput text-left bg-white shadow mt-3 mx-3">
+                <pre id="code" v-highlightjs="logs"><code class="bash hljs hljs-string"></code></pre>
+            </div>
+        </div>
+        <div class="d-flex flex-row logmenu">
+            <div class="d-flex align-self-center">
+                <ul class="nav nav-pills">
+                    <li class="nav-item">
+                        <a
+                            @click="fetch('system/log/apache2/access.log', 'accesslog')"
+                            :class="accesslogStyle"
+                            href="#"
+                        >Accesslog</a>
+                    </li>
+                    <li class="nav-item">
+                        <a
+                            @click="fetch('system/log/apache2/error.log', 'errorlog')"
+                            :class="errorlogStyle"
+                            href="#"
+                        >Errorlog</a>
+                    </li>
+                </ul>
+            </div>
+            <div class="statuscontainer d-flex flex-row">
+                <div class="statusline ml-3 mt-1 align-self-center">
+                    <h3>Last refresh: {{refreshed}}</h3>
+                </div>
+                <div class="refresh">
+                    <button class="btn btn-dark" @click="fetch(currentFile, currentLog)">Refresh</button>
+                </div>
+            </div>
+        </div>
+    </div>
+</template>
+
+<style lang="sass" scoped>
+.statuscontainer
+  width: 87%
+  position: relative
+  
+.logmenu
+  margin-left: 5rem
+  min-width: 60vw
+  
+#code
+  overflow: auto
+  
+.refresh
+  position: absolute
+  right: 0
+  
+.logoutput
+  width: 95%
+  height: 85vh
+  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: 7rem
+</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/admin/systemconfiguration.vue	Thu Nov 22 07:08:38 2018 +0100
@@ -0,0 +1,144 @@
+<template>
+    <div class="d-flex flex-row">
+        <div class="card sysconfig mt-3 mx-auto">
+            <div class="card-header shadow-sm text-white bg-info mb-6">
+                Systemconfiguration
+            </div>
+            <div class="card-body config">
+                <section class="configsection">
+                    <h4 class="card-title">Bottleneck Areas stroke-color</h4>
+                    <compact-picker v-model="strokeColor" />
+                </section>
+                <section>
+                    <h4 class="card-title">Bottleneck Areas fill-color</h4>
+                    <chrome-picker v-model="fillColor" />
+                </section>
+                <div class="sendbutton">
+                    <a @click.prevent="submit" class="btn btn-info">Send</a>
+                </div>
+            </div> <!-- card-body -->
+        </div>
+    </div>
+</template>
+
+<style scoped lang="sass">
+.config
+  text-align: left
+  
+.configsection
+  margin-bottom: $large-offset
+  
+.sendbutton
+  position: absolute
+  right: $offset
+  bottom: $offset
+  
+.inputs
+  margin-left: auto
+  margin-right: auto
+  
+.sysconfig
+  width: 30vw
+</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 { Chrome } from "vue-color";
+import { Compact } from "vue-color";
+
+import { HTTP } from "../../lib/http";
+import { displayError } from "../../lib/errors.js";
+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 },
+  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: "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: "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: "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: "Backend Error",
+          message: `${status}: ${data.message || data}`
+        });
+      });
+  }
+};
+</script>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/client/src/components/admin/usermanagement/Passwordfield.vue	Thu Nov 22 07:08:38 2018 +0100
@@ -0,0 +1,64 @@
+<template>
+    <div>
+        <label for="password">{{this.label}}</label>
+        <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"><i :class="eyeIcon"></i></span>
+        </div>
+        <div v-show="passworderrors" class="text-danger"><small><i class="fa fa-warning"></i> {{ 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";
+    },
+    eyeIcon() {
+      return {
+        fa: true,
+        "fa-eye": !this.readablePassword,
+        "fa-eye-slash": this.readablePassword
+      };
+    }
+  }
+};
+</script>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/client/src/components/admin/usermanagement/Userdetail.vue	Thu Nov 22 07:08:38 2018 +0100
@@ -0,0 +1,298 @@
+<template>
+    <div class="userdetails h-100 mt-3 mr-auto shadow fadeIn animated">
+        <div class="card">
+            <div class="card-header shadow-sm text-white bg-info mb-3">
+                {{ this.cardHeader }}
+                <span @click="closeDetailview" class="pull-right">
+                    <i class="fa fa-close"></i>
+                </span>
+            </div>
+            <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">Username</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>
+                                    <i class="fa fa-warning"></i> {{ errors.user }}</small>
+                            </div>
+                        </div>
+                        <div class="form-group row">
+                            <label for="country">Country</label>
+                            <select class="form-control form-control-sm" v-on:change="validateCountry" v-model="currentUser.country">
+                                <option disabled value="">Please select one</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>
+                                    <i class="fa fa-warning"></i> {{ errors.country }}</small>
+                            </div>
+                        </div>
+                        <div class="form-group row">
+                            <label for="email">Email address</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>
+                                    <i class="fa fa-warning"></i> {{ errors.email }}</small>
+                            </div>
+                        </div>
+                        <div class="form-group row">
+                            <label for="role">Role</label>
+                            <select class="form-control form-control-sm" v-on:change="validateRole" v-model="currentUser.role">
+                                <option disabled value="">Please select one</option>
+                                <option value="sys_admin">Sysadmin</option>
+                                <option value="waterway_admin">Waterway Admin</option>
+                                <option value="waterway_user">Waterway User</option>
+                            </select>
+                            <div v-show="errors.role" class="text-danger">
+                                <small>
+                                    <i class="fa fa-warning"></i> {{ 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 pull-right">Submit</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"><i class="fa fa-telegram"> Send testmail</i></a>
+                        <div v-if="mailsent">Mail was sent</div>
+                    </div>
+                </form>
+            </div>
+        </div>
+    </div>
+</template>
+
+<style lang="sass" scoped>
+.mailbutton
+  width: 12vw
+
+.formfields
+  width: 10vw
+
+.userdetails
+  min-width: 40vw
+
+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: "Password",
+      passwordReLabel: "Repeat Password",
+      passwordPlaceholder: "password",
+      passwordRePlaceholder: "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: "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
+        ? ""
+        : "Please choose a country";
+    },
+    validateRole() {
+      this.errors.role = this.currentUser.role ? "" : "Please choose a role";
+    },
+    validatePassword() {
+      this.errors.passwordre =
+        this.password === this.passwordre ? "" : "Passwords do not match!";
+      this.errors.password =
+        this.password === "" || !violatedPasswordRules(this.password)
+          ? ""
+          : "Password should at least be 8 char long including 1 digit and 1 special char like $";
+    },
+    validateEmailaddress() {
+      this.errors.email = isEmailValid(this.currentUser.email)
+        ? ""
+        : "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: "Backend Error",
+              message: `${status}: ${data.message || data}`
+            });
+          });
+        })
+        .catch(error => {
+          this.submitted = false;
+          const { status, data } = error.response;
+          displayError({
+            title: "Error while saving user",
+            message: `${status}: ${data.message || data}`
+          });
+        });
+    }
+  }
+};
+</script>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/client/src/components/admin/usermanagement/Usermanagement.vue	Thu Nov 22 07:08:38 2018 +0100
@@ -0,0 +1,302 @@
+<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">
+                        <div class="card-header shadow-sm text-white bg-info mb-3">
+                            Users
+                        </div>
+                        <div class="card-body">
+                            <table id="datatable" :class="tableStyle">
+                                <thead>
+                                    <tr>
+                                        <th scope="col" @click="sortBy('user')">
+                                            <span>Username&nbsp;
+                                                <i v-if="sortCriterion=='user'" class="fa fa-angle-down"></i>
+                                            </span>
+                                        </th>
+                                        <th scope="col" @click="sortBy('country')">
+                                            <span>Country&nbsp;
+                                                <i v-if="sortCriterion=='country'" class="fa fa-angle-down"></i>
+                                            </span>
+                                        </th>
+                                        <th scope="col" @click="sortBy('email')">
+                                            <span>Email&nbsp;
+                                                <i v-if="sortCriterion=='email'" class="fa fa-angle-down"></i>
+                                            </span>
+                                        </th>
+                                        <th scope="col" @click="sortBy('role')">
+                                            <span>Role&nbsp;
+                                                <i v-if="sortCriterion=='role'" class="fa fa-angle-down"></i>
+                                            </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>
+                                            <i v-tooltip="user.roleLabel" :class="{
+                        fa:true,
+                        icon:true, 
+                        'fa-user':user.role==='waterway_user',
+                        'fa-star':user.role=='sys_admin',
+                        'fa-adn':user.role==='waterway_admin'}"></i>
+                                        </td>
+                                        <td>
+                                            <i @click="deleteUser(user.user)" class="icon fa fa-trash-o"></i>
+                                        </td>
+                                    </tr>
+                                </tbody>
+                            </table>
+                        </div>
+                        <div class="d-flex flex-row pagination">
+                            <i @click=" prevPage " v-if="this.currentPage!=1 " class="mr-2 btn btn-sm btn-light align-self-center pages fa fa-caret-left "></i> {{this.currentPage}} / {{this.pages}}
+                            <i @click="nextPage " class="ml-2 btn btn-sm btn-light align-self-center pages fa fa-caret-right "></i>
+                        </div>
+                        <div class="mr-3 pb-3">
+                            <button @click="addUser " class="btn btn-info pull-right shadow-sm ">Add User</button>
+                        </div>
+                    </div>
+                </div>
+                <Userdetail v-if="isUserDetailsVisible "></Userdetail>
+            </div>
+        </div>
+    </div>
+</template>
+
+<style scoped lang="sass">
+@import "../../../assets/tooltip.sass"
+
+.spacer
+  height: 100vh
+
+.spacer-collapsed
+  min-width: $icon-width + $offset
+  transition: $transition-fast
+
+@media screen and (min-width: 600px)
+  .spacer-expanded
+    min-width: $icon-width + $offset
+
+@media screen and (max-width: 1650px)
+  .spacer-expanded
+    min-width: $sidebar-width + $offset
+
+.main
+  height: 100vh
+
+@media screen and (min-width: 600px)
+  .content
+    margin-left: $sidebar-width
+    margin-right: auto
+
+@media screen and (min-width: 1650px)
+  .content
+    margin-left: $sidebar-width
+    margin-right: auto
+
+.icon
+  font-size: large
+
+.userlist
+  min-width: 520px
+  height: 100%
+
+.pagination
+  margin-left: auto
+  margin-right: auto
+
+.userlistsmall
+  width: 30vw
+
+.userlistextended
+  width: 70vw
+
+.table
+  width: 90% !important
+  margin: auto
+
+.table th,
+.pages
+  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", "showUsermenu"]),
+    spacerStyle() {
+      return [
+        "spacer ml-3",
+        {
+          "spacer-expanded": this.showUsermenu && this.showSidebar,
+          "spacer-collapsed": !this.showUsermenu && 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",
+        {
+          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: "Backend Error",
+              message: `${status}: ${data.message || data}`
+            });
+          });
+        })
+        .catch(error => {
+          const { status, data } = error.response;
+          displayError({
+            title: "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);
+    }
+  },
+  beforeRouteEnter(to, from, next) {
+    store
+      .dispatch("usermanagement/loadUsers")
+      .then(next)
+      .catch(error => {
+        const { status, data } = error.response;
+        displayError({
+          title: "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/admin/usermanagement/Users.vue	Thu Nov 22 07:08:38 2018 +0100
@@ -0,0 +1,279 @@
+<template>
+    <div class="main d-flex flex-column">
+        <div class="d-flex content flex-column">
+            <div class="d-flex flex-row">
+                <div :class="userlistStyle">
+                    <div class="card">
+                        <div class="card-header shadow-sm text-white bg-info mb-3">
+                            Users
+                        </div>
+                        <div class="card-body">
+                            <table id="datatable" :class="tableStyle">
+                                <thead>
+                                    <tr>
+                                        <th scope="col" @click="sortBy('user')">
+                                            <span>Username&nbsp;
+                                                <i v-if="sortCriterion=='user'" class="fa fa-angle-down"></i>
+                                            </span>
+                                        </th>
+                                        <th scope="col" @click="sortBy('country')">
+                                            <span>Country&nbsp;
+                                                <i v-if="sortCriterion=='country'" class="fa fa-angle-down"></i>
+                                            </span>
+                                        </th>
+                                        <th scope="col" @click="sortBy('email')">
+                                            <span>Email&nbsp;
+                                                <i v-if="sortCriterion=='email'" class="fa fa-angle-down"></i>
+                                            </span>
+                                        </th>
+                                        <th scope="col" @click="sortBy('role')">
+                                            <span>Role&nbsp;
+                                                <i v-if="sortCriterion=='role'" class="fa fa-angle-down"></i>
+                                            </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>
+                                            <i v-tooltip="user.roleLabel" :class="{
+                        fa:true,
+                        icon:true, 
+                        'fa-user':user.role==='waterway_user',
+                        'fa-star':user.role=='sys_admin',
+                        'fa-adn':user.role==='waterway_admin'}"></i>
+                                        </td>
+                                        <td>
+                                            <i @click="deleteUser(user.user)" class="icon fa fa-trash-o"></i>
+                                        </td>
+                                    </tr>
+                                </tbody>
+                            </table>
+                        </div>
+                        <div class="d-flex flex-row pagination">
+                            <i @click=" prevPage " v-if="this.currentPage!=1 " class="backwards btn btn-sm btn-light align-self-center pages fa fa-caret-left "></i> {{this.currentPage}} / {{this.pages}}
+                            <i @click="nextPage " class="forwards btn btn-sm btn-light align-self-center pages fa fa-caret-right "></i>
+                        </div>
+                        <div class="mr-3 pb-3 ">
+                            <button @click="addUser " class="btn btn-info pull-right shadow-sm ">Add User</button>
+                        </div>
+                    </div>
+                </div>
+                <Userdetail v-if="isUserDetailsVisible "></Userdetail>
+            </div>
+        </div>
+    </div>
+</template>
+
+<style lang="sass" scoped>
+@import "../../../assets/tooltip.sass"
+
+.main
+  height: 100vh
+
+.backwards
+  margin-right: 0.5rem
+
+.forwards
+  margin-left: 0.5rem
+
+.content
+  margin-top: $large-offset
+  margin-left: auto
+  margin-right: auto
+
+.icon
+  font-size: large
+
+.userlist
+  margin-top: $topbarheight
+  min-width: 520px
+  height: 100%
+
+.pagination
+  margin-left: auto
+  margin-right: auto
+  
+.userlistsmall
+  width: 30vw
+
+.userlistextended
+  width: 70vw
+
+.table
+  width: 90% !important
+  margin: auto
+
+.table th,
+.pages
+  cursor: pointer
+
+.table th,
+td
+  font-size: 0.9rem
+  border-top: 0px !important
+  text-align: left
+  padding: 0.5rem !important
+
+.table td
+  font-size: 0.9rem
+  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 } 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"]),
+    ...mapGetters("application", ["sidebarCollapsed"]),
+    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 mr-3 shadow",
+        {
+          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: "Backend Error",
+              message: `${status}: ${data.message || data}`
+            });
+          });
+        })
+        .catch(error => {
+          const { status, data } = error.response;
+          displayError({
+            title: "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);
+    }
+  },
+  beforeRouteEnter(to, from, next) {
+    store
+      .dispatch("usermanagement/loadUsers")
+      .then(next)
+      .catch(error => {
+        const { status, data } = error.response;
+        displayError({
+          title: "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/map/Bottlenecks.vue	Thu Nov 22 07:08:38 2018 +0100
@@ -0,0 +1,233 @@
+<template>
+    <div>
+        <h6 class="mb-0 py-2 px-3 border-bottom d-flex align-items-center bg-info text-white">
+          <i class="fa fa-ship mr-2"></i>
+          Bottlenecks
+        </h6>
+        <div class="row p-2 text-left small">
+            <div class="col-5">
+                <a href="#" @click="sortBy('name')" class="sort-link">Name</a>
+                <i :class="sortClass" v-if="sortColumn === 'name'"></i>
+            </div>
+            <div class="col-2">
+                <a
+                    href="#"
+                    @click="sortBy('latestMeasurement')"
+                    class="sort-link"
+                >Latest Measurement</a>
+                <i :class="sortClass" v-if="sortColumn === 'latestMeasurement'"></i>
+            </div>
+            <div class="col-3">
+                <a href="#" @click="sortBy('chainage')" class="sort-link">Chainage</a>
+                <i :class="sortClass" v-if="sortColumn === 'chainage'"></i>
+            </div>
+            <div class="col-2"></div>
+        </div>
+        <div class="bottleneck-list small text-left" v-if="filteredAndSortedBottlenecks().length">
+            <div
+                v-for="bottleneck in filteredAndSortedBottlenecks()"
+                :key="bottleneck.properties.name"
+                class="border-top row mx-0 py-2"
+            >
+                <div class="col-5 text-left">
+                    <a
+                        href="#"
+                        class="d-block"
+                        @click="moveToBottleneck(bottleneck)"
+                    >{{ bottleneck.properties.name }}</a>
+                </div>
+                <div class="col-2">{{ displayCurrentSurvey(bottleneck.properties.current) }}</div>
+                <div
+                    class="col-3"
+                >{{ displayCurrentChainage(bottleneck.properties.from, bottleneck.properties.from) }}</div>
+                <div class="col-2 text-right">
+                    <button
+                        type="button"
+                        class="btn btn-sm btn-outline-secondary"
+                        @click="toggleBottleneck(bottleneck.properties.name)"
+                    >
+                        <i class="fa fa-angle-down"></i>
+                    </button>
+                </div>
+                <div
+                    :class="['col-12', 'surveys', {open: openBottleneck === bottleneck.properties.name}]"
+                >
+                    <a
+                        href="#"
+                        class="d-block p-2"
+                        v-for="(survey, index) in openBottleneckSurveys"
+                        :key="index"
+                        @click="selectSurvey(survey, bottleneck)"
+                    >{{ survey.date_info }}</a>
+                </div>
+            </div>
+        </div>
+        <div v-else class="small text-center py-3 border-top">
+            No results.
+        </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";
+
+export default {
+  name: "bottlenecks",
+  data() {
+    return {
+      sortColumn: "name",
+      sortDirection: "ASC",
+      openBottleneck: null,
+      openBottleneckSurveys: null
+    };
+  },
+  computed: {
+    ...mapState("application", ["searchQuery", "showSearchbarLastState"]),
+    ...mapState("bottlenecks", ["bottlenecks"]),
+    sortClass() {
+      return [
+        "fa ml-1",
+        {
+          "fa-sort-amount-asc": this.sortDirection === "ASC",
+          "fa-sort-amount-desc": this.sortDirection === "DESC"
+        }
+      ];
+    }
+  },
+  methods: {
+    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
+      );
+      this.$store.commit("bottlenecks/setSelectedSurvey", survey);
+      this.moveToBottleneck(bottleneck);
+    },
+    moveToBottleneck(bottleneck) {
+      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";
+    },
+    toggleBottleneck(name) {
+      this.openBottleneckSurveys = null;
+      if (name === this.openBottleneck) {
+        this.openBottleneck = null;
+      } else {
+        this.openBottleneck = 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;
+          })
+          .catch(error => {
+            const { status, data } = error.response;
+            displayError({
+              title: "Backend Error",
+              message: `${status}: ${data.message || data}`
+            });
+          });
+      }
+    },
+    displayCurrentSurvey(current) {
+      return current ? current.substr(0, current.length - 1) : "";
+    },
+    displayCurrentChainage(from, to) {
+      return from / 10 + " - " + to / 10;
+    }
+  },
+  mounted() {
+    this.$store.dispatch("bottlenecks/loadBottlenecks");
+  }
+};
+</script>
+
+<style lang="sass" scoped>
+.bottleneck-list
+  overflow-y: auto
+  max-height: 500px
+
+.surveys
+  max-height: 0
+  overflow: hidden
+  transition: max-height 0.3s ease
+
+.surveys.open
+  max-height: 999px
+
+.sort-link
+  color: #444
+  font-weight: bold
+</style>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/client/src/components/map/Contextbox.vue	Thu Nov 22 07:08:38 2018 +0100
@@ -0,0 +1,91 @@
+<template>
+    <div :class="style">
+        <div @click="close" class="ui-element close-contextbox">
+            <i class="fa fa-close"></i>
+        </div>
+        <Bottlenecks v-if="showInContextBox === 'bottlenecks'"></Bottlenecks>
+        <Imports v-if="showInContextBox === 'imports'"></Imports>
+        <Staging v-if="showInContextBox === '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"),
+    Imports: () => import("./imports/Imports.vue"),
+    Staging: () => import("./Staging.vue")
+  },
+  computed: {
+    ...mapState("application", ["showSearchbarLastState", "showInContextBox"]),
+    style() {
+      return [
+        "ui-element shadow-xs contextbox ml-3",
+        {
+          contextboxcollapsed: !this.showInContextBox,
+          contextboxextended: this.showInContextBox,
+          "rounded-bottom": this.showInContextBox !== "imports",
+          rounded: this.showInContextBox === "imports"
+        }
+      ];
+    }
+  },
+  methods: {
+    close() {
+      this.$store.commit("application/showInContextBox", null);
+      this.$store.commit(
+        "application/showSearchbar",
+        this.showSearchbarLastState
+      );
+    }
+  }
+};
+</script>
+
+<style lang="sass" scoped>
+.contextbox
+  position: relative
+  background-color: #ffffff
+  opacity: $slight-transparent
+  transition: left 0.3s ease
+  overflow: hidden
+  background: #fff
+
+.contextboxcollapsed
+  width: 0
+  height: 0
+  transition: $transition-fast
+
+.contextboxextended
+  min-width: 600px
+
+.close-contextbox
+  position: absolute
+  z-index: 2
+  right: 0
+  top: 7px
+  height: $icon-width
+  width: $icon-height
+  display: none
+  color: #fff
+
+.contextboxextended .close-contextbox
+  display: block
+</style>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/client/src/components/map/Identify.vue	Thu Nov 22 07:08:38 2018 +0100
@@ -0,0 +1,86 @@
+<template>
+    <div :class="['box ui-element rounded bg-white mb-auto text-nowrap', { expanded: showIdentify }]">
+        <div style="width: 20rem">
+            <h6 class="mb-0 py-2 px-3 border-bottom d-flex align-items-center bg-info text-white">
+              <i class="fa fa-info mr-2"></i>
+              Identified
+              <i class="fa fa-times ml-auto" @click="$store.commit('application/showIdentify', false)"></i>
+            </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 }">
+                        <b>{{ feature.getId().replace(/[.][^.]*$/,"") /* cut away everything from the last . to the end */}}:</b>
+                        <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">
+                    No features identified.
+                </div>
+            </div>
+            <div class="versioninfo border-top p-3 text-left">
+                gemma
+                <a href="https://hg.intevation.de/gemma/file/tip">source-code</a>
+                {{ versionStr }}
+                <br>Some data ©
+                <a href="https://www.openstreetmap.org/copyright">OpenSteetMap</a>contributors
+            </div>
+        </div>
+    </div>
+</template>
+
+<style lang="sass" scoped>
+.features
+  max-height: 19rem
+  overflow-y: auto
+
+.versioninfo
+  font-size: 60%
+</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/map/Main.vue	Thu Nov 22 07:08:38 2018 +0100
@@ -0,0 +1,126 @@
+<template>
+    <div class="main d-flex flex-column">
+        <Maplayer :split="showSplitscreen" :lat="6155376" :long="1819178" :zoom="11"></Maplayer>
+        <FairwayProfile
+            :additionalSurveys="additionalSurveys"
+            :height="height"
+            :width="width"
+            :xScale="xAxis"
+            :yScaleLeft="yAxisLeft"
+            :yScaleRight="yAxisRight"
+            :margin="margins"
+        ></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";
+import { mapState } from "vuex";
+import debounce from "debounce";
+
+export default {
+  name: "mainview",
+  components: {
+    Maplayer,
+    FairwayProfile
+  },
+  data() {
+    return {
+      width: null,
+      height: null,
+      margin: {
+        top: 20,
+        right: 40,
+        bottom: 30,
+        left: 40
+      }
+    };
+  },
+  computed: {
+    ...mapState("application", ["showSplitscreen"]),
+    ...mapState("fairwayprofile", [
+      "currentProfile",
+      "minAlt",
+      "maxAlt",
+      "totalLength",
+      "waterLevels",
+      "fairwayCoordinates",
+      "selectedWaterLevel"
+    ]),
+    ...mapState("bottlenecks", ["surveys", "selectedSurvey"]),
+    additionalSurveys() {
+      if (!this.surveys) return [];
+      if (!this.selectedSurvey) return this.surveys;
+      return this.surveys.filter(survey => {
+        return survey.date_info !== this.selectedSurvey.date_info;
+      });
+    },
+    xAxis() {
+      return [this.xScale.x, this.xScale.y];
+    },
+    yAxisLeft() {
+      const hi = Math.max(this.maxAlt, this.selectedWaterLevel);
+      return [this.yScaleLeft.lo, hi];
+    },
+    yAxisRight() {
+      const DELTA = this.maxAlt * 1.1 - this.maxAlt;
+      return [this.maxAlt * 1 + DELTA, -DELTA];
+    },
+    margins() {
+      return this.margin;
+    },
+    yScaleLeft() {
+      return {
+        lo: this.minAlt,
+        hi: this.maxAlt
+      };
+    },
+    xScale() {
+      return {
+        x: 0,
+        y: this.totalLength
+      };
+    }
+  },
+  created() {
+    window.addEventListener("resize", debounce(this.scaleFairwayProfile), 100);
+    window.addEventListener("onbeforeprint", this.test);
+  },
+  updated() {
+    this.scaleFairwayProfile();
+  },
+  destroyed() {
+    window.removeEventListener("resize", debounce(this.scaleFairwayProfile));
+  },
+  methods: {
+    test(evt) {
+      console.log("test: ", evt);
+    },
+    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;
+    }
+  }
+};
+</script>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/client/src/components/map/Maplayer.vue	Thu Nov 22 07:08:38 2018 +0100
@@ -0,0 +1,430 @@
+<template>
+    <div id="map" :class="mapStyle"></div>
+</template>
+
+<style lang="sass" scoped>
+.nocursor
+  cursor: none
+
+.mapsplit
+  height: 50vh
+
+.mapfull
+  height: 100vh
+
+@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";
+import { getCenter } from "ol/extent";
+
+/* for the sake of debugging */
+/* eslint-disable no-console */
+export default {
+  name: "maplayer",
+  props: ["lat", "long", "zoom", "split"],
+  data() {
+    return {
+      projection: "EPSG:3857"
+    };
+  },
+  computed: {
+    ...mapGetters("map", ["getLayerByName"]),
+    ...mapState("map", [
+      "layers",
+      "openLayersMap",
+      "lineTool",
+      "polygonTool",
+      "cutTool"
+    ]),
+    ...mapState("bottlenecks", ["selectedSurvey"]),
+    mapStyle() {
+      return {
+        mapfull: !this.split,
+        mapsplit: this.split,
+        nocursor: this.hasActiveInteractions
+      };
+    },
+    hasActiveInteractions() {
+      return (
+        (this.lineTool && this.lineTool.getActive()) ||
+        (this.polygonTool && this.polygonTool.getActive()) ||
+        (this.cutTool && this.cutTool.getActive())
+      );
+    }
+  },
+  methods: {
+    identify(coordinate, pixel) {
+      if (!this.hasActiveInteractions) {
+        this.$store.commit("map/setIdentifiedFeatures", []);
+        // checking our WFS layers
+        var features = this.openLayersMap.getFeaturesAtPixel(pixel);
+        if (features) {
+          this.$store.commit("map/setIdentifiedFeatures", features);
+
+          // get selected bottleneck from identified features
+          for (let feature of features) {
+            let id = feature.getId();
+            // RegExp.prototype.test() works with number, str and undefined
+            if (/^bottlenecks\./.test(id)) {
+              this.$store.dispatch(
+                "bottlenecks/setSelectedBottleneck",
+                feature.get("objnam")
+              );
+              this.$store.commit("map/moveMap", {
+                coordinates: getCenter(
+                  feature
+                    .getGeometry()
+                    .clone()
+                    .transform("EPSG:3857", "EPSG:4326")
+                    .getExtent()
+                ),
+                zoom: 17,
+                preventZoomOut: true
+              });
+            }
+          }
+        }
+
+        // DEBUG output and example how to remove the GeometryName
+        /*
+        for (let feature of features) {
+          console.log("Identified:", feature.getId());
+          for (let key of feature.getKeys()) {
+            if (key != feature.getGeometryName()) {
+              console.log(key, feature.get(key));
+            }
+          }
+        }
+        */
+
+        // trying the GetFeatureInfo way for WMS
+        var wmsSource = this.getLayerByName(
+          "Inland ECDIS chart Danube"
+        ).data.getSource();
+        var url = wmsSource.getGetFeatureInfoUrl(
+          coordinate,
+          100 /* resolution */,
+          "EPSG:3857",
+          // { INFO_FORMAT: "application/vnd.ogc.gml" } // not allowed by d4d
+          { INFO_FORMAT: "text/plain" }
+        );
+
+        if (url) {
+          // cannot directly query here because of SOP
+          console.log("GetFeatureInfo url:", url);
+        }
+      }
+    },
+    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);
+      var layer = this.getLayerByName("Bottleneck isolines");
+      var wmsSrc = layer.data.getSource();
+
+      if (bottleneck_id != "does_not_exist") {
+        wmsSrc.updateParams({
+          cql_filter:
+            "date_info='" +
+            datestr +
+            "' AND bottleneck_id='" +
+            bottleneck_id +
+            "'"
+        });
+        layer.isVisible = true;
+        layer.data.setVisible(true);
+      } else {
+        layer.isVisible = false;
+        layer.data.setVisible(false);
+      }
+    },
+    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: {
+    split() {
+      const map = this.openLayersMap;
+      this.$nextTick(() => {
+        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.long, this.lat],
+        zoom: this.zoom,
+        projection: this.projection
+      })
+    });
+    this.$store.commit("map/setOpenLayersMap", 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 => {
+      var features = new GeoJSON().readFeatures(JSON.stringify(response.data));
+      var vectorSrc = this.getLayerByName(
+        "Fairway Dimensions"
+      ).data.getSource();
+      vectorSrc.addFeatures(features);
+      // 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.openLayersMap.on(["singleclick", "dblclick"], event => {
+      this.identify(event.coordinate, event.pixel);
+    });
+  }
+};
+</script>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/client/src/components/map/Pdftool.vue	Thu Nov 22 07:08:38 2018 +0100
@@ -0,0 +1,96 @@
+<template>
+    <div :class="['box ui-element rounded bg-white mb-auto text-nowrap', { expanded: showPdfTool }]">
+        <div style="width: 15rem">
+            <h6 class="mb-0 py-2 px-3 border-bottom d-flex align-items-center bg-info text-white">
+              <i class="fa fa-file-pdf-o mr-2"></i>
+              Generate PDF
+              <i class="fa fa-times ml-auto" @click="$store.commit('application/showPdfTool', false)"></i>
+            </h6>
+            <div class="p-3">
+                <b>Chose format:</b>
+                <select v-model="form.format" class="form-control d-block w-100">
+                    <option>landscape</option>
+                    <option>portrait</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">Download</label>
+                    <input
+                        type="radio"
+                        id="pdfexport-downloadtype-open"
+                        value="open"
+                        v-model="form.downloadType"
+                    >
+                    <label for="pdfexport-downloadtype-open" class="ml-1">Open in new window</label>
+                </small>
+                <button
+                    @click="download"
+                    type="button"
+                    class="btn btn-sm btn-info d-block w-100"
+                >Generate PDF</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/map/Search.vue	Thu Nov 22 07:08:38 2018 +0100
@@ -0,0 +1,223 @@
+<template>
+    <div :class="searchbarContainerStyle">
+        <div class="input-group-prepend">
+            <span @click="toggleSearchbar" :class="searchButtonStyle" for="search">
+                <i class="fa fa-search d-print-none"></i>
+            </span>
+        </div>
+        <div class="searchgroup flex-fill">
+            <input
+                @keyup.enter="takeFirstSearchresult"
+                v-if="showSearchbar"
+                id="search"
+                v-model="searchQuery"
+                type="text"
+                :class="searchInputStyle"
+            >
+            <div v-if="showSearchbar && searchResults !== null && !showInContextBox" class="searchresults border-top ui-element bg-white rounded-bottom d-print-none">
+                <div v-for="entry of searchResults" :key="entry.name" class="border-top py-2">
+                    <a href="#" @click.prevent="moveToSearchResult(entry)">{{ entry.name }}</a>
+                </div>
+            </div>
+        </div>
+    </div>
+</template>
+
+<style lang="sass" scoped>
+  .searchcontainer
+    height: $icon-height
+    opacity: $slight-transparent
+
+  .searchbar-expanded
+    min-width: 600px
+    .searchbar
+      border-top-left-radius: 0 !important
+      border-bottom-left-radius: 0 !important
+
+  .searchbar-collapsed
+    width: $icon-width !important
+    transition: $transition-fast
+
+  .searchbar
+    height: $icon-height !important
+    box-shadow: none !important
+    &.rounded-top-right
+      border-radius: 0 !important
+      border-top-right-radius: $border-radius !important
+
+  .searchlabel
+    &.rounded-top-left
+      border-radius: 0 !important
+      border-top-left-radius: $border-radius !important
+
+  .input-group-text
+    height: $icon-height
+    width: $icon-width
+
+  .input-group-prepend
+    .fa-search
+      color: #666
+
+  .searchresults
+    margin-left: -31px
+    max-height: 20rem
+    overflow: auto
+    > div:first-child
+      border-top: 0 !important
+</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", "showInContextBox"]),
+    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 ml-3 shadow-xs",
+        {
+          "searchbar-collapsed": !this.showSearchbar,
+          "searchbar-expanded": this.showSearchbar,
+          "d-flex": this.showInContextBox !== "imports",
+          "d-none": this.showInContextBox === "imports"
+        }
+      ];
+    },
+    searchInputStyle() {
+      return [
+        "form-control ui-element search searchbar d-print-none border-0",
+        { "rounded-top-right": this.showInContextBox || 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.showInContextBox || 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: "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.showInContextBox) {
+        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/map/Staging.vue	Thu Nov 22 07:08:38 2018 +0100
@@ -0,0 +1,148 @@
+<template>
+    <div>
+        <h6 class="mb-0 py-2 px-3 border-bottom d-flex align-items-center bg-info text-white">
+          <i class="fa fa-list-ol mr-2"></i>
+          Staging Area
+        </h6>
+        <table class="table mb-0">
+            <thead>
+                <tr>
+                    <th>Name</th>
+                    <th>Datatype</th>
+                    <th>Importdate</th>
+                    <th>ImportID</th>
+                    <th>&nbsp;</th>
+                </tr>
+            </thead>
+            <tbody v-if="filteredData.length">
+                <tr v-for="data in filteredData" :key="data.id">
+                    <td>
+                        <a @click="zoomTo(data.location)" href="#">{{ data.name }}</a>
+                    </td>
+                    <td>{{ data.type }}</td>
+                    <td>{{ data.date }}</td>
+                    <td>{{ data.importID }}</td>
+                    <td>
+                        <button class="btn btn-outline-info">
+                            <i class="fa fa-thumbs-up"></i>
+                        </button>
+                        &nbsp;
+                        <button class="btn btn-outline-info">
+                            <i class="fa fa-thumbs-down"></i>
+                        </button>
+                    </td>
+                </tr>
+            </tbody>
+            <tbody v-else>
+                <tr>
+                    <td class="text-center" colspan="6">No results.</td>
+                </tr>
+            </tbody>
+        </table>
+        <div class="p-3" v-if="filteredData.length">
+            <button class="btn btn-info">Confirm</button>
+        </div>
+    </div>
+</template>
+
+<script>
+import { mapState } from "vuex";
+
+const demodata = [
+  {
+    id: 1,
+    name: "B1",
+    date: "2018-11-19 10:23",
+    location: [16.5364, 48.1471],
+    status: "Not approved",
+    importID: "123456789",
+    type: "bottleneck"
+  },
+  {
+    id: 2,
+    name: "B2",
+    date: "2018-11-19 10:24",
+    location: [16.5364, 48.1472],
+    status: "Not approved",
+    importID: "123456789",
+    type: "bottleneck"
+  },
+  {
+    id: 3,
+    name: "s1",
+    date: "2018-11-13 10:25",
+    location: [16.5364, 48.1473],
+    status: "Not approved",
+    importID: "987654321",
+    type: "soundingresult"
+  },
+  {
+    id: 4,
+    name: "s2",
+    date: "2018-11-13 10:26",
+    location: [16.5364, 48.1474],
+    status: "Not approved",
+    importID: "987654321",
+    type: "soundingresult"
+  }
+];
+
+export default {
+  computed: {
+    ...mapState("application", ["searchQuery"]),
+    filteredData() {
+      return demodata.filter(data => {
+        const nameFound = data.name
+          .toLowerCase()
+          .includes(this.searchQuery.toLowerCase());
+        const dateFound = data.date
+          .toLowerCase()
+          .includes(this.searchQuery.toLowerCase());
+        const locationFound = data.location.find(coord => {
+          return coord
+            .toString()
+            .toLowerCase()
+            .includes(this.searchQuery.toLowerCase());
+        });
+        const statusFound = data.status
+          .toLowerCase()
+          .includes(this.searchQuery.toLowerCase());
+        const importIDFound = data.importID
+          .toLowerCase()
+          .includes(this.searchQuery.toLowerCase());
+        const typeFound = data.type
+          .toLowerCase()
+          .includes(this.searchQuery.toLowerCase());
+
+        return (
+          nameFound ||
+          dateFound ||
+          locationFound ||
+          statusFound ||
+          importIDFound ||
+          typeFound
+        );
+      });
+    }
+  },
+  methods: {
+    zoomTo(coordinates) {
+      this.$store.commit("map/moveMap", {
+        coordinates: coordinates,
+        zoom: 17,
+        preventZoomOut: true
+      });
+    }
+  }
+};
+</script>
+
+<style lang="sass" scoped>
+.table th,
+td
+  font-size: 0.9rem
+  border-top: 0px !important
+  border-bottom-width: 1px
+  text-align: left
+  padding: 0.5rem !important
+</style>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/client/src/components/map/Zoom.vue	Thu Nov 22 07:08:38 2018 +0100
@@ -0,0 +1,51 @@
+<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 border-right" @click="zoomIn">
+            <i class="fa fa-plus"></i>
+        </button>
+        <button class="zoomButton border-0 bg-white rounded-right ui-element" @click="zoomOut">
+            <i class="fa fa-minus"></i>
+        </button>
+    </div>
+</template>
+
+<style lang="sass" scoped>
+.buttoncontainer
+  bottom: 0
+  left: 50%
+  margin-left: -$icon-width
+
+.zoomButton
+  min-height: $icon-width
+  min-width: $icon-width
+  z-index: 2
+  outline: none
+  color: #666
+</style>
+<script>
+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>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/client/src/components/map/fairway/Fairwayprofile.vue	Thu Nov 22 07:08:38 2018 +0100
@@ -0,0 +1,579 @@
+<template>
+    <div :class="['position-relative', {show: showSplitscreen}]" v-if="Object.keys(currentProfile).length">
+        <button
+            class="rounded-bottom bg-white border-0 position-absolute splitscreen-toggle shadow-sm"
+            @click="$store.commit('application/showSplitscreen', false)"
+            v-if="showSplitscreen">
+            <i class="fa fa-angle-down"></i>
+        </button>
+        <button
+            class="rounded-bottom bg-white border-0 position-absolute clear-selection shadow-sm"
+            @click="$store.dispatch('fairwayprofile/clearSelection');"
+            v-if="showSplitscreen">
+            <i class="fa fa-times text-danger"></i>
+        </button>
+        <div class="profile bg-white position-relative d-flex flex-column pr-5">
+            <h5 class="mb-0 mt-2">{{ selectedBottleneck }} ({{ selectedSurvey.date_info }})</h5>
+            <div class="d-flex flex-fill">
+                <div class="fairwayprofile m-3 mt-0 bg-white flex-grow-1"></div>
+                <div class="additionalsurveys d-flex flex-column">
+                    <small>
+                        Additional Surveys
+                        <select v-model="additionalSurvey" class="form-control form-control-sm">
+                            <option value="">None</option>
+                            <option
+                                v-for="survey in additionalSurveys"
+                                :key="survey.date_info"
+                                :value="survey"
+                            >{{survey.date_info}}</option>
+                        </select>
+                        <hr>
+                        <div class="d-flex text-left mb-2">
+                            <div class="text-nowrap mr-1">
+                                <b>Start:</b>
+                                <br>
+                                Lat: {{ startPoint[1] }}
+                                <br>
+                                Lon: {{ startPoint[0] }}
+                            </div>
+                            <div class="text-nowrap ml-1">
+                                <b>End:</b>
+                                <br>
+                                Lat: {{ endPoint[1] }}
+                                <br>
+                                Lon: {{ endPoint[0] }}
+                            </div>
+                            <button class="btn btn-outline-secondary btn-sm ml-2 mt-auto"
+                                    @click="showLabelInput = !showLabelInput">
+                              <i :class="'fa fa-' + (showLabelInput ? 'times' : 'save')"></i>
+                            </button>
+                            <button v-clipboard:copy="coordinatesForClipboard"
+                                    v-clipboard:success="onCopyCoordinates"
+                                    class="btn btn-outline-secondary btn-sm ml-2 mt-auto">
+                              <i class="fa fa-copy"></i>
+                            </button>
+                        </div>
+                        <div v-if="showLabelInput">
+                          Enter label for cross profile:
+                          <div class="position-relative">
+                              <input class="form-control form-control-sm pr-5" v-model="cutLabel" /><br>
+                              <button class="btn btn-sm btn-outline-secondary position-absolute"
+                                      @click="saveCut"
+                                      v-if="cutLabel"
+                                      style="top: 0; right: 0;">
+                                <i class="fa fa-check"></i>
+                              </button>
+                          </div>
+                        </div>
+                        Saved cross profiles:
+                        <select class="form-control form-control-sm mb-2" v-model="coordinatesSelect">
+                            <option></option>
+                            <option v-for="(cut, index) in previousCuts" :value="cut.coordinates" :key="index">
+                              {{ cut.label }}
+                            </option>
+                        </select>
+                        Enter coordinates manually:
+                        <div class="position-relative">
+                            <input class="form-control form-control-sm pr-5" placeholder="Lat,Lon,Lat,Lon" v-model="coordinatesInput" /><br>
+                            <button class="btn btn-sm btn-outline-secondary position-absolute" 
+                                    @click="applyManualCoordinates"
+                                    style="top: 0; right: 0;"
+                                    v-if="coordinatesInput">
+                              <i class="fa fa-check"></i>
+                            </button>
+                        </div>
+                    </small>
+                </div>
+            </div>
+        </div>
+    </div>
+</template>
+
+<style lang="sass" scoped>
+.profile
+  width: 100vw
+  height: 0
+  overflow: hidden
+  z-index: 2
+
+.splitscreen-toggle,
+.clear-selection
+  right: $icon-width + $offset
+  width: $icon-width
+  height: $icon-height
+  margin-top: 2px
+  z-index: 3
+  outline: none
+
+.clear-selection
+  right: $offset
+
+.show
+  .profile
+    height: 50vh
+
+.waterlevelselection
+  margin-top: $large-offset
+  margin-right: $large-offset
+
+.additionalsurveys
+  margin-top: $large-offset
+  margin-bottom: auto
+  margin-right: $large-offset
+  margin-left: auto
+  max-width: 300px
+
+.additionalsurveys input
+  margin-right: $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 * as d3 from "d3";
+import { mapState, mapGetters } from "vuex";
+import { displayError, displayInfo } from "../../../lib/errors.js";
+import Feature from "ol/Feature";
+import LineString from "ol/geom/LineString";
+
+const GROUND_COLOR = "#4A2F06";
+
+export default {
+  name: "fairwayprofile",
+  props: [
+    "width",
+    "height",
+    "xScale",
+    "yScaleLeft",
+    "yScaleRight",
+    "margin",
+    "additionalSurveys"
+  ],
+  data() {
+    return {
+      wait: false,
+      coordinatesInput: "",
+      coordinatesSelect: null,
+      cutLabel: "",
+      showLabelInput: false
+    };
+  },
+  computed: {
+    ...mapGetters("map", ["getLayerByName"]),
+    ...mapState("application", ["showSplitscreen"]),
+    ...mapState("fairwayprofile", [
+      "startPoint",
+      "endPoint",
+      "currentProfile",
+      "minAlt",
+      "maxAlt",
+      "totalLength",
+      "fairwayCoordinates",
+      "waterLevels",
+      "selectedWaterLevel",
+      "previousCuts"
+    ]),
+    ...mapState("bottlenecks", ["selectedBottleneck", "selectedSurvey"]),
+    additionalSurvey: {
+      get() {
+        return this.$store.getters["fairwayprofile/additionalSurvey"];
+      },
+      set(value) {
+        this.$store.commit("fairwayprofile/setAdditionalSurvey", value);
+        this.selectAdditionalSurveyData();
+      }
+    },
+    currentData() {
+      if (
+        !this.selectedSurvey ||
+        !this.currentProfile.hasOwnProperty(this.selectedSurvey.date_info)
+      )
+        return [];
+      return this.currentProfile[this.selectedSurvey.date_info];
+    },
+    additionalData() {
+      if (
+        !this.additionalSurvey ||
+        !this.currentProfile.hasOwnProperty(this.additionalSurvey.date_info)
+      )
+        return [];
+      return this.currentProfile[this.additionalSurvey.date_info];
+    },
+    waterColor() {
+      const result = this.waterLevels.find(
+        x => x.level === this.selectedWaterLevel
+      );
+      return result.color;
+    },
+    coordinatesForClipboard() {
+      return (
+        this.startPoint[1] +
+        "," +
+        this.startPoint[0] +
+        "," +
+        this.endPoint[1] +
+        "," +
+        this.endPoint[0]
+      );
+    }
+  },
+  watch: {
+    showSplitscreen() {
+      this.drawDiagram();
+    },
+    currentData() {
+      this.drawDiagram();
+    },
+    width() {
+      this.drawDiagram();
+    },
+    height() {
+      this.drawDiagram();
+    },
+    waterLevels() {
+      this.drawDiagram();
+    },
+    selectedWaterLevel() {
+      this.drawDiagram();
+    },
+    fairwayCoordinates() {
+      this.drawDiagram();
+    },
+    selectedBottleneck() {
+      this.$store.dispatch("fairwayprofile/previousCuts");
+      this.cutLabel =
+        this.selectedBottleneck + " (" + new Date().toISOString() + ")";
+    },
+    coordinatesSelect(newValue) {
+      if (newValue) {
+        this.applyCoordinates(newValue);
+      }
+    }
+  },
+  methods: {
+    selectAdditionalSurveyData() {
+      if (
+        !this.additionalSurvey ||
+        this.wait ||
+        this.currentProfile[this.additionalSurvey.date_info]
+      ) {
+        this.drawDiagram();
+        return;
+      }
+      this.$store
+        .dispatch("fairwayprofile/loadProfile", this.additionalSurvey)
+        .then(() => {
+          this.wait = false;
+        })
+        .catch(error => {
+          this.wait = false;
+          let status = "ERROR";
+          let data = error;
+          const response = error.response;
+          if (response) {
+            status = response.status;
+            data = response.data;
+          }
+          displayError({
+            title: "Backend Error",
+            message: `${status}: ${data.message || data}`
+          });
+        });
+    },
+    drawDiagram() {
+      this.coordinatesSelect = null;
+      const chartDiv = document.querySelector(".fairwayprofile");
+      d3.select("svg").remove();
+      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,
+        yScaleLeft,
+        graph
+      } = this.generateCoordinates(svg, height, width);
+      this.drawWaterlevel({
+        graph,
+        xScale,
+        yScaleRight,
+        height,
+        width
+      });
+      if (currentData) {
+        this.drawProfile({
+          graph,
+          xScale,
+          yScaleRight,
+          currentData,
+          height,
+          width,
+          color: GROUND_COLOR,
+          strokeColor: "black",
+          opacity: 1
+        });
+      }
+      if (additionalData) {
+        this.drawProfile({
+          graph,
+          xScale,
+          yScaleRight,
+          currentData: additionalData,
+          height,
+          width,
+          color: GROUND_COLOR,
+          strokeColor: "#943007",
+          opacity: 0.6
+        });
+      }
+      this.drawLabels({
+        graph,
+        xScale,
+        yScaleLeft,
+        currentData,
+        height,
+        width
+      });
+      this.drawFairway({
+        graph,
+        xScale,
+        yScaleRight,
+        currentData,
+        height,
+        width
+      });
+    },
+    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);
+      }
+    },
+    onCopyCoordinates() {
+      displayInfo({
+        title: "Success",
+        message: "Coordinates copied to clipboard!"
+      });
+    },
+    applyManualCoordinates() {
+      const coordinates = this.coordinatesInput
+        .split(",")
+        .map(coord => parseFloat(coord.trim()));
+      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
+        const cutLayer = this.getLayerByName("Cut Tool");
+        cutLayer.data.getSource().clear();
+        const cut = new Feature({
+          geometry: new LineString([
+            [coordinates[0], coordinates[1]],
+            [coordinates[2], coordinates[3]]
+          ]).transform("EPSG:4326", "EPSG:3857")
+        });
+        cutLayer.data.getSource().addFeature(cut);
+
+        // draw diagram
+        this.$store.dispatch("fairwayprofile/cut", cut);
+      } else {
+        displayError({
+          title: "Invalid input",
+          message:
+            "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]
+      };
+      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: "Coordinates saved!",
+        message:
+          'You can now select these coordinates from the "Saved cross profiles" menu to restore this cross profile.'
+      });
+    }
+  },
+  mounted() {
+    this.drawDiagram();
+  }
+};
+</script>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/client/src/components/map/fairway/Infobar.vue	Thu Nov 22 07:08:38 2018 +0100
@@ -0,0 +1,45 @@
+<template>
+    <div v-if="selectedSurvey && !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">
+            <h6 class="my-auto px-2">
+                {{ selectedBottleneck }}
+                ({{ selectedSurvey.date_info }})
+            </h6>
+            <i class="fa fa-angle-up py-2 px-2 border-left" @click="$store.commit('application/showSplitscreen', true)" v-if="Object.keys(currentProfile).length"></i>
+            <i class="fa fa-close text-danger py-2 px-2 border-left" @click="$store.dispatch('fairwayprofile/clearSelection');"></i>
+        </div>
+    </div>
+</template>
+
+<style lang="sass" scoped>
+.infobar
+  height: $icon-width
+  z-index: 2
+</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/map/fairway/Surveys.vue	Thu Nov 22 07:08:38 2018 +0100
@@ -0,0 +1,50 @@
+<template>
+    <div class="box expanded ui-element rounded bg-white ml-auto mr-3 mb-3 text-nowrap" v-if="selectedBottleneck && surveys && !selectedSurvey">
+        <div style="width: 15rem">
+            <h6 class="mb-0 py-2 px-3 border-bottom d-flex align-items-center bg-info text-white">
+              {{ selectedBottleneck }}
+              <i class="fa fa-times ml-auto" @click="$store.dispatch('fairwayprofile/clearSelection');"></i>
+            </h6>
+            <div class="p-3">
+                <div
+                    v-for="(survey, i) of surveys"
+                    :key="survey.data_info"
+                    :class="{ 'mt-1': i }"
+                    @click.prevent="$store.commit('bottlenecks/setSelectedSurvey', survey)"
+                >
+                    <a href="#" @click.prevent>{{ survey.date_info }}</a>
+                </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):
+ * Thomas Junk <thomas.junk@intevation.de>
+ * Markus Kottländer <markus.kottlaender@intevation.de>
+ */
+import { mapState } from "vuex";
+
+export default {
+  name: "surveys",
+  computed: {
+    ...mapState("bottlenecks", [
+      "selectedBottleneck",
+      "surveys",
+      "selectedSurvey"
+    ])
+  }
+};
+</script>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/client/src/components/map/imports/Importqueue.vue	Thu Nov 22 07:08:38 2018 +0100
@@ -0,0 +1,170 @@
+<template>
+    <div class="d-flex flex-row">
+        <div :class="spacerStyle"></div>
+        <div class="mt-3 mx-auto">
+            <div class="card importqueuecard">
+                <div class="card-header shadow-sm text-white bg-info mb-3">Importqueue</div>
+                <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">
+                                        <i class="fa fa-search"></i>
+                                    </span>
+                                </div>
+                                <input
+                                    type="text"
+                                    class="form-control"
+                                    placeholder=""
+                                    aria-label="Search"
+                                    aria-describedby="search"
+                                >
+                            </div>
+                            <div class="filters">
+                                <button
+                                    @click="setFilter('successful')"
+                                    :class="successfulStyle"
+                                >Successful</button>
+                                <button @click="setFilter('failed')" :class="failedStyle">Failed</button>
+                                <button @click="setFilter('pending')" :class="pendingStyle">Pending</button>
+                            </div>
+                        </div>
+                        <table class="table">
+                            <thead>
+                                <tr>
+                                    <th>Enqueued</th>
+                                    <th>Kind</th>
+                                    <th>User</th>
+                                    <th>State</th>
+                                </tr>
+                            </thead>
+                            <tbody>
+                                <tr v-for="job in imports" :key="job.id">
+                                    <td>{{job.enqueued}}</td>
+                                    <td>{{job.kind}}</td>
+                                    <td>{{job.user}}</td>
+                                    <td>{{job.state}}</td>
+                                </tr>
+                            </tbody>
+                        </table>
+                    </div>
+                </div>
+            </div>
+        </div>
+    </div>
+</template>
+
+<script>
+import { displayError } from "../../../lib/errors.js";
+import { mapState } from "vuex";
+
+export default {
+  name: "importqueue",
+  data() {
+    return {
+      successful: false,
+      failed: false,
+      pending: false
+    };
+  },
+  methods: {
+    setFilter(name) {
+      this[name] = !this[name];
+      const allSet = this.successful && this.failed && this.pending;
+      if (allSet) {
+        this.all = false;
+        this.successful = false;
+        this.failed = false;
+        this.pending = false;
+      }
+    }
+  },
+  computed: {
+    ...mapState("imports", ["imports"]),
+    ...mapState("application", ["showSidebar"]),
+    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
+      };
+    }
+  },
+  mounted() {
+    this.$store.dispatch("imports/getImports").catch(error => {
+      const { status, data } = error.response;
+      displayError({
+        title: "Backend Error",
+        message: `${status}: ${data.message || data}`
+      });
+    });
+  }
+};
+</script>
+
+<style lang="sass" scoped>
+.spacer
+  height: 100vh
+
+.spacer-collapsed
+  min-width: $icon-width + $offset
+  transition: $transition-fast
+
+.spacer-expanded
+  min-width: $sidebar-width + $offset
+
+.importqueuecard
+  width: 80vw
+  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: 50%
+</style>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/client/src/components/map/imports/Imports.vue	Thu Nov 22 07:08:38 2018 +0100
@@ -0,0 +1,260 @@
+<template>
+    <div>
+        <h6 class="mb-0 py-2 px-3 border-bottom d-flex align-items-center bg-info text-white">
+          <i class="fa fa-upload mr-2"></i>
+          Import Soundingresults
+        </h6>
+        <hr class="mr-auto ml-auto mb-0 w-90">
+        <div v-if="editState" class="ml-auto mr-auto mt-4 w-90">
+            <div class="d-flex flex-row input-group mb-4">
+                <div class="offset-r">
+                    <label for="bottleneck" class="label-text" id="bottlenecklabel">Bottleneck</label>
+                </div>
+                <input
+                    id="bottleneck"
+                    type="text"
+                    class="form-control"
+                    placeholder="Name of Bottleneck"
+                    aria-label="bottleneck"
+                    aria-describedby="bottlenecklabel"
+                    v-model="bottleneck"
+                >
+            </div>
+            <div class="d-flex flex-row input-group mb-4">
+                <div class="offset-r">
+                    <label class="label-text" for="importdate" id="importdatelabel">Date</label>
+                </div>
+                <input
+                    id="importdate"
+                    type="date"
+                    class="form-control"
+                    placeholder="Date of import"
+                    aria-label="bottleneck"
+                    aria-describedby="bottlenecklabel"
+                    v-model="importDate"
+                >
+            </div>
+            <div class="d-flex flex-row input-group mb-4">
+                <div class="offset-r">
+                    <label class="label-text" for="depthreference">Depth reference</label>
+                </div>
+                <select v-model="depthReference" class="custom-select" id="depthreference">
+                    <option
+                        v-for="option in this.$options.depthReferenceOptions"
+                        :key="option"
+                    >{{option}}</option>
+                </select>
+            </div>
+        </div>
+        <div class="w-90 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
+                        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"
+                >Download Meta.json</a>
+                <button
+                    v-if="editState"
+                    @click="deleteTempData"
+                    class="btn btn-danger"
+                    type="button"
+                >Cancel Upload</button>
+                <button
+                    :disabled="disableUpload"
+                    @click="submit"
+                    class="btn btn-info"
+                    type="button"
+                >{{uploadState?"Upload":"Confirm"}}</button>
+            </div>
+        </div>
+    </div>
+</template>
+
+<script>
+import { HTTP } from "../../../lib/http";
+import { displayError, displayInfo } from "../../../lib/errors.js";
+
+const defaultLabel = "Choose .zip-file";
+const IMPORTSTATE = { UPLOAD: "UPLOAD", EDIT: "EDIT" };
+
+export default {
+  name: "imports",
+  data() {
+    return {
+      importState: IMPORTSTATE.UPLOAD,
+      depthReference: "",
+      bottleneck: "",
+      importDate: "",
+      uploadLabel: defaultLabel,
+      uploadFile: null,
+      disableUpload: false,
+      token: null
+    };
+  },
+  methods: {
+    initialState() {
+      this.importState = IMPORTSTATE.UPLOAD;
+      this.depthReference = "";
+      this.bottleneck = "";
+      this.importDate = "";
+      this.uploadLabel = defaultLabel;
+      this.uploadFile = null;
+      this.disableUpload = false;
+      this.token = null;
+    },
+    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: "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 => {
+          const { bottleneck, date } = response.data.meta;
+          const depthReference = response.data.meta["depth-reference"];
+          this.importState = IMPORTSTATE.EDIT;
+          this.bottleneck = bottleneck;
+          this.depthReference = depthReference;
+          this.importDate = new Date(date).toISOString().split("T")[0];
+          this.token = response.data.token;
+        })
+        .catch(error => {
+          const { status, data } = error.response;
+          const messages = data.messages ? data.messages.join(", ") : "";
+          displayError({
+            title: "Backend Error",
+            message: `${status}: ${messages}`
+          });
+        });
+    },
+    confirm() {
+      let formData = new FormData();
+      formData.append("token", this.token);
+      ["bottleneck", "importDate", "depthReference"].forEach(x => {
+        if (this[x]) formData.append(x, this[x]);
+      });
+      HTTP.post("/imports/soundingresult", formData, {
+        headers: {
+          "X-Gemma-Auth": localStorage.getItem("token"),
+          "Content-Type": "multipart/form-data"
+        }
+      })
+        .then(() => {
+          displayInfo({
+            title: "Import",
+            message: "Starting import for " + this.bottleneck
+          });
+          this.initialState();
+        })
+        .catch(error => {
+          const { status, data } = error.response;
+          displayError({
+            title: "Backend Error",
+            message: `${status}: ${data.message || data}`
+          });
+        });
+    }
+  },
+  computed: {
+    editState() {
+      return this.importState === IMPORTSTATE.EDIT;
+    },
+    uploadState() {
+      return this.importState === IMPORTSTATE.UPLOAD;
+    },
+    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="sass" scoped>
+.offset-r
+  margin-right: $large-offset
+
+.buttons button
+  margin-left: $offset !important
+
+.label-text
+  width: 10rem
+  text-align: left
+  line-height: 2.25rem
+</style>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/client/src/components/map/layers/Layers.vue	Thu Nov 22 07:08:38 2018 +0100
@@ -0,0 +1,56 @@
+<template>
+    <div :class="['box ui-element rounded bg-white mb-auto text-nowrap', { expanded: showLayers }]">
+        <div style="width: 20rem">
+            <h6 class="mb-0 py-2 px-3 border-bottom d-flex align-items-center bg-info text-white">
+              <i class="fa fa-th-list mr-2"></i>
+              Layers
+              <i class="fa fa-times ml-auto" @click="$store.commit('application/showLayers', false)"></i>
+            </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/map/layers/Layerselect.vue	Thu Nov 22 07:08:38 2018 +0100
@@ -0,0 +1,75 @@
+<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">{{layername}}</label>
+        </div>
+        <div v-if="isVisible && (layername == 'Bottleneck isolines')">
+            <img class="rounded my-1 d-block" :src="isolinesLegendImgUrl">
+        </div>
+    </div>
+</template>
+
+<style lang="sass" 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/map/layers/LegendElement.vue	Thu Nov 22 07:08:38 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="sass" scoped>
+.legendelement
+  max-height: 1.5rem
+  width: 2rem
+</style>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/client/src/components/map/toolbar/Cuttool.vue	Thu Nov 22 07:08:38 2018 +0100
@@ -0,0 +1,76 @@
+<template>
+  <div @click="toggleCutTool" class="toolbar-button">
+      <i :class="['fa fa-area-chart', { inverted: cutTool && cutTool.getActive(), grey: !selectedSurvey }]"></i>
+  </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";
+import Draw from "ol/interaction/Draw.js";
+import { Stroke, Style, Circle, Fill } from "ol/style.js";
+
+export default {
+  name: "cuttool",
+  computed: {
+    ...mapGetters("map", ["getLayerByName"]),
+    ...mapState("map", ["lineTool", "polygonTool", "cutTool", "openLayersMap"]),
+    ...mapState("bottlenecks", ["selectedSurvey"])
+  },
+  methods: {
+    toggleCutTool() {
+      console.log(this.selectedSurvey);
+      if (this.selectedSurvey) {
+        this.cutTool.setActive(!this.cutTool.getActive());
+        this.lineTool.setActive(false);
+        this.polygonTool.setActive(false);
+        this.$store.commit("map/setCurrentMeasurement", null);
+      }
+    },
+    cutEnd(event) {
+      this.$store.dispatch("fairwayprofile/cut", event.feature);
+    }
+  },
+  created() {
+    if (!this.cutTool) {
+      const cutVectorSrc = this.getLayerByName("Cut Tool").data.getSource();
+      const cutTool = new Draw({
+        source: cutVectorSrc,
+        type: "LineString",
+        maxPoints: 2,
+        style: new Style({
+          stroke: new Stroke({
+            color: "#444",
+            width: 2,
+            lineDash: [7, 7]
+          }),
+          image: new Circle({
+            fill: new Fill({ color: "#333" }),
+            stroke: new Stroke({ color: "#fff", width: 1.5 }),
+            radius: 6
+          })
+        })
+      });
+      cutTool.setActive(false);
+      cutTool.on("drawstart", () => {
+        cutVectorSrc.clear();
+      });
+      cutTool.on("drawend", this.cutEnd);
+      this.$store.commit("map/cutTool", cutTool);
+      this.openLayersMap.addInteraction(cutTool);
+    }
+  }
+};
+</script>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/client/src/components/map/toolbar/Identify.vue	Thu Nov 22 07:08:38 2018 +0100
@@ -0,0 +1,29 @@
+<template>
+    <div @click="$store.commit('application/showIdentify', !showIdentify)" class="toolbar-button">
+        <i :class="['fa fa-info', {inverted: showIdentify}]"></i>
+    </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"])
+  }
+};
+</script>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/client/src/components/map/toolbar/Layers.vue	Thu Nov 22 07:08:38 2018 +0100
@@ -0,0 +1,29 @@
+<template>
+    <div @click="$store.commit('application/showLayers', !showLayers)" class="toolbar-button">
+        <i :class="['fa fa-th-list', {inverted: showLayers}]"></i>
+    </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/map/toolbar/Linetool.vue	Thu Nov 22 07:08:38 2018 +0100
@@ -0,0 +1,70 @@
+<template>
+    <div @click="toggleLineTool" class="toolbar-button">
+        <i :class="['fa fa-pencil', {inverted: lineTool && lineTool.getActive()}]"></i>
+    </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";
+import { getLength } from "ol/sphere.js";
+import Draw from "ol/interaction/Draw.js";
+
+export default {
+  name: "linetool",
+  computed: {
+    ...mapGetters("map", ["getLayerByName"]),
+    ...mapState("map", ["lineTool", "polygonTool", "cutTool", "openLayersMap"])
+  },
+  methods: {
+    toggleLineTool() {
+      this.lineTool.setActive(!this.lineTool.getActive());
+      this.polygonTool.setActive(false);
+      this.cutTool.setActive(false);
+      this.$store.commit("map/setCurrentMeasurement", null);
+      this.getLayerByName("Draw Tool")
+        .data.getSource()
+        .clear();
+    },
+    lineEnd(event) {
+      const length = getLength(event.feature.getGeometry());
+      this.$store.commit("map/setCurrentMeasurement", {
+        quantity: "Length",
+        unitSymbol: "m",
+        value: Math.round(length * 10) / 10
+      });
+      this.$store.commit("application/showIdentify", true);
+    }
+  },
+  created() {
+    if (!this.lineTool) {
+      const drawVectorSrc = this.getLayerByName("Draw Tool").data.getSource();
+      const lineTool = new Draw({
+        source: drawVectorSrc,
+        type: "LineString",
+        maxPoints: 2
+      });
+      lineTool.setActive(false);
+      lineTool.on("drawstart", () => {
+        drawVectorSrc.clear();
+        this.$store.commit("map/setCurrentMeasurement", null);
+      });
+      lineTool.on("drawend", this.lineEnd);
+      this.$store.commit("map/lineTool", lineTool);
+      this.openLayersMap.addInteraction(lineTool);
+    }
+  }
+};
+</script>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/client/src/components/map/toolbar/Pdftool.vue	Thu Nov 22 07:08:38 2018 +0100
@@ -0,0 +1,29 @@
+<template>
+    <div @click="$store.commit('application/showPdfTool', !showPdfTool)" class="toolbar-button">
+        <i :class="['fa fa-file-pdf-o', {inverted: showPdfTool}]"></i>
+    </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/map/toolbar/Polygontool.vue	Thu Nov 22 07:08:38 2018 +0100
@@ -0,0 +1,73 @@
+<template>
+    <div @click="togglePolygonTool" class="toolbar-button">
+        <i :class="['fa fa-edit', {inverted: polygonTool && polygonTool.getActive()}]"></i>
+    </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";
+import { getArea } from "ol/sphere.js";
+import Draw from "ol/interaction/Draw.js";
+
+export default {
+  name: "polygontool",
+  computed: {
+    ...mapGetters("map", ["getLayerByName"]),
+    ...mapState("map", ["lineTool", "polygonTool", "cutTool", "openLayersMap"])
+  },
+  methods: {
+    togglePolygonTool() {
+      this.polygonTool.setActive(!this.polygonTool.getActive());
+      this.lineTool.setActive(false);
+      this.cutTool.setActive(false);
+      this.$store.commit("map/setCurrentMeasurement", null);
+      this.getLayerByName("Draw Tool")
+        .data.getSource()
+        .clear();
+    },
+    polygonEnd(event) {
+      const areaSize = getArea(event.feature.getGeometry());
+      this.$store.commit("map/setCurrentMeasurement", {
+        quantity: "Area",
+        unitSymbol: areaSize > 100000 ? "km²" : "m²",
+        value:
+          areaSize > 100000
+            ? Math.round(areaSize / 1000) / 1000 // convert into 1 km² == 1000*1000 m² and round to 1000 m²
+            : Math.round(areaSize)
+      });
+      this.$store.commit("application/showIdentify", true);
+    }
+  },
+  created() {
+    if (!this.polygonTool) {
+      const drawVectorSrc = this.getLayerByName("Draw Tool").data.getSource();
+      const polygonTool = new Draw({
+        source: drawVectorSrc,
+        type: "Polygon",
+        maxPoints: 50
+      });
+      polygonTool.setActive(false);
+      polygonTool.on("drawstart", () => {
+        drawVectorSrc.clear();
+        this.$store.commit("map/setCurrentMeasurement", null);
+      });
+      polygonTool.on("drawend", this.polygonEnd);
+      this.$store.commit("map/polygonTool", polygonTool);
+      this.openLayersMap.addInteraction(polygonTool);
+    }
+  }
+};
+</script>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/client/src/components/map/toolbar/Toolbar.vue	Thu Nov 22 07:08:38 2018 +0100
@@ -0,0 +1,96 @@
+<template>
+    <div class="ml-2">
+        <div :class="'toolbar toolbar-' + (expandToolbar ? 'expanded' : 'collapsed')">
+            <Identify></Identify>
+            <Layers></Layers>
+            <Cuttool></Cuttool>
+            <Linetool></Linetool>
+            <Polygontool></Polygontool>
+            <Pdftool></Pdftool>
+        </div>
+        <div @click="$store.commit('application/expandToolbar', !expandToolbar)" class="toolbar-button bg-info text-white">
+            <i :class="'fa fa-angle-' + (expandToolbar ? 'up' : 'down')"></i>
+        </div>
+    </div>
+</template>
+
+<style lang="sass">
+// not scoped to affect nested components
+// doen't work when put in application/assets/application.sass... why??? o_O
+.toolbar
+  overflow: hidden
+  transition: max-height 0.4s
+
+.toolbar-collapsed
+  max-height: (3 * $icon-height) + (3 * $offset)
+
+.toolbar-expanded
+  max-height: 100%
+
+.toolbar-button
+  opacity: $slight-transparent
+  color: #666
+  height: $icon-width
+  width: $icon-height
+  align-items: center
+  justify-content: center
+  display: flex
+  background: #fff
+  margin-bottom: $offset
+  border-radius: $border-radius
+  box-shadow: $shadow-xs
+  z-index: 2
+  pointer-events: auto
+  .inverted
+    color: $color-info
+  .grey
+    color: #ddd
+</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"),
+    Cuttool: () => import("./Cuttool.vue"),
+    Pdftool: () => import("./Pdftool.vue")
+  },
+  computed: {
+    ...mapGetters("map", ["getLayerByName"]),
+    ...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.getLayerByName("Draw Tool")
+          .data.getSource()
+          .clear();
+      }
+    });
+  }
+};
+</script>
--- a/client/src/fairway/Fairwayprofile.vue	Wed Nov 21 16:47:02 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,579 +0,0 @@
-<template>
-    <div :class="['position-relative', {show: showSplitscreen}]" v-if="Object.keys(currentProfile).length">
-        <button
-            class="rounded-bottom bg-white border-0 position-absolute splitscreen-toggle shadow-sm"
-            @click="$store.commit('application/showSplitscreen', false)"
-            v-if="showSplitscreen">
-            <i class="fa fa-angle-down"></i>
-        </button>
-        <button
-            class="rounded-bottom bg-white border-0 position-absolute clear-selection shadow-sm"
-            @click="$store.dispatch('fairwayprofile/clearSelection');"
-            v-if="showSplitscreen">
-            <i class="fa fa-times text-danger"></i>
-        </button>
-        <div class="profile bg-white position-relative d-flex flex-column pr-5">
-            <h5 class="mb-0 mt-2">{{ selectedBottleneck }} ({{ selectedSurvey.date_info }})</h5>
-            <div class="d-flex flex-fill">
-                <div class="fairwayprofile m-3 mt-0 bg-white flex-grow-1"></div>
-                <div class="additionalsurveys d-flex flex-column">
-                    <small>
-                        Additional Surveys
-                        <select v-model="additionalSurvey" class="form-control form-control-sm">
-                            <option value="">None</option>
-                            <option
-                                v-for="survey in additionalSurveys"
-                                :key="survey.date_info"
-                                :value="survey"
-                            >{{survey.date_info}}</option>
-                        </select>
-                        <hr>
-                        <div class="d-flex text-left mb-2">
-                            <div class="text-nowrap mr-1">
-                                <b>Start:</b>
-                                <br>
-                                Lat: {{ startPoint[1] }}
-                                <br>
-                                Lon: {{ startPoint[0] }}
-                            </div>
-                            <div class="text-nowrap ml-1">
-                                <b>End:</b>
-                                <br>
-                                Lat: {{ endPoint[1] }}
-                                <br>
-                                Lon: {{ endPoint[0] }}
-                            </div>
-                            <button class="btn btn-outline-secondary btn-sm ml-2 mt-auto"
-                                    @click="showLabelInput = !showLabelInput">
-                              <i :class="'fa fa-' + (showLabelInput ? 'times' : 'save')"></i>
-                            </button>
-                            <button v-clipboard:copy="coordinatesForClipboard"
-                                    v-clipboard:success="onCopyCoordinates"
-                                    class="btn btn-outline-secondary btn-sm ml-2 mt-auto">
-                              <i class="fa fa-copy"></i>
-                            </button>
-                        </div>
-                        <div v-if="showLabelInput">
-                          Enter label for cross profile:
-                          <div class="position-relative">
-                              <input class="form-control form-control-sm pr-5" v-model="cutLabel" /><br>
-                              <button class="btn btn-sm btn-outline-secondary position-absolute"
-                                      @click="saveCut"
-                                      v-if="cutLabel"
-                                      style="top: 0; right: 0;">
-                                <i class="fa fa-check"></i>
-                              </button>
-                          </div>
-                        </div>
-                        Saved cross profiles:
-                        <select class="form-control form-control-sm mb-2" v-model="coordinatesSelect">
-                            <option></option>
-                            <option v-for="(cut, index) in previousCuts" :value="cut.coordinates" :key="index">
-                              {{ cut.label }}
-                            </option>
-                        </select>
-                        Enter coordinates manually:
-                        <div class="position-relative">
-                            <input class="form-control form-control-sm pr-5" placeholder="Lat,Lon,Lat,Lon" v-model="coordinatesInput" /><br>
-                            <button class="btn btn-sm btn-outline-secondary position-absolute" 
-                                    @click="applyManualCoordinates"
-                                    style="top: 0; right: 0;"
-                                    v-if="coordinatesInput">
-                              <i class="fa fa-check"></i>
-                            </button>
-                        </div>
-                    </small>
-                </div>
-            </div>
-        </div>
-    </div>
-</template>
-
-<style lang="sass" scoped>
-.profile
-  width: 100vw
-  height: 0
-  overflow: hidden
-  z-index: 2
-
-.splitscreen-toggle,
-.clear-selection
-  right: $icon-width + $offset
-  width: $icon-width
-  height: $icon-height
-  margin-top: 2px
-  z-index: 3
-  outline: none
-
-.clear-selection
-  right: $offset
-
-.show
-  .profile
-    height: 50vh
-
-.waterlevelselection
-  margin-top: $large-offset
-  margin-right: $large-offset
-
-.additionalsurveys
-  margin-top: $large-offset
-  margin-bottom: auto
-  margin-right: $large-offset
-  margin-left: auto
-  max-width: 300px
-
-.additionalsurveys input
-  margin-right: $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 * as d3 from "d3";
-import { mapState, mapGetters } from "vuex";
-import { displayError, displayInfo } from "../application/lib/errors.js";
-import Feature from "ol/Feature";
-import LineString from "ol/geom/LineString";
-
-const GROUND_COLOR = "#4A2F06";
-
-export default {
-  name: "fairwayprofile",
-  props: [
-    "width",
-    "height",
-    "xScale",
-    "yScaleLeft",
-    "yScaleRight",
-    "margin",
-    "additionalSurveys"
-  ],
-  data() {
-    return {
-      wait: false,
-      coordinatesInput: "",
-      coordinatesSelect: null,
-      cutLabel: "",
-      showLabelInput: false
-    };
-  },
-  computed: {
-    ...mapGetters("map", ["getLayerByName"]),
-    ...mapState("application", ["showSplitscreen"]),
-    ...mapState("fairwayprofile", [
-      "startPoint",
-      "endPoint",
-      "currentProfile",
-      "minAlt",
-      "maxAlt",
-      "totalLength",
-      "fairwayCoordinates",
-      "waterLevels",
-      "selectedWaterLevel",
-      "previousCuts"
-    ]),
-    ...mapState("bottlenecks", ["selectedBottleneck", "selectedSurvey"]),
-    additionalSurvey: {
-      get() {
-        return this.$store.getters["fairwayprofile/additionalSurvey"];
-      },
-      set(value) {
-        this.$store.commit("fairwayprofile/setAdditionalSurvey", value);
-        this.selectAdditionalSurveyData();
-      }
-    },
-    currentData() {
-      if (
-        !this.selectedSurvey ||
-        !this.currentProfile.hasOwnProperty(this.selectedSurvey.date_info)
-      )
-        return [];
-      return this.currentProfile[this.selectedSurvey.date_info];
-    },
-    additionalData() {
-      if (
-        !this.additionalSurvey ||
-        !this.currentProfile.hasOwnProperty(this.additionalSurvey.date_info)
-      )
-        return [];
-      return this.currentProfile[this.additionalSurvey.date_info];
-    },
-    waterColor() {
-      const result = this.waterLevels.find(
-        x => x.level === this.selectedWaterLevel
-      );
-      return result.color;
-    },
-    coordinatesForClipboard() {
-      return (
-        this.startPoint[1] +
-        "," +
-        this.startPoint[0] +
-        "," +
-        this.endPoint[1] +
-        "," +
-        this.endPoint[0]
-      );
-    }
-  },
-  watch: {
-    showSplitscreen() {
-      this.drawDiagram();
-    },
-    currentData() {
-      this.drawDiagram();
-    },
-    width() {
-      this.drawDiagram();
-    },
-    height() {
-      this.drawDiagram();
-    },
-    waterLevels() {
-      this.drawDiagram();
-    },
-    selectedWaterLevel() {
-      this.drawDiagram();
-    },
-    fairwayCoordinates() {
-      this.drawDiagram();
-    },
-    selectedBottleneck() {
-      this.$store.dispatch("fairwayprofile/previousCuts");
-      this.cutLabel =
-        this.selectedBottleneck + " (" + new Date().toISOString() + ")";
-    },
-    coordinatesSelect(newValue) {
-      if (newValue) {
-        this.applyCoordinates(newValue);
-      }
-    }
-  },
-  methods: {
-    selectAdditionalSurveyData() {
-      if (
-        !this.additionalSurvey ||
-        this.wait ||
-        this.currentProfile[this.additionalSurvey.date_info]
-      ) {
-        this.drawDiagram();
-        return;
-      }
-      this.$store
-        .dispatch("fairwayprofile/loadProfile", this.additionalSurvey)
-        .then(() => {
-          this.wait = false;
-        })
-        .catch(error => {
-          this.wait = false;
-          let status = "ERROR";
-          let data = error;
-          const response = error.response;
-          if (response) {
-            status = response.status;
-            data = response.data;
-          }
-          displayError({
-            title: "Backend Error",
-            message: `${status}: ${data.message || data}`
-          });
-        });
-    },
-    drawDiagram() {
-      this.coordinatesSelect = null;
-      const chartDiv = document.querySelector(".fairwayprofile");
-      d3.select("svg").remove();
-      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,
-        yScaleLeft,
-        graph
-      } = this.generateCoordinates(svg, height, width);
-      this.drawWaterlevel({
-        graph,
-        xScale,
-        yScaleRight,
-        height,
-        width
-      });
-      if (currentData) {
-        this.drawProfile({
-          graph,
-          xScale,
-          yScaleRight,
-          currentData,
-          height,
-          width,
-          color: GROUND_COLOR,
-          strokeColor: "black",
-          opacity: 1
-        });
-      }
-      if (additionalData) {
-        this.drawProfile({
-          graph,
-          xScale,
-          yScaleRight,
-          currentData: additionalData,
-          height,
-          width,
-          color: GROUND_COLOR,
-          strokeColor: "#943007",
-          opacity: 0.6
-        });
-      }
-      this.drawLabels({
-        graph,
-        xScale,
-        yScaleLeft,
-        currentData,
-        height,
-        width
-      });
-      this.drawFairway({
-        graph,
-        xScale,
-        yScaleRight,
-        currentData,
-        height,
-        width
-      });
-    },
-    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);
-      }
-    },
-    onCopyCoordinates() {
-      displayInfo({
-        title: "Success",
-        message: "Coordinates copied to clipboard!"
-      });
-    },
-    applyManualCoordinates() {
-      const coordinates = this.coordinatesInput
-        .split(",")
-        .map(coord => parseFloat(coord.trim()));
-      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
-        const cutLayer = this.getLayerByName("Cut Tool");
-        cutLayer.data.getSource().clear();
-        const cut = new Feature({
-          geometry: new LineString([
-            [coordinates[0], coordinates[1]],
-            [coordinates[2], coordinates[3]]
-          ]).transform("EPSG:4326", "EPSG:3857")
-        });
-        cutLayer.data.getSource().addFeature(cut);
-
-        // draw diagram
-        this.$store.dispatch("fairwayprofile/cut", cut);
-      } else {
-        displayError({
-          title: "Invalid input",
-          message:
-            "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]
-      };
-      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: "Coordinates saved!",
-        message:
-          'You can now select these coordinates from the "Saved cross profiles" menu to restore this cross profile.'
-      });
-    }
-  },
-  mounted() {
-    this.drawDiagram();
-  }
-};
-</script>
--- a/client/src/fairway/Infobar.vue	Wed Nov 21 16:47:02 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,45 +0,0 @@
-<template>
-    <div v-if="selectedSurvey && !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">
-            <h6 class="my-auto px-2">
-                {{ selectedBottleneck }}
-                ({{ selectedSurvey.date_info }})
-            </h6>
-            <i class="fa fa-angle-up py-2 px-2 border-left" @click="$store.commit('application/showSplitscreen', true)" v-if="Object.keys(currentProfile).length"></i>
-            <i class="fa fa-close text-danger py-2 px-2 border-left" @click="$store.dispatch('fairwayprofile/clearSelection');"></i>
-        </div>
-    </div>
-</template>
-
-<style lang="sass" scoped>
-.infobar
-  height: $icon-width
-  z-index: 2
-</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/fairway/Surveys.vue	Wed Nov 21 16:47:02 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,50 +0,0 @@
-<template>
-    <div class="box expanded ui-element rounded bg-white ml-auto mr-3 mb-3 text-nowrap" v-if="selectedBottleneck && surveys && !selectedSurvey">
-        <div style="width: 15rem">
-            <h6 class="mb-0 py-2 px-3 border-bottom d-flex align-items-center bg-info text-white">
-              {{ selectedBottleneck }}
-              <i class="fa fa-times ml-auto" @click="$store.dispatch('fairwayprofile/clearSelection');"></i>
-            </h6>
-            <div class="p-3">
-                <div
-                    v-for="(survey, i) of surveys"
-                    :key="survey.data_info"
-                    :class="{ 'mt-1': i }"
-                    @click.prevent="$store.commit('bottlenecks/setSelectedSurvey', survey)"
-                >
-                    <a href="#" @click.prevent>{{ survey.date_info }}</a>
-                </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):
- * Thomas Junk <thomas.junk@intevation.de>
- * Markus Kottländer <markus.kottlaender@intevation.de>
- */
-import { mapState } from "vuex";
-
-export default {
-  name: "surveys",
-  computed: {
-    ...mapState("bottlenecks", [
-      "selectedBottleneck",
-      "surveys",
-      "selectedSurvey"
-    ])
-  }
-};
-</script>
--- a/client/src/identify/Identify.vue	Wed Nov 21 16:47:02 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,86 +0,0 @@
-<template>
-    <div :class="['box ui-element rounded bg-white mb-auto text-nowrap', { expanded: showIdentify }]">
-        <div style="width: 20rem">
-            <h6 class="mb-0 py-2 px-3 border-bottom d-flex align-items-center bg-info text-white">
-              <i class="fa fa-info mr-2"></i>
-              Identified
-              <i class="fa fa-times ml-auto" @click="$store.commit('application/showIdentify', false)"></i>
-            </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 }">
-                        <b>{{ feature.getId().replace(/[.][^.]*$/,"") /* cut away everything from the last . to the end */}}:</b>
-                        <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">
-                    No features identified.
-                </div>
-            </div>
-            <div class="versioninfo border-top p-3 text-left">
-                gemma
-                <a href="https://hg.intevation.de/gemma/file/tip">source-code</a>
-                {{ versionStr }}
-                <br>Some data ©
-                <a href="https://www.openstreetmap.org/copyright">OpenSteetMap</a>contributors
-            </div>
-        </div>
-    </div>
-</template>
-
-<style lang="sass" scoped>
-.features
-  max-height: 19rem
-  overflow-y: auto
-
-.versioninfo
-  font-size: 60%
-</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/imports/Importqueue.vue	Wed Nov 21 16:47:02 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,170 +0,0 @@
-<template>
-    <div class="d-flex flex-row">
-        <div :class="spacerStyle"></div>
-        <div class="mt-3 mx-auto">
-            <div class="card importqueuecard">
-                <div class="card-header shadow-sm text-white bg-info mb-3">Importqueue</div>
-                <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">
-                                        <i class="fa fa-search"></i>
-                                    </span>
-                                </div>
-                                <input
-                                    type="text"
-                                    class="form-control"
-                                    placeholder=""
-                                    aria-label="Search"
-                                    aria-describedby="search"
-                                >
-                            </div>
-                            <div class="filters">
-                                <button
-                                    @click="setFilter('successful')"
-                                    :class="successfulStyle"
-                                >Successful</button>
-                                <button @click="setFilter('failed')" :class="failedStyle">Failed</button>
-                                <button @click="setFilter('pending')" :class="pendingStyle">Pending</button>
-                            </div>
-                        </div>
-                        <table class="table">
-                            <thead>
-                                <tr>
-                                    <th>Enqueued</th>
-                                    <th>Kind</th>
-                                    <th>User</th>
-                                    <th>State</th>
-                                </tr>
-                            </thead>
-                            <tbody>
-                                <tr v-for="job in imports" :key="job.id">
-                                    <td>{{job.enqueued}}</td>
-                                    <td>{{job.kind}}</td>
-                                    <td>{{job.user}}</td>
-                                    <td>{{job.state}}</td>
-                                </tr>
-                            </tbody>
-                        </table>
-                    </div>
-                </div>
-            </div>
-        </div>
-    </div>
-</template>
-
-<script>
-import { displayError } from "../application/lib/errors.js";
-import { mapState } from "vuex";
-
-export default {
-  name: "importqueue",
-  data() {
-    return {
-      successful: false,
-      failed: false,
-      pending: false
-    };
-  },
-  methods: {
-    setFilter(name) {
-      this[name] = !this[name];
-      const allSet = this.successful && this.failed && this.pending;
-      if (allSet) {
-        this.all = false;
-        this.successful = false;
-        this.failed = false;
-        this.pending = false;
-      }
-    }
-  },
-  computed: {
-    ...mapState("imports", ["imports"]),
-    ...mapState("application", ["showSidebar"]),
-    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
-      };
-    }
-  },
-  mounted() {
-    this.$store.dispatch("imports/getImports").catch(error => {
-      const { status, data } = error.response;
-      displayError({
-        title: "Backend Error",
-        message: `${status}: ${data.message || data}`
-      });
-    });
-  }
-};
-</script>
-
-<style lang="sass" scoped>
-.spacer
-  height: 100vh
-
-.spacer-collapsed
-  min-width: $icon-width + $offset
-  transition: $transition-fast
-
-.spacer-expanded
-  min-width: $sidebar-width + $offset
-
-.importqueuecard
-  width: 80vw
-  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: 50%
-</style>
--- a/client/src/imports/Imports.vue	Wed Nov 21 16:47:02 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,260 +0,0 @@
-<template>
-    <div>
-        <h6 class="mb-0 py-2 px-3 border-bottom d-flex align-items-center bg-info text-white">
-          <i class="fa fa-upload mr-2"></i>
-          Import Soundingresults
-        </h6>
-        <hr class="mr-auto ml-auto mb-0 w-90">
-        <div v-if="editState" class="ml-auto mr-auto mt-4 w-90">
-            <div class="d-flex flex-row input-group mb-4">
-                <div class="offset-r">
-                    <label for="bottleneck" class="label-text" id="bottlenecklabel">Bottleneck</label>
-                </div>
-                <input
-                    id="bottleneck"
-                    type="text"
-                    class="form-control"
-                    placeholder="Name of Bottleneck"
-                    aria-label="bottleneck"
-                    aria-describedby="bottlenecklabel"
-                    v-model="bottleneck"
-                >
-            </div>
-            <div class="d-flex flex-row input-group mb-4">
-                <div class="offset-r">
-                    <label class="label-text" for="importdate" id="importdatelabel">Date</label>
-                </div>
-                <input
-                    id="importdate"
-                    type="date"
-                    class="form-control"
-                    placeholder="Date of import"
-                    aria-label="bottleneck"
-                    aria-describedby="bottlenecklabel"
-                    v-model="importDate"
-                >
-            </div>
-            <div class="d-flex flex-row input-group mb-4">
-                <div class="offset-r">
-                    <label class="label-text" for="depthreference">Depth reference</label>
-                </div>
-                <select v-model="depthReference" class="custom-select" id="depthreference">
-                    <option
-                        v-for="option in this.$options.depthReferenceOptions"
-                        :key="option"
-                    >{{option}}</option>
-                </select>
-            </div>
-        </div>
-        <div class="w-90 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
-                        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"
-                >Download Meta.json</a>
-                <button
-                    v-if="editState"
-                    @click="deleteTempData"
-                    class="btn btn-danger"
-                    type="button"
-                >Cancel Upload</button>
-                <button
-                    :disabled="disableUpload"
-                    @click="submit"
-                    class="btn btn-info"
-                    type="button"
-                >{{uploadState?"Upload":"Confirm"}}</button>
-            </div>
-        </div>
-    </div>
-</template>
-
-<script>
-import { HTTP } from "../application/lib/http";
-import { displayError, displayInfo } from "../application/lib/errors.js";
-
-const defaultLabel = "Choose .zip-file";
-const IMPORTSTATE = { UPLOAD: "UPLOAD", EDIT: "EDIT" };
-
-export default {
-  name: "imports",
-  data() {
-    return {
-      importState: IMPORTSTATE.UPLOAD,
-      depthReference: "",
-      bottleneck: "",
-      importDate: "",
-      uploadLabel: defaultLabel,
-      uploadFile: null,
-      disableUpload: false,
-      token: null
-    };
-  },
-  methods: {
-    initialState() {
-      this.importState = IMPORTSTATE.UPLOAD;
-      this.depthReference = "";
-      this.bottleneck = "";
-      this.importDate = "";
-      this.uploadLabel = defaultLabel;
-      this.uploadFile = null;
-      this.disableUpload = false;
-      this.token = null;
-    },
-    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: "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 => {
-          const { bottleneck, date } = response.data.meta;
-          const depthReference = response.data.meta["depth-reference"];
-          this.importState = IMPORTSTATE.EDIT;
-          this.bottleneck = bottleneck;
-          this.depthReference = depthReference;
-          this.importDate = new Date(date).toISOString().split("T")[0];
-          this.token = response.data.token;
-        })
-        .catch(error => {
-          const { status, data } = error.response;
-          const messages = data.messages ? data.messages.join(", ") : "";
-          displayError({
-            title: "Backend Error",
-            message: `${status}: ${messages}`
-          });
-        });
-    },
-    confirm() {
-      let formData = new FormData();
-      formData.append("token", this.token);
-      ["bottleneck", "importDate", "depthReference"].forEach(x => {
-        if (this[x]) formData.append(x, this[x]);
-      });
-      HTTP.post("/imports/soundingresult", formData, {
-        headers: {
-          "X-Gemma-Auth": localStorage.getItem("token"),
-          "Content-Type": "multipart/form-data"
-        }
-      })
-        .then(() => {
-          displayInfo({
-            title: "Import",
-            message: "Starting import for " + this.bottleneck
-          });
-          this.initialState();
-        })
-        .catch(error => {
-          const { status, data } = error.response;
-          displayError({
-            title: "Backend Error",
-            message: `${status}: ${data.message || data}`
-          });
-        });
-    }
-  },
-  computed: {
-    editState() {
-      return this.importState === IMPORTSTATE.EDIT;
-    },
-    uploadState() {
-      return this.importState === IMPORTSTATE.UPLOAD;
-    },
-    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="sass" scoped>
-.offset-r
-  margin-right: $large-offset
-
-.buttons button
-  margin-left: $offset !important
-
-.label-text
-  width: 10rem
-  text-align: left
-  line-height: 2.25rem
-</style>
--- a/client/src/layers/Layers.vue	Wed Nov 21 16:47:02 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,56 +0,0 @@
-<template>
-    <div :class="['box ui-element rounded bg-white mb-auto text-nowrap', { expanded: showLayers }]">
-        <div style="width: 20rem">
-            <h6 class="mb-0 py-2 px-3 border-bottom d-flex align-items-center bg-info text-white">
-              <i class="fa fa-th-list mr-2"></i>
-              Layers
-              <i class="fa fa-times ml-auto" @click="$store.commit('application/showLayers', false)"></i>
-            </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/layers/Layerselect.vue	Wed Nov 21 16:47:02 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,75 +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">{{layername}}</label>
-        </div>
-        <div v-if="isVisible && (layername == 'Bottleneck isolines')">
-            <img class="rounded my-1 d-block" :src="isolinesLegendImgUrl">
-        </div>
-    </div>
-</template>
-
-<style lang="sass" 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 "../application/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/layers/LegendElement.vue	Wed Nov 21 16:47:02 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="sass" scoped>
-.legendelement
-  max-height: 1.5rem
-  width: 2rem
-</style>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/client/src/lib/errors.js	Thu Nov 22 07:08:38 2018 +0100
@@ -0,0 +1,33 @@
+/*
+ * 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>
+ */
+
+/** facade to wrap calls to vue2-toastr */
+import app from "../main";
+
+const displayError = ({ title, message }) => {
+  app.$toast.error({
+    title: title,
+    message: message
+  });
+};
+
+const displayInfo = ({ title, message }) => {
+  app.$toast.info({
+    title: title,
+    message: message
+  });
+};
+
+export { displayError, displayInfo };
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/client/src/lib/geo.js	Thu Nov 22 07:08:38 2018 +0100
@@ -0,0 +1,208 @@
+/*
+ * 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>
+ */
+
+/**
+ *
+ * Distance calculations
+ * JS transposition of cross.go functions
+ *
+ */
+
+import { GeoJSON } from "ol/format.js";
+import Feature from "ol/Feature";
+import distance from "@turf/distance";
+import {
+  lineString as turfLineString,
+  polygon as turfPolygon
+} from "@turf/helpers";
+import lineIntersect from "@turf/line-intersect";
+
+const EARTHRADIUS = 6378137.0;
+
+/**
+ * Converts to radiant
+ * @param {degree} d
+ */
+const deg2rad = d => {
+  return (d * Math.PI) / 180.0;
+};
+
+/**
+ * Calculates the difference between two points in m
+ *
+ * Points are given with {lat: $lat, lon: $lon}
+ *
+ * @param {object} P1
+ * @param {object} P2
+ */
+const distanceBetween = (P1, P2) => {
+  const dLat = deg2rad(P2.lat - P1.lat);
+  let dLng = Math.abs(deg2rad(P2.lon - P1.lon));
+  if (dLng > Math.PI) {
+    dLng = 2 * Math.PI - dLng;
+  }
+  const x = dLng * Math.cos(deg2rad((P1.lat + P2.lat) / 2.0));
+  return Math.sqrt(dLat * dLat + x * x) * EARTHRADIUS;
+};
+
+/**
+ * Takes a triple of [lat, long, alt] and generates
+ * an object with according attributes
+ * {
+ *  lat: $lat,
+ *  lon: $lon,
+ *  alt: $alt
+ * }
+ *
+ * @param {array} coords
+ */
+const Point = coords => {
+  return {
+    lon: coords[0],
+    lat: coords[1],
+    alt: coords[2]
+  };
+};
+
+/**
+ * Has geoJSON as its input and transforms
+ * given coordinates into points representing
+ * distance from startpoint / altitude information
+ *
+ * a) extracting the minimum altitude
+ * b) extracting the maximum altitude
+ * c) calculating the total length of the given profile
+ * d) transposes the datapoints relative to a given start point
+ *
+ * The calculation of total equals the sum of partial distances between points
+ *
+ * The x-value of a point is equal to the total distance up to this point
+ *
+ * The distance between the last point of the last segment and the end point is added
+ * to the total
+ *
+ * @param {object} geoJSON, startPoint, endPoint
+ */
+const transform = ({ geoJSON, startPoint, endPoint }) => {
+  const lineSegments = geoJSON.geometry.coordinates;
+  let segmentPoints = [];
+  let lengthPolyLine = 0;
+  let referencePoint = Point(startPoint);
+  let minAlt = Math.abs(lineSegments[0][0][2]);
+  let maxAlt = Math.abs(lineSegments[0][0][2]);
+  let currentPoint = null;
+  for (let segment of lineSegments) {
+    let points = [];
+    for (let coordinateTriplet of segment) {
+      currentPoint = Point(coordinateTriplet);
+      lengthPolyLine += distanceBetween(referencePoint, currentPoint);
+      let y = Math.abs(currentPoint.alt);
+      points.push({
+        x: lengthPolyLine,
+        y: y
+      });
+      if (y < minAlt) minAlt = y;
+      if (y > maxAlt) maxAlt = y;
+      referencePoint = currentPoint;
+    }
+    segmentPoints.push(points);
+  }
+  lengthPolyLine += distanceBetween(currentPoint, Point(endPoint));
+  return { segmentPoints, lengthPolyLine, minAlt, maxAlt };
+};
+
+/**
+ * Prepare profile takes geoJSON data in form of
+ * a MultiLineString, e.g.
+ *
+ * {
+ *   type: "Feature",
+ *   geometry: {
+ *   type: "MultiLineString",
+ *   coordinates: [
+ *                 [
+ *                  [16.53593398, 48.14694085, -146.52392755]
+ *                  ...
+ *                  ]]
+ *
+ * and transforms it to a structure representing the number of sections
+ * where data is present with according lengths and the points
+ *
+ * {
+ *  { points:
+ *           [
+ *            [ { x: 0.005798201616417183, y: -146.52419461 },
+ *              { x: 0, y: -146.52394016 }
+ *              ...
+ *            ]
+ *           ]
+ *   lengthPolyLine: 160.06814078495722,
+ *   minAlt: -146.73122231,
+ *   maxAlt: -145.65155866
+ *  }
+ *
+ * @param {object} geoJSON
+ */
+const prepareProfile = ({ geoJSON, startPoint, endPoint }) => {
+  const { segmentPoints, lengthPolyLine, minAlt, maxAlt } = transform({
+    geoJSON,
+    startPoint,
+    endPoint
+  });
+  return {
+    points: segmentPoints,
+    lengthPolyLine: lengthPolyLine,
+    minAlt: minAlt,
+    maxAlt: maxAlt
+  };
+};
+
+const generateFeatureRequest = (profileLine, bottleneck_id, date_info) => {
+  const feature = new Feature({
+    geometry: profileLine,
+    bottleneck: bottleneck_id,
+    date: date_info
+  });
+  return new GeoJSON({ geometryName: "geometry" }).writeFeature(feature);
+};
+
+const calculateFairwayCoordinates = (profileLine, fairwayGeometry, depth) => {
+  // both geometries have to be in EPSG:4326
+  // uses turfjs distance() function
+  let fairwayCoordinates = [];
+  var line = turfLineString(profileLine.getCoordinates());
+  var polygon = turfPolygon(fairwayGeometry.getCoordinates());
+  var intersects = lineIntersect(line, polygon);
+  var l = intersects.features.length;
+  if (l % 2 != 0) {
+    console.log("Ignoring fairway because profile only intersects once.");
+  } else {
+    for (let i = 0; i < l; i += 2) {
+      let pStartPoint = profileLine.getCoordinates()[0];
+      let fStartPoint = intersects.features[i].geometry.coordinates;
+      let fEndPoint = intersects.features[i + 1].geometry.coordinates;
+      let opts = { units: "kilometers" };
+
+      fairwayCoordinates.push([
+        distance(pStartPoint, fStartPoint, opts) * 1000,
+        distance(pStartPoint, fEndPoint, opts) * 1000,
+        depth
+      ]);
+    }
+  }
+  return fairwayCoordinates;
+};
+
+export { generateFeatureRequest, prepareProfile, calculateFairwayCoordinates };
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/client/src/lib/http.js	Thu Nov 22 07:08:38 2018 +0100
@@ -0,0 +1,23 @@
+/*
+ * 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 axios from "axios";
+
+export const HTTP = axios.create({
+  baseURL: process.env.VUE_APP_API_URL || "/api"
+  /* headers: {
+    Authorization: 'Bearer {token}'
+  }*/
+});
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/client/src/lib/session.js	Thu Nov 22 07:08:38 2018 +0100
@@ -0,0 +1,38 @@
+/*
+ * 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>
+ */
+
+/**
+ * Compares whether session is current
+ * based on the expiry information and the
+ * current date
+ *
+ * @param  {number} expiresFromPastSession
+ */
+function sessionStillActive(expiresFromPastSession) {
+  if (!expiresFromPastSession) return false;
+  const now = Date.now();
+  const stillActive = now < expiresFromPastSession;
+  return stillActive;
+}
+/**
+ * Converts a given unix time to Milliseconds
+ *
+ * @param  {string} timestring
+ */
+function toMillisFromString(timestring) {
+  return timestring * 1000;
+}
+
+export { sessionStillActive, toMillisFromString };
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/client/src/locale/translations.json	Thu Nov 22 07:08:38 2018 +0100
@@ -0,0 +1,16 @@
+{
+  "en_GB": {
+    "Enter username": "Enter username",
+    "Enter passphrase": "Enter passphrase",
+    "Login failed": "Login failed",
+    "Login": "Login",
+    "Forgot password": "Forgot password"
+  },
+  "de_AT": {
+    "Enter username": "Benutzername",
+    "Enter passphrase": "Passphrase",
+    "Login failed": "Login fehlgeschlagen",
+    "Login": "Login",
+    "Forgot password": "Passwort vergessen"
+  }
+}
--- a/client/src/login/Login.vue	Wed Nov 21 16:47:02 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,182 +0,0 @@
-(<template>
-    <div class="d-flex flex-column login bg-white shadow">
-        <div class="m-5">
-            <!-- logo section -->
-            <div class="d-flex flex-row justify-content-center mb-3">
-                <div class="logo mr-3"><img src="../application/assets/logo.png"></div>
-                <div class="title">
-                    <h1>{{ appTitle }}</h1>
-                </div>
-            </div>
-            <!-- end logo section -->
-            <form class="loginform mx-auto" @submit.prevent="login">
-                <div id="alert" :style="errorMessageStyle" :class="errorMessageClass" role="alert">
-                    <span>{{ errorMessage }}</span>
-                </div>
-                <div class="input-group mb-3">
-                    <input type="text" v-model="user" id="inputUsername" class="form-control shadow-sm" :placeholder="usernameLabel" required autofocus>
-                </div>
-                <div class="input-group mb-3">
-                    <input :type="isPasswordVisible" v-model="password" id="inputPassword" class="form-control shadow-sm" :placeholder='passwordLabel' :required='!showPasswordReset' :disabled='showPasswordReset'>
-                    <div class="input-group-append">
-                        <span class="input-group-text disabled" id="basic-addon2" @click="showPassword">
-                            <font-awesome-icon icon="eye" v-if="!readablePassword" />
-                            <font-awesome-icon icon="eye-slash" v-if="readablePassword" />
-                        </span>
-                    </div>
-                </div>
-                <button v-if="showPasswordReset==false" class="btn btn-primary btn-block shadow-sm" :disabled="submitted || showPasswordReset" type="submit">
-                    <translate>Login</translate>
-                </button>
-                <div v-if="showPasswordReset" class="passwordreset">
-                    <button class="btn btn-block btn-info" type="button" @click="resetPassword">
-                        <translate>Request password reset!</translate>
-                    </button>
-                    <div class="pull-right">
-                        <a href="#" @click.prevent="togglePasswordReset">
-                            <translate>back to login</translate>
-                        </a>
-                    </div>
-                </div>
-                <div v-else class="pull-right">
-                    <a href="#" @click.prevent="togglePasswordReset">
-                        <translate>Forgot password</translate>
-                    </a>
-                </div>
-            </form>
-
-            <!-- bottom logo section -->
-            <div class="mb-3 secondary-logo mx-auto mb-auto"><img :src="secondaryLogo"></div>
-        </div>
-    </div>
-</template>)
-
-<style lang="sass" scoped>
-.login
-  min-width: 375px
-  min-height: 500px
-  @extend %fully-centered
-
-.loginform
-  max-width: 375px
-
-.alert
-  padding: 0.5rem
-
-.secondary-logo
-  max-width: 375px
-
-/* avoid IE and Edge show a password reveal as we do our own */
-input[type="password"]::-ms-reveal
-  display: none
-</style>
-
-<script>
-import { mapState } from "vuex";
-import { HTTP } from "../application/lib/http.js";
-import { displayError } from "../application/lib/errors.js";
-
-const UNAUTHORIZED = 401;
-
-export default {
-  name: "login",
-  data() {
-    return {
-      user: "",
-      password: "",
-      submitted: false,
-      loginFailed: false,
-      passwordJustResetted: false,
-      readablePassword: false,
-      showPasswordReset: false,
-      usernameToReset: ""
-    };
-  },
-  computed: {
-    errorMessage() {
-      if (this.loginFailed) return this.$gettext("Login failed");
-      if (this.passwordJustResetted)
-        return this.$gettext("Password reset requested!");
-      return "&npsp;";
-    },
-    passwordLabel() {
-      return this.$gettext("Enter passphrase");
-    },
-    usernameLabel() {
-      return this.$gettext("Enter username");
-    },
-    isPasswordVisible() {
-      return this.readablePassword ? "text" : "password";
-    },
-    errorMessageStyle() {
-      if (this.loginFailed || this.passwordJustResetted) {
-        return "visibility:visible";
-      }
-      return "visibility:hidden";
-    },
-    errorMessageClass() {
-      let result = {
-        "mb-3": true,
-        errormessage: true,
-        alert: true
-      };
-      if (this.loginFailed) {
-        result["alert-danger"] = true;
-      }
-      if (this.passwordJustResetted) {
-        result["alert-info"] = true;
-      }
-      return result;
-    },
-    ...mapState("application", ["appTitle", "secondaryLogo"])
-  },
-  methods: {
-    login() {
-      this.submitted = true;
-      this.passwordJustResetted = false;
-      const { user, password } = this;
-      this.$store
-        .dispatch("user/login", { user, password })
-        .then(() => {
-          this.loginFailed = false;
-          this.$router.push("/");
-        })
-        .catch(error => {
-          this.loginFailed = true;
-          this.submitted = false;
-          const { status, data } = error.response;
-          if (status !== UNAUTHORIZED) {
-            //Unauthorized is handled in alert-div
-            displayError({
-              title: "Backend Error",
-              message: `${status}: ${data.message || data}`
-            });
-          }
-        });
-    },
-    showPassword() {
-      // disallowing toggle when in reset mode
-      if (this.showPasswordReset) return;
-      this.readablePassword = !this.readablePassword;
-    },
-    togglePasswordReset() {
-      this.passwordJustResetted = false;
-      this.showPasswordReset = !this.showPasswordReset;
-      this.loginFailed = false;
-    },
-    resetPassword() {
-      if (this.user) {
-        HTTP.post("/users/passwordreset", { user: this.user }).catch(error => {
-          const { status, data } = error.response;
-          displayError({
-            title: "Backend Error",
-            message: `${status}: ${data.message || data}`
-          });
-        });
-        this.togglePasswordReset();
-        this.passwordJustResetted = true;
-      }
-    }
-  }
-};
-</script>
--- a/client/src/logs/logs.vue	Wed Nov 21 16:47:02 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,158 +0,0 @@
-<template>
-    <div class="main d-flex flex-column">
-        <div class="d-flex flex-row">
-            <div :class="spacer"></div>
-            <div class="logoutput text-left bg-white shadow mt-3 mx-3">
-                <pre id="code" v-highlightjs="logs"><code class="bash hljs hljs-string"></code></pre>
-            </div>
-        </div>
-        <div class="d-flex flex-row logmenu">
-            <div class="d-flex align-self-center">
-                <ul class="nav nav-pills">
-                    <li class="nav-item">
-                        <a
-                            @click="fetch('system/log/apache2/access.log', 'accesslog')"
-                            :class="accesslogStyle"
-                            href="#"
-                        >Accesslog</a>
-                    </li>
-                    <li class="nav-item">
-                        <a
-                            @click="fetch('system/log/apache2/error.log', 'errorlog')"
-                            :class="errorlogStyle"
-                            href="#"
-                        >Errorlog</a>
-                    </li>
-                </ul>
-            </div>
-            <div class="statuscontainer d-flex flex-row">
-                <div class="statusline ml-3 mt-1 align-self-center">
-                    <h3>Last refresh: {{refreshed}}</h3>
-                </div>
-                <div class="refresh">
-                    <button class="btn btn-dark" @click="fetch(currentFile, currentLog)">Refresh</button>
-                </div>
-            </div>
-        </div>
-    </div>
-</template>
-
-<style lang="sass" scoped>
-.statuscontainer
-  width: 87%
-  position: relative
-  
-.logmenu
-  margin-left: 5rem
-  min-width: 60vw
-  
-#code
-  overflow: auto
-  
-.refresh
-  position: absolute
-  right: 0
-  
-.logoutput
-  width: 95%
-  height: 85vh
-  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: 7rem
-</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 "../application/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/main.js	Wed Nov 21 16:47:02 2018 +0100
+++ b/client/src/main.js	Thu Nov 22 07:08:38 2018 +0100
@@ -13,11 +13,11 @@
  */
 
 import Vue from "vue";
-import App from "./App.vue";
+import App from "./components/App.vue";
 import router from "./router";
 import store from "./store";
 import GetTextPlugin from "vue-gettext";
-import translations from "./translations.json";
+import translations from "./locale/translations.json";
 import locale2 from "locale2";
 import CxltToastr from "cxlt-vue2-toastr";
 import "../node_modules/bootstrap/dist/css/bootstrap.min.css";
--- a/client/src/map/Maplayer.vue	Wed Nov 21 16:47:02 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,430 +0,0 @@
-<template>
-    <div id="map" :class="mapStyle"></div>
-</template>
-
-<style lang="sass" scoped>
-.nocursor
-  cursor: none
-
-.mapsplit
-  height: 50vh
-
-.mapfull
-  height: 100vh
-
-@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 "../application/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";
-import { getCenter } from "ol/extent";
-
-/* for the sake of debugging */
-/* eslint-disable no-console */
-export default {
-  name: "maplayer",
-  props: ["lat", "long", "zoom", "split"],
-  data() {
-    return {
-      projection: "EPSG:3857"
-    };
-  },
-  computed: {
-    ...mapGetters("map", ["getLayerByName"]),
-    ...mapState("map", [
-      "layers",
-      "openLayersMap",
-      "lineTool",
-      "polygonTool",
-      "cutTool"
-    ]),
-    ...mapState("bottlenecks", ["selectedSurvey"]),
-    mapStyle() {
-      return {
-        mapfull: !this.split,
-        mapsplit: this.split,
-        nocursor: this.hasActiveInteractions
-      };
-    },
-    hasActiveInteractions() {
-      return (
-        (this.lineTool && this.lineTool.getActive()) ||
-        (this.polygonTool && this.polygonTool.getActive()) ||
-        (this.cutTool && this.cutTool.getActive())
-      );
-    }
-  },
-  methods: {
-    identify(coordinate, pixel) {
-      if (!this.hasActiveInteractions) {
-        this.$store.commit("map/setIdentifiedFeatures", []);
-        // checking our WFS layers
-        var features = this.openLayersMap.getFeaturesAtPixel(pixel);
-        if (features) {
-          this.$store.commit("map/setIdentifiedFeatures", features);
-
-          // get selected bottleneck from identified features
-          for (let feature of features) {
-            let id = feature.getId();
-            // RegExp.prototype.test() works with number, str and undefined
-            if (/^bottlenecks\./.test(id)) {
-              this.$store.dispatch(
-                "bottlenecks/setSelectedBottleneck",
-                feature.get("objnam")
-              );
-              this.$store.commit("map/moveMap", {
-                coordinates: getCenter(
-                  feature
-                    .getGeometry()
-                    .clone()
-                    .transform("EPSG:3857", "EPSG:4326")
-                    .getExtent()
-                ),
-                zoom: 17,
-                preventZoomOut: true
-              });
-            }
-          }
-        }
-
-        // DEBUG output and example how to remove the GeometryName
-        /*
-        for (let feature of features) {
-          console.log("Identified:", feature.getId());
-          for (let key of feature.getKeys()) {
-            if (key != feature.getGeometryName()) {
-              console.log(key, feature.get(key));
-            }
-          }
-        }
-        */
-
-        // trying the GetFeatureInfo way for WMS
-        var wmsSource = this.getLayerByName(
-          "Inland ECDIS chart Danube"
-        ).data.getSource();
-        var url = wmsSource.getGetFeatureInfoUrl(
-          coordinate,
-          100 /* resolution */,
-          "EPSG:3857",
-          // { INFO_FORMAT: "application/vnd.ogc.gml" } // not allowed by d4d
-          { INFO_FORMAT: "text/plain" }
-        );
-
-        if (url) {
-          // cannot directly query here because of SOP
-          console.log("GetFeatureInfo url:", url);
-        }
-      }
-    },
-    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);
-      var layer = this.getLayerByName("Bottleneck isolines");
-      var wmsSrc = layer.data.getSource();
-
-      if (bottleneck_id != "does_not_exist") {
-        wmsSrc.updateParams({
-          cql_filter:
-            "date_info='" +
-            datestr +
-            "' AND bottleneck_id='" +
-            bottleneck_id +
-            "'"
-        });
-        layer.isVisible = true;
-        layer.data.setVisible(true);
-      } else {
-        layer.isVisible = false;
-        layer.data.setVisible(false);
-      }
-    },
-    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: {
-    split() {
-      const map = this.openLayersMap;
-      this.$nextTick(() => {
-        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.long, this.lat],
-        zoom: this.zoom,
-        projection: this.projection
-      })
-    });
-    this.$store.commit("map/setOpenLayersMap", 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 => {
-      var features = new GeoJSON().readFeatures(JSON.stringify(response.data));
-      var vectorSrc = this.getLayerByName(
-        "Fairway Dimensions"
-      ).data.getSource();
-      vectorSrc.addFeatures(features);
-      // 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.openLayersMap.on(["singleclick", "dblclick"], event => {
-      this.identify(event.coordinate, event.pixel);
-    });
-  }
-};
-</script>
--- a/client/src/pdftool/Pdftool.vue	Wed Nov 21 16:47:02 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,96 +0,0 @@
-<template>
-    <div :class="['box ui-element rounded bg-white mb-auto text-nowrap', { expanded: showPdfTool }]">
-        <div style="width: 15rem">
-            <h6 class="mb-0 py-2 px-3 border-bottom d-flex align-items-center bg-info text-white">
-              <i class="fa fa-file-pdf-o mr-2"></i>
-              Generate PDF
-              <i class="fa fa-times ml-auto" @click="$store.commit('application/showPdfTool', false)"></i>
-            </h6>
-            <div class="p-3">
-                <b>Chose format:</b>
-                <select v-model="form.format" class="form-control d-block w-100">
-                    <option>landscape</option>
-                    <option>portrait</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">Download</label>
-                    <input
-                        type="radio"
-                        id="pdfexport-downloadtype-open"
-                        value="open"
-                        v-model="form.downloadType"
-                    >
-                    <label for="pdfexport-downloadtype-open" class="ml-1">Open in new window</label>
-                </small>
-                <button
-                    @click="download"
-                    type="button"
-                    class="btn btn-sm btn-info d-block w-100"
-                >Generate PDF</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/router.js	Wed Nov 21 16:47:02 2018 +0100
+++ b/client/src/router.js	Thu Nov 22 07:08:38 2018 +0100
@@ -15,19 +15,17 @@
 import Vue from "vue";
 import Router from "vue-router";
 import store from "./store";
-import {
-  sessionStillActive,
-  toMillisFromString
-} from "./application/lib/session";
+import { sessionStillActive, toMillisFromString } from "./lib/session";
 
 /*  facilitate codesplitting */
-const Login = () => import("./login/Login.vue");
-const Main = () => import("./application/Main.vue");
-const Usermanagement = () => import("./usermanagement/Usermanagement.vue");
-const Logs = () => import("./logs/logs.vue");
-const Importqueue = () => import("./imports/Importqueue.vue");
+const Login = () => import("./components/Login.vue");
+const Main = () => import("./components/map/Main.vue");
+const Usermanagement = () =>
+  import("./components/admin/usermanagement/Usermanagement.vue");
+const Logs = () => import("./components/admin/logs.vue");
+const Importqueue = () => import("./components/map/imports/Importqueue.vue");
 const Systemconfiguration = () =>
-  import("./systemconfiguration/systemconfiguration.vue");
+  import("./components/admin/systemconfiguration.vue");
 
 Vue.use(Router);
 
--- a/client/src/staging/Staging.vue	Wed Nov 21 16:47:02 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,148 +0,0 @@
-<template>
-    <div>
-        <h6 class="mb-0 py-2 px-3 border-bottom d-flex align-items-center bg-info text-white">
-          <i class="fa fa-list-ol mr-2"></i>
-          Staging Area
-        </h6>
-        <table class="table mb-0">
-            <thead>
-                <tr>
-                    <th>Name</th>
-                    <th>Datatype</th>
-                    <th>Importdate</th>
-                    <th>ImportID</th>
-                    <th>&nbsp;</th>
-                </tr>
-            </thead>
-            <tbody v-if="filteredData.length">
-                <tr v-for="data in filteredData" :key="data.id">
-                    <td>
-                        <a @click="zoomTo(data.location)" href="#">{{ data.name }}</a>
-                    </td>
-                    <td>{{ data.type }}</td>
-                    <td>{{ data.date }}</td>
-                    <td>{{ data.importID }}</td>
-                    <td>
-                        <button class="btn btn-outline-info">
-                            <i class="fa fa-thumbs-up"></i>
-                        </button>
-                        &nbsp;
-                        <button class="btn btn-outline-info">
-                            <i class="fa fa-thumbs-down"></i>
-                        </button>
-                    </td>
-                </tr>
-            </tbody>
-            <tbody v-else>
-                <tr>
-                    <td class="text-center" colspan="6">No results.</td>
-                </tr>
-            </tbody>
-        </table>
-        <div class="p-3" v-if="filteredData.length">
-            <button class="btn btn-info">Confirm</button>
-        </div>
-    </div>
-</template>
-
-<script>
-import { mapState } from "vuex";
-
-const demodata = [
-  {
-    id: 1,
-    name: "B1",
-    date: "2018-11-19 10:23",
-    location: [16.5364, 48.1471],
-    status: "Not approved",
-    importID: "123456789",
-    type: "bottleneck"
-  },
-  {
-    id: 2,
-    name: "B2",
-    date: "2018-11-19 10:24",
-    location: [16.5364, 48.1472],
-    status: "Not approved",
-    importID: "123456789",
-    type: "bottleneck"
-  },
-  {
-    id: 3,
-    name: "s1",
-    date: "2018-11-13 10:25",
-    location: [16.5364, 48.1473],
-    status: "Not approved",
-    importID: "987654321",
-    type: "soundingresult"
-  },
-  {
-    id: 4,
-    name: "s2",
-    date: "2018-11-13 10:26",
-    location: [16.5364, 48.1474],
-    status: "Not approved",
-    importID: "987654321",
-    type: "soundingresult"
-  }
-];
-
-export default {
-  computed: {
-    ...mapState("application", ["searchQuery"]),
-    filteredData() {
-      return demodata.filter(data => {
-        const nameFound = data.name
-          .toLowerCase()
-          .includes(this.searchQuery.toLowerCase());
-        const dateFound = data.date
-          .toLowerCase()
-          .includes(this.searchQuery.toLowerCase());
-        const locationFound = data.location.find(coord => {
-          return coord
-            .toString()
-            .toLowerCase()
-            .includes(this.searchQuery.toLowerCase());
-        });
-        const statusFound = data.status
-          .toLowerCase()
-          .includes(this.searchQuery.toLowerCase());
-        const importIDFound = data.importID
-          .toLowerCase()
-          .includes(this.searchQuery.toLowerCase());
-        const typeFound = data.type
-          .toLowerCase()
-          .includes(this.searchQuery.toLowerCase());
-
-        return (
-          nameFound ||
-          dateFound ||
-          locationFound ||
-          statusFound ||
-          importIDFound ||
-          typeFound
-        );
-      });
-    }
-  },
-  methods: {
-    zoomTo(coordinates) {
-      this.$store.commit("map/moveMap", {
-        coordinates: coordinates,
-        zoom: 17,
-        preventZoomOut: true
-      });
-    }
-  }
-};
-</script>
-
-<style lang="sass" scoped>
-.table th,
-td
-  font-size: 0.9rem
-  border-top: 0px !important
-  border-bottom-width: 1px
-  text-align: left
-  padding: 0.5rem !important
-</style>
--- a/client/src/store.js	Wed Nov 21 16:47:02 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,39 +0,0 @@
-/*
- * 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 Vue from "vue";
-import Vuex from "vuex";
-import application from "./store/application";
-import user from "./store/user";
-import usermanagement from "./store/usermanagement";
-import map from "./store/map";
-import fairwayprofile from "./store/fairway";
-import bottlenecks from "./store/bottlenecks";
-import imports from "./store/imports";
-
-Vue.use(Vuex);
-
-export default new Vuex.Store({
-  modules: {
-    application,
-    fairwayprofile,
-    imports,
-    bottlenecks,
-    map,
-    user,
-    usermanagement
-  }
-});
--- a/client/src/store/bottlenecks.js	Wed Nov 21 16:47:02 2018 +0100
+++ b/client/src/store/bottlenecks.js	Thu Nov 22 07:08:38 2018 +0100
@@ -12,9 +12,9 @@
  * Author(s):
  * Markus Kottländer <markuks.kottlaender@intevation.de>
  */
-import { HTTP } from "../application/lib/http";
+import { HTTP } from "../lib/http";
 import { WFS } from "ol/format.js";
-import { displayError } from "../application/lib/errors.js";
+import { displayError } from "../lib/errors.js";
 
 export default {
   namespaced: true,
--- a/client/src/store/fairway.js	Wed Nov 21 16:47:02 2018 +0100
+++ b/client/src/store/fairway.js	Thu Nov 22 07:08:38 2018 +0100
@@ -14,13 +14,13 @@
  * Markus Kottländer <markuks.kottlaender@intevation.de>
  */
 import Vue from "vue";
-import { HTTP } from "../application/lib/http";
-import { prepareProfile } from "../application/lib/geo";
+import { HTTP } from "../lib/http";
+import { prepareProfile } from "../lib/geo";
 import LineString from "ol/geom/LineString.js";
-import { generateFeatureRequest } from "../application/lib/geo.js";
+import { generateFeatureRequest } from "../lib/geo.js";
 import { getLength } from "ol/sphere.js";
-import { calculateFairwayCoordinates } from "../application/lib/geo.js";
-import { displayError } from "../application/lib/errors.js";
+import { calculateFairwayCoordinates } from "../lib/geo.js";
+import { displayError } from "../lib/errors.js";
 
 const DEMOLEVEL = 149.345;
 const DEMODATA = 2.5;
--- a/client/src/store/imports.js	Wed Nov 21 16:47:02 2018 +0100
+++ b/client/src/store/imports.js	Thu Nov 22 07:08:38 2018 +0100
@@ -13,7 +13,7 @@
  * Thomas Junk <thomas.junk@intevation.de>
  */
 
-import { HTTP } from "../application/lib/http";
+import { HTTP } from "../lib/http";
 
 const Imports = {
   namespaced: true,
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/client/src/store/index.js	Thu Nov 22 07:08:38 2018 +0100
@@ -0,0 +1,39 @@
+/*
+ * 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 Vue from "vue";
+import Vuex from "vuex";
+import application from "./application";
+import user from "./user";
+import usermanagement from "./usermanagement";
+import map from "./map";
+import fairwayprofile from "./fairway";
+import bottlenecks from "./bottlenecks";
+import imports from "./imports";
+
+Vue.use(Vuex);
+
+export default new Vuex.Store({
+  modules: {
+    application,
+    fairwayprofile,
+    imports,
+    bottlenecks,
+    map,
+    user,
+    usermanagement
+  }
+});
--- a/client/src/store/map.js	Wed Nov 21 16:47:02 2018 +0100
+++ b/client/src/store/map.js	Thu Nov 22 07:08:38 2018 +0100
@@ -29,7 +29,7 @@
 import VectorSource from "ol/source/Vector.js";
 import Point from "ol/geom/Point.js";
 import { bbox as bboxStrategy } from "ol/loadingstrategy";
-import { HTTP } from "../application/lib/http";
+import { HTTP } from "../lib/http";
 import { fromLonLat } from "ol/proj";
 
 export default {
@@ -263,7 +263,7 @@
                     geometry: new Point(end),
                     image: new Icon({
                       // we need to make sure the image is loaded by Vue Loader
-                      src: require("../application/assets/linestring_arrow.png"),
+                      src: require("../assets/linestring_arrow.png"),
                       // fiddling with the anchor's y value does not help to
                       // position the image more centered on the line ending, as the
                       // default line style seems to be slightly uncentered in the
@@ -312,7 +312,7 @@
                     geometry: new Point(end),
                     image: new Icon({
                       // we need to make sure the image is loaded by Vue Loader
-                      src: require("../application/assets/linestring_arrow_grey.png"),
+                      src: require("../assets/linestring_arrow_grey.png"),
                       // fiddling with the anchor's y value does not help to
                       // position the image more centered on the line ending, as the
                       // default line style seems to be slightly uncentered in the
--- a/client/src/store/user.js	Wed Nov 21 16:47:02 2018 +0100
+++ b/client/src/store/user.js	Thu Nov 22 07:08:38 2018 +0100
@@ -13,7 +13,7 @@
  * Thomas Junk <thomas.junk@intevation.de>
  */
 
-import { HTTP } from "../application/lib/http";
+import { HTTP } from "../lib/http";
 
 export default {
   namespaced: true,
--- a/client/src/store/usermanagement.js	Wed Nov 21 16:47:02 2018 +0100
+++ b/client/src/store/usermanagement.js	Thu Nov 22 07:08:38 2018 +0100
@@ -13,7 +13,7 @@
  * Thomas Junk <thomas.junk@intevation.de>
  */
 
-import { HTTP } from "../application/lib/http";
+import { HTTP } from "../lib/http";
 
 const newUser = () => {
   return {
--- a/client/src/systemconfiguration/systemconfiguration.vue	Wed Nov 21 16:47:02 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,144 +0,0 @@
-<template>
-    <div class="d-flex flex-row">
-        <div class="card sysconfig mt-3 mx-auto">
-            <div class="card-header shadow-sm text-white bg-info mb-6">
-                Systemconfiguration
-            </div>
-            <div class="card-body config">
-                <section class="configsection">
-                    <h4 class="card-title">Bottleneck Areas stroke-color</h4>
-                    <compact-picker v-model="strokeColor" />
-                </section>
-                <section>
-                    <h4 class="card-title">Bottleneck Areas fill-color</h4>
-                    <chrome-picker v-model="fillColor" />
-                </section>
-                <div class="sendbutton">
-                    <a @click.prevent="submit" class="btn btn-info">Send</a>
-                </div>
-            </div> <!-- card-body -->
-        </div>
-    </div>
-</template>
-
-<style scoped lang="sass">
-.config
-  text-align: left
-  
-.configsection
-  margin-bottom: $large-offset
-  
-.sendbutton
-  position: absolute
-  right: $offset
-  bottom: $offset
-  
-.inputs
-  margin-left: auto
-  margin-right: auto
-  
-.sysconfig
-  width: 30vw
-</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 { Chrome } from "vue-color";
-import { Compact } from "vue-color";
-
-import { HTTP } from "../application/lib/http";
-import { displayError } from "../application/lib/errors.js";
-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 },
-  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: "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: "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: "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: "Backend Error",
-          message: `${status}: ${data.message || data}`
-        });
-      });
-  }
-};
-</script>
--- a/client/src/toolbar/Toolbar.vue	Wed Nov 21 16:47:02 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,96 +0,0 @@
-<template>
-    <div class="ml-2">
-        <div :class="'toolbar toolbar-' + (expandToolbar ? 'expanded' : 'collapsed')">
-            <Identify></Identify>
-            <Layers></Layers>
-            <Cuttool></Cuttool>
-            <Linetool></Linetool>
-            <Polygontool></Polygontool>
-            <Pdftool></Pdftool>
-        </div>
-        <div @click="$store.commit('application/expandToolbar', !expandToolbar)" class="toolbar-button bg-info text-white">
-            <i :class="'fa fa-angle-' + (expandToolbar ? 'up' : 'down')"></i>
-        </div>
-    </div>
-</template>
-
-<style lang="sass">
-// not scoped to affect nested components
-// doen't work when put in application/assets/application.sass... why??? o_O
-.toolbar
-  overflow: hidden
-  transition: max-height 0.4s
-
-.toolbar-collapsed
-  max-height: (3 * $icon-height) + (3 * $offset)
-
-.toolbar-expanded
-  max-height: 100%
-
-.toolbar-button
-  opacity: $slight-transparent
-  color: #666
-  height: $icon-width
-  width: $icon-height
-  align-items: center
-  justify-content: center
-  display: flex
-  background: #fff
-  margin-bottom: $offset
-  border-radius: $border-radius
-  box-shadow: $shadow-xs
-  z-index: 2
-  pointer-events: auto
-  .inverted
-    color: $color-info
-  .grey
-    color: #ddd
-</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("./buttons/Identify.vue"),
-    Layers: () => import("./buttons/Layers.vue"),
-    Linetool: () => import("./buttons/Linetool.vue"),
-    Polygontool: () => import("./buttons/Polygontool.vue"),
-    Cuttool: () => import("./buttons/Cuttool.vue"),
-    Pdftool: () => import("./buttons/Pdftool.vue")
-  },
-  computed: {
-    ...mapGetters("map", ["getLayerByName"]),
-    ...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.getLayerByName("Draw Tool")
-          .data.getSource()
-          .clear();
-      }
-    });
-  }
-};
-</script>
--- a/client/src/toolbar/buttons/Cuttool.vue	Wed Nov 21 16:47:02 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,76 +0,0 @@
-<template>
-  <div @click="toggleCutTool" class="toolbar-button">
-      <i :class="['fa fa-area-chart', { inverted: cutTool && cutTool.getActive(), grey: !selectedSurvey }]"></i>
-  </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";
-import Draw from "ol/interaction/Draw.js";
-import { Stroke, Style, Circle, Fill } from "ol/style.js";
-
-export default {
-  name: "cuttool",
-  computed: {
-    ...mapGetters("map", ["getLayerByName"]),
-    ...mapState("map", ["lineTool", "polygonTool", "cutTool", "openLayersMap"]),
-    ...mapState("bottlenecks", ["selectedSurvey"])
-  },
-  methods: {
-    toggleCutTool() {
-      console.log(this.selectedSurvey);
-      if (this.selectedSurvey) {
-        this.cutTool.setActive(!this.cutTool.getActive());
-        this.lineTool.setActive(false);
-        this.polygonTool.setActive(false);
-        this.$store.commit("map/setCurrentMeasurement", null);
-      }
-    },
-    cutEnd(event) {
-      this.$store.dispatch("fairwayprofile/cut", event.feature);
-    }
-  },
-  created() {
-    if (!this.cutTool) {
-      const cutVectorSrc = this.getLayerByName("Cut Tool").data.getSource();
-      const cutTool = new Draw({
-        source: cutVectorSrc,
-        type: "LineString",
-        maxPoints: 2,
-        style: new Style({
-          stroke: new Stroke({
-            color: "#444",
-            width: 2,
-            lineDash: [7, 7]
-          }),
-          image: new Circle({
-            fill: new Fill({ color: "#333" }),
-            stroke: new Stroke({ color: "#fff", width: 1.5 }),
-            radius: 6
-          })
-        })
-      });
-      cutTool.setActive(false);
-      cutTool.on("drawstart", () => {
-        cutVectorSrc.clear();
-      });
-      cutTool.on("drawend", this.cutEnd);
-      this.$store.commit("map/cutTool", cutTool);
-      this.openLayersMap.addInteraction(cutTool);
-    }
-  }
-};
-</script>
--- a/client/src/toolbar/buttons/Identify.vue	Wed Nov 21 16:47:02 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,29 +0,0 @@
-<template>
-    <div @click="$store.commit('application/showIdentify', !showIdentify)" class="toolbar-button">
-        <i :class="['fa fa-info', {inverted: showIdentify}]"></i>
-    </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"])
-  }
-};
-</script>
--- a/client/src/toolbar/buttons/Layers.vue	Wed Nov 21 16:47:02 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,29 +0,0 @@
-<template>
-    <div @click="$store.commit('application/showLayers', !showLayers)" class="toolbar-button">
-        <i :class="['fa fa-th-list', {inverted: showLayers}]"></i>
-    </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/toolbar/buttons/Linetool.vue	Wed Nov 21 16:47:02 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,70 +0,0 @@
-<template>
-    <div @click="toggleLineTool" class="toolbar-button">
-        <i :class="['fa fa-pencil', {inverted: lineTool && lineTool.getActive()}]"></i>
-    </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";
-import { getLength } from "ol/sphere.js";
-import Draw from "ol/interaction/Draw.js";
-
-export default {
-  name: "linetool",
-  computed: {
-    ...mapGetters("map", ["getLayerByName"]),
-    ...mapState("map", ["lineTool", "polygonTool", "cutTool", "openLayersMap"])
-  },
-  methods: {
-    toggleLineTool() {
-      this.lineTool.setActive(!this.lineTool.getActive());
-      this.polygonTool.setActive(false);
-      this.cutTool.setActive(false);
-      this.$store.commit("map/setCurrentMeasurement", null);
-      this.getLayerByName("Draw Tool")
-        .data.getSource()
-        .clear();
-    },
-    lineEnd(event) {
-      const length = getLength(event.feature.getGeometry());
-      this.$store.commit("map/setCurrentMeasurement", {
-        quantity: "Length",
-        unitSymbol: "m",
-        value: Math.round(length * 10) / 10
-      });
-      this.$store.commit("application/showIdentify", true);
-    }
-  },
-  created() {
-    if (!this.lineTool) {
-      const drawVectorSrc = this.getLayerByName("Draw Tool").data.getSource();
-      const lineTool = new Draw({
-        source: drawVectorSrc,
-        type: "LineString",
-        maxPoints: 2
-      });
-      lineTool.setActive(false);
-      lineTool.on("drawstart", () => {
-        drawVectorSrc.clear();
-        this.$store.commit("map/setCurrentMeasurement", null);
-      });
-      lineTool.on("drawend", this.lineEnd);
-      this.$store.commit("map/lineTool", lineTool);
-      this.openLayersMap.addInteraction(lineTool);
-    }
-  }
-};
-</script>
--- a/client/src/toolbar/buttons/Pdftool.vue	Wed Nov 21 16:47:02 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,29 +0,0 @@
-<template>
-    <div @click="$store.commit('application/showPdfTool', !showPdfTool)" class="toolbar-button">
-        <i :class="['fa fa-file-pdf-o', {inverted: showPdfTool}]"></i>
-    </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/toolbar/buttons/Polygontool.vue	Wed Nov 21 16:47:02 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,73 +0,0 @@
-<template>
-    <div @click="togglePolygonTool" class="toolbar-button">
-        <i :class="['fa fa-edit', {inverted: polygonTool && polygonTool.getActive()}]"></i>
-    </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";
-import { getArea } from "ol/sphere.js";
-import Draw from "ol/interaction/Draw.js";
-
-export default {
-  name: "polygontool",
-  computed: {
-    ...mapGetters("map", ["getLayerByName"]),
-    ...mapState("map", ["lineTool", "polygonTool", "cutTool", "openLayersMap"])
-  },
-  methods: {
-    togglePolygonTool() {
-      this.polygonTool.setActive(!this.polygonTool.getActive());
-      this.lineTool.setActive(false);
-      this.cutTool.setActive(false);
-      this.$store.commit("map/setCurrentMeasurement", null);
-      this.getLayerByName("Draw Tool")
-        .data.getSource()
-        .clear();
-    },
-    polygonEnd(event) {
-      const areaSize = getArea(event.feature.getGeometry());
-      this.$store.commit("map/setCurrentMeasurement", {
-        quantity: "Area",
-        unitSymbol: areaSize > 100000 ? "km²" : "m²",
-        value:
-          areaSize > 100000
-            ? Math.round(areaSize / 1000) / 1000 // convert into 1 km² == 1000*1000 m² and round to 1000 m²
-            : Math.round(areaSize)
-      });
-      this.$store.commit("application/showIdentify", true);
-    }
-  },
-  created() {
-    if (!this.polygonTool) {
-      const drawVectorSrc = this.getLayerByName("Draw Tool").data.getSource();
-      const polygonTool = new Draw({
-        source: drawVectorSrc,
-        type: "Polygon",
-        maxPoints: 50
-      });
-      polygonTool.setActive(false);
-      polygonTool.on("drawstart", () => {
-        drawVectorSrc.clear();
-        this.$store.commit("map/setCurrentMeasurement", null);
-      });
-      polygonTool.on("drawend", this.polygonEnd);
-      this.$store.commit("map/polygonTool", polygonTool);
-      this.openLayersMap.addInteraction(polygonTool);
-    }
-  }
-};
-</script>
--- a/client/src/translations.json	Wed Nov 21 16:47:02 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,16 +0,0 @@
-{
-  "en_GB": {
-    "Enter username": "Enter username",
-    "Enter passphrase": "Enter passphrase",
-    "Login failed": "Login failed",
-    "Login": "Login",
-    "Forgot password": "Forgot password"
-  },
-  "de_AT": {
-    "Enter username": "Benutzername",
-    "Enter passphrase": "Passphrase",
-    "Login failed": "Login fehlgeschlagen",
-    "Login": "Login",
-    "Forgot password": "Passwort vergessen"
-  }
-}
--- a/client/src/usermanagement/Passwordfield.vue	Wed Nov 21 16:47:02 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,64 +0,0 @@
-<template>
-    <div>
-        <label for="password">{{this.label}}</label>
-        <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"><i :class="eyeIcon"></i></span>
-        </div>
-        <div v-show="passworderrors" class="text-danger"><small><i class="fa fa-warning"></i> {{ 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";
-    },
-    eyeIcon() {
-      return {
-        fa: true,
-        "fa-eye": !this.readablePassword,
-        "fa-eye-slash": this.readablePassword
-      };
-    }
-  }
-};
-</script>
--- a/client/src/usermanagement/Userdetail.vue	Wed Nov 21 16:47:02 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,298 +0,0 @@
-<template>
-    <div class="userdetails h-100 mt-3 mr-auto shadow fadeIn animated">
-        <div class="card">
-            <div class="card-header shadow-sm text-white bg-info mb-3">
-                {{ this.cardHeader }}
-                <span @click="closeDetailview" class="pull-right">
-                    <i class="fa fa-close"></i>
-                </span>
-            </div>
-            <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">Username</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>
-                                    <i class="fa fa-warning"></i> {{ errors.user }}</small>
-                            </div>
-                        </div>
-                        <div class="form-group row">
-                            <label for="country">Country</label>
-                            <select class="form-control form-control-sm" v-on:change="validateCountry" v-model="currentUser.country">
-                                <option disabled value="">Please select one</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>
-                                    <i class="fa fa-warning"></i> {{ errors.country }}</small>
-                            </div>
-                        </div>
-                        <div class="form-group row">
-                            <label for="email">Email address</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>
-                                    <i class="fa fa-warning"></i> {{ errors.email }}</small>
-                            </div>
-                        </div>
-                        <div class="form-group row">
-                            <label for="role">Role</label>
-                            <select class="form-control form-control-sm" v-on:change="validateRole" v-model="currentUser.role">
-                                <option disabled value="">Please select one</option>
-                                <option value="sys_admin">Sysadmin</option>
-                                <option value="waterway_admin">Waterway Admin</option>
-                                <option value="waterway_user">Waterway User</option>
-                            </select>
-                            <div v-show="errors.role" class="text-danger">
-                                <small>
-                                    <i class="fa fa-warning"></i> {{ 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 pull-right">Submit</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"><i class="fa fa-telegram"> Send testmail</i></a>
-                        <div v-if="mailsent">Mail was sent</div>
-                    </div>
-                </form>
-            </div>
-        </div>
-    </div>
-</template>
-
-<style lang="sass" scoped>
-.mailbutton
-  width: 12vw
-
-.formfields
-  width: 10vw
-
-.userdetails
-  min-width: 40vw
-
-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 "../application/lib/http";
-import { displayError } from "../application/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: "Password",
-      passwordReLabel: "Repeat Password",
-      passwordPlaceholder: "password",
-      passwordRePlaceholder: "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: "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
-        ? ""
-        : "Please choose a country";
-    },
-    validateRole() {
-      this.errors.role = this.currentUser.role ? "" : "Please choose a role";
-    },
-    validatePassword() {
-      this.errors.passwordre =
-        this.password === this.passwordre ? "" : "Passwords do not match!";
-      this.errors.password =
-        this.password === "" || !violatedPasswordRules(this.password)
-          ? ""
-          : "Password should at least be 8 char long including 1 digit and 1 special char like $";
-    },
-    validateEmailaddress() {
-      this.errors.email = isEmailValid(this.currentUser.email)
-        ? ""
-        : "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: "Backend Error",
-              message: `${status}: ${data.message || data}`
-            });
-          });
-        })
-        .catch(error => {
-          this.submitted = false;
-          const { status, data } = error.response;
-          displayError({
-            title: "Error while saving user",
-            message: `${status}: ${data.message || data}`
-          });
-        });
-    }
-  }
-};
-</script>
--- a/client/src/usermanagement/Usermanagement.vue	Wed Nov 21 16:47:02 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,302 +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">
-                        <div class="card-header shadow-sm text-white bg-info mb-3">
-                            Users
-                        </div>
-                        <div class="card-body">
-                            <table id="datatable" :class="tableStyle">
-                                <thead>
-                                    <tr>
-                                        <th scope="col" @click="sortBy('user')">
-                                            <span>Username&nbsp;
-                                                <i v-if="sortCriterion=='user'" class="fa fa-angle-down"></i>
-                                            </span>
-                                        </th>
-                                        <th scope="col" @click="sortBy('country')">
-                                            <span>Country&nbsp;
-                                                <i v-if="sortCriterion=='country'" class="fa fa-angle-down"></i>
-                                            </span>
-                                        </th>
-                                        <th scope="col" @click="sortBy('email')">
-                                            <span>Email&nbsp;
-                                                <i v-if="sortCriterion=='email'" class="fa fa-angle-down"></i>
-                                            </span>
-                                        </th>
-                                        <th scope="col" @click="sortBy('role')">
-                                            <span>Role&nbsp;
-                                                <i v-if="sortCriterion=='role'" class="fa fa-angle-down"></i>
-                                            </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>
-                                            <i v-tooltip="user.roleLabel" :class="{
-                        fa:true,
-                        icon:true, 
-                        'fa-user':user.role==='waterway_user',
-                        'fa-star':user.role=='sys_admin',
-                        'fa-adn':user.role==='waterway_admin'}"></i>
-                                        </td>
-                                        <td>
-                                            <i @click="deleteUser(user.user)" class="icon fa fa-trash-o"></i>
-                                        </td>
-                                    </tr>
-                                </tbody>
-                            </table>
-                        </div>
-                        <div class="d-flex flex-row pagination">
-                            <i @click=" prevPage " v-if="this.currentPage!=1 " class="mr-2 btn btn-sm btn-light align-self-center pages fa fa-caret-left "></i> {{this.currentPage}} / {{this.pages}}
-                            <i @click="nextPage " class="ml-2 btn btn-sm btn-light align-self-center pages fa fa-caret-right "></i>
-                        </div>
-                        <div class="mr-3 pb-3">
-                            <button @click="addUser " class="btn btn-info pull-right shadow-sm ">Add User</button>
-                        </div>
-                    </div>
-                </div>
-                <Userdetail v-if="isUserDetailsVisible "></Userdetail>
-            </div>
-        </div>
-    </div>
-</template>
-
-<style scoped lang="sass">
-@import "../application/assets/tooltip.sass"
-
-.spacer
-  height: 100vh
-
-.spacer-collapsed
-  min-width: $icon-width + $offset
-  transition: $transition-fast
-
-@media screen and (min-width: 600px)
-  .spacer-expanded
-    min-width: $icon-width + $offset
-
-@media screen and (max-width: 1650px)
-  .spacer-expanded
-    min-width: $sidebar-width + $offset
-
-.main
-  height: 100vh
-
-@media screen and (min-width: 600px)
-  .content
-    margin-left: $sidebar-width
-    margin-right: auto
-
-@media screen and (min-width: 1650px)
-  .content
-    margin-left: $sidebar-width
-    margin-right: auto
-
-.icon
-  font-size: large
-
-.userlist
-  min-width: 520px
-  height: 100%
-
-.pagination
-  margin-left: auto
-  margin-right: auto
-
-.userlistsmall
-  width: 30vw
-
-.userlistextended
-  width: 70vw
-
-.table
-  width: 90% !important
-  margin: auto
-
-.table th,
-.pages
-  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 "../application/lib/errors.js";
-
-export default {
-  name: "userview",
-  data() {
-    return {
-      sortCriterion: "user",
-      pageSize: 10,
-      currentPage: 1
-    };
-  },
-  components: {
-    Userdetail
-  },
-  computed: {
-    ...mapGetters("usermanagement", ["isUserDetailsVisible"]),
-    ...mapState("application", ["showSidebar", "showUsermenu"]),
-    spacerStyle() {
-      return [
-        "spacer ml-3",
-        {
-          "spacer-expanded": this.showUsermenu && this.showSidebar,
-          "spacer-collapsed": !this.showUsermenu && 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",
-        {
-          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: "Backend Error",
-              message: `${status}: ${data.message || data}`
-            });
-          });
-        })
-        .catch(error => {
-          const { status, data } = error.response;
-          displayError({
-            title: "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);
-    }
-  },
-  beforeRouteEnter(to, from, next) {
-    store
-      .dispatch("usermanagement/loadUsers")
-      .then(next)
-      .catch(error => {
-        const { status, data } = error.response;
-        displayError({
-          title: "Backend Error",
-          message: `${status}: ${data}`
-        });
-      });
-  },
-  beforeRouteLeave(to, from, next) {
-    store.commit("usermanagement/clearCurrentUser");
-    store.commit("usermanagement/setUserDetailsInvisible");
-    next();
-  }
-};
-</script>
--- a/client/src/usermanagement/Users.vue	Wed Nov 21 16:47:02 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,279 +0,0 @@
-<template>
-    <div class="main d-flex flex-column">
-        <div class="d-flex content flex-column">
-            <div class="d-flex flex-row">
-                <div :class="userlistStyle">
-                    <div class="card">
-                        <div class="card-header shadow-sm text-white bg-info mb-3">
-                            Users
-                        </div>
-                        <div class="card-body">
-                            <table id="datatable" :class="tableStyle">
-                                <thead>
-                                    <tr>
-                                        <th scope="col" @click="sortBy('user')">
-                                            <span>Username&nbsp;
-                                                <i v-if="sortCriterion=='user'" class="fa fa-angle-down"></i>
-                                            </span>
-                                        </th>
-                                        <th scope="col" @click="sortBy('country')">
-                                            <span>Country&nbsp;
-                                                <i v-if="sortCriterion=='country'" class="fa fa-angle-down"></i>
-                                            </span>
-                                        </th>
-                                        <th scope="col" @click="sortBy('email')">
-                                            <span>Email&nbsp;
-                                                <i v-if="sortCriterion=='email'" class="fa fa-angle-down"></i>
-                                            </span>
-                                        </th>
-                                        <th scope="col" @click="sortBy('role')">
-                                            <span>Role&nbsp;
-                                                <i v-if="sortCriterion=='role'" class="fa fa-angle-down"></i>
-                                            </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>
-                                            <i v-tooltip="user.roleLabel" :class="{
-                        fa:true,
-                        icon:true, 
-                        'fa-user':user.role==='waterway_user',
-                        'fa-star':user.role=='sys_admin',
-                        'fa-adn':user.role==='waterway_admin'}"></i>
-                                        </td>
-                                        <td>
-                                            <i @click="deleteUser(user.user)" class="icon fa fa-trash-o"></i>
-                                        </td>
-                                    </tr>
-                                </tbody>
-                            </table>
-                        </div>
-                        <div class="d-flex flex-row pagination">
-                            <i @click=" prevPage " v-if="this.currentPage!=1 " class="backwards btn btn-sm btn-light align-self-center pages fa fa-caret-left "></i> {{this.currentPage}} / {{this.pages}}
-                            <i @click="nextPage " class="forwards btn btn-sm btn-light align-self-center pages fa fa-caret-right "></i>
-                        </div>
-                        <div class="mr-3 pb-3 ">
-                            <button @click="addUser " class="btn btn-info pull-right shadow-sm ">Add User</button>
-                        </div>
-                    </div>
-                </div>
-                <Userdetail v-if="isUserDetailsVisible "></Userdetail>
-            </div>
-        </div>
-    </div>
-</template>
-
-<style lang="sass" scoped>
-@import "../application/assets/tooltip.sass"
-
-.main
-  height: 100vh
-
-.backwards
-  margin-right: 0.5rem
-
-.forwards
-  margin-left: 0.5rem
-
-.content
-  margin-top: $large-offset
-  margin-left: auto
-  margin-right: auto
-
-.icon
-  font-size: large
-
-.userlist
-  margin-top: $topbarheight
-  min-width: 520px
-  height: 100%
-
-.pagination
-  margin-left: auto
-  margin-right: auto
-  
-.userlistsmall
-  width: 30vw
-
-.userlistextended
-  width: 70vw
-
-.table
-  width: 90% !important
-  margin: auto
-
-.table th,
-.pages
-  cursor: pointer
-
-.table th,
-td
-  font-size: 0.9rem
-  border-top: 0px !important
-  text-align: left
-  padding: 0.5rem !important
-
-.table td
-  font-size: 0.9rem
-  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 } from "vuex";
-import { displayError } from "../application/lib/errors.js";
-
-export default {
-  name: "userview",
-  data() {
-    return {
-      sortCriterion: "user",
-      pageSize: 10,
-      currentPage: 1
-    };
-  },
-  components: {
-    Userdetail
-  },
-  computed: {
-    ...mapGetters("usermanagement", ["isUserDetailsVisible"]),
-    ...mapGetters("application", ["sidebarCollapsed"]),
-    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 mr-3 shadow",
-        {
-          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: "Backend Error",
-              message: `${status}: ${data.message || data}`
-            });
-          });
-        })
-        .catch(error => {
-          const { status, data } = error.response;
-          displayError({
-            title: "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);
-    }
-  },
-  beforeRouteEnter(to, from, next) {
-    store
-      .dispatch("usermanagement/loadUsers")
-      .then(next)
-      .catch(error => {
-        const { status, data } = error.response;
-        displayError({
-          title: "Backend Error",
-          message: `${status}: ${data}`
-        });
-      });
-  },
-  beforeRouteLeave(to, from, next) {
-    store.commit("usermanagement/clearCurrentUser");
-    store.commit("usermanagement/setUserDetailsInvisible");
-    next();
-  }
-};
-</script>
--- a/client/src/zoom/zoom.vue	Wed Nov 21 16:47:02 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,51 +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 border-right" @click="zoomIn">
-            <i class="fa fa-plus"></i>
-        </button>
-        <button class="zoomButton border-0 bg-white rounded-right ui-element" @click="zoomOut">
-            <i class="fa fa-minus"></i>
-        </button>
-    </div>
-</template>
-
-<style lang="sass" scoped>
-.buttoncontainer
-  bottom: 0
-  left: 50%
-  margin-left: -$icon-width
-
-.zoomButton
-  min-height: $icon-width
-  min-width: $icon-width
-  z-index: 2
-  outline: none
-  color: #666
-</style>
-<script>
-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/vue.config.js	Wed Nov 21 16:47:02 2018 +0100
+++ b/client/vue.config.js	Thu Nov 22 07:08:38 2018 +0100
@@ -28,7 +28,7 @@
       // pass options to sass-loader
       sass: {
         // @/ is an alias to src/
-        data: `@import "@/application/assets/application.sass";`
+        data: `@import "@/assets/application.sass";`
       }
     }
   },