homework.wenqian.dev
← Back to index
Homework 32026-08-27big project

Sydney Navigator

Build a working routing engine for Sydney — real suburbs, real traffic, a road closure, and a map in your browser that runs on your own Python.

What You Are Building

The finish line: You type python3 serve.py, open a browser, click two Sydney suburbs, and a route lights up with a time on it. Every number on that screen came out of code you wrote. Nothing on this page is new — it is weeks 4 to 7 pointed at one real problem.

Download the complete starter project

Includes router.py, sydney_map.txt, check_router.py, serve.py, app.html, and the README.

Download ZIP

Already started? Download the safe update

Updates the map, events, checker, and instructions. It deliberately does not include router.py; UPGRADE.md explains the small migration.

Download update

33

suburbs on the map

53

roads and ferries

4

times of day

15

jobs to finish

This is the target

Every button below is a question your finished program will answer. The numbers are real: they were produced by the reference solution running on the same map file you are given.

Real Sydney geography · teaching network overlaid

46 minutes, 9 suburbslocalmain roadmotorwayferrymorning breakdownclosed today

You write

router.py

Fifteen functions. That is the whole engine.

You are given

sydney_map.txt · check_router.py · serve.py · app.html

The map, a marker that tells you exactly what is missing, and the web page. Do not edit them.

Rules

  • Estimated time: 8 – 12 hours. Do not try to do it in one sitting.
  • Only router.py is yours to edit. Everything else is provided.
  • Do the jobs in order. Run python3 check_router.py after every single one. It names the exact job that is failing.
  • The only Python import you need is heapq. No pip or Python packages. The real map background loads in the browser, so it needs an internet connection.
  • Write each search once. Jobs 11, 12 and 14 must all call job 9. If you have written Dijkstra twice, you have gone wrong.

The Map File

A plain text file, exactly the kind you read in week 4. Every line starts with a word that says what it is. Blank lines and anything after a # are comments.

sydney_map.txt
# Sydney road network - a simplified but real-shaped map.## NODE   id  lat  lng  display name# EDGE   from  to  minutes  road_type      (works both ways)# ONEWAY from  to  minutes  road_type      (one direction only)# FERRY  from  to  minutes                 (not a road; cars cannot use it)# TRAFFIC period road_type multiplier# INCIDENT from to period effect multiplier reason#   period is free, am_peak, pm_peak, weekend, or all#   effect is closed or slow; slow multiplies the already traffic-adjusted timeNODE circular_quay   -33.8610  151.2105  Circular QuayNODE the_rocks       -33.8593  151.2085  The RocksNODE wynyard         -33.8657  151.2062  WynyardNODE town_hall       -33.8731  151.2065  Town HallNODE darling_harbour -33.8737  151.2010  Darling HarbourNODE central         -33.8830  151.2065  CentralNODE surry_hills     -33.8845  151.2110  Surry HillsNODE redfern         -33.8925  151.1988  RedfernNODE glebe           -33.8797  151.1855  GlebeNODE newtown         -33.8960  151.1794  NewtownNODE marrickville    -33.9110  151.1550  MarrickvilleNODE ashfield        -33.8886  151.1257  AshfieldNODE burwood         -33.8770  151.1040  BurwoodNODE strathfield     -33.8720  151.0940  StrathfieldNODE olympic_park    -33.8470  151.0680  Olympic ParkNODE parramatta      -33.8150  151.0000  ParramattaNODE ryde            -33.8150  151.1050  RydeNODE macquarie_park  -33.7770  151.1200  Macquarie ParkNODE epping          -33.7730  151.0820  EppingNODE chatswood       -33.7969  151.1803  ChatswoodNODE north_sydney    -33.8404  151.2073  North SydneyNODE milsons_point   -33.8465  151.2120  Milsons PointNODE cremorne        -33.8290  151.2270  CremorneNODE mosman          -33.8290  151.2410  MosmanNODE manly           -33.7969  151.2870  Manly...  (48 more lines: the rest of the roads, the closure, and the traffic table)

