Skip to content

Commit

Permalink
Initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
jtbandes committed Apr 22, 2016
0 parents commit 907e8f5
Show file tree
Hide file tree
Showing 15 changed files with 704 additions and 0 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
*.xcuserdatad
106 changes: 106 additions & 0 deletions DSCOVR.playground/Contents.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import Foundation
import CoreGraphics
import XCPlayground

/*:
## DSCOVR the Earth
*[Jacob Bandes-Storch](https://bandes-stor.ch/), Earth Day 2016*
On Feb. 11, 2015, a SpaceX rocket [launched](https://www.youtube.com/watch?v=OvHJSIKP0Hg#t=15m48s) the [Deep Space Climate Observatory](https://en.wikipedia.org/wiki/Deep_Space_Climate_Observatory) satellite (DSCOVR) into orbit.
![DSCOVR launch clip](dscovr-launch.gif)
One of the instruments aboard DSCOVR is the [Earth Polychromatic Imaging Camera](http://epic.gsfc.nasa.gov/) (EPIC), which captures images of Earth several times per day in wavelengths surrounding the visible spectrum. Since DSCOVR sits at the L₁ [Lagrange point](https://en.wikipedia.org/wiki/Lagrangian_point), it always sees the sunlit face of Earth.
The [@dscovr_epic](https://twitter.com/dscovr_epic) Twitter bot, by Russ Garrett, posts some of these images on Twitter after some [extra post-processing](https://russ.garrett.co.uk/bots/dscovr_epic.html). I thought it would be neat to animate these, and I figured this would be a good excuse to play with the Twitter API and JSON parsing from Swift.
### Contents of this playground
- Note: Open this playground’s Assistant Editor (or press ⌥⌘↩) to view the slideshow.
- Note: The playground is written to work with both iOS & OS X APIs. Open the File Inspector (⌥⌘1) to choose a platform.
The following types are defined in the Swift files in this playground’s “Sources” directory. Press ⌘1 to open up the Project Navigator, and expand the Sources folder to see the additional source files.
- `SlideshowView`: a view class which uses Core Animation to display photos and captions.
- `JSON` and the `JSONCreatable` protocol: one of *many, many* possible ways of handling JSON in Swift. Compare it with others, love it, hate it... it’s up to you!
- `TwitterAPI`: a class which handles the nasty details of authentication and response parsing.
- `Tweet`: a convenience layer above plain JSON which lets us read tweets.
- `AsyncOperation` and `FetchImageOperation`: two [NSOperation](https://developer.apple.com/library/mac/documentation/Cocoa/Reference/NSOperation_class/) subclasses which help with executing and chaining asynchronous tasks.
First, let’s set up a slideshow view to flip through a series of images and captions.
*/
let slideshowView = SlideshowView(frame: CGRect(x: 0, y: 0, width: 500, height: 500))
slideshowView.speed = 3

XCPlaygroundPage.currentPage.liveView = slideshowView
XCPlaygroundPage.currentPage.needsIndefiniteExecution = true

/*:
Now, we’ll authenticate with Twitter and make a request to the [`user_timeline` API](https://dev.twitter.com/rest/reference/get/statuses/user_timeline) to retrieve the most recent tweets from @dscovr_epic.
- Important: Visit <https://apps.twitter.com/> and create a test app. You’ll be given a **consumer key** and **consumer secret**. Paste them below.
*/
let twitter = try TwitterAPI(
consumerKey: "<#Your consumer key here#>",
consumerSecret: "<#Your consumer secret here#>")

try twitter.makeAPICall(
.GET, "1.1/statuses/user_timeline.json",
params: ["screen_name": "dscovr_epic", "count": "20"])
{ (tweets: [Tweet]?) in

guard let tweets = tweets else {
print("failed to fetch tweets")
return
}

//: Then we can select the tweets which contiain photos, and create a `FetchImageOperation` instance to fetch the photo for each one:
let tweetsWithPhotos = tweets.filter { $0.photoURL != nil }

let fetchImageOps: [FetchImageOperation] = tweetsWithPhotos.map {
let op = FetchImageOperation(url: $0.photoURL!)
NSOperationQueue.mainQueue().addOperation(op)
return op
}
/*:
Once the FetchImageOperations have finished, we take the images which were successfully fetched, and combine those with tweet captions to make Slides.
We’ll do this from a simple NSBlockOperation, which depends on all of the FetchImageOperations (so it’ll wait for them to finish).
*/
let showSlideshow = NSBlockOperation {
slideshowView.slides = zip(tweetsWithPhotos, fetchImageOps).flatMap { tweet, op in
guard let photo = op.image else { return nil }
return SlideshowView.Slide(caption: tweet.caption, image: photo)
}
}

fetchImageOps.forEach { showSlideshow.addDependency($0) }

NSOperationQueue.mainQueue().addOperation(showSlideshow)
}

