diff --git a/aviation/airport.go b/aviation/airport.go index 1eb8f8050..1d953b4a7 100644 --- a/aviation/airport.go +++ b/aviation/airport.go @@ -46,17 +46,25 @@ type Airport struct { } type VFRRandomsSpec struct { - Rate int `json:"rate"` - Fleet string `json:"fleet"` + Rate int `json:"rate"` + Fleet string `json:"fleet"` + CommonAircraft []VFRCommonAircraft `json:"common_aircraft,omitempty"` +} + +type VFRCommonAircraft struct { + Airline string `json:"airline,omitempty"` + Callsign string `json:"callsign,omitempty"` + Type string `json:"type,omitempty"` } type VFRRouteSpec struct { - Name string `json:"name"` - Rate int `json:"rate"` - Fleet string `json:"fleet"` - Waypoints WaypointArray `json:"waypoints"` - Destination string `json:"destination"` - Description string `json:"description"` + Name string `json:"name"` + Rate int `json:"rate"` + Fleet string `json:"fleet"` + CommonAircraft []VFRCommonAircraft `json:"common_aircraft,omitempty"` + Waypoints WaypointArray `json:"waypoints"` + Destination string `json:"destination"` + Description string `json:"description"` } type CRDAPair struct { @@ -488,6 +496,29 @@ func (ap *Airport) PostDeserialize(icao string, loc Locator, nmPerLongitude floa e.ErrorString(`"fleet" specified for "vfr" "random_routes" but "rate" is not specified or is zero.`) } } + for j, ca := range ap.VFR.Randoms.CommonAircraft { + if ca.Airline == "" && ca.Callsign == "" { + e.ErrorString(`random_routes common_aircraft[%d]: must specify "airline" or "callsign"`, j) + continue + } + if ca.Airline != "" && ca.Callsign != "" { + e.ErrorString(`random_routes common_aircraft[%d]: cannot specify both "airline" and "callsign"`, j) + continue + } + if ca.Callsign != "" && ca.Type == "" { + e.ErrorString(`random_routes common_aircraft[%d]: must specify "type" when "callsign" is set`, j) + } + if ca.Airline != "" { + if _, ok := DB.Airlines[strings.ToUpper(ca.Airline)]; !ok { + e.ErrorString(`random_routes common_aircraft[%d]: airline %q unknown`, j, ca.Airline) + } + } + if ca.Type != "" { + if _, ok := DB.AircraftPerformance[ca.Type]; !ok { + e.ErrorString(`random_routes common_aircraft[%d]: aircraft type %q unknown`, j, ca.Type) + } + } + } for i := range ap.VFR.Routes { ap.VFR.Routes[i].Waypoints = ap.VFR.Routes[i].Waypoints.InitializeLocations(loc, nmPerLongitude, magneticVariation, false, e) @@ -520,6 +551,29 @@ func (ap *Airport) PostDeserialize(icao string, loc Locator, nmPerLongitude floa if _, ok := DB.Airports[spec.Destination]; !ok { e.ErrorString("Destination airport %q unknown", spec.Destination) } + for j, ca := range spec.CommonAircraft { + if ca.Airline == "" && ca.Callsign == "" { + e.ErrorString(`common_aircraft[%d]: must specify "airline" or "callsign"`, j) + continue + } + if ca.Airline != "" && ca.Callsign != "" { + e.ErrorString(`common_aircraft[%d]: cannot specify both "airline" and "callsign"`, j) + continue + } + if ca.Callsign != "" && ca.Type == "" { + e.ErrorString(`common_aircraft[%d]: must specify "type" when "callsign" is set`, j) + } + if ca.Airline != "" { + if _, ok := DB.Airlines[strings.ToUpper(ca.Airline)]; !ok { + e.ErrorString(`common_aircraft[%d]: airline %q unknown`, j, ca.Airline) + } + } + if ca.Type != "" { + if _, ok := DB.AircraftPerformance[ca.Type]; !ok { + e.ErrorString(`common_aircraft[%d]: aircraft type %q unknown`, j, ca.Type) + } + } + } e.Pop() } e.Pop() diff --git a/sim/spawn_departures.go b/sim/spawn_departures.go index 717ec57c3..e4ae62d5d 100644 --- a/sim/spawn_departures.go +++ b/sim/spawn_departures.go @@ -432,10 +432,9 @@ func (s *Sim) makeNewVFRDeparture(depart string, runway av.RunwayID) (ac *Aircra s.lg.Errorf("%s: unable to sample VFR destination airport???", depart) continue } - ac, _, err = s.createUncontrolledVFRDeparture(depart, arrive, sampledRandoms.Fleet, nil, s.State.SimTime) + ac, _, err = s.createUncontrolledVFRDeparture(depart, arrive, &av.VFRRouteSpec{Fleet: sampledRandoms.Fleet, CommonAircraft: sampledRandoms.CommonAircraft}, s.State.SimTime) } else if sampledRoute != nil { - ac, _, err = s.createUncontrolledVFRDeparture(depart, sampledRoute.Destination, sampledRoute.Fleet, - sampledRoute.Waypoints, s.State.SimTime) + ac, _, err = s.createUncontrolledVFRDeparture(depart, sampledRoute.Destination, sampledRoute, s.State.SimTime) } if err == nil && ac != nil { @@ -693,7 +692,7 @@ func (s *Sim) CreateVFRDeparture(departureAirport string) (*Aircraft, error) { // This shouldn't happen... return nil, nil } else { - ac, _, err := s.createUncontrolledVFRDeparture(departureAirport, arrive, ap.VFR.Randoms.Fleet, nil, s.State.SimTime) + ac, _, err := s.createUncontrolledVFRDeparture(departureAirport, arrive, &av.VFRRouteSpec{Fleet: ap.VFR.Randoms.Fleet, CommonAircraft: ap.VFR.Randoms.CommonAircraft}, s.State.SimTime) return ac, err } } @@ -724,14 +723,34 @@ func makeDepartureAircraft(ac *Aircraft, simTime Time, model *wx.Model, r *rand. return d } -func (s *Sim) createUncontrolledVFRDeparture(depart, arrive, fleet string, routeWps []av.Waypoint, simTime Time) (*Aircraft, string, error) { +func (s *Sim) createUncontrolledVFRDeparture(depart, arrive string, route *av.VFRRouteSpec, simTime Time) (*Aircraft, string, error) { depap, arrap := av.DB.Airports[depart], av.DB.Airports[arrive] rwy, _, ok := s.currentVFRRunway(depart) if !ok { return nil, "", fmt.Errorf("%s: unable to find current VFR runway", depart) } - ac, acType := s.sampleAircraft(av.AirlineSpecifier{ICAO: "N", Fleet: fleet}, depart, arrive, s.lg) + var ac *Aircraft + var acType string + if len(route.CommonAircraft) > 0 { + ca := route.CommonAircraft[s.Rand.Intn(len(route.CommonAircraft))] + if ca.Callsign != "" { + callsigns := s.currentCallsigns() + if av.CallsignClashesWithExisting(callsigns, ca.Callsign, s.EnforceUniqueCallsignSuffix) { + return nil, "", fmt.Errorf("callsign %s already in use", ca.Callsign) + } + ac = &Aircraft{ADSBCallsign: av.ADSBCallsign(ca.Callsign), Mode: av.TransponderModeAltitude} + acType = ca.Type + } else { + var types []string + if ca.Type != "" { + types = []string{ca.Type} + } + ac, acType = s.sampleAircraft(av.AirlineSpecifier{ICAO: ca.Airline, AircraftTypes: types}, depart, arrive, s.lg) + } + } else { + ac, acType = s.sampleAircraft(av.AirlineSpecifier{ICAO: "N", Fleet: route.Fleet}, depart, arrive, s.lg) + } if ac == nil { return nil, "", fmt.Errorf("unable to sample a valid aircraft") } @@ -781,8 +800,8 @@ func (s *Sim) createUncontrolledVFRDeparture(depart, arrive, fleet string, route // Fly a downwind if needed var hdg math.TrueHeading - if len(routeWps) > 0 { - hdg = math.Heading2LL(opp, routeWps[0].Location, s.State.NmPerLongitude) + if len(route.Waypoints) > 0 { + hdg = math.Heading2LL(opp, route.Waypoints[0].Location, s.State.NmPerLongitude) } else { hdg = math.Heading2LL(opp, mid, s.State.NmPerLongitude) } @@ -800,8 +819,8 @@ func (s *Sim) createUncontrolledVFRDeparture(depart, arrive, fleet string, route } var randomizeAltitudeRange bool - if len(routeWps) > 0 { - wps = append(wps, routeWps...) + if len(route.Waypoints) > 0 { + wps = append(wps, route.Waypoints...) randomizeAltitudeRange = true } else { randomizeAltitudeRange = false diff --git a/sim/spawn_pattern.go b/sim/spawn_pattern.go index 74e805662..dd5bdfb5b 100644 --- a/sim/spawn_pattern.go +++ b/sim/spawn_pattern.go @@ -189,7 +189,18 @@ func (s *Sim) spawnPatternAircraft() { var ac *Aircraft var acType string for range 20 { - ac, acType = s.sampleAircraft(av.AirlineSpecifier{ICAO: "N", Fleet: ap.VFR.Randoms.Fleet}, name, name, s.lg) + spec := av.AirlineSpecifier{ICAO: "N", Fleet: ap.VFR.Randoms.Fleet} + if len(ap.VFR.Randoms.CommonAircraft) > 0 { + ca := ap.VFR.Randoms.CommonAircraft[s.Rand.Intn(len(ap.VFR.Randoms.CommonAircraft))] + if ca.Callsign == "" { + var types []string + if ca.Type != "" { + types = []string{ca.Type} + } + spec = av.AirlineSpecifier{ICAO: ca.Airline, AircraftTypes: types} + } + } + ac, acType = s.sampleAircraft(spec, name, name, s.lg) if ac == nil { continue } diff --git a/website/facility-engineering.html b/website/facility-engineering.html index 4ef3b1931..f536f627f 100644 --- a/website/facility-engineering.html +++ b/website/facility-engineering.html @@ -3922,7 +3922,12 @@