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 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? 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 store(String key, List 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> _encodeStops(List 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 _decodeStops(List raw) { final result = []; 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 _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); } } }