IOS

3. IOS 강의 Enroute Picker, Core Data

dennis 2021. 1. 22. 15:01

www.youtube.com/watch?v=fCfC6m7XUew&feature=youtu.be

www.youtube.com/watch?v=yOhyOpXvaec&feature=youtu.be

11. Slides.pdf
0.71MB
12. Slides.pdf
0.88MB
EnrouteL.zip
0.14MB

 

 

소스

FilterFlights.swift

import SwiftUI

struct FilterFlights: View {
    @FetchRequest(fetchRequest: Airport.fetchRequest(.all)) var airports: FetchedResults<Airport>
    @FetchRequest(fetchRequest: Airline.fetchRequest(.all)) var airlines: FetchedResults<Airline>

    @Binding var flightSearch: FlightSearch
    @Binding var isPresented: Bool
    
    @State private var draft: FlightSearch
    
    init(flightSearch: Binding<FlightSearch>, isPresented: Binding<Bool>) {
        _flightSearch = flightSearch
        _isPresented = isPresented
        _draft = State(wrappedValue: flightSearch.wrappedValue)
    }
    
    var body: some View {
        NavigationView {
            Form {
                Picker("Destination", selection: $draft.destination) {
                    ForEach(airports.sorted(), id: \.self) { airport in
                        Text("\(airport.friendlyName)").tag(airport)
                    }
                }
                Picker("Origin", selection: $draft.origin) {
                    Text("Any").tag(Airport?.none)
                    ForEach(airports.sorted(), id: \.self) { (airport: Airport?) in
                        Text("\(airport?.friendlyName ?? "Any")").tag(airport)
                    }
                }
                Picker("Airline", selection: $draft.airline) {
                    Text("Any").tag(Airline?.none)
                    ForEach(airlines.sorted(), id: \.self) { (airline: Airline?) in
                        Text("\(airline?.friendlyName ?? "Any")").tag(airline)
                    }
                }
                Toggle(isOn: $draft.inTheAir) { Text("Enroute Only") }
            }
            .navigationBarTitle("Filter Flights")
            .navigationBarItems(leading: cancel, trailing: done)
        }
    }
    
    var cancel: some View {
        Button("Cancel") {
            self.isPresented = false
        }
    }
    
    var done: some View {
        Button("Done") {
            if self.draft.destination != self.flightSearch.destination {
                self.draft.destination.fetchIncomingFlights()
            }
            self.flightSearch = self.draft
            self.isPresented = false
        }
    }
}

//struct FilterFlights_Previews: PreviewProvider {
//    static var previews: some View {
//        FilterFlights()
//    }
//}

 

 

FlightsEnrouteView.swift

import SwiftUI
import CoreData

struct FlightSearch {
    var destination: Airport
    var origin: Airport?
    var airline: Airline?
    var inTheAir: Bool = true
}

extension FlightSearch {
    var predicate: NSPredicate {
        var format = "destination_ = %@"
        var args: [NSManagedObject] = [destination] // args could be [Any] if needed
        if origin != nil {
            format += " and origin_ = %@"
            args.append(origin!)
        }
        if airline != nil {
            format += " and airline_ = %@"
            args.append(airline!)
        }
        if inTheAir { format += " and departure != nil" }
        return NSPredicate(format: format, argumentArray: args)
    }
}

struct FlightsEnrouteView: View {
    @Environment(\.managedObjectContext) var context
    
    @State var flightSearch: FlightSearch
    
    var body: some View {
        NavigationView {
            FlightList(flightSearch)
                .navigationBarItems(leading: simulation, trailing: filter)
        }
    }
    
    @State private var showFilter = false
    
    var filter: some View {
        Button("Filter") {
            self.showFilter = true
        }
        .sheet(isPresented: $showFilter) {
            FilterFlights(flightSearch: self.$flightSearch, isPresented: self.$showFilter)
                .environment(\.managedObjectContext, self.context)
        }
    }
    
    // if no FlightAware credentials exist in Info.plist
    // then we simulate data from KSFO and KLAS (Las Vegas, NV)
    // the simulation time must match the times in the simulation data
    // so, to orient the UI, this simulation View shows the time we are simulating
    var simulation: some View {
        let isSimulating = Date.currentFlightTime.timeIntervalSince(Date()) < -1
        return Text(isSimulating ? DateFormatter.shortTime.string(from: Date.currentFlightTime) : "")
    }
}

struct FlightList: View {
    @FetchRequest var flights: FetchedResults<Flight>
    
    init(_ flightSearch: FlightSearch) {
        let request = Flight.fetchRequest(flightSearch.predicate)
        _flights = FetchRequest(fetchRequest: request)
    }

    var body: some View {
        List {
            ForEach(flights, id: \.ident) { flight in
                FlightListEntry(flight: flight)
            }
        }
        .navigationBarTitle(title)
    }
    
    private var title: String {
        let title = "Flights"
        if let destination = flights.first?.destination.icao {
            return title + " to \(destination)"
        } else {
            return title
        }
    }
}

struct FlightListEntry: View {
    @ObservedObject var flight: Flight

    var body: some View {
        VStack(alignment: .leading) {
            Text(name)
            Text(arrives).font(.caption)
            Text(origin).font(.caption)
        }
            .lineLimit(1)
    }
    
    var name: String {
        return "\(flight.airline.friendlyName) \(flight.number)"
    }

    var arrives: String {
        let time = DateFormatter.stringRelativeToToday(Date.currentFlightTime, from: flight.arrival)
        if flight.departure == nil {
            return "scheduled to arrive \(time) (not departed)"
        } else if flight.arrival < Date.currentFlightTime {
            return "arrived \(time)"
        } else {
            return "arrives \(time)"
        }
    }

    var origin: String {
        return "from " + (flight.origin.friendlyName)
    }
}

//struct ContentView_Previews: PreviewProvider {
//    static var previews: some View {
//        FlightsEnrouteView(flightSearch: FlightSearch(destination: "KSFO"))
//    }
//}

 

 

 

Enroute.xcdatamodeld

 

 

 

Flight.swift

import CoreData
import Combine

extension Flight { // should probably be Identifiable & Comparable
    @discardableResult
    static func update(from faflight: FAFlight, in context: NSManagedObjectContext) -> Flight {
        let request = fetchRequest(NSPredicate(format: "ident_ = %@", faflight.ident))
        let results = (try? context.fetch(request)) ?? []
        let flight = results.first ?? Flight(context: context)
        flight.ident = faflight.ident
        flight.origin = Airport.withICAO(faflight.origin, context: context)
        flight.destination = Airport.withICAO(faflight.destination, context: context)
        flight.arrival = faflight.arrival
        flight.departure = faflight.departure
        flight.filed = faflight.filed
        flight.aircraft = faflight.aircraft
        flight.airline = Airline.withCode(faflight.airlineCode, in: context)
        flight.objectWillChange.send()
        // might want to save() here
        // Flights are currently only loaded from Airport.fetchIncomingFlights()
        // which saves
        // but it might be nice if this method could stand on its own and save itself
        return flight
    }
    
    static func fetchRequest(_ predicate: NSPredicate) -> NSFetchRequest<Flight> {
        let request = NSFetchRequest<Flight>(entityName: "Flight")
        request.sortDescriptors = [NSSortDescriptor(key: "arrival_", ascending: true)]
        request.predicate = predicate
        return request
    }
    
    var arrival: Date {
        get { arrival_ ?? Date(timeIntervalSinceReferenceDate: 0) }
        set { arrival_ = newValue }
    }
    var ident: String {
        get { ident_ ?? "Unknown" }
        set { ident_ = newValue }
    }
    var destination: Airport {
        get { destination_! } // TODO: protect against nil before shipping?
        set { destination_ = newValue }
    }
    var origin: Airport {
        get { origin_! } // TODO: maybe protect against when app ships?
        set { origin_ = newValue }
    }
    var airline: Airline {
        get { airline_! } // TODO: maybe protect against when app ships?
        set { airline_ = newValue }
    }
    var number: Int {
        Int(String(ident.drop(while: { !$0.isNumber }))) ?? 0
    }
}

 

 

 

Airport.swift

import CoreData
import Combine

extension Airport: Identifiable, Comparable {
    static func withICAO(_ icao: String, context: NSManagedObjectContext) -> Airport {
        // look up icao in Core Data
        let request = fetchRequest(NSPredicate(format: "icao_ = %@", icao))
        let airports = (try? context.fetch(request)) ?? []
        if let airport = airports.first {
            // if found, return it
            return airport
        } else {
            // if not, create one and fetch from FlightAware
            let airport = Airport(context: context)
            airport.icao = icao
            AirportInfoRequest.fetch(icao) { airportInfo in
                self.update(from: airportInfo, context: context)
            }
            return airport
        }
    }
    
    func fetchIncomingFlights() {
        Self.flightAwareRequest?.stopFetching()
        if let context = managedObjectContext {
            Self.flightAwareRequest = EnrouteRequest.create(airport: icao, howMany: 90)
            Self.flightAwareRequest?.fetch(andRepeatEvery: 60)
            Self.flightAwareResultsCancellable = Self.flightAwareRequest?.results.sink { results in
                for faflight in results {
                    Flight.update(from: faflight, in: context)
                }
                do {
                    try context.save()
                } catch(let error) {
                    print("couldn't save flight update to CoreData: \(error.localizedDescription)")
                }
            }
        }
    }

    private static var flightAwareRequest: EnrouteRequest!
    private static var flightAwareResultsCancellable: AnyCancellable?

    static func update(from info: AirportInfo, context: NSManagedObjectContext) {
        if let icao = info.icao {
            let airport = self.withICAO(icao, context: context)
            airport.latitude = info.latitude
            airport.longitude = info.longitude
            airport.name = info.name
            airport.location = info.location
            airport.timezone = info.timezone
            airport.objectWillChange.send()
            airport.flightsTo.forEach { $0.objectWillChange.send() }
            airport.flightsFrom.forEach { $0.objectWillChange.send() }
            try? context.save()
        }
    }

    static func fetchRequest(_ predicate: NSPredicate) -> NSFetchRequest<Airport> {
        let request = NSFetchRequest<Airport>(entityName: "Airport")
        request.sortDescriptors = [NSSortDescriptor(key: "location", ascending: true)]
        request.predicate = predicate
        return request
    }

    var flightsTo: Set<Flight> {
        get { (flightsTo_ as? Set<Flight>) ?? [] }
        set { flightsTo_ = newValue as NSSet }
    }
    var flightsFrom: Set<Flight> {
        get { (flightsFrom_ as? Set<Flight>) ?? [] }
        set { flightsFrom_ = newValue as NSSet }
    }

    var icao: String {
        get { icao_! } // TODO: maybe protect against when app ships?
        set { icao_ = newValue }
    }

    var friendlyName: String {
        let friendly = AirportInfo.friendlyName(name: self.name ?? "", location: self.location ?? "")
        return friendly.isEmpty ? icao : friendly
    }

    public var id: String { icao }

    public static func < (lhs: Airport, rhs: Airport) -> Bool {
        lhs.location ?? lhs.friendlyName < rhs.location ?? rhs.friendlyName
    }
}

 

 

 

Airline.swift

import CoreData
import Combine

extension Airline: Identifiable, Comparable {
    static func withCode(_ code: String, in context: NSManagedObjectContext) -> Airline {
        let request = fetchRequest(NSPredicate(format: "code_ = %@", code))
        let results = (try? context.fetch(request)) ?? []
        if let airline = results.first {
            return airline
        } else {
            let airline = Airline(context: context)
            airline.code = code
            AirlineInfoRequest.fetch(code) { info in
                let airline = self.withCode(code, in: context)
                airline.name = info.name
                airline.shortname = info.shortname
                airline.objectWillChange.send()
                airline.flights.forEach { $0.objectWillChange.send() }
                try? context.save()
            }
            return airline
        }
    }

    static func fetchRequest(_ predicate: NSPredicate) -> NSFetchRequest<Airline> {
        let request = NSFetchRequest<Airline>(entityName: "Airline")
        request.sortDescriptors = [NSSortDescriptor(key: "name_", ascending: true)]
        request.predicate = predicate
        return request
    }
    
    var code: String {
        get { code_! } // TODO: maybe protect against when app ships?
        set { code_ = newValue }
    }
    var name: String {
        get { name_ ?? code }
        set { name_ = newValue }
    }
    var shortname: String {
        get { (shortname_ ?? "").isEmpty ? name : shortname_! }
        set { shortname_ = newValue }
    }
    var flights: Set<Flight> {
        get { (flights_ as? Set<Flight>) ?? [] }
        set { flights_ = newValue as NSSet }
    }
    var friendlyName: String { shortname.isEmpty ? name : shortname }

    public var id: String { code }

    public static func < (lhs: Airline, rhs: Airline) -> Bool {
        lhs.name < rhs.name
    }
}

 

 

 

FlightAwareRequest.swift

import Foundation
import Combine

// very simple scheduled, sequential fetcher of FlightAware data
// using the FlightAware REST API
// just enough to support our demo needs
// has some simple cacheing to make starting/stopping in demo all the time
//  so that it does not overwhelm with FlightAware requests
//  (also, FlightAware requests are not free!)
// also has a simple "simulation mode"
// so that it will "work" when no valid FlightAware credentials exist

// to make this actually fetch from FlightAware
// you need a FlightAware account and an API key
// (fetches are not free, see flightaware.com/api for details)
// put your account name and API key in the Info.plist
// under the key "FlightAware Credentials"
// example credentials: "joepilot:2ab78c93fccc11f999999111030304"
// if that key does not exist, simulation mode automatically kicks in

class FlightAwareRequest<Fetched> where Fetched: Codable, Fetched: Hashable
{
    // this is the latest accumulation of results from fetches
    // this is a CurrentValueSubject
    // a CurrentValueSubject is a Publisher that holds a value
    // and publishes it whenver it changes
    private(set) var results = CurrentValueSubject<Set<Fetched>, Never>([])

    let batchSize = 15
    var offset: Int = 0
    lazy var howMany: Int = batchSize
    private(set) var fetchInterval: TimeInterval = 0

    // MARK: - Subclassers Overrides
    
    var cacheKey: String? { return nil } // nil means no cacheing
    var query: String { "" } // e.g. Enroute?airport=KSFO
    func decode(_ json: Data) -> Set<Fetched> { Set<Fetched>() } // json is JSON received from FlightAware
    func filter(_ results: Set<Fetched>) -> Set<Fetched> { results } // optional filtering of results
    var fetchTimer: Timer? // so that subclasses can throttle fetches of their kind of object

    // MARK: - Private Data
    
    private var captureSimulationData = false
    
    private var urlRequest: URLRequest? { Self.authorizedURLRequest(query: query) }
    private var fetchCancellable: AnyCancellable?
    private var fetchSequenceCount: Int = 0
    
    private var cacheData: Data? { cacheKey != nil ? UserDefaults.standard.data(forKey: cacheKey!) : nil }
    private var cacheTimestampKey: String { (cacheKey ?? "")+".timestamp" }
    private var cacheAge: TimeInterval? {
        let since1970 = UserDefaults.standard.double(forKey: cacheTimestampKey)
        if since1970 > 0 {
            return Date.currentFlightTime.timeIntervalSince1970 - since1970
        } else {
            return nil
        }
    }

    // MARK: - Fetching
    
    // sets the fetchInterval to interval and fetch()es
    func fetch(andRepeatEvery interval: TimeInterval, useCache: Bool? = nil) {
        fetchInterval = interval
        if useCache != nil {
            fetch(useCache: useCache!)
        } else {
            fetch()
        }
    }
    
    // stops fetching
    // fetching can be restarted by calling one of the fetch functions
    func stopFetching() {
        fetchCancellable?.cancel()
        fetchTimer?.invalidate()
        fetchInterval = 0
        fetchSequenceCount = 0
    }
    
    // immediately fetches new data (from cache if available and requested)
    // and, when that data returns, calls handleResults with it
    // (which will schedule the next fetch if appropriate)
    func fetch(useCache: Bool = true) {
        if !useCache || !fetchFromCache() {
            if let urlRequest = self.urlRequest {
                print("fetching \(urlRequest)")
                if offset == 0 { fetchSequenceCount = 0 }
                fetchCancellable = URLSession.shared.dataTaskPublisher(for: urlRequest)
                    .map { [weak self] data, response in
                        if self?.captureSimulationData ?? false {
                            flightSimulationData[self?.query ?? ""] = data.utf8
                        }
                        return self?.decode(data) ?? []
                    }
                    .replaceError(with: [])
                    .receive(on: DispatchQueue.main)
                    .sink { [weak self] results in self?.handleResults(results) }
            } else {
                if let json = flightSimulationData[query]?.data(using: .utf8) {
                    print("simulating \(query)")
                    handleResults(decode(json), isCacheable: false)
                }
            }
        }
    }
    
    // unions the newResults with our existing results.value
    // keeps fetching immediately (1s later) if ...
    //   our results.value.count < howMany
    //   and we haven't done howMany/15 fetches in a row (throttle)
    // otherwise schedules our next fetch after fetchInterval (and caches results)
    private func handleResults(_ newResults: Set<Fetched>, age: TimeInterval = 0, isCacheable: Bool = true) {
        let existingCount = results.value.count
        let newValue = fetchSequenceCount > 0 ? results.value.union(newResults) : newResults.union(results.value)
        let added = newValue.count - existingCount
        results.value = filter(newValue)
        let sequencing = age == 0 && added == batchSize && results.value.count < howMany && fetchSequenceCount < (howMany-(batchSize-1))/batchSize
        let interval = sequencing ? 1 : (age > 0 && age < fetchInterval) ? fetchInterval - age : fetchInterval
        if isCacheable, age == 0, !sequencing {
            cache(newValue)
        }
        if interval > 0 { // }, urlRequest != nil {
            if sequencing {
                fetchSequenceCount += 1
            } else {
                offset = 0
                fetchSequenceCount = 0
            }
            fetchTimer = Timer.scheduledTimer(withTimeInterval: interval, repeats: false, block: { [weak self] timer in
                if (self?.fetchInterval ?? 0) > 0 || (self?.fetchSequenceCount ?? 0) > 0 {
                    self?.fetch()
                }
            })
        }
    }
    
    // MARK: - Cacheing
    
    // this is mostly because, during a demo, we're constantly re-launching the application
    // and there's no need to be refetching data that was just fetched
    // the real solution to this is to make the data persistent
    // (for example, in Core Data)
    
    private func fetchFromCache() -> Bool { // returns whether we were able to
        if fetchSequenceCount == 0, let key = cacheKey, let age = cacheAge {
            if age > 0, (fetchInterval == 0) || (age < fetchInterval) || urlRequest == nil, let data = cacheData {
                if let cachedResults = try? JSONDecoder().decode(Set<Fetched>.self, from: data) {
                    print("using \(Int(age))s old cache \(key)")
                    handleResults(cachedResults, age: age)
                    return true
                } else {
                    print("couldn't decode information from \(Int(age))s old cache \(cacheKey!)")
                }
            }
        }
        return false
    }
    
    private func cache(_ results: Set<Fetched>) {
        if let key = self.cacheKey, let data = try? JSONEncoder().encode(results) {
            print("caching \(key) at \(DateFormatter.short.string(from: Date.currentFlightTime))")
            UserDefaults.standard.set(Date.currentFlightTime.timeIntervalSince1970, forKey: self.cacheTimestampKey)
            UserDefaults.standard.set(data, forKey: key)
        }
    }
    
    // MARK: - Utility
        
    static func authorizedURLRequest(query: String, credentials: String? = Bundle.main.object(forInfoDictionaryKey: "FlightAware Credentials") as? String) -> URLRequest? {
        let flightAware = "https://flightxml.flightaware.com/json/FlightXML2/"
        if let url = URL(string: flightAware + query), let credentials = (credentials?.isEmpty ?? true) ? nil : credentials?.base64 {
            var request = URLRequest(url: url)
            request.setValue("Basic \(credentials)", forHTTPHeaderField: "Authorization")
            return request
        }
        return nil
    }
}

// MARK: - Extensions

extension String {
    mutating func addFlightAwareArgument(_ name: String, _ value: Int? = nil, `default` defaultValue: Int = 0) {
        if value != nil, value != defaultValue {
            addFlightAwareArgument(name, "\(value!)")
        }
    }
    mutating func addFlightAwareArgument(_ name: String, _ value: Date?) {
        if value != nil {
            addFlightAwareArgument(name, "\(Int(value!.timeIntervalSince1970))")
        }
    }
    
    mutating func addFlightAwareArgument(_ name: String, _ value: String?) {
        if value != nil {
            self += (hasSuffix("?") ? "" : "&") + name + "=" + value!
        }
    }
}

// MARK: - Simulation Support

// while simulating, we pretend its the time the simulation data was grabbed

extension Date {
    private static let launch = Date()
        
    static var currentFlightTime: Date {
        let credentials = Bundle.main.object(forInfoDictionaryKey: "FlightAware Credentials") as? String
        if credentials == nil || credentials!.isEmpty, !flightSimulationData.isEmpty, let simulationDate = flightSimulationDate {
            return simulationDate.addingTimeInterval(Date().timeIntervalSince(launch))
        } else {
            return Date()
        }
    }
}

 

 

 

FAFlight.swift

import Foundation

// json decoded directly from what comes back from FlightAware's "Enroute?"

struct FAFlight: Codable, Hashable, Identifiable, Comparable, CustomStringConvertible
{
    private(set) var ident: String
    private(set) var aircraft: String
    
    var number: Int { Int(String(ident.drop(while: { !$0.isNumber }))) ?? 0 }
    var airlineCode: String { String(ident.prefix(while: { !$0.isNumber })) }
    
    var departure: Date? { actualdeparturetime > 0 ? Date(timeIntervalSince1970: TimeInterval(actualdeparturetime)) : nil }
    var arrival: Date { Date(timeIntervalSince1970: TimeInterval(estimatedarrivaltime)) }
    var filed: Date { Date(timeIntervalSince1970: TimeInterval(filed_departuretime)) }
    
    private(set) var destination: String
    private(set) var destinationName: String
    private(set) var destinationCity: String
    
    private(set) var origin: String
    private(set) var originName: String
    private(set) var originCity: String
    
    var originFullName: String {
        let origin = self.origin.first == "K" ? String(self.origin.dropFirst()) : self.origin
        if originName.contains(elementIn: originCity.components(separatedBy: ",")) {
            return origin + " " + originCity
        }
        return origin + " \(originName), \(originCity)"
    }
    
    private enum CodingKeys: String, CodingKey {
        case ident
        case aircraft = "aircrafttype"
        case actualdeparturetime, estimatedarrivaltime, filed_departuretime
        case origin, destination
        case originName, originCity
        case destinationName, destinationCity
    }
    
    private var actualdeparturetime: Int
    private var estimatedarrivaltime: Int
    private var filed_departuretime: Int
    
    var id: String { ident }
    func hash(into hasher: inout Hasher) { hasher.combine(id) }
    static func ==(lhs: FAFlight, rhs: FAFlight) -> Bool { lhs.id == rhs.id }
    
    static func < (lhs: FAFlight, rhs: FAFlight) -> Bool {
        if lhs.arrival < rhs.arrival {
            return true
        } else if rhs.arrival < lhs.arrival {
            return false
        } else {
            return lhs.departure ?? lhs.filed < rhs.departure ?? rhs.filed
        }
    }

    var description: String {
        if let departure = self.departure {
            return "\(ident) departed \(origin) at \(departure) arriving \(arrival)"
        } else {
            return "\(ident) scheduled to depart \(origin) at \(filed) arriving \(arrival)"
        }
    }
}

 

 

 

EnroutRequest.swift

import Foundation
import Combine

// fetches FAFlight objects from FlightAware using "Enroute?"
// (flights enroute to a specified airport)
// generally supports fetching only one airport's enroute flights at a time
// (just to minimize FlightAware API requests)

class EnrouteRequest: FlightAwareRequest<FAFlight>, Codable
{
    private(set) var airport: String!
    
    private static var requests = [String:EnrouteRequest]()
    
    static func create(airport: String, howMany: Int? = nil) -> EnrouteRequest {
        if let request = requests[airport] {
            request.howMany = howMany ?? request.howMany
            return request
        } else {
            let request = EnrouteRequest(airport: airport, howMany: howMany)
            requests[airport] = request
            return request
        }
    }
    
    private init(airport: String, howMany: Int? = nil) {
        super.init()
        self.airport = airport
        if howMany != nil { self.howMany = howMany! }
    }
    
    private static var sharedFetchTimer: Timer?
    
    override var fetchTimer: Timer? {
        get { Self.sharedFetchTimer }
        set {
            Self.sharedFetchTimer?.invalidate()
            Self.sharedFetchTimer = newValue
        }
    }

    override var cacheKey: String? { "\(type(of: self)).\(airport!)" }
    
    override func decode(_ data: Data) -> Set<FAFlight> {
        let result = (try? JSONDecoder().decode(EnrouteRequest.self, from: data))?.flightAwareResult
        offset = result?.next_offset ?? 0
        return Set(result?.enroute ?? [])
    }
    
    override func filter(_ results: Set<FAFlight>) -> Set<FAFlight> {
        results.filter { $0.arrival > Date.currentFlightTime }
    }
    
    override var query: String {
        var request = "Enroute?"
        request.addFlightAwareArgument("airport", airport)
        request.addFlightAwareArgument("howMany", batchSize)
        request.addFlightAwareArgument("filter", "airline")
        request.addFlightAwareArgument("offset", offset)
        return request
    }
    
    private var flightAwareResult: EnrouteResult?
    
    private enum CodingKeys: String, CodingKey {
        case flightAwareResult = "EnrouteResult"
    }
    
    private struct EnrouteResult: Codable {
        var next_offset: Int
        var enroute: [FAFlight]
    }
}

 

 

AirlineInfo.swift

import Foundation
import Combine

// json decoded directly from what comes back from FlightAware's "AirlineInfo?"

struct AirlineInfo: Codable, Hashable, Identifiable, Comparable {
    fileprivate(set) var code: String?
    private(set) var callsign: String
    private(set) var country: String
    private(set) var location: String
    private(set) var name: String
    private(set) var phone: String
    private(set) var shortname: String
    private(set) var url: String
    
    var friendlyName: String { shortname.isEmpty ? (name.isEmpty ? (code ?? "???") : name) : shortname }
    
    var id: String { code ?? callsign }
    func hash(into hasher: inout Hasher) { hasher.combine(id) }
    static func == (lhs: AirlineInfo, rhs: AirlineInfo) -> Bool { lhs.id == rhs.id }
    static func < (lhs: AirlineInfo, rhs: AirlineInfo) -> Bool { lhs.id < rhs.id }
}

// TODO: share code with AirportInfoRequest

class AirlineInfoRequest: FlightAwareRequest<AirlineInfo>, Codable {
    private(set) var airline: String?
    
    static var all: [AirlineInfo] {
        requests.values.compactMap({ $0.results.value.first }).sorted()
    }
    
    var info: AirlineInfo? { results.value.first }

    private static var requests = [String:AirlineInfoRequest]()
    private static var cancellables = [AnyCancellable]()
    
    @discardableResult
    static func fetch(_ airline: String, perform: ((AirlineInfo) -> Void)? = nil) -> AirlineInfo? {
        let request = Self.requests[airline]
        if request == nil {
            Self.requests[airline] = AirlineInfoRequest(airline: airline)
            Self.requests[airline]?.fetch()
            return self.fetch(airline, perform: perform)
        } else if perform != nil {
            if let info = request!.info {
                perform!(info)
            } else {
                request!.results.sink { infos in
                    if let info = infos.first {
                        perform!(info)
                    }
                }.store(in: &Self.cancellables)
            }
        }
        return Self.requests[airline]?.results.value.first
    }
    
    private init(airline: String) {
        super.init()
        self.airline = airline
    }
    
    override var query: String {
        var request = "AirlineInfo?"
        request.addFlightAwareArgument("airlineCode", airline)
        return request
    }
    
    override var cacheKey: String? { "\(type(of: self)).\(airline!)" }

    override func decode(_ data: Data) -> Set<AirlineInfo> {
        var result = (try? JSONDecoder().decode(AirlineInfoRequest.self, from: data))?.flightAwareResult
        result?.code = airline
        return Set(result == nil ? [] : [result!])
    }

    private var flightAwareResult: AirlineInfo?

    private enum CodingKeys: String, CodingKey {
        case flightAwareResult = "AirlineInfoResult"
    }
}

 

 

 

AirportInfo.swift

import Foundation
import Combine

// json decoded directly from what comes back from FlightAware's "AirportInfo?"

struct AirportInfo: Codable, Hashable, Identifiable, Comparable {
    fileprivate(set) var icao: String?
    private(set) var latitude: Double
    private(set) var longitude: Double
    private(set) var location: String
    private(set) var name: String
    private(set) var timezone: String
    
    var friendlyName: String {
        Self.friendlyName(name: name, location: location)
    }
    
    static func friendlyName(name: String, location: String) -> String {
        var shortName = name
            .replacingOccurrences(of: " Intl", with: " ")
            .replacingOccurrences(of: " Int'l", with: " ")
            .replacingOccurrences(of: "Intl ", with: " ")
            .replacingOccurrences(of: "Int'l ", with: " ")
        for nameComponent in location.components(separatedBy: ",").map({ $0.trim }) {
            shortName = shortName
                .replacingOccurrences(of: nameComponent+" ", with: " ")
                .replacingOccurrences(of: " "+nameComponent, with: " ")
        }
        shortName = shortName.trim
        shortName = shortName.components(separatedBy: CharacterSet.whitespaces).joined(separator: " ")
        if !shortName.isEmpty {
            return "\(shortName), \(location)"
        } else {
            return location
        }
    }

    var id: String { icao ?? name }
    func hash(into hasher: inout Hasher) { hasher.combine(id) }
    static func == (lhs: AirportInfo, rhs: AirportInfo) -> Bool { lhs.id == rhs.id }
    static func < (lhs: AirportInfo, rhs: AirportInfo) -> Bool { lhs.id < rhs.id }
}

// TODO: share code with AirlineInfoRequest

class AirportInfoRequest: FlightAwareRequest<AirportInfo>, Codable {
    private(set) var airport: String?
    
    static var all: [AirportInfo] {
        requests.values.compactMap({ $0.results.value.first }).sorted()
    }
    
    var info: AirportInfo? { results.value.first }

    private static var requests = [String:AirportInfoRequest]()
    private static var cancellables = [AnyCancellable]()
    
    @discardableResult
    static func fetch(_ airport: String, perform: ((AirportInfo) -> Void)? = nil) -> AirportInfo? {
        let request = Self.requests[airport]
        if request == nil {
            Self.requests[airport] = AirportInfoRequest(airport: airport)
            Self.requests[airport]?.fetch()
            return self.fetch(airport, perform: perform)
        } else if perform != nil {
            if let info = request!.info {
                perform!(info)
            } else {
                request!.results.sink { infos in
                    if let info = infos.first {
                        perform!(info)
                    }
                }.store(in: &Self.cancellables)
            }
        }
        return Self.requests[airport]?.results.value.first
    }
    
    private init(airport: String) {
        super.init()
        self.airport = airport
    }
    
    override var query: String {
        var request = "AirportInfo?"
        request.addFlightAwareArgument("airportCode", airport)
        return request
    }
    
    override var cacheKey: String? { "\(type(of: self)).\(airport!)" }

    override func decode(_ data: Data) -> Set<AirportInfo> {
        var result = (try? JSONDecoder().decode(AirportInfoRequest.self, from: data))?.flightAwareResult
        result?.icao = airport
        return Set(result == nil ? [] : [result!])
    }

    private var flightAwareResult: AirportInfo?

    private enum CodingKeys: String, CodingKey {
        case flightAwareResult = "AirportInfoResult"
    }
}

 

 

 

FlightSimulationData.swift

import Foundation

let flightSimulationDate = DateFormatter.short.date(from: "5/9/20, 3:35 PM")

