Mercurial > gemma
view client/src/lib/mixins.js @ 5736:55892008ec96 default tip
Fixed a bunch of corner cases in WG import.
author | Sascha Wilde <wilde@sha-bang.de> |
---|---|
date | Wed, 29 May 2024 19:02:42 +0200 |
parents | a857d6ae1264 |
children |
line wrap: on
line source
/* 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): * * Markus Kottländer <markus.kottlaender@intevation.de> * * Fadi Abbud <fadi.abbud@intevation.de> * * Bernhard Reiter <bernhard.reiter@intevation.de> */ import jsPDF from "jspdf-yworks"; import svg2pdf from "svg2pdf.js"; import locale2 from "locale2"; import { mapState } from "vuex"; import { HTTP } from "@/lib/http"; import * as d3 from "d3"; import sanitize from "sanitize-filename"; import canvg from "canvg"; /*eslint no-unused-vars: ["error", { "varsIgnorePattern": "[debugSVG|_]" }]*/ const debugSVG = ({ svg, svgWidth, svgHeight }) => { d3.select(svg) .append("rect") .attr("width", svgWidth) .attr("height", svgHeight) .attr("fill-opacity", 0) .attr("stroke", "#ff0000"); }; export const sortTable = { data() { return { sortColumn: "", sortDirection: "ASC", pageSize: 20, page: 1 }; }, methods: { sortTable(sorting) { this.sortColumn = sorting.sortColumn; this.sortDirection = sorting.sortDirection; } } }; /** * Since the names of LDC and HDC aren't normalized, we have to do guesswork * best fit is key with HDC or LDC in it */ export const refwaterlevels = { methods: { determineLDCHDC(refWaterLevels) { let HDC = refWaterLevels[Object.keys(refWaterLevels).find(e => /HDC/.test(e))]; let LDC = refWaterLevels[Object.keys(refWaterLevels).find(e => /LDC/.test(e))]; return { LDC, HDC }; } } }; export const diagram = { methods: { getDimensions({ svgWidth, svgHeight, main, nav }) { const mainMargin = main || { top: Math.floor(0.08 * svgHeight), right: Math.floor(0.08 * svgWidth), bottom: Math.floor(0.2 * svgHeight), left: Math.floor(0.08 * svgWidth) }; const navMargin = nav || { top: Math.floor(0.78 * svgHeight), right: Math.floor(0.013 * svgWidth), bottom: Math.floor(0.095 * svgHeight), left: Math.floor(0.07 * svgWidth) }; const width = Number(svgWidth) - mainMargin.left - mainMargin.right; const mainHeight = Number(svgHeight) - mainMargin.top - mainMargin.bottom; const navHeight = Number(svgHeight) - navMargin.top - navMargin.bottom; return { width, mainHeight, navHeight, mainMargin, navMargin }; } } }; export const pane = { computed: { paneId() { return this.$parent.pane.id; } } }; export const templateLoader = { methods: { downloadFilename(type, name) { return `${type}-${sanitize(name).replace( / /g, "-" )}-${this.dateForPDF()}`; }, loadTemplates(url) { return new Promise((resolve, reject) => { HTTP.get(url, { headers: { "X-Gemma-Auth": localStorage.getItem("token"), "Content-type": "text/xml; charset=UTF-8" } }) .then(response => { resolve(response); }) .catch(error => { reject(error); }); }); }, prepareImages(elements) { /** * In order to render the images from the template, we need to convert * each image to dataURIs. Since this happens asynchronous, * we need to wrap each image into its own promise and only after all are * finished, we continue with the flow. */ return new Promise(resolve => { const imageElementLoaders = elements.reduce((o, n, i) => { if (n.type === "image") { o.push( new Promise(resolve => { const image = new Image(); image.onload = function() { var canvas = document.createElement("canvas"); canvas.width = this.naturalWidth; // or 'width' if you want a special/scaled size canvas.height = this.naturalHeight; // or 'height' if you want a special/scaled size canvas.getContext("2d").drawImage(this, 0, 0); resolve({ index: i, url: canvas.toDataURL("image/png") }); }; let URL = n.URL; if (!URL) { URL = this.logoForPDF ? this.logoForPDF : "/img/gemma-logo-for-pdf.png"; } image.src = URL; }) ); } return o; }, []); Promise.all(imageElementLoaders).then(values => { resolve(values); }); }); } } }; export const pdfgen = { computed: { ...mapState("application", ["logoForPDF"]), ...mapState("user", ["user"]), ...mapState("bottlenecks", ["soundingInfo"]) }, methods: { downloadImage(elementName, title) { const offScreen = document.querySelector("#offScreen"); const DPI = 96; const svgWidth = this.millimeter2pixels(428, DPI); const svgHeight = this.millimeter2pixels(119, DPI); offScreen.style.width = `${svgWidth}px`; offScreen.style.height = `${svgHeight}px`; let zoomLevel = this.zoomStore; const layout = this.getPrintLayout(svgHeight, svgWidth); this.renderTo({ element: offScreen, dimensions: this.getDimensions({ svgWidth: svgWidth, svgHeight: svgHeight, ...layout }), zoomLevel // passing the zoom level to draw the diagram on pdf at this point }); const diagramContainer = document.getElementById("offScreen"); const { clientHeight, clientWidth } = diagramContainer; diagramContainer.querySelector("svg").setAttribute("width", clientWidth); diagramContainer .querySelector("svg") .setAttribute("height", clientHeight + 180); const svg = diagramContainer.querySelector("svg").outerHTML; const canvas = document.createElement("canvas"); canvg(canvas, svg, { offsetY: 70 }); const ctx = canvas.getContext("2d"); // Draw white rectangular and place the title on it ctx.beginPath(); ctx.fillStyle = "#ffffff"; ctx.fillRect(0, 0, clientWidth, 70); ctx.stroke(); ctx.fillStyle = "steelblue"; ctx.font = "500 30px sans-serif"; ctx.textAlign = "center"; ctx.fillText(title, clientWidth / 2, 35); ctx.closePath(); // Add diagramlegend this.addLegendToCanvas(ctx, { height: clientHeight + 100, width: clientWidth }); const imgData = canvas.toDataURL("image/png"); document.getElementById(elementName).setAttribute("href", imgData); offScreen.removeChild(offScreen.firstChild); }, addDiagram(position, offset, width, height) { let x = offset.x, y = offset.y; const DPI = 80; const svgWidth = this.millimeter2pixels(width, DPI); const svgHeight = this.millimeter2pixels(height, DPI); // draw the diagram in a separated html element to get the full size const offScreen = document.querySelector("#offScreen"); offScreen.style.width = `${svgWidth}px`; offScreen.style.height = `${svgHeight}px`; let zoomLevel = this.zoomStore; const layout = this.getPrintLayout(svgHeight, svgWidth); this.renderTo({ element: offScreen, dimensions: this.getDimensions({ svgWidth: svgWidth, svgHeight: svgHeight, ...layout }), zoomLevel // passing the zoom level to draw the diagram on pdf at this point }); var svg = offScreen.querySelector("svg"); 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; } //debugSVG({ svg, svgWidth, svgHeight }); svg2pdf(svg, this.pdf.doc, { xOffset: x, yOffset: y, scale: this.pixel2millimeter(1, DPI) }); offScreen.removeChild(svg); }, getPaperDimensions(format) { const dims = { A3: { height: 297, width: 420 }, A4: { height: 210, width: 297 } }; return dims[format.toUpperCase()]; }, millimeter2pixels(length, dpi) { return (dpi * length) / 25.4; }, pixel2millimeter(pixels, dpi) { return (pixels * 25.4) / dpi; }, isrsInfo(gauge) { // See https://www.elwis.de/DE/Service/Daten-und-Fakten/RIS-Index/RIS-Index-node.html const [ _, countryCode, loCode, fairwaySection, orc, hectometre ] = gauge.properties.isrs_code.match( /(\w{2})(\w{3})(\w{5})(\w{5})(\w{5})/ ); return { countryCode: countryCode, loCode: loCode, fairwaySection: fairwaySection, orc: orc, hectometre: hectometre }; }, generatePDF(params) { // creates a new jsPDF object into this.pdf.doc // will call functions that the calling context has to provide // as specified in the templateData let templateData = params["templateData"]; let diagramTitle = params["diagramTitle"]; this.pdf.doc = new jsPDF("l", "mm", templateData.properties.paperSize); // pdf width and height in millimeter (landscape) if (templateData.properties.paperSize === "a3") { this.pdf.width = 420; this.pdf.height = 297; } else { this.pdf.width = 297; this.pdf.height = 210; } // check the template elements if (templateData) { this.pdf.doc.setFont("linbiolinum", "normal"); let defaultFontSize = 11, defaultColor = "black", defaultTextColor = "black", defaultBorderColor = "white", defaultBgColor = "white", defaultRounding = 2, defaultPadding = 2, defaultOffset = { x: 0, y: 0 }; templateData.elements.forEach(e => { switch (e.type) { case "diagram": { this.addDiagram( e.position, e.offset || defaultOffset, // use default width,height if they are missing in template definition e.width || (this.templateData.properties.paperSize === "a3" ? 318 : 230), e.height || (this.templateData.properties.paperSize === "a3" ? 104 : 110) ); break; } case "diagramlegend": { this.addDiagramLegend( e.position, e.offset || defaultOffset, e.color || defaultColor ); break; } case "diagramtitle": { this.addDiagramTitle( e.position, e.offset || defaultOffset, e.fontsize || defaultFontSize, e.color || defaultColor, diagramTitle ); break; } case "text": { this.addText( e.position, e.offset || defaultOffset, e.width, e.fontsize || defaultFontSize, e.color || defaultTextColor, e.text || "" ); break; } case "image": { this.addImage( e.url, e.format || "", e.position, e.offset || defaultOffset, e.width || 90, e.height || 60 ); 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; } } }); } }, // add text at specific coordinates and do line breaks addText(position, offset, width, fontSize, color, text) { text = this.replacePlaceholders(text); // split the incoming string to an array, each element is a string of // words in a single line this.pdf.doc.setFontStyle("normal"); this.pdf.doc.setTextColor(color); this.pdf.doc.setFontSize(fontSize); // x/y defaults to offset for topleft corner (normal x/y coordinates) let x = offset.x; let y = offset.y; // if position is on the right, x needs to be calculate with pdf width and // the size of the element if (!width) { width = this.pdf.doc.getTextWidth(text); } var textLines = this.pdf.doc.splitTextToSize(text, width); if ( ["topright", "topleft"].indexOf(position) !== -1 && y < this.getTextHeight(1) ) { y = this.getTextHeight(1); } 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(textLines.length); } this.pdf.doc.text(textLines, x, y, { baseline: "hanging" }); }, replacePlaceholders(text) { if (text.includes("{date}")) { text = text.replace("{date}", new Date().toLocaleString(locale2)); } const shortDate = d => { return ( (d.getDate() < 10 ? "0" : "") + d.getDate() + "." + (d.getMonth() + 1 < 10 ? "0" : "") + (d.getMonth() + 1) + "." + d.getFullYear() ); }; const hasVisibleSurvey = this.soundingInfo && this.bottleneckForPrint && this.selectedSurvey && this.openLayersMap() .getLayer("BOTTLENECKISOLINE") .getVisible(); if (text.includes("{surveydate}") && hasVisibleSurvey) { const dateFromSurvey = new Date(this.selectedSurvey["date_info"]); let dt = shortDate(dateFromSurvey); text = text.replace("{surveydate}", dt.toLocaleString(locale2)); } else { let dt = shortDate(new Date()); text = text.replace("{surveydate}", dt.toLocaleString(locale2)); } // get only day,month and year from the Date object if (text.includes("{date-minor}")) { var date = new Date(); let dt = shortDate(date); text = text.replace("{date-minor}", dt.toLocaleString(locale2)); } if (text.includes("{user}")) { text = text.replace("{user}", this.user); } if (text.includes("{zpg-exception}")) { // Print the text followed by "zpg-exception" if this value set to true if ( this.soundingInfo && this.soundingInfo.number > 0 && this.soundingInfo.feature.properties.zpg_exception ) { text = text.replace("{zpg-exception}", ""); // Otherwise nothing to print } else { text = ""; } } return text; }, addImage(url, format, position, offset, width, height) { let x = offset.x; let y = offset.y; 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; } let image = new Image(); image.src = url; if (format === "") { let tmp = image.src.split("."); format = tmp[tmp.length - 1].toUpperCase(); } this.pdf.doc.addImage(image, format, x, y, width, height); }, // add text at specific coordinates with a background box addBox(position, offset, width, height, rounding, color, brcolor) { // x/y defaults to offset for topleft corner (normal x/y coordinates) let x = offset.x; let y = offset.y; // 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 - height; } this.addRoundedBox(x, y, width, height, color, rounding, brcolor); }, getTextHeight(numberOfLines) { // Return estimated height in mm. // FontSize is given in desktop publishing points defined as 1/72 inch. // aka 25.4 / 72 mm let fontSize = this.pdf.doc.getFontSize(); let lineHeightFactor = 1.15; // default from jspdf-yworks 2.0.2 if (typeof this.pdf.doc.getLineHeightFactor !== "undefined") { lineHeightFactor = this.pdf.doc.getLineHeightFactor(); } return numberOfLines * fontSize * (25.4 / 72) * lineHeightFactor; }, // title for diagram addDiagramTitle(position, offset, size, color, text) { let x = offset.x, y = offset.y; this.pdf.doc.setFontSize(size); this.pdf.doc.setFontStyle("bold"); this.pdf.doc.setTextColor(color); let width = (this.pdf.doc.getStringUnitWidth(text) * size) / (72 / 25.6) + size / 2; // 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(1); } this.pdf.doc.text(text, x, y, { baseline: "hanging" }); }, addRoundedBox(x, y, w, h, color, rounding, brcolor) { this.pdf.doc.setDrawColor(brcolor); this.pdf.doc.setFillColor(color); this.pdf.doc.roundedRect(x, y, w, h, rounding, rounding, "FD"); }, addTextBox( position, offset, width, height, rounding, padding, fontSize, color, background, text, brcolor ) { this.pdf.doc.setFontSize(fontSize); text = this.replacePlaceholders(text); if (!width) { width = this.pdf.doc.getTextWidth(text) + 2 * padding; } let textWidth = width - 2 * padding; let textLines = this.pdf.doc.splitTextToSize(text, textWidth); if (!height) { height = this.getTextHeight(textLines.length) + 2 * padding; } this.addBox( position, offset, width, height, rounding, background, brcolor ); let yForText = ["bottomright", "bottomleft"].indexOf(position) !== -1 ? offset.y : offset.y + height - this.getTextHeight(textLines.length); this.addText( position, { x: offset.x + padding, y: yForText }, textWidth, fontSize, color, text ); }, dateForPDF() { return new Date() .toISOString() .slice(0, 10) .replace(/-/g, ""); } } };