Mercurial > gemma
view client/src/components/fairway/Fairwayprofile.vue @ 3644:9e91b416d5bb
client: cross profile: display arrow in diagram
consciously diceded to not draw it in the svg so it will not be exported to pdf since there it does not make sense without the map
author | Markus Kottlaender <markus@intevation.de> |
---|---|
date | Wed, 12 Jun 2019 17:10:49 +0200 |
parents | 309084558808 |
children | c91bcb92e0b7 |
line wrap: on
line source
<template> <div class="d-flex flex-column flex-fill"> <UIBoxHeader icon="chart-area" :title="title" :closeCallback="close" /> <div class="d-flex flex-fill"> <DiagramLegend> <div class="legend"> <span style="background-color: #5995ff; width: 20px; height: 20px;" ></span> Water </div> <div class="legend"> <span :style=" 'width: 16px; height: 16px; background-color: rgba(' + this.getLayerStyle(1).fillColor.join(',') + ',' + this.getLayerStyle(1).fillOpacity + '); border: dotted 2px rgba(' + this.getLayerStyle(1).strokeColor.join(',') + ',' + this.getLayerStyle(1).strokeOpacity + '); background-clip: padding-box; box-sizing: content-box;' " ></span> Fairway (LOS 1) </div> <div class="legend"> <span :style=" 'width: 16px; height: 16px; background-color: rgba(' + this.getLayerStyle(2).fillColor.join(',') + ',' + this.getLayerStyle(2).fillOpacity + '); border: dashed 2px rgba(' + this.getLayerStyle(2).strokeColor.join(',') + ',' + this.getLayerStyle(2).strokeOpacity + '); background-clip: padding-box; box-sizing: content-box;' " ></span> Fairway (LOS 2) </div> <div class="legend"> <span :style=" 'width: 16px; height: 16px; background-color: rgba(' + this.getLayerStyle(3).fillColor.join(',') + ',' + this.getLayerStyle(3).fillOpacity + '); border: solid 2px rgba(' + this.getLayerStyle(3).strokeColor.join(',') + ',' + this.getLayerStyle(3).strokeOpacity + '); background-clip: padding-box; box-sizing: content-box;' " ></span> Fairway (LOS 3) </div> <div class="legend"> <span style="width: 14px; height: 14px; background-color: #4a2f06; border: solid 3px black; background-clip: padding-box; box-sizing: content-box;" ></span> Sediment </div> <div class="legend"> <span style="width: 14px; height: 14px; background-color: rgba(74, 47, 6, 0.6); border: solid 3px #943007; background-clip: padding-box; box-sizing: content-box;" ></span> Sediment (Compare) </div> <div> <select v-model="form.template" @change="applyChange" class="form-control d-block custom-select-sm w-100" > <option v-for="template in templates" :value="template" :key="template.name" > {{ template.name }} </option> </select> <button @click="downloadPDF" type="button" class="btn btn-sm btn-info d-block w-100 mt-2" > <translate>Export to PDF</translate> </button> </div> </DiagramLegend> <div ref="diagramContainer" class="d-flex flex-fill justify-content-center align-items-center diagram-container position-relative" > <div class="direction-indicator"></div> <div v-if="!fairwayData"> <translate>No data available.</translate> </div> </div> </div> </div> </template> <style lang="sass" scoped> .direction-indicator width: 70px height: 0 border-top: dashed 2px #333 position: absolute bottom: 20px left: 115px margin-left: -35px &::after content: "" width: 0 height: 0 border-width: 10px border-top-width: 5px border-bottom-width: 5px border-style: solid border-color: transparent border-left-color: #333 position: absolute right: -20px top: -6px </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> * Markus Kottländer <markus.kottlaender@intevation.de> * Fadi Abbud <fadi.abbud@intevation.de> */ import * as d3 from "d3"; import { mapState, mapGetters } from "vuex"; import debounce from "debounce"; import jsPDF from "jspdf"; import canvg from "canvg"; import { pdfgen } from "@/lib/mixins"; import { HTTP } from "@/lib/http"; import { displayError } from "@/lib/errors"; const GROUND_COLOR = "#4A2F06"; const WATER_COLOR = "#005DFF"; export default { mixins: [pdfgen], name: "fairwayprofile", components: { DiagramLegend: () => import("@/components/DiagramLegend") }, data() { return { width: null, height: null, margin: { top: 20, right: 80, bottom: 60, left: 80 }, form: { template: null }, templates: [], defaultTemplate: { name: "default", properties: { paperSize: "a4" }, elements: [ { type: "diagram", position: "topleft", offset: { x: 20, y: 60 }, width: 290, height: 100 }, { type: "diagramtitle", position: "topleft", offset: { x: 90, y: 30 }, fontsize: 22, color: "steelblue" }, { type: "diagramlegend", position: "topleft", offset: { x: 30, y: 160 }, color: "black" } ] }, pdf: { doc: null, width: 32, height: 297 }, templateData: null }; }, computed: { ...mapGetters("map", ["openLayersMap"]), ...mapGetters("fairwayprofile", ["totalLength"]), ...mapState("fairwayprofile", [ "additionalSurvey", "currentProfile", "startPoint", "endPoint", "fairwayData", "maxAlt", "selectedWaterLevel" ]), ...mapState("bottlenecks", ["selectedSurvey", "selectedBottleneck"]), ...mapState("application", ["paneSetup"]), title() { let dates = [this.selectedSurvey.date_info]; let waterlevelLabel = this.selectedWaterLevel === "ref" ? this.selectedSurvey.depth_reference : "Current"; if (this.additionalSurvey) dates.push(this.additionalSurvey.date_info); dates.map(d => this.$options.filters.dateTime(d, true)); return `${this.$gettext("Fairwayprofile")}: ${ this.selectedBottleneck } (${dates.join( ", " )}) WL: ${waterlevelLabel} (${this.$options.filters.waterlevel( this.waterlevel )} m)`; }, currentData() { if ( !this.selectedSurvey || !this.currentProfile.hasOwnProperty(this.selectedSurvey.date_info) ) return []; return this.currentProfile[this.selectedSurvey.date_info].points; }, additionalData() { if ( !this.additionalSurvey || !this.currentProfile.hasOwnProperty(this.additionalSurvey.date_info) ) return []; return this.currentProfile[this.additionalSurvey.date_info].points; }, bottleneck() { return this.openLayersMap() .getLayer("BOTTLENECKS") .getSource() .getFeatures() .find(f => f.get("objnam") === this.selectedBottleneck); }, waterlevel() { return this.selectedWaterLevel === "ref" ? this.refWaterlevel : this.bottleneck.get("gm_waterlevel"); }, refWaterlevel() { return this.selectedSurvey.waterlevel_value; } }, watch: { currentData() { this.drawDiagram(); }, additionalData() { this.drawDiagram(); }, width() { this.drawDiagram(); }, height() { this.drawDiagram(); }, waterLevels() { this.drawDiagram(); }, selectedWaterLevel() { this.drawDiagram(); }, fairwayData() { this.drawDiagram(); }, selectedBottleneck() { this.$store.commit("application/paneSetup", "DEFAULT"); } }, methods: { close() { this.$store.commit( "application/paneSetup", this.paneSetup === "COMPARESURVEYS_FAIRWAYPROFILE" ? "COMPARESURVEYS" : "DEFAULT" ); this.$store.dispatch("fairwayprofile/clearCurrentProfile"); }, getLayerStyle(los) { let style = this.openLayersMap() .getLayer("FAIRWAYDIMENSIONSLOS" + los) .getStyle()()[0]; // use spread operator to clone arrays let fillColor = [...style.getFill().getColor()]; let fillOpacity = fillColor.pop(); let strokeColor = [...style.getStroke().getColor()]; let strokeOpacity = strokeColor.pop(); let strokeDash = style.getStroke().getLineDash(); return { fillColor, fillOpacity, strokeColor, strokeOpacity, strokeDash }; }, applyChange() { if (this.form.template.hasOwnProperty("properties")) { this.templateData = this.defaultTemplate; return; } if (this.form.template) { HTTP.get("/templates/diagram/" + this.form.template.name, { headers: { "X-Gemma-Auth": localStorage.getItem("token"), "Content-type": "text/xml; charset=UTF-8" } }) .then(response => { this.templateData = response.data.template_data; }) .catch(e => { const { status, data } = e.response; displayError({ title: this.$gettext("Backend Error"), message: `${status}: ${data.message || data}` }); }); } }, downloadPDF() { if (this.templateData) { this.pdf.doc = new jsPDF( "l", "mm", this.templateData.properties.paperSize ); // pdf width and height in millimeter (landscape) this.pdf.width = this.templateData.properties.paperSize === "a3" ? 420 : 297; this.pdf.height = this.templateData.properties.paperSize === "a3" ? 297 : 210; // default values if some are missing in template let defaultFontSize = 11, defaultWidth = 70, defaultTextColor = "black", defaultBorderColor = "white", defaultBgColor = "white", defaultRounding = 2, defaultPadding = 2, defaultOffset = { x: 0, y: 0 }; this.templateData.elements.forEach(e => { switch (e.type) { case "diagram": { this.addDiagram( e.position, e.offset || defaultOffset, e.width, e.height ); break; } case "diagramlegend": { this.addDiagramLegend( e.position, e.offset || defaultOffset, e.color || defaultTextColor ); break; } case "diagramtitle": { let fairwayInfo = this.selectedBottleneck + " (" + this.selectedSurvey.date_info + ")"; this.addDiagramTitle( e.position, e.offset || defaultOffset, e.fontsize || defaultFontSize, e.color || defaultTextColor, fairwayInfo ); break; } case "image": { this.addImage( e.url, e.format || "", e.position, e.offset || defaultOffset, e.width || 90, e.height || 60 ); break; } case "text": { this.addText( e.position, e.offset || defaultOffset, e.width || defaultWidth, e.fontsize || defaultFontSize, e.color || defaultTextColor, e.text || "" ); break; } case "box": { this.addBox( e.position, e.offset || defaultOffset, e.width || 90, e.height || 60, e.rounding === 0 || e.rounding ? e.rounding : defaultRounding, e.color || defaultBgColor, e.brcolor || defaultBorderColor ); break; } case "textbox": { this.addTextBox( e.position, e.offset || defaultOffset, e.width, e.height, e.rounding === 0 || e.rounding ? e.rounding : defaultRounding, e.padding || defaultPadding, e.fontsize || defaultFontSize, e.color || defaultTextColor, e.background || defaultBgColor, e.text || "", e.brcolor || defaultBorderColor ); break; } } }); } this.pdf.doc.save( this.title.replace(/\s/g, "_").replace(/[():,]/g, "") + ".pdf" ); }, addDiagram(position, offset, width, height) { let x = offset.x, y = offset.y; var svg = this.$refs.diagramContainer.innerHTML; if (svg) { svg = svg.replace(/\r?\n|\r/g, "").trim(); } // use default width,height if they are missing in the template definition if (!width) { width = this.templateData.properties.paperSize === "a3" ? 380 : 290; } if (!height) { height = this.templateData.properties.paperSize === "a3" ? 130 : 100; } if (["topright", "bottomright"].indexOf(position) !== -1) { x = this.pdf.width - offset.x - width; } if (["bottomright", "bottomleft"].indexOf(position) !== -1) { y = this.pdf.height - offset.y - height; } var canvas = document.createElement("canvas"); canvas.width = window.innerWidth; canvas.height = window.innerHeight / 2; canvg(canvas, svg, { ignoreMouse: true, ignoreAnimation: true, ignoreDimensions: true }); var imgData = canvas.toDataURL("image/png"); this.pdf.doc.addImage(imgData, "PNG", x, y, width, height); }, // Diagram legend addDiagramLegend(position, offset, color) { let x = offset.x, y = offset.y; this.pdf.doc.setFontSize(10); let width = (this.pdf.doc.getStringUnitWidth("Sediment (Compare)") * 10) / (72 / 25.6) + 4; if (["topright", "bottomright"].indexOf(position) !== -1) { x = this.pdf.width - offset.x - width; } if (["bottomright", "bottomleft"].indexOf(position) !== -1) { y = this.pdf.height - offset.y - this.getTextHeight(8); } this.pdf.doc.setTextColor(color); this.pdf.doc.setDrawColor("#5995ff"); this.pdf.doc.setFillColor("#5995ff"); this.pdf.doc.circle(x, y, 2, "FD"); this.pdf.doc.text(x + 3, y + 1, "Water"); this.pdf.doc.setLineDash([0.3]); this.pdf.doc.setDrawColor("#0000ff"); this.pdf.doc.setFillColor("#fcfacc"); this.pdf.doc.circle(x, y + 5, 2, "FD"); this.pdf.doc.text(x + 3, y + 6, "Fairway (LOS 1)"); this.pdf.doc.setLineDash([1]); this.pdf.doc.setFillColor("#fdfce5"); this.pdf.doc.circle(x, y + 10, 2, "FD"); this.pdf.doc.text(x + 3, y + 11, "Fairway (LOS 2)"); this.pdf.doc.setLineDash(); this.pdf.doc.setFillColor("#ffffff"); this.pdf.doc.circle(x, y + 15, 2, "FD"); this.pdf.doc.text(x + 3, y + 16, "Fairway (LOS 3)"); this.pdf.doc.setDrawColor("black"); this.pdf.doc.setFillColor("#4a2e06"); this.pdf.doc.circle(x, y + 20, 2, "FD"); this.pdf.doc.text(x + 3, y + 21, "Sediment"); this.pdf.doc.setDrawColor("#943007"); this.pdf.doc.setFillColor("#928269"); this.pdf.doc.circle(x, y + 25, 2, "FD"); this.pdf.doc.text(x + 3, y + 26, "Sediment (Compare)"); }, drawDiagram() { d3.select(".diagram-container svg").remove(); this.scaleFairwayProfile(); let svg = d3.select(".diagram-container").append("svg"); svg.attr("width", "100%"); svg.attr("height", "100%"); const width = this.width - this.margin.right - this.margin.left; const height = this.height - this.margin.top - this.margin.bottom; const currentData = this.currentData; const additionalData = this.additionalData; const { xScale, yScaleRight, graph } = this.generateScalesAndGraph( svg, height, width ); if (!this.height || !this.width) return; // do not try to render when height and width are unknown this.drawWaterlevel({ graph, xScale, yScaleRight, height }); this.drawLabels({ graph, height }); if (currentData) { this.drawProfile({ graph, xScale, yScaleRight, currentData, height, color: GROUND_COLOR, strokeColor: "black", opacity: 1 }); } if (additionalData) { this.drawProfile({ graph, xScale, yScaleRight, currentData: additionalData, height, color: GROUND_COLOR, strokeColor: "#943007", opacity: 0.6 }); } this.drawFairway({ graph, xScale, yScaleRight }); }, drawFairway({ graph, xScale, yScaleRight }) { if (this.fairwayData === undefined) { return; } for (let data of this.fairwayData) { data.coordinates.forEach(coordinates => { 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", `rgba(${this.getLayerStyle(data.los).fillColor.join(",")})` ) .attr("fill-opacity", this.getLayerStyle(data.los).fillOpacity) .attr( "stroke", `rgba(${this.getLayerStyle(data.los).strokeColor.join(",")})` ) .attr("stroke-opacity", this.getLayerStyle(data.los).strokeOpacity) .attr("stroke-dasharray", this.getLayerStyle(data.los).strokeDash) .attr("d", fairwayArea); }); } }, drawLabels({ graph, height }) { graph .append("text") .attr("transform", ["rotate(-90)"]) .attr("y", this.width - 100) .attr("x", -(this.height - this.margin.top - this.margin.bottom) / 2) .attr("fill", "black") .style("text-anchor", "middle") .text("Depth [m]"); graph .append("text") .attr("transform", ["rotate(-90)"]) .attr("y", -50) .attr("x", -(this.height - this.margin.top - this.margin.bottom) / 2) .attr("fill", "black") .style("text-anchor", "middle") .text("Waterlevel [m]"); graph .append("text") .attr("y", -50) .attr("x", -(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]"); }, generateScalesAndGraph(svg, height, width) { let xScale = d3 .scaleLinear() .domain([0, this.totalLength]) .rangeRound([0, width]); let yScaleRight = d3 .scaleLinear() .domain([ this.maxAlt * 1.1 + Math.abs(this.waterlevel - this.refWaterlevel) / 100, -(this.maxAlt * 0.1) ]) .rangeRound([height, 0]); let yScaleLeft = d3 .scaleLinear() .domain([ this.waterlevel - (this.maxAlt * 100 + Math.abs(this.waterlevel - this.refWaterlevel)), this.waterlevel + this.maxAlt * 0.1 * 100 ]) .rangeRound([height, 0]); let xAxis = d3.axisBottom(xScale).ticks(5); let yAxisRight = d3.axisRight(yScaleRight); let yAxisLeft = d3 .axisLeft(yScaleLeft) .tickFormat(d => this.$options.filters.waterlevel(d)); let graph = svg .append("g") .attr( "transform", "translate(" + this.margin.left + "," + this.margin.top + ")" ); graph .append("g") .attr("transform", "translate(0," + height + ")") .call(xAxis) .selectAll(".tick text") .attr("fill", "black") .select(function() { return this.parentNode; }) .selectAll(".tick line") .attr("stroke", "black"); graph .append("g") .attr("transform", "translate(" + width + ",0)") .call(yAxisRight) .selectAll(".tick text") .attr("fill", "black") .select(function() { return this.parentNode; }) .selectAll(".tick line") .attr("stroke", "black"); graph .append("g") .attr("transform", "translate(0 0)") .call(yAxisLeft) .selectAll(".tick text") .attr("fill", "black") .select(function() { return this.parentNode; }) .selectAll(".tick line") .attr("stroke", "black"); graph.selectAll(".domain").attr("stroke", "black"); return { xScale, 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-opacity", 0.65) .attr("fill", WATER_COLOR) .attr("stroke", "transparent") .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 => yScaleRight( d.y + Math.abs(this.waterlevel - this.refWaterlevel) / 100 ) ); let profileArea = d3 .area() .x(function(d) { return xScale(d.x); }) .y0(height) .y1(d => yScaleRight( d.y + Math.abs(this.waterlevel - this.refWaterlevel) / 100 ) ); graph .append("path") .datum(part) .attr("fill", color) .attr("stroke", "transparent") .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", 0) .attr("d", profileLine); } }, scaleFairwayProfile() { if (!document.querySelector(".diagram-container")) return; const clientHeight = document.querySelector(".diagram-container") .clientHeight; const clientWidth = document.querySelector(".diagram-container") .clientWidth; if (!clientHeight || !clientWidth) return; this.height = clientHeight; this.width = clientWidth; } }, created() { window.addEventListener("resize", debounce(this.drawDiagram), 100); }, mounted() { this.drawDiagram(); this.templates[0] = this.defaultTemplate; this.form.template = this.templates[0]; this.templateData = this.form.template; HTTP.get("/templates/diagram", { headers: { "X-Gemma-Auth": localStorage.getItem("token"), "Content-type": "text/xml; charset=UTF-8" } }) .then(response => { if (response.data.length) { this.templates = response.data; this.form.template = this.templates[0]; this.templates[this.templates.length] = this.defaultTemplate; this.applyChange(); } }) .catch(e => { const { status, data } = e.response; displayError({ title: this.$gettext("Backend Error"), message: `${status}: ${data.message || data}` }); }); }, updated() { this.drawDiagram(); }, destroyed() { window.removeEventListener("resize", debounce(this.drawDiagram)); } }; </script>