fa466fc5ed
- Step3Circumstances: удалено дублирующее «Последнее известное место» (last_known_place); остаётся только tnp_address на шаге координат. - Step4Environment: кнопка «Найти координаты» теперь РАБОЧАЯ — вызывает новый GET /api/v1/geocode (прокси Nominatim, countrycodes=by, короткий запрос «пункт, адрес» по граблям geocoding), заполняет lat/lon, показывает найденный display_name или ошибку. Без debounce-автоплейса (Pitfall 2) — только явный клик. - Step3Circumstances: «Прошло времени» считается автоматически из lost_time (пересчёт каждые 30 с, округление до 0.1 ч); поле остаётся редактируемым для корректировки. - БАГФИКС: форма шлёт lost_time, модель хранит loss_time — алиас в _normalize_case_data. Раньше время пропажи молча терялось (Pydantic игнорирует неизвестные поля) — elapsed_hours приходилось вводить руками. - last_known_place убран из валидации шага 4. - backend/routers/geocode.py: Nominatim-прокси с auth (operator/field/admin). 192 passed, JSX валиден.
74 lines
2.5 KiB
Python
74 lines
2.5 KiB
Python
"""Геокодирование: прокси к Nominatim (OpenStreetMap).
|
|
|
|
GET /api/v1/geocode?q=<запрос>&limit=5
|
|
Возвращает {results: [{lat, lng, display_name, type}]}.
|
|
|
|
Nominatim Usage Policy: User-Agent обязателен, ≤1 rps — проксируем через
|
|
бэкенд с корректным UA и ограничением countrycodes=by.
|
|
|
|
Правила запросов (nominatim-грабли, см. homelab-ops skill):
|
|
- Короткий запрос: пункт + адрес, БЕЗ района/области.
|
|
- Пункт ПЕРВЫМ словом.
|
|
"""
|
|
import os
|
|
from typing import List, Optional
|
|
|
|
import httpx
|
|
from fastapi import APIRouter, HTTPException, Query
|
|
from pydantic import BaseModel
|
|
|
|
from backend.routers.auth import require_roles
|
|
from fastapi import Depends
|
|
|
|
router = APIRouter(prefix='/api/v1/geocode', tags=['geocode'])
|
|
|
|
NOMINATIM_URL = 'https://nominatim.openstreetmap.org/search'
|
|
USER_AGENT = os.getenv('NOMINATIM_USER_AGENT', 'Vector-MChS/1.0 (geocoding proxy)')
|
|
COUNTRY_CODE = 'by'
|
|
|
|
|
|
class GeocodeResult(BaseModel):
|
|
lat: float
|
|
lng: float
|
|
display_name: str
|
|
type: Optional[str] = None
|
|
|
|
|
|
class GeocodeResponse(BaseModel):
|
|
results: List[GeocodeResult]
|
|
|
|
|
|
@router.get('', response_model=GeocodeResponse,
|
|
dependencies=[Depends(require_roles(['operator', 'field', 'admin']))])
|
|
async def geocode(
|
|
q: str = Query(..., min_length=2, description='Адрес: «пункт, улица»'),
|
|
limit: int = Query(5, ge=1, le=10),
|
|
):
|
|
"""Геокодирование через Nominatim, ограничено Беларусью."""
|
|
headers = {'User-Agent': USER_AGENT}
|
|
params = {
|
|
'q': q.strip(),
|
|
'format': 'jsonv2',
|
|
'limit': limit,
|
|
'countrycodes': COUNTRY_CODE,
|
|
'addressdetails': 1,
|
|
}
|
|
try:
|
|
async with httpx.AsyncClient(timeout=10) as client:
|
|
resp = await client.get(NOMINATIM_URL, headers=headers, params=params)
|
|
resp.raise_for_status()
|
|
data = resp.json()
|
|
except httpx.TimeoutException:
|
|
raise HTTPException(status_code=504, detail='Сервис геокодирования не ответил')
|
|
except Exception as e:
|
|
raise HTTPException(status_code=502, detail=f'Ошибка геокодирования: {e}')
|
|
|
|
return {'results': [
|
|
GeocodeResult(
|
|
lat=float(item['lat']),
|
|
lng=float(item['lon']),
|
|
display_name=item.get('display_name', ''),
|
|
type=item.get('type'),
|
|
)
|
|
for item in data
|
|
]} |