import { AQIData, WeatherData } from '../types'; import { StorageService } from './storageService'; // Robust Geolocation Helper const getCoordinates = (): Promise<{lat: number, lon: number, error?: string}> => { return new Promise(async (resolve) => { // Check App Settings Permission First const settings = await StorageService.getSettings(); if (!settings.permissions.location) { // Fallback to Kathmandu if privacy is on resolve({ lat: 27.7172, lon: 85.3240, error: "Location access disabled in settings." }); return; } if (!navigator.geolocation) { resolve({ lat: 27.7172, lon: 85.3240, error: "Geolocation is not supported by this browser." }); return; } const success = (position: GeolocationPosition) => { resolve({ lat: position.coords.latitude, lon: position.coords.longitude }); }; const error = (err: GeolocationPositionError) => { let errorMsg = "An unknown error occurred."; switch(err.code) { case err.PERMISSION_DENIED: errorMsg = "User denied the request for Geolocation."; break; case err.POSITION_UNAVAILABLE: errorMsg = "Location information is unavailable."; break; case err.TIMEOUT: errorMsg = "The request to get user location timed out."; break; default: errorMsg = "An unknown error occurred."; break; } console.warn("Geolocation Error:", errorMsg); // Fallback to Kathmandu resolve({ lat: 27.7172, lon: 85.3240, error: errorMsg }); }; navigator.geolocation.getCurrentPosition(success, error, { enableHighAccuracy: settings.gpsAccuracy === 'high', timeout: 10000, maximumAge: 0 }); }); }; const getReverseGeocoding = async (lat: number, lon: number): Promise => { try { // Using OpenStreetMap Nominatim for reverse geocoding (free, requires attribution) const response = await fetch(`https://nominatim.openstreetmap.org/reverse?format=json&lat=${lat}&lon=${lon}&zoom=12&addressdetails=1`, { headers: { 'User-Agent': 'RudrakshaApp/1.0' } }); if (response.ok) { const data = await response.json(); const addr = data.address; // Prioritize meaningful location names return addr.city || addr.town || addr.village || addr.municipality || addr.suburb || addr.county || "Unknown Location"; } } catch (e) { console.warn("Reverse geocoding failed", e); } return `Lat: ${lat.toFixed(2)}, Lon: ${lon.toFixed(2)}`; }; const mapWmoCodeToCondition = (code: number): WeatherData['condition'] => { // WMO Weather interpretation codes (WW) if (code === 0 || code === 1) return 'Sunny'; if (code === 2 || code === 3) return 'Cloudy'; if (code === 45 || code === 48) return 'Foggy'; if (code >= 51 && code <= 67) return 'Rainy'; if (code >= 80 && code <= 82) return 'Rainy'; if (code >= 71 && code <= 77) return 'Rainy'; // Snow/Sleet mapped to Rainy for simplicity if (code >= 95) return 'Stormy'; return 'Cloudy'; // Default }; export const AirQualityService = { getAQI: async (): Promise => { try { const { lat, lon } = await getCoordinates(); // Open-Meteo Air Quality API const response = await fetch( `https://air-quality-api.open-meteo.com/v1/air-quality?latitude=${lat}&longitude=${lon}¤t=us_aqi,pm2_5,pm10,nitrogen_dioxide,ozone&timezone=auto` ); if (!response.ok) throw new Error('AQI API failed'); const data = await response.json(); const current = data.current; // Use US AQI standard which is widely recognized const aqiValue = current.us_aqi || 0; // Determine dominant pollutant (simple logic) let pollutant = 'PM2.5'; if (current.pm10 > current.pm2_5 && current.pm10 > current.nitrogen_dioxide) pollutant = 'PM10'; if (current.nitrogen_dioxide > current.pm10) pollutant = 'NO2'; if (current.ozone > current.us_aqi) pollutant = 'O3'; const locationName = await getReverseGeocoding(lat, lon); return getNepalAQIStatus(aqiValue, locationName, pollutant); } catch (error) { console.warn("Using mock AQI data due to API error", error); // Fallback return new Promise((resolve) => { setTimeout(() => { resolve(generateMockAQI("Kathmandu (Simulated)")); }, 800); }); } }, getWeather: async (): Promise => { try { const { lat, lon } = await getCoordinates(); const locationName = await getReverseGeocoding(lat, lon); const response = await fetch( `https://api.open-meteo.com/v1/forecast?latitude=${lat}&longitude=${lon}¤t=temperature_2m,relative_humidity_2m,apparent_temperature,is_day,weather_code,wind_speed_10m&daily=uv_index_max&timezone=auto` ); if (!response.ok) throw new Error('Weather API failed'); const data = await response.json(); const current = data.current; const daily = data.daily; return { temp: Math.round(current.temperature_2m), condition: mapWmoCodeToCondition(current.weather_code), humidity: current.relative_humidity_2m, windSpeed: current.wind_speed_10m, uvIndex: daily.uv_index_max && daily.uv_index_max.length > 0 ? daily.uv_index_max[0] : 0, feelsLike: Math.round(current.apparent_temperature), location: locationName }; } catch (error) { console.warn("Using mock weather data due to API error", error); // Fallback to mock data if API fails return new Promise((resolve) => { setTimeout(() => { const conditions: WeatherData['condition'][] = ['Sunny', 'Cloudy', 'Rainy', 'Foggy']; const randomCondition = conditions[Math.floor(Math.random() * conditions.length)]; const temp = Math.floor(Math.random() * (30 - 15 + 1)) + 15; resolve({ temp: temp, condition: randomCondition, humidity: Math.floor(Math.random() * (90 - 40 + 1)) + 40, windSpeed: Math.floor(Math.random() * 15) + 2, uvIndex: randomCondition === 'Sunny' ? Math.floor(Math.random() * 5) + 3 : 1, feelsLike: temp + 2, location: "Kathmandu (Simulated)" }); }, 800); }); } } }; const generateMockAQI = (locationName: string): AQIData => { // Simulate AQI typical for Nepal context (often Moderate to Poor) const aqi = Math.floor(Math.random() * (180 - 45 + 1)) + 45; return getNepalAQIStatus(aqi, locationName, 'PM2.5'); }; const getNepalAQIStatus = (aqi: number, location: string, pollutant: string = 'PM2.5'): AQIData => { let status = ''; let color = ''; let advice = ''; // Nepal Government (Department of Environment) Scale / US AQI Scale interpretation if (aqi <= 50) { status = 'Good'; color = '#00B050'; // Green advice = 'Air is fresh. No mask needed. Enjoy the outdoors!'; } else if (aqi <= 100) { status = 'Satisfactory'; color = '#A8D08D'; // Yellow-Green advice = 'Acceptable quality. Sensitive people should check air before prolonged exertion.'; } else if (aqi <= 150) { status = 'Moderately Polluted'; color = '#F4B083'; // Orange advice = 'Sensitive groups (children, elderly) should wear masks. Limit outdoor exertion.'; } else if (aqi <= 200) { status = 'Poor'; color = '#FFC000'; // Amber/Yellow (as requested) usually implies caution advice = 'Everyone should wear a mask (N95 recommended). Avoid outdoor activities.'; } else if (aqi <= 300) { status = 'Very Poor'; color = '#FF0000'; // Red advice = 'Health alert: serious health effects possible. Wear N95 mask. Stay indoors.'; } else { status = 'Severe'; color = '#C00000'; // Dark Red advice = 'Emergency conditions. The entire population is likely to be affected.'; } return { aqi, status, color, advice, pollutant, location }; };