changeset 5036:8f421cd3c746 time-sliding

client: Implemented first version of time-sliding
author Fadi Abbud <fadi.abbud@intevation.de>
date Thu, 27 Feb 2020 09:18:17 +0100
parents b65898de11ad
children 9d288d9b851b
files client/src/components/App.vue client/src/components/TimeSlider.vue client/src/components/map/Zoom.vue client/src/components/toolbar/TimeSlider.vue client/src/components/toolbar/Toolbar.vue client/src/store/application.js
diffstat 6 files changed, 225 insertions(+), 3 deletions(-) [+]
line wrap: on
line diff
--- a/client/src/components/App.vue	Fri Feb 21 10:08:32 2020 +0100
+++ b/client/src/components/App.vue	Thu Feb 27 09:18:17 2020 +0100
@@ -26,6 +26,7 @@
         </div>
       </div>
       <MapPopup />
+      <TimeSlider v-if="isMapVisible" />
     </div>
     <router-view />
     <vue-snotify />
@@ -111,6 +112,7 @@
     Layers: () => import("./layers/Layers"),
     Sidebar: () => import("./Sidebar"),
     Search: () => import("./Search"),
+    TimeSlider: () => import("./TimeSlider"),
     Contextbox: () => import("./Contextbox"),
     Toolbar: () => import("./toolbar/Toolbar"),
     Popup: () => import("./Popup"),
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/client/src/components/TimeSlider.vue	Thu Feb 27 09:18:17 2020 +0100
@@ -0,0 +1,172 @@
+<template>
+  <div
+    id="slider"
+    :class="[
+      'd-flex box ui-element rounded bg-white mw-100 flex-row',
+      { expand: showTimeSlider }
+    ]"
+  >
+    <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">
+#slider
+  position: absolute
+  bottom: 0
+  width: 100%
+  &.expand
+    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";
+export default {
+  name: "timeslider",
+  data() {
+    return {
+      selectedTime: new Date("2020-01-04"),
+      newX: null
+    };
+  },
+  computed: {
+    ...mapState("application", ["showTimeSlider"])
+  },
+  methods: {
+    close() {
+      this.$store.commit("application/showTimeSlider", false);
+    },
+    createSlider() {
+      const element = document.getElementById("sliderContainer");
+      const svgWidth = element ? element.clientWidth : 0,
+        svgHeight = 40,
+        marginTop = 20,
+        marginLeft = 0;
+
+      this.newX = this.getScale();
+      let svg = d3
+        .select(".sliderContainer")
+        .append("svg")
+        .attr("width", svgWidth)
+        .attr("height", svgHeight);
+
+      // zoom event
+      let zoom = d3
+        .zoom()
+        .scaleExtent([0, Infinity])
+        .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(
+          d3.axisBottom(this.newX).ticks(12)
+          //.tickFormat(dFormat)
+        );
+
+      // 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("2020-01-01")),
+          d3.isoParse(new Date("2020-03-01"))
+        ]);
+    },
+    zoomed() {
+      let scale = this.getScale();
+      this.newX = d3.event.transform.rescaleX(scale);
+      d3.select(".axis--x").call(
+        d3.axisBottom(this.newX).ticks(12)
+        //.tickFormat(dFormat)
+      );
+      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>
--- a/client/src/components/map/Zoom.vue	Fri Feb 21 10:08:32 2020 +0100
+++ b/client/src/components/map/Zoom.vue	Thu Feb 27 09:18:17 2020 +0100
@@ -1,5 +1,5 @@
 <template>
-  <div class="zoom-buttons shadow-xs">
+  <div :class="['zoom-buttons shadow-xs', { move: showTimeSlider }]">
     <button
       class="zoom-button border-0 bg-white rounded-left ui-element"
       @click="zoomOut"
@@ -24,7 +24,8 @@
   margin-left: -$icon-width
   margin-bottom: 0
   transition: margin-bottom 0.3s
-
+  &.move
+    bottom: $large-offset * 1.5
   .zoom-button
     min-height: $icon-width
     min-width: $icon-width
@@ -34,6 +35,7 @@
 </style>
 
 <script>
+import { mapState } from "vuex";
 /* This is Free Software under GNU Affero General Public License v >= 3.0
  * without warranty, see README.md and license for details.
  *
@@ -51,6 +53,7 @@
 export default {
   props: ["map"],
   computed: {
+    ...mapState("application", ["showTimeSlider"]),
     zoomLevel: {
       get() {
         return this.map.getView().getZoom();
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/client/src/components/toolbar/TimeSlider.vue	Thu Feb 27 09:18:17 2020 +0100
@@ -0,0 +1,35 @@
+<template>
+  <div
+    @click="$store.commit('application/showTimeSlider', !showTimeSlider)"
+    class="toolbar-button"
+    v-tooltip.right="label"
+  >
+    <font-awesome-icon icon="clock" :class="{ 'text-info': showTimeSlider }" />
+  </div>
+</template>
+
+<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";
+
+export default {
+  computed: {
+    ...mapState("application", ["showTimeSlider"]),
+    label() {
+      return this.$gettext("Time slider");
+    }
+  }
+};
+</script>
--- a/client/src/components/toolbar/Toolbar.vue	Fri Feb 21 10:08:32 2020 +0100
+++ b/client/src/components/toolbar/Toolbar.vue	Thu Feb 27 09:18:17 2020 +0100
@@ -14,6 +14,7 @@
       <Linetool />
       <Polygontool />
       <Pdftool />
+      <TimeSlider />
     </div>
     <div
       @click="$store.commit('application/expandToolbar', !expandToolbar)"
@@ -128,7 +129,8 @@
     Profiles: () => import("./Profiles"),
     Gauges: () => import("./Gauges"),
     Pdftool: () => import("./Pdftool"),
-    AvailableFairwayDepth: () => import("./AvailableFairwayDepth")
+    AvailableFairwayDepth: () => import("./AvailableFairwayDepth"),
+    TimeSlider: () => import("./TimeSlider")
   },
   computed: {
     ...mapState("application", ["expandToolbar"])
--- a/client/src/store/application.js	Fri Feb 21 10:08:32 2020 +0100
+++ b/client/src/store/application.js	Thu Feb 27 09:18:17 2020 +0100
@@ -41,10 +41,12 @@
     showGauges: false,
     showFairwayDepth: false,
     showFairwayDepthLNWL: false,
+    showTimeSlider: false,
     contextBoxContent: null, // bottlenecks, imports, staging
     expandToolbar: true,
     countries: ["AT", "SK", "HU", "HR", "RS", "BG", "RO"],
     searchQuery: "",
+    selectedTime: "2020-01-04",
     version,
     tempRoute: "",
     config: {}
@@ -93,6 +95,12 @@
         if (state.paneRotate === 5) state.paneRotate = 1;
       }
     },
+    setselectedTime: (state, time) => {
+      state.selectedTime = time;
+    },
+    showTimeSlider: (state, show) => {
+      state.showTimeSlider = show;
+    },
     showSidebar: (state, show) => {
       state.showSidebar = show;
     },