Back to Map Tracker

HSL-LIVE Changelog

Continuous updates and live tracking releases for HSL Trams & Buses

All notable changes to this project will be documented in this file.

v1.2.0 2026-10-07

Added


v1.1.0 2026-10-03

Added


v1.0.0 2026-10-03

Removed


v0.82.0 2026-10-03

Added


v0.81.1 2026-10-03

Fixed


v0.81.0 2026-10-03

Added


v0.80.0 2026-10-03

Added


v0.79.0 2026-10-03

Added

Fixed


v0.78.0 2026-10-03

Changed


v0.77.0 2026-10-03

Changed


v0.76.2 2026-10-03

Fixed

Changed


v0.76.1 2026-10-03

Fixed


v0.76.0 2026-10-02

Changed


v0.75.4 2026-09-25

Fixed


v0.75.2 2026-09-22

Fixed

Changed


v0.75.0 2026-09-19

Added

So the spacing is measured rather than estimated. Every position names the stop its vehicle is heading for, and the moment that name changes is the moment the vehicle left the stop. The backend now times every vehicle out of every stop, and each position carries a new hw object: how far the vehicle is running behind the one ahead of it on the same line and direction, which is exactly how long a rider at that stop waited between the two. Between stops a hole is seen opening before the tram behind reaches the next one: the tram ahead left that stop so long ago, and this one has not got there yet, so the gap is at least that long and the payload says it is a lower bound.

What the gap *should* be comes from the same two passages. Each vehicle reports how late it is, so each passage also says when that vehicle was due, and the difference between two due times is the timetabled spacing of the pair, at that stop, whichever variant of the route either is running. A line's headway is the median of that across the pairs currently on it — one say per pair, so a cancelled trip shows as the twelve-minute hole it leaves rather than as a new twelve-minute timetable.

On the map, a bunched pair (within 30% of the headway) is tied together with a coral bar, everywhere. A gap (1.8× the headway, and three minutes over it) runs in amber dashes along the rails between the two trams, round the corners rather than across the blocks, on the lines you pick out: a hole in the service can be kilometres of the city, and drawing all of them at once would bury the map. The Lines panel lists the worst few, bunches grouped so three trams together are one entry, with a dot on each affected line's button; picking one opens the tram in question. A tram's own panel gains a Spacing card: the trams either side of it, the gap to each drawn to scale against the timetable, and a tap on either neighbour to open it.

It is honest about what it does not know. A measurement needs a tram to leave a stop the one ahead left too, so after a restart nothing is shown for about one headway. A tram laying over at its terminus while the one ahead pulls out is seconds behind it by the bound and will still leave on time, so a bound alone can show a gap but never a bunch. Measured against the live feed on a Saturday evening, the headways the backend derived from delays matched the trams' own timetabled start times, and the line medians settled on the 12-minute evening frequency.


v0.74.0 2026-09-16

Removed

Fixed


v0.68.1 2026-09-08

Changed

Fixed


v0.68.0 2026-09-08

Added

A single fix cannot answer the question and the app does not pretend otherwise. At a stop you are as close to the tram you are waiting for as to the one you just left, and a bus passing a pavement comes within a few metres of everyone standing on it. What separates a ride from a near miss is that a ride *keeps* being near, so the evidence is gathered per vehicle over a rolling window: how long it has stayed within reach, how close it has been on average, and — the part that does the real work — how much ground the two of you have covered *together*. A vehicle is claimed outright only after four fixes, twenty-five seconds and a hundred and fifty metres of shared travel with no fix in between where it was not alongside; on weaker evidence it is offered as a question ("On the tram 9?") rather than asserted, because being told you are on the 7 while you are standing beside it is worse than being asked. Say no and that run is not offered again.

How far away still counts is not a fixed radius: it is the length of the vehicle's body, plus whatever uncertainty the phone reports for its own fix, plus the ground the vehicle has covered since its last report — a generous allowance for a 27 m tram and a poor GPS in a metal box, which is exactly the case this has to work in. Two things it deliberately refuses to answer: a fix too vague to place anyone on one side of a street is dropped rather than scored, and two vehicles running different journeys that fit the evidence equally well are reported as ambiguous rather than resolved by a coin toss. The coupled halves of one train are not ambiguous — either answer names the same ride — so those are settled by which unit is nearer.

While it is still looking, every mode streams, because the bus you are sitting on cannot be found if the bus feed is off; once a ride is locked, only that ride's own mode is kept on, and the vehicle stays on the map whatever the line filters say. Stepping off ends it: three fixes in a row without the vehicle alongside, or a minute of silence from it, and the camera is released and the search resumes rather than chasing a tram you have left. Nothing starts on its own — geolocation costs a permission prompt and a radio, so the watch begins on a tap, and is resumed unprompted on a later visit only for a reader who has both asked for it before and already granted the permission.

