view client/src/components/fairway/Fairwayprofile.vue @ 5095:e21cbb9768a2

Prevent duplicate fairway areas In principal, there can be only one or no fairway area at each point on the map. Since polygons from real data will often be topologically inexact, just disallow equal geometries. This will also help to avoid importing duplicates with concurrent imports, once the history of fairway dimensions will be preserved.
author Tom Gottfried <tom@intevation.de>
date Wed, 25 Mar 2020 18:10:02 +0100
parents b65898de11ad
children de86a96d55c3
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>
          <span class="fix-trans-space" style="display:inline;" v-translate
            >Water</span
          >
        </div>
        <div class="legend">
          <span
            :style="
              'width: 16px; height: 16px; background:' +
                this.getLayerStyle(1).fillColor +
                '; border: dotted 2px ' +
                this.getLayerStyle(1).strokeColor +
                '; background-clip: padding-box; box-sizing: content-box;'
            "
          ></span>
          <span class="fix-trans-space" style="display:inline;" v-translate
            >Fairway (LOS 1)</span
          >
        </div>
        <div class="legend">
          <span
            :style="
              'width: 16px; height: 16px; background:' +
                this.getLayerStyle(2).fillColor +
                '; border: dashed 2px ' +
                this.getLayerStyle(2).strokeColor +
                '; background-clip: padding-box; box-sizing: content-box;'
            "
          ></span>
          <span class="fix-trans-space" style="display:inline;" v-translate
            >Fairway (LOS 2)</span
          >
        </div>
        <div class="legend">
          <span
            :style="
              'width: 16px; height: 16px; background:' +
                this.getLayerStyle(3).fillColor +
                '; border: solid 2px ' +
                this.getLayerStyle(3).strokeColor +
                '; background-clip: padding-box; box-sizing: content-box;'
            "
          ></span>
          <span class="fix-trans-space" style="display:inline;" v-translate
            >Fairway (LOS 3)</span
          >
        </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>
          <span class="fix-trans-space" style="display:inline;" v-translate
            >Sediment</span
          >
        </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>
          <span class="fix-trans-space" style="display:inline;" v-translate
            >Sediment (Compare)</span
          >
        </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>
          <a
            @click="downloadImage('fairwaypng', title)"
            id="fairwaypng"
            class="btn btn-sm btn-info text-white d-block w-100 mt-2"
            :download="`${fileName}.png`"
          >
            <translate>Export as Image</translate>
          </a>
        </div>
      </DiagramLegend>
      <div
        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 v-if="!refWaterlevelValid">
          <translate>No valid reference waterlevel data available.</translate>
        </div>
        <div v-if="!waterlevelValid">
          <translate>No valid waterlevel 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";

