view client/src/map/Maplayer.vue @ 1022:74229d9f7028

refac: restructure maptool for understandability
author Thomas Junk <thomas.junk@intevation.de>
date Wed, 24 Oct 2018 12:03:51 +0200
parents e89be4af3a9f
children a55f20dc8d8d
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>
 */
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 } from "ol/sphere.js";
import { Icon, Stroke, Style, Fill } from "ol/style.js";
import { generateFeatureRequest } from "../application/lib/geo.js";

import distance from "@turf/distance";
import {
  lineString as turfLineString,
  polygon as turfPolygon
} from "@turf/helpers";
import lineIntersect from "@turf/line-intersect";
import { displayError } from "../application/lib/errors.js";

const DEMODATA = 2.5;

/* for the sake of debugging */
/* eslint-disable no-console */
export default {
  name: "maplayer",
  props: ["drawMode", "lat", "long", "zoom", "split"],
  data() {
    return {
      projection: "EPSG:3857",
      interaction: null,
      vectorLayer: null,
      vectorSource: null
    };
  },
  computed: {
    ...mapGetters("mapstore", ["layers", "getLayerByName"]),
    ...mapState("mapstore", ["openLayersMap"]),
    ...mapState("morphstore", ["selectedMorph"]),
    mapStyle() {
      return {
        mapfull: !this.split,
        mapsplit: this.split
      };
    },
    layerData() {
      const l = this.layers.map(x => {
        return x.data;
      });
      return [...l, this.vectorLayer];
    }
  },
  methods: {
    createVectorSource() {
      this.vectorSource = new VectorSource({ wrapX: false });
    },
    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
          })
        })
      ];

      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;
    },
    createVectorLayer() {
      this.vectorLayer = new VectorLayer({
        source: this.vectorSource,
        style: this.drawStyleFunction
      });
    },
    removeCurrentInteraction() {
      this.openLayersMap.removeInteraction(this.interaction);
      this.interaction = null;
    },
    createInteraction() {
      this.vectorSource.clear();
      var draw = new Draw({
        source: this.vectorSource,
        type: this.drawMode,
        maxPoints: 2
      });
      draw.on("drawstart", event => {
        this.vectorSource.clear();
        this.$store.commit("identifystore/setCurrentMeasurement", null);
        event.feature.setId("drawn.1"); // unique id for new feature
      });
      draw.on("drawend", this.drawEnd);
      return draw;
    },
    drawEnd(event) {
      const length = getLength(event.feature.getGeometry());
      this.$store.commit("identifystore/setCurrentMeasurement", length);
      // also place the a rounded length in a property, so identify can show it
      event.feature.set("length", 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.selectedMorph) {
        console.log("requesting profile for", this.selectedMorph);
        const inputLineString = event.feature.getGeometry().clone();
        inputLineString.transform("EPSG:3857", "EPSG:4326");
        const [start, end] = inputLineString.getCoordinates();
        this.$store.commit("fairwayprofile/setStartPoint", start);
        this.$store.commit("fairwayprofile/setEndPoint", end);
        this.requestProfile(start, end, this.selectedMorph);
      }
    },
    requestProfile(start, end, survey) {
      // survey has to have the properties bottleneck_id and date_info
      // prepare to send the first line seqment to the server as GeoJSON
      const profileLine = new LineString([start, end]);
      const geoJSON = generateFeatureRequest(profileLine, survey);
      HTTP.post("/cross", geoJSON, {
        headers: { "X-Gemma-Auth": localStorage.getItem("token") }
      })
        .then(response => {
          this.$store.commit("fairwayprofile/profileLoaded", response);
        })
        .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 = this.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
      );
    },
    calculateFairwayCoordinates(profileLine, fairwayGeometry, depth) {
      // both geometries have to be in EPSG:4326
      // uses turfjs distance() function
      let fairwayCoordinates = [];
      var line = turfLineString(profileLine.getCoordinates());
      var polygon = turfPolygon(fairwayGeometry.getCoordinates());
      var intersects = lineIntersect(line, polygon);
      var l = intersects.features.length;
      if (l % 2 != 0) {
        console.log("Ignoring fairway because profile only intersects once.");
      } else {
        for (let i = 0; i < l; i += 2) {
          let pStartPoint = profileLine.getCoordinates()[0];
          let fStartPoint = intersects.features[i].geometry.coordinates;
          let fEndPoint = intersects.features[i + 1].geometry.coordinates;
          let opts = { units: "kilometers" };

          fairwayCoordinates.push([
            distance(pStartPoint, fStartPoint, opts) * 1000,
            distance(pStartPoint, fEndPoint, opts) * 1000,
            depth
          ]);
        }
      }
      return fairwayCoordinates;
    },
    activateInteraction() {
      const interaction = this.createInteraction(this.drawMode);
      this.interaction = interaction;
      this.openLayersMap.addInteraction(interaction);
    },
    activateIdentifyMode() {
      this.openLayersMap.on("singleclick", event => {
        // console.log("single click on map:", event);
        this.identify(event.coordinate, event.pixel);
      });
    },
    identify(coordinate, pixel) {
      this.$store.commit("identifystore/setIdentifiedFeatures", []);
      // checking our WFS layers
      var features = this.openLayersMap.getFeaturesAtPixel(pixel);
      this.$store.commit("identifystore/setIdentifiedFeatures", features);

      // 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() {
      if (this.interaction) {
        this.removeCurrentInteraction();
      } else {
        this.activateInteraction();
      }
    },
    split() {
      const map = this.openLayersMap;
      this.$nextTick(() => {
        map.updateSize();
      });
    },
    selectedMorph(newSelectedMorph) {
      if (newSelectedMorph) {
        this.updateBottleneckFilter(
          newSelectedMorph.bottleneck_id,
          newSelectedMorph.date_info
        );
      } else {
        this.updateBottleneckFilter("does_not_exist", "1999-10-01");
      }
    }
  },
  mounted() {
    this.createVectorSource();
    this.createVectorLayer();
    let map = new Map({
      layers: this.layerData,
      target: "map",
      controls: [],
      view: new View({
        center: [this.long, this.lat],
        zoom: this.zoom,
        projection: this.projection
      })
    });
    this.$store.commit("mapstore/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 featureRequest2 = 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(featureRequest2),
      {
        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.activateIdentifyMode();
  }
};
</script>