view client/src/store/map.js @ 2643:27933e66e848

client: gauges: use isrs_code as unique id for gauges
author Markus Kottlaender <markus@intevation.de>
date Thu, 14 Mar 2019 13:11:22 +0100
parents 85f9bf4a6eba
children b79f5c5404c2
line wrap: on
line source

/* 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):
 * * Bernhard Reiter <bernhard.reiter@intevation.de>
 * * Markus Kottländer <markus@intevation.de>
 * * Thomas Junk <thomas.junk@intevation.de>
 */

//import { HTTP } from "../lib/http";

import TileWMS from "ol/source/TileWMS.js";
import { Tile as TileLayer, Vector as VectorLayer } from "ol/layer.js";
import OSM from "ol/source/OSM";
import Draw from "ol/interaction/Draw.js";
import { Icon, Stroke, Style, Fill, Text, Circle } from "ol/style.js";
import VectorSource from "ol/source/Vector.js";
import Point from "ol/geom/Point.js";
import { bbox as bboxStrategy } from "ol/loadingstrategy";
import { HTTP } from "../lib/http";
import { fromLonLat } from "ol/proj";
import { getLength, getArea } from "ol/sphere.js";
import { unByKey } from "ol/Observable";
import { getCenter } from "ol/extent";
import { transformExtent } from "ol/proj.js";
import bbox from "@turf/bbox";
import app from "../main";

const LAYERS = {
  OPENSTREETMAP: "Open Streetmap",
  INLANDECDIS: "Inland ECDIS chart Danube",
  WATERWAYAREA: "Waterway Area",
  STRETCHES: "Stretches",
  FAIRWAYDIMENSIONSLOS1: "LOS 1 Fairway Dimensions",
  FAIRWAYDIMENSIONSLOS2: "LOS 2 Fairway Dimensions",
  FAIRWAYDIMENSIONSLOS3: "LOS 3 Fairway Dimensions",
  WATERWAYAXIS: "Waterway Axis",
  WATERWAYPROFILES: "Waterway Profiles",
  BOTTLENECKS: "Bottlenecks",
  BOTTLENECKSTATUS: "Critical Bottlenecks",
  BOTTLENECKISOLINE: "Bottleneck isolines",
  DISTANCEMARKS: "Distance marks",
  DISTANCEMARKSAXIS: "Distance marks, Axis",
  GAUGES: "Gauges",
  DRAWTOOL: "Draw Tool",
  CUTTOOL: "Cut Tool"
};

const moveMap = ({ view, extent, zoom, preventZoomOut }) => {
  const currentZoom = view.getZoom();
  view.fit(extent, {
    maxZoom: preventZoomOut ? Math.max(zoom, currentZoom) : zoom,
    duration: 700
  });
};

