480 lines
21 KiB
PHP
480 lines
21 KiB
PHP
<?php
|
||
|
||
namespace App\Models;
|
||
|
||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||
use Illuminate\Database\Eloquent\Model;
|
||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||
|
||
class EvaluationRuleSet extends Model
|
||
{
|
||
use HasFactory;
|
||
|
||
protected $table = 'evaluation_rule_sets';
|
||
|
||
/**
|
||
* Atributy rulesetu používané při vyhodnocování.
|
||
*
|
||
* Skupiny:
|
||
* - scoring_mode/points_per_*: způsob bodování.
|
||
* - multipliers*: multiplikátory a jejich scope.
|
||
* - *_policy / penalty_*: penalizace a způsob započtení chyb.
|
||
* - exchange_* / discard_*: pravidla výměny a busted detekce.
|
||
* - match_* / callsign_*: pravidla párování a normalizace.
|
||
* - out_of_window_*: chování mimo časové okno.
|
||
*/
|
||
protected $fillable = [
|
||
'name',
|
||
'code',
|
||
'description',
|
||
|
||
|
||
'scoring_mode', // DISTANCE / FIXED_POINTS
|
||
'points_per_qso', // Fixní body za valid QSO
|
||
'points_per_km', // Body za km v režimu DISTANCE
|
||
|
||
'use_multipliers', // Zapnout multiplikátory ve výsledku
|
||
'multiplier_type', // NONE / WWL / DXCC / SECTION / COUNTRY
|
||
|
||
'dup_qso_policy', // ZERO_POINTS / PENALTY
|
||
'nil_qso_policy', // ZERO_POINTS / PENALTY
|
||
'no_counterpart_log_policy', // INVALID / ZERO_POINTS / FLAG_ONLY / PENALTY
|
||
'not_in_counterpart_log_policy', // INVALID / ZERO_POINTS / FLAG_ONLY / PENALTY
|
||
'unique_qso_policy', // INVALID / ZERO_POINTS / FLAG_ONLY
|
||
'busted_call_policy', // ZERO_POINTS / PENALTY
|
||
'busted_exchange_policy',// ZERO_POINTS / PENALTY
|
||
'busted_serial_policy', // ZERO_POINTS / PENALTY
|
||
'busted_locator_policy', // ZERO_POINTS / PENALTY
|
||
'penalty_dup_points', // Penalizace za DUP
|
||
'penalty_nil_points', // Penalizace za NIL
|
||
'penalty_busted_call_points', // Penalizace za BUSTED_CALL
|
||
'penalty_busted_exchange_points', // Penalizace za BUSTED_EXCHANGE
|
||
'penalty_busted_serial_points', // Penalizace za BUSTED_SERIAL
|
||
'penalty_busted_locator_points', // Penalizace za BUSTED_LOCATOR
|
||
'dupe_scope', // BAND / BAND_MODE
|
||
'callsign_normalization',// STRICT / IGNORE_SUFFIX
|
||
'distance_rounding', // FLOOR / ROUND / CEIL
|
||
'min_distance_km', // Minimum km pro bodované QSO
|
||
'require_locators', // Bez lokátorů = nevalidní / bez bodů
|
||
'out_of_window_policy', // IGNORE / ZERO_POINTS / PENALTY / INVALID
|
||
'penalty_out_of_window_points', // Penalizace za OUT_OF_WINDOW
|
||
|
||
'exchange_type', // SERIAL / WWL / SERIAL_WWL / CUSTOM
|
||
'exchange_requires_wwl', // WWL je povinná část výměny
|
||
'exchange_requires_serial', // Serial je povinná část výměny
|
||
'exchange_requires_report', // RST je povinná část výměny
|
||
'exchange_pattern', // Regex pro CUSTOM výměnu
|
||
|
||
'ignore_slash_part', // Ignorovat suffix za lomítkem v callsign
|
||
'ignore_third_part', // Ignorovat 3. část callsign
|
||
'letters_in_rst', // Povolit písmena v RST
|
||
'rst_ignore_third_char', // Ignorovat 3. znak v RST
|
||
'discard_qso_rec_diff_call', // RX neshoda callsign -> BUSTED_CALL (error_side=RX)
|
||
'discard_qso_sent_diff_call', // TX neshoda callsign -> BUSTED_CALL (error_side=TX)
|
||
'discard_qso_rec_diff_rst', // RX neshoda RST -> BUSTED_RST (error_side=RX)
|
||
'discard_qso_sent_diff_rst', // TX neshoda RST -> BUSTED_RST (error_side=TX)
|
||
'discard_qso_rec_diff_code', // RX neshoda exchange -> BUSTED_SERIAL/LOCATOR (error_side=RX)
|
||
'discard_qso_sent_diff_code',// TX neshoda exchange -> BUSTED_SERIAL/LOCATOR (error_side=TX)
|
||
'discard_qso_rec_diff_serial', // RX neshoda serial -> BUSTED_SERIAL (error_side=RX)
|
||
'discard_qso_sent_diff_serial',// TX neshoda serial -> BUSTED_SERIAL (error_side=TX)
|
||
'discard_qso_rec_diff_wwl', // RX neshoda WWL -> BUSTED_LOCATOR (error_side=RX)
|
||
'discard_qso_sent_diff_wwl', // TX neshoda WWL -> BUSTED_LOCATOR (error_side=TX)
|
||
'busted_rst_policy', // ZERO_POINTS / PENALTY
|
||
'penalty_busted_rst_points', // Penalizace za BUSTED_RST
|
||
|
||
'match_tiebreak_order', // Pořadí tiebreak kritérií
|
||
'match_require_locator_match', // Matching vyžaduje lokátor
|
||
'match_require_exchange_match',// Matching vyžaduje exchange
|
||
|
||
'multiplier_scope', // PER_BAND / OVERALL
|
||
'multiplier_source', // VALID_ONLY / ALL_MATCHED
|
||
'wwl_multiplier_level', // LOCATOR_2 / LOCATOR_4 / LOCATOR_6
|
||
|
||
'checklog_matching', // CHECK logy v matchingu
|
||
'out_of_window_dq_threshold', // DQ logu při nadlimitních OOW QSO
|
||
'time_diff_dq_threshold_percent', // DQ logu při nadlimitním % časového rozdílu
|
||
'time_diff_dq_threshold_sec', // Prah časového rozdílu v sekundách
|
||
'bad_qso_dq_threshold_percent', // DQ logu při nadlimitním % špatných QSO
|
||
|
||
'time_tolerance_sec', // Tolerance času v matchingu (sekundy)
|
||
'require_unique_qso', // Zapnout detekci duplicit v logu
|
||
'allow_time_shift_one_hour', // Povolit posun o 1 hodinu při matchingu
|
||
'time_shift_seconds', // Velikost časového posunu v sekundách
|
||
'time_mismatch_policy', // INVALID / ZERO_POINTS / FLAG_ONLY / PENALTY
|
||
'callsign_suffix_max_len', // Max délka suffixu za /
|
||
'callsign_levenshtein_max', // Maximální Levenshtein vzdálenost pro fuzzy match
|
||
'allow_time_mismatch_pairing', // Povolit párování mimo toleranci
|
||
'time_mismatch_max_sec', // Max. odchylka mimo toleranci (NULL = bez limitu)
|
||
'dup_resolution_strategy', // Pořadí pravidel pro volbu survivor v duplicitách
|
||
|
||
'operating_window_mode', // NONE / BEST_CONTIGUOUS
|
||
'operating_window_hours', // Délka okna v hodinách
|
||
'sixhr_ranking_mode', // IARU / CRK
|
||
|
||
'options', // Volitelné JSON rozšíření pravidel
|
||
];
|
||
|
||
protected $casts = [
|
||
|
||
'points_per_qso' => 'integer',
|
||
'points_per_km' => 'float',
|
||
|
||
'use_multipliers' => 'boolean',
|
||
'time_tolerance_sec' => 'integer',
|
||
'require_unique_qso' => 'boolean',
|
||
'penalty_dup_points' => 'integer',
|
||
'penalty_nil_points' => 'integer',
|
||
'penalty_busted_call_points' => 'integer',
|
||
'penalty_busted_exchange_points' => 'integer',
|
||
'penalty_busted_serial_points' => 'integer',
|
||
'penalty_busted_locator_points' => 'integer',
|
||
'min_distance_km' => 'integer',
|
||
'require_locators' => 'boolean',
|
||
'penalty_out_of_window_points' => 'integer',
|
||
|
||
'exchange_requires_wwl' => 'boolean',
|
||
'exchange_requires_serial' => 'boolean',
|
||
'exchange_requires_report' => 'boolean',
|
||
'ignore_slash_part' => 'boolean',
|
||
'ignore_third_part' => 'boolean',
|
||
'letters_in_rst' => 'boolean',
|
||
'rst_ignore_third_char' => 'boolean',
|
||
'discard_qso_rec_diff_call' => 'boolean',
|
||
'discard_qso_sent_diff_call' => 'boolean',
|
||
'discard_qso_rec_diff_rst' => 'boolean',
|
||
'discard_qso_sent_diff_rst' => 'boolean',
|
||
'discard_qso_rec_diff_code' => 'boolean',
|
||
'discard_qso_sent_diff_code' => 'boolean',
|
||
'discard_qso_rec_diff_serial' => 'boolean',
|
||
'discard_qso_sent_diff_serial' => 'boolean',
|
||
'discard_qso_rec_diff_wwl' => 'boolean',
|
||
'discard_qso_sent_diff_wwl' => 'boolean',
|
||
'penalty_busted_rst_points' => 'integer',
|
||
|
||
'match_tiebreak_order' => 'array',
|
||
'match_require_locator_match' => 'boolean',
|
||
'match_require_exchange_match' => 'boolean',
|
||
|
||
'checklog_matching' => 'boolean',
|
||
'out_of_window_dq_threshold' => 'integer',
|
||
'time_diff_dq_threshold_percent' => 'integer',
|
||
'time_diff_dq_threshold_sec' => 'integer',
|
||
'bad_qso_dq_threshold_percent' => 'integer',
|
||
'allow_time_shift_one_hour' => 'boolean',
|
||
'time_shift_seconds' => 'integer',
|
||
'callsign_suffix_max_len' => 'integer',
|
||
'callsign_levenshtein_max' => 'integer',
|
||
'allow_time_mismatch_pairing' => 'boolean',
|
||
'time_mismatch_max_sec' => 'integer',
|
||
'dup_resolution_strategy' => 'array',
|
||
|
||
'operating_window_mode' => 'string',
|
||
'operating_window_hours' => 'integer',
|
||
'sixhr_ranking_mode' => 'string',
|
||
|
||
'options' => 'array',
|
||
];
|
||
|
||
/**
|
||
* Mapování: kde je jednotlivý atribut rulesetu použit.
|
||
* Pokud je hodnota "unused", musí být explicitně zdůvodněno.
|
||
*/
|
||
public const FLAG_USAGE = [
|
||
'name' => 'metadata (UI, identifikace rulesetu)',
|
||
'code' => 'metadata (UI, identifikace rulesetu)',
|
||
'description' => 'metadata (UI, identifikace rulesetu)',
|
||
'scoring_mode' => 'ScoringService::computeBasePoints',
|
||
'points_per_qso' => 'ScoringService::computeBasePoints',
|
||
'points_per_km' => 'ScoringService::computeBasePoints',
|
||
'use_multipliers' => 'ScoreGroupJob::applyMultipliers + AggregateLogResultsJob',
|
||
'multiplier_type' => 'ScoreGroupJob::applyMultipliers + AggregateLogResultsJob',
|
||
'dup_qso_policy' => 'ScoringService::penaltyPointsFor',
|
||
'nil_qso_policy' => 'ScoringService::penaltyPointsFor',
|
||
'no_counterpart_log_policy' => 'ScoreGroupJob (validita/bodování)',
|
||
'not_in_counterpart_log_policy' => 'ScoreGroupJob (validita/bodování)',
|
||
'unique_qso_policy' => 'ScoreGroupJob (validita/bodování)',
|
||
'busted_call_policy' => 'ScoringService::penaltyPointsFor',
|
||
'busted_exchange_policy' => 'ScoringService::penaltyPointsFor',
|
||
'busted_serial_policy' => 'ScoringService::penaltyPointsFor',
|
||
'busted_locator_policy' => 'ScoringService::penaltyPointsFor',
|
||
'penalty_dup_points' => 'ScoringService::penaltyPointsFor',
|
||
'penalty_nil_points' => 'ScoringService::penaltyPointsFor',
|
||
'penalty_busted_call_points' => 'ScoringService::penaltyPointsFor',
|
||
'penalty_busted_exchange_points' => 'ScoringService::penaltyPointsFor',
|
||
'penalty_busted_serial_points' => 'ScoringService::penaltyPointsFor',
|
||
'penalty_busted_locator_points' => 'ScoringService::penaltyPointsFor',
|
||
'dupe_scope' => 'BuildWorkingSetLogJob::dupe_key',
|
||
'callsign_normalization' => 'MatchingService::normalizeCallsign + MatchQsoGroupJob',
|
||
'distance_rounding' => 'ScoringService::calculateDistanceKm',
|
||
'min_distance_km' => 'ScoringService::calculateDistanceKm',
|
||
'require_locators' => 'ScoreGroupJob (validita/bodování)',
|
||
'out_of_window_policy' => 'ScoringService::outOfWindowDecision + ScoreGroupJob',
|
||
'penalty_out_of_window_points' => 'ScoringService::penaltyPointsFor',
|
||
'exchange_type' => 'MatchQsoGroupJob::resolveExchangeMatch',
|
||
'exchange_requires_wwl' => 'MatchQsoGroupJob::resolveExchangeMatch',
|
||
'exchange_requires_serial' => 'MatchQsoGroupJob::resolveExchangeMatch',
|
||
'exchange_requires_report' => 'MatchQsoGroupJob::resolveExchangeMatch',
|
||
'exchange_pattern' => 'MatchQsoGroupJob::resolveExchangeMatch',
|
||
'ignore_slash_part' => 'MatchQsoGroupJob::normalizeCallsign',
|
||
'ignore_third_part' => 'MatchQsoGroupJob::normalizeCallsign',
|
||
'letters_in_rst' => 'MatchQsoGroupJob::normalizeRst',
|
||
'rst_ignore_third_char' => 'MatchQsoGroupJob::reportsMatch',
|
||
'discard_qso_rec_diff_call' => 'MatchQsoGroupJob (error_side=RX)',
|
||
'discard_qso_sent_diff_call' => 'MatchQsoGroupJob (error_side=TX)',
|
||
'discard_qso_rec_diff_rst' => 'MatchQsoGroupJob (error_side=RX)',
|
||
'discard_qso_sent_diff_rst' => 'MatchQsoGroupJob (error_side=TX)',
|
||
'discard_qso_rec_diff_code' => 'MatchQsoGroupJob (RX exchange mismatch)',
|
||
'discard_qso_sent_diff_code' => 'MatchQsoGroupJob (TX exchange mismatch)',
|
||
'discard_qso_rec_diff_serial' => 'MatchQsoGroupJob (RX serial mismatch)',
|
||
'discard_qso_sent_diff_serial' => 'MatchQsoGroupJob (TX serial mismatch)',
|
||
'discard_qso_rec_diff_wwl' => 'MatchQsoGroupJob (RX locator mismatch)',
|
||
'discard_qso_sent_diff_wwl' => 'MatchQsoGroupJob (TX locator mismatch)',
|
||
'busted_rst_policy' => 'ScoringService::penaltyPointsFor',
|
||
'penalty_busted_rst_points' => 'ScoringService::penaltyPointsFor',
|
||
'match_tiebreak_order' => 'MatchQsoGroupJob::rankDecision',
|
||
'match_require_locator_match' => 'MatchQsoGroupJob::resolveExchangeMatch',
|
||
'match_require_exchange_match' => 'MatchQsoGroupJob::resolveExchangeMatch',
|
||
'multiplier_scope' => 'AggregateLogResultsJob::aggregateMultipliers',
|
||
'multiplier_source' => 'AggregateLogResultsJob::aggregateMultipliers',
|
||
'wwl_multiplier_level' => 'ScoreGroupJob::applyMultipliers',
|
||
'checklog_matching' => 'MatchQsoGroupJob::groupLogsByKey',
|
||
'out_of_window_dq_threshold' => 'AggregateLogResultsJob (DQ logu)',
|
||
'time_diff_dq_threshold_percent' => 'AggregateLogResultsJob (DQ logu)',
|
||
'time_diff_dq_threshold_sec' => 'AggregateLogResultsJob (DQ logu)',
|
||
'bad_qso_dq_threshold_percent' => 'AggregateLogResultsJob (DQ logu)',
|
||
'time_tolerance_sec' => 'MatchQsoGroupJob::findCandidates',
|
||
'require_unique_qso' => 'UnpairedClassificationJob::UNIQUE',
|
||
'allow_time_shift_one_hour' => 'MatchQsoGroupJob::matchWithTimeShift',
|
||
'time_shift_seconds' => 'MatchQsoGroupJob::matchWithTimeShift',
|
||
'time_mismatch_policy' => 'ScoreGroupJob (validita/bodování)',
|
||
'callsign_suffix_max_len' => 'MatchingService::normalizeCallsign',
|
||
'callsign_levenshtein_max' => 'MatchQsoGroupJob::rankDecision',
|
||
'allow_time_mismatch_pairing' => 'MatchQsoGroupJob::findCandidates',
|
||
'time_mismatch_max_sec' => 'MatchQsoGroupJob::findCandidates',
|
||
'dup_resolution_strategy' => 'DuplicateResolutionJob::sort',
|
||
'operating_window_mode' => 'AggregateLogResultsJob (operating window, 6H agregace)',
|
||
'operating_window_hours' => 'AggregateLogResultsJob (operating window, 6H agregace)',
|
||
'sixhr_ranking_mode' => 'RecalculateOfficialRanksJob (6H ranking mode)',
|
||
'options' => 'fallback pro hodnoty bez sloupců (EvaluationRuleSet::getOption)',
|
||
];
|
||
|
||
/**
|
||
* Options fallback (používá se jen pokud není vyplněn odpovídající sloupec).
|
||
*
|
||
* Klíče, typy, defaulty a fáze použití:
|
||
* - ignore_slash_part (bool, default=true) – matching (normalizace callsign)
|
||
* - callsign_suffix_max_len (int, default=4) – matching (normalizace callsign)
|
||
* - rst_ignore_third_char (bool, default=true) – matching (porovnání RST)
|
||
* - letters_in_rst (bool, default=false) – matching (porovnání RST)
|
||
* - callsign_levenshtein_max (int, default=0) – matching (fuzzy callsign)
|
||
* - allow_time_shift_one_hour (bool, default=true) – matching (časový posun)
|
||
* - time_shift_seconds (int, default=3600) – matching (časový posun)
|
||
* - allow_time_mismatch_pairing (bool, default=false) – matching (TIME_MISMATCH pairing)
|
||
* - time_mismatch_max_sec (int|null, default=null) – matching (max odchylka mimo toleranci)
|
||
* - match_require_locator_match (bool, default=false) – matching (mismatch flagging)
|
||
* - match_require_exchange_match (bool, default=false) – matching (mismatch flagging)
|
||
* - unique_qso_enabled (bool, default=true) – unpaired klasifikace (UNIQUE)
|
||
* - unique_qso_policy (string, default=FLAG_ONLY) – scoring (validita/bodování)
|
||
* - no_counterpart_log_policy (string, default=FLAG_ONLY) – scoring
|
||
* - not_in_counterpart_log_policy (string, default=ZERO_POINTS) – scoring
|
||
* - duplicate_resolution_strategy (array, default=[paired_first, ok_first, earlier_time, lower_id]) – duplicity
|
||
* - distance_rounding (string, default=CEIL) – scoring (vzdálenost)
|
||
* - min_distance_km (int, default=1) – scoring (vzdálenost)
|
||
*/
|
||
|
||
// ----- Vztahy -----
|
||
|
||
public function evaluationRuns(): HasMany
|
||
{
|
||
return $this->hasMany(EvaluationRun::class, 'rule_set_id');
|
||
}
|
||
|
||
// ----- Pomocné metody pro logiku vyhodnocení -----
|
||
|
||
public function isDistanceScoring(): bool
|
||
{
|
||
return $this->scoring_mode === 'DISTANCE';
|
||
}
|
||
|
||
public function isFixedPointsScoring(): bool
|
||
{
|
||
return $this->scoring_mode === 'FIXED_POINTS';
|
||
}
|
||
|
||
public function usesMultipliers(): bool
|
||
{
|
||
return $this->use_multipliers && $this->multiplier_type !== 'NONE';
|
||
}
|
||
|
||
public function multiplierIsWwl(): bool
|
||
{
|
||
return $this->multiplier_type === 'WWL';
|
||
}
|
||
|
||
public function multiplierIsDxcc(): bool
|
||
{
|
||
return $this->multiplier_type === 'DXCC';
|
||
}
|
||
|
||
public function multiplierIsSection(): bool
|
||
{
|
||
return $this->multiplier_type === 'SECTION';
|
||
}
|
||
|
||
public function multiplierIsCountry(): bool
|
||
{
|
||
return $this->multiplier_type === 'COUNTRY';
|
||
}
|
||
|
||
public function dupCountsAsPenalty(): bool
|
||
{
|
||
return $this->dup_qso_policy === 'PENALTY';
|
||
}
|
||
|
||
// ----- Options fallback & typed accessors -----
|
||
|
||
protected function getOption(string $key, mixed $default = null): mixed
|
||
{
|
||
$options = $this->options ?? [];
|
||
return array_key_exists($key, $options) ? $options[$key] : $default;
|
||
}
|
||
|
||
public function getString(string $key, ?string $columnValue, ?string $default = null): ?string
|
||
{
|
||
if ($columnValue !== null) {
|
||
return $columnValue;
|
||
}
|
||
$value = $this->getOption($key, $default);
|
||
return is_string($value) ? $value : $default;
|
||
}
|
||
|
||
public function getBool(string $key, ?bool $columnValue, bool $default = false): bool
|
||
{
|
||
if ($columnValue !== null) {
|
||
return (bool) $columnValue;
|
||
}
|
||
return (bool) $this->getOption($key, $default);
|
||
}
|
||
|
||
public function getInt(string $key, ?int $columnValue, ?int $default = null): ?int
|
||
{
|
||
if ($columnValue !== null) {
|
||
return (int) $columnValue;
|
||
}
|
||
$value = $this->getOption($key, $default);
|
||
return $value === null ? null : (int) $value;
|
||
}
|
||
|
||
public function getArray(string $key, ?array $columnValue, array $default = []): array
|
||
{
|
||
if ($columnValue !== null) {
|
||
return $columnValue;
|
||
}
|
||
$value = $this->getOption($key, $default);
|
||
return is_array($value) ? $value : $default;
|
||
}
|
||
|
||
public function ignoreSlashPart(): bool
|
||
{
|
||
return $this->getBool('ignore_slash_part', $this->ignore_slash_part);
|
||
}
|
||
|
||
public function ignoreThirdPart(): bool
|
||
{
|
||
return $this->getBool('ignore_third_part', $this->ignore_third_part);
|
||
}
|
||
|
||
public function lettersInRst(): bool
|
||
{
|
||
return $this->getBool('letters_in_rst', $this->letters_in_rst);
|
||
}
|
||
|
||
public function callsignLevenshteinMax(): int
|
||
{
|
||
return (int) ($this->getInt('callsign_levenshtein_max', $this->callsign_levenshtein_max, 0) ?? 0);
|
||
}
|
||
|
||
public function callsignSuffixMaxLen(): int
|
||
{
|
||
return (int) ($this->getInt('callsign_suffix_max_len', $this->callsign_suffix_max_len, 4) ?? 4);
|
||
}
|
||
|
||
public function rstIgnoreThirdChar(): bool
|
||
{
|
||
return $this->getBool('rst_ignore_third_char', $this->rst_ignore_third_char, true);
|
||
}
|
||
|
||
public function matchRequireLocatorMatch(): bool
|
||
{
|
||
return $this->getBool('match_require_locator_match', $this->match_require_locator_match, false);
|
||
}
|
||
|
||
public function matchRequireExchangeMatch(): bool
|
||
{
|
||
return $this->getBool('match_require_exchange_match', $this->match_require_exchange_match, false);
|
||
}
|
||
|
||
public function allowTimeShiftOneHour(): bool
|
||
{
|
||
return $this->getBool('allow_time_shift_one_hour', $this->allow_time_shift_one_hour, true);
|
||
}
|
||
|
||
public function allowTimeMismatchPairing(): bool
|
||
{
|
||
return $this->getBool('allow_time_mismatch_pairing', $this->allow_time_mismatch_pairing, false);
|
||
}
|
||
|
||
public function timeMismatchMaxSec(): ?int
|
||
{
|
||
return $this->getInt('time_mismatch_max_sec', $this->time_mismatch_max_sec, null);
|
||
}
|
||
|
||
public function uniqueQsoEnabled(): bool
|
||
{
|
||
return $this->getBool('unique_qso_enabled', $this->require_unique_qso, true);
|
||
}
|
||
|
||
public function dupResolutionStrategy(): array
|
||
{
|
||
$fallback = $this->getArray('duplicate_resolution_strategy', null, [
|
||
'paired_first',
|
||
'ok_first',
|
||
'earlier_time',
|
||
'lower_id',
|
||
]);
|
||
|
||
$strategy = $this->getArray('duplicate_resolution_strategy', $this->dup_resolution_strategy, $fallback);
|
||
if ($strategy !== $fallback) {
|
||
return $strategy;
|
||
}
|
||
|
||
return $this->getArray('dup_resolution_strategy', $this->dup_resolution_strategy, $fallback);
|
||
}
|
||
|
||
public function distanceRounding(): string
|
||
{
|
||
return $this->getString('distance_rounding', $this->distance_rounding, 'CEIL') ?? 'CEIL';
|
||
}
|
||
|
||
public function minDistanceKm(): ?int
|
||
{
|
||
return $this->getInt('min_distance_km', $this->min_distance_km, 1);
|
||
}
|
||
|
||
public function nilCountsAsPenalty(): bool
|
||
{
|
||
return $this->nil_qso_policy === 'PENALTY';
|
||
}
|
||
|
||
public function bustedCallCountsAsPenalty(): bool
|
||
{
|
||
return $this->busted_call_policy === 'PENALTY';
|
||
}
|
||
|
||
public function bustedExchangeCountsAsPenalty(): bool
|
||
{
|
||
return $this->busted_exchange_policy === 'PENALTY';
|
||
}
|
||
|
||
}
|