contrib/OpenLayers/OpenLayersMapView.js

import Marionette from 'backbone.marionette';
import ol from 'openlayers';
import $ from 'jquery';

import { getISODateTimeString } from '../../core/util';

require('openlayers/dist/ol.css');

/**
 * @memberof contrib/OpenLayers
 */

class OpenLayersMapView extends Marionette.ItemView {
  /**
   * Initialize an OpenLayersMapView
   * @param {Object} options - Options to initialize the view with.
   * @param {core/models.FiltersModel} options.filters - the filters model
   */

  initialize(options) {
    this.baseLayersCollection = options.baseLayersCollection;
    this.layersCollection = options.layersCollection;
    this.overlayLayersCollection = options.overlayLayersCollection;

    this.mapModel = options.mapModel;
    this.filtersModel = options.filtersModel;

    this.map = undefined;

    this.isPanning = false;
    this.isZooming = false;
  }

  onRender() {
    this.createMap();
    return this;
  }

  onAttach() {
    if (this.map) {
      this.map.setTarget(this.el);
      $(window).resize(() => this.onResize());
    }
  }

  /**
   * Convenience function to setup the map.
   */

  createMap(options = {}) {
    // assure that we only set up everything once

    if (this.map) {
      return this;
    }

    // TODO: move this to layout containing this view
    this.$el.css({
      width: '100%',
      height: '100%',
      // 'min-height': '100%',
      // 'background-color': 'red',
      position: 'absolute',
    });

    // create the map object
    this.map = new ol.Map({
      controls: ol.control.defaults().extend([
        new ol.control.MousePosition({
          coordinateFormat: ol.coordinate.createStringXY(4),
          projection: 'EPSG:4326',
          undefinedHTML: ' ',
        }),
      ]),
      renderer: options.mapRenderer || 'canvas',
      view: new ol.View({
        projection: ol.proj.get('EPSG:4326'),
        center: this.mapModel.get('center') || [0, 0],
        zoom: this.mapModel.get('zoom') || 2,
      }),
    });

    // create layer groups for base, normal and overlay layers

    const createGroupForCollection = (collection) => {
      const group = new ol.layer.Group({
        layers: collection.map(layerModel => this.createLayer(layerModel)),
      });
      this.map.addLayer(group);
      return group;
    };

    this.groups = {
      baseLayers: createGroupForCollection(this.baseLayersCollection),
      layers: createGroupForCollection(this.layersCollection),
      overlayLayers: createGroupForCollection(this.overlayLayersCollection),
    };

    this.groups.layers.getLayers().forEach((layer) => {
      this.applyLayerFilters(layer, this.filtersModel);
    }, this);


    const selectionStyle = new ol.style.Style({
      fill: new ol.style.Fill({
        color: 'rgba(255, 255, 255, 0.2)',
      }),
      stroke: new ol.style.Stroke({
        color: '#ffcc33',
        width: 2,
      }),
      image: new ol.style.Circle({
        radius: 7,
        fill: new ol.style.Fill({
          color: '#ffcc33',
        }),
      }),
    });

    this.selectionSource = new ol.source.Vector();

    // this.selectionSource.on("change", this.onDone);

    const selectionLayer = new ol.layer.Vector({
      source: this.selectionSource,
      style: selectionStyle,
    });

    // create layer for highlighting features

    const highlightStyle = new ol.style.Style({
      fill: new ol.style.Fill({
        color: 'rgba(255, 255, 255, 0.2)',
      }),
      stroke: new ol.style.Stroke({
        color: '#cccccc',
        width: 1,
      }),
    });

    this.highlightSource = new ol.source.Vector();

    const highlightLayer = new ol.layer.Vector({
      source: this.highlightSource,
      style: highlightStyle,
    });

    this.map.addLayer(highlightLayer);
    this.map.addLayer(selectionLayer);


    // attach to signals of the collections

    this.setupEvents();
    this.setupControls(selectionStyle);

    return this;
  }

