189 lines
6.0 KiB
PHP
189 lines
6.0 KiB
PHP
<?php
|
|
|
|
namespace App\Console\Commands;
|
|
|
|
use App\Models\WeatherPeriod;
|
|
use App\Models\WeatherReport;
|
|
use Carbon\Carbon;
|
|
use Illuminate\Console\Command;
|
|
use Illuminate\Support\Facades\File;
|
|
|
|
class IngestWeatherData extends Command
|
|
{
|
|
/**
|
|
* @var string
|
|
*/
|
|
protected $signature = 'weather:ingest
|
|
{--date= : Import specific date only (YYYY-MM-DD)}
|
|
{--type= : Import specific type only (hourly or weekly)}
|
|
{--force : Re-import existing records}';
|
|
|
|
/**
|
|
* @var string
|
|
*/
|
|
protected $description = 'Ingest weather JSON files from storage/app/private/weather/';
|
|
|
|
public function handle(): int
|
|
{
|
|
$basePath = storage_path('app/private/weather');
|
|
|
|
if (! File::isDirectory($basePath)) {
|
|
$this->error("Weather data directory not found: {$basePath}");
|
|
|
|
return self::FAILURE;
|
|
}
|
|
|
|
$dateFilter = $this->option('date');
|
|
$typeFilter = $this->option('type');
|
|
$force = $this->option('force');
|
|
|
|
$dateFolders = File::directories($basePath);
|
|
|
|
if ($dateFilter) {
|
|
$dateFolders = array_filter($dateFolders, fn ($dir) => basename($dir) === $dateFilter);
|
|
}
|
|
|
|
if (empty($dateFolders)) {
|
|
$this->warn('No date folders found to process.');
|
|
|
|
return self::SUCCESS;
|
|
}
|
|
|
|
$this->info('Starting weather data ingestion...');
|
|
$progressBar = $this->output->createProgressBar(count($dateFolders));
|
|
|
|
$totalImported = 0;
|
|
$totalSkipped = 0;
|
|
|
|
foreach ($dateFolders as $dateFolder) {
|
|
$date = basename($dateFolder);
|
|
$files = File::files($dateFolder);
|
|
|
|
foreach ($files as $file) {
|
|
$filename = $file->getFilename();
|
|
|
|
if (! str_ends_with($filename, '.json')) {
|
|
continue;
|
|
}
|
|
|
|
$type = $this->getReportType($filename);
|
|
|
|
if ($type === null || ($typeFilter && $type !== $typeFilter)) {
|
|
continue;
|
|
}
|
|
|
|
$reportedAt = $this->parseReportedAt($date, $filename);
|
|
|
|
$existingReport = WeatherReport::query()
|
|
->where('type', $type)
|
|
->where('reported_at', $reportedAt)
|
|
->first();
|
|
|
|
if ($existingReport && ! $force) {
|
|
$totalSkipped++;
|
|
|
|
continue;
|
|
}
|
|
|
|
$jsonContent = File::get($file->getPathname());
|
|
$data = json_decode($jsonContent, true);
|
|
|
|
if (! $data) {
|
|
$this->warn("Failed to parse JSON: {$file->getPathname()}");
|
|
|
|
continue;
|
|
}
|
|
|
|
if ($existingReport && $force) {
|
|
$existingReport->delete();
|
|
}
|
|
|
|
$this->importReport($type, $reportedAt, $data);
|
|
$totalImported++;
|
|
}
|
|
|
|
$progressBar->advance();
|
|
}
|
|
|
|
$progressBar->finish();
|
|
$this->newLine(2);
|
|
$this->info("Imported: {$totalImported} reports, Skipped: {$totalSkipped} existing");
|
|
|
|
return self::SUCCESS;
|
|
}
|
|
|
|
private function getReportType(string $filename): ?string
|
|
{
|
|
if (str_starts_with($filename, 'hourly_')) {
|
|
return 'hourly';
|
|
}
|
|
|
|
if (str_starts_with($filename, 'weekly_')) {
|
|
return 'weekly';
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
private function parseReportedAt(string $date, string $filename): Carbon
|
|
{
|
|
preg_match('/(\d{2})-(\d{2})\.json$/', $filename, $matches);
|
|
$hour = $matches[1] ?? '00';
|
|
$minute = $matches[2] ?? '00';
|
|
|
|
return Carbon::parse("{$date} {$hour}:{$minute}:00");
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $data
|
|
*/
|
|
private function importReport(string $type, Carbon $reportedAt, array $data): void
|
|
{
|
|
$properties = $data['properties'] ?? [];
|
|
$geometry = $data['geometry'] ?? [];
|
|
|
|
$coordinates = $geometry['coordinates'][0] ?? [];
|
|
$centerLat = 0;
|
|
$centerLng = 0;
|
|
|
|
if (! empty($coordinates)) {
|
|
$lats = array_column($coordinates, 1);
|
|
$lngs = array_column($coordinates, 0);
|
|
$centerLat = array_sum($lats) / count($lats);
|
|
$centerLng = array_sum($lngs) / count($lngs);
|
|
}
|
|
|
|
$report = WeatherReport::create([
|
|
'type' => $type,
|
|
'reported_at' => $reportedAt,
|
|
'generated_at' => Carbon::parse($properties['generatedAt'] ?? $properties['updateTime'] ?? $reportedAt),
|
|
'latitude' => $centerLat,
|
|
'longitude' => $centerLng,
|
|
'elevation_meters' => $properties['elevation']['value'] ?? null,
|
|
]);
|
|
|
|
$periods = $properties['periods'] ?? [];
|
|
|
|
foreach ($periods as $period) {
|
|
WeatherPeriod::create([
|
|
'weather_report_id' => $report->id,
|
|
'period_number' => $period['number'],
|
|
'name' => $period['name'] ?: null,
|
|
'start_time' => Carbon::parse($period['startTime']),
|
|
'end_time' => Carbon::parse($period['endTime']),
|
|
'is_daytime' => $period['isDaytime'],
|
|
'temperature' => $period['temperature'],
|
|
'temperature_unit' => $period['temperatureUnit'],
|
|
'precipitation_probability' => $period['probabilityOfPrecipitation']['value'] ?? null,
|
|
'dewpoint_celsius' => isset($period['dewpoint']['value']) ? round($period['dewpoint']['value'], 2) : null,
|
|
'relative_humidity' => $period['relativeHumidity']['value'] ?? null,
|
|
'wind_speed' => $period['windSpeed'] ?? null,
|
|
'wind_direction' => $period['windDirection'] ?? null,
|
|
'icon_url' => $period['icon'] ?? null,
|
|
'short_forecast' => $period['shortForecast'] ?? null,
|
|
'detailed_forecast' => $period['detailedForecast'] ?: null,
|
|
]);
|
|
}
|
|
}
|
|
}
|