import 'dart:convert'; import 'dart:math' as math; import 'package:http/http.dart' as http; import 'package:latlong2/latlong.dart'; class BusStop { const BusStop({ required this.id, required this.name, required this.position, this.stopLetter, this.towards, }); final String id; final String name; final LatLng position; // the letter on the physical bus stop flag e.g. "A", "ZH" final String? stopLetter; // may be null for parent stops final String? towards; } class TflBusStopService { TflBusStopService(); static const _baseUrl = 'https://api.tfl.gov.uk'; // max radius the TfL API accepts is 1600m but in practice // we clamp to 800 so we dont get swamped at low zoom static const _maxRadius = 800; Future> fetchNearby(LatLng center, {int radius = 600}) async { final r = radius.clamp(0, _maxRadius); final uri = Uri.parse( '$_baseUrl/StopPoint' '?stopTypes=NaptanPublicBusCoachTram' '&lat=${center.latitude}' '&lon=${center.longitude}' '&radius=$r' '&modes=bus', ); final response = await http.get(uri, headers: { 'Accept': 'application/json', }); if (response.statusCode != 200) { throw Exception('TfL StopPoint request failed (${response.statusCode})'); } final body = jsonDecode(response.body) as Map; final stopPoints = body['stopPoints']; if (stopPoints is! List) return const []; final stops = []; for (final raw in stopPoints) { if (raw is! Map) continue; final lat = (raw['lat'] as num?)?.toDouble(); final lon = (raw['lon'] as num?)?.toDouble(); if (lat == null || lon == null) continue; final id = raw['id'] as String? ?? ''; final name = raw['commonName'] as String? ?? ''; final stopLetter = raw['stopLetter'] as String?; String? towards; final addInfo = raw['additionalProperties']; if (addInfo is List) { for (final prop in addInfo) { if (prop is Map && prop['key'] == 'Towards') { towards = prop['value'] as String?; break; } } } stops.add(BusStop( id: id, name: name, position: LatLng(lat, lon), stopLetter: stopLetter, towards: towards, )); } return stops; } // rough haversine distance in metres, good enough for radius estimation static double metersPerPixel(double lat, double zoom) { const earthCircumference = 40075016.686; final latRad = lat * math.pi / 180.0; return (earthCircumference * math.cos(latRad)) / (256 * math.pow(2, zoom)); } }