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 { CalendarSchedules } from '../calendar/CalendarSchedules';
|
||||||
import ImportantLinks from '../calendar/ImportantLinks';
|
import ImportantLinks from '../calendar/ImportantLinks';
|
||||||
import TeamLinks from '../calendar/TeamLinks';
|
import TeamLinks from '../calendar/TeamLinks';
|
||||||
|
import CampusMap from './CampusMap';
|
||||||
|
|
||||||
const manifest = chrome.runtime.getManifest();
|
const manifest = chrome.runtime.getManifest();
|
||||||
const LDIconURL = new URL('/src/assets/LD-icon.png', import.meta.url).href;
|
const LDIconURL = new URL('/src/assets/LD-icon.png', import.meta.url).href;
|
||||||
@@ -52,7 +53,7 @@ export default function Map(): JSX.Element {
|
|||||||
<CalendarFooter />
|
<CalendarFooter />
|
||||||
</div>
|
</div>
|
||||||
<div className='flex p-12'>
|
<div className='flex p-12'>
|
||||||
<img src={UTMapURL} alt='LD Icon' />
|
<CampusMap />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</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