mirror of
https://github.com/OpenSquawk/OpenSquawk
synced 2026-08-12 20:55:41 +08:00
Revise fundamentals lessons and add frequency handoff
This commit is contained in:
@@ -12,90 +12,93 @@ function gradientArt(colors: string[]): string {
|
|||||||
const fundamentalsLessons = [
|
const fundamentalsLessons = [
|
||||||
{
|
{
|
||||||
id: 'icao-alphabet',
|
id: 'icao-alphabet',
|
||||||
title: 'ICAO Alphabet & Numbers',
|
title: 'Decode ICAO Letters & Numbers',
|
||||||
desc: 'Spell letters and digits clearly',
|
desc: 'Turn phonetic blocks into identifiers',
|
||||||
keywords: ['Alphabet', 'Numbers', 'Basics'],
|
keywords: ['Alphabet', 'Numbers', 'Basics'],
|
||||||
hints: [
|
hints: [
|
||||||
'Spell each letter with its ICAO name (e.g. Delta, Lima, Hotel).',
|
'Convert the NATO letters and ATC numbers back into characters.',
|
||||||
'Use ATC numbers: tree, fower, fife, niner.'
|
'Write the identifier exactly as you would enter it into the FMS.'
|
||||||
],
|
],
|
||||||
fields: [
|
fields: [
|
||||||
{
|
{
|
||||||
key: 'letters',
|
key: 'icao-code',
|
||||||
label: 'Letters',
|
label: 'Identifier',
|
||||||
expected: scenario => scenario.callsignNato,
|
expected: scenario => scenario.phoneticCode,
|
||||||
placeholder: 'Delta Lima Hotel',
|
alternatives: scenario => {
|
||||||
width: 'xl',
|
const spelled = scenario.phoneticCodeWords
|
||||||
threshold: 0.9
|
const upper = scenario.phoneticCode.toUpperCase()
|
||||||
},
|
const lower = upper.toLowerCase()
|
||||||
{
|
const spacedUpper = upper.split('').join(' ')
|
||||||
key: 'digits',
|
const spacedLower = lower.split('').join(' ')
|
||||||
label: 'Numbers',
|
return Array.from(
|
||||||
expected: scenario => scenario.flightNumberWords,
|
new Set([
|
||||||
placeholder: 'one two three',
|
upper,
|
||||||
width: 'lg',
|
lower,
|
||||||
threshold: 0.88
|
spelled,
|
||||||
|
spelled.toLowerCase(),
|
||||||
|
spacedUpper,
|
||||||
|
spacedLower
|
||||||
|
])
|
||||||
|
)
|
||||||
|
},
|
||||||
|
placeholder: 'SWR59',
|
||||||
|
width: 'lg'
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
readback: [
|
readback: [
|
||||||
{ type: 'text', text: 'Call sign: ' },
|
{ type: 'text', text: 'Identifier ' },
|
||||||
{ type: 'field', key: 'letters', width: 'lg' },
|
{ type: 'field', key: 'icao-code', width: 'lg' }
|
||||||
{ type: 'text', text: ' · ' },
|
|
||||||
{ type: 'field', key: 'digits', width: 'md' }
|
|
||||||
],
|
],
|
||||||
defaultFrequency: 'DEL',
|
defaultFrequency: 'DEL',
|
||||||
phrase: scenario => `${scenario.callsignNato} ${scenario.flightNumberWords}`,
|
phrase: scenario => scenario.phoneticCodeWords,
|
||||||
info: scenario => [
|
info: scenario => [
|
||||||
`Callsign: ${scenario.callsign}`,
|
`Heard: ${scenario.phoneticCodeWords}`,
|
||||||
`Radio call: ${scenario.radioCall}`,
|
`Identifier: ${scenario.phoneticCode}`
|
||||||
`ICAO spelling: ${scenario.callsignNato}`,
|
|
||||||
`Flight number spoken: ${scenario.flightNumberWords}`
|
|
||||||
],
|
],
|
||||||
generate: createBaseScenario
|
generate: createBaseScenario
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'radio-call',
|
id: 'radio-call',
|
||||||
title: 'Radio Call Sign',
|
title: 'Radio Call Sign',
|
||||||
desc: 'Say the spoken and ICAO call sign',
|
desc: 'Say the spoken or ICAO call sign confidently',
|
||||||
keywords: ['Basics', 'Callsign'],
|
keywords: ['Basics', 'Callsign'],
|
||||||
hints: [
|
hints: [
|
||||||
'Use the airline telephony plus digits for the spoken version.',
|
'Use the airline telephony plus digits when talking to ATC.',
|
||||||
'ICAO format keeps the three-letter code with the numbers.'
|
'Readbacks can use either the spoken or ICAO format.'
|
||||||
],
|
],
|
||||||
fields: [
|
fields: [
|
||||||
{
|
{
|
||||||
key: 'radio-call-spoken',
|
key: 'radio-call',
|
||||||
label: 'Spoken Call Sign',
|
label: 'Call sign',
|
||||||
expected: scenario => scenario.radioCall,
|
expected: scenario => scenario.radioCall,
|
||||||
alternatives: scenario => [
|
alternatives: scenario => {
|
||||||
scenario.radioCall,
|
const variants = new Set<string>()
|
||||||
`${scenario.airlineCall} ${scenario.flightNumber}`,
|
const add = (value?: string) => {
|
||||||
`${scenario.airlineCall} ${scenario.flightNumberWords}`
|
if (!value) return
|
||||||
],
|
variants.add(value)
|
||||||
width: 'lg'
|
variants.add(value.toLowerCase())
|
||||||
},
|
}
|
||||||
{
|
add(scenario.radioCall)
|
||||||
key: 'radio-call-icao',
|
add(`${scenario.airlineCall} ${scenario.flightNumber}`)
|
||||||
label: 'ICAO Identifier',
|
add(`${scenario.airlineCall} ${scenario.flightNumberWords}`)
|
||||||
expected: scenario => scenario.callsign,
|
add(scenario.callsign)
|
||||||
alternatives: scenario => [
|
add(`${scenario.airlineCode} ${scenario.flightNumber}`)
|
||||||
scenario.callsign,
|
add(`${scenario.callsignNato} ${scenario.flightNumberWords}`)
|
||||||
`${scenario.airlineCode}${scenario.flightNumber}`
|
return Array.from(variants)
|
||||||
],
|
},
|
||||||
width: 'md'
|
width: 'xl'
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
readback: [
|
readback: [
|
||||||
{ type: 'text', text: 'Call sign ' },
|
{ type: 'text', text: 'Call sign ' },
|
||||||
{ type: 'field', key: 'radio-call-spoken', width: 'lg' },
|
{ type: 'field', key: 'radio-call', width: 'xl' }
|
||||||
{ type: 'text', text: ' · ICAO ' },
|
|
||||||
{ type: 'field', key: 'radio-call-icao', width: 'md' }
|
|
||||||
],
|
],
|
||||||
defaultFrequency: 'DEL',
|
defaultFrequency: 'DEL',
|
||||||
phrase: scenario => `${scenario.radioCall}`,
|
phrase: scenario => `${scenario.radioCall}`,
|
||||||
info: scenario => [
|
info: scenario => [
|
||||||
`Spoken call sign: ${scenario.radioCall}`,
|
`Spoken: ${scenario.radioCall}`,
|
||||||
`ICAO identifier: ${scenario.callsign}`
|
`ICAO: ${scenario.callsign}`,
|
||||||
|
`Phonetic: ${scenario.callsignNato} ${scenario.flightNumberWords}`
|
||||||
],
|
],
|
||||||
generate: createBaseScenario
|
generate: createBaseScenario
|
||||||
},
|
},
|
||||||
@@ -105,8 +108,8 @@ const fundamentalsLessons = [
|
|||||||
desc: 'Extract the identifier and key data from the ATIS',
|
desc: 'Extract the identifier and key data from the ATIS',
|
||||||
keywords: ['ATIS', 'Weather'],
|
keywords: ['ATIS', 'Weather'],
|
||||||
hints: [
|
hints: [
|
||||||
'Remember the ATIS identifier as a single letter.',
|
'Remember the ATIS identifier as its NATO word (e.g. Information Yankee).',
|
||||||
'Order: runway – wind – visibility – temperature – dew point – QNH.'
|
'Order: runway – wind – visibility – temperature – dew point – QNH; copy the numbers as digits.'
|
||||||
],
|
],
|
||||||
fields: [
|
fields: [
|
||||||
{
|
{
|
||||||
@@ -116,7 +119,12 @@ const fundamentalsLessons = [
|
|||||||
alternatives: scenario => [
|
alternatives: scenario => [
|
||||||
scenario.atisCode,
|
scenario.atisCode,
|
||||||
scenario.atisCode.toLowerCase(),
|
scenario.atisCode.toLowerCase(),
|
||||||
`Information ${scenario.atisCode}`
|
scenario.atisCodeWord,
|
||||||
|
scenario.atisCodeWord.toLowerCase(),
|
||||||
|
`Information ${scenario.atisCode}`,
|
||||||
|
`information ${scenario.atisCode.toLowerCase()}`,
|
||||||
|
`Information ${scenario.atisCodeWord}`,
|
||||||
|
`information ${scenario.atisCodeWord.toLowerCase()}`
|
||||||
],
|
],
|
||||||
placeholder: 'Letter',
|
placeholder: 'Letter',
|
||||||
width: 'xs',
|
width: 'xs',
|
||||||
@@ -143,22 +151,93 @@ const fundamentalsLessons = [
|
|||||||
{
|
{
|
||||||
key: 'atis-visibility',
|
key: 'atis-visibility',
|
||||||
label: 'Visibility',
|
label: 'Visibility',
|
||||||
expected: scenario => scenario.atisSummary.visibility,
|
expected: scenario => scenario.visibility,
|
||||||
alternatives: scenario => [scenario.visibility, scenario.visibilityWords],
|
alternatives: scenario => {
|
||||||
|
const values = new Set<string>()
|
||||||
|
const add = (value?: string) => {
|
||||||
|
if (!value) return
|
||||||
|
values.add(value)
|
||||||
|
values.add(value.toLowerCase())
|
||||||
|
}
|
||||||
|
add(scenario.visibility)
|
||||||
|
add(scenario.atisSummary.visibility)
|
||||||
|
add(scenario.visibilityWords)
|
||||||
|
const numeric = Number(scenario.visibility)
|
||||||
|
if (!Number.isNaN(numeric)) {
|
||||||
|
if (numeric >= 1000) {
|
||||||
|
const precise = (numeric / 1000).toFixed(1).replace(/\.0$/, '')
|
||||||
|
add(precise)
|
||||||
|
add(`${precise}km`)
|
||||||
|
add(`${precise} km`)
|
||||||
|
}
|
||||||
|
if (numeric >= 1000) {
|
||||||
|
const rounded = Math.round(numeric / 1000)
|
||||||
|
add(rounded.toString())
|
||||||
|
add(`${rounded}km`)
|
||||||
|
add(`${rounded} km`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (scenario.visibility === '9999') {
|
||||||
|
add('10')
|
||||||
|
add('10km')
|
||||||
|
add('10 km')
|
||||||
|
}
|
||||||
|
return Array.from(values)
|
||||||
|
},
|
||||||
width: 'sm'
|
width: 'sm'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'atis-temp',
|
key: 'atis-temp',
|
||||||
label: 'Temperature',
|
label: 'Temperature',
|
||||||
expected: scenario => scenario.atisSummary.temperature,
|
expected: scenario => scenario.temperature.toString(),
|
||||||
alternatives: scenario => [scenario.temperatureWords],
|
alternatives: scenario => {
|
||||||
|
const value = scenario.temperature
|
||||||
|
const base = value.toString()
|
||||||
|
const plus = value > 0 ? `+${base}` : base
|
||||||
|
const c = `${base}°C`
|
||||||
|
const cSpaced = `${base} °C`
|
||||||
|
const shortC = value > 0 ? `${plus}C` : `${base}C`
|
||||||
|
const options = new Set<string>([
|
||||||
|
base,
|
||||||
|
plus,
|
||||||
|
c,
|
||||||
|
cSpaced,
|
||||||
|
shortC,
|
||||||
|
scenario.temperatureWords,
|
||||||
|
formatTemp(value)
|
||||||
|
])
|
||||||
|
options.add(`${plus}°C`)
|
||||||
|
options.add(`${plus} °C`)
|
||||||
|
options.add(scenario.temperatureWords.toLowerCase())
|
||||||
|
return Array.from(options)
|
||||||
|
},
|
||||||
width: 'sm'
|
width: 'sm'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'atis-dew',
|
key: 'atis-dew',
|
||||||
label: 'Dew point',
|
label: 'Dew point',
|
||||||
expected: scenario => scenario.atisSummary.dewpoint,
|
expected: scenario => scenario.dewpoint.toString(),
|
||||||
alternatives: scenario => [scenario.dewpointWords],
|
alternatives: scenario => {
|
||||||
|
const value = scenario.dewpoint
|
||||||
|
const base = value.toString()
|
||||||
|
const plus = value > 0 ? `+${base}` : base
|
||||||
|
const c = `${base}°C`
|
||||||
|
const cSpaced = `${base} °C`
|
||||||
|
const shortC = value > 0 ? `${plus}C` : `${base}C`
|
||||||
|
const options = new Set<string>([
|
||||||
|
base,
|
||||||
|
plus,
|
||||||
|
c,
|
||||||
|
cSpaced,
|
||||||
|
shortC,
|
||||||
|
scenario.dewpointWords,
|
||||||
|
formatTemp(value)
|
||||||
|
])
|
||||||
|
options.add(`${plus}°C`)
|
||||||
|
options.add(`${plus} °C`)
|
||||||
|
options.add(scenario.dewpointWords.toLowerCase())
|
||||||
|
return Array.from(options)
|
||||||
|
},
|
||||||
width: 'sm'
|
width: 'sm'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -188,8 +267,8 @@ const fundamentalsLessons = [
|
|||||||
defaultFrequency: 'ATIS',
|
defaultFrequency: 'ATIS',
|
||||||
phrase: scenario => scenario.atisText,
|
phrase: scenario => scenario.atisText,
|
||||||
info: () => [
|
info: () => [
|
||||||
'Note the identifier, runway, wind, visibility, temperature, dew point, and QNH.',
|
'Note the identifier (NATO word), runway, wind, visibility, temperature, dew point, and QNH.',
|
||||||
'Visibility: four digits or 9999 for ≥10 km; QNH as a four-digit value.'
|
'Temperatures and dew point can be recorded as plain numbers; visibility may be metres or kilometres.'
|
||||||
],
|
],
|
||||||
generate: createBaseScenario
|
generate: createBaseScenario
|
||||||
},
|
},
|
||||||
@@ -261,22 +340,32 @@ const fundamentalsLessons = [
|
|||||||
{
|
{
|
||||||
id: 'radio-check',
|
id: 'radio-check',
|
||||||
title: 'Radio Check',
|
title: 'Radio Check',
|
||||||
desc: 'Report readability',
|
desc: 'Acknowledge a requested radio check',
|
||||||
keywords: ['Ground', 'Comms'],
|
keywords: ['Ground', 'Comms'],
|
||||||
hints: [
|
hints: [
|
||||||
'Reply with "Readability" followed by the number.',
|
'Reply with "Readability" plus the number you hear.',
|
||||||
'Always finish the check with your call sign.'
|
'Finish with your call sign; no need to repeat the frequency.'
|
||||||
],
|
],
|
||||||
fields: [
|
fields: [
|
||||||
{
|
{
|
||||||
key: 'rc-callsign',
|
key: 'rc-callsign',
|
||||||
label: 'Callsign',
|
label: 'Callsign',
|
||||||
expected: scenario => scenario.radioCall,
|
expected: scenario => scenario.radioCall,
|
||||||
alternatives: scenario => [
|
alternatives: scenario => {
|
||||||
scenario.radioCall,
|
const variants = new Set<string>()
|
||||||
`${scenario.airlineCall} ${scenario.flightNumber}`,
|
const add = (value?: string) => {
|
||||||
scenario.callsign
|
if (!value) return
|
||||||
],
|
variants.add(value)
|
||||||
|
variants.add(value.toLowerCase())
|
||||||
|
}
|
||||||
|
add(scenario.radioCall)
|
||||||
|
add(`${scenario.airlineCall} ${scenario.flightNumber}`)
|
||||||
|
add(`${scenario.airlineCall} ${scenario.flightNumberWords}`)
|
||||||
|
add(scenario.callsign)
|
||||||
|
add(`${scenario.airlineCode} ${scenario.flightNumber}`)
|
||||||
|
add(`${scenario.callsignNato} ${scenario.flightNumberWords}`)
|
||||||
|
return Array.from(variants)
|
||||||
|
},
|
||||||
placeholder: 'Lufthansa one two three',
|
placeholder: 'Lufthansa one two three',
|
||||||
width: 'lg'
|
width: 'lg'
|
||||||
},
|
},
|
||||||
@@ -284,22 +373,106 @@ const fundamentalsLessons = [
|
|||||||
key: 'rc-readability',
|
key: 'rc-readability',
|
||||||
label: 'Readability',
|
label: 'Readability',
|
||||||
expected: scenario => scenario.readabilityWord,
|
expected: scenario => scenario.readabilityWord,
|
||||||
alternatives: scenario => [scenario.readability.toString(), scenario.readabilityWord],
|
alternatives: scenario => [
|
||||||
|
scenario.readability.toString(),
|
||||||
|
scenario.readabilityWord,
|
||||||
|
scenario.readabilityWord.toLowerCase()
|
||||||
|
],
|
||||||
placeholder: 'five',
|
placeholder: 'five',
|
||||||
width: 'sm'
|
width: 'sm'
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
readback: [
|
readback: [
|
||||||
{ type: 'text', text: 'This is ' },
|
{ type: 'text', text: 'Readability ' },
|
||||||
{ type: 'field', key: 'rc-callsign', width: 'lg' },
|
{ type: 'field', key: 'rc-readability', width: 'sm' },
|
||||||
{ type: 'text', text: ', readability ' },
|
{ type: 'text', text: ', ' },
|
||||||
{ type: 'field', key: 'rc-readability', width: 'sm' }
|
{ type: 'field', key: 'rc-callsign', width: 'lg' }
|
||||||
],
|
],
|
||||||
defaultFrequency: 'GND',
|
defaultFrequency: 'GND',
|
||||||
phrase: scenario => `${scenario.airport.name} Ground, ${scenario.radioCall}, radio check on ${scenario.groundFreq}.`,
|
phrase: scenario => `${scenario.radioCall}, ${scenario.airport.name} Ground, say radio check on ${scenario.groundFreq}.`,
|
||||||
info: scenario => [
|
info: scenario => [
|
||||||
`Frequency: ${scenario.groundFreq} (${scenario.frequencyWords.GND})`,
|
`Request: ${scenario.airport.name} Ground on ${scenario.groundFreq} (${scenario.frequencyWords.GND})`,
|
||||||
`Expected response: Readability ${scenario.readability} (${scenario.readabilityWord})`
|
`Respond: Readability ${scenario.readability} (${scenario.readabilityWord}) with your call sign`
|
||||||
|
],
|
||||||
|
generate: createBaseScenario
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'frequency-change',
|
||||||
|
title: 'Frequency Change Readback',
|
||||||
|
desc: 'Confirm a handoff instruction',
|
||||||
|
keywords: ['Comms', 'Frequency'],
|
||||||
|
hints: [
|
||||||
|
'Repeat the station and frequency before your call sign.',
|
||||||
|
'State the frequency as digits or with "decimal".'
|
||||||
|
],
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
key: 'freq-contact-frequency',
|
||||||
|
label: 'Frequency',
|
||||||
|
expected: scenario => scenario.handoff.frequency,
|
||||||
|
alternatives: scenario => {
|
||||||
|
const freq = scenario.handoff.frequency
|
||||||
|
const trimmed = freq.replace(/\.?0+$/, '')
|
||||||
|
const comma = freq.replace('.', ',')
|
||||||
|
const trimmedComma = trimmed.replace('.', ',')
|
||||||
|
const spaced = freq.replace('.', ' ')
|
||||||
|
const trimmedSpaced = trimmed.replace('.', ' ')
|
||||||
|
const words = scenario.handoff.frequencyWords
|
||||||
|
const pointWords = words.replace('decimal', 'point')
|
||||||
|
const compact = freq.replace('.', '')
|
||||||
|
const trimmedCompact = trimmed.replace('.', '')
|
||||||
|
const values = new Set<string>([
|
||||||
|
freq,
|
||||||
|
trimmed,
|
||||||
|
comma,
|
||||||
|
trimmedComma,
|
||||||
|
spaced,
|
||||||
|
trimmedSpaced,
|
||||||
|
words,
|
||||||
|
words.toLowerCase(),
|
||||||
|
pointWords,
|
||||||
|
pointWords.toLowerCase(),
|
||||||
|
compact,
|
||||||
|
trimmedCompact
|
||||||
|
])
|
||||||
|
return Array.from(values)
|
||||||
|
},
|
||||||
|
placeholder: '125.35',
|
||||||
|
width: 'md'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'freq-contact-callsign',
|
||||||
|
label: 'Callsign',
|
||||||
|
expected: scenario => scenario.radioCall,
|
||||||
|
alternatives: scenario => {
|
||||||
|
const variants = new Set<string>()
|
||||||
|
const add = (value?: string) => {
|
||||||
|
if (!value) return
|
||||||
|
variants.add(value)
|
||||||
|
variants.add(value.toLowerCase())
|
||||||
|
}
|
||||||
|
add(scenario.radioCall)
|
||||||
|
add(`${scenario.airlineCall} ${scenario.flightNumber}`)
|
||||||
|
add(`${scenario.airlineCall} ${scenario.flightNumberWords}`)
|
||||||
|
add(scenario.callsign)
|
||||||
|
add(`${scenario.airlineCode} ${scenario.flightNumber}`)
|
||||||
|
add(`${scenario.callsignNato} ${scenario.flightNumberWords}`)
|
||||||
|
return Array.from(variants)
|
||||||
|
},
|
||||||
|
width: 'lg'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
readback: [
|
||||||
|
{ type: 'text', text: scenario => `Contact ${scenario.handoff.facility} on ` },
|
||||||
|
{ type: 'field', key: 'freq-contact-frequency', width: 'md' },
|
||||||
|
{ type: 'text', text: ', ' },
|
||||||
|
{ type: 'field', key: 'freq-contact-callsign', width: 'lg' }
|
||||||
|
],
|
||||||
|
defaultFrequency: 'TWR',
|
||||||
|
phrase: scenario => `${scenario.radioCall}, contact ${scenario.handoff.facility} on ${scenario.handoff.frequencyWords}.`,
|
||||||
|
info: scenario => [
|
||||||
|
`Station: ${scenario.handoff.facility}`,
|
||||||
|
`Frequency: ${scenario.handoff.frequency} (${scenario.handoff.frequencyWords})`
|
||||||
],
|
],
|
||||||
generate: createBaseScenario
|
generate: createBaseScenario
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -190,6 +190,30 @@ function randomFlightNumber(): string {
|
|||||||
return value
|
return value
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const phoneticCodeCharacters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'.split('')
|
||||||
|
|
||||||
|
function randomPhoneticCode(length = 5): string {
|
||||||
|
let value = ''
|
||||||
|
for (let i = 0; i < length; i++) {
|
||||||
|
value += choice(phoneticCodeCharacters)
|
||||||
|
}
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
function codeToPhonetic(value: string): string {
|
||||||
|
return value
|
||||||
|
.toUpperCase()
|
||||||
|
.split('')
|
||||||
|
.map(char => {
|
||||||
|
if (natoMap[char]) return natoMap[char]
|
||||||
|
if (atcNumberWords[char]) return atcNumberWords[char]
|
||||||
|
return char
|
||||||
|
})
|
||||||
|
.join(' ')
|
||||||
|
.replace(/\s+/g, ' ')
|
||||||
|
.trim()
|
||||||
|
}
|
||||||
|
|
||||||
function generateSquawk(): string {
|
function generateSquawk(): string {
|
||||||
let code = ''
|
let code = ''
|
||||||
for (let i = 0; i < 4; i++) {
|
for (let i = 0; i < 4; i++) {
|
||||||
@@ -301,6 +325,8 @@ export function createBaseScenario(): Scenario {
|
|||||||
const airline = choice(airlines)
|
const airline = choice(airlines)
|
||||||
const flightNumber = randomFlightNumber()
|
const flightNumber = randomFlightNumber()
|
||||||
const callsign = `${airline.code}${flightNumber}`
|
const callsign = `${airline.code}${flightNumber}`
|
||||||
|
const phoneticCode = randomPhoneticCode()
|
||||||
|
const phoneticCodeWords = codeToPhonetic(phoneticCode)
|
||||||
const runway = choice(airport.runways)
|
const runway = choice(airport.runways)
|
||||||
const stand = choice(airport.stands)
|
const stand = choice(airport.stands)
|
||||||
const taxiRoute = choice(airport.taxi)
|
const taxiRoute = choice(airport.taxi)
|
||||||
@@ -325,6 +351,7 @@ export function createBaseScenario(): Scenario {
|
|||||||
const temperature = randInt(-3, 28)
|
const temperature = randInt(-3, 28)
|
||||||
const dewpoint = Math.max(temperature - randInt(2, 6), -10)
|
const dewpoint = Math.max(temperature - randInt(2, 6), -10)
|
||||||
const atisCode = choice(atisLetters)
|
const atisCode = choice(atisLetters)
|
||||||
|
const atisCodeWord = lettersToNato(atisCode)
|
||||||
const remarks = choice(['NOSIG', 'BECMG 4000', 'TEMPO -SHRA'])
|
const remarks = choice(['NOSIG', 'BECMG 4000', 'TEMPO -SHRA'])
|
||||||
const arrivalQnh = randInt(980, 1035)
|
const arrivalQnh = randInt(980, 1035)
|
||||||
const arrivalWindDirection = randInt(0, 35) * 10
|
const arrivalWindDirection = randInt(0, 35) * 10
|
||||||
@@ -384,9 +411,15 @@ export function createBaseScenario(): Scenario {
|
|||||||
return acc
|
return acc
|
||||||
}, {} as Record<FrequencyType, string>)
|
}, {} as Record<FrequencyType, string>)
|
||||||
|
|
||||||
|
const contactFrequencies = frequencies.filter(freq => freq.type !== 'ATIS')
|
||||||
|
const handoffTarget = choice(contactFrequencies)
|
||||||
|
const handoffCity = handoffTarget.type === 'CTR' ? destination.city : airport.city
|
||||||
|
const handoffFacility = `${handoffCity} ${handoffTarget.label}`.trim()
|
||||||
|
const handoffFrequencyWords = frequencyWords[handoffTarget.type]
|
||||||
|
|
||||||
const readability = choice(readabilityScale)
|
const readability = choice(readabilityScale)
|
||||||
|
|
||||||
const atisText = `${airport.name} information ${atisCode}, time ${timestamp.slice(2, 4)}${timestamp.slice(4, 6)}, runway ${
|
const atisText = `${airport.name} information ${atisCodeWord}, time ${timestamp.slice(2, 4)}${timestamp.slice(4, 6)}, runway ${
|
||||||
runwayToWords(runway)
|
runwayToWords(runway)
|
||||||
} in use, wind ${windToWords(windDirection, windSpeed)}, visibility ${visibilityToWords(visibility)}, temperature ${
|
} in use, wind ${windToWords(windDirection, windSpeed)}, visibility ${visibilityToWords(visibility)}, temperature ${
|
||||||
temperatureToWords(temperature)
|
temperatureToWords(temperature)
|
||||||
@@ -397,6 +430,8 @@ export function createBaseScenario(): Scenario {
|
|||||||
airlineCode: airline.code,
|
airlineCode: airline.code,
|
||||||
airlineCall: airline.call,
|
airlineCall: airline.call,
|
||||||
radioCall: `${airline.call} ${digitsToWords(flightNumber)}`,
|
radioCall: `${airline.call} ${digitsToWords(flightNumber)}`,
|
||||||
|
phoneticCode,
|
||||||
|
phoneticCodeWords,
|
||||||
callsignNato: lettersToNato(airline.code),
|
callsignNato: lettersToNato(airline.code),
|
||||||
flightNumber,
|
flightNumber,
|
||||||
flightNumberWords: digitsToWords(flightNumber),
|
flightNumberWords: digitsToWords(flightNumber),
|
||||||
@@ -430,6 +465,7 @@ export function createBaseScenario(): Scenario {
|
|||||||
qnh,
|
qnh,
|
||||||
qnhWords: qnhToWords(qnh),
|
qnhWords: qnhToWords(qnh),
|
||||||
atisCode,
|
atisCode,
|
||||||
|
atisCodeWord,
|
||||||
atisText,
|
atisText,
|
||||||
atisSummary: {
|
atisSummary: {
|
||||||
runway,
|
runway,
|
||||||
@@ -470,6 +506,13 @@ export function createBaseScenario(): Scenario {
|
|||||||
readabilityPhrase: `Readability ${readability.word}`,
|
readabilityPhrase: `Readability ${readability.word}`,
|
||||||
frequencies,
|
frequencies,
|
||||||
frequencyWords,
|
frequencyWords,
|
||||||
|
handoff: {
|
||||||
|
type: handoffTarget.type,
|
||||||
|
facility: handoffFacility,
|
||||||
|
short: handoffTarget.label,
|
||||||
|
frequency: handoffTarget.value,
|
||||||
|
frequencyWords: handoffFrequencyWords
|
||||||
|
},
|
||||||
atisFreq: airport.freqs.atis,
|
atisFreq: airport.freqs.atis,
|
||||||
deliveryFreq: airport.freqs.delivery,
|
deliveryFreq: airport.freqs.delivery,
|
||||||
groundFreq: airport.freqs.ground,
|
groundFreq: airport.freqs.ground,
|
||||||
|
|||||||
@@ -46,6 +46,8 @@ export type Scenario = {
|
|||||||
airlineCode: string
|
airlineCode: string
|
||||||
airlineCall: string
|
airlineCall: string
|
||||||
radioCall: string
|
radioCall: string
|
||||||
|
phoneticCode: string
|
||||||
|
phoneticCodeWords: string
|
||||||
callsignNato: string
|
callsignNato: string
|
||||||
flightNumber: string
|
flightNumber: string
|
||||||
flightNumberWords: string
|
flightNumberWords: string
|
||||||
@@ -79,6 +81,7 @@ export type Scenario = {
|
|||||||
qnh: number
|
qnh: number
|
||||||
qnhWords: string
|
qnhWords: string
|
||||||
atisCode: string
|
atisCode: string
|
||||||
|
atisCodeWord: string
|
||||||
atisText: string
|
atisText: string
|
||||||
atisSummary: {
|
atisSummary: {
|
||||||
runway: string
|
runway: string
|
||||||
@@ -119,6 +122,13 @@ export type Scenario = {
|
|||||||
readabilityPhrase: string
|
readabilityPhrase: string
|
||||||
frequencies: Frequency[]
|
frequencies: Frequency[]
|
||||||
frequencyWords: Record<FrequencyType, string>
|
frequencyWords: Record<FrequencyType, string>
|
||||||
|
handoff: {
|
||||||
|
type: FrequencyType
|
||||||
|
facility: string
|
||||||
|
short: string
|
||||||
|
frequency: string
|
||||||
|
frequencyWords: string
|
||||||
|
}
|
||||||
atisFreq: string
|
atisFreq: string
|
||||||
deliveryFreq: string
|
deliveryFreq: string
|
||||||
groundFreq: string
|
groundFreq: string
|
||||||
|
|||||||
Reference in New Issue
Block a user