var flightSimulationData = ["AirportInfo?airportCode=ZSPD": "{\"AirportInfoResult\":{\"name\":\"Shanghai Pudong Int\'l\",\"location\":\"Shanghai\",\"longitude\":121.792367,\"latitude\":31.142797,\"timezone\":\":Asia/Shanghai\"}}\n", "AirportInfo?airportCode=KPSP": "{\"AirportInfoResult\":{\"name\":\"Palm Springs Intl\",\"location\":\"Palm Springs, CA\",\"longitude\":-116.5066944,\"latitude\":33.8296667,\"timezone\":\":America/Los_Angeles\"}}\n", "AirportInfo?airportCode=KBUR": "{\"AirportInfoResult\":{\"name\":\"Bob Hope\",\"location\":\"Burbank, CA\",\"longitude\":-118.3586667,\"latitude\":34.2006944,\"timezone\":\":America/Los_Angeles\"}}\n", "AirportInfo?airportCode=KMKE": "{\"AirportInfoResult\":{\"name\":\"Milwaukee Mitchell Intl Airport\",\"location\":\"Milwaukee, WI\",\"longitude\":-87.8970556,\"latitude\":42.9469444,\"timezone\":\":America/Chicago\"}}\n", "AirportInfo?airportCode=KPHX": "{\"AirportInfoResult\":{\"name\":\"Phoenix Sky Harbor Intl\",\"location\":\"Phoenix, AZ\",\"longitude\":-112.0115833,\"latitude\":33.4342778,\"timezone\":\":America/Phoenix\"}}\n", "AirportInfo?airportCode=KORD": "{\"AirportInfoResult\":{\"name\":\"Chicago O\'Hare Intl\",\"location\":\"Chicago, IL\",\"longitude\":-87.9065972,\"latitude\":41.9745219,\"timezone\":\":America/Chicago\"}}\n", "AirportInfo?airportCode=KSAN": "{\"AirportInfoResult\":{\"name\":\"San Diego Intl\",\"location\":\"San Diego, CA\",\"longitude\":-117.1896667,\"latitude\":32.7335556,\"timezone\":\":America/Los_Angeles\"}}\n", "AirportInfo?airportCode=KSNA": "{\"AirportInfoResult\":{\"name\":\"John Wayne\",\"location\":\"Santa Ana, CA\",\"longitude\":-117.8682222,\"latitude\":33.6756667,\"timezone\":\":America/Los_Angeles\"}}\n", "AirportInfo?airportCode=KBNA": "{\"AirportInfoResult\":{\"name\":\"Nashville Intl\",\"location\":\"Nashville, TN\",\"longitude\":-86.6781667,\"latitude\":36.1244722,\"timezone\":\":America/Chicago\"}}\n", "AirportInfo?airportCode=KRNO": "{\"AirportInfoResult\":{\"name\":\"Reno/Tahoe Intl\",\"location\":\"Reno, NV\",\"longitude\":-119.7681111,\"latitude\":39.4991111,\"timezone\":\":America/Los_Angeles\"}}\n", "AirportInfo?airportCode=KDFW": "{\"AirportInfoResult\":{\"name\":\"Dallas-Fort Worth Intl\",\"location\":\"Dallas-Fort Worth, TX\",\"longitude\":-97.0376949,\"latitude\":32.8972316,\"timezone\":\":America/Chicago\"}}\n", "AirportInfo?airportCode=KSMF": "{\"AirportInfoResult\":{\"name\":\"Sacramento Intl\",\"location\":\"Sacramento, CA\",\"longitude\":-121.5907778,\"latitude\":38.6954444,\"timezone\":\":America/Los_Angeles\"}}\n", "AirportInfo?airportCode=KAUS": "{\"AirportInfoResult\":{\"name\":\"Austin-Bergstrom Intl\",\"location\":\"Austin, TX\",\"longitude\":-97.6698761,\"latitude\":30.1945272,\"timezone\":\":America/Chicago\"}}\n", "AirportInfo?airportCode=KEWR": "{\"AirportInfoResult\":{\"name\":\"Newark Liberty Intl\",\"location\":\"Newark, NJ\",\"longitude\":-74.1686868,\"latitude\":40.6924798,\"timezone\":\":America/New_York\"}}\n", "AirportInfo?airportCode=KBOS": "{\"AirportInfoResult\":{\"name\":\"Boston Logan Intl\",\"location\":\"Boston, MA\",\"longitude\":-71.0063889,\"latitude\":42.3629444,\"timezone\":\":America/New_York\"}}\n", "AirlineInfo?airlineCode=BAW": "{\"AirlineInfoResult\":{\"name\":\"British Airways\",\"shortname\":\"British Airways\",\"callsign\":\"Speedbird\",\"location\":\"United Kingdom\",\"country\":\"United Kingdom\",\"url\":\"http://www.british-airways.com/\",\"phone\":\"+1-800-247-9297\"}}\n", "AirlineInfo?airlineCode=FDX": "{\"AirlineInfoResult\":{\"name\":\"Federal Express Corporation\",\"shortname\":\"FedEx\",\"callsign\":\"FedEx\",\"location\":\"Memphis, TN\",\"country\":\"United States\",\"url\":\"http://www.fedex.com/\",\"phone\":\"\"}}\n", "AirportInfo?airportCode=KGEG": "{\"AirportInfoResult\":{\"name\":\"Spokane Intl\",\"location\":\"Spokane, WA\",\"longitude\":-117.5352222,\"latitude\":47.6190278,\"timezone\":\":America/Los_Angeles\"}}\n", "AirportInfo?airportCode=KMDW": "{\"AirportInfoResult\":{\"name\":\"Chicago Midway Intl\",\"location\":\"Chicago, IL\",\"longitude\":-87.7524167,\"latitude\":41.7859722,\"timezone\":\":America/Chicago\"}}\n", "Enroute?airport=KLAS&howMany=15&filter=airline&offset=15": "{\"EnrouteResult\":{\"next_offset\":30,\"enroute\":[{\"ident\":\"FFT773\",\"aircrafttype\":\"A20N\",\"actualdeparturetime\":0,\"estimatedarrivaltime\":1589072100,\"filed_departuretime\":1589066700,\"origin\":\"KDEN\",\"destination\":\"KLAS\",\"originName\":\"Denver Intl\",\"originCity\":\"Denver, CO\",\"destinationName\":\"McCarran Intl\",\"destinationCity\":\"Las Vegas, NV\"},{\"ident\":\"SWA1705\",\"aircrafttype\":\"B737\",\"actualdeparturetime\":0,\"estimatedarrivaltime\":1589072160,\"filed_departuretime\":1589066100,\"origin\":\"KPDX\",\"destination\":\"KLAS\",\"originName\":\"Portland Intl\",\"originCity\":\"Portland, OR\",\"destinationName\":\"McCarran Intl\",\"destinationCity\":\"Las Vegas, NV\"},{\"ident\":\"SWA1152\",\"aircrafttype\":\"B738\",\"actualdeparturetime\":1589056920,\"estimatedarrivaltime\":1589072220,\"filed_departuretime\":1589057100,\"origin\":\"KATL\",\"destination\":\"KLAS\",\"originName\":\"Hartsfield-Jackson Intl\",\"originCity\":\"Atlanta, GA\",\"destinationName\":\"McCarran Intl\",\"destinationCity\":\"Las Vegas, NV\"},{\"ident\":\"SWA1348\",\"aircrafttype\":\"B738\",\"actualdeparturetime\":1589059078,\"estimatedarrivaltime\":1589072460,\"filed_departuretime\":1589058900,\"origin\":\"KBNA\",\"destination\":\"KLAS\",\"originName\":\"Nashville Intl\",\"originCity\":\"Nashville, TN\",\"destinationName\":\"McCarran Intl\",\"destinationCity\":\"Las Vegas, NV\"},{\"ident\":\"AAL9607\",\"aircrafttype\":\"A320\",\"actualdeparturetime\":1589055129,\"estimatedarrivaltime\":1589072760,\"filed_departuretime\":1589054400,\"origin\":\"KCLT\",\"destination\":\"KLAS\",\"originName\":\"Charlotte/Douglas Intl\",\"originCity\":\"Charlotte, NC\",\"destinationName\":\"McCarran Intl\",\"destinationCity\":\"Las Vegas, NV\"},{\"ident\":\"SWA1313\",\"aircrafttype\":\"B737\",\"actualdeparturetime\":1589063452,\"estimatedarrivaltime\":1589072760,\"filed_departuretime\":1589063100,\"origin\":\"KOMA\",\"destination\":\"KLAS\",\"originName\":\"Eppley Airfield\",\"originCity\":\"Omaha, NE\",\"destinationName\":\"McCarran Intl\",\"destinationCity\":\"Las Vegas, NV\"},{\"ident\":\"SWA381\",\"aircrafttype\":\"B738\",\"actualdeparturetime\":1589063501,\"estimatedarrivaltime\":1589073000,\"filed_departuretime\":1589063700,\"origin\":\"KAUS\",\"destination\":\"KLAS\",\"originName\":\"Austin-Bergstrom Intl\",\"originCity\":\"Austin, TX\",\"destinationName\":\"McCarran Intl\",\"destinationCity\":\"Las Vegas, NV\"},{\"ident\":\"PCM7798\",\"aircrafttype\":\"C208\",\"actualdeparturetime\":0,\"estimatedarrivaltime\":1589073351,\"filed_departuretime\":1589070240,\"origin\":\"KSGU\",\"destination\":\"KLAS\",\"originName\":\"St George Rgnl\",\"originCity\":\"St George, UT\",\"destinationName\":\"McCarran Intl\",\"destinationCity\":\"Las Vegas, NV\"},{\"ident\":\"SWA444\",\"aircrafttype\":\"B737\",\"actualdeparturetime\":1589063498,\"estimatedarrivaltime\":1589073600,\"filed_departuretime\":1589064000,\"origin\":\"KMCI\",\"destination\":\"KLAS\",\"originName\":\"Kansas City Intl\",\"originCity\":\"Kansas City, MO\",\"destinationName\":\"McCarran Intl\",\"destinationCity\":\"Las Vegas, NV\"},{\"ident\":\"NKS1199\",\"aircrafttype\":\"A319\",\"actualdeparturetime\":1589063215,\"estimatedarrivaltime\":1589073660,\"filed_departuretime\":1589064300,\"origin\":\"KIAH\",\"destination\":\"KLAS\",\"originName\":\"Houston Bush Int\'ctl\",\"originCity\":\"Houston, TX\",\"destinationName\":\"McCarran Intl\",\"destinationCity\":\"Las Vegas, NV\"},{\"ident\":\"AAL627\",\"aircrafttype\":\"A321\",\"actualdeparturetime\":1589057334,\"estimatedarrivaltime\":1589073780,\"filed_departuretime\":1589057400,\"origin\":\"KCLT\",\"destination\":\"KLAS\",\"originName\":\"Charlotte/Douglas Intl\",\"originCity\":\"Charlotte, NC\",\"destinationName\":\"McCarran Intl\",\"destinationCity\":\"Las Vegas, NV\"},{\"ident\":\"NKS297\",\"aircrafttype\":\"A319\",\"actualdeparturetime\":1589059452,\"estimatedarrivaltime\":1589073900,\"filed_departuretime\":1589058840,\"origin\":\"KDTW\",\"destination\":\"KLAS\",\"originName\":\"Detroit Metro Wayne Co\",\"originCity\":\"Detroit, MI\",\"destinationName\":\"McCarran Intl\",\"destinationCity\":\"Las Vegas, NV\"},{\"ident\":\"SWA1808\",\"aircrafttype\":\"B737\",\"actualdeparturetime\":1589062012,\"estimatedarrivaltime\":1589074200,\"filed_departuretime\":1589062500,\"origin\":\"KMDW\",\"destination\":\"KLAS\",\"originName\":\"Chicago Midway Intl\",\"originCity\":\"Chicago, IL\",\"destinationName\":\"McCarran Intl\",\"destinationCity\":\"Las Vegas, NV\"},{\"ident\":\"FDX541\",\"aircrafttype\":\"B763\",\"actualdeparturetime\":1589062880,\"estimatedarrivaltime\":1589074560,\"filed_departuretime\":1589059440,\"origin\":\"KMEM\",\"destination\":\"KLAS\",\"originName\":\"Memphis Intl\",\"originCity\":\"Memphis, TN\",\"destinationName\":\"McCarran Intl\",\"destinationCity\":\"Las Vegas, NV\"},{\"ident\":\"SWA186\",\"aircrafttype\":\"B737\",\"actualdeparturetime\":0,\"estimatedarrivaltime\":1589075100,\"filed_departuretime\":1589072400,\"origin\":\"KLAX\",\"destination\":\"KLAS\",\"originName\":\"Los Angeles Intl\",\"originCity\":\"Los Angeles, CA\",\"destinationName\":\"McCarran Intl\",\"destinationCity\":\"Las Vegas, NV\"}]}}\n", "AirlineInfo?airlineCode=SWA": "{\"AirlineInfoResult\":{\"name\":\"Southwest Airlines Co.\",\"shortname\":\"Southwest\",\"callsign\":\"Southwest\",\"location\":\"Dallas, TX\",\"country\":\"United States\",\"url\":\"http://www.southwest.com/\",\"phone\":\"+1-800-435-9792\"}}\n", "AirportInfo?airportCode=KDTW": "{\"AirportInfoResult\":{\"name\":\"Detroit Metro Wayne Co\",\"location\":\"Detroit, MI\",\"longitude\":-83.3533889,\"latitude\":42.2124444,\"timezone\":\":America/New_York\"}}\n", "AirlineInfo?airlineCode=EVA": "{\"AirlineInfoResult\":{\"name\":\"EVA Airways\",\"shortname\":\"EVA Air\",\"callsign\":\"Eva\",\"location\":\"Taoyuan\",\"country\":\"Taiwan\",\"url\":\"http://www.evaair.com/html/b2c/english/\",\"phone\":\"+1-800-695-1188\"}}\n", "AirportInfo?airportCode=KOMA": "{\"AirportInfoResult\":{\"name\":\"Eppley Airfield\",\"location\":\"Omaha, NE\",\"longitude\":-95.8940556,\"latitude\":41.3031667,\"timezone\":\":America/Chicago\"}}\n", "AirportInfo?airportCode=EGLL": "{\"AirportInfoResult\":{\"name\":\"London Heathrow\",\"location\":\"London, England\",\"longitude\":-0.461389,\"latitude\":51.4775,\"timezone\":\":Europe/London\"}}\n", "AirportInfo?airportCode=KEUG": "{\"AirportInfoResult\":{\"name\":\"Mahlon Sweet Field\",\"location\":\"Eugene, OR\",\"longitude\":-123.2119722,\"latitude\":44.1245833,\"timezone\":\":America/Los_Angeles\"}}\n", "AirportInfo?airportCode=KRDM": "{\"AirportInfoResult\":{\"name\":\"Roberts Field\",\"location\":\"Redmond, OR\",\"longitude\":-121.1499714,\"latitude\":44.2540689,\"timezone\":\":America/Los_Angeles\"}}\n", "AirportInfo?airportCode=KATL": "{\"AirportInfoResult\":{\"name\":\"Hartsfield-Jackson Intl\",\"location\":\"Atlanta, GA\",\"longitude\":-84.427864,\"latitude\":33.6366996,\"timezone\":\":America/New_York\"}}\n", "AirportInfo?airportCode=KONT": "{\"AirportInfoResult\":{\"name\":\"Ontario Intl\",\"location\":\"Ontario, CA\",\"longitude\":-117.6011944,\"latitude\":34.056,\"timezone\":\":America/Los_Angeles\"}}\n", "Enroute?airport=KLAS&howMany=15&filter=airline": "{\"EnrouteResult\":{\"next_offset\":15,\"enroute\":[{\"ident\":\"AAL1203\",\"aircrafttype\":\"B738\",\"actualdeparturetime\":0,\"estimatedarrivaltime\":1589062500,\"filed_departuretime\":1589052600,\"origin\":\"KDFW\",\"destination\":\"KLAS\",\"originName\":\"Dallas-Fort Worth Intl\",\"originCity\":\"Dallas-Fort Worth, TX\",\"destinationName\":\"McCarran Intl\",\"destinationCity\":\"Las Vegas, NV\"},{\"ident\":\"AAL9660\",\"aircrafttype\":\"B738\",\"actualdeparturetime\":1589055596,\"estimatedarrivaltime\":1589064780,\"filed_departuretime\":1589054400,\"origin\":\"KDFW\",\"destination\":\"KLAS\",\"originName\":\"Dallas-Fort Worth Intl\",\"originCity\":\"Dallas-Fort Worth, TX\",\"destinationName\":\"McCarran Intl\",\"destinationCity\":\"Las Vegas, NV\"},{\"ident\":\"SWA4478\",\"aircrafttype\":\"B737\",\"actualdeparturetime\":1589058240,\"estimatedarrivaltime\":1589064840,\"filed_departuretime\":1589058300,\"origin\":\"KGEG\",\"destination\":\"KLAS\",\"originName\":\"Spokane Intl\",\"originCity\":\"Spokane, WA\",\"destinationName\":\"McCarran Intl\",\"destinationCity\":\"Las Vegas, NV\"},{\"ident\":\"SWA1462\",\"aircrafttype\":\"B737\",\"actualdeparturetime\":1589052806,\"estimatedarrivaltime\":1589064960,\"filed_departuretime\":1589049900,\"origin\":\"KMKE\",\"destination\":\"KLAS\",\"originName\":\"Milwaukee Mitchell Intl Airport\",\"originCity\":\"Milwaukee, WI\",\"destinationName\":\"McCarran Intl\",\"destinationCity\":\"Las Vegas, NV\"},{\"ident\":\"SWA1097\",\"aircrafttype\":\"B738\",\"actualdeparturetime\":1589061406,\"estimatedarrivaltime\":1589065380,\"filed_departuretime\":1589061600,\"origin\":\"KSMF\",\"destination\":\"KLAS\",\"originName\":\"Sacramento Intl\",\"originCity\":\"Sacramento, CA\",\"destinationName\":\"McCarran Intl\",\"destinationCity\":\"Las Vegas, NV\"},{\"ident\":\"SWA393\",\"aircrafttype\":\"B737\",\"actualdeparturetime\":1589063354,\"estimatedarrivaltime\":1589065980,\"filed_departuretime\":1589063400,\"origin\":\"KPHX\",\"destination\":\"KLAS\",\"originName\":\"Phoenix Sky Harbor Intl\",\"originCity\":\"Phoenix, AZ\",\"destinationName\":\"McCarran Intl\",\"destinationCity\":\"Las Vegas, NV\"},{\"ident\":\"SWA1744\",\"aircrafttype\":\"B737\",\"actualdeparturetime\":1589055060,\"estimatedarrivaltime\":1589067480,\"filed_departuretime\":1589055300,\"origin\":\"KMDW\",\"destination\":\"KLAS\",\"originName\":\"Chicago Midway Intl\",\"originCity\":\"Chicago, IL\",\"destinationName\":\"McCarran Intl\",\"destinationCity\":\"Las Vegas, NV\"},{\"ident\":\"SWA424\",\"aircrafttype\":\"B738\",\"actualdeparturetime\":1589062335,\"estimatedarrivaltime\":1589067660,\"filed_departuretime\":1589062500,\"origin\":\"KDEN\",\"destination\":\"KLAS\",\"originName\":\"Denver Intl\",\"originCity\":\"Denver, CO\",\"destinationName\":\"McCarran Intl\",\"destinationCity\":\"Las Vegas, NV\"},{\"ident\":\"SWA2093\",\"aircrafttype\":\"B737\",\"actualdeparturetime\":1589061459,\"estimatedarrivaltime\":1589068860,\"filed_departuretime\":1589061900,\"origin\":\"KSEA\",\"destination\":\"KLAS\",\"originName\":\"Seattle-Tacoma Intl\",\"originCity\":\"Seattle, WA\",\"destinationName\":\"McCarran Intl\",\"destinationCity\":\"Las Vegas, NV\"},{\"ident\":\"AAL2609\",\"aircrafttype\":\"B738\",\"actualdeparturetime\":0,\"estimatedarrivaltime\":1589069820,\"filed_departuretime\":1589059800,\"origin\":\"KDFW\",\"destination\":\"KLAS\",\"originName\":\"Dallas-Fort Worth Intl\",\"originCity\":\"Dallas-Fort Worth, TX\",\"destinationName\":\"McCarran Intl\",\"destinationCity\":\"Las Vegas, NV\"},{\"ident\":\"NKS245\",\"aircrafttype\":\"A319\",\"actualdeparturetime\":1589058555,\"estimatedarrivaltime\":1589070840,\"filed_departuretime\":1589058600,\"origin\":\"KORD\",\"destination\":\"KLAS\",\"originName\":\"Chicago O\'Hare Intl\",\"originCity\":\"Chicago, IL\",\"destinationName\":\"McCarran Intl\",\"destinationCity\":\"Las Vegas, NV\"},{\"ident\":\"SWA1016\",\"aircrafttype\":\"B738\",\"actualdeparturetime\":1589057776,\"estimatedarrivaltime\":1589071020,\"filed_departuretime\":1589058300,\"origin\":\"KIND\",\"destination\":\"KLAS\",\"originName\":\"Indianapolis Intl\",\"originCity\":\"Indianapolis, IN\",\"destinationName\":\"McCarran Intl\",\"destinationCity\":\"Las Vegas, NV\"},{\"ident\":\"SWA1525\",\"aircrafttype\":\"B737\",\"actualdeparturetime\":0,\"estimatedarrivaltime\":1589071800,\"filed_departuretime\":1589068800,\"origin\":\"KSNA\",\"destination\":\"KLAS\",\"originName\":\"John Wayne\",\"originCity\":\"Santa Ana, CA\",\"destinationName\":\"McCarran Intl\",\"destinationCity\":\"Las Vegas, NV\"},{\"ident\":\"FDX590\",\"aircrafttype\":\"B752\",\"actualdeparturetime\":0,\"estimatedarrivaltime\":1589071860,\"filed_departuretime\":1589058840,\"origin\":\"KMEM\",\"destination\":\"KLAS\",\"originName\":\"Memphis Intl\",\"originCity\":\"Memphis, TN\",\"destinationName\":\"McCarran Intl\",\"destinationCity\":\"Las Vegas, NV\"}]}}\n", "AirportInfo?airportCode=EHAM": "{\"AirportInfoResult\":{\"name\":\"Amsterdam Schiphol\",\"location\":\"Amsterdam\",\"longitude\":4.763889,\"latitude\":52.308613,\"timezone\":\":Europe/Amsterdam\"}}\n", "AirportInfo?airportCode=KIAH": "{\"AirportInfoResult\":{\"name\":\"Houston Bush Int\'ctl\",\"location\":\"Houston, TX\",\"longitude\":-95.3414425,\"latitude\":29.9844353,\"timezone\":\":America/Chicago\"}}\n", "AirportInfo?airportCode=KACV": "{\"AirportInfoResult\":{\"name\":\"California Redwood Coast-Humboldt County\",\"location\":\"Arcata/Eureka, CA\",\"longitude\":-124.1084722,\"latitude\":40.9778333,\"timezone\":\":America/Los_Angeles\"}}\n", "AirportInfo?airportCode=KBOI": "{\"AirportInfoResult\":{\"name\":\"Gowen Field\",\"location\":\"Boise, ID\",\"longitude\":-116.2228611,\"latitude\":43.5643611,\"timezone\":\":America/Denver\"}}\n", "AirportInfo?airportCode=KSBP": "{\"AirportInfoResult\":{\"name\":\"San Luis County Rgnl\",\"location\":\"San Luis Obispo, CA\",\"longitude\":-120.6426111,\"latitude\":35.2372778,\"timezone\":\":America/Los_Angeles\"}}\n", "AirlineInfo?airlineCode=UAL": "{\"AirlineInfoResult\":{\"name\":\"United Air Lines Inc.\",\"shortname\":\"United\",\"callsign\":\"United\",\"location\":\"\",\"country\":\"United States\",\"url\":\"http://www.united.com/\",\"phone\":\"1-800-864-8331\"}}\n", "AirportInfo?airportCode=KCLT": "{\"AirportInfoResult\":{\"name\":\"Charlotte/Douglas Intl\",\"location\":\"Charlotte, NC\",\"longitude\":-80.9490556,\"latitude\":35.21375,\"timezone\":\":America/New_York\"}}\n", "AirlineInfo?airlineCode=CPA": "{\"AirlineInfoResult\":{\"name\":\"Cathay Pacific Airways Ltd.\",\"shortname\":\"Cathay Pacific\",\"callsign\":\"Cathay\",\"location\":\"China\",\"country\":\"China\",\"url\":\"http://www.cathaypacific.com/cpa/en_INTL/homepage\",\"phone\":\"+1-800-233-2742\"}}\n", "AirlineInfo?airlineCode=SKW": "{\"AirlineInfoResult\":{\"name\":\"SkyWest Airlines\",\"shortname\":\"SkyWest\",\"callsign\":\"SkyWest\",\"location\":\"St. George, UT\",\"country\":\"United States\",\"url\":\"http://www.skywest.com/\",\"phone\":\"\"}}\n", "AirportInfo?airportCode=KMCI": "{\"AirportInfoResult\":{\"name\":\"Kansas City Intl\",\"location\":\"Kansas City, MO\",\"longitude\":-94.7138889,\"latitude\":39.2976111,\"timezone\":\":America/Chicago\"}}\n", "AirportInfo?airportCode=RCTP": "{\"AirportInfoResult\":{\"name\":\"Taiwan Taoyuan Int\'l\",\"location\":\"Taipei\",\"longitude\":121.232822,\"latitude\":25.077731,\"timezone\":\":Asia/Taipei\"}}\n", "AirportInfo?airportCode=VHHH": "{\"AirportInfoResult\":{\"name\":\"Hong Kong Int\'l\",\"location\":\"Hong Kong\",\"longitude\":113.914603,\"latitude\":22.308919,\"timezone\":\":Asia/Hong_Kong\"}}\n", "Enroute?airport=KSFO&howMany=15&filter=airline&offset=15": "{\"EnrouteResult\":{\"next_offset\":30,\"enroute\":[{\"ident\":\"SKW5984\",\"aircrafttype\":\"E75L\",\"actualdeparturetime\":0,\"estimatedarrivaltime\":1589065260,\"filed_departuretime\":1589064860,\"origin\":\"KMRY\",\"destination\":\"KSFO\",\"originName\":\"Monterey Rgnl\",\"originCity\":\"Monterey, CA\",\"destinationName\":\"San Francisco Intl\",\"destinationCity\":\"San Francisco, CA\"},{\"ident\":\"SKW5257\",\"aircrafttype\":\"CRJ7\",\"actualdeparturetime\":1589061276,\"estimatedarrivaltime\":1589065260,\"filed_departuretime\":1589062200,\"origin\":\"KEUG\",\"destination\":\"KSFO\",\"originName\":\"Mahlon Sweet Field\",\"originCity\":\"Eugene, OR\",\"destinationName\":\"San Francisco Intl\",\"destinationCity\":\"San Francisco, CA\"},{\"ident\":\"SKW5362\",\"aircrafttype\":\"CRJ7\",\"actualdeparturetime\":1589062501,\"estimatedarrivaltime\":1589065380,\"filed_departuretime\":1589063100,\"origin\":\"KBUR\",\"destination\":\"KSFO\",\"originName\":\"Bob Hope\",\"originCity\":\"Burbank, CA\",\"destinationName\":\"San Francisco Intl\",\"destinationCity\":\"San Francisco, CA\"},{\"ident\":\"BAW287\",\"aircrafttype\":\"B789\",\"actualdeparturetime\":1589029604,\"estimatedarrivaltime\":1589065661,\"filed_departuretime\":1589029509,\"origin\":\"EGLL\",\"destination\":\"KSFO\",\"originName\":\"London Heathrow\",\"originCity\":\"London, England\",\"destinationName\":\"San Francisco Intl\",\"destinationCity\":\"San Francisco, CA\"},{\"ident\":\"SKW5214\",\"aircrafttype\":\"CRJ2\",\"actualdeparturetime\":1589063099,\"estimatedarrivaltime\":1589065680,\"filed_departuretime\":1589064000,\"origin\":\"KACV\",\"destination\":\"KSFO\",\"originName\":\"California Redwood Coast-Humboldt County\",\"originCity\":\"Arcata/Eureka, CA\",\"destinationName\":\"San Francisco Intl\",\"destinationCity\":\"San Francisco, CA\"},{\"ident\":\"CPA892\",\"aircrafttype\":\"B77W\",\"actualdeparturetime\":1589022913,\"estimatedarrivaltime\":1589065814,\"filed_departuretime\":1589022900,\"origin\":\"VHHH\",\"destination\":\"KSFO\",\"originName\":\"Hong Kong Int\'l\",\"originCity\":\"Hong Kong\",\"destinationName\":\"San Francisco Intl\",\"destinationCity\":\"San Francisco, CA\"},{\"ident\":\"SKW5589\",\"aircrafttype\":\"E75L\",\"actualdeparturetime\":1589061213,\"estimatedarrivaltime\":1589065860,\"filed_departuretime\":1589061000,\"origin\":\"KBOI\",\"destination\":\"KSFO\",\"originName\":\"Gowen Field\",\"originCity\":\"Boise, ID\",\"destinationName\":\"San Francisco Intl\",\"destinationCity\":\"San Francisco, CA\"},{\"ident\":\"UAL202\",\"aircrafttype\":\"A319\",\"actualdeparturetime\":1589044260,\"estimatedarrivaltime\":1589066040,\"filed_departuretime\":1589044200,\"origin\":\"KBOS\",\"destination\":\"KSFO\",\"originName\":\"Boston Logan Intl\",\"originCity\":\"Boston, MA\",\"destinationName\":\"San Francisco Intl\",\"destinationCity\":\"San Francisco, CA\"},{\"ident\":\"AAL2683\",\"aircrafttype\":\"B738\",\"actualdeparturetime\":1589053977,\"estimatedarrivaltime\":1589066160,\"filed_departuretime\":1589054340,\"origin\":\"KDFW\",\"destination\":\"KSFO\",\"originName\":\"Dallas-Fort Worth Intl\",\"originCity\":\"Dallas-Fort Worth, TX\",\"destinationName\":\"San Francisco Intl\",\"destinationCity\":\"San Francisco, CA\"},{\"ident\":\"SKW5395\",\"aircrafttype\":\"CRJ2\",\"actualdeparturetime\":0,\"estimatedarrivaltime\":1589066280,\"filed_departuretime\":1589065200,\"origin\":\"KFAT\",\"destination\":\"KSFO\",\"originName\":\"Fresno Yosemite Intl\",\"originCity\":\"Fresno, CA\",\"destinationName\":\"San Francisco Intl\",\"destinationCity\":\"San Francisco, CA\"},{\"ident\":\"SWA1124\",\"aircrafttype\":\"B738\",\"actualdeparturetime\":1589062561,\"estimatedarrivaltime\":1589066340,\"filed_departuretime\":1589063100,\"origin\":\"KSAN\",\"destination\":\"KSFO\",\"originName\":\"San Diego Intl\",\"originCity\":\"San Diego, CA\",\"destinationName\":\"San Francisco Intl\",\"destinationCity\":\"San Francisco, CA\"},{\"ident\":\"SKW5678\",\"aircrafttype\":\"E75L\",\"actualdeparturetime\":1589063168,\"estimatedarrivaltime\":1589066520,\"filed_departuretime\":1589063400,\"origin\":\"KSNA\",\"destination\":\"KSFO\",\"originName\":\"John Wayne\",\"originCity\":\"Santa Ana, CA\",\"destinationName\":\"San Francisco Intl\",\"destinationCity\":\"San Francisco, CA\"},{\"ident\":\"SKW5329\",\"aircrafttype\":\"CRJ2\",\"actualdeparturetime\":1589062697,\"estimatedarrivaltime\":1589066580,\"filed_departuretime\":1589062200,\"origin\":\"KPSP\",\"destination\":\"KSFO\",\"originName\":\"Palm Springs Intl\",\"originCity\":\"Palm Springs, CA\",\"destinationName\":\"San Francisco Intl\",\"destinationCity\":\"San Francisco, CA\"},{\"ident\":\"SKW5318\",\"aircrafttype\":\"E75L\",\"actualdeparturetime\":1589061124,\"estimatedarrivaltime\":1589066820,\"filed_departuretime\":1589061300,\"origin\":\"KPHX\",\"destination\":\"KSFO\",\"originName\":\"Phoenix Sky Harbor Intl\",\"originCity\":\"Phoenix, AZ\",\"destinationName\":\"San Francisco Intl\",\"destinationCity\":\"San Francisco, CA\"},{\"ident\":\"SKW5373\",\"aircrafttype\":\"E75L\",\"actualdeparturetime\":1589060868,\"estimatedarrivaltime\":1589067060,\"filed_departuretime\":1589060340,\"origin\":\"KSEA\",\"destination\":\"KSFO\",\"originName\":\"Seattle-Tacoma Intl\",\"originCity\":\"Seattle, WA\",\"destinationName\":\"San Francisco Intl\",\"destinationCity\":\"San Francisco, CA\"}]}}\n", "AirportInfo?airportCode=KLAS": "{\"AirportInfoResult\":{\"name\":\"McCarran Intl\",\"location\":\"Las Vegas, NV\",\"longitude\":-115.1522347,\"latitude\":36.0800439,\"timezone\":\":America/Los_Angeles\"}}\n", "AirportInfo?airportCode=KTUS": "{\"AirportInfoResult\":{\"name\":\"Tucson Intl\",\"location\":\"Tucson, AZ\",\"longitude\":-110.9410139,\"latitude\":32.1160692,\"timezone\":\":America/Phoenix\"}}\n", "AirlineInfo?airlineCode=CAO": "{\"AirlineInfoResult\":{\"name\":\"Air China Cargo\",\"shortname\":\"\",\"callsign\":\"Airchina Freight\",\"location\":\"China\",\"country\":\"China\",\"url\":\"\",\"phone\":\"\"}}\n", "AirportInfo?airportCode=KSFO": "{\"AirportInfoResult\":{\"name\":\"San Francisco Intl\",\"location\":\"San Francisco, CA\",\"longitude\":-122.3754167,\"latitude\":37.6188056,\"timezone\":\":America/Los_Angeles\"}}\n", "Enroute?airport=KSFO&howMany=15&filter=airline": "{\"EnrouteResult\":{\"next_offset\":15,\"enroute\":[{\"ident\":\"ASA1002\",\"aircrafttype\":\"A321\",\"actualdeparturetime\":0,\"estimatedarrivaltime\":1588131360,\"filed_departuretime\":1588109400,\"origin\":\"KDCA\",\"destination\":\"KSFO\",\"originName\":\"Reagan National\",\"originCity\":\"Washington, DC\",\"destinationName\":\"San Francisco Intl\",\"destinationCity\":\"San Francisco, CA\"},{\"ident\":\"ASA1077\",\"aircrafttype\":\"B739\",\"actualdeparturetime\":0,\"estimatedarrivaltime\":1588134480,\"filed_departuretime\":1588113300,\"origin\":\"KIAD\",\"destination\":\"KSFO\",\"originName\":\"Washington Dulles Intl\",\"originCity\":\"Washington, DC\",\"destinationName\":\"San Francisco Intl\",\"destinationCity\":\"San Francisco, CA\"},{\"ident\":\"JBU915\",\"aircrafttype\":\"A320\",\"actualdeparturetime\":0,\"estimatedarrivaltime\":1588298460,\"filed_departuretime\":1588274100,\"origin\":\"KJFK\",\"destination\":\"KSFO\",\"originName\":\"John F Kennedy Intl\",\"originCity\":\"New York, NY\",\"destinationName\":\"San Francisco Intl\",\"destinationCity\":\"San Francisco, CA\"},{\"ident\":\"AAL2688\",\"aircrafttype\":\"B738\",\"actualdeparturetime\":0,\"estimatedarrivaltime\":1588565940,\"filed_departuretime\":1588542600,\"origin\":\"KMIA\",\"destination\":\"KSFO\",\"originName\":\"Miami Intl\",\"originCity\":\"Miami, FL\",\"destinationName\":\"San Francisco Intl\",\"destinationCity\":\"San Francisco, CA\"},{\"ident\":\"UAL2771\",\"aircrafttype\":\"B77W\",\"actualdeparturetime\":1589026439,\"estimatedarrivaltime\":1589063751,\"filed_departuretime\":1589026200,\"origin\":\"EHAM\",\"destination\":\"KSFO\",\"originName\":\"Amsterdam Schiphol\",\"originCity\":\"Amsterdam\",\"destinationName\":\"San Francisco Intl\",\"destinationCity\":\"San Francisco, CA\"},{\"ident\":\"EVA18\",\"aircrafttype\":\"B77W\",\"actualdeparturetime\":1589024062,\"estimatedarrivaltime\":1589063886,\"filed_departuretime\":1589024400,\"origin\":\"RCTP\",\"destination\":\"KSFO\",\"originName\":\"Taiwan Taoyuan Int\'l\",\"originCity\":\"Taipei\",\"destinationName\":\"San Francisco Intl\",\"destinationCity\":\"San Francisco, CA\"},{\"ident\":\"SKW5679\",\"aircrafttype\":\"CRJ2\",\"actualdeparturetime\":1589061685,\"estimatedarrivaltime\":1589063940,\"filed_departuretime\":1589062200,\"origin\":\"KRNO\",\"destination\":\"KSFO\",\"originName\":\"Reno/Tahoe Intl\",\"originCity\":\"Reno, NV\",\"destinationName\":\"San Francisco Intl\",\"destinationCity\":\"San Francisco, CA\"},{\"ident\":\"UAL2779\",\"aircrafttype\":\"B77W\",\"actualdeparturetime\":1589025332,\"estimatedarrivaltime\":1589063983,\"filed_departuretime\":1589025300,\"origin\":\"EDDF\",\"destination\":\"KSFO\",\"originName\":\"Frankfurt Int\'l\",\"originCity\":\"Frankfurt am Main\",\"destinationName\":\"San Francisco Intl\",\"destinationCity\":\"San Francisco, CA\"},{\"ident\":\"SKW5280\",\"aircrafttype\":\"CRJ2\",\"actualdeparturetime\":1589059860,\"estimatedarrivaltime\":1589064120,\"filed_departuretime\":1589060400,\"origin\":\"KRDM\",\"destination\":\"KSFO\",\"originName\":\"Roberts Field\",\"originCity\":\"Redmond, OR\",\"destinationName\":\"San Francisco Intl\",\"destinationCity\":\"San Francisco, CA\"},{\"ident\":\"SKW5756\",\"aircrafttype\":\"CRJ7\",\"actualdeparturetime\":1589062307,\"estimatedarrivaltime\":1589064180,\"filed_departuretime\":1589062680,\"origin\":\"KSBP\",\"destination\":\"KSFO\",\"originName\":\"San Luis County Rgnl\",\"originCity\":\"San Luis Obispo, CA\",\"destinationName\":\"San Francisco Intl\",\"destinationCity\":\"San Francisco, CA\"},{\"ident\":\"SKW5282\",\"aircrafttype\":\"E75L\",\"actualdeparturetime\":1589061684,\"estimatedarrivaltime\":1589064660,\"filed_departuretime\":1589061300,\"origin\":\"KLAX\",\"destination\":\"KSFO\",\"originName\":\"Los Angeles Intl\",\"originCity\":\"Los Angeles, CA\",\"destinationName\":\"San Francisco Intl\",\"destinationCity\":\"San Francisco, CA\"},{\"ident\":\"SKW3314\",\"aircrafttype\":\"E75L\",\"actualdeparturetime\":1589060746,\"estimatedarrivaltime\":1589064660,\"filed_departuretime\":1589061000,\"origin\":\"KLAS\",\"destination\":\"KSFO\",\"originName\":\"McCarran Intl\",\"originCity\":\"Las Vegas, NV\",\"destinationName\":\"San Francisco Intl\",\"destinationCity\":\"San Francisco, CA\"},{\"ident\":\"UAL2264\",\"aircrafttype\":\"B738\",\"actualdeparturetime\":1589044457,\"estimatedarrivaltime\":1589064840,\"filed_departuretime\":1589044200,\"origin\":\"KEWR\",\"destination\":\"KSFO\",\"originName\":\"Newark Liberty Intl\",\"originCity\":\"Newark, NJ\",\"destinationName\":\"San Francisco Intl\",\"destinationCity\":\"San Francisco, CA\"},{\"ident\":\"SKW5407\",\"aircrafttype\":\"CRJ2\",\"actualdeparturetime\":1589061431,\"estimatedarrivaltime\":1589064840,\"filed_departuretime\":1589061300,\"origin\":\"KONT\",\"destination\":\"KSFO\",\"originName\":\"Ontario Intl\",\"originCity\":\"Ontario, CA\",\"destinationName\":\"San Francisco Intl\",\"destinationCity\":\"San Francisco, CA\"},{\"ident\":\"SKW5767\",\"aircrafttype\":\"E75L\",\"actualdeparturetime\":1589058705,\"estimatedarrivaltime\":1589065140,\"filed_departuretime\":1589059140,\"origin\":\"KTUS\",\"destination\":\"KSFO\",\"originName\":\"Tucson Intl\",\"originCity\":\"Tucson, AZ\",\"destinationName\":\"San Francisco Intl\",\"destinationCity\":\"San Francisco, CA\"}]}}\n", "AirportInfo?airportCode=EDDF": "{\"AirportInfoResult\":{\"name\":\"Frankfurt Int\'l\",\"location\":\"Frankfurt am Main\",\"longitude\":8.543125,\"latitude\":50.026421,\"timezone\":\":Europe/Berlin\"}}\n", "Enroute?airport=KLAX&howMany=15&filter=airline": "{\"EnrouteResult\":{\"next_offset\":15,\"enroute\":[{\"ident\":\"FFT403\",\"aircrafttype\":\"A320\",\"actualdeparturetime\":0,\"estimatedarrivaltime\":1588296300,\"filed_departuretime\":1588286940,\"origin\":\"KDEN\",\"destination\":\"KLAX\",\"originName\":\"Denver Intl\",\"originCity\":\"Denver, CO\",\"destinationName\":\"Los Angeles Intl\",\"destinationCity\":\"Los Angeles, CA\"},{\"ident\":\"JBU687\",\"aircrafttype\":\"A320\",\"actualdeparturetime\":0,\"estimatedarrivaltime\":1588299840,\"filed_departuretime\":1588276560,\"origin\":\"KBOS\",\"destination\":\"KLAX\",\"originName\":\"Boston Logan Intl\",\"originCity\":\"Boston, MA\",\"destinationName\":\"Los Angeles Intl\",\"destinationCity\":\"Los Angeles, CA\"},{\"ident\":\"SKW4033\",\"aircrafttype\":\"E75L\",\"actualdeparturetime\":0,\"estimatedarrivaltime\":1588383060,\"filed_departuretime\":1588375800,\"origin\":\"KABQ\",\"destination\":\"KLAX\",\"originName\":\"Albuquerque Intl Sunport\",\"originCity\":\"Albuquerque, NM\",\"destinationName\":\"Los Angeles Intl\",\"destinationCity\":\"Los Angeles, CA\"},{\"ident\":\"AAL707\",\"aircrafttype\":\"A321\",\"actualdeparturetime\":0,\"estimatedarrivaltime\":1588471800,\"filed_departuretime\":1588452300,\"origin\":\"KCLT\",\"destination\":\"KLAX\",\"originName\":\"Charlotte/Douglas Intl\",\"originCity\":\"Charlotte, NC\",\"destinationName\":\"Los Angeles Intl\",\"destinationCity\":\"Los Angeles, CA\"},{\"ident\":\"CCA983\",\"aircrafttype\":\"B773\",\"actualdeparturetime\":0,\"estimatedarrivaltime\":1588478700,\"filed_departuretime\":1588435500,\"origin\":\"ZBAA\",\"destination\":\"KLAX\",\"originName\":\"Beijing Capital Int\'l\",\"originCity\":\"Beijing\",\"destinationName\":\"Los Angeles Intl\",\"destinationCity\":\"Los Angeles, CA\"},{\"ident\":\"CPZ6007\",\"aircrafttype\":\"E75L\",\"actualdeparturetime\":0,\"estimatedarrivaltime\":1589061480,\"filed_departuretime\":1589055000,\"origin\":\"KABQ\",\"destination\":\"KLAX\",\"originName\":\"Albuquerque Intl Sunport\",\"originCity\":\"Albuquerque, NM\",\"destinationName\":\"Los Angeles Intl\",\"destinationCity\":\"Los Angeles, CA\"},{\"ident\":\"UAL675\",\"aircrafttype\":\"B738\",\"actualdeparturetime\":1589044035,\"estimatedarrivaltime\":1589064000,\"filed_departuretime\":1589044200,\"origin\":\"KEWR\",\"destination\":\"KLAX\",\"originName\":\"Newark Liberty Intl\",\"originCity\":\"Newark, NJ\",\"destinationName\":\"Los Angeles Intl\",\"destinationCity\":\"Los Angeles, CA\"},{\"ident\":\"SKW4033\",\"aircrafttype\":\"E75L\",\"actualdeparturetime\":1589058300,\"estimatedarrivaltime\":1589064360,\"filed_departuretime\":1589058600,\"origin\":\"KABQ\",\"destination\":\"KLAX\",\"originName\":\"Albuquerque Intl Sunport\",\"originCity\":\"Albuquerque, NM\",\"destinationName\":\"Los Angeles Intl\",\"destinationCity\":\"Los Angeles, CA\"},{\"ident\":\"CCA983\",\"aircrafttype\":\"B77W\",\"actualdeparturetime\":1589021367,\"estimatedarrivaltime\":1589064589,\"filed_departuretime\":1589021367,\"origin\":\"ZBAA\",\"destination\":\"KLAX\",\"originName\":\"Beijing Capital Int\'l\",\"originCity\":\"Beijing\",\"destinationName\":\"Los Angeles Intl\",\"destinationCity\":\"Los Angeles, CA\"},{\"ident\":\"AAL2946\",\"aircrafttype\":\"B738\",\"actualdeparturetime\":1589050440,\"estimatedarrivaltime\":1589064600,\"filed_departuretime\":1589050380,\"origin\":\"KORD\",\"destination\":\"KLAX\",\"originName\":\"Chicago O\'Hare Intl\",\"originCity\":\"Chicago, IL\",\"destinationName\":\"Los Angeles Intl\",\"destinationCity\":\"Los Angeles, CA\"},{\"ident\":\"CAO1057\",\"aircrafttype\":\"B77L\",\"actualdeparturetime\":1589028809,\"estimatedarrivaltime\":1589064814,\"filed_departuretime\":1589025300,\"origin\":\"ZSPD\",\"destination\":\"KLAX\",\"originName\":\"Shanghai Pudong Int\'l\",\"originCity\":\"Shanghai\",\"destinationName\":\"Los Angeles Intl\",\"destinationCity\":\"Los Angeles, CA\"},{\"ident\":\"SWA1412\",\"aircrafttype\":\"B737\",\"actualdeparturetime\":1589061779,\"estimatedarrivaltime\":1589064900,\"filed_departuretime\":1589062200,\"origin\":\"KPHX\",\"destination\":\"KLAX\",\"originName\":\"Phoenix Sky Harbor Intl\",\"originCity\":\"Phoenix, AZ\",\"destinationName\":\"Los Angeles Intl\",\"destinationCity\":\"Los Angeles, CA\"},{\"ident\":\"KAL9011\",\"aircrafttype\":\"B789\",\"actualdeparturetime\":1589026772,\"estimatedarrivaltime\":1589064957,\"filed_departuretime\":1589026772,\"origin\":\"RKSI\",\"destination\":\"KLAX\",\"originName\":\"Incheon Int\'l\",\"originCity\":\"Seoul (Incheon)\",\"destinationName\":\"Los Angeles Intl\",\"destinationCity\":\"Los Angeles, CA\"},{\"ident\":\"AAL1830\",\"aircrafttype\":\"A321\",\"actualdeparturetime\":1589055088,\"estimatedarrivaltime\":1589065200,\"filed_departuretime\":1589055000,\"origin\":\"KDFW\",\"destination\":\"KLAX\",\"originName\":\"Dallas-Fort Worth Intl\",\"originCity\":\"Dallas-Fort Worth, TX\",\"destinationName\":\"Los Angeles Intl\",\"destinationCity\":\"Los Angeles, CA\"}]}}\n", "AirportInfo?airportCode=KDEN": "{\"AirportInfoResult\":{\"name\":\"Denver Intl\",\"location\":\"Denver, CO\",\"longitude\":-104.6731667,\"latitude\":39.8616667,\"timezone\":\":America/Denver\"}}\n", "AirportInfo?airportCode=KSEA": "{\"AirportInfoResult\":{\"name\":\"Seattle-Tacoma Intl\",\"location\":\"Seattle, WA\",\"longitude\":-122.3117778,\"latitude\":47.4498889,\"timezone\":\":America/Los_Angeles\"}}\n", "AirportInfo?airportCode=KIND": "{\"AirportInfoResult\":{\"name\":\"Indianapolis Intl\",\"location\":\"Indianapolis, IN\",\"longitude\":-86.2946389,\"latitude\":39.7173056,\"timezone\":\":America/Indiana/Indianapolis\"}}\n", "AirlineInfo?airlineCode=NKS": "{\"AirlineInfoResult\":{\"name\":\"Spirit Airlines, Inc.\",\"shortname\":\"Spirit\",\"callsign\":\"Spirit Wings\",\"location\":\"Miramar, FL\",\"country\":\"United States\",\"url\":\"http://spirit.com/\",\"phone\":\"+1-801-401-2200\"}}\n", "AirportInfo?airportCode=KLAX": "{\"AirportInfoResult\":{\"name\":\"Los Angeles Intl\",\"location\":\"Los Angeles, CA\",\"longitude\":-118.4080486,\"latitude\":33.9424964,\"timezone\":\":America/Los_Angeles\"}}\n", "AirportInfo?airportCode=KMEM": "{\"AirportInfoResult\":{\"name\":\"Memphis Intl\",\"location\":\"Memphis, TN\",\"longitude\":-89.9766792,\"latitude\":35.0424114,\"timezone\":\":America/Chicago\"}}\n", "AirportInfo?airportCode=RKSI": "{\"AirportInfoResult\":{\"name\":\"Incheon Int\'l\",\"location\":\"Seoul (Incheon)\",\"longitude\":126.44,\"latitude\":37.463333,\"timezone\":\":Asia/Seoul\"}}\n", "AirlineInfo?airlineCode=KAL": "{\"AirlineInfoResult\":{\"name\":\"Korean Air Lines Co., Ltd.\",\"shortname\":\"Korean Air Lines Co.\",\"callsign\":\"Koreanair\",\"location\":\"Republic Of Korea\",\"country\":\"South Korea\",\"url\":\"http://www.koreanair.com/\",\"phone\":\"+1-800-438-5000\"}}\n", "AirlineInfo?airlineCode=AAL": "{\"AirlineInfoResult\":{\"name\":\"American Airlines Inc.\",\"shortname\":\"American Airlines\",\"callsign\":\"American\",\"location\":\"\",\"country\":\"United States\",\"url\":\"http://www.aa.com/\",\"phone\":\"+1-800-433-7300\"}}\n"]
{
    didSet {
        print("\(flightSimulationData)")
    }
}

 

 

 

