Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions app/Allocator/Stand/SelectsStandsUsingStandardConditions.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

use App\Models\Vatsim\NetworkAircraft;
use Closure;
use Carbon\Carbon;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Collection;

Expand Down Expand Up @@ -73,6 +74,9 @@ private function standardConditionsStandQuery(
);
}

/**
* Build stand ordering for a given aircraft/query mode.
*/
private function orderByForStandsQuery(
NetworkAircraft $aircraft,
array $customOrders,
Expand All @@ -93,12 +97,44 @@ private function orderByForStandsQuery(
: $this->commonOrderByConditionsWithoutAssignmentPriorityForAircraft($aircraft);
}

$nightTimeRemoteStandCondition = $this->nightTimeRemoteStandOrderCondition();

if ($nightTimeRemoteStandCondition !== null) {
$commonConditions = array_merge([$nightTimeRemoteStandCondition], $commonConditions);
}

return array_merge(
$customOrders,
$commonConditions
);
}

/**
* Returns an optional SQL ORDER BY fragment that prefers remote stands overnight
* within the configured night window, or null when the bias should not be applied.
*/
private function nightTimeRemoteStandOrderCondition(): ?string
{
$config = config('stands.night_remote_stand_weighting');

// Feature flag: if disabled or misconfigured, do not apply any bias.
if (!(bool) ($config['enabled'] ?? false)) {
return null;
}

// Check if current time is within the configured night window.
$hour = Carbon::now('Europe/London')->hour;
$startHour = (int) ($config['start_hour'] ?? 22);
$endHour = (int) ($config['end_hour'] ?? 6);

// Supports both normal windows (e.g. 01 -> 05) and windows crossing midnight (e.g. 22 -> 06).
$isNightWindow = $startHour <= $endHour
? $hour >= $startHour && $hour < $endHour
: $hour >= $startHour || $hour < $endHour;

return $isNightWindow ? 'CASE WHEN stands.overnight_remote_preferred = 1 THEN 0 ELSE 1 END ASC' : null;
}

private function commonOrderByConditionsForAircraft(NetworkAircraft $aircraft): array
{
return $aircraft->cid === null
Expand Down
6 changes: 6 additions & 0 deletions app/Filament/Resources/StandResource.php
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,10 @@ public static function form(Form $form): Form
->maxValue(9999)
->default(100)
->required(),
Toggle::make('overnight_remote_preferred')
->label(self::translateFormPath('overnight_remote_preferred.label'))
->helperText(self::translateFormPath('overnight_remote_preferred.helper'))
->default(false),
TextInput::make('origin_slug')
->label(self::translateFormPath('origin_slug.label'))
->helperText(self::translateFormPath('origin_slug.helper'))
Expand Down Expand Up @@ -203,6 +207,8 @@ public static function table(Table $table): Table
->label(self::translateTablePath('columns.priority'))
->sortable()
->searchable(),
Tables\Columns\BooleanColumn::make('overnight_remote_preferred')
->label(self::translateTablePath('columns.overnight_remote_preferred')),
])
->actions([
Tables\Actions\ViewAction::make(),
Expand Down
2 changes: 2 additions & 0 deletions app/Models/Stand/Stand.php
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ class Stand extends Model
'max_aircraft_length',
'max_aircraft_wingspan',
'assignment_priority',
'overnight_remote_preferred',
'closed_at',
'isOpen',
];
Expand All @@ -46,6 +47,7 @@ class Stand extends Model
'max_aircraft_length' => 'double',
'max_aircraft_wingspan' => 'double',
'assignment_priority' => 'integer',
'overnight_remote_preferred' => 'boolean',
'created_at' => 'datetime',
'updated_at' => 'datetime',
'closed_at' => 'datetime',
Expand Down
6 changes: 6 additions & 0 deletions config/stands.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,10 @@
return [
'auto_allocate' => env('AUTO_ALLOCATE_STANDS', false),
'assignment_acars_message' => env('SEND_STAND_ACARS_MESSAGES', true),
'night_remote_stand_weighting' => [
'enabled' => true,
// Europe/London time start and end hour (24h) for the overnight window (Automatic DST support).
'start_hour' => 22,
'end_hour' => 6,
],
];
1 change: 1 addition & 0 deletions database/factories/Stand/StandFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ public function definition()
'longitude' => $this->faker->longitude(),
'aerodrome_reference_code' => 'F', // A380
'assignment_priority' => $this->faker->numberBetween(0, 1000),
'overnight_remote_preferred' => false,
];
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration {
public function up(): void
{
Schema::table('stands', function (Blueprint $table): void {
$table->boolean('overnight_remote_preferred')
->default(false)
->after('assignment_priority');
});
}

public function down(): void
{
Schema::table('stands', function (Blueprint $table): void {
$table->dropColumn('overnight_remote_preferred');
});
}
};
4 changes: 4 additions & 0 deletions lang/en/stands/form.php
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,10 @@
'label' => 'Allocation Priority',
'helper' => 'Global priority when assigning. Lower value is higher priority. Minimum 1, maximum 9999.',
],
'overnight_remote_preferred' => [
'label' => 'Overnight Remote Preferred',
'helper' => 'If enabled, this stand is preferred for overnight remote parking by the allocator.',
],
'origin_slug' => [
'label' => 'Origin Slug',
'helper' => 'Full or partial airfield ICAO to match arrival aircraft against. This is used when doing a "any flights from these origin airports" allocation and does not override airline-specific rules.',
Expand Down
1 change: 1 addition & 0 deletions lang/en/stands/table.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
'airlines' => 'Airlines',
'used' => 'Used',
'priority' => 'Allocation Priority',
'overnight_remote_preferred' => 'Overnight Remote Preferred',
],
'airlines' => [
'description' => 'Airlines can be assigned to specific stands based on various parameters. See the allocation guide
Expand Down
81 changes: 81 additions & 0 deletions tests/app/Allocator/Stand/FallbackArrivalStandAllocatorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,87 @@ public function testItAssignsStandsInPriorityOrder()
$this->assertEquals($highPriority->id, $assignment);
}

