view client/src/components/layers/layers.js @ 5087:d77dd0220780 time-sliding

client: Timebased request for waterway_axis
author Fadi Abbud <fadi.abbud@intevation.de>
date Fri, 20 Mar 2020 15:27:25 +0100
parents fe63733750d7
children 8a7c39804d2f
line wrap: on
line source

import { GeoJSON, WFS } from "ol/format";
import { Icon, Stroke, Style } from "ol/style";
import {
  Image as ImageLayer,
  Tile as TileLayer,
  Vector as VectorLayer
} from "ol/layer";
import { and as andFilter, equalTo } from "ol/format/filter";

import { HTTP } from "@/lib/http";
import { ImageWMS as ImageSource } from "ol/source";
import OSM from "ol/source/OSM";
import Point from "ol/geom/Point";
import TileWMS from "ol/source/TileWMS";
import VectorSource from "ol/source/Vector";
import { bbox as bboxStrategy } from "ol/loadingstrategy";
import store from "@/store/index";
import { styleFactory } from "./styles";

export const buildVectorLoader = (
  featureRequestOptions,
  vectorSource,
  bboxStrategyDisabled,
  featurePostProcessor
) => {
  // 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: the geometryName has to be given in featureRequestOptions if
  // bboxStrategy (default) is used
  featureRequestOptions.featureNS = "gemma";
  featureRequestOptions.featurePrefix = "gemma";
  featureRequestOptions.outputFormat = "application/json";
  return (extent, resolution, projection) => {
    if (!bboxStrategyDisabled) {
      featureRequestOptions.bbox = extent;
    }
    featureRequestOptions.srsName = projection.getCode();
    HTTP.post(
      "/internal/wfs",
      new XMLSerializer().serializeToString(
        new WFS().writeGetFeature(featureRequestOptions)
      ),
      {
        headers: {
          "X-Gemma-Auth": localStorage.getItem("token"),
          "Content-type": "text/xml; charset=UTF-8"
        }
      }
    )
      .then(response => {
        const features = new GeoJSON().readFeatures(
          JSON.stringify(response.data)
        );
        if (featurePostProcessor) {
          features.map(f => featurePostProcessor(f, store, features));
        }
        vectorSource.addFeatures(features);
      })
      .catch(() => {
        vectorSource.removeLoadedExtent(extent);
        store.dispatch("application/reportBackendError");
      });
  };
};

// SHARED LAYERS:
// DRAW- and CUTLAYER are shared across maps. E.g. you want to see the cross cut
// arrow on both maps when comparing surveys. So we don't need to initialize a
// new VectorLayer object for each map. Instead we use these two constants so
// that all maps use the same object.
const DRAWLAYER = new VectorLayer({
  id: "DRAWTOOL",
  label: "Draw Tool",
  visible: true,
  source: new VectorSource({ wrapX: false }),
  style: function(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("@/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;
  }
});

const CUTLAYER = new VectorLayer({
  id: "CUTTOOL",
  label: "Cut Tool",
  visible: true,
  source: new VectorSource({ wrapX: false }),
  style: function(feature) {
    // adapted from OpenLayer's LineString Arrow Example
    var geometry = feature.getGeometry();
    var styles = [
      // linestring
      new Style({
        stroke: new Stroke({
          color: "#FFFFFF",
          width: 5,
          lineDash: [7, 7]
        })
      }),
      new Style({
        stroke: new Stroke({
          color: "#333333",
          width: 3,
          lineDash: [7, 7]
        })
      })
    ];

    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("@/assets/linestring_arrow_grey.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;
  }
});

let layerConfigs = {};

export const unsetLayerConfigs = function() {
  layerConfigs = {};
};

export const layerFactory = function(mapId) {
  const styles = styleFactory(mapId);
  // Shared feature source for layers:
  // BOTTLENECKS, BOTTLENECKSTATUS and BOTTLENECKFAIRWAYAVAILABILITY
  // Reduces bottlenecks_geoserver requests and number of stored feature objects.
  const FDREVIEWLAYER = new VectorLayer({
    id: "FDREVIEWLAYER",
    label: "Review",
    visible: true,
    source: new VectorSource({ wrapX: false }),
    style: styles.sections
  });
  const bottlenecksSource = new VectorSource({ strategy: bboxStrategy });
  bottlenecksSource.setLoader(
    buildVectorLoader(
      {
        featureTypes: ["bottlenecks_geoserver"],
        geometryName: "area"
      },
      bottlenecksSource,
      false,
      async (f, store) => {
        if (f.get("fa_critical")) {
          // look for fairway availability data in store. If present and
          // not older than 15 min use it or fetch new data and store it.
          let data = store.getters["fairwayavailability/fwLNWLOverviewData"](f);
          if (
            data &&
            new Date().getTime() - data.createdAt.getTime() < 900000
          ) {
            f.set("fa_data", data.data);
          } else {
            let date = new Date();
            data = await store.dispatch(
              "fairwayavailability/loadAvailableFairwayDepthLNWLForMap",
              {
                feature: f,
                from: date.toISOString().split("T")[0],
                to: date.toISOString().split("T")[0],
                frequency: "monthly",
                LOS: 3
              }
            );
            if (data) {
              store.commit("fairwayavailability/addFwLNWLOverviewData", {
                feature: f,
                data,
                createdAt: new Date()
              });
              f.set("fa_data", data);
            }
          }
        }
        return f;
      }
    )
  );

  // either use existing config or create new one
  // important is only each map has its individual layer config
  // but we don't want to create new layer objects each time a store value
  // that is used here changes.
  const config = layerConfigs.hasOwnProperty(mapId)
    ? layerConfigs[mapId]
    : [
        new TileLayer({
          id: "OPENSTREETMAP",
          label: "Open Streetmap",
          visible: true,
          source: new OSM()
        }),
        new ImageLayer({
          id: "INLANDECDIS",
          label: "Inland ECDIS chart Danube",
          visible: true,
          source: null
        }),
        new ImageLayer({
          id: "WATERWAYAREA",
          label: "Waterway Area",
          maxResolution: 100,
          minResolution: 0,
          source: new ImageSource({
            url: window.location.origin + "/api/internal/wms",
            params: {
              LAYERS: "waterway_area",
              VERSION: "1.1.1",
              TILED: true
            },
            imageLoadFunction: function(tile, src) {
              HTTP.get(src, {
                headers: {
                  "X-Gemma-Auth": localStorage.getItem("token")
                },
                responseType: "blob"
              }).then(response => {
                tile.getImage().src = URL.createObjectURL(response.data);
              });
            } // TODO  tile.setState(TileState.ERROR);
          })
        }),
        (function() {
          const source = new VectorSource({ strategy: bboxStrategy });
          source.setLoader(
            buildVectorLoader(
              {
                featureTypes: ["stretches_geoserver"],
                geometryName: "area"
              },
              source,
              true,
              (f, store) => {
                if (f.getId() === store.state.imports.selectedStretchId) {
                  f.set("highlighted", true);
                }
                return f;
              }
            )
          );
          return new VectorLayer({
            id: "STRETCHES",
            label: "Stretches",
            visible: false,
            style: styles.stretches,
            source
          });
        })(),
        (function() {
          const source = new VectorSource({ strategy: bboxStrategy });
          source.setLoader(
            buildVectorLoader(
              {
                featureTypes: ["sections_geoserver"],
                geometryName: "area"
              },
              source,
              true,
              (f, store) => {
                if (f.getId() === store.state.imports.selectedSectionId) {
                  f.set("highlighted", true);
                }
                return f;
              }
            )
          );
          return new VectorLayer({
            id: "SECTIONS",
            label: "Sections",
            visible: false,
            style: styles.sections,
            source
          });
        })(),
        (function() {
          return new VectorLayer({
            id: "BOTTLENECKS",
            label: "Bottlenecks",
            visible: true,
            style: styles.bottleneck,
            source: bottlenecksSource
          });
        })(),
        new TileLayer({
          id: "BOTTLENECKISOLINE",
          label: "Bottleneck morphology",
          visible: false,
          source: new TileWMS({
            preload: 0,
            projection: "EPSG:3857",
            url: window.location.origin + "/api/internal/wms",
            params: {
              LAYERS: "sounding_results_areas_geoserver",
              VERSION: "1.1.1",
              TILED: true
            },
            tileLoadFunction: function(tile, src) {
              HTTP.get(src, {
                headers: {
                  "X-Gemma-Auth": localStorage.getItem("token")
                },
                responseType: "blob"
              })
                .then(response => {
                  tile.getImage().src = URL.createObjectURL(response.data);
                })
                .catch(() => {
                  store.dispatch("application/reportBackendError");
                });
            } // TODO  tile.setState(TileState.ERROR);
          })
        }),
        new TileLayer({
          id: "DIFFERENCES",
          label: "Bottleneck Differences",
          visible: false,
          source: new TileWMS({
            preload: 0,
            projection: "EPSG:3857",
            url: window.location.origin + "/api/internal/wms",
            params: {
              LAYERS: "sounding_differences",
              VERSION: "1.1.1",
              TILED: true,
              CQL_FILTER: "id=" + store.state.fairwayprofile.currentDifference
            },
            tileLoadFunction: function(tile, src) {
              HTTP.get(src, {
                headers: {
                  "X-Gemma-Auth": localStorage.getItem("token")
                },
                responseType: "blob"
              })
                .then(response => {
                  tile.getImage().src = URL.createObjectURL(response.data);
                })
                .catch(() => {
                  store.dispatch("application/reportBackendError");
                });
            } // TODO  tile.setState(TileState.ERROR);
          })
        }),
        (function() {
          const source = new VectorSource({ strategy: bboxStrategy });
          source.setLoader(
            buildVectorLoader(
              {
                geometryName: "area",
                featureTypes: ["fairway_dimensions"],
                filter: andFilter(
                  equalTo("level_of_service", 1),
                  equalTo("staging_done", true)
                )
              },
              source,
              false
            )
          );
          return new VectorLayer({
            id: "FAIRWAYDIMENSIONSLOS1",
            label: "LOS 1 Fairway Dimensions",
            visible: false,
            style: styles.fwd1,
            maxResolution: 80,
            minResolution: 0,
            source
          });
        })(),
        (function() {
          const source = new VectorSource({ strategy: bboxStrategy });
          source.setLoader(
            buildVectorLoader(
              {
                geometryName: "area",
                featureTypes: ["fairway_dimensions"],
                filter: andFilter(
                  equalTo("level_of_service", 2),
                  equalTo("staging_done", true)
                )
              },
              source,
              false
            )
          );
          return new VectorLayer({
            id: "FAIRWAYDIMENSIONSLOS2",
            label: "LOS 2 Fairway Dimensions",
            visible: false,
            style: styles.fwd2,
            maxResolution: 80,
            minResolution: 0,
            source
          });
        })(),
        (function() {
          const source = new VectorSource({ strategy: bboxStrategy });
          source.setLoader(
            buildVectorLoader(
              {
                geometryName: "area",
                featureTypes: ["fairway_dimensions"],
                filter: andFilter(
                  equalTo("level_of_service", 3),
                  equalTo("staging_done", true)
                )
              },
              source,
              false
            )
          );
          return new VectorLayer({
            id: "FAIRWAYDIMENSIONSLOS3",
            label: "LOS 3 Fairway Dimensions",
            visible: true,
            style: styles.fwd3,
            maxResolution: 80,
            minResolution: 0,
            source
          });
        })(),
        new ImageLayer({
          id: "WATERWAYAXIS",
          label: "Waterway Axis",
          source: new ImageSource({
            url: window.location.origin + "/api/internal/wms",
            params: {
              LAYERS: "waterway_axis",
              VERSION: "1.1.1",
              TILED: true,
              TIME: store.state.application.refreshLayersTime.toISOString()
            },
            imageLoadFunction: function(tile, src) {
              HTTP.get(src, {
                headers: {
                  "X-Gemma-Auth": localStorage.getItem("token")
                },
                responseType: "blob"
              })
                .then(response => {
                  tile.getImage().src = URL.createObjectURL(response.data);
                })
                .catch(() => {
                  store.dispatch("application/reportBackendError");
                });
            } // TODO  tile.setState(TileState.ERROR);
          })
        }),
        (function() {
          const source = new VectorSource({ strategy: bboxStrategy });
          source.setLoader(
            buildVectorLoader(
              {
                featureTypes: ["waterway_profiles"],
                geometryName: "geom"
              },
              source
            )
          );
          return new VectorLayer({
            id: "WATERWAYPROFILES",
            label: "Waterway Profiles",
            visible: true,
            style: new Style({
              stroke: new Stroke({
                color: "rgba(0, 0, 255, .5)",
                lineDash: [5, 5],
                width: 2
              })
            }),
            maxResolution: 2.5,
            minResolution: 0,
            source
          });
        })(),
        (function() {
          return new VectorLayer({
            id: "BOTTLENECKSTATUS",
            label: "Critical Bottlenecks",
            forLegendStyle: { point: true, resolution: 16 },
            visible: true,
            zIndex: 1,
            style: styles.bottleneckStatus,
            source: bottlenecksSource
          });
        })(),
        (function() {
          return new VectorLayer({
            id: "BOTTLENECKFAIRWAYAVAILABILITY",
            label: "Bottleneck Fairway Availability",
            forLegendStyle: { point: true, resolution: 16 },
            visible: false,
            zIndex: 1,
            style: styles.bottleneckFairwayAvailability,
            source: bottlenecksSource
          });
        })(),
        (function() {
          const source = new VectorSource({ strategy: bboxStrategy });
          source.setLoader(
            buildVectorLoader(
              {
                featureTypes: [
                  "bottlenecks_geoserver",
                  "gauges_geoserver",
                  "stretches_geoserver",
                  "sections_geoserver"
                ]
              },
              source,
              true,
              // since we don't use bbox strategy, features will contain all features and we can use it
              // to find reference gauges for bottlenecks, yeah!
              async (f, store, features) => {
                // attach reference gauge to bottleneck
                if (f.getId().indexOf("bottlenecks") > -1) {
                  f.set(
                    "gauge_obj",
                    features.find(feat => {
                      return (
                        feat.getId().indexOf("gauges") > -1 &&
                        feat.get("objname") === f.get("gauge_objname")
                      );
                    })
                  );
                }

                // attach nsc data to gauge
                if (f.getId().indexOf("gauges") > -1) {
                  store
                    .dispatch(
                      "gauges/getNashSutcliffeForISRS",
                      f.get("isrs_code")
                    )
                    .then(response => {
                      f.set("nsc_data", response);
                    });
                }
              }
            )
          );
          return new VectorLayer({
            id: "DATAAVAILABILITY",
            label: "Data Availability/Accuracy",
            forLegendStyle: { point: true, resolution: 16 },
            visible: false,
            zIndex: 1,
            style: styles.dataAvailability,
            source
          });
        })(),
        new ImageLayer({
          id: "DISTANCEMARKS",
          label: "Distance Marks",
          maxResolution: 10,
          minResolution: 0,
          source: new ImageSource({
            url: window.location.origin + "/api/internal/wms",
            params: {
              LAYERS: "distance_marks_ashore_geoserver",
              VERSION: "1.1.1",
              TILED: true
            },
            imageLoadFunction: function(tile, src) {
              HTTP.get(src, {
                headers: {
                  "X-Gemma-Auth": localStorage.getItem("token")
                },
                responseType: "blob"
              })
                .then(response => {
                  tile.getImage().src = URL.createObjectURL(response.data);
                })
                .catch(() => {
                  store.dispatch("application/reportBackendError");
                });
            } // TODO  tile.setState(TileState.ERROR);
          })
        }),
        new ImageLayer({
          id: "DISTANCEMARKSAXIS",
          label: "Distance Marks, Axis",
          source: new ImageSource({
            url: window.location.origin + "/api/internal/wms",
            params: {
              LAYERS: "distance_marks_geoserver",
              VERSION: "1.1.1",
              TILED: true
            },
            imageLoadFunction: function(tile, src) {
              HTTP.get(src, {
                headers: {
                  "X-Gemma-Auth": localStorage.getItem("token")
                },
                responseType: "blob"
              }).then(response => {
                tile.getImage().src = URL.createObjectURL(response.data);
              });
            } // TODO  tile.setState(TileState.ERROR);
          })
        }),
        (function() {
          const source = new VectorSource({ strategy: bboxStrategy });
          source.setLoader(
            buildVectorLoader(
              {
                featureTypes: ["gauges_geoserver"],
                geometryName: "geom"
              },
              source
            )
          );
          return new VectorLayer({
            id: "GAUGES",
            label: "Gauges",
            forLegendStyle: { point: true, resolution: 8 },
            visible: true,
            style: styles.gauge,
            maxResolution: 100,
            minResolution: 0,
            source
          });
        })(),
        new TileLayer({
          id: "FAIRWAYMARKS",
          label: "Fairway marks",
          visible: true,
          source: new TileWMS({
            preload: 0,
            projection: "EPSG:3857",
            url: window.location.origin + "/api/internal/wms",
            params: {
              LAYERS: "fairway_marks",
              VERSION: "1.1.1",
              TILED: true,
              TIME: store.state.application.refreshLayersTime.toISOString()
            },
            tileLoadFunction: function(tile, src) {
              HTTP.get(src, {
                headers: {
                  "X-Gemma-Auth": localStorage.getItem("token")
                },
                responseType: "blob"
              })
                .then(response => {
                  tile.getImage().src = URL.createObjectURL(response.data);
                })
                .catch(() => {
                  store.dispatch("application/reportBackendError");
                });
            } // TODO  tile.setState(TileState.ERROR);
          })
        }),
        DRAWLAYER,
        CUTLAYER,
        FDREVIEWLAYER
      ];

  layerConfigs[mapId] = config;

  return {
    get(id) {
      return config.find(l => l.get("id") === id);
    },
    config
  };
};