FoundationExtensions.swift

import Foundation

extension NSPredicate {
    static var all = NSPredicate(format: "TRUEPREDICATE")
    static var none = NSPredicate(format: "FALSEPREDICATE")
}

extension Data {
    var utf8: String? { String(data: self, encoding: .utf8 ) }
}

extension String {
    var trim: String {
        var trimmed = self.drop(while: { $0.isWhitespace })
        while trimmed.last?.isWhitespace ?? false {
            trimmed = trimmed.dropLast()
        }
        return String(trimmed)
    }

    var base64: String? { self.data(using: .utf8)?.base64EncodedString() }
    
    func contains(elementIn array: [String]) -> Bool {
        array.contains(where: { self.contains($0) })
    }
}

extension DateFormatter {
    static var short: DateFormatter = {
        let formatter = DateFormatter()
        formatter.locale = Locale(identifier: "en_US")
        formatter.dateStyle = .short
        formatter.timeStyle = .short
        return formatter
    }()
    
    static var shortTime: DateFormatter = {
        let formatter = DateFormatter()
        formatter.locale = Locale(identifier: "en_US")
        formatter.dateStyle = .none
        formatter.timeStyle = .short
        return formatter
    }()
    
    static var shortDate: DateFormatter = {
        let formatter = DateFormatter()
        formatter.locale = Locale(identifier: "en_US")
        formatter.dateStyle = .short
        formatter.timeStyle = .none
        return formatter
    }()

