# HG changeset patch # User Sascha L. Teichmann # Date 1585045406 -3600 # Node ID 1154b73328ec7b187b2b081a1c7be429817ca3f4 # Parent 56c589f7435d1fbdc4b682c0b8a261b24e1fb0b8# Parent 70bd5c82463940b32670ea49a00224071794b655 Merged time-sliding branch into default. diff -r 56c589f7435d -r 1154b73328ec client/src/components/App.vue --- 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 @@ + @@ -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"), diff -r 56c589f7435d -r 1154b73328ec client/src/components/TimeSlider.vue --- /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 @@ + + + diff -r 56c589f7435d -r 1154b73328ec client/src/components/identify/Identify.vue --- 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 { diff -r 56c589f7435d -r 1154b73328ec client/src/components/importoverview/FairwayDimensionDetail.vue --- 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"; diff -r 56c589f7435d -r 1154b73328ec client/src/components/layers/Layers.vue --- 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"); diff -r 56c589f7435d -r 1154b73328ec client/src/components/layers/layers.js --- /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 + }; +}; diff -r 56c589f7435d -r 1154b73328ec client/src/components/layers/styles.js --- /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 = ``; + let above = ``; + let between = ``; + let below = ``; + let frame = ``; + let svg = `data:image/svg+xml,${frame}${lnwl}${above}${between}${below}`; + 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 = ``; + let waterlevel = ``; + let accuracy = ``; + let comparison = ``; + let svg = `data:image/svg+xml,${frame}${waterlevel}${comparison}${accuracy}`; + 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 = ``; + let svg = `data:image/svg+xml,${frame}`; + 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 }; diff -r 56c589f7435d -r 1154b73328ec client/src/components/map/Map.vue --- 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 */ diff -r 56c589f7435d -r 1154b73328ec client/src/components/map/Zoom.vue --- 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 @@