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()
}
}