view client/src/components/fairway/Fairwayprofile.vue @ 3240:5240f5440b62

client: implemnt pdf-template for fairwayprofile diagram
author Fadi Abbud <fadi.abbud@intevation.de>
date Fri, 10 May 2019 12:09:24 +0200
parents a90091aaef67
children ecfa09241437
line wrap: on
line source

<template>
  <div class="d-flex flex-column flex-fill">
    <UIBoxHeader icon="chart-area" :title="title" :closeCallback="close" />
    <div class="d-flex flex-fill">
      <DiagramLegend>
        <div class="legend">
          <span style="background-color: #5995ff"></span> Water
        </div>
        <div class="legend">
          <span style="background-color: #1f4fff"></span> Fairway
        </div>
        <div class="legend">
          <span style="background-color: #4a2f06"></span>
          Ground
        </div>
        <div>
          <select
            v-model="form.template"
            @change="applyChange"
            class="form-control d-block custom-select-sm w-100"
          >
            <option
              v-for="template in templates"
              :value="template"
              :key="template.name"
            >
              {{ template.name }}
            </option>
          </select>
          <button
            @click="downloadPDF"
            type="button"
            class="btn btn-sm btn-info d-block w-100 mt-2"
          >
            <translate>Export to PDF</translate>
          </button>
        </div>
      </DiagramLegend>
      <div
        ref="diagramContainer"
        class="d-flex flex-fill justify-content-center align-items-center diagram-container"
      >
        <div v-if="!fairwayData">
          <translate>No data available.</translate>
        </div>
      </div>
    </div>
  </div>
</template>

<script>
/* This is Free Software under GNU Affero General Public License v >= 3.0
 * without warranty, see README.md and license for details.
 *
 * SPDX-License-Identifier: AGPL-3.0-or-later
 * License-Filename: LICENSES/AGPL-3.0.txt
 *
 * Copyright (C) 2018 by via donau
 *   – Österreichische Wasserstraßen-Gesellschaft mbH
 * Software engineering by Intevation GmbH
 *
 * Author(s):
 * Thomas Junk <thomas.junk@intevation.de>
 * Markus Kottländer <markus.kottlaender@intevation.de>
 */
import * as d3 from "d3";
import { mapState, mapGetters } from "vuex";
import debounce from "debounce";
import jsPDF from "jspdf";
import canvg from "canvg";
import { pdfgen } from "@/lib/mixins";
import { HTTP } from "@/lib/http";
import { displayError } from "@/lib/errors";

const GROUND_COLOR = "#4A2F06";
const WATER_COLOR = "#005DFF";

