Added Notifications
This commit is contained in:
@@ -0,0 +1,51 @@
|
||||
import { computed, ComputedRef } from 'vue'
|
||||
import { Flight } from '@/Types/types'
|
||||
|
||||
// ── Types ──────────────────────────────────────────────────────────────────
|
||||
|
||||
export interface AircraftFamilyEntry {
|
||||
family: string
|
||||
designators: string[]
|
||||
flights: Flight[]
|
||||
}
|
||||
|
||||
// ── Composable ─────────────────────────────────────────────────────────────
|
||||
|
||||
export function useAircraftFamilies(
|
||||
flights: ComputedRef<Flight[]>,
|
||||
families: Record<string, string[]>,
|
||||
) {
|
||||
// Build a lookup from designator → family name for O(1) matching
|
||||
const designatorToFamily = Object.entries(families).reduce<Record<string, string>>(
|
||||
(acc, [family, designators]) => {
|
||||
for (const d of designators) acc[d] = family
|
||||
return acc
|
||||
},
|
||||
{}
|
||||
)
|
||||
|
||||
const entries = computed<AircraftFamilyEntry[]>(() => {
|
||||
// Pre-populate all families with empty flight lists
|
||||
const map = new Map<string, AircraftFamilyEntry>(
|
||||
Object.entries(families).map(([family, designators]) => [
|
||||
family,
|
||||
{ family, designators, flights: [] },
|
||||
])
|
||||
)
|
||||
|
||||
for (const flight of flights.value) {
|
||||
const designator = flight.aircraft?.designator
|
||||
if (!designator) continue
|
||||
const family = designatorToFamily[designator]
|
||||
if (!family) continue
|
||||
map.get(family)!.flights.push(flight)
|
||||
}
|
||||
|
||||
return [...map.values()]
|
||||
})
|
||||
|
||||
const completedCount = computed(() => entries.value.filter(e => e.flights.length > 0).length)
|
||||
const totalCount = computed(() => entries.value.length)
|
||||
|
||||
return { entries, completedCount, totalCount }
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import { computed, type Ref } from 'vue'
|
||||
import type { Flight } from '@/Types/types'
|
||||
|
||||
export type CodeType = 'iata' | 'icao'
|
||||
|
||||
const letters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'.split('')
|
||||
const digits = '0123456789'.split('')
|
||||
|
||||
export function getAllLetters(showNumbers: boolean): string[] {
|
||||
return showNumbers ? [...digits, ...letters] : letters
|
||||
}
|
||||
|
||||
export function useAlphabetAirlines(
|
||||
flights: Ref<Flight[]>,
|
||||
codeType: Ref<CodeType>,
|
||||
showNumbers: Ref<boolean>,
|
||||
) {
|
||||
const allLetters = computed(() => getAllLetters(showNumbers.value))
|
||||
|
||||
const flightsByLetter = computed(() => {
|
||||
const map: Record<string, Flight[]> = {}
|
||||
|
||||
for (const flight of flights.value) {
|
||||
const raw = codeType.value === 'iata'
|
||||
? flight.airline?.IATA_code
|
||||
: flight.airline?.ICAO_code
|
||||
|
||||
const code = raw?.trim().toUpperCase()
|
||||
if (!code) continue
|
||||
|
||||
const key = code[0]
|
||||
if (!allLetters.value.includes(key)) continue
|
||||
|
||||
;(map[key] ??= []).push(flight)
|
||||
}
|
||||
|
||||
return map
|
||||
})
|
||||
|
||||
const visitedLetters = computed(() => new Set(Object.keys(flightsByLetter.value)))
|
||||
|
||||
return { flightsByLetter, visitedLetters, allLetters }
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
import { computed, type ComputedRef, type Ref } from 'vue'
|
||||
import type { Flight } from '@/Types/types'
|
||||
|
||||
export type CodeType = 'iata' | 'icao'
|
||||
|
||||
export interface AlphabetFlightData {
|
||||
/** All 26 uppercase letters A–Z */
|
||||
allLetters: string[]
|
||||
/** Letters the user has visited (has at least one qualifying airport) */
|
||||
visitedLetters: ComputedRef<Set<string>>
|
||||
/** Map of letter → flights that touch an airport starting with that letter */
|
||||
flightsByLetter: ComputedRef<Record<string, Flight[]>>
|
||||
/** The code type being used */
|
||||
codeType: CodeType
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns which letters of the alphabet have been "visited" based on
|
||||
* the IATA or ICAO code of the departure or arrival airport on each flight.
|
||||
*
|
||||
* A letter is considered visited when at least one flight departs from or
|
||||
* arrives at an airport whose chosen code starts with that letter.
|
||||
*
|
||||
* Airports without a code of the chosen type are ignored.
|
||||
*/
|
||||
export function useAlphabetFlights(
|
||||
flights: Ref<Flight[]> | ComputedRef<Flight[]>,
|
||||
codeType: CodeType = 'iata',
|
||||
): AlphabetFlightData {
|
||||
const allLetters = Array.from({ length: 26 }, (_, i) =>
|
||||
String.fromCharCode(65 + i), // 'A' … 'Z'
|
||||
)
|
||||
|
||||
/** Pick the right code from an airport, uppercased first character or null */
|
||||
function getCode(airport: Flight['departure_airport'] | Flight['arrival_airport']): string | null {
|
||||
const raw = codeType === 'iata' ? airport.iata_code : airport.icao_code
|
||||
return raw ? raw.trim().toUpperCase() : null
|
||||
}
|
||||
|
||||
const flightsByLetter = computed<Record<string, Flight[]>>(() => {
|
||||
const map: Record<string, Flight[]> = {}
|
||||
for (const letter of allLetters) {
|
||||
map[letter] = []
|
||||
}
|
||||
|
||||
for (const flight of flights.value) {
|
||||
const codes = new Set<string>()
|
||||
|
||||
const depCode = getCode(flight.departure_airport)
|
||||
if (depCode) codes.add(depCode[0])
|
||||
|
||||
const arrCode = getCode(flight.arrival_airport)
|
||||
if (arrCode) codes.add(arrCode[0])
|
||||
|
||||
for (const letter of codes) {
|
||||
if (letter >= 'A' && letter <= 'Z') {
|
||||
map[letter]?.push(flight)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return map
|
||||
})
|
||||
|
||||
const visitedLetters = computed<Set<string>>(() => {
|
||||
const visited = new Set<string>()
|
||||
for (const [letter, letterFlights] of Object.entries(flightsByLetter.value)) {
|
||||
if (letterFlights.length > 0) {
|
||||
visited.add(letter)
|
||||
}
|
||||
}
|
||||
return visited
|
||||
})
|
||||
|
||||
return {
|
||||
allLetters,
|
||||
visitedLetters,
|
||||
flightsByLetter,
|
||||
codeType,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
import { computed, ComputedRef } from 'vue'
|
||||
import { Flight } from '@/Types/types'
|
||||
|
||||
export interface Continent {
|
||||
id: number
|
||||
name: string
|
||||
code: string
|
||||
internal_name: string
|
||||
}
|
||||
|
||||
export interface ContinentPairEntry {
|
||||
key: string
|
||||
label: string
|
||||
flights: Flight[]
|
||||
}
|
||||
|
||||
/** Stable undirected key — always alphabetically sorted so A|B === B|A */
|
||||
export function undirectedKey(a: string, b: string): string {
|
||||
return [a, b].sort().join('|')
|
||||
}
|
||||
|
||||
/** Directed key — order preserved */
|
||||
export function directedKey(dep: string, arr: string): string {
|
||||
return `${dep}|${arr}`
|
||||
}
|
||||
|
||||
export function labelFor(a: string, b: string): string {
|
||||
return `${a} ↔ ${b}`
|
||||
}
|
||||
|
||||
export function continentNameOf(flight: Flight, side: 'departure' | 'arrival'): string | null {
|
||||
const airport = side === 'departure' ? flight.departure_airport : flight.arrival_airport
|
||||
return airport.region?.continent?.name ?? null
|
||||
}
|
||||
|
||||
export function isInternational(flight: Flight): boolean {
|
||||
const depCountry = flight.departure_airport.region?.country_id
|
||||
const arrCountry = flight.arrival_airport.region?.country_id
|
||||
if (depCountry == null || arrCountry == null) return true
|
||||
return depCountry !== arrCountry
|
||||
}
|
||||
|
||||
/** Filter flights to only those that qualify for continent-pair achievements */
|
||||
export function qualifyingFlights(flights: Flight[]): Flight[] {
|
||||
return flights.filter(flight => {
|
||||
const dep = continentNameOf(flight, 'departure')
|
||||
const arr = continentNameOf(flight, 'arrival')
|
||||
if (!dep || !arr) return false
|
||||
if (dep === arr && !isInternational(flight)) return false
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
// ── One-way (undirected) pairs ─────────────────────────────────────────────
|
||||
|
||||
export function useUndirectedContinentPairs(
|
||||
flights: ComputedRef<Flight[]>,
|
||||
continents: ComputedRef<Continent[]>,
|
||||
) {
|
||||
const allKeys = computed<string[]>(() => {
|
||||
const names = continents.value.map(c => c.name).sort()
|
||||
const keys: string[] = []
|
||||
for (let i = 0; i < names.length; i++) {
|
||||
for (let j = i; j < names.length; j++) {
|
||||
keys.push(undirectedKey(names[i], names[j]))
|
||||
}
|
||||
}
|
||||
return keys.sort((a, b) => {
|
||||
const [a1, a2] = a.split('|')
|
||||
const [b1, b2] = b.split('|')
|
||||
return labelFor(a1, a2).localeCompare(labelFor(b1, b2))
|
||||
})
|
||||
})
|
||||
|
||||
const flightsByKey = computed<Map<string, Flight[]>>(() => {
|
||||
const map = new Map(allKeys.value.map(k => [k, [] as Flight[]]))
|
||||
for (const flight of qualifyingFlights(flights.value)) {
|
||||
const dep = continentNameOf(flight, 'departure')!
|
||||
const arr = continentNameOf(flight, 'arrival')!
|
||||
const key = undirectedKey(dep, arr)
|
||||
map.get(key)?.push(flight)
|
||||
}
|
||||
return map
|
||||
})
|
||||
|
||||
const entries = computed<ContinentPairEntry[]>(() =>
|
||||
allKeys.value.map(key => {
|
||||
const [a, b] = key.split('|')
|
||||
return { key, label: labelFor(a, b), flights: flightsByKey.value.get(key) ?? [] }
|
||||
})
|
||||
)
|
||||
|
||||
const completedCount = computed(() => entries.value.filter(e => e.flights.length > 0).length)
|
||||
const totalCount = computed(() => entries.value.length)
|
||||
|
||||
return { entries, completedCount, totalCount }
|
||||
}
|
||||
|
||||
// ── Both-ways (directed) pairs ─────────────────────────────────────────────
|
||||
|
||||
export function useDirectedContinentPairs(
|
||||
flights: ComputedRef<Flight[]>,
|
||||
continents: ComputedRef<Continent[]>,
|
||||
) {
|
||||
/** All directed keys grouped by the departure continent name */
|
||||
const keysByDeparture = computed<Map<string, string[]>>(() => {
|
||||
const names = continents.value.map(c => c.name).sort()
|
||||
const map = new Map<string, string[]>()
|
||||
for (const dep of names) {
|
||||
map.set(dep, names.map(arr => directedKey(dep, arr)).sort((a, b) => {
|
||||
const [, a2] = a.split('|')
|
||||
const [, b2] = b.split('|')
|
||||
return a2.localeCompare(b2)
|
||||
}))
|
||||
}
|
||||
return map
|
||||
})
|
||||
|
||||
const flightsByKey = computed<Map<string, Flight[]>>(() => {
|
||||
const map = new Map<string, Flight[]>()
|
||||
for (const keys of keysByDeparture.value.values()) {
|
||||
for (const key of keys) map.set(key, [])
|
||||
}
|
||||
for (const flight of qualifyingFlights(flights.value)) {
|
||||
const dep = continentNameOf(flight, 'departure')!
|
||||
const arr = continentNameOf(flight, 'arrival')!
|
||||
const key = directedKey(dep, arr)
|
||||
map.get(key)?.push(flight)
|
||||
}
|
||||
return map
|
||||
})
|
||||
|
||||
/** Entries grouped by departure continent, each sorted by arrival continent name */
|
||||
const entriesByDeparture = computed<Map<string, ContinentPairEntry[]>>(() => {
|
||||
const map = new Map<string, ContinentPairEntry[]>()
|
||||
for (const [dep, keys] of keysByDeparture.value) {
|
||||
map.set(dep, keys.map(key => {
|
||||
const [a, b] = key.split('|')
|
||||
return { key, label: `${a} → ${b}`, flights: flightsByKey.value.get(key) ?? [] }
|
||||
}))
|
||||
}
|
||||
return map
|
||||
})
|
||||
|
||||
const departureNames = computed(() => [...keysByDeparture.value.keys()])
|
||||
|
||||
const completedCount = computed(() => {
|
||||
let count = 0
|
||||
for (const entries of entriesByDeparture.value.values()) {
|
||||
count += entries.filter(e => e.flights.length > 0).length
|
||||
}
|
||||
return count
|
||||
})
|
||||
|
||||
const totalCount = computed(() => {
|
||||
let count = 0
|
||||
for (const entries of entriesByDeparture.value.values()) {
|
||||
count += entries.length
|
||||
}
|
||||
return count
|
||||
})
|
||||
|
||||
return { entriesByDeparture, departureNames, completedCount, totalCount }
|
||||
}
|
||||
@@ -33,7 +33,6 @@ function getDateParts(f: Flight): { year: number; month: number; day: number } {
|
||||
// ── Per year / month / day ────────────────────────────────────────────────────
|
||||
|
||||
export function getFlightsPerYear(flights: Flight[], upcomingFlights: Flight[]) {
|
||||
console.time('getFlightsPerYear')
|
||||
const allFlights = [...flights, ...upcomingFlights]
|
||||
|
||||
const allYears = new Set(allFlights.map(f => getDateParts(f).year))
|
||||
@@ -57,12 +56,10 @@ export function getFlightsPerYear(flights: Flight[], upcomingFlights: Flight[])
|
||||
{ name: 'Upcoming', data: countByYear(upcomingFlights) },
|
||||
],
|
||||
}
|
||||
console.timeEnd('getFlightsPerYear')
|
||||
return result
|
||||
}
|
||||
|
||||
export function getFlightsPerMonth(flights: Flight[], upcomingFlights: Flight[]) {
|
||||
console.time('getFlightsPerMonth')
|
||||
const countByMonth = (list: Flight[]) =>
|
||||
MONTHS.map((_, i) => list.filter(f => getDateParts(f).month === i).length)
|
||||
|
||||
@@ -73,12 +70,10 @@ export function getFlightsPerMonth(flights: Flight[], upcomingFlights: Flight[])
|
||||
{ name: 'Upcoming', data: countByMonth(upcomingFlights) },
|
||||
],
|
||||
}
|
||||
console.timeEnd('getFlightsPerMonth')
|
||||
return result
|
||||
}
|
||||
|
||||
export function getFlightsPerDay(flights: Flight[], upcomingFlights: Flight[]) {
|
||||
console.time('getFlightsPerDay')
|
||||
const countByDay = (list: Flight[]) =>
|
||||
DAYS.map((_, i) => list.filter(f => getDateParts(f).day === i).length)
|
||||
|
||||
@@ -89,7 +84,6 @@ export function getFlightsPerDay(flights: Flight[], upcomingFlights: Flight[]) {
|
||||
{ name: 'Upcoming', data: countByDay(upcomingFlights) },
|
||||
],
|
||||
}
|
||||
console.timeEnd('getFlightsPerDay')
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -109,23 +103,17 @@ function groupByName(flights: Flight[], accessor: (f: Flight) => string) {
|
||||
}
|
||||
|
||||
export function getFlightReasons(flights: Flight[]) {
|
||||
console.time('getFlightReasons')
|
||||
const result = groupByName(flights, f => f.flight_reason?.name ?? 'Unknown')
|
||||
console.timeEnd('getFlightReasons')
|
||||
return result
|
||||
}
|
||||
|
||||
export function getFlightClasses(flights: Flight[]) {
|
||||
console.time('getFlightClasses')
|
||||
const result = groupByName(flights, f => f.flight_class?.name ?? 'Unknown')
|
||||
console.timeEnd('getFlightClasses')
|
||||
return result
|
||||
}
|
||||
|
||||
export function getSeatTypes(flights: Flight[]) {
|
||||
console.time('getSeatTypes')
|
||||
const result = groupByName(flights, f => f.seat_type?.name ?? 'Unknown')
|
||||
console.timeEnd('getSeatTypes')
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -152,7 +140,6 @@ function countCountries(flights: Flight[]) {
|
||||
}
|
||||
|
||||
export function getCountries(flights: Flight[], upcomingFlights: Flight[]) {
|
||||
console.time('getCountries')
|
||||
const past = countCountries(flights)
|
||||
const upcoming = countCountries(upcomingFlights)
|
||||
const allNames = new Set([...past.keys(), ...upcoming.keys()])
|
||||
@@ -173,14 +160,12 @@ export function getCountries(flights: Flight[], upcomingFlights: Flight[]) {
|
||||
{ name: 'Upcoming', data: sorted.map(s => s.upcoming) },
|
||||
],
|
||||
}
|
||||
console.timeEnd('getCountries')
|
||||
return result
|
||||
}
|
||||
|
||||
// ── Continents ────────────────────────────────────────────────────────────────
|
||||
|
||||
export function getContinents(flights: Flight[]) {
|
||||
console.time('getContinents')
|
||||
const counts = new Map<string, number>()
|
||||
flights.forEach(f => {
|
||||
const continents = new Set<string>()
|
||||
@@ -195,7 +180,6 @@ export function getContinents(flights: Flight[]) {
|
||||
labels: sorted.map(([name]) => name),
|
||||
series: sorted.map(([, count]) => count),
|
||||
}
|
||||
console.timeEnd('getContinents')
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -214,7 +198,6 @@ function countAirlines(flights: Flight[]) {
|
||||
}
|
||||
|
||||
export function getTopAirlines(flights: Flight[], upcomingFlights: Flight[]) {
|
||||
console.time('getTopAirlines')
|
||||
const past = countAirlines(flights)
|
||||
const upcoming = countAirlines(upcomingFlights)
|
||||
const allNames = new Set([...past.keys(), ...upcoming.keys()])
|
||||
@@ -236,7 +219,6 @@ export function getTopAirlines(flights: Flight[], upcomingFlights: Flight[]) {
|
||||
{ name: 'Upcoming', data: sorted.map(s => s.upcoming) },
|
||||
],
|
||||
}
|
||||
console.timeEnd('getTopAirlines')
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -256,7 +238,6 @@ function airportLabel(airport: Airport | null | undefined): string | null {
|
||||
}
|
||||
|
||||
export function getTopAirports(flights: Flight[], upcomingFlights: Flight[]) {
|
||||
console.time('getTopAirports')
|
||||
const map = new Map<string, AirportItem>()
|
||||
const empty = (): AirportItem => ({ departures: 0, arrivals: 0, upcoming: 0, label: '', fullName: '' })
|
||||
|
||||
@@ -313,7 +294,6 @@ export function getTopAirports(flights: Flight[], upcomingFlights: Flight[]) {
|
||||
{ name: 'Upcoming', data: sorted.map(s => s.upcoming) },
|
||||
],
|
||||
}
|
||||
console.timeEnd('getTopAirports')
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -421,7 +401,6 @@ export function getTopRoutes(flights: Flight[], upcomingFlights: Flight[]) {
|
||||
// ── Flight types ──────────────────────────────────────────────────────────────
|
||||
|
||||
export function getFlightTypes(flights: Flight[]) {
|
||||
console.time('getFlightTypes')
|
||||
const counts = { International: 0, Domestic: 0 }
|
||||
flights.forEach(f => {
|
||||
const dep = f.departure_airport.region?.country?.id
|
||||
@@ -435,7 +414,6 @@ export function getFlightTypes(flights: Flight[]) {
|
||||
labels: sorted.map(([name]) => name),
|
||||
series: sorted.map(([, count]) => count),
|
||||
}
|
||||
console.timeEnd('getFlightTypes')
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -447,22 +425,16 @@ export function useFlightStats(flights: Ref<Flight[]>) {
|
||||
const now = new Date()
|
||||
|
||||
watch(flights, (list) => {
|
||||
console.time('dateCache warm')
|
||||
list.forEach(f => getDateParts(f))
|
||||
console.timeEnd('dateCache warm')
|
||||
}, { immediate: true })
|
||||
|
||||
const pastFlights = computed(() => {
|
||||
console.time('pastFlights')
|
||||
const result = flights.value.filter(f => new Date(f.departure_date) <= now)
|
||||
console.timeEnd('pastFlights')
|
||||
return result
|
||||
})
|
||||
|
||||
const upcomingFlights = computed(() => {
|
||||
console.time('upcomingFlights')
|
||||
const result = flights.value.filter(f => new Date(f.departure_date) > now)
|
||||
console.timeEnd('upcomingFlights')
|
||||
return result
|
||||
})
|
||||
|
||||
|
||||
@@ -3,13 +3,15 @@ import {onMounted, ref} from "vue";
|
||||
import {Flight} from "@/Types/types";
|
||||
import axios from "axios";
|
||||
|
||||
export function useFlights(url: string) {
|
||||
export function useFlights(url: string, departedOnly: boolean = false) {
|
||||
const flights = ref<Flight[]>([])
|
||||
const flightsLoading = ref(true)
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
const response = await axios.get(url)
|
||||
const response = await axios.get(url, {
|
||||
params: departedOnly ? { departed_only: true } : {}
|
||||
})
|
||||
flights.value = response.data
|
||||
} finally {
|
||||
flightsLoading.value = false
|
||||
|
||||
@@ -21,7 +21,7 @@ export function useRegionFlights(
|
||||
})
|
||||
|
||||
const flightsByRegion = computed(() => {
|
||||
return filteredFlights.value.reduce((grouped, flight) => {
|
||||
const grouped = filteredFlights.value.reduce((grouped, flight) => {
|
||||
const dep = flight.departure_airport?.region
|
||||
const arr = flight.arrival_airport?.region
|
||||
|
||||
@@ -37,6 +37,14 @@ export function useRegionFlights(
|
||||
|
||||
return grouped
|
||||
}, {} as Record<string, Flight[]>)
|
||||
|
||||
return Object.fromEntries(
|
||||
Object.entries(grouped).sort(([codeA], [codeB]) => {
|
||||
const nameA = regionNames.value[codeA] ?? codeA
|
||||
const nameB = regionNames.value[codeB] ?? codeB
|
||||
return nameA.localeCompare(nameB)
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
const regionNames = computed(() => {
|
||||
|
||||
Reference in New Issue
Block a user