view client/src/components/fairway/Fairwayprofile.vue @ 4231:6f31a99cd92d

clinet: fix translations process and update source strings * move strings for translations from *.po files to the component itself to let gettext() mark only the strings without the html elements. (make makemessages complains to have html elements in the .po files and stops the process).
author Fadi Abbud <fadi.abbud@intevation.de>
date Wed, 21 Aug 2019 11:13:12 +0200
parents a41fd26c1644
children 51aae07d5f7b
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" v-if="openLayersMap()">
      <DiagramLegend>
        <div class="legend">
          <span
            style="background-color: #5995ff; width: 20px; height: 20px;"
          ></span>
          Water
        </div>
        <div class="legend">
          <span
            :style="
              'width: 16px; height: 16px; background-color: rgba(' +
                this.getLayerStyle(1).fillColor.join(',') +
                ',' +
                this.getLayerStyle(1).fillOpacity +
                '); border: dotted 2px rgba(' +
                this.getLayerStyle(1).strokeColor.join(',') +
                ',' +
                this.getLayerStyle(1).strokeOpacity +
                '); background-clip: padding-box; box-sizing: content-box;'
            "
          ></span>
          Fairway (LOS 1)
        </div>
        <div class="legend">
          <span
            :style="
              'width: 16px; height: 16px; background-color: rgba(' +
                this.getLayerStyle(2).fillColor.join(',') +
                ',' +
                this.getLayerStyle(2).fillOpacity +
                '); border: dashed 2px rgba(' +
                this.getLayerStyle(2).strokeColor.join(',') +
                ',' +
                this.getLayerStyle(2).strokeOpacity +
                '); background-clip: padding-box; box-sizing: content-box;'
            "
          ></span>
          Fairway (LOS 2)
        </div>
        <div class="legend">
          <span
            :style="
              'width: 16px; height: 16px; background-color: rgba(' +
                this.getLayerStyle(3).fillColor.join(',') +
                ',' +
                this.getLayerStyle(3).fillOpacity +
                '); border: solid 2px rgba(' +
                this.getLayerStyle(3).strokeColor.join(',') +
                ',' +
                this.getLayerStyle(3).strokeOpacity +
                '); background-clip: padding-box; box-sizing: content-box;'
            "
          ></span>
          Fairway (LOS 3)
        </div>
        <div class="legend">
          <span
            style="width: 14px; height: 14px; background-color: #4a2f06; border: solid 3px black; background-clip: padding-box; box-sizing: content-box;"
          ></span>
          Sediment
        </div>
        <div class="legend">
          <span
            style="width: 14px; height: 14px; background-color: rgba(74, 47, 6, 0.6); border: solid 3px #943007; background-clip: padding-box; box-sizing: content-box;"
          ></span>
          Sediment (Compare)
        </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="pdfContainer"
        id="pdfContainer"
        class="d-flex flex-fill justify-content-center align-items-center diagram-container position-relative"
      >
        <div class="direction-indicator"></div>
        <div v-if="!fairwayData">
          <translate>No data available.</translate>
        </div>
      </div>
    </div>
  </div>
</template>

<style lang="sass" scoped>
.direction-indicator
  width: 70px
  height: 0
  border-top: dashed 2px #333
  position: absolute
  bottom: 50px
  left: 115px
  margin-left: -35px
  &::after
    content: ""
    width: 0
    height: 0
    border-width: 10px
    border-top-width: 5px
    border-bottom-width: 5px
    border-style: solid
    border-color: transparent
    border-left-color: #333
    position: absolute
    right: -20px
    top: -6px
</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 { mapState, mapGetters } from "vuex";
import debounce from "debounce";
import { diagram, pdfgen, templateLoader } from "@/lib/mixins";
import { HTTP } from "@/lib/http";
import { displayError } from "@/lib/errors";
import { defaultDiagramTemplate } from "@/lib/DefaultDiagramTemplate";

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

