'Color certain grid tiles based on coordinates

I would like to color certain grid tiles based on their coordinates.

I created the following grid:

<!DOCTYPE html>
<html>

<head>
    <title>Color Tiles</title>
    <meta charset="utf-8" />
    <link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous">
    <link rel="stylesheet" href="https://unpkg.com/[email protected]/dist/leaflet.css" />

    <style>
        body {
            padding: 0;
            margin: 0;
        }

        html,
        body,
        #map {
            height: 100%;
            width: 100%;
        }
    </style>
</head>

<body>

  <div id="map"></div>

    <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js" integrity="sha384-ka7Sk0Gln4gmtz2MlQnikT1wXgYsOg+OMhuP+IlRH9sENBO0LRn5q+8nbTov4+1p" crossorigin="anonymous"></script>
    <script src="https://unpkg.com/[email protected]/dist/leaflet.js"></script>

    <!-- Make sure you put this AFtER leaflet.js, when using with leaflet 
    <script src="https://unpkg.com/[email protected]/dist/geosearch.umd.js"></script>
    -->

    <script>
        var map = new L.Map('map', { center: [10, 0], zoom: 2 });

        let zoomLevel = map.getZoom()
        console.log("zoomLevel " + zoomLevel)

        // if(zoomLevel === 1) {
            var tiles = new L.GridLayer();
            tiles.createTile = function (coords) {
                var tile = L.DomUtil.create('canvas', 'leaflet-tile');
                var ctx = tile.getContext('2d');
                var size = this.getTileSize()
                // console.log("size: " + size.toString())
                tile.width = size.x
                tile.height = size.y

                // calculate projection coordinates of top left tile pixel
                var nwPoint = coords.scaleBy(size)

                // calculate geographic coordinates of top left tile pixel
                var nw = map.unproject(nwPoint, coords.z)

                ctx.fillStyle = 'white';
                ctx.fillRect(0, 0, size.x, 50);
                ctx.fillStyle = 'black';
                ctx.fillText('x: ' + coords.x + ', y: ' + coords.y + ', zoom: ' + coords.z, 20, 20);
                ctx.fillText('lat: ' + nw.lat + ', lon: ' + nw.lng, 20, 40);
                ctx.strokeStyle = 'black';
                ctx.beginPath();
                ctx.moveTo(0, 0);
                ctx.lineTo(size.x - 1, 0);
                ctx.lineTo(size.x - 1, size.y - 1);
                ctx.lineTo(0, size.y - 1);
                ctx.closePath();
                ctx.stroke();
                return tile;
            }

            L.tileLayer('http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
                attribution: 'Map data &copy; <a href="http://www.osm.org">OpenStreetMap</a>',
            }).addTo(map)

            tiles.addTo(map)
    
    </script>
</body>

</html>

I would like to have the following:

enter image description here

I tried to style it directly via css, which seems to work. However, I would like to style it based on the provided coordinates.

Any suggestion how to do this?



Solution 1:[1]

You can use the internal _tileCoordsToBounds function from Leaflet to get the bounds of the tile and check if the latlng is in it.

I extended L.GridLayer because it is much cleaner. The important function is _isLatLngInCoords:

L.ColoredTiles = L.GridLayer.extend({
    colorLatLngs: [],
    initialize (latlngs, options) {
        L.Util.setOptions(this, options);
        if(latlngs && L.Util.isArray(latlngs) && latlngs.length > 0) {
            this.colorLatLngs = latlngs;
        }

        if(this.options.debug) {
            this._showDebugLatLngs();
        }
    },
    createTile (coords) {
        var tile = L.DomUtil.create('canvas', 'leaflet-tile');
        var ctx = tile.getContext('2d');
        var size = this.getTileSize()
        // console.log("size: " + size.toString())
        tile.width = size.x
        tile.height = size.y

        // calculate projection coordinates of top left tile pixel
        var nwPoint = coords.scaleBy(size)

        // calculate geographic coordinates of top left tile pixel
        var nw = map.unproject(nwPoint, coords.z)

        ctx.fillStyle = 'white';
        ctx.fillRect(0, 0, size.x, 50);
        ctx.fillStyle = 'black';
        ctx.fillText('x: ' + coords.x + ', y: ' + coords.y + ', zoom: ' + coords.z, 20, 20);
        ctx.fillText('lat: ' + nw.lat + ', lon: ' + nw.lng, 20, 40);
        ctx.strokeStyle = 'black';
        ctx.beginPath();
        ctx.moveTo(0, 0);
        ctx.lineTo(size.x - 1, 0);
        ctx.lineTo(size.x - 1, size.y - 1);
        ctx.lineTo(0, size.y - 1);
        ctx.closePath();
        ctx.stroke();

        if (this._isLatLngInCoords(coords)) {
            ctx.fillStyle = 'rgba(0, 0, 240, 0.3)';
            ctx.fillRect(0, 0, size.x, size.y);
        }

        return tile;
    },
    _isLatLngInCoords(coords) {
        var tileBounds = this._tileCoordsToBounds(coords);
        return this.colorLatLngs && this.colorLatLngs.some(function (a) {
            return tileBounds.contains(a);
        });
    },
    setColorLatLngs(latlngs){
        this.colorLatLngs = latlngs;
        this.redraw();

        if(this.options.debug) {
            this._showDebugLatLngs();
        }
    },
    _showDebugLatLngs(){
        this.fg = this.fg || L.featureGroup().addTo(map);
        this.fg.clearLayers();
        this.colorLatLngs && this.colorLatLngs.forEach((latlng)=>{
            L.marker(latlng).addTo(this.fg);
        })
    }
});

