Created
May 1, 2019 21:52
-
-
Save nixta/03182b4922c0d26418432277242d3594 to your computer and use it in GitHub Desktop.
A custom AGSLocationDataSource to integrate the International Space Station's realtime location into the ArcGIS Runtime
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import Foundation | |
import ArcGIS | |
//MARK: - Custom AGSLocationDataSource | |
/// A custom AGSLocationDataSource that uses the public ISS Location API to return | |
/// realtime ISS locations at 5 second intervals. | |
class ISSLocationDataSource: AGSLocationDataSource { | |
private let issLocationAPIURL = URL(string: "http://api.open-notify.org/iss-now.json")! | |
private var pollingTimer: Timer? | |
private var currentRequest: AGSJSONRequestOperation? | |
private var previousLocation: AGSLocation? | |
// MARK: - FROM AGSLocationDisplay: start AGSLocationDataSource. | |
override func doStart() { | |
startRequestingLocationUpdates() | |
// MARK: TO AGSLocationDisplay: data source started OK. | |
didStartOrFailWithError(nil) | |
} | |
// MARK: FROM AGSLocationDisplay: stop AGSLocationDataSource. | |
override func doStop() { | |
stopRetrievingISSLocationsFromAPI() | |
didStop() | |
} | |
// MARK: - | |
func startRequestingLocationUpdates() { | |
// Get ISS positions every 5 seconds (as recommended on the | |
// API documentation pages): | |
// http://open-notify.org/Open-Notify-API/ISS-Location-Now/ | |
pollingTimer = Timer.scheduledTimer(withTimeInterval: 5, repeats: true) { | |
[weak self] _ in | |
// Request the next ISS location from the API and build an AGSLocation. | |
self?.requestNextLocation { newISSLocation in | |
// MARK: TO AGSLocationDisplay: new location available. | |
self?.didUpdate(newISSLocation) | |
} | |
} | |
} | |
func stopRetrievingISSLocationsFromAPI() { | |
// Stop asking for ISS location. | |
pollingTimer?.invalidate() | |
pollingTimer = nil | |
// Cancel any open requests. | |
if let activeRequest = currentRequest { | |
activeRequest.cancel() | |
} | |
} | |
//MARK: - ISS Location API | |
//MARK: Make a request for the current location | |
/// Make a request to the ISS Tracking API and return an AGSLocation. | |
/// | |
/// - Parameter completion: The completion closure is called when the AGSLocation has been obtained. | |
func requestNextLocation(completion: @escaping (AGSLocation) -> Void) { | |
// Check we don't have a pending request. | |
guard currentRequest == nil else { return } | |
// Create a new request against the ISS location API. | |
let locationRequest = AGSJSONRequestOperation(url: issLocationAPIURL) | |
// Keep a handle onto the request so we can avoid sending multiple requests, and | |
// to cancel the request if need be. | |
currentRequest = locationRequest | |
locationRequest.registerListener(self) { [weak self] (json, error) in | |
defer { | |
// When we're done processing this response, clear the flag we | |
// use to prevent concurrent requests being sent. | |
self?.currentRequest = nil | |
} | |
guard let self = self else { return } | |
/// 1. Do some sanity checking of the response. | |
guard error == nil else { | |
print("Error from location API: \(error!.localizedDescription)") | |
return | |
} | |
// Get the JSON. | |
guard let json = json as? Dictionary<String, Any> else { | |
print("Could not get a Dictionary<String, Any> out of the response!") | |
return | |
} | |
/// 2. Now turn the response into an AGSLocation to be used by an AGSLocationDisplay. | |
if let location = self.parseISSJSONIntoAGSLocation(json) { | |
// Call back with the new location. | |
completion(location) | |
// Remember this as the previous location so that we can calculate velocity and heading | |
// when we get the next location back. | |
self.previousLocation = location | |
} else { | |
print("Could not get ISS Location from JSON!") | |
} | |
} | |
// Send the JSON request to get the ISS position. | |
locationRequest.execute() | |
} | |
// MARK: Parse the API's JSON response into an AGSLocation | |
/// Parse the JSON returned from the ISS Location API to obtain an `AGSLocation`. | |
/// | |
/// - Parameter json: json: A JSON dictionary from the ISS Location API. | |
/// - Returns: An `AGSLocation`. | |
private func parseISSJSONIntoAGSLocation(_ json: Dictionary<String, Any>) -> AGSLocation? { | |
// Extract the info we need for our AGSLocation. | |
guard (json["message"] as? String) == "success", | |
let timeStamp = json["timestamp"] as? TimeInterval, | |
let locationDictionary = json["iss_position"] as? Dictionary<String, String>, | |
let latStr = locationDictionary["latitude"], let lonStr = locationDictionary["longitude"], | |
let lat = Double(latStr), let lon = Double(lonStr) else { | |
print("Could not parse expected information out of the JSON!") | |
return nil | |
} | |
/// Now create an AGSLocation from the information we got out of the JSON. | |
// Set the altitude in meters (guesstimated from https://www.heavens-above.com/IssHeight.aspx). | |
let position = AGSGeometryEngine.geometry(bySettingZ: 407000, in: AGSPointMakeWGS84(lat, lon)) as! AGSPoint | |
let timestamp = Date(timeIntervalSince1970: timeStamp) | |
// Get the velocity and heading if we can. If not, return just the location with a guess at the velocity. | |
guard let previousLocation = self.previousLocation, let previousPosition = previousLocation.position, | |
let posDiff = AGSGeometryEngine.geodeticDistanceBetweenPoint1(position, point2: previousPosition, | |
distanceUnit: .meters(), | |
azimuthUnit: .degrees(), | |
curveType: .geodesic) else { | |
// If this is the first location, we will set AGSLocation.lastKnown to true. | |
// This causes the AGSLocationDisplay to use the `acquiringSymbol` to display the current location. | |
let isFirstLocation = self.previousLocation == nil | |
// We couldn't calculate the velocity and heading, so just hard-code the velocity and return. | |
return AGSLocation(position: position, timestamp: timestamp, | |
horizontalAccuracy: 0, verticalAccuracy: 0, | |
velocity: 7666, course: 0, lastKnown: isFirstLocation) | |
} | |
// We were able to get enough info to calculate the velocity and heading… | |
let timeDiff = timestamp.timeIntervalSince(previousLocation.timestamp) | |
let velocity = posDiff.distance/timeDiff | |
// Return a full AGSLocation with calculated velocity and heading. | |
return AGSLocation(position: position, timestamp: timestamp, | |
horizontalAccuracy: 0, verticalAccuracy: 0, | |
velocity: velocity, course: posDiff.azimuth2, lastKnown: false) | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment