class Traffic(Entity): """ Traffic class definition : Traffic data Methods: Traffic() : constructor reset() : Reset traffic database w.r.t a/c data create(acid,actype,aclat,aclon,achdg,acalt,acspd) : create aircraft delete(acid) : delete an aircraft from traffic data deletall() : delete all traffic update(sim) : do a numerical integration step id2idx(name) : return index in traffic database of given call sign engchange(i,engtype) : change engine type of an aircraft setnoise(A) : Add turbulence Members: see create Created by : Jacco M. Hoekstra """ def __init__(self): super().__init__() # Traffic is the toplevel trafficarrays object self.setroot(self) self.ntraf = 0 self.cond = Condition() # Conditional commands list self.wind = WindSim() self.turbulence = Turbulence() self.translvl = 5000. * ft # [m] Default transition level with self.settrafarrays(): # Aircraft Info self.id = [] # identifier (string) self.type = [] # aircaft type (string) # Positions self.lat = np.array([]) # latitude [deg] self.lon = np.array([]) # longitude [deg] self.distflown = np.array([]) # distance travelled [m] self.alt = np.array([]) # altitude [m] self.hdg = np.array([]) # traffic heading [deg] self.trk = np.array([]) # track angle [deg] # Velocities self.tas = np.array([]) # true airspeed [m/s] self.gs = np.array([]) # ground speed [m/s] self.gsnorth = np.array([]) # ground speed [m/s] self.gseast = np.array([]) # ground speed [m/s] self.cas = np.array([]) # calibrated airspeed [m/s] self.M = np.array([]) # mach number self.vs = np.array([]) # vertical speed [m/s] # Acceleration self.ax = np.array([]) # [m/s2] current longitudinal acceleration # Atmosphere self.p = np.array([]) # air pressure [N/m2] self.rho = np.array([]) # air density [kg/m3] self.Temp = np.array([]) # air temperature [K] self.dtemp = np.array([]) # delta t for non-ISA conditions # Wind speeds self.windnorth = np.array( []) # wind speed north component a/c pos [m/s] self.windeast = np.array( []) # wind speed east component a/c pos [m/s] # Traffic autopilot settings self.selspd = np.array([]) # selected spd(CAS or Mach) [m/s or -] self.aptas = np.array([]) # just for initializing self.selalt = np.array([]) # selected alt[m] self.selvs = np.array([]) # selected vertical speed [m/s] # Whether to perform LNAV and VNAV self.swlnav = np.array([], dtype=np.bool) self.swvnav = np.array([], dtype=np.bool) self.swvnavspd = np.array([], dtype=np.bool) # Flight Models self.cd = ConflictDetection() self.cr = ConflictResolution() self.ap = Autopilot() self.aporasas = APorASAS() self.adsb = ADSB() self.trails = Trails() self.actwp = ActiveWaypoint() self.perf = PerfBase() # Group Logic self.groups = TrafficGroups() # Traffic autopilot data self.swhdgsel = np.array( [], dtype=np.bool) # determines whether aircraft is turning # Traffic autothrottle settings self.swats = np.array( [], dtype=np.bool ) # Switch indicating whether autothrottle system is on/off self.thr = np.array( []) # Thottle seeting (0.0-1.0), negative = non-valid/auto # Display information on label self.label = [] # Text and bitmap of traffic label # Miscallaneous self.coslat = np.array([]) # Cosine of latitude for computations self.eps = np.array([]) # Small nonzero numbers self.work = np.array([]) # Work done throughout the flight # Default bank angles per flight phase self.bphase = np.deg2rad(np.array([15, 35, 35, 35, 15, 45])) def reset(self): ''' Clear all traffic data upon simulation reset. ''' # Some child reset functions depend on a correct value of self.ntraf self.ntraf = 0 # This ensures that the traffic arrays (which size is dynamic) # are all reset as well, so all lat,lon,sdp etc but also objects adsb super().reset() # reset performance model self.perf.reset() # Reset models self.wind.clear() # Build new modules for turbulence self.turbulence.reset() # Noise (turbulence, ADBS-transmission noise, ADSB-truncated effect) self.setnoise(False) # Reset transition level to default value self.translvl = 5000. * ft def mcre(self, n, actype="B744", acalt=None, acspd=None, dest=None): """ Create one or more random aircraft in a specified area """ area = bs.scr.getviewbounds() # Generate random callsigns idtmp = chr(randint(65, 90)) + chr(randint(65, 90)) + '{:>05}' acid = [idtmp.format(i) for i in range(n)] # Generate random positions aclat = np.random.rand(n) * (area[1] - area[0]) + area[0] aclon = np.random.rand(n) * (area[3] - area[2]) + area[2] achdg = np.random.randint(1, 360, n) acalt = acalt or np.random.randint(2000, 39000, n) * ft acspd = acspd or np.random.randint(250, 450, n) * kts self.cre(acid, actype, aclat, aclon, achdg, acalt, acspd) def cre(self, acid, actype="B744", aclat=52., aclon=4., achdg=None, acalt=0, acspd=0): """ Create one or more aircraft. """ # Determine number of aircraft to create from array length of acid n = 1 if isinstance(acid, str) else len(acid) if isinstance(acid, str): # Check if not already exist if self.id.count(acid.upper()) > 0: return False, acid + " already exists." # already exists do nothing acid = n * [acid] # Adjust the size of all traffic arrays super().create(n) self.ntraf += n if isinstance(actype, str): actype = n * [actype] if isinstance(aclat, (float, int)): aclat = np.array(n * [aclat]) if isinstance(aclon, (float, int)): aclon = np.array(n * [aclon]) # Limit longitude to [-180.0, 180.0] aclon[aclon > 180.0] -= 360.0 aclon[aclon < -180.0] += 360.0 achdg = (refdata.hdg or 0.0) if achdg is None else achdg # Aircraft Info self.id[-n:] = acid self.type[-n:] = actype # Positions self.lat[-n:] = aclat self.lon[-n:] = aclon self.alt[-n:] = acalt self.hdg[-n:] = achdg self.trk[-n:] = achdg # Velocities self.tas[-n:], self.cas[-n:], self.M[-n:] = vcasormach(acspd, acalt) self.gs[-n:] = self.tas[-n:] hdgrad = np.radians(achdg) self.gsnorth[-n:] = self.tas[-n:] * np.cos(hdgrad) self.gseast[-n:] = self.tas[-n:] * np.sin(hdgrad) # Atmosphere self.p[-n:], self.rho[-n:], self.Temp[-n:] = vatmos(acalt) # Wind if self.wind.winddim > 0: applywind = self.alt[-n:] > 50. * ft self.windnorth[-n:], self.windeast[-n:] = self.wind.getdata( self.lat[-n:], self.lon[-n:], self.alt[-n:]) self.gsnorth[ -n:] = self.gsnorth[-n:] + self.windnorth[-n:] * applywind self.gseast[ -n:] = self.gseast[-n:] + self.windeast[-n:] * applywind self.trk[-n:] = np.logical_not(applywind)*achdg + \ applywind*np.degrees(np.arctan2(self.gseast[-n:], self.gsnorth[-n:])) self.gs[-n:] = np.sqrt(self.gsnorth[-n:]**2 + self.gseast[-n:]**2) else: self.windnorth[-n:] = 0.0 self.windeast[-n:] = 0.0 # Traffic autopilot settings self.selspd[-n:] = self.cas[-n:] self.aptas[-n:] = self.tas[-n:] self.selalt[-n:] = self.alt[-n:] # Display information on label self.label[-n:] = n * [['', '', '', 0]] # Miscallaneous self.coslat[-n:] = np.cos(np.radians( aclat)) # Cosine of latitude for flat-earth aproximations self.eps[-n:] = 0.01 # Finally call create for child TrafficArrays. This only needs to be done # manually in Traffic. self.create_children(n) # Record as individual CRE commands for repeatability #print(self.ntraf-n,self.ntraf) for j in range(self.ntraf - n, self.ntraf): # Reconstruct CRE command line = "CRE " + ",".join([ self.id[j], self.type[j], str(self.lat[j]), str(self.lon[j]), str(round(self.trk[j])), str(round(self.alt[j] / ft)), str(round(self.cas[j] / kts)) ]) # Savecmd(cmd,line): line is saved, cmd is used to prevent recording PAN & ZOOM commands and CRE # So insert a dummy command to record the line savecmd("---", line) def creconfs(self, acid, actype, targetidx, dpsi, dcpa, tlosh, dH=None, tlosv=None, spd=None): ''' Create an aircraft in conflict with target aircraft. Arguments: - acid: callsign of new aircraft - actype: aircraft type of new aircraft - targetidx: id (callsign) of target aircraft - dpsi: Conflict angle (angle between tracks of ownship and intruder) (deg) - cpa: Predicted distance at closest point of approach (NM) - tlosh: Horizontal time to loss of separation ((hh:mm:)sec) - dH: Vertical distance (ft) - tlosv: Vertical time to loss of separation - spd: Speed of new aircraft (CAS/Mach, kts/-) ''' latref = self.lat[targetidx] # deg lonref = self.lon[targetidx] # deg altref = self.alt[targetidx] # m trkref = radians(self.trk[targetidx]) gsref = self.gs[targetidx] # m/s tasref = self.tas[targetidx] # m/s vsref = self.vs[targetidx] # m/s cpa = dcpa * nm pzr = bs.settings.asas_pzr * nm pzh = bs.settings.asas_pzh * ft trk = trkref + radians(dpsi) if dH is None: acalt = altref acvs = 0.0 else: acalt = altref + dH tlosv = tlosh if tlosv is None else tlosv acvs = vsref - np.sign(dH) * (abs(dH) - pzh) / tlosv if spd: # CAS or Mach provided: convert to groundspeed, assuming that # wind at intruder position is similar to wind at ownship position tas = tasref if spd is None else casormach2tas(spd, acalt) tasn, tase = tas * cos(trk), tas * sin(trk) wn, we = self.wind.getdata(latref, lonref, acalt) gsn, gse = tasn + wn, tase + we else: # Groundspeed is the same as ownship gsn, gse = gsref * cos(trk), gsref * sin(trk) # Horizontal relative velocity vector vreln, vrele = gsref * cos(trkref) - gsn, gsref * sin(trkref) - gse # Relative velocity magnitude vrel = sqrt(vreln * vreln + vrele * vrele) # Relative travel distance to closest point of approach drelcpa = tlosh * vrel + (0 if cpa > pzr else sqrt(pzr * pzr - cpa * cpa)) # Initial intruder distance dist = sqrt(drelcpa * drelcpa + cpa * cpa) # Rotation matrix diagonal and cross elements for distance vector rd = drelcpa / dist rx = cpa / dist # Rotate relative velocity vector to obtain intruder bearing brn = degrees(atan2(-rx * vreln + rd * vrele, rd * vreln + rx * vrele)) # Calculate intruder lat/lon aclat, aclon = geo.kwikpos(latref, lonref, brn, dist / nm) # convert groundspeed to CAS, and track to heading using actual # intruder position wn, we = self.wind.getdata(aclat, aclon, acalt) tasn, tase = gsn - wn, gse - we acspd = tas2cas(sqrt(tasn * tasn + tase * tase), acalt) achdg = degrees(atan2(tase, tasn)) # Create and, when necessary, set vertical speed self.cre(acid, actype, aclat, aclon, achdg, acalt, acspd) self.ap.selaltcmd(len(self.lat) - 1, altref, acvs) self.vs[-1] = acvs def delete(self, idx): """Delete an aircraft""" # If this is a multiple delete, sort first for list delete # (which will use list in reverse order to avoid index confusion) if isinstance(idx, Collection): idx = np.sort(idx) # Call the actual delete function super().delete(idx) # Update number of aircraft self.ntraf = len(self.lat) return True def update(self): # Update only if there is traffic --------------------- if self.ntraf == 0: return #---------- Atmosphere -------------------------------- self.p, self.rho, self.Temp = vatmos(self.alt) #---------- ADSB Update ------------------------------- self.adsb.update() #---------- Fly the Aircraft -------------------------- self.ap.update() # Autopilot logic self.update_asas() # Airborne Separation Assurance self.aporasas.update() # Decide to use autopilot or ASAS for commands #---------- Performance Update ------------------------ self.perf.update() #---------- Limit commanded speeds based on performance ------------------------------ self.aporasas.tas, self.aporasas.vs, self.aporasas.alt = \ self.perf.limits(self.aporasas.tas, self.aporasas.vs, self.aporasas.alt, self.ax) #---------- Kinematics -------------------------------- self.update_airspeed() self.update_groundspeed() self.update_pos() #---------- Simulate Turbulence ----------------------- self.turbulence.update() # Check whether new traffic state triggers conditional commands self.cond.update() #---------- Aftermath --------------------------------- self.trails.update() @timed_function(name='asas', dt=bs.settings.asas_dt, manual=True) def update_asas(self): # Conflict detection and resolution self.cd.update(self, self) self.cr.update(self.cd, self, self) def update_airspeed(self): # Compute horizontal acceleration delta_spd = self.aporasas.tas - self.tas need_ax = np.abs(delta_spd) > np.abs(bs.sim.simdt * self.perf.axmax) self.ax = need_ax * np.sign(delta_spd) * self.perf.axmax # Update velocities self.tas = np.where(need_ax, self.tas + self.ax * bs.sim.simdt, self.aporasas.tas) self.cas = vtas2cas(self.tas, self.alt) self.M = vtas2mach(self.tas, self.alt) # Turning turnrate = np.degrees(g0 * np.tan(np.where(self.ap.turnphi>self.eps,self.ap.turnphi,self.ap.bankdef) \ / np.maximum(self.tas, self.eps))) delhdg = (self.aporasas.hdg - self.hdg + 180) % 360 - 180 # [deg] self.swhdgsel = np.abs(delhdg) > np.abs(bs.sim.simdt * turnrate) # Update heading self.hdg = np.where( self.swhdgsel, self.hdg + bs.sim.simdt * turnrate * np.sign(delhdg), self.aporasas.hdg) % 360.0 # Update vertical speed (alt select, capture and hold autopilot mode) delta_alt = self.aporasas.alt - self.alt # Old dead band version: # self.swaltsel = np.abs(delta_alt) > np.maximum( # 10 * ft, np.abs(2 * bs.sim.simdt * self.vs)) # Update version: time based engage of altitude capture (to adapt for UAV vs airliner scale) self.swaltsel = np.abs(delta_alt) > 1.05*np.maximum(np.abs(bs.sim.simdt * self.aporasas.vs), \ np.abs(bs.sim.simdt * self.vs)) target_vs = self.swaltsel * np.sign(delta_alt) * np.abs( self.aporasas.vs) delta_vs = target_vs - self.vs # print(delta_vs / fpm) need_az = np.abs(delta_vs) > 300 * fpm # small threshold self.az = need_az * np.sign(delta_vs) * ( 300 * fpm) # fixed vertical acc approx 1.6 m/s^2 self.vs = np.where(need_az, self.vs + self.az * bs.sim.simdt, target_vs) self.vs = np.where(np.isfinite(self.vs), self.vs, 0) # fix vs nan issue def update_groundspeed(self): # Compute ground speed and track from heading, airspeed and wind if self.wind.winddim == 0: # no wind self.gsnorth = self.tas * np.cos(np.radians(self.hdg)) self.gseast = self.tas * np.sin(np.radians(self.hdg)) self.gs = self.tas self.trk = self.hdg self.windnorth[:], self.windeast[:] = 0.0, 0.0 else: applywind = self.alt > 50. * ft # Only apply wind when airborne vnwnd, vewnd = self.wind.getdata(self.lat, self.lon, self.alt) self.windnorth[:], self.windeast[:] = vnwnd, vewnd self.gsnorth = self.tas * np.cos(np.radians( self.hdg)) + self.windnorth * applywind self.gseast = self.tas * np.sin(np.radians( self.hdg)) + self.windeast * applywind self.gs = np.logical_not(applywind)*self.tas + \ applywind*np.sqrt(self.gsnorth**2 + self.gseast**2) self.trk = np.logical_not(applywind)*self.hdg + \ applywind*np.degrees(np.arctan2(self.gseast, self.gsnorth)) % 360. self.work += (self.perf.thrust * bs.sim.simdt * np.sqrt(self.gs * self.gs + self.vs * self.vs)) def update_pos(self): # Update position self.alt = np.where(self.swaltsel, np.round(self.alt + self.vs * bs.sim.simdt, 6), self.aporasas.alt) self.lat = self.lat + np.degrees(bs.sim.simdt * self.gsnorth / Rearth) self.coslat = np.cos(np.deg2rad(self.lat)) self.lon = self.lon + np.degrees( bs.sim.simdt * self.gseast / self.coslat / Rearth) self.distflown += self.gs * bs.sim.simdt def id2idx(self, acid): """Find index of aircraft id""" if not isinstance(acid, str): # id2idx is called for multiple id's # Fast way of finding indices of all ACID's in a given list tmp = dict((v, i) for i, v in enumerate(self.id)) return [tmp.get(acidi, -1) for acidi in acid] else: # Catch last created id (* or # symbol) if acid in ('#', '*'): return self.ntraf - 1 try: return self.id.index(acid.upper()) except: return -1 def setnoise(self, noise=None): """Noise (turbulence, ADBS-transmission noise, ADSB-truncated effect)""" if noise is None: return True, "Noise is currently " + ( "on" if self.turbulence.active else "off") self.turbulence.setnoise(noise) self.adsb.setnoise(noise) return True def engchange(self, acid, engid): """Change of engines""" self.perf.engchange(acid, engid) return def move(self, idx, lat, lon, alt=None, hdg=None, casmach=None, vspd=None): self.lat[idx] = lat self.lon[idx] = lon if alt is not None: self.alt[idx] = alt self.selalt[idx] = alt if hdg is not None: self.hdg[idx] = hdg self.ap.trk[idx] = hdg if casmach is not None: self.tas[idx], self.selspd[idx], _ = vcasormach(casmach, alt) if vspd is not None: self.vs[idx] = vspd self.swvnav[idx] = False def poscommand( self, idxorwp ): # Show info on aircraft(int) or waypoint or airport (str) """POS command: Show info or an aircraft, airport, waypoint or navaid""" # Aircraft index if type(idxorwp) == int and idxorwp >= 0: idx = idxorwp acid = self.id[idx] actype = self.type[idx] latlon = latlon2txt(self.lat[idx], self.lon[idx]) alt = round(self.alt[idx] / ft) hdg = round(self.hdg[idx]) trk = round(self.trk[idx]) cas = round(self.cas[idx] / kts) tas = round(self.tas[idx] / kts) gs = round(self.gs[idx] / kts) M = self.M[idx] VS = round(self.vs[idx] / ft * 60.) route = self.ap.route[idx] # Position report lines = "Info on %s %s index = %d\n" %(acid, actype, idx) \ + "Pos: "+latlon+ "\n" \ + "Hdg: %03d Trk: %03d\n" %(hdg, trk) \ + "Alt: %d ft V/S: %d fpm\n" %(alt,VS) \ + "CAS/TAS/GS: %d/%d/%d kts M: %.3f\n"%(cas,tas,gs,M) # FMS AP modes if self.swlnav[idx] and route.nwp > 0 and route.iactwp >= 0: if self.swvnav[idx]: if self.swvnavspd[idx]: lines = lines + "VNAV (incl.VNAVSPD), " else: lines = lines + "VNAV (NOT VNAVSPD), " lines += "LNAV to " + route.wpname[route.iactwp] + "\n" # Flight info: Destination and origin if self.ap.orig[idx] != "" or self.ap.dest[idx] != "": lines = lines + "Flying" if self.ap.orig[idx] != "": lines = lines + " from " + self.ap.orig[idx] if self.ap.dest[idx] != "": lines = lines + " to " + self.ap.dest[idx] # Show a/c info and highlight route of aircraft in radar window # and pan to a/c (to show route) bs.scr.showroute(acid) return True, lines # Waypoint: airport, navaid or fix else: wp = idxorwp.upper() # Reference position for finding nearest reflat, reflon = bs.scr.getviewctr() lines = "Info on " + wp + ":\n" # First try airports (most used and shorter, hence faster list) iap = bs.navdb.getaptidx(wp) if iap >= 0: aptypes = ["large", "medium", "small"] lines = lines + bs.navdb.aptname[iap]+"\n" \ + "is a "+ aptypes[max(-1,bs.navdb.aptype[iap]-1)] \ +" airport at:\n" \ + latlon2txt(bs.navdb.aptlat[iap], \ bs.navdb.aptlon[iap]) + "\n" \ + "Elevation: " \ + str(int(round(bs.navdb.aptelev[iap]/ft))) \ + " ft \n" # Show country name try: ico = bs.navdb.cocode2.index(bs.navdb.aptco[iap].upper()) lines = lines + "in "+bs.navdb.coname[ico]+" ("+ \ bs.navdb.aptco[iap]+")" except: ico = -1 lines = lines + "Country code: " + bs.navdb.aptco[iap] try: runways = bs.navdb.rwythresholds[ bs.navdb.aptid[iap]].keys() if runways: lines = lines + "\nRunways: " + ", ".join(runways) except KeyError: pass # Not found as airport, try waypoints & navaids else: iwps = bs.navdb.getwpindices(wp, reflat, reflon) if iwps[0] >= 0: typetxt = "" desctxt = "" lastdesc = "XXXXXXXX" for i in iwps: # One line type text if typetxt == "": typetxt = typetxt + bs.navdb.wptype[i] else: typetxt = typetxt + " and " + bs.navdb.wptype[i] # Description: multi-line samedesc = bs.navdb.wpdesc[i] == lastdesc if desctxt == "": desctxt = desctxt + bs.navdb.wpdesc[i] lastdesc = bs.navdb.wpdesc[i] elif not samedesc: desctxt = desctxt + "\n" + bs.navdb.wpdesc[i] lastdesc = bs.navdb.wpdesc[i] # Navaid: frequency if bs.navdb.wptype[i] in ["VOR", "DME", "TACAN" ] and not samedesc: desctxt = desctxt + " " + str( bs.navdb.wpfreq[i]) + " MHz" elif bs.navdb.wptype[i] == "NDB" and not samedesc: desctxt = desctxt + " " + str( bs.navdb.wpfreq[i]) + " kHz" iwp = iwps[0] # Basic info lines = lines + wp +" is a "+ typetxt \ + " at\n"\ + latlon2txt(bs.navdb.wplat[iwp], \ bs.navdb.wplon[iwp]) # Navaids have description if len(desctxt) > 0: lines = lines + "\n" + desctxt # VOR give variation if bs.navdb.wptype[iwp] == "VOR": lines = lines + "\nVariation: "+ \ str(bs.navdb.wpvar[iwp])+" deg" # How many others? nother = bs.navdb.wpid.count(wp) - len(iwps) if nother > 0: verb = ["is ", "are "][min(1, max(0, nother - 1))] lines = lines +"\nThere "+verb + str(nother) +\ " other waypoint(s) also named " + wp # In which airways? connect = bs.navdb.listconnections(wp, \ bs.navdb.wplat[iwp], bs.navdb.wplon[iwp]) if len(connect) > 0: awset = set([]) for c in connect: awset.add(c[0]) lines = lines + "\nAirways: " + "-".join(awset) # Try airway id else: # airway awid = wp airway = bs.navdb.listairway(awid) if len(airway) > 0: lines = "" for segment in airway: lines = lines+"Airway "+ awid + ": " + \ " - ".join(segment)+"\n" lines = lines[:-1] # cut off final newline else: return False, idxorwp + " not found as a/c, airport, navaid or waypoint" # Show what we found on airport and navaid/waypoint return True, lines def airwaycmd(self, key): ''' Show conections of a waypoint or airway. ''' reflat, reflon = bs.scr.getviewctr() if bs.navdb.awid.count(key) > 0: return self.poscommand(key) # Find connecting airway legs wpid = key iwp = bs.navdb.getwpidx(wpid, reflat, reflon) if iwp < 0: return False, key + " not found." wplat = bs.navdb.wplat[iwp] wplon = bs.navdb.wplon[iwp] connect = bs.navdb.listconnections(key, wplat, wplon) if connect: lines = "" for c in connect: if len(c) >= 2: # Add airway, direction, waypoint lines = lines + c[0] + ": to " + c[1] + "\n" return True, lines[:-1] # exclude final newline return False, f"No airway legs found for {key}" def settrans(self, alt=-999.): """ Set or show transition level""" # in case a valid value is ginve set it if alt > -900.: if alt > 0.: self.translvl = alt return True return False, "Transition level needs to be ft/FL and larger than zero" # In case no value is given, show it tlvl = int(round(self.translvl / ft)) return True, f"Transition level = {tlvl}/FL{int(round(tlvl/100.))}" def setbanklim(self, idx, bankangle=None): ''' Set bank limit for given aircraft. ''' if bankangle: self.ap.bankdef[idx] = np.radians(bankangle) # [rad] return True return True, f"Banklimit of {self.id[idx]} is {int(np.degrees(self.ap.bankdef[idx]))} deg" def setthrottle(self, idx, throttle=""): """Set throttle to given value or AUTO, meaning autothrottle on (default)""" if throttle: if throttle in ('AUTO', 'OFF'): # throttle mode off, ATS on self.swats[idx] = True # Autothrottle on self.thr[idx] = -999. # Set to invalid elif throttle == "IDLE": self.swats[idx] = False self.thr[idx] = 0.0 else: # Check for percent unit if throttle.count("%") == 1: throttle = throttle.replace("%", "") factor = 0.01 else: factor = 1.0 # Remaining option is that it is a float, so try conversion try: x = factor * float(throttle) except: return False, "THR invalid argument " + throttle # Check whether value makes sense if x < 0.0 or x > 1.0: return False, "THR invalid value " + throttle + ". Needs to be [0.0 , 1.0]" # Valid value, set throttle and disable autothrottle self.swats[idx] = False self.thr[idx] = x return True if self.swats[idx]: return True, "ATS of " + self.id[idx] + " is ON" return True, "ATS of " + self.id[idx] + " is OFF. THR is " + str( self.thr[idx])
class Traffic(TrafficArrays): """ Traffic class definition : Traffic data Methods: Traffic() : constructor reset() : Reset traffic database w.r.t a/c data create(acid,actype,aclat,aclon,achdg,acalt,acspd) : create aircraft delete(acid) : delete an aircraft from traffic data deletall() : delete all traffic update(sim) : do a numerical integration step id2idx(name) : return index in traffic database of given call sign engchange(i,engtype) : change engine type of an aircraft setnoise(A) : Add turbulence Members: see create Created by : Jacco M. Hoekstra """ def __init__(self): super(Traffic, self).__init__() # Traffic is the toplevel trafficarrays object TrafficArrays.SetRoot(self) self.ntraf = 0 self.cond = Condition() # Conditional commands list self.wind = WindSim() self.turbulence = Turbulence() self.translvl = 5000. * ft # [m] Default transition level with RegisterElementParameters(self): # Aircraft Info self.id = [] # identifier (string) self.type = [] # aircaft type (string) self.dest_temp = np.array([], dtype=np.int32) self.dest_hdg = np.array([], dtype=np.int32) # Positions self.lat = np.array([]) # latitude [deg] self.lon = np.array([]) # longitude [deg] self.distflown = np.array([]) # distance travelled [m] self.alt = np.array([]) # altitude [m] self.hdg = np.array([]) # traffic heading [deg] self.trk = np.array([]) # track angle [deg] # Velocities self.tas = np.array([]) # true airspeed [m/s] self.gs = np.array([]) # ground speed [m/s] self.gsnorth = np.array([]) # ground speed [m/s] self.gseast = np.array([]) # ground speed [m/s] self.cas = np.array([]) # calibrated airspeed [m/s] self.M = np.array([]) # mach number self.vs = np.array([]) # vertical speed [m/s] # Atmosphere self.p = np.array([]) # air pressure [N/m2] self.rho = np.array([]) # air density [kg/m3] self.Temp = np.array([]) # air temperature [K] self.dtemp = np.array([]) # delta t for non-ISA conditions # Wind speeds self.windnorth = np.array( []) # wind speed north component a/c pos [m/s] self.windeast = np.array( []) # wind speed east component a/c pos [m/s] # Traffic autopilot settings self.selspd = np.array([]) # selected spd(CAS or Mach) [m/s or -] self.aptas = np.array([]) # just for initializing self.selalt = np.array([]) # selected alt[m] self.selvs = np.array([]) # selected vertical speed [m/s] # Whether to perform LNAV and VNAV self.swlnav = np.array([], dtype=np.bool) self.swvnav = np.array([], dtype=np.bool) self.swvnavspd = np.array([], dtype=np.bool) # Flight Models self.cd = ConflictDetection() self.cr = ConflictResolution() self.ap = Autopilot() self.pilot = Pilot() self.adsb = ADSB() self.trails = Trails() self.actwp = ActiveWaypoint() self.perf = Perf() # Group Logic self.groups = TrafficGroups() # Traffic performance data self.apvsdef = np.array( []) # [m/s]default vertical speed of autopilot self.aphi = np.array([]) # [rad] bank angle setting of autopilot self.ax = np.array( []) # [m/s2] absolute value of longitudinal accelleration self.bank = np.array([]) # nominal bank angle, [radian] self.swhdgsel = np.array( [], dtype=np.bool) # determines whether aircraft is turning # limit settings self.limspd = np.array([]) # limit speed self.limspd_flag = np.array( [], dtype=np.bool ) # flag for limit spd - we have to test for max and min self.limalt = np.array([]) # limit altitude self.limalt_flag = np.array( []) # A need to limit altitude has been detected self.limvs = np.array( []) # limit vertical speed due to thrust limitation self.limvs_flag = np.array([]) # A need to limit V/S detected # Display information on label self.label = [] # Text and bitmap of traffic label # Miscallaneous self.coslat = np.array([]) # Cosine of latitude for computations self.eps = np.array([]) # Small nonzero numbers # Default bank angles per flight phase self.bphase = np.deg2rad(np.array([15, 35, 35, 35, 15, 45])) def reset(self): ''' Clear all traffic data upon simulation reset. ''' # Some child reset functions depend on a correct value of self.ntraf self.ntraf = 0 # This ensures that the traffic arrays (which size is dynamic) # are all reset as well, so all lat,lon,sdp etc but also objects adsb super(Traffic, self).reset() # reset performance model self.perf.reset() # Reset models self.wind.clear() # Build new modules for turbulence self.turbulence.reset() # Noise (turbulence, ADBS-transmission noise, ADSB-truncated effect) self.setnoise(False) # Reset transition level to default value self.translvl = 5000. * ft def create(self, n=1, actype='B737-800', acalt=5000 * ft, acspd=250 * kts, aclat=None, aclon=None, achdg=None, acid=None, dest=None): """ Create multiple random aircraft in a specified area """ area = bs.scr.getviewbounds() # acid = None if acid is None or acid == 'NONE': # print('aids') idtmp = chr(randint(65, 90)) + chr(randint(65, 90)) + chr( randint(65, 90)) + '{:>05}' acid = [idtmp.format(i) for i in range(n)] elif isinstance(acid, str): # Check if not already exist if self.id.count(acid.upper()) > 0: return False, acid + " already exists." # already exists do nothing acid = [acid] else: # TODO: for a list of a/c, check each callsign pass super(Traffic, self).create(n) # Increase number of aircraft self.ntraf += n if aclat is None: aclat = np.random.rand(n) * (area[1] - area[0]) + area[0] elif isinstance(aclat, (float, int)): aclat = np.array(n * [aclat]) if aclon is None: aclon = np.random.rand(n) * (area[3] - area[2]) + area[2] elif isinstance(aclon, (float, int)): aclon = np.array(n * [aclon]) # Limit longitude to [-180.0, 180.0] if n == 1: aclon = aclon - 360 if aclon > 180 else \ aclon + 360 if aclon < -180.0 else aclon else: aclon[aclon > 180.0] -= 360.0 aclon[aclon < -180.0] += 360.0 if achdg is None: achdg = np.random.randint(1, 360, n) elif isinstance(achdg, (float, int)): achdg = np.array(n * [achdg]) if acalt is None: acalt = np.random.randint(2000, 39000, n) * ft elif isinstance(acalt, (float, int)): acalt = np.array(n * [acalt]) if acspd is None: acspd = np.random.randint(250, 450, n) * kts elif isinstance(acspd, (float, int)): acspd = np.array(n * [acspd]) actype = n * [actype] if isinstance(actype, str) else actype # dest = n * [dest] if isinstance(dest, str) else dest if dest is None: dest = np.random.randint(1, 7, n) # SAVEIC: save cre command when filled in # Special provision in case SAVEIC is on: then save individual CRE commands # Names of aircraft (acid) need to be recorded for saved future commands # And positions need to be the same in case of *MCRE" for i in range(n): bs.stack.savecmd(" ".join([ "CRE", acid[i], actype[i], str(aclat[i]), str(aclon[i]), str(int(round(achdg[i]))), str(int(round(acalt[i] / ft))), str(int(round(acspd[i] / kts))) ])) # Aircraft Info self.id[-n:] = acid self.type[-n:] = actype self.dest_temp[-n:] = dest # self.dest_hdg[-n:] = dest_hdg # Positions self.lat[-n:] = aclat self.lon[-n:] = aclon self.alt[-n:] = acalt self.hdg[-n:] = achdg self.trk[-n:] = achdg # Velocities self.tas[-n:], self.cas[-n:], self.M[-n:] = vcasormach(acspd, acalt) self.gs[-n:] = self.tas[-n:] hdgrad = np.radians(achdg) self.gsnorth[-n:] = self.tas[-n:] * np.cos(hdgrad) self.gseast[-n:] = self.tas[-n:] * np.sin(hdgrad) # Atmosphere self.p[-n:], self.rho[-n:], self.Temp[-n:] = vatmos(acalt) # Wind if self.wind.winddim > 0: applywind = self.alt[-n:] > 50. * ft self.windnorth[-n:], self.windeast[-n:] = self.wind.getdata( self.lat[-n:], self.lon[-n:], self.alt[-n:]) self.gsnorth[ -n:] = self.gsnorth[-n:] + self.windnorth[-n:] * applywind self.gseast[ -n:] = self.gseast[-n:] + self.windeast[-n:] * applywind self.trk[-n:] = np.logical_not(applywind)*achdg + \ applywind*np.degrees(np.arctan2(self.gseast[-n:], self.gsnorth[-n:])) self.gs[-n:] = np.sqrt(self.gsnorth[-n:]**2 + self.gseast[-n:]**2) else: self.windnorth[-n:] = 0.0 self.windeast[-n:] = 0.0 # Traffic performance data #(temporarily default values) self.apvsdef[-n:] = 1500. * fpm # default vertical speed of autopilot self.aphi[-n:] = np.radians(25.) # bank angle setting of autopilot self.ax[-n:] = kts # absolute value of longitudinal accelleration self.bank[-n:] = np.radians(25.) # Traffic autopilot settings self.selspd[-n:] = self.cas[-n:] self.aptas[-n:] = self.tas[-n:] self.selalt[-n:] = self.alt[-n:] # Display information on label self.label[-n:] = n * [['', '', '', 0]] # Miscallaneous self.coslat[-n:] = np.cos(np.radians( aclat)) # Cosine of latitude for flat-earth aproximations self.eps[-n:] = 0.01 # Finally call create for child TrafficArrays. This only needs to be done # manually in Traffic. self.create_children(n) def creconfs(self, acid, actype, targetidx, dpsi, cpa, tlosh, dH=None, tlosv=None, spd=None): latref = self.lat[targetidx] # deg lonref = self.lon[targetidx] # deg altref = self.alt[targetidx] # m trkref = radians(self.trk[targetidx]) gsref = self.gs[targetidx] # m/s vsref = self.vs[targetidx] # m/s cpa = cpa * nm pzr = bs.settings.asas_pzr * nm pzh = bs.settings.asas_pzh * ft trk = trkref + radians(dpsi) gs = gsref if spd is None else spd if dH is None: acalt = altref acvs = 0.0 else: acalt = altref + dH tlosv = tlosh if tlosv is None else tlosv acvs = vsref - np.sign(dH) * (abs(dH) - pzh) / tlosv # Horizontal relative velocity vector gsn, gse = gs * cos(trk), gs * sin(trk) vreln, vrele = gsref * cos(trkref) - gsn, gsref * sin(trkref) - gse # Relative velocity magnitude vrel = sqrt(vreln * vreln + vrele * vrele) # Relative travel distance to closest point of approach drelcpa = tlosh * vrel + (0 if cpa > pzr else sqrt(pzr * pzr - cpa * cpa)) # Initial intruder distance dist = sqrt(drelcpa * drelcpa + cpa * cpa) # Rotation matrix diagonal and cross elements for distance vector rd = drelcpa / dist rx = cpa / dist # Rotate relative velocity vector to obtain intruder bearing brn = degrees(atan2(-rx * vreln + rd * vrele, rd * vreln + rx * vrele)) # Calculate intruder lat/lon aclat, aclon = geo.kwikpos(latref, lonref, brn, dist / nm) # convert groundspeed to CAS, and track to heading wn, we = self.wind.getdata(aclat, aclon, acalt) tasn, tase = gsn - wn, gse - we acspd = tas2cas(sqrt(tasn * tasn + tase * tase), acalt) achdg = degrees(atan2(tase, tasn)) # Create and, when necessary, set vertical speed self.create(1, actype, acalt, acspd, None, aclat, aclon, achdg, acid) self.ap.selaltcmd(len(self.lat) - 1, altref, acvs) self.vs[-1] = acvs def delete(self, idx): """Delete an aircraft""" # If this is a multiple delete, sort first for list delete # (which will use list in reverse order to avoid index confusion) if isinstance(idx, Collection): idx = np.sort(idx) # Call the actual delete function super(Traffic, self).delete(idx) # Update conditions list self.cond.delac(idx) # Update number of aircraft self.ntraf = len(self.lat) return True def update(self): # Update only if there is traffic --------------------- if self.ntraf == 0: return #---------- Atmosphere -------------------------------- self.p, self.rho, self.Temp = vatmos(self.alt) #---------- ADSB Update ------------------------------- self.adsb.update() #---------- Fly the Aircraft -------------------------- self.ap.update() # Autopilot logic self.update_asas() # Airborne Separation Assurance self.pilot.APorASAS() # Decide autopilot or ASAS #---------- Performance Update ------------------------ self.perf.update() #---------- Limit Speeds ------------------------------ self.pilot.applylimits() #---------- Kinematics -------------------------------- self.update_airspeed() self.update_groundspeed() self.update_pos() #---------- Simulate Turbulence ----------------------- self.turbulence.update() # Check whether new traffic state triggers conditional commands self.cond.update() #---------- Aftermath --------------------------------- self.trails.update() return @timed_function('asas', dt=bs.settings.asas_dt) def update_asas(self): # Conflict detection and resolution self.cd.update(self, self) self.cr.update(self.cd, self, self) def update_airspeed(self): # Compute horizontal acceleration delta_spd = self.pilot.tas - self.tas ax = self.perf.acceleration() need_ax = np.abs(delta_spd) > np.abs(bs.sim.simdt * ax) self.ax = need_ax * np.sign(delta_spd) * ax # Update velocities self.tas = np.where(need_ax, self.tas + self.ax * bs.sim.simdt, self.pilot.tas) self.cas = vtas2cas(self.tas, self.alt) self.M = vtas2mach(self.tas, self.alt) # Turning turnrate = np.degrees(g0 * np.tan(self.bank) / np.maximum(self.tas, self.eps)) delhdg = (self.pilot.hdg - self.hdg + 180) % 360 - 180 # [deg] self.swhdgsel = np.abs(delhdg) > np.abs(bs.sim.simdt * turnrate) # Update heading self.hdg = np.where( self.swhdgsel, self.hdg + bs.sim.simdt * turnrate * np.sign(delhdg), self.pilot.hdg) % 360.0 # Update vertical speed delta_alt = self.pilot.alt - self.alt self.swaltsel = np.abs(delta_alt) > np.maximum( 10 * ft, np.abs(2 * np.abs(bs.sim.simdt * self.vs))) target_vs = self.swaltsel * np.sign(delta_alt) * np.abs(self.pilot.vs) delta_vs = target_vs - self.vs # print(delta_vs / fpm) need_az = np.abs(delta_vs) > 300 * fpm # small threshold self.az = need_az * np.sign(delta_vs) * ( 300 * fpm) # fixed vertical acc approx 1.6 m/s^2 self.vs = np.where(need_az, self.vs + self.az * bs.sim.simdt, target_vs) self.vs = np.where(np.isfinite(self.vs), self.vs, 0) # fix vs nan issue def update_groundspeed(self): # Compute ground speed and track from heading, airspeed and wind if self.wind.winddim == 0: # no wind self.gsnorth = self.tas * np.cos(np.radians(self.hdg)) self.gseast = self.tas * np.sin(np.radians(self.hdg)) self.gs = self.tas self.trk = self.hdg self.windnorth[:], self.windeast[:] = 0.0, 0.0 else: applywind = self.alt > 50. * ft # Only apply wind when airborne vnwnd, vewnd = self.wind.getdata(self.lat, self.lon, self.alt) self.windnorth[:], self.windeast[:] = vnwnd, vewnd self.gsnorth = self.tas * np.cos(np.radians( self.hdg)) + self.windnorth * applywind self.gseast = self.tas * np.sin(np.radians( self.hdg)) + self.windeast * applywind self.gs = np.logical_not(applywind)*self.tas + \ applywind*np.sqrt(self.gsnorth**2 + self.gseast**2) self.trk = np.logical_not(applywind)*self.hdg + \ applywind*np.degrees(np.arctan2(self.gseast, self.gsnorth)) % 360. def update_pos(self): # Update position self.alt = np.where(self.swaltsel, np.round(self.alt + self.vs * bs.sim.simdt, 6), self.pilot.alt) self.lat = self.lat + np.degrees(bs.sim.simdt * self.gsnorth / Rearth) self.coslat = np.cos(np.deg2rad(self.lat)) self.lon = self.lon + np.degrees( bs.sim.simdt * self.gseast / self.coslat / Rearth) self.distflown += self.gs * bs.sim.simdt def id2idx(self, acid): """Find index of aircraft id""" if not isinstance(acid, str): # id2idx is called for multiple id's # Fast way of finding indices of all ACID's in a given list tmp = dict((v, i) for i, v in enumerate(self.id)) return [tmp.get(acidi, -1) for acidi in acid] else: # Catch last created id (* or # symbol) if acid in ('#', '*'): return self.ntraf - 1 try: return self.id.index(acid.upper()) except: return -1 def setnoise(self, noise=None): """Noise (turbulence, ADBS-transmission noise, ADSB-truncated effect)""" if noise is None: return True, "Noise is currently " + ( "on" if self.turbulence.active else "off") self.turbulence.setnoise(noise) self.adsb.setnoise(noise) return True def engchange(self, acid, engid): """Change of engines""" self.perf.engchange(acid, engid) return def move(self, idx, lat, lon, alt=None, hdg=None, casmach=None, vspd=None): self.lat[idx] = lat self.lon[idx] = lon if alt is not None: self.alt[idx] = alt self.selalt[idx] = alt if hdg is not None: self.hdg[idx] = hdg self.ap.trk[idx] = hdg if casmach is not None: self.tas[idx], self.selspd[idx], _ = vcasormach(casmach, alt) if vspd is not None: self.vs[idx] = vspd self.swvnav[idx] = False def nom(self, idx): """ Reset acceleration back to nominal (1 kt/s^2): NOM acid """ self.ax[idx] = kts #[m/s2] def poscommand( self, idxorwp ): # Show info on aircraft(int) or waypoint or airport (str) """POS command: Show info or an aircraft, airport, waypoint or navaid""" # Aircraft index if type(idxorwp) == int and idxorwp >= 0: idx = idxorwp acid = self.id[idx] actype = self.type[idx] latlon = latlon2txt(self.lat[idx], self.lon[idx]) alt = round(self.alt[idx] / ft) hdg = round(self.hdg[idx]) trk = round(self.trk[idx]) cas = round(self.cas[idx] / kts) tas = round(self.tas[idx] / kts) gs = round(self.gs[idx] / kts) M = self.M[idx] VS = round(self.vs[idx] / ft * 60.) route = self.ap.route[idx] # Position report lines = "Info on %s %s index = %d\n" %(acid, actype, idx) \ + "Pos: "+latlon+ "\n" \ + "Hdg: %03d Trk: %03d\n" %(hdg, trk) \ + "Alt: %d ft V/S: %d fpm\n" %(alt,VS) \ + "CAS/TAS/GS: %d/%d/%d kts M: %.3f\n"%(cas,tas,gs,M) # FMS AP modes if self.swlnav[idx] and route.nwp > 0 and route.iactwp >= 0: if self.swvnav[idx]: if self.swvnavspd[idx]: lines = lines + "VNAV (incl.VNAVSPD), " else: lines = lines + "VNAV (NOT VNAVSPD), " lines += "LNAV to " + route.wpname[route.iactwp] + "\n" # Flight info: Destination and origin if self.ap.orig[idx] != "" or self.ap.dest[idx] != "": lines = lines + "Flying" if self.ap.orig[idx] != "": lines = lines + " from " + self.ap.orig[idx] if self.ap.dest[idx] != "": lines = lines + " to " + self.ap.dest[idx] # Show a/c info and highlight route of aircraft in radar window # and pan to a/c (to show route) bs.scr.showroute(acid) return True, lines # Waypoint: airport, navaid or fix else: wp = idxorwp.upper() # Reference position for finding nearest reflat, reflon = bs.scr.getviewctr() lines = "Info on " + wp + ":\n" # First try airports (most used and shorter, hence faster list) iap = bs.navdb.getaptidx(wp) if iap >= 0: aptypes = ["large", "medium", "small"] lines = lines + bs.navdb.aptname[iap]+"\n" \ + "is a "+ aptypes[max(-1,bs.navdb.aptype[iap]-1)] \ +" airport at:\n" \ + latlon2txt(bs.navdb.aptlat[iap], \ bs.navdb.aptlon[iap]) + "\n" \ + "Elevation: " \ + str(int(round(bs.navdb.aptelev[iap]/ft))) \ + " ft \n" # Show country name try: ico = bs.navdb.cocode2.index(bs.navdb.aptco[iap].upper()) lines = lines + "in "+bs.navdb.coname[ico]+" ("+ \ bs.navdb.aptco[iap]+")" except: ico = -1 lines = lines + "Country code: " + bs.navdb.aptco[iap] try: runways = bs.navdb.rwythresholds[ bs.navdb.aptid[iap]].keys() if runways: lines = lines + "\nRunways: " + ", ".join(runways) except: pass # Not found as airport, try waypoints & navaids else: iwps = bs.navdb.getwpindices(wp, reflat, reflon) if iwps[0] >= 0: typetxt = "" desctxt = "" lastdesc = "XXXXXXXX" for i in iwps: # One line type text if typetxt == "": typetxt = typetxt + bs.navdb.wptype[i] else: typetxt = typetxt + " and " + bs.navdb.wptype[i] # Description: multi-line samedesc = bs.navdb.wpdesc[i] == lastdesc if desctxt == "": desctxt = desctxt + bs.navdb.wpdesc[i] lastdesc = bs.navdb.wpdesc[i] elif not samedesc: desctxt = desctxt + "\n" + bs.navdb.wpdesc[i] lastdesc = bs.navdb.wpdesc[i] # Navaid: frequency if bs.navdb.wptype[i] in ["VOR", "DME", "TACAN" ] and not samedesc: desctxt = desctxt + " " + str( bs.navdb.wpfreq[i]) + " MHz" elif bs.navdb.wptype[i] == "NDB" and not samedesc: desctxt = desctxt + " " + str( bs.navdb.wpfreq[i]) + " kHz" iwp = iwps[0] # Basic info lines = lines + wp +" is a "+ typetxt \ + " at\n"\ + latlon2txt(bs.navdb.wplat[iwp], \ bs.navdb.wplon[iwp]) # Navaids have description if len(desctxt) > 0: lines = lines + "\n" + desctxt # VOR give variation if bs.navdb.wptype[iwp] == "VOR": lines = lines + "\nVariation: "+ \ str(bs.navdb.wpvar[iwp])+" deg" # How many others? nother = bs.navdb.wpid.count(wp) - len(iwps) if nother > 0: verb = ["is ", "are "][min(1, max(0, nother - 1))] lines = lines +"\nThere "+verb + str(nother) +\ " other waypoint(s) also named " + wp # In which airways? connect = bs.navdb.listconnections(wp, \ bs.navdb.wplat[iwp], bs.navdb.wplon[iwp]) if len(connect) > 0: awset = set([]) for c in connect: awset.add(c[0]) lines = lines + "\nAirways: " + "-".join(awset) # Try airway id else: # airway awid = wp airway = bs.navdb.listairway(awid) if len(airway) > 0: lines = "" for segment in airway: lines = lines+"Airway "+ awid + ": " + \ " - ".join(segment)+"\n" lines = lines[:-1] # cut off final newline else: return False, idxorwp + " not found as a/c, airport, navaid or waypoint" # Show what we found on airport and navaid/waypoint return True, lines def airwaycmd(self, key=""): # Show conections of a waypoint reflat, reflon = bs.scr.getviewctr() if key == "": return False, 'AIRWAY needs waypoint or airway' if bs.navdb.awid.count(key) > 0: return self.poscommand(key.upper()) else: # Find connecting airway legs wpid = key.upper() iwp = bs.navdb.getwpidx(wpid, reflat, reflon) if iwp < 0: return False, key + " not found." wplat = bs.navdb.wplat[iwp] wplon = bs.navdb.wplon[iwp] connect = bs.navdb.listconnections(key.upper(), wplat, wplon) if len(connect) > 0: lines = "" for c in connect: if len(c) >= 2: # Add airway, direction, waypoint lines = lines + c[0] + ": to " + c[1] + "\n" return True, lines[:-1] # exclude final newline else: return False, "No airway legs found for ", key def settrans(self, alt=-999.): """ Set or show transition level""" # in case a valid value is ginve set it if alt > -900.: if alt > 0.: self.translvl = alt return True else: return False, "Transition level needs to be ft/FL and larger than zero" # In case no value is given, show it else: tlvl = int(round(self.translvl / ft)) return True,"Transition level = " + \ str(tlvl) + "/FL" + str(int(round(tlvl/100.)))