Skip to content
Documentation

Vehicle timelapse

Every vehicle on the routes that stop in an area, over one day. Each moves along its trip's shape at its scheduled times and leaves a trail that fades behind it, in its route's own color or a color for its route type. The positions come from static schedules only, with no realtime data.

The timelapse comes from static GTFS schedules alone. It makes two kinds of request to the Transitland REST API, works out where each vehicle is at the playback time, and draws the trails with a WebGL shader on a MapLibre map.

The vehicle-timelapse example is a complete version that you can run on your own computer with your own API key. This version has a slightly simplified UI and options, but is otherwise identical in code and behavior to the example on this page.

  1. GET /api/v2/rest/routes?bbox=west,south,east,north lists the routes with a stop in the area, with each route's ID, route_type_basic and route_color. The page asks for 1,000 routes at a time, the most the API returns. A full page comes with a cursor, meta.after, which the page passes as after to get the next 1,000.
  2. GET /api/v2/rest/routes/{route_id}/patterns?service_date=YYYY-MM-DD&include_timetable=true&include_shapes=true returns the route's stop patterns that run on the date. Each pattern has a timetable and the shapes its trips follow. The page makes one of these requests per route, eight at a time.

The patterns endpoint, which returns the timetables, is new, and will get its own page in the REST API documentation shortly. Until then, the OpenAPI document describes its parameters and response, and transitland-lib's route_pattern_request.gql holds the GraphQL query it runs.

A timetable holds three grids: arrival times, departure times, and distances along each trip's shape. Each grid has a row per stop and a value per trip. A shape is a GeoJSON LineString, with the distance along the shape at each of its points.

A trip that runs past midnight belongs to the day it started, with times past 24:00. With Include the previous day's trips checked, the page also requests each route's patterns for the day before, moves those trips' times back a day, and keeps the ones still running after midnight. That makes a second request per route.

Each REST request counts against your API key's allowance. Loading an area costs a request for each 1,000 of its routes, plus one per route, or two per route with the previous day's trips.

Each trip in a timetable becomes a run: the times its vehicle reaches and leaves each stop, and how far along its shape each stop lies. A frequency-based trip makes a run for each start its headways give. On each frame, the page works out how far along its shape each running vehicle has driven at the playback time, interpolating between its stops, and finds the point at that distance along the shape. The vehicles are points in a GeoJSON source, redrawn on each frame.

The trails are a MapLibre custom layer, drawn with WebGL2. The time the vehicle passed each point goes into the vertex data, so the GPU can fade each pixel of a trail by its age.

  • On each frame, the page collects the stretch of shape each vehicle drove during the trail's length, as segments between the shape's points, and uploads them to the GPU.
  • Each segment is 10 floats in one buffer: its two ends in Web Mercator coordinates, the times the vehicle passed each end, and the points before and after it on the path. A second buffer holds its route's color as four bytes. Positions are measured from an origin near the trails, which keeps them precise as 32-bit floats, and the layer adds the origin back in the projection matrix.
  • One instanced draw call draws every segment as a quad of two triangles. The vertex shader projects the segment's ends to the screen, widens the quad by the trail's width in pixels, and miters its corners with its neighbors' corners, so the quads meet edge to edge. Each corner carries the time the vehicle passed its end, and the GPU interpolates the time along the quad.
  • The fragment shader takes each pixel's age: the playback time, minus the time the vehicle passed the pixel. It discards a pixel ahead of the vehicle or older than the trail, and fades the rest from full color at the vehicle to transparent at the trail's end.