MapUtil Update

This commit is contained in:
2025-09-28 07:31:58 -04:00
parent 148ead6d34
commit 8708ea2273
2 changed files with 186 additions and 3 deletions

View File

@@ -4,7 +4,7 @@ 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 { calculateRouteDensity } from '../../lib/mapUtils';
import { fetchSafeRoute, type SafeRouteData } from '../../lib/flaskApi'; import { fetchSafeRoute, type SafeRouteData } from '../../lib/flaskApi';
interface Props { interface Props {
@@ -315,8 +315,12 @@ export default function DirectionsSidebar({ mapRef, profile = "mapbox/driving",
// Apply crash density gradient to all routes if crash data is available and gradient routes enabled // Apply crash density gradient to all routes if crash data is available and gradient routes enabled
if (gradientRoutes && crashDataHook.data.length > 0) { if (gradientRoutes && crashDataHook.data.length > 0) {
const routeCoordinates = (route.geometry as any).coordinates as [number, number][]; const routeCoordinates = (route.geometry as any).coordinates as [number, number][];
const crashDensities = calculateRouteCrashDensity(routeCoordinates, crashDataHook.data, 150); const crashDensity = calculateRouteDensity(routeCoordinates, crashDataHook.data, 150);
const gradientStops = createRouteGradientStops(crashDensities); // For now, create a simple gradient based on the density value
const gradientStops = [
[0, crashDensity > 2 ? '#ff0000' : crashDensity > 1 ? '#ff6600' : '#00ff00'],
[1, crashDensity > 2 ? '#8b0000' : crashDensity > 1 ? '#ff4500' : '#006400']
];
map.setPaintProperty(layerId, 'line-gradient', gradientStops as [string, ...any[]]); map.setPaintProperty(layerId, 'line-gradient', gradientStops as [string, ...any[]]);
map.setPaintProperty(layerId, 'line-color', undefined); // Remove solid color when using gradient map.setPaintProperty(layerId, 'line-color', undefined); // Remove solid color when using gradient

179
web/src/lib/mapUtils.ts Normal file
View File

@@ -0,0 +1,179 @@
import { CrashData } from '../app/api/crashes/route';
export type PointFeature = GeoJSON.Feature<GeoJSON.Point, {
mag: number;
crashData?: CrashData;
aiPredicted?: boolean;
}>;
/**
* Calculate traditional magnitude based on crash severity factors
*/
export const calculateTraditionalMagnitude = (crash: CrashData): number => {
let magnitude = 0;
// Fatalities contribute heavily (3 points each)
const fatalities = (crash.fatalDriver || 0) + (crash.fatalPedestrian || 0) + (crash.fatalBicyclist || 0);
magnitude += fatalities * 3;
// Major injuries contribute significantly (2 points each)
const majorInjuries = (crash.majorInjuriesDriver || 0) + (crash.majorInjuriesPedestrian || 0) + (crash.majorInjuriesBicyclist || 0);
magnitude += majorInjuries * 2;
// Vehicle involvement (diminishing returns after first vehicle)
const vehicleCount = (crash.totalVehicles || 0) + (crash.totalPedestrians || 0) + (crash.totalBicycles || 0);
magnitude += Math.min(vehicleCount - 1, 3) * 0.5; // Cap vehicle contribution
// Speeding factor
if (crash.speedingInvolved && crash.speedingInvolved > 0) {
magnitude *= 1.2;
}
// Ensure minimum severity of 1, maximum of 10
return Math.max(1, Math.min(magnitude, 10));
};
/**
* Convert crash data to GeoJSON format with traditional calculations
*/
export const convertCrashDataToGeoJSON = (crashes: CrashData[]): GeoJSON.FeatureCollection => {
const features: PointFeature[] = crashes
.filter(crash =>
crash &&
typeof crash.latitude === 'number' &&
typeof crash.longitude === 'number' &&
!isNaN(crash.latitude) && !isNaN(crash.longitude) &&
crash.latitude !== 0 && crash.longitude !== 0
)
.map(crash => {
const magnitude = calculateTraditionalMagnitude(crash);
return {
type: 'Feature',
geometry: {
type: 'Point',
coordinates: [crash.longitude, crash.latitude]
},
properties: {
mag: Math.min(6, magnitude), // Cap at 6 for consistent visualization
crashData: crash,
aiPredicted: false
}
};
});
const geoJSON: GeoJSON.FeatureCollection = {
type: 'FeatureCollection' as const,
features
};
return geoJSON;
};
/**
* Haversine formula to calculate distance between two coordinates
*/
export const haversine = (a: [number, number], b: [number, 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 a_calc = sinDLat * sinDLat + Math.cos(lat1) * Math.cos(lat2) * sinDLon * sinDLon;
const c = 2 * Math.atan2(Math.sqrt(a_calc), Math.sqrt(1 - a_calc));
return R * c;
};
/**
* Generate synthetic crash data for testing/demo purposes
*/
export const createSyntheticCrashData = (
centerLat: number,
centerLng: number,
count: number,
radiusKm = 10
): CrashData[] => {
const crashes: CrashData[] = [];
for (let i = 0; i < count; i++) {
// Random point within radius
const angle = Math.random() * 2 * Math.PI;
const distance = Math.sqrt(Math.random()) * radiusKm / 111; // Rough conversion to degrees
const lat = centerLat + distance * Math.cos(angle);
const lng = centerLng + distance * Math.sin(angle);
// Random severity factors
const hasInjuries = Math.random() < 0.3;
const hasFatalities = Math.random() < 0.05;
const crash: CrashData = {
id: `synthetic-${i + 1}`,
reportDate: new Date(Date.now() - Math.random() * 365 * 24 * 60 * 60 * 1000).toISOString(),
latitude: lat,
longitude: lng,
address: `Sample Address ${i + 1}`,
ward: `Ward ${Math.floor(Math.random() * 8) + 1}`,
totalVehicles: Math.floor(Math.random() * 3) + 1,
totalPedestrians: Math.random() < 0.2 ? 1 : 0,
totalBicycles: Math.random() < 0.1 ? 1 : 0,
fatalDriver: hasFatalities ? Math.floor(Math.random() * 2) : 0,
fatalPedestrian: hasFatalities && Math.random() < 0.3 ? 1 : 0,
fatalBicyclist: hasFatalities && Math.random() < 0.1 ? 1 : 0,
majorInjuriesDriver: hasInjuries ? Math.floor(Math.random() * 2) : 0,
majorInjuriesPedestrian: hasInjuries && Math.random() < 0.3 ? 1 : 0,
majorInjuriesBicyclist: hasInjuries && Math.random() < 0.1 ? 1 : 0,
speedingInvolved: Math.random() < 0.25 ? 1 : 0
};
crashes.push(crash);
}
return crashes;
};
/**
* Convert synthetic crash data to GeoJSON format
*/
export const convertSyntheticDataToGeoJSON = (
centerLat: number,
centerLng: number,
count: number
): GeoJSON.FeatureCollection => {
const syntheticCrashes = createSyntheticCrashData(centerLat, centerLng, count);
return convertCrashDataToGeoJSON(syntheticCrashes);
};
/**
* Calculate crash density for route analysis
*/
export const calculateRouteDensity = (
routeCoordinates: [number, number][],
crashes: CrashData[],
bufferMeters = 100
): number => {
let totalCrashes = 0;
const routeLength = routeCoordinates.length;
for (let i = 0; i < routeLength - 1; i++) {
const segmentStart = routeCoordinates[i];
const segmentEnd = routeCoordinates[i + 1];
// Count crashes near this segment
const segmentCrashes = crashes.filter(crash => {
const crashPoint: [number, number] = [crash.longitude, crash.latitude];
const distanceToSegment = Math.min(
haversine(crashPoint, segmentStart),
haversine(crashPoint, segmentEnd)
);
return distanceToSegment <= bufferMeters;
});
totalCrashes += segmentCrashes.length;
}
return totalCrashes / Math.max(1, routeLength);
};