view client/src/components/fairway/Fairwayprofile.vue @ 2503:51dbcbf11c5f critical-bottlenecks

client: addendum for e13daf439068 Of course, as you'd expect, this only solves the problem if you don't care about significant changes in the tables sorting behavior. To point out the difference this commit shows the other way to solve the problem without changing the tables behavior.
author Markus Kottlaender <markus@intevation.de>
date Mon, 04 Mar 2019 16:28:49 +0100
parents e2f05ac81471
children 3c17d401fbd4
line wrap: on
line source

<template>
  <div :class="['position-relative', { show: showSplitscreen }]">
    <div class="profile bg-white position-relative d-flex flex-column">
      <div
        class="d-flex flex-row justify-content-between align-items-center border-bottom position-relative"
      >
        <div class="d-flex flex-row align-items-center position-absolute">
          <small class="text-muted px-2 text-right" style="line-height: 1rem;">
            <translate v-if="availableWaterlevels.length > 1"
              >Available Waterlevels:</translate
            >
            <translate v-else>Waterlevel:</translate>
          </small>
          <select
            class="form-control pl-1"
            v-model="currentLevel"
            v-if="availableWaterlevels.length > 1"
            style="width: 100px; height: 30px; font-size: 80%;"
          >
            <option
              v-for="level in availableWaterlevels"
              :value="level"
              :key="level"
            >
              {{ formatSurveyDate(level) }}
            </option>
          </select>
          <small v-else>{{ formatSurveyDate(currentLevel) }}</small>
        </div>
        <div class="flex-row mr-auto ml-auto">
          <h5
            class="headline mb-0 py-2"
            v-if="selectedBottleneck && selectedSurvey"
          >
            {{ selectedBottleneck }} ({{ selectedSurvey.date_info }})
          </h5>
        </div>
        <div>
          <button
            class="rounded-bottom bg-white border-0 splitscreen-toggle"
            @click="$store.commit('application/showSplitscreen', false)"
            v-if="showSplitscreen"
          >
            <font-awesome-icon icon="angle-down" />
          </button>
          <button
            class="rounded-bottom bg-white border-0 clear-selection"
            @click="$store.dispatch('fairwayprofile/clearSelection')"
            v-if="showSplitscreen"
          >
            <font-awesome-icon icon="times" class="pointer" />
          </button>
        </div>
      </div>
      <div class="d-flex flex-fill">
        <div
          class="loading d-flex justify-content-center align-items-center"
          v-if="surveysLoading || profileLoading"
        >
          <font-awesome-icon icon="spinner" spin />
        </div>
        <div class="fairwayprofile m-3 mt-0 bg-white flex-grow-1"></div>
      </div>
    </div>
  </div>
</template>

<style lang="scss" scoped>
.profile {
  width: 100vw;
  height: 0;
  overflow: hidden;
  z-index: 2;
}

.splitscreen-toggle,
.clear-selection {
  width: 2rem;
  height: 2rem;
  margin-top: 8px;
  z-index: 3;
  outline: none;
}

.splitscreen-toggle svg path,
.clear-selection svg path {
  fill: #666;
}

.splitscreen-toggle {
  right: 2.5rem;
}

.clear-selection {
  right: 0.5rem;
}

.show .profile {
  height: 50vh;
}

.loading {
  background: rgba(255, 255, 255, 0.96);
  position: absolute;
  z-index: 99;
  top: 0;
  right: 0;
  bottom: 0;
  left: 0;
}
</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>
 */
import * as d3 from "d3";
import { mapState, mapGetters } from "vuex";
import debounce from "debounce";
import { formatSurveyDate } from "@/lib/date.js";

const GROUND_COLOR = "#4A2F06";

