view client/src/components/fairway/Fairwayprofile.vue @ 2460:e2f05ac81471

client: available waterlevels in fairway profile: improved styling
author Markus Kottlaender <markus@intevation.de>
date Mon, 04 Mar 2019 13:18:35 +0100
parents 0b8793a1947c
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>