view client/src/components/TimeSlider.vue @ 5560:f2204f91d286

Join the log lines of imports to the log exports to recover data from them. Used in SR export to extract information that where in the meta json but now are only found in the log.
author Sascha L. Teichmann <sascha.teichmann@intevation.de>
date Wed, 09 Feb 2022 18:34:40 +0100
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>