From 9ba4b0fe16ee3d81722eedcd32e9696f63437516 Mon Sep 17 00:00:00 2001 From: Michael Date: Mon, 19 Jan 2026 22:03:12 -0500 Subject: [PATCH 1/6] scenario: specify specific callsigns --- aviation/aviation.go | 212 +++++++++++++++++++++++++++++----------- sim/spawn_arrivals.go | 85 +++++++++++++--- sim/spawn_departures.go | 22 ++++- 3 files changed, 247 insertions(+), 72 deletions(-) diff --git a/aviation/aviation.go b/aviation/aviation.go index 7fe56d0b9..228b31de4 100644 --- a/aviation/aviation.go +++ b/aviation/aviation.go @@ -127,6 +127,7 @@ type Arrival struct { type AirlineSpecifier struct { ICAO string `json:"icao"` + Callsign string `json:"callsign,omitempty"` Fleet string `json:"fleet,omitempty"` AircraftTypes []string `json:"types,omitempty"` } @@ -159,22 +160,64 @@ func (a AirlineSpecifier) Aircraft() []FleetAircraft { } } +func CallsignClashesWithExisting(currentCallsigns []ADSBCallsign, proposed string, uniqueSuffix bool) bool { + if uniqueSuffix { + // Reject if the last 2 characters of callsign match an existing callsign. + suffixMatches := func(cs ADSBCallsign) bool { + return len(proposed) >= 2 && strings.HasSuffix(string(cs), proposed[len(proposed)-2:]) + } + return slices.ContainsFunc(currentCallsigns, suffixMatches) + } + // Reject only if there's an exact match + return slices.Contains(currentCallsigns, ADSBCallsign(proposed)) +} + func (a *AirlineSpecifier) Check(e *util.ErrorLogger) { defer e.CheckDepth(e.CurrentDepth()) e.Push("Airline " + a.ICAO) defer e.Pop() - al, ok := DB.Airlines[strings.ToUpper(a.ICAO)] - if !ok { - e.ErrorString("airline not known") - return + a.ICAO = strings.ToUpper(strings.TrimSpace(a.ICAO)) + a.Callsign = strings.ToUpper(strings.TrimSpace(a.Callsign)) + + if a.Callsign != "" { + for _, ch := range a.Callsign { + if (ch < 'A' || ch > 'Z') && (ch < '0' || ch > '9') { + e.ErrorString("callsign has invalid character %q", ch) + break + } + } + } + + if a.ICAO == "" && a.Callsign != "" { + if icao := icaoFromCallsign(a.Callsign); icao != "" { + a.ICAO = icao + } + } + + var al Airline + if a.ICAO != "" { + var ok bool + al, ok = DB.Airlines[a.ICAO] + if !ok { + e.ErrorString("airline not known") + return + } } if a.Fleet == "" && len(a.AircraftTypes) == 0 { + if a.ICAO == "" { + e.ErrorString("must specify \"types\" when no \"icao\" is provided") + return + } a.Fleet = "default" } if a.Fleet != "" { + if a.ICAO == "" { + e.ErrorString("must specify \"icao\" when \"fleet\" is set") + return + } if len(a.AircraftTypes) != 0 { e.ErrorString("cannot specify both \"fleet\" and \"types\"") return @@ -203,6 +246,75 @@ func (a *AirlineSpecifier) Check(e *util.ErrorLogger) { } } +func (a AirlineSpecifier) sampleAcType(r *rand.Rand, departureAirport, arrivalAirport string, lg *log.Logger) string { + if a.ICAO == "" { + if len(a.AircraftTypes) == 0 { + lg.Errorf("No aircraft types available for callsign %q", a.Callsign) + return "" + } + actype := rand.SampleSlice(r, a.AircraftTypes) + if _, ok := DB.AircraftPerformance[actype]; !ok { + lg.Errorf("Aircraft %q not found in performance database for callsign %q", actype, a.Callsign) + return "" + } + return actype + } + if _, ok := DB.Airlines[strings.ToUpper(a.ICAO)]; !ok { + // TODO: this should be caught at load validation time... + lg.Errorf("Airline %q not found in database", a.ICAO) + return "" + } + + // Calculate flight distance to filter aircraft by CWT category + dep, arr := DB.Airports[departureAirport], DB.Airports[arrivalAirport] + flightDistance := math.NMDistance2LL(dep.Location, arr.Location) + + // Sample according to fleet count, filtering by maximum distance for CWT category + var actype string + acCount := 0 + for _, ac := range a.Aircraft() { + // Filter based on flight distance and aircraft CWT category + if flightDistance > 0 && !slices.Contains(extraLongRange, ac.ICAO) { + if perf, ok := DB.AircraftPerformance[ac.ICAO]; ok { + if maxRange, ok := cwtMaxRanges[perf.Category.CWT]; ok { + // Check if flight distance exceeds category maximum (0 means no limit) + if maxRange > 0 && flightDistance > maxRange { + continue + } + } + } + } + + // Reservoir sampling... + acCount += ac.Count + if r.Float32() < float32(ac.Count)/float32(acCount) { + actype = ac.ICAO + } + } + if actype == "" { + // Try again without considering range. + for _, ac := range a.Aircraft() { + acCount += ac.Count + if r.Float32() < float32(ac.Count)/float32(acCount) { + actype = ac.ICAO + } + } + } + if actype != "" { + if _, ok := DB.AircraftPerformance[actype]; !ok { + // TODO: validation stage... + lg.Errorf("Aircraft %q not found in performance database for airline %+v", + actype, a) + return "" + } + } + return actype +} + +func (a AirlineSpecifier) SampleAcType(r *rand.Rand, departureAirport, arrivalAirport string, lg *log.Logger) string { + return a.sampleAcType(r, departureAirport, arrivalAirport, lg) +} + var badCallsigns map[string]any = map[string]any{ // 9/11 "AAL11": nil, @@ -262,68 +374,30 @@ var extraLongRange = []string{"A35K", "A359"} // currentCallsigns will be empty if we don't care about unique suffixes. func (a AirlineSpecifier) SampleAcTypeAndCallsign(r *rand.Rand, currentCallsigns []ADSBCallsign, uniqueSuffix bool, departureAirport, arrivalAirport string, lg *log.Logger) (actype, callsign string) { - dbAirline, ok := DB.Airlines[strings.ToUpper(a.ICAO)] - if !ok { - // TODO: this should be caught at load validation time... - lg.Errorf("Airline %q not found in database", a.ICAO) + actype = a.sampleAcType(r, departureAirport, arrivalAirport, lg) + if actype == "" { return "", "" } - // Calculate flight distance to filter aircraft by CWT category - dep, arr := DB.Airports[departureAirport], DB.Airports[arrivalAirport] - flightDistance := math.NMDistance2LL(dep.Location, arr.Location) - - // Sample according to fleet count, filtering by maximum distance for CWT category - acCount := 0 - for _, ac := range a.Aircraft() { - // Filter based on flight distance and aircraft CWT category - if flightDistance > 0 && !slices.Contains(extraLongRange, ac.ICAO) { - if perf, ok := DB.AircraftPerformance[ac.ICAO]; ok { - if maxRange, ok := cwtMaxRanges[perf.Category.CWT]; ok { - // Check if flight distance exceeds category maximum (0 means no limit) - if maxRange > 0 && flightDistance > maxRange { - continue - } - } - } + if a.Callsign != "" { + callsign = strings.ToUpper(strings.TrimSpace(a.Callsign)) + if callsign == "" { + return "", "" } - - // Reservoir sampling... - acCount += ac.Count - if r.Float32() < float32(ac.Count)/float32(acCount) { - actype = ac.ICAO + if _, ok := badCallsigns[callsign]; ok { + return "", "" } - } - if actype == "" { - // Try again without considering range. - for _, ac := range a.Aircraft() { - acCount += ac.Count - if r.Float32() < float32(ac.Count)/float32(acCount) { - actype = ac.ICAO - } + if CallsignClashesWithExisting(currentCallsigns, callsign, uniqueSuffix) { + return "", "" } + return actype, callsign } - if _, ok := DB.AircraftPerformance[actype]; !ok { - // TODO: validation stage... - lg.Errorf("Aircraft %q not found in performance database for airline %+v", - actype, a) + dbAirline, ok := DB.Airlines[strings.ToUpper(a.ICAO)] + if !ok { return "", "" } - callsignClashesWithExisting := func(proposed string) bool { - if uniqueSuffix { - // Reject if the last 2 characters of callsign match an existing callsign. - suffixMatches := func(cs ADSBCallsign) bool { - return strings.HasSuffix(string(cs), proposed[len(proposed)-2:]) - } - return slices.ContainsFunc(currentCallsigns, suffixMatches) - } else { - // Reject only if there's an exact match - return slices.Contains(currentCallsigns, ADSBCallsign(proposed)) - } - } - // random callsign var cs strings.Builder for range 100 { @@ -367,7 +441,7 @@ func (a AirlineSpecifier) SampleAcTypeAndCallsign(r *rand.Rand, currentCallsigns } else if slices.Contains(currentCallsigns, ADSBCallsign(cs.String())) { cs.Reset() continue - } else if callsignClashesWithExisting(cs.String()) { + } else if CallsignClashesWithExisting(currentCallsigns, cs.String(), uniqueSuffix) { cs.Reset() continue } @@ -377,6 +451,19 @@ func (a AirlineSpecifier) SampleAcTypeAndCallsign(r *rand.Rand, currentCallsigns return "", "" } +func icaoFromCallsign(callsign string) string { + if len(callsign) < 3 { + return "" + } + for i := 0; i < 3; i++ { + ch := callsign[i] + if ch < 'A' || ch > 'Z' { + return "" + } + } + return callsign[:3] +} + type Runway struct { Id string Heading float32 @@ -733,6 +820,11 @@ func (ar *Arrival) PostDeserialize(loc Locator, nmPerLongitude float32, magnetic } } + if len(ar.Airlines) == 0 { + e.ErrorString("no \"airlines\" specified for arrivals") + return + } + for icao := range ar.Airlines { airport, ok := DB.Airports[icao] if !ok { @@ -886,6 +978,10 @@ func (ar *Arrival) PostDeserialize(loc Locator, nmPerLongitude float32, magnetic approachAssigned := ar.ExpectApproach.A != nil || ar.ExpectApproach.B != nil ar.Waypoints.CheckArrival(e, controlPositions, approachAssigned, checkScratchpad) + if len(ar.Airlines) == 0 { + e.ErrorString("no \"airlines\" specified for arrivals") + } + for arrivalAirport := range ar.Airlines { e.Push("Arrival airport " + arrivalAirport) if len(ar.Airlines[arrivalAirport]) == 0 { @@ -930,7 +1026,9 @@ func (ar *Arrival) PostDeserialize(loc Locator, nmPerLongitude float32, magnetic "airport %q is listed in \"expect_approach\" but is not in arrival airports", airport, ) - } else if ap, ok := airports[airport]; ok { + continue + } + if ap, ok := airports[airport]; ok { if _, ok := ap.Approaches[appr]; !ok { e.ErrorString( "arrival airport %q doesn't have a %q approach for \"expect_approach\"", diff --git a/sim/spawn_arrivals.go b/sim/spawn_arrivals.go index 482c0ead5..490ea98ac 100644 --- a/sim/spawn_arrivals.go +++ b/sim/spawn_arrivals.go @@ -10,6 +10,7 @@ import ( "maps" gomath "math" "slices" + "strings" "time" av "github.com/mmp/vice/aviation" @@ -101,14 +102,31 @@ func (s *Sim) createArrivalNoLock(group string, arrivalAirport string) (*Aircraf } arr := arrivals[idx] - airline := rand.SampleSlice(s.Rand, arr.Airlines[arrivalAirport]) - ac, acType := s.sampleAircraft(airline.AirlineSpecifier, airline.Airport, arrivalAirport, s.lg) + airlines := arr.Airlines[arrivalAirport] + if len(airlines) == 0 { + return nil, fmt.Errorf("no airlines for arrival group %s airport %s", group, arrivalAirport) + } + + var ac *Aircraft + var acType string + var departureAirport string + for range 50 { + airline := rand.SampleSlice(s.Rand, airlines) + departureAirport = airline.Airport + if airline.Callsign != "" { + ac, acType = s.sampleAircraftWithAirlineCallsign(airline.AirlineSpecifier, departureAirport, arrivalAirport, s.lg) + } else { + ac, acType = s.sampleAircraft(airline.AirlineSpecifier, departureAirport, arrivalAirport, s.lg) + } + if ac != nil { + break + } + } if ac == nil { - return nil, fmt.Errorf("unable to sample a valid aircraft for airline %+v at %q", airline, - arrivalAirport) + return nil, fmt.Errorf("unable to sample a valid aircraft for arrivals to %q", arrivalAirport) } - ac.InitializeFlightPlan(av.FlightRulesIFR, acType, airline.Airport, arrivalAirport) + ac.InitializeFlightPlan(av.FlightRulesIFR, acType, departureAirport, arrivalAirport) err := ac.InitializeArrival(s.State.Airports[arrivalAirport], &arr, s.State.NmPerLongitude, s.State.MagneticVariation, s.wxModel, s.State.SimTime, s.lg) @@ -149,12 +167,17 @@ func (s *Sim) createArrivalNoLock(group string, arrivalAirport string) (*Aircraf return ac, err } -func (s *Sim) sampleAircraft(al av.AirlineSpecifier, departureAirport, arrivalAirport string, lg *log.Logger) (*Aircraft, string) { - // Collect all currently in-use or soon-to-be in-use callsigns. +func (s *Sim) currentCallsigns() []av.ADSBCallsign { callsigns := slices.Collect(maps.Keys(s.Aircraft)) for _, fp := range s.STARSComputer.FlightPlans { callsigns = append(callsigns, av.ADSBCallsign(fp.ACID)) } + return callsigns +} + +func (s *Sim) sampleAircraft(al av.AirlineSpecifier, departureAirport, arrivalAirport string, lg *log.Logger) (*Aircraft, string) { + // Collect all currently in-use or soon-to-be in-use callsigns. + callsigns := s.currentCallsigns() actype, callsign := al.SampleAcTypeAndCallsign(s.Rand, callsigns, s.EnforceUniqueCallsignSuffix, departureAirport, arrivalAirport, lg) @@ -167,6 +190,27 @@ func (s *Sim) sampleAircraft(al av.AirlineSpecifier, departureAirport, arrivalAi Mode: av.TransponderModeAltitude, }, actype } +func (s *Sim) sampleAircraftWithAirlineCallsign(al av.AirlineSpecifier, departureAirport, arrivalAirport string, lg *log.Logger) (*Aircraft, string) { + callsign := strings.ToUpper(strings.TrimSpace(al.Callsign)) + if callsign == "" { + return nil, "" + } + + callsigns := s.currentCallsigns() + if av.CallsignClashesWithExisting(callsigns, callsign, s.EnforceUniqueCallsignSuffix) { + return nil, "" + } + + actype := al.SampleAcType(s.Rand, departureAirport, arrivalAirport, lg) + if actype == "" { + return nil, "" + } + + return &Aircraft{ + ADSBCallsign: av.ADSBCallsign(callsign), + Mode: av.TransponderModeAltitude, + }, actype +} // assignSquawk allocates an enroute squawk code and assigns it to both the // aircraft and NAS flight plan. @@ -250,13 +294,32 @@ func (s *Sim) createOverflightNoLock(group string) (*Aircraft, error) { overflights := s.State.InboundFlows[group].Overflights of := rand.SampleSlice(s.Rand, overflights) - airline := rand.SampleSlice(s.Rand, of.Airlines) - ac, acType := s.sampleAircraft(airline.AirlineSpecifier, airline.DepartureAirport, airline.ArrivalAirport, s.lg) + if len(of.Airlines) == 0 { + return nil, fmt.Errorf("no airlines for overflights in %q", group) + } + + var ac *Aircraft + var acType string + var departureAirport string + var arrivalAirport string + for range 50 { + airline := rand.SampleSlice(s.Rand, of.Airlines) + departureAirport = airline.DepartureAirport + arrivalAirport = airline.ArrivalAirport + if airline.Callsign != "" { + ac, acType = s.sampleAircraftWithAirlineCallsign(airline.AirlineSpecifier, departureAirport, arrivalAirport, s.lg) + } else { + ac, acType = s.sampleAircraft(airline.AirlineSpecifier, departureAirport, arrivalAirport, s.lg) + } + if ac != nil { + break + } + } if ac == nil { - return nil, fmt.Errorf("unable to sample a valid aircraft for overflight %+v in %q", airline, group) + return nil, fmt.Errorf("unable to sample a valid aircraft for overflight in %q", group) } - ac.InitializeFlightPlan(av.FlightRulesIFR, acType, airline.DepartureAirport, airline.ArrivalAirport) + ac.InitializeFlightPlan(av.FlightRulesIFR, acType, departureAirport, arrivalAirport) if err := ac.InitializeOverflight(&of, s.State.NmPerLongitude, s.State.MagneticVariation, s.wxModel, s.State.SimTime, s.lg); err != nil { diff --git a/sim/spawn_departures.go b/sim/spawn_departures.go index 62f7239a7..497d8173a 100644 --- a/sim/spawn_departures.go +++ b/sim/spawn_departures.go @@ -611,11 +611,25 @@ func (s *Sim) createIFRDepartureNoLock(departureAirport, runway, category string } dep := &ap.Departures[idx] - airline := rand.SampleSlice(s.Rand, dep.Airlines) - ac, acType := s.sampleAircraft(airline.AirlineSpecifier, departureAirport, dep.Destination, s.lg) + if len(dep.Airlines) == 0 { + return nil, fmt.Errorf("no airlines for departure at %q", departureAirport) + } + + var ac *Aircraft + var acType string + for range 50 { + airline := rand.SampleSlice(s.Rand, dep.Airlines) + if airline.Callsign != "" { + ac, acType = s.sampleAircraftWithAirlineCallsign(airline.AirlineSpecifier, departureAirport, dep.Destination, s.lg) + } else { + ac, acType = s.sampleAircraft(airline.AirlineSpecifier, departureAirport, dep.Destination, s.lg) + } + if ac != nil { + break + } + } if ac == nil { - return nil, fmt.Errorf("unable to sample a valid aircraft for airline %+v at %q", airline, - departureAirport) + return nil, fmt.Errorf("unable to sample a valid aircraft for departures at %q", departureAirport) } ac.InitializeFlightPlan(av.FlightRulesIFR, acType, departureAirport, dep.Destination) From f54f70672f0699c65caa4b1c2f8bafa7afd9017b Mon Sep 17 00:00:00 2001 From: Michael Date: Mon, 19 Jan 2026 22:03:21 -0500 Subject: [PATCH 2/6] update `index.html` --- website/index.html | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/website/index.html b/website/index.html index 9bb737465..7c0115a50 100644 --- a/website/index.html +++ b/website/index.html @@ -6736,6 +6736,11 @@

Airlines and Aircraft

String The ICAO code of the airline (which must be present in vice's openscope-airlines.json file.) + + "callsign" + Array of strings + The callsign of the airline. (Both callsigns and ICAO cannot be specified at the same time.) + "fleet" String @@ -7312,6 +7317,12 @@

Departures

"icao": "AFR" } ], + "callsigns": [ + { + "callsign": "AFR1021", + "types": ["A320"] + } + ], "destination": "LPFG", "exit": "HAPIE", "route": "HAPIE YAHOO WHALE N251A JOOPY NATZ MALOT NATZ GISTI LESLU M142 LND N160 NAKID M25 ANNET UM25 UVSUV UM25 INGOR UM25 LUKIP LUKIP9E", @@ -7335,6 +7346,7 @@

Departures

Each object specifies an airline and the types of aircraft it flies on the route. See Airlines and Aircraft for the form of the airline objects. + "destination" String @@ -7431,6 +7443,7 @@

Arrivals

"icao", "fleet", and "types" members of the airline object documented in Airlines and Aircraft as well as some additional fields. See the example below. + "assigned_altitude" Number @@ -7658,6 +7671,7 @@

Overflights

Specification of the airlines flying the overflight and their departure/arrival airports. See the example below. + "assigned_altitude" Number From 64ae3081075478394527c26d32b85eea68470c29 Mon Sep 17 00:00:00 2001 From: Michael Date: Tue, 20 Jan 2026 15:40:17 -0500 Subject: [PATCH 3/6] Remove redundant check --- aviation/aviation.go | 3 --- 1 file changed, 3 deletions(-) diff --git a/aviation/aviation.go b/aviation/aviation.go index 228b31de4..87c75a4a3 100644 --- a/aviation/aviation.go +++ b/aviation/aviation.go @@ -978,9 +978,6 @@ func (ar *Arrival) PostDeserialize(loc Locator, nmPerLongitude float32, magnetic approachAssigned := ar.ExpectApproach.A != nil || ar.ExpectApproach.B != nil ar.Waypoints.CheckArrival(e, controlPositions, approachAssigned, checkScratchpad) - if len(ar.Airlines) == 0 { - e.ErrorString("no \"airlines\" specified for arrivals") - } for arrivalAirport := range ar.Airlines { e.Push("Arrival airport " + arrivalAirport) From 8bccfdcf5fec7b53c7c1e7c51a888323771cffac Mon Sep 17 00:00:00 2001 From: Michael Date: Tue, 20 Jan 2026 15:40:55 -0500 Subject: [PATCH 4/6] refactor for loop --- aviation/aviation.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aviation/aviation.go b/aviation/aviation.go index 87c75a4a3..cc4edb14f 100644 --- a/aviation/aviation.go +++ b/aviation/aviation.go @@ -455,7 +455,7 @@ func icaoFromCallsign(callsign string) string { if len(callsign) < 3 { return "" } - for i := 0; i < 3; i++ { + for i := range 3 { ch := callsign[i] if ch < 'A' || ch > 'Z' { return "" From 7a07e56bdd896df56a11457dfb3f40d3356317df Mon Sep 17 00:00:00 2001 From: Michael Date: Tue, 20 Jan 2026 15:42:19 -0500 Subject: [PATCH 5/6] Fixup website information --- website/index.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/website/index.html b/website/index.html index 7c0115a50..ec464ae2b 100644 --- a/website/index.html +++ b/website/index.html @@ -6738,8 +6738,8 @@

Airlines and Aircraft

"callsign" - Array of strings - The callsign of the airline. (Both callsigns and ICAO cannot be specified at the same time.) + String + A specific callsign (eg. AAL123) to use for an aircraft flying this route. (Both callsigns and ICAO cannot be specified at the same time.) "fleet" From 288d6fc02d07e857d4eea7c8ff6b5a22105bcc78 Mon Sep 17 00:00:00 2001 From: Michael Date: Tue, 20 Jan 2026 16:20:56 -0500 Subject: [PATCH 6/6] use `rand.SampleWeighted` for aircraft sample calls --- aviation/aviation.go | 30 +++++++++++++++++++----------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/aviation/aviation.go b/aviation/aviation.go index cc4edb14f..0b0ef82fb 100644 --- a/aviation/aviation.go +++ b/aviation/aviation.go @@ -271,7 +271,9 @@ func (a AirlineSpecifier) sampleAcType(r *rand.Rand, departureAirport, arrivalAi // Sample according to fleet count, filtering by maximum distance for CWT category var actype string - acCount := 0 + + // First attempt: filter aircraft by distance and sample weighted by fleet count + filteredAircraft := make([]FleetAircraft, 0) for _, ac := range a.Aircraft() { // Filter based on flight distance and aircraft CWT category if flightDistance > 0 && !slices.Contains(extraLongRange, ac.ICAO) { @@ -284,20 +286,27 @@ func (a AirlineSpecifier) sampleAcType(r *rand.Rand, departureAirport, arrivalAi } } } + filteredAircraft = append(filteredAircraft, ac) + } - // Reservoir sampling... - acCount += ac.Count - if r.Float32() < float32(ac.Count)/float32(acCount) { - actype = ac.ICAO + if len(filteredAircraft) > 0 { + sampled, ok := rand.SampleWeighted(r, filteredAircraft, func(ac FleetAircraft) float32 { + return float32(ac.Count) + }) + + if ok { + actype = sampled.ICAO } } + if actype == "" { // Try again without considering range. - for _, ac := range a.Aircraft() { - acCount += ac.Count - if r.Float32() < float32(ac.Count)/float32(acCount) { - actype = ac.ICAO - } + sampled, ok := rand.SampleWeighted(r, a.Aircraft(), func(ac FleetAircraft) float32 { + return float32(ac.Count) + }) + + if ok { + actype = sampled.ICAO } } if actype != "" { @@ -978,7 +987,6 @@ func (ar *Arrival) PostDeserialize(loc Locator, nmPerLongitude float32, magnetic approachAssigned := ar.ExpectApproach.A != nil || ar.ExpectApproach.B != nil ar.Waypoints.CheckArrival(e, controlPositions, approachAssigned, checkScratchpad) - for arrivalAirport := range ar.Airlines { e.Push("Arrival airport " + arrivalAirport) if len(ar.Airlines[arrivalAirport]) == 0 {