Mercurial > gemma
view client/src/components/Pdftool.vue @ 3726:be939dcdfdfd
client: waterlevel diagrams: make now line label not update its position when zooming
Of course the line and label could both be updated and move with the time but since all other data is static after generating
the diagram once, a moving now line would be to fancy... but I was tempted.
author | Markus Kottlaender <markus@intevation.de> |
---|---|
date | Thu, 20 Jun 2019 19:16:26 +0200 |
parents | a26c1d745fc3 |
children | 2b6734a6730a |
line wrap: on
line source
<template> <div :class="[ 'box ui-element rounded bg-white text-nowrap', { expanded: showPdfTool } ]" > <div style="width: 18rem"> <UIBoxHeader icon="file-pdf" :title="generatePdfLable" :closeCallback="close" /> <div class="box-body"> <select @change="applyTemplateToForm" v-model="form.template" class="form-control d-block mb-2 w-100 font-weight-bold" > <option v-for="template in templates" :value="template" :key="template.name" > {{ template.name }} </option> </select> <select v-model="form.format" class="form-control form-control-sm d-block mb-2 w-100" > <option value="landscape"><translate>landscape</translate></option> <option value="portrait"><translate>portrait</translate></option> </select> <div class="d-flex"> <div class="flex-fill mr-2"> <select v-model="form.resolution" class="form-control form-control-sm mb-2 d-block w-100" > <option value="80"><translate>80 dpi</translate></option> <option value="120"><translate>120 dpi</translate></option> <option value="200"><translate>200 dpi</translate></option> </select> </div> <div class="flex-fill ml-2"> <select v-model="form.paperSize" class="form-control form-control-sm mb-2 d-block w-100" > <option value="a4"><translate>A4</translate></option> <option value="a3"><translate>A3</translate></option> </select> </div> </div> <button @click="download" :key="'downloadBtn'" type="button" v-if="readyToGenerate" class="btn btn-sm btn-info d-block w-100 mt-2" > <translate>Generate PDF</translate> </button> <button @click="cancel" :key="'cancelBtn'" type="button" v-else class="btn btn-sm btn-danger d-block w-100 mt-2" > <font-awesome-icon class="mr-1" icon="spinner" spin /> <translate>Cancel</translate> </button> </div> </div> </div> </template> <style lang="scss" scoped> input, select { font-size: 0.8em; } </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): * * Markus Kottländer <markus.kottlaender@intevation.de> * * Bernhard E. Reiter <bernhard@intevation.de> * * Fadi Abbud <fadi.abbud@intevation.de> */ import { mapState, mapGetters } from "vuex"; import jsPDF from "jspdf"; import "@/lib/font-linbiolinum"; import { getPointResolution } from "ol/proj"; import { HTTP } from "@/lib/http"; import { displayError } from "@/lib/errors"; import { pdfgen } from "@/lib/mixins"; var paperSizes = { // in millimeter, landscape [width, height] a3: [420, 297], a4: [297, 210] }; export default { mixins: [pdfgen], name: "pdftool", data() { return { form: { template: null, format: "landscape", paperSize: "a4", downloadType: "download", resolution: "80" }, templates: [ { name: "Default", properties: { format: "landscape", paperSize: "a4", resolution: "80" }, elements: [ { type: "scalebar", position: "bottomright", offset: { x: 2, y: 2 } }, { type: "textbox", position: "bottomleft", offset: { x: 2, y: 2 }, width: 60, fontSize: 8, text: this.$gettext("Generated by") + " " + "{user}, {date}" }, { type: "northarrow", position: "topright", offset: { x: 6, y: 4 }, size: 2 } ] } ], templateData: null, pdf: { doc: null, width: null, height: null }, logoImageForPDF: null, // a HTMLImageElement instance readyToGenerate: true, // if the user is allowed to press the button rendercompleteListener: null, mapSize: null, mapExtent: null }; }, computed: { ...mapState("application", ["showPdfTool", "logoForPDF"]), ...mapState("bottlenecks", ["selectedBottleneck", "selectedSurvey"]), ...mapState("map", ["isolinesLegendImgDataURL"]), ...mapGetters("map", ["openLayersMap"]), generatePdfLable() { return this.$gettext("Generate PDF"); }, filename() { let date = new Date() .toISOString() .slice(0, 10) .replace(/-/g, ""); let filename = "map"; if (this.selectedBottleneck) { filename = this.selectedBottleneck; if (this.selectedSurvey) { filename += "-sr" + this.selectedSurvey.date_info.replace(/-/g, ""); } } filename = filename .replace(/[^\w-]/gi, "") // remove everything but wordchars and dash .toLowerCase() + "-exported" + date + ".pdf"; return filename; } }, methods: { close() { this.$store.commit("application/showPdfTool", false); }, // When a template is chosen from the dropdown, its propoerties are // applied to the rest of the form. applyTemplateToForm() { if (this.form.template) { HTTP.get( "/templates/" + this.form.template.type + "/" + 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; this.form.format = this.templateData.properties.format; this.form.paperSize = this.templateData.properties.paperSize; this.form.resolution = this.templateData.properties.resolution; }) .catch(e => { const { status, data } = e.response; displayError({ title: this.$gettext("Backend Error"), message: `${status}: ${data.message || data}` }); }); } }, download() { // disable button while working on it this.readyToGenerate = false; console.log( "will generate pdf with", this.form.paperSize, this.form.format, this.form.resolution ); if (this.form.format !== "portrait") { // landscape, default this.pdf.width = paperSizes[this.form.paperSize][0]; this.pdf.height = paperSizes[this.form.paperSize][1]; } else { // switch width and height this.pdf.width = paperSizes[this.form.paperSize][1]; this.pdf.height = paperSizes[this.form.paperSize][0]; } // FUTURE: consider margins // dots per mm = dots per inch / (25.4 mm/inch) var pixelsPerMapMillimeter = this.form.resolution / 25.4; var mapSizeForPrint = [ // in pixel Math.round(this.pdf.width * pixelsPerMapMillimeter), Math.round(this.pdf.height * pixelsPerMapMillimeter) ]; // generate PDF and open it // our units are milimeters; width 0 x height 0 is left upper corner // Step 1 prepare and save current map extend // Then add callback "rendercomplete" for Step 3 // which will generate the pdf and resets the map view // Step 2 which starts rendering a map with the necessary image size var map = this.openLayersMap(); this.mapSize = map.getSize(); // size in pixels of the map in the DOM // Calculate the extent for the current view state and the passed size. // The size is the pixel dimensions of the box into which the calculated // extent should fit. this.mapExtent = map.getView().calculateExtent(this.mapSize); this.pdf.doc = new jsPDF(this.form.format, "mm", this.form.paperSize); // set a callback for after the next complete rendering of the map this.rendercompleteListener = map.once("rendercomplete", event => { let canvas = event.context.canvas; // because we are using Web Mercator, a pixel represents // a differently sized spot depending on the place of the map. // So we use a value calculated from the center of the current view. let view = map.getView(); let proj = view.getProjection(); let metersPerPixel = // average meters (reality) per pixel (map) getPointResolution(proj, view.getResolution(), view.getCenter()) * proj.getMetersPerUnit(); // DEBUG console.log("metersPerPixel = ", metersPerPixel); let scaleDenominator = Math.round( // the x in 1:x map scale 1000 * pixelsPerMapMillimeter * metersPerPixel ); console.log("scaleDenominator = ", scaleDenominator); var data = canvas.toDataURL("image/jpeg"); this.pdf.doc.addImage( data, "JPEG", 0, 0, this.pdf.width, this.pdf.height ); if (this.templateData) { this.pdf.doc.setFont("linbiolinum", "normal"); let defaultFontSize = 11, defaultRounding = 2, defaultTextColor = "black", defaultBgColor = "white", defaultPadding = 3, defaultOffset = { x: 0, y: 0 }, defaultBorderColor = "white"; this.templateData.elements.forEach(e => { switch (e.type) { case "text": { this.addText( e.position, e.offset || defaultOffset, e.width, e.fontSize || defaultFontSize, e.color || defaultTextColor, e.text ); break; } case "box": { this.addBox( e.position, e.offset || defaultOffset, e.width, e.height, // handling the case when the rectangle not rounded (rounding = 0) 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; } case "image": { this.addImage( e.url, e.format, e.position, e.offset || defaultOffset, e.width, e.height ); break; } case "bottleneck": { this.addBottleneckInfo( e.position, e.offset || defaultOffset, e.rounding === 0 || e.rounding ? e.rounding : defaultRounding, e.color || defaultTextColor, e.brcolor || defaultBorderColor ); break; } case "legend": { this.addLegend( e.position, e.offset || defaultOffset, e.rounding === 0 || e.rounding ? e.rounding : defaultRounding, e.brcolor || defaultBorderColor ); break; } case "scalebar": { this.addScaleBar( scaleDenominator, e.position, e.offset || defaultOffset, e.rounding === 0 || e.rounding ? e.rounding : defaultRounding, e.brcolor || defaultBorderColor ); break; } case "scale": { this.addScale( scaleDenominator, e.position, e.width, e.offset || defaultOffset, e.fontSize || defaultFontSize, e.color || defaultTextColor ); break; } case "northarrow": { this.addNorthArrow( e.position, e.offset || defaultOffset, e.size ); break; } } }); this.pdf.doc.save(this.filename); } // reset to original size map.setSize(this.mapSize); map.getView().fit(this.mapExtent, { size: this.mapSize, // necessary to get to the previous zoom level in all cases // details see https://github.com/openlayers/openlayers/issues/9235 constrainResolution: false }); // as we are done: re-enable button this.readyToGenerate = true; }); // trigger rendering map.setSize(mapSizeForPrint); map.getView().fit(this.mapExtent, { size: mapSizeForPrint }); }, cancel() { this.openLayersMap().un( this.rendercompleteListener.type, this.rendercompleteListener.listener ); this.openLayersMap().setSize(this.mapSize); this.openLayersMap() .getView() .fit(this.mapExtent, { size: this.mapSize }); this.readyToGenerate = true; }, // add the used map scale and papersize addScale(scaleDenominator, position, width, offset, fontSize, color) { //TODO: check the correctence of the scalnominator value here. let str = this.$gettext("Scale") + " 1 : " + scaleDenominator + " " + "(DIN" + " " + this.form.paperSize.toUpperCase() + ")"; this.addText(position, offset, width, fontSize, color, str); }, addScaleBar(scaleDenominator, position, offset, rounding, brcolor) { // scaleDenominator is the x in 1:x of the map scale // hardcode maximal width for now let maxWidth = 80; // in mm // reduce width until we'll find a nice number for printing // strategy: // 1. check which unit prefix we shall use to get [10:10000[ // 2. using a mapping for the leading digit to get [1:10[ // 3. select a smaller number which is nicely dividable // 4. scale up again to get length in paper mm and to be shown // from https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/log10#Polyfill let log10 = Math.log10 || // more precise, but unsupported by IE function(x) { return Math.log(x) * Math.LOG10E; }; let maxLength = maxWidth * scaleDenominator; let unit = "mm"; let unitConversionFactor = 1; if (maxLength >= 1e7) { // >= 10 km unit = "km"; unitConversionFactor = 1e6; } else if (maxLength >= 1e4) { // >= 10 m unit = "m"; unitConversionFactor = 1e3; } maxLength /= unitConversionFactor; // DEBUG console.log(maxLength, unit); let unroundedLength = maxLength; let numberOfDigits = Math.floor(log10(unroundedLength)); let factor = Math.pow(10, numberOfDigits); let mapped = unroundedLength / factor; // DEBUG console.log(mapped); var length = Math.floor(maxLength); // just to have an upper limit // manually only use numbers that are very nice to devide by 4 // note that this is taken into account for rounding later if (mapped > 8) { length = 8 * factor; } else if (mapped > 4) { length = 4 * factor; } else if (mapped > 2) { length = 2 * factor; } else { length = factor; } let size = (length * unitConversionFactor) / scaleDenominator / 4; let fullSize = size * 4; // 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 - fullSize - 8; } if (["bottomright", "bottomleft"].indexOf(position) !== -1) { y = this.pdf.height - offset.y - 10; } // to give the outer white box 4mm padding let scaleBarX = x + 4; let scaleBarY = y + 5; // 5 because above the scalebar will be the numbers // draw outer white box this.addRoundedBox(x, y, fullSize + 8, 10, "white", rounding, brcolor); // draw first part of scalebar this.pdf.doc.setDrawColor(0, 0, 0); this.pdf.doc.setFillColor(0, 0, 0); this.pdf.doc.rect(scaleBarX, scaleBarY, size, 1, "FD"); // draw second part of scalebar this.pdf.doc.setDrawColor(0, 0, 0); this.pdf.doc.setFillColor(255, 255, 255); this.pdf.doc.rect(scaleBarX + size, scaleBarY, size, 1, "FD"); // draw third part of scalebar this.pdf.doc.setDrawColor(0, 0, 0); this.pdf.doc.setFillColor(0, 0, 0); this.pdf.doc.rect(scaleBarX + size * 2, scaleBarY, size * 2, 1, "FD"); // draw numeric labels above scalebar this.pdf.doc.setTextColor("black"); this.pdf.doc.setFontSize(6); this.pdf.doc.text(scaleBarX, scaleBarY - 1, "0"); // /4 and could give 2.5. We still round, because of floating point arith this.pdf.doc.text( scaleBarX + size - 1, scaleBarY - 1, (Math.round((length * 10) / 4) / 10).toString() ); this.pdf.doc.text( scaleBarX + size * 2 - 2, scaleBarY - 1, Math.round(length / 2).toString() ); this.pdf.doc.text( scaleBarX + size * 4 - 4, scaleBarY - 1, Math.round(length).toString() + " " + unit ); }, addNorthArrow(position, offset, size) { // TODO: fix positioning // x/y defaults to offset for topleft corner (normal x/y coordinates) let x1 = offset.x; let y1 = 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) { x1 = this.pdf.width - offset.x - size; } if (["bottomright", "bottomleft"].indexOf(position) !== -1) { y1 = this.pdf.height - offset.y - size; } var y2 = y1 + size * 3; var x3 = x1 - size * 2; var y3 = y1 + size * 5; var x4 = x1 + size * 2; // white triangle this.pdf.doc.setFillColor(255, 255, 255); this.pdf.doc.setDrawColor(255, 255, 255); this.pdf.doc.triangle( x3 - 0.8, y3 + 1.2, x1, y1 - 1.2, x1, y2 + 0.6, "F" ); this.pdf.doc.triangle( x1, y1 - 1.2, x1, y2 + 0.6, x4 + 0.8, y3 + 1.2, "F" ); // north arrow this.pdf.doc.setDrawColor(0, 0, 0); this.pdf.doc.setFillColor(255, 255, 255); this.pdf.doc.triangle(x3, y3, x1 - 0.1, y1 + 0.2, x1 - 0.1, y2, "FD"); this.pdf.doc.setFillColor(0, 0, 0); this.pdf.doc.triangle(x1 + 0.1, y1 + 0.2, x1 + 0.1, y2, x4, y3, "FD"); this.pdf.doc.setFontSize(size * 3.1); this.pdf.doc.setTextColor(255, 255, 255); this.pdf.doc.setFontStyle("bold"); this.pdf.doc.text(size < 3 ? x1 - 0.5 : x1 - 1.3, y3 + 1, "N"); this.pdf.doc.setFontSize(size * 3); this.pdf.doc.setTextColor(0, 0, 0); this.pdf.doc.setFontStyle("normal"); this.pdf.doc.text(size < 3 ? x1 - 0.5 : x1 - 1.3, y3 + 1, "N"); }, addLegend(position, offset, rounding, brcolor) { if ( this.selectedBottleneck && this.selectedSurvey && this.openLayersMap() .getLayer("BOTTLENECKISOLINE") .getVisible() ) { // transforming into an HTMLImageElement only to find out // the width x height of the legend image // FUTURE: find a better way to get the width and height let legendImage = new Image(); legendImage.src = this.isolinesLegendImgDataURL; let aspectRatio = legendImage.width / legendImage.height; let width = 54; let height = width / aspectRatio; let padding = 2; // 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, "white", rounding, brcolor); this.pdf.doc.addImage( legendImage, x + padding, y + padding, width - 2 * padding, height - 2 * padding ); } }, addBottleneckInfo(position, offset, rounding, color, brcolor) { if ( this.selectedBottleneck && this.selectedSurvey && this.openLayersMap() .getLayer("BOTTLENECKISOLINE") .getVisible() ) { let survey = this.selectedSurvey; // determine text dimensions // this is a little bit cumbersome but we need to separate width // calculations and writing this.pdf.doc.setFontSize(10); this.pdf.doc.setTextColor(color); let textOptions = { baseline: "hanging" }; let str1_1 = this.$gettext("Bottleneck") + ": "; let str1_2 = this.selectedBottleneck; let str2_1 = this.$gettext("Survey date") + ": "; let str2_2 = survey.date_info; let str3_1 = this.$gettext("Ref gauge") + ": "; let str3_2 = survey.gauge_objname; let str4_1 = this.$gettext("Depth relativ to") + ": "; let str4_2 = survey.depth_reference + " = " + (survey.hasOwnProperty("waterlevel_value") ? survey.waterlevel_value + " cm" : "?"); this.pdf.doc.setFontStyle("italic"); let w1_1 = this.pdf.doc.getTextWidth(str1_1); this.pdf.doc.setFontStyle("bold"); let w1_2 = this.pdf.doc.getTextWidth(str1_2); this.pdf.doc.setFontStyle("italic"); let w2_1 = this.pdf.doc.getTextWidth(str2_1); this.pdf.doc.setFontStyle("normal"); let w2_2 = this.pdf.doc.getTextWidth(str2_2); this.pdf.doc.setFontStyle("italic"); let w3_1 = this.pdf.doc.getTextWidth(str3_1); this.pdf.doc.setFontStyle("normal"); let w3_2 = this.pdf.doc.getTextWidth(str3_2); this.pdf.doc.setFontStyle("italic"); let w4_1 = this.pdf.doc.getTextWidth(str4_1); this.pdf.doc.setFontStyle("normal"); let w4_2 = this.pdf.doc.getTextWidth(str4_2); let height = 21; let padding = 3; let width = Math.max(w1_1 + w1_2, w2_1 + w2_2, w3_1 + w3_2, w4_1 + w4_2) + 2 * padding; // 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; } // white background box this.addRoundedBox(x, y, width, height, "white", rounding, brcolor); // bottleneck this.pdf.doc.setFontStyle("italic"); this.pdf.doc.text(x + padding, y + padding, str1_1, textOptions); this.pdf.doc.setFontStyle("bold"); this.pdf.doc.text(x + padding + w1_1, y + padding, str1_2, textOptions); // survey date this.pdf.doc.setFontStyle("italic"); this.pdf.doc.text(x + padding, y + padding + 4, str2_1, textOptions); this.pdf.doc.setFontStyle("normal"); this.pdf.doc.text( x + padding + w2_1, y + padding + 4, str2_2, textOptions ); // ref gauge this.pdf.doc.setFontStyle("italic"); this.pdf.doc.text(x + padding, y + padding + 8, str3_1, textOptions); this.pdf.doc.setFontStyle("normal"); this.pdf.doc.text( x + padding + w3_1, y + padding + 8, str3_2, textOptions ); // depth relative to this.pdf.doc.setFontStyle("italic"); this.pdf.doc.text(x + padding, y + padding + 12, str4_1, textOptions); this.pdf.doc.setFontStyle("normal"); this.pdf.doc.text( x + padding + w4_1, y + padding + 12, str4_2, textOptions ); } }, getTextHeight(numberOfLines) { return ( numberOfLines * ((this.pdf.doc.getFontSize() * 25.4) / parseInt(this.form.resolution)) * this.pdf.doc.getLineHeightFactor() ); } }, mounted() { this.form.template = this.templates[0]; this.templateData = this.form.template; HTTP.get("/templates/map", { 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.applyTemplateToForm(); } }) .catch(e => { const { status, data } = e.response; displayError({ title: this.$gettext("Backend Error"), message: `${status}: ${data.message || data}` }); }); } }; </script>