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
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.
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.pyis yours to edit. Everything else is provided. - Do the jobs in order. Run
python3 check_router.pyafter 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 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.
load_map
week 4Read 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.
is_closed
week 4Is this road shut today?
returns
True or False
watch out for
A closure blocks the road in BOTH directions.
travel_time
week 7How 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.
build_graph
weeks 5 and 7Turn 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.
reachable
week 6Which 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.
separate_areas
week 6How many disconnected pieces is Sydney in?
returns
list of areas
watch out for
By road: two pieces, 32 suburbs and Manly. With ferries: one.
fewest_changes
week 5The 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.
time_map
weeks 6 and 7The 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.
rebuild
weeks 5 to 7Turn came_from into a route.
returns
list of ids, or []
watch out for
Walk backwards from the goal, then turn the list around.
quickest_route
week 7The fastest way from A to B.
returns
(route, minutes) or ([], None)
watch out for
Call time_map. Do not write a second search.
route_via
week 7Collect 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.
within_minutes
week 7Where 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.
nearest_of
weeks 6 and 7Which hospital is closest?
returns
(winner, route, minutes)
watch out for
One search from where you are, then pick the cheapest target.
Navigator
weeks 2 and 3The 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
# 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:8000What finished looks like
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.pyYour 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
"""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
| what | points |
|---|---|
| check_router.py says 14/14 | 40 |
| serve.py starts and the map draws | 20 |
| all four modes work in the browser | 20 |
| your code is readable: no repeated searches | 10 |
| one extension of your own | 10 |
| total | 100 |
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
| week | what you learnt | where it shows up here |
|---|---|---|
| 2 · 3 | class, object, self | job 14, the Navigator itself |
| 4 | dict, dict inside dict, reading a file | jobs 1 to 4 |
| 5 | graph, queue, BFS | jobs 5 and 7 |
| 6 | flood fill, separate pieces, many starts | jobs 5, 6 and 8 |
| 7 | weights, Dijkstra, heapq, going via | jobs 3, 8, 10 to 13 |