view client/src/lib/mixins.js @ 4805:7de099c4824c

client: image-export: improve hyperlink ids for download * use own id-names for <a> element for each diagram to avoid having same id name in dom in case of two diagrams on screen
author Fadi Abbud <fadi.abbud@intevation.de>
date Mon, 28 Oct 2019 12:24:35 +0100
parents 9412bc2545e8
children 7cd40008124b
line wrap: on
line source

/* 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>
 * * Fadi Abbud <fadi.abbud@intevation.de>
 * * Bernhard Reiter <bernhard.reiter@intevation.de>
 */
import jsPDF from "jspdf-yworks";
import svg2pdf from "svg2pdf.js";
import locale2 from "locale2";
import { mapState } from "vuex";
import { HTTP } from "@/lib/http";
import * as d3 from "d3";
import sanitize from "sanitize-filename";
import canvg from "canvg";

/*eslint no-unused-vars: ["error", { "varsIgnorePattern": "[debugSVG|_]" }]*/
const debugSVG = ({ svg, svgWidth, svgHeight }) => {
  d3.select(svg)
    .append("rect")
    .attr("width", svgWidth)
    .attr("height", svgHeight)
    .attr("fill-opacity", 0)
    .attr("stroke", "#ff0000");
};

export const sortTable = {
  data() {
    return {
      sortColumn: "",
      sortDirection: "ASC",
      pageSize: 20,
      page: 1
    };
  },
  methods: {
    sortTable(sorting) {
      this.sortColumn = sorting.sortColumn;
      this.sortDirection = sorting.sortDirection;
    }
  }
};
/**
 * Since the names of LDC and HDC aren't normalized, we have to do guesswork
 * best fit is key with HDC or LDC in it
 */
export const refwaterlevels = {
  methods: {
    determineLDCHDC(refWaterLevels) {
      let HDC =
        refWaterLevels[Object.keys(refWaterLevels).find(e => /HDC/.test(e))];
      let LDC =
        refWaterLevels[Object.keys(refWaterLevels).find(e => /LDC/.test(e))];
      return { LDC, HDC };
    }
  }
};

export const diagram = {
  methods: {
    getDimensions({ svgWidth, svgHeight, main, nav }) {
      const mainMargin = main || {
        top: Math.floor(0.08 * svgHeight),
        right: Math.floor(0.08 * svgWidth),
        bottom: Math.floor(0.2 * svgHeight),
        left: Math.floor(0.08 * svgWidth)
      };
      const navMargin = nav || {
        top: Math.floor(0.78 * svgHeight),
        right: Math.floor(0.013 * svgWidth),
        bottom: Math.floor(0.095 * svgHeight),
        left: Math.floor(0.07 * svgWidth)
      };
      const width = Number(svgWidth) - mainMargin.left - mainMargin.right;
      const mainHeight = Number(svgHeight) - mainMargin.top - mainMargin.bottom;
      const navHeight = Number(svgHeight) - navMargin.top - navMargin.bottom;
      return { width, mainHeight, navHeight, mainMargin, navMargin };
    }
  }
};

export const pane = {
  computed: {
    paneId() {
      return this.$parent.pane.id;
    }
  }
};

