Mercurial > gemma
diff client/src/components/fairway/Profiles.vue @ 1558:0ded4c56978e
refac: component filestructure. remove admin/map hierarchy
author | Thomas Junk <thomas.junk@intevation.de> |
---|---|
date | Wed, 12 Dec 2018 09:22:20 +0100 |
parents | client/src/components/map/fairway/Profiles.vue@35f85da41fdb |
children | f2d24dceecc7 |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/client/src/components/fairway/Profiles.vue Wed Dec 12 09:22:20 2018 +0100 @@ -0,0 +1,471 @@ +<template> + <div + :class="[ + 'box ui-element rounded bg-white text-nowrap', + { expanded: showProfiles } + ]" + > + <div> + <h6 class="mb-0 py-2 px-3 border-bottom d-flex align-items-center"> + <font-awesome-icon icon="chart-area" class="mr-2"></font-awesome-icon> + <translate>Profiles</translate> + <font-awesome-icon + icon="times" + class="ml-auto text-muted" + @click="$store.commit('application/showProfiles', false)" + ></font-awesome-icon> + </h6> + <div + class="d-flex flex-column p-3 flex-grow-1 text-left position-relative" + > + <div + class="loading d-flex justify-content-center align-items-center" + v-if="surveysLoading || profileLoading" + > + <font-awesome-icon icon="spinner" spin /> + </div> + <select + @click="moveToBottleneck" + v-model="selectedBottleneck" + class="form-control font-weight-bold" + > + <option :value="null"> + <translate>Select Bottleneck</translate> + </option> + <option + v-for="bn in bottlenecks" + :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>Sounding Result</translate>: + </small> + <select + v-model="selectedSurvey" + class="form-control form-control-sm" + > + <option + v-for="survey in surveys" + :key="survey.date_info" + :value="survey" + >{{ formatSurveyDate(survey.date_info) }}</option + > + </select> + </div> + <div + class="flex-fill ml-3" + 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" + > + <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-danger input-button-right" + @click="confirmDeleteSelectedCut = true" + v-if="selectedCut && !confirmDeleteSelectedCut" + > + <font-awesome-icon icon="trash" /> + </button> + <button + class="btn btn-sm btn-info rounded-0" + @click="confirmDeleteSelectedCut = false" + v-if="selectedCut && confirmDeleteSelectedCut" + > + <font-awesome-icon icon="times" /> + </button> + <button + class="btn btn-sm btn-danger input-button-right" + @click="deleteSelectedCut" + v-if="selectedCut && confirmDeleteSelectedCut" + > + <font-awesome-icon icon="check" /> + </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"; + +export default { + name: "profiles", + data() { + return { + coordinatesInput: "", + cutLabel: "", + showLabelInput: false, + confirmDeleteSelectedCut: false + }; + }, + computed: { + ...mapGetters("map", ["getVSourceByName"]), + ...mapState("application", ["showProfiles"]), + ...mapState("map", ["lineTool", "polygonTool", "cutTool"]), + ...mapState("bottlenecks", ["bottlenecks", "surveys", "surveysLoading"]), + ...mapState("fairwayprofile", [ + "previousCuts", + "startPoint", + "endPoint", + "profileLoading" + ]), + selectedBottleneck: { + get() { + return this.$store.state.bottlenecks.selectedBottleneck; + }, + set(name) { + this.$store + .dispatch("bottlenecks/setSelectedBottleneck", name) + .then(() => { + this.$store.commit("bottlenecks/setFirstSurveySelected"); + }); + } + }, + 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("Cut Tool").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.confirmDeleteSelectedCut = false; + this.applyCoordinates(cut.coordinates); + } + } + }, + methods: { + 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({ + title: this.$gettext("Success"), + 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("Cut Tool").clear(); + const cut = new Feature({ + geometry: new LineString([ + [coordinates[0], coordinates[1]], + [coordinates[2], coordinates[3]] + ]).transform("EPSG:4326", "EPSG:3857") + }); + this.getVSourceByName("Cut Tool").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() { + 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!") }); + }, + moveToBottleneck() { + const bottleneck = this.bottlenecks.find( + bn => bn.properties.name === this.selectedBottleneck + ); + if (!bottleneck) return; + this.$store.commit("map/moveMap", { + coordinates: bottleneck.geometry.coordinates, + zoom: 17, + preventZoomOut: true + }); + } + } +}; +</script>