Merge branch 'main' of github.com:SirBlobby/VTHacks13
This commit is contained in:
@@ -366,6 +366,87 @@ def get_single_route_endpoint():
|
|||||||
'error': str(e)
|
'error': str(e)
|
||||||
}), 500
|
}), 500
|
||||||
|
|
||||||
|
@app.route('/predict', methods=['POST', 'OPTIONS'])
|
||||||
|
def predict_crash_magnitude():
|
||||||
|
"""
|
||||||
|
Predict crash magnitude for a route using AI model.
|
||||||
|
Expected request body:
|
||||||
|
{
|
||||||
|
"source": {"lat": float, "lon": float},
|
||||||
|
"destination": {"lat": float, "lon": float}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
# Handle preflight CORS request
|
||||||
|
if request.method == 'OPTIONS':
|
||||||
|
response = jsonify({'status': 'ok'})
|
||||||
|
response.headers.add('Access-Control-Allow-Origin', '*')
|
||||||
|
response.headers.add('Access-Control-Allow-Headers', 'Content-Type')
|
||||||
|
response.headers.add('Access-Control-Allow-Methods', 'POST, OPTIONS')
|
||||||
|
return response
|
||||||
|
|
||||||
|
try:
|
||||||
|
data = request.get_json()
|
||||||
|
if not data:
|
||||||
|
return jsonify({'error': 'No JSON data provided'}), 400
|
||||||
|
|
||||||
|
# Validate required fields
|
||||||
|
if 'source' not in data or 'destination' not in data:
|
||||||
|
return jsonify({'error': 'Missing source or destination coordinates'}), 400
|
||||||
|
|
||||||
|
source = data['source']
|
||||||
|
destination = data['destination']
|
||||||
|
|
||||||
|
# Validate coordinate format
|
||||||
|
required_fields = ['lat', 'lon']
|
||||||
|
for coord_set, name in [(source, 'source'), (destination, 'destination')]:
|
||||||
|
for field in required_fields:
|
||||||
|
if field not in coord_set:
|
||||||
|
return jsonify({'error': f'Missing {field} in {name} coordinates'}), 400
|
||||||
|
try:
|
||||||
|
float(coord_set[field])
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
return jsonify({'error': f'Invalid {field} value in {name} coordinates'}), 400
|
||||||
|
|
||||||
|
# For now, return a mock prediction based on distance
|
||||||
|
# In a real implementation, this would call your AI model
|
||||||
|
import math
|
||||||
|
|
||||||
|
lat1, lon1 = float(source['lat']), float(source['lon'])
|
||||||
|
lat2, lon2 = float(destination['lat']), float(destination['lon'])
|
||||||
|
|
||||||
|
# Calculate distance (rough approximation)
|
||||||
|
distance = math.sqrt((lat2 - lat1)**2 + (lon2 - lon1)**2)
|
||||||
|
|
||||||
|
# Mock prediction: longer routes might have higher crash magnitude
|
||||||
|
# This is just placeholder logic until you integrate your actual AI model
|
||||||
|
base_magnitude = min(distance * 50, 1.0) # Cap at 1.0
|
||||||
|
confidence = 0.85 # Mock confidence
|
||||||
|
|
||||||
|
response_data = {
|
||||||
|
'prediction': {
|
||||||
|
'prediction': base_magnitude,
|
||||||
|
'confidence': confidence
|
||||||
|
},
|
||||||
|
'called_with': f"Route from ({lat1}, {lon1}) to ({lat2}, {lon2})",
|
||||||
|
'diagnostics': {
|
||||||
|
'input_dim': 4 # lat1, lon1, lat2, lon2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
print(f"🔮 Crash magnitude prediction request: {data}")
|
||||||
|
print(f"📊 Returning prediction: {response_data['prediction']['prediction']:.3f}")
|
||||||
|
|
||||||
|
response = jsonify(response_data)
|
||||||
|
response.headers.add('Access-Control-Allow-Origin', '*')
|
||||||
|
return response
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ Error in crash magnitude prediction: {e}")
|
||||||
|
traceback.print_exc()
|
||||||
|
error_response = jsonify({'error': str(e)})
|
||||||
|
error_response.headers.add('Access-Control-Allow-Origin', '*')
|
||||||
|
return error_response, 500
|
||||||
|
|
||||||
@app.errorhandler(404)
|
@app.errorhandler(404)
|
||||||
def not_found(error):
|
def not_found(error):
|
||||||
return jsonify({'success': False, 'error': 'Endpoint not found'}), 404
|
return jsonify({'success': False, 'error': 'Endpoint not found'}), 404
|
||||||
@@ -382,6 +463,7 @@ if __name__ == '__main__':
|
|||||||
print(" - POST /api/analyze-crashes")
|
print(" - POST /api/analyze-crashes")
|
||||||
print(" - POST /api/find-safe-route")
|
print(" - POST /api/find-safe-route")
|
||||||
print(" - POST /api/get-single-route")
|
print(" - POST /api/get-single-route")
|
||||||
|
print(" - POST /predict (AI crash magnitude prediction)")
|
||||||
print("\n🌐 Server running on http://localhost:5001")
|
print("\n🌐 Server running on http://localhost:5001")
|
||||||
|
|
||||||
app.run(debug=True, host='0.0.0.0', port=5001)
|
app.run(debug=True, host='0.0.0.0', port=5001)
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
from flask import Flask, request, jsonify
|
from flask import Flask, request, jsonify
|
||||||
|
from flask_cors import CORS
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
from train import compute_index
|
from train import compute_index
|
||||||
from models import load_model
|
from models import load_model
|
||||||
@@ -8,6 +9,14 @@ from models import MLP
|
|||||||
load_dotenv()
|
load_dotenv()
|
||||||
|
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
|
# Enable CORS for all routes, origins, and methods
|
||||||
|
CORS(app, resources={
|
||||||
|
r"/*": {
|
||||||
|
"origins": "*",
|
||||||
|
"methods": ["GET", "POST", "PUT", "DELETE", "OPTIONS"],
|
||||||
|
"allow_headers": ["Content-Type", "Authorization", "Accept", "Origin", "X-Requested-With"]
|
||||||
|
}
|
||||||
|
})
|
||||||
import os
|
import os
|
||||||
import threading
|
import threading
|
||||||
import json
|
import json
|
||||||
@@ -121,6 +130,7 @@ def predict_endpoint():
|
|||||||
# build feature vector of correct length and populate lat/lon using preprocess meta if available
|
# build feature vector of correct length and populate lat/lon using preprocess meta if available
|
||||||
feature_vector = np.zeros(int(input_dim), dtype=float)
|
feature_vector = np.zeros(int(input_dim), dtype=float)
|
||||||
meta_path = os.path.join(os.getcwd(), 'preprocess_meta.npz')
|
meta_path = os.path.join(os.getcwd(), 'preprocess_meta.npz')
|
||||||
|
|
||||||
if os.path.exists(meta_path):
|
if os.path.exists(meta_path):
|
||||||
try:
|
try:
|
||||||
meta = np.load(meta_path, allow_pickle=True)
|
meta = np.load(meta_path, allow_pickle=True)
|
||||||
@@ -128,33 +138,83 @@ def predict_endpoint():
|
|||||||
means = meta.get('means')
|
means = meta.get('means')
|
||||||
if means is not None and len(means) == input_dim:
|
if means is not None and len(means) == input_dim:
|
||||||
feature_vector[:] = means
|
feature_vector[:] = means
|
||||||
|
|
||||||
col_lower = [c.lower() for c in cols]
|
col_lower = [c.lower() for c in cols]
|
||||||
if 'lat' in col_lower:
|
print(f"📋 Available columns: {col_lower[:10]}...") # Show first 10 columns
|
||||||
feature_vector[col_lower.index('lat')] = src_lat
|
|
||||||
elif 'latitude' in col_lower:
|
# Try to find and populate coordinate fields
|
||||||
feature_vector[col_lower.index('latitude')] = src_lat
|
coord_mappings = [
|
||||||
else:
|
(('lat', 'latitude', 'src_lat', 'source_lat'), src_lat),
|
||||||
feature_vector[0] = src_lat
|
(('lon', 'lng', 'longitude', 'src_lon', 'source_lon'), src_lon),
|
||||||
if 'lon' in col_lower:
|
(('dst_lat', 'dest_lat', 'destination_lat', 'end_lat'), dst_lat),
|
||||||
feature_vector[col_lower.index('lon')] = src_lon
|
(('dst_lon', 'dest_lon', 'destination_lon', 'end_lon', 'dst_lng'), dst_lon)
|
||||||
elif 'longitude' in col_lower:
|
]
|
||||||
feature_vector[col_lower.index('longitude')] = src_lon
|
|
||||||
else:
|
for possible_names, value in coord_mappings:
|
||||||
if input_dim > 1:
|
for name in possible_names:
|
||||||
feature_vector[1] = src_lon
|
if name in col_lower:
|
||||||
except Exception:
|
idx = col_lower.index(name)
|
||||||
|
feature_vector[idx] = value
|
||||||
|
print(f"✅ Mapped {name} (index {idx}) = {value}")
|
||||||
|
break
|
||||||
|
|
||||||
|
# Calculate route features that might be useful
|
||||||
|
route_distance = ((dst_lat - src_lat)**2 + (dst_lon - src_lon)**2)**0.5
|
||||||
|
midpoint_lat = (src_lat + dst_lat) / 2
|
||||||
|
midpoint_lon = (src_lon + dst_lon) / 2
|
||||||
|
|
||||||
|
# Try to populate additional features that might exist
|
||||||
|
additional_features = {
|
||||||
|
'distance': route_distance,
|
||||||
|
'route_distance': route_distance,
|
||||||
|
'midpoint_lat': midpoint_lat,
|
||||||
|
'midpoint_lon': midpoint_lon,
|
||||||
|
'lat_diff': abs(dst_lat - src_lat),
|
||||||
|
'lon_diff': abs(dst_lon - src_lon)
|
||||||
|
}
|
||||||
|
|
||||||
|
for feature_name, feature_value in additional_features.items():
|
||||||
|
if feature_name in col_lower:
|
||||||
|
idx = col_lower.index(feature_name)
|
||||||
|
feature_vector[idx] = feature_value
|
||||||
|
print(f"✅ Mapped {feature_name} (index {idx}) = {feature_value}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"⚠️ Error processing metadata: {e}")
|
||||||
|
# Fallback to simple coordinate mapping
|
||||||
feature_vector[:] = 0.0
|
feature_vector[:] = 0.0
|
||||||
feature_vector[0] = src_lat
|
feature_vector[0] = src_lat
|
||||||
if input_dim > 1:
|
if input_dim > 1:
|
||||||
feature_vector[1] = src_lon
|
feature_vector[1] = src_lon
|
||||||
|
if input_dim > 2:
|
||||||
|
feature_vector[2] = dst_lat
|
||||||
|
if input_dim > 3:
|
||||||
|
feature_vector[3] = dst_lon
|
||||||
else:
|
else:
|
||||||
|
print("⚠️ No preprocess_meta.npz found, using simple coordinate mapping")
|
||||||
|
# Simple fallback mapping
|
||||||
feature_vector[0] = src_lat
|
feature_vector[0] = src_lat
|
||||||
if input_dim > 1:
|
if input_dim > 1:
|
||||||
feature_vector[1] = src_lon
|
feature_vector[1] = src_lon
|
||||||
|
if input_dim > 2:
|
||||||
|
feature_vector[2] = dst_lat
|
||||||
|
if input_dim > 3:
|
||||||
|
feature_vector[3] = dst_lon
|
||||||
|
|
||||||
|
# Add some derived features to create more variation
|
||||||
|
if input_dim > 4:
|
||||||
|
feature_vector[4] = ((dst_lat - src_lat)**2 + (dst_lon - src_lon)**2)**0.5 # distance
|
||||||
|
if input_dim > 5:
|
||||||
|
feature_vector[5] = (src_lat + dst_lat) / 2 # midpoint lat
|
||||||
|
if input_dim > 6:
|
||||||
|
feature_vector[6] = (src_lon + dst_lon) / 2 # midpoint lon
|
||||||
|
|
||||||
# compute index using model
|
# compute index using model
|
||||||
try:
|
try:
|
||||||
|
print(f"🔍 Feature vector for prediction: {feature_vector[:8]}...") # Show first 8 values
|
||||||
|
print(f"📍 Coordinates: src({src_lat}, {src_lon}) → dst({dst_lat}, {dst_lon})")
|
||||||
index = compute_index(model, feature_vector)
|
index = compute_index(model, feature_vector)
|
||||||
|
print(f"📊 Computed index: {index}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return jsonify({"error": "compute_index failed", "detail": str(e)}), 500
|
return jsonify({"error": "compute_index failed", "detail": str(e)}), 500
|
||||||
|
|
||||||
|
|||||||
@@ -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, haversine, PointFeature, convertCrashDataToGeoJSON } from '../lib/mapUtils';
|
import { generateDCPoints, generateDCPointsWithAI, haversine, PointFeature, convertCrashDataToGeoJSON, convertCrashDataToGeoJSONWithAI } 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,6 +49,7 @@ 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({
|
||||||
@@ -63,7 +64,8 @@ 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);
|
||||||
@@ -71,6 +73,7 @@ export default function MapView({
|
|||||||
const styleChoiceRef = useRef<'dark' | 'streets'>(mapStyleChoice);
|
const styleChoiceRef = useRef<'dark' | 'streets'>(mapStyleChoice);
|
||||||
const isMapPickingModeRef = useRef<boolean>(isMapPickingMode);
|
const isMapPickingModeRef = useRef<boolean>(isMapPickingMode);
|
||||||
const [size, setSize] = useState({ width: 0, height: 0 });
|
const [size, setSize] = useState({ width: 0, height: 0 });
|
||||||
|
const [isLoadingAIPredictions, setIsLoadingAIPredictions] = useState(false);
|
||||||
const dcDataRef = useRef<GeoJSON.FeatureCollection | null>(null);
|
const dcDataRef = useRef<GeoJSON.FeatureCollection | null>(null);
|
||||||
const internalCrashDataHook = useCrashData({ autoLoad: false, limit: 10000 }); // Don't auto-load if external data provided
|
const internalCrashDataHook = useCrashData({ autoLoad: false, limit: 10000 }); // Don't auto-load if external data provided
|
||||||
|
|
||||||
@@ -84,9 +87,25 @@ export default function MapView({
|
|||||||
const currentCrashDataHook = crashDataHook || internalCrashDataHook;
|
const currentCrashDataHook = crashDataHook || internalCrashDataHook;
|
||||||
const activeData = crashData.length > 0 ? crashData : currentCrashDataHook.data;
|
const activeData = crashData.length > 0 ? crashData : currentCrashDataHook.data;
|
||||||
console.log('MapView useEffect: crashData.length =', crashData.length, 'crashDataHook.data.length =', currentCrashDataHook.data.length);
|
console.log('MapView useEffect: crashData.length =', crashData.length, 'crashDataHook.data.length =', currentCrashDataHook.data.length);
|
||||||
|
|
||||||
if (useRealCrashData && activeData.length > 0) {
|
if (useRealCrashData && activeData.length > 0) {
|
||||||
console.log('Converting crash data to GeoJSON...');
|
console.log('Converting crash data to GeoJSON...');
|
||||||
dcDataRef.current = convertCrashDataToGeoJSON(activeData);
|
|
||||||
|
const processData = async () => {
|
||||||
|
setIsLoadingAIPredictions(useAIMagnitudes);
|
||||||
|
|
||||||
|
let geoJSONData: GeoJSON.FeatureCollection;
|
||||||
|
if (useAIMagnitudes) {
|
||||||
|
console.log('🤖 Using AI-enhanced crash data conversion...');
|
||||||
|
geoJSONData = await convertCrashDataToGeoJSONWithAI(activeData);
|
||||||
|
} else {
|
||||||
|
console.log('📊 Using standard crash data conversion...');
|
||||||
|
geoJSONData = convertCrashDataToGeoJSON(activeData);
|
||||||
|
}
|
||||||
|
|
||||||
|
dcDataRef.current = geoJSONData;
|
||||||
|
setIsLoadingAIPredictions(false);
|
||||||
|
|
||||||
// Update the map source if map is ready
|
// Update the map source if map is ready
|
||||||
const map = mapRef.current;
|
const map = mapRef.current;
|
||||||
if (map && map.isStyleLoaded()) {
|
if (map && map.isStyleLoaded()) {
|
||||||
@@ -94,70 +113,14 @@ export default function MapView({
|
|||||||
if (map.getSource('dc-quakes')) {
|
if (map.getSource('dc-quakes')) {
|
||||||
(map.getSource('dc-quakes') as mapboxgl.GeoJSONSource).setData(dcDataRef.current);
|
(map.getSource('dc-quakes') as mapboxgl.GeoJSONSource).setData(dcDataRef.current);
|
||||||
} else {
|
} else {
|
||||||
console.log('Source not found, calling addDataAndLayers');
|
console.log('Source not found, will be added when map loads');
|
||||||
// Call the inner function manually - we need to recreate it here
|
|
||||||
if (dcDataRef.current) {
|
|
||||||
console.log('Adding data and layers, data has', dcDataRef.current.features.length, 'features');
|
|
||||||
if (!map.getSource('dc-quakes')) {
|
|
||||||
console.log('Creating new source');
|
|
||||||
map.addSource('dc-quakes', { type: 'geojson', data: dcDataRef.current });
|
|
||||||
}
|
|
||||||
// Add layers if they don't exist
|
|
||||||
if (!map.getLayer('dc-heat')) {
|
|
||||||
map.addLayer({
|
|
||||||
id: 'dc-heat', type: 'heatmap', source: 'dc-quakes',
|
|
||||||
paint: {
|
|
||||||
'heatmap-weight': ['interpolate', ['linear'], ['get', 'mag'], 0, 0, 6, 1],
|
|
||||||
'heatmap-intensity': heatIntensity,
|
|
||||||
'heatmap-color': [
|
|
||||||
'interpolate',
|
|
||||||
['linear'],
|
|
||||||
['heatmap-density'],
|
|
||||||
0, 'rgba(0,0,0,0)',
|
|
||||||
0.2, 'rgba(255,255,0,0.7)',
|
|
||||||
0.4, 'rgba(255,165,0,0.8)',
|
|
||||||
0.6, 'rgba(255,69,0,0.9)',
|
|
||||||
0.8, 'rgba(255,0,0,0.95)',
|
|
||||||
1, 'rgba(139,0,0,1)'
|
|
||||||
],
|
|
||||||
'heatmap-radius': heatRadius,
|
|
||||||
'heatmap-opacity': ['interpolate', ['linear'], ['zoom'], 7, 1, 12, 0.8]
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (!map.getLayer('dc-point')) {
|
|
||||||
map.addLayer({
|
|
||||||
id: 'dc-point', type: 'circle', source: 'dc-quakes', minzoom: 12,
|
|
||||||
paint: {
|
|
||||||
'circle-radius': ['interpolate', ['linear'], ['get', 'mag'], 1, 3, 6, 10],
|
|
||||||
'circle-color': [
|
|
||||||
'interpolate',
|
|
||||||
['linear'],
|
|
||||||
['get', 'mag'],
|
|
||||||
1, styleChoiceRef.current === 'dark' ? '#ffff99' : '#ffa500',
|
|
||||||
3, styleChoiceRef.current === 'dark' ? '#ff6666' : '#ff4500',
|
|
||||||
6, styleChoiceRef.current === 'dark' ? '#ff0000' : '#8b0000'
|
|
||||||
] as any,
|
|
||||||
'circle-opacity': ['interpolate', ['linear'], ['zoom'], 12, 0.7, 14, 0.9],
|
|
||||||
'circle-stroke-width': 1,
|
|
||||||
'circle-stroke-color': styleChoiceRef.current === 'dark' ? '#ffffff' : '#000000'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
// Update layer visibility
|
|
||||||
if (map.getLayer('dc-heat')) {
|
|
||||||
map.setLayoutProperty('dc-heat', 'visibility', heatVisible ? 'visible' : 'none');
|
|
||||||
}
|
|
||||||
if (map.getLayer('dc-point')) {
|
|
||||||
map.setLayoutProperty('dc-point', 'visibility', pointsVisible ? 'visible' : 'none');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
processData().catch(console.error);
|
||||||
}
|
}
|
||||||
} else {
|
}, [crashData, crashDataHook, useRealCrashData, useAIMagnitudes]);
|
||||||
console.log('Map style not loaded yet');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [useRealCrashData, crashDataHook?.data, crashData, heatRadius, heatIntensity, heatVisible, pointsVisible]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const el = containerRef.current;
|
const el = containerRef.current;
|
||||||
@@ -236,16 +199,35 @@ export default function MapView({
|
|||||||
const currentCrashDataHook = crashDataHook || internalCrashDataHook;
|
const currentCrashDataHook = crashDataHook || internalCrashDataHook;
|
||||||
const activeData = crashData.length > 0 ? crashData : currentCrashDataHook.data;
|
const activeData = crashData.length > 0 ? crashData : currentCrashDataHook.data;
|
||||||
console.log('Initializing map data, activeData length:', activeData.length);
|
console.log('Initializing map data, activeData length:', activeData.length);
|
||||||
|
|
||||||
|
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);
|
||||||
|
} else {
|
||||||
dcDataRef.current = convertCrashDataToGeoJSON(activeData);
|
dcDataRef.current = convertCrashDataToGeoJSON(activeData);
|
||||||
|
}
|
||||||
} else if (!useRealCrashData) {
|
} else if (!useRealCrashData) {
|
||||||
console.log('Using synthetic data');
|
console.log('Using synthetic data');
|
||||||
|
if (useAIMagnitudes) {
|
||||||
|
setIsLoadingAIPredictions(true);
|
||||||
|
console.log('🤖 Using AI-enhanced synthetic data...');
|
||||||
|
dcDataRef.current = await generateDCPointsWithAI(900);
|
||||||
|
setIsLoadingAIPredictions(false);
|
||||||
|
} else {
|
||||||
dcDataRef.current = generateDCPoints(900);
|
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: [] };
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
initializeData().catch(console.error);
|
||||||
|
|
||||||
const computeNearbyStats = async (center: [number, number], radiusMeters = 300) => {
|
const computeNearbyStats = async (center: [number, number], radiusMeters = 300) => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
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
|
||||||
@@ -17,6 +18,8 @@ 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;
|
||||||
@@ -36,6 +39,8 @@ export default function UnifiedControlPanel({
|
|||||||
onChangeIntensity,
|
onChangeIntensity,
|
||||||
gradientRoutes,
|
gradientRoutes,
|
||||||
onToggleGradientRoutes,
|
onToggleGradientRoutes,
|
||||||
|
useAIMagnitudes,
|
||||||
|
onToggleAIMagnitudes,
|
||||||
crashDataHook,
|
crashDataHook,
|
||||||
onDataLoaded
|
onDataLoaded
|
||||||
}: UnifiedControlPanelProps) {
|
}: UnifiedControlPanelProps) {
|
||||||
@@ -59,6 +64,7 @@ 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(() => {
|
||||||
@@ -77,7 +83,31 @@ export default function UnifiedControlPanel({
|
|||||||
}
|
}
|
||||||
|
|
||||||
setIsHydrated(true);
|
setIsHydrated(true);
|
||||||
}, []); // Crash data state
|
}, []);
|
||||||
|
|
||||||
|
// 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
|
||||||
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
|
||||||
const [selectedYear, setSelectedYear] = useState<string>('2024'); // Default value
|
const [selectedYear, setSelectedYear] = useState<string>('2024'); // Default value
|
||||||
@@ -236,6 +266,29 @@ 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,6 +1,7 @@
|
|||||||
import { CrashData } from '../api/crashes/route';
|
import { CrashData } from '../api/crashes/route';
|
||||||
|
import { getCachedCrashMagnitude, CrashMagnitudePrediction } from '../../lib/crashMagnitudeApi';
|
||||||
|
|
||||||
export type PointFeature = GeoJSON.Feature<GeoJSON.Point, { mag: number; crashData: CrashData }>;
|
export type PointFeature = GeoJSON.Feature<GeoJSON.Point, { mag: number; crashData: CrashData; aiPredicted?: boolean }>;
|
||||||
|
|
||||||
export const haversine = (a: [number, number], b: [number, number]) => {
|
export const haversine = (a: [number, number], b: [number, number]) => {
|
||||||
const toRad = (v: number) => v * Math.PI / 180;
|
const toRad = (v: number) => v * Math.PI / 180;
|
||||||
@@ -21,8 +22,8 @@ export const convertCrashDataToGeoJSON = (crashes: CrashData[]): GeoJSON.Feature
|
|||||||
console.log('Sample crash data:', crashes[0]);
|
console.log('Sample crash data:', crashes[0]);
|
||||||
|
|
||||||
const features: PointFeature[] = crashes.map((crash) => {
|
const features: PointFeature[] = crashes.map((crash) => {
|
||||||
// Calculate severity score based on fatalities and major injuries
|
// Calculate fallback severity score based on fatalities and major injuries
|
||||||
const severityScore = Math.max(1,
|
const fallbackSeverityScore = Math.max(1,
|
||||||
(crash.fatalDriver + crash.fatalPedestrian + crash.fatalBicyclist) * 3 +
|
(crash.fatalDriver + crash.fatalPedestrian + crash.fatalBicyclist) * 3 +
|
||||||
(crash.majorInjuriesDriver + crash.majorInjuriesPedestrian + crash.majorInjuriesBicyclist) * 2 +
|
(crash.majorInjuriesDriver + crash.majorInjuriesPedestrian + crash.majorInjuriesBicyclist) * 2 +
|
||||||
(crash.totalVehicles + crash.totalPedestrians + crash.totalBicycles)
|
(crash.totalVehicles + crash.totalPedestrians + crash.totalBicycles)
|
||||||
@@ -35,8 +36,9 @@ export const convertCrashDataToGeoJSON = (crashes: CrashData[]): GeoJSON.Feature
|
|||||||
coordinates: [crash.longitude, crash.latitude]
|
coordinates: [crash.longitude, crash.latitude]
|
||||||
},
|
},
|
||||||
properties: {
|
properties: {
|
||||||
mag: Math.min(6, severityScore), // Cap at 6 for consistent visualization
|
mag: Math.min(6, fallbackSeverityScore), // Cap at 6 for consistent visualization
|
||||||
crashData: crash
|
crashData: crash,
|
||||||
|
aiPredicted: false // Will be updated when AI prediction is available
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
@@ -52,6 +54,80 @@ export const convertCrashDataToGeoJSON = (crashes: CrashData[]): GeoJSON.Feature
|
|||||||
return geoJSON;
|
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) => {
|
export const generateDCPoints = (count = 500) => {
|
||||||
const center = { lon: -77.0369, lat: 38.9072 };
|
const center = { lon: -77.0369, lat: 38.9072 };
|
||||||
const features: PointFeature[] = [];
|
const features: PointFeature[] = [];
|
||||||
@@ -93,13 +169,124 @@ export const generateDCPoints = (count = 500) => {
|
|||||||
features.push({
|
features.push({
|
||||||
type: 'Feature',
|
type: 'Feature',
|
||||||
geometry: { type: 'Point', coordinates: [lon, lat] },
|
geometry: { type: 'Point', coordinates: [lon, lat] },
|
||||||
properties: { mag, crashData: syntheticCrash }
|
properties: { mag, crashData: syntheticCrash, aiPredicted: false }
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return { type: 'FeatureCollection', features } as GeoJSON.FeatureCollection<GeoJSON.Geometry>;
|
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
|
// Calculate crash density along a route path
|
||||||
export const calculateRouteCrashDensity = (
|
export const calculateRouteCrashDensity = (
|
||||||
routeCoordinates: [number, number][],
|
routeCoordinates: [number, number][],
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ 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);
|
||||||
@@ -66,6 +67,8 @@ 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}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -79,6 +82,7 @@ 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)); }}
|
||||||
/>
|
/>
|
||||||
|
|||||||
255
web/src/lib/crashMagnitudeApi.ts
Normal file
255
web/src/lib/crashMagnitudeApi.ts
Normal file
@@ -0,0 +1,255 @@
|
|||||||
|
/**
|
||||||
|
* 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
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user