| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221 |
- <?php
- /**
- * api/weather.php
- *
- * Returns current weather + 7-day forecast + past 7 days of daily rainfall
- * for the logged-in user's location, sourced from Open-Meteo (free, no API key).
- *
- * Flow:
- * 1. Load user's postcode from client_records
- * 2. Geocode postcode → lat/lng via Nominatim (OpenStreetMap, free)
- * 3. Call Open-Meteo for current conditions, daily forecast, past rainfall
- * 4. Cache result in session for 30 minutes
- * 5. Return JSON
- *
- * GET params: none required (uses session user)
- * Optional: ?lat=<lat>&lng=<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;
|