├── .gitmodules
├── GPXPreprocessing
├── AppDelegate.swift
├── Assets.xcassets
│ └── AppIcon.appiconset
│ │ └── Contents.json
├── Base.lproj
│ └── MainMenu.xib
├── GPXPreprocessing.entitlements
├── Info.plist
├── ViewController.swift
└── de.lproj
│ └── MainMenu.strings
├── Gemfile
├── Incremental copy-Info.plist
├── Incremental
├── Helpers.swift
├── Incremental+NSObject.swift
├── Incremental+UIKit
│ ├── ActivityIndicator.swift
│ ├── Button.swift
│ ├── Constraints.swift
│ ├── Driver.swift
│ ├── Incremental+NSGestureRecognizer.swift
│ ├── Incremental+UIGestureRecognizer.swift
│ ├── Incremental+UIKit+AppKit.swift
│ ├── Label.swift
│ ├── MapView.swift
│ ├── NavigationController.swift
│ ├── ProgressIndicator.swift
│ ├── StackView.swift
│ ├── Switch.swift
│ ├── TableView.swift
│ ├── TextField.swift
│ └── ViewController.swift
├── Incremental.h
├── Incremental.swift
├── IncrementalArray.swift
├── IncrementalStdLib.swift
└── Info.plist
├── Incremental_Mac
├── Incremental_Mac.h
└── Info.plist
├── Laufpark.xcodeproj
└── project.pbxproj
├── Laufpark
├── AppDelegate.swift
├── Assets.xcassets
│ ├── AppIcon.appiconset
│ │ ├── Contents.json
│ │ ├── Logo76@2x.png
│ │ ├── Logo_60@2x.png
│ │ ├── Logo_60@3x-1.png
│ │ ├── Logo_60@3x.png
│ │ └── Logo_Laufpark_Stechlin.png
│ ├── Color.colorset
│ │ └── Contents.json
│ └── Contents.json
├── Attribution_en.rtf
├── Base.lproj
│ ├── LaunchScreen.storyboard
│ ├── Localizable.strings
│ └── Main.storyboard
├── Helpers.swift
├── Icon@3x.png
├── Info.plist
├── LaunchScreen.storyboard
├── LineView.swift
├── ParallelArray.swift
├── SegmentedControl.swift
├── State.swift
├── ViewController.swift
├── Views.swift
├── btn_close@2x.png
├── btn_map.png
├── btn_map@2x.png
├── btn_route@2x.png
├── btn_satellite.png
├── btn_satellite@2x.png
├── credits.txt
├── de.lproj
│ └── Localizable.strings
├── en.lproj
│ └── Localizable.strings
├── graph.json
├── map.png
├── map@2x.png
└── partner.png
├── LaufparkUITests
├── Info.plist
└── LaufparkUITests.swift
├── README.md
├── Shared Code
├── Model+MapKit.swift
├── Model+UIKit.swift
├── Model.swift
├── Routing.swift
├── SortedArray.swift
└── gpx
│ ├── wabe beige-strecke 0.gpx
│ ├── wabe beige-strecke 1.gpx
│ ├── wabe beige-strecke 2.gpx
│ ├── wabe blau-strecke 0.gpx
│ ├── wabe blau-strecke 1.gpx
│ ├── wabe blau-strecke 2.gpx
│ ├── wabe blau-strecke 3.gpx
│ ├── wabe braun-strecke 0.gpx
│ ├── wabe braun-strecke 1.gpx
│ ├── wabe braun-strecke 2.gpx
│ ├── wabe braun-strecke 3.gpx
│ ├── wabe braun-strecke 4.gpx
│ ├── wabe braun-strecke braun 4.gpx
│ ├── wabe gelb-strecke 0.gpx
│ ├── wabe gelb-strecke 1.gpx
│ ├── wabe gelb-strecke 2.gpx
│ ├── wabe gelb-strecke 3.gpx
│ ├── wabe gelb-strecke 4.gpx
│ ├── wabe grau-strecke 0.gpx
│ ├── wabe gruen-strecke 0.gpx
│ ├── wabe gruen-strecke 1.gpx
│ ├── wabe gruen-strecke 2.gpx
│ ├── wabe gruen-strecke 3.gpx
│ ├── wabe gruen-strecke 4.gpx
│ ├── wabe hellblau-strecke 0.gpx
│ ├── wabe hellblau-strecke 1.gpx
│ ├── wabe hellblau-strecke 2.gpx
│ ├── wabe hellblau-strecke 3.gpx
│ ├── wabe hellblau-strecke 4.gpx
│ ├── wabe hellbraun-strecke 0.gpx
│ ├── wabe hellbraun-strecke 1.gpx
│ ├── wabe hellbraun-strecke 2.gpx
│ ├── wabe hellbraun-strecke 3.gpx
│ ├── wabe hellbraun-strecke 4.gpx
│ ├── wabe hellbraun-strecke 5.gpx
│ ├── wabe hellgruen-strecke 0.gpx
│ ├── wabe hellgruen-strecke 1.gpx
│ ├── wabe hellgruen-strecke 2.gpx
│ ├── wabe hellgruen-strecke 3.gpx
│ ├── wabe hellgruen-strecke 4.gpx
│ ├── wabe hellgruen-strecke 5.gpx
│ ├── wabe hellgruen-strecke 6.gpx
│ ├── wabe hellgruen-strecke 7.gpx
│ ├── wabe lila-strecke 0.gpx
│ ├── wabe lila-strecke 1.gpx
│ ├── wabe lila-strecke 2.gpx
│ ├── wabe lila-strecke 3.gpx
│ ├── wabe orange-strecke 0.gpx
│ ├── wabe pink-strecke 0.gpx
│ ├── wabe pink-strecke 1.gpx
│ ├── wabe pink-strecke 2.gpx
│ ├── wabe pink-strecke 3.gpx
│ ├── wabe pink-strecke 4.gpx
│ ├── wabe rosa-strecke 0.gpx
│ ├── wabe rosa-strecke 1.gpx
│ ├── wabe rosa-strecke 2.gpx
│ ├── wabe rosa-strecke 3.gpx
│ ├── wabe rosa-strecke 4.gpx
│ ├── wabe rosa-strecke 5.gpx
│ ├── wabe rosa-strecke 6.gpx
│ ├── wabe rot-strecke 0.gpx
│ ├── wabe rot-strecke 1.gpx
│ ├── wabe rot-strecke 2.gpx
│ ├── wabe rot-strecke 3.gpx
│ ├── wabe rot-strecke 4.gpx
│ ├── wabe tuerkis-strecke 0.gpx
│ ├── wabe tuerkis-strecke 1.gpx
│ ├── wabe tuerkis-strecke 2.gpx
│ ├── wabe tuerkis-strecke 3.gpx
│ ├── wabe tuerkis-strecke 4.gpx
│ ├── wabe tuerkis-strecke 5.gpx
│ ├── wabe tuerkis-strecke 6.gpx
│ ├── wabe violett-strecke 0.gpx
│ ├── wabe violett-strecke 1.gpx
│ ├── wabe violett-strecke 2.gpx
│ ├── wabe violett-strecke 3.gpx
│ └── wabe violett-strecke 4.gpx
└── fastlane
├── Appfile
├── Deliverfile
├── Fastfile
├── Gymfile
├── Matchfile
├── README.md
├── Snapfile
├── SnapshotHelper.swift
├── metadata
├── app_icon.jpg
├── copyright.txt
├── de-DE
│ ├── description.txt
│ ├── keywords.txt
│ ├── marketing_url.txt
│ ├── name.txt
│ ├── privacy_url.txt
│ ├── promotional_text.txt
│ ├── release_notes.txt
│ ├── subtitle.txt
│ └── support_url.txt
├── en-US
│ ├── description.txt
│ ├── keywords.txt
│ ├── marketing_url.txt
│ ├── name.txt
│ ├── privacy_url.txt
│ ├── promotional_text.txt
│ ├── release_notes.txt
│ ├── subtitle.txt
│ └── support_url.txt
├── primary_category.txt
├── primary_first_sub_category.txt
├── primary_second_sub_category.txt
├── review_information
│ ├── demo_password.txt
│ ├── demo_user.txt
│ ├── email_address.txt
│ ├── first_name.txt
│ ├── last_name.txt
│ ├── notes.txt
│ └── phone_number.txt
├── secondary_category.txt
├── secondary_first_sub_category.txt
├── secondary_second_sub_category.txt
└── trade_representative_contact_information
│ ├── address_line1.txt
│ ├── city_name.txt
│ ├── country.txt
│ ├── is_displayed_on_app_store.txt
│ ├── postal_code.txt
│ └── trade_name.txt
└── screenshots
├── README.txt
├── de-DE
├── iPad Pro (12.9-inch)-01HomeScreen.png
├── iPad Pro (12.9-inch)-02Selection.png
├── iPad Pro (9.7-inch)-01HomeScreen.png
├── iPad Pro (9.7-inch)-02Selection.png
├── iPhone 6-01HomeScreen.png
├── iPhone 6-02Selection.png
├── iPhone 8 Plus-01HomeScreen.png
├── iPhone 8 Plus-02Selection.png
├── iPhone X-01HomeScreen.png
└── iPhone X-02Selection.png
├── en-US
├── iPad Pro (12.9-inch)-01HomeScreen.png
├── iPad Pro (12.9-inch)-02Selection.png
├── iPad Pro (9.7-inch)-01HomeScreen.png
├── iPad Pro (9.7-inch)-02Selection.png
├── iPhone 6-01HomeScreen.png
├── iPhone 6-02Selection.png
├── iPhone 8 Plus-01HomeScreen.png
├── iPhone 8 Plus-02Selection.png
├── iPhone X-01HomeScreen.png
└── iPhone X-02Selection.png
└── screenshots.html
/.gitmodules:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chriseidhof/laufpark-stechlin/807aec8af39d13f34813f9ef795d77f76f017e71/.gitmodules
--------------------------------------------------------------------------------
/GPXPreprocessing/AppDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AppDelegate.swift
3 | // GPXPreprocessing
4 | //
5 | // Created by Chris Eidhof on 12.11.17.
6 | // Copyright © 2017 objc.io. All rights reserved.
7 | //
8 |
9 | import Cocoa
10 |
11 | @NSApplicationMain
12 | class AppDelegate: NSObject, NSApplicationDelegate {
13 |
14 | @IBOutlet weak var window: NSWindow!
15 |
16 |
17 | func applicationDidFinishLaunching(_ aNotification: Notification) {
18 | }
19 |
20 | func applicationWillTerminate(_ aNotification: Notification) {
21 | // Insert code here to tear down your application
22 | }
23 |
24 |
25 | }
26 |
27 |
--------------------------------------------------------------------------------
/GPXPreprocessing/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "mac",
5 | "size" : "16x16",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "mac",
10 | "size" : "16x16",
11 | "scale" : "2x"
12 | },
13 | {
14 | "idiom" : "mac",
15 | "size" : "32x32",
16 | "scale" : "1x"
17 | },
18 | {
19 | "idiom" : "mac",
20 | "size" : "32x32",
21 | "scale" : "2x"
22 | },
23 | {
24 | "idiom" : "mac",
25 | "size" : "128x128",
26 | "scale" : "1x"
27 | },
28 | {
29 | "idiom" : "mac",
30 | "size" : "128x128",
31 | "scale" : "2x"
32 | },
33 | {
34 | "idiom" : "mac",
35 | "size" : "256x256",
36 | "scale" : "1x"
37 | },
38 | {
39 | "idiom" : "mac",
40 | "size" : "256x256",
41 | "scale" : "2x"
42 | },
43 | {
44 | "idiom" : "mac",
45 | "size" : "512x512",
46 | "scale" : "1x"
47 | },
48 | {
49 | "idiom" : "mac",
50 | "size" : "512x512",
51 | "scale" : "2x"
52 | }
53 | ],
54 | "info" : {
55 | "version" : 1,
56 | "author" : "xcode"
57 | }
58 | }
--------------------------------------------------------------------------------
/GPXPreprocessing/GPXPreprocessing.entitlements:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/GPXPreprocessing/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIconFile
10 |
11 | CFBundleIdentifier
12 | $(PRODUCT_BUNDLE_IDENTIFIER)
13 | CFBundleInfoDictionaryVersion
14 | 6.0
15 | CFBundleName
16 | $(PRODUCT_NAME)
17 | CFBundlePackageType
18 | APPL
19 | CFBundleShortVersionString
20 | 1.0
21 | CFBundleVersion
22 | 1
23 | LSMinimumSystemVersion
24 | $(MACOSX_DEPLOYMENT_TARGET)
25 | NSHumanReadableCopyright
26 | Copyright © 2017 objc.io. All rights reserved.
27 | NSMainNibFile
28 | MainMenu
29 | NSPrincipalClass
30 | NSApplication
31 |
32 |
33 |
--------------------------------------------------------------------------------
/GPXPreprocessing/ViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ViewController.swift
3 | // GPXPreprocessing
4 | //
5 | // Created by Chris Eidhof on 12.11.17.
6 | // Copyright © 2017 objc.io. All rights reserved.
7 | //
8 |
9 | import Cocoa
10 | import MapKit
11 | import Incremental_Mac
12 |
13 | struct StoredState: Equatable, Codable {
14 | var annotationsVisible: Bool = false
15 | var satellite: Bool = false
16 | var showConfiguration: Bool = false
17 | }
18 |
19 | struct Path: Equatable, Codable {
20 | let entries: [Graph.Entry]
21 | let distance: CLLocationDistance
22 | }
23 |
24 | struct CoordinateAndTrack: Equatable, Codable { // tuples aren't codable
25 | let coordinate: Coordinate
26 | let track: Track
27 | var pathFromPrevious: Path?
28 | }
29 |
30 | //struct Route: Equatable, Codable {
31 | //
32 | // let startingPoint: CoordinateAndTrack
33 | // var points: [CoordinateAndTrack] = []
34 | //
35 | // init(track: Track, coordinate: Coordinate) {
36 | // startingPoint = CoordinateAndTrack(coordinate: coordinate, track: track, pathFromPrevious: nil)
37 | // }
38 | //
39 | // mutating func add(coordinate: Coordinate, inTrack track: Track, graph: Graph) {
40 | // let previous = points.last ?? startingPoint
41 | // let path = graph.shortestPath(from: previous.coordinate, to: coordinate).map {
42 | // Path(entries: $0.path, distance: $0.distance)
43 | // }
44 | //// assert(path != nil)
45 | // let result = CoordinateAndTrack(coordinate: coordinate, track: track, pathFromPrevious: path)
46 | // points.append(result)
47 | // }
48 | //
49 | // var wayPoints: [Coordinate] {
50 | // return [startingPoint.coordinate] + points.map { $0.coordinate }
51 | // }
52 | //
53 | // var segments: [(Coordinate, Coordinate)] {
54 | // let coordinates = points.map { $0.coordinate }
55 | // return Array(zip([startingPoint.coordinate] + coordinates, coordinates))
56 | // }
57 | //
58 | // var distance: Double {
59 | // return points.map { $0.pathFromPrevious?.distance ?? 0 }.reduce(into: 0, +=)
60 | // }
61 | //
62 | // func allPoints(tracks: [Track]) -> [Coordinate] {
63 | // var result: [Coordinate] = [startingPoint.coordinate]
64 | // for wayPoint in points {
65 | // if let p = wayPoint.pathFromPrevious?.entries {
66 | // for entry in p {
67 | // if entry.trackName != "Close" {
68 | // let track = tracks.first { $0.name == entry.trackName }!
69 | // result += track.points(between: result.last!, and: entry.destination).map { $0.coordinate }
70 | // }
71 | // result.append(entry.destination)
72 | // }
73 | // }
74 | // result.append(wayPoint.coordinate)
75 | // }
76 | // return result
77 | // }
78 | //
79 | // mutating func removeLastWaypoint() {
80 | // guard !points.isEmpty else { return }
81 | // points.removeLast()
82 | // }
83 | //}
84 |
85 | struct DisplayState: Equatable, Codable {
86 | var tracks: [Track]
87 | var tmpPoints: [Coordinate] = []
88 | var graph: Graph? = nil
89 | var loading: Bool { return tracks.isEmpty }
90 |
91 | var selection: Track? {
92 | didSet {
93 | trackPosition = nil
94 | }
95 | }
96 |
97 | var hasSelection: Bool {
98 | return selection != nil
99 | }
100 |
101 | var firstPoint: Coordinate?
102 |
103 | var trackPosition: CGFloat? // 0...1
104 |
105 | init(tracks: [Track]) {
106 | selection = nil
107 | trackPosition = nil
108 | self.tracks = tracks
109 | }
110 |
111 | var route: Route? = nil
112 |
113 | var draggedLocation: (Double, CLLocation)? {
114 | guard let track = selection,
115 | let location = trackPosition else { return nil }
116 | let distance = Double(location) * track.distance
117 | guard let point = track.point(at: distance) else { return nil }
118 | return (distance: distance, location: point)
119 | }
120 | }
121 |
122 |
123 | extension DisplayState {
124 | mutating func addWayPoint(track: Track, coordinate c2d: CLLocationCoordinate2D, segment: Segment) {
125 | guard graph != nil else { return }
126 | let coordinate = Coordinate(c2d)
127 | let d = c2d.squaredDistance(to: segment).squareRoot()
128 | assert(d < 0.1)
129 |
130 | let d0 = segment.0.squaredDistanceApproximation(to: c2d).squareRoot()
131 | let d1 = segment.1.squaredDistanceApproximation(to: c2d).squareRoot()
132 |
133 | let segment0 = Coordinate(segment.0)
134 | let segment1 = Coordinate(segment.1)
135 |
136 | func add(from: Coordinate, _ entry: Graph.Entry) {
137 | graph!.add(from: from, entry)
138 | }
139 | add(from: coordinate, Graph.Entry(destination: segment0, distance: d0, trackName: track.name))
140 | add(from: coordinate, Graph.Entry(destination: segment1, distance: d1, trackName: track.name))
141 |
142 | // todo add a vertex from segment.0 to the graph entry before and after
143 |
144 | if let vertex = track.vertexAfter(coordinate: segment0, graph: graph!) {
145 | add(from: segment0, Graph.Entry(destination: vertex.0, distance: vertex.1, trackName: track.name))
146 | } else {
147 | print("error")
148 | }
149 | if let vertex = track.vertexBefore(coordinate: segment0, graph: graph!) {
150 | add(from: segment0, Graph.Entry(destination: vertex.0, distance: vertex.1, trackName: track.name))
151 | } else {
152 | print("error")
153 | }
154 |
155 | if let vertex = track.vertexAfter(coordinate: segment1, graph: graph!) {
156 | add(from: segment1, Graph.Entry(destination: vertex.0, distance: vertex.1, trackName: track.name))
157 | } else {
158 | print("error")
159 | }
160 | if let vertex = track.vertexBefore(coordinate: segment1, graph: graph!) {
161 | add(from: segment1, Graph.Entry(destination: vertex.0, distance: vertex.1, trackName: track.name))
162 | } else {
163 | print("error")
164 | }
165 |
166 |
167 | if route == nil {
168 | route = Route(track: track, coordinate: coordinate)
169 | } else {
170 | route!.add(coordinate: coordinate, inTrack: track, graph: graph!)
171 | }
172 | }
173 | }
174 |
175 | func polygonRenderer(polygon: MKPolygon, strokeColor: I, fillColor: I, alpha: I, lineWidth: I) -> IBox {
176 | let renderer = MKPolygonRenderer(polygon: polygon)
177 | let box = IBox(renderer)
178 | box.bind(strokeColor, to: \.strokeColor)
179 | box.bind(alpha, to : \.alpha)
180 | box.bind(lineWidth, to: \.lineWidth)
181 | box.bind(fillColor, to: \.fillColor)
182 | return box
183 | }
184 |
185 | func annotation(location: I) -> IBox {
186 | let result = IBox(MKPointAnnotation())
187 | result.bind(location, to: \.coordinate)
188 | return result
189 | }
190 |
191 | extension CLLocationCoordinate2D: Equatable {
192 | public static func ==(lhs: CLLocationCoordinate2D, rhs: CLLocationCoordinate2D) -> Bool {
193 | return lhs.latitude == rhs.latitude && lhs.longitude == rhs.longitude
194 | }
195 | }
196 |
197 | extension Sequence {
198 | var cycled: AnyIterator {
199 | var current = makeIterator()
200 | return AnyIterator {
201 | guard let result = current.next() else {
202 | current = self.makeIterator()
203 | return current.next()
204 | }
205 | return result
206 | }
207 |
208 | }
209 | }
210 |
211 | var debugMapView: MKMapView!
212 |
213 | /// Returns a function that you can call to set the visible map rect
214 | func addMapView(persistent: Input, state: Input, rootView: IBox) -> ((MKMapRect) -> ()) {
215 | var polygonToTrack: [MKPolygon:Track] = [:]
216 | let darkMode = persistent[\.satellite]
217 |
218 | func buildRenderer(_ polygon: MKPolygon) -> IBox {
219 | let track = polygonToTrack[polygon]!
220 | let isSelected = state.i[\.selection].map { $0 == track }
221 | let shouldHighlight = !state.i[\.hasSelection] || isSelected
222 | let lineColor = polygonToTrack[polygon]!.color.uiColor
223 | let fillColor = if_(isSelected, then: lineColor.withAlphaComponent(0.2), else: lineColor.withAlphaComponent(0.1))
224 | return polygonRenderer(polygon: polygon,
225 | strokeColor: I(constant: lineColor),
226 | fillColor: fillColor.map { $0 },
227 | alpha: if_(shouldHighlight, then: I(constant: 1.0), else: if_(darkMode, then: 0.7, else: 1.0)),
228 | lineWidth: if_(shouldHighlight, then: I(constant: 3.0), else: if_(darkMode, then: 1.0, else: 1.0)))
229 | }
230 |
231 | let mapView: IBox = newMapView()
232 | debugMapView = mapView.unbox
233 | rootView.addSubview(mapView, constraints: sizeToParent())
234 |
235 |
236 | var color = NSColor.white
237 | // var colors = cycle(elements: [NSColor.white]) //, .black, .blue, .brown, .cyan, .darkGray, .green, .magenta, .orange])
238 | // MapView
239 | mapView.delegate = MapViewDelegate(rendererForOverlay: { [unowned mapView] mapView_, overlay in
240 | if let polygon = overlay as? MKPolygon {
241 | let renderer = buildRenderer(polygon)
242 | mapView.disposables.append(renderer)
243 | return renderer.unbox
244 | } else if let l = overlay as? MKPolyline {
245 | let renderer = MKPolylineRenderer(polyline: l)
246 | renderer.lineWidth = 5
247 | renderer.strokeColor = color
248 | if l.title == "green" {
249 | renderer.strokeColor = .green
250 | } else if l.title == "blue" {
251 | renderer.strokeColor = .blue
252 | }
253 | return renderer
254 | }
255 | return MKOverlayRenderer()
256 | }, viewForAnnotation: { (mapView, annotation) -> MKAnnotationView? in
257 | guard annotation is MKPointAnnotation else { return nil }
258 | if POI.all.contains(where: { $0.location == annotation.coordinate }) {
259 | let result: MKAnnotationView
260 | result = MKPinAnnotationView(annotation: annotation, reuseIdentifier: nil)
261 | result.canShowCallout = true
262 | return result
263 | } else {
264 | let result = MKPinAnnotationView(annotation: annotation, reuseIdentifier: nil)
265 | result.pinTintColor = annotation.title! == "x" ? .red : .black
266 | result.canShowCallout = true
267 | return result
268 | }
269 | }, regionDidChangeAnimated: { [unowned mapView] _ in
270 | // print(mapView.unbox.region)
271 | }, didSelectAnnotation: { mapView, annotationView in
272 | state.change {
273 | if $0.route != nil && $0.route?.wayPoints.last?.clLocationCoordinate == annotationView.annotation?.coordinate {
274 | $0.route!.removeLastWaypoint()
275 | }
276 | }
277 | })
278 |
279 | mapView.bind(annotations: state.i.map { $0.tmpPoints.map { MKPointAnnotation(coordinate: $0.clLocationCoordinate, title: "" )} })
280 | mapView.disposables.append(state.i.map { $0.tracks }.observe { [unowned mapView] in
281 | mapView.unbox.removeOverlays(mapView.unbox.overlays)
282 | $0.forEach { track in
283 | let polygon = track.polygon
284 | polygonToTrack[polygon] = track
285 | mapView.unbox.add(polygon)
286 | }
287 | })
288 |
289 | // Visualize graphs
290 | /*
291 | mapView.bind(overlays: state.i.map { $0.graph?.items.flatMap({ entry -> [MKPolyline] in
292 | entry.value.map {
293 | let coords = [entry.key.clLocationCoordinate, $0.destination.clLocationCoordinate]
294 | let result = MKPolyline(coordinates: coords, count: 2)
295 | result.title = "green"
296 | return result
297 | }
298 | }) ?? [] })
299 | */
300 |
301 | let waypoints: I<[Coordinate]> = state.i.map { $0.route?.wayPoints ?? [] }
302 | let waypointAnnotations = waypoints.map { coordinates in
303 | coordinates.map {
304 | MKPointAnnotation(coordinate: $0.clLocationCoordinate, title: "")
305 | }
306 | }
307 | mapView.bind(annotations: waypointAnnotations)
308 |
309 |
310 |
311 | let allPoints: I<[Coordinate]> = state.i.map { $0.route?.allPoints(tracks: $0.tracks).map { $0.coordinate } ?? [] }
312 | let lines: I<[MKPolyline]> = allPoints.map {
313 | if $0.isEmpty {
314 | return []
315 | } else {
316 | let coords = $0.map { $0.clLocationCoordinate }
317 | return [MKPolyline(coordinates: coords, count: coords.count)]
318 | }
319 | }
320 | mapView.bind(overlays: lines)
321 |
322 |
323 |
324 | mapView.observe(value: state.i.map { $0.route?.distance }, onChange: { print($1) })
325 |
326 | func addWaypoint(mapView: IBox, sender: NSClickGestureRecognizer) {
327 | let point = sender.location(in: mapView.unbox)
328 | let coordinate = mapView.unbox.convert(point, toCoordinateFrom: mapView.unbox)
329 | let mapPoint = MKMapPointForCoordinate(coordinate)
330 |
331 | let region = MKCoordinateRegionMakeWithDistance(mapView.unbox.centerCoordinate, 1, 1)
332 | let rect = mapView.unbox.convertRegion(region, toRectTo: mapView.unbox)
333 | let meterPerPixel = Double(1/rect.width)
334 | let tresholdPixels: Double = 40
335 | let treshold = meterPerPixel*tresholdPixels
336 |
337 | let possibilities = polygonToTrack.filter { (polygon, track) in
338 | polygon.boundingMapRect.contains(mapPoint)
339 | }
340 |
341 | if let (track, segment) = possibilities.flatMap({ (_,track) in track.segment(closestTo: coordinate, maxDistance: treshold).map { (track, $0) }}).first {
342 | state.change {
343 | let pointOnSegment = coordinate.closestPointOn(segment: segment)
344 | $0.addWayPoint(track: track, coordinate: pointOnSegment, segment: segment)
345 | }
346 | }
347 | }
348 |
349 | mapView.addGestureRecognizer(clickGestureRecognizer({ sender in
350 | addWaypoint(mapView: mapView, sender: sender)
351 | }))
352 |
353 |
354 | mapView.bind(persistent.i.map { $0.satellite ? .hybrid : .standard }, to: \.mapType)
355 |
356 | return { mapView.unbox.setVisibleMapRect($0, animated: true) }
357 | }
358 |
359 | extension CGRect {
360 | init(centerX: CGFloat, centerY: CGFloat, width: CGFloat, height: CGFloat) {
361 | let x = centerX - width/2
362 | let y = centerY - height/2
363 | self = CGRect(x: x, y: y, width: width, height: height)
364 | }
365 | }
366 |
367 |
368 | func time(name: StaticString = #function, line: Int = #line, _ f: () -> Result) -> Result {
369 | let startTime = DispatchTime.now()
370 | let result = f()
371 | let endTime = DispatchTime.now()
372 | let diff = Double(endTime.uptimeNanoseconds - startTime.uptimeNanoseconds) / 1_000_000_000 as Double
373 | print("\(name) (line \(line)): \(diff) sec")
374 | return result
375 | }
376 |
377 |
378 | final class ViewController: NSViewController {
379 | @IBOutlet var _mapView: MKMapView!
380 |
381 | let storedState = Input(StoredState(annotationsVisible: false, satellite: true, showConfiguration: false))
382 | let state = Input(DisplayState(tracks: []))
383 | var rootView: IBox!
384 |
385 | override func viewDidLoad() {
386 | rootView = IBox(view)
387 | let setMapRect = addMapView(persistent: storedState, state: state, rootView: rootView)
388 | setMapRect(MKMapRect(origin: MKMapPoint(x: 143758507.60971117, y: 86968700.835495561), size: MKMapSize(width: 437860.61378830671, height: 749836.27541357279)))
389 | let mapView = self.view.subviews[0] as! MKMapView
390 | DispatchQueue(label: "async").async {
391 | let tracks = Array(Track.load()).map { (track: Track) -> Track in
392 | var copy = track
393 | let before = copy.coordinates.count
394 | copy.coordinates = track.coordinates.douglasPeucker(coordinate: { $0.coordinate.clLocationCoordinate }, squaredEpsilonInMeters: epsilon*epsilon)
395 | return copy
396 | }
397 |
398 | DispatchQueue.main.async {
399 | self.state.change {
400 | $0.tracks = tracks
401 | var rects = $0.tracks.map { $0.polygon.boundingMapRect }
402 | let first = rects.removeFirst()
403 | let boundingBox = rects.reduce(into: first, { (rect1, rect2) in
404 | rect1 = MKMapRectUnion(rect1, rect2)
405 | })
406 | setMapRect(boundingBox)
407 | }
408 | }
409 | time {
410 | buildGraph(tracks: tracks, url: graphURL, progress: { print($0) })
411 | }
412 | let graph = readGraph(url: graphURL)
413 | DispatchQueue.main.async {
414 | self.state.change {
415 | $0.graph = graph
416 | }
417 | }
418 | }
419 | }
420 | }
421 |
422 |
423 | let graphURL = URL(fileURLWithPath: "/Users/chris/Downloads/graph.json")
424 |
--------------------------------------------------------------------------------
/GPXPreprocessing/de.lproj/MainMenu.strings:
--------------------------------------------------------------------------------
1 |
2 | /* Class = "NSMenuItem"; title = "Customize Toolbar…"; ObjectID = "1UK-8n-QPP"; */
3 | "1UK-8n-QPP.title" = "Customize Toolbar…";
4 |
5 | /* Class = "NSMenuItem"; title = "GPXPreprocessing"; ObjectID = "1Xt-HY-uBw"; */
6 | "1Xt-HY-uBw.title" = "GPXPreprocessing";
7 |
8 | /* Class = "NSMenu"; title = "Find"; ObjectID = "1b7-l0-nxx"; */
9 | "1b7-l0-nxx.title" = "Find";
10 |
11 | /* Class = "NSMenuItem"; title = "Lower"; ObjectID = "1tx-W0-xDw"; */
12 | "1tx-W0-xDw.title" = "Lower";
13 |
14 | /* Class = "NSMenuItem"; title = "Raise"; ObjectID = "2h7-ER-AoG"; */
15 | "2h7-ER-AoG.title" = "Raise";
16 |
17 | /* Class = "NSMenuItem"; title = "Transformations"; ObjectID = "2oI-Rn-ZJC"; */
18 | "2oI-Rn-ZJC.title" = "Transformations";
19 |
20 | /* Class = "NSMenu"; title = "Spelling"; ObjectID = "3IN-sU-3Bg"; */
21 | "3IN-sU-3Bg.title" = "Spelling";
22 |
23 | /* Class = "NSMenuItem"; title = "Use Default"; ObjectID = "3Om-Ey-2VK"; */
24 | "3Om-Ey-2VK.title" = "Use Default";
25 |
26 | /* Class = "NSMenu"; title = "Speech"; ObjectID = "3rS-ZA-NoH"; */
27 | "3rS-ZA-NoH.title" = "Speech";
28 |
29 | /* Class = "NSMenuItem"; title = "Tighten"; ObjectID = "46P-cB-AYj"; */
30 | "46P-cB-AYj.title" = "Tighten";
31 |
32 | /* Class = "NSMenuItem"; title = "Find"; ObjectID = "4EN-yA-p0u"; */
33 | "4EN-yA-p0u.title" = "Find";
34 |
35 | /* Class = "NSMenuItem"; title = "Enter Full Screen"; ObjectID = "4J7-dP-txa"; */
36 | "4J7-dP-txa.title" = "Enter Full Screen";
37 |
38 | /* Class = "NSMenuItem"; title = "Quit GPXPreprocessing"; ObjectID = "4sb-4s-VLi"; */
39 | "4sb-4s-VLi.title" = "Quit GPXPreprocessing";
40 |
41 | /* Class = "NSMenuItem"; title = "Edit"; ObjectID = "5QF-Oa-p0T"; */
42 | "5QF-Oa-p0T.title" = "Edit";
43 |
44 | /* Class = "NSMenuItem"; title = "Copy Style"; ObjectID = "5Vv-lz-BsD"; */
45 | "5Vv-lz-BsD.title" = "Copy Style";
46 |
47 | /* Class = "NSMenuItem"; title = "About GPXPreprocessing"; ObjectID = "5kV-Vb-QxS"; */
48 | "5kV-Vb-QxS.title" = "About GPXPreprocessing";
49 |
50 | /* Class = "NSMenuItem"; title = "Redo"; ObjectID = "6dh-zS-Vam"; */
51 | "6dh-zS-Vam.title" = "Redo";
52 |
53 | /* Class = "NSMenuItem"; title = "Correct Spelling Automatically"; ObjectID = "78Y-hA-62v"; */
54 | "78Y-hA-62v.title" = "Correct Spelling Automatically";
55 |
56 | /* Class = "NSMenu"; title = "Writing Direction"; ObjectID = "8mr-sm-Yjd"; */
57 | "8mr-sm-Yjd.title" = "Writing Direction";
58 |
59 | /* Class = "NSMenuItem"; title = "Substitutions"; ObjectID = "9ic-FL-obx"; */
60 | "9ic-FL-obx.title" = "Substitutions";
61 |
62 | /* Class = "NSMenuItem"; title = "Smart Copy/Paste"; ObjectID = "9yt-4B-nSM"; */
63 | "9yt-4B-nSM.title" = "Smart Copy/Paste";
64 |
65 | /* Class = "NSMenu"; title = "Main Menu"; ObjectID = "AYu-sK-qS6"; */
66 | "AYu-sK-qS6.title" = "Main Menu";
67 |
68 | /* Class = "NSMenuItem"; title = "Preferences…"; ObjectID = "BOF-NM-1cW"; */
69 | "BOF-NM-1cW.title" = "Preferences…";
70 |
71 | /* Class = "NSMenuItem"; title = "\tLeft to Right"; ObjectID = "BgM-ve-c93"; */
72 | "BgM-ve-c93.title" = "\tLeft to Right";
73 |
74 | /* Class = "NSMenuItem"; title = "Save As…"; ObjectID = "Bw7-FT-i3A"; */
75 | "Bw7-FT-i3A.title" = "Save As…";
76 |
77 | /* Class = "NSMenuItem"; title = "Close"; ObjectID = "DVo-aG-piG"; */
78 | "DVo-aG-piG.title" = "Close";
79 |
80 | /* Class = "NSMenuItem"; title = "Spelling and Grammar"; ObjectID = "Dv1-io-Yv7"; */
81 | "Dv1-io-Yv7.title" = "Spelling and Grammar";
82 |
83 | /* Class = "NSMenu"; title = "Help"; ObjectID = "F2S-fz-NVQ"; */
84 | "F2S-fz-NVQ.title" = "Help";
85 |
86 | /* Class = "NSMenuItem"; title = "GPXPreprocessing Help"; ObjectID = "FKE-Sm-Kum"; */
87 | "FKE-Sm-Kum.title" = "GPXPreprocessing Help";
88 |
89 | /* Class = "NSMenuItem"; title = "Text"; ObjectID = "Fal-I4-PZk"; */
90 | "Fal-I4-PZk.title" = "Text";
91 |
92 | /* Class = "NSMenu"; title = "Substitutions"; ObjectID = "FeM-D8-WVr"; */
93 | "FeM-D8-WVr.title" = "Substitutions";
94 |
95 | /* Class = "NSMenuItem"; title = "Bold"; ObjectID = "GB9-OM-e27"; */
96 | "GB9-OM-e27.title" = "Bold";
97 |
98 | /* Class = "NSMenu"; title = "Format"; ObjectID = "GEO-Iw-cKr"; */
99 | "GEO-Iw-cKr.title" = "Format";
100 |
101 | /* Class = "NSMenuItem"; title = "Use Default"; ObjectID = "GUa-eO-cwY"; */
102 | "GUa-eO-cwY.title" = "Use Default";
103 |
104 | /* Class = "NSMenuItem"; title = "Font"; ObjectID = "Gi5-1S-RQB"; */
105 | "Gi5-1S-RQB.title" = "Font";
106 |
107 | /* Class = "NSMenuItem"; title = "Writing Direction"; ObjectID = "H1b-Si-o9J"; */
108 | "H1b-Si-o9J.title" = "Writing Direction";
109 |
110 | /* Class = "NSMenuItem"; title = "View"; ObjectID = "H8h-7b-M4v"; */
111 | "H8h-7b-M4v.title" = "View";
112 |
113 | /* Class = "NSMenuItem"; title = "Text Replacement"; ObjectID = "HFQ-gK-NFA"; */
114 | "HFQ-gK-NFA.title" = "Text Replacement";
115 |
116 | /* Class = "NSMenuItem"; title = "Show Spelling and Grammar"; ObjectID = "HFo-cy-zxI"; */
117 | "HFo-cy-zxI.title" = "Show Spelling and Grammar";
118 |
119 | /* Class = "NSMenu"; title = "View"; ObjectID = "HyV-fh-RgO"; */
120 | "HyV-fh-RgO.title" = "View";
121 |
122 | /* Class = "NSMenuItem"; title = "Subscript"; ObjectID = "I0S-gh-46l"; */
123 | "I0S-gh-46l.title" = "Subscript";
124 |
125 | /* Class = "NSMenuItem"; title = "Open…"; ObjectID = "IAo-SY-fd9"; */
126 | "IAo-SY-fd9.title" = "Open…";
127 |
128 | /* Class = "NSMenuItem"; title = "Justify"; ObjectID = "J5U-5w-g23"; */
129 | "J5U-5w-g23.title" = "Justify";
130 |
131 | /* Class = "NSMenuItem"; title = "Use None"; ObjectID = "J7y-lM-qPV"; */
132 | "J7y-lM-qPV.title" = "Use None";
133 |
134 | /* Class = "NSMenuItem"; title = "Revert to Saved"; ObjectID = "KaW-ft-85H"; */
135 | "KaW-ft-85H.title" = "Revert to Saved";
136 |
137 | /* Class = "NSMenuItem"; title = "Show All"; ObjectID = "Kd2-mp-pUS"; */
138 | "Kd2-mp-pUS.title" = "Show All";
139 |
140 | /* Class = "NSMenuItem"; title = "Bring All to Front"; ObjectID = "LE2-aR-0XJ"; */
141 | "LE2-aR-0XJ.title" = "Bring All to Front";
142 |
143 | /* Class = "NSMenuItem"; title = "Paste Ruler"; ObjectID = "LVM-kO-fVI"; */
144 | "LVM-kO-fVI.title" = "Paste Ruler";
145 |
146 | /* Class = "NSMenuItem"; title = "\tLeft to Right"; ObjectID = "Lbh-J2-qVU"; */
147 | "Lbh-J2-qVU.title" = "\tLeft to Right";
148 |
149 | /* Class = "NSMenuItem"; title = "Copy Ruler"; ObjectID = "MkV-Pr-PK5"; */
150 | "MkV-Pr-PK5.title" = "Copy Ruler";
151 |
152 | /* Class = "NSMenuItem"; title = "Services"; ObjectID = "NMo-om-nkz"; */
153 | "NMo-om-nkz.title" = "Services";
154 |
155 | /* Class = "NSMenuItem"; title = "\tDefault"; ObjectID = "Nop-cj-93Q"; */
156 | "Nop-cj-93Q.title" = "\tDefault";
157 |
158 | /* Class = "NSMenuItem"; title = "Minimize"; ObjectID = "OY7-WF-poV"; */
159 | "OY7-WF-poV.title" = "Minimize";
160 |
161 | /* Class = "NSMenuItem"; title = "Baseline"; ObjectID = "OaQ-X3-Vso"; */
162 | "OaQ-X3-Vso.title" = "Baseline";
163 |
164 | /* Class = "NSMenuItem"; title = "Hide GPXPreprocessing"; ObjectID = "Olw-nP-bQN"; */
165 | "Olw-nP-bQN.title" = "Hide GPXPreprocessing";
166 |
167 | /* Class = "NSMenuItem"; title = "Find Previous"; ObjectID = "OwM-mh-QMV"; */
168 | "OwM-mh-QMV.title" = "Find Previous";
169 |
170 | /* Class = "NSMenuItem"; title = "Stop Speaking"; ObjectID = "Oyz-dy-DGm"; */
171 | "Oyz-dy-DGm.title" = "Stop Speaking";
172 |
173 | /* Class = "NSMenuItem"; title = "Bigger"; ObjectID = "Ptp-SP-VEL"; */
174 | "Ptp-SP-VEL.title" = "Bigger";
175 |
176 | /* Class = "NSMenuItem"; title = "Show Fonts"; ObjectID = "Q5e-8K-NDq"; */
177 | "Q5e-8K-NDq.title" = "Show Fonts";
178 |
179 | /* Class = "NSWindow"; title = "GPXPreprocessing"; ObjectID = "QvC-M9-y7g"; */
180 | "QvC-M9-y7g.title" = "GPXPreprocessing";
181 |
182 | /* Class = "NSMenuItem"; title = "Zoom"; ObjectID = "R4o-n2-Eq4"; */
183 | "R4o-n2-Eq4.title" = "Zoom";
184 |
185 | /* Class = "NSMenuItem"; title = "\tRight to Left"; ObjectID = "RB4-Sm-HuC"; */
186 | "RB4-Sm-HuC.title" = "\tRight to Left";
187 |
188 | /* Class = "NSMenuItem"; title = "Superscript"; ObjectID = "Rqc-34-cIF"; */
189 | "Rqc-34-cIF.title" = "Superscript";
190 |
191 | /* Class = "NSMenuItem"; title = "Select All"; ObjectID = "Ruw-6m-B2m"; */
192 | "Ruw-6m-B2m.title" = "Select All";
193 |
194 | /* Class = "NSMenuItem"; title = "Jump to Selection"; ObjectID = "S0p-oC-mLd"; */
195 | "S0p-oC-mLd.title" = "Jump to Selection";
196 |
197 | /* Class = "NSMenu"; title = "Window"; ObjectID = "Td7-aD-5lo"; */
198 | "Td7-aD-5lo.title" = "Window";
199 |
200 | /* Class = "NSMenuItem"; title = "Capitalize"; ObjectID = "UEZ-Bs-lqG"; */
201 | "UEZ-Bs-lqG.title" = "Capitalize";
202 |
203 | /* Class = "NSMenuItem"; title = "Center"; ObjectID = "VIY-Ag-zcb"; */
204 | "VIY-Ag-zcb.title" = "Center";
205 |
206 | /* Class = "NSMenuItem"; title = "Hide Others"; ObjectID = "Vdr-fp-XzO"; */
207 | "Vdr-fp-XzO.title" = "Hide Others";
208 |
209 | /* Class = "NSMenuItem"; title = "Italic"; ObjectID = "Vjx-xi-njq"; */
210 | "Vjx-xi-njq.title" = "Italic";
211 |
212 | /* Class = "NSMenu"; title = "Edit"; ObjectID = "W48-6f-4Dl"; */
213 | "W48-6f-4Dl.title" = "Edit";
214 |
215 | /* Class = "NSMenuItem"; title = "Underline"; ObjectID = "WRG-CD-K1S"; */
216 | "WRG-CD-K1S.title" = "Underline";
217 |
218 | /* Class = "NSMenuItem"; title = "New"; ObjectID = "Was-JA-tGl"; */
219 | "Was-JA-tGl.title" = "New";
220 |
221 | /* Class = "NSMenuItem"; title = "Paste and Match Style"; ObjectID = "WeT-3V-zwk"; */
222 | "WeT-3V-zwk.title" = "Paste and Match Style";
223 |
224 | /* Class = "NSMenuItem"; title = "Find…"; ObjectID = "Xz5-n4-O0W"; */
225 | "Xz5-n4-O0W.title" = "Find…";
226 |
227 | /* Class = "NSMenuItem"; title = "Find and Replace…"; ObjectID = "YEy-JH-Tfz"; */
228 | "YEy-JH-Tfz.title" = "Find and Replace…";
229 |
230 | /* Class = "NSMenuItem"; title = "\tDefault"; ObjectID = "YGs-j5-SAR"; */
231 | "YGs-j5-SAR.title" = "\tDefault";
232 |
233 | /* Class = "NSMenuItem"; title = "Start Speaking"; ObjectID = "Ynk-f8-cLZ"; */
234 | "Ynk-f8-cLZ.title" = "Start Speaking";
235 |
236 | /* Class = "NSMenuItem"; title = "Align Left"; ObjectID = "ZM1-6Q-yy1"; */
237 | "ZM1-6Q-yy1.title" = "Align Left";
238 |
239 | /* Class = "NSMenuItem"; title = "Paragraph"; ObjectID = "ZvO-Gk-QUH"; */
240 | "ZvO-Gk-QUH.title" = "Paragraph";
241 |
242 | /* Class = "NSMenuItem"; title = "Print…"; ObjectID = "aTl-1u-JFS"; */
243 | "aTl-1u-JFS.title" = "Print…";
244 |
245 | /* Class = "NSMenuItem"; title = "Window"; ObjectID = "aUF-d1-5bR"; */
246 | "aUF-d1-5bR.title" = "Window";
247 |
248 | /* Class = "NSMenu"; title = "Font"; ObjectID = "aXa-aM-Jaq"; */
249 | "aXa-aM-Jaq.title" = "Font";
250 |
251 | /* Class = "NSMenuItem"; title = "Use Default"; ObjectID = "agt-UL-0e3"; */
252 | "agt-UL-0e3.title" = "Use Default";
253 |
254 | /* Class = "NSMenuItem"; title = "Show Colors"; ObjectID = "bgn-CT-cEk"; */
255 | "bgn-CT-cEk.title" = "Show Colors";
256 |
257 | /* Class = "NSMenu"; title = "File"; ObjectID = "bib-Uj-vzu"; */
258 | "bib-Uj-vzu.title" = "File";
259 |
260 | /* Class = "NSMenuItem"; title = "Use Selection for Find"; ObjectID = "buJ-ug-pKt"; */
261 | "buJ-ug-pKt.title" = "Use Selection for Find";
262 |
263 | /* Class = "NSMenu"; title = "Transformations"; ObjectID = "c8a-y6-VQd"; */
264 | "c8a-y6-VQd.title" = "Transformations";
265 |
266 | /* Class = "NSMenuItem"; title = "Use None"; ObjectID = "cDB-IK-hbR"; */
267 | "cDB-IK-hbR.title" = "Use None";
268 |
269 | /* Class = "NSMenuItem"; title = "Selection"; ObjectID = "cqv-fj-IhA"; */
270 | "cqv-fj-IhA.title" = "Selection";
271 |
272 | /* Class = "NSMenuItem"; title = "Smart Links"; ObjectID = "cwL-P1-jid"; */
273 | "cwL-P1-jid.title" = "Smart Links";
274 |
275 | /* Class = "NSMenuItem"; title = "Make Lower Case"; ObjectID = "d9M-CD-aMd"; */
276 | "d9M-CD-aMd.title" = "Make Lower Case";
277 |
278 | /* Class = "NSMenu"; title = "Text"; ObjectID = "d9c-me-L2H"; */
279 | "d9c-me-L2H.title" = "Text";
280 |
281 | /* Class = "NSMenuItem"; title = "File"; ObjectID = "dMs-cI-mzQ"; */
282 | "dMs-cI-mzQ.title" = "File";
283 |
284 | /* Class = "NSMenuItem"; title = "Undo"; ObjectID = "dRJ-4n-Yzg"; */
285 | "dRJ-4n-Yzg.title" = "Undo";
286 |
287 | /* Class = "NSMenuItem"; title = "Paste"; ObjectID = "gVA-U4-sdL"; */
288 | "gVA-U4-sdL.title" = "Paste";
289 |
290 | /* Class = "NSMenuItem"; title = "Smart Quotes"; ObjectID = "hQb-2v-fYv"; */
291 | "hQb-2v-fYv.title" = "Smart Quotes";
292 |
293 | /* Class = "NSMenuItem"; title = "Check Document Now"; ObjectID = "hz2-CU-CR7"; */
294 | "hz2-CU-CR7.title" = "Check Document Now";
295 |
296 | /* Class = "NSMenu"; title = "Services"; ObjectID = "hz9-B4-Xy5"; */
297 | "hz9-B4-Xy5.title" = "Services";
298 |
299 | /* Class = "NSMenuItem"; title = "Smaller"; ObjectID = "i1d-Er-qST"; */
300 | "i1d-Er-qST.title" = "Smaller";
301 |
302 | /* Class = "NSMenu"; title = "Baseline"; ObjectID = "ijk-EB-dga"; */
303 | "ijk-EB-dga.title" = "Baseline";
304 |
305 | /* Class = "NSMenuItem"; title = "Kern"; ObjectID = "jBQ-r6-VK2"; */
306 | "jBQ-r6-VK2.title" = "Kern";
307 |
308 | /* Class = "NSMenuItem"; title = "\tRight to Left"; ObjectID = "jFq-tB-4Kx"; */
309 | "jFq-tB-4Kx.title" = "\tRight to Left";
310 |
311 | /* Class = "NSMenuItem"; title = "Format"; ObjectID = "jxT-CU-nIS"; */
312 | "jxT-CU-nIS.title" = "Format";
313 |
314 | /* Class = "NSMenuItem"; title = "Show Sidebar"; ObjectID = "kIP-vf-haE"; */
315 | "kIP-vf-haE.title" = "Show Sidebar";
316 |
317 | /* Class = "NSMenuItem"; title = "Check Grammar With Spelling"; ObjectID = "mK6-2p-4JG"; */
318 | "mK6-2p-4JG.title" = "Check Grammar With Spelling";
319 |
320 | /* Class = "NSMenuItem"; title = "Ligatures"; ObjectID = "o6e-r0-MWq"; */
321 | "o6e-r0-MWq.title" = "Ligatures";
322 |
323 | /* Class = "NSMenu"; title = "Open Recent"; ObjectID = "oas-Oc-fiZ"; */
324 | "oas-Oc-fiZ.title" = "Open Recent";
325 |
326 | /* Class = "NSMenuItem"; title = "Loosen"; ObjectID = "ogc-rX-tC1"; */
327 | "ogc-rX-tC1.title" = "Loosen";
328 |
329 | /* Class = "NSMenuItem"; title = "Delete"; ObjectID = "pa3-QI-u2k"; */
330 | "pa3-QI-u2k.title" = "Delete";
331 |
332 | /* Class = "NSMenuItem"; title = "Save…"; ObjectID = "pxx-59-PXV"; */
333 | "pxx-59-PXV.title" = "Save…";
334 |
335 | /* Class = "NSMenuItem"; title = "Find Next"; ObjectID = "q09-fT-Sye"; */
336 | "q09-fT-Sye.title" = "Find Next";
337 |
338 | /* Class = "NSMenuItem"; title = "Page Setup…"; ObjectID = "qIS-W8-SiK"; */
339 | "qIS-W8-SiK.title" = "Page Setup…";
340 |
341 | /* Class = "NSMenuItem"; title = "Check Spelling While Typing"; ObjectID = "rbD-Rh-wIN"; */
342 | "rbD-Rh-wIN.title" = "Check Spelling While Typing";
343 |
344 | /* Class = "NSMenuItem"; title = "Smart Dashes"; ObjectID = "rgM-f4-ycn"; */
345 | "rgM-f4-ycn.title" = "Smart Dashes";
346 |
347 | /* Class = "NSMenuItem"; title = "Show Toolbar"; ObjectID = "snW-S8-Cw5"; */
348 | "snW-S8-Cw5.title" = "Show Toolbar";
349 |
350 | /* Class = "NSMenuItem"; title = "Data Detectors"; ObjectID = "tRr-pd-1PS"; */
351 | "tRr-pd-1PS.title" = "Data Detectors";
352 |
353 | /* Class = "NSMenuItem"; title = "Open Recent"; ObjectID = "tXI-mr-wws"; */
354 | "tXI-mr-wws.title" = "Open Recent";
355 |
356 | /* Class = "NSMenu"; title = "Kern"; ObjectID = "tlD-Oa-oAM"; */
357 | "tlD-Oa-oAM.title" = "Kern";
358 |
359 | /* Class = "NSMenu"; title = "GPXPreprocessing"; ObjectID = "uQy-DD-JDr"; */
360 | "uQy-DD-JDr.title" = "GPXPreprocessing";
361 |
362 | /* Class = "NSMenuItem"; title = "Cut"; ObjectID = "uRl-iY-unG"; */
363 | "uRl-iY-unG.title" = "Cut";
364 |
365 | /* Class = "NSMenuItem"; title = "Paste Style"; ObjectID = "vKC-jM-MkH"; */
366 | "vKC-jM-MkH.title" = "Paste Style";
367 |
368 | /* Class = "NSMenuItem"; title = "Show Ruler"; ObjectID = "vLm-3I-IUL"; */
369 | "vLm-3I-IUL.title" = "Show Ruler";
370 |
371 | /* Class = "NSMenuItem"; title = "Clear Menu"; ObjectID = "vNY-rz-j42"; */
372 | "vNY-rz-j42.title" = "Clear Menu";
373 |
374 | /* Class = "NSMenuItem"; title = "Make Upper Case"; ObjectID = "vmV-6d-7jI"; */
375 | "vmV-6d-7jI.title" = "Make Upper Case";
376 |
377 | /* Class = "NSMenu"; title = "Ligatures"; ObjectID = "w0m-vy-SC9"; */
378 | "w0m-vy-SC9.title" = "Ligatures";
379 |
380 | /* Class = "NSMenuItem"; title = "Align Right"; ObjectID = "wb2-vD-lq4"; */
381 | "wb2-vD-lq4.title" = "Align Right";
382 |
383 | /* Class = "NSMenuItem"; title = "Help"; ObjectID = "wpr-3q-Mcd"; */
384 | "wpr-3q-Mcd.title" = "Help";
385 |
386 | /* Class = "NSMenuItem"; title = "Copy"; ObjectID = "x3v-GG-iWU"; */
387 | "x3v-GG-iWU.title" = "Copy";
388 |
389 | /* Class = "NSMenuItem"; title = "Use All"; ObjectID = "xQD-1f-W4t"; */
390 | "xQD-1f-W4t.title" = "Use All";
391 |
392 | /* Class = "NSMenuItem"; title = "Speech"; ObjectID = "xrE-MZ-jX0"; */
393 | "xrE-MZ-jX0.title" = "Speech";
394 |
395 | /* Class = "NSMenuItem"; title = "Show Substitutions"; ObjectID = "z6F-FW-3nz"; */
396 | "z6F-FW-3nz.title" = "Show Substitutions";
397 |
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | source "https://rubygems.org"
2 |
3 | gem "fastlane"
4 |
--------------------------------------------------------------------------------
/Incremental copy-Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | FMWK
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleVersion
20 | $(CURRENT_PROJECT_VERSION)
21 | NSPrincipalClass
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/Incremental/Helpers.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Helpers.swift
3 | // Incremental
4 | //
5 | // Created by Chris Eidhof on 19.09.17.
6 | // Copyright © 2017 objc.io. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | struct Register {
12 | typealias Token = Int
13 | private var items: [Token:A] = [:]
14 | private let freshNumber: () -> Int
15 | init() {
16 | var iterator = (0...).makeIterator()
17 | freshNumber = { iterator.next()! }
18 | }
19 |
20 | @discardableResult
21 | mutating func add(_ value: A) -> Token {
22 | let token = freshNumber()
23 | items[token] = value
24 | return token
25 | }
26 |
27 | mutating func remove(_ token: Token) {
28 | items[token] = nil
29 | }
30 |
31 | subscript(token: Token) -> A? {
32 | return items[token]
33 | }
34 |
35 | var values: AnySequence {
36 | return AnySequence(items.values)
37 | }
38 |
39 | mutating func removeAll() {
40 | items = [:]
41 | }
42 |
43 | var keys: AnySequence {
44 | return AnySequence(items.keys)
45 | }
46 | }
47 |
48 | public final class Disposable {
49 | private let dispose: () -> ()
50 | public init(dispose: @escaping () -> ()) {
51 | self.dispose = dispose
52 | }
53 |
54 | deinit {
55 | self.dispose()
56 | }
57 | }
58 |
59 | struct Height: CustomStringConvertible, Comparable {
60 | var value: Int
61 |
62 | init(_ value: Int = 0) {
63 | self.value = value
64 | }
65 |
66 | static let zero = Height(0)
67 | static let minusOne = Height(-1) // observers
68 |
69 | mutating func join(_ other: Height) {
70 | value = max(value, other.value)
71 | }
72 |
73 | func incremented() -> Height {
74 | return Height(value + 1)
75 | }
76 |
77 | var description: String {
78 | return "Height(\(value))"
79 | }
80 |
81 | static func <(lhs: Height, rhs: Height) -> Bool {
82 | return lhs.value < rhs.value
83 | }
84 |
85 | static func ==(lhs: Height, rhs: Height) -> Bool {
86 | return lhs.value == rhs.value
87 | }
88 |
89 | static func +(lhs: Height, rhs: Height) -> Height {
90 | return Height(lhs.value + rhs.value)
91 | }
92 | }
93 |
94 | extension Sequence where Element == Height {
95 | var leastUpperBound: Height {
96 | return reduce(into: .zero, { $0.join($1) })
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/Incremental/Incremental+NSObject.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Incremental+NSObject.swift
3 | // Incremental
4 | //
5 | // Created by Chris Eidhof on 18.09.17.
6 | // Copyright © 2017 objc.io. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 |
12 | // Could this be in a conditional block? Only works for Foundation w/ ObjC runtime
13 | extension NSObjectProtocol where Self: NSObject {
14 | public subscript(_ keyPath: KeyPath) -> I where Value: Equatable {
15 | let i: I = I(value: self[keyPath: keyPath])
16 | let observation = observe(keyPath) { (obj, change) in
17 | i.write(obj[keyPath: keyPath])
18 | }
19 | i.strongReferences.add(observation)
20 | return i
21 | }
22 | }
23 |
24 | public final class IBox: Equatable {
25 | public private(set) var unbox: V
26 | public var disposables: [Any] = []
27 |
28 | public init(_ object: V) {
29 | self.unbox = object
30 | }
31 |
32 | public func bind(_ value: I, to: WritableKeyPath) {
33 | disposables.append(value.observe { [unowned self] in
34 | self.unbox[keyPath: to] = $0
35 | })
36 | }
37 |
38 | public func bind(_ value: I, to: WritableKeyPath) where A: Equatable {
39 | disposables.append(value.observe { [unowned self] in
40 | self.unbox[keyPath: to] = $0
41 | })
42 | }
43 |
44 | public func observe(value: I, onChange: @escaping (V,A) -> ()) {
45 | disposables.append(value.observe { newValue in
46 | onChange(self.unbox,newValue) // ownership?
47 | })
48 | }
49 |
50 | public static func ==(lhs: IBox, rhs: IBox) -> Bool {
51 | return lhs === rhs
52 | }
53 |
54 | /// This also copies the `disposables`
55 | public func map(_ transform: (V) -> B) -> IBox {
56 | let result = IBox(transform(unbox))
57 | result.disposables.append(self)
58 | return result
59 | }
60 | }
61 |
62 |
63 | extension IBox where V: NSObject {
64 | public subscript(keyPath: KeyPath) -> I where A: Equatable {
65 | get {
66 | return unbox[keyPath]
67 | }
68 | }
69 | }
70 |
71 | extension NSObjectProtocol where Self: NSObject {
72 | /// One-way binding
73 | public func bind(keyPath: ReferenceWritableKeyPath, _ i: I) -> Disposable {
74 | return i.observe {
75 | self[keyPath: keyPath] = $0
76 | }
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/Incremental/Incremental+UIKit/ActivityIndicator.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ActivityIndicator.swift
3 | // Incremental
4 | //
5 | // Created by Chris Eidhof on 27.09.17.
6 | // Copyright © 2017 objc.io. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | extension UIActivityIndicatorView {
12 | var animating: Bool {
13 | get { return isAnimating }
14 | set {
15 | if newValue { self.startAnimating() }
16 | else { self.stopAnimating() }
17 | }
18 | }
19 | }
20 |
21 | public func activityIndicator(style: I = I(constant: .white), animating: I) -> IBox {
22 | let loadingIndicator = IBox(UIActivityIndicatorView())
23 | loadingIndicator.unbox.hidesWhenStopped = true
24 | loadingIndicator.bind(animating, to: \.animating)
25 | loadingIndicator.bind(style, to: \.style)
26 | return loadingIndicator.cast
27 | }
28 |
--------------------------------------------------------------------------------
/Incremental/Incremental+UIKit/Button.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Button.swift
3 | // Incremental
4 | //
5 | // Created by Chris Eidhof on 27.09.17.
6 | // Copyright © 2017 objc.io. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | public func button(type: UIButton.ButtonType = .custom, backgroundColor: I = I(constant: .white), tintColor: I, onTap: @escaping () -> ()) -> IBox {
12 | let result = IBox(UIButton(type: type))
13 | result.bind(backgroundColor, to: \.backgroundColor)
14 | result.observe(value: tintColor, onChange: { $0.tintColor = $1 })
15 | result.handle(.touchUpInside, onTap)
16 | return result
17 | }
18 |
19 | public func button(type: UIButton.ButtonType = .custom, title: I, backgroundColor: I = I(constant: .white), titleColor: I = I(constant: nil), onTap: @escaping () -> ()) -> IBox {
20 | let result = IBox(UIButton(type: type))
21 | result.bind(backgroundColor, to: \.backgroundColor)
22 | result.observe(value: title, onChange: { $0.setTitle($1, for: .normal) })
23 | result.observe(value: titleColor, onChange: { $0.setTitleColor($1, for: .normal)})
24 | result.handle(.touchUpInside, onTap)
25 | return result
26 | }
27 |
28 | // todo dry
29 | public func button(type: UIButton.ButtonType = .custom, titleImage: I, backgroundColor: I = I(constant: .white), onTap: @escaping () -> ()) -> IBox {
30 | let result = IBox(UIButton(type: type))
31 | result.bind(backgroundColor, to: \.backgroundColor)
32 | result.observe(value: titleImage, onChange: { $0.setImage($1, for: .normal) })
33 | result.handle(.touchUpInside, onTap)
34 | return result
35 | }
36 |
37 | extension IBox where V: UIControl {
38 | public func handle(_ events: UIControl.Event, _ handler: @escaping () -> ()) {
39 | let ta = TargetAction(handler)
40 | unbox.addTarget(ta, action: #selector(TargetAction.action(_:)), for: events)
41 | disposables.append(ta)
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/Incremental/Incremental+UIKit/Constraints.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Constraints.swift
3 | // Incremental
4 | //
5 | // Created by Chris Eidhof on 27.09.17.
6 | // Copyright © 2017 objc.io. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 |
12 |
13 | public typealias Constraint = (_ parent: IncView, _ child: IncView) -> IBox
14 |
15 | public typealias Animation = (_ parent: IncView, _ child: IncView) -> ()
16 |
17 | public let noAnimation: Animation = { _,_ in () }
18 |
19 | public func equal(_ keyPath: KeyPath, to: KeyPath, constant: I = I(constant: 0), animation: @escaping Animation = noAnimation) -> Constraint where Anchor: NSLayoutAnchor {
20 | return { parent, child in
21 | let result = IBox(parent[keyPath: keyPath].constraint(equalTo: child[keyPath: keyPath]))
22 | result.bindConstant(constant, animation: { animation(parent, child) } )
23 | return result
24 | }
25 | }
26 |
27 | public func equal(_ keyPath: KeyPath, to: KeyPath, constant: CGFloat, animation: @escaping Animation = noAnimation) -> Constraint where Anchor: NSLayoutAnchor {
28 | return equal(keyPath, to: to, constant: I(constant: constant), animation: animation)
29 | }
30 |
31 | public func sizeToParent(inset constant: I = I(constant: 0), animation: @escaping Animation = noAnimation) -> [Constraint] {
32 | return [equal(\.leadingAnchor, constant: -constant, animation: animation),
33 | equal(\.trailingAnchor, constant: constant, animation: animation),
34 | equal(\.topAnchor, constant: -constant, animation: animation),
35 | equal(\.bottomAnchor, constant: constant, animation: animation)]
36 | }
37 |
38 | public func equal(_ keyPath: KeyPath, constant: I = I(constant: 0), animation: @escaping Animation = noAnimation) -> Constraint where Anchor: NSLayoutAnchor {
39 | return equal(keyPath, to: keyPath, constant: constant, animation: animation)
40 | }
41 |
42 | public func equal(_ keyPath: KeyPath, _ constant: CGFloat, animation: @escaping Animation = noAnimation) -> Constraint where Anchor: NSLayoutAnchor {
43 | return equal(keyPath, to: keyPath, constant: I(constant: constant), animation: animation)
44 | }
45 |
46 |
47 | public func equalTo(constant: I = I(constant: 0), _ keyPath: KeyPath, animation: @escaping Animation = noAnimation) -> Constraint {
48 | return { parent, child in
49 | let constraint = IBox(child[keyPath: keyPath].constraint(equalToConstant: 0))
50 | constraint.bindConstant(constant, animation: { animation(parent, child) } )
51 | return constraint
52 | }
53 | }
54 |
55 | extension IBox where V: NSLayoutConstraint {
56 | public func bindConstant(_ i: I, animation: @escaping () -> ()) {
57 | disposables.append((i).observe { [unowned self] newValue in
58 | self.unbox.constant = newValue
59 | animation()
60 | })
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/Incremental/Incremental+UIKit/Driver.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Driver.swift
3 | // Incremental
4 | //
5 | // Created by Chris Eidhof on 11-10-17.
6 | // Copyright © 2017 objc.io. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
--------------------------------------------------------------------------------
/Incremental/Incremental+UIKit/Incremental+NSGestureRecognizer.swift:
--------------------------------------------------------------------------------
1 | // Incremental
2 | //
3 | // Created by Chris Eidhof on 22.09.17.
4 | // Copyright © 2017 objc.io. All rights reserved.
5 | //
6 |
7 | import Cocoa
8 |
9 | public func clickGestureRecognizer(_ tapped: @escaping (NSClickGestureRecognizer) -> ()) -> IBox {
10 | let recognizer = NSClickGestureRecognizer()
11 | let targetAction = TargetAction { tapped(recognizer) }
12 | recognizer.target = targetAction
13 | recognizer.action = #selector(TargetAction.action(_:))
14 | let result = IBox(recognizer)
15 | result.disposables.append(targetAction)
16 | return result
17 | }
18 |
19 | extension IBox where V: NSView {
20 | public func addGestureRecognizer(_ gestureRecognizer: IBox) {
21 | self.unbox.addGestureRecognizer(gestureRecognizer.unbox)
22 | disposables.append(gestureRecognizer)
23 | }
24 | }
25 |
26 |
--------------------------------------------------------------------------------
/Incremental/Incremental+UIKit/Incremental+UIGestureRecognizer.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Incremental+UIKit.swift
3 | // Incremental
4 | //
5 | // Created by Chris Eidhof on 22.09.17.
6 | // Copyright © 2017 objc.io. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | // todo dry
12 | public func panGestureRecognizer(_ panned: @escaping (UIPanGestureRecognizer) -> ()) -> IBox {
13 | let recognizer = UIPanGestureRecognizer()
14 | let targetAction = TargetAction { panned(recognizer) }
15 | recognizer.addTarget(targetAction, action: #selector(TargetAction.action(_:)))
16 | let result = IBox(recognizer)
17 | result.disposables.append(targetAction)
18 | return result
19 | }
20 |
21 | public func tapGestureRecognizer(_ tapped: @escaping (UITapGestureRecognizer) -> ()) -> IBox {
22 | let recognizer = UITapGestureRecognizer()
23 | let targetAction = TargetAction { tapped(recognizer) }
24 | recognizer.addTarget(targetAction, action: #selector(TargetAction.action(_:)))
25 | let result = IBox(recognizer)
26 | result.disposables.append(targetAction)
27 | return result
28 | }
29 |
30 | extension IBox where V: UIView {
31 | public func addGestureRecognizer(_ gestureRecognizer: IBox) {
32 | self.unbox.addGestureRecognizer(gestureRecognizer.unbox)
33 | disposables.append(gestureRecognizer)
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/Incremental/Incremental+UIKit/Incremental+UIKit+AppKit.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Incremental+UIKit+AppKit.swift
3 | // Incremental
4 | //
5 | // Created by Chris Eidhof on 12.11.17.
6 | // Copyright © 2017 objc.io. All rights reserved.
7 | //
8 |
9 | #if os(OSX)
10 | import Cocoa
11 | public typealias IncColor = NSColor
12 | public typealias IncView = NSView
13 | #else
14 | import UIKit
15 | public typealias IncView = UIView
16 | public typealias IncColor = UIColor
17 | #endif
18 |
19 |
20 | #if os(OSX)
21 | extension NSView {
22 | func insertSubview(_ s: NSView, at index: Int) {
23 | self.subviews.insert(s, at: index)
24 | }
25 | }
26 | #endif
27 |
28 |
29 | extension IncView {
30 | public func addSubview(_ subview: V, constraints: [NSLayoutConstraint]) {
31 | addSubview(subview)
32 | if !constraints.isEmpty {
33 | subview.translatesAutoresizingMaskIntoConstraints = false
34 | NSLayoutConstraint.activate(constraints)
35 | }
36 |
37 | }
38 | }
39 |
40 |
41 | extension IBox where V: IncView {
42 | public func addSubview(_ subview: IBox, path: KeyPath? = nil, constraints: [Constraint] = []) where S: IncView {
43 | disposables.append(subview)
44 | let target: IncView = path.map { kp in unbox[keyPath: kp] } ?? unbox
45 | let evaluatedConstraints = constraints.map { $0(self.unbox, subview.unbox) }
46 | target.addSubview(subview.unbox, constraints: evaluatedConstraints.map { $0.unbox })
47 | disposables.append(evaluatedConstraints)
48 |
49 | }
50 |
51 | private func insert(_ subview: IBox, at index: Int) {
52 | self.disposables.append(subview)
53 | self.unbox.insertSubview(subview.unbox, at: index)
54 | }
55 |
56 | private func remove(at index: Int, ofType: View.Type) {
57 | let oldView = self.unbox.subviews[index] as! View
58 | guard let i = self.disposables.firstIndex(where: {
59 | if let oldDisposable = $0 as? IBox, oldDisposable.unbox == oldView {
60 | return true
61 | }
62 | return false
63 | }) else {
64 | fatalError()
65 | }
66 | self.disposables.remove(at: i)
67 | oldView.removeFromSuperview()
68 | }
69 |
70 | public func bindSubviews(_ iArray: I>>) {
71 | // todo replace with custom array observing
72 | disposables.append(iArray.observe { value in // todo owernship of self?
73 | assert(self.unbox.subviews.isEmpty)
74 | for view in value.initial { self.unbox.addSubview(view.unbox) }
75 | value.changes.read { changeList in
76 | return changeList.reduce(eq: { _,_ in false }, initial: (), combine: { (change, _) in
77 |
78 | switch change {
79 | case let .insert(subview, index):
80 | self.insert(subview, at: index)
81 | case .remove(let index):
82 | self.remove(at: index, ofType: View.self)
83 | case let .replace(with: subview, at: i):
84 | self.insert(subview, at: i)
85 | self.remove(at: i+1, ofType: View.self)
86 | case let .move(at: i, to: j):
87 | let view = self.unbox.subviews[i]
88 | view.removeFromSuperview()
89 | let offset = j > i ? -1 : 0
90 | self.unbox.insertSubview(view, at: j + offset)
91 |
92 | }
93 | return ()
94 | })
95 | }
96 | })
97 | }
98 |
99 | public var cast: IBox {
100 | return map { $0 }
101 | }
102 | }
103 |
104 | public class TargetAction: NSObject {
105 | let callback: () -> ()
106 | public init(_ callback: @escaping () -> ()) {
107 | self.callback = callback
108 | }
109 | @objc public func action(_ sender: AnyObject) {
110 | callback()
111 | }
112 | }
113 |
--------------------------------------------------------------------------------
/Incremental/Incremental+UIKit/Label.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Label.swift
3 | // Incremental
4 | //
5 | // Created by Chris Eidhof on 27.09.17.
6 | // Copyright © 2017 objc.io. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | public func label(text: I, backgroundColor: I = I(constant: .clear), textColor: I = I(constant: .black)) -> IBox {
12 | let result = IBox(UILabel(frame: .zero))
13 | result.bind(text, to: \.text)
14 | result.observe(value: textColor, onChange: { $0.textColor = $1 }) // doesn't work with bind because textColor is an IOU
15 | result.bind(backgroundColor, to: \.backgroundColor)
16 | return result
17 | }
18 |
19 |
20 | public func label(text: I, backgroundColor: I = I(constant: .clear), textColor: I = I(constant: .black), font: I) -> IBox {
21 | let result = IBox(UILabel(frame: .zero))
22 | result.bind(text, to: \.text)
23 | result.observe(value: textColor, onChange: { $0.textColor = $1 }) // doesn't work with bind because textColor is an IOU
24 | result.bind(backgroundColor, to: \.backgroundColor)
25 | result.observe(value: font, onChange: { $0.font = $1 })
26 | return result
27 | }
28 |
--------------------------------------------------------------------------------
/Incremental/Incremental+UIKit/MapView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MapView.swift
3 | // Incremental
4 | //
5 | // Created by Chris Eidhof on 18.10.17.
6 | // Copyright © 2017 objc.io. All rights reserved.
7 | //
8 |
9 | import MapKit
10 |
11 | extension IBox where V: MKMapView {
12 | public func bind(annotations: [MKPointAnnotation], visible: I) {
13 | disposables.append(visible.observe { [unowned self] value in
14 | if value {
15 | self.unbox.addAnnotations(annotations)
16 | } else {
17 | self.unbox.removeAnnotations(annotations)
18 | }
19 | })
20 |
21 | }
22 |
23 | public func bind(annotations: I<[MKPointAnnotation]>) {
24 | var previous: [MKPointAnnotation]? = nil
25 | disposables.append(annotations.observe { [unowned self] value in
26 | if let p = previous {
27 | self.unbox.removeAnnotations(p)
28 | }
29 | self.unbox.addAnnotations(value)
30 | previous = value
31 | })
32 |
33 | }
34 |
35 | public func bind(overlays: I<[O]>) {
36 | var previous: [O]? = nil
37 | disposables.append(overlays.observe { [unowned self] value in
38 | if let p = previous {
39 | self.unbox.removeOverlays(p)
40 | }
41 | self.unbox.addOverlays(value)
42 | previous = value
43 | })
44 |
45 | }
46 |
47 | public var delegate: MKMapViewDelegate? {
48 | get { return unbox.delegate }
49 | set {
50 | if let existing = disposables.firstIndex(where: { ($0 as? MKMapViewDelegate) === unbox.delegate }) {
51 | disposables.remove(at: existing)
52 | }
53 | if let value = newValue { disposables.append(value) }
54 | unbox.delegate = newValue
55 | }
56 | }
57 | }
58 |
59 | public final class MapViewDelegate: NSObject, MKMapViewDelegate {
60 | let rendererForOverlay: (_ mapView: MKMapView, _ overlay: MKOverlay) -> MKOverlayRenderer
61 | let viewForAnnotation: (_ mapView: MKMapView, _ annotation: MKAnnotation) -> MKAnnotationView?
62 | let regionDidChangeAnimated: (_ mapView: MKMapView) -> ()
63 | let didSelectAnnotation: ((_ mapView: MKMapView, _ annotation: MKAnnotationView) -> ())?
64 |
65 | public init(rendererForOverlay: @escaping (_ mapView: MKMapView, _ overlay: MKOverlay) -> MKOverlayRenderer,
66 | viewForAnnotation: @escaping (_ mapView: MKMapView, _ annotation: MKAnnotation) -> MKAnnotationView?,
67 | regionDidChangeAnimated: @escaping (_ mapView: MKMapView) -> (),
68 | didSelectAnnotation: ((_ mapView: MKMapView, _ annotation: MKAnnotationView) -> ())? = nil) {
69 | self.rendererForOverlay = rendererForOverlay
70 | self.viewForAnnotation = viewForAnnotation
71 | self.regionDidChangeAnimated = regionDidChangeAnimated
72 | self.didSelectAnnotation = didSelectAnnotation
73 | }
74 |
75 |
76 | public func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer {
77 | return rendererForOverlay(mapView, overlay)
78 | }
79 |
80 | public func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
81 | return viewForAnnotation(mapView, annotation)
82 | }
83 |
84 | public func mapView(_ mapView: MKMapView, didSelect view: MKAnnotationView) {
85 | didSelectAnnotation?(mapView, view)
86 | }
87 |
88 | public func mapView(_ mapView: MKMapView, regionDidChangeAnimated animated: Bool) {
89 | return regionDidChangeAnimated(mapView)
90 | }
91 | }
92 |
93 | public func newMapView() -> IBox {
94 | let box = IBox(MKMapView())
95 | let view = box.unbox
96 | view.showsCompass = true
97 | view.showsScale = true
98 | view.showsUserLocation = true
99 | view.mapType = .standard
100 | view.isRotateEnabled = false
101 | view.isPitchEnabled = false
102 | return box
103 | }
104 |
105 | public func polygonRenderer(polygon: MKPolygon, accessibilityValue: String, strokeColor: I, fillColor: I, alpha: I, lineWidth: I) -> IBox {
106 | let renderer = MKPolygonRenderer(polygon: polygon)
107 | // renderer.accessibilityValue = accessibilityValue
108 | let box = IBox(renderer)
109 | box.bind(strokeColor, to: \.strokeColor)
110 | box.bind(alpha, to : \.alpha)
111 | box.bind(lineWidth, to: \.lineWidth)
112 | box.bind(fillColor, to: \.fillColor)
113 | return box
114 | }
115 |
116 | public func annotation(location: I) -> IBox {
117 | let result = IBox(MKPointAnnotation())
118 | result.bind(location, to: \.coordinate)
119 | return result
120 | }
121 |
--------------------------------------------------------------------------------
/Incremental/Incremental+UIKit/NavigationController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // NavigationController.swift
3 | // Incremental
4 | //
5 | // Created by Chris Eidhof on 02.10.17.
6 | // Copyright © 2017 objc.io. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | public func barButtonItem(systemItem: UIBarButtonItem.SystemItem, onTap: @escaping () -> ()) -> IBox {
12 | let ta = TargetAction(onTap)
13 | let result = UIBarButtonItem(barButtonSystemItem: systemItem, target: ta, action: #selector(TargetAction.action(_:)))
14 | let box = IBox(result)
15 | box.disposables.append(ta)
16 | return box
17 | }
18 |
19 | public func navigationController(_ viewControllers: ArrayWithHistory>) -> IBox {
20 | let nc = UINavigationController()
21 | let result = IBox(nc)
22 | result.bindViewControllers(to: viewControllers)
23 | return result
24 | }
25 |
26 | extension IBox where V: UINavigationController {
27 | func appendViewController(_ vc: IBox) {
28 | self.unbox.viewControllers.append(vc.unbox)
29 | self.disposables.append(vc)
30 | }
31 |
32 | public func bindViewControllers(to value: ArrayWithHistory>) {
33 | self.disposables.append(value.observe(current: { initialVCs in
34 | assert(self.unbox.viewControllers == [])
35 | for v in initialVCs {
36 | self.appendViewController(v)
37 | }
38 | }) { [unowned self] in
39 | switch $0 {
40 | case let .insert(v, at: i):
41 | if i == self.unbox.viewControllers.count {
42 | self.unbox.pushViewController(v.unbox, animated: true)
43 | } else {
44 | self.unbox.viewControllers.insert(v.unbox, at: i)
45 | }
46 | self.disposables.append(v)
47 | case .remove(at: let i):
48 | let v: UIViewController = self.unbox.viewControllers[i]
49 | let index = self.disposables.firstIndex { d in
50 | if let vcBox = d as? IBox, vcBox.unbox === v {
51 | return true
52 | }
53 | return false
54 | }
55 | self.disposables.remove(at: index!)
56 | self.unbox.viewControllers.remove(at: i)
57 | case .replace(let with, let i):
58 | let v: UIViewController = self.unbox.viewControllers[i]
59 | let index = self.disposables.firstIndex { d in
60 | if let vcBox = d as? IBox, vcBox.unbox === v {
61 | return true
62 | }
63 | return false
64 | }
65 | self.disposables.remove(at: index!)
66 | self.unbox.viewControllers[i] = with.unbox
67 | self.disposables.append(with)
68 | case let .move(at: from, to: to):
69 | self.unbox.viewControllers.swapAt(from, to)
70 | }
71 | })
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/Incremental/Incremental+UIKit/ProgressIndicator.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ProgressIndicator.swift
3 | // Incremental
4 | //
5 | // Created by Chris Eidhof on 29.11.17.
6 | // Copyright © 2017 objc.io. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | public func progressView(progress: I) -> IBox {
12 | let result = UIProgressView(progressViewStyle: .default)
13 | let box = IBox(result)
14 | box.bind(progress, to: \.progress)
15 | return box
16 | }
17 |
--------------------------------------------------------------------------------
/Incremental/Incremental+UIKit/StackView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // StackViews.swift
3 | // Incremental
4 | //
5 | // Created by Chris Eidhof on 27.09.17.
6 | // Copyright © 2017 objc.io. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | extension IBox where V == UIStackView {
12 | public convenience init(arrangedSubviews: [IBox], axis: NSLayoutConstraint.Axis = .vertical) where S: UIView {
13 | let stackView = UIStackView(arrangedSubviews: arrangedSubviews.map { $0.unbox })
14 | stackView.axis = axis
15 | self.init(stackView)
16 | disposables.append(arrangedSubviews)
17 | }
18 |
19 | public convenience init(arrangedSubviews: ArrayWithHistory>, axis: NSLayoutConstraint.Axis = .vertical) where S: UIView {
20 | let stackView = UIStackView(arrangedSubviews: [])
21 | self.init(stackView)
22 | self.bindArrangedSubviews(to: arrangedSubviews)
23 | }
24 |
25 | }
26 |
27 | extension IBox where V: UIStackView {
28 |
29 | func addArrangedSubview(_ i: IBox) {
30 | disposables.append(i)
31 | unbox.addArrangedSubview(i.unbox)
32 | }
33 |
34 | func insertArrangedSubview(_ subview: IBox, at index: Int) {
35 | disposables.append(subview)
36 | unbox.insertArrangedSubview(subview.unbox, at: index)
37 | }
38 |
39 | func removeArrangedSubview(_ subview: V) {
40 | guard let index = disposables.firstIndex(where: { ($0 as? IBox)?.unbox === subview }) else {
41 | assertionFailure("Can't find subview.")
42 | return
43 | }
44 | disposables.remove(at: index)
45 | unbox.removeArrangedSubview(subview)
46 | }
47 |
48 | private func insert(_ v: IBox, at i: Int, duration: TimeInterval) {
49 | v.unbox.isHidden = true
50 | let offset = self.unbox.arrangedSubviews[0..(to value: ArrayWithHistory>, animationDuration duration: TimeInterval = 0.2) {
70 | self.disposables.append(value.observe(current: { initialArrangedSubviews in
71 | assert(self.unbox.arrangedSubviews == [])
72 | for v in initialArrangedSubviews {
73 | self.addArrangedSubview(v)
74 | }
75 | }) {
76 | switch $0 {
77 | case let .insert(v, at: i):
78 | self.insert(v, at: i, duration: duration)
79 | case .remove(at: let i):
80 | self.remove(at: i, duration: duration)
81 | case let .replace(with: element, at: i):
82 | // todo guard if they're the same? or should we replace?
83 | self.insert(element, at: i, duration: duration)
84 | self.remove(at: i+1, duration: duration)
85 | case let .move(at: i, to: j):
86 | let offset = j > i ? -1 : 0
87 | let v = self.unbox.arrangedSubviews.filter { !$0.isHidden }[i]
88 | self.unbox.removeArrangedSubview(v)
89 | self.unbox.insertArrangedSubview(v, at: j + offset)
90 | }
91 | })
92 | }
93 | }
94 |
95 | public func stackView(arrangedSubviews: [IBox], axis: NSLayoutConstraint.Axis = .vertical, spacing: I = I(constant: 10)) -> IBox {
96 | let stackView = IBox(arrangedSubviews: arrangedSubviews)
97 | stackView.unbox.axis = axis
98 | stackView.bind(spacing, to: \.spacing)
99 | return stackView
100 | }
101 |
102 |
103 |
--------------------------------------------------------------------------------
/Incremental/Incremental+UIKit/Switch.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Switch.swift
3 | // Incremental
4 | //
5 | // Created by Chris Eidhof on 02.12.17.
6 | // Copyright © 2017 objc.io. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | public func uiSwitch(value: I, valueChange: @escaping (_ isOn: Bool) -> ()) -> IBox {
12 | let view = UISwitch()
13 | let result = IBox(view)
14 | result.handle(.valueChanged) { [unowned view] in
15 | valueChange(view.isOn)
16 | }
17 | result.bind(value, to: \.isOn)
18 | return result
19 | }
20 |
--------------------------------------------------------------------------------
/Incremental/Incremental+UIKit/TableView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TableViews.swift
3 | // Incremental
4 | //
5 | // Created by Chris Eidhof on 27.09.17.
6 | // Copyright © 2017 objc.io. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | final class TableVC: UITableViewController {
12 | var items: [A] = []
13 | let configure: (UITableViewCell,A) -> ()
14 | let didSelect: ((A) -> ())?
15 | let didDelete: ((A) -> ())?
16 |
17 | init(_ items: [A], didSelect: ((A) -> ())? = nil, didDelete: ((A) -> ())? = nil, configure: @escaping (UITableViewCell,A) -> ()) {
18 | self.items = items
19 | self.configure = configure
20 | self.didSelect = didSelect
21 | self.didDelete = didDelete
22 | super.init(style: .plain)
23 | }
24 |
25 | required init?(coder aDecoder: NSCoder) {
26 | fatalError()
27 | }
28 |
29 | override func viewDidLoad() {
30 | tableView.register(UITableViewCell.self, forCellReuseIdentifier: "Identifier")
31 | }
32 |
33 | override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
34 | didSelect?(items[indexPath.row])
35 | }
36 |
37 | override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
38 | guard editingStyle == .delete else { return }
39 | didDelete?(items[indexPath.row])
40 | }
41 |
42 | override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
43 | return items.count
44 | }
45 |
46 | override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
47 | let cell = tableView.dequeueReusableCell(withIdentifier: "Identifier")!
48 | configure(cell, items[indexPath.row])
49 | return cell
50 | }
51 | }
52 |
53 | extension TableVC where A: Equatable {
54 | func apply(_ change: ArrayChange) {
55 | items.apply(change)
56 | switch change {
57 | case let .insert(_, at: index):
58 | let indexPath = IndexPath(row: index, section: 0)
59 | tableView.insertRows(at: [indexPath], with: .automatic)
60 | case let .remove(at: index):
61 | let indexPath = IndexPath(row: index, section: 0)
62 | tableView.deleteRows(at: [indexPath], with: .automatic)
63 | case let .replace(with: _, at: index):
64 | let indexPath = IndexPath(row: index, section: 0)
65 | tableView.reloadRows(at: [indexPath], with: .automatic)
66 | case let .move(at: i, to: j):
67 | tableView.moveRow(at: IndexPath(row: i, section: 0), to: IndexPath(row: j, section: 0))
68 | }
69 | }
70 | }
71 |
72 | public func tableViewController(items value: ArrayWithHistory, didSelect: ((A) -> ())? = nil, configure: @escaping (UITableViewCell, A) -> ()) -> IBox {
73 | let tableVC = TableVC([], didSelect: didSelect, configure: configure)
74 | let box = IBox(tableVC)
75 | box.disposables.append(value.observe(current: {
76 | tableVC.items = $0
77 | }, handleChange: { change in
78 | tableVC.apply(change)
79 | }))
80 | return box
81 | }
82 |
83 | public func tableViewController(items: I>, didSelect: ((A) -> ())? = nil, didDelete: ((A) -> ())? = nil, configure: @escaping (UITableViewCell, A) -> ()) -> IBox {
84 | let tableVC = TableVC([], didSelect: didSelect, didDelete: didDelete, configure: configure)
85 | let box = IBox(tableVC)
86 | var previousObserver: Any? // this warning is expected, we need to retain the previousObserver
87 | box.disposables.append(items.observe { value in
88 | previousObserver = nil
89 | previousObserver = value.observe(current: {
90 | tableVC.items = $0
91 | tableVC.tableView.reloadData()
92 | }, handleChange: { change in
93 | tableVC.apply(change)
94 | })
95 | })
96 | return box
97 | }
98 |
99 | public func tableViewController(items value: I<[A]>, didSelect: ((A) -> ())? = nil, configure: @escaping (UITableViewCell, A) -> ()) -> IBox {
100 | let tableVC = TableVC([], didSelect: didSelect, configure: configure)
101 | let box = IBox(tableVC)
102 | box.disposables.append(value.observe {
103 | tableVC.items = $0
104 | tableVC.tableView.reloadData()
105 | })
106 | return box
107 | }
108 |
--------------------------------------------------------------------------------
/Incremental/Incremental+UIKit/TextField.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TextField.swift
3 | // Incremental
4 | //
5 | // Created by Chris Eidhof on 27.09.17.
6 | // Copyright © 2017 objc.io. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | public func textField(text: I, backgroundColor: I = I(constant: nil), onChange: @escaping (String?) -> ()) -> IBox {
12 | let textField = UITextField()
13 | let result = IBox(textField)
14 | result.bind(text, to: \.text)
15 | result.bind(backgroundColor, to: \.backgroundColor)
16 |
17 | let ta = TargetAction { onChange(textField.text) }
18 | textField.addTarget(ta, action: #selector(TargetAction.action(_:)), for: .editingChanged)
19 | result.disposables.append(ta)
20 | return result.cast
21 | }
22 |
--------------------------------------------------------------------------------
/Incremental/Incremental+UIKit/ViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ViewController.swift
3 | // Incremental
4 | //
5 | // Created by Chris Eidhof on 27.09.17.
6 | // Copyright © 2017 objc.io. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | public func viewController(rootView: IBox, constraints: [Constraint] = []) -> IBox {
12 | let vc = UIViewController()
13 | let box = IBox(vc)
14 | vc.view.addSubview(rootView.unbox)
15 | vc.view.backgroundColor = .white
16 | box.disposables.append(rootView)
17 | rootView.unbox.translatesAutoresizingMaskIntoConstraints = false
18 |
19 | let evaluatedConstraints = constraints.map { $0(vc.view, rootView.unbox) }
20 | NSLayoutConstraint.activate(evaluatedConstraints.map { $0.unbox })
21 | box.disposables.append(evaluatedConstraints)
22 | return box
23 | }
24 |
25 | extension IBox where V: UIViewController {
26 | public func setRightBarButtonItems(_ value: [IBox]) {
27 | let existing = unbox.navigationItem.rightBarButtonItems ?? []
28 | precondition(existing == [])
29 | for b in value {
30 | disposables.append(b)
31 | }
32 | unbox.navigationItem.rightBarButtonItems = value.map { $0.unbox }
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/Incremental/Incremental.h:
--------------------------------------------------------------------------------
1 | //
2 | // Incremental.h
3 | // Incremental
4 | //
5 | // Created by Chris Eidhof on 13.09.17.
6 | // Copyright © 2017 objc.io. All rights reserved.
7 | //
8 |
9 | #import
10 |
11 | //! Project version number for Incremental.
12 | FOUNDATION_EXPORT double IncrementalVersionNumber;
13 |
14 | //! Project version string for Incremental.
15 | FOUNDATION_EXPORT const unsigned char IncrementalVersionString[];
16 |
17 | // In this header, you should import all the public headers of your framework using statements like #import
18 |
19 |
20 |
--------------------------------------------------------------------------------
/Incremental/Incremental.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | // TODO: expose an Incremental.transaction method which allows multiple writes before processing.
4 | // This class is (by design) not thread-safe
5 | final class Queue {
6 | static let shared = Queue()
7 | var edges: [(Edge, Height)] = []
8 | var processed: [Edge] = []
9 | var fired: [AnyI] = []
10 | var processing: Bool = false
11 |
12 | func enqueue(_ edges: S) where S.Element: Edge {
13 | self.edges.append(contentsOf: edges.map { ($0 as Edge, $0.height) })
14 | self.edges.sort { $0.1 < $1.1 }
15 | }
16 |
17 | func enqueue(_ edges: S) where S.Element == Edge {
18 | self.edges.append(contentsOf: edges.map { ($0, $0.height) })
19 | self.edges.sort { $0.1 < $1.1 }
20 | }
21 |
22 | func fired(_ source: AnyI) {
23 | fired.append(source)
24 | }
25 |
26 | func process() {
27 | guard !processing else { return }
28 | processing = true
29 | while let (edge, _) = edges.popLast() {
30 | guard !processed.contains(where: { $0 === edge }) else {
31 | continue
32 | }
33 | processed.append(edge)
34 | edge.fire()
35 | }
36 |
37 | // cleanup
38 | for i in fired {
39 | i.firedAlready = false
40 | }
41 | fired = []
42 | processed = []
43 | processing = false
44 | }
45 | }
46 |
47 | protocol Node {
48 | var height: Height { get }
49 | }
50 |
51 | protocol Edge: class, Node {
52 | func fire()
53 | }
54 |
55 | final class Observer: Edge {
56 | let observer: () -> ()
57 |
58 | init(_ fire: @escaping () -> ()) {
59 | self.observer = fire
60 | fire()
61 | }
62 | let height = Height.minusOne
63 | func fire() {
64 | observer()
65 | }
66 | }
67 |
68 | protocol Reader: Edge {
69 | var invalidated: Bool { get set }
70 | }
71 |
72 | class AnyReader: Reader {
73 | let read: () -> Node
74 | var height: Height {
75 | return target.height.incremented()
76 | }
77 | var target: Node
78 | var invalidated: Bool = false
79 | init(read: @escaping () -> Node) {
80 | self.read = read
81 | target = read()
82 | }
83 |
84 | func fire() {
85 | if invalidated {
86 | return
87 | }
88 | target = read()
89 | }
90 | }
91 |
92 | // We don't need MapReader and FlatMapReader, but could express everything in terms of AnyReader. Not sure what is better: less concepts, or more (and duplicated) but clearer code.
93 | final class MapReader: Reader {
94 | let read: () -> ()
95 | unowned var target: AnyI
96 | var invalidated: Bool = false
97 |
98 | init(source: I, transform: @escaping (A) -> B, target: I) {
99 | read = { [unowned target] in
100 | target.write(transform(source.value))
101 | }
102 | read()
103 | self.target = target
104 | }
105 | var height: Height {
106 | return target.height.incremented()
107 | }
108 | func fire() {
109 | if invalidated {
110 | return // todo dry
111 | }
112 | read()
113 |
114 | }
115 | }
116 |
117 | final class FlatMapReader: Reader {
118 | var read: (() -> ())!
119 | unowned var target: AnyI
120 | var invalidated: Bool = false
121 | var sourceNode: AnyI!
122 | var token: Register.Token? = nil
123 | var disposable: Any?
124 |
125 | init(source: I, transform: @escaping (A) -> I, target: I) {
126 | self.target = target
127 | read = { [unowned target] in
128 | self.disposable = nil
129 | let newSourceNode = transform(source.value)
130 | self.disposable = newSourceNode.addReader(MapReader(source: newSourceNode, transform: { $0 }, target: target))
131 | target.write(newSourceNode.value)
132 | self.sourceNode = newSourceNode // todo should this be a strong reference?
133 | }
134 | read()
135 | }
136 | var height: Height {
137 | return sourceNode.height.incremented()
138 | }
139 | func fire() {
140 | if invalidated {
141 | return // todo dry
142 | }
143 | read()
144 |
145 | }
146 | }
147 |
148 |
149 | public final class Input {
150 | public let i: I
151 |
152 | public init(eq: @escaping (A,A) -> Bool, _ value: A) {
153 | i = I(eq: eq, value: value)
154 | }
155 |
156 | public init(alwaysPropagate value: A) {
157 | i = I(eq: { _, _ in false }, value: value)
158 | }
159 |
160 | public func write(_ newValue: A) {
161 | i.write(newValue)
162 | }
163 |
164 | public func change(_ by: (inout A) -> B) -> B {
165 | var copy = i.value!
166 | let result = by(©)
167 | i.write(copy)
168 | return result
169 | }
170 |
171 | public subscript(keyPath: KeyPath) -> I {
172 | return i.map { $0[keyPath: keyPath] }
173 | }
174 | }
175 |
176 | public extension Input where A: Equatable {
177 | convenience init(_ value: A) {
178 | self.init(eq: ==, value)
179 | }
180 | }
181 |
182 |
183 | protocol AnyI: class, Node {
184 | var firedAlready: Bool { get set }
185 | var strongReferences: Register { get set }
186 | var height: Height { get }
187 | }
188 |
189 | public final class I: AnyI, Node {
190 | internal(set) public var value: A! // todo this will not be public!
191 | var observers = Register()
192 | var readers: Register = Register()
193 | var height: Height {
194 | return readers.values.map { $0.height }.leastUpperBound.incremented()
195 | }
196 | var firedAlready: Bool = false
197 | var strongReferences: Register = Register()
198 | var eq: (A,A) -> Bool
199 | private var constant: Bool
200 |
201 | init(eq: @escaping (A, A) -> Bool, value: A) {
202 | self.value = value
203 | self.eq = eq
204 | self.constant = false
205 | }
206 |
207 | init(eq: @escaping (A,A) -> Bool) {
208 | self.eq = eq
209 | self.constant = false
210 | }
211 |
212 | public init(constant: A) {
213 | self.value = constant
214 | self.eq = { _, _ in true }
215 | self.constant = true
216 | }
217 |
218 | public func observe(_ observer: @escaping (A) -> ()) -> Disposable {
219 | let token = observers.add(Observer {
220 | observer(self.value)
221 | })
222 | return Disposable { /* should this be weak/unowned? */
223 | self.observers.remove(token)
224 | }
225 | }
226 |
227 | func _writeHelper(_ value: A) -> I {
228 | if let existing = self.value, eq(existing, value) { return self }
229 | self.value = value
230 |
231 | guard !firedAlready else { return self }
232 | firedAlready = true
233 | let r: [Edge] = Array(readers.values)
234 | Queue.shared.enqueue(r)
235 | Queue.shared.enqueue(observers.values)
236 | Queue.shared.fired(self)
237 | Queue.shared.process()
238 | return self
239 | }
240 | /// Returns `self`
241 | @discardableResult
242 | func write(_ value: A, file: StaticString = #file, line: UInt = #line) -> I {
243 | precondition(!constant, file: file, line: line)
244 | return _writeHelper(value)
245 | }
246 |
247 | @discardableResult
248 | func write(constant value: A, file: StaticString = #file, line: UInt = #line) -> I {
249 | assert(!constant, file: file, line: line)
250 | self.constant = true // this node will never fire again
251 | return _writeHelper(value)
252 | }
253 |
254 | func addReader(_ reader: Reader) -> Disposable {
255 | let token = readers.add(reader)
256 | return Disposable {
257 | self.readers[token]?.invalidated = true
258 | self.readers.remove(token)
259 | }
260 | }
261 |
262 | /// The `target` strongly references the reader. If the target goes away, the reader will be removed as well.
263 | /// The `read` needs to return a `Node`: this is the direct dependency of the read function (used to ultimately compute the topological order).
264 | @discardableResult
265 | func read(target: AnyI, _ read: @escaping (A) -> Node) -> AnyReader {
266 | let reader = AnyReader { read(self.value) }
267 | guard !constant else {
268 | return reader
269 | }
270 | let disposable = addReader(reader)
271 | target.strongReferences.add(disposable)
272 | return reader
273 | }
274 |
275 | @discardableResult
276 | func read(_ read: @escaping (A) -> Node) -> (AnyReader, Disposable?) {
277 | let reader = AnyReader { read(self.value) }
278 | guard !constant else {
279 | return (reader, nil)
280 | }
281 | let disposable = addReader(reader)
282 | return (reader, disposable)
283 | }
284 |
285 | public func map(eq: @escaping (B,B) -> Bool, _ transform: @escaping (A) -> B) -> I {
286 | guard !constant else {
287 | return I(constant: transform(self.value))
288 | }
289 |
290 | let result = I(eq: eq)
291 | let reader = MapReader(source: self, transform: transform, target: result)
292 | result.strongReferences.add(addReader(reader))
293 | return result
294 | }
295 |
296 | public func flatMap(eq: @escaping (B,B) -> Bool, _ transform: @escaping (A) -> I) -> I {
297 | guard !constant else {
298 | return transform(self.value)
299 | }
300 | let result = I(eq: eq) // todo: could we somehow pull eq out of the transform's result?
301 | let reader = FlatMapReader(source: self, transform: transform, target: result)
302 | result.strongReferences.add(addReader(reader))
303 | return result
304 | }
305 |
306 | func mutate(_ transform: (inout A) -> ()) {
307 | var newValue = value!
308 | transform(&newValue)
309 | write(newValue)
310 | }
311 | }
312 |
313 | extension I {
314 | public func flatMap(_ transform: @escaping (A) -> I) -> I {
315 | return flatMap(eq: ==, transform)
316 | }
317 |
318 | public func zip2(_ other: I, _ with: @escaping (A,B) -> C) -> I {
319 | return flatMap { value in other.map { with(value, $0) } }
320 | }
321 |
322 | public func zip3(_ x: I, _ y: I, _ with: @escaping (A,B,C) -> D) -> I {
323 | return flatMap { value1 in
324 | x.flatMap { value2 in
325 | y.map { with(value1, value2, $0) }
326 | }
327 | }
328 | }
329 |
330 | public subscript(keyPath: KeyPath) -> I {
331 | return map { $0[keyPath: keyPath] }
332 | }
333 |
334 | // All of the below goes away with conditional conformance
335 |
336 | // convenience for optionals
337 | public subscript(keyPath: KeyPath) -> I {
338 | return map(eq: ==, { $0[keyPath: keyPath] })
339 | }
340 |
341 | // convenience for equatable
342 | public func map(_ transform: @escaping (A) -> B) -> I {
343 | return map(eq: ==, transform)
344 | }
345 |
346 | // convenience for optionals
347 | public func map(_ transform: @escaping (A) -> B?) -> I {
348 | return map(eq: ==, transform)
349 | }
350 |
351 | // convenience for arrays
352 | public func map(_ transform: @escaping (A) -> [B]) -> I<[B]> {
353 | return map(eq: ==, transform)
354 | }
355 |
356 | // convenience for tuples
357 | public func map(_ transform: @escaping (A) -> (B, C)) -> I<(B,C)> {
358 | return map(eq: ==, transform)
359 | }
360 |
361 | public func map(_ transform: @escaping (A) -> [(B, C)]) -> I<[(B,C)]> {
362 | return map(eq: lift(==), transform)
363 | }
364 |
365 | // convenience for optional tuples
366 | public func map(_ transform: @escaping (A) -> (B, C)?) -> I<(B,C)?> {
367 | return map(eq: lift(==), transform)
368 | }
369 |
370 | }
371 |
372 | public func lift(_ f: @escaping (A,A) -> Bool) -> (A?,A?) -> Bool {
373 | return { l, r in
374 | switch (l,r) {
375 | case (nil,nil): return true
376 | case let (x?, y?): return f(x,y)
377 | default: return false
378 | }
379 | }
380 | }
381 |
382 | public func lift(_ f: @escaping (A,A) -> Bool) -> ([A],[A]) -> Bool {
383 | return { l, r in
384 | l.count == r.count && !zip(l,r).lazy.map(f).contains(false)
385 | }
386 | }
387 |
388 |
389 |
390 | extension I where A: Equatable {
391 | convenience init() {
392 | self.init(eq: ==)
393 | }
394 | convenience init(value: A) {
395 | self.init(eq: ==, value: value)
396 | }
397 | }
398 |
399 | extension I: Equatable {
400 | public static func ==(lhs: I, rhs: I) -> Bool {
401 | return lhs === rhs
402 | }
403 | }
404 |
405 |
--------------------------------------------------------------------------------
/Incremental/IncrementalStdLib.swift:
--------------------------------------------------------------------------------
1 | //
2 | // IncrementalStdLib.swift
3 | // Incremental
4 | //
5 | // Created by Chris Eidhof on 19.09.17.
6 | // Copyright © 2017 objc.io. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | // idea make certain properties configurable from "the outside". We could make an "ExternalConfig" struct which reads from JSON (and is settable by the server).
12 |
13 | public func if_(_ condition: I, then l: I, else r: I) -> I {
14 | return condition.flatMap { $0 ? l : r }
15 | }
16 |
17 | public func if_(_ condition: I, then l: A, else r: A) -> I {
18 | return condition.map { $0 ? l : r }
19 | }
20 |
21 | public func if_(_ condition: I, then l: A?) -> I {
22 | return condition.map { $0 ? l : nil }
23 | }
24 |
25 | public func &&(l: I, r: I) -> I {
26 | return l.zip2(r, { $0 && $1 })
27 | }
28 |
29 | public func ||(l: I, r: I) -> I {
30 | return l.zip2(r, { $0 || $1 })
31 | }
32 |
33 | public func +(l: I, r: I) -> I {
34 | return l.zip2(r, +)
35 | }
36 |
37 | public func ??(l: I, r: A) -> I {
38 | return l.map { $0 ?? r }
39 | }
40 |
41 | public func ??(l: I, r: I) -> I {
42 | return l.zip2(r, { $0 ?? $1 })
43 | }
44 |
45 | public prefix func !(l: I) -> I {
46 | return l.map { !$0 }
47 | }
48 |
49 | public prefix func -(l: I) -> I {
50 | return l.map { -$0 }
51 | }
52 |
53 | public func ==(l: I, r: I) -> I where A: Equatable {
54 | return l.zip2(r, ==)
55 | }
56 |
57 | public func ==(l: I, r: A) -> I where A: Equatable {
58 | return l.map { $0 == r }
59 | }
60 |
61 | public func ==(l: I, r: A?) -> I where A: Equatable {
62 | return l.map { $0 == r }
63 | }
64 |
--------------------------------------------------------------------------------
/Incremental/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | FMWK
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleVersion
20 | $(CURRENT_PROJECT_VERSION)
21 | NSPrincipalClass
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/Incremental_Mac/Incremental_Mac.h:
--------------------------------------------------------------------------------
1 | //
2 | // Incremental_Mac.h
3 | // Incremental_Mac
4 | //
5 | // Created by Chris Eidhof on 22.09.17.
6 | // Copyright © 2017 objc.io. All rights reserved.
7 | //
8 |
9 | #import
10 |
11 | //! Project version number for Incremental_Mac.
12 | FOUNDATION_EXPORT double Incremental_MacVersionNumber;
13 |
14 | //! Project version string for Incremental_Mac.
15 | FOUNDATION_EXPORT const unsigned char Incremental_MacVersionString[];
16 |
17 | // In this header, you should import all the public headers of your framework using statements like #import
18 |
--------------------------------------------------------------------------------
/Incremental_Mac/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | FMWK
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleVersion
20 | $(CURRENT_PROJECT_VERSION)
21 | NSHumanReadableCopyright
22 | Copyright © 2017 objc.io. All rights reserved.
23 | NSPrincipalClass
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/Laufpark/AppDelegate.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 |
3 | @UIApplicationMain
4 | class AppDelegate: UIResponder, UIApplicationDelegate {
5 |
6 | var window: UIWindow?
7 | var mapViewController: ViewController?
8 |
9 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
10 | window = UIWindow(frame: UIScreen.main.bounds)
11 | let insets: UIEdgeInsets
12 | if #available(iOS 11.0, *) {
13 | insets = window!.safeAreaInsets
14 | } else {
15 | insets = .zero
16 | }
17 | mapViewController = ViewController(safeAreaInsets: insets)
18 | window?.rootViewController = mapViewController
19 | window?.makeKeyAndVisible()
20 | DispatchQueue(label: "Track Loading").async {
21 | let tracks = Track.load()
22 | let simplifiedTracks = tracks.map { (track: Track) -> Track in
23 | var copy = track
24 | copy.coordinates = track.coordinates.douglasPeucker(coordinate: { $0.coordinate.clLocationCoordinate }, squaredEpsilonInMeters: epsilon*epsilon)
25 | return copy
26 | }
27 | DispatchQueue.main.async {
28 | self.mapViewController?.setTracks(simplifiedTracks)
29 | }
30 | }
31 | return true
32 | }
33 |
34 |
35 | }
36 |
37 |
--------------------------------------------------------------------------------
/Laufpark/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "iphone",
5 | "size" : "20x20",
6 | "scale" : "2x"
7 | },
8 | {
9 | "idiom" : "iphone",
10 | "size" : "20x20",
11 | "scale" : "3x"
12 | },
13 | {
14 | "idiom" : "iphone",
15 | "size" : "29x29",
16 | "scale" : "2x"
17 | },
18 | {
19 | "idiom" : "iphone",
20 | "size" : "29x29",
21 | "scale" : "3x"
22 | },
23 | {
24 | "idiom" : "iphone",
25 | "size" : "40x40",
26 | "scale" : "2x"
27 | },
28 | {
29 | "idiom" : "iphone",
30 | "size" : "40x40",
31 | "scale" : "3x"
32 | },
33 | {
34 | "size" : "60x60",
35 | "idiom" : "iphone",
36 | "filename" : "Logo_60@2x.png",
37 | "scale" : "2x"
38 | },
39 | {
40 | "size" : "60x60",
41 | "idiom" : "iphone",
42 | "filename" : "Logo_60@3x.png",
43 | "scale" : "3x"
44 | },
45 | {
46 | "idiom" : "ipad",
47 | "size" : "20x20",
48 | "scale" : "1x"
49 | },
50 | {
51 | "idiom" : "ipad",
52 | "size" : "20x20",
53 | "scale" : "2x"
54 | },
55 | {
56 | "idiom" : "ipad",
57 | "size" : "29x29",
58 | "scale" : "1x"
59 | },
60 | {
61 | "idiom" : "ipad",
62 | "size" : "29x29",
63 | "scale" : "2x"
64 | },
65 | {
66 | "idiom" : "ipad",
67 | "size" : "40x40",
68 | "scale" : "1x"
69 | },
70 | {
71 | "idiom" : "ipad",
72 | "size" : "40x40",
73 | "scale" : "2x"
74 | },
75 | {
76 | "idiom" : "ipad",
77 | "size" : "76x76",
78 | "scale" : "1x"
79 | },
80 | {
81 | "size" : "76x76",
82 | "idiom" : "ipad",
83 | "filename" : "Logo76@2x.png",
84 | "scale" : "2x"
85 | },
86 | {
87 | "size" : "83.5x83.5",
88 | "idiom" : "ipad",
89 | "filename" : "Logo_60@3x-1.png",
90 | "scale" : "2x"
91 | },
92 | {
93 | "size" : "1024x1024",
94 | "idiom" : "ios-marketing",
95 | "filename" : "Logo_Laufpark_Stechlin.png",
96 | "scale" : "1x"
97 | }
98 | ],
99 | "info" : {
100 | "version" : 1,
101 | "author" : "xcode"
102 | }
103 | }
--------------------------------------------------------------------------------
/Laufpark/Assets.xcassets/AppIcon.appiconset/Logo76@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chriseidhof/laufpark-stechlin/807aec8af39d13f34813f9ef795d77f76f017e71/Laufpark/Assets.xcassets/AppIcon.appiconset/Logo76@2x.png
--------------------------------------------------------------------------------
/Laufpark/Assets.xcassets/AppIcon.appiconset/Logo_60@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chriseidhof/laufpark-stechlin/807aec8af39d13f34813f9ef795d77f76f017e71/Laufpark/Assets.xcassets/AppIcon.appiconset/Logo_60@2x.png
--------------------------------------------------------------------------------
/Laufpark/Assets.xcassets/AppIcon.appiconset/Logo_60@3x-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chriseidhof/laufpark-stechlin/807aec8af39d13f34813f9ef795d77f76f017e71/Laufpark/Assets.xcassets/AppIcon.appiconset/Logo_60@3x-1.png
--------------------------------------------------------------------------------
/Laufpark/Assets.xcassets/AppIcon.appiconset/Logo_60@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chriseidhof/laufpark-stechlin/807aec8af39d13f34813f9ef795d77f76f017e71/Laufpark/Assets.xcassets/AppIcon.appiconset/Logo_60@3x.png
--------------------------------------------------------------------------------
/Laufpark/Assets.xcassets/AppIcon.appiconset/Logo_Laufpark_Stechlin.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chriseidhof/laufpark-stechlin/807aec8af39d13f34813f9ef795d77f76f017e71/Laufpark/Assets.xcassets/AppIcon.appiconset/Logo_Laufpark_Stechlin.png
--------------------------------------------------------------------------------
/Laufpark/Assets.xcassets/Color.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "version" : 1,
4 | "author" : "xcode"
5 | },
6 | "colors" : [
7 | {
8 | "idiom" : "universal",
9 | "color" : {
10 | "color-space" : "srgb",
11 | "components" : {
12 | "red" : "1.000",
13 | "alpha" : "1.000",
14 | "blue" : "1.000",
15 | "green" : "1.000"
16 | }
17 | }
18 | }
19 | ]
20 | }
--------------------------------------------------------------------------------
/Laufpark/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "version" : 1,
4 | "author" : "xcode"
5 | }
6 | }
--------------------------------------------------------------------------------
/Laufpark/Attribution_en.rtf:
--------------------------------------------------------------------------------
1 | {\rtf1\ansi\ansicpg1252\cocoartf1561\cocoasubrtf200
2 | {\fonttbl\f0\fswiss\fcharset0 Helvetica;}
3 | {\colortbl;\red255\green255\blue255;}
4 | {\*\expandedcolortbl;;}
5 | \paperw11900\paperh16840\margl1440\margr1440\vieww10800\viewh8400\viewkind0
6 | \pard\tx566\tx1133\tx1700\tx2267\tx2834\tx3401\tx3968\tx4535\tx5102\tx5669\tx6236\tx6803\pardirnatural\partightenfactor0
7 |
8 | \f0\fs24 \cf0 All content and data is made available by the {\field{\*\fldinst{HYPERLINK "http://www.laufpark-stechlin.de/"}}{\fldrslt Laufpark Stechlin}}.\
9 | \pard\tx566\tx1133\tx1700\tx2267\tx2834\tx3401\tx3968\tx4535\tx5102\tx5669\tx6236\tx6803\pardirnatural\partightenfactor0
10 | \cf0 \
11 | The source code of this app is {\field{\*\fldinst{HYPERLINK "https://github.com/chriseidhof/laufpark-stechlin"}}{\fldrslt available on GitHub}}.\
12 | \
13 | {\field{\*\fldinst{HYPERLINK "https://thenounproject.com/search/?q=close&i=1217859#"}}{\fldrslt Close}} by Icon Depot from the Noun Project\
14 | {\field{\*\fldinst{HYPERLINK "https://thenounproject.com/search/?q=route&i=939673"}}{\fldrslt Route}} by Numero Uno from the Noun Project\
15 | }
--------------------------------------------------------------------------------
/Laufpark/Base.lproj/LaunchScreen.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |