Step-4: Improvements of Previous steps and Zoom-To feature

Step-4:  Improvements of the Previous steps and Zoom-To feature

Introduction

This step-4 is an improvement of previous steps with the introduction of bounding box and more elaborately instanced labels and implementation of zoom-to feature on click.

Getting Started

This guide starts by defining a bounding box and then add labels and mouse events such as zoom-to click.

A bounding box is an area defined by two longitudes and two latitudes. It is typically expressed as an array of coordinate pairs. This guide uses the helper method getBoundingBox() which generates topology data.

getBoundingBox() takes a set of coordinates and returns the maximum and minimum bounds for those coordinates on map.

function getBoundingBox(feature) {
    var bounds = {
            xMin: Number.MAX_VALUE,
            yMin: Number.MAX_VALUE,
            xMax: Number.MIN_VALUE,
            yMax: Number.MIN_VALUE,
        },
        coords, point, latitude, longitude,
        sum_lng = 0.0,
        sum_lat = 0.0,
        countcoords = 0;

    var __rec_extract = function(part) {
        if (typeof(part[0]) == 'number') {
            longitude = part[0];
            latitude = part[1];

            sum_lng += longitude, sum_lat += latitude, countcoords += 1;
            bounds.xMin = bounds.xMin < longitude ? bounds.xMin : longitude;
            bounds.xMax = bounds.xMax > longitude ? bounds.xMax : longitude;
            bounds.yMin = bounds.yMin < latitude ? bounds.yMin : latitude;
            bounds.yMax = bounds.yMax > latitude ? bounds.yMax : latitude;
        } else {
            for (var i = 0; i < part.length; i++) {
                __rec_extract(part[i]);
            }
        }
    }
    __rec_extract(feature.geometry.coordinates);
    bounds.lng_avg = sum_lng / countcoords, bounds.lat_avg = sum_lat / countcoords;
    return bounds;
}

Adding Labels

In this guide labels are added on the polygon layer which is defined by using the bounds returned by the getBoundingBox() helper function. The steps to add a source and a polygon has already been explained in step-3.

Zoom-To on Click

To implement zoom-to feature on mouse click event, fitBounds() method is used to pan and zoom the map to contain its visible area within the specified geographical bounds:

fitBounds(bounds, options?, eventData?)

Parameters:

NameDescription
bounds Center these bounds in the viewport and use the highest zoom level up to that fits them in the viewport.
options(Object?) Options supports all properties from AnimationOptions and CameraOptions. For additional options please see Mapbox GL JS API documentation
eventData(Object?) Additional properties to be added to event objects of events triggered by this method.

Example:

map.on('click', 'polygons', function(e) {
    e.preventDefault();
    var boundingBox = JSON.parse(e.features[0].properties.topo);
    map.fitBounds([
        [boundingBox.xMin, boundingBox.yMin],
        [boundingBox.xMax, boundingBox.yMax]
    ]);
});

Final Code

Here is the final code that adds a polygon layers with labels using bounding box and implementation of zoom-to feature on click with mouse events handling.

A working example can be seen here.

