'Ipyleaflet: Create your own arbitrary non-geographic coordinate system basemap

I am trying to use ipyleaflet with an arbitrary non-geographic tiled image as basemap. i.e.not a projected image. Think like a tiled image of your dog

The code I have below displays the tiled image just fine but places it on top of the default globe map.

  1. How do I avoid getting the default map as background or how do I remove it? Note putting the background earth map to transparent is not really an option as this is still making calls to another server to get and load this unwanted data.

  2. How would one work with marker coordinates for overlays for this kind of unprojected image? I have found a solution for Leaflet.js in Rastercoods however I would like to know how to do this is in Ipyleaflet

from ipyleaflet import Map, LocalTileLayer

tile_layer = LocalTileLayer(path='tiles/{z}/{x}/{y}.png')

m = Map(
   default_tiles=tile_layer,
   zoom=4,
   scroll_wheel_zoom=True)

m.add_layer(tile_layer)

m


Solution 1:[1]

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).

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 garlon4