Initial import: WebAisMap

Closes TG-4

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-05-04 07:56:45 +03:00
commit 03075f1ef1
1460 changed files with 16334 additions and 0 deletions
+261
View File
@@ -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()