view client/src/components/fairway/BottleneckDialogue.vue @ 4402:5e894e680e0d

dsr implemented in frontend
author Thomas Junk <thomas.junk@intevation.de>
date Mon, 16 Sep 2019 16:59:01 +0200
parents 36d384326407
children d336a78985ad
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="profilesLable"
        :closeCallback="close"
      />
      <div class="box-body">
        <UISpinnerOverlay v-if="surveysLoading || profileLoading" />
        <select
          @change="moveToBottleneck"
          v-model="selectedBottleneck"
          class="form-control font-weight-bold"
        >
          <option :value="null">
            <translate>Select Bottleneck</translate>
          </option>
          <optgroup
            v-for="(bottlenecksForCountry, cc) in orderedBottlenecks"
            :key="cc"
            :label="cc"
          >
            <option
              v-for="bn in bottlenecksForCountry"
              :key="bn.properties.id"
              :value="bn.properties.name"
            >
              {{ bn.properties.name }}
            </option>
          </optgroup>
        </select>
        <div v-if="selectedBottleneck">
          <div class="d-flex flex-column 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="ref">
                  <translate>Depth Reference</translate>
                  <template v-if="selectedSurvey">
                    ({{ selectedSurvey.depth_reference }}/{{
                      $options.filters.waterlevel(
                        selectedSurvey.waterlevel_value
                      )
                    }}
                    m)
                  </template>
                </option>
                <option value="current">
                  <translate>Current Waterlevel</translate>
                  <template v-if="bottleneck">
                    ({{
                      $options.filters.waterlevel(
                        bottleneck.get("gm_waterlevel")
                      )
                    }}
                    m)
                  </template>
                </option>
              </select>
            </div>
            <div class="flex-fill">
              <small class="text-muted"> <translate>Survey</translate>: </small>
              <div class="d-flex">
                <select
                  v-model="selectedSurvey"
                  class="form-control form-control-sm small"
                >
                  <option
                    v-for="survey in surveys"
                    :key="survey.date_info"
                    :value="survey"
                    >{{ survey.date_info | surveyDate }}</option
                  >
                </select>
                <button
                  class="btn btn-dark btn-xs ml-2"
                  @click="deleteSelectedSurvey"
                >
                  <font-awesome-icon icon="trash" />
                </button>
              </div>
            </div>
            <div class="flex-fill" 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"
                  >{{ survey.date_info | surveyDate }}</option
                >
              </select>
            </div>
          </div>
          <div class="mt-2 d-flex" v-if="additionalSurvey">
            <button
              v-if="differencesLoading"
              class="btn btn-info btn-xs flex-fill"
              disabled
            >
              <font-awesome-icon icon="spinner" spin class="mr-1" />
              <translate>Calculating differences</translate>
            </button>
            <button
              class="btn btn-info btn-xs flex-fill"
              @click="differencesVisible ? showSurvey() : showDifferences()"
              v-else
            >
              <translate v-if="differencesVisible" key="showsurvey"
                >Show survey</translate
              >
              <translate v-else key="showdifferences"
                >Show differences</translate
              >
            </button>
            <button
              v-if="!paneSetup.includes('FAIRWAYPROFILE')"
              class="btn btn-info btn-xs ml-2"
              @click="$store.commit('application/paneRotate')"
              v-tooltip="rotatePanesTooltip"
            >
              <font-awesome-icon icon="redo" fixed-width />
            </button>
            <button
              class="btn btn-info btn-xs ml-2"
              @click="toggleSyncMaps()"
              v-tooltip="syncMapsTooltip"
            >
              <font-awesome-icon
                :icon="mapsAreSynced ? 'unlink' : 'link'"
                fixed-width
              />
            </button>
          </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-xs btn-dark ml-2"
              @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"
                :disabled="!selectedSurvey"
              >
                <font-awesome-icon :icon="cutToolEnabled ? 'times' : 'plus'" />
                {{ cutToolEnabled ? "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>
.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;
}

input,
select {
  font-size: 0.8em;
}
</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";
import { HTTP } from "@/lib/http";
import { COMPARESURVEYS } from "@/components/paneSetups";
import lib from "@/lib/filters";

export default {
  name: "profiles",
  data() {
    return {
      coordinatesInput: "",
      cutLabel: "",
      showLabelInput: false
    };
  },
  computed: {
    ...mapState("application", ["showProfiles", "paneSetup"]),
    ...mapState("map", ["openLayersMaps", "syncedMaps", "cutToolEnabled"]),
    ...mapState("bottlenecks", [
      "bottlenecksList",
      "surveys",
      "surveysLoading"
    ]),
    ...mapState("fairwayprofile", [
      "previousCuts",
      "startPoint",
      "endPoint",
      "profileLoading",
      "differencesLoading",
      "waterLevels",
      "currentProfile"
    ]),
    ...mapGetters("map", ["openLayersMap"]),
    ...mapGetters("bottlenecks", ["orderedBottlenecks"]),
    profilesLable() {
      return this.$gettext("Bottleneck Surveys");
    },
    selectedBottleneck: {
      get() {
        return this.$store.state.bottlenecks.selectedBottleneck;
      },
      set(name) {
        this.$store.dispatch("bottlenecks/setSelectedBottleneck", name);
      }
    },
    selectedWaterLevel: {
      get() {
        return this.$store.state.fairwayprofile.selectedWaterLevel;
      },
      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.openLayersMaps.forEach(m => {
            m.getLayer("CUTTOOL")
              .getSource()
              .clear();
          });
        }
      }
    },
    additionalSurveys() {
      return this.surveys.filter(
        survey => survey.date_info !== this.selectedSurvey.date_info
      );
    },
    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;
    },
    differencesVisible() {
      return (
        this.openLayersMap(COMPARESURVEYS.compare.id) &&
        !this.openLayersMap(COMPARESURVEYS.compare.id)
          .getLayer("BOTTLENECKISOLINE")
          .getVisible() &&
        this.openLayersMap(COMPARESURVEYS.compare.id)
          .getLayer("DIFFERENCES")
          .getVisible()
      );
    },
    rotatePanesTooltip() {
      return this.$gettext("Rotate Maps");
    },
    syncMapsTooltip() {
      return this.$gettext(
        this.mapsAreSynced ? "Unsynchronize Maps" : "Synchronize Maps"
      );
    },
    mapsAreSynced() {
      return this.syncedMaps.includes(COMPARESURVEYS.compare.id);
    },
    bottleneck() {
      return this.openLayersMap()
        ? this.openLayersMap()
            .getLayer("BOTTLENECKS")
            .getSource()
            .getFeatures()
            .find(f => f.get("objnam") === this.selectedBottleneck)
        : null;
    }
  },
  watch: {
    selectedBottleneck() {
      this.$store.dispatch("fairwayprofile/previousCuts");
      this.cutLabel =
        this.selectedBottleneck + " (" + new Date().toISOString() + ")";
    },
    selectedSurvey(survey) {
      this.loadProfile(survey);
    },
    additionalSurvey(survey) {
      if (survey) {
        this.loadDifferences();
        this.$store.commit(
          "application/paneSetup",
          Object.keys(this.currentProfile).length
            ? "COMPARESURVEYS_FAIRWAYPROFILE"
            : "COMPARESURVEYS"
        );
        this.$store.commit("map/syncedMaps", [COMPARESURVEYS.compare.id]);
      } else {
        this.$store.commit(
          "application/paneSetup",
          Object.keys(this.currentProfile).length ? "FAIRWAYPROFILE" : "DEFAULT"
        );
        this.$store.commit("map/syncedMaps", []);
      }
      this.loadProfile(survey);
    },
    selectedCut(cut) {
      if (cut) {
        this.applyCoordinates(cut.coordinates);
      }
    }
  },
  methods: {
    toggleSyncMaps() {
      if (this.mapsAreSynced) {
        this.$store.commit(
          "map/syncedMaps",
          this.syncedMaps.filter(m => m !== COMPARESURVEYS.compare.id)
        );
      } else {
        this.$store.commit("map/syncedMaps", [COMPARESURVEYS.compare.id]);
      }
    },
    loadDifferences() {
      this.$store.commit("fairwayprofile/setDifferencesLoading", true);
      HTTP.post(
        "/diff",
        {
          bottleneck: this.selectedSurvey.bottleneck_id,
          minuend: this.selectedSurvey.date_info,
          subtrahend: this.additionalSurvey.date_info
        },
        {
          headers: {
            "X-Gemma-Auth": localStorage.getItem("token")
          }
        }
      )
        .then()
        .catch(error => {
          let status, data, message;
          if (error.response) {
            status = error.response.status;
            data = error.response.data;
            message = `${status}: ${data.message || data}`;
          } else {
            message = error;
          }
          displayError({
            title: this.$gettext("Backend Error"),
            message: message
          });
        })
        .finally(() => {
          this.$store.commit("fairwayprofile/setDifferencesLoading", false);
        });
    },
    showDifferences() {
      this.openLayersMap(COMPARESURVEYS.compare.id)
        .getLayer("BOTTLENECKISOLINE")
        .setVisible(false);
      this.openLayersMap(COMPARESURVEYS.compare.id)
        .getLayer("DIFFERENCES")
        .setVisible(true);
    },
    showSurvey() {
      this.openLayersMap(COMPARESURVEYS.compare.id)
        .getLayer("BOTTLENECKISOLINE")
        .setVisible(true);
      this.openLayersMap(COMPARESURVEYS.compare.id)
        .getLayer("DIFFERENCES")
        .setVisible(false);
    },
    close() {
      this.$store.commit("application/showProfiles", false);
    },
    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.$store.commit("map/cutToolEnabled", !this.cutToolEnabled);
      this.$store.commit("map/lineToolEnabled", false);
      this.$store.commit("map/polygonToolEnabled", 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.openLayersMaps.forEach(m => {
          m.getLayer("CUTTOOL")
            .getSource()
            .clear();
        });
        const cut = new Feature({
          geometry: new LineString([
            [coordinates[0], coordinates[1]],
            [coordinates[2], coordinates[3]]
          ]).transform("EPSG:4326", "EPSG:3857")
        });
        this.openLayersMaps.forEach(m => {
          m.getLayer("CUTTOOL")
            .getSource()
            .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.'
        )
      });
    },
    deleteSelectedSurvey() {
      HTTP.post(
        "/imports/dsr",
        {
          "bottleneck-id": this.selectedBottleneck,
          "date-info": this.selectedSurvey.date_info
        },
        {
          headers: { "X-Gemma-Auth": localStorage.getItem("token") }
        }
      )
        .then(() => {
          displayInfo({
            title: this.$gettext("Survey"),
            message:
              this.$gettext("Deleting ") +
              `${this.selectedBottleneck}: ${this.selectedSurvey.date_info}`
          });
        })
        .catch(error => {
          const { status, data } = error.response;
          displayError({
            title: this.$gettext("Backend Error"),
            message: `${status}: ${data.message || data}`
          });
        });
    },
    deleteSelectedCut(cut) {
      this.$store.commit("application/popup", {
        icon: "trash",
        title: this.$gettext("Delete cross profile"),
        content:
          `<small><b>` +
          this.$gettext("Do you really want to delete the cross profile:") +
          `</b><br>
        ${cut.label}</small>`,
        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.dispatch("map/moveToFeauture", {
        feature: bottleneck,
        zoom: 17,
        preventZoomOut: true
      });
    }
  },
  mounted() {
    this.$store.dispatch("bottlenecks/loadBottlenecksList");
  }
};
</script>