changeset 1143:846e336d8ee5

merge
author Thomas Junk <thomas.junk@intevation.de>
date Mon, 12 Nov 2018 15:00:45 +0100
parents dc3f0277628a (current diff) a473d91b0856 (diff)
children 5f98d0c9d738
files client/src/application/Sidebar.vue client/src/application/Topbar.vue client/src/bottlenecks/Bottlenecks.vue client/src/linetool/Linetool.vue client/src/map/Maplayer.vue
diffstat 18 files changed, 320 insertions(+), 251 deletions(-) [+]
line wrap: on
line diff
--- a/client/src/App.vue	Mon Nov 12 15:00:04 2018 +0100
+++ b/client/src/App.vue	Mon Nov 12 15:00:45 2018 +0100
@@ -15,8 +15,8 @@
             <div class="bottomcontainer d-flex flex-row align-items-end">
                 <Userbar></Userbar>
                 <Morphtool v-if="routeName == 'mainview'"></Morphtool>
-                <Linetool v-if="routeName == 'mainview'"></Linetool>
                 <Pdftool v-if="routeName == 'mainview'"></Pdftool>
+                <Drawtool v-if="routeName == 'mainview'"></Drawtool>
             </div>
             <Zoom v-if="routeName == 'mainview'"></Zoom>
         </div>
@@ -26,7 +26,7 @@
     </div>
 </template>
 
