Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .env_sample
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
API_KEY=<API_KEY_HERE>

# region_name:min_lat,min_lon,max_lat,max_lon; <more if desired...>
REGIONS="
southern_ontario:41.5, -83.5, 45.5, -75.0;
las_vegas:35.5, -116.0, 37.5, -114.0;
grand_canyon:35.5, -113.0, 37.0, -111.0
"
WORKERS=10
MINZOOM=1
MAXZOOM=14
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
maps/*
.env
32 changes: 12 additions & 20 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ This Python script downloads map tiles from Thunderforest's Mobile Atlas for spe
- Python 3.x
- `requests` library
- `tqdm` library
- `dotenv` library
- For KMLtoTiles: `fastkml` library

## Installation
Expand All @@ -30,33 +31,27 @@ This Python script downloads map tiles from Thunderforest's Mobile Atlas for spe
2. Install the required Python packages:

```bash
pip install requests tqdm fastkml
pip install -r requirements.txt
```
Note: fastkml is not required to use `TileDL.py`

## Configuration

1. Obtain an API key from [Thunderforest](https://www.thunderforest.com/docs/apikeys/).

2. Edit the script to include your API key:
2. copy .env_sample as .env and edit it to include your API key, desired regions, max workers, and zoom levels:

```python
api_key = "your_api_key_here"
```

3. Specify the regions and zoom levels you want to download in the script:

```python
# Define the bounding boxes and zoom levels
regions = {
"southern_ontario": (41.5, -83.5, 45.5, -75.0),
"las_vegas": (35.5, -116.0, 37.5, -114.0),
"grand_canyon": (35.5, -113.0, 37.0, -111.0)
}
zoom_levels = range(1, 15) # Focusing on zoom levels 1 to 14
```bash
API_KEY="your_api_key_here"
REGIONS="southern_ontario:41.5, -83.5, 45.5, -75.0;
las_vegas:35.5, -116.0, 37.5, -114.0;
grand_canyon:35.5, -113.0, 37.0, -111.0"
WORKERS=10
MINZOOM=1
MAXZOOM=14
```

4. Choose map style
2. Choose map style

```python
# mapstyle = "cycle"
Expand All @@ -70,9 +65,6 @@ This Python script downloads map tiles from Thunderforest's Mobile Atlas for spe
# mapstyle = "neighbourhood"
# mapstyle = "atlas"
```




## Usage (TileDL.py)

Expand Down
86 changes: 62 additions & 24 deletions TileDL.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,40 @@
import os
from dotenv import load_dotenv
import requests
from math import log, tan, cos, pi
from tqdm import tqdm
from concurrent.futures import ThreadPoolExecutor, as_completed

load_dotenv()

def load_regions_from_env():
regions_str = os.getenv("REGIONS", "")
regions = {}
if regions_str:
for region in regions_str.split(";"):
if region.strip():
parts = region.split(":")
if len(parts) == 2:
name = parts[0].strip()
values = tuple(float(v.strip()) for v in parts[1].split(","))
min_lat, min_lon, max_lat, max_lon = values

# Ensure min values are actually less than max values
if min_lon > max_lon:
print(f"Warning: min_lon ({min_lon}) > max_lon ({max_lon}) for region {name}, swapping")
min_lon, max_lon = max_lon, min_lon
if min_lat > max_lat:
print(f"Warning: min_lat ({min_lat}) > max_lat ({max_lat}) for region {name}, swapping")
min_lat, max_lat = max_lat, min_lat

regions[name] = (min_lat, min_lon, max_lat, max_lon)
return regions

# Define the bounding boxes and zoom levels. Below are random examples.
regions = {
"southern_ontario": (41.5, -83.5, 45.5, -75.0),
"las_vegas": (35.5, -116.0, 37.5, -114.0),
"grand_canyon": (35.5, -113.0, 37.0, -111.0)
}
zoom_levels = range(1, 15) # Focusing on zoom levels 1 to 14
regions = load_regions_from_env()
print(regions)
WORKERS=os.getenv("WORKERS", 10)
zoom_levels = range(int(os.getenv("MINZOOM", 1)), int(os.getenv("MAXZOOM", 14))+1) # Defaults to zoom levels 1 to 14

# mapstyle = "cycle"
# mapstyle = "transport"
Expand All @@ -23,8 +48,8 @@
# mapstyle = "atlas"

# API Key and output directory
api_key = "your_api_key_here"
output_dir = os.path.join(os.path.expanduser("~"), "Desktop", "tiles")
api_key = os.getenv("API_KEY")
output_dir = "./maps"
os.makedirs(output_dir, exist_ok=True)

def lon2tilex(lon, zoom):
Expand All @@ -33,43 +58,56 @@ def lon2tilex(lon, zoom):
def lat2tiley(lat, zoom):
return int((1.0 - log(tan(lat * pi / 180.0) + 1.0 / cos(lat * pi / 180.0)) / pi) / 2.0 * (1 << zoom))

def download_tile(zoom, x, y):
def download_tile(zoom, region_name, x, y):
url = f"https://tile.thunderforest.com/{mapstyle}/{zoom}/{x}/{y}.png?apikey={api_key}"
tile_dir = os.path.join(output_dir, str(zoom), str(x))
tile_dir = os.path.join(output_dir, region_name, str(zoom), str(x))
tile_path = os.path.join(tile_dir, f"{y}.png")

if os.path.exists(tile_path):
return

os.makedirs(tile_dir, exist_ok=True)

if not os.path.exists(tile_path):
response = requests.get(url)
if response.status_code == 200:
with open(tile_path, "wb") as file:
file.write(response.content)
else:
print(f"Failed to download tile {zoom}/{x}/{y}: {response.status_code} {response.reason}")
response = requests.get(url)
if response.status_code == 200:
with open(tile_path, "wb") as file:
file.write(response.content)
else:
print(f"Failed to download tile {zoom}/{x}/{y}: {response.status_code} {response.reason}")

def main():
total_tiles = 0

for zoom in zoom_levels:
for min_lat, min_lon, max_lat, max_lon in regions.values():
for region_name, (min_lat, min_lon, max_lat, max_lon) in regions.items():
start_x = lon2tilex(min_lon, zoom)
end_x = lon2tilex(max_lon, zoom)
start_y = lat2tiley(max_lat, zoom)
end_y = lat2tiley(min_lat, zoom)

#
print(zoom)
print(f"folders={end_x-start_x +1} tiles/folder = {end_y-start_y +1}" )
print(f"{(end_x - start_x + 1) * (end_y - start_y + 1)} Tiles at Zoom level {zoom}")
total_tiles += (end_x - start_x + 1) * (end_y - start_y + 1)

print(total_tiles, "total tiles")


with tqdm(total=total_tiles, desc="Downloading tiles") as pbar:
for zoom in zoom_levels:
for min_lat, min_lon, max_lat, max_lon in regions.values():
for region_name, (min_lat, min_lon, max_lat, max_lon) in regions.items():
start_x = lon2tilex(min_lon, zoom)
end_x = lon2tilex(max_lon, zoom)
start_y = lat2tiley(max_lat, zoom)
end_y = lat2tiley(min_lat, zoom)

for x in range(start_x, end_x + 1):
for y in range(start_y, end_y + 1):
download_tile(zoom, x, y)

tiles = [(x, y) for x in range(start_x, end_x + 1) for y in range(start_y, end_y + 1)]

with ThreadPoolExecutor(max_workers=WORKERS) as executor:
futures = {executor.submit(download_tile, zoom, region_name, x, y): (x, y) for x, y in tiles}
for future in as_completed(futures):
x, y = futures[future]
pbar.set_description(f"📁:{x - start_x + 1}/{end_x-start_x +1} 🖼️:{y - start_y + 1}/{end_y-start_y +1} of 🔍:{zoom}")
pbar.update(1)

if __name__ == "__main__":
Expand Down
Binary file added requirements.txt
Binary file not shown.