The countdown is taken from the vehicle running your leg, not from the clock: the leg carries its own stop list, so the vehicle's reported next stop is looked up in it and what is left is the number of stops still to run. Where no vehicle can be unambiguously matched to the leg — two trams on the same trip identity, a leg with no trip ID, a mode whose feed is off — it falls back to the arrival prediction and *says* that it has, because "the tram says your stop is next" and "the timetable thinks you arrive in ninety seconds" are not the same claim, and a rider deciding whether to stand up should know which of the two they have been given.

Which leg you are on is settled by the ride detection above where it has an answer, and by the clock otherwise. A stop the vehicle has already left is reported as passed rather than counted down to — an alert that fires after the fact is noise on top of a missed stop — and "get ready" is shown on screen but never pushed to the lock screen, because three interruptions per leg teaches a rider to ignore all three. The buzz and the banner never depend on the notification permission; a browser that has frozen the tab cannot fire anything at all, which is the honest limit of a web page in a pocket.


v0.67.4 2026-09-07

Fixed

A block is now sized by the work it makes rather than by the clock it covers: half a minute of history read second by second, eight minutes of it read every thirtieth second, and roughly two thousand readings either way. Every span is a doubling of the smallest and aligned to its own size, so replaying a stretch still asks for the same URLs and the browser cache still answers them. The parse that used to cost three megabytes now costs three quarters of one, four times as often — small enough to land inside a frame or two instead of stopping the map.

A block that size is also a much shorter wait when the scrubber is dragged somewhere new: the first window a seek needs comes back in a second and a half rather than three and a half. And the parallel fetching added in v0.67.3 now only runs in parallel when the cursor is actually close to running out of history — at real time, where one block lasts half a minute, they land one at a time, because three blocks parsed back to back would be three times the hitch this change exists to remove.


v0.67.3 2026-09-07

Fixed

The map's glide between two snapshots is measured from the gap between them, floored at seven tenths of a second because a live feed that speaks once a second and reports twice in a tenth is hiccuping, not talking fast. A replay at sixty times genuinely does deliver a snapshot every eighth of a second, and the floor meant every glide was cut off at a fifth of its length by the next one: the vehicles were drawn covering a fifth of the ground they had actually travelled, falling further behind on each snapshot until the guard against teleports fired and put them where they belonged in one jump. That guard was the second cause: it measures a step against a second of travel, and eight seconds of perfectly ordinary tram between two snapshots at sixty times looks, by that measure, like a vehicle crossing the city — so it fired on nearly every step and turned the replay into a slideshow. Both are now measured against the history a window carries rather than the wall clock it took, and past a couple of seconds per window the easing curve steps aside too: the two ends of the glide are both measured positions several hundred metres apart, and the honest way between them is a straight line at a constant rate rather than an ease-in and ease-out eight times a second.

The disappearances were the player outrunning its own fetching. Windows were fetched one at a time, and at two hundred and forty times the cursor crosses a two-minute block in half a second — so the player spent most of its time waiting out a round trip, playing into history it did not hold. What it drew there was the last thing it knew: a crowd of vehicles going stale one by one and winking out as they passed the minute the live map drops a silent vehicle at. Fetching now runs a few windows in parallel and asks for the next one the moment one lands, and the cursor waits where it is when it does reach the end of what has been fetched, which is what a video player does and reads as what it is. A speed change no longer empties the buffer either: a reading is a reading, so speeding up mid-playback keeps everything already held and carries straight on instead of blanking the map.

Underneath, the playback clock is now driven by animation frames rather than by a timer — an eighth-of-a-second setInterval is coalesced and drifts, and the map draws unevenly spaced snapshots as unevenly moving trams — and the buffer merges arriving readings into what it holds instead of re-sorting and replaying the whole thing from the start on every fetch, which at speed was several thousand readings landing twice a second into a buffer of tens of thousands.


v0.67.2 2026-09-07

Changed


v0.67.1 2026-09-07

Fixed

The middle is recoverable from the city's own street geometry, and now is. avoindata:Liikennevaylat carries the carriageway centrelines noded at every junction, so a junction is a point three or more of them meet at — several such points where a dual carriageway or a staggered crossing splits, and the centroid of the ones belonging to one crossing is the middle of the whole thing. The marked pedestrian crossings (Suojatie) ring that same box, so their centroid is a second, independent reading of it, and the only reading there is for a mid-block signal, which has no meeting of streets at all. Where both readings exist the centre is their mean: scored against a sample of junctions, each reading flatters itself and the open-data point is worst under either scoring, so the mean is the answer that stays close whichever way it is measured. 466 of 565 signals move, by a median of 7.4 m.

