diff --git a/app/api/controllers/route.ts b/app/api/controllers/route.ts new file mode 100644 index 0000000..9ba24ec --- /dev/null +++ b/app/api/controllers/route.ts @@ -0,0 +1,104 @@ +import { NextResponse } from 'next/server'; + +interface Controller { + callsign: string; + frequency: string; + facility: number; +} + +const facilityTypes = { + 0: "OBS", + 1: "FSS", + 2: "DEL", + 3: "GND", + 4: "TWR", + 5: "APP", + 6: "CTR", +} as const; + +const facilityLongNames = { + OBS: "Observer", + FSS: "Flight Service Station", + DEL: "Clearance Delivery", + GND: "Ground", + TWR: "Tower", + APP: "Approach/Departure", + CTR: "Centre", +} as const; + +// Define FIR coverage for airports +const firCoverage: Record = { + CZQM: ["CYHZ", "CYFC", "CYQM", "CYSJ", "CYZX", "CYYG", "CYYT", "CYQX", "CYYR", "LFVP", "CYQI", "CYAY", "CYDF", "CYJT"], + CZUL: ["CYZV"], +}; + +export async function GET() { + try { + const response = await fetch('https://data.vatsim.net/v3/vatsim-data.json', { + next: { revalidate: 60 } + }); + + if (!response.ok) { + throw new Error('Failed to fetch VATSIM data'); + } + + const data = await response.json(); + + // First, get all controllers including CTR + const allControllers = data.controllers.map((controller: Controller) => { + const shortFacility = facilityTypes[controller.facility as keyof typeof facilityTypes] || "Unknown"; + return { + callsign: controller.callsign, + frequency: controller.frequency, + facility: shortFacility, + facilityLong: facilityLongNames[shortFacility as keyof typeof facilityLongNames] || "Unknown", + facilityType: controller.facility, + airport: controller.callsign.split('_')[0] + }; + }); + + // Separate CTR controllers + const ctrControllers = allControllers.filter(c => c.facilityType === 6); + + // Get non-CTR controllers + const localControllers = allControllers.filter(c => c.facilityType < 6 && c.facilityType > 0 && !c.callsign.includes("ATIS")); + + // Group local controllers by airport + const controllersByAirport = localControllers.reduce((acc: any, controller) => { + if (!acc[controller.airport]) { + acc[controller.airport] = []; + } + acc[controller.airport].push(controller); + return acc; + }, {}); + + // Sort controllers at each airport by facility type (higher number = higher priority) + Object.values(controllersByAirport).forEach((controllers: any) => { + controllers.sort((a: any, b: any) => b.facilityType - a.facilityType); + }); + + // Add CTR coverage for airports without local controllers + ctrControllers.forEach(ctr => { + const fir = ctr.callsign.split('_')[0]; + const coveredAirports = firCoverage[fir] || []; + + coveredAirports.forEach(airport => { + // Only add CTR if no local controllers are available + if (!controllersByAirport[airport] || controllersByAirport[airport].length === 0) { + controllersByAirport[airport] = [{ + callsign: ctr.callsign, + frequency: ctr.frequency, + facility: ctr.facility, + facilityLong: "Centre (Top-down)", + airport: airport + }]; + } + }); + }); + + return NextResponse.json(controllersByAirport); + } catch (error) { + console.error(error); + return NextResponse.json({ error: 'Failed to fetch controller data' }, { status: 500 }); + } +} \ No newline at end of file diff --git a/app/api/metar/route.ts b/app/api/metar/route.ts index 5536d52..9ddf38b 100644 --- a/app/api/metar/route.ts +++ b/app/api/metar/route.ts @@ -10,7 +10,7 @@ export async function GET() { headers: { 'Authorization': 'Bearer vmtkb1D8Tuva2Jw2tXihWcKE3m2sfDJkySBZygVx82I' }, - next: { revalidate: 60 } // Cache for 1 minutes + next: { revalidate: 300 } // Cache for 5 minutes }); if (!response.ok) return null; diff --git a/app/api/vatsim/[icao]/route.ts b/app/api/vatsim/[icao]/route.ts index 76a802b..0211872 100644 --- a/app/api/vatsim/[icao]/route.ts +++ b/app/api/vatsim/[icao]/route.ts @@ -34,7 +34,7 @@ export async function GET( .map((pilot) => ({ callsign: pilot.callsign, type: pilot.flight_plan.departure === icao ? 'Departure' : 'Arrival', - time: pilot.flight_plan.departure === icao ? pilot.flight_plan.deptime : pilot.flight_plan.arrival, + time: pilot.flight_plan.departure === icao ? pilot.flight_plan.deptime : pilot.flight_plan.enroute_time, route: pilot.flight_plan.route, origin: pilot.flight_plan.departure, destination: pilot.flight_plan.arrival diff --git a/components/metar-table.tsx b/components/metar-table.tsx index 3a97131..fb2be7e 100644 --- a/components/metar-table.tsx +++ b/components/metar-table.tsx @@ -1,6 +1,6 @@ "use client"; -import { useState, Fragment } from "react"; +import { useState, Fragment, useEffect } from "react"; import { Table, TableBody, @@ -10,7 +10,14 @@ import { TableRow, } from "@/components/ui/table"; import { Skeleton } from "@/components/ui/skeleton"; -import { ChevronDown, ChevronUp, Plane, PlaneLanding, PlaneTakeoff } from "lucide-react"; +import { ChevronDown, ChevronUp, Plane, PlaneLanding, PlaneTakeoff, Radio } from "lucide-react"; +import { Badge } from "@/components/ui/badge"; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@/components/ui/tooltip"; interface Flight { callsign: string; @@ -21,6 +28,13 @@ interface Flight { destination: string; } +interface Controller { + callsign: string; + frequency: string; + facility: string; + facilityLong: string; +} + interface MetarTableProps { data: any[]; loading: boolean; @@ -30,6 +44,25 @@ export function MetarTable({ data, loading }: MetarTableProps) { const [expandedAirport, setExpandedAirport] = useState(null); const [flights, setFlights] = useState([]); const [loadingFlights, setLoadingFlights] = useState(false); + const [controllers, setControllers] = useState>({}); + + // Fetch controller data + useEffect(() => { + async function fetchControllers() { + try { + const response = await fetch('/api/controllers'); + const data = await response.json(); + setControllers(data); + } catch (error) { + console.error('Failed to fetch controller data:', error); + } + } + + fetchControllers(); + // Refresh controller data every minute + const interval = setInterval(fetchControllers, 60000); + return () => clearInterval(interval); + }, []); const handleAirportClick = async (airport: string) => { if (expandedAirport === airport) { @@ -50,6 +83,19 @@ export function MetarTable({ data, loading }: MetarTableProps) { } }; + const getBadgeVariant = (facility: string) => { + switch (facility) { + case 'CTR': + return "secondary"; + case 'APP': + return "default"; + case 'TWR': + return "default"; + default: + return "outline"; + } + }; + if (loading) { return ; } @@ -65,6 +111,7 @@ export function MetarTable({ data, loading }: MetarTableProps) { Headwind (kts) Xwind (kts) Altimeter + ATC METAR @@ -95,11 +142,38 @@ export function MetarTable({ data, loading }: MetarTableProps) { {row.crosswind} {row.altimeter} + + {controllers[row.airport] ? ( +
+ + {controllers[row.airport].map((controller, i) => ( + + + + + {controller.facility} {controller.frequency} + + + +

{controller.facilityLong}

+

{controller.callsign}

+
+
+ ))} +
+
+ ) : ( + Uncontrolled + )} +
{row.metar} {expandedAirport === row.airport && ( - + {loadingFlights ? (
@@ -170,6 +244,7 @@ function LoadingSkeleton() { +
))} diff --git a/lib/utils/metar.ts b/lib/utils/metar.ts index b78433d..d8f36b6 100644 --- a/lib/utils/metar.ts +++ b/lib/utils/metar.ts @@ -36,7 +36,7 @@ export function getUTCtime() { const now = new Date(); return `${String(now.getUTCHours()).padStart(2, "0")}:${String( now.getUTCMinutes() - ).padStart(2, "0")} UTC`; + ).padStart(2, "0")}Z`; } export function calculateAirportData(airportCode: string, data: any) {