export const templateLoader = {
  methods: {
    downloadFilename(type, name) {
      return `${type}-${sanitize(name).replace(
        / /g,
        "-"
      )}-${this.dateForPDF()}`;
    },
    loadTemplates(url) {
      return new Promise((resolve, reject) => {
        HTTP.get(url, {
          headers: {
            "X-Gemma-Auth": localStorage.getItem("token"),
            "Content-type": "text/xml; charset=UTF-8"
          }
        })
          .then(response => {
            resolve(response);
          })
          .catch(error => {
            reject(error);
          });
      });
    },
    prepareImages(elements) {
      /**
       * In order to render the images from the template, we need to convert
       * each image to dataURIs. Since this happens asynchronous,
       * we need to wrap each image into its own promise and only after all are
       * finished, we continue with the flow.
       */
      return new Promise(resolve => {
        const imageElementLoaders = elements.reduce((o, n, i) => {
          if (n.type === "image") {
            o.push(
              new Promise(resolve => {
                const image = new Image();
                image.onload = function() {
                  var canvas = document.createElement("canvas");
                  canvas.width = this.naturalWidth; // or 'width' if you want a special/scaled size
                  canvas.height = this.naturalHeight; // or 'height' if you want a special/scaled size
                  canvas.getContext("2d").drawImage(this, 0, 0);
                  resolve({
                    index: i,
                    url: canvas.toDataURL("image/png")
                  });
                };
                let URL = n.URL;
                if (!URL) {
                  URL = this.logoForPDF
                    ? this.logoForPDF
                    : "/img/gemma-logo-for-pdf.png";
                }
                image.src = URL;
              })
            );
          }
          return o;
        }, []);
        Promise.all(imageElementLoaders).then(values => {
          resolve(values);
        });
      });
    }
  }
};

