Web Update
This commit is contained in:
@@ -4,7 +4,8 @@ import React, { useEffect, useRef, useState } from "react";
|
|||||||
import mapboxgl from "mapbox-gl";
|
import mapboxgl from "mapbox-gl";
|
||||||
import GeocodeInput from './GeocodeInput';
|
import GeocodeInput from './GeocodeInput';
|
||||||
import { useCrashData } from '../hooks/useCrashData';
|
import { useCrashData } from '../hooks/useCrashData';
|
||||||
import { calculateRouteCrashDensity, createRouteGradientStops } from '../lib/mapUtils';
|
import { calculateRouteCrashDensity, createRouteGradientStops } from '../../lib/mapUtils';
|
||||||
|
import { fetchSafeRoute, type SafeRouteData } from '../../lib/flaskApi';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
mapRef: React.MutableRefObject<mapboxgl.Map | null>;
|
mapRef: React.MutableRefObject<mapboxgl.Map | null>;
|
||||||
@@ -16,6 +17,8 @@ interface Props {
|
|||||||
|
|
||||||
// Routing now uses geocoder-only selection inside the sidebar (no manual coordinate parsing)
|
// Routing now uses geocoder-only selection inside the sidebar (no manual coordinate parsing)
|
||||||
|
|
||||||
|
// Routing now uses geocoder-only selection inside the sidebar (no manual coordinate parsing)
|
||||||
|
|
||||||
export default function DirectionsSidebar({ mapRef, profile = "mapbox/driving", onMapPickingModeChange, gradientRoutes = true, mapStyleChoice = 'dark' }: Props) {
|
export default function DirectionsSidebar({ mapRef, profile = "mapbox/driving", onMapPickingModeChange, gradientRoutes = true, mapStyleChoice = 'dark' }: Props) {
|
||||||
// Sidebar supports collapse via a hamburger button in the header
|
// Sidebar supports collapse via a hamburger button in the header
|
||||||
const [collapsed, setCollapsed] = useState(false);
|
const [collapsed, setCollapsed] = useState(false);
|
||||||
@@ -32,6 +35,10 @@ export default function DirectionsSidebar({ mapRef, profile = "mapbox/driving",
|
|||||||
const [isDestMapPicking, setIsDestMapPicking] = useState(false);
|
const [isDestMapPicking, setIsDestMapPicking] = useState(false);
|
||||||
const [routes, setRoutes] = useState<any[]>([]);
|
const [routes, setRoutes] = useState<any[]>([]);
|
||||||
const [selectedRouteIndex, setSelectedRouteIndex] = useState(0);
|
const [selectedRouteIndex, setSelectedRouteIndex] = useState(0);
|
||||||
|
// Safe route functionality
|
||||||
|
const [safeRouteData, setSafeRouteData] = useState<SafeRouteData | null>(null);
|
||||||
|
const [safeRouteLoading, setSafeRouteLoading] = useState(false);
|
||||||
|
const [showSafeRoute, setShowSafeRoute] = useState(false);
|
||||||
// custom geocoder inputs + suggestions (we implement our own UI instead of the library)
|
// custom geocoder inputs + suggestions (we implement our own UI instead of the library)
|
||||||
const originQueryRef = useRef<string>("");
|
const originQueryRef = useRef<string>("");
|
||||||
const destQueryRef = useRef<string>("");
|
const destQueryRef = useRef<string>("");
|
||||||
@@ -45,6 +52,26 @@ export default function DirectionsSidebar({ mapRef, profile = "mapbox/driving",
|
|||||||
const destInputRef = useRef<HTMLDivElement | null>(null);
|
const destInputRef = useRef<HTMLDivElement | null>(null);
|
||||||
const mountedRef = useRef(true);
|
const mountedRef = useRef(true);
|
||||||
|
|
||||||
|
// Helper function to normalize safety score to 0-10 scale
|
||||||
|
const normalizeSafetyScore = (rawScore: number): number => {
|
||||||
|
console.log('Raw safety score:', rawScore); // Debug log
|
||||||
|
|
||||||
|
// Based on the safety score calculation:
|
||||||
|
// - 0 = no crashes (perfectly safe)
|
||||||
|
// - 1-5 = low danger
|
||||||
|
// - 5-20 = moderate danger
|
||||||
|
// - 20+ = high danger
|
||||||
|
|
||||||
|
if (rawScore === 0) return 10; // Perfect safety
|
||||||
|
|
||||||
|
// Use logarithmic scale to handle wide range of scores
|
||||||
|
// Map common ranges: 0.1->9.5, 1->8, 5->6, 20->3, 50->1, 100+->0
|
||||||
|
const safetyScore = Math.max(0, 10 - Math.log10(rawScore + 1) * 2.5);
|
||||||
|
|
||||||
|
console.log('Normalized safety score:', safetyScore); // Debug log
|
||||||
|
return Math.max(0, Math.min(10, safetyScore));
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return () => { mountedRef.current = false; };
|
return () => { mountedRef.current = false; };
|
||||||
}, []);
|
}, []);
|
||||||
@@ -495,6 +522,75 @@ export default function DirectionsSidebar({ mapRef, profile = "mapbox/driving",
|
|||||||
// Clear multiple routes state
|
// Clear multiple routes state
|
||||||
setRoutes([]);
|
setRoutes([]);
|
||||||
setSelectedRouteIndex(0);
|
setSelectedRouteIndex(0);
|
||||||
|
// Clear safe route state
|
||||||
|
setSafeRouteData(null);
|
||||||
|
setShowSafeRoute(false);
|
||||||
|
// Remove safe route from map if it exists
|
||||||
|
if (map) {
|
||||||
|
try {
|
||||||
|
if (map.getLayer("safe-route-line")) map.removeLayer("safe-route-line");
|
||||||
|
if (map.getSource("safe-route")) map.removeSource("safe-route");
|
||||||
|
} catch (e) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Safe route functionality
|
||||||
|
async function handleGetSafeRoute() {
|
||||||
|
const map = mapRef.current;
|
||||||
|
if (!map) return;
|
||||||
|
const o = originCoord;
|
||||||
|
const d = destCoord;
|
||||||
|
if (!o || !d) {
|
||||||
|
alert('Please select both origin and destination using the location search boxes.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setSafeRouteLoading(true);
|
||||||
|
try {
|
||||||
|
const safeRouteResult = await fetchSafeRoute(o[1], o[0], d[1], d[0]); // API expects lat, lon
|
||||||
|
console.log('Safe route result:', safeRouteResult);
|
||||||
|
setSafeRouteData(safeRouteResult);
|
||||||
|
setShowSafeRoute(true);
|
||||||
|
|
||||||
|
// If we have a recommended route, display it
|
||||||
|
if (safeRouteResult.recommended_route?.geometry?.coordinates) {
|
||||||
|
const safeRouteGeo: GeoJSON.Feature<GeoJSON.Geometry> = {
|
||||||
|
type: "Feature",
|
||||||
|
properties: { type: "safe-route" },
|
||||||
|
geometry: {
|
||||||
|
type: "LineString",
|
||||||
|
coordinates: safeRouteResult.recommended_route.geometry.coordinates
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add safe route to map
|
||||||
|
if (!map.getSource("safe-route")) {
|
||||||
|
map.addSource("safe-route", { type: "geojson", data: safeRouteGeo });
|
||||||
|
} else {
|
||||||
|
(map.getSource("safe-route") as mapboxgl.GeoJSONSource).setData(safeRouteGeo);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!map.getLayer("safe-route-line")) {
|
||||||
|
map.addLayer({
|
||||||
|
id: "safe-route-line",
|
||||||
|
type: "line",
|
||||||
|
source: "safe-route",
|
||||||
|
layout: { "line-join": "round", "line-cap": "round" },
|
||||||
|
paint: {
|
||||||
|
"line-color": "#10b981", // green color for safe route
|
||||||
|
"line-width": 6,
|
||||||
|
"line-opacity": 0.8,
|
||||||
|
"line-dasharray": [1, 1] // subtle dashed line to differentiate
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching safe route:', error);
|
||||||
|
alert('Failed to fetch safe route. Please try again.');
|
||||||
|
} finally {
|
||||||
|
setSafeRouteLoading(false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// re-add layers after style change
|
// re-add layers after style change
|
||||||
@@ -683,8 +779,9 @@ export default function DirectionsSidebar({ mapRef, profile = "mapbox/driving",
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex gap-2 mt-2">
|
<div className="flex gap-2 mt-2">
|
||||||
<button onClick={handleGetRoute} disabled={loading} className="flex-1 px-4 py-2 rounded-lg text-white shadow-md disabled:opacity-60 transition-colors" style={{ backgroundColor: 'var(--panel-dark)' }}>{loading ? 'Routing…' : 'Get Route'}</button>
|
<button onClick={handleGetRoute} disabled={loading} className="flex-1 px-3 py-2 rounded-lg text-white shadow-md disabled:opacity-60 transition-colors text-sm" style={{ backgroundColor: 'var(--panel-dark)' }}>{loading ? 'Routing…' : 'Get Route'}</button>
|
||||||
<button onClick={handleClear} className="px-4 py-2 rounded-lg border text-sm text-[#d1d5db] transition-colors" style={{ backgroundColor: 'var(--panel-medium)', borderColor: 'var(--panel-light)' }} onMouseEnter={(e) => (e.target as HTMLButtonElement).style.backgroundColor = 'var(--panel-light)'} onMouseLeave={(e) => (e.target as HTMLButtonElement).style.backgroundColor = 'var(--panel-medium)'}>Clear</button>
|
<button onClick={handleGetSafeRoute} disabled={safeRouteLoading} className="flex-1 px-3 py-2 rounded-lg text-white shadow-md disabled:opacity-60 transition-colors text-sm" style={{ backgroundColor: '#10b981' }}>{safeRouteLoading ? 'AI Routing…' : 'Safe Route'}</button>
|
||||||
|
<button onClick={handleClear} className="px-3 py-2 rounded-lg border text-sm text-[#d1d5db] transition-colors" style={{ backgroundColor: 'var(--panel-medium)', borderColor: 'var(--panel-light)' }} onMouseEnter={(e) => (e.target as HTMLButtonElement).style.backgroundColor = 'var(--panel-light)'} onMouseLeave={(e) => (e.target as HTMLButtonElement).style.backgroundColor = 'var(--panel-medium)'}>Clear</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Route Options */}
|
{/* Route Options */}
|
||||||
@@ -787,6 +884,66 @@ export default function DirectionsSidebar({ mapRef, profile = "mapbox/driving",
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Safe Route Analysis */}
|
||||||
|
{showSafeRoute && safeRouteData && (
|
||||||
|
<div className="mt-4 p-3 rounded-lg border border-[#10b981]" style={{ backgroundColor: 'var(--panel-dark)' }}>
|
||||||
|
<div className="flex items-center gap-2 mb-2">
|
||||||
|
<svg className="w-4 h-4 text-[#10b981]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" />
|
||||||
|
</svg>
|
||||||
|
<span className="text-sm font-medium text-[#ecfdf5]">AI Safe Route Analysis</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="text-xs text-[#a7f3d0]">
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span>Safety Score:</span>
|
||||||
|
<span className={`font-medium ${
|
||||||
|
normalizeSafetyScore(safeRouteData.recommended_route.safety_score) >= 7
|
||||||
|
? 'text-green-400'
|
||||||
|
: normalizeSafetyScore(safeRouteData.recommended_route.safety_score) >= 4
|
||||||
|
? 'text-yellow-400'
|
||||||
|
: 'text-red-400'
|
||||||
|
}`}>
|
||||||
|
{normalizeSafetyScore(safeRouteData.recommended_route.safety_score).toFixed(1)}/10
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span>Distance:</span>
|
||||||
|
<span className="font-medium">{safeRouteData.recommended_route.distance_km.toFixed(1)} km</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span>Duration:</span>
|
||||||
|
<span className="font-medium">{Math.round(safeRouteData.recommended_route.duration_min)} min</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span>Crashes Nearby:</span>
|
||||||
|
<span className="font-medium">{safeRouteData.recommended_route.crashes_nearby}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{safeRouteData.safety_analysis && (
|
||||||
|
<div className="text-xs text-[#a7f3d0] border-t border-[#065f46] pt-2">
|
||||||
|
<div className="font-medium mb-1">Safety Analysis:</div>
|
||||||
|
<div className="text-[#6ee7b7]">{safeRouteData.safety_analysis}</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{safeRouteData.weather_summary && (
|
||||||
|
<div className="text-xs text-[#a7f3d0] border-t border-[#065f46] pt-2">
|
||||||
|
<div className="font-medium mb-1">Weather Conditions:</div>
|
||||||
|
<div className="text-[#6ee7b7]">{safeRouteData.weather_summary}</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="mt-2 text-xs text-[#a7f3d0] flex items-center">
|
||||||
|
<span className="inline-block w-3 h-0.5 bg-[#10b981] mr-2"></span>
|
||||||
|
Green line shows AI-recommended safe route
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Route Safety Legend */}
|
{/* Route Safety Legend */}
|
||||||
{(originCoord && destCoord) && (
|
{(originCoord && destCoord) && (
|
||||||
<div className="mt-4 p-3 rounded-lg border" style={{ backgroundColor: 'var(--panel-medium)', borderColor: 'var(--panel-light)' }}>
|
<div className="mt-4 p-3 rounded-lg border" style={{ backgroundColor: 'var(--panel-medium)', borderColor: 'var(--panel-light)' }}>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import mapboxgl from 'mapbox-gl';
|
|||||||
import 'mapbox-gl/dist/mapbox-gl.css';
|
import 'mapbox-gl/dist/mapbox-gl.css';
|
||||||
import MapboxGeocoder from '@mapbox/mapbox-gl-geocoder';
|
import MapboxGeocoder from '@mapbox/mapbox-gl-geocoder';
|
||||||
import '@mapbox/mapbox-gl-geocoder/dist/mapbox-gl-geocoder.css';
|
import '@mapbox/mapbox-gl-geocoder/dist/mapbox-gl-geocoder.css';
|
||||||
import { generateDCPoints, generateDCPointsWithAI, haversine, PointFeature, convertCrashDataToGeoJSON, convertCrashDataToGeoJSONWithAI } from '../lib/mapUtils';
|
import { convertSyntheticDataToGeoJSON, haversine, PointFeature, convertCrashDataToGeoJSON } from '../../lib/mapUtils';
|
||||||
import { useCrashData, UseCrashDataResult } from '../hooks/useCrashData';
|
import { useCrashData, UseCrashDataResult } from '../hooks/useCrashData';
|
||||||
import { CrashData } from '../api/crashes/route';
|
import { CrashData } from '../api/crashes/route';
|
||||||
import { WeatherData, CrashAnalysisData } from '../../lib/flaskApi';
|
import { WeatherData, CrashAnalysisData } from '../../lib/flaskApi';
|
||||||
@@ -49,7 +49,6 @@ interface MapViewProps {
|
|||||||
crashData?: CrashData[]; // external crash data to use
|
crashData?: CrashData[]; // external crash data to use
|
||||||
crashDataHook?: UseCrashDataResult; // the crash data hook from main page
|
crashDataHook?: UseCrashDataResult; // the crash data hook from main page
|
||||||
isMapPickingMode?: boolean; // whether map is in picking mode (prevents popups)
|
isMapPickingMode?: boolean; // whether map is in picking mode (prevents popups)
|
||||||
useAIMagnitudes?: boolean; // whether to use AI-predicted crash magnitudes
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function MapView({
|
export default function MapView({
|
||||||
@@ -64,8 +63,7 @@ export default function MapView({
|
|||||||
useRealCrashData = true,
|
useRealCrashData = true,
|
||||||
crashData = [],
|
crashData = [],
|
||||||
crashDataHook,
|
crashDataHook,
|
||||||
isMapPickingMode = false,
|
isMapPickingMode = false
|
||||||
useAIMagnitudes = true // Default to true to use AI predictions
|
|
||||||
}: MapViewProps) {
|
}: MapViewProps) {
|
||||||
const containerRef = useRef<HTMLDivElement | null>(null);
|
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||||
const mapContainerRef = useRef<HTMLDivElement | null>(null);
|
const mapContainerRef = useRef<HTMLDivElement | null>(null);
|
||||||
@@ -92,16 +90,11 @@ export default function MapView({
|
|||||||
console.log('Converting crash data to GeoJSON...');
|
console.log('Converting crash data to GeoJSON...');
|
||||||
|
|
||||||
const processData = async () => {
|
const processData = async () => {
|
||||||
setIsLoadingAIPredictions(useAIMagnitudes);
|
setIsLoadingAIPredictions(false); // No AI predictions anymore
|
||||||
|
|
||||||
let geoJSONData: GeoJSON.FeatureCollection;
|
let geoJSONData: GeoJSON.FeatureCollection;
|
||||||
if (useAIMagnitudes) {
|
console.log(`🗺️ Processing crash data: traditional mode`);
|
||||||
console.log('🤖 Using AI-enhanced crash data conversion...');
|
|
||||||
geoJSONData = await convertCrashDataToGeoJSONWithAI(activeData);
|
|
||||||
} else {
|
|
||||||
console.log('📊 Using standard crash data conversion...');
|
|
||||||
geoJSONData = convertCrashDataToGeoJSON(activeData);
|
geoJSONData = convertCrashDataToGeoJSON(activeData);
|
||||||
}
|
|
||||||
|
|
||||||
dcDataRef.current = geoJSONData;
|
dcDataRef.current = geoJSONData;
|
||||||
setIsLoadingAIPredictions(false);
|
setIsLoadingAIPredictions(false);
|
||||||
@@ -120,7 +113,7 @@ export default function MapView({
|
|||||||
|
|
||||||
processData().catch(console.error);
|
processData().catch(console.error);
|
||||||
}
|
}
|
||||||
}, [crashData, crashDataHook, useRealCrashData, useAIMagnitudes]);
|
}, [crashData, crashDataHook, useRealCrashData]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const el = containerRef.current;
|
const el = containerRef.current;
|
||||||
@@ -203,24 +196,13 @@ export default function MapView({
|
|||||||
const initializeData = async () => {
|
const initializeData = async () => {
|
||||||
if (useRealCrashData && activeData.length > 0) {
|
if (useRealCrashData && activeData.length > 0) {
|
||||||
console.log('Using real crash data');
|
console.log('Using real crash data');
|
||||||
if (useAIMagnitudes) {
|
|
||||||
setIsLoadingAIPredictions(true);
|
|
||||||
console.log('🤖 Using AI-enhanced real crash data...');
|
|
||||||
dcDataRef.current = await convertCrashDataToGeoJSONWithAI(activeData);
|
|
||||||
setIsLoadingAIPredictions(false);
|
setIsLoadingAIPredictions(false);
|
||||||
} else {
|
console.log(`🗺️ Processing real crash data: traditional mode`);
|
||||||
dcDataRef.current = convertCrashDataToGeoJSON(activeData);
|
dcDataRef.current = convertCrashDataToGeoJSON(activeData);
|
||||||
}
|
setIsLoadingAIPredictions(false);
|
||||||
} else if (!useRealCrashData) {
|
} else if (!useRealCrashData) {
|
||||||
console.log('Using synthetic data');
|
console.log('Using synthetic data');
|
||||||
if (useAIMagnitudes) {
|
dcDataRef.current = convertSyntheticDataToGeoJSON(38.9072, -77.0369, 900);
|
||||||
setIsLoadingAIPredictions(true);
|
|
||||||
console.log('🤖 Using AI-enhanced synthetic data...');
|
|
||||||
dcDataRef.current = await generateDCPointsWithAI(900);
|
|
||||||
setIsLoadingAIPredictions(false);
|
|
||||||
} else {
|
|
||||||
dcDataRef.current = generateDCPoints(900);
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
console.log('No data available yet, using empty data');
|
console.log('No data available yet, using empty data');
|
||||||
dcDataRef.current = { type: 'FeatureCollection' as const, features: [] };
|
dcDataRef.current = { type: 'FeatureCollection' as const, features: [] };
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect, useRef } from 'react';
|
||||||
|
import CrashDataControls from './CrashDataControls';
|
||||||
import { UseCrashDataResult } from '../hooks/useCrashData';
|
import { UseCrashDataResult } from '../hooks/useCrashData';
|
||||||
import { getCircuitBreakerStatus } from '../../lib/crashMagnitudeApi';
|
|
||||||
|
|
||||||
interface UnifiedControlPanelProps {
|
interface UnifiedControlPanelProps {
|
||||||
// Map controls props
|
// Map controls props
|
||||||
@@ -18,8 +18,6 @@ interface UnifiedControlPanelProps {
|
|||||||
onChangeIntensity: (v: number) => void;
|
onChangeIntensity: (v: number) => void;
|
||||||
gradientRoutes: boolean;
|
gradientRoutes: boolean;
|
||||||
onToggleGradientRoutes: (v: boolean) => void;
|
onToggleGradientRoutes: (v: boolean) => void;
|
||||||
useAIMagnitudes: boolean;
|
|
||||||
onToggleAIMagnitudes: (v: boolean) => void;
|
|
||||||
|
|
||||||
// Crash data controls props
|
// Crash data controls props
|
||||||
crashDataHook: UseCrashDataResult;
|
crashDataHook: UseCrashDataResult;
|
||||||
@@ -39,8 +37,6 @@ export default function UnifiedControlPanel({
|
|||||||
onChangeIntensity,
|
onChangeIntensity,
|
||||||
gradientRoutes,
|
gradientRoutes,
|
||||||
onToggleGradientRoutes,
|
onToggleGradientRoutes,
|
||||||
useAIMagnitudes,
|
|
||||||
onToggleAIMagnitudes,
|
|
||||||
crashDataHook,
|
crashDataHook,
|
||||||
onDataLoaded
|
onDataLoaded
|
||||||
}: UnifiedControlPanelProps) {
|
}: UnifiedControlPanelProps) {
|
||||||
@@ -64,7 +60,6 @@ export default function UnifiedControlPanel({
|
|||||||
const [isMapControlsSectionOpen, setIsMapControlsSectionOpen] = useState(getInitialMapControlsState);
|
const [isMapControlsSectionOpen, setIsMapControlsSectionOpen] = useState(getInitialMapControlsState);
|
||||||
const [isCrashDataSectionOpen, setIsCrashDataSectionOpen] = useState(getInitialCrashDataState);
|
const [isCrashDataSectionOpen, setIsCrashDataSectionOpen] = useState(getInitialCrashDataState);
|
||||||
const [isHydrated, setIsHydrated] = useState(false);
|
const [isHydrated, setIsHydrated] = useState(false);
|
||||||
const [aiApiStatus, setAiApiStatus] = useState<{ isOpen: boolean; failures: number }>({ isOpen: false, failures: 0 });
|
|
||||||
|
|
||||||
// Load localStorage values after hydration
|
// Load localStorage values after hydration
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -85,28 +80,6 @@ export default function UnifiedControlPanel({
|
|||||||
setIsHydrated(true);
|
setIsHydrated(true);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Check AI API status when AI magnitudes are enabled
|
|
||||||
useEffect(() => {
|
|
||||||
if (useAIMagnitudes) {
|
|
||||||
const checkApiStatus = async () => {
|
|
||||||
try {
|
|
||||||
const status = await getCircuitBreakerStatus();
|
|
||||||
setAiApiStatus(status);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error checking API status:', error);
|
|
||||||
setAiApiStatus({ isOpen: true, failures: 1 });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Check immediately
|
|
||||||
checkApiStatus();
|
|
||||||
|
|
||||||
// Check every 30 seconds
|
|
||||||
const interval = setInterval(checkApiStatus, 30000);
|
|
||||||
return () => clearInterval(interval);
|
|
||||||
}
|
|
||||||
}, [useAIMagnitudes]);
|
|
||||||
|
|
||||||
// Crash data state
|
// Crash data state
|
||||||
const { data, loading, error, pagination, loadMore, refresh, yearFilter, setYearFilter } = crashDataHook;
|
const { data, loading, error, pagination, loadMore, refresh, yearFilter, setYearFilter } = crashDataHook;
|
||||||
const [currentYear, setCurrentYear] = useState('2024'); // Default to prevent hydration mismatch
|
const [currentYear, setCurrentYear] = useState('2024'); // Default to prevent hydration mismatch
|
||||||
@@ -266,29 +239,6 @@ export default function UnifiedControlPanel({
|
|||||||
<input type="checkbox" checked={gradientRoutes} onChange={(e) => onToggleGradientRoutes(e.target.checked)} />
|
<input type="checkbox" checked={gradientRoutes} onChange={(e) => onToggleGradientRoutes(e.target.checked)} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mc-row">
|
|
||||||
<label className="mc-label">
|
|
||||||
AI Magnitudes 🤖
|
|
||||||
<span style={{
|
|
||||||
fontSize: 8,
|
|
||||||
padding: '2px 6px',
|
|
||||||
borderRadius: 4,
|
|
||||||
marginLeft: 8,
|
|
||||||
backgroundColor: aiApiStatus.isOpen ? '#d4edda' : '#f8d7da',
|
|
||||||
color: aiApiStatus.isOpen ? '#155724' : '#721c24'
|
|
||||||
}}>
|
|
||||||
{aiApiStatus.isOpen ? 'Available' : `Unavailable (${aiApiStatus.failures} failures)`}
|
|
||||||
</span>
|
|
||||||
</label>
|
|
||||||
<input type="checkbox" checked={useAIMagnitudes} onChange={(e) => onToggleAIMagnitudes(e.target.checked)} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{useAIMagnitudes && (
|
|
||||||
<div style={{ fontSize: 10, color: 'var(--text-secondary)', marginTop: -4, marginBottom: 8, lineHeight: 1.3 }}>
|
|
||||||
Uses AI to predict crash severity. Falls back to traditional calculation if API unavailable.
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div style={{ marginBottom: 6 }}>
|
<div style={{ marginBottom: 6 }}>
|
||||||
<label style={{ display: 'block', fontSize: 12 }}>Radius: {heatRadius}</label>
|
<label style={{ display: 'block', fontSize: 12 }}>Radius: {heatRadius}</label>
|
||||||
<input className="mc-range" type="range" min={5} max={100} value={heatRadius} onChange={(e) => onChangeRadius(Number(e.target.value))} style={{ width: '100%' }} />
|
<input className="mc-range" type="range" min={5} max={100} value={heatRadius} onChange={(e) => onChangeRadius(Number(e.target.value))} style={{ width: '100%' }} />
|
||||||
|
|||||||
@@ -1,42 +0,0 @@
|
|||||||
export type PointFeature = GeoJSON.Feature<GeoJSON.Point, { mag: number }>;
|
|
||||||
|
|
||||||
export const haversine = (a: [number, number], b: [number, number]) => {
|
|
||||||
const toRad = (v: number) => v * Math.PI / 180;
|
|
||||||
const R = 6371000; // meters
|
|
||||||
const dLat = toRad(b[1] - a[1]);
|
|
||||||
const dLon = toRad(b[0] - a[0]);
|
|
||||||
const lat1 = toRad(a[1]);
|
|
||||||
const lat2 = toRad(b[1]);
|
|
||||||
const sinDLat = Math.sin(dLat/2);
|
|
||||||
const sinDLon = Math.sin(dLon/2);
|
|
||||||
const aH = sinDLat*sinDLat + sinDLon*sinDLon * Math.cos(lat1)*Math.cos(lat2);
|
|
||||||
const c = 2 * Math.atan2(Math.sqrt(aH), Math.sqrt(1-aH));
|
|
||||||
return R * c;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const generateDCPoints = (count = 500) => {
|
|
||||||
const center = { lon: -77.0369, lat: 38.9072 };
|
|
||||||
const features: PointFeature[] = [];
|
|
||||||
|
|
||||||
const randNormal = () => {
|
|
||||||
let u = 0, v = 0;
|
|
||||||
while (u === 0) u = Math.random();
|
|
||||||
while (v === 0) v = Math.random();
|
|
||||||
return Math.sqrt(-2.0 * Math.log(u)) * Math.cos(2.0 * Math.PI * v);
|
|
||||||
};
|
|
||||||
|
|
||||||
for (let i = 0; i < count; i++) {
|
|
||||||
const radius = Math.abs(randNormal()) * 0.02;
|
|
||||||
const angle = Math.random() * Math.PI * 2;
|
|
||||||
const lon = center.lon + Math.cos(angle) * radius;
|
|
||||||
const lat = center.lat + Math.sin(angle) * radius;
|
|
||||||
const mag = Math.round(Math.max(1, Math.abs(randNormal()) * 6));
|
|
||||||
features.push({
|
|
||||||
type: 'Feature',
|
|
||||||
geometry: { type: 'Point', coordinates: [lon, lat] },
|
|
||||||
properties: { mag }
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return { type: 'FeatureCollection', features } as GeoJSON.FeatureCollection<GeoJSON.Geometry>;
|
|
||||||
};
|
|
||||||
@@ -1,367 +0,0 @@
|
|||||||
import { CrashData } from '../api/crashes/route';
|
|
||||||
import { getCachedCrashMagnitude, CrashMagnitudePrediction } from '../../lib/crashMagnitudeApi';
|
|
||||||
|
|
||||||
export type PointFeature = GeoJSON.Feature<GeoJSON.Point, { mag: number; crashData: CrashData; aiPredicted?: boolean }>;
|
|
||||||
|
|
||||||
export const haversine = (a: [number, number], b: [number, number]) => {
|
|
||||||
const toRad = (v: number) => v * Math.PI / 180;
|
|
||||||
const R = 6371000; // meters
|
|
||||||
const dLat = toRad(b[1] - a[1]);
|
|
||||||
const dLon = toRad(b[0] - a[0]);
|
|
||||||
const lat1 = toRad(a[1]);
|
|
||||||
const lat2 = toRad(b[1]);
|
|
||||||
const sinDLat = Math.sin(dLat/2);
|
|
||||||
const sinDLon = Math.sin(dLon/2);
|
|
||||||
const aH = sinDLat*sinDLat + sinDLon*sinDLon * Math.cos(lat1)*Math.cos(lat2);
|
|
||||||
const c = 2 * Math.atan2(Math.sqrt(aH), Math.sqrt(1-aH));
|
|
||||||
return R * c;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const convertCrashDataToGeoJSON = (crashes: CrashData[]): GeoJSON.FeatureCollection => {
|
|
||||||
console.log('Converting crash data to GeoJSON:', crashes.length, 'crashes');
|
|
||||||
console.log('Sample crash data:', crashes[0]);
|
|
||||||
|
|
||||||
const features: PointFeature[] = crashes.map((crash) => {
|
|
||||||
// Calculate fallback severity score based on fatalities and major injuries
|
|
||||||
const fallbackSeverityScore = Math.max(1,
|
|
||||||
(crash.fatalDriver + crash.fatalPedestrian + crash.fatalBicyclist) * 3 +
|
|
||||||
(crash.majorInjuriesDriver + crash.majorInjuriesPedestrian + crash.majorInjuriesBicyclist) * 2 +
|
|
||||||
(crash.totalVehicles + crash.totalPedestrians + crash.totalBicycles)
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
|
||||||
type: 'Feature',
|
|
||||||
geometry: {
|
|
||||||
type: 'Point',
|
|
||||||
coordinates: [crash.longitude, crash.latitude]
|
|
||||||
},
|
|
||||||
properties: {
|
|
||||||
mag: Math.min(6, fallbackSeverityScore), // Cap at 6 for consistent visualization
|
|
||||||
crashData: crash,
|
|
||||||
aiPredicted: false // Will be updated when AI prediction is available
|
|
||||||
}
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
const geoJSON = {
|
|
||||||
type: 'FeatureCollection' as const,
|
|
||||||
features
|
|
||||||
};
|
|
||||||
|
|
||||||
console.log('Generated GeoJSON with', features.length, 'features');
|
|
||||||
console.log('Sample feature:', features[0]);
|
|
||||||
|
|
||||||
return geoJSON;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Enhanced version that fetches AI predictions for crash magnitudes
|
|
||||||
*/
|
|
||||||
export const convertCrashDataToGeoJSONWithAI = async (crashes: CrashData[]): Promise<GeoJSON.FeatureCollection> => {
|
|
||||||
console.log('🤖 Converting crash data to GeoJSON with AI predictions:', crashes.length, 'crashes');
|
|
||||||
|
|
||||||
// Start with the basic conversion
|
|
||||||
const baseGeoJSON = convertCrashDataToGeoJSON(crashes);
|
|
||||||
|
|
||||||
// Limit concurrent API calls to avoid overwhelming the API
|
|
||||||
const BATCH_SIZE = 10;
|
|
||||||
const enhancedFeatures = [...baseGeoJSON.features];
|
|
||||||
let successfulPredictions = 0;
|
|
||||||
|
|
||||||
for (let i = 0; i < crashes.length; i += BATCH_SIZE) {
|
|
||||||
const batch = crashes.slice(i, i + BATCH_SIZE);
|
|
||||||
const batchPromises = batch.map(async (crash, batchIndex) => {
|
|
||||||
const featureIndex = i + batchIndex;
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Get AI prediction for this crash location
|
|
||||||
const prediction = await getCachedCrashMagnitude(crash.latitude, crash.longitude);
|
|
||||||
|
|
||||||
if (prediction && typeof prediction.prediction === 'number') {
|
|
||||||
// Scale the roadcast API prediction to a reasonable range for visualization
|
|
||||||
// Roadcast returns values like 47, so we'll scale them to 1-10 range
|
|
||||||
let scaledMagnitude = prediction.prediction;
|
|
||||||
|
|
||||||
// If the value seems to be in the roadcast range (typically 0-100), scale it down
|
|
||||||
if (prediction.prediction > 20) {
|
|
||||||
scaledMagnitude = Math.max(1, Math.min(10, Math.round(prediction.prediction / 10)));
|
|
||||||
} else {
|
|
||||||
scaledMagnitude = Math.max(1, Math.min(10, Math.round(prediction.prediction)));
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`🎯 Scaled magnitude from ${prediction.prediction} to ${scaledMagnitude}`);
|
|
||||||
|
|
||||||
enhancedFeatures[featureIndex] = {
|
|
||||||
...enhancedFeatures[featureIndex],
|
|
||||||
properties: {
|
|
||||||
...enhancedFeatures[featureIndex].properties,
|
|
||||||
mag: scaledMagnitude,
|
|
||||||
aiPredicted: true
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return true; // Success
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.warn(`⚠️ Failed to get AI prediction for crash ${featureIndex}:`, error);
|
|
||||||
}
|
|
||||||
|
|
||||||
return false; // Failed
|
|
||||||
});
|
|
||||||
|
|
||||||
const results = await Promise.allSettled(batchPromises);
|
|
||||||
successfulPredictions += results.filter(r => r.status === 'fulfilled' && r.value === true).length;
|
|
||||||
|
|
||||||
// Small delay between batches to be nice to the API
|
|
||||||
if (i + BATCH_SIZE < crashes.length) {
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 100));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const enhancedGeoJSON = {
|
|
||||||
type: 'FeatureCollection' as const,
|
|
||||||
features: enhancedFeatures as PointFeature[]
|
|
||||||
};
|
|
||||||
|
|
||||||
console.log(`✅ Enhanced GeoJSON with ${successfulPredictions}/${crashes.length} AI predictions`);
|
|
||||||
|
|
||||||
return enhancedGeoJSON;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const generateDCPoints = (count = 500) => {
|
|
||||||
const center = { lon: -77.0369, lat: 38.9072 };
|
|
||||||
const features: PointFeature[] = [];
|
|
||||||
|
|
||||||
const randNormal = () => {
|
|
||||||
let u = 0, v = 0;
|
|
||||||
while (u === 0) u = Math.random();
|
|
||||||
while (v === 0) v = Math.random();
|
|
||||||
return Math.sqrt(-2.0 * Math.log(u)) * Math.cos(2.0 * Math.PI * v);
|
|
||||||
};
|
|
||||||
|
|
||||||
for (let i = 0; i < count; i++) {
|
|
||||||
const radius = Math.abs(randNormal()) * 0.02;
|
|
||||||
const angle = Math.random() * Math.PI * 2;
|
|
||||||
const lon = center.lon + Math.cos(angle) * radius;
|
|
||||||
const lat = center.lat + Math.sin(angle) * radius;
|
|
||||||
const mag = Math.round(Math.max(1, Math.abs(randNormal()) * 6));
|
|
||||||
|
|
||||||
// Create synthetic crash data for backward compatibility
|
|
||||||
const syntheticCrash: CrashData = {
|
|
||||||
id: `synthetic-${i}`,
|
|
||||||
latitude: lat,
|
|
||||||
longitude: lon,
|
|
||||||
reportDate: new Date().toISOString(),
|
|
||||||
address: `Synthetic Location ${i}`,
|
|
||||||
ward: 'Ward 1',
|
|
||||||
totalVehicles: Math.floor(Math.random() * 3) + 1,
|
|
||||||
totalPedestrians: Math.floor(Math.random() * 2),
|
|
||||||
totalBicycles: Math.floor(Math.random() * 2),
|
|
||||||
fatalDriver: 0,
|
|
||||||
fatalPedestrian: 0,
|
|
||||||
fatalBicyclist: 0,
|
|
||||||
majorInjuriesDriver: Math.floor(Math.random() * 2),
|
|
||||||
majorInjuriesPedestrian: 0,
|
|
||||||
majorInjuriesBicyclist: 0,
|
|
||||||
speedingInvolved: Math.floor(Math.random() * 2),
|
|
||||||
};
|
|
||||||
|
|
||||||
features.push({
|
|
||||||
type: 'Feature',
|
|
||||||
geometry: { type: 'Point', coordinates: [lon, lat] },
|
|
||||||
properties: { mag, crashData: syntheticCrash, aiPredicted: false }
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return { type: 'FeatureCollection', features } as GeoJSON.FeatureCollection<GeoJSON.Geometry>;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Enhanced version of generateDCPoints that uses AI predictions
|
|
||||||
*/
|
|
||||||
export const generateDCPointsWithAI = async (count = 500) => {
|
|
||||||
const center = { lon: -77.0369, lat: 38.9072 };
|
|
||||||
const features: PointFeature[] = [];
|
|
||||||
|
|
||||||
const randNormal = () => {
|
|
||||||
let u = 0, v = 0;
|
|
||||||
while (u === 0) u = Math.random();
|
|
||||||
while (v === 0) v = Math.random();
|
|
||||||
return Math.sqrt(-2.0 * Math.log(u)) * Math.cos(2.0 * Math.PI * v);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Generate locations first
|
|
||||||
const locations = [];
|
|
||||||
for (let i = 0; i < count; i++) {
|
|
||||||
const radius = Math.abs(randNormal()) * 0.02;
|
|
||||||
const angle = Math.random() * Math.PI * 2;
|
|
||||||
const lon = center.lon + Math.cos(angle) * radius;
|
|
||||||
const lat = center.lat + Math.sin(angle) * radius;
|
|
||||||
locations.push({ lon, lat, index: i });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get AI predictions in batches to avoid overwhelming the API
|
|
||||||
console.log(`🤖 Getting AI predictions for ${count} synthetic points...`);
|
|
||||||
const BATCH_SIZE = 20;
|
|
||||||
const predictions: (any | null)[] = new Array(count).fill(null);
|
|
||||||
let successfulPredictions = 0;
|
|
||||||
|
|
||||||
for (let i = 0; i < locations.length; i += BATCH_SIZE) {
|
|
||||||
const batch = locations.slice(i, i + BATCH_SIZE);
|
|
||||||
const batchPromises = batch.map(async (location) => {
|
|
||||||
try {
|
|
||||||
const prediction = await getCachedCrashMagnitude(location.lat, location.lon);
|
|
||||||
return prediction;
|
|
||||||
} catch (error) {
|
|
||||||
console.warn(`⚠️ Failed to get AI prediction for synthetic point ${location.index}:`, error);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const batchResults = await Promise.allSettled(batchPromises);
|
|
||||||
batchResults.forEach((result, batchIndex) => {
|
|
||||||
const globalIndex = i + batchIndex;
|
|
||||||
if (result.status === 'fulfilled') {
|
|
||||||
predictions[globalIndex] = result.value;
|
|
||||||
if (result.value) successfulPredictions++;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Small delay between batches
|
|
||||||
if (i + BATCH_SIZE < locations.length) {
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 50));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create features with AI predictions or fallback magnitudes
|
|
||||||
for (let i = 0; i < count; i++) {
|
|
||||||
const location = locations[i];
|
|
||||||
const prediction = predictions[i];
|
|
||||||
|
|
||||||
// Use AI prediction if available, otherwise use random magnitude
|
|
||||||
let mag: number;
|
|
||||||
let aiPredicted = false;
|
|
||||||
|
|
||||||
if (prediction && typeof prediction.prediction === 'number') {
|
|
||||||
// Scale the roadcast API prediction to a reasonable range for visualization
|
|
||||||
if (prediction.prediction > 20) {
|
|
||||||
mag = Math.max(1, Math.min(10, Math.round(prediction.prediction / 10)));
|
|
||||||
} else {
|
|
||||||
mag = Math.max(1, Math.min(10, Math.round(prediction.prediction)));
|
|
||||||
}
|
|
||||||
console.log(`🎯 Synthetic point scaled magnitude from ${prediction.prediction} to ${mag}`);
|
|
||||||
aiPredicted = true;
|
|
||||||
} else {
|
|
||||||
mag = Math.round(Math.max(1, Math.abs(randNormal()) * 6));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create synthetic crash data for backward compatibility
|
|
||||||
const syntheticCrash: CrashData = {
|
|
||||||
id: `synthetic-${i}`,
|
|
||||||
latitude: location.lat,
|
|
||||||
longitude: location.lon,
|
|
||||||
reportDate: new Date().toISOString(),
|
|
||||||
address: `Synthetic Location ${i}`,
|
|
||||||
ward: 'Ward 1',
|
|
||||||
totalVehicles: Math.floor(Math.random() * 3) + 1,
|
|
||||||
totalPedestrians: Math.floor(Math.random() * 2),
|
|
||||||
totalBicycles: Math.floor(Math.random() * 2),
|
|
||||||
fatalDriver: 0,
|
|
||||||
fatalPedestrian: 0,
|
|
||||||
fatalBicyclist: 0,
|
|
||||||
majorInjuriesDriver: Math.floor(Math.random() * 2),
|
|
||||||
majorInjuriesPedestrian: 0,
|
|
||||||
majorInjuriesBicyclist: 0,
|
|
||||||
speedingInvolved: Math.floor(Math.random() * 2),
|
|
||||||
};
|
|
||||||
|
|
||||||
features.push({
|
|
||||||
type: 'Feature',
|
|
||||||
geometry: { type: 'Point', coordinates: [location.lon, location.lat] },
|
|
||||||
properties: { mag, crashData: syntheticCrash, aiPredicted }
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`✅ Generated ${count} synthetic points with ${successfulPredictions} AI predictions`);
|
|
||||||
|
|
||||||
return { type: 'FeatureCollection', features } as GeoJSON.FeatureCollection<GeoJSON.Geometry>;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Calculate crash density along a route path
|
|
||||||
export const calculateRouteCrashDensity = (
|
|
||||||
routeCoordinates: [number, number][],
|
|
||||||
crashData: CrashData[],
|
|
||||||
searchRadiusMeters: number = 100
|
|
||||||
): number[] => {
|
|
||||||
if (!routeCoordinates || routeCoordinates.length === 0) return [];
|
|
||||||
|
|
||||||
const densities: number[] = [];
|
|
||||||
|
|
||||||
for (let i = 0; i < routeCoordinates.length; i++) {
|
|
||||||
const currentPoint = routeCoordinates[i];
|
|
||||||
let crashCount = 0;
|
|
||||||
let severityScore = 0;
|
|
||||||
|
|
||||||
// Count crashes within search radius of current point
|
|
||||||
for (const crash of crashData) {
|
|
||||||
const crashPoint: [number, number] = [crash.longitude, crash.latitude];
|
|
||||||
const distance = haversine(currentPoint, crashPoint);
|
|
||||||
|
|
||||||
if (distance <= searchRadiusMeters) {
|
|
||||||
crashCount++;
|
|
||||||
// Weight by severity
|
|
||||||
const severity = Math.max(1,
|
|
||||||
(crash.fatalDriver + crash.fatalPedestrian + crash.fatalBicyclist) * 5 +
|
|
||||||
(crash.majorInjuriesDriver + crash.majorInjuriesPedestrian + crash.majorInjuriesBicyclist) * 3 +
|
|
||||||
(crash.totalVehicles + crash.totalPedestrians + crash.totalBicycles)
|
|
||||||
);
|
|
||||||
severityScore += severity;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Normalize density score (0-1 range)
|
|
||||||
const density = Math.min(1, severityScore / 20); // Adjust divisor based on data
|
|
||||||
densities.push(density);
|
|
||||||
}
|
|
||||||
|
|
||||||
return densities;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Create gradient stops based on crash densities along route
|
|
||||||
export const createRouteGradientStops = (densities: number[]): any[] => {
|
|
||||||
if (!densities || densities.length === 0) {
|
|
||||||
// Default gradient: green to red
|
|
||||||
return [
|
|
||||||
'interpolate',
|
|
||||||
['linear'],
|
|
||||||
['line-progress'],
|
|
||||||
0, 'green',
|
|
||||||
1, 'red'
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
const stops: any[] = ['interpolate', ['linear'], ['line-progress']];
|
|
||||||
|
|
||||||
for (let i = 0; i < densities.length; i++) {
|
|
||||||
const progress = i / (densities.length - 1);
|
|
||||||
const density = densities[i];
|
|
||||||
|
|
||||||
// Color based on crash density: green (safe) to red (dangerous)
|
|
||||||
let color: string;
|
|
||||||
if (density < 0.2) {
|
|
||||||
color = '#22c55e'; // green
|
|
||||||
} else if (density < 0.4) {
|
|
||||||
color = '#eab308'; // yellow
|
|
||||||
} else if (density < 0.6) {
|
|
||||||
color = '#f97316'; // orange
|
|
||||||
} else if (density < 0.8) {
|
|
||||||
color = '#dc2626'; // red
|
|
||||||
} else {
|
|
||||||
color = '#7f1d1d'; // dark red
|
|
||||||
}
|
|
||||||
|
|
||||||
stops.push(progress, color);
|
|
||||||
}
|
|
||||||
|
|
||||||
return stops;
|
|
||||||
};
|
|
||||||
@@ -18,7 +18,6 @@ export default function Home() {
|
|||||||
const [heatRadius, setHeatRadius] = useState(16);
|
const [heatRadius, setHeatRadius] = useState(16);
|
||||||
const [heatIntensity, setHeatIntensity] = useState(1);
|
const [heatIntensity, setHeatIntensity] = useState(1);
|
||||||
const [gradientRoutes, setGradientRoutes] = useState(true);
|
const [gradientRoutes, setGradientRoutes] = useState(true);
|
||||||
const [useAIMagnitudes, setUseAIMagnitudes] = useState(true); // Default to true since roadcast API is reliable
|
|
||||||
|
|
||||||
const [popup, setPopup] = useState<PopupData>(null);
|
const [popup, setPopup] = useState<PopupData>(null);
|
||||||
const [popupVisible, setPopupVisible] = useState(false);
|
const [popupVisible, setPopupVisible] = useState(false);
|
||||||
@@ -67,8 +66,6 @@ export default function Home() {
|
|||||||
onChangeIntensity={(v) => setHeatIntensity(v)}
|
onChangeIntensity={(v) => setHeatIntensity(v)}
|
||||||
gradientRoutes={gradientRoutes}
|
gradientRoutes={gradientRoutes}
|
||||||
onToggleGradientRoutes={(v) => setGradientRoutes(v)}
|
onToggleGradientRoutes={(v) => setGradientRoutes(v)}
|
||||||
useAIMagnitudes={useAIMagnitudes}
|
|
||||||
onToggleAIMagnitudes={(v) => setUseAIMagnitudes(v)}
|
|
||||||
crashDataHook={crashDataHook}
|
crashDataHook={crashDataHook}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -82,7 +79,6 @@ export default function Home() {
|
|||||||
crashData={crashDataHook.data}
|
crashData={crashDataHook.data}
|
||||||
crashDataHook={crashDataHook}
|
crashDataHook={crashDataHook}
|
||||||
isMapPickingMode={isMapPickingMode}
|
isMapPickingMode={isMapPickingMode}
|
||||||
useAIMagnitudes={useAIMagnitudes}
|
|
||||||
onMapReady={(m) => { mapRef.current = m; }}
|
onMapReady={(m) => { mapRef.current = m; }}
|
||||||
onPopupCreate={(p) => { setPopupVisible(false); setPopup(p); requestAnimationFrame(() => setPopupVisible(true)); }}
|
onPopupCreate={(p) => { setPopupVisible(false); setPopup(p); requestAnimationFrame(() => setPopupVisible(true)); }}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,255 +0,0 @@
|
|||||||
/**
|
|
||||||
* API service for crash magnitude prediction using roadcast model
|
|
||||||
*/
|
|
||||||
|
|
||||||
export interface CrashMagnitudePrediction {
|
|
||||||
prediction: number;
|
|
||||||
confidence?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface CrashMagnitudeRequest {
|
|
||||||
source: {
|
|
||||||
lat: number;
|
|
||||||
lon: number;
|
|
||||||
};
|
|
||||||
destination: {
|
|
||||||
lat: number;
|
|
||||||
lon: number;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface CrashMagnitudeResponse {
|
|
||||||
prediction: CrashMagnitudePrediction;
|
|
||||||
called_with: string;
|
|
||||||
diagnostics?: {
|
|
||||||
input_dim: number;
|
|
||||||
};
|
|
||||||
index?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get crash magnitude prediction from roadcast API
|
|
||||||
* Simplified version that always tries to get the prediction
|
|
||||||
*/
|
|
||||||
export async function getCrashMagnitudePrediction(
|
|
||||||
sourceLat: number,
|
|
||||||
sourceLon: number,
|
|
||||||
destLat: number,
|
|
||||||
destLon: number
|
|
||||||
): Promise<CrashMagnitudePrediction | null> {
|
|
||||||
|
|
||||||
try {
|
|
||||||
const requestBody: CrashMagnitudeRequest = {
|
|
||||||
source: {
|
|
||||||
lat: sourceLat,
|
|
||||||
lon: sourceLon
|
|
||||||
},
|
|
||||||
destination: {
|
|
||||||
lat: destLat,
|
|
||||||
lon: destLon
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
console.log('<27> Requesting crash magnitude from roadcast API:', requestBody);
|
|
||||||
|
|
||||||
// Create fetch options with timeout
|
|
||||||
const fetchOptions: RequestInit = {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'Accept': 'application/json',
|
|
||||||
},
|
|
||||||
body: JSON.stringify(requestBody),
|
|
||||||
};
|
|
||||||
|
|
||||||
// Add timeout if AbortSignal.timeout is supported
|
|
||||||
try {
|
|
||||||
if (typeof AbortSignal !== 'undefined' && 'timeout' in AbortSignal) {
|
|
||||||
fetchOptions.signal = AbortSignal.timeout(10000); // 10 second timeout
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
// AbortSignal.timeout not supported, continue without timeout
|
|
||||||
console.log('⚠️ AbortSignal.timeout not supported, continuing without timeout');
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await fetch('http://localhost:5000/predict', fetchOptions);
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
console.error('❌ Roadcast API error:', response.status, response.statusText);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const data: CrashMagnitudeResponse = await response.json();
|
|
||||||
console.log('✅ Roadcast magnitude prediction received:', data);
|
|
||||||
|
|
||||||
// Handle roadcast API response format
|
|
||||||
// The roadcast API returns the magnitude in the 'index' field
|
|
||||||
if (data.index !== undefined) {
|
|
||||||
console.log('🎯 Using roadcast index as crash magnitude:', data.index);
|
|
||||||
return {
|
|
||||||
prediction: data.index,
|
|
||||||
confidence: 0.95 // High confidence for roadcast model
|
|
||||||
};
|
|
||||||
} else if (data.prediction && typeof data.prediction === 'object' && data.prediction.prediction !== undefined) {
|
|
||||||
// Fallback: Response format: { prediction: { prediction: number } }
|
|
||||||
return data.prediction;
|
|
||||||
} else if (typeof data.prediction === 'number') {
|
|
||||||
// Fallback: Response format: { prediction: number }
|
|
||||||
return { prediction: data.prediction };
|
|
||||||
}
|
|
||||||
|
|
||||||
console.warn('⚠️ No usable magnitude data in roadcast API response:', data);
|
|
||||||
return null;
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
|
|
||||||
if (error instanceof Error) {
|
|
||||||
if (error.name === 'AbortError') {
|
|
||||||
console.warn('⏰ Crash magnitude API request timed out');
|
|
||||||
} else if (error.message.includes('fetch')) {
|
|
||||||
console.warn('🌐 Network error accessing crash magnitude API:', error.message);
|
|
||||||
} else {
|
|
||||||
console.warn('❌ Error fetching crash magnitude prediction:', error.message);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.warn('❌ Unknown error fetching crash magnitude prediction:', error);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get crash magnitude for a single point (using same point for source and destination)
|
|
||||||
*/
|
|
||||||
export async function getPointCrashMagnitude(
|
|
||||||
lat: number,
|
|
||||||
lon: number
|
|
||||||
): Promise<CrashMagnitudePrediction | null> {
|
|
||||||
return getCrashMagnitudePrediction(lat, lon, lat, lon);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Batch get crash magnitude predictions for multiple locations
|
|
||||||
*/
|
|
||||||
export async function getBatchCrashMagnitudes(
|
|
||||||
locations: Array<{ lat: number; lon: number; id?: string }>
|
|
||||||
): Promise<Array<{ prediction: CrashMagnitudePrediction | null; id?: string }>> {
|
|
||||||
const results = await Promise.allSettled(
|
|
||||||
locations.map(async (location) => {
|
|
||||||
const prediction = await getPointCrashMagnitude(location.lat, location.lon);
|
|
||||||
return { prediction, id: location.id };
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
return results.map((result, index) => {
|
|
||||||
if (result.status === 'fulfilled') {
|
|
||||||
return result.value;
|
|
||||||
} else {
|
|
||||||
console.error(`❌ Failed to get magnitude for location ${index}:`, result.reason);
|
|
||||||
return { prediction: null, id: locations[index].id };
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Cache for magnitude predictions to avoid repeated API calls
|
|
||||||
*/
|
|
||||||
const magnitudeCache = new Map<string, { prediction: CrashMagnitudePrediction; timestamp: number }>();
|
|
||||||
const CACHE_DURATION = 5 * 60 * 1000; // 5 minutes
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Status tracking for roadcast API (simplified - always available)
|
|
||||||
*/
|
|
||||||
|
|
||||||
function isCircuitBreakerOpen(): boolean {
|
|
||||||
// Roadcast API is local and reliable, always return false
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
function recordCircuitBreakerFailure(): void {
|
|
||||||
// Not needed for local roadcast API, but kept for compatibility
|
|
||||||
}
|
|
||||||
|
|
||||||
function recordCircuitBreakerSuccess(): void {
|
|
||||||
// Not needed for local roadcast API, but kept for compatibility
|
|
||||||
}
|
|
||||||
|
|
||||||
function getCacheKey(lat: number, lon: number): string {
|
|
||||||
return `${lat.toFixed(6)},${lon.toFixed(6)}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get cached crash magnitude or fetch if not available/expired
|
|
||||||
*/
|
|
||||||
export async function getCachedCrashMagnitude(
|
|
||||||
lat: number,
|
|
||||||
lon: number
|
|
||||||
): Promise<CrashMagnitudePrediction | null> {
|
|
||||||
const cacheKey = getCacheKey(lat, lon);
|
|
||||||
const cached = magnitudeCache.get(cacheKey);
|
|
||||||
|
|
||||||
if (cached && Date.now() - cached.timestamp < CACHE_DURATION) {
|
|
||||||
console.log('📦 Using cached magnitude prediction for:', cacheKey);
|
|
||||||
return cached.prediction;
|
|
||||||
}
|
|
||||||
|
|
||||||
const prediction = await getPointCrashMagnitude(lat, lon);
|
|
||||||
|
|
||||||
if (prediction) {
|
|
||||||
magnitudeCache.set(cacheKey, {
|
|
||||||
prediction,
|
|
||||||
timestamp: Date.now()
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return prediction;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get current status of the roadcast API by testing connection
|
|
||||||
*/
|
|
||||||
export async function getCircuitBreakerStatus(): Promise<{ isOpen: boolean; failures: number; resetTime?: number }> {
|
|
||||||
try {
|
|
||||||
// Test the roadcast API with a simple request
|
|
||||||
const controller = new AbortController();
|
|
||||||
const timeoutId = setTimeout(() => controller.abort(), 3000); // 3 second timeout
|
|
||||||
|
|
||||||
const response = await fetch('http://localhost:5000/predict', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
source: { lat: 38.9, lon: -77.0 },
|
|
||||||
destination: { lat: 38.91, lon: -77.01 }
|
|
||||||
}),
|
|
||||||
signal: controller.signal
|
|
||||||
});
|
|
||||||
|
|
||||||
clearTimeout(timeoutId);
|
|
||||||
|
|
||||||
if (response.ok) {
|
|
||||||
const data = await response.json();
|
|
||||||
console.log('🟢 Roadcast API status check successful:', data.index);
|
|
||||||
return {
|
|
||||||
isOpen: false, // API is available
|
|
||||||
failures: 0,
|
|
||||||
resetTime: undefined
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
console.log('🔴 Roadcast API status check failed:', response.status);
|
|
||||||
return {
|
|
||||||
isOpen: true, // API returned error
|
|
||||||
failures: 1,
|
|
||||||
resetTime: undefined
|
|
||||||
};
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.log('🔌 Roadcast API unavailable:', error);
|
|
||||||
return {
|
|
||||||
isOpen: true, // API is unavailable
|
|
||||||
failures: 1,
|
|
||||||
resetTime: undefined
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
const FLASK_API_BASE = 'http://127.0.0.1:5001';
|
const FLASK_API_BASE = 'https://llm.sirblob.co';
|
||||||
|
|
||||||
export interface WeatherData {
|
export interface WeatherData {
|
||||||
temperature: number;
|
temperature: number;
|
||||||
@@ -22,6 +22,33 @@ export interface CrashAnalysisData {
|
|||||||
safetyAnalysis?: string;
|
safetyAnalysis?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface SafeRouteData {
|
||||||
|
success: boolean;
|
||||||
|
start_coordinates: { lat: number; lon: number };
|
||||||
|
end_coordinates: { lat: number; lon: number };
|
||||||
|
recommended_route: {
|
||||||
|
coordinates: [number, number][];
|
||||||
|
distance_km: number;
|
||||||
|
duration_min: number;
|
||||||
|
geometry?: any; // GeoJSON for Mapbox
|
||||||
|
safety_score: number;
|
||||||
|
crashes_nearby: number;
|
||||||
|
max_danger_score: number;
|
||||||
|
};
|
||||||
|
safety_analysis: string;
|
||||||
|
weather_summary?: string;
|
||||||
|
route_comparison?: any;
|
||||||
|
alternative_routes: Array<{
|
||||||
|
route_id: string;
|
||||||
|
coordinates: [number, number][];
|
||||||
|
distance_km: number;
|
||||||
|
duration_min: number;
|
||||||
|
geometry?: any;
|
||||||
|
safety_score: number;
|
||||||
|
crashes_nearby: number;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
|
||||||
export const fetchWeatherData = async (lat: number, lng: number): Promise<WeatherData> => {
|
export const fetchWeatherData = async (lat: number, lng: number): Promise<WeatherData> => {
|
||||||
const response = await fetch(`${FLASK_API_BASE}/api/weather?lat=${lat}&lon=${lng}`);
|
const response = await fetch(`${FLASK_API_BASE}/api/weather?lat=${lat}&lon=${lng}`);
|
||||||
|
|
||||||
@@ -50,6 +77,38 @@ export const fetchCrashAnalysis = async (lat: number, lng: number): Promise<Cras
|
|||||||
return transformCrashAnalysis(data);
|
return transformCrashAnalysis(data);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const fetchSafeRoute = async (
|
||||||
|
startLat: number,
|
||||||
|
startLon: number,
|
||||||
|
endLat: number,
|
||||||
|
endLon: number
|
||||||
|
): Promise<SafeRouteData> => {
|
||||||
|
const response = await fetch(`${FLASK_API_BASE}/api/find-safe-route`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
start_lat: startLat,
|
||||||
|
start_lon: startLon,
|
||||||
|
end_lat: endLat,
|
||||||
|
end_lon: endLon,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Failed to fetch safe route');
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (!data.success) {
|
||||||
|
throw new Error(data.error || 'Unknown error occurred');
|
||||||
|
}
|
||||||
|
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
// Transform Flask weather API response to our WeatherData interface
|
// Transform Flask weather API response to our WeatherData interface
|
||||||
const transformWeatherData = (apiResponse: any): WeatherData => {
|
const transformWeatherData = (apiResponse: any): WeatherData => {
|
||||||
// Extract summary if available
|
// Extract summary if available
|
||||||
@@ -86,7 +145,7 @@ const transformWeatherData = (apiResponse: any): WeatherData => {
|
|||||||
const transformCrashAnalysis = (apiResponse: any): CrashAnalysisData => {
|
const transformCrashAnalysis = (apiResponse: any): CrashAnalysisData => {
|
||||||
const data = apiResponse;
|
const data = apiResponse;
|
||||||
|
|
||||||
// Extract risk level from safety analysis text
|
// Extract risk level from safety analysis text with multiple fallback strategies
|
||||||
let riskLevel = 'unknown';
|
let riskLevel = 'unknown';
|
||||||
let recommendations: string[] = [];
|
let recommendations: string[] = [];
|
||||||
|
|
||||||
@@ -94,10 +153,11 @@ const transformCrashAnalysis = (apiResponse: any): CrashAnalysisData => {
|
|||||||
const safetyText = data.safety_analysis;
|
const safetyText = data.safety_analysis;
|
||||||
const safetyTextLower = safetyText.toLowerCase();
|
const safetyTextLower = safetyText.toLowerCase();
|
||||||
|
|
||||||
// Look for danger level assessment (now without markdown formatting)
|
// Strategy 1: Look for explicit danger level assessment
|
||||||
const dangerLevelMatch = safetyText.match(/danger level assessment[:\s]*([^.\n]+)/i);
|
const dangerLevelMatch = safetyText.match(/danger level assessment[:\s]*([^.\n]+)/i);
|
||||||
if (dangerLevelMatch) {
|
if (dangerLevelMatch) {
|
||||||
const level = dangerLevelMatch[1].trim().toLowerCase();
|
const level = dangerLevelMatch[1].trim().toLowerCase();
|
||||||
|
console.log('🎯 Found danger level assessment:', level);
|
||||||
if (level.includes('very high') || level.includes('extreme')) {
|
if (level.includes('very high') || level.includes('extreme')) {
|
||||||
riskLevel = 'high';
|
riskLevel = 'high';
|
||||||
} else if (level.includes('high')) {
|
} else if (level.includes('high')) {
|
||||||
@@ -107,21 +167,48 @@ const transformCrashAnalysis = (apiResponse: any): CrashAnalysisData => {
|
|||||||
} else if (level.includes('low')) {
|
} else if (level.includes('low')) {
|
||||||
riskLevel = 'low';
|
riskLevel = 'low';
|
||||||
}
|
}
|
||||||
} else {
|
}
|
||||||
// Fallback to searching for risk indicators in the text
|
|
||||||
if (safetyTextLower.includes('very high') || safetyTextLower.includes('extremely dangerous')) {
|
// Strategy 2: Look for risk level patterns if first strategy failed
|
||||||
riskLevel = 'high';
|
if (riskLevel === 'unknown') {
|
||||||
} else if (safetyTextLower.includes('high risk') || safetyTextLower.includes('very dangerous')) {
|
const riskPatterns = [
|
||||||
riskLevel = 'high';
|
{ keywords: ['very high risk', 'extremely dangerous', 'very dangerous', 'extreme danger'], level: 'high' },
|
||||||
} else if (safetyTextLower.includes('moderate risk') || safetyTextLower.includes('medium risk')) {
|
{ keywords: ['high risk', 'high danger', 'dangerous area', 'significant risk'], level: 'high' },
|
||||||
riskLevel = 'medium';
|
{ keywords: ['moderate risk', 'medium risk', 'moderate danger', 'moderately dangerous'], level: 'medium' },
|
||||||
} else if (safetyTextLower.includes('low risk') || safetyTextLower.includes('relatively safe')) {
|
{ keywords: ['low risk', 'relatively safe', 'low danger', 'minimal risk'], level: 'low' }
|
||||||
riskLevel = 'low';
|
];
|
||||||
|
|
||||||
|
for (const pattern of riskPatterns) {
|
||||||
|
if (pattern.keywords.some(keyword => safetyTextLower.includes(keyword))) {
|
||||||
|
riskLevel = pattern.level;
|
||||||
|
console.log('🎯 Found risk pattern:', pattern.keywords[0], '→', pattern.level);
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extract recommendations from safety analysis (now without markdown)
|
// Strategy 3: Fallback to crash data severity if text parsing fails
|
||||||
const recommendationsMatch = safetyText.match(/specific recommendations[^:]*:([\s\S]*?)(?=\n\n|\d+\.|$)/i);
|
if (riskLevel === 'unknown' && data.crash_summary) {
|
||||||
|
const summary = data.crash_summary;
|
||||||
|
const totalCrashes = summary.total_crashes || 0;
|
||||||
|
const totalCasualties = summary.total_casualties || 0;
|
||||||
|
|
||||||
|
if (totalCasualties > 10 || totalCrashes > 20) {
|
||||||
|
riskLevel = 'high';
|
||||||
|
console.log('🎯 Fallback: High risk based on casualties/crashes', { totalCasualties, totalCrashes });
|
||||||
|
} else if (totalCasualties > 3 || totalCrashes > 8) {
|
||||||
|
riskLevel = 'medium';
|
||||||
|
console.log('🎯 Fallback: Medium risk based on casualties/crashes', { totalCasualties, totalCrashes });
|
||||||
|
} else if (totalCrashes > 0) {
|
||||||
|
riskLevel = 'low';
|
||||||
|
console.log('🎯 Fallback: Low risk based on crashes', { totalCrashes });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('📊 Final risk level determination:', riskLevel);
|
||||||
|
|
||||||
|
// Extract recommendations from safety analysis (more flexible approach)
|
||||||
|
const recommendationsMatch = safetyText.match(/(?:specific recommendations|recommendations)[^:]*:([\s\S]*?)(?=\n\n|\d+\.|$)/i);
|
||||||
if (recommendationsMatch) {
|
if (recommendationsMatch) {
|
||||||
const recommendationsText = recommendationsMatch[1];
|
const recommendationsText = recommendationsMatch[1];
|
||||||
// Split by lines and filter for meaningful recommendations
|
// Split by lines and filter for meaningful recommendations
|
||||||
|
|||||||
Reference in New Issue
Block a user