view client/src/components/map/fairway/Profiles.vue @ 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
children fa7d647f8d77
line wrap: on
line source

<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>