view client/src/store/map.js @ 3207:ba7bc3740fb3

client: renamed store modules to better reflect their context
author Markus Kottlaender <markus@intevation.de>
date Wed, 08 May 2019 17:43:18 +0200
parents dffcf1cc8a8b
children 48801e13ef18
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 { toLonLat } from "ol/proj";

// initial state
const init = () => {
  return {
    openLayersMaps: [],
    syncedMaps: [],
    syncedView: null,
    mapPopup: null,
    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;
    },
    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
      });
      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
      });
      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,
        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);
        if (
          state.lineToolEnabled ||
          state.polygonToolEnabled ||
          state.cutToolEnabled
        )
          return;
        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.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("application/showStatistics", true, { root: true });
              commit("fairwayavailability/type", "stretches", { root: true });
              commit("imports/selectedStretchId", stretches[0].getId(), {
                root: true
              });
              dispatch("moveToFeauture", { feature: stretches[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));
              }
            }
          }
          */

        var waterwayAxisSource = map.getLayer("WATERWAYAXIS").getSource();
        var waxisUrl = waterwayAxisSource.getGetFeatureInfoUrl(
          event.coordinate,
          100 /* 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,
          100 /* 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,
          100 /* 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,
          100 /* 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,
          100 /* 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);
        }
      });
    },
    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
      });
    }
  }
};