changeset 3296:fa7dc3f31ef3

available_fairwaydepth_vs_lnwl: POC etd.
author Thomas Junk <thomas.junk@intevation.de>
date Thu, 16 May 2019 16:23:19 +0200
parents a409608dd6b3
children 0358bf723769
files client/src/components/Main.vue client/src/components/Pane.vue client/src/components/fairway/AvailableFairwayDepthDialogue.vue client/src/components/fairway/AvailableFairwayDepthLNWL.vue client/src/components/paneSetups.js client/src/store/application.js
diffstat 6 files changed, 590 insertions(+), 1 deletions(-) [+]
line wrap: on
line diff
--- a/client/src/components/Main.vue	Thu May 16 15:36:11 2019 +0200
+++ b/client/src/components/Main.vue	Thu May 16 16:23:19 2019 +0200
@@ -97,6 +97,12 @@
           : ["w-50 h-100", "w-50 h-100"];
       }
 
+      if (this.paneSetup === "AVAILABLEFAIRWAYDEPTHLNWL") {
+        return [1, 3].includes(this.paneRotate)
+          ? ["w-100 h-50", "w-100 h-50"]
+          : ["w-50 h-100", "w-50 h-100"];
+      }
+
       if (this.paneSetup === "COMPARESURVEYS_FAIRWAYPROFILE") {
         return [1, 3].includes(this.paneRotate)
           ? ["wh-50", "wh-50", "w-100 h-50"]
--- a/client/src/components/Pane.vue	Thu May 16 15:36:11 2019 +0200
+++ b/client/src/components/Pane.vue	Thu May 16 16:23:19 2019 +0200
@@ -26,6 +26,8 @@
     Map: () => import("./map/Map"),
     Fairwayprofile: () => import("./fairway/Fairwayprofile"),
     AvailableFairwayDepth: () => import("./fairway/AvailableFairwayDepth"),
+    AvailableFairwayDepthLNWL: () =>
+      import("./fairway/AvailableFairwayDepthLNWL"),
     Waterlevel: () => import("./gauge/Waterlevel"),
     HydrologicalConditions: () => import("./gauge/HydrologicalConditions")
   }
--- a/client/src/components/fairway/AvailableFairwayDepthDialogue.vue	Thu May 16 15:36:11 2019 +0200
+++ b/client/src/components/fairway/AvailableFairwayDepthDialogue.vue	Thu May 16 16:23:19 2019 +0200
@@ -128,7 +128,16 @@
             :disabled="isComplete"
             class="btn btn-info btn-sm w-100"
           >
-            <translate>Available Fairway Depth</translate>
+            <translate>Available fairway depth</translate>
+          </button>
+        </div>
+        <div class="mt-3">
+          <button
+            @click="openFairwaydepthLNWL"
+            :disabled="isComplete"
+            class="btn btn-info btn-sm w-100"
+          >
+            <translate>Available fairway depth vs LNWL</translate>
           </button>
         </div>
       </div>
