In this tutorial, we’ll walk through how to create a custom map using a large image in Leaflet, a lightweight JavaScript library for building interactive maps in web applications.
This technique is useful when working with non-geographical maps or images that you want to make interactive, zoomable, and possibly annotated.
Why Use Custom Maps?
Here are some use cases where custom maps from image are applicable:
- Indoor Maps: This is a common use case—think of malls, airports, or exhibition halls where a visual indoor layout helps visitors navigate.
- Video Game Maps: Many games come with expansive in-game worlds. Custom maps allow for interactive player guides. For example, The Forest Map provides an interactive map for players of The Forest game.
- Zoomable Images: You’re not limited to maps. If you have a large image, such as a blueprint, artwork, or infographic, you can use Leaflet to let users zoom into specific areas without loading the entire image at once. You can even add markers or annotations for more context. (ex. ESO’s 9-gigapixel zoomable image of the Milky Way galaxy, even though this is using HTML5 canvas, you can also make something similar using Leaflet. )
Preparation and Setup
Before we begin, make sure you have the following tools installed:
- Geospatial Data Abstraction Library (GDAL) – a powerful library for reading and processing raster and vector geospatial data.
- Python 3 (with GDAL Python bindings)
- npm – for setting up and running our web application.
Installing GDAL and Python
If you don’t have GDAL installed yet, the recommended method (which we’ll use in this tutorial) is via Anaconda or Miniconda. This approach is beginner-friendly and includes the required Python bindings out of the box.
I would recommend checking out this step-by-step GDAL installation guide for installing GDAL with Anaconda
While other installation methods are available (e.g., via Homebrew, apt, or pip), using Anaconda ensures fewer compatibility issues.
Tiling The Image
Getting the Sample Image
For this tutorial, we'll use a sample image named treasure-map.jpg. Open the link then download the file provided.
You can also substitute this with your own image, keeping in mind the recommendations regarding image dimensions discussed later.
Tiling with gdal2tiles.py
Assuming that you’ve followed the provided step-by-step guide for installing GDAL earlier, be sure to activate the conda environment:
conda activate geospatial
Now, let's generate the image tiles. We'll use the following command:
gdal2tiles.py --xyz -p raster -z 0-2 -w leaflet treasure-map.jpg map/
Let's break down each parameter:
-
--xyz
: This crucial flag ensures that the generated tile coordinates are compatible with common web mapping libraries like Leaflet. Without--xyz
,gdal2tiles.py
defaults to TMS (Tile Map Service) coordinate system, which can result in jumbled or incorrectly displayed tiles in Leaflet. -
-p raster
: This parameter simply indicates that our input file is a raster image. -
-z 0-2
: This specifies the zoom range for tile generation. We are generating tiles from zoom level 0 (the most zoomed-out view) up to zoom level 2 (more zoomed-in). Generating more zoom levels will create a larger number of tile files, but it allows for greater detail as the user zooms in. -
-w leaflet
: This parameter specifies the desired web viewer. Whilegdal2tiles.py
doesn't strictly generate a full-fledged web viewer for Leaflet (you'll still need to write some HTML/JavaScript for that), setting it toleaflet
prevents the generation of files for other map web viewers that you might not need. You can also set this tonone
if you prefer to avoid generating any viewer-specific files.
For a complete list of options, refer to the official gdal2tiles CLI documentation.
Understanding Zoom Levels and Image Dimensions
The choice of zoom levels is directly related to your image's dimensions. Web map tiles typically have a size of 256x256 pixels. Each subsequent zoom level doubles the resolution of the previous level, effectively quadrupling the number of tiles needed to cover the same area.
The table below illustrates the relationship between zoom level and the size of the area covered by a single tile at that level:
zoom level | size(px) |
---|---|
0 | 256 |
1 | 512 |
2 | 1024 |
3 | 2048 |
4 | 4096 |
5 | 8192 |
Since our treasure-map.jpg
is 1024x1024, the maximum recommended zoom level is 2. Going beyond this ideal zoom level would not add further detail but might simply stretch or pixelate the image, affecting its quality.
This can be calculated based on the size of a leaflet tile, which is 256px and has each square size increasing based on the power of 2.
imagesize = max(height, width)
ceiling( log(imagesize / tilesize) / log(2) )
After running the gdal2tiles.py
command, you will find a map/
directory containing subdirectories for each zoom level, and within those, individual image tiles. We’ll be using these tiles in our web mapping application, such as Leaflet, by pointing the map’s tile layer to the map/
directory.
Creating the Web Application with Vite and Leaflet
Now that we have our image tiles, let's build a simple web application to display them using Vite and Leaflet. Vite provides a fast development experience with features like hot module replacement and a local development server.
Initialize the Project with Vite
First, navigate to your desired projects folder in the terminal and create a new Vite project. We'll use the vanilla
template for a basic setup. You can name your project anything you like; here, we'll use my-leaflet-app
:
npm create vite@latest my-leaflet-app -- --template vanilla
This command will create a new directory named my-leaflet-app
(or whatever you specified). Follow the on-screen instructions, which will typically guide you to:
-
Change into the new project directory:
cd my-leaflet-app
-
Install the project dependencies:
npm install
-
Start the development server:
This will open your application in a browser, and it will automatically reload whenever you make changes to your code.
npm run dev
Install Required Packages
Next, we need to install the core libraries for our map application:
-
leaflet
: The popular open-source JavaScript library for interactive maps. -
leaflet-rastercoords
: A useful utility for projecting pixel coordinates from our image to Leaflet's map coordinates.
Install them using npm:
npm i -S leaflet leaflet-rastercoords
Creating the Map
Now that our project is set up and dependencies are installed, let's clean up the default Vite code and start building our map.
-
Clean up
src/main.js
andsrc/style.css
: Remove all existing content from these two files. -
Add Styles to
src/style.css
: Add the following CSS to style our web page and the map container:
body {
font-family: "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif;
color: #505050;
display: block;
max-width: 768px;
width: 100%;
margin: 0 auto;
}
#app {
max-width: 768px;
border: 1px solid #eee;
width: 100%;
height: 550px;
}
We've defined a `div` with the ID `app` where our map will be rendered.
-
Initialize the Map in
src/main.js
: Now, let's add the JavaScript code tosrc/main.js
to create and display our Leaflet map.
import "leaflet/dist/leaflet.css";
import "./style.css";
import * as L from "leaflet";
import "leaflet-rastercoords";
// Note for Markers:
// Since Leaflet's default marker icons don't work properly with package bundlers like Vite,
// we need to explicitly import and re-create them.
import marker from "leaflet/dist/images/marker-icon.png";
import marker2x from "leaflet/dist/images/marker-icon-2x.png";
import markerShadow from "leaflet/dist/images/marker-shadow.png";
function init() {
const container = document.getElementById("app");
if (!container) throw new Error('There is no div with the id: "app" ');
const height = 1024;
const width = 1024;
const imageDimensions = [width, height];
const map = L.map(container, {
center: L.latLng(0, 0),
noWrap: true,
crs: L.CRS.Simple,
});
L.tileLayer("/map/{z}/{x}/{y}.png", {
noWrap: true,
maxZoom: 2,
}).addTo(map);
// Initialize Leaflet.RasterCoords with the map and image dimensions
const rc = new L.RasterCoords(map, imageDimensions);
map.setView(rc.unproject([width, height]), 1);
// Re-create Leaflet's default icon for markers (due to bundler issues)
const icon = L.icon({
iconUrl: marker,
iconRetinaUrl: marker2x,
shadowUrl: markerShadow,
iconSize: [25, 41],
iconAnchor: [12, 41],
popupAnchor: [1, -34],
shadowSize: [41, 41],
});
// Example of adding a marker at specific pixel coordinates
// L.marker(rc.unproject([611, 433]), { icon }).addTo(map);
}
init();
Copying the Tiles to the Public Folder
For our Vite development server to serve the generated map tiles, we need to copy the map/
folder (created in the previous gdal2tiles.py
step) into the public/
directory of your Vite project.
If your local development server (npm run dev
) is still running, you should now be able to see your treasure-map.jpg
image displayed as a zoomable map in your browser!
Projecting Coordinates with leaflet-rastercoords
and L.CRS.Simple
In our main.js, you might have noticed the following lines when initializing the map:
const map = L.map(container, {
center: L.latLng(0, 0),
noWrap: true,
crs: L.CRS.Simple,
});
// ...
const rc = new L.RasterCoords(map, imageDimensions);
map.setView(rc.unproject([width, height]), 1);
Here's why leaflet-rastercoords
and CRS.Simple
are essential for working with custom image maps:
Understanding Coordinate Reference Systems (CRS)
Typically, web maps use a geographic coordinate system (like EPSG:3857, also known as Web Mercator) to represent real-world locations. However, for our custom image map, we're not dealing with geographic coordinates (latitude and longitude). Instead, we're working directly with the pixel coordinates of our image. This is where CRS.Simple
comes in. It's a special, non-projected coordinate reference system provided by Leaflet that treats pixel coordinates as its primary unit.
CRS.Simple
's origin starts at the top left corner of the map (0,0) and the X and Y coordinates increases as you move to the right and down respectively. This setup aligns with how pixel coordinates are typically referenced in most photo editing softwares.
For more in-depth guide and examples, check out the Leaflet's official post regarding non-geographical maps
The Role of leaflet-rastercoords
While L.CRS.Simple
provides the foundation, leaflet-rastercoords
significantly simplifies the process of integrating our image with Leaflet's map functions. This utility library makes it easy to convert between your image's raw pixel coordinates.
This library also automatically handles the map boundaries so that cannot scroll endlessly outside the bounds of your image, as it sets the pan limits.
This means you can easily work with the familiar pixel coordinates from your image (e.g., obtained directly from a photo editing software like GIMP or Photoshop) and convert them to map locations for adding markers, polygons, or other interactive elements.
For example, if you want to place a marker at pixel coordinates X: 611, Y: 433 on our sample image treasure-map.jpg
, you can do so by using rc.unproject()
to set the coordinates:
L.marker(rc.unproject([611, 433]), { icon }).addTo(map);
After adding this line and refreshing your browser (or if Vite automatically reloads), you should see a marker appear and the coordinate based the Y and X coordinates.
Conclusion
In this tutorial we have created a custom zoomable map in Leaflet from an existing image. This setup provides a foundation for building more custom image-based map. Feel free to experiment by using different images, adding more layers such as markers and shapes while exploring other leaflet functionalities as well.
You can find the complete source code for this project at the Github Repository: Leaflet Raster to Tiles Example
References:
Zoomable images with Leaflet - I used this as basis for my earlier project for an earlier project back in 2013. Maptiler was used on that project instead ofgdal2tiles.py
.
Cover Image by Pexels from Pixabay
Top comments (0)