diff --git a/app/Http/Controllers/AircraftController.php b/app/Http/Controllers/AircraftController.php index cf2993d..29172f5 100644 --- a/app/Http/Controllers/AircraftController.php +++ b/app/Http/Controllers/AircraftController.php @@ -3,8 +3,10 @@ namespace App\Http\Controllers; use App\Models\Aircraft; +use App\Models\Airline; use App\Models\Airport; use App\Support\ActivityLevel; +use App\Support\MapBounds; use Illuminate\Http\Request; use Illuminate\Support\Facades\Session; @@ -97,7 +99,59 @@ public function index(Request $request) { ->limit($limit) ->get(); - return view('fleet.index', ['fleet' => $fleet, 'maxPages' => $maxPages, 'currentPage' => $page, 'showRetired' => $showRetired]); + // Fleet-location map data: the full active-airline fleet (retired aircraft + // excluded), independent of the table's search/pagination, grouped by airport. + [$mapMarkers, $mapView, $aircraftWithoutLocation] = $this->fleetMapData($currentActiveAirline); + + return view('fleet.index', [ + 'fleet' => $fleet, + 'maxPages' => $maxPages, + 'currentPage' => $page, + 'showRetired' => $showRetired, + 'mapMarkers' => $mapMarkers, + 'mapCenter' => $mapView['center'], + 'mapZoom' => $mapView['zoom'], + 'aircraftWithoutLocation' => $aircraftWithoutLocation, + ]); + } + + /** + * Build the Fleet Location map payload for an airline: one marker per airport + * (aircraft grouped together, popup listing them), the framing center/zoom, and + * a count of aircraft whose current location could not be resolved. + * + * @return array{0: array, 1: array{center: array{lat: float, long: float}, zoom: int}, 2: int} + */ + private function fleetMapData(Airline $airline): array + { + $aircraft = Aircraft::with('location') + ->where('used_by', $airline->id) + ->notRetired() + ->get(); + + $markers = []; + $points = []; + + // Skip (but count) aircraft with an unresolved/invalid current_loc, then + // group the rest by airport so each airport yields a single marker. + $located = $aircraft->filter(fn ($a) => $a->location !== null); + $withoutLocation = $aircraft->count() - $located->count(); + $groups = $located->groupBy(fn ($a) => $a->location->icao_code); + + foreach ($groups as $parked) { + $airport = $parked->first()->location; + $points[] = ['lat' => (float) $airport->latitude_deg, 'long' => (float) $airport->longitude_deg]; + $markers[] = [ + 'lat' => (float) $airport->latitude_deg, + 'long' => (float) $airport->longitude_deg, + 'info' => view('fleet._map_popup', [ + 'airport' => $airport, + 'aircraft' => $parked, + ])->render(), + ]; + } + + return [$markers, MapBounds::fit($points), $withoutLocation]; } public function create(Request $request) { diff --git a/app/Support/MapBounds.php b/app/Support/MapBounds.php new file mode 100644 index 0000000..bfca247 --- /dev/null +++ b/app/Support/MapBounds.php @@ -0,0 +1,58 @@ + float, 'long' => float]` (the component's marker keys). + */ +class MapBounds +{ + /** Clamp range for the derived zoom level (Leaflet 0 = whole world). */ + private const MIN_ZOOM = 2; + private const MAX_ZOOM = 10; + + /** + * @param array $points + * @return array{center: array{lat: float, long: float}, zoom: int} + */ + public static function fit(array $points): array + { + // No points: show the whole world. + if (empty($points)) { + return ['center' => ['lat' => 20.0, 'long' => 0.0], 'zoom' => self::MIN_ZOOM]; + } + + $lats = array_column($points, 'lat'); + $lngs = array_column($points, 'long'); + + // Single point (or several stacked on one spot): center on it, zoom in. + if (count($points) === 1) { + return [ + 'center' => ['lat' => (float) $lats[0], 'long' => (float) $lngs[0]], + 'zoom' => 5, + ]; + } + + $minLat = min($lats); + $maxLat = max($lats); + $minLng = min($lngs); + $maxLng = max($lngs); + + $center = [ + 'lat' => ($minLat + $maxLat) / 2, + 'long' => ($minLng + $maxLng) / 2, + ]; + + // Zoom from the larger of the two spans so both fit; -1 for padding. + $span = max($maxLat - $minLat, $maxLng - $minLng, 0.0001); + $zoom = (int) floor(log(360 / $span, 2)) - 1; + $zoom = max(self::MIN_ZOOM, min(self::MAX_ZOOM, $zoom)); + + return ['center' => $center, 'zoom' => $zoom]; + } +} diff --git a/resources/views/fleet/_map_popup.blade.php b/resources/views/fleet/_map_popup.blade.php new file mode 100644 index 0000000..b2450ce --- /dev/null +++ b/resources/views/fleet/_map_popup.blade.php @@ -0,0 +1,19 @@ +{{-- Fleet map marker popup: one airport, the aircraft parked there. --}} +
+
{{ $airport->icao_code }}
+ @if($airport->name) +
{{ $airport->name }}
+ @endif +
{{ $aircraft->count() }} {{ \Illuminate\Support\Str::plural('aircraft', $aircraft->count()) }}
+
    + @foreach($aircraft as $ac) +
  • + {{ $ac->registration }} + {{ $ac->full_type }} + @if($ac->status === \App\Models\Aircraft::STATUS_INACTIVE) + Inactive + @endif +
  • + @endforeach +
+
diff --git a/resources/views/fleet/index.blade.php b/resources/views/fleet/index.blade.php index 5b07911..bbc8fc9 100644 --- a/resources/views/fleet/index.blade.php +++ b/resources/views/fleet/index.blade.php @@ -58,6 +58,36 @@ class="btn btn-outline-secondary px-3 py-2 d-inline-flex align-items-center gap- + +
+ +
+ @if(count($mapMarkers) === 0) +
+ + No aircraft with a known location yet. +
+ @else + + @if($aircraftWithoutLocation > 0) + + @endif + @endif +
+
+ @if($errors->any())