htmlcssleafletmandelbrotsymmetry

LeafletJS: How to flip tiles vertically on-the-fly?


Background: I've produced a 1-terapixel rendering of the Mandelbrot Set and am using LeafletJS to zoom and pan around in it interactively. It works great. But since the Mandelbrot Set is symmetric along the real axis, I'm currently using twice as many tile images as necessary.

Question: How can I hook into LeafletJS's display-time code (using some callback?) so that whenever a tile is loaded via HTTP, it either passes through unchanged or is flipped vertically? This would allow me to reduce the data by many tens of gigabytes on higher zoom levels.

Example: Here are four tiles from zoom level 1 (shown here separated by one pixel). I'd like to throw away the bottom two tile images and load them instead as vertically-flipped versions of the top two tiles. Can this be done on-the-fly with LeafletJS?

Zoom Level 1 tiles

More concretely: If I know zoom level z and tile coordinates x,y, I'd like to flip the tile vertically at load-time whenever y is less than 2^(zā€“1). For instance, at zoom level z=10, I'd like to flip the tiles vertically for all y < 512.

I imagine the answer is going to involve something like setting the transform, -moz-transform, -o-transform, and -webkit-transform properties of the <img> tag to scaleY(-1) and maybe filter and -ms-filter to FlipV, but I don't know where/how to define these in a LeafletJS context.


Solution

  • You would just need to modify the y number of bottom tiles in L.TileLayer._loadTile method, before it gets applied on the image URL.

    As for flipping the image itself, unfortunately we cannot use classes because a transform property is already applied by Leaflet directly on the tiles (images), so it overrides any transform in class. Then we have to append any transform, -moz-transform etc. on the tile.style.

    L.HalfTileLayer = L.TileLayer.extend({
        _loadTile: function (tile, tilePoint) {
    
            tile._layer  = this;
            tile.onload  = this._tileOnLoad;
            tile.onerror = this._tileOnError;
    
            this._adjustTilePoint(tilePoint);
    
            //////////////////
            var limit = Math.pow(2, tilePoint.z - 1),
                y = tilePoint.y;
    
            if (y >= limit) { // modify for bottom tiles, i.e. higher y
                tilePoint.y = 2 * limit - y - 1; // y starts at 0
                tile.style.transform += " scaleY(-1)"; // append
                // apply more transforms for cross-browser
            }
            /////////////////
    
            tile.src     = this.getTileUrl(tilePoint);
    
            this.fire('tileloadstart', {
                tile: tile,
                url: tile.src
            });
        }
    });
    
    (new L.HalfTileLayer('http://{s}.tile.osm.org/{z}/{x}/{y}.png')).addTo(map);
    

    Demo: http://jsfiddle.net/ve2huzxw/73/

    Note that in the default configuration, y = 0 is the top, y = 2^z - 1 is the bottom.