view client/src/store/map.js @ 3678:8f58851927c0

client: make layer factory only return new layer config for individual maps instead of each time it is invoked. The purpose of the factory was to support multiple maps with individual layers. But returning a new config each time it is invoked leads to bugs that rely on the layer's state. Now this factory reuses the same objects it created before, per map.
author Markus Kottlaender <markus@intevation.de>
date Mon, 17 Jun 2019 17:31:35 +0200
parents c0f5f62343c9
children 45eab8e9b580
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 Draw from "ol/interaction/Draw";
import { Stroke, Style, Fill, Circle } from "ol/style";
import { fromLonLat } from "ol/proj";
import { getLength, getArea } from "ol/sphere";
import { transformExtent } from "ol/proj";
import bbox from "@turf/bbox";
import app from "@/main";
import { HTTP } from "@/lib/http";
import Feature from "ol/Feature";
import Point from "ol/geom/Point";
import { Vector as VectorLayer } from "ol/layer";
import { toLonLat } from "ol/proj";

// initial state
const init = () => {
  return {
    openLayersMaps: [],
    syncedMaps: [],
    syncedView: null,
    mapPopup: null,
    mapPopupEnabled: true,
    initialLoad: true,
    extent: {
      lat: 6155376,
      lon: 1819178,
      zoom: 11
    },
    identifiedFeatures: [], // map features identified by clicking on the map
    identifiedCoordinates: null,
    currentMeasurement: null, // distance or area from line-/polygon-/cutTool
    lineToolEnabled: false,
    polygonToolEnabled: false,
    cutToolEnabled: false,
    isolinesLegendImgDataURL: "",
    differencesLegendImgDataURL: ""
  };
};

export default {
  init,
  namespaced: true,
  state: init(),
  getters: {
    openLayersMap: state => id => {
      return state.openLayersMaps.find(
        map => map.getTarget() === "map-" + (id || "main")
      );
    },
    filteredIdentifiedFeatures: state => {
      return state.identifiedFeatures.filter(f => f.getId());
    }
  },
  mutations: {
    initialLoad: (state, initialLoad) => {
      state.initialLoad = initialLoad;
    },
    extent: (state, extent) => {
      state.extent = extent;
    },
    addOpenLayersMap: (state, map) => {
      state.openLayersMaps.push(map);
    },
    removeOpenLayersMap: (state, map) => {
      let index = state.openLayersMaps.findIndex(
        m => m.getTarget() === map.getTarget()
      );
      if (index !== -1) {
        state.openLayersMaps.splice(index, 1);
      }
    },
    syncedMaps: (state, ids) => {
      state.syncedMaps = ids;
    },
    syncedView: (state, view) => {
      state.syncedView = view;
    },
    mapPopup: (state, popup) => {
      state.mapPopup = popup;
    },
    mapPopupEnabled: (state, enabled) => {
      state.mapPopupEnabled = enabled;
    },
    setIdentifiedFeatures: (state, identifiedFeatures) => {
      state.identifiedFeatures = identifiedFeatures;
    },
    addIdentifiedFeatures: (state, identifiedFeatures) => {
      state.identifiedFeatures = state.identifiedFeatures.concat(
        identifiedFeatures
      );
    },
    identifiedCoordinates: (state, coordinates) => {
      state.identifiedCoordinates = coordinates;
    },
    setCurrentMeasurement: (state, measurement) => {
      state.currentMeasurement = measurement;
    },
    lineToolEnabled: (state, enabled) => {
      state.lineToolEnabled = enabled;
      state.openLayersMaps.forEach(m => {
        let tool = m
          .getInteractions()
          .getArray()
          .find(i => i.get("id") === "linetool");
        if (tool) {
          tool.setActive(enabled);
        }
      });
    },
    polygonToolEnabled: (state, enabled) => {
      state.polygonToolEnabled = enabled;
      state.openLayersMaps.forEach(m => {
        let tool = m
          .getInteractions()
          .getArray()
          .find(i => i.get("id") === "polygontool");
        if (tool) {
          tool.setActive(enabled);
        }
      });
    },
    cutToolEnabled: (state, enabled) => {
      state.cutToolEnabled = enabled;
      state.openLayersMaps.forEach(m => {
        let tool = m
          .getInteractions()
          .getArray()
          .find(i => i.get("id") === "cuttool");
        if (tool) {
          tool.setActive(enabled);
        }
      });
    },
    isolinesLegendImgDataURL: (state, isolinesLegendImgDataURL) => {
      state.isolinesLegendImgDataURL = isolinesLegendImgDataURL;
    },
    differencesLegendImgDataURL: (state, differencesLegendImgDataURL) => {
      state.differencesLegendImgDataURL = differencesLegendImgDataURL;
    }
  },
  actions: {
    openLayersMap({ state, commit, dispatch }, map) {
      const drawVectorSrc = map.getLayer("DRAWTOOL").getSource();
      const cutVectorSrc = map.getLayer("CUTTOOL").getSource();

      // init line tool
      const lineTool = new Draw({
        source: drawVectorSrc,
        type: "LineString",
        maxPoints: 2,
        stopClick: true
      });
      lineTool.set("id", "linetool");
      lineTool.setActive(false);
      lineTool.on("drawstart", () => {
        state.openLayersMaps.forEach(m => {
          m.getLayer("DRAWTOOL")
            .getSource()
            .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,
        stopClick: true
      });
      polygonTool.set("id", "polygontool");
      polygonTool.setActive(false);
      polygonTool.on("drawstart", () => {
        state.openLayersMaps.forEach(m => {
          m.getLayer("DRAWTOOL")
            .getSource()
            .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,
        stopClick: true,
        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.set("id", "cuttool");
      cutTool.setActive(false);
      cutTool.on("drawstart", () => {
        state.openLayersMaps.forEach(m => {
          m.getLayer("CUTTOOL")
            .getSource()
            .clear();
        });
      });
      cutTool.on("drawend", event => {
        commit("fairwayprofile/selectedCut", null, { root: true });
        dispatch("fairwayprofile/cut", event.feature, { root: true });
      });

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

      // If there are multiple maps and you enable one of the draw tools, when
      // moving the mouse to another map, the cursor for the draw tool stays
      // visible in the first map, right next to the edge where the cursor left
      // the map. So here we disable all draw layers except the ones in the map
      // that the user currently hovering with the mouse.
      map.getTargetElement().addEventListener("mouseenter", () => {
        if (
          state.lineToolEnabled ||
          state.polygonToolEnabled ||
          state.cutToolEnabled
        ) {
          state.openLayersMaps.forEach(m => {
            let lineTool = m
              .getInteractions()
              .getArray()
              .find(i => i.get("id") === "linetool");
            let polygonTool = m
              .getInteractions()
              .getArray()
              .find(i => i.get("id") === "polygontool");
            let cutTool = m
              .getInteractions()
              .getArray()
              .find(i => i.get("id") === "cuttool");
            if (lineTool) lineTool.setActive(false);
            if (polygonTool) polygonTool.setActive(false);
            if (cutTool) cutTool.setActive(false);
          });
          let lineTool = map
            .getInteractions()
            .getArray()
            .find(i => i.get("id") === "linetool");
          let polygonTool = map
            .getInteractions()
            .getArray()
            .find(i => i.get("id") === "polygontool");
          let cutTool = map
            .getInteractions()
            .getArray()
            .find(i => i.get("id") === "cuttool");
          if (lineTool && state.lineToolEnabled) lineTool.setActive(true);
          if (polygonTool && state.polygonToolEnabled)
            polygonTool.setActive(true);
          if (cutTool && state.cutToolEnabled) cutTool.setActive(true);
        }
      });

      commit("addOpenLayersMap", map);
    },
    initIdentifyTool({ state, rootState, commit, dispatch }, map) {
      map.on(["singleclick", "dblclick"], event => {
        commit(
          "identifiedCoordinates",
          toLonLat(event.coordinate, map.getView().getProjection())
        );
        state.mapPopup.setPosition(undefined);
        commit("setIdentifiedFeatures", []);
        // checking our WFS layers
        var features = map.getFeaturesAtPixel(event.pixel, { hitTolerance: 7 });
        if (features) {
          let all = [];
          let bottlenecks = [];
          let gauges = [];
          let stretches = [];
          let sections = [];

          for (let feature of features) {
            // avoid identifying the same feature twice
            if (all.findIndex(f => f.getId() === feature.getId()) === -1)
              all.push(feature);

            let id = feature.getId();
            // RegExp.prototype.test() works with number, str and undefined
            // get selected bottleneck
            if (/^bottlenecks/.test(id)) bottlenecks.push(feature);
            // get selected gauge
            if (/^gauges/.test(id)) gauges.push(feature);
            // get selected stretch
            if (/^stretches/.test(id)) stretches.push(feature);
            // get selected section
            if (/^sections/.test(id)) sections.push(feature);
          }

          commit("setIdentifiedFeatures", all);

          // Decide whether we open a related dialog immediately or show the
          // popup with possible options first.
          // The following cases require a manual decision via the popup because
          // the targeted feature is not clear.
          if (
            (bottlenecks.length ||
              gauges.length > 1 ||
              stretches.length > 1 ||
              sections.length > 1 ||
              (sections.length && stretches.length) ||
              (gauges.length && sections.length) ||
              (gauges.length && stretches.length)) &&
            state.mapPopupEnabled
          ) {
            state.mapPopup.setMap(map);
            state.mapPopup.setPosition(event.coordinate);
          }
          // The following scenarios lead to a distinct action without popup.
          if (
            gauges.length === 1 &&
            !bottlenecks.length &&
            !sections.length &&
            !stretches.length
          ) {
            commit("application/showGauges", true, { root: true });
            dispatch("gauges/selectedGaugeISRS", gauges[0].get("isrs_code"), {
              root: true
            });
          }
          if (
            stretches.length === 1 &&
            !sections.length &&
            !bottlenecks.length &&
            !gauges.length
          ) {
            if (rootState.imports.selectedStretchId === stretches[0].getId()) {
              commit("imports/selectedStretchId", null, {
                root: true
              });
            } else {
              commit("imports/selectedStretchId", stretches[0].getId(), {
                root: true
              });
              commit("fairwayavailability/type", "stretches", { root: true });
              commit("application/showFairwayDepth", true, { root: true });
              dispatch("moveToFeauture", { feature: stretches[0], zoom: 17 });
            }
          }
          if (
            sections.length === 1 &&
            !stretches.length &&
            !bottlenecks.length &&
            !gauges.length
          ) {
            if (rootState.imports.selectedSectionId === sections[0].getId()) {
              commit("imports/selectedSectionId", null, {
                root: true
              });
            } else {
              commit("imports/selectedSectionId", sections[0].getId(), {
                root: true
              });
              commit("fairwayavailability/type", "sections", { root: true });
              commit("application/showFairwayDepth", true, { root: true });
              dispatch("moveToFeauture", { feature: sections[0], zoom: 17 });
            }
          }
        }

        // 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));
              }
            }
          }
          */

        let currentResolution = map.getView().getResolution();

        var waterwayAxisSource = map.getLayer("WATERWAYAXIS").getSource();
        var waxisUrl = waterwayAxisSource.getGetFeatureInfoUrl(
          event.coordinate,
          currentResolution /* resolution */,
          "EPSG:3857",
          // { INFO_FORMAT: "application/vnd.ogc.gml" } // not allowed by d4d
          { INFO_FORMAT: "application/json" }
        );

        if (waxisUrl) {
          // cannot directly query here because of SOP
          HTTP.get(waxisUrl, {
            headers: {
              "X-Gemma-Auth": localStorage.getItem("token")
            }
          }).then(response => {
            let features = response.data.features.map(f => {
              let feat = new Feature({
                geometry: new Point(f.geometry.coordinates)
              });
              feat.setId(f.id);
              feat.setProperties(f.properties);
              return feat;
            });
            commit("addIdentifiedFeatures", features);
          });
        }
        var waterwayAreaSource = map.getLayer("WATERWAYAREA").getSource();
        var wareaUrl = waterwayAreaSource.getGetFeatureInfoUrl(
          event.coordinate,
          currentResolution /* resolution */,
          "EPSG:3857",
          // { INFO_FORMAT: "application/vnd.ogc.gml" } // not allowed by d4d
          { INFO_FORMAT: "application/json" }
        );

        if (wareaUrl) {
          HTTP.get(wareaUrl, {
            headers: {
              "X-Gemma-Auth": localStorage.getItem("token")
            }
          }).then(response => {
            let features = response.data.features.map(f => {
              let feat = new Feature({
                geometry: new Point(f.geometry.coordinates)
              });
              feat.setId(f.id);
              feat.setProperties(f.properties);
              return feat;
            });
            commit("addIdentifiedFeatures", features);
          });
        }
        var dmSource = map.getLayer("DISTANCEMARKS").getSource();
        var dmUrl = dmSource.getGetFeatureInfoUrl(
          event.coordinate,
          currentResolution /* resolution */,
          "EPSG:3857",
          // { INFO_FORMAT: "application/vnd.ogc.gml" } // not allowed by d4d
          { INFO_FORMAT: "application/json" }
        );

        if (dmUrl) {
          HTTP.get(dmUrl + "&BUFFER=5", {
            headers: {
              "X-Gemma-Auth": localStorage.getItem("token")
            }
          }).then(response => {
            let features = response.data.features.map(f => {
              let feat = new Feature({
                geometry: new Point(f.geometry.coordinates)
              });
              feat.setId(f.id);
              feat.setProperties(f.properties);
              return feat;
            });
            commit("addIdentifiedFeatures", features);
          });
        }
        var dmaSource = map.getLayer("DISTANCEMARKSAXIS").getSource();
        var dmaUrl = dmaSource.getGetFeatureInfoUrl(
          event.coordinate,
          currentResolution /* resolution */,
          "EPSG:3857",
          // { INFO_FORMAT: "application/vnd.ogc.gml" } // not allowed by d4d
          { INFO_FORMAT: "application/json" }
        );

        if (dmaUrl) {
          HTTP.get(dmaUrl + "&BUFFER=5", {
            headers: {
              "X-Gemma-Auth": localStorage.getItem("token")
            }
          }).then(response => {
            let features = response.data.features.map(f => {
              let feat = new Feature({
                geometry: new Point(f.geometry.coordinates)
              });
              feat.setId(f.id);
              feat.setProperties(f.properties);
              return feat;
            });
            commit("addIdentifiedFeatures", features);
          });
        }
        // trying the GetFeatureInfo way for WMS
        var iecdisSource = map.getLayer("INLANDECDIS").getSource();
        var iecdisUrl = iecdisSource.getGetFeatureInfoUrl(
          event.coordinate,
          currentResolution /* resolution */,
          "EPSG:3857",
          // { INFO_FORMAT: "application/vnd.ogc.gml" } // not allowed by d4d
          { INFO_FORMAT: "text/plain" }
        );

        if (iecdisUrl) {
          // cannot directly query here because of SOP
          console.log("GetFeatureInfo url:", iecdisUrl);
        }
      });
    },
    refreshLayers({ state }) {
      state.openLayersMaps.forEach(map => {
        let layers = map.getLayers().getArray();
        for (let i = 0; i < layers.length; i++) {
          let layer = layers[i];
          if (
            layer instanceof VectorLayer &&
            layer.get("source").loader_.name != "VOID"
          ) {
            layer.getSource().clear(true);
            layer.getSource().refresh({ force: true });
          }
        }
      });
    },
    moveToBoundingBox(
      { state },
      { boundingBox, zoom, preventZoomOut, duration }
    ) {
      const extent = transformExtent(boundingBox, "EPSG:4326", "EPSG:3857");
      const currentZoom = state.syncedView.getZoom();
      zoom = zoom || currentZoom;
      state.syncedView.fit(extent, {
        maxZoom: preventZoomOut ? Math.max(zoom, currentZoom) : zoom,
        duration: duration || 700
      });
    },
    moveToFeauture({ dispatch }, { feature, zoom, preventZoomOut }) {
      const boundingBox = feature.hasOwnProperty("geometry")
        ? bbox(feature.geometry)
        : feature
            .getGeometry()
            .clone()
            .transform("EPSG:3857", "EPSG:4326")
            .getExtent();
      dispatch("moveToBoundingBox", { boundingBox, zoom, preventZoomOut });
    },
    moveMap({ state }, { coordinates, zoom, preventZoomOut }) {
      const currentZoom = state.syncedView.getZoom();
      zoom = zoom || currentZoom;
      state.syncedView.animate({
        zoom: preventZoomOut ? Math.max(zoom, currentZoom) : zoom,
        center: fromLonLat(coordinates, state.syncedView.getProjection()),
        duration: 700
      });
    }
  }
};