changeset 1372:553aadd97087

new cross profile workflow (WIP) Needs fixing of some bugs and not so nice looks.
author Markus Kottlaender <markus@intevation.de>
date Tue, 27 Nov 2018 12:59:26 +0100
parents 5b9b8eabcd01
children 353d36dd2571 fabfffa54926
files client/src/components/App.vue client/src/components/map/Main.vue client/src/components/map/Maplayer.vue client/src/components/map/fairway/Fairwayprofile.vue client/src/components/map/fairway/Infobar.vue client/src/components/map/fairway/Profiles.vue client/src/components/map/toolbar/Cuttool.vue client/src/components/map/toolbar/Profiles.vue client/src/components/map/toolbar/Toolbar.vue client/src/main.js client/src/store/application.js client/src/store/bottlenecks.js client/src/store/fairway.js
diffstat 13 files changed, 461 insertions(+), 318 deletions(-) [+]
line wrap: on
line diff
--- a/client/src/components/App.vue	Tue Nov 27 12:49:53 2018 +0100
+++ b/client/src/components/App.vue	Tue Nov 27 12:59:26 2018 +0100
@@ -11,18 +11,18 @@
         </div>
         <div class="ml-auto d-flex">
           <div class="d-flex flex-column align-items-end">
-            <Layers v-if="routeName == 'mainview'"></Layers>
+            <Profiles v-if="routeName == 'mainview'"></Profiles>
+            <Pdftool v-if="routeName == 'mainview'"></Pdftool>
           </div>
           <div class="d-flex flex-column align-items-end">
             <Identify v-if="routeName == 'mainview'"></Identify>
-            <Pdftool v-if="routeName == 'mainview'"></Pdftool>
+            <Layers v-if="routeName == 'mainview'"></Layers>
           </div>
           <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>
@@ -86,7 +86,7 @@
     }
   },
   components: {
-    Surveys: () => import("./map/fairway/Surveys"),
+    Profiles: () => import("./map/fairway/Profiles"),
     Infobar: () => import("./map/fairway/Infobar"),
     Pdftool: () => import("./map/Pdftool"),
     Zoom: () => import("./map/Zoom"),
--- a/client/src/components/map/Main.vue	Tue Nov 27 12:49:53 2018 +0100
+++ b/client/src/components/map/Main.vue	Tue Nov 27 12:59:26 2018 +0100
@@ -2,11 +2,9 @@
   <div class="main d-flex flex-column">
     <Maplayer :split="showSplitscreen"></Maplayer>
     <FairwayProfile
-      :additionalSurveys="additionalSurveys"
       :xScale="xAxis"
       :yScaleLeft="yAxisLeft"
       :yScaleRight="yAxisRight"
-      :margin="margins"
     ></FairwayProfile>
   </div>
 </template>
@@ -36,37 +34,15 @@
     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];
     },
@@ -78,9 +54,6 @@
       const DELTA = this.maxAlt * 1.1 - this.maxAlt;
       return [this.maxAlt * 1 + DELTA, -DELTA];
     },
-    margins() {
-      return this.margin;
-    },
     yScaleLeft() {
       return {
         lo: this.minAlt,
--- a/client/src/components/map/Maplayer.vue	Tue Nov 27 12:49:53 2018 +0100
+++ b/client/src/components/map/Maplayer.vue	Tue Nov 27 12:59:26 2018 +0100
@@ -433,6 +433,7 @@
     this.openLayersMap.on(["singleclick", "dblclick"], event => {
       this.identify(event.coordinate, event.pixel);
     });
+    this.$store.dispatch("bottlenecks/loadBottlenecks");
   }
 };
 </script>
