Skip to content

Add cyclist position availability fields to VehiclePosition and CarriageDetails#611

Draft
oliver-sturrock wants to merge 1 commit intogoogle:masterfrom
oliver-sturrock:cyclist-positions-experimental
Draft

Add cyclist position availability fields to VehiclePosition and CarriageDetails#611
oliver-sturrock wants to merge 1 commit intogoogle:masterfrom
oliver-sturrock:cyclist-positions-experimental

Conversation

@oliver-sturrock
Copy link

@oliver-sturrock oliver-sturrock commented Feb 11, 2026

Resolves #610

Summary

This proposal adds two new experimental fields to the VehiclePosition and CarriageDetails messages to enable real-time reporting of bicycle rack/storage availability on transit vehicles.

Proposed Fields

Field Type Message Description
cyclist_positions_available uint32 VehiclePosition, CarriageDetails Number of cyclist positions currently available
total_cyclist_positions uint32 VehiclePosition, CarriageDetails Total cyclist position capacity on the vehicle/carriage

Motivation

Transit passengers traveling with bicycles currently have no way to know whether a bike rack or bike storage area will be available on an approaching vehicle. A typical transit bus has only 2–3 external bike rack positions, and when full, cyclists are denied boarding — they must wait for the next vehicle or abandon their transit trip entirely.

This is a documented and quantified problem:

  • The Santa Clara Valley Transportation Authority (VTA), in a USDOT SMART Grant implementation report (August 2025), found that bicycle rack occupancy varies significantly by route, time of day, and stop location, and that denied boardings represent a measurable service quality issue.
  • VTA's stakeholder engagement confirmed that cyclists value real-time rack availability data and would use it to adjust departure times, select alternate routes, or complete trips by bike instead of waiting.
  • No transit agency in North America currently provides real-time bike rack availability to passengers through standard data feeds.

Providing this data in GTFS-RT enables trip planning applications (Transit App, Google Maps, Apple Maps) to display bike rack availability alongside existing vehicle position and occupancy information.

Working Implementation

Producer: Sportworks Global LLC — Velolink — a commercially available bike rack occupancy monitoring system deployed on transit buses. Velolink uses magnetic reed switches embedded in Sportworks Apex-3 bike racks to detect individual slot occupancy in real-time, transmitting data via cellular modem to a cloud platform with a public API.

Consumer: [TO BE CONFIRMED — we are actively reaching out to Transit App, Swiftly, OpenTripPlanner, Google Maps, and Apple Maps to secure a consumer implementation before calling for a vote.] Example consumer: https://cascadia-metro-gtfs-rt.sportworks.com/consumer

Ecosystem Validation Results

The public GTFS-RT feed has been tested against all major ecosystem tools. All 11 tests pass:

# Tool Maintainer Result Notes
1 Self-validation (/validate) PASS (11/11 checks) All entity references valid, bounds correct, deterministic
2 Determinism check PASS Byte-identical protobuf on concurrent requests
3 protoc --decode_raw Google PASS All fields decoded, fields 12 & 13 visible
4 protoc --decode (official proto) Google PASS Standard fields named, fields 12 & 13 as unknown (correct)
5 protoc --decode (extended proto) Google PASS cyclist_positions_available and total_cyclist_positions named
6 Python gtfs-realtime-bindings 2.0.0 MobilityData PASS Entities parsed, 803-byte round-trip identical
7 Node.js gtfs-realtime-bindings MobilityData PASS Entities parsed, unknown fields silently skipped
8 Wire format tag verification PASS 0x60/0x68 tags correct, all values in bounds
9 CORS headers PASS Access-Control-Allow-Origin: * on all endpoints
10 GTFS static feed PASS 7 files, 24 stops, 5 routes, 363 trips, 0 invalid refs
11 Unit tests (vitest) PASS (31/31) 76,378 vehicle states verified across 10,000+ timestamps

Forward Compatibility

Adding optional uint32 fields is a fully forward-compatible change under proto2 semantics:

  1. Python protobuf: Unknown fields preserved in round-trip serialization (803 → 803 bytes)
  2. Java protobuf: Unknown fields stored in UnknownFieldSet and preserved
  3. Node.js ProtoBuf.js: Unknown fields silently discarded (no errors)
  4. Go protobuf: Unknown fields stored in XXX_unrecognized and preserved
  5. protoc: Decodes successfully with or without the extended schema

This is the same pattern used when occupancy_status (field 9), occupancy_percentage (field 10), and multi_carriage_details (field 11) were previously added to the specification.

Design Decisions

Why separate available and total fields?

This mirrors the existing pattern of occupancy_status alongside occupancy_percentage. For cyclist positions:

  • cyclist_positions_available provides the real-time signal consumers need ("2 of 3 bike spots open")
  • total_cyclist_positions provides the capacity context needed for meaningful display

