Heatmap on custom layers

Open in CodeSandbox

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1" />
    <script crossorigin src="https://cdn.jsdelivr.net/npm/@babel/standalone@7/babel.min.js"></script>
    <!-- To make the map appear, you must add your apikey -->
    <script src="https://js.api.mappable.world/v3/?apikey=<YOUR_APIKEY>&lang=en_US" type="text/javascript"></script>
    <script
      data-plugins="transform-modules-umd"
      data-presets="typescript"
      type="text/babel"
      src="../variables.ts"
    ></script>
    <script
      data-plugins="transform-modules-umd"
      data-presets="typescript"
      type="text/babel"
      src="./common.ts"
    ></script>
    <script
      data-plugins="transform-modules-umd"
      data-presets="typescript"
      type="text/babel"
      src="../heatmap-gl-impl.ts"
    ></script>
    <script data-plugins="transform-modules-umd" data-presets="typescript" type="text/babel">
      import {getRandomPoints, getDefaultMapProps} from './common';
      import {getHeatmapImpl} from '../heatmap-gl-impl';

      window.map = null;

      main();
      async function main() {
        // Waiting for all api elements to be loaded
        await mappable.ready;
        const {MMap, MMapDefaultSchemeLayer, MMapLayer} = mappable;

        const {location: defaultLocation, projection: defaultProjection} = getDefaultMapProps();

        // Initialize the map
        map = new MMap(
          // Pass the link to the HTMLElement of the container
          document.getElementById('app'),
          // Pass the map initialization parameters
          {location: defaultLocation, showScaleInCopyrights: true, projection: defaultProjection},
          // Add a map scheme layer
          [new MMapDefaultSchemeLayer({})]
        );

        const points = getRandomPoints(1500);

        // Add a custom layer to the map
        map.addChild(
          new MMapLayer({
            id: 'heatmap',
            type: 'custom',
            zIndex: 1201,
            grouppedWith: `${MMapDefaultSchemeLayer.defaultProps.source}:buildings`,
            source: MMapDefaultSchemeLayer.defaultProps.source,
            implementation: ({effectiveMode}) =>
              effectiveMode === 'vector' ? getHeatmapImpl(defaultProjection, () => points) : undefined
          })
        );
      }
    </script>

    <!-- prettier-ignore -->
    <style> html, body, #app { width: 100%; height: 100%; margin: 0; padding: 0; font-family: Arial, Helvetica, sans-serif; } .toolbar { position: absolute; z-index: 1000; top: 0; left: 0; display: flex; align-items: center; padding: 16px; } .toolbar a { padding: 16px; }  </style>
  </head>
  <body>
    <div id="app"></div>
  </body>
