changeset 5059:c4f90dcd7c15 time-sliding

merge default into time-slinding branch
author Fadi Abbud <fadi.abbud@intevation.de>
date Mon, 09 Mar 2020 12:41:47 +0100
parents e916651d3f93 (diff) 9e210879bd88 (current diff)
children ed1d963017e7
files
diffstat 6 files changed, 374 insertions(+), 4 deletions(-) [+]
line wrap: on
line diff
--- a/client/src/components/App.vue	Fri Mar 06 18:41:30 2020 +0100
+++ b/client/src/components/App.vue	Mon Mar 09 12:41:47 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	Mon Mar 09 12:41:47 2020 +0100
@@ -0,0 +1,277 @@
+<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>
--- a/client/src/components/map/Zoom.vue	Fri Mar 06 18:41:30 2020 +0100
+++ b/client/src/components/map/Zoom.vue	Mon Mar 09 12:41:47 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	Mon Mar 09 12:41:47 2020 +0100
@@ -0,0 +1,77 @@
+<template>
+  <div
+    @click="$store.commit('application/showTimeSlider', !showTimeSlider)"
+    class="toolbar-button"
+    v-tooltip.right="label"
+  >
+    <pre
+      :class="[
+        'menuEntry',
+        {
+          'text-info': this.showTimeSlider
+        }
+      ]"
+      >{{ currentTimeSelection }}</pre
+    >
+  </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";
+import locale2 from "locale2";
+import { format } from "date-fns";
+
+export default {
+  computed: {
+    ...mapState("application", ["showTimeSlider", "selectedTime"]),
+    label() {
+      const date = this.selectedTime;
+      return `<b>${this.selectedTime.toLocaleDateString(locale2, {
+        day: "2-digit",
+        month: "2-digit",
+        year: "numeric"
+      })} ${format(date, "HH:mm")}</b>`;
+    },
+    currentTimeSelection() {
+      const date = this.selectedTime;
+      const result = date.toLocaleDateString(locale2, {
+        day: "2-digit",
+        month: "2-digit"
+      });
+      return `${format(date, "HH:mm")}\n${result}\n${date.getFullYear()}`;
+    }
+  }
+};
+</script>
+<style lang="scss" scoped>
+.menuEntry {
+  font-size: 9px;
+  font-weight: bold;
+  line-height: normal;
+}
+
+pre {
+  margin-top: 0px;
+  margin-bottom: 0px;
+  text-align: left;
+  font-family: sans-serif;
+}
+
+.toolbar-button {
+  height: 2.5rem;
+  width: auto;
+}
+</style>
--- a/client/src/components/toolbar/Toolbar.vue	Fri Mar 06 18:41:30 2020 +0100
+++ b/client/src/components/toolbar/Toolbar.vue	Mon Mar 09 12:41:47 2020 +0100
@@ -7,6 +7,7 @@
       "
     >
       <Identify />
+      <TimeSlider />
       <Layers />
       <Profiles />
       <Gauges />
@@ -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 Mar 06 18:41:30 2020 +0100
+++ b/client/src/store/application.js	Mon Mar 09 12:41:47 2020 +0100
@@ -14,8 +14,9 @@
  *   Bernhard E. Reiter <bernhard.reiter@intevation.de>
  */
 
+import { displayError, displayInfo } from "@/lib/errors";
+
 import { HTTP } from "@/lib/http";
-import { displayError, displayInfo } from "@/lib/errors";
 import { version } from "../../package.json";
 
 // initial state
@@ -41,10 +42,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: new Date(),
     version,
     tempRoute: "",
     config: {}
@@ -93,6 +96,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;
     },