view client/src/components/Pdftool.vue @ 2205:59c21dee6315 pdf-export

pdf-gen: improve pdftool to use value from template * change download function to check the commig values for template * old coordinates are still used , improve it to get them from template is still needed #: src/components/Logs.vue:25 msgid "Accesslog" msgstr "Zugriffs-Protokoll"
author Fadi Abbud <fadi.abbud@intevation.de>
date Tue, 05 Feb 2019 16:33:01 +0100
parents af136b1a854e
children da902f0dac88
line wrap: on
line source

<template>
  <div
    :class="[
      'box ui-element rounded bg-white text-nowrap',
      { expanded: showPdfTool }
    ]"
  >
    <div style="width: 20rem">
      <h6 class="mb-0 py-2 px-3 border-bottom d-flex align-items-center">
        <font-awesome-icon icon="file-pdf" class="mr-2"></font-awesome-icon>
        <translate>Generate PDF</translate>
        <font-awesome-icon
          icon="times"
          class="ml-auto text-muted"
          @click="$store.commit('application/showPdfTool', false)"
        ></font-awesome-icon>
      </h6>
      <div class="p-3 text-left">
        <select
          @change="applyTemplateToForm"
          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"
            :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"
        >
          <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">
            <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>
            </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>
            </select>
          </div>
        </div>
        <!--
        <small class="d-block my-2">
          <input
            type="radio"
            id="pdfexport-downloadtype-download"
            value="download"
            v-model="form.downloadType"
            selected
          />
          <label for="pdfexport-downloadtype-download" class="ml-1 mr-2">
            <translate>Download</translate>
          </label>
          <input
            type="radio"
            id="pdfexport-downloadtype-open"
            value="open"
            v-model="form.downloadType"
          />
          <label for="pdfexport-downloadtype-open" class="ml-1">
            <translate>Open in new window</translate>
          </label>
        </small>
        -->
        <button
          @click="download"
          type="button"
          :disabled="!readyToGenerate"
          class="btn btn-sm btn-info d-block w-100 mt-2"
        >
          <translate>Generate PDF</translate>
        </button>
      </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, 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 { mapGetters, mapState } from "vuex";
import jsPDF from "jspdf";
import { getPointResolution } from "ol/proj.js";
import locale2 from "locale2";

var paperSizes = {
  // in millimeter, landscape [width, height]
  a3: [420, 297],
  a4: [297, 210]
};

