view client/src/components/Pdftool.vue @ 5447:7a96321be219

Commented out a console logging in client to supress build warnings.
author Sascha L. Teichmann <sascha.teichmann@intevation.de>
date Wed, 14 Jul 2021 17:52:32 +0200
parents 351d38269e4e
children 661af6353d3b
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 => {
          // console.log(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>