This works, except that X and Y are reversed from what you'd expect.
First, let's define some details about our specific image. Note that leaflet expects tiles to be square.
# This is from the source image
metadata = {
'levels': 9,
'sizeX': 32001,
'sizeY': 38474,
'tileHeight': 240,
'tileWidth': 240
}
tileUrl = 'tiles/{z}/{x}/{y}.png'
Note that levels
is computable via 1 + math.ceil(math.log(max(sizeX/tileWidth, sizeY/tileHeight))/math.log(2))
.
Now, the ipyleaflet code:
import ipyleaflet as ipy
# Warning: coordinates are Y, X
proj = dict(
name='PixelSpace',
custom=True,
# The 20 is arbitrary -- it could just be defined for the zoom levels we can reach, but going higher doesn't cause any issues
resolutions=[256 * 2 ** (-l) for l in range(20)],
# This works but has x and y reversed
# The s in esu inverts the y axis so 0,0 is at the top left not the bottom right
proj4def='+proj=longlat +axis=esu',
bounds=[[0,0],[metadata['sizeY'],metadata['sizeX']]],
origin=[0,0],
)
tileLayer = ipl.TileLayer(
url=tileUrl,
bounds=proj['bounds'] ,
min_zoom=0,
max_native_zoom=metadata['levels'] - 1,
tile_size=metadata['tileWidth'],
)
map = ipl.Map(
crs=proj,
basemap=tileLayer,
min_zoom=0,
# allow zooming in 2 steps further than the image
max_zoom=metadata['levels'] + 1,
zoom=0,
# all coordinates are y, x
center=[metadata['sizeY']/2, metadata['sizeX']/2],
scroll_wheel_zoom=True,
dragging=True,
attribution_control=False,
zoom_snap=False,
)
map.fit_bounds(proj['bounds'])
# Render the map
map
We can draw some rectangles to show we are in a pixel coordinate system:
# Make some rectangles to prove with have pixel space coordinates
rectangle = ipl.Rectangle(bounds=[[0,0],[metadata['sizeY'],metadata['sizeX']]])
map.add_layer(rectangle)
# This will be wider than it is tall
rectangle = ipl.Rectangle(bounds=[[0,0],[5000,10000]])
map.add_layer(rectangle)
It seems like we should be able to use a different projection to swap the axes to the expected x, y
instead of y, x
, but it doesn't seem to be properly supported. You can do something like proj4def='+proj=longlat +axis=seu'
, but the bounds somehow crop the image unless the extend to negative values and zooming jumps around oddly. The other alternative of using a proj.4 pipeline doesn't seem supported in the least.
Besides x and y being reversed, the map.bounds()
call also returns surprising results for the "south east" corner because it is somehow adjusted based on the latlong
projection. You can use the eqc
projection instead, but then the resolution has to be scaled by some factor (maybe instead of 256
it is 256 * (length of one degree of arc at the equator in meters)
, but I'm not sure).