Mercurial > gemma
changeset 3011:fc8fbea24568
client: moved map component, layer factory and styles to own subdirectory
author | Markus Kottlaender <markus@intevation.de> |
---|---|
date | Thu, 11 Apr 2019 12:14:01 +0200 |
parents | 293bdd05ffcd |
children | 802fcb50c484 |
files | client/src/components/Main.vue client/src/components/Maplayer.vue client/src/components/layers/layers.js client/src/components/layers/styles.js client/src/components/map/Map.vue client/src/components/map/layers.js client/src/components/map/styles.js client/src/store/application.js |
diffstat | 8 files changed, 922 insertions(+), 922 deletions(-) [+] |
line wrap: on
line diff
--- a/client/src/components/Main.vue Thu Apr 11 12:13:27 2019 +0200 +++ b/client/src/components/Main.vue Thu Apr 11 12:14:01 2019 +0200 @@ -121,7 +121,7 @@ export default { components: { // all components that are supposed to be displayed in a pane must be registered here - Maplayer: () => import("./Maplayer") + Map: () => import("./map/Map") }, computed: { ...mapState("application", ["panes", "paneMode"])
--- a/client/src/components/Maplayer.vue Thu Apr 11 12:13:27 2019 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,225 +0,0 @@ -<template> - <div - :id="'map-' + uuid" - :class="[ - 'map', - { - splitscreen: this.splitscreen, - nocursor: this.hasActiveInteractions - } - ]" - ></div> -</template> - -<style lang="sass" scoped> -.map - width: 100% - height: 100% - - &.splitscreen - height: 50% - - &.nocursor - cursor: 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) 2018, 2019 by via donau - * – Österreichische Wasserstraßen-Gesellschaft mbH - * Software engineering by Intevation GmbH - * - * Author(s): - * * Thomas Junk <thomas.junk@intevation.de> - * * Bernhard E. Reiter <bernhard.reiter@intevation.de> - */ -import { HTTP } from "@/lib/http"; -import { mapState } from "vuex"; -import { Map, View } from "ol"; -import { Stroke, Style, Fill } from "ol/style"; -import { displayError } from "@/lib/errors"; -import { uuid } from "@/lib/mixins"; -import layers from "@/components/layers/layers"; -import "ol/ol.css"; - -/* for the sake of debugging */ -/* eslint-disable no-console */ -export default { - mixins: [uuid], - data() { - return { - splitscreen: false - }; - }, - computed: { - ...mapState("map", [ - "initialLoad", - "extent", - "openLayersMap", - "lineTool", - "polygonTool", - "cutTool" - ]), - ...mapState("bottlenecks", ["selectedSurvey"]), - ...mapState("application", ["showSplitscreen"]), - ...mapState("imports", ["selectedStretchId"]), - hasActiveInteractions() { - return ( - (this.lineTool && this.lineTool.getActive()) || - (this.polygonTool && this.polygonTool.getActive()) || - (this.cutTool && this.cutTool.getActive()) - ); - } - }, - methods: { - updateBottleneckFilter(bottleneck_id, datestr) { - const exists = bottleneck_id != "does_not_exist"; - - if (exists) { - layers - .get("BOTTLENECKISOLINE") - .getSource() - .updateParams({ - cql_filter: `date_info='${datestr}' AND bottleneck_id='${bottleneck_id}'` - }); - } - layers.get("BOTTLENECKISOLINE").setVisible(exists); - } - }, - watch: { - showSplitscreen(show) { - if (show) { - setTimeout(() => { - this.splitscreen = true; - }, 350); - } else { - this.splitscreen = false; - } - }, - splitscreen() { - const map = this.openLayersMap; - this.$nextTick(() => { - map && map.updateSize(); - }); - }, - selectedSurvey(newSelectedSurvey) { - if (newSelectedSurvey) { - this.updateBottleneckFilter( - newSelectedSurvey.bottleneck_id, - newSelectedSurvey.date_info - ); - } else { - this.updateBottleneckFilter("does_not_exist", "1999-10-01"); - } - }, - selectedStretchId(id) { - layers - .get("STRETCHES") - .getSource() - .getFeatures() - .forEach(f => { - f.set("highlighted", false); - if (id === f.getId()) { - f.set("highlighted", true); - } - }); - } - }, - mounted() { - const map = new Map({ - layers: layers.config, - target: "map-" + this.uuid, - controls: [], - view: new View({ - center: [this.extent.lon, this.extent.lat], - minZoom: 5, // restrict zooming out to ~size of Europe for width 1000px - zoom: this.extent.zoom, - projection: "EPSG:3857" - }) - }); - map.getLayer = id => layers.get(id); - - // store map position on every move - // will be obsolete once we abandoned the separated admin context - map.on("moveend", event => { - const center = event.map.getView().getCenter(); - this.$store.commit("map/extent", { - lat: center[1], - lon: center[0], - zoom: event.map.getView().getZoom() - }); - }); - this.$store.dispatch("map/openLayersMap", map); - - // move to user specific default extent if map loads for the first timeout - // checking initialLoad will be obsolete once we abandoned the separated admin context - if (this.initialLoad) { - this.$store.commit("map/initialLoad", false); - var currentUser = this.$store.state.user.user; - HTTP.get("/users/" + currentUser, { - headers: { - "X-Gemma-Auth": localStorage.getItem("token"), - "Content-type": "text/xml; charset=UTF-8" - } - }) - .then(response => { - this.$store.commit("map/moveToBoundingBox", { - boundingBox: [ - response.data.extent.x1, - response.data.extent.y1, - response.data.extent.x2, - response.data.extent.y2 - ], - zoom: 17, - preventZoomOut: true - }); - }) - .catch(error => { - const { status, data } = error.response; - displayError({ - title: this.$gettext("Backend Error"), - message: `${status}: ${data.message || data}` - }); - }); - } - - // load configured bottleneck colors - HTTP.get("/system/style/Bottlenecks/stroke", { - headers: { "X-Gemma-Auth": localStorage.getItem("token") } - }) - .then(response => { - let btlnStrokeC = response.data.code; - HTTP.get("/system/style/Bottlenecks/fill", { - headers: { "X-Gemma-Auth": localStorage.getItem("token") } - }) - .then(response => { - let btlnFillC = response.data.code; - var newStyle = new Style({ - stroke: new Stroke({ - color: btlnStrokeC, - width: 4 - }), - fill: new Fill({ - color: btlnFillC - }) - }); - layers.get("BOTTLENECKS").setStyle(newStyle); - }) - .catch(error => { - console.log(error); - }); - }) - .catch(error => { - console.log(error); - }); - - this.$store.dispatch("map/disableIdentifyTool"); - this.$store.dispatch("map/enableIdentifyTool"); - } -}; -</script>
--- a/client/src/components/layers/layers.js Thu Apr 11 12:13:27 2019 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,494 +0,0 @@ -import TileWMS from "ol/source/TileWMS"; -import { Tile as TileLayer, Vector as VectorLayer } from "ol/layer"; -import OSM from "ol/source/OSM"; -import { Icon, Stroke, Style } from "ol/style"; -import VectorSource from "ol/source/Vector"; -import Point from "ol/geom/Point"; -import { bbox as bboxStrategy } from "ol/loadingstrategy"; -import { WFS, GeoJSON } from "ol/format"; -import { equalTo } from "ol/format/filter"; -import { HTTP } from "@/lib/http"; -import styles from "./styles"; - -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)); - } - vectorSource.addFeatures(features); - }) - .catch(() => { - vectorSource.removeLoadedExtent(extent); - }); - }; -}; - -export default (function() { - return { - get(id) { - return this.config.find(l => l.get("id") === id); - }, - config: [ - new TileLayer({ - id: "OPENSTREETMAP", - label: "Open Streetmap", - visible: true, - source: new OSM() - }), - new TileLayer({ - id: "INLANDECDIS", - label: "Inland ECDIS chart Danube", - visible: true, - source: new TileWMS({ - preload: 1, - url: "https://service.d4d-portal.info/wms/", - crossOrigin: "anonymous", - params: { LAYERS: "d4d", VERSION: "1.1.1", TILED: true } - }) - }), - (function() { - const source = new VectorSource({ strategy: bboxStrategy }); - source.setLoader( - buildVectorLoader( - { - featureTypes: ["waterway_area"], - geometryName: "area" - }, - source - ) - ); - return new VectorLayer({ - id: "WATERWAYAREA", - label: "Waterway Area", - visible: true, - style: new Style({ - stroke: new Stroke({ - color: "rgba(0, 102, 0, 1)", - width: 2 - }) - }), - source - }); - })(), - (function() { - const source = new VectorSource({ strategy: bboxStrategy }); - source.setLoader( - buildVectorLoader( - { - featureTypes: ["stretches_geoserver"], - geometryName: "area" - }, - source, - f => { - if (f.getId() === this.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(); - source.setLoader( - buildVectorLoader( - { - featureTypes: ["fairway_dimensions"], - filter: equalTo("level_of_service", 1) - }, - source, - true - ) - ); - return new VectorLayer({ - id: "FAIRWAYDIMENSIONSLOS1", - label: "LOS 1 Fairway Dimensions", - visible: false, - style: styles.fwd1, - source - }); - })(), - (function() { - const source = new VectorSource(); - source.setLoader( - buildVectorLoader( - { - featureTypes: ["fairway_dimensions"], - filter: equalTo("level_of_service", 2) - }, - source, - true - ) - ); - return new VectorLayer({ - id: "FAIRWAYDIMENSIONSLOS2", - label: "LOS 2 Fairway Dimensions", - visible: false, - style: styles.fwd2, - source - }); - })(), - (function() { - const source = new VectorSource(); - source.setLoader( - buildVectorLoader( - { - featureTypes: ["fairway_dimensions"], - filter: equalTo("level_of_service", 3) - }, - source, - true - ) - ); - return new VectorLayer({ - id: "FAIRWAYDIMENSIONSLOS3", - label: "LOS 3 Fairway Dimensions", - visible: true, - style: styles.fwd3, - source - }); - })(), - (function() { - const source = new VectorSource({ strategy: bboxStrategy }); - source.setLoader( - buildVectorLoader( - { - featureTypes: ["waterway_axis"], - geometryName: "wtwaxs" - }, - source - ) - ); - return new VectorLayer({ - id: "WATERWAYAXIS", - label: "Waterway Axis", - visible: true, - style: new Style({ - stroke: new Stroke({ - color: "rgba(0, 0, 255, .5)", - lineDash: [5, 5], - width: 2 - }) - }), - // TODO: Set layer in layertree active/inactive depending on - // resolution. - maxResolution: 5, - minResolution: 0, - source - }); - })(), - (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() { - const source = new VectorSource({ strategy: bboxStrategy }); - source.setLoader( - buildVectorLoader( - { - featureTypes: ["bottlenecks_geoserver"], - geometryName: "area" - }, - source - ) - ); - return new VectorLayer({ - id: "BOTTLENECKS", - label: "Bottlenecks", - visible: true, - style: styles.bottleneck, - source - }); - })(), - new TileLayer({ - id: "BOTTLENECKISOLINE", - label: "Bottleneck isolines", - visible: false, - source: new TileWMS({ - preload: 0, - projection: "EPSG:3857", - url: window.location.origin + "/api/internal/wms", - params: { - LAYERS: "sounding_results_contour_lines_geoserver", - VERSION: "1.1.1", - TILED: true - }, - tileLoadFunction: function(tile, src) { - // console.log("calling for", 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); - }) - }), - new TileLayer({ - id: "DIFFERENCES", - label: "Bottleneck Differences", - visible: false, - source: new TileWMS({ - preload: 0, - projection: "EPSG:4326", - url: window.location.origin + "/api/internal/wms", - params: { - LAYERS: "sounding_differences", - VERSION: "1.1.1", - TILED: true - }, - tileLoadFunction: function(tile, src) { - // console.log("calling for", 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: ["bottlenecks_geoserver"], - geometryName: "area" - }, - source - ) - ); - return new VectorLayer({ - id: "BOTTLENECKSTATUS", - label: "Critical Bottlenecks", - forLegendStyle: { point: true, resolution: 16 }, - visible: true, - style: styles.bottleneckStatus, - source - }); - })(), - (function() { - const source = new VectorSource({ strategy: bboxStrategy }); - source.setLoader( - buildVectorLoader( - { - featureTypes: ["distance_marks_ashore_geoserver"], - geometryName: "geom" - }, - source - ) - ); - return new VectorLayer({ - id: "DISTANCEMARKS", - label: "Distance marks", - forLegendStyle: { point: true, resolution: 8 }, - visible: false, - source - }); - })(), - (function() { - const source = new VectorSource({ strategy: bboxStrategy }); - source.setLoader( - buildVectorLoader( - { - featureTypes: ["distance_marks_geoserver"], - geometryName: "geom" - }, - source - ) - ); - return new VectorLayer({ - id: "DISTANCEMARKSAXIS", - label: "Distance marks, Axis", - forLegendStyle: { point: true, resolution: 8 }, - visible: true, - style: styles.dma, - source - }); - })(), - (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 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; - } - }), - 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: "#333333", - width: 2, - 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; - } - }) - ] - }; -})();
--- a/client/src/components/layers/styles.js Thu Apr 11 12:13:27 2019 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,201 +0,0 @@ -import { Icon, Stroke, Style, Fill, Text, Circle } from "ol/style"; -import Point from "ol/geom/Point"; -import { getCenter } from "ol/extent"; - -const styles = { - blue1: new Style({ - stroke: new Stroke({ - color: "rgba(0, 0, 255, 0.8)", - lineDash: [2, 4], - lineCap: "round", - width: 2 - }), - fill: new Fill({ - color: "rgba(240, 230, 0, 0.2)" - }) - }), - blue2: new Style({ - stroke: new Stroke({ - color: "rgba(0, 0, 255, 0.9)", - lineDash: [3, 6], - lineCap: "round", - width: 2 - }), - fill: new Fill({ - color: "rgba(240, 230, 0, 0.1)" - }) - }), - blue3: new Style({ - stroke: new Stroke({ - color: "rgba(0, 0, 255, 1.0)", - width: 2 - }), - fill: new Fill({ - color: "rgba(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)" - }) - }), - 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 - }) - }) -}; - -export default { - stretches(feature) { - let style = styles.yellow2; - if (feature.get("highlighted")) { - style = styles.yellow3; - } - 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; - }, - 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) { - return [ - new Style({ - image: new Icon({ - src: require("@/assets/marker-gauge.png"), - anchor: [0.5, isLegend ? 0.5 : 1], - scale: isLegend ? 0.5 : 1 - }), - text: new Text({ - font: '10px "Open Sans", "sans-serif"', - offsetY: 8, - fill: new Fill({ - color: "white" - }), - text: feature.get("objname") - }) - }), - new Style({ - text: new Text({ - font: '10px "Open Sans", "sans-serif"', - offsetY: 7, - offsetX: -1, - fill: new Fill({ - color: "black" - }), - text: feature.get("objname") - }) - }) - ]; - } -};
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/client/src/components/map/Map.vue Thu Apr 11 12:14:01 2019 +0200 @@ -0,0 +1,225 @@ +<template> + <div + :id="'map-' + uuid" + :class="[ + 'map', + { + splitscreen: this.splitscreen, + nocursor: this.hasActiveInteractions + } + ]" + ></div> +</template> + +<style lang="sass" scoped> +.map + width: 100% + height: 100% + + &.splitscreen + height: 50% + + &.nocursor + cursor: 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) 2018, 2019 by via donau + * – Österreichische Wasserstraßen-Gesellschaft mbH + * Software engineering by Intevation GmbH + * + * Author(s): + * * Thomas Junk <thomas.junk@intevation.de> + * * Bernhard E. Reiter <bernhard.reiter@intevation.de> + */ +import { HTTP } from "@/lib/http"; +import { mapState } from "vuex"; +import { Map, View } from "ol"; +import { Stroke, Style, Fill } from "ol/style"; +import { displayError } from "@/lib/errors"; +import { uuid } from "@/lib/mixins"; +import layers from "@/components/map/layers"; +import "ol/ol.css"; + +/* for the sake of debugging */ +/* eslint-disable no-console */ +export default { + mixins: [uuid], + data() { + return { + splitscreen: false + }; + }, + computed: { + ...mapState("map", [ + "initialLoad", + "extent", + "openLayersMap", + "lineTool", + "polygonTool", + "cutTool" + ]), + ...mapState("bottlenecks", ["selectedSurvey"]), + ...mapState("application", ["showSplitscreen"]), + ...mapState("imports", ["selectedStretchId"]), + hasActiveInteractions() { + return ( + (this.lineTool && this.lineTool.getActive()) || + (this.polygonTool && this.polygonTool.getActive()) || + (this.cutTool && this.cutTool.getActive()) + ); + } + }, + methods: { + updateBottleneckFilter(bottleneck_id, datestr) { + const exists = bottleneck_id != "does_not_exist"; + + if (exists) { + layers + .get("BOTTLENECKISOLINE") + .getSource() + .updateParams({ + cql_filter: `date_info='${datestr}' AND bottleneck_id='${bottleneck_id}'` + }); + } + layers.get("BOTTLENECKISOLINE").setVisible(exists); + } + }, + watch: { + showSplitscreen(show) { + if (show) { + setTimeout(() => { + this.splitscreen = true; + }, 350); + } else { + this.splitscreen = false; + } + }, + splitscreen() { + const map = this.openLayersMap; + this.$nextTick(() => { + map && map.updateSize(); + }); + }, + selectedSurvey(newSelectedSurvey) { + if (newSelectedSurvey) { + this.updateBottleneckFilter( + newSelectedSurvey.bottleneck_id, + newSelectedSurvey.date_info + ); + } else { + this.updateBottleneckFilter("does_not_exist", "1999-10-01"); + } + }, + selectedStretchId(id) { + layers + .get("STRETCHES") + .getSource() + .getFeatures() + .forEach(f => { + f.set("highlighted", false); + if (id === f.getId()) { + f.set("highlighted", true); + } + }); + } + }, + mounted() { + const map = new Map({ + layers: layers.config, + target: "map-" + this.uuid, + controls: [], + view: new View({ + center: [this.extent.lon, this.extent.lat], + minZoom: 5, // restrict zooming out to ~size of Europe for width 1000px + zoom: this.extent.zoom, + projection: "EPSG:3857" + }) + }); + map.getLayer = id => layers.get(id); + + // store map position on every move + // will be obsolete once we abandoned the separated admin context + map.on("moveend", event => { + const center = event.map.getView().getCenter(); + this.$store.commit("map/extent", { + lat: center[1], + lon: center[0], + zoom: event.map.getView().getZoom() + }); + }); + this.$store.dispatch("map/openLayersMap", map); + + // move to user specific default extent if map loads for the first timeout + // checking initialLoad will be obsolete once we abandoned the separated admin context + if (this.initialLoad) { + this.$store.commit("map/initialLoad", false); + var currentUser = this.$store.state.user.user; + HTTP.get("/users/" + currentUser, { + headers: { + "X-Gemma-Auth": localStorage.getItem("token"), + "Content-type": "text/xml; charset=UTF-8" + } + }) + .then(response => { + this.$store.commit("map/moveToBoundingBox", { + boundingBox: [ + response.data.extent.x1, + response.data.extent.y1, + response.data.extent.x2, + response.data.extent.y2 + ], + zoom: 17, + preventZoomOut: true + }); + }) + .catch(error => { + const { status, data } = error.response; + displayError({ + title: this.$gettext("Backend Error"), + message: `${status}: ${data.message || data}` + }); + }); + } + + // load configured bottleneck colors + HTTP.get("/system/style/Bottlenecks/stroke", { + headers: { "X-Gemma-Auth": localStorage.getItem("token") } + }) + .then(response => { + let btlnStrokeC = response.data.code; + HTTP.get("/system/style/Bottlenecks/fill", { + headers: { "X-Gemma-Auth": localStorage.getItem("token") } + }) + .then(response => { + let btlnFillC = response.data.code; + var newStyle = new Style({ + stroke: new Stroke({ + color: btlnStrokeC, + width: 4 + }), + fill: new Fill({ + color: btlnFillC + }) + }); + layers.get("BOTTLENECKS").setStyle(newStyle); + }) + .catch(error => { + console.log(error); + }); + }) + .catch(error => { + console.log(error); + }); + + this.$store.dispatch("map/disableIdentifyTool"); + this.$store.dispatch("map/enableIdentifyTool"); + } +}; +</script>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/client/src/components/map/layers.js Thu Apr 11 12:14:01 2019 +0200 @@ -0,0 +1,494 @@ +import TileWMS from "ol/source/TileWMS"; +import { Tile as TileLayer, Vector as VectorLayer } from "ol/layer"; +import OSM from "ol/source/OSM"; +import { Icon, Stroke, Style } from "ol/style"; +import VectorSource from "ol/source/Vector"; +import Point from "ol/geom/Point"; +import { bbox as bboxStrategy } from "ol/loadingstrategy"; +import { WFS, GeoJSON } from "ol/format"; +import { equalTo } from "ol/format/filter"; +import { HTTP } from "@/lib/http"; +import styles from "./styles"; + +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)); + } + vectorSource.addFeatures(features); + }) + .catch(() => { + vectorSource.removeLoadedExtent(extent); + }); + }; +}; + +export default (function() { + return { + get(id) { + return this.config.find(l => l.get("id") === id); + }, + config: [ + new TileLayer({ + id: "OPENSTREETMAP", + label: "Open Streetmap", + visible: true, + source: new OSM() + }), + new TileLayer({ + id: "INLANDECDIS", + label: "Inland ECDIS chart Danube", + visible: true, + source: new TileWMS({ + preload: 1, + url: "https://service.d4d-portal.info/wms/", + crossOrigin: "anonymous", + params: { LAYERS: "d4d", VERSION: "1.1.1", TILED: true } + }) + }), + (function() { + const source = new VectorSource({ strategy: bboxStrategy }); + source.setLoader( + buildVectorLoader( + { + featureTypes: ["waterway_area"], + geometryName: "area" + }, + source + ) + ); + return new VectorLayer({ + id: "WATERWAYAREA", + label: "Waterway Area", + visible: true, + style: new Style({ + stroke: new Stroke({ + color: "rgba(0, 102, 0, 1)", + width: 2 + }) + }), + source + }); + })(), + (function() { + const source = new VectorSource({ strategy: bboxStrategy }); + source.setLoader( + buildVectorLoader( + { + featureTypes: ["stretches_geoserver"], + geometryName: "area" + }, + source, + f => { + if (f.getId() === this.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(); + source.setLoader( + buildVectorLoader( + { + featureTypes: ["fairway_dimensions"], + filter: equalTo("level_of_service", 1) + }, + source, + true + ) + ); + return new VectorLayer({ + id: "FAIRWAYDIMENSIONSLOS1", + label: "LOS 1 Fairway Dimensions", + visible: false, + style: styles.fwd1, + source + }); + })(), + (function() { + const source = new VectorSource(); + source.setLoader( + buildVectorLoader( + { + featureTypes: ["fairway_dimensions"], + filter: equalTo("level_of_service", 2) + }, + source, + true + ) + ); + return new VectorLayer({ + id: "FAIRWAYDIMENSIONSLOS2", + label: "LOS 2 Fairway Dimensions", + visible: false, + style: styles.fwd2, + source + }); + })(), + (function() { + const source = new VectorSource(); + source.setLoader( + buildVectorLoader( + { + featureTypes: ["fairway_dimensions"], + filter: equalTo("level_of_service", 3) + }, + source, + true + ) + ); + return new VectorLayer({ + id: "FAIRWAYDIMENSIONSLOS3", + label: "LOS 3 Fairway Dimensions", + visible: true, + style: styles.fwd3, + source + }); + })(), + (function() { + const source = new VectorSource({ strategy: bboxStrategy }); + source.setLoader( + buildVectorLoader( + { + featureTypes: ["waterway_axis"], + geometryName: "wtwaxs" + }, + source + ) + ); + return new VectorLayer({ + id: "WATERWAYAXIS", + label: "Waterway Axis", + visible: true, + style: new Style({ + stroke: new Stroke({ + color: "rgba(0, 0, 255, .5)", + lineDash: [5, 5], + width: 2 + }) + }), + // TODO: Set layer in layertree active/inactive depending on + // resolution. + maxResolution: 5, + minResolution: 0, + source + }); + })(), + (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() { + const source = new VectorSource({ strategy: bboxStrategy }); + source.setLoader( + buildVectorLoader( + { + featureTypes: ["bottlenecks_geoserver"], + geometryName: "area" + }, + source + ) + ); + return new VectorLayer({ + id: "BOTTLENECKS", + label: "Bottlenecks", + visible: true, + style: styles.bottleneck, + source + }); + })(), + new TileLayer({ + id: "BOTTLENECKISOLINE", + label: "Bottleneck isolines", + visible: false, + source: new TileWMS({ + preload: 0, + projection: "EPSG:3857", + url: window.location.origin + "/api/internal/wms", + params: { + LAYERS: "sounding_results_contour_lines_geoserver", + VERSION: "1.1.1", + TILED: true + }, + tileLoadFunction: function(tile, src) { + // console.log("calling for", 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); + }) + }), + new TileLayer({ + id: "DIFFERENCES", + label: "Bottleneck Differences", + visible: false, + source: new TileWMS({ + preload: 0, + projection: "EPSG:4326", + url: window.location.origin + "/api/internal/wms", + params: { + LAYERS: "sounding_differences", + VERSION: "1.1.1", + TILED: true + }, + tileLoadFunction: function(tile, src) { + // console.log("calling for", 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: ["bottlenecks_geoserver"], + geometryName: "area" + }, + source + ) + ); + return new VectorLayer({ + id: "BOTTLENECKSTATUS", + label: "Critical Bottlenecks", + forLegendStyle: { point: true, resolution: 16 }, + visible: true, + style: styles.bottleneckStatus, + source + }); + })(), + (function() { + const source = new VectorSource({ strategy: bboxStrategy }); + source.setLoader( + buildVectorLoader( + { + featureTypes: ["distance_marks_ashore_geoserver"], + geometryName: "geom" + }, + source + ) + ); + return new VectorLayer({ + id: "DISTANCEMARKS", + label: "Distance marks", + forLegendStyle: { point: true, resolution: 8 }, + visible: false, + source + }); + })(), + (function() { + const source = new VectorSource({ strategy: bboxStrategy }); + source.setLoader( + buildVectorLoader( + { + featureTypes: ["distance_marks_geoserver"], + geometryName: "geom" + }, + source + ) + ); + return new VectorLayer({ + id: "DISTANCEMARKSAXIS", + label: "Distance marks, Axis", + forLegendStyle: { point: true, resolution: 8 }, + visible: true, + style: styles.dma, + source + }); + })(), + (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 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; + } + }), + 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: "#333333", + width: 2, + 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; + } + }) + ] + }; +})();
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/client/src/components/map/styles.js Thu Apr 11 12:14:01 2019 +0200 @@ -0,0 +1,201 @@ +import { Icon, Stroke, Style, Fill, Text, Circle } from "ol/style"; +import Point from "ol/geom/Point"; +import { getCenter } from "ol/extent"; + +const styles = { + blue1: new Style({ + stroke: new Stroke({ + color: "rgba(0, 0, 255, 0.8)", + lineDash: [2, 4], + lineCap: "round", + width: 2 + }), + fill: new Fill({ + color: "rgba(240, 230, 0, 0.2)" + }) + }), + blue2: new Style({ + stroke: new Stroke({ + color: "rgba(0, 0, 255, 0.9)", + lineDash: [3, 6], + lineCap: "round", + width: 2 + }), + fill: new Fill({ + color: "rgba(240, 230, 0, 0.1)" + }) + }), + blue3: new Style({ + stroke: new Stroke({ + color: "rgba(0, 0, 255, 1.0)", + width: 2 + }), + fill: new Fill({ + color: "rgba(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)" + }) + }), + 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 + }) + }) +}; + +export default { + stretches(feature) { + let style = styles.yellow2; + if (feature.get("highlighted")) { + style = styles.yellow3; + } + 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; + }, + 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) { + return [ + new Style({ + image: new Icon({ + src: require("@/assets/marker-gauge.png"), + anchor: [0.5, isLegend ? 0.5 : 1], + scale: isLegend ? 0.5 : 1 + }), + text: new Text({ + font: '10px "Open Sans", "sans-serif"', + offsetY: 8, + fill: new Fill({ + color: "white" + }), + text: feature.get("objname") + }) + }), + new Style({ + text: new Text({ + font: '10px "Open Sans", "sans-serif"', + offsetY: 7, + offsetX: -1, + fill: new Fill({ + color: "black" + }), + text: feature.get("objname") + }) + }) + ]; + } +};
--- a/client/src/store/application.js Thu Apr 11 12:13:27 2019 +0200 +++ b/client/src/store/application.js Thu Apr 11 12:14:01 2019 +0200 @@ -24,7 +24,7 @@ secondaryLogo: process.env.VUE_APP_SECONDARY_LOGO_URL, logoForPDF: process.env.VUE_APP_LOGO_FOR_PDF_URL, popup: null, - panes: ["Maplayer"], + panes: ["Map"], paneMode: null, splitscreens: [], splitscreenLoading: false,