view client/src/components/Pdftool.vue @ 5560:f2204f91d286

Join the log lines of imports to the log exports to recover data from them. Used in SR export to extract information that where in the meta json but now are only found in the log.
author Sascha L. Teichmann <sascha.teichmann@intevation.de>
date Wed, 09 Feb 2022 18:34:40 +0100
parents 661af6353d3b
children 3b842e951317
line wrap: on
line source

<template>
  <div
    :class="[
      'box ui-element rounded bg-white text-nowrap',
      { expanded: showPdfTool }
    ]"
  >
    <div style="width: 17rem">
      <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>
        <div class="d-flex flex-fill-row">
          <small class="my-auto text-muted">
            <translate>Scale to 1:</translate>
          </small>
          <input
            class="form-control form-control-sm w-100 ml-2"
            :placeholder="scalePlaceholder"
            v-model.number="form.scale"
            type="number"
          />
        </div>
        <button
          @click="download"
          :key="'downloadBtn'"
          type="button"
          v-if="readyToGenerate"
          class="btn btn-sm btn-info d-block w-100 mt-2"
          :disabled="sourcesLoading > 0"
        >
          <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";
import sanitize from "sanitize-filename";
import { WFS } from "ol/format";
import { equalTo as equalToFilter } from "ol/format/filter";
import { intersects } from "ol/extent";

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",
        scale: null
      },
      templates: [
        {
          name: DEFAULT_TEMPLATE,
          properties: {
            format: "landscape",
            paperSize: "a4",
            resolution: "80"
          },
          elements: [
            {
              type: "scalebar",
              position: "bottomright",
              offset: { x: 1, y: 1 }
            },
            {
              type: "textbox",
              position: "bottomleft",
              offset: { x: 1, y: 1 },
              fontSize: 8,
              text: this.$gettext("Generated by") + " " + "{user}, {date}"
            },
            {
              type: "northarrow",
              position: "topleft",
              offset: { x: 6, y: 4 },
              size: 2
            },
            {
              type: "bottleneck",
              position: "topright",
              offset: { x: 2, y: 2 }
            },
            {
              type: "legend",
              position: "topright",
              offset: { x: 2, y: 25 }
            }
          ]
        }
      ],
      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,
      resolution: null
    };
  },
  computed: {
    ...mapState("application", ["showPdfTool", "logoForPDF"]),
    ...mapState("bottlenecks", [
      "selectedBottleneck",
      "selectedSurvey",
      "bottleneckForPrint"
    ]),
    ...mapState("map", ["isolinesLegendImgDataURL", "openLayersMaps"]),
    ...mapGetters("map", ["openLayersMap"]),
    generatePdfLable() {
      return this.$gettext("Generate PDF");
    },
    sourcesLoading() {
      let counter = 0;
      this.openLayersMaps.forEach(map => {
        let layers = map.getLayers().getArray();
        for (let i = 0; i < layers.length; i++) {
          if (layers[i].getSource().loading) counter++;
        }
      });
      return counter;
    },
    scalePlaceholder() {
      if (typeof this.openLayersMap() !== "undefined") {
        return this.calculateScaleDenominator();
      } else {
        return "10000";
      }
    }
  },
  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 && 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(error => {
            let message = "Backend not reachable";
            if (error.response) {
              const { status, data } = error.response;
              message = `${status}: ${data.message || data}`;
            }
            displayError({
              title: this.$gettext("Backend Error"),
              message: message
            });
          });
      } 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;
    },
    getSoundingInfo() {
      return new Promise((resolve, reject) => {
        const map = this.openLayersMap();
        const currentExtent = map.getView().calculateExtent(map.getSize());
        const params = {
          srsName: "EPSG:3857",
          featureNS: "gemma",
          featurePrefix: "gemma",
          featureTypes: ["sounding_results_areas_geoserver"],
          outputFormat: "application/json",
          bbox: currentExtent,
          geometryName: "areas"
        };
        const survey = this.selectedSurvey;
        if (survey) {
          if (survey["survey_type"] === "marking") {
            params["featureTypes"] = [
              "sounding_results_marking_points_geoserver"
            ];
            params["geometryName"] = "points";
          }
          params["filter"] = equalToFilter(
            "bottleneck_id",
            this.selectedSurvey.bottleneck_id
          );
        }
        const getSoundingResultFeatures = new WFS().writeGetFeature(params);
        HTTP.post(
          "/internal/wfs",
          new XMLSerializer().serializeToString(getSoundingResultFeatures),
          {
            headers: {
              "X-Gemma-Auth": localStorage.getItem("token"),
              "Content-type": "text/xml; charset=UTF-8"
            }
          }
        )
          .then(response => {
            resolve(response);
          })
          .catch(error => {
            reject(error);
          });
      });
    },
    download() {
      this.getSoundingInfo()
        .then(response => {
          let soundingInfo = {};
          if (this.selectedSurvey) {
            soundingInfo = {
              number: response.data.numberMatched || 0,
              feature:
                response.data.features.filter(
                  f => f.properties.date_info === this.selectedSurvey.date_info
                )[0] || {}
            };
          } else {
            soundingInfo = { number: 0, feature: {} };
          }
          this.$store.commit("bottlenecks/setSoundingInfo", soundingInfo);
          this.generatePDF(soundingInfo);
        })
        .catch(error => {
          let message = "Backend not reachable";
          if (error.response) {
            const { status, data } = error.response;
            message = `${status}: ${data.message || data}`;
          }
          displayError({
            title: this.$gettext("Backend Error"),
            message: message
          });
        });
    },
    generatePDF(soundingInfo) {
      /**
       * In order to generate the image with the appropriate resolution
       * we have to temporaily scale the visible part of the map.
       * The newly rendered canvas is converted to Base64 DataURL.
       * After that is done, the resolution is resetted to its previous state.
       *
       * calculateExtent() and fit() do not give the desired result
       * when the view is rotated so we replace them completely by setting resolution
       *
       * Details: https://gis.stackexchange.com/questions/328933/openlayers-generating-clientside-pdfs
       *
       */
      this.$store.commit("application/setOngoingPDFExport", true);
      this.readyToGenerate = false;
      this.setPDFDimension();
      // FUTURE: consider margins
      const mapSizeForPrint = this.setMapSizForPrint();
      var map = this.openLayersMap();
      this.mapSize = map.getSize();
      this.resolution = map.getView().getResolution();

      this.pdf.doc = new jsPDF(this.form.format, "mm", this.form.paperSize);
      this.rendercompleteListener = map.once("rendercomplete", event => {
        let canvas = event.context.canvas;
        let scaleDenominator = this.calculateScaleDenominator();
        var snapshot = canvas.toDataURL("image/jpeg");
        this.pdf.doc.addImage(
          snapshot,
          "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";
          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,
                  soundingInfo
                );
                break;
              }
              case "box": {
                this.addBox(
                  e.position,
                  e.offset || defaultOffset,
                  e.width,
                  e.height,
                  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,
                  soundingInfo.number > 0
                );
                break;
              }
              case "legend": {
                this.addLegend(
                  e.position,
                  e.offset || defaultOffset,
                  e.rounding === 0 || e.rounding ? e.rounding : defaultRounding,
                  e.brcolor || defaultBorderColor,
                  soundingInfo.number > 0
                );
                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;
              }
            }
          });
          // Check if the bottlenck in the current view Extent
          const isBottlenckVisible = () => {
            const currentExtent = map.getView().calculateExtent(map.getSize());
            const btnExtent = map
              .getLayer("BOTTLENECKS")
              .getSource()
              .getFeatures()
              .find(f => f.get("objnam") === this.bottleneckForPrint)
              .getGeometry()
              .getExtent();
            return intersects(currentExtent, btnExtent);
          };
          let filename = "map";
          if (
            this.bottleneckForPrint &&
            (soundingInfo.number > 0 || isBottlenckVisible())
          ) {
            filename = `BN-${sanitize(this.bottleneckForPrint).replace(
              / /g,
              "-"
            )}`;
            if (this.selectedSurvey) {
              filename +=
                "-sr" + this.selectedSurvey.date_info.replace(/-/g, "");
            }
          }
          this.pdf.doc.save(`${filename}-${this.dateForPDF()}.pdf`);
        }
        map.setSize(this.mapSize);
        map.getView().setResolution(this.resolution);
        this.readyToGenerate = true;
        this.$store.commit("application/setOngoingPDFExport", false);
      });

      const size = map.getSize();
      const [width, height] = mapSizeForPrint;
      map.setSize(mapSizeForPrint);
      const scaling = Math.min(width / size[0], height / size[1]);
      map
        .getView()
        .setResolution(
          this.form.scale
            ? this.getResolutionFromScale()
            : this.resolution / scaling
        );
    },
    getResolutionFromScale() {
      const scaling = Math.round(this.form.scale / 1000);
      return scaling / this.getMeterPerPixel(this.form.resolution / 25.4);
    },
    getMeterPerPixel(f) {
      var map = this.openLayersMap();
      let view = map.getView();
      let proj = view.getProjection();
      return (
        getPointResolution(proj, f, view.getCenter()) * proj.getMetersPerUnit()
      );
    },
    cancel() {
      try {
        this.openLayersMap().un(
          this.rendercompleteListener.type,
          this.rendercompleteListener.listener
        );
        this.openLayersMap().setSize(this.mapSize);
        this.openLayersMap()
          .getView()
          .setResolution(this.resolution);
      } finally {
        this.$store.commit("application/setOngoingPDFExport", false);
        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;

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

      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, hasSounding) {
      if (
        hasSounding &&
        this.bottleneckForPrint &&
        this.selectedSurvey &&
        this.openLayersMap()
          .getLayer("BOTTLENECKISOLINE")
          .getVisible()
      ) {
        const ZPGEXCEPTION =
          this.soundingInfo &&
          this.soundingInfo.number > 0 &&
          this.soundingInfo.feature.properties.zpg_exception;
        let SPACER = ZPGEXCEPTION ? 10 : 4;

        // 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 + SPACER;

        // 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 - SPACER - 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, hasSounding) {
      if (
        hasSounding &&
        this.bottleneckForPrint &&
        this.selectedSurvey &&
        this.openLayersMap()
          .getLayer("BOTTLENECKISOLINE")
          .getVisible()
      ) {
        const ZPGEXCEPTION =
          this.soundingInfo &&
          this.soundingInfo.number > 0 &&
          this.soundingInfo.feature.properties.zpg_exception;

        let survey = this.selectedSurvey;
        const SURVEYTYPES = {
          marking: "Marking Vessel",
          multi: "Multibeam",
          single: "Singlebeam"
        };
        // 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 + " (" + SURVEYTYPES[survey["survey_type"]] + ")";
        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;
        if (!ZPGEXCEPTION) {
          str4_2 +=
            " = " +
            (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 str5_1 = "";
        let w5_1 = 0;
        let SPACER = 6;
        if (ZPGEXCEPTION) {
          str5_1 = this.$gettext("Bottleneck with ZPG Exception");
          this.pdf.doc.setFontStyle("normal");
          w5_1 = this.pdf.doc.getTextWidth(str5_1);
        }

        let height = ZPGEXCEPTION ? 24 + SPACER : 24;
        let padding = 2;
        let width = ZPGEXCEPTION
          ? Math.max(
              w1_1 + w1_2,
              w2_1 + w2_2,
              w3_1 + w3_2,
              w4_1 + w4_2 + w5_1
            ) +
            2 * padding
          : 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 + 1 + SPACER, str2_1, textOptions);
        this.pdf.doc.setFontStyle("normal");
        this.pdf.doc.text(
          x + padding + w2_1,
          y + 1 + SPACER,
          str2_2,
          textOptions
        );

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

        // depth relative to
        this.pdf.doc.setFontStyle("italic");
        this.pdf.doc.text(
          x + padding,
          y + 0.5 + 3 * SPACER,
          str4_1,
          textOptions
        );
        this.pdf.doc.setFontStyle("normal");
        this.pdf.doc.text(
          x + padding + w4_1,
          y + 0.5 + 3 * SPACER,
          str4_2,
          textOptions
        );
        if (ZPGEXCEPTION) {
          this.pdf.doc.setFontStyle("bold");
          this.pdf.doc.text(x + padding, y + 4 * SPACER, str5_1, textOptions);
        }
      }
    },
    calculateScaleDenominator() {
      const pixelsPerMapMillimeter = this.form.resolution / 25.4;
      if (!this.form.scale) {
        this.setPDFDimension();
        const mapSizeForPrint = this.setMapSizForPrint();
        const size = this.openLayersMap().getSize();
        const [width, height] = mapSizeForPrint;
        const scaling = Math.min(width / size[0], height / size[1]);
        return Math.round(
          1000 *
            pixelsPerMapMillimeter *
            this.getMeterPerPixel(
              this.openLayersMap()
                .getView()
                .getResolution() / scaling
            )
        );
      }
      return Math.round(
        1000 *
          pixelsPerMapMillimeter *
          this.getMeterPerPixel(
            this.openLayersMap()
              .getView()
              .getResolution()
          )
      );
    },
    setPDFDimension() {
      if (this.form.format !== "portrait") {
        this.pdf.width = paperSizes[this.form.paperSize][0];
        this.pdf.height = paperSizes[this.form.paperSize][1];
      } else {
        this.pdf.width = paperSizes[this.form.paperSize][1];
        this.pdf.height = paperSizes[this.form.paperSize][0];
      }
    },
    setMapSizForPrint() {
      const pixelsPerMapMillimeter = this.form.resolution / 25.4;
      return [
        Math.round(this.pdf.width * pixelsPerMapMillimeter),
        Math.round(this.pdf.height * pixelsPerMapMillimeter)
      ];
    }
  },
  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(error => {
        let message = "Backend not reachable";
        if (error.response) {
          const { status, data } = error.response;
          message = `${status}: ${data.message || data}`;
        }
        displayError({
          title: this.$gettext("Backend Error"),
          message: message
        });
      });
  }
};
</script>