import 'dart:typed_data'; import 'package:dio/dio.dart'; import 'radar_manifest.dart'; import 'radar_source.dart'; /// Radar frames published by our worker from Radar-DPC data. /// /// This talks to **our CDN**, never to `radar-api.protezionecivile.it`. The DPC /// API hands out pre-signed URLs that expire in minutes, serves whole-Italy /// GeoTIFFs a phone has no business decoding, and would be hit by every install /// at once. The worker is the only client of that API; the app reads the flat /// files it publishes. /// /// The manifest URL is the unit of configuration: frame paths inside it are /// relative, so the whole published tree can move between hosts without /// republishing the frames. class DpcRadarSource implements RadarSource { DpcRadarSource({required this.manifestUrl, Dio? client}) : _client = client ?? Dio( BaseOptions( connectTimeout: const Duration(seconds: 10), receiveTimeout: const Duration(seconds: 20), // Frames are PNG and the manifest is JSON; let this class decide // how to read each rather than have Dio guess. responseType: ResponseType.bytes, ), ); /// Absolute URL of `manifest.json`. final String manifestUrl; final Dio _client; RadarManifest? _cached; /// Drops the memoised manifest so the next read goes back to the CDN. void invalidate() => _cached = null; @override Future getLatestManifest() async { final Response> response; try { response = await _client.get>(manifestUrl); } on DioException catch (error) { throw RadarUnavailableException( 'could not fetch the radar manifest from $manifestUrl', cause: error, ); } final body = response.data; if (body == null || body.isEmpty) { throw RadarUnavailableException( 'radar manifest at $manifestUrl is empty', ); } try { return _cached = RadarManifest.parse(String.fromCharCodes(body)); } on FormatException catch (error) { // A malformed manifest means the worker published something wrong. There // is nothing the app can do about it, and reaching for a stale copy is // the caller's decision, so it surfaces as plain unavailability. throw RadarUnavailableException( 'radar manifest at $manifestUrl is malformed', cause: error, ); } } @override Future> getFrames() async { final cached = _cached; if (cached != null) return cached.frames; return (await getLatestManifest()).frames; } @override Future loadFrameBytes(RadarFrame frame) async { final url = resolveFrameUrl(frame); try { final response = await _client.get>(url); final body = response.data; if (body == null || body.isEmpty) { throw RadarUnavailableException('radar frame at $url is empty'); } return Uint8List.fromList(body); } on DioException catch (error) { throw RadarUnavailableException( 'could not fetch the radar frame at $url', cause: error, ); } } /// Resolves a frame's manifest-relative path against [manifestUrl]. String resolveFrameUrl(RadarFrame frame) => Uri.parse(manifestUrl).resolve(frame.path).toString(); }