Mercurial > gemma
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; },