102 lines
2.6 KiB
Dart
102 lines
2.6 KiB
Dart
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<List<BusStop>> 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<String, dynamic>;
|
|
final stopPoints = body['stopPoints'];
|
|
if (stopPoints is! List) return const [];
|
|
|
|
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;
|
|
|
|
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));
|
|
}
|
|
}
|