The project is currently live at 🚇🚇🚇.ml.
The new tube map will be used when the Elizabeth Line is fully open and the map is in its final form. The map update will take a lot of effort so I won't do it until it won't need to be done again for ages.
Contents: Minimum Viable Product • Information sources • Next Steps • Rough Roadmap • Style • Logic Explained • Extended To Do List
I made this project because I wanted to see how much of the TfL network I've been on...and because ticking off stations on a physical map was too easy.
I am open to new ideas for names.
- The map displays and is faded out initially.
- Any previously added stations are retrieved from
localStorage
and added to the map. - The input box makes the station fade in, stores it in
localStorage
, and updates the line to be complete. - You can upload a CSV file (downloaded from the Oyster website) to mass-add stations.
- You can see a progress bar for how many stations you've visited.
- Bus data is also saved in case of future use.
I stole the Oyster SVG Map from: https://tfl.gov.uk/Modules/TubeMap?nightMode=false
(I spent hours changing the IDs on the map.svg so that I could manipulate them. Perhaps longer than I should have. I'm fragile, please don't say if there was an easier way as I may collapse.)
stations.json and lines.json were adapted from @paulcuth's and @Lissy93's gists, as well as a TfL Source.
(Some of the three-letter station codes were non-existent so I made them up. I didn't keep track of which are made up. I'm sorry.)
To keep it open source, the station labels on the map are Hammersmith One
—a copyright friendly font alternative to the official Johnston
.
The body is currently Roboto
. This may change in future.
Borrowed from oobrien.com.
Line | Colour Hex |
---|---|
Bakerloo | #B36305 |
Cable Car | #E21836 |
Central | #E32017 |
Circle | #FFD300 |
District | #00782A |
DLR | #00A4A7 |
Elizabeth | #7156A5 |
Hammersmith & City | #F3A9BB |
Jubilee | #A0A5A9 |
Metropolitan | #9B0056 |
Northern | #000000 |
Overground | #EE7C0E |
Piccadilly | #003688 |
Trams | #84B817 |
Victoria | #0098D4 |
Waterloo & City | #95CDBA |
Where possible, all the colours are one of the above.
In data.ts
, there are two variables for us to access: lines
and stations
.
stations
is a simple object that holds the station name (as it appears on an Oyster statement) as the key, and it's corresponding unique three-letter code as the value.
lines
holds the information about the line and it's stations, primarily: if it has branches or not, and the order the stations appear it. North to South, East to West (with one exception: the Overground line from Liverpool Street to Enfield Town/Chesnut/Chingford.)
The Bakerloo Line is represented like this:
bakerloo: {
branch: false,
line: 'bakerloo',
stations: ['HAW', 'SKT', 'NWM', 'WEM', 'SPK', 'HSD', 'WJN', 'KGN', 'QPK', 'KPK', 'MDV', 'WAR', 'PAD', 'ERB', 'MYB', 'BST', 'RPK', 'OXC', 'PIC', 'CHX', 'EMB', 'WLO', 'LAM', 'ELE']
}
with no branches, the stations are in an array in N-S order.
The Picadilly Line branches at one the southern end, so is represented like so:
piccadilly: {
branch: 'true',
line: 'piccadilly',
top: [
['CFS', 'OAK', 'SGT', 'AGR', 'BGR', 'WGN', 'TPL', 'TPL', 'MNR', 'FPK', 'ARL', 'HRD', 'CRD', 'KXX', 'RSQ', 'HOL', 'COV', 'LSQ', 'PIC', 'GPK', 'HPC', 'KNB', 'SKN', 'GRD', 'ECT', 'BCT', 'HMD', 'TGR']
],
bottom: [
['TGR', 'ACT', 'ECM', 'NEL', 'PRY', 'ALP', 'STN', 'SHL', 'SHR', 'RLN', 'ETE', 'RUM', 'RUI', 'ICK', 'HDN', 'UXB'],
['TGR', 'ACT', 'SEL', 'NFD', 'BOS', 'OST', 'HNE', 'HNC', 'HNW', 'HTX', 'HRC', 'HTF', 'HTC'],
['TGR', 'ACT', 'SEL', 'NFD', 'BOS', 'OST', 'HNE', 'HNC', 'HNW', 'HTX', 'HRC', 'HRV']
]
}
The stations
array is now replaced by arrays top
and bottom
. The top
array holds all the stations from north to south up to Turnham Green (TGR), the station just before the line splits. The bottom
array holds all the stations from Turnham Green to their termini. As it splits futher down in the southern end at the Heathrow stations, the arrays contain repeated data as if each end-point were their own branch.
top
and bottom
are always named so, even if they could be more aptly named left
and right
.
To interpret these arrays, there's the updateLineSegs()
function.
let stnCodes = findVisCodes(usrData('get', 'stations'));
First we get an array of station codes that the user has visited. usrData('get', 'stations')
retrieves a list of all the stations a user has visited and returns the array. findVisCodes()
takes in the array and outputs a new array of only the three-letter codes.
In future I might change it so the stations are saved as codes.
let data = {
bakerloo: 0,
central: 0,
piccadilly: 0,
jubilee: 0,
metropolitan: 0,
victoria: 0,
northern: 0,
circle: 0,
'hammersmith-city': 0,
district: 0,
elizabeth: 0,
overground: 0,
'waterloo-city': 0,
'cable-car': 0,
dlr: 0,
OSI: 0
};
This is for the stats. We set up the object to assume the user has visited none of them.
for (const l in lines) {
const lineObj = lines[l];
For each of the lines in the line
variable we have in data.ts
, explose the line's object to lineObj
.
if (lineObj['branch']) {
We then check to see if this line has branches. If it does we set up two functions: top()
and bottom()
which see if the corresponding branch has visited stations in it.
function top() {
let active = false;
lineObj['top'].forEach((a: string[]) => {
a.forEach((s) => {
if (stnCodes.includes(s)) {
active = true;
} else {
// Hasn't been visited.
}
});
});
return active;
}
function bottom() {
let active = false;
lineObj['bottom'].forEach((a: string[]) => {
a.forEach((s) => {
if (stnCodes.includes(s)) {
active = true;
} else {
// Hasn't been visited.
}
});
});
return active;
}
If cycles through each station in each branch in the top
and bottom
arrays and if the previously retrieved station codes contain any of them then the branch is deemed active.
We then have a function called complete(top, bottom)
which takes in Booleans
to fill in the line segements depending on if one of both of the sections are active.
if (top && bottom) {
let total = 0; // For the stats saved earlier
lineObj['top'].forEach((e: string[]) => {
let first = 100; // The first station will never be at array position 100.
e.forEach((a) => {
// For each array in top.
const index = e.indexOf(a); // Get index of station in array.
if (stnCodes.includes(a)) {
// If the station has been visited by the user.
total++; // Increase the total.
if (index <= first) {
// If it's earlier than the current earliest station.
first = index; // Make it the earliest.
}
}
});
for (let i = first; i < e.length; i++) {
// Between the first station visited and the last on the branch.
$(`#lul-${lineObj['line']}_${e[i]}-${e[i + 1]}`).addClass('visible'); // Make the segment between the current station and the next visible.
// Stations have id #lul-[line name]_[stnCode]-[stnCode].
// e.g. #lul-central_OXC-BST
}
});
lineObj['bottom'].forEach((e) => {
let last = 0;
e.forEach((a: string) => {
const index = e.indexOf(a);
if (stnCodes.includes(a)) {
total++;
if (index >= last) {
last = index;
}
}
});
for (let i = 0; i < last; i++) {
$(`#lul-${lineObj['line']}_${e[i]}-${e[i + 1]}`).addClass('visible');
}
});
// Same is done for the bottom branches, except we find the last station visited.
data[lineObj['line']] = data[lineObj['line']] + total; // Update total in data object.
}
If just the top
or bottom
branches have been visited, we essneitally combine the above to find the first and last visited stations and loop through the segments between them to make them visible.
Finally we call complete(top(), bottom())
passing in the earlier functions and their returned Booleans
.
If there are no branches we copy the code of filling in the line segments as if only one branch were visited. Finding the first and last station visited and looping between their indexes.
Finally we call the updateStats()
function, passing in our data
variable which should now be complete for each line.
Each line in data.ts
is sometimes split into segments, for example the DLR is explit into three segments as it is a mini-network in itself. TfL Rail (soon to be the Elizabeth Line) acts as two separate lines as currently it is disconnected.
- PWA
- Hover on line/stations for emphasis and information (number of visits, last visit)
- Challenges/Achievements:
- Streak: visit a new station every month
- 272 stations; 272/12 = 22 years to do them all
- Travel through the whole of each line
- Stations at either end have to be visited
- 11 lines + overground + trams
- Visit every station on a line
- Use every line
- Use every branch of the Northern line?
- Or another fun idiosyncrasy of TfL
- Use every overground line
- Have gotten the first/last train of the day of that line/on TfL
- Streak: visit a new station every month
- Map of Riverboat services
- Leaderboards?
- Would require authentication of CSVs
- Complicated:- See if their journeys were feasible. Times taken + whether the line/stations were open on that day/time + gap between entries
- Easier:- User history. Not too many visited too often. Account age and number of stations visited.
- Easiest:- Honour system
- Can be turned on/off by users
- Rankings of users stats
- Would require authentication of CSVs