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,