view client/src/components/fairway/AvailableFairwayDepthLNWL.vue @ 3474:a39795393c7c

legend
author Thomas Junk <thomas.junk@intevation.de>
date Mon, 27 May 2019 12:16:45 +0200
parents 48d09fb1d6c7
children 45104cc6fd50
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 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 { diagram } from "@/lib/mixins";
import { mapState } from "vuex";
import filters from "@/lib/filters.js";
import jsPDF from "jspdf";
import canvg from "canvg";
import { pdfgen } from "@/lib/mixins";
import { HTTP } from "@/lib/http";
import { displayError } from "@/lib/errors";

export default {
  mixins: [diagram, pdfgen],
  components: {
    DiagramLegend: () => import("@/components/DiagramLegend")
  },
  data() {
    return {
      containerId: "availablefairwaydepthlnwl",
      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() {
    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}`
        });
      });
  },
  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.selectedFairwayAvailabilityFeature.properties.name
      }-${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() {
      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[0]};`,
        2: `background-color: ${this.$options.AFDCOLORS[1]};`,
        3: `background-color: ${this.$options.LWNLCOLORS.HDC};`
      };
      return style[index];
    },
    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() {
      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;
      if (this.templateData) {
        // default values if some are missing in template
        let defaultFontSize = 11,
          defaultColor = "black",
          defaultWidth = 70,
          defaultTextColor = "black",
          defaultBorderColor = "white",
          defaultBgColor = "white",
          defaultRounding = 2,
          defaultPadding = 2,
          defaultOffset = { x: 0, y: 0 };
        this.templateData.elements.forEach(e => {
          switch (e.type) {
            case "diagram": {
              this.addDiagram(
                e.position,
                e.offset || defaultOffset,
                e.width,
                e.height
              );
              break;
            }
            case "diagramtitle": {
              let title = `Available Fairway Depth vs LNWL: ${
                this.featureName
              }`;
              this.addDiagramTitle(
                e.position,
                e.offset || defaultOffset,
                e.fontsize || defaultFontSize,
                e.color || defaultColor,
                title
              );
              break;
            }
            case "diagramlegend": {
              this.addDiagramLegend(
                e.position,
                e.offset || defaultOffset,
                e.color || defaultColor
              );
              break;
            }
            case "text": {
              this.addText(
                e.position,
                e.offset || defaultOffset,
                e.width || defaultWidth,
                e.fontsize || defaultFontSize,
                e.color || defaultTextColor,
                e.text
              );
              break;
            }
            case "image": {
              this.addImage(
                e.url,
                e.format,
                e.position,
                e.offset || defaultOffset,
                e.width,
                e.height
              );
              break;
            }
            case "box": {
              this.addBox(
                e.position,
                e.offset,
                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;
            }
          }
        });
      }
      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();
      }
      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 [h]") * 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(7);
      }
      this.pdf.doc.setTextColor(color);
      this.pdf.doc.setDrawColor("rgb(255, 133, 94)");
      this.pdf.doc.setFillColor("rgb(255, 133, 94)");
      this.pdf.doc.rect(x, y, 8, 4, "FD");
      this.pdf.doc.text(">= LDC [h]", x + 10, y + 3);

      this.pdf.doc.setDrawColor("rgb(255, 66, 79)");
      this.pdf.doc.setFillColor("rgb(255, 66, 79)");
      this.pdf.doc.rect(x, y + 5, 8, 4, "FD");
      this.pdf.doc.text("< 200.00 [h]", x + 10, y + 8);

      this.pdf.doc.setDrawColor("rgb(255, 115, 124)");
      this.pdf.doc.setFillColor("rgb(255, 115, 124)");
      this.pdf.doc.rect(x, y + 10, 8, 4, "FD");
      this.pdf.doc.text(">= 200.00 [h]", x + 10, y + 13);

      this.pdf.doc.setDrawColor("rgb(255, 153, 160)");
      this.pdf.doc.setFillColor("rgb(255, 153, 160)");
      this.pdf.doc.rect(x, y + 15, 8, 4, "FD");
      this.pdf.doc.text(">= 230.00 [h]", x + 10, y + 18);

      this.pdf.doc.setDrawColor("rgb(45, 132, 179)");
      this.pdf.doc.setFillColor("rgb(45, 132, 179)");
      this.pdf.doc.rect(x, y + 20, 8, 4, "FD");
      this.pdf.doc.text(">= 250.00 [h]", x + 10, y + 23);
    },
    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(
          "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>