Integration with Decl GL

Open in CodeSandbox

Deck.gl is a WebGL-powered framework for visual exploratory data analysis of large datasets.
It is built on top of the popular React library and is designed to work with different JS map API.

This example demonstrates how to integrate deck.gl with mappable.

Warning

Decl.gl version 9.0 only works on WebGL2RenderingContext, but the Mappable Maps JS API does not yet support it. Therefore, all examples on decl.gl version 8.0

Warning

To integrate Deck.GL, you'll need the 'Custom layer implementations' option. To enable it, contact support.

More information about custom layers is available in the documentation.

How to run

Download full sources from codesandbox and run it locally:

  1. Run npm ci
  2. Run npm start
  3. Open http://localhost:8081/ in your browser

How it works

The example uses deck.gl to render a layers near with vector layers from mappable as custom layers.

Deck.gl is drawn into the framebuffer, which is then used as a texture for the map layer by the mappable vector engine.

When initializing Deck, an experimental _framebuffer parameter can be set to render to the framebuffer.
It can be created using Framebuffer from 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()]
});

The common.ts file describes the abstract class DeckGlCustomLayer, which describes the logic for rendering with deck.gl and mappable inside framebuffer.
The remaining files are layer implementations for deck.gl.
For example, if you want to add a new layer, then you need to create a new file with the implementation of the layer.

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://static.mappable.world/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
      })
    ];
  }
}

Read more about how custom vector layers work in the documentation.

Since an instance of the DeckGlCustomLayer is created by the mappable vector engine, and we cannot influence the input parameters, we can use the EventEmitter pattern to pass parameters to the DeckGlCustomLayer.

import {DummyMapEngine} from './dummy-map-engine';

// You can take any EventEmitter from npm
declare class EventEmitter {
  on(event: string, listener: Function): void;
  emit(event: string, ...args: any[]): void;
}

export function getLayer(map: MMap) {
  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
      });

      // Subscribe to the parameters update event
      emitter.on('updateProps', (someProps: {opacity: number}) => {
        this.__someProps = someProps;
        this._deck.setProps({
          layers: this._getDeckGlLayers() // update layers
        });
      });
    }

    protected _getDeckGlLayers() {
      return [
        new GeoJsonLayer({
          id: 'geojson',
          data: DATA_URL,
          opacity: this.__someProps.opacity // Passing a new parameter to the layer
          // ...
        })
      ];
    }
  }

  return [MyCustomLayer, emitter];
}

Then, when creating a map and a custom layer, it will be possible to update the layer parameters from the outside.

import {getLayer} from './my-custom-layer';

const map = new mappable.MMap({...});

const [MyCustomLayer, emitter] = getLayer(map);

map.addChild(
  new mappable.MMapLayer({
    type: 'custom',
    grouppedWith: mappable.MMapDefaultSchemeLayer.defaultProps.source + ':buildings',
    implementation({type, effectiveMode}: any) {
      if (type === 'custom' && effectiveMode === 'vector') {
        return MyCustomLayer;
      }
    }
  })
);

// Later, somewhere in we update the layer parameters
setTimeout(() => {
  emitter.emit('updateProps', {opacity: 0.5});
}, 1000);

Examples

Heatmap

The example shows how to create a heatmap using the deck.gl library.
It's forked from the deck.gl example and adapted for mappable.

  • File: heatmap-layer.ts

Hexagons

This example shows how to create a hex grid layer using the deck.gl library and display it on the mappable map.
In the example, a csv file with data on accidents in the UK is loaded and processed.
It's forked from the deck.gl example and adapted for mappable.

  • File: hexagon-layer.ts

GeoJSON Polygons

The example shows how to create a layer with GeoJSON polygons showing the property values of Vancouver using the deck.gl library.
It's forked from the deck.gl example and adapted for mappable.

  • File: geojson-layer-polygons.ts

GeoJSON Paths

The example shows how to create a layer with GeoJSON paths showing fatal accidents on U.S. highways using the deck.gl library.
It's forked from the deck.gl example and adapted for mappable.

  • File: geojson-layer-paths.ts

Primitive cube

The example shows how to create a layer with a primitive cube using the deck.gl and luma.gl library.

  • File: cube-layer.ts

GLTF layer

The example shows how to create a layer with a GLTF model using the @deck.gl/mesh-layer and @turf/turf library.
In the example, terf is used to create many random points within a polygon enclosing the Thames channel,
which are then displayed on the map as a GLTF model.

  • File: gltf-layer.ts