</html>
<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1" />
    <script crossorigin src="https://cdn.jsdelivr.net/npm/react@17/umd/react.production.min.js"></script>
    <script crossorigin src="https://cdn.jsdelivr.net/npm/react-dom@17/umd/react-dom.production.min.js"></script>
    <script crossorigin src="https://cdn.jsdelivr.net/npm/@babel/standalone@7/babel.min.js"></script>
    <!-- To make the map appear, you must add your apikey -->
    <script src="https://js.api.mappable.world/v3/?apikey=<YOUR_APIKEY>&lang=en_US" type="text/javascript"></script>
    <script
      data-plugins="transform-modules-umd"
      data-presets="typescript"
      type="text/babel"
      src="../variables.ts"
    ></script>
    <script
      data-plugins="transform-modules-umd"
      data-presets="react, typescript"
      type="text/babel"
      src="./common.ts"
    ></script>
    <script
      data-plugins="transform-modules-umd"
      data-presets="typescript"
      type="text/babel"
      src="../heatmap-gl-impl.ts"
    ></script>
    <script data-plugins="transform-modules-umd" data-presets="react, typescript" type="text/babel">
      import {getDefaultMapProps, getRandomPoints} from './common';
      import {getHeatmapImpl} from '../heatmap-gl-impl';

      window.map = null;

      main();
      async function main() {
        // For each object in the JS API, there is a React counterpart
        // To use the React version of the API, include the module @mappable-world/mappable-reactify
        const [mappableReact] = await Promise.all([
          mappable.import('@mappable-world/mappable-reactify'),
          mappable.ready
        ]);
        const reactify = mappableReact.reactify.bindTo(React, ReactDOM);
        const {MMap, MMapDefaultSchemeLayer, MMapLayer} = reactify.module(mappable);
        const {useState} = React;

        const points = getRandomPoints(1500);

        const {location: defaultLocation, projection: defaultProjection} = getDefaultMapProps();

        function App() {
          const [location, setLocation] = useState(defaultLocation);

          return (
            // Initialize the map and pass initialization parameters
            <MMap
              location={location}
              projection={defaultProjection}
              showScaleInCopyrights={true}
              ref={(x) => (map = x)}
            >
              {/* Add a map scheme layer */}
              <MMapDefaultSchemeLayer />
              <MMapLayer
                id={'heatmap'}
                type={'custom'}
                zIndex={1201}
                grouppedWith={mappable.MMapDefaultSchemeLayer.defaultProps.source + ':buildings'}
                source={mappable.MMapDefaultSchemeLayer.defaultProps.source}
                implementation={React.useCallback(
                  ({effectiveMode}) =>
                    effectiveMode === 'vector' ? getHeatmapImpl(defaultProjection, () => points) : undefined,
                  []
                )}
              />
            </MMap>
          );
        }

        ReactDOM.render(
          <React.StrictMode>
            <App />
          </React.StrictMode>,
          document.getElementById('app')
        );
      }
    </script>

    <!-- prettier-ignore -->
    <style> html, body, #app { width: 100%; height: 100%; margin: 0; padding: 0; font-family: Arial, Helvetica, sans-serif; } .toolbar { position: absolute; z-index: 1000; top: 0; left: 0; display: flex; align-items: center; padding: 16px; } .toolbar a { padding: 16px; }  </style>
  </head>
  <body>
    <div id="app"></div>
  </body>
</html>
<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1" />
    <script crossorigin src="https://cdn.jsdelivr.net/npm/vue@3/dist/vue.global.js"></script>
    <script crossorigin src="https://cdn.jsdelivr.net/npm/@babel/standalone@7/babel.min.js"></script>

    <script src="https://js.api.mappable.world/v3/?apikey=<YOUR_APIKEY>&lang=en_US" type="text/javascript"></script>
    <script
      data-plugins="transform-modules-umd"
      data-presets="typescript"
      type="text/babel"
      src="../variables.ts"
    ></script>
    <script
      data-plugins="transform-modules-umd"
      data-presets="typescript"
      type="text/babel"
      src="./common.ts"
    ></script>
    <script
      data-plugins="transform-modules-umd"
      data-presets="typescript"
      type="text/babel"
      src="../heatmap-gl-impl.ts"
    ></script>
    <script data-plugins="transform-modules-umd" data-presets="typescript" type="text/babel">
      import {getDefaultMapProps, getRandomPoints} from './common';
      import {getHeatmapImpl} from '../heatmap-gl-impl';

      window.map = null;

      async function main() {
        const [mappableVue] = await Promise.all([mappable.import('@mappable-world/mappable-vuefy'), mappable.ready]);
        const vuefy = mappableVue.vuefy.bindTo(Vue);
        const {MMap, MMapDefaultSchemeLayer, MMapControls, MMapLayer} = vuefy.module(mappable);
        const {MMapZoomControl} = vuefy.module(await mappable.import('@mappable-world/mappable-controls@0.0.1'));

        const {location: defaultLocation, projection: defaultProjection} = getDefaultMapProps();

        const points = getRandomPoints(1500);

        const app = Vue.createApp({
          components: {MMap, MMapDefaultSchemeLayer, MMapControls, MMapZoomControl, MMapLayer},
          setup() {
            const refMap = (ref) => {
              window.map = ref?.entity;
            };
            return {
              LOCATION: defaultLocation,
              refMap,
              projection: defaultProjection,
              implementation: ({effectiveMode}) => {
                return effectiveMode === 'vector' ? getHeatmapImpl(defaultProjection, () => points) : undefined;
              }
            };
          },
          template: `
            <MMap :location="LOCATION" :showScaleInCopyrights="true" :ref="refMap" :projection="projection">
                <MMapDefaultSchemeLayer />
                <MMapLayer
                    id="heatmap"
                    type="custom"
                    :zIndex=1201
                    grouppedWith="${mappable.MMapDefaultSchemeLayer.defaultProps.source}:buildings"
                    source="${mappable.MMapDefaultSchemeLayer.defaultProps.source}"
                    :implementation="implementation"
                />
                <MMapControls position="right">
                    <MMapZoomControl></MMapZoomControl>
                </MMapControls>
            </MMap>`
        });
        app.mount('#app');
      }
      main();
    </script>

    <!-- prettier-ignore -->
    <style> html, body, #app { width: 100%; height: 100%; margin: 0; padding: 0; font-family: Arial, Helvetica, sans-serif; } .toolbar { position: absolute; z-index: 1000; top: 0; left: 0; display: flex; align-items: center; padding: 16px; } .toolbar a { padding: 16px; }  </style>
  </head>
  <body>
    <div id="app"></div>
  </body>
</html>
import type {LngLat} from '@mappable-world/mappable-types';
import {LOCATION} from './variables';

export function getDefaultMapProps() {
  return {
    location: LOCATION,
    projection: mappable.projections.sphericalMercator
  };
}

const seed = (s) => () => {
  s = Math.sin(s) * 10000;
  return s - Math.floor(s);
};

export const rnd = seed(10000);

export type Point = {coordinates: LngLat; value: number};

function rndSign(): number {
  return rnd() > 0.5 ? 1 : -1;
}
export function getRandomPoints(count: number, delta: number = 1.5): Point[] {
  const result: Point[] = [];

  for (let i = 0; i < count; i++) {
    const coordinates: LngLat = [
      LOCATION.center[0] + rnd() * delta * rndSign(),
      LOCATION.center[1] + rnd() * delta * rndSign()
    ];
    const value = rnd() * 1;
    result.push({coordinates, value});
  }

  return result;
}
import type {
  PixelCoordinates,
  Projection,
  WorldCoordinates,
  VectorLayerImplementation,
  VectorLayerImplementationConstructor,
  VectorLayerImplementationRenderProps
} from '@mappable-world/mappable-types';
import type {Point} from './common';

type FrameBuffer = {
  color: WebGLTexture;
  depth: WebGLTexture;
  buffer: WebGLFramebuffer;
  size: PixelCoordinates;
};

type Program = {
  attributes: Record<string, number>;
  uniforms: Record<string, WebGLUniformLocation>;
  program: WebGLProgram;
  vs: WebGLShader;
  fs: WebGLShader;
  vao: WebGLVertexArrayObjectOES;
};
type RenderWorldProps = Omit<VectorLayerImplementationRenderProps, 'worlds'> & {
  world: VectorLayerImplementationRenderProps['worlds'][0];
};

type DrawBuffer = {
  count: number;
  buffer: WebGLBuffer;
};