public function testItPrefersRemoteStandsAtNight()
{
$airfield = Airfield::factory()->create(['code' => 'EXXA']);

Carbon::setTestNow(Carbon::create(2024, 1, 1, 23, 0, 0, 'Europe/London'));

try {
// Lower assignment priority = more desirable stand during daytime
Stand::create(
[
'airfield_id' => $airfield->id,
'identifier' => 'T1',
'latitude' => 54.65875500,
'longitude' => -6.22258694,
'aerodrome_reference_code' => 'C',
'assignment_priority' => 1,
]
);

$remoteStand = Stand::create(
[
'airfield_id' => $airfield->id,
'identifier' => 'R1',
'latitude' => 54.65875500,
'longitude' => -6.22258694,
'aerodrome_reference_code' => 'C',
'assignment_priority' => 100,
'overnight_remote_preferred' => true,
]
);

$aircraft = $this->createAircraft('AEU252', 'B738', $airfield->code);

$assignment = $this->allocator->allocate($aircraft);

$this->assertEquals($remoteStand->id, $assignment);
} finally {
Carbon::setTestNow();
}
}

public function testItDoesNotPreferRemoteStandsOutsideNightHours()
{
$airfield = Airfield::factory()->create(['code' => 'EXXB']);

Carbon::setTestNow(Carbon::create(2024, 1, 1, 12, 0, 0, 'Europe/London'));

try {
$terminalStand = Stand::create(
[
'airfield_id' => $airfield->id,
'identifier' => 'T1',
'latitude' => 54.65875500,
'longitude' => -6.22258694,
'aerodrome_reference_code' => 'C',
'assignment_priority' => 1,
]
);

Stand::create(
[
'airfield_id' => $airfield->id,
'identifier' => 'R1',
'latitude' => 54.65875500,
'longitude' => -6.22258694,
'aerodrome_reference_code' => 'C',
'assignment_priority' => 100,
'overnight_remote_preferred' => true,
]
);

$aircraft = $this->createAircraft('AEU252', 'B738', $airfield->code);

$assignment = $this->allocator->allocate($aircraft);

$this->assertEquals($terminalStand->id, $assignment);
} finally {
Carbon::setTestNow();
}
}

public function testItOnlyAssignsNonCargoStands()
{
// Create a stand a cargo stand
Expand Down
Loading