/*:
## Protect the Planet
*Don’t just sit there, do something!*
Many organizations are working hard to protect the Earth. Donate or volunteer:
- <http://www.earthday.org/take-action/>
- <http://www.worldwildlife.org/how-to-help>
- <http://appstore.com/appsforearth>
## Further Exploration
- Experiment: NASA provides its own [DSCOVR API](http://epic.gsfc.nasa.gov/about.html) which has more advanced features, like searching for images which show particular areas of the Earth. Try fetching images from this API instead of Twitter.
You can explore some of the code powering the @dscovr_epic bot: <https://github.com/russss/dscovr-epic>
The Japan Meteorological Agency has a weather satellite called [Himawari 8](https://en.wikipedia.org/wiki/Himawari_8) which captures images of the Eart at even higher resolution than EPIC does. Its orbit keeps it stationary above Japan.
- How-to about generating animations from Himawari 8 imagery: <https://gist.github.com/celoyd/b92d0de6fae1f18791ef>
- Some scripts to do so: <https://github.com/m-ad/himawari-8>
- A beautiful “installation” of one such animation: <https://glittering.blue/>
*/
Binary file added DSCOVR.playground/Resources/dscovr-launch.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
54 changes: 54 additions & 0 deletions DSCOVR.playground/Sources/AsyncOperation.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import Foundation

/// An operation whose execution is asynchronous.
public class AsyncOperation: NSOperation
{
public typealias FinishBlock = () -> Void

/// The `executionBlock` will be called from the operation’s `start` method (which is
/// automatically called by NSOperationQueue. The `FinishBlock` passed in can be called
/// to complete the operation.
/// - Important: If `executionBlock` captures `self` strongly, this is a retain cycle.
/// If the operation never executes, this will result in a memory leak.
public var executionBlock: (FinishBlock -> Void)?
public init(executionBlock: (FinishBlock -> Void)?)
{
self.executionBlock = executionBlock
}

override public final func start()
{
guard let executionBlock = executionBlock else {
assertionFailure("executionBlock must be set; start() should not be called more than once")
return
}
self.executionBlock = nil

executing = true
executionBlock {
self.executing = false
self.finished = true
}
}

private var _executing = false
private var _finished = false

public private(set) override var executing: Bool {
get { return _executing }
set {
willChangeValueForKey("isExecuting")
_executing = newValue
didChangeValueForKey("isExecuting")
}
}

public private(set) override var finished: Bool {
get { return _finished }
set {
willChangeValueForKey("isFinished")
_finished = newValue
didChangeValueForKey("isFinished")
}
}
}
39 changes: 39 additions & 0 deletions DSCOVR.playground/Sources/Compatibility.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// This file provides definitions that help with cross-platform compatibility.

#if os(OSX)
import AppKit
public typealias NativeView = NSView
public typealias NativeImage = NSImage
public typealias NativeFont = NSFont
public typealias NativeColor = NSColor
private let regularWeight = NSFontWeightRegular
#else
import UIKit
public typealias NativeView = UIView
public typealias NativeImage = UIImage
public typealias NativeFont = UIFont
public typealias NativeColor = UIColor
private let regularWeight = UIFontWeightRegular
#endif


extension NativeFont
{
static func monospacedDigitSystemFont() -> NativeFont
{
return monospacedDigitSystemFontOfSize(0, weight: regularWeight)
}
}


extension NativeImage
{
/// An object that can be used as a CALayer’s `contents`.
var layerContents: AnyObject {
#if os(OSX)
return self
#else
return self.CGImage!
#endif
}
}
27 changes: 27 additions & 0 deletions DSCOVR.playground/Sources/FetchImage.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import Foundation