    static func stringRelativeToToday(_ today: Date, from date: Date) -> String {
        let dateComponents = Calendar.current.dateComponents(in: .current, from: date)
        var nowComponents = Calendar.current.dateComponents(in: .current, from: today)
        if dateComponents.isSameDay(as: nowComponents) {
            return "today at " + DateFormatter.shortTime.string(from: date)
        }
        nowComponents = Calendar.current.dateComponents(in: .current, from: today.addingTimeInterval(24*60*60))
        if dateComponents.isSameDay(as: nowComponents) {
            return "tomorrow at " + DateFormatter.shortTime.string(from: date)
        }
        nowComponents = Calendar.current.dateComponents(in: .current, from: today.addingTimeInterval(-24*60*60))
        if dateComponents.isSameDay(as: nowComponents) {
            return "yesterday at " + DateFormatter.shortTime.string(from: date)
        }
        return DateFormatter.short.string(from: date)
    }
}

extension DateComponents {
    func isSameDay(as other: DateComponents) -> Bool {
        return self.year == other.year && self.month == other.month && self.day == other.day
    }
}

 

 

 

AppDelegate.swift

import UIKit
import CoreData

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {



    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        // Override point for customization after application launch.
        return true
    }

    // MARK: UISceneSession Lifecycle

    func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
        // Called when a new scene session is being created.
        // Use this method to select a configuration to create the new scene with.
        return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
    }

    func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set<UISceneSession>) {
        // Called when the user discards a scene session.
        // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions.
        // Use this method to release any resources that were specific to the discarded scenes, as they will not return.
    }

    // MARK: - Core Data stack

    lazy var persistentContainer: NSPersistentCloudKitContainer = {
        /*
         The persistent container for the application. This implementation
         creates and returns a container, having loaded the store for the
         application to it. This property is optional since there are legitimate
         error conditions that could cause the creation of the store to fail.
        */
        let container = NSPersistentCloudKitContainer(name: "Enroute")
        container.loadPersistentStores(completionHandler: { (storeDescription, error) in
            if let error = error as NSError? {
                // Replace this implementation with code to handle the error appropriately.
                // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
                 
                /*
                 Typical reasons for an error here include:
                 * The parent directory does not exist, cannot be created, or disallows writing.
                 * The persistent store is not accessible, due to permissions or data protection when the device is locked.
                 * The device is out of space.
                 * The store could not be migrated to the current model version.
                 Check the error message to determine what the actual problem was.
                 */
                fatalError("Unresolved error \(error), \(error.userInfo)")
            }
        })
        return container
    }()

    // MARK: - Core Data Saving support

    func saveContext () {
        let context = persistentContainer.viewContext
        if context.hasChanges {
            do {
                try context.save()
            } catch {
                // Replace this implementation with code to handle the error appropriately.
                // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
                let nserror = error as NSError
                fatalError("Unresolved error \(nserror), \(nserror.userInfo)")
            }
        }
    }

}

 

 

 

