Skip to content
Add a Simple Custom Layer on a Globe

Add a Simple Custom Layer on a Globe

Use a custom layer to draw simple WebGL content on a globe. Click the toggle button to switch between globe and Mercator projection.

const API_KEY = 'toursprung';

    const map = new maptoolkit.Map({
        container: 'map',
        style: `https://static.maptoolkit.net/styles/toursprung/terrain.json?api_key=${API_KEY}`,
        zoom: 3,
        center: [10.0, 52.0],
        attributionControl: { compact: false },
        canvasContextAttributes: { antialias: true }
    });

    map.addControl(new maptoolkit.NavigationControl(), 'top-right');

    map.on('style.load', () => {
        map.setProjection({ type: 'globe' });
    });

    document.getElementById('project').addEventListener('click', () => {
        const currentProjection = map.getProjection();
        map.setProjection({
            type: currentProjection.type === 'globe' ? 'mercator' : 'globe',
        });
    });

    const highlightLayer = {
        id: 'highlight',
        type: 'custom',
        shaderMap: new Map(),

        getShader(gl, shaderDescription) {
            if (this.shaderMap.has(shaderDescription.variantName)) {
                return this.shaderMap.get(shaderDescription.variantName);
            }

            const vertexSource = `#version 300 es
            ${shaderDescription.vertexShaderPrelude}
            ${shaderDescription.define}
            in vec2 a_pos;
            void main() {
                gl_Position = projectTile(a_pos);
            }`;

            const fragmentSource = `#version 300 es
            out highp vec4 fragColor;
            void main() {
                fragColor = vec4(1.0, 0.0, 1.0, 0.75);
            }`;

            const vertexShader = gl.createShader(gl.VERTEX_SHADER);
            gl.shaderSource(vertexShader, vertexSource);
            gl.compileShader(vertexShader);

            const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER);
            gl.shaderSource(fragmentShader, fragmentSource);
            gl.compileShader(fragmentShader);

            const program = gl.createProgram();
            gl.attachShader(program, vertexShader);
            gl.attachShader(program, fragmentShader);
            gl.linkProgram(program);

            this.aPos = gl.getAttribLocation(program, 'a_pos');
            this.shaderMap.set(shaderDescription.variantName, program);
            return program;
        },

        onAdd(map, gl) {
            const helsinki = maptoolkit.MercatorCoordinate.fromLngLat({ lng: 25.004, lat: 60.239 });
            const berlin = maptoolkit.MercatorCoordinate.fromLngLat({ lng: 13.403, lat: 52.562 });
            const kyiv = maptoolkit.MercatorCoordinate.fromLngLat({ lng: 30.498, lat: 50.541 });

            this.buffer = gl.createBuffer();
            gl.bindBuffer(gl.ARRAY_BUFFER, this.buffer);
            gl.bufferData(
                gl.ARRAY_BUFFER,
                new Float32Array([helsinki.x, helsinki.y, kyiv.x, kyiv.y, berlin.x, berlin.y]),
                gl.STATIC_DRAW
            );
        },

        render(gl, args) {
            const program = this.getShader(gl, args.shaderData);
            gl.useProgram(program);
            gl.uniformMatrix4fv(gl.getUniformLocation(program, 'u_projection_fallback_matrix'), false, args.defaultProjectionData.fallbackMatrix);
            gl.uniformMatrix4fv(gl.getUniformLocation(program, 'u_projection_matrix'), false, args.defaultProjectionData.mainMatrix);
            gl.uniform4f(gl.getUniformLocation(program, 'u_projection_tile_mercator_coords'), ...args.defaultProjectionData.tileMercatorCoords);
            gl.uniform4f(gl.getUniformLocation(program, 'u_projection_clipping_plane'), ...args.defaultProjectionData.clippingPlane);
            gl.uniform1f(gl.getUniformLocation(program, 'u_projection_transition'), args.defaultProjectionData.projectionTransition);

            gl.bindBuffer(gl.ARRAY_BUFFER, this.buffer);
            gl.enableVertexAttribArray(this.aPos);
            gl.vertexAttribPointer(this.aPos, 2, gl.FLOAT, false, 0, 0);
            gl.enable(gl.BLEND);
            gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
            gl.drawArrays(gl.TRIANGLE_STRIP, 0, 3);
        }
    };

    map.on('load', () => {
        map.addLayer(highlightLayer);
    });