And then add it to the map:

var colorLatLngs = [
    L.latLng(0,0),
    L.latLng(2,2),
];
        
var tiles = new L.ColoredTiles(colorLatLngs).addTo(map)

I also added the function setColorLatLngs(array) to change the latlngs and update the colored tiles.

tiles.setColorLatLngs([L.latLng(3,3)]);

And for the debug purpose I added the option debug: true to set markers on each latlng. So it is clearly visible which tiles should be colored.

var tiles = new L.ColoredTiles(colorLatLngs, {debug: true}).addTo(map)

Demo:

<!DOCTYPE html>
<html>

<head>
<title>Color Tiles</title>
<meta charset="utf-8" />
<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous">
<link rel="stylesheet" href="https://unpkg.com/[email protected]/dist/leaflet.css" />

<style>
    body {
        padding: 0;
        margin: 0;
    }

    html,
    body,
    #map {
        height: 100%;
        width: 100%;
    }
</style>
</head>

<body>

<div id="map"></div>

<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js" integrity="sha384-ka7Sk0Gln4gmtz2MlQnikT1wXgYsOg+OMhuP+IlRH9sENBO0LRn5q+8nbTov4+1p" crossorigin="anonymous"></script>
<script src="https://unpkg.com/[email protected]/dist/leaflet.js"></script>

<!-- Make sure you put this AFtER leaflet.js, when using with leaflet 
<script src="https://unpkg.com/[email protected]/dist/geosearch.umd.js"></script>
-->

<script>
var map = new L.Map('map', { center: [10, 0], zoom: 2 });

let zoomLevel = map.getZoom()
console.log("zoomLevel " + zoomLevel)

L.ColoredTiles = L.GridLayer.extend({
    colorLatLngs: [],
    initialize (latlngs, options) {
        L.Util.setOptions(this, options);
        if(latlngs && L.Util.isArray(latlngs) && latlngs.length > 0) {
            this.colorLatLngs = latlngs;
        }

        if(this.options.debug) {
            this._showDebugLatLngs();
        }
    },
    createTile (coords) {
        var tile = L.DomUtil.create('canvas', 'leaflet-tile');
        var ctx = tile.getContext('2d');
        var size = this.getTileSize()
        // console.log("size: " + size.toString())
        tile.width = size.x
        tile.height = size.y

        // calculate projection coordinates of top left tile pixel
        var nwPoint = coords.scaleBy(size)

        // calculate geographic coordinates of top left tile pixel
        var nw = map.unproject(nwPoint, coords.z)

        ctx.fillStyle = 'white';
        ctx.fillRect(0, 0, size.x, 50);
        ctx.fillStyle = 'black';
        ctx.fillText('x: ' + coords.x + ', y: ' + coords.y + ', zoom: ' + coords.z, 20, 20);
        ctx.fillText('lat: ' + nw.lat + ', lon: ' + nw.lng, 20, 40);
        ctx.strokeStyle = 'black';
        ctx.beginPath();
        ctx.moveTo(0, 0);
        ctx.lineTo(size.x - 1, 0);
        ctx.lineTo(size.x - 1, size.y - 1);
        ctx.lineTo(0, size.y - 1);
        ctx.closePath();
        ctx.stroke();

        if (this._isLatLngInCoords(coords)) {
            ctx.fillStyle = 'rgba(0, 0, 240, 0.3)';
            ctx.fillRect(0, 0, size.x, size.y);
        }

        return tile;
    },
    _isLatLngInCoords(coords) {
        var tileBounds = this._tileCoordsToBounds(coords);
        return this.colorLatLngs && this.colorLatLngs.some(function (a) {
            return tileBounds.contains(a);
        });
    },
    setColorLatLngs(latlngs){
        this.colorLatLngs = latlngs;
        this.redraw();

        if(this.options.debug) {
            this._showDebugLatLngs();
        }
    },
    _showDebugLatLngs(){
        this.fg = this.fg || L.featureGroup().addTo(map);
        this.fg.clearLayers();
        this.colorLatLngs && this.colorLatLngs.forEach((latlng)=>{
            L.marker(latlng).addTo(this.fg);
        })
    }
});

