view client/src/components/fairway/AvailableFairwayDepthLNWL.vue @ 3806:cc80a37173f8 yworks-svg2pdf

Available-fairway-depth(both): use mixin for template loading and image processing
author Thomas Junk <thomas.junk@intevation.de>
date Thu, 04 Jul 2019 11:43:06 +0200
parents 26325370ba18
children ff8ca2d80ce9
line wrap: on
line source

<template>
  <div class="d-flex flex-column flex-fill">
    <UIBoxHeader icon="chart-area" :title="title" :closeCallback="close" />
    <UISpinnerOverlay v-if="loading" />
    <div class="d-flex flex-fill">
      <DiagramLegend>
        <div v-for="(entry, index) in legendLNWL" :key="index" class="legend">
          <span
            :style="
              `${legendStyle(
                index
              )}; border-radius: 0.25rem; width: 40px; height: 20px;`
            "
          ></span>
          {{ entry }}
        </div>
        <div>
          <select
            @change="applyChange"
            v-model="form.template"
            class="form-control d-block custom-select-sm w-100 mt-2"
          >
            <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>
          <a
            :href="dataLink"
            :download="csvFileName"
            class="mt-2 btn btn-sm btn-info w-100"
            >Download CSV</a
          >
        </div>
      </DiagramLegend>
      <div
        ref="diagramContainer"
        :id="containerId"
        class="mx-auto my-auto diagram-container"
      ></div>
    </div>
  </div>
</template>

<style></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):
 * * Thomas Junk <thomas.junk@intevation.de>
 * * Markus Kottländer <markus.kottlaender@intevation.de>
 * * Fadi Abbud <fadi.abbud@intevation.de>
 */
import * as d3 from "d3";
import app from "@/main";
import debounce from "debounce";
import { mapState } from "vuex";
import filters from "@/lib/filters.js";
import canvg from "canvg";
import { diagram, pdfgen, templateLoader } from "@/lib/mixins";
import { HTTP } from "@/lib/http";
import { displayError } from "@/lib/errors";

