Mercurial > gemma
view client/src/components/fairway/BottleneckDialogue.vue @ 5095:e21cbb9768a2
Prevent duplicate fairway areas
In principal, there can be only one or no fairway area at each point
on the map. Since polygons from real data will often be topologically
inexact, just disallow equal geometries. This will also help to
avoid importing duplicates with concurrent imports, once the history
of fairway dimensions will be preserved.
author | Tom Gottfried <tom@intevation.de> |
---|---|
date | Wed, 25 Mar 2020 18:10:02 +0100 |
parents | b5028b98e7c4 |
children | 901b70f9c6bd |
line wrap: on
line source
<template> <div :class="[ 'box ui-element rounded bg-white text-nowrap', { expanded: showProfiles } ]" > <div style="width: 17rem"> <UIBoxHeader icon="chart-area" :title="profilesLable" :closeCallback="close" /> <div class="box-body"> <UISpinnerOverlay v-if="surveysLoading || profileLoading" /> <div class="d-flex flex-row"> <select v-model="selectedBottleneck" class="w-90 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> <button @click="takeMeThere" class="btn btn-sm btn-info"> <font-awesome-icon icon="crosshairs" /> </button> </div> <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 v-if="isAllowedToDelete" 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>Custom Depth</translate>: </small> <div class="d-flex"> <input class="form-control form-control-sm w-100 mt-1" v-model.number="depth" type="number" step="0.1" min="0" /> <button @click="useCustomDepth = !useCustomDepth" :class="[ 'btn', 'btn-xs', 'ml-2', { 'btn-info': useCustomDepth, 'btn-secondary': !useCustomDepth } ]" > {{ useCustomDepth ? "disable" : "enable" }} </button> </div> <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("user", ["user"]), ...mapGetters("user", ["isWaterwayAdmin", "isSysAdmin"]), ...mapGetters("usermanagement", ["userCountries"]), ...mapState("application", ["showProfiles", "paneSetup"]), ...mapState("map", ["openLayersMaps", "syncedMaps", "cutToolEnabled"]), ...mapState("bottlenecks", [ "bottlenecksList", "surveys", "surveysLoading" ]), isAllowedToDelete() { const userCountryCode = this.userCountries[this.user]; const bottleneck = this.bottlenecksList.find( bn => bn.properties.name === this.selectedBottleneck ); if (!bottleneck) return; if (this.isWaterwayAdmin || this.isSysAdmin) { if ( userCountryCode === "global" || bottleneck.properties.responsible_country === userCountryCode ) { return true; } } return false; }, ...mapState("fairwayprofile", [ "previousCuts", "startPoint", "endPoint", "profileLoading", "differencesLoading", "currentDifference", "waterLevels", "currentProfile" ]), ...mapGetters("map", ["openLayersMap"]), ...mapGetters("bottlenecks", ["orderedBottlenecks"]), profilesLable() { return this.$gettext("Bottleneck Surveys"); }, useCustomDepth: { get() { return this.$store.state.fairwayprofile.useCustomDepth; }, set(value) { this.$store.commit("fairwayprofile/setUseCustomDepth", value); } }, depth: { get() { return this.$store.state.fairwayprofile.depth; }, set(value) { this.$store.commit("fairwayprofile/setDepth", value); } }, 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) { if (cut.depth) { this.depth = cut.depth; this.useCustomDepth = cut.useCustomDepth; } 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(response => { this.$store.commit( "fairwayprofile/setCurrentDifference", response.data.id ); if (this.openLayersMap(COMPARESURVEYS.compare.id)) { this.openLayersMap(COMPARESURVEYS.compare.id) .getLayer("DIFFERENCES") .getSource() .updateParams({ LAYERS: "sounding_differences", VERSION: "1.1.1", TILED: true, CQL_FILTER: "id=" + response.data.id }); this.openLayersMap(COMPARESURVEYS.compare.id) .getLayer("DIFFERENCES") .getSource() .refresh(); this.openLayersMap(COMPARESURVEYS.compare.id) .getLayer("DIFFERENCES") .setVisible(false); } }) .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; } if (status === 429) { displayInfo({ message: this.$gettext( "Too many difference calculations already in progress. Please try again later." ) }); } else { 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(), depth: this.depth, useCustomDepth: this.useCustomDepth }; 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 => { let message = "Backend not reachable"; if (error.response) { const { status, data } = error.response; message = `${status}: ${data.message || data}`; } displayError({ title: this.$gettext("Backend Error"), message: message }); }); }, 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" } }); }, takeMeThere() { const bottleneck = this.bottlenecksList.find( bn => bn.properties.name === this.selectedBottleneck ); if (!bottleneck) return; this.$store.commit( "bottlenecks/setBottleneckForPrint", this.selectedBottleneck ); this.$store.dispatch("map/moveToFeauture", { feature: bottleneck, zoom: 17, preventZoomOut: true }); } }, mounted() { this.$store.dispatch("usermanagement/loadUsers").catch(error => { let message = "Backend not reachable"; if (error.response) { const { status, data } = error.response; message = `${status}: ${data.message || data}`; } displayError({ title: this.$gettext("Backend Error"), message: message }); }); this.$store.dispatch("bottlenecks/loadBottlenecksList"); } }; </script>