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
25 changes: 25 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
## 0.2.0 (2025-11-02)

### Feat

- integrate frontend
- **path.py**: validation of path objects (#28)
- add a* algorithm (#6)
- **preprocessing.py**: added map from id to name (#5)

### Fix

- **main.py**: small import fix (#22)
- fixed type of parameter for graph in algorithms (#20)
- absolute imports rather than relative (#8)
- skip node if empty data
- path dataclass fix

### Refactor

- **path.py**: force use of factory methods (#37)
- delete bfs.py (#31)
- more path invariants (#30)
- import shenanigans (#29)
- remove raising errors and just return empty path (#27)
- **path.py**: rename distance to time (#26)
14 changes: 11 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ Project 2 for COP3530
---

Table of Contents

- [Overview](#overview)
- [Features](#features)
- [Requirements](#requirements)
Expand Down Expand Up @@ -55,6 +56,12 @@ Run the script directly (inside waypoint/waypoint):
uv run main.py
```

Run the website (inside waypoint/web):

```bash
uv run app.py
```

## Testing

Run tests in the main directory:
Expand All @@ -65,9 +72,10 @@ uv run pytest

## Repository Structure

- `waypoint/` - Source code
- `data/` - Data is small enough to easily include in repo
- `tests/` - Unit tests
- `waypoint/` - Source code
- `data/` - Data is small enough to easily include in repo
- `tests/` - Unit tests
- `web/` - Web application code

## Contributing

Expand Down
13 changes: 12 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
[project]
name = "waypoint"
version = "0.1.0"
version = "0.2.0"
description = "Finding the shortest flight combination between two airports."
readme = "README.md"
requires-python = ">=3.12"
dependencies = [
"flask>=3.1.2",
]

[dependency-groups]
dev = [
Expand All @@ -16,3 +19,11 @@ build-backend = "setuptools.build_meta"

[tool.setuptools]
packages = ["waypoint"]

[tool.commitizen]
name = "cz_conventional_commits"
tag_format = "$version"
version_scheme = "semver2"
version_provider = "uv"
update_changelog_on_bump = true
major_version_zero = true
140 changes: 139 additions & 1 deletion uv.lock

Large diffs are not rendered by default.

54 changes: 54 additions & 0 deletions web/app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
from typing import cast, TypedDict

from flask import Flask, render_template, request, jsonify

from waypoint.preprocessing import file_to_graph, file_to_airports
from waypoint.algorithms import djikstra
from waypoint.path import Path

app = Flask(__name__)

_ = app.config.from_pyfile("config.py")

data_path = cast(str, app.config["DATA_PATH"])
graph: dict[int, dict[int, float]] = file_to_graph(data_path)

airports_path = cast(str, app.config["AIRPORTS_PATH"])
airports: dict[int, str] = file_to_airports(airports_path)


class RequestData(TypedDict):
start: int
end: int


@app.route("/")
def index():
return render_template("index.html")


@app.route("/process", methods=["POST"])
def process():
data: RequestData = cast(RequestData, request.get_json())
start = int(data["start"])
end = int(data["end"])

path = djikstra(graph, start, end)

if path == Path.empty():
return jsonify({"message": "No path found."})

return jsonify(
{
"flights": (
[airports[airport_id] for airport_id in path.flights]
if path.flights is not None
else []
),
"time": path.time,
}
)


if __name__ == "__main__":
app.run(debug=True)
2 changes: 2 additions & 0 deletions web/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
DATA_PATH = "../data/data.csv"
AIRPORTS_PATH = "../data/airports.csv"
Binary file added web/static/reverse.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
58 changes: 58 additions & 0 deletions web/static/script.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
const startInput = document.getElementById("startInput");
const endInput = document.getElementById("endInput");
const reverseBtn = document.getElementById("reverseBtn");
const routeBtn = document.getElementById("routeBtn");
const output = document.getElementById("output");

reverseBtn.addEventListener("click", () => {
const temp = startInput.value;
startInput.value = endInput.value;
endInput.value = temp;
});

//Placeholder bhvr already handled by browser and CSS opacity
//Ensure placeholder reappears if text removed

[startInput, endInput].forEach((input) => {
input.addEventListener("focus", () => {
input.classList.add("has-focus");
});

input.addEventListener("blur", () => {
input.classList.remove("has-focus");
});
});

routeBtn.addEventListener("click", async () => {
const start = startInput.value.trim();
const end = endInput.value.trim();

if (!start || !end) {
output.textContent =
"Please enter a start location and an end destination.";
return;
}

output.textContent = "Loading route...";

try {
const res = await fetch("/process", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ start, end }),
});

if (!res.ok) throw new Error(`Server error: ${res.status}`);

const json = await res.json();
if (json.message) output.textContent = json.message;
else {
output.textContent =
json.flights.map((airport) => `${airport}`).join("\n") +
`\nTotal travel time: ${json.time} minutes`;
}
} catch (err) {
output.textContent = `Error: ${err.message}`;
console.error(err);
}
});
149 changes: 149 additions & 0 deletions web/static/styles.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
:root {
--header-blue: #1800ad;
--bg-purple: #5944dd;
--card-white: #ffffff;
--box-grey: #bbb8b8;
--btn-blue: #2e15c6;
}

* {
box-sizing: border-box;
}

html,
body {
height: 100%
}

body {
margin: 0;
font-family: "Segoe UI", sans-serif;
background: var(--bg-purple);
color: #0b0b0b;
}

.header {
width: 100%;
background: var(--header-blue);
box-shadow: 0 2px 0 rgba(0, 0, 0, .15);
position: relative;
}

.header-inner {
max-width: 1400px;
margin: 0 auto;
display: flex;
align-items: center;
justify-content: space-between;
padding: 18px 28px;
}

.site-title {
margin: 0;
color: white;
font-weight: 800;
letter-spacing: 1px;
font-size: 28px;
}

.page {
display: flex;
justify-content: center;
align-items: flex-start;
padding: 70px 20px;
min-height: calc(100vh-70px);
}

.card {
background: var(--card-white);
width: 560px;
max-width: calc(100%-40px);
padding: 48px 40px;
border-radius: 28px;
text-align: center;
box-shadow: 0 10px 35px rgba(0, 0, 0, 0.25);
}

.input-label {
display: block;
font-size: 20px;
margin-bottom: 10px;
text-align: left;
color: #1f1f1f;
}

.input-box {
display: block;
width: 70%;
margin: 0 auto 28px auto;
padding: 14px 18px;
border-radius: 22px;
border: none;
background: var(--box-grey);
text-align: center;
font-size: 16px;
outline: none;
transition: box-shadow .12s ease;
}

input::placeholder {
opacity: .45;
color: #2a2a2a;
transition: opacity .12s ease;
}

input:focus::placeholder {
opacity: 0;
}

input-box:focus {
box-shadow: 0 0 0 4px rgba(46, 21, 198, .08);
}

.reverse-btn {
display: inline-block;
margin: 8px auto 28px auto;
background: transparent;
border: none;
cursor: pointer;
padding: 6px;
}

.reverse-icon {
width: 36px;
height: 36px;
display: block;
}

.submit-btn {
display: block;
width: 86%;
margin: 12px auto 0 auto;
padding: 16px 18px;
background: var(--btn-blue);
color: white;
border: none;
font-size: 18px;
font-weight: 600;
border-radius: 28px;
cursor: pointer;
box-shadow: 0 8px 18px rgba(46, 21, 198, .18);
transition: transform .12s ease, opacity .12s ease;
}

.submit-btn:hover {
transform: translateY(-2px);
opacity: .98;
}

.submit-btn:active {
transform: translateY(0);
}

.output-box {
margin-top: 18px;
font-size: 15px;
min-height: 1.2em;
color: #111;
text-align: center;
}
Loading