Why uint32 instead of an enum?

Cyclist positions are discrete, low-count integers (typically 2–3 on buses, 4–10 per train car). An exact count is more useful than a qualitative classification, and the low cardinality makes exact counts practical.

Why both VehiclePosition and CarriageDetails?

  • VehiclePosition: For buses and single-unit vehicles with one bike rack
  • CarriageDetails: For trains and multi-car vehicles where bike storage varies by carriage

Absence vs. zero semantics

Following GTFS-RT conventions:

  • Field absent: Producer does not have cyclist position data
  • Value = 0: Vehicle/carriage has zero cyclist positions (none exist, or all occupied)

Privacy Considerations

A typical bus has 2–3 bike rack positions. Sequential occupancy changes could theoretically identify an individual cyclist boarding. However:

  • No PII is transmitted — only anonymous integer counts at the vehicle level
  • The same limitation applies to existing OccupancyStatus values on small vehicles
  • Data granularity is consistent with existing APC data that agencies already publish

Example: Bus with Apex-3 Bike Rack

entity {
  id: "vehicle_4403"
  vehicle {
    trip {
      trip_id: "trip_22_1045"
      route_id: "22"
      direction_id: 0
    }
    position {
      latitude: 37.3382
      longitude: -121.8863
    }
    occupancy_status: FEW_SEATS_AVAILABLE
    cyclist_positions_available: 1    # 1 of 3 rack slots open
    total_cyclist_positions: 3        # Apex-3 rack = 3 slots
  }
}

Example: Multi-Car Train

entity {
  id: "vehicle_lrv_101"
  vehicle {
    cyclist_positions_available: 5    # Vehicle-level total
    total_cyclist_positions: 8
    multi_carriage_details {
      id: "car_A"
      carriage_sequence: 1
      cyclist_positions_available: 2
      total_cyclist_positions: 4
    }
    multi_carriage_details {
      id: "car_B"
      carriage_sequence: 2
      # No cyclist fields — no bike storage
    }
    multi_carriage_details {
      id: "car_C"
      carriage_sequence: 3
      cyclist_positions_available: 3
      total_cyclist_positions: 4
    }
  }
}

Checklist

  • New fields added to .proto file with correct field numbers
  • Reference documentation updated with field descriptions
  • Fields follow experimental field conventions
  • At least one producer implementation (Sportworks Global LLC — Velolink)
  • Public-facing GTFS-RT feed URL provided
  • Feed validated against ecosystem tools (protoc, Python bindings, Node.js bindings)
  • Google CLA signed (Sportworks Global LLC)
  • At least one consumer implementation confirmed
  • Announced on GTFS Realtime mailing list
  • 7-day discussion period completed
  • Vote called (80% approval required for experimental fields)

Contact

Oliver Sturrock
olst+gtfs@silverfallscapital.com
(425) 358-9055
Sportworks Global LLC / Silver Falls Capital

…ageDetails

Add four new experimental fields to enable real-time reporting of bicycle
rack/storage availability on transit vehicles:

- VehiclePosition.cyclist_positions_available (field 12)
- VehiclePosition.total_cyclist_positions (field 13)
- CarriageDetails.cyclist_positions_available (field 6)
- CarriageDetails.total_cyclist_positions (field 7)

These fields allow transit apps to display bike rack availability
alongside existing vehicle position and occupancy information.

A working producer implementation (Sportworks Global LLC Velolink) is
live at https://cascadia-metro-gtfs-rt.sportworks.com/gtfs-rt/vehicle-positions
and has been validated against protoc, Python gtfs-realtime-bindings,
and Node.js gtfs-realtime-bindings.
@google-cla
Copy link

google-cla bot commented Feb 11, 2026

Thanks for your pull request! It looks like this may be your first contribution to a Google open source project. Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA).

View this failed invocation of the CLA check for more information.

For the most up to date status, view the checks section at the bottom of the pull request.

@oliver-sturrock oliver-sturrock marked this pull request as draft February 11, 2026 05:47
@oliver-sturrock oliver-sturrock marked this pull request as ready for review February 11, 2026 05:48
@etienne0101
Copy link
Collaborator

Hello @oliver-sturrock, and thank you for this proposal.

According to the GTFS Realtime governance process, we typically start by reaching community consensus on an Issue regarding the field's usefulness and type. A Pull Request follows once consensus is reached.

Since you’ve opened both, I suggest moving this PR to draft for now so we can focus the discussion on the Issue first.

@oliver-sturrock oliver-sturrock marked this pull request as draft February 11, 2026 17:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Proposal: Add cyclist position availability fields to VehiclePosition and CarriageDetails

2 participants