view client/src/fairway/Fairwayprofile.vue @ 1204:ddfdf440da24

made searchbar contextual if bottlenecks are open search bar refers to that list if imports are open search bar is hidden
author Markus Kottlaender <markus@intevation.de>
date Mon, 19 Nov 2018 11:23:00 +0100
parents 502e0b960424
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>