--- a/client/src/components/map/fairway/Fairwayprofile.vue	Tue Nov 27 12:49:53 2018 +0100
+++ b/client/src/components/map/fairway/Fairwayprofile.vue	Tue Nov 27 12:59:26 2018 +0100
@@ -23,94 +23,6 @@
       >{{ 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"
-              >
-                <font-awesome-icon :icon="showLabelInput ? 'times' : 'folder-plus'" size="lg"/>
-              </button>
-              <button
-                v-clipboard:copy="coordinatesForClipboard"
-                v-clipboard:success="onCopyCoordinates"
-                class="btn btn-outline-secondary btn-sm ml-2 mt-auto"
-              >
-                <font-awesome-icon icon="copy"/>
-              </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;"
-                >
-                  <font-awesome-icon icon="check"/>
-                </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"
-              >
-                <font-awesome-icon icon="check"/>
-              </button>
-            </div>
-          </small>
-        </div>
       </div>
     </div>
   </div>
@@ -144,20 +56,6 @@
 .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>
@@ -176,16 +74,14 @@
  */
 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";
+import { displayError } from "../../../lib/errors.js";
 import debounce from "debounce";
 
 const GROUND_COLOR = "#4A2F06";
 
 export default {
   name: "fairwayprofile",
-  props: ["xScale", "yScaleLeft", "yScaleRight", "margin", "additionalSurveys"],
+  props: ["xScale", "yScaleLeft", "yScaleRight"],
   data() {
     return {
       wait: false,
@@ -194,7 +90,13 @@
       cutLabel: "",
       showLabelInput: false,
       width: null,
-      height: null
+      height: null,
+      margin: {
+        top: 20,
+        right: 40,
+        bottom: 30,
+        left: 40
+      }
     };
   },
   computed: {
@@ -243,23 +145,9 @@
         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();
     },
@@ -277,16 +165,6 @@
     },
     fairwayCoordinates() {
       this.drawDiagram();
-    },
-    selectedBottleneck() {
-      this.$store.dispatch("fairwayprofile/previousCuts");
-      this.cutLabel =
-        this.selectedBottleneck + " (" + new Date().toISOString() + ")";
-    },
-    coordinatesSelect(newValue) {
-      if (newValue) {
-        this.applyCoordinates(newValue);
-      }
     }
   },
   methods: {
@@ -542,71 +420,6 @@
       if (!clientHeight || !clientWidth) return;
       this.height = clientHeight;
       this.width = clientWidth;
-    },
-    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.'
-      });
     }
   },
   created() {
--- a/client/src/components/map/fairway/Infobar.vue	Tue Nov 27 12:49:53 2018 +0100
+++ b/client/src/components/map/fairway/Infobar.vue	Tue Nov 27 12:59:26 2018 +0100
@@ -1,6 +1,6 @@
 <template>
   <div
-    v-if="selectedSurvey && !showSplitscreen"
+    v-if="Object.keys(currentProfile).length && !showSplitscreen"
     class="ui-element shadow-xs infobar rounded bg-white ml-auto mb-3 mr-3"
   >
     <div class="d-flex flex-row justify-content-between">
@@ -11,7 +11,6 @@
       <span
         class="p-2 border-left d-flex align-items-center"
         @click="$store.commit('application/showSplitscreen', true)"
-        v-if="Object.keys(currentProfile).length"
       >
         <font-awesome-icon icon="angle-up"></font-awesome-icon>
       </span>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/client/src/components/map/fairway/Profiles.vue	Tue Nov 27 12:59:26 2018 +0100
@@ -0,0 +1,336 @@
+<template>
+  <div :class="['box ui-element rounded bg-white text-nowrap', { expanded: showProfiles }]">
+    <div style="width: 20rem">
+      <h6 class="mb-0 py-2 px-3 border-bottom d-flex align-items-center">
+        <font-awesome-icon icon="chart-area" class="mr-2"></font-awesome-icon>
+        Profiles
+        <font-awesome-icon
+        icon="times"
+        class="ml-auto text-muted"
+        @click="$store.commit('application/showProfiles', false)"
+        ></font-awesome-icon>
+      </h6>
+      <div class="d-flex flex-column p-3 flex-grow-1 text-left position-relative">
+        <div class="loading d-flex justify-content-center align-items-center" v-if="surveysLoading || profileLoading">
+          <font-awesome-icon icon="spinner" spin />
+        </div>
+        <small class="text-muted">Bottleneck:</small>
+        <select @click="moveToBottleneck" v-model="selectedBottleneck" class="form-control form-control-sm">
+          <option :value="null">Select Bottleneck</option>
+          <option
+          v-for="bn in bottlenecks"
+          :key="bn.properties.name"
+          :value="bn.properties.name"
+          >{{ bn.properties.name }}</option>
+        </select>
+        <div v-if="selectedBottleneck">
+          <div class="d-flex mt-2">
+            <div class="flex-fill">
+              <small class="text-muted">Sounding Result:</small>
+              <select v-model="selectedSurvey" class="form-control form-control-sm">
+                <option
+                v-for="survey in surveys"
+                :key="survey.date_info"
+                :value="survey"
+                >{{ survey.date_info }}</option>
+              </select>
+            </div>
+            <div class="flex-fill ml-3" v-if="selectedSurvey && surveys.length > 1">
+              <small class="text-muted mt-1">Compare with:</small>
+              <select v-model="additionalSurvey" class="form-control form-control-sm">
+                <option :value="null">None</option>
+                <option
+                v-for="survey in additionalSurveys"
+                :key="survey.date_info"
+                :value="survey"
+                >{{ survey.date_info }}</option>
+              </select>
+            </div>
+          </div>
+          <hr class="w-100">
+          <small class="d-flex text-left my-2" v-if="startPoint && endPoint">
+            <div class="text-nowrap mr-3">
+              <b>Start:</b>
+              <br>
+              Lat: {{ startPoint[1] }}
+              <br>
+              Lon: {{ startPoint[0] }}
+            </div>
+            <div class="text-nowrap">
+              <b>End:</b>
+              <br>
+              Lat: {{ endPoint[1] }}
+              <br>
+              Lon: {{ endPoint[0] }}
+            </div>
+            <button v-clipboard:copy="coordinatesForClipboard"
+            v-clipboard:success="onCopyCoordinates"
+            class="btn btn-info btn-sm ml-auto mt-auto">
+            <font-awesome-icon icon="copy" />
+          </button>
+        </small>
+        <div class="d-flex">
+          <div class="pr-3 w-50" v-if="startPoint && endPoint">
+            <button class="btn btn-info btn-sm w-100"
+            @click="showLabelInput = !showLabelInput">
+            <font-awesome-icon :icon="showLabelInput ? 'times' : 'check'" />
+            {{ showLabelInput ? "Cancel" : "Save" }}
+          </button>
+        </div>
+        <div :class="startPoint && endPoint ? 'w-50' : 'w-100'">
+          <button
+          class="btn btn-info btn-sm w-100"
+          @click="toggleCutTool"
+          >
+          <font-awesome-icon :icon="cutTool && cutTool.getActive() ? 'times' : 'plus'"></font-awesome-icon>
+          {{ cutTool && cutTool.getActive() ? "Cancel" : "New" }}
+        </button>
+      </div>
+    </div>
+    <div v-if="showLabelInput" class="mt-2">
+      <small class="text-muted">Enter label for cross profile:</small>
+      <div class="position-relative">
+        <input class="form-control form-control-sm pr-5" v-model="cutLabel" />
+        <button class="btn btn-sm btn-info position-absolute input-button-right"
+        @click="saveCut"
+        v-if="cutLabel"
+        style="top: 0; right: 0;">
+        <font-awesome-icon icon="check" />
+      </button>
+    </div>
+  </div>
+  <small class="text-muted d-block mt-2">Saved cross profiles:</small>
+  <select class="form-control form-control-sm" v-model="coordinatesSelect">
+    <option></option>
+    <option v-for="(cut, index) in previousCuts" :value="cut.coordinates" :key="index">
+      {{ cut.label }}
+    </option>
+  </select>
+  <small class="text-muted d-block mt-2">Enter coordinates manually:</small>
+  <div class="position-relative">
+    <input class="form-control form-control-sm pr-5" placeholder="Lat,Lon,Lat,Lon" v-model="coordinatesInput" />
+    <button class="btn btn-sm btn-info position-absolute input-button-right" 
+    @click="applyManualCoordinates"
+    style="top: 0; right: 0;"
+    v-if="coordinatesInputIsValid">
+    <font-awesome-icon icon="check" />
+  </button>
+</div>
+</div>
+</div>
+</div>
+</div>
+</template>
+
+<style lang="sass" scoped>
+  .loading
+    background: rgba(255, 255, 255, 0.96)
+    position: absolute
+    z-index: 99
+    top: 0
+    right: 0
+    bottom: 0
+    left: 0
+
+  .input-button-right
+    border-top-right-radius: $border-radius
+    border-bottom-right-radius: $border-radius
+    border-top-left-radius: 0 !important
+    border-bottom-left-radius: 0 !important
+</style>
+
+<script>
+/* This is Free Software under GNU Affero General Public License v >= 3.0
+ * without warranty, see README.md and license for details.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ * License-Filename: LICENSES/AGPL-3.0.txt
+ *
+ * Copyright (C) 2018 by via donau 
+ *   – Österreichische Wasserstraßen-Gesellschaft mbH
+ * Software engineering by Intevation GmbH
+ *
+ * Author(s):
+ * Markus Kottländer <markus.kottlaender@intevation.de>
+ */
+import { mapState, mapGetters } from "vuex";
+import Feature from "ol/Feature";
+import LineString from "ol/geom/LineString";
+import { displayError, displayInfo } from "../../../lib/errors.js";
+
+export default {
+  name: "profiles",
+  data() {
+    return {
+      coordinatesInput: "",
+      coordinatesSelect: null,
+      cutLabel: "",
+      showLabelInput: false
+    };
+  },
+  computed: {
+    ...mapGetters("map", ["getVSourceByName"]),
+    ...mapState("application", ["showProfiles"]),
+    ...mapState("map", ["lineTool", "polygonTool", "cutTool"]),
+    ...mapState("bottlenecks", ["bottlenecks", "surveys", "surveysLoading"]),
+    ...mapState("fairwayprofile", [
+      "previousCuts",
+      "startPoint",
+      "endPoint",
+      "profileLoading"
+    ]),
+    selectedBottleneck: {
+      get() {
+        return this.$store.state.bottlenecks.selectedBottleneck;
+      },
+      set(name) {
+        this.loading = true;
+        this.$store.dispatch("bottlenecks/setSelectedBottleneck", name);
+      }
+    },
+    selectedSurvey: {
+      get() {
+        return this.$store.state.bottlenecks.selectedSurvey;
+      },
+      set(survey) {
+        this.$store.commit("bottlenecks/setSelectedSurvey", survey);
+      }
+    },
+    additionalSurvey: {
+      get() {
+        return this.$store.state.fairwayprofile.additionalSurvey;
+      },
+      set(survey) {
+        this.$store.commit("fairwayprofile/setAdditionalSurvey", survey);
+      }
+    },
+    additionalSurveys() {
+      return this.surveys.filter(survey => survey !== this.selectedSurvey);
+    },
+    coordinatesForClipboard() {
+      return (
+        this.startPoint[1] +
+        "," +
+        this.startPoint[0] +
+        "," +
+        this.endPoint[1] +
+        "," +
+        this.endPoint[0]
+      );
+    },
+    coordinatesInputIsValid() {
+      const coordinates = this.coordinatesInput
+        .split(",")
+        .map(coord => parseFloat(coord.trim()))
+        .filter(c => Number(c) === c);
+      return coordinates.length === 4;
+    }
+  },
+  watch: {
+    selectedBottleneck() {
+      this.$store.dispatch("fairwayprofile/previousCuts");
+      this.cutLabel =
+        this.selectedBottleneck + " (" + new Date().toISOString() + ")";
+    },
+    selectedSurvey(survey) {
+      if (survey) this.$store.dispatch("fairwayprofile/loadProfile", survey);
+    },
+    additionalSurvey(survey) {
+      if (survey) this.$store.dispatch("fairwayprofile/loadProfile", survey);
+    },
+    coordinatesSelect(newValue) {
+      if (newValue) {
+        this.applyCoordinates(newValue);
+      }
+    }
+  },
+  methods: {
+    toggleCutTool() {
+      if (this.selectedSurvey) {
+        this.cutTool.setActive(!this.cutTool.getActive());
+        this.lineTool.setActive(false);
+        this.polygonTool.setActive(false);
+        this.$store.commit("map/setCurrentMeasurement", null);
+      }
+    },
+    onCopyCoordinates() {
+      displayInfo({
+        title: "Success",
+        message: "Coordinates copied to clipboard!"
+      });
+    },
+    applyManualCoordinates() {
+      const coordinates = this.coordinatesInput
+        .split(",")
+        .map(coord => parseFloat(coord.trim()));
+      this.coordinatesSelect = null;
+      this.coordinatesInput = "";
+      this.applyCoordinates([
+        coordinates[1],
+        coordinates[0],
+        coordinates[3],
+        coordinates[2]
+      ]);
+    },
+    applyCoordinates(coordinates) {
+      // allow only numbers
+      coordinates = coordinates.filter(c => Number(c) === c);
+      if (coordinates.length === 4) {
+        // draw line on map
+        this.getVSourceByName("Cut Tool").clear();
+        const cut = new Feature({
+          geometry: new LineString([
+            [coordinates[0], coordinates[1]],
+            [coordinates[2], coordinates[3]]
+          ]).transform("EPSG:4326", "EPSG:3857")
+        });
+        this.getVSourceByName("Cut Tool").addFeature(cut);
+
+        // draw diagram
+        this.$store.dispatch("fairwayprofile/cut", cut);
+      } else {
+        displayError({
+          title: "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;
+      this.cutLabel = "";
+      displayInfo({
+        title: "Coordinates saved!",
+        message:
+          'You can now select these coordinates from the "Saved cross profiles" menu to restore this cross profile.'
+      });
+    },
+    moveToBottleneck() {
+      const bottleneck = this.bottlenecks.find(
+        bn => bn.properties.name === this.selectedBottleneck
+      );
+      if (!bottleneck) return;
+      this.$store.commit("map/moveMap", {
+        coordinates: bottleneck.geometry.coordinates,
+        zoom: 17,
+        preventZoomOut: true
+      });
+    }
+  }
+};
+</script>
--- a/client/src/components/map/toolbar/Cuttool.vue	Tue Nov 27 12:49:53 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,40 +0,0 @@
-<template>
-    <div @click="toggleCutTool" class="toolbar-button">
-        <font-awesome-icon icon="chart-area" :class="{ 'text-info': cutTool && cutTool.getActive(), grey: !selectedSurvey }"></font-awesome-icon>
-    </div>
-</template>
-
-<script>
-/* This is Free Software under GNU Affero General Public License v >= 3.0
- * without warranty, see README.md and license for details.
- *
- * SPDX-License-Identifier: AGPL-3.0-or-later
- * License-Filename: LICENSES/AGPL-3.0.txt
- *
- * Copyright (C) 2018 by via donau
- *   – Österreichische Wasserstraßen-Gesellschaft mbH
- * Software engineering by Intevation GmbH
- *
- * Author(s):
- * Markus Kottländer <markus.kottlaender@intevation.de>
- */
-import { mapState } from "vuex";
-
-export default {
-  name: "cuttool",
-  computed: {
-    ...mapState("map", ["lineTool", "polygonTool", "cutTool"]),
-    ...mapState("bottlenecks", ["selectedSurvey"])
-  },
-  methods: {
-    toggleCutTool() {
-      if (this.selectedSurvey) {
-        this.cutTool.setActive(!this.cutTool.getActive());
-        this.lineTool.setActive(false);
-        this.polygonTool.setActive(false);
-        this.$store.commit("map/setCurrentMeasurement", null);
-      }
-    }
-  }
-};
-</script>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/client/src/components/map/toolbar/Profiles.vue	Tue Nov 27 12:59:26 2018 +0100
@@ -0,0 +1,29 @@
+<template>
+    <div @click="$store.commit('application/showProfiles', !showProfiles)" class="toolbar-button">
+        <font-awesome-icon icon="chart-area" :class="{ 'text-info': showProfiles }"></font-awesome-icon>
+    </div>
+</template>
+
+<script>
+/* This is Free Software under GNU Affero General Public License v >= 3.0
+ * without warranty, see README.md and license for details.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ * License-Filename: LICENSES/AGPL-3.0.txt
+ *
+ * Copyright (C) 2018 by via donau
+ *   – Österreichische Wasserstraßen-Gesellschaft mbH
+ * Software engineering by Intevation GmbH
+ *
+ * Author(s):
+ * Markus Kottländer <markus.kottlaender@intevation.de>
+ */
+import { mapState } from "vuex";
+
+export default {
+  name: "profiles",
+  computed: {
+    ...mapState("application", ["showProfiles"])
+  }
+};
+</script>
--- a/client/src/components/map/toolbar/Toolbar.vue	Tue Nov 27 12:49:53 2018 +0100
+++ b/client/src/components/map/toolbar/Toolbar.vue	Tue Nov 27 12:59:26 2018 +0100
@@ -3,7 +3,7 @@
         <div :class="'toolbar toolbar-' + (expandToolbar ? 'expanded' : 'collapsed')">
             <Identify></Identify>
             <Layers></Layers>
-            <Cuttool></Cuttool>
+            <Profiles></Profiles>
             <Linetool></Linetool>
             <Polygontool></Polygontool>
             <Pdftool></Pdftool>
@@ -70,7 +70,7 @@
     Layers: () => import("./Layers.vue"),
     Linetool: () => import("./Linetool.vue"),
     Polygontool: () => import("./Polygontool.vue"),
-    Cuttool: () => import("./Cuttool.vue"),
+    Profiles: () => import("./Profiles.vue"),
     Pdftool: () => import("./Pdftool.vue")
   },
   computed: {
--- a/client/src/main.js	Tue Nov 27 12:49:53 2018 +0100
+++ b/client/src/main.js	Tue Nov 27 12:59:26 2018 +0100
@@ -64,7 +64,8 @@
   faPlus,
   faMinus,
   faSortAmountUp,
-  faSortAmountDown
+  faSortAmountDown,
+  faSpinner
 } from "@fortawesome/free-solid-svg-icons";
 import { faAdn } from "@fortawesome/free-brands-svg-icons";
 import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
@@ -107,7 +108,8 @@
   faPlus,
   faMinus,
   faSortAmountUp,
-  faSortAmountDown
+  faSortAmountDown,
+  faSpinner
 );
 Vue.component("font-awesome-icon", FontAwesomeIcon);
 
--- a/client/src/store/application.js	Tue Nov 27 12:49:53 2018 +0100
+++ b/client/src/store/application.js	Tue Nov 27 12:59:26 2018 +0100
@@ -30,6 +30,7 @@
     showLayers: true,
     showPdfTool: false,
     showContextBox: false,
+    showProfiles: false,
     contextBoxContent: null, // bottlenecks, imports, staging
     expandToolbar: false,
     countries: ["AT", "SK", "HU", "HR", "RS", "BiH", "BG", "RO", "UA"],
@@ -84,6 +85,9 @@
     showContextBox: (state, show) => {
       state.showContextBox = show;
     },
+    showProfiles: (state, show) => {
+      state.showProfiles = show;
+    },
     contextBoxContent: (state, context) => {
       state.contextBoxContent = context;
       if (context) {
--- a/client/src/store/bottlenecks.js	Tue Nov 27 12:49:53 2018 +0100
+++ b/client/src/store/bottlenecks.js	Tue Nov 27 12:59:26 2018 +0100
@@ -22,7 +22,8 @@
     bottlenecks: [],
     selectedBottleneck: null,
     surveys: [],
-    selectedSurvey: null
+    selectedSurvey: null,
+    surveysLoading: false
   };
 };
 
@@ -42,6 +43,9 @@
     },
     setSelectedSurvey(state, survey) {
       state.selectedSurvey = survey;
+    },
+    surveysLoading: (state, loading) => {
+      state.surveysLoading = loading;
     }
   },
   actions: {
@@ -50,6 +54,9 @@
         commit("setSelectedSurvey", null);
         commit("fairwayprofile/clearCurrentProfile", null, { root: true });
       }