export const getHeatmapImpl = (
  projection: Projection,
  getData: () => Point[],
  options: {
    gradient: MappedGradient;
    density: number;
    max: number;
    blur: number;
    size: number;
  } = {
    gradient: gradientMapper(defaultGradient),
    density: 4,
    max: 1,
    blur: 1,
    size: 8
  }
): VectorLayerImplementationConstructor => {
  return class {
    private _requestRender: () => void;

    private _mainFrameBuffer: FrameBuffer;
    private _gradientFrameBuffer: FrameBuffer;

    private _gradientProgram: Program;
    private _heatmapProgram: Program;

    private _vertexBuffer: DrawBuffer;
    private _intensityBuffer: DrawBuffer;
    private _heatmapVertexBuffer: DrawBuffer;
    private _heatmapTextCoordBuffer: DrawBuffer;

    constructor(
      private readonly gl: WebGLRenderingContext,
      options: {size: PixelCoordinates; requestRender: () => void}
    ) {
      this._requestRender = options.requestRender;

      // This one is used to main rendering
      this._mainFrameBuffer = this.__createFrameBuffer();
      // This one is used to render the heatmap
      this._gradientFrameBuffer = this.__createFrameBuffer();

      this._gradientProgram = createProgram(gl, GRADIENT_SHADERS.vertex, GRADIENT_SHADERS.fragment, {
        attributes: ['a_position', 'a_intensity'],
        uniforms: ['u_viewProjectionMatrix', 'u_lookAt', 'u_size', 'u_density', 'u_max', 'u_blur']
      });

      this._heatmapProgram = createProgram(gl, HEATMAP_SHADERS.vertex, HEATMAP_SHADERS.fragment, {
        attributes: ['a_position', 'a_texCoord'],
        uniforms: ['u_gradient', 'u_colorArr', 'u_offset', 'u_opacity']
      });

      const {vertexData, intensityData} = generateVertexData(projection, getData());

      // Buffers for gradient points rendering
      const vertexBuffer = gl.createBuffer();
      gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
      gl.bufferData(gl.ARRAY_BUFFER, vertexData, gl.STATIC_DRAW);
      this._vertexBuffer = {count: vertexData.length / 2, buffer: vertexBuffer};

      const intensityBuffer = gl.createBuffer();
      gl.bindBuffer(gl.ARRAY_BUFFER, intensityBuffer);
      gl.bufferData(gl.ARRAY_BUFFER, intensityData, gl.STATIC_DRAW);
      this._intensityBuffer = {count: intensityData.length, buffer: intensityBuffer};

      // Buffers for heatmap rendering
      const heatmapVertexBuffer = gl.createBuffer();
      gl.bindBuffer(gl.ARRAY_BUFFER, heatmapVertexBuffer);
      gl.bufferData(
        gl.ARRAY_BUFFER,
        new Float32Array([-1.0, -1.0, 1.0, -1.0, -1.0, 1.0, -1.0, 1.0, 1.0, -1.0, 1.0, 1.0]),
        gl.STATIC_DRAW
      );
      this._heatmapVertexBuffer = {count: 6, buffer: heatmapVertexBuffer};

      const heatmapTexCoordBuffer = gl.createBuffer();
      gl.bindBuffer(gl.ARRAY_BUFFER, heatmapTexCoordBuffer);
      gl.bufferData(
        gl.ARRAY_BUFFER,
        new Float32Array([0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 1.0, 1.0, 0.0, 1.0, 1.0]),
        gl.STATIC_DRAW
      );
      this._heatmapTextCoordBuffer = {count: 6, buffer: heatmapTexCoordBuffer};
    }

    /**
     * Create frame buffer with color and depth textures
     */
    private __createFrameBuffer() {
      const {gl} = this;

      const buffer = gl.createFramebuffer();
      const colorTexture = gl.createTexture();
      const depthTexture = gl.createTexture();

      gl.bindTexture(gl.TEXTURE_2D, colorTexture);
      gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
      gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
      gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
      gl.bindTexture(gl.TEXTURE_2D, null);

      gl.getExtension('WEBGL_depth_texture');
      gl.bindTexture(gl.TEXTURE_2D, depthTexture);
      gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
      gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
      gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);

      gl.bindFramebuffer(gl.FRAMEBUFFER, buffer);
      gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, colorTexture, 0);
      gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.TEXTURE_2D, depthTexture, 0);

      return {
        color: colorTexture,
        depth: depthTexture,
        buffer,
        size: {x: 0, y: 0}
      };
    }

    destroy() {
      const {gl} = this;
      for (const buffer of [this._mainFrameBuffer, this._gradientFrameBuffer]) {
        gl.deleteFramebuffer(buffer.buffer);
        gl.deleteTexture(buffer.color);
        gl.deleteTexture(buffer.depth);
      }

      for (const program of [this._gradientProgram, this._heatmapProgram]) {
        gl.deleteProgram(program.program);
        gl.deleteShader(program.vs);
        gl.deleteShader(program.fs);
        gl.getExtension('OES_vertex_array_object').deleteVertexArrayOES(program.vao);
      }

      for (const buffer of [
        this._vertexBuffer,
        this._intensityBuffer,
        this._heatmapVertexBuffer,
        this._heatmapTextCoordBuffer
      ]) {
        gl.deleteBuffer(buffer.buffer);
      }
    }

    render({size, worlds}: Parameters<VectorLayerImplementation['render']>[0]) {
      const {gl} = this;

      for (const {lookAt, viewProjMatrix} of worlds) {
        this.__renderGradient(size, lookAt, viewProjMatrix);
        this.__renderHeatmap(size);
      }

      gl.bindFramebuffer(gl.FRAMEBUFFER, null);
      gl.bindBuffer(gl.ARRAY_BUFFER, null);
      gl.bindTexture(gl.TEXTURE_2D, null);
      gl.useProgram(null);
      gl.getExtension('OES_vertex_array_object').bindVertexArrayOES(null);

      return {
        color: this._mainFrameBuffer.color,
        depth: this._mainFrameBuffer.depth
      };
    }

    /**
     * Bind frame buffer and set viewport size
     */
    private __bindFrameBuffer(size: PixelCoordinates, fb: FrameBuffer) {
      const {gl} = this;
      gl.bindFramebuffer(gl.FRAMEBUFFER, fb.buffer);
      gl.viewport(0, 0, size.x, size.y);

      if (fb.size.x === size.x && fb.size.y === size.y) {
        return;
      }

      fb.size = size;
      gl.bindTexture(gl.TEXTURE_2D, fb.color);
      gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, size.x, size.y, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);
      gl.bindTexture(gl.TEXTURE_2D, fb.depth);
      gl.texImage2D(
        gl.TEXTURE_2D,
        0,
        gl.DEPTH_COMPONENT,
        size.x,
        size.y,
        0,
        gl.DEPTH_COMPONENT,
        gl.UNSIGNED_INT,
        null
      );
    }

    /**
     * Drawing points in the form of circles with blur
     */
    private __renderGradient(
      size: PixelCoordinates,
      lookAt: WorldCoordinates,
      viewProjMatrix: RenderWorldProps['world']['viewProjMatrix']
    ) {
      const {gl} = this;

      this.__bindFrameBuffer(size, this._gradientFrameBuffer);

      gl.disable(gl.DEPTH_TEST);
      gl.enable(gl.BLEND);
      gl.blendEquation(gl.FUNC_ADD);
      gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);

      gl.clearColor(0, 0, 0, 0);
      gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);

      const {vao, program, attributes, uniforms} = this._gradientProgram;
      gl.useProgram(program);

      gl.getExtension('OES_vertex_array_object').bindVertexArrayOES(vao);

      gl.uniformMatrix4fv(uniforms.u_viewProjectionMatrix, false, viewProjMatrix as Float32List);
      gl.uniform2f(uniforms.u_lookAt, -lookAt.x, -lookAt.y);

      gl.uniform1f(uniforms.u_density, options.density);
      gl.uniform1f(uniforms.u_max, options.max);
      gl.uniform1f(uniforms.u_size, options.size);
      gl.uniform1f(uniforms.u_blur, options.blur);

      // We can optimize this and set only once but for example purposes we set it every time
      // https://developer.mozilla.org/en-US/docs/Web/API/WebGL_API/WebGL_best_practices#avoid_changing_vao_attachments_vertexattribpointer_disableenablevertexattribarray
      gl.bindBuffer(gl.ARRAY_BUFFER, this._vertexBuffer.buffer);
      gl.enableVertexAttribArray(attributes.a_position);
      gl.vertexAttribPointer(attributes.a_position, 2, gl.FLOAT, false, 0, 0);

      gl.bindBuffer(gl.ARRAY_BUFFER, this._intensityBuffer.buffer);
      gl.enableVertexAttribArray(attributes.a_intensity);
      gl.vertexAttribPointer(attributes.a_intensity, 1, gl.FLOAT, false, 0, 0);

      gl.drawArrays(gl.POINTS, 0, this._vertexBuffer.count);
    }

    /**
     * Draw a rectangle on the entire screen and apply a texture from the framebuffer to it
     */
    private __renderHeatmap(size: PixelCoordinates) {
      const {gl} = this;
      const {vao, program, attributes, uniforms} = this._heatmapProgram;
      this.__bindFrameBuffer(size, this._mainFrameBuffer);

      gl.enable(gl.DEPTH_TEST);
      gl.clearColor(0, 0, 0, 0);
      gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);

      gl.useProgram(program);

      gl.getExtension('OES_vertex_array_object').bindVertexArrayOES(vao);

      gl.uniform4fv(uniforms.u_colorArr, options.gradient.value);
      gl.uniform1fv(uniforms.u_offset, new Float32Array(options.gradient.offset));
      gl.uniform1f(uniforms.u_opacity, 1);

      gl.activeTexture(gl.TEXTURE0 + 0);
      gl.bindTexture(gl.TEXTURE_2D, this._gradientFrameBuffer.color);
      gl.uniform1i(uniforms.u_gradient, 0);

      gl.bindBuffer(gl.ARRAY_BUFFER, this._heatmapVertexBuffer.buffer);
      gl.enableVertexAttribArray(attributes.a_position);
      gl.vertexAttribPointer(attributes.a_position, 2, gl.FLOAT, false, 0, 0);

      gl.bindBuffer(gl.ARRAY_BUFFER, this._heatmapTextCoordBuffer.buffer);
      gl.enableVertexAttribArray(attributes.a_texCoord);
      gl.vertexAttribPointer(attributes.a_texCoord, 2, gl.FLOAT, false, 0, 0);

      gl.drawArrays(gl.TRIANGLES, 0, 6);
    }
  };
};

