Mercurial > gemma
changeset 4322:fabe67e204e7
bottleneckDialogue added
author | Thomas Junk <thomas.junk@intevation.de> |
---|---|
date | Wed, 04 Sep 2019 14:10:50 +0200 |
parents | 6dfbf534818b |
children | ab7d80baebe6 |
files | client/src/components/fairway/BottleneckDialogue.vue |
diffstat | 1 files changed, 702 insertions(+), 0 deletions(-) [+] |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/client/src/components/fairway/BottleneckDialogue.vue Wed Sep 04 14:10:50 2019 +0200 @@ -0,0 +1,702 @@ +<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"; + +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() { + this.$store.commit("application/popup", { + icon: "trash", + title: this.$gettext("Delete survey"), + content: + this.$gettext("Do you really want to delete the survey:") + + `<br> + <b>${this.selectedBottleneck}: ${this.selectedSurvey.date_info}</b>`, + confirm: { + label: this.$gettext("Delete"), + icon: "trash", + callback: () => { + console.log("delete selected"); + displayInfo({ title: this.$gettext("Profile deleted!") }); + } + }, + cancel: { + label: this.$gettext("Cancel"), + icon: "times" + } + }); + }, + 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.dispatch("map/moveToFeauture", { + feature: bottleneck, + zoom: 17, + preventZoomOut: true + }); + } + }, + mounted() { + this.$store.dispatch("bottlenecks/loadBottlenecksList"); + } +}; +</script>