view client/src/components/Pdftool.vue @ 3960:4268bb8e2839

clinet: pdf-gen: fix addText function in pdftool * set default width value to avoid stop rendering of elements if its missed in the template-definition
author Fadi Abbud <fadi.abbud@intevation.de>
date Tue, 16 Jul 2019 16:54:55 +0200
parents 7df9ef183985
children 7316b7e8246f
line wrap: on
line source

<template>
  <div
    :class="[
      'box ui-element rounded bg-white text-nowrap',
      { expanded: showPdfTool }
    ]"
  >
    <div style="width: 18rem">
      <UIBoxHeader
        icon="file-pdf"
        :title="generatePdfLable"
        :closeCallback="close"
      />
      <div class="box-body">
        <select
          @change="applyTemplateToForm"
          v-model="form.template"
          class="form-control d-block mb-2 w-100 font-weight-bold"
        >
          <option
            v-for="template in templates"
            :value="template"
            :key="template.name"
          >
            {{ template.name }}
          </option>
        </select>
        <select
          v-model="form.format"
          class="form-control form-control-sm d-block mb-2 w-100"
        >
          <option value="landscape"><translate>landscape</translate></option>
          <option value="portrait"><translate>portrait</translate></option>
        </select>
        <div class="d-flex">
          <div class="flex-fill mr-2">
            <select
              v-model="form.resolution"
              class="form-control form-control-sm mb-2 d-block w-100"
            >
              <option value="80"><translate>80 dpi</translate></option>
              <option value="120"><translate>120 dpi</translate></option>
              <option value="200"><translate>200 dpi</translate></option>
            </select>
          </div>
          <div class="flex-fill ml-2">
            <select
              v-model="form.paperSize"
              class="form-control form-control-sm mb-2 d-block w-100"
            >
              <option value="a4"><translate>A4</translate></option>
              <option value="a3"><translate>A3</translate></option>
            </select>
          </div>
        </div>
        <button
          @click="download"
          :key="'downloadBtn'"
          type="button"
          v-if="readyToGenerate"
          class="btn btn-sm btn-info d-block w-100 mt-2"
        >
          <translate>Generate PDF</translate>
        </button>
        <button
          @click="cancel"
          :key="'cancelBtn'"
          type="button"
          v-else
          class="btn btn-sm btn-danger d-block w-100 mt-2"
        >
          <font-awesome-icon class="mr-1" icon="spinner" spin />
          <translate>Cancel</translate>
        </button>
      </div>
    </div>
  </div>
</template>

<style lang="scss" scoped>
input,
select {
  font-size: 0.8em;
}
</style>

<script>
/* This is Free Software under GNU Affero General Public License v >= 3.0
 * without warranty, see README.md and license for details.
 *
 * SPDX-License-Identifier: AGPL-3.0-or-later
 * License-Filename: LICENSES/AGPL-3.0.txt
 *
 * Copyright (C) 2018, 2019 by via donau
 *   – Österreichische Wasserstraßen-Gesellschaft mbH
 * Software engineering by Intevation GmbH
 *
 * Author(s):
 * * Markus Kottländer <markus.kottlaender@intevation.de>
 * * Bernhard E. Reiter <bernhard@intevation.de>
 * * Fadi Abbud <fadi.abbud@intevation.de>
 */
import { mapState, mapGetters } from "vuex";
import jsPDF from "jspdf-yworks";
import "@/lib/font-linbiolinum";
import { getPointResolution } from "ol/proj";
import { HTTP } from "@/lib/http";
import { displayError } from "@/lib/errors";
import { pdfgen, templateLoader } from "@/lib/mixins";

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

const DEFAULT_TEMPLATE = "Default";

