116 lines
3.1 KiB
Dart
116 lines
3.1 KiB
Dart
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<List<BusStop>> 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<BusStop>? _parseStops(String json) {
|
|
try {
|
|
final body = jsonDecode(json) as Map<String, dynamic>;
|
|
final stopPoints = body['stopPoints'];
|
|
if (stopPoints is! List) return null;
|
|
|
|
final stops = <BusStop>[];
|
|
for (final raw in stopPoints) {
|
|
if (raw is! Map<String, dynamic>) 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));
|
|
}
|
|
}
|