← Back to index

SubwayVehicleService

Spots
CodeWhat It DoesHow It Does It
▶ IMPORTS
import Foundation import CoreLocation import MapKitFramework importsImports Foundation, CoreLocation, MapKit.
▶ MODEL
struct SubwayVehicle: Identifiable { let id: String let route: String let tripId: String // GTFS trip_id — stable across stops for one run let stopId: String // GTFS stop_id of next/current stop let latitude: Double let longitude: Double let bearing: Double /// 0 = INCOMING_AT, 1 = STOPPED_AT, 2 = IN_TRANSIT_TO let currentStatus: Int /// Unix timestamp of last detected movement (0 if absent) let timestamp: UInt64 }`SubwayVehicle` structDefines the `SubwayVehicle` struct. Conforms to Identifiable.
▶ MAP ANNOTATION
final class SubwayVehicleAnnotation: NSObject, MKAnnotation { let vehicle: SubwayVehicle @objc dynamic var coordinate: CLLocationCoordinate2D { CLLocationCoordinate2D(latitude: vehicle.latitude, longitude: vehicle.longitude) } var title: String? { vehicle.route } init(_ v: SubwayVehicle) { self.vehicle = v; super.init() } }`SubwayVehicleAnnotation` classDefines the `SubwayVehicleAnnotation` class. Conforms to NSObject, MKAnnotation.
▶ MINIMAL PROTOBUF READER
// // GTFS-RT uses only wire types 0 (varint), 1 (64-bit), 2 (length-delimited), 5 (32-bit). // We only need to read length-delimited fields (strings and embedded messages) // and skip everything else.Documentation commentDescribes the following declaration.
private struct ProtoReader { let data: Data var pos: Int init(_ data: Data) { // Swift Data slices preserve the original index range (startIndex != 0). // All our pos-based access starts at 0, so force a copy when the slice // is offset — otherwise data[0] is out of bounds and Foundation traps. self.data = data.startIndex == 0 ? data : Data(data) self.pos = 0 } var hasMore: Bool { pos < data.count } // Read a little-endian base-128 varint. mutating func varint() -> UInt64 { var result: UInt64 = 0 var shift = 0 while pos < data.count { let b = data[pos]; pos += 1 result |= UInt64(b & 0x7F) << shift if b & 0x80 == 0 { break } shift += 7 } return result } // Read (fieldNumber, wireType); returns nil at end-of-buffer. mutating func tag() -> (f: Int, w: Int)? { guard hasMore else { return nil } let v = varint() return (f: Int(v >> 3), w: Int(v & 0x7)) } // Read a length-prefixed byte slice (wire type 2). // Safe: guards against UInt64→Int overflow (malformed or non-protobuf input) // and against pos+len exceeding the buffer. mutating func bytes() -> Data? { let raw = varint() // UInt64 values that don't fit in a non-negative Int would indicate corrupt data. guard raw <= UInt64(Int.max) else { pos = data.count; return nil } let len = Int(raw) // Guard against integer overflow in pos+len and out-of-bounds slice. guard len <= data.count - pos else { pos = data.count; return nil } defer { pos += len } return data[pos ..< pos + len] } // Read a UTF-8 string (wire type 2). mutating func string() -> String? { guard let d = bytes() else { return nil } return String(data: d, encoding: .utf8) } // Skip the value for the current wire type without parsing it. mutating func skip(wire w: Int) { switch w { case 0: _ = varint() case 1: pos = min(pos + 8, data.count) case 2: _ = bytes() case 5: pos = min(pos + 4, data.count) default: pos = data.count // unknown wire type → abandon parsing } } }`ProtoReader` structDefines the `ProtoReader` struct.
▶ GTFS-RT PARSING HELPERS
private struct VehicleRecord { var routeId = "" var tripId = "" var stopId = "" // Per GTFS-RT spec: if current_status field is absent, IN_TRANSIT_TO is assumed. var currentStatus = 2 // 0=INCOMING_AT, 1=STOPPED_AT, 2=IN_TRANSIT_TO var hasStatus = false // true if field 6 was present in the message var timestamp: UInt64 = 0 // Unix seconds of last detected movement (field 5) }`VehicleRecord` structDefines the `VehicleRecord` struct.
/// Extracts trip_id (field 1) and route_id (field 5) from a serialised TripDescriptor. private func parseTripDescriptor(_ data: Data) -> (routeId: String, tripId: String) { var r = ProtoReader(data) var routeId = "" var tripId = "" while let (f, w) = r.tag() { switch (f, w) { case (1, 2): tripId = r.string() ?? "" case (5, 2): routeId = r.string() ?? "" default: r.skip(wire: w) } } return (routeId: routeId, tripId: tripId) }Documentation commentDescribes the following declaration.
/// Extracts trip.route_id, stop_id, and current_status from a serialised VehiclePosition. /// /// MTA NYCT field layout (verified by raw proto dump): /// field 1, wire 2 → trip (TripDescriptor) /// field 3, wire 0 → current_stop_sequence (varint) /// field 5, wire 0 → timestamp (Unix seconds) /// field 6, wire 0 → current_status 0=INCOMING_AT 1=STOPPED_AT 2=IN_TRANSIT_TO /// field 7, wire 2 → stop_id (string) private func parseVehiclePosition(_ data: Data) -> VehicleRecord? { var r = ProtoReader(data) var rec = VehicleRecord() while let (f, w) = r.tag() { switch (f, w) { case (1, 2): // trip: TripDescriptor if let d = r.bytes() { let parsed = parseTripDescriptor(d) rec.routeId = parsed.routeId rec.tripId = parsed.tripId } case (5, 0): // timestamp (Unix seconds of last detected movement) rec.timestamp = r.varint() case (6, 0): // current_status rec.currentStatus = Int(r.varint()) rec.hasStatus = true case (7, 2): // stop_id rec.stopId = r.string() ?? "" default: r.skip(wire: w) } } return (!rec.routeId.isEmpty && !rec.stopId.isEmpty) ? rec : nil }Documentation commentDescribes the following declaration.
/// Parses a full GTFS-RT FeedMessage binary blob and returns all vehicle records. private func parseFeedMessage(_ data: Data) -> [VehicleRecord] { var r = ProtoReader(data) var records = [VehicleRecord]() while let (f, w) = r.tag() { guard f == 2, w == 2 else { r.skip(wire: w); continue } // entity field guard let entityData = r.bytes() else { continue } // Parse FeedEntity: look for field 4 = VehiclePosition. var er = ProtoReader(entityData) while let (ef, ew) = er.tag() { if ef == 4, ew == 2 { if let vpData = er.bytes(), let rec = parseVehiclePosition(vpData) { records.append(rec) } } else { er.skip(wire: ew) } } } return records }Documentation commentDescribes the following declaration.
▶ SERVICE
struct SubwayVehicleService { /// MTA GTFS-RT feed URLs — one per line group. /// No API key required (api-endpoint.mta.info, 2024+). private static let feedURLs: [String] = [ "https://api-endpoint.mta.info/Dataservice/mtagtfsfeeds/nyct%2Fgtfs", // 1 2 3 4 5 6 7 "https://api-endpoint.mta.info/Dataservice/mtagtfsfeeds/nyct%2Fgtfs-ace", // A C E "https://api-endpoint.mta.info/Dataservice/mtagtfsfeeds/nyct%2Fgtfs-bdfm", // B D F M "https://api-endpoint.mta.info/Dataservice/mtagtfsfeeds/nyct%2Fgtfs-g", // G "https://api-endpoint.mta.info/Dataservice/mtagtfsfeeds/nyct%2Fgtfs-jz", // J Z "https://api-endpoint.mta.info/Dataservice/mtagtfsfeeds/nyct%2Fgtfs-l", // L "https://api-endpoint.mta.info/Dataservice/mtagtfsfeeds/nyct%2Fgtfs-nqrw", // N Q R W "https://api-endpoint.mta.info/Dataservice/mtagtfsfeeds/nyct%2Fgtfs-sir", // Staten Island ] /// Fetches all MTA subway feeds concurrently and returns every active vehicle /// whose current stop can be resolved from `stopCoords`. /// The caller is responsible for filtering the result to the visible map region. static func fetchAll(stopCoords: [String: CLLocationCoordinate2D]) async -> [SubwayVehicle] { let cfg = URLSessionConfiguration.ephemeral cfg.timeoutIntervalForRequest = 10 cfg.timeoutIntervalForResource = 15 let session = URLSession(configuration: cfg) defer { session.invalidateAndCancel() } // Fetch all 8 feeds in parallel. var records = [VehicleRecord]() await withTaskGroup(of: [VehicleRecord].self) { group in for urlString in feedURLs { guard let url = URL(string: urlString) else { continue } group.addTask { guard let (data, _) = try? await session.data(from: url) else { return [] } return parseFeedMessage(data) } } for await feedRecords in group { records.append(contentsOf: feedRecords) } } let s0 = records.filter { $0.currentStatus == 0 }.count let s1 = records.filter { $0.currentStatus == 1 }.count let s2 = records.filter { $0.currentStatus == 2 }.count let sX = records.filter { $0.currentStatus != 0 && $0.currentStatus != 1 && $0.currentStatus != 2 }.count let present = records.filter { $0.hasStatus }.count print("🚇 MTA feeds: \(records.count) vehicles — INCOMING_AT:\(s0) STOPPED_AT:\(s1) IN_TRANSIT_TO:\(s2) other:\(sX) | field6_present:\(present)/\(records.count)") // Resolve each vehicle's coordinate from its stop ID. return records.compactMap { rec -> SubwayVehicle? in let stopID = rec.stopId // Stop IDs end in N (northbound) or S (southbound); strip to get the base ID. let isN = stopID.hasSuffix("N") let isS = stopID.hasSuffix("S") let bearing: Double = isS ? 180 : 0 let baseID = (isN || isS) && stopID.count > 1 ? String(stopID.dropLast()) : stopID guard let coord = stopCoords[baseID] else { return nil } return SubwayVehicle(id: UUID().uuidString, route: rec.routeId, tripId: rec.tripId, stopId: rec.stopId, latitude: coord.latitude, longitude: coord.longitude, bearing: bearing, currentStatus: rec.currentStatus, timestamp: rec.timestamp) } } }`SubwayVehicleService` structDefines the `SubwayVehicleService` struct.
// subwayLineUIColor(for:) and subwayLineTextUIColor(for:) are defined in NearbyMapView.swiftDocumentation commentDescribes the following declaration.