feat(testing): improve pathfinding
This commit is contained in:
@@ -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,18 +55,33 @@ 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
|
||||||
<PathSegment
|
key={`${nodeId}-${connectionId}`}
|
||||||
key={`${startNode}-${path.points[index + 1]}`}
|
x1={node.x}
|
||||||
start={startNode}
|
y1={node.y}
|
||||||
end={path.points[index + 1] || ''}
|
x2={graphNodes[connectionId]?.x || 0}
|
||||||
isHighlighted={highlightedPath.includes(path.id)}
|
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={`path-${nodeId}-${selectedPath[index + 1]}`}
|
||||||
|
start={nodeId}
|
||||||
|
end={selectedPath[index + 1] || ''}
|
||||||
/>
|
/>
|
||||||
))}
|
|
||||||
</g>
|
|
||||||
))}
|
))}
|
||||||
|
|
||||||
{/* Draw nodes */}
|
{/* Draw nodes */}
|
||||||
@@ -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>
|
<option value=''>Select Building</option>
|
||||||
<div className='flex items-center gap-2'>
|
{Object.entries(graphNodes)
|
||||||
<div className='h-1 w-6 bg-[#BF5700]' />
|
.filter(([_, node]) => node.type === 'building')
|
||||||
<span>Walking Paths</span>
|
.map(([id]) => (
|
||||||
</div>
|
<option key={id} value={id}>
|
||||||
|
{id}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='overflow-y-scroll space-y-2'>
|
<div className='space-y-2'>
|
||||||
<p className='text-sm font-medium'>Building Paths:</p>
|
<label className='block text-sm font-medium'>End Building:</label>
|
||||||
{generateAllBuildingPaths().map(path => (
|
<select
|
||||||
<button
|
value={endBuilding}
|
||||||
key={path.id}
|
onChange={e => setEndBuilding(e.target.value)}
|
||||||
onClick={() => setHighlightedPath([path.id])}
|
className='w-full border border-gray-300 rounded-md p-1'
|
||||||
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='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>
|
||||||
|
|||||||
@@ -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
|
|
||||||
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
|
// Extended type for nodes with connections (after graph generation)
|
||||||
export const PathSegment = ({
|
type ConnectedNode = BaseNode & {
|
||||||
start,
|
connections: string[];
|
||||||
end,
|
};
|
||||||
isHighlighted,
|
|
||||||
}: {
|
|
||||||
start: string;
|
|
||||||
end: string;
|
|
||||||
isHighlighted?: boolean;
|
|
||||||
}): JSX.Element | null => {
|
|
||||||
const startNode = graphNodes[start];
|
|
||||||
const endNode = graphNodes[end];
|
|
||||||
|
|
||||||
if (!startNode || !endNode) return null;
|
// Graph types
|
||||||
|
type RawNodes = {
|
||||||
|
[key: string]: BaseNode;
|
||||||
|
};
|
||||||
|
|
||||||
if (!isHighlighted) return null;
|
type GraphNodes = {
|
||||||
|
[key: string]: ConnectedNode;
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
type PathfindingResult = {
|
||||||
<line
|
path: string[];
|
||||||
x1={startNode.x}
|
distance: number;
|
||||||
y1={startNode.y}
|
};
|
||||||
x2={endNode.x}
|
|
||||||
y2={endNode.y}
|
/**
|
||||||
// stroke={isHighlighted ? '#FF8C00' : '#BF5700'}
|
* Configuration options for automatic connection generation
|
||||||
stroke={isHighlighted ? '#000' : '#BF5700'} // TODO
|
*/
|
||||||
// strokeWidth={isHighlighted ? '4' : '2'}
|
type ConnectionConfig = {
|
||||||
strokeWidth={isHighlighted ? '10' : '2'}
|
// Maximum distance for direct connections between intersections
|
||||||
strokeLinecap='round'
|
maxIntersectionDistance: number;
|
||||||
className={`opacity-60 ${isHighlighted ? 'z-10000' : ''}`}
|
// 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
|
||||||
);
|
);
|
||||||
};
|
|
||||||
|
|
||||||
export const graphNodes: GraphNodes = {
|
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 nodes;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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
|
// 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' : ''}`}
|
||||||
|
// />
|
||||||
|
// );
|
||||||
|
// };
|
||||||
|
|||||||
Reference in New Issue
Block a user