/**
 * Create a program with vertex and fragment shaders
 */
function createProgram(
  gl: WebGLRenderingContext,
  vertexShader: string,
  fragmentShader: string,
  options: {
    attributes?: string[];
    uniforms?: string[];
  } = {}
): Program {
  const program = gl.createProgram()!;
  const vs = gl.createShader(gl.VERTEX_SHADER)!;
  const fs = gl.createShader(gl.FRAGMENT_SHADER)!;

  gl.shaderSource(vs, vertexShader);
  gl.compileShader(vs);
  gl.attachShader(program, vs);

  gl.shaderSource(fs, fragmentShader);
  gl.compileShader(fs);
  gl.attachShader(program, fs);

  gl.linkProgram(program);

  const attributes = {};
  for (const attribute of options.attributes ?? []) {
    attributes[attribute] = gl.getAttribLocation(program, attribute);
  }

  const uniforms = {};
  for (const uniform of options.uniforms ?? []) {
    uniforms[uniform] = gl.getUniformLocation(program, uniform);
  }

  return {
    program,
    vs,
    fs,
    attributes,
    uniforms,
    vao: gl.getExtension('OES_vertex_array_object').createVertexArrayOES()
  };
}

// The shader draws dots on the screen and, depending on the intensity, blurs from the center to the edge
const GRADIENT_SHADERS = {
  vertex: `
        attribute vec2 a_position;
        attribute float a_intensity;

        uniform mat4 u_viewProjectionMatrix;
        uniform vec2 u_lookAt;
        uniform float u_size;
        uniform float u_density;

        varying float v_intensity;

        void main() {
            vec4 offsetPosition = vec4(a_position + u_lookAt, 0.000003, 1);
            vec4 position = u_viewProjectionMatrix * offsetPosition;
            gl_Position = position;

            gl_PointSize = u_size * u_density;
            v_intensity = a_intensity;
        }
    `,
  fragment: `
        precision mediump float;

        uniform float u_max;
        uniform float u_blur;

        varying float v_intensity;

        void main() {
            float r = 0.0;
            vec2 cxy = 2.0 * gl_PointCoord - 1.0;
            r = dot(cxy, cxy);
            if(r <= 1.0) {
                gl_FragColor = vec4(0, 0, 0, (v_intensity/u_max) * u_blur * (1.0 - sqrt(r)));
            }
        }
    `
};