Traffic is the point

A road takes its minutes multiplied by a number that depends on the time of day AND what kind of road it is. Motorways clog worst: 2.6x at morning peak, while a local street is only 1.2x. That is why the best route changes.

One road is shut

The Spit Bridge is closed for repairs, so Manly cannot be driven to at all. That is not a bug in your code — it is why job 6 finds two separate pieces of Sydney, and why the ferry matters.

Live events reroute

At morning peak an M4 breakdown doubles one link; at evening peak an airport crash slows a tunnel and lane resurfacing closes a north-shore road. The map shows orange for slow and red for closed, and your graph must use exactly those conditions.

The Fifteen Jobs

Every one of these is something you have already built in class. The week it came from is written on each card.

1

load_map

week 4

Read sydney_map.txt into Python.

returns

nodes, roads, ferries, traffic, incidents

watch out for

33 suburbs, 51 roads, 2 ferries, 4 live events. A NODE name can contain spaces.

2

is_closed

week 4

Is this road shut in these travel conditions?

returns

True or False

watch out for

A closure can apply all day or only at one peak, in BOTH directions.

3

incident_multiplier

week 4

How much does a temporary incident slow this road right now?

returns

a multiplier, or 1.0

watch out for

Read the event period. A morning breakdown should disappear at free flow.

4

travel_time

week 7

How long does this road take right now?

returns

minutes × traffic × incident multiplier

watch out for

A motorway at am_peak takes 2.6×; a live breakdown can multiply it again.

5

build_graph

weeks 5 and 7

Turn the map into a graph for one time of day.

returns

dict: id -> list of (neighbour, minutes, kind)

watch out for

Every suburb needs a key. Skip closed roads. ONEWAY goes one way only.

6

reachable

week 6

Which suburbs can I drive to at all?

returns

sorted list of ids

watch out for

Flood fill. With the Spit Bridge shut, Manly reaches only itself.

7

separate_areas

week 6

How many disconnected pieces is Sydney in?

returns

list of areas

watch out for

By road: two pieces, 32 suburbs and Manly. With ferries: one.

8

fewest_changes

week 5

The route through the fewest suburbs.

returns

list of ids, or []

watch out for

Plain BFS. It ignores how long each road takes - and it is often slower.

9

time_map

weeks 6 and 7

The quickest time to EVERY suburb.

returns

(best, came_from)

watch out for

Dijkstra with heapq, no goal test. sources is a list, so several starts work.

10

rebuild

weeks 5 to 7

Turn came_from into a route.

returns

list of ids, or []

watch out for

Walk backwards from the goal, then turn the list around.

11

quickest_route

week 7

The fastest way from A to B.

returns

(route, minutes) or ([], None)

watch out for

Call time_map. Do not write a second search.

12

route_via

week 7

Collect a parcel on the way.

returns

(route, minutes) or ([], None)

watch out for

Call quickest_route twice and glue. Do not list the stop twice.

13

within_minutes

week 7

Where can I get before my meeting?

returns

sorted list of ids

watch out for

Filter the times dict. 11 suburbs are within 20 minutes at morning peak.

14

nearest_of

weeks 6 and 7

Which hospital is closest?

returns

(winner, route, minutes)

watch out for

One search from where you are, then pick the cheapest target.

15

Navigator

weeks 2 and 3

The system itself: load once, answer many questions.

returns

a class with route() and describe()

watch out for

Cache each graph you build. serve.py calls this class and nothing else.

How To Run It

bash
# 1. keep all six files in one folder#      router.py          <- the only file you edit#      sydney_map.txt     <- the map#      check_router.py    <- your marker#      serve.py           <- the local server#      app.html           <- the interactive map interface#      README.md          <- the instructions# 2. after every job, run the markerpython3 check_router.py# 3. when it says 15/15, start your navigatorpython3 serve.py# 4. open this in a browser#      http://localhost:8000

