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.

33

suburbs on the map

53

roads and ferries

4

times of day

14

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.

Circular QuayThe RocksWynyardTown HallDarling HarbourCentralSurry HillsRedfernGlebeNewtownMarrickvilleAshfieldBurwoodStrathfieldOlympic ParkParramattaRydeMacquarie ParkEppingChatswoodNorth SydneyMilsons PointCremorneMosmanManlyBondi JunctionBondi BeachRandwickCoogeeMascotSydney AirportRockdaleHurstville
46 minutes, 9 suburbslocalmain roadmotorwayferryclosed today

You write

router.py

Fourteen functions. That is the whole engine.

You are given

sydney_map.txt · check_router.py · serve.py

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 import you need is heapq. No pip, no libraries, no internet.
  • Write each search once. Job 10, 11, 12 and 13 must all call job 8. 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 reason                  (this road is closed)NODE 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.

The Fourteen 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, 1 closure. A NODE name can contain spaces.

2

is_closed

week 4

Is this road shut today?

returns

True or False

watch out for

A closure blocks the road in BOTH directions.

3

travel_time

week 7

How long does this road take right now?

returns

minutes x the traffic multiplier

watch out for

A motorway at am_peak takes 2.6 times its free-flow time.

4

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.

5

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.

6

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.

7

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.

8

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.

9

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.

10

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.

11

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.

12

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.

13

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.

14

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. put all four files in one folder#      router.py          <- the only file you edit#      sydney_map.txt     <- the map#      check_router.py    <- your marker#      serve.py           <- the website# 2. after every job, run the markerpython3 check_router.py# 3. when it says 14/14, 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   travel_time  [x] TODO 4   build_graph  [x] TODO 5   reachable  [x] TODO 6   separate_areas  [x] TODO 7   fewest_changes  [x] TODO 8   time_map  [x] TODO 9   rebuild  [x] TODO 10  quickest_route  [x] TODO 11  route_via  [x] TODO 12  within_minutes  [x] TODO 13  nearest_of  [x] TODO 14  Navigator  14/14 finished  All done. Now run:  python3 serve.py
If the page is blank: serve.py never fails on its own. If a button does nothing or the map is empty, it is router.py. The browser shows you the exact Python error in the Result box — read it, 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.Fourteen 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 fourteen 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", "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):    # TODO 2    # Return True when this road matches an incident, in EITHER direction.    return Falsedef travel_time(road, traffic, period):    # TODO 3  -  this is where the traffic happens (week 7)    # Return the road's minutes multiplied by the right traffic number.    # A motorway at am_peak is 2.6 times its free-flow time.    return 0def build_graph(nodes, links, traffic, period, incidents):    # TODO 4  -  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 any closed road    #   - a normal road goes in BOTH directions, a ONEWAY only one (week 7)    return {}def reachable(graph, start):    # TODO 5  -  flood fill (week 6)    # Return a sorted list of every node you can drive to from start.    return []def separate_areas(graph):    # TODO 6  -  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 7  -  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 8  -  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 9  -  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 10    # 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 11  -  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 12  -  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 13  -  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 14  -  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 14a: 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 14b: 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 14c: 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 14d: 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 14/1440
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 fourteen 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 14, the Navigator itself
4dict, dict inside dict, reading a filejobs 1 to 4
5graph, queue, BFSjobs 5 and 7
6flood fill, separate pieces, many startsjobs 5, 6 and 8
7weights, Dijkstra, heapq, going viajobs 3, 8, 10 to 13
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 —