/**
 * A shader that simply draws a rectangle on the entire screen and overlays a texture from the frame buffer on it
 */
const HEATMAP_SHADERS = {
  vertex: `
        attribute vec2 a_position;
        attribute vec2 a_texCoord;

        varying vec2 v_texCoord;

        void main() {
            gl_Position = vec4(a_position, 0.000001, 1);
            v_texCoord = a_texCoord;
        }
    `,
  fragment: `
        precision mediump float;

        uniform sampler2D u_gradient;
        uniform vec4 u_colorArr[11];
        uniform float u_offset[11];
        uniform float u_opacity;

        varying vec2 v_texCoord;

        float remap ( float minval, float maxval, float curval ) {
            return ( curval - minval ) / ( maxval - minval );
        }

        void main() {
            float alpha = texture2D(u_gradient, v_texCoord.xy).a;

            if (alpha > 0.0 && alpha <= 1.0) {
                vec4 color;

                if (alpha <= u_offset[0]) {
                    color = u_colorArr[0];
                } else if (alpha <= u_offset[1]) {
                    color = mix( u_colorArr[0], u_colorArr[1], remap( u_offset[0], u_offset[1], alpha ) );
                } else if (alpha <= u_offset[2]) {
                    color = mix( u_colorArr[1], u_colorArr[2], remap( u_offset[1], u_offset[2], alpha ) );
                } else if (alpha <= u_offset[3]) {
                    color = mix( u_colorArr[2], u_colorArr[3], remap( u_offset[2], u_offset[3], alpha ) );
                } else if (alpha <= u_offset[4]) {
                    color = mix( u_colorArr[3], u_colorArr[4], remap( u_offset[3], u_offset[4], alpha ) );
                } else if (alpha <= u_offset[5]) {
                    color = mix( u_colorArr[4], u_colorArr[5], remap( u_offset[4], u_offset[5], alpha ) );
                } else if (alpha <= u_offset[6]) {
                    color = mix( u_colorArr[5], u_colorArr[6], remap( u_offset[5], u_offset[6], alpha ) );
                } else if (alpha <= u_offset[7]) {
                    color = mix( u_colorArr[6], u_colorArr[7], remap( u_offset[6], u_offset[7], alpha ) );
                } else if (alpha <= u_offset[8]) {
                    color = mix( u_colorArr[7], u_colorArr[8], remap( u_offset[7], u_offset[8], alpha ) );
                } else if (alpha <= u_offset[9]) {
                    color = mix( u_colorArr[8], u_colorArr[9], remap( u_offset[8], u_offset[9], alpha ) );
                } else if (alpha <= u_offset[10]) {
                    color = mix( u_colorArr[9], u_colorArr[10], remap( u_offset[9], u_offset[10], alpha ) );
                } else {
                    color = vec4(0.0, 0.0, 0.0, 0.0);
                }

                color.a = color.a - (1.0 - u_opacity);

                if (color.a < 0.0) {
                    discard;
                }

                gl_FragColor = color;
            }
        }
    `
};

