Initial import: WebAisMap
Closes TG-4 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,261 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Скрипт для скачивания тайлов карты OpenStreetMap для офлайн использования.
|
||||
Соблюдает политику использования тайлов OpenStreetMap:
|
||||
- Использует корректный User-Agent
|
||||
- Соблюдает минимальную задержку 1 секунда между запросами
|
||||
- Обрабатывает ошибки rate limiting
|
||||
|
||||
Использование:
|
||||
python download_tiles.py --min-zoom 5 --max-zoom 12 --bounds 55.5,37.5,56.0,38.0
|
||||
"""
|
||||
|
||||
import os
|
||||
import math
|
||||
import argparse
|
||||
import requests
|
||||
from pathlib import Path
|
||||
import time
|
||||
|
||||
|
||||
def deg2num(lat_deg, lon_deg, zoom):
|
||||
"""Преобразует координаты в номер тайла"""
|
||||
lat_rad = math.radians(lat_deg)
|
||||
n = 2.0 ** zoom
|
||||
xtile = int((lon_deg + 180.0) / 360.0 * n)
|
||||
ytile = int((1.0 - math.asinh(math.tan(lat_rad)) / math.pi) / 2.0 * n)
|
||||
return (xtile, ytile)
|
||||
|
||||
|
||||
def num2deg(xtile, ytile, zoom):
|
||||
"""Преобразует номер тайла в координаты"""
|
||||
n = 2.0 ** zoom
|
||||
lon_deg = xtile / n * 360.0 - 180.0
|
||||
lat_rad = math.atan(math.sinh(math.pi * (1 - 2 * ytile / n)))
|
||||
lat_deg = math.degrees(lat_rad)
|
||||
return (lat_deg, lon_deg)
|
||||
|
||||
|
||||
def download_tile(z, x, y, output_dir, retries=3, delay=1.0):
|
||||
"""Скачивает один тайл с соблюдением политики использования OpenStreetMap"""
|
||||
tile_dir = os.path.join(output_dir, str(z), str(x))
|
||||
os.makedirs(tile_dir, exist_ok=True)
|
||||
|
||||
tile_path = os.path.join(tile_dir, f"{y}.png")
|
||||
|
||||
# Если тайл уже существует, пропускаем
|
||||
if os.path.exists(tile_path):
|
||||
return True
|
||||
|
||||
# URL для скачивания тайла
|
||||
# Используем разные поддомены для распределения нагрузки
|
||||
subdomains = ['a', 'b', 'c']
|
||||
subdomain = subdomains[(x + y) % len(subdomains)]
|
||||
url = f"https://{subdomain}.tile.openstreetmap.org/{z}/{x}/{y}.png"
|
||||
|
||||
# Заголовки согласно политике использования OpenStreetMap
|
||||
headers = {
|
||||
'User-Agent': 'PythonAisMap/1.0 (Offline Map Tile Downloader; contact: localhost)',
|
||||
'Referer': 'http://localhost:8000/'
|
||||
}
|
||||
|
||||
for attempt in range(retries):
|
||||
try:
|
||||
response = requests.get(url, headers=headers, timeout=30)
|
||||
|
||||
if response.status_code == 200:
|
||||
with open(tile_path, 'wb') as f:
|
||||
f.write(response.content)
|
||||
time.sleep(delay) # Задержка между запросами (минимум 1 сек согласно политике)
|
||||
return True
|
||||
elif response.status_code == 404:
|
||||
# Тайл не существует (например, для океана)
|
||||
return False
|
||||
elif response.status_code == 429:
|
||||
# Too Many Requests - нужно увеличить задержку
|
||||
wait_time = delay * (2 ** attempt)
|
||||
print(f" Превышен лимит запросов для {z}/{x}/{y}, ожидание {wait_time} сек...")
|
||||
time.sleep(wait_time)
|
||||
continue
|
||||
elif response.status_code == 403:
|
||||
# Forbidden - возможно, нарушена политика использования
|
||||
print(f" Доступ запрещен для {z}/{x}/{y}. Проверьте User-Agent и задержки.")
|
||||
return False
|
||||
else:
|
||||
print(f" Неожиданный статус {response.status_code} для {z}/{x}/{y}")
|
||||
if attempt < retries - 1:
|
||||
time.sleep(delay * (attempt + 1))
|
||||
else:
|
||||
return False
|
||||
except requests.exceptions.Timeout:
|
||||
if attempt < retries - 1:
|
||||
print(f" Таймаут для {z}/{x}/{y}, повторная попытка...")
|
||||
time.sleep(delay * (attempt + 1))
|
||||
else:
|
||||
print(f" Ошибка таймаута при скачивании тайла {z}/{x}/{y}")
|
||||
return False
|
||||
except Exception as e:
|
||||
if attempt < retries - 1:
|
||||
time.sleep(delay * (attempt + 1))
|
||||
else:
|
||||
print(f" Ошибка при скачивании тайла {z}/{x}/{y}: {e}")
|
||||
return False
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def download_tiles(min_lat, min_lon, max_lat, max_lon, min_zoom, max_zoom, output_dir="static/tiles", delay=1.0):
|
||||
"""Скачивает тайлы для указанной области и уровней масштабирования"""
|
||||
print(f"Начинаем скачивание тайлов...")
|
||||
print(f"Область: ({min_lat}, {min_lon}) - ({max_lat}, {max_lon})")
|
||||
print(f"Уровни масштабирования: {min_zoom} - {max_zoom}")
|
||||
print(f"Выходная папка: {output_dir}")
|
||||
|
||||
total_tiles = 0
|
||||
downloaded_tiles = 0
|
||||
skipped_tiles = 0
|
||||
|
||||
for z in range(min_zoom, max_zoom + 1):
|
||||
print(f"\nОбработка уровня масштабирования {z}...")
|
||||
|
||||
# Определяем диапазон тайлов для данного уровня
|
||||
min_tile_x, max_tile_y = deg2num(min_lat, min_lon, z)
|
||||
max_tile_x, min_tile_y = deg2num(max_lat, max_lon, z)
|
||||
|
||||
# Убеждаемся, что координаты в правильном порядке
|
||||
if min_tile_x > max_tile_x:
|
||||
min_tile_x, max_tile_x = max_tile_x, min_tile_x
|
||||
if min_tile_y > max_tile_y:
|
||||
min_tile_y, max_tile_y = max_tile_y, min_tile_y
|
||||
|
||||
level_tiles = (max_tile_x - min_tile_x + 1) * (max_tile_y - min_tile_y + 1)
|
||||
total_tiles += level_tiles
|
||||
|
||||
print(f" Тайлов на уровне {z}: {level_tiles} ({min_tile_x}-{max_tile_x}, {min_tile_y}-{max_tile_y})")
|
||||
|
||||
level_downloaded = 0
|
||||
level_skipped = 0
|
||||
|
||||
for x in range(min_tile_x, max_tile_x + 1):
|
||||
for y in range(min_tile_y, max_tile_y + 1):
|
||||
if download_tile(z, x, y, output_dir, delay=delay):
|
||||
level_downloaded += 1
|
||||
downloaded_tiles += 1
|
||||
else:
|
||||
level_skipped += 1
|
||||
skipped_tiles += 1
|
||||
|
||||
# Показываем прогресс каждые 10 тайлов
|
||||
if (level_downloaded + level_skipped) % 10 == 0:
|
||||
print(f" Прогресс: {level_downloaded + level_skipped}/{level_tiles} тайлов")
|
||||
|
||||
print(f" Уровень {z} завершен: скачано {level_downloaded}, пропущено {level_skipped}")
|
||||
|
||||
print(f"\n=== Итоги ===")
|
||||
print(f"Всего тайлов: {total_tiles}")
|
||||
print(f"Скачано: {downloaded_tiles}")
|
||||
print(f"Пропущено: {skipped_tiles}")
|
||||
print(f"Готово!")
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Скачивание тайлов OpenStreetMap для офлайн использования"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--min-zoom",
|
||||
type=int,
|
||||
default=5,
|
||||
help="Минимальный уровень масштабирования (по умолчанию: 5)"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--max-zoom",
|
||||
type=int,
|
||||
default=12,
|
||||
help="Максимальный уровень масштабирования (по умолчанию: 12)"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--delay",
|
||||
type=float,
|
||||
default=1.0,
|
||||
help="Задержка между запросами в секундах (по умолчанию: 1.0, минимум рекомендуется 1.0)"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--bounds",
|
||||
type=str,
|
||||
help="Границы области в формате: min_lat,min_lon,max_lat,max_lon (например: 55.5,37.5,56.0,38.0)"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--center",
|
||||
type=str,
|
||||
help="Центр области и радиус в формате: lat,lon,radius_km (например: 55.75,37.62,50)"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--output",
|
||||
type=str,
|
||||
default="static/tiles",
|
||||
help="Выходная папка для тайлов (по умолчанию: static/tiles)"
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# Определяем границы области
|
||||
if args.bounds:
|
||||
parts = args.bounds.split(",")
|
||||
if len(parts) != 4:
|
||||
print("Ошибка: --bounds должен содержать 4 значения: min_lat,min_lon,max_lat,max_lon")
|
||||
return
|
||||
min_lat, min_lon, max_lat, max_lon = map(float, parts)
|
||||
elif args.center:
|
||||
parts = args.center.split(",")
|
||||
if len(parts) != 3:
|
||||
print("Ошибка: --center должен содержать 3 значения: lat,lon,radius_km")
|
||||
return
|
||||
center_lat, center_lon, radius_km = map(float, parts)
|
||||
# Приблизительно вычисляем границы на основе радиуса
|
||||
# 1 градус широты ≈ 111 км
|
||||
lat_delta = radius_km / 111.0
|
||||
# 1 градус долготы зависит от широты
|
||||
lon_delta = radius_km / (111.0 * math.cos(math.radians(center_lat)))
|
||||
min_lat = center_lat - lat_delta
|
||||
max_lat = center_lat + lat_delta
|
||||
min_lon = center_lon - lon_delta
|
||||
max_lon = center_lon + lon_delta
|
||||
else:
|
||||
# По умолчанию: Москва и окрестности
|
||||
print("Используются границы по умолчанию: Москва и окрестности")
|
||||
min_lat, min_lon = 55.5, 37.3
|
||||
max_lat, max_lon = 56.0, 37.9
|
||||
|
||||
# Проверяем валидность границ
|
||||
if min_lat >= max_lat or min_lon >= max_lon:
|
||||
print("Ошибка: неверные границы области")
|
||||
return
|
||||
|
||||
if args.min_zoom < 0 or args.max_zoom > 19 or args.min_zoom > args.max_zoom:
|
||||
print("Ошибка: неверные уровни масштабирования (должны быть от 0 до 19, min <= max)")
|
||||
return
|
||||
|
||||
# Создаем выходную папку
|
||||
os.makedirs(args.output, exist_ok=True)
|
||||
|
||||
# Проверяем задержку (рекомендуется минимум 1 секунда)
|
||||
if args.delay < 1.0:
|
||||
print("ВНИМАНИЕ: Задержка менее 1 секунды может привести к блокировке!")
|
||||
print("Рекомендуется использовать задержку не менее 1.0 секунды.")
|
||||
response = input("Продолжить с текущей задержкой? (y/n): ")
|
||||
if response.lower() != 'y':
|
||||
return
|
||||
|
||||
# Скачиваем тайлы
|
||||
download_tiles(
|
||||
min_lat, min_lon, max_lat, max_lon,
|
||||
args.min_zoom, args.max_zoom,
|
||||
args.output,
|
||||
args.delay
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
Reference in New Issue
Block a user