// initial state
const init = () => {
  return {
    openLayersMap: null,
    initialLoad: true,
    extent: {
      lat: 6155376,
      lon: 1819178,
      zoom: 11
    },
    identifyTool: null, // event binding (singleclick, dblclick)
    identifiedFeatures: [], // map features identified by clicking on the map
    currentMeasurement: null, // distance or area from line-/polygon-/cutTool
    lineTool: null, // open layers interaction object (Draw)
    polygonTool: null, // open layers interaction object (Draw)
    cutTool: null, // open layers interaction object (Draw)
    isolinesLegendImgDataURL: "",
    layers: [
      {
        name: LAYERS.OPENSTREETMAP,
        data: new TileLayer({
          source: new OSM()
        }),
        isVisible: true,
        showInLegend: true
      },
      {
        name: LAYERS.INLANDECDIS,
        data: new TileLayer({
          source: new TileWMS({
            preload: 1,
            url: "https://service.d4d-portal.info/wms/",
            crossOrigin: "anonymous",
            params: { LAYERS: "d4d", VERSION: "1.1.1", TILED: true }
          })
        }),
        isVisible: true,
        showInLegend: true
      },
      {
        name: LAYERS.WATERWAYAREA,
        data: new VectorLayer({
          source: new VectorSource({
            strategy: bboxStrategy
          }),
          style: new Style({
            stroke: new Stroke({
              color: "rgba(0, 102, 0, 1)",
              width: 2
            })
          })
        }),
        isVisible: true,
        showInLegend: true
      },
      {
        name: LAYERS.STRETCHES,
        data: new VectorLayer({
          source: new VectorSource({
            strategy: bboxStrategy
          }),
          style: new Style({
            stroke: new Stroke({
              color: "rgba(250, 200, 0, .8)",
              width: 2
            }),
            fill: new Fill({
              color: "rgba(250, 200, 10, .3)"
            })
          })
        }),
        isVisible: false,
        showInLegend: true
      },
      {
        name: LAYERS.FAIRWAYDIMENSIONSLOS3,
        data: new VectorLayer({
          source: new VectorSource(),
          style: function() {
            return [
              new Style({
                stroke: new Stroke({
                  color: "rgba(0, 0, 255, 1.0)",
                  width: 2
                }),
                fill: new Fill({
                  color: "rgba(255, 255, 255, 0.4)"
                })
              }),
              new Style({
                text: new Text({
                  font: 'bold 12px "Open Sans", "sans-serif"',
                  placement: "line",
                  fill: new Fill({
                    color: "black"
                  }),
                  text: "LOS: 3"
                  //, zIndex: 10
                })
              })
            ];
          }
        }),
        isVisible: true,
        showInLegend: true
      },
      {
        name: LAYERS.FAIRWAYDIMENSIONSLOS2,
        data: new VectorLayer({
          source: new VectorSource(),
          style: function() {
            return [
              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)"
                })
              }),
              new Style({
                text: new Text({
                  font: 'bold 12px "Open Sans", "sans-serif"',
                  placement: "line",
                  fill: new Fill({
                    color: "black"
                  }),
                  text: "LOS: 2"
                  //, zIndex: 10
                })
              })
            ];
          }
        }),
        isVisible: false,
        showInLegend: true
      },
      {
        name: LAYERS.FAIRWAYDIMENSIONSLOS1,
        data: new VectorLayer({
          source: new VectorSource(),
          style: function() {
            return [
              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)"
                })
              }),
              new Style({
                text: new Text({
                  font: 'bold 12px "Open Sans", "sans-serif"',
                  placement: "line",
                  fill: new Fill({
                    color: "black"
                  }),
                  text: "LOS: 1"
                  //, zIndex: 10
                })
              })
            ];
          }
        }),
        isVisible: false,
        showInLegend: true
      },
      {
        name: LAYERS.WATERWAYAXIS,
        data: new VectorLayer({
          source: new VectorSource({
            strategy: bboxStrategy
          }),
          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
        }),
        isVisible: true,
        showInLegend: true
      },
      {
        name: LAYERS.WATERWAYPROFILES,
        data: new VectorLayer({
          source: new VectorSource({
            strategy: bboxStrategy
          }),
          style: new Style({
            stroke: new Stroke({
              color: "rgba(0, 0, 255, .5)",
              lineDash: [5, 5],
              width: 2
            })
          }),
          maxResolution: 2.5,
          minResolution: 0
        }),
        isVisible: true,
        showInLegend: true
      },
      {
        name: LAYERS.BOTTLENECKS,
        data: new VectorLayer({
          source: new VectorSource({
            strategy: bboxStrategy
          }),
          style: function() {
            return new Style({
              stroke: new Stroke({
                color: "rgba(230, 230, 10, .8)",
                width: 4
              }),
              fill: new Fill({
                color: "rgba(230, 230, 10, .3)"
              })
            });
          }
        }),
        isVisible: true,
        showInLegend: true
      },
      {
        name: LAYERS.BOTTLENECKISOLINE,
        data: new TileLayer({
          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);
          })
        }),
        isVisible: false,
        showInLegend: true
      },
      {
        name: LAYERS.BOTTLENECKSTATUS,
        forLegendStyle: { point: true, resolution: 16 },
        data: new VectorLayer({
          source: new VectorSource({
            strategy: bboxStrategy
          }),
          style: function(feature, resolution, isLegend) {
            let styles = [];
            if ((feature.get("fa_critical") && resolution > 15) || isLegend) {
              let bnCenter = getCenter(feature.getGeometry().getExtent());
              styles.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) {
              styles.push(
                new Style({
                  stroke: new Stroke({
                    color: "rgba(255, 0, 0, 1)",
                    width: 4
                  })
                })
              );
            }
            return styles;
          }
        }),
        isVisible: true,
        showInLegend: true
      },
      {
        name: LAYERS.DISTANCEMARKS,
        forLegendStyle: { point: true, resolution: 8 },
        data: new VectorLayer({
          source: new VectorSource({
            strategy: bboxStrategy
          })
        }),
        isVisible: false,
        showInLegend: true
      },
      {
        name: LAYERS.DISTANCEMARKSAXIS,
        forLegendStyle: { point: true, resolution: 8 },
        data: new VectorLayer({
          source: new VectorSource({
            strategy: bboxStrategy
          }),
          style: function(feature, resolution) {
            if (resolution < 10) {
              var s = new Style({
                image: new Circle({
                  radius: 5,
                  fill: new Fill({ color: "rgba(255, 0, 0, 0.1)" }),
                  stroke: new Stroke({ color: "blue", width: 1 })
                })
              });
              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;
            } else {
              return [];
            }
          }
        }),
        isVisible: true,
        showInLegend: true
      },
      {
        name: LAYERS.GAUGES,
        forLegendStyle: { point: true, resolution: 8 },
        data: new VectorLayer({
          source: new VectorSource({
            strategy: bboxStrategy
          }),
          style: function(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")
                })
              })
            ];
          },
          maxResolution: 100,
          minResolution: 0
        }),
        isVisible: true,
        showInLegend: true
      },
      {
        name: LAYERS.DRAWTOOL,
        data: new VectorLayer({
          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;
          }
        }),
        isVisible: true,
        showInLegend: false
      },
      {
        name: LAYERS.CUTTOOL,
        data: new VectorLayer({
          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;
          }
        }),
        isVisible: true,
        showInLegend: false
      }
    ]
  };
};

