import 'dart:async'; import 'dart:convert'; import 'dart:math' as math; import 'package:http/http.dart' as http; import 'package:latlong2/latlong.dart'; import 'package:rra_app/pages/map/constants.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; final String? stopLetter; final String? towards; } class TflBusStopService { TflBusStopService(); // streams bus stops — emits once from cache, then again from TfL if fresher Stream> fetchInRect(LatLng sw, LatLng ne) async* { final uri = Uri.parse( '$kBackendBaseUrl/stops' '?minLat=${sw.latitude}' '&minLon=${sw.longitude}' '&maxLat=${ne.latitude}' '&maxLon=${ne.longitude}', ); final client = http.Client(); try { final request = http.Request('GET', uri); final streamed = await client.send(request); final buffer = StringBuffer(); await for (final chunk in streamed.stream.transform(utf8.decoder)) { buffer.write(chunk); final text = buffer.toString(); // SSE events are delimited by \n\n int idx; var remaining = text; while ((idx = remaining.indexOf('\n\n')) != -1) { final event = remaining.substring(0, idx).trim(); remaining = remaining.substring(idx + 2); if (event.startsWith('data: ')) { final json = event.substring(6); final stops = _parseStops(json); if (stops != null) yield stops; } } buffer.clear(); buffer.write(remaining); } } finally { client.close(); } } static List? _parseStops(String json) { try { final body = jsonDecode(json) as Map; final stopPoints = body['stopPoints']; if (stopPoints is! List) return null; 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; 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: raw['id'] as String? ?? '', name: raw['commonName'] as String? ?? '', position: LatLng(lat, lon), stopLetter: raw['stopLetter'] as String?, towards: towards, )); } return stops; } catch (_) { return null; } } 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)); } }