+      if (name) {
+        commit("application/showProfiles", true, { root: true });
+      }
       commit("setSelectedBottleneck", name);
       dispatch("querySurveys", name);
     },
@@ -79,6 +86,7 @@
     },
     querySurveys({ commit }, name) {
       if (name) {
+        commit("surveysLoading", true);
         HTTP.get("/surveys/" + name, {
           headers: {
             "X-Gemma-Auth": localStorage.getItem("token"),
@@ -86,16 +94,22 @@
           }
         })
           .then(response => {
-            commit("setSurveys", response.data.surveys);
+            const surveys = response.data.surveys.sort(
+              (a, b) => (a.date_info < b.date_info ? 1 : -1)
+            );
+            commit("setSelectedSurvey", surveys[0]);
+            commit("setSurveys", surveys);
           })
           .catch(error => {
             commit("setSurveys", []);
+            commit("setSelectedSurvey", null);
             const { status, data } = error.response;
             displayError({
               title: "Backend Error",
               message: `${status}: ${data.message || data}`
             });
-          });
+          })
+          .finally(() => commit("surveysLoading", false));
       } else {
         commit("setSurveys", []);
       }
--- a/client/src/store/fairway.js	Tue Nov 27 12:49:53 2018 +0100
+++ b/client/src/store/fairway.js	Tue Nov 27 12:59:26 2018 +0100
@@ -37,7 +37,8 @@
     fairwayCoordinates: [],
     startPoint: null,
     endPoint: null,