A correction is only kept when the geometry agrees with the point it is correcting — under 45 m of movement, and never on a single piece of evidence — because being 20 m off is a blemish and being snapped to the next junction down the street is a lie. The 99 signals with no clear answer keep their open-data coordinates exactly as before, and say so: a feature carries centered: true only when the point served is a computed centre. A junction listed twice under one number, which is two masts of the same crossing, is now served once instead of as two markers stacked on the same spot.

None of this is work the app does at runtime: 111,000 street centrelines are not something to intersect on a request, or on a timer, for a dataset that changes when a street is rebuilt. scripts/generate-junction-centers.mjs computes the table offline against Helsinki's WFS, and the backend embeds it and applies it as it serves /api/v1/traffic-lights.


v0.67.0 2026-09-07

Changed

scripts/verify-traffic-lights.mjs drops its mast measurements for what is left to check: that every state's art decodes and paints, that a granted request paints green the idle marker paints none of, that the marker is drawn and lit at street level, at close range and under a 60° pitch, and that a warning light stays one amber lamp with no signal lenses.


v0.66.1 2026-09-07

Fixed

Two smaller things stood in the way of a finger even when the counting was right. The badge is nine-point type in the map's corner, too small a target to land on twice in a row, so on narrow screens it is padded out to something a thumb can hit; and a double tap on it was also the browser's own zoom gesture, which touch-action: manipulation now hands back to the badge. The link itself is unchanged: one tap still opens the changelog once the grace period shows no second tap is coming, falling back to this tab where a popup opened from a timer is refused.


v0.66.0 2026-09-07

Added

It is also the one vehicle that is not a carriage, and it is not drawn as one. The marker is a vessel seen from above — a raked bow, a beam half as wide as the hull is long, a deckhouse set inboard of open decks fore and aft, a wheelhouse forward and a funnel behind it — because that silhouette is what makes the fifth mode identifiable before any colour is read, next to four rectangles with noses. The 3D body follows the same drawing at real scale: a 35 m hull on an 8.5 m beam, a saloon, a bridge, a funnel, a short wake astern while it is making way, and no wheels, no bogies, no pantograph and no articulation, because a boat has none of those.

The load goes into both. The marker's deckhouse is a saloon that fills from the stern forward and is ringed in the same colour, so the state survives being shrunk to twenty pixels; the 3D body carries the gauge along its saloon roof, which is the one surface a pitched camera looks straight down on, so it reads from every bearing like a battery meter. The colours run the opposite way to the city-bike gauge and for the same reason — there a full rack is the good news, here an empty deck is: green through amber to red, in the palette the bike gauges already use. The telemetry panel states it in words and a meter (Filling up · 62%), and the panel schematic draws the saloon as a glass tank with the water and a bow wave under it.

A vessel nobody has counted gets an empty grey gauge, never a green one. "No report" is not "nobody aboard", and a mode that does not measure occupancy at all is treated as having none rather than as being empty — which is why a tram's occu: 0 still draws no gauge anywhere.

