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:
2026-09-09 12:48:44 +03:00
parent 2d264a5779
commit 98e8e58023
5 changed files with 366 additions and 21 deletions
@@ -33,6 +33,7 @@ const AnalysisResult = () => {
const [error, setError] = useState(null);
const [caseData, setCaseData] = useState(null);
const [analysisData, setAnalysisData] = useState(null);
const [waterData, setWaterData] = useState(null);
useEffect(() => {
const loadCaseAndAnalyze = async () => {
@@ -55,6 +56,14 @@ const AnalysisResult = () => {
const analysis = await analyzeResponse.json();
setAnalysisData(analysis);
// 3. Водоёмы вокруг ТНП (слой на карте; 404/409 — нет координат, не ошибка)
try {
const waterResponse = await apiFetch(`/api/v1/water/${caseId}`);
if (waterResponse.ok) setWaterData(await waterResponse.json());
} catch {
/* водоёмы — опциональный слой, не блокируют анализ */
}
} catch (err) {
console.error("Error loading case:", err);
setError(err.message);
@@ -262,10 +271,24 @@ const AnalysisResult = () => {
{tnpLat && tnpLon && (
<div className="map-section">
<h2>Карта зон поиска</h2>
{waterData?.warning && (
<div style={{ marginBottom: '10px', padding: '10px', background: '#2d1b0e', border: '1px solid #ff4d00', borderRadius: '6px' }}>
<p style={{ margin: 0, fontSize: '13px', color: '#ff4d00' }}>
🚨 {waterData.warning}
</p>
</div>
)}
{waterData?.count > 0 && (
<p style={{ margin: '0 0 8px 0', fontSize: '13px', color: '#8b949e' }}>
💧 Водоёмы в зоне поиска: {waterData.count}
{waterData.warning && ' — приоритетная проверка'} (данные OSM)
</p>
)}
<SearchMap zones={zones}
maxDistance={parseFloat(analysisData?.max_distance_km)}
tnpLat={parseFloat(tnpLat)}
tnpLon={parseFloat(tnpLon)}
waterFeatures={waterData?.features || []}
/>
<div className="map-info" style={{ marginTop: "12px", fontSize: "14px", color: "#8b949e" }}>
<p>Координаты ТНП: {tnpLat}, {tnpLon}</p>