view client/src/components/fairway/Profiles.vue @ 2540:3c17d401fbd4

client: cross profiles: moved waterlevel select to Profiles dialog aaaand also switched to the popup component as confirmation mechanism for deleting saved profiles
author Markus Kottlaender <markus@intevation.de>
date Thu, 07 Mar 2019 15:19:16 +0100
parents bb5286acfee2
children 468c8dc796cf
line wrap: on
line source

<template>
  <div
    :class="[
      'box ui-element rounded bg-white text-nowrap',
      { expanded: showProfiles }
    ]"
  >
    <div style="width: 18rem">
      <UIBoxHeader icon="chart-area" title="Profiles" :closeCallback="close" />
      <div class="box-body">
        <div
          class="loading d-flex justify-content-center align-items-center"
          v-if="surveysLoading || profileLoading"
        >
          <font-awesome-icon icon="spinner" spin />
        </div>
        <select
          @change="moveToBottleneck"
          v-model="selectedBottleneck"
          class="form-control font-weight-bold"
        >
          <option :value="null">
            <translate>Select Bottleneck</translate>
          </option>
          <option
            v-for="bn in bottlenecksList"
            :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>Waterlevel</translate>:
              </small>
              <select
                v-model="selectedWaterLevel"
                class="form-control form-control-sm small"
              >
                <option value="" v-if="Object.keys(waterLevels).length === 0">
                  <translate>Current</translate>
                </option>
                <option
                  v-for="wl in Object.keys(waterLevels)"
                  :key="wl"
                  :value="wl"
                >
                  {{ formatSurveyDate(wl) }}
                </option>
              </select>
            </div>
            <div class="flex-fill ml-2">
              <small class="text-muted"> <translate>Survey</translate>: </small>
              <select
                v-model="selectedSurvey"
                class="form-control form-control-sm small"
              >
                <option
                  v-for="survey in surveys"
                  :key="survey.date_info"
                  :value="survey"
                  >{{ formatSurveyDate(survey.date_info) }}</option
                >
              </select>
            </div>
            <div
              class="flex-fill ml-2"
              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 small"
              >
                <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-dark input-button-right"
              @click="deleteSelectedCut(selectedCut)"
              v-if="selectedCut"
            >
              <font-awesome-icon icon="trash" />
            </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";
import { LAYERS } from "@/store/map.js";

export default {
  name: "profiles",
  data() {
    return {
      coordinatesInput: "",
      cutLabel: "",
      showLabelInput: false
    };
  },
  computed: {
    ...mapGetters("map", ["getVSourceByName"]),
    ...mapState("application", ["showProfiles"]),
    ...mapState("map", ["lineTool", "polygonTool", "cutTool"]),
    ...mapState("bottlenecks", [
      "bottlenecksList",
      "surveys",
      "surveysLoading"
    ]),
    ...mapState("fairwayprofile", [
      "previousCuts",
      "startPoint",
      "endPoint",
      "profileLoading",
      "waterLevels"
    ]),
    selectedBottleneck: {
      get() {
        return this.$store.state.bottlenecks.selectedBottleneck;
      },
      set(name) {
        this.$store
          .dispatch("bottlenecks/setSelectedBottleneck", name)
          .then(() => {
            this.$store.commit("bottlenecks/setFirstSurveySelected");
          });
      }
    },
    selectedWaterLevel: {
      get() {
        return this.$store.state.fairwayprofile.selectedWaterLevel.date || "";
      },
      set(value) {
        this.$store.commit("fairwayprofile/setSelectedWaterLevel", value);
      }
    },
    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(LAYERS.CUTTOOL).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.applyCoordinates(cut.coordinates);
      }
    }
  },
  methods: {
    close() {
      this.$store.commit("application/showProfiles", false);
    },
    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({
        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(LAYERS.CUTTOOL).clear();
        const cut = new Feature({
          geometry: new LineString([
            [coordinates[0], coordinates[1]],
            [coordinates[2], coordinates[3]]
          ]).transform("EPSG:4326", "EPSG:3857")
        });
        this.getVSourceByName(LAYERS.CUTTOOL).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(cut) {
      this.$store.commit("application/popup", {
        icon: "trash",
        title: this.$gettext("Delete cross profile"),
        content:
          this.$gettext("Do you really want to delete the cross profile:") +
          `<br>
        <b>${cut.label}</b>`,
        confirm: {
          label: this.$gettext("Delete"),
          icon: "trash",
          callback: () => {
            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!") });
          }
        },
        cancel: {
          label: this.$gettext("Cancel"),
          icon: "times"
        }
      });
    },
    moveToBottleneck() {
      const bottleneck = this.bottlenecksList.find(
        bn => bn.properties.name === this.selectedBottleneck
      );
      if (!bottleneck) return;
      this.$store.commit("map/moveToExtent", {
        feature: bottleneck,
        zoom: 17,
        preventZoomOut: true
      });
    }
  },
  mounted() {
    this.$store.dispatch("bottlenecks/loadBottlenecksList");
  }
};
</script>