view client/src/map/Maplayer.vue @ 942:912d016275ee

client: add arrow to drawn linesegment * Add styling function that will place an icon png image at the end of each drawn line segment, in the right rotation. Note that this does not look perfectly centered, see comment in the code.
author Bernhard Reiter <bernhard@intevation.de>
date Tue, 09 Oct 2018 18:39:01 +0200
parents fe923c8ef08f
children c2b03f4755b6
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>
import { HTTP } from "../application/lib/http";
import { mapGetters, mapState } from "vuex";
import "ol/ol.css";
import { Map, View } from "ol";
import Feature from "ol/Feature";
// import { bbox as bboxFilter } from "ol/format/filter.js";
import { WFS, GeoJSON } from "ol/format.js";
// import GeometryType from "ol/geom/GeometryType.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 distance from "@turf/distance";
import {
  lineString as turfLineString,
  polygon as turfPolygon
} from "@turf/helpers";
//import { lineIntersect as turfLineIntersect } from "@turf/line-intersect";
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", "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("mapstore/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("mapstore/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);
        this.requestProfile(event, this.selectedMorph);
      }
    },
    requestProfile(event, 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 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);
      const profileLine = new LineString([start, end]);
      const feature = new Feature({
        geometry: profileLine,
        bottleneck: survey.bottleneck_id,
        date: survey.date_info
      });
      const geoJSON = new GeoJSON({ geometryName: "geometry" }).writeFeature(
        feature
      );
      this.$store
        .dispatch("fairwayprofile/loadProfile", geoJSON)
        .then(() => {
          var vectorSource = this.getLayerByName(
            "Fairway Dimensions"
          ).data.getSource();
          vectorSource.forEachFeatureIntersectingExtent(
            // need to use EPSG:3857 which is the proj of vectorSource
            profileLine
              .clone()
              .transform("EPSG:4326", "EPSG:3857")
              .getExtent(),
            feature => {
              // transform back to prepare for usage
              var intersectingPolygon = feature
                .getGeometry()
                .clone()
                .transform("EPSG:3857", "EPSG:4326");
              this.addToFairwayRectangle(
                profileLine,
                intersectingPolygon,
                DEMODATA
              );
            }
          );
          this.$store.commit("application/openSplitScreen");
        })
        .catch(error => {
          const { status, data } = error.response;
          displayError({
            title: "Backend Error",
            message: `${status}: ${data.message || data}`
          });
        });
    },
    addToFairwayRectangle(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
          ]);
        }
      }
      this.$store.commit(
        "fairwayprofile/setFairwayCoordinates",
        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("mapstore/setIdentifiedFeatures", []);
      // checking our WFS layers
      var features = this.openLayersMap.getFeaturesAtPixel(pixel);
      this.$store.commit("mapstore/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>