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;
@@ -13,7 +13,40 @@ const UTMapURL = new URL('/src/assets/UT-Map.png', import.meta.url).href;
* - Building selection controls to highlight specific paths.
*/
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 (
<div className='relative h-full w-full'>
@@ -22,19 +55,34 @@ export default function CampusMap() {
{/* SVG Overlay */}
<svg className='absolute left-0 top-0 h-full w-full' viewBox='0 0 784 754' preserveAspectRatio='none'>
{/* Draw all building-to-building paths */}
{generateAllBuildingPaths().map(path => (
<g key={path.id}>
{path.points.slice(0, -1).map((startNode, index) => (
{/* Debug visualization of all connections */}
{Object.entries(graphNodes).map(([nodeId, node]) =>
node.connections.map(connectionId => (
<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
key={`${startNode}-${path.points[index + 1]}`}
start={startNode}
end={path.points[index + 1] || ''}
isHighlighted={highlightedPath.includes(path.id)}
key={`path-${nodeId}-${selectedPath[index + 1]}`}
start={nodeId}
end={selectedPath[index + 1] || ''}
/>
))}
</g>
))}
{/* Draw nodes */}
{Object.entries(graphNodes).map(([id, node]) => (
@@ -43,13 +91,15 @@ export default function CampusMap() {
cx={node.x}
cy={node.y}
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'
strokeWidth='2'
className='opacity-90'
/>
{/* Only label buildings */}
{node.type === 'building' && (
<text x={node.x + 12} y={node.y + 4} fill='#000000' fontSize='14' className='font-bold'>
{id}
@@ -59,34 +109,66 @@ export default function CampusMap() {
))}
</svg>
{/* Building Selection 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='text-sm space-y-2'>
<div className='flex items-center gap-2'>
<div className='h-3 w-3 rounded-full bg-[#BF5700]' />
<span>Buildings</span>
</div>
<div className='flex items-center gap-2'>
<div className='h-3 w-3 rounded-full bg-[#666666]' />
<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'
{/* Controls */}
<div className='absolute right-4 top-4 max-w-xs rounded-md bg-white/90 p-4 shadow-sm space-y-4'>
<div className='space-y-4'>
<div className='space-y-2'>
<label className='block text-sm font-medium'>Start Building:</label>
<select
value={startBuilding}
onChange={e => setStartBuilding(e.target.value)}
className='w-full border border-gray-300 rounded-md p-1'
>
{path.points[0]} {path.points[path.points.length - 1]}
</button>
))}
<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='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>

View File

@@ -1,110 +1,163 @@
import type { MainCampusBuildingsCode } from '@shared/types/MainCampusBuildings';
import React from 'react';
import React, { memo } from 'react';
// Type definitions
type NodeType = 'building' | 'intersection';
interface Node {
type Node = {
x: number;
y: number;
// connections: string[];
connections: string[];
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
export const getAllBuildings = (graph: GraphNodes): string[] =>
Object.entries(graph)
.filter(([_key, node]) => node.type === 'building')
.map(([id]) => id);
type BaseNode = {
x: number;
y: number;
type: NodeType;
};
// Generate all possible building-to-building paths
export const generateAllBuildingPaths = (): Path[] => {
const buildings = getAllBuildings(graphNodes);
const paths: Path[] = [];
// Extended type for nodes with connections (after graph generation)
type ConnectedNode = BaseNode & {
connections: string[];
};
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];
// Graph types
type RawNodes = {
[key: string]: BaseNode;
};
if (building1 && building2) {
// Get nearest intersections for both buildings
const int1 = findNearestIntersection(building1, graphNodes);
const int2 = findNearestIntersection(building2, graphNodes);
type GraphNodes = {
[key: string]: ConnectedNode;
};
paths.push({
id: `${building1}-${building2}`,
points: [building1, int1, int2, building2],
});
}
type PathfindingResult = {
path: string[];
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 = ({
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' : ''}`}
/>
);
};
export const graphNodes: GraphNodes = {
/**
* Explanation of the connections property:
*
* The connections property in the Node interface represents the graph edges in our map.
* It's essential for pathfinding because:
*
* 1. It defines which nodes are directly connected to each other
* 2. It represents actual walkable paths on campus
* 3. It constrains the possible routes for the pathfinding algorithm
*
* How to determine connections:
*
* 1. For intersections:
* - Look at the actual walkways on the map
* - Connect to adjacent intersections that have direct paths
* - Connect to buildings that have entrances on that walkway
*
* 2. For buildings:
* - Connect to the nearest intersections that have direct walkway access
* - Connect to adjacent buildings only if there's a direct walkway
*
*/
export const rawGraphNodes: RawNodes = {
// Building nodes
GDC: {
x: 257,
@@ -121,91 +174,91 @@ export const graphNodes: GraphNodes = {
y: 420,
type: 'building',
},
GRE: {
x: 260,
y: 375,
type: 'building',
},
MAI: {
x: 167,
y: 310,
type: 'building',
},
WEL: {
x: 216,
y: 268,
type: 'building',
},
BEL: {
x: 365,
y: 377,
type: 'building',
},
WCP: {
x: 260,
y: 343,
type: 'building',
},
RLP: {
x: 300,
y: 335,
type: 'building',
},
UTC: {
x: 197,
y: 410,
type: 'building',
},
CBA: {
x: 232,
y: 363,
type: 'building',
},
GSB: {
x: 208,
y: 382,
type: 'building',
},
PMA: {
x: 255,
y: 185,
type: 'building',
},
PAT: {
x: 258,
y: 222,
type: 'building',
},
EER: {
x: 289,
y: 208,
type: 'building',
},
ECJ: {
x: 289,
y: 280,
type: 'building',
},
UNB: {
x: 105,
y: 288,
type: 'building',
},
FAC: {
x: 133,
y: 298,
type: 'building',
},
HRC: {
x: 112,
y: 380,
type: 'building',
},
COM: {
x: 195,
y: 318,
type: 'building',
},
// GRE: {
// x: 260,
// y: 375,
// type: 'building',
// },
// MAI: {
// x: 167,
// y: 310,
// type: 'building',
// },
// WEL: {
// x: 216,
// y: 268,
// type: 'building',
// },
// BEL: {
// x: 365,
// y: 377,
// type: 'building',
// },
// WCP: {
// x: 260,
// y: 343,
// type: 'building',
// },
// RLP: {
// x: 300,
// y: 335,
// type: 'building',
// },
// UTC: {
// x: 197,
// y: 410,
// type: 'building',
// },
// CBA: {
// x: 232,
// y: 363,
// type: 'building',
// },
// GSB: {
// x: 208,
// y: 382,
// type: 'building',
// },
// PMA: {
// x: 255,
// y: 185,
// type: 'building',
// },
// PAT: {
// x: 258,
// y: 222,
// type: 'building',
// },
// EER: {
// x: 289,
// y: 208,
// type: 'building',
// },
// ECJ: {
// x: 289,
// y: 280,
// type: 'building',
// },
// UNB: {
// x: 105,
// y: 288,
// type: 'building',
// },
// FAC: {
// x: 133,
// y: 298,
// type: 'building',
// },
// HRC: {
// x: 112,
// y: 380,
// type: 'building',
// },
// COM: {
// x: 195,
// y: 318,
// type: 'building',
// },
// Intersection nodes
'speedway-24th': {
@@ -218,114 +271,354 @@ export const graphNodes: GraphNodes = {
y: 400,
type: 'intersection',
},
'speedway-e-mai-stairs': {
x: 241,
y: 315,
type: 'intersection',
},
'speedway-w-eer': {
x: 241,
y: 208,
type: 'intersection',
},
'guad-24th': {
x: 89,
y: 250,
type: 'intersection',
},
'guad-21st': {
x: 89,
y: 400,
type: 'intersection',
},
'guad-icd': {
x: 89,
y: 353,
type: 'intersection',
},
'uni-ave-21st': {
x: 166,
y: 400,
type: 'intersection',
},
'wichita-21st': {
x: 187,
y: 400,
type: 'intersection',
},
'n-mai-24th': {
x: 167,
y: 250,
type: 'intersection',
},
's-mai-stairs': {
x: 167,
y: 347,
type: 'intersection',
},
'e-mai-stairs': {
x: 215,
y: 315,
type: 'intersection',
},
'guad-w-mai': {
x: 89,
y: 317,
type: 'intersection',
},
'n-mai-turtle-pond': {
x: 167,
y: 282,
type: 'intersection',
},
'icd-ne': {
x: 207,
y: 289,
type: 'intersection',
},
'icd-nne': {
x: 190,
y: 282,
type: 'intersection',
},
'icd-se': {
x: 212,
y: 338,
type: 'intersection',
},
'icd-sse': {
x: 190,
y: 347,
type: 'intersection',
},
'san-jac-21th': {
x: 354,
y: 400,
type: 'intersection',
},
'san-jac-24th': {
x: 357,
y: 250,
type: 'intersection',
},
'san-jac-23rd': {
x: 358,
y: 318,
type: 'intersection',
},
'mlk-jr-statue': {
x: 280,
y: 318,
type: 'intersection',
},
'pcl-nw-21st-walkway': {
x: 208,
y: 400,
type: 'intersection',
},
// 'speedway-e-mai-stairs': {
// x: 241,
// y: 315,
// type: 'intersection',
// },
// 'speedway-w-eer': {
// x: 241,
// y: 208,
// type: 'intersection',
// },
// 'guad-24th': {
// x: 89,
// y: 250,
// type: 'intersection',
// },
// 'guad-21st': {
// x: 89,
// y: 400,
// type: 'intersection',
// },
// 'guad-icd': {
// x: 89,
// y: 353,
// type: 'intersection',
// },
// 'uni-ave-21st': {
// x: 166,
// y: 400,
// type: 'intersection',
// },
// 'wichita-21st': {
// x: 187,
// y: 400,
// type: 'intersection',
// },
// 'n-mai-24th': {
// x: 167,
// y: 250,
// type: 'intersection',
// },
// 's-mai-stairs': {
// x: 167,
// y: 347,
// type: 'intersection',
// },
// 'e-mai-stairs': {
// x: 215,
// y: 315,
// type: 'intersection',
// },
// 'guad-w-mai': {
// x: 89,
// y: 317,
// type: 'intersection',
// },
// 'n-mai-turtle-pond': {
// x: 167,
// y: 282,
// type: 'intersection',
// },
// 'icd-ne': {
// x: 207,
// y: 289,
// type: 'intersection',
// },
// 'icd-nne': {
// x: 190,
// y: 282,
// type: 'intersection',
// },
// 'icd-se': {
// x: 212,
// y: 338,
// type: 'intersection',
// },
// 'icd-sse': {
// x: 190,
// y: 347,
// type: 'intersection',
// },
// 'san-jac-21th': {
// x: 354,
// y: 400,
// type: 'intersection',
// },
// 'san-jac-24th': {
// x: 357,
// y: 250,
// type: 'intersection',
// },
// 'san-jac-23rd': {
// x: 358,
// y: 318,
// type: 'intersection',
// },
// 'mlk-jr-statue': {
// x: 280,
// y: 318,
// type: 'intersection',
// },
// 'pcl-nw-21st-walkway': {
// x: 208,
// y: 400,
// type: 'intersection',
// },
'pcl-w-speedway': {
x: 241,
y: 425,
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' : ''}`}
// />
// );
// };