export default {
  name: "fairwayprofile",
  data() {
    return {
      coordinatesInput: "",
      coordinatesSelect: null,
      cutLabel: "",
      showLabelInput: false,
      width: null,
      height: null,
      margin: {
        top: 20,
        right: 40,
        bottom: 30,
        left: 40
      }
    };
  },
  computed: {
    ...mapGetters("fairwayprofile", ["totalLength"]),
    ...mapState("application", ["showSplitscreen"]),
    ...mapState("fairwayprofile", [
      "additionalSurvey",
      "currentProfile",
      "endPoint",
      "fairwayData",
      "minAlt",
      "maxAlt",
      "profileLoading",
      "referenceWaterLevel",
      "selectedWaterLevel",
      "startPoint",
      "waterLevels"
    ]),
    ...mapState("bottlenecks", [
      "selectedBottleneck",
      "selectedSurvey",
      "surveysLoading"
    ]),
    relativeWaterLevelDelta() {
      return this.selectedWaterLevel.value - this.referenceWaterLevel;
    },
    currentLevel: {
      get() {
        return this.selectedWaterLevel.date;
      },
      set(value) {
        this.$store.commit("fairwayprofile/setSelectedWaterLevel", value);
      }
    },
    availableWaterlevels() {
      return Object.keys(this.waterLevels);
    },
    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;
    },
    waterColor() {
      return "#005DFF";
    },
    xScale() {
      return [0, this.totalLength];
    },
    yScaleRight() {
      //ToDO calcReleativeDepth(this.maxAlt) to get the
      // maximum depth according to the actual waterlevel
      // additionally: take the one which is higher reference or current waterlevel
      const DELTA = this.maxAlt * 1.1 - this.maxAlt;
      return [this.maxAlt * 1 + DELTA, -DELTA];
    }
  },
  watch: {
    currentData() {
      this.drawDiagram();
    },
    additionalData() {
      this.drawDiagram();
    },
    width() {
      this.drawDiagram();
    },
    height() {
      this.drawDiagram();
    },
    waterLevels() {
      this.drawDiagram();
    },
    selectedWaterLevel() {
      this.drawDiagram();
    },
    fairwayData() {
      this.drawDiagram();
    }
  },
  methods: {
    formatSurveyDate(value) {
      return formatSurveyDate(value);
    },
    calcRelativeDepth(depth) {
      /* takes a depth value and substracts the delta of the relative waterlevel
       * say the reference level is above the current level, the ground is nearer,
       * thus, the depth is lower.
       *
       * E.g.:
       *
       * Reference waterlevel 5m, current 4m => delta = -1m
       * If the distance to the ground was 3m from the 5m mark
       * it is now only 2m from the current waterlevel.
       *
       *  Vice versa:
       *
       *  If the reference level is 5m and the current 6m => delta = +1m
       *  The ground is one meter farer away from the current waterlevel
       *
       */
      return depth - this.relativeWaterLevelDelta;
    },
    drawDiagram() {
      this.coordinatesSelect = null;
      const chartDiv = document.querySelector(".fairwayprofile");
      d3.select(".fairwayprofile svg").remove();
      this.scaleFairwayProfile();
      let svg = d3.select(chartDiv).append("svg");
      svg.attr("width", this.width);
      svg.attr("height", this.height);
      const width = this.width - this.margin.right - 1.5 * this.margin.left;
      const height = this.height - this.margin.top - 2 * this.margin.bottom;
      const currentData = this.currentData;
      const additionalData = this.additionalData;
      const { xScale, yScaleRight, graph } = this.generateCoordinates(
        svg,
        height,
        width
      );
      if (!this.height || !this.width) return; // do not try to render when height and width are unknown
      this.drawWaterlevel({ graph, xScale, yScaleRight, height });
      this.drawLabels({ graph, height });
      if (currentData) {
        this.drawProfile({
          graph,
          xScale,
          yScaleRight,
          currentData,
          height,
          color: GROUND_COLOR,
          strokeColor: "black",
          opacity: 1
        });
      }
      if (additionalData) {
        this.drawProfile({
          graph,
          xScale,
          yScaleRight,
          currentData: additionalData,
          height,
          color: GROUND_COLOR,
          strokeColor: "#943007",
          opacity: 0.6
        });
      }
      this.drawFairway({ graph, xScale, yScaleRight });
    },
    drawFairway({ graph, xScale, yScaleRight }) {
      if (this.fairwayData === undefined) {
        return;
      }
      for (let data of this.fairwayData) {
        const [startPoint, endPoint, depth] = data.coordinates[0];
        const style = data.style();
        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", "#002AFF")
          .attr("fill-opacity", 0.65)
          .attr("stroke", style[0].getStroke().getColor())
          .attr("d", fairwayArea);
      }
    },
    drawLabels({ graph, height }) {
      graph
        .append("text")
        .attr("transform", ["rotate(-90)"])
        .attr("y", this.width - 60)
        .attr("x", -(this.height - this.margin.top - this.margin.bottom) / 2)
        .attr("dy", "1em")
        .attr("fill", "black")
        .style("text-anchor", "middle")
        .text("Depth [m]");
      graph
        .append("text")
        .attr("y", 0 - this.margin.left)
        .attr("x", 0 - height / 4)
        .attr("dy", "1em")
        .attr("fill", "black")
        .style("text-anchor", "middle")
        .attr("transform", [
          "translate(" + this.width / 2 + "," + this.height + ")",
          "rotate(0)"
        ])
        .text("Width [m]");
    },
    generateCoordinates(svg, height, width) {
      let xScale = d3
        .scaleLinear()
        .domain(this.xScale)
        .rangeRound([0, width]);

      xScale.ticks(5);

      let yScaleRight = d3
        .scaleLinear()
        .domain(this.yScaleRight)
        .rangeRound([height, 0]);

      let xAxis = d3.axisBottom(xScale);
      let yAxis2 = d3.axisRight(yScaleRight);
      let graph = svg
        .append("g")
        .attr(
          "transform",
          "translate(" + this.margin.left + "," + this.margin.top + ")"
        );
      graph
        .append("g")
        .attr("transform", "translate(0," + height + ")")
        .call(xAxis.ticks(5));
      graph
        .append("g")
        .attr("transform", "translate(" + width + ",0)")
        .call(yAxis2);
      return { xScale, yScaleRight, graph };
    },
    drawWaterlevel({ graph, xScale, yScaleRight, height }) {
      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", this.waterColor)
        .attr("stroke", this.waterColor)
        .attr("d", waterArea);
    },
    drawProfile({
      graph,
      xScale,
      yScaleRight,
      currentData,
      height,
      color,
      strokeColor,
      opacity
    }) {
      for (let part of currentData) {
        let profileLine = d3
          .line()
          .x(d => {
            return xScale(d.x);
          })
          .y(d => {
            return yScaleRight(d.y);
          });
        let profileArea = d3
          .area()
          .x(function(d) {
            return xScale(d.x);
          })
          .y0(height)
          .y1(function(d) {
            return yScaleRight(d.y);
          });
        graph
          .append("path")
          .datum(part)
          .attr("fill", color)
          .attr("stroke", color)
          .attr("stroke-width", 3)
          .attr("stroke-opacity", opacity)
          .attr("fill-opacity", opacity)
          .attr("d", profileArea);
        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", opacity)
          .attr("d", profileLine);
      }
    },
    scaleFairwayProfile() {
      if (!document.querySelector(".fairwayprofile")) return;
      const clientHeight = document.querySelector(".fairwayprofile")
        .clientHeight;
      const clientWidth = document.querySelector(".fairwayprofile").clientWidth;
      if (!clientHeight || !clientWidth) return;
      this.height = clientHeight;
      this.width = clientWidth;
    }
  },
  created() {
    window.addEventListener("resize", debounce(this.drawDiagram), 100);
  },
  mounted() {
    this.drawDiagram();
  },
  updated() {
    this.scaleFairwayProfile();
  },
  destroyed() {
    window.removeEventListener("resize", debounce(this.drawDiagram));
  }
};
</script>