<!DOCTYPE html>
<html lang="en">
<head>
    <title>Add a Simple Custom Layer on a Globe – Maptoolkit Maps JS</title>
    <meta property="og:description" content="Use a custom layer to draw simple WebGL content on a globe." />
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <script src="https://unpkg.com/@maptoolkit/maps@11.0.0-beta.2/dist/maptoolkit.js"></script>
    <link rel="stylesheet" href="https://unpkg.com/@maptoolkit/maps@11.0.0-beta.2/dist/maptoolkit.css" />
    <style>
        html, body { width: 100%; height: 100%; margin: 0; padding: 0; }
        #map { width: 100%; height: 100%; }
        #project {
            display: block;
            position: absolute;
            top: 20px;
            left: 50%;
            transform: translate(-50%);
            padding: 10px 20px;
            border: none;
            border-radius: 3px;
            font-size: 12px;
            color: #fff;
            background: #ee8a65;
            cursor: pointer;
        }
    </style>
</head>
<body>
<div id="map"></div>
<button id="project">Toggle projection</button>
<script type="module">
    const API_KEY = 'toursprung';

    const map = new maptoolkit.Map({
        container: 'map',
        style: `https://static.maptoolkit.net/styles/toursprung/terrain.json?api_key=${API_KEY}`,
        zoom: 3,
        center: [10.0, 52.0],
        attributionControl: { compact: false },
        canvasContextAttributes: { antialias: true }
    });

    map.addControl(new maptoolkit.NavigationControl(), 'top-right');

    map.on('style.load', () => {
        map.setProjection({ type: 'globe' });
    });

    document.getElementById('project').addEventListener('click', () => {
        const currentProjection = map.getProjection();
        map.setProjection({
            type: currentProjection.type === 'globe' ? 'mercator' : 'globe',
        });
    });

    const highlightLayer = {
        id: 'highlight',
        type: 'custom',
        shaderMap: new Map(),

        getShader(gl, shaderDescription) {
            if (this.shaderMap.has(shaderDescription.variantName)) {
                return this.shaderMap.get(shaderDescription.variantName);
            }

            const vertexSource = `#version 300 es
            ${shaderDescription.vertexShaderPrelude}
            ${shaderDescription.define}
            in vec2 a_pos;
            void main() {
                gl_Position = projectTile(a_pos);
            }`;

            const fragmentSource = `#version 300 es
            out highp vec4 fragColor;
            void main() {
                fragColor = vec4(1.0, 0.0, 1.0, 0.75);
            }`;

            const vertexShader = gl.createShader(gl.VERTEX_SHADER);
            gl.shaderSource(vertexShader, vertexSource);
            gl.compileShader(vertexShader);

            const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER);
            gl.shaderSource(fragmentShader, fragmentSource);
            gl.compileShader(fragmentShader);

            const program = gl.createProgram();
            gl.attachShader(program, vertexShader);
            gl.attachShader(program, fragmentShader);
            gl.linkProgram(program);

            this.aPos = gl.getAttribLocation(program, 'a_pos');
            this.shaderMap.set(shaderDescription.variantName, program);
            return program;
        },

        onAdd(map, gl) {
            const helsinki = maptoolkit.MercatorCoordinate.fromLngLat({ lng: 25.004, lat: 60.239 });
            const berlin = maptoolkit.MercatorCoordinate.fromLngLat({ lng: 13.403, lat: 52.562 });
            const kyiv = maptoolkit.MercatorCoordinate.fromLngLat({ lng: 30.498, lat: 50.541 });

            this.buffer = gl.createBuffer();
            gl.bindBuffer(gl.ARRAY_BUFFER, this.buffer);
            gl.bufferData(
                gl.ARRAY_BUFFER,
                new Float32Array([helsinki.x, helsinki.y, kyiv.x, kyiv.y, berlin.x, berlin.y]),
                gl.STATIC_DRAW
            );
        },

        render(gl, args) {
            const program = this.getShader(gl, args.shaderData);
            gl.useProgram(program);
            gl.uniformMatrix4fv(gl.getUniformLocation(program, 'u_projection_fallback_matrix'), false, args.defaultProjectionData.fallbackMatrix);
            gl.uniformMatrix4fv(gl.getUniformLocation(program, 'u_projection_matrix'), false, args.defaultProjectionData.mainMatrix);
            gl.uniform4f(gl.getUniformLocation(program, 'u_projection_tile_mercator_coords'), ...args.defaultProjectionData.tileMercatorCoords);
            gl.uniform4f(gl.getUniformLocation(program, 'u_projection_clipping_plane'), ...args.defaultProjectionData.clippingPlane);
            gl.uniform1f(gl.getUniformLocation(program, 'u_projection_transition'), args.defaultProjectionData.projectionTransition);

            gl.bindBuffer(gl.ARRAY_BUFFER, this.buffer);
            gl.enableVertexAttribArray(this.aPos);
            gl.vertexAttribPointer(this.aPos, 2, gl.FLOAT, false, 0, 0);
            gl.enable(gl.BLEND);
            gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
            gl.drawArrays(gl.TRIANGLE_STRIP, 0, 3);
        }
    };

    map.on('load', () => {
        map.addLayer(highlightLayer);
    });
</script>
</body>
</html>