Интеграция с 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 и запустите локально:

  1. Выполните npm ci
  2. Выполните npm start
  3. Откройте 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);
    }
}