feat: add working paths
This commit is contained in:
1486
src/shared/types/MainCampusBuildings.ts
Normal file
1486
src/shared/types/MainCampusBuildings.ts
Normal file
File diff suppressed because it is too large
Load Diff
94
src/views/components/map/CampusMap.tsx
Normal file
94
src/views/components/map/CampusMap.tsx
Normal file
@@ -0,0 +1,94 @@
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import { generateAllBuildingPaths, graphNodes, PathSegment } from './mapUtils';
|
||||
|
||||
const UTMapURL = new URL('/src/assets/UT-Map.png', import.meta.url).href;
|
||||
|
||||
/**
|
||||
* CampusMap component renders an interactive map of the UT campus.
|
||||
*
|
||||
* The map includes:
|
||||
* - An image of the campus map.
|
||||
* - An SVG overlay to draw paths and nodes.
|
||||
* - Building selection controls to highlight specific paths.
|
||||
*/
|
||||
export default function CampusMap() {
|
||||
const [highlightedPath, setHighlightedPath] = useState<string[]>([]);
|
||||
|
||||
return (
|
||||
<div className='relative h-full w-full'>
|
||||
{/* Map Image: 784x754 */}
|
||||
<img src={UTMapURL} alt='UT Campus Map' className='h-full w-full object-contain' />
|
||||
|
||||
{/* 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) => (
|
||||
<PathSegment
|
||||
key={`${startNode}-${path.points[index + 1]}`}
|
||||
start={startNode}
|
||||
end={path.points[index + 1] || ''}
|
||||
isHighlighted={highlightedPath.includes(path.id)}
|
||||
/>
|
||||
))}
|
||||
</g>
|
||||
))}
|
||||
|
||||
{/* Draw nodes */}
|
||||
{Object.entries(graphNodes).map(([id, node]) => (
|
||||
<g key={id}>
|
||||
<circle
|
||||
cx={node.x}
|
||||
cy={node.y}
|
||||
r={node.type === 'building' ? 6 : 4}
|
||||
fill={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}
|
||||
</text>
|
||||
)}
|
||||
</g>
|
||||
))}
|
||||
</svg>
|
||||
|
||||
{/* Building Selection Controls */}
|
||||
<div className='absolute right-8 top-8 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='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]}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -11,6 +11,7 @@ import CalendarFooter from '../calendar/CalendarFooter';
|
||||
import { CalendarSchedules } from '../calendar/CalendarSchedules';
|
||||
import ImportantLinks from '../calendar/ImportantLinks';
|
||||
import TeamLinks from '../calendar/TeamLinks';
|
||||
import CampusMap from './CampusMap';
|
||||
|
||||
const manifest = chrome.runtime.getManifest();
|
||||
const LDIconURL = new URL('/src/assets/LD-icon.png', import.meta.url).href;
|
||||
@@ -52,7 +53,7 @@ export default function Map(): JSX.Element {
|
||||
<CalendarFooter />
|
||||
</div>
|
||||
<div className='flex p-12'>
|
||||
<img src={UTMapURL} alt='LD Icon' />
|
||||
<CampusMap />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
329
src/views/components/map/mapUtils.tsx
Normal file
329
src/views/components/map/mapUtils.tsx
Normal file
@@ -0,0 +1,329 @@
|
||||
import type { MainCampusBuildingsCode } from '@shared/types/MainCampusBuildings';
|
||||
import React from 'react';
|
||||
|
||||
// Type definitions
|
||||
type NodeType = 'building' | 'intersection';
|
||||
|
||||
interface Node {
|
||||
x: number;
|
||||
y: number;
|
||||
// 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);
|
||||
|
||||
// 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;
|
||||
|
||||
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-1000' : ''}`}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const graphNodes: GraphNodes = {
|
||||
// Building nodes
|
||||
GDC: {
|
||||
x: 257,
|
||||
y: 283,
|
||||
type: 'building',
|
||||
},
|
||||
PCL: {
|
||||
x: 222,
|
||||
y: 430,
|
||||
type: 'building',
|
||||
},
|
||||
JES: {
|
||||
x: 260,
|
||||
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',
|
||||
},
|
||||
|
||||
// Intersection nodes
|
||||
'speedway-24th': {
|
||||
x: 241,
|
||||
y: 250,
|
||||
type: 'intersection',
|
||||
},
|
||||
'speedway-21st': {
|
||||
x: 241,
|
||||
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',
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user