view client/src/components/map/fairway/Fairwayprofile.vue @ 1312:3c37017f5eb8

fixed cross profile diagram after switching to to admin context and back and it's responsive behavior The recalculation of the available space (scaleFairwayProfile) needs to be done after removing the current diagram (in drawDiagram).
author Markus Kottlaender <markus@intevation.de>
date Fri, 23 Nov 2018 13:57:31 +0100
parents 6c0c204f6bce
children e4e35fb2d995
line wrap: on
line source

<template>
    <div :class="['position-relative', {show: showSplitscreen}]" v-if="Object.keys(currentProfile).length">
        <button
            class="rounded-bottom bg-white border-0 position-absolute 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 position-absolute clear-selection"
            @click="$store.dispatch('fairwayprofile/clearSelection');"
            v-if="showSplitscreen">
            <font-awesome-icon icon="times" />
        </button>
        <div class="profile bg-white position-relative d-flex flex-column">
            <h5 class="headline border-bottom mb-0 py-2">
              {{ selectedBottleneck }} ({{ selectedSurvey.date_info }})
            </h5>
            <div class="d-flex flex-fill">
                <div class="fairwayprofile m-3 mt-0 bg-white flex-grow-1"></div>
                <div class="additionalsurveys d-flex flex-column">
                    <small>
                        Additional Surveys
                        <select v-model="additionalSurvey" class="form-control form-control-sm">
                            <option value="">None</option>
                            <option
                                v-for="survey in additionalSurveys"
                                :key="survey.date_info"
                                :value="survey"
                            >{{survey.date_info}}</option>
                        </select>
                        <hr>
                        <div class="d-flex text-left mb-2">
                            <div class="text-nowrap mr-1">
                                <b>Start:</b>
                                <br>
                                Lat: {{ startPoint[1] }}
                                <br>
                                Lon: {{ startPoint[0] }}
                            </div>
                            <div class="text-nowrap ml-1">
                                <b>End:</b>
                                <br>
                                Lat: {{ endPoint[1] }}
                                <br>
                                Lon: {{ endPoint[0] }}
                            </div>
                            <button class="btn btn-outline-secondary btn-sm ml-2 mt-auto"
                                    @click="showLabelInput = !showLabelInput">
                              <font-awesome-icon :icon="showLabelInput ? 'times' : 'folder-plus'" size="lg" />
                            </button>
                            <button v-clipboard:copy="coordinatesForClipboard"
                                    v-clipboard:success="onCopyCoordinates"
                                    class="btn btn-outline-secondary btn-sm ml-2 mt-auto">
                              <font-awesome-icon icon="copy" />
                            </button>
                        </div>
                        <div v-if="showLabelInput">
                          Enter label for cross profile:
                          <div class="position-relative">
                              <input class="form-control form-control-sm pr-5" v-model="cutLabel" /><br>
                              <button class="btn btn-sm btn-outline-secondary position-absolute"
                                      @click="saveCut"
                                      v-if="cutLabel"
                                      style="top: 0; right: 0;">
                                <font-awesome-icon icon="check" />
                              </button>
                          </div>
                        </div>
                        Saved cross profiles:
                        <select class="form-control form-control-sm mb-2" v-model="coordinatesSelect">
                            <option></option>
                            <option v-for="(cut, index) in previousCuts" :value="cut.coordinates" :key="index">
                              {{ cut.label }}
                            </option>
                        </select>
                        Enter coordinates manually:
                        <div class="position-relative">
                            <input class="form-control form-control-sm pr-5" placeholder="Lat,Lon,Lat,Lon" v-model="coordinatesInput" /><br>
                            <button class="btn btn-sm btn-outline-secondary position-absolute" 
                                    @click="applyManualCoordinates"
                                    style="top: 0; right: 0;"
                                    v-if="coordinatesInput">
                              <font-awesome-icon icon="check" />
                            </button>
                        </div>
                    </small>
                </div>
            </div>
        </div>
    </div>
</template>

<style lang="sass" scoped>
.profile
  width: 100vw
  height: 0
  overflow: hidden
  z-index: 2
  .headline
    border-top: solid 3px $color-info