export interface Gradient {
  color: [number, number, number, number];
  offset: number;
}

interface MappedGradient {
  value: Float32Array;
  length: number;
  offset: number[];
}

const defaultGradient: Gradient[] = [
  {
    color: [255, 255, 255, 0.0],
    offset: 0
  },
  {
    color: [212, 225, 255, 1.0],
    offset: 0.2
  },
  {
    color: [166, 255, 115, 1.0],
    offset: 0.45
  },
  {
    color: [255, 255, 0, 0.5],
    offset: 0.75
  },
  {
    color: [255, 0, 0, 1.0],
    offset: 1.0
  }
];

function gradientMapper(grad: Gradient[]): MappedGradient {
  const arr: number[] = [];
  const gradLength = grad.length;
  const offSetsArray: number[] = [];

  grad.forEach(function (d) {
    arr.push(d.color[0] / 255);
    arr.push(d.color[1] / 255);
    arr.push(d.color[2] / 255);
    arr.push(d.color[3] === undefined ? 1.0 : d.color[3]);
    offSetsArray.push(d.offset);
  });

  return {
    value: new Float32Array(arr),
    length: gradLength,
    offset: offSetsArray
  };
}

/**
 * Generate vertex and intensity float32 data arrays
 */
function generateVertexData(projection: Projection, data: Point[]) {
  const vertexData = new Float32Array(data.length * 2); // 2 floats per vertex x an y
  for (let i = 0; i < data.length; i++) {
    const {x, y} = projection.toWorldCoordinates(data[i].coordinates);
    vertexData[i * 2] = x;
    vertexData[i * 2 + 1] = y;
  }
  return {vertexData, intensityData: new Float32Array(data.map((p) => p.value))};
}
import type {MMapLocationRequest} from '@mappable-world/mappable-types';

export const LOCATION: MMapLocationRequest = {
  center: [55.2744, 25.1972], // starting position [lng, lat]
  zoom: 7 // starting zoom
};