-    previousCuts: []
+    previousCuts: [],
+    profileLoading: false
   };
 };
 
@@ -103,6 +104,9 @@
     },
     previousCuts: (state, previousCuts) => {
       state.previousCuts = previousCuts;
+    },
+    profileLoading: (state, loading) => {
+      state.profileLoading = loading;
     }
   },
   actions: {
@@ -116,27 +120,34 @@
         .clear();
     },
     loadProfile({ commit, state }, survey) {
-      return new Promise((resolve, reject) => {
-        const profileLine = new LineString([state.startPoint, state.endPoint]);
-        const geoJSON = generateFeatureRequest(
-          profileLine,
-          survey.bottleneck_id,
-          survey.date_info
-        );
-        HTTP.post("/cross", geoJSON, {
-          headers: { "X-Gemma-Auth": localStorage.getItem("token") }
-        })
-          .then(response => {
-            commit("profileLoaded", {
-              response: response,
-              surveyDate: survey.date_info
-            });
-            resolve(response);
+      if (state.startPoint && state.endPoint) {
+        return new Promise((resolve, reject) => {
+          commit("profileLoading", true);
+          const profileLine = new LineString([
+            state.startPoint,
+            state.endPoint
+          ]);
+          const geoJSON = generateFeatureRequest(
+            profileLine,
+            survey.bottleneck_id,
+            survey.date_info
+          );
+          HTTP.post("/cross", geoJSON, {
+            headers: { "X-Gemma-Auth": localStorage.getItem("token") }
           })
-          .catch(error => {
-            reject(error);
-          });
-      });
+            .then(response => {
+              commit("profileLoaded", {
+                response: response,
+                surveyDate: survey.date_info
+              });
+              resolve(response);
+            })
+            .catch(error => {
+              reject(error);
+            })
+            .finally(() => commit("profileLoading", false));
+        });
+      }
     },
     cut({ commit, dispatch, rootState, rootGetters }, cut) {
       const length = getLength(cut.getGeometry());
@@ -165,28 +176,29 @@
         const profileLine = new LineString([start, end]);
         dispatch("loadProfile", rootState.bottlenecks.selectedSurvey)
           .then(() => {
-            rootGetters["map/getLayerByName"]("Fairway Dimensions")
-              .data.getSource()
-              .forEachFeatureIntersectingExtent(
-                // need to use EPSG:3857 which is the proj of vectorSource
-                profileLine
+            rootState.map.cutTool.setActive(false);
+            rootGetters["map/getVSourceByName"](
+              "Fairway Dimensions"
+            ).forEachFeatureIntersectingExtent(
+              // need to use EPSG:3857 which is the proj of vectorSource
+              profileLine
+                .clone()
+                .transform("EPSG:4326", "EPSG:3857")
+                .getExtent(),
+              feature => {
+                // transform back to prepare for usage
+                var intersectingPolygon = feature
+                  .getGeometry()
                   .clone()
-                  .transform("EPSG:4326", "EPSG:3857")
-                  .getExtent(),
-                feature => {
-                  // transform back to prepare for usage
-                  var intersectingPolygon = feature
-                    .getGeometry()
-                    .clone()
-                    .transform("EPSG:3857", "EPSG:4326");
-                  const fairwayCoordinates = calculateFairwayCoordinates(
-                    profileLine,
-                    intersectingPolygon,
-                    DEMODATA
-                  );
-                  commit("setFairwayCoordinates", fairwayCoordinates);
-                }
-              );
+                  .transform("EPSG:3857", "EPSG:4326");
+                const fairwayCoordinates = calculateFairwayCoordinates(
+                  profileLine,
+                  intersectingPolygon,
+                  DEMODATA
+                );
+                commit("setFairwayCoordinates", fairwayCoordinates);
+              }
+            );
           })
           .then(() => {
             commit("application/showSplitscreen", true, { root: true });