public/js/components/map.js (202 lines of code) (raw):
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import maplibre from 'maplibre-gl';
import mbRtlPlugin from '@mapbox/mapbox-gl-rtl-text/mapbox-gl-rtl-text.min.js';
import turfBbox from '@turf/bbox';
import turfCenter from '@turf/center';
import React, { Component } from 'react';
import chroma from 'chroma-js';
maplibre.setRTLTextPlugin(mbRtlPlugin);
export class Map extends Component {
static isSupported() {
try {
const canvas = document.createElement('canvas');
const contextIds = ['webgl', 'experimental-webgl'];
return contextIds.some( (c) => {
const ctx = canvas.getContext(c);
return ctx && ctx instanceof WebGLRenderingContext;
});
} catch (error) { //eslint-disable-line no-unused-vars
return false;
}
}
constructor(props) {
super(props);
this._overlaySourceId = 'overlay-source';
this._overlayFillLayerId = 'overlay-fill-layer';
this._overlayLineLayerId = 'overlay-line-layer';
this._overlayFillHighlightId = 'overlay-fill-highlight-layer';
this._tmsSourceId = 'vector-tms-source';
this._tmsLayerId = 'vector-tms-layer';
}
componentDidMount() {
this._maplibreMap = new maplibre.Map({
container: this.refs.mapContainer,
style: {
version: 8,
sources: {},
layers: [],
},
transformRequest: (url) => {
return { url: new URL(url, window.location.origin).href };
},
});
this._maplibreMap.addControl(new maplibre.FullscreenControl());
this._maplibreMap.dragRotate.disable();
this._maplibreMap.touchZoomRotate.disableRotation();
this._maplibreMap.on('click', this._overlayFillLayerId, (e) => {
this._highlightFeature(e.features[0], e.lngLat);
});
// Change the cursor to a pointer when the mouse is over the places layer.
this._maplibreMap.on('mouseenter', this._overlayFillLayerId, () => {
this._maplibreMap.getCanvas().style.cursor = 'pointer';
});
// Change it back to a pointer when it leaves.
this._maplibreMap.on('mouseleave', this._overlayFillLayerId, () => {
this._maplibreMap.getCanvas().style.cursor = '';
});
}
_getOverlayLayerIds() {
return [this._overlayFillLayerId, this._overlayLineLayerId, this._overlayFillHighlightId];
}
_highlightFeature(feature, lngLat) {
this._removePopup();
const keys = Object.keys(feature.properties);
let rows = '';
keys.forEach((key) => {
if (key === '__id__') {
return;
}
rows += `<dt>${key}</dt><dd>${feature.properties[key]}</dd>`;
});
const html = `<div class="euiText euiText--extraSmall"><dl class="popup_feature_list">${rows}</dl></div>`;
this._currentPopup = new maplibre.Popup();
this._currentPopup.setLngLat(lngLat);
this._currentPopup.setHTML(html);
this._currentPopup.addTo(this._maplibreMap);
this._maplibreMap.setFilter(this._overlayFillHighlightId, ['==', '__id__', feature.properties.__id__]);
}
highlightFeature(feature) {
const center = turfCenter(feature);
this._highlightFeature(feature, new maplibre.LngLat(center.geometry.coordinates[0], center.geometry.coordinates[1]));
const bbox = turfBbox(feature);
this._maplibreMap.fitBounds(bbox);
}
filterFeatures(features) {
const idFilterPrefix = ['in', '__id__'];
const filterArgs = features.map((f) => f.properties.__id__);
const filter = idFilterPrefix.concat(filterArgs);
this._maplibreMap.setFilter(this._overlayFillLayerId, filter);
this._maplibreMap.setFilter(this._overlayLineLayerId, filter);
}
_removeTmsLayer() {
if (this._maplibreMap.getLayer(this._tmsLayerId)) {
this._maplibreMap.removeLayer(this._tmsLayerId);
}
if (this._maplibreMap.getSource(this._tmsSourceId)) {
this._maplibreMap.removeSource(this._tmsSourceId);
}
}
_removeOverlayLayer() {
if (this._maplibreMap.getLayer(this._overlayFillLayerId)) {
this._maplibreMap.removeLayer(this._overlayFillLayerId);
}
if (this._maplibreMap.getLayer(this._overlayLineLayerId)) {
this._maplibreMap.removeLayer(this._overlayLineLayerId);
}
if (this._maplibreMap.getLayer(this._overlayFillHighlightId)) {
this._maplibreMap.removeLayer(this._overlayFillHighlightId);
}
if (this._maplibreMap.getSource(this._overlaySourceId)) {
this._maplibreMap.removeSource(this._overlaySourceId);
}
this._removePopup();
}
_removePopup() {
if (this._currentPopup) {
this._currentPopup.remove();
this._currentPopup = null;
}
}
_persistOverlayLayers(source) {
const overlayLayerIds = this._getOverlayLayerIds();
const curStyle = this._maplibreMap.getStyle();
const overlayLayers = curStyle.layers.filter(layer => overlayLayerIds.includes(layer.id));
const overlaySource = { ...curStyle.sources };
const nonLabelLayers = source.layers.filter(l => l.type !== 'symbol');
const labelLayers = source.layers.filter(l => l.type === 'symbol');
const layers = [ ...nonLabelLayers, ...overlayLayers, ...labelLayers];
const sources = { ...source.sources, ...overlaySource };
return {
...source,
...{ layers, sources }
};
}
waitForStyleLoaded(callback) {
const waiting = () => {
if (!this._maplibreMap.isStyleLoaded()) {
setTimeout(waiting, 50);
} else {
callback();
}
};
waiting();
}
setTmsLayer(source, callback) {
// The setStyle method removes all layers and sources from the map including the overlays.
// We must persist the overlay layers and overlay source by creating a new style from
// the incoming source and the overlay layers.
const newStyle = this._persistOverlayLayers(source);
this._maplibreMap.setStyle(newStyle, { diff: false });
if (callback) {
this.waitForStyleLoaded(callback);
}
}
setOverlayLayer(featureCollection, skipZoom, fillColor) {
if (fillColor && !chroma.valid(fillColor)) {
throw new Error(`${fillColor} is not a valid color representation`);
}
this._removeOverlayLayer();
const fill = fillColor ? chroma(fillColor) : chroma('rgb(220,220,220)');
// highlight with the complementary color
const highlight = fillColor ? fill.set('hsl.h', '+180') : chroma('#627BC1');
const border = fill.darken(2);
this._maplibreMap.addSource(this._overlaySourceId, {
type: 'geojson',
data: featureCollection,
});
//Get the first symbol layer id
const firstSymbol = this._maplibreMap.getStyle().layers.find(l => l.type === 'symbol');
this._maplibreMap.addLayer({
id: this._overlayFillLayerId,
source: this._overlaySourceId,
type: 'fill',
paint: {
'fill-color': fill.css(),
'fill-opacity': 0.6,
},
}, firstSymbol?.id);
this._maplibreMap.addLayer({
id: this._overlayLineLayerId,
source: this._overlaySourceId,
type: 'line',
paint: {
'line-color': border.css(),
'line-width': 1,
},
}, firstSymbol?.id);
this._maplibreMap.addLayer({
id: this._overlayFillHighlightId,
source: this._overlaySourceId,
type: 'fill',
layout: {},
paint: {
'fill-color': highlight.css(),
'fill-opacity': 1,
},
filter: ['==', 'name', ''],
}, firstSymbol?.id);
if (!skipZoom) {
const bbox = turfBbox(featureCollection);
//bug in mapbox-gl dealing with wrapping bounds
//without normalization, maplibre will throw on the world layer
//seems to be fixed when cropping the bounds slightly.
if (bbox[2] - bbox[0] > 360) {
bbox[0] = -175;
bbox[1] = -85;
bbox[2] = 175;
bbox[3] = 85;
}
this._maplibreMap.fitBounds(bbox);
}
}
render() {
return (<div className="mapContainer" ref="mapContainer" />);
}
}