view client/src/fairway/Fairwayprofile.vue @ 1207:70116d392387

close bottleneck list: made searchbar collapse only if it was collapsed before opening the bottleneck list will expand the searchbar, closing the bottleneck list was always collapsing the searchbar too. Now it stays open if it was open beforeopening the bottleneck list
author Markus Kottlaender <markus@intevation.de>
date Mon, 19 Nov 2018 13:02:48 +0100
parents ddfdf440da24
children ba8cd80d68b6
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 shadow-sm"
            @click="$store.commit('application/showSplitscreen', false)"
            v-if="showSplitscreen">
            <i class="fa fa-angle-down"></i>
        </button>
        <button
            class="rounded-bottom bg-white border-0 position-absolute clear-selection shadow-sm"
            @click="$store.dispatch('fairwayprofile/clearSelection');"
            v-if="showSplitscreen">
            <i class="fa fa-times text-danger"></i>
        </button>
        <div class="profile d-flex flex-column pr-5">
            <h5 class="mb-0 mt-2">{{ selectedBottleneck }} ({{ selectedSurvey.date_info }})</h5>
            <div class="d-flex flex-fill">
                <div class="fairwayprofile 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">
                              <i :class="'fa fa-' + (showLabelInput ? 'times' : 'save')"></i>
                            </button>
                            <button v-clipboard:copy="coordinatesForClipboard"
                                    v-clipboard:success="onCopyCoordinates"
                                    class="btn btn-outline-secondary btn-sm ml-2 mt-auto">
                              <i class="fa fa-copy"></i>
                            </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;">
                                <i class="fa fa-check"></i>
                              </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">
                              <i class="fa fa-check"></i>
                            </button>
                        </div>
                    </small>
                </div>
            </div>
        </div>
    </div>
</template>

<style lang="sass" scoped>
.profile
  background-color: white
  width: 100vw
  height: 0
  overflow: hidden
  position: relative
  z-index: 2

.splitscreen-toggle,
.clear-selection
  right: $icon-width + $offset
  width: $icon-width
  height: $icon-height
  margin-top: 2px
  z-index: 3
  outline: none

.clear-selection
  right: $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

.fairwayprofile
  background-color: white
  margin: $offset
  margin-top: 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 { displayError, displayInfo } from "../application/lib/errors.js";
import Feature from "ol/Feature";
import LineString from "ol/geom/LineString";

const GROUND_COLOR = "#4A2F06";

export default {
  name: "fairwayprofile",
  props: [
    "width",
    "height",
    "xScale",
    "yScaleLeft",
    "yScaleRight",
    "margin",
    "additionalSurveys"
  ],
  data() {
    return {
      wait: false,
      coordinatesInput: "",
      coordinatesSelect: null,
      cutLabel: "",
      showLabelInput: false
    };
  },
  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("svg").remove();
      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);
      }
    },
    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.'
      });
    }
  },
  mounted() {
    this.drawDiagram();
  }
};
</script>