Menu

Change language

GitHub repo

Flight Radar Map

blocklogistics

Real-time commercial and cargo aviation radar with aircraft altitudes, ground speeds, callsigns, and flight telemetry.

Installation

$npx uipkge-ng@latest add flight-radar-map
Manifest:https://uipkge.dev/r/angular/flight-radar-map.jsonInstalls to:src/app/components/blocks/flight-radar-map/

Variants

Loading interactive previews…

Schema

Type aliases exported from this item's source. Use these to shape the data you pass in.

Flight
interface Flight {
  id: string
  callsign: string
  airline: string
  origin: string
  destination: string
  coords: [number, number]
  heading: number
  altitude: number
  speed: number
  aircraft: string
  status: 'Cruising' | 'Ascending' | 'Descending'
}

npm dependencies

Files installed (2)

  • src/app/components/blocks/flight-radar-map/flight-radar-map.component.ts9.2 kB
    // Real-time commercial and cargo aviation radar with aircraft altitudes, ground speeds,
    // callsigns, and flight telemetry. Port of the React block 1:1 on Mapbox; markers follow
    // the hub-locator-map Angular pattern.
    import {
      type AfterViewInit,
      ChangeDetectionStrategy,
      Component,
      type ElementRef,
      Input,
      type OnDestroy,
      type OnInit,
      type QueryList,
      ViewChildren,
      signal,
    } from '@angular/core'
    import type * as mapboxgl from 'mapbox-gl'
    import { cn } from '@/lib/utils'
    import { UiBadgeComponent } from '@/ui/badge/badge.component'
    import { UiButtonComponent } from '@/ui/button/button.component'
    import {
      UiCardComponent,
      UiCardContentComponent,
      UiCardDescriptionComponent,
      UiCardHeaderComponent,
      UiCardTitleComponent,
    } from '@/ui/card/card.component'
    import { UiMapComponent } from '@/ui/map/map.component'
    
    export interface Flight {
      id: string
      callsign: string
      airline: string
      origin: string
      destination: string
      coords: [number, number]
      heading: number
      altitude: number
      speed: number
      aircraft: string
      status: 'Cruising' | 'Ascending' | 'Descending'
    }
    
    export const DEFAULT_FLIGHTS: Flight[] = [
      {
        id: 'f-101',
        callsign: 'UAL482',
        airline: 'United Airlines',
        origin: 'SFO',
        destination: 'ORD',
        coords: [-112.0, 39.5],
        heading: 78,
        altitude: 34000,
        speed: 520,
        aircraft: 'B787-9',
        status: 'Cruising',
      },
      {
        id: 'f-102',
        callsign: 'DAL1209',
        airline: 'Delta Air Lines',
        origin: 'LAX',
        destination: 'JFK',
        coords: [-104.5, 36.2],
        heading: 72,
        altitude: 38000,
        speed: 545,
        aircraft: 'A350-900',
        status: 'Cruising',
      },
      {
        id: 'f-103',
        callsign: 'AAL89',
        airline: 'American Airlines',
        origin: 'DFW',
        destination: 'SEA',
        coords: [-107.2, 42.1],
        heading: 325,
        altitude: 29000,
        speed: 480,
        aircraft: 'B737-MAX8',
        status: 'Cruising',
      },
      {
        id: 'f-104',
        callsign: 'FDX14',
        airline: 'FedEx Express',
        origin: 'MEM',
        destination: 'OAK',
        coords: [-115.8, 38.0],
        heading: 285,
        altitude: 22000,
        speed: 440,
        aircraft: 'B777-F',
        status: 'Descending',
      },
    ]
    
    @Component({
      changeDetection: ChangeDetectionStrategy.Eager,
      selector: 'ui-flight-radar-map, [ui-flight-radar-map]',
      standalone: true,
      imports: [
        UiBadgeComponent,
        UiButtonComponent,
        UiCardComponent,
        UiCardContentComponent,
        UiCardDescriptionComponent,
        UiCardHeaderComponent,
        UiCardTitleComponent,
        UiMapComponent,
      ],
      host: {
        'data-slot': 'flight-radar-map',
        '[class]': 'hostClass',
      },
      template: `
        <div class="grid gap-4 lg:grid-cols-[1fr_320px]">
          <div class="border-border bg-card relative h-[480px] overflow-hidden rounded-xl border">
            <div
              ui-map
              [accessToken]="accessToken ?? ''"
              variant="dark"
              [center]="activeFlight().coords"
              [zoom]="5.2"
              class="h-full w-full"
              (created)="onMapCreated($event)"
            >
              <!-- Marker elements: handed to mapbox-gl Markers once the map exists, which moves the
                   inner #marker node onto the map; the outer node stays put for Angular's @for. -->
              <div class="hidden">
                @for (f of flights; track f.id) {
                  <div>
                    <div #marker>
                      <div class="group relative flex cursor-pointer flex-col items-center" (click)="selectFlight(f)">
                        <div [class]="pinClass(f.id)" [style.transform]="'rotate(' + f.heading + 'deg)'">▲</div>
                        <span [class]="callsignClass(f.id)">{{ f.callsign }}</span>
                      </div>
                    </div>
                  </div>
                }
              </div>
            </div>
          </div>
    
          <!-- Aviation Telemetry Radar Drawer -->
          <div ui-card class="flex flex-col justify-between">
            <div ui-card-header>
              <div class="flex items-center justify-between">
                <span ui-badge variant="outline" class="font-mono text-xs">{{ activeFlight().airline }}</span>
                <span ui-badge class="border-info/20 bg-info/10 text-info">{{ activeFlight().status }}</span>
              </div>
              <h3 ui-card-title class="mt-2 font-mono text-xl">{{ activeFlight().callsign }}</h3>
              <p ui-card-description class="flex items-center gap-2 font-mono text-xs">
                <span>{{ activeFlight().origin }}</span>
                <span>✈ ─── ✈</span>
                <span>{{ activeFlight().destination }}</span>
              </p>
            </div>
            <div ui-card-content class="space-y-4">
              <div class="border-border bg-muted/50 grid grid-cols-2 gap-2 rounded-lg border p-2.5 font-mono text-xs">
                <div>
                  <div class="text-muted-foreground text-xs uppercase">Altitude</div>
                  <div class="text-foreground text-sm font-bold">{{ activeFlight().altitude.toLocaleString() }} ft</div>
                </div>
                <div>
                  <div class="text-muted-foreground text-xs uppercase">Ground Speed</div>
                  <div class="text-foreground text-sm font-bold">{{ activeFlight().speed }} kts</div>
                </div>
                <div class="mt-2">
                  <div class="text-muted-foreground text-xs uppercase">Heading</div>
                  <div class="text-foreground text-sm font-bold">{{ activeFlight().heading }}°</div>
                </div>
                <div class="mt-2">
                  <div class="text-muted-foreground text-xs uppercase">Equipment</div>
                  <div class="text-foreground text-sm font-bold">{{ activeFlight().aircraft }}</div>
                </div>
              </div>
    
              <!-- Flight Selector List -->
              <div class="border-border space-y-1 border-t pt-3">
                <div class="text-muted-foreground mb-1.5 font-mono text-xs tracking-wider uppercase">
                  Monitored In-Flight
                </div>
                @for (f of flights; track f.id) {
                  <button type="button" [class]="flightRowClass(f.id)" (click)="selectFlight(f)">
                    <span class="font-mono">{{ f.callsign }} ({{ f.origin }}→{{ f.destination }})</span>
                    <span class="font-mono text-xs opacity-75">{{ f.altitude / 1000 }}k ft</span>
                  </button>
                }
              </div>
    
              <button ui-button variant="outline" size="sm" class="w-full font-mono text-xs" (click)="recenter()">
                Recenter Camera
              </button>
            </div>
          </div>
        </div>
      `,
    })
    export class UiFlightRadarMapComponent implements OnInit, AfterViewInit, OnDestroy {
      @Input() flights: Flight[] = DEFAULT_FLIGHTS.map((f) => ({ ...f, coords: [...f.coords] as [number, number] }))
      @Input() accessToken?: string
      @Input('class') className?: string
    
      @ViewChildren('marker') private markerEls?: QueryList<ElementRef<HTMLDivElement>>
    
      readonly activeId = signal('')
    
      private map: mapboxgl.Map | null = null
      private markers: mapboxgl.Marker[] = []
      private destroyed = false
    
      get hostClass(): string {
        return cn('block w-full', this.className)
      }
    
      ngOnInit(): void {
        this.activeId.set(this.flights[0]?.id ?? '')
      }
    
      ngAfterViewInit(): void {
        // A new flights list re-renders the marker elements; put the new set on the map.
        this.markerEls?.changes.subscribe(() => void this.syncMarkers())
      }
    
      ngOnDestroy(): void {
        this.destroyed = true
        this.clearMarkers()
        this.map = null
      }
    
      activeFlight(): Flight {
        return this.flights.find((f) => f.id === this.activeId()) ?? this.flights[0]!
      }
    
      pinClass(id: string): string {
        return cn(
          'text-info flex size-7 items-center justify-center rounded-full font-bold',
          this.activeId() === id ? 'bg-info/20 ring-2 ring-sky-400' : 'bg-background/80',
        )
      }
    
      callsignClass(id: string): string {
        return cn(
          'border-border bg-card/90 mt-1 rounded border px-1 py-0.5 font-mono text-xs font-semibold tracking-wider whitespace-nowrap shadow-xs',
          this.activeId() === id ? 'border-info/50 text-info' : 'text-foreground',
        )
      }
    
      flightRowClass(id: string): string {
        return cn(
          'flex w-full items-center justify-between rounded-md px-2 py-1.5 text-xs transition-colors',
          this.activeId() === id
            ? 'bg-accent text-accent-foreground font-semibold'
            : 'hover:bg-muted text-muted-foreground',
        )
      }
    
      onMapCreated(map: mapboxgl.Map): void {
        this.map = map
        void this.syncMarkers()
      }
    
      selectFlight(f: Flight): void {
        this.activeId.set(f.id)
        this.map?.flyTo?.({ center: f.coords, zoom: 6.5 })
      }
    
      recenter(): void {
        const active = this.activeFlight()
        if (active) this.selectFlight(active)
      }
    
      /** One mapbox-gl Marker per flight, anchored at its centre (the React MapMarker, anchor="center"). */
      private async syncMarkers(): Promise<void> {
        const map = this.map
        const els = this.markerEls?.toArray() ?? []
        if (!map) return
        const mod = await import('mapbox-gl')
        const mapbox = mod.default ?? mod
        if (this.destroyed || map !== this.map) return
        this.clearMarkers()
        this.markers = els.flatMap((el, i) => {
          const flight = this.flights[i]
          if (!flight) return []
          return [new mapbox.Marker({ element: el.nativeElement, anchor: 'center' }).setLngLat(flight.coords).addTo(map)]
        })
      }
    
      private clearMarkers(): void {
        for (const m of this.markers) m.remove()
        this.markers = []
      }
    }
    
  • src/app/components/blocks/flight-radar-map/index.ts0.1 kB

Raw manifest:https://uipkge.dev/r/angular/flight-radar-map.json