view client/src/map/Maplayer.vue @ 1115:1b160eda22cf store-refactoring

moved drawMode to map store
author Markus Kottlaender <markus@intevation.de>
date Mon, 05 Nov 2018 14:05:01 +0100
parents 595654ad3f66
children 035dc35e1dfc
line wrap: on
line source

<template>
    <div id="map" :class="mapStyle"></div>
</template>

<style lang="scss">
.mapsplit {
  height: 50vh;
}

.mapfull {
  height: 100vh;
}

@media print {
  .mapfull {
    width: 2000px;
    height: 2828px;
  }
  .mapsplit {
    width: 2000px;
    height: 2828px;
  }
}
</style>

<script>
/*
 * This is Free Software under GNU Affero General Public License v >= 3.0
 * without warranty, see README.md and license for details.
 *
 * SPDX-License-Identifier: AGPL-3.0-or-later
 * License-Filename: LICENSES/AGPL-3.0.txt
 *
 * Copyright (C) 2018 by via donau 
 *   – Österreichische Wasserstraßen-Gesellschaft mbH
 * Software engineering by Intevation GmbH
 *
 * Author(s):
 * * Thomas Junk <thomas.junk@intevation.de>
 * * Bernhard E. Reiter <bernhard.reiter@intevation.de>
 */
import { HTTP } from "../application/lib/http";
import { mapGetters, mapState } from "vuex";
import "ol/ol.css";
import { Map, View } from "ol";
import { WFS, GeoJSON } from "ol/format.js";
import LineString from "ol/geom/LineString.js";
import Point from "ol/geom/Point.js";
import Draw from "ol/interaction/Draw.js";
import { Vector as VectorLayer } from "ol/layer.js";
import { Vector as VectorSource } from "ol/source.js";
import { getLength, getArea } from "ol/sphere.js";
import { Icon, Stroke, Style, Fill } from "ol/style.js";

import { displayError } from "../application/lib/errors.js";
import { calculateFairwayCoordinates } from "../application/lib/geo.js";

const DEMODATA = 2.5;