L.tileLayer('http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
    attribution: 'Map data &copy; <a href="http://www.osm.org">OpenStreetMap</a>',
}).addTo(map)

var colorLatLngs = [
    L.latLng(0,0),
    L.latLng(2,2),
];
var tiles = new L.ColoredTiles(colorLatLngs, {debug: true}).addTo(map)
</script>
</body>

</html>

Solution 2:[2]

I'm thinking about something this way :

Added :

if (nw.lat > 10) {
   ctx.fillStyle = "rgba(0, 0, 0, 0.4)";
   ctx.fillRect(0, 50, size.x, size.y - 50);
}

Up to you to use any condition that allow you to localize the tiles.

<!DOCTYPE html>
<html>

<head>
    <title>Color Tiles</title>
    <meta charset="utf-8" />
    <link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous">
    <link rel="stylesheet" href="https://unpkg.com/[email protected]/dist/leaflet.css" />

    <style>
        body {
            padding: 0;
            margin: 0;
        }

        html,
        body,
        #map {
            height: 100%;
            width: 100%;
        }
    </style>
</head>

<body>

  <div id="map"></div>

    <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js" integrity="sha384-ka7Sk0Gln4gmtz2MlQnikT1wXgYsOg+OMhuP+IlRH9sENBO0LRn5q+8nbTov4+1p" crossorigin="anonymous"></script>
    <script src="https://unpkg.com/[email protected]/dist/leaflet.js"></script>

    <!-- Make sure you put this AFtER leaflet.js, when using with leaflet 
    <script src="https://unpkg.com/[email protected]/dist/geosearch.umd.js"></script>
    -->

    <script>
        var map = new L.Map('map', { center: [10, 0], zoom: 2 });

        let zoomLevel = map.getZoom()
        console.log("zoomLevel " + zoomLevel)

        // if(zoomLevel === 1) {
            var tiles = new L.GridLayer();
            tiles.createTile = function (coords) {
                var tile = L.DomUtil.create('canvas', 'leaflet-tile');
                var ctx = tile.getContext('2d');
                var size = this.getTileSize()
                // console.log("size: " + size.toString())
                tile.width = size.x
                tile.height = size.y

                // calculate projection coordinates of top left tile pixel
                var nwPoint = coords.scaleBy(size)

                // calculate geographic coordinates of top left tile pixel
                var nw = map.unproject(nwPoint, coords.z)

                ctx.fillStyle = 'white';
                ctx.fillRect(0, 0, size.x, 50);
                if (nw.lat > 10) {
                    ctx.fillStyle = "rgba(0, 0, 0, 0.4)";
                    ctx.fillRect(0, 50, size.x, size.y - 50);
                }
                ctx.fillStyle = 'black';
                ctx.fillText('x: ' + coords.x + ', y: ' + coords.y + ', zoom: ' + coords.z, 20, 20);
                ctx.fillText('lat: ' + nw.lat + ', lon: ' + nw.lng, 20, 40);
                ctx.strokeStyle = 'black';
                ctx.beginPath();
                ctx.moveTo(0, 0);
                ctx.lineTo(size.x - 1, 0);
                ctx.lineTo(size.x - 1, size.y - 1);
                ctx.lineTo(0, size.y - 1);
                ctx.closePath();
                ctx.stroke();
                return tile;
            }

            L.tileLayer('http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
                attribution: 'Map data &copy; <a href="http://www.osm.org">OpenStreetMap</a>',
            }).addTo(map)

            tiles.addTo(map)
    
    </script>
</body>

</html>

Solution 3:[3]

This snippet simultaneously solves the related question. It decides which tiles to color based on an array of TileNumber objects:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>GridLayer Test</title>
    <link rel="stylesheet" href="https://unpkg.com/[email protected]/dist/leaflet.css" />
    <style>
        body {
            padding: 0;
            margin: 0;
        }

        html,
        body,
        #map {
            height: 100%;
            width: 100%;
        }
    </style>
</head>

<body>
<div id="map"></div>

<script src="https://unpkg.com/[email protected]/dist/leaflet.js"></script>

