Real-time London Underground: Skapa din egen karta

in
6–9 minuter

Jag bor ingenstans i närheten av London, pendlar inte med tunnelbanan där och har inget yrkesmässigt skäl att veta när nästa Piccadilly line-tåg går från King’s Cross. Jag byggde det ändå, för att Transport for London råkar driva ett av de bästa gratis öppna API:er jag stött på, och jag ville se hur långt en enda statisk HTML-sida — ingen backend, ingen databas, ingen serverkod alls — kunde ta mig.

Resultatet är en live-karta över varenda station på Londons tunnelbana och DLR, med realtidsavgångar designade som de amber-LED-skyltar som hänger över perrongerna, aktuell linjestatus, lokalt väder, hur fullt det är på en station just nu jämfört med det normala mönstret, och närmaste Santander Cycles-dockningsstation. Här är hur det fungerar, och vad jag sprang på längs vägen, ifall du vill bygga något liknande med en annan stads öppna data.

Vad TfL:s öppna data faktiskt ger dig

TfL:s enhetliga API (api.tfl.gov.uk) är gratis, kräver inga betaluppgifter, och täcker mycket mer än de flesta förväntar sig:

  • StopPoint — varje station, dess koordinater, vilka linjer som trafikerar den, tillgänglighetsinfo och zonindelning
  • Arrivals — live nedräkningsprognoser per station, ner till destination och plattform
  • Line/Status — realtidsstatus för störningar per linje, plus en läsbar text som förklarar varför när något är fel
  • Crowding — hur fullt det är på en station just nu, och en historisk profil för vad som är typiskt per veckodag och tid
  • BikePoint — live tillgänglighet vid varenda Santander Cycles-dockningsstation
  • Road, Journey Planner, Air Quality — och mer, som jag inte använde här

Allt kommer tillbaka som JSON över vanlig HTTPS med CORS aktiverat, vilket är detaljen som gör hela det här upplägget möjligt: en webbläsare kan anropa de här endpointsen direkt, utan någon server emellan.

Arkitektur: ingen backend, medvetet

Hela sajten är tre HTML-sidor (karta, sök, om), en delad CSS-fil och en delad JavaScript-fil. Inget byggsteg, inget ramverk, ingen server-side-rendering. All data — stationslista, avgångar, trängsel, väder — hämtas klientsidan, i besökarens egen webbläsare, i det ögonblick sidan laddas.

/index.html → interaktiv Leaflet-karta
/search.html → textsökning + "använd min plats"
/about.html → credits och hur det fungerar
/common.js → delad hämtnings-/renderingslogik
/styles.css → delad styling

Det håller hostingen enkel (det är bara statiska filer bakom nginx med ett Let’s Encrypt-certifikat) och betyder att det i praktiken inte finns något för mig att driva, patcha eller övervaka. Om TfL:s API skulle ligga nere degraderar sajten snyggt och visar ett felmeddelande — det finns ingen cache som kan bli inaktuell, inget cron-jobb som kan misslyckas tyst.

Kartan: en markör per station, inte per plattform

Det uppenbara första försöket — plotta varenda StopPoint TfL returnerar — blir rörigt. TfL:s data modellerar en station som flera separata poster: en för själva stationen, en per plattform, en per entré. Att filtrera på stopType === 'NaptanMetroStation' kommer nära, men bytesstationer returnerar ändå ibland dubbletter för samma fysiska plats. Jag löste det genom att avrunda koordinater till tre decimaler och slå ihop poster som hamnar på samma nyckel, och slå ihop deras linjelistor samtidigt.

För stationer som trafikeras av flera linjer räcker inte en enfärgad markör för att berätta hela historien, så varje markör är en liten cirkel renderad med en CSS conic-gradient, uppdelad i lika stora tårtbitar — en per linje, i respektive linjes officiella TfL-färg:

js

function buildLinePieBackground(lineIds) {
const colors = lineIds.map(id => lineColors[id] || '#888888');
const unique = [...new Set(colors)];
if (unique.length <= 1) return unique[0] || '#888888';
const step = 360 / unique.length;
const stops = unique
.map((c, i) => `${c} ${i * step}deg, ${c} ${(i + 1) * step}deg`)
.join(', ');
return `conic-gradient(from 0deg, ${stops})`;
}

King’s Cross St. Pancras blir ett litet hjul av fem-sex färger istället för en godtycklig enskild färg — en liten detalj, men den gör kartan genuint lättare att läsa på en snabb blick.

Avgångstavlan: designad för att se ut som den riktiga

Istället för en generisk lista är avgångspanelen stylad efter de amber-LED-skyltar man faktiskt ser på perrongerna: monospace-typsnitt, mörk bakgrund, en glödande text-shadow, en live-klocka som tickar längst ner. Avgångarna grupperas per riktning (utläst ur TfL:s platformName, t.ex. ”Westbound – Platform 1”), och tavlan uppdaterar sig tyst var 30:e sekund så den beter sig som en riktig skylt istället för en ögonblicksbild som blir inaktuell så fort man slutar titta.

Den intressanta buggen: TfL:s crowding-API är oense med sig självt

Det här är delen värd att dela om du planerar att använda TfL:s Crowding-endpoints själv, för inkonsekvensen är inte dokumenterad någonstans jag kunde hitta.

TfL exponerar två relaterade endpoints:

  • GET /crowding/{naptanId}/Live — trängsel just nu
  • GET /crowding/{naptanId} — en historisk profil, uppdelad per veckodag och 15-minutersintervall