/* for the sake of debugging */
/* eslint-disable no-console */
export default {
  name: "maplayer",
  props: ["lat", "long", "zoom", "split"],
  data() {
    return {
      projection: "EPSG:3857",
      interaction: null,
      vectorLayer: null,
      vectorSource: null
    };
  },
  computed: {
    ...mapGetters("map", ["layers", "getLayerByName"]),
    ...mapState("map", ["openLayersMap", "drawMode"]),
    ...mapState("bottlenecks", ["selectedSurvey"]),
    mapStyle() {
      return {
        mapfull: !this.split,
        mapsplit: this.split
      };
    }
  },
  methods: {
    drawStyleFunction(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("../application/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;
    },
    removeCurrentInteraction() {
      this.$store.commit("map/setCurrentMeasurement", null);
      this.vectorSource.clear();
      this.openLayersMap.removeInteraction(this.interaction);
      this.interaction = null;
    },
    createInteraction(drawMode) {
      this.vectorSource.clear();
      var draw = new Draw({
        source: this.vectorSource,
        type: drawMode,
        maxPoints: drawMode === "LineString" ? 2 : 50
      });
      draw.on("drawstart", () => {
        this.vectorSource.clear();
        this.$store.commit("map/setCurrentMeasurement", null);
        // we are not setting an id here, to avoid the regular identify to
        // pick it up
        // event.feature.setId("drawn.1"); // unique id for new feature
      });
      draw.on("drawend", this.drawEnd);
      return draw;
    },
    drawEnd(event) {
      if (this.drawMode === "Polygon") {
        const areaSize = getArea(event.feature.getGeometry());
        // also place the a rounded areaSize in a property,
        // so identify will show it
        if (areaSize > 100000) {
          this.$store.commit("map/setCurrentMeasurement", {
            quantity: "Area",
            unitSymbol: "km²",
            // convert into 1 km² == 1000*1000 m² and round to 1000 m²
            value: Math.round(areaSize / 1000) / 1000
          });
        } else {
          this.$store.commit("map/setCurrentMeasurement", {
            quantity: "Area",
            unitSymbol: "m²",
            value: Math.round(areaSize)
          });
        }
      }
      if (this.drawMode === "LineString") {
        const length = getLength(event.feature.getGeometry());
        this.$store.commit("map/setCurrentMeasurement", {
          quantity: "Length",
          unitSymbol: "m",
          value: Math.round(length * 10) / 10
        });
      }

      // if a survey has been selected, request a profile
      // TODO an improvement could be to check if the line intersects
      // with the bottleneck area's polygon before trying the server request
      if (this.selectedSurvey) {
        this.$store.commit("fairwayprofile/clearCurrentProfile");
        console.log("requesting profile for", this.selectedSurvey);
        const inputLineString = event.feature.getGeometry().clone();
        inputLineString.transform("EPSG:3857", "EPSG:4326");
        const [start, end] = inputLineString
          .getCoordinates()
          .map(coords => coords.map(coord => parseFloat(coord.toFixed(8))));
        this.$store.commit("fairwayprofile/setStartPoint", start);
        this.$store.commit("fairwayprofile/setEndPoint", end);
        const profileLine = new LineString([start, end]);
        this.$store
          .dispatch("fairwayprofile/loadProfile", this.selectedSurvey)
          .then(() => {
            var vectorSource = this.getLayerByName(
              "Fairway Dimensions"
            ).data.getSource();
            this.calculateIntersection(vectorSource, profileLine);
          })
          .then(() => {
            this.$store.commit("application/openSplitScreen");
          })
          .catch(error => {
            const { status, data } = error.response;
            displayError({
              title: "Backend Error",
              message: `${status}: ${data.message || data}`
            });
          });
      }
    },
    calculateIntersection(vectorSource, profileLine) {
      const transformedLine = profileLine
        .clone()
        .transform("EPSG:4326", "EPSG:3857")
        .getExtent();
      const featureCallback = feature => {
        // transform back to prepare for usage
        var intersectingPolygon = feature
          .getGeometry()
          .clone()
          .transform("EPSG:3857", "EPSG:4326");
        const fairwayCoordinates = calculateFairwayCoordinates(
          profileLine,
          intersectingPolygon,
          DEMODATA
        );
        this.$store.commit(
          "fairwayprofile/setFairwayCoordinates",
          fairwayCoordinates
        );
      };
      vectorSource.forEachFeatureIntersectingExtent(
        // need to use EPSG:3857 which is the proj of vectorSource
        transformedLine,
        featureCallback
      );
    },
    activateInteraction() {
      const interaction = this.createInteraction(this.drawMode);
      this.interaction = interaction;
      this.openLayersMap.addInteraction(interaction);
    },
    identify(coordinate, pixel) {
      this.$store.commit("map/setIdentifiedFeatures", []);
      // checking our WFS layers
      var features = this.openLayersMap.getFeaturesAtPixel(pixel);
      if (features) {
        this.$store.commit("map/setIdentifiedFeatures", features);
        
        // get selected bottleneck from identified features
        for (let feature of features) {
          let id = feature.getId();
          // RegExp.prototype.test() works with number, str and undefined
          if (/^bottlenecks\./.test(id)) {
            this.$store.dispatch("bottlenecks/setSelectedBottleneck", feature.get("objnam"));
          }
        }
      }

      // 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 = this.getLayerByName(
        "Inland ECDIS chart Danube"
      ).data.getSource();
      var url = wmsSource.getGetFeatureInfoUrl(
        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);
      }
    },
    buildVectorLoader(featureRequestOptions, endpoint, vectorSource) {
      // build a function to be used for VectorSource.setLoader()
      // make use of WFS().writeGetFeature to build the request
      // and use our HTTP library to actually do it
      // NOTE: a) the geometryName has to be given in featureRequestOptions,
      //          because we want to load depending on the bbox
      //  b) the VectorSource has to have the option strategy: bbox
      featureRequestOptions["outputFormat"] = "application/json";
      var loader = function(extent, resolution, projection) {
        featureRequestOptions["bbox"] = extent;
        featureRequestOptions["srsName"] = projection.getCode();
        var featureRequest = new WFS().writeGetFeature(featureRequestOptions);
        // DEBUG console.log(featureRequest);
        HTTP.post(
          endpoint,
          new XMLSerializer().serializeToString(featureRequest),
          {
            headers: {
              "X-Gemma-Auth": localStorage.getItem("token"),
              "Content-type": "text/xml; charset=UTF-8"
            }
          }
        )
          .then(response => {
            var features = new GeoJSON().readFeatures(
              JSON.stringify(response.data)
            );
            vectorSource.addFeatures(features);
            // console.log(
            //   "loaded",
            //   features.length,
            //   featureRequestOptions.featureTypes,
            //   "features"
            // );
            // DEBUG console.log("loaded ", features, "for", vectorSource);
            // eslint-disable-next-line
          })
          .catch(() => {
            vectorSource.removeLoadedExtent(extent);
          });
      };
      return loader;
    },
    updateBottleneckFilter(bottleneck_id, datestr) {
      console.log("updating filter with", bottleneck_id, datestr);
      var layer = this.getLayerByName("Bottleneck isolines");
      var wmsSrc = layer.data.getSource();

      if (bottleneck_id != "does_not_exist") {
        wmsSrc.updateParams({
          cql_filter:
            "date_info='" +
            datestr +
            "' AND bottleneck_id='" +
            bottleneck_id +
            "'"
        });
        layer.isVisible = true;
        layer.data.setVisible(true);
      } else {
        layer.isVisible = false;
        layer.data.setVisible(false);
      }
    },
    onBeforePrint(/* evt */) {
      // console.log("onBeforePrint(", evt ,")");
      //
      // the following code shows how to get the current map canvas
      // and change it, however this does not work well enough, as
      // another mechanism seems to update the size again before the rendering
      // for printing is done:
      // console.log(this.openLayersMap.getViewport());
      // var canvas = this.openLayersMap.getViewport().getElementsByTagName("canvas")[0];
      // console.log(canvas);
      // canvas.width=1000;
      // canvas.height=1414;
      //
      // An experiment which also did not work:
      // this.openLayersMap.setSize([1000, 1414]); // estimate portait DIN A4
      //
      // according to documentation
      // http://openlayers.org/en/latest/apidoc/module-ol_PluggableMap-PluggableMap.html#updateSize
      // "Force a recalculation of the map viewport size. This should be called when third-party code changes the size of the map viewport."
      // but did not help
      // this.openLayersMap.updateSize();
    },
    onAfterPrint(/* evt */) {
      // could be used to undo changes that have been done for printing
      // though https://www.tjvantoll.com/2012/06/15/detecting-print-requests-with-javascript/
      // reported that this was not feasable (back then).
      // console.log("onAfterPrint(", evt, ")");
    }
  },
  watch: {
    drawMode(newValue) {
      if (this.interaction) {
        this.removeCurrentInteraction();
      }
      if (newValue) {
        this.activateInteraction();
      }
    },
    split() {
      const map = this.openLayersMap;
      this.$nextTick(() => {
        map.updateSize();
      });
    },
    selectedSurvey(newSelectedSurvey) {
      if (newSelectedSurvey) {
        this.updateBottleneckFilter(
          newSelectedSurvey.bottleneck_id,
          newSelectedSurvey.date_info
        );
      } else {
        this.updateBottleneckFilter("does_not_exist", "1999-10-01");
      }
    }
  },
  mounted() {
    this.vectorSource = new VectorSource({ wrapX: false });
    this.vectorLayer = new VectorLayer({
      source: this.vectorSource,
      style: this.drawStyleFunction
    });
    let map = new Map({
      layers: [...this.layers.map(x => x.data), this.vectorLayer],
      target: "map",
      controls: [],
      view: new View({
        center: [this.long, this.lat],
        zoom: this.zoom,
        projection: this.projection
      })
    });
    this.$store.commit("map/setOpenLayersMap", map);

    // TODO make display of layers more dynamic, e.g. from a list

    // loading the full WFS layer, by not setting the loader function
    // and without bboxStrategy
    var featureRequest = new WFS().writeGetFeature({
      srsName: "EPSG:3857",
      featureNS: "gemma",
      featurePrefix: "gemma",
      featureTypes: ["fairway_dimensions"],
      outputFormat: "application/json"
    });

    // NOTE: loading the full fairway_dimensions makes sure
    //       that all are available for the intersection with the profile
    HTTP.post(
      "/internal/wfs",
      new XMLSerializer().serializeToString(featureRequest),
      {
        headers: {
          "X-Gemma-Auth": localStorage.getItem("token"),
          "Content-type": "text/xml; charset=UTF-8"
        }
      }
    ).then(response => {
      var features = new GeoJSON().readFeatures(JSON.stringify(response.data));
      var vectorSrc = this.getLayerByName(
        "Fairway Dimensions"
      ).data.getSource();
      vectorSrc.addFeatures(features);
      // would scale to the extend of all resulting features
      // this.openLayersMap.getView().fit(vectorSrc.getExtent());
    });

    // load following layers with bboxStrategy (using our request builder)
    var layer = null;

    layer = this.getLayerByName("Waterway Area");
    layer.data.getSource().setLoader(
      this.buildVectorLoader(
        {
          featurePrefix: "ws-wamos",
          featureTypes: ["ienc_wtware"],
          geometryName: "geom"
        },
        "/external/d4d",
        layer.data.getSource()
      )
    );

    layer = this.getLayerByName("Waterway Axis");
    layer.data.getSource().setLoader(
      this.buildVectorLoader(
        {
          featurePrefix: "ws-wamos",
          featureTypes: ["ienc_wtwaxs"],
          geometryName: "geom"
        },
        "/external/d4d",
        layer.data.getSource()
      )
    );

    layer = this.getLayerByName("Distance marks");
    layer.data.getSource().setLoader(
      this.buildVectorLoader(
        {
          featurePrefix: "ws-wamos",
          featureTypes: ["ienc_dismar"],
          geometryName: "geom" //,
          /* restrict loading approximately to extend of danube in Austria */
          // filter: bboxFilter("geom", [13.3, 48.0, 17.1, 48.6], "EPSG:4326")
        },
        "/external/d4d",
        layer.data.getSource()
      )
    );
    layer.data.setVisible(layer.isVisible);

    layer = this.getLayerByName("Distance marks, Axis");
    layer.data.getSource().setLoader(
      this.buildVectorLoader(
        {
          featureNS: "gemma",
          featurePrefix: "gemma",
          featureTypes: ["distance_marks_geoserver"],
          geometryName: "geom"
        },
        "/internal/wfs",
        layer.data.getSource()
      )
    );

    layer = this.getLayerByName("Waterway Area, named");
    layer.data.getSource().setLoader(
      this.buildVectorLoader(
        {
          featureNS: "gemma",
          featurePrefix: "gemma",
          featureTypes: ["hydro_seaare"],
          geometryName: "geom"
        },
        "/external/d4d",
        layer.data.getSource()
      )
    );
    layer.data.setVisible(layer.isVisible);

    layer = this.getLayerByName("Bottlenecks");
    layer.data.getSource().setLoader(
      this.buildVectorLoader(
        {
          featureNS: "gemma",
          featurePrefix: "gemma",
          featureTypes: ["bottlenecks"],
          geometryName: "area"
        },
        "/internal/wfs",
        layer.data.getSource()
      )
    );
    HTTP.get("/system/style/Bottlenecks/stroke", {
      headers: { "X-Gemma-Auth": localStorage.getItem("token") }
    })
      .then(response => {
        this.btlnStrokeC = response.data.code;
        HTTP.get("/system/style/Bottlenecks/fill", {
          headers: { "X-Gemma-Auth": localStorage.getItem("token") }
        })
          .then(response => {
            this.btlnFillC = response.data.code;
            var newstyle = new Style({
              stroke: new Stroke({
                color: this.btlnStrokeC,
                width: 4
              }),
              fill: new Fill({
                color: this.btlnFillC
              })
            });
            layer.data.setStyle(newstyle);
          })
          .catch(error => {
            console.log(error);
          });
      })
      .catch(error => {
        console.log(error);
      });

    window.addEventListener("beforeprint", this.onBeforePrint);
    window.addEventListener("afterprint", this.onAfterPrint);

    // so none is shown
    this.updateBottleneckFilter("does_not_exist", "1999-10-01");
    this.openLayersMap.on(["singleclick", "dblclick"], event => {
      this.identify(event.coordinate, event.pixel);
    });
  }
};
</script>