<script>

    const numTilesX = 2 ** 17
    const numTilesY = 2 ** 17

    class TileNumber {
        constructor(x, y) {
            this.x = x;
            this.y = y;
        }
        equals(other) {
            return this.x === other.x && this.y === other.y;
        }
    }

    let coloredTiles = [
        new TileNumber(70435, 45249),
        new TileNumber(70434, 45248),
        new TileNumber(70441, 45245)
    ]

    function latLngToTileNumber(latLng) {
        const lngDegrees = latLng.lng;
        const latRadians = latLng.lat * (Math.PI/180);
        return new L.Point(
            numTilesX * ((lngDegrees + 180) / 360),
            numTilesY * (1 - Math.log(Math.tan(latRadians) + 1 / Math.cos(latRadians)) / Math.PI) / 2
        );
    }

    const map = new L.Map('map', {center: [48.5748229, 13.4609744], zoom: 16, maxZoom: 19});

    L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
        attribution: 'Map data &copy; <a href="https://www.osm.org">OpenStreetMap</a>', maxZoom: 19
    }).addTo(map)

    const tiles = new L.GridLayer({minZoom: 12});
    tiles.createTile = function (coords) {
        const tile = L.DomUtil.create('canvas', 'leaflet-tile');
        const ctx = tile.getContext('2d');
        const size = this.getTileSize();
        tile.width = size.x
        tile.height = size.y

        // calculate projection coordinates of top left tile pixel
        const nwPoint = coords.scaleBy(size);
        // calculate geographic coordinates of top left tile pixel
        const nw = map.unproject(nwPoint, coords.z);
        // calculate fraction tile number at top left point
        const nwTile = latLngToTileNumber(nw, Math.floor)

        // calculate projection coordinates of bottom right tile pixel
        const sePoint = new L.Point(nwPoint.x + size.x - 1, nwPoint.y + size.y - 1)
        // calculate geographic coordinates of bottom right tile pixel
        const se = map.unproject(sePoint, coords.z);
        // calculate fractional tile number at bottom right point
        const seTile = latLngToTileNumber(se, Math.ceil)

        const minTileX = nwTile.x
        const maxTileX = seTile.x
        const minTileY = nwTile.y
        const maxTileY = seTile.y

        for (let x = Math.ceil(minTileX) - 1; x <= Math.floor(maxTileX) + 1; x++) {
            for (let y = Math.ceil(minTileY) - 1; y <= Math.floor(maxTileY) + 1; y++) {

                let tile = new TileNumber(x, y)

                const xMinPixel = Math.round(size.x * (x - minTileX) / (maxTileX - minTileX));
                const xMaxPixel = Math.round(size.x * (x + 1 - minTileX) / (maxTileX - minTileX));
                const yMinPixel = Math.round(size.y * (y - minTileY) / (maxTileY - minTileY));
                const yMaxPixel = Math.round(size.y * (y + 1 - minTileY) / (maxTileY - minTileY));

                // fill the rectangle with a color
                ctx.fillStyle = coloredTiles.some(t => t.equals(tile))
                    ? 'rgba(0, 0, 255, 0.3)'
                    : 'rgba(255, 255, 255, 0)';
                ctx.fillRect(xMinPixel, yMinPixel, xMaxPixel - xMinPixel, yMaxPixel - yMinPixel);

                if (coords.z >= 16) {
                    // draw the white rectangle and text at the top of the cell
                    ctx.fillStyle = 'white';
                    ctx.fillRect(xMinPixel, yMinPixel, xMaxPixel - xMinPixel, 28);
                    ctx.fillStyle = 'black';
                    ctx.font = "15px Arial"
                    ctx.fillText(tile.x + "," + tile.y, xMinPixel + 10, yMinPixel + 20, xMaxPixel - xMinPixel);
                }

                if (coords.z >= 13) {
                    // draw a border
                    ctx.strokeStyle = 'black';
                    ctx.strokeRect(xMinPixel, yMinPixel, xMaxPixel - xMinPixel, yMaxPixel - yMinPixel);
                }

            }
        }

        return tile;

    }

    tiles.addTo(map);

    map.on('click', e => {

        const fractionalTileNumber = latLngToTileNumber(e.latlng);
        const tileNumber = new TileNumber(Math.floor(fractionalTileNumber.x), Math.floor(fractionalTileNumber.y));

        console.log("Tile " + tileNumber.x + " " + tileNumber.y  + " clicked");

        if (coloredTiles.some(t => t.equals(tileNumber))) {
            coloredTiles = coloredTiles.filter(t => !t.equals(tileNumber));
        } else {
            coloredTiles.push(tileNumber);
        }

        tiles.redraw();

    });

</script>
</body>
</html>

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1
Solution 2 Mteuahasan
Solution 3