  /**
   * Creates an OpenLayers layer from a given LayerModel.
   *
   * @param {core/models.LayerModel} layerModel The layerModel to create a layer for.
   * @returns {ol.Layer} The OpenLayers layer object
   */
  createLayer(layerModel) {
    const params = layerModel.get('display');
    let layer;

    const projection = ol.proj.get('EPSG:4326');
    const projectionExtent = projection.getExtent();
    const size = ol.extent.getWidth(projectionExtent) / 256;
    const resolutions = new Array(18);
    const matrixIds = new Array(18);
    for (let z = 0; z < 18; ++z) {
      // generate resolutions and matrixIds arrays for this WMTS
      resolutions[z] = size / Math.pow(2, z + 1);
      matrixIds[z] = z;
    }

    switch (params.protocol) {
      case 'WMTS':
        layer = new ol.layer.Tile({
          visible: params.visible,
          source: new ol.source.WMTS({
            urls: (params.url) ? [params.url] : params.urls,
            layer: params.id,
            matrixSet: params.matrixSet,
            format: params.format,
            projection: params.projection,
            tileGrid: new ol.tilegrid.WMTS({
              origin: ol.extent.getTopLeft(projectionExtent),
              resolutions,
              matrixIds,
            }),
            style: params.style,
            attributions: [
              new ol.Attribution({
                html: params.attribution,
              }),
            ],
            wrapX: true,
          }),
        });
        break;
      case 'WMS':
        layer = new ol.layer.Tile({
          visible: params.visible,
          source: new ol.source.TileWMS({
            crossOrigin: 'anonymous',
            params: {
              LAYERS: params.id,
              VERSION: '1.1.0',
              FORMAT: params.format, // TODO: use format here?
            },
            url: params.url || params.urls[0],
            wrapX: true,
          }),
          attribution: params.attribution,
        });
        break;
      default:
        throw new Error('Unsupported view protocol');
    }
    layer.id = layerModel.get('id');
    return layer;
  }

  applyLayerFilters(layer, filtersModel) {
    const time = filtersModel.get('time');
    const isoTime = (time !== null) ?
        `${getISODateTimeString(time[0])}/${getISODateTimeString(time[1])}` : null;
    const source = layer.getSource();
    const params = source.getParams();
    if (isoTime !== null) {
      params.time = isoTime;
    } else {
      delete params.time;
    }
    source.updateParams(params);
  }

  /**
   * Remove the layer from the given group;
   *
   */
  removeLayer(layerModel, group) {
    group.getLayers().remove(this.getLayerOfGroup(layerModel, group));
  }

  getLayerOfGroup(layerModel, group) {
    const id = layerModel.get('id');
    let foundLayer;
    group.getLayers().forEach(layer => {
      if (layer.id === id) {
        foundLayer = layer;
      }
    });
    return foundLayer;
  }

  /**
   * Set up all events from the layer collections
   *
   */

  setupEvents() {
    // setup collection signals
    this.listenTo(this.layersCollection, 'add', (layerModel) =>
      this.addLayer(layerModel, this.groups.layers)
    );
    this.listenTo(this.layersCollection, 'change', (layerModel) =>
      this.onLayerChange(layerModel, this.groups.layers)
    );
    this.listenTo(this.layersCollection, 'remove', (layerModel) =>
      this.removeLayer(layerModel, this.groups.layers)
    );
    this.listenTo(this.layersCollection, 'sort', (layers) => this.onLayersSorted(layers));

    this.listenTo(this.baseLayersCollection, 'add', (layerModel) =>
      this.addLayer(layerModel, this.groups.baseLayers)
    );
    this.listenTo(this.baseLayersCollection, 'change', (layerModel) =>
      this.onLayerChange(layerModel, this.groups.baseLayers)
    );
    this.listenTo(this.baseLayersCollection, 'remove', (layerModel) =>
      this.removeLayer(layerModel, this.groups.baseLayers)
    );

    this.listenTo(this.overlayLayersCollection, 'add', (layerModel) =>
      this.addLayer(layerModel, this.groups.overlayLayers)
    );
    this.listenTo(this.overlayLayersCollection, 'change', (layerModel) =>
      this.onLayerChange(layerModel, this.groups.overlayLayers)
    );
    this.listenTo(this.overlayLayersCollection, 'remove', (layerModel) =>
      this.removeLayer(layerModel, this.groups.overlayLayers)
    );


    // setup mapModel signals

    // directly tie the changes to the map
    this.listenTo(this.mapModel, 'change:center', (mapModel) => {
      if (!this.isPanning) {
        this.map.getView().setCenter(mapModel.get('center'));
      }
    });
    this.listenTo(this.mapModel, 'change:zoom', (mapModel) => {
      if (!this.isZooming) {
        this.map.getView().setZoom(mapModel.get('zoom'));
      }
    });

    this.listenTo(this.mapModel, 'change:roll', (mapModel) => {
      this.map.getView().setRotation(mapModel.get('roll'));
    });

    this.listenTo(this.mapModel, 'change:bbox', (mapModel) => {
      if (!this.isPanning) {
        this.map.getView().fit(mapModel.get('bbox'), this.map.getSize());
      }
    });

    this.listenTo(this.mapModel, 'change:tool', this.onToolChange);


    this.listenTo(this.mapModel, 'change:highlightFootprint', (mapModel) => {
      this.highlightSource.clear();
      const footprint = mapModel.get('highlightFootprint');
      if (footprint) {
        const polygon = new ol.geom.Polygon([footprint]);
        const feature = new ol.Feature();
        feature.setGeometry(polygon);
        this.highlightSource.addFeature(feature);
      }
    });


    // setup filters signals

    this.listenTo(this.filtersModel, 'change:time', this.onFiltersTimeChange);


    // setup map events

    const self = this;
    this.map.on('pointerdrag', () => {
      // TODO: check if the currently selected tool is the panning tool
      // TODO: improve this to allow
      self.isPanning = true;
    });

    this.map.on('moveend', () => {
      self.mapModel.set({
        center: self.map.getView().getCenter(),
        zoom: self.map.getView().getZoom(),
        bbox: self.map.getView().calculateExtent(self.map.getSize()),
      });
      self.isPanning = false;
      self.isZooming = false;
    });
  }

