feat: водоёмы в зоне поиска на карте (рекомендации профилей → геослой)
- GET /api/v1/water/{case_id}: полигоны водоёмов/болот OSM вокруг ТНП
(Overpass, natural=water+wetland, кэш 72ч, circuit breaker от geo_service);
бейдж-предупреждение по профилям случая (РАС/эпилепсия/не умеет плавать)
- geo_service: цепочка зеркал Overpass (maps.mail.ru → overpass-api.de):
основной сервер имеет AAAA, на хостах без IPv6-маршрута httpx падал
ConnectError'ом до IPv4-фолбэка; на CT108 фапало стабильно
- SearchMap: слой водоёмов под зонами (синие полигоны, болота пунктиром),
попапы с площадью/дистанцией от ТНП; AnalysisResult грузит слой,
показывает предупреждение профиля и счётчик водоёмов
- проверено на CT108: РАС-кейс (Минск) — 43 объекта, витя-кейс
(Каменец-борисовская обл.) — 7 объектов, включая водохранилище Загацце
This commit is contained in:
+29
-18
@@ -27,7 +27,14 @@ class Zone(BaseModel):
|
||||
# Cache configuration
|
||||
CACHE_DIR = Path("/tmp/overpass_cache")
|
||||
CACHE_TTL_HOURS = 24
|
||||
OVERPASS_URL = "https://overpass-api.de/api/interpreter"
|
||||
# Порядок зеркал: основной сервер имеет AAAA, но на хостах без IPv6-маршрута
|
||||
# (наш LXC в VPN-контуре) httpx падает на ConnectError ещё до IPv4-фолбэка.
|
||||
# Поэтому основной — зеркало без AAAA (maps.mail.ru), overpass-api.de — резерв.
|
||||
OVERPASS_URLS = [
|
||||
"https://maps.mail.ru/osm/tools/overpass/api/interpreter",
|
||||
"https://overpass-api.de/api/interpreter",
|
||||
]
|
||||
OVERPASS_URL = OVERPASS_URLS[0] # обратная совместимость для импортов
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -220,27 +227,31 @@ async def query_overpass(query: str) -> Dict:
|
||||
if _overpass_circuit_open():
|
||||
return {'elements': []}
|
||||
|
||||
# Query API
|
||||
# Query API — по цепочке зеркал (первый успешный ответ кэшируется)
|
||||
timeout = httpx.Timeout(30.0, connect=OVERPASS_CONNECT_TIMEOUT)
|
||||
async with httpx.AsyncClient(timeout=timeout) as client:
|
||||
try:
|
||||
response = await client.post(
|
||||
OVERPASS_URL,
|
||||
data={'data': query},
|
||||
headers={'Content-Type': 'application/x-www-form-urlencoded'}
|
||||
)
|
||||
response.raise_for_status()
|
||||
data = response.json()
|
||||
for overpass_url in OVERPASS_URLS:
|
||||
try:
|
||||
response = await client.post(
|
||||
overpass_url,
|
||||
data={'data': query},
|
||||
headers={'Content-Type': 'application/x-www-form-urlencoded'}
|
||||
)
|
||||
response.raise_for_status()
|
||||
data = response.json()
|
||||
|
||||
# Save to cache
|
||||
save_to_cache(cache_key, data)
|
||||
# Save to cache
|
||||
save_to_cache(cache_key, data)
|
||||
|
||||
_reset_overpass_circuit()
|
||||
return data
|
||||
except Exception as e:
|
||||
_record_overpass_failure()
|
||||
# Return empty result on error
|
||||
return {'elements': []}
|
||||
_reset_overpass_circuit()
|
||||
return data
|
||||
except Exception as e:
|
||||
logger.info("Overpass %s недоступен: %s", overpass_url, e)
|
||||
continue
|
||||
# Все зеркала недоступны
|
||||
_record_overpass_failure()
|
||||
# Return empty result on error
|
||||
return {'elements': []}
|
||||
|
||||
|
||||
def calculate_road_length(elements: List[Dict]) -> float:
|
||||
|
||||
Reference in New Issue
Block a user