view client/src/components/TimeSlider.vue @ 5627:7768f14f6535 729-node-js-newer-version

Transformed scss variables into css custom properties
author Luisa Beerboom <lbeerboom@intevation.de>
date Tue, 09 May 2023 13:17:58 +0200
parents de86a96d55c3
children 84d01a536bec
line wrap: on
line source

<template>
  <div
    id="slider"
    :class="[
      'd-flex box ui-element rounded bg-white flex-row',
      { expanded: showTimeSlider },
      {
        reposition: ['DEFAULT', 'COMPARESURVEYS'].indexOf(this.paneSetup) === -1
      }
    ]"
  >
    <div id="timeselection" class="d-flex mt-1 mr-1">
      <input
        class="form-control-sm mr-1"
        type="date"
        v-model="dateSelection"
        min="2015-01-01"
        :max="new Date().toISOString().split('T')[0]"
        required
      />
      <input
        type="time"
        min="00:00"
        max="23:59"
        v-model="timeSelection"
        class="form-control-sm"
        required
      />
    </div>
    <div
      id="sliderContainer"
      class="d-flex sliderContainer"
      style="width: 98%;"
    ></div>
    <div
      id="closebutton"
      @click="close"
      class="d-flex box-control mr-0"
      style="width: 2%;"
    >
      <font-awesome-icon icon="times"></font-awesome-icon>
    </div>
  </div>
</template>
<style lang="scss" scoped>
#slider {
  position: absolute;
  bottom: 0;
  min-width: 100vw;
}
// reposition time slider in case of opened diagram
#slider.reposition {
  bottom: 50%;
}
#slider.expanded {
  max-height: 100%;
  max-width: 100%;
  margin: 0;
}
input::-webkit-clear-button {
  display: none;
}
// hide clear button on IE
input::-ms-clear {
  display: none;
}
</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) 2020 by via donau
 *   – Österreichische Wasserstraßen-Gesellschaft mbH
 * Software engineering by Intevation GmbH
 *
 * Author(s):
 * Fadi Abbud <fadi.abbud@intevation.de>
 * Thomas Junk <thomas.junk@intevation.de>
 */
import { mapState } from "vuex";
import * as d3 from "d3";
import app from "@/main";
import { localeDateString } from "@/lib/datelocalization";
import { format, setHours, setMinutes, compareAsc } from "date-fns";
import debounce from "debounce";

let zoom = null;
let xScale = null;
let xAxis = null;
let currentScaleFactor = 1;

