mirror of
https://github.com/OpenSquawk/OpenSquawk
synced 2026-08-07 18:15:49 +08:00
feat(frequency): work out which stations are on offer and which are reachable
A session loaded one airport's frequencies once, at the start, and never revisited it: on a departure the destination's tower never appeared however close you got, and every listed station worked from any distance. Two separate questions, kept apart. Which airports are *on offer* — both of them, always, since hiding one would stop a pilot dialling ahead — with the one belonging to the current phase marked primary. And which stations are *reachable*, which is line-of-sight from height: the standard 1.23·√ft with the ground antenna's own horizon added, checked against the tests' textbook figures at 1000 ft, 10000 ft and FL350. The governing constraint is that the software has to work with no bridge connected, which is the normal case for someone practising without a simulator. Position is therefore an enhancement, never a requirement: without one, both airports are offered and nothing is marked unreachable, because a station that cannot be *proven* out of range must not be taken away. The phase decides which airport leads instead.
This commit is contained in:
98
tests/shared/stationAvailability.test.ts
Normal file
98
tests/shared/stationAvailability.test.ts
Normal file
@@ -0,0 +1,98 @@
|
||||
import { describe, it } from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
|
||||
import {
|
||||
offeredAirports,
|
||||
stationReachable,
|
||||
vhfRangeNm,
|
||||
} from '~~/shared/utils/stationAvailability'
|
||||
|
||||
describe('vhfRangeNm', () => {
|
||||
it('grows with the square root of height, not linearly', () => {
|
||||
const low = vhfRangeNm(1000)
|
||||
const high = vhfRangeNm(4000)
|
||||
// Four times the height is about twice the range, never four times.
|
||||
assert.ok(high < low * 2.2, `${high} vs ${low}`)
|
||||
assert.ok(high > low * 1.7, `${high} vs ${low}`)
|
||||
})
|
||||
|
||||
it('matches the textbook figures closely enough to trust', () => {
|
||||
// 1.23·√ft plus the ground antenna's own horizon.
|
||||
assert.ok(Math.abs(vhfRangeNm(1000) - 48) < 5, String(vhfRangeNm(1000)))
|
||||
assert.ok(Math.abs(vhfRangeNm(10_000) - 132) < 8, String(vhfRangeNm(10_000)))
|
||||
assert.ok(Math.abs(vhfRangeNm(35_000) - 239) < 12, String(vhfRangeNm(35_000)))
|
||||
})
|
||||
|
||||
it('still gives a station some range on the ground', () => {
|
||||
assert.ok(vhfRangeNm(0) > 5)
|
||||
})
|
||||
|
||||
it('treats rubbish as zero rather than NaN', () => {
|
||||
assert.ok(Number.isFinite(vhfRangeNm(NaN as unknown as number)))
|
||||
assert.ok(Number.isFinite(vhfRangeNm(-1000)))
|
||||
})
|
||||
})
|
||||
|
||||
describe('stationReachable', () => {
|
||||
it('reaches a nearby station from the ground', () => {
|
||||
assert.equal(stationReachable({ distanceNm: 5, altitudeFt: 0 }), true)
|
||||
})
|
||||
|
||||
it('does not reach a distant station from the ground', () => {
|
||||
assert.equal(stationReachable({ distanceNm: 150, altitudeFt: 0 }), false)
|
||||
})
|
||||
|
||||
it('reaches the same station once high enough', () => {
|
||||
assert.equal(stationReachable({ distanceNm: 150, altitudeFt: 35_000 }), true)
|
||||
})
|
||||
|
||||
// The no-bridge case, and the reason this defaults the way it does: a station
|
||||
// that cannot be proven out of range must stay usable, or someone flying
|
||||
// without a simulator loses their radio.
|
||||
it('treats an unknown position as reachable', () => {
|
||||
assert.equal(stationReachable({}), true)
|
||||
assert.equal(stationReachable({ altitudeFt: 3000 }), true)
|
||||
assert.equal(stationReachable({ distanceNm: 400 }), true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('offeredAirports', () => {
|
||||
const flight = { departureIcao: 'EDDF', destinationIcao: 'EDDM' }
|
||||
|
||||
it('offers both airports of the flight', () => {
|
||||
const out = offeredAirports({ ...flight, phase: 'tower' })
|
||||
assert.deepEqual(out.map(a => a.icao), ['EDDF', 'EDDM'])
|
||||
})
|
||||
|
||||
it('marks the departure primary while still on the ground there', () => {
|
||||
for (const phase of ['clearance', 'taxi', 'tower', 'departure']) {
|
||||
const out = offeredAirports({ ...flight, phase })
|
||||
assert.equal(out.find(a => a.role === 'departure')?.primary, true, phase)
|
||||
assert.equal(out.find(a => a.role === 'destination')?.primary, false, phase)
|
||||
}
|
||||
})
|
||||
|
||||
it('marks the destination primary once the arrival is being flown', () => {
|
||||
for (const phase of ['enroute', 'approach', 'arrival']) {
|
||||
const out = offeredAirports({ ...flight, phase })
|
||||
assert.equal(out.find(a => a.role === 'destination')?.primary, true, phase)
|
||||
assert.equal(out.find(a => a.role === 'departure')?.primary, false, phase)
|
||||
}
|
||||
})
|
||||
|
||||
it('falls back to the departure when the phase is unknown', () => {
|
||||
const out = offeredAirports(flight)
|
||||
assert.equal(out.find(a => a.role === 'departure')?.primary, true)
|
||||
})
|
||||
|
||||
it('does not list the same airport twice on a circuit', () => {
|
||||
const out = offeredAirports({ departureIcao: 'EDDF', destinationIcao: 'EDDF', phase: 'tower' })
|
||||
assert.deepEqual(out.map(a => a.icao), ['EDDF'])
|
||||
})
|
||||
|
||||
it('copes with only one airport known', () => {
|
||||
assert.deepEqual(offeredAirports({ departureIcao: 'EDDF' }).map(a => a.icao), ['EDDF'])
|
||||
assert.deepEqual(offeredAirports({ destinationIcao: 'EDDM' }).map(a => a.icao), ['EDDM'])
|
||||
assert.deepEqual(offeredAirports({}), [])
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user