Mercurial > gemma
view client/src/components/map/fairway/Fairwayprofile.vue @ 1312:3c37017f5eb8
fixed cross profile diagram after switching to to admin context and back and it's responsive behavior
The recalculation of the available space (scaleFairwayProfile) needs to be done after removing the current diagram (in drawDiagram).
author | Markus Kottlaender <markus@intevation.de> |
---|---|
date | Fri, 23 Nov 2018 13:57:31 +0100 |
parents | 6c0c204f6bce |
children | e4e35fb2d995 |
line wrap: on
line source
<template> <div :class="['position-relative', {show: showSplitscreen}]" v-if="Object.keys(currentProfile).length"> <button class="rounded-bottom bg-white border-0 position-absolute splitscreen-toggle" @click="$store.commit('application/showSplitscreen', false)" v-if="showSplitscreen"> <font-awesome-icon icon="angle-down" /> </button> <button class="rounded-bottom bg-white border-0 position-absolute clear-selection" @click="$store.dispatch('fairwayprofile/clearSelection');" v-if="showSplitscreen"> <font-awesome-icon icon="times" /> </button> <div class="profile bg-white position-relative d-flex flex-column"> <h5 class="headline border-bottom mb-0 py-2"> {{ selectedBottleneck }} ({{ selectedSurvey.date_info }}) </h5> <div class="d-flex flex-fill"> <div class="fairwayprofile m-3 mt-0 bg-white flex-grow-1"></div> <div class="additionalsurveys d-flex flex-column"> <small> Additional Surveys <select v-model="additionalSurvey" class="form-control form-control-sm"> <option value="">None</option> <option v-for="survey in additionalSurveys" :key="survey.date_info" :value="survey" >{{survey.date_info}}</option> </select> <hr> <div class="d-flex text-left mb-2"> <div class="text-nowrap mr-1"> <b>Start:</b> <br> Lat: {{ startPoint[1] }} <br> Lon: {{ startPoint[0] }} </div> <div class="text-nowrap ml-1"> <b>End:</b> <br> Lat: {{ endPoint[1] }} <br> Lon: {{ endPoint[0] }} </div> <button class="btn btn-outline-secondary btn-sm ml-2 mt-auto" @click="showLabelInput = !showLabelInput"> <font-awesome-icon :icon="showLabelInput ? 'times' : 'folder-plus'" size="lg" /> </button> <button v-clipboard:copy="coordinatesForClipboard" v-clipboard:success="onCopyCoordinates" class="btn btn-outline-secondary btn-sm ml-2 mt-auto"> <font-awesome-icon icon="copy" /> </button> </div> <div v-if="showLabelInput"> Enter label for cross profile: <div class="position-relative"> <input class="form-control form-control-sm pr-5" v-model="cutLabel" /><br> <button class="btn btn-sm btn-outline-secondary position-absolute" @click="saveCut" v-if="cutLabel" style="top: 0; right: 0;"> <font-awesome-icon icon="check" /> </button> </div> </div> Saved cross profiles: <select class="form-control form-control-sm mb-2" v-model="coordinatesSelect"> <option></option> <option v-for="(cut, index) in previousCuts" :value="cut.coordinates" :key="index"> {{ cut.label }} </option> </select> Enter coordinates manually: <div class="position-relative"> <input class="form-control form-control-sm pr-5" placeholder="Lat,Lon,Lat,Lon" v-model="coordinatesInput" /><br> <button class="btn btn-sm btn-outline-secondary position-absolute" @click="applyManualCoordinates" style="top: 0; right: 0;" v-if="coordinatesInput"> <font-awesome-icon icon="check" /> </button> </div> </small> </div> </div> </div> </div> </template> <style lang="sass" scoped> .profile width: 100vw height: 0 overflow: hidden z-index: 2 .headline border-top: solid 3px $color-info .splitscreen-toggle, .clear-selection width: $icon-width height: $icon-height margin-top: 8px z-index: 3 outline: none svg path fill: #666 .splitscreen-toggle right: $small-offset + $icon-width .clear-selection right: $small-offset .show .profile height: 50vh .waterlevelselection margin-top: $large-offset margin-right: $large-offset .additionalsurveys margin-top: $large-offset margin-bottom: auto margin-right: $large-offset margin-left: auto max-width: 300px .additionalsurveys input margin-right: $small-offset </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): * Thomas Junk <thomas.junk@intevation.de> */ import * as d3 from "d3"; import { mapState, mapGetters } from "vuex"; import { displayError, displayInfo } from "../../../lib/errors.js"; import Feature from "ol/Feature"; import LineString from "ol/geom/LineString"; import debounce from "debounce"; const GROUND_COLOR = "#4A2F06"; export default { name: "fairwayprofile", props: [ "xScale", "yScaleLeft", "yScaleRight", "margin", "additionalSurveys" ], data() { return { wait: false, coordinatesInput: "", coordinatesSelect: null, cutLabel: "", showLabelInput: false, width: null, height: null }; }, computed: { ...mapGetters("map", ["getLayerByName"]), ...mapState("application", ["showSplitscreen"]), ...mapState("fairwayprofile", [ "startPoint", "endPoint", "currentProfile", "minAlt", "maxAlt", "totalLength", "fairwayCoordinates", "waterLevels", "selectedWaterLevel", "previousCuts" ]), ...mapState("bottlenecks", ["selectedBottleneck", "selectedSurvey"]), additionalSurvey: { get() { return this.$store.getters["fairwayprofile/additionalSurvey"]; }, set(value) { this.$store.commit("fairwayprofile/setAdditionalSurvey", value); this.selectAdditionalSurveyData(); } }, currentData() { if ( !this.selectedSurvey || !this.currentProfile.hasOwnProperty(this.selectedSurvey.date_info) ) return []; return this.currentProfile[this.selectedSurvey.date_info]; }, additionalData() { if ( !this.additionalSurvey || !this.currentProfile.hasOwnProperty(this.additionalSurvey.date_info) ) return []; return this.currentProfile[this.additionalSurvey.date_info]; }, waterColor() { const result = this.waterLevels.find( x => x.level === this.selectedWaterLevel ); return result.color; }, coordinatesForClipboard() { return ( this.startPoint[1] + "," + this.startPoint[0] + "," + this.endPoint[1] + "," + this.endPoint[0] ); } }, watch: { showSplitscreen() { this.drawDiagram(); }, currentData() { this.drawDiagram(); }, width() { this.drawDiagram(); }, height() { this.drawDiagram(); }, waterLevels() { this.drawDiagram(); }, selectedWaterLevel() { this.drawDiagram(); }, fairwayCoordinates() { this.drawDiagram(); }, selectedBottleneck() { this.$store.dispatch("fairwayprofile/previousCuts"); this.cutLabel = this.selectedBottleneck + " (" + new Date().toISOString() + ")"; }, coordinatesSelect(newValue) { if (newValue) { this.applyCoordinates(newValue); } } }, methods: { selectAdditionalSurveyData() { if ( !this.additionalSurvey || this.wait || this.currentProfile[this.additionalSurvey.date_info] ) { this.drawDiagram(); return; } this.$store .dispatch("fairwayprofile/loadProfile", this.additionalSurvey) .then(() => { this.wait = false; }) .catch(error => { this.wait = false; let status = "ERROR"; let data = error; const response = error.response; if (response) { status = response.status; data = response.data; } displayError({ title: "Backend Error", message: `${status}: ${data.message || data}` }); }); }, drawDiagram() { this.coordinatesSelect = null; const chartDiv = document.querySelector(".fairwayprofile"); d3.select(".fairwayprofile svg").remove(); this.scaleFairwayProfile(); let svg = d3.select(chartDiv).append("svg"); svg.attr("width", this.width); svg.attr("height", this.height); const width = this.width - this.margin.right - 1.5 * this.margin.left; const height = this.height - this.margin.top - 2 * this.margin.bottom; const currentData = this.currentData; const additionalData = this.additionalData; const { xScale, yScaleRight, yScaleLeft, graph } = this.generateCoordinates(svg, height, width); this.drawWaterlevel({ graph, xScale, yScaleRight, height, width }); if (currentData) { this.drawProfile({ graph, xScale, yScaleRight, currentData, height, width, color: GROUND_COLOR, strokeColor: "black", opacity: 1 }); } if (additionalData) { this.drawProfile({ graph, xScale, yScaleRight, currentData: additionalData, height, width, color: GROUND_COLOR, strokeColor: "#943007", opacity: 0.6 }); } this.drawLabels({ graph, xScale, yScaleLeft, currentData, height, width }); this.drawFairway({ graph, xScale, yScaleRight, currentData, height, width }); }, drawFairway({ graph, xScale, yScaleRight }) { for (let coordinates of this.fairwayCoordinates) { const [startPoint, endPoint, depth] = coordinates; let fairwayArea = d3 .area() .x(function(d) { return xScale(d.x); }) .y0(yScaleRight(0)) .y1(function(d) { return yScaleRight(d.y); }); graph .append("path") .datum([{ x: startPoint, y: depth }, { x: endPoint, y: depth }]) .attr("fill", "#002AFF") .attr("stroke-opacity", 0.65) .attr("fill-opacity", 0.65) .attr("stroke", "#FFD20D") .attr("d", fairwayArea); } }, drawLabels({ graph, height }) { graph .append("text") .attr("transform", ["rotate(-90)"]) .attr("y", this.width - 60) .attr("x", -(this.height - this.margin.top - this.margin.bottom) / 2) .attr("dy", "1em") .attr("fill", "black") .style("text-anchor", "middle") .text("Depth [m]"); graph .append("text") .attr("y", 0 - this.margin.left) .attr("x", 0 - height / 4) .attr("dy", "1em") .attr("fill", "black") .style("text-anchor", "middle") .attr("transform", [ "translate(" + this.width / 2 + "," + this.height + ")", "rotate(0)" ]) .text("Width [m]"); }, generateCoordinates(svg, height, width) { let xScale = d3 .scaleLinear() .domain(this.xScale) .rangeRound([0, width]); xScale.ticks(5); let yScaleLeft = d3 .scaleLinear() .domain(this.yScaleLeft) .rangeRound([height, 0]); let yScaleRight = d3 .scaleLinear() .domain(this.yScaleRight) .rangeRound([height, 0]); let xAxis = d3.axisBottom(xScale); let yAxis2 = d3.axisRight(yScaleRight); let graph = svg .append("g") .attr( "transform", "translate(" + this.margin.left + "," + this.margin.top + ")" ); graph .append("g") .attr("transform", "translate(0," + height + ")") .call(xAxis.ticks(5)); graph .append("g") .attr("transform", "translate(" + width + ",0)") .call(yAxis2); return { xScale, yScaleLeft, yScaleRight, graph }; }, drawWaterlevel({ graph, xScale, yScaleRight, height }) { let waterArea = d3 .area() .x(function(d) { return xScale(d.x); }) .y0(height) .y1(function(d) { return yScaleRight(d.y); }); graph .append("path") .datum([{ x: 0, y: 0 }, { x: this.totalLength, y: 0 }]) .attr("fill", this.waterColor) .attr("stroke", this.waterColor) .attr("d", waterArea); }, drawProfile({ graph, xScale, yScaleRight, currentData, height, color, strokeColor, opacity }) { for (let part of currentData) { let profileLine = d3 .line() .x(d => { return xScale(d.x); }) .y(d => { return yScaleRight(d.y); }); let profileArea = d3 .area() .x(function(d) { return xScale(d.x); }) .y0(height) .y1(function(d) { return yScaleRight(d.y); }); graph .append("path") .datum(part) .attr("fill", color) .attr("stroke", color) .attr("stroke-width", 3) .attr("stroke-opacity", opacity) .attr("fill-opacity", opacity) .attr("d", profileArea); graph .append("path") .datum(part) .attr("fill", "none") .attr("stroke", strokeColor) .attr("stroke-linejoin", "round") .attr("stroke-linecap", "round") .attr("stroke-width", 3) .attr("stroke-opacity", opacity) .attr("fill-opacity", opacity) .attr("d", profileLine); } }, scaleFairwayProfile() { if (!document.querySelector(".fairwayprofile")) return; const clientHeight = document.querySelector(".fairwayprofile") .clientHeight; const clientWidth = document.querySelector(".fairwayprofile").clientWidth; if (!clientHeight || !clientWidth) return; this.height = clientHeight; this.width = clientWidth; }, onCopyCoordinates() { displayInfo({ title: "Success", message: "Coordinates copied to clipboard!" }); }, applyManualCoordinates() { const coordinates = this.coordinatesInput .split(",") .map(coord => parseFloat(coord.trim())); 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 const cutLayer = this.getLayerByName("Cut Tool"); cutLayer.data.getSource().clear(); const cut = new Feature({ geometry: new LineString([ [coordinates[0], coordinates[1]], [coordinates[2], coordinates[3]] ]).transform("EPSG:4326", "EPSG:3857") }); cutLayer.data.getSource().addFeature(cut); // draw diagram this.$store.dispatch("fairwayprofile/cut", cut); } else { displayError({ title: "Invalid input", message: "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] }; 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: "Coordinates saved!", message: 'You can now select these coordinates from the "Saved cross profiles" menu to restore this cross profile.' }); } }, created() { window.addEventListener("resize", debounce(this.drawDiagram), 100); }, mounted() { this.drawDiagram(); }, updated() { this.scaleFairwayProfile(); }, destroyed() { window.removeEventListener("resize", debounce(this.drawDiagram)); } }; </script>