From 5523497e1903a57de87720e51b8671f47a2c5b30 Mon Sep 17 00:00:00 2001 From: GamerBoss101 Date: Sun, 28 Sep 2025 07:42:45 -0400 Subject: [PATCH] Gradient Routing --- web/src/app/components/DirectionsSidebar.tsx | 18 ++--- web/src/lib/mapUtils.ts | 78 ++++++++++++++++++++ 2 files changed, 86 insertions(+), 10 deletions(-) diff --git a/web/src/app/components/DirectionsSidebar.tsx b/web/src/app/components/DirectionsSidebar.tsx index b675389..4cb4cbd 100644 --- a/web/src/app/components/DirectionsSidebar.tsx +++ b/web/src/app/components/DirectionsSidebar.tsx @@ -4,7 +4,7 @@ import React, { useEffect, useRef, useState } from "react"; import mapboxgl from "mapbox-gl"; import GeocodeInput from './GeocodeInput'; import { useCrashData } from '../hooks/useCrashData'; -import { calculateRouteDensity } from '../../lib/mapUtils'; +import { calculateRouteDensity, calculateRouteSegmentDensities, createRouteGradientStops } from '../../lib/mapUtils'; import { fetchSafeRoute, type SafeRouteData } from '../../lib/flaskApi'; interface Props { @@ -315,14 +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 if (gradientRoutes && crashDataHook.data.length > 0) { const routeCoordinates = (route.geometry as any).coordinates as [number, number][]; - const crashDensity = calculateRouteDensity(routeCoordinates, crashDataHook.data, 150); - // 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'] - ]; + const segmentDensities = calculateRouteSegmentDensities(routeCoordinates, crashDataHook.data, 150); + const gradientExpression = createRouteGradientStops(segmentDensities); - map.setPaintProperty(layerId, 'line-gradient', gradientStops as [string, ...any[]]); + console.log('🎨 Applying gradient to route:', layerId, 'segments:', segmentDensities.length, 'expression:', gradientExpression); + + map.setPaintProperty(layerId, 'line-gradient', gradientExpression); map.setPaintProperty(layerId, 'line-color', undefined); // Remove solid color when using gradient map.setPaintProperty(layerId, 'line-width', isSelected ? routeWidths[0] : routeWidths[1]); map.setPaintProperty(layerId, 'line-opacity', isSelected ? routeOpacities[0] : routeOpacities[1]); @@ -435,7 +433,7 @@ export default function DirectionsSidebar({ mapRef, profile = "mapbox/driving", // Add alternate route source and layer if (!map.getSource("alternate-route")) { - map.addSource("alternate-route", { type: "geojson", data: alternateGeo }); + map.addSource("alternate-route", { type: "geojson", data: alternateGeo, lineMetrics: true }); } else { (map.getSource("alternate-route") as mapboxgl.GeoJSONSource).setData(alternateGeo); } @@ -569,7 +567,7 @@ export default function DirectionsSidebar({ mapRef, profile = "mapbox/driving", // Add safe route to map if (!map.getSource("safe-route")) { - map.addSource("safe-route", { type: "geojson", data: safeRouteGeo }); + map.addSource("safe-route", { type: "geojson", data: safeRouteGeo, lineMetrics: true }); } else { (map.getSource("safe-route") as mapboxgl.GeoJSONSource).setData(safeRouteGeo); } diff --git a/web/src/lib/mapUtils.ts b/web/src/lib/mapUtils.ts index 4850f07..ad34510 100644 --- a/web/src/lib/mapUtils.ts +++ b/web/src/lib/mapUtils.ts @@ -176,4 +176,82 @@ export const calculateRouteDensity = ( } return totalCrashes / Math.max(1, routeLength); +}; + +/** + * Calculate crash density for each segment of a route (for gradient visualization) + */ +export const calculateRouteSegmentDensities = ( + routeCoordinates: [number, number][], + crashes: CrashData[], + bufferMeters = 100 +): number[] => { + const densities: number[] = []; + const routeLength = routeCoordinates.length; + + if (routeLength < 2) return []; + + 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; + }); + + densities.push(segmentCrashes.length); + } + + return densities; +}; + +/** + * Create Mapbox gradient stops from route segment densities + */ +export const createRouteGradientStops = (segmentDensities: number[]): any => { + if (segmentDensities.length === 0) { + return [ + 'interpolate', + ['linear'], + ['line-progress'], + 0, '#00ff00', + 1, '#00ff00' + ]; // Default green if no data + } + + const maxDensity = Math.max(...segmentDensities, 1); // Ensure at least 1 to avoid division by zero + const expression: any[] = ['interpolate', ['linear'], ['line-progress']]; + + segmentDensities.forEach((density, index) => { + const position = index / Math.max(1, segmentDensities.length - 1); // Position along route (0 to 1) + + // Color based on density relative to max density + const intensity = density / maxDensity; + let color: string; + + if (intensity > 0.7) { + color = '#ff0000'; // High density: red + } else if (intensity > 0.4) { + color = '#ff6600'; // Medium density: orange + } else if (intensity > 0.2) { + color = '#ffff00'; // Low-medium density: yellow + } else { + color = '#00ff00'; // Low density: green + } + + expression.push(position, color); + }); + + // Ensure we have at least start and end + if (segmentDensities.length === 1) { + expression.push(1, expression[expression.length - 1]); // Duplicate last color for end + } + + return expression; }; \ No newline at end of file