Everything else the other four modes have, the ferry now has: its own toggle in the corner (a fifth chip, in HSL's ferry cyan #00b9e4), the quays as stop discs and as sign boards carrying a vessel over its own wake, the quay as 3D furniture at the pier's own scale, the dashed cyan route line across the water following the toggle instead of being hidden outright, the line filter, the journey planner and the stop departures switching the feed on when a leg or a departure needs it, and its own dead-reckoning ceiling — 10 m/s, set from what a Suomenlinna boat actually does rather than from the feed, on the same two-second horizon the street modes use.

The history archive already packs occu into its 28-byte record (255 meaning "not reported"), so a deployment that names ferry in REPLAY_MODES gets the load gauge in the timelapse too, filling and emptying through the week exactly as it did live. The default stays trams alone, for the reason it always was: a mode that is only ingested while somebody is watching would leave holes in its own history.


v0.65.0 2026-09-07

Added

It reads them now. The newest exchange per vehicle is held in the ingestion worker and folded into that vehicle's next position update, so it reaches clients on the stream they already read rather than as a second one to join; it is dropped again after 25 seconds, because a request is a momentary thing and a tram three junctions further on is not still asking this one. An answer carries neither the junction nor what was asked for, only a request ID, so it inherits both from the request it matches — and where no request was seen, it stands on its own with the junction read off the topic. tlr also fires when a vehicle reaches a junction it is equipped to ask and *decides not to*, which is a different thing from asking and is reported as its own state with the reason attached.

Junctions are joined by identity, not by geography: HFP's sid is Helsinki's own junction number, which is exactly the id the traffic-lights endpoint already served. Measured against a capture of the live feed, 145 of 147 tram requests named a junction that is in the open-data set. So a stopped tram's popup no longer says it is *probably near* some traffic lights — where the tram named them, it says it is waiting at *those* ones, and the Diagnostics tab carries the whole exchange: request type (on approach, on closing doors, on opening doors, in advance), priority level, attempt number, signal group, and the radio protocol it went out on.

Only trams and buses run the equipment. Tram priority streams always; a bus's arrives with the bus feed and leaves with it.

The index behind the markers had been keeping only the leading exchange per junction, because a marker can show one state; it keeps them all now, because a corner two tram lines share routinely has more than one vehicle negotiating with it at the same moment, and "one of them" is the wrong answer to "who is asking?". The marker still shows the leading state — an answer outranks a request — and the junction being read takes the same selection gold a stop or a vehicle does, on its ring and on its 3D mast, never on its lenses: recolouring those to mean "you clicked me" would overwrite the one thing the signal exists to say.

Both markers open it, the flat one and the 3D mast, and the panel is careful with its words throughout: "granted" is the junction acknowledging a request, not a green light showing. HFP publishes the negotiation and never the aspect.


v0.64.0 2026-09-06

Added

A replayed tram is not a different kind of thing from a live one. It carries the same telemetry, rides the same rails, opens the same doors and reports the same delay, because the archive stores exactly what the live map was shown — the reading after ingestion has deduplicated HSL's fourfold tram copies, paired down the metro's coupled units, read the next stop off the topic and normalised the delay's sign. Everything downstream of the source switch — filtering, selection, the three-tab vehicle panel, the rail snapping, the dead reckoning — works on history without knowing it is history.

Everything on the map that is *not* a vehicle is fetched for right now, so entering a replay clears the stop, journey, bike and arrival selections rather than showing tonight's departures beside this morning's trams.

That layout is also what makes a *place* answerable rather than only a moment. GET /api/v1/replay/timelapse takes a bounding box and a span up to the whole retention window, and rejects a reading by reading eight bytes of it and decoding nothing else. Measured, a junction-sized box costs 117 µs per minute of archive, so a whole week is about 1.2 seconds.

Changed

Fixed


v0.63.0 2026-09-06

Added

The photo is a raster layer slid *under* the dark style's labels rather than a style of its own. Everything the vector style draws below its first label — land, water, roads, buildings — ends up beneath an opaque photo and is simply never seen, while street and place names keep drawing on top, which is the one thing a bare aerial view badly lacks; anything the style happens to order after its labels is switched off so it cannot draw over the imagery. The stops, platforms, route ribbons and vehicles are unchanged and sit above both, in their dark-theme colours, because dark is what the labels under them are.

The open interface serves the orthophotos to zoom 16 — about 1.2 m per pixel at Helsinki's latitude — and the map goes to 18, so the last two zooms are the level-16 tiles overzoomed: softer up close, but imagery that keeps going where the stops get interesting rather than cutting out there.

The mode needs a (free) National Land Survey key in MML_API_KEY, served to the browser by GET /api/v1/config beside the Digitransit map key. A deployment without one does not show the chip at all, rather than offering a map that would come back as a grid of 401s.


v0.62.3 2026-09-06

Fixed

A junction needed one thing more. A line's own polyline can pass through the same few metres of street twice — a loop, a turn-back, two arms of a crossing — and position and heading both say the same thing about all of them. So the placement is now measured for continuity as well: a tram is expected to be found within its own speed's travel of where it was last placed, and a fix somewhere else on the route is charged 60 m before it can win. Only the arm that continues the run the tram is already making passes that test.

Being on the rails also means being carried along them: a snapped tram now slides *along* its track between two position reports, through curves a straight interpolation used to cut across. A tram that is genuinely off its route — a diversion, a depot run, a replacement working — is still drawn where it says it is; past 35 m from any of its own rails nothing is snapped at all.

Changed

Added


v0.62.2 2026-09-06

Fixed


v0.61.1 2026-09-06

Fixed


v0.61.0 2026-09-06

Changed

Added


v0.60.0 2026-09-06

Fixed

The stop a vehicle is heading for was there the whole time, one level up: next_stop, level 13 of the MQTT topic itself. Unlike the payload field it is stated on every message — 44,432 out of 44,432 in that capture — and where the payload did name a stop the two agreed exactly, 22,376 times out of 22,376, because a vehicle that has not yet pulled away is both at a stop and heading for it. It is now parsed off the topic and published as nextStop on every vehicle, with eol for a vehicle that has run out of line. Replaying 102,172 captured live messages through the ingestion path, all 83 vehicles left in the cache carried a next stop; only 34 of them carried a payload stop.

Changed


v0.59.1 2026-09-06

Changed


v0.59.0 2026-09-06

Added

The countdown is the departures feed's own prediction, exactly as in the panel, so a label needs no vehicle located and turns on no extra live feed to be drawn — a bus stop labels itself without the bus feed being subscribed. A stop with nothing honest to say gets no label at all rather than an empty badge, because an empty badge over a stop reads as “nothing runs here”, which is a different claim from “we don't know yet”. A departure more than thirty seconds gone stops being a label before it stops being a timetable row.

Changed


v0.58.1 2026-09-06

Fixed


v0.58.0 2026-09-06

Added

Changed


v0.57.1 2026-09-06

Fixed


v0.57.0 2026-09-05

Added

Changed


v0.56.2 2026-09-05

Fixed


v0.56.1 2026-09-04

Fixed


v0.56.0 2026-09-03

Added


v0.55.2 2026-09-03

Fixed


v0.55.1 2026-09-03

Fixed


v0.55.0 2026-09-03

Added


v0.54.2 2026-09-03

Fixed


v0.54.1 2026-09-03

Fixed


v0.54.0 2026-09-01

Changed


v0.53.2 2026-09-01

Fixed


v0.53.1 2026-09-01

Fixed


v0.53.0 2026-09-01

Added


v0.52.2 2026-09-01

Fixed

Changed


v0.52.1 2026-09-01

Fixed


v0.52.0 2026-09-01

Added

Changed

Fixed


v0.51.1 2026-08-31

Fixed

Changed


v0.51.0 2026-08-22

Changed

Removed


v0.50.4 2026-08-22

Changed


v0.50.3 2026-08-21

Fixed

Added


v0.50.2 2026-08-10

Changed


v0.50.1 2026-08-03

Changed


v0.50.0 2026-08-01

Added


v0.49.1 2026-07-25

Changed


v0.49.0 2026-07-25

Changed


v0.48.0 2026-07-25

Changed


v0.47.4 2026-07-25

Fixed


v0.47.3 2026-07-25

Fixed


v0.47.2 2026-07-25

Fixed

Added


v0.47.1 2026-07-25

Changed

Added


v0.47.0 2026-07-25

Changed

Added


v0.46.1 2026-07-25

Fixed

Changed


v0.46.0 2026-07-25

Added

Changed

Fixed


v0.45.1 2026-07-24

Fixed


v0.45.0 2026-07-23

Changed


v0.44.10 2026-07-22

Changed

v0.44.8 2026-07-22

Fixed

v0.44.7 2026-07-22

Fixed

Changed

Fixed


v0.44.5 2026-07-21

Changed


v0.44.4 2026-07-21

Changed


v0.44.3 2026-07-21

Fixed

v0.44.2 2026-07-21

Changed


v0.44.1 2026-07-21

Changed

Fixed


v0.44.0 2026-07-21

Changed


v0.43.1 2026-07-21

Fixed


v0.43.0 2026-07-21

Added


v0.42.1 2026-07-21

Changed


v0.42.0 2026-07-21

Changed


v0.41.2 2026-07-21

Changed


v0.41.1 2026-07-21

Fixed

Added


v0.41.0 2026-07-21

Added


v0.40.1 2026-07-21

Fixed


v0.40.0 2026-07-21

Added


v0.39.0 2026-07-20

Added


v0.38.1 2026-07-19

Fixed


v0.38.0 2026-07-19

Added


v0.37.0 2026-07-19

Added


v0.36.1 2026-07-19

Fixed


v0.36.0 2026-07-18

Added


v0.35.2 2026-07-18

Fixed


v0.35.1 2026-07-18

Fixed


v0.35.0 2026-07-18

Added


v0.34.0 2026-07-18

Added


v0.33.2 2026-07-13

Fixed


v0.33.1 2026-07-13

Fixed


v0.33.0 2026-07-13

Added

Changed


v0.32.0 2026-07-13

Added


v0.31.0 2026-07-13

Fixed


v0.30.0 2026-06-17

Added


v0.29.2 2026-06-17

Fixed


v0.29.1 2026-06-16

Fixed


v0.29.0 2026-06-16

Added


v0.27.0 2026-06-16

Added

Changed


v0.26.3 2026-06-16

Fixed


v0.25.0 2026-06-15

Added

Fixed


v0.24.0 2026-06-12

Added