Two separate Flask apps in one repository. Both put map tiles on your own storage for offline use; they differ in what you point them at. Neither one touches the other — run either, or both at once, on different ports.
v1 — app.py |
v2 — app_nav.py |
|
|---|---|---|
| You select | a rectangle on the map | a route through the points you click, or a rectangle with no route |
| It downloads | every tile in the box | a corridor around the route, or every tile in the rectangle |
| You get | tiles.zip or tiles.mbtiles |
a ready-to-unzip SD card, tiles already in RGB565 |
| Turn instructions | — | route.bin with geometry, per-zoom detail and turns |
| Tile format | PNG, convert them yourself afterwards | LVGL v9 RGB565 .bin, converted in memory |
| Tile sources | public OpenStreetMap and ArcGIS | only sources whose terms permit offline use; API key entered in the page |
| Runs on | http://127.0.0.1:5000 |
http://127.0.0.1:5001 |
| Made for | any offline map viewer | map_tiles v2.0.0 navigation on ESP32 |
| Documented in | this file, below | this file, below and README_NAV.md |
Which do you want? For a patch of map to pan around on a v1 project, that is v1, and it is unchanged. For the v2 firmware, that is v2: a route with turn instructions, or a patch of map with no route at all, either way on an SD card the device reads as it is.
No Python module is shared between the two apps, so a change to one cannot break the other.
| v1 | v2 | |
|---|---|---|
| App | app.py |
app_nav.py |
| Page | templates/index.html |
templates/nav_index.html |
| Requirements | requirements.txt |
requirements_nav.txt (adds Pillow) |
| README | README.md (the v1 half, below) |
README_NAV.md |
| Also imports | — | tile_sources.py, settings.py, route_format.py |
| Tile cache | tiles/<style>/ |
tiles_v2/<source>/ |
| Settings | none | ~/.config/offline-map-downloader/settings.json |
app.py imports nothing from app_nav.py and vice versa. The two only share
static/ (favicon and stylesheet), LICENSE and this README.
This section is v2. The full v2 manual is README_NAV.md; what follows is the tour. For the original rectangle downloader, jump to v1.
The v1 rectangle is the right shape for a map you pan around by hand, and the wrong one for navigation. Building a route onto a device ran into four walls, and none of them could be fixed by choosing a better rectangle.
A route's bounding box is mostly tiles you will never see. A 20 km drive across a city needs the streets along it, not the square it happens to sit in. Worked out for one running diagonally at z15–17: about 850 tiles for a 300 m corridor, against about 4,300 for its bounding box — and the box grows with the square of the distance.
Tiles alone are not navigation. The device also needs the line to follow and the turns along it. That meant exporting a GPX somewhere else, and still having nothing to say at a junction.
The tiles needed converting afterwards. v1 hands you PNGs; the device reads
RGB565 with a 12-byte header, so every build ended with a separate pass of
lvgl_map_tile_converter.py over the whole folder.
And the public OpenStreetMap servers stop you. Their usage policy forbids bulk downloading and the servers enforce it — past the limit you stop receiving tiles and start receiving an "Access Blocked — App is not following the tile usage policy" picture, with HTTP 200, which lands on the card looking like any other tile. You find out on the device.
Click waypoints, plan, build. Out comes one zip you unpack onto the card:
navigation_sdcard.zip
├── tiles1/<zoom>/<x>/<y>.bin RGB565, converted in memory
├── routes/my-route.bin geometry, per-zoom detail, turn instructions
├── routes/my-route.txt what this build contained
└── README.txt how to merge more routes onto the card
Every point you click is a via point, so the route goes the way you drew it rather than the way the router would have preferred.
Switch to Area · tiles only, draw a rectangle, build: the same ready-to-unzip card with no route in it. The v2 firmware opens it as a map on its own, with your position on it and nothing to follow.
Tick Return to the start and the route closes. The route file records that, and the device laps it instead of arriving. Pick Exercise as well and the panel counts elapsed time and distance instead of counting down what is left.
Pick a tile source and, if it needs a key, the field appears underneath. Saving
takes effect immediately — no restart, no re-exporting in every new shell. Keys
live in ~/.config/offline-map-downloader/settings.json, outside the repository
so one cannot be committed by accident, and are only ever sent back to the page
masked.
v2 will not build a card from the public OpenStreetMap servers. It offers the ones that permit offline use instead, and shows what each one's terms actually say — they differ a great deal, and two of the best-known names say no on their standard plans. Geoapify has a free tier that allows it; self-hosting has no quota at all.
It also watches what comes back: if the same bytes keep arriving for different coordinates, that is an error picture rather than a map, and the build stops rather than writing several hundred copies of it to your card. (A plain one-colour tile, like open sea, is allowed to repeat.)
Tiles are cached per source, and so are the absences. Rebuilding the same route costs zero requests — measured on an 82-tile corridor: 82 cold, 0 on every rebuild after.
Unzip as many builds as you like onto the same card. Each route records the tile folder it belongs to, so routes over the same ground share those tiles instead of carrying a copy each, and a route in another region gets its own folder.
/tiles_oc/... /routes/sna-to-home.bin, /routes/sunday-circuit.bin
/tiles_tahoe/... /routes/holiday.bin
pip install -r requirements_nav.txt
python app_nav.py👉 http://127.0.0.1:5001 — then set a tile source in the page.
Already have a GPX? python route_format.py --gpx ride.gpx --out route.bin
📖 README_NAV.md has the rest: which tile sources permit
what, self-hosting, the route.bin format, how laps are counted, and how to put
several routes on one card.
The card v2 builds is read by
map_tiles v2.0.0 or later — the release
that added the navigation layer. Firmware that navigates from it, on a round
AMOLED, is in
map_tiles_projects/v2.
A v1 card built with app.py still works with the v1 projects, once its PNGs
have been through lvgl_map_tile_converter.py.
From here on the page is v1, apart from the Dependencies and Attribution sections at the end, which cover both. v1 itself is unchanged from before v2 existed. For routes and navigation, go back to v2 or read README_NAV.md.
This is a Flask web application that allows you to select a geographic area on a map and download OpenStreetMap or Satellite tiles as a .zip or .mbtiles file for offline use.
v1 is intended for personal, educational, or experimental use only.
It does not use any API keys or authenticated tile services, and fetches tiles directly from public endpoints like OpenStreetMap and ArcGIS. As such:
Do not use this tool for commercial applications or large-scale automated downloads.
Please respect the tile providers' usage policies.
This is one of the reasons v2 exists: it refuses to build a card from the public OpenStreetMap servers and offers sources whose terms allow offline use instead. See Uses tile sources that allow this.
- 📍 Select area with a rectangle on the map
- 🔍 Choose zoom level range (10–19)
- 🌐 Switch between OpenStreetMap and Satellite view
- 🧮 Preview tile count before download
- 🎨 Live preview of selected area using actual map tiles
- 💾 Export to
.zipor.mbtiles
git clone https://github.com/0015/OfflineMapDownloader.git
cd OfflineMapDownloaderpython3 -m venv .venv
source .venv/bin/activate # macOS/Linux
# OR
.venv\Scripts\activate # Windowspip install -r requirements.txtpython app.pyThen open your browser and go to:
👉 http://127.0.0.1:5000
(v2 is python app_nav.py on port 5001, so the two can run side by side.)
tiles.zip– folder structure with PNG tiles by zoom/x/ytiles.mbtiles– SQLite-based format (flat database file)
v1 — requirements.txt |
v2 — requirements_nav.txt |
|
|---|---|---|
Flask |
✅ | ✅ |
requests |
✅ | ✅ |
Pillow |
— | ✅ (RGB565 conversion in memory) |
Install one or the other, or both into the same virtual environment.
MIT License
(c) 2025 Eric Nam / ThatProject
v1 map tiles provided by:
v2 does not download from those endpoints; its page only shows an OpenStreetMap map to click on, which is ordinary browsing. It lists the attribution its chosen tile source requires and writes it onto the card alongside the tiles — see Where the tiles come from. Routing in v2 is by OSRM for driving, and by Valhalla for walking, cycling and driving that avoids tolls or highways.
This project was inspired by AliFlux/MapTilesDownloader
Special thanks to their work on simplifying tile downloading logic.
Created by @ThatProject