export default {
  mixins: [diagram, pdfgen, templateLoader],
  name: "fairwayprofile",
  components: {
    DiagramLegend: () => import("@/components/DiagramLegend")
  },
  data() {
    return {
      resizeListenerFunction: null,
      width: null,
      height: null,
      form: {
        template: null
      },
      templates: [],
      defaultTemplate: defaultDiagramTemplate,
      pdf: {
        doc: null,
        width: 32,
        height: 297
      },
      templateData: null
    };
  },
  computed: {
    ...mapGetters("map", ["openLayersMap"]),
    ...mapGetters("fairwayprofile", ["totalLength"]),
    ...mapState("fairwayprofile", [
      "additionalSurvey",
      "currentProfile",
      "startPoint",
      "endPoint",
      "fairwayData",
      "maxAlt",
      "selectedWaterLevel"
    ]),
    ...mapState("bottlenecks", ["selectedSurvey", "selectedBottleneck"]),
    ...mapState("application", ["paneSetup"]),
    title() {
      let dates = [this.selectedSurvey.date_info];
      let waterlevelLabel =
        this.selectedWaterLevel === "ref"
          ? this.selectedSurvey.depth_reference
          : "Current";
      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(
        ", "
      )}) WL: ${waterlevelLabel} (${this.$options.filters.waterlevel(
        this.waterlevel
      )} m)`;
    },
    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;
    },
    bottleneck() {
      return this.openLayersMap()
        .getLayer("BOTTLENECKS")
        .getSource()
        .getFeatures()
        .find(f => f.get("objnam") === this.selectedBottleneck);
    },
    waterlevel() {
      return this.selectedWaterLevel === "ref"
        ? this.refWaterlevel
        : this.bottleneck.get("gm_waterlevel");
    },
    refWaterlevel() {
      return this.selectedSurvey.waterlevel_value;
    }
  },
  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");
    },
    getLayerStyle(los) {
      let style = this.openLayersMap()
        .getLayer("FAIRWAYDIMENSIONSLOS" + los)
        .getStyle()()[0];

      // use spread operator to clone arrays
      let fillColor = [...style.getFill().getColor()];
      let fillOpacity = fillColor.pop();
      let strokeColor = [...style.getStroke().getColor()];
      let strokeOpacity = strokeColor.pop();
      let strokeDash = style.getStroke().getLineDash();

      return { fillColor, fillOpacity, strokeColor, strokeOpacity, strokeDash };
    },
    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 fairwayInfo =
        this.selectedBottleneck + " (" + this.selectedSurvey.date_info + ")";

      this.generatePDF({
        templateData: this.templateData,
        diagramTitle: fairwayInfo
      });

      this.pdf.doc.save(
        this.title.replace(/\s/g, "_").replace(/[():,]/g, "") + ".pdf"
      );
    },

    // Diagram legend
    addDiagramLegend(position, offset, color) {
      let x = offset.x,
        y = offset.y;
      this.pdf.doc.setFontSize(10);
      let width =
        (this.pdf.doc.getStringUnitWidth("Sediment (Compare)") * 10) /
          (72 / 25.6) +
        4;
      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(8);
      }

      this.pdf.doc.setTextColor(color);
      this.pdf.doc.setDrawColor("#5995ff");
      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.setLineDashPattern([0.8], 0);
      this.pdf.doc.setDrawColor("#0000ff");
      this.pdf.doc.setFillColor("#fcfacc");
      this.pdf.doc.circle(x, y + 5, 2, "FD");
      this.pdf.doc.text(x + 3, y + 6, "Fairway (LOS 1)");

      this.pdf.doc.setLineDashPattern([1.8], 0);
      this.pdf.doc.setFillColor("#fdfce5");
      this.pdf.doc.circle(x, y + 10, 2, "FD");
      this.pdf.doc.text(x + 3, y + 11, "Fairway (LOS 2)");

      this.pdf.doc.setLineDashPattern([], 0);
      this.pdf.doc.setFillColor("#ffffff");
      this.pdf.doc.circle(x, y + 15, 2, "FD");
      this.pdf.doc.text(x + 3, y + 16, "Fairway (LOS 3)");

      this.pdf.doc.setDrawColor("black");
      this.pdf.doc.setFillColor("#4a2e06");
      this.pdf.doc.circle(x, y + 20, 2, "FD");
      this.pdf.doc.text(x + 3, y + 21, "Sediment");

      this.pdf.doc.setDrawColor("#943007");
      this.pdf.doc.setFillColor("#928269");
      this.pdf.doc.circle(x, y + 25, 2, "FD");
      this.pdf.doc.text(x + 3, y + 26, "Sediment (Compare)");
    },
    getPrintLayout(svgHeight, svgWidth) {
      return {
        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)
        }
      };
    },
    drawDiagram() {
      d3.select(".diagram-container svg").remove();
      this.scaleFairwayProfile();
      if (!this.height || !this.width) return; // do not try to render when height and width are unknown
      const layout = this.getPrintLayout(this.height, this.width);
      this.renderTo({
        element: ".diagram-container",
        dimensions: this.getDimensions({
          svgWidth: this.width,
          svgHeight: this.height,
          ...layout
        })
      });
    },
    renderTo({ element, dimensions }) {
      let svg = d3.select(element).append("svg");
      svg.attr("width", "100%");
      svg.attr("height", "100%");
      const width = dimensions.width;
      const height = dimensions.mainHeight;
      const offsetY = 15;
      const currentData = this.currentData;
      const additionalData = this.additionalData;
      const { xScale, yScaleRight, graph } = this.generateScalesAndGraph({
        svg,
        height,
        width,
        dimensions,
        offsetY
      });
      this.drawWaterlevel({ graph, xScale, yScaleRight, height, offsetY });
      this.drawLabels({ graph, dimensions });
      if (currentData) {
        this.drawProfile({
          graph,
          xScale,
          yScaleRight,
          currentData,
          height,
          color: GROUND_COLOR,
          strokeColor: "black",
          opacity: 1,
          offsetY
        });
      }
      if (additionalData) {
        this.drawProfile({
          graph,
          xScale,
          yScaleRight,
          currentData: additionalData,
          height,
          color: GROUND_COLOR,
          strokeColor: "#943007",
          opacity: 0.6,
          offsetY
        });
      }
      this.drawFairway({ graph, xScale, yScaleRight, offsetY });
    },
    drawFairway({ graph, xScale, yScaleRight, offsetY }) {
      if (this.fairwayData === undefined) {
        return;
      }
      for (let data of this.fairwayData) {
        data.coordinates.forEach(coordinates => {
          const [startPoint, endPoint, depth] = coordinates;
          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",
              `#${this.getLayerStyle(data.los)
                .fillColor.map(x => {
                  if (x < 10) return "0" + x.toString(16);
                  return x.toString(16);
                })
                .join("")}`
            )
            .attr("fill-opacity", this.getLayerStyle(data.los).fillOpacity)
            .attr(
              "stroke",
              `#${this.getLayerStyle(data.los)
                .strokeColor.map(x => {
                  if (x < 10) return "0" + x.toString(16);
                  return x.toString(16);
                })
                .join("")}`
            )
            .attr("stroke-opacity", this.getLayerStyle(data.los).strokeOpacity)
            .attr("stroke-dasharray", this.getLayerStyle(data.los).strokeDash)
            .attr("d", fairwayArea)
            .attr("transform", `translate(0 ${-offsetY})`);
        });
      }
    },
    drawLabels({ graph, dimensions }) {
      graph
        .append("text")
        .attr("transform", ["rotate(-90)"])
        .attr("y", dimensions.width + Math.floor(0.06 * dimensions.width))
        .attr("x", -dimensions.mainHeight / 2)
        .attr("fill", "black")
        .style("text-anchor", "middle")
        .text("Depth [m]");
      graph
        .append("text")
        .attr("transform", ["rotate(-90)"])
        .attr("y", -1 * Math.floor(0.065 * dimensions.width))
        .attr("x", -dimensions.mainHeight / 2)
        .attr("fill", "black")
        .style("text-anchor", "middle")
        .text("Waterlevel [m]");
      graph
        .append("text")
        .attr("y", 0)
        .attr("x", 0)
        .attr("dy", "1em")
        .attr("fill", "black")
        .style("text-anchor", "middle")
        .attr("transform", [
          `translate(${dimensions.width / 2} ${dimensions.mainHeight})`,
          "rotate(0)"
        ])
        .text("Width [m]");
    },
    generateScalesAndGraph({ svg, height, width, dimensions, offsetY }) {
      let xScale = d3
        .scaleLinear()
        .domain([0, this.totalLength])
        .rangeRound([0, width]);

      let yScaleRight = d3
        .scaleLinear()
        .domain([
          this.maxAlt * 1.1 +
            Math.abs(this.waterlevel - this.refWaterlevel) / 100,
          -(this.maxAlt * 0.1)
        ])
        .rangeRound([height, 0]);

      let yScaleLeft = d3
        .scaleLinear()
        .domain([
          this.waterlevel -
            (this.maxAlt * 100 +
              Math.abs(this.waterlevel - this.refWaterlevel)),
          this.waterlevel + this.maxAlt * 0.1 * 100
        ])
        .rangeRound([height, 0]);

      let xAxis = d3.axisBottom(xScale).ticks(5);
      let yAxisRight = d3.axisRight(yScaleRight);
      let yAxisLeft = d3
        .axisLeft(yScaleLeft)
        .tickFormat(d => this.$options.filters.waterlevel(d));

      let graph = svg
        .append("g")
        .attr(
          "transform",
          "translate(" +
            dimensions.mainMargin.left +
            "," +
            dimensions.mainMargin.top +
            ")"
        );
      graph
        .append("g")
        .attr("transform", `translate(0 ${height - offsetY})`)
        .call(xAxis)
        .selectAll(".tick text")
        .attr("fill", "black")
        .select(function() {
          return this.parentNode;
        })
        .selectAll(".tick line")
        .attr("stroke", "black");
      graph
        .append("g")
        .attr("transform", `translate(${width} ${-offsetY})`)
        .call(yAxisRight)
        .selectAll(".tick text")
        .attr("fill", "black")
        .select(function() {
          return this.parentNode;
        })
        .selectAll(".tick line")
        .attr("stroke", "black");
      graph
        .append("g")
        .attr("transform", `translate(0 ${-offsetY})`)
        .call(yAxisLeft)
        .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, offsetY }) {
      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", "transparent")
        .attr("d", waterArea)
        .attr("transform", `translate(0 ${-offsetY})`);
    },
    drawProfile({
      graph,
      xScale,
      yScaleRight,
      currentData,
      height,
      color,
      strokeColor,
      opacity,
      offsetY
    }) {
      for (let part of currentData) {
        let profileLine = d3
          .line()
          .x(d => {
            return xScale(d.x);
          })
          .y(d =>
            yScaleRight(
              d.y + Math.abs(this.waterlevel - this.refWaterlevel) / 100
            )
          );
        let profileArea = d3
          .area()
          .x(function(d) {
            return xScale(d.x);
          })
          .y0(height)
          .y1(d =>
            yScaleRight(
              d.y + Math.abs(this.waterlevel - this.refWaterlevel) / 100
            )
          );
        graph
          .append("path")
          .datum(part)
          .attr("fill", color)
          .attr("stroke", "transparent")
          .attr("fill-opacity", opacity)
          .attr("d", profileArea)
          .attr("transform", `translate(0 ${-offsetY})`);
        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)
          .attr("transform", `translate(0 ${-offsetY})`);
      }
    },
    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() {
    this.resizeListenerFunction = debounce(this.drawDiagram, 100);
    window.addEventListener("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}`
        });
      });
  },
  updated() {
    this.drawDiagram();
  },
  destroyed() {
    window.removeEventListener("resize", this.resizeListenerFunction);
  }
};
</script>