<!DOCTYPE html>
<html>

    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="initial-scale=1.0, user-scalable=no" />
        <title>Viamap vektordemo</title>

        <!-- add viamap bootstrap code -->
        <script src="./js/viamapstrap.js"></script>
    </head>

    <body style="margin:0; padding:0;">
        <nav id="menu"></nav>
        <div id='map' style="position:absolute; top:0; bottom:0; width:100%;"></div>
        <script>
            vms.initmap({
                    container: 'map',
                    hash: false,
                })
                .then(function(map) {

                    // add some controls
                    map.addControl(new mapboxgl.NavigationControl(), 'top-left');
                    // minimal feature toplogy data generator
                    function getBoundingBox(feature) {
                        var bounds = {
                                xMin: Number.MAX_VALUE,
                                yMin: Number.MAX_VALUE,
                                xMax: Number.MIN_VALUE,
                                yMax: Number.MIN_VALUE,
                            },
                            coords, point, latitude, longitude,
                            sum_lng = 0.0,
                            sum_lat = 0.0,
                            countcoords = 0;

                        var __rec_extract = function(part) {
                            if (typeof(part[0]) == 'number') {
                                longitude = part[0];
                                latitude = part[1];

                                sum_lng += longitude, sum_lat += latitude, countcoords += 1;
                                bounds.xMin = bounds.xMin < longitude ? bounds.xMin : longitude;
                                bounds.xMax = bounds.xMax > longitude ? bounds.xMax : longitude;
                                bounds.yMin = bounds.yMin < latitude ? bounds.yMin : latitude;
                                bounds.yMax = bounds.yMax > latitude ? bounds.yMax : latitude;
                            } else {
                                for (var i = 0; i < part.length; i++) {
                                    __rec_extract(part[i]);
                                }
                            }
                        }
                        __rec_extract(feature.geometry.coordinates);
                        bounds.lng_avg = sum_lng / countcoords, bounds.lat_avg = sum_lat / countcoords;
                        return bounds;
                    }

                    vms.ajaxcall("https://vektordemo.viamap.net/starterkit/data/kommuner.json", function(kd) {

                        var municipal_data = JSON.parse(kd.responseText),
                            municipal_labels = {
                                "type": "FeatureCollection",
                                "features": []
                            }


                        // enrich featurecollection, could have been done offline as well
                        for (var i = 0; i < municipal_data.features.length; i++) {
                            municipal_data.features[i].properties['topo'] = getBoundingBox(municipal_data.features[i]);
                            municipal_labels.features.push({
                                properties: municipal_data.features[i].properties,
                                type: "Feature",
                                id: municipal_data.features[i].id,
                                geometry: {
                                    "type": "Point",
                                    "coordinates": [municipal_data.features[i].properties['topo']['lng_avg'], municipal_data.features[i].properties['topo']['lat_avg']]
                                }
                            })
                        }

                        map.addSource("municipalities", {
                            type: "geojson",
                            data: municipal_data
                        });
                        // add municipality geometries
                        map.addLayer({
                            id: "polygons",
                            type: "fill",
                            source: "municipalities",
                            'paint': {
                                "fill-opacity": ["case",
                                    ["boolean", ["feature-state", "hover"], false],
                                    0.01,
                                    0.15
                                ],
                                'fill-color': '#008',
                                'fill-outline-color': '#800',
                            }
                        });

                        // note that the label position is calculated as the average of points in feature, a better approach would be to use something like: https://github.com/mapbox/polylabel to calculate the optimal placement
                        map.addSource("municipality-labels", {
                            type: "geojson",
                            data: municipal_labels
                        });

                        // add municipality labels 
                        map.addLayer({
                            id: "municipality-name",
                            type: "symbol",
                            source: 'municipality-labels',
                            layout: {
                                "text-field": "{name}\n",
                                "text-font": ["Droid Sans Regular"],
                                "text-size": 15,
                                'symbol-placement': "point"
                            },
                            paint: {
                                "text-color": ["case",
                                    ["boolean", ["feature-state", "hover"], false],
                                    'rgba(255,0,0,0.75)',
                                    'rgba(0,0,0,0.75)'
                                ],
                                "text-halo-color": ["case",
                                    ["boolean", ["feature-state", "hover"], false],
                                    'rgba(255,255,0,0.75)',
                                    'rgba(255,255,255,0.75)'
                                ],
                                "text-halo-width": 2,
                                "text-halo-blur": 0,
                            }
                        });

                        // zoom to calculated bounding box 
                        map.on('click', 'polygons', function(e) {
                            e.preventDefault();
                            var boundingBox = JSON.parse(e.features[0].properties.topo);
                            map.fitBounds([
                                [boundingBox.xMin, boundingBox.yMin],
                                [boundingBox.xMax, boundingBox.yMax]
                            ]);
                        });


                        var onmouseenterhandler = function(e) {
                            if (e.features.length > 0) {
                                if (hoveredStateId) {
                                    map.setFeatureState({
                                        source: 'municipalities',
                                        id: hoveredStateId
                                    }, {
                                        hover: false
                                    });
                                    map.setFeatureState({
                                        source: 'municipality-labels',
                                        id: hoveredStateId
                                    }, {
                                        hover: false
                                    });

                                }
                                hoveredStateId = e.features[0].id;
                                map.setFeatureState({
                                    source: 'municipalities',
                                    id: hoveredStateId
                                }, {
                                    hover: true
                                });
                                map.setFeatureState({
                                    source: 'municipality-labels',
                                    id: hoveredStateId
                                }, {
                                    hover: true
                                });
                            }
                        }

                        // change mouse cursor over 
                        map.on('mouseenter', 'polygons', function() {
                            map.getCanvas().style.cursor = 'pointer';
                            onmouseenterhandler(e);
                        });

                        // hover effect from this example: 
                        // https://www.mapbox.com/mapbox-gl-js/example/hover-styles/
                        var hoveredStateId = null;
                        map.on("mousemove", "polygons", function(e) {
                            onmouseenterhandler(e);
                        });

                        map.on("mouseleave", "polygons", function() {
                            if (hoveredStateId) {
                                map.setFeatureState({
                                    source: 'municipalities',
                                    id: hoveredStateId
                                }, {
                                    hover: false
                                });
                                map.setFeatureState({
                                    source: 'municipality-labels',
                                    id: hoveredStateId
                                }, {
                                    hover: false
                                });
                            }
                            hoveredStateId = null;
                            // Change cursor back to a pointer when leaving.
                            map.getCanvas().style.cursor = '';
                        });
                    })
                });
        </script>
    </body>

</html>

Do you want to try Viamap? Download “starter kit