@@ -170,6 +179,34 @@
     };
   },
   methods: {
+    openFairwaydepthLNWL() {
+      this.loading = true;
+      this.$store
+        .dispatch("fairwayavailability/loadAvailableFairwayDepth", {
+          feature: this.selectedFairwayAvailabilityFeature,
+          from: this.from,
+          to: this.to,
+          frequency: this.frequency,
+          LOS: this.los
+        })
+        .then(() => {
+          this.$store.commit(
+            "application/paneSetup",
+            "AVAILABLEFAIRWAYDEPTHLNWL"
+          );
+        })
+        .catch(error => {
+          console.log(error);
+          const { status, data } = error.response;
+          displayError({
+            title: this.$gettext("Backend Error"),
+            message: `${status}: ${data.message || data}`
+          });
+        })
+        .finally(() => {
+          this.loading = false;
+        });
+    },
     openFairwaydepth() {
       this.loading = true;
       this.$store
@@ -197,6 +234,7 @@
     },
     close() {
       this.$store.commit("application/showFairwayDepth", false);
+      this.$store.commit("application/showFairwayDepthLNWL", false);
     },
     entrySelected() {
       if (this.type === this.$options.BOTTLENECKS) {
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/client/src/components/fairway/AvailableFairwayDepthLNWL.vue	Thu May 16 16:23:19 2019 +0200
@@ -0,0 +1,531 @@
+<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 legend" :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-1"
+          >
+            <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-1"
+          >
+            <translate>Export to PDF</translate>
+          </button>
+        </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";
+
+const hoursInDays = x => x / 24;
+
+export default {
+  mixins: [diagram, pdfgen],
+  components: {
+    DiagramLegend: () => import("@/components/DiagramLegend")
+  },
+  data() {
+    return {
+      containerId: "availablefairwaydepth",
+      loading: false,
+      width: 1000,
+      height: 600,
+      paddingRight: 100,
+      spaceBetween: 80,
+      labelPaddingTop: 15,
+      scalePaddingLeft: 50,
+      paddingTop: 10,
+      diagram: null,
+      yScale: null,
+      barsWidth: 60,
+      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",
+      "fwData",
+      "from",
+      "to",
+      "frequency",
+      "legend"
+    ]),
+    ...mapState("user", ["user"]),
+    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;
+    }
+  },
+  methods: {
+    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);
+      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);
+    },
+    legendStyle(index) {
+      if (index == 0) return `background-color: ${this.$options.COLORS.LDC};`;
+      if (index < 4)
+        return `background-color: ${this.$options.COLORS.REST[index - 1]};`;
+      return `background-color: ${this.$options.COLORS.HIGHEST};`;
+    },
+    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([-33, 33])
+        .range([this.dimensions.mainHeight - 30, 0]);
+      d3.select(".diagram-container svg").remove();
+      this.generateDiagramContainer();
+      this.drawBars();
+      this.drawScaleLabel();
+      this.drawScale();
+    },
+    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() {
+      const everyBar = this.diagram
+        .selectAll("g")
+        .data(this.fwData)
+        .enter()
+        .append("g")
+        .attr("transform", (d, i) => {
+          const dx = this.paddingRight + i * this.spaceBetween;
+          return `translate(${dx})`;
+        });
+      this.drawSingleBars(everyBar);
+      this.drawLabelPerBar(everyBar);
+    },
+    drawSingleBars(everyBar) {
+      this.drawLDC(everyBar);
+      this.drawHighestLevel(everyBar);
+      this.drawLowerLevels(everyBar);
+    },
+    drawLowerLevels(everyBar) {
+      everyBar
+        .selectAll("g")
+        .data(d => d.lowerLevels.reverse())
+        .enter()
+        .append("rect")
+        .attr("y", this.yScale(0))
+        .attr("height", d => {
+          return this.yScale(0) - this.yScale(hoursInDays(d));
+        })
+        .attr("width", this.barsWidth)
+        .attr("fill", (d, i) => {
+          return this.$options.COLORS.REST[i];
+        });
+    },
+    fnheight(name) {
+      return d => this.yScale(0) - this.yScale(hoursInDays(d[name]));
+    },
+    drawLDC(everyBar) {
+      const height = this.fnheight("ldc");
+      everyBar
+        .append("rect")
+        .attr("y", this.yScale(0))
+        .attr("height", height)
+        .attr("width", this.barsWidth)
+        .attr("transform", d => `translate(0 ${-1 * height(d)})`)
+        .attr("fill", this.$options.COLORS.LDC)
+        .attr("id", "ldc");
+    },
+    drawHighestLevel(everyBar) {
+      const height = this.fnheight("highestLevel");
+      everyBar
+        .append("rect")
+        .attr("y", this.yScale(0))
+        .attr("height", height)
+        .attr("width", this.barsWidth - 5)
+        .attr("transform", d => `translate(0 ${-1 * height(d)})`)
+        .attr("fill", this.$options.COLORS.HIGHEST);
+    },
+    drawLabelPerBar(everyBar) {
+      everyBar
+        .append("text")
+        .text(d => d.label)
+        .attr("y", this.yScale(0) + this.labelPaddingTop);
+    },
+    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: {
+    fwData() {
+      this.drawDiagram();
+    }
+  },
+  LEGEND: app.$gettext("Sum of days"),
+  COLORS: {
+    LDC: "#cdcdcd",
+    HIGHEST: "#3675ff",
+    REST: ["#782121", "#ff6c6c", "#ffaaaa"]
+  }
+};
+</script>
--- a/client/src/components/paneSetups.js	Thu May 16 15:36:11 2019 +0200
+++ b/client/src/components/paneSetups.js	Thu May 16 16:23:19 2019 +0200
@@ -18,6 +18,14 @@
   }
 };
 
+export const AVAILABLEFAIRWAYDEPTHLNWL = {
+  main,
+  availablefairwaydepth: {
+    id: "availablefairwaydepthlnwl",
+    component: "AvailableFairwayDepthLNWL"
+  }
+};
+
 export const COMPARESURVEYS_FAIRWAYPROFILE = {
   main,
   compare: { id: "compare-survey", component: "Map" },
--- a/client/src/store/application.js	Thu May 16 15:36:11 2019 +0200
+++ b/client/src/store/application.js	Thu May 16 16:23:19 2019 +0200
@@ -36,6 +36,7 @@
     showProfiles: false,
     showGauges: false,
     showFairwayDepth: false,
+    showFairwayDepthLNWL: false,
     contextBoxContent: null, // bottlenecks, imports, staging
     expandToolbar: false,
     countries: ["AT", "SK", "HU", "HR", "RS", "BiH", "BG", "RO", "UA"],
@@ -117,6 +118,9 @@
     showFairwayDepth: (state, show) => {
       state.showFairwayDepth = show;
     },
+    showFairwayDepthLNWL: (state, show) => {
+      state.showFairwayDepthLNWL = show;
+    },
     contextBoxContent: (state, context) => {
       state.contextBoxContent = context;
       if (context) {