-<style lang="scss">
+<style lang="scss" scoped>
 html {
   height: 100%;
   width: 100%;
@@ -115,7 +115,7 @@
     Bottlenecks: () => import("./bottlenecks/Bottlenecks"),
     Topbar: () => import("./application/Topbar"),
     Userbar: () => import("./application/Userbar"),
-    Linetool: () => import("./linetool/Linetool"),
+    Drawtool: () => import("./drawtool/Drawtool"),
     Morphtool: () => import("./morphtool/Morphtool"),
     Pdftool: () => import("./pdftool/Pdftool"),
     Zoom: () => import("./zoom/zoom")
--- a/client/src/application/Main.vue	Mon Nov 12 15:00:04 2018 +0100
+++ b/client/src/application/Main.vue	Mon Nov 12 15:00:45 2018 +0100
@@ -15,7 +15,7 @@
     </div>
 </template>
 
-<style lang="scss">
+<style lang="scss" scoped>
 .profile {
   background-color: white;
   height: 50vh;
--- a/client/src/application/Sidebar.vue	Mon Nov 12 15:00:04 2018 +0100
+++ b/client/src/application/Sidebar.vue	Mon Nov 12 15:00:45 2018 +0100
@@ -81,7 +81,7 @@
 };
 </script>
 
-<style lang="scss">
+<style lang="scss" scoped>
 .menupoints {
   text-align: left;
 }
--- a/client/src/application/Topbar.vue	Mon Nov 12 15:00:04 2018 +0100
+++ b/client/src/application/Topbar.vue	Mon Nov 12 15:00:45 2018 +0100
@@ -30,7 +30,7 @@
     </div>
 </template>
 
-<style lang="scss">
+<style lang="scss" scoped>
 .searchgroup {
   width: 90%;
 }
--- a/client/src/application/Userbar.vue	Mon Nov 12 15:00:04 2018 +0100
+++ b/client/src/application/Userbar.vue	Mon Nov 12 15:00:45 2018 +0100
@@ -10,7 +10,7 @@
     </div>
 </template>
 
-<style lang="scss">
+<style lang="scss" scoped>
 .userpic {
   background: white;
   position: absolute;
--- a/client/src/bottlenecks/Bottlenecks.vue	Mon Nov 12 15:00:04 2018 +0100
+++ b/client/src/bottlenecks/Bottlenecks.vue	Mon Nov 12 15:00:45 2018 +0100
@@ -217,7 +217,7 @@
 };
 </script>
 
-<style lang="scss">
+<style lang="scss" scoped>
 .bottlenecks {
   position: absolute;
   z-index: -2;
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/client/src/drawtool/Drawtool.vue	Mon Nov 12 15:00:45 2018 +0100
@@ -0,0 +1,226 @@
+<template>
+    <div class="d-flex flex-column">
+        <div @click="toggleLineMode" class="ui-element d-flex shadow drawtool">
+            <i :class="['fa fa-pencil', {inverted: drawMode === 'LineString'}]"></i>
+        </div>
+        <div @click="togglePolygonMode" class="ui-element d-flex shadow drawtool">
+            <i :class="['fa fa-edit', {inverted: drawMode === 'Polygon'}]"></i>
+        </div>
+        <div @click="toggleCutMode" class="ui-element d-flex shadow drawtool" v-if="selectedSurvey">
+            <i :class="['fa fa-area-chart', {inverted: cutMode}]"></i>
+        </div>
+    </div>
+</template>
+
+<style lang="sass" scoped>
+.drawtool
+  background-color: white
+  padding: $small-offset
+  border-radius: $border-radius
+  margin-left: $offset
+  height: $icon-width
+  width: $icon-height
+  margin-bottom: $offset
+  margin-right: $offset
+  z-index: 2
+
+.inverted
+  color: #0077ff
+</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, mapGetters } from "vuex";
+import { getLength, getArea } from "ol/sphere.js";
+import LineString from "ol/geom/LineString.js";
+import Draw from "ol/interaction/Draw.js";
+import { displayError } from "../application/lib/errors.js";
+import { calculateFairwayCoordinates } from "../application/lib/geo.js";
+
+const DEMODATA = 2.5;
+
+export default {
+  name: "drawtool",
+  computed: {
+    ...mapGetters("map", ["getLayerByName"]),
+    ...mapState("map", ["drawMode", "drawTool", "cutMode", "cutTool", "openLayersMap"]),
+    ...mapState("bottlenecks", ["selectedSurvey"])
+  },
+  methods: {
+    toggleLineMode() {
+      this.disableDrawTool();
+      this.disableCutTool();
+      this.$store.commit("map/drawMode", this.drawMode !== "LineString" ? "LineString" : null);
+      this.$store.commit("map/cutMode", null);
+      if (this.drawMode) this.enableDrawTool();
+    },
+    togglePolygonMode() {
+      this.disableDrawTool();
+      this.disableCutTool();
+      this.$store.commit("map/drawMode", this.drawMode !== "Polygon" ? "Polygon" : null);
+      this.$store.commit("map/cutMode", null);
+      if (this.drawMode) this.enableDrawTool();
+    },
+    toggleCutMode() {
+      this.disableCutTool();
+      this.disableDrawTool();
+      this.$store.commit('map/cutMode', !this.cutMode);
+      this.$store.commit("map/drawMode", null);
+      if (this.cutMode) this.enableCutTool();
+    },
+    enableDrawTool() {
+      const drawVectorSrc = this.getLayerByName("Draw Tool").data.getSource();
+      drawVectorSrc.clear();
+      const drawTool = new Draw({
+        source: drawVectorSrc,
+        type: this.drawMode,
+        maxPoints: this.drawMode === "LineString" ? 2 : 50
+      });
+      drawTool.on("drawstart", () => {
+        drawVectorSrc.clear();
+        this.$store.commit("map/setCurrentMeasurement", null);
+        // we are not setting an id here, to avoid the regular identify to
+        // pick it up
+        // event.feature.setId("drawn.1"); // unique id for new feature
+      });
+      drawTool.on("drawend", this.drawEnd);
+      this.$store.commit("map/drawTool", drawTool);
+      this.openLayersMap.addInteraction(drawTool);
+    },
+    disableDrawTool() {
+      this.$store.commit("map/setCurrentMeasurement", null);
+      this.getLayerByName("Draw Tool").data.getSource().clear();
+      this.openLayersMap.removeInteraction(this.drawTool);
+      this.$store.commit("map/drawTool", null);
+    },
+    drawEnd(event) {
+      if (this.drawMode === "Polygon") {
+        const areaSize = getArea(event.feature.getGeometry());
+        // also place the a rounded areaSize in a property,
+        // so identify will show it
+        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)
+        });
+      }
+      if (this.drawMode === "LineString") {
+        const length = getLength(event.feature.getGeometry());
+        this.$store.commit("map/setCurrentMeasurement", {
+          quantity: "Length",
+          unitSymbol: "m",
+          value: Math.round(length * 10) / 10
+        });
+      }
+    },
+    enableCutTool() {
+      const cutVectorSrc = this.getLayerByName("Cut Tool").data.getSource();
+      cutVectorSrc.clear();
+      const cutTool = new Draw({
+        source: cutVectorSrc,
+        type: "LineString",
+        maxPoints: 2
+      });
+      cutTool.on("drawstart", () => {
+        cutVectorSrc.clear();
+        // we are not setting an id here, to avoid the regular identify to
+        // pick it up
+        // event.feature.setId("drawn.1"); // unique id for new feature
+      });
+      cutTool.on("drawend", this.cutEnd);
+      this.$store.commit("map/cutTool", cutTool);
+      this.openLayersMap.addInteraction(cutTool);
+    },
+    disableCutTool() {
+      this.$store.commit("map/setCurrentMeasurement", null);
+      this.getLayerByName("Cut Tool").data.getSource().clear();
+      this.openLayersMap.removeInteraction(this.cutTool);
+      this.$store.commit("map/cutTool", null);
+    },
+    cutEnd(event) {
+      const length = getLength(event.feature.getGeometry());
+      this.$store.commit("map/setCurrentMeasurement", {
+        quantity: "Length",
+        unitSymbol: "m",
+        value: Math.round(length * 10) / 10
+      });
+
+      // if a survey has been selected, request a profile
+      // TODO an improvement could be to check if the line intersects
+      // with the bottleneck area's polygon before trying the server request
+      if (this.selectedSurvey) {
+        this.$store.commit("fairwayprofile/clearCurrentProfile");
+        console.log("requesting profile for", this.selectedSurvey);
+        const inputLineString = event.feature.getGeometry().clone();
+        inputLineString.transform("EPSG:3857", "EPSG:4326");
+        const [start, end] = inputLineString
+          .getCoordinates()
+          .map(coords => coords.map(coord => parseFloat(coord.toFixed(8))));
+        this.$store.commit("fairwayprofile/setStartPoint", start);
+        this.$store.commit("fairwayprofile/setEndPoint", end);
+        const profileLine = new LineString([start, end]);
+        this.$store
+          .dispatch("fairwayprofile/loadProfile", this.selectedSurvey)
+          .then(() => {
+            var vectorSource = this.getLayerByName(
+              "Fairway Dimensions"
+            ).data.getSource();
+            this.calculateIntersection(vectorSource, profileLine);
+          })
+          .then(() => {
+            this.$store.commit("application/showSplitscreen", true);
+          })
+          .catch(error => {
+            const { status, data } = error.response;
+            displayError({
+              title: "Backend Error",
+              message: `${status}: ${data.message || data}`
+            });
+          });
+      }
+    },
+    calculateIntersection(vectorSource, profileLine) {
+      const transformedLine = profileLine
+        .clone()
+        .transform("EPSG:4326", "EPSG:3857")
+        .getExtent();
+      const featureCallback = feature => {
+        // transform back to prepare for usage
+        var intersectingPolygon = feature
+          .getGeometry()
+          .clone()
+          .transform("EPSG:3857", "EPSG:4326");
+        const fairwayCoordinates = calculateFairwayCoordinates(
+          profileLine,
+          intersectingPolygon,
+          DEMODATA
+        );
+        this.$store.commit(
+          "fairwayprofile/setFairwayCoordinates",
+          fairwayCoordinates
+        );
+      };
+      vectorSource.forEachFeatureIntersectingExtent(
+        // need to use EPSG:3857 which is the proj of vectorSource
+        transformedLine,
+        featureCallback
+      );
+    },
+  }
+};
+</script>
--- a/client/src/identify/Identify.vue	Mon Nov 12 15:00:04 2018 +0100
+++ b/client/src/identify/Identify.vue	Mon Nov 12 15:00:45 2018 +0100
@@ -39,7 +39,7 @@
     </div>
 </template>
 
