changeset 5076:96a544504818 time-sliding

move layer.js and styles.js to layers directory
author Thomas Junk <thomas.junk@intevation.de>
date Fri, 13 Mar 2020 11:48:15 +0100
parents aeb100b4c41b
children c9354fcf6050
files client/src/components/identify/Identify.vue client/src/components/importoverview/FairwayDimensionDetail.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/lib/session.js
diffstat 8 files changed, 1129 insertions(+), 1129 deletions(-) [+]
line wrap: on
line diff
--- a/client/src/components/identify/Identify.vue	Fri Mar 13 10:37:44 2020 +0100
+++ b/client/src/components/identify/Identify.vue	Fri Mar 13 11:48:15 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 13 10:37:44 2020 +0100
+++ b/client/src/components/importoverview/FairwayDimensionDetail.vue	Fri Mar 13 11:48:15 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";
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/client/src/components/layers/layers.js	Fri Mar 13 11:48:15 2020 +0100
@@ -0,0 +1,723 @@
+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
+              //CQL_FILTER: "" + store.state.application.currentVisibleTime
+            },
+            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	Fri Mar 13 11:48:15 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 13 10:37:44 2020 +0100
+++ b/client/src/components/map/Map.vue	Fri Mar 13 11:48:15 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/layers.js	Fri Mar 13 10:37:44 2020 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,723 +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
-              //CQL_FILTER: "" + store.state.application.currentVisibleTime
-            },
-            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 13 10:37:44 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 };
--- a/client/src/lib/session.js	Fri Mar 13 10:37:44 2020 +0100
+++ b/client/src/lib/session.js	Fri Mar 13 11:48:15 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");