Mercurial > gemma
view client/src/components/fairway/BottleneckDialogue.vue @ 4434:0ca5e6c4f195
Added tag v4-preview20190918 for changeset b5619087e3e9
author | Sascha Wilde <wilde@intevation.de> |
---|---|
date | Wed, 18 Sep 2019 18:04:39 +0200 |
parents | d336a78985ad |
children | 2d2c9f98360a |
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"; 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.selectedSurvey.bottleneck_id, "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>