← Back to index

BusStopService

Spots
CodeWhat It DoesHow It Does It
▶ IMPORTS
import Foundation import CoreLocation import MapKit import SwiftUIFramework importsImports Foundation, CoreLocation, MapKit, SwiftUI.
▶ MODEL
struct BusStop: Codable, Identifiable { let id: String let lat: Double let lon: Double let name: String let routes: [String] var coordinate: CLLocationCoordinate2D { CLLocationCoordinate2D(latitude: lat, longitude: lon) } }`BusStop` structDefines the `BusStop` struct. Conforms to Codable, Identifiable.
▶ MAP ANNOTATION
final class BusStopAnnotation: NSObject, MKAnnotation { let stop: BusStop @objc dynamic var coordinate: CLLocationCoordinate2D { stop.coordinate } var title: String? { stop.name } init(_ stop: BusStop) { self.stop = stop } }`BusStopAnnotation` classDefines the `BusStopAnnotation` class. Conforms to NSObject, MKAnnotation.
▶ SERVICE
class BusStopService { static let shared = BusStopService() private var stops: [BusStop] = [] private(set) var isLoaded: Bool = false private init() {} // MARK: Load func loadIfNeeded() { guard !isLoaded else { return } DispatchQueue.global(qos: .utility).async { [weak self] in guard let self else { return } guard let url = Bundle.main.url(forResource: "bus_stops", withExtension: "json"), let data = try? Data(contentsOf: url), let decoded = try? JSONDecoder().decode([BusStop].self, from: data) else { print("🚌 BusStopService: failed to load bus_stops.json") return } self.stops = decoded // already lat-sorted by build script self.isLoaded = true print("🚌 BusStopService: loaded \(decoded.count) stops") } } // MARK: Spatial query /// Returns all stops within `radiusMeters` of `coordinate`, sorted by distance. func stops(near coordinate: CLLocationCoordinate2D, radiusMeters: Double = 400) -> [BusStop] { guard isLoaded, !stops.isEmpty else { return [] } let metersPerDegLat: Double = 111_320 let latDelta = radiusMeters / metersPerDegLat let lo = lowerBound(lat: coordinate.latitude - latDelta) let hi = upperBound(lat: coordinate.latitude + latDelta, from: lo) let origin = CLLocation(latitude: coordinate.latitude, longitude: coordinate.longitude) return stops[lo..<hi].filter { stop in CLLocation(latitude: stop.lat, longitude: stop.lon).distance(from: origin) <= radiusMeters } .sorted { CLLocation(latitude: $0.lat, longitude: $0.lon).distance(from: origin) < CLLocation(latitude: $1.lat, longitude: $1.lon).distance(from: origin) } } /// Returns the unique sorted route names served within `radiusMeters`. func routes(near coordinate: CLLocationCoordinate2D, radiusMeters: Double = 400) -> [String] { var seen = Set<String>() var result: [String] = [] for stop in stops(near: coordinate, radiusMeters: radiusMeters) { for route in stop.routes where seen.insert(route).inserted { result.append(route) } } return result.sorted { busRouteSortKey($0) < busRouteSortKey($1) } } // MARK: Binary search helpers private func lowerBound(lat: Double) -> Int { var lo = 0, hi = stops.count while lo < hi { let mid = (lo + hi) / 2 if stops[mid].lat < lat { lo = mid + 1 } else { hi = mid } } return lo } private func upperBound(lat: Double, from start: Int) -> Int { var lo = start, hi = stops.count while lo < hi { let mid = (lo + hi) / 2 if stops[mid].lat <= lat { lo = mid + 1 } else { hi = mid } } return lo } // MARK: Sort key ("M1" → "M001", "Bx12" → "Bx012") private func busRouteSortKey(_ route: String) -> String { let prefix = String(route.prefix(while: { $0.isLetter })) let rest = String(route.dropFirst(prefix.count)) let num = String(rest.prefix(while: { $0.isNumber })) let suffix = String(rest.dropFirst(num.count)) return "\(prefix)\(String(format: "%03d", Int(num) ?? 0))\(suffix)" } }`BusStopService` classDefines the `BusStopService` class.
▶ BOROUGH COLOR PALETTE
/// Borough-keyed colors that match MTA's informal color scheme. func busRouteColor(for route: String) -> Color { let up = route.uppercased() if up.hasPrefix("BX") { return Color(red: 0/255, green: 147/255, blue: 60/255) } // Bronx – green if up.hasPrefix("M") { return Color(red: 0/255, green: 57/255, blue: 166/255) } // Manhattan – blue if up.hasPrefix("B") && !up.hasPrefix("BX") { return Color(red: 255/255, green: 99/255, blue: 25/255) } // Brooklyn – orange if up.hasPrefix("Q") { return Color(red: 185/255, green: 51/255, blue: 173/255) } // Queens – purple if up.hasPrefix("S") { return Color(red: 128/255, green: 129/255, blue: 131/255) } // Staten Island – gray return Color(red: 0/255, green: 57/255, blue: 166/255) }Documentation commentDescribes the following declaration.
▶ BUS VEHICLE (REAL-TIME POSITION)
struct BusVehicle: Identifiable { let id: String // VehicleRef e.g. "MTA NYCT_7812" let route: String // cleaned label e.g. "M15" let lineRef: String // full LineRef e.g. "MTA NYCT_M15" let latitude: Double let longitude: Double let bearing: Double // degrees clockwise from north let destination: String let recordedAt: Date // RecordedAtTime from SIRI — used for dead-reckoning }`BusVehicle` structDefines the `BusVehicle` struct. Conforms to Identifiable.
▶ BUS VEHICLE ANNOTATION
final class BusVehicleAnnotation: NSObject, MKAnnotation { let vehicle: BusVehicle @objc dynamic var coordinate: CLLocationCoordinate2D // set to dead-reckoned position at init var title: String? { vehicle.route } init(_ vehicle: BusVehicle, displayCoordinate: CLLocationCoordinate2D? = nil) { self.vehicle = vehicle self.coordinate = displayCoordinate ?? CLLocationCoordinate2D(latitude: vehicle.latitude, longitude: vehicle.longitude) super.init() } }`BusVehicleAnnotation` classDefines the `BusVehicleAnnotation` class. Conforms to NSObject, MKAnnotation.
▶ BUS VEHICLE SERVICE
struct BusVehicleService { /// Fetches real-time vehicle positions via SIRI VehicleMonitoring. /// Pass a lineRef (e.g. "MTA NYCT_M15") to limit to one route, or nil for all routes. static func fetch(lineRef: String? = nil) async -> [BusVehicle] { guard !BusArrivalsService.apiKey.isEmpty else { return [] } var comps = URLComponents(string: "https://bustime.mta.info/api/siri/vehicle-monitoring.json")! var queryItems = [ URLQueryItem(name: "key", value: BusArrivalsService.apiKey), URLQueryItem(name: "OperatorRef", value: "MTA"), URLQueryItem(name: "version", value: "2"), ] if let lineRef { queryItems.append(URLQueryItem(name: "LineRef", value: lineRef)) } comps.queryItems = queryItems guard let url = comps.url else { return [] } let cfg = URLSessionConfiguration.ephemeral cfg.timeoutIntervalForRequest = 10 cfg.timeoutIntervalForResource = 15 let session = URLSession(configuration: cfg) defer { session.invalidateAndCancel() } guard let (data, _) = try? await session.data(from: url), let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any], let siri = json["Siri"] as? [String: Any], let delivery = siri["ServiceDelivery"] as? [String: Any], let vmdArray = delivery["VehicleMonitoringDelivery"] as? [[String: Any]], let vmd = vmdArray.first, let activities = vmd["VehicleActivity"] as? [[String: Any]] else { return [] } let isoParser: ISO8601DateFormatter = { let f = ISO8601DateFormatter() f.formatOptions = [.withInternetDateTime, .withFractionalSeconds] return f }() return activities.compactMap { activity -> BusVehicle? in guard let mvj = activity["MonitoredVehicleJourney"] as? [String: Any], let loc = mvj["VehicleLocation"] as? [String: Any], let lat = loc["Latitude"] as? Double, let lon = loc["Longitude"] as? Double else { return nil } let vehicleRef = (mvj["VehicleRef"] as? String) ?? "" let rawRoute = siriStringValue(mvj["PublishedLineName"]) ?? siriStringValue(mvj["LineRef"]) ?? lineRef ?? "" let cleanRoute = rawRoute.firstIndex(of: "_").map { String(rawRoute[rawRoute.index(after: $0)...]) } ?? rawRoute let destination = siriStringValue(mvj["DestinationName"]) ?? "" let bearing = (mvj["Bearing"] as? Double) ?? 0 // RecordedAtTime is at the activity level, not inside MonitoredVehicleJourney let recordedStr = activity["RecordedAtTime"] as? String ?? "" let recordedAt = isoParser.date(from: recordedStr) ?? Date() return BusVehicle(id: vehicleRef, route: cleanRoute, lineRef: lineRef ?? "", latitude: lat, longitude: lon, bearing: bearing, destination: destination, recordedAt: recordedAt) } } private static func siriStringValue(_ value: Any?) -> String? { if let s = value as? String { return s.isEmpty ? nil : s } if let a = value as? [String], let s = a.first { return s.isEmpty ? nil : s } if let a = value as? [Any], let s = a.first as? String { return s.isEmpty ? nil : s } return nil } }`BusVehicleService` structDefines the `BusVehicleService` struct.