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 (
Загрузка данных и расчет зон поиска...
{error}
ID: {caseId}
Формула: 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 || 'нет'})
Психотип: {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}
Причина: {caseData.reason || 'не указана'}
Последнее известное место: {caseData.last_known_place || 'не указано'}
{caseData.direction &&Направление движения: {caseData.direction}
} {caseData.circumstances_description && (Описание: {caseData.circumstances_description}
)}Координаты ТНП: {caseData.tnp_lat}, {caseData.tnp_lon}
{caseData.tnp_address &&Адрес: {caseData.tnp_address}
}Диагнозы: {caseData.diagnosis_type?.join(', ') || 'есть'}
)} {caseData.fitness_level && (Физическая подготовка: {caseData.fitness_level}
)} {caseData.cant_swim &&⚠️ Не умеет плавать
} {caseData.has_transport && (Транспорт: {caseData.has_transport}
)}Сезон: {caseData.season || 'не указан'}
{caseData.temperature &&Температура: {caseData.temperature}°C
}Осадки: {caseData.precipitation || 'нет'}
{caseData.visibility &&Видимость: {caseData.visibility}
}