Интеграция с Deck GL
Deck.gl — это WebGL-фреймворк для визуального исследования больших наборов данных.
Он построен на основе популярной библиотеки React и предназначен для работы с различными JS-API карт.
Этот пример демонстрирует интеграцию deck.gl с ymaps3.
Важно
Для запуска этого примера необходим форкнутый DevBox на CodeSandbox: нажмите «Open in CodeSandbox» на этой странице, затем «Fork» в правом верхнем углу, затем «Fork as DevBox» в новом CodeSandbox.
Важно
Deck.gl версии 9.0 работает только с WebGL2RenderingContext, но JS API Яндекс Карт ещё не поддерживает его. Поэтому все примеры используют deck.gl версии 8.0.
Важно
Для интеграции Deck.GL необходима опция «Custom layer implementations». Чтобы её включить, обратитесь в поддержку.
Подробнее о пользовательских слоях — в документации.
Как запустить
Скачайте исходники из CodeSandbox и запустите локально:
- Выполните
npm ci - Выполните
npm start - Откройте
http://localhost:8081/в браузере
Как это работает
В примере deck.gl используется для отрисовки слоёв рядом с векторными слоями из ymaps3 в качестве пользовательских слоёв.
Deck.gl отрисовывается во фреймбуфер, который затем используется как текстура для слоя карты векторным движком ymaps3.
При инициализации Deck можно задать экспериментальный параметр _framebuffer для рендеринга во фреймбуфер.
Его можно создать с помощью Framebuffer из luma.gl.
import {Deck} from '@deck.gl/core/typeings';
import {Framebuffer} from '@luma.gl/core';
const deck = new Deck({
_framebuffer: new Framebuffer(gl, {width: 1024, height: 1024}),
layers: [new MyCustomLayer()]
});
Файл common.ts описывает абстрактный класс DeckGlCustomLayer, содержащий логику рендеринга с deck.gl и ymaps3 внутри фреймбуфера.
Остальные файлы — реализации слоёв для deck.gl.
Чтобы добавить новый слой, нужно создать новый файл с его реализацией.
import {DeckGlCustomLayer} from './custom';
import {AmbientLight, PointLight, LightingEffect} from '@deck.gl/core/typed';
import {GeoJsonLayer} from '@deck.gl/layers/typed';
const ambientLight = new AmbientLight({
color: [255, 255, 255],
intensity: 1.0
});
const pointLight1 = new PointLight({
color: [255, 255, 255],
intensity: 0.8,
position: [-0.144528, 49.739968, 80000]
});
const pointLight2 = new PointLight({
color: [255, 255, 255],
intensity: 0.8,
position: [-3.807751, 54.104682, 8000]
});
const lightingEffect = new LightingEffect({ambientLight, pointLight1, pointLight2});
const DATA_URL = 'https://yastatic.net/s3/front-maps-static/maps-front-jsapi-3/examples/deck-example/vancouver-blocks.json';
export class MyCustomLayer extends DeckGlCustomLayer {
protected override _getDeckGlEffects() {
return [lightingEffect];
}
protected override _getDeckGlLayers() {
return [
new GeoJsonLayer({
id: 'geojson',
data: DATA_URL,
opacity: 0.8,
stroked: false,
filled: true,
extruded: true,
wireframe: false,
getElevation: (f: any) => {
return Math.sqrt(f.properties.valuePerSqm) * 10;
},
getFillColor: (f: any) => COLOR_SCALE(f.properties.growth),
getLineColor: [1, 255, 255],
pickable: true
})
];
}
}
Подробнее о работе пользовательских векторных слоёв — в документации.
Поскольку экземпляр DeckGlCustomLayer создаётся векторным движком ymaps3 и мы не можем влиять на входные параметры, можно использовать паттерн EventEmitter для передачи параметров в DeckGlCustomLayer.
import {DummyMapEngine} from './dummy-map-engine';
// Можно использовать любой EventEmitter из npm
declare class EventEmitter {
on(event: string, listener: Function): void;
emit(event: string, ...args: any[]): void;
}
export function getLayer(map: YMap) {
const emitter = new EventEmitter();
class MyCustomLayer extends DeckGlCustomLayer {
private __spmeProps: {
opacity: number;
} = {
opacity: 1
};
constructor(
gl: WebGLRenderingContext,
options: {
requestRender: () => void;
}
) {
super({
map,
eventEmitter,
gl,
options
});
// Подписка на событие обновления параметров
emitter.on('updateProps', (someProps: {opacity: number}) => {
this.__someProps = someProps;
this._deck.setProps({
layers: this._getDeckGlLayers() // обновляем слои
});
});
}
protected _getDeckGlLayers() {
return [
new GeoJsonLayer({
id: 'geojson',
data: DATA_URL,
opacity: this.__someProps.opacity // передаём новый параметр в слой
// ...
})
];
}
}
return [MyCustomLayer, emitter];
}
После создания карты и пользовательского слоя параметры слоя можно обновлять извне.
import {getLayer} from './my-custom-layer';
const map = new ymaps3.YMap({...});
const [MyCustomLayer, emitter] = getLayer(map);
map.addChild(
new ymaps3.YMapLayer({
type: 'custom',
grouppedWith: ymaps3.YMapDefaultSchemeLayer.defaultProps.source + ':buildings',
implementation({type, effectiveMode}: any) {
if (type === 'custom' && effectiveMode === 'vector') {
return MyCustomLayer;
}
}
})
);
// Позже, где-то в коде, обновляем параметры слоя
setTimeout(() => {
emitter.emit('updateProps', {opacity: 0.5});
}, 1000);
Примеры
Тепловая карта
Пример показывает, как создать тепловую карту с помощью библиотеки deck.gl.
Основан на примере deck.gl и адаптирован для ymaps3.
- Файл:
heatmap-layer.ts
Гексагоны
Пример показывает, как создать слой гексагональной сетки с помощью библиотеки deck.gl и отобразить его на карте ymaps3.
В примере загружается и обрабатывается CSV-файл с данными об авариях в Великобритании.
Основан на примере deck.gl и адаптирован для ymaps3.
- Файл:
hexagon-layer.ts
Полигоны GeoJSON
Пример показывает, как создать слой с полигонами GeoJSON, отображающими стоимость недвижимости в Ванкувере, с помощью библиотеки deck.gl.
Основан на примере deck.gl и адаптирован для ymaps3.
- Файл:
geojson-layer-polygons.ts
Пути GeoJSON
Пример показывает, как создать слой с путями GeoJSON, отображающими смертельные аварии на автодорогах США, с помощью библиотеки deck.gl.
Основан на примере deck.gl и адаптирован для ymaps3.
- Файл:
geojson-layer-paths.ts
Примитивный куб
Пример показывает, как создать слой с примитивным кубом с помощью библиотек deck.gl и luma.gl.
- Файл:
cube-layer.ts
Слой GLTF
Пример показывает, как создать слой с GLTF-моделью с помощью библиотек @deck.gl/mesh-layer и @turf/turf.
В примере с помощью turf создаётся множество случайных точек внутри полигона, ограничивающего русло Темзы, которые затем отображаются на карте в виде GLTF-модели.
- Файл:
gltf-layer.ts
import type {Camera} from '@yandex/ymaps3-types';
import {Deck, type LayersList, type Effect, type MapViewState, MapView, type PickingInfo} from '@deck.gl/core/typed';
import {Framebuffer, Texture2D} from '@luma.gl/webgl';
import {default as GL} from '@luma.gl/constants';
import type {LngLat, VectorLayerImplementation, VectorLayerImplementationRenderProps, YMap} from '@yandex/ymaps3-types';
import {EventEmitter} from './event-emitter';
/**
* Base class for custom layers based on deck.gl
*/
export abstract class DeckGlCustomLayer<Props extends {} = {}> implements VectorLayerImplementation {
protected _deck: Deck; // https://deck.gl/docs/api-reference/core/deck
protected _framebuffer: Framebuffer; // https://luma.gl/docs/api-reference/core/resources/framebuffer
protected _props: Props; // We will use this property to pass parameters to deck.gl layers
protected _gl: WebGLRenderingContext;
protected _requestRender: Function;
protected _map: YMap;
protected _events: EventEmitter; // Just a simple event emitter
protected _view = new MapView({
id: 'deck-view',
repeat: true,
nearZMultiplier: 0.01 // By default, it 0.1, but it's too big for ymaps3 https://deck.gl/docs/api-reference/core/map-view#nearzmultiplier
});
constructor({
props = {} as Props,
map,
gl,
eventEmitter,
options: {requestRender}
}: {
map: YMap;
eventEmitter: EventEmitter;
gl: WebGLRenderingContext;
options: {
requestRender: () => void;
};
props?: Props;
}) {
this._props = props;
this._requestRender = requestRender;
this._gl = gl;
this._map = map;
this._events = eventEmitter;
// Create a framebuffer with color and depth textures. We will use it to render deck.gl layers`
gl.getExtension('WEBGL_depth_texture');
const attachments = {
[GL.COLOR_ATTACHMENT0]: new Texture2D(gl, {
format: GL.RGBA,
type: gl.UNSIGNED_BYTE,
dataFormat: GL.RGBA,
parameters: {
[GL.TEXTURE_MIN_FILTER]: GL.LINEAR,
[GL.TEXTURE_WRAP_S]: GL.CLAMP_TO_EDGE,
[GL.TEXTURE_WRAP_T]: GL.CLAMP_TO_EDGE
}
}),
[GL.DEPTH_ATTACHMENT]: new Texture2D(gl, {
format: GL.DEPTH_COMPONENT,
type: gl.UNSIGNED_INT,
dataFormat: GL.DEPTH_COMPONENT,
mipmaps: false,
parameters: {
[GL.TEXTURE_MIN_FILTER]: GL.NEAREST,
[GL.TEXTURE_WRAP_S]: GL.CLAMP_TO_EDGE,
[GL.TEXTURE_WRAP_T]: GL.CLAMP_TO_EDGE
}
})
};
this._framebuffer = new Framebuffer(gl, {
width: gl.drawingBufferWidth,
height: gl.drawingBufferHeight,
color: true,
depth: true,
attachments
});
this._deck = this.__initDeckGl();
/**
* From outside, we will change the properties of the layers using the event emitter
* ```js
* eventEmitter.emit('props', {count: 1000, animation: 1, size: 10});
* ```
*/
this.__setLayersProps = this.__setLayersProps.bind(this);
this._events.on('props', this.__setLayersProps);
this.__tooltip.classList.add('tooltip');
}
private __setLayersProps(props: Props) {
this._props = props;
/**
* When the properties of the layers change, we need to update the layers in the deck.gl
* Layers recreated every time, don't worry about performance issues https://deck.gl/docs/developer-guide/using-layers#layer-id
*/
this._deck.setProps({
layers: this._getDeckGlLayers()
});
// We need to redraw whole map
this._requestRender();
}
/**
* This method will be called when the user hovers over the layer
* By default, it returns null, but you can override it
*/
protected _getTooltip({object}: PickingInfo): string | null {
return null;
}
/**
* Method for initializing the deck.gl instance https://deck.gl/docs/api-reference/core/deck
*/
protected __initDeckGl(): Deck {
return new Deck({
views: this._view,
_framebuffer: this._framebuffer,
gl: this._gl,
width: '100%',
height: '100%',
pickingRadius: 100, // For object picking https://deck.gl/docs/api-reference/core/deck#pickingradius
controller: false, // Deck positioning is entirely handled by ymaps3
onHover: this.__onHover.bind(this),
layers: this._getDeckGlLayers(),
effects: this._getDeckGlEffects()
});
}
/**
* Main method that should be overridden in the child class
* It should return an array of deck.gl layers
*/
protected _getDeckGlLayers(): LayersList | undefined {
return [];
}
/**
* Method that should be overridden in the child class
* It should return an array of deck.gl effects
*/
protected _getDeckGlEffects(): Effect[] | undefined {
return [];
}
private __tooltip: HTMLElement = document.createElement('div');
private __tooltipTimeout: number | null = null;
/**
* Method for displaying a tooltip when hovering over a layer
*/
private __onHover(info: PickingInfo, ...args: any[]) {
const tooltip = this._getTooltip(info);
if (tooltip) {
window.clearTimeout(this.__tooltipTimeout);
this.__tooltip.innerHTML = tooltip;
this.__tooltip.style.left = `${info.x}px`;
this.__tooltip.style.top = `${info.y}px`;
(this._gl.canvas as HTMLCanvasElement).parentElement?.appendChild(this.__tooltip);
} else if (this.__tooltip.parentElement) {
this.__tooltipTimeout = window.setTimeout(() => {
this.__tooltip.remove();
}, 100);
}
}
/**
* We must implement the VectorLayerImplementation.render method, which will be called every time the map is updated
*/
render(props: VectorLayerImplementationRenderProps): ReturnType<VectorLayerImplementation['render']> {
const gl = this._gl;
this._framebuffer
.resize({
width: props.size.x,
height: props.size.y
})
.clear({
color: [0, 0, 0, 0],
depth: true,
stencil: false
});
/**
* We control the camera of the deck.gl using the camera of the ymaps3
*/
// @ts-ignore TODO Remove ts-ignore after release types
this._view.props.fovy = props.camera.fov / (Math.PI / 180);
this._deck.setProps({
viewState: this._getDeckViewState(
this._map.projection.fromWorldCoordinates(props.camera.worldCenter),
props.camera
)
});
this._deck.redraw('Reason: Map update');
this._requestRender();
// We must return the color and depth textures of the framebuffer
return {
color: this._framebuffer.color.handle,
depth: this._framebuffer.depth.handle
};
}
/**
* We must implement the VectorLayerImplementation.destroy method, which will be called when the layer is destroyed
*/
destroy() {
this._events.off('props', this.__setLayersProps);
this._deck.finalize();
}
/**
* Method for converting the camera of the ymaps3 to the camera of the deck.gl
*/
private _getDeckViewState(center: LngLat, camera: Camera): MapViewState {
return {
longitude: center[0],
latitude: center[1],
zoom: camera.zoom - 1,
bearing: -camera.azimuth / (Math.PI / 180),
pitch: camera.tilt / (Math.PI / 180),
maxZoom: 21,
maxPitch: 50
};
}
}
import type {LngLat, YMap} from '@yandex/ymaps3-types';
import '../common.css';
import {customLayer, customStyle, ui} from '../variables';
import {BEHAVIORS} from '../variables';
import {addUI} from './ui';
declare global {
interface Window {
map: YMap | null;
}
}
main();
async function main() {
// Waiting for all api elements to be loaded
await ymaps3.ready;
const {YMap, YMapDefaultSchemeLayer} = ymaps3;
const {SphericalMercator} = await ymaps3.import('@yandex/ymaps3-spherical-mercator-projection@0.0.1');
const app = document.getElementById('app')!;
app.classList.add('loading');
// Initialize the map
const map = new YMap(
// Pass the link to the HTMLElement of the container
app,
// Pass the map initialization parameters
{
theme: 'light',
behaviors: BEHAVIORS,
location: {center: ui.view.center as LngLat, zoom: ui.view.zoom},
camera: ui.view,
showScaleInCopyrights: true,
mode: 'vector',
projection: new SphericalMercator()
},
// Add a map scheme layer
[
new YMapDefaultSchemeLayer({
customization: customStyle
})
]
);
window.map = map;
const {Layer, eventEmitter} = await customLayer(map);
// Add a custom layer
map.addChild(
new ymaps3.YMapLayer({
type: 'custom',
grouppedWith: ymaps3.YMapDefaultSchemeLayer.defaultProps.source + ':buildings',
implementation({type, effectiveMode}: any) {
if (type === 'custom' && effectiveMode === 'vector') {
return Layer;
}
}
})
);
eventEmitter.on('ready', () => {
app.classList.remove('loading');
});
app.appendChild(addUI(ui, eventEmitter));
// Add a listener for the mousemove event. Used in gltf-layer.ts
map.addChild(
new ymaps3.YMapListener({
onMouseMove: (obj: unknown, {coordinates}) => {
eventEmitter.emit('mousemove', coordinates);
}
})
);
}
import type {YMap} from '@yandex/ymaps3-types';
import {
type Color,
LightingEffect,
AmbientLight,
_SunLight as SunLight,
type Effect,
type PickingInfo
} from '@deck.gl/core/typed';
import {GeoJsonLayer, PolygonLayer} from '@deck.gl/layers/typed';
import {scaleThreshold} from 'd3-scale';
import {DeckGlCustomLayer} from '../common';
import {EventEmitter} from '../event-emitter';
import {CustomLayerDescription} from '../interface';
/**
* Forked from deck.gl example:
* https://github.com/visgl/deck.gl/blob/9.0-release/examples/website/geojson/
*/
export const COLOR_SCALE = scaleThreshold<number, Color>()
.domain([-0.6, -0.45, -0.3, -0.15, 0, 0.15, 0.3, 0.45, 0.6, 0.75, 0.9, 1.05, 1.2])
.range([
[65, 182, 196],
[127, 205, 187],
[199, 233, 180],
[237, 248, 177],
// zero
[255, 255, 204],
[255, 237, 160],
[254, 217, 118],
[254, 178, 76],
[253, 141, 60],
[252, 78, 42],
[227, 26, 28],
[189, 0, 38],
[128, 0, 38]
]);
const landCover = [
[
[-123.0, 49.196],
[-123.0, 49.324],
[-123.306, 49.324],
[-123.306, 49.196]
]
];
const ambientLight = new AmbientLight({
color: [255, 255, 255],
intensity: 1.0
});
const dirLight = new SunLight({
timestamp: Date.UTC(2019, 7, 1, 22),
color: [255, 255, 255],
intensity: 1.0,
_shadow: true
});
const lightingEffect = new LightingEffect({ambientLight, dirLight});
lightingEffect.shadowColor = [0, 0, 0, 0.5];
// Source data GeoJSON
const DATA_URL = 'https://yastatic.net/s3/front-maps-static/maps-front-jsapi-3/examples/deck-example/vancouver-blocks.json'; // eslint-disable-line
export async function getCustomGeojsonPolygonsLayer(map: YMap): Promise<CustomLayerDescription> {
const eventEmitter = new EventEmitter();
class CustomPolygonVectorDeckLayer extends DeckGlCustomLayer {
constructor(
gl: WebGLRenderingContext,
options: {
requestRender: () => void;
}
) {
super({map, eventEmitter, gl, options});
}
protected override _getTooltip({object}: PickingInfo) {
return object && `\
<div><b>Average Property Value</b></div>
<div>${object.properties.valuePerParcel ?? ''} / parcel</div>
<div>${object.properties.valuePerSqm} / m<sup>2</sup></div>
<div><b>Growth</b></div>
<div>${Math.round(object.properties.growth * 100)}%</div>
`;
}
protected override _getDeckGlEffects(): Effect[] {
return [lightingEffect];
}
protected override _getDeckGlLayers() {
return [
new GeoJsonLayer({
id: 'geojson',
data: DATA_URL,
opacity: 0.8,
stroked: false,
filled: true,
extruded: true,
wireframe: false,
getElevation: (f) => Math.sqrt(f.properties.valuePerSqm) * 10,
getFillColor: (f) => COLOR_SCALE(f.properties.growth),
getLineColor: [1, 255, 255],
pickable: true,
onDataLoad: () => {
eventEmitter.emit('ready');
this._requestRender();
}
})
];
}
}
return {Layer: CustomPolygonVectorDeckLayer, eventEmitter};
}
import {GeoJsonLayer} from '@deck.gl/layers/typed';
import {scaleLinear, scaleThreshold} from 'd3-scale';
import type {Feature, LineString, MultiLineString} from 'geojson';
import type {Color, PickingInfo} from '@deck.gl/core/typed';
import {DeckGlCustomLayer} from '../common';
import type {YMap} from '@yandex/ymaps3-types';
import {EventEmitter} from '../event-emitter';
import {CustomLayerDescription} from '../interface';
/**
* Forked from deck.gl example:
* https://github.com/visgl/deck.gl/tree/9.0-release/examples/website/highway/
*/
// Source data GeoJSON
const DATA_URL = {
ACCIDENTS: 'https://yastatic.net/s3/front-maps-static/maps-front-jsapi-3/examples/deck-example/accidents.csv',
ROADS: 'https://yastatic.net/s3/front-maps-static/maps-front-jsapi-3/examples/deck-example/roads.json'
};
export const COLOR_SCALE = scaleThreshold<number, Color>()
.domain([0, 4, 8, 12, 20, 32, 52, 84, 136, 220])
.range([
[26, 152, 80],
[102, 189, 99],
[166, 217, 106],
[217, 239, 139],
[255, 255, 191],
[254, 224, 139],
[253, 174, 97],
[244, 109, 67],
[215, 48, 39],
[168, 0, 0]
]);
const WIDTH_SCALE = scaleLinear().clamp(true).domain([0, 200]).range([10, 2000]);
type Accident = {
state: string;
type: string;
id: string;
year: number;
incidents: number;
fatalities: number;
};
type RoadProperties = {
state: string;
type: string;
id: string;
name: string;
length: number;
};
type Road = Feature<LineString | MultiLineString, RoadProperties>;
function getKey({state, type, id}: Accident | RoadProperties) {
return `${state}-${type}-${id}`;
}
function aggregateAccidents(accidents?: Accident[]) {
const incidents: {[year: number]: Record<string, number>} = {};
const fatalities: {[year: number]: Record<string, number>} = {};
if (accidents) {
for (const a of accidents) {
const r = (incidents[a.year] = incidents[a.year] || {});
const f = (fatalities[a.year] = fatalities[a.year] || {});
const key = getKey(a);
r[key] = a.incidents;
f[key] = a.fatalities;
}
}
return {incidents, fatalities};
}
const year = 1990;
export const getCustomPathsLayer = async (map: YMap): Promise<CustomLayerDescription> => {
const accidents: Accident[] = await fetch(DATA_URL.ACCIDENTS)
.then((response) => response.text())
.then((text) => {
const lines = text.trim().split('\n');
const fields = lines[0].split(',');
const data = [];
for (let i = 1; i < lines.length; i++) {
const acc = lines[i].split(',').reduce((acc, val, idx) => {
acc[fields[idx]] = /^\d+$/.test(val) ? parseInt(val, 10) : val;
return acc;
}, {} as Record<string, string | number>);
data.push(acc);
}
return data as unknown as Accident[];
});
const {incidents, fatalities} = aggregateAccidents(accidents);
const eventEmitter = new EventEmitter();
class CustomVectorDeckLayer extends DeckGlCustomLayer<{year: number}> {
constructor(
gl: WebGLRenderingContext,
options: {
requestRender: () => void;
}
) {
super({
map,
eventEmitter,
gl,
options,
props: {
year
}
});
}
protected override _getTooltip({object}: PickingInfo) {
if (!object) {
return null;
}
const props = object.properties;
const key = getKey(props);
const f = fatalities[this._props.year][key];
const r = incidents[this._props.year][key];
const content = r
? `<div>
<b>${f}</b> people died in <b>${r}</b> crashes on
${props.type === 'SR' ? props.state : props.type}-${props.id} in <b>${this._props.year}</b>
</div>`
: `<div>
no accidents recorded in <b>${this._props.year}</b>
</div>`;
return `<big>
${props.name} (${props.state})
</big>
${content}`;
}
protected override _getDeckGlLayers() {
return [
new GeoJsonLayer<RoadProperties>({
id: 'geojson',
data: DATA_URL.ROADS,
stroked: false,
filled: false,
lineWidthMinPixels: 0.5,
onHover: (info) => {
console.log(info);
},
getLineColor: (f: Road) => {
if (!fatalities[this._props.year]) {
return [200, 200, 200];
}
const key = getKey(f.properties);
const fatalitiesPer1KMile =
((fatalities[this._props.year][key] || 0) / f.properties.length) * 1000;
return COLOR_SCALE(fatalitiesPer1KMile);
},
getLineWidth: (f: Road) => {
if (!incidents[this._props.year]) {
return 10;
}
const key = getKey(f.properties);
const incidentsPer1KMile =
((incidents[this._props.year][key] || 0) / f.properties.length) * 1000;
return WIDTH_SCALE(incidentsPer1KMile);
},
pickable: true,
updateTriggers: {
getLineColor: {year: this._props.year},
getLineWidth: {year: this._props.year}
},
onDataLoad: () => {
eventEmitter.emit('ready');
this._requestRender();
},
transitions: {
getLineColor: 1000,
getLineWidth: 1000
}
})
];
}
}
return {Layer: CustomVectorDeckLayer, eventEmitter};
};
import type {YMap} from '@yandex/ymaps3-types';
import type {LayersList} from '@deck.gl/core/typed';
import {HeatmapLayer} from '@deck.gl/aggregation-layers/typed';
import {DeckGlCustomLayer} from '../common';
import {EventEmitter} from '../event-emitter';
import {CustomLayerDescription} from '../interface';
/**
* Forked from deck.gl example:
* https://github.com/visgl/deck.gl/blob/9.0-release/examples/website/heatmap/
*/
const DATA_URL =
'https://yastatic.net/s3/front-maps-static/maps-front-jsapi-3/examples/deck-example/uber-pickup-locations.json'; // eslint-disable-line
type DataPoint = [longitude: number, latitude: number, count: number];
const intensity = 1,
threshold = 0.03,
radiusPixels = 30;
interface CustomHeatmapVectorDeckLayerProps {
intensity: number;
threshold: number;
radiusPixels: number;
}
export async function getCustomHeatmapLayer(map: YMap): Promise<CustomLayerDescription> {
const eventEmitter = new EventEmitter();
class CustomHeatmapVectorDeckLayer extends DeckGlCustomLayer<CustomHeatmapVectorDeckLayerProps> {
constructor(
gl: WebGLRenderingContext,
options: {
requestRender: () => void;
}
) {
super({
map,
eventEmitter,
gl,
options,
props: {
intensity,
threshold,
radiusPixels
}
});
}
protected override _getDeckGlLayers() {
return [
new HeatmapLayer<DataPoint>({
data: DATA_URL,
id: 'heatmap-layer',
pickable: false,
getPosition: (d: any) => [d[0], d[1]],
getWeight: (d: any) => d[2],
radiusPixels: this._props.radiusPixels,
intensity: this._props.intensity,
threshold: this._props.threshold,
onDataLoad: () => {
eventEmitter.emit('ready');
this._requestRender();
}
})
] as unknown as LayersList;
}
}
return {Layer: CustomHeatmapVectorDeckLayer, eventEmitter};
}
import {AmbientLight, PointLight, LightingEffect, Layer, type PickingInfo} from '@deck.gl/core/typed';
import {HexagonLayer} from '@deck.gl/aggregation-layers/typed';
import type {YMap} from '@yandex/ymaps3-types';
import {DeckGlCustomLayer} from '../common';
import type {Color} from '@deck.gl/core/typed';
import {EventEmitter} from '../event-emitter';
import {CustomLayerDescription} from '../interface';
/**
* Forked from deck.gl example:
* https://github.com/visgl/deck.gl/tree/9.0-release/examples/website/3d-heatmap
*/
// Source data CSV
const DATA_URL = 'https://yastatic.net/s3/front-maps-static/maps-front-jsapi-3/examples/deck-example/heatmap-data.csv'; // eslint-disable-line
const ambientLight = new AmbientLight({
color: [255, 255, 255],
intensity: 1.0
});
const pointLight1 = new PointLight({
color: [255, 255, 255],
intensity: 0.8,
position: [-0.144528, 49.739968, 80000]
});
const pointLight2 = new PointLight({
color: [255, 255, 255],
intensity: 0.8,
position: [-3.807751, 54.104682, 8000]
});
const lightingEffect = new LightingEffect({ambientLight, pointLight1, pointLight2});
export const colorRange: Color[] = [
[1, 152, 189],
[73, 227, 206],
[216, 254, 181],
[254, 237, 177],
[254, 173, 84],
[209, 55, 78]
];
type DataPoint = [longitude: number, latitude: number];
type CustomHexagonVectorDeckLayerProps = {
coverage: number;
radius: number;
upperPercentile: number;
};
export async function getCustomHexagonLayer(map: YMap): Promise<CustomLayerDescription> {
const radius: number = 1000,
upperPercentile: number = 100,
coverage: number = 1;
const data: DataPoint[] = await fetch(DATA_URL)
.then((response) => response.text())
.then((text) => {
return text
.trim()
.split('\n')
.map((line) => {
const [lng, lat] = line.split(',').map(Number);
return [lng, lat];
});
});
const eventEmitter = new EventEmitter();
class CustomHexagonVectorDeckLayer extends DeckGlCustomLayer<CustomHexagonVectorDeckLayerProps> {
constructor(
gl: WebGLRenderingContext,
options: {
requestRender: () => void;
}
) {
super({
map,
eventEmitter,
gl,
options,
props: {
coverage,
radius,
upperPercentile
}
});
eventEmitter.emit('ready');
}
protected override _getTooltip({object}: PickingInfo) {
if (!object) {
return null;
}
const lat = object.position[1];
const lng = object.position[0];
const count = object.points.length;
return `
latitude: ${Number.isFinite(lat) ? lat.toFixed(6) : ''}<br>
longitude: ${Number.isFinite(lng) ? lng.toFixed(6) : ''}<br>
${count} Accidents`;
}
protected override _getDeckGlEffects() {
return [lightingEffect];
}
protected override _getDeckGlLayers() {
return [
new HexagonLayer<DataPoint>({
id: 'heatmap',
colorRange: colorRange,
coverage: this._props.coverage,
data: data,
elevationRange: [0, 3000],
elevationScale: data && data.length ? 50 : 0,
extruded: true,
getPosition: (d) => d,
pickable: true,
radius: this._props.radius,
upperPercentile: this._props.upperPercentile,
material: {
ambient: 0.64,
diffuse: 0.6,
shininess: 32,
specularColor: [51, 51, 51]
},
transitions: {
elevationScale: {
type: 'interpolation',
duration: 3000
}
}
})
] as unknown as Layer[];
}
}
return {Layer: CustomHexagonVectorDeckLayer, eventEmitter};
}
import {Layer} from '@deck.gl/core/typed';
import {Model, CubeGeometry, Texture2D} from '@luma.gl/core';
import type {
LngLat,
VectorLayerImplementation,
VectorLayerImplementationRenderProps,
YMap
} from '@yandex/ymaps3-types';
import {Matrix4} from '@math.gl/core';
import {DeckGlCustomLayer} from '../common';
import {EventEmitter} from '../event-emitter';
import type {CustomLayerDescription} from '../interface';
import viSLogo from '../assets/vis-logo.png';
type RenderView = VectorLayerImplementationRenderProps['worlds'][0];
const GLSL_VERTEX_SHADER = `\
attribute vec3 positions;
attribute vec2 texCoords;
uniform mat4 uViewProjMatrix;
uniform mat4 uModelMatrix;
uniform vec2 uLookAt;
varying vec2 vUV;
void main(void) {
vec4 position = uModelMatrix * vec4(positions.xyz, 1.0);
position.xy += uLookAt;
gl_Position = uViewProjMatrix * position;
vUV = texCoords;
}
`;
const GLSL_FRAGMENT_SHADER = `\
precision highp float;
uniform sampler2D uTexture;
varying vec2 vUV;
void main(void) {
gl_FragColor = texture2D(uTexture, vec2(vUV.x, 1.0 - vUV.y));
}
`;
class CubeLayer extends Layer<{id: string; modelMatrix: Matrix4; getRenderView: () => RenderView;}> {
constructor(props: {id: string; modelMatrix: Matrix4; getRenderView: () => RenderView;}) {
super(props);
}
initializeState() {
const {gl} = this.context;
this.setState({
model: this._getModel(gl)
});
}
draw() {
const {model} = this.state;
const {lookAt, viewProjMatrix} = this.props.getRenderView();
model
.setUniforms({
uLookAt: [-lookAt.x, -lookAt.y],
uModelMatrix: this.props.modelMatrix,
uViewProjMatrix: viewProjMatrix
})
.draw();
}
_getModel(gl: WebGLRenderingContext) {
const texture = new Texture2D(gl, {
data: viSLogo
});
return new Model(gl, {
vs: GLSL_VERTEX_SHADER,
fs: GLSL_FRAGMENT_SHADER,
id: this.props.id,
geometry: new CubeGeometry({
id: this.props.id
}),
uniforms: {
uTexture: texture
}
});
}
}
CubeLayer.layerName = 'CubeLayer';
export const getCustomCubeLayer = async (map: YMap): Promise<CustomLayerDescription> => {
const eventEmitter = new EventEmitter();
class CustomVectorDeckLayer extends DeckGlCustomLayer<{size: number}> {
constructor(
gl: WebGLRenderingContext,
options: {
requestRender: () => void;
}
) {
super({
map,
eventEmitter,
gl,
options,
props: {
size: 0.000004
}
});
eventEmitter.emit('ready');
}
private __view: RenderView;
override render(props: VectorLayerImplementationRenderProps): ReturnType<VectorLayerImplementation['render']> {
this.__view = props.worlds[0];
return super.render(props);
}
protected override _getDeckGlLayers() {
const move = this._map.projection.toWorldCoordinates(this._map.center as LngLat);
const modelMatrix = new Matrix4().translate([move.x, move.y, 0]).scale(this._props.size);
return [
new CubeLayer({
id: 'cube-layer',
modelMatrix,
getRenderView: () => {
return this.__view;
},
})
];
}
}
return {
Layer: CustomVectorDeckLayer,
eventEmitter
};
};
import type {LngLat, YMap} from '@yandex/ymaps3-types';
import type {CustomLayerDescription} from '../interface';
import {EventEmitter} from '../event-emitter';
import {DeckGlCustomLayer} from '../common';
import {ScenegraphLayer} from '@deck.gl/mesh-layers/typed';
import * as turf from '@turf/turf';
const seed = (s: number) => () => {
s = Math.sin(s) * 10000;
return s - Math.floor(s);
};
const GOOSE_MODEL = 'https://yastatic.net/s3/front-maps-static/maps-front-jsapi-3/examples/deck-example/goose.gltf';
// Part of Temza River
const polygon = turf.polygon([
[
[-0.12127519912525243, 51.494612955024714],
[-0.12437963183870979, 51.49486293088027],
[-0.12434347846320806, 51.49636153547172],
[-0.12412602613708126, 51.49787572356336],
[-0.12378136188540767, 51.49906724787776],
[-0.1233831627215626, 51.50051641282496],
[-0.12332189822869563, 51.5019019751426],
[-0.12318033136757142, 51.50367397677575],
[-0.12249762821910615, 51.50509001636788],
[-0.12207597427661632, 51.506184479810685],
[-0.1218132687319204, 51.50674767504787],
[-0.12051023115367045, 51.50803308684461],
[-0.11923451314441308, 51.50910910164905],
[-0.11741710673490778, 51.50990978355104],
[-0.11539439448322579, 51.51045215000003],
[-0.11270566856240793, 51.5108163535849],
[-0.11042687612091191, 51.51095529023991],
[-0.1083042715126757, 51.510965764282794],
[-0.10474489010654504, 51.510885142078884],
[-0.10470736646867146, 51.508806848879736],
[-0.1078658862072071, 51.50871857280208],
[-0.111751713195451, 51.50843908693093],
[-0.11555510321619353, 51.50756701656866],
[-0.116955001597116, 51.50698651255428],
[-0.11806855839783362, 51.50604571185182],
[-0.11923291893910992, 51.50471552566837],
[-0.12127519912525243, 51.494612955024714]
]
]);
type ModelPositionState = {
zOffset: number;
coordinates: LngLat;
};
const bbox = turf.bbox(polygon);
export const getCustomGLTFLayer = async (map: YMap): Promise<CustomLayerDescription> => {
const eventEmitter = new EventEmitter();
class CustomVectorDeckLayer extends DeckGlCustomLayer<{count: number; animation: number; size: number}> {
private __currentMousePosition: LngLat = map.center as LngLat;
constructor(
gl: WebGLRenderingContext,
options: {
requestRender: () => void;
}
) {
super({
map,
eventEmitter,
gl,
options,
props: {
count: 1000,
animation: 1,
size: 10
}
});
eventEmitter.emit('ready');
eventEmitter.on('mousemove', (coordinates: LngLat) => {
this.__currentMousePosition = coordinates;
this._deck.setProps({layers: this._getDeckGlLayers()});
});
}
protected override _getDeckGlLayers(): any[] {
const data: ModelPositionState[] = [];
const rnd = seed(10000);
for (let i = 0; i < this._props.count; i++) {
do {
const coordinates = [rnd() * (bbox[2] - bbox[0]) + bbox[0], rnd() * (bbox[3] - bbox[1]) + bbox[1]];
if (!turf.booleanPointInPolygon(turf.point(coordinates), polygon)) {
continue;
}
data.push({
zOffset: 0,
coordinates: coordinates as [number, number]
});
break;
} while (true);
}
return [
new ScenegraphLayer<ModelPositionState>({
id: 'ScenegraphLayer',
data,
getPosition: (d: ModelPositionState) => d.coordinates as [number, number, number],
getTranslation: (d: ModelPositionState) => [0, 0, d.zOffset],
getOrientation: (d: ModelPositionState) => {
const start = map.projection.toWorldCoordinates(d.coordinates);
const end = map.projection.toWorldCoordinates(this.__currentMousePosition);
const angleRadians = Math.atan2(end.y - start.y, end.x - start.x);
return [0, (angleRadians * 180) / Math.PI + 180, 90];
},
scenegraph: GOOSE_MODEL,
sizeScale: this._props.size
})
];
}
}
return {
Layer: CustomVectorDeckLayer,
eventEmitter
};
};
export class EventEmitter {
private _events: Map<string, Set<Function>> = new Map();
on(event: string, callback: Function): this {
if (!this._events.has(event)) {
this._events.set(event, new Set());
}
this._events.get(event)!.add(callback);
this.__emitEvent('onAddListener:' + event);
this._events.delete('onAddListener:' + event);
return this;
}
off(event: string, callback: Function) {
this._events.get(event)?.delete(callback);
}
private __emitEvent(event: string, ...args: any[]): void {
this._events.get(event)?.forEach((callback) => callback(...args));
}
emit(event: string, ...args: any[]): void {
if (!this._events.has(event) || !this._events.get(event)?.size) {
this.on('onAddListener:' + event, () => this.__emitEvent(event, ...args));
return;
}
this.__emitEvent(event, ...args);
}
}