138 lines
4.4 KiB
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);
|
|
});
|