feat(testing): improve pathfinding

This commit is contained in:
doprz
2024-10-23 17:24:51 -05:00
parent c79172dec5
commit 87998cedbf
2 changed files with 697 additions and 322 deletions

View File

@@ -1,6 +1,6 @@
import React, { useState } from 'react'; import React, { useCallback, useMemo, useState } from 'react';
import { generateAllBuildingPaths, graphNodes, PathSegment } from './mapUtils'; import { connectionConfig, findShortestPath, generateConnections, PathSegment, rawGraphNodes } from './mapUtils';
const UTMapURL = new URL('/src/assets/UT-Map.png', import.meta.url).href; const UTMapURL = new URL('/src/assets/UT-Map.png', import.meta.url).href;
@@ -13,7 +13,40 @@ const UTMapURL = new URL('/src/assets/UT-Map.png', import.meta.url).href;
* - Building selection controls to highlight specific paths. * - Building selection controls to highlight specific paths.
*/ */
export default function CampusMap() { export default function CampusMap() {
const [highlightedPath, setHighlightedPath] = useState<string[]>([]); const [selectedPath, setSelectedPath] = useState<string[]>([]);
const [startBuilding, setStartBuilding] = useState<string>('');
const [endBuilding, setEndBuilding] = useState<string>('');
const [allowThroughBuildings, setAllowThroughBuildings] = useState(false);
const [debugInfo, setDebugInfo] = useState<string>('');
// Generate graph with connections once using useMemo
const graphNodes = useMemo(() => generateConnections(rawGraphNodes, connectionConfig), []);
// Function to draw path between buildings
const drawPathBetweenBuildings = useCallback(
(start: string, end: string) => {
if (!start || !end || start === end) {
setSelectedPath([]);
setDebugInfo('No path to draw - invalid start/end');
return;
}
try {
const result = findShortestPath(start, end, graphNodes, allowThroughBuildings);
setDebugInfo(`Found path: ${result.path.join(' → ')} (distance: ${result.distance.toFixed(2)})`);
setSelectedPath(result.path);
} catch (error) {
setDebugInfo(`Error finding path: ${error instanceof Error ? error.message : 'Unknown error'}`);
setSelectedPath([]);
}
},
[allowThroughBuildings, graphNodes]
);
// Update path when selections change
React.useEffect(() => {
drawPathBetweenBuildings(startBuilding, endBuilding);
}, [startBuilding, endBuilding, allowThroughBuildings, drawPathBetweenBuildings]);
return ( return (
<div className='relative h-full w-full'> <div className='relative h-full w-full'>
@@ -22,19 +55,34 @@ export default function CampusMap() {
{/* SVG Overlay */} {/* SVG Overlay */}
<svg className='absolute left-0 top-0 h-full w-full' viewBox='0 0 784 754' preserveAspectRatio='none'> <svg className='absolute left-0 top-0 h-full w-full' viewBox='0 0 784 754' preserveAspectRatio='none'>
{/* Draw all building-to-building paths */} {/* Debug visualization of all connections */}
{generateAllBuildingPaths().map(path => ( {Object.entries(graphNodes).map(([nodeId, node]) =>
<g key={path.id}> node.connections.map(connectionId => (
{path.points.slice(0, -1).map((startNode, index) => ( <line
key={`${nodeId}-${connectionId}`}
x1={node.x}
y1={node.y}
x2={graphNodes[connectionId]?.x || 0}
y2={graphNodes[connectionId]?.y || 0}
stroke='#dddddd'
strokeWidth='1'
strokeDasharray='4,4'
className='opacity-30'
/>
))
)}
{/* Draw selected path */}
{selectedPath.length > 1 &&
selectedPath
.slice(0, -1)
.map((nodeId, index) => (
<PathSegment <PathSegment
key={`${startNode}-${path.points[index + 1]}`} key={`path-${nodeId}-${selectedPath[index + 1]}`}
start={startNode} start={nodeId}
end={path.points[index + 1] || ''} end={selectedPath[index + 1] || ''}
isHighlighted={highlightedPath.includes(path.id)}
/> />
))} ))}
</g>
))}
{/* Draw nodes */} {/* Draw nodes */}
{Object.entries(graphNodes).map(([id, node]) => ( {Object.entries(graphNodes).map(([id, node]) => (
@@ -43,13 +91,15 @@ export default function CampusMap() {
cx={node.x} cx={node.x}
cy={node.y} cy={node.y}
r={node.type === 'building' ? 6 : 4} r={node.type === 'building' ? 6 : 4}
fill={node.type === 'building' ? '#BF5700' : '#666666'} fill={
// eslint-disable-next-line no-nested-ternary
selectedPath.includes(id) ? '#FF8C00' : node.type === 'building' ? '#BF5700' : '#666666'
}
stroke='white' stroke='white'
strokeWidth='2' strokeWidth='2'
className='opacity-90' className='opacity-90'
/> />
{/* Only label buildings */}
{node.type === 'building' && ( {node.type === 'building' && (
<text x={node.x + 12} y={node.y + 4} fill='#000000' fontSize='14' className='font-bold'> <text x={node.x + 12} y={node.y + 4} fill='#000000' fontSize='14' className='font-bold'>
{id} {id}
@@ -59,34 +109,66 @@ export default function CampusMap() {
))} ))}
</svg> </svg>
{/* Building Selection Controls */} {/* Controls */}
<div className='absolute right-8 top-8 h-full h-full rounded-md bg-white/90 p-3 shadow-sm space-y-4'> <div className='absolute right-4 top-4 max-w-xs rounded-md bg-white/90 p-4 shadow-sm space-y-4'>
<div className='text-sm space-y-2'> <div className='space-y-4'>
<div className='flex items-center gap-2'> <div className='space-y-2'>
<div className='h-3 w-3 rounded-full bg-[#BF5700]' /> <label className='block text-sm font-medium'>Start Building:</label>
<span>Buildings</span> <select
</div> value={startBuilding}
<div className='flex items-center gap-2'> onChange={e => setStartBuilding(e.target.value)}
<div className='h-3 w-3 rounded-full bg-[#666666]' /> className='w-full border border-gray-300 rounded-md p-1'
<span>Path Intersections</span>
</div>
<div className='flex items-center gap-2'>
<div className='h-1 w-6 bg-[#BF5700]' />
<span>Walking Paths</span>
</div>
</div>
<div className='overflow-y-scroll space-y-2'>
<p className='text-sm font-medium'>Building Paths:</p>
{generateAllBuildingPaths().map(path => (
<button
key={path.id}
onClick={() => setHighlightedPath([path.id])}
className='block text-sm text-gray-600 hover:text-gray-900'
> >
{path.points[0]} {path.points[path.points.length - 1]} <option value=''>Select Building</option>
</button> {Object.entries(graphNodes)
))} .filter(([_, node]) => node.type === 'building')
.map(([id]) => (
<option key={id} value={id}>
{id}
</option>
))}
</select>
</div>
<div className='space-y-2'>
<label className='block text-sm font-medium'>End Building:</label>
<select
value={endBuilding}
onChange={e => setEndBuilding(e.target.value)}
className='w-full border border-gray-300 rounded-md p-1'
>
<option value=''>Select Building</option>
{Object.entries(graphNodes)
.filter(([_, node]) => node.type === 'building')
.map(([id]) => (
<option key={id} value={id}>
{id}
</option>
))}
</select>
</div>
<div className='flex items-center gap-2'>
<input
type='checkbox'
id='allowBuildings'
checked={allowThroughBuildings}
onChange={e => setAllowThroughBuildings(e.target.checked)}
className='rounded'
/>
<label htmlFor='allowBuildings' className='text-sm'>
Allow paths through buildings
</label>
</div>
{/* Debug info */}
{debugInfo && (
<div className='rounded bg-gray-100 p-2 text-xs'>
<pre className='whitespace-pre-wrap'>{debugInfo}</pre>
</div>
)}
{selectedPath.length > 0 && <div className='text-sm'>Path: {selectedPath.join(' → ')}</div>}
</div> </div>
</div> </div>
</div> </div>

View File

@@ -1,110 +1,163 @@
import type { MainCampusBuildingsCode } from '@shared/types/MainCampusBuildings'; import React, { memo } from 'react';
import React from 'react';
// Type definitions // Type definitions
type NodeType = 'building' | 'intersection'; type NodeType = 'building' | 'intersection';
interface Node { type Node = {
x: number; x: number;
y: number; y: number;
// connections: string[]; connections: string[];
type: NodeType; type: NodeType;
}
interface GraphNodes {
[key: string]: Node;
}
interface Path {
id: string;
points: string[]; // Array of node IDs representing the path
}
// Helper functions for path finding
export const findNearestIntersection = (buildingId: string, graph: GraphNodes): string => {
const building = graph[buildingId];
let nearestIntersection = '';
let shortestDistance = Infinity;
Object.entries(graph).forEach(([nodeId, node]) => {
if (node.type === 'intersection' && building) {
const distance = Math.sqrt((node.x - building.x) ** 2 + (node.y - building.y) ** 2);
if (distance < shortestDistance) {
shortestDistance = distance;
nearestIntersection = nodeId;
}
}
});
return nearestIntersection;
}; };
// Get all buildings from the graph type BaseNode = {
export const getAllBuildings = (graph: GraphNodes): string[] => x: number;
Object.entries(graph) y: number;
.filter(([_key, node]) => node.type === 'building') type: NodeType;
.map(([id]) => id); };
// Generate all possible building-to-building paths // Extended type for nodes with connections (after graph generation)
export const generateAllBuildingPaths = (): Path[] => { type ConnectedNode = BaseNode & {
const buildings = getAllBuildings(graphNodes); connections: string[];
const paths: Path[] = []; };
for (let i = 0; i < buildings.length; i++) { // Graph types
for (let j = i + 1; j < buildings.length; j++) { type RawNodes = {
const building1 = buildings[i]; [key: string]: BaseNode;
const building2 = buildings[j]; };
if (building1 && building2) { type GraphNodes = {
// Get nearest intersections for both buildings [key: string]: ConnectedNode;
const int1 = findNearestIntersection(building1, graphNodes); };
const int2 = findNearestIntersection(building2, graphNodes);
paths.push({ type PathfindingResult = {
id: `${building1}-${building2}`, path: string[];
points: [building1, int1, int2, building2], distance: number;
}); };
}
/**
* Configuration options for automatic connection generation
*/
type ConnectionConfig = {
// Maximum distance for direct connections between intersections
maxIntersectionDistance: number;
// Maximum distance for building to nearest intersection connections
maxBuildingToIntersectionDistance: number;
// Maximum deviation from straight line to consider nodes aligned
alignmentTolerance: number;
};
export const connectionConfig = {
maxIntersectionDistance: 100, // Adjust based on your map scale
maxBuildingToIntersectionDistance: 50, // Adjust based on your map scale
alignmentTolerance: 10, // Adjust based on needed precision
} as const satisfies ConnectionConfig;
/**
* Utility functions for graph building
*/
const GraphUtils = {
calculateDistance(point1: Pick<BaseNode, 'x' | 'y'>, point2: Pick<BaseNode, 'x' | 'y'>): number {
return Math.sqrt((point2.x - point1.x) ** 2 + (point2.y - point2.y) ** 2);
},
findNearestIntersections(buildingId: string, nodes: RawNodes, maxDistance: number): string[] {
const building = nodes[buildingId];
if (!building) {
throw new Error(`Building ${buildingId} not found in graph`);
}
return Object.entries(nodes)
.filter(([_, node]) => node.type === 'intersection')
.map(([id, node]) => ({
id,
distance: this.calculateDistance(building, node),
}))
.filter(({ distance }) => distance <= maxDistance)
.sort((a, b) => a.distance - b.distance)
.map(i => i.id);
},
};
// Function to generate connections
export function generateConnections(rawNodes: RawNodes, config: ConnectionConfig): GraphNodes {
const nodes: GraphNodes = {};
// Initialize nodes with empty connections
for (const [id, node] of Object.entries(rawNodes)) {
nodes[id] = {
...node,
connections: [],
};
}
// Generate connections
for (const [id, node] of Object.entries(nodes)) {
if (node.type === 'building') {
// Connect buildings to nearest intersections
const nearestIntersections = GraphUtils.findNearestIntersections(
id,
rawNodes,
config.maxBuildingToIntersectionDistance
);
node.connections = nearestIntersections;
// Add bidirectional connections
nearestIntersections.forEach(intersectionId => {
if (nodes[intersectionId] && !nodes[intersectionId].connections.includes(id)) {
nodes[intersectionId].connections.push(id);
}
});
} else {
// Connect intersections to other nearby intersections
const otherIntersections = Object.entries(rawNodes)
.filter(
([otherId, otherNode]) =>
otherId !== id &&
otherNode.type === 'intersection' &&
GraphUtils.calculateDistance(node, otherNode) <= config.maxIntersectionDistance
)
.map(([otherId]) => otherId);
node.connections = otherIntersections;
// Add bidirectional connections
otherIntersections.forEach(otherId => {
if (nodes[otherId] && !nodes[otherId].connections.includes(id)) {
nodes[otherId].connections.push(id);
}
});
} }
} }
return paths; return nodes;
}; }
// Draw a single path segment /**
export const PathSegment = ({ * Explanation of the connections property:
start, *
end, * The connections property in the Node interface represents the graph edges in our map.
isHighlighted, * It's essential for pathfinding because:
}: { *
start: string; * 1. It defines which nodes are directly connected to each other
end: string; * 2. It represents actual walkable paths on campus
isHighlighted?: boolean; * 3. It constrains the possible routes for the pathfinding algorithm
}): JSX.Element | null => { *
const startNode = graphNodes[start]; * How to determine connections:
const endNode = graphNodes[end]; *
* 1. For intersections:
if (!startNode || !endNode) return null; * - Look at the actual walkways on the map
* - Connect to adjacent intersections that have direct paths
if (!isHighlighted) return null; * - Connect to buildings that have entrances on that walkway
*
return ( * 2. For buildings:
<line * - Connect to the nearest intersections that have direct walkway access
x1={startNode.x} * - Connect to adjacent buildings only if there's a direct walkway
y1={startNode.y} *
x2={endNode.x} */
y2={endNode.y} export const rawGraphNodes: RawNodes = {
// stroke={isHighlighted ? '#FF8C00' : '#BF5700'}
stroke={isHighlighted ? '#000' : '#BF5700'} // TODO
// strokeWidth={isHighlighted ? '4' : '2'}
strokeWidth={isHighlighted ? '10' : '2'}
strokeLinecap='round'
className={`opacity-60 ${isHighlighted ? 'z-10000' : ''}`}
/>
);
};
export const graphNodes: GraphNodes = {
// Building nodes // Building nodes
GDC: { GDC: {
x: 257, x: 257,
@@ -121,91 +174,91 @@ export const graphNodes: GraphNodes = {
y: 420, y: 420,
type: 'building', type: 'building',
}, },
GRE: { // GRE: {
x: 260, // x: 260,
y: 375, // y: 375,
type: 'building', // type: 'building',
}, // },
MAI: { // MAI: {
x: 167, // x: 167,
y: 310, // y: 310,
type: 'building', // type: 'building',
}, // },
WEL: { // WEL: {
x: 216, // x: 216,
y: 268, // y: 268,
type: 'building', // type: 'building',
}, // },
BEL: { // BEL: {
x: 365, // x: 365,
y: 377, // y: 377,
type: 'building', // type: 'building',
}, // },
WCP: { // WCP: {
x: 260, // x: 260,
y: 343, // y: 343,
type: 'building', // type: 'building',
}, // },
RLP: { // RLP: {
x: 300, // x: 300,
y: 335, // y: 335,
type: 'building', // type: 'building',
}, // },
UTC: { // UTC: {
x: 197, // x: 197,
y: 410, // y: 410,
type: 'building', // type: 'building',
}, // },
CBA: { // CBA: {
x: 232, // x: 232,
y: 363, // y: 363,
type: 'building', // type: 'building',
}, // },
GSB: { // GSB: {
x: 208, // x: 208,
y: 382, // y: 382,
type: 'building', // type: 'building',
}, // },
PMA: { // PMA: {
x: 255, // x: 255,
y: 185, // y: 185,
type: 'building', // type: 'building',
}, // },
PAT: { // PAT: {
x: 258, // x: 258,
y: 222, // y: 222,
type: 'building', // type: 'building',
}, // },
EER: { // EER: {
x: 289, // x: 289,
y: 208, // y: 208,
type: 'building', // type: 'building',
}, // },
ECJ: { // ECJ: {
x: 289, // x: 289,
y: 280, // y: 280,
type: 'building', // type: 'building',
}, // },
UNB: { // UNB: {
x: 105, // x: 105,
y: 288, // y: 288,
type: 'building', // type: 'building',
}, // },
FAC: { // FAC: {
x: 133, // x: 133,
y: 298, // y: 298,
type: 'building', // type: 'building',
}, // },
HRC: { // HRC: {
x: 112, // x: 112,
y: 380, // y: 380,
type: 'building', // type: 'building',
}, // },
COM: { // COM: {
x: 195, // x: 195,
y: 318, // y: 318,
type: 'building', // type: 'building',
}, // },
// Intersection nodes // Intersection nodes
'speedway-24th': { 'speedway-24th': {
@@ -218,114 +271,354 @@ export const graphNodes: GraphNodes = {
y: 400, y: 400,
type: 'intersection', type: 'intersection',
}, },
'speedway-e-mai-stairs': { // 'speedway-e-mai-stairs': {
x: 241, // x: 241,
y: 315, // y: 315,
type: 'intersection', // type: 'intersection',
}, // },
'speedway-w-eer': { // 'speedway-w-eer': {
x: 241, // x: 241,
y: 208, // y: 208,
type: 'intersection', // type: 'intersection',
}, // },
'guad-24th': { // 'guad-24th': {
x: 89, // x: 89,
y: 250, // y: 250,
type: 'intersection', // type: 'intersection',
}, // },
'guad-21st': { // 'guad-21st': {
x: 89, // x: 89,
y: 400, // y: 400,
type: 'intersection', // type: 'intersection',
}, // },
'guad-icd': { // 'guad-icd': {
x: 89, // x: 89,
y: 353, // y: 353,
type: 'intersection', // type: 'intersection',
}, // },
'uni-ave-21st': { // 'uni-ave-21st': {
x: 166, // x: 166,
y: 400, // y: 400,
type: 'intersection', // type: 'intersection',
}, // },
'wichita-21st': { // 'wichita-21st': {
x: 187, // x: 187,
y: 400, // y: 400,
type: 'intersection', // type: 'intersection',
}, // },
'n-mai-24th': { // 'n-mai-24th': {
x: 167, // x: 167,
y: 250, // y: 250,
type: 'intersection', // type: 'intersection',
}, // },
's-mai-stairs': { // 's-mai-stairs': {
x: 167, // x: 167,
y: 347, // y: 347,
type: 'intersection', // type: 'intersection',
}, // },
'e-mai-stairs': { // 'e-mai-stairs': {
x: 215, // x: 215,
y: 315, // y: 315,
type: 'intersection', // type: 'intersection',
}, // },
'guad-w-mai': { // 'guad-w-mai': {
x: 89, // x: 89,
y: 317, // y: 317,
type: 'intersection', // type: 'intersection',
}, // },
'n-mai-turtle-pond': { // 'n-mai-turtle-pond': {
x: 167, // x: 167,
y: 282, // y: 282,
type: 'intersection', // type: 'intersection',
}, // },
'icd-ne': { // 'icd-ne': {
x: 207, // x: 207,
y: 289, // y: 289,
type: 'intersection', // type: 'intersection',
}, // },
'icd-nne': { // 'icd-nne': {
x: 190, // x: 190,
y: 282, // y: 282,
type: 'intersection', // type: 'intersection',
}, // },
'icd-se': { // 'icd-se': {
x: 212, // x: 212,
y: 338, // y: 338,
type: 'intersection', // type: 'intersection',
}, // },
'icd-sse': { // 'icd-sse': {
x: 190, // x: 190,
y: 347, // y: 347,
type: 'intersection', // type: 'intersection',
}, // },
'san-jac-21th': { // 'san-jac-21th': {
x: 354, // x: 354,
y: 400, // y: 400,
type: 'intersection', // type: 'intersection',
}, // },
'san-jac-24th': { // 'san-jac-24th': {
x: 357, // x: 357,
y: 250, // y: 250,
type: 'intersection', // type: 'intersection',
}, // },
'san-jac-23rd': { // 'san-jac-23rd': {
x: 358, // x: 358,
y: 318, // y: 318,
type: 'intersection', // type: 'intersection',
}, // },
'mlk-jr-statue': { // 'mlk-jr-statue': {
x: 280, // x: 280,
y: 318, // y: 318,
type: 'intersection', // type: 'intersection',
}, // },
'pcl-nw-21st-walkway': { // 'pcl-nw-21st-walkway': {
x: 208, // x: 208,
y: 400, // y: 400,
type: 'intersection', // type: 'intersection',
}, // },
'pcl-w-speedway': { 'pcl-w-speedway': {
x: 241, x: 241,
y: 425, y: 425,
type: 'intersection', type: 'intersection',
}, },
}; };
// Type guard to ensure node exists
function assertNodeExists(node: Node | undefined, id: string): asserts node is Node {
if (!node) throw new Error(`Node ${id} not found in graph`);
}
/**
* Calculates distance between two points
*/
const calculateDistance = (point1: Pick<Node, 'x' | 'y'>, point2: Pick<Node, 'x' | 'y'>): number =>
Math.sqrt((point2.x - point1.x) ** 2 + (point2.y - point1.y) ** 2);
/**
* Implements Dijkstra's algorithm for finding shortest path between two nodes
*/
export function findShortestPath(
start: string,
end: string,
graph: GraphNodes,
allowThroughBuildings: boolean = false
): PathfindingResult {
// Initialize data structures
const distances = new Map<string, number>();
const previous = new Map<string, string>();
const unvisited = new Set<string>();
// Setup initial distances
Object.keys(graph).forEach(nodeId => {
// Only add nodes that are valid for pathfinding
if (allowThroughBuildings || graph[nodeId]?.type === 'intersection' || nodeId === start || nodeId === end) {
distances.set(nodeId, Infinity);
unvisited.add(nodeId);
}
});
// Set start distance to 0
distances.set(start, 0);
while (unvisited.size > 0) {
// Find unvisited node with smallest distance
let current: string | null = null;
let smallestDistance = Infinity;
unvisited.forEach(nodeId => {
const distance = distances.get(nodeId) ?? Infinity;
if (distance < smallestDistance) {
smallestDistance = distance;
current = nodeId;
}
});
if (!current || current === end) break;
if (smallestDistance === Infinity) break;
unvisited.delete(current);
const currentNode = graph[current];
if (!currentNode) {
throw new Error(`Node ${current} not found in graph`);
}
// Process each neighbor
currentNode.connections.forEach(neighborId => {
const neighbor = graph[neighborId];
if (!neighbor) {
throw new Error(`Node ${neighborId} not found in graph`);
}
// Skip if this is a building we can't path through
if (!allowThroughBuildings && neighbor.type === 'building' && neighborId !== end) {
return;
}
// Calculate distance to neighbor
const distance = Math.sqrt((neighbor.x - currentNode.x) ** 2 + (neighbor.y - currentNode.y) ** 2);
const totalDistance = (distances.get(current!) ?? 0) + distance;
if (totalDistance < (distances.get(neighborId) ?? Infinity)) {
distances.set(neighborId, totalDistance);
if (current) {
previous.set(neighborId, current);
}
}
});
}
// Reconstruct path
const path: string[] = [];
let current: string | undefined = end;
while (current !== undefined && current !== start) {
path.unshift(current);
current = previous.get(current);
}
if (current === start) {
path.unshift(start);
}
return {
path,
distance: distances.get(end) ?? Infinity,
};
}
// Get all buildings for the selection dropdowns
export const buildingOptions = Object.entries(rawGraphNodes)
.filter(([_, node]) => node.type === 'building')
.map(([id]) => id)
.sort();
// Draw a path segment
// export const PathSegment = ({ start, end }: { start: string; end: string }): JSX.Element | null => {
// const startNode = graphNodes[start];
// const endNode = graphNodes[end];
// if (!startNode || !endNode) return null;
// return (
// <line
// x1={startNode.x}
// y1={startNode.y}
// x2={endNode.x}
// y2={endNode.y}
// stroke='#FF8C00'
// strokeWidth='4'
// strokeLinecap='round'
// className='opacity-80'
// />
// );
// };
export const PathSegment = memo(({ start, end }: { start: string; end: string }): JSX.Element | null => {
const startNode = rawGraphNodes[start];
const endNode = rawGraphNodes[end];
if (!startNode || !endNode) {
console.warn(`Missing node data for path segment ${start} -> ${end}`);
return null;
}
return (
<line
x1={startNode.x}
y1={startNode.y}
x2={endNode.x}
y2={endNode.y}
stroke='#FF8C00'
strokeWidth='4'
strokeLinecap='round'
className='opacity-80'
/>
);
});
// // Helper functions for path finding
// export const findNearestIntersection = (buildingId: string, graph: GraphNodes): string => {
// const building = graph[buildingId];
// let nearestIntersection = '';
// let shortestDistance = Infinity;
// Object.entries(graph).forEach(([nodeId, node]) => {
// if (node.type === 'intersection' && building) {
// const distance = Math.sqrt((node.x - building.x) ** 2 + (node.y - building.y) ** 2);
// if (distance < shortestDistance) {
// shortestDistance = distance;
// nearestIntersection = nodeId;
// }
// }
// });
// return nearestIntersection;
// };
// // Get all buildings from the graph
// export const getAllBuildings = (graph: GraphNodes): string[] =>
// Object.entries(graph)
// .filter(([_key, node]) => node.type === 'building')
// .map(([id]) => id);
// // Generate all possible building-to-building paths
// export const generateAllBuildingPaths = (): Path[] => {
// const buildings = getAllBuildings(graphNodes);
// const paths: Path[] = [];
// for (let i = 0; i < buildings.length; i++) {
// for (let j = i + 1; j < buildings.length; j++) {
// const building1 = buildings[i];
// const building2 = buildings[j];
// if (building1 && building2) {
// // Get nearest intersections for both buildings
// const int1 = findNearestIntersection(building1, graphNodes);
// const int2 = findNearestIntersection(building2, graphNodes);
// paths.push({
// id: `${building1}-${building2}`,
// points: [building1, int1, int2, building2],
// });
// }
// }
// }
// return paths;
// };
// // Draw a single path segment
// export const PathSegment = ({
// start,
// end,
// isHighlighted,
// }: {
// start: string;
// end: string;
// isHighlighted?: boolean;
// }): JSX.Element | null => {
// const startNode = graphNodes[start];
// const endNode = graphNodes[end];
// if (!startNode || !endNode) return null;
// if (!isHighlighted) return null;
// return (
// <line
// x1={startNode.x}
// y1={startNode.y}
// x2={endNode.x}
// y2={endNode.y}
// // stroke={isHighlighted ? '#FF8C00' : '#BF5700'}
// stroke={isHighlighted ? '#000' : '#BF5700'} // TODO
// // strokeWidth={isHighlighted ? '4' : '2'}
// strokeWidth={isHighlighted ? '10' : '2'}
// strokeLinecap='round'
// className={`opacity-60 ${isHighlighted ? 'z-10000' : ''}`}
// />
// );
// };