export default {
  mixins: [pdfgen, templateLoader],
  name: "pdftool",
  data() {
    return {
      form: {
        template: null,
        format: "landscape",
        paperSize: "a4",
        downloadType: "download",
        resolution: "80"
      },
      templates: [
        {
          name: DEFAULT_TEMPLATE,
          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: "topleft",
              offset: { x: 6, y: 4 },
              size: 2
            }
          ]
        }
      ],
      templateData: null,
      pdf: {
        doc: null,
        width: null,
        height: null
      },
      logoImageForPDF: null, // a HTMLImageElement instance
      readyToGenerate: true, // if the user is allowed to press the button
      rendercompleteListener: null,
      mapSize: null,
      mapExtent: null
    };
  },
  computed: {
    ...mapState("application", ["showPdfTool", "logoForPDF"]),
    ...mapState("bottlenecks", ["selectedBottleneck", "selectedSurvey"]),
    ...mapState("map", ["isolinesLegendImgDataURL"]),
    ...mapGetters("map", ["openLayersMap"]),
    generatePdfLable() {
      return this.$gettext("Generate PDF");
    },
    filename() {
      let date = new Date()
        .toISOString()
        .slice(0, 10)
        .replace(/-/g, "");
      let filename = "map";

      if (this.selectedBottleneck) {
        filename = this.selectedBottleneck;
        if (this.selectedSurvey) {
          filename += "-sr" + this.selectedSurvey.date_info.replace(/-/g, "");
        }
      }

      filename =
        filename
          .replace(/[^\w-]/gi, "") // remove everything but wordchars and dash
          .toLowerCase() +
        "-exported" +
        date +
        ".pdf";

      return filename;
    }
  },
  methods: {
    close() {
      this.$store.commit("application/showPdfTool", false);
    },
    millimeter2pixels(length, dpi) {
      return (dpi * length) / 25.4;
    },
    pixel2millimeter(pixels, dpi) {
      return (pixels * 25.4) / dpi;
    },
    // When a template is chosen from the dropdown, its propoerties are
    // applied to the rest of the form.
    applyTemplateToForm() {
      if (this.form.template && this.form.template.name !== DEFAULT_TEMPLATE) {
        this.loadTemplates(
          `/templates/${this.form.template.type}/${this.form.template.name}`
        )
          .then(response => {
            this.prepareImages(response.data.template_data.elements).then(
              values => {
                values.forEach(v => {
                  response.data.template_data.elements[v.index].url = v.url;
                });
                this.setTemplate(response.data.template_data);
              }
            );
          })
          .catch(e => {
            const { status, data } = e.response;
            displayError({
              title: this.$gettext("Backend Error"),
              message: `${status}: ${data.message || data}`
            });
          });
      } else {
        this.setTemplate(this.templates[0]);
      }
    },
    setTemplate(template) {
      this.templateData = template;
      this.form.format = this.templateData.properties.format;
      this.form.paperSize = this.templateData.properties.paperSize;
      this.form.resolution = this.templateData.properties.resolution;
    },
    download() {
      // disable button while working on it
      this.readyToGenerate = false;

      console.log(
        "will generate pdf with",
        this.form.paperSize,
        this.form.format,
        this.form.resolution
      );

      if (this.form.format !== "portrait") {
        // landscape, default
        this.pdf.width = paperSizes[this.form.paperSize][0];
        this.pdf.height = paperSizes[this.form.paperSize][1];
      } else {
        // switch width and height
        this.pdf.width = paperSizes[this.form.paperSize][1];
        this.pdf.height = paperSizes[this.form.paperSize][0];
      }

      // FUTURE: consider margins

      // dots per mm = dots per inch / (25.4 mm/inch)
      var pixelsPerMapMillimeter = this.form.resolution / 25.4;
      var mapSizeForPrint = [
        // in pixel
        Math.round(this.pdf.width * pixelsPerMapMillimeter),
        Math.round(this.pdf.height * pixelsPerMapMillimeter)
      ];

      // generate PDF and open it
      // our units are milimeters; width 0 x height 0 is left upper corner

      // Step 1 prepare and save current map extend
      // Then add callback "rendercomplete" for Step 3
      //    which will generate the pdf and resets the map view
      // Step 2 which starts rendering a map with the necessary image size

      var map = this.openLayersMap();
      this.mapSize = map.getSize(); // size in pixels of the map in the DOM
      // Calculate the extent for the current view state and the passed size.
      // The size is the pixel dimensions of the box into which the calculated
      // extent should fit.
      this.mapExtent = map.getView().calculateExtent(this.mapSize);

      this.pdf.doc = new jsPDF(this.form.format, "mm", this.form.paperSize);
      // set a callback for after the next complete rendering of the map
      this.rendercompleteListener = map.once("rendercomplete", event => {
        let canvas = event.context.canvas;

        // because we are using Web Mercator, a pixel represents
        // a differently sized spot depending on the place of the map.
        // So we use a value calculated from the center of the current view.
        let view = map.getView();
        let proj = view.getProjection();
        let metersPerPixel = // average meters (reality) per pixel (map)
          getPointResolution(proj, view.getResolution(), view.getCenter()) *
          proj.getMetersPerUnit();
        // DEBUG console.log("metersPerPixel = ", metersPerPixel);

        let scaleDenominator = Math.round(
          // the x in 1:x map scale
          1000 * pixelsPerMapMillimeter * metersPerPixel
        );
        console.log("scaleDenominator = ", scaleDenominator);
        const width80DPI = this.millimeter2pixels(this.pdf.width, 80);
        const height80DPI = this.millimeter2pixels(this.pdf.height, 80);
        const currentHeight = this.millimeter2pixels(
          this.pdf.height,
          this.form.resolution
        );
        const currentWidth = this.millimeter2pixels(
          this.pdf.width,
          this.form.resolution
        );
        //const scaleFactor = this.form.resolution / 80;
        var snapshot = canvas.toDataURL("image/jpeg");
        const offscreen = document.createElement("canvas");
        offscreen.width = currentWidth;
        offscreen.height = currentHeight;
        const image = new Image();
        image.src = snapshot;
        image.onload = () => {
          offscreen
            .getContext("2d")
            .drawImage(
              image,
              (currentWidth - width80DPI) / 2,
              (currentHeight - height80DPI) / 2,
              width80DPI,
              height80DPI,
              0,
              0,
              currentWidth,
              currentHeight
            );
          const data = offscreen.toDataURL("image/jpeg");
          this.pdf.doc.addImage(
            data,
            "JPEG",
            0,
            0,
            this.pdf.width,
            this.pdf.height
          );

          if (this.templateData) {
            this.pdf.doc.setFont("linbiolinum", "normal");
            let defaultFontSize = 11,
              defaultRounding = 2,
              defaultTextColor = "black",
              defaultBgColor = "white",
              defaultPadding = 3,
              defaultOffset = { x: 0, y: 0 },
              defaultBorderColor = "white",
              defaultWidth = 100;
            this.templateData.elements.forEach(e => {
              switch (e.type) {
                case "text": {
                  this.addText(
                    e.position,
                    e.offset || defaultOffset,
                    e.width || defaultWidth,
                    e.fontSize || defaultFontSize,
                    e.color || defaultTextColor,
                    e.text
                  );
                  break;
                }
                case "box": {
                  this.addBox(
                    e.position,
                    e.offset || defaultOffset,
                    e.width,
                    e.height,
                    // handling the case when the rectangle  not rounded (rounding = 0)
                    e.rounding === 0 || e.rounding
                      ? e.rounding
                      : defaultRounding,
                    e.color || defaultBgColor,
                    e.brcolor || defaultBorderColor
                  );
                  break;
                }
                case "textbox": {
                  this.addTextBox(
                    e.position,
                    e.offset || defaultOffset,
                    e.width,
                    e.height,
                    e.rounding === 0 || e.rounding
                      ? e.rounding
                      : defaultRounding,
                    e.padding || defaultPadding,
                    e.fontSize || defaultFontSize,
                    e.color || defaultTextColor,
                    e.background || defaultBgColor,
                    e.text,
                    e.brcolor || defaultBorderColor
                  );
                  break;
                }
                case "image": {
                  this.addImage(
                    e.url,
                    e.format,
                    e.position,
                    e.offset || defaultOffset,
                    e.width,
                    e.height
                  );
                  break;
                }
                case "bottleneck": {
                  this.addBottleneckInfo(
                    e.position,
                    e.offset || defaultOffset,
                    e.rounding === 0 || e.rounding
                      ? e.rounding
                      : defaultRounding,
                    e.color || defaultTextColor,
                    e.brcolor || defaultBorderColor
                  );
                  break;
                }
                case "legend": {
                  this.addLegend(
                    e.position,
                    e.offset || defaultOffset,
                    e.rounding === 0 || e.rounding
                      ? e.rounding
                      : defaultRounding,
                    e.brcolor || defaultBorderColor
                  );
                  break;
                }
                case "scalebar": {
                  this.addScaleBar(
                    scaleDenominator,
                    e.position,
                    e.offset || defaultOffset,
                    e.rounding === 0 || e.rounding
                      ? e.rounding
                      : defaultRounding,
                    e.brcolor || defaultBorderColor
                  );
                  break;
                }
                case "scale": {
                  this.addScale(
                    scaleDenominator,
                    e.position,
                    e.width,
                    e.offset || defaultOffset,
                    e.fontSize || defaultFontSize,
                    e.color || defaultTextColor
                  );
                  break;
                }
                case "northarrow": {
                  this.addNorthArrow(
                    e.position,
                    e.offset || defaultOffset,
                    e.size
                  );
                  break;
                }
              }
            });

            this.pdf.doc.save(this.filename);
          }

          // reset to original size
          map.setSize(this.mapSize);
          map.getView().fit(this.mapExtent, {
            size: this.mapSize,
            // necessary to get to the previous zoom level in all cases
            // details see https://github.com/openlayers/openlayers/issues/9235
            constrainResolution: false
          });

          // as we are done: re-enable button
          this.readyToGenerate = true;
        };
      });

      // trigger rendering
      map.setSize(mapSizeForPrint);
      map.getView().fit(this.mapExtent, { size: mapSizeForPrint });
    },
    cancel() {
      this.openLayersMap().un(
        this.rendercompleteListener.type,
        this.rendercompleteListener.listener
      );
      this.openLayersMap().setSize(this.mapSize);
      this.openLayersMap()
        .getView()
        .fit(this.mapExtent, { size: this.mapSize });
      this.readyToGenerate = true;
    },
    // add the used map scale and papersize
    addScale(scaleDenominator, position, width, offset, fontSize, color) {
      //TODO: check the correctence of the scalnominator value here.
      let str =
        this.$gettext("Scale") +
        " 1 : " +
        scaleDenominator +
        " " +
        "(DIN" +
        " " +
        this.form.paperSize.toUpperCase() +
        ")";
      this.addText(position, offset, width, fontSize, color, str);
    },
    addScaleBar(scaleDenominator, position, offset, rounding, brcolor) {
      // scaleDenominator is the x in 1:x of the map scale

      // hardcode maximal width for now
      let maxWidth = 80; // in mm

      // reduce width until we'll find a nice number for printing
      // strategy:
      //           1. check which unit prefix we shall use to get [10:10000[
      //           2. using a mapping for the leading digit to get [1:10[
      //           3. select a smaller number which is nicely dividable
      //           4. scale up again to get length in paper mm and to be shown

      // from https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/log10#Polyfill
      let log10 =
        Math.log10 || // more precise, but unsupported by IE
        function(x) {
          return Math.log(x) * Math.LOG10E;
        };

      let maxLength = maxWidth * scaleDenominator;

      let unit = "mm";
      let unitConversionFactor = 1;
      if (maxLength >= 1e7) {
        // >= 10 km
        unit = "km";
        unitConversionFactor = 1e6;
      } else if (maxLength >= 1e4) {
        // >= 10 m
        unit = "m";
        unitConversionFactor = 1e3;
      }

      maxLength /= unitConversionFactor;

      // DEBUG console.log(maxLength, unit);
      let unroundedLength = maxLength;
      let numberOfDigits = Math.floor(log10(unroundedLength));
      let factor = Math.pow(10, numberOfDigits);
      let mapped = unroundedLength / factor;
      // DEBUG console.log(mapped);

      var length = Math.floor(maxLength); // just to have an upper limit

      // manually only use numbers that are very nice to devide by 4
      // note that this is taken into account for rounding later
      if (mapped > 8) {
        length = 8 * factor;
      } else if (mapped > 4) {
        length = 4 * factor;
      } else if (mapped > 2) {
        length = 2 * factor;
      } else {
        length = factor;
      }

      let size = (length * unitConversionFactor) / scaleDenominator / 4;
      let fullSize = size * 4;

      // x/y defaults to offset for topleft corner (normal x/y coordinates)
      let x = offset.x;
      let y = offset.y;

      // if position is on the right, x needs to be calculate with pdf width and
      // the size of the element
      if (["topright", "bottomright"].indexOf(position) !== -1) {
        x = this.pdf.width - offset.x - fullSize - 8;
      }
      if (["bottomright", "bottomleft"].indexOf(position) !== -1) {
        y = this.pdf.height - offset.y - 10;
      }

      // to give the outer white box 4mm padding
      let scaleBarX = x + 4;
      let scaleBarY = y + 5; // 5 because above the scalebar will be the numbers

      // draw outer white box
      this.addRoundedBox(x, y, fullSize + 8, 10, "white", rounding, brcolor);

      // draw first part of scalebar
      this.pdf.doc.setDrawColor(0, 0, 0);
      this.pdf.doc.setFillColor(0, 0, 0);
      this.pdf.doc.rect(scaleBarX, scaleBarY, size, 1, "FD");

      // draw second part of scalebar
      this.pdf.doc.setDrawColor(0, 0, 0);
      this.pdf.doc.setFillColor(255, 255, 255);
      this.pdf.doc.rect(scaleBarX + size, scaleBarY, size, 1, "FD");

      // draw third part of scalebar
      this.pdf.doc.setDrawColor(0, 0, 0);
      this.pdf.doc.setFillColor(0, 0, 0);
      this.pdf.doc.rect(scaleBarX + size * 2, scaleBarY, size * 2, 1, "FD");

      // draw numeric labels above scalebar
      this.pdf.doc.setTextColor("black");
      this.pdf.doc.setFontSize(6);
      this.pdf.doc.text(scaleBarX, scaleBarY - 1, "0");
      // /4 and could give 2.5. We still round, because of floating point arith
      this.pdf.doc.text(
        scaleBarX + size - 1,
        scaleBarY - 1,
        (Math.round((length * 10) / 4) / 10).toString()
      );
      this.pdf.doc.text(
        scaleBarX + size * 2 - 2,
        scaleBarY - 1,
        Math.round(length / 2).toString()
      );
      this.pdf.doc.text(
        scaleBarX + size * 4 - 4,
        scaleBarY - 1,
        Math.round(length).toString() + " " + unit
      );
    },
    addNorthArrow(position, offset, size) {
      // TODO: fix positioning
      // x/y defaults to offset for topleft corner (normal x/y coordinates)
      let x1 = offset.x;
      let y1 = offset.y;

      // if position is on the right, x needs to be calculate with pdf width and
      // the size of the element
      if (["topright", "bottomright"].indexOf(position) !== -1) {
        x1 = this.pdf.width - offset.x - size;
      }
      if (["bottomright", "bottomleft"].indexOf(position) !== -1) {
        y1 = this.pdf.height - offset.y - size;
      }

      var y2 = y1 + size * 3;
      var x3 = x1 - size * 2;
      var y3 = y1 + size * 5;
      var x4 = x1 + size * 2;
      // white triangle
      this.pdf.doc.setFillColor(255, 255, 255);
      this.pdf.doc.setDrawColor(255, 255, 255);
      this.pdf.doc.triangle(
        x3 - 0.8,
        y3 + 1.2,
        x1,
        y1 - 1.2,
        x1,
        y2 + 0.6,
        "F"
      );
      this.pdf.doc.triangle(
        x1,
        y1 - 1.2,
        x1,
        y2 + 0.6,
        x4 + 0.8,
        y3 + 1.2,
        "F"
      );

      // north arrow
      this.pdf.doc.setDrawColor(0, 0, 0);
      this.pdf.doc.setFillColor(255, 255, 255);
      this.pdf.doc.triangle(x3, y3, x1 - 0.1, y1 + 0.2, x1 - 0.1, y2, "FD");
      this.pdf.doc.setFillColor(0, 0, 0);
      this.pdf.doc.triangle(x1 + 0.1, y1 + 0.2, x1 + 0.1, y2, x4, y3, "FD");
      this.pdf.doc.setFontSize(size * 3.1);
      this.pdf.doc.setTextColor(255, 255, 255);
      this.pdf.doc.setFontStyle("bold");
      this.pdf.doc.text(size < 3 ? x1 - 0.5 : x1 - 1.3, y3 + 1, "N");
      this.pdf.doc.setFontSize(size * 3);
      this.pdf.doc.setTextColor(0, 0, 0);
      this.pdf.doc.setFontStyle("normal");
      this.pdf.doc.text(size < 3 ? x1 - 0.5 : x1 - 1.3, y3 + 1, "N");
    },
    addLegend(position, offset, rounding, brcolor) {
      if (
        this.selectedBottleneck &&
        this.selectedSurvey &&
        this.openLayersMap()
          .getLayer("BOTTLENECKISOLINE")
          .getVisible()
      ) {
        // transforming into an HTMLImageElement only to find out
        // the width x height of the legend image
        // FUTURE: find a better way to get the width and height
        let legendImage = new Image();
        legendImage.src = this.isolinesLegendImgDataURL;
        let aspectRatio = legendImage.width / legendImage.height;
        let width = 54;
        let height = width / aspectRatio;
        let padding = 2;

        // x/y defaults to offset for topleft corner (normal x/y coordinates)
        let x = offset.x;
        let y = offset.y;

        // if position is on the right, x needs to be calculate with pdf width and
        // the size of the element
        if (["topright", "bottomright"].indexOf(position) !== -1) {
          x = this.pdf.width - offset.x - width;
        }
        if (["bottomright", "bottomleft"].indexOf(position) !== -1) {
          y = this.pdf.height - offset.y - height;
        }

        this.addRoundedBox(x, y, width, height, "white", rounding, brcolor);
        this.pdf.doc.addImage(
          legendImage,
          x + padding,
          y + padding,
          width - 2 * padding,
          height - 2 * padding
        );
      }
    },
    addBottleneckInfo(position, offset, rounding, color, brcolor) {
      if (
        this.selectedBottleneck &&
        this.selectedSurvey &&
        this.openLayersMap()
          .getLayer("BOTTLENECKISOLINE")
          .getVisible()
      ) {
        let survey = this.selectedSurvey;

        // determine text dimensions
        // this is a little bit cumbersome but we need to separate width
        // calculations and writing
        this.pdf.doc.setFontSize(10);
        this.pdf.doc.setTextColor(color);
        let textOptions = { baseline: "hanging" };
        let str1_1 = this.$gettext("Bottleneck") + ": ";
        let str1_2 = this.selectedBottleneck;
        let str2_1 = this.$gettext("Survey date") + ": ";
        let str2_2 = survey.date_info;
        let str3_1 = this.$gettext("Ref gauge") + ": ";
        let str3_2 = survey.gauge_objname;
        let str4_1 = this.$gettext("Depth relativ to") + ": ";
        let str4_2 =
          survey.depth_reference +
          " = " +
          (survey.hasOwnProperty("waterlevel_value")
            ? survey.waterlevel_value + " cm"
            : "?");

        this.pdf.doc.setFontStyle("italic");
        let w1_1 = this.pdf.doc.getTextWidth(str1_1);
        this.pdf.doc.setFontStyle("bold");
        let w1_2 = this.pdf.doc.getTextWidth(str1_2);
        this.pdf.doc.setFontStyle("italic");
        let w2_1 = this.pdf.doc.getTextWidth(str2_1);
        this.pdf.doc.setFontStyle("normal");
        let w2_2 = this.pdf.doc.getTextWidth(str2_2);
        this.pdf.doc.setFontStyle("italic");
        let w3_1 = this.pdf.doc.getTextWidth(str3_1);
        this.pdf.doc.setFontStyle("normal");
        let w3_2 = this.pdf.doc.getTextWidth(str3_2);
        this.pdf.doc.setFontStyle("italic");
        let w4_1 = this.pdf.doc.getTextWidth(str4_1);
        this.pdf.doc.setFontStyle("normal");
        let w4_2 = this.pdf.doc.getTextWidth(str4_2);

        let height = 21;
        let padding = 3;
        let width =
          Math.max(w1_1 + w1_2, w2_1 + w2_2, w3_1 + w3_2, w4_1 + w4_2) +
          2 * padding;

        // x/y defaults to offset for topleft corner (normal x/y coordinates)
        let x = offset.x;
        let y = offset.y;

        // if position is on the right, x needs to be calculate with pdf width and
        // the size of the element
        if (["topright", "bottomright"].indexOf(position) !== -1) {
          x = this.pdf.width - offset.x - width;
        }
        if (["bottomright", "bottomleft"].indexOf(position) !== -1) {
          y = this.pdf.height - offset.y - height;
        }

        // white background box
        this.addRoundedBox(x, y, width, height, "white", rounding, brcolor);

        // bottleneck
        this.pdf.doc.setFontStyle("italic");
        this.pdf.doc.text(x + padding, y + padding, str1_1, textOptions);
        this.pdf.doc.setFontStyle("bold");
        this.pdf.doc.text(x + padding + w1_1, y + padding, str1_2, textOptions);

        // survey date
        this.pdf.doc.setFontStyle("italic");
        this.pdf.doc.text(x + padding, y + padding + 4, str2_1, textOptions);
        this.pdf.doc.setFontStyle("normal");
        this.pdf.doc.text(
          x + padding + w2_1,
          y + padding + 4,
          str2_2,
          textOptions
        );

        // ref gauge
        this.pdf.doc.setFontStyle("italic");
        this.pdf.doc.text(x + padding, y + padding + 8, str3_1, textOptions);
        this.pdf.doc.setFontStyle("normal");
        this.pdf.doc.text(
          x + padding + w3_1,
          y + padding + 8,
          str3_2,
          textOptions
        );

        // depth relative to
        this.pdf.doc.setFontStyle("italic");
        this.pdf.doc.text(x + padding, y + padding + 12, str4_1, textOptions);
        this.pdf.doc.setFontStyle("normal");
        this.pdf.doc.text(
          x + padding + w4_1,
          y + padding + 12,
          str4_2,
          textOptions
        );
      }
    }
  },
  mounted() {
    HTTP.get("/templates/map", {
      headers: {
        "X-Gemma-Auth": localStorage.getItem("token"),
        "Content-type": "text/xml; charset=UTF-8"
      }
    })
      .then(response => {
        if (response.data.length) {
          this.templates = [...this.templates, ...response.data];
          this.form.template = this.templates[1];
          this.applyTemplateToForm();
        } else {
          this.form.template = this.templates[0];
          this.templateData = this.form.template;
        }
      })
      .catch(e => {
        const { status, data } = e.response;
        displayError({
          title: this.$gettext("Backend Error"),
          message: `${status}: ${data.message || data}`
        });
      });
  }
};
</script>