import EventEmitter from 'events';
import ol from 'openlayers';
import LayerSwitcher from './layer-switcher';
import popupTemplate from '../../templates/map/feature-popup.hbs';
/**
* This class takes care of everything related with the Map.
*/
class MapManager extends EventEmitter {
/**
* @param {Object} [config] - Configuration object
* @param {Number[]} [config.center] - The initial center for the map
* @param {Number} [config.zoom] - The initial zoom level
*/
constructor(config = {}) {
super();
this.defaultCenter = config.center || [0, 0];
this.defaultZoom = config.zoom || 1;
this.baseLayers = [];
this.overlayLayers = [];
this.createMap();
this.createOverlay();
this.addLayerSwitcher();
}
/**
* Dashboard filter string wrapper
* @member {String}
* @readonly
*/
get filterString() {
return this.dashboard.filterString;
}
/**
* Dashboard filters array wrapper
* @member {String}
* @readonly
*/
get filters() {
return this.dashboard.filters;
}
/**
* Creates the [OpenLayers Map](https://openlayers.org/en/latest/apidoc/ol.Map.html) and sets its initial status.
* @private
*/
createMap() {
this.view = new ol.View({
center: ol.proj.fromLonLat(this.defaultCenter),
zoom: this.defaultZoom,
});
this.map = new ol.Map({
view: this.view,
loadTilesWhileInteracting: true,
interactions: ol.interaction.defaults({ mouseWheelZoom: false }),
layers: this.layers,
});
this.viewProjection = this.view.getProjection();
this.viewResolution = this.view.getResolution();
this.map.on('moveend', (event) => {
event.extent = this.map.getView().calculateExtent(this.map.getSize());
this.emit('mapchange', event);
});
}
/**
* Creates the [OpenLayers Overlay](https://openlayers.org/en/latest/apidoc/ol.Overlay.html) to show popups.
* @private
*/
createOverlay() {
this.overlay = new ol.Overlay({});
this.map.addOverlay(this.overlay);
this.map.on('singleclick', this.featurePopup.bind(this));
}
/**
* Adds the LayerSwitcher control to the map
* @private
*/
addLayerSwitcher() {
this.layerSwitcher = new LayerSwitcher();
this.layerSwitcher.manager = this;
this.layerSwitcher.on('layerChanged', () => {
this.overlay.setElement(null);
});
this.map.addControl(this.layerSwitcher);
}
/**
* Renders the map and all its components into the specefied container.
* @param {HTMLElement} container - The element where the map will be rendered
*/
render(container) {
this.map.setTarget(container);
setTimeout(() => this.map.updateSize(), 100);
}
/**
* Adds a BaseLayer to the map.
* @param {BaseLayer} layer - The layer to add
*/
addBaseLayer(layer) {
layer.manager = this;
this.baseLayers.push(layer);
this.map.addLayer(layer.layer);
}
/**
* Adds an OverlayLayer to the map.
* @param {OverlayLayer} layer - The layer to add
*/
addOverlayLayer(layer) {
layer.manager = this;
this.overlayLayers.push(layer);
this.map.addLayer(layer.layer);
layer.on('loaded', () => this.emit('loaded'));
layer.refresh();
}
/**
* Refreshes all OverLayer layers
*/
refresh() {
this.overlayLayers.forEach(layer => layer.refresh());
}
/**
* Centers map to definied coordinates and zoom level
* @param {Number[]} center - Center coordinates
* @param {Number} [zoom] - Zoom level
*/
center(center) {
this.map.beforeRender(ol.animation.pan({
source: this.view.getCenter(),
}));
this.view.setCenter(ol.proj.fromLonLat(center));
}
/**
* Fits map to definied extent
* @param {Number[]} extent - Array of numbers representing an extent: [minx, miny, maxx, maxy]
*/
fit(extent) {
if (extent && extent[0] && Number.isFinite(extent[0])) {
this.map.beforeRender(ol.animation.zoom({
resolution: this.view.getResolution(),
}));
this.map.beforeRender(ol.animation.pan({
source: this.view.getCenter(),
}));
this.view.fit(extent, this.map.getSize());
}
}
/**
* Fits map to defined layer
* @param {Layer} layer - Layer to fit in map
*/
fitToLayer(layer) {
if (layer) {
this.fit(layer.source.getExtent());
}
}
/**
* Zooms map to the defined level
* @param {Number} zoom - Zoom level
*/
zoom(zoom = 1) {
this.map.beforeRender(ol.animation.zoom({
resolution: this.view.getResolution(),
}));
this.view.setZoom(zoom);
}
/**
* Popup handler.
* @param {Event} event - The event object triggered by user interaction
*/
featurePopup(event) {
const pixel = this.map.getEventPixel(event.originalEvent);
const result = this.map.forEachFeatureAtPixel(pixel, (feature, layer) => ({
feature,
layer,
}));
if (result && result.feature) {
const properties = result.layer.popup.map(property => ({
title: property.title,
value: property.format ? property.format(result.feature.get(property.property)) :
result.feature.get(property.property),
}));
const element = document.createElement('div');
element.innerHTML = popupTemplate({
properties,
});
this.map.beforeRender(ol.animation.pan({
duration: 1000,
source: this.view.getCenter(),
}));
pixel[0] += element.offsetWidth + 100;
pixel[1] -= element.offsetHeight - 150;
this.view.setCenter(this.map.getCoordinateFromPixel(pixel));
this.overlay.setElement(element);
this.overlay.setPosition(event.coordinate);
} else {
this.overlay.setElement(null);
}
}
/**
* Searches and returns a specifc layer
* @param {String} id - The layer ID to find
* @returns {Layer}
*/
getLayerById(id) {
let layer = this.baseLayers.find(l => l.id === id);
if (!layer) {
layer = this.overlayLayers.find(l => l.id === id);
}
return layer;
}
}
export default MapManager;