├── .gitignore ├── ActivityTypeClassifierExamples.md ├── BackgroundLocationMonitoring.md ├── CHANGELOG.md ├── LICENSE ├── LocationFilteringExamples.md ├── LocoKit Demo App.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist └── xcshareddata │ └── xcschemes │ └── LocoKit Demo App.xcscheme ├── LocoKit Demo App.xcworkspace ├── contents.xcworkspacedata └── xcshareddata │ └── IDEWorkspaceChecks.plist ├── LocoKit Demo App ├── AppDelegate.swift ├── Array.helpers.swift ├── Assets.xcassets │ ├── AppIcon.appiconset │ │ └── Contents.json │ ├── Contents.json │ ├── dot.imageset │ │ ├── Contents.json │ │ └── dot.png │ └── inactiveDot.imageset │ │ ├── Contents.json │ │ └── dot.png ├── Base.lproj │ └── LaunchScreen.storyboard ├── ClassifierView.swift ├── CoreLocation.helpers.swift ├── DebugLog.swift ├── Info.plist ├── LocoView.swift ├── LogView.swift ├── MapView.swift ├── PathPolyline.swift ├── Settings.swift ├── SettingsView.swift ├── String.helpers.swift ├── TimelineView.swift ├── ToggleBox.swift ├── UIStackView.helpers.swift ├── ViewController.swift ├── VisitAnnotation.swift ├── VisitAnnotationView.swift └── VisitCircle.swift ├── LocoKit.podspec ├── LocoKit ├── Base │ ├── ActivityBrain.swift │ ├── ActivityBrainSample.swift │ ├── ActivityTypeName.swift │ ├── AppGroup.swift │ ├── CMActivityTypeEvent.swift │ ├── CoreMotionActivityTypeName.swift │ ├── CwlMutex.swift │ ├── DeviceMotion.swift │ ├── Helpers │ │ ├── ArrayTools.swift │ │ ├── CLLocationTools.swift │ │ ├── MiscTools.swift │ │ └── StringTools.swift │ ├── Jobs.swift │ ├── KalmanAltitude.swift │ ├── KalmanCoordinates.swift │ ├── KalmanFilter.swift │ ├── LocoKitService.swift │ ├── LocomotionManager.swift │ ├── LocomotionSample.swift │ ├── MovingState.swift │ ├── NotificationNames.swift │ ├── RecordingState.swift │ ├── Strings │ │ ├── Localizable.cat.strings │ │ ├── Localizable.de.strings │ │ ├── Localizable.en.strings │ │ ├── Localizable.es.strings │ │ ├── Localizable.fr.strings │ │ ├── Localizable.ja.strings │ │ └── Localizable.th.strings │ └── TrustAssessor.swift ├── Info.plist ├── LocoKit.h └── Timelines │ ├── ActivityTypes │ ├── ActivityType.scores.swift │ ├── ActivityType.swift │ ├── ActivityTypeClassifiable.swift │ ├── ActivityTypeClassifier.swift │ ├── ActivityTypeTrainable.swift │ ├── ActivityTypesCache.swift │ ├── ClassifierResultItem.swift │ ├── ClassifierResults.swift │ ├── CoordinatesMatrix.swift │ ├── Histogram.swift │ ├── MLClassifier.swift │ ├── MLClassifierManager.swift │ ├── MLCompositeClassifier.swift │ ├── MLModel.swift │ ├── MLModelSource.swift │ └── MutableActivityType.swift │ ├── CLPlacemarkCache.swift │ ├── CoordinateTrust.swift │ ├── CoordinateTrustManager.swift │ ├── ItemsObserver.swift │ ├── Merge.swift │ ├── MergeScores.swift │ ├── TimelineClassifier.swift │ ├── TimelineObjects │ ├── ItemSegment.swift │ ├── Path.swift │ ├── PersistentSample.swift │ ├── RowCopy.swift │ ├── TimelineItem.swift │ ├── TimelineObject.swift │ ├── TimelineSegment.swift │ └── Visit.swift │ ├── TimelineProcessor.swift │ ├── TimelineRecorder.swift │ ├── TimelineStore+Migrations.swift │ └── TimelineStore.swift ├── LocoKitCore.framework.zip ├── LocoKitCore.framework ├── Headers │ ├── LocoKitCore-Swift.h │ └── LocoKitCore.h ├── Info.plist ├── LocoKitCore └── Modules │ ├── LocoKitCore.swiftmodule │ ├── arm.swiftdoc │ ├── arm.swiftinterface │ ├── arm.swiftmodule │ ├── arm64-apple-ios.swiftdoc │ ├── arm64-apple-ios.swiftinterface │ ├── arm64-apple-ios.swiftmodule │ ├── arm64.swiftdoc │ ├── arm64.swiftinterface │ ├── arm64.swiftmodule │ ├── armv7-apple-ios.swiftdoc │ ├── armv7-apple-ios.swiftinterface │ ├── armv7-apple-ios.swiftmodule │ ├── armv7.swiftdoc │ ├── armv7.swiftinterface │ ├── armv7.swiftmodule │ ├── i386-apple-ios-simulator.swiftdoc │ ├── i386-apple-ios-simulator.swiftinterface │ ├── i386-apple-ios-simulator.swiftmodule │ ├── i386.swiftdoc │ ├── i386.swiftinterface │ ├── i386.swiftmodule │ ├── x86_64-apple-ios-simulator.swiftdoc │ ├── x86_64-apple-ios-simulator.swiftinterface │ ├── x86_64-apple-ios-simulator.swiftmodule │ ├── x86_64.swiftdoc │ ├── x86_64.swiftinterface │ └── x86_64.swiftmodule │ └── module.modulemap ├── LocoKitCore.podspec ├── Package.swift ├── Podfile ├── Podfile.lock ├── README.md ├── Screenshots ├── raw_plus_smoothed.png ├── smoothed_only.png ├── smoothed_plus_visits.png ├── stationary.png ├── tuktuk_raw.png ├── tuktuk_smoothed.png ├── tuktuk_smoothed_plus_visits.png └── walking.png ├── TimelineItemDescription.md └── docs └── index.html /.gitignore: -------------------------------------------------------------------------------- 1 | xcuserdata 2 | .idea 3 | Pods 4 | *.xcscmblueprint 5 | metadata 6 | *.ipa 7 | *.dSYM.zip 8 | *.mobileprovision 9 | report.xml 10 | *.log 11 | tags 12 | build 13 | .DS_Store -------------------------------------------------------------------------------- /ActivityTypeClassifierExamples.md: -------------------------------------------------------------------------------- 1 | # Activity Type Classifier Examples 2 | 3 | These screenshots were taken from the ArcKit Demo App. Compile and run the Demo App on device to 4 | experiment with the SDK and see results in your local area. 5 | 6 | ### Simple Stationary and Walking Examples 7 | 8 | These examples are unimpressive placeholders until I get out of the house tomorrow. It shows the 9 | classifiers correctly detecting that I'm stationary at home, then walking around my house. 10 | 11 | | Stationary | Walking | 12 | | ---------- | ------- | 13 | | ![](https://raw.githubusercontent.com/sobri909/ArcKit/master/Screenshots/stationary.png) | ![](https://raw.githubusercontent.com/sobri909/ArcKit/master/Screenshots/walking.png) | 14 | -------------------------------------------------------------------------------- /BackgroundLocationMonitoring.md: -------------------------------------------------------------------------------- 1 | # Background location monitoring 2 | 3 | If you want the app to be relaunched after the user force quits, enable significant location change monitoring. (And I always throw in CLVisit monitoring as well, even though signicant location monitoring makes it redundant.) 4 | 5 | **Note:** You will most likely want the optional `LocoKit/LocalStore` subspec to retain your samples and timeline items in the SQL persistent store. 6 | 7 | There are four general requirements for background location recording: 8 | 9 | 1. Your app has been granted "always" location permission 10 | 2. Your app has "Location updates" toggled on in Xcode's "Background Modes" 11 | 3. You called `startRecording()` while in the foreground 12 | 4. You start monitoring visits and significant location changes 13 | ```swift 14 | loco.locationManager.startMonitoringVisits() 15 | loco.locationManager.startMonitoringSignificantLocationChanges() 16 | ``` 17 | 18 | ## Background task 19 | 20 | If you want the app to continue recording after being launched in the background, start a background task when you `startRecording()`. 21 | 22 | ```swift 23 | var backgroundTask = UIBackgroundTaskInvalid 24 | 25 | func startBackgroundTask() { 26 | guard backgroundTask == UIBackgroundTaskInvalid else { 27 | return 28 | } 29 | 30 | backgroundTask = UIApplication.shared.beginBackgroundTask(withName: "LocoKitBackground") { [weak self] in 31 | self?.endBackgroundTask() 32 | } 33 | } 34 | 35 | func endBackgroundTask() { 36 | guard backgroundTask != UIBackgroundTaskInvalid else { 37 | return 38 | } 39 | UIApplication.shared.endBackgroundTask(backgroundTask) 40 | backgroundTask = UIBackgroundTaskInvalid 41 | } 42 | 43 | ``` 44 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [5.2.0] - 2018-04-29 4 | 5 | ### Added 6 | 7 | - Added an experimental "deep sleep" mode to LocomotionManager (not turned on by default) 8 | - Added some missing database indexes to persistent stores 9 | - Forwarded more LocationManager delegate events 10 | 11 | ### Changed 12 | 13 | - Explicit edit() blocks do an immediate save to the store instead of delayed save 14 | - Made the Reachability dependency optional 15 | - Reduced the updates frequency for desiredAccuracy, to potentially reduce energy use 16 | - Now treating all item segments inside path items as "recording" state 17 | 18 | ### Fixed 19 | 20 | - Avoid edge sample stealing over a sensible time threshold 21 | - Misc cleanups to timeline item classifier results storage 22 | - Improved handling of "data gap" timeline items 23 | - Misc timeline item processing (merging heuristics) improvements 24 | 25 | ## [5.1.1] - 2018-04-03 26 | 27 | - Got rid of the remaining Swift 4.1 warnings 28 | 29 | ## [5.1.0] - 2018-03-30 30 | 31 | - Swift 4.1 support 32 | 33 | ## [5.0.0] - 2018-03-18 34 | 35 | ### Added 36 | 37 | - Added a high level `TimelineManager`, for post processing LocomotionSamples into Visits and 38 | Paths, See the `TimelineManager` API docs for details, and the LocoKit Demo App for code examples. 39 | - Added `PersistentTimelineManager`, an optional persistent SQL store for timeline items. To make 40 | use of the persistent store, add `pod "LocoKit/LocalStore"` to your Podfile and use 41 | `PersistentTimelineManager` instead of `TimelineManager`. 42 | - Added `TimelineClassifier` to make it easier to classify collections of LocomotionSamples. 43 | - Added convenience methods to arrays of LocomotionSamples, for example 44 | `arrayOfSamples.weightedCenter`, `arrayOfSamples.duration`. 45 | 46 | ### Changed 47 | 48 | - Renamed ArcKit to LocoKit, to avoid confusion with Arc App. Note that you now need to set 49 | your API key on `LocoKitService.apiKey` instead of `ArcKitService.apiKey`. All other methods 50 | and classes remain unaffected by the project name change. 51 | - Made various `LocomotionSample` properties (`stepHz`, `courseVariance`, `xyAcceleration`, 52 | `zAcceleration`) optional, to avoid requiring magic numbers when 53 | their source data is unavailable. 54 | 55 | ## [4.0.2] - 2017-11-27 56 | 57 | - Stopped doing unnecessary ArcKitService API requests, and tidied up some console logging 58 | 59 | ## [4.0.1] - 2017-11-27 60 | 61 | ### Fixed 62 | 63 | - Fixed overly aggressive reentry to sleep mode after calling `stopRecording()` then 64 | `startRecording()`. 65 | 66 | ## [4.0.0] - 2017-11-27 67 | 68 | ### Added 69 | 70 | - Added a low power Sleep Mode. Read the `LocomotionManager.useLowPowerSleepModeWhileStationary` API 71 | docs for more details. 72 | - Added ability to disable dynamic desiredAccuracy adjustments. Read the 73 | `LocomotionManager.dynamicallyAdjustDesiredAccuracy` API docs for more details. 74 | - Added LocomotionManager settings for configuring which (if any) Core Motion features to make use of 75 | whilst recording. 76 | 77 | ### Removed 78 | 79 | - `startCoreLocation()` has been renamed to `startRecording()` and now starts both Core Location 80 | and Core Motion recording (depending on your LocomotionManager settings). Additionally, 81 | `stopCoreLocation()` has been renamed to `stopRecording()`, and `startCoreMotion()` and 82 | `stopCoreMotion()` have been removed. 83 | - `recordingCoreLocation` and `recordingCoreMotion` have been removed, and replaced by 84 | `recordingState`. 85 | - The `locomotionSampleUpdated` notification no longer includes a userInfo dict. 86 | 87 | ## [3.0.0] - 2017-11-23 88 | 89 | ### Added 90 | 91 | - Open sourced `LocomotionManager` and `LocomotionSample`. 92 | 93 | ### Changed 94 | 95 | - Moved `apiKey` from `LocomotionManager` to `ArcKitService`. Note that this is a breaking 96 | change - you will need up update your code to set the API key in the new location. 97 | - Split the SDK into two separate frameworks. The `ArcKit` framework now contains only the open 98 | source portions, while the new `ArcKitCore` contains the binary framework. (Over time I will 99 | be open sourcing more code by migrating it from the binary framework to the source framework.) 100 | 101 | ## [2.1.0] - 2017-11-02 102 | 103 | ### Added 104 | 105 | - Supports / requires Xcode 9.1 (pin to `~> 2.0.1` if you require Xcode 9.0 support) 106 | - Added a `locomotionManager.locationManagerDelegate` to allow forwarding of 107 | CLLocationManagerDelegate events from the internal CLLocationManager 108 | - Made public the `classifier.accuracyScore` property 109 | - Added an `isEmpty` property to `ClassifierResults` 110 | 111 | ### Fixed 112 | 113 | - Properly reports ArcKit API request failures to console 114 | 115 | ## [2.0.1] - 2017-10-09 116 | 117 | ### Added 118 | 119 | - Added `isStale` property to classifiers, to know whether it's worth fetching a 120 | replacement classifier yet 121 | - Added `coverageScore` property to classifiers, to give an indication of the usability of the 122 | model data in the classifier's geographic region. (The score is the result of 123 | `completenessScore * accuracyScore`) 124 | 125 | ## [2.0.0] - 2017-09-15 126 | 127 | ### Added 128 | 129 | - New machine learning engine for activity type detection. Includes the same base types 130 | supported by Core Motion, plus also car, train, bus, motorcycle, boat, airplane, where 131 | data is available. 132 | 133 | ### Fixed 134 | 135 | - Misc minor tweaks and improvements to the location data filtering, smoothing, and dynamic 136 | accuracy adjustments 137 | 138 | 139 | ## [1.0.0] - 2017-07-28 140 | 141 | - Initial release 142 | -------------------------------------------------------------------------------- /LocationFilteringExamples.md: -------------------------------------------------------------------------------- 1 | # Location Filtering Examples 2 | 3 | These screenshots were taken from the ArcKit Demo App. Compile and run the Demo App on device to 4 | experiment with the SDK and see results in your local area. 5 | 6 | ### Filtering and Smoothing 7 | 8 | ArcKit uses a two pass system of filtering and smoothing location data. 9 | 10 | The first pass is a [Kalman filter](https://en.wikipedia.org/wiki/Kalman_filter) to remove noise from 11 | the raw locations. The second pass is a dynamically sized, weighted moving average, to turn potentially 12 | erratic paths into smoothed, presentable lines. 13 | 14 | ### Short Walk Between Nearby Buildings 15 | 16 | | Raw (red) + Smoothed (blue) | Smoothed (blue) + Visits (orange) | Smoothed (blue) + Visits (orange) | 17 | | --------------------------- | --------------------------------- | --------------------------------- | 18 | | ![](https://raw.githubusercontent.com/sobri909/ArcKit/master/Screenshots/raw_plus_smoothed.png) | ![](https://raw.githubusercontent.com/sobri909/ArcKit/master/Screenshots/smoothed_plus_visits.png) | ![](https://raw.githubusercontent.com/sobri909/ArcKit/master/Screenshots/smoothed_only.png) | 19 | 20 | The blue segments indicate locations that ArcKit determined to be moving. The orange segments indicate 21 | stationary. Note that locations inside buildings are more likely to classified as stationary, thus 22 | allowing location data to be more easily clustered into "visits". 23 | 24 | ### Tuk-tuk Ride Through Traffic in Built-up City Area 25 | 26 | | Raw Locations | Smoothed (blue) + Stuck (orange) | Smoothed (blue) + Stuck (orange) | 27 | | ------------- | -------------------------------- | -------------------------------- | 28 | | ![](https://raw.githubusercontent.com/sobri909/ArcKit/master/Screenshots/tuktuk_raw.png) | ![](https://raw.githubusercontent.com/sobri909/ArcKit/master/Screenshots/tuktuk_smoothed_plus_visits.png) | ![](https://raw.githubusercontent.com/sobri909/ArcKit/master/Screenshots/tuktuk_smoothed.png) | 29 | 30 | Location accuracy for this trip ranged from 30 to 100 metres, with minimal GPS line of sight and 31 | significant "urban canyon" effects (GPS blocked on both sides by tall buildings and blocked from above by 32 | an elevated rail line). However stationary / moving state detection was still achieved to an accuracy of 33 | 5 to 10 metres. 34 | 35 | **Note:** The orange dots in the second screenshot indicate "stuck in traffic". The third screenshot 36 | shows the "stuck" segments as paths, for easier inspection. 37 | -------------------------------------------------------------------------------- /LocoKit Demo App.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /LocoKit Demo App.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /LocoKit Demo App.xcodeproj/xcshareddata/xcschemes/LocoKit Demo App.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 39 | 40 | 41 | 42 | 43 | 44 | 54 | 56 | 62 | 63 | 64 | 65 | 66 | 67 | 73 | 75 | 81 | 82 | 83 | 84 | 86 | 87 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /LocoKit Demo App.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /LocoKit Demo App.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /LocoKit Demo App/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // LocoKit Demo App 4 | // 5 | // Created by Matt Greenfield on 10/07/17. 6 | // Copyright © 2017 Big Paua. All rights reserved. 7 | // 8 | 9 | import LocoKit 10 | 11 | @UIApplicationMain 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | 14 | var window: UIWindow? 15 | 16 | func application(_ application: UIApplication, 17 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 18 | 19 | window = UIWindow(frame: UIScreen.main.bounds) 20 | 21 | window?.rootViewController = ViewController() 22 | window?.makeKeyAndVisible() 23 | 24 | DebugLog.deleteLogFile() 25 | 26 | return true 27 | } 28 | 29 | func applicationDidEnterBackground(_ application: UIApplication) { 30 | // request "always" location permission 31 | LocomotionManager.highlander.requestLocationPermission(background: true) 32 | } 33 | 34 | func applicationDidBecomeActive(_ application: UIApplication) { 35 | guard let controller = window?.rootViewController as? ViewController else { return } 36 | 37 | // update the UI on appear 38 | controller.updateAllViews() 39 | } 40 | 41 | } 42 | 43 | -------------------------------------------------------------------------------- /LocoKit Demo App/Array.helpers.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Array.helpers.swift 3 | // LocoKit Demo App 4 | // 5 | // Created by Matt Greenfield on 5/09/17. 6 | // Copyright © 2017 Big Paua. All rights reserved. 7 | // 8 | 9 | extension Array where Element: FloatingPoint { 10 | 11 | var sum: Element { 12 | return reduce(0, +) 13 | } 14 | 15 | var mean: Element { 16 | return isEmpty ? 0 : sum / Element(count) 17 | } 18 | 19 | var variance: Element { 20 | let mean = self.mean 21 | let squareDiffs = self.map { value -> Element in 22 | let diff = value - mean 23 | return diff * diff 24 | } 25 | return squareDiffs.mean 26 | } 27 | 28 | var standardDeviation: Element { 29 | return variance.squareRoot() 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /LocoKit Demo App/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 | "idiom" : "iphone", 35 | "size" : "60x60", 36 | "scale" : "2x" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "size" : "60x60", 41 | "scale" : "3x" 42 | }, 43 | { 44 | "idiom" : "ios-marketing", 45 | "size" : "1024x1024", 46 | "scale" : "1x" 47 | } 48 | ], 49 | "info" : { 50 | "version" : 1, 51 | "author" : "xcode" 52 | } 53 | } -------------------------------------------------------------------------------- /LocoKit Demo App/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /LocoKit Demo App/Assets.xcassets/dot.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "filename" : "dot.png", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /LocoKit Demo App/Assets.xcassets/dot.imageset/dot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sobri909/LocoKit/1c3c5f9cb696ce993d26ae180b2eba53f00bae9b/LocoKit Demo App/Assets.xcassets/dot.imageset/dot.png -------------------------------------------------------------------------------- /LocoKit Demo App/Assets.xcassets/inactiveDot.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "filename" : "dot.png", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /LocoKit Demo App/Assets.xcassets/inactiveDot.imageset/dot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sobri909/LocoKit/1c3c5f9cb696ce993d26ae180b2eba53f00bae9b/LocoKit Demo App/Assets.xcassets/inactiveDot.imageset/dot.png -------------------------------------------------------------------------------- /LocoKit Demo App/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 | 27 | 28 | -------------------------------------------------------------------------------- /LocoKit Demo App/ClassifierView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ClassifierView.swift 3 | // LocoKit Demo App 4 | // 5 | // Created by Matt Greenfield on 12/12/17. 6 | // Copyright © 2017 Big Paua. All rights reserved. 7 | // 8 | 9 | import LocoKit 10 | import Anchorage 11 | 12 | class ClassifierView: UIScrollView { 13 | 14 | lazy var rows: UIStackView = { 15 | let box = UIStackView() 16 | box.axis = .vertical 17 | return box 18 | }() 19 | 20 | init() { 21 | super.init(frame: CGRect.zero) 22 | backgroundColor = .white 23 | alwaysBounceVertical = true 24 | update() 25 | } 26 | 27 | required init?(coder aDecoder: NSCoder) { 28 | fatalError("init(coder:) has not been implemented") 29 | } 30 | 31 | override func didMoveToSuperview() { 32 | addSubview(rows) 33 | rows.topAnchor == rows.superview!.topAnchor 34 | rows.bottomAnchor == rows.superview!.bottomAnchor - 8 35 | rows.leftAnchor == rows.superview!.leftAnchor + 16 36 | rows.rightAnchor == rows.superview!.rightAnchor - 16 37 | rows.rightAnchor == superview!.rightAnchor - 16 38 | } 39 | 40 | func update(sample: LocomotionSample? = nil) { 41 | // don't bother updating the UI when we're not in the foreground 42 | guard UIApplication.shared.applicationState == .active else { return } 43 | 44 | // don't bother updating the table if we're not the visible tab 45 | if sample != nil && Settings.visibleTab != self { return } 46 | 47 | rows.arrangedSubviews.forEach { $0.removeFromSuperview() } 48 | 49 | rows.addGap(height: 18) 50 | rows.addSubheading(title: "Sample Classifier Results") 51 | rows.addGap(height: 6) 52 | 53 | let timelineClassifier = TimelineClassifier.highlander 54 | 55 | if let sampleClassifier = timelineClassifier.sampleClassifier { 56 | rows.addRow(leftText: "Region coverageScore", rightText: sampleClassifier.coverageScoreString) 57 | } else { 58 | rows.addRow(leftText: "Region coverageScore", rightText: "-") 59 | } 60 | rows.addGap(height: 6) 61 | 62 | // if we weren't given a sample, then we were only here to build the initial empty table 63 | guard let sample = sample else { return } 64 | 65 | // get the classifier results for the given sample 66 | guard let results = timelineClassifier.classify(sample) else { return } 67 | 68 | for result in results { 69 | let row = rows.addRow(leftText: result.name.rawValue.capitalized, 70 | rightText: String(format: "%.7f", result.score)) 71 | 72 | if result.score < 0.01 { 73 | row.subviews.forEach { subview in 74 | if let label = subview as? UILabel { 75 | label.textColor = UIColor(white: 0.1, alpha: 0.45) 76 | } 77 | } 78 | } 79 | } 80 | } 81 | 82 | } 83 | -------------------------------------------------------------------------------- /LocoKit Demo App/CoreLocation.helpers.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CLLocation.helpers.swift 3 | // LocoKit 4 | // 5 | // Created by Matt Greenfield on 11/07/17. 6 | // Copyright © 2017 Big Paua. All rights reserved. 7 | // 8 | 9 | import CoreLocation 10 | 11 | typealias Radians = Double 12 | 13 | extension CLLocation { 14 | 15 | // find the centre of an array of locations 16 | convenience init?(locations: [CLLocation]) { 17 | guard !locations.isEmpty else { 18 | return nil 19 | } 20 | 21 | if locations.count == 1, let location = locations.first { 22 | self.init(latitude: location.coordinate.latitude, longitude: location.coordinate.longitude) 23 | return 24 | } 25 | 26 | var x: [Double] = [] 27 | var y: [Double] = [] 28 | var z: [Double] = [] 29 | 30 | for location in locations { 31 | let lat = location.coordinate.latitude.radiansValue 32 | let lng = location.coordinate.longitude.radiansValue 33 | 34 | x.append(cos(lat) * cos(lng)) 35 | y.append(cos(lat) * sin(lng)) 36 | z.append(sin(lat)) 37 | } 38 | 39 | let meanx = x.mean 40 | let meany = y.mean 41 | let meanz = z.mean 42 | 43 | let finalLng: Radians = atan2(meany, meanx) 44 | let hyp = (meanx * meanx + meany * meany).squareRoot() 45 | let finalLat: Radians = atan2(meanz, hyp) 46 | 47 | self.init(latitude: finalLat.degreesValue, longitude: finalLng.degreesValue) 48 | } 49 | 50 | } 51 | 52 | extension Array where Element: CLLocation { 53 | 54 | func radiusFrom(center: CLLocation) -> (mean: CLLocationDistance, sd: CLLocationDistance) { 55 | guard count > 1 else { 56 | return (0, 0) 57 | } 58 | 59 | let distances = self.map { $0.distance(from: center) } 60 | 61 | return (distances.mean, distances.standardDeviation) 62 | } 63 | 64 | } 65 | 66 | extension CLLocationDegrees { 67 | var radiansValue: Radians { 68 | return self * Double.pi / 180.0 69 | } 70 | } 71 | 72 | extension Radians { 73 | var degreesValue: CLLocationDegrees { 74 | return self * 180.0 / Double.pi 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /LocoKit Demo App/DebugLog.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Logging.swift 3 | // LocoKit Demo App 4 | // 5 | // Created by Matt Greenfield on 7/12/17. 6 | // Copyright © 2017 Big Paua. All rights reserved. 7 | // 8 | 9 | import os.log 10 | import SwiftNotes 11 | 12 | extension NSNotification.Name { 13 | public static let logFileUpdated = Notification.Name("logFileUpdated") 14 | } 15 | 16 | func log(_ format: String = "", _ values: CVarArg...) { 17 | DebugLog.logToFile(format, values) 18 | } 19 | 20 | class DebugLog { 21 | static let formatter = DateFormatter() 22 | 23 | static func logToFile(_ format: String = "", _ values: CVarArg...) { 24 | let prefix = String(format: "[%@] ", Date().timeLogString) 25 | let logString = String(format: prefix + format, arguments: values) 26 | do { 27 | try logString.appendLineTo(logFile) 28 | } catch { 29 | // don't care 30 | } 31 | print("[LocoKit] " + logString) 32 | trigger(.logFileUpdated) 33 | } 34 | 35 | static var logFile: URL { 36 | let dir = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).last! 37 | return dir.appendingPathComponent("LocoKitDemoApp.log") 38 | } 39 | 40 | static func deleteLogFile() { 41 | do { 42 | try FileManager.default.removeItem(at: logFile) 43 | } catch { 44 | // don't care 45 | } 46 | trigger(.logFileUpdated) 47 | } 48 | } 49 | 50 | extension Date { 51 | var timeLogString: String { 52 | let formatter = DebugLog.formatter 53 | formatter.dateFormat = "HH:mm:ss" 54 | return formatter.string(from: self) 55 | } 56 | } 57 | 58 | extension String { 59 | func appendLineTo(_ url: URL) throws { 60 | try appendingFormat("\n").appendTo(url) 61 | } 62 | 63 | func appendTo(_ url: URL) throws { 64 | let dataObj = data(using: String.Encoding.utf8)! 65 | try dataObj.appendTo(url) 66 | } 67 | } 68 | 69 | extension Data { 70 | func appendTo(_ url: URL) throws { 71 | if let fileHandle = try? FileHandle(forWritingTo: url) { 72 | defer { 73 | fileHandle.closeFile() 74 | } 75 | fileHandle.seekToEndOfFile() 76 | fileHandle.write(self) 77 | } else { 78 | try write(to: url, options: .atomic) 79 | } 80 | } 81 | } 82 | 83 | -------------------------------------------------------------------------------- /LocoKit Demo App/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleDisplayName 8 | LocoKit Demo 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 | 2.0 21 | CFBundleVersion 22 | 8 23 | Fabric 24 | 25 | APIKey 26 | 8eb92dfc2cbb526daa9a955ff461a40f41f354e0 27 | Kits 28 | 29 | 30 | KitInfo 31 | 32 | KitName 33 | Crashlytics 34 | 35 | 36 | 37 | LSRequiresIPhoneOS 38 | 39 | NSLocationAlwaysAndWhenInUseUsageDescription 40 | Location is used to demonstrate LocoKit's functionality 41 | NSLocationAlwaysUsageDescription 42 | Location is used to demonstrate LocoKit's functionality 43 | NSLocationWhenInUseUsageDescription 44 | Location is used to demonstrate LocoKit's functionality 45 | NSMotionUsageDescription 46 | Motion data is used to demonstrate LocoKit's functionality 47 | UIBackgroundModes 48 | 49 | location 50 | 51 | UILaunchStoryboardName 52 | LaunchScreen 53 | UIRequiredDeviceCapabilities 54 | 55 | armv7 56 | 57 | UISupportedInterfaceOrientations 58 | 59 | UIInterfaceOrientationPortrait 60 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /LocoKit Demo App/LocoView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LocoView.swift 3 | // LocoKit Demo App 4 | // 5 | // Created by Matt Greenfield on 12/12/17. 6 | // Copyright © 2017 Big Paua. All rights reserved. 7 | // 8 | 9 | import LocoKit 10 | import Anchorage 11 | import CoreLocation 12 | 13 | class LocoView: UIScrollView { 14 | 15 | lazy var rows: UIStackView = { 16 | let box = UIStackView() 17 | box.axis = .vertical 18 | return box 19 | }() 20 | 21 | init() { 22 | super.init(frame: CGRect.zero) 23 | backgroundColor = .white 24 | alwaysBounceVertical = true 25 | update() 26 | } 27 | 28 | required init?(coder aDecoder: NSCoder) { 29 | fatalError("init(coder:) has not been implemented") 30 | } 31 | 32 | override func didMoveToSuperview() { 33 | addSubview(rows) 34 | rows.topAnchor == rows.superview!.topAnchor 35 | rows.bottomAnchor == rows.superview!.bottomAnchor - 8 36 | rows.leftAnchor == rows.superview!.leftAnchor + 16 37 | rows.rightAnchor == rows.superview!.rightAnchor - 16 38 | rows.rightAnchor == superview!.rightAnchor - 16 39 | } 40 | 41 | func update(sample: LocomotionSample? = nil) { 42 | // don't bother updating the UI when we're not in the foreground 43 | guard UIApplication.shared.applicationState == .active else { return } 44 | 45 | let loco = LocomotionManager.highlander 46 | 47 | if sample != nil && Settings.visibleTab != self { 48 | return 49 | } 50 | 51 | rows.arrangedSubviews.forEach { $0.removeFromSuperview() } 52 | 53 | rows.addGap(height: 18) 54 | rows.addSubheading(title: "Locomotion Manager") 55 | rows.addGap(height: 6) 56 | 57 | rows.addRow(leftText: "Recording state", rightText: loco.recordingState.rawValue) 58 | 59 | if loco.recordingState == .off { 60 | rows.addRow(leftText: "Requesting accuracy", rightText: "-") 61 | 62 | } else { // must be recording or in sleep mode 63 | let requesting = loco.locationManager.desiredAccuracy 64 | if requesting == kCLLocationAccuracyBest { 65 | rows.addRow(leftText: "Requesting accuracy", rightText: "kCLLocationAccuracyBest") 66 | } else if requesting == Double.greatestFiniteMagnitude { 67 | rows.addRow(leftText: "Requesting accuracy", rightText: "Double.greatestFiniteMagnitude") 68 | } else { 69 | rows.addRow(leftText: "Requesting accuracy", rightText: String(format: "%.0f metres", requesting)) 70 | } 71 | } 72 | 73 | var receivingString = "-" 74 | if loco.recordingState == .recording, let sample = sample { 75 | var receivingHertz = 0.0 76 | if let locations = sample.filteredLocations, let duration = locations.dateInterval?.duration, duration > 0 { 77 | receivingHertz = Double(locations.count) / duration 78 | } 79 | 80 | if let location = sample.filteredLocations?.last { 81 | receivingString = String(format: "%.0f metres @ %.1f Hz", location.horizontalAccuracy, receivingHertz) 82 | } 83 | } 84 | rows.addRow(leftText: "Receiving accuracy", rightText: receivingString) 85 | 86 | rows.addGap(height: 14) 87 | rows.addSubheading(title: "Locomotion Sample") 88 | rows.addGap(height: 6) 89 | 90 | if let sample = sample { 91 | rows.addRow(leftText: "Latest sample", rightText: sample.description) 92 | rows.addRow(leftText: "Behind now", rightText: String(duration: sample.date.age)) 93 | rows.addRow(leftText: "Moving state", rightText: sample.movingState.rawValue) 94 | 95 | if loco.recordPedometerEvents, let stepHz = sample.stepHz { 96 | rows.addRow(leftText: "Steps per second", rightText: String(format: "%.1f Hz", stepHz)) 97 | } else { 98 | rows.addRow(leftText: "Steps per second", rightText: "-") 99 | } 100 | 101 | if loco.recordAccelerometerEvents { 102 | if let xyAcceleration = sample.xyAcceleration { 103 | rows.addRow(leftText: "XY Acceleration", rightText: String(format: "%.2f g", xyAcceleration)) 104 | } else { 105 | rows.addRow(leftText: "XY Acceleration", rightText: "-") 106 | } 107 | if let zAcceleration = sample.zAcceleration { 108 | rows.addRow(leftText: "Z Acceleration", rightText: String(format: "%.2f g", zAcceleration)) 109 | } else { 110 | rows.addRow(leftText: "Z Acceleration", rightText: "-") 111 | } 112 | } 113 | 114 | if loco.recordCoreMotionActivityTypeEvents { 115 | if let coreMotionType = sample.coreMotionActivityType { 116 | rows.addRow(leftText: "Core Motion activity", rightText: coreMotionType.rawValue) 117 | } else { 118 | rows.addRow(leftText: "Core Motion activity", rightText: "-") 119 | } 120 | } 121 | 122 | } else { 123 | rows.addRow(leftText: "Latest sample", rightText: "-") 124 | } 125 | } 126 | 127 | } 128 | 129 | -------------------------------------------------------------------------------- /LocoKit Demo App/LogView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LogView.swift 3 | // LocoKit Demo App 4 | // 5 | // Created by Matt Greenfield on 12/12/17. 6 | // Copyright © 2017 Big Paua. All rights reserved. 7 | // 8 | 9 | import LocoKit 10 | import Anchorage 11 | import SwiftNotes 12 | 13 | class LogView: UIScrollView { 14 | 15 | lazy var label: UILabel = { 16 | let label = UILabel() 17 | label.textColor = UIColor.black 18 | label.font = UIFont(name: "Menlo", size: 8) 19 | label.numberOfLines = 0 20 | return label 21 | }() 22 | 23 | init() { 24 | super.init(frame: CGRect.zero) 25 | backgroundColor = .white 26 | alwaysBounceVertical = true 27 | 28 | when(.logFileUpdated) { _ in 29 | onMain { self.update() } 30 | } 31 | 32 | update() 33 | } 34 | 35 | required init?(coder aDecoder: NSCoder) { 36 | fatalError("init(coder:) has not been implemented") 37 | } 38 | 39 | override func didMoveToSuperview() { 40 | addSubview(label) 41 | label.topAnchor == label.superview!.topAnchor + 10 42 | label.bottomAnchor == label.superview!.bottomAnchor - 10 43 | label.leftAnchor == label.superview!.leftAnchor + 8 44 | label.rightAnchor == label.superview!.rightAnchor - 8 45 | label.rightAnchor == superview!.rightAnchor - 8 46 | } 47 | 48 | func update() { 49 | guard UIApplication.shared.applicationState == .active else { return } 50 | 51 | guard let logString = try? String(contentsOf: DebugLog.logFile) else { 52 | label.text = "" 53 | return 54 | } 55 | 56 | label.text = logString 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /LocoKit Demo App/MapView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MapView.swift 3 | // LocoKit Demo App 4 | // 5 | // Created by Matt Greenfield on 12/12/17. 6 | // Copyright © 2017 Big Paua. All rights reserved. 7 | // 8 | 9 | import LocoKit 10 | import MapKit 11 | 12 | class MapView: MKMapView { 13 | 14 | init() { 15 | super.init(frame: CGRect.zero) 16 | self.delegate = self 17 | self.isRotateEnabled = false 18 | self.isPitchEnabled = false 19 | self.showsScale = true 20 | } 21 | 22 | required init?(coder aDecoder: NSCoder) { 23 | fatalError("init(coder:) has not been implemented") 24 | } 25 | 26 | func update(with items: [TimelineItem]) { 27 | // don't bother updating the map when we're not in the foreground 28 | guard UIApplication.shared.applicationState == .active else { return } 29 | 30 | let loco = LocomotionManager.highlander 31 | 32 | removeOverlays(overlays) 33 | removeAnnotations(annotations) 34 | 35 | showsUserLocation = Settings.showUserLocation && (loco.recordingState == .recording || loco.recordingState == .wakeup) 36 | 37 | let newMapType: MKMapType = Settings.showSatelliteMap ? .hybrid : .standard 38 | if mapType != newMapType { 39 | self.mapType = newMapType 40 | } 41 | 42 | if Settings.showTimelineItems { 43 | for timelineItem in items { 44 | if let path = timelineItem as? Path { 45 | add(path) 46 | 47 | } else if let visit = timelineItem as? Visit { 48 | add(visit) 49 | } 50 | } 51 | 52 | } else { 53 | var samples: [LocomotionSample] = [] 54 | 55 | // do these as sets, because need to deduplicate 56 | var rawLocations: Set = [] 57 | var filteredLocations: Set = [] 58 | 59 | // collect samples and locations from the timeline items 60 | for timelineItem in items.reversed() { 61 | for sample in timelineItem.samples { 62 | samples.append(sample) 63 | if let locations = sample.rawLocations { 64 | rawLocations = rawLocations.union(locations) 65 | } 66 | if let locations = sample.filteredLocations { 67 | filteredLocations = filteredLocations.union(locations) 68 | } 69 | } 70 | } 71 | 72 | if Settings.showRawLocations { 73 | add(rawLocations.sorted { $0.timestamp < $1.timestamp }, color: .red) 74 | } 75 | 76 | if Settings.showFilteredLocations { 77 | add(filteredLocations.sorted { $0.timestamp < $1.timestamp }, color: .purple) 78 | } 79 | 80 | if Settings.showLocomotionSamples { 81 | let groups = sampleGroups(from: samples) 82 | for group in groups { 83 | add(group) 84 | } 85 | } 86 | } 87 | 88 | if Settings.autoZoomMap { 89 | zoomToShow(overlays: overlays) 90 | } 91 | } 92 | 93 | func sampleGroups(from samples: [LocomotionSample]) -> [[LocomotionSample]] { 94 | var groups: [[LocomotionSample]] = [] 95 | var currentGroup: [LocomotionSample]? 96 | 97 | for sample in samples where sample.location != nil { 98 | let currentState = sample.movingState 99 | 100 | // state changed? close off the previous group, add to the collection, and start a new one 101 | if let previousState = currentGroup?.last?.movingState, previousState != currentState { 102 | 103 | // add new sample to previous grouping, to link them end to end 104 | currentGroup?.append(sample) 105 | 106 | // add it to the collection 107 | groups.append(currentGroup!) 108 | 109 | currentGroup = nil 110 | } 111 | 112 | currentGroup = currentGroup ?? [] 113 | currentGroup?.append(sample) 114 | } 115 | 116 | // add the final grouping to the collection 117 | if let grouping = currentGroup { 118 | groups.append(grouping) 119 | } 120 | 121 | return groups 122 | } 123 | 124 | func add(_ locations: [CLLocation], color: UIColor) { 125 | guard !locations.isEmpty else { 126 | return 127 | } 128 | 129 | var coords = locations.compactMap { $0.coordinate } 130 | let path = PathPolyline(coordinates: &coords, count: coords.count) 131 | path.color = color 132 | 133 | addOverlay(path) 134 | } 135 | 136 | func add(_ samples: [LocomotionSample]) { 137 | guard let movingState = samples.first?.movingState else { 138 | return 139 | } 140 | 141 | let locations = samples.compactMap { $0.location } 142 | 143 | switch movingState { 144 | case .moving: 145 | add(locations, color: .blue) 146 | 147 | case .stationary: 148 | add(locations, color: .orange) 149 | 150 | case .uncertain: 151 | add(locations, color: .magenta) 152 | } 153 | } 154 | 155 | func add(_ path: Path) { 156 | if path.samples.isEmpty { return } 157 | 158 | var coords = path.samples.compactMap { $0.location?.coordinate } 159 | let line = PathPolyline(coordinates: &coords, count: coords.count) 160 | line.color = .brown 161 | 162 | addOverlay(line) 163 | } 164 | 165 | func add(_ visit: Visit) { 166 | guard let center = visit.center else { return } 167 | 168 | addAnnotation(VisitAnnotation(coordinate: center.coordinate, visit: visit)) 169 | 170 | let circle = VisitCircle(center: center.coordinate, radius: visit.radius2sd) 171 | circle.color = .orange 172 | addOverlay(circle, level: .aboveLabels) 173 | } 174 | 175 | 176 | func zoomToShow(overlays: [MKOverlay]) { 177 | guard !overlays.isEmpty else { return } 178 | 179 | var mapRect: MKMapRect? 180 | for overlay in overlays { 181 | if mapRect == nil { 182 | mapRect = overlay.boundingMapRect 183 | } else { 184 | mapRect = mapRect!.union(overlay.boundingMapRect) 185 | } 186 | } 187 | 188 | let padding = UIEdgeInsets(top: 20, left: 20, bottom: 20, right: 20) 189 | 190 | setVisibleMapRect(mapRect!, edgePadding: padding, animated: true) 191 | } 192 | } 193 | 194 | extension MapView: MKMapViewDelegate { 195 | 196 | func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer { 197 | if let path = overlay as? PathPolyline { return path.renderer } 198 | if let circle = overlay as? VisitCircle { return circle.renderer } 199 | fatalError("you wot?") 200 | } 201 | 202 | func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? { 203 | return (annotation as? VisitAnnotation)?.view 204 | } 205 | 206 | } 207 | -------------------------------------------------------------------------------- /LocoKit Demo App/PathPolyline.swift: -------------------------------------------------------------------------------- 1 | // Created by Matt Greenfield on 16/11/15. 2 | // Copyright (c) 2015 Big Paua. All rights reserved. 3 | 4 | import MapKit 5 | 6 | class PathPolyline: MKPolyline { 7 | 8 | var color: UIColor? 9 | 10 | var renderer: MKPolylineRenderer { 11 | let renderer = MKPolylineRenderer(polyline: self) 12 | renderer.strokeColor = color 13 | renderer.lineWidth = 3 14 | return renderer 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /LocoKit Demo App/Settings.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Settings.swift 3 | // LocoKit Demo App 4 | // 5 | // Created by Matt Greenfield on 12/12/17. 6 | // Copyright © 2017 Big Paua. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class Settings { 12 | 13 | static var showTimelineItems = true 14 | 15 | static var showRawLocations = true 16 | static var showFilteredLocations = true 17 | static var showLocomotionSamples = true 18 | 19 | static var showSatelliteMap = false 20 | static var showUserLocation = true 21 | static var autoZoomMap = true 22 | 23 | static var showDebugTimelineDetails = false 24 | 25 | static var visibleTab: UIView? 26 | 27 | } 28 | -------------------------------------------------------------------------------- /LocoKit Demo App/SettingsView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SettingsView.swift 3 | // LocoKit Demo App 4 | // 5 | // Created by Matt Greenfield on 9/10/17. 6 | // Copyright © 2017 Big Paua. All rights reserved. 7 | // 8 | 9 | import SwiftNotes 10 | import Anchorage 11 | 12 | extension NSNotification.Name { 13 | public static let settingsChanged = Notification.Name("settingsChanged") 14 | } 15 | 16 | class SettingsView: UIScrollView { 17 | 18 | var locoDataToggleBoxes: [ToggleBox] = [] 19 | 20 | lazy var rows: UIStackView = { 21 | let box = UIStackView() 22 | box.axis = .vertical 23 | return box 24 | }() 25 | 26 | // MARK: - 27 | 28 | init() { 29 | super.init(frame: CGRect.zero) 30 | backgroundColor = .white 31 | alwaysBounceVertical = true 32 | buildViewTree() 33 | } 34 | 35 | required init?(coder aDecoder: NSCoder) { 36 | fatalError("init(coder:) has not been implemented") 37 | } 38 | 39 | override func didMoveToSuperview() { 40 | addSubview(rows) 41 | rows.topAnchor == rows.superview!.topAnchor 42 | rows.bottomAnchor == rows.superview!.bottomAnchor 43 | rows.leftAnchor == rows.superview!.leftAnchor + 8 44 | rows.rightAnchor == rows.superview!.rightAnchor - 8 45 | rows.rightAnchor == superview!.rightAnchor - 8 46 | } 47 | 48 | // MARK: - 49 | 50 | func buildViewTree() { 51 | rows.addGap(height: 24) 52 | rows.addSubheading(title: "Map Style", alignment: .center) 53 | rows.addGap(height: 6) 54 | rows.addUnderline() 55 | 56 | let currentLocation = ToggleBox(text: "Enable showsUserLocation", toggleDefault: Settings.showUserLocation) { isOn in 57 | Settings.showUserLocation = isOn 58 | trigger(.settingsChanged, on: self) 59 | } 60 | rows.addRow(views: [currentLocation]) 61 | 62 | rows.addUnderline() 63 | 64 | let satellite = ToggleBox(text: "Satellite map", toggleDefault: Settings.showSatelliteMap) { isOn in 65 | Settings.showSatelliteMap = isOn 66 | trigger(.settingsChanged, on: self) 67 | } 68 | let zoom = ToggleBox(text: "Auto zoom") { isOn in 69 | Settings.autoZoomMap = isOn 70 | trigger(.settingsChanged, on: self) 71 | } 72 | rows.addRow(views: [satellite, zoom]) 73 | 74 | rows.addGap(height: 18) 75 | rows.addSubheading(title: "Map Data", alignment: .center) 76 | rows.addGap(height: 6) 77 | rows.addUnderline() 78 | 79 | // toggle for showing timeline items 80 | let visits = ToggleBox(dotColors: [.brown, .orange], text: "Timeline", toggleDefault: Settings.showTimelineItems) { isOn in 81 | Settings.showTimelineItems = isOn 82 | self.locoDataToggleBoxes.forEach { $0.disabled = isOn } 83 | trigger(.settingsChanged, on: self) 84 | } 85 | 86 | // toggle for showing filtered locations 87 | let filtered = ToggleBox(dotColors: [.purple], text: "Filtered", toggleDefault: Settings.showFilteredLocations) { isOn in 88 | Settings.showFilteredLocations = isOn 89 | trigger(.settingsChanged, on: self) 90 | } 91 | filtered.disabled = Settings.showTimelineItems 92 | locoDataToggleBoxes.append(filtered) 93 | 94 | // add the toggles to the view 95 | rows.addRow(views: [visits, filtered]) 96 | 97 | rows.addUnderline() 98 | 99 | // toggle for showing locomotion samples 100 | let samples = ToggleBox(dotColors: [.blue, .magenta], text: "Samples", toggleDefault: Settings.showLocomotionSamples) { isOn in 101 | Settings.showLocomotionSamples = isOn 102 | trigger(.settingsChanged, on: self) 103 | } 104 | samples.disabled = Settings.showTimelineItems 105 | locoDataToggleBoxes.append(samples) 106 | 107 | // toggle for showing raw locations 108 | let raw = ToggleBox(dotColors: [.red], text: "Raw", toggleDefault: Settings.showRawLocations) { isOn in 109 | Settings.showRawLocations = isOn 110 | trigger(.settingsChanged, on: self) 111 | } 112 | raw.disabled = Settings.showTimelineItems 113 | locoDataToggleBoxes.append(raw) 114 | 115 | // add the toggles to the view 116 | rows.addRow(views: [samples, raw]) 117 | 118 | rows.addGap(height: 18) 119 | } 120 | 121 | } 122 | -------------------------------------------------------------------------------- /LocoKit Demo App/String.helpers.swift: -------------------------------------------------------------------------------- 1 | // 2 | // String.helpers.swift 3 | // LocoKit Demo App 4 | // 5 | // Created by Matt Greenfield on 5/08/17. 6 | // Copyright © 2017 Big Paua. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | extension String { 12 | 13 | init(duration: TimeInterval, style: DateComponentsFormatter.UnitsStyle = .full, maximumUnits: Int = 2) { 14 | let formatter = DateComponentsFormatter() 15 | formatter.maximumUnitCount = maximumUnits 16 | formatter.unitsStyle = style 17 | 18 | if duration < 60 { 19 | formatter.allowedUnits = [.second, .minute, .hour, .day, .month] 20 | } else { 21 | formatter.allowedUnits = [.minute, .hour, .day, .month] 22 | } 23 | 24 | self.init(format: formatter.string(from: duration)!) 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /LocoKit Demo App/TimelineView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TimelineView.swift 3 | // LocoKit Demo App 4 | // 5 | // Created by Matt Greenfield on 12/12/17. 6 | // Copyright © 2017 Big Paua. All rights reserved. 7 | // 8 | 9 | import LocoKit 10 | import Anchorage 11 | 12 | class TimelineView: UIScrollView { 13 | 14 | lazy var rows: UIStackView = { 15 | let box = UIStackView() 16 | box.axis = .vertical 17 | return box 18 | }() 19 | 20 | init() { 21 | super.init(frame: CGRect.zero) 22 | backgroundColor = .white 23 | alwaysBounceVertical = true 24 | } 25 | 26 | required init?(coder aDecoder: NSCoder) { 27 | fatalError("init(coder:) has not been implemented") 28 | } 29 | 30 | override func didMoveToSuperview() { 31 | addSubview(rows) 32 | rows.topAnchor == rows.superview!.topAnchor 33 | rows.bottomAnchor == rows.superview!.bottomAnchor - 16 34 | rows.leftAnchor == rows.superview!.leftAnchor + 16 35 | rows.rightAnchor == rows.superview!.rightAnchor - 16 36 | rows.rightAnchor == superview!.rightAnchor - 16 37 | } 38 | 39 | func update(with items: [TimelineItem]) { 40 | // don't bother updating the UI when we're not in the foreground 41 | guard UIApplication.shared.applicationState == .active else { return } 42 | 43 | rows.arrangedSubviews.forEach { $0.removeFromSuperview() } 44 | 45 | rows.addGap(height: 18) 46 | rows.addHeading(title: "Timeline Items") 47 | rows.addGap(height: 2) 48 | 49 | if items.isEmpty { 50 | rows.addRow(leftText: "-") 51 | return 52 | } 53 | 54 | for timelineItem in items { 55 | if timelineItem.isDataGap { 56 | addDataGap(timelineItem) 57 | } else { 58 | add(timelineItem) 59 | } 60 | } 61 | } 62 | 63 | func add(_ timelineItem: TimelineItem) { 64 | rows.addGap(height: 14) 65 | var title = "" 66 | if let start = timelineItem.startDate { 67 | title += "[\(dateFormatter.string(from: start))] " 68 | } 69 | if timelineItem.isCurrentItem { 70 | title += "Current " 71 | } 72 | title += timelineItem.isNolo ? "Nolo" : timelineItem is Visit ? "Visit" : "Path" 73 | if let path = timelineItem as? Path, let activityType = path.movingActivityType { 74 | title += " (\(activityType)" 75 | if Settings.showDebugTimelineDetails { 76 | if let modeType = path.modeMovingActivityType { 77 | title += ", mode: \(modeType)" 78 | } 79 | } 80 | title += ")" 81 | } 82 | rows.addSubheading(title: title) 83 | rows.addGap(height: 6) 84 | 85 | if timelineItem.hasBrokenEdges { 86 | if timelineItem.nextItem == nil && !timelineItem.isCurrentItem { 87 | rows.addRow(leftText: "nextItem is nil", color: .red) 88 | } 89 | if timelineItem.previousItem == nil { 90 | rows.addRow(leftText: "previousItem is nil", color: .red) 91 | } 92 | } 93 | 94 | rows.addRow(leftText: "Duration", rightText: String(duration: timelineItem.duration)) 95 | 96 | if let path = timelineItem as? Path { 97 | rows.addRow(leftText: "Distance", rightText: String(metres: path.distance)) 98 | rows.addRow(leftText: "Speed", rightText: String(metresPerSecond: path.metresPerSecond)) 99 | } 100 | 101 | if let visit = timelineItem as? Visit { 102 | rows.addRow(leftText: "Radius", rightText: String(metres: visit.radius2sd)) 103 | } 104 | 105 | let keeperString = timelineItem.isInvalid ? "invalid" : timelineItem.isWorthKeeping ? "keeper" : "valid" 106 | rows.addRow(leftText: "Keeper status", rightText: keeperString) 107 | 108 | // the rest of the rows are debug bits, mostly for my benefit only 109 | guard Settings.showDebugTimelineDetails else { 110 | return 111 | } 112 | 113 | let debugColor = UIColor(white: 0.94, alpha: 1) 114 | 115 | if let previous = timelineItem.previousItem, !timelineItem.withinMergeableDistance(from: previous) { 116 | if 117 | let timeGap = timelineItem.timeInterval(from: previous), 118 | let distGap = timelineItem.distance(from: previous) 119 | { 120 | rows.addRow(leftText: "Unmergeable gap from previous", 121 | rightText: "\(String(duration: timeGap)) (\(String(metres: distGap)))", 122 | background: debugColor) 123 | } else { 124 | rows.addRow(leftText: "Unmergeable gap from previous", rightText: "unknown gap size", 125 | background: debugColor) 126 | } 127 | let maxMerge = timelineItem.maximumMergeableDistance(from: previous) 128 | rows.addRow(leftText: "Max mergeable gap", rightText: "\(String(metres: maxMerge))", background: debugColor) 129 | } 130 | 131 | rows.addRow(leftText: "Samples", rightText: "\(timelineItem.samples.count)", background: debugColor) 132 | 133 | rows.addRow(leftText: "ItemId", rightText: timelineItem.itemId.uuidString, background: debugColor) 134 | } 135 | 136 | func addDataGap(_ timelineItem: TimelineItem) { 137 | guard timelineItem.isDataGap else { return } 138 | 139 | rows.addGap(height: 14) 140 | rows.addUnderline() 141 | rows.addGap(height: 14) 142 | 143 | rows.addSubheading(title: "Timeline Gap (\(String(duration: timelineItem.duration)))", color: .red) 144 | 145 | rows.addGap(height: 14) 146 | rows.addUnderline() 147 | } 148 | 149 | lazy var dateFormatter: DateFormatter = { 150 | let formatter = DateFormatter() 151 | formatter.dateFormat = "HH:mm" 152 | return formatter 153 | }() 154 | } 155 | -------------------------------------------------------------------------------- /LocoKit Demo App/ToggleBox.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ToggleBox.swift 3 | // LocoKit Demo App 4 | // 5 | // Created by Matt Greenfield on 5/08/17. 6 | // Copyright © 2017 Big Paua. All rights reserved. 7 | // 8 | 9 | import Anchorage 10 | 11 | class ToggleBox: UIView { 12 | 13 | let toggle = UISwitch() 14 | var onChange: (Bool) -> Void 15 | 16 | var disabled: Bool { 17 | get { 18 | return toggle.alpha < 1 19 | } 20 | set(disable) { 21 | toggle.isEnabled = !disable 22 | subviews.forEach { $0.alpha = disable ? 0.45 : 1 } 23 | } 24 | } 25 | 26 | init(dotColors: [UIColor] = [], text: String, toggleDefault: Bool = true, onChange: @escaping ((Bool) -> Void)) { 27 | self.onChange = onChange 28 | 29 | super.init(frame: CGRect.zero) 30 | 31 | backgroundColor = .white 32 | 33 | var lastDot: UIView? 34 | for color in dotColors { 35 | let dot = self.dot(color: color) 36 | let dotWidth = dot.frame.size.width 37 | addSubview(dot) 38 | 39 | dot.centerYAnchor == dot.superview!.centerYAnchor 40 | dot.heightAnchor == dotWidth 41 | dot.widthAnchor == dotWidth 42 | 43 | if let lastDot = lastDot { 44 | dot.leftAnchor == lastDot.rightAnchor - 4 45 | } else { 46 | dot.leftAnchor == dot.superview!.leftAnchor + 8 47 | } 48 | 49 | lastDot = dot 50 | } 51 | 52 | let label = UILabel() 53 | label.text = text 54 | label.font = UIFont.preferredFont(forTextStyle: .body) 55 | label.textColor = UIColor(white: 0.1, alpha: 1) 56 | 57 | toggle.isOn = toggleDefault 58 | toggle.addTarget(self, action: #selector(ToggleBox.triggerOnChange), for: .valueChanged) 59 | 60 | addSubview(label) 61 | addSubview(toggle) 62 | 63 | if let lastDot = lastDot { 64 | label.leftAnchor == lastDot.rightAnchor + 5 65 | 66 | } else { 67 | label.leftAnchor == label.superview!.leftAnchor + 9 68 | } 69 | 70 | label.topAnchor == label.superview!.topAnchor 71 | label.bottomAnchor == label.superview!.bottomAnchor 72 | label.heightAnchor == 44 73 | 74 | toggle.centerYAnchor == toggle.superview!.centerYAnchor 75 | toggle.rightAnchor == toggle.superview!.rightAnchor - 10 76 | toggle.leftAnchor == label.rightAnchor 77 | } 78 | 79 | @objc func triggerOnChange() { 80 | onChange(toggle.isOn) 81 | } 82 | 83 | required init?(coder aDecoder: NSCoder) { 84 | fatalError("init(coder:) has not been implemented") 85 | } 86 | 87 | func dot(color: UIColor) -> UIView { 88 | let dot = UIView(frame: CGRect(x: 0, y: 0, width: 14, height: 14)) 89 | 90 | let shape = CAShapeLayer() 91 | shape.fillColor = color.cgColor 92 | shape.path = UIBezierPath(roundedRect: dot.bounds, cornerRadius: 7).cgPath 93 | shape.strokeColor = UIColor.white.cgColor 94 | shape.lineWidth = 2 95 | dot.layer.addSublayer(shape) 96 | 97 | return dot 98 | } 99 | } 100 | 101 | -------------------------------------------------------------------------------- /LocoKit Demo App/UIStackView.helpers.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIStackView.helpers.swift 3 | // LocoKit Demo App 4 | // 5 | // Created by Matt Greenfield on 5/08/17. 6 | // Copyright © 2017 Big Paua. All rights reserved. 7 | // 8 | 9 | import Anchorage 10 | 11 | extension UIStackView { 12 | 13 | func addUnderline() { 14 | let underline = UIView() 15 | underline.backgroundColor = UIColor(white: 0.85, alpha: 1) 16 | addArrangedSubview(underline) 17 | underline.heightAnchor == 1.0 / UIScreen.main.scale 18 | } 19 | 20 | func addGap(height: CGFloat) { 21 | let gap = UIView() 22 | gap.backgroundColor = .white 23 | addArrangedSubview(gap) 24 | gap.heightAnchor == height 25 | } 26 | 27 | func addHeading(title: String, alignment: NSTextAlignment = .left) { 28 | let header = UILabel() 29 | header.backgroundColor = .white 30 | header.font = UIFont.preferredFont(forTextStyle: .headline) 31 | header.textAlignment = alignment 32 | header.text = title 33 | addArrangedSubview(header) 34 | } 35 | 36 | func addSubheading(title: String, alignment: NSTextAlignment = .left, color: UIColor = .black) { 37 | let header = UILabel() 38 | header.backgroundColor = .white 39 | header.font = UIFont.preferredFont(forTextStyle: .subheadline) 40 | header.textAlignment = alignment 41 | header.textColor = color 42 | header.text = title 43 | addArrangedSubview(header) 44 | } 45 | 46 | func addRow(views: [UIView]) { 47 | let row = UIStackView() 48 | row.distribution = .fillEqually 49 | row.spacing = 0.5 50 | views.forEach { row.addArrangedSubview($0) } 51 | addArrangedSubview(row) 52 | } 53 | 54 | @discardableResult func addRow(leftText: String? = nil, rightText: String? = nil, 55 | color: UIColor = UIColor(white: 0.1, alpha: 1), 56 | background: UIColor = .white) -> UIStackView { 57 | let leftLabel = UILabel() 58 | leftLabel.text = leftText 59 | leftLabel.font = UIFont.preferredFont(forTextStyle: .caption1) 60 | leftLabel.textColor = color 61 | leftLabel.backgroundColor = background 62 | 63 | let rightLabel = UILabel() 64 | rightLabel.text = rightText 65 | rightLabel.textAlignment = .right 66 | rightLabel.font = UIFont.preferredFont(forTextStyle: .caption1) 67 | rightLabel.textColor = color 68 | rightLabel.backgroundColor = background 69 | 70 | let leftPad = UIView() 71 | leftPad.backgroundColor = background 72 | 73 | let rightPad = UIView() 74 | rightPad.backgroundColor = background 75 | 76 | let row = UIStackView() 77 | row.addArrangedSubview(leftPad) 78 | row.addArrangedSubview(leftLabel) 79 | row.addArrangedSubview(rightLabel) 80 | row.addArrangedSubview(rightPad) 81 | addArrangedSubview(row) 82 | 83 | leftPad.widthAnchor == 8 84 | rightPad.widthAnchor == 8 85 | row.heightAnchor == 20 86 | 87 | return row 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /LocoKit Demo App/VisitAnnotation.swift: -------------------------------------------------------------------------------- 1 | // Created by Matt Greenfield on 21/01/16. 2 | // Copyright (c) 2016 Big Paua. All rights reserved. 3 | 4 | import MapKit 5 | import LocoKit 6 | 7 | class VisitAnnotation: NSObject, MKAnnotation { 8 | 9 | var coordinate: CLLocationCoordinate2D 10 | var visit: Visit 11 | 12 | init(coordinate: CLLocationCoordinate2D, visit: Visit) { 13 | self.coordinate = coordinate 14 | self.visit = visit 15 | super.init() 16 | } 17 | 18 | var view: VisitAnnotationView { 19 | return VisitAnnotationView(annotation: self, reuseIdentifier: nil) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /LocoKit Demo App/VisitAnnotationView.swift: -------------------------------------------------------------------------------- 1 | // Created by Matt Greenfield on 4/10/16. 2 | // Copyright © 2016 Big Paua. All rights reserved. 3 | 4 | import MapKit 5 | 6 | class VisitAnnotationView: MKAnnotationView { 7 | 8 | override init(annotation: MKAnnotation?, reuseIdentifier: String?) { 9 | super.init(annotation: annotation, reuseIdentifier: reuseIdentifier) 10 | image = UIImage(named: "dot") 11 | } 12 | 13 | required init?(coder aDecoder: NSCoder) { 14 | fatalError("init(coder:) has not been implemented") 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /LocoKit Demo App/VisitCircle.swift: -------------------------------------------------------------------------------- 1 | // Created by Matt Greenfield on 9/11/15. 2 | // Copyright (c) 2015 Big Paua. All rights reserved. 3 | 4 | import MapKit 5 | 6 | class VisitCircle: MKCircle { 7 | 8 | var color: UIColor? 9 | 10 | var renderer: MKCircleRenderer { 11 | let renderer = MKCircleRenderer(circle: self) 12 | renderer.fillColor = color?.withAlphaComponent(0.2) 13 | renderer.strokeColor = nil 14 | renderer.lineWidth = 0 15 | return renderer 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /LocoKit.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | s.name = "LocoKit" 3 | s.version = "7.1.0" 4 | s.summary = "Location and activity recording framework" 5 | s.homepage = "https://www.bigpaua.com/locokit/" 6 | s.author = { "Matt Greenfield" => "matt@bigpaua.com" } 7 | s.license = { :text => "Copyright 2018 Matt Greenfield. All rights reserved.", 8 | :type => "Commercial" } 9 | 10 | s.source = { :git => 'https://github.com/sobri909/LocoKit.git', :tag => '7.1.0' } 11 | s.frameworks = 'CoreLocation', 'CoreMotion' 12 | s.swift_version = '5.0' 13 | s.ios.deployment_target = '13.0' 14 | s.default_subspec = 'Base' 15 | 16 | s.subspec 'Base' do |sp| 17 | sp.source_files = 'LocoKit/Base/**/*', 'LocoKit/Timelines/**/*' 18 | sp.dependency 'Upsurge', '~> 0.10' 19 | sp.dependency 'GRDB.swift', '~> 4' 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /LocoKit/Base/ActivityTypeName.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ActivityTypeName.swift 3 | // LocoKit 4 | // 5 | // Created by Matt Greenfield on 12/10/17. 6 | // Copyright © 2017 Big Paua. All rights reserved. 7 | // 8 | 9 | /** 10 | The possible Activity Types for a Locomotion Sample. Use an `ActivityTypeClassifier` to determine the type of a 11 | `LocomotionSample`. 12 | 13 | - Note: The stationary type may indicate that the device is lying on a stationary surface such as a table, or that 14 | the device is in the user's hand or pocket but the user is otherwise stationary. 15 | */ 16 | public enum ActivityTypeName: String, Codable { 17 | 18 | // special types 19 | case unknown 20 | case bogus 21 | 22 | // base types 23 | case stationary 24 | case walking 25 | case running 26 | case cycling 27 | case car 28 | case airplane 29 | 30 | // transport types 31 | case train 32 | case bus 33 | case motorcycle 34 | case boat 35 | case tram 36 | case tractor 37 | case tuktuk 38 | case songthaew 39 | case scooter 40 | case metro 41 | case cableCar 42 | case funicular 43 | case chairlift 44 | case skiLift 45 | case taxi 46 | 47 | // active types 48 | case skateboarding 49 | case inlineSkating 50 | case snowboarding 51 | case skiing 52 | case horseback 53 | case swimming 54 | case golf 55 | case wheelchair 56 | case rowing 57 | case kayaking 58 | 59 | public var displayName: String { 60 | switch self { 61 | case .tuktuk: 62 | return "tuk-tuk" 63 | case .inlineSkating: 64 | return "inline skating" 65 | case .cableCar: 66 | return "cable car" 67 | case .skiLift: 68 | return "ski lift" 69 | default: 70 | return rawValue 71 | } 72 | } 73 | 74 | // MARK: - Convenience Arrays 75 | 76 | /// A convenience array containing the base activity types. 77 | public static let baseTypes = [stationary, walking, running, cycling, car, airplane] 78 | 79 | /// A convenience array containing the extended transport types. 80 | public static let extendedTypes = [ 81 | train, bus, motorcycle, boat, tram, tractor, tuktuk, songthaew, skateboarding, inlineSkating, snowboarding, skiing, horseback, 82 | scooter, metro, cableCar, funicular, chairlift, skiLift, taxi, swimming, golf, wheelchair, rowing, kayaking, bogus 83 | ] 84 | 85 | /// A convenience array containing all activity types. 86 | public static let allTypes = baseTypes + extendedTypes 87 | 88 | /// Activity types that can sensibly have related step counts 89 | public static let stepsTypes = [walking, running, cycling, golf, rowing, kayaking] 90 | 91 | } 92 | -------------------------------------------------------------------------------- /LocoKit/Base/CMActivityTypeEvent.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Matt Greenfield on 13/11/15. 3 | // Copyright (c) 2015 Big Paua. All rights reserved. 4 | // 5 | 6 | import CoreMotion 7 | 8 | internal struct CMActivityTypeEvent: Equatable { 9 | 10 | static let decayRate: TimeInterval = 30 // the time it takes for 1.0 confidence to reach 0.0 11 | 12 | var name: CoreMotionActivityTypeName 13 | var date = Date() 14 | var initialConfidence: CMMotionActivityConfidence 15 | 16 | init(name: CoreMotionActivityTypeName, confidence: CMMotionActivityConfidence, date: Date) { 17 | self.name = name 18 | self.date = date 19 | self.initialConfidence = confidence 20 | } 21 | 22 | var age: TimeInterval { 23 | return -date.timeIntervalSinceNow 24 | } 25 | 26 | var currentConfidence: Double { 27 | let decay = age / CMActivityTypeEvent.decayRate 28 | let currentConfidence = initialConfidenceDoubleValue - decay 29 | 30 | if currentConfidence > 0 { 31 | return currentConfidence 32 | 33 | } else { 34 | return 0 35 | } 36 | } 37 | 38 | var initialConfidenceDoubleValue: Double { 39 | var result: Double 40 | 41 | switch initialConfidence { 42 | case .low: 43 | result = 0.33 44 | case .medium: 45 | result = 0.66 46 | case .high: 47 | result = 1.00 48 | } 49 | 50 | if name == .stationary { 51 | result -= 0.01 52 | } 53 | 54 | return result 55 | } 56 | 57 | } 58 | 59 | func ==(lhs: CMActivityTypeEvent, rhs: CMActivityTypeEvent) -> Bool { 60 | return lhs.name == rhs.name && lhs.date == rhs.date 61 | } 62 | -------------------------------------------------------------------------------- /LocoKit/Base/CoreMotionActivityTypeName.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CoreMotionActivityTypeName.swift 3 | // LocoKitCore 4 | // 5 | // Created by Matt Greenfield on 13/10/17. 6 | // Copyright © 2017 Big Paua. All rights reserved. 7 | // 8 | 9 | /** 10 | A convenience enum to provide type safe storage of 11 | [CMMotionActivity](https://developer.apple.com/documentation/coremotion/cmmotionactivity) activity type names. 12 | */ 13 | public enum CoreMotionActivityTypeName: String, Codable { 14 | 15 | /** 16 | Equivalent to the `unknown` property on a `CMMotionActivity`. 17 | */ 18 | case unknown 19 | 20 | /** 21 | Equivalent to the `stationary` property on a `CMMotionActivity`. 22 | */ 23 | case stationary 24 | 25 | /** 26 | Equivalent to the `automotive` property on a `CMMotionActivity`. 27 | */ 28 | case automotive 29 | 30 | /** 31 | Equivalent to the `walking` property on a `CMMotionActivity`. 32 | */ 33 | case walking 34 | 35 | /** 36 | Equivalent to the `running` property on a `CMMotionActivity`. 37 | */ 38 | case running 39 | 40 | /** 41 | Equivalent to the `cycling` property on a `CMMotionActivity`. 42 | */ 43 | case cycling 44 | 45 | /** 46 | A convenience array containing all type names. 47 | */ 48 | public static let allTypes = [stationary, automotive, walking, running, cycling, unknown] 49 | } 50 | -------------------------------------------------------------------------------- /LocoKit/Base/CwlMutex.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CwlMutex.swift 3 | // CwlUtils 4 | // 5 | // Created by Matt Gallagher on 2015/02/03. 6 | // Copyright © 2015 Matt Gallagher ( http://cocoawithlove.com ). All rights reserved. 7 | // 8 | // Permission to use, copy, modify, and/or distribute this software for any 9 | // purpose with or without fee is hereby granted, provided that the above 10 | // copyright notice and this permission notice appear in all copies. 11 | // 12 | // THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 13 | // WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 14 | // MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY 15 | // SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 16 | // WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 17 | // ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR 18 | // IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 19 | // 20 | 21 | import Foundation 22 | 23 | public protocol ScopedMutex { 24 | @discardableResult func sync(execute work: () throws -> R) rethrows -> R 25 | @discardableResult func trySync(execute work: () throws -> R) rethrows -> R? 26 | } 27 | 28 | public protocol RawMutex: ScopedMutex { 29 | associatedtype MutexPrimitive 30 | 31 | /// The raw primitive is exposed as an "unsafe" property for faster access in some cases 32 | var unsafeMutex: MutexPrimitive { get set } 33 | 34 | func unbalancedLock() 35 | func unbalancedTryLock() -> Bool 36 | func unbalancedUnlock() 37 | } 38 | 39 | public extension RawMutex { 40 | @discardableResult func sync(execute work: () throws -> R) rethrows -> R { 41 | unbalancedLock() 42 | defer { unbalancedUnlock() } 43 | return try work() 44 | } 45 | @discardableResult func trySync(execute work: () throws -> R) rethrows -> R? { 46 | guard unbalancedTryLock() else { return nil } 47 | defer { unbalancedUnlock() } 48 | return try work() 49 | } 50 | } 51 | 52 | public final class PThreadMutex: RawMutex { 53 | public typealias MutexPrimitive = pthread_mutex_t 54 | 55 | public enum PThreadMutexType { 56 | case normal // PTHREAD_MUTEX_NORMAL 57 | case recursive // PTHREAD_MUTEX_RECURSIVE 58 | } 59 | 60 | public var unsafeMutex = pthread_mutex_t() 61 | 62 | public init(type: PThreadMutexType = .normal) { 63 | var attr = pthread_mutexattr_t() 64 | guard pthread_mutexattr_init(&attr) == 0 else { 65 | preconditionFailure() 66 | } 67 | switch type { 68 | case .normal: 69 | pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_NORMAL) 70 | case .recursive: 71 | pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE) 72 | } 73 | guard pthread_mutex_init(&unsafeMutex, &attr) == 0 else { 74 | preconditionFailure() 75 | } 76 | } 77 | 78 | deinit { pthread_mutex_destroy(&unsafeMutex) } 79 | 80 | public func unbalancedLock() { pthread_mutex_lock(&unsafeMutex) } 81 | public func unbalancedTryLock() -> Bool { return pthread_mutex_trylock(&unsafeMutex) == 0 } 82 | public func unbalancedUnlock() { pthread_mutex_unlock(&unsafeMutex) } 83 | } 84 | 85 | public final class UnfairLock: RawMutex { 86 | public typealias MutexPrimitive = os_unfair_lock 87 | 88 | public init() {} 89 | 90 | /// Exposed as an "unsafe" property so non-scoped patterns can be implemented, if required. 91 | public var unsafeMutex = os_unfair_lock() 92 | 93 | public func unbalancedLock() { os_unfair_lock_lock(&unsafeMutex) } 94 | public func unbalancedTryLock() -> Bool { return os_unfair_lock_trylock(&unsafeMutex) } 95 | public func unbalancedUnlock() { os_unfair_lock_unlock(&unsafeMutex) } 96 | } 97 | -------------------------------------------------------------------------------- /LocoKit/Base/DeviceMotion.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DeviceMotion.swift 3 | // LearnerCoacher 4 | // 5 | // Created by Matt Greenfield on 21/03/17. 6 | // Copyright © 2017 Big Paua. All rights reserved. 7 | // 8 | 9 | import CoreMotion 10 | 11 | internal class DeviceMotion { 12 | 13 | let cmMotion: CMDeviceMotion 14 | 15 | init(cmMotion: CMDeviceMotion) { 16 | self.cmMotion = cmMotion 17 | } 18 | 19 | lazy var userAccelerationInReferenceFrame: CMAcceleration = { 20 | return self.cmMotion.userAccelerationInReferenceFrame 21 | }() 22 | 23 | } 24 | -------------------------------------------------------------------------------- /LocoKit/Base/Helpers/ArrayTools.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ArrayTools.swift 3 | // LocoKit 4 | // 5 | // Created by Matt Greenfield on 5/09/17. 6 | // Copyright © 2017 Big Paua. All rights reserved. 7 | // 8 | 9 | public extension Array { 10 | 11 | var second: Element? { 12 | guard count > 1 else { return nil } 13 | return self[1] 14 | } 15 | 16 | var secondToLast: Element? { 17 | guard count > 1 else { return nil } 18 | return self[count - 2] 19 | } 20 | 21 | } 22 | 23 | public extension Array where Element: FloatingPoint { 24 | 25 | var sum: Element { return reduce(0, +) } 26 | var mean: Element { return isEmpty ? 0 : sum / Element(count) } 27 | 28 | var variance: Element { 29 | let mean = self.mean 30 | let squareDiffs = self.map { value -> Element in 31 | let diff = value - mean 32 | return diff * diff 33 | } 34 | return squareDiffs.mean 35 | } 36 | 37 | var standardDeviation: Element { return variance.squareRoot() } 38 | 39 | } 40 | 41 | public extension Array where Element: Equatable { 42 | mutating func remove(_ object: Element) { if let index = index(of: object) { remove(at: index) } } 43 | mutating func removeObjects(_ array: [Element]) { for object in array { remove(object) } } 44 | } 45 | 46 | public extension Array where Element: Comparable { 47 | var range: (min: Element, max: Element)? { 48 | guard let min = self.min(), let max = self.max() else { return nil } 49 | return (min: min, max: max) 50 | } 51 | } 52 | 53 | -------------------------------------------------------------------------------- /LocoKit/Base/Helpers/MiscTools.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MiscTools.swift 3 | // LocoKit 4 | // 5 | // Created by Matt Greenfield on 4/12/17. 6 | // Copyright © 2017 Big Paua. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public func onMain(_ closure: @escaping () -> ()) { 12 | if Thread.isMainThread { 13 | closure() 14 | } else { 15 | DispatchQueue.main.async(execute: closure) 16 | } 17 | } 18 | 19 | public extension Comparable { 20 | mutating func clamp(min: Self, max: Self) { 21 | if self < min { self = min } 22 | if self > max { self = max } 23 | } 24 | func clamped(min: Self, max: Self) -> Self { 25 | var result = self 26 | if result < min { result = min } 27 | if result > max { result = max } 28 | return result 29 | } 30 | } 31 | 32 | public extension UUID { 33 | var shortString: String { 34 | return String(uuidString.split(separator: "-")[0]) 35 | } 36 | } 37 | 38 | public extension DateInterval { 39 | var middle: Date { 40 | return start + duration * 0.5 41 | } 42 | 43 | func contains(_ other: DateInterval) -> Bool { 44 | if let overlap = intersection(with: other), overlap == other { 45 | return true 46 | } 47 | return false 48 | } 49 | 50 | var containsNow: Bool { 51 | return contains(Date()) 52 | } 53 | } 54 | 55 | public extension Date { 56 | var age: TimeInterval { return -timeIntervalSinceNow } 57 | var startOfDay: Date { return Calendar.current.startOfDay(for: self) } 58 | var sinceStartOfDay: TimeInterval { return self.timeIntervalSince(self.startOfDay) } 59 | func isSameDayAs(_ date: Date) -> Bool { return Calendar.current.isDate(date, inSameDayAs: self) } 60 | func isSameMonthAs(_ date: Date) -> Bool { return Calendar.current.isDate(date, equalTo: self, toGranularity: .month) } 61 | } 62 | 63 | public extension TimeInterval { 64 | static var oneMinute: TimeInterval { return 60 } 65 | static var oneHour: TimeInterval { return oneMinute * 60 } 66 | static var oneDay: TimeInterval { return oneHour * 24 } 67 | static var oneWeek: TimeInterval { return oneDay * 7 } 68 | static var oneMonth: TimeInterval { return oneDay * 30 } 69 | static var oneYear: TimeInterval { return oneDay * 365 } 70 | } 71 | 72 | extension Data { 73 | var hexString: String { 74 | return map { String(format: "%02.2hhx", $0) }.joined() 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /LocoKit/Base/Helpers/StringTools.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Matt Greenfield on 14/04/16. 3 | // Copyright (c) 2016 Big Paua. All rights reserved. 4 | // 5 | 6 | import CoreLocation 7 | 8 | public extension String { 9 | 10 | init(duration: TimeInterval, fractionalUnit: Bool = false, style: DateComponentsFormatter.UnitsStyle = .full, 11 | maximumUnits: Int = 2, alwaysIncludeSeconds: Bool = false) { 12 | if duration.isNaN { 13 | self.init(format: "NaN") 14 | return 15 | } 16 | 17 | if fractionalUnit { 18 | let unitStyle: Formatter.UnitStyle 19 | switch style { 20 | case .positional: unitStyle = .short 21 | case .abbreviated: unitStyle = .short 22 | case .short: unitStyle = .medium 23 | case .brief: unitStyle = .medium 24 | case .full: unitStyle = .long 25 | case .spellOut: unitStyle = .long 26 | } 27 | self.init(String(duration: Measurement(value: duration, unit: UnitDuration.seconds), style: unitStyle)) 28 | return 29 | } 30 | 31 | let formatter = DateComponentsFormatter() 32 | formatter.maximumUnitCount = maximumUnits 33 | formatter.unitsStyle = style 34 | 35 | if alwaysIncludeSeconds || duration < 60 * 3 { 36 | formatter.allowedUnits = [.second, .minute, .hour, .day, .month] 37 | } else { 38 | formatter.allowedUnits = [.minute, .hour, .day, .month] 39 | } 40 | 41 | self.init(format: formatter.string(from: duration)!) 42 | } 43 | 44 | init(duration: Measurement, style: Formatter.UnitStyle = .medium) { 45 | let formatter = MeasurementFormatter() 46 | formatter.unitStyle = style 47 | formatter.unitOptions = .naturalScale 48 | formatter.numberFormatter.maximumFractionDigits = 1 49 | self.init(format: formatter.string(from: duration)) 50 | } 51 | 52 | init(distance: CLLocationDistance, style: MeasurementFormatter.UnitStyle = .medium, isAltitude: Bool = false) { 53 | self.init(metres: distance, style: style, isAltitude: isAltitude) 54 | } 55 | 56 | init(metres: CLLocationDistance, style: MeasurementFormatter.UnitStyle = .medium, isAltitude: Bool = false) { 57 | let formatter = MeasurementFormatter() 58 | formatter.unitStyle = style 59 | 60 | if isAltitude { 61 | formatter.unitOptions = .providedUnit 62 | formatter.numberFormatter.maximumFractionDigits = 0 63 | if Locale.current.usesMetricSystem { 64 | self.init(format: formatter.string(from: metres.measurement)) 65 | } else { 66 | self.init(format: formatter.string(from: metres.measurement.converted(to: UnitLength.feet))) 67 | } 68 | return 69 | } 70 | 71 | formatter.unitOptions = .naturalScale 72 | if metres < 1000 || metres > 20000 { 73 | formatter.numberFormatter.maximumFractionDigits = 0 74 | } else { 75 | formatter.numberFormatter.maximumFractionDigits = 1 76 | } 77 | self.init(format: formatter.string(from: metres.measurement)) 78 | } 79 | 80 | init(speed: CLLocationSpeed, style: Formatter.UnitStyle? = nil) { 81 | self.init(metresPerSecond: speed, style: style) 82 | } 83 | 84 | init(metresPerSecond mps: CLLocationSpeed, style: Formatter.UnitStyle? = nil) { 85 | let formatter = MeasurementFormatter() 86 | if let style = style { 87 | formatter.unitStyle = style 88 | } 89 | if mps.kmh < 10 { 90 | formatter.numberFormatter.maximumFractionDigits = 1 91 | } else { 92 | formatter.numberFormatter.maximumFractionDigits = 0 93 | } 94 | self.init(format: formatter.string(from: mps.speedMeasurement)) 95 | } 96 | 97 | func deletingPrefix(_ prefix: String) -> String { 98 | guard self.hasPrefix(prefix) else { return self } 99 | return String(self.dropFirst(prefix.count)) 100 | } 101 | 102 | } 103 | -------------------------------------------------------------------------------- /LocoKit/Base/KalmanAltitude.swift: -------------------------------------------------------------------------------- 1 | // 2 | // KalmanAltitude.swift 3 | // LearnerCoacher 4 | // 5 | // Created by Matt Greenfield on 14/06/17. 6 | // Copyright © 2017 Big Paua. All rights reserved. 7 | // 8 | 9 | import CoreLocation 10 | 11 | internal class KalmanAltitude: KalmanFilter { 12 | 13 | var altitude: Double? 14 | var unfilteredLocation: CLLocation? 15 | 16 | init(qMetresPerSecond: Double) { 17 | super.init(q: qMetresPerSecond) 18 | } 19 | 20 | override func reset() { 21 | super.reset() 22 | altitude = nil 23 | } 24 | 25 | func add(location: CLLocation) { 26 | guard location.verticalAccuracy > 0 else { 27 | return 28 | } 29 | 30 | guard location.timestamp.timeIntervalSince1970 >= timestamp else { 31 | return 32 | } 33 | 34 | unfilteredLocation = location 35 | 36 | // update the kalman internals 37 | update(date: location.timestamp, accuracy: location.verticalAccuracy) 38 | 39 | // apply the k 40 | if let oldAltitude = altitude { 41 | self.altitude = oldAltitude + (k * (location.altitude - oldAltitude)) 42 | } else { 43 | self.altitude = location.altitude 44 | } 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /LocoKit/Base/KalmanCoordinates.swift: -------------------------------------------------------------------------------- 1 | // 2 | // KalmanCoordinates.swift 3 | // LearnerCoacher 4 | // 5 | // Created by Matt Greenfield on 14/06/17. 6 | // Copyright © 2017 Big Paua. All rights reserved. 7 | // 8 | 9 | import os.log 10 | import CoreLocation 11 | 12 | internal class KalmanCoordinates: KalmanFilter { 13 | 14 | fileprivate var latitude: Double = 0 15 | fileprivate var longitude: Double = 0 16 | var unfilteredLocation: CLLocation? 17 | 18 | init(qMetresPerSecond: Double) { 19 | super.init(q: qMetresPerSecond) 20 | } 21 | 22 | override func reset() { 23 | super.reset() 24 | latitude = 0 25 | longitude = 0 26 | } 27 | 28 | func add(location: CLLocation) { 29 | guard location.hasUsableCoordinate else { 30 | return 31 | } 32 | 33 | guard location.timestamp.timeIntervalSince1970 > timestamp else { 34 | return 35 | } 36 | 37 | unfilteredLocation = location 38 | 39 | // update the kalman internals 40 | update(date: location.timestamp, accuracy: location.horizontalAccuracy) 41 | 42 | // apply the k 43 | latitude = predictedValueFor(oldValue: latitude, newValue: location.coordinate.latitude) 44 | longitude = predictedValueFor(oldValue: longitude, newValue: location.coordinate.longitude) 45 | } 46 | 47 | } 48 | 49 | extension KalmanCoordinates { 50 | 51 | var coordinate: CLLocationCoordinate2D? { 52 | guard variance >= 0 else { 53 | return nil 54 | } 55 | 56 | return CLLocationCoordinate2D(latitude: latitude, longitude: longitude) 57 | } 58 | 59 | } 60 | -------------------------------------------------------------------------------- /LocoKit/Base/KalmanFilter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // KalmanFilter.swift 3 | // 4 | // 5 | // Created by Matt Greenfield on 29/05/17. 6 | // 7 | 8 | import os.log 9 | import CoreLocation 10 | 11 | // source: https://stackoverflow.com/a/15657798/790036 12 | 13 | internal class KalmanFilter { 14 | 15 | var q: Double // expected change per sample 16 | var k: Double = 1 // trust percentage to apply to new values 17 | var variance: Double = -1 // p matrix 18 | var timestamp: TimeInterval = 0 19 | 20 | init(q: Double) { 21 | self.q = q 22 | } 23 | 24 | // next input will be treated as first 25 | func reset() { 26 | k = 1 27 | variance = -1 28 | } 29 | 30 | func resetVarianceTo(accuracy: Double) { 31 | variance = accuracy * accuracy 32 | } 33 | 34 | func update(date: Date, accuracy: Double) { 35 | 36 | // first input after init or reset 37 | if variance < 0 { 38 | variance = accuracy * accuracy 39 | timestamp = date.timeIntervalSince1970 40 | return 41 | } 42 | 43 | // uncertainty in the current value increases as time passes 44 | let timeDiff = date.timeIntervalSince1970 - timestamp 45 | if timeDiff > 0 { 46 | variance += timeDiff * q * q 47 | timestamp = date.timeIntervalSince1970 48 | } 49 | 50 | // gain matrix k = covariance * inverse(covariance + measurementVariance) 51 | k = variance / (variance + accuracy * accuracy) 52 | 53 | // new covariance matrix is (identityMatrix - k) * covariance 54 | variance = (1.0 - k) * variance 55 | } 56 | 57 | } 58 | 59 | extension KalmanFilter { 60 | 61 | var accuracy: Double { 62 | return variance.squareRoot() 63 | } 64 | 65 | var date: Date { 66 | return Date(timeIntervalSince1970: timestamp) 67 | } 68 | 69 | func predictedValueFor(oldValue: Double, newValue: Double) -> Double { 70 | return oldValue + (k * (newValue - oldValue)) 71 | } 72 | 73 | } 74 | -------------------------------------------------------------------------------- /LocoKit/Base/MovingState.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MovingState.swift 3 | // LocoKitCore 4 | // 5 | // Created by Matt Greenfield on 21/11/17. 6 | // Copyright © 2017 Big Paua. All rights reserved. 7 | // 8 | 9 | /** 10 | The device's location moving / stationary state at a point in time. 11 | 12 | ### Outdoor Accuracy 13 | 14 | The slowest detectable moving speed varies depending on the accuracy of available location data, however under typical 15 | conditions the slowest detected moving speed is a slow walk (~3 km/h) while outdoors. 16 | 17 | ### Indoor Accuracy 18 | 19 | Most normal indoor movement will be classified as stationary, due to lack of GPS line of sight resulting in available 20 | location accuracy of 65 metres or worse. This has the side benefit of allowing indoor events to be clustered into 21 | distinct "visits". 22 | 23 | ### iBeacons 24 | 25 | A building fitted with multiple iBeacons may increase the available indoor location accuracy to as high as 5 26 | metres. In such an environment, indoor movement may be detectable with similar accuracy to outdoor movement. 27 | */ 28 | public enum MovingState: String, Codable { 29 | 30 | /** 31 | The device has been determined to be moving between places, based on available location data. 32 | */ 33 | case moving 34 | 35 | /** 36 | The device has been determined to be either stationary, or moving slower than the slowest currently detectable 37 | moving speed. 38 | */ 39 | case stationary 40 | 41 | /** 42 | The device's moving / stationary state could not be confidently determined. 43 | 44 | This state can occur either due to no available location data, or the available location data falling below 45 | necessary quality or quantity thresholds. 46 | */ 47 | case uncertain 48 | } 49 | -------------------------------------------------------------------------------- /LocoKit/Base/NotificationNames.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NotificationNames.swift 3 | // LocoKit 4 | // 5 | // Created by Matt Greenfield on 5/11/18. 6 | // 7 | 8 | import Foundation 9 | 10 | /** 11 | Custom notification events that the `LocomotionManager`` may send. 12 | */ 13 | public extension NSNotification.Name { 14 | 15 | /** 16 | `locomotionSampleUpdated` is sent whenever an updated LocomotionSample is available. 17 | 18 | Typically this indicates that a new CLLocation has arrived, however this notification will also be periodically 19 | sent even when no new location data is arriving, to indicate other changes to the current state. The location in 20 | these samples may differ from previous even though new location data is not available, due to older raw locations 21 | being discarded from the sample. 22 | */ 23 | static let locomotionSampleUpdated = Notification.Name("locomotionSampleUpdated") 24 | 25 | /** 26 | `willStartRecording` is sent when recording is about to begin or resume. 27 | */ 28 | static let willStartRecording = Notification.Name("willStartRecording") 29 | 30 | /** 31 | `recordingStateChanged` is sent after each `recordingState` change. 32 | */ 33 | static let recordingStateChanged = Notification.Name("recordingStateChanged") 34 | 35 | /** 36 | `movingStateChanged` is sent after each `movingState` change. 37 | */ 38 | static let movingStateChanged = Notification.Name("movingStateChanged") 39 | 40 | /** 41 | `willStartSleepMode` is sent when sleep mode is about to begin or resume. 42 | 43 | - Note: This includes both transitions from `recording` state and `wakeup` state. 44 | */ 45 | static let willStartSleepMode = Notification.Name("willStartSleepMode") 46 | 47 | /** 48 | `didStartSleepMode` is sent after sleep mode has begun or resumed. 49 | 50 | - Note: This includes both transitions from `recording` state and `wakeup` state. 51 | */ 52 | static let didStartSleepMode = Notification.Name("didStartSleepMode") 53 | 54 | /** 55 | `willStartDeepSleepMode` is sent when deep sleep mode is about to begin or resume. 56 | 57 | - Note: This includes both transitions from `recording` state and `wakeup` state. 58 | */ 59 | static let willStartDeepSleepMode = Notification.Name("willStartDeepSleepMode") 60 | 61 | /** 62 | `wentFromRecordingToSleepMode` is sent after transitioning from `recording` state to `sleeping` state. 63 | */ 64 | static let wentFromRecordingToSleepMode = Notification.Name("wentFromRecordingToSleepMode") 65 | 66 | /** 67 | `wentFromSleepModeToRecording` is sent after transitioning from `sleeping` state to `recording` state. 68 | */ 69 | static let wentFromSleepModeToRecording = Notification.Name("wentFromSleepModeToRecording") 70 | 71 | static let concededRecording = Notification.Name("concededRecording") 72 | static let tookOverRecording = Notification.Name("tookOverRecording") 73 | static let timelineObjectsExternallyModified = Notification.Name("timelineObjectsExternallyModified") 74 | 75 | // broadcasted CLLocationManagerDelegate events 76 | static let didChangeAuthorizationStatus = Notification.Name("didChangeAuthorizationStatus") 77 | static let didUpdateLocations = Notification.Name("didUpdateLocations") 78 | static let didRangeBeacons = Notification.Name("didRangeBeacons") 79 | static let didEnterRegion = Notification.Name("didEnterRegion") 80 | static let didExitRegion = Notification.Name("didExitRegion") 81 | static let didVisit = Notification.Name("didVisit") 82 | 83 | @available(*, unavailable, renamed: "wentFromRecordingToSleepMode") 84 | static let startedSleepMode = Notification.Name("startedSleepMode") 85 | 86 | @available(*, unavailable, renamed: "wentFromSleepModeToRecording") 87 | static let stoppedSleepMode = Notification.Name("stoppedSleepMode") 88 | } 89 | -------------------------------------------------------------------------------- /LocoKit/Base/RecordingState.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RecordingState.swift 3 | // LocoKit 4 | // 5 | // Created by Matt Greenfield on 26/11/17. 6 | // Copyright © 2017 Big Paua. All rights reserved. 7 | // 8 | 9 | /** 10 | The recording state of the LocomotionManager. 11 | */ 12 | public enum RecordingState: String, Codable { 13 | 14 | /** 15 | This state indicates that the LocomotionManager is turned on and recording location data. It may also be recording 16 | motion data, depending on the LocomotionManager's settings. 17 | */ 18 | case recording 19 | 20 | /** 21 | This state indicates that the LocomotionManager is in low power sleep mode. 22 | */ 23 | case sleeping 24 | 25 | /** 26 | This state indicates that the LocomotionManager is not recording, but is ready to be woken up by iOS and restart 27 | recording at an appropriate time. 28 | */ 29 | case deepSleeping 30 | 31 | /** 32 | This state indicates that the LocomotionManager is performing a periodic wakeup from sleep mode, to determine 33 | whether it should resume recording or should continue sleeping. 34 | */ 35 | case wakeup 36 | 37 | /** 38 | Recording is off, but the app is kept alive and the manager is ready to restart recording immediately if requested. 39 | */ 40 | case standby 41 | 42 | /** 43 | This state indicates that the LocomotionManager is turned off and is not recording location or motion data. 44 | */ 45 | case off 46 | 47 | public var isSleeping: Bool { return RecordingState.sleepStates.contains(self) } 48 | public var isCurrentRecorder: Bool { return RecordingState.activeRecorderStates.contains(self) } 49 | 50 | public static let sleepStates = [wakeup, sleeping, deepSleeping] 51 | public static let activeRecorderStates = [recording, wakeup, sleeping, deepSleeping] 52 | 53 | } 54 | -------------------------------------------------------------------------------- /LocoKit/Base/Strings/Localizable.cat.strings: -------------------------------------------------------------------------------- 1 | /* 2 | Localizable.strings 3 | LocoKit 4 | 5 | Created by Matt Greenfield on 26/1/20. 6 | Copyright © 2020 Big Paua. All rights reserved. 7 | */ 8 | 9 | "Unknown" = "Desconegut"; 10 | "Bogus" = "Errònia"; 11 | "Transport" = "Transport"; 12 | "Data Gap" = "Sense Dades"; 13 | 14 | // base types 15 | "Stationary" = "Estacionari"; 16 | "Walking" = "Caminant"; 17 | "Running" = "Corrent"; 18 | "Cycling" = "Bicicleta"; 19 | "Car" = "Cotxe"; 20 | "Airplane" = "Avió"; 21 | 22 | // extended types 23 | "Train" = "Tren"; 24 | "Bus" = "Autobús"; 25 | "Motorcycle" = "Moto"; 26 | "Boat" = "Vaixell"; 27 | "Tram" = "Tramvia"; 28 | "Tractor" = "Tractor"; 29 | "Tuk-Tuk" = "Tuk-Tuk"; 30 | "Songthaew" = "Songthaew"; 31 | "Scooter" = "Patinet Elèctric"; 32 | "Metro" = "Metro"; 33 | "Cable Car" = "Telefèric"; 34 | "Funicular" = "Funicular"; 35 | "Chairlift" = "Telecadira"; 36 | "Ski Lift" = "Telesquí"; 37 | "Taxi" = "Taxi"; 38 | 39 | // active types 40 | "Skateboarding" = "Skateboarding"; 41 | "Inline Skating" = "Patinatge en línia"; 42 | "Snowboarding" = "Snowboarding"; 43 | "Skiing" = "Esquí"; 44 | "Horseback" = "A cavall"; 45 | "Swimming" = "Nedar"; 46 | "Golf" = "Golf"; 47 | "Wheelchair" = "Cadira de rodes"; 48 | "Rowing" = "Rem"; 49 | "Kayaking" = "Caiac"; 50 | -------------------------------------------------------------------------------- /LocoKit/Base/Strings/Localizable.de.strings: -------------------------------------------------------------------------------- 1 | /* 2 | Localizable.strings 3 | LocoKit 4 | 5 | Created by Matt Greenfield on 26/1/20. 6 | Copyright © 2020 Big Paua. All rights reserved. 7 | */ 8 | 9 | "Unknown" = "Unbekannt"; 10 | "Bogus" = "Quatsch"; 11 | "Transport" = "Unterwegs"; 12 | "Data Gap" = "Datenlücke"; 13 | 14 | // base types 15 | "Stationary" = "Aufenthalt"; 16 | "Walking" = "Gehen"; 17 | "Running" = "Laufen"; 18 | "Cycling" = "Fahrrad"; 19 | "Car" = "Auto"; 20 | "Airplane" = "Flugzeug"; 21 | 22 | // extended types 23 | "Train" = "Zug"; 24 | "Bus" = "Bus"; 25 | "Motorcycle" = "Motorrad"; 26 | "Boat" = "Boot"; 27 | "Tram" = "Straßenbahn"; 28 | "Tractor" = "Traktor"; 29 | "Tuk-Tuk" = "Tuk-Tuk"; 30 | "Songthaew" = "Songthaeo"; 31 | "Scooter" = "Roller"; 32 | "Metro" = "U-Bahn"; 33 | "Cable Car" = "Seilbahn"; 34 | "Funicular" = "Standseilbahn"; 35 | "Chairlift" = "Sessellift"; 36 | "Ski Lift" = "Skilift"; 37 | "Taxi" = "Taxi"; 38 | 39 | // active types 40 | "Skateboarding" = "Skateboard"; 41 | "Inline Skating" = "Inlineskates"; 42 | "Snowboarding" = "Snowboard"; 43 | "Skiing" = "Ski"; 44 | "Horseback" = "Pferd"; 45 | "Swimming" = "Schwimmen"; 46 | "Golf" = "Golf"; 47 | "Wheelchair" = "Rollstuhl"; 48 | "Rowing" = "Ruderboot"; 49 | "Kayaking" = "Kayak"; 50 | -------------------------------------------------------------------------------- /LocoKit/Base/Strings/Localizable.en.strings: -------------------------------------------------------------------------------- 1 | /* 2 | Localizable.strings 3 | LocoKit 4 | 5 | Created by Matt Greenfield on 26/1/20. 6 | Copyright © 2020 Big Paua. All rights reserved. 7 | */ 8 | 9 | "Unknown" = "Unknown"; 10 | "Bogus" = "Bogus"; 11 | "Transport" = "Transport"; 12 | "Data Gap" = "Data Gap"; 13 | 14 | // base types 15 | "Stationary" = "Stationary"; 16 | "Walking" = "Walking"; 17 | "Running" = "Running"; 18 | "Cycling" = "Cycling"; 19 | "Car" = "Car"; 20 | "Airplane" = "Airplane"; 21 | 22 | // extended types 23 | "Train" = "Train"; 24 | "Bus" = "Bus"; 25 | "Motorcycle" = "Motorcycle"; 26 | "Boat" = "Boat"; 27 | "Tram" = "Tram"; 28 | "Tractor" = "Tractor"; 29 | "Tuk-Tuk" = "Tuk-Tuk"; 30 | "Songthaew" = "Songthaew"; 31 | "Scooter" = "Scooter"; 32 | "Metro" = "Metro"; 33 | "Cable Car" = "Cable Car"; 34 | "Funicular" = "Funicular"; 35 | "Chairlift" = "Chairlift"; 36 | "Ski Lift" = "Ski Lift"; 37 | "Taxi" = "Taxi"; 38 | 39 | // active types 40 | "Skateboarding" = "Skateboarding"; 41 | "Inline Skating" = "Inline Skating"; 42 | "Snowboarding" = "Snowboarding"; 43 | "Skiing" = "Skiing"; 44 | "Horseback" = "Horseback"; 45 | "Swimming" = "Swimming"; 46 | "Golf" = "Golf"; 47 | "Wheelchair" = "Wheelchair"; 48 | "Rowing" = "Rowing"; 49 | "Kayaking" = "Kayaking"; 50 | -------------------------------------------------------------------------------- /LocoKit/Base/Strings/Localizable.es.strings: -------------------------------------------------------------------------------- 1 | /* 2 | Localizable.strings 3 | LocoKit 4 | 5 | Created by Matt Greenfield on 26/1/20. 6 | Copyright © 2020 Big Paua. All rights reserved. 7 | */ 8 | 9 | "Unknown" = "Desconocido"; 10 | "Bogus" = "Errónea"; 11 | "Transport" = "Transporte"; 12 | "Data Gap" = "Sin Datos"; 13 | 14 | // base types 15 | "Stationary" = "Estacionario"; 16 | "Walking" = "Caminando"; 17 | "Running" = "Corriendo"; 18 | "Cycling" = "Bicicleta"; 19 | "Car" = "Coche"; 20 | "Airplane" = "Avion"; 21 | 22 | // extended types 23 | "Train" = "Tren"; 24 | "Bus" = "Autobús"; 25 | "Motorcycle" = "Moto"; 26 | "Boat" = "Barco"; 27 | "Tram" = "Tranvía"; 28 | "Tractor" = "Tractor"; 29 | "Tuk-Tuk" = "Tuk-Tuk"; 30 | "Songthaew" = "Songthaew"; 31 | "Scooter" = "Patinete Eléctrico"; 32 | "Metro" = "Metro"; 33 | "Cable Car" = "Teleférico"; 34 | "Funicular" = "Funicular"; 35 | "Chairlift" = "Telesilla"; 36 | "Ski Lift" = "Telesquí"; 37 | "Taxi" = "Taxi"; 38 | 39 | // active types 40 | "Skateboarding" = "Skateboarding"; 41 | "Inline Skating" = "Patinaje en línea"; 42 | "Snowboarding" = "Snowboarding"; 43 | "Skiing" = "Esquí"; 44 | "Horseback" = "A caballo"; 45 | "Swimming" = "Nadar"; 46 | "Golf" = "Golf"; 47 | "Wheelchair" = "Silla de ruedas"; 48 | "Rowing" = "Remo"; 49 | "Kayaking" = "Kayak"; 50 | -------------------------------------------------------------------------------- /LocoKit/Base/Strings/Localizable.fr.strings: -------------------------------------------------------------------------------- 1 | /* 2 | Localizable.strings 3 | LocoKit 4 | 5 | Created by Matt Greenfield on 26/1/20. 6 | Copyright © 2020 Big Paua. All rights reserved. 7 | */ 8 | 9 | "Unknown" = "Inconnu"; 10 | "Bogus" = "Faux mouvement"; 11 | "Transport" = "Transport"; 12 | "Data Gap" = "Données manquantes"; 13 | 14 | // base types 15 | "Stationary" = "Stationnaire"; 16 | "Walking" = "Marche"; 17 | "Running" = "Course à pied"; 18 | "Cycling" = "Vélo"; 19 | "Car" = "Voiture"; 20 | "Airplane" = "Avion"; 21 | 22 | // extended types 23 | "Train" = "Train"; 24 | "Bus" = "Bus"; 25 | "Motorcycle" = "Moto"; 26 | "Boat" = "Bateau"; 27 | "Tram" = "Tramway"; 28 | "Tractor" = "Tracteur"; 29 | "Tuk-Tuk" = "Tuk-Tuk"; 30 | "Songthaew" = "Songthaew"; 31 | "Scooter" = "Scooter"; 32 | "Metro" = "Métro"; 33 | "Cable Car" = "Téléphérique"; 34 | "Funicular" = "Funiculaire"; 35 | "Chairlift" = "Télésiège"; 36 | "Ski Lift" = "Téléski"; 37 | "Taxi" = "Taxi"; 38 | 39 | // active types 40 | "Skateboarding" = "Skateboard"; 41 | "Inline Skating" = "Roller"; 42 | "Snowboarding" = "Snowboard"; 43 | "Skiing" = "Ski"; 44 | "Horseback" = "Equitation"; 45 | "Swimming" = "Natation"; 46 | "Golf" = "Golf"; 47 | "Wheelchair" = "Fauteuil roulant"; 48 | "Rowing" = "Aviron"; 49 | "Kayaking" = "Kayak"; 50 | -------------------------------------------------------------------------------- /LocoKit/Base/Strings/Localizable.ja.strings: -------------------------------------------------------------------------------- 1 | /* 2 | Localizable.strings 3 | LocoKit 4 | 5 | Created by Matt Greenfield on 26/1/20. 6 | Copyright © 2020 Big Paua. All rights reserved. 7 | */ 8 | 9 | "Unknown" = "不明"; 10 | "Bogus" = "不正確なデータ"; 11 | "Transport" = "移動"; 12 | "Data Gap" = "データの欠落"; 13 | 14 | // base types 15 | "Stationary" = "滞在"; 16 | "Walking" = "徒歩"; 17 | "Running" = "ランニング"; 18 | "Cycling" = "サイクリング"; 19 | "Car" = "車"; 20 | "Airplane" = "飛行機"; 21 | 22 | // extended types 23 | "Train" = "電車"; 24 | "Bus" = "バス"; 25 | "Motorcycle" = "オートバイ"; 26 | "Boat" = "ボート"; 27 | "Tram" = "路面電車"; 28 | "Tractor" = "トラクター"; 29 | "Tuk-Tuk" = "トゥクトゥク"; 30 | "Songthaew" = "ソンテウ"; 31 | "Scooter" = "スクーター"; 32 | "Metro" = "地下鉄"; 33 | "Cable Car" = "ケーブルカー"; 34 | "Funicular" = "フニクラ"; 35 | "Chairlift" = "チェアリフト"; 36 | "Ski Lift" = "スキーリフト"; 37 | "Taxi" = "タクシー"; 38 | 39 | // active types 40 | "Skateboarding" = "スケートボード"; 41 | "Inline Skating" = "インラインスケート"; 42 | "Snowboarding" = "スノーボード"; 43 | "Skiing" = "スキー"; 44 | "Horseback" = "乗馬"; 45 | "Swimming" = "スイミング"; 46 | "Golf" = "ゴルフ"; 47 | "Wheelchair" = "車いす"; 48 | "Rowing" = "ボート漕ぎ"; 49 | "Kayaking" = "カヤック漕ぎ"; 50 | -------------------------------------------------------------------------------- /LocoKit/Base/Strings/Localizable.th.strings: -------------------------------------------------------------------------------- 1 | /* 2 | Localizable.strings 3 | LocoKit 4 | 5 | Created by Matt Greenfield on 26/1/20. 6 | Copyright © 2020 Big Paua. All rights reserved. 7 | */ 8 | 9 | "Unknown" = "ไม่ปรากฏ"; 10 | "Bogus" = "เก๊"; 11 | "Transport" = "การเดินทาง"; 12 | "Data Gap" = "Data Gap"; 13 | 14 | // base types 15 | "Stationary" = "อยู่กับที่"; 16 | "Walking" = "เดิน"; 17 | "Running" = "วิ่ง"; 18 | "Cycling" = "ขี่จักรยาน"; 19 | "Car" = "รถ"; 20 | "Airplane" = "เครื่องบิน"; 21 | 22 | // extended types 23 | "Train" = "รถไฟ"; 24 | "Bus" = "รถเมล์"; 25 | "Motorcycle" = "มอเตอร์ไซค์"; 26 | "Boat" = "เรือ"; 27 | "Tram" = "รถราง"; 28 | "Tractor" = "แทรกเตอร์"; 29 | "Tuk-Tuk" = "ตุ๊กตุ๊ก"; 30 | "Songthaew" = "สองแถว"; 31 | "Scooter" = "สกูตเตอร์"; 32 | "Metro" = "รถไฟฟ้าใต้ดิน"; 33 | "Cable Car" = "กระเช้าไฟฟ้า"; 34 | "Funicular" = "กระเช้าไฟฟ้าบนราง"; 35 | "Chairlift" = "ลิฟท์เก้าอี้"; 36 | "Ski Lift" = "ลิฟท์สกี"; 37 | "Taxi" = "แท็กซี่"; 38 | 39 | // active types 40 | "Skateboarding" = "สเกตบอร์ด"; 41 | "Inline Skating" = "อินไลน์สเกต"; 42 | "Snowboarding" = "สโนว์บอร์ด"; 43 | "Skiing" = "สกี"; 44 | "Horseback" = "ขี่ม้า"; 45 | "Swimming" = "ว่ายน้ำ"; 46 | "Golf" = "กอล์ฟ"; 47 | "Wheelchair" = "เก้าอี้เข็น"; 48 | "Rowing" = "พายเรือ"; 49 | "Kayaking" = "เรือคายัก"; 50 | -------------------------------------------------------------------------------- /LocoKit/Base/TrustAssessor.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TrustAssessor.swift 3 | // LocoKit 4 | // 5 | // Created by Matt Greenfield on 30/6/19. 6 | // 7 | 8 | import CoreLocation 9 | 10 | public protocol TrustAssessor { 11 | func trustFactorFor(_ coordinate: CLLocationCoordinate2D) -> Double? 12 | } 13 | -------------------------------------------------------------------------------- /LocoKit/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 | -------------------------------------------------------------------------------- /LocoKit/LocoKit.h: -------------------------------------------------------------------------------- 1 | // 2 | // LocoKit.h 3 | // LocoKit 4 | // 5 | // Created by Matt Greenfield on 22/11/17. 6 | // Copyright © 2017 Big Paua. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | //! Project version number for LocoKit. 12 | FOUNDATION_EXPORT double LocoKitVersionNumber; 13 | 14 | //! Project version string for LocoKit. 15 | FOUNDATION_EXPORT const unsigned char LocoKitVersionString[]; 16 | 17 | -------------------------------------------------------------------------------- /LocoKit/Timelines/ActivityTypes/ActivityTypeClassifiable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ActivityTypeScorable.swift 3 | // LearnerCoacher 4 | // 5 | // Created by Matt Greenfield on 8/01/17. 6 | // Copyright © 2017 Big Paua. All rights reserved. 7 | // 8 | 9 | import CoreLocation 10 | 11 | public protocol ActivityTypeClassifiable: class { 12 | 13 | var location: CLLocation? { get } 14 | var movingState: MovingState { get } 15 | var coreMotionActivityType: CoreMotionActivityTypeName? { get } 16 | var stepHz: Double? { get } 17 | var courseVariance: Double? { get } 18 | var xyAcceleration: Double? { get } 19 | var zAcceleration: Double? { get } 20 | var timeOfDay: TimeInterval { get } 21 | var previousSampleConfirmedType: ActivityTypeName? { get } 22 | 23 | var classifierResults: ClassifierResults? { get set } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /LocoKit/Timelines/ActivityTypes/ActivityTypeClassifier.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ActivityTypeClassifier.swift 3 | // LocoKitCore 4 | // 5 | // Created by Matt Greenfield on 3/08/17. 6 | // Copyright © 2017 Big Paua. All rights reserved. 7 | // 8 | 9 | import os.log 10 | import CoreLocation 11 | 12 | /** 13 | Activity Type Classifiers are Machine Learning Classifiers. Use an Activity Type Classifier to determine the 14 | `ActivityTypeName` of a `LocomotionSample`. 15 | 16 | - Precondition: An API key is required to make use of classifiers. See `LocoKitService.apiKey` for details. 17 | 18 | ## Supported Activity Types 19 | 20 | #### Base Types 21 | 22 | stationary, walking, running, cycling 23 | 24 | Base types match one-to-one with [Core Motion activity types](https://developer.apple.com/documentation/coremotion/cmmotionactivity), 25 | with the exception of Core Motion's "automotive" type, which is instead handled by extended types in LocoKit. 26 | 27 | #### Extended Types 28 | 29 | car, train, bus, motorcycle, boat, airplane, tram, horseback, scooter, skateboarding, tractor, skiing, 30 | inline skating, metro, tuk-tuk, songthaew 31 | 32 | ## Region Specific Classifiers 33 | 34 | LocoKit provides geographical region specific machine learning data, with each classifier containing the data for a 35 | specific region. 36 | 37 | This allows for detecting activity types based on region specific characteristics, with much higher accuracy than 38 | iOS's built in Core Motion types detection. It also makes it possible to detect a greater number of activity types, 39 | for example distinguishing between travel by car or train. 40 | 41 | LocoKit's data regions are roughly 100 kilometres by 100 kilometres squared (0.1 by 0.1 degrees), or about the size of 42 | a small town, or a single neighbourhood in a larger city. 43 | 44 | Larger cities might encompass anywhere from four to ten or more classifier regions, thus allowing the classifers to 45 | accurately detect activity type differences within different areas of a single city. 46 | 47 | ## Determining Regional Coverage 48 | 49 | - [LocoKit transport coverage maps](https://www.bigpaua.com/locokit/coverage/transport) 50 | - [LocoKit cycling coverage maps](https://www.bigpaua.com/locokit/coverage/cycling) 51 | 52 | #### Stationary, Walking, Running, Cycling 53 | 54 | The base activity types of stationary, walking, running, and should achieve high detection accuracy everywhere in 55 | the world, regardless of local data availability. 56 | 57 | These types can be considered to have global coverage. 58 | 59 | #### Car, Train, Bus, Motorcycle, Airplane, Boat, etc 60 | 61 | Determining the specific mode of transport requires local knowledge. If knowing the specific mode of transport is 62 | important to your application, you should check the coverage maps for your required regions. 63 | 64 | When local data coverage is not high enough to distinguish specific modes of transport, a threshold probability 65 | score should be used on the "best match" classifier result, to determine when to fall back to presenting a generic 66 | "transport" classification to the user. 67 | 68 | For example if the highest scoring type is "cycling", but its probability score is only 0.001, that identifies it as 69 | a terrible match, thus the real type is most likely some other mode of transport. Your UI should then avoid claiming 70 | "cycling", and instead report a generic type name to the user, such as "transport", "automotive", or "unknown". 71 | */ 72 | public class ActivityTypeClassifier: MLClassifier { 73 | 74 | public typealias Cache = ActivityTypesCache 75 | public typealias ParentClassifier = Cache.ParentClassifier 76 | 77 | let cache = Cache.highlander 78 | 79 | public let depth: Int 80 | public let supportedTypes: [ActivityTypeName] 81 | public let models: [Cache.Model] 82 | 83 | private var _parent: ParentClassifier? 84 | public var parent: ParentClassifier? { 85 | get { 86 | if let parent = _parent { 87 | return parent 88 | } 89 | 90 | let parentDepth = depth - 1 91 | 92 | // can only get supported depths 93 | guard cache.providesDepths.contains(parentDepth) else { 94 | return nil 95 | } 96 | 97 | // no point in getting a parent if current depth is complete 98 | guard completenessScore < 1 else { 99 | return nil 100 | } 101 | 102 | // can't do anything without a coord 103 | guard let coordinate = centerCoordinate else { 104 | return nil 105 | } 106 | 107 | // try to fetch one 108 | _parent = ParentClassifier(requestedTypes: supportedTypes, coordinate: coordinate, depth: parentDepth) 109 | 110 | return _parent 111 | } 112 | 113 | set (newParent) { 114 | _parent = newParent 115 | } 116 | } 117 | 118 | public lazy var lastUpdated: Date? = { 119 | return self.models.lastUpdated 120 | }() 121 | 122 | public lazy var lastFetched: Date = { 123 | return models.lastFetched 124 | }() 125 | 126 | public lazy var accuracyScore: Double? = { 127 | return self.models.accuracyScore 128 | }() 129 | 130 | public lazy var completenessScore: Double = { 131 | return self.models.completenessScore 132 | }() 133 | 134 | // MARK: - Init 135 | 136 | public convenience required init?(requestedTypes: [ActivityTypeName] = ActivityTypeName.baseTypes, 137 | coordinate: CLLocationCoordinate2D) { 138 | self.init(requestedTypes: requestedTypes, coordinate: coordinate, depth: 2) 139 | } 140 | 141 | convenience init?(requestedTypes: [ActivityTypeName], coordinate: CLLocationCoordinate2D, depth: Int) { 142 | if requestedTypes.isEmpty { 143 | return nil 144 | } 145 | 146 | let models = Cache.highlander.modelsFor(names: requestedTypes, coordinate: coordinate, depth: depth) 147 | 148 | guard !models.isEmpty else { 149 | return nil 150 | } 151 | 152 | self.init(supportedTypes: requestedTypes, models: models, depth: depth) 153 | 154 | // bootstrap the parent 155 | _ = parent 156 | } 157 | 158 | init(supportedTypes: [ActivityTypeName], models: [Cache.Model], depth: Int) { 159 | self.supportedTypes = supportedTypes 160 | self.depth = depth 161 | self.models = models 162 | } 163 | } 164 | 165 | -------------------------------------------------------------------------------- /LocoKit/Timelines/ActivityTypes/ActivityTypeTrainable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ActivityTypeTrainable.swift 3 | // LocoKitCore 4 | // 5 | // Created by Matt Greenfield on 20/08/17. 6 | // Copyright © 2017 Big Paua. All rights reserved. 7 | // 8 | 9 | public protocol ActivityTypeTrainable: ActivityTypeClassifiable { 10 | 11 | var confirmedType: ActivityTypeName? { get set } 12 | var classifiedType: ActivityTypeName? { get } 13 | 14 | } 15 | -------------------------------------------------------------------------------- /LocoKit/Timelines/ActivityTypes/ActivityTypesCache.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ActivityTypesCache.swift 3 | // LocoKitCore 4 | // 5 | // Created by Matt Greenfield on 30/07/17. 6 | // Copyright © 2017 Big Paua. All rights reserved. 7 | // 8 | 9 | import os.log 10 | import CoreLocation 11 | import GRDB 12 | 13 | public final class ActivityTypesCache: MLModelSource { 14 | 15 | public typealias Model = ActivityType 16 | public typealias ParentClassifier = ActivityTypeClassifier 17 | 18 | public static var highlander = ActivityTypesCache() 19 | 20 | internal static let minimumRefetchWait: TimeInterval = .oneHour 21 | internal static let staleLastUpdatedAge: TimeInterval = .oneMonth * 2 22 | internal static let staleLastFetchedAge: TimeInterval = .oneWeek 23 | 24 | public var store: TimelineStore? 25 | let mutex = UnfairLock() 26 | 27 | public init() {} 28 | 29 | public var providesDepths = [0, 1, 2] 30 | 31 | public func modelFor(name: ActivityTypeName, coordinate: CLLocationCoordinate2D, depth: Int) -> ActivityType? { 32 | guard let store = store else { return nil } 33 | guard providesDepths.contains(depth) else { return nil } 34 | 35 | var query = "SELECT * FROM ActivityTypeModel WHERE isShared = 1 AND name = ? AND depth = ?" 36 | var arguments: [DatabaseValueConvertible] = [name.rawValue, depth] 37 | 38 | if depth > 0 { 39 | query += " AND latitudeMin <= ? AND latitudeMax >= ? AND longitudeMin <= ? AND longitudeMax >= ?" 40 | arguments.append(coordinate.latitude) 41 | arguments.append(coordinate.latitude) 42 | arguments.append(coordinate.longitude) 43 | arguments.append(coordinate.longitude) 44 | } 45 | 46 | return store.model(for: query, arguments: StatementArguments(arguments)) 47 | } 48 | 49 | public func modelsFor(names: [ActivityTypeName], coordinate: CLLocationCoordinate2D, depth: Int) -> [ActivityType] { 50 | guard let store = store else { return [] } 51 | guard providesDepths.contains(depth) else { return [] } 52 | 53 | var query = "SELECT * FROM ActivityTypeModel WHERE isShared = 1 AND depth = ?" 54 | var arguments: [DatabaseValueConvertible] = [depth] 55 | 56 | let marks = repeatElement("?", count: names.count).joined(separator: ",") 57 | query += " AND name IN (\(marks))" 58 | arguments += names.map { $0.rawValue } as [DatabaseValueConvertible] 59 | 60 | if depth > 0 { 61 | query += " AND latitudeMin <= ? AND latitudeMax >= ? AND longitudeMin <= ? AND longitudeMax >= ?" 62 | arguments.append(coordinate.latitude) 63 | arguments.append(coordinate.latitude) 64 | arguments.append(coordinate.longitude) 65 | arguments.append(coordinate.longitude) 66 | } 67 | 68 | let models = store.models(for: query, arguments: StatementArguments(arguments)) 69 | 70 | // start a new fetch if needed 71 | if models.isEmpty || models.isStale { 72 | fetchTypesFor(coordinate: coordinate, depth: depth) 73 | } 74 | 75 | // if not D2, only return base types (all extended types are coordinate bound) 76 | if depth < 2 { return models.filter { ActivityTypeName.baseTypes.contains($0.name) } } 77 | 78 | return models 79 | } 80 | 81 | // MARK: - Remote model fetching 82 | 83 | func fetchTypesFor(coordinate: CLLocationCoordinate2D, depth: Int) { 84 | let latRange = ActivityType.latitudeRangeFor(depth: depth, coordinate: coordinate) 85 | let lngRange = ActivityType.longitudeRangeFor(depth: depth, coordinate: coordinate) 86 | let latWidth = latRange.max - latRange.min 87 | let lngWidth = lngRange.max - lngRange.min 88 | let depthCenter = CLLocationCoordinate2D(latitude: latRange.min + latWidth * 0.5, 89 | longitude: lngRange.min + lngWidth * 0.5) 90 | 91 | LocoKitService.fetchModelsFor(coordinate: depthCenter, depth: depth) { json in 92 | if let json = json { self.parseTypes(json: json) } 93 | } 94 | } 95 | 96 | func parseTypes(json: [String: Any]) { 97 | guard let store = store else { return } 98 | guard let typeDicts = json["activityTypes"] as? [[String: Any]] else { return } 99 | 100 | for dict in typeDicts { 101 | let model = ActivityType(dict: dict, in: store) 102 | model?.save() 103 | } 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /LocoKit/Timelines/ActivityTypes/ClassifierResultItem.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ClassifierResultItem.swift 3 | // LocoKitCore 4 | // 5 | // Created by Matt Greenfield on 13/10/17. 6 | // Copyright © 2017 Big Paua. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public enum ClassifierResultScoreGroup: Int { 12 | case perfect = 5 13 | case veryGood = 4 14 | case good = 3 15 | case bad = 2 16 | case veryBad = 1 17 | case terrible = 0 18 | } 19 | 20 | /** 21 | An individual result row in a `ClassifierResults` instance, for a single activity type. 22 | */ 23 | public struct ClassifierResultItem: Equatable { 24 | 25 | /** 26 | The activity type name for the result. 27 | */ 28 | public let name: ActivityTypeName 29 | 30 | /** 31 | The match probability score for the result, in the range of 0.0 to 1.0 (0% match to 100% match). 32 | */ 33 | public let score: Double 34 | 35 | public let modelAccuracyScore: Double? 36 | 37 | public init(name: ActivityTypeName, score: Double, modelAccuracyScore: Double? = nil) { 38 | self.name = name 39 | self.score = score 40 | self.modelAccuracyScore = modelAccuracyScore 41 | } 42 | 43 | public func normalisedScore(in results: ClassifierResults) -> Double { 44 | let scoresTotal = results.scoresTotal 45 | guard scoresTotal > 0 else { return 0 } 46 | return score / scoresTotal 47 | } 48 | 49 | public func normalisedScoreGroup(in results: ClassifierResults) -> ClassifierResultScoreGroup { 50 | let normalisedScore = self.normalisedScore(in: results) 51 | switch Int(round(normalisedScore * 100)) { 52 | case 100: return .perfect 53 | case 80...100: return .veryGood 54 | case 50...80: return .good 55 | case 20...50: return .bad 56 | case 1...20: return .veryBad 57 | default: return .terrible 58 | } 59 | } 60 | 61 | /** 62 | Result items are considered equal if they have matching `name` values. 63 | */ 64 | public static func ==(lhs: ClassifierResultItem, rhs: ClassifierResultItem) -> Bool { 65 | return lhs.name == rhs.name 66 | } 67 | 68 | } 69 | -------------------------------------------------------------------------------- /LocoKit/Timelines/ActivityTypes/ClassifierResults.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ClassifierResults.swift 3 | // LocoKitCore 4 | // 5 | // Created by Matt Greenfield on 29/08/17. 6 | // Copyright © 2017 Big Paua. All rights reserved. 7 | // 8 | 9 | /** 10 | The results of a call to `classify(_:types:)` on an `ActivityTypeClassifier`. 11 | 12 | Classifier Results are an iterable sequence of `ClassifierResultItem` rows, with each row representing a single 13 | `ActivityTypeName` and its match probability score. 14 | 15 | The results are ordered from best match to worst match, thus the first result row represents the best match for the 16 | given sample. 17 | 18 | ## Using The Results 19 | 20 | The simplest way to use the results is to take the first result row (ie the best match) and ignore the rest. 21 | 22 | ```swift 23 | let results = classifier.classify(sample) 24 | 25 | let bestMatch = results.first 26 | ``` 27 | 28 | You could also iterate through the results, in order from best match to worst match. 29 | 30 | ```swift 31 | for result in results { 32 | print("name: \(result.name) score: \(result.score)") 33 | } 34 | ``` 35 | 36 | If you want to know the probability score of a specific type, you could extract that result row by `ActivityTypeName`: 37 | 38 | ```swift 39 | let walkingResult = results[.walking] 40 | ``` 41 | 42 | If you want the first and second result rows: 43 | 44 | ```swift 45 | let firstResult = results[0] 46 | let secondResult = results[1] 47 | ``` 48 | 49 | ## Interpreting Classifier Results 50 | 51 | Two key indicators can help to interpret the probability scores. The first being the most obvious: a higher score 52 | indicates a better match. 53 | 54 | The second, and perhaps more important indicator, is the ratio of the best match's score to the second best match's 55 | score. 56 | 57 | For example if the first result row has a probability score of 0.9 (a 90% match) while the second result row's score 58 | is 0.1 (a 10% match), that indicates that the best match is nine times more probable than the second best match 59 | (`0.9 / 0.1 = 9.0`). However if the second row's score where instead 0.8, the first row would only be 1.125 times more 60 | probable than the second (`0.9 / 0.8 = 1.125`). 61 | 62 | The ratio between the first and second best matches can be loosely considered a "confidence" score. Thus the 63 | `0.9 / 0.1 = 9.0` example gives a confidence score of 9.0, whilst the second example of `0.9 / 0.8 = 1.125` gives 64 | a much lower confidence score of 1.125. 65 | 66 | A real world example might be results that have "car" and "bus" as the top two results. If both types achieve a high 67 | probability score, but the scores are close together, that indicates there is high confidence that the type is either 68 | car or bus, but low confidence of knowing which one of the two it is. 69 | 70 | The easiest way to apply these two metrics is with simple thresholds. For example a raw score threshold of 0.01 71 | and a first-to-second-match ratio threshold of 2.0. If the first match falls below these thresholds, you could consider 72 | it an "uncertain" match. Although which kinds of thresholds to use will depend heavily on the application. 73 | */ 74 | public struct ClassifierResults: Sequence, IteratorProtocol { 75 | 76 | internal let results: [ClassifierResultItem] 77 | 78 | public init(results: [ClassifierResultItem], moreComing: Bool) { 79 | self.results = results.sorted { $0.score > $1.score } 80 | self.moreComing = moreComing 81 | } 82 | 83 | public init(confirmedType: ActivityTypeName) { 84 | var resultItems = [ClassifierResultItem(name: confirmedType, score: 1)] 85 | for activityType in ActivityTypeName.allTypes where activityType != confirmedType { 86 | resultItems.append(ClassifierResultItem(name: activityType, score: 0)) 87 | } 88 | self.results = resultItems 89 | self.moreComing = false 90 | } 91 | 92 | private lazy var arrayIterator: IndexingIterator> = { 93 | return self.results.makeIterator() 94 | }() 95 | 96 | /** 97 | Indicates that the classifier does not yet have all relevant model data, so a subsequent attempt to classify the 98 | same sample again may produce new results with higher accuracy. 99 | 100 | - Note: Classifiers manage the fetching and caching of model data internally, so if the classifier returns results 101 | flagged with `moreComing` it will already have requested the missing model data from the server. Provided a 102 | working internet connection is available, the missing model data should be available in the classifier in less 103 | than a second. 104 | */ 105 | public let moreComing: Bool 106 | 107 | /** 108 | Returns the result rows as a plain array. 109 | */ 110 | public var array: [ClassifierResultItem] { 111 | return results 112 | } 113 | 114 | public var isEmpty: Bool { 115 | return count == 0 116 | } 117 | 118 | public var count: Int { 119 | return results.count 120 | } 121 | 122 | public var best: ClassifierResultItem { 123 | if let first = first, first.score > 0 { return first } 124 | return ClassifierResultItem(name: .unknown, score: 0) 125 | } 126 | 127 | public var first: ClassifierResultItem? { 128 | return self.results.first 129 | } 130 | 131 | public var scoresTotal: Double { 132 | return results.map { $0.score }.sum 133 | } 134 | 135 | // MARK: - 136 | 137 | public subscript(index: Int) -> ClassifierResultItem { 138 | return results[index] 139 | } 140 | 141 | /** 142 | A convenience subscript to enable lookup by `ActivityTypeName`. 143 | 144 | ```swift 145 | let walkingResult = results[.walking] 146 | ``` 147 | */ 148 | public subscript(activityType: ActivityTypeName) -> ClassifierResultItem? { 149 | return results.first { $0.name == activityType } 150 | } 151 | 152 | public mutating func next() -> ClassifierResultItem? { 153 | return arrayIterator.next() 154 | } 155 | } 156 | 157 | public func +(left: ClassifierResults, right: ClassifierResults) -> ClassifierResults { 158 | return ClassifierResults(results: left.array + right.array, moreComing: left.moreComing || right.moreComing) 159 | } 160 | 161 | public func -(left: ClassifierResults, right: ActivityTypeName) -> ClassifierResults { 162 | return ClassifierResults(results: left.array.filter { $0.name != right }, moreComing: left.moreComing) 163 | } 164 | -------------------------------------------------------------------------------- /LocoKit/Timelines/ActivityTypes/MLCompositeClassifier.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MLCompositeClassifier.swift 3 | // Pods 4 | // 5 | // Created by Matt Greenfield on 10/04/18. 6 | // 7 | 8 | import CoreLocation 9 | 10 | public protocol MLCompositeClassifier: class { 11 | 12 | func canClassify(_ coordinate: CLLocationCoordinate2D?) -> Bool 13 | func classify(_ classifiable: ActivityTypeClassifiable, previousResults: ClassifierResults?) -> ClassifierResults? 14 | func classify(_ samples: [ActivityTypeClassifiable], timeout: TimeInterval?) -> ClassifierResults? 15 | func classify(_ timelineItem: TimelineItem, timeout: TimeInterval?) -> ClassifierResults? 16 | func classify(_ segment: ItemSegment, timeout: TimeInterval?) -> ClassifierResults? 17 | 18 | } 19 | -------------------------------------------------------------------------------- /LocoKit/Timelines/ActivityTypes/MLModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MLModel.swift 3 | // LocoKitCore 4 | // 5 | // Created by Matt Greenfield on 12/08/17. 6 | // Copyright © 2017 Big Paua. All rights reserved. 7 | // 8 | 9 | import CoreLocation 10 | 11 | public protocol MLModel: Hashable { 12 | var name: ActivityTypeName { get } 13 | var depth: Int { get } 14 | var totalSamples: Int { get } 15 | var lastFetched: Date { get } 16 | var lastUpdated: Date? { get } 17 | var coverageScore: Double { get } 18 | var accuracyScore: Double? { get } 19 | var completenessScore: Double { get } 20 | var centerCoordinate: CLLocationCoordinate2D { get } 21 | 22 | func contains(coordinate: CLLocationCoordinate2D) -> Bool 23 | func scoreFor(classifiable scorable: ActivityTypeClassifiable, previousResults: ClassifierResults?) -> Double 24 | } 25 | -------------------------------------------------------------------------------- /LocoKit/Timelines/ActivityTypes/MLModelSource.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MLModelCache.swift 3 | // LocoKitCore 4 | // 5 | // Created by Matt Greenfield on 11/08/17. 6 | // Copyright © 2017 Big Paua. All rights reserved. 7 | // 8 | 9 | import CoreLocation 10 | 11 | public protocol MLModelSource { 12 | associatedtype Model: MLModel 13 | associatedtype ParentClassifier: MLClassifier 14 | 15 | static var highlander: Self { get } 16 | var providesDepths: [Int] { get } 17 | func modelFor(name: ActivityTypeName, coordinate: CLLocationCoordinate2D, depth: Int) -> Model? 18 | func modelsFor(names: [ActivityTypeName], coordinate: CLLocationCoordinate2D, depth: Int) -> [Model] 19 | } 20 | 21 | public extension Array where Element: MLModel { 22 | 23 | var completenessScore: Double { 24 | if isEmpty { 25 | return 0 26 | } 27 | var total = 0.0 28 | var modelCount = 0 29 | for model in self where model.name != .bogus { 30 | total += model.completenessScore 31 | modelCount += 1 32 | } 33 | return modelCount > 0 ? total / Double(modelCount) : 0 34 | } 35 | 36 | var accuracyScore: Double? { 37 | var totalScore = 0.0, totalWeight = 0.0 38 | for model in self { 39 | if let score = model.accuracyScore, score >= 0 { 40 | totalScore += score * Double(model.totalSamples) 41 | totalWeight += Double(model.totalSamples) 42 | } 43 | } 44 | return totalWeight > 0 ? totalScore / totalWeight : nil 45 | } 46 | 47 | var lastUpdated: Date? { 48 | var mostRecentUpdate: Date? 49 | for model in self { 50 | if let lastUpdated = model.lastUpdated, mostRecentUpdate == nil || lastUpdated > mostRecentUpdate! { 51 | mostRecentUpdate = lastUpdated 52 | } 53 | } 54 | return mostRecentUpdate 55 | } 56 | 57 | var lastFetched: Date { 58 | var mostRecentFetch = Date.distantPast 59 | for model in self { 60 | if model.lastFetched > mostRecentFetch { 61 | mostRecentFetch = model.lastFetched 62 | } 63 | } 64 | return mostRecentFetch 65 | } 66 | 67 | var missingBaseTypes: [ActivityTypeName] { 68 | let haveTypes = self.map { $0.name } 69 | return ActivityTypeName.baseTypes.filter { !haveTypes.contains($0) } 70 | } 71 | 72 | var isStale: Bool { 73 | if isEmpty { return true } 74 | 75 | // missing a base model? 76 | guard missingBaseTypes.isEmpty else { return true } 77 | 78 | // nil lastUpdated is presumably UD models pending first update 79 | guard let lastUpdated = lastUpdated else { return false } 80 | 81 | // last fetch was too recent? 82 | if lastFetched.age < ActivityTypesCache.minimumRefetchWait { return false } 83 | 84 | // last updated recently enough? 85 | if lastUpdated.age < ActivityTypesCache.staleLastUpdatedAge * completenessScore { return false } 86 | 87 | // last fetched recently enough? 88 | if lastFetched.age < ActivityTypesCache.staleLastFetchedAge * completenessScore { return false } 89 | 90 | return true 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /LocoKit/Timelines/CLPlacemarkCache.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CLPlacemarkCache.swift 3 | // LocoKit 4 | // 5 | // Created by Matt Greenfield on 29/7/18. 6 | // 7 | 8 | import CoreLocation 9 | 10 | public class CLPlacemarkCache { 11 | 12 | private static let cache = NSCache() 13 | 14 | private static let mutex = UnfairLock() 15 | 16 | private static var fetching: Set = [] 17 | 18 | public static func fetchPlacemark(for location: CLLocation, completion: @escaping (CLPlacemark?) -> Void) { 19 | 20 | // have a cached value? use that 21 | if let cached = cache.object(forKey: location) { 22 | completion(cached) 23 | return 24 | } 25 | 26 | let alreadyFetching = mutex.sync { fetching.contains(location.hashValue) } 27 | if alreadyFetching { 28 | completion(nil) 29 | return 30 | } 31 | 32 | mutex.sync { fetching.insert(location.hashValue) } 33 | 34 | CLGeocoder().reverseGeocodeLocation(location) { placemarks, error in 35 | mutex.sync { fetching.remove(location.hashValue) } 36 | 37 | // nil result? nil completion 38 | guard let placemark = placemarks?.first else { 39 | completion(nil) 40 | return 41 | } 42 | 43 | // cache the result and return it 44 | cache.setObject(placemark, forKey: location) 45 | completion(placemark) 46 | } 47 | } 48 | 49 | private init() {} 50 | 51 | } 52 | -------------------------------------------------------------------------------- /LocoKit/Timelines/CoordinateTrust.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CoordinateTrust.swift 3 | // LocoKit 4 | // 5 | // Created by Matt Greenfield on 28/6/19. 6 | // 7 | 8 | import GRDB 9 | import CoreLocation 10 | 11 | class CoordinateTrust: Record, Codable { 12 | 13 | var latitude: CLLocationDegrees 14 | var longitude: CLLocationDegrees 15 | var trustFactor: Double 16 | 17 | // MARK: - 18 | 19 | var coordinate: CLLocationCoordinate2D { 20 | get { 21 | return CLLocationCoordinate2D(latitude: latitude, longitude: longitude) 22 | } 23 | set { 24 | latitude = newValue.latitude 25 | longitude = newValue.longitude 26 | } 27 | } 28 | 29 | // MARK: - Init 30 | 31 | init(coordinate: CLLocationCoordinate2D) { 32 | self.latitude = coordinate.latitude 33 | self.longitude = coordinate.longitude 34 | self.trustFactor = 1 35 | super.init() 36 | } 37 | 38 | // MARK: - Updating 39 | 40 | func update(from samples: [LocomotionSample]) { 41 | let speeds = samples.compactMap { $0.location?.speed }.filter { $0 >= 0 } 42 | let meanSpeed = speeds.mean 43 | 44 | // most common walking speed is 4.4 kmh 45 | // most common running speed is 9.7 kmh 46 | 47 | let maximumDistrust = 5.0 // maximum distrusted stationary speed in kmh 48 | 49 | trustFactor = 1.0 - (meanSpeed.kmh / maximumDistrust).clamped(min: 0, max: 1) 50 | } 51 | 52 | // MARK: - Record 53 | 54 | override class var databaseTableName: String { return "CoordinateTrust" } 55 | 56 | enum Columns: String, ColumnExpression { 57 | case latitude, longitude, trustFactor 58 | } 59 | 60 | required init(row: Row) { 61 | self.latitude = row[Columns.latitude] 62 | self.longitude = row[Columns.longitude] 63 | self.trustFactor = row[Columns.trustFactor] 64 | super.init(row: row) 65 | } 66 | 67 | override func encode(to container: inout PersistenceContainer) { 68 | container[Columns.latitude] = coordinate.latitude 69 | container[Columns.longitude] = coordinate.longitude 70 | container[Columns.trustFactor] = trustFactor 71 | } 72 | 73 | } 74 | -------------------------------------------------------------------------------- /LocoKit/Timelines/CoordinateTrustManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CoordinateTrustManager.swift 3 | // LocoKit 4 | // 5 | // Created by Matt Greenfield on 30/6/19. 6 | // 7 | 8 | import os.log 9 | import CoreLocation 10 | 11 | public class CoordinateTrustManager: TrustAssessor { 12 | 13 | private let cache = NSCache() 14 | public private(set) var lastUpdated: Date? 15 | public let store: TimelineStore 16 | 17 | // MARK: - 18 | 19 | public init(store: TimelineStore) { 20 | self.store = store 21 | } 22 | 23 | // MARK: - Fetching 24 | 25 | public func trustFactorFor(_ coordinate: CLLocationCoordinate2D) -> Double? { 26 | return modelFor(coordinate)?.trustFactor 27 | } 28 | 29 | func modelFor(_ coordinate: CLLocationCoordinate2D) -> CoordinateTrust? { 30 | let rounded = CoordinateTrustManager.roundedCoordinateFor(coordinate) 31 | 32 | // cached? 33 | if let model = cache.object(forKey: rounded) { return model } 34 | 35 | if let model = try? store.auxiliaryPool.read({ 36 | try CoordinateTrust.fetchOne($0, sql: "SELECT * FROM CoordinateTrust WHERE latitude = ? AND longitude = ?", 37 | arguments: [rounded.latitude, rounded.longitude]) 38 | }) { 39 | cache.setObject(model, forKey: rounded) 40 | return model 41 | } 42 | 43 | return nil 44 | } 45 | 46 | // MARK: - 47 | 48 | static func roundedCoordinateFor(_ coordinate: CLLocationCoordinate2D) -> Coordinate { 49 | let rounded = CLLocationCoordinate2D(latitude: round(coordinate.latitude * 10000) / 10000, 50 | longitude: round(coordinate.longitude * 10000) / 10000) 51 | return Coordinate(coordinate: rounded) 52 | } 53 | 54 | // MARK: - Updating 55 | 56 | public func updateTrustFactors() { 57 | // don't update too frequently 58 | if let lastUpdated = lastUpdated, lastUpdated.age < .oneDay { return } 59 | 60 | os_log("CoordinateTrustManager.updateTrustFactors", type: .debug) 61 | 62 | self.lastUpdated = Date() 63 | 64 | // fetch most recent X confirmed stationary samples 65 | let samples = self.store.samples(where: "confirmedType = ? ORDER BY lastSaved DESC LIMIT 2000", arguments: ["stationary"]) 66 | 67 | // collate the samples into coordinate buckets 68 | var buckets: [Coordinate: [LocomotionSample]] = [:] 69 | for sample in samples where sample.hasUsableCoordinate { 70 | guard let coordinate = sample.location?.coordinate else { continue } 71 | 72 | let rounded = CoordinateTrustManager.roundedCoordinateFor(coordinate) 73 | if let samples = buckets[rounded] { 74 | buckets[rounded] = samples + [sample] 75 | } else { 76 | buckets[rounded] = [sample] 77 | } 78 | } 79 | 80 | // for each bucket, fetch/create the model 81 | var models: [CoordinateTrust] = [] 82 | for (coordinate, samples) in buckets { 83 | let model: CoordinateTrust 84 | if let trust = self.modelFor(coordinate.coordinate) { 85 | model = trust 86 | } else { 87 | model = CoordinateTrust(coordinate: coordinate.coordinate) 88 | } 89 | models.append(model) 90 | 91 | // update the model's trustFactor 92 | model.update(from: samples) 93 | } 94 | 95 | // save/update the models 96 | do { 97 | try self.store.auxiliaryPool.write { db in 98 | for model in models { 99 | try model.save(db) 100 | } 101 | } 102 | } catch { 103 | print("ERROR: \(error)") 104 | } 105 | } 106 | 107 | } 108 | 109 | class Coordinate: NSObject { 110 | 111 | let latitude: CLLocationDegrees 112 | let longitude: CLLocationDegrees 113 | var coordinate: CLLocationCoordinate2D { return CLLocationCoordinate2D(latitude: latitude, longitude: longitude) } 114 | 115 | init(coordinate: CLLocationCoordinate2D) { 116 | self.latitude = coordinate.latitude 117 | self.longitude = coordinate.longitude 118 | } 119 | 120 | // MARK: - Hashable 121 | 122 | override func isEqual(_ object: Any?) -> Bool { 123 | guard let other = object as? Coordinate else { return false } 124 | return other.latitude == latitude && other.longitude == longitude 125 | } 126 | 127 | override var hash: Int { 128 | return latitude.hashValue ^ latitude.hashValue 129 | } 130 | 131 | } 132 | -------------------------------------------------------------------------------- /LocoKit/Timelines/ItemsObserver.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ItemsObserver.swift 3 | // LocoKit 4 | // 5 | // Created by Matt Greenfield on 11/9/18. 6 | // 7 | 8 | import Foundation 9 | import os.log 10 | import GRDB 11 | 12 | class ItemsObserver: TransactionObserver { 13 | 14 | var store: TimelineStore 15 | var changedRowIds: Set = [] 16 | 17 | init(store: TimelineStore) { 18 | self.store = store 19 | } 20 | 21 | // observe updates to next/prev item links 22 | func observes(eventsOfKind eventKind: DatabaseEventKind) -> Bool { 23 | switch eventKind { 24 | case .update(let tableName, let columnNames): 25 | guard tableName == "TimelineItem" else { return false } 26 | let itemEdges: Set = ["previousItemId", "nextItemId"] 27 | return itemEdges.intersection(columnNames).count > 0 28 | default: return false 29 | } 30 | } 31 | 32 | func databaseDidChange(with event: DatabaseEvent) { 33 | changedRowIds.insert(event.rowID) 34 | } 35 | 36 | func databaseDidCommit(_ db: Database) { 37 | let rowIds: Set = store.mutex.sync { 38 | let rowIds = changedRowIds 39 | changedRowIds = [] 40 | return rowIds 41 | } 42 | 43 | if rowIds.isEmpty { return } 44 | 45 | /** maintain the timeline items linked list locally, for changes made outside the managed environment **/ 46 | 47 | do { 48 | let marks = repeatElement("?", count: rowIds.count).joined(separator: ",") 49 | let query = "SELECT itemId, previousItemId, nextItemId FROM TimelineItem WHERE rowId IN (\(marks))" 50 | let rows = try Row.fetchCursor(db, sql: query, arguments: StatementArguments(rowIds)) 51 | 52 | while let row = try rows.next() { 53 | let previousItemIdString = row["previousItemId"] as String? 54 | let nextItemIdString = row["nextItemId"] as String? 55 | 56 | guard let uuidString = row["itemId"] as String?, let itemId = UUID(uuidString: uuidString) else { continue } 57 | guard let item = store.object(for: itemId) as? TimelineItem else { continue } 58 | 59 | if let uuidString = previousItemIdString, item.previousItemId?.uuidString != uuidString { 60 | item.previousItemId = UUID(uuidString: uuidString) 61 | 62 | } else if previousItemIdString == nil && item.previousItemId != nil { 63 | item.previousItemId = nil 64 | } 65 | 66 | if let uuidString = nextItemIdString, item.nextItemId?.uuidString != uuidString { 67 | item.nextItemId = UUID(uuidString: uuidString) 68 | 69 | } else if nextItemIdString == nil && item.nextItemId != nil { 70 | item.nextItemId = nil 71 | } 72 | } 73 | 74 | } catch { 75 | os_log("SQL Exception: %@", error.localizedDescription) 76 | } 77 | } 78 | 79 | func databaseDidRollback(_ db: Database) {} 80 | } 81 | -------------------------------------------------------------------------------- /LocoKit/Timelines/Merge.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Matt Greenfield on 25/05/16. 3 | // Copyright (c) 2016 Big Paua. All rights reserved. 4 | // 5 | 6 | import os.log 7 | import Foundation 8 | 9 | public extension NSNotification.Name { 10 | static let mergedTimelineItems = Notification.Name("mergedTimelineItems") 11 | } 12 | 13 | typealias MergeScore = ConsumptionScore 14 | public typealias MergeResult = (kept: TimelineItem, killed: [TimelineItem]) 15 | 16 | internal class Merge: Hashable, CustomStringConvertible { 17 | 18 | var keeper: TimelineItem 19 | var betweener: TimelineItem? 20 | var deadman: TimelineItem 21 | 22 | var isValid: Bool { 23 | if keeper.deleted || deadman.deleted || betweener?.deleted == true { return false } 24 | if keeper.invalidated || deadman.invalidated || betweener?.invalidated == true { return false } 25 | 26 | // check for dupes (which should be impossible, but weird stuff happens) 27 | var itemIds: Set = [keeper.itemId, deadman.itemId] 28 | if let betweener = betweener { 29 | itemIds.insert(betweener.itemId) 30 | if itemIds.count != 3 { return false } 31 | } else { 32 | if itemIds.count != 2 { return false } 33 | } 34 | 35 | if let betweener = betweener { 36 | // keeper -> betweener -> deadman 37 | if keeper.nextItem == betweener, betweener.nextItem == deadman { return true } 38 | // deadman -> betweener -> keeper 39 | if deadman.nextItem == betweener, betweener.nextItem == keeper { return true } 40 | } else { 41 | // keeper -> deadman 42 | if keeper.nextItem == deadman { return true } 43 | // deadman -> keeper 44 | if deadman.nextItem == keeper { return true } 45 | } 46 | 47 | return false 48 | } 49 | 50 | lazy var score: MergeScore = { 51 | if keeper.isMergeLocked || deadman.isMergeLocked || betweener?.isMergeLocked == true { return .impossible } 52 | guard isValid else { return .impossible } 53 | return self.keeper.scoreForConsuming(item: self.deadman) 54 | }() 55 | 56 | init(keeper: TimelineItem, betweener: TimelineItem? = nil, deadman: TimelineItem) { 57 | self.keeper = keeper 58 | self.deadman = deadman 59 | if let betweener = betweener { 60 | self.betweener = betweener 61 | } 62 | } 63 | 64 | @discardableResult func doIt() -> MergeResult { 65 | let description = String(describing: self) 66 | if TimelineProcessor.debugLogging { os_log("Doing:\n%@", type: .debug, description) } 67 | 68 | merge(deadman, into: keeper) 69 | 70 | let results: MergeResult 71 | if let betweener = betweener { 72 | results = (kept: keeper, killed: [deadman, betweener]) 73 | } else { 74 | results = (kept: keeper, killed: [deadman]) 75 | } 76 | 77 | // notify listeners 78 | let note = Notification(name: .mergedTimelineItems, object: self, 79 | userInfo: ["description": description, "results": results]) 80 | NotificationCenter.default.post(note) 81 | 82 | return results 83 | } 84 | 85 | private func merge(_ deadman: TimelineItem, into keeper: TimelineItem) { 86 | guard isValid else { os_log("Invalid merge", type: .error); return } 87 | 88 | // deadman is previous 89 | if keeper.previousItem == deadman || (betweener != nil && keeper.previousItem == betweener) { 90 | keeper.previousItem = deadman.previousItem 91 | 92 | // deadman is next 93 | } else if keeper.nextItem == deadman || (betweener != nil && keeper.nextItem == betweener) { 94 | keeper.nextItem = deadman.nextItem 95 | 96 | } else { 97 | return 98 | } 99 | 100 | // deal with a betweener 101 | if let betweener = betweener { 102 | keeper.willConsume(item: betweener) 103 | keeper.add(betweener.samples) 104 | betweener.delete() 105 | } 106 | 107 | // deal with the deadman 108 | keeper.willConsume(item: deadman) 109 | keeper.add(deadman.samples) 110 | deadman.delete() 111 | } 112 | 113 | // MARK: - Hashable 114 | 115 | func hash(into hasher: inout Hasher) { 116 | hasher.combine(keeper) 117 | hasher.combine(deadman) 118 | if let betweener = betweener { 119 | hasher.combine(betweener) 120 | } 121 | if let startDate = keeper.startDate { 122 | hasher.combine(startDate) 123 | } 124 | } 125 | 126 | static func == (lhs: Merge, rhs: Merge) -> Bool { 127 | return lhs.hashValue == rhs.hashValue 128 | } 129 | 130 | // MARK: - CustomStringConvertible 131 | 132 | var description: String { 133 | if let betweener = betweener { 134 | return String(format: "score: %d (%@) <- (%@) <- (%@)", score.rawValue, String(describing: keeper), 135 | String(describing: betweener), String(describing: deadman)) 136 | } else { 137 | return String(format: "score: %d (%@) <- (%@)", score.rawValue, String(describing: keeper), 138 | String(describing: deadman)) 139 | } 140 | } 141 | 142 | } 143 | -------------------------------------------------------------------------------- /LocoKit/Timelines/MergeScores.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MergeScores.swift 3 | // LearnerCoacher 4 | // 5 | // Created by Matt Greenfield on 15/12/16. 6 | // Copyright © 2016 Big Paua. All rights reserved. 7 | // 8 | 9 | import os.log 10 | import Foundation 11 | 12 | public enum ConsumptionScore: Int { 13 | case perfect = 5 14 | case high = 4 15 | case medium = 3 16 | case low = 2 17 | case veryLow = 1 18 | case impossible = 0 19 | } 20 | 21 | class MergeScores { 22 | 23 | // MARK: - SOMETHING <- SOMETHING 24 | static func consumptionScoreFor(_ consumer: TimelineItem, toConsume consumee: TimelineItem) -> ConsumptionScore { 25 | 26 | // can't do anything with merge locked items 27 | if consumer.isMergeLocked || consumee.isMergeLocked { return .impossible } 28 | 29 | // deadmen can't consume anyone 30 | if consumer.deleted { return .impossible } 31 | 32 | // if consumee has zero samples, call it a perfect merge 33 | if consumee.samples.isEmpty { return .perfect } 34 | 35 | // if consumer has zero samples, call it impossible 36 | if consumer.samples.isEmpty { return .impossible } 37 | 38 | // data gaps can only consume data gaps 39 | if consumer.isDataGap { return consumee.isDataGap ? .perfect : .impossible } 40 | 41 | // anyone can consume an invalid data gap, but no one can consume a valid data gap 42 | if consumee.isDataGap { return consumee.isInvalid ? .medium : .impossible } 43 | 44 | // nolos can only consume nolos 45 | if consumer.isNolo { return consumee.isNolo ? .perfect : .impossible } 46 | 47 | // anyone can consume an invalid nolo 48 | if consumee.isNolo && consumee.isInvalid { return .medium } 49 | 50 | // test for impossible separation distance 51 | guard consumer.withinMergeableDistance(from: consumee) else { return .impossible } 52 | 53 | // visit <- something 54 | if let visit = consumer as? Visit { return consumptionScoreFor(visit: visit, toConsume: consumee) } 55 | 56 | // path <- something 57 | if let path = consumer as? Path { return consumptionScoreFor(path: path, toConsume: consumee) } 58 | 59 | return .impossible 60 | } 61 | 62 | // MARK: - PATH <- SOMETHING 63 | private static func consumptionScoreFor(path consumer: Path, toConsume consumee: TimelineItem) -> ConsumptionScore { 64 | 65 | // consumer is invalid 66 | if consumer.isInvalid { 67 | 68 | // invalid <- invalid 69 | if consumee.isInvalid { return .veryLow } 70 | 71 | // invalid <- valid 72 | return .impossible 73 | } 74 | 75 | // path <- visit 76 | if let visit = consumee as? Visit { return consumptionScoreFor(path: consumer, toConsumeVisit: visit) } 77 | 78 | // path <- vpath 79 | if let path = consumee as? Path { return consumptionScoreFor(path: consumer, toConsumePath: path) } 80 | 81 | return .impossible 82 | } 83 | 84 | // MARK: - PATH <- VISIT 85 | private static func consumptionScoreFor(path consumer: Path, toConsumeVisit consumee: Visit) -> ConsumptionScore { 86 | 87 | // can't consume a keeper visit 88 | if consumee.isWorthKeeping { return .impossible } 89 | 90 | // consumer is keeper 91 | if consumer.isWorthKeeping { 92 | 93 | // keeper <- invalid 94 | if consumee.isInvalid { return .medium } 95 | 96 | // keeper <- valid 97 | return .low 98 | } 99 | 100 | // consumer is valid 101 | if consumer.isValid { 102 | 103 | // valid <- invalid 104 | if consumee.isInvalid { return .low } 105 | 106 | // valid <- valid 107 | return .veryLow 108 | } 109 | 110 | // consumer is invalid (actually already dealt with in previous method) 111 | return .impossible 112 | } 113 | 114 | // MARK: - PATH <- PATH 115 | private static func consumptionScoreFor(path consumer: Path, toConsumePath consumee: Path) -> ConsumptionScore { 116 | let consumerType = consumer.modeMovingActivityType ?? consumer.modeActivityType 117 | let consumeeType = consumee.modeMovingActivityType ?? consumee.modeActivityType 118 | 119 | // no types means it's a random guess 120 | if consumerType == nil && consumeeType == nil { return .medium } 121 | 122 | // perfect type match 123 | if consumeeType == consumerType { return .perfect } 124 | 125 | // can't consume a keeper path 126 | if consumee.isWorthKeeping { return .impossible } 127 | 128 | // a path with nil type can't consume anyone 129 | guard let scoringType = consumerType else { return .impossible } 130 | 131 | guard let typeResult = consumee.classifierResults?.first(where: { $0.name == scoringType }) else { 132 | return .impossible 133 | } 134 | 135 | // consumee's type score for consumer's type, as a usable Int 136 | let typeScore = Int(floor(typeResult.score * 1000)) 137 | 138 | switch typeScore { 139 | case 75...Int.max: 140 | return .perfect 141 | case 50...75: 142 | return .high 143 | case 25...50: 144 | return .medium 145 | case 10...25: 146 | return .low 147 | default: 148 | return .veryLow 149 | } 150 | } 151 | 152 | // MARK: - VISIT <- SOMETHING 153 | private static func consumptionScoreFor(visit consumer: Visit, toConsume consumee: TimelineItem) -> ConsumptionScore { 154 | 155 | // visit <- visit 156 | if let visit = consumee as? Visit { return consumptionScoreFor(visit: consumer, toConsumeVisit: visit) } 157 | 158 | // visit <- path 159 | if let path = consumee as? Path { return consumptionScoreFor(visit: consumer, toConsumePath: path) } 160 | 161 | return .impossible 162 | } 163 | 164 | // MARK: - VISIT <- VISIT 165 | private static func consumptionScoreFor(visit consumer: Visit, toConsumeVisit consumee: Visit) -> ConsumptionScore { 166 | 167 | // overlapping visits 168 | if consumer.overlaps(consumee) { 169 | return consumer.duration > consumee.duration ? .perfect : .high 170 | } 171 | 172 | return .impossible 173 | } 174 | 175 | // MARK: - VISIT <- PATH 176 | private static func consumptionScoreFor(visit consumer: Visit, toConsumePath consumee: Path) -> ConsumptionScore { 177 | 178 | // percentage of path inside the visit 179 | let pctInsideScore = Int(floor(consumee.percentInside(consumer) * 10)) 180 | 181 | // valid / keeper visit <- invalid path 182 | if consumer.isValid && consumee.isInvalid { 183 | switch pctInsideScore { 184 | case 10: // 100% 185 | return .low 186 | default: 187 | return .veryLow 188 | } 189 | } 190 | 191 | return .impossible 192 | } 193 | 194 | } 195 | -------------------------------------------------------------------------------- /LocoKit/Timelines/TimelineClassifier.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TimelineClassifier.swift 3 | // LocoKit 4 | // 5 | // Created by Matt Greenfield on 30/12/17. 6 | // Copyright © 2017 Big Paua. All rights reserved. 7 | // 8 | 9 | #if canImport(Reachability) 10 | import Reachability 11 | #endif 12 | 13 | public class TimelineClassifier: MLClassifierManager { 14 | 15 | public typealias Classifier = ActivityTypeClassifier 16 | 17 | public let minimumTransportCoverage = 0.10 18 | 19 | public static var highlander = TimelineClassifier() 20 | 21 | public var sampleClassifier: Classifier? 22 | 23 | #if canImport(Reachability) 24 | public let reachability = Reachability()! 25 | #endif 26 | 27 | public let mutex = PThreadMutex(type: .recursive) 28 | 29 | } 30 | -------------------------------------------------------------------------------- /LocoKit/Timelines/TimelineObjects/RowCopy.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RowCopy.swift 3 | // LocoKit 4 | // 5 | // Created by Matt Greenfield on 1/05/18. 6 | // 7 | 8 | import GRDB 9 | 10 | internal class RowCopy: FetchableRecord { 11 | 12 | internal let row: Row 13 | 14 | required init(row: Row) { 15 | self.row = row.copy() 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /LocoKit/Timelines/TimelineObjects/TimelineObject.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TimelineObject.swift 3 | // LocoKit 4 | // 5 | // Created by Matt Greenfield on 27/01/18. 6 | // Copyright © 2018 Big Paua. All rights reserved. 7 | // 8 | 9 | import os.log 10 | import Foundation 11 | import GRDB 12 | 13 | public protocol TimelineObject: class, Encodable, PersistableRecord { 14 | 15 | var objectId: UUID { get } 16 | var source: String { get set } 17 | var store: TimelineStore? { get } 18 | 19 | var transactionDate: Date? { get set } 20 | var lastSaved: Date? { get set } 21 | var unsaved: Bool { get } 22 | var hasChanges: Bool { get set } 23 | var needsSave: Bool { get } 24 | 25 | func save(immediate: Bool) 26 | func save(in db: Database) throws 27 | 28 | var invalidated: Bool { get } 29 | func invalidate() 30 | 31 | } 32 | 33 | public extension TimelineObject { 34 | var unsaved: Bool { return lastSaved == nil } 35 | var needsSave: Bool { return unsaved || hasChanges } 36 | func save(immediate: Bool = false) { store?.save(self, immediate: immediate) } 37 | func save(in db: Database) throws { 38 | if invalidated { os_log(.error, "Can't save changes to an invalid object"); return } 39 | if unsaved { try insert(db) } else if hasChanges { try update(db) } 40 | hasChanges = false 41 | } 42 | static var persistenceConflictPolicy: PersistenceConflictPolicy { 43 | return PersistenceConflictPolicy(insert: .replace, update: .abort) 44 | } 45 | } 46 | 47 | -------------------------------------------------------------------------------- /LocoKitCore.framework.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sobri909/LocoKit/1c3c5f9cb696ce993d26ae180b2eba53f00bae9b/LocoKitCore.framework.zip -------------------------------------------------------------------------------- /LocoKitCore.framework/Headers/LocoKitCore.h: -------------------------------------------------------------------------------- 1 | // 2 | // LocoKit.h 3 | // LocoKitCore 4 | // 5 | // Created by Matt Greenfield on 2/07/17. 6 | // Copyright © 2017 Big Paua. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | //! Project version number for LocoKitCore. 12 | FOUNDATION_EXPORT double LocoKitCoreVersionNumber; 13 | 14 | //! Project version string for LocoKitCore. 15 | FOUNDATION_EXPORT const unsigned char LocoKitCoreVersionString[]; 16 | 17 | -------------------------------------------------------------------------------- /LocoKitCore.framework/Info.plist: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sobri909/LocoKit/1c3c5f9cb696ce993d26ae180b2eba53f00bae9b/LocoKitCore.framework/Info.plist -------------------------------------------------------------------------------- /LocoKitCore.framework/LocoKitCore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sobri909/LocoKit/1c3c5f9cb696ce993d26ae180b2eba53f00bae9b/LocoKitCore.framework/LocoKitCore -------------------------------------------------------------------------------- /LocoKitCore.framework/Modules/LocoKitCore.swiftmodule/arm.swiftdoc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sobri909/LocoKit/1c3c5f9cb696ce993d26ae180b2eba53f00bae9b/LocoKitCore.framework/Modules/LocoKitCore.swiftmodule/arm.swiftdoc -------------------------------------------------------------------------------- /LocoKitCore.framework/Modules/LocoKitCore.swiftmodule/arm.swiftinterface: -------------------------------------------------------------------------------- 1 | // swift-interface-format-version: 1.0 2 | // swift-compiler-version: Apple Swift version 5.1.3 effective-4.2 (swiftlang-1100.0.282.1 clang-1100.0.33.15) 3 | // swift-module-flags: -target armv7-apple-ios10.0 -enable-objc-interop -enable-library-evolution -swift-version 4.2 -enforce-exclusivity=checked -O -module-name LocoKitCore 4 | import Accelerate 5 | import CoreLocation 6 | import CoreMotion 7 | import Darwin 8 | import Foundation 9 | @_exported import LocoKitCore 10 | import Swift 11 | import os.log 12 | import os 13 | public enum CoreMotionActivityTypeName : Swift.String, Swift.Codable { 14 | case unknown 15 | case stationary 16 | case automotive 17 | case walking 18 | case running 19 | case cycling 20 | public static let allTypes: [LocoKitCore.CoreMotionActivityTypeName] 21 | public typealias RawValue = Swift.String 22 | public init?(rawValue: Swift.String) 23 | public var rawValue: Swift.String { 24 | get 25 | } 26 | } 27 | public class ActivityBrain { 28 | public var processHistoricalLocations: Swift.Bool 29 | public static let highlander: LocoKitCore.ActivityBrain 30 | public var presentSample: LocoKitCore.ActivityBrainSample { 31 | get 32 | set 33 | } 34 | public var stationaryPeriodStart: Foundation.Date? 35 | @objc deinit 36 | } 37 | extension ActivityBrain { 38 | public static var historicalLocationsBrain: LocoKitCore.ActivityBrain { 39 | get 40 | } 41 | public func add(rawLocation location: CoreLocation.CLLocation, trustFactor: Swift.Double? = nil) 42 | public func update() 43 | public func freezeTheBrain() 44 | public var movingState: LocoKitCore.MovingState { 45 | get 46 | } 47 | public var horizontalAccuracy: Swift.Double { 48 | get 49 | } 50 | public var kalmanLocation: CoreLocation.CLLocation? { 51 | get 52 | } 53 | public func resetKalmans() 54 | public var kalmanRequiredN: Swift.Double { 55 | get 56 | } 57 | public var speedRequiredN: Swift.Double { 58 | get 59 | } 60 | public var requiredN: Swift.Int { 61 | get 62 | } 63 | public var dynamicMinimumConfidenceN: Swift.Int { 64 | get 65 | } 66 | public func spread(_ locations: [CoreLocation.CLLocation]) -> Foundation.TimeInterval 67 | public func add(pedoData: CoreMotion.CMPedometerData) 68 | public func add(deviceMotion: CoreMotion.CMDeviceMotion) 69 | public func add(cmMotionActivity activity: CoreMotion.CMMotionActivity) 70 | } 71 | public enum MovingState : Swift.String, Swift.Codable { 72 | case moving 73 | case stationary 74 | case uncertain 75 | public typealias RawValue = Swift.String 76 | public init?(rawValue: Swift.String) 77 | public var rawValue: Swift.String { 78 | get 79 | } 80 | } 81 | public class ActivityBrainSample { 82 | public var movingState: LocoKitCore.MovingState 83 | public var rawLocations: [CoreLocation.CLLocation] { 84 | get 85 | } 86 | public var filteredLocations: [CoreLocation.CLLocation] { 87 | get 88 | } 89 | public var date: Foundation.Date { 90 | get 91 | } 92 | public var location: CoreLocation.CLLocation? { 93 | get 94 | } 95 | public var stepHz: Swift.Double? { 96 | get 97 | } 98 | public var coreMotionActivityType: LocoKitCore.CoreMotionActivityTypeName? { 99 | get 100 | } 101 | public var speed: CoreLocation.CLLocationSpeed { 102 | get 103 | } 104 | public var course: CoreLocation.CLLocationDirection { 105 | get 106 | } 107 | public var courseVariance: Swift.Double? { 108 | get 109 | } 110 | public var xyAcceleration: Swift.Double? { 111 | get 112 | } 113 | public var zAcceleration: Swift.Double? { 114 | get 115 | } 116 | @objc deinit 117 | } 118 | postfix operator ′ 119 | public protocol ScopedMutex { 120 | @discardableResult 121 | func sync(execute work: () throws -> R) rethrows -> R 122 | @discardableResult 123 | func trySync(execute work: () throws -> R) rethrows -> R? 124 | } 125 | public protocol RawMutex : LocoKitCore.ScopedMutex { 126 | associatedtype MutexPrimitive 127 | var unsafeMutex: Self.MutexPrimitive { get set } 128 | func unbalancedLock() 129 | func unbalancedTryLock() -> Swift.Bool 130 | func unbalancedUnlock() 131 | } 132 | extension RawMutex { 133 | @discardableResult 134 | public func sync(execute work: () throws -> R) rethrows -> R 135 | @discardableResult 136 | public func trySync(execute work: () throws -> R) rethrows -> R? 137 | } 138 | final public class PThreadMutex : LocoKitCore.RawMutex { 139 | public typealias MutexPrimitive = Darwin.pthread_mutex_t 140 | public enum PThreadMutexType { 141 | case normal 142 | case recursive 143 | public static func == (a: LocoKitCore.PThreadMutex.PThreadMutexType, b: LocoKitCore.PThreadMutex.PThreadMutexType) -> Swift.Bool 144 | public var hashValue: Swift.Int { 145 | get 146 | } 147 | public func hash(into hasher: inout Swift.Hasher) 148 | } 149 | final public var unsafeMutex: Darwin.pthread_mutex_t 150 | public init(type: LocoKitCore.PThreadMutex.PThreadMutexType = .normal) 151 | @objc deinit 152 | final public func unbalancedLock() 153 | final public func unbalancedTryLock() -> Swift.Bool 154 | final public func unbalancedUnlock() 155 | } 156 | final public class UnfairLock : LocoKitCore.RawMutex { 157 | public typealias MutexPrimitive = Darwin.os_unfair_lock 158 | public init() 159 | final public var unsafeMutex: Darwin.os_unfair_lock 160 | final public func unbalancedLock() 161 | final public func unbalancedTryLock() -> Swift.Bool 162 | final public func unbalancedUnlock() 163 | @objc deinit 164 | } 165 | public typealias Radians = Swift.Double 166 | public typealias AccuracyRange = (best: CoreLocation.CLLocationAccuracy, worst: CoreLocation.CLLocationAccuracy) 167 | extension Array where Element : CoreLocation.CLLocation { 168 | public var dateInterval: Foundation.DateInterval? { 169 | get 170 | } 171 | } 172 | infix operator ≅ : ComparisonPrecedence 173 | infix operator • : DefaultPrecedence 174 | public struct LocoKitService { 175 | public static var apiKey: Swift.String? { 176 | get 177 | set(key) 178 | } 179 | public static var deviceToken: Foundation.Data? 180 | public static var requestedWakeupCall: Foundation.Date? { 181 | get 182 | } 183 | public static var requestingWakeupCall: Swift.Bool { 184 | get 185 | } 186 | @discardableResult 187 | public static func requestWakeup(at requestedDate: Foundation.Date) -> Swift.Bool 188 | public static func fetchModelsFor(coordinate: CoreLocation.CLLocationCoordinate2D, depth: Swift.Int, completion: @escaping ([Swift.String : Any]?) -> Swift.Void) 189 | } 190 | extension Date { 191 | public var startOfDay: Foundation.Date { 192 | get 193 | } 194 | public var sinceStartOfDay: Foundation.TimeInterval { 195 | get 196 | } 197 | public var age: Foundation.TimeInterval { 198 | get 199 | } 200 | } 201 | extension LocoKitCore.CoreMotionActivityTypeName : Swift.Equatable {} 202 | extension LocoKitCore.CoreMotionActivityTypeName : Swift.Hashable {} 203 | extension LocoKitCore.CoreMotionActivityTypeName : Swift.RawRepresentable {} 204 | extension LocoKitCore.MovingState : Swift.Hashable {} 205 | extension LocoKitCore.MovingState : Swift.RawRepresentable {} 206 | extension LocoKitCore.PThreadMutex.PThreadMutexType : Swift.Equatable {} 207 | extension LocoKitCore.PThreadMutex.PThreadMutexType : Swift.Hashable {} 208 | -------------------------------------------------------------------------------- /LocoKitCore.framework/Modules/LocoKitCore.swiftmodule/arm.swiftmodule: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sobri909/LocoKit/1c3c5f9cb696ce993d26ae180b2eba53f00bae9b/LocoKitCore.framework/Modules/LocoKitCore.swiftmodule/arm.swiftmodule -------------------------------------------------------------------------------- /LocoKitCore.framework/Modules/LocoKitCore.swiftmodule/arm64-apple-ios.swiftdoc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sobri909/LocoKit/1c3c5f9cb696ce993d26ae180b2eba53f00bae9b/LocoKitCore.framework/Modules/LocoKitCore.swiftmodule/arm64-apple-ios.swiftdoc -------------------------------------------------------------------------------- /LocoKitCore.framework/Modules/LocoKitCore.swiftmodule/arm64-apple-ios.swiftinterface: -------------------------------------------------------------------------------- 1 | // swift-interface-format-version: 1.0 2 | // swift-compiler-version: Apple Swift version 5.1.3 effective-4.2 (swiftlang-1100.0.282.1 clang-1100.0.33.15) 3 | // swift-module-flags: -target arm64-apple-ios10.0 -enable-objc-interop -enable-library-evolution -swift-version 4.2 -enforce-exclusivity=checked -O -module-name LocoKitCore 4 | import Accelerate 5 | import CoreLocation 6 | import CoreMotion 7 | import Darwin 8 | import Foundation 9 | @_exported import LocoKitCore 10 | import Swift 11 | import os.log 12 | import os 13 | public enum CoreMotionActivityTypeName : Swift.String, Swift.Codable { 14 | case unknown 15 | case stationary 16 | case automotive 17 | case walking 18 | case running 19 | case cycling 20 | public static let allTypes: [LocoKitCore.CoreMotionActivityTypeName] 21 | public typealias RawValue = Swift.String 22 | public init?(rawValue: Swift.String) 23 | public var rawValue: Swift.String { 24 | get 25 | } 26 | } 27 | public class ActivityBrain { 28 | public var processHistoricalLocations: Swift.Bool 29 | public static let highlander: LocoKitCore.ActivityBrain 30 | public var presentSample: LocoKitCore.ActivityBrainSample { 31 | get 32 | set 33 | } 34 | public var stationaryPeriodStart: Foundation.Date? 35 | @objc deinit 36 | } 37 | extension ActivityBrain { 38 | public static var historicalLocationsBrain: LocoKitCore.ActivityBrain { 39 | get 40 | } 41 | public func add(rawLocation location: CoreLocation.CLLocation, trustFactor: Swift.Double? = nil) 42 | public func update() 43 | public func freezeTheBrain() 44 | public var movingState: LocoKitCore.MovingState { 45 | get 46 | } 47 | public var horizontalAccuracy: Swift.Double { 48 | get 49 | } 50 | public var kalmanLocation: CoreLocation.CLLocation? { 51 | get 52 | } 53 | public func resetKalmans() 54 | public var kalmanRequiredN: Swift.Double { 55 | get 56 | } 57 | public var speedRequiredN: Swift.Double { 58 | get 59 | } 60 | public var requiredN: Swift.Int { 61 | get 62 | } 63 | public var dynamicMinimumConfidenceN: Swift.Int { 64 | get 65 | } 66 | public func spread(_ locations: [CoreLocation.CLLocation]) -> Foundation.TimeInterval 67 | public func add(pedoData: CoreMotion.CMPedometerData) 68 | public func add(deviceMotion: CoreMotion.CMDeviceMotion) 69 | public func add(cmMotionActivity activity: CoreMotion.CMMotionActivity) 70 | } 71 | public enum MovingState : Swift.String, Swift.Codable { 72 | case moving 73 | case stationary 74 | case uncertain 75 | public typealias RawValue = Swift.String 76 | public init?(rawValue: Swift.String) 77 | public var rawValue: Swift.String { 78 | get 79 | } 80 | } 81 | public class ActivityBrainSample { 82 | public var movingState: LocoKitCore.MovingState 83 | public var rawLocations: [CoreLocation.CLLocation] { 84 | get 85 | } 86 | public var filteredLocations: [CoreLocation.CLLocation] { 87 | get 88 | } 89 | public var date: Foundation.Date { 90 | get 91 | } 92 | public var location: CoreLocation.CLLocation? { 93 | get 94 | } 95 | public var stepHz: Swift.Double? { 96 | get 97 | } 98 | public var coreMotionActivityType: LocoKitCore.CoreMotionActivityTypeName? { 99 | get 100 | } 101 | public var speed: CoreLocation.CLLocationSpeed { 102 | get 103 | } 104 | public var course: CoreLocation.CLLocationDirection { 105 | get 106 | } 107 | public var courseVariance: Swift.Double? { 108 | get 109 | } 110 | public var xyAcceleration: Swift.Double? { 111 | get 112 | } 113 | public var zAcceleration: Swift.Double? { 114 | get 115 | } 116 | @objc deinit 117 | } 118 | postfix operator ′ 119 | public protocol ScopedMutex { 120 | @discardableResult 121 | func sync(execute work: () throws -> R) rethrows -> R 122 | @discardableResult 123 | func trySync(execute work: () throws -> R) rethrows -> R? 124 | } 125 | public protocol RawMutex : LocoKitCore.ScopedMutex { 126 | associatedtype MutexPrimitive 127 | var unsafeMutex: Self.MutexPrimitive { get set } 128 | func unbalancedLock() 129 | func unbalancedTryLock() -> Swift.Bool 130 | func unbalancedUnlock() 131 | } 132 | extension RawMutex { 133 | @discardableResult 134 | public func sync(execute work: () throws -> R) rethrows -> R 135 | @discardableResult 136 | public func trySync(execute work: () throws -> R) rethrows -> R? 137 | } 138 | final public class PThreadMutex : LocoKitCore.RawMutex { 139 | public typealias MutexPrimitive = Darwin.pthread_mutex_t 140 | public enum PThreadMutexType { 141 | case normal 142 | case recursive 143 | public static func == (a: LocoKitCore.PThreadMutex.PThreadMutexType, b: LocoKitCore.PThreadMutex.PThreadMutexType) -> Swift.Bool 144 | public var hashValue: Swift.Int { 145 | get 146 | } 147 | public func hash(into hasher: inout Swift.Hasher) 148 | } 149 | final public var unsafeMutex: Darwin.pthread_mutex_t 150 | public init(type: LocoKitCore.PThreadMutex.PThreadMutexType = .normal) 151 | @objc deinit 152 | final public func unbalancedLock() 153 | final public func unbalancedTryLock() -> Swift.Bool 154 | final public func unbalancedUnlock() 155 | } 156 | final public class UnfairLock : LocoKitCore.RawMutex { 157 | public typealias MutexPrimitive = Darwin.os_unfair_lock 158 | public init() 159 | final public var unsafeMutex: Darwin.os_unfair_lock 160 | final public func unbalancedLock() 161 | final public func unbalancedTryLock() -> Swift.Bool 162 | final public func unbalancedUnlock() 163 | @objc deinit 164 | } 165 | public typealias Radians = Swift.Double 166 | public typealias AccuracyRange = (best: CoreLocation.CLLocationAccuracy, worst: CoreLocation.CLLocationAccuracy) 167 | extension Array where Element : CoreLocation.CLLocation { 168 | public var dateInterval: Foundation.DateInterval? { 169 | get 170 | } 171 | } 172 | infix operator ≅ : ComparisonPrecedence 173 | infix operator • : DefaultPrecedence 174 | public struct LocoKitService { 175 | public static var apiKey: Swift.String? { 176 | get 177 | set(key) 178 | } 179 | public static var deviceToken: Foundation.Data? 180 | public static var requestedWakeupCall: Foundation.Date? { 181 | get 182 | } 183 | public static var requestingWakeupCall: Swift.Bool { 184 | get 185 | } 186 | @discardableResult 187 | public static func requestWakeup(at requestedDate: Foundation.Date) -> Swift.Bool 188 | public static func fetchModelsFor(coordinate: CoreLocation.CLLocationCoordinate2D, depth: Swift.Int, completion: @escaping ([Swift.String : Any]?) -> Swift.Void) 189 | } 190 | extension Date { 191 | public var startOfDay: Foundation.Date { 192 | get 193 | } 194 | public var sinceStartOfDay: Foundation.TimeInterval { 195 | get 196 | } 197 | public var age: Foundation.TimeInterval { 198 | get 199 | } 200 | } 201 | extension LocoKitCore.CoreMotionActivityTypeName : Swift.Equatable {} 202 | extension LocoKitCore.CoreMotionActivityTypeName : Swift.Hashable {} 203 | extension LocoKitCore.CoreMotionActivityTypeName : Swift.RawRepresentable {} 204 | extension LocoKitCore.MovingState : Swift.Hashable {} 205 | extension LocoKitCore.MovingState : Swift.RawRepresentable {} 206 | extension LocoKitCore.PThreadMutex.PThreadMutexType : Swift.Equatable {} 207 | extension LocoKitCore.PThreadMutex.PThreadMutexType : Swift.Hashable {} 208 | -------------------------------------------------------------------------------- /LocoKitCore.framework/Modules/LocoKitCore.swiftmodule/arm64-apple-ios.swiftmodule: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sobri909/LocoKit/1c3c5f9cb696ce993d26ae180b2eba53f00bae9b/LocoKitCore.framework/Modules/LocoKitCore.swiftmodule/arm64-apple-ios.swiftmodule -------------------------------------------------------------------------------- /LocoKitCore.framework/Modules/LocoKitCore.swiftmodule/arm64.swiftdoc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sobri909/LocoKit/1c3c5f9cb696ce993d26ae180b2eba53f00bae9b/LocoKitCore.framework/Modules/LocoKitCore.swiftmodule/arm64.swiftdoc -------------------------------------------------------------------------------- /LocoKitCore.framework/Modules/LocoKitCore.swiftmodule/arm64.swiftinterface: -------------------------------------------------------------------------------- 1 | // swift-interface-format-version: 1.0 2 | // swift-compiler-version: Apple Swift version 5.1.3 effective-4.2 (swiftlang-1100.0.282.1 clang-1100.0.33.15) 3 | // swift-module-flags: -target arm64-apple-ios10.0 -enable-objc-interop -enable-library-evolution -swift-version 4.2 -enforce-exclusivity=checked -O -module-name LocoKitCore 4 | import Accelerate 5 | import CoreLocation 6 | import CoreMotion 7 | import Darwin 8 | import Foundation 9 | @_exported import LocoKitCore 10 | import Swift 11 | import os.log 12 | import os 13 | public enum CoreMotionActivityTypeName : Swift.String, Swift.Codable { 14 | case unknown 15 | case stationary 16 | case automotive 17 | case walking 18 | case running 19 | case cycling 20 | public static let allTypes: [LocoKitCore.CoreMotionActivityTypeName] 21 | public typealias RawValue = Swift.String 22 | public init?(rawValue: Swift.String) 23 | public var rawValue: Swift.String { 24 | get 25 | } 26 | } 27 | public class ActivityBrain { 28 | public var processHistoricalLocations: Swift.Bool 29 | public static let highlander: LocoKitCore.ActivityBrain 30 | public var presentSample: LocoKitCore.ActivityBrainSample { 31 | get 32 | set 33 | } 34 | public var stationaryPeriodStart: Foundation.Date? 35 | @objc deinit 36 | } 37 | extension ActivityBrain { 38 | public static var historicalLocationsBrain: LocoKitCore.ActivityBrain { 39 | get 40 | } 41 | public func add(rawLocation location: CoreLocation.CLLocation, trustFactor: Swift.Double? = nil) 42 | public func update() 43 | public func freezeTheBrain() 44 | public var movingState: LocoKitCore.MovingState { 45 | get 46 | } 47 | public var horizontalAccuracy: Swift.Double { 48 | get 49 | } 50 | public var kalmanLocation: CoreLocation.CLLocation? { 51 | get 52 | } 53 | public func resetKalmans() 54 | public var kalmanRequiredN: Swift.Double { 55 | get 56 | } 57 | public var speedRequiredN: Swift.Double { 58 | get 59 | } 60 | public var requiredN: Swift.Int { 61 | get 62 | } 63 | public var dynamicMinimumConfidenceN: Swift.Int { 64 | get 65 | } 66 | public func spread(_ locations: [CoreLocation.CLLocation]) -> Foundation.TimeInterval 67 | public func add(pedoData: CoreMotion.CMPedometerData) 68 | public func add(deviceMotion: CoreMotion.CMDeviceMotion) 69 | public func add(cmMotionActivity activity: CoreMotion.CMMotionActivity) 70 | } 71 | public enum MovingState : Swift.String, Swift.Codable { 72 | case moving 73 | case stationary 74 | case uncertain 75 | public typealias RawValue = Swift.String 76 | public init?(rawValue: Swift.String) 77 | public var rawValue: Swift.String { 78 | get 79 | } 80 | } 81 | public class ActivityBrainSample { 82 | public var movingState: LocoKitCore.MovingState 83 | public var rawLocations: [CoreLocation.CLLocation] { 84 | get 85 | } 86 | public var filteredLocations: [CoreLocation.CLLocation] { 87 | get 88 | } 89 | public var date: Foundation.Date { 90 | get 91 | } 92 | public var location: CoreLocation.CLLocation? { 93 | get 94 | } 95 | public var stepHz: Swift.Double? { 96 | get 97 | } 98 | public var coreMotionActivityType: LocoKitCore.CoreMotionActivityTypeName? { 99 | get 100 | } 101 | public var speed: CoreLocation.CLLocationSpeed { 102 | get 103 | } 104 | public var course: CoreLocation.CLLocationDirection { 105 | get 106 | } 107 | public var courseVariance: Swift.Double? { 108 | get 109 | } 110 | public var xyAcceleration: Swift.Double? { 111 | get 112 | } 113 | public var zAcceleration: Swift.Double? { 114 | get 115 | } 116 | @objc deinit 117 | } 118 | postfix operator ′ 119 | public protocol ScopedMutex { 120 | @discardableResult 121 | func sync(execute work: () throws -> R) rethrows -> R 122 | @discardableResult 123 | func trySync(execute work: () throws -> R) rethrows -> R? 124 | } 125 | public protocol RawMutex : LocoKitCore.ScopedMutex { 126 | associatedtype MutexPrimitive 127 | var unsafeMutex: Self.MutexPrimitive { get set } 128 | func unbalancedLock() 129 | func unbalancedTryLock() -> Swift.Bool 130 | func unbalancedUnlock() 131 | } 132 | extension RawMutex { 133 | @discardableResult 134 | public func sync(execute work: () throws -> R) rethrows -> R 135 | @discardableResult 136 | public func trySync(execute work: () throws -> R) rethrows -> R? 137 | } 138 | final public class PThreadMutex : LocoKitCore.RawMutex { 139 | public typealias MutexPrimitive = Darwin.pthread_mutex_t 140 | public enum PThreadMutexType { 141 | case normal 142 | case recursive 143 | public static func == (a: LocoKitCore.PThreadMutex.PThreadMutexType, b: LocoKitCore.PThreadMutex.PThreadMutexType) -> Swift.Bool 144 | public var hashValue: Swift.Int { 145 | get 146 | } 147 | public func hash(into hasher: inout Swift.Hasher) 148 | } 149 | final public var unsafeMutex: Darwin.pthread_mutex_t 150 | public init(type: LocoKitCore.PThreadMutex.PThreadMutexType = .normal) 151 | @objc deinit 152 | final public func unbalancedLock() 153 | final public func unbalancedTryLock() -> Swift.Bool 154 | final public func unbalancedUnlock() 155 | } 156 | final public class UnfairLock : LocoKitCore.RawMutex { 157 | public typealias MutexPrimitive = Darwin.os_unfair_lock 158 | public init() 159 | final public var unsafeMutex: Darwin.os_unfair_lock 160 | final public func unbalancedLock() 161 | final public func unbalancedTryLock() -> Swift.Bool 162 | final public func unbalancedUnlock() 163 | @objc deinit 164 | } 165 | public typealias Radians = Swift.Double 166 | public typealias AccuracyRange = (best: CoreLocation.CLLocationAccuracy, worst: CoreLocation.CLLocationAccuracy) 167 | extension Array where Element : CoreLocation.CLLocation { 168 | public var dateInterval: Foundation.DateInterval? { 169 | get 170 | } 171 | } 172 | infix operator ≅ : ComparisonPrecedence 173 | infix operator • : DefaultPrecedence 174 | public struct LocoKitService { 175 | public static var apiKey: Swift.String? { 176 | get 177 | set(key) 178 | } 179 | public static var deviceToken: Foundation.Data? 180 | public static var requestedWakeupCall: Foundation.Date? { 181 | get 182 | } 183 | public static var requestingWakeupCall: Swift.Bool { 184 | get 185 | } 186 | @discardableResult 187 | public static func requestWakeup(at requestedDate: Foundation.Date) -> Swift.Bool 188 | public static func fetchModelsFor(coordinate: CoreLocation.CLLocationCoordinate2D, depth: Swift.Int, completion: @escaping ([Swift.String : Any]?) -> Swift.Void) 189 | } 190 | extension Date { 191 | public var startOfDay: Foundation.Date { 192 | get 193 | } 194 | public var sinceStartOfDay: Foundation.TimeInterval { 195 | get 196 | } 197 | public var age: Foundation.TimeInterval { 198 | get 199 | } 200 | } 201 | extension LocoKitCore.CoreMotionActivityTypeName : Swift.Equatable {} 202 | extension LocoKitCore.CoreMotionActivityTypeName : Swift.Hashable {} 203 | extension LocoKitCore.CoreMotionActivityTypeName : Swift.RawRepresentable {} 204 | extension LocoKitCore.MovingState : Swift.Hashable {} 205 | extension LocoKitCore.MovingState : Swift.RawRepresentable {} 206 | extension LocoKitCore.PThreadMutex.PThreadMutexType : Swift.Equatable {} 207 | extension LocoKitCore.PThreadMutex.PThreadMutexType : Swift.Hashable {} 208 | -------------------------------------------------------------------------------- /LocoKitCore.framework/Modules/LocoKitCore.swiftmodule/arm64.swiftmodule: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sobri909/LocoKit/1c3c5f9cb696ce993d26ae180b2eba53f00bae9b/LocoKitCore.framework/Modules/LocoKitCore.swiftmodule/arm64.swiftmodule -------------------------------------------------------------------------------- /LocoKitCore.framework/Modules/LocoKitCore.swiftmodule/armv7-apple-ios.swiftdoc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sobri909/LocoKit/1c3c5f9cb696ce993d26ae180b2eba53f00bae9b/LocoKitCore.framework/Modules/LocoKitCore.swiftmodule/armv7-apple-ios.swiftdoc -------------------------------------------------------------------------------- /LocoKitCore.framework/Modules/LocoKitCore.swiftmodule/armv7-apple-ios.swiftinterface: -------------------------------------------------------------------------------- 1 | // swift-interface-format-version: 1.0 2 | // swift-compiler-version: Apple Swift version 5.1.3 effective-4.2 (swiftlang-1100.0.282.1 clang-1100.0.33.15) 3 | // swift-module-flags: -target armv7-apple-ios10.0 -enable-objc-interop -enable-library-evolution -swift-version 4.2 -enforce-exclusivity=checked -O -module-name LocoKitCore 4 | import Accelerate 5 | import CoreLocation 6 | import CoreMotion 7 | import Darwin 8 | import Foundation 9 | @_exported import LocoKitCore 10 | import Swift 11 | import os.log 12 | import os 13 | public enum CoreMotionActivityTypeName : Swift.String, Swift.Codable { 14 | case unknown 15 | case stationary 16 | case automotive 17 | case walking 18 | case running 19 | case cycling 20 | public static let allTypes: [LocoKitCore.CoreMotionActivityTypeName] 21 | public typealias RawValue = Swift.String 22 | public init?(rawValue: Swift.String) 23 | public var rawValue: Swift.String { 24 | get 25 | } 26 | } 27 | public class ActivityBrain { 28 | public var processHistoricalLocations: Swift.Bool 29 | public static let highlander: LocoKitCore.ActivityBrain 30 | public var presentSample: LocoKitCore.ActivityBrainSample { 31 | get 32 | set 33 | } 34 | public var stationaryPeriodStart: Foundation.Date? 35 | @objc deinit 36 | } 37 | extension ActivityBrain { 38 | public static var historicalLocationsBrain: LocoKitCore.ActivityBrain { 39 | get 40 | } 41 | public func add(rawLocation location: CoreLocation.CLLocation, trustFactor: Swift.Double? = nil) 42 | public func update() 43 | public func freezeTheBrain() 44 | public var movingState: LocoKitCore.MovingState { 45 | get 46 | } 47 | public var horizontalAccuracy: Swift.Double { 48 | get 49 | } 50 | public var kalmanLocation: CoreLocation.CLLocation? { 51 | get 52 | } 53 | public func resetKalmans() 54 | public var kalmanRequiredN: Swift.Double { 55 | get 56 | } 57 | public var speedRequiredN: Swift.Double { 58 | get 59 | } 60 | public var requiredN: Swift.Int { 61 | get 62 | } 63 | public var dynamicMinimumConfidenceN: Swift.Int { 64 | get 65 | } 66 | public func spread(_ locations: [CoreLocation.CLLocation]) -> Foundation.TimeInterval 67 | public func add(pedoData: CoreMotion.CMPedometerData) 68 | public func add(deviceMotion: CoreMotion.CMDeviceMotion) 69 | public func add(cmMotionActivity activity: CoreMotion.CMMotionActivity) 70 | } 71 | public enum MovingState : Swift.String, Swift.Codable { 72 | case moving 73 | case stationary 74 | case uncertain 75 | public typealias RawValue = Swift.String 76 | public init?(rawValue: Swift.String) 77 | public var rawValue: Swift.String { 78 | get 79 | } 80 | } 81 | public class ActivityBrainSample { 82 | public var movingState: LocoKitCore.MovingState 83 | public var rawLocations: [CoreLocation.CLLocation] { 84 | get 85 | } 86 | public var filteredLocations: [CoreLocation.CLLocation] { 87 | get 88 | } 89 | public var date: Foundation.Date { 90 | get 91 | } 92 | public var location: CoreLocation.CLLocation? { 93 | get 94 | } 95 | public var stepHz: Swift.Double? { 96 | get 97 | } 98 | public var coreMotionActivityType: LocoKitCore.CoreMotionActivityTypeName? { 99 | get 100 | } 101 | public var speed: CoreLocation.CLLocationSpeed { 102 | get 103 | } 104 | public var course: CoreLocation.CLLocationDirection { 105 | get 106 | } 107 | public var courseVariance: Swift.Double? { 108 | get 109 | } 110 | public var xyAcceleration: Swift.Double? { 111 | get 112 | } 113 | public var zAcceleration: Swift.Double? { 114 | get 115 | } 116 | @objc deinit 117 | } 118 | postfix operator ′ 119 | public protocol ScopedMutex { 120 | @discardableResult 121 | func sync(execute work: () throws -> R) rethrows -> R 122 | @discardableResult 123 | func trySync(execute work: () throws -> R) rethrows -> R? 124 | } 125 | public protocol RawMutex : LocoKitCore.ScopedMutex { 126 | associatedtype MutexPrimitive 127 | var unsafeMutex: Self.MutexPrimitive { get set } 128 | func unbalancedLock() 129 | func unbalancedTryLock() -> Swift.Bool 130 | func unbalancedUnlock() 131 | } 132 | extension RawMutex { 133 | @discardableResult 134 | public func sync(execute work: () throws -> R) rethrows -> R 135 | @discardableResult 136 | public func trySync(execute work: () throws -> R) rethrows -> R? 137 | } 138 | final public class PThreadMutex : LocoKitCore.RawMutex { 139 | public typealias MutexPrimitive = Darwin.pthread_mutex_t 140 | public enum PThreadMutexType { 141 | case normal 142 | case recursive 143 | public static func == (a: LocoKitCore.PThreadMutex.PThreadMutexType, b: LocoKitCore.PThreadMutex.PThreadMutexType) -> Swift.Bool 144 | public var hashValue: Swift.Int { 145 | get 146 | } 147 | public func hash(into hasher: inout Swift.Hasher) 148 | } 149 | final public var unsafeMutex: Darwin.pthread_mutex_t 150 | public init(type: LocoKitCore.PThreadMutex.PThreadMutexType = .normal) 151 | @objc deinit 152 | final public func unbalancedLock() 153 | final public func unbalancedTryLock() -> Swift.Bool 154 | final public func unbalancedUnlock() 155 | } 156 | final public class UnfairLock : LocoKitCore.RawMutex { 157 | public typealias MutexPrimitive = Darwin.os_unfair_lock 158 | public init() 159 | final public var unsafeMutex: Darwin.os_unfair_lock 160 | final public func unbalancedLock() 161 | final public func unbalancedTryLock() -> Swift.Bool 162 | final public func unbalancedUnlock() 163 | @objc deinit 164 | } 165 | public typealias Radians = Swift.Double 166 | public typealias AccuracyRange = (best: CoreLocation.CLLocationAccuracy, worst: CoreLocation.CLLocationAccuracy) 167 | extension Array where Element : CoreLocation.CLLocation { 168 | public var dateInterval: Foundation.DateInterval? { 169 | get 170 | } 171 | } 172 | infix operator ≅ : ComparisonPrecedence 173 | infix operator • : DefaultPrecedence 174 | public struct LocoKitService { 175 | public static var apiKey: Swift.String? { 176 | get 177 | set(key) 178 | } 179 | public static var deviceToken: Foundation.Data? 180 | public static var requestedWakeupCall: Foundation.Date? { 181 | get 182 | } 183 | public static var requestingWakeupCall: Swift.Bool { 184 | get 185 | } 186 | @discardableResult 187 | public static func requestWakeup(at requestedDate: Foundation.Date) -> Swift.Bool 188 | public static func fetchModelsFor(coordinate: CoreLocation.CLLocationCoordinate2D, depth: Swift.Int, completion: @escaping ([Swift.String : Any]?) -> Swift.Void) 189 | } 190 | extension Date { 191 | public var startOfDay: Foundation.Date { 192 | get 193 | } 194 | public var sinceStartOfDay: Foundation.TimeInterval { 195 | get 196 | } 197 | public var age: Foundation.TimeInterval { 198 | get 199 | } 200 | } 201 | extension LocoKitCore.CoreMotionActivityTypeName : Swift.Equatable {} 202 | extension LocoKitCore.CoreMotionActivityTypeName : Swift.Hashable {} 203 | extension LocoKitCore.CoreMotionActivityTypeName : Swift.RawRepresentable {} 204 | extension LocoKitCore.MovingState : Swift.Hashable {} 205 | extension LocoKitCore.MovingState : Swift.RawRepresentable {} 206 | extension LocoKitCore.PThreadMutex.PThreadMutexType : Swift.Equatable {} 207 | extension LocoKitCore.PThreadMutex.PThreadMutexType : Swift.Hashable {} 208 | -------------------------------------------------------------------------------- /LocoKitCore.framework/Modules/LocoKitCore.swiftmodule/armv7-apple-ios.swiftmodule: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sobri909/LocoKit/1c3c5f9cb696ce993d26ae180b2eba53f00bae9b/LocoKitCore.framework/Modules/LocoKitCore.swiftmodule/armv7-apple-ios.swiftmodule -------------------------------------------------------------------------------- /LocoKitCore.framework/Modules/LocoKitCore.swiftmodule/armv7.swiftdoc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sobri909/LocoKit/1c3c5f9cb696ce993d26ae180b2eba53f00bae9b/LocoKitCore.framework/Modules/LocoKitCore.swiftmodule/armv7.swiftdoc -------------------------------------------------------------------------------- /LocoKitCore.framework/Modules/LocoKitCore.swiftmodule/armv7.swiftinterface: -------------------------------------------------------------------------------- 1 | // swift-interface-format-version: 1.0 2 | // swift-compiler-version: Apple Swift version 5.1.3 effective-4.2 (swiftlang-1100.0.282.1 clang-1100.0.33.15) 3 | // swift-module-flags: -target armv7-apple-ios10.0 -enable-objc-interop -enable-library-evolution -swift-version 4.2 -enforce-exclusivity=checked -O -module-name LocoKitCore 4 | import Accelerate 5 | import CoreLocation 6 | import CoreMotion 7 | import Darwin 8 | import Foundation 9 | @_exported import LocoKitCore 10 | import Swift 11 | import os.log 12 | import os 13 | public enum CoreMotionActivityTypeName : Swift.String, Swift.Codable { 14 | case unknown 15 | case stationary 16 | case automotive 17 | case walking 18 | case running 19 | case cycling 20 | public static let allTypes: [LocoKitCore.CoreMotionActivityTypeName] 21 | public typealias RawValue = Swift.String 22 | public init?(rawValue: Swift.String) 23 | public var rawValue: Swift.String { 24 | get 25 | } 26 | } 27 | public class ActivityBrain { 28 | public var processHistoricalLocations: Swift.Bool 29 | public static let highlander: LocoKitCore.ActivityBrain 30 | public var presentSample: LocoKitCore.ActivityBrainSample { 31 | get 32 | set 33 | } 34 | public var stationaryPeriodStart: Foundation.Date? 35 | @objc deinit 36 | } 37 | extension ActivityBrain { 38 | public static var historicalLocationsBrain: LocoKitCore.ActivityBrain { 39 | get 40 | } 41 | public func add(rawLocation location: CoreLocation.CLLocation, trustFactor: Swift.Double? = nil) 42 | public func update() 43 | public func freezeTheBrain() 44 | public var movingState: LocoKitCore.MovingState { 45 | get 46 | } 47 | public var horizontalAccuracy: Swift.Double { 48 | get 49 | } 50 | public var kalmanLocation: CoreLocation.CLLocation? { 51 | get 52 | } 53 | public func resetKalmans() 54 | public var kalmanRequiredN: Swift.Double { 55 | get 56 | } 57 | public var speedRequiredN: Swift.Double { 58 | get 59 | } 60 | public var requiredN: Swift.Int { 61 | get 62 | } 63 | public var dynamicMinimumConfidenceN: Swift.Int { 64 | get 65 | } 66 | public func spread(_ locations: [CoreLocation.CLLocation]) -> Foundation.TimeInterval 67 | public func add(pedoData: CoreMotion.CMPedometerData) 68 | public func add(deviceMotion: CoreMotion.CMDeviceMotion) 69 | public func add(cmMotionActivity activity: CoreMotion.CMMotionActivity) 70 | } 71 | public enum MovingState : Swift.String, Swift.Codable { 72 | case moving 73 | case stationary 74 | case uncertain 75 | public typealias RawValue = Swift.String 76 | public init?(rawValue: Swift.String) 77 | public var rawValue: Swift.String { 78 | get 79 | } 80 | } 81 | public class ActivityBrainSample { 82 | public var movingState: LocoKitCore.MovingState 83 | public var rawLocations: [CoreLocation.CLLocation] { 84 | get 85 | } 86 | public var filteredLocations: [CoreLocation.CLLocation] { 87 | get 88 | } 89 | public var date: Foundation.Date { 90 | get 91 | } 92 | public var location: CoreLocation.CLLocation? { 93 | get 94 | } 95 | public var stepHz: Swift.Double? { 96 | get 97 | } 98 | public var coreMotionActivityType: LocoKitCore.CoreMotionActivityTypeName? { 99 | get 100 | } 101 | public var speed: CoreLocation.CLLocationSpeed { 102 | get 103 | } 104 | public var course: CoreLocation.CLLocationDirection { 105 | get 106 | } 107 | public var courseVariance: Swift.Double? { 108 | get 109 | } 110 | public var xyAcceleration: Swift.Double? { 111 | get 112 | } 113 | public var zAcceleration: Swift.Double? { 114 | get 115 | } 116 | @objc deinit 117 | } 118 | postfix operator ′ 119 | public protocol ScopedMutex { 120 | @discardableResult 121 | func sync(execute work: () throws -> R) rethrows -> R 122 | @discardableResult 123 | func trySync(execute work: () throws -> R) rethrows -> R? 124 | } 125 | public protocol RawMutex : LocoKitCore.ScopedMutex { 126 | associatedtype MutexPrimitive 127 | var unsafeMutex: Self.MutexPrimitive { get set } 128 | func unbalancedLock() 129 | func unbalancedTryLock() -> Swift.Bool 130 | func unbalancedUnlock() 131 | } 132 | extension RawMutex { 133 | @discardableResult 134 | public func sync(execute work: () throws -> R) rethrows -> R 135 | @discardableResult 136 | public func trySync(execute work: () throws -> R) rethrows -> R? 137 | } 138 | final public class PThreadMutex : LocoKitCore.RawMutex { 139 | public typealias MutexPrimitive = Darwin.pthread_mutex_t 140 | public enum PThreadMutexType { 141 | case normal 142 | case recursive 143 | public static func == (a: LocoKitCore.PThreadMutex.PThreadMutexType, b: LocoKitCore.PThreadMutex.PThreadMutexType) -> Swift.Bool 144 | public var hashValue: Swift.Int { 145 | get 146 | } 147 | public func hash(into hasher: inout Swift.Hasher) 148 | } 149 | final public var unsafeMutex: Darwin.pthread_mutex_t 150 | public init(type: LocoKitCore.PThreadMutex.PThreadMutexType = .normal) 151 | @objc deinit 152 | final public func unbalancedLock() 153 | final public func unbalancedTryLock() -> Swift.Bool 154 | final public func unbalancedUnlock() 155 | } 156 | final public class UnfairLock : LocoKitCore.RawMutex { 157 | public typealias MutexPrimitive = Darwin.os_unfair_lock 158 | public init() 159 | final public var unsafeMutex: Darwin.os_unfair_lock 160 | final public func unbalancedLock() 161 | final public func unbalancedTryLock() -> Swift.Bool 162 | final public func unbalancedUnlock() 163 | @objc deinit 164 | } 165 | public typealias Radians = Swift.Double 166 | public typealias AccuracyRange = (best: CoreLocation.CLLocationAccuracy, worst: CoreLocation.CLLocationAccuracy) 167 | extension Array where Element : CoreLocation.CLLocation { 168 | public var dateInterval: Foundation.DateInterval? { 169 | get 170 | } 171 | } 172 | infix operator ≅ : ComparisonPrecedence 173 | infix operator • : DefaultPrecedence 174 | public struct LocoKitService { 175 | public static var apiKey: Swift.String? { 176 | get 177 | set(key) 178 | } 179 | public static var deviceToken: Foundation.Data? 180 | public static var requestedWakeupCall: Foundation.Date? { 181 | get 182 | } 183 | public static var requestingWakeupCall: Swift.Bool { 184 | get 185 | } 186 | @discardableResult 187 | public static func requestWakeup(at requestedDate: Foundation.Date) -> Swift.Bool 188 | public static func fetchModelsFor(coordinate: CoreLocation.CLLocationCoordinate2D, depth: Swift.Int, completion: @escaping ([Swift.String : Any]?) -> Swift.Void) 189 | } 190 | extension Date { 191 | public var startOfDay: Foundation.Date { 192 | get 193 | } 194 | public var sinceStartOfDay: Foundation.TimeInterval { 195 | get 196 | } 197 | public var age: Foundation.TimeInterval { 198 | get 199 | } 200 | } 201 | extension LocoKitCore.CoreMotionActivityTypeName : Swift.Equatable {} 202 | extension LocoKitCore.CoreMotionActivityTypeName : Swift.Hashable {} 203 | extension LocoKitCore.CoreMotionActivityTypeName : Swift.RawRepresentable {} 204 | extension LocoKitCore.MovingState : Swift.Hashable {} 205 | extension LocoKitCore.MovingState : Swift.RawRepresentable {} 206 | extension LocoKitCore.PThreadMutex.PThreadMutexType : Swift.Equatable {} 207 | extension LocoKitCore.PThreadMutex.PThreadMutexType : Swift.Hashable {} 208 | -------------------------------------------------------------------------------- /LocoKitCore.framework/Modules/LocoKitCore.swiftmodule/armv7.swiftmodule: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sobri909/LocoKit/1c3c5f9cb696ce993d26ae180b2eba53f00bae9b/LocoKitCore.framework/Modules/LocoKitCore.swiftmodule/armv7.swiftmodule -------------------------------------------------------------------------------- /LocoKitCore.framework/Modules/LocoKitCore.swiftmodule/i386-apple-ios-simulator.swiftdoc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sobri909/LocoKit/1c3c5f9cb696ce993d26ae180b2eba53f00bae9b/LocoKitCore.framework/Modules/LocoKitCore.swiftmodule/i386-apple-ios-simulator.swiftdoc -------------------------------------------------------------------------------- /LocoKitCore.framework/Modules/LocoKitCore.swiftmodule/i386-apple-ios-simulator.swiftmodule: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sobri909/LocoKit/1c3c5f9cb696ce993d26ae180b2eba53f00bae9b/LocoKitCore.framework/Modules/LocoKitCore.swiftmodule/i386-apple-ios-simulator.swiftmodule -------------------------------------------------------------------------------- /LocoKitCore.framework/Modules/LocoKitCore.swiftmodule/i386.swiftdoc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sobri909/LocoKit/1c3c5f9cb696ce993d26ae180b2eba53f00bae9b/LocoKitCore.framework/Modules/LocoKitCore.swiftmodule/i386.swiftdoc -------------------------------------------------------------------------------- /LocoKitCore.framework/Modules/LocoKitCore.swiftmodule/i386.swiftinterface: -------------------------------------------------------------------------------- 1 | // swift-interface-format-version: 1.0 2 | // swift-compiler-version: Apple Swift version 5.1.3 effective-4.2 (swiftlang-1100.0.282.1 clang-1100.0.33.15) 3 | // swift-module-flags: -target i386-apple-ios10.0-simulator -enable-objc-interop -enable-library-evolution -swift-version 4.2 -enforce-exclusivity=checked -O -module-name LocoKitCore 4 | import Accelerate 5 | import CoreLocation 6 | import CoreMotion 7 | import Darwin 8 | import Foundation 9 | @_exported import LocoKitCore 10 | import Swift 11 | import os.log 12 | import os 13 | public enum CoreMotionActivityTypeName : Swift.String, Swift.Codable { 14 | case unknown 15 | case stationary 16 | case automotive 17 | case walking 18 | case running 19 | case cycling 20 | public static let allTypes: [LocoKitCore.CoreMotionActivityTypeName] 21 | public typealias RawValue = Swift.String 22 | public init?(rawValue: Swift.String) 23 | public var rawValue: Swift.String { 24 | get 25 | } 26 | } 27 | public class ActivityBrain { 28 | public var processHistoricalLocations: Swift.Bool 29 | public static let highlander: LocoKitCore.ActivityBrain 30 | public var presentSample: LocoKitCore.ActivityBrainSample { 31 | get 32 | set 33 | } 34 | public var stationaryPeriodStart: Foundation.Date? 35 | @objc deinit 36 | } 37 | extension ActivityBrain { 38 | public static var historicalLocationsBrain: LocoKitCore.ActivityBrain { 39 | get 40 | } 41 | public func add(rawLocation location: CoreLocation.CLLocation, trustFactor: Swift.Double? = nil) 42 | public func update() 43 | public func freezeTheBrain() 44 | public var movingState: LocoKitCore.MovingState { 45 | get 46 | } 47 | public var horizontalAccuracy: Swift.Double { 48 | get 49 | } 50 | public var kalmanLocation: CoreLocation.CLLocation? { 51 | get 52 | } 53 | public func resetKalmans() 54 | public var kalmanRequiredN: Swift.Double { 55 | get 56 | } 57 | public var speedRequiredN: Swift.Double { 58 | get 59 | } 60 | public var requiredN: Swift.Int { 61 | get 62 | } 63 | public var dynamicMinimumConfidenceN: Swift.Int { 64 | get 65 | } 66 | public func spread(_ locations: [CoreLocation.CLLocation]) -> Foundation.TimeInterval 67 | public func add(pedoData: CoreMotion.CMPedometerData) 68 | public func add(deviceMotion: CoreMotion.CMDeviceMotion) 69 | public func add(cmMotionActivity activity: CoreMotion.CMMotionActivity) 70 | } 71 | public enum MovingState : Swift.String, Swift.Codable { 72 | case moving 73 | case stationary 74 | case uncertain 75 | public typealias RawValue = Swift.String 76 | public init?(rawValue: Swift.String) 77 | public var rawValue: Swift.String { 78 | get 79 | } 80 | } 81 | public class ActivityBrainSample { 82 | public var movingState: LocoKitCore.MovingState 83 | public var rawLocations: [CoreLocation.CLLocation] { 84 | get 85 | } 86 | public var filteredLocations: [CoreLocation.CLLocation] { 87 | get 88 | } 89 | public var date: Foundation.Date { 90 | get 91 | } 92 | public var location: CoreLocation.CLLocation? { 93 | get 94 | } 95 | public var stepHz: Swift.Double? { 96 | get 97 | } 98 | public var coreMotionActivityType: LocoKitCore.CoreMotionActivityTypeName? { 99 | get 100 | } 101 | public var speed: CoreLocation.CLLocationSpeed { 102 | get 103 | } 104 | public var course: CoreLocation.CLLocationDirection { 105 | get 106 | } 107 | public var courseVariance: Swift.Double? { 108 | get 109 | } 110 | public var xyAcceleration: Swift.Double? { 111 | get 112 | } 113 | public var zAcceleration: Swift.Double? { 114 | get 115 | } 116 | @objc deinit 117 | } 118 | postfix operator ′ 119 | public protocol ScopedMutex { 120 | @discardableResult 121 | func sync(execute work: () throws -> R) rethrows -> R 122 | @discardableResult 123 | func trySync(execute work: () throws -> R) rethrows -> R? 124 | } 125 | public protocol RawMutex : LocoKitCore.ScopedMutex { 126 | associatedtype MutexPrimitive 127 | var unsafeMutex: Self.MutexPrimitive { get set } 128 | func unbalancedLock() 129 | func unbalancedTryLock() -> Swift.Bool 130 | func unbalancedUnlock() 131 | } 132 | extension RawMutex { 133 | @discardableResult 134 | public func sync(execute work: () throws -> R) rethrows -> R 135 | @discardableResult 136 | public func trySync(execute work: () throws -> R) rethrows -> R? 137 | } 138 | final public class PThreadMutex : LocoKitCore.RawMutex { 139 | public typealias MutexPrimitive = Darwin.pthread_mutex_t 140 | public enum PThreadMutexType { 141 | case normal 142 | case recursive 143 | public static func == (a: LocoKitCore.PThreadMutex.PThreadMutexType, b: LocoKitCore.PThreadMutex.PThreadMutexType) -> Swift.Bool 144 | public var hashValue: Swift.Int { 145 | get 146 | } 147 | public func hash(into hasher: inout Swift.Hasher) 148 | } 149 | final public var unsafeMutex: Darwin.pthread_mutex_t 150 | public init(type: LocoKitCore.PThreadMutex.PThreadMutexType = .normal) 151 | @objc deinit 152 | final public func unbalancedLock() 153 | final public func unbalancedTryLock() -> Swift.Bool 154 | final public func unbalancedUnlock() 155 | } 156 | final public class UnfairLock : LocoKitCore.RawMutex { 157 | public typealias MutexPrimitive = Darwin.os_unfair_lock 158 | public init() 159 | final public var unsafeMutex: Darwin.os_unfair_lock 160 | final public func unbalancedLock() 161 | final public func unbalancedTryLock() -> Swift.Bool 162 | final public func unbalancedUnlock() 163 | @objc deinit 164 | } 165 | public typealias Radians = Swift.Double 166 | public typealias AccuracyRange = (best: CoreLocation.CLLocationAccuracy, worst: CoreLocation.CLLocationAccuracy) 167 | extension Array where Element : CoreLocation.CLLocation { 168 | public var dateInterval: Foundation.DateInterval? { 169 | get 170 | } 171 | } 172 | infix operator ≅ : ComparisonPrecedence 173 | infix operator • : DefaultPrecedence 174 | public struct LocoKitService { 175 | public static var apiKey: Swift.String? { 176 | get 177 | set(key) 178 | } 179 | public static var deviceToken: Foundation.Data? 180 | public static var requestedWakeupCall: Foundation.Date? { 181 | get 182 | } 183 | public static var requestingWakeupCall: Swift.Bool { 184 | get 185 | } 186 | @discardableResult 187 | public static func requestWakeup(at requestedDate: Foundation.Date) -> Swift.Bool 188 | public static func fetchModelsFor(coordinate: CoreLocation.CLLocationCoordinate2D, depth: Swift.Int, completion: @escaping ([Swift.String : Any]?) -> Swift.Void) 189 | } 190 | extension Date { 191 | public var startOfDay: Foundation.Date { 192 | get 193 | } 194 | public var sinceStartOfDay: Foundation.TimeInterval { 195 | get 196 | } 197 | public var age: Foundation.TimeInterval { 198 | get 199 | } 200 | } 201 | extension LocoKitCore.CoreMotionActivityTypeName : Swift.Equatable {} 202 | extension LocoKitCore.CoreMotionActivityTypeName : Swift.Hashable {} 203 | extension LocoKitCore.CoreMotionActivityTypeName : Swift.RawRepresentable {} 204 | extension LocoKitCore.MovingState : Swift.Hashable {} 205 | extension LocoKitCore.MovingState : Swift.RawRepresentable {} 206 | extension LocoKitCore.PThreadMutex.PThreadMutexType : Swift.Equatable {} 207 | extension LocoKitCore.PThreadMutex.PThreadMutexType : Swift.Hashable {} 208 | -------------------------------------------------------------------------------- /LocoKitCore.framework/Modules/LocoKitCore.swiftmodule/i386.swiftmodule: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sobri909/LocoKit/1c3c5f9cb696ce993d26ae180b2eba53f00bae9b/LocoKitCore.framework/Modules/LocoKitCore.swiftmodule/i386.swiftmodule -------------------------------------------------------------------------------- /LocoKitCore.framework/Modules/LocoKitCore.swiftmodule/x86_64-apple-ios-simulator.swiftdoc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sobri909/LocoKit/1c3c5f9cb696ce993d26ae180b2eba53f00bae9b/LocoKitCore.framework/Modules/LocoKitCore.swiftmodule/x86_64-apple-ios-simulator.swiftdoc -------------------------------------------------------------------------------- /LocoKitCore.framework/Modules/LocoKitCore.swiftmodule/x86_64-apple-ios-simulator.swiftmodule: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sobri909/LocoKit/1c3c5f9cb696ce993d26ae180b2eba53f00bae9b/LocoKitCore.framework/Modules/LocoKitCore.swiftmodule/x86_64-apple-ios-simulator.swiftmodule -------------------------------------------------------------------------------- /LocoKitCore.framework/Modules/LocoKitCore.swiftmodule/x86_64.swiftdoc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sobri909/LocoKit/1c3c5f9cb696ce993d26ae180b2eba53f00bae9b/LocoKitCore.framework/Modules/LocoKitCore.swiftmodule/x86_64.swiftdoc -------------------------------------------------------------------------------- /LocoKitCore.framework/Modules/LocoKitCore.swiftmodule/x86_64.swiftmodule: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sobri909/LocoKit/1c3c5f9cb696ce993d26ae180b2eba53f00bae9b/LocoKitCore.framework/Modules/LocoKitCore.swiftmodule/x86_64.swiftmodule -------------------------------------------------------------------------------- /LocoKitCore.framework/Modules/module.modulemap: -------------------------------------------------------------------------------- 1 | framework module LocoKitCore { 2 | umbrella header "LocoKitCore.h" 3 | 4 | export * 5 | module * { export * } 6 | } 7 | 8 | module LocoKitCore.Swift { 9 | header "LocoKitCore-Swift.h" 10 | requires objc 11 | } 12 | -------------------------------------------------------------------------------- /LocoKitCore.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | s.name = "LocoKitCore" 3 | s.version = "7.0.0" 4 | s.summary = "Location and activity recording framework" 5 | s.homepage = "https://www.bigpaua.com/locokit/" 6 | s.author = { "Matt Greenfield" => "matt@bigpaua.com" } 7 | s.license = { :text => "Copyright 2018 Matt Greenfield. All rights reserved.", 8 | :type => "Commercial" } 9 | s.source = { :git => 'https://github.com/sobri909/LocoKit.git', :tag => '7.0.0' } 10 | s.frameworks = 'CoreLocation', 'CoreMotion' 11 | s.ios.deployment_target = '10.0' 12 | s.ios.vendored_frameworks = 'LocoKitCore.framework' 13 | end 14 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.3 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "LocoKit", 8 | platforms: [.iOS(.v13)], 9 | products: [ 10 | .library(name: "LocoKit", targets: ["LocoKit"]) 11 | ], 12 | dependencies: [ 13 | .package(url: "https://github.com/alejandro-isaza/Upsurge.git", from: "0.11.0"), 14 | .package(name: "GRDB", url: "https://github.com/groue/GRDB.swift.git", from: "4.0.0") 15 | ], 16 | targets: [ 17 | .target( 18 | name: "LocoKit", 19 | dependencies: ["Upsurge", "GRDB"], 20 | path: "LocoKit" 21 | ) 22 | ] 23 | ) 24 | -------------------------------------------------------------------------------- /Podfile: -------------------------------------------------------------------------------- 1 | target 'LocoKit Demo App' 2 | platform :ios, '10.0' 3 | use_frameworks! 4 | 5 | pod 'LocoKit' 6 | pod 'SwiftNotes' 7 | pod 'Anchorage' 8 | -------------------------------------------------------------------------------- /Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - Anchorage (4.4.0) 3 | - GRDB.swift (4.11.0): 4 | - GRDB.swift/standard (= 4.11.0) 5 | - GRDB.swift/standard (4.11.0) 6 | - LocoKit (7.0.0): 7 | - LocoKit/Base (= 7.0.0) 8 | - LocoKit/Base (7.0.0): 9 | - GRDB.swift (~> 4) 10 | - LocoKitCore (= 7.0.0) 11 | - Upsurge (~> 0.10) 12 | - LocoKitCore (7.0.0) 13 | - SwiftNotes (1.1.0) 14 | - Upsurge (0.10.2) 15 | 16 | DEPENDENCIES: 17 | - Anchorage 18 | - LocoKit 19 | - SwiftNotes 20 | 21 | SPEC REPOS: 22 | trunk: 23 | - Anchorage 24 | - GRDB.swift 25 | - LocoKit 26 | - LocoKitCore 27 | - SwiftNotes 28 | - Upsurge 29 | 30 | SPEC CHECKSUMS: 31 | Anchorage: d7f02c4f9425b537053237aab11ae97e59b55f36 32 | GRDB.swift: 22e9d04cb732dfa9fa4440bc0bbb069ee8195183 33 | LocoKit: 8a06074e90dfd24ee10e94c9f5274e300f6b9d2f 34 | LocoKitCore: 30cff6a1e4ac5a32eefe232c19e24fd79485661d 35 | SwiftNotes: 51d71568d7515c5c82aa791b88900cf8059b8beb 36 | Upsurge: 5866beadc3da27f91c5df4ac795deb3f3238d678 37 | 38 | PODFILE CHECKSUM: c9cb1714b431c31888efb12c1a62351506e334e2 39 | 40 | COCOAPODS: 1.9.0 41 | -------------------------------------------------------------------------------- /Screenshots/raw_plus_smoothed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sobri909/LocoKit/1c3c5f9cb696ce993d26ae180b2eba53f00bae9b/Screenshots/raw_plus_smoothed.png -------------------------------------------------------------------------------- /Screenshots/smoothed_only.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sobri909/LocoKit/1c3c5f9cb696ce993d26ae180b2eba53f00bae9b/Screenshots/smoothed_only.png -------------------------------------------------------------------------------- /Screenshots/smoothed_plus_visits.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sobri909/LocoKit/1c3c5f9cb696ce993d26ae180b2eba53f00bae9b/Screenshots/smoothed_plus_visits.png -------------------------------------------------------------------------------- /Screenshots/stationary.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sobri909/LocoKit/1c3c5f9cb696ce993d26ae180b2eba53f00bae9b/Screenshots/stationary.png -------------------------------------------------------------------------------- /Screenshots/tuktuk_raw.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sobri909/LocoKit/1c3c5f9cb696ce993d26ae180b2eba53f00bae9b/Screenshots/tuktuk_raw.png -------------------------------------------------------------------------------- /Screenshots/tuktuk_smoothed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sobri909/LocoKit/1c3c5f9cb696ce993d26ae180b2eba53f00bae9b/Screenshots/tuktuk_smoothed.png -------------------------------------------------------------------------------- /Screenshots/tuktuk_smoothed_plus_visits.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sobri909/LocoKit/1c3c5f9cb696ce993d26ae180b2eba53f00bae9b/Screenshots/tuktuk_smoothed_plus_visits.png -------------------------------------------------------------------------------- /Screenshots/walking.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sobri909/LocoKit/1c3c5f9cb696ce993d26ae180b2eba53f00bae9b/Screenshots/walking.png -------------------------------------------------------------------------------- /TimelineItemDescription.md: -------------------------------------------------------------------------------- 1 | # TimelineItems 2 | 3 | Each TimelineItem is a high level grouping of samples, representing either a `Visit` or a `Path`, depending on whether the user was stationary or travelling between places. The durations can be as brief as a few seconds and as long as days (eg if the user stays at home for several days). 4 | 5 | Inside each `TimelineItem` there is a time ordered array of `LocomotionSample` samples. These are found in `timelineItem.samples`. The first sample in that array is the sample taken when the timeline item began, and the last sample marks the end of the timeline item. 6 | 7 | LocomotionSamples typically represent between 6 seconds and 30 seconds. If location data accuracy is high, new samples will be produced about every 6 seconds. But if location data accuracy is low, samples can be produced less frequently, due to iOS updating the location less frequently. 8 | 9 | The maximum frequency is configurable, with [TimelineManager.samplesPerMinute](https://www.bigpaua.com/locokit/docs/Classes/TimelineManager.html#/Settings) 10 | 11 | So for something like a Path timeline item, for example a few minutes walk between places, the Path object itself will have an `activityType` of `.walking`, but there are also all the individual samples that make up that path, some of which might not be `.walking`. For example if the user walks for a minute, pauses for a few seconds, then starts walking again, there might be a `.stationary` sample somewhere half way through the array. -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | ArcKit API docs are now hosted on the ArcKit website. 2 | --------------------------------------------------------------------------------