diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d05ffef..32623e6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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 diff --git a/README.md b/README.md index 5e0338c..b732c2a 100644 --- a/README.md +++ b/README.md @@ -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| @@ -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: @@ -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 @@ -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 ✓ ``` @@ -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) diff --git a/app/logic/data_entry.py b/app/logic/data_entry.py index 913980b..48d14d5 100644 --- a/app/logic/data_entry.py +++ b/app/logic/data_entry.py @@ -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 @@ -55,6 +57,7 @@ def add_node(): "id": node_id, "name": label, "type": node_type, + "building": DEFAULT_BUILDING, "coords": [x, y], "floor": floor, } diff --git a/app/logic/graph_manager.py b/app/logic/graph_manager.py index 88a8874..00f24b5 100644 --- a/app/logic/graph_manager.py +++ b/app/logic/graph_manager.py @@ -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']}." diff --git a/app/routes.py b/app/routes.py index 1de759a..cd887f5 100644 --- a/app/routes.py +++ b/app/routes.py @@ -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") @@ -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() @@ -43,6 +51,7 @@ def directions(): steps=steps, coordinates=coordinates, floor_bounds=floor_bounds, + map_error=map_error, error=error, entranceOptions=entrances, classroomOptions=classrooms, diff --git a/app/static/css/styles.css b/app/static/css/styles.css index a464b4b..1d9257a 100644 --- a/app/static/css/styles.css +++ b/app/static/css/styles.css @@ -97,7 +97,7 @@ button:hover { margin: 10px 0; padding-left: 20px; } -.ateps-box li { +.steps-box li { margin-bottom: 8px; color: #555; } @@ -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; @@ -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; @@ -155,7 +155,7 @@ button:hover { border-radius: 5px; cursor: pointer; } -#next--step-btn:hover { +#next-step-btn:hover { background: #0056b3; } @@ -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; + } +} diff --git a/app/static/js/directions.js b/app/static/js/directions.js index db205da..7a2a0bb 100644 --- a/app/static/js/directions.js +++ b/app/static/js/directions.js @@ -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 = ` -
-

You have arrived!

-
- `; - return; - } - - const stepText = directionSteps[index]; + container.replaceChildren(); const card = document.createElement("div"); - card.className = "direction-card active"; - - card.innerHTML = ` -

Step ${index + 1}

-

${stepText}

- - `; + 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); diff --git a/app/static/js/map.js b/app/static/js/map.js index 34ae9d0..9475ac7 100644 --- a/app/static/js/map.js +++ b/app/static/js/map.js @@ -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, @@ -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], @@ -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], @@ -60,21 +79,20 @@ 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", @@ -82,7 +100,7 @@ document.addEventListener("DOMContentLoaded", () => { }).addTo(map); // End marker (red) - L.circleMarker(latlngs[latlngs.length - 1], { + window.L.circleMarker(latlngs[latlngs.length - 1], { radius: 6, color: "red", fillColor: "red", diff --git a/app/templates/index.html b/app/templates/index.html index 628f645..94edca7 100644 --- a/app/templates/index.html +++ b/app/templates/index.html @@ -29,7 +29,7 @@

Choose Your Options



- +
Route: @@ -61,7 +61,15 @@

Directions

{% endif %}
- {% if coordinates %} + {% if map_error %} +
+

{{ map_error }}

+
+ {% elif coordinates %}