logId); if (! $log) { return; } $round = Round::with(['bands', 'categories', 'powerCategories'])->find($log->round_id); $remarksEval = $this->decodeRemarksEval($log->remarks_eval); // TDate validace hlídá, že log odpovídá termínu kola. $tDateInvalid = ! $this->isTDateWithinRound($log->tdate, $round); if ($tDateInvalid) { $this->addRemark($remarksEval, 'Datum v TDate neodpovídá termínu závodu.'); $log->remarks_eval = $this->encodeRemarksEval($remarksEval); $log->save(); } $categoryId = $this->resolveCategoryId($log, $round, $remarksEval); $bandId = $this->resolveBandId($log, $round, $remarksEval); [$powerCategoryId, $powerMismatch] = $this->resolvePowerCategoryId($log, $round, $remarksEval); // 6H kategorie je povolená jen pro vybraná pásma. if ($log->sixhr_category && ! $this->isSixHourBand($bandId)) { $remarksEval[] = '6H kategorie je povolena jen pro pásma 145 MHz a 435 MHz.'; } $missingClaimedQso = $log->claimed_qso_count === null; $missingClaimedScore = $log->claimed_score === null; if ($missingClaimedQso) { $remarksEval[] = 'Nebyl načten CQSOs.'; } if ($missingClaimedScore) { $remarksEval[] = 'Nebyl načten CToSc.'; } $log->remarks_eval = $this->encodeRemarksEval($remarksEval); $log->save(); $claimedRun = ClaimedRunResolver::forRound($log->round_id); $claimedQsoCount = $log->claimed_qso_count ?? 0; $claimedScore = $log->claimed_score ?? 0; $scorePerQso = $claimedQsoCount > 0 ? round($claimedScore / $claimedQsoCount, 2) : null; $status = 'OK'; $statusReason = null; if ($this->isCheckLog($log)) { $status = 'CHECK'; } // IGNORED = log nelze bezpečně zařadit do claimed scoreboardu. if ($tDateInvalid || $categoryId === null || $bandId === null || $missingClaimedQso || $missingClaimedScore || $powerMismatch) { $status = 'IGNORED'; $reasons = []; if ($tDateInvalid) { $reasons[] = 'TDate mimo termín závodu.'; } if ($categoryId === null) { $reasons[] = 'Kategorie nebyla rozpoznána.'; } if ($bandId === null) { $reasons[] = 'Pásmo nebylo rozpoznáno.'; } if ($missingClaimedQso) { $reasons[] = 'Chybí CQSOs.'; } if ($missingClaimedScore) { $reasons[] = 'Chybí CToSc.'; } if ($powerMismatch) { $reasons[] = 'Výkon neodpovídá zvolené kategorii.'; } $statusReason = implode(' ', $reasons); } LogResult::updateOrCreate( ['log_id' => $log->id, 'evaluation_run_id' => $claimedRun->id], [ 'evaluation_run_id' => $claimedRun->id, 'category_id' => $categoryId, 'band_id' => $bandId, 'power_category_id' => $powerCategoryId, 'claimed_qso_count' => $log->claimed_qso_count, 'claimed_score' => $log->claimed_score, 'total_qso_count' => $claimedQsoCount, 'discarded_qso_count' => 0, 'discarded_points' => 0, 'discarded_qso_percent' => 0, 'unique_qso_count' => 0, 'score_per_qso' => $scorePerQso, 'official_score' => 0, 'penalty_score' => 0, 'status' => $status, 'status_reason' => $statusReason, ] ); } protected function resolveCategoryId(Log $log, ?Round $round, array &$remarksEval): ?int { $resolveCategory = function (?string $value) use ($round): ?int { if (! $value) { return null; } $ediCat = EdiCategory::whereRaw('LOWER(value) = ?', [mb_strtolower(trim($value))])->first(); if (! $ediCat) { $ediCat = $this->matchEdiCategoryByRegex($value); if (! $ediCat) { return null; } } $mappedCategoryId = $ediCat->categories()->value('categories.id'); if (! $mappedCategoryId) { return null; } if ($round && $round->categories()->where('categories.id', $mappedCategoryId)->exists()) { return $mappedCategoryId; } if (! $round || $round->categories()->count() === 0) { return $mappedCategoryId; } return null; }; // V PSect může být více tokenů – zkoušíme je postupně. $categoryId = null; if ($log->psect) { $categoryId = $resolveCategory($log->psect); if ($categoryId === null) { $parts = preg_split('/\\s+/', trim((string) $log->psect)) ?: []; if (count($parts) > 1) { foreach ($parts as $part) { $categoryId = $resolveCategory($part); if ($categoryId !== null) { break; } } } } } if ($categoryId === null) { $remarksEval[] = 'Kategorie nebyla rozpoznána.'; } return $categoryId; } protected function matchEdiCategoryByRegex(string $value): ?EdiCategory { $candidates = EdiCategory::whereNotNull('regex_pattern')->get(); foreach ($candidates as $candidate) { $pattern = $candidate->regex_pattern; if (! $pattern) { continue; } $delimited = preg_match('/^([#\\/]).+\\1[imsxuADSUXJ]*$/', $pattern) ? $pattern : '/' . $pattern . '/i'; set_error_handler(function () { }); $matched = @preg_match($delimited, $value) === 1; restore_error_handler(); if ($matched) { return $candidate; } } return null; } protected function resolveBandId(Log $log, ?Round $round, array &$remarksEval): ?int { $bandId = null; if ($log->pband) { // Nejprve přímá mapa přes EDI bandy, fallback je interval v MHz. $pbandVal = mb_strtolower(trim($log->pband)); $ediBand = EdiBand::whereRaw('LOWER(value) = ?', [$pbandVal])->first(); if ($ediBand) { $mappedBandId = $ediBand->bands()->value('bands.id'); if ($mappedBandId) { if ($round && $round->bands()->where('bands.id', $mappedBandId)->exists()) { $bandId = $mappedBandId; } elseif (! $round || $round->bands()->count() === 0) { $bandId = $mappedBandId; } } } else { $num = is_numeric($pbandVal) ? (float) $pbandVal : null; if ($num === null && $log->pband) { if (preg_match('/([0-9]+(?:[\\.,][0-9]+)?)/', $log->pband, $m)) { $num = (float) str_replace(',', '.', $m[1]); } } if ($num !== null) { $bandMatch = \App\Models\Band::where('edi_band_begin', '<=', $num) ->where('edi_band_end', '>=', $num) ->first(); if ($bandMatch) { if ($round && $round->bands()->where('bands.id', $bandMatch->id)->exists()) { $bandId = $bandMatch->id; } elseif (! $round || $round->bands()->count() === 0) { $bandId = $bandMatch->id; } } } } } if ($bandId === null) { $remarksEval[] = 'Pásmo nebylo rozpoznáno.'; } return $bandId; } protected function resolvePowerCategoryId(Log $log, ?Round $round, array &$remarksEval): array { $powerCategoryId = null; $powerMismatch = false; if ($log->power_category_id) { $powerCategoryId = $log->power_category_id; } if ($round && $round->powerCategories()->count() > 0) { $exists = $round->powerCategories()->where('power_categories.id', $powerCategoryId)->exists(); if (! $exists) { $powerMismatch = true; } } return [$powerCategoryId, $powerMismatch]; } protected function isSixHourBand(?int $bandId): bool { if (! $bandId) { return false; } return in_array($bandId, [1, 2], true); } protected function isCheckLog(Log $log): bool { $psect = trim((string) $log->psect); return $psect !== '' && preg_match('/\\bcheck\\b/i', $psect) === 1; } protected function decodeRemarksEval(?string $value): array { if (! $value) { return []; } $decoded = json_decode($value, true); return is_array($decoded) ? $decoded : []; } protected function encodeRemarksEval(array $value): ?string { $filtered = array_values(array_filter($value, fn ($item) => is_string($item) && trim($item) !== '')); $filtered = array_values(array_unique($filtered)); if (count($filtered) === 0) { return null; } return json_encode($filtered, JSON_UNESCAPED_UNICODE); } protected function addRemark(array &$remarksEval, string $message): void { if (! in_array($message, $remarksEval, true)) { $remarksEval[] = $message; } } protected function isTDateWithinRound(?string $tdate, ?Round $round): bool { if (! $tdate || ! $round || ! $round->start_time || ! $round->end_time) { return true; } $parts = explode(';', $tdate); if (count($parts) !== 2) { return false; } if (!preg_match('/^\\d{8}$/', $parts[0]) || !preg_match('/^\\d{8}$/', $parts[1])) { return false; } $start = \Carbon\Carbon::createFromFormat('Ymd', $parts[0])->startOfDay(); $end = \Carbon\Carbon::createFromFormat('Ymd', $parts[1])->endOfDay(); return $start->lte($round->end_time) && $end->gte($round->start_time); } }