I am displaying a gamemap using Leaflet. The map is nonwrapping.

I use L.CRS.Simple which is recommended for this (and makes sense).

I add multiple tilelayers, each created using gdal2tiles. The source images can differ in size (2048², 4096² or 8192², always square).

Displaying all that works fine with a bit of unproject magic, but now I wanted to add measurement tools and a scalebar. As the tiles have a size of 256px, the Leaflet internal scalebar always assumes the tilelayers to have an extent of 256 "meters".

Gdal2tiles already provides this information in the accompanying tilemapresource.xml as

An example fiddle of a map that actually should have an extent of 2048x2048 meters, where the source image (used to create the tiles) was 4096x4096 (giving tilesets). Gdal2tiles helpfully creates a tilemapresource.xml that provides this information:

<BoundingBox minx="-4096.00000000000000" miny="0.00000000000000" maxx="0.00000000000000" maxy="4096.00000000000000"/>
<Origin x="-4096.00000000000000" y="0.00000000000000"/>
<TileFormat width="256" height="256" mime-type="image/png" extension="png"/>
<TileSets profile="raster">
    <TileSet href="0" units-per-pixel="16.00000000000000" order="0"/>
    <TileSet href="1" units-per-pixel="8.00000000000000" order="1"/>
    <TileSet href="2" units-per-pixel="4.00000000000000" order="2"/>
    <TileSet href="3" units-per-pixel="2.00000000000000" order="3"/>
    <TileSet href="4" units-per-pixel="1.00000000000000" order="4"/>
    <TileSet href="5" units-per-pixel="0.50000000000000" order="5"/>

See this JSFiddle for an illustration of the nonadjusted scaleba (showing 256m width of the entire map instead of 2048 I want):


However, I cannot see a way to tell Leaflet what scale a provided tilelayer actually has (something like setunitsperpixel(x, zoomlevel))"

Is there an option or setting I can use to tell Leaflet that a provided tilelayer actually has a specific "units-per-pixel" at a given zoom level? Or is this simply not supported by Leaflet and I have to manually hack around this?

This project is using Leaflet 1.0 .

1 Answer 1


Spent quite some time testing, and as usual, the solution is fairly simple once you know how Leaflet wants to work.

The solution is to specify a custom CRS with a custom transform function. This s where you can specify what each map unit/pixel represents, and this is what is used by Leaflet internally for all distance calculations and - well - transforms :) .

Leaflet does neither support changing CRS (Coordinate Reference System) of a map after its been initialized, nor does it support different CRS per layer. This means you have to specify this custom CRS before you initialize the map. Or you have to reinitialize the map when you want to load a different layer: If you need to do that, know that you can use "map.remove()" to properly remove an already initialized map (this function is not documented, so I had to search a bit).

Look at this fiddle for how I fixed the problem fiddle posted in the question:


var factorx = 0.125
var factory = 0.125

L.CRS.pr = L.extend({}, L.CRS.Simple, {
  projection: L.Projection.LonLat,
  transformation: new L.Transformation(factorx, 0, -factory, 0),
  // Changing the transformation is the key part, everything else is the same.
  // By specifying a factor, you specify what distance in meters one pixel occupies (as it still is CRS.Simple in all other regards).
  // In this case, I have a tile layer with 256px pieces, so Leaflet thinks it's only 256 meters wide.
  // I know the map is supposed to be 2048x2048 meters, so I specify a factor of 0.125 to multiply in both directions.
  // In the actual project, I compute all that from the gdal2tiles tilemapresources.xml, 
  // which gives the necessary information about tilesizes, total bounds and units-per-pixel at different levels.

// Scale, zoom and distance are entirely unchanged from CRS.Simple
  scale: function(zoom) {
    return Math.pow(2, zoom);

  zoom: function(scale) {
    return Math.log(scale) / Math.LN2;

  distance: function(latlng1, latlng2) {
    var dx = latlng2.lng - latlng1.lng,
      dy = latlng2.lat - latlng1.lat;

    return Math.sqrt(dx * dx + dy * dy);
  infinite: true

var MAP = L.map('map', {
  crs: L.CRS.pr
}).setView([0, 0], 2);

var mapheight = 2048;
var mapwidth = 2048;
var sw = MAP.unproject([0, mapheight], 4);  // Level 4, because this is the level where meters-per-pixel is exactly 1
var ne = MAP.unproject([mapwidth, 0], 4);
var layerbounds = new L.LatLngBounds(sw, ne);
var mapname = "beirut"
var mapimage =     L.tileLayer('http://tournament.realitymod.com/mapviewer/tiles/' + mapname + '/{z}/{x}/{y}.jpg', {
  minZoom: 0,
  maxZoom: 5,
  bounds: layerbounds,
  noWrap: true,
  attribution: '<a href="http://tournament.realitymod.com/showthread.php?t=34254">Project Reality Tournament</a>'
  imperial: false
  • 1
    Hi I met the same question and can not find a solution, the jsfiddle link you post is invalid now, I can't open it, can you post your sample code in another place or just email me([email protected])? I really need this, thanks a lot :)
    – acoustic
    Commented Apr 23, 2017 at 13:18
  • 1
    The fiddle in my answer works just fine for me?
    – Senshi
    Commented Apr 24, 2017 at 8:43
  • There is no issue with the jsfiddle per se; the problem seems to be with the tournament.realitymod.com URL which might be only available during, well, tournaments; I've managed to change the URL so that it displays tiles from a map that allegedly will work even outside tournaments: jsfiddle.net/GwynethLlewelyn/kyf7muta/1 Commented Nov 19, 2019 at 22:13
  • It is expected to replace the tile source with your own, obviously. the jsfiddle does not rely on that, the code will still work with your own tiles.
    – Senshi
    Commented Feb 27, 2021 at 17:18

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.