import React, { useState, useEffect } from "react"; import { useParams } from "react-router-dom"; import SearchMap from '../../components/SearchMap'; import "./AnalysisResult.css"; const AnalysisResult = () => { const { caseId } = useParams(); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [caseData, setCaseData] = useState(null); const [analysisData, setAnalysisData] = useState(null); useEffect(() => { const loadCaseAndAnalyze = async () => { try { setLoading(true); setError(null); const caseResponse = await fetch(`/api/v1/cases/${caseId}`); if (!caseResponse.ok) { throw new Error("Не удалось загрузить карточку"); } const caseResult = await caseResponse.json(); setCaseData(caseResult); const analysis = calculateFullDistance(caseResult); setAnalysisData(analysis); } catch (err) { console.error("Error loading case:", err); setError(err.message); } finally { setLoading(false); } }; if (caseId) { loadCaseAndAnalyze(); } }, [caseId]); const getTimeOfDay = (lostTime) => { if (!lostTime) return 'день'; // Значение может прийти как "YYYY-MM-DDTHH:MM" (input type="datetime-local") // или как "HH:MM" — берём часы из обоих форматов. const timePart = lostTime.includes('T') ? lostTime.split('T')[1] : lostTime; const hour = parseInt(timePart.split(':')[0]); if (hour >= 6 && hour < 18) return 'день'; if (hour >= 18 && hour < 22) return 'сумерки'; return 'ночь'; }; const getPsychotypeModifiers = (psychotype) => { const modifiers = { 'dominant': { zone_0_500: 0.7, zone_500_1500: 1.2, zone_1500_2500: 1.4, zone_2500plus: 1.1, description: 'Доминантный: активное движение, большие расстояния' }, 'harmonic': { zone_0_500: 0.8, zone_500_1500: 1.0, zone_1500_2500: 1.1, zone_2500plus: 0.9, description: 'Гармоничный: рациональное движение по ориентирам' }, 'anxious': { zone_0_500: 1.4, zone_500_1500: 1.1, zone_1500_2500: 0.5, zone_2500plus: 0.3, description: 'Тревожный: минимальное движение, остается близко' }, 'introvert_passive': { zone_0_500: 1.3, zone_500_1500: 0.9, zone_1500_2500: 0.6, zone_2500plus: 0.2, description: 'Интроверт пассивный: прячется, минимальное движение' }, 'introvert_active': { zone_0_500: 0.8, zone_500_1500: 1.1, zone_1500_2500: 1.2, zone_2500plus: 0.9, description: 'Интроверт активный: целенаправленное движение' } }; return modifiers[psychotype] || modifiers['harmonic']; }; const calculateFullDistance = (caseData) => { const age = caseData.age_years || 10; const elapsed = caseData.elapsed_hours || 1; const terrain = caseData.terrain?.[0] || 'лес'; const timeOfDay = getTimeOfDay(caseData.lost_time); const weather = caseData.precipitation || 'нет'; // НормС - Скорость смещения потерявшегося ребёнка (км/ч) // Не скорость ходьбы, а реальное смещение: испуг, кружение, остановки // Основа: ПСО ЭКСТРЕМУМ, 400 случаев — 94% найдены в пределах 3 км let baseSpeed = 1.5; if (age <= 2) baseSpeed = 0.3; else if (age <= 5) baseSpeed = 0.7; else if (age <= 8) baseSpeed = 1.2; else if (age <= 12) baseSpeed = 1.5; else if (age <= 15) baseSpeed = 2.0; else if (age <= 17) baseSpeed = 2.5; else if (age <= 64) baseSpeed = 2.5; else baseSpeed = 1.5; // СП - Коэффициент местности let terrainCoef = 0.5; const terrainLower = terrain.toLowerCase(); if (terrainLower.includes('дорог') || terrainLower.includes('троп')) terrainCoef = 0.8; else if (terrainLower.includes('поле') || terrainLower.includes('луг')) terrainCoef = 0.9; else if (terrainLower.includes('болот')) terrainCoef = 0.2; else if (terrainLower.includes('лес')) terrainCoef = 0.5; else if (terrainLower.includes('населённый')) terrainCoef = 1.0; else if (terrainLower.includes('овраг')) terrainCoef = 0.3; // СКД - Коэффициент диагноза (влияние на физическую способность двигаться) let diagnosisCoef = 1.0; const diagTypes = (caseData.diagnosis_type || []) .filter(d => d && d !== 'нет' && d !== 'none') .map(d => d.toLowerCase()); if (caseData.has_diagnosis && diagTypes.length > 0) { if (diagTypes.some(d => d.includes('эпилепси'))) diagnosisCoef = 0.6; else if (diagTypes.some(d => d.includes('зпр'))) diagnosisCoef = 0.8; else if (diagTypes.some(d => d.includes('рас') || d.includes('аутизм'))) diagnosisCoef = 1.0; else if (diagTypes.some(d => d.includes('сдвг'))) diagnosisCoef = 1.1; else diagnosisCoef = 0.9; } // СУ - Коэффициент транспорта // 'none' — это строка, проверяем явно чтобы не получить false positive let urgencyCoef = 1.0; if (caseData.has_transport && caseData.has_transport !== 'none') { if (caseData.has_transport === 'bike') urgencyCoef = 5.0; else if (caseData.has_transport === 'car') urgencyCoef = 8.0; else urgencyCoef = 3.0; } // СУТ - Коэффициент усталости const fatigueCoef = Math.max(0.3, 1.0 - (0.05 * elapsed)); // ВВС - Коэффициент времени суток let timeCoef = 1.0; if (timeOfDay === 'ночь') timeCoef = 0.5; else if (timeOfDay === 'сумерки') timeCoef = 0.7; // ВП - Коэффициент погоды let weatherCoef = 1.0; const weatherLower = weather.toLowerCase(); if (weatherLower.includes('дождь')) weatherCoef = 0.8; else if (weatherLower.includes('снег')) weatherCoef = 0.6; else if (weatherLower.includes('туман')) weatherCoef = 0.7; // Полная формула const distance = elapsed * baseSpeed * terrainCoef * diagnosisCoef * urgencyCoef * fatigueCoef * timeCoef * weatherCoef; // Получаем модификаторы психотипа const psychotypeModifiers = caseData.psychotype ? getPsychotypeModifiers(caseData.psychotype) : null; return { max_distance_km: distance.toFixed(2), coefficients: { base_speed: baseSpeed, terrain: terrainCoef, diagnosis: diagnosisCoef, urgency: urgencyCoef, fatigue: fatigueCoef, time_of_day: timeCoef, weather: weatherCoef }, time_of_day: timeOfDay, psychotype_modifiers: psychotypeModifiers, zones: generateZonesWithPsychotype(distance, caseData, psychotypeModifiers) }; }; const generateZonesWithPsychotype = (maxDist, caseData, psychotypeModifiers) => { const directions = [ { dir: 'N', name: 'Север' }, { dir: 'NE', name: 'Северо-Восток' }, { dir: 'E', name: 'Восток' }, { dir: 'SE', name: 'Юго-Восток' }, { dir: 'S', name: 'Юг' }, { dir: 'SW', name: 'Юго-Запад' }, { dir: 'W', name: 'Запад' }, { dir: 'NW', name: 'Северо-Запад' } ]; const preferredDir = caseData.direction; return directions.map((d, i) => { let score = 100 - (i * 10); // Применяем модификатор направления if (preferredDir && d.dir === preferredDir) { score += 30; } // Применяем модификатор психотипа в зависимости от дистанции if (psychotypeModifiers) { const distKm = maxDist * 0.75; // средняя дистанция сектора let psychoMod = 1.0; if (distKm < 0.5) psychoMod = psychotypeModifiers.zone_0_500; else if (distKm < 1.5) psychoMod = psychotypeModifiers.zone_500_1500; else if (distKm < 2.5) psychoMod = psychotypeModifiers.zone_1500_2500; else psychoMod = psychotypeModifiers.zone_2500plus; score = score * psychoMod; } let reasoning = 'Расчет по алгоритму Экстремум'; if (preferredDir === d.dir) reasoning += ' (указанное направление)'; if (psychotypeModifiers) reasoning += ` (психотип учтен)`; return { priority: i + 1, name: `Сектор ${d.name}`, direction: d.dir, distance: `${(maxDist * 0.5).toFixed(1)}-${maxDist.toFixed(1)} км`, score: Math.min(100, Math.round(score)), reasoning }; }).sort((a, b) => b.score - a.score); }; if (loading) { return (

Загрузка данных и расчет зон поиска...

); } if (error) { return (

Ошибка загрузки

{error}

); } if (!caseData) { return (

Карточка не найдена

ID: {caseId}

); } const urgencyLevel = caseData.elapsed_hours > 6 ? "ВЫСОКИЙ" : caseData.elapsed_hours > 3 ? "СРЕДНИЙ" : "СТАНДАРТНЫЙ"; const urgencyReason = `Прошло ${caseData.elapsed_hours || 1} часов. ${caseData.has_transport ? `Транспорт: ${caseData.has_transport} - расширенный радиус.` : ''} ${caseData.has_diagnosis ? 'Есть диагноз - требуется особое внимание.' : ''}`; const zones = analysisData?.zones || []; const coeffs = analysisData?.coefficients || {}; return (

ВЕКТОР Результаты анализа

{caseData.child_name || 'Ребенок'}, {caseData.age_years} лет ({caseData.gender}) • Время потери: {caseData.lost_time || 'не указано'} • Прошло: {caseData.elapsed_hours || 1} ч
{/* Блок 1: Уровень срочности */}
{urgencyLevel}
{urgencyReason}
{/* Блок 2: Расчет по алгоритму Экстремум */}

📏 Расчет по алгоритму Экстремум

{analysisData?.max_distance_km} км

Формула: Distance = Time × НормС × СП × СКД × СУ × СУТ × ВВС × ВП


Время (Time): {caseData.elapsed_hours || 1} ч

Базовая скорость (НормС): {coeffs.base_speed} км/ч (возраст {caseData.age_years} лет)

Местность (СП): {coeffs.terrain} ({caseData.terrain?.join(', ') || 'не указана'})

Диагноз (СКД): {coeffs.diagnosis} {caseData.has_diagnosis ? `(${caseData.diagnosis_type?.join(', ')})` : '(нет)'}

Транспорт (СУ): {coeffs.urgency} {caseData.has_transport ? `(${caseData.has_transport})` : '(нет)'}

Усталость (СУТ): {coeffs.fatigue?.toFixed(2)} (снижение 5%/час)

Время суток (ВВС): {coeffs.time_of_day} ({analysisData?.time_of_day})

Погода (ВП): {coeffs.weather} ({caseData.precipitation || 'нет'})

{/* Блок 3: Психотип */} {caseData.psychotype && analysisData?.psychotype_modifiers && (

🧠 Психотип и модель поведения

Психотип: {caseData.psychotype}

Модель движения: {analysisData.psychotype_modifiers.description}

Модификаторы зон:

• 0-500м: ×{analysisData.psychotype_modifiers.zone_0_500}
• 500-1500м: ×{analysisData.psychotype_modifiers.zone_500_1500}
• 1500-2500м: ×{analysisData.psychotype_modifiers.zone_1500_2500}
• 2500м+: ×{analysisData.psychotype_modifiers.zone_2500plus}

)} {/* Блок 4: Обстоятельства */}

📋 Обстоятельства потери

Причина: {caseData.reason || 'не указана'}

Последнее известное место: {caseData.last_known_place || 'не указано'}

{caseData.direction &&

Направление движения: {caseData.direction}

} {caseData.circumstances_description && (

Описание: {caseData.circumstances_description}

)}
{/* Блок 5: Зоны поиска */}

Приоритетные зоны поиска

{zones.slice(0, 8).map((zone, idx) => (
#{idx + 1} {zone.name}
{zone.score}%
Направление: {zone.direction} • Дистанция: {zone.distance}
{zone.reasoning}
))}
{/* Блок 6: Карта */} {caseData.tnp_lat && caseData.tnp_lon && (

Карта зон поиска

Координаты ТНП: {caseData.tnp_lat}, {caseData.tnp_lon}

{caseData.tnp_address &&

Адрес: {caseData.tnp_address}

}
)} {/* Блок 7: Здоровье и особенности */} {(caseData.has_diagnosis || caseData.cant_swim || caseData.fitness_level) && (

🏥 Здоровье и особенности

{caseData.has_diagnosis && (

Диагнозы: {caseData.diagnosis_type?.join(', ') || 'есть'}

)} {caseData.fitness_level && (

Физическая подготовка: {caseData.fitness_level}

)} {caseData.cant_swim &&

⚠️ Не умеет плавать

} {caseData.has_transport && (

Транспорт: {caseData.has_transport}

)}
)} {/* Блок 8: Условия среды */}

🌤 Условия среды

Сезон: {caseData.season || 'не указан'}

{caseData.temperature &&

Температура: {caseData.temperature}°C

}

Осадки: {caseData.precipitation || 'нет'}

{caseData.visibility &&

Видимость: {caseData.visibility}

}
); }; export default AnalysisResult;