|
|
@@ -0,0 +1,221 @@
|
|
|
+<?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;
|