view client/src/store/map.js @ 2957:b74ebeb2bdc8

client: layers: improved structure of layer configuration The object is now less cluttered, access to the layers is more direct, no need for helper methods anymore.
author Markus Kottlaender <markus@intevation.de>
date Mon, 08 Apr 2019 15:32:53 +0200
parents 1ac58e024942
children 1b8bb4f89227
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 moveMap = ({ view, extent, zoom, preventZoomOut }) => {
  const currentZoom = view.getZoom();
  zoom = zoom || currentZoom;
  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: "",
    differencesLegendImgDataURL: "",
    layers: {
      OPENSTREETMAP: new TileLayer({
        label: "Open Streetmap",
        visible: true,
        source: new OSM()
      }),
      INLANDECDIS: new TileLayer({
        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 }
        })
      }),
      WATERWAYAREA: new VectorLayer({
        label: "Waterway Area",
        visible: true,
        source: new VectorSource({
          strategy: bboxStrategy
        }),
        style: new Style({
          stroke: new Stroke({
            color: "rgba(0, 102, 0, 1)",
            width: 2
          })
        })
      }),
      STRETCHES: new VectorLayer({
        label: "Stretches",
        visible: false,
        source: new VectorSource({
          strategy: bboxStrategy
        }),
        style: feature => {
          let style = new Style({
            stroke: new Stroke({
              color: "rgba(250, 200, 0, .8)",
              width: 2
            }),
            fill: new Fill({
              color: "rgba(250, 200, 10, .3)"
            })
          });
          if (feature.get("highlighted")) {
            style = new Style({
              stroke: new Stroke({
                color: "rgba(250, 240, 10, .9)",
                width: 5
              }),
              fill: new Fill({
                color: "rgba(250, 240, 0, .7)"
              })
            });
          }

          return style;
        }
      }),
      FAIRWAYDIMENSIONSLOS1: new VectorLayer({
        label: "LOS 1 Fairway Dimensions",
        visible: false,
        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
              })
            })
          ];
        }
      }),
      FAIRWAYDIMENSIONSLOS2: new VectorLayer({
        label: "LOS 2 Fairway Dimensions",
        visible: false,
        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
              })
            })
          ];
        }
      }),
      FAIRWAYDIMENSIONSLOS3: new VectorLayer({
        label: "LOS 3 Fairway Dimensions",
        visible: true,
        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
              })
            })
          ];
        }
      }),
      WATERWAYAXIS: new VectorLayer({
        label: "Waterway Axis",
        visible: true,
        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
      }),
      WATERWAYPROFILES: new VectorLayer({
        label: "Waterway Profiles",
        visible: true,
        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
      }),
      BOTTLENECKS: new VectorLayer({
        label: "Bottlenecks",
        visible: true,
        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)"
            })
          });
        }
      }),
      BOTTLENECKISOLINE: new TileLayer({
        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);
        })
      }),
      DIFFERENCES: new TileLayer({
        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);
        })
      }),
      BOTTLENECKSTATUS: new VectorLayer({
        label: "Critical Bottlenecks",
        forLegendStyle: { point: true, resolution: 16 },
        visible: true,
        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;
        }
      }),
      DISTANCEMARKS: new VectorLayer({
        label: "Distance marks",
        forLegendStyle: { point: true, resolution: 8 },
        visible: false,
        source: new VectorSource({
          strategy: bboxStrategy
        })
      }),
      DISTANCEMARKSAXIS: new VectorLayer({
        label: "Distance marks, Axis",
        forLegendStyle: { point: true, resolution: 8 },
        visible: true,
        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 [];
          }
        }
      }),
      GAUGES: new VectorLayer({
        label: "Gauges",
        forLegendStyle: { point: true, resolution: 8 },
        visible: true,
        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
      }),
      DRAWTOOL: new VectorLayer({
        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;
        }
      }),
      CUTTOOL: new VectorLayer({
        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;
        }
      })
    }
  };
};

export default {
  init,
  namespaced: true,
  state: init(),
  getters: {
    filteredIdentifiedFeatures: state => {
      return state.identifiedFeatures.filter(f => f.getId());
    }
  },
  mutations: {
    initialLoad: (state, initialLoad) => {
      state.initialLoad = initialLoad;
    },
    extent: (state, extent) => {
      state.extent = extent;
    },
    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();
      zoom = zoom || currentZoom;
      view.animate({
        zoom: preventZoomOut ? Math.max(zoom, currentZoom) : zoom,
        center: fromLonLat(coordinates, view.getProjection()),
        duration: 700
      });
    },
    isolinesLegendImgDataURL: (state, isolinesLegendImgDataURL) => {
      state.isolinesLegendImgDataURL = isolinesLegendImgDataURL;
    },
    differencesLegendImgDataURL: (state, differencesLegendImgDataURL) => {
      state.differencesLegendImgDataURL = differencesLegendImgDataURL;
    }
  },
  actions: {
    openLayersMap({ commit, dispatch, state }, map) {
      const drawVectorSrc = state.layers.DRAWTOOL.getSource();
      const cutVectorSrc = state.layers.CUTTOOL.getSource();

      // 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 }) {
      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: null,
                      preventZoomOut: true
                    });
                  }
                }

                // get selected stretch
                if (/^stretches/.test(id)) {
                  if (rootState.imports.selectedStretchId === feature.getId()) {
                    commit("imports/selectedStretchId", null, { root: true });
                  } else {
                    commit("imports/selectedStretchId", feature.getId(), {
                      root: true
                    });
                    commit("moveMap", {
                      coordinates: getCenter(
                        feature
                          .getGeometry()
                          .clone()
                          .transform("EPSG:3857", "EPSG:4326")
                          .getExtent()
                      ),
                      zoom: null,
                      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 = state.layers.INLANDECDIS.getSource();
            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);
            }
          }
        );
      }
    }
  }
};