What finished looks like

plain
Sydney Navigator - checking your work  [x] TODO 1   load_map  [x] TODO 2   is_closed  [x] TODO 3   incident_multiplier  [x] TODO 4   travel_time  [x] TODO 5   build_graph  [x] TODO 6   reachable  [x] TODO 7   separate_areas  [x] TODO 8   fewest_changes  [x] TODO 9   time_map  [x] TODO 10  rebuild  [x] TODO 11  quickest_route  [x] TODO 12  route_via  [x] TODO 13  within_minutes  [x] TODO 14  nearest_of  [x] TODO 15  Navigator  15/15 finished  All done. Now run:  python3 serve.py
If the page is blank: The map is a live console: trip controls, route modes, supplied events, and your own temporary events all call router.py. If a result fails, the Result box shows the Python error; then run check_router.py.

Your File

This is router.py as you receive it. Every TODO tells you what goes in, what comes out, and which week it came from.

router.py

python
"""Sydney Navigator - your file.Fifteen jobs. Every one of them is something you have already built:  week 4   dict, a dict inside a dict, reading a text file  week 5   a graph, a frontier, BFS  week 6   flood fill, separate pieces, a distance map, several starts at once  week 7   weights, Dijkstra, heapq, going via a stopRun  python3 check_router.py  after each job. It tells you exactly whichone is not finished yet. When all fifteen pass, run  python3 serve.pyand open http://localhost:8000 to drive your own routing engine."""from heapq import heappush, heappopPERIODS = ["free", "am_peak", "pm_peak", "weekend"]def load_map(path):    # TODO 1  -  read sydney_map.txt into Python (week 4)    # Return five things, in this order:    #   nodes     dict: id -> {"id", "lat", "lng", "name"}    #   roads     list of {"from", "to", "minutes", "kind", "oneway"}    #   ferries   list of the same shape, with kind "ferry" and oneway False    #   traffic   dict: period -> {road kind -> multiplier}    #   incidents list of {"from", "to", "period", "effect", "multiplier", "reason"}    #    # Ignore blank lines and anything after a '#'.    # A NODE line is:  NODE  id  lat  lng  the display name, which may have spaces    return {}, [], [], {}, []def is_closed(road, incidents, period):    # TODO 2    # Return True for an active closed incident, in EITHER direction.    return Falsedef incident_multiplier(road, incidents, period):    # TODO 3  -  temporary congestion (week 4)    # Return the product of every active slow incident multiplier for this    # road, in either direction. Return 1.0 when there are none.    return 1.0def travel_time(road, traffic, period, incidents):    # TODO 4  -  this is where the traffic happens (week 7)    # Return base minutes × traffic multiplier × incident_multiplier.    # A motorway at am_peak is 2.6 times its free-flow time.    return 0def build_graph(nodes, links, traffic, period, incidents):    # TODO 5  -  turn the map into a graph (weeks 5 and 7)    # Return a dict: node id -> list of (neighbour, minutes, kind)    #   - every node needs a key, even one with no roads    #   - skip roads closed in this period    #   - a normal road goes in BOTH directions, a ONEWAY only one (week 7)    return {}def reachable(graph, start):    # TODO 6  -  flood fill (week 6)    # Return a sorted list of every node you can drive to from start.    return []def separate_areas(graph):    # TODO 7  -  how many pieces is the map in? (week 6)    # Return a list of areas. Each area is the list reachable() gave back.    # Walk the node ids in sorted order and skip any already claimed.    return []def fewest_changes(graph, start, goal):    # TODO 8  -  BFS (week 5)    # Return the route that passes through the FEWEST suburbs,    # ignoring how long each road takes. [] if there is no way through.    return []def time_map(graph, sources):    # TODO 9  -  Dijkstra with heapq, and no goal (weeks 6 and 7)    # Input: sources is a LIST. Every source starts at 0 minutes.    # Return: (best, came_from)    #   best      dict: node -> quickest minutes to reach it    #   came_from dict: node -> the node you arrived from    return {}, {}def rebuild(came_from, goal):    # TODO 10  -  walk backwards (weeks 5 to 7)    # Return the route from the start to goal, or [] if goal is not in there.    return []def quickest_route(graph, start, goal):    # TODO 11    # Return (route, minutes), or ([], None) when there is no way through.    # Use time_map. Do not write another search.    return [], Nonedef route_via(graph, start, via, goal):    # TODO 12  -  pick someone up on the way (week 7)    # Return (whole route, total minutes), or ([], None).    # Call quickest_route twice. Do not list the via stop twice.    return [], Nonedef within_minutes(times, budget):    # TODO 13  -  what can I reach before my meeting? (week 7)    # Input: times is the dict time_map() returned.    # Return: a sorted list of every node reachable in budget minutes or less.    return []def nearest_of(graph, start, targets):    # TODO 14  -  which hospital is closest? (weeks 6 and 7)    # Return (winner, route to it, minutes), or (None, [], None).    # If two are equally quick, pick the smaller id so the answer is stable.    return None, [], Noneclass Navigator:    """TODO 15  -  the system itself (weeks 2 and 3).    Load the map ONCE in __init__, then answer many questions about it.    Building the graph is slow, so keep the ones you have already built    in self._cache and hand them back next time.    """    def __init__(self, path):        # TODO 15a: load the map into self.nodes, self.roads, self.ferries,        #           self.traffic, self.incidents, and start an empty cache.        pass    def graph(self, period, include_ferries=False):        # TODO 15b: build the graph for this period, or return the cached one.        # With include_ferries, the links are self.roads + self.ferries.        return {}    def route(self, start, goal, period, via=None, include_ferries=False):        # TODO 15c: return {"path": [...], "minutes": number or None,        #                   "legs": self.describe(...)}        # Round the minutes to one decimal place.        # Use route_via when via is given, otherwise quickest_route.        return {"path": [], "minutes": None, "legs": []}    def describe(self, path, period, include_ferries=False):        # TODO 15d: turn a route into a list of legs, one per hop:        #   {"from", "to", "minutes", "kind"}        # Look each hop up in the graph to find how long it took and what        # kind of road it was.        return []

Marking

whatpoints
check_router.py says 15/1540
serve.py starts and the map draws20
all four modes work in the browser20
your code is readable: no repeated searches10
one extension of your own10
total100

Pick One Extension

Ten of the marks are for one thing you added yourself. Choose one of these, or invent your own. Before you write anything, say which of the fifteen functions it touches.

Add a suburb

Add yourself to sydney_map.txt with roads to two neighbours. Nothing in router.py should need changing.

Close a road and see what happens

Add an INCIDENT on the Harbour Bridge and watch how far the north shore moves away.

Avoid motorways

Add a mode that refuses motorway roads, like a driver with no toll tag.

Cheapest, not quickest

Give motorways a toll and let the user choose to minimise money instead of minutes.

Two people meeting

Given two starts, find the suburb where the LATER of the two arrivals is earliest.

Show the second-best route

Ban one road from the best route, search again, and draw both.

Where Everything Came From

weekwhat you learntwhere it shows up here
2 · 3class, object, selfjob 15, the Navigator itself
4dict, dict inside dict, reading a filejobs 1 to 3
5graph, queue, BFSjobs 6 and 8
6flood fill, separate pieces, many startsjobs 6, 7 and 9
7weights, Dijkstra, heapq, going viajobs 3, 4, 9 and 11 to 14
The point: There is not one new idea in this project. What is new is the size. A real system is not made of clever parts — it is made of small parts you already understand, wired together carefully.
— Homework 3 End —