Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,15 @@ jobs:

- name: "Tests"
run: just test

- name: "Frontend Tests"
run: just test-frontend

- name: "Compile Check"
run: just compile

- name: "Validate Data"
run: just validate-data

- name: "Docker Compose Config"
run: just docker-config
22 changes: 10 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ Each instruction can be dismissed as you go, similar to how Google Maps lets you
|**Flask**|Turns our Python code into a running website|
|**NetworkX**|Models the building as a graph and calculates the shortest path between rooms|
|**HTML & CSS**|Structure and styling of the webpage|
|**SVG / Leaflet.js**|Draws the animated blue route line over the floor plan image|
|**Leaflet.js**|Draws the building grid, route line, and route markers|
|**JSON**|Text files that store our building data (rooms, coordinates, connections)|
|**uv**|Package manager — installs and manages Python libraries|
|**Git**|Version control — tracks changes so the team can collaborate without overwriting each other|
Expand All @@ -35,8 +35,8 @@ The building is modeled as a **graph** — locations are **nodes** and the walki
2. **name** — the human-readable label (e.g., `"5.102"` or `"Main Staircase"`), displayed in directions
3. **type** — the category of location (e.g., `"room"`, `"staircase"`, `"elevator"`, `"waypoint"`, `"spine"`). A `"spine"` node represents a main hallway/backbone connector used to route movement through the building, and the field is used to filter and categorize nodes
4. **building** — the building code (e.g., `"NPB"`), allowing multi-building support
5. **coords** — the pixel coordinates `[x, y]` on the floor plan image, used to draw the blue route line
6. **floor** — the floor number the node is on, allowing multi-floor pathfinding and filtering
5. **coords** — the `[x, y]` coordinates used to draw the blue route line on the map grid
6. **floor** — the floor number the node is on, allowing multi-floor pathfinding and filtering. The browser map preview supports one floor at a time; multi-floor routes still show their step-by-step directions.

Each edge stores 4 things:

Expand Down Expand Up @@ -75,15 +75,13 @@ campus_navigator/
│ │
│ ├── static/ ← Files the browser downloads directly
│ │ ├── css/
│ │ │ └── style.css ← Page styling
│ │ ├── js/
│ │ │ └── map.js ← Draws the blue route line (Leaflet.js logic)
│ │ └── img/
│ │ └── floorplan.png ← The floor plan image displayed on screen
│ │ │ └── styles.css ← Page styling
│ │ └── js/
│ │ ├── directions.js ← Step-by-step direction cards
│ │ └── map.js ← Draws the route on a Leaflet grid
│ │
│ └── templates/ ← HTML pages Flask serves to the user
│ ├── base.html ← Shared page skeleton (nav bar, footer, etc.)
│ └── index.html ← The main search + map page
│ └── index.html ← The search, directions, and map page
│
└── data/ ← Building data — treated like a database
├── nodes.json ← Every room/intersection with its name and coordinates
Expand Down Expand Up @@ -116,7 +114,7 @@ graph_manager.py returns: text directions + pixel coordinates
routes.py passes results to index.html (via Flask's render_template)
↓
index.html displays the step-by-step text directions
map.js reads the coordinates and draws the blue line over floorplan.png
map.js reads the coordinates and draws the blue line over a coordinate grid
↓
User sees directions on screen ✓
```
Expand All @@ -143,7 +141,7 @@ Write the Python function that loads the JSON files, builds the NetworkX graph,

**Workspace: `app/templates/`, `app/static/`**

Build the HTML/CSS pages and write the JavaScript (`map.js`) that draws the animated blue line over the floor plan. Keep it mobile-friendly — users will be on their phones while walking.
Build the HTML/CSS pages and write the JavaScript that draws the animated blue line over the coordinate grid. Keep it mobile-friendly. Users will be on their phones while walking.

### 🔌 Integration (1 person)

Expand Down
5 changes: 4 additions & 1 deletion app/logic/data_entry.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@
from pathlib import Path

BASE_DIR = Path(__file__).resolve().parent
DATA_DIR = BASE_DIR / "data"
REPO_ROOT = BASE_DIR.parents[1]
DATA_DIR = REPO_ROOT / "data"
NODES_FILE = DATA_DIR / "nodes.json"
EDGES_FILE = DATA_DIR / "edges.json"
DEFAULT_BUILDING = "NPB"


# JSON helpers
Expand Down Expand Up @@ -55,6 +57,7 @@ def add_node():
"id": node_id,
"name": label,
"type": node_type,
"building": DEFAULT_BUILDING,
"coords": [x, y],
"floor": floor,
}
Expand Down
5 changes: 5 additions & 0 deletions app/logic/graph_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,11 @@ def get_directions(start, end):
directions.append(
f"Take the stairs from floor {current_node['floor']} to floor {next_node['floor']}."
)
elif (
current_node.get("type") == "elevator"
and next_node.get("type") == "elevator"
):
directions.append(edge_data["instruction"])
else:
directions.append(
f"Move from floor {current_node['floor']} to floor {next_node['floor']}."
Expand Down
15 changes: 12 additions & 3 deletions app/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ def directions():
steps = None
coordinates = None
floor_bounds = None
map_error = None
error = None
entrance = request.form.get("entrance")
classroom = request.form.get("classroom")
Expand All @@ -32,9 +33,16 @@ def directions():
steps = result["directions"]
coordinates = result["coordinates"]
if coordinates:
building = entrance.split("_")[0]
floor = coordinates[0]["floor"]
floor_bounds = get_floor_bounds(building, floor)
route_floors = {coordinate["floor"] for coordinate in coordinates}
if len(route_floors) > 1:
map_error = (
"Map preview is unavailable for routes spanning multiple floors. "
"Follow the directions panel for this route."
)
else:
building = entrance.split("_")[0]
floor = coordinates[0]["floor"]
floor_bounds = get_floor_bounds(building, floor)
except RuntimeError as e:
error = str(e)
entrances, classrooms = get_options()
Expand All @@ -43,6 +51,7 @@ def directions():
steps=steps,
coordinates=coordinates,
floor_bounds=floor_bounds,
map_error=map_error,
error=error,
entranceOptions=entrances,
classroomOptions=classrooms,
Expand Down
30 changes: 26 additions & 4 deletions app/static/css/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ button:hover {
margin: 10px 0;
padding-left: 20px;
}
.ateps-box li {
.steps-box li {
margin-bottom: 8px;
color: #555;
}
Expand All @@ -111,7 +111,7 @@ button:hover {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}

.direction-box h2 {
.directions-box h2 {
margin-top: 0;
color: #0c2340;
font-size: 18px;
Expand Down Expand Up @@ -146,7 +146,7 @@ button:hover {
background: #d4ffd4;
border-left: 5px solid #28a745;
}
#next--step-btn {
#next-step-btn {
margin-top: 10px;
padding: 8px 12px;
background: #007bff;
Expand All @@ -155,7 +155,7 @@ button:hover {
border-radius: 5px;
cursor: pointer;
}
#next--step-btn:hover {
#next-step-btn:hover {
background: #0056b3;
}

Expand All @@ -181,3 +181,25 @@ html {
width: 100%;
height: 100vh;
}

@media (max-width: 768px) {
.container {
flex-direction: column;
height: auto;
min-height: 100vh;
}

.left-panel,
.right-panel {
box-sizing: border-box;
width: 100%;
}

.right-panel {
height: 50vh;
}

#map {
height: 50vh;
}
}
53 changes: 29 additions & 24 deletions app/static/js/directions.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,37 +2,42 @@ document.addEventListener("DOMContentLoaded", () => {
if (typeof directionSteps === "undefined" || !directionSteps.length) return;

const container = document.getElementById("directions-card-container");
if (!container) return;

let currentIndex = 0;

function renderSteps(index) {
container.innerHTML = "";

if (index >= directionSteps.length) {
container.innerHTML = `
<div class="direction-card arrived">
<h3>You have arrived!</h3>
</div>
`;
return;
}

const stepText = directionSteps[index];
container.replaceChildren();

const card = document.createElement("div");
card.className = "direction-card active";

card.innerHTML = `
<h3>Step ${index + 1}</h3>
<p>${stepText}</p>
<button id="next-step-btn">Next</button>
`;
card.className =
index >= directionSteps.length
? "direction-card arrived"
: "direction-card active";

const heading = document.createElement("h3");
heading.textContent =
index >= directionSteps.length
? "You have arrived!"
: `Step ${index + 1}`;
card.appendChild(heading);

if (index < directionSteps.length) {
const paragraph = document.createElement("p");
paragraph.textContent = directionSteps[index];
card.appendChild(paragraph);

const nextButton = document.createElement("button");
nextButton.id = "next-step-btn";
nextButton.textContent = "Next";
nextButton.addEventListener("click", () => {
currentIndex++;
renderSteps(currentIndex);
});
card.appendChild(nextButton);
}

container.appendChild(card);

document.getElementById("next-step-btn").addEventListener("click", () => {
currentIndex++;
renderSteps(currentIndex);
});
}

renderSteps(currentIndex);
Expand Down
54 changes: 36 additions & 18 deletions app/static/js/map.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,31 @@
document.addEventListener("DOMContentLoaded", () => {
if (!window.routeCoordinates || !window.floorBounds) {
const { routeCoordinates, floorBounds } = window;
if (!routeCoordinates || !floorBounds) {
console.error("routeCoordinates or floorBounds missing from template");
return;
}
if (!window.L) {
console.error("Leaflet is missing from the page");
return;
}
if (!Array.isArray(routeCoordinates)) {
console.error("routeCoordinates must be an array");
return;
}

const floors = new Set(
routeCoordinates
.map((coordinate) => coordinate.floor)
.filter((floor) => floor !== undefined),
);
if (floors.size > 1) {
console.error("Map preview only supports routes on one floor");
return;
}

// Initialize map with simple CRS
const map = L.map("map", {
crs: L.CRS.Simple,
const map = window.L.map("map", {
crs: window.L.CRS.Simple,
minZoom: -2,
maxZoom: 4,
zoomControl: true,
Expand All @@ -26,7 +45,7 @@ document.addEventListener("DOMContentLoaded", () => {

// Vertical lines
for (let x = 0; x <= floorBounds.width; x += gridSize) {
L.polyline(
window.L.polyline(
[
[0, x],
[floorBounds.height, x],
Expand All @@ -41,7 +60,7 @@ document.addEventListener("DOMContentLoaded", () => {

// Horizontal lines
for (let y = 0; y <= floorBounds.height; y += gridSize) {
L.polyline(
window.L.polyline(
[
[y, 0],
[y, floorBounds.width],
Expand All @@ -60,29 +79,28 @@ document.addEventListener("DOMContentLoaded", () => {
// Convert backend coordinates → Leaflet [lat, lng]
const latlngs = routeCoordinates.map((c) => [c.y, c.x]);

// Draw animated route line
const routeLine = L.polyline(latlngs, {
color: "blue",
weight: 4,
dashArray: "10, 10",
className: "animated-route",
}).addTo(map);
if (latlngs.length > 0) {
// Draw animated route line
const routeLine = window.L.polyline(latlngs, {
color: "blue",
weight: 4,
dashArray: "10, 10",
className: "animated-route",
}).addTo(map);

// Fit map to route
map.fitBounds(routeLine.getBounds(), { padding: [20, 20] });
// Fit map to route
map.fitBounds(routeLine.getBounds(), { padding: [20, 20] });

// Start + end markers
if (latlngs.length > 0) {
// Start marker (green)
L.circleMarker(latlngs[0], {
window.L.circleMarker(latlngs[0], {
radius: 6,
color: "green",
fillColor: "green",
fillOpacity: 1,
}).addTo(map);

// End marker (red)
L.circleMarker(latlngs[latlngs.length - 1], {
window.L.circleMarker(latlngs[latlngs.length - 1], {
radius: 6,
color: "red",
fillColor: "red",
Expand Down
12 changes: 10 additions & 2 deletions app/templates/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ <h1>Choose Your Options</h1>
</select>
<br>
<br>
<button type="/directions">Get Directions</button>
<button type="submit">Get Directions</button>
<div style="height: 30px;"></div>
<div class="radio-group">
<strong>Route:</strong>
Expand Down Expand Up @@ -61,7 +61,15 @@ <h2>Directions</h2>
{% endif %}
</div>
<div class="right-panel">
{% if coordinates %}
{% if map_error %}
<div id="map"
style="background: #f4f5f7;
display: flex;
align-items: center;
justify-content: center">
<p style="color: #999;">{{ map_error }}</p>
</div>
{% elif coordinates %}
<div id="map"></div>
<script>
window.routeCoordinates = {{ coordinates | tojson }};
Expand Down
Loading
Loading