├── .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 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /Laufpark/Base.lproj/Localizable.strings: -------------------------------------------------------------------------------- 1 | /* 2 | Localized.strings 3 | Laufpark 4 | 5 | Created by Chris Eidhof on 12.12.17. 6 | Copyright © 2017 objc.io. All rights reserved. 7 | */ 8 | 9 | tapAnyWhereToStart = "Tippe auf die Karte um einen Startpunkt festzulegen"; 10 | loadingGraph = "Laden..."; 11 | karte = "Karte"; 12 | satellite = "Satellit"; 13 | route = "Planer"; 14 | close = "Schließen"; 15 | undo = "Rückgangig machen"; 16 | -------------------------------------------------------------------------------- /Laufpark/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /Laufpark/Helpers.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Helpers.swift 3 | // Laufpark 4 | // 5 | // Created by Chris Eidhof on 18.09.17. 6 | // Copyright © 2017 objc.io. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Incremental 11 | 12 | extension Bool { 13 | mutating func toggle() { 14 | self = !self 15 | } 16 | } 17 | 18 | extension Comparable { 19 | func clamped(to: ClosedRange) -> Self { 20 | if self < to.lowerBound { return to.lowerBound } 21 | if self > to.upperBound { return to.upperBound } 22 | return self 23 | } 24 | } 25 | 26 | func time(name: StaticString = #function, line: Int = #line, _ f: () -> Result) -> Result { 27 | let startTime = DispatchTime.now() 28 | let result = f() 29 | let endTime = DispatchTime.now() 30 | let diff = Double(endTime.uptimeNanoseconds - startTime.uptimeNanoseconds) / 1_000_000_000 as Double 31 | print("\(name) (line \(line)): \(diff) sec") 32 | return result 33 | } 34 | 35 | var globalPersistentValues: [String:Any] = [:] 36 | 37 | // Stores the state S in userDefaults under the provided key 38 | func persistent(key: String, initial start: S) -> Input { 39 | let defaults = UserDefaults.standard 40 | let initial = defaults.data(forKey: key).flatMap { 41 | let decoder = JSONDecoder() 42 | let result = try? decoder.decode(S.self, from: $0) 43 | return result 44 | } ?? start 45 | 46 | let input = Input(initial) 47 | let encoder = JSONEncoder() 48 | let disposable = input.i.observe { value in 49 | let data = try! encoder.encode(value) 50 | defaults.set(data, forKey: key) 51 | defaults.synchronize() 52 | } 53 | globalPersistentValues[key] = disposable 54 | return input 55 | } 56 | -------------------------------------------------------------------------------- /Laufpark/Icon@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chriseidhof/laufpark-stechlin/807aec8af39d13f34813f9ef795d77f76f017e71/Laufpark/Icon@3x.png -------------------------------------------------------------------------------- /Laufpark/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleDisplayName 8 | Laufpark 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 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 | 2 23 | LSRequiresIPhoneOS 24 | 25 | NSLocationWhenInUseUsageDescription 26 | Show your location on the map 27 | UILaunchStoryboardName 28 | LaunchScreen 29 | UIRequiredDeviceCapabilities 30 | 31 | armv7 32 | arm64 33 | 34 | UISupportedInterfaceOrientations 35 | 36 | UIInterfaceOrientationPortrait 37 | UIInterfaceOrientationLandscapeLeft 38 | UIInterfaceOrientationLandscapeRight 39 | 40 | UISupportedInterfaceOrientations~ipad 41 | 42 | UIInterfaceOrientationPortrait 43 | UIInterfaceOrientationPortraitUpsideDown 44 | UIInterfaceOrientationLandscapeLeft 45 | UIInterfaceOrientationLandscapeRight 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /Laufpark/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /Laufpark/LineView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LineView.swift 3 | // Laufpark 4 | // 5 | // Created by Chris Eidhof on 08.09.17. 6 | // Copyright © 2017 objc.io. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import MapKit 11 | 12 | extension CGContext { 13 | func drawLine(from start: CGPoint, to end: CGPoint, color: UIColor) { 14 | color.setStroke() 15 | move(to: start) 16 | addLine(to: end) 17 | strokePath() 18 | } 19 | } 20 | 21 | final class LineView: UIView { 22 | struct Point: Equatable { 23 | var x: Double 24 | var y: Double 25 | } 26 | 27 | var strokeWidth: CGFloat = 1 { didSet { setNeedsDisplay() }} 28 | var strokeColor: UIColor = .black { didSet { setNeedsDisplay() }} 29 | var position: CGFloat? = nil { didSet { setNeedsDisplay() }} 30 | var positionColor: UIColor = .red { didSet { setNeedsDisplay() }} 31 | let distanceFormatter: MKDistanceFormatter = { 32 | let result = MKDistanceFormatter() 33 | result.unitStyle = .abbreviated 34 | return result 35 | }() 36 | 37 | private var _pointsRect: CGRect = .zero 38 | 39 | var points: [Point] = [] { 40 | didSet { 41 | recomputePointsRect() 42 | setNeedsDisplay() 43 | } 44 | } 45 | 46 | var horizontalTick: CGFloat { 47 | // distanceFormatter.units doesn't return the right value... 48 | return distanceFormatter.locale.usesMetricSystem ? 5000 : 4828.03 49 | } 50 | var tickColor: UIColor = UIColor.gray.withAlphaComponent(0.3) { didSet { setNeedsDisplay() } } 51 | 52 | override func layoutSubviews() { 53 | super.layoutSubviews() 54 | setNeedsDisplay() 55 | } 56 | 57 | func recomputePointsRect() { 58 | var (minY, maxY, maxX): (CGFloat, CGFloat, CGFloat) = (.greatestFiniteMagnitude, 0, 0) 59 | for p in points { 60 | minY = min(minY, CGFloat(p.y)) 61 | maxY = max(maxY, CGFloat(p.y)) 62 | maxX = max(maxX, CGFloat(p.x)) 63 | } 64 | 65 | _pointsRect = CGRect(x: 0, y: minY, width: maxX.rounded(.up), height: maxY-minY) 66 | } 67 | 68 | override func draw(_ rect: CGRect) { 69 | guard !self.points.isEmpty else { return } 70 | guard let context = UIGraphicsGetCurrentContext() else { return } 71 | 72 | 73 | let labelPadding: CGFloat = 20 74 | 75 | let scaleX = bounds.size.width/_pointsRect.size.width 76 | let scaleY = (bounds.size.height-labelPadding)/_pointsRect.size.height 77 | 78 | 79 | 80 | // drawing ticks 81 | let cgTickWidth = horizontalTick * scaleX 82 | let ticks: [CGFloat] = Array(sequence(state: cgTickWidth, next: { (currentTick: inout CGFloat) in 83 | defer { currentTick += cgTickWidth } 84 | guard currentTick < self.bounds.size.width else { return nil } 85 | return currentTick 86 | })) 87 | 88 | let attributes: [NSAttributedString.Key : Any] = [ 89 | .foregroundColor: strokeColor, 90 | .font: Stylesheet.smallFont 91 | ] 92 | 93 | for tick in ticks { 94 | let start = CGPoint(x: tick, y: 0) 95 | let end = CGPoint(x: tick, y: bounds.size.height-labelPadding) 96 | context.setLineWidth(1) 97 | context.drawLine(from: start, to: end, color: tickColor) 98 | 99 | let text = distanceFormatter.string(fromDistance: CLLocationDistance((tick/scaleX))) as NSString 100 | let width = text.size(withAttributes: attributes).width 101 | guard (tick + width/2) < bounds.width else { continue } 102 | (text as NSString).draw(at: CGPoint(x: tick - (width/2), y: bounds.size.height-labelPadding + 5), withAttributes: attributes) 103 | } 104 | 105 | // drawing the "cursor" 106 | // todo this should be a separate uiview so that we don't need to redraw all the time 107 | if let position = position { 108 | let start = CGPoint(x: position*scaleX, y: 0) 109 | let end = CGPoint(x: position*scaleX, y: bounds.size.height-labelPadding) 110 | context.move(to: start) 111 | context.addLine(to: end) 112 | positionColor.setStroke() 113 | context.strokePath() 114 | } 115 | 116 | context.saveGState() 117 | context.translateBy(x: 0, y: bounds.size.height-labelPadding) 118 | context.scaleBy(x: 1, y: -1) 119 | context.setLineWidth(strokeWidth) 120 | strokeColor.setStroke() 121 | let points = self.points.map { 122 | CGPoint(x: (CGFloat($0.x)-_pointsRect.origin.x) * scaleX, y: (CGFloat($0.y)-_pointsRect.origin.y) * scaleY) 123 | } 124 | guard let start = points.first else { return } 125 | context.move(to: start) 126 | for p in points.dropFirst() { 127 | context.addLine(to: p) 128 | } 129 | context.strokePath() 130 | context.restoreGState() 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /Laufpark/ParallelArray.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ParallelArray.swift 3 | // Laufpark 4 | // 5 | // Created by Chris Eidhof on 22.10.17. 6 | // Copyright © 2017 objc.io. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | -------------------------------------------------------------------------------- /Laufpark/SegmentedControl.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SegmentedControl.swift 3 | // Laufpark 4 | // 5 | // Created by Chris Eidhof on 06.12.17. 6 | // Copyright © 2017 objc.io. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import Incremental 11 | 12 | enum HorizontalAlignment { 13 | case left 14 | case center 15 | case right 16 | } 17 | 18 | enum VerticalAlignment { 19 | case top 20 | case middle 21 | case bottom 22 | } 23 | 24 | extension CGSize { 25 | func align(horizontal: HorizontalAlignment = .left, vertical: VerticalAlignment = .top, in frame: CGRect) -> CGRect { 26 | var result = CGRect(origin: .zero, size: self) 27 | switch horizontal { 28 | case .left: result.origin.x = 0 29 | case .center: result.origin.x = (frame.size.width - width)/2 30 | case .right: result.origin.x = frame.size.width - width 31 | } 32 | switch vertical { 33 | case .top: result.origin.y = 0 34 | case .middle: result.origin.y = (frame.size.height - height)/2 35 | case .bottom: result.origin.y = frame.size.height - height 36 | } 37 | 38 | return result.integral 39 | } 40 | } 41 | 42 | extension CGRect { 43 | mutating func alignTo(horizontal: HorizontalAlignment, vertical: VerticalAlignment, of rect: CGRect) { 44 | self = size.align(horizontal: horizontal, vertical: vertical, in: rect) 45 | } 46 | } 47 | 48 | 49 | func segmentedControl(segments: I<[SegmentedControl.Segment]>, value: I, textColor: I, selectedTextColor: I, onChange: @escaping (Int) -> ()) -> IBox { 50 | let c = IBox(SegmentedControl()) 51 | c.bind(segments, to: \SegmentedControl.segments) 52 | c.bind(textColor, to: \.textColor) 53 | c.bind(selectedTextColor, to: \.selectedTextColor) 54 | c.observe(value: value) { (c, v) in 55 | c.selectedSegmentIndex = v 56 | } 57 | let ta = TargetAction { [unowned c] in 58 | onChange(c.unbox.selectedSegmentIndex!) 59 | } 60 | c.unbox.addTarget(ta, action: #selector(TargetAction.action(_:)), for: .valueChanged) 61 | c.disposables.append(ta) 62 | return c 63 | } 64 | 65 | final class SegmentView: UIView { 66 | var label: UILabel = UILabel() 67 | var imageView = UIImageView() 68 | var textColor: UIColor { 69 | get { return label.textColor } 70 | set { 71 | label.textColor = newValue 72 | imageView.tintColor = newValue 73 | } 74 | } 75 | var size: CGSize = .zero 76 | 77 | override var intrinsicContentSize: CGSize { 78 | return size 79 | } 80 | } 81 | 82 | func segment(_ image: UIImage, title: String, textColor: UIColor, size: CGSize) -> SegmentView { 83 | let view = SegmentView(frame: CGRect(x: 0, y: 0, width: size.width, height: size.height)) 84 | view.size = size 85 | let imageView = view.imageView 86 | imageView.image = image 87 | let label = view.label 88 | label.text = title.uppercased() 89 | label.font = .preferredFont(forTextStyle: .caption1) 90 | label.textColor = textColor 91 | view.addSubview(imageView) 92 | view.addSubview(label) 93 | view.label = label 94 | 95 | imageView.frame = image.size.align(horizontal: .center, vertical: .top, in: view.frame) 96 | label.frame = label.intrinsicContentSize.align(horizontal: .center, vertical: .bottom, in: view.frame) 97 | view.accessibilityLabel = title 98 | view.accessibilityTraits = UIAccessibilityTraits.button 99 | view.isAccessibilityElement = true 100 | return view 101 | } 102 | 103 | let segmentSize = CGSize(width: 46, height: 55) 104 | 105 | public final class SegmentedControl: UIControl { 106 | public struct Segment: Equatable { 107 | let image: UIImage 108 | let title: String 109 | } 110 | 111 | public var selectedSegmentIndex: Int? = nil { 112 | didSet { 113 | indicator.isHidden = selectedSegmentIndex == nil 114 | sendActions(for: .valueChanged) 115 | } 116 | } 117 | let indicatorSpacing: CGFloat = 3 118 | let indicatorHeight: CGFloat = 2 119 | let spacing: CGFloat = 20 120 | let animationDuration: TimeInterval = 0.2 121 | 122 | public var textColor: UIColor = .black { 123 | didSet { 124 | UIView.animate(withDuration: animationDuration) { 125 | for (index, label) in self.segmentLabels.enumerated() { 126 | if index == self.selectedSegmentIndex { continue } 127 | label.textColor = self.textColor 128 | } 129 | } 130 | } 131 | } 132 | 133 | var segmentLabels: [UILabel] { 134 | return subviews.dropLast().map { $0.subviews.compactMap { $0 as? UILabel }.first! } 135 | } 136 | 137 | public var selectedTextColor: UIColor = .white { 138 | didSet { 139 | UIView.animate(withDuration: animationDuration) { 140 | if let i = self.selectedSegmentIndex { 141 | self.segmentLabels[i].textColor = self.selectedTextColor 142 | } 143 | self.indicator.backgroundColor = self.selectedTextColor 144 | } 145 | } 146 | } 147 | 148 | public var indicatorColor: UIColor { 149 | set { 150 | indicator.backgroundColor = newValue 151 | } 152 | get { 153 | return indicator.backgroundColor! 154 | } 155 | } 156 | private lazy var indicator: UIView = { 157 | let result = UIView() 158 | result.frame.size.height = indicatorHeight 159 | result.frame.size.width = segmentSize.width 160 | result.backgroundColor = selectedTextColor 161 | return result 162 | }() 163 | public var segments: [Segment] = [] { 164 | didSet { 165 | subviews.forEach { $0.removeFromSuperview() } 166 | for (i, s) in segments.enumerated() { 167 | let color = i == selectedSegmentIndex ? selectedTextColor : textColor 168 | addSubview(segment(s.image, title: s.title, textColor: color, size: segmentSize)) 169 | } 170 | addSubview(indicator) 171 | 172 | invalidateIntrinsicContentSize() 173 | } 174 | } 175 | 176 | public override init(frame: CGRect) { 177 | super.init(frame: frame) 178 | 179 | addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(tapped(_:)))) 180 | } 181 | 182 | @objc func tapped(_ sender: UITapGestureRecognizer) { 183 | let location = sender.location(in: self) 184 | guard let index = subviews.firstIndex(where: { $0.frame.contains(location) }) else { return } // todo should we compute this with math? 185 | selectedSegmentIndex = index 186 | let newX = leftForSegment(i: index) 187 | UIView.animate(withDuration: animationDuration) { [weak self] in 188 | self?.indicator.frame.origin.x = newX 189 | } 190 | } 191 | 192 | public required init?(coder aDecoder: NSCoder) { 193 | fatalError() 194 | } 195 | 196 | public override var intrinsicContentSize: CGSize { 197 | let count = CGFloat(segments.count) 198 | return CGSize(width: count * segmentSize.width + (count-1) * spacing, 199 | height: segmentSize.height + indicatorSpacing + indicatorHeight) 200 | } 201 | 202 | func leftForSegment(i: Int) -> CGFloat { 203 | return CGFloat(i) * (spacing + segmentSize.width) 204 | } 205 | 206 | public override func layoutSubviews() { 207 | super.layoutSubviews() 208 | 209 | 210 | for i in 0.. 1 { 33 | route!.removeLastWaypoint() 34 | } else { 35 | route = nil 36 | } 37 | } 38 | 39 | mutating func addWayPoint(track: Track, coordinate c2d: CLLocationCoordinate2D, segment: Segment) { 40 | guard graph != nil else { return } 41 | 42 | let coordinate = Coordinate(c2d) 43 | assert(c2d.squaredDistance(to: segment).squareRoot() < 0.1) 44 | 45 | let segment0 = Coordinate(segment.0) 46 | let segment1 = Coordinate(segment.1) 47 | 48 | func add(from: Coordinate, _ entry: Graph.Entry) { 49 | graph!.add(from: from, entry) 50 | } 51 | add(from: coordinate, Graph.Entry(destination: segment0, distance: segment.0.squaredDistanceApproximation(to: c2d).squareRoot(), trackName: track.name)) 52 | add(from: coordinate, Graph.Entry(destination: segment1, distance: segment.1.squaredDistanceApproximation(to: c2d).squareRoot(), trackName: track.name)) 53 | 54 | if let vertex = track.vertexAfter(coordinate: segment0, graph: graph!) { 55 | add(from: segment0, Graph.Entry(destination: vertex.0, distance: vertex.1, trackName: track.name)) 56 | } else { 57 | print("error") 58 | } 59 | if let vertex = track.vertexBefore(coordinate: segment0, graph: graph!) { 60 | add(from: segment0, Graph.Entry(destination: vertex.0, distance: vertex.1, trackName: track.name)) 61 | } else { 62 | print("error") 63 | } 64 | 65 | if let vertex = track.vertexAfter(coordinate: segment1, graph: graph!) { 66 | add(from: segment1, Graph.Entry(destination: vertex.0, distance: vertex.1, trackName: track.name)) 67 | } else { 68 | print("error") 69 | } 70 | if let vertex = track.vertexBefore(coordinate: segment1, graph: graph!) { 71 | add(from: segment1, Graph.Entry(destination: vertex.0, distance: vertex.1, trackName: track.name)) 72 | } else { 73 | print("error") 74 | } 75 | 76 | 77 | if route == nil { 78 | route = Route(track: track, coordinate: coordinate) 79 | } else { 80 | route!.add(coordinate: coordinate, inTrack: track, graph: graph!) 81 | } 82 | } 83 | } 84 | 85 | struct DisplayState: Equatable, Codable { 86 | var tracks: [Track] 87 | var loading: Bool { return tracks.isEmpty } 88 | 89 | var routing: Bool = false { 90 | didSet { 91 | if routing { 92 | selection = nil 93 | } else { 94 | route = nil 95 | } 96 | } 97 | } 98 | var route: Route? 99 | 100 | var selection: Track? { 101 | didSet { 102 | trackPosition = nil 103 | } 104 | } 105 | 106 | var graph: Graph? 107 | var graphBuildingProgress: Float = 0 108 | 109 | var hasSelection: Bool { 110 | return routing == false && selection != nil 111 | } 112 | 113 | var trackPosition: CGFloat? // 0...1 114 | 115 | init(tracks: [Track]) { 116 | selection = nil 117 | trackPosition = nil 118 | self.tracks = tracks 119 | } 120 | 121 | var draggedLocation: (Double, CLLocation)? { 122 | guard let track = selection, 123 | let location = trackPosition else { return nil } 124 | let distance = Double(location) * track.distance 125 | guard let point = track.point(at: distance) else { return nil } 126 | return (distance: distance, location: point) 127 | } 128 | 129 | static func ==(lhs: DisplayState, rhs: DisplayState) -> Bool { 130 | return lhs.selection == rhs.selection && lhs.trackPosition == rhs.trackPosition && lhs.tracks == rhs.tracks && lhs.graph == rhs.graph && lhs.routing == rhs.routing && lhs.route == rhs.route && lhs.graphBuildingProgress == rhs.graphBuildingProgress 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /Laufpark/Views.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Views.swift 3 | // Laufpark 4 | // 5 | // Created by Chris Eidhof on 17.09.17. 6 | // Copyright © 2017 objc.io. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import Incremental 11 | import MapKit 12 | 13 | enum Stylesheet { 14 | static let emphasis: I = I(constant: UIFont.boldSystemFont(ofSize: 17)) 15 | 16 | static let smallFont = UIFont.systemFont(ofSize: 10) 17 | 18 | static let blue: UIColor = UIColor(red: 13.0/255, green: 107.0/255, blue: 181.0/255, alpha: 1) 19 | 20 | static let regularInset: CGFloat = 10 21 | 22 | static let dampingAnimation: Animation = { parent, _ in 23 | UIViewPropertyAnimator(duration: 0.2, dampingRatio: 0.6) { 24 | parent.layoutIfNeeded() 25 | }.startAnimation() 26 | } 27 | } 28 | 29 | extension LineView.Point { 30 | func distanceTo(segment: (LineView.Point, LineView.Point)) -> Double { 31 | let a = x - segment.0.x 32 | let b = y - segment.0.y 33 | let c = segment.1.x - segment.0.x 34 | let d = segment.1.x - segment.0.x 35 | 36 | let dot = a * c + b * d 37 | let lenSq = c*c + d*d 38 | let param = dot / lenSq 39 | 40 | let p: LineView.Point 41 | if param < 0 || (segment.0 == segment.1) { 42 | p = segment.0 43 | } else if param > 1 { 44 | p = segment.1 45 | } else { 46 | p = LineView.Point(x: segment.0.x + param * c, y: segment.0.y + param * d) 47 | } 48 | 49 | let dx = x - p.x 50 | let dy = y - p.y 51 | return (dx*dx + dy*dy).squareRoot() 52 | } 53 | } 54 | 55 | func trackInfoView(position: I, points: I<[LineView.Point]>, track: I, darkMode: I) -> (IBox, location: I) { 56 | let pannedLocation: Input = Input(0) 57 | let result = IBox(UIView()) 58 | 59 | let foregroundColor: I = if_(darkMode, then: I(constant: .white), else: I(constant: .black)) 60 | let lv = lineView(position: position, points: points, strokeColor: foregroundColor) 61 | lv.unbox.backgroundColor = .clear 62 | lv.addGestureRecognizer(panGestureRecognizer { sender in 63 | let normalizedLocation = (sender.location(in: sender.view!).x / 64 | sender.view!.bounds.size.width).clamped(to: 0.0...1.0) 65 | pannedLocation.write(normalizedLocation) 66 | }) 67 | 68 | let formatter = MKDistanceFormatter() 69 | let formattedDistance = track.map { track in 70 | track.map { formatter.string(fromDistance: $0.distance) } 71 | } ?? "" 72 | let formattedAscent = track.map { track in 73 | track.map { "↗ \(formatter.string(fromDistance: $0.ascent))" } 74 | } ?? "" 75 | //let name = label(text: track.map { $0?.name ?? "" }, textColor: foregroundColor.map { $0 }) 76 | let totalDistance = label(text: formattedDistance, textColor: foregroundColor.map { $0 }, font: Stylesheet.emphasis) 77 | let totalAscent = label(text: formattedAscent, textColor: foregroundColor.map { $0 }, font: Stylesheet.emphasis) 78 | let spacer = IBox(UILabel()) 79 | 80 | // Track information 81 | let trackInfo = IBox(arrangedSubviews: [totalDistance, totalAscent, spacer], axis: .horizontal) 82 | trackInfo.unbox.distribution = .equalCentering 83 | trackInfo.unbox.spacing = 10 84 | 85 | result.addSubview(trackInfo, constraints: [equal(\.leadingAnchor), equal(\.trailingAnchor), equal(\.topAnchor)]) 86 | result.addSubview(lv.cast, constraints: [equal(\.leadingAnchor), equal(\.trailingAnchor), equal(\.bottomAnchor), equalTo(constant: I(constant: 100), \.heightAnchor)]) 87 | lv.unbox.topAnchor.constraint(equalTo: trackInfo.unbox.bottomAnchor).isActive = true 88 | 89 | return (result, pannedLocation.i) 90 | } 91 | 92 | func effectView(effect: I) -> IBox { 93 | let view = UIVisualEffectView(effect: nil) 94 | let result = IBox(view) 95 | result.observe(value: effect, onChange: { view, value in 96 | UIView.animate(withDuration: 0.2) { 97 | view.effect = value 98 | } 99 | }) 100 | return result 101 | } 102 | 103 | func trackNumberView(_ track: I) -> IBox { 104 | let diameter: CGFloat = 42 105 | let circle = UIView(frame: .init(origin: .zero, size: CGSize(width: diameter, height: diameter))) 106 | circle.layer.cornerRadius = diameter/2 107 | circle.layer.masksToBounds = true 108 | circle.translatesAutoresizingMaskIntoConstraints = false 109 | circle.widthAnchor.constraint(equalToConstant: diameter).isActive = true 110 | circle.heightAnchor.constraint(equalToConstant: diameter).isActive = true 111 | 112 | let backgroundColor = track.map { $0.color.uiColor } 113 | let result = IBox(circle) 114 | result.bind(backgroundColor, to: \.backgroundColor) 115 | 116 | 117 | let numberLabel = label(text: track.map { $0.numbers }, backgroundColor: backgroundColor.map { $0 }, textColor: track.map { $0.color.textColor }, font: Stylesheet.emphasis) 118 | result.addSubview(numberLabel, constraints: [ 119 | equal(\.centerXAnchor), equal(\.centerYAnchor)]) 120 | 121 | return result 122 | } 123 | 124 | extension CLLocationCoordinate2D: Equatable { 125 | public static func ==(lhs: CLLocationCoordinate2D, rhs: CLLocationCoordinate2D) -> Bool { 126 | return lhs.latitude == rhs.latitude && lhs.longitude == rhs.longitude 127 | } 128 | } 129 | 130 | func lineView(position: I, points: I<[LineView.Point]>, strokeColor: I) -> IBox { 131 | let box = IBox(LineView()) 132 | box.bind(position, to: \.position) 133 | box.bind(points, to: \.points) 134 | box.bind(strokeColor, to: \.strokeColor) 135 | return box 136 | } 137 | -------------------------------------------------------------------------------- /Laufpark/btn_close@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chriseidhof/laufpark-stechlin/807aec8af39d13f34813f9ef795d77f76f017e71/Laufpark/btn_close@2x.png -------------------------------------------------------------------------------- /Laufpark/btn_map.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chriseidhof/laufpark-stechlin/807aec8af39d13f34813f9ef795d77f76f017e71/Laufpark/btn_map.png -------------------------------------------------------------------------------- /Laufpark/btn_map@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chriseidhof/laufpark-stechlin/807aec8af39d13f34813f9ef795d77f76f017e71/Laufpark/btn_map@2x.png -------------------------------------------------------------------------------- /Laufpark/btn_route@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chriseidhof/laufpark-stechlin/807aec8af39d13f34813f9ef795d77f76f017e71/Laufpark/btn_route@2x.png -------------------------------------------------------------------------------- /Laufpark/btn_satellite.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chriseidhof/laufpark-stechlin/807aec8af39d13f34813f9ef795d77f76f017e71/Laufpark/btn_satellite.png -------------------------------------------------------------------------------- /Laufpark/btn_satellite@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chriseidhof/laufpark-stechlin/807aec8af39d13f34813f9ef795d77f76f017e71/Laufpark/btn_satellite@2x.png -------------------------------------------------------------------------------- /Laufpark/credits.txt: -------------------------------------------------------------------------------- 1 | Icons: 2 | 3 | - close and route button: the noun project 4 | -------------------------------------------------------------------------------- /Laufpark/de.lproj/Localizable.strings: -------------------------------------------------------------------------------- 1 | /* 2 | Localized.strings 3 | Laufpark 4 | 5 | Created by Chris Eidhof on 12.12.17. 6 | Copyright © 2017 objc.io. All rights reserved. 7 | */ 8 | 9 | "tapAnyWhereToStart" = "Tippe auf die Karte um einen Startpunkt festzulegen"; 10 | "loadingGraph" = "Laden..."; 11 | "karte" = "Karte"; 12 | "satellite" = "Satellit"; 13 | "route" = "Planer"; 14 | "close" = "Schließen"; 15 | "undo" = "Rückgangig machen"; 16 | -------------------------------------------------------------------------------- /Laufpark/en.lproj/Localizable.strings: -------------------------------------------------------------------------------- 1 | /* 2 | Localized.strings 3 | Laufpark 4 | 5 | Created by Chris Eidhof on 12.12.17. 6 | Copyright © 2017 objc.io. All rights reserved. 7 | */ 8 | 9 | tapAnyWhereToStart = "Tap on the map to start"; 10 | loadingGraph = "Loading..."; 11 | karte = "Map"; 12 | satellite = "Satellite"; 13 | route = "Route"; 14 | close = "Close"; 15 | undo = "Undo"; 16 | 17 | -------------------------------------------------------------------------------- /Laufpark/map.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chriseidhof/laufpark-stechlin/807aec8af39d13f34813f9ef795d77f76f017e71/Laufpark/map.png -------------------------------------------------------------------------------- /Laufpark/map@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chriseidhof/laufpark-stechlin/807aec8af39d13f34813f9ef795d77f76f017e71/Laufpark/map@2x.png -------------------------------------------------------------------------------- /Laufpark/partner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chriseidhof/laufpark-stechlin/807aec8af39d13f34813f9ef795d77f76f017e71/Laufpark/partner.png -------------------------------------------------------------------------------- /LaufparkUITests/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 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /LaufparkUITests/LaufparkUITests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LaufparkUITests.swift 3 | // LaufparkUITests 4 | // 5 | // Created by Chris Eidhof on 23.12.17. 6 | // Copyright © 2017 objc.io. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | 11 | class LaufparkUITests: XCTestCase { 12 | var app: XCUIApplication! = nil 13 | 14 | override func setUp() { 15 | super.setUp() 16 | // In UI tests it is usually best to stop immediately when a failure occurs. 17 | continueAfterFailure = false 18 | 19 | app = XCUIApplication() 20 | setupSnapshot(app) 21 | } 22 | 23 | override func tearDown() { 24 | // Put teardown code here. This method is called after the invocation of each test method in the class. 25 | super.tearDown() 26 | } 27 | 28 | func testMain() { 29 | app.launchArguments.append(contentsOf: ["--uitesting", "--testHome"]) 30 | app.launch() 31 | snapshot("01HomeScreen") 32 | } 33 | 34 | func testSelection() { 35 | app.launchArguments.append(contentsOf: ["--uitesting", "--testSelection"]) 36 | app.launch() 37 | app.maps.element.coordinate(withNormalizedOffset: CGVector(dx: 0.6, dy: 0.4)).tap() 38 | snapshot("02Selection") 39 | 40 | } 41 | 42 | // func testSatellite() { 43 | // 44 | // } 45 | } 46 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | On the App Store: [Laufpark Stechlin](https://itunes.apple.com/us/app/laufpark-stechlin/id1329204936?mt=8) 2 | -------------------------------------------------------------------------------- /Shared Code/Model+MapKit.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Model+MapKit.swift 3 | // Laufpark 4 | // 5 | // Created by Chris Eidhof on 12.11.17. 6 | // Copyright © 2017 objc.io. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import MapKit 11 | 12 | extension Track { 13 | var polygon: MKPolygon { 14 | var coordinates = self.coordinates.map { $0.coordinate.clLocationCoordinate } 15 | let result = MKPolygon(coordinates: &coordinates, count: coordinates.count) 16 | return result 17 | } 18 | 19 | typealias ElevationProfile = [(distance: CLLocationDistance, elevation: Double)] 20 | var elevationProfile: ElevationProfile { 21 | let result = coordinates.diffed { l, r in 22 | (CLLocation(l.coordinate.clLocationCoordinate).distance(from: CLLocation(r.coordinate.clLocationCoordinate)), r.elevation) 23 | } 24 | var distanceTotal = 0 as CLLocationDistance 25 | return result.map { pair in 26 | defer { distanceTotal += pair.0 } 27 | return (distance: distanceTotal, elevation: pair.1) 28 | } 29 | } 30 | } 31 | 32 | extension MKPointAnnotation { 33 | convenience init(coordinate: CLLocationCoordinate2D, title: String) { 34 | self.init() 35 | self.coordinate = coordinate 36 | self.title = title 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Shared Code/Model+UIKit.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Model+UIKit.swift 3 | // Laufpark 4 | // 5 | // Created by Chris Eidhof on 08.09.17. 6 | // Copyright © 2017 objc.io. All rights reserved. 7 | // 8 | 9 | #if os(OSX) 10 | import Cocoa 11 | typealias LColor = NSColor 12 | #else 13 | import UIKit 14 | typealias LColor = UIColor 15 | #endif 16 | 17 | 18 | extension LColor { 19 | convenience init(r: Int, g: Int, b: Int) { 20 | self.init(red: CGFloat(r)/255, green: CGFloat(g)/255, blue: CGFloat(b)/255, alpha: 1) 21 | } 22 | } 23 | 24 | extension Color { 25 | var textColor: LColor { 26 | switch self { 27 | case .yellow, .gray, .beige: 28 | return .black 29 | default: 30 | return .white 31 | } 32 | } 33 | var uiColor: LColor { 34 | switch self { 35 | case .red: 36 | return LColor(r: 255, g: 0, b: 0) 37 | case .turquoise: 38 | return LColor(r: 0, g: 159, b: 159) 39 | case .brightGreen: 40 | return LColor(r: 104, g: 195, b: 12) 41 | case .violet: 42 | return LColor(r: 174, g: 165, b: 213) 43 | case .purple: 44 | return LColor(r: 135, g: 27, b: 138) 45 | case .green: 46 | return LColor(r: 0, g: 132, b: 70) 47 | case .beige: 48 | return LColor(r: 227, g: 177, b: 151) 49 | case .blue: 50 | return LColor(r: 0, g: 92, b: 181) 51 | case .brown: 52 | return LColor(r: 126, g: 50, b: 55) 53 | case .yellow: 54 | return LColor(r: 255, g: 244, b: 0) 55 | case .gray: 56 | return LColor(r: 174, g: 165, b: 213) 57 | case .lightBlue: 58 | return LColor(r: 0, g: 166, b: 198) 59 | case .lightBrown: 60 | return LColor(r: 190, g: 135, b: 90) 61 | case .orange: 62 | return LColor(r: 255, g: 122, b: 36) 63 | case .pink: 64 | return LColor(r: 255, g: 0, b: 94) 65 | case .lightPink: 66 | return LColor(r: 255, g: 122, b: 183) 67 | } 68 | } 69 | } 70 | 71 | 72 | -------------------------------------------------------------------------------- /Shared Code/Model.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Model.swift 3 | // Laufpark 4 | // 5 | // Created by Chris Eidhof on 07.09.17. 6 | // Copyright © 2017 objc.io. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import CoreLocation 11 | 12 | extension Color { 13 | var name: String { 14 | switch self { 15 | case .red: return "rot" 16 | case .turquoise: return "tuerkis" 17 | case .brightGreen: return "hellgruen" 18 | case .beige: return "beige" 19 | case .green: return "gruen" 20 | case .purple: return "lila" 21 | case .violet: return "violett" 22 | case .blue: return "blau" 23 | case .brown: return "braun" 24 | case .yellow: return "gelb" 25 | case .gray: return "grau" 26 | case .lightBlue: return "hellblau" 27 | case .lightBrown: return "hellbraun" 28 | case .orange: return "orange" 29 | case .pink: return "pink" 30 | case .lightPink: return "rosa" 31 | } 32 | } 33 | } 34 | 35 | struct POI { 36 | let location: CLLocationCoordinate2D 37 | let name: String 38 | 39 | static let all: [POI] = [ 40 | POI(location: CLLocationCoordinate2D(latitude: 53.187240, longitude: 13.088585), name: "Gasthaus Haveleck"), 41 | POI(location: CLLocationCoordinate2D(latitude: 53.191610, longitude: 13.159954), name: "Jugendherberge Ravensbrück"), 42 | POI(location: CLLocationCoordinate2D(latitude: 53.179984, longitude: 12.899209), name: "Hotel & Ferienanlage Precise Resort Marina Wolfsbruch"), 43 | POI(location: CLLocationCoordinate2D(latitude: 52.966637,longitude: 13.281789), name: "Pension Lindenhof"), 44 | POI(location: CLLocationCoordinate2D(latitude: 53.091639, longitude: 13.093251), name: "Gut Zernikow"), 45 | POI(location: CLLocationCoordinate2D(latitude: 53.031421, longitude: 13.30988), name: "Ziegeleipark Mildenberg"), 46 | POI(location: CLLocationCoordinate2D(latitude: 53.112691, longitude: 13.104139), name: "Hotel und Restaurant \"Zum Birkenhof\""), 47 | POI(location: CLLocationCoordinate2D(latitude: 53.167976, longitude: 13.23558), name: "Campingpark Himmelpfort"), 48 | POI(location: CLLocationCoordinate2D(latitude: 53.115591, longitude: 12.889571), name: "Maritim Hafenhotel Reinsberg"), 49 | POI(location: CLLocationCoordinate2D(latitude: 53.175714, longitude: 13.232601), name: "Ferienwohnung in der Mühle Himmelpfort"), 50 | POI(location: CLLocationCoordinate2D(latitude: 53.115685, longitude: 13.25494), name: "Gut Boltenhof"), 51 | POI(location: CLLocationCoordinate2D(latitude: 53.053821, longitude: 13.083495), name: "Werkshof Wolfsruh") 52 | ] 53 | } 54 | 55 | enum Color: Int, Codable { 56 | case red 57 | case turquoise 58 | case brightGreen 59 | case violet 60 | case purple 61 | case green 62 | case beige 63 | case blue 64 | case brown 65 | case yellow 66 | case gray 67 | case lightBlue 68 | case lightBrown 69 | case orange 70 | case pink 71 | case lightPink 72 | } 73 | 74 | extension CLLocation { 75 | convenience init(_ coordinate: CLLocationCoordinate2D) { 76 | self.init(latitude: coordinate.latitude, longitude: coordinate.longitude) 77 | } 78 | } 79 | 80 | extension Collection { 81 | func diffed() -> AnySequence<(Element, Element)> { 82 | return AnySequence(zip(self, self.dropFirst())) 83 | } 84 | 85 | func diffed(with combine: (Element, Element) -> Result) -> [Result] { 86 | return zip(self, self.dropFirst()).map { combine($0.0, $0.1) } 87 | } 88 | 89 | } 90 | 91 | struct Coordinate: Codable { 92 | let latitude: Double 93 | let longitude: Double 94 | } 95 | 96 | extension Coordinate: Equatable, Hashable { } 97 | 98 | extension Coordinate { 99 | init(_ locationCoordinate: CLLocationCoordinate2D) { 100 | self.latitude = locationCoordinate.latitude 101 | self.longitude = locationCoordinate.longitude 102 | } 103 | 104 | var clLocationCoordinate: CLLocationCoordinate2D { 105 | return CLLocationCoordinate2D(latitude: self.latitude, longitude: self.longitude) 106 | } 107 | } 108 | 109 | struct CoordinateWithElevation: Codable { 110 | let coordinate: Coordinate 111 | let elevation: Double 112 | } 113 | 114 | extension Collection where Element == CLLocation { 115 | var distance: CLLocationDistance { 116 | guard let first = self.first else { return 0 } 117 | 118 | let (result, _) = reduce(into: (0 as CLLocationDistance, previous: first)) { r, coord in 119 | let distance = coord.distance(from: r.1) 120 | r.1 = coord 121 | r.0 += distance 122 | } 123 | return result 124 | 125 | } 126 | } 127 | struct Track: Codable { 128 | var coordinates: [CoordinateWithElevation] 129 | let color: Color 130 | let number: Int 131 | let name: String 132 | 133 | var distance: CLLocationDistance { 134 | return coordinates.map { CLLocation($0.coordinate.clLocationCoordinate) }.distance 135 | } 136 | 137 | var ascent: Double { 138 | let elevations = coordinates.lazy.map { $0.elevation } 139 | return elevations.diffed(with: -).filter({ $0 > 0 }).reduce(0,+) 140 | } 141 | 142 | func point(at distance: CLLocationDistance) -> CLLocation? { 143 | var current = 0 as CLLocationDistance 144 | for (p1, p2) in coordinates.lazy.map({ CLLocation($0.coordinate.clLocationCoordinate) }).diffed() { 145 | current += p2.distance(from: p1) 146 | if current > distance { return p2 } 147 | } 148 | return nil 149 | } 150 | 151 | var numbers: String { 152 | let components = name.split(separator: " ") 153 | guard !components.isEmpty else { return "" } 154 | 155 | func simplify(_ numbers: [S]) -> String { 156 | if numbers.count == 1 { return String(numbers[0]) } 157 | return String("\(numbers[0])-\(numbers.last!)") 158 | } 159 | 160 | return simplify(components.last!.split(separator: "/")) 161 | } 162 | } 163 | 164 | extension Track: Equatable { 165 | static func ==(l: Track, r: Track) -> Bool { 166 | return l.name == r.name // todo 167 | } 168 | } 169 | 170 | extension Track { 171 | init(color: Color, number: Int, name: String, points: [Point]) { 172 | self.color = color 173 | self.number = number 174 | self.name = name 175 | coordinates = points.map { point in 176 | CoordinateWithElevation(coordinate: Coordinate(latitude: point.lat, longitude: point.lon), elevation: point.ele) 177 | } 178 | } 179 | } 180 | 181 | struct Point { 182 | let lat: Double 183 | let lon: Double 184 | let ele: Double 185 | } 186 | 187 | extension String { 188 | func remove(prefix: String) -> String { 189 | return String(dropFirst(prefix.count)) 190 | } 191 | } 192 | 193 | extension Track { 194 | static func load() -> [Track] { 195 | let definitions: [(Color, Int)] = [ 196 | (.red, 4), 197 | (.turquoise, 6), 198 | (.brightGreen, 7), 199 | (.beige, 2), 200 | (.green, 4), 201 | (.purple, 3), 202 | (.violet, 4), 203 | (.blue, 3), 204 | (.brown, 4), 205 | (.yellow, 4), 206 | (.gray, 0), 207 | (.lightBlue, 4), 208 | (.lightBrown, 5), 209 | (.orange, 0), 210 | (.pink, 4), 211 | (.lightPink, 6) 212 | ] 213 | var allTracks: [[Track]] = [] 214 | allTracks = definitions.map { (color, count) in 215 | let begin = count == 0 ? 0 : 1 216 | let trackNames: [(Int, String)] = (begin...count).map { ($0, "wabe \(color.name)-strecke \($0)") } 217 | return trackNames.map { numberAndName -> Track in 218 | let reader = TrackReader(url: Bundle.main.url(forResource: numberAndName.1, withExtension: "gpx")!)! 219 | return Track(color: color, number: numberAndName.0, name: reader.name, points: reader.points) 220 | } 221 | } 222 | return Array(allTracks.joined()) 223 | } 224 | } 225 | 226 | final class TrackReader: NSObject, XMLParserDelegate { 227 | var inTrk = false 228 | 229 | var points: [Point] = [] 230 | var pending: (lat: Double, lon: Double)? 231 | var elementContents: String = "" 232 | var name = "" 233 | 234 | init?(url: URL) { 235 | guard let parser = XMLParser(contentsOf: url) else { return nil } 236 | super.init() 237 | parser.delegate = self 238 | guard parser.parse() else { return nil } 239 | } 240 | 241 | func parser(_ parser: XMLParser, foundCharacters string: String) { 242 | elementContents += string 243 | } 244 | 245 | func parser(_ parser: XMLParser, didStartElement elementName: String, namespaceURI: String?, qualifiedName qName: String?, attributes attributeDict: [String : String] = [:]) { 246 | guard inTrk else { 247 | inTrk = elementName == "trk" 248 | return 249 | } 250 | if elementName == "trkpt" { 251 | guard let latStr = attributeDict["lat"], let lat = Double(latStr), 252 | let lonStr = attributeDict["lon"], let lon = Double(lonStr) else { return } 253 | pending = (lat: lat, lon: lon) 254 | } 255 | } 256 | 257 | func parser(_ parser: XMLParser, didEndElement elementName: String, namespaceURI: String?, qualifiedName qName: String?) { 258 | defer { elementContents = "" } 259 | var trimmed: String { return elementContents.trimmingCharacters(in: .whitespacesAndNewlines) } 260 | if elementName == "trk" { 261 | inTrk = false 262 | } else if elementName == "ele" { 263 | guard let p = pending, let ele = Double(trimmed) else { return } 264 | points.append(Point(lat: p.lat, lon: p.lon, ele: ele)) 265 | } else if elementName == "name" && inTrk { 266 | name = trimmed.remove(prefix: "Laufpark Stechlin - Wabe ") 267 | } 268 | } 269 | } 270 | -------------------------------------------------------------------------------- /Shared Code/SortedArray.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SortedArray.swift 3 | // Laufpark 4 | // 5 | // Created by Chris Eidhof on 11.12.17. 6 | // Copyright © 2017 objc.io. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | import Foundation 12 | 13 | struct SortedArray { 14 | var elements: [Element] 15 | let isAscending: (Element, Element) -> Bool 16 | 17 | init(unsorted: S, isAscending: @escaping (Element, Element) -> Bool) where S.Iterator.Element == Element { 18 | elements = unsorted.sorted(by: isAscending) 19 | self.isAscending = isAscending 20 | } 21 | 22 | func index(for element: Element) -> Int { 23 | var start = elements.startIndex 24 | var end = elements.endIndex 25 | while start < end { 26 | let middle = start + (end - start) / 2 27 | if isAscending(elements[middle], element) { 28 | start = middle + 1 29 | } else { 30 | end = middle 31 | } 32 | } 33 | assert(start == end) 34 | return start 35 | } 36 | 37 | mutating func insert(_ element: Element) { 38 | elements.insert(element, at: index(for: element)) 39 | } 40 | 41 | mutating func mutate(at index: Int, _ change: (inout Element) -> ()) { 42 | var value = elements.remove(at: index) // todo: could be more optimal? 43 | change(&value) 44 | insert(value) 45 | } 46 | 47 | mutating func popLast() -> Element? { 48 | return elements.popLast() 49 | } 50 | 51 | func index(of needle: Element, _ isEqual: (Element, Element) -> Bool) -> Int? { 52 | let i = index(for: needle) 53 | guard i < endIndex else { return nil } 54 | return isEqual(elements[i], needle) ? i : nil 55 | } 56 | } 57 | 58 | extension SortedArray where Element: Equatable { 59 | func contains(element: Element) -> Bool { 60 | let index = self.index(for: element) 61 | guard index < elements.endIndex else { return false } 62 | return self[index] == element 63 | } 64 | } 65 | 66 | extension SortedArray: Collection { 67 | var startIndex: Int { 68 | return elements.startIndex 69 | } 70 | 71 | var endIndex: Int { 72 | return elements.endIndex 73 | } 74 | 75 | subscript(index: Int) -> Element { 76 | return elements[index] 77 | } 78 | 79 | func index(after i: Int) -> Int { 80 | return elements.index(after: i) 81 | } 82 | 83 | func min() -> Element? { 84 | return elements.first 85 | } 86 | } 87 | 88 | -------------------------------------------------------------------------------- /Shared Code/gpx/wabe tuerkis-strecke 3.gpx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Laufpark Stechlin - Wabe Türkis/Strecke 3 5 | 6 | Laufpark Stechlin - Community 7 | 8 | 9 | 10 | 11 | Laufpark Stechlin - Wabe Türkis/Strecke 3 12 | Startort: Vogelsang 13 | 14 | 15 | 51.41048 16 | 17 | 18 | 51.40311 19 | 20 | 21 | 51.39005 22 | 23 | 24 | 51.35782 25 | 26 | 27 | 51.32008 28 | 29 | 30 | 51.20940 31 | 32 | 33 | 51.18556 34 | 35 | 36 | 51.24585 37 | 38 | 39 | 51.24585 40 | 41 | 42 | 51.15723 43 | 44 | 45 | 51.11346 46 | 47 | 48 | 51.07194 49 | 50 | 51 | 50.91746 52 | 53 | 54 | 50.68216 55 | 56 | 57 | 50.47147 58 | 59 | 60 | 50.07811 61 | 62 | 63 | 49.39531 64 | 65 | 66 | 48.99259 67 | 68 | 69 | 49.00494 70 | 71 | 72 | 49.02434 73 | 74 | 75 | 49.02434 76 | 77 | 78 | 48.87274 79 | 80 | 81 | 48.70689 82 | 83 | 84 | 48.59093 85 | 86 | 87 | 48.56743 88 | 89 | 90 | 48.52632 91 | 92 | 93 | 48.52175 94 | 95 | 96 | 48.51523 97 | 98 | 99 | 48.50227 100 | 101 | 102 | 48.49690 103 | 104 | 105 | 48.49012 106 | 107 | 108 | 48.48122 109 | 110 | 111 | 48.47552 112 | 113 | 114 | 48.46927 115 | 116 | 117 | 48.45914 118 | 119 | 120 | 48.45065 121 | 122 | 123 | 48.43958 124 | 125 | 126 | 48.43804 127 | 128 | 129 | 48.43658 130 | 131 | 132 | 48.43457 133 | 134 | 135 | 48.43241 136 | 137 | 138 | 48.43013 139 | 140 | 141 | 48.42875 142 | 143 | 144 | 48.42608 145 | 146 | 147 | 48.42181 148 | 149 | 150 | 48.41852 151 | 152 | 153 | 48.41176 154 | 155 | 156 | 48.40303 157 | 158 | 159 | 48.39576 160 | 161 | 162 | 48.39469 163 | 164 | 165 | 48.41660 166 | 167 | 168 | 48.35316 169 | 170 | 171 | 48.34972 172 | 173 | 174 | 48.34972 175 | 176 | 177 | 48.39455 178 | 179 | 180 | 48.46800 181 | 182 | 183 | 48.49443 184 | 185 | 186 | 48.51223 187 | 188 | 189 | 48.51223 190 | 191 | 192 | 48.51159 193 | 194 | 195 | 48.51098 196 | 197 | 198 | 48.51098 199 | 200 | 201 | 48.50850 202 | 203 | 204 | 48.50616 205 | 206 | 207 | 48.50471 208 | 209 | 210 | 48.49933 211 | 212 | 213 | 48.49537 214 | 215 | 216 | 48.49072 217 | 218 | 219 | 48.48222 220 | 221 | 222 | 48.47846 223 | 224 | 225 | 48.47062 226 | 227 | 228 | 48.46753 229 | 230 | 231 | 48.46261 232 | 233 | 234 | 48.44767 235 | 236 | 237 | 48.32204 238 | 239 | 240 | 48.39842 241 | 242 | 243 | 48.41179 244 | 245 | 246 | 48.45485 247 | 248 | 249 | 48.48825 250 | 251 | 252 | 48.52638 253 | 254 | 255 | 48.54087 256 | 257 | 258 | 48.55184 259 | 260 | 261 | 48.55461 262 | 263 | 264 | 48.62974 265 | 266 | 267 | 48.95979 268 | 269 | 270 | 49.30324 271 | 272 | 273 | 49.40866 274 | 275 | 276 | 49.54242 277 | 278 | 279 | 49.67163 280 | 281 | 282 | 49.74003 283 | 284 | 285 | 49.89897 286 | 287 | 288 | 50.01534 289 | 290 | 291 | 50.01534 292 | 293 | 294 | 50.01534 295 | 296 | 297 | 50.01534 298 | 299 | 300 | 50.01534 301 | 302 | 303 | 50.31778 304 | 305 | 306 | 51.18038 307 | 308 | 309 | 51.28553 310 | 311 | 312 | 51.30197 313 | 314 | 315 | 51.44004 316 | 317 | 318 | 51.58605 319 | 320 | 321 | 51.81807 322 | 323 | 324 | 52.06446 325 | 326 | 327 | 52.73801 328 | 329 | 330 | 53.63950 331 | 332 | 333 | 52.65998 334 | 335 | 336 | 52.60367 337 | 338 | 339 | 52.49730 340 | 341 | 342 | 52.48900 343 | 344 | 345 | 52.47102 346 | 347 | 348 | 52.08576 349 | 350 | 351 | 51.89471 352 | 353 | 354 | 52.24084 355 | 356 | 357 | 52.83582 358 | 359 | 360 | 52.83582 361 | 362 | 363 | 52.50707 364 | 365 | 366 | 52.20923 367 | 368 | 369 | 52.06143 370 | 371 | 372 | 51.96517 373 | 374 | 375 | 51.93189 376 | 377 | 378 | 51.90443 379 | 380 | 381 | 51.86851 382 | 383 | 384 | 51.54174 385 | 386 | 387 | 51.49264 388 | 389 | 390 | 51.45616 391 | 392 | 393 | 51.41080 394 | 395 | 396 | 397 | 398 | -------------------------------------------------------------------------------- /fastlane/Appfile: -------------------------------------------------------------------------------- 1 | app_identifier "de.laufpark-stechlin.laufpark" # The bundle identifier of your app 2 | apple_id "chris+ipad@eidhof.nl" # Your Apple email address 3 | 4 | team_id "83YZ9QLR9K" # Developer Portal Team ID 5 | 6 | # you can even provide different app identifiers, Apple IDs and team names per lane: 7 | # More information: https://github.com/fastlane/fastlane/blob/master/fastlane/docs/Appfile.md 8 | -------------------------------------------------------------------------------- /fastlane/Deliverfile: -------------------------------------------------------------------------------- 1 | ###################### More Options ###################### 2 | # If you want to have even more control, check out the documentation 3 | # https://docs.fastlane.tools/actions/deliver 4 | 5 | 6 | ###################### Automatically generated ###################### 7 | # Feel free to remove the following line if you use fastlane (which you should) 8 | 9 | app_identifier "de.laufpark-stechlin.laufpark" # The bundle identifier of your app 10 | username "chris+ipad@eidhof.nl" # your Apple ID user 11 | -------------------------------------------------------------------------------- /fastlane/Fastfile: -------------------------------------------------------------------------------- 1 | fastlane_version "2.28.3" 2 | 3 | ENV["FASTLANE_EXPLICIT_OPEN_SIMULATOR"] = "2" 4 | 5 | default_platform :ios 6 | 7 | platform :ios do 8 | desc "Runs all the tests" 9 | lane :test do 10 | scan 11 | end 12 | 13 | desc "Submit a new Beta Build to Apple TestFlight" 14 | desc "This will also make sure the profile is up to date" 15 | lane :beta do 16 | # match(type: "appstore") # more information: https://codesigning.guide 17 | gym # Build your app - more options available 18 | pilot 19 | 20 | # sh "your_script.sh" 21 | # You can also use other beta testing services here (run `fastlane actions`) 22 | end 23 | 24 | desc "Deploy a new version to the App Store" 25 | lane :release do 26 | match(type: "appstore") 27 | snapshot 28 | gym # Build your app - more options available 29 | deliver 30 | frameit 31 | end 32 | 33 | lane :screenshots do 34 | capture_screenshots 35 | upload_to_app_store 36 | end 37 | 38 | after_all do |lane| 39 | end 40 | 41 | error do |lane, exception| 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /fastlane/Gymfile: -------------------------------------------------------------------------------- 1 | scheme "Laufpark" 2 | 3 | clean true 4 | -------------------------------------------------------------------------------- /fastlane/Matchfile: -------------------------------------------------------------------------------- 1 | git_url "https://github.com/chriseidhof/laufpark-stechlin-sigh.git" 2 | 3 | type "development" # The default type, can be: appstore, adhoc, enterprise or development 4 | 5 | # app_identifier ["tools.fastlane.app", "tools.fastlane.app2"] 6 | # username "user@fastlane.tools" # Your Apple Developer Portal username 7 | 8 | # For all available options run `fastlane match --help` 9 | # Remove the # in the beginning of the line to enable the other options 10 | -------------------------------------------------------------------------------- /fastlane/README.md: -------------------------------------------------------------------------------- 1 | fastlane documentation 2 | ================ 3 | # Installation 4 | 5 | Make sure you have the latest version of the Xcode command line tools installed: 6 | 7 | ``` 8 | xcode-select --install 9 | ``` 10 | 11 | ## Choose your installation method: 12 | 13 | | Method | OS support | Description | 14 | |----------------------------|-----------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------| 15 | | [Homebrew](http://brew.sh) | macOS | `brew cask install fastlane` | 16 | | Installer Script | macOS | [Download the zip file](https://download.fastlane.tools). Then double click on the `install` script (or run it in a terminal window). | 17 | | RubyGems | macOS or Linux with Ruby 2.0.0 or above | `sudo gem install fastlane -NV` | 18 | 19 | # Available Actions 20 | ## iOS 21 | ### ios test 22 | ``` 23 | fastlane ios test 24 | ``` 25 | Runs all the tests 26 | ### ios beta 27 | ``` 28 | fastlane ios beta 29 | ``` 30 | Submit a new Beta Build to Apple TestFlight 31 | 32 | This will also make sure the profile is up to date 33 | ### ios release 34 | ``` 35 | fastlane ios release 36 | ``` 37 | Deploy a new version to the App Store 38 | 39 | ---- 40 | 41 | This README.md is auto-generated and will be re-generated every time [fastlane](https://fastlane.tools) is run. 42 | More information about fastlane can be found on [fastlane.tools](https://fastlane.tools). 43 | The documentation of fastlane can be found on [docs.fastlane.tools](https://docs.fastlane.tools). 44 | -------------------------------------------------------------------------------- /fastlane/Snapfile: -------------------------------------------------------------------------------- 1 | # Uncomment the lines below you want to change by removing the # in the beginning 2 | 3 | # A list of devices you want to take the screenshots from 4 | devices([ 5 | "iPhone 6", 6 | "iPhone 8 Plus", 7 | "iPad Pro (12.9-inch)", 8 | "iPad Pro (9.7-inch)", 9 | "iPhone X" 10 | ]) 11 | 12 | languages([ 13 | "en-US", 14 | "de-DE" 15 | ]) 16 | 17 | # The name of the scheme which contains the UI Tests 18 | scheme "Laufpark" 19 | 20 | # Where should the resulting screenshots be stored? 21 | output_directory "./fastlane/screenshots" 22 | 23 | # clear_previous_screenshots true # remove the '#' to clear all previously generated screenshots before creating new ones 24 | 25 | # Choose which project/workspace to use 26 | # project "./Project.xcodeproj" 27 | # workspace "./Project.xcworkspace" 28 | 29 | # Arguments to pass to the app on launch. See https://docs.fastlane.tools/actions/snapshot/#launch-arguments 30 | # launch_arguments(["-favColor red"]) 31 | 32 | # For more information about all available options run 33 | # fastlane snapshot --help 34 | -------------------------------------------------------------------------------- /fastlane/SnapshotHelper.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SnapshotHelper.swift 3 | // Example 4 | // 5 | // Created by Felix Krause on 10/8/15. 6 | // Copyright © 2015 Felix Krause. All rights reserved. 7 | // 8 | 9 | // ----------------------------------------------------- 10 | // IMPORTANT: When modifying this file, make sure to 11 | // increment the version number at the very 12 | // bottom of the file to notify users about 13 | // the new SnapshotHelper.swift 14 | // ----------------------------------------------------- 15 | 16 | import Foundation 17 | import XCTest 18 | 19 | var deviceLanguage = "" 20 | var locale = "" 21 | 22 | func setupSnapshot(_ app: XCUIApplication) { 23 | Snapshot.setupSnapshot(app) 24 | } 25 | 26 | func snapshot(_ name: String, waitForLoadingIndicator: Bool) { 27 | if waitForLoadingIndicator { 28 | Snapshot.snapshot(name) 29 | } else { 30 | Snapshot.snapshot(name, timeWaitingForIdle: 0) 31 | } 32 | } 33 | 34 | /// - Parameters: 35 | /// - name: The name of the snapshot 36 | /// - timeout: Amount of seconds to wait until the network loading indicator disappears. Pass `0` if you don't want to wait. 37 | func snapshot(_ name: String, timeWaitingForIdle timeout: TimeInterval = 20) { 38 | Snapshot.snapshot(name, timeWaitingForIdle: timeout) 39 | } 40 | 41 | enum SnapshotError: Error, CustomDebugStringConvertible { 42 | case cannotDetectUser 43 | case cannotFindHomeDirectory 44 | case cannotFindSimulatorHomeDirectory 45 | case cannotAccessSimulatorHomeDirectory(String) 46 | 47 | var debugDescription: String { 48 | switch self { 49 | case .cannotDetectUser: 50 | return "Couldn't find Snapshot configuration files - can't detect current user " 51 | case .cannotFindHomeDirectory: 52 | return "Couldn't find Snapshot configuration files - can't detect `Users` dir" 53 | case .cannotFindSimulatorHomeDirectory: 54 | return "Couldn't find simulator home location. Please, check SIMULATOR_HOST_HOME env variable." 55 | case .cannotAccessSimulatorHomeDirectory(let simulatorHostHome): 56 | return "Can't prepare environment. Simulator home location is inaccessible. Does \(simulatorHostHome) exist?" 57 | } 58 | } 59 | } 60 | 61 | @objcMembers 62 | open class Snapshot: NSObject { 63 | static var app: XCUIApplication! 64 | static var cacheDirectory: URL! 65 | static var screenshotsDirectory: URL? { 66 | return cacheDirectory.appendingPathComponent("screenshots", isDirectory: true) 67 | } 68 | 69 | open class func setupSnapshot(_ app: XCUIApplication) { 70 | do { 71 | let cacheDir = try pathPrefix() 72 | Snapshot.cacheDirectory = cacheDir 73 | Snapshot.app = app 74 | setLanguage(app) 75 | setLocale(app) 76 | setLaunchArguments(app) 77 | } catch let error { 78 | print(error) 79 | } 80 | } 81 | 82 | class func setLanguage(_ app: XCUIApplication) { 83 | let path = cacheDirectory.appendingPathComponent("language.txt") 84 | 85 | do { 86 | let trimCharacterSet = CharacterSet.whitespacesAndNewlines 87 | deviceLanguage = try String(contentsOf: path, encoding: .utf8).trimmingCharacters(in: trimCharacterSet) 88 | app.launchArguments += ["-AppleLanguages", "(\(deviceLanguage))"] 89 | } catch { 90 | print("Couldn't detect/set language...") 91 | } 92 | } 93 | 94 | class func setLocale(_ app: XCUIApplication) { 95 | let path = cacheDirectory.appendingPathComponent("locale.txt") 96 | 97 | do { 98 | let trimCharacterSet = CharacterSet.whitespacesAndNewlines 99 | locale = try String(contentsOf: path, encoding: .utf8).trimmingCharacters(in: trimCharacterSet) 100 | } catch { 101 | print("Couldn't detect/set locale...") 102 | } 103 | if locale.isEmpty { 104 | locale = Locale(identifier: deviceLanguage).identifier 105 | } 106 | app.launchArguments += ["-AppleLocale", "\"\(locale)\""] 107 | } 108 | 109 | class func setLaunchArguments(_ app: XCUIApplication) { 110 | let path = cacheDirectory.appendingPathComponent("snapshot-launch_arguments.txt") 111 | app.launchArguments += ["-FASTLANE_SNAPSHOT", "YES", "-ui_testing"] 112 | 113 | do { 114 | let launchArguments = try String(contentsOf: path, encoding: String.Encoding.utf8) 115 | let regex = try NSRegularExpression(pattern: "(\\\".+?\\\"|\\S+)", options: []) 116 | let matches = regex.matches(in: launchArguments, options: [], range: NSRange(location: 0, length: launchArguments.count)) 117 | let results = matches.map { result -> String in 118 | (launchArguments as NSString).substring(with: result.range) 119 | } 120 | app.launchArguments += results 121 | } catch { 122 | print("Couldn't detect/set launch_arguments...") 123 | } 124 | } 125 | 126 | open class func snapshot(_ name: String, timeWaitingForIdle timeout: TimeInterval = 20) { 127 | if timeout > 0 { 128 | waitForLoadingIndicatorToDisappear(within: timeout) 129 | } 130 | 131 | print("snapshot: \(name)") // more information about this, check out https://github.com/fastlane/fastlane/tree/master/snapshot#how-does-it-work 132 | 133 | sleep(1) // Waiting for the animation to be finished (kind of) 134 | 135 | #if os(OSX) 136 | XCUIApplication().typeKey(XCUIKeyboardKeySecondaryFn, modifierFlags: []) 137 | #else 138 | let screenshot = app.windows.firstMatch.screenshot() 139 | guard let simulator = ProcessInfo().environment["SIMULATOR_DEVICE_NAME"], let screenshotsDir = screenshotsDirectory else { return } 140 | let path = screenshotsDir.appendingPathComponent("\(simulator)-\(name).png") 141 | do { 142 | try screenshot.pngRepresentation.write(to: path) 143 | } catch let error { 144 | print("Problem writing screenshot: \(name) to \(path)") 145 | print(error) 146 | } 147 | #endif 148 | } 149 | 150 | class func waitForLoadingIndicatorToDisappear(within timeout: TimeInterval) { 151 | #if os(tvOS) 152 | return 153 | #endif 154 | 155 | let networkLoadingIndicator = XCUIApplication().otherElements.deviceStatusBars.networkLoadingIndicators.element 156 | let networkLoadingIndicatorDisappeared = XCTNSPredicateExpectation(predicate: NSPredicate(format: "exists == false"), object: networkLoadingIndicator) 157 | _ = XCTWaiter.wait(for: [networkLoadingIndicatorDisappeared], timeout: timeout) 158 | } 159 | 160 | class func pathPrefix() throws -> URL? { 161 | let homeDir: URL 162 | // on OSX config is stored in /Users//Library 163 | // and on iOS/tvOS/WatchOS it's in simulator's home dir 164 | #if os(OSX) 165 | guard let user = ProcessInfo().environment["USER"] else { 166 | throw SnapshotError.cannotDetectUser 167 | } 168 | 169 | guard let usersDir = FileManager.default.urls(for: .userDirectory, in: .localDomainMask).first else { 170 | throw SnapshotError.cannotFindHomeDirectory 171 | } 172 | 173 | homeDir = usersDir.appendingPathComponent(user) 174 | #else 175 | guard let simulatorHostHome = ProcessInfo().environment["SIMULATOR_HOST_HOME"] else { 176 | throw SnapshotError.cannotFindSimulatorHomeDirectory 177 | } 178 | guard let homeDirUrl = URL(string: simulatorHostHome) else { 179 | throw SnapshotError.cannotAccessSimulatorHomeDirectory(simulatorHostHome) 180 | } 181 | homeDir = URL(fileURLWithPath: homeDirUrl.path) 182 | #endif 183 | return homeDir.appendingPathComponent("Library/Caches/tools.fastlane") 184 | } 185 | } 186 | 187 | private extension XCUIElementAttributes { 188 | var isNetworkLoadingIndicator: Bool { 189 | if hasWhiteListedIdentifier { return false } 190 | 191 | let hasOldLoadingIndicatorSize = frame.size == CGSize(width: 10, height: 20) 192 | let hasNewLoadingIndicatorSize = frame.size.width.isBetween(46, and: 47) && frame.size.height.isBetween(2, and: 3) 193 | 194 | return hasOldLoadingIndicatorSize || hasNewLoadingIndicatorSize 195 | } 196 | 197 | var hasWhiteListedIdentifier: Bool { 198 | let whiteListedIdentifiers = ["GeofenceLocationTrackingOn", "StandardLocationTrackingOn"] 199 | 200 | return whiteListedIdentifiers.contains(identifier) 201 | } 202 | 203 | func isStatusBar(_ deviceWidth: CGFloat) -> Bool { 204 | if elementType == .statusBar { return true } 205 | guard frame.origin == .zero else { return false } 206 | 207 | let oldStatusBarSize = CGSize(width: deviceWidth, height: 20) 208 | let newStatusBarSize = CGSize(width: deviceWidth, height: 44) 209 | 210 | return [oldStatusBarSize, newStatusBarSize].contains(frame.size) 211 | } 212 | } 213 | 214 | private extension XCUIElementQuery { 215 | var networkLoadingIndicators: XCUIElementQuery { 216 | let isNetworkLoadingIndicator = NSPredicate { (evaluatedObject, _) in 217 | guard let element = evaluatedObject as? XCUIElementAttributes else { return false } 218 | 219 | return element.isNetworkLoadingIndicator 220 | } 221 | 222 | return self.containing(isNetworkLoadingIndicator) 223 | } 224 | 225 | var deviceStatusBars: XCUIElementQuery { 226 | let deviceWidth = XCUIApplication().frame.width 227 | 228 | let isStatusBar = NSPredicate { (evaluatedObject, _) in 229 | guard let element = evaluatedObject as? XCUIElementAttributes else { return false } 230 | 231 | return element.isStatusBar(deviceWidth) 232 | } 233 | 234 | return self.containing(isStatusBar) 235 | } 236 | } 237 | 238 | private extension CGFloat { 239 | func isBetween(_ numberA: CGFloat, and numberB: CGFloat) -> Bool { 240 | return numberA...numberB ~= self 241 | } 242 | } 243 | 244 | // Please don't remove the lines below 245 | // They are used to detect outdated configuration files 246 | // SnapshotHelperVersion [1.8] 247 | -------------------------------------------------------------------------------- /fastlane/metadata/app_icon.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chriseidhof/laufpark-stechlin/807aec8af39d13f34813f9ef795d77f76f017e71/fastlane/metadata/app_icon.jpg -------------------------------------------------------------------------------- /fastlane/metadata/copyright.txt: -------------------------------------------------------------------------------- 1 | 2017 Chris Eidhof 2 | -------------------------------------------------------------------------------- /fastlane/metadata/de-DE/description.txt: -------------------------------------------------------------------------------- 1 | This app allows you to see all the marked trails in Laufpark Stechlin, and create your own custom routes. 2 | -------------------------------------------------------------------------------- /fastlane/metadata/de-DE/keywords.txt: -------------------------------------------------------------------------------- 1 | running laufpark stechlin walking laufen 2 | -------------------------------------------------------------------------------- /fastlane/metadata/de-DE/marketing_url.txt: -------------------------------------------------------------------------------- 1 | http://www.laufpark-stechlin.de 2 | -------------------------------------------------------------------------------- /fastlane/metadata/de-DE/name.txt: -------------------------------------------------------------------------------- 1 | Laufpark Stechlin 2 | -------------------------------------------------------------------------------- /fastlane/metadata/de-DE/privacy_url.txt: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /fastlane/metadata/de-DE/promotional_text.txt: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /fastlane/metadata/de-DE/release_notes.txt: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /fastlane/metadata/de-DE/subtitle.txt: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /fastlane/metadata/de-DE/support_url.txt: -------------------------------------------------------------------------------- 1 | http://www.laufpark-stechlin.de 2 | -------------------------------------------------------------------------------- /fastlane/metadata/en-US/description.txt: -------------------------------------------------------------------------------- 1 | This app allows you to see all the marked trails in Laufpark Stechlin, and create your own custom routes. 2 | -------------------------------------------------------------------------------- /fastlane/metadata/en-US/keywords.txt: -------------------------------------------------------------------------------- 1 | running laufpark stechlin walking laufen 2 | -------------------------------------------------------------------------------- /fastlane/metadata/en-US/marketing_url.txt: -------------------------------------------------------------------------------- 1 | http://www.laufpark-stechlin.de 2 | -------------------------------------------------------------------------------- /fastlane/metadata/en-US/name.txt: -------------------------------------------------------------------------------- 1 | Laufpark Stechlin 2 | -------------------------------------------------------------------------------- /fastlane/metadata/en-US/privacy_url.txt: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /fastlane/metadata/en-US/promotional_text.txt: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /fastlane/metadata/en-US/release_notes.txt: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /fastlane/metadata/en-US/subtitle.txt: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /fastlane/metadata/en-US/support_url.txt: -------------------------------------------------------------------------------- 1 | http://www.laufpark-stechlin.de 2 | -------------------------------------------------------------------------------- /fastlane/metadata/primary_category.txt: -------------------------------------------------------------------------------- 1 | MZGenre.Healthcare_Fitness 2 | -------------------------------------------------------------------------------- /fastlane/metadata/primary_first_sub_category.txt: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /fastlane/metadata/primary_second_sub_category.txt: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /fastlane/metadata/review_information/demo_password.txt: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /fastlane/metadata/review_information/demo_user.txt: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /fastlane/metadata/review_information/email_address.txt: -------------------------------------------------------------------------------- 1 | chris@eidhof.nl 2 | -------------------------------------------------------------------------------- /fastlane/metadata/review_information/first_name.txt: -------------------------------------------------------------------------------- 1 | Chris 2 | -------------------------------------------------------------------------------- /fastlane/metadata/review_information/last_name.txt: -------------------------------------------------------------------------------- 1 | Eidhof 2 | -------------------------------------------------------------------------------- /fastlane/metadata/review_information/notes.txt: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /fastlane/metadata/review_information/phone_number.txt: -------------------------------------------------------------------------------- 1 | +4915785914175 2 | -------------------------------------------------------------------------------- /fastlane/metadata/secondary_category.txt: -------------------------------------------------------------------------------- 1 | MZGenre.Navigation 2 | -------------------------------------------------------------------------------- /fastlane/metadata/secondary_first_sub_category.txt: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /fastlane/metadata/secondary_second_sub_category.txt: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /fastlane/metadata/trade_representative_contact_information/address_line1.txt: -------------------------------------------------------------------------------- 1 | Dorfstr. 16 2 | -------------------------------------------------------------------------------- /fastlane/metadata/trade_representative_contact_information/city_name.txt: -------------------------------------------------------------------------------- 1 | Stechlin 2 | -------------------------------------------------------------------------------- /fastlane/metadata/trade_representative_contact_information/country.txt: -------------------------------------------------------------------------------- 1 | Germany 2 | -------------------------------------------------------------------------------- /fastlane/metadata/trade_representative_contact_information/is_displayed_on_app_store.txt: -------------------------------------------------------------------------------- 1 | false 2 | -------------------------------------------------------------------------------- /fastlane/metadata/trade_representative_contact_information/postal_code.txt: -------------------------------------------------------------------------------- 1 | 16775 2 | -------------------------------------------------------------------------------- /fastlane/metadata/trade_representative_contact_information/trade_name.txt: -------------------------------------------------------------------------------- 1 | Chris Eidhof 2 | -------------------------------------------------------------------------------- /fastlane/screenshots/README.txt: -------------------------------------------------------------------------------- 1 | Put all screenshots you want to use inside the folder of its language (e.g. en-US). 2 | The device type will automatically be recognized using the image resolution. Apple TV screenshots 3 | should be stored in a subdirectory named appleTV with language folders inside of it. iMessage 4 | screenshots, like Apple TV screenshots, should also be stored in a subdirectory named iMessage 5 | with language folders inside of it. 6 | 7 | The screenshots can be named whatever you want, but keep in mind they are sorted alphabetically. 8 | -------------------------------------------------------------------------------- /fastlane/screenshots/de-DE/iPad Pro (12.9-inch)-01HomeScreen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chriseidhof/laufpark-stechlin/807aec8af39d13f34813f9ef795d77f76f017e71/fastlane/screenshots/de-DE/iPad Pro (12.9-inch)-01HomeScreen.png -------------------------------------------------------------------------------- /fastlane/screenshots/de-DE/iPad Pro (12.9-inch)-02Selection.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chriseidhof/laufpark-stechlin/807aec8af39d13f34813f9ef795d77f76f017e71/fastlane/screenshots/de-DE/iPad Pro (12.9-inch)-02Selection.png -------------------------------------------------------------------------------- /fastlane/screenshots/de-DE/iPad Pro (9.7-inch)-01HomeScreen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chriseidhof/laufpark-stechlin/807aec8af39d13f34813f9ef795d77f76f017e71/fastlane/screenshots/de-DE/iPad Pro (9.7-inch)-01HomeScreen.png -------------------------------------------------------------------------------- /fastlane/screenshots/de-DE/iPad Pro (9.7-inch)-02Selection.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chriseidhof/laufpark-stechlin/807aec8af39d13f34813f9ef795d77f76f017e71/fastlane/screenshots/de-DE/iPad Pro (9.7-inch)-02Selection.png -------------------------------------------------------------------------------- /fastlane/screenshots/de-DE/iPhone 6-01HomeScreen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chriseidhof/laufpark-stechlin/807aec8af39d13f34813f9ef795d77f76f017e71/fastlane/screenshots/de-DE/iPhone 6-01HomeScreen.png -------------------------------------------------------------------------------- /fastlane/screenshots/de-DE/iPhone 6-02Selection.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chriseidhof/laufpark-stechlin/807aec8af39d13f34813f9ef795d77f76f017e71/fastlane/screenshots/de-DE/iPhone 6-02Selection.png -------------------------------------------------------------------------------- /fastlane/screenshots/de-DE/iPhone 8 Plus-01HomeScreen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chriseidhof/laufpark-stechlin/807aec8af39d13f34813f9ef795d77f76f017e71/fastlane/screenshots/de-DE/iPhone 8 Plus-01HomeScreen.png -------------------------------------------------------------------------------- /fastlane/screenshots/de-DE/iPhone 8 Plus-02Selection.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chriseidhof/laufpark-stechlin/807aec8af39d13f34813f9ef795d77f76f017e71/fastlane/screenshots/de-DE/iPhone 8 Plus-02Selection.png -------------------------------------------------------------------------------- /fastlane/screenshots/de-DE/iPhone X-01HomeScreen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chriseidhof/laufpark-stechlin/807aec8af39d13f34813f9ef795d77f76f017e71/fastlane/screenshots/de-DE/iPhone X-01HomeScreen.png -------------------------------------------------------------------------------- /fastlane/screenshots/de-DE/iPhone X-02Selection.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chriseidhof/laufpark-stechlin/807aec8af39d13f34813f9ef795d77f76f017e71/fastlane/screenshots/de-DE/iPhone X-02Selection.png -------------------------------------------------------------------------------- /fastlane/screenshots/en-US/iPad Pro (12.9-inch)-01HomeScreen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chriseidhof/laufpark-stechlin/807aec8af39d13f34813f9ef795d77f76f017e71/fastlane/screenshots/en-US/iPad Pro (12.9-inch)-01HomeScreen.png -------------------------------------------------------------------------------- /fastlane/screenshots/en-US/iPad Pro (12.9-inch)-02Selection.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chriseidhof/laufpark-stechlin/807aec8af39d13f34813f9ef795d77f76f017e71/fastlane/screenshots/en-US/iPad Pro (12.9-inch)-02Selection.png -------------------------------------------------------------------------------- /fastlane/screenshots/en-US/iPad Pro (9.7-inch)-01HomeScreen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chriseidhof/laufpark-stechlin/807aec8af39d13f34813f9ef795d77f76f017e71/fastlane/screenshots/en-US/iPad Pro (9.7-inch)-01HomeScreen.png -------------------------------------------------------------------------------- /fastlane/screenshots/en-US/iPad Pro (9.7-inch)-02Selection.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chriseidhof/laufpark-stechlin/807aec8af39d13f34813f9ef795d77f76f017e71/fastlane/screenshots/en-US/iPad Pro (9.7-inch)-02Selection.png -------------------------------------------------------------------------------- /fastlane/screenshots/en-US/iPhone 6-01HomeScreen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chriseidhof/laufpark-stechlin/807aec8af39d13f34813f9ef795d77f76f017e71/fastlane/screenshots/en-US/iPhone 6-01HomeScreen.png -------------------------------------------------------------------------------- /fastlane/screenshots/en-US/iPhone 6-02Selection.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chriseidhof/laufpark-stechlin/807aec8af39d13f34813f9ef795d77f76f017e71/fastlane/screenshots/en-US/iPhone 6-02Selection.png -------------------------------------------------------------------------------- /fastlane/screenshots/en-US/iPhone 8 Plus-01HomeScreen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chriseidhof/laufpark-stechlin/807aec8af39d13f34813f9ef795d77f76f017e71/fastlane/screenshots/en-US/iPhone 8 Plus-01HomeScreen.png -------------------------------------------------------------------------------- /fastlane/screenshots/en-US/iPhone 8 Plus-02Selection.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chriseidhof/laufpark-stechlin/807aec8af39d13f34813f9ef795d77f76f017e71/fastlane/screenshots/en-US/iPhone 8 Plus-02Selection.png -------------------------------------------------------------------------------- /fastlane/screenshots/en-US/iPhone X-01HomeScreen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chriseidhof/laufpark-stechlin/807aec8af39d13f34813f9ef795d77f76f017e71/fastlane/screenshots/en-US/iPhone X-01HomeScreen.png -------------------------------------------------------------------------------- /fastlane/screenshots/en-US/iPhone X-02Selection.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chriseidhof/laufpark-stechlin/807aec8af39d13f34813f9ef795d77f76f017e71/fastlane/screenshots/en-US/iPhone X-02Selection.png --------------------------------------------------------------------------------