Mercurial > gemma
changeset 5091:1154b73328ec
Merged time-sliding branch into default.
author | Sascha L. Teichmann <sascha.teichmann@intevation.de> |
---|---|
date | Tue, 24 Mar 2020 11:23:26 +0100 |
parents | 56c589f7435d (current diff) 70bd5c824639 (diff) |
children | 8dc27fc1d05c f64ff954ee31 |
files | client/src/components/map/layers.js client/src/components/map/styles.js |
diffstat | 16 files changed, 1679 insertions(+), 1133 deletions(-) [+] |
line wrap: on
line diff
--- a/client/src/components/App.vue Fri Mar 20 17:24:03 2020 +0100 +++ b/client/src/components/App.vue Tue Mar 24 11:23:26 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 Tue Mar 24 11:23:26 2020 +0100 @@ -0,0 +1,388 @@ +<template> + <div + id="slider" + :class="[ + 'd-flex box ui-element rounded bg-white flex-row', + { expanded: showTimeSlider } + ]" + :style="reposition" + > + <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; +} +#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 <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, 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" + ]), + 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; + let date = new Date(value); + const [hours, minutes] = this.timeSelection.split(":"); + date = setHours(date, hours); + date = setMinutes(date, minutes); + 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]); + 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); + 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); + + svg + .append("g") + .attr("class", "axis--x") + .attr("transform", `translate(${marginLeft}, ${marginTop})`) + .call(xAxis); + + // 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", 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); + }, + 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; + const isHourly = currentScaleFactor > 400; + if (this.isSelectedTimeHourly != isHourly) + this.isSelectedTimeHourly = isHourly; + xAxis.scale(newX); + d3.select(".axis--x").call(xAxis); + d3.select(".line").attr("x", 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(xAxis.scale().invert(p.x + 2)); + }, + onDrag() { + this.selectedTime = d3.isoParse(xAxis.scale().invert(d3.event.x + 2)); + 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>
--- a/client/src/components/identify/Identify.vue Fri Mar 20 17:24:03 2020 +0100 +++ b/client/src/components/identify/Identify.vue Tue Mar 24 11:23:26 2020 +0100 @@ -270,7 +270,7 @@ import { formatter } from "./formatter"; import { getCenter } from "ol/extent"; import classifications from "@/lib/classifications"; -import { styleFactory } from "@/components/map/styles"; +import { styleFactory } from "@/components/layers/styles"; import filters from "@/lib/filters"; const {
--- a/client/src/components/importoverview/FairwayDimensionDetail.vue Fri Mar 20 17:24:03 2020 +0100 +++ b/client/src/components/importoverview/FairwayDimensionDetail.vue Tue Mar 24 11:23:26 2020 +0100 @@ -30,7 +30,7 @@ //import { displayError } from "@/lib/errors"; import { mapGetters } from "vuex"; import VectorSource from "ol/source/Vector"; -import { buildVectorLoader } from "@/components/map/layers.js"; +import { buildVectorLoader } from "@/components/layers/layers.js"; import { bbox as bboxStrategy } from "ol/loadingstrategy"; import { WFS } from "ol/format"; import { HTTP } from "@/lib/http";
--- a/client/src/components/layers/Layers.vue Fri Mar 20 17:24:03 2020 +0100 +++ b/client/src/components/layers/Layers.vue Tue Mar 24 11:23:26 2020 +0100 @@ -99,6 +99,7 @@ this.$store.commit("application/showLayers", false); }, refreshLayers() { + this.$store.commit("application/setLayerRefreshedTime", new Date()); this.$store.commit("map/startRefreshLayers"); this.$store.commit("gauges/deleteNashSutcliffeCache"); this.$store.dispatch("map/refreshLayers");
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/client/src/components/layers/layers.js Tue Mar 24 11:23:26 2020 +0100 @@ -0,0 +1,724 @@ +import { GeoJSON, WFS } from "ol/format"; +import { Icon, Stroke, Style } from "ol/style"; +import { + Image as ImageLayer, + Tile as TileLayer, + Vector as VectorLayer +} from "ol/layer"; +import { and as andFilter, equalTo } from "ol/format/filter"; + +import { HTTP } from "@/lib/http"; +import { ImageWMS as ImageSource } from "ol/source"; +import OSM from "ol/source/OSM"; +import Point from "ol/geom/Point"; +import TileWMS from "ol/source/TileWMS"; +import VectorSource from "ol/source/Vector"; +import { bbox as bboxStrategy } from "ol/loadingstrategy"; +import store from "@/store/index"; +import { styleFactory } from "./styles"; + +export const buildVectorLoader = ( + featureRequestOptions, + vectorSource, + bboxStrategyDisabled, + featurePostProcessor +) => { + // build a function to be used for VectorSource.setLoader() + // make use of WFS().writeGetFeature to build the request + // and use our HTTP library to actually do it + // NOTE: the geometryName has to be given in featureRequestOptions if + // bboxStrategy (default) is used + featureRequestOptions.featureNS = "gemma"; + featureRequestOptions.featurePrefix = "gemma"; + featureRequestOptions.outputFormat = "application/json"; + return (extent, resolution, projection) => { + if (!bboxStrategyDisabled) { + featureRequestOptions.bbox = extent; + } + featureRequestOptions.srsName = projection.getCode(); + HTTP.post( + "/internal/wfs", + new XMLSerializer().serializeToString( + new WFS().writeGetFeature(featureRequestOptions) + ), + { + headers: { + "X-Gemma-Auth": localStorage.getItem("token"), + "Content-type": "text/xml; charset=UTF-8" + } + } + ) + .then(response => { + const features = new GeoJSON().readFeatures( + JSON.stringify(response.data) + ); + if (featurePostProcessor) { + features.map(f => featurePostProcessor(f, store, features)); + } + vectorSource.addFeatures(features); + }) + .catch(() => { + vectorSource.removeLoadedExtent(extent); + store.dispatch("application/reportBackendError"); + }); + }; +}; + +// SHARED LAYERS: +// DRAW- and CUTLAYER are shared across maps. E.g. you want to see the cross cut +// arrow on both maps when comparing surveys. So we don't need to initialize a +// new VectorLayer object for each map. Instead we use these two constants so +// that all maps use the same object. +const DRAWLAYER = new VectorLayer({ + id: "DRAWTOOL", + label: "Draw Tool", + visible: true, + source: new VectorSource({ wrapX: false }), + style: function(feature) { + // adapted from OpenLayer's LineString Arrow Example + var geometry = feature.getGeometry(); + var styles = [ + // linestring + new Style({ + stroke: new Stroke({ + color: "#369aca", + width: 2 + }) + }) + ]; + + if (geometry.getType() === "LineString") { + geometry.forEachSegment(function(start, end) { + var dx = end[0] - start[0]; + var dy = end[1] - start[1]; + var rotation = Math.atan2(dy, dx); + // arrows + styles.push( + new Style({ + geometry: new Point(end), + image: new Icon({ + // we need to make sure the image is loaded by Vue Loader + src: require("@/assets/linestring_arrow.png"), + // fiddling with the anchor's y value does not help to + // position the image more centered on the line ending, as the + // default line style seems to be slightly uncentered in the + // anti-aliasing, but the image is not placed with subpixel + // precision + anchor: [0.75, 0.5], + rotateWithView: true, + rotation: -rotation + }) + }) + ); + }); + } + return styles; + } +}); + +const CUTLAYER = new VectorLayer({ + id: "CUTTOOL", + label: "Cut Tool", + visible: true, + source: new VectorSource({ wrapX: false }), + style: function(feature) { + // adapted from OpenLayer's LineString Arrow Example + var geometry = feature.getGeometry(); + var styles = [ + // linestring + new Style({ + stroke: new Stroke({ + color: "#FFFFFF", + width: 5, + lineDash: [7, 7] + }) + }), + new Style({ + stroke: new Stroke({ + color: "#333333", + width: 3, + lineDash: [7, 7] + }) + }) + ]; + + if (geometry.getType() === "LineString") { + geometry.forEachSegment(function(start, end) { + var dx = end[0] - start[0]; + var dy = end[1] - start[1]; + var rotation = Math.atan2(dy, dx); + // arrows + styles.push( + new Style({ + geometry: new Point(end), + image: new Icon({ + // we need to make sure the image is loaded by Vue Loader + src: require("@/assets/linestring_arrow_grey.png"), + // fiddling with the anchor's y value does not help to + // position the image more centered on the line ending, as the + // default line style seems to be slightly uncentered in the + // anti-aliasing, but the image is not placed with subpixel + // precision + anchor: [0.75, 0.5], + rotateWithView: true, + rotation: -rotation + }) + }) + ); + }); + } + return styles; + } +}); + +let layerConfigs = {}; + +export const unsetLayerConfigs = function() { + layerConfigs = {}; +}; + +export const layerFactory = function(mapId) { + const styles = styleFactory(mapId); + // Shared feature source for layers: + // BOTTLENECKS, BOTTLENECKSTATUS and BOTTLENECKFAIRWAYAVAILABILITY + // Reduces bottlenecks_geoserver requests and number of stored feature objects. + const FDREVIEWLAYER = new VectorLayer({ + id: "FDREVIEWLAYER", + label: "Review", + visible: true, + source: new VectorSource({ wrapX: false }), + style: styles.sections + }); + const bottlenecksSource = new VectorSource({ strategy: bboxStrategy }); + bottlenecksSource.setLoader( + buildVectorLoader( + { + featureTypes: ["bottlenecks_geoserver"], + geometryName: "area" + }, + bottlenecksSource, + false, + async (f, store) => { + if (f.get("fa_critical")) { + // look for fairway availability data in store. If present and + // not older than 15 min use it or fetch new data and store it. + let data = store.getters["fairwayavailability/fwLNWLOverviewData"](f); + if ( + data && + new Date().getTime() - data.createdAt.getTime() < 900000 + ) { + f.set("fa_data", data.data); + } else { + let date = new Date(); + data = await store.dispatch( + "fairwayavailability/loadAvailableFairwayDepthLNWLForMap", + { + feature: f, + from: date.toISOString().split("T")[0], + to: date.toISOString().split("T")[0], + frequency: "monthly", + LOS: 3 + } + ); + if (data) { + store.commit("fairwayavailability/addFwLNWLOverviewData", { + feature: f, + data, + createdAt: new Date() + }); + f.set("fa_data", data); + } + } + } + return f; + } + ) + ); + + // either use existing config or create new one + // important is only each map has its individual layer config + // but we don't want to create new layer objects each time a store value + // that is used here changes. + const config = layerConfigs.hasOwnProperty(mapId) + ? layerConfigs[mapId] + : [ + new TileLayer({ + id: "OPENSTREETMAP", + label: "Open Streetmap", + visible: true, + source: new OSM() + }), + new ImageLayer({ + id: "INLANDECDIS", + label: "Inland ECDIS chart Danube", + visible: true, + source: null + }), + new ImageLayer({ + id: "WATERWAYAREA", + label: "Waterway Area", + maxResolution: 100, + minResolution: 0, + source: new ImageSource({ + url: window.location.origin + "/api/internal/wms", + params: { + LAYERS: "waterway_area", + VERSION: "1.1.1", + TILED: true + }, + imageLoadFunction: function(tile, src) { + HTTP.get(src, { + headers: { + "X-Gemma-Auth": localStorage.getItem("token") + }, + responseType: "blob" + }).then(response => { + tile.getImage().src = URL.createObjectURL(response.data); + }); + } // TODO tile.setState(TileState.ERROR); + }) + }), + (function() { + const source = new VectorSource({ strategy: bboxStrategy }); + source.setLoader( + buildVectorLoader( + { + featureTypes: ["stretches_geoserver"], + geometryName: "area" + }, + source, + true, + (f, store) => { + if (f.getId() === store.state.imports.selectedStretchId) { + f.set("highlighted", true); + } + return f; + } + ) + ); + return new VectorLayer({ + id: "STRETCHES", + label: "Stretches", + visible: false, + style: styles.stretches, + source + }); + })(), + (function() { + const source = new VectorSource({ strategy: bboxStrategy }); + source.setLoader( + buildVectorLoader( + { + featureTypes: ["sections_geoserver"], + geometryName: "area" + }, + source, + true, + (f, store) => { + if (f.getId() === store.state.imports.selectedSectionId) { + f.set("highlighted", true); + } + return f; + } + ) + ); + return new VectorLayer({ + id: "SECTIONS", + label: "Sections", + visible: false, + style: styles.sections, + source + }); + })(), + (function() { + return new VectorLayer({ + id: "BOTTLENECKS", + label: "Bottlenecks", + visible: true, + style: styles.bottleneck, + source: bottlenecksSource + }); + })(), + new TileLayer({ + id: "BOTTLENECKISOLINE", + label: "Bottleneck morphology", + visible: false, + source: new TileWMS({ + preload: 0, + projection: "EPSG:3857", + url: window.location.origin + "/api/internal/wms", + params: { + LAYERS: "sounding_results_areas_geoserver", + VERSION: "1.1.1", + TILED: true + }, + tileLoadFunction: function(tile, src) { + HTTP.get(src, { + headers: { + "X-Gemma-Auth": localStorage.getItem("token") + }, + responseType: "blob" + }) + .then(response => { + tile.getImage().src = URL.createObjectURL(response.data); + }) + .catch(() => { + store.dispatch("application/reportBackendError"); + }); + } // TODO tile.setState(TileState.ERROR); + }) + }), + new TileLayer({ + id: "DIFFERENCES", + label: "Bottleneck Differences", + visible: false, + source: new TileWMS({ + preload: 0, + projection: "EPSG:3857", + url: window.location.origin + "/api/internal/wms", + params: { + LAYERS: "sounding_differences", + VERSION: "1.1.1", + TILED: true, + CQL_FILTER: "id=" + store.state.fairwayprofile.currentDifference + }, + tileLoadFunction: function(tile, src) { + HTTP.get(src, { + headers: { + "X-Gemma-Auth": localStorage.getItem("token") + }, + responseType: "blob" + }) + .then(response => { + tile.getImage().src = URL.createObjectURL(response.data); + }) + .catch(() => { + store.dispatch("application/reportBackendError"); + }); + } // TODO tile.setState(TileState.ERROR); + }) + }), + (function() { + const source = new VectorSource({ strategy: bboxStrategy }); + source.setLoader( + buildVectorLoader( + { + geometryName: "area", + featureTypes: ["fairway_dimensions"], + filter: andFilter( + equalTo("level_of_service", 1), + equalTo("staging_done", true) + ) + }, + source, + false + ) + ); + return new VectorLayer({ + id: "FAIRWAYDIMENSIONSLOS1", + label: "LOS 1 Fairway Dimensions", + visible: false, + style: styles.fwd1, + maxResolution: 80, + minResolution: 0, + source + }); + })(), + (function() { + const source = new VectorSource({ strategy: bboxStrategy }); + source.setLoader( + buildVectorLoader( + { + geometryName: "area", + featureTypes: ["fairway_dimensions"], + filter: andFilter( + equalTo("level_of_service", 2), + equalTo("staging_done", true) + ) + }, + source, + false + ) + ); + return new VectorLayer({ + id: "FAIRWAYDIMENSIONSLOS2", + label: "LOS 2 Fairway Dimensions", + visible: false, + style: styles.fwd2, + maxResolution: 80, + minResolution: 0, + source + }); + })(), + (function() { + const source = new VectorSource({ strategy: bboxStrategy }); + source.setLoader( + buildVectorLoader( + { + geometryName: "area", + featureTypes: ["fairway_dimensions"], + filter: andFilter( + equalTo("level_of_service", 3), + equalTo("staging_done", true) + ) + }, + source, + false + ) + ); + return new VectorLayer({ + id: "FAIRWAYDIMENSIONSLOS3", + label: "LOS 3 Fairway Dimensions", + visible: true, + style: styles.fwd3, + maxResolution: 80, + minResolution: 0, + source + }); + })(), + new ImageLayer({ + id: "WATERWAYAXIS", + label: "Waterway Axis", + source: new ImageSource({ + url: window.location.origin + "/api/internal/wms", + params: { + LAYERS: "waterway_axis", + VERSION: "1.1.1", + TILED: true, + TIME: store.state.application.refreshLayersTime.toISOString() + }, + imageLoadFunction: function(tile, src) { + HTTP.get(src, { + headers: { + "X-Gemma-Auth": localStorage.getItem("token") + }, + responseType: "blob" + }) + .then(response => { + tile.getImage().src = URL.createObjectURL(response.data); + }) + .catch(() => { + store.dispatch("application/reportBackendError"); + }); + } // TODO tile.setState(TileState.ERROR); + }) + }), + (function() { + const source = new VectorSource({ strategy: bboxStrategy }); + source.setLoader( + buildVectorLoader( + { + featureTypes: ["waterway_profiles"], + geometryName: "geom" + }, + source + ) + ); + return new VectorLayer({ + id: "WATERWAYPROFILES", + label: "Waterway Profiles", + visible: true, + style: new Style({ + stroke: new Stroke({ + color: "rgba(0, 0, 255, .5)", + lineDash: [5, 5], + width: 2 + }) + }), + maxResolution: 2.5, + minResolution: 0, + source + }); + })(), + (function() { + return new VectorLayer({ + id: "BOTTLENECKSTATUS", + label: "Critical Bottlenecks", + forLegendStyle: { point: true, resolution: 16 }, + visible: true, + zIndex: 1, + style: styles.bottleneckStatus, + source: bottlenecksSource + }); + })(), + (function() { + return new VectorLayer({ + id: "BOTTLENECKFAIRWAYAVAILABILITY", + label: "Bottleneck Fairway Availability", + forLegendStyle: { point: true, resolution: 16 }, + visible: false, + zIndex: 1, + style: styles.bottleneckFairwayAvailability, + source: bottlenecksSource + }); + })(), + (function() { + const source = new VectorSource({ strategy: bboxStrategy }); + source.setLoader( + buildVectorLoader( + { + featureTypes: [ + "bottlenecks_geoserver", + "gauges_geoserver", + "stretches_geoserver", + "sections_geoserver" + ] + }, + source, + true, + // since we don't use bbox strategy, features will contain all features and we can use it + // to find reference gauges for bottlenecks, yeah! + async (f, store, features) => { + // attach reference gauge to bottleneck + if (f.getId().indexOf("bottlenecks") > -1) { + f.set( + "gauge_obj", + features.find(feat => { + return ( + feat.getId().indexOf("gauges") > -1 && + feat.get("objname") === f.get("gauge_objname") + ); + }) + ); + } + + // attach nsc data to gauge + if (f.getId().indexOf("gauges") > -1) { + store + .dispatch( + "gauges/getNashSutcliffeForISRS", + f.get("isrs_code") + ) + .then(response => { + f.set("nsc_data", response); + }); + } + } + ) + ); + return new VectorLayer({ + id: "DATAAVAILABILITY", + label: "Data Availability/Accuracy", + forLegendStyle: { point: true, resolution: 16 }, + visible: false, + zIndex: 1, + style: styles.dataAvailability, + source + }); + })(), + new ImageLayer({ + id: "DISTANCEMARKS", + label: "Distance Marks", + maxResolution: 10, + minResolution: 0, + source: new ImageSource({ + url: window.location.origin + "/api/internal/wms", + params: { + LAYERS: "distance_marks_ashore_geoserver", + VERSION: "1.1.1", + TILED: true + }, + imageLoadFunction: function(tile, src) { + HTTP.get(src, { + headers: { + "X-Gemma-Auth": localStorage.getItem("token") + }, + responseType: "blob" + }) + .then(response => { + tile.getImage().src = URL.createObjectURL(response.data); + }) + .catch(() => { + store.dispatch("application/reportBackendError"); + }); + } // TODO tile.setState(TileState.ERROR); + }) + }), + new ImageLayer({ + id: "DISTANCEMARKSAXIS", + label: "Distance Marks, Axis", + source: new ImageSource({ + url: window.location.origin + "/api/internal/wms", + params: { + LAYERS: "distance_marks_geoserver", + VERSION: "1.1.1", + TILED: true + }, + imageLoadFunction: function(tile, src) { + HTTP.get(src, { + headers: { + "X-Gemma-Auth": localStorage.getItem("token") + }, + responseType: "blob" + }).then(response => { + tile.getImage().src = URL.createObjectURL(response.data); + }); + } // TODO tile.setState(TileState.ERROR); + }) + }), + (function() { + const source = new VectorSource({ strategy: bboxStrategy }); + source.setLoader( + buildVectorLoader( + { + featureTypes: ["gauges_geoserver"], + geometryName: "geom" + }, + source + ) + ); + return new VectorLayer({ + id: "GAUGES", + label: "Gauges", + forLegendStyle: { point: true, resolution: 8 }, + visible: true, + style: styles.gauge, + maxResolution: 100, + minResolution: 0, + source + }); + })(), + new TileLayer({ + id: "FAIRWAYMARKS", + label: "Fairway marks", + visible: true, + source: new TileWMS({ + preload: 0, + projection: "EPSG:3857", + url: window.location.origin + "/api/internal/wms", + params: { + LAYERS: "fairway_marks", + VERSION: "1.1.1", + TILED: true, + TIME: store.state.application.refreshLayersTime.toISOString() + }, + tileLoadFunction: function(tile, src) { + HTTP.get(src, { + headers: { + "X-Gemma-Auth": localStorage.getItem("token") + }, + responseType: "blob" + }) + .then(response => { + tile.getImage().src = URL.createObjectURL(response.data); + }) + .catch(() => { + store.dispatch("application/reportBackendError"); + }); + } // TODO tile.setState(TileState.ERROR); + }) + }), + DRAWLAYER, + CUTLAYER, + FDREVIEWLAYER + ]; + + layerConfigs[mapId] = config; + + return { + get(id) { + return config.find(l => l.get("id") === id); + }, + config + }; +};
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/client/src/components/layers/styles.js Tue Mar 24 11:23:26 2020 +0100 @@ -0,0 +1,400 @@ +import { Icon, Stroke, Style, Fill, Text, Circle } from "ol/style"; +import Point from "ol/geom/Point"; +import { getCenter } from "ol/extent"; +import store from "@/store/index"; +import classifications from "../../lib/classifications"; + +const styles = { + blue1: new Style({ + stroke: new Stroke({ + color: [0, 0, 255, 0.8], + lineDash: [2, 4], + lineCap: "round", + width: 2 + }), + fill: new Fill({ + color: [240, 230, 0, 0.2] + }) + }), + blue2: new Style({ + stroke: new Stroke({ + color: [0, 0, 255, 0.9], + lineDash: [3, 6], + lineCap: "round", + width: 2 + }), + fill: new Fill({ + color: [240, 230, 0, 0.1] + }) + }), + blue3: new Style({ + stroke: new Stroke({ + color: [0, 0, 255, 1.0], + width: 2 + }), + fill: new Fill({ + color: [255, 255, 255, 0.4] + }) + }), + yellow1: new Style({ + stroke: new Stroke({ + color: "rgba(230, 230, 10, .8)", + width: 4 + }), + fill: new Fill({ + color: "rgba(230, 230, 10, .3)" + }) + }), + yellow2: new Style({ + stroke: new Stroke({ + color: "rgba(250, 200, 0, .8)", + width: 2 + }), + fill: new Fill({ + color: "rgba(250, 200, 10, .3)" + }) + }), + yellow3: new Style({ + stroke: new Stroke({ + color: "rgba(250, 240, 10, .9)", + width: 5 + }), + fill: new Fill({ + color: "rgba(250, 240, 0, .7)" + }) + }), + orange1: new Style({ + stroke: new Stroke({ + color: "rgba(255, 150, 10, .8)", + width: 2 + }), + fill: new Fill({ + color: "rgba(255, 150, 0, .3)" + }) + }), + orange2: new Style({ + stroke: new Stroke({ + color: "rgba(255, 166, 10, .9)", + width: 5 + }), + fill: new Fill({ + color: "rgba(255, 166, 0, .7)" + }) + }), + red1: new Style({ + stroke: new Stroke({ + color: "rgba(255, 0, 0, 1)", + width: 4 + }) + }), + circleBlue: new Style({ + image: new Circle({ + radius: 5, + fill: new Fill({ color: "rgba(255, 0, 0, 0.1)" }), + stroke: new Stroke({ color: "blue", width: 1 }) + }) + }), + textFW1: new Style({ + text: new Text({ + font: 'bold 12px "Open Sans", "sans-serif"', + placement: "line", + fill: new Fill({ + color: "black" + }), + text: "LOS: 1" + //, zIndex: 10 + }) + }), + textFW2: new Style({ + text: new Text({ + font: 'bold 12px "Open Sans", "sans-serif"', + placement: "line", + fill: new Fill({ + color: "black" + }), + text: "LOS: 2" + //, zIndex: 10 + }) + }), + textFW3: new Style({ + text: new Text({ + font: 'bold 12px "Open Sans", "sans-serif"', + placement: "line", + fill: new Fill({ + color: "black" + }), + text: "LOS: 3" + //, zIndex: 10 + }) + }) +}; + +const styleFactory = function(mapId) { + const recencyColorCodes = { + OK: "lime", + WARNING: "yellow", + DANGER: "red", + NEUTRAL: "white" + }; + const gmAvailabilityColorCodes = { + OK: "lime", + WARNING: "yellow", + DANGER: "red", + NEUTRAL: "white" + }; + const forecastAccuracyColorCodes = { + OK: "lime", + WARNING: "yellow", + DANGER: "red", + NEUTRAL: "white" + }; + + const forecastVsRealityColorCodes = { + OK: "lime", + WARNING: "yellow", + DANGER: "red", + NEUTRAL: "white" + }; + return { + recencyColorCodes: recencyColorCodes, + gmAvailabilityColorCodes: gmAvailabilityColorCodes, + forecastAccuracyColorCodes: forecastAccuracyColorCodes, + forecastVsRealityColorCodes: forecastVsRealityColorCodes, + stretches(feature) { + let style = styles.yellow2; + if (feature.get("highlighted")) { + style = styles.yellow3; + } + return style; + }, + sections(feature) { + let style = styles.orange1; + if (feature.get("highlighted")) { + style = styles.orange2; + } + return style; + }, + fwd1() { + return [styles.blue1, styles.textFW1]; + }, + fwd2() { + return [styles.blue2, styles.textFW2]; + }, + fwd3() { + return [styles.blue3, styles.textFW3]; + }, + bottleneck() { + return styles.yellow1; + }, + bottleneckStatus(feature, resolution, isLegend) { + let s = []; + if ((feature.get("fa_critical") && resolution > 15) || isLegend) { + let bnCenter = getCenter(feature.getGeometry().getExtent()); + s.push( + new Style({ + geometry: new Point(bnCenter), + image: new Icon({ + src: require("@/assets/marker-bottleneck-critical.png"), + anchor: [0.5, 0.5], + scale: isLegend ? 0.5 : 1 + }) + }) + ); + } + if (feature.get("fa_critical") && !isLegend) { + s.push(styles.red1); + } + return s; + }, + bottleneckFairwayAvailability(feature, resolution, isLegend) { + let s = []; + if (isLegend) { + s.push( + new Style({ + image: new Icon({ + src: require("@/assets/fa-diagram.png"), + anchor: [0.5, 0.5], + scale: 1 + }) + }) + ); + } + if (feature.get("fa_critical") && feature.get("fa_data")) { + let data = feature.get("fa_data"); + const heightInPixel = 80; + const relativeHeightInPercent = heightInPixel / 100; + let lnwlHeight = relativeHeightInPercent * data.ldc; + let belowThresholdHeight = relativeHeightInPercent * data.below; + let betweenThresholdHeight = relativeHeightInPercent * data.between; + let aboveThresholdHeight = relativeHeightInPercent * data.above; + let lnwl = `<rect x='2' y='${2 + + heightInPixel - + lnwlHeight}' width='10' height='${lnwlHeight}' stroke-width='0' fill='aqua'/>`; + let above = `<rect x='12' y='${2 + + heightInPixel - + aboveThresholdHeight}' width='18' height='${aboveThresholdHeight}' stroke-width='0' fill='blue'/>`; + let between = `<rect x='12' y='${2 + + heightInPixel - + aboveThresholdHeight - + betweenThresholdHeight}' width='18' height='${betweenThresholdHeight}' stroke-width='0' fill='darksalmon'/>`; + let below = `<rect x='12' y='${2 + + heightInPixel - + aboveThresholdHeight - + betweenThresholdHeight - + belowThresholdHeight}' width='18' height='${belowThresholdHeight}' stroke-width='0' fill='hotpink'/>`; + let frame = `<rect x='0' y='0' width='32' height='84' stroke-width='0' fill='white'/>`; + let svg = `data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' width='32' height='84'><g>${frame}${lnwl}${above}${between}${below}</g></svg>`; + let bnCenter = getCenter(feature.getGeometry().getExtent()); + s.push( + new Style({ + geometry: new Point(bnCenter), + image: new Icon({ + src: svg, + anchor: [1.2, 1.2] + }) + }) + ); + } + return s; + }, + dataAvailability(feature, resolution, isLegend) { + let s = []; + if (isLegend) { + s.push( + new Style({ + image: new Icon({ + src: require("@/assets/da-diagram.png"), + anchor: [0.5, 0.5], + scale: 1 + }) + }) + ); + } else { + // TODO: Get information from feature and check the ranges according to #423, #424, #425 + let colorWaterlevel = + gmAvailabilityColorCodes[classifications.gmAvailability(feature)]; + let colorComparison = + forecastVsRealityColorCodes[ + classifications.forecastVsReality(feature) + ]; + let colorAccuracy = + forecastAccuracyColorCodes[classifications.forecastAccuracy(feature)]; + let map = store.getters["map/openLayersMap"](mapId); + let geom = feature.getGeometry(); + if (!(geom instanceof Point)) { + geom = new Point(getCenter(feature.getGeometry().getExtent())); + } + if ( + (map.getLayer("BOTTLENECKS").getVisible() && + feature.getId().indexOf("bottlenecks") > -1) || + (map.getLayer("SECTIONS").getVisible() && + feature.getId().indexOf("sections") > -1) || + (map.getLayer("STRETCHES").getVisible() && + feature.getId().indexOf("stretches") > -1) || + (map.getLayer("GAUGES").getVisible() && + feature.getId().indexOf("gauges") > -1) + ) { + let frame = `<polyline points='16,0 32,28 0,28 16,0' stroke='grey' stroke-width='1' fill='white'/>`; + let waterlevel = `<polyline points="16,0 24,14 16,28 8,14 16,0" stroke='grey' stroke-width='1' fill='${colorWaterlevel}'/>`; + let accuracy = `<polyline points="24,14 32,28 16,28 24,14" stroke='grey' stroke-width='1' fill='${colorAccuracy}'/>`; + let comparison = `<polyline points="8,14 16,28 0,28 8,14" stroke='grey' stroke-width='1' fill='${colorComparison}'/>`; + let svg = `data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' width='32' height='28'><g>${frame}${waterlevel}${comparison}${accuracy}</g></svg>`; + s.push( + new Style({ + geometry: geom, + image: new Icon({ + src: svg, + anchor: [-0.5, 1] + }) + }) + ); + } + + if ( + map.getLayer("BOTTLENECKS").getVisible() && + feature.getId().indexOf("bottlenecks") > -1 + ) { + let colorUniformTriangle = + recencyColorCodes[classifications.surveyRecency(feature)]; + let frame = `<polyline points='16,0 32,28 0,28 16,0' stroke='grey' stroke-width='1' fill='${colorUniformTriangle}'/>`; + let svg = `data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' width='32' height='28'><g>${frame}</g></svg>`; + s.push( + new Style({ + geometry: geom, + image: new Icon({ + src: svg, + anchor: [0.5, 1] + }) + }) + ); + } + } + return s; + }, + dma(feature, resolution) { + if (resolution < 10) { + var s = styles.circleBlue; + if (resolution < 6) { + s.setText( + new Text({ + offsetY: 12, + font: '10px "Open Sans", "sans-serif"', + fill: new Fill({ + color: "black" + }), + text: (feature.get("hectometre") / 10).toString() + }) + ); + } + return s; + } + return []; + }, + gauge(feature, resolution, isLegend) { + let waterlevel = feature.get("gm_waterlevel"); + let text = feature.get("objname"); + let iconColor = "white"; + if (waterlevel) { + text += "\n(" + waterlevel + " cm)"; + let refWaterlevels = JSON.parse(feature.get("reference_water_levels")); + if (refWaterlevels) { + const HDC = + refWaterlevels[ + Object.keys(refWaterlevels).find(e => /HDC/.test(e)) + ]; + const LDC = + refWaterlevels[ + Object.keys(refWaterlevels).find(e => /LDC/.test(e)) + ]; + if (waterlevel < LDC) iconColor = "brown"; + if (waterlevel > LDC && waterlevel < HDC) iconColor = "blue"; + if (waterlevel > HDC) iconColor = "red"; + } + } + + return [ + new Style({ + image: new Icon({ + src: require("@/assets/marker-gauge-" + iconColor + ".png"), + anchor: [0.5, isLegend ? 0.5 : 1], + scale: isLegend ? 0.5 : 1 + }), + text: new Text({ + font: '10px "Open Sans", "sans-serif"', + offsetY: 15, + fill: new Fill({ + color: "black" + }), + backgroundFill: new Fill({ + color: "rgba(255, 255, 255, 0.7)" + }), + padding: [2, 2, 2, 2], + text + }) + }) + ]; + } + }; +}; + +export { styles, styleFactory };
--- a/client/src/components/map/Map.vue Fri Mar 20 17:24:03 2020 +0100 +++ b/client/src/components/map/Map.vue Tue Mar 24 11:23:26 2020 +0100 @@ -41,9 +41,9 @@ import { Stroke, Style, Fill } from "ol/style"; import { displayError } from "@/lib/errors"; import { pane } from "@/lib/mixins"; -import { layerFactory } from "@/components/map/layers"; +import { layerFactory } from "@/components/layers/layers"; import { ImageWMS as ImageSource } from "ol/source"; -import { styles } from "./styles"; +import { styles } from "@/components/layers/styles"; import "ol/ol.css"; /* for the sake of debugging */
--- a/client/src/components/map/Zoom.vue Fri Mar 20 17:24:03 2020 +0100 +++ b/client/src/components/map/Zoom.vue Tue Mar 24 11:23:26 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();
--- a/client/src/components/map/layers.js Fri Mar 20 17:24:03 2020 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,722 +0,0 @@ -import { GeoJSON, WFS } from "ol/format"; -import { Icon, Stroke, Style } from "ol/style"; -import { - Image as ImageLayer, - Tile as TileLayer, - Vector as VectorLayer -} from "ol/layer"; -import { and as andFilter, equalTo } from "ol/format/filter"; - -import { HTTP } from "@/lib/http"; -import { ImageWMS as ImageSource } from "ol/source"; -import OSM from "ol/source/OSM"; -import Point from "ol/geom/Point"; -import TileWMS from "ol/source/TileWMS"; -import VectorSource from "ol/source/Vector"; -import { bbox as bboxStrategy } from "ol/loadingstrategy"; -import store from "@/store/index"; -import { styleFactory } from "./styles"; - -export const buildVectorLoader = ( - featureRequestOptions, - vectorSource, - bboxStrategyDisabled, - featurePostProcessor -) => { - // build a function to be used for VectorSource.setLoader() - // make use of WFS().writeGetFeature to build the request - // and use our HTTP library to actually do it - // NOTE: the geometryName has to be given in featureRequestOptions if - // bboxStrategy (default) is used - featureRequestOptions.featureNS = "gemma"; - featureRequestOptions.featurePrefix = "gemma"; - featureRequestOptions.outputFormat = "application/json"; - return (extent, resolution, projection) => { - if (!bboxStrategyDisabled) { - featureRequestOptions.bbox = extent; - } - featureRequestOptions.srsName = projection.getCode(); - HTTP.post( - "/internal/wfs", - new XMLSerializer().serializeToString( - new WFS().writeGetFeature(featureRequestOptions) - ), - { - headers: { - "X-Gemma-Auth": localStorage.getItem("token"), - "Content-type": "text/xml; charset=UTF-8" - } - } - ) - .then(response => { - const features = new GeoJSON().readFeatures( - JSON.stringify(response.data) - ); - if (featurePostProcessor) { - features.map(f => featurePostProcessor(f, store, features)); - } - vectorSource.addFeatures(features); - }) - .catch(() => { - vectorSource.removeLoadedExtent(extent); - store.dispatch("application/reportBackendError"); - }); - }; -}; - -// SHARED LAYERS: -// DRAW- and CUTLAYER are shared across maps. E.g. you want to see the cross cut -// arrow on both maps when comparing surveys. So we don't need to initialize a -// new VectorLayer object for each map. Instead we use these two constants so -// that all maps use the same object. -const DRAWLAYER = new VectorLayer({ - id: "DRAWTOOL", - label: "Draw Tool", - visible: true, - source: new VectorSource({ wrapX: false }), - style: function(feature) { - // adapted from OpenLayer's LineString Arrow Example - var geometry = feature.getGeometry(); - var styles = [ - // linestring - new Style({ - stroke: new Stroke({ - color: "#369aca", - width: 2 - }) - }) - ]; - - if (geometry.getType() === "LineString") { - geometry.forEachSegment(function(start, end) { - var dx = end[0] - start[0]; - var dy = end[1] - start[1]; - var rotation = Math.atan2(dy, dx); - // arrows - styles.push( - new Style({ - geometry: new Point(end), - image: new Icon({ - // we need to make sure the image is loaded by Vue Loader - src: require("@/assets/linestring_arrow.png"), - // fiddling with the anchor's y value does not help to - // position the image more centered on the line ending, as the - // default line style seems to be slightly uncentered in the - // anti-aliasing, but the image is not placed with subpixel - // precision - anchor: [0.75, 0.5], - rotateWithView: true, - rotation: -rotation - }) - }) - ); - }); - } - return styles; - } -}); - -const CUTLAYER = new VectorLayer({ - id: "CUTTOOL", - label: "Cut Tool", - visible: true, - source: new VectorSource({ wrapX: false }), - style: function(feature) { - // adapted from OpenLayer's LineString Arrow Example - var geometry = feature.getGeometry(); - var styles = [ - // linestring - new Style({ - stroke: new Stroke({ - color: "#FFFFFF", - width: 5, - lineDash: [7, 7] - }) - }), - new Style({ - stroke: new Stroke({ - color: "#333333", - width: 3, - lineDash: [7, 7] - }) - }) - ]; - - if (geometry.getType() === "LineString") { - geometry.forEachSegment(function(start, end) { - var dx = end[0] - start[0]; - var dy = end[1] - start[1]; - var rotation = Math.atan2(dy, dx); - // arrows - styles.push( - new Style({ - geometry: new Point(end), - image: new Icon({ - // we need to make sure the image is loaded by Vue Loader - src: require("@/assets/linestring_arrow_grey.png"), - // fiddling with the anchor's y value does not help to - // position the image more centered on the line ending, as the - // default line style seems to be slightly uncentered in the - // anti-aliasing, but the image is not placed with subpixel - // precision - anchor: [0.75, 0.5], - rotateWithView: true, - rotation: -rotation - }) - }) - ); - }); - } - return styles; - } -}); - -let layerConfigs = {}; - -export const unsetLayerConfigs = function() { - layerConfigs = {}; -}; - -export const layerFactory = function(mapId) { - const styles = styleFactory(mapId); - // Shared feature source for layers: - // BOTTLENECKS, BOTTLENECKSTATUS and BOTTLENECKFAIRWAYAVAILABILITY - // Reduces bottlenecks_geoserver requests and number of stored feature objects. - const FDREVIEWLAYER = new VectorLayer({ - id: "FDREVIEWLAYER", - label: "Review", - visible: true, - source: new VectorSource({ wrapX: false }), - style: styles.sections - }); - const bottlenecksSource = new VectorSource({ strategy: bboxStrategy }); - bottlenecksSource.setLoader( - buildVectorLoader( - { - featureTypes: ["bottlenecks_geoserver"], - geometryName: "area" - }, - bottlenecksSource, - false, - async (f, store) => { - if (f.get("fa_critical")) { - // look for fairway availability data in store. If present and - // not older than 15 min use it or fetch new data and store it. - let data = store.getters["fairwayavailability/fwLNWLOverviewData"](f); - if ( - data && - new Date().getTime() - data.createdAt.getTime() < 900000 - ) { - f.set("fa_data", data.data); - } else { - let date = new Date(); - data = await store.dispatch( - "fairwayavailability/loadAvailableFairwayDepthLNWLForMap", - { - feature: f, - from: date.toISOString().split("T")[0], - to: date.toISOString().split("T")[0], - frequency: "monthly", - LOS: 3 - } - ); - if (data) { - store.commit("fairwayavailability/addFwLNWLOverviewData", { - feature: f, - data, - createdAt: new Date() - }); - f.set("fa_data", data); - } - } - } - return f; - } - ) - ); - - // either use existing config or create new one - // important is only each map has its individual layer config - // but we don't want to create new layer objects each time a store value - // that is used here changes. - const config = layerConfigs.hasOwnProperty(mapId) - ? layerConfigs[mapId] - : [ - new TileLayer({ - id: "OPENSTREETMAP", - label: "Open Streetmap", - visible: true, - source: new OSM() - }), - new ImageLayer({ - id: "INLANDECDIS", - label: "Inland ECDIS chart Danube", - visible: true, - source: null - }), - new ImageLayer({ - id: "WATERWAYAREA", - label: "Waterway Area", - maxResolution: 100, - minResolution: 0, - source: new ImageSource({ - url: window.location.origin + "/api/internal/wms", - params: { - LAYERS: "waterway_area", - VERSION: "1.1.1", - TILED: true - }, - imageLoadFunction: function(tile, src) { - HTTP.get(src, { - headers: { - "X-Gemma-Auth": localStorage.getItem("token") - }, - responseType: "blob" - }).then(response => { - tile.getImage().src = URL.createObjectURL(response.data); - }); - } // TODO tile.setState(TileState.ERROR); - }) - }), - (function() { - const source = new VectorSource({ strategy: bboxStrategy }); - source.setLoader( - buildVectorLoader( - { - featureTypes: ["stretches_geoserver"], - geometryName: "area" - }, - source, - true, - (f, store) => { - if (f.getId() === store.state.imports.selectedStretchId) { - f.set("highlighted", true); - } - return f; - } - ) - ); - return new VectorLayer({ - id: "STRETCHES", - label: "Stretches", - visible: false, - style: styles.stretches, - source - }); - })(), - (function() { - const source = new VectorSource({ strategy: bboxStrategy }); - source.setLoader( - buildVectorLoader( - { - featureTypes: ["sections_geoserver"], - geometryName: "area" - }, - source, - true, - (f, store) => { - if (f.getId() === store.state.imports.selectedSectionId) { - f.set("highlighted", true); - } - return f; - } - ) - ); - return new VectorLayer({ - id: "SECTIONS", - label: "Sections", - visible: false, - style: styles.sections, - source - }); - })(), - (function() { - return new VectorLayer({ - id: "BOTTLENECKS", - label: "Bottlenecks", - visible: true, - style: styles.bottleneck, - source: bottlenecksSource - }); - })(), - new TileLayer({ - id: "BOTTLENECKISOLINE", - label: "Bottleneck morphology", - visible: false, - source: new TileWMS({ - preload: 0, - projection: "EPSG:3857", - url: window.location.origin + "/api/internal/wms", - params: { - LAYERS: "sounding_results_areas_geoserver", - VERSION: "1.1.1", - TILED: true - }, - tileLoadFunction: function(tile, src) { - HTTP.get(src, { - headers: { - "X-Gemma-Auth": localStorage.getItem("token") - }, - responseType: "blob" - }) - .then(response => { - tile.getImage().src = URL.createObjectURL(response.data); - }) - .catch(() => { - store.dispatch("application/reportBackendError"); - }); - } // TODO tile.setState(TileState.ERROR); - }) - }), - new TileLayer({ - id: "DIFFERENCES", - label: "Bottleneck Differences", - visible: false, - source: new TileWMS({ - preload: 0, - projection: "EPSG:3857", - url: window.location.origin + "/api/internal/wms", - params: { - LAYERS: "sounding_differences", - VERSION: "1.1.1", - TILED: true, - CQL_FILTER: "id=" + store.state.fairwayprofile.currentDifference - }, - tileLoadFunction: function(tile, src) { - HTTP.get(src, { - headers: { - "X-Gemma-Auth": localStorage.getItem("token") - }, - responseType: "blob" - }) - .then(response => { - tile.getImage().src = URL.createObjectURL(response.data); - }) - .catch(() => { - store.dispatch("application/reportBackendError"); - }); - } // TODO tile.setState(TileState.ERROR); - }) - }), - (function() { - const source = new VectorSource({ strategy: bboxStrategy }); - source.setLoader( - buildVectorLoader( - { - geometryName: "area", - featureTypes: ["fairway_dimensions"], - filter: andFilter( - equalTo("level_of_service", 1), - equalTo("staging_done", true) - ) - }, - source, - false - ) - ); - return new VectorLayer({ - id: "FAIRWAYDIMENSIONSLOS1", - label: "LOS 1 Fairway Dimensions", - visible: false, - style: styles.fwd1, - maxResolution: 80, - minResolution: 0, - source - }); - })(), - (function() { - const source = new VectorSource({ strategy: bboxStrategy }); - source.setLoader( - buildVectorLoader( - { - geometryName: "area", - featureTypes: ["fairway_dimensions"], - filter: andFilter( - equalTo("level_of_service", 2), - equalTo("staging_done", true) - ) - }, - source, - false - ) - ); - return new VectorLayer({ - id: "FAIRWAYDIMENSIONSLOS2", - label: "LOS 2 Fairway Dimensions", - visible: false, - style: styles.fwd2, - maxResolution: 80, - minResolution: 0, - source - }); - })(), - (function() { - const source = new VectorSource({ strategy: bboxStrategy }); - source.setLoader( - buildVectorLoader( - { - geometryName: "area", - featureTypes: ["fairway_dimensions"], - filter: andFilter( - equalTo("level_of_service", 3), - equalTo("staging_done", true) - ) - }, - source, - false - ) - ); - return new VectorLayer({ - id: "FAIRWAYDIMENSIONSLOS3", - label: "LOS 3 Fairway Dimensions", - visible: true, - style: styles.fwd3, - maxResolution: 80, - minResolution: 0, - source - }); - })(), - new ImageLayer({ - id: "WATERWAYAXIS", - label: "Waterway Axis", - source: new ImageSource({ - url: window.location.origin + "/api/internal/wms", - params: { - LAYERS: "waterway_axis", - VERSION: "1.1.1", - TILED: true - }, - imageLoadFunction: function(tile, src) { - HTTP.get(src, { - headers: { - "X-Gemma-Auth": localStorage.getItem("token") - }, - responseType: "blob" - }) - .then(response => { - tile.getImage().src = URL.createObjectURL(response.data); - }) - .catch(() => { - store.dispatch("application/reportBackendError"); - }); - } // TODO tile.setState(TileState.ERROR); - }) - }), - (function() { - const source = new VectorSource({ strategy: bboxStrategy }); - source.setLoader( - buildVectorLoader( - { - featureTypes: ["waterway_profiles"], - geometryName: "geom" - }, - source - ) - ); - return new VectorLayer({ - id: "WATERWAYPROFILES", - label: "Waterway Profiles", - visible: true, - style: new Style({ - stroke: new Stroke({ - color: "rgba(0, 0, 255, .5)", - lineDash: [5, 5], - width: 2 - }) - }), - maxResolution: 2.5, - minResolution: 0, - source - }); - })(), - (function() { - return new VectorLayer({ - id: "BOTTLENECKSTATUS", - label: "Critical Bottlenecks", - forLegendStyle: { point: true, resolution: 16 }, - visible: true, - zIndex: 1, - style: styles.bottleneckStatus, - source: bottlenecksSource - }); - })(), - (function() { - return new VectorLayer({ - id: "BOTTLENECKFAIRWAYAVAILABILITY", - label: "Bottleneck Fairway Availability", - forLegendStyle: { point: true, resolution: 16 }, - visible: false, - zIndex: 1, - style: styles.bottleneckFairwayAvailability, - source: bottlenecksSource - }); - })(), - (function() { - const source = new VectorSource({ strategy: bboxStrategy }); - source.setLoader( - buildVectorLoader( - { - featureTypes: [ - "bottlenecks_geoserver", - "gauges_geoserver", - "stretches_geoserver", - "sections_geoserver" - ] - }, - source, - true, - // since we don't use bbox strategy, features will contain all features and we can use it - // to find reference gauges for bottlenecks, yeah! - async (f, store, features) => { - // attach reference gauge to bottleneck - if (f.getId().indexOf("bottlenecks") > -1) { - f.set( - "gauge_obj", - features.find(feat => { - return ( - feat.getId().indexOf("gauges") > -1 && - feat.get("objname") === f.get("gauge_objname") - ); - }) - ); - } - - // attach nsc data to gauge - if (f.getId().indexOf("gauges") > -1) { - store - .dispatch( - "gauges/getNashSutcliffeForISRS", - f.get("isrs_code") - ) - .then(response => { - f.set("nsc_data", response); - }); - } - } - ) - ); - return new VectorLayer({ - id: "DATAAVAILABILITY", - label: "Data Availability/Accuracy", - forLegendStyle: { point: true, resolution: 16 }, - visible: false, - zIndex: 1, - style: styles.dataAvailability, - source - }); - })(), - new ImageLayer({ - id: "DISTANCEMARKS", - label: "Distance Marks", - maxResolution: 10, - minResolution: 0, - source: new ImageSource({ - url: window.location.origin + "/api/internal/wms", - params: { - LAYERS: "distance_marks_ashore_geoserver", - VERSION: "1.1.1", - TILED: true - }, - imageLoadFunction: function(tile, src) { - HTTP.get(src, { - headers: { - "X-Gemma-Auth": localStorage.getItem("token") - }, - responseType: "blob" - }) - .then(response => { - tile.getImage().src = URL.createObjectURL(response.data); - }) - .catch(() => { - store.dispatch("application/reportBackendError"); - }); - } // TODO tile.setState(TileState.ERROR); - }) - }), - new ImageLayer({ - id: "DISTANCEMARKSAXIS", - label: "Distance Marks, Axis", - source: new ImageSource({ - url: window.location.origin + "/api/internal/wms", - params: { - LAYERS: "distance_marks_geoserver", - VERSION: "1.1.1", - TILED: true - }, - imageLoadFunction: function(tile, src) { - HTTP.get(src, { - headers: { - "X-Gemma-Auth": localStorage.getItem("token") - }, - responseType: "blob" - }).then(response => { - tile.getImage().src = URL.createObjectURL(response.data); - }); - } // TODO tile.setState(TileState.ERROR); - }) - }), - (function() { - const source = new VectorSource({ strategy: bboxStrategy }); - source.setLoader( - buildVectorLoader( - { - featureTypes: ["gauges_geoserver"], - geometryName: "geom" - }, - source - ) - ); - return new VectorLayer({ - id: "GAUGES", - label: "Gauges", - forLegendStyle: { point: true, resolution: 8 }, - visible: true, - style: styles.gauge, - maxResolution: 100, - minResolution: 0, - source - }); - })(), - new TileLayer({ - id: "FAIRWAYMARKS", - label: "Fairway marks", - visible: true, - source: new TileWMS({ - preload: 0, - projection: "EPSG:3857", - url: window.location.origin + "/api/internal/wms", - params: { - LAYERS: "fairway_marks", - VERSION: "1.1.1", - TILED: true - }, - tileLoadFunction: function(tile, src) { - HTTP.get(src, { - headers: { - "X-Gemma-Auth": localStorage.getItem("token") - }, - responseType: "blob" - }) - .then(response => { - tile.getImage().src = URL.createObjectURL(response.data); - }) - .catch(() => { - store.dispatch("application/reportBackendError"); - }); - } // TODO tile.setState(TileState.ERROR); - }) - }), - DRAWLAYER, - CUTLAYER, - FDREVIEWLAYER - ]; - - layerConfigs[mapId] = config; - - return { - get(id) { - return config.find(l => l.get("id") === id); - }, - config - }; -};
--- a/client/src/components/map/styles.js Fri Mar 20 17:24:03 2020 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,400 +0,0 @@ -import { Icon, Stroke, Style, Fill, Text, Circle } from "ol/style"; -import Point from "ol/geom/Point"; -import { getCenter } from "ol/extent"; -import store from "@/store/index"; -import classifications from "../../lib/classifications"; - -const styles = { - blue1: new Style({ - stroke: new Stroke({ - color: [0, 0, 255, 0.8], - lineDash: [2, 4], - lineCap: "round", - width: 2 - }), - fill: new Fill({ - color: [240, 230, 0, 0.2] - }) - }), - blue2: new Style({ - stroke: new Stroke({ - color: [0, 0, 255, 0.9], - lineDash: [3, 6], - lineCap: "round", - width: 2 - }), - fill: new Fill({ - color: [240, 230, 0, 0.1] - }) - }), - blue3: new Style({ - stroke: new Stroke({ - color: [0, 0, 255, 1.0], - width: 2 - }), - fill: new Fill({ - color: [255, 255, 255, 0.4] - }) - }), - yellow1: new Style({ - stroke: new Stroke({ - color: "rgba(230, 230, 10, .8)", - width: 4 - }), - fill: new Fill({ - color: "rgba(230, 230, 10, .3)" - }) - }), - yellow2: new Style({ - stroke: new Stroke({ - color: "rgba(250, 200, 0, .8)", - width: 2 - }), - fill: new Fill({ - color: "rgba(250, 200, 10, .3)" - }) - }), - yellow3: new Style({ - stroke: new Stroke({ - color: "rgba(250, 240, 10, .9)", - width: 5 - }), - fill: new Fill({ - color: "rgba(250, 240, 0, .7)" - }) - }), - orange1: new Style({ - stroke: new Stroke({ - color: "rgba(255, 150, 10, .8)", - width: 2 - }), - fill: new Fill({ - color: "rgba(255, 150, 0, .3)" - }) - }), - orange2: new Style({ - stroke: new Stroke({ - color: "rgba(255, 166, 10, .9)", - width: 5 - }), - fill: new Fill({ - color: "rgba(255, 166, 0, .7)" - }) - }), - red1: new Style({ - stroke: new Stroke({ - color: "rgba(255, 0, 0, 1)", - width: 4 - }) - }), - circleBlue: new Style({ - image: new Circle({ - radius: 5, - fill: new Fill({ color: "rgba(255, 0, 0, 0.1)" }), - stroke: new Stroke({ color: "blue", width: 1 }) - }) - }), - textFW1: new Style({ - text: new Text({ - font: 'bold 12px "Open Sans", "sans-serif"', - placement: "line", - fill: new Fill({ - color: "black" - }), - text: "LOS: 1" - //, zIndex: 10 - }) - }), - textFW2: new Style({ - text: new Text({ - font: 'bold 12px "Open Sans", "sans-serif"', - placement: "line", - fill: new Fill({ - color: "black" - }), - text: "LOS: 2" - //, zIndex: 10 - }) - }), - textFW3: new Style({ - text: new Text({ - font: 'bold 12px "Open Sans", "sans-serif"', - placement: "line", - fill: new Fill({ - color: "black" - }), - text: "LOS: 3" - //, zIndex: 10 - }) - }) -}; - -const styleFactory = function(mapId) { - const recencyColorCodes = { - OK: "lime", - WARNING: "yellow", - DANGER: "red", - NEUTRAL: "white" - }; - const gmAvailabilityColorCodes = { - OK: "lime", - WARNING: "yellow", - DANGER: "red", - NEUTRAL: "white" - }; - const forecastAccuracyColorCodes = { - OK: "lime", - WARNING: "yellow", - DANGER: "red", - NEUTRAL: "white" - }; - - const forecastVsRealityColorCodes = { - OK: "lime", - WARNING: "yellow", - DANGER: "red", - NEUTRAL: "white" - }; - return { - recencyColorCodes: recencyColorCodes, - gmAvailabilityColorCodes: gmAvailabilityColorCodes, - forecastAccuracyColorCodes: forecastAccuracyColorCodes, - forecastVsRealityColorCodes: forecastVsRealityColorCodes, - stretches(feature) { - let style = styles.yellow2; - if (feature.get("highlighted")) { - style = styles.yellow3; - } - return style; - }, - sections(feature) { - let style = styles.orange1; - if (feature.get("highlighted")) { - style = styles.orange2; - } - return style; - }, - fwd1() { - return [styles.blue1, styles.textFW1]; - }, - fwd2() { - return [styles.blue2, styles.textFW2]; - }, - fwd3() { - return [styles.blue3, styles.textFW3]; - }, - bottleneck() { - return styles.yellow1; - }, - bottleneckStatus(feature, resolution, isLegend) { - let s = []; - if ((feature.get("fa_critical") && resolution > 15) || isLegend) { - let bnCenter = getCenter(feature.getGeometry().getExtent()); - s.push( - new Style({ - geometry: new Point(bnCenter), - image: new Icon({ - src: require("@/assets/marker-bottleneck-critical.png"), - anchor: [0.5, 0.5], - scale: isLegend ? 0.5 : 1 - }) - }) - ); - } - if (feature.get("fa_critical") && !isLegend) { - s.push(styles.red1); - } - return s; - }, - bottleneckFairwayAvailability(feature, resolution, isLegend) { - let s = []; - if (isLegend) { - s.push( - new Style({ - image: new Icon({ - src: require("@/assets/fa-diagram.png"), - anchor: [0.5, 0.5], - scale: 1 - }) - }) - ); - } - if (feature.get("fa_critical") && feature.get("fa_data")) { - let data = feature.get("fa_data"); - const heightInPixel = 80; - const relativeHeightInPercent = heightInPixel / 100; - let lnwlHeight = relativeHeightInPercent * data.ldc; - let belowThresholdHeight = relativeHeightInPercent * data.below; - let betweenThresholdHeight = relativeHeightInPercent * data.between; - let aboveThresholdHeight = relativeHeightInPercent * data.above; - let lnwl = `<rect x='2' y='${2 + - heightInPixel - - lnwlHeight}' width='10' height='${lnwlHeight}' stroke-width='0' fill='aqua'/>`; - let above = `<rect x='12' y='${2 + - heightInPixel - - aboveThresholdHeight}' width='18' height='${aboveThresholdHeight}' stroke-width='0' fill='blue'/>`; - let between = `<rect x='12' y='${2 + - heightInPixel - - aboveThresholdHeight - - betweenThresholdHeight}' width='18' height='${betweenThresholdHeight}' stroke-width='0' fill='darksalmon'/>`; - let below = `<rect x='12' y='${2 + - heightInPixel - - aboveThresholdHeight - - betweenThresholdHeight - - belowThresholdHeight}' width='18' height='${belowThresholdHeight}' stroke-width='0' fill='hotpink'/>`; - let frame = `<rect x='0' y='0' width='32' height='84' stroke-width='0' fill='white'/>`; - let svg = `data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' width='32' height='84'><g>${frame}${lnwl}${above}${between}${below}</g></svg>`; - let bnCenter = getCenter(feature.getGeometry().getExtent()); - s.push( - new Style({ - geometry: new Point(bnCenter), - image: new Icon({ - src: svg, - anchor: [1.2, 1.2] - }) - }) - ); - } - return s; - }, - dataAvailability(feature, resolution, isLegend) { - let s = []; - if (isLegend) { - s.push( - new Style({ - image: new Icon({ - src: require("@/assets/da-diagram.png"), - anchor: [0.5, 0.5], - scale: 1 - }) - }) - ); - } else { - // TODO: Get information from feature and check the ranges according to #423, #424, #425 - let colorWaterlevel = - gmAvailabilityColorCodes[classifications.gmAvailability(feature)]; - let colorComparison = - forecastVsRealityColorCodes[ - classifications.forecastVsReality(feature) - ]; - let colorAccuracy = - forecastAccuracyColorCodes[classifications.forecastAccuracy(feature)]; - let map = store.getters["map/openLayersMap"](mapId); - let geom = feature.getGeometry(); - if (!(geom instanceof Point)) { - geom = new Point(getCenter(feature.getGeometry().getExtent())); - } - if ( - (map.getLayer("BOTTLENECKS").getVisible() && - feature.getId().indexOf("bottlenecks") > -1) || - (map.getLayer("SECTIONS").getVisible() && - feature.getId().indexOf("sections") > -1) || - (map.getLayer("STRETCHES").getVisible() && - feature.getId().indexOf("stretches") > -1) || - (map.getLayer("GAUGES").getVisible() && - feature.getId().indexOf("gauges") > -1) - ) { - let frame = `<polyline points='16,0 32,28 0,28 16,0' stroke='grey' stroke-width='1' fill='white'/>`; - let waterlevel = `<polyline points="16,0 24,14 16,28 8,14 16,0" stroke='grey' stroke-width='1' fill='${colorWaterlevel}'/>`; - let accuracy = `<polyline points="24,14 32,28 16,28 24,14" stroke='grey' stroke-width='1' fill='${colorAccuracy}'/>`; - let comparison = `<polyline points="8,14 16,28 0,28 8,14" stroke='grey' stroke-width='1' fill='${colorComparison}'/>`; - let svg = `data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' width='32' height='28'><g>${frame}${waterlevel}${comparison}${accuracy}</g></svg>`; - s.push( - new Style({ - geometry: geom, - image: new Icon({ - src: svg, - anchor: [-0.5, 1] - }) - }) - ); - } - - if ( - map.getLayer("BOTTLENECKS").getVisible() && - feature.getId().indexOf("bottlenecks") > -1 - ) { - let colorUniformTriangle = - recencyColorCodes[classifications.surveyRecency(feature)]; - let frame = `<polyline points='16,0 32,28 0,28 16,0' stroke='grey' stroke-width='1' fill='${colorUniformTriangle}'/>`; - let svg = `data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' width='32' height='28'><g>${frame}</g></svg>`; - s.push( - new Style({ - geometry: geom, - image: new Icon({ - src: svg, - anchor: [0.5, 1] - }) - }) - ); - } - } - return s; - }, - dma(feature, resolution) { - if (resolution < 10) { - var s = styles.circleBlue; - if (resolution < 6) { - s.setText( - new Text({ - offsetY: 12, - font: '10px "Open Sans", "sans-serif"', - fill: new Fill({ - color: "black" - }), - text: (feature.get("hectometre") / 10).toString() - }) - ); - } - return s; - } - return []; - }, - gauge(feature, resolution, isLegend) { - let waterlevel = feature.get("gm_waterlevel"); - let text = feature.get("objname"); - let iconColor = "white"; - if (waterlevel) { - text += "\n(" + waterlevel + " cm)"; - let refWaterlevels = JSON.parse(feature.get("reference_water_levels")); - if (refWaterlevels) { - const HDC = - refWaterlevels[ - Object.keys(refWaterlevels).find(e => /HDC/.test(e)) - ]; - const LDC = - refWaterlevels[ - Object.keys(refWaterlevels).find(e => /LDC/.test(e)) - ]; - if (waterlevel < LDC) iconColor = "brown"; - if (waterlevel > LDC && waterlevel < HDC) iconColor = "blue"; - if (waterlevel > HDC) iconColor = "red"; - } - } - - return [ - new Style({ - image: new Icon({ - src: require("@/assets/marker-gauge-" + iconColor + ".png"), - anchor: [0.5, isLegend ? 0.5 : 1], - scale: isLegend ? 0.5 : 1 - }), - text: new Text({ - font: '10px "Open Sans", "sans-serif"', - offsetY: 15, - fill: new Fill({ - color: "black" - }), - backgroundFill: new Fill({ - color: "rgba(255, 255, 255, 0.7)" - }), - padding: [2, 2, 2, 2], - text - }) - }) - ]; - } - }; -}; - -export { styles, styleFactory };
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/client/src/components/toolbar/TimeSlider.vue Tue Mar 24 11:23:26 2020 +0100 @@ -0,0 +1,92 @@ +<template> + <div @click="showSlider" 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", + "currentVisibleTime", + "storedTime" + ]), + label() { + const date = this.currentVisibleTime; + return `<b>${this.currentVisibleTime.toLocaleDateString(locale2, { + day: "2-digit", + month: "2-digit", + year: "numeric" + })} ${format(date, "HH:mm")}</b>`; + }, + currentTimeSelection() { + const date = this.currentVisibleTime; + const result = date.toLocaleDateString(locale2, { + day: "2-digit", + month: "2-digit" + }); + return `${format(date, "HH:mm")}\n${result}\n${date.getFullYear()}`; + } + }, + methods: { + showSlider() { + if (this.showTimeSlider) { + this.$store.commit( + "application/setStoredTime", + this.currentVisibleTime + ); + this.$store.commit("application/setSelectedTime", new Date()); + this.$store.commit("application/showTimeSlider", false); + } else { + this.$store.commit("application/setSelectedTime", this.storedTime); + this.$store.commit("application/showTimeSlider", true); + } + } + } +}; +</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 20 17:24:03 2020 +0100 +++ b/client/src/components/toolbar/Toolbar.vue Tue Mar 24 11:23:26 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/lib/session.js Fri Mar 20 17:24:03 2020 +0100 +++ b/client/src/lib/session.js Tue Mar 24 11:23:26 2020 +0100 @@ -12,10 +12,10 @@ * Thomas Junk <thomas.junk@intevation.de> */ +import { HTTP } from "@/lib/http"; import app from "@/main"; -import { unsetLayerConfigs } from "@/components/map/layers"; -import { HTTP } from "@/lib/http"; import { displayError } from "@/lib/errors"; +import { unsetLayerConfigs } from "@/components/layers/layers"; const logOff = () => { const hasToken = localStorage.getItem("token");
--- a/client/src/store/application.js Fri Mar 20 17:24:03 2020 +0100 +++ b/client/src/store/application.js Tue Mar 24 11:23:26 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,15 @@ 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(), + currentVisibleTime: new Date(), + refreshLayersTime: new Date(), + storedTime: new Date(), version, tempRoute: "", config: {} @@ -76,6 +82,15 @@ } }, mutations: { + setCurrentVisibleTime: (state, currentVisibleTime) => { + state.currentVisibleTime = currentVisibleTime; + }, + setLayerRefreshedTime: (state, refreshLayersTime) => { + state.refreshLayersTime = refreshLayersTime; + }, + setStoredTime: (state, storedTime) => { + state.storedTime = storedTime; + }, setTempRoute: (state, tempRoute) => { state.tempRoute = tempRoute; }, @@ -93,6 +108,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; },
--- a/client/src/store/map.js Fri Mar 20 17:24:03 2020 +0100 +++ b/client/src/store/map.js Tue Mar 24 11:23:26 2020 +0100 @@ -53,6 +53,7 @@ isolinesLegendImgDataURL: "", differencesLegendImgDataURL: "", ongoingRefresh: false, + ongoingTimeSlide: false, reviewActive: false }; }; @@ -102,6 +103,12 @@ finishRefreshLayers: state => { state.ongoingRefresh = false; }, + startTimeSlide: state => { + state.ongoingTimeSlide = true; + }, + finishTimeSlide: state => { + state.ongoingTimeSlide = false; + }, initialLoad: (state, initialLoad) => { state.initialLoad = initialLoad; }, @@ -579,7 +586,10 @@ event.coordinate, currentResolution, "EPSG:3857", - { INFO_FORMAT: "application/json" } + { + INFO_FORMAT: "application/json", + TIME: rootState.application.currentVisibleTime.toISOString() + } ); if (fmSource) { HTTP.get(fmURL, { @@ -612,6 +622,31 @@ } }); }, + refreshTimebasedLayers({ state, rootState }) { + const layers = [ + "BOTTLENECKS", + "GAUGES", + "FAIRWAYDIMENSIONSLOS1", + "FAIRWAYDIMENSIONSLOS2", + "FAIRWAYDIMENSIONSLOS3", + "WATERWAYAXIS", + "FAIRWAYMARKS" + ]; + state.openLayersMaps.forEach(map => { + for (let i = 0; i < layers.length; i++) { + let layer = map.getLayer(layers[i]); + if (layer instanceof VectorLayer) { + layer.getSource().clear(true); + } else { + // Refresh layers with updated TIME value + layer.getSource().updateParams({ + TIME: rootState.application.refreshLayersTime.toISOString() + }); + } + layer.getSource().refresh(); + } + }); + }, moveToBoundingBox( { state }, { boundingBox, zoom, preventZoomOut, duration }