import type {Camera} from '@mappable-world/mappable-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,
  MMap
} from '@mappable-world/mappable-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: MMap;

  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 mappable https://deck.gl/docs/api-reference/core/map-view#nearzmultiplier
  });

  constructor({
    props = {} as Props,
    map,
    gl,
    eventEmitter,
    options: {requestRender}
  }: {
    map: MMap;
    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 mappable
      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 mappable
     */
    // @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 mappable 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, MMap} from '@mappable-world/mappable-types';

import '../common.css';
import {customLayer, customStyle, ui} from '../variables';
import {BEHAVIORS} from '../variables';
import {addUI} from './ui';

declare global {
  interface Window {
    map: MMap | null;
  }
}

main();
async function main() {
  // Waiting for all api elements to be loaded
  await mappable.ready;
  const {MMap, MMapDefaultSchemeLayer} = mappable;
  const {SphericalMercator} = await mappable.import('@mappable-world/mappable-spherical-mercator-projection@0.0.1');

  const app = document.getElementById('app')!;
  app.classList.add('loading');

  // Initialize the map
  const map = new MMap(
    // 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 MMapDefaultSchemeLayer({
        customization: customStyle
      })
    ]
  );

  window.map = map;

  const {Layer, eventEmitter} = await customLayer(map);

  // Add a custom layer
  map.addChild(
    new mappable.MMapLayer({
      type: 'custom',
      grouppedWith: mappable.MMapDefaultSchemeLayer.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 mappable.MMapListener({
      onMouseMove: (obj: unknown, {coordinates}) => {
        eventEmitter.emit('mousemove', coordinates);
      }
    })
  );
}
import type {MMap} from '@mappable-world/mappable-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://static.mappable.world/s3/front-maps-static/maps-front-jsapi-3/examples/deck-example/vancouver-blocks.json'; // eslint-disable-line

export async function getCustomGeojsonPolygonsLayer(map: MMap): Promise<CustomLayerDescription> {
  const eventEmitter = new EventEmitter();

  class CustomPolygonVectorDeclLayer 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: CustomPolygonVectorDeclLayer, 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 {MMap} from '@mappable-world/mappable-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://static.mappable.world/s3/front-maps-static/maps-front-jsapi-3/examples/deck-example/accidents.csv',
  ROADS: 'https://static.mappable.world/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: MMap): 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 CustomVectorDeclLayer 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: CustomVectorDeclLayer, eventEmitter};
};
import type {MMap} from '@mappable-world/mappable-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://static.mappable.world/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 CustomHeatmapVectorDeclLayerProps {
  intensity: number;
  threshold: number;
  radiusPixels: number;
}

export async function getCustomHeatmapLayer(map: MMap): Promise<CustomLayerDescription> {
  const eventEmitter = new EventEmitter();

  class CustomHeatmapVectorDeclLayer extends DeckGlCustomLayer<CustomHeatmapVectorDeclLayerProps> {
    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: CustomHeatmapVectorDeclLayer, eventEmitter};
}
import {AmbientLight, PointLight, LightingEffect, Layer, type PickingInfo} from '@deck.gl/core/typed';
import {HexagonLayer} from '@deck.gl/aggregation-layers/typed';

import type {MMap} from '@mappable-world/mappable-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://static.mappable.world/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 CustomHexagonVectorDeclLayerProps = {
  coverage: number;
  radius: number;
  upperPercentile: number;
};

export async function getCustomHexagonLayer(map: MMap): 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 CustomHexagonVectorDeclLayer extends DeckGlCustomLayer<CustomHexagonVectorDeclLayerProps> {
    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: CustomHexagonVectorDeclLayer, eventEmitter};
}
import {Layer} from '@deck.gl/core/typed';
import {Model, CubeGeometry, Texture2D} from '@luma.gl/core';
import type {
  LngLat,
  VectorLayerImplementation,
  VectorLayerImplementationRenderProps,
  MMap
} from '@mappable-world/mappable-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: MMap): Promise<CustomLayerDescription> => {
  const eventEmitter = new EventEmitter();

  class CustomVectorDeclLayer 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: CustomVectorDeclLayer,
    eventEmitter
  };
};
import type {LngLat, MMap} from '@mappable-world/mappable-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://static.mappable.world/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: MMap): Promise<CustomLayerDescription> => {
  const eventEmitter = new EventEmitter();

  class CustomVectorDeclLayer 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: CustomVectorDeclLayer,
    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);
  }
}