/// An operation which fetches an image from a given URL.
public final class FetchImageOperation: AsyncOperation
{
/// After the operation executes, this property will be set to the image that was fetched
/// (or `nil` if an error occurred).
public var image: NativeImage?

public init(url: NSURL)
{
// Only set executionBlock after init, because it requires capturing `self`.
super.init(executionBlock: nil)

executionBlock = { [weak self] finishBlock in
NSURLSession.sharedSession().dataTaskWithURL(url) { data, response, error in
if let strongSelf = self {
strongSelf.image = data.flatMap{ NativeImage(data: $0) }
}
if let error = error {
print("error fetching \(url): \(error)")
}
finishBlock()
}.resume()
}
}
}
120 changes: 120 additions & 0 deletions DSCOVR.playground/Sources/JSON.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import Foundation

/// This protocol enables our APIs to produce any "kind" of JSON object by using generics.
/// Functions can be declared like `func makeAPIRequest<T: JSONCreatable>(...)` and simply use
/// `T(json: JSON(data: ...))` to produce the JSON-creatable type requested by the caller.
///
/// The easiest way to make a custom type JSONCreatable is by inheriting from the JSON class,
/// and defining convenience accessors which return optional values:
///
/// class MyType: JSON {
/// var myConvenienceVariable: String? {
/// return self["some", 0, "key", "path"]?.asString
/// }
/// }
public protocol JSONCreatable
{
init(json: JSON) throws
}


extension Array: JSONCreatable
{
/// Arrays of JSONCreatable elements are automatically creatable from JSON.
///
/// (Attempting to construct from JSON an array whose Element type is non-JSONCreatable will throw an error.
/// In the future, Swift 3+ may bring improvements to generics which allow us to enforce this at compile-time.)
public init(json: JSON) throws
{
guard let Witness = Element.self as? JSONCreatable.Type, let array = json.asArray else {
throw JSON.Error.TypeMismatch
}
self = try array.map { try Witness.init(json: $0) as! Element }
}
}


/// A class which stores a JSON object and provides convenient accessors for traversal and type conversion:
///
/// json.asString, json.asNumber, json.asBool, json.asArray
/// json["stuff"], json["stuff"][2], json["stuff", 2]
public class JSON: JSONCreatable
{
private final var storage: AnyObject
private init(storage: AnyObject) {
self.storage = storage
}

public enum Error: ErrorType {
case TypeMismatch
}

public enum SubscriptPathComponent: StringLiteralConvertible, IntegerLiteralConvertible {
case ObjectKey(String)
case ArrayIndex(Int)

public init(integerLiteral value: Int) { self = .ArrayIndex(value) }
public init(stringLiteral value: String) { self = .ObjectKey(value) }
public init(extendedGraphemeClusterLiteral value: String) { self = .ObjectKey(value) }
public init(unicodeScalarLiteral value: String) { self = .ObjectKey(value) }
}

public required init(json: JSON) throws {
storage = json.storage
}

public required init(data: NSData) throws {
storage = try NSJSONSerialization.JSONObjectWithData(data, options: .AllowFragments)
}

public final var asString: String? {
return storage as? String
}

public final var asNumber: NSNumber? {
return storage as? NSNumber
}

public final var asBool: NSNumber? {
return storage as? Bool
}

public final var asArray: [JSON]? {
return (storage as? [AnyObject])?.map { JSON(storage: $0) }
}

public final subscript(path: SubscriptPathComponent...) -> JSON? {
return self[pathComponents: path]
}

public final subscript(component: SubscriptPathComponent) -> JSON? {
return self[pathComponents: [component]]
}

private subscript(pathComponents path: [SubscriptPathComponent]) -> JSON?
{
// Traverse through the object according to the given subscript path.
var storage = self.storage
for component in path {
switch component {
case .ArrayIndex(let index):
guard let array = storage as? [AnyObject] where index < array.count else { return nil }
storage = array[index]

case .ObjectKey(let key):
guard let object = storage as? [String: AnyObject], let value = object[key] else { return nil }
storage = value
}
}
return JSON(storage: storage)
}
}


extension JSON: CustomStringConvertible
{
public var description: String {
return storage.description ?? "JSON@\(unsafeAddressOf(self))"
}
}

Loading

0 comments on commit 907e8f5

Please sign in to comment.