Mercurial > gemma
view client/src/components/fairway/AvailableFairwayDepthLNWL.vue @ 3806:cc80a37173f8 yworks-svg2pdf
Available-fairway-depth(both): use mixin for template loading and image processing
author | Thomas Junk <thomas.junk@intevation.de> |
---|---|
date | Thu, 04 Jul 2019 11:43:06 +0200 |
parents | 26325370ba18 |
children | ff8ca2d80ce9 |
line wrap: on
line source
<template> <div class="d-flex flex-column flex-fill"> <UIBoxHeader icon="chart-area" :title="title" :closeCallback="close" /> <UISpinnerOverlay v-if="loading" /> <div class="d-flex flex-fill"> <DiagramLegend> <div v-for="(entry, index) in legendLNWL" :key="index" class="legend"> <span :style=" `${legendStyle( index )}; border-radius: 0.25rem; width: 40px; height: 20px;` " ></span> {{ entry }} </div> <div> <select @change="applyChange" v-model="form.template" class="form-control d-block custom-select-sm w-100 mt-2" > <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> <a :href="dataLink" :download="csvFileName" class="mt-2 btn btn-sm btn-info w-100" >Download CSV</a > </div> </DiagramLegend> <div ref="diagramContainer" :id="containerId" class="mx-auto my-auto diagram-container" ></div> </div> </div> </template> <style></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, 2019 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 app from "@/main"; import debounce from "debounce"; import { mapState } from "vuex"; import filters from "@/lib/filters.js"; import canvg from "canvg"; import { diagram, pdfgen, templateLoader } from "@/lib/mixins"; import { HTTP } from "@/lib/http"; import { displayError } from "@/lib/errors"; export default { mixins: [diagram, pdfgen, templateLoader], components: { DiagramLegend: () => import("@/components/DiagramLegend") }, data() { return { containerId: "availablefairwaydepthlnwl", resizeListenerFunction: null, loading: false, labelPaddingTop: 15, scalePaddingLeft: 50, paddingTop: 10, diagram: null, yScale: null, dimensions: null, pdf: { doc: null, width: null, height: null }, form: { template: null }, templateData: 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: 70, y: 20 }, fontsize: 20, color: "steelblue" }, { type: "diagramlegend", position: "topleft", offset: { x: 30, y: 160 }, color: "black" } ] } }; }, created() { this.resizeListenerFunction = debounce(this.drawDiagram, 100); window.addEventListener("resize", this.resizeListenerFunction); }, destroyed() { window.removeEventListener("resize", this.resizeListenerFunction); }, mounted() { // Nasty but necessary if we don't want to use the updated hook to re-draw // the diagram because this would re-draw it also for irrelevant reasons. // In this case we need to wait for the child component (DiagramLegend) to // render. According to the docs (https://vuejs.org/v2/api/#mounted) this // should be possible with $nextTick() but it doesn't work because it does // not guarantee that the DOM is not only updated but also re-painted on the // screen. setTimeout(this.drawDiagram, 150); 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}` }); }); }, computed: { ...mapState("fairwayavailability", [ "selectedFairwayAvailabilityFeature", "fwLNWLData", "from", "to", "frequency", "csv", "depthlimit1", "depthlimit2", "widthlimit1", "widthlimit2" ]), legendLNWL() { const d = [this.depthlimit1, this.depthlimit2].sort(); const w = [this.widthlimit1, this.widthlimit2].sort(); const lowerBound = [d[0], w[0]].filter(x => x).join(", "); const upperBound = [d[1], w[1]].filter(x => x).join(", "); return [ `> LDC`, `< ${lowerBound}`, `< ${upperBound}`, `>= ${upperBound}` ]; }, dataLink() { return `data:text/csv;charset=utf-8, ${encodeURIComponent(this.csv)}`; }, csvFileName() { return `${this.$gettext("fairwayavailabilityLNWL")}-${ this.featureName }-${filters.surveyDate(this.fromDate)}-${filters.surveyDate( this.toDate )}-${this.$gettext(this.frequency)}-.csv`; }, fromDate() { return this.from; }, toDate() { return this.to; }, availability() { return this.plainAvailability; }, title() { return `Available Fairway Depth vs LNWL: ${ this.featureName } (${filters.surveyDate(this.fromDate)} - ${filters.surveyDate( this.toDate )}) ${this.$gettext(this.frequency)}`; }, featureName() { if (this.selectedFairwayAvailabilityFeature == null) return ""; return this.selectedFairwayAvailabilityFeature.properties.name; }, widthPerItem() { return Math.min( (this.dimensions.width - this.scalePaddingLeft) / this.fwLNWLData.length, 180 ); }, ldcWidth() { return this.widthPerItem * 0.3; }, afdWidth() { return this.widthPerItem * 0.5; }, spaceBetween() { return this.widthPerItem * 0.2; } }, methods: { legendStyle(index) { const style = { 0: `background-color: ${this.$options.LWNLCOLORS.LDC};`, 1: `background-color: ${this.$options.AFDCOLORS[2]};`, 2: `background-color: ${this.$options.AFDCOLORS[1]};`, 3: `background-color: ${this.$options.AFDCOLORS[0]};` }; return style[index]; }, applyChange() { if (this.form.template.hasOwnProperty("properties")) { this.templateData = this.defaultTemplate; return; } if (this.form.template) { this.loadTemplates("/templates/diagram/" + this.form.template.name) .then(response => { this.prepareImages(response.data.template_data.elements).then( values => { values.forEach(v => { response.data.template_data.elements[v.index].url = v.url; }); 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() { let title = `Available Fairway Depth vs LNWL: ${this.featureName}`; this.generatePDF({ templateData: this.templateData, diagramTitle: title }); this.pdf.doc.save(`Available Fairway Depth LNWL: ${this.featureName}`); }, 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); }, addDiagramLegend(position, offset, color) { let x = offset.x, y = offset.y; this.pdf.doc.setFontSize(10); let width = (this.pdf.doc.getStringUnitWidth(">= LDC") * 10) / (72 / 25.6) + 15; // if position is on the right, x needs to be calculate with pdf width and // the size of the element 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(6); } this.pdf.doc.setTextColor(color); this.pdf.doc.setDrawColor(this.$options.LWNLCOLORS.LDC); this.pdf.doc.setFillColor(this.$options.LWNLCOLORS.LDC); this.pdf.doc.roundedRect(x, y, 10, 4, 1.5, 1.5, "FD"); this.pdf.doc.text(this.legendLNWL[0], x + 12, y + 3); this.pdf.doc.setDrawColor(this.$options.AFDCOLORS[2]); this.pdf.doc.setFillColor(this.$options.AFDCOLORS[2]); this.pdf.doc.roundedRect(x, y + 5, 10, 4, 1.5, 1.5, "FD"); this.pdf.doc.text(this.legendLNWL[1], x + 12, y + 8); this.pdf.doc.setDrawColor(this.$options.AFDCOLORS[1]); this.pdf.doc.setFillColor(this.$options.AFDCOLORS[1]); this.pdf.doc.roundedRect(x, y + 10, 10, 4, 1.5, 1.5, "FD"); this.pdf.doc.text(this.legendLNWL[2], x + 12, y + 13); this.pdf.doc.setDrawColor(this.$options.AFDCOLORS[0]); this.pdf.doc.setFillColor(this.$options.AFDCOLORS[0]); this.pdf.doc.roundedRect(x, y + 15, 10, 4, 1.5, 1.5, "FD"); this.pdf.doc.text(this.legendLNWL[3], x + 12, y + 18); }, close() { this.$store.commit("application/paneSetup", "DEFAULT"); }, drawDiagram() { this.dimensions = this.getDimensions({ main: { top: 20, right: 20, bottom: 110, left: 200 } }); this.yScale = d3 .scaleLinear() .domain([0, 100]) .range([this.dimensions.mainHeight - 30, 0]); d3.select(".diagram-container svg").remove(); this.generateDiagramContainer(); this.drawBars(); this.drawScaleLabel(); this.drawScale(); this.drawTooltip(); }, drawTooltip() { this.diagram .append("text") .text("") .attr("font-size", "0.8em") .attr("opacity", 0) .attr("id", "tooltip"); }, generateDiagramContainer() { const diagram = d3 .select(".diagram-container") .append("svg") .attr("width", this.dimensions.width) .attr("height", this.dimensions.mainHeight); this.diagram = diagram .append("g") .attr("transform", `translate(0 ${this.paddingTop})`); }, drawBars() { if (this.fwLNWLData) { this.fwLNWLData.forEach((data, i) => { this.drawLNWL(data, i); this.drawAFD(data, i); this.drawLabel(data.date, i); }); } }, drawLabel(date, i) { this.diagram .append("text") .text(date) .attr("text-anchor", "middle") .attr("font-size", "smaller") .attr( "transform", `translate(${this.scalePaddingLeft + this.widthPerItem * i + this.widthPerItem / 2} ${this.dimensions.mainHeight - 15})` ); }, drawAFD(data, i) { let afd = this.diagram .append("g") .attr( "transform", `translate(${this.scalePaddingLeft + this.spaceBetween / 2 + this.widthPerItem * i + this.ldcWidth})` ); afd .selectAll("rect") .data([data.above, data.between, data.below]) .enter() .append("rect") .on("mouseover", function() { d3.select(this).attr("opacity", "0.8"); d3.select("#tooltip").attr("opacity", 1); }) .on("mouseout", function() { d3.select(this).attr("opacity", 1); d3.select("#tooltip").attr("opacity", 0); }) .on("mousemove", function(d) { let y = d3.mouse(this)[1]; const dy = document .querySelector(".diagram-container") .getBoundingClientRect().left; d3.select("#tooltip") .text(d.toFixed(2)) .attr("y", y - 10) .attr("x", d3.event.pageX - dy); //d3.event.pageX gives coordinates relative to SVG //dy gives offset of svg on page }) .attr("height", d => { return this.yScale(0) - this.yScale(d); }) .attr("y", (d, i) => { if (i === 0) { return this.yScale(d); } if (i === 1) { return this.yScale(data.above + d); } if (i === 2) { return this.yScale(data.above + data.between + d); } }) .attr("width", this.afdWidth) .attr("fill", (d, i) => { return this.$options.AFDCOLORS[i]; }); }, drawLNWL(data, i) { let lnwl = this.diagram .append("g") .attr( "transform", `translate(${this.scalePaddingLeft + this.spaceBetween / 2 + this.widthPerItem * i})` ); lnwl .append("rect") .datum([data.ldc]) .on("mouseover", function() { d3.select(this).attr("opacity", "0.8"); d3.select("#tooltip").attr("opacity", 1); }) .on("mouseout", function() { d3.select(this).attr("opacity", 1); d3.select("#tooltip").attr("opacity", 0); }) .on("mousemove", function(d) { let y = d3.mouse(this)[1]; const dy = document .querySelector(".diagram-container") .getBoundingClientRect().left; d3.select("#tooltip") .text(d[0].toFixed(2)) .attr("y", y - 10) .attr("x", d3.event.pageX - dy); //d3.event.pageX gives coordinates relative to SVG //dy gives offset of svg on page }) .attr("height", d => { return this.yScale(0) - this.yScale(d); }) .attr("y", d => { return this.yScale(d); }) .attr("width", this.ldcWidth) .attr("fill", () => { return this.$options.LWNLCOLORS.LDC; }); }, drawScaleLabel() { const center = this.dimensions.mainHeight / 2; this.diagram .append("text") .text(this.$options.LEGEND) .attr("text-anchor", "middle") .attr("x", 0) .attr("y", 0) .attr("dy", "1em") .attr("transform", `translate(0, ${center}), rotate(-90)`); }, drawScale() { const yAxis = d3.axisLeft().scale(this.yScale); this.diagram .append("g") .attr("transform", `translate(${this.scalePaddingLeft})`) .call(yAxis) .selectAll(".tick text") .attr("fill", "black") .select(function() { return this.parentNode; }) .selectAll(".tick line") .attr("stroke", "black"); this.diagram.selectAll(".domain").attr("stroke", "black"); } }, watch: { fwLNWLData() { this.drawDiagram(); } }, LEGEND: app.$gettext("Percent"), AFDCOLORS: ["#3636ff", "#f49b7f", "#e15472"], LWNLCOLORS: { LDC: "#97ddf3", HDC: "#43FFE1" } }; </script>