Mercurial > gemma
changeset 2229:6cce66a6ceb5
merged pdf-export into default
author | Markus Kottlaender <markus@intevation.de> |
---|---|
date | Wed, 13 Feb 2019 08:00:26 +0100 |
parents | 25f73251a6ac (current diff) 9b15293d028c (diff) |
children | 4374d942b23d |
files | client/src/components/Systemconfiguration.vue |
diffstat | 8 files changed, 913 insertions(+), 416 deletions(-) [+] |
line wrap: on
line diff
--- a/client/src/assets/application.scss Tue Feb 12 23:28:01 2019 +0100 +++ b/client/src/assets/application.scss Wed Feb 13 08:00:26 2019 +0100 @@ -99,16 +99,13 @@ } .popup { - position: absolute; - top: 40%; - left: 50%; - margin-left: -150px; width: 300px; max-width: 300px; + @extend %fully-centered; } .popup.show { - margin: 0.5rem 0 0 -150px; + margin: 0.5rem 0 0 0; max-height: 999px; } @@ -133,6 +130,14 @@ font-weight: bold; } +.list-fade-enter-active, .list-fade-leave-active { + transition: transform .3s; +} +.list-fade-enter, .list-fade-leave-to { + opacity: 0; + transform: translateY(20px); +} + .pointer { cursor: pointer; }
--- a/client/src/components/Pdftool.vue Tue Feb 12 23:28:01 2019 +0100 +++ b/client/src/components/Pdftool.vue Wed Feb 13 08:00:26 2019 +0100 @@ -21,19 +21,15 @@ v-model="form.template" class="form-control d-block mb-2 w-100 font-weight-bold" > - <option :value="null"><translate>Chose preset</translate></option> <option - v-for="template in templates" - :value="template.name" + v-for="template in pdfTemplates" + :value="template" :key="template.name" > <translate>{{ template.name }}</translate> </option> </select> - <hr class="mb-1" /> - <small class="text-muted"><translate>Format</translate></small> <select - @change="compareFormWithTemplates" v-model="form.format" class="form-control form-control-sm d-block mb-2 w-100" > @@ -42,26 +38,22 @@ </select> <div class="d-flex"> <div class="flex-fill mr-2"> - <small class="text-muted"><translate>Resolution</translate></small> <select - @change="compareFormWithTemplates" v-model="form.resolution" class="form-control form-control-sm mb-2 d-block w-100" > - <option value="80">80 dpi</option> - <option value="120">120 dpi</option> - <option value="200">200 dpi</option> + <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"> - <small class="text-muted"><translate>Size</translate></small> <select - @change="compareFormWithTemplates" v-model="form.paperSize" class="form-control form-control-sm mb-2 d-block w-100" > - <option value="a3"><translate>ISO A3</translate></option> - <option value="a4"><translate>ISO A4</translate></option> + <option value="a4"><translate>A4</translate></option> + <option value="a3"><translate>A3</translate></option> </select> </div> </div> @@ -137,32 +129,19 @@ format: "landscape", paperSize: "a4", downloadType: "download", - resolution: "120" + resolution: "80" }, - templates: [ - { - name: "Template 1", - properties: { - format: "landscape", - resolution: "80", - paperSize: "a4" - } - }, - { - name: "Template 2", - properties: { - format: "portrait", - resolution: "120", - paperSize: "a3" - } - } - ], + pdf: { + doc: null, + width: null, + height: null + }, logoImageForPDF: null, // a HTMLImageElement instance readyToGenerate: true // if the user is allowed to press the button }; }, computed: { - ...mapState("application", ["showPdfTool", "logoForPDF"]), + ...mapState("application", ["showPdfTool", "logoForPDF", "pdfTemplates"]), ...mapState("bottlenecks", ["selectedBottleneck", "selectedSurvey"]), ...mapState("map", ["openLayersMap", "isolinesLegendImgDataURL"]), ...mapGetters("map", ["getLayerByName"]), @@ -172,28 +151,15 @@ // When a template is chosen from the dropdown, its propoerties are // applied to the rest of the form. applyTemplateToForm() { - let template = this.templates.find(t => t.name === this.form.template); - if (template) { - this.form.format = template.properties.format; - this.form.paperSize = template.properties.paperSize; - this.form.resolution = template.properties.resolution; + if (this.form.template) { + this.form.format = this.form.template.properties.format; + this.form.paperSize = this.form.template.properties.paperSize; + this.form.resolution = this.form.template.properties.resolution; } }, - // If there's a template that matches all the form values, this template - // will be set in the dropdown. - compareFormWithTemplates() { - this.form.template = null; - this.templates.forEach(t => { - if ( - this.form.format === t.properties.format && - this.form.paperSize === t.properties.paperSize && - this.form.resolution === t.properties.resolution - ) { - this.form.template = t.name; - } - }); - }, download() { + let template = this.form.template; + // disable button while working on it this.readyToGenerate = false; @@ -203,16 +169,15 @@ this.form.format, this.form.resolution ); - var width, height; if (this.form.format !== "portrait") { // landscape, default - width = paperSizes[this.form.paperSize][0]; - height = paperSizes[this.form.paperSize][1]; + this.pdf.width = paperSizes[this.form.paperSize][0]; + this.pdf.height = paperSizes[this.form.paperSize][1]; } else { // switch width and height - width = paperSizes[this.form.paperSize][1]; - height = paperSizes[this.form.paperSize][0]; + this.pdf.width = paperSizes[this.form.paperSize][1]; + this.pdf.height = paperSizes[this.form.paperSize][0]; } // FUTURE: consider margins @@ -221,8 +186,8 @@ var pixelsPerMapMillimeter = this.form.resolution / 25.4; var mapSizeForPrint = [ // in pixel - Math.round(width * pixelsPerMapMillimeter), - Math.round(height * pixelsPerMapMillimeter) + Math.round(this.pdf.width * pixelsPerMapMillimeter), + Math.round(this.pdf.height * pixelsPerMapMillimeter) ]; // generate PDF and open it @@ -240,12 +205,9 @@ // extent should fit. var mapExtent = map.getView().calculateExtent(mapSize); - var pdf = new jsPDF(this.form.format, "mm", this.form.paperSize); - var northarrowSize = 3; - var self = this; - + this.pdf.doc = new jsPDF(this.form.format, "mm", this.form.paperSize); // set a callback for after the next complete rendering of the map - map.once("rendercomplete", function(event) { + map.once("rendercomplete", event => { let canvas = event.context.canvas; // because we are using Web Mercator, a pixel represents @@ -265,70 +227,238 @@ console.log("scaleNominator = ", scaleNominator); var data = canvas.toDataURL("image/jpeg"); - pdf.addImage(data, "JPEG", 0, 0, width, height); - self.addScaleBar(pdf, width, height, scaleNominator); - self.addNorthArrow(pdf, 15, 9, northarrowSize); - self.addPageInfo(pdf); - self.addAboutBox(pdf, width, height); + this.pdf.doc.addImage( + data, + "JPEG", + 0, + 0, + this.pdf.width, + this.pdf.height + ); - if (self.getLayerByName("Bottleneck isolines").isVisible) { - self.addBottleneckInfo(pdf, 15.5, width, height); - self.addLegend(pdf, 16.5, width, height); + if (template) { + let defaultFontSize = 10, + defaultRounding = 0, + defaultTextColor = "black", + defaultBgColor = "white", + defaultPadding = 3, + defaultOffset = { x: 0, y: 0 }; + template.elements.forEach(e => { + switch (e.type) { + case "text": { + this.addText( + e.position, + e.offset || defaultOffset, + e.width, + e.rounding || defaultRounding, + e.fontSize || defaultFontSize, + e.color || defaultTextColor, + e.text + ); + break; + } + case "box": { + this.addBox( + e.position, + e.offset || defaultOffset, + e.width, + e.height, + e.rounding || defaultRounding, + e.color || defaultBgColor + ); + break; + } + case "textbox": { + this.addTextBox( + e.position, + e.offset || defaultOffset, + e.width, + e.height, + e.rounding || defaultRounding, + e.padding || defaultPadding, + e.fontSize || defaultFontSize, + e.color || defaultTextColor, + e.background || defaultBgColor, + e.text + ); + 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 || defaultRounding, + e.color || defaultTextColor + ); + break; + } + case "legend": { + this.addLegend( + e.position, + e.offset || defaultOffset, + e.rounding || defaultRounding + ); + break; + } + case "scalebar": { + this.addScaleBar( + scaleNominator, + e.position, + e.offset || defaultOffset, + e.rounding || defaultRounding + ); + break; + } + case "northarrow": { + this.addNorthArrow( + e.position, + e.offset || defaultOffset, + e.size + ); + break; + } + } + }); + + this.pdf.doc.save("map.pdf"); } - - pdf.save("map.pdf"); // reset to original size map.setSize(mapSize); map.getView().fit(mapExtent, { size: mapSize }); // as we are done: re-enable button - self.readyToGenerate = true; + this.readyToGenerate = true; }); // trigger rendering - this.prepareRendering(function() { - map.setSize(mapSizeForPrint); - map.getView().fit(mapExtent, { size: mapSizeForPrint }); - - /* - let a = document.createElement("a"); - a.href = src; // need the generated PDF in here (as dataURL?) - - if (this.form.downloadType === "download") - a.download = src.substr(src.lastIndexOf("/") + 1); - else a.target = "_blank"; - - document.body.appendChild(a); - a.click(); - document.body.removeChild(a); - */ - }); + map.setSize(mapSizeForPrint); + map.getView().fit(mapExtent, { size: mapSizeForPrint }); }, - prepareRendering(callback) { - // call callback() once the preparations are done - this.logoImageForPDF = new Image(); - - this.logoImageForPDF.onload = function() { - callback(); - }; - - if (this.logoForPDF) { - this.logoImageForPDF.src = this.logoForPDF; - } else { - this.logoImageForPDF.src = "/img/gemma-logo-for-pdf.png"; - } - }, - addRoundedBox(doc, x, y, w, h) { + addRoundedBox(x, y, w, h, color, rounding) { // draws a rounded background box at (x,y) width x height // using jsPDF units - doc.setDrawColor(255, 255, 255); - doc.setFillColor(255, 255, 255); - doc.roundedRect(x, y, w, h, 3, 3, "FD"); + this.pdf.doc.setDrawColor(color); + this.pdf.doc.setFillColor(color); + this.pdf.doc.roundedRect(x, y, w, h, rounding, rounding, "FD"); + }, + // add some text at specific coordinates and determine how many wrolds in single line + addText(position, offset, width, rounding, 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.setTextColor(color); + this.pdf.doc.setFontSize(fontSize); + var textLines = this.pdf.doc.splitTextToSize(text, width); + + // 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 - this.getTextHeight(textLines.length); + } + + this.pdf.doc.text(textLines, x, y, { baseline: "hanging" }); + }, + addBox(position, offset, width, height, rounding, color) { + // 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); }, - addScaleBar(doc, docWidth, docHeight, scaleNominator) { + // add some text at specific coordinates with a background box + addTextBox( + position, + offset, + width, + height, + rounding, + padding, + fontSize, + color, + background, + text + ) { + if (!width) { + width = this.pdf.width - offset.x; + } + let textWidth = width - 2 * padding; + if (!height) { + this.pdf.doc.setFontSize(fontSize); + text = this.replacePlaceholders(text); + let textLines = this.pdf.doc.splitTextToSize(text, textWidth); + height = this.getTextHeight(textLines.length) + 2 * padding; + } + + this.addBox(position, offset, width, height, rounding, background); + this.addText( + position, + { x: offset.x + padding, y: offset.y + padding }, + textWidth, + rounding, + fontSize, + color, + text + ); + }, + addImage(url, format, position, offset, width, height) { + // 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; + } + + let image = new Image(); + if (url) { + image.src = url; + } else { + if (this.logoForPDF) { + image.src = this.logoForPDF; + } else { + image.src = "/img/gemma-logo-for-pdf.png"; + } + } + + this.pdf.doc.addImage(image, x, y, width, height); + }, + addScaleBar(scaleNominator, position, offset, rounding) { // scaleNominator is the x in 1:x of the map scale - // hardcode maximal width for now and place in lower right corner + // hardcode maximal width for now let maxWidth = 80; // in mm // reduce width until we'll find a nice number for printing @@ -383,154 +513,234 @@ } let size = (length * unitConversionFactor) / scaleNominator / 4; + let fullSize = size * 4; - let x = docWidth - (size * 4 + 8); - let y = docHeight - 6; + // x/y defaults to offset for topleft corner (normal x/y coordinates) + let x = offset.x; + let y = offset.y; - this.addRoundedBox(doc, x - 4, y - 4, size * 4 + 12, 10); + // 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); - doc.setDrawColor(0, 0, 0); - doc.setFillColor(0, 0, 0); - doc.rect(x, y, size, 1, "FD"); - doc.setFillColor(255, 255, 255); - doc.setDrawColor(0, 0, 0); - doc.rect(x + size, y, size, 1, "FD"); - doc.setFillColor(0, 0, 0); - doc.setDrawColor(0, 0, 0); - doc.rect(x + size * 2, y, size * 2, 1, "FD"); - doc.setFontSize(5); - doc.text(x, y + 3, "0"); + // 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(5); + this.pdf.doc.text(scaleBarX, scaleBarY - 1, "0"); // /4 and could give 2.5. We still round, because of floating point arith - doc.text( - x + size, - y + 3, + this.pdf.doc.text( + scaleBarX + size - 1, + scaleBarY - 1, (Math.round((length * 10) / 4) / 10).toString() ); - doc.text(x + size * 2, y + 3, Math.round(length / 2).toString()); - doc.text(x + size * 4, y + 3, Math.round(length).toString() + " " + unit); + 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; - addNorthArrow(doc, x1, y1, size) { + // 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 - doc.setFillColor(255, 255, 255); - doc.setDrawColor(255, 255, 255); - doc.triangle(x3 - 0.8, y3 + 1.2, x1, y1 - 1.2, x1, y2 + 0.6, "F"); - doc.triangle(x1, y1 - 1.2, x1, y2 + 0.6, x4 + 0.8, y3 + 1.2, "F"); - //north arrow - doc.setDrawColor(0, 0, 0); - doc.setFillColor(255, 255, 255); - doc.triangle(x3, y3, x1, y1, x1, y2, "FD"); - doc.setFillColor(0, 0, 0); - doc.triangle(x1, y1, x1, y2, x4, y3, "FD"); - doc.setFontSize(size * 3.1); - doc.setTextColor(255, 255, 255); - doc.setFontStyle("bold"); - doc.text(size < 3 ? x1 - 0.5 : x1 - 1.3, y3 + 1, "N"); - doc.setFontSize(size * 3); - doc.setTextColor(0, 0, 0); - doc.setFontStyle("normal"); - doc.text(size < 3 ? x1 - 0.5 : x1 - 1.3, y3 + 1, "N"); + // 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, y1, x1, y2, "FD"); + this.pdf.doc.setFillColor(0, 0, 0); + this.pdf.doc.triangle(x1, y1, x1, 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"); }, - // add some text at specific coordinates and determine how many wrolds in single line - addText(doc, postitionX, positionY, size, color, lineWidth, text) { - // split the incoming string to an array, each element is a string of words in a single line - var textLines = doc.splitTextToSize(text, lineWidth); - doc.setTextColor(color); - doc.setFontSize(size); - doc.text(postitionX, positionY, textLines); - }, - addPageInfo(doc) { - this.addRoundedBox(doc, 0, 0, 110, 8); - let str = - this.$gettext("Date of publication:") + - " " + - new Date().toLocaleString(locale2) + - " " + - this.$gettext("– generated by:") + - " " + - this.user; - this.addText(doc, 5, 5, 9, "black", 100, str); - }, - addAboutBox(doc, docWidth, docHeight) { - let top = docHeight - 20; - this.addRoundedBox(doc, 0, top, 120, 20); + addLegend(position, offset, rounding) { + if ( + this.selectedBottleneck && + this.getLayerByName("Bottleneck isolines").isVisible + ) { + // 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; - let logoImage = this.logoImageForPDF; - let aspectRatio = logoImage.width / logoImage.height; - doc.addImage(logoImage, "PNG", 5, docHeight - 19, 110, 110 / aspectRatio); + // 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; + } - let str = - "Dislaimer: Lorem ipsum dolor sit amet, consectetur adipisici elit, sed eiusmod tempor incidunt ut labore et dolore magna aliqua."; - this.addText(doc, 5, docHeight - 6, 8, "black", 115, str); + this.addRoundedBox(x, y, width, height, "white", rounding); + this.pdf.doc.addImage( + legendImage, + x + padding, + y + padding, + width - 2 * padding, + height - 2 * padding + ); + } }, - addLegend(doc, fromTop, docWidth) { - // 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; + addBottleneckInfo(position, offset, rounding, color) { + if ( + this.selectedBottleneck && + this.getLayerByName("Bottleneck isolines").isVisible + ) { + let width = 54; + let height = 13; + let padding = 5; + + // x/y defaults to offset for topleft corner (normal x/y coordinates) + let x = offset.x; + let y = offset.y; - this.addRoundedBox(doc, docWidth - 54, fromTop, 54, 50 / aspectRatio + 4); - doc.addImage( - legendImage, - docWidth - 52, - fromTop + 2, - 50, - 50 / aspectRatio - ); - }, - addBottleneckInfo(doc, height, docWidth) { - this.addRoundedBox(doc, docWidth - 54, 0, 54, height); + // 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); - console.log("Fontlist =", doc.getFontList()); - doc.setFont("times", "normal"); + this.pdf.doc.setFont("times", "normal"); + this.pdf.doc.setFontSize(9); + this.pdf.doc.setTextColor(color); + + let str, w; - let name, w, str; + str = this.$gettext("Bottleneck") + ": "; + w = this.pdf.doc.getTextWidth(str); + this.pdf.doc.setFontStyle("italic"); + this.pdf.doc.text(x + padding, y + padding, str); + str = this.selectedBottleneck; + this.pdf.doc.setFontStyle("bold"); + this.pdf.doc.text(x + padding + w, y + padding, str); - doc.setFontStyle("italic"); - name = this.$gettext("Bottleneck") + ": "; - w = doc.getTextWidth(name); - this.addText(doc, docWidth - 51, 4, 8, "black", 46, name); - doc.setFontStyle("bold"); - str = this.selectedBottleneck; - this.addText(doc, docWidth - 51 + w, 4, 8, "black", 46, str); - - let survey = this.selectedSurvey; - - doc.setFontStyle("italic"); - name = this.$gettext("Survey date") + ": "; - w = doc.getTextWidth(name); - this.addText(doc, docWidth - 51, 7.5, 8, "black", 46, name); - doc.setFontStyle("normal"); - str = survey.date_info; - this.addText(doc, docWidth - 51 + w, 7.5, 8, "black", 46, str); + str = this.$gettext("Survey date") + ": "; + w = this.pdf.doc.getTextWidth(str); + this.pdf.doc.setFontStyle("italic"); + this.pdf.doc.text(x + padding, y + padding + 3, str); + str = this.selectedSurvey.date_info; + this.pdf.doc.setFontStyle("normal"); + this.pdf.doc.text(x + padding + w, y + padding + 3, str); - doc.setFontStyle("italic"); - name = this.$gettext("Ref gauge") + ": "; - w = doc.getTextWidth(name); - this.addText(doc, docWidth - 51, 11, 8, "black", 46, name); - doc.setFontStyle("normal"); - str = survey.gauge_objname; - this.addText(doc, docWidth - 51 + w, 11, 8, "black", 46, str); - - doc.setFontStyle("italic"); - name = this.$gettext("Depth relativ to") + ": "; - w = doc.getTextWidth(name); - this.addText(doc, docWidth - 51, 14.5, 8, "black", 46, name); - doc.setFontStyle("normal"); - str = survey.depth_reference + " = "; - if (survey.hasOwnProperty("waterlevel_value")) { - str += survey.waterlevel_value + " cm"; - } else { - str += "?"; + str = this.$gettext("Ref gauge") + ": "; + w = this.pdf.doc.getTextWidth(str); + this.pdf.doc.setFontStyle("italic"); + this.pdf.doc.text(x + padding, y + padding + 6, str); + str = this.selectedSurvey.gauge_objname; + this.pdf.doc.setFontStyle("normal"); + this.pdf.doc.text(x + padding + w, y + padding + 6, str); + } + }, + replacePlaceholders(text) { + if (text.includes("{date}")) { + text = text.replace("{date}", new Date().toLocaleString(locale2)); } - this.addText(doc, docWidth - 51 + w, 14.5, 8, "black", 46, str); + if (text.includes("{user}")) { + text = text.replace("{user}", this.user); + } + return text; + }, + getTextHeight(numberOfLines) { + return ( + numberOfLines * + ((this.pdf.doc.getFontSize() * 25.4) / parseInt(this.form.resolution)) * + this.pdf.doc.getLineHeightFactor() + ); } + }, + mounted() { + this.$store.dispatch("application/loadPdfTemplates").then(() => { + this.form.template = this.pdfTemplates[0]; + }); } }; </script>
--- a/client/src/components/Systemconfiguration.vue Tue Feb 12 23:28:01 2019 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,171 +0,0 @@ -<template> - <div class="d-flex flex-row"> - <Spacer></Spacer> - <div class="card sysconfig mt-3 shadow-xs"> - <h6 - class="mb-0 py-2 px-3 border-bottom d-flex text-info align-items-center" - > - <font-awesome-icon icon="wrench" class="mr-2"></font-awesome-icon> - <translate class="headline">Systemconfiguration</translate> - </h6> - <div class="card-body config"> - <section class="configsection"> - <h4 class="card-title"> - <translate>Bottleneck Areas stroke-color</translate> - </h4> - <compact-picker v-model="strokeColor" /> - </section> - <section> - <h4 class="card-title"> - <translate>Bottleneck Areas fill-color</translate> - </h4> - <chrome-picker v-model="fillColor" /> - </section> - <div class="sendbutton"> - <a @click.prevent="submit" class="btn btn-info text-white"> - <translate>Send</translate> - </a> - </div> - </div> - <!-- card-body --> - </div> - </div> -</template> - -<style scoped lang="scss"> -.config { - text-align: left; -} - -.configsection { - margin-bottom: $large-offset; -} - -.sendbutton { - position: absolute; - right: $offset; - bottom: $offset; -} - -.inputs { - margin-left: auto; - margin-right: auto; -} - -.sysconfig { - margin-right: $offset; - width: 100%; - height: 100%; -} -</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> - * Bernhard Reiter <bernhard@intevation.de> - */ -import { Chrome } from "vue-color"; -import { Compact } from "vue-color"; - -import { HTTP } from "@/lib/http"; -import { displayError } from "@/lib/errors.js"; -import { mapState } from "vuex"; - -export default { - name: "systemconfiguration", - data() { - return { - sent: false, - strokeColor: { r: 0, g: 0, b: 0, a: 1.0 }, - fillColor: { r: 0, g: 0, b: 0, a: 1.0 }, - currentConfig: null - }; - }, - components: { - "chrome-picker": Chrome, - "compact-picker": Compact, - Spacer: () => import("./Spacer") - }, - computed: { - ...mapState("application", ["showSidebar"]) - }, - methods: { - submit() { - HTTP.put("/system/style/Bottlenecks/stroke", this.strokeColor.rgba, { - headers: { - "X-Gemma-Auth": localStorage.getItem("token"), - "Content-type": "application/json" - } - }) - .then() - .catch(error => { - const { status, data } = error.response; - displayError({ - title: this.$gettext("Backend Error"), - message: `${status}: ${data.message || data}` - }); - }); - - HTTP.put("/system/style/Bottlenecks/fill", this.fillColor.rgba, { - headers: { - "X-Gemma-Auth": localStorage.getItem("token"), - "Content-type": "application/json" - } - }) - .then() - .catch(error => { - const { status, data } = error.response; - displayError({ - title: this.$gettext("Backend Error"), - message: `${status}: ${data.message || data}` - }); - }); - } - }, - mounted() { - HTTP.get("/system/style/Bottlenecks/stroke", { - headers: { - "X-Gemma-Auth": localStorage.getItem("token"), - "Content-type": "application/json" - } - }) - .then(response => { - this.strokeColor = response.data.colour; - }) - .catch(error => { - const { status, data } = error.response; - displayError({ - title: this.$gettext("Backend Error"), - message: `${status}: ${data.message || data}` - }); - }); - - HTTP.get("/system/style/Bottlenecks/fill", { - headers: { - "X-Gemma-Auth": localStorage.getItem("token"), - "Content-type": "application/json" - } - }) - .then(response => { - this.fillColor = response.data.colour; - }) - .catch(error => { - const { status, data } = error.response; - displayError({ - title: this.$gettext("Backend Error"), - message: `${status}: ${data.message || data}` - }); - }); - } -}; -</script>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/client/src/components/systemconfiguration/PDFTemplates.vue Wed Feb 13 08:00:26 2019 +0100 @@ -0,0 +1,142 @@ +<template> + <div class="d-flex flex-column mt-4"> + <div class="d-flex flex-row justify-content-between"> + <h5><translate>PDF-Templates</translate></h5> + <input + @change="upload" + id="uploadPDFTemplates" + ref="uploadPDFTemplates" + type="file" + style="visibility:hidden" + /> + <button + class="btn btn-sm btn-info" + @click="$refs.uploadPDFTemplates.click()" + > + <font-awesome-icon icon="spinner" class="fa-spin" v-if="uploading" /> + <font-awesome-icon icon="plus" v-else /> + </button> + </div> + <div class="d-flex mt-1"> + <table class="table table-sm"> + <thead> + <tr> + <th>Name</th> + <th>Description</th> + <th>Date</th> + <th>Country</th> + <th></th> + </tr> + </thead> + <transition-group name="list-fade" tag="tbody"> + <tr v-for="template in pdfTemplates" :key="template.name"> + <td>{{ template.name }}</td> + <td>{{ template.description }}</td> + <td>{{ template.date }}</td> + <td></td> + <td class="text-right"> + <button class="btn btn-sm btn-info mr-2"> + <font-awesome-icon icon="download" /> + </button> + <button + class="btn btn-sm btn-danger" + @click=" + showDeleteTemplatePrompt = true; + templateToDelete = template; + " + > + <font-awesome-icon icon="trash" /> + </button> + </td> + </tr> + </transition-group> + </table> + </div> + + <div + :class="[ + 'box popup ui-element rounded bg-white', + { show: showDeleteTemplatePrompt } + ]" + > + <div> + <h6 class="mb-0 py-2 px-3 border-bottom d-flex align-items-center"> + <font-awesome-icon icon="trash" class="mr-2"></font-awesome-icon> + <translate>Delete PDF Template</translate> + <font-awesome-icon + icon="times" + class="ml-auto text-muted" + @click="showDeleteTemplatePrompt = false" + ></font-awesome-icon> + </h6> + <div class="p-3 text-left"> + <translate class="text-center d-block"> + Do you really want to delete the following template: + </translate> + <h5 class="mt-3 text-center">{{ templateToDelete.name }}</h5> + </div> + <div + class="py-2 px-3 border-top d-flex align-items-center justify-content-between" + > + <button + class="btn btn-sm btn-warning" + @click="showDeleteTemplatePrompt = false" + > + no + </button> + <button class="btn btn-sm btn-info" @click="remove(templateToDelete)"> + yes + </button> + </div> + </div> + </div> + </div> +</template> + +<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): + * Markus Kottländer <markus@intevation.de> + */ +import { mapState } from "vuex"; + +export default { + name: "pdftemplates", + data() { + return { + uploading: false, + templateToDelete: "", + showDeleteTemplatePrompt: false + }; + }, + computed: { + ...mapState("application", ["pdfTemplates"]) + }, + methods: { + upload() { + this.uploading = true; + this.$store + .dispatch( + "application/uploadPDFTemplates", + this.$refs.uploadPDFTemplates.files + ) + .then(() => { + this.uploading = false; + }); + }, + remove(template) { + this.showDeleteTemplatePrompt = false; + this.$store.dispatch("application/removePDFTemplate", template); + } + } +}; +</script>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/client/src/components/systemconfiguration/Systemconfiguration.vue Wed Feb 13 08:00:26 2019 +0100 @@ -0,0 +1,159 @@ +<template> + <div class="d-flex flex-row"> + <Spacer></Spacer> + <div class="card sysconfig mt-3 shadow-xs"> + <h6 + class="mb-0 py-2 px-3 border-bottom d-flex text-info align-items-center" + > + <font-awesome-icon icon="wrench" class="mr-2"></font-awesome-icon> + <translate class="headline">Systemconfiguration</translate> + </h6> + <div class="card-body text-left"> + <h5 class="border-bottom pb-3 mb-3"> + <translate>Color settings</translate> + </h5> + <div class="d-flex"> + <div> + <h6 class="card-title"> + <translate>Bottleneck Areas fill-color</translate> + </h6> + <chrome-picker v-model="fillColor" /> + </div> + <div class="ml-4"> + <h6 class="card-title"> + <translate>Bottleneck Areas stroke-color</translate> + </h6> + <compact-picker v-model="strokeColor" /> + </div> + </div> + <div class="mt-4"> + <a @click.prevent="submit" class="btn btn-info text-white"> + <translate>Send</translate> + </a> + </div> + <!-- <PDFTemplates /> --> + </div> + <!-- card-body --> + </div> + </div> +</template> + +<style scoped lang="scss"> +.sysconfig { + margin-right: $offset; + width: 100%; + height: 100%; +} +</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> + * Bernhard Reiter <bernhard@intevation.de> + */ +import { Chrome } from "vue-color"; +import { Compact } from "vue-color"; + +import { HTTP } from "@/lib/http"; +import { displayError } from "@/lib/errors.js"; +import { mapState } from "vuex"; + +export default { + name: "systemconfiguration", + data() { + return { + sent: false, + strokeColor: { r: 0, g: 0, b: 0, a: 1.0 }, + fillColor: { r: 0, g: 0, b: 0, a: 1.0 }, + currentConfig: null + }; + }, + components: { + "chrome-picker": Chrome, + "compact-picker": Compact, + Spacer: () => import("../Spacer"), + PDFTemplates: () => import("./PDFTemplates") + }, + computed: { + ...mapState("application", ["showSidebar"]) + }, + methods: { + submit() { + HTTP.put("/system/style/Bottlenecks/stroke", this.strokeColor.rgba, { + headers: { + "X-Gemma-Auth": localStorage.getItem("token"), + "Content-type": "application/json" + } + }) + .then() + .catch(error => { + const { status, data } = error.response; + displayError({ + title: this.$gettext("Backend Error"), + message: `${status}: ${data.message || data}` + }); + }); + + HTTP.put("/system/style/Bottlenecks/fill", this.fillColor.rgba, { + headers: { + "X-Gemma-Auth": localStorage.getItem("token"), + "Content-type": "application/json" + } + }) + .then() + .catch(error => { + const { status, data } = error.response; + displayError({ + title: this.$gettext("Backend Error"), + message: `${status}: ${data.message || data}` + }); + }); + } + }, + mounted() { + HTTP.get("/system/style/Bottlenecks/stroke", { + headers: { + "X-Gemma-Auth": localStorage.getItem("token"), + "Content-type": "application/json" + } + }) + .then(response => { + this.strokeColor = response.data.colour; + }) + .catch(error => { + const { status, data } = error.response; + displayError({ + title: this.$gettext("Backend Error"), + message: `${status}: ${data.message || data}` + }); + }); + + HTTP.get("/system/style/Bottlenecks/fill", { + headers: { + "X-Gemma-Auth": localStorage.getItem("token"), + "Content-type": "application/json" + } + }) + .then(response => { + this.fillColor = response.data.colour; + }) + .catch(error => { + const { status, data } = error.response; + displayError({ + title: this.$gettext("Backend Error"), + message: `${status}: ${data.message || data}` + }); + }); + } +}; +</script>
--- a/client/src/main.js Tue Feb 12 23:28:01 2019 +0100 +++ b/client/src/main.js Wed Feb 13 08:00:26 2019 +0100 @@ -43,6 +43,7 @@ faClock, faCloudUploadAlt, faCopy, + faDownload, faDrawPolygon, faExclamationTriangle, faEye, @@ -96,6 +97,7 @@ faClock, faCloudUploadAlt, faCopy, + faDownload, faDrawPolygon, faExclamationTriangle, faEye,
--- a/client/src/router.js Tue Feb 12 23:28:01 2019 +0100 +++ b/client/src/router.js Wed Feb 13 08:00:26 2019 +0100 @@ -65,7 +65,8 @@ { path: "/systemconfiguration", name: "systemconfiguration", - component: () => import("./components/Systemconfiguration.vue"), + component: () => + import("./components/systemconfiguration/Systemconfiguration.vue"), meta: { requiresAuth: true },
--- a/client/src/store/application.js Tue Feb 12 23:28:01 2019 +0100 +++ b/client/src/store/application.js Wed Feb 13 08:00:26 2019 +0100 @@ -15,6 +15,7 @@ */ import { version } from "../../package.json"; +// import { HTTP } from "../lib/http"; // initial state const init = () => { @@ -22,6 +23,7 @@ appTitle: process.env.VUE_APP_TITLE, secondaryLogo: process.env.VUE_APP_SECONDARY_LOGO_URL, logoForPDF: process.env.VUE_APP_LOGO_FOR_PDF_URL, + pdfTemplates: [], showSidebar: false, showUsermenu: false, showSplitscreen: false, @@ -103,6 +105,153 @@ }, searchQuery: (state, searchQuery) => { state.searchQuery = searchQuery; + }, + pdfTemplates: (state, pdfTemplates) => { + state.pdfTemplates = pdfTemplates; + } + }, + actions: { + loadPdfTemplates({ commit }) { + return new Promise(resolve => { + // pretend we do something async + setTimeout(function() { + commit("pdfTemplates", [ + { + name: "Default Template", + properties: { + format: "landscape", + resolution: "120", + paperSize: "a4" + }, + elements: [ + { + type: "textbox", + position: "bottomleft", + offset: { x: 2, y: 2 }, + width: 100, + fontSize: 8, + text: "Date of publication: {date} - generated by: {user}" + }, + { + type: "bottleneck", + position: "topleft", + offset: { x: 2, y: 2 } + }, + { + type: "legend", + position: "topleft", + offset: { x: 2, y: 18 } + }, + { + type: "scalebar", + position: "bottomright", + offset: { x: 2, y: 2 } + }, + { + type: "northarrow", + position: "topright", + offset: { x: 10, y: 5 }, + size: 2 + } + ] + }, + { + name: "Full Template", + properties: { + format: "landscape", + resolution: "120", + paperSize: "a4" + }, + elements: [ + { + type: "box", + position: "topleft", + offset: { x: 20, y: 2 }, + width: 60, + height: 25, + color: "green" + }, + { + type: "text", + position: "topleft", + offset: { x: 23, y: 5 }, + width: 56, + fontSize: 10, + color: "greenyellow", + text: + "Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua." + }, + { + type: "textbox", + position: "bottomleft", + offset: { x: 2, y: 2 }, + width: 90, + padding: 3, + fontSize: 7, + color: "gray", + background: "white", + text: "Date of publication: {date} - generated by: {user}" + }, + { + type: "image", + format: "PNG", + position: "bottomleft", + offset: { x: 2, y: 10 }, + width: 90, + height: 12, + border: 2 + }, + { + type: "bottleneck", + position: "topright", + offset: { x: 2, y: 2 } + }, + { + type: "legend", + position: "topright", + offset: { x: 2, y: 18 } + }, + { + type: "scalebar", + position: "bottomright", + offset: { x: 2, y: 2 } + }, + { + type: "northarrow", + position: "topleft", + offset: { x: 8, y: 4 }, + size: 2 + } + ] + } + ]); + resolve(); + }, 500); + }); + }, + uploadPDFTemplates({ state, commit }, files) { + return new Promise((resolve, reject) => { + setTimeout(() => { + const reader = new FileReader(); + reader.onload = event => { + let templates = state.pdfTemplates; + templates.push(JSON.parse(event.target.result)); + commit("pdfTemplates", templates); + // TODO: send template to server + }; + reader.onerror = error => reject(error); + reader.readAsText(files[0]); + resolve(); + }, 1000); + }); + }, + removePDFTemplate({ state, commit }, template) { + let templates = state.pdfTemplates; + let removeIndex = templates.findIndex(t => t.name === template.name); + if (removeIndex !== -1) { + templates.splice(removeIndex, 1); + commit("pdfTemplates", templates); + } } } };