view client/src/components/TimeSlider.vue @ 5058:e916651d3f93 time-sliding

client: adjust time-slider position in case of opened diagram
author Fadi Abbud <fadi.abbud@intevation.de>
date Fri, 06 Mar 2020 17:21:20 +0100
parents abe3a70526a6
children ed1d963017e7
line wrap: on
line source

<template>
  <div
    id="slider"
    :class="[
      'd-flex box ui-element rounded bg-white flex-row',
      { expanded: showTimeSlider }
    ]"
    :style="reposition"
  >
    <div 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]"
      />
      <input
        type="time"
        min="00:00"
        max="23:59"
        v-model="timeSelection"
        class="form-control-sm"
      />
    </div>
    <div
      id="sliderContainer"
      class="d-flex sliderContainer"
      style="width: 98%;"
    ></div>
    <div @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="sass" scoped>
#slider
  position: absolute
  bottom: 0
  min-width: 100vw
  &.expanded
    max-height: 100%
    max-width: 100%
    margin: 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) 2020 by via donau
 *   – Österreichische Wasserstraßen-Gesellschaft mbH
 * Software engineering by Intevation GmbH
 *
 * Author(s):
 * Fadi Abbud <fadiabbud@intevation.de>
 */
import { mapState } from "vuex";
import * as d3 from "d3";
import app from "@/main";
import { localeDateString } from "@/lib/datelocalization";
import { format, setHours, setMinutes } from "date-fns";

let zoom = null;
export default {
  name: "timeslider",
  data() {
    return {
      newX: null
    };
  },
  watch: {
    ongoingRefresh() {
      if (this.ongoingRefresh) return;
      this.$store.commit("application/setSelectedTime", new Date());
      this.$nextTick(this.redrawSlider);
    }
  },
  computed: {
    ...mapState("application", ["showTimeSlider", "paneSetup"]),
    ...mapState("map", ["ongoingRefresh"]),
    reposition() {
      // reposition time slider in case of opened diagram
      if (["DEFAULT", "COMPARESURVEYS"].indexOf(this.paneSetup) === -1) {
        const height = document.getElementById("main").clientHeight + 1;
        return `bottom: ${height}px`;
      } else {
        return "";
      }
    },
    dateSelection: {
      get() {
        const date = this.$store.state.application.selectedTime;
        return format(date, "YYYY-MM-DD");
      },
      set(value) {
        if (!value) return;
        const date = new Date(value);
        this.$store.commit("application/setSelectedTime", date);
        zoom.translateTo(
          d3.select(".zoom"),
          this.getScale()(d3.isoParse(this.selectedTime.toISOString())),
          0
        );
        zoom.scaleTo(d3.select(".zoom"), 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]);
        this.$store.commit("application/setSelectedTime", date);
        zoom.scaleTo(d3.select(".zoom"), 800);
        zoom.translateTo(
          d3.select(".zoom"),
          this.getScale()(d3.isoParse(this.selectedTime.toISOString())),
          0
        );
      }
    },
    selectedTime: {
      get() {
        return this.$store.state.application.selectedTime;
      },
      set(value) {
        this.$store.commit("application/setSelectedTime", value);
      }
    }
  },
  methods: {
    close() {
      this.$store.commit("application/showTimeSlider", false);
    },
    redrawSlider() {
      this.createSlider();
      zoom.translateTo(
        d3.select(".line"),
        this.newX(d3.isoParse(this.selectedTime.toISOString())),
        1
      );
    },
    createSlider() {
      const element = document.getElementById("sliderContainer");
      const svgWidth = element ? element.clientWidth : 0,
        svgHeight = 40,
        marginTop = 20,
        marginLeft = 0;

      d3.timeFormatDefaultLocale(localeDateString);
      this.newX = this.getScale();
      d3.select(".sliderContainer svg").remove();
      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);

      svg
        .append("g")
        .attr("class", "axis--x")
        .attr("transform", `translate(${marginLeft}, ${marginTop})`)
        .call(this.getAxes());

      // create rectanlge on the slider area to capture mouse events
      const eventRect = svg
        .append("rect")
        .attr("id", "zoom")
        .attr("class", "zoom")
        .attr("width", svgWidth)
        .attr("height", svgHeight)
        .attr("fill", "white")
        .attr("opacity", 0.2)
        .on("mouseover", () => {
          svg.select(".zoom").attr("cursor", "move");
        });
      eventRect.call(zoom).on("click", this.onClick);

      const toIsoDate = d => {
        return d.toISOString();
      };

      let drag = d3
        .drag()
        .on("start", () => {
          d3.select(".line")
            .raise()
            .classed("active", true);
        })
        .on("drag", this.onDrag)
        .on("end", () => {
          d3.select(".line").classed("active", false);
        });

      // Create cursor to indicate to the selected time
      svg
        .append("rect")
        .attr("class", "line")
        .attr("id", "scrubber")
        .attr("x", this.newX(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);
    },
    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(this.newX)
        .ticks(12)
        .tickFormat(axesFormat);
    },
    zoomed() {
      let scale = this.getScale();
      this.newX = d3.event.transform.rescaleX(scale);
      d3.select(".axis--x").call(this.getAxes());
      d3.select(".line").attr("x", this.newX(d3.isoParse(this.selectedTime)));
    },
    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);
      this.selectedTime = d3.isoParse(this.newX.invert(p.x + 2));
    },
    onDrag() {
      this.selectedTime = d3.isoParse(this.newX.invert(d3.event.x + 2));
      d3.select(".line").attr("x", d3.event.x);
    }
  },
  mounted() {
    setTimeout(this.createSlider, 150);
  }
};
</script>