  /**
   * Creates OpenLayers interactions and adds them to the map.
   *
   */
  setupControls(selectionStyle) {
    this.drawControls = {
      point: new ol.interaction.Draw({ source: this.selectionSource, type: 'Point' }),
      line: new ol.interaction.Draw({ source: this.selectionSource, type: 'LineString' }),
      polygon: new ol.interaction.Draw({ source: this.selectionSource, type: 'Polygon' }),
      bbox: new ol.interaction.DragBox({ style: selectionStyle }),
    };

    this.drawControls.bbox.on('boxstart', (evt) => {
      this.drawControls.bbox.boxstart = evt.coordinate;
    }, this);

    this.drawControls.bbox.on('boxend', (evt) => {
      const boxend = evt.coordinate;
      const boxstart = this.drawControls.bbox.boxstart;
      const polygon = ol.geom.Polygon.fromExtent([
        boxstart[0], boxstart[1], boxend[0], boxend[1],
      ]);

      // if (this.selectionType === "single"){
      //   var features = this.source.getFeatures();
      //   for (var i in features){
      //     this.source.removeFeature(features[i]);
      //   }
      //   Communicator.mediator.trigger("selection:changed", null);
      // }

      const feature = new ol.Feature();
      feature.setGeometry(polygon);
      this.selectionSource.addFeature(feature);
    }, this);
  }


  // collection/model signal handlers

  onLayersSorted(layersCollection) {
    const ids = layersCollection.pluck('id');
    const layers = this.groups.layers.getLayers();

    this.groups.layers.setLayers(
      new ol.Collection(layers.getArray().sort((layer) => ids.indexOf(layer.id)))
    );
  }

  onLayerChange(layerModel, group) {
    const layer = this.getLayerOfGroup(layerModel, group);
    if (layerModel.hasChanged('display')) {
      const display = layerModel.get('display');
      layer.setVisible(display.visible);
      layer.setOpacity(display.opacity);
    }
  }

  onFiltersTimeChange(filtersModel) {
    this.layersCollection.forEach((layerModel) => {
      this.applyLayerFilters(this.getLayerOfGroup(layerModel, this.groups.layers), filtersModel);
    }, this);
  }

  onToolChange(mapModel) {
    const toolName = mapModel.get('tool');
    // deactivate all potentially activated tools
    for (const key in this.drawControls) {
      if (this.drawControls.hasOwnProperty(key)) {
        this.map.removeInteraction(this.drawControls[key]);
      }
    }
    // activate the requested tool if it is available
    if (this.drawControls.hasOwnProperty(toolName)) {
      this.map.addInteraction(this.drawControls[toolName]);
    }
  }

  onDestroy() {
    // TODO: necessary?
  }

  onResize() {
    this.map.updateSize();
  }
}

OpenLayersMapView.prototype.template = () => '';

OpenLayersMapView.prototype.events = {
  resize: 'onResize',
};

export default OpenLayersMapView;