export const pdfgen = {
  computed: {
    ...mapState("application", ["logoForPDF"]),
    ...mapState("user", ["user"])
  },
  methods: {
    downloadImage(elementName) {
      const diagramContainer = document.getElementById(this.containerId);
      const { clientHeight, clientWidth } = diagramContainer;
      diagramContainer.firstElementChild.setAttribute("width", clientWidth);
      diagramContainer.firstElementChild.setAttribute("height", clientHeight);
      const svg = diagramContainer.firstElementChild.outerHTML;
      const canvas = document.createElement("canvas");
      canvg(canvas, svg);
      const imgData = canvas.toDataURL("image/png");
      document.getElementById(elementName).setAttribute("href", imgData);
    },
    addDiagram(position, offset, width, height) {
      let x = offset.x,
        y = offset.y;
      const DPI = 80;
      const svgWidth = this.millimeter2pixels(width, DPI);
      const svgHeight = this.millimeter2pixels(height, DPI);
      // draw the diagram in a separated html element to get the full size
      const offScreen = document.querySelector("#offScreen");
      offScreen.style.width = `${svgWidth}px`;
      offScreen.style.height = `${svgHeight}px`;
      let zoomLevel = this.zoomStore;
      const layout = this.getPrintLayout(svgHeight, svgWidth);
      this.renderTo({
        element: offScreen,
        dimensions: this.getDimensions({
          svgWidth: svgWidth,
          svgHeight: svgHeight,
          ...layout
        }),
        zoomLevel // passing the zoom level to draw the diagram on pdf at this point
      });
      var svg = offScreen.querySelector("svg");
      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;
      }
      //debugSVG({ svg, svgWidth, svgHeight });
      svg2pdf(svg, this.pdf.doc, {
        xOffset: x,
        yOffset: y,
        scale: this.pixel2millimeter(1, DPI)
      });
      offScreen.removeChild(svg);
    },
    getPaperDimensions(format) {
      const dims = {
        A3: {
          height: 297,
          width: 420
        },
        A4: {
          height: 210,
          width: 297
        }
      };
      return dims[format.toUpperCase()];
    },
    millimeter2pixels(length, dpi) {
      return (dpi * length) / 25.4;
    },
    pixel2millimeter(pixels, dpi) {
      return (pixels * 25.4) / dpi;
    },
    isrsInfo(gauge) {
      // See https://www.elwis.de/DE/Service/Daten-und-Fakten/RIS-Index/RIS-Index-node.html
      const [
        _,
        countryCode,
        loCode,
        fairwaySection,
        orc,
        hectometre
      ] = gauge.properties.isrs_code.match(
        /(\w{2})(\w{3})(\w{5})(\w{5})(\w{5})/
      );
      return {
        countryCode: countryCode,
        loCode: loCode,
        fairwaySection: fairwaySection,
        orc: orc,
        hectometre: hectometre
      };
    },
    generatePDF(params) {
      // creates a new jsPDF object into this.pdf.doc
      // will call functions that the calling context has to provide
      // as specified in the templateData
      let templateData = params["templateData"];
      let diagramTitle = params["diagramTitle"];

      this.pdf.doc = new jsPDF("l", "mm", templateData.properties.paperSize);
      // pdf width and height in millimeter (landscape)
      if (templateData.properties.paperSize === "a3") {
        this.pdf.width = 420;
        this.pdf.height = 297;
      } else {
        this.pdf.width = 297;
        this.pdf.height = 210;
      }
      // check the template elements
      if (templateData) {
        this.pdf.doc.setFont("linbiolinum", "normal");
        let defaultFontSize = 11,
          defaultColor = "black",
          defaultTextColor = "black",
          defaultBorderColor = "white",
          defaultBgColor = "white",
          defaultRounding = 2,
          defaultPadding = 2,
          defaultOffset = { x: 0, y: 0 };
        templateData.elements.forEach(e => {
          switch (e.type) {
            case "diagram": {
              this.addDiagram(
                e.position,
                e.offset || defaultOffset,
                // use default width,height if they are missing in template definition
                e.width ||
                  (this.templateData.properties.paperSize === "a3" ? 318 : 230),
                e.height ||
                  (this.templateData.properties.paperSize === "a3" ? 104 : 110)
              );
              break;
            }
            case "diagramlegend": {
              this.addDiagramLegend(
                e.position,
                e.offset || defaultOffset,
                e.color || defaultColor
              );
              break;
            }
            case "diagramtitle": {
              this.addDiagramTitle(
                e.position,
                e.offset || defaultOffset,
                e.fontsize || defaultFontSize,
                e.color || defaultColor,
                diagramTitle
              );
              break;
            }
            case "text": {
              this.addText(
                e.position,
                e.offset || defaultOffset,
                e.width,
                e.fontsize || defaultFontSize,
                e.color || defaultTextColor,
                e.text || ""
              );
              break;
            }
            case "image": {
              this.addImage(
                e.url,
                e.format || "",
                e.position,
                e.offset || defaultOffset,
                e.width || 90,
                e.height || 60
              );
              break;
            }
            case "box": {
              this.addBox(
                e.position,
                e.offset || defaultOffset,
                e.width || 90,
                e.height || 60,
                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;
            }
          }
        });
      }
    },
    // add text at specific coordinates and do line breaks
    addText(position, offset, width, fontSize, color, text) {
      text = this.replacePlaceholders(text);
      // split the incoming string to an array, each element is a string of
      // words in a single line
      this.pdf.doc.setFontStyle("normal");
      this.pdf.doc.setTextColor(color);
      this.pdf.doc.setFontSize(fontSize);
      // 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 (!width) {
        width = this.pdf.doc.getTextWidth(text);
      }
      var textLines = this.pdf.doc.splitTextToSize(text, width);
      if (
        ["topright", "topleft"].indexOf(position) !== -1 &&
        y < this.getTextHeight(1)
      ) {
        y = this.getTextHeight(1);
      }
      if (["topright", "bottomright"].indexOf(position) !== -1) {
        x = this.pdf.width - offset.x - width;
      }
      if (["bottomright", "bottomleft"].indexOf(position) !== -1) {
        y = this.pdf.height - offset.y - this.getTextHeight(textLines.length);
      }
      this.pdf.doc.text(textLines, x, y, { baseline: "hanging" });
    },
    replacePlaceholders(text) {
      if (text.includes("{date}")) {
        text = text.replace("{date}", new Date().toLocaleString(locale2));
      }
      // get only day,month and year from the Date object
      if (text.includes("{date-minor}")) {
        var date = new Date();
        var dt =
          (date.getDate() < 10 ? "0" : "") +
          date.getDate() +
          "." +
          (date.getMonth() + 1 < 10 ? "0" : "") +
          (date.getMonth() + 1) +
          "." +
          date.getFullYear();
        text = text.replace("{date-minor}", dt.toLocaleString(locale2));
      }
      if (text.includes("{user}")) {
        text = text.replace("{user}", this.user);
      }
      return text;
    },
    addImage(url, format, position, offset, width, height) {
      let x = offset.x;
      let y = offset.y;
      if (["topright", "bottomright"].indexOf(position) !== -1) {
        x = this.pdf.width - offset.x - width;
      }
      if (["bottomright", "bottomleft"].indexOf(position) !== -1) {
        y = this.pdf.height - offset.y - height;
      }
      let image = new Image();
      image.src = url;
      if (format === "") {
        let tmp = image.src.split(".");
        format = tmp[tmp.length - 1].toUpperCase();
      }
      this.pdf.doc.addImage(image, format, x, y, width, height);
    },
    // add text at specific coordinates with a background box
    addBox(position, offset, width, height, rounding, color, brcolor) {
      // x/y defaults to offset for topleft corner (normal x/y coordinates)
      let x = offset.x;
      let y = offset.y;

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

      // FontSize is given in desktop publishing points defined as 1/72 inch.
      // aka 25.4 / 72 mm
      let fontSize = this.pdf.doc.getFontSize();
      let lineHeightFactor = 1.15; // default from jspdf-yworks 2.0.2
      if (typeof this.pdf.doc.getLineHeightFactor !== "undefined") {
        lineHeightFactor = this.pdf.doc.getLineHeightFactor();
      }
      return numberOfLines * fontSize * (25.4 / 72) * lineHeightFactor;
    },
    // title for diagram
    addDiagramTitle(position, offset, size, color, text) {
      let x = offset.x,
        y = offset.y;
      this.pdf.doc.setFontSize(size);
      this.pdf.doc.setFontStyle("bold");
      this.pdf.doc.setTextColor(color);
      let width =
        (this.pdf.doc.getStringUnitWidth(text) * size) / (72 / 25.6) + size / 2;
      // if position is on the right, x needs to be calculate with pdf width and
      // the size of the element
      if (["topright", "bottomright"].indexOf(position) !== -1) {
        x = this.pdf.width - offset.x - width;
      }
      if (["bottomright", "bottomleft"].indexOf(position) !== -1) {
        y = this.pdf.height - offset.y - this.getTextHeight(1);
      }
      this.pdf.doc.text(text, x, y, { baseline: "hanging" });
    },
    addRoundedBox(x, y, w, h, color, rounding, brcolor) {
      this.pdf.doc.setDrawColor(brcolor);
      this.pdf.doc.setFillColor(color);
      this.pdf.doc.roundedRect(x, y, w, h, rounding, rounding, "FD");
    },
    addTextBox(
      position,
      offset,
      width,
      height,
      rounding,
      padding,
      fontSize,
      color,
      background,
      text,
      brcolor
    ) {
      this.pdf.doc.setFontSize(fontSize);
      text = this.replacePlaceholders(text);

      if (!width) {
        width = this.pdf.doc.getTextWidth(text) + 2 * padding;
      }
      let textWidth = width - 2 * padding;
      let textLines = this.pdf.doc.splitTextToSize(text, textWidth);
      if (!height) {
        height = this.getTextHeight(textLines.length) + 2 * padding;
      }
      this.addBox(
        position,
        offset,
        width,
        height,
        rounding,
        background,
        brcolor
      );
      let yForText =
        ["bottomright", "bottomleft"].indexOf(position) !== -1
          ? offset.y
          : offset.y + height - this.getTextHeight(textLines.length);
      this.addText(
        position,
        { x: offset.x + padding, y: yForText },
        textWidth,
        fontSize,
        color,
        text
      );
    },
    dateForPDF() {
      return new Date()
        .toISOString()
        .slice(0, 10)
        .replace(/-/g, "");
    }
  }
};