-<style lang="scss">
+<style lang="scss" scoped>
 .features {
   max-height: $identify-height;
   overflow-y: auto;
--- a/client/src/layers/Layers.vue	Mon Nov 12 15:00:04 2018 +0100
+++ b/client/src/layers/Layers.vue	Mon Nov 12 15:00:45 2018 +0100
@@ -19,7 +19,7 @@
     </div>
 </template>
 
-<style lang="scss">
+<style lang="scss" scoped>
 .layerselectmenu {
   position: relative;
   margin-right: $offset;
--- a/client/src/layers/Layerselect.vue	Mon Nov 12 15:00:04 2018 +0100
+++ b/client/src/layers/Layerselect.vue	Mon Nov 12 15:00:45 2018 +0100
@@ -11,7 +11,7 @@
     </div>
 </template>
 
-<style lang="scss">
+<style lang="scss" scoped>
 .selection {
   text-align: left;
 }
--- a/client/src/linetool/Linetool.vue	Mon Nov 12 15:00:04 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,69 +0,0 @@
-<template>
-    <div @click="cycleDrawMode" class="ui-element d-flex shadow drawtool">
-        <i :class="icon"></i>
-    </div>
-</template>
-
-<style lang="scss">
-.drawtool {
-  position: absolute;
-  bottom: 0;
-  right: 0;
-  background-color: white;
-  padding: $small-offset;
-  border-radius: $border-radius;
-  margin-left: $offset;
-  height: $icon-width;
-  width: $icon-height;
-  margin-bottom: $offset;
-  margin-right: $offset;
-  z-index: 2;
-}
-
-.inverted {
-  color: #0077ff;
-}
-</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";
-
-export default {
-  name: "linetool",
-  methods: {
-    cycleDrawMode() {
-      if (!this.selectedSurvey && this.drawMode === "LineString") {
-        this.$store.commit("map/activateDrawModePolygon");
-      } else {
-        this.$store.commit("map/toggleDrawModeLine");
-      }
-    }
-  },
-  computed: {
-    ...mapState("map", ["identifiedFeatures", "drawMode"]),
-    ...mapState("bottlenecks", ["selectedSurvey"]),
-    icon() {
-      return {
-        fa: true,
-        "fa-area-chart": this.selectedSurvey,
-        "fa-edit": !this.selectedSurvey && this.drawMode === "Polygon",
-        "fa-pencil": !this.selectedSurvey && this.drawMode !== "Polygon",
-        inverted: this.drawMode
-      };
-    }
-  }
-};
-</script>
--- a/client/src/login/Login.vue	Mon Nov 12 15:00:04 2018 +0100
+++ b/client/src/login/Login.vue	Mon Nov 12 15:00:45 2018 +0100
@@ -51,7 +51,7 @@
     </div>
 </template>)
 
-<style lang="scss">
+<style lang="scss" scoped>
 .login {
   background-color: white;
   min-width: 375px;
--- a/client/src/map/Maplayer.vue	Mon Nov 12 15:00:04 2018 +0100
+++ b/client/src/map/Maplayer.vue	Mon Nov 12 15:00:45 2018 +0100
@@ -2,7 +2,7 @@
     <div id="map" :class="mapStyle"></div>
 </template>
 
-<style lang="scss">
+<style lang="scss" scoped>
 .mapsplit {
   height: 50vh;
 }
@@ -44,18 +44,8 @@
 import "ol/ol.css";
 import { Map, View } from "ol";
 import { WFS, GeoJSON } from "ol/format.js";
-import LineString from "ol/geom/LineString.js";
-import Draw from "ol/interaction/Draw.js";
-import { Vector as VectorLayer } from "ol/layer.js";
-import { Vector as VectorSource } from "ol/source.js";
-import { getLength, getArea } from "ol/sphere.js";
 import { Stroke, Style, Fill } from "ol/style.js";
 
-import { displayError } from "../application/lib/errors.js";
-import { calculateFairwayCoordinates } from "../application/lib/geo.js";
-
-const DEMODATA = 2.5;
-
 /* for the sake of debugging */
 /* eslint-disable no-console */
 export default {
@@ -63,8 +53,7 @@
   props: ["lat", "long", "zoom", "split"],
   data() {
     return {
-      projection: "EPSG:3857",
-      interaction: null
+      projection: "EPSG:3857"
     };
   },
   computed: {
@@ -79,143 +68,19 @@
     }
   },
   methods: {
-    removeCurrentInteraction() {
-      this.$store.commit("map/setCurrentMeasurement", null);
-      this.getLayerByName("Draw Tool")
-        .data.getSource()
-        .clear();
-      this.openLayersMap.removeInteraction(this.interaction);
-      this.interaction = null;
-    },
-    createInteraction(drawMode) {
-      const drawVectorSrc = this.getLayerByName("Draw Tool").data.getSource();
-      drawVectorSrc.clear();
-      var draw = new Draw({
-        source: drawVectorSrc,
-        type: drawMode,
-        maxPoints: drawMode === "LineString" ? 2 : 50
-      });
-      draw.on("drawstart", () => {
-        drawVectorSrc.clear();
-        this.$store.commit("map/setCurrentMeasurement", null);
-        // we are not setting an id here, to avoid the regular identify to
-        // pick it up
-        // event.feature.setId("drawn.1"); // unique id for new feature
-      });
-      draw.on("drawend", this.drawEnd);
-      return draw;
-    },
-    drawEnd(event) {
-      if (this.drawMode === "Polygon") {
-        const areaSize = getArea(event.feature.getGeometry());
-        // also place the a rounded areaSize in a property,
-        // so identify will show it
-        if (areaSize > 100000) {
-          this.$store.commit("map/setCurrentMeasurement", {
-            quantity: "Area",
-            unitSymbol: "km²",
-            // convert into 1 km² == 1000*1000 m² and round to 1000 m²
-            value: Math.round(areaSize / 1000) / 1000
-          });
-        } else {
-          this.$store.commit("map/setCurrentMeasurement", {
-            quantity: "Area",
-            unitSymbol: "m²",
-            value: Math.round(areaSize)
-          });
-        }
-      }
-      if (this.drawMode === "LineString") {
-        const length = getLength(event.feature.getGeometry());
-        this.$store.commit("map/setCurrentMeasurement", {
-          quantity: "Length",
-          unitSymbol: "m",
-          value: Math.round(length * 10) / 10
-        });
-      }
-
-      // if a survey has been selected, request a profile
-      // TODO an improvement could be to check if the line intersects
-      // with the bottleneck area's polygon before trying the server request
-      if (this.selectedSurvey) {
-        this.$store.commit("fairwayprofile/clearCurrentProfile");
-        console.log("requesting profile for", this.selectedSurvey);
-        const inputLineString = event.feature.getGeometry().clone();
-        inputLineString.transform("EPSG:3857", "EPSG:4326");
-        const [start, end] = inputLineString
-          .getCoordinates()
-          .map(coords => coords.map(coord => parseFloat(coord.toFixed(8))));
-        this.$store.commit("fairwayprofile/setStartPoint", start);
-        this.$store.commit("fairwayprofile/setEndPoint", end);
-        const profileLine = new LineString([start, end]);
-        this.$store
-          .dispatch("fairwayprofile/loadProfile", this.selectedSurvey)
-          .then(() => {
-            var vectorSource = this.getLayerByName(
-              "Fairway Dimensions"
-            ).data.getSource();
-            this.calculateIntersection(vectorSource, profileLine);
-          })
-          .then(() => {
-            this.$store.commit("application/showSplitscreen", true);
-          })
-          .catch(error => {
-            const { status, data } = error.response;
-            displayError({
-              title: "Backend Error",
-              message: `${status}: ${data.message || data}`
-            });
-          });
-      }
-    },
-    calculateIntersection(vectorSource, profileLine) {
-      const transformedLine = profileLine
-        .clone()
-        .transform("EPSG:4326", "EPSG:3857")
-        .getExtent();
-      const featureCallback = feature => {
-        // transform back to prepare for usage
-        var intersectingPolygon = feature
-          .getGeometry()
-          .clone()
-          .transform("EPSG:3857", "EPSG:4326");
-        const fairwayCoordinates = calculateFairwayCoordinates(
-          profileLine,
-          intersectingPolygon,
-          DEMODATA
-        );
-        this.$store.commit(
-          "fairwayprofile/setFairwayCoordinates",
-          fairwayCoordinates
-        );
-      };
-      vectorSource.forEachFeatureIntersectingExtent(
-        // need to use EPSG:3857 which is the proj of vectorSource
-        transformedLine,
-        featureCallback
-      );
-    },
-    activateInteraction() {
-      const interaction = this.createInteraction(this.drawMode);
-      this.interaction = interaction;
-      this.openLayersMap.addInteraction(interaction);
-    },
     identify(coordinate, pixel) {
       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.dispatch("bottlenecks/setSelectedBottleneck", feature.get("objnam"));
           }
         }
       }
@@ -343,14 +208,6 @@
     }
   },
   watch: {
-    drawMode(newValue) {
-      if (this.interaction) {
-        this.removeCurrentInteraction();
-      }
-      if (newValue) {
-        this.activateInteraction();
-      }
-    },
     split() {
       const map = this.openLayersMap;
       this.$nextTick(() => {
--- a/client/src/morphtool/Morphtool.vue	Mon Nov 12 15:00:04 2018 +0100
+++ b/client/src/morphtool/Morphtool.vue	Mon Nov 12 15:00:45 2018 +0100
@@ -104,12 +104,13 @@
  * Author(s):
  * Thomas Junk <thomas.junk@intevation.de>
  */
-import { mapState } from "vuex";
+import { mapState, mapGetters } from "vuex";
 
 export default {
   name: "morphtool",
   computed: {
-    ...mapState("map", ["drawMode"]),
+    ...mapGetters("map", ["getLayerByName"]),
+    ...mapState("map", ["openLayersMap", "cutTool"]),
     ...mapState("bottlenecks", [
       "selectedBottleneck",
       "surveys",
@@ -120,9 +121,9 @@
     clearSelection() {
       this.$store.dispatch("bottlenecks/setSelectedBottleneck", null);
       this.$store.commit("application/showSplitscreen", false);
-      if (this.drawMode) {
-        this.$store.commit("map/toggleDrawModeLine");
-      }
+      this.$store.commit("map/cutMode", false);
+      this.getLayerByName("Cut Tool").data.getSource().clear();
+      this.openLayersMap.removeInteraction(this.cutTool)
     }
   }
 };
--- a/client/src/pdftool/Pdftool.vue	Mon Nov 12 15:00:04 2018 +0100
+++ b/client/src/pdftool/Pdftool.vue	Mon Nov 12 15:00:45 2018 +0100
@@ -1,5 +1,5 @@
 <template>
-    <div class="pdftool">
+    <div class="pdftool" :style="selectedSurvey ? 'bottom: 140px' : ''">
         <div @click="$store.commit('application/showPdfTool', !showPdfTool)" class="d-flex flex-column ui-element minimizer">
             <i :class="['fa', 'mt-1', {'fa-file-pdf-o': !showPdfTool}, {'fa-close': showPdfTool}]"></i>
         </div>
@@ -44,10 +44,10 @@
 
 <style lang="scss" scoped>
 .pdftool {
-  position: relative;
+  position: absolute;
   margin-right: $offset;
   margin-bottom: $offset;
-  bottom: 48px;
+  bottom: 96px;
   right: 0;
 }
 
@@ -110,6 +110,7 @@
   },
   computed: {
     ...mapState("application", ["showPdfTool"]),
+    ...mapState("bottlenecks", ["selectedSurvey"]),
     style() {
       return {
         "ui-element": true,
--- a/client/src/store/map.js	Mon Nov 12 15:00:04 2018 +0100
+++ b/client/src/store/map.js	Mon Nov 12 15:00:45 2018 +0100
@@ -35,10 +35,12 @@
   namespaced: true,
   state: {
     openLayersMap: null,
-    identifiedFeatures: [],
-    currentMeasurement: null,
-    // there are three states of drawMode: null, "LineString", "Polygon"
-    drawMode: null,
+    identifiedFeatures: [], // map features identified by clicking on the map
+    currentMeasurement: null, // distance or area from drawTool
+    drawMode: null, // null, "LineString", "Polygon"
+    drawTool: null, // open layers interaction object (Draw)
+    cutMode: false, // true or false
+    cutTool: null, // open layers interaction object (Draw)
     layers: [
       {
         name: "Open Streetmap",
@@ -280,6 +282,54 @@
         }),
         isVisible: true,
         showInLegend: false
+      },
+      {
+        name: "Cut Tool",
+        data: new VectorLayer({
+          source: new VectorSource({ wrapX: false }),
+          style: function(feature) {
+            // adapted from OpenLayer's LineString Arrow Example
+            var geometry = feature.getGeometry();
+            var styles = [
+              // linestring
+              new Style({
+                stroke: new Stroke({
+                  color: "#369aca",
+                  width: 2
+                })
+              })
+            ];
+
+            if (geometry.getType() === "LineString") {
+              geometry.forEachSegment(function(start, end) {
+                var dx = end[0] - start[0];
+                var dy = end[1] - start[1];
+                var rotation = Math.atan2(dy, dx);
+                // arrows
+                styles.push(
+                  new Style({
+                    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"),
+                      // 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
+                      // anti-aliasing, but the image is not placed with subpixel
+                      // precision
+                      anchor: [0.75, 0.5],
+                      rotateWithView: true,
+                      rotation: -rotation
+                    })
+                  })
+                );
+              });
+            }
+            return styles;
+          }
+        }),
+        isVisible: true,
+        showInLegend: false
       }
     ]
   },
@@ -305,15 +355,17 @@
     setCurrentMeasurement: (state, measurement) => {
       state.currentMeasurement = measurement;
     },
-    toggleDrawModeLine: state => {
-      if (state.drawMode) {
-        state.drawMode = null;
-      } else {
-        state.drawMode = "LineString";
-      }
+    drawMode: (state, mode) => {
+      state.drawMode = mode;
+    },
+    drawTool: (state, drawTool) => {
+      state.drawTool = drawTool;
     },
-    activateDrawModePolygon: state => {
-      state.drawMode = "Polygon";
-    }
+    cutMode: (state, mode) => {
+      state.cutMode = mode;
+    },
+    cutTool: (state, cutTool) => {
+      state.cutTool = cutTool;
+    },
   }
 };
--- a/client/src/usermanagement/Userdetail.vue	Mon Nov 12 15:00:04 2018 +0100
+++ b/client/src/usermanagement/Userdetail.vue	Mon Nov 12 15:00:45 2018 +0100
@@ -70,7 +70,7 @@
     </div>
 </template>
 
-<style lang="scss">
+<style lang="scss" scoped>
 .mailbutton {
   width: 12vw;
 }
@@ -90,6 +90,7 @@
   font-size: $smaller;
 }
 </style>
+
 <script>
 /*
  * This is Free Software under GNU Affero General Public License v >= 3.0
--- a/client/src/usermanagement/Users.vue	Mon Nov 12 15:00:04 2018 +0100
+++ b/client/src/usermanagement/Users.vue	Mon Nov 12 15:00:45 2018 +0100
@@ -69,7 +69,7 @@
     </div>
 </template>
 
-<style lang="scss">
+<style lang="scss" scoped>
 @import "../application/assets/tooltip.scss";
 .main {
   height: 100vh;