Search code examples
javascripttypescriptcanvasleafletmaps

How to move the canvas?


On moveEnd, when I moved the large canvas and calculated the coordinate of the upper left corner of the tile, everything that is drawn inside the canvas needs to be moved. I need to offset the canvas by the difference between the old position of the upper left corner and the new one.

My code:

import L from 'leaflet';

class CanvasTileLayer extends L.TileLayer {
    tileSize: L.Point;
    canvas: HTMLCanvasElement;
    ctx: CanvasRenderingContext2D | null;

    constructor(urlTemplate: string, options?: L.TileLayerOptions) {
        super(urlTemplate, options);
        this.tileSize = this.getTileSize();
        this.canvas = L.DomUtil.create('canvas', 'leaflet-tile-pane');
        this.ctx = this.canvas.getContext('2d');

        this.canvas.width = window.innerWidth;
        this.canvas.height = window.innerHeight;
    }

    createTile(coords: L.Coords, done: L.DoneCallback): HTMLElement {
        const tile = super.createTile(coords, done);
        const url = this.getTileUrl(coords);

        this.canvasRedraw(tile, url, coords);
        return tile;
    }

    private canvasRedraw(tile: HTMLElement, url: string, coords: L.Coords) {
        const map: L.Map = this._map;
        const bounds = map.getPixelBounds();

        // @ts-ignore
        const pos = this._getTilePos(coords);

        tile.onload = () => {
            // delete the original tile that was created with createTile
            this.removeTileElement(tile);

            this.ctx?.drawImage(
                // @ts-ignore
                tile,
                // Need to find out the difference in coordinates after onmoveend and then subtract it from pos
                pos.x,
                pos.y,
                this.tileSize.x,
                this.tileSize.y
            );
        };

        map.on('moveend', () => {
            const newBounds = map.getPixelBounds();

            // @ts-ignore
            const deltaX = Math.floor(newBounds.min.x - bounds.min.x);
            // @ts-ignore
            const deltaY = Math.floor(newBounds.min.y - bounds.min.y);

            console.log(pos.subtract([deltaX, deltaY]));

            L.DomUtil.setPosition(this.canvas, pos.subtract([deltaX, deltaY]));

            this.ctx?.drawImage(
                // @ts-ignore
                tile,
                pos.x - deltaX,
                pos.y - deltaY,
                this.tileSize.x,
                this.tileSize.y
            );
        });

        tile.onerror = () => {
            console.log(`Failed to load tile: ${url}`);
        };
    }

    removeTileElement(tile: HTMLElement) {
        tile.parentNode?.removeChild(tile);
    }

    onAdd(map: L.Map): this {
        super.onAdd(map);

        this.getPane()?.appendChild(this.canvas);

        return this;
    }
}

export default CanvasTileLayer;

I thought the map would move and redraw after the moveend events, but the map moves to the wrong place.


Solution

  • I was able to get the desired result.

    import L from "leaflet";
    
    class CanvasTileLayer extends L.TileLayer {
        readonly tileSize: L.Point;
        readonly canvas: HTMLCanvasElement;
        readonly ctx: CanvasRenderingContext2D | null;
    
        constructor(urlTemplate: string, options?: L.TileLayerOptions) {
            super(urlTemplate, options);
    
            this.tileSize = this.getTileSize();
            this.canvas = L.DomUtil.create("canvas", "leaflet-tile-pane");
            this.ctx = this.canvas.getContext("2d", { willReadFrequently: true });
    
            this.canvas.width = window.innerWidth;
            this.canvas.height = window.innerHeight;
        }
    
        createTile(coords: L.Coords, done: L.DoneCallback): HTMLImageElement {
            const tile = super.createTile(coords, done) as HTMLImageElement;
            tile.crossOrigin = "Anonymous";
            const url = this.getTileUrl(coords);
    
            this.canvasRedraw(tile, url, coords);
    
            return tile;
        }
    
        private canvasRedraw(tile: HTMLImageElement, url: string, coords: L.Coords) {
            // @ts-ignore
            const pos = this._getTilePos(coords);
    
            tile.onload = () => {
                // delete the original tile that was created with createTile
                this.removeTileElement(tile);
    
                this.ctx?.drawImage(
                    // @ts-ignore
                    tile,
                    pos.x,
                    pos.y,
                    this.tileSize.x,
                    this.tileSize.y,
                );
            };
    
            tile.onerror = () => {
                console.log(`Failed to load tile: ${url}`);
            };
        }
    
        removeTileElement(tile: HTMLElement) {
            tile.parentNode?.removeChild(tile);
        }
    
        onAdd(map: L.Map): this {
            super.onAdd(map);
    
            this.getPane()?.appendChild(this.canvas);
    
            const bounds = map.getPixelBounds();
    
            map.on("moveend", () => {
                const newBounds = map.getPixelBounds();
    
                let deltaX: number = 0;
                let deltaY: number = 0;
    
                if (newBounds.min && bounds.min) {
                    deltaX = Math.floor(newBounds.min.x - bounds.min.x);
                    deltaY = Math.floor(newBounds.min.y - bounds.min.y);
                }
    
                L.DomUtil.setPosition(this.canvas, new L.Point(deltaX, deltaY));
    
                const imageData = this.ctx?.getImageData(
                    0,
                    0,
                    this.canvas.width,
                    this.canvas.height,
                );
    
                if (imageData) this.ctx?.putImageData(imageData, -deltaX, -deltaY);
    
                for (const tile of Object.values(this._tiles)) {
                    // @ts-ignore
                    const pos = this._getTilePos(tile.coords);
                    this.ctx?.drawImage(
                        tile.el as HTMLImageElement,
                        pos.x - deltaX,
                        pos.y - deltaY,
                        this.tileSize.x,
                        this.tileSize.y,
                    );
                }
            });
    
            return this;
        }
    }
    
    export default CanvasTileLayer;