view client/src/components/Pdftool.vue @ 1887:3ed036adc80f dev-pdf-generation

client: pdf-gen: fix scale calculation * Change scale calculation to use ol.proj.getPointResolution() at the center of the current view to make up for different point dimensions if you are not on the equator for the web mercator projection. This is also how the scaleline control of OpenLayers does it.
author Bernhard Reiter <bernhard@intevation.de>
date Thu, 17 Jan 2019 21:48:11 +0100
parents 20fe31b4dd5d
children c78efb1ddb02
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">
        <b><translate>Choose format:</translate></b>
        <select v-model="form.format" class="form-control d-block w-100">
          <option value="landscape"><translate>landscape</translate></option>
          <option value="portrait"><translate>portrait</translate></option>
        </select>
        <select v-model="form.resolution" class="form-control d-block w-100">
          <option value="80">80 dpi</option>
          <option value="120">120 dpi</option>
          <option value="200">200 dpi</option>
        </select>
        <select v-model="form.paperSize" class="form-control d-block w-100">
          <option value="a3"><translate>ISO A3</translate></option>
          <option value="a4"><translate>ISO A4</translate></option>
        </select>
        <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"
          class="btn btn-sm btn-info d-block w-100"
        >
          <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 { mapState } from "vuex";
import jsPDF from "jspdf";
import { getPointResolution } from "ol/proj.js";

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

export default {
  name: "pdftool",
  data() {
    return {
      form: {
        format: "landscape",
        paperSize: "a4",
        downloadType: "download",
        resolution: "120"
      }
    };
  },
  computed: {
    ...mapState("map", ["openLayersMap"]),
    ...mapState("application", ["showPdfTool"]),
    ...mapState("bottlenecks", ["selectedSurvey"])
  },
  methods: {
    download() {
      // FUTURE: disable button while working on it
      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
      console.log(width, height);

      // 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 scalebarSize =
        this.form.format === "portrait" && this.form.paperSize === "a4"
          ? 10
          : 15;
      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();
        console.log("metersPerPixel = ", metersPerPixel);

        var data = canvas.toDataURL("image/jpeg");
        pdf.addImage(data, "JPEG", 0, 0, width, height);
        self.addRoundedBox(
          pdf,
          width - scalebarSize * 5.5,
          height - scalebarSize,
          scalebarSize * 5,
          scalebarSize
        );
        self.addScalebar(
          pdf,
          width - scalebarSize * 5,
          height - scalebarSize / 2,
          scalebarSize,
          scalebarSize * pixelsPerMapMillimeter * metersPerPixel
        );
        self.addText(
          pdf,
          width - scalebarSize * 5,
          height - scalebarSize * 0.6,
          10,
          "black",
          50,
          "Scale 1:" +
            Math.round(1000 * pixelsPerMapMillimeter * metersPerPixel)
        );
        //self.addText(pdf, 150, 20, 10, "black", 70, "some text");
        self.addNorthArrow(pdf, 15, 8, northarrowSize);
        pdf.save("map.pdf");
        // reset to original size
        map.setSize(mapSize);
        map.getView().fit(mapExtent, { size: mapSize });

        // FUTURE: re-enable button when done
      });

      // trigger rendering
      map.setSize(mapSizeForPrint);
      map.getView().fit(mapExtent, { size: mapSizeForPrint });

      // TODO: replace this src with an API reponse after actually generating PDFs
      /*
      let src =
        this.form.format === "landscape"
          ? "/img/PrintTemplate-Var2-Landscape.pdf"
          : "/img/PrintTemplate-Var2-Portrait.pdf";

      let a = document.createElement("a");
      a.href = src;

      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);
      */
    },
    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, x, y, size, realLength) {
      // realLength as number in meters (reality)
      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");
      doc.text(x + size, y + 3, Math.round(realLength).toString());
      doc.text(x + size * 2, y + 3, Math.round(realLength * 2).toString());
      doc.text(
        x + size * 4,
        y + 3,
        Math.round(realLength * 4).toString() + " m"
      );
    },

    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);
      // get the longest line to fit the white backround to it
      var longestString = "";
      textLines.forEach(function(element) {
        if (element.length > longestString.length) longestString = element;
      });
      var indexOfMaxString = textLines.indexOf(longestString);
      // white background (rectangular) around the text
      doc.setFillColor(255, 255, 255);
      doc.setDrawColor(255, 255, 255);
      doc.rect(
        postitionX - doc.getStringUnitWidth(textLines[indexOfMaxString]) / size,
        size > 10 ? positionY - size / 1.8 : positionY - size / 2.4,
        doc.getStringUnitWidth(textLines[indexOfMaxString]) * (size / 2.6),
        textLines.length * (size / 2),
        "FD"
      );
      //rounded rectangular
      /* doc.roundedRect(
        postitionX - doc.getStringUnitWidth(textLines[indexOfMaxString]) / size,
        size > 10 ? positionY - size / 1.8 : positionY - size / 2.6,
        doc.getStringUnitWidth(textLines[indexOfMaxString]) * (size / 2.6),
        textLines.length * (size / 2),
        3,
        3,
        "FD"
      ); */
      doc.setTextColor(color);
      doc.setFontSize(size);
      doc.text(postitionX, positionY, textLines);
    }
  }
};
</script>