'date' ]; protected $fillable = [ 'name', 'mail_template', 'mail_subject', 'mail_frequency', 'mail_day', 'mail_scheduled', 'download_path', 'pivot_json_path', 'span_json_path', 'harvest_json_path', 'min_harvest_date', 'borders', ]; public static function saveWithFormData(mixed $formData) { $uniqueIdentifier = ['id' => $formData['id'] ?? null]; /** * @var Project $project */ logger($formData); $project = Project::updateOrCreate($uniqueIdentifier, $formData); $baseFrom = 'livewire-tmp/'; $baseTo = $project->download_path.'/Data/'; if ($formData['pivot_file']) { Storage::copy($baseFrom.$formData['pivot_file']['tmpFilename'], $baseTo.'pivot.geojson'); $project->update(['pivot_json_path' => $baseTo.'pivot.geojson']); } if ($formData['span_file']) { Storage::copy($baseFrom.$formData['span_file']['tmpFilename'], $baseTo.'span.geojson'); $project->update(['span_json_path' => $baseTo.'span.geojson']); } if ($formData['harvest_file']) { Storage::copy($baseFrom.$formData['harvest_file']['tmpFilename'], $baseTo.'harvest.'.$formData['harvest_file']['extension']); if ($project->update(['harvest_json_path' => $baseTo.'harvest.'.$formData['harvest_file']['extension']])) { $project->setMinHarvestDate(); } } self::upsertMailRecipients($formData, $project); } private static function upsertMailRecipients($formData, Project $project) { $mailRecipientsData = array_map(function ($mailRecipient) use ($project) { $mailRecipient['project_id'] = $project->id; unset($mailRecipient['created_at']); unset($mailRecipient['updated_at']); $mailRecipient['id'] ??= null; return $mailRecipient; }, $formData['mail_recipients'] ?? []); ProjectEmailRecipient::upsert( $mailRecipientsData, ['id', 'project_id'], ['name', 'email',] ); } public function getMosaicPath() { return sprintf('%s/%s', $this->download_path, 'weekly_mosaic'); } public function getMosaicList(): Collection { return collect(Storage::files($this->getMosaicPath())) ->filter(fn($filename) => Str::endsWith($filename, '.tif')) ->values(); } protected static function boot() { parent::boot(); // TODO: Change the autogenerated stub static::deleting(function ($project) { $project->emailRecipients()->delete(); $project->mailings()->each(function ($mailing) { $mailing->attachments()->delete(); $mailing->recipients()->delete(); }); $project->mailings()->delete(); }); } public function reports() { return $this->hasMany(ProjectReport::class); } public function getAttachmentPathAttribute() { return storage_path(sprintf('%s/attachments', $this->download_path)); return '/storage/'.$this->download_path.'/attachments'; } public function emailRecipients() { return $this->hasMany(ProjectEmailRecipient::class); } public function mailings() { return $this->hasMany(ProjectMailing::class); } public function downloads(): \Illuminate\Database\Eloquent\Relations\HasMany { return $this->hasMany(ProjectDownload::class); } public function nonFailedDownloads(): \Illuminate\Database\Eloquent\Relations\HasMany { return $this->hasMany(ProjectDownload::class)->where('status', '<>', 'failed'); } public function mosaics(): \Illuminate\Database\Eloquent\Relations\HasMany { return $this->hasMany(ProjectMosaic::class); } public function allMosaicsPresent(Carbon $endDate): bool { // end date is in the future if ($endDate->isFuture()) { throw new \Exception('Mosaic can\'t be generated for the future. Change the end date.'); } $mosaicsNotPresentInFilesystem = $this->getMissingMosaicsInFileSystem($endDate); if ($mosaicsNotPresentInFilesystem->count() === 0) { return true; } $message = sprintf( 'Missing mosaics: %s', $mosaicsNotPresentInFilesystem->implode(', ') ); throw new \Exception($message); } public function getMissingMosaicsInFileSystem(Carbon $endDate) { return collect($this->getMosaicFilenameListByEndDate($endDate)) ->filter(function ($filename) { return !$this->getMosaicList()->contains(function ($mosaicFilename) use ($filename) { return Str::endsWith($mosaicFilename, substr($filename, -16)); }); }); } public static function getMosaicFilenameListByEndDate(Carbon $endDate, int $offset = 7): \Generator { for ($i = 0; $i < 4; $i++) { yield (ProjectMosaic::getFilenameByPeriod($endDate->copy()->subDays($offset * $i), $offset)); } } public function getMosiacList() { return collect(Storage::files($this->getMosaicPath())) ->filter(fn($file) => Str::endsWith($file, '.tif')) ->sortByDesc(function ($file) { $parts = explode('_', str_replace('.tif', '', $file)); $week = $parts[1]; $year = $parts[2]; return $year.sprintf('%02d', $week); }) ->values(); } public static function getAllDatesOfWeeksInYear($year, $weekNumber): Collection { $startOfWeek = Carbon::now()->setISODate($year, $weekNumber)->startOfWeek(); $dates = collect([]); for ($day = 0; $day < 7; $day++) { $dates->push((clone $startOfWeek)->addDays($day)->toDateString()); } return $dates; } public function allMergedTiffsPresent(Collection $haystack, Collection $needles) { $needlesNotInHaystack = $needles->filter(function ($needle) use ($haystack) { return !$haystack->contains(function ($item) use ($needle) { return str_contains($item, $needle); }); }); if ($needlesNotInHaystack->count() > 0) { $message = sprintf( 'Missing merged tiffs: %s', $needlesNotInHaystack->implode(', ') ); throw new \Exception($message); } return true; } public function getMergedTiffList() { return collect(Storage::files($this->download_path.'/merged_final_tif')) ->filter(fn($file) => Str::endsWith($file, '.tif')) ->sortByDesc(function ($file) { $parts = explode('_', str_replace('.tif', '', $file)); $date = $parts[1]; return $date; }) ->values(); } public function hasPendingDownload(): bool { return $this->downloads()->statusPending()->count() > 0; } public function hasPendingReport(): bool { return $this->reports()->statusPending()->count() > 0; } public function hasPendingMosaic(): bool { return $this->mosaics()->statusPending()->count() > 0; } public function startDownload(Carbon $date) { $downloadRequest = $this->downloads()->updateOrCreate( [ 'project_id' => $this->id, // of een andere manier om project_id te bepalen 'name' => sprintf('%s.tif', $date->format('Y-m-d')), ], [ 'path' => sprintf('%s/%s/%s.tif', $this->download_path, 'merged_final_tif', $date->format('Y-m-d')), ] ); ProjectDownloadTiffJob::dispatch($downloadRequest, $date); } public function schedule() { //TODO check the ranges. $this->scheduleReport(); } public function shouldSchedule(): bool { if (strtolower($this->mail_day) === strtolower(now()->englishDayOfWeek)) { return strtolower($this->mail_frequency) === 'weekly' || now()->day <= 7; } return false; } public function hasInvalidMosaicFor(Carbon $endDate, int $offset): bool { // parameters : $ // check if the mail day happens the day before mosaic -> good $dayOfWeekIso = Carbon::parse($this->mail_day)->dayOfWeekIso; $min_updated_at_date = $endDate->copy() ->startOfWeek() ->addDays($dayOfWeekIso - 1) ->format('Y-m-d'); return $this->mosaics() ->where('updated_at', '>=', $min_updated_at_date) ->statusSuccess() ->where(['end_date' => $endDate, 'offset' => $offset]) ->exists(); } public function scheduleReport(?Carbon $endDate = null, ?int $offset = null) { if ($endDate?->isFuture() || $endDate?->isToday() || $offset <= 0) { logger('EndDate is today or in the future.'); $endDate = null; $offset = null; } $endDate ??= Carbon::yesterday(); $offset ??= 7; for ($step = 0; $step<=3; $step++) { $latestMosaicToDelete = ProjectMosaic::projectMosaicNameFormat($endDate->clone(), $offset*$step); logger('Deleting mosaic: '.$latestMosaicToDelete); $this->mosaics()->where('name', $latestMosaicToDelete)->first()?->delete(); } logger('Scheduling report for '.$endDate->format('d-m-Y').' with offset '.$offset.' days'); Bus::chain([ Bus::batch($this->getFileDownloadsFor($endDate->clone(), $offset)), Bus::batch($this->getMosaicsFor($endDate->clone(), $offset)), Bus::batch( [ new ProjectInterpolateGrowthModelJob($this), $this->getReportFor($endDate->clone(), $offset, true) ]), ]) ->dispatch(); return "done"; } public function scheduleTestReport() { $endDate = Carbon::yesterday(); $offset = 7; $this->mail_day = $endDate->dayName; for ($step = 0; $step<=3; $step++) { $latestMosaicToDelete = ProjectMosaic::projectMosaicNameFormat($endDate->clone(), $offset*$step); logger('Deleting mosaic: '.$latestMosaicToDelete); $this->mosaics()->where('name', $latestMosaicToDelete)->first()?->delete(); } logger('Scheduling test report for '.$endDate->format('d-m-Y').' with offset '.$offset.' days'); Bus::chain([ Bus::batch($this->getFileDownloadsFor($endDate->clone(), $offset)), Bus::batch($this->getTestMosaicsFor($endDate->clone())), Bus::batch( [ new ProjectInterpolateGrowthModelJob($this), $this->getTestReportFor($endDate->clone(), $offset, true) ]), ]) ->dispatch(); return "done"; } public function getReportFor(Carbon $endDate, int $offset = 7, $sendMail = false): ProjectReportGeneratorJob { $report = $this->reports()->create([ 'name' => 'Report of the '.$endDate->format('d-m-Y').' from the past '.$offset.' days', 'end_date' => $endDate, 'offset' => $offset, 'path' => 'reports/'.ProjectReport::getFileName($endDate, $offset).'.docx', ]); return (new ProjectReportGeneratorJob($report, $sendMail)); } public function getTestReportFor(Carbon $endDate, int $offset = 7, $sendMail = false): ProjectReportGeneratorJob { $report = $this->reports()->create([ 'name' => 'Test Report of the '.$endDate->format('d-m-Y').' from the past '.$offset.' days', 'end_date' => $endDate, 'offset' => $offset, 'path' => 'reports/'.ProjectReport::getFileName($endDate, $offset).'.docx', ]); return (new ProjectReportGeneratorJob($report, $sendMail, true)); } public function getFileDownloadsFor(Carbon $endDate, int $offset = 7): array { $startOfRange = (clone $endDate)->subdays(4 * $offset - 1); $dateRange = CarbonPeriod::create($startOfRange, $endDate); return collect($dateRange) ->map(fn($date) => ProjectDownloadTiffJob::handleForDate($this, $date)) ->filter() ->toArray(); } public function getMosaicsFor(Carbon $endDate, int $offset = 7): array { return collect(range(0, 3)) ->map(function () use ($endDate, $offset) { $currentEndDate = $endDate->clone(); if (!$currentEndDate->isDayOfWeek($this->mail_day)) { $endDate->previous($this->mail_day); } $endDate->subDay(); $calculatedOffSet = (int) $endDate->clone()->diffInDays($currentEndDate); return ProjectMosiacGeneratorJob::handleFor($this, $currentEndDate, $calculatedOffSet); }) ->filter() ->toArray(); } public function getTestMosaicsFor(Carbon $endDate) { return collect(range(0, 3)) ->map(function () use ($endDate) { $currentEndDate = $endDate->clone(); $endDate->subWeek(); return ProjectMosiacGeneratorJob::handleFor($this, $currentEndDate, 7); }) ->filter() ->toArray(); } public function handleMissingDownloads() { $this->getMissingDownloads() ->each(function (Carbon $date) { dispatch(ProjectDownloadTiffJob::handleForDate($this, $date)); }); } public function hasMissingDownloadsDateInHarvestFile(): bool { return $this->getMissingDownloads()->count() > 0; } public function getMissingDownloads(): Collection { if (!$this->min_harvest_date) { return collect([]); } $harvest_dates = $this->nonFailedDownloads()->get()->map(fn($d) => $d->date); $all_dates = collect(CarbonPeriod::create($this->min_harvest_date, '1 day', now())->toArray()); return $all_dates->diff($harvest_dates); } public function setMinHarvestDate(): bool { if (!$this->harvest_json_path) { return false; } return $this->update(['min_harvest_date' => $this->getMinimumDateFromHarvestExcelFile()->format('Y-m-d')]); } private function getMinimumDateFromHarvestExcelFile(): Carbon { $value = Storage::disk('local')->path($this->harvest_json_path); $data = Excel::toCollection(new \App\Imports\ExcelFileImport(), $value); $season_start_index = $data->first()->first()->search('season_start'); return collect($data->first()->slice(1))->reduce(function ($carry, $value, $key) use ($season_start_index) { return min($carry, Carbon::instance(SharedDate::excelToDateTimeObject($value[$season_start_index]))); }, now()); } public function newDownloadsUploaded() { // $this->createDownloadRecordsInDatabaseAndUpdateStatusForAllDownloadedImages(); $date = Carbon::parse('2023-02-09'); $now = Carbon::now(); $offset = (int) $date->diffInDays($now); dispatch_sync(new ProjectDownloadRDSJob($this, Carbon::yesterday(), $offset)); dispatch_sync(new ProjectInterpolateGrowthModelJob($this)); } public function getRdsAsDownload() { $path = $this->download_path.'/Data/extracted_ci/cumulative_vals/combined_CI_data.rds'; return Storage::download( $path, 'combined_CI_data.rds' ); } public function getTifsAsZip(Carbon $startDate, Carbon $endDate) { $path = $this->download_path.'/merged_final_tif'; $files = collect(Storage::files($path)) ->filter(fn($file) => Str::endsWith($file, '.tif')) ->filter(function ($file) use ($startDate, $endDate) { $dateString = str_replace('.tif', '', basename($file)); $date = Carbon::parse($dateString); return $date->between($startDate, $endDate); }); logger(__CLASS__.'::'.__METHOD__.'::'.__LINE__); logger($files); return $this->createZipArchiveAndReturn($files); } public function getMosaicsAsZip(Carbon $startDate, Carbon $endDate) { $path = $this->download_path.'/weekly_mosaic'; // create a collection of all week numbers and years for given date range $allowedFiles = collect(CarbonPeriod::create($startDate, $endDate)->toArray()) ->map(fn($date) => sprintf('week_%s_%s.tif', $date->weekOfYear, $date->year)) ->unique(); $files = collect(Storage::files($path)) ->filter(fn($file) => Str::endsWith($file, '.tif')) ->filter(function ($file) use ($allowedFiles) { return $allowedFiles->contains(basename($file)); }); putenv('TMPDIR='.storage_path('app')); return $this->createZipArchiveAndReturn($files); } private function createZipArchiveAndReturn(Collection $files) { $zipPath = storage_path('app/'.$this->download_path.'/download.zip'); $zip = new \ZipArchive(); $zip->open($zipPath, \ZipArchive::CREATE | \ZipArchive::OVERWRITE); $files->each(function ($file) use ($zip) { $zip->addFile(storage_path('app/'.$file), basename($file)); }); $zip->close(); return response()->download( $zipPath, 'download.zip' ); } public function createDownloadRecordsInDatabaseAndUpdateStatusForAllDownloadedImages(): void { $merged_tiffs = $this->getMergedTiffList(); $this->downloads->each(function ($download) use ($merged_tiffs) { if ($merged_tiffs->contains($download->path) && $download->status !== 'success') { $download->setStatusSuccess(); } }); $merged_tiffs->each(function ($path) { if ($this->downloads()->where('path', $path)->count() === 0) { $this->downloads()->create([ 'path' => $path, 'status' => 'success', 'name' => explode('/', $path)[count(explode('/', $path)) - 1] ]); } }); } public function createAllMosaicsInDatabaseAndUpdateStatusForAllDownloadedImages() { $list_of_desired_mosaics = $this->getMergedTiffList() ->map(fn($file) => str_replace('.tif', '', basename($file))) ->map(function ($date) { $carbon = Carbon::parse($date); return sprintf('week_%s_%s', $carbon->format('W'), $carbon->year); }) ->unique(); } }