export default {
  name: "timeslider",
  data() {
    return {
      isSelectedTimeHourly: false,
      resizeListenerFunction: null
    };
  },
  watch: {
    ongoingRefresh() {
      if (this.ongoingRefresh) return;
      this.$store.commit("application/setSelectedTime", new Date());
      this.$nextTick(this.rescaleSlider(1));
    },
    ongoingTimeSlide() {
      if (this.ongoingTimeSlide) return;
      this.$store.commit(
        "application/setCurrentVisibleTime",
        this.refreshLayersTime
      );
    },
    selectedTime() {
      this.triggerMapReload();
    },
    sourcesLoading() {
      // initiate refresh layers request if the time for the finished request
      // differs from the selected time on time slider
      if (this.sourcesLoading !== 0) return;
      if (compareAsc(this.selectedTime, this.currentVisibleTime) === 0) return;
      this.triggerMapReload();
    }
  },
  computed: {
    ...mapState("application", [
      "showTimeSlider",
      "paneSetup",
      "currentVisibleTime",
      "refreshLayersTime"
    ]),
    ...mapState("map", [
      "ongoingRefresh",
      "ongoingTimeSlide",
      "openLayersMaps"
    ]),
    dateSelection: {
      get() {
        const date = this.$store.state.application.selectedTime;
        return format(date, "YYYY-MM-DD");
      },
      set(value) {
        if (!value) return;
        let date = new Date(value);
        const [hours, minutes] = this.timeSelection.split(":");
        date = setHours(date, hours);
        date = setMinutes(date, minutes);
        const now = new Date();
        if (date > now) {
          date = now;
        }
        this.$store.commit("application/setSelectedTime", date);
        this.rescaleSlider(50);
      }
    },
    timeSelection: {
      get() {
        const time = this.$store.state.application.selectedTime;
        return format(time, "HH:mm");
      },
      set(value) {
        if (!value) return;
        let date = this.selectedTime;
        date = setHours(date, value.split(":")[0]);
        date = setMinutes(date, value.split(":")[1]);
        const now = new Date();
        if (date > now) {
          date = now;
        }
        this.$store.commit("application/setSelectedTime", date);
        this.rescaleSlider(800);
      }
    },
    selectedTime: {
      get() {
        return this.$store.state.application.selectedTime;
      },
      set(value) {
        if (!this.isSelectedTimeHourly) {
          value = setHours(value, 12);
          value = setMinutes(value, 0);
        }
        this.$store.commit("application/setSelectedTime", value);
      }
    },
    sourcesLoading() {
      const layers = [
        "BOTTLENECKS",
        "GAUGES",
        "FAIRWAYDIMENSIONSLOS1",
        "FAIRWAYDIMENSIONSLOS2",
        "FAIRWAYDIMENSIONSLOS3",
        "WATERWAYAXIS",
        "FAIRWAYMARKS"
      ];
      let counter = 0;
      this.openLayersMaps.forEach(map => {
        for (let i = 0; i < layers.length; i++) {
          let layer = map.getLayer(layers[i]);
          if (layer.getSource().loading) counter++;
        }
      });
      return counter;
    }
  },
  methods: {
    close() {
      this.$store.commit("application/showTimeSlider", false);
      this.$store.commit("application/setStoredTime", this.currentVisibleTime);
      this.$store.commit("application/setSelectedTime", new Date());
    },
    triggerMapReload() {
      // trigger refresh layers only when last loading of layers was ended
      if (this.sourcesLoading) {
        return;
      }
      this.$store.commit(
        "application/setLayerRefreshedTime",
        this.selectedTime
      );
      this.$store.commit("map/startTimeSlide");
      this.$store.dispatch("map/refreshTimebasedLayers");
      this.$nextTick(() => {
        this.$store.commit("map/finishTimeSlide");
      });
    },
    rescaleSlider(scaleFactor) {
      const tx =
        -scaleFactor *
          this.getScale()(d3.isoParse(this.selectedTime.toISOString())) +
        document.getElementById("sliderContainer").clientWidth / 2;
      var t = d3.zoomIdentity.translate(tx, 0).scale(scaleFactor);
      this.getScale().domain(t.rescaleX(this.getScale()));
      d3.select(".zoom").call(zoom.transform, t);
    },
    createSlider() {
      const element = document.getElementById("sliderContainer");
      const svgWidth = element ? element.clientWidth : 0,
        svgHeight = 40,
        marginTop = 20,
        marginLeft = 0;

      d3.timeFormatDefaultLocale(localeDateString);
      const toIsoDate = d => {
        return d.toISOString();
      };
      xScale = this.getScale();
      xAxis = this.getAxes();
      let svg = d3
        .select(".sliderContainer")
        .append("svg")
        .attr("width", svgWidth)
        .attr("height", svgHeight);

      zoom = d3
        .zoom()
        .scaleExtent([0.8, 102000])
        .translateExtent([
          [0, 0],
          [svgWidth, svgHeight]
        ])
        .extent([[0, 0], [(svgWidth, svgHeight)]])
        .on("zoom", this.zoomed);

      const drag = d3
        .drag()
        .on("start", () => {
          d3.select(".line")
            .raise()
            .classed("active", true);
        })
        .on("drag", this.onDrag)
        .on("end", () => {
          d3.select(".line").classed("active", false);
        });
      const main = svg
        .append("g")
        .attr("id", "zoom")
        .attr("class", "zoom")
        .attr("width", svgWidth)
        .attr("height", svgHeight)
        .on("mouseover", () => {
          svg.select(".zoom").attr("cursor", "move");
        });

      main
        .append("rect")
        .attr("class", "main")
        .attr("width", svgWidth)
        .attr("height", svgHeight)
        .attr("opacity", 0)
        .on("mouseover", () => {
          svg.select(".main").attr("cursor", "move");
        });

      main
        .append("g")
        .attr("class", "axis--x")
        .attr("id", "zoom")
        .attr("transform", `translate(${marginLeft}, ${marginTop})`)
        .call(xAxis);

      svg
        .append("rect")
        .attr("class", "future")
        .attr("x", xAxis.scale()(d3.isoParse(toIsoDate(new Date()))))
        .attr("y", 0)
        .attr("width", svgWidth)
        .attr("height", svgHeight)
        .attr("fill", "#333333")
        .attr("opacity", 0.3);

      // Create cursor to indicate to the selected time
      main
        .append("rect")
        .attr("class", "line")
        .attr("id", "scrubber")
        .attr("x", xAxis.scale()(d3.isoParse(toIsoDate(this.selectedTime))))
        .attr("y", 0)
        .attr("width", 2)
        .attr("height", svgHeight)
        .attr("stroke", "#17a2b8")
        .attr("stroke-width", 2)
        .attr("opacity", 0.6)
        .on("mouseover", () => {
          svg.select(".line").attr("cursor", "e-resize");
        })
        .call(drag);

      main.call(zoom).on("click", this.onClick);
    },
    getScale() {
      return d3
        .scaleTime()
        .range([0, document.getElementById("sliderContainer").clientWidth || 0])
        .domain([d3.isoParse(new Date("2015-01-01")), d3.isoParse(new Date())]);
    },
    getAxes() {
      const axesFormat = date => {
        return (d3.timeSecond(date) < date
          ? d3.timeFormat(".%L")
          : d3.timeMinute(date) < date
          ? d3.timeFormat(":%S")
          : d3.timeHour(date) < date
          ? d3.timeFormat("%H:%M")
          : d3.timeDay(date) < date
          ? d3.timeFormat("%H:%M")
          : d3.timeMonth(date) < date
          ? d3.timeWeek(date) < date
            ? d3.timeFormat(app.$gettext("%a %d"))
            : d3.timeFormat(app.$gettext("%b %d"))
          : d3.timeYear(date) < date
          ? d3.timeFormat("%B")
          : d3.timeFormat("%Y"))(date);
      };
      return d3
        .axisBottom(xScale)
        .ticks(12)
        .tickFormat(axesFormat);
    },
    zoomed() {
      let newX = d3.event.transform.rescaleX(xScale);
      currentScaleFactor = d3.event.transform.k;
      this.checkSelectedTimeHourly();
      xAxis.scale(newX);
      d3.select(".axis--x").call(xAxis);
      d3.select(".line").attr("x", newX(d3.isoParse(this.selectedTime)));
      d3.select(".future").attr("x", newX(d3.isoParse(new Date())));
    },
    checkSelectedTimeHourly() {
      const isHourly = currentScaleFactor > 400;
      if (this.isSelectedTimeHourly != isHourly)
        this.isSelectedTimeHourly = isHourly;
    },
    onClick() {
      // Extract the click location
      let point = d3.mouse(document.getElementById("zoom")),
        p = { x: point[0], y: point[1] };
      d3.select(".line").attr("x", p.x);
      const xTime = d3.isoParse(xAxis.scale().invert(p.x + 2)).toDateString();
      const now = new Date();
      // Avoid moving the cursor to future area if the "now" value is earlier than 12:00
      if (
        xTime === now.toDateString() &&
        !this.isSelectedTimeHourly &&
        now.getHours() < 12
      ) {
        this.isSelectedTimeHourly = true;
        this.selectedTime = d3.isoParse(new Date());
      } else {
        this.selectedTime = d3.isoParse(xAxis.scale().invert(p.x + 2));
      }
      this.checkSelectedTimeHourly();
    },
    onDrag() {
      this.selectedTime = d3.isoParse(xAxis.scale().invert(d3.event.x + 2));
      const now = new Date();
      const startTimeOnSlider = d3.isoParse(xAxis.scale().invert(0));
      const endTimeOnSlider = d3.isoParse(
        xAxis.scale().invert(xScale.range()[1])
      );
      // Prevent dragging outside the visible area on slider
      if (endTimeOnSlider < this.selectedTime) {
        this.selectedTime = endTimeOnSlider;
        d3.select(".line").attr(
          "x",
          xAxis.scale()(d3.isoParse(endTimeOnSlider))
        );
      } else {
        if (startTimeOnSlider > this.selectedTime) {
          this.selectedTime = startTimeOnSlider;
          d3.select(".line").attr(
            "x",
            xAxis.scale()(d3.isoParse(startTimeOnSlider))
          );
        } else {
          if (this.selectedTime > now) {
            this.isSelectedTimeHourly = true;
            this.selectedTime = now;
            d3.select(".line").attr("x", xAxis.scale()(d3.isoParse(now)));
            this.checkSelectedTimeHourly();
          } else {
            d3.select(".line").attr("x", d3.event.x);
          }
        }
      }
    },
    redrawTimeSlider() {
      const bodyWidth = document.querySelector("body").clientWidth;
      const timeSelectionWidth = document.querySelector("#timeselection")
        .clientWidth;
      const closeButton = document.querySelector("#closebutton").clientWidth;
      const svgWidth = bodyWidth - timeSelectionWidth - closeButton;
      document
        .querySelector(".sliderContainer svg")
        .setAttribute("width", svgWidth);
      xScale.range([0, svgWidth]);
      xAxis.scale(xScale);
      d3.select(".axis--x").call(xAxis);
      d3.select(".line").attr(
        "x",
        xAxis.scale()(d3.isoParse(this.selectedTime))
      );
      this.rescaleSlider(currentScaleFactor);
    }
  },
  created() {
    this.resizeListenerFunction = debounce(this.redrawTimeSlider, 100);
    window.addEventListener("resize", this.resizeListenerFunction);
  },
  destroyed() {
    window.removeEventListener("resize", this.resizeListenerFunction);
  },
  mounted() {
    setTimeout(this.createSlider, 150);
  }
};
</script>