sojorn/sojorn_app/lib/services/local_intel_service.dart
2026-02-15 00:33:24 -06:00

261 lines
7.6 KiB
Dart

import 'dart:convert';
import 'package:http/http.dart' as http;
import '../models/local_intel.dart';
/// Service for fetching local environmental intelligence data.
/// Uses OpenMeteo (free, no API key) and Overpass API (OpenStreetMap).
class LocalIntelService {
static const String _weatherBaseUrl = 'https://api.open-meteo.com/v1/forecast';
static const String _airQualityBaseUrl = 'https://air-quality-api.open-meteo.com/v1/air-quality';
static const String _overpassBaseUrl = 'https://overpass-api.de/api/interpreter';
/// Fetch current weather conditions
Future<WeatherConditions?> fetchWeather(double lat, double lng) async {
try {
final uri = Uri.parse(_weatherBaseUrl).replace(queryParameters: {
'latitude': lat.toString(),
'longitude': lng.toString(),
'current': [
'temperature_2m',
'weather_code',
'uv_index',
'wind_speed_10m',
'relative_humidity_2m',
'apparent_temperature',
].join(','),
'temperature_unit': 'fahrenheit',
'wind_speed_unit': 'mph',
'timezone': 'auto',
});
final response = await http.get(uri).timeout(
const Duration(seconds: 10),
);
if (response.statusCode == 200) {
final json = jsonDecode(response.body) as Map<String, dynamic>;
return WeatherConditions.fromJson(json);
}
} catch (e) {
// Log error but don't crash - return null
}
return null;
}
/// Fetch air quality and environmental hazards
Future<EnvironmentalHazards?> fetchAirQuality(double lat, double lng) async {
try {
final uri = Uri.parse(_airQualityBaseUrl).replace(queryParameters: {
'latitude': lat.toString(),
'longitude': lng.toString(),
'current': [
'us_aqi',
'pm2_5',
'pm10',
'grass_pollen',
'birch_pollen',
'olive_pollen',
'ragweed_pollen',
'uv_index',
].join(','),
'timezone': 'auto',
});
final response = await http.get(uri).timeout(
const Duration(seconds: 10),
);
if (response.statusCode == 200) {
final json = jsonDecode(response.body) as Map<String, dynamic>;
return EnvironmentalHazards.fromJson(json);
}
} catch (e) {
// Log error but don't crash - return null
}
return null;
}
/// Fetch sunrise/sunset and visibility data
Future<VisibilityData?> fetchVisibilityData(double lat, double lng) async {
try {
final uri = Uri.parse(_weatherBaseUrl).replace(queryParameters: {
'latitude': lat.toString(),
'longitude': lng.toString(),
'daily': [
'sunrise',
'sunset',
'daylight_duration',
].join(','),
'timezone': 'auto',
'forecast_days': '1',
});
final response = await http.get(uri).timeout(
const Duration(seconds: 10),
);
if (response.statusCode == 200) {
final json = jsonDecode(response.body) as Map<String, dynamic>;
return VisibilityData.fromJson(json);
}
} catch (e) {
// Log error but don't crash - return null
}
return null;
}
/// Fetch nearby public resources using Overpass API
/// Searches for libraries, parks, hospitals, police stations, pharmacies, fire stations
Future<List<PublicResource>> findNearbyResources(
double lat,
double lng, {
double radiusMeters = 2000,
List<ResourceType>? types,
}) async {
try {
// Build Overpass QL query
final query = _buildOverpassQuery(lat, lng, radiusMeters, types);
final response = await http.post(
Uri.parse(_overpassBaseUrl),
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
body: 'data=${Uri.encodeComponent(query)}',
).timeout(
const Duration(seconds: 15),
);
if (response.statusCode == 200) {
final json = jsonDecode(response.body) as Map<String, dynamic>;
final elements = json['elements'] as List<dynamic>? ?? [];
final resources = elements
.map((e) => PublicResource.fromOverpassElement(
e as Map<String, dynamic>,
lat,
lng,
))
.toList();
// Sort by distance
resources.sort((a, b) =>
(a.distanceMeters ?? double.infinity)
.compareTo(b.distanceMeters ?? double.infinity));
return resources;
}
} catch (e) {
// Log error but don't crash - return empty list
}
return [];
}
/// Build Overpass QL query for nearby amenities
String _buildOverpassQuery(
double lat,
double lng,
double radius,
List<ResourceType>? types,
) {
final amenities = <String>[];
final leisure = <String>[];
final targetTypes = types ??
[
ResourceType.library,
ResourceType.park,
ResourceType.hospital,
ResourceType.police,
ResourceType.pharmacy,
ResourceType.fireStation,
];
for (final type in targetTypes) {
switch (type) {
case ResourceType.library:
amenities.add('library');
break;
case ResourceType.hospital:
amenities.add('hospital');
break;
case ResourceType.police:
amenities.add('police');
break;
case ResourceType.pharmacy:
amenities.add('pharmacy');
break;
case ResourceType.fireStation:
amenities.add('fire_station');
break;
case ResourceType.park:
leisure.add('park');
break;
case ResourceType.other:
break;
}
}
final buffer = StringBuffer();
buffer.writeln('[out:json][timeout:15];');
buffer.writeln('(');
// Add amenity queries
for (final amenity in amenities) {
buffer.writeln(' node["amenity"="$amenity"](around:$radius,$lat,$lng);');
buffer.writeln(' way["amenity"="$amenity"](around:$radius,$lat,$lng);');
}
// Add leisure queries
for (final l in leisure) {
buffer.writeln(' node["leisure"="$l"](around:$radius,$lat,$lng);');
buffer.writeln(' way["leisure"="$l"](around:$radius,$lat,$lng);');
}
buffer.writeln(');');
buffer.writeln('out center tags;');
return buffer.toString();
}
/// Fetch all local intel data at once
Future<LocalIntelData> fetchAllIntel(double lat, double lng) async {
// Fetch all data in parallel
final results = await Future.wait([
fetchWeather(lat, lng),
fetchAirQuality(lat, lng),
fetchVisibilityData(lat, lng),
findNearbyResources(lat, lng),
]);
return LocalIntelData(
weather: results[0] as WeatherConditions?,
hazards: results[1] as EnvironmentalHazards?,
visibility: results[2] as VisibilityData?,
resources: results[3] as List<PublicResource>,
fetchedAt: DateTime.now(),
);
}
/// Fetch only weather-related intel (faster, fewer API calls)
Future<LocalIntelData> fetchWeatherIntel(double lat, double lng) async {
final results = await Future.wait([
fetchWeather(lat, lng),
fetchVisibilityData(lat, lng),
]);
return LocalIntelData(
weather: results[0] as WeatherConditions?,
visibility: results[1] as VisibilityData?,
fetchedAt: DateTime.now(),
);
}
/// Fetch only hazard-related intel
Future<LocalIntelData> fetchHazardIntel(double lat, double lng) async {
final hazards = await fetchAirQuality(lat, lng);
return LocalIntelData(
hazards: hazards,
fetchedAt: DateTime.now(),
);
}
}