weather.php 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221
  1. <?php
  2. /**
  3. * api/weather.php
  4. *
  5. * Returns current weather + 7-day forecast + past 7 days of daily rainfall
  6. * for the logged-in user's location, sourced from Open-Meteo (free, no API key).
  7. *
  8. * Flow:
  9. * 1. Load user's postcode from client_records
  10. * 2. Geocode postcode → lat/lng via Nominatim (OpenStreetMap, free)
  11. * 3. Call Open-Meteo for current conditions, daily forecast, past rainfall
  12. * 4. Cache result in session for 30 minutes
  13. * 5. Return JSON
  14. *
  15. * GET params: none required (uses session user)
  16. * Optional: ?lat=<lat>&lng=<lng> (manual override)
  17. */
  18. if (session_status() === PHP_SESSION_NONE) {
  19. session_start();
  20. }
  21. require_once __DIR__ . '/../config/database.php';
  22. require_once __DIR__ . '/../lib/auth.php';
  23. header('Content-Type: application/json');
  24. if (!isLoggedIn()) {
  25. http_response_code(401);
  26. echo json_encode(['error' => 'Not authenticated']);
  27. exit;
  28. }
  29. $userId = getCurrentUserId();
  30. // ── Manual lat/lng override ───────────────────────────────────────────────────
  31. $manualLat = isset($_GET['lat']) && is_numeric($_GET['lat']) ? (float)$_GET['lat'] : null;
  32. $manualLng = isset($_GET['lng']) && is_numeric($_GET['lng']) ? (float)$_GET['lng'] : null;
  33. // ── Session cache (30 min) ────────────────────────────────────────────────────
  34. $cacheKey = 'weather_' . $userId . ($manualLat ? "_{$manualLat}_{$manualLng}" : '');
  35. if (
  36. !$manualLat
  37. && isset($_SESSION[$cacheKey], $_SESSION[$cacheKey . '_ts'])
  38. && (time() - $_SESSION[$cacheKey . '_ts']) < 1800
  39. ) {
  40. echo $_SESSION[$cacheKey];
  41. exit;
  42. }
  43. // ── Resolve lat/lng ───────────────────────────────────────────────────────────
  44. $lat = $manualLat;
  45. $lng = $manualLng;
  46. $locationLabel = 'Your Location';
  47. if (!$lat) {
  48. // Load client postcode
  49. $postcode = null;
  50. try {
  51. $pdo = getDBConnection();
  52. $stmt = $pdo->prepare(
  53. 'SELECT state_postcode, address FROM client_records WHERE modx_user_id = ? LIMIT 1'
  54. );
  55. $stmt->execute([$userId]);
  56. $client = $stmt->fetch(PDO::FETCH_ASSOC);
  57. if ($client && !empty($client['state_postcode'])) {
  58. // Extract just the postcode portion (e.g. "SA 5000" → "5000")
  59. preg_match('/\d{4}/', $client['state_postcode'], $m);
  60. $postcode = $m[0] ?? null;
  61. $locationLabel = trim($client['state_postcode']);
  62. }
  63. } catch (PDOException $e) {
  64. error_log('weather.php DB error: ' . $e->getMessage());
  65. }
  66. if ($postcode) {
  67. // Geocode via Nominatim
  68. $geocodeUrl = 'https://nominatim.openstreetmap.org/search?'
  69. . http_build_query([
  70. 'postalcode' => $postcode,
  71. 'country' => 'AU',
  72. 'format' => 'json',
  73. 'limit' => 1,
  74. ]);
  75. $ch = curl_init($geocodeUrl);
  76. curl_setopt_array($ch, [
  77. CURLOPT_RETURNTRANSFER => true,
  78. CURLOPT_TIMEOUT => 5,
  79. CURLOPT_HTTPHEADER => ['User-Agent: CropMonitor/1.0 (contact@cropmonitor.com.au)'],
  80. ]);
  81. $geoResp = curl_exec($ch);
  82. curl_close($ch);
  83. if ($geoResp) {
  84. $geoData = json_decode($geoResp, true);
  85. if (!empty($geoData[0]['lat'])) {
  86. $lat = (float)$geoData[0]['lat'];
  87. $lng = (float)$geoData[0]['lon'];
  88. }
  89. }
  90. }
  91. // Default to Adelaide if geocoding fails
  92. if (!$lat) {
  93. $lat = -34.9285;
  94. $lng = 138.6007;
  95. $locationLabel = 'Adelaide, SA (default)';
  96. }
  97. }
  98. // ── Open-Meteo API call ───────────────────────────────────────────────────────
  99. // current: temperature, apparent temp, weather code, wind speed, humidity, precipitation
  100. // daily (14 days: 7 past + 7 forecast): max/min temp, precipitation sum, weather code
  101. $meteoUrl = 'https://api.open-meteo.com/v1/forecast?' . http_build_query([
  102. 'latitude' => round($lat, 4),
  103. 'longitude' => round($lng, 4),
  104. 'current' => 'temperature_2m,apparent_temperature,weather_code,wind_speed_10m,relative_humidity_2m,precipitation',
  105. 'daily' => 'weather_code,temperature_2m_max,temperature_2m_min,precipitation_sum,wind_speed_10m_max',
  106. 'timezone' => 'Australia/Adelaide',
  107. 'past_days' => 7,
  108. 'forecast_days' => 7,
  109. 'wind_speed_unit' => 'kmh',
  110. ]);
  111. $ch = curl_init($meteoUrl);
  112. curl_setopt_array($ch, [
  113. CURLOPT_RETURNTRANSFER => true,
  114. CURLOPT_TIMEOUT => 8,
  115. ]);
  116. $meteoResp = curl_exec($ch);
  117. $meteoCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
  118. curl_close($ch);
  119. if (!$meteoResp || $meteoCode !== 200) {
  120. http_response_code(502);
  121. echo json_encode(['error' => 'Could not fetch weather data from Open-Meteo']);
  122. exit;
  123. }
  124. $meteo = json_decode($meteoResp, true);
  125. // ── WMO weather code → description + icon name ────────────────────────────────
  126. // Icon names match Skycons library used in dashboard
  127. function wmoToCondition(int $code): array
  128. {
  129. $map = [
  130. 0 => ['Clear', 'clear-day'],
  131. 1 => ['Mostly Clear', 'clear-day'],
  132. 2 => ['Partly Cloudy', 'partly-cloudy-day'],
  133. 3 => ['Overcast', 'cloudy'],
  134. 45 => ['Fog', 'fog'],
  135. 48 => ['Icy Fog', 'fog'],
  136. 51 => ['Light Drizzle', 'rain'],
  137. 53 => ['Drizzle', 'rain'],
  138. 55 => ['Heavy Drizzle', 'rain'],
  139. 61 => ['Light Rain', 'rain'],
  140. 63 => ['Rain', 'rain'],
  141. 65 => ['Heavy Rain', 'rain'],
  142. 71 => ['Light Snow', 'snow'],
  143. 73 => ['Snow', 'snow'],
  144. 75 => ['Heavy Snow', 'snow'],
  145. 77 => ['Snow Grains', 'sleet'],
  146. 80 => ['Light Showers', 'showers-day'],
  147. 81 => ['Showers', 'showers-day'],
  148. 82 => ['Heavy Showers', 'showers-day'],
  149. 85 => ['Snow Showers', 'snow'],
  150. 86 => ['Heavy Snow Showers', 'snow'],
  151. 95 => ['Thunderstorm', 'thunderstorm'],
  152. 96 => ['Hail Storm', 'hail'],
  153. 99 => ['Heavy Hail Storm', 'hail'],
  154. ];
  155. return $map[$code] ?? ['Unknown', 'cloudy'];
  156. }
  157. // ── Build response ────────────────────────────────────────────────────────────
  158. $current = $meteo['current'] ?? [];
  159. $daily = $meteo['daily'] ?? [];
  160. [$curLabel, $curIcon] = wmoToCondition((int)($current['weather_code'] ?? 0));
  161. $days = [];
  162. $today = date('Y-m-d');
  163. foreach (($daily['time'] ?? []) as $i => $date) {
  164. [$label, $icon] = wmoToCondition((int)($daily['weather_code'][$i] ?? 0));
  165. $days[] = [
  166. 'date' => $date,
  167. 'is_past' => $date < $today,
  168. 'is_today' => $date === $today,
  169. 'label' => $label,
  170. 'icon' => $icon,
  171. 'temp_max' => round($daily['temperature_2m_max'][$i] ?? 0, 1),
  172. 'temp_min' => round($daily['temperature_2m_min'][$i] ?? 0, 1),
  173. 'rain' => round($daily['precipitation_sum'][$i] ?? 0, 1),
  174. 'wind_max' => round($daily['wind_speed_10m_max'][$i] ?? 0, 0),
  175. 'day_name' => date('D', strtotime($date)),
  176. 'day_short' => date('j M', strtotime($date)),
  177. ];
  178. }
  179. $result = json_encode([
  180. 'location' => $locationLabel,
  181. 'lat' => $lat,
  182. 'lng' => $lng,
  183. 'current' => [
  184. 'temp' => round($current['temperature_2m'] ?? 0, 1),
  185. 'feels_like' => round($current['apparent_temperature'] ?? 0, 1),
  186. 'humidity' => round($current['relative_humidity_2m'] ?? 0, 0),
  187. 'wind' => round($current['wind_speed_10m'] ?? 0, 0),
  188. 'rain' => round($current['precipitation'] ?? 0, 1),
  189. 'label' => $curLabel,
  190. 'icon' => $curIcon,
  191. ],
  192. 'days' => $days,
  193. ]);
  194. // Cache in session
  195. $_SESSION[$cacheKey] = $result;
  196. $_SESSION[$cacheKey . '_ts'] = time();
  197. echo $result;