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:
https://jsfiddle.net/pdqavdup/2/
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>'
})
mapimage.addTo(MAP);
L.control.scale({
imperial: false
}).addTo(MAP);
I think dpi matters. I presume you did the export via Project > Import/Export > Export Map to Image...
wherein you can set the resolution (in dpi):
calculating the dpi theoretically necessary:
x * px/inch = px/m
x = (px/m) / (px/inch)
x = inch/m = 2.54cm / 100cm = 0.0254
So you would have to set the resolution to 0.0254 dpi.
Unfortunately at least in this export tool float values for the resolution are allways rounded to integers, which implies that you cannot set resolutions less than 1.
Best Answer
You have indeed made some errors in your calculation. 156543.04 meters/pixel is a constant "based on the diameter of the Earth and the equations Microsoft used to set the zoom levels" used in two equations on the page you reference - it's not a variable. Your mistake was in substituting the map resolution your openlayers code returned for the constant. The first equation is:
In theory, according to the table provided at the page you linked, this would be 611.5 m/p at zoom level 8, at the equator. That's almost exactly the number you were given by your openlayers code, but if you solve that equation at 51.02 lat it comes out to be closer to 384.7 m/p. That matches up with the sentence under their chart, which gives an example (at a different zoom level) for a lat other than the equator. However it is a bit odd that your openlayers code is returning 611.5 for zoom level 8 at 51.02 and not 384.7. That suggests to me that the equation may not hold for openlayers [actually it looks like very similar numbers in the openlayers docs] - the page you link to is specific to Bing maps and as noted above is based on their equations to set zoom levels. Or it may just be that code returns the at-the-equator value, regardless of where you're actually looking, while the displayed scalebar takes the lat into account.
The formula you actually want to solve (theoretically, with your values) should be:
Now, as I mentioned in my comment, your true screen resolution in terms of ppi is hardware/device dependent and varies from display to display. If you want to find it, it may be a published specification or you may have to calculate it. You first need the native 'resolution' of the display. This would be more properly termed pixel dimensions, because resolution is actually the density of pixels (ppi) - but we use resolution interchangeably.
So let's say 1920x1080, a standard HD display (and note this needs to be the native number, NOT what it's being run at). Since we need the diagonal pixel dimension, simple a^2 + b^2 = c^2 gives us 2202.9 diagonal pixels. Now we need the actual diagonal dimension of the display. For instance a 24" display may only be 23.8" - this is pretty much always in the specs (usually as 'viewable' or some similar adjective). Diagonal pixels divided by diagonal inches gives us 92.6 ppi. Were it a 27" display at the same resolution, it would be 81.6 ppi. A 5" gives 440.6 ppi. I found a handy calculator on the web that can do this math for you if you have the right numbers.
This will give you a much more accurate (possibly even correct) result than trying to use the Microsoft equation and should be easier. The relationship, per your question title, is somewhat complex - we're talking about scaled scales essentially. And if you run a resolution (or rather pixel dimension) different than native, it gets even more complicated.
Go back to what scale is - unit x on map representing unit y in the real world. In computers there is no scale relative to anything but pixels. That's the first equation, what they term map resolution. Say 100 pixels represents 600m. On a phone display those 100 pixels may only be 0.25 inch, while on a 30" computer monitor they might be 1 inch - but they still both represent 600m. That's why the page has the second equation - because most people don't think of map scale in terms of pixels, they want a inch : mile ratio (or whatever units). The problem is that ratio changes depending on what display you're looking at while the pixel : real world unit ratio doesn't, assuming the same pixel dimensions and zoom on the image.