diff client/src/components/fairway/Profiles.vue @ 1558:0ded4c56978e

refac: component filestructure. remove admin/map hierarchy
author Thomas Junk <thomas.junk@intevation.de>
date Wed, 12 Dec 2018 09:22:20 +0100
parents client/src/components/map/fairway/Profiles.vue@35f85da41fdb
children f2d24dceecc7
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/client/src/components/fairway/Profiles.vue	Wed Dec 12 09:22:20 2018 +0100
@@ -0,0 +1,471 @@
+<template>
+  <div
+    :class="[
+      'box ui-element rounded bg-white text-nowrap',
+      { expanded: showProfiles }
+    ]"
+  >
+    <div>
+      <h6 class="mb-0 py-2 px-3 border-bottom d-flex align-items-center">
+        <font-awesome-icon icon="chart-area" class="mr-2"></font-awesome-icon>
+        <translate>Profiles</translate>
+        <font-awesome-icon
+          icon="times"
+          class="ml-auto text-muted"
+          @click="$store.commit('application/showProfiles', false)"
+        ></font-awesome-icon>
+      </h6>
+      <div
+        class="d-flex flex-column p-3 flex-grow-1 text-left position-relative"
+      >
+        <div
+          class="loading d-flex justify-content-center align-items-center"
+          v-if="surveysLoading || profileLoading"
+        >
+          <font-awesome-icon icon="spinner" spin />
+        </div>
+        <select
+          @click="moveToBottleneck"
+          v-model="selectedBottleneck"
+          class="form-control font-weight-bold"
+        >
+          <option :value="null">
+            <translate>Select Bottleneck</translate>
+          </option>
+          <option
+            v-for="bn in bottlenecks"
+            :key="bn.properties.name"
+            :value="bn.properties.name"
+            >{{ bn.properties.name }}</option
+          >
+        </select>
+        <div v-if="selectedBottleneck">
+          <div class="d-flex mt-2">
+            <div class="flex-fill">
+              <small class="text-muted">
+                <translate>Sounding Result</translate>:
+              </small>
+              <select
+                v-model="selectedSurvey"
+                class="form-control form-control-sm"
+              >
+                <option
+                  v-for="survey in surveys"
+                  :key="survey.date_info"
+                  :value="survey"
+                  >{{ formatSurveyDate(survey.date_info) }}</option
+                >
+              </select>
+            </div>
+            <div
+              class="flex-fill ml-3"
+              v-if="selectedSurvey && surveys.length > 1"
+            >
+              <small class="text-muted mt-1">
+                <translate>Compare with</translate>:
+              </small>
+              <select
+                v-model="additionalSurvey"
+                class="form-control form-control-sm"
+              >
+                <option :value="null">None</option>
+                <option
+                  v-for="survey in additionalSurveys"
+                  :key="survey.date_info"
+                  :value="survey"
+                  >{{ formatSurveyDate(survey.date_info) }}</option
+                >
+              </select>
+            </div>
+          </div>
+          <hr class="w-100 mb-0" />
+          <small class="text-muted d-block mt-2">
+            <translate>Saved cross profiles</translate>:
+          </small>
+          <div class="d-flex">
+            <select
+              :class="[
+                'form-control form-control-sm flex-fill',
+                { 'rounded-left-only': selectedCut }
+              ]"
+              v-model="selectedCut"
+            >
+              <option></option>
+              <option
+                v-for="(cut, index) in previousCuts"
+                :value="cut"
+                :key="index"
+                >{{ cut.label }}</option
+              >
+            </select>
+            <button
+              class="btn btn-sm btn-danger input-button-right"
+              @click="confirmDeleteSelectedCut = true"
+              v-if="selectedCut && !confirmDeleteSelectedCut"
+            >
+              <font-awesome-icon icon="trash" />
+            </button>
+            <button
+              class="btn btn-sm btn-info rounded-0"
+              @click="confirmDeleteSelectedCut = false"
+              v-if="selectedCut && confirmDeleteSelectedCut"
+            >
+              <font-awesome-icon icon="times" />
+            </button>
+            <button
+              class="btn btn-sm btn-danger input-button-right"
+              @click="deleteSelectedCut"
+              v-if="selectedCut && confirmDeleteSelectedCut"
+            >
+              <font-awesome-icon icon="check" />
+            </button>
+          </div>
+          <small class="text-muted d-block mt-2">
+            <translate>Enter coordinates manually</translate>:
+          </small>
+          <div class="position-relative">
+            <input
+              class="form-control form-control-sm pr-5"
+              placeholder="Lat,Lon,Lat,Lon"
+              v-model="coordinatesInput"
+            />
+            <button
+              class="btn btn-sm btn-info position-absolute input-button-right"
+              @click="applyManualCoordinates"
+              style="top: 0; right: 0;"
+              v-if="coordinatesInputIsValid"
+            >
+              <font-awesome-icon icon="check" />
+            </button>
+          </div>
+          <small class="d-flex text-left mt-2" v-if="startPoint && endPoint">
+            <div class="text-nowrap mr-3">
+              <b> <translate>Start</translate>: </b> <br />
+              Lat: {{ startPoint[1] }} <br />
+              Lon: {{ startPoint[0] }}
+            </div>
+            <div class="text-nowrap">
+              <b>End:</b> <br />
+              Lat: {{ endPoint[1] }} <br />
+              Lon: {{ endPoint[0] }}
+            </div>
+            <button
+              v-clipboard:copy="coordinatesForClipboard"
+              v-clipboard:success="onCopyCoordinates"
+              class="btn btn-info btn-sm ml-auto mt-auto"
+            >
+              <font-awesome-icon icon="copy" />
+            </button>
+          </small>
+          <div class="d-flex mt-3">
+            <div
+              class="pr-3 w-50"
+              v-if="startPoint && endPoint && !selectedCut"
+            >
+              <button
+                class="btn btn-info btn-sm w-100"
+                @click="showLabelInput = !showLabelInput"
+              >
+                <font-awesome-icon :icon="showLabelInput ? 'times' : 'check'" />
+                {{ showLabelInput ? "Cancel" : "Save" }}
+              </button>
+            </div>
+            <div
+              :class="startPoint && endPoint && !selectedCut ? 'w-50' : 'w-100'"
+            >
+              <button class="btn btn-info btn-sm w-100" @click="toggleCutTool">
+                <font-awesome-icon
+                  :icon="cutTool && cutTool.getActive() ? 'times' : 'plus'"
+                ></font-awesome-icon>
+                {{ cutTool && cutTool.getActive() ? "Cancel" : "New" }}
+              </button>
+            </div>
+          </div>
+          <div v-if="showLabelInput" class="mt-2">
+            <small class="text-muted">
+              <translate>Enter label for cross profile</translate>:
+            </small>
+            <div class="position-relative">
+              <input
+                class="form-control form-control-sm pr-5"
+                v-model="cutLabel"
+              />
+              <button
+                class="btn btn-sm btn-info position-absolute input-button-right"
+                @click="saveCut"
+                v-if="cutLabel"
+                style="top: 0; right: 0;"
+              >
+                <font-awesome-icon icon="check" />
+              </button>
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<style lang="scss" scoped>
+.loading {
+  background: rgba(255, 255, 255, 0.9);
+  position: absolute;
+  z-index: 99;
+  top: 0;
+  right: 0;
+  bottom: 0;
+  left: 0;
+}
+
+.input-button-right {
+  border-top-right-radius: $border-radius;
+  border-bottom-right-radius: $border-radius;
+  border-top-left-radius: 0 !important;
+  border-bottom-left-radius: 0 !important;
+}
+
+.rounded-left-only {
+  border-top-right-radius: 0 !important;
+  border-bottom-right-radius: 0 !important;
+  border-top-left-radius: $border-radius;
+  border-bottom-left-radius: $border-radius;
+}
+</style>
+
+<script>
+/* This is Free Software under GNU Affero General Public License v >= 3.0
+ * without warranty, see README.md and license for details.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ * License-Filename: LICENSES/AGPL-3.0.txt
+ *
+ * Copyright (C) 2018 by via donau
+ *   – Österreichische Wasserstraßen-Gesellschaft mbH
+ * Software engineering by Intevation GmbH
+ *
+ * Author(s):
+ * Markus Kottländer <markus.kottlaender@intevation.de>
+ */
+import { mapState, mapGetters } from "vuex";
+import Feature from "ol/Feature";
+import LineString from "ol/geom/LineString";
+import { displayError, displayInfo } from "../../lib/errors.js";
+import { formatSurveyDate } from "../../lib/date.js";
+
+export default {
+  name: "profiles",
+  data() {
+    return {
+      coordinatesInput: "",
+      cutLabel: "",
+      showLabelInput: false,
+      confirmDeleteSelectedCut: false
+    };
+  },
+  computed: {
+    ...mapGetters("map", ["getVSourceByName"]),
+    ...mapState("application", ["showProfiles"]),
+    ...mapState("map", ["lineTool", "polygonTool", "cutTool"]),
+    ...mapState("bottlenecks", ["bottlenecks", "surveys", "surveysLoading"]),
+    ...mapState("fairwayprofile", [
+      "previousCuts",
+      "startPoint",
+      "endPoint",
+      "profileLoading"
+    ]),
+    selectedBottleneck: {
+      get() {
+        return this.$store.state.bottlenecks.selectedBottleneck;
+      },
+      set(name) {
+        this.$store
+          .dispatch("bottlenecks/setSelectedBottleneck", name)
+          .then(() => {
+            this.$store.commit("bottlenecks/setFirstSurveySelected");
+          });
+      }
+    },
+    selectedSurvey: {
+      get() {
+        return this.$store.state.bottlenecks.selectedSurvey;
+      },
+      set(survey) {
+        this.$store.commit("fairwayprofile/additionalSurvey", null);
+        this.$store.commit("bottlenecks/selectedSurvey", survey);
+      }
+    },
+    additionalSurvey: {
+      get() {
+        return this.$store.state.fairwayprofile.additionalSurvey;
+      },
+      set(survey) {
+        this.$store.commit("fairwayprofile/additionalSurvey", survey);
+      }
+    },
+    selectedCut: {
+      get() {
+        return this.$store.state.fairwayprofile.selectedCut;
+      },
+      set(cut) {
+        this.$store.commit("fairwayprofile/selectedCut", cut);
+        if (!cut) {
+          this.$store.commit("fairwayprofile/clearCurrentProfile");
+          this.$store.commit("application/showSplitscreen", false);
+          this.getVSourceByName("Cut Tool").clear();
+        }
+      }
+    },
+    additionalSurveys() {
+      return this.surveys.filter(survey => survey !== this.selectedSurvey);
+    },
+    coordinatesForClipboard() {
+      return (
+        this.startPoint[1] +
+        "," +
+        this.startPoint[0] +
+        "," +
+        this.endPoint[1] +
+        "," +
+        this.endPoint[0]
+      );
+    },
+    coordinatesInputIsValid() {
+      const coordinates = this.coordinatesInput
+        .split(",")
+        .map(coord => parseFloat(coord.trim()))
+        .filter(c => Number(c) === c);
+      return coordinates.length === 4;
+    }
+  },
+  watch: {
+    selectedBottleneck() {
+      this.$store.dispatch("fairwayprofile/previousCuts");
+      this.cutLabel =
+        this.selectedBottleneck + " (" + new Date().toISOString() + ")";
+    },
+    selectedSurvey(survey) {
+      this.loadProfile(survey);
+    },
+    additionalSurvey(survey) {
+      this.loadProfile(survey);
+    },
+    selectedCut(cut) {
+      if (cut) {
+        this.confirmDeleteSelectedCut = false;
+        this.applyCoordinates(cut.coordinates);
+      }
+    }
+  },
+  methods: {
+    formatSurveyDate(date) {
+      return formatSurveyDate(date);
+    },
+    loadProfile(survey) {
+      if (survey) {
+        this.$store.commit("fairwayprofile/profileLoading", true);
+        this.$store
+          .dispatch("fairwayprofile/loadProfile", survey)
+          .finally(() =>
+            this.$store.commit("fairwayprofile/profileLoading", false)
+          );
+      }
+    },
+    toggleCutTool() {
+      this.cutTool.setActive(!this.cutTool.getActive());
+      this.lineTool.setActive(false);
+      this.polygonTool.setActive(false);
+      this.$store.commit("map/setCurrentMeasurement", null);
+    },
+    onCopyCoordinates() {
+      displayInfo({
+        title: this.$gettext("Success"),
+        message: this.$gettext("Coordinates copied to clipboard!")
+      });
+    },
+    applyManualCoordinates() {
+      const coordinates = this.coordinatesInput
+        .split(",")
+        .map(coord => parseFloat(coord.trim()));
+      this.selectedCut = null;
+      this.coordinatesInput = "";
+      this.applyCoordinates([
+        coordinates[1],
+        coordinates[0],
+        coordinates[3],
+        coordinates[2]
+      ]);
+    },
+    applyCoordinates(coordinates) {
+      // allow only numbers
+      coordinates = coordinates.filter(c => Number(c) === c);
+      if (coordinates.length === 4) {
+        // draw line on map
+        this.getVSourceByName("Cut Tool").clear();
+        const cut = new Feature({
+          geometry: new LineString([
+            [coordinates[0], coordinates[1]],
+            [coordinates[2], coordinates[3]]
+          ]).transform("EPSG:4326", "EPSG:3857")
+        });
+        this.getVSourceByName("Cut Tool").addFeature(cut);
+
+        // draw diagram
+        this.$store.dispatch("fairwayprofile/cut", cut);
+      } else {
+        displayError({
+          title: this.$gettext("Invalid input"),
+          message: this.$gettext(
+            "Please enter correct coordinates in the format: Lat,Lon,Lat,Lon"
+          )
+        });
+      }
+    },
+    saveCut() {
+      const previousCuts =
+        JSON.parse(localStorage.getItem("previousCuts")) || [];
+      const newEntry = {
+        label: this.cutLabel,
+        bottleneckName: this.selectedBottleneck,
+        coordinates: [...this.startPoint, ...this.endPoint],
+        timestamp: new Date().getTime()
+      };
+      const existingEntry = previousCuts.find(cut => {
+        return JSON.stringify(cut) === JSON.stringify(newEntry);
+      });
+      if (!existingEntry) previousCuts.push(newEntry);
+      if (previousCuts.length > 100) previousCuts.shift();
+      localStorage.setItem("previousCuts", JSON.stringify(previousCuts));
+      this.$store.dispatch("fairwayprofile/previousCuts");
+
+      this.showLabelInput = false;
+      displayInfo({
+        title: this.$gettext("Profile saved!"),
+        message: this.$gettext(
+          'You can now select these coordinates from the "Saved cross profiles" menu to restore this cross profile.'
+        )
+      });
+    },
+    deleteSelectedCut() {
+      let previousCuts = JSON.parse(localStorage.getItem("previousCuts")) || [];
+      previousCuts = previousCuts.filter(cut => {
+        return JSON.stringify(cut) !== JSON.stringify(this.selectedCut);
+      });
+      localStorage.setItem("previousCuts", JSON.stringify(previousCuts));
+      this.$store.commit("fairwayprofile/selectedCut", null);
+      this.$store.dispatch("fairwayprofile/previousCuts");
+      displayInfo({ title: this.$gettext("Profile deleted!") });
+    },
+    moveToBottleneck() {
+      const bottleneck = this.bottlenecks.find(
+        bn => bn.properties.name === this.selectedBottleneck
+      );
+      if (!bottleneck) return;
+      this.$store.commit("map/moveMap", {
+        coordinates: bottleneck.geometry.coordinates,
+        zoom: 17,
+        preventZoomOut: true
+      });
+    }
+  }
+};
+</script>