Skip to content

Instantly share code, notes, and snippets.

@nixta
Created May 1, 2019 21:52
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save nixta/03182b4922c0d26418432277242d3594 to your computer and use it in GitHub Desktop.
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
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