Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 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
37 changes: 37 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,12 @@ private function standardConditionsStandQuery(
);
}

/**
* Build stand ordering for a given aircraft/query mode.
*
* We keep existing common ordering intact, but may prepend an additional
* night-time remote-stand condition when configured and applicable.
*/
Comment thread
MrAdder marked this conversation as resolved.
private function orderByForStandsQuery(
NetworkAircraft $aircraft,
array $customOrders,
Expand All @@ -93,12 +100,42 @@ private function orderByForStandsQuery(
: $this->commonOrderByConditionsWithoutAssignmentPriorityForAircraft($aircraft);
}

$nightTimeRemoteStandCondition = $this->nightTimeRemoteStandOrderCondition();

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

return array_merge(
$customOrders,
$commonConditions
);
Comment thread
kristiankunc marked this conversation as resolved.
}

/**
* 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');

$hour = Carbon::now('Europe/London')->hour;
Comment thread
MrAdder marked this conversation as resolved.
$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;

if (!$isNightWindow) {
return null;
}

return 'CASE WHEN stands.overnight_remote_preferred = 1 THEN 0 ELSE 1 END ASC';
}
Comment thread
MrAdder marked this conversation as resolved.

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
7 changes: 7 additions & 0 deletions config/stands.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,11 @@
return [
'auto_allocate' => env('AUTO_ALLOCATE_STANDS', false),
'assignment_acars_message' => env('SEND_STAND_ACARS_MESSAGES', true),
// Overnight bias for remote parking.
Comment thread
MrAdder marked this conversation as resolved.
Outdated
'night_remote_stand_weighting' => [
// Local Europe/London start hour (24h) for the overnight window (inclusive).
'start_hour' => 22,
// Local Europe/London end hour (24h) for the overnight window (exclusive).
'end_hour' => 6,
],
Comment thread
MrAdder marked this conversation as resolved.
Outdated
Comment thread
kristiankunc marked this conversation as resolved.
];
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,27 @@
<?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')
->comment('Whether the stand should be preferred for overnight remote parking bias');
Comment thread
MrAdder marked this conversation as resolved.
Outdated

$table->index('overnight_remote_preferred');
});
}

public function down(): void
{
Schema::table('stands', function (Blueprint $table): void {
$table->dropIndex(['overnight_remote_preferred']);
Comment thread
MrAdder marked this conversation as resolved.
Outdated
$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 when remote weighting is active for the airfield.',
Comment thread
MrAdder marked this conversation as resolved.
Outdated
],
'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
77 changes: 77 additions & 0 deletions tests/app/Allocator/Stand/FallbackArrivalStandAllocatorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,83 @@ 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'));

Comment thread
kristiankunc marked this conversation as resolved.
// 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);

Carbon::setTestNow();
}
Comment thread
MrAdder marked this conversation as resolved.

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

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

$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);

Carbon::setTestNow();
}

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