|
<?php |
|
/** |
|
* Weather Forecast Script |
|
* |
|
* This script retrieves weather forecast and current weather data for a list of cities |
|
* based on their city IDs and visit dates. It fetches data from the OpenWeatherMap API, |
|
* caches the data, and processes it to find the forecast for noon on specific visit dates. |
|
* The results are then formatted and output as JSON. |
|
* |
|
* Features: |
|
* - Fetches weather forecast and current weather data from the OpenWeatherMap API. |
|
* - Caches the fetched data to reduce API calls and improve performance. |
|
* - Retrieves and processes the forecast for noon on specific visit dates. |
|
* - Includes cache metadata (hit/miss status and cache age) in the output. |
|
* - Formats the visit date in German for display purposes. |
|
* |
|
* Functions: |
|
* - fetchFromAPI($url): Fetches data from the specified API URL using cURL. |
|
* - getWeatherForecast($cityId, $apiKey): Retrieves and caches the weather forecast data for a city. |
|
* - getCurrentWeather($cityId, $apiKey): Retrieves and caches the current weather data for a city. |
|
* - getNoonForecast($weatherData, $dateVisited): Finds the forecast for noon or the closest time on the visit date. |
|
* - processForecast($noonForecast): Processes the forecast data to extract relevant information. |
|
* - getCityInfo($weatherData, $currentWeatherData): Retrieves city information from the weather data. |
|
* - kelvinToCelsius($kelvin): Converts temperature from Kelvin to Celsius. |
|
* - getRelativeDate($dateVisited, $currentDate): Formats the visit date in German. |
|
* |
|
* |
|
* Weaknesses and Limitations: |
|
* - Timezone Issues: The script assumes the server's timezone for date and time operations. It does not account for the timezone differences between the server and the locations being queried. |
|
* - No Cache Expiry for Permanent Cache: The permanent cache never expires, which may lead to outdated data being used indefinitely. |
|
* - Limited Error Handling: The script does not handle errors returned from the API calls gracefully. |
|
* - |
|
* |
|
* Usage: |
|
* 1. Create a .env file with your OpenWeatherMap API key. |
|
* 2. List the city IDs and their corresponding visit dates. |
|
* 3. Run the script to fetch, process, and output the weather data as JSON. |
|
* |
|
* Example .env file: |
|
* ```ini |
|
* OPENWEATHERMAP_APIKEY=your_api_key_here |
|
* ``` |
|
* |
|
* Example usage in PHP script: |
|
* ```php |
|
* <?php |
|
* $cityVisits = [ |
|
* ['cityId' => 2832495, 'dateVisited' => '2024-06-21'], |
|
* ['cityId' => 2832495, 'dateVisited' => '2024-06-22'], |
|
* ]; |
|
* ?> |
|
* ``` |
|
* |
|
* Note: |
|
* - Ensure the cache directories exist and are writable. |
|
* - This script is designed to be run infrequently to minimize API calls. |
|
* |
|
* @version 1.0 |
|
* @license CC BY 4.0 |
|
* |
|
* @author Christian Prior-Mamulyan <cprior@gmail.com> |
|
*/ |
|
|
|
// ini_set('display_errors', 1); |
|
// ini_set('display_startup_errors', 1); |
|
// error_reporting(E_ALL); |
|
|
|
|
|
// Load API key from .env file |
|
$envFilePath = '../weather/.env'; |
|
if (file_exists($envFilePath)) { |
|
$dotenv = parse_ini_file($envFilePath); |
|
if (isset($dotenv['OPENWEATHERMAP_APIKEY'])) { |
|
define('OPENWEATHERMAP_APIKEY', $dotenv['OPENWEATHERMAP_APIKEY']); |
|
} else { |
|
header('HTTP/1.1 500 Internal Server Error'); |
|
echo json_encode(['error' => 'OPENWEATHERMAP_APIKEY not found in .env file.']); |
|
exit; |
|
} |
|
} else { |
|
header('HTTP/1.1 500 Internal Server Error'); |
|
echo json_encode(['error' => '.env file not found.']); |
|
exit; |
|
} |
|
|
|
define('CACHE_LIFETIME', 7200); // Cache lifetime in seconds |
|
|
|
// List of city IDs and their corresponding visit dates |
|
$cityVisits = [ |
|
['cityId' => 2832495, 'dateVisited' => '2024-06-22'], // Siegen |
|
['cityId' => 3033123, 'dateVisited' => '2024-06-23'], // Besançon |
|
['cityId' => 3014728, 'dateVisited' => '2024-06-24'], // Grenoble |
|
['cityId' => 3028382, 'dateVisited' => '2024-06-25'], // Castellane |
|
['cityId' => 2995469, 'dateVisited' => '2024-06-26'], // Marseille |
|
['cityId' => 3032833, 'dateVisited' => '2024-06-27'], // Béziers |
|
['cityId' => 3128978, 'dateVisited' => '2024-06-28'], // Balaguer |
|
['cityId' => 3126580, 'dateVisited' => '2024-06-29'], // Canfranc |
|
['cityId' => 3031582, 'dateVisited' => '2024-06-30'], // Bordeaux |
|
['cityId' => 3037025, 'dateVisited' => '2024-07-01'], // Argenton |
|
['cityId' => 2971549, 'dateVisited' => '2024-07-02'], // Troyes |
|
['cityId' => 6554291, 'dateVisited' => '2024-07-03'], // Trier |
|
]; |
|
|
|
// Cache directory |
|
$cacheDir = __DIR__ . '/cache/'; |
|
if (!file_exists($cacheDir)) { |
|
mkdir($cacheDir, 0777, true); |
|
} |
|
|
|
$archiveCacheDir = __DIR__ . '/permanentcache/'; |
|
if (!file_exists($archiveCacheDir)) { |
|
mkdir($archiveCacheDir, 0777, true); |
|
} |
|
|
|
/** |
|
* Fetch data from the specified API URL using cURL |
|
* |
|
* @param string $url The API URL to fetch data from |
|
* @return string|null The response data or null if an error occurred |
|
*/ |
|
function fetchFromAPI($url) { |
|
$ch = curl_init(); |
|
curl_setopt($ch, CURLOPT_URL, $url); |
|
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); |
|
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); |
|
curl_setopt($ch, CURLOPT_TIMEOUT, 30); |
|
|
|
$response = curl_exec($ch); |
|
if (curl_errno($ch) || curl_getinfo($ch, CURLINFO_HTTP_CODE) >= 400) { |
|
curl_close($ch); |
|
return null; |
|
} |
|
|
|
curl_close($ch); |
|
return $response; |
|
} |
|
|
|
/** |
|
* Retrieve and cache the weather forecast data for a city on a specific date |
|
* |
|
* @param int $cityId The ID of the city to retrieve the forecast for |
|
* @param string $dateVisited The date for which to retrieve the forecast |
|
* @return array|null The weather forecast data and cache metadata or null if an error occurred |
|
*/ |
|
function getWeatherForecast($cityId, $dateVisited) { |
|
global $cacheDir, $archiveCacheDir; |
|
|
|
$cacheFile = "{$cacheDir}/forecast_{$cityId}_{$dateVisited}.json"; |
|
$archiveCacheFile = "{$archiveCacheDir}/latest_{$cityId}_{$dateVisited}.json"; |
|
$cacheMetadata = [ |
|
'cacheStatus' => 'miss', |
|
'cacheFileTime' => null |
|
]; |
|
|
|
// Check if the archive cache should be used |
|
$currentDate = new DateTime(); |
|
$visitDate = new DateTime($dateVisited); |
|
$visitDate->setTime(12, 0); // Set visit date time to 12:00 noon |
|
if (file_exists($archiveCacheFile) && $currentDate >= $visitDate) { |
|
$cacheMetadata['cacheStatus'] = 'hit'; |
|
$cacheMetadata['cacheFileTime'] = filemtime($archiveCacheFile); |
|
return [ |
|
'data' => json_decode(file_get_contents($archiveCacheFile), true), |
|
'cacheMetadata' => $cacheMetadata |
|
]; |
|
} |
|
|
|
// Check if regular cache is valid |
|
if (file_exists($cacheFile) && (time() - filemtime($cacheFile)) < CACHE_LIFETIME) { |
|
$cacheMetadata['cacheStatus'] = 'hit'; |
|
$cacheMetadata['cacheFileTime'] = filemtime($cacheFile); |
|
return [ |
|
'data' => json_decode(file_get_contents($cacheFile), true), |
|
'cacheMetadata' => $cacheMetadata |
|
]; |
|
} |
|
|
|
// Fetch data from API |
|
$url = "http://api.openweathermap.org/data/2.5/forecast?id={$cityId}&appid=" . OPENWEATHERMAP_APIKEY; |
|
$forecastData = fetchFromAPI($url); |
|
if ($forecastData === null) { |
|
return null; |
|
} |
|
|
|
// Save data to cache |
|
file_put_contents($cacheFile, $forecastData); |
|
file_put_contents($archiveCacheFile, $forecastData); |
|
|
|
return [ |
|
'data' => json_decode($forecastData, true), |
|
'cacheMetadata' => $cacheMetadata |
|
]; |
|
} |
|
|
|
|
|
/** |
|
* Retrieve and cache the current weather data for a city on a specific date |
|
* |
|
* @param int $cityId The ID of the city to retrieve the current weather for |
|
* @param string $dateVisited The date for which to retrieve the current weather |
|
* @return array|null The current weather data and cache metadata or null if an error occurred |
|
*/ |
|
function getCurrentWeather($cityId, $dateVisited) { |
|
global $cacheDir, $archiveCacheDir; |
|
|
|
$cacheFile = "{$cacheDir}/current_{$cityId}_{$dateVisited}.json"; |
|
$archiveCacheFile = "{$archiveCacheDir}/latest_{$cityId}_{$dateVisited}.json"; |
|
$cacheMetadata = [ |
|
'cacheStatus' => 'miss', |
|
'cacheFileTime' => null |
|
]; |
|
|
|
// Check if the archive cache should be used |
|
$currentDate = new DateTime(); |
|
$visitDate = new DateTime($dateVisited); |
|
$visitDate->setTime(12, 0); // Set visit date time to 12:00 noon |
|
if (file_exists($archiveCacheFile) && $currentDate >= $visitDate) { |
|
$cacheMetadata['cacheStatus'] = 'hit'; |
|
$cacheMetadata['cacheFileTime'] = filemtime($archiveCacheFile); |
|
return [ |
|
'data' => json_decode(file_get_contents($archiveCacheFile), true), |
|
'cacheMetadata' => $cacheMetadata |
|
]; |
|
} |
|
|
|
// Check if regular cache is valid |
|
if (file_exists($cacheFile) && (time() - filemtime($cacheFile)) < CACHE_LIFETIME) { |
|
$cacheMetadata['cacheStatus'] = 'hit'; |
|
$cacheMetadata['cacheFileTime'] = filemtime($cacheFile); |
|
return [ |
|
'data' => json_decode(file_get_contents($cacheFile), true), |
|
'cacheMetadata' => $cacheMetadata |
|
]; |
|
} |
|
|
|
// Fetch data from API |
|
$url = "http://api.openweathermap.org/data/2.5/weather?id={$cityId}&appid=" . OPENWEATHERMAP_APIKEY; |
|
$weatherData = fetchFromAPI($url); |
|
if ($weatherData === null) { |
|
return null; |
|
} |
|
|
|
// Save data to cache |
|
file_put_contents($cacheFile, $weatherData); |
|
file_put_contents($archiveCacheFile, $weatherData); |
|
|
|
return [ |
|
'data' => json_decode($weatherData, true), |
|
'cacheMetadata' => $cacheMetadata |
|
]; |
|
} |
|
|
|
|
|
/** |
|
* Find the forecast for noon or the closest time on the visit date |
|
* |
|
* @param array $weatherData The weather data to search through |
|
* @param string $dateVisited The date to find the noon forecast for |
|
* @return array|null The noon forecast data or null if not found |
|
*/ |
|
function getNoonForecast($weatherData, $dateVisited) { |
|
foreach ($weatherData['list'] as $forecast) { |
|
$forecastDateTime = strtotime($forecast['dt_txt']); |
|
$forecastDate = date('Y-m-d', $forecastDateTime); |
|
$forecastHour = date('H', $forecastDateTime); |
|
|
|
if ($forecastDate == $dateVisited && $forecastHour == '12') { |
|
return $forecast; |
|
} |
|
} |
|
|
|
// If no exact noon forecast found, get the closest time |
|
foreach ($weatherData['list'] as $forecast) { |
|
$forecastDateTime = strtotime($forecast['dt_txt']); |
|
$forecastDate = date('Y-m-d', $forecastDateTime); |
|
|
|
if ($forecastDate == $dateVisited) { |
|
return $forecast; |
|
} |
|
} |
|
|
|
return null; |
|
} |
|
|
|
/** |
|
* Process the forecast data to extract relevant information |
|
* |
|
* @param array|null $noonForecast The noon forecast data to process |
|
* @return array The processed forecast information |
|
*/ |
|
function processForecast($noonForecast) { |
|
if ($noonForecast) { |
|
$temperature = kelvinToCelsius($noonForecast['main']['temp']); |
|
$feelsLike = kelvinToCelsius($noonForecast['main']['feels_like']); |
|
$tempMin = kelvinToCelsius($noonForecast['main']['temp_min']); |
|
$tempMax = kelvinToCelsius($noonForecast['main']['temp_max']); |
|
$icon = isset($noonForecast['weather'][0]['icon']) ? $noonForecast['weather'][0]['icon'] : ''; |
|
$forecastDescription = $noonForecast['weather'][0]['description']; |
|
} else { |
|
$temperature = $feelsLike = $tempMin = $tempMax = null; |
|
$icon = $forecastDescription = ''; |
|
} |
|
return [ |
|
'temperature' => $temperature, |
|
'feelsLike' => $feelsLike, |
|
'tempMin' => $tempMin, |
|
'tempMax' => $tempMax, |
|
'icon' => $icon, |
|
'forecastDescription' => $forecastDescription |
|
]; |
|
} |
|
|
|
/** |
|
* Retrieve city information from the weather data |
|
* |
|
* @param array|null $weatherData The weather forecast data |
|
* @param array|null $currentWeatherData The current weather data |
|
* @return array The city information |
|
*/ |
|
function getCityInfo($weatherData, $currentWeatherData) { |
|
if (isset($weatherData['data']['city'])) { |
|
return $weatherData['data']['city']; |
|
} |
|
if ($currentWeatherData) { |
|
return [ |
|
'name' => $currentWeatherData['data']['name'], |
|
'country' => $currentWeatherData['data']['sys']['country'], |
|
'coord' => [ |
|
'lat' => $currentWeatherData['data']['coord']['lat'], |
|
'lon' => $currentWeatherData['data']['coord']['lon'] |
|
], |
|
'sunrise' => $currentWeatherData['data']['sys']['sunrise'], |
|
'sunset' => $currentWeatherData['data']['sys']['sunset'] |
|
]; |
|
} |
|
return [ |
|
'name' => '', |
|
'country' => '', |
|
'coord' => ['lat' => null, 'lon' => null], |
|
'sunrise' => null, |
|
'sunset' => null |
|
]; |
|
} |
|
|
|
/** |
|
* Iterate over each city visit, fetch the weather data, process the noon forecast, |
|
* retrieve city information, and prepare the data for JSON output. |
|
* |
|
* For each visit: |
|
* 1. Retrieve the weather forecast data. |
|
* 2. Retrieve the current weather data as a fallback. |
|
* 3. Find and process the forecast for noon on the visit date. |
|
* 4. Retrieve city information from the forecast or current weather data. |
|
* 5. Format the data, including cache metadata, for JSON output. |
|
*/ |
|
$forecasts = []; |
|
$currentDate = date('Y-m-d'); |
|
|
|
foreach ($cityVisits as $visit) { |
|
$cityId = $visit['cityId']; |
|
$dateVisited = $visit['dateVisited']; |
|
$formattedDate = getRelativeDate($dateVisited, $currentDate); |
|
|
|
// Get the weather forecast for the city |
|
$weatherData = getWeatherForecast($cityId, $dateVisited); |
|
$currentWeatherData = getCurrentWeather($cityId, $dateVisited); |
|
|
|
// Get the noon forecast |
|
$noonForecast = getNoonForecast($weatherData['data'], $dateVisited); |
|
$forecast = processForecast($noonForecast); |
|
|
|
// Get the city information |
|
$cityInfo = getCityInfo($weatherData, $currentWeatherData); |
|
|
|
$latitude = $cityInfo['coord']['lat']; |
|
$longitude = $cityInfo['coord']['lon']; |
|
$sunrise = date('H:i', $cityInfo['sunrise']); |
|
$sunset = date('H:i', $cityInfo['sunset']); |
|
|
|
$forecasts[] = [ |
|
'cityId' => $cityId, |
|
'dateVisited' => $dateVisited, |
|
'formattedDate' => $formattedDate, |
|
'cityName' => $cityInfo['name'], |
|
'country' => $cityInfo['country'], |
|
'latitude' => $latitude, |
|
'longitude' => $longitude, |
|
'sunrise' => $sunrise, |
|
'sunset' => $sunset, |
|
'weather' => [ |
|
'temperature' => $forecast['temperature'] !== null ? round($forecast['temperature'], 2) : 'N/A', |
|
'feelsLike' => $forecast['feelsLike'] !== null ? round($forecast['feelsLike'], 2) : 'N/A', |
|
'tempMin' => $forecast['tempMin'] !== null ? round($forecast['tempMin'], 2) : 'N/A', |
|
'tempMax' => $forecast['tempMax'] !== null ? round($forecast['tempMax'], 2) : 'N/A', |
|
'description' => $forecast['forecastDescription'] |
|
], |
|
'icon' => $forecast['icon'] ? "http://openweathermap.org/img/wn/{$forecast['icon']}.png" : '', |
|
'forecastCache' => $weatherData['cacheMetadata'] |
|
]; |
|
} |
|
|
|
// Control output to ensure only JSON is returned |
|
ob_start(); |
|
header('Content-Type: application/json'); |
|
echo json_encode($forecasts); |
|
|
|
// Flush output buffer |
|
ob_end_flush(); |
|
|
|
// Helper functions |
|
/** |
|
* Convert temperature from Kelvin to Celsius |
|
* |
|
* @param float $kelvin The temperature in Kelvin |
|
* @return float The temperature in Celsius |
|
*/ |
|
function kelvinToCelsius($kelvin) { |
|
return $kelvin - 273.15; |
|
} |
|
|
|
/** |
|
* Format the visit date in German |
|
* |
|
* @param string $dateVisited The visit date to format |
|
* @param string $currentDate The current date |
|
* @return string The formatted visit date in German |
|
*/ |
|
function getRelativeDate($dateVisited, $currentDate) { |
|
$dateVisitedTimestamp = strtotime($dateVisited); |
|
$currentDateTimestamp = strtotime($currentDate); |
|
$diffDays = ceil(($dateVisitedTimestamp - $currentDateTimestamp) / (60 * 60 * 24)); |
|
|
|
if ($diffDays > 0) { |
|
return $diffDays == 1 ? "in 1 Tag" : "in $diffDays Tagen"; |
|
} else if ($diffDays < 0) { |
|
return abs($diffDays) == 1 ? "vor 1 Tag" : "vor " . abs($diffDays) . " Tagen"; |
|
} else { |
|
return "heute"; |
|
} |
|
} |
|
?> |