MotorSignal-App/lib/services/weather/weather_service.dart

138 lines
4.4 KiB
Dart

import 'dart:async';
import 'dart:convert';
import 'package:flutter/foundation.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:http/http.dart' as http;
/// Weather and road surface telemetry derived from real-time meteorology.
@immutable
class WeatherReport {
final double temperatureC;
final double temperatureF;
final double precipitationMm;
final int weatherCode;
final String surfaceCondition;
final String gripStatus;
final bool isDry;
final bool isCold;
const WeatherReport({
required this.temperatureC,
required this.temperatureF,
required this.precipitationMm,
required this.weatherCode,
required this.surfaceCondition,
required this.gripStatus,
this.isDry = true,
this.isCold = false,
});
static const defaultReport = WeatherReport(
temperatureC: 21.0,
temperatureF: 70.0,
precipitationMm: 0.0,
weatherCode: 0,
surfaceCondition: 'Dry Asphalt',
gripStatus: '0% Precip • Grip Optimal',
isDry: true,
isCold: false,
);
}
class WeatherService {
final http.Client _client;
final Map<String, (DateTime, WeatherReport)> _cache = {};
static const Duration _cacheTtl = Duration(minutes: 10);
WeatherService({http.Client? client}) : _client = client ?? http.Client();
/// Fetches real-time weather and surface grip assessment from Open-Meteo.
Future<WeatherReport> fetchWeather(double latitude, double longitude) async {
final cacheKey =
'${latitude.toStringAsFixed(2)}_${longitude.toStringAsFixed(2)}';
final now = DateTime.now();
final cached = _cache[cacheKey];
if (cached != null && now.difference(cached.$1) < _cacheTtl) {
return cached.$2;
}
try {
final uri = Uri.parse(
'https://api.open-meteo.com/v1/forecast?'
'latitude=${latitude.toStringAsFixed(4)}&'
'longitude=${longitude.toStringAsFixed(4)}&'
'current=temperature_2m,relative_humidity_2m,precipitation,weather_code',
);
final response = await _client.get(uri).timeout(const Duration(seconds: 4));
if (response.statusCode == 200) {
final data = jsonDecode(response.body) as Map<String, dynamic>;
final current = data['current'] as Map<String, dynamic>?;
if (current != null) {
final tempC = (current['temperature_2m'] as num?)?.toDouble() ?? 21.0;
final precipMm = (current['precipitation'] as num?)?.toDouble() ?? 0.0;
final code = (current['weather_code'] as num?)?.toInt() ?? 0;
final tempF = (tempC * 9.0 / 5.0) + 32.0;
final isCold = tempC < 3.0;
final isDry = precipMm <= 0.1;
String surfaceCondition;
String gripStatus;
if (isCold && !isDry) {
surfaceCondition = 'Icy / Slush Hazard';
gripStatus = 'Severe Low Grip';
} else if (isCold) {
surfaceCondition = 'Cold Asphalt';
gripStatus = 'Tire Warmup Advised';
} else if (precipMm > 2.0) {
surfaceCondition = 'Heavy Wet Asphalt';
gripStatus = '${precipMm.toStringAsFixed(1)}mm • Standing Water';
} else if (precipMm > 0.1) {
surfaceCondition = 'Damp / Wet Surface';
gripStatus = '${precipMm.toStringAsFixed(1)}mm • Grip Reduced';
} else {
surfaceCondition = 'Dry Asphalt';
gripStatus = '0% Precip • Grip Optimal';
}
final report = WeatherReport(
temperatureC: tempC,
temperatureF: tempF,
precipitationMm: precipMm,
weatherCode: code,
surfaceCondition: surfaceCondition,
gripStatus: gripStatus,
isDry: isDry,
isCold: isCold,
);
_cache[cacheKey] = (now, report);
return report;
}
}
} catch (e) {
debugPrint('[WeatherService] Error fetching weather: $e');
}
return _cache[cacheKey]?.$2 ?? WeatherReport.defaultReport;
}
}
final weatherServiceProvider = Provider<WeatherService>((ref) {
return WeatherService();
});
final rideWeatherCoordinatesProvider = StateProvider<({double lat, double lng})?>((ref) => null);
final rideWeatherProvider = FutureProvider<WeatherReport>((ref) async {
final coords = ref.watch(rideWeatherCoordinatesProvider);
if (coords == null) {
return WeatherReport.defaultReport;
}
final service = ref.watch(weatherServiceProvider);
return service.fetchWeather(coords.lat, coords.lng);
});