export default {
  mixins: [pdfgen],
  name: "fairwayprofile",
  components: {
    DiagramLegend: () => import("@/components/DiagramLegend")
  },
  data() {
    return {
      width: null,
      height: null,
      margin: {
        top: 20,
        right: 40,
        bottom: 30,
        left: 40
      },
      form: {
        template: null
      },
      templates: [],
      defaultTemplate: {
        name: "default",
        properties: {
          paperSize: "a4",
          format: "lanscape"
        },
        elements: [
          {
            type: "diagram",
            position: "topleft",
            offset: { x: 20, y: 60 },
            width: 290,
            height: 100
          },
          {
            type: "diagramtitle",
            position: "topleft",
            offset: { x: 90, y: 30 },
            fontsize: 22,
            color: "steelblue"
          },
          {
            type: "diagramlegend",
            position: "topleft",
            offset: { x: 30, y: 160 },
            color: "black"
          }
        ]
      },
      pdf: {
        doc: null,
        width: 32,
        height: 297
      },
      templateData: null
    };
  },
  computed: {
    ...mapGetters("fairwayprofile", ["totalLength"]),
    ...mapState("fairwayprofile", [
      "additionalSurvey",
      "currentProfile",
      "startPoint",
      "endPoint",
      "fairwayData",
      "maxAlt",
      "referenceWaterLevel",
      "selectedWaterLevel",
      "waterLevels"
    ]),
    ...mapState("application", ["paneSetup"]),
    title() {
      let dates = [this.selectedSurvey.date_info];
      if (this.additionalSurvey) dates.push(this.additionalSurvey.date_info);
      dates.map(d => this.$options.filters.dateTime(d, true));
      return `${this.$gettext("Fairwayprofile")}: ${
        this.selectedBottleneck
      } (${dates.join(", ")})`;
    },
    selectedSurvey: {
      get() {
        return this.$store.state.bottlenecks.selectedSurvey;
      }
    },
    selectedBottleneck: {
      get() {
        return this.$store.state.bottlenecks.selectedBottleneck;
      }
    },
    ...mapState("bottlenecks", ["selectedSurvey"]),
    relativeWaterLevelDelta() {
      return this.selectedWaterLevel.value - this.referenceWaterLevel;
    },
    currentData() {
      if (
        !this.selectedSurvey ||
        !this.currentProfile.hasOwnProperty(this.selectedSurvey.date_info)
      )
        return [];
      return this.currentProfile[this.selectedSurvey.date_info].points;
    },
    additionalData() {
      if (
        !this.additionalSurvey ||
        !this.currentProfile.hasOwnProperty(this.additionalSurvey.date_info)
      )
        return [];
      return this.currentProfile[this.additionalSurvey.date_info].points;
    },
    xScale() {
      return [0, this.totalLength];
    },
    yScaleRight() {
      //ToDO calcReleativeDepth(this.maxAlt) to get the
      // maximum depth according to the actual waterlevel
      // additionally: take the one which is higher reference or current waterlevel
      const DELTA = this.maxAlt * 1.1 - this.maxAlt;
      return [this.maxAlt * 1 + DELTA, -DELTA];
    }
  },
  watch: {
    currentData() {
      this.drawDiagram();
    },
    additionalData() {
      this.drawDiagram();
    },
    width() {
      this.drawDiagram();
    },
    height() {
      this.drawDiagram();
    },
    waterLevels() {
      this.drawDiagram();
    },
    selectedWaterLevel() {
      this.drawDiagram();
    },
    fairwayData() {
      this.drawDiagram();
    },
    selectedBottleneck() {
      this.$store.commit("application/paneSetup", "DEFAULT");
    }
  },
  methods: {
    close() {
      this.$store.commit(
        "application/paneSetup",
        this.paneSetup === "COMPARESURVEYS_FAIRWAYPROFILE"
          ? "COMPARESURVEYS"
          : "DEFAULT"
      );
      this.$store.dispatch("fairwayprofile/clearCurrentProfile");
    },
    applyChange() {
      if (this.form.template.hasOwnProperty("properties")) {
        this.templateData = this.defaultTemplate;
        return;
      }
      if (this.form.template) {
        HTTP.get("/templates/diagram/" + this.form.template.name, {
          headers: {
            "X-Gemma-Auth": localStorage.getItem("token"),
            "Content-type": "text/xml; charset=UTF-8"
          }
        })
          .then(response => {
            this.templateData = response.data.template_data;
          })
          .catch(e => {
            const { status, data } = e.response;
            displayError({
              title: this.$gettext("Backend Error"),
              message: `${status}: ${data.message || data}`
            });
          });
      }
    },
    downloadPDF() {
      if (this.templateData) {
        this.pdf.doc = new jsPDF(
          "l",
          "mm",
          this.templateData.properties.paperSize
        );
        // pdf width and height in millimeter (landscape)
        this.pdf.width =
          this.templateData.properties.paperSize === "a3" ? 420 : 297;
        this.pdf.height =
          this.templateData.properties.paperSize === "a3" ? 297 : 210;
        let defaultOffset = { x: 0, Y: 0 },
          defaultColor = "black";
        this.templateData.elements.forEach(e => {
          switch (e.type) {
            case "diagram": {
              this.addDiagram(
                e.position,
                e.offset || defaultOffset,
                e.width,
                e.height
              );
              break;
            }
            case "diagramlegend": {
              this.addDiagramLegend(
                e.position,
                e.offset || defaultOffset,
                e.color || defaultColor
              );
              break;
            }
            case "diagramtitle": {
              this.addDiagramTitle(e.position, e.offset, e.fontsize, e.color);
              break;
            }
          }
        });
      }
      this.pdf.doc.save("Fairwayprofile diagram");
    },
    addDiagram(position, offset, width, height) {
      let x = offset.x,
        y = offset.y;
      var svg = this.$refs.diagramContainer.innerHTML;
      if (svg) {
        svg = svg.replace(/\r?\n|\r/g, "").trim();
      }
      // landscape format is used for both a3,a4 papersize
      if (!width) {
        width = this.form.paperSize === "a3" ? 380 : 290;
      }
      if (!height) {
        height = this.form.paperSize === "a3" ? 130 : 100;
      }
      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;
      }
      var canvas = document.createElement("canvas");
      canvas.width = window.innerWidth;
      canvas.height = window.innerHeight / 2;
      canvg(canvas, svg, {
        ignoreMouse: true,
        ignoreAnimation: true,
        ignoreDimensions: true
      });
      var imgData = canvas.toDataURL("image/png");
      this.pdf.doc.addImage(imgData, "PNG", x, y, width, height);
    },
    addDiagramTitle(position, offset, size, color) {
      let x = offset.x,
        y = offset.y;
      let fairwayInfo =
        this.selectedBottleneck + " (" + this.selectedSurvey.date_info + ")";
      let width =
        (this.pdf.doc.getStringUnitWidth(fairwayInfo) * 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.setTextColor(color);
      this.pdf.doc.setFontSize(size);
      this.pdf.doc.setFontStyle("bold");
      this.pdf.doc.text(fairwayInfo, x, y, { baseline: "hanging" });
    },
    // Diagram legend
    addDiagramLegend(position, offset, color) {
      let x = offset.x,
        y = offset.y;
      let width =
        (this.pdf.doc.getStringUnitWidth("Ground") * 10) / (72 / 25.6) + 5;
      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(3);
      }
      this.pdf.doc.setFontSize(10);
      this.pdf.doc.setTextColor(color);
      this.pdf.doc.setDrawColor("white");
      this.pdf.doc.setFillColor("#5995ff");
      this.pdf.doc.circle(x, y, 2, "FD");
      this.pdf.doc.text(x + 3, y + 1, "Water");
      this.pdf.doc.setFillColor("#1f4fff");
      this.pdf.doc.circle(x, y + 5, 2, "FD");
      this.pdf.doc.text(x + 3, y + 6, "Fairway");
      this.pdf.doc.setFillColor("#4a2f06");
      this.pdf.doc.circle(x, y + 10, 2, "FD");
      this.pdf.doc.text(x + 3, y + 11, "Ground");
    },
    getTextHeight(numberOfLines) {
      return (
        numberOfLines *
        ((this.pdf.doc.getFontSize() * 25.4) / 80) *
        this.pdf.doc.getLineHeightFactor()
      );
    },
    calcRelativeDepth(depth) {
      /* takes a depth value and substracts the delta of the relative waterlevel
       * say the reference level is above the current level, the ground is nearer,
       * thus, the depth is lower.
       *
       * E.g.:
       *
       * Reference waterlevel 5m, current 4m => delta = -1m
       * If the distance to the ground was 3m from the 5m mark
       * it is now only 2m from the current waterlevel.
       *
       *  Vice versa:
       *
       *  If the reference level is 5m and the current 6m => delta = +1m
       *  The ground is one meter farer away from the current waterlevel
       *
       */
      return depth - this.relativeWaterLevelDelta;
    },
    drawDiagram() {
      d3.select(".diagram-container svg").remove();
      this.scaleFairwayProfile();
      let svg = d3.select(".diagram-container").append("svg");
      svg.attr("width", "100%");
      svg.attr("height", "100%");
      const width = this.width - this.margin.right - 1.5 * this.margin.left;
      const height = this.height - this.margin.top - 2 * this.margin.bottom;
      const currentData = this.currentData;
      const additionalData = this.additionalData;
      const { xScale, yScaleRight, graph } = this.generateCoordinates(
        svg,
        height,
        width
      );
      if (!this.height || !this.width) return; // do not try to render when height and width are unknown
      this.drawWaterlevel({ graph, xScale, yScaleRight, height });
      this.drawLabels({ graph, height });
      if (currentData) {
        this.drawProfile({
          graph,
          xScale,
          yScaleRight,
          currentData,
          height,
          color: GROUND_COLOR,
          strokeColor: "black",
          opacity: 1
        });
      }
      if (additionalData) {
        this.drawProfile({
          graph,
          xScale,
          yScaleRight,
          currentData: additionalData,
          height,
          color: GROUND_COLOR,
          strokeColor: "#943007",
          opacity: 0.6
        });
      }
      this.drawFairway({ graph, xScale, yScaleRight });
    },
    drawFairway({ graph, xScale, yScaleRight }) {
      if (this.fairwayData === undefined) {
        return;
      }
      for (let data of this.fairwayData) {
        const [startPoint, endPoint, depth] = data.coordinates[0];
        const style = data.style();
        let fairwayArea = d3
          .area()
          .x(function(d) {
            return xScale(d.x);
          })
          .y0(yScaleRight(0))
          .y1(function(d) {
            return yScaleRight(d.y);
          });
        graph
          .append("path")
          .datum([{ x: startPoint, y: depth }, { x: endPoint, y: depth }])
          .attr("fill", "#002AFF")
          .attr("fill-opacity", 0.65)
          .attr("stroke", style[0].getStroke().getColor())
          .attr("d", fairwayArea);
      }
    },
    drawLabels({ graph, height }) {
      graph
        .append("text")
        .attr("transform", ["rotate(-90)"])
        .attr("y", this.width - 70)
        .attr("x", -(this.height - this.margin.top - this.margin.bottom) / 2)
        .attr("dy", "1em")
        .attr("fill", "black")
        .style("text-anchor", "middle")
        .text("Depth [m]");
      graph
        .append("text")
        .attr("y", -50)
        .attr("x", -(height / 4))
        .attr("dy", "1em")
        .attr("fill", "black")
        .style("text-anchor", "middle")
        .attr("transform", [
          "translate(" + this.width / 2 + "," + this.height + ")",
          "rotate(0)"
        ])
        .text("Width [m]");
    },
    generateCoordinates(svg, height, width) {
      let xScale = d3
        .scaleLinear()
        .domain(this.xScale)
        .rangeRound([0, width]);

      xScale.ticks(5);

      let yScaleRight = d3
        .scaleLinear()
        .domain(this.yScaleRight)
        .rangeRound([height, 0]);

      let xAxis = d3.axisBottom(xScale);
      let yAxis2 = d3.axisRight(yScaleRight);
      let graph = svg
        .append("g")
        .attr(
          "transform",
          "translate(" + this.margin.left + "," + this.margin.top + ")"
        );
      graph
        .append("g")
        .attr("transform", "translate(0," + height + ")")
        .call(xAxis.ticks(5))
        .selectAll(".tick text")
        .attr("fill", "black")
        .select(function() {
          return this.parentNode;
        })
        .selectAll(".tick line")
        .attr("stroke", "black");
      graph
        .append("g")
        .attr("transform", "translate(" + width + ",0)")
        .call(yAxis2)
        .selectAll(".tick text")
        .attr("fill", "black")
        .select(function() {
          return this.parentNode;
        })
        .selectAll(".tick line")
        .attr("stroke", "black");
      graph.selectAll(".domain").attr("stroke", "black");
      return { xScale, yScaleRight, graph };
    },
    drawWaterlevel({ graph, xScale, yScaleRight, height }) {
      let waterArea = d3
        .area()
        .x(function(d) {
          return xScale(d.x);
        })
        .y0(height)
        .y1(function(d) {
          return yScaleRight(d.y);
        });
      graph
        .append("path")
        .datum([{ x: 0, y: 0 }, { x: this.totalLength, y: 0 }])
        .attr("fill-opacity", 0.65)
        .attr("fill", WATER_COLOR)
        .attr("stroke", WATER_COLOR)
        .attr("d", waterArea);
    },
    drawProfile({
      graph,
      xScale,
      yScaleRight,
      currentData,
      height,
      color,
      strokeColor,
      opacity
    }) {
      for (let part of currentData) {
        let profileLine = d3
          .line()
          .x(d => {
            return xScale(d.x);
          })
          .y(d => {
            return yScaleRight(d.y);
          });
        let profileArea = d3
          .area()
          .x(function(d) {
            return xScale(d.x);
          })
          .y0(height)
          .y1(function(d) {
            return yScaleRight(d.y);
          });
        graph
          .append("path")
          .datum(part)
          .attr("fill", color)
          .attr("stroke", color)
          .attr("stroke-width", 3)
          .attr("stroke-opacity", opacity)
          .attr("fill-opacity", opacity)
          .attr("d", profileArea);
        graph
          .append("path")
          .datum(part)
          .attr("fill", "none")
          .attr("stroke", strokeColor)
          .attr("stroke-linejoin", "round")
          .attr("stroke-linecap", "round")
          .attr("stroke-width", 3)
          .attr("stroke-opacity", opacity)
          .attr("fill-opacity", 0)
          .attr("d", profileLine);
      }
    },
    scaleFairwayProfile() {
      if (!document.querySelector(".diagram-container")) return;
      const clientHeight = document.querySelector(".diagram-container")
        .clientHeight;
      const clientWidth = document.querySelector(".diagram-container")
        .clientWidth;
      if (!clientHeight || !clientWidth) return;
      this.height = clientHeight;
      this.width = clientWidth;
    }
  },
  created() {
    window.addEventListener("resize", debounce(this.drawDiagram), 100);
  },
  mounted() {
    this.drawDiagram();
    this.templates[0] = this.defaultTemplate;
    this.form.template = this.templates[0];
    this.templateData = this.form.template;
    HTTP.get("/templates/diagram", {
      headers: {
        "X-Gemma-Auth": localStorage.getItem("token"),
        "Content-type": "text/xml; charset=UTF-8"
      }
    })
      .then(response => {
        if (response.data.length) {
          this.templates = response.data;
          this.form.template = this.templates[0];
          this.templates[this.templates.length] = this.defaultTemplate;
          this.applyChange();
        }
      })
      .catch(e => {
        const { status, data } = e.response;
        displayError({
          title: this.$gettext("Backend Error"),
          message: `${status}: ${data.message || data}`
        });
      });
  },
  updated() {
    this.drawDiagram();
  },
  destroyed() {
    window.removeEventListener("resize", debounce(this.drawDiagram));
  }
};
</script>