SceneDelegate.swift

import UIKit
import SwiftUI

class SceneDelegate: UIResponder, UIWindowSceneDelegate {

    var window: UIWindow?


    func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
        // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`.
        // If using a storyboard, the `window` property will automatically be initialized and attached to the scene.
        // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead).

        // Get the managed object context from the shared persistent container.
        let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext

        // Create the SwiftUI view and set the context as the value for the managedObjectContext environment keyPath.
        // Add `@Environment(\.managedObjectContext)` in the views that will need the context.
        let airport = Airport.withICAO("KSFO", context: context)
        airport.fetchIncomingFlights()
        let contentView = FlightsEnrouteView(flightSearch: FlightSearch(destination: airport))
            .environment(\.managedObjectContext, context)

        // Use a UIHostingController as window root view controller.
        if let windowScene = scene as? UIWindowScene {
            let window = UIWindow(windowScene: windowScene)
            window.rootViewController = UIHostingController(rootView: contentView)
            self.window = window
            window.makeKeyAndVisible()
        }
    }

    func sceneDidDisconnect(_ scene: UIScene) {
        // Called as the scene is being released by the system.
        // This occurs shortly after the scene enters the background, or when its session is discarded.
        // Release any resources associated with this scene that can be re-created the next time the scene connects.
        // The scene may re-connect later, as its session was not neccessarily discarded (see `application:didDiscardSceneSessions` instead).
    }

    func sceneDidBecomeActive(_ scene: UIScene) {
        // Called when the scene has moved from an inactive state to an active state.
        // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive.
    }

    func sceneWillResignActive(_ scene: UIScene) {
        // Called when the scene will move from an active state to an inactive state.
        // This may occur due to temporary interruptions (ex. an incoming phone call).
    }

    func sceneWillEnterForeground(_ scene: UIScene) {
        // Called as the scene transitions from the background to the foreground.
        // Use this method to undo the changes made on entering the background.
    }

    func sceneDidEnterBackground(_ scene: UIScene) {
        // Called as the scene transitions from the foreground to the background.
        // Use this method to save data, release shared resources, and store enough scene-specific state information
        // to restore the scene back to its current state.

        // Save changes in the application's managed object context when the application transitions to the background.
        (UIApplication.shared.delegate as? AppDelegate)?.saveContext()
    }


}