&lng= (manual override) */ if (session_status() === PHP_SESSION_NONE) { session_start(); } require_once __DIR__ . '/../config/database.php'; require_once __DIR__ . '/../lib/auth.php'; header('Content-Type: application/json'); if (!isLoggedIn()) { http_response_code(401); echo json_encode(['error' => 'Not authenticated']); exit; } $userId = getCurrentUserId(); // ── Manual lat/lng override ─────────────────────────────────────────────────── $manualLat = isset($_GET['lat']) && is_numeric($_GET['lat']) ? (float)$_GET['lat'] : null; $manualLng = isset($_GET['lng']) && is_numeric($_GET['lng']) ? (float)$_GET['lng'] : null; // ── Session cache (30 min) ──────────────────────────────────────────────────── $cacheKey = 'weather_' . $userId . ($manualLat ? "_{$manualLat}_{$manualLng}" : ''); if ( !$manualLat && isset($_SESSION[$cacheKey], $_SESSION[$cacheKey . '_ts']) && (time() - $_SESSION[$cacheKey . '_ts']) < 1800 ) { echo $_SESSION[$cacheKey]; exit; } // ── Resolve lat/lng ─────────────────────────────────────────────────────────── $lat = $manualLat; $lng = $manualLng; $locationLabel = 'Your Location'; if (!$lat) { // Load client postcode $postcode = null; try { $pdo = getDBConnection(); $stmt = $pdo->prepare( 'SELECT state_postcode, address FROM client_records WHERE modx_user_id = ? LIMIT 1' ); $stmt->execute([$userId]); $client = $stmt->fetch(PDO::FETCH_ASSOC); if ($client && !empty($client['state_postcode'])) { // Extract just the postcode portion (e.g. "SA 5000" → "5000") preg_match('/\d{4}/', $client['state_postcode'], $m); $postcode = $m[0] ?? null; $locationLabel = trim($client['state_postcode']); } } catch (PDOException $e) { error_log('weather.php DB error: ' . $e->getMessage()); } if ($postcode) { // Geocode via Nominatim $geocodeUrl = 'https://nominatim.openstreetmap.org/search?' . http_build_query([ 'postalcode' => $postcode, 'country' => 'AU', 'format' => 'json', 'limit' => 1, ]); $ch = curl_init($geocodeUrl); curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 5, CURLOPT_HTTPHEADER => ['User-Agent: CropMonitor/1.0 (contact@cropmonitor.com.au)'], ]); $geoResp = curl_exec($ch); curl_close($ch); if ($geoResp) { $geoData = json_decode($geoResp, true); if (!empty($geoData[0]['lat'])) { $lat = (float)$geoData[0]['lat']; $lng = (float)$geoData[0]['lon']; } } } // Default to Adelaide if geocoding fails if (!$lat) { $lat = -34.9285; $lng = 138.6007; $locationLabel = 'Adelaide, SA (default)'; } } // ── Open-Meteo API call ─────────────────────────────────────────────────────── // current: temperature, apparent temp, weather code, wind speed, humidity, precipitation // daily (14 days: 7 past + 7 forecast): max/min temp, precipitation sum, weather code $meteoUrl = 'https://api.open-meteo.com/v1/forecast?' . http_build_query([ 'latitude' => round($lat, 4), 'longitude' => round($lng, 4), 'current' => 'temperature_2m,apparent_temperature,weather_code,wind_speed_10m,relative_humidity_2m,precipitation', 'daily' => 'weather_code,temperature_2m_max,temperature_2m_min,precipitation_sum,wind_speed_10m_max', 'timezone' => 'Australia/Adelaide', 'past_days' => 7, 'forecast_days' => 7, 'wind_speed_unit' => 'kmh', ]); $ch = curl_init($meteoUrl); curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 8, ]); $meteoResp = curl_exec($ch); $meteoCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); if (!$meteoResp || $meteoCode !== 200) { http_response_code(502); echo json_encode(['error' => 'Could not fetch weather data from Open-Meteo']); exit; } $meteo = json_decode($meteoResp, true); // ── WMO weather code → description + icon name ──────────────────────────────── // Icon names match Skycons library used in dashboard function wmoToCondition(int $code): array { $map = [ 0 => ['Clear', 'clear-day'], 1 => ['Mostly Clear', 'clear-day'], 2 => ['Partly Cloudy', 'partly-cloudy-day'], 3 => ['Overcast', 'cloudy'], 45 => ['Fog', 'fog'], 48 => ['Icy Fog', 'fog'], 51 => ['Light Drizzle', 'rain'], 53 => ['Drizzle', 'rain'], 55 => ['Heavy Drizzle', 'rain'], 61 => ['Light Rain', 'rain'], 63 => ['Rain', 'rain'], 65 => ['Heavy Rain', 'rain'], 71 => ['Light Snow', 'snow'], 73 => ['Snow', 'snow'], 75 => ['Heavy Snow', 'snow'], 77 => ['Snow Grains', 'sleet'], 80 => ['Light Showers', 'showers-day'], 81 => ['Showers', 'showers-day'], 82 => ['Heavy Showers', 'showers-day'], 85 => ['Snow Showers', 'snow'], 86 => ['Heavy Snow Showers', 'snow'], 95 => ['Thunderstorm', 'thunderstorm'], 96 => ['Hail Storm', 'hail'], 99 => ['Heavy Hail Storm', 'hail'], ]; return $map[$code] ?? ['Unknown', 'cloudy']; } // ── Build response ──────────────────────────────────────────────────────────── $current = $meteo['current'] ?? []; $daily = $meteo['daily'] ?? []; [$curLabel, $curIcon] = wmoToCondition((int)($current['weather_code'] ?? 0)); $days = []; $today = date('Y-m-d'); foreach (($daily['time'] ?? []) as $i => $date) { [$label, $icon] = wmoToCondition((int)($daily['weather_code'][$i] ?? 0)); $days[] = [ 'date' => $date, 'is_past' => $date < $today, 'is_today' => $date === $today, 'label' => $label, 'icon' => $icon, 'temp_max' => round($daily['temperature_2m_max'][$i] ?? 0, 1), 'temp_min' => round($daily['temperature_2m_min'][$i] ?? 0, 1), 'rain' => round($daily['precipitation_sum'][$i] ?? 0, 1), 'wind_max' => round($daily['wind_speed_10m_max'][$i] ?? 0, 0), 'day_name' => date('D', strtotime($date)), 'day_short' => date('j M', strtotime($date)), ]; } $result = json_encode([ 'location' => $locationLabel, 'lat' => $lat, 'lng' => $lng, 'current' => [ 'temp' => round($current['temperature_2m'] ?? 0, 1), 'feels_like' => round($current['apparent_temperature'] ?? 0, 1), 'humidity' => round($current['relative_humidity_2m'] ?? 0, 0), 'wind' => round($current['wind_speed_10m'] ?? 0, 0), 'rain' => round($current['precipitation'] ?? 0, 1), 'label' => $curLabel, 'icon' => $curIcon, ], 'days' => $days, ]); // Cache in session $_SESSION[$cacheKey] = $result; $_SESSION[$cacheKey . '_ts'] = time(); echo $result;