export default {
  init,
  namespaced: true,
  state: init(),
  getters: {
    layersForLegend: state => {
      return state.layers.filter(layer => layer.showInLegend);
    },
    getLayerByName: state => name => {
      return state.layers.find(layer => layer.name === name);
    },
    getVSourceByName: (state, getters) => name => {
      return getters.getLayerByName(name).data.getSource();
    },
    filteredIdentifiedFeatures: state => {
      return state.identifiedFeatures.filter(f => f.getId());
    }
  },
  mutations: {
    initialLoad: (state, initialLoad) => {
      state.initialLoad = initialLoad;
    },
    extent: (state, extent) => {
      state.extent = extent;
    },
    setLayerVisible: (state, name) => {
      const layer = state.layers.findIndex(l => l.name === name);
      state.layers[layer].isVisible = true;
      state.layers[layer].data.setVisible(true);
    },
    setLayerInvisible: (state, name) => {
      const layer = state.layers.findIndex(l => l.name === name);
      state.layers[layer].isVisible = false;
      state.layers[layer].data.setVisible(false);
    },
    toggleVisibilityByName: (state, name) => {
      const layer = state.layers.findIndex(l => l.name === name);
      state.layers[layer].isVisible = !state.layers[layer].isVisible;
      state.layers[layer].data.setVisible(state.layers[layer].isVisible);
    },
    toggleVisibility: (state, layer) => {
      state.layers[layer].isVisible = !state.layers[layer].isVisible;
      state.layers[layer].data.setVisible(state.layers[layer].isVisible);
    },
    openLayersMap: (state, map) => {
      state.openLayersMap = map;
    },
    identifyTool: (state, events) => {
      state.identifyTool = events;
    },
    setIdentifiedFeatures: (state, identifiedFeatures) => {
      state.identifiedFeatures = identifiedFeatures;
    },
    setCurrentMeasurement: (state, measurement) => {
      state.currentMeasurement = measurement;
    },
    lineTool: (state, lineTool) => {
      state.lineTool = lineTool;
    },
    polygonTool: (state, polygonTool) => {
      state.polygonTool = polygonTool;
    },
    cutTool: (state, cutTool) => {
      state.cutTool = cutTool;
    },
    moveToBoundingBox: (state, { boundingBox, zoom, preventZoomOut }) => {
      const extent = transformExtent(boundingBox, "EPSG:4326", "EPSG:3857");
      let view = state.openLayersMap.getView();
      moveMap({ view, extent, zoom, preventZoomOut });
    },
    moveToExtent: (state, { feature, zoom, preventZoomOut }) => {
      const boundingBox = bbox(feature.geometry);
      const extent = transformExtent(boundingBox, "EPSG:4326", "EPSG:3857");
      let view = state.openLayersMap.getView();
      moveMap({ view, extent, zoom, preventZoomOut });
    },
    moveMap: (state, { coordinates, zoom, preventZoomOut }) => {
      let view = state.openLayersMap.getView();
      const currentZoom = view.getZoom();
      view.animate({
        zoom: preventZoomOut ? Math.max(zoom, currentZoom) : zoom,
        center: fromLonLat(coordinates, view.getProjection()),
        duration: 700
      });
    },
    isolinesLegendImgDataURL: (state, isolinesLegendImgDataURL) => {
      state.isolinesLegendImgDataURL = isolinesLegendImgDataURL;
    }
  },
  actions: {
    openLayersMap({ commit, dispatch, getters }, map) {
      const drawVectorSrc = getters.getVSourceByName("Draw Tool");
      const cutVectorSrc = getters.getVSourceByName("Cut Tool");

      // init line tool
      const lineTool = new Draw({
        source: drawVectorSrc,
        type: "LineString",
        maxPoints: 2
      });
      lineTool.setActive(false);
      lineTool.on("drawstart", () => {
        drawVectorSrc.clear();
        commit("setCurrentMeasurement", null);
      });
      lineTool.on("drawend", event => {
        commit("setCurrentMeasurement", {
          quantity: app.$gettext("Length"),
          unitSymbol: "m",
          value: Math.round(getLength(event.feature.getGeometry()) * 10) / 10
        });
        commit("application/showIdentify", true, { root: true });
      });

      // init polygon tool
      const polygonTool = new Draw({
        source: drawVectorSrc,
        type: "Polygon",
        maxPoints: 50
      });
      polygonTool.setActive(false);
      polygonTool.on("drawstart", () => {
        drawVectorSrc.clear();
        commit("setCurrentMeasurement", null);
      });
      polygonTool.on("drawend", event => {
        const areaSize = getArea(event.feature.getGeometry());
        commit("setCurrentMeasurement", {
          quantity: app.$gettext("Area"),
          unitSymbol: areaSize > 100000 ? "km²" : "m²",
          value:
            areaSize > 100000
              ? Math.round(areaSize / 1000) / 1000 // convert into 1 km² == 1000*1000 m² and round to 1000 m²
              : Math.round(areaSize)
        });
        commit("application/showIdentify", true, { root: true });
      });

      // init cut tool
      const cutTool = new Draw({
        source: cutVectorSrc,
        type: "LineString",
        maxPoints: 2,
        style: new Style({
          stroke: new Stroke({
            color: "#444",
            width: 2,
            lineDash: [7, 7]
          }),
          image: new Circle({
            fill: new Fill({ color: "#333" }),
            stroke: new Stroke({ color: "#fff", width: 1.5 }),
            radius: 6
          })
        })
      });
      cutTool.setActive(false);
      cutTool.on("drawstart", () => {
        dispatch("disableIdentifyTool");
        cutVectorSrc.clear();
      });
      cutTool.on("drawend", event => {
        commit("fairwayprofile/selectedCut", null, { root: true });
        dispatch("fairwayprofile/cut", event.feature, { root: true }).then(() =>
          // This setTimeout is an ugly workaround. If we would enable the
          // identifyTool here immediately then the click event from ending the
          // cut will trigger it. We don't want that.
          setTimeout(() => dispatch("enableIdentifyTool"), 1000)
        );
      });

      map.addInteraction(lineTool);
      map.addInteraction(cutTool);
      map.addInteraction(polygonTool);

      commit("lineTool", lineTool);
      commit("polygonTool", polygonTool);
      commit("cutTool", cutTool);
      commit("openLayersMap", map);
    },
    disableIdentifyTool({ state }) {
      unByKey(state.identifyTool);
      state.identifyTool = null;
    },
    enableIdentifyTool({ state, rootState, commit, dispatch, getters }) {
      if (!state.identifyTool) {
        state.identifyTool = state.openLayersMap.on(
          ["singleclick", "dblclick"],
          event => {
            commit("setIdentifiedFeatures", []);
            // checking our WFS layers
            var features = state.openLayersMap.getFeaturesAtPixel(event.pixel, {
              hitTolerance: 7
            });
            if (features) {
              let identifiedFeatures = [];

              for (let feature of features) {
                let id = feature.getId();

                // avoid identifying the same feature twice
                if (
                  identifiedFeatures.findIndex(
                    f => f.getId() === feature.getId()
                  ) === -1
                ) {
                  identifiedFeatures.push(feature);
                }

                // get selected bottleneck
                // RegExp.prototype.test() works with number, str and undefined
                if (/^bottlenecks/.test(id)) {
                  if (
                    rootState.bottlenecks.selectedBottleneck !=
                    feature.get("objnam")
                  ) {
                    dispatch(
                      "bottlenecks/setSelectedBottleneck",
                      feature.get("objnam"),
                      { root: true }
                    ).then(() => {
                      this.commit("bottlenecks/setFirstSurveySelected");
                    });
                    commit("moveMap", {
                      coordinates: getCenter(
                        feature
                          .getGeometry()
                          .clone()
                          .transform("EPSG:3857", "EPSG:4326")
                          .getExtent()
                      ),
                      zoom: 17,
                      preventZoomOut: true
                    });
                  }
                }

                // get selected gauge
                if (/^gauges/.test(id)) {
                  if (
                    rootState.gauges.selectedGaugeISRS !==
                    feature.get("isrs_code")
                  ) {
                    dispatch(
                      "gauges/selectedGaugeISRS",
                      feature.get("isrs_code"),
                      {
                        root: true
                      }
                    );
                    commit("moveMap", {
                      coordinates: getCenter(
                        feature
                          .getGeometry()
                          .clone()
                          .transform("EPSG:3857", "EPSG:4326")
                          .getExtent()
                      ),
                      zoom: 17,
                      preventZoomOut: true
                    });
                  }
                }
              }

              commit("setIdentifiedFeatures", identifiedFeatures);
            }

            // DEBUG output and example how to remove the GeometryName
            /*
            for (let feature of features) {
              console.log("Identified:", feature.getId());
              for (let key of feature.getKeys()) {
                if (key != feature.getGeometryName()) {
                  console.log(key, feature.get(key));
                }
              }
            }
            */

            // trying the GetFeatureInfo way for WMS
            var wmsSource = getters.getVSourceByName(
              "Inland ECDIS chart Danube"
            );
            var url = wmsSource.getGetFeatureInfoUrl(
              event.coordinate,
              100 /* resolution */,
              "EPSG:3857",
              // { INFO_FORMAT: "application/vnd.ogc.gml" } // not allowed by d4d
              { INFO_FORMAT: "text/plain" }
            );

            if (url) {
              // cannot directly query here because of SOP
              console.log("GetFeatureInfo url:", url);
            }
          }
        );
      }
    }
  }
};

export { LAYERS };