export default {
  mixins: [diagram, pdfgen, templateLoader],
  components: {
    DiagramLegend: () => import("@/components/DiagramLegend")
  },
  data() {
    return {
      containerId: "availablefairwaydepthlnwl",
      resizeListenerFunction: null,
      loading: false,
      labelPaddingTop: 15,
      scalePaddingLeft: 50,
      paddingTop: 10,
      diagram: null,
      yScale: null,
      dimensions: null,
      pdf: {
        doc: null,
        width: null,
        height: null
      },
      form: {
        template: null
      },
      templateData: null,
      templates: [],
      defaultTemplate: {
        name: "Default",
        properties: {
          paperSize: "a4"
        },
        elements: [
          {
            type: "diagram",
            position: "topleft",
            offset: { x: 20, y: 60 },
            width: 290,
            height: 100
          },
          {
            type: "diagramtitle",
            position: "topleft",
            offset: { x: 70, y: 20 },
            fontsize: 20,
            color: "steelblue"
          },
          {
            type: "diagramlegend",
            position: "topleft",
            offset: { x: 30, y: 160 },
            color: "black"
          }
        ]
      }
    };
  },
  created() {
    this.resizeListenerFunction = debounce(this.drawDiagram, 100);
    window.addEventListener("resize", this.resizeListenerFunction);
  },
  destroyed() {
    window.removeEventListener("resize", this.resizeListenerFunction);
  },
  mounted() {
    // Nasty but necessary if we don't want to use the updated hook to re-draw
    // the diagram because this would re-draw it also for irrelevant reasons.
    // In this case we need to wait for the child component (DiagramLegend) to
    // render. According to the docs (https://vuejs.org/v2/api/#mounted) this
    // should be possible with $nextTick() but it doesn't work because it does
    // not guarantee that the DOM is not only updated but also re-painted on the
    // screen.
    setTimeout(this.drawDiagram, 150);

    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}`
        });
      });
  },
  computed: {
    ...mapState("fairwayavailability", [
      "selectedFairwayAvailabilityFeature",
      "fwLNWLData",
      "from",
      "to",
      "frequency",
      "csv",
      "depthlimit1",
      "depthlimit2",
      "widthlimit1",
      "widthlimit2"
    ]),
    legendLNWL() {
      const d = [this.depthlimit1, this.depthlimit2].sort();
      const w = [this.widthlimit1, this.widthlimit2].sort();
      const lowerBound = [d[0], w[0]].filter(x => x).join(", ");
      const upperBound = [d[1], w[1]].filter(x => x).join(", ");
      return [
        `> LDC`,
        `< ${lowerBound}`,
        `< ${upperBound}`,
        `>= ${upperBound}`
      ];
    },
    dataLink() {
      return `data:text/csv;charset=utf-8, ${encodeURIComponent(this.csv)}`;
    },
    csvFileName() {
      return `${this.$gettext("fairwayavailabilityLNWL")}-${
        this.featureName
      }-${filters.surveyDate(this.fromDate)}-${filters.surveyDate(
        this.toDate
      )}-${this.$gettext(this.frequency)}-.csv`;
    },
    fromDate() {
      return this.from;
    },
    toDate() {
      return this.to;
    },
    availability() {
      return this.plainAvailability;
    },
    title() {
      return `Available Fairway Depth vs LNWL: ${
        this.featureName
      } (${filters.surveyDate(this.fromDate)} - ${filters.surveyDate(
        this.toDate
      )}) ${this.$gettext(this.frequency)}`;
    },
    featureName() {
      if (this.selectedFairwayAvailabilityFeature == null) return "";
      return this.selectedFairwayAvailabilityFeature.properties.name;
    },
    widthPerItem() {
      return Math.min(
        (this.dimensions.width - this.scalePaddingLeft) /
          this.fwLNWLData.length,
        180
      );
    },
    ldcWidth() {
      return this.widthPerItem * 0.3;
    },
    afdWidth() {
      return this.widthPerItem * 0.5;
    },
    spaceBetween() {
      return this.widthPerItem * 0.2;
    }
  },
  methods: {
    legendStyle(index) {
      const style = {
        0: `background-color: ${this.$options.LWNLCOLORS.LDC};`,
        1: `background-color: ${this.$options.AFDCOLORS[2]};`,
        2: `background-color: ${this.$options.AFDCOLORS[1]};`,
        3: `background-color: ${this.$options.AFDCOLORS[0]};`
      };
      return style[index];
    },
    applyChange() {
      if (this.form.template.hasOwnProperty("properties")) {
        this.templateData = this.defaultTemplate;
        return;
      }
      if (this.form.template) {
        this.loadTemplates("/templates/diagram/" + 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.templateData = response.data.template_data;
              }
            );
          })
          .catch(e => {
            const { status, data } = e.response;
            displayError({
              title: this.$gettext("Backend Error"),
              message: `${status}: ${data.message || data}`
            });
          });
      }
    },
    downloadPDF() {
      let title = `Available Fairway Depth vs LNWL: ${this.featureName}`;
      this.generatePDF({
        templateData: this.templateData,
        diagramTitle: title
      });
      this.pdf.doc.save(`Available Fairway Depth LNWL: ${this.featureName}`);
    },
    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();
      }
      // use default width,height if they are missing in the template definition
      if (!width) {
        width = this.templateData.properties.paperSize === "a3" ? 380 : 290;
      }
      if (!height) {
        height = this.templateData.properties.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);
    },
    addDiagramLegend(position, offset, color) {
      let x = offset.x,
        y = offset.y;
      this.pdf.doc.setFontSize(10);
      let width =
        (this.pdf.doc.getStringUnitWidth(">= LDC") * 10) / (72 / 25.6) + 15;
      // 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(6);
      }
      this.pdf.doc.setTextColor(color);
      this.pdf.doc.setDrawColor(this.$options.LWNLCOLORS.LDC);
      this.pdf.doc.setFillColor(this.$options.LWNLCOLORS.LDC);
      this.pdf.doc.roundedRect(x, y, 10, 4, 1.5, 1.5, "FD");
      this.pdf.doc.text(this.legendLNWL[0], x + 12, y + 3);

      this.pdf.doc.setDrawColor(this.$options.AFDCOLORS[2]);
      this.pdf.doc.setFillColor(this.$options.AFDCOLORS[2]);
      this.pdf.doc.roundedRect(x, y + 5, 10, 4, 1.5, 1.5, "FD");
      this.pdf.doc.text(this.legendLNWL[1], x + 12, y + 8);

      this.pdf.doc.setDrawColor(this.$options.AFDCOLORS[1]);
      this.pdf.doc.setFillColor(this.$options.AFDCOLORS[1]);
      this.pdf.doc.roundedRect(x, y + 10, 10, 4, 1.5, 1.5, "FD");
      this.pdf.doc.text(this.legendLNWL[2], x + 12, y + 13);

      this.pdf.doc.setDrawColor(this.$options.AFDCOLORS[0]);
      this.pdf.doc.setFillColor(this.$options.AFDCOLORS[0]);
      this.pdf.doc.roundedRect(x, y + 15, 10, 4, 1.5, 1.5, "FD");
      this.pdf.doc.text(this.legendLNWL[3], x + 12, y + 18);
    },
    close() {
      this.$store.commit("application/paneSetup", "DEFAULT");
    },
    drawDiagram() {
      this.dimensions = this.getDimensions({
        main: { top: 20, right: 20, bottom: 110, left: 200 }
      });
      this.yScale = d3
        .scaleLinear()
        .domain([0, 100])
        .range([this.dimensions.mainHeight - 30, 0]);
      d3.select(".diagram-container svg").remove();
      this.generateDiagramContainer();
      this.drawBars();
      this.drawScaleLabel();
      this.drawScale();
      this.drawTooltip();
    },
    drawTooltip() {
      this.diagram
        .append("text")
        .text("")
        .attr("font-size", "0.8em")
        .attr("opacity", 0)
        .attr("id", "tooltip");
    },
    generateDiagramContainer() {
      const diagram = d3
        .select(".diagram-container")
        .append("svg")
        .attr("width", this.dimensions.width)
        .attr("height", this.dimensions.mainHeight);
      this.diagram = diagram
        .append("g")
        .attr("transform", `translate(0 ${this.paddingTop})`);
    },
    drawBars() {
      if (this.fwLNWLData) {
        this.fwLNWLData.forEach((data, i) => {
          this.drawLNWL(data, i);
          this.drawAFD(data, i);
          this.drawLabel(data.date, i);
        });
      }
    },
    drawLabel(date, i) {
      this.diagram
        .append("text")
        .text(date)
        .attr("text-anchor", "middle")
        .attr("font-size", "smaller")
        .attr(
          "transform",
          `translate(${this.scalePaddingLeft +
            this.widthPerItem * i +
            this.widthPerItem / 2} ${this.dimensions.mainHeight - 15})`
        );
    },
    drawAFD(data, i) {
      let afd = this.diagram
        .append("g")
        .attr(
          "transform",
          `translate(${this.scalePaddingLeft +
            this.spaceBetween / 2 +
            this.widthPerItem * i +
            this.ldcWidth})`
        );
      afd
        .selectAll("rect")
        .data([data.above, data.between, data.below])
        .enter()
        .append("rect")
        .on("mouseover", function() {
          d3.select(this).attr("opacity", "0.8");
          d3.select("#tooltip").attr("opacity", 1);
        })
        .on("mouseout", function() {
          d3.select(this).attr("opacity", 1);
          d3.select("#tooltip").attr("opacity", 0);
        })
        .on("mousemove", function(d) {
          let y = d3.mouse(this)[1];
          const dy = document
            .querySelector(".diagram-container")
            .getBoundingClientRect().left;
          d3.select("#tooltip")
            .text(d.toFixed(2))
            .attr("y", y - 10)
            .attr("x", d3.event.pageX - dy);
          //d3.event.pageX gives coordinates relative to SVG
          //dy gives offset of svg on page
        })
        .attr("height", d => {
          return this.yScale(0) - this.yScale(d);
        })
        .attr("y", (d, i) => {
          if (i === 0) {
            return this.yScale(d);
          }
          if (i === 1) {
            return this.yScale(data.above + d);
          }
          if (i === 2) {
            return this.yScale(data.above + data.between + d);
          }
        })
        .attr("width", this.afdWidth)
        .attr("fill", (d, i) => {
          return this.$options.AFDCOLORS[i];
        });
    },
    drawLNWL(data, i) {
      let lnwl = this.diagram
        .append("g")
        .attr(
          "transform",
          `translate(${this.scalePaddingLeft +
            this.spaceBetween / 2 +
            this.widthPerItem * i})`
        );
      lnwl
        .append("rect")
        .datum([data.ldc])
        .on("mouseover", function() {
          d3.select(this).attr("opacity", "0.8");
          d3.select("#tooltip").attr("opacity", 1);
        })
        .on("mouseout", function() {
          d3.select(this).attr("opacity", 1);
          d3.select("#tooltip").attr("opacity", 0);
        })
        .on("mousemove", function(d) {
          let y = d3.mouse(this)[1];
          const dy = document
            .querySelector(".diagram-container")
            .getBoundingClientRect().left;
          d3.select("#tooltip")
            .text(d[0].toFixed(2))
            .attr("y", y - 10)
            .attr("x", d3.event.pageX - dy);
          //d3.event.pageX gives coordinates relative to SVG
          //dy gives offset of svg on page
        })
        .attr("height", d => {
          return this.yScale(0) - this.yScale(d);
        })
        .attr("y", d => {
          return this.yScale(d);
        })
        .attr("width", this.ldcWidth)
        .attr("fill", () => {
          return this.$options.LWNLCOLORS.LDC;
        });
    },
    drawScaleLabel() {
      const center = this.dimensions.mainHeight / 2;
      this.diagram
        .append("text")
        .text(this.$options.LEGEND)
        .attr("text-anchor", "middle")
        .attr("x", 0)
        .attr("y", 0)
        .attr("dy", "1em")
        .attr("transform", `translate(0, ${center}), rotate(-90)`);
    },
    drawScale() {
      const yAxis = d3.axisLeft().scale(this.yScale);
      this.diagram
        .append("g")
        .attr("transform", `translate(${this.scalePaddingLeft})`)
        .call(yAxis)
        .selectAll(".tick text")
        .attr("fill", "black")
        .select(function() {
          return this.parentNode;
        })
        .selectAll(".tick line")
        .attr("stroke", "black");
      this.diagram.selectAll(".domain").attr("stroke", "black");
    }
  },
  watch: {
    fwLNWLData() {
      this.drawDiagram();
    }
  },
  LEGEND: app.$gettext("Percent"),
  AFDCOLORS: ["#3636ff", "#f49b7f", "#e15472"],
  LWNLCOLORS: {
    LDC: "#97ddf3",
    HDC: "#43FFE1"
  }
};
</script>