view client/src/components/Pdftool.vue @ 2539:ad2a1d02ad1c

client:pdf-gen: improve north arrow drawing
author Fadi Abbud <fadi.abbud@intevation.de>
date Thu, 07 Mar 2019 14:59:03 +0100
parents bb5286acfee2
children 4d5f419a2318
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="Generate PDF"
        :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"
          >
            <translate>{{ template.name }}</translate>
          </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>

<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 "@/lib/font-linbiolinum.js";
import { getPointResolution } from "ol/proj.js";
import locale2 from "locale2";
import { HTTP } from "../lib/http";
import { displayError } from "@/lib/errors.js";
import { LAYERS } from "@/store/map.js";

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: "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", ["openLayersMap", "isolinesLegendImgDataURL"]),
    ...mapGetters("map", ["getLayerByName"]),
    ...mapState("user", ["user"]),
    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/print/" + 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 scaleNominator = Math.round(
          // the x in 1:x map scale
          1000 * pixelsPerMapMillimeter * metersPerPixel
        );
        console.log("scaleNominator = ", scaleNominator);

        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 };
          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,
                  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(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;
    },
    addRoundedBox(x, y, w, h, color, rounding) {
      // draws a rounded background box at (x,y) width x height
      // using jsPDF units
      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, 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);
    },
    // add some text at specific coordinates with a background box
    addTextBox(
      position,
      offset,
      width,
      height,
      rounding,
      padding,
      fontSize,
      color,
      background,
      text
    ) {
      this.pdf.doc.setFontSize(fontSize);
      text = this.replacePlaceholders(text);

      if (!width) {
        width = this.pdf.doc.getTextWidth(text) + 2 * padding;
      }
      let textWidth = width - 2 * padding;
      if (!height) {
        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,
        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
      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 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);

      // 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) {
      if (
        this.selectedBottleneck &&
        this.selectedSurvey &&
        this.getLayerByName(LAYERS.BOTTLENECKISOLINE).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;

        // 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);
        this.pdf.doc.addImage(
          legendImage,
          x + padding,
          y + padding,
          width - 2 * padding,
          height - 2 * padding
        );
      }
    },
    addBottleneckInfo(position, offset, rounding, color) {
      if (
        this.selectedBottleneck &&
        this.selectedSurvey &&
        this.getLayerByName(LAYERS.BOTTLENECKISOLINE).isVisible
      ) {
        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);

        // 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
        );
      }
    },
    replacePlaceholders(text) {
      if (text.includes("{date}")) {
        text = text.replace("{date}", new Date().toLocaleString(locale2));
      }
      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.form.template = this.templates[0];
    this.templateData = this.form.template;
    HTTP.get("/templates/print", {
      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>