export default {
  name: "pdftool",
  data() {
    return {
      form: {
        template: null,
        format: "landscape",
        paperSize: "a4",
        downloadType: "download",
        resolution: "120"
      },
      templates: [
        {
          name: "Template 1",
          properties: {
            format: "landscape",
            resolution: "80",
            paperSize: "a4"
          },
          elements: [
            {
              type: "docinfo",
              x_coordinate: 0,
              y_coordinate: 0,
              elementWidth: 118,
              elementHeight: 8
            },
            {
              type: "image",
              imageType: "PNG",
              imageUrl: "",
              x_coordinate: 30,
              y_coordinate: 297,
              imageWidth: 50,
              imageHeight: 23
            },
            {
              type: "scalebar",
              //x_coordinate:,
              y_coordinate: 204
              //elementsize: 50
            },
            {
              type: "textbox",
              x_coordinate: 50,
              y_coordinate: 190,
              elementSize: 8,
              text: "textfrom template",
              color: "black"
            },
            {
              type: "aboutbox"
              //x_coordinate: 0,
              //y_coordinate: 210 - 20
            }
          ]
        },
        {
          name: "Template 2",
          properties: {
            format: "portrait",
            resolution: "120",
            paperSize: "a3"
          },
          elements: [
            {
              type: "image",
              imageType: "PNG",
              imageUrl: "",
              x_coordinate: 30,
              y_coordinate: 297,
              imageWidth: 50,
              imageHeight: 23
            },
            {
              type: "scalebar",
              //x_coordinate: 250,
              y_coordinate: 420 - 6
              //elementsize: 50
            },
            {
              type: "textbox",
              x_coordinate: 50,
              y_coordinate: 50,
              elementSize: 22,
              text: "from template",
              color: "black"
            },
            {
              type: "docinfo",
              x_coordinate: 0,
              y_coordinate: 0,
              elementWidth: 118,
              elementHeight: 8
            },
            {
              type: "aboutbox"
              //x_coordinate: 0,
              //y_coordinate: 210 - 20
            }
          ]
        }
      ],
      logoImageForPDF: null, // a HTMLImageElement instance
      readyToGenerate: true // if the user is allowed to press the button
    };
  },
  computed: {
    ...mapState("application", ["showPdfTool", "logoForPDF"]),
    ...mapState("bottlenecks", ["selectedSurvey"]),
    ...mapState("map", ["openLayersMap", "isolinesLegendImgDataURL"]),
    ...mapGetters("map", ["getLayerByName"]),
    ...mapState("user", ["user"])
  },
  methods: {
    // 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 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.templates.find(t => t.name === this.form.template);
      // disable button while working on it
      this.readyToGenerate = false;

      console.log(
        "will generate pdf with",
        this.form.paperSize,
        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];
      } else {
        // switch width and height
        width = paperSizes[this.form.paperSize][1];
        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(width * pixelsPerMapMillimeter),
        Math.round(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;
      var 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.
      var mapExtent = map.getView().calculateExtent(mapSize);

      var pdf = new jsPDF(this.form.format, "mm", this.form.paperSize);
      var northarrowSize = 3;
      var self = this;

      // set a callback for after the next complete rendering of the map
      map.once("rendercomplete", function(event) {
        let canvas = event.context.canvas;

        // because we are using Web Mercator, a pixel represents
        // a differently sizes 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 scaleNominator = Math.round(
          // the x in 1:x map scale
          1000 * pixelsPerMapMillimeter * metersPerPixel
        );
        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);

        if (self.getLayerByName("Bottleneck isolines").isVisible) {
          self.addLegend(pdf, width, height);
        }
        if (template) {
          template.elements.forEach(t => {
            switch (t.type) {
              case "image": {
                console.log("autobox from template");
                if (t.imageUrl.length > 0) {
                  pdf.addImage(
                    t.imageUrl,
                    t.imageType,
                    t.x_coordinate,
                    t.y_coordinate,
                    t.imageWidth,
                    t.imageHeight
                  );
                }
                break;
              }
              case "scalebar": {
                self.addScaleBar(
                  pdf,
                  width,
                  height,
                  scaleNominator,
                  t.y_coordinate
                );
                break;
              }
              case "textbox": {
                self.addText(
                  pdf,
                  t.x_coordinate,
                  t.y_coordinate,
                  t.elementSize,
                  t.color,
                  100,
                  t.text
                );
                break;
              }
              case "docinfo": {
                self.addPageInfo(pdf);
                break;
              }
              case "aboutbox": {
                self.addAboutBox(pdf, width, height);
                break;
              }
              /*  case "docinfo": {
                self.addAboutBox(
                  pdf,
                  t.x_coordinate,
                  t.y_coordinate,
                  t.elementWidth,
                  t.elementHeight
                );
              } */
            }
          });

          pdf.save("map.pdf");
        } else {
          self.addScaleBar(pdf, width, height, scaleNominator, 204);
          self.addPageInfo(pdf);
          self.addAboutBox(pdf, width, height);
          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;
      });

      // 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);
      */
      });
    },
    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) {
      // 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");
    },
    addScaleBar(doc, docWidth, docHeight, scaleNominator, y) {
      // scaleNominator is the x in 1:x of the map scale

      // hardcode maximal width for now and place in lower right corner
      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 * scaleNominator;

      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) / scaleNominator / 4;

      let x = docWidth - (size * 4 + 8);
      //let y = docHeight - 6;

      this.addRoundedBox(doc, x - 4, y - 4, size * 4 + 12, 10);

      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");
      // /4 and could give 2.5. We still round, because of floating point arith
      doc.text(
        x + size,
        y + 3,
        (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);
    },

    addNorthArrow(doc, x1, y1, 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");
    },
    // 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);

      let logoImage = this.logoImageForPDF;
      let aspectRatio = logoImage.width / logoImage.height;
      doc.addImage(logoImage, "PNG", 5, docHeight - 19, 110, 110 / aspectRatio);

      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);
    },
    addLegend(doc, 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;

      this.addRoundedBox(doc, docWidth - 54, 0, 54, 50 / aspectRatio + 4);
      doc.addImage(legendImage, docWidth - 52, 2, 50, 50 / aspectRatio);
    }
  }
};
</script>