.splitscreen-toggle,
.clear-selection
  width: $icon-width
  height: $icon-height
  margin-top: 8px
  z-index: 3
  outline: none
  svg path
    fill: #666

.splitscreen-toggle
  right: $small-offset + $icon-width

.clear-selection
  right: $small-offset

.show
  .profile
    height: 50vh

.waterlevelselection
  margin-top: $large-offset
  margin-right: $large-offset

.additionalsurveys
  margin-top: $large-offset
  margin-bottom: auto
  margin-right: $large-offset
  margin-left: auto
  max-width: 300px

.additionalsurveys input
  margin-right: $small-offset
</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 { displayError, displayInfo } from "../../../lib/errors.js";
import Feature from "ol/Feature";
import LineString from "ol/geom/LineString";
import debounce from "debounce";

const GROUND_COLOR = "#4A2F06";

export default {
  name: "fairwayprofile",
  props: [
    "xScale",
    "yScaleLeft",
    "yScaleRight",
    "margin",
    "additionalSurveys"
  ],
  data() {
    return {
      wait: false,
      coordinatesInput: "",
      coordinatesSelect: null,
      cutLabel: "",
      showLabelInput: false,
      width: null,
      height: null
    };
  },
  computed: {
    ...mapGetters("map", ["getLayerByName"]),
    ...mapState("application", ["showSplitscreen"]),
    ...mapState("fairwayprofile", [
      "startPoint",
      "endPoint",
      "currentProfile",
      "minAlt",
      "maxAlt",
      "totalLength",
      "fairwayCoordinates",
      "waterLevels",
      "selectedWaterLevel",
      "previousCuts"
    ]),
    ...mapState("bottlenecks", ["selectedBottleneck", "selectedSurvey"]),
    additionalSurvey: {
      get() {
        return this.$store.getters["fairwayprofile/additionalSurvey"];
      },
      set(value) {
        this.$store.commit("fairwayprofile/setAdditionalSurvey", value);
        this.selectAdditionalSurveyData();
      }
    },
    currentData() {
      if (
        !this.selectedSurvey ||
        !this.currentProfile.hasOwnProperty(this.selectedSurvey.date_info)
      )
        return [];
      return this.currentProfile[this.selectedSurvey.date_info];
    },
    additionalData() {
      if (
        !this.additionalSurvey ||
        !this.currentProfile.hasOwnProperty(this.additionalSurvey.date_info)
      )
        return [];
      return this.currentProfile[this.additionalSurvey.date_info];
    },
    waterColor() {
      const result = this.waterLevels.find(
        x => x.level === this.selectedWaterLevel
      );
      return result.color;
    },
    coordinatesForClipboard() {
      return (
        this.startPoint[1] +
        "," +
        this.startPoint[0] +
        "," +
        this.endPoint[1] +
        "," +
        this.endPoint[0]
      );
    }
  },
  watch: {
    showSplitscreen() {
      this.drawDiagram();
    },
    currentData() {
      this.drawDiagram();
    },
    width() {
      this.drawDiagram();
    },
    height() {
      this.drawDiagram();
    },
    waterLevels() {
      this.drawDiagram();
    },
    selectedWaterLevel() {
      this.drawDiagram();
    },
    fairwayCoordinates() {
      this.drawDiagram();
    },
    selectedBottleneck() {
      this.$store.dispatch("fairwayprofile/previousCuts");
      this.cutLabel =
        this.selectedBottleneck + " (" + new Date().toISOString() + ")";
    },
    coordinatesSelect(newValue) {
      if (newValue) {
        this.applyCoordinates(newValue);
      }
    }
  },
  methods: {
    selectAdditionalSurveyData() {
      if (
        !this.additionalSurvey ||
        this.wait ||
        this.currentProfile[this.additionalSurvey.date_info]
      ) {
        this.drawDiagram();
        return;
      }
      this.$store
        .dispatch("fairwayprofile/loadProfile", this.additionalSurvey)
        .then(() => {
          this.wait = false;
        })
        .catch(error => {
          this.wait = false;
          let status = "ERROR";
          let data = error;
          const response = error.response;
          if (response) {
            status = response.status;
            data = response.data;
          }
          displayError({
            title: "Backend Error",
            message: `${status}: ${data.message || data}`
          });
        });
    },
    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,
        yScaleLeft,
        graph
      } = this.generateCoordinates(svg, height, width);
      this.drawWaterlevel({
        graph,
        xScale,
        yScaleRight,
        height,
        width
      });
      if (currentData) {
        this.drawProfile({
          graph,
          xScale,
          yScaleRight,
          currentData,
          height,
          width,
          color: GROUND_COLOR,
          strokeColor: "black",
          opacity: 1
        });
      }
      if (additionalData) {
        this.drawProfile({
          graph,
          xScale,
          yScaleRight,
          currentData: additionalData,
          height,
          width,
          color: GROUND_COLOR,
          strokeColor: "#943007",
          opacity: 0.6
        });
      }
      this.drawLabels({
        graph,
        xScale,
        yScaleLeft,
        currentData,
        height,
        width
      });
      this.drawFairway({
        graph,
        xScale,
        yScaleRight,
        currentData,
        height,
        width
      });
    },
    drawFairway({ graph, xScale, yScaleRight }) {
      for (let coordinates of this.fairwayCoordinates) {
        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", "#002AFF")
          .attr("stroke-opacity", 0.65)
          .attr("fill-opacity", 0.65)
          .attr("stroke", "#FFD20D")
          .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 yScaleLeft = d3
        .scaleLinear()
        .domain(this.yScaleLeft)
        .rangeRound([height, 0]);

      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, yScaleLeft, 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", 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;
    },
    onCopyCoordinates() {
      displayInfo({
        title: "Success",
        message: "Coordinates copied to clipboard!"
      });
    },
    applyManualCoordinates() {
      const coordinates = this.coordinatesInput
        .split(",")
        .map(coord => parseFloat(coord.trim()));
      this.applyCoordinates([
        coordinates[1],
        coordinates[0],
        coordinates[3],
        coordinates[2]
      ]);
    },
    applyCoordinates(coordinates) {
      // allow only numbers
      coordinates = coordinates.filter(c => Number(c) === c);
      if (coordinates.length === 4) {
        // draw line on map
        const cutLayer = this.getLayerByName("Cut Tool");
        cutLayer.data.getSource().clear();
        const cut = new Feature({
          geometry: new LineString([
            [coordinates[0], coordinates[1]],
            [coordinates[2], coordinates[3]]
          ]).transform("EPSG:4326", "EPSG:3857")
        });
        cutLayer.data.getSource().addFeature(cut);

        // draw diagram
        this.$store.dispatch("fairwayprofile/cut", cut);
      } else {
        displayError({
          title: "Invalid input",
          message:
            "Please enter correct coordinates in the format: Lat,Lon,Lat,Lon"
        });
      }
    },
    saveCut() {
      const previousCuts =
        JSON.parse(localStorage.getItem("previousCuts")) || [];
      const newEntry = {
        label: this.cutLabel,
        bottleneckName: this.selectedBottleneck,
        coordinates: [...this.startPoint, ...this.endPoint]
      };
      const existingEntry = previousCuts.find(cut => {
        return JSON.stringify(cut) === JSON.stringify(newEntry);
      });
      if (!existingEntry) previousCuts.push(newEntry);
      if (previousCuts.length > 100) previousCuts.shift();
      localStorage.setItem("previousCuts", JSON.stringify(previousCuts));
      this.$store.dispatch("fairwayprofile/previousCuts");

      this.showLabelInput = false;
      displayInfo({
        title: "Coordinates saved!",
        message:
          'You can now select these coordinates from the "Saved cross profiles" menu to restore this cross profile.'
      });
    }
  },
  created() {
    window.addEventListener("resize", debounce(this.drawDiagram), 100);
  },
  mounted() {
    this.drawDiagram();
  },
  updated() {
    this.scaleFairwayProfile();
  },
  destroyed() {
    window.removeEventListener("resize", debounce(this.drawDiagram));
  }
};
</script>