Using Leaflet.js with non-geographic imagery
Hey! This was written for Leaflet.js 0.5.1, sorry, it might not work anymore but hopefully it's still helpful. The tileup gem should still be useful. I plan to update the guide when I have time.
Leaflet.js is a free & open source javascript mapping library, akin to Google Maps. The tutorials show how easy it is to wire up something like open street maps and start mapping, but it's not so obvious how you can use Leaflet to explore non-geographical imagery (with non lat-lng coordinates).
The steps roughly required are:
- Split large image into smaller tiles.
- Rename tiles to fit naming conventions.
- Insert JS to convert between pixel coordinates and set the max bounds of the map.
For this example we'll assume our image is 1024x6144
.
Lets get started.
Splitting your image in to tiles
Update: I wrote a gem called tileup, you can read about how to use and install it here. Once you've installed tileup
, skip down to "Using the tiles in leaflet".
Install imagemagick
if you don't already have it installed (avaliable on homebrew) via brew install imagemagick
. There should only be 3 or 4 packages needed if you're running OS X 10.8.
Once you have imagemagick
installed, you can use it to chop your image up into multiple tiles. Assuming that you have an image called big_image.png
:
$ convert -crop 256x256 +repage big_image.png tiles_%d.png
256x256
is the default tile size for Leaflet and we're assuming that your image divides evenly by this. You can override the default tile size, so if this doesn't work for you, change it to something more suitable. +repage
will fix some PNG encoding issues. After imagemagick
finishes, you should have a stack of new tiles named tiles_[0-N].png
.
Renaming the tiles
Leaflet expects to be able to request tiles by an X,Y coordinate, so now we have to rename these tiles from a linear sequence to something useable. To do this, we'll use a simple ruby script:
tile_width = 256 tile_height = 256 image_width = 1024 image_height = 6144 n = 0 # To get this number, look at the number of tiles # generated, find the last tile number and add 1 # e.g. tiles_99.png => total_tiles = 100 total_tiles = 100 tiles_per_column = image_width/tile_width row = 0 column = 0 (n...total_tiles).each do |i| filename = "tiles_#{i}.png" # current filename target = "map_#{column}_#{row}.png" # new filename puts "copy #{filename} to #{target}" `cp -f #{filename} #{target}` # rename # work out next step column = column + 1 if column >= tiles_per_column column = 0 row = row + 1 end end
With that saved to the same directory as your tile images:
cd ~/tiles_dir
ruby rename_linear_to_xy.rb
You should now have a second set of tiles, named map_[0..NX]_[0..NY].png
, which we can plug into Leaflet.
Using the tiles in Leaflet
To use the tiles in Leaflet we have to do a two things
- Set the map coordinate reference system (CRS) to something that will convert lat-lng pairs to pixels.
- Correctly set the map bounds.
Leaflet provides a CRS to cover step 1, so when you setup your map, set the crs
option to L.CRS.Simple
.
While we're setting up the map, we should also set the min, max and default zoom levels to maximum zoom (because we're only providing one level of zoom).
var map = L.map('map', { maxZoom: 20, minZoom: 20, crs: L.CRS.Simple }).setView([0, 0], 20);
Now we should set the map's max bounds to fit our image, obviously replace these values with your own original large image width and height. We'll use map.unproject
to convert from pixel coordinates to lat-lng coordinates. Leaflet still uses lat-lng internally. (Remember, 1024
is our image width, 6145
is our height.)
var southWest = map.unproject([0, 6145], map.getMaxZoom()); var northEast = map.unproject([1024, 0], map.getMaxZoom()); map.setMaxBounds(new L.LatLngBounds(southWest, northEast));
Finally we'll add our tile layer to the map (this is identical to using regular geographic imagery). {x}
and {y}
will automatically be replaced by Leaflet as required.
L.tileLayer('/map-tiles/map_{x}_{y}.png', { attribution: 'Map data © ???', }).addTo(map);
You can add markers like so (note that we are using map.unproject
to get from pixel coordinates to lat-lng coordinates):
// pixel coordinates in large image var m = { x: 102, y: 404 } var marker = L.marker(map.unproject([m.x, m.y], map.getMaxZoom())).addTo(map);
That's all there is to it.
Adding zoom
Adding the ability to zoom is pretty simple too.
- Starting with your largest image (highest level zoom):
- Perform steps above for splitting and renaming your tiles.
- Save these tiles a directory called
map-tiles/20/
- Shrink your large image to half its original size.
- Perform steps above for splitting and renaming your tiles.
- Save these tiles a directory called
map-tiles/19/
- Repeat as necessary.
You should now have something roughly like this:
map-tiles/20/map_0_0.png map-tiles/20/map_1_0.png map-tiles/20/map_2_0.png ... map-tiles/19/map_0_0.png map-tiles/19/map_1_0.png map-tiles/19/map_2_0.png ... map-tiles/18/map_0_0.png map-tiles/18/map_1_0.png map-tiles/18/map_2_0.png
Update the layer url to include a zoom attribute ({z}
):
L.tileLayer('/map-tiles/{z}/map_{x}_{y}.png', { attribution: 'Map data © ???', }).addTo(map);
Set your maps min and max zoom levels to match your avaliable zooms:
var map = L.map('map', { maxZoom: 20, minZoom: 18, crs: L.CRS.Simple }).setView([0, 0], 18);
Lastly, don't forget to check that your map max bounds are correct. They should match the with and height of your largest image (the full zoom image).
var southWest = map.unproject([0, 18435], map.getMaxZoom()); var northEast = map.unproject([3072, 0], map.getMaxZoom()); map.setMaxBounds(new L.LatLngBounds(southWest, northEast));
Hit refresh and hopefully you'll see a zoomable map. Leaflet seems to behave strangely if your map tiles don't cover the whole map view port, so if one of your zoom levels has a particularly small tile set (something in the order of 512px wide combined), then you may have to disable that zoom level.
Questions? Improvements? Got something to say about this? Send me an email.
See more,