const isNumber = value => {
  if (typeof value !== "number") {
    return false;
  }

  if (value !== Number(value)) {
    return false;
  }

  if (Number.isFinite(value) === false) {
    return false;
  }

  return true;
};

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",
      "minAlt",
      "maxAlt",
      "selectedWaterLevel",
      "depth",
      "useCustomDepth"
    ]),
    ...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));
      const waterlevelMeasurement =
        this.waterlevelValid && this.refWaterlevelValid
          ? `${this.$options.filters.waterlevel(this.waterlevel)} m`
          : this.$gettext("No valid value available");
      return `${this.$gettext("Fairwayprofile")}: ${
        this.selectedBottleneck
      } (${dates.join(
        ", "
      )}) WL: ${waterlevelLabel} ( ${waterlevelMeasurement} )`;
    },
    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() {
      if (!this.selectedSurvey) return 0;
      return this.selectedSurvey.waterlevel_value;
    },
    waterlevelValid() {
      return isNumber(this.waterlevel);
    },
    refWaterlevelValid() {
      return isNumber(this.refWaterlevel);
    },
    fileName() {
      return this.downloadFilename(
        this.$gettext("Fairwayprofile"),
        this.selectedBottleneck
      );
    }
  },
  watch: {
    depth() {
      if (!this.useCustomDepth) return;
      this.drawDiagram();
    },
    useCustomDepth() {
      this.drawDiagram();
    },
    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: {
    addLegendToCanvas(ctx, { width, height }) {
      let x = width / 12,
        y = height - 55;
      ctx.font = "12px sans-serif";
      ctx.textAlign = "start";

      ctx.beginPath();
      ctx.fillStyle = "#5995ff";
      ctx.strokeStyle = "#5995ff";
      ctx.arc(x, y, 8, 0, 2 * Math.PI);
      ctx.fill();
      ctx.stroke();
      ctx.fillStyle = "black";
      ctx.fillText(this.$gettext("Water"), x + 14, y + 5);
      ctx.closePath();

      ctx.beginPath();
      ctx.fillStyle = this.getLayerStyle(1).fillColor;
      ctx.strokeStyle = this.getLayerStyle(1).strokeColor;
      ctx.arc(x, (y += 20), 8, 0, 2 * Math.PI);
      ctx.setLineDash([0.8], 0);
      ctx.fill();
      ctx.stroke();
      ctx.fillStyle = "black";
      ctx.fillText(this.$gettext("Fairway (LOS 1)"), x + 14, y + 5);
      ctx.closePath();

      ctx.beginPath();
      ctx.fillStyle = this.getLayerStyle(2).fillColor;
      ctx.strokeStyle = this.getLayerStyle(2).strokeColor;
      ctx.arc(x, (y += 20), 8, 0, 2 * Math.PI);
      ctx.setLineDash([1.8], 0);
      ctx.fill();
      ctx.stroke();
      ctx.fillStyle = "black";
      ctx.fillText(this.$gettext("Fairway (LOS 2)"), x + 14, y + 5);
      ctx.closePath();

      ctx.beginPath();
      ctx.fillStyle = this.getLayerStyle(3).fillColor;
      ctx.strokeStyle = this.getLayerStyle(3).strokeColor;
      ctx.arc(x, (y += 20), 8, 0, 2 * Math.PI);
      ctx.setLineDash([]);
      ctx.fill();
      ctx.stroke();
      ctx.fillStyle = "black";
      ctx.fillText(this.$gettext("Fairway (LOS 3)"), x + 14, y + 5);
      ctx.closePath();

      ctx.beginPath();
      ctx.fillStyle = "#4a2e06";
      ctx.strokeStyle = "black";
      ctx.arc(x, (y += 20), 8, 0, 2 * Math.PI);
      ctx.setLineDash([]);
      ctx.fill();
      ctx.stroke();
      ctx.fillStyle = "black";
      ctx.fillText(this.$gettext("Sediment"), x + 14, y + 5);
      ctx.closePath();

      ctx.beginPath();
      ctx.fillStyle = "rgba(74, 47, 6, 0.6)";
      ctx.strokeStyle = "#943007";
      ctx.arc(x, (y += 20), 8, 0, 2 * Math.PI);
      ctx.setLineDash([]);
      ctx.fill();
      ctx.stroke();
      ctx.fillStyle = "black";
      ctx.fillText(this.$gettext("Sediment (compare)"), x + 14, y + 5);
      ctx.closePath();
    },
    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 strokeColor = style.getStroke().getColor();
      let strokeDash = style.getStroke().getLineDash();
      return { fillColor, strokeColor, 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(error => {
            let message = "Backend not reachable";
            if (error.response) {
              const { status, data } = error.response;
              message = `${status}: ${data.message || data}`;
            }
            displayError({
              title: this.$gettext("Backend Error"),
              message: message
            });
          });
      }
    },
    downloadPDF() {
      let fairwayInfo =
        this.selectedBottleneck + " (" + this.selectedSurvey.date_info + ")";
      this.generatePDF({
        templateData: this.templateData,
        diagramTitle: fairwayInfo
      });
      this.pdf.doc.save(this.fileName + ".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, this.$gettext("Water"));

      const toRGB = s => {
        let [, r, g, b] = s.match(/.*?(\d+), (\d+), (\d+), .*/);
        const toHex = n => {
          let val = parseInt(n).toString(16);
          if (val.length === 1) return `0${val}`;
          return val;
        };
        return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
      };

      const los1Color = toRGB(this.getLayerStyle(1).strokeColor);
      const los1Fill = toRGB(this.getLayerStyle(1).fillColor);
      const los2Color = toRGB(this.getLayerStyle(2).strokeColor);
      const los2Fill = toRGB(this.getLayerStyle(2).fillColor);
      const los3Color = toRGB(this.getLayerStyle(3).strokeColor);
      const los3Fill = toRGB(this.getLayerStyle(3).fillColor);

      this.pdf.doc.setLineDashPattern([0.8], 0);
      this.pdf.doc.setDrawColor(los1Color);
      this.pdf.doc.setFillColor(los1Fill);
      this.pdf.doc.circle(x, y + 5, 2, "FD");
      this.pdf.doc.text(x + 3, y + 6, this.$gettext("Fairway (LOS 1)"));

      this.pdf.doc.setLineDashPattern([1.8], 0);
      this.pdf.doc.setFillColor(los2Fill);
      this.pdf.doc.setDrawColor(los2Color);
      this.pdf.doc.circle(x, y + 10, 2, "FD");
      this.pdf.doc.text(x + 3, y + 11, this.$gettext("Fairway (LOS 2)"));

      this.pdf.doc.setLineDashPattern([], 0);
      this.pdf.doc.setFillColor(los3Fill);
      this.pdf.doc.setDrawColor(los3Color);
      this.pdf.doc.circle(x, y + 15, 2, "FD");
      this.pdf.doc.text(x + 3, y + 16, this.$gettext("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, this.$gettext("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, this.$gettext("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 }) {
      if (!this.waterlevelValid || !this.refWaterlevelValid) return;
      let svg = d3.select(element).append("svg");
      svg.attr("width", "100%");
      svg.attr("height", "100%");
      svg
        .append("g")
        .append("rect")
        .attr("width", "100%")
        .attr("height", "100%")
        .attr("fill", "#ffffff");
      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 });
    },
    /**
     * Draws Fairway rectangle
     *
     * start       end
     *  ____________
     * [___________] customDepth | referenceDepth
     *
     * Starting point is the 0 line of the diagram
     *
     */
    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;
          const referenceDepth =
            this.maxAlt * 1.1 + (this.waterlevel - this.refWaterlevel) / 100;
          let customdepth =
            this.depth < referenceDepth ? this.depth : referenceDepth;
          let fairwayArea = d3
            .area()
            .x(function(d) {
              return xScale(d.x);
            })
            .y0(yScaleRight(0))
            .y1(function(d) {
              return yScaleRight(d.y);
            });
          let strokColor = this.getLayerStyle(data.los).strokeColor;
          // Convert stroke value to rgb() and opacity to pass them separately
          let [r, g, b, opacity] = strokColor
            .substring(5, strokColor.length - 1)
            .split(",");
          let rgb = `rgb(${r}, ${g}, ${b})`;
          graph
            .append("path")
            .datum([
              { x: startPoint, y: this.useCustomDepth ? customdepth : depth },
              { x: endPoint, y: this.useCustomDepth ? customdepth : depth }
            ])
            .attr("fill", `${this.getLayerStyle(data.los).fillColor}`)
            .attr("stroke", rgb)
            .attr("stroke-opacity", opacity)
            .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(this.$gettext("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(this.$gettext("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(this.$gettext("Width [m]"));
    },
    generateScalesAndGraph({ svg, height, width, dimensions, offsetY }) {
      let xScale = d3
        .scaleLinear()
        .domain([0, this.totalLength])
        .rangeRound([0, width]);
      // Upper limit is relevant for the extension of the y-Axis
      // If there is no measure above the waterlevel we choose a cosmetic value of this.maxAlt *0.1
      // to get a bit of space above the waterlevel.
      // Otherwise we take the maximum measured value
      const upperLimit =
        this.minAlt > 0 ? this.maxAlt * 0.1 : Math.abs(this.minAlt);
      // In order to draw positive values downwards, we switch both values in the
      // domain definition [min, max] => [max, min] has the desired effect.
      let yScaleRight = d3
        .scaleLinear()
        .domain([
          this.maxAlt * 1.1 + (this.waterlevel - this.refWaterlevel) / 100,
          -upperLimit
        ])
        .rangeRound([height, 0]);
      // This definition is accordingly but uses values * 100 for the tickformatter
      let yScaleLeft = d3
        .scaleLinear()
        .domain([
          this.waterlevel -
            (this.maxAlt * 110 + (this.waterlevel - this.refWaterlevel)),
          this.waterlevel + upperLimit * 100
        ])
        .rangeRound([height, 0]);

      let xAxis = d3
        .axisBottom(xScale)
        .tickSizeOuter(0)
        .ticks(5);
      let yAxisRight = d3
        .axisRight()
        .tickSizeOuter(0)
        .tickSizeInner(5)
        .scale(yScaleRight);
      let yAxisLeft = d3
        .axisLeft()
        .tickSizeOuter(0)
        .tickSizeInner(5)
        .scale(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 };
    },
    /**
     * Draws a rectangle for the waterlevel
     * (0,0)        (totalLength,0)
     *  ____________
     * [____________]
     * (0,height)   (totalLength, height)
     */
    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})`);
    },
    /**
     *
     * Draws the ground level
     *
     * (x, y + ΔWaterlevel)
     *
     * ΔWaterlevel is the difference between the current waterlevel and the reference level
     *
     * Is the current level higher as the reference level ΔWaterlevel is positive, which in
     * turn means that the distance between the surface and the ground is increased.
     *
     * Is the current level below the reference level ΔWaterlevel is negative, which in turn means
     * that the distance between the surface and the ground is decreased.
     *
     */
    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 + (this.waterlevel - this.refWaterlevel) / 100)
          );
        let profileArea = d3
          .area()
          .x(function(d) {
            return xScale(d.x);
          })
          .y0(height)
          .y1(d =>
            yScaleRight(d.y + (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(error => {
        let message = "Backend not reachable";
        if (error.response) {
          const { status, data } = error.response;
          message = `${status}: ${data.message || data}`;
        }
        displayError({
          title: this.$gettext("Backend Error"),
          message: message
        });
      });
  },
  updated() {
    this.drawDiagram();
  },
  destroyed() {
    window.removeEventListener("resize", this.resizeListenerFunction);
  }
};
</script>