Mapbox GL JS – Select an icon

Recently, I had a chance to play with Mapbox. I helped playmaps to sort out some issues with icons. At that time, I have spent some time to select an icon from a click. However, haven’t find any good example at that point. For this reason, I have decided to share the way how I manage to achieve selcting an icon from a click.

Mapbox

If you are reading this, I believe you are already aware of what Mapbox is. And, Mapbox GL JS is one of the client libraries to use Mapbox

Mapbox GL JS is a JavaScript library that uses WebGL to render interactive maps from vector tiles and Mapbox styles.

Mapbox GL JS API Reference

Here, you’ll find many examples to start with Mapbox GL JS api

Playmaps

Playmaps┬« is the first children’s brand in the world to create road map play-mats in one of the healthiest and most sustainable material ever. You can explore your neighbourhood as your mat. Type in your address, Drag & Drop icons of your favourite places, to boost the sense of belonging. PlayMaps┬« gives that special feeling of playing on a unique product made especially for you. Basically, you can create your personalized playmaps mat. Meanwhile, Playmaps is using Mapbox under the hood.

Select an Icon

Icons will appear as symbols in a map based on whatever icons have been set in Mapbox studio. To select one of those icon, I am taking following few steps

  1. Get the map canvas
  2. Attach an event listener to canvas mouse-move event
  3. The event handler will
    • queryRenderedFeatures based on X and Y axis of the mouse
    • Get all symbols in the map based on returned features from the above query
    • The last one in the array will be considered as a selected icon
    • Clicked the selected symbols

The following code will allow to click on an icon and it’ll show the text in the popup.

<!DOCTYPE html>
<html>

<head>
    <meta charset='utf-8' />
    <title>Select icons in a map</title>
    <meta name='viewport' content='initial-scale=1,maximum-scale=1,user-scalable=no' />
    <script src='https://api.tiles.mapbox.com/mapbox-gl-js/v1.3.0/mapbox-gl.js'></script>
    <link href='https://api.tiles.mapbox.com/mapbox-gl-js/v1.3.0/mapbox-gl.css' rel='stylesheet' />
    <style>
        #map {
            position: absolute;
            top: 0;
            bottom: 0;
            width: 100%;
        }
    </style>
</head>

<body>
    <div id='map'></div>
    <script>
        function initPage(mapboxgl) {
            var isDraggable = false;
            var selectedSymbol = {};
            mapboxgl.accessToken = 'pk.eyJ1IjoicGxheW1hcHMiLCJhIjoiY2pxdW91Mmt3MGRiajQ0bXF0bjNkZGlzNiJ9.D8WVzHCcxCgZYCkB7M-XWQ';
            var map = new mapboxgl.Map({
                container: 'map',
                style: 'mapbox://styles/mapbox/streets-v11',
                center: [-65.017, -16.457],
                zoom: 7
            });

            let findInCanvas = (e) => {
                var layerX = e.layerX;
                var layerY = e.layerY;

                var features = map.queryRenderedFeatures([layerX, layerY], {});

                if (features && features.length > 0) {
                    var symbols = getSymbolsWithPoint(features);

                    if (symbols.length > 0) {
                        if (selectedSymbol !== symbols[(symbols.length - 1)]) {
                            isDraggable = true;
                            if (canvas.style.cursor == '' || canvas.style.cursor == 'auto') {
                                canvas.style.cursor = 'pointer';
                            }
                            selectedSymbol = symbols[(symbols.length - 1)];
                            map.on('click', symbolSelected);
                        }
                    }
                    else
                        clearSelectedSymbol();
                }
                else
                    clearSelectedSymbol();
            };

            let clearSelectedSymbol = () => {
                if (isDraggable) {
                    isDraggable = false;
                    selectedSymbol = null;
                    canvas.style.cursor = 'auto';
                    map.off('click', symbolSelected);
                }
            };

            let symbolSelected = (e) => {
                if (selectedSymbol == null) {
                    return;
                }
                map.off('click', symbolSelected);
                canvas.removeEventListener('mousemove', findInCanvas);

                addPopup(selectedSymbol);
                selectedSymbol = null;

                map.on('click', symbolSelected);
                canvas.addEventListener('mousemove', findInCanvas);
            }

            let createSource = (feature) => {
                return {
                    type: "geojson",
                    data: feature
                }
            }

            let createLayer = (sourceName, feature) => {                                
                return {
                    "id": sourceName,
                    "source": sourceName,
                    "type": "symbol",
                    "layout": feature.layout
                };
            }

            function getSymbolsWithPoint(array) {
                var symbols = [];

                for (var i = array.length - 1; i > - 1; i--) {
                    if (array[i].layer.type === "symbol"
                        && (array[i].geometry.type === "Point"
                            || array[i].geometry.type === "Polygon"))
                        symbols.push(array[i]);
                }

                return symbols;
            }

            function addPopup(selectedFeature) {
                var pointId = "point" + selectedFeature.id + "." + Math.random() * 10;
                if (!map.getSource(pointId)) {
                    map.addSource(pointId, createSource(selectedFeature));
                    map.addLayer(createLayer(pointId, selectedFeature.layer));
                
                    map.on('click', pointId, (e) => {                        
                        var popup = new mapboxgl.Popup({ closeButton: true })
                            .setLngLat(e.lngLat)
                            .setText(selectedFeature.layer["source-layer"] + " layer")
                            .addTo(map);                    
                        if (e.originalEvent.stopPropagation) {
                            e.originalEvent.stopPropagation();
                        }
                        e.preventDefault();
                        return false;
                    });
                }
            }

            var canvas = map.getCanvas();
            canvas.addEventListener('mousemove', findInCanvas);
        };

        initPage(mapboxgl);
    </script>

</body>

</html>

I would like to explain a few methods from the above example, findInCanvas , getSymbolsWithPoint and addPopup.

findInCanvas has been attached to mousemove event of the map canvas. Whenever mouse point to any icon, this listener retrieves the feature. The getSymbolsWithPoint method filter and return the symbol mouse is pointing to. addPopup method add a new geojson source and a layer into the map. This method also adds a mapboxgl.Popup

Please check the JSFiddle here for full sample. I would love to hear your feedback. That’s all for today

I would like to hear your thoughts