view client/src/components/map/layers.js @ 3547:47c61ea894b1

client: map layers: share same vector source object for layers that use the same geoserver view (bottlenecks_geoserver) The layers Bottlenecks, Critical Bottlenecks and Bottlenecks Fairway Availability used the exact same geoserver_view/data. Thus the vector source object can be shared to avoid multiple identical requests. Unfortunately this does not work across maps. So each map has it's own vector source object for the three bottleneck layers.
author Markus Kottlaender <markus@intevation.de>
date Fri, 31 May 2019 12:20:08 +0200
parents a606d003730c
children f3102fa16a69
line wrap: on
line source

import TileWMS from "ol/source/TileWMS";
import {
  Tile as TileLayer,
  Vector as VectorLayer,
  Image as ImageLayer
} from "ol/layer";
import { Icon, Stroke, Style } from "ol/style";
import VectorSource from "ol/source/Vector";
import { ImageWMS as ImageSource } from "ol/source";
import Point from "ol/geom/Point";
import { bbox as bboxStrategy } from "ol/loadingstrategy";
import { WFS, GeoJSON } from "ol/format";
import OSM from "ol/source/OSM";
import { equalTo } from "ol/format/filter";
import { HTTP } from "@/lib/http";
import styles from "./styles";
import store from "@/store/index";

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);
      });
  };
};

// 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: "#333333",
          width: 2,
          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;
  }
});

export default function() {
  // Shared feature source for layers:
  // BOTTLENECKS, BOTTLENECKSTATUS and BOTTLENECKFAIRWAYAVAILABILITY
  // Reduces bottlenecks_geoserver requests and number of stored feature objects.
  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;
      }
    )
  );

  return {
    get(id) {
      return this.config.find(l => l.get("id") === id);
    },
    config: [
      new TileLayer({
        id: "OPENSTREETMAP",
        label: "Open Streetmap",
        visible: true,
        source: new OSM()
      }),
      new ImageLayer({
        id: "INLANDECDIS",
        label: "Inland ECDIS chart Danube",
        visible: true,
        source: new ImageSource({
          preload: 1,
          url: "https://service.d4d-portal.info/wms/",
          crossOrigin: "anonymous",
          params: { LAYERS: "d4d", VERSION: "1.1.1", TILED: true }
        })
      }),
      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() {
        const source = new VectorSource();
        source.setLoader(
          buildVectorLoader(
            {
              featureTypes: ["fairway_dimensions"],
              filter: equalTo("level_of_service", 1)
            },
            source,
            true
          )
        );
        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();
        source.setLoader(
          buildVectorLoader(
            {
              featureTypes: ["fairway_dimensions"],
              filter: equalTo("level_of_service", 2)
            },
            source,
            true
          )
        );
        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();
        source.setLoader(
          buildVectorLoader(
            {
              featureTypes: ["fairway_dimensions"],
              filter: equalTo("level_of_service", 3)
            },
            source,
            true
          )
        );
        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 },
          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: ["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: "BOTTLENECKS",
          label: "Bottlenecks",
          visible: true,
          style: styles.bottleneck,
          source: bottlenecksSource
        });
      })(),
      new TileLayer({
        id: "BOTTLENECKISOLINE",
        label: "Bottleneck isolines",
        visible: false,
        source: new TileWMS({
          preload: 0,
          projection: "EPSG:3857",
          url: window.location.origin + "/api/internal/wms",
          params: {
            LAYERS: "sounding_results_contour_lines_geoserver",
            VERSION: "1.1.1",
            TILED: true
          },
          tileLoadFunction: function(tile, src) {
            // console.log("calling for", 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);
        })
      }),
      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
          },
          tileLoadFunction: function(tile, src) {
            // console.log("calling for", 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() {
        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: "Bottlenecks 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) {
                // look for nashSutcliffeOverview in store. If present and
                // not older than 15 min use it or fetch new data and store it.
                let data = store.getters["gauges/nashSutcliffeOverview"](f);
                if (
                  data &&
                  new Date().getTime() - data.createdAt.getTime() < 900000
                ) {
                  f.set("nsc_data", data.data);
                } else {
                  data = await store.dispatch(
                    "gauges/loadNashSutcliffeForOverview",
                    f.get("isrs_code")
                  );
                  if (data) {
                    store.commit("gauges/addNashSutcliffeOverviewEntry", {
                      feature: f,
                      data,
                      createdAt: new Date()
                    });
                    f.set("nsc_data", data);
                  }
                }
              }
            }
          )
        );
        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);
            });
          } // 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
        });
      })(),
      DRAWLAYER,
      CUTLAYER
    ]
  };
}