Roadbound-Map-Utility/lib/pages/map/tfl/tfl_bus_stop_cache.dart

116 lines
3 KiB
Dart

import 'dart:convert';
import 'package:hive/hive.dart';
import 'package:latlong2/latlong.dart';
import 'package:rra_app/pages/map/tfl/tfl_bus_stop_service.dart';
class TflBusStopCache {
TflBusStopCache._();
static const _boxName = 'tfl_bus_stop_cache_v1';
static const _staleAfter = Duration(hours: 12);
static const _maxEntries = 512;
static Future<void> init() async {
if (!Hive.isBoxOpen(_boxName)) {
await Hive.openBox(_boxName);
}
}
static Box? get _box {
try {
if (!Hive.isBoxOpen(_boxName)) return null;
return Hive.box(_boxName);
} catch (_) {
return null;
}
}
// rounds to ~1km grid cells so nearby pans reuse the same cache entry
static String keyFor(LatLng center) {
final lat = (center.latitude * 100).round() / 100;
final lon = (center.longitude * 100).round() / 100;
return '$lat,$lon';
}
static List<BusStop>? peek(String key) {
final box = _box;
if (box == null) return null;
final entry = box.get(key);
if (entry is! Map) return null;
final ts = entry['ts'];
if (ts is! int) return null;
final age = DateTime.now().millisecondsSinceEpoch - ts;
if (age > _staleAfter.inMilliseconds) return null;
final raw = entry['stops'];
if (raw is! List) return null;
try {
return _decodeStops(raw);
} catch (_) {
return null;
}
}
static Future<void> store(String key, List<BusStop> stops) async {
final box = _box;
if (box == null) return;
await box.put(key, {
'ts': DateTime.now().millisecondsSinceEpoch,
'stops': _encodeStops(stops),
});
await _pruneIfNeeded(box);
}
static List<Map<String, dynamic>> _encodeStops(List<BusStop> stops) {
return stops.map((s) => {
'id': s.id,
'name': s.name,
'lat': s.position.latitude,
'lon': s.position.longitude,
if (s.stopLetter != null) 'stopLetter': s.stopLetter,
if (s.towards != null) 'towards': s.towards,
}).toList();
}
static List<BusStop> _decodeStops(List raw) {
final result = <BusStop>[];
for (final item in raw) {
if (item is! Map) continue;
final lat = (item['lat'] as num?)?.toDouble();
final lon = (item['lon'] as num?)?.toDouble();
if (lat == null || lon == null) continue;
result.add(BusStop(
id: item['id'] as String? ?? '',
name: item['name'] as String? ?? '',
position: LatLng(lat, lon),
stopLetter: item['stopLetter'] as String?,
towards: item['towards'] as String?,
));
}
return result;
}
static Future<void> _pruneIfNeeded(Box box) async {
final overflow = box.length - _maxEntries;
if (overflow <= 0) return;
final entries = box.keys.map((k) {
final v = box.get(k);
final ts = (v is Map && v['ts'] is int) ? v['ts'] as int : 0;
return MapEntry(k, ts);
}).toList()
..sort((a, b) => a.value.compareTo(b.value));
for (var i = 0; i < overflow; i++) {
await box.delete(entries[i].key);
}
}
}