#!/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()