Båda returnerar ett fält som låter som samma sak — percentageOfBaseline kontra percentageOfBaseLine (lägg märke till versaliseringen) — och jag antog att de skulle bete sig likadant. Det gör de inte:

  • Live returnerar en decimal mellan 0 och 1 (0.3478803 betyder 34,8 %)
  • Profilen returnerar också en decimal mellan 0 och 1, men identifierar dessutom varje veckodag som en trebokstavskod i versaler ("FRI"), medan mitt första försök jämförde mot fulla dagsnamn ("Friday")

Båda missarna misslyckades tyst — inget fel kastades, ingen krasch. Koden föll bara alltid tillbaka på ”ingen data för den här tiden” utan att klaga, vilket är den mest irriterande sortens bugg: inget går sönder högljutt, den ger bara tyst fel (eller tomt) svar.

Fixen var trist så fort jag faktiskt kunde se svaret:

js

// Live: decimal 0-1, t.ex. 0.3478803
return { percentage: data.percentageOfBaseline * 100 };
// Profil: veckodagen är en trebokstavskod, inte ett fullt namn
const dayNames = ['SUN', 'MON', 'TUE', 'WED', 'THU', 'FRI', 'SAT'];
const dayEntry = profile.daysOfWeek.find(d => d.dayOfWeek === dayNames[now.getDay()]);

Lärdomen, om du ska integrera något myndighets- eller kollektivtrafik-API för öppna data: öppna webbläsarens Network-flik och läs det faktiska svaret innan du litar på ett fältnamn eller en skala. Dokumentation ligger efter verkligheten oftare än man tror, och ”det returnerade 200 OK utan fel” säger ingenting om huruvida siffrorna inuti betyder det du antar att de betyder.

Cykelparkering i närheten och ”hitta min närmaste station”

Två mindre funktioner som visade sig bli nästan gratis när väl grunddatan för stationerna fanns på plats:

  • BikePoint-närhet — TfL:s /BikePoint-endpoint returnerar varenda Santander Cycles-dockningsstation med live antal cyklar/lediga lås. En enkel haversine-avståndsberäkning mot den just visade stationens koordinater hittar den närmaste (om någon finns inom 500 m).
  • ”Använd min plats” på sök-sidan anropar webbläsarens egen navigator.geolocation-API, och kör sedan samma haversine-beräkning mot hela stationslistan för att visa de åtta närmaste stationerna — ingen server-tur-och-retur, ingen tredjeparts geokodningstjänst behövs.

Rate limits: skaffa en gratis nyckel innan du behöver den

TfL:s API fungerar helt utan registrering, men anonyma anrop begränsas till ungefär 50 per timme — delat mellan alla som anropar API:et från samma IP. Det låter som gott om marginal tills man räknar på en sida som hämtar avgångar, linjestatus, trängsel (två gånger) och cykeldata vid varje klick, och sedan upprepar allt det där var 30:e sekund medan en tavla är öppen. Det blir snabbt gott och väl över 50 anrop under de första minuterna.

Att registrera sig för en gratis nyckel på api-portal.tfl.gov.uk tar ungefär två minuter och höjer gränsen till 500 anrop per minut — samma data, samma endpoints, bara ett mycket högre tak. Värt att göra innan man sitter och felsöker mystiskt tomma svar som egentligen bara är tysta 429:or.

Driftsättning

Inget exotiskt här, vilket är hela poängen:

  • DNS via Route53, en enda A-post
  • TLS via acme.sh med dns_aws-pluginet för en DNS-01-challenge (behöver inte exponera port 80)
  • Let’s Encrypt som CA (jag råkade ut för ett orelaterat driftavbrott hos ZeroSSL under uppsättningen, vilket blev en bra påminnelse om att sätta --server letsencrypt explicit istället för att lita på acme.sh:s default)
  • Ett vanligt nginx-serverblock som serverar statiska filer, TLS termineras med acme.sh --install-cert-reload-hooken så förnyelser automatiskt startar om nginx

Om du vill bygga något liknande

Mönstret generaliserar bra bortom London:

  1. Hitta en kollektivtrafikmyndighet eller stad med ett publikt, CORS-aktiverat JSON-API (många har det — kolla efter ”öppna data”-portal tillsammans med din lokala kollektivtrafikoperatör)
  2. Börja med den minsta möjliga funktionen — en karta med markörer — innan du lägger till livedata
  3. Hämta livedata klientsidan och hoppa över backend helt om API:et stödjer CORS; det förenklar hostingen dramatiskt och tar bort en hel kategori av saker som kan gå sönder
  4. Läs faktiska API-svar i Network-fliken istället för att lita på dokumentation eller egna antaganden om fältnamn och skalor
  5. Registrera en gratis API-nyckel tidigt, även om du inte strikt behöver den än — den kostar ingenting och sparar en felsökningskväll senare

Total kostnad för projektet: priset för ett domännamn plus den bråkdel av en liten VPS det använder, vilket för en handfull statiska filer avrundas ner till ingenting. Total tid: en rad kvällar, mestadels tillbringade med att stirra på Network-flikens svar och försöka förstå varför en siffra som borde säga ”35 %” envisades med att säga ”0 %”.

Om du vill utforska själv ligger sajten live på tube.hakdah.com — fulla credits och datakällor finns på om-sidan.

Comments

Lämna ett svar

Upptäck mer från Håkan Dahlström

Prenumerera nu för att fortsätta läsa och få tillgång till hela arkivet.

Fortsätt läsa