diff --git a/.gitignore b/.gitignore
index d33625c8..125abffa 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,13 +1,75 @@
-xcuserdata
-.idea
-Pods
+# Xcode
+#
+# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
+
+## Build generated
+build/
+DerivedData/
+.DS_Store
+
+## Various settings
+*.pbxuser
+!default.pbxuser
+*.mode1v3
+!default.mode1v3
+*.mode2v3
+!default.mode2v3
+*.perspectivev3
+!default.perspectivev3
+xcuserdata/
+*.xcuserstate
+
+## Other
+*.moved-aside
+*.xccheckout
*.xcscmblueprint
-metadata
+
+## Obj-C/Swift specific
+*.hmap
*.ipa
*.dSYM.zip
-*.mobileprovision
-report.xml
-*.log
-tags
-build
-.DS_Store
\ No newline at end of file
+*.dSYM
+
+## Playgrounds
+timeline.xctimeline
+playground.xcworkspace
+
+# Swift Package Manager
+#
+# Add this line if you want to avoid checking in source code from Swift Package Manager dependencies.
+# Packages/
+# Package.pins
+# Package.resolved
+.build/
+
+# CocoaPods
+#
+# We recommend against adding the Pods directory to your .gitignore. However
+# you should judge for yourself, the pros and cons are mentioned at:
+# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
+#
+Pods/
+
+# Carthage
+#
+# Add this line if you want to avoid checking in source code from Carthage dependencies.
+Carthage/Checkouts
+Carthage/Build
+
+#AppCode
+.idea/
+
+# fastlane
+#
+# It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the
+# screenshots whenever they are needed.
+# For more information about the recommended setup visit:
+# https://docs.fastlane.tools/best-practices/source-control/#source-control
+
+fastlane/report.xml
+fastlane/Preview.html
+fastlane/screenshots/**/*.png
+fastlane/test_output
+
+vendor/
+xcodebuild.log
diff --git a/LocoKit Demo App.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
similarity index 57%
rename from LocoKit Demo App.xcodeproj/project.xcworkspace/contents.xcworkspacedata
rename to .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
index b5edcfaf..919434a6 100644
--- a/LocoKit Demo App.xcodeproj/project.xcworkspace/contents.xcworkspacedata
+++ b/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
@@ -2,6 +2,6 @@
+ location = "self:">
diff --git a/ActivityTypeClassifierExamples.md b/ActivityTypeClassifierExamples.md
deleted file mode 100644
index 1caa961b..00000000
--- a/ActivityTypeClassifierExamples.md
+++ /dev/null
@@ -1,13 +0,0 @@
-# Activity Type Classifier Examples
-
-These screenshots were taken from the ArcKit Demo App. Compile and run the Demo App on device to
-experiment with the SDK and see results in your local area.
-
-### Simple Stationary and Walking Examples
-
-These examples are unimpressive placeholders until I get out of the house tomorrow. It shows the
-classifiers correctly detecting that I'm stationary at home, then walking around my house.
-
-| Stationary | Walking |
-| ---------- | ------- |
-| ![](https://raw.githubusercontent.com/sobri909/ArcKit/master/Screenshots/stationary.png) | ![](https://raw.githubusercontent.com/sobri909/ArcKit/master/Screenshots/walking.png) |
diff --git a/LocoKit Demo App.xcodeproj/project.pbxproj b/LocoKit Demo App.xcodeproj/project.pbxproj
deleted file mode 100644
index 2c744d9a..00000000
--- a/LocoKit Demo App.xcodeproj/project.pbxproj
+++ /dev/null
@@ -1,517 +0,0 @@
-// !$*UTF8*$!
-{
- archiveVersion = 1;
- classes = {
- };
- objectVersion = 46;
- objects = {
-
-/* Begin PBXBuildFile section */
- C91CAF4B1F8B88A300416418 /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C91CAF4A1F8B88A300416418 /* SettingsView.swift */; };
- C95141541FE00B3F00A6A348 /* TimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C95141531FE00B3F00A6A348 /* TimelineView.swift */; };
- C95141561FE015CD00A6A348 /* Settings.swift in Sources */ = {isa = PBXBuildFile; fileRef = C95141551FE015CD00A6A348 /* Settings.swift */; };
- C95644241F5EAABF00E9F428 /* Array.helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = C95644231F5EAABF00E9F428 /* Array.helpers.swift */; };
- C96981A71F3605860021238C /* ToggleBox.swift in Sources */ = {isa = PBXBuildFile; fileRef = C96981A61F3605860021238C /* ToggleBox.swift */; };
- C96981A91F360ADF0021238C /* String.helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = C96981A81F360ADF0021238C /* String.helpers.swift */; };
- C96B3C3E1F35EB590089854F /* UIStackView.helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = C96B3C3D1F35EB590089854F /* UIStackView.helpers.swift */; };
- C9A023D91F18A50D00DFFEBD /* CoreLocation.helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9A023D41F18A50D00DFFEBD /* CoreLocation.helpers.swift */; };
- C9A023DA1F18A50D00DFFEBD /* PathPolyline.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9A023D51F18A50D00DFFEBD /* PathPolyline.swift */; };
- C9A023DB1F18A50D00DFFEBD /* VisitAnnotation.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9A023D61F18A50D00DFFEBD /* VisitAnnotation.swift */; };
- C9A023DC1F18A50D00DFFEBD /* VisitAnnotationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9A023D71F18A50D00DFFEBD /* VisitAnnotationView.swift */; };
- C9A023DD1F18A50D00DFFEBD /* VisitCircle.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9A023D81F18A50D00DFFEBD /* VisitCircle.swift */; };
- C9AD5E3E1FD973AF00F88EAE /* DebugLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9AD5E3D1FD973AF00F88EAE /* DebugLog.swift */; };
- C9B96BDB1FE01CAC00B44F38 /* LocoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9B96BDA1FE01CAC00B44F38 /* LocoView.swift */; };
- C9B96BDD1FE0217F00B44F38 /* ClassifierView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9B96BDC1FE0217F00B44F38 /* ClassifierView.swift */; };
- C9B96BDF1FE0252E00B44F38 /* LogView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9B96BDE1FE0252E00B44F38 /* LogView.swift */; };
- C9B96BE21FE02D4E00B44F38 /* MapView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9B96BE11FE02D4E00B44F38 /* MapView.swift */; };
- C9FE56681F18A35500F941FE /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9FE56671F18A35500F941FE /* AppDelegate.swift */; };
- C9FE566A1F18A35500F941FE /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9FE56691F18A35500F941FE /* ViewController.swift */; };
- C9FE566F1F18A35500F941FE /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = C9FE566E1F18A35500F941FE /* Assets.xcassets */; };
- C9FE56721F18A35500F941FE /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = C9FE56701F18A35500F941FE /* LaunchScreen.storyboard */; };
- D948B26B243395FF9C9B94BF /* Pods_LocoKit_Demo_App.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 410E894751DDA03B6BF17581 /* Pods_LocoKit_Demo_App.framework */; };
-/* End PBXBuildFile section */
-
-/* Begin PBXCopyFilesBuildPhase section */
- C9650DB51F6BD02D00EDF791 /* Embed Frameworks */ = {
- isa = PBXCopyFilesBuildPhase;
- buildActionMask = 2147483647;
- dstPath = "";
- dstSubfolderSpec = 10;
- files = (
- );
- name = "Embed Frameworks";
- runOnlyForDeploymentPostprocessing = 0;
- };
-/* End PBXCopyFilesBuildPhase section */
-
-/* Begin PBXFileReference section */
- 410E894751DDA03B6BF17581 /* Pods_LocoKit_Demo_App.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_LocoKit_Demo_App.framework; sourceTree = BUILT_PRODUCTS_DIR; };
- 99F746E0E74737EE45BCE661 /* Pods-LocoKit Demo App.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-LocoKit Demo App.release.xcconfig"; path = "Pods/Target Support Files/Pods-LocoKit Demo App/Pods-LocoKit Demo App.release.xcconfig"; sourceTree = ""; };
- C91CAF4A1F8B88A300416418 /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = ""; };
- C95141531FE00B3F00A6A348 /* TimelineView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TimelineView.swift; sourceTree = ""; };
- C95141551FE015CD00A6A348 /* Settings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Settings.swift; sourceTree = ""; };
- C95644231F5EAABF00E9F428 /* Array.helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Array.helpers.swift; sourceTree = ""; };
- C96981A61F3605860021238C /* ToggleBox.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ToggleBox.swift; sourceTree = ""; };
- C96981A81F360ADF0021238C /* String.helpers.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = String.helpers.swift; sourceTree = ""; };
- C96B3C3D1F35EB590089854F /* UIStackView.helpers.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIStackView.helpers.swift; sourceTree = ""; };
- C9A023D41F18A50D00DFFEBD /* CoreLocation.helpers.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CoreLocation.helpers.swift; sourceTree = ""; };
- C9A023D51F18A50D00DFFEBD /* PathPolyline.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PathPolyline.swift; sourceTree = ""; };
- C9A023D61F18A50D00DFFEBD /* VisitAnnotation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VisitAnnotation.swift; sourceTree = ""; };
- C9A023D71F18A50D00DFFEBD /* VisitAnnotationView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VisitAnnotationView.swift; sourceTree = ""; };
- C9A023D81F18A50D00DFFEBD /* VisitCircle.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VisitCircle.swift; sourceTree = ""; };
- C9AD5E3D1FD973AF00F88EAE /* DebugLog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DebugLog.swift; sourceTree = ""; };
- C9B96BDA1FE01CAC00B44F38 /* LocoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocoView.swift; sourceTree = ""; };
- C9B96BDC1FE0217F00B44F38 /* ClassifierView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClassifierView.swift; sourceTree = ""; };
- C9B96BDE1FE0252E00B44F38 /* LogView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogView.swift; sourceTree = ""; };
- C9B96BE11FE02D4E00B44F38 /* MapView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapView.swift; sourceTree = ""; };
- C9FE56641F18A35500F941FE /* LocoKit Demo App.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "LocoKit Demo App.app"; sourceTree = BUILT_PRODUCTS_DIR; };
- C9FE56671F18A35500F941FE /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; };
- C9FE56691F18A35500F941FE /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; };
- C9FE566E1F18A35500F941FE /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
- C9FE56711F18A35500F941FE /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; };
- C9FE56731F18A35500F941FE /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
- DCAC0B010930E734D60E4954 /* Pods-LocoKit Demo App.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-LocoKit Demo App.debug.xcconfig"; path = "Pods/Target Support Files/Pods-LocoKit Demo App/Pods-LocoKit Demo App.debug.xcconfig"; sourceTree = ""; };
-/* End PBXFileReference section */
-
-/* Begin PBXFrameworksBuildPhase section */
- C9FE56611F18A35500F941FE /* Frameworks */ = {
- isa = PBXFrameworksBuildPhase;
- buildActionMask = 2147483647;
- files = (
- D948B26B243395FF9C9B94BF /* Pods_LocoKit_Demo_App.framework in Frameworks */,
- );
- runOnlyForDeploymentPostprocessing = 0;
- };
-/* End PBXFrameworksBuildPhase section */
-
-/* Begin PBXGroup section */
- 08E01CC06D1B5C63EFF90DA4 /* Frameworks */ = {
- isa = PBXGroup;
- children = (
- 410E894751DDA03B6BF17581 /* Pods_LocoKit_Demo_App.framework */,
- );
- name = Frameworks;
- sourceTree = "";
- };
- AC8391991FD403448C09FE98 /* Pods */ = {
- isa = PBXGroup;
- children = (
- DCAC0B010930E734D60E4954 /* Pods-LocoKit Demo App.debug.xcconfig */,
- 99F746E0E74737EE45BCE661 /* Pods-LocoKit Demo App.release.xcconfig */,
- );
- name = Pods;
- sourceTree = "";
- };
- C96981AA1F360B160021238C /* Extensions */ = {
- isa = PBXGroup;
- children = (
- C95644231F5EAABF00E9F428 /* Array.helpers.swift */,
- C9A023D41F18A50D00DFFEBD /* CoreLocation.helpers.swift */,
- C96981A81F360ADF0021238C /* String.helpers.swift */,
- C96B3C3D1F35EB590089854F /* UIStackView.helpers.swift */,
- );
- name = Extensions;
- sourceTree = "";
- };
- C9A023DE1F18A51900DFFEBD /* Views */ = {
- isa = PBXGroup;
- children = (
- C9A023D51F18A50D00DFFEBD /* PathPolyline.swift */,
- C96981A61F3605860021238C /* ToggleBox.swift */,
- C9A023D61F18A50D00DFFEBD /* VisitAnnotation.swift */,
- C9A023D71F18A50D00DFFEBD /* VisitAnnotationView.swift */,
- C9A023D81F18A50D00DFFEBD /* VisitCircle.swift */,
- );
- name = Views;
- sourceTree = "";
- };
- C9B96BE01FE0255200B44F38 /* Models */ = {
- isa = PBXGroup;
- children = (
- C9AD5E3D1FD973AF00F88EAE /* DebugLog.swift */,
- C95141551FE015CD00A6A348 /* Settings.swift */,
- );
- name = Models;
- sourceTree = "";
- };
- C9FE565B1F18A35500F941FE = {
- isa = PBXGroup;
- children = (
- C9FE56661F18A35500F941FE /* LocoKit Demo App */,
- C9FE56651F18A35500F941FE /* Products */,
- AC8391991FD403448C09FE98 /* Pods */,
- 08E01CC06D1B5C63EFF90DA4 /* Frameworks */,
- );
- sourceTree = "";
- };
- C9FE56651F18A35500F941FE /* Products */ = {
- isa = PBXGroup;
- children = (
- C9FE56641F18A35500F941FE /* LocoKit Demo App.app */,
- );
- name = Products;
- sourceTree = "";
- };
- C9FE56661F18A35500F941FE /* LocoKit Demo App */ = {
- isa = PBXGroup;
- children = (
- C9FE56671F18A35500F941FE /* AppDelegate.swift */,
- C9FE56691F18A35500F941FE /* ViewController.swift */,
- C9B96BE11FE02D4E00B44F38 /* MapView.swift */,
- C91CAF4A1F8B88A300416418 /* SettingsView.swift */,
- C95141531FE00B3F00A6A348 /* TimelineView.swift */,
- C9B96BDC1FE0217F00B44F38 /* ClassifierView.swift */,
- C9B96BDA1FE01CAC00B44F38 /* LocoView.swift */,
- C9B96BDE1FE0252E00B44F38 /* LogView.swift */,
- C9B96BE01FE0255200B44F38 /* Models */,
- C9A023DE1F18A51900DFFEBD /* Views */,
- C96981AA1F360B160021238C /* Extensions */,
- C9FE56731F18A35500F941FE /* Info.plist */,
- C9FE566E1F18A35500F941FE /* Assets.xcassets */,
- C9FE56701F18A35500F941FE /* LaunchScreen.storyboard */,
- );
- path = "LocoKit Demo App";
- sourceTree = "";
- };
-/* End PBXGroup section */
-
-/* Begin PBXNativeTarget section */
- C9FE56631F18A35500F941FE /* LocoKit Demo App */ = {
- isa = PBXNativeTarget;
- buildConfigurationList = C9FE56761F18A35500F941FE /* Build configuration list for PBXNativeTarget "LocoKit Demo App" */;
- buildPhases = (
- 0E6A0291254168344255F408 /* [CP] Check Pods Manifest.lock */,
- C9FE56601F18A35500F941FE /* Sources */,
- C9FE56611F18A35500F941FE /* Frameworks */,
- C9FE56621F18A35500F941FE /* Resources */,
- ED1FDDEF8FD69960ACDA7050 /* [CP] Embed Pods Frameworks */,
- C9650DB51F6BD02D00EDF791 /* Embed Frameworks */,
- );
- buildRules = (
- );
- dependencies = (
- );
- name = "LocoKit Demo App";
- productName = "ArcKit Demo App";
- productReference = C9FE56641F18A35500F941FE /* LocoKit Demo App.app */;
- productType = "com.apple.product-type.application";
- };
-/* End PBXNativeTarget section */
-
-/* Begin PBXProject section */
- C9FE565C1F18A35500F941FE /* Project object */ = {
- isa = PBXProject;
- attributes = {
- LastSwiftUpdateCheck = 0830;
- LastUpgradeCheck = 1020;
- ORGANIZATIONNAME = "Big Paua";
- TargetAttributes = {
- C9FE56631F18A35500F941FE = {
- CreatedOnToolsVersion = 8.3.3;
- DevelopmentTeam = U5N7VG8DUG;
- LastSwiftMigration = 1000;
- ProvisioningStyle = Automatic;
- SystemCapabilities = {
- com.apple.BackgroundModes = {
- enabled = 1;
- };
- };
- };
- };
- };
- buildConfigurationList = C9FE565F1F18A35500F941FE /* Build configuration list for PBXProject "LocoKit Demo App" */;
- compatibilityVersion = "Xcode 3.2";
- developmentRegion = English;
- hasScannedForEncodings = 0;
- knownRegions = (
- English,
- en,
- Base,
- );
- mainGroup = C9FE565B1F18A35500F941FE;
- productRefGroup = C9FE56651F18A35500F941FE /* Products */;
- projectDirPath = "";
- projectRoot = "";
- targets = (
- C9FE56631F18A35500F941FE /* LocoKit Demo App */,
- );
- };
-/* End PBXProject section */
-
-/* Begin PBXResourcesBuildPhase section */
- C9FE56621F18A35500F941FE /* Resources */ = {
- isa = PBXResourcesBuildPhase;
- buildActionMask = 2147483647;
- files = (
- C9FE56721F18A35500F941FE /* LaunchScreen.storyboard in Resources */,
- C9FE566F1F18A35500F941FE /* Assets.xcassets in Resources */,
- );
- runOnlyForDeploymentPostprocessing = 0;
- };
-/* End PBXResourcesBuildPhase section */
-
-/* Begin PBXShellScriptBuildPhase section */
- 0E6A0291254168344255F408 /* [CP] Check Pods Manifest.lock */ = {
- isa = PBXShellScriptBuildPhase;
- buildActionMask = 2147483647;
- files = (
- );
- inputPaths = (
- "${PODS_PODFILE_DIR_PATH}/Podfile.lock",
- "${PODS_ROOT}/Manifest.lock",
- );
- name = "[CP] Check Pods Manifest.lock";
- outputPaths = (
- "$(DERIVED_FILE_DIR)/Pods-LocoKit Demo App-checkManifestLockResult.txt",
- );
- runOnlyForDeploymentPostprocessing = 0;
- shellPath = /bin/sh;
- shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
- showEnvVarsInLog = 0;
- };
- ED1FDDEF8FD69960ACDA7050 /* [CP] Embed Pods Frameworks */ = {
- isa = PBXShellScriptBuildPhase;
- buildActionMask = 2147483647;
- files = (
- );
- inputPaths = (
- "${PODS_ROOT}/Target Support Files/Pods-LocoKit Demo App/Pods-LocoKit Demo App-frameworks.sh",
- "${BUILT_PRODUCTS_DIR}/Anchorage/Anchorage.framework",
- "${BUILT_PRODUCTS_DIR}/GRDB.swift/GRDB.framework",
- "${BUILT_PRODUCTS_DIR}/LocoKit/LocoKit.framework",
- "${PODS_ROOT}/LocoKitCore/LocoKitCore.framework",
- "${BUILT_PRODUCTS_DIR}/SwiftNotes/SwiftNotes.framework",
- "${BUILT_PRODUCTS_DIR}/Upsurge/Upsurge.framework",
- );
- name = "[CP] Embed Pods Frameworks";
- outputPaths = (
- "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Anchorage.framework",
- "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/GRDB.framework",
- "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/LocoKit.framework",
- "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/LocoKitCore.framework",
- "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SwiftNotes.framework",
- "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Upsurge.framework",
- );
- runOnlyForDeploymentPostprocessing = 0;
- shellPath = /bin/sh;
- shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-LocoKit Demo App/Pods-LocoKit Demo App-frameworks.sh\"\n";
- showEnvVarsInLog = 0;
- };
-/* End PBXShellScriptBuildPhase section */
-
-/* Begin PBXSourcesBuildPhase section */
- C9FE56601F18A35500F941FE /* Sources */ = {
- isa = PBXSourcesBuildPhase;
- buildActionMask = 2147483647;
- files = (
- C91CAF4B1F8B88A300416418 /* SettingsView.swift in Sources */,
- C9B96BDB1FE01CAC00B44F38 /* LocoView.swift in Sources */,
- C95141541FE00B3F00A6A348 /* TimelineView.swift in Sources */,
- C9AD5E3E1FD973AF00F88EAE /* DebugLog.swift in Sources */,
- C96981A71F3605860021238C /* ToggleBox.swift in Sources */,
- C95644241F5EAABF00E9F428 /* Array.helpers.swift in Sources */,
- C9A023DD1F18A50D00DFFEBD /* VisitCircle.swift in Sources */,
- C9B96BDF1FE0252E00B44F38 /* LogView.swift in Sources */,
- C9FE566A1F18A35500F941FE /* ViewController.swift in Sources */,
- C96981A91F360ADF0021238C /* String.helpers.swift in Sources */,
- C9A023D91F18A50D00DFFEBD /* CoreLocation.helpers.swift in Sources */,
- C95141561FE015CD00A6A348 /* Settings.swift in Sources */,
- C9A023DB1F18A50D00DFFEBD /* VisitAnnotation.swift in Sources */,
- C9A023DA1F18A50D00DFFEBD /* PathPolyline.swift in Sources */,
- C9B96BDD1FE0217F00B44F38 /* ClassifierView.swift in Sources */,
- C9B96BE21FE02D4E00B44F38 /* MapView.swift in Sources */,
- C9FE56681F18A35500F941FE /* AppDelegate.swift in Sources */,
- C9A023DC1F18A50D00DFFEBD /* VisitAnnotationView.swift in Sources */,
- C96B3C3E1F35EB590089854F /* UIStackView.helpers.swift in Sources */,
- );
- runOnlyForDeploymentPostprocessing = 0;
- };
-/* End PBXSourcesBuildPhase section */
-
-/* Begin PBXVariantGroup section */
- C9FE56701F18A35500F941FE /* LaunchScreen.storyboard */ = {
- isa = PBXVariantGroup;
- children = (
- C9FE56711F18A35500F941FE /* Base */,
- );
- name = LaunchScreen.storyboard;
- sourceTree = "";
- };
-/* End PBXVariantGroup section */
-
-/* Begin XCBuildConfiguration section */
- C9FE56741F18A35500F941FE /* Debug */ = {
- isa = XCBuildConfiguration;
- buildSettings = {
- ALWAYS_SEARCH_USER_PATHS = NO;
- CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;
- CLANG_ANALYZER_NONNULL = YES;
- CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
- CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
- CLANG_CXX_LIBRARY = "libc++";
- CLANG_ENABLE_MODULES = YES;
- CLANG_ENABLE_OBJC_ARC = YES;
- CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
- CLANG_WARN_BOOL_CONVERSION = YES;
- CLANG_WARN_COMMA = YES;
- CLANG_WARN_CONSTANT_CONVERSION = YES;
- CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
- CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
- CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
- CLANG_WARN_EMPTY_BODY = YES;
- CLANG_WARN_ENUM_CONVERSION = YES;
- CLANG_WARN_INFINITE_RECURSION = YES;
- CLANG_WARN_INT_CONVERSION = YES;
- CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
- CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
- CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
- CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
- CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
- CLANG_WARN_STRICT_PROTOTYPES = YES;
- CLANG_WARN_SUSPICIOUS_MOVE = YES;
- CLANG_WARN_UNREACHABLE_CODE = YES;
- CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
- "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
- COPY_PHASE_STRIP = NO;
- DEBUG_INFORMATION_FORMAT = dwarf;
- ENABLE_STRICT_OBJC_MSGSEND = YES;
- ENABLE_TESTABILITY = YES;
- GCC_C_LANGUAGE_STANDARD = gnu99;
- GCC_DYNAMIC_NO_PIC = NO;
- GCC_NO_COMMON_BLOCKS = YES;
- GCC_OPTIMIZATION_LEVEL = 0;
- GCC_PREPROCESSOR_DEFINITIONS = (
- "DEBUG=1",
- "$(inherited)",
- );
- GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
- GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
- GCC_WARN_UNDECLARED_SELECTOR = YES;
- GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
- GCC_WARN_UNUSED_FUNCTION = YES;
- GCC_WARN_UNUSED_VARIABLE = YES;
- IPHONEOS_DEPLOYMENT_TARGET = 10.3;
- MTL_ENABLE_DEBUG_INFO = YES;
- ONLY_ACTIVE_ARCH = YES;
- SDKROOT = iphoneos;
- SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
- SWIFT_OPTIMIZATION_LEVEL = "-Onone";
- };
- name = Debug;
- };
- C9FE56751F18A35500F941FE /* Release */ = {
- isa = XCBuildConfiguration;
- buildSettings = {
- ALWAYS_SEARCH_USER_PATHS = NO;
- CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;
- CLANG_ANALYZER_NONNULL = YES;
- CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
- CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
- CLANG_CXX_LIBRARY = "libc++";
- CLANG_ENABLE_MODULES = YES;
- CLANG_ENABLE_OBJC_ARC = YES;
- CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
- CLANG_WARN_BOOL_CONVERSION = YES;
- CLANG_WARN_COMMA = YES;
- CLANG_WARN_CONSTANT_CONVERSION = YES;
- CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
- CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
- CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
- CLANG_WARN_EMPTY_BODY = YES;
- CLANG_WARN_ENUM_CONVERSION = YES;
- CLANG_WARN_INFINITE_RECURSION = YES;
- CLANG_WARN_INT_CONVERSION = YES;
- CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
- CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
- CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
- CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
- CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
- CLANG_WARN_STRICT_PROTOTYPES = YES;
- CLANG_WARN_SUSPICIOUS_MOVE = YES;
- CLANG_WARN_UNREACHABLE_CODE = YES;
- CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
- "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
- COPY_PHASE_STRIP = NO;
- DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
- ENABLE_NS_ASSERTIONS = NO;
- ENABLE_STRICT_OBJC_MSGSEND = YES;
- GCC_C_LANGUAGE_STANDARD = gnu99;
- GCC_NO_COMMON_BLOCKS = YES;
- GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
- GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
- GCC_WARN_UNDECLARED_SELECTOR = YES;
- GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
- GCC_WARN_UNUSED_FUNCTION = YES;
- GCC_WARN_UNUSED_VARIABLE = YES;
- IPHONEOS_DEPLOYMENT_TARGET = 10.3;
- MTL_ENABLE_DEBUG_INFO = NO;
- SDKROOT = iphoneos;
- SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule";
- VALIDATE_PRODUCT = YES;
- };
- name = Release;
- };
- C9FE56771F18A35500F941FE /* Debug */ = {
- isa = XCBuildConfiguration;
- baseConfigurationReference = DCAC0B010930E734D60E4954 /* Pods-LocoKit Demo App.debug.xcconfig */;
- buildSettings = {
- ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
- DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
- DEVELOPMENT_TEAM = U5N7VG8DUG;
- FRAMEWORK_SEARCH_PATHS = (
- "$(inherited)",
- "$(PROJECT_DIR)",
- );
- INFOPLIST_FILE = "LocoKit Demo App/Info.plist";
- LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
- PRODUCT_BUNDLE_IDENTIFIER = "com.bigpaua.LocoKit-Demo-App";
- PRODUCT_NAME = "$(TARGET_NAME)";
- SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule";
- SWIFT_VERSION = 4.2;
- };
- name = Debug;
- };
- C9FE56781F18A35500F941FE /* Release */ = {
- isa = XCBuildConfiguration;
- baseConfigurationReference = 99F746E0E74737EE45BCE661 /* Pods-LocoKit Demo App.release.xcconfig */;
- buildSettings = {
- ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
- DEVELOPMENT_TEAM = U5N7VG8DUG;
- FRAMEWORK_SEARCH_PATHS = (
- "$(inherited)",
- "$(PROJECT_DIR)",
- );
- INFOPLIST_FILE = "LocoKit Demo App/Info.plist";
- LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
- ONLY_ACTIVE_ARCH = YES;
- PRODUCT_BUNDLE_IDENTIFIER = "com.bigpaua.LocoKit-Demo-App";
- PRODUCT_NAME = "$(TARGET_NAME)";
- SWIFT_VERSION = 4.2;
- };
- name = Release;
- };
-/* End XCBuildConfiguration section */
-
-/* Begin XCConfigurationList section */
- C9FE565F1F18A35500F941FE /* Build configuration list for PBXProject "LocoKit Demo App" */ = {
- isa = XCConfigurationList;
- buildConfigurations = (
- C9FE56741F18A35500F941FE /* Debug */,
- C9FE56751F18A35500F941FE /* Release */,
- );
- defaultConfigurationIsVisible = 0;
- defaultConfigurationName = Release;
- };
- C9FE56761F18A35500F941FE /* Build configuration list for PBXNativeTarget "LocoKit Demo App" */ = {
- isa = XCConfigurationList;
- buildConfigurations = (
- C9FE56771F18A35500F941FE /* Debug */,
- C9FE56781F18A35500F941FE /* Release */,
- );
- defaultConfigurationIsVisible = 0;
- defaultConfigurationName = Release;
- };
-/* End XCConfigurationList section */
- };
- rootObject = C9FE565C1F18A35500F941FE /* Project object */;
-}
diff --git a/LocoKit Demo App.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/LocoKit Demo App.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
deleted file mode 100644
index 18d98100..00000000
--- a/LocoKit Demo App.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
+++ /dev/null
@@ -1,8 +0,0 @@
-
-
-
-
- IDEDidComputeMac32BitWarning
-
-
-
diff --git a/LocoKit Demo App.xcodeproj/xcshareddata/xcschemes/LocoKit Demo App.xcscheme b/LocoKit Demo App.xcodeproj/xcshareddata/xcschemes/LocoKit Demo App.xcscheme
deleted file mode 100644
index b5745e38..00000000
--- a/LocoKit Demo App.xcodeproj/xcshareddata/xcschemes/LocoKit Demo App.xcscheme
+++ /dev/null
@@ -1,91 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/LocoKit Demo App.xcworkspace/contents.xcworkspacedata b/LocoKit Demo App.xcworkspace/contents.xcworkspacedata
deleted file mode 100644
index 44da02f6..00000000
--- a/LocoKit Demo App.xcworkspace/contents.xcworkspacedata
+++ /dev/null
@@ -1,10 +0,0 @@
-
-
-
-
-
-
-
diff --git a/LocoKit Demo App.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/LocoKit Demo App.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
deleted file mode 100644
index 18d98100..00000000
--- a/LocoKit Demo App.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
+++ /dev/null
@@ -1,8 +0,0 @@
-
-
-
-
- IDEDidComputeMac32BitWarning
-
-
-
diff --git a/LocoKit Demo App/AppDelegate.swift b/LocoKit Demo App/AppDelegate.swift
deleted file mode 100644
index e3bc9ac5..00000000
--- a/LocoKit Demo App/AppDelegate.swift
+++ /dev/null
@@ -1,42 +0,0 @@
-//
-// AppDelegate.swift
-// LocoKit Demo App
-//
-// Created by Matt Greenfield on 10/07/17.
-// Copyright © 2017 Big Paua. All rights reserved.
-//
-
-import LocoKit
-
-@UIApplicationMain
-class AppDelegate: UIResponder, UIApplicationDelegate {
-
- var window: UIWindow?
-
- func application(_ application: UIApplication,
- didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
-
- window = UIWindow(frame: UIScreen.main.bounds)
-
- window?.rootViewController = ViewController()
- window?.makeKeyAndVisible()
-
- DebugLog.deleteLogFile()
-
- return true
- }
-
- func applicationDidEnterBackground(_ application: UIApplication) {
- // request "always" location permission
- LocomotionManager.highlander.requestLocationPermission(background: true)
- }
-
- func applicationDidBecomeActive(_ application: UIApplication) {
- guard let controller = window?.rootViewController as? ViewController else { return }
-
- // update the UI on appear
- controller.updateAllViews()
- }
-
-}
-
diff --git a/LocoKit Demo App/Array.helpers.swift b/LocoKit Demo App/Array.helpers.swift
deleted file mode 100644
index ed25215f..00000000
--- a/LocoKit Demo App/Array.helpers.swift
+++ /dev/null
@@ -1,32 +0,0 @@
-//
-// Array.helpers.swift
-// LocoKit Demo App
-//
-// Created by Matt Greenfield on 5/09/17.
-// Copyright © 2017 Big Paua. All rights reserved.
-//
-
-extension Array where Element: FloatingPoint {
-
- var sum: Element {
- return reduce(0, +)
- }
-
- var mean: Element {
- return isEmpty ? 0 : sum / Element(count)
- }
-
- var variance: Element {
- let mean = self.mean
- let squareDiffs = self.map { value -> Element in
- let diff = value - mean
- return diff * diff
- }
- return squareDiffs.mean
- }
-
- var standardDeviation: Element {
- return variance.squareRoot()
- }
-
-}
diff --git a/LocoKit Demo App/Assets.xcassets/AppIcon.appiconset/Contents.json b/LocoKit Demo App/Assets.xcassets/AppIcon.appiconset/Contents.json
deleted file mode 100644
index 19882d56..00000000
--- a/LocoKit Demo App/Assets.xcassets/AppIcon.appiconset/Contents.json
+++ /dev/null
@@ -1,53 +0,0 @@
-{
- "images" : [
- {
- "idiom" : "iphone",
- "size" : "20x20",
- "scale" : "2x"
- },
- {
- "idiom" : "iphone",
- "size" : "20x20",
- "scale" : "3x"
- },
- {
- "idiom" : "iphone",
- "size" : "29x29",
- "scale" : "2x"
- },
- {
- "idiom" : "iphone",
- "size" : "29x29",
- "scale" : "3x"
- },
- {
- "idiom" : "iphone",
- "size" : "40x40",
- "scale" : "2x"
- },
- {
- "idiom" : "iphone",
- "size" : "40x40",
- "scale" : "3x"
- },
- {
- "idiom" : "iphone",
- "size" : "60x60",
- "scale" : "2x"
- },
- {
- "idiom" : "iphone",
- "size" : "60x60",
- "scale" : "3x"
- },
- {
- "idiom" : "ios-marketing",
- "size" : "1024x1024",
- "scale" : "1x"
- }
- ],
- "info" : {
- "version" : 1,
- "author" : "xcode"
- }
-}
\ No newline at end of file
diff --git a/LocoKit Demo App/Assets.xcassets/Contents.json b/LocoKit Demo App/Assets.xcassets/Contents.json
deleted file mode 100644
index da4a164c..00000000
--- a/LocoKit Demo App/Assets.xcassets/Contents.json
+++ /dev/null
@@ -1,6 +0,0 @@
-{
- "info" : {
- "version" : 1,
- "author" : "xcode"
- }
-}
\ No newline at end of file
diff --git a/LocoKit Demo App/Assets.xcassets/dot.imageset/Contents.json b/LocoKit Demo App/Assets.xcassets/dot.imageset/Contents.json
deleted file mode 100644
index 45f8491a..00000000
--- a/LocoKit Demo App/Assets.xcassets/dot.imageset/Contents.json
+++ /dev/null
@@ -1,21 +0,0 @@
-{
- "images" : [
- {
- "idiom" : "universal",
- "scale" : "1x"
- },
- {
- "idiom" : "universal",
- "filename" : "dot.png",
- "scale" : "2x"
- },
- {
- "idiom" : "universal",
- "scale" : "3x"
- }
- ],
- "info" : {
- "version" : 1,
- "author" : "xcode"
- }
-}
\ No newline at end of file
diff --git a/LocoKit Demo App/Assets.xcassets/dot.imageset/dot.png b/LocoKit Demo App/Assets.xcassets/dot.imageset/dot.png
deleted file mode 100644
index 45d236fd..00000000
Binary files a/LocoKit Demo App/Assets.xcassets/dot.imageset/dot.png and /dev/null differ
diff --git a/LocoKit Demo App/Assets.xcassets/inactiveDot.imageset/Contents.json b/LocoKit Demo App/Assets.xcassets/inactiveDot.imageset/Contents.json
deleted file mode 100644
index 45f8491a..00000000
--- a/LocoKit Demo App/Assets.xcassets/inactiveDot.imageset/Contents.json
+++ /dev/null
@@ -1,21 +0,0 @@
-{
- "images" : [
- {
- "idiom" : "universal",
- "scale" : "1x"
- },
- {
- "idiom" : "universal",
- "filename" : "dot.png",
- "scale" : "2x"
- },
- {
- "idiom" : "universal",
- "scale" : "3x"
- }
- ],
- "info" : {
- "version" : 1,
- "author" : "xcode"
- }
-}
\ No newline at end of file
diff --git a/LocoKit Demo App/Assets.xcassets/inactiveDot.imageset/dot.png b/LocoKit Demo App/Assets.xcassets/inactiveDot.imageset/dot.png
deleted file mode 100644
index 8e647452..00000000
Binary files a/LocoKit Demo App/Assets.xcassets/inactiveDot.imageset/dot.png and /dev/null differ
diff --git a/LocoKit Demo App/Base.lproj/LaunchScreen.storyboard b/LocoKit Demo App/Base.lproj/LaunchScreen.storyboard
deleted file mode 100644
index fdf3f97d..00000000
--- a/LocoKit Demo App/Base.lproj/LaunchScreen.storyboard
+++ /dev/null
@@ -1,27 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/LocoKit Demo App/ClassifierView.swift b/LocoKit Demo App/ClassifierView.swift
deleted file mode 100644
index 284efdf5..00000000
--- a/LocoKit Demo App/ClassifierView.swift
+++ /dev/null
@@ -1,82 +0,0 @@
-//
-// ClassifierView.swift
-// LocoKit Demo App
-//
-// Created by Matt Greenfield on 12/12/17.
-// Copyright © 2017 Big Paua. All rights reserved.
-//
-
-import LocoKit
-import Anchorage
-
-class ClassifierView: UIScrollView {
-
- lazy var rows: UIStackView = {
- let box = UIStackView()
- box.axis = .vertical
- return box
- }()
-
- init() {
- super.init(frame: CGRect.zero)
- backgroundColor = .white
- alwaysBounceVertical = true
- update()
- }
-
- required init?(coder aDecoder: NSCoder) {
- fatalError("init(coder:) has not been implemented")
- }
-
- override func didMoveToSuperview() {
- addSubview(rows)
- rows.topAnchor == rows.superview!.topAnchor
- rows.bottomAnchor == rows.superview!.bottomAnchor - 8
- rows.leftAnchor == rows.superview!.leftAnchor + 16
- rows.rightAnchor == rows.superview!.rightAnchor - 16
- rows.rightAnchor == superview!.rightAnchor - 16
- }
-
- func update(sample: LocomotionSample? = nil) {
- // don't bother updating the UI when we're not in the foreground
- guard UIApplication.shared.applicationState == .active else { return }
-
- // don't bother updating the table if we're not the visible tab
- if sample != nil && Settings.visibleTab != self { return }
-
- rows.arrangedSubviews.forEach { $0.removeFromSuperview() }
-
- rows.addGap(height: 18)
- rows.addSubheading(title: "Sample Classifier Results")
- rows.addGap(height: 6)
-
- let timelineClassifier = TimelineClassifier.highlander
-
- if let sampleClassifier = timelineClassifier.sampleClassifier {
- rows.addRow(leftText: "Region coverageScore", rightText: sampleClassifier.coverageScoreString)
- } else {
- rows.addRow(leftText: "Region coverageScore", rightText: "-")
- }
- rows.addGap(height: 6)
-
- // if we weren't given a sample, then we were only here to build the initial empty table
- guard let sample = sample else { return }
-
- // get the classifier results for the given sample
- guard let results = timelineClassifier.classify(sample) else { return }
-
- for result in results {
- let row = rows.addRow(leftText: result.name.rawValue.capitalized,
- rightText: String(format: "%.7f", result.score))
-
- if result.score < 0.01 {
- row.subviews.forEach { subview in
- if let label = subview as? UILabel {
- label.textColor = UIColor(white: 0.1, alpha: 0.45)
- }
- }
- }
- }
- }
-
-}
diff --git a/LocoKit Demo App/CoreLocation.helpers.swift b/LocoKit Demo App/CoreLocation.helpers.swift
deleted file mode 100644
index 2b32574b..00000000
--- a/LocoKit Demo App/CoreLocation.helpers.swift
+++ /dev/null
@@ -1,76 +0,0 @@
-//
-// CLLocation.helpers.swift
-// LocoKit
-//
-// Created by Matt Greenfield on 11/07/17.
-// Copyright © 2017 Big Paua. All rights reserved.
-//
-
-import CoreLocation
-
-typealias Radians = Double
-
-extension CLLocation {
-
- // find the centre of an array of locations
- convenience init?(locations: [CLLocation]) {
- guard !locations.isEmpty else {
- return nil
- }
-
- if locations.count == 1, let location = locations.first {
- self.init(latitude: location.coordinate.latitude, longitude: location.coordinate.longitude)
- return
- }
-
- var x: [Double] = []
- var y: [Double] = []
- var z: [Double] = []
-
- for location in locations {
- let lat = location.coordinate.latitude.radiansValue
- let lng = location.coordinate.longitude.radiansValue
-
- x.append(cos(lat) * cos(lng))
- y.append(cos(lat) * sin(lng))
- z.append(sin(lat))
- }
-
- let meanx = x.mean
- let meany = y.mean
- let meanz = z.mean
-
- let finalLng: Radians = atan2(meany, meanx)
- let hyp = (meanx * meanx + meany * meany).squareRoot()
- let finalLat: Radians = atan2(meanz, hyp)
-
- self.init(latitude: finalLat.degreesValue, longitude: finalLng.degreesValue)
- }
-
-}
-
-extension Array where Element: CLLocation {
-
- func radiusFrom(center: CLLocation) -> (mean: CLLocationDistance, sd: CLLocationDistance) {
- guard count > 1 else {
- return (0, 0)
- }
-
- let distances = self.map { $0.distance(from: center) }
-
- return (distances.mean, distances.standardDeviation)
- }
-
-}
-
-extension CLLocationDegrees {
- var radiansValue: Radians {
- return self * Double.pi / 180.0
- }
-}
-
-extension Radians {
- var degreesValue: CLLocationDegrees {
- return self * 180.0 / Double.pi
- }
-}
diff --git a/LocoKit Demo App/DebugLog.swift b/LocoKit Demo App/DebugLog.swift
deleted file mode 100644
index 12759fe6..00000000
--- a/LocoKit Demo App/DebugLog.swift
+++ /dev/null
@@ -1,82 +0,0 @@
-//
-// Logging.swift
-// LocoKit Demo App
-//
-// Created by Matt Greenfield on 7/12/17.
-// Copyright © 2017 Big Paua. All rights reserved.
-//
-
-import os.log
-import SwiftNotes
-
-extension NSNotification.Name {
- public static let logFileUpdated = Notification.Name("logFileUpdated")
-}
-
-func log(_ format: String = "", _ values: CVarArg...) {
- DebugLog.logToFile(format, values)
-}
-
-class DebugLog {
- static let formatter = DateFormatter()
-
- static func logToFile(_ format: String = "", _ values: CVarArg...) {
- let prefix = String(format: "[%@] ", Date().timeLogString)
- let logString = String(format: prefix + format, arguments: values)
- do {
- try logString.appendLineTo(logFile)
- } catch {
- // don't care
- }
- print("[LocoKit] " + logString)
- trigger(.logFileUpdated)
- }
-
- static var logFile: URL {
- let dir = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).last!
- return dir.appendingPathComponent("LocoKitDemoApp.log")
- }
-
- static func deleteLogFile() {
- do {
- try FileManager.default.removeItem(at: logFile)
- } catch {
- // don't care
- }
- trigger(.logFileUpdated)
- }
-}
-
-extension Date {
- var timeLogString: String {
- let formatter = DebugLog.formatter
- formatter.dateFormat = "HH:mm:ss"
- return formatter.string(from: self)
- }
-}
-
-extension String {
- func appendLineTo(_ url: URL) throws {
- try appendingFormat("\n").appendTo(url)
- }
-
- func appendTo(_ url: URL) throws {
- let dataObj = data(using: String.Encoding.utf8)!
- try dataObj.appendTo(url)
- }
-}
-
-extension Data {
- func appendTo(_ url: URL) throws {
- if let fileHandle = try? FileHandle(forWritingTo: url) {
- defer {
- fileHandle.closeFile()
- }
- fileHandle.seekToEndOfFile()
- fileHandle.write(self)
- } else {
- try write(to: url, options: .atomic)
- }
- }
-}
-
diff --git a/LocoKit Demo App/Info.plist b/LocoKit Demo App/Info.plist
deleted file mode 100644
index 98e8a521..00000000
--- a/LocoKit Demo App/Info.plist
+++ /dev/null
@@ -1,62 +0,0 @@
-
-
-
-
- CFBundleDevelopmentRegion
- en
- CFBundleDisplayName
- LocoKit Demo
- CFBundleExecutable
- $(EXECUTABLE_NAME)
- CFBundleIdentifier
- $(PRODUCT_BUNDLE_IDENTIFIER)
- CFBundleInfoDictionaryVersion
- 6.0
- CFBundleName
- $(PRODUCT_NAME)
- CFBundlePackageType
- APPL
- CFBundleShortVersionString
- 2.0
- CFBundleVersion
- 8
- Fabric
-
- APIKey
- 8eb92dfc2cbb526daa9a955ff461a40f41f354e0
- Kits
-
-
- KitInfo
-
- KitName
- Crashlytics
-
-
-
- LSRequiresIPhoneOS
-
- NSLocationAlwaysAndWhenInUseUsageDescription
- Location is used to demonstrate LocoKit's functionality
- NSLocationAlwaysUsageDescription
- Location is used to demonstrate LocoKit's functionality
- NSLocationWhenInUseUsageDescription
- Location is used to demonstrate LocoKit's functionality
- NSMotionUsageDescription
- Motion data is used to demonstrate LocoKit's functionality
- UIBackgroundModes
-
- location
-
- UILaunchStoryboardName
- LaunchScreen
- UIRequiredDeviceCapabilities
-
- armv7
-
- UISupportedInterfaceOrientations
-
- UIInterfaceOrientationPortrait
-
-
-
diff --git a/LocoKit Demo App/LocoView.swift b/LocoKit Demo App/LocoView.swift
deleted file mode 100644
index a10dd892..00000000
--- a/LocoKit Demo App/LocoView.swift
+++ /dev/null
@@ -1,128 +0,0 @@
-//
-// LocoView.swift
-// LocoKit Demo App
-//
-// Created by Matt Greenfield on 12/12/17.
-// Copyright © 2017 Big Paua. All rights reserved.
-//
-
-import LocoKit
-import Anchorage
-import CoreLocation
-
-class LocoView: UIScrollView {
-
- lazy var rows: UIStackView = {
- let box = UIStackView()
- box.axis = .vertical
- return box
- }()
-
- init() {
- super.init(frame: CGRect.zero)
- backgroundColor = .white
- alwaysBounceVertical = true
- update()
- }
-
- required init?(coder aDecoder: NSCoder) {
- fatalError("init(coder:) has not been implemented")
- }
-
- override func didMoveToSuperview() {
- addSubview(rows)
- rows.topAnchor == rows.superview!.topAnchor
- rows.bottomAnchor == rows.superview!.bottomAnchor - 8
- rows.leftAnchor == rows.superview!.leftAnchor + 16
- rows.rightAnchor == rows.superview!.rightAnchor - 16
- rows.rightAnchor == superview!.rightAnchor - 16
- }
-
- func update(sample: LocomotionSample? = nil) {
- // don't bother updating the UI when we're not in the foreground
- guard UIApplication.shared.applicationState == .active else { return }
-
- let loco = LocomotionManager.highlander
-
- if sample != nil && Settings.visibleTab != self {
- return
- }
-
- rows.arrangedSubviews.forEach { $0.removeFromSuperview() }
-
- rows.addGap(height: 18)
- rows.addSubheading(title: "Locomotion Manager")
- rows.addGap(height: 6)
-
- rows.addRow(leftText: "Recording state", rightText: loco.recordingState.rawValue)
-
- if loco.recordingState == .off {
- rows.addRow(leftText: "Requesting accuracy", rightText: "-")
-
- } else { // must be recording or in sleep mode
- let requesting = loco.locationManager.desiredAccuracy
- if requesting == kCLLocationAccuracyBest {
- rows.addRow(leftText: "Requesting accuracy", rightText: "kCLLocationAccuracyBest")
- } else if requesting == Double.greatestFiniteMagnitude {
- rows.addRow(leftText: "Requesting accuracy", rightText: "Double.greatestFiniteMagnitude")
- } else {
- rows.addRow(leftText: "Requesting accuracy", rightText: String(format: "%.0f metres", requesting))
- }
- }
-
- var receivingString = "-"
- if loco.recordingState == .recording, let sample = sample {
- var receivingHertz = 0.0
- if let locations = sample.filteredLocations, let duration = locations.dateInterval?.duration, duration > 0 {
- receivingHertz = Double(locations.count) / duration
- }
-
- if let location = sample.filteredLocations?.last {
- receivingString = String(format: "%.0f metres @ %.1f Hz", location.horizontalAccuracy, receivingHertz)
- }
- }
- rows.addRow(leftText: "Receiving accuracy", rightText: receivingString)
-
- rows.addGap(height: 14)
- rows.addSubheading(title: "Locomotion Sample")
- rows.addGap(height: 6)
-
- if let sample = sample {
- rows.addRow(leftText: "Latest sample", rightText: sample.description)
- rows.addRow(leftText: "Behind now", rightText: String(duration: sample.date.age))
- rows.addRow(leftText: "Moving state", rightText: sample.movingState.rawValue)
-
- if loco.recordPedometerEvents, let stepHz = sample.stepHz {
- rows.addRow(leftText: "Steps per second", rightText: String(format: "%.1f Hz", stepHz))
- } else {
- rows.addRow(leftText: "Steps per second", rightText: "-")
- }
-
- if loco.recordAccelerometerEvents {
- if let xyAcceleration = sample.xyAcceleration {
- rows.addRow(leftText: "XY Acceleration", rightText: String(format: "%.2f g", xyAcceleration))
- } else {
- rows.addRow(leftText: "XY Acceleration", rightText: "-")
- }
- if let zAcceleration = sample.zAcceleration {
- rows.addRow(leftText: "Z Acceleration", rightText: String(format: "%.2f g", zAcceleration))
- } else {
- rows.addRow(leftText: "Z Acceleration", rightText: "-")
- }
- }
-
- if loco.recordCoreMotionActivityTypeEvents {
- if let coreMotionType = sample.coreMotionActivityType {
- rows.addRow(leftText: "Core Motion activity", rightText: coreMotionType.rawValue)
- } else {
- rows.addRow(leftText: "Core Motion activity", rightText: "-")
- }
- }
-
- } else {
- rows.addRow(leftText: "Latest sample", rightText: "-")
- }
- }
-
-}
-
diff --git a/LocoKit Demo App/LogView.swift b/LocoKit Demo App/LogView.swift
deleted file mode 100644
index edeeb0e4..00000000
--- a/LocoKit Demo App/LogView.swift
+++ /dev/null
@@ -1,58 +0,0 @@
-//
-// LogView.swift
-// LocoKit Demo App
-//
-// Created by Matt Greenfield on 12/12/17.
-// Copyright © 2017 Big Paua. All rights reserved.
-//
-
-import LocoKit
-import Anchorage
-import SwiftNotes
-
-class LogView: UIScrollView {
-
- lazy var label: UILabel = {
- let label = UILabel()
- label.textColor = UIColor.black
- label.font = UIFont(name: "Menlo", size: 8)
- label.numberOfLines = 0
- return label
- }()
-
- init() {
- super.init(frame: CGRect.zero)
- backgroundColor = .white
- alwaysBounceVertical = true
-
- when(.logFileUpdated) { _ in
- onMain { self.update() }
- }
-
- update()
- }
-
- required init?(coder aDecoder: NSCoder) {
- fatalError("init(coder:) has not been implemented")
- }
-
- override func didMoveToSuperview() {
- addSubview(label)
- label.topAnchor == label.superview!.topAnchor + 10
- label.bottomAnchor == label.superview!.bottomAnchor - 10
- label.leftAnchor == label.superview!.leftAnchor + 8
- label.rightAnchor == label.superview!.rightAnchor - 8
- label.rightAnchor == superview!.rightAnchor - 8
- }
-
- func update() {
- guard UIApplication.shared.applicationState == .active else { return }
-
- guard let logString = try? String(contentsOf: DebugLog.logFile) else {
- label.text = ""
- return
- }
-
- label.text = logString
- }
-}
diff --git a/LocoKit Demo App/MapView.swift b/LocoKit Demo App/MapView.swift
deleted file mode 100644
index d1d2cb4f..00000000
--- a/LocoKit Demo App/MapView.swift
+++ /dev/null
@@ -1,206 +0,0 @@
-//
-// MapView.swift
-// LocoKit Demo App
-//
-// Created by Matt Greenfield on 12/12/17.
-// Copyright © 2017 Big Paua. All rights reserved.
-//
-
-import LocoKit
-import MapKit
-
-class MapView: MKMapView {
-
- init() {
- super.init(frame: CGRect.zero)
- self.delegate = self
- self.isRotateEnabled = false
- self.isPitchEnabled = false
- self.showsScale = true
- }
-
- required init?(coder aDecoder: NSCoder) {
- fatalError("init(coder:) has not been implemented")
- }
-
- func update(with items: [TimelineItem]) {
- // don't bother updating the map when we're not in the foreground
- guard UIApplication.shared.applicationState == .active else { return }
-
- let loco = LocomotionManager.highlander
-
- removeOverlays(overlays)
- removeAnnotations(annotations)
-
- showsUserLocation = Settings.showUserLocation && (loco.recordingState == .recording || loco.recordingState == .wakeup)
-
- let newMapType: MKMapType = Settings.showSatelliteMap ? .hybrid : .standard
- if mapType != newMapType {
- self.mapType = newMapType
- }
-
- if Settings.showTimelineItems {
- for timelineItem in items {
- if let path = timelineItem as? Path {
- add(path)
-
- } else if let visit = timelineItem as? Visit {
- add(visit)
- }
- }
-
- } else {
- var samples: [LocomotionSample] = []
-
- // do these as sets, because need to deduplicate
- var rawLocations: Set = []
- var filteredLocations: Set = []
-
- // collect samples and locations from the timeline items
- for timelineItem in items.reversed() {
- for sample in timelineItem.samples {
- samples.append(sample)
- if let locations = sample.rawLocations {
- rawLocations = rawLocations.union(locations)
- }
- if let locations = sample.filteredLocations {
- filteredLocations = filteredLocations.union(locations)
- }
- }
- }
-
- if Settings.showRawLocations {
- add(rawLocations.sorted { $0.timestamp < $1.timestamp }, color: .red)
- }
-
- if Settings.showFilteredLocations {
- add(filteredLocations.sorted { $0.timestamp < $1.timestamp }, color: .purple)
- }
-
- if Settings.showLocomotionSamples {
- let groups = sampleGroups(from: samples)
- for group in groups {
- add(group)
- }
- }
- }
-
- if Settings.autoZoomMap {
- zoomToShow(overlays: overlays)
- }
- }
-
- func sampleGroups(from samples: [LocomotionSample]) -> [[LocomotionSample]] {
- var groups: [[LocomotionSample]] = []
- var currentGroup: [LocomotionSample]?
-
- for sample in samples where sample.location != nil {
- let currentState = sample.movingState
-
- // state changed? close off the previous group, add to the collection, and start a new one
- if let previousState = currentGroup?.last?.movingState, previousState != currentState {
-
- // add new sample to previous grouping, to link them end to end
- currentGroup?.append(sample)
-
- // add it to the collection
- groups.append(currentGroup!)
-
- currentGroup = nil
- }
-
- currentGroup = currentGroup ?? []
- currentGroup?.append(sample)
- }
-
- // add the final grouping to the collection
- if let grouping = currentGroup {
- groups.append(grouping)
- }
-
- return groups
- }
-
- func add(_ locations: [CLLocation], color: UIColor) {
- guard !locations.isEmpty else {
- return
- }
-
- var coords = locations.compactMap { $0.coordinate }
- let path = PathPolyline(coordinates: &coords, count: coords.count)
- path.color = color
-
- addOverlay(path)
- }
-
- func add(_ samples: [LocomotionSample]) {
- guard let movingState = samples.first?.movingState else {
- return
- }
-
- let locations = samples.compactMap { $0.location }
-
- switch movingState {
- case .moving:
- add(locations, color: .blue)
-
- case .stationary:
- add(locations, color: .orange)
-
- case .uncertain:
- add(locations, color: .magenta)
- }
- }
-
- func add(_ path: Path) {
- if path.samples.isEmpty { return }
-
- var coords = path.samples.compactMap { $0.location?.coordinate }
- let line = PathPolyline(coordinates: &coords, count: coords.count)
- line.color = .brown
-
- addOverlay(line)
- }
-
- func add(_ visit: Visit) {
- guard let center = visit.center else { return }
-
- addAnnotation(VisitAnnotation(coordinate: center.coordinate, visit: visit))
-
- let circle = VisitCircle(center: center.coordinate, radius: visit.radius2sd)
- circle.color = .orange
- addOverlay(circle, level: .aboveLabels)
- }
-
-
- func zoomToShow(overlays: [MKOverlay]) {
- guard !overlays.isEmpty else { return }
-
- var mapRect: MKMapRect?
- for overlay in overlays {
- if mapRect == nil {
- mapRect = overlay.boundingMapRect
- } else {
- mapRect = mapRect!.union(overlay.boundingMapRect)
- }
- }
-
- let padding = UIEdgeInsets(top: 20, left: 20, bottom: 20, right: 20)
-
- setVisibleMapRect(mapRect!, edgePadding: padding, animated: true)
- }
-}
-
-extension MapView: MKMapViewDelegate {
-
- func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer {
- if let path = overlay as? PathPolyline { return path.renderer }
- if let circle = overlay as? VisitCircle { return circle.renderer }
- fatalError("you wot?")
- }
-
- func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
- return (annotation as? VisitAnnotation)?.view
- }
-
-}
diff --git a/LocoKit Demo App/PathPolyline.swift b/LocoKit Demo App/PathPolyline.swift
deleted file mode 100644
index 2fd43041..00000000
--- a/LocoKit Demo App/PathPolyline.swift
+++ /dev/null
@@ -1,16 +0,0 @@
-// Created by Matt Greenfield on 16/11/15.
-// Copyright (c) 2015 Big Paua. All rights reserved.
-
-import MapKit
-
-class PathPolyline: MKPolyline {
-
- var color: UIColor?
-
- var renderer: MKPolylineRenderer {
- let renderer = MKPolylineRenderer(polyline: self)
- renderer.strokeColor = color
- renderer.lineWidth = 3
- return renderer
- }
-}
diff --git a/LocoKit Demo App/Settings.swift b/LocoKit Demo App/Settings.swift
deleted file mode 100644
index 22d03d6a..00000000
--- a/LocoKit Demo App/Settings.swift
+++ /dev/null
@@ -1,27 +0,0 @@
-//
-// Settings.swift
-// LocoKit Demo App
-//
-// Created by Matt Greenfield on 12/12/17.
-// Copyright © 2017 Big Paua. All rights reserved.
-//
-
-import UIKit
-
-class Settings {
-
- static var showTimelineItems = true
-
- static var showRawLocations = true
- static var showFilteredLocations = true
- static var showLocomotionSamples = true
-
- static var showSatelliteMap = false
- static var showUserLocation = true
- static var autoZoomMap = true
-
- static var showDebugTimelineDetails = false
-
- static var visibleTab: UIView?
-
-}
diff --git a/LocoKit Demo App/SettingsView.swift b/LocoKit Demo App/SettingsView.swift
deleted file mode 100644
index ddf5d82a..00000000
--- a/LocoKit Demo App/SettingsView.swift
+++ /dev/null
@@ -1,121 +0,0 @@
-//
-// SettingsView.swift
-// LocoKit Demo App
-//
-// Created by Matt Greenfield on 9/10/17.
-// Copyright © 2017 Big Paua. All rights reserved.
-//
-
-import SwiftNotes
-import Anchorage
-
-extension NSNotification.Name {
- public static let settingsChanged = Notification.Name("settingsChanged")
-}
-
-class SettingsView: UIScrollView {
-
- var locoDataToggleBoxes: [ToggleBox] = []
-
- lazy var rows: UIStackView = {
- let box = UIStackView()
- box.axis = .vertical
- return box
- }()
-
- // MARK: -
-
- init() {
- super.init(frame: CGRect.zero)
- backgroundColor = .white
- alwaysBounceVertical = true
- buildViewTree()
- }
-
- required init?(coder aDecoder: NSCoder) {
- fatalError("init(coder:) has not been implemented")
- }
-
- override func didMoveToSuperview() {
- addSubview(rows)
- rows.topAnchor == rows.superview!.topAnchor
- rows.bottomAnchor == rows.superview!.bottomAnchor
- rows.leftAnchor == rows.superview!.leftAnchor + 8
- rows.rightAnchor == rows.superview!.rightAnchor - 8
- rows.rightAnchor == superview!.rightAnchor - 8
- }
-
- // MARK: -
-
- func buildViewTree() {
- rows.addGap(height: 24)
- rows.addSubheading(title: "Map Style", alignment: .center)
- rows.addGap(height: 6)
- rows.addUnderline()
-
- let currentLocation = ToggleBox(text: "Enable showsUserLocation", toggleDefault: Settings.showUserLocation) { isOn in
- Settings.showUserLocation = isOn
- trigger(.settingsChanged, on: self)
- }
- rows.addRow(views: [currentLocation])
-
- rows.addUnderline()
-
- let satellite = ToggleBox(text: "Satellite map", toggleDefault: Settings.showSatelliteMap) { isOn in
- Settings.showSatelliteMap = isOn
- trigger(.settingsChanged, on: self)
- }
- let zoom = ToggleBox(text: "Auto zoom") { isOn in
- Settings.autoZoomMap = isOn
- trigger(.settingsChanged, on: self)
- }
- rows.addRow(views: [satellite, zoom])
-
- rows.addGap(height: 18)
- rows.addSubheading(title: "Map Data", alignment: .center)
- rows.addGap(height: 6)
- rows.addUnderline()
-
- // toggle for showing timeline items
- let visits = ToggleBox(dotColors: [.brown, .orange], text: "Timeline", toggleDefault: Settings.showTimelineItems) { isOn in
- Settings.showTimelineItems = isOn
- self.locoDataToggleBoxes.forEach { $0.disabled = isOn }
- trigger(.settingsChanged, on: self)
- }
-
- // toggle for showing filtered locations
- let filtered = ToggleBox(dotColors: [.purple], text: "Filtered", toggleDefault: Settings.showFilteredLocations) { isOn in
- Settings.showFilteredLocations = isOn
- trigger(.settingsChanged, on: self)
- }
- filtered.disabled = Settings.showTimelineItems
- locoDataToggleBoxes.append(filtered)
-
- // add the toggles to the view
- rows.addRow(views: [visits, filtered])
-
- rows.addUnderline()
-
- // toggle for showing locomotion samples
- let samples = ToggleBox(dotColors: [.blue, .magenta], text: "Samples", toggleDefault: Settings.showLocomotionSamples) { isOn in
- Settings.showLocomotionSamples = isOn
- trigger(.settingsChanged, on: self)
- }
- samples.disabled = Settings.showTimelineItems
- locoDataToggleBoxes.append(samples)
-
- // toggle for showing raw locations
- let raw = ToggleBox(dotColors: [.red], text: "Raw", toggleDefault: Settings.showRawLocations) { isOn in
- Settings.showRawLocations = isOn
- trigger(.settingsChanged, on: self)
- }
- raw.disabled = Settings.showTimelineItems
- locoDataToggleBoxes.append(raw)
-
- // add the toggles to the view
- rows.addRow(views: [samples, raw])
-
- rows.addGap(height: 18)
- }
-
-}
diff --git a/LocoKit Demo App/String.helpers.swift b/LocoKit Demo App/String.helpers.swift
deleted file mode 100644
index 3e453989..00000000
--- a/LocoKit Demo App/String.helpers.swift
+++ /dev/null
@@ -1,27 +0,0 @@
-//
-// String.helpers.swift
-// LocoKit Demo App
-//
-// Created by Matt Greenfield on 5/08/17.
-// Copyright © 2017 Big Paua. All rights reserved.
-//
-
-import Foundation
-
-extension String {
-
- init(duration: TimeInterval, style: DateComponentsFormatter.UnitsStyle = .full, maximumUnits: Int = 2) {
- let formatter = DateComponentsFormatter()
- formatter.maximumUnitCount = maximumUnits
- formatter.unitsStyle = style
-
- if duration < 60 {
- formatter.allowedUnits = [.second, .minute, .hour, .day, .month]
- } else {
- formatter.allowedUnits = [.minute, .hour, .day, .month]
- }
-
- self.init(format: formatter.string(from: duration)!)
- }
-
-}
diff --git a/LocoKit Demo App/TimelineView.swift b/LocoKit Demo App/TimelineView.swift
deleted file mode 100644
index 7ebf55e5..00000000
--- a/LocoKit Demo App/TimelineView.swift
+++ /dev/null
@@ -1,154 +0,0 @@
-//
-// TimelineView.swift
-// LocoKit Demo App
-//
-// Created by Matt Greenfield on 12/12/17.
-// Copyright © 2017 Big Paua. All rights reserved.
-//
-
-import LocoKit
-import Anchorage
-
-class TimelineView: UIScrollView {
-
- lazy var rows: UIStackView = {
- let box = UIStackView()
- box.axis = .vertical
- return box
- }()
-
- init() {
- super.init(frame: CGRect.zero)
- backgroundColor = .white
- alwaysBounceVertical = true
- }
-
- required init?(coder aDecoder: NSCoder) {
- fatalError("init(coder:) has not been implemented")
- }
-
- override func didMoveToSuperview() {
- addSubview(rows)
- rows.topAnchor == rows.superview!.topAnchor
- rows.bottomAnchor == rows.superview!.bottomAnchor - 16
- rows.leftAnchor == rows.superview!.leftAnchor + 16
- rows.rightAnchor == rows.superview!.rightAnchor - 16
- rows.rightAnchor == superview!.rightAnchor - 16
- }
-
- func update(with items: [TimelineItem]) {
- // don't bother updating the UI when we're not in the foreground
- guard UIApplication.shared.applicationState == .active else { return }
-
- rows.arrangedSubviews.forEach { $0.removeFromSuperview() }
-
- rows.addGap(height: 18)
- rows.addHeading(title: "Timeline Items")
- rows.addGap(height: 2)
-
- if items.isEmpty {
- rows.addRow(leftText: "-")
- return
- }
-
- for timelineItem in items {
- if timelineItem.isDataGap {
- addDataGap(timelineItem)
- } else {
- add(timelineItem)
- }
- }
- }
-
- func add(_ timelineItem: TimelineItem) {
- rows.addGap(height: 14)
- var title = ""
- if let start = timelineItem.startDate {
- title += "[\(dateFormatter.string(from: start))] "
- }
- if timelineItem.isCurrentItem {
- title += "Current "
- }
- title += timelineItem.isNolo ? "Nolo" : timelineItem is Visit ? "Visit" : "Path"
- if let path = timelineItem as? Path, let activityType = path.movingActivityType {
- title += " (\(activityType)"
- if Settings.showDebugTimelineDetails {
- if let modeType = path.modeMovingActivityType {
- title += ", mode: \(modeType)"
- }
- }
- title += ")"
- }
- rows.addSubheading(title: title)
- rows.addGap(height: 6)
-
- if timelineItem.hasBrokenEdges {
- if timelineItem.nextItem == nil && !timelineItem.isCurrentItem {
- rows.addRow(leftText: "nextItem is nil", color: .red)
- }
- if timelineItem.previousItem == nil {
- rows.addRow(leftText: "previousItem is nil", color: .red)
- }
- }
-
- rows.addRow(leftText: "Duration", rightText: String(duration: timelineItem.duration))
-
- if let path = timelineItem as? Path {
- rows.addRow(leftText: "Distance", rightText: String(metres: path.distance))
- rows.addRow(leftText: "Speed", rightText: String(metresPerSecond: path.metresPerSecond))
- }
-
- if let visit = timelineItem as? Visit {
- rows.addRow(leftText: "Radius", rightText: String(metres: visit.radius2sd))
- }
-
- let keeperString = timelineItem.isInvalid ? "invalid" : timelineItem.isWorthKeeping ? "keeper" : "valid"
- rows.addRow(leftText: "Keeper status", rightText: keeperString)
-
- // the rest of the rows are debug bits, mostly for my benefit only
- guard Settings.showDebugTimelineDetails else {
- return
- }
-
- let debugColor = UIColor(white: 0.94, alpha: 1)
-
- if let previous = timelineItem.previousItem, !timelineItem.withinMergeableDistance(from: previous) {
- if
- let timeGap = timelineItem.timeInterval(from: previous),
- let distGap = timelineItem.distance(from: previous)
- {
- rows.addRow(leftText: "Unmergeable gap from previous",
- rightText: "\(String(duration: timeGap)) (\(String(metres: distGap)))",
- background: debugColor)
- } else {
- rows.addRow(leftText: "Unmergeable gap from previous", rightText: "unknown gap size",
- background: debugColor)
- }
- let maxMerge = timelineItem.maximumMergeableDistance(from: previous)
- rows.addRow(leftText: "Max mergeable gap", rightText: "\(String(metres: maxMerge))", background: debugColor)
- }
-
- rows.addRow(leftText: "Samples", rightText: "\(timelineItem.samples.count)", background: debugColor)
-
- rows.addRow(leftText: "ItemId", rightText: timelineItem.itemId.uuidString, background: debugColor)
- }
-
- func addDataGap(_ timelineItem: TimelineItem) {
- guard timelineItem.isDataGap else { return }
-
- rows.addGap(height: 14)
- rows.addUnderline()
- rows.addGap(height: 14)
-
- rows.addSubheading(title: "Timeline Gap (\(String(duration: timelineItem.duration)))", color: .red)
-
- rows.addGap(height: 14)
- rows.addUnderline()
- }
-
- lazy var dateFormatter: DateFormatter = {
- let formatter = DateFormatter()
- formatter.dateFormat = "HH:mm"
- return formatter
- }()
-}
diff --git a/LocoKit Demo App/ToggleBox.swift b/LocoKit Demo App/ToggleBox.swift
deleted file mode 100644
index 25c412f3..00000000
--- a/LocoKit Demo App/ToggleBox.swift
+++ /dev/null
@@ -1,100 +0,0 @@
-//
-// ToggleBox.swift
-// LocoKit Demo App
-//
-// Created by Matt Greenfield on 5/08/17.
-// Copyright © 2017 Big Paua. All rights reserved.
-//
-
-import Anchorage
-
-class ToggleBox: UIView {
-
- let toggle = UISwitch()
- var onChange: (Bool) -> Void
-
- var disabled: Bool {
- get {
- return toggle.alpha < 1
- }
- set(disable) {
- toggle.isEnabled = !disable
- subviews.forEach { $0.alpha = disable ? 0.45 : 1 }
- }
- }
-
- init(dotColors: [UIColor] = [], text: String, toggleDefault: Bool = true, onChange: @escaping ((Bool) -> Void)) {
- self.onChange = onChange
-
- super.init(frame: CGRect.zero)
-
- backgroundColor = .white
-
- var lastDot: UIView?
- for color in dotColors {
- let dot = self.dot(color: color)
- let dotWidth = dot.frame.size.width
- addSubview(dot)
-
- dot.centerYAnchor == dot.superview!.centerYAnchor
- dot.heightAnchor == dotWidth
- dot.widthAnchor == dotWidth
-
- if let lastDot = lastDot {
- dot.leftAnchor == lastDot.rightAnchor - 4
- } else {
- dot.leftAnchor == dot.superview!.leftAnchor + 8
- }
-
- lastDot = dot
- }
-
- let label = UILabel()
- label.text = text
- label.font = UIFont.preferredFont(forTextStyle: .body)
- label.textColor = UIColor(white: 0.1, alpha: 1)
-
- toggle.isOn = toggleDefault
- toggle.addTarget(self, action: #selector(ToggleBox.triggerOnChange), for: .valueChanged)
-
- addSubview(label)
- addSubview(toggle)
-
- if let lastDot = lastDot {
- label.leftAnchor == lastDot.rightAnchor + 5
-
- } else {
- label.leftAnchor == label.superview!.leftAnchor + 9
- }
-
- label.topAnchor == label.superview!.topAnchor
- label.bottomAnchor == label.superview!.bottomAnchor
- label.heightAnchor == 44
-
- toggle.centerYAnchor == toggle.superview!.centerYAnchor
- toggle.rightAnchor == toggle.superview!.rightAnchor - 10
- toggle.leftAnchor == label.rightAnchor
- }
-
- @objc func triggerOnChange() {
- onChange(toggle.isOn)
- }
-
- required init?(coder aDecoder: NSCoder) {
- fatalError("init(coder:) has not been implemented")
- }
-
- func dot(color: UIColor) -> UIView {
- let dot = UIView(frame: CGRect(x: 0, y: 0, width: 14, height: 14))
-
- let shape = CAShapeLayer()
- shape.fillColor = color.cgColor
- shape.path = UIBezierPath(roundedRect: dot.bounds, cornerRadius: 7).cgPath
- shape.strokeColor = UIColor.white.cgColor
- shape.lineWidth = 2
- dot.layer.addSublayer(shape)
-
- return dot
- }
-}
-
diff --git a/LocoKit Demo App/UIStackView.helpers.swift b/LocoKit Demo App/UIStackView.helpers.swift
deleted file mode 100644
index 35ed72a2..00000000
--- a/LocoKit Demo App/UIStackView.helpers.swift
+++ /dev/null
@@ -1,89 +0,0 @@
-//
-// UIStackView.helpers.swift
-// LocoKit Demo App
-//
-// Created by Matt Greenfield on 5/08/17.
-// Copyright © 2017 Big Paua. All rights reserved.
-//
-
-import Anchorage
-
-extension UIStackView {
-
- func addUnderline() {
- let underline = UIView()
- underline.backgroundColor = UIColor(white: 0.85, alpha: 1)
- addArrangedSubview(underline)
- underline.heightAnchor == 1.0 / UIScreen.main.scale
- }
-
- func addGap(height: CGFloat) {
- let gap = UIView()
- gap.backgroundColor = .white
- addArrangedSubview(gap)
- gap.heightAnchor == height
- }
-
- func addHeading(title: String, alignment: NSTextAlignment = .left) {
- let header = UILabel()
- header.backgroundColor = .white
- header.font = UIFont.preferredFont(forTextStyle: .headline)
- header.textAlignment = alignment
- header.text = title
- addArrangedSubview(header)
- }
-
- func addSubheading(title: String, alignment: NSTextAlignment = .left, color: UIColor = .black) {
- let header = UILabel()
- header.backgroundColor = .white
- header.font = UIFont.preferredFont(forTextStyle: .subheadline)
- header.textAlignment = alignment
- header.textColor = color
- header.text = title
- addArrangedSubview(header)
- }
-
- func addRow(views: [UIView]) {
- let row = UIStackView()
- row.distribution = .fillEqually
- row.spacing = 0.5
- views.forEach { row.addArrangedSubview($0) }
- addArrangedSubview(row)
- }
-
- @discardableResult func addRow(leftText: String? = nil, rightText: String? = nil,
- color: UIColor = UIColor(white: 0.1, alpha: 1),
- background: UIColor = .white) -> UIStackView {
- let leftLabel = UILabel()
- leftLabel.text = leftText
- leftLabel.font = UIFont.preferredFont(forTextStyle: .caption1)
- leftLabel.textColor = color
- leftLabel.backgroundColor = background
-
- let rightLabel = UILabel()
- rightLabel.text = rightText
- rightLabel.textAlignment = .right
- rightLabel.font = UIFont.preferredFont(forTextStyle: .caption1)
- rightLabel.textColor = color
- rightLabel.backgroundColor = background
-
- let leftPad = UIView()
- leftPad.backgroundColor = background
-
- let rightPad = UIView()
- rightPad.backgroundColor = background
-
- let row = UIStackView()
- row.addArrangedSubview(leftPad)
- row.addArrangedSubview(leftLabel)
- row.addArrangedSubview(rightLabel)
- row.addArrangedSubview(rightPad)
- addArrangedSubview(row)
-
- leftPad.widthAnchor == 8
- rightPad.widthAnchor == 8
- row.heightAnchor == 20
-
- return row
- }
-}
diff --git a/LocoKit Demo App/ViewController.swift b/LocoKit Demo App/ViewController.swift
deleted file mode 100644
index 920a4ff2..00000000
--- a/LocoKit Demo App/ViewController.swift
+++ /dev/null
@@ -1,331 +0,0 @@
-//
-// ViewController.swift
-// LocoKit Demo App
-//
-// Created by Matt Greenfield on 10/07/17.
-// Copyright © 2017 Big Paua. All rights reserved.
-//
-
-import LocoKit
-import Anchorage
-import SwiftNotes
-import CoreLocation
-
-class ViewController: UIViewController {
-
- let store = TimelineStore()
-
- var recorder: TimelineRecorder
- var dataSet: TimelineSegment?
-
- lazy var mapView = { return MapView() }()
- lazy var timelineView = { return TimelineView() }()
- let classifierView = ClassifierView()
- let settingsView = SettingsView()
- let locoView = LocoView()
- let logView = LogView()
-
- // MARK: controller lifecycle
-
- init() {
- // LocoKit's Activity Type Classifiers require an API key
- // API keys can be created at: https://www.bigpaua.com/locokit/account
- // LocoKitService.apiKey = ""
-
- if LocoKitService.apiKey != nil {
- ActivityTypesCache.highlander.store = store
- recorder = TimelineRecorder(store: store, classifier: TimelineClassifier.highlander)
-
- } else {
- recorder = TimelineRecorder(store: store)
- }
-
- super.init(nibName: nil, bundle: nil)
- }
-
- required init?(coder aDecoder: NSCoder) {
- fatalError("init(coder:) has not been implemented")
- }
-
- override func viewDidLoad() {
- super.viewDidLoad()
-
- let query = "deleted = 0 AND endDate > datetime('now','-24 hours') ORDER BY startDate DESC"
- dataSet = TimelineSegment(where: query, in: store) {
- onMain {
- let items = self.itemsToShow
- self.mapView.update(with: items)
- self.timelineView.update(with: items)
- }
- }
-
- // observe new timeline items
- when(.newTimelineItem) { _ in
- if let currentItem = self.recorder.currentItem {
- log(".newTimelineItem (\(String(describing: type(of: currentItem))))")
- }
- }
-
- when(.mergedTimelineItems) { note in
- if let description = note.userInfo?["description"] as? String {
- log(".mergedItems (\(description))")
- }
- }
-
- let loco = LocomotionManager.highlander
-
- // observe incoming location / locomotion updates
- when(loco, does: .locomotionSampleUpdated) { _ in
- self.locomotionSampleUpdated()
- }
-
- // observe changes in the recording state (recording / sleeping)
- when(loco, does: .recordingStateChanged) { _ in
- // don't log every type of state change, because it gets noisy
- if loco.recordingState == .recording || loco.recordingState == .off {
- log(".recordingStateChanged (\(loco.recordingState))")
- }
- self.locoView.update()
- self.mapView.update(with: self.itemsToShow)
- }
-
- // observe changes in the moving state (moving / stationary)
- when(loco, does: .movingStateChanged) { _ in
- log(".movingStateChanged (\(loco.movingState))")
- }
-
- when(loco, does: .wentFromRecordingToSleepMode) { _ in
- log(".startedSleepMode")
- }
-
- when(loco, does: .wentFromSleepModeToRecording) { _ in
- log(".stoppedSleepMode")
- }
-
- when(settingsView, does: .settingsChanged) { _ in
- self.mapView.update(with: self.itemsToShow)
- self.setNeedsStatusBarAppearanceUpdate()
- }
-
- when(UIApplication.didReceiveMemoryWarningNotification) { _ in
- log("UIApplicationDidReceiveMemoryWarning")
- }
-
- // housekeeping
- when(UIApplication.didEnterBackgroundNotification) { _ in
- log("store.hardDeleteSoftDeletedObjects()")
- self.store.hardDeleteSoftDeletedObjects()
- }
-
- // view tree stuff
- view.backgroundColor = .white
- buildViewTree()
-
- // get things started by asking permission
- loco.requestLocationPermission()
- }
-
- override var preferredStatusBarStyle: UIStatusBarStyle {
- return mapView.mapType == .standard ? .default : .lightContent
- }
-
- // MARK: process incoming locations
-
- func locomotionSampleUpdated() {
- let loco = LocomotionManager.highlander
-
- // don't bother updating the UI when we're not in the foreground
- guard UIApplication.shared.applicationState == .active else {
- return
- }
-
- // get the latest sample and update the results views
- let sample = loco.locomotionSample()
- locoView.update(sample: sample)
- classifierView.update(sample: sample)
- }
-
- // MARK: tap actions
-
- @objc func tappedStart() {
- log("tappedStart()")
-
- recorder.startRecording()
-
- startButton.isHidden = true
- stopButton.isHidden = false
- }
-
- @objc func tappedStop() {
- log("tappedStop()")
-
- recorder.stopRecording()
-
- stopButton.isHidden = true
- startButton.isHidden = false
- }
-
- @objc func tappedClear() {
- DebugLog.deleteLogFile()
- // TODO: flush the timeline data
- }
-
- @objc func tappedViewToggle() {
- var chosenView: UIScrollView
-
- switch viewToggle.selectedSegmentIndex {
- case 0:
- chosenView = timelineView
- case 1:
- chosenView = locoView
- case 2:
- chosenView = classifierView
- case 3:
- chosenView = logView
- default:
- chosenView = settingsView
- }
-
- view.bringSubviewToFront(chosenView)
- view.bringSubviewToFront(viewToggleBar)
- chosenView.flashScrollIndicators()
- Settings.visibleTab = chosenView
- }
-
- // MARK: view tree building
-
- func buildViewTree() {
- view.addSubview(mapView)
- mapView.topAnchor == mapView.superview!.topAnchor
- mapView.leftAnchor == mapView.superview!.leftAnchor
- mapView.rightAnchor == mapView.superview!.rightAnchor
- mapView.heightAnchor == mapView.superview!.heightAnchor * 0.35
-
- view.addSubview(topButtons)
- topButtons.topAnchor == mapView.bottomAnchor
- topButtons.leftAnchor == topButtons.superview!.leftAnchor
- topButtons.rightAnchor == topButtons.superview!.rightAnchor
- topButtons.heightAnchor == 56
-
- topButtons.addSubview(startButton)
- topButtons.addSubview(stopButton)
- topButtons.addSubview(clearButton)
-
- startButton.topAnchor == stopButton.topAnchor
- stopButton.topAnchor == clearButton.topAnchor
- startButton.bottomAnchor == stopButton.bottomAnchor
- stopButton.bottomAnchor == clearButton.bottomAnchor
-
- startButton.topAnchor == startButton.superview!.topAnchor
- startButton.bottomAnchor == startButton.superview!.bottomAnchor - 0.5
- startButton.leftAnchor == startButton.superview!.leftAnchor
- startButton.rightAnchor == startButton.superview!.centerXAnchor
-
- stopButton.edgeAnchors == startButton.edgeAnchors
-
- clearButton.leftAnchor == startButton.rightAnchor + 0.5
- clearButton.rightAnchor == clearButton.superview!.rightAnchor
-
- view.addSubview(locoView)
- view.addSubview(classifierView)
- view.addSubview(logView)
- view.addSubview(settingsView)
- view.addSubview(timelineView)
- view.addSubview(viewToggleBar)
- Settings.visibleTab = timelineView
-
- viewToggleBar.bottomAnchor == bottomLayoutGuide.topAnchor
- viewToggleBar.leftAnchor == viewToggleBar.superview!.leftAnchor
- viewToggleBar.rightAnchor == viewToggleBar.superview!.rightAnchor
-
- locoView.topAnchor == topButtons.bottomAnchor
- locoView.leftAnchor == locoView.superview!.leftAnchor
- locoView.rightAnchor == locoView.superview!.rightAnchor
- locoView.bottomAnchor == viewToggleBar.topAnchor
-
- settingsView.edgeAnchors == locoView.edgeAnchors
- timelineView.edgeAnchors == locoView.edgeAnchors
- classifierView.edgeAnchors == locoView.edgeAnchors
- logView.edgeAnchors == locoView.edgeAnchors
- }
-
- func updateAllViews() {
- // don't bother updating the UI when we're not in the foreground
- guard UIApplication.shared.applicationState == .active else { return }
-
- let items = itemsToShow
- timelineView.update(with: items)
- mapView.update(with: items)
- logView.update()
- locoView.update()
- classifierView.update()
- }
-
- var itemsToShow: [TimelineItem] {
- return dataSet?.timelineItems ?? []
- }
-
- // MARK: view property getters
-
- lazy var topButtons: UIView = {
- let box = UIView()
- box.backgroundColor = UIColor(white: 0.85, alpha: 1)
- return box
- }()
-
- lazy var startButton: UIButton = {
- let button = UIButton(type: .system)
-
- button.backgroundColor = .white
- button.setTitle("Start", for: .normal)
- button.addTarget(self, action: #selector(ViewController.tappedStart), for: .touchUpInside)
-
- return button
- }()
-
- lazy var stopButton: UIButton = {
- let button = UIButton(type: .system)
- button.isHidden = true
-
- button.backgroundColor = .white
- button.setTitle("Stop", for: .normal)
- button.setTitleColor(.red, for: .normal)
- button.addTarget(self, action: #selector(ViewController.tappedStop), for: .touchUpInside)
-
- return button
- }()
-
- lazy var clearButton: UIButton = {
- let button = UIButton(type: .system)
-
- button.backgroundColor = .white
- button.setTitle("Clear", for: .normal)
- button.addTarget(self, action: #selector(ViewController.tappedClear), for: .touchUpInside)
-
- return button
- }()
-
- lazy var viewToggleBar: UIToolbar = {
- let bar = UIToolbar()
- bar.isTranslucent = false
- bar.items = [
- UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil),
- UIBarButtonItem(customView: self.viewToggle),
- UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil)
- ]
- return bar
- }()
-
- lazy var viewToggle: UISegmentedControl = {
- let toggle = UISegmentedControl(items: ["TM", "LM", "AC", "Log", "Settings"])
- toggle.setWidth(66, forSegmentAt: 0)
- toggle.setWidth(66, forSegmentAt: 1)
- toggle.setWidth(66, forSegmentAt: 2)
- toggle.setWidth(66, forSegmentAt: 3)
- toggle.setWidth(66, forSegmentAt: 4)
- toggle.selectedSegmentIndex = 0
- toggle.addTarget(self, action: #selector(tappedViewToggle), for: .valueChanged)
- return toggle
- }()
-}
-
diff --git a/LocoKit Demo App/VisitAnnotation.swift b/LocoKit Demo App/VisitAnnotation.swift
deleted file mode 100644
index 8365871a..00000000
--- a/LocoKit Demo App/VisitAnnotation.swift
+++ /dev/null
@@ -1,21 +0,0 @@
-// Created by Matt Greenfield on 21/01/16.
-// Copyright (c) 2016 Big Paua. All rights reserved.
-
-import MapKit
-import LocoKit
-
-class VisitAnnotation: NSObject, MKAnnotation {
-
- var coordinate: CLLocationCoordinate2D
- var visit: Visit
-
- init(coordinate: CLLocationCoordinate2D, visit: Visit) {
- self.coordinate = coordinate
- self.visit = visit
- super.init()
- }
-
- var view: VisitAnnotationView {
- return VisitAnnotationView(annotation: self, reuseIdentifier: nil)
- }
-}
diff --git a/LocoKit Demo App/VisitAnnotationView.swift b/LocoKit Demo App/VisitAnnotationView.swift
deleted file mode 100644
index b2f8a7d8..00000000
--- a/LocoKit Demo App/VisitAnnotationView.swift
+++ /dev/null
@@ -1,17 +0,0 @@
-// Created by Matt Greenfield on 4/10/16.
-// Copyright © 2016 Big Paua. All rights reserved.
-
-import MapKit
-
-class VisitAnnotationView: MKAnnotationView {
-
- override init(annotation: MKAnnotation?, reuseIdentifier: String?) {
- super.init(annotation: annotation, reuseIdentifier: reuseIdentifier)
- image = UIImage(named: "dot")
- }
-
- required init?(coder aDecoder: NSCoder) {
- fatalError("init(coder:) has not been implemented")
- }
-
-}
diff --git a/LocoKit Demo App/VisitCircle.swift b/LocoKit Demo App/VisitCircle.swift
deleted file mode 100644
index 23a820bc..00000000
--- a/LocoKit Demo App/VisitCircle.swift
+++ /dev/null
@@ -1,17 +0,0 @@
-// Created by Matt Greenfield on 9/11/15.
-// Copyright (c) 2015 Big Paua. All rights reserved.
-
-import MapKit
-
-class VisitCircle: MKCircle {
-
- var color: UIColor?
-
- var renderer: MKCircleRenderer {
- let renderer = MKCircleRenderer(circle: self)
- renderer.fillColor = color?.withAlphaComponent(0.2)
- renderer.strokeColor = nil
- renderer.lineWidth = 0
- return renderer
- }
-}
diff --git a/LocoKit.podspec b/LocoKit.podspec
deleted file mode 100644
index 49b32fa8..00000000
--- a/LocoKit.podspec
+++ /dev/null
@@ -1,21 +0,0 @@
-Pod::Spec.new do |s|
- s.name = "LocoKit"
- s.version = "7.1.0"
- s.summary = "Location and activity recording framework"
- s.homepage = "https://www.bigpaua.com/locokit/"
- s.author = { "Matt Greenfield" => "matt@bigpaua.com" }
- s.license = { :text => "Copyright 2018 Matt Greenfield. All rights reserved.",
- :type => "Commercial" }
-
- s.source = { :git => 'https://github.com/sobri909/LocoKit.git', :tag => '7.1.0' }
- s.frameworks = 'CoreLocation', 'CoreMotion'
- s.swift_version = '5.0'
- s.ios.deployment_target = '13.0'
- s.default_subspec = 'Base'
-
- s.subspec 'Base' do |sp|
- sp.source_files = 'LocoKit/Base/**/*', 'LocoKit/Timelines/**/*'
- sp.dependency 'Upsurge', '~> 0.10'
- sp.dependency 'GRDB.swift', '~> 4'
- end
-end
diff --git a/LocoKit/Base/ActivityBrain.swift b/LocoKit/Base/ActivityBrain.swift
index 98179a5c..626fb0a7 100644
--- a/LocoKit/Base/ActivityBrain.swift
+++ b/LocoKit/Base/ActivityBrain.swift
@@ -5,7 +5,9 @@
import os.log
import CoreLocation
+#if canImport(CoreMotion)
import CoreMotion
+#endif
public class ActivityBrain {
@@ -14,9 +16,9 @@ public class ActivityBrain {
internal static let worstAllowedPastSampleRadius: CLLocationDistance = 65 // small enough for slow walking to be detected
internal static let maximumSampleAge: TimeInterval = 60
- internal static let minimumWakeupConfidenceN = 10
+ internal static let minimumWakeupConfidenceN = 8
internal static let minimumConfidenceN = 6
- internal static let minimumRequiredN = 10
+ internal static let minimumRequiredN = 8
internal static let maximumRequiredN = 60
internal static let speedSampleN: Int = 4
@@ -167,18 +169,19 @@ public extension ActivityBrain {
// MARK: -
var kalmanRequiredN: Double {
+ let adjust: Double = 0.8
let accuracy = coordinatesKalman.accuracy
- return accuracy > 0 ? accuracy : 30
+ return accuracy > 0 ? accuracy * adjust : 30
}
// slower speed means higher required (zero speed == max required)
var speedRequiredN: Double {
- let maxSpeedReq: Double = 10
- let speedReqKmh: Double = 10
+ let maxSpeedReq: Double = 8 // maximum required N for slow speeds
+ let speedReqKmh: Double = 5 // faster than this requires no extra N
let kmh = presentSample.speed * 3.6
- // negative speed is useless here, so fallback to max required
+ // negative speed is useless here, so fall back to max required
guard kmh >= 0 else {
return maxSpeedReq
}
@@ -215,7 +218,8 @@ public extension ActivityBrain {
func add(deviceMotion: CMDeviceMotion) {
presentSample.addDeviceMotion(deviceMotion)
}
-
+ @available(macOS, unavailable)
+ @available(iOS 13.0, watchOS 6.0, *)
func add(cmMotionActivity activity: CMMotionActivity) {
for name in CoreMotionActivityTypeName.allTypes {
if let boolValue = activity.value(forKey: name.rawValue) as? Bool, boolValue == true {
diff --git a/LocoKit/Base/ActivityBrainSample.swift b/LocoKit/Base/ActivityBrainSample.swift
index 34d2d9ba..65a66b88 100644
--- a/LocoKit/Base/ActivityBrainSample.swift
+++ b/LocoKit/Base/ActivityBrainSample.swift
@@ -8,7 +8,9 @@
import os.log
import CoreLocation
+#if canImport(CoreMotion)
import CoreMotion
+#endif
public class ActivityBrainSample {
diff --git a/LocoKit/Base/AppGroup.swift b/LocoKit/Base/AppGroup.swift
index 7224df04..421843ac 100644
--- a/LocoKit/Base/AppGroup.swift
+++ b/LocoKit/Base/AppGroup.swift
@@ -17,7 +17,6 @@ public class AppGroup {
public let thisApp: AppName
public let suiteName: String
- public var timelineRecorder: TimelineRecorder?
public private(set) var apps: [AppName: AppState] = [:]
public private(set) lazy var groupDefaults: UserDefaults? = { UserDefaults(suiteName: suiteName) }()
@@ -92,17 +91,27 @@ public class AppGroup {
}
var currentAppState: AppState {
- if let currentItem = timelineRecorder?.currentItem {
- return AppState(appName: thisApp, recordingState: LocomotionManager.highlander.recordingState,
- currentItemId: currentItem.itemId, currentItemTitle: currentItem.title)
+ var deepSleepUntil: Date?
+ if let until = LocoKitService.requestedWakeupCall, until.age < 0 {
+ deepSleepUntil = until
}
- return AppState(appName: thisApp, recordingState: LocomotionManager.highlander.recordingState)
+ return AppState(appName: thisApp, recordingState: LocomotionManager.highlander.recordingState, deepSleepingUntil: deepSleepUntil)
}
public func notifyObjectChanges(objectIds: Set) {
let messageInfo = MessageInfo(date: Date(), message: .modifiedObjects, appName: thisApp, modifiedObjectIds: objectIds)
send(message: .modifiedObjects, messageInfo: messageInfo)
}
+
+ // MARK: - Shared settings
+
+ public func get(setting key: String) -> Any? {
+ return groupDefaults?.value(forKey: "sharedSetting." + key) as Any?
+ }
+
+ public func set(setting key: String, value: Any?) {
+ groupDefaults?.set(value, forKey: "sharedSetting." + key)
+ }
// MARK: - Private
@@ -136,10 +145,8 @@ public class AppGroup {
print("RECEIVED: .updatedState, from: \(by)")
guard let currentRecorder = currentRecorder else { print("wtf. no currentRecorder"); return }
guard let currentItemId = currentRecorder.currentItemId else { print("wtf. no currentItemId"); return }
- if currentAppState.currentItemId != currentItemId {
+ if !isAnActiveRecorder, currentAppState.currentItemId != currentItemId {
print("need to update local currentItem (mine: \(currentAppState.currentItemId?.uuidString ?? "nil"), theirs: \(currentItemId))")
- timelineRecorder?.store.connectToDatabase()
- timelineRecorder?.updateCurrentItem()
}
}
@@ -160,17 +167,34 @@ public class AppGroup {
// MARK: - Interfaces
- public enum AppName: String, CaseIterable, Codable { case arcV3, arcMini, arcV4 }
+ public enum AppName: String, CaseIterable, Codable {
+ case arcV3, arcMini, arcV4
+ public var sortIndex: Int {
+ switch self {
+ case .arcV3: return 0
+ case .arcMini: return 1
+ case .arcV4: return 2
+ }
+ }
+ }
public struct AppState: Codable {
public let appName: AppName
public let recordingState: RecordingState
public var currentItemId: UUID?
public var currentItemTitle: String?
+ public var deepSleepingUntil: Date?
public var updated = Date()
- public var isAlive: Bool { return updated.age < LocomotionManager.highlander.standbyCycleDuration + 2 }
+ public var isAlive: Bool {
+ if isDeepSleeping { return true }
+ return updated.age < LocomotionManager.highlander.standbyCycleDuration + 2
+ }
public var isAliveAndRecording: Bool { return isAlive && recordingState != .off && recordingState != .standby }
+ public var isDeepSleeping: Bool {
+ guard let until = deepSleepingUntil else { return false }
+ return until.age < 0
+ }
}
public enum Message: String, CaseIterable, Codable {
diff --git a/LocoKit/Base/CMActivityTypeEvent.swift b/LocoKit/Base/CMActivityTypeEvent.swift
index 9cea50fd..71cfe72e 100644
--- a/LocoKit/Base/CMActivityTypeEvent.swift
+++ b/LocoKit/Base/CMActivityTypeEvent.swift
@@ -3,7 +3,9 @@
// Copyright (c) 2015 Big Paua. All rights reserved.
//
+#if canImport(CoreMotion)
import CoreMotion
+#endif
internal struct CMActivityTypeEvent: Equatable {
@@ -45,6 +47,9 @@ internal struct CMActivityTypeEvent: Equatable {
result = 0.66
case .high:
result = 1.00
+ @unknown default:
+ assertionFailure("New value in CMMotionActivityConfidence")
+ result = 0
}
if name == .stationary {
diff --git a/LocoKit/Base/DeviceMotion.swift b/LocoKit/Base/DeviceMotion.swift
index 8d5b4dee..e5333918 100644
--- a/LocoKit/Base/DeviceMotion.swift
+++ b/LocoKit/Base/DeviceMotion.swift
@@ -6,7 +6,9 @@
// Copyright © 2017 Big Paua. All rights reserved.
//
+#if canImport(CoreMotion)
import CoreMotion
+#endif
internal class DeviceMotion {
diff --git a/LocoKit/Base/Helpers/ArrayTools.swift b/LocoKit/Base/Helpers/ArrayTools.swift
index 2d2e3579..1fc56f95 100644
--- a/LocoKit/Base/Helpers/ArrayTools.swift
+++ b/LocoKit/Base/Helpers/ArrayTools.swift
@@ -39,7 +39,7 @@ public extension Array where Element: FloatingPoint {
}
public extension Array where Element: Equatable {
- mutating func remove(_ object: Element) { if let index = index(of: object) { remove(at: index) } }
+ mutating func remove(_ object: Element) { if let index = firstIndex(of: object) { remove(at: index) } }
mutating func removeObjects(_ array: [Element]) { for object in array { remove(object) } }
}
diff --git a/LocoKit/Base/Helpers/CLLocationTools.swift b/LocoKit/Base/Helpers/CLLocationTools.swift
index 3ccd5c15..8867c935 100644
--- a/LocoKit/Base/Helpers/CLLocationTools.swift
+++ b/LocoKit/Base/Helpers/CLLocationTools.swift
@@ -7,7 +7,9 @@
//
import CoreLocation
+#if canImport(CoreMotion)
import CoreMotion
+#endif
public typealias Radians = Double
@@ -129,14 +131,6 @@ extension CLLocationCoordinate2D: Codable {
public extension CLLocation {
- convenience init?(weightedCenterFor samples: [LocomotionSample]) {
- self.init(weightedCenterFor: samples.compactMap { $0.hasUsableCoordinate ? $0.location : nil })
- }
-
- convenience init?(centerFor samples: [LocomotionSample]) {
- self.init(centerFor: samples.compactMap { $0.hasUsableCoordinate ? $0.location : nil })
- }
-
/// The weighted centre for an array of locations
convenience init?(weightedCenterFor locations: [CLLocation]) {
if locations.isEmpty { return nil }
diff --git a/LocoKit/Base/Helpers/MiscTools.swift b/LocoKit/Base/Helpers/MiscTools.swift
index 331c1774..1705aa6a 100644
--- a/LocoKit/Base/Helpers/MiscTools.swift
+++ b/LocoKit/Base/Helpers/MiscTools.swift
@@ -74,3 +74,23 @@ extension Data {
return map { String(format: "%02.2hhx", $0) }.joined()
}
}
+
+#if os(iOS) || os(tvOS)
+
+import UIKit
+
+typealias AppKitOrUIKitApplication = UIApplication
+#endif
+
+#if os(macOS)
+
+import AppKit
+
+public typealias AppKitOrUIKitApplication = NSApplication
+#endif
+
+enum ApplicationState {
+ case active
+ case inactive
+ case background
+}
diff --git a/LocoKit/Base/Helpers/StringTools.swift b/LocoKit/Base/Helpers/StringTools.swift
index c21e689c..e1bbe2cd 100644
--- a/LocoKit/Base/Helpers/StringTools.swift
+++ b/LocoKit/Base/Helpers/StringTools.swift
@@ -23,6 +23,9 @@ public extension String {
case .brief: unitStyle = .medium
case .full: unitStyle = .long
case .spellOut: unitStyle = .long
+ @unknown default:
+ assertionFailure("New value in Formatter.UnitStyle")
+ unitStyle = .long
}
self.init(String(duration: Measurement(value: duration, unit: UnitDuration.seconds), style: unitStyle))
return
@@ -76,6 +79,15 @@ public extension String {
}
self.init(format: formatter.string(from: metres.measurement))
}
+
+ init(paceForSpeed mps: CLLocationSpeed) {
+ let totalSeconds = TimeInterval(1000.0 / mps)
+ let minutes = floor(totalSeconds / 60)
+ let remainderSeconds = totalSeconds - (minutes * 60)
+ print("mps: \(mps), totalSeconds: \(totalSeconds), minutes: \(minutes), remainderSeconds: \(remainderSeconds)")
+
+ self.init(format: "%.0f'%.0f\"", minutes, remainderSeconds)
+ }
init(speed: CLLocationSpeed, style: Formatter.UnitStyle? = nil) {
self.init(metresPerSecond: speed, style: style)
diff --git a/LocoKit/Base/Helpers/Upsurge/Arithmetic.swift b/LocoKit/Base/Helpers/Upsurge/Arithmetic.swift
new file mode 100644
index 00000000..3a3eb6be
--- /dev/null
+++ b/LocoKit/Base/Helpers/Upsurge/Arithmetic.swift
@@ -0,0 +1,37 @@
+// Copyright © 2015 Venture Media Labs.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+import Accelerate
+
+func mean(_ x: M) -> Double where M.Element == Double {
+ var result: Double = 0.0
+ withPointer(x) { xp in
+ vDSP_meanvD(xp + x.startIndex, x.step, &result, vDSP_Length(x.count))
+ }
+ return result
+}
+
+func mean(_ x: M) -> Float where M.Element == Float {
+ var result = Float()
+ withPointer(x) { xp in
+ vDSP_meanv(xp + x.startIndex, x.step, &result, vDSP_Length(x.count))
+ }
+ return result
+}
diff --git a/LocoKit/Base/Helpers/Upsurge/LinearType.swift b/LocoKit/Base/Helpers/Upsurge/LinearType.swift
new file mode 100644
index 00000000..499f5fac
--- /dev/null
+++ b/LocoKit/Base/Helpers/Upsurge/LinearType.swift
@@ -0,0 +1,67 @@
+// Copyright © 2015 Venture Media Labs.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+/// The `LinearType` protocol should be implemented by any collection that stores its values in a contiguous memory block. This is the building block for one-dimensional operations that are single-instruction, multiple-data (SIMD).
+protocol LinearType: CustomStringConvertible, CustomDebugStringConvertible, BidirectionalCollection {
+
+ associatedtype Element
+
+ var count: Int { get }
+
+ /// Call `body(pointer)` with the pointer to the beginning of the memory block
+ func withUnsafePointer(_ body: (UnsafePointer) throws -> R) rethrows -> R
+
+ /// The index of the first valid element
+ var startIndex: Int { get }
+
+ /// One past the end of the data
+ var endIndex: Int { get }
+
+ /// The step size between valid elements
+ var step: Int { get }
+
+ subscript(position: Int) -> Element { get }
+}
+
+extension LinearType {
+ var dimensions: [Int] {
+ return [count]
+ }
+
+ func index(after i: Int) -> Int {
+ return i + 1
+ }
+
+ func index(before i: Int) -> Int {
+ return i - 1
+ }
+
+ func formIndex(after i: inout Int) {
+ i += 1
+ }
+
+ var description: String {
+ return "[\(map { "\($0)" }.joined(separator: ", "))]"
+ }
+
+ var debugDescription: String {
+ return description
+ }
+}
diff --git a/LocoKit/Base/Helpers/Upsurge/PointerUtilities.swift b/LocoKit/Base/Helpers/Upsurge/PointerUtilities.swift
new file mode 100644
index 00000000..18edc82e
--- /dev/null
+++ b/LocoKit/Base/Helpers/Upsurge/PointerUtilities.swift
@@ -0,0 +1,28 @@
+// Copyright © 2016 Venture Media Labs.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+import Foundation
+
+// MARK: One parameter
+
+/// Call `body(pointer)` with the pointer for the type
+func withPointer(_ t: T, body: (UnsafePointer) throws -> R) rethrows -> R {
+ return try t.withUnsafePointer(body)
+}
diff --git a/LocoKit/Base/Helpers/Upsurge/Sequence.swift b/LocoKit/Base/Helpers/Upsurge/Sequence.swift
new file mode 100644
index 00000000..622006a2
--- /dev/null
+++ b/LocoKit/Base/Helpers/Upsurge/Sequence.swift
@@ -0,0 +1,27 @@
+// Copyright © 2015 Venture Media Labs.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+import Foundation
+
+internal extension Collection {
+ func indexIsValid(_ index: Index) -> Bool {
+ return (startIndex..: LinearType, CustomStringConvertible, Equatable {
+ typealias Index = Int
+ typealias IndexDistance = Int
+
+ var mutablePointer: UnsafeMutablePointer
+ var capacity: IndexDistance
+ var count: IndexDistance
+
+ var startIndex: Index {
+ return 0
+ }
+
+ var endIndex: Index {
+ return count
+ }
+
+ var step: IndexDistance {
+ return 1
+ }
+
+ func withUnsafePointer(_ body: (UnsafePointer) throws -> R) rethrows -> R {
+ return try body(mutablePointer)
+ }
+
+ var pointer: UnsafePointer {
+ return UnsafePointer(mutablePointer)
+ }
+
+ /// Construct an uninitialized ValueArray with the given capacity
+ required init(capacity: IndexDistance) {
+ mutablePointer = UnsafeMutablePointer.allocate(capacity: capacity)
+ self.capacity = capacity
+ self.count = 0
+ }
+
+ deinit {
+ mutablePointer.deallocate()
+ }
+
+ subscript(index: Index) -> Element {
+ get {
+ assert(indexIsValid(index))
+ return pointer[index]
+ }
+ set {
+ assert(indexIsValid(index))
+ mutablePointer[index] = newValue
+ }
+ }
+
+ subscript(intervals: [Int]) -> Element {
+ get {
+ assert(intervals.count == 1)
+ return self[intervals[0]]
+ }
+ set {
+ assert(intervals.count == 1)
+ self[intervals[0]] = newValue
+ }
+ }
+
+ func append(_ newElement: Element) {
+ precondition(count + 1 <= capacity)
+ mutablePointer[count] = newElement
+ count += 1
+ }
+
+ func append(contentsOf newElements: S) where S.Iterator.Element == Element {
+ let a = Array(newElements)
+ precondition(count + a.count <= capacity)
+ let endPointer = mutablePointer + count
+ _ = UnsafeMutableBufferPointer(start: endPointer, count: capacity - count).initialize(from: a)
+ count += a.count
+ }
+
+ func replaceSubrange(_ subrange: Range, with newElements: C) where C.Iterator.Element == Element {
+ assert(subrange.lowerBound >= startIndex && subrange.upperBound <= endIndex)
+ _ = UnsafeMutableBufferPointer(start: mutablePointer + subrange.lowerBound, count: capacity - subrange.lowerBound).initialize(from: newElements)
+ }
+
+ // MARK: - Equatable
+
+ static func == (lhs: ValueArray, rhs: ValueArray) -> Bool {
+ return lhs.count == rhs.count && lhs.elementsEqual(rhs)
+ }
+}
diff --git a/LocoKit/Base/Jobs.swift b/LocoKit/Base/Jobs.swift
index 18dd127e..76a8e863 100644
--- a/LocoKit/Base/Jobs.swift
+++ b/LocoKit/Base/Jobs.swift
@@ -6,10 +6,15 @@
//
import os.log
+import Combine
+#if canImport(UIKit)
import UIKit
+#else
+import AppKit
+#endif
import Foundation
-public class Jobs {
+public class Jobs: ObservableObject {
// MARK: - PUBLIC
@@ -35,7 +40,7 @@ public class Jobs {
let queue = OperationQueue()
queue.name = "LocoKit.secondaryQueue"
queue.qualityOfService = loco.applicationState == .active ? .utility : .background
- queue.maxConcurrentOperationCount = loco.applicationState == .active ? OperationQueue.defaultMaxConcurrentOperationCount : 1
+ queue.maxConcurrentOperationCount = loco.applicationState == .active ? 4 : 1
return queue
}()
@@ -87,6 +92,11 @@ public class Jobs {
if self.primaryQueue.operationCount == 0, self.resumeWorkItem == nil {
self.resumeManagedQueues()
}
+ onMain { self.objectWillChange.send() }
+ })
+
+ observers.append(secondaryQueue.observe(\.operationCount) { _, _ in
+ onMain { self.objectWillChange.send() }
})
// debug observers
@@ -106,12 +116,20 @@ public class Jobs {
}
let center = NotificationCenter.default
- center.addObserver(forName: UIApplication.didBecomeActiveNotification, object: nil, queue: nil) { [weak self] note in
+ center.addObserver(forName: AppKitOrUIKitApplication.didBecomeActiveNotification, object: nil, queue: nil) { [weak self] note in
self?.didBecomeActive()
}
+
+ #if !os(macOS)
center.addObserver(forName: UIApplication.didEnterBackgroundNotification, object: nil, queue: nil) { [weak self] note in
- self?.didEnterBackground()
+ self?.didEnterBackground()
+ }
+ #else
+ center.addObserver(forName: NSApplication.didResignActiveNotification, object: nil, queue: nil) { [weak self] note in
+ self?.didEnterBackground()
}
+ #endif
+
}
private func logSerialQueueState() {
@@ -216,6 +234,10 @@ public class Jobs {
}
}
}
+
+ // MARK: - ObservableObject
+
+ public let objectWillChange = ObservableObjectPublisher()
}
diff --git a/LocoKit/Base/LocomotionManager.swift b/LocoKit/Base/LocomotionManager.swift
index 0b4ae473..c77fb56f 100644
--- a/LocoKit/Base/LocomotionManager.swift
+++ b/LocoKit/Base/LocomotionManager.swift
@@ -4,8 +4,14 @@
//
import os.log
+#if canImport(UIKit)
import UIKit
+#else
+import AppKit
+#endif
+#if canImport(CoreMotion)
import CoreMotion
+#endif
import CoreLocation
/**
@@ -49,6 +55,7 @@ import CoreLocation
either from the `movingState` property on the LocomotionManager, or on the latest `locomotionSample`. See the
`movingState` documentation for further details.
*/
+@available(iOS 13.0, watchOS 6.0, *)
@objc public class LocomotionManager: NSObject, CLLocationManagerDelegate {
// internal settings
@@ -61,8 +68,11 @@ import CoreLocation
public static let miminumDeepSleepDuration: TimeInterval = 60 * 15
public let pedometer = CMPedometer()
+
+ @available(macOS, unavailable)
private let activityManager = CMMotionActivityManager()
+ @available(macOS, unavailable)
private lazy var wiggles: CMMotionManager = {
let wiggles = CMMotionManager()
wiggles.deviceMotionUpdateInterval = 1.0 / LocomotionManager.wiggleHz
@@ -104,8 +114,8 @@ import CoreLocation
public var coordinateAssessor: TrustAssessor?
public var appGroup: AppGroup?
-
- public var applicationState: UIApplication.State = .background
+
+ var applicationState: ApplicationState = .background
// MARK: - The Singleton
@@ -264,18 +274,7 @@ import CoreLocation
public var filteredLocation: CLLocation? {
return ActivityBrain.highlander.kalmanLocation
}
-
- /**
- Returns a new `LocomotionSample` representing the most recent filtered and smoothed locomotion state, with
- combined location, motion, and activity properties.
-
- - Note: This method will create a new sample instance on each call. As such, you should retain and reuse the
- resulting sample until a new sample is needed.
- */
- public func locomotionSample() -> LocomotionSample {
- return LocomotionSample(from: ActivityBrain.highlander.presentSample)
- }
-
+
// MARK: - Current Moving State
/**
@@ -321,9 +320,11 @@ import CoreLocation
locationManager.desiredAccuracy = maximumDesiredLocationAccuracy
locationManager.distanceFilter = kCLDistanceFilterNone
locationManager.startUpdatingLocation()
-
+
+ #if !os(macOS)
// start the motion gimps
startCoreMotion()
+ #endif
// to avoid a desiredAccuracy update on first location arrival (which will have unacceptably bad accuracy)
lastAccuracyUpdate = Date()
@@ -360,8 +361,9 @@ import CoreLocation
locationManager.stopUpdatingLocation()
// stop the motion gimps
+ #if !os(macOS)
stopCoreMotion()
-
+ #endif
// stop the safety nets
locationManager.stopMonitoringVisits()
locationManager.stopMonitoringSignificantLocationChanges()
@@ -392,12 +394,16 @@ import CoreLocation
start using Core Motion. I will make this method private soon, and provide a more tidy way to trigger a Core
Motion permission request modal.
*/
+ @available(macOS, unavailable)
+ @available(iOS 13.0, watchOS 6.0, *)
public func startCoreMotion() {
startTheM()
startThePedometer()
startTheWiggles()
}
+ @available(macOS, unavailable)
+ @available(iOS 13.0, watchOS 6.0, *)
private func stopCoreMotion() {
stopTheM()
stopThePedometer()
@@ -432,17 +438,15 @@ import CoreLocation
/**
The device authorisation state for monitoring Core Motion data.
*/
+ @available(macOS, unavailable)
+ @available(iOS 13.0, watchOS 6.0, *)
public var haveCoreMotionPermission: Bool {
if coreMotionPermission {
return true
}
- if #available(iOS 11.0, *) {
- coreMotionPermission = CMMotionActivityManager.authorizationStatus() == .authorized
- } else {
- coreMotionPermission = CMSensorRecorder.isAuthorizedForRecording()
- }
-
+ coreMotionPermission = CMMotionActivityManager.authorizationStatus() == .authorized
+
return coreMotionPermission
}
@@ -453,7 +457,11 @@ import CoreLocation
*/
public var haveLocationPermission: Bool {
let status = CLLocationManager.authorizationStatus()
+ #if os(macOS)
+ return status == .authorizedAlways
+ #else
return status == .authorizedWhenInUse || status == .authorizedAlways
+ #endif
}
/**
@@ -495,12 +503,18 @@ import CoreLocation
private override init() {
super.init()
let center = NotificationCenter.default
- center.addObserver(forName: UIApplication.didBecomeActiveNotification, object: nil, queue: nil) { [weak self] note in
+ center.addObserver(forName: AppKitOrUIKitApplication.didBecomeActiveNotification, object: nil, queue: nil) { [weak self] note in
self?.applicationState = .active
}
+ #if !os(macOS)
center.addObserver(forName: UIApplication.didEnterBackgroundNotification, object: nil, queue: nil) { [weak self] note in
self?.applicationState = .background
}
+ #else
+ center.addObserver(forName: NSApplication.didResignActiveNotification, object: nil, queue: nil) { [weak self] note in
+ self?.applicationState = .background
+ }
+ #endif
}
// MARK: - Sleep mode management
@@ -515,8 +529,9 @@ import CoreLocation
NotificationCenter.default.post(Notification(name: .willStartSleepMode, object: self, userInfo: nil))
// stop the gimps
+ #if !os(macOS)
stopCoreMotion()
-
+ #endif
// set the location manager to ask for nothing and ignore everything
locationManager.desiredAccuracy = Double.greatestFiniteMagnitude
locationManager.distanceFilter = CLLocationDistanceMax
@@ -570,8 +585,10 @@ import CoreLocation
locationManager.stopUpdatingLocation()
// stop the motion gimps
+ #if !os(macOS)
stopCoreMotion()
-
+ #endif
+
// stop the timers
stopTheWakeupTimer()
stopTheUpdateTimer()
@@ -581,9 +598,13 @@ import CoreLocation
}
public var canDeepSleep: Bool {
+ #if !os(macOS)
guard haveBackgroundLocationPermission else { return false }
- guard UIApplication.shared.backgroundRefreshStatus == .available else { return false }
+// guard UIApplication.shared.backgroundRefreshStatus == .available else { return false }
return true
+ #else
+ return false
+ #endif
}
@objc public func startWakeup() {
@@ -619,8 +640,10 @@ import CoreLocation
public func startStandby() {
// stop the gimps
+ #if !os(macOS)
stopCoreMotion()
-
+ #endif
+
// set the location manager to ask for almost nothing and ignore everything
locationManager.desiredAccuracy = kCLLocationAccuracyThreeKilometers
locationManager.distanceFilter = kCLDistanceFilterNone
@@ -665,7 +688,9 @@ import CoreLocation
} else {
// could be moving, so let's fire up the gimps to get a head start on the data delay
+ #if !os(macOS)
startCoreMotion()
+ #endif
}
}
@@ -736,6 +761,8 @@ import CoreLocation
// MARK: - Core Motion management
+ @available(macOS, unavailable)
+ @available(iOS 13.0, watchOS 6.0, *)
private func startTheM() {
if watchingTheM {
return
@@ -755,6 +782,8 @@ import CoreLocation
}
}
+ @available(macOS, unavailable)
+ @available(iOS 13.0, watchOS 6.0, *)
private func stopTheM() {
if !watchingTheM {
return
@@ -800,6 +829,8 @@ import CoreLocation
// MARK: - Accelerometer
+ @available(macOS, unavailable)
+ @available(iOS 13.0, watchOS 6.0, *)
private func startTheWiggles() {
if watchingTheWiggles {
return
@@ -822,6 +853,8 @@ import CoreLocation
}
}
+ @available(macOS, unavailable)
+ @available(iOS 13.0, watchOS 6.0, *)
private func stopTheWiggles() {
if !watchingTheWiggles {
return
@@ -980,16 +1013,15 @@ import CoreLocation
// MARK: - CLLocationManagerDelegate
- public func locationManager(_ manager: CLLocationManager, didRangeBeacons beacons: [CLBeacon], in region: CLBeaconRegion) {
-
+ public func locationManager(_ manager: CLLocationManager, didRange beacons: [CLBeacon], satisfying beaconConstraint: CLBeaconIdentityConstraint) {
// broadcast a notification
let note = Notification(name: .didRangeBeacons, object: self, userInfo: ["beacons": beacons])
NotificationCenter.default.post(note)
// forward the delegate event
- locationManagerDelegate?.locationManager?(manager, didRangeBeacons: beacons, in: region)
+ locationManagerDelegate?.locationManager?(manager, didRange: beacons, satisfying: beaconConstraint)
}
-
+
public func locationManager(_ manager: CLLocationManager, didEnterRegion region: CLRegion) {
// broadcast a notification
@@ -1032,7 +1064,6 @@ import CoreLocation
// see if the visit should trigger a recording start
startWakeup()
}
-
public func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
// broadcast a notification
diff --git a/LocoKit/Base/LocomotionSample.swift b/LocoKit/Base/LocomotionSample.swift
deleted file mode 100644
index 2a950297..00000000
--- a/LocoKit/Base/LocomotionSample.swift
+++ /dev/null
@@ -1,408 +0,0 @@
-//
-// Created by Matt Greenfield on 5/07/17.
-// Copyright (c) 2017 Big Paua. All rights reserved.
-//
-
-import CoreLocation
-
-/**
- A composite, high level representation of the device's location, motion, and activity states over a brief
- duration of time.
-
- The current sample can be retrieved from `LocomotionManager.highlander.locomotionSample()`.
-
- ## Dynamic Sample Sizes
-
- Each sample's duration is dynamically determined, depending on the quality and quantity of available ocation
- and motion data. Samples sizes typically range from 10 to 60 seconds, however varying conditions can sometimes
- produce sample durations outside those bounds.
-
- Higher quality and quantity of available data results in shorter sample durations, with more specific
- representations of single moments in time.
-
- Lesser quality or quantity of available data result in longer sample durations, thus representing the average or most
- common states and location over the sample period instead of a single specific moment.
- */
-open class LocomotionSample: ActivityTypeTrainable, Codable {
-
- public let sampleId: UUID
-
- /// The timestamp for the weighted centre of the sample period. Equivalent to `location.timestamp`.
- public let date: Date
-
- public let secondsFromGMT: Int?
-
- // MARK: Location Properties
-
- /**
- The sample's smoothed location, equivalent to the weighted centre of the sample's `filteredLocations`.
-
- This is the most high level location value, representing the final result of all available filtering and smoothing
- algorithms. This value is most useful for drawing smooth, coherent paths on a map for end user consumption.
- */
- public let location: CLLocation?
-
- /**
- The raw locations received over the sample duration.
- */
- public let rawLocations: [CLLocation]?
-
- /**
- The Kalman filtered locations recorded over the sample duration.
- */
- public let filteredLocations: [CLLocation]?
-
- /// The moving or stationary state for the sample. See `MovingState` for details on possible values.
- public let movingState: MovingState
-
- // The recording state of the LocomotionManager at the time the sample was taken.
- public let recordingState: RecordingState
-
- // MARK: Motion Properties
-
- /**
- The user's walking/running/cycling cadence (steps per second) over the sample duration.
-
- This value is taken from [CMPedometer](https://developer.apple.com/documentation/coremotion/cmpedometer). and will
- only contain a usable value if `startCoreMotion()` has been called on the LocomotionManager.
-
- - Note: If the user is travelling by vehicle, this value may report a false value due to bumpy motion being
- misinterpreted as steps by CMPedometer.
- */
- public let stepHz: Double?
-
- /**
- The degree of variance in course direction over the sample duration.
-
- A value of 0.0 represents a perfectly straight path. A value of 1.0 represents complete inconsistency of
- direction between each location.
-
- This value may indicate several different conditions, such as high or low location accuracy (ie clean or erratic
- paths due to noisy location data), or the user travelling in either a straight or curved path. However given that
- the filtered locations already have the majority of path jitter removed, this value should not be considered in
- isolation from other factors - no firm conclusions can be drawn from it alone.
- */
- public let courseVariance: Double?
-
- /**
- The average amount of accelerometer motion on the XY plane over the sample duration.
-
- This value can be taken to be `mean(abs(xyAccelerations)) + (std(abs(xyAccelerations) * 3.0)`, with
- xyAccelerations being the recorded accelerometer X and Y values over the sample duration. Thus it represents the
- mean + 3SD of the unsigned acceleration values.
- */
- public let xyAcceleration: Double?
-
- /**
- The average amount of accelerometer motion on the Z axis over the sample duration.
-
- This value can be taken to be `mean(abs(zAccelerations)) + (std(abs(zAccelerations) * 3.0)`, with
- zAccelerations being the recorded accelerometer Z values over the sample duration. Thus it represents the
- mean + 3SD of the unsigned acceleration values.
- */
- public let zAcceleration: Double?
-
- // MARK: Activity Type Properties
-
- /**
- The highest scoring Core Motion activity type
- ([CMMotionActivity](https://developer.apple.com/documentation/coremotion/cmmotionactivity)) at the time of the
- sample's `date`.
- */
- public let coreMotionActivityType: CoreMotionActivityTypeName?
-
- public var classifierResults: ClassifierResults?
-
- public var activityType: ActivityTypeName? {
- return confirmedType ?? classifiedType
- }
-
- public var confirmedType: ActivityTypeName?
-
- public var previousSampleConfirmedType: ActivityTypeName?
-
- internal var _classifiedType: ActivityTypeName?
- public var classifiedType: ActivityTypeName? {
- if let cached = _classifiedType { return cached }
- guard let results = classifierResults else { return nil }
- guard results.best.score > 0 else { return nil }
- if !results.moreComing {
- _classifiedType = results.best.name
- }
- return results.best.name
- }
-
- // MARK: - Convenience Getters
-
- public lazy var timeOfDay: TimeInterval = { return self.date.sinceStartOfDay }()
-
- public var hasUsableCoordinate: Bool { return location?.hasUsableCoordinate ?? false }
-
- public var isNolo: Bool { return location?.isNolo ?? true }
-
- private var _localTimeZone: TimeZone?
- public var localTimeZone: TimeZone? {
- if let cached = _localTimeZone { return cached }
-
- // create one from utc offset
- if let secondsFromGMT = secondsFromGMT {
- _localTimeZone = TimeZone(secondsFromGMT: secondsFromGMT)
- return _localTimeZone
- }
-
- guard let location = location else { return nil }
- guard location.hasUsableCoordinate else { return nil }
-
- // try to fetch one from remote, hopefully available on next access
- CLPlacemarkCache.fetchPlacemark(for: location) { [weak self] placemark in
- self?._localTimeZone = placemark?.timeZone
- }
-
- return nil
- }
-
- public func distance(from otherSample: LocomotionSample) -> CLLocationDistance? {
- guard let myLocation = location, let theirLocation = otherSample.location else { return nil }
- return myLocation.distance(from: theirLocation)
- }
-
- // MARK: - Required initialisers
-
- public required init(from sample: ActivityBrainSample) {
- self.sampleId = UUID()
-
- self.date = sample.date
- self.secondsFromGMT = TimeZone.current.secondsFromGMT()
- self.recordingState = LocomotionManager.highlander.recordingState
- self.movingState = sample.movingState
- self.location = sample.location
- self.rawLocations = sample.rawLocations
- self.filteredLocations = sample.filteredLocations
- self.courseVariance = sample.courseVariance
- self.xyAcceleration = sample.xyAcceleration
- self.zAcceleration = sample.zAcceleration
- self.coreMotionActivityType = sample.coreMotionActivityType
-
- if let sampleStepHz = sample.stepHz {
- self.stepHz = sampleStepHz
- } else if LocomotionManager.highlander.recordPedometerEvents {
- self.stepHz = 0 // store nil as zero, because CMPedometer returns nil while stationary
- } else {
- self.stepHz = nil
- }
- }
-
- public required init(from dict: [String: Any?]) {
- if let uuidString = dict["sampleId"] as? String {
- self.sampleId = UUID(uuidString: uuidString)!
- } else {
- self.sampleId = UUID()
- }
- self.date = dict["date"] as! Date
- if let secondsFromGMT = dict["secondsFromGMT"] as? Int { self.secondsFromGMT = secondsFromGMT }
- else if let secondsFromGMT = dict["secondsFromGMT"] as? Int64 { self.secondsFromGMT = Int(secondsFromGMT) }
- else { self.secondsFromGMT = nil }
- self.movingState = MovingState(rawValue: dict["movingState"] as! String)!
- self.recordingState = RecordingState(rawValue: dict["recordingState"] as! String)!
- self.stepHz = dict["stepHz"] as? Double
- self.courseVariance = dict["courseVariance"] as? Double
- self.xyAcceleration = dict["xyAcceleration"] as? Double
- self.zAcceleration = dict["zAcceleration"] as? Double
- if let rawValue = dict["coreMotionActivityType"] as? String {
- self.coreMotionActivityType = CoreMotionActivityTypeName(rawValue: rawValue)
- } else {
- self.coreMotionActivityType = nil
- }
- if let location = dict["location"] as? CLLocation {
- self.location = location
- } else {
- var locationDict = dict
- locationDict["timestamp"] = dict["date"]
- self.location = CLLocation(from: locationDict)
- }
- if let rawValue = dict["confirmedType"] as? String {
- self.confirmedType = ActivityTypeName(rawValue: rawValue)
- } else {
- self.confirmedType = nil
- }
- if let rawValue = dict["classifiedType"] as? String {
- self._classifiedType = ActivityTypeName(rawValue: rawValue)
- } else {
- self._classifiedType = nil
- }
- if let rawValue = dict["previousSampleConfirmedType"] as? String {
- self.previousSampleConfirmedType = ActivityTypeName(rawValue: rawValue)
- } else {
- self.previousSampleConfirmedType = nil
- }
-
- self.rawLocations = nil
- self.filteredLocations = nil
- }
-
- /// For recording samples to mark special events such as app termination.
- public required init(date: Date, location: CLLocation? = nil, movingState: MovingState = .uncertain,
- recordingState: RecordingState) {
- self.sampleId = UUID()
-
- self.recordingState = recordingState
- self.movingState = movingState
- self.date = date
- self.secondsFromGMT = TimeZone.current.secondsFromGMT()
-
- self.filteredLocations = []
- self.rawLocations = []
- self.location = location
-
- self.stepHz = nil
- self.courseVariance = nil
- self.xyAcceleration = nil
- self.zAcceleration = nil
- self.coreMotionActivityType = nil
- }
-
- // MARK: - Codable
-
- public required init(from decoder: Decoder) throws {
- let container = try decoder.container(keyedBy: CodingKeys.self)
-
- self.sampleId = (try? container.decode(UUID.self, forKey: .sampleId)) ?? UUID()
- self.date = try container.decode(Date.self, forKey: .date)
- self.secondsFromGMT = try? container.decode(Int.self, forKey: .secondsFromGMT)
- self.movingState = try container.decode(MovingState.self, forKey: .movingState)
- self.recordingState = try container.decode(RecordingState.self, forKey: .recordingState)
- self.stepHz = try? container.decode(Double.self, forKey: .stepHz)
- self.courseVariance = try? container.decode(Double.self, forKey: .courseVariance)
- self.xyAcceleration = try? container.decode(Double.self, forKey: .xyAcceleration)
- self.zAcceleration = try? container.decode(Double.self, forKey: .zAcceleration)
- self.coreMotionActivityType = try? container.decode(CoreMotionActivityTypeName.self, forKey: .coreMotionActivityType)
- self.confirmedType = try? container.decode(ActivityTypeName.self, forKey: .confirmedType)
-
- if let codableLocation = try? container.decode(CodableLocation.self, forKey: .location) {
- self.location = CLLocation(from: codableLocation)
- } else {
- self.location = nil
- }
-
- self.rawLocations = nil
- self.filteredLocations = nil
- }
-
- open func encode(to encoder: Encoder) throws {
- var container = encoder.container(keyedBy: CodingKeys.self)
- try container.encode(sampleId, forKey: .sampleId)
- try container.encode(date, forKey: .date)
- if secondsFromGMT != nil { try container.encode(secondsFromGMT, forKey: .secondsFromGMT) }
- try container.encode(location?.codable, forKey: .location)
- try container.encode(movingState, forKey: .movingState)
- try container.encode(recordingState, forKey: .recordingState)
- if stepHz != nil { try container.encode(stepHz, forKey: .stepHz) }
- if courseVariance != nil { try container.encode(courseVariance, forKey: .courseVariance) }
- if xyAcceleration != nil { try container.encode(xyAcceleration, forKey: .xyAcceleration) }
- if zAcceleration != nil { try container.encode(zAcceleration, forKey: .zAcceleration) }
- if coreMotionActivityType != nil { try container.encode(coreMotionActivityType, forKey: .coreMotionActivityType) }
- if confirmedType != nil { try container.encode(confirmedType, forKey: .confirmedType) }
- }
-
- private enum CodingKeys: String, CodingKey {
- case sampleId
- case date
- case secondsFromGMT
- case location
- case movingState
- case recordingState
- case stepHz
- case courseVariance
- case xyAcceleration
- case zAcceleration
- case coreMotionActivityType
- case confirmedType
- }
-}
-
-extension LocomotionSample: CustomStringConvertible {
- public var description: String {
- guard let locations = filteredLocations else { return "LocomotionSample \(sampleId)" }
- let seconds = locations.dateInterval?.duration ?? 0
- let locationsN = locations.count
- let locationsHz = locationsN > 0 && seconds > 0 ? Double(locationsN) / seconds : 0.0
- return String(format: "\(locationsN) locations (%.1f Hz), \(String(duration: seconds))", locationsHz)
- }
-}
-
-extension LocomotionSample: Hashable {
- public func hash(into hasher: inout Hasher) { hasher.combine(sampleId) }
- public static func ==(lhs: LocomotionSample, rhs: LocomotionSample) -> Bool { return lhs.sampleId == rhs.sampleId }
-}
-
-public extension Array where Element: LocomotionSample {
- var duration: TimeInterval {
- guard let firstDate = first?.date, let lastDate = last?.date else { return 0 }
- return lastDate.timeIntervalSince(firstDate)
- }
- var distance: CLLocationDistance {
- return compactMap { $0.hasUsableCoordinate ? $0.location : nil }.distance
- }
- var weightedMeanAltitude: CLLocationDistance? {
- return compactMap { $0.hasUsableCoordinate ? $0.location : nil }.weightedMeanAltitude
- }
- var horizontalAccuracyRange: AccuracyRange? {
- return compactMap { $0.hasUsableCoordinate ? $0.location : nil }.horizontalAccuracyRange
- }
- var verticalAccuracyRange: AccuracyRange? {
- return compactMap { $0.hasUsableCoordinate ? $0.location : nil }.verticalAccuracyRange
- }
- var haveAnyUsableLocations: Bool {
- for sample in self { if sample.hasUsableCoordinate { return true } }
- return false
- }
- func radius(from center: CLLocation) -> Radius {
- return compactMap { $0.hasUsableCoordinate ? $0.location : nil }.radius(from: center)
- }
-
- // MARK: -
-
- var center: CLLocation? { return CLLocation(centerFor: self) }
-
- /**
- The weighted centre for an array of samples
- - Note: More weight will be given to samples classified with "stationary" type
- */
- var weightedCenter: CLLocation? {
- if self.isEmpty { return nil }
-
- guard let accuracyRange = self.horizontalAccuracyRange else { return nil }
-
- // only one sample? that's the centre then
- if self.count == 1, let first = self.first {
- return first.hasUsableCoordinate ? first.location : nil
- }
-
- var sumx: Double = 0, sumy: Double = 0, sumz: Double = 0, totalWeight: Double = 0
-
- for sample in self where sample.hasUsableCoordinate {
- guard let location = sample.location else { continue }
-
- let lat = location.coordinate.latitude.radiansValue
- let lng = location.coordinate.longitude.radiansValue
-
- var weight = location.horizontalAccuracyWeight(inRange: accuracyRange)
-
- // give extra weight to stationary samples
- if let activityType = sample.activityType, activityType == .stationary { weight *= 3 }
-
- sumx += (cos(lat) * cos(lng)) * weight
- sumy += (cos(lat) * sin(lng)) * weight
- sumz += sin(lat) * weight
- totalWeight += weight
- }
-
- if totalWeight == 0 { return nil }
-
- let meanx = sumx / totalWeight
- let meany = sumy / totalWeight
- let meanz = sumz / totalWeight
-
- return CLLocation(x: meanx, y: meany, z: meanz)
- }
-}
diff --git a/LocoKit/Info.plist b/LocoKit/Info.plist
deleted file mode 100644
index 1007fd9d..00000000
--- a/LocoKit/Info.plist
+++ /dev/null
@@ -1,24 +0,0 @@
-
-
-
-
- CFBundleDevelopmentRegion
- $(DEVELOPMENT_LANGUAGE)
- CFBundleExecutable
- $(EXECUTABLE_NAME)
- CFBundleIdentifier
- $(PRODUCT_BUNDLE_IDENTIFIER)
- CFBundleInfoDictionaryVersion
- 6.0
- CFBundleName
- $(PRODUCT_NAME)
- CFBundlePackageType
- FMWK
- CFBundleShortVersionString
- 1.0
- CFBundleVersion
- $(CURRENT_PROJECT_VERSION)
- NSPrincipalClass
-
-
-
diff --git a/LocoKit/LocoKit.h b/LocoKit/LocoKit.h
deleted file mode 100644
index 877122ff..00000000
--- a/LocoKit/LocoKit.h
+++ /dev/null
@@ -1,16 +0,0 @@
-//
-// LocoKit.h
-// LocoKit
-//
-// Created by Matt Greenfield on 22/11/17.
-// Copyright © 2017 Big Paua. All rights reserved.
-//
-
-#import
-
-//! Project version number for LocoKit.
-FOUNDATION_EXPORT double LocoKitVersionNumber;
-
-//! Project version string for LocoKit.
-FOUNDATION_EXPORT const unsigned char LocoKitVersionString[];
-
diff --git a/LocoKit/Timelines/ActivityTypes/ActivityType.scores.swift b/LocoKit/Timelines/ActivityTypes/ActivityType.scores.swift
deleted file mode 100644
index efff1cbd..00000000
--- a/LocoKit/Timelines/ActivityTypes/ActivityType.scores.swift
+++ /dev/null
@@ -1,212 +0,0 @@
-//
-// ActivityType.scores.swift
-// LocoKitCore
-//
-// Created by Matt Greenfield on 14/12/16.
-// Copyright © 2016 Big Paua. All rights reserved.
-//
-
-import CoreLocation
-
-extension ActivityType {
-
- var bucketMax: Int {
- switch depth {
- case 2: return ActivityType.latLongBucketMaxDepth2
- case 1: return ActivityType.latLongBucketMaxDepth1
- default: return ActivityType.latLongBucketMaxDepth0
- }
- }
-
- public func scoreFor(classifiable scorable: ActivityTypeClassifiable, previousResults: ClassifierResults?) -> Double {
- let depth = self.depth
-
- // motion weights
- let movingWeight = 1.0
- var speedWeight = 1.0
- var stepHzWeight = 1.0
- var varianceWeight = 1.0
- var xyWeight = 1.0
- var zWeight = 1.0
-
- // context weights
- let timeOfDayWeight = 1.0
- let courseWeight = 1.0
- let altitudeWeight = 1.0
- var latLongWeight = 1.0
- let horizAccuracyWeight = 1.0
- let markovWeight = 1.0
-
- if depth == 2 {
- speedWeight = 2.0 // cars, trains, etc go different speeds in different locales
- stepHzWeight = 1.2 // local roads equal different fake step jiggles
- varianceWeight = 1.2 // local location accuracy plays a big part in variance
- xyWeight = 1.2 // local roads equal different jiggles
- zWeight = 1.2 // local roads equal different jiggles
- latLongWeight = 10.0 // local usage locations per type are massive important
- }
-
- var scores: [Double] = []
-
- /** motion scores **/
-
- if let movingScore = self.movingScore(for: scorable.movingState) {
- scores.append(movingScore * movingWeight)
- }
-
- if let speed = scorable.location?.speed, speed >= 0 {
- scores.append(speedScore(for: speed) * speedWeight)
- }
-
- if let stepHz = scorable.stepHz {
- scores.append(stepHzScore(for: stepHz) * stepHzWeight)
- }
-
- if let courseVariance = scorable.courseVariance {
- scores.append(courseVarianceScore(for: courseVariance) * varianceWeight)
- }
-
- if name != .stationary && name != .bogus { // stationary and bogus are allowed any kinds of wiggles
- if let xyAcceleration = scorable.xyAcceleration {
- scores.append(xyScore(for: xyAcceleration) * xyWeight)
- }
-
- if let zAcceleration = scorable.zAcceleration {
- scores.append(zScore(for: zAcceleration) * zWeight)
- }
- }
-
- /** context scores **/
-
- if name != .bogus, let previous = previousResults?.first?.name, !previousSampleActivityTypeScores.isEmpty {
- scores.append(previousTypeScore(for: previous) * markovWeight)
- }
-
- if let altitude = scorable.location?.altitude, altitude != LocomotionMagicValue.nilAltitude {
- scores.append(altitudeScore(for: altitude) * altitudeWeight)
- }
-
- if depth > 0 && name != .stationary { // D0 and stationary should ignore these context factors
- if let course = scorable.location?.course, course >= 0 {
- scores.append(courseScore(for: course) * courseWeight)
- }
-
- // walking and running are golden childs. don't bother with time of day checks
- if name != .walking && name != .running {
- scores.append(timeOfDayScore(for: scorable.timeOfDay) * timeOfDayWeight)
- }
- }
-
- if depth > 0 { // coords are irrelevant at D0
- if let coordinate = scorable.location?.coordinate {
- scores.append(latLongScore(for: coordinate) * latLongWeight)
- }
- }
-
- if depth == 2 { // horizontalAccuracy is very neighbourhood specific
- if let accuracy = scorable.location?.horizontalAccuracy, accuracy >= 0 {
- scores.append(horizAccuracyScore(for: accuracy) * horizAccuracyWeight)
- }
- }
-
- let score = scores.reduce(1.0, *)
-
- return score.clamped(min: 0, max: 1)
- }
-
- // MARK: -
-
- func scoreFor(_ value: Double, in histogram: Histogram) -> Double {
- return histogram.probabilityFor(value)
- }
-
- // MARK: -
-
- func movingScore(for movingState: MovingState) -> Double? {
- if movingState == .uncertain {
- return nil
- }
-
- guard movingPct >= 0 else {
- return nil
- }
-
- let movingValue = movingPct
- let notMovingValue = 1.0 - movingPct
- let maxValue = max(movingValue, notMovingValue)
-
- return movingState == .moving
- ? movingValue / maxValue
- : notMovingValue / maxValue
- }
-
- func coreMotionScore(for coreMotionType: CoreMotionActivityTypeName) -> Double {
- guard let value = coreMotionTypeScores[coreMotionType] else { return 0 }
- guard let maxPercent = coreMotionTypeScores.values.max(), maxPercent > 0 else { return 0 }
- return value / maxPercent
- }
-
- func previousTypeScore(for previousType: ActivityTypeName) -> Double {
- guard let value = previousSampleActivityTypeScores[previousType] else { return 0 }
- guard let maxPercent = previousSampleActivityTypeScores.values.max(), maxPercent > 0 else { return 0 }
- return value / maxPercent
- }
-
- func speedScore(for speed: Double) -> Double {
- return speedHistogram?.probabilityFor(speed) ?? 0
- }
-
- func stepHzScore(for stepHz: Double) -> Double {
- return stepHzHistogram?.probabilityFor(stepHz) ?? 0
- }
-
- func xyScore(for value: Double) -> Double {
- return xyAccelerationHistogram?.probabilityFor(value) ?? 0
- }
-
- func zScore(for value: Double) -> Double {
- return zAccelerationHistogram?.probabilityFor(value) ?? 0
- }
-
- func courseScore(for value: Double) -> Double {
- return courseHistogram?.probabilityFor(value) ?? 0
- }
-
- func courseVarianceScore(for value: Double) -> Double {
- return courseVarianceHistogram?.probabilityFor(value) ?? 0
- }
-
- func altitudeScore(for value: Double) -> Double {
- return altitudeHistogram?.probabilityFor(value) ?? 0
- }
-
- func timeOfDayScore(for value: Double) -> Double {
- return timeOfDayHistogram?.probabilityFor(value) ?? 0
- }
-
- func horizAccuracyScore(for value: Double) -> Double {
- return horizontalAccuracyHistogram?.probabilityFor(value) ?? 0
- }
-
- func latLongScore(for coordinate: CLLocationCoordinate2D) -> Double {
- return coordinatesMatrix?.probabilityFor(coordinate, maxThreshold: bucketMax) ?? 0
- }
-
- // MARK: -
-
- var coreMotionTypeScoresString: String {
- var scores = coreMotionTypeScores.map { name, score in (name: name, score: score) }
-
- scores.sort { $0.score > $1.score }
-
- var result = ""
- for type in scores {
- if type.score > 0 {
- result += "\(type.name): " + String(format: "%.2f", type.score) + ", "
- }
- }
-
- return result.trimmingCharacters(in: CharacterSet(charactersIn: ", "))
- }
-
-}
diff --git a/LocoKit/Timelines/ActivityTypes/ActivityType.swift b/LocoKit/Timelines/ActivityTypes/ActivityType.swift
deleted file mode 100644
index 49a6cdc7..00000000
--- a/LocoKit/Timelines/ActivityTypes/ActivityType.swift
+++ /dev/null
@@ -1,479 +0,0 @@
-//
-// Created by Matt Greenfield on 23/05/16.
-// Copyright (c) 2016 Big Paua. All rights reserved.
-//
-
-import os.log
-import CoreMotion
-import CoreLocation
-import GRDB
-
-open class ActivityType: MLModel, PersistableRecord {
-
- public static let currentVersion = 700000
-
- static let numberOfLatBucketsDepth0 = 18
- static let numberOfLongBucketsDepth0 = 36
- static let numberOfLatBucketsDepth1 = 100
- static let numberOfLongBucketsDepth1 = 100
- static let numberOfLatBucketsDepth2 = 200
- static let numberOfLongBucketsDepth2 = 200
-
- static let latLongBucketMaxDepth0 = 7200 // cutoff of roughly 12 hours of data for each depth 0 bucket
- static let latLongBucketMaxDepth1 = 1800 // cutoff of roughly 3 hours of data for each depth 1 bucket
- static let latLongBucketMaxDepth2 = 80 // cutoff of roughly 8 mins of data for each depth 2 bucket
-
- public var store: TimelineStore?
-
- public internal(set) var name: ActivityTypeName
- public internal(set) var geoKey: String = ""
- public internal(set) var isShared: Bool
- public internal(set) var version: Int = 0
- internal var geoKeyPrefix = "G"
-
- public internal(set) var depth: Int
- public internal(set) var accuracyScore: Double?
- public internal(set) var totalSamples: Int = 0
-
- public internal(set) var lastFetched: Date
- public internal(set) var lastUpdated: Date?
- public internal(set) var transactionDate: Date?
-
- /** movement factors **/
-
- var movingPct: Double = -1
- var coreMotionTypeScores: [CoreMotionActivityTypeName: Double] = [:]
- var speedHistogram: Histogram?
- var stepHzHistogram: Histogram?
- var courseVarianceHistogram: Histogram?
- var xyAccelerationHistogram: Histogram?
- var zAccelerationHistogram: Histogram?
- var horizontalAccuracyHistogram: Histogram?
- var previousSampleActivityTypeScores: [ActivityTypeName: Double] = [:]
-
- /** context factors **/
-
- var courseHistogram: Histogram?
- var altitudeHistogram: Histogram?
- var timeOfDayHistogram: Histogram?
- var serialisedCoordinatesMatrix: String?
-
- lazy var coordinatesMatrix: CoordinatesMatrix? = {
- if let string = self.serialisedCoordinatesMatrix {
- return CoordinatesMatrix(string: string)
- }
- return nil
- }()
-
- var cachedHashValue: Int?
-
- public var latitudeRange: (min: Double, max: Double) = (0, 0)
- public var longitudeRange: (min: Double, max: Double) = (0, 0)
-
- public var coreMotionTypeScoresArray: [Double] {
- var result: [Double] = []
- for typeName in CoreMotionActivityTypeName.allTypes {
- if let score = coreMotionTypeScores[typeName] {
- result.append(score)
- } else {
- result.append(0)
- }
- }
- return result
- }
-
- public var previousSampleActivityTypeScoresSerialised: String {
- var result = ""
- for (activityType, score) in previousSampleActivityTypeScores {
- result += "\(activityType.rawValue):\(score);"
- }
- return result
- }
-
- // MARK: Init
-
- public init?(dict: [String: Any?], geoKeyPrefix: String? = nil, in store: TimelineStore) {
- self.store = store
-
- guard let string = dict["name"] as? String, let name = ActivityTypeName(rawValue: string) else {
- return nil
- }
- self.name = name
-
- self.lastSaved = dict["lastSaved"] as? Date
- self.lastUpdated = dict["lastUpdated"] as? Date
- self.lastFetched = Date()
-
- if let geoKeyPrefix = geoKeyPrefix {
- self.geoKeyPrefix = geoKeyPrefix
- }
-
- if let latitudeMin = dict["latitudeMin"] as? Double, let latitudeMax = dict["latitudeMax"] as? Double {
- self.latitudeRange.min = latitudeMin
- self.latitudeRange.max = latitudeMax
- }
-
- if let longitudeMin = dict["longitudeMin"] as? Double, let longitudeMax = dict["longitudeMax"] as? Double {
- self.longitudeRange.min = longitudeMin
- self.longitudeRange.max = longitudeMax
- }
-
- isShared = dict["isShared"] as? Bool ?? true
-
- if let depth = dict["depth"] as? Int { self.depth = depth }
- else if let depth = dict["depth"] as? Int64 { self.depth = Int(depth) }
- else { fatalError("nil model depth") }
-
- geoKey = dict["geoKey"] as? String ?? inferredGeoKey
-
- if let version = dict["version"] as? Int { self.version = version }
- else if let version = dict["version"] as? Int64 { self.version = Int(version) }
-
- if let total = dict["totalSamples"] as? Int { totalSamples = total }
- else if let total = dict["totalSamples"] as? Int64 { totalSamples = Int(total) }
- else if let total = dict["totalEvents"] as? Int { totalSamples = total }
- else if let total = dict["totalEvents"] as? Int64 { totalSamples = Int(total) }
-
- accuracyScore = dict["accuracyScore"] as? Double
-
- if let updated = dict["lastUpdated"] as? Date {
- lastUpdated = updated
- } else if let updated = dict["lastUpdated"] as? Double { // expecting JS style bullshit milliseconds
- lastUpdated = Date(timeIntervalSince1970: updated / 1000)
- }
-
- movingPct = dict["movingPct"] as? Double ?? -1
-
- if let serialised = dict["speedHistogram"] as? String {
- speedHistogram = Histogram(string: serialised)
- speedHistogram?.printModifier = 3.6
- speedHistogram?.printFormat = "%6.1f kmh"
- speedHistogram?.name = "SPEED"
- }
-
- if let serialised = dict["stepHzHistogram"] as? String {
- stepHzHistogram = Histogram(string: serialised)
- stepHzHistogram?.printFormat = "%7.2f Hz"
- stepHzHistogram?.name = "STEPHZ"
- }
-
- if let serialised = dict["courseVarianceHistogram"] as? String {
- courseVarianceHistogram = Histogram(string: serialised)
- courseVarianceHistogram?.printFormat = "%10.2f"
- courseVarianceHistogram?.name = "COURSE VARIANCE"
- }
-
- if let serialised = dict["courseHistogram"] as? String {
- courseHistogram = Histogram(string: serialised)
- courseHistogram?.name = "COURSE"
- }
-
- if let serialised = dict["altitudeHistogram"] as? String {
- altitudeHistogram = Histogram(string: serialised)
- altitudeHistogram?.name = "ALTITUDE"
- }
-
- if let serialised = dict["timeOfDayHistogram"] as? String {
- timeOfDayHistogram = Histogram(string: serialised)
- timeOfDayHistogram?.printModifier = 60 / 60 / 60 / 60
- timeOfDayHistogram?.printFormat = "%8.2f h"
- timeOfDayHistogram?.name = "TIME OF DAY"
- }
-
- if let serialised = dict["xyAccelerationHistogram"] as? String {
- xyAccelerationHistogram = Histogram(string: serialised)
- xyAccelerationHistogram?.name = "WIGGLES XY"
- }
-
- if let serialised = dict["zAccelerationHistogram"] as? String {
- zAccelerationHistogram = Histogram(string: serialised)
- zAccelerationHistogram?.name = "WIGGLES Z"
- }
-
- if let serialised = dict["horizontalAccuracyHistogram"] as? String {
- horizontalAccuracyHistogram = Histogram(string: serialised)
- horizontalAccuracyHistogram?.name = "HORIZ ACCURACY"
- }
-
- serialisedCoordinatesMatrix = dict["coordinatesMatrix"] as? String
-
- var cmTypeScoreDoubles: [Double]?
- if let cmTypeScores = dict["coreMotionTypeScores"] as? String {
- cmTypeScoreDoubles = cmTypeScores.split(separator: ",", omittingEmptySubsequences: false).map { Double($0) ?? 0 }
- } else if let doubles = dict["coreMotionTypeScores"] as? [Double], !doubles.isEmpty {
- cmTypeScoreDoubles = doubles
- }
- if let doubles = cmTypeScoreDoubles {
- for (index, score) in doubles.enumerated() {
- let name = CoreMotionActivityTypeName.allTypes[index]
- coreMotionTypeScores[name] = score
- }
- }
-
- if let markovScores = dict["previousSampleActivityTypeScores"] as? String {
- var typeScores: [ActivityTypeName: Double] = [:]
- let typeScoreRows = markovScores.split(separator: ";")
- for row in typeScoreRows {
- let bits = row.split(separator: ":")
- guard let name = ActivityTypeName(rawValue: String(bits[0])) else { continue }
- guard let score = Double(String(bits[1])) else { continue }
- typeScores[name] = score
- }
- previousSampleActivityTypeScores = typeScores
- }
-
- store.add(self)
- }
-
- var inferredGeoKey: String {
- return String(format: "\(geoKeyPrefix)D\(depth) \(name) %.2f,%.2f", centerCoordinate.latitude, centerCoordinate.longitude)
- }
-
- // MARK: - Misc computed properties
-
- public var completenessScore: Double {
- let parentDepth = depth - 1
-
- guard parentDepth >= 0 else { return 1.0 }
-
- var maxEvents: Int
- switch parentDepth {
- case 2: maxEvents = ActivityType.latLongBucketMaxDepth2
- case 1: maxEvents = ActivityType.latLongBucketMaxDepth1
- default: maxEvents = ActivityType.latLongBucketMaxDepth0
- }
-
- return min(1.0, Double(totalSamples) / Double(maxEvents))
- }
-
- public var coverageScore: Double {
- if let accuracyScore = accuracyScore {
- return accuracyScore * completenessScore
- }
- return completenessScore
- }
-
- var numberOfLatBuckets: Int {
- switch depth {
- case 2: return ActivityType.numberOfLatBucketsDepth2
- case 1: return ActivityType.numberOfLatBucketsDepth1
- default: return ActivityType.numberOfLatBucketsDepth0
- }
- }
-
- var numberOfLongBuckets: Int {
- switch depth {
- case 2: return ActivityType.numberOfLongBucketsDepth2
- case 1: return ActivityType.numberOfLongBucketsDepth1
- default: return ActivityType.numberOfLongBucketsDepth0
- }
- }
-
- var latitudeWidth: Double { return latitudeRange.max - latitudeRange.min }
-
- var longitudeWidth: Double { return longitudeRange.max - longitudeRange.min }
-
- public var centerCoordinate: CLLocationCoordinate2D {
- return CLLocationCoordinate2D(latitude: latitudeRange.min + latitudeWidth * 0.5,
- longitude: longitudeRange.min + longitudeWidth * 0.5)
- }
-
- public static func latitudeBinSizeFor(depth: Int) -> Double {
- let depth0 = 180.0 / Double(ActivityType.numberOfLatBucketsDepth0)
- let depth1 = depth0 / Double(ActivityType.numberOfLatBucketsDepth1)
- let depth2 = depth1 / Double(ActivityType.numberOfLatBucketsDepth2)
-
- switch depth {
- case 2: return depth2
- case 1: return depth1
- default: return depth0
- }
- }
-
- public static func longitudeBinSizeFor(depth: Int) -> Double {
- let depth0 = 360.0 / Double(ActivityType.numberOfLongBucketsDepth0)
- let depth1 = depth0 / Double(ActivityType.numberOfLongBucketsDepth1)
- let depth2 = depth1 / Double(ActivityType.numberOfLongBucketsDepth2)
-
- switch depth {
- case 2: return depth2
- case 1: return depth1
- default: return depth0
- }
- }
-
- public static func latitudeRangeFor(depth: Int, coordinate: CLLocationCoordinate2D) -> (min: Double, max: Double) {
- let depth0Range = (min: -90.0, max: 90.0)
-
- switch depth {
- case 2:
- let bucketSize = ActivityType.latitudeBinSizeFor(depth: 1)
- let parentRange = latitudeRangeFor(depth: 1, coordinate: coordinate)
- let bucket = Int((coordinate.latitude - parentRange.min) / bucketSize)
- return (min: parentRange.min + (bucketSize * Double(bucket)),
- max: parentRange.min + (bucketSize * Double(bucket + 1)))
-
- case 1:
- let bucketSize = ActivityType.latitudeBinSizeFor(depth: 0)
- let parentRange = latitudeRangeFor(depth: 0, coordinate: coordinate)
- let bucket = Int((coordinate.latitude - parentRange.min) / bucketSize)
- return (min: parentRange.min + (bucketSize * Double(bucket)),
- max: parentRange.min + (bucketSize * Double(bucket + 1)))
-
- default:
- return depth0Range
- }
- }
-
- public static func longitudeRangeFor(depth: Int, coordinate: CLLocationCoordinate2D) -> (min: Double, max: Double) {
- let depth0Range = (min: -180.0, max: 180.0)
-
- switch depth {
- case 2:
- let bucketSize = ActivityType.longitudeBinSizeFor(depth: 1)
- let parentRange = longitudeRangeFor(depth: 1, coordinate: coordinate)
- let bucket = Int((coordinate.longitude - parentRange.min) / bucketSize)
- return (min: parentRange.min + (bucketSize * Double(bucket)),
- max: parentRange.min + (bucketSize * Double(bucket + 1)))
-
- case 1:
- let bucketSize = ActivityType.longitudeBinSizeFor(depth: 0)
- let parentRange = longitudeRangeFor(depth: 0, coordinate: coordinate)
- let bucket = Int((coordinate.longitude - parentRange.min) / bucketSize)
- return (min: parentRange.min + (bucketSize * Double(bucket)),
- max: parentRange.min + (bucketSize * Double(bucket + 1)))
-
- default:
- return depth0Range
- }
- }
-
- public func contains(coordinate: CLLocationCoordinate2D) -> Bool {
- return contains(coordinate: coordinate, acceptZeroZero: false)
- }
-
- func contains(coordinate: CLLocationCoordinate2D, acceptZeroZero: Bool = false) -> Bool {
- guard CLLocationCoordinate2DIsValid(coordinate) else { return false }
-
- guard acceptZeroZero || coordinate.latitude != 0 || coordinate.longitude != 0 else { return false }
-
- let latRange = latitudeRange
- let longRange = longitudeRange
-
- if latRange.min > coordinate.latitude || latRange.max < coordinate.latitude { return false }
- if longRange.min > coordinate.longitude || longRange.max < coordinate.longitude { return false }
-
- return true
- }
-
- // MARK: - Debug output
-
- public func printStats() {
- print(statsString)
- }
-
- var statsString: String {
- var output = ""
-
- output += "geoKey: \(geoKey)\n"
- output += "totalSamples: \(totalSamples)\n"
-
- if let accuracy = accuracyScore {
- output += String(format: "accuracyScore: %.2f\n\n", accuracy)
- }
-
- output += "movingPct: \(String(format: "%.2f", movingPct))\n"
- output += "coreMotionTypeScores: \(coreMotionTypeScoresString)\n"
-
- if let matrix = coordinatesMatrix {
- output += matrix.description
- } else {
- output += "NO COORDS MATRIX\n"
- }
-
- if let histogram = speedHistogram { output += String(describing: histogram) + "\n" }
- if let histogram = stepHzHistogram { output += String(describing: histogram) + "\n" }
- if let histogram = xyAccelerationHistogram { output += String(describing: histogram) + "\n" }
- if let histogram = zAccelerationHistogram { output += String(describing: histogram) + "\n" }
- if let histogram = courseVarianceHistogram { output += String(describing: histogram) + "\n" }
-
- if let histogram = timeOfDayHistogram { output += String(describing: histogram) + "\n" }
- if let histogram = altitudeHistogram { output += String(describing: histogram) + "\n" }
- if let histogram = courseHistogram { output += String(describing: histogram) + "\n" }
-
- return output
- }
-
- // MARK: - Saving
-
- public var lastSaved: Date?
-
- public func save() {
- do {
- try store?.auxiliaryPool.write { db in
- self.transactionDate = Date()
- try self.save(in: db)
- self.lastSaved = self.transactionDate
- }
- } catch {
- os_log("%@", type: .error, error.localizedDescription)
- }
- }
-
- public var unsaved: Bool { return lastSaved == nil }
- public func save(in db: Database) throws {
- if unsaved { try insert(db) } else { try update(db) }
- }
-
- // MARK: - PersistableRecord
-
- public static let databaseTableName = "ActivityTypeModel"
-
- public static var persistenceConflictPolicy: PersistenceConflictPolicy {
- return PersistenceConflictPolicy(insert: .replace, update: .abort)
- }
-
- open func encode(to container: inout PersistenceContainer) {
- container["geoKey"] = geoKey
- container["lastSaved"] = transactionDate ?? lastSaved ?? Date()
- container["version"] = version
-
- container["name"] = name.rawValue
- container["depth"] = depth
- container["isShared"] = isShared
- container["lastUpdated"] = lastUpdated
- container["totalSamples"] = totalSamples
- container["accuracyScore"] = accuracyScore
-
- container["latitudeMin"] = latitudeRange.min
- container["latitudeMax"] = latitudeRange.max
- container["longitudeMin"] = longitudeRange.min
- container["longitudeMax"] = longitudeRange.max
-
- container["movingPct"] = movingPct
- container["coreMotionTypeScores"] = coreMotionTypeScoresArray.map { String($0) }.joined(separator: ",")
- container["previousSampleActivityTypeScores"] = previousSampleActivityTypeScoresSerialised
-
- container["altitudeHistogram"] = altitudeHistogram?.serialised
- container["courseHistogram"] = courseHistogram?.serialised
- container["courseVarianceHistogram"] = courseVarianceHistogram?.serialised
- container["speedHistogram"] = speedHistogram?.serialised
- container["stepHzHistogram"] = stepHzHistogram?.serialised
- container["timeOfDayHistogram"] = timeOfDayHistogram?.serialised
- container["xyAccelerationHistogram"] = xyAccelerationHistogram?.serialised
- container["zAccelerationHistogram"] = zAccelerationHistogram?.serialised
- container["horizontalAccuracyHistogram"] = horizontalAccuracyHistogram?.serialised
- container["coordinatesMatrix"] = coordinatesMatrix?.serialised
- }
-
- // MARK: - Equatable
-
- open func hash(into hasher: inout Hasher) {
- hasher.combine(geoKey)
- }
-
- public static func ==(lhs: ActivityType, rhs: ActivityType) -> Bool {
- return lhs.hashValue == rhs.hashValue
- }
-
-}
diff --git a/LocoKit/Timelines/ActivityTypes/ActivityTypeClassifiable.swift b/LocoKit/Timelines/ActivityTypes/ActivityTypeClassifiable.swift
deleted file mode 100644
index 3162f227..00000000
--- a/LocoKit/Timelines/ActivityTypes/ActivityTypeClassifiable.swift
+++ /dev/null
@@ -1,25 +0,0 @@
-//
-// ActivityTypeScorable.swift
-// LearnerCoacher
-//
-// Created by Matt Greenfield on 8/01/17.
-// Copyright © 2017 Big Paua. All rights reserved.
-//
-
-import CoreLocation
-
-public protocol ActivityTypeClassifiable: class {
-
- var location: CLLocation? { get }
- var movingState: MovingState { get }
- var coreMotionActivityType: CoreMotionActivityTypeName? { get }
- var stepHz: Double? { get }
- var courseVariance: Double? { get }
- var xyAcceleration: Double? { get }
- var zAcceleration: Double? { get }
- var timeOfDay: TimeInterval { get }
- var previousSampleConfirmedType: ActivityTypeName? { get }
-
- var classifierResults: ClassifierResults? { get set }
-
-}
diff --git a/LocoKit/Timelines/ActivityTypes/ActivityTypeClassifier.swift b/LocoKit/Timelines/ActivityTypes/ActivityTypeClassifier.swift
deleted file mode 100644
index 94e123e1..00000000
--- a/LocoKit/Timelines/ActivityTypes/ActivityTypeClassifier.swift
+++ /dev/null
@@ -1,164 +0,0 @@
-//
-// ActivityTypeClassifier.swift
-// LocoKitCore
-//
-// Created by Matt Greenfield on 3/08/17.
-// Copyright © 2017 Big Paua. All rights reserved.
-//
-
-import os.log
-import CoreLocation
-
-/**
- Activity Type Classifiers are Machine Learning Classifiers. Use an Activity Type Classifier to determine the
- `ActivityTypeName` of a `LocomotionSample`.
-
- - Precondition: An API key is required to make use of classifiers. See `LocoKitService.apiKey` for details.
-
- ## Supported Activity Types
-
- #### Base Types
-
- stationary, walking, running, cycling
-
- Base types match one-to-one with [Core Motion activity types](https://developer.apple.com/documentation/coremotion/cmmotionactivity),
- with the exception of Core Motion's "automotive" type, which is instead handled by extended types in LocoKit.
-
- #### Extended Types
-
- car, train, bus, motorcycle, boat, airplane, tram, horseback, scooter, skateboarding, tractor, skiing,
- inline skating, metro, tuk-tuk, songthaew
-
- ## Region Specific Classifiers
-
- LocoKit provides geographical region specific machine learning data, with each classifier containing the data for a
- specific region.
-
- This allows for detecting activity types based on region specific characteristics, with much higher accuracy than
- iOS's built in Core Motion types detection. It also makes it possible to detect a greater number of activity types,
- for example distinguishing between travel by car or train.
-
- LocoKit's data regions are roughly 100 kilometres by 100 kilometres squared (0.1 by 0.1 degrees), or about the size of
- a small town, or a single neighbourhood in a larger city.
-
- Larger cities might encompass anywhere from four to ten or more classifier regions, thus allowing the classifers to
- accurately detect activity type differences within different areas of a single city.
-
- ## Determining Regional Coverage
-
- - [LocoKit transport coverage maps](https://www.bigpaua.com/locokit/coverage/transport)
- - [LocoKit cycling coverage maps](https://www.bigpaua.com/locokit/coverage/cycling)
-
- #### Stationary, Walking, Running, Cycling
-
- The base activity types of stationary, walking, running, and should achieve high detection accuracy everywhere in
- the world, regardless of local data availability.
-
- These types can be considered to have global coverage.
-
- #### Car, Train, Bus, Motorcycle, Airplane, Boat, etc
-
- Determining the specific mode of transport requires local knowledge. If knowing the specific mode of transport is
- important to your application, you should check the coverage maps for your required regions.
-
- When local data coverage is not high enough to distinguish specific modes of transport, a threshold probability
- score should be used on the "best match" classifier result, to determine when to fall back to presenting a generic
- "transport" classification to the user.
-
- For example if the highest scoring type is "cycling", but its probability score is only 0.001, that identifies it as
- a terrible match, thus the real type is most likely some other mode of transport. Your UI should then avoid claiming
- "cycling", and instead report a generic type name to the user, such as "transport", "automotive", or "unknown".
- */
-public class ActivityTypeClassifier: MLClassifier {
-
- public typealias Cache = ActivityTypesCache
- public typealias ParentClassifier = Cache.ParentClassifier
-
- let cache = Cache.highlander
-
- public let depth: Int
- public let supportedTypes: [ActivityTypeName]
- public let models: [Cache.Model]
-
- private var _parent: ParentClassifier?
- public var parent: ParentClassifier? {
- get {
- if let parent = _parent {
- return parent
- }
-
- let parentDepth = depth - 1
-
- // can only get supported depths
- guard cache.providesDepths.contains(parentDepth) else {
- return nil
- }
-
- // no point in getting a parent if current depth is complete
- guard completenessScore < 1 else {
- return nil
- }
-
- // can't do anything without a coord
- guard let coordinate = centerCoordinate else {
- return nil
- }
-
- // try to fetch one
- _parent = ParentClassifier(requestedTypes: supportedTypes, coordinate: coordinate, depth: parentDepth)
-
- return _parent
- }
-
- set (newParent) {
- _parent = newParent
- }
- }
-
- public lazy var lastUpdated: Date? = {
- return self.models.lastUpdated
- }()
-
- public lazy var lastFetched: Date = {
- return models.lastFetched
- }()
-
- public lazy var accuracyScore: Double? = {
- return self.models.accuracyScore
- }()
-
- public lazy var completenessScore: Double = {
- return self.models.completenessScore
- }()
-
- // MARK: - Init
-
- public convenience required init?(requestedTypes: [ActivityTypeName] = ActivityTypeName.baseTypes,
- coordinate: CLLocationCoordinate2D) {
- self.init(requestedTypes: requestedTypes, coordinate: coordinate, depth: 2)
- }
-
- convenience init?(requestedTypes: [ActivityTypeName], coordinate: CLLocationCoordinate2D, depth: Int) {
- if requestedTypes.isEmpty {
- return nil
- }
-
- let models = Cache.highlander.modelsFor(names: requestedTypes, coordinate: coordinate, depth: depth)
-
- guard !models.isEmpty else {
- return nil
- }
-
- self.init(supportedTypes: requestedTypes, models: models, depth: depth)
-
- // bootstrap the parent
- _ = parent
- }
-
- init(supportedTypes: [ActivityTypeName], models: [Cache.Model], depth: Int) {
- self.supportedTypes = supportedTypes
- self.depth = depth
- self.models = models
- }
-}
-
diff --git a/LocoKit/Timelines/ActivityTypes/ActivityTypeTrainable.swift b/LocoKit/Timelines/ActivityTypes/ActivityTypeTrainable.swift
deleted file mode 100644
index 98a9fae0..00000000
--- a/LocoKit/Timelines/ActivityTypes/ActivityTypeTrainable.swift
+++ /dev/null
@@ -1,14 +0,0 @@
-//
-// ActivityTypeTrainable.swift
-// LocoKitCore
-//
-// Created by Matt Greenfield on 20/08/17.
-// Copyright © 2017 Big Paua. All rights reserved.
-//
-
-public protocol ActivityTypeTrainable: ActivityTypeClassifiable {
-
- var confirmedType: ActivityTypeName? { get set }
- var classifiedType: ActivityTypeName? { get }
-
-}
diff --git a/LocoKit/Timelines/ActivityTypes/ActivityTypesCache.swift b/LocoKit/Timelines/ActivityTypes/ActivityTypesCache.swift
deleted file mode 100644
index 310aec2f..00000000
--- a/LocoKit/Timelines/ActivityTypes/ActivityTypesCache.swift
+++ /dev/null
@@ -1,105 +0,0 @@
-//
-// ActivityTypesCache.swift
-// LocoKitCore
-//
-// Created by Matt Greenfield on 30/07/17.
-// Copyright © 2017 Big Paua. All rights reserved.
-//
-
-import os.log
-import CoreLocation
-import GRDB
-
-public final class ActivityTypesCache: MLModelSource {
-
- public typealias Model = ActivityType
- public typealias ParentClassifier = ActivityTypeClassifier
-
- public static var highlander = ActivityTypesCache()
-
- internal static let minimumRefetchWait: TimeInterval = .oneHour
- internal static let staleLastUpdatedAge: TimeInterval = .oneMonth * 2
- internal static let staleLastFetchedAge: TimeInterval = .oneWeek
-
- public var store: TimelineStore?
- let mutex = UnfairLock()
-
- public init() {}
-
- public var providesDepths = [0, 1, 2]
-
- public func modelFor(name: ActivityTypeName, coordinate: CLLocationCoordinate2D, depth: Int) -> ActivityType? {
- guard let store = store else { return nil }
- guard providesDepths.contains(depth) else { return nil }
-
- var query = "SELECT * FROM ActivityTypeModel WHERE isShared = 1 AND name = ? AND depth = ?"
- var arguments: [DatabaseValueConvertible] = [name.rawValue, depth]
-
- if depth > 0 {
- query += " AND latitudeMin <= ? AND latitudeMax >= ? AND longitudeMin <= ? AND longitudeMax >= ?"
- arguments.append(coordinate.latitude)
- arguments.append(coordinate.latitude)
- arguments.append(coordinate.longitude)
- arguments.append(coordinate.longitude)
- }
-
- return store.model(for: query, arguments: StatementArguments(arguments))
- }
-
- public func modelsFor(names: [ActivityTypeName], coordinate: CLLocationCoordinate2D, depth: Int) -> [ActivityType] {
- guard let store = store else { return [] }
- guard providesDepths.contains(depth) else { return [] }
-
- var query = "SELECT * FROM ActivityTypeModel WHERE isShared = 1 AND depth = ?"
- var arguments: [DatabaseValueConvertible] = [depth]
-
- let marks = repeatElement("?", count: names.count).joined(separator: ",")
- query += " AND name IN (\(marks))"
- arguments += names.map { $0.rawValue } as [DatabaseValueConvertible]
-
- if depth > 0 {
- query += " AND latitudeMin <= ? AND latitudeMax >= ? AND longitudeMin <= ? AND longitudeMax >= ?"
- arguments.append(coordinate.latitude)
- arguments.append(coordinate.latitude)
- arguments.append(coordinate.longitude)
- arguments.append(coordinate.longitude)
- }
-
- let models = store.models(for: query, arguments: StatementArguments(arguments))
-
- // start a new fetch if needed
- if models.isEmpty || models.isStale {
- fetchTypesFor(coordinate: coordinate, depth: depth)
- }
-
- // if not D2, only return base types (all extended types are coordinate bound)
- if depth < 2 { return models.filter { ActivityTypeName.baseTypes.contains($0.name) } }
-
- return models
- }
-
- // MARK: - Remote model fetching
-
- func fetchTypesFor(coordinate: CLLocationCoordinate2D, depth: Int) {
- let latRange = ActivityType.latitudeRangeFor(depth: depth, coordinate: coordinate)
- let lngRange = ActivityType.longitudeRangeFor(depth: depth, coordinate: coordinate)
- let latWidth = latRange.max - latRange.min
- let lngWidth = lngRange.max - lngRange.min
- let depthCenter = CLLocationCoordinate2D(latitude: latRange.min + latWidth * 0.5,
- longitude: lngRange.min + lngWidth * 0.5)
-
- LocoKitService.fetchModelsFor(coordinate: depthCenter, depth: depth) { json in
- if let json = json { self.parseTypes(json: json) }
- }
- }
-
- func parseTypes(json: [String: Any]) {
- guard let store = store else { return }
- guard let typeDicts = json["activityTypes"] as? [[String: Any]] else { return }
-
- for dict in typeDicts {
- let model = ActivityType(dict: dict, in: store)
- model?.save()
- }
- }
-}
diff --git a/LocoKit/Timelines/ActivityTypes/ClassifierResultItem.swift b/LocoKit/Timelines/ActivityTypes/ClassifierResultItem.swift
deleted file mode 100644
index 8f8018ce..00000000
--- a/LocoKit/Timelines/ActivityTypes/ClassifierResultItem.swift
+++ /dev/null
@@ -1,68 +0,0 @@
-//
-// ClassifierResultItem.swift
-// LocoKitCore
-//
-// Created by Matt Greenfield on 13/10/17.
-// Copyright © 2017 Big Paua. All rights reserved.
-//
-
-import Foundation
-
-public enum ClassifierResultScoreGroup: Int {
- case perfect = 5
- case veryGood = 4
- case good = 3
- case bad = 2
- case veryBad = 1
- case terrible = 0
-}
-
-/**
- An individual result row in a `ClassifierResults` instance, for a single activity type.
- */
-public struct ClassifierResultItem: Equatable {
-
- /**
- The activity type name for the result.
- */
- public let name: ActivityTypeName
-
- /**
- The match probability score for the result, in the range of 0.0 to 1.0 (0% match to 100% match).
- */
- public let score: Double
-
- public let modelAccuracyScore: Double?
-
- public init(name: ActivityTypeName, score: Double, modelAccuracyScore: Double? = nil) {
- self.name = name
- self.score = score
- self.modelAccuracyScore = modelAccuracyScore
- }
-
- public func normalisedScore(in results: ClassifierResults) -> Double {
- let scoresTotal = results.scoresTotal
- guard scoresTotal > 0 else { return 0 }
- return score / scoresTotal
- }
-
- public func normalisedScoreGroup(in results: ClassifierResults) -> ClassifierResultScoreGroup {
- let normalisedScore = self.normalisedScore(in: results)
- switch Int(round(normalisedScore * 100)) {
- case 100: return .perfect
- case 80...100: return .veryGood
- case 50...80: return .good
- case 20...50: return .bad
- case 1...20: return .veryBad
- default: return .terrible
- }
- }
-
- /**
- Result items are considered equal if they have matching `name` values.
- */
- public static func ==(lhs: ClassifierResultItem, rhs: ClassifierResultItem) -> Bool {
- return lhs.name == rhs.name
- }
-
-}
diff --git a/LocoKit/Timelines/ActivityTypes/ClassifierResults.swift b/LocoKit/Timelines/ActivityTypes/ClassifierResults.swift
deleted file mode 100644
index 33ee03ed..00000000
--- a/LocoKit/Timelines/ActivityTypes/ClassifierResults.swift
+++ /dev/null
@@ -1,163 +0,0 @@
-//
-// ClassifierResults.swift
-// LocoKitCore
-//
-// Created by Matt Greenfield on 29/08/17.
-// Copyright © 2017 Big Paua. All rights reserved.
-//
-
-/**
- The results of a call to `classify(_:types:)` on an `ActivityTypeClassifier`.
-
- Classifier Results are an iterable sequence of `ClassifierResultItem` rows, with each row representing a single
- `ActivityTypeName` and its match probability score.
-
- The results are ordered from best match to worst match, thus the first result row represents the best match for the
- given sample.
-
- ## Using The Results
-
- The simplest way to use the results is to take the first result row (ie the best match) and ignore the rest.
-
- ```swift
- let results = classifier.classify(sample)
-
- let bestMatch = results.first
- ```
-
- You could also iterate through the results, in order from best match to worst match.
-
- ```swift
- for result in results {
- print("name: \(result.name) score: \(result.score)")
- }
- ```
-
- If you want to know the probability score of a specific type, you could extract that result row by `ActivityTypeName`:
-
- ```swift
- let walkingResult = results[.walking]
- ```
-
- If you want the first and second result rows:
-
- ```swift
- let firstResult = results[0]
- let secondResult = results[1]
- ```
-
- ## Interpreting Classifier Results
-
- Two key indicators can help to interpret the probability scores. The first being the most obvious: a higher score
- indicates a better match.
-
- The second, and perhaps more important indicator, is the ratio of the best match's score to the second best match's
- score.
-
- For example if the first result row has a probability score of 0.9 (a 90% match) while the second result row's score
- is 0.1 (a 10% match), that indicates that the best match is nine times more probable than the second best match
- (`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
- probable than the second (`0.9 / 0.8 = 1.125`).
-
- The ratio between the first and second best matches can be loosely considered a "confidence" score. Thus the
- `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
- a much lower confidence score of 1.125.
-
- A real world example might be results that have "car" and "bus" as the top two results. If both types achieve a high
- probability score, but the scores are close together, that indicates there is high confidence that the type is either
- car or bus, but low confidence of knowing which one of the two it is.
-
- The easiest way to apply these two metrics is with simple thresholds. For example a raw score threshold of 0.01
- and a first-to-second-match ratio threshold of 2.0. If the first match falls below these thresholds, you could consider
- it an "uncertain" match. Although which kinds of thresholds to use will depend heavily on the application.
- */
-public struct ClassifierResults: Sequence, IteratorProtocol {
-
- internal let results: [ClassifierResultItem]
-
- public init(results: [ClassifierResultItem], moreComing: Bool) {
- self.results = results.sorted { $0.score > $1.score }
- self.moreComing = moreComing
- }
-
- public init(confirmedType: ActivityTypeName) {
- var resultItems = [ClassifierResultItem(name: confirmedType, score: 1)]
- for activityType in ActivityTypeName.allTypes where activityType != confirmedType {
- resultItems.append(ClassifierResultItem(name: activityType, score: 0))
- }
- self.results = resultItems
- self.moreComing = false
- }
-
- private lazy var arrayIterator: IndexingIterator> = {
- return self.results.makeIterator()
- }()
-
- /**
- Indicates that the classifier does not yet have all relevant model data, so a subsequent attempt to classify the
- same sample again may produce new results with higher accuracy.
-
- - Note: Classifiers manage the fetching and caching of model data internally, so if the classifier returns results
- flagged with `moreComing` it will already have requested the missing model data from the server. Provided a
- working internet connection is available, the missing model data should be available in the classifier in less
- than a second.
- */
- public let moreComing: Bool
-
- /**
- Returns the result rows as a plain array.
- */
- public var array: [ClassifierResultItem] {
- return results
- }
-
- public var isEmpty: Bool {
- return count == 0
- }
-
- public var count: Int {
- return results.count
- }
-
- public var best: ClassifierResultItem {
- if let first = first, first.score > 0 { return first }
- return ClassifierResultItem(name: .unknown, score: 0)
- }
-
- public var first: ClassifierResultItem? {
- return self.results.first
- }
-
- public var scoresTotal: Double {
- return results.map { $0.score }.sum
- }
-
- // MARK: -
-
- public subscript(index: Int) -> ClassifierResultItem {
- return results[index]
- }
-
- /**
- A convenience subscript to enable lookup by `ActivityTypeName`.
-
- ```swift
- let walkingResult = results[.walking]
- ```
- */
- public subscript(activityType: ActivityTypeName) -> ClassifierResultItem? {
- return results.first { $0.name == activityType }
- }
-
- public mutating func next() -> ClassifierResultItem? {
- return arrayIterator.next()
- }
-}
-
-public func +(left: ClassifierResults, right: ClassifierResults) -> ClassifierResults {
- return ClassifierResults(results: left.array + right.array, moreComing: left.moreComing || right.moreComing)
-}
-
-public func -(left: ClassifierResults, right: ActivityTypeName) -> ClassifierResults {
- return ClassifierResults(results: left.array.filter { $0.name != right }, moreComing: left.moreComing)
-}
diff --git a/LocoKit/Timelines/ActivityTypes/CoordinatesMatrix.swift b/LocoKit/Timelines/ActivityTypes/CoordinatesMatrix.swift
deleted file mode 100644
index 57067529..00000000
--- a/LocoKit/Timelines/ActivityTypes/CoordinatesMatrix.swift
+++ /dev/null
@@ -1,220 +0,0 @@
-//
-// Matrix.swift
-// LearnerCoacher
-//
-// Created by Matt Greenfield on 7/05/17.
-// Copyright © 2017 Big Paua. All rights reserved.
-//
-
-import os.log
-import CoreLocation
-
-open class CoordinatesMatrix: CustomStringConvertible {
-
- public static let minimumProbability = 0.001
-
- public let bins: [[UInt16]] // [lat][long]
- public let lngBinWidth: Double
- public let latBinWidth: Double
- public let lngRange: (min: Double, max: Double)
- public let latRange: (min: Double, max: Double)
- public let pseudoCount: UInt16
-
- // used for loading from serialised strings
- public convenience init?(string: String) {
- let lines = string.split(separator: ";", omittingEmptySubsequences: false)
-
- guard lines.count > 3 else {
- return nil
- }
-
- let sizeLine = lines[0].split(separator: ",", omittingEmptySubsequences: false)
- guard let latBinCount = Int(sizeLine[0]), let lngBinCount = Int(sizeLine[1]), let pseudoCount = UInt16(sizeLine[2]) else {
- os_log("BIN COUNTS FAIL")
- return nil
- }
-
- let latRangeLine = lines[1].split(separator: ",", omittingEmptySubsequences: false)
- guard let latMin = Double(latRangeLine[0]), let latMax = Double(latRangeLine[1]) else {
- os_log("LAT RANGE FAIL")
- return nil
- }
-
- let lngRangeLine = lines[2].split(separator: ",", omittingEmptySubsequences: false)
- guard let lngMin = Double(lngRangeLine[0]), let lngMax = Double(lngRangeLine[1]) else {
- os_log("LNG RANGE FAIL")
- return nil
- }
-
- let latRange = (min: latMin, max: latMax)
- let lngRange = (min: lngMin, max: lngMax)
- let lngBinWidth = (lngRange.max - lngRange.min) / Double(lngBinCount)
- let latBinWidth = (latRange.max - latRange.min) / Double(latBinCount)
-
- var bins = Array(repeating: Array(repeating: pseudoCount, count: lngBinCount), count: latBinCount)
-
- let binLines = lines.suffix(from: 3)
- for binLine in binLines {
- let bits = binLine.split(separator: ",", omittingEmptySubsequences: false)
- guard bits.count == 3 else {
- continue
- }
-
- guard let latBin = Int(bits[0]), let lngBin = Int(bits[1]), var value = Int(bits[2]) else {
- os_log("CoordinatesMatrix bin fail: %@", bits)
- return nil
- }
-
- // fix overflows
- if value > Int(UInt16.max) {
- value = Int(UInt16.max)
- }
-
- bins[latBin][lngBin] = UInt16(value)
- }
-
- self.init(bins: bins, latBinWidth: latBinWidth, lngBinWidth: lngBinWidth, latRange: latRange,
- lngRange: lngRange, pseudoCount: pseudoCount)
- }
-
- // everything pre determined except which bins the coordinates go in. ActivityType uses this directly
- public convenience init(coordinates: [CLLocationCoordinate2D], latBinCount: Int, lngBinCount: Int,
- latRange: (min: Double, max: Double), lngRange: (min: Double, max: Double),
- pseudoCount: UInt16) {
- let latBinWidth = (latRange.max - latRange.min) / Double(latBinCount)
- let lngBinWidth = (lngRange.max - lngRange.min) / Double(lngBinCount)
-
- // pre fill the bins with pseudo count
- var bins = Array(repeating: Array(repeating: pseudoCount, count: lngBinCount), count: latBinCount)
-
- // proper fill the bins
- for coordinate in coordinates {
- let lngBin = Int((coordinate.longitude - lngRange.min) / lngBinWidth)
- let latBin = Int((coordinate.latitude - latRange.min) / latBinWidth)
-
- guard latBin >= 0 && latBin < latBinCount && lngBin >= 0 && lngBin < lngBinCount else {
- continue
- }
-
- let existingValue = bins[latBin][lngBin]
- if existingValue < UInt16.max {
- bins[latBin][lngBin] = existingValue + 1
- }
- }
-
- self.init(bins: bins, latBinWidth: latBinWidth, lngBinWidth: lngBinWidth, latRange: latRange,
- lngRange: lngRange, pseudoCount: pseudoCount)
- }
-
- public init(bins: [[UInt16]], latBinWidth: Double, lngBinWidth: Double, latRange: (min: Double, max: Double),
- lngRange: (min: Double, max: Double), pseudoCount: UInt16) {
- self.bins = bins
- self.lngRange = lngRange
- self.latRange = latRange
- self.lngBinWidth = lngBinWidth
- self.latBinWidth = latBinWidth
- self.pseudoCount = pseudoCount
- }
-
- lazy var matrixMax: UInt16 = {
- var matrixMax: UInt16 = 0
- for bin in bins {
- if let rowMax = bin.max() {
- matrixMax = max(rowMax, UInt16(matrixMax))
- }
- }
- return matrixMax
- }()
-
- // MARK: - Scores
-
- public func probabilityFor(_ coordinate: CLLocationCoordinate2D, maxThreshold: Int? = nil) -> Double {
- guard latBinWidth > 0 && lngBinWidth > 0 else { return 0 }
- guard matrixMax > 0 else { return 0 }
-
- var trimmedMatrixMax = matrixMax
-
- if var maxThreshold = maxThreshold {
- // fix overflows
- if maxThreshold > Int(UInt16.max) {
- maxThreshold = Int(UInt16.max)
- }
-
- trimmedMatrixMax.clamp(min: 0, max: UInt16(maxThreshold))
- }
-
- let latBin = Int((coordinate.latitude - latRange.min) / latBinWidth)
- let lngBin = Int((coordinate.longitude - lngRange.min) / lngBinWidth)
-
- guard latBin >= 0 && latBin < bins.count else {
- return (Double(pseudoCount) / Double(trimmedMatrixMax)).clamped(min: 0, max: 1)
- }
- guard lngBin >= 0 && lngBin < bins[0].count else {
- return (Double(pseudoCount) / Double(trimmedMatrixMax)).clamped(min: 0, max: 1)
- }
-
- let binCount = bins[latBin][lngBin]
-
- return (Double(binCount) / Double(trimmedMatrixMax)).clamped(min: 0, max: 1)
- }
-
- // MARK: - Serialisation
-
- // xCount,yCount,pseudoCount;
- // xMin,xMax;
- // yMin,yMax;
- // x,y,value; ...
-
- public var serialised: String {
- var result = "\(bins.count),\(bins[0].count),\(pseudoCount);"
- result += "\(latRange.min),\(latRange.max);"
- result += "\(lngRange.min),\(lngRange.max);"
-
- for (x, bin) in bins.enumerated() {
- for (y, value) in bin.enumerated() {
- if value > pseudoCount {
- result += "\(x),\(y),\(value);"
- }
- }
- }
-
- return result
- }
-
- // MARK: - CustomStringConvertible
-
- public var description: String {
- var result = ""
-
- result += "lngRange: \(lngRange)\n"
- result += "latRange: \(latRange)\n"
-
- var matrixMax: UInt16 = 0
- for lngBins in bins {
- if let maxBin = lngBins.max(), maxBin > matrixMax {
- matrixMax = maxBin
- }
- }
-
- // TODO: this doesn't take into account the maxThreshold (eg 10 events per D2 bin)
- for lngBins in bins.reversed() {
- var yString = ""
- for value in lngBins {
- let pctOfMax = Double(value) / Double(matrixMax)
- if value <= pseudoCount {
- yString += "-"
- } else if pctOfMax >= 1 {
- yString += "X"
- } else {
- yString += String(format: "%1.0f", pctOfMax * 10)
- }
- }
-
- result += yString + "\n"
-
- }
-
- return result
- }
-
-}
diff --git a/LocoKit/Timelines/ActivityTypes/Histogram.swift b/LocoKit/Timelines/ActivityTypes/Histogram.swift
deleted file mode 100644
index 68369a9d..00000000
--- a/LocoKit/Timelines/ActivityTypes/Histogram.swift
+++ /dev/null
@@ -1,375 +0,0 @@
-//
-// Histogram.swift
-// LearnerCoacher
-//
-// Created by Matt Greenfield on 1/05/17.
-// Copyright © 2017 Big Paua. All rights reserved.
-//
-
-import os.log
-
-open class Histogram: CustomStringConvertible {
-
- public let bins: [Int]
- public let binWidth: Double
- public let range: (min: Double, max: Double)
- public let pseudoCount: Int
-
- public static let defaultPseudoCount = 1
-
- public var name: String?
- public var binName: String?
- public var binValueName: String?
- public var binValueNamePlural: String?
- public var printFormat: String?
- public var printModifier: Double?
-
- public var binCount: Int { return bins.count }
-
- public convenience init(values: [Double], maxBins: Int? = nil, minBoundary: Double? = nil, maxBoundary: Double? = nil,
- pseudoCount: Int = Histogram.defaultPseudoCount, trimOutliers: Bool = false,
- snapToBoundaries: Bool = false, name: String? = nil,
- printFormat: String? = nil, printModifier: Double? = nil) {
-
- let mean = values.mean
- let sd = values.standardDeviation
-
- var filteredValues = values
-
- if trimOutliers {
- let trimRange = (min: mean - (sd * 4), max: mean + (sd * 4))
- filteredValues = filteredValues.filter { $0 >= trimRange.min && $0 <= trimRange.max }
- }
-
- if let minBoundary = minBoundary {
- filteredValues = filteredValues.filter { $0 >= minBoundary }
- }
-
- if let maxBoundary = maxBoundary {
- filteredValues = filteredValues.filter { $0 <= maxBoundary }
- }
-
- guard let minValue = filteredValues.min(), let maxValue = filteredValues.max() else {
- self.init(bins: [pseudoCount], range: (min: 0, max: 0), pseudoCount: pseudoCount)
- return
- }
-
- var range = (min: minValue, max: maxValue)
-
- // snap range to boundaries if min/max values are close enough
- if snapToBoundaries, let minBoundary = minBoundary, let maxBoundary = maxBoundary {
- let boundarySpread = maxBoundary - minBoundary
- if minValue - minBoundary < boundarySpread * 0.02 { range.min = minBoundary }
- if maxBoundary - maxValue < boundarySpread * 0.02 { range.max = maxBoundary }
- }
-
- guard range.min < range.max else {
- self.init(bins: [pseudoCount + 1], range: range, pseudoCount: pseudoCount)
- return
- }
-
- var binCount = Histogram.numberOfBins(filteredValues)
- if let maxBins = maxBins, binCount > maxBins {
- binCount = maxBins
- }
- let binWidth = (range.max - range.min) / Double(binCount)
-
- var bins = [Int](repeating: pseudoCount, count: binCount)
- for value in filteredValues {
- let bucketDouble = (value - range.min) / binWidth
- var bucket = Int(bucketDouble)
-
- // cope with values just over top of range (ie float inaccuracies)
- if bucket == binCount {
- let overage = bucketDouble - Double(binCount)
- if overage < 0.001 {
- bucket = binCount - 1
- }
- }
-
- guard bucket >= 0 && bucket < binCount else {
- continue
- }
-
- bins[bucket] += 1
- }
-
- self.init(bins: bins, range: range, pseudoCount: pseudoCount)
-
- self.name = name
- self.printFormat = printFormat
- self.printModifier = printModifier
- }
-
- // used for loading from serialised strings
- public convenience init?(string: String) {
- let lines = string.split(separator: ";", omittingEmptySubsequences: false)
-
- guard lines.count > 2 else {
- return nil
- }
-
- let sizeLine = lines[0].split(separator: ",", omittingEmptySubsequences: false)
- guard let binCount = Int(sizeLine[0]), let pseudoCount = Int(sizeLine[1]) else {
- os_log("BIN COUNTS FAIL")
- return nil
- }
-
- let rangeLine = lines[1].split(separator: ",", omittingEmptySubsequences: false)
- guard let rangeMin = Double(rangeLine[0]), let rangeMax = Double(rangeLine[1]) else {
- os_log("RANGE FAIL")
- return nil
- }
-
- let range = (min: rangeMin, max: rangeMax)
-
- var bins = [Int](repeating: pseudoCount, count: binCount)
-
- let binLines = lines.suffix(from: 2)
- for binLine in binLines {
- let bits = binLine.split(separator: ",", omittingEmptySubsequences: false)
- guard bits.count == 2 else {
- continue
- }
-
- guard let bin = Int(bits[0]), let value = Int(bits[1]) else {
- os_log("Histogram bin fail: %@", bits)
- return nil
- }
-
- bins[bin] = value
- }
-
- self.init(bins: bins, range: range, pseudoCount: pseudoCount)
- }
-
- public init(bins: [Int], range: (min: Double, max: Double), pseudoCount: Int) {
- self.bins = bins
- self.range = range
- self.binWidth = (range.max - range.min) / Double(bins.count)
- self.pseudoCount = pseudoCount
- }
-
- public func binFor(_ value: Double, numberOfBins: Int, range: (min: Double, max: Double)) -> Int? {
- let binWidth = (range.max - range.min) / Double(numberOfBins)
- let maxBucket = Double(numberOfBins - 1)
-
- let bucket = (value - range.min) / binWidth
-
- // cope with out of range values
- if floor(bucket) > maxBucket {
- if bucket - Double(numberOfBins) < 0.001 { // return maxBucket for values ~equal to max
- return Int(maxBucket)
- } else {
- os_log("value: %f binWidth: %f maxBucket: %f bucket: %f range: %f - %f",
- value, binWidth, maxBucket, bucket, range.min, range.max)
- return nil
- }
- }
-
- return Int(bucket)
- }
-
- public var isEmpty: Bool {
- return bins.count == 1 && bins[0] == 0
- }
-
- public func probabilityFor(_ value: Double) -> Double {
- guard let max = bins.max() else {
- return 0
- }
-
- // shouldn't be possible. but...
- guard !binWidth.isNaN else {
- return 0
- }
-
- // single bin histograms result in binary 0 or 1 scores
- if bins.count == 1 {
- return value == range.min ? 1 : 0
- }
-
- let bin: Int
- if value == range.max {
- bin = bins.count - 1
- } else {
- let binDouble = floor((value - range.min) / binWidth)
-
- if binDouble > Double(bins.count - 1) {
- return 0
- }
-
- guard !binDouble.isNaN && binDouble > Double(Int.min) && binDouble < Double(Int.max) else {
- return 0
- }
-
- bin = binWidth > 0 ? Int(binDouble) : 0
- }
-
- guard bin >= 0 && bin < bins.count else {
- return 0
- }
-
- return (Double(bins[bin]) / Double(max)).clamped(min: 0, max: 1)
- }
-
- public func percentOfTotalFor(bin: Int) -> Double? {
- guard bin < bins.count else {
- return nil
- }
-
- let sum = bins.reduce(0, +)
-
- guard sum > 0 else {
- return nil
- }
-
- return Double(bins[bin]) / Double(sum)
- }
-
- public func bottomFor(bin: Int) -> Double {
- return range.min + (binWidth * Double(bin))
- }
-
- public func middleFor(bin: Int) -> Double {
- let valueBottom = bottomFor(bin: bin)
- return valueBottom + (binWidth * 0.5)
- }
-
- public func topFor(bin: Int) -> Double {
- let valueBottom = bottomFor(bin: bin)
- return valueBottom + binWidth
- }
-
- public func formattedStringFor(bin: Int) -> String {
- let format = printFormat ?? "%.2f"
- let modifier = printModifier ?? 1.0
-
- return String(format: format, middleFor(bin: bin) * modifier)
- }
-
- public var peakIndexes: [Int]? {
- guard let maxBin = bins.max(), maxBin > 0 else {
- return nil
- }
-
- // find all the max bins
- var peakIndexes: [Int] = []
- for (i, binValue) in bins.enumerated() {
- if binValue == maxBin {
- peakIndexes.append(i)
- }
- }
-
- return peakIndexes
- }
-
- // the first (and hopefully the only) peak index
- public var peakIndex: Int? {
- guard let peakIndexes = peakIndexes, peakIndexes.count == 1 else {
- return nil
- }
-
- return peakIndexes.first
- }
-
- public var peakRanges: [(from: Double, to: Double)]? {
- guard let peakIndexes = peakIndexes else {
- return nil
- }
-
- var previousBucket: Int?
- var currentRange: (from: Double, to: Double)?
- var ranges: [(from: Double, to: Double)] = []
-
- for bucket in peakIndexes {
-
- // add previous range if non sequential
- if let previous = previousBucket, bucket != previous + 1, let range = currentRange {
- ranges.append(range)
- currentRange = nil
- }
-
- let bottom = range.min + (binWidth * Double(bucket))
- let top = bottom + binWidth
-
- if currentRange == nil {
- currentRange = (from: bottom, to: top)
-
- } else {
- currentRange!.to = top
- }
-
- previousBucket = bucket
- }
-
- // add the last one
- if let range = currentRange {
- ranges.append(range)
- }
-
- return ranges
- }
-
- public static func numberOfBins(_ metric: [Double], defaultBins: Int = 10) -> Int {
- let h = binWidth(metric)
- guard let ulim = metric.max(), let llim = metric.min() else { return 1 }
- if h <= (ulim - llim) / Double(metric.count) {
- return defaultBins
- }
- return Int(ceil((ulim - llim) / h))
- }
-
- static func binWidth(_ metric: [Double]) -> Double {
- return 2.0 * iqr(metric) * pow(Double(metric.count), -1.0 / 3.0)
- }
-
- static func iqr(_ metric: [Double]) -> Double {
- let sorted = metric.sorted { $0 < $1 }
- let q1 = sorted[Int(floor(Double(sorted.count) / 4.0))]
- let q3 = sorted[Int(floor(Double(sorted.count) * 3.0 / 4.0))]
- return q3 - q1
- }
-
- // binsCount,pseudoCount;
- // range.min,range.max;
- // binIndex,value; ...
-
- public var serialised: String {
- var result = "\(bins.count),\(pseudoCount);"
- result += "\(range.min),\(range.max);"
-
- for (binIndex, value) in bins.enumerated() {
- if value > pseudoCount {
- result += "\(binIndex),\(value);"
- }
- }
-
- return result
- }
-
- // MARK: - CustomStringConvertible
-
- public var description: String {
- guard let max = bins.max(), max > 0 else {
- return "\(name ?? "UNNAMED"): Nada."
- }
-
- var result = "\(name ?? "UNNAMED") (pseudoCount: \(pseudoCount))\n"
-
- for bin in 0 ..< binCount {
- let binText = formattedStringFor(bin: bin)
-
- let lengthPct = Double(bins[bin]) / Double(max)
- let barWidth = Int(130.0 * lengthPct)
-
- let bar = "".padding(toLength: barWidth, withPad: "+", startingAt: 0)
- let bucketString = binText + ": " + bar
-
- result += bucketString + "\n"
- }
-
- return result
- }
-
-}
diff --git a/LocoKit/Timelines/ActivityTypes/MLClassifier.swift b/LocoKit/Timelines/ActivityTypes/MLClassifier.swift
deleted file mode 100644
index d0d835a2..00000000
--- a/LocoKit/Timelines/ActivityTypes/MLClassifier.swift
+++ /dev/null
@@ -1,244 +0,0 @@
-//
-// MLClassifier.swift
-// LocoKitCore
-//
-// Created by Matt Greenfield on 20/12/17.
-// Copyright © 2017 Big Paua. All rights reserved.
-//
-
-import CoreLocation
-
-public protocol MLClassifier {
-
- associatedtype Cache: MLModelSource
-
- var depth: Int { get }
- var parent: Cache.ParentClassifier? { get }
- var supportedTypes: [ActivityTypeName] { get }
- var models: [Cache.Model] { get }
-
- var availableTypes: [ActivityTypeName] { get }
- var centerCoordinate: CLLocationCoordinate2D? { get }
-
- // MARK: Creating a Classifier
-
- /**
- Use this init method to create a new classifier.
-
- The classifier will be created from locally cached model data. If no appropriate model data is found in cache,
- a fetch request will be made to the server, and the init will immediately return nil. Assuming an internet
- connection is available, a second attempt to create the classifier, a second later, should return a valid
- classifier.
-
- - Note: Classifiers should be retained and reused. Classifier creation requires potentially expensive cache
- lookups and remote model data fetches. As such, creating new classifiers should only be done on an as needed
- basis, and existing classifiers should be reused while still valid.
- */
- init?(requestedTypes: [ActivityTypeName], coordinate: CLLocationCoordinate2D)
-
- /**
- Classify a `LocomotionSample` to determine its most likely `ActivityTypeName`.
-
- - Note: This method is the main purpose of classifiers, and is optimised with the expectation that it will called
- repeatedly during an app session. Although there is unavoidably some energy cost to classifying samples,
- so the frequency of classifications should be considered carefully, especially in long running apps.
-
- For example, the LocoKit Demo App calls a classifier every time a new location is received, thus up to about
- once every second. On the other hand, [Arc App](https://itunes.apple.com/us/app/arc-app-location-activity-tracker/id1063151918?mt=8)
- classifies samples only once every six seconds at most, due to the expectation that the app will be recording
- and classifying potentially hours of data each day.
- */
- func classify(_ classifiable: ActivityTypeClassifiable, previousResults: ClassifierResults?) -> ClassifierResults
-
- /**
- Determine whether the given coordinate is inside the classifier's geographical region.
-
- This method works well in combination with `isStale` as a test for whether a classifier should be used to classify
- a sample, or whether a fresh classifier should instead be requested.
-
- - Note: Ideally classifiers should only be used to classify samples that fall inside the classifier's region.
- However if relevant model data is in the local cache and no internet connection is available, thus a fresh
- classifier cannot be fetched, a stale and/or geographically inappropriate classifier may continue to be used,
- albeit with potentially reduced accuracy.
-
- Extended transport activity types (car, train, etc) will have especially inaccurate results when classified
- by a geographically inappropriate classifier. However base types (walking, running, etc) should receive
- adequately accurate results in any classifier, regardless of geographical appropriateness.
- */
- func contains(coordinate: CLLocationCoordinate2D) -> Bool
-
- // MARK: Classifier Validity
-
- /**
- Whether the classifier's model data is old enough to justify requesting a new classifier with fresh model data.
-
- This bool works well in combination with `contains(coordinate:)` as a test for whether a classifier should be
- used to classify a sample, or whether a fresh classifier should instead be used.
- */
- var isStale: Bool { get }
-
- var lastUpdated: Date? { get }
-
- // MARK: Data Coverage and Accuracy
-
- /**
- Coverage Score is the result of `completenessScore x accuracyScore`, in the range of 0.0 to 1.0.
-
- This value is best used to get a general sense of the expected quality and usability of the classifier's results.
-
- In practice, any score above 0.15 indicates a usable classifier in terms of local model data, however you should
- experiment with a range of thresholds to determine a best fit minimum for your app's accuracy requirements.
- */
- var coverageScore: Double { get }
-
- /**
- Accuracy Score is the expected minimum accuracy of the clasifier's results, in the range of 0.0 to 1.0.
-
- This value should not be used directly. Instead you should use `coverageStore` to determine the usability of a
- classifier.
-
- - Note: This number represents the worst case accuracy. The achieved accuracy will typically be considerably
- higher than this value. A classifier with an Accuracy Score above 0.75 will appear to give essentially
- perfect results in most cases.
- */
- var accuracyScore: Double? { get }
-
- /**
- Completeness Score is an internal, machine learning specific measure of the number of training samples used to
- compose the model versus a threshold sample count.
-
- This value should not be used directly. Instead you should use `coverageStore` to determine the usability of a
- classifier.
- */
- var completenessScore: Double { get }
-
- var coverageScoreString: String { get }
-}
-
-extension MLClassifier {
-
- public func classify(_ classifiable: ActivityTypeClassifiable, previousResults: ClassifierResults?) -> ClassifierResults {
- var totalSamples = 1 // start with 1 to avoid potential div by zero
- for model in models {
- totalSamples += model.totalSamples
- }
-
- var scores: [ClassifierResultItem] = []
- for model in models {
- let typeScore = model.scoreFor(classifiable: classifiable, previousResults: previousResults)
- let pctOfAllEvents = Double(model.totalSamples) / Double(totalSamples)
- let finalScore = typeScore * pctOfAllEvents
-
- let result = ClassifierResultItem(name: model.name, score: finalScore,
- modelAccuracyScore: model.accuracyScore)
- scores.append(result)
- }
-
- var contained = false
- if let coordinate = classifiable.location?.coordinate, contains(coordinate: coordinate) {
- contained = true
- }
-
- // classifier is complete, and contains the coord?
- if contained && completenessScore >= 1 {
- return ClassifierResults(results: scores, moreComing: false)
- }
-
- // no parent? we'll have to settle for what we've got
- guard let parent = parent else {
- return ClassifierResults(results: scores, moreComing: depth > 1)
- }
-
- let parentResults = parent.classify(classifiable, previousResults: previousResults)
-
- // if classifier doesn't contain the coord, it should defer all weight to parent
- let selfWeight = contained ? completenessScore : 0
- let parentWeight = 1.0 - selfWeight
-
- var selfScoresDict: [ActivityTypeName: ClassifierResultItem] = [:]
- for result in scores {
- selfScoresDict[result.name] = result
- }
-
- var finalScores: [ClassifierResultItem] = []
-
- for typeName in supportedTypes {
-
- // combine self result and parent result
- if let selfResult = selfScoresDict[typeName], let parentResult = parentResults[typeName] {
- let score = (parentResult.score * parentWeight) + (selfResult.score * selfWeight)
- let finalResult = ClassifierResultItem(name: selfResult.name, score: score,
- modelAccuracyScore: selfResult.modelAccuracyScore)
- finalScores.append(finalResult)
- continue
- }
-
- // only have self result
- if let selfResult = selfScoresDict[typeName] {
- let score = (selfResult.score * selfWeight)
- let finalResult = ClassifierResultItem(name: selfResult.name, score: score,
- modelAccuracyScore: selfResult.modelAccuracyScore)
- finalScores.append(finalResult)
- continue
- }
-
- // only have parent result
- if let parentResult = parentResults[typeName] {
- let score = (parentResult.score * parentWeight)
- let finalResult = ClassifierResultItem(name: parentResult.name, score: score, modelAccuracyScore: nil)
- finalScores.append(finalResult)
- continue
- }
- }
-
- return ClassifierResults(results: finalScores, moreComing: parentResults.moreComing)
- }
-
- public func contains(coordinate: CLLocationCoordinate2D) -> Bool {
- if depth == 0 { return true }
- guard let firstType = models.first else { return false }
- return firstType.contains(coordinate: coordinate)
- }
-
- public var availableTypes: [ActivityTypeName] {
- return models.sorted { $0.totalSamples > $1.totalSamples }.map { $0.name }
- }
-
- public var centerCoordinate: CLLocationCoordinate2D? {
- return models.first?.centerCoordinate
- }
-
- public var isStale: Bool {
- return models.isStale
- }
-
- public var coverageScore: Double {
- guard let accuracyScore = self.accuracyScore else {
- return self.completenessScore
- }
- return self.completenessScore * accuracyScore
- }
-
- public var coverageScoreString: String {
- let score = coverageScore
-
- let intScore = Int(score * 10).clamped(min: 0, max: 10)
-
- var words: String
- switch intScore {
- case 8...10:
- words = "Excellent"
- case 5...7:
- words = "Very Good"
- case 3...4:
- words = "Good"
- case 1...2:
- words = "Low"
- default:
- words = "Very Low"
- }
-
- return String(format: "%@ (%.0f%%)", words, score * 100)
- }
-}
-
diff --git a/LocoKit/Timelines/ActivityTypes/MLClassifierManager.swift b/LocoKit/Timelines/ActivityTypes/MLClassifierManager.swift
deleted file mode 100644
index 22ea1ba7..00000000
--- a/LocoKit/Timelines/ActivityTypes/MLClassifierManager.swift
+++ /dev/null
@@ -1,182 +0,0 @@
-//
-// MLClassifierManager.swift
-// Pods
-//
-// Created by Matt Greenfield on 3/04/18.
-//
-
-import os.log
-import Upsurge
-import CoreLocation
-
-#if canImport(Reachability)
-import Reachability
-#endif
-
-public protocol MLClassifierManager: MLCompositeClassifier {
-
- associatedtype Classifier: MLClassifier
-
- var sampleClassifier: Classifier? { get set }
-
- #if canImport(Reachability)
- var reachability: Reachability { get }
- #endif
-
- var mutex: PThreadMutex { get }
-
-}
-
-extension MLClassifierManager {
-
- public func canClassify(_ coordinate: CLLocationCoordinate2D? = nil) -> Bool {
- if let coordinate = coordinate { mutex.sync { updateTheSampleClassifier(for: coordinate) } }
- return mutex.sync { sampleClassifier } != nil
- }
-
- public func classify(_ classifiable: ActivityTypeClassifiable, previousResults: ClassifierResults? = nil) -> ClassifierResults? {
- return mutex.sync {
- // make sure we're capable of returning sensible results
- guard canClassify(classifiable.location?.coordinate) else { return nil }
-
- // get the sample classifier
- guard let classifier = mutex.sync(execute: { return sampleClassifier }) else { return nil }
-
- // get the results
- return classifier.classify(classifiable, previousResults: previousResults)
- }
- }
-
- public func classify(_ timelineItem: TimelineItem, timeout: TimeInterval? = nil) -> ClassifierResults? {
- guard let results = classify(timelineItem.samples, timeout: timeout) else { return nil }
-
- // radius is small enough to consider stationary a valid result
- if timelineItem.radius3sd < Visit.maximumRadius { return results }
-
- guard let stationary = results[.stationary] else { return results }
-
- // radius is too big for stationary. so let's zero out its score
- var resultsArray = results.array
- resultsArray.remove(stationary)
- resultsArray.append(ClassifierResultItem(name: .stationary, score: 0,
- modelAccuracyScore: stationary.modelAccuracyScore))
-
- return ClassifierResults(results: resultsArray, moreComing: results.moreComing)
- }
-
- public func classify(_ segment: ItemSegment, timeout: TimeInterval? = nil) -> ClassifierResults? {
- guard let results = classify(segment.samples, timeout: timeout) else { return nil }
-
- // radius is small enough to consider stationary a valid result
- if segment.radius.with3sd < Visit.maximumRadius {
- return results
- }
-
- guard let stationary = results[.stationary] else {
- return results
- }
-
- // radius is too big for stationary. so let's zero out its score
- var resultsArray = results.array
- resultsArray.remove(stationary)
- resultsArray.append(ClassifierResultItem(name: .stationary, score: 0,
- modelAccuracyScore: stationary.modelAccuracyScore))
-
- return ClassifierResults(results: resultsArray, moreComing: results.moreComing)
- }
-
- // Note: samples must be provided in date ascending order
- public func classify(_ samples: [ActivityTypeClassifiable], timeout: TimeInterval? = nil) -> ClassifierResults? {
- if samples.isEmpty { return nil }
-
- let start = Date()
-
- var allScores: [ActivityTypeName: ValueArray] = [:]
- var allAccuracies: [ActivityTypeName: ValueArray] = [:]
- for typeName in ActivityTypeName.allTypes {
- allScores[typeName] = ValueArray(capacity: samples.count)
- allAccuracies[typeName] = ValueArray(capacity: samples.count)
- }
-
- var moreComing = false
- var lastResults: ClassifierResults?
-
- for sample in samples {
- if let timeout = timeout, start.age >= timeout {
- os_log("Classifer reached timeout limit", type: .debug)
- moreComing = true
- break
- }
-
- var tmpResults = sample.classifierResults
-
- // nil or incomplete existing results? get fresh results
- if tmpResults == nil || tmpResults?.moreComing == true {
- sample.classifierResults = classify(sample, previousResults: lastResults)
- tmpResults = sample.classifierResults ?? tmpResults
- }
-
- guard let results = tmpResults else { continue }
-
- if results.moreComing { moreComing = true }
-
- for typeName in ActivityTypeName.allTypes {
- // if sample has confirmedType, give it a 1.0 score
- if let sample = sample as? ActivityTypeTrainable, typeName == sample.confirmedType {
- allScores[typeName]!.append(1.0)
- allAccuracies[typeName]!.append(1.0)
-
- } else if let resultRow = results[typeName] {
- allScores[resultRow.name]!.append(resultRow.score)
- allAccuracies[resultRow.name]!.append(resultRow.modelAccuracyScore ?? 0)
-
- } else {
- allScores[typeName]!.append(0)
- allAccuracies[typeName]!.append(0)
- }
- }
-
- lastResults = results
- }
-
- var finalResults: [ClassifierResultItem] = []
-
- for typeName in ActivityTypeName.allTypes {
- var finalScore = 0.0
- if let scores = allScores[typeName], !scores.isEmpty {
- finalScore = mean(scores)
- }
-
- var finalAccuracy: Double?
- if let accuracies = allAccuracies[typeName], !accuracies.isEmpty {
- finalAccuracy = mean(accuracies)
- }
-
- finalResults.append(ClassifierResultItem(name: typeName, score: finalScore,
- modelAccuracyScore: finalAccuracy))
- }
-
- return ClassifierResults(results: finalResults, moreComing: moreComing)
- }
-
- // MARK: Region specific classifier management
-
- private func updateTheSampleClassifier(for coordinate: CLLocationCoordinate2D) {
-
- // have a classifier already, and it's still valid?
- if let classifier = sampleClassifier, classifier.contains(coordinate: coordinate), !classifier.isStale {
- return
- }
-
- #if canImport(Reachability)
- // don't try to fetch classifiers without a network connection
- guard reachability.connection != .none else { return }
- #endif
-
- // attempt to get an updated classifier
- if let replacement = Classifier(requestedTypes: ActivityTypeName.allTypes, coordinate: coordinate) {
- sampleClassifier = replacement
- }
- }
-
-}
diff --git a/LocoKit/Timelines/ActivityTypes/MLCompositeClassifier.swift b/LocoKit/Timelines/ActivityTypes/MLCompositeClassifier.swift
deleted file mode 100644
index d38c8eb2..00000000
--- a/LocoKit/Timelines/ActivityTypes/MLCompositeClassifier.swift
+++ /dev/null
@@ -1,18 +0,0 @@
-//
-// MLCompositeClassifier.swift
-// Pods
-//
-// Created by Matt Greenfield on 10/04/18.
-//
-
-import CoreLocation
-
-public protocol MLCompositeClassifier: class {
-
- func canClassify(_ coordinate: CLLocationCoordinate2D?) -> Bool
- func classify(_ classifiable: ActivityTypeClassifiable, previousResults: ClassifierResults?) -> ClassifierResults?
- func classify(_ samples: [ActivityTypeClassifiable], timeout: TimeInterval?) -> ClassifierResults?
- func classify(_ timelineItem: TimelineItem, timeout: TimeInterval?) -> ClassifierResults?
- func classify(_ segment: ItemSegment, timeout: TimeInterval?) -> ClassifierResults?
-
-}
diff --git a/LocoKit/Timelines/ActivityTypes/MLModel.swift b/LocoKit/Timelines/ActivityTypes/MLModel.swift
deleted file mode 100644
index bebac900..00000000
--- a/LocoKit/Timelines/ActivityTypes/MLModel.swift
+++ /dev/null
@@ -1,24 +0,0 @@
-//
-// MLModel.swift
-// LocoKitCore
-//
-// Created by Matt Greenfield on 12/08/17.
-// Copyright © 2017 Big Paua. All rights reserved.
-//
-
-import CoreLocation
-
-public protocol MLModel: Hashable {
- var name: ActivityTypeName { get }
- var depth: Int { get }
- var totalSamples: Int { get }
- var lastFetched: Date { get }
- var lastUpdated: Date? { get }
- var coverageScore: Double { get }
- var accuracyScore: Double? { get }
- var completenessScore: Double { get }
- var centerCoordinate: CLLocationCoordinate2D { get }
-
- func contains(coordinate: CLLocationCoordinate2D) -> Bool
- func scoreFor(classifiable scorable: ActivityTypeClassifiable, previousResults: ClassifierResults?) -> Double
-}
diff --git a/LocoKit/Timelines/ActivityTypes/MLModelSource.swift b/LocoKit/Timelines/ActivityTypes/MLModelSource.swift
deleted file mode 100644
index 2a08e9fe..00000000
--- a/LocoKit/Timelines/ActivityTypes/MLModelSource.swift
+++ /dev/null
@@ -1,92 +0,0 @@
-//
-// MLModelCache.swift
-// LocoKitCore
-//
-// Created by Matt Greenfield on 11/08/17.
-// Copyright © 2017 Big Paua. All rights reserved.
-//
-
-import CoreLocation
-
-public protocol MLModelSource {
- associatedtype Model: MLModel
- associatedtype ParentClassifier: MLClassifier
-
- static var highlander: Self { get }
- var providesDepths: [Int] { get }
- func modelFor(name: ActivityTypeName, coordinate: CLLocationCoordinate2D, depth: Int) -> Model?
- func modelsFor(names: [ActivityTypeName], coordinate: CLLocationCoordinate2D, depth: Int) -> [Model]
-}
-
-public extension Array where Element: MLModel {
-
- var completenessScore: Double {
- if isEmpty {
- return 0
- }
- var total = 0.0
- var modelCount = 0
- for model in self where model.name != .bogus {
- total += model.completenessScore
- modelCount += 1
- }
- return modelCount > 0 ? total / Double(modelCount) : 0
- }
-
- var accuracyScore: Double? {
- var totalScore = 0.0, totalWeight = 0.0
- for model in self {
- if let score = model.accuracyScore, score >= 0 {
- totalScore += score * Double(model.totalSamples)
- totalWeight += Double(model.totalSamples)
- }
- }
- return totalWeight > 0 ? totalScore / totalWeight : nil
- }
-
- var lastUpdated: Date? {
- var mostRecentUpdate: Date?
- for model in self {
- if let lastUpdated = model.lastUpdated, mostRecentUpdate == nil || lastUpdated > mostRecentUpdate! {
- mostRecentUpdate = lastUpdated
- }
- }
- return mostRecentUpdate
- }
-
- var lastFetched: Date {
- var mostRecentFetch = Date.distantPast
- for model in self {
- if model.lastFetched > mostRecentFetch {
- mostRecentFetch = model.lastFetched
- }
- }
- return mostRecentFetch
- }
-
- var missingBaseTypes: [ActivityTypeName] {
- let haveTypes = self.map { $0.name }
- return ActivityTypeName.baseTypes.filter { !haveTypes.contains($0) }
- }
-
- var isStale: Bool {
- if isEmpty { return true }
-
- // missing a base model?
- guard missingBaseTypes.isEmpty else { return true }
-
- // nil lastUpdated is presumably UD models pending first update
- guard let lastUpdated = lastUpdated else { return false }
-
- // last fetch was too recent?
- if lastFetched.age < ActivityTypesCache.minimumRefetchWait { return false }
-
- // last updated recently enough?
- if lastUpdated.age < ActivityTypesCache.staleLastUpdatedAge * completenessScore { return false }
-
- // last fetched recently enough?
- if lastFetched.age < ActivityTypesCache.staleLastFetchedAge * completenessScore { return false }
-
- return true
- }
-}
diff --git a/LocoKit/Timelines/ActivityTypes/MutableActivityType.swift b/LocoKit/Timelines/ActivityTypes/MutableActivityType.swift
deleted file mode 100644
index cd9b0fcc..00000000
--- a/LocoKit/Timelines/ActivityTypes/MutableActivityType.swift
+++ /dev/null
@@ -1,263 +0,0 @@
-//
-// MutableActivityType.swift
-// LocoKitCore
-//
-// Created by Matt Greenfield on 14/12/16.
-// Copyright © 2016 Big Paua. All rights reserved.
-//
-
-import os.log
-import CoreLocation
-import GRDB
-
-public struct LocomotionMagicValue {
- @available(*, deprecated)
- public static let nilCourse: Double = -360
- public static let nilAltitude: CLLocationDistance = -1000
-}
-
-open class MutableActivityType: ActivityType {
-
- static let statsDebug = false
-
- public var needsUpdate = false
-
- public func updateFrom(samples: S) where S.Iterator.Element: ActivityTypeTrainable {
- if isShared { return }
-
- var totalSamples = 0, totalMoving = 0, accuracyScorables = 0, correctScorables = 0
-
- var allAltitudes: [Double] = [], allSpeeds: [Double] = [], allStepHz: [Double] = []
- var allCourses: [Double] = [], allCourseVariances: [Double] = [], allTimesOfDay: [Double] = []
- var allXYAccelerations: [Double] = [], allZAccelerations: [Double] = []
- var allCoordinates: [CLLocationCoordinate2D] = []
- var allCoreMotionTypes: [CoreMotionActivityTypeName] = []
- var allAccuracies: [CLLocationAccuracy] = []
- // bootstrap with one of each, for the pseudo count
- var allPreviousTypes: [ActivityTypeName] = ActivityTypeName.allTypes
-
- for sample in samples {
-
- // only accept confirmed samples that match the model
- guard let confirmedType = sample.confirmedType, confirmedType == self.name else {
- continue
- }
-
- // only accept samples that have a coordinate inside the model
- guard let location = sample.location, location.coordinate.isUsable else { continue }
- guard self.contains(coordinate: location.coordinate) else { continue }
-
- totalSamples += 1
-
- // collect accuracy counts
- if let classifiedType = sample.classifiedType {
- accuracyScorables += 1
- if classifiedType == confirmedType {
- correctScorables += 1
- }
- }
-
- if sample.movingState == .moving {
- totalMoving += 1
- }
-
- // ignore zero stepHz for walking, because it's a far too common gap in the raw data
- if let stepHz = sample.stepHz, (self.name != .walking || stepHz > 0) {
- allStepHz.append(stepHz)
- }
-
- if let courseVariance = sample.courseVariance {
- allCourseVariances.append(courseVariance)
- }
-
- allTimesOfDay.append(sample.timeOfDay)
-
- if let xyAcceleration = sample.xyAcceleration {
- allXYAccelerations.append(xyAcceleration)
- }
- if let zAcceleration = sample.zAcceleration {
- allZAccelerations.append(zAcceleration)
- }
-
- if let coreMotionType = sample.coreMotionActivityType {
- allCoreMotionTypes.append(coreMotionType)
- }
-
- allCoordinates.append(location.coordinate)
-
- if !location.altitude.isNaN && location.verticalAccuracy >= 0 && location.altitude != LocomotionMagicValue.nilAltitude {
- allAltitudes.append(location.altitude)
- }
-
- if location.horizontalAccuracy >= 0 {
- allAccuracies.append(location.horizontalAccuracy)
- }
-
- // exclude impossible speeds
- if location.speed >= 0 && location.speed.kmh < 1000 {
- allSpeeds.append(location.speed)
- }
-
- if location.course >= 0 {
- allCourses.append(location.course)
- }
-
- // markov
- if let previousType = sample.previousSampleConfirmedType {
- allPreviousTypes.append(previousType)
- }
- }
-
- self.totalSamples = totalSamples
-
- if accuracyScorables > 0 {
- accuracyScore = Double(correctScorables) / Double(accuracyScorables)
- } else {
- accuracyScore = nil
- }
-
- coreMotionTypeScores = coreMotionTypeScoresDict(for: allCoreMotionTypes)
- previousSampleActivityTypeScores = markovScoresDict(for: allPreviousTypes)
-
- if totalSamples == 0 {
- movingPct = 0.5
- speedHistogram = nil
- stepHzHistogram = nil
- xyAccelerationHistogram = nil
- zAccelerationHistogram = nil
- courseVarianceHistogram = nil
- altitudeHistogram = nil
- courseHistogram = nil
- timeOfDayHistogram = nil
- horizontalAccuracyHistogram = nil
- coordinatesMatrix = nil
-
- } else {
-
- // motion factors
- movingPct = Double(totalMoving) / Double(totalSamples)
- speedHistogram = Histogram(values: allSpeeds, minBoundary: 0, trimOutliers: true, name: "SPEED",
- printFormat: "%6.1f kmh", printModifier: 3.6)
- stepHzHistogram = Histogram(values: allStepHz, minBoundary: 0, trimOutliers: true, name: "STEPHZ",
- printFormat: "%7.2f Hz")
- xyAccelerationHistogram = Histogram(values: allXYAccelerations, minBoundary: 0, trimOutliers: true,
- name: "WIGGLES XY")
- zAccelerationHistogram = Histogram(values: allZAccelerations, minBoundary: 0, trimOutliers: true,
- name: "WIGGLES Z")
- courseVarianceHistogram = Histogram(values: allCourseVariances, minBoundary: 0, maxBoundary: 1,
- name: "COURSE VARIANCE", printFormat: "%10.2f")
-
- // context factors
- altitudeHistogram = Histogram(values: allAltitudes, trimOutliers: true, name: "ALTITUDE",
- printFormat: "%8.0f m")
- courseHistogram = Histogram(values: allCourses, minBoundary: 0, maxBoundary: 360, name: "COURSE",
- printFormat: "%8.0f °")
- timeOfDayHistogram = Histogram(values: allTimesOfDay, minBoundary: 0, maxBoundary: 60 * 60 * 24,
- pseudoCount: 100, name: "TIME OF DAY", printFormat: "%8.2f h",
- printModifier: 60 / 60 / 60 / 60)
- horizontalAccuracyHistogram = Histogram(values: allAccuracies, minBoundary: 0, trimOutliers: true,
- name: "HORIZ ACCURACY")
-
- // type requires a coordinate match to be non zero?
- let pseudoCount = ActivityTypeName.extendedTypes.contains(name) ? 0 : 1
-
- coordinatesMatrix = CoordinatesMatrix(coordinates: allCoordinates, latBinCount: numberOfLatBuckets,
- lngBinCount: numberOfLongBuckets, latRange: latitudeRange,
- lngRange: longitudeRange, pseudoCount: UInt16(pseudoCount))
- }
-
- version = ActivityType.currentVersion
- lastUpdated = Date()
- needsUpdate = false
-
- if MutableActivityType.statsDebug {
- printStats()
- }
- }
-
- private func coreMotionTypeScoresDict(for values: [CoreMotionActivityTypeName]) -> [CoreMotionActivityTypeName: Double] {
- var totals: [CoreMotionActivityTypeName: Double] = [:]
- var scores: [CoreMotionActivityTypeName: Double] = [:]
-
- for coreMotionType in CoreMotionActivityTypeName.allTypes {
- totals[coreMotionType] = 0
- scores[coreMotionType] = 0
- }
-
- guard values.count > 0 else {
- return scores
- }
-
- for coreMotionType in values {
- if totals[coreMotionType] != nil {
- totals[coreMotionType]! += 1
- }
- }
-
- for coreMotionType in CoreMotionActivityTypeName.allTypes {
- if let total = totals[coreMotionType], total > 0 {
- scores[coreMotionType] = total / Double(values.count)
- }
- }
-
- return scores
- }
-
- private func markovScoresDict(for values: [ActivityTypeName]) -> [ActivityTypeName: Double] {
- var totals: [ActivityTypeName: Double] = [:]
- var scores: [ActivityTypeName: Double] = [:]
-
- for activityType in ActivityTypeName.allTypes {
- totals[activityType] = 0
- scores[activityType] = 0
- }
-
- guard values.count > 0 else {
- return scores
- }
-
- for activityType in values {
- if totals[activityType] != nil {
- totals[activityType]! += 1
- }
- }
-
- for activityType in ActivityTypeName.allTypes {
- if let total = totals[activityType], total > 0 {
- scores[activityType] = total / Double(values.count)
- }
- }
-
- return scores
- }
-
- public var statsDict: [String: Any] {
- var dict: [String: Any] = [:]
-
- dict["latitudeMin"] = latitudeRange.min
- dict["latitudeMax"] = latitudeRange.max
- dict["longitudeMin"] = longitudeRange.min
- dict["longitudeMax"] = longitudeRange.max
-
- dict["movingPct"] = movingPct
- dict["coreMotionTypeScores"] = coreMotionTypeScoresArray
- dict["speedHistogram"] = speedHistogram?.serialised
- dict["stepHzHistogram"] = stepHzHistogram?.serialised
- dict["courseVarianceHistogram"] = courseVarianceHistogram?.serialised
- dict["altitudeHistogram"] = altitudeHistogram?.serialised
- dict["courseHistogram"] = courseHistogram?.serialised
- dict["timeOfDayHistogram"] = timeOfDayHistogram?.serialised
- dict["xyAccelerationHistogram"] = xyAccelerationHistogram?.serialised
- dict["zAccelerationHistogram"] = zAccelerationHistogram?.serialised
- dict["horizontalAccuracyHistogram"] = horizontalAccuracyHistogram?.serialised
- dict["coordinatesMatrix"] = coordinatesMatrix?.serialised
-
- return dict
- }
-
- open override func encode(to container: inout PersistenceContainer) {
- super.encode(to: &container)
- container["needsUpdate"] = needsUpdate
- }
-
-}
diff --git a/LocoKit/Timelines/CLPlacemarkCache.swift b/LocoKit/Timelines/CLPlacemarkCache.swift
deleted file mode 100644
index fa6cd610..00000000
--- a/LocoKit/Timelines/CLPlacemarkCache.swift
+++ /dev/null
@@ -1,51 +0,0 @@
-//
-// CLPlacemarkCache.swift
-// LocoKit
-//
-// Created by Matt Greenfield on 29/7/18.
-//
-
-import CoreLocation
-
-public class CLPlacemarkCache {
-
- private static let cache = NSCache()
-
- private static let mutex = UnfairLock()
-
- private static var fetching: Set = []
-
- public static func fetchPlacemark(for location: CLLocation, completion: @escaping (CLPlacemark?) -> Void) {
-
- // have a cached value? use that
- if let cached = cache.object(forKey: location) {
- completion(cached)
- return
- }
-
- let alreadyFetching = mutex.sync { fetching.contains(location.hashValue) }
- if alreadyFetching {
- completion(nil)
- return
- }
-
- mutex.sync { fetching.insert(location.hashValue) }
-
- CLGeocoder().reverseGeocodeLocation(location) { placemarks, error in
- mutex.sync { fetching.remove(location.hashValue) }
-
- // nil result? nil completion
- guard let placemark = placemarks?.first else {
- completion(nil)
- return
- }
-
- // cache the result and return it
- cache.setObject(placemark, forKey: location)
- completion(placemark)
- }
- }
-
- private init() {}
-
-}
diff --git a/LocoKit/Timelines/CoordinateTrust.swift b/LocoKit/Timelines/CoordinateTrust.swift
deleted file mode 100644
index c405418d..00000000
--- a/LocoKit/Timelines/CoordinateTrust.swift
+++ /dev/null
@@ -1,73 +0,0 @@
-//
-// CoordinateTrust.swift
-// LocoKit
-//
-// Created by Matt Greenfield on 28/6/19.
-//
-
-import GRDB
-import CoreLocation
-
-class CoordinateTrust: Record, Codable {
-
- var latitude: CLLocationDegrees
- var longitude: CLLocationDegrees
- var trustFactor: Double
-
- // MARK: -
-
- var coordinate: CLLocationCoordinate2D {
- get {
- return CLLocationCoordinate2D(latitude: latitude, longitude: longitude)
- }
- set {
- latitude = newValue.latitude
- longitude = newValue.longitude
- }
- }
-
- // MARK: - Init
-
- init(coordinate: CLLocationCoordinate2D) {
- self.latitude = coordinate.latitude
- self.longitude = coordinate.longitude
- self.trustFactor = 1
- super.init()
- }
-
- // MARK: - Updating
-
- func update(from samples: [LocomotionSample]) {
- let speeds = samples.compactMap { $0.location?.speed }.filter { $0 >= 0 }
- let meanSpeed = speeds.mean
-
- // most common walking speed is 4.4 kmh
- // most common running speed is 9.7 kmh
-
- let maximumDistrust = 5.0 // maximum distrusted stationary speed in kmh
-
- trustFactor = 1.0 - (meanSpeed.kmh / maximumDistrust).clamped(min: 0, max: 1)
- }
-
- // MARK: - Record
-
- override class var databaseTableName: String { return "CoordinateTrust" }
-
- enum Columns: String, ColumnExpression {
- case latitude, longitude, trustFactor
- }
-
- required init(row: Row) {
- self.latitude = row[Columns.latitude]
- self.longitude = row[Columns.longitude]
- self.trustFactor = row[Columns.trustFactor]
- super.init(row: row)
- }
-
- override func encode(to container: inout PersistenceContainer) {
- container[Columns.latitude] = coordinate.latitude
- container[Columns.longitude] = coordinate.longitude
- container[Columns.trustFactor] = trustFactor
- }
-
-}
diff --git a/LocoKit/Timelines/CoordinateTrustManager.swift b/LocoKit/Timelines/CoordinateTrustManager.swift
deleted file mode 100644
index b7571c35..00000000
--- a/LocoKit/Timelines/CoordinateTrustManager.swift
+++ /dev/null
@@ -1,131 +0,0 @@
-//
-// CoordinateTrustManager.swift
-// LocoKit
-//
-// Created by Matt Greenfield on 30/6/19.
-//
-
-import os.log
-import CoreLocation
-
-public class CoordinateTrustManager: TrustAssessor {
-
- private let cache = NSCache()
- public private(set) var lastUpdated: Date?
- public let store: TimelineStore
-
- // MARK: -
-
- public init(store: TimelineStore) {
- self.store = store
- }
-
- // MARK: - Fetching
-
- public func trustFactorFor(_ coordinate: CLLocationCoordinate2D) -> Double? {
- return modelFor(coordinate)?.trustFactor
- }
-
- func modelFor(_ coordinate: CLLocationCoordinate2D) -> CoordinateTrust? {
- let rounded = CoordinateTrustManager.roundedCoordinateFor(coordinate)
-
- // cached?
- if let model = cache.object(forKey: rounded) { return model }
-
- if let model = try? store.auxiliaryPool.read({
- try CoordinateTrust.fetchOne($0, sql: "SELECT * FROM CoordinateTrust WHERE latitude = ? AND longitude = ?",
- arguments: [rounded.latitude, rounded.longitude])
- }) {
- cache.setObject(model, forKey: rounded)
- return model
- }
-
- return nil
- }
-
- // MARK: -
-
- static func roundedCoordinateFor(_ coordinate: CLLocationCoordinate2D) -> Coordinate {
- let rounded = CLLocationCoordinate2D(latitude: round(coordinate.latitude * 10000) / 10000,
- longitude: round(coordinate.longitude * 10000) / 10000)
- return Coordinate(coordinate: rounded)
- }
-
- // MARK: - Updating
-
- public func updateTrustFactors() {
- // don't update too frequently
- if let lastUpdated = lastUpdated, lastUpdated.age < .oneDay { return }
-
- os_log("CoordinateTrustManager.updateTrustFactors", type: .debug)
-
- self.lastUpdated = Date()
-
- // fetch most recent X confirmed stationary samples
- let samples = self.store.samples(where: "confirmedType = ? ORDER BY lastSaved DESC LIMIT 2000", arguments: ["stationary"])
-
- // collate the samples into coordinate buckets
- var buckets: [Coordinate: [LocomotionSample]] = [:]
- for sample in samples where sample.hasUsableCoordinate {
- guard let coordinate = sample.location?.coordinate else { continue }
-
- let rounded = CoordinateTrustManager.roundedCoordinateFor(coordinate)
- if let samples = buckets[rounded] {
- buckets[rounded] = samples + [sample]
- } else {
- buckets[rounded] = [sample]
- }
- }
-
- // for each bucket, fetch/create the model
- var models: [CoordinateTrust] = []
- for (coordinate, samples) in buckets {
- let model: CoordinateTrust
- if let trust = self.modelFor(coordinate.coordinate) {
- model = trust
- } else {
- model = CoordinateTrust(coordinate: coordinate.coordinate)
- }
- models.append(model)
-
- // update the model's trustFactor
- model.update(from: samples)
- }
-
- // save/update the models
- do {
- try self.store.auxiliaryPool.write { db in
- for model in models {
- try model.save(db)
- }
- }
- } catch {
- print("ERROR: \(error)")
- }
- }
-
-}
-
-class Coordinate: NSObject {
-
- let latitude: CLLocationDegrees
- let longitude: CLLocationDegrees
- var coordinate: CLLocationCoordinate2D { return CLLocationCoordinate2D(latitude: latitude, longitude: longitude) }
-
- init(coordinate: CLLocationCoordinate2D) {
- self.latitude = coordinate.latitude
- self.longitude = coordinate.longitude
- }
-
- // MARK: - Hashable
-
- override func isEqual(_ object: Any?) -> Bool {
- guard let other = object as? Coordinate else { return false }
- return other.latitude == latitude && other.longitude == longitude
- }
-
- override var hash: Int {
- return latitude.hashValue ^ latitude.hashValue
- }
-
-}
diff --git a/LocoKit/Timelines/ItemsObserver.swift b/LocoKit/Timelines/ItemsObserver.swift
deleted file mode 100644
index bd581c78..00000000
--- a/LocoKit/Timelines/ItemsObserver.swift
+++ /dev/null
@@ -1,80 +0,0 @@
-//
-// ItemsObserver.swift
-// LocoKit
-//
-// Created by Matt Greenfield on 11/9/18.
-//
-
-import Foundation
-import os.log
-import GRDB
-
-class ItemsObserver: TransactionObserver {
-
- var store: TimelineStore
- var changedRowIds: Set = []
-
- init(store: TimelineStore) {
- self.store = store
- }
-
- // observe updates to next/prev item links
- func observes(eventsOfKind eventKind: DatabaseEventKind) -> Bool {
- switch eventKind {
- case .update(let tableName, let columnNames):
- guard tableName == "TimelineItem" else { return false }
- let itemEdges: Set = ["previousItemId", "nextItemId"]
- return itemEdges.intersection(columnNames).count > 0
- default: return false
- }
- }
-
- func databaseDidChange(with event: DatabaseEvent) {
- changedRowIds.insert(event.rowID)
- }
-
- func databaseDidCommit(_ db: Database) {
- let rowIds: Set = store.mutex.sync {
- let rowIds = changedRowIds
- changedRowIds = []
- return rowIds
- }
-
- if rowIds.isEmpty { return }
-
- /** maintain the timeline items linked list locally, for changes made outside the managed environment **/
-
- do {
- let marks = repeatElement("?", count: rowIds.count).joined(separator: ",")
- let query = "SELECT itemId, previousItemId, nextItemId FROM TimelineItem WHERE rowId IN (\(marks))"
- let rows = try Row.fetchCursor(db, sql: query, arguments: StatementArguments(rowIds))
-
- while let row = try rows.next() {
- let previousItemIdString = row["previousItemId"] as String?
- let nextItemIdString = row["nextItemId"] as String?
-
- guard let uuidString = row["itemId"] as String?, let itemId = UUID(uuidString: uuidString) else { continue }
- guard let item = store.object(for: itemId) as? TimelineItem else { continue }
-
- if let uuidString = previousItemIdString, item.previousItemId?.uuidString != uuidString {
- item.previousItemId = UUID(uuidString: uuidString)
-
- } else if previousItemIdString == nil && item.previousItemId != nil {
- item.previousItemId = nil
- }
-
- if let uuidString = nextItemIdString, item.nextItemId?.uuidString != uuidString {
- item.nextItemId = UUID(uuidString: uuidString)
-
- } else if nextItemIdString == nil && item.nextItemId != nil {
- item.nextItemId = nil
- }
- }
-
- } catch {
- os_log("SQL Exception: %@", error.localizedDescription)
- }
- }
-
- func databaseDidRollback(_ db: Database) {}
-}
diff --git a/LocoKit/Timelines/Merge.swift b/LocoKit/Timelines/Merge.swift
deleted file mode 100644
index 81ed51e1..00000000
--- a/LocoKit/Timelines/Merge.swift
+++ /dev/null
@@ -1,142 +0,0 @@
-//
-// Created by Matt Greenfield on 25/05/16.
-// Copyright (c) 2016 Big Paua. All rights reserved.
-//
-
-import os.log
-import Foundation
-
-public extension NSNotification.Name {
- static let mergedTimelineItems = Notification.Name("mergedTimelineItems")
-}
-
-typealias MergeScore = ConsumptionScore
-public typealias MergeResult = (kept: TimelineItem, killed: [TimelineItem])
-
-internal class Merge: Hashable, CustomStringConvertible {
-
- var keeper: TimelineItem
- var betweener: TimelineItem?
- var deadman: TimelineItem
-
- var isValid: Bool {
- if keeper.deleted || deadman.deleted || betweener?.deleted == true { return false }
- if keeper.invalidated || deadman.invalidated || betweener?.invalidated == true { return false }
-
- // check for dupes (which should be impossible, but weird stuff happens)
- var itemIds: Set = [keeper.itemId, deadman.itemId]
- if let betweener = betweener {
- itemIds.insert(betweener.itemId)
- if itemIds.count != 3 { return false }
- } else {
- if itemIds.count != 2 { return false }
- }
-
- if let betweener = betweener {
- // keeper -> betweener -> deadman
- if keeper.nextItem == betweener, betweener.nextItem == deadman { return true }
- // deadman -> betweener -> keeper
- if deadman.nextItem == betweener, betweener.nextItem == keeper { return true }
- } else {
- // keeper -> deadman
- if keeper.nextItem == deadman { return true }
- // deadman -> keeper
- if deadman.nextItem == keeper { return true }
- }
-
- return false
- }
-
- lazy var score: MergeScore = {
- if keeper.isMergeLocked || deadman.isMergeLocked || betweener?.isMergeLocked == true { return .impossible }
- guard isValid else { return .impossible }
- return self.keeper.scoreForConsuming(item: self.deadman)
- }()
-
- init(keeper: TimelineItem, betweener: TimelineItem? = nil, deadman: TimelineItem) {
- self.keeper = keeper
- self.deadman = deadman
- if let betweener = betweener {
- self.betweener = betweener
- }
- }
-
- @discardableResult func doIt() -> MergeResult {
- let description = String(describing: self)
- if TimelineProcessor.debugLogging { os_log("Doing:\n%@", type: .debug, description) }
-
- merge(deadman, into: keeper)
-
- let results: MergeResult
- if let betweener = betweener {
- results = (kept: keeper, killed: [deadman, betweener])
- } else {
- results = (kept: keeper, killed: [deadman])
- }
-
- // notify listeners
- let note = Notification(name: .mergedTimelineItems, object: self,
- userInfo: ["description": description, "results": results])
- NotificationCenter.default.post(note)
-
- return results
- }
-
- private func merge(_ deadman: TimelineItem, into keeper: TimelineItem) {
- guard isValid else { os_log("Invalid merge", type: .error); return }
-
- // deadman is previous
- if keeper.previousItem == deadman || (betweener != nil && keeper.previousItem == betweener) {
- keeper.previousItem = deadman.previousItem
-
- // deadman is next
- } else if keeper.nextItem == deadman || (betweener != nil && keeper.nextItem == betweener) {
- keeper.nextItem = deadman.nextItem
-
- } else {
- return
- }
-
- // deal with a betweener
- if let betweener = betweener {
- keeper.willConsume(item: betweener)
- keeper.add(betweener.samples)
- betweener.delete()
- }
-
- // deal with the deadman
- keeper.willConsume(item: deadman)
- keeper.add(deadman.samples)
- deadman.delete()
- }
-
- // MARK: - Hashable
-
- func hash(into hasher: inout Hasher) {
- hasher.combine(keeper)
- hasher.combine(deadman)
- if let betweener = betweener {
- hasher.combine(betweener)
- }
- if let startDate = keeper.startDate {
- hasher.combine(startDate)
- }
- }
-
- static func == (lhs: Merge, rhs: Merge) -> Bool {
- return lhs.hashValue == rhs.hashValue
- }
-
- // MARK: - CustomStringConvertible
-
- var description: String {
- if let betweener = betweener {
- return String(format: "score: %d (%@) <- (%@) <- (%@)", score.rawValue, String(describing: keeper),
- String(describing: betweener), String(describing: deadman))
- } else {
- return String(format: "score: %d (%@) <- (%@)", score.rawValue, String(describing: keeper),
- String(describing: deadman))
- }
- }
-
-}
diff --git a/LocoKit/Timelines/MergeScores.swift b/LocoKit/Timelines/MergeScores.swift
deleted file mode 100644
index 58d6b136..00000000
--- a/LocoKit/Timelines/MergeScores.swift
+++ /dev/null
@@ -1,194 +0,0 @@
-//
-// MergeScores.swift
-// LearnerCoacher
-//
-// Created by Matt Greenfield on 15/12/16.
-// Copyright © 2016 Big Paua. All rights reserved.
-//
-
-import os.log
-import Foundation
-
-public enum ConsumptionScore: Int {
- case perfect = 5
- case high = 4
- case medium = 3
- case low = 2
- case veryLow = 1
- case impossible = 0
-}
-
-class MergeScores {
-
- // MARK: - SOMETHING <- SOMETHING
- static func consumptionScoreFor(_ consumer: TimelineItem, toConsume consumee: TimelineItem) -> ConsumptionScore {
-
- // can't do anything with merge locked items
- if consumer.isMergeLocked || consumee.isMergeLocked { return .impossible }
-
- // deadmen can't consume anyone
- if consumer.deleted { return .impossible }
-
- // if consumee has zero samples, call it a perfect merge
- if consumee.samples.isEmpty { return .perfect }
-
- // if consumer has zero samples, call it impossible
- if consumer.samples.isEmpty { return .impossible }
-
- // data gaps can only consume data gaps
- if consumer.isDataGap { return consumee.isDataGap ? .perfect : .impossible }
-
- // anyone can consume an invalid data gap, but no one can consume a valid data gap
- if consumee.isDataGap { return consumee.isInvalid ? .medium : .impossible }
-
- // nolos can only consume nolos
- if consumer.isNolo { return consumee.isNolo ? .perfect : .impossible }
-
- // anyone can consume an invalid nolo
- if consumee.isNolo && consumee.isInvalid { return .medium }
-
- // test for impossible separation distance
- guard consumer.withinMergeableDistance(from: consumee) else { return .impossible }
-
- // visit <- something
- if let visit = consumer as? Visit { return consumptionScoreFor(visit: visit, toConsume: consumee) }
-
- // path <- something
- if let path = consumer as? Path { return consumptionScoreFor(path: path, toConsume: consumee) }
-
- return .impossible
- }
-
- // MARK: - PATH <- SOMETHING
- private static func consumptionScoreFor(path consumer: Path, toConsume consumee: TimelineItem) -> ConsumptionScore {
-
- // consumer is invalid
- if consumer.isInvalid {
-
- // invalid <- invalid
- if consumee.isInvalid { return .veryLow }
-
- // invalid <- valid
- return .impossible
- }
-
- // path <- visit
- if let visit = consumee as? Visit { return consumptionScoreFor(path: consumer, toConsumeVisit: visit) }
-
- // path <- vpath
- if let path = consumee as? Path { return consumptionScoreFor(path: consumer, toConsumePath: path) }
-
- return .impossible
- }
-
- // MARK: - PATH <- VISIT
- private static func consumptionScoreFor(path consumer: Path, toConsumeVisit consumee: Visit) -> ConsumptionScore {
-
- // can't consume a keeper visit
- if consumee.isWorthKeeping { return .impossible }
-
- // consumer is keeper
- if consumer.isWorthKeeping {
-
- // keeper <- invalid
- if consumee.isInvalid { return .medium }
-
- // keeper <- valid
- return .low
- }
-
- // consumer is valid
- if consumer.isValid {
-
- // valid <- invalid
- if consumee.isInvalid { return .low }
-
- // valid <- valid
- return .veryLow
- }
-
- // consumer is invalid (actually already dealt with in previous method)
- return .impossible
- }
-
- // MARK: - PATH <- PATH
- private static func consumptionScoreFor(path consumer: Path, toConsumePath consumee: Path) -> ConsumptionScore {
- let consumerType = consumer.modeMovingActivityType ?? consumer.modeActivityType
- let consumeeType = consumee.modeMovingActivityType ?? consumee.modeActivityType
-
- // no types means it's a random guess
- if consumerType == nil && consumeeType == nil { return .medium }
-
- // perfect type match
- if consumeeType == consumerType { return .perfect }
-
- // can't consume a keeper path
- if consumee.isWorthKeeping { return .impossible }
-
- // a path with nil type can't consume anyone
- guard let scoringType = consumerType else { return .impossible }
-
- guard let typeResult = consumee.classifierResults?.first(where: { $0.name == scoringType }) else {
- return .impossible
- }
-
- // consumee's type score for consumer's type, as a usable Int
- let typeScore = Int(floor(typeResult.score * 1000))
-
- switch typeScore {
- case 75...Int.max:
- return .perfect
- case 50...75:
- return .high
- case 25...50:
- return .medium
- case 10...25:
- return .low
- default:
- return .veryLow
- }
- }
-
- // MARK: - VISIT <- SOMETHING
- private static func consumptionScoreFor(visit consumer: Visit, toConsume consumee: TimelineItem) -> ConsumptionScore {
-
- // visit <- visit
- if let visit = consumee as? Visit { return consumptionScoreFor(visit: consumer, toConsumeVisit: visit) }
-
- // visit <- path
- if let path = consumee as? Path { return consumptionScoreFor(visit: consumer, toConsumePath: path) }
-
- return .impossible
- }
-
- // MARK: - VISIT <- VISIT
- private static func consumptionScoreFor(visit consumer: Visit, toConsumeVisit consumee: Visit) -> ConsumptionScore {
-
- // overlapping visits
- if consumer.overlaps(consumee) {
- return consumer.duration > consumee.duration ? .perfect : .high
- }
-
- return .impossible
- }
-
- // MARK: - VISIT <- PATH
- private static func consumptionScoreFor(visit consumer: Visit, toConsumePath consumee: Path) -> ConsumptionScore {
-
- // percentage of path inside the visit
- let pctInsideScore = Int(floor(consumee.percentInside(consumer) * 10))
-
- // valid / keeper visit <- invalid path
- if consumer.isValid && consumee.isInvalid {
- switch pctInsideScore {
- case 10: // 100%
- return .low
- default:
- return .veryLow
- }
- }
-
- return .impossible
- }
-
-}
diff --git a/LocoKit/Timelines/TimelineClassifier.swift b/LocoKit/Timelines/TimelineClassifier.swift
deleted file mode 100644
index d0ff762f..00000000
--- a/LocoKit/Timelines/TimelineClassifier.swift
+++ /dev/null
@@ -1,29 +0,0 @@
-//
-// TimelineClassifier.swift
-// LocoKit
-//
-// Created by Matt Greenfield on 30/12/17.
-// Copyright © 2017 Big Paua. All rights reserved.
-//
-
-#if canImport(Reachability)
-import Reachability
-#endif
-
-public class TimelineClassifier: MLClassifierManager {
-
- public typealias Classifier = ActivityTypeClassifier
-
- public let minimumTransportCoverage = 0.10
-
- public static var highlander = TimelineClassifier()
-
- public var sampleClassifier: Classifier?
-
- #if canImport(Reachability)
- public let reachability = Reachability()!
- #endif
-
- public let mutex = PThreadMutex(type: .recursive)
-
-}
diff --git a/LocoKit/Timelines/TimelineObjects/ItemSegment.swift b/LocoKit/Timelines/TimelineObjects/ItemSegment.swift
deleted file mode 100644
index 40d95290..00000000
--- a/LocoKit/Timelines/TimelineObjects/ItemSegment.swift
+++ /dev/null
@@ -1,221 +0,0 @@
-//
-// ItemSegment.swift
-// LocoKit
-//
-// Created by Matt Greenfield on 24/12/17.
-// Copyright © 2017 Big Paua. All rights reserved.
-//
-
-import CoreLocation
-
-public class ItemSegment: Equatable, Identifiable {
-
- public weak var timelineItem: TimelineItem?
-
- private var unsortedSamples: Set = []
-
- private var _samples: [PersistentSample]?
- public var samples: [PersistentSample] {
- get {
- if let cached = _samples { return cached }
- _samples = unsortedSamples.sorted { $0.date < $1.date }
- return _samples!
- }
- set(newSamples) {
- unsortedSamples.removeAll()
- add(newSamples)
- }
- }
-
- // MARK: - Initialisers
-
- public init(samples: [PersistentSample], timelineItem: TimelineItem? = nil) {
- self.timelineItem = timelineItem
- self.add(samples)
- }
-
- public init(startDate: Date, activityType: ActivityTypeName, recordingState: RecordingState) {
- self.manualStartDate = startDate
- self.manualActivityType = activityType
- self.manualRecordingState = recordingState
- }
-
- /**
- A final sample, to mark the end of this segment outside of the `samples` array.
-
- Typically this is shared with (and owned by) the following item segment, acting as a bridge to allow this segment
- to end at the point where the next begins, without the shared sample being incorrectly subjected to modifications
- made to this segment (eg activity type changes).
- */
- public var endSample: PersistentSample? { didSet { samplesChanged() } }
-
- private var manualStartDate: Date?
- private var manualEndDate: Date?
- private var manualRecordingState: RecordingState?
- public var manualActivityType: ActivityTypeName?
-
- public var startDate: Date? { return manualStartDate ?? samples.first?.date }
- public var endDate: Date? {
- get { return manualEndDate ?? endSample?.date ?? samples.last?.date }
- set(newValue) {
- manualEndDate = newValue
- samplesChanged()
- }
- }
-
- public var recordingState: RecordingState? {
- if let manual = manualRecordingState { return manual }
-
- // segments in paths should always be treated as recording state
- if timelineItem is Path && timelineItem?.isDataGap == false { return .recording }
-
- return samples.first?.recordingState
- }
-
- public var duration: TimeInterval { return dateRange?.duration ?? 0 }
-
- private var _dateRange: DateInterval?
- public var dateRange: DateInterval? {
- if let cached = _dateRange { return cached }
- if let start = startDate, let end = endDate { _dateRange = DateInterval(start: start, end: end) }
- return _dateRange
- }
-
- private var _center: CLLocation?
- public var center: CLLocation? {
- if let center = _center { return center }
- _center = samples.weightedCenter
- return _center
- }
-
- private var _radius: Radius?
- public var radius: Radius {
- if let radius = _radius { return radius }
- if let center = center { _radius = samples.radius(from: center) }
- else { _radius = Radius.zero }
- return _radius!
- }
-
- private var _distance: CLLocationDistance?
- public var distance: CLLocationDistance {
- if let distance = _distance { return distance }
- let distance = samples.distance
- _distance = distance
- return distance
- }
-
- public var hasAnyUsableLocations: Bool {
- return samples.haveAnyUsableLocations
- }
-
- // MARK: - Keepness scores
-
- public var isInvalid: Bool { return !isValid }
-
- public var isValid: Bool {
- if activityType == .stationary {
- if samples.isEmpty { return false }
- if duration < Visit.minimumValidDuration { return false }
- } else {
- if samples.count < Path.minimumValidSamples { return false }
- if duration < Path.minimumValidDuration { return false }
- if distance < Path.minimumValidDistance { return false }
- }
- return true
- }
-
- public var isWorthKeeping: Bool {
- if !isValid { return false }
- if activityType == .stationary {
- if duration < Visit.minimumKeeperDuration { return false }
- } else {
- if duration < Path.minimumKeeperDuration { return false }
- if distance < Path.minimumKeeperDistance { return false }
- }
- return true
- }
-
- public var isDataGap: Bool {
- if samples.isEmpty { return false }
- for sample in samples {
- if sample.recordingState != .off { return false }
- }
- return true
- }
-
- // MARK: - Activity Types
-
- public var activityType: ActivityTypeName? {
- return manualActivityType ?? samples.first?.activityType
- }
-
- public var confirmedType: ActivityTypeName? {
- guard let activityType = activityType else { return nil }
- for sample in samples {
- if sample.confirmedType != activityType { return nil }
- }
- return activityType
- }
-
- private var _classifierResults: ClassifierResults? = nil
- public var classifierResults: ClassifierResults? {
- if let results = _classifierResults { return results }
- guard let results = timelineItem?.classifier?.classify(self, timeout: 30) else { return nil }
- if results.moreComing { return results }
- _classifierResults = results
- return results
- }
-
- // MARK: - Modifying the item segment
-
- func canAdd(_ sample: PersistentSample, ignoreRecordingState: Bool = false) -> Bool {
-
- // need at least an activityType match
- if sample.activityType != activityType { return false }
-
- // don't care about recordingStates?
- if ignoreRecordingState { return true }
-
- // need a recordingState match
- return sample.recordingState == recordingState
- }
-
- public func add(_ sample: PersistentSample) {
- add([sample])
- }
-
- public func add(_ samples: [PersistentSample]) {
- unsortedSamples.formUnion(samples)
- samplesChanged()
- }
-
- public func remove(_ sample: PersistentSample) {
- remove([sample])
- }
-
- public func remove(_ samples: [PersistentSample]) {
- unsortedSamples.subtract(samples)
- samplesChanged()
- }
-
- public func samplesChanged() {
- _samples = nil
- _dateRange = nil
- _center = nil
- _radius = nil
- _distance = nil
- _classifierResults = nil
- }
-
- // MARK: - Equatable
-
- public static func ==(lhs: ItemSegment, rhs: ItemSegment) -> Bool {
- return lhs.dateRange == rhs.dateRange && lhs.samples.count == rhs.samples.count
- }
-
- // MARK: - Identifiable
-
- public private(set) var id = UUID()
-
-}
-
diff --git a/LocoKit/Timelines/TimelineObjects/Path.swift b/LocoKit/Timelines/TimelineObjects/Path.swift
deleted file mode 100644
index 27a59dae..00000000
--- a/LocoKit/Timelines/TimelineObjects/Path.swift
+++ /dev/null
@@ -1,306 +0,0 @@
-//
-// Path.swift
-// LocoKit
-//
-// Created by Matt Greenfield on 2/12/17.
-// Copyright © 2017 Big Paua. All rights reserved.
-//
-
-import GRDB
-import CoreLocation
-
-open class Path: TimelineItem, CustomStringConvertible {
-
- // valid path settings
- public static var minimumValidDuration: TimeInterval = 10
- public static var minimumValidDistance: Double = 10
- public static var minimumValidSamples = 2
-
- // keeper path settings
- public static var minimumKeeperDuration: TimeInterval = 60
- public static var minimumKeeperDistance: Double = 20
-
- // data gap settings
- public static var minimumValidDataGapDuration: TimeInterval = 60
- public static var minimumKeeperDataGapDuration: TimeInterval = 60 * 60 * 24
-
- public static var maximumModeShiftSpeed = CLLocationSpeed(kmh: 2)
-
- public private(set) var _distance: CLLocationDistance?
-
- // MARK: -
-
- public required init(from dict: [String: Any?], in store: TimelineStore) {
- self._distance = dict["distance"] as? CLLocationDistance
- super.init(from: dict, in: store)
- }
-
- public required init(from decoder: Decoder) throws {
- let container = try decoder.container(keyedBy: CodingKeys.self)
- guard let isVisit = try? container.decode(Bool.self, forKey: .isVisit), !isVisit else {
- throw DecodeError.runtimeError("Trying to decode a Visit as a Path")
- }
- try super.init(from: decoder)
- }
-
- public required init(in store: TimelineStore) {
- super.init(in: store)
- }
-
- // MARK: -
-
- open override var title: String {
- if isDataGap { return "Data Gap" }
- return activityType?.displayName.capitalized ?? "Unknown"
- }
-
- // MARK: - Item validity
-
- open override var isValid: Bool {
- if isDataGap { return isValidDataGap }
- if isNolo { return isValidPathNolo }
- if samples.count < Path.minimumValidSamples { return false }
- if duration < Path.minimumValidDuration { return false }
- if distance < Path.minimumValidDistance { return false }
- return true
- }
-
- open override var isWorthKeeping: Bool {
- if isDataGap { return dataGapIsWorthKeeping }
- if !isValid { return false }
- if duration < Path.minimumKeeperDuration { return false }
- if distance < Path.minimumKeeperDistance { return false }
- return true
- }
-
- private var isValidDataGap: Bool {
- if duration < Path.minimumValidDataGapDuration { return false }
- return true
- }
-
- private var isValidPathNolo: Bool {
- if samples.count < Path.minimumValidSamples { return false }
- if duration < Path.minimumValidDuration { return false }
- return true
- }
-
- private var dataGapIsWorthKeeping: Bool {
- if !isValidDataGap { return false }
- if duration < Path.minimumKeeperDataGapDuration { return false }
- return true
- }
-
- // MARK: - Distance and speed
-
- /// The distance of the path, as the sum of the distances between each sample.
- public var distance: CLLocationDistance {
- if let distance = _distance { return distance }
- let distance = samples.distance
- _distance = distance
- return distance
- }
-
- public var metresPerSecond: CLLocationSpeed {
- if samples.count == 1, let sampleSpeed = samples.first?.location?.speed, sampleSpeed >= 0 { return sampleSpeed }
- if duration > 0 { return distance / duration }
- return 0
- }
-
- public var speed: CLLocationSpeed { return metresPerSecond }
-
- public var mps: CLLocationSpeed { return metresPerSecond }
-
- public var kph: Double { return kilometresPerHour }
-
- public var kmh: Double { return kilometresPerHour }
-
- public var kilometresPerHour: Double { return mps * 3.6 }
-
- public var mph: Double { return milesPerHour }
-
- public var milesPerHour: Double { return kilometresPerHour / 1.609344 }
-
- // MARK: - Comparisons and Helpers
-
- public override func distance(from otherItem: TimelineItem) -> CLLocationDistance? {
- if let path = otherItem as? Path { return distance(from: path) }
- if let visit = otherItem as? Visit { return distance(from: visit) }
- return nil
- }
-
- private func distance(from visit: Visit) -> CLLocationDistance? { return visit.distance(from: self) }
-
- private func distance(from otherPath: Path) -> CLLocationDistance? {
- guard let myStart = startDate, let theirStart = otherPath.startDate else { return nil }
- if myStart < theirStart {
- if let myEdge = samples.last, let theirEdge = otherPath.samples.first {
- return myEdge.distance(from: theirEdge)
- }
- } else {
- if let myEdge = samples.first, let theirEdge = otherPath.samples.last {
- return myEdge.distance(from: theirEdge)
- }
- }
- return nil
- }
-
- public override func contains(_ location: CLLocation, sd: Double?) -> Bool {
- var sampleLocation: CLLocation?
- var distanceToPrev: CLLocationDistance = 0
- var distanceToNext: CLLocationDistance = 0
-
- for nextSample in samples {
- guard let nextSampleLocation = nextSample.location else {
- continue
- }
-
- // TODO: this could use the sample's horizontalAccuracy
- let minimumRadius: CLLocationDistance = 10
-
- // get distance from current to next
- if let current = sampleLocation {
- distanceToNext = current.distance(from: nextSampleLocation)
- }
-
- // choose largest distance of toPrev, toNext, and minRadius
- let radius = max(max(distanceToPrev, distanceToNext), minimumRadius)
-
- // test location against current
- if let current = sampleLocation {
- if location.distance(from: current) <= radius {
- return true
- }
- }
-
- // prep the next cycle
- distanceToPrev = distanceToNext
- sampleLocation = nextSampleLocation
- }
-
- return false
- }
-
- open func samplesInside(_ visit: Visit) -> Set {
- guard let visitCenter = visit.center else {
- return []
- }
- var insiders: Set = []
- for sample in samples where sample.hasUsableCoordinate {
- guard let sampleLocation = sample.location else { continue }
- let metresFromCentre = visitCenter.distance(from: sampleLocation)
- if metresFromCentre <= visit.radius1sd {
- insiders.insert(sample)
- }
- }
- return insiders
- }
-
- public func samplesOutside(_ visit: Visit) -> Set {
- return Set(samples).subtracting(samplesInside(visit))
- }
-
- /// The percentage of the path's distance, duration, and sample count that is contained inside the given visit.
- public func percentInside(_ visit: Visit) -> Double {
- return visit.containedPercentOf(self)
- }
-
- public override func maximumMergeableDistance(from otherItem: TimelineItem) -> CLLocationDistance {
- if let path = otherItem as? Path {
- return maximumMergeableDistance(from: path)
- }
- if let visit = otherItem as? Visit {
- return maximumMergeableDistance(from: visit)
- }
- return 0
- }
-
- private func maximumMergeableDistance(from visit: Visit) -> CLLocationDistance {
- return visit.maximumMergeableDistance(from: self)
- }
-
- private func maximumMergeableDistance(from otherPath: Path) -> CLLocationDistance {
- guard let timeSeparation = self.timeInterval(from: otherPath) else {
- return 0
- }
- var speeds: [CLLocationSpeed] = []
- if self.mps > 0 {
- speeds.append(self.mps)
- }
- if otherPath.mps > 0 {
- speeds.append(otherPath.mps)
- }
- return CLLocationDistance(speeds.mean * timeSeparation * 4)
- }
-
- internal override func cleanseEdge(with otherPath: Path, excluding: Set) -> LocomotionSample? {
- if self.isMergeLocked || otherPath.isMergeLocked { return nil }
- if self.isDataGap || otherPath.isDataGap { return nil }
- if self.deleted || otherPath.deleted { return nil }
- if otherPath.samples.isEmpty { return nil }
-
- // fail out if separation distance is too much
- guard withinMergeableDistance(from: otherPath) else { return nil }
-
- // fail out if separation time is too much
- guard let timeGap = timeInterval(from: otherPath), timeGap < 60 * 10 else { return nil }
-
- // get the activity types
- guard let myActivityType = self.activityType else { return nil }
- guard let theirActivityType = otherPath.activityType else { return nil }
-
- // can't path-path cleanse two paths of same type
- if myActivityType == theirActivityType { return nil }
-
- // get the edges
- guard let myEdge = self.edgeSample(with: otherPath) else { return nil }
- guard let theirEdge = otherPath.edgeSample(with: self) else { return nil }
- guard myEdge.hasUsableCoordinate, theirEdge.hasUsableCoordinate else { return nil }
- guard let myEdgeLocation = myEdge.location, let theirEdgeLocation = theirEdge.location else { return nil }
-
- let mySpeedIsSlow = myEdgeLocation.speed < Path.maximumModeShiftSpeed
- let theirSpeedIsSlow = theirEdgeLocation.speed < Path.maximumModeShiftSpeed
-
- // are the edges on opposite sides of the mode change speed boundary?
- if mySpeedIsSlow != theirSpeedIsSlow { return nil }
-
- // is their edge my activity type?
- if !excluding.contains(theirEdge), theirEdge.activityType == myActivityType {
- print("stealing otherPath edge")
- self.add(theirEdge)
- return theirEdge
- }
-
- return nil
- }
-
- override open func samplesChanged() {
- super.samplesChanged()
- _distance = nil
- }
-
- // MARK: - PersistableRecord
-
- open override func encode(to container: inout PersistenceContainer) {
- super.encode(to: &container)
- container["isVisit"] = false
- container["distance"] = _distance
- container["activityType"] = _modeMovingActivityType?.rawValue
- }
-
- // MARK: - Encodable
-
- open override func encode(to encoder: Encoder) throws {
- var container = encoder.container(keyedBy: CodingKeys.self)
- if modeMovingActivityType != nil { try container.encode(modeMovingActivityType, forKey: .activityType) }
- try super.encode(to: encoder)
- }
-
- // MARK: - CustomStringConvertible
-
- public var description: String {
- let itemType = isDataGap ? "datagap" : isNolo ? "nolo" : "path"
- return String(format: "%@ %@", keepnessString, itemType)
- }
-
-}
-
diff --git a/LocoKit/Timelines/TimelineObjects/PersistentSample.swift b/LocoKit/Timelines/TimelineObjects/PersistentSample.swift
deleted file mode 100644
index a0248d9e..00000000
--- a/LocoKit/Timelines/TimelineObjects/PersistentSample.swift
+++ /dev/null
@@ -1,202 +0,0 @@
-//
-// PersistentSample.swift
-// LocoKit
-//
-// Created by Matt Greenfield on 9/01/18.
-// Copyright © 2018 Big Paua. All rights reserved.
-//
-
-import GRDB
-import CoreLocation
-
-open class PersistentSample: LocomotionSample, TimelineObject {
-
- // MARK: - TimelineObject
-
- public var objectId: UUID { return sampleId }
- public weak var store: TimelineStore? { didSet { if store != nil { store?.add(self) } } }
- public var source: String = "LocoKit"
-
- private var _invalidated = false
- public var invalidated: Bool { return _invalidated }
- public func invalidate() { _invalidated = true }
-
- internal override var _classifiedType: ActivityTypeName? {
- didSet { if oldValue != _classifiedType { hasChanges = true; save() } }
- }
-
- public override var confirmedType: ActivityTypeName? {
- didSet {
- if oldValue != confirmedType {
- hasChanges = true
- nextSample?.previousSampleConfirmedType = confirmedType
- save()
- }
- }
- }
-
- public override var previousSampleConfirmedType: ActivityTypeName? {
- didSet { if oldValue != previousSampleConfirmedType { hasChanges = true; save() } }
- }
-
- public override var hasUsableCoordinate: Bool {
- if confirmedType == .bogus { return false }
- return super.hasUsableCoordinate
- }
-
- // MARK: - Convenience initialisers
-
- public convenience init(from dict: [String: Any?], in store: TimelineStore) {
- self.init(from: dict)
- self.store = store
- store.add(self)
- }
-
- public convenience init(from sample: ActivityBrainSample, in store: TimelineStore) {
- self.init(from: sample)
- self.store = store
- store.add(self)
- }
-
- public convenience init(date: Date, location: CLLocation? = nil, movingState: MovingState = .uncertain,
- recordingState: RecordingState, in store: TimelineStore) {
- self.init(date: date, location: location, movingState: movingState, recordingState: recordingState)
- self.store = store
- store.add(self)
- }
-
- // MARK: - Required initialisers
-
- public required init(from dict: [String: Any?]) {
- self.lastSaved = dict["lastSaved"] as? Date
- if let uuidString = dict["timelineItemId"] as? String { self.timelineItemId = UUID(uuidString: uuidString)! }
- if let source = dict["source"] as? String, !source.isEmpty { self.source = source }
-
- super.init(from: dict)
- }
-
- public required init(from sample: ActivityBrainSample) { super.init(from: sample) }
-
- public required init(date: Date, location: CLLocation? = nil, movingState: MovingState = .uncertain,
- recordingState: RecordingState) {
- super.init(date: date, location: location, movingState: movingState, recordingState: recordingState)
- }
-
- // MARK: - Decodable
-
- public required init(from decoder: Decoder) throws {
- let container = try decoder.container(keyedBy: PersistentCodingKeys.self)
- self.timelineItemId = try? container.decode(UUID.self, forKey: .timelineItemId)
- self.lastSaved = try? container.decode(Date.self, forKey: .lastSaved)
- if let deleted = try? container.decode(Bool.self, forKey: .deleted) { self.deleted = deleted }
- try super.init(from: decoder)
- }
-
- open override func encode(to encoder: Encoder) throws {
- var container = encoder.container(keyedBy: PersistentCodingKeys.self)
- if timelineItemId != nil { try container.encode(timelineItemId, forKey: .timelineItemId) }
- try container.encode(lastSaved, forKey: .lastSaved)
- if deleted { try container.encode(deleted, forKey: .deleted) }
- try super.encode(to: encoder)
- }
-
- private enum PersistentCodingKeys: String, CodingKey {
- case timelineItemId
- case lastSaved
- case deleted
- }
-
- // MARK: - Relationships
-
- private weak var _timelineItem: TimelineItem?
-
- public var timelineItemId: UUID? {
- didSet {
- if oldValue != timelineItemId {
- hasChanges = true
- save()
- }
- }
- }
-
- /// The sample's parent `TimelineItem`.
- public var timelineItem: TimelineItem? {
- get {
- if let cached = self._timelineItem, cached.itemId == self.timelineItemId { return cached }
- if let itemId = self.timelineItemId, let item = store?.item(for: itemId) { self._timelineItem = item }
- return self._timelineItem
- }
- set(newValue) {
- let oldValue = self.timelineItem
-
- // no change? do nothing
- if newValue == oldValue { return }
-
- // disconnect the old relationship
- oldValue?.remove(self)
-
- // store the new value
- self._timelineItem = newValue
- self.timelineItemId = newValue?.itemId
-
- // complete the other side of the new relationship
- newValue?.add(self)
- }
- }
-
- private weak var _nextSample: PersistentSample?
- public var nextSample: PersistentSample? {
- if let cached = _nextSample { return cached }
- _nextSample = store?.sample(where: "date > ? ORDER BY date", arguments: [self.date])
- return _nextSample
- }
-
- public private(set) var deleted = false
- open func delete() {
- deleted = true
- hasChanges = true
- timelineItem?.remove(self)
- save()
- }
-
- // MARK: - PersistableRecord
-
- public static let databaseTableName = "LocomotionSample"
-
- open func encode(to container: inout PersistenceContainer) {
- container["sampleId"] = sampleId.uuidString
- container["source"] = source
- container["date"] = date
- container["secondsFromGMT"] = secondsFromGMT
- container["deleted"] = deleted
- container["lastSaved"] = transactionDate ?? lastSaved ?? Date()
- container["movingState"] = movingState.rawValue
- container["recordingState"] = recordingState.rawValue
- container["timelineItemId"] = timelineItemId?.uuidString
- container["stepHz"] = stepHz
- container["courseVariance"] = courseVariance
- container["xyAcceleration"] = xyAcceleration
- container["zAcceleration"] = zAcceleration
- container["coreMotionActivityType"] = coreMotionActivityType?.rawValue
- container["confirmedType"] = confirmedType?.rawValue
- container["classifiedType"] = _classifiedType?.rawValue
- container["previousSampleConfirmedType"] = previousSampleConfirmedType?.rawValue
-
- // location
- container["latitude"] = location?.coordinate.latitude
- container["longitude"] = location?.coordinate.longitude
- container["altitude"] = location?.altitude
- container["horizontalAccuracy"] = location?.horizontalAccuracy
- container["verticalAccuracy"] = location?.verticalAccuracy
- container["speed"] = location?.speed
- container["course"] = location?.course
- }
-
- // MARK: - PersistentObject
-
- public var transactionDate: Date?
- public var lastSaved: Date?
- public var hasChanges: Bool = false
-
-}
-
diff --git a/LocoKit/Timelines/TimelineObjects/RowCopy.swift b/LocoKit/Timelines/TimelineObjects/RowCopy.swift
deleted file mode 100644
index a3986d34..00000000
--- a/LocoKit/Timelines/TimelineObjects/RowCopy.swift
+++ /dev/null
@@ -1,18 +0,0 @@
-//
-// RowCopy.swift
-// LocoKit
-//
-// Created by Matt Greenfield on 1/05/18.
-//
-
-import GRDB
-
-internal class RowCopy: FetchableRecord {
-
- internal let row: Row
-
- required init(row: Row) {
- self.row = row.copy()
- }
-
-}
diff --git a/LocoKit/Timelines/TimelineObjects/TimelineItem.swift b/LocoKit/Timelines/TimelineObjects/TimelineItem.swift
deleted file mode 100644
index b213c329..00000000
--- a/LocoKit/Timelines/TimelineObjects/TimelineItem.swift
+++ /dev/null
@@ -1,881 +0,0 @@
-//
-// TimelineItem.swift
-// LocoKit
-//
-// Created by Matt Greenfield on 2/12/17.
-// Copyright © 2017 Big Paua. All rights reserved.
-//
-
-import os.log
-import GRDB
-import CoreLocation
-import CoreMotion
-import Combine
-
-/// The abstract base class for timeline items.
-open class TimelineItem: TimelineObject, Hashable, Comparable, Codable, Identifiable, ObservableObject {
-
- // MARK: - Identifiable
-
- public var id: UUID { return objectId }
-
- // MARK: - TimelineObject
-
- public var objectId: UUID { return itemId }
- public weak var store: TimelineStore? { didSet { if store != nil { store?.add(self) } } }
- public var transactionDate: Date?
- public var lastSaved: Date?
- public var hasChanges: Bool = false
-
- public var classifier: MLCompositeClassifier? { return store?.recorder?.classifier }
-
- public var mutex = PThreadMutex(type: .recursive)
-
- public let itemId: UUID
-
- public var source: String = "LocoKit"
-
- private var _invalidated = false
- public var invalidated: Bool { return _invalidated }
- public func invalidate() { _invalidated = true }
-
- public var isVisit: Bool {
- return self is Visit
- }
-
- open var isMergeLocked: Bool {
- if isCurrentItem && !isWorthKeeping { return true }
- if invalidated { return true }
- return false
- }
-
- public var hasBrokenEdges: Bool {
- return hasBrokenPreviousItemEdge || hasBrokenNextItemEdge
- }
-
- public var hasBrokenPreviousItemEdge: Bool {
- if deleted { return false }
- if previousItem == nil { return true }
- return false
- }
-
- public var hasBrokenNextItemEdge: Bool {
- if deleted { return false }
- if nextItem == nil && !isCurrentItem { return true }
- return false
- }
-
- public private(set) var deleted = false
- open func delete() {
- if isMergeLocked {
- os_log(.debug, "Can't delete (TimelineItem.isMergeLocked).")
- return
- }
- guard samples.isEmpty else {
- os_log(.debug, "Can't delete an item that has samples. Assign the samples to another item first.")
- return
- }
- deleted = true
- previousItem = nil
- nextItem = nil
- hasChanges = true
- save()
- }
-
- private var updatingPedometerData = false
- private var pedometerDataIsStale = false
-
- public private(set) var _stepCount: Int?
- open var stepCount: Int? {
- get {
- if CMPedometer.isStepCountingAvailable() && (_stepCount == nil || pedometerDataIsStale) {
- updatePedometerData()
- }
- return _stepCount
- }
- set(newValue) { _stepCount = newValue }
- }
-
- public private(set) var _floorsAscended: Int?
- public var floorsAscended: Int? {
- if CMPedometer.isFloorCountingAvailable() && (_floorsAscended == nil || pedometerDataIsStale) {
- updatePedometerData()
- }
- return _floorsAscended
- }
-
- public private(set) var _floorsDescended: Int?
- public var floorsDescended: Int? {
- if CMPedometer.isFloorCountingAvailable() && (_floorsDescended == nil || pedometerDataIsStale) {
- updatePedometerData()
- }
- return _floorsDescended
- }
-
- open var title: String {
- fatalError()
- }
-
- // MARK: - Relationships
-
- public var includeSamplesWhenEncoding = true
-
- private var _samples: [PersistentSample]?
- open var samples: [PersistentSample] {
- return mutex.sync {
- if let existing = _samples { return existing }
- if lastSaved == nil {
- _samples = []
- } else if let store = store {
- _samples = store.samples(where: "timelineItemId = ? AND deleted = 0 ORDER BY date",
- arguments: [itemId.uuidString])
- } else {
- _samples = []
- }
- return _samples!
- }
- }
-
- public var previousItemId: UUID? {
- didSet {
- if previousItemId == itemId { fatalError("Can't link to self") }
- if previousItemId != nil, previousItemId == nextItemId {
- fatalError("Can't set previousItem and nextItem to the same item")
- }
- if oldValue != previousItemId { hasChanges = true; save() }
- }
- }
- public var nextItemId: UUID? {
- didSet {
- if nextItemId == itemId { fatalError("Can't link to self") }
- if nextItemId != nil, previousItemId == nextItemId {
- fatalError("Can't set previousItem and nextItem to the same item")
- }
- if oldValue != nextItemId { hasChanges = true; save() }
- }
- }
-
- private weak var _previousItem: TimelineItem?
- public var previousItem: TimelineItem? {
- get {
- if let cached = _previousItem, cached.itemId == previousItemId, !cached.deleted, cached != self { return cached }
- if let itemId = previousItemId, let item = store?.item(for: itemId), !item.deleted, item != self {
- _previousItem = item
- return item
- }
- return nil
- }
- set(newValue) {
- if newValue == self { os_log("Can't link to self", type: .error); return }
- if newValue?.deleted == true { os_log("Can't link to a deleted item", type: .error); return }
- if newValue != nil, newValue?.itemId == nextItemId { os_log("Can't set previousItem and nextItem to the same item", type: .error); return }
- mutex.sync {
- let oldValue = self.previousItem
-
- // no change? do nothing
- if newValue == oldValue { return }
-
- // store the new value
- self._previousItem = newValue
- self.previousItemId = newValue?.itemId
-
- // disconnect the old relationship
- if oldValue?.nextItemId == self.itemId {
- oldValue?.nextItemId = nil
- }
-
- // complete the other side of the new relationship
- if newValue?.nextItemId != self.itemId {
- newValue?.nextItemId = self.itemId
- }
- }
- }
- }
-
- private weak var _nextItem: TimelineItem?
- public var nextItem: TimelineItem? {
- get {
- if let cached = _nextItem, cached.itemId == nextItemId, !cached.deleted, cached != self { return cached }
- if let itemId = nextItemId, let item = store?.item(for: itemId), !item.deleted, item != self {
- _nextItem = item
- return item
- }
- return nil
- }
- set(newValue) {
- if newValue == self { os_log("Can't link to self", type: .error); return }
- if newValue?.deleted == true { os_log("Can't link to a deleted item", type: .error); return }
- if newValue != nil, newValue?.itemId == previousItemId { os_log("Can't set previousItem and nextItem to the same item", type: .error); return }
- mutex.sync {
- let oldValue = self.nextItem
-
- // no change? do nothing
- if newValue == oldValue { return }
-
- // store the new value
- self._nextItem = newValue
- self.nextItemId = newValue?.itemId
-
- // disconnect the old relationship
- if oldValue?.previousItemId == self.itemId {
- oldValue?.previousItemId = nil
- }
-
- // complete the other side of the new relationship
- if newValue?.previousItemId != self.itemId {
- newValue?.previousItemId = self.itemId
- }
- }
- }
- }
-
- public var isCurrentItem: Bool { return store?.recorder?.currentItem == self }
-
- // MARK: - Dates, times, durations
-
- private(set) public var _dateRange: DateInterval?
- public var dateRange: DateInterval? {
- if let cached = _dateRange { return cached }
- guard let start = samples.first?.date else { return nil }
- if let nextItemStart = nextItem?.startDate, nextItemStart > start {
- _dateRange = DateInterval(start: start, end: nextItemStart)
- } else if let end = samples.last?.date {
- _dateRange = DateInterval(start: start, end: end)
- }
- return _dateRange
- }
-
- public var startDate: Date? { return dateRange?.start }
- public var endDate: Date? { return dateRange?.end }
- public var duration: TimeInterval { return dateRange?.duration ?? 0 }
-
- public var startTimeZone: TimeZone? {
- return samples.first?.localTimeZone
- }
-
- public var endTimeZone: TimeZone? {
- return samples.last?.localTimeZone
- }
-
- // MARK: - Item validity
-
- public var isInvalid: Bool { return !isValid }
-
- open var isValid: Bool { fatalError("Shouldn't be here.") }
-
- open var isWorthKeeping: Bool { fatalError("Shouldn't be here.") }
-
- public var keepnessScore: Int {
- if isWorthKeeping { return 2 }
- if isValid { return 1 }
- return 0
- }
-
- public var keepnessString: String {
- if isWorthKeeping { return "keeper" }
- if isValid { return "valid" }
- return "invalid"
- }
-
- public var isDataGap: Bool {
- if self is Visit { return false }
- if samples.isEmpty { return false }
- for sample in samples {
- if sample.recordingState != .off { return false }
- }
- return true
- }
-
- private var _isNolo: Bool?
- public var isNolo: Bool {
- if isDataGap { return false }
- if let nolo = _isNolo { return nolo }
- _isNolo = !samples.haveAnyUsableLocations
- return _isNolo!
- }
-
- // ~50% of samples
- public var radius0sd: Double {
- return radius.with0sd.clamped(min: Visit.minimumRadius, max: Visit.maximumRadius)
- }
-
- // ~84% of samples
- public var radius1sd: Double {
- return radius.with1sd.clamped(min: Visit.minimumRadius, max: Visit.maximumRadius)
- }
-
- // ~98% of samples
- public var radius2sd: Double {
- return radius.with2sd.clamped(min: Visit.minimumRadius, max: Visit.maximumRadius)
- }
-
- // ~100% of samples
- public var radius3sd: Double {
- return radius.with3sd.clamped(min: Visit.minimumRadius, max: Visit.maximumRadius)
- }
-
- /// The timeline item's samples grouped up into ItemSegments by sequentially matching activityType and recordingState.
- private var _segments: [ItemSegment]? = nil
- public var segments: [ItemSegment] {
- if let segments = _segments { return segments }
-
- var segments: [ItemSegment] = []
- var current: ItemSegment?
- for sample in samples {
-
- // first segment?
- if current == nil {
- current = ItemSegment(samples: [sample], timelineItem: self)
- segments.append(current!)
- continue
- }
-
- // can add it to the current segment?
- if current?.canAdd(sample) == true {
- current?.add(sample)
- continue
- }
-
- /** NEED A NEW SEGMENT **/
-
- // use the sample to finalise the current segment before starting the next
- current?.endSample = sample
-
- // create the next segment
- current = ItemSegment(samples: [sample], timelineItem: self)
- segments.append(current!)
- }
-
- _segments = segments
- return segments
- }
-
- /// The timeline item's samples grouped up into ItemSegments by sequentially matching activityType.
- private var _segmentsByActivityType: [ItemSegment]? = nil
- public var segmentsByActivityType: [ItemSegment] {
- if let segments = _segmentsByActivityType { return segments }
-
- var segments: [ItemSegment] = []
- var current: ItemSegment?
- for sample in samples {
-
- // first segment?
- if current == nil {
- current = ItemSegment(samples: [sample], timelineItem: self)
- segments.append(current!)
- continue
- }
-
- // can add it to the current segment?
- if current?.canAdd(sample, ignoreRecordingState: true) == true {
- current?.add(sample)
- continue
- }
-
- /** NEED A NEW SEGMENT **/
-
- // use the sample to finalise the current segment before starting the next
- current?.endSample = sample
-
- // create the next segment
- current = ItemSegment(samples: [sample], timelineItem: self)
- segments.append(current!)
- }
-
- _segmentsByActivityType = segments
- return segments
- }
-
- // MARK: - Activity Types
-
- open var activityType: ActivityTypeName? {
- if self is Visit { return .stationary }
-
- // if cached classifier results available, use classified moving type
- if _classifierResults != nil, let activityType = movingActivityType { return activityType }
-
- // if cached classifier result not available, use mode moving type
- if let activityType = modeMovingActivityType { return activityType }
-
- // if there's no mode moving type, fall back to mode type (most likely stationary)
- if let activityType = modeActivityType { return activityType }
-
- return nil
- }
-
- public private(set) var _classifierResults: ClassifierResults? = nil
-
- /// The `ActivityTypeClassifier` results for the timeline item.
- public var classifierResults: ClassifierResults? {
- if let cached = _classifierResults { return cached }
-
- guard let results = classifier?.classify(self, timeout: 30) else { return nil }
-
- // don't cache if it's incomplete
- if results.moreComing { return results }
-
- _classifierResults = results
- return results
- }
-
- public private(set) var _movingActivityType: ActivityTypeName? = nil
-
- public var movingActivityType: ActivityTypeName? {
- if let cached = _movingActivityType { return cached }
- guard let results = classifierResults else { return nil }
- guard let first = results.first(where: { $0.name != .stationary }) else { return nil }
- guard first.score > 0 else { return nil }
- if !results.moreComing {
- _movingActivityType = first.name
- }
- return first.name
- }
-
- public private(set) var _modeActivityType: ActivityTypeName? = nil
-
- /// The most common activity type for the timeline item's samples.
- public var modeActivityType: ActivityTypeName? {
- if let cached = _modeActivityType { return cached }
-
- let sampleTypes = samples.compactMap { $0.activityType }
- if sampleTypes.isEmpty { return nil }
-
- let counted = NSCountedSet(array: sampleTypes)
- let modeType = counted.max { counted.count(for: $0) < counted.count(for: $1) }
-
- _modeActivityType = modeType as? ActivityTypeName
- return _modeActivityType
- }
-
- public private(set) var _modeMovingActivityType: ActivityTypeName? = nil
-
- /// The most common moving activity type for the timeline item's samples.
- public var modeMovingActivityType: ActivityTypeName? {
- if let modeType = _modeMovingActivityType { return modeType }
-
- let sampleTypes = samples.compactMap { $0.activityType != .stationary ? $0.activityType : nil }
- if sampleTypes.isEmpty { return nil }
-
- let counted = NSCountedSet(array: sampleTypes)
- let modeType = counted.max { counted.count(for: $0) < counted.count(for: $1) }
-
- _modeMovingActivityType = modeType as? ActivityTypeName
- return _modeMovingActivityType
- }
-
- // MARK: - Comparisons and Helpers
-
- /**
- The time interval between this item and the given item.
-
- - Note: A negative value indicates overlapping items, and thus the duration of their overlap.
- */
- public func timeInterval(from otherItem: TimelineItem) -> TimeInterval? {
- guard let myRange = self.dateRange else { return nil }
- guard let theirRange = otherItem.dateRange else { return nil }
-
- // items overlap?
- if let intersection = myRange.intersection(with: theirRange) { return -intersection.duration }
-
- if myRange.end <= theirRange.start { return theirRange.start.timeIntervalSince(myRange.end) }
- if myRange.start >= theirRange.end { return myRange.start.timeIntervalSince(theirRange.end) }
-
- return nil
- }
-
- internal func edgeSample(with otherItem: TimelineItem) -> PersistentSample? {
- if otherItem == previousItem {
- return samples.first
- }
- if otherItem == nextItem {
- return samples.last
- }
- return nil
- }
-
- internal func secondToEdgeSample(with otherItem: TimelineItem) -> PersistentSample? {
- if otherItem == previousItem { return samples.second }
- if otherItem == nextItem { return samples.secondToLast }
- return nil
- }
-
- open func withinMergeableDistance(from otherItem: TimelineItem) -> Bool {
- if self.isNolo || otherItem.isNolo { return true }
- if let gap = distance(from: otherItem), gap <= maximumMergeableDistance(from: otherItem) { return true }
-
- // if the items overlap in time, any physical distance is acceptable
- guard let timeGap = self.timeInterval(from: otherItem), timeGap < 0 else { return true }
-
- return false
- }
-
- public func contains(_ location: CLLocation, sd: Double) -> Bool {
- fatalError("Shouldn't be here.")
- }
-
- public func distance(from: TimelineItem) -> CLLocationDistance? {
- fatalError("Shouldn't be here.")
- }
-
- public func maximumMergeableDistance(from: TimelineItem) -> CLLocationDistance {
- fatalError("Shouldn't be here.")
- }
-
- @discardableResult
- internal func sanitiseEdges(excluding: Set = []) -> Set {
- var allMoved: Set = []
- let maximumEdgeSteals = 30
-
- while allMoved.count < maximumEdgeSteals {
- var movedThisLoop: Set = []
-
- if let previousPath = self.previousItem as? Path {
- if let moved = self.cleanseEdge(with: previousPath, excluding: excluding.union(allMoved)) {
- movedThisLoop.insert(moved)
- }
- }
- if let nextPath = self.nextItem as? Path {
- if let moved = self.cleanseEdge(with: nextPath, excluding: excluding.union(allMoved)) {
- movedThisLoop.insert(moved)
- }
- }
-
- // no changes, so we're done
- if movedThisLoop.isEmpty { break }
-
- // break from an infinite loop
- guard movedThisLoop.intersection(allMoved).isEmpty else { break }
-
- // keep track of changes
- allMoved.formUnion(movedThisLoop)
- }
-
- return allMoved
- }
-
- internal func cleanseEdge(with path: Path, excluding: Set) -> LocomotionSample? {
- fatalError("Shouldn't be here.")
- }
-
- open func scoreForConsuming(item: TimelineItem) -> ConsumptionScore {
- return MergeScores.consumptionScoreFor(self, toConsume: item)
- }
-
- /**
- For subclasses to perform additional actions when merging items, for example copying and preserving
- subclass properties.
- */
- open func willConsume(item: TimelineItem) {}
-
- // MARK: - Modifying the timeline item
-
- public func add(_ sample: PersistentSample) { add([sample]) }
-
- public func remove(_ sample: PersistentSample) { remove([sample]) }
-
- open func add(_ samples: [PersistentSample]) {
- var madeChanges = false
- mutex.sync {
- _samples = Set(self.samples + samples).sorted { $0.date < $1.date }
- for sample in samples where sample.timelineItem != self || sample.timelineItemId != self.itemId {
- sample.timelineItem = self
- madeChanges = true
- }
- }
- if madeChanges { samplesChanged() }
- }
-
- open func remove(_ samples: [PersistentSample]) {
- var madeChanges = false
- mutex.sync {
- _samples?.removeObjects(samples)
- for sample in samples where sample.timelineItemId == self.itemId {
- sample.timelineItemId = nil
- madeChanges = true
- }
- }
- if madeChanges { samplesChanged() }
- }
-
- public func breakEdges() {
- previousItemId = nil
- nextItemId = nil
- }
-
- open func sampleTypesChanged() {
- _segments = nil
- _segmentsByActivityType = nil
- _classifierResults = nil
- _movingActivityType = nil
- _modeMovingActivityType = nil
- _modeActivityType = nil
- }
-
- open func samplesChanged() {
- sampleTypesChanged()
-
- _isNolo = nil
- _center = nil
- _radius = nil
- _altitude = nil
-
- let oldDateRange = dateRange
- _dateRange = nil
-
- if oldDateRange != dateRange { pedometerDataIsStale = true }
-
- hasChanges = true
- save()
-
- onMain {
- NotificationCenter.default.post(Notification(name: .updatedTimelineItem, object: self, userInfo: nil))
- self.objectWillChange.send()
- }
- }
-
- public private(set) var _center: CLLocation?
- public var center: CLLocation? {
- if let cached = _center { return cached }
- _center = samples.weightedCenter
- return _center
- }
-
- public private(set) var _radius: Radius?
- public var radius: Radius {
- if let cached = _radius { return cached }
- if let center = center { _radius = samples.radius(from: center) }
- else { _radius = Radius.zero }
- return _radius!
- }
-
- public private(set) var _altitude: CLLocationDistance?
- public var altitude: CLLocationDistance? {
- if let cached = _altitude { return cached }
- _altitude = samples.weightedMeanAltitude
- return _altitude
- }
-
- public func updatePedometerData() {
- let loco = LocomotionManager.highlander
-
- if updatingPedometerData { return }
- guard loco.recordPedometerEvents && loco.haveCoreMotionPermission else { return }
- guard let dateRange = dateRange, dateRange.duration > 0 else { return }
-
- // iOS doesn't keep pedometer data older than one week
- guard dateRange.start.age < 60 * 60 * 24 * 7 else { return }
-
- pedometerDataIsStale = false
- updatingPedometerData = true
-
- loco.pedometer.queryPedometerData(from: dateRange.start, to: dateRange.end) { data, error in
- self.updatingPedometerData = false
-
- guard let data = data else { return }
-
- var madeChanges = false
- if self._stepCount != data.numberOfSteps.intValue {
- self._stepCount = data.numberOfSteps.intValue
- madeChanges = true
- }
- if let floorsAscended = data.floorsAscended?.intValue, self._floorsAscended != floorsAscended {
- self._floorsAscended = floorsAscended
- madeChanges = true
- }
- if let floorsDescended = data.floorsDescended?.intValue, self._floorsDescended != floorsDescended {
- self._floorsDescended = floorsDescended
- madeChanges = true
- }
-
- if madeChanges {
- onMain {
- NotificationCenter.default.post(Notification(name: .updatedTimelineItem, object: self, userInfo: nil))
- self.objectWillChange.send()
- }
- }
- }
- }
-
- // MARK: - Item metadata copying
-
- open func copyMetadata(from otherItem: TimelineItem) {}
-
- // MARK: - PersistableRecord
-
- public static let databaseTableName = "TimelineItem"
-
- open func encode(to container: inout PersistenceContainer) {
- container["itemId"] = itemId.uuidString
- container["lastSaved"] = transactionDate ?? lastSaved ?? Date()
- container["deleted"] = deleted
- container["source"] = source
- let range = _dateRange ?? dateRange
- container["startDate"] = range?.start
- container["endDate"] = range?.end
- if deleted {
- container["previousItemId"] = nil
- container["nextItemId"] = nil
- } else {
- container["previousItemId"] = previousItemId?.uuidString
- container["nextItemId"] = nextItemId?.uuidString
- }
- container["radiusMean"] = _radius?.mean
- container["radiusSD"] = _radius?.sd
- container["altitude"] = _altitude
- container["stepCount"] = stepCount
- container["floorsAscended"] = floorsAscended
- container["floorsDescended"] = floorsDescended
- container["latitude"] = _center?.coordinate.latitude
- container["longitude"] = _center?.coordinate.longitude
- }
-
- // MARK: - Hashable, Comparable
-
- public func hash(into hasher: inout Hasher) {
- hasher.combine(itemId)
- }
-
- public static func ==(lhs: TimelineItem, rhs: TimelineItem) -> Bool { return lhs.itemId == rhs.itemId }
-
- public static func <(lhs: TimelineItem, rhs: TimelineItem) -> Bool {
- if let leftEnd = lhs.endDate, let rightEnd = rhs.endDate, leftEnd < rightEnd { return true }
- return false
- }
-
- // MARK: - Required initialisers
-
- public required init(in store: TimelineStore) {
- self.itemId = UUID()
- self.store = store
- store.add(self)
- }
-
- public required init(from dict: [String: Any?], in store: TimelineStore) {
- self.store = store
- if let uuidString = dict["itemId"] as? String {
- self.itemId = UUID(uuidString: uuidString)!
- } else {
- self.itemId = UUID()
- }
- self.lastSaved = dict["lastSaved"] as? Date
- self.deleted = dict["deleted"] as? Bool ?? false
- if let uuidString = dict["previousItemId"] as? String { self.previousItemId = UUID(uuidString: uuidString)! }
- if let uuidString = dict["nextItemId"] as? String { self.nextItemId = UUID(uuidString: uuidString)! }
- if let mean = dict["radiusMean"] as? Double, let sd = dict["radiusSD"] as? Double {
- self._radius = Radius(mean: mean, sd: sd)
- }
- if let start = dict["startDate"] as? Date, let end = dict["endDate"] as? Date, start <= end {
- _dateRange = DateInterval(start: start, end: end)
- }
- self._altitude = dict["altitude"] as? Double
- if let steps = dict["stepCount"] as? Int64 {
- self._stepCount = Int(steps)
- }
- if let floors = dict["floorsAscended"] as? Int64 {
- self._floorsAscended = Int(floors)
- }
- if let floors = dict["floorsDescended"] as? Int64 {
- self._floorsDescended = Int(floors)
- }
- if let center = dict["center"] as? CLLocation {
- self._center = center
- } else if let latitude = dict["latitude"] as? Double, let longitude = dict["longitude"] as? Double {
- self._center = CLLocation(latitude: latitude, longitude: longitude)
- }
- if let rawValue = dict["activityType"] as? String, let activityType = ActivityTypeName(rawValue: rawValue) {
- if self is Path {
- _modeMovingActivityType = activityType
- } else {
- _modeActivityType = activityType
- }
- }
- if let source = dict["source"] as? String, !source.isEmpty {
- self.source = source
- }
- store.add(self)
- }
-
- // MARK: - Codable
-
- public required init(from decoder: Decoder) throws {
- let container = try decoder.container(keyedBy: CodingKeys.self)
-
- self.itemId = (try? container.decode(UUID.self, forKey: .itemId)) ?? UUID()
- self.deleted = (try? container.decode(Bool.self, forKey: .deleted)) ?? false
- self.lastSaved = try? container.decode(Date.self, forKey: .lastSaved)
- self.previousItemId = try? container.decode(UUID.self, forKey: .previousItemId)
- self.nextItemId = try? container.decode(UUID.self, forKey: .nextItemId)
-
- let start = try? container.decode(Date.self, forKey: .startDate)
- let end = try? container.decode(Date.self, forKey: .endDate)
- if let start = start, let end = end, start <= end { self._dateRange = DateInterval(start: start, end: end) }
-
- self._radius = try? container.decode(Radius.self, forKey: .radius)
- self._altitude = try? container.decode(CLLocationDistance.self, forKey: .altitude)
- self._stepCount = try? container.decode(Int.self, forKey: .stepCount)
- self._floorsAscended = try? container.decode(Int.self, forKey: .floorsAscended)
- self._floorsDescended = try? container.decode(Int.self, forKey: .floorsDescended)
-
- if let coordinate = try? container.decode(CLLocationCoordinate2D.self, forKey: .center) {
- self._center = CLLocation(latitude: coordinate.latitude, longitude: coordinate.longitude)
- } else if let codableLocation = try? container.decode(CodableLocation.self, forKey: .center) {
- self._center = CLLocation(from: codableLocation)
- }
- }
-
- open func encode(to encoder: Encoder) throws {
- var container = encoder.container(keyedBy: CodingKeys.self)
-
- try container.encode(itemId, forKey: .itemId)
- try container.encode(self is Visit, forKey: .isVisit)
- if deleted { try container.encode(deleted, forKey: .deleted) }
- if lastSaved != nil { try container.encode(lastSaved, forKey: .lastSaved) }
- if previousItemId != nil { try container.encode(previousItemId, forKey: .previousItemId) }
- if nextItemId != nil { try container.encode(nextItemId, forKey: .nextItemId) }
- if stepCount != nil { try container.encode(stepCount, forKey: .stepCount) }
- if floorsAscended != nil { try container.encode(floorsAscended, forKey: .floorsAscended) }
- if floorsDescended != nil { try container.encode(floorsDescended, forKey: .floorsDescended) }
-
- let range = _dateRange ?? dateRange
- if let range = range {
- try container.encode(range.start, forKey: .startDate)
- try container.encode(range.end, forKey: .endDate)
- }
-
- if includeSamplesWhenEncoding {
- try container.encode(samples, forKey: .samples)
- if altitude != nil { try container.encode(altitude, forKey: .altitude) }
-
- } else {
- if let _altitude = _altitude { try container.encode(_altitude, forKey: .altitude) }
- }
- }
-
- internal enum CodingKeys: String, CodingKey {
- case itemId
- case deleted
- case isVisit
- case previousItemId
- case nextItemId
- case startDate
- case endDate
- case lastSaved
- case center
- case radius
- case altitude
- case stepCount
- case activityType
- case floorsAscended
- case floorsDescended
- case samples
- }
-
- // MARK: - ObservableObject
-
- public let objectWillChange = ObservableObjectPublisher()
-
-}
-
-internal enum DecodeError: Error {
- case runtimeError(String)
-}
diff --git a/LocoKit/Timelines/TimelineObjects/TimelineObject.swift b/LocoKit/Timelines/TimelineObjects/TimelineObject.swift
deleted file mode 100644
index c7454ead..00000000
--- a/LocoKit/Timelines/TimelineObjects/TimelineObject.swift
+++ /dev/null
@@ -1,46 +0,0 @@
-//
-// TimelineObject.swift
-// LocoKit
-//
-// Created by Matt Greenfield on 27/01/18.
-// Copyright © 2018 Big Paua. All rights reserved.
-//
-
-import os.log
-import Foundation
-import GRDB
-
-public protocol TimelineObject: class, Encodable, PersistableRecord {
-
- var objectId: UUID { get }
- var source: String { get set }
- var store: TimelineStore? { get }
-
- var transactionDate: Date? { get set }
- var lastSaved: Date? { get set }
- var unsaved: Bool { get }
- var hasChanges: Bool { get set }
- var needsSave: Bool { get }
-
- func save(immediate: Bool)
- func save(in db: Database) throws
-
- var invalidated: Bool { get }
- func invalidate()
-
-}
-
-public extension TimelineObject {
- var unsaved: Bool { return lastSaved == nil }
- var needsSave: Bool { return unsaved || hasChanges }
- func save(immediate: Bool = false) { store?.save(self, immediate: immediate) }
- func save(in db: Database) throws {
- if invalidated { os_log(.error, "Can't save changes to an invalid object"); return }
- if unsaved { try insert(db) } else if hasChanges { try update(db) }
- hasChanges = false
- }
- static var persistenceConflictPolicy: PersistenceConflictPolicy {
- return PersistenceConflictPolicy(insert: .replace, update: .abort)
- }
-}
-
diff --git a/LocoKit/Timelines/TimelineObjects/TimelineSegment.swift b/LocoKit/Timelines/TimelineObjects/TimelineSegment.swift
deleted file mode 100644
index 33b52d5a..00000000
--- a/LocoKit/Timelines/TimelineObjects/TimelineSegment.swift
+++ /dev/null
@@ -1,293 +0,0 @@
-//
-// TimelineSegment.swift
-// LocoKit
-//
-// Created by Matt Greenfield on 29/04/18.
-//
-
-import os.log
-import Foundation
-import Combine
-import GRDB
-
-public extension NSNotification.Name {
- static let timelineSegmentUpdated = Notification.Name("timelineSegmentUpdated")
-}
-
-public class TimelineSegment: TransactionObserver, Encodable, Hashable, ObservableObject {
-
- // MARK: -
-
- public var debugLogging = false
- public var shouldReprocessOnUpdate = false
- public var shouldUpdateMarkovValues = true
- public var shouldReclassifySamples = true
-
- // MARK: -
-
- public let store: TimelineStore
- public var onUpdate: (() -> Void)?
-
- // MARK: -
-
- private var _timelineItems: [TimelineItem]?
- public var timelineItems: [TimelineItem] {
- if pendingChanges || _timelineItems == nil {
- _timelineItems = store.items(for: query, arguments: arguments)
- pendingChanges = false
- }
- return _timelineItems ?? []
- }
-
- private let query: String
- private let arguments: StatementArguments
- public var dateRange: DateInterval?
-
- // MARK: -
-
- private var updateTimer: Timer?
- private var lastSaveDate: Date?
- private var lastItemCount: Int?
- private var pendingChanges = false {
- willSet(haveChanges) { if haveChanges { onMain { self.objectWillChange.send() } } }
- }
- private var updatingEnabled = true
-
- // MARK: -
-
- public init(where query: String, arguments: StatementArguments? = nil, in store: TimelineStore,
- onUpdate: (() -> Void)? = nil) {
- self.store = store
- self.query = "SELECT * FROM TimelineItem WHERE " + query
- self.arguments = arguments ?? StatementArguments()
- self.onUpdate = onUpdate
- store.pool?.add(transactionObserver: self)
- }
-
- public func startUpdating() {
- if updatingEnabled { return }
- updatingEnabled = true
- needsUpdate()
- }
-
- public func stopUpdating() {
- if !updatingEnabled { return }
- updatingEnabled = false
- _timelineItems = nil
- }
-
- // MARK: - Result updating
-
- private func needsUpdate() {
- onMain {
- guard self.updatingEnabled else { return }
- self.updateTimer?.invalidate()
- self.updateTimer = Timer.scheduledTimer(withTimeInterval: 1, repeats: false) { [weak self] _ in
- self?.update()
- }
- }
- }
-
- private func update() {
- guard updatingEnabled else { return }
- Jobs.addSecondaryJob("TimelineSegment.\(self.hashValue).update", dontDupe: true) {
- guard self.updatingEnabled else { return }
- guard self.hasChanged else { return }
-
- if self.shouldReprocessOnUpdate {
- self.timelineItems.forEach { TimelineProcessor.healEdges(of: $0) }
- }
-
- self.reclassifySamples()
-
- if self.shouldReprocessOnUpdate {
- self.updateMarkovValues()
- self.process()
- }
-
- self.onUpdate?()
-
- NotificationCenter.default.post(Notification(name: .timelineSegmentUpdated, object: self))
- }
- }
-
- private var hasChanged: Bool {
- let items = timelineItems
-
- let freshLastSaveDate = items.compactMap { $0.lastSaved }.max()
- let freshItemCount = items.count
-
- defer {
- lastSaveDate = freshLastSaveDate
- lastItemCount = freshItemCount
- }
-
- if freshItemCount != lastItemCount { return true }
- if freshLastSaveDate != lastSaveDate { return true }
- return false
- }
-
- // Note: this expects samples to be in date ascending order
- private func reclassifySamples() {
- guard shouldReclassifySamples else { return }
-
- guard let classifier = store.recorder?.classifier else { return }
-
- var lastResults: ClassifierResults?
-
- for item in timelineItems {
- var count = 0
-
- for sample in item.samples where sample.confirmedType == nil {
-
- // don't reclassify samples if they've been done within the past few months
- if sample._classifiedType != nil, let lastSaved = sample.lastSaved, lastSaved.age < .oneMonth * 6 { continue }
-
- if sample._classifiedType == nil {
- print("Classifying sample: \(sample.date), segment.dateRange: \(dateRange)")
- } else {
- print("Reclassifying sample: \(sample.date), segment.dateRange: \(dateRange)")
- }
-
- let oldClassifiedType = sample._classifiedType
- sample._classifiedType = nil
- sample.classifierResults = classifier.classify(sample, previousResults: lastResults)
- if sample.classifiedType != oldClassifiedType {
- count += 1
- }
-
- lastResults = sample.classifierResults
- }
-
- // item needs rebuild?
- if count > 0 { item.sampleTypesChanged() }
-
- if debugLogging && count > 0 {
- os_log("Reclassified samples: %d", type: .debug, count)
- }
- }
- }
-
- public func updateMarkovValues() {
- guard shouldUpdateMarkovValues else { return }
-
- for item in timelineItems {
- for sample in item.samples where sample.confirmedType != nil {
- sample.nextSample?.previousSampleConfirmedType = sample.confirmedType
- }
- }
- }
-
- private func process() {
-
- // shouldn't do processing if currentItem is in the segment and isn't a keeper
- // (the TimelineRecorder should be the sole authority on processing those cases)
- for item in timelineItems { if item.isCurrentItem && !item.isWorthKeeping { return } }
-
- TimelineProcessor.process(timelineItems)
- }
-
- // MARK: - TransactionObserver
-
- public func observes(eventsOfKind eventKind: DatabaseEventKind) -> Bool {
- guard updatingEnabled else { return false }
- return eventKind.tableName == "TimelineItem"
- }
-
- public func databaseDidChange(with event: DatabaseEvent) {
- pendingChanges = true
-
- // it is pointless to keep on tracking further changes
- stopObservingDatabaseChangesUntilNextTransaction()
- }
-
- public func databaseDidCommit(_ db: Database) {
- guard pendingChanges else { return }
- onMain { [weak self] in
- self?.needsUpdate()
- }
- }
-
- public func databaseDidRollback(_ db: Database) {
- pendingChanges = false
- }
-
- // MARK: - Export helpers
-
- public var filename: String? {
- if dateRange == nil, timelineItems.count == 1 {
- return singleItemFilename
- }
-
- guard let dateRange = dateRange else { return nil }
-
- if (dateRange.start + 1).isSameDayAs(dateRange.end - 1) {
- return dayFilename
- }
-
- if (dateRange.start + 1).isSameMonthAs(dateRange.end - 1) {
- return monthFilename
- }
-
- return yearFilename
- }
-
- public var singleItemFilename: String? {
- guard let firstRange = timelineItems.first?.dateRange else { return nil }
- guard timelineItems.count == 1 else { return nil }
-
- let formatter = DateFormatter()
- formatter.dateFormat = "yyyy-MM-dd HHmm"
- return formatter.string(from: firstRange.start)
- }
-
- public var dayFilename: String? {
- guard let dateRange = dateRange else { return nil }
- let formatter = DateFormatter()
- formatter.dateFormat = "yyyy-MM-dd"
- return formatter.string(from: dateRange.middle)
- }
-
- public var monthFilename: String? {
- guard let dateRange = dateRange else { return nil }
- let formatter = DateFormatter()
- formatter.dateFormat = "yyyy-MM"
- return formatter.string(from: dateRange.middle)
- }
-
- public var yearFilename: String? {
- guard let dateRange = dateRange else { return nil }
- let formatter = DateFormatter()
- formatter.dateFormat = "yyyy"
- return formatter.string(from: dateRange.middle)
- }
-
- // MARK: - ObservableObject
-
- public let objectWillChange = ObservableObjectPublisher()
-
- // MARK: - Encodable
-
- enum CodingKeys: String, CodingKey {
- case timelineItems
- }
-
- public func encode(to encoder: Encoder) throws {
- var container = encoder.container(keyedBy: CodingKeys.self)
- try container.encode(timelineItems, forKey: .timelineItems)
- }
-
- // MARK: - Hashable
-
- public func hash(into hasher: inout Hasher) {
- hasher.combine(query)
- hasher.combine(arguments.description)
- }
-
- // MARK: - Equatable
-
- public static func == (lhs: TimelineSegment, rhs: TimelineSegment) -> Bool {
- return lhs.query == rhs.query && lhs.arguments == rhs.arguments
- }
-
-}
diff --git a/LocoKit/Timelines/TimelineObjects/Visit.swift b/LocoKit/Timelines/TimelineObjects/Visit.swift
deleted file mode 100644
index 6e220e6b..00000000
--- a/LocoKit/Timelines/TimelineObjects/Visit.swift
+++ /dev/null
@@ -1,227 +0,0 @@
-//
-// Visit.swift
-// LocoKit
-//
-// Created by Matt Greenfield on 2/12/17.
-// Copyright © 2017 Big Paua. All rights reserved.
-//
-
-import GRDB
-import CoreLocation
-
-open class Visit: TimelineItem {
-
- public static var minimumKeeperDuration: TimeInterval = 60 * 2
- public static var minimumValidDuration: TimeInterval = 10
-
- public static var minimumRadius: CLLocationDistance = 10
- public static var maximumRadius: CLLocationDistance = 150
-
- // MARK: -
-
- public required init(from dict: [String: Any?], in store: TimelineStore) {
- super.init(from: dict, in: store)
- }
-
- public required init(from decoder: Decoder) throws {
- let container = try decoder.container(keyedBy: CodingKeys.self)
- guard let isVisit = try? container.decode(Bool.self, forKey: .isVisit), isVisit else {
- throw DecodeError.runtimeError("Trying to decode a Path as a Visit")
- }
- try super.init(from: decoder)
- }
-
- public required init(in store: TimelineStore) {
- super.init(in: store)
- }
-
- open override var title: String {
- if isWorthKeeping { return "Visit" }
- return "Brief Stop"
- }
-
- // MARK: - Item validity
-
- open override var isValid: Bool {
- if samples.isEmpty { return false }
- if isNolo { return false }
- if duration < Visit.minimumValidDuration { return false }
- return true
- }
-
- open override var isWorthKeeping: Bool {
- if isInvalid { return false }
- if duration < Visit.minimumKeeperDuration { return false }
- return true
- }
-
- // MARK: - Comparisons and Helpers
-
- /// Whether the given location falls within this visit's radius.
- public override func contains(_ location: CLLocation, sd: Double = 4) -> Bool {
- guard let center = center else { return false }
- let testRadius = radius.withSD(sd).clamped(min: Visit.minimumRadius, max: Visit.maximumRadius)
- return location.distance(from: center) <= testRadius
- }
-
- /// Whether the given visit overlaps this visit.
- public func overlaps(_ otherVisit: Visit) -> Bool {
- guard let separation = distance(from: otherVisit) else {
- return false
- }
- return separation < 0
- }
-
- /// The percentage of the given path's distance, duration, and sample count that is contained inside this visit.
- public func containedPercentOf(_ path: Path) -> Double {
- let insiders = Array(path.samplesInside(self)).sorted { $0.date < $1.date }
-
- let insidersDuration = insiders.duration
- let insidersDistance = insiders.distance
-
- let percentOfPathDuration = path.duration > 0 ? insidersDuration / path.duration : 0
- let percentOfPathDistance = path.distance > 0 ? insidersDistance / path.distance : 0
- let percentOfPathSamples = path.samples.count > 0 ? Double(insiders.count) / Double(path.samples.count) : 0
-
- return [percentOfPathSamples, percentOfPathDuration, percentOfPathDistance].mean
- }
-
- public override func distance(from otherItem: TimelineItem) -> CLLocationDistance? {
- if let path = otherItem as? Path {
- return distance(from: path)
- }
- if let visit = otherItem as? Visit {
- return distance(from: visit)
- }
- return nil
- }
-
- internal func distance(from otherVisit: Visit) -> CLLocationDistance? {
- guard let center = center, let otherCenter = otherVisit.center else {
- return nil
- }
- return center.distance(from: otherCenter) - radius1sd - otherVisit.radius1sd
- }
-
- internal func distance(from path: Path) -> CLLocationDistance? {
- guard let center = center else {
- return nil
- }
- guard let pathEdge = path.edgeSample(with: self)?.location, pathEdge.hasUsableCoordinate else {
- return nil
- }
- return center.distance(from: pathEdge) - radius1sd
- }
-
- public override func maximumMergeableDistance(from otherItem: TimelineItem) -> CLLocationDistance {
- if let path = otherItem as? Path {
- return maximumMergeableDistance(from: path)
- }
- if let visit = otherItem as? Visit {
- return maximumMergeableDistance(from: visit)
- }
- return 0
- }
-
- private func maximumMergeableDistance(from path: Path) -> CLLocationDistance {
-
- // visit-path gaps less than this should be forgiven
- let minimum: CLLocationDistance = 150
-
- guard let timeSeparation = self.timeInterval(from: path) else {
- return 0
- }
- let rawMax = CLLocationDistance(path.mps * timeSeparation * 4)
-
- return max(rawMax, minimum)
- }
-
- private func maximumMergeableDistance(from visit: Visit) -> CLLocationDistance {
- return CLLocationDistanceMax
- }
-
- internal override func cleanseEdge(with path: Path, excluding: Set) -> LocomotionSample? {
- if self.isMergeLocked || path.isMergeLocked { return nil }
- if self.isDataGap || path.isDataGap { return nil }
- if self.deleted || path.deleted { return nil }
-
- // edge cleansing isn't allowed to push a path into invalid state
- guard path.samples.count > Path.minimumValidSamples else { return nil }
-
- // fail out if separation distance is too much
- guard withinMergeableDistance(from: path) else { return nil }
-
- // fail out if separation time is too much
- guard let timeGap = timeInterval(from: path), timeGap < 60 * 10 else { return nil }
-
- /** GET ALL THE REQUIRED VARS **/
-
- guard let visitEdge = self.edgeSample(with: path), visitEdge.hasUsableCoordinate else { return nil }
- guard let visitEdgeNext = self.secondToEdgeSample(with: path), visitEdgeNext.hasUsableCoordinate else { return nil }
- guard let pathEdge = path.edgeSample(with: self), pathEdge.hasUsableCoordinate else { return nil }
- guard let pathEdgeNext = path.secondToEdgeSample(with: self), pathEdgeNext.hasUsableCoordinate else { return nil }
-
- guard let pathEdgeLocation = pathEdge.location else { return nil }
- guard let pathEdgeNextLocation = pathEdgeNext.location else { return nil }
-
- let pathEdgeIsInside = self.contains(pathEdgeLocation, sd: 1)
- let pathEdgeNextIsInside = self.contains(pathEdgeNextLocation, sd: 1)
-
- /** ATTEMPT TO MOVE PATH EDGE TO THE VISIT **/
-
- // path edge is inside and path edge next is inside: move path edge to the visit
- if !excluding.contains(pathEdge), pathEdgeIsInside && pathEdgeNextIsInside {
- self.add(pathEdge)
- return pathEdge
- }
-
- /** ATTEMPT TO MOVE VISIT EDGE TO THE PATH **/
-
- // not allowed to move visit edge if too much duration between edge and edge next
- let edgeNextDuration = abs(visitEdge.date.timeIntervalSince(visitEdgeNext.date))
- if edgeNextDuration > .oneMinute * 2 {
- return nil
- }
-
- // path edge is outside: move visit edge to the path
- if !excluding.contains(visitEdge), !pathEdgeIsInside {
- path.add(visitEdge)
- return visitEdge
- }
-
- return nil
- }
-
- override open func samplesChanged() {
- super.samplesChanged()
- }
-
- // MARK: - PersistantRecord
-
- open override func encode(to container: inout PersistenceContainer) {
- super.encode(to: &container)
- container["isVisit"] = true
- }
-
- // MARK: - Encodable
-
- open override func encode(to encoder: Encoder) throws {
- var container = encoder.container(keyedBy: CodingKeys.self)
- if let center = center { try container.encode(center.coordinate, forKey: .center) }
- if includeSamplesWhenEncoding {
- try container.encode(radius, forKey: .radius)
- } else {
- if let _radius = _radius { try container.encode(_radius, forKey: .radius) }
- }
- try super.encode(to: encoder)
- }
-
-}
-
-extension Visit: CustomStringConvertible {
-
- public var description: String {
- return keepnessString + " visit"
- }
-
-}
diff --git a/LocoKit/Timelines/TimelineProcessor.swift b/LocoKit/Timelines/TimelineProcessor.swift
deleted file mode 100644
index 30a045aa..00000000
--- a/LocoKit/Timelines/TimelineProcessor.swift
+++ /dev/null
@@ -1,654 +0,0 @@
-//
-// TimelineProcessor.swift
-// LocoKit
-//
-// Created by Matt Greenfield on 30/04/18.
-//
-
-import os.log
-import Foundation
-import GRDB
-
-public class TimelineProcessor {
-
- public static var debugLogging = false
- public static var maximumItemsInProcessingLoop = 9
-
- // MARK: - Sequential item processing
-
- public static func itemsToProcess(from fromItem: TimelineItem) -> [TimelineItem] {
- var items: [TimelineItem] = [fromItem]
-
- // collect items before fromItem, up to two keepers
- var keeperCount = 0
- var workingItem = fromItem
- while keeperCount < 2, let previous = workingItem.previousItem {
- items.append(previous)
- if previous.isWorthKeeping { keeperCount += 1 }
- workingItem = previous
- }
-
- // collect items after fromItem, up to two keepers
- keeperCount = 0
- workingItem = fromItem
- while keeperCount < 2, items.count < TimelineProcessor.maximumItemsInProcessingLoop, let next = workingItem.nextItem {
- items.append(next)
- if next.isWorthKeeping { keeperCount += 1 }
- workingItem = next
- }
-
- return items
- }
-
- public static func process(from fromItem: TimelineItem) {
- fromItem.store?.process {
- let items = itemsToProcess(from: fromItem)
-
- // recurse until no remaining possible merges
- process(items) { results in
- if let kept = results?.kept {
- delay(0.3) { process(from: kept) }
- }
- }
- }
- }
-
- private static var lastCleansedSamples: Set = []
-
- public static func process(_ givenItems: [TimelineItem], completion: ((MergeResult?) -> Void)? = nil) {
- guard let store = givenItems.first?.store else { return }
-
- let startDate = givenItems.compactMap({ $0.startDate }).min()
- let endDate = givenItems.compactMap({ $0.startDate }).max()
-
- // sanitise the store in the items date range
- if let start = startDate, let end = endDate {
- store.process {
- sanitise(store: store, inRange: DateInterval(start: start, end: end))
- }
- }
-
- store.process {
- var items = givenItems
-
- // use all timeline items in the range, not just the given ones (might be new ones from sanitise, or local cache might be invalid)
- if let start = startDate, let end = endDate {
- items = store.items(where: "startDate >= ? AND endDate <= ?", arguments: [start, end])
- }
-
- var merges: Set = []
- var itemsToSanitise = Set(items.prefix(10)) // limit to 10 items, to avoid massive processing loops
-
- /** collate all the potential merges **/
-
- for workingItem in items {
-
- // add in the merges for one step forward
- if let next = workingItem.nextItem {
- itemsToSanitise.insert(next)
-
- merges.insert(Merge(keeper: workingItem, deadman: next))
- merges.insert(Merge(keeper: next, deadman: workingItem))
-
- // if next has a lesser keepness, look at doing a merge against next-next
- if !workingItem.isDataGap, next.keepnessScore < workingItem.keepnessScore {
- if let nextNext = next.nextItem, !nextNext.isDataGap, nextNext.keepnessScore > next.keepnessScore {
- itemsToSanitise.insert(nextNext)
-
- merges.insert(Merge(keeper: workingItem, betweener: next, deadman: nextNext))
- merges.insert(Merge(keeper: nextNext, betweener: next, deadman: workingItem))
- }
- }
- }
-
- // add in the merges for one step backward
- if let previous = workingItem.previousItem {
- itemsToSanitise.insert(previous)
-
- merges.insert(Merge(keeper: workingItem, deadman: previous))
- merges.insert(Merge(keeper: previous, deadman: workingItem))
-
- // if previous has a lesser keepness, look at doing a merge against previous-previous
- if !workingItem.isDataGap, previous.keepnessScore < workingItem.keepnessScore {
- if let prevPrev = previous.previousItem, !prevPrev.isDataGap, prevPrev.keepnessScore > previous.keepnessScore {
- itemsToSanitise.insert(prevPrev)
-
- merges.insert(Merge(keeper: workingItem, betweener: previous, deadman: prevPrev))
- merges.insert(Merge(keeper: prevPrev, betweener: previous, deadman: workingItem))
- }
- }
- }
-
- // if keepness scores allow, add in a bridge merge over top of working item
- if let previous = workingItem.previousItem, let next = workingItem.nextItem,
- previous.keepnessScore > workingItem.keepnessScore, next.keepnessScore > workingItem.keepnessScore,
- !previous.isDataGap, !next.isDataGap
- {
- merges.insert(Merge(keeper: previous, betweener: workingItem, deadman: next))
- merges.insert(Merge(keeper: next, betweener: workingItem, deadman: previous))
- }
- }
-
- /** sanitise the edges **/
- var allMoved: Set = []
- itemsToSanitise.forEach {
- let moved = $0.sanitiseEdges(excluding: lastCleansedSamples)
- allMoved.formUnion(moved)
- }
-
- // infinite loop breakers, for the next processing cycle
- lastCleansedSamples = allMoved
-
-
- // check for invalid merges
- for merge in merges {
- if !merge.isValid {
- print("INVALID MERGE. BREAKING EDGES")
- merge.keeper.breakEdges()
- merge.betweener?.breakEdges()
- merge.deadman.breakEdges()
- }
- }
-
- /** sort the merges by highest to lowest score **/
-
- let sortedMerges = merges.sorted { $0.score.rawValue > $1.score.rawValue }
-
- if !sortedMerges.isEmpty {
- var descriptions = ""
- for merge in sortedMerges { descriptions += String(describing: merge) + "\n" }
- if debugLogging { os_log("Considering %d merges:\n%@", type: .debug, merges.count, descriptions) }
- }
-
- /** find the highest scoring valid merge **/
-
- guard let winningMerge = sortedMerges.first, winningMerge.score != .impossible else {
- completion?(nil)
- return
- }
-
- /** do it **/
-
- let results = winningMerge.doIt()
-
- // don't need infinite loop breakers now, because the merge broke the loop
- lastCleansedSamples = []
-
- completion?(results)
- }
- }
-
- // MARK: - Item safe deletion
-
- /**
- Attempt to delete the given timeline item by merging it into an adjacent item.
-
- Calls the completion handler with the timeline item that the deleted item was merged into,
- or nil if a safe delete wasn't possible.
- */
- public static func safeDelete(_ deadman: TimelineItem, completion: ((TimelineItem?) -> Void)? = nil) {
- guard let store = deadman.store else { return }
- store.process {
- deadman.sanitiseEdges()
-
- var merges: Set = []
-
- // merge next and previous
- if let next = deadman.nextItem, let previous = deadman.previousItem {
- merges.insert(Merge(keeper: next, betweener: deadman, deadman: previous))
- merges.insert(Merge(keeper: previous, betweener: deadman, deadman: next))
- }
-
- // merge into previous
- if let previous = deadman.previousItem {
- merges.insert(Merge(keeper: previous, deadman: deadman))
- }
-
- // merge into next
- if let next = deadman.nextItem {
- merges.insert(Merge(keeper: next, deadman: deadman))
- }
-
- let sortedMerges = merges.sorted { $0.score.rawValue > $1.score.rawValue }
-
- var results: (kept: TimelineItem, killed: [TimelineItem])?
-
- defer {
- if let results = results {
- // clean up the leftovers
- self.process(from: results.kept)
- completion?(results.kept)
-
- } else {
- completion?(nil)
- }
- }
-
- // do the highest scoring valid merge
- if let winningMerge = sortedMerges.first, winningMerge.score != .impossible {
- results = winningMerge.doIt()
- return
- }
-
- // fall back to doing an "impossible" (ie completely undesirable) merge
- if let shittyMerge = sortedMerges.first {
- results = shittyMerge.doIt()
- return
- }
- }
- }
-
- // MARK: - ItemSegment brexiting
-
- public static func extractItem(for segment: ItemSegment, in store: TimelineStore, completion: ((TimelineItem?) -> Void)? = nil) {
- store.process {
- guard let segmentRange = segment.dateRange else {
- completion?(nil)
- return
- }
-
- // find the overlapping items
- let overlappers = store.items(
- where: "endDate > :startDate AND startDate < :endDate AND deleted = 0 ORDER BY startDate",
- arguments: ["startDate": segmentRange.start, "endDate": segmentRange.end])
-
- var modifiedItems: [TimelineItem] = []
- var samplesToSteal: Set = Set(segment.samples)
-
- // find existing samples that fall inside the segment's range
- for overlapper in overlappers {
- if overlapper.isMergeLocked {
- os_log("An overlapper is merge locked. Aborting extraction.", type: .debug)
- completion?(nil)
- return
- }
-
- var lostPrevEdge = false, lostNextEdge = false
-
- // find samples inside the segment's range
- for sample in overlapper.samples where segmentRange.contains(sample.date) {
- if sample == overlapper.samples.first { lostPrevEdge = true }
- if sample == overlapper.samples.last { lostNextEdge = true }
- samplesToSteal.insert(sample)
- }
-
- // detach previous edge, if modified
- if lostPrevEdge {
- overlapper.previousItem = nil
- modifiedItems.append(overlapper)
- }
-
- // detach next edge, if modified
- if lostNextEdge {
- overlapper.nextItem = nil
- modifiedItems.append(overlapper)
- }
- }
-
- // create the new item
- let newItem = segment.activityType == .stationary
- ? store.createVisit(from: segment.samples)
- : store.createPath(from: segment.samples)
-
- // add the stolen samples to the new item
- if !samplesToSteal.isEmpty {
- newItem.add(Array(samplesToSteal))
- }
-
- // delete any newly empty items
- for modifiedItem in modifiedItems where modifiedItem.samples.isEmpty {
- modifiedItem.delete()
- }
-
- // if the new item is inside an overlapper, split that overlapper in two
- for overlapper in overlappers where !overlapper.deleted {
- guard let newItemRange = newItem.dateRange else { break }
- guard let overlapperRange = overlapper.dateRange else { continue }
- guard let intersection = overlapperRange.intersection(with: newItemRange) else { continue }
- guard intersection.duration < overlapper.duration else { continue }
-
- os_log("Splitting an overlapping item in two", type: .debug)
-
- // get all samples from overlapper up to the point of overlap
- let samplesToExtract = overlapper.samples.prefix { $0.date < newItemRange.start }
-
- // create a new item from those samples
- let splitItem = overlapper is Path
- ? store.createPath(from: Array(samplesToExtract))
- : store.createVisit(from: Array(samplesToExtract))
- modifiedItems.append(splitItem)
-
- // detach the edge to allow proper reconnect at healing time
- overlapper.previousItem = nil
-
- // copy metadata to the splitter
- splitItem.copyMetadata(from: overlapper)
- }
-
- // attempt to connect up the new item
- healEdges(of: newItem)
-
- // edge heal all modified items, or delete if empty
- for modifiedItem in modifiedItems {
- healEdges(of: modifiedItem)
- }
-
- // extract paths around a new visit, as appropriate
- if let visit = newItem as? Visit {
- extractPathEdgesFor(visit, in: store)
- }
-
- // keep currentItem sane
- store.recorder?.updateCurrentItem()
-
- // complete with the new item
- completion?(newItem)
- }
- }
-
- public static func extractPathEdgesFor(_ visit: Visit, in store: TimelineStore) {
- if visit.deleted || visit.isMergeLocked { return }
-
- if let previousVisit = visit.previousItem as? Visit {
- extractPathBetween(visit: visit, and: previousVisit, in: store)
- }
-
- if let nextVisit = visit.nextItem as? Visit {
- extractPathBetween(visit: visit, and: nextVisit, in: store)
- }
- }
-
- public static func extractPathBetween(visit: Visit, and otherVisit: Visit, in store: TimelineStore) {
- if visit.deleted || visit.isMergeLocked { return }
- if otherVisit.deleted || otherVisit.isMergeLocked { return }
- guard visit.nextItem == otherVisit || visit.previousItem == otherVisit else { return }
-
- let previousVisit = visit.nextItem == otherVisit ? visit : otherVisit
- let nextVisit = visit.nextItem == otherVisit ? otherVisit : visit
-
- var pathSegment: ItemSegment
- if let nextStart = nextVisit.segmentsByActivityType.first, nextStart.activityType != .stationary {
- pathSegment = nextStart
- } else if let previousEnd = previousVisit.segmentsByActivityType.last, previousEnd.activityType != .stationary {
- pathSegment = previousEnd
- } else {
- return
- }
-
- extractItem(for: pathSegment, in: store)
- }
-
- // MARK: - Item edge healing
-
- public static func healEdges(of items: [TimelineItem]) {
- items.forEach { healEdges(of: $0) }
- }
-
- public static func healEdges(of brokenItem: TimelineItem) {
- if brokenItem.isMergeLocked { return }
- if !brokenItem.hasBrokenEdges { return }
- guard let store = brokenItem.store else { return }
-
- store.process {
- self.healPreviousEdge(of: brokenItem)
- self.healNextEdge(of: brokenItem)
-
- // it's wholly contained by another item?
- guard brokenItem.hasBrokenPreviousItemEdge && brokenItem.hasBrokenNextItemEdge else { return }
- guard let dateRange = brokenItem.dateRange else { return }
-
- if let overlapper = store.item(
- where: """
- startDate <= :startDate AND endDate >= :endDate AND startDate IS NOT NULL AND endDate IS NOT NULL
- AND deleted = 0 AND itemId != :itemId
- """,
- arguments: ["startDate": dateRange.start, "endDate": dateRange.end,
- "itemId": brokenItem.itemId.uuidString]),
- !overlapper.deleted && !overlapper.isMergeLocked
- {
- overlapper.add(brokenItem.samples)
- brokenItem.delete()
- return
- }
- }
- }
-
- private static func healNextEdge(of brokenItem: TimelineItem) {
- guard let store = brokenItem.store else { return }
- if brokenItem.isMergeLocked { return }
- guard brokenItem.hasBrokenNextItemEdge else { return }
- guard let endDate = brokenItem.endDate else { return }
-
- if let overlapper = store.item(
- where: """
- startDate < :endDate1 AND endDate > :endDate2 AND startDate IS NOT NULL AND endDate IS NOT NULL
- AND isVisit = :isVisit AND deleted = 0 AND itemId != :itemId
- """,
- arguments: ["endDate1": endDate, "endDate2": endDate, "isVisit": brokenItem is Visit,
- "itemId": brokenItem.itemId.uuidString]),
- !overlapper.deleted && !overlapper.isMergeLocked
- {
- overlapper.add(brokenItem.samples)
- brokenItem.delete()
- return
- }
-
- if let nearest = store.item(
- where: "startDate IS NOT NULL AND deleted = 0 AND itemId != :itemId ORDER BY ABS(strftime('%s', startDate) - :timestamp)",
- arguments: ["endDate": endDate, "itemId": brokenItem.itemId.uuidString,
- "timestamp": endDate.timeIntervalSince1970]),
- !nearest.deleted && !nearest.isMergeLocked
- {
- // nearest is already this item's edge? eh?
- if nearest.previousItemId == brokenItem.itemId { return }
-
- // nearest is already this item's other edge? wtf no
- if brokenItem.previousItemId == nearest.itemId { return }
-
- if let gap = nearest.timeInterval(from: brokenItem) {
-
- // nearest already has an edge connection?
- if let theirEdge = nearest.previousItem {
-
- if let theirGap = nearest.timeInterval(from: theirEdge) {
-
- // broken item's edge is closer than nearest's current edge? steal it
- if abs(gap) < abs(theirGap) {
- brokenItem.nextItem = nearest
- return
- }
- }
-
- } else { // they don't have an edge connection, so it's safe to connect
- brokenItem.nextItem = nearest
- return
- }
- }
- }
- }
-
- private static func healPreviousEdge(of brokenItem: TimelineItem) {
- guard let store = brokenItem.store else { return }
- if brokenItem.isMergeLocked { return }
- guard brokenItem.hasBrokenPreviousItemEdge else { return }
- guard let startDate = brokenItem.startDate else { return }
-
- if let overlapper = store.item(
- where: """
- startDate < :startDate1 AND endDate > :startDate2 AND startDate IS NOT NULL AND endDate IS NOT NULL
- AND isVisit = :isVisit AND deleted = 0 AND itemId != :itemId
- """,
- arguments: ["startDate1": startDate, "startDate2": startDate, "isVisit": brokenItem is Visit,
- "itemId": brokenItem.itemId.uuidString]),
- !overlapper.deleted && !overlapper.isMergeLocked
- {
- overlapper.add(brokenItem.samples)
- brokenItem.delete()
- return
- }
-
- if let nearest = store.item(
- where: "endDate IS NOT NULL AND deleted = 0 AND itemId != :itemId ORDER BY ABS(strftime('%s', endDate) - :timestamp)",
- arguments: ["startDate": startDate, "itemId": brokenItem.itemId.uuidString,
- "timestamp": startDate.timeIntervalSince1970]),
- !nearest.deleted && !nearest.isMergeLocked
- {
- // nearest is already this item's edge? eh?
- if nearest.nextItemId == brokenItem.itemId { return }
-
- // nearest is already this item's other edge? wtf no
- if brokenItem.nextItemId == nearest.itemId { return }
-
- if let gap = nearest.timeInterval(from: brokenItem) {
-
- // nearest already has an edge connection?
- if let theirEdge = nearest.nextItem {
-
- if let theirGap = nearest.timeInterval(from: theirEdge) {
-
- // broken item's edge is closer than nearest's current edge? steal it
- if abs(gap) < abs(theirGap) {
- brokenItem.previousItem = nearest
- return
- }
- }
-
- } else { // they don't have an edge connection, so it's safe to connect
- brokenItem.previousItem = nearest
- return
- }
- }
- }
- }
-
- // MARK: - Data gap insertion
-
- public static func insertDataGapBetween(newer newerItem: TimelineItem, older olderItem: TimelineItem) {
- guard let store = newerItem.store else { return }
- store.process {
- guard !newerItem.isDataGap && !olderItem.isDataGap else { return }
-
- guard let gap = newerItem.timeInterval(from: olderItem), gap > 60 * 5 else { return }
-
- guard let startDate = olderItem.endDate else { return }
- guard let endDate = newerItem.startDate else { return }
-
- // the edge samples
- let startSample = store.createSample(date: startDate, recordingState: .off)
- let endSample = store.createSample(date: endDate, recordingState: .off)
-
- // the gap item
- let gapItem = store.createPath(from: startSample)
- gapItem.previousItem = olderItem
- gapItem.nextItem = newerItem
- gapItem.add(endSample)
- }
- }
-
- // MARK: - Database sanitising
-
- public static func sanitise(store: TimelineStore, inRange dateRange: DateInterval? = nil) {
- orphanSamplesFromDeadParents(in: store, inRange: dateRange)
- adoptOrphanedSamples(in: store, inRange: dateRange)
- detachDeadmenEdges(in: store, inRange: dateRange)
- }
-
- private static func adoptOrphanedSamples(in store: TimelineStore, inRange dateRange: DateInterval? = nil) {
- store.connectToDatabase()
-
- var query = "timelineItemId IS NULL AND deleted = 0"
- var arguments: [DatabaseValueConvertible] = []
- if let dateRange = dateRange {
- query += " AND date >= ? AND date <= ?"
- arguments = [dateRange.start, dateRange.end]
- }
-
- let orphans = store.samples(where: query + " ORDER BY date DESC", arguments: StatementArguments(arguments))
-
- if orphans.isEmpty { return }
-
- os_log("Found orphaned samples: %d", type: .debug, orphans.count)
-
- var newParents: [TimelineItem] = []
-
- for orphan in orphans where orphan.timelineItem == nil {
- if let item = store.item(where: "startDate <= ? AND endDate >= ? AND deleted = 0",
- arguments: [orphan.date, orphan.date]) {
- os_log("ADOPTED AN ORPHAN (item: %@, sample: %@, date: %@)", type: .debug, item.itemId.shortString,
- orphan.sampleId.shortString, String(describing: orphan.date))
- item.add(orphan)
-
- } else { // create a new item for the orphan
- if orphan.movingState == .stationary {
- newParents.append(store.createVisit(from: orphan))
- } else {
- newParents.append(store.createPath(from: orphan))
- }
- os_log("CREATED NEW PARENT FOR ORPHAN (sample: %@, date: %@)", type: .debug,
- orphan.sampleId.shortString, String(describing: orphan.date))
- }
- }
-
- store.save()
-
- if newParents.isEmpty { return }
-
- // clean up the new parents
- newParents.forEach {
- TimelineProcessor.healEdges(of: $0)
- TimelineProcessor.process(from: $0)
- }
- }
-
- private static func orphanSamplesFromDeadParents(in store: TimelineStore, inRange dateRange: DateInterval? = nil) {
- store.connectToDatabase()
-
- var query = """
- SELECT LocomotionSample.* FROM LocomotionSample
- JOIN TimelineItem ON timelineItemId = TimelineItem.itemId
- WHERE TimelineItem.deleted = 1
- """
- var arguments: [DatabaseValueConvertible] = []
- if let dateRange = dateRange {
- query += " AND date >= ? AND date <= ?"
- arguments = [dateRange.start, dateRange.end]
- }
-
- let orphans = store.samples(for: query, arguments: StatementArguments(arguments))
-
- if orphans.isEmpty { return }
-
- os_log("Samples holding onto dead parents: %d", type: .debug, orphans.count)
-
- for orphan in orphans where orphan.timelineItemId != nil {
- orphan.timelineItemId = nil
- }
-
- store.save()
- }
-
- private static func detachDeadmenEdges(in store: TimelineStore, inRange dateRange: DateInterval? = nil) {
- store.connectToDatabase()
-
- var query = "deleted = 1 AND (previousItemId IS NOT NULL OR nextItemId IS NOT NULL)"
- var arguments: [DatabaseValueConvertible] = []
- if let dateRange = dateRange {
- query += " AND startDate >= ? AND endDate <= ?"
- arguments = [dateRange.start, dateRange.end]
- }
-
- let deadmen = store.items(where: query, arguments: StatementArguments(arguments))
-
- if deadmen.isEmpty { return }
-
- os_log("Deadmen to edge detach: %d", type: .debug, deadmen.count)
-
- for deadman in deadmen {
- deadman.previousItemId = nil
- deadman.nextItemId = nil
- }
-
- store.save()
- }
-
-}
diff --git a/LocoKit/Timelines/TimelineRecorder.swift b/LocoKit/Timelines/TimelineRecorder.swift
deleted file mode 100644
index 84ff85ad..00000000
--- a/LocoKit/Timelines/TimelineRecorder.swift
+++ /dev/null
@@ -1,310 +0,0 @@
-//
-// TimelineRecorder.swift
-// LocoKit
-//
-// Created by Matt Greenfield on 29/04/18.
-//
-
-import CoreLocation
-
-public extension NSNotification.Name {
- static let newTimelineItem = Notification.Name("newTimelineItem")
- static let updatedTimelineItem = Notification.Name("updatedTimelineItem")
-}
-
-public class TimelineRecorder {
-
- // MARK: - Settings
-
- /**
- The maximum number of samples to record per minute.
-
- - Note: The actual number of samples recorded per minute may be less than this, depending on data availability.
- */
- public var samplesPerMinute: Double = 10
-
- private(set) public var store: TimelineStore
- private(set) public var classifier: MLCompositeClassifier?
- private(set) public var lastClassifierResults: ClassifierResults?
-
- // MARK: - Recorder creation
-
- public init(store: TimelineStore, classifier: MLCompositeClassifier? = nil) {
- self.store = store
- store.recorder = self
- self.classifier = classifier
-
- let loco = LocomotionManager.highlander
-
- let notes = NotificationCenter.default
- notes.addObserver(forName: .locomotionSampleUpdated, object: nil, queue: nil) { [weak self] _ in
- self?.recordSample()
- }
- notes.addObserver(forName: .wentFromRecordingToSleepMode, object: nil, queue: nil) { [weak self] _ in
- if let currentItem = self?.currentItem {
- TimelineProcessor.process(from: currentItem)
- }
- }
- notes.addObserver(forName: .willStartSleepMode, object: nil, queue: nil) { [weak self] _ in
- self?.recordSample()
- }
- notes.addObserver(forName: .recordingStateChanged, object: nil, queue: nil) { [weak self] _ in
- self?.updateSleepModeAcceptability()
- if loco.recordingState.isCurrentRecorder {
- store.connectToDatabase()
- }
- }
- notes.addObserver(forName: .tookOverRecording, object: nil, queue: nil) { [weak self] _ in
- self?.updateCurrentItem()
- loco.resetLocationFilter() // reset the Kalmans
- }
-
- // keep currentItem sane after merges
- notes.addObserver(forName: .mergedTimelineItems, object: nil, queue: nil) { [weak self] note in
- guard let results = note.userInfo?["results"] as? MergeResult else { return }
- guard let current = self?.currentItem else { return }
- if results.killed.contains(current) {
- self?.currentItem = results.kept
- }
- }
- }
-
- // convenience access to an often used optional bool
- public func canClassify(_ coordinate: CLLocationCoordinate2D? = nil) -> Bool {
- return classifier?.canClassify(coordinate) == true
- }
-
- // MARK: - Starting and stopping recording
-
- public func startRecording() {
- if isRecording { return }
- store.connectToDatabase()
- if LocomotionManager.highlander.appGroup?.currentRecorder == nil {
- addDataGapItem()
- }
- LocomotionManager.highlander.startRecording()
- }
-
- public func stopRecording() {
- LocomotionManager.highlander.stopRecording()
- }
-
- public var isRecording: Bool {
- return LocomotionManager.highlander.recordingState != .off
- }
-
- // MARK: - Startup
-
- private func addDataGapItem() {
- guard let lastItem = currentItem, let lastEndDate = lastItem.endDate else { return }
-
- // don't add a data gap after a data gap
- if lastItem.isDataGap { return }
-
- // is the gap too short to be worth filling?
- if lastEndDate.age < LocomotionManager.highlander.sleepCycleDuration { return }
-
- // the edge samples
- let startSample = store.createSample(date: lastEndDate, recordingState: .off)
- let endSample = store.createSample(date: Date(), recordingState: .off)
-
- // the gap item
- let gapItem = self.store.createPath(from: startSample)
- gapItem.previousItem = lastItem
- gapItem.add(endSample)
-
- // need to explicitly save because not in a process() block
- store.save()
-
- // make it current
- currentItem = gapItem
- }
-
- // MARK: - The recording cycle
-
- private var _currentItem: TimelineItem?
- public private(set) var currentItem: TimelineItem? {
- get {
- if let item = _currentItem { return item }
- _currentItem = store.mostRecentItem
- return _currentItem
- }
- set(newValue) {
- _currentItem = newValue
- }
- }
-
- public func updateCurrentItem() {
- _currentItem = store.mostRecentItem
- }
-
- public var currentVisit: Visit? { return currentItem as? Visit }
-
- private var lastRecorded: Date?
-
- private func recordSample() {
- guard isRecording else { return }
-
- // don't record too soon
- if let lastRecorded = lastRecorded, lastRecorded.age < 60.0 / samplesPerMinute { return }
-
- lastRecorded = Date()
-
- let sample = store.createSample(from: ActivityBrain.highlander.presentSample)
-
- // classify the sample, if a classifier has been provided
- if let classifier = classifier, classifier.canClassify(sample.location?.coordinate) {
- sample.classifierResults = classifier.classify(sample, previousResults: lastClassifierResults)
- lastClassifierResults = sample.classifierResults
- }
-
- // make sure sleep mode doesn't happen prematurely
- updateSleepModeAcceptability()
-
- store.process {
- self.process(sample)
- self.updateSleepModeAcceptability()
- }
-
- // recreate the location manager on nolo, to work around iOS 13.3 bug
- if sample.isNolo {
- LocomotionManager.highlander.recreateTheLocationManager()
- }
- }
-
- private func process(_ sample: PersistentSample) {
-
- /** first timeline item **/
- guard let currentItem = currentItem else {
- createTimelineItem(from: sample)
- return
- }
-
- /** datagap -> anything **/
- if currentItem.isDataGap {
- createTimelineItem(from: sample)
- return
- }
-
- let previouslyMoving = currentItem is Path
- let currentlyMoving = sample.movingState != .stationary
-
- /** stationary -> moving || moving -> stationary **/
- if currentlyMoving != previouslyMoving {
- createTimelineItem(from: sample)
- return
- }
-
- /** moving -> moving **/
- if previouslyMoving && currentlyMoving {
-
- // if activityType hasn't changed, reuse current
- if sample.activityType == currentItem.movingActivityType {
- currentItem.add(sample)
- return
- }
-
- // if edge speeds are above the mode change threshold, reuse current
- if let currentSpeed = currentItem.samples.last?.location?.speed, let sampleSpeed = sample.location?.speed {
- if currentSpeed > Path.maximumModeShiftSpeed && sampleSpeed > Path.maximumModeShiftSpeed {
- currentItem.add(sample)
- return
- }
- }
-
- // couldn't reuse current path
- createTimelineItem(from: sample)
- return
- }
-
- /** stationary -> stationary **/
-
- currentItem.add(sample)
-
- // if in sleep mode, only retain the last X sleep mode samples
- if RecordingState.sleepStates.contains(sample.recordingState) {
- pruneSleepModeSamples(for: currentItem)
- }
- }
-
- private func pruneSleepModeSamples(for item: TimelineItem) {
- guard let endDate = item.endDate else { return }
-
- // collect the contiguous sleep samples from the end
- let edgeSleepSamples = item.samples.reversed().prefix {
- RecordingState.sleepStates.contains($0.recordingState)
- }
-
- // keep most recent 20 minutes of sleep samples
- let keeperBoundary: TimeInterval = .oneMinute * 20
- let durationBetween: TimeInterval = .oneMinute * 5
-
- var lastKept: PersistentSample? = edgeSleepSamples.last
- var samplesToKill: [PersistentSample] = []
-
- for sample in edgeSleepSamples.reversed() {
- // sample younger than the time window? then we done
- if endDate.timeIntervalSince(sample.date) < keeperBoundary { break }
-
- // always keep the newest sleep sample
- if sample == edgeSleepSamples.first { break }
-
- // always keep the oldest sleep sample
- if sample == edgeSleepSamples.last { continue }
-
- // sample is too close to the previously kept one?
- if let lastKept = lastKept, sample.date.timeIntervalSince(lastKept.date) < durationBetween {
- samplesToKill.append(sample)
- continue
- }
-
- // must've kept it
- lastKept = sample
- }
-
- samplesToKill.forEach { $0.delete() }
- }
-
- private func updateSleepModeAcceptability() {
- let loco = LocomotionManager.highlander
-
- // don't muck about with recording state if it's been explicitly turned off
- if loco.recordingState == .off { return }
-
- // don't be fiddling when someone else is responsible for recording
- if loco.recordingState == .standby { return }
-
- // sleep mode requires currentItem to be a keeper visit
- guard let currentVisit = currentVisit, currentVisit.isWorthKeeping else {
- loco.useLowPowerSleepModeWhileStationary = false
-
- // not recording, but should be?
- if loco.recordingState != .recording { loco.startRecording() }
-
- return
- }
-
- // permit sleep mode
- loco.useLowPowerSleepModeWhileStationary = true
- }
-
- // MARK: - Timeline item creation
-
- private func createTimelineItem(from sample: PersistentSample) {
- let newItem: TimelineItem = sample.movingState == .stationary
- ? store.createVisit(from: sample)
- : store.createPath(from: sample)
-
- // keep the list linked
- newItem.previousItem = currentItem
-
- // new item becomes current
- currentItem = newItem
-
- onMain {
- let note = Notification(name: .newTimelineItem, object: self, userInfo: ["timelineItem": newItem])
- NotificationCenter.default.post(note)
- }
- }
-
-}
diff --git a/LocoKit/Timelines/TimelineStore+Migrations.swift b/LocoKit/Timelines/TimelineStore+Migrations.swift
deleted file mode 100644
index cac3acf1..00000000
--- a/LocoKit/Timelines/TimelineStore+Migrations.swift
+++ /dev/null
@@ -1,241 +0,0 @@
-//
-// TimelineStore+Migrations.swift
-// LocoKit
-//
-// Created by Matt Greenfield on 4/6/18.
-//
-
-import GRDB
-
-internal extension TimelineStore {
-
- func registerMigrations() {
-
- // initial tables creation
- migrator.registerMigration("CreateTables") { db in
- try db.create(table: "TimelineItem") { table in
- table.column("itemId", .text).primaryKey()
-
- table.column("lastSaved", .datetime).notNull().indexed()
- table.column("deleted", .boolean).notNull()
- table.column("isVisit", .boolean).notNull().indexed()
- table.column("startDate", .datetime).indexed()
- table.column("endDate", .datetime).indexed()
- table.column("source", .text).defaults(to: "LocoKit").indexed()
-
- table.column("previousItemId", .text).indexed().references("TimelineItem", onDelete: .setNull, deferred: true)
- .check(sql: "previousItemId != itemId AND (previousItemId IS NULL OR deleted = 0)")
- table.column("nextItemId", .text).indexed().references("TimelineItem", onDelete: .setNull, deferred: true)
- .check(sql: "nextItemId != itemId AND (nextItemId IS NULL OR deleted = 0)")
-
- table.column("radiusMean", .double)
- table.column("radiusSD", .double)
- table.column("altitude", .double)
- table.column("stepCount", .integer)
- table.column("floorsAscended", .integer)
- table.column("floorsDescended", .integer)
- table.column("activityType", .text)
- table.column("distance", .double)
-
- // item.center
- table.column("latitude", .double)
- table.column("longitude", .double)
- }
-
- try db.create(table: "LocomotionSample") { table in
- table.column("sampleId", .text).primaryKey()
-
- table.column("date", .datetime).notNull().indexed()
- table.column("deleted", .boolean).notNull().indexed()
- table.column("lastSaved", .datetime).notNull().indexed()
- table.column("source", .text).defaults(to: "LocoKit").indexed()
-
- table.column("movingState", .text).notNull()
- table.column("recordingState", .text).notNull()
-
- table.column("timelineItemId", .text).references("TimelineItem", onDelete: .setNull, deferred: true)
-
- table.column("stepHz", .double)
- table.column("courseVariance", .double)
- table.column("xyAcceleration", .double)
- table.column("zAcceleration", .double)
- table.column("coreMotionActivityType", .text)
- table.column("classifiedType", .text)
- table.column("confirmedType", .text)
- table.column("previousSampleConfirmedType", .text)
-
- // sample.location
- table.column("latitude", .double).indexed()
- table.column("longitude", .double).indexed()
- table.column("altitude", .double)
- table.column("horizontalAccuracy", .double)
- table.column("verticalAccuracy", .double)
- table.column("speed", .double)
- table.column("course", .double)
- }
-
- try db.create(index: "LocomotionSample_on_timelineItemId_deleted_date", on: "LocomotionSample",
- columns: ["timelineItemId", "deleted", "date"])
- try db.create(index: "LocomotionSample_on_confirmedType_latitude_longitude_date", on: "LocomotionSample",
- columns: ["confirmedType", "latitude", "longitude", "date"])
- try db.create(index: "LocomotionSample_on_confirmedType_lastSaved", on: "LocomotionSample",
- columns: ["confirmedType", "lastSaved"])
-
- /** maintaining the linked list **/
-
- try db.execute(sql: """
- CREATE TRIGGER TimelineItem_UPDATE_nextItemId
- AFTER UPDATE OF nextItemId ON TimelineItem
- BEGIN
- UPDATE TimelineItem SET previousItemId = NULL WHERE itemId = OLD.nextItemId;
- UPDATE TimelineItem SET previousItemId = NEW.itemId WHERE itemId = NEW.nextItemId;
- END
- """)
-
- try db.execute(sql: """
- CREATE TRIGGER TimelineItem_UPDATE_previousItemId
- AFTER UPDATE OF previousItemId ON TimelineItem
- BEGIN
- UPDATE TimelineItem SET nextItemId = NULL WHERE itemId = OLD.previousItemId;
- UPDATE TimelineItem SET nextItemId = NEW.itemId WHERE itemId = NEW.previousItemId;
- END
- """)
-
- try db.execute(sql: """
- CREATE TRIGGER TimelineItem_INSERT_previousEdge
- AFTER INSERT ON TimelineItem
- WHEN NEW.previousItemId IS NOT NULL
- BEGIN
- UPDATE TimelineItem SET nextItemId = NEW.itemId WHERE itemId = NEW.previousItemId;
- END
- """)
- try db.execute(sql: """
- CREATE TRIGGER TimelineItem_INSERT_nextEdge
- AFTER INSERT ON TimelineItem
- WHEN NEW.nextItemId IS NOT NULL
- BEGIN
- UPDATE TimelineItem SET previousItemId = NEW.itemId WHERE itemId = NEW.nextItemId;
- END
- """)
-
- /** ensure the edges are detached when an item is soft deleted **/
-
- try db.execute(sql: """
- CREATE TRIGGER TimelineItem_UPDATE_deleted_previousEdge
- AFTER UPDATE OF deleted ON TimelineItem
- WHEN NEW.deleted = 1 AND NEW.previousItemId IS NOT NULL
- BEGIN
- UPDATE TimelineItem SET nextItemId = NULL WHERE itemId = NEW.previousItemId;
- UPDATE TimelineItem SET previousItemId = NULL WHERE itemId = NEW.itemId;
- END
- """)
-
- try db.execute(sql: """
- CREATE TRIGGER TimelineItem_UPDATE_deleted_nextEdge
- AFTER UPDATE OF deleted ON TimelineItem
- WHEN NEW.deleted = 1 AND NEW.nextItemId IS NOT NULL
- BEGIN
- UPDATE TimelineItem SET previousItemId = NULL WHERE itemId = NEW.nextItemId;
- UPDATE TimelineItem SET nextItemId = NULL WHERE itemId = NEW.itemId;
- END
- """)
- }
-
- migrator.registerMigration("7.0.1 segments") { db in
- try db.create(index: "TimelineItem_on_deleted_startDate", on: "TimelineItem",
- columns: ["deleted", "startDate"])
- }
-
- migrator.registerMigration("7.0.2") { db in
- try? db.alter(table: "LocomotionSample") { table in
- table.add(column: "previousSampleConfirmedType", .text)
- }
- }
-
- migrator.registerMigration("7.0.4 timezones") { db in
- try db.alter(table: "LocomotionSample") { table in
- table.add(column: "secondsFromGMT", .integer)
- }
- }
-
- migrator.registerMigration("7.0.5 cached activity types") { db in
- try? db.alter(table: "LocomotionSample") { table in
- table.add(column: "classifiedType", .text)
- }
- }
- }
-
- // MARK: - Auxiliary database
-
- func registerAuxiliaryDbMigrations() {
- auxiliaryDbMigrator.registerMigration("7.0.0 models") { db in
- try db.create(table: "ActivityTypeModel") { table in
- table.column("geoKey", .text).primaryKey()
- table.column("lastSaved", .datetime).notNull().indexed()
- table.column("version", .integer).notNull().indexed()
- table.column("lastUpdated", .datetime).indexed()
-
- table.column("name", .text).notNull().indexed()
- table.column("depth", .integer).notNull().indexed()
- table.column("isShared", .boolean).notNull().indexed()
- table.column("needsUpdate", .boolean).indexed()
- table.column("totalSamples", .integer).notNull()
- table.column("accuracyScore", .double)
-
- table.column("latitudeMax", .double).notNull().indexed()
- table.column("latitudeMin", .double).notNull().indexed()
- table.column("longitudeMax", .double).notNull().indexed()
- table.column("longitudeMin", .double).notNull().indexed()
-
- table.column("movingPct", .double)
- table.column("coreMotionTypeScores", .text)
- table.column("altitudeHistogram", .text)
- table.column("courseHistogram", .text)
- table.column("courseVarianceHistogram", .text)
- table.column("speedHistogram", .text)
- table.column("stepHzHistogram", .text)
- table.column("timeOfDayHistogram", .text)
- table.column("xyAccelerationHistogram", .text)
- table.column("zAccelerationHistogram", .text)
- table.column("horizontalAccuracyHistogram", .text)
- table.column("coordinatesMatrix", .text)
- table.column("previousSampleActivityTypeScores", .text)
- }
- }
-
- auxiliaryDbMigrator.registerMigration("7.0.6 trust factor") { db in
- try db.create(table: "CoordinateTrust") { table in
- table.column("latitude", .double).notNull()
- table.column("longitude", .double).notNull()
- table.primaryKey(["latitude", "longitude"])
- table.column("trustFactor", .double).notNull()
- }
- }
- }
-
- // MARK: - Delayable migrations
-
- func registerDelayedMigrations() {
- migrator.registerMigration("7.0.6 recent confirmed samples") { db in
- try? db.create(index: "LocomotionSample_on_confirmedType_lastSaved", on: "LocomotionSample",
- columns: ["confirmedType", "lastSaved"])
- }
-
- migrator.registerMigration("7.0.6 models have moved") { db in
- try? db.drop(table: "ActivityTypeModel")
- try? db.drop(table: "CoordinateTrust")
- }
-
- migrator.registerMigration("7.0.6 redundant indexes") { db in
- try? db.drop(index: "LocomotionSample_on_confirmedType")
- try? db.drop(index: "LocomotionSample_on_timelineItemId")
- }
-
- migrator.registerMigration("7.0.6 even better sample index") { db in
- try? db.drop(index: "LocomotionSample_on_confirmedType_latitude_longitude")
- try? db.create(index: "LocomotionSample_on_confirmedType_latitude_longitude_date", on: "LocomotionSample",
- columns: ["confirmedType", "latitude", "longitude", "date"])
- }
- }
-
-}
diff --git a/LocoKit/Timelines/TimelineStore.swift b/LocoKit/Timelines/TimelineStore.swift
deleted file mode 100644
index 1b341fb1..00000000
--- a/LocoKit/Timelines/TimelineStore.swift
+++ /dev/null
@@ -1,572 +0,0 @@
-//
-// TimelineStore.swift
-// LocoKit
-//
-// Created by Matt Greenfield on 18/12/17.
-// Copyright © 2017 Big Paua. All rights reserved.
-//
-
-import os.log
-import UIKit
-import CoreLocation
-import GRDB
-
-public extension NSNotification.Name {
- static let processingStarted = Notification.Name("processingStarted")
- static let processingStopped = Notification.Name("processingStopped")
-}
-
-/// An SQL database backed persistent timeline store.
-open class TimelineStore {
-
- public init() {
- connectToDatabase()
- migrateDatabases()
- pool?.add(transactionObserver: itemsObserver)
-
- let center = NotificationCenter.default
- center.addObserver(forName: UIApplication.didBecomeActiveNotification, object: nil, queue: nil) { [weak self] note in
- self?.didBecomeActive()
- }
- center.addObserver(forName: UIApplication.didEnterBackgroundNotification, object: nil, queue: nil) { [weak self] note in
- self?.didEnterBackground()
- }
- center.addObserver(forName: .timelineObjectsExternallyModified, object: nil, queue: nil) { [weak self] note in
- guard let objectIds = note.userInfo?["objectIds"] as? Set else { return }
- self?.invalidate(objectIds: objectIds)
- }
- }
-
- open var keepDeletedObjectsFor: TimeInterval = 60 * 60
- public var sqlDebugLogging = false
-
- public var recorder: TimelineRecorder?
-
- public let mutex = UnfairLock()
-
- private let itemMap = NSMapTable.strongToWeakObjects()
- private let sampleMap = NSMapTable.strongToWeakObjects()
- private let modelMap = NSMapTable.strongToWeakObjects()
- private let segmentMap = NSMapTable.strongToWeakObjects()
-
- public private(set) var processing = false {
- didSet {
- guard processing != oldValue else { return }
- let noteName: NSNotification.Name = processing ? .processingStarted : .processingStopped
- onMain { NotificationCenter.default.post(Notification(name: noteName, object: self, userInfo: nil)) }
- }
- }
-
- public var itemsInStore: Int { return mutex.sync { itemMap.objectEnumerator()?.allObjects.count ?? 0 } }
- public var samplesInStore: Int { return mutex.sync { sampleMap.objectEnumerator()?.allObjects.count ?? 0 } }
- public var modelsInStore: Int { return mutex.sync { modelMap.objectEnumerator()?.allObjects.count ?? 0 } }
- public var segmentsInStore: Int { return mutex.sync { segmentMap.objectEnumerator()?.allObjects.count ?? 0 } }
-
- public var itemsToSave: Set = []
- public var samplesToSave: Set = []
-
- private lazy var itemsObserver = {
- return ItemsObserver(store: self)
- }()
-
- open lazy var dbDir: URL = {
- return try! FileManager.default.url(for: .applicationSupportDirectory, in: .userDomainMask, appropriateFor: nil, create: true)
- }()
-
- open lazy var dbUrl: URL = {
- return dbDir.appendingPathComponent("LocoKit.sqlite")
- }()
-
- open lazy var auxiliaryDbUrl: URL = {
- return dbDir.appendingPathComponent("LocoKitAuxiliary.sqlite")
- }()
-
- public lazy var poolConfig: Configuration = {
- var config = Configuration()
- config.busyMode = .timeout(30)
- config.defaultTransactionKind = .immediate
- config.maximumReaderCount = 12
- if sqlDebugLogging {
- config.trace = {
- if self.sqlDebugLogging { os_log("SQL: %@", type: .default, $0) }
- }
- }
- return config
- }()
-
- public private(set) var pool: DatabasePool?
-
- public lazy var auxiliaryPool: DatabasePool = {
- return try! DatabasePool(path: self.auxiliaryDbUrl.path, configuration: self.poolConfig)
- }()
-
- public func connectToDatabase() {
- guard pool == nil else { return }
- pool = try! DatabasePool(path: self.dbUrl.path, configuration: self.poolConfig)
- }
-
- public func disconnectFromDatabase() {
- pool = nil
- }
-
- // MARK: - Object creation
-
- open func createVisit(from sample: PersistentSample) -> Visit {
- let visit = Visit(in: self)
- visit.add(sample)
- return visit
- }
-
- open func createPath(from sample: PersistentSample) -> Path {
- let path = Path(in: self)
- path.add(sample)
- return path
- }
-
- open func createVisit(from samples: [PersistentSample]) -> Visit {
- let visit = Visit(in: self)
- visit.add(samples)
- return visit
- }
-
- open func createPath(from samples: [PersistentSample]) -> Path {
- let path = Path(in: self)
- path.add(samples)
- return path
- }
-
- open func createSample(from sample: ActivityBrainSample) -> PersistentSample {
- let sample = PersistentSample(from: sample, in: self)
- saveOne(sample) // save the sample immediately, to avoid mystery data loss
- return sample
- }
-
- open func createSample(date: Date, location: CLLocation? = nil, movingState: MovingState = .uncertain,
- recordingState: RecordingState) -> PersistentSample {
- let sample = PersistentSample(date: date, location: location, recordingState: recordingState, in: self)
- saveOne(sample) // save the sample immediately, to avoid mystery data loss
- return sample
- }
-
- // MARK: - Object adding
-
- open func add(_ timelineItem: TimelineItem) {
- mutex.sync { itemMap.setObject(timelineItem, forKey: timelineItem.itemId as NSUUID) }
- }
-
- open func add(_ sample: PersistentSample) {
- mutex.sync { sampleMap.setObject(sample, forKey: sample.sampleId as NSUUID) }
- }
-
- open func add(_ model: ActivityType) {
- mutex.sync { modelMap.setObject(model, forKey: model.geoKey as NSString) }
- }
-
- open func add(_ segment: TimelineSegment) {
- mutex.sync { segmentMap.setObject(segment, forKey: NSNumber(value: segment.hashValue)) }
- }
-
- // MARK: - Object fetching
-
- open func object(for objectId: UUID) -> TimelineObject? {
- return mutex.sync {
- if let item = itemMap.object(forKey: objectId as NSUUID), !item.invalidated { return item }
- if let sample = sampleMap.object(forKey: objectId as NSUUID), !sample.invalidated { return sample }
- return nil
- }
- }
-
- public func itemInStore(matching: (TimelineItem) -> Bool) -> TimelineItem? {
- return mutex.sync {
- guard let enumerator = itemMap.objectEnumerator() else { return nil }
- for case let item as TimelineItem in enumerator {
- if item.invalidated { continue }
- if matching(item) { return item }
- }
- return nil
- }
- }
-
- public func object(for row: Row) -> TimelineObject {
- if row["itemId"] as String? != nil { return item(for: row) }
- if row["sampleId"] as String? != nil { return sample(for: row) }
- fatalError("Couldn't create an object for the row.")
- }
-
- // MARK: - Item fetching
-
- open var mostRecentItem: TimelineItem? {
- return item(where: "deleted = 0 ORDER BY endDate DESC")
- }
-
- open func item(for itemId: UUID) -> TimelineItem? {
- if let item = object(for: itemId) as? TimelineItem { return item }
- return item(where: "itemId = ?", arguments: [itemId.uuidString])
- }
-
- public func item(where query: String, arguments: StatementArguments = StatementArguments()) -> TimelineItem? {
- return item(for: "SELECT * FROM TimelineItem WHERE " + query, arguments: arguments)
- }
-
- public func items(where query: String, arguments: StatementArguments = StatementArguments()) -> [TimelineItem] {
- return items(for: "SELECT * FROM TimelineItem WHERE " + query, arguments: arguments)
- }
-
- public func item(for query: String, arguments: StatementArguments = StatementArguments()) -> TimelineItem? {
- guard let pool = pool else { fatalError("Attempting to access the database when disconnected") }
- return try! pool.read { db in
- guard let row = try Row.fetchOne(db, sql: query, arguments: arguments) else { return nil }
- return item(for: row)
- }
- }
-
- public func items(for query: String, arguments: StatementArguments = StatementArguments()) -> [TimelineItem] {
- guard let pool = pool else { fatalError("Attempting to access the database when disconnected") }
- return try! pool.read { db in
- var items: [TimelineItem] = []
- let itemRows = try Row.fetchCursor(db, sql: query, arguments: arguments)
- while let row = try itemRows.next() { items.append(item(for: row)) }
- return items
- }
- }
-
- open func item(for row: Row) -> TimelineItem {
- guard let itemId = row["itemId"] as String? else { fatalError("MISSING ITEMID") }
- if let item = object(for: UUID(uuidString: itemId)!) as? TimelineItem { return item }
- guard let isVisit = row["isVisit"] as Bool? else { fatalError("MISSING ISVISIT BOOL") }
- return isVisit
- ? Visit(from: row.asDict(in: self), in: self)
- : Path(from: row.asDict(in: self), in: self)
- }
-
- // MARK: Sample fetching
-
- open func sample(for sampleId: UUID) -> PersistentSample? {
- if let sample = object(for: sampleId) as? PersistentSample { return sample }
- return sample(for: "SELECT * FROM LocomotionSample WHERE sampleId = ?", arguments: [sampleId.uuidString])
- }
-
- public func sample(where query: String, arguments: StatementArguments = StatementArguments()) -> PersistentSample? {
- return sample(for: "SELECT * FROM LocomotionSample WHERE " + query, arguments: arguments)
- }
-
- public func samples(where query: String, arguments: StatementArguments = StatementArguments()) -> [PersistentSample] {
- return samples(for: "SELECT * FROM LocomotionSample WHERE " + query, arguments: arguments)
- }
-
- public func sample(for query: String, arguments: StatementArguments = StatementArguments()) -> PersistentSample? {
- guard let pool = pool else { fatalError("Attempting to access the database when disconnected") }
- return try! pool.read { db in
- guard let row = try Row.fetchOne(db, sql: query, arguments: arguments) else { return nil }
- return sample(for: row)
- }
- }
-
- public func samples(for query: String, arguments: StatementArguments = StatementArguments()) -> [PersistentSample] {
- guard let pool = pool else { fatalError("Attempting to access the database when disconnected") }
- let rows = try! pool.read { db in
- return try Row.fetchAll(db, sql: query, arguments: arguments)
- }
- return rows.map { sample(for: $0) }
- }
-
- open func sample(for row: Row) -> PersistentSample {
- guard let sampleId = row["sampleId"] as String? else { fatalError("MISSING SAMPLEID") }
- if let sample = object(for: UUID(uuidString: sampleId)!) as? PersistentSample { return sample }
- return PersistentSample(from: row.asDict(in: self), in: self)
- }
-
- // MARK: - Model fetching
-
- public func model(where query: String, arguments: StatementArguments = StatementArguments()) -> ActivityType? {
- return model(for: "SELECT * FROM ActivityTypeModel WHERE " + query, arguments: arguments)
- }
-
- public func model(for query: String, arguments: StatementArguments = StatementArguments()) -> ActivityType? {
- return try! auxiliaryPool.read { db in
- guard let row = try Row.fetchOne(db, sql: query, arguments: arguments) else { return nil }
- return model(for: row)
- }
- }
-
- public func models(where query: String, arguments: StatementArguments = StatementArguments()) -> [ActivityType] {
- return models(for: "SELECT * FROM ActivityTypeModel WHERE " + query, arguments: arguments)
- }
-
- public func models(for query: String, arguments: StatementArguments = StatementArguments()) -> [ActivityType] {
- let rows = try! auxiliaryPool.read { db in
- return try Row.fetchAll(db, sql: query, arguments: arguments)
- }
- return rows.map { model(for: $0) }
- }
-
- func model(for row: Row) -> ActivityType {
- guard let geoKey = row["geoKey"] as String? else { fatalError("MISSING GEOKEY") }
- if let cached = mutex.sync(execute: { modelMap.object(forKey: geoKey as NSString) }) { return cached }
- if let model = ActivityType(dict: row.asDict(in: self), in: self) { return model }
- fatalError("FAILED MODEL INIT FROM ROW")
- }
-
- // MARK: - Segments
-
- public func segment(for dateRange: DateInterval) -> TimelineSegment {
- let segment = self.segment(where: "endDate > :startDate AND startDate < :endDate AND deleted = 0 ORDER BY startDate",
- arguments: ["startDate": dateRange.start, "endDate": dateRange.end])
- segment.dateRange = dateRange
- return segment
- }
-
- public func segment(where query: String, arguments: StatementArguments? = nil) -> TimelineSegment {
- var hasher = Hasher()
- hasher.combine("SELECT * FROM TimelineItem WHERE " + query)
- if let arguments = arguments { hasher.combine(arguments.description) }
- let hashValue = hasher.finalize()
-
- // have an existing one?
- if let cached = segmentMap.object(forKey: NSNumber(value: hashValue)) { return cached }
-
- // make a fresh one
- let segment = TimelineSegment(where: query, arguments: arguments, in: self)
- self.add(segment)
- return segment
- }
-
- // MARK: - Counting
-
- public func countItems(where query: String = "1", arguments: StatementArguments = StatementArguments()) -> Int {
- guard let pool = pool else { fatalError("Attempting to access the database when disconnected") }
- return try! pool.read { db in
- return try Int.fetchOne(db, sql: "SELECT COUNT(*) FROM TimelineItem WHERE " + query, arguments: arguments)!
- }
- }
-
- public func countSamples(where query: String = "1", arguments: StatementArguments = StatementArguments()) -> Int {
- guard let pool = pool else { fatalError("Attempting to access the database when disconnected") }
- return try! pool.read { db in
- return try Int.fetchOne(db, sql: "SELECT COUNT(*) FROM LocomotionSample WHERE " + query, arguments: arguments)!
- }
- }
-
- public func countModels(where query: String = "1", arguments: StatementArguments = StatementArguments()) -> Int {
- return try! auxiliaryPool.read { db in
- return try Int.fetchOne(db, sql: "SELECT COUNT(*) FROM ActivityTypeModel WHERE " + query, arguments: arguments)!
- }
- }
-
- // MARK: - Saving
-
- public func save(_ object: TimelineObject, immediate: Bool) {
- mutex.sync {
- if let item = object as? TimelineItem {
- itemsToSave.insert(item)
- } else if let sample = object as? PersistentSample {
- samplesToSave.insert(sample)
- }
- }
- if immediate { save() }
- }
-
- open func save() {
- guard let pool = pool else { fatalError("Attempting to access the database when disconnected") }
-
- var savingItems: Set = []
- var savingSamples: Set = []
-
- mutex.sync {
- savingItems = itemsToSave.filter { $0.needsSave && !$0.invalidated }
- itemsToSave = []
-
- savingSamples = samplesToSave.filter { $0.needsSave && !$0.invalidated }
- samplesToSave = []
- }
-
- var savedObjectIds: Set = []
-
- if !savingItems.isEmpty {
- do {
- try pool.write { db in
- let now = Date()
- for case let item as TimelineObject in savingItems {
- item.transactionDate = now
- do { try item.save(in: db) }
- catch PersistenceError.recordNotFound { os_log("PersistenceError.recordNotFound", type: .error) }
- catch let error as DatabaseError where error.resultCode == .SQLITE_CONSTRAINT {
- // constraint fails (linked list inconsistencies) are non fatal
- // so let's break the edges and put the item back in the queue
- (item as? TimelineItem)?.previousItemId = nil
- (item as? TimelineItem)?.nextItemId = nil
- save(item, immediate: false)
-
- } catch {
- os_log("%@", type: .error, String(describing: error))
- save(item, immediate: false)
- }
- savedObjectIds.insert(item.objectId)
- }
- db.afterNextTransactionCommit { db in
- for case let item as TimelineObject in savingItems where !item.hasChanges {
- item.lastSaved = item.transactionDate
- }
- }
- }
-
- } catch {
- os_log("%@", type: .error, String(describing: error))
- }
- }
- if !savingSamples.isEmpty {
- do {
- try pool.write { db in
- let now = Date()
- for case let sample as TimelineObject in savingSamples {
- sample.transactionDate = now
- do { try sample.save(in: db) }
- catch PersistenceError.recordNotFound { os_log("PersistenceError.recordNotFound", type: .error) }
- catch let error as DatabaseError where error.resultCode == .SQLITE_CONSTRAINT {
- // break the edge and put it back in the queue
- (sample as? PersistentSample)?.timelineItem = nil
- save(sample, immediate: false)
-
- } catch {
- os_log("%@", type: .error, String(describing: error))
- save(sample, immediate: false)
- }
- savedObjectIds.insert(sample.objectId)
- }
- db.afterNextTransactionCommit { db in
- for case let sample as TimelineObject in savingSamples where !sample.hasChanges {
- sample.lastSaved = sample.transactionDate
- }
- }
- }
-
- } catch {
- os_log("%@", type: .error, String(describing: error))
- }
- }
-
- // tell the app group about db objects that've changed
- if !savedObjectIds.isEmpty {
- LocomotionManager.highlander.appGroup?.notifyObjectChanges(objectIds: savedObjectIds)
- }
- }
-
- public func saveOne(_ object: TimelineObject) {
- guard let pool = pool else { fatalError("Attempting to access the database when disconnected") }
- do {
- try pool.write { db in
- object.transactionDate = Date()
- do { try object.save(in: db) }
- catch PersistenceError.recordNotFound { os_log("PersistenceError.recordNotFound", type: .error) }
- db.afterNextTransactionCommit { db in
- object.lastSaved = object.transactionDate
- }
- }
- } catch {
- os_log("%@", type: .error, error.localizedDescription)
- }
- }
-
- // MARK: - Object invalidation
-
- open func invalidate(objectIds: Set) {
- for objectId in objectIds {
- object(for: objectId)?.invalidate()
- }
- }
-
- // MARK: - Processing
-
- public func process(changes: @escaping () -> Void) {
- Jobs.addPrimaryJob("TimelineStore.process") {
- self.processing = true
- changes()
- self.save()
- self.processing = false
- }
- }
-
- // MARK: - Background and Foreground
-
- private func didBecomeActive() {
- guard let segments = mutex.sync(execute: { segmentMap.objectEnumerator()?.allObjects as? [TimelineSegment] }) else { return }
- segments.forEach { $0.shouldReclassifySamples = true }
- }
-
- private func didEnterBackground() {
- guard let segments = mutex.sync(execute: { segmentMap.objectEnumerator()?.allObjects as? [TimelineSegment] }) else { return }
- segments.forEach { $0.shouldReclassifySamples = false }
- }
-
- // MARK: - Database housekeeping
-
- open func hardDeleteSoftDeletedObjects() {
- guard let pool = pool else { fatalError("Attempting to access the database when disconnected") }
- let deadline = Date(timeIntervalSinceNow: -keepDeletedObjectsFor)
- do {
- try pool.write { db in
- try db.execute(sql: "DELETE FROM LocomotionSample WHERE deleted = 1 AND date < ?", arguments: [deadline])
- try db.execute(sql: "DELETE FROM TimelineItem WHERE deleted = 1 AND (endDate < ? OR endDate IS NULL)", arguments: [deadline])
- }
- } catch {
- os_log("%@", error.localizedDescription)
- }
- }
-
- open func deleteStaleSharedModels() {
- let deadline = Date(timeIntervalSinceNow: -ActivityTypesCache.staleLastUpdatedAge)
- do {
- try auxiliaryPool.write { db in
- try db.execute(sql: "DELETE FROM ActivityTypeModel WHERE isShared = 1 AND version = 0")
- try db.execute(sql: "DELETE FROM ActivityTypeModel WHERE isShared = 1 AND lastUpdated IS NULL")
- try db.execute(sql: "DELETE FROM ActivityTypeModel WHERE isShared = 1 AND lastUpdated < ?", arguments: [deadline])
- }
- } catch {
- os_log("%@", error.localizedDescription)
- }
- }
-
- // MARK: - Database creation and migrations
-
- public var migrator = DatabaseMigrator()
- public var auxiliaryDbMigrator = DatabaseMigrator()
-
- open func migrateDatabases() {
- guard let pool = pool else { fatalError("Attempting to access the database when disconnected") }
-
- registerMigrations()
- try! migrator.migrate(pool)
-
- registerAuxiliaryDbMigrations()
- try! auxiliaryDbMigrator.migrate(auxiliaryPool)
-
- delay(20, onQueue: DispatchQueue.global()) {
- self.registerDelayedMigrations()
- try! self.migrator.migrate(pool)
- }
- }
-
- open var dateFields: [String] { return ["lastSaved", "lastUpdated", "startDate", "endDate", "date"] }
- open var boolFields: [String] { return ["isVisit", "deleted", "locationIsBogus", "isShared", "needsUpdate"] }
-
-}
-
-public extension Row {
- func asDict(in store: TimelineStore) -> [String: Any?] {
- let dateFields = store.dateFields
- let boolFields = store.boolFields
- return Dictionary(self.map { column, value in
- if dateFields.contains(column) { return (column, Date.fromDatabaseValue(value)) }
- if boolFields.contains(column) { return (column, Bool.fromDatabaseValue(value)) }
- return (column, value.storage.value)
- }, uniquingKeysWith: { left, _ in left })
- }
-}
-
-public extension Database {
- func explain(query: String, arguments: StatementArguments = StatementArguments()) throws {
- for explain in try Row.fetchAll(self, sql: "EXPLAIN QUERY PLAN " + query, arguments: arguments) {
- print("EXPLAIN: \(explain)")
- }
- }
-}
diff --git a/LocoKitCore.framework.zip b/LocoKitCore.framework.zip
deleted file mode 100644
index 1026d171..00000000
Binary files a/LocoKitCore.framework.zip and /dev/null differ
diff --git a/LocoKitCore.framework/Headers/LocoKitCore-Swift.h b/LocoKitCore.framework/Headers/LocoKitCore-Swift.h
deleted file mode 100644
index 593e8d89..00000000
--- a/LocoKitCore.framework/Headers/LocoKitCore-Swift.h
+++ /dev/null
@@ -1,430 +0,0 @@
-#if 0
-#elif defined(__arm64__) && __arm64__
-// Generated by Apple Swift version 5.1.3 effective-4.2 (swiftlang-1100.0.282.1 clang-1100.0.33.15)
-#pragma clang diagnostic push
-#pragma clang diagnostic ignored "-Wgcc-compat"
-
-#if !defined(__has_include)
-# define __has_include(x) 0
-#endif
-#if !defined(__has_attribute)
-# define __has_attribute(x) 0
-#endif
-#if !defined(__has_feature)
-# define __has_feature(x) 0
-#endif
-#if !defined(__has_warning)
-# define __has_warning(x) 0
-#endif
-
-#if __has_include()
-# include
-#endif
-
-#pragma clang diagnostic ignored "-Wauto-import"
-#include
-#include
-#include
-#include
-
-#if !defined(SWIFT_TYPEDEFS)
-# define SWIFT_TYPEDEFS 1
-# if __has_include()
-# include
-# elif !defined(__cplusplus)
-typedef uint_least16_t char16_t;
-typedef uint_least32_t char32_t;
-# endif
-typedef float swift_float2 __attribute__((__ext_vector_type__(2)));
-typedef float swift_float3 __attribute__((__ext_vector_type__(3)));
-typedef float swift_float4 __attribute__((__ext_vector_type__(4)));
-typedef double swift_double2 __attribute__((__ext_vector_type__(2)));
-typedef double swift_double3 __attribute__((__ext_vector_type__(3)));
-typedef double swift_double4 __attribute__((__ext_vector_type__(4)));
-typedef int swift_int2 __attribute__((__ext_vector_type__(2)));
-typedef int swift_int3 __attribute__((__ext_vector_type__(3)));
-typedef int swift_int4 __attribute__((__ext_vector_type__(4)));
-typedef unsigned int swift_uint2 __attribute__((__ext_vector_type__(2)));
-typedef unsigned int swift_uint3 __attribute__((__ext_vector_type__(3)));
-typedef unsigned int swift_uint4 __attribute__((__ext_vector_type__(4)));
-#endif
-
-#if !defined(SWIFT_PASTE)
-# define SWIFT_PASTE_HELPER(x, y) x##y
-# define SWIFT_PASTE(x, y) SWIFT_PASTE_HELPER(x, y)
-#endif
-#if !defined(SWIFT_METATYPE)
-# define SWIFT_METATYPE(X) Class
-#endif
-#if !defined(SWIFT_CLASS_PROPERTY)
-# if __has_feature(objc_class_property)
-# define SWIFT_CLASS_PROPERTY(...) __VA_ARGS__
-# else
-# define SWIFT_CLASS_PROPERTY(...)
-# endif
-#endif
-
-#if __has_attribute(objc_runtime_name)
-# define SWIFT_RUNTIME_NAME(X) __attribute__((objc_runtime_name(X)))
-#else
-# define SWIFT_RUNTIME_NAME(X)
-#endif
-#if __has_attribute(swift_name)
-# define SWIFT_COMPILE_NAME(X) __attribute__((swift_name(X)))
-#else
-# define SWIFT_COMPILE_NAME(X)
-#endif
-#if __has_attribute(objc_method_family)
-# define SWIFT_METHOD_FAMILY(X) __attribute__((objc_method_family(X)))
-#else
-# define SWIFT_METHOD_FAMILY(X)
-#endif
-#if __has_attribute(noescape)
-# define SWIFT_NOESCAPE __attribute__((noescape))
-#else
-# define SWIFT_NOESCAPE
-#endif
-#if __has_attribute(warn_unused_result)
-# define SWIFT_WARN_UNUSED_RESULT __attribute__((warn_unused_result))
-#else
-# define SWIFT_WARN_UNUSED_RESULT
-#endif
-#if __has_attribute(noreturn)
-# define SWIFT_NORETURN __attribute__((noreturn))
-#else
-# define SWIFT_NORETURN
-#endif
-#if !defined(SWIFT_CLASS_EXTRA)
-# define SWIFT_CLASS_EXTRA
-#endif
-#if !defined(SWIFT_PROTOCOL_EXTRA)
-# define SWIFT_PROTOCOL_EXTRA
-#endif
-#if !defined(SWIFT_ENUM_EXTRA)
-# define SWIFT_ENUM_EXTRA
-#endif
-#if !defined(SWIFT_CLASS)
-# if __has_attribute(objc_subclassing_restricted)
-# define SWIFT_CLASS(SWIFT_NAME) SWIFT_RUNTIME_NAME(SWIFT_NAME) __attribute__((objc_subclassing_restricted)) SWIFT_CLASS_EXTRA
-# define SWIFT_CLASS_NAMED(SWIFT_NAME) __attribute__((objc_subclassing_restricted)) SWIFT_COMPILE_NAME(SWIFT_NAME) SWIFT_CLASS_EXTRA
-# else
-# define SWIFT_CLASS(SWIFT_NAME) SWIFT_RUNTIME_NAME(SWIFT_NAME) SWIFT_CLASS_EXTRA
-# define SWIFT_CLASS_NAMED(SWIFT_NAME) SWIFT_COMPILE_NAME(SWIFT_NAME) SWIFT_CLASS_EXTRA
-# endif
-#endif
-#if !defined(SWIFT_RESILIENT_CLASS)
-# if __has_attribute(objc_class_stub)
-# define SWIFT_RESILIENT_CLASS(SWIFT_NAME) SWIFT_CLASS(SWIFT_NAME) __attribute__((objc_class_stub))
-# define SWIFT_RESILIENT_CLASS_NAMED(SWIFT_NAME) __attribute__((objc_class_stub)) SWIFT_CLASS_NAMED(SWIFT_NAME)
-# else
-# define SWIFT_RESILIENT_CLASS(SWIFT_NAME) SWIFT_CLASS(SWIFT_NAME)
-# define SWIFT_RESILIENT_CLASS_NAMED(SWIFT_NAME) SWIFT_CLASS_NAMED(SWIFT_NAME)
-# endif
-#endif
-
-#if !defined(SWIFT_PROTOCOL)
-# define SWIFT_PROTOCOL(SWIFT_NAME) SWIFT_RUNTIME_NAME(SWIFT_NAME) SWIFT_PROTOCOL_EXTRA
-# define SWIFT_PROTOCOL_NAMED(SWIFT_NAME) SWIFT_COMPILE_NAME(SWIFT_NAME) SWIFT_PROTOCOL_EXTRA
-#endif
-
-#if !defined(SWIFT_EXTENSION)
-# define SWIFT_EXTENSION(M) SWIFT_PASTE(M##_Swift_, __LINE__)
-#endif
-
-#if !defined(OBJC_DESIGNATED_INITIALIZER)
-# if __has_attribute(objc_designated_initializer)
-# define OBJC_DESIGNATED_INITIALIZER __attribute__((objc_designated_initializer))
-# else
-# define OBJC_DESIGNATED_INITIALIZER
-# endif
-#endif
-#if !defined(SWIFT_ENUM_ATTR)
-# if defined(__has_attribute) && __has_attribute(enum_extensibility)
-# define SWIFT_ENUM_ATTR(_extensibility) __attribute__((enum_extensibility(_extensibility)))
-# else
-# define SWIFT_ENUM_ATTR(_extensibility)
-# endif
-#endif
-#if !defined(SWIFT_ENUM)
-# define SWIFT_ENUM(_type, _name, _extensibility) enum _name : _type _name; enum SWIFT_ENUM_ATTR(_extensibility) SWIFT_ENUM_EXTRA _name : _type
-# if __has_feature(generalized_swift_name)
-# define SWIFT_ENUM_NAMED(_type, _name, SWIFT_NAME, _extensibility) enum _name : _type _name SWIFT_COMPILE_NAME(SWIFT_NAME); enum SWIFT_COMPILE_NAME(SWIFT_NAME) SWIFT_ENUM_ATTR(_extensibility) SWIFT_ENUM_EXTRA _name : _type
-# else
-# define SWIFT_ENUM_NAMED(_type, _name, SWIFT_NAME, _extensibility) SWIFT_ENUM(_type, _name, _extensibility)
-# endif
-#endif
-#if !defined(SWIFT_UNAVAILABLE)
-# define SWIFT_UNAVAILABLE __attribute__((unavailable))
-#endif
-#if !defined(SWIFT_UNAVAILABLE_MSG)
-# define SWIFT_UNAVAILABLE_MSG(msg) __attribute__((unavailable(msg)))
-#endif
-#if !defined(SWIFT_AVAILABILITY)
-# define SWIFT_AVAILABILITY(plat, ...) __attribute__((availability(plat, __VA_ARGS__)))
-#endif
-#if !defined(SWIFT_WEAK_IMPORT)
-# define SWIFT_WEAK_IMPORT __attribute__((weak_import))
-#endif
-#if !defined(SWIFT_DEPRECATED)
-# define SWIFT_DEPRECATED __attribute__((deprecated))
-#endif
-#if !defined(SWIFT_DEPRECATED_MSG)
-# define SWIFT_DEPRECATED_MSG(...) __attribute__((deprecated(__VA_ARGS__)))
-#endif
-#if __has_feature(attribute_diagnose_if_objc)
-# define SWIFT_DEPRECATED_OBJC(Msg) __attribute__((diagnose_if(1, Msg, "warning")))
-#else
-# define SWIFT_DEPRECATED_OBJC(Msg) SWIFT_DEPRECATED_MSG(Msg)
-#endif
-#if !defined(IBSegueAction)
-# define IBSegueAction
-#endif
-#if __has_feature(modules)
-#if __has_warning("-Watimport-in-framework-header")
-#pragma clang diagnostic ignored "-Watimport-in-framework-header"
-#endif
-@import CoreLocation;
-@import CoreMotion;
-#endif
-
-#pragma clang diagnostic ignored "-Wproperty-attribute-mismatch"
-#pragma clang diagnostic ignored "-Wduplicate-method-arg"
-#if __has_warning("-Wpragma-clang-attribute")
-# pragma clang diagnostic ignored "-Wpragma-clang-attribute"
-#endif
-#pragma clang diagnostic ignored "-Wunknown-pragmas"
-#pragma clang diagnostic ignored "-Wnullability"
-
-#if __has_attribute(external_source_symbol)
-# pragma push_macro("any")
-# undef any
-# pragma clang attribute push(__attribute__((external_source_symbol(language="Swift", defined_in="LocoKitCore",generated_declaration))), apply_to=any(function,enum,objc_interface,objc_category,objc_protocol))
-# pragma pop_macro("any")
-#endif
-
-
-
-
-
-
-
-#if __has_attribute(external_source_symbol)
-# pragma clang attribute pop
-#endif
-#pragma clang diagnostic pop
-
-#elif defined(__ARM_ARCH_7A__) && __ARM_ARCH_7A__
-// Generated by Apple Swift version 5.1.3 effective-4.2 (swiftlang-1100.0.282.1 clang-1100.0.33.15)
-#pragma clang diagnostic push
-#pragma clang diagnostic ignored "-Wgcc-compat"
-
-#if !defined(__has_include)
-# define __has_include(x) 0
-#endif
-#if !defined(__has_attribute)
-# define __has_attribute(x) 0
-#endif
-#if !defined(__has_feature)
-# define __has_feature(x) 0
-#endif
-#if !defined(__has_warning)
-# define __has_warning(x) 0
-#endif
-
-#if __has_include()
-# include
-#endif
-
-#pragma clang diagnostic ignored "-Wauto-import"
-#include
-#include
-#include
-#include
-
-#if !defined(SWIFT_TYPEDEFS)
-# define SWIFT_TYPEDEFS 1
-# if __has_include()
-# include
-# elif !defined(__cplusplus)
-typedef uint_least16_t char16_t;
-typedef uint_least32_t char32_t;
-# endif
-typedef float swift_float2 __attribute__((__ext_vector_type__(2)));
-typedef float swift_float3 __attribute__((__ext_vector_type__(3)));
-typedef float swift_float4 __attribute__((__ext_vector_type__(4)));
-typedef double swift_double2 __attribute__((__ext_vector_type__(2)));
-typedef double swift_double3 __attribute__((__ext_vector_type__(3)));
-typedef double swift_double4 __attribute__((__ext_vector_type__(4)));
-typedef int swift_int2 __attribute__((__ext_vector_type__(2)));
-typedef int swift_int3 __attribute__((__ext_vector_type__(3)));
-typedef int swift_int4 __attribute__((__ext_vector_type__(4)));
-typedef unsigned int swift_uint2 __attribute__((__ext_vector_type__(2)));
-typedef unsigned int swift_uint3 __attribute__((__ext_vector_type__(3)));
-typedef unsigned int swift_uint4 __attribute__((__ext_vector_type__(4)));
-#endif
-
-#if !defined(SWIFT_PASTE)
-# define SWIFT_PASTE_HELPER(x, y) x##y
-# define SWIFT_PASTE(x, y) SWIFT_PASTE_HELPER(x, y)
-#endif
-#if !defined(SWIFT_METATYPE)
-# define SWIFT_METATYPE(X) Class
-#endif
-#if !defined(SWIFT_CLASS_PROPERTY)
-# if __has_feature(objc_class_property)
-# define SWIFT_CLASS_PROPERTY(...) __VA_ARGS__
-# else
-# define SWIFT_CLASS_PROPERTY(...)
-# endif
-#endif
-
-#if __has_attribute(objc_runtime_name)
-# define SWIFT_RUNTIME_NAME(X) __attribute__((objc_runtime_name(X)))
-#else
-# define SWIFT_RUNTIME_NAME(X)
-#endif
-#if __has_attribute(swift_name)
-# define SWIFT_COMPILE_NAME(X) __attribute__((swift_name(X)))
-#else
-# define SWIFT_COMPILE_NAME(X)
-#endif
-#if __has_attribute(objc_method_family)
-# define SWIFT_METHOD_FAMILY(X) __attribute__((objc_method_family(X)))
-#else
-# define SWIFT_METHOD_FAMILY(X)
-#endif
-#if __has_attribute(noescape)
-# define SWIFT_NOESCAPE __attribute__((noescape))
-#else
-# define SWIFT_NOESCAPE
-#endif
-#if __has_attribute(warn_unused_result)
-# define SWIFT_WARN_UNUSED_RESULT __attribute__((warn_unused_result))
-#else
-# define SWIFT_WARN_UNUSED_RESULT
-#endif
-#if __has_attribute(noreturn)
-# define SWIFT_NORETURN __attribute__((noreturn))
-#else
-# define SWIFT_NORETURN
-#endif
-#if !defined(SWIFT_CLASS_EXTRA)
-# define SWIFT_CLASS_EXTRA
-#endif
-#if !defined(SWIFT_PROTOCOL_EXTRA)
-# define SWIFT_PROTOCOL_EXTRA
-#endif
-#if !defined(SWIFT_ENUM_EXTRA)
-# define SWIFT_ENUM_EXTRA
-#endif
-#if !defined(SWIFT_CLASS)
-# if __has_attribute(objc_subclassing_restricted)
-# define SWIFT_CLASS(SWIFT_NAME) SWIFT_RUNTIME_NAME(SWIFT_NAME) __attribute__((objc_subclassing_restricted)) SWIFT_CLASS_EXTRA
-# define SWIFT_CLASS_NAMED(SWIFT_NAME) __attribute__((objc_subclassing_restricted)) SWIFT_COMPILE_NAME(SWIFT_NAME) SWIFT_CLASS_EXTRA
-# else
-# define SWIFT_CLASS(SWIFT_NAME) SWIFT_RUNTIME_NAME(SWIFT_NAME) SWIFT_CLASS_EXTRA
-# define SWIFT_CLASS_NAMED(SWIFT_NAME) SWIFT_COMPILE_NAME(SWIFT_NAME) SWIFT_CLASS_EXTRA
-# endif
-#endif
-#if !defined(SWIFT_RESILIENT_CLASS)
-# if __has_attribute(objc_class_stub)
-# define SWIFT_RESILIENT_CLASS(SWIFT_NAME) SWIFT_CLASS(SWIFT_NAME) __attribute__((objc_class_stub))
-# define SWIFT_RESILIENT_CLASS_NAMED(SWIFT_NAME) __attribute__((objc_class_stub)) SWIFT_CLASS_NAMED(SWIFT_NAME)
-# else
-# define SWIFT_RESILIENT_CLASS(SWIFT_NAME) SWIFT_CLASS(SWIFT_NAME)
-# define SWIFT_RESILIENT_CLASS_NAMED(SWIFT_NAME) SWIFT_CLASS_NAMED(SWIFT_NAME)
-# endif
-#endif
-
-#if !defined(SWIFT_PROTOCOL)
-# define SWIFT_PROTOCOL(SWIFT_NAME) SWIFT_RUNTIME_NAME(SWIFT_NAME) SWIFT_PROTOCOL_EXTRA
-# define SWIFT_PROTOCOL_NAMED(SWIFT_NAME) SWIFT_COMPILE_NAME(SWIFT_NAME) SWIFT_PROTOCOL_EXTRA
-#endif
-
-#if !defined(SWIFT_EXTENSION)
-# define SWIFT_EXTENSION(M) SWIFT_PASTE(M##_Swift_, __LINE__)
-#endif
-
-#if !defined(OBJC_DESIGNATED_INITIALIZER)
-# if __has_attribute(objc_designated_initializer)
-# define OBJC_DESIGNATED_INITIALIZER __attribute__((objc_designated_initializer))
-# else
-# define OBJC_DESIGNATED_INITIALIZER
-# endif
-#endif
-#if !defined(SWIFT_ENUM_ATTR)
-# if defined(__has_attribute) && __has_attribute(enum_extensibility)
-# define SWIFT_ENUM_ATTR(_extensibility) __attribute__((enum_extensibility(_extensibility)))
-# else
-# define SWIFT_ENUM_ATTR(_extensibility)
-# endif
-#endif
-#if !defined(SWIFT_ENUM)
-# define SWIFT_ENUM(_type, _name, _extensibility) enum _name : _type _name; enum SWIFT_ENUM_ATTR(_extensibility) SWIFT_ENUM_EXTRA _name : _type
-# if __has_feature(generalized_swift_name)
-# define SWIFT_ENUM_NAMED(_type, _name, SWIFT_NAME, _extensibility) enum _name : _type _name SWIFT_COMPILE_NAME(SWIFT_NAME); enum SWIFT_COMPILE_NAME(SWIFT_NAME) SWIFT_ENUM_ATTR(_extensibility) SWIFT_ENUM_EXTRA _name : _type
-# else
-# define SWIFT_ENUM_NAMED(_type, _name, SWIFT_NAME, _extensibility) SWIFT_ENUM(_type, _name, _extensibility)
-# endif
-#endif
-#if !defined(SWIFT_UNAVAILABLE)
-# define SWIFT_UNAVAILABLE __attribute__((unavailable))
-#endif
-#if !defined(SWIFT_UNAVAILABLE_MSG)
-# define SWIFT_UNAVAILABLE_MSG(msg) __attribute__((unavailable(msg)))
-#endif
-#if !defined(SWIFT_AVAILABILITY)
-# define SWIFT_AVAILABILITY(plat, ...) __attribute__((availability(plat, __VA_ARGS__)))
-#endif
-#if !defined(SWIFT_WEAK_IMPORT)
-# define SWIFT_WEAK_IMPORT __attribute__((weak_import))
-#endif
-#if !defined(SWIFT_DEPRECATED)
-# define SWIFT_DEPRECATED __attribute__((deprecated))
-#endif
-#if !defined(SWIFT_DEPRECATED_MSG)
-# define SWIFT_DEPRECATED_MSG(...) __attribute__((deprecated(__VA_ARGS__)))
-#endif
-#if __has_feature(attribute_diagnose_if_objc)
-# define SWIFT_DEPRECATED_OBJC(Msg) __attribute__((diagnose_if(1, Msg, "warning")))
-#else
-# define SWIFT_DEPRECATED_OBJC(Msg) SWIFT_DEPRECATED_MSG(Msg)
-#endif
-#if !defined(IBSegueAction)
-# define IBSegueAction
-#endif
-#if __has_feature(modules)
-#if __has_warning("-Watimport-in-framework-header")
-#pragma clang diagnostic ignored "-Watimport-in-framework-header"
-#endif
-@import CoreLocation;
-@import CoreMotion;
-#endif
-
-#pragma clang diagnostic ignored "-Wproperty-attribute-mismatch"
-#pragma clang diagnostic ignored "-Wduplicate-method-arg"
-#if __has_warning("-Wpragma-clang-attribute")
-# pragma clang diagnostic ignored "-Wpragma-clang-attribute"
-#endif
-#pragma clang diagnostic ignored "-Wunknown-pragmas"
-#pragma clang diagnostic ignored "-Wnullability"
-
-#if __has_attribute(external_source_symbol)
-# pragma push_macro("any")
-# undef any
-# pragma clang attribute push(__attribute__((external_source_symbol(language="Swift", defined_in="LocoKitCore",generated_declaration))), apply_to=any(function,enum,objc_interface,objc_category,objc_protocol))
-# pragma pop_macro("any")
-#endif
-
-
-
-
-
-
-
-#if __has_attribute(external_source_symbol)
-# pragma clang attribute pop
-#endif
-#pragma clang diagnostic pop
-
-#endif
diff --git a/LocoKitCore.framework/Headers/LocoKitCore.h b/LocoKitCore.framework/Headers/LocoKitCore.h
deleted file mode 100644
index 4b501e11..00000000
--- a/LocoKitCore.framework/Headers/LocoKitCore.h
+++ /dev/null
@@ -1,16 +0,0 @@
-//
-// LocoKit.h
-// LocoKitCore
-//
-// Created by Matt Greenfield on 2/07/17.
-// Copyright © 2017 Big Paua. All rights reserved.
-//
-
-#import
-
-//! Project version number for LocoKitCore.
-FOUNDATION_EXPORT double LocoKitCoreVersionNumber;
-
-//! Project version string for LocoKitCore.
-FOUNDATION_EXPORT const unsigned char LocoKitCoreVersionString[];
-
diff --git a/LocoKitCore.framework/Info.plist b/LocoKitCore.framework/Info.plist
deleted file mode 100644
index e82104a6..00000000
Binary files a/LocoKitCore.framework/Info.plist and /dev/null differ
diff --git a/LocoKitCore.framework/LocoKitCore b/LocoKitCore.framework/LocoKitCore
deleted file mode 100755
index 777fc6db..00000000
Binary files a/LocoKitCore.framework/LocoKitCore and /dev/null differ
diff --git a/LocoKitCore.framework/Modules/LocoKitCore.swiftmodule/arm.swiftdoc b/LocoKitCore.framework/Modules/LocoKitCore.swiftmodule/arm.swiftdoc
deleted file mode 100644
index e3084f08..00000000
Binary files a/LocoKitCore.framework/Modules/LocoKitCore.swiftmodule/arm.swiftdoc and /dev/null differ
diff --git a/LocoKitCore.framework/Modules/LocoKitCore.swiftmodule/arm.swiftinterface b/LocoKitCore.framework/Modules/LocoKitCore.swiftmodule/arm.swiftinterface
deleted file mode 100644
index a4b4557f..00000000
--- a/LocoKitCore.framework/Modules/LocoKitCore.swiftmodule/arm.swiftinterface
+++ /dev/null
@@ -1,207 +0,0 @@
-// swift-interface-format-version: 1.0
-// swift-compiler-version: Apple Swift version 5.1.3 effective-4.2 (swiftlang-1100.0.282.1 clang-1100.0.33.15)
-// 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
-import Accelerate
-import CoreLocation
-import CoreMotion
-import Darwin
-import Foundation
-@_exported import LocoKitCore
-import Swift
-import os.log
-import os
-public enum CoreMotionActivityTypeName : Swift.String, Swift.Codable {
- case unknown
- case stationary
- case automotive
- case walking
- case running
- case cycling
- public static let allTypes: [LocoKitCore.CoreMotionActivityTypeName]
- public typealias RawValue = Swift.String
- public init?(rawValue: Swift.String)
- public var rawValue: Swift.String {
- get
- }
-}
-public class ActivityBrain {
- public var processHistoricalLocations: Swift.Bool
- public static let highlander: LocoKitCore.ActivityBrain
- public var presentSample: LocoKitCore.ActivityBrainSample {
- get
- set
- }
- public var stationaryPeriodStart: Foundation.Date?
- @objc deinit
-}
-extension ActivityBrain {
- public static var historicalLocationsBrain: LocoKitCore.ActivityBrain {
- get
- }
- public func add(rawLocation location: CoreLocation.CLLocation, trustFactor: Swift.Double? = nil)
- public func update()
- public func freezeTheBrain()
- public var movingState: LocoKitCore.MovingState {
- get
- }
- public var horizontalAccuracy: Swift.Double {
- get
- }
- public var kalmanLocation: CoreLocation.CLLocation? {
- get
- }
- public func resetKalmans()
- public var kalmanRequiredN: Swift.Double {
- get
- }
- public var speedRequiredN: Swift.Double {
- get
- }
- public var requiredN: Swift.Int {
- get
- }
- public var dynamicMinimumConfidenceN: Swift.Int {
- get
- }
- public func spread(_ locations: [CoreLocation.CLLocation]) -> Foundation.TimeInterval
- public func add(pedoData: CoreMotion.CMPedometerData)
- public func add(deviceMotion: CoreMotion.CMDeviceMotion)
- public func add(cmMotionActivity activity: CoreMotion.CMMotionActivity)
-}
-public enum MovingState : Swift.String, Swift.Codable {
- case moving
- case stationary
- case uncertain
- public typealias RawValue = Swift.String
- public init?(rawValue: Swift.String)
- public var rawValue: Swift.String {
- get
- }
-}
-public class ActivityBrainSample {
- public var movingState: LocoKitCore.MovingState
- public var rawLocations: [CoreLocation.CLLocation] {
- get
- }
- public var filteredLocations: [CoreLocation.CLLocation] {
- get
- }
- public var date: Foundation.Date {
- get
- }
- public var location: CoreLocation.CLLocation? {
- get
- }
- public var stepHz: Swift.Double? {
- get
- }
- public var coreMotionActivityType: LocoKitCore.CoreMotionActivityTypeName? {
- get
- }
- public var speed: CoreLocation.CLLocationSpeed {
- get
- }
- public var course: CoreLocation.CLLocationDirection {
- get
- }
- public var courseVariance: Swift.Double? {
- get
- }
- public var xyAcceleration: Swift.Double? {
- get
- }
- public var zAcceleration: Swift.Double? {
- get
- }
- @objc deinit
-}
-postfix operator ′
-public protocol ScopedMutex {
- @discardableResult
- func sync(execute work: () throws -> R) rethrows -> R
- @discardableResult
- func trySync(execute work: () throws -> R) rethrows -> R?
-}
-public protocol RawMutex : LocoKitCore.ScopedMutex {
- associatedtype MutexPrimitive
- var unsafeMutex: Self.MutexPrimitive { get set }
- func unbalancedLock()
- func unbalancedTryLock() -> Swift.Bool
- func unbalancedUnlock()
-}
-extension RawMutex {
- @discardableResult
- public func sync(execute work: () throws -> R) rethrows -> R
- @discardableResult
- public func trySync(execute work: () throws -> R) rethrows -> R?
-}
-final public class PThreadMutex : LocoKitCore.RawMutex {
- public typealias MutexPrimitive = Darwin.pthread_mutex_t
- public enum PThreadMutexType {
- case normal
- case recursive
- public static func == (a: LocoKitCore.PThreadMutex.PThreadMutexType, b: LocoKitCore.PThreadMutex.PThreadMutexType) -> Swift.Bool
- public var hashValue: Swift.Int {
- get
- }
- public func hash(into hasher: inout Swift.Hasher)
- }
- final public var unsafeMutex: Darwin.pthread_mutex_t
- public init(type: LocoKitCore.PThreadMutex.PThreadMutexType = .normal)
- @objc deinit
- final public func unbalancedLock()
- final public func unbalancedTryLock() -> Swift.Bool
- final public func unbalancedUnlock()
-}
-final public class UnfairLock : LocoKitCore.RawMutex {
- public typealias MutexPrimitive = Darwin.os_unfair_lock
- public init()
- final public var unsafeMutex: Darwin.os_unfair_lock
- final public func unbalancedLock()
- final public func unbalancedTryLock() -> Swift.Bool
- final public func unbalancedUnlock()
- @objc deinit
-}
-public typealias Radians = Swift.Double
-public typealias AccuracyRange = (best: CoreLocation.CLLocationAccuracy, worst: CoreLocation.CLLocationAccuracy)
-extension Array where Element : CoreLocation.CLLocation {
- public var dateInterval: Foundation.DateInterval? {
- get
- }
-}
-infix operator ≅ : ComparisonPrecedence
-infix operator • : DefaultPrecedence
-public struct LocoKitService {
- public static var apiKey: Swift.String? {
- get
- set(key)
- }
- public static var deviceToken: Foundation.Data?
- public static var requestedWakeupCall: Foundation.Date? {
- get
- }
- public static var requestingWakeupCall: Swift.Bool {
- get
- }
- @discardableResult
- public static func requestWakeup(at requestedDate: Foundation.Date) -> Swift.Bool
- public static func fetchModelsFor(coordinate: CoreLocation.CLLocationCoordinate2D, depth: Swift.Int, completion: @escaping ([Swift.String : Any]?) -> Swift.Void)
-}
-extension Date {
- public var startOfDay: Foundation.Date {
- get
- }
- public var sinceStartOfDay: Foundation.TimeInterval {
- get
- }
- public var age: Foundation.TimeInterval {
- get
- }
-}
-extension LocoKitCore.CoreMotionActivityTypeName : Swift.Equatable {}
-extension LocoKitCore.CoreMotionActivityTypeName : Swift.Hashable {}
-extension LocoKitCore.CoreMotionActivityTypeName : Swift.RawRepresentable {}
-extension LocoKitCore.MovingState : Swift.Hashable {}
-extension LocoKitCore.MovingState : Swift.RawRepresentable {}
-extension LocoKitCore.PThreadMutex.PThreadMutexType : Swift.Equatable {}
-extension LocoKitCore.PThreadMutex.PThreadMutexType : Swift.Hashable {}
diff --git a/LocoKitCore.framework/Modules/LocoKitCore.swiftmodule/arm.swiftmodule b/LocoKitCore.framework/Modules/LocoKitCore.swiftmodule/arm.swiftmodule
deleted file mode 100644
index 336c45f0..00000000
Binary files a/LocoKitCore.framework/Modules/LocoKitCore.swiftmodule/arm.swiftmodule and /dev/null differ
diff --git a/LocoKitCore.framework/Modules/LocoKitCore.swiftmodule/arm64-apple-ios.swiftdoc b/LocoKitCore.framework/Modules/LocoKitCore.swiftmodule/arm64-apple-ios.swiftdoc
deleted file mode 100644
index ffe1feb3..00000000
Binary files a/LocoKitCore.framework/Modules/LocoKitCore.swiftmodule/arm64-apple-ios.swiftdoc and /dev/null differ
diff --git a/LocoKitCore.framework/Modules/LocoKitCore.swiftmodule/arm64-apple-ios.swiftinterface b/LocoKitCore.framework/Modules/LocoKitCore.swiftmodule/arm64-apple-ios.swiftinterface
deleted file mode 100644
index 91f4cb6f..00000000
--- a/LocoKitCore.framework/Modules/LocoKitCore.swiftmodule/arm64-apple-ios.swiftinterface
+++ /dev/null
@@ -1,207 +0,0 @@
-// swift-interface-format-version: 1.0
-// swift-compiler-version: Apple Swift version 5.1.3 effective-4.2 (swiftlang-1100.0.282.1 clang-1100.0.33.15)
-// 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
-import Accelerate
-import CoreLocation
-import CoreMotion
-import Darwin
-import Foundation
-@_exported import LocoKitCore
-import Swift
-import os.log
-import os
-public enum CoreMotionActivityTypeName : Swift.String, Swift.Codable {
- case unknown
- case stationary
- case automotive
- case walking
- case running
- case cycling
- public static let allTypes: [LocoKitCore.CoreMotionActivityTypeName]
- public typealias RawValue = Swift.String
- public init?(rawValue: Swift.String)
- public var rawValue: Swift.String {
- get
- }
-}
-public class ActivityBrain {
- public var processHistoricalLocations: Swift.Bool
- public static let highlander: LocoKitCore.ActivityBrain
- public var presentSample: LocoKitCore.ActivityBrainSample {
- get
- set
- }
- public var stationaryPeriodStart: Foundation.Date?
- @objc deinit
-}
-extension ActivityBrain {
- public static var historicalLocationsBrain: LocoKitCore.ActivityBrain {
- get
- }
- public func add(rawLocation location: CoreLocation.CLLocation, trustFactor: Swift.Double? = nil)
- public func update()
- public func freezeTheBrain()
- public var movingState: LocoKitCore.MovingState {
- get
- }
- public var horizontalAccuracy: Swift.Double {
- get
- }
- public var kalmanLocation: CoreLocation.CLLocation? {
- get
- }
- public func resetKalmans()
- public var kalmanRequiredN: Swift.Double {
- get
- }
- public var speedRequiredN: Swift.Double {
- get
- }
- public var requiredN: Swift.Int {
- get
- }
- public var dynamicMinimumConfidenceN: Swift.Int {
- get
- }
- public func spread(_ locations: [CoreLocation.CLLocation]) -> Foundation.TimeInterval
- public func add(pedoData: CoreMotion.CMPedometerData)
- public func add(deviceMotion: CoreMotion.CMDeviceMotion)
- public func add(cmMotionActivity activity: CoreMotion.CMMotionActivity)
-}
-public enum MovingState : Swift.String, Swift.Codable {
- case moving
- case stationary
- case uncertain
- public typealias RawValue = Swift.String
- public init?(rawValue: Swift.String)
- public var rawValue: Swift.String {
- get
- }
-}
-public class ActivityBrainSample {
- public var movingState: LocoKitCore.MovingState
- public var rawLocations: [CoreLocation.CLLocation] {
- get
- }
- public var filteredLocations: [CoreLocation.CLLocation] {
- get
- }
- public var date: Foundation.Date {
- get
- }
- public var location: CoreLocation.CLLocation? {
- get
- }
- public var stepHz: Swift.Double? {
- get
- }
- public var coreMotionActivityType: LocoKitCore.CoreMotionActivityTypeName? {
- get
- }
- public var speed: CoreLocation.CLLocationSpeed {
- get
- }
- public var course: CoreLocation.CLLocationDirection {
- get
- }
- public var courseVariance: Swift.Double? {
- get
- }
- public var xyAcceleration: Swift.Double? {
- get
- }
- public var zAcceleration: Swift.Double? {
- get
- }
- @objc deinit
-}
-postfix operator ′
-public protocol ScopedMutex {
- @discardableResult
- func sync(execute work: () throws -> R) rethrows -> R
- @discardableResult
- func trySync(execute work: () throws -> R) rethrows -> R?
-}
-public protocol RawMutex : LocoKitCore.ScopedMutex {
- associatedtype MutexPrimitive
- var unsafeMutex: Self.MutexPrimitive { get set }
- func unbalancedLock()
- func unbalancedTryLock() -> Swift.Bool
- func unbalancedUnlock()
-}
-extension RawMutex {
- @discardableResult
- public func sync(execute work: () throws -> R) rethrows -> R
- @discardableResult
- public func trySync(execute work: () throws -> R) rethrows -> R?
-}
-final public class PThreadMutex : LocoKitCore.RawMutex {
- public typealias MutexPrimitive = Darwin.pthread_mutex_t
- public enum PThreadMutexType {
- case normal
- case recursive
- public static func == (a: LocoKitCore.PThreadMutex.PThreadMutexType, b: LocoKitCore.PThreadMutex.PThreadMutexType) -> Swift.Bool
- public var hashValue: Swift.Int {
- get
- }
- public func hash(into hasher: inout Swift.Hasher)
- }
- final public var unsafeMutex: Darwin.pthread_mutex_t
- public init(type: LocoKitCore.PThreadMutex.PThreadMutexType = .normal)
- @objc deinit
- final public func unbalancedLock()
- final public func unbalancedTryLock() -> Swift.Bool
- final public func unbalancedUnlock()
-}
-final public class UnfairLock : LocoKitCore.RawMutex {
- public typealias MutexPrimitive = Darwin.os_unfair_lock
- public init()
- final public var unsafeMutex: Darwin.os_unfair_lock
- final public func unbalancedLock()
- final public func unbalancedTryLock() -> Swift.Bool
- final public func unbalancedUnlock()
- @objc deinit
-}
-public typealias Radians = Swift.Double
-public typealias AccuracyRange = (best: CoreLocation.CLLocationAccuracy, worst: CoreLocation.CLLocationAccuracy)
-extension Array where Element : CoreLocation.CLLocation {
- public var dateInterval: Foundation.DateInterval? {
- get
- }
-}
-infix operator ≅ : ComparisonPrecedence
-infix operator • : DefaultPrecedence
-public struct LocoKitService {
- public static var apiKey: Swift.String? {
- get
- set(key)
- }
- public static var deviceToken: Foundation.Data?
- public static var requestedWakeupCall: Foundation.Date? {
- get
- }
- public static var requestingWakeupCall: Swift.Bool {
- get
- }
- @discardableResult
- public static func requestWakeup(at requestedDate: Foundation.Date) -> Swift.Bool
- public static func fetchModelsFor(coordinate: CoreLocation.CLLocationCoordinate2D, depth: Swift.Int, completion: @escaping ([Swift.String : Any]?) -> Swift.Void)
-}
-extension Date {
- public var startOfDay: Foundation.Date {
- get
- }
- public var sinceStartOfDay: Foundation.TimeInterval {
- get
- }
- public var age: Foundation.TimeInterval {
- get
- }
-}
-extension LocoKitCore.CoreMotionActivityTypeName : Swift.Equatable {}
-extension LocoKitCore.CoreMotionActivityTypeName : Swift.Hashable {}
-extension LocoKitCore.CoreMotionActivityTypeName : Swift.RawRepresentable {}
-extension LocoKitCore.MovingState : Swift.Hashable {}
-extension LocoKitCore.MovingState : Swift.RawRepresentable {}
-extension LocoKitCore.PThreadMutex.PThreadMutexType : Swift.Equatable {}
-extension LocoKitCore.PThreadMutex.PThreadMutexType : Swift.Hashable {}
diff --git a/LocoKitCore.framework/Modules/LocoKitCore.swiftmodule/arm64-apple-ios.swiftmodule b/LocoKitCore.framework/Modules/LocoKitCore.swiftmodule/arm64-apple-ios.swiftmodule
deleted file mode 100644
index 05aa1e72..00000000
Binary files a/LocoKitCore.framework/Modules/LocoKitCore.swiftmodule/arm64-apple-ios.swiftmodule and /dev/null differ
diff --git a/LocoKitCore.framework/Modules/LocoKitCore.swiftmodule/arm64.swiftdoc b/LocoKitCore.framework/Modules/LocoKitCore.swiftmodule/arm64.swiftdoc
deleted file mode 100644
index ffe1feb3..00000000
Binary files a/LocoKitCore.framework/Modules/LocoKitCore.swiftmodule/arm64.swiftdoc and /dev/null differ
diff --git a/LocoKitCore.framework/Modules/LocoKitCore.swiftmodule/arm64.swiftinterface b/LocoKitCore.framework/Modules/LocoKitCore.swiftmodule/arm64.swiftinterface
deleted file mode 100644
index 91f4cb6f..00000000
--- a/LocoKitCore.framework/Modules/LocoKitCore.swiftmodule/arm64.swiftinterface
+++ /dev/null
@@ -1,207 +0,0 @@
-// swift-interface-format-version: 1.0
-// swift-compiler-version: Apple Swift version 5.1.3 effective-4.2 (swiftlang-1100.0.282.1 clang-1100.0.33.15)
-// 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
-import Accelerate
-import CoreLocation
-import CoreMotion
-import Darwin
-import Foundation
-@_exported import LocoKitCore
-import Swift
-import os.log
-import os
-public enum CoreMotionActivityTypeName : Swift.String, Swift.Codable {
- case unknown
- case stationary
- case automotive
- case walking
- case running
- case cycling
- public static let allTypes: [LocoKitCore.CoreMotionActivityTypeName]
- public typealias RawValue = Swift.String
- public init?(rawValue: Swift.String)
- public var rawValue: Swift.String {
- get
- }
-}
-public class ActivityBrain {
- public var processHistoricalLocations: Swift.Bool
- public static let highlander: LocoKitCore.ActivityBrain
- public var presentSample: LocoKitCore.ActivityBrainSample {
- get
- set
- }
- public var stationaryPeriodStart: Foundation.Date?
- @objc deinit
-}
-extension ActivityBrain {
- public static var historicalLocationsBrain: LocoKitCore.ActivityBrain {
- get
- }
- public func add(rawLocation location: CoreLocation.CLLocation, trustFactor: Swift.Double? = nil)
- public func update()
- public func freezeTheBrain()
- public var movingState: LocoKitCore.MovingState {
- get
- }
- public var horizontalAccuracy: Swift.Double {
- get
- }
- public var kalmanLocation: CoreLocation.CLLocation? {
- get
- }
- public func resetKalmans()
- public var kalmanRequiredN: Swift.Double {
- get
- }
- public var speedRequiredN: Swift.Double {
- get
- }
- public var requiredN: Swift.Int {
- get
- }
- public var dynamicMinimumConfidenceN: Swift.Int {
- get
- }
- public func spread(_ locations: [CoreLocation.CLLocation]) -> Foundation.TimeInterval
- public func add(pedoData: CoreMotion.CMPedometerData)
- public func add(deviceMotion: CoreMotion.CMDeviceMotion)
- public func add(cmMotionActivity activity: CoreMotion.CMMotionActivity)
-}
-public enum MovingState : Swift.String, Swift.Codable {
- case moving
- case stationary
- case uncertain
- public typealias RawValue = Swift.String
- public init?(rawValue: Swift.String)
- public var rawValue: Swift.String {
- get
- }
-}
-public class ActivityBrainSample {
- public var movingState: LocoKitCore.MovingState
- public var rawLocations: [CoreLocation.CLLocation] {
- get
- }
- public var filteredLocations: [CoreLocation.CLLocation] {
- get
- }
- public var date: Foundation.Date {
- get
- }
- public var location: CoreLocation.CLLocation? {
- get
- }
- public var stepHz: Swift.Double? {
- get
- }
- public var coreMotionActivityType: LocoKitCore.CoreMotionActivityTypeName? {
- get
- }
- public var speed: CoreLocation.CLLocationSpeed {
- get
- }
- public var course: CoreLocation.CLLocationDirection {
- get
- }
- public var courseVariance: Swift.Double? {
- get
- }
- public var xyAcceleration: Swift.Double? {
- get
- }
- public var zAcceleration: Swift.Double? {
- get
- }
- @objc deinit
-}
-postfix operator ′
-public protocol ScopedMutex {
- @discardableResult
- func sync(execute work: () throws -> R) rethrows -> R
- @discardableResult
- func trySync(execute work: () throws -> R) rethrows -> R?
-}
-public protocol RawMutex : LocoKitCore.ScopedMutex {
- associatedtype MutexPrimitive
- var unsafeMutex: Self.MutexPrimitive { get set }
- func unbalancedLock()
- func unbalancedTryLock() -> Swift.Bool
- func unbalancedUnlock()
-}
-extension RawMutex {
- @discardableResult
- public func sync(execute work: () throws -> R) rethrows -> R
- @discardableResult
- public func trySync(execute work: () throws -> R) rethrows -> R?
-}
-final public class PThreadMutex : LocoKitCore.RawMutex {
- public typealias MutexPrimitive = Darwin.pthread_mutex_t
- public enum PThreadMutexType {
- case normal
- case recursive
- public static func == (a: LocoKitCore.PThreadMutex.PThreadMutexType, b: LocoKitCore.PThreadMutex.PThreadMutexType) -> Swift.Bool
- public var hashValue: Swift.Int {
- get
- }
- public func hash(into hasher: inout Swift.Hasher)
- }
- final public var unsafeMutex: Darwin.pthread_mutex_t
- public init(type: LocoKitCore.PThreadMutex.PThreadMutexType = .normal)
- @objc deinit
- final public func unbalancedLock()
- final public func unbalancedTryLock() -> Swift.Bool
- final public func unbalancedUnlock()
-}
-final public class UnfairLock : LocoKitCore.RawMutex {
- public typealias MutexPrimitive = Darwin.os_unfair_lock
- public init()
- final public var unsafeMutex: Darwin.os_unfair_lock
- final public func unbalancedLock()
- final public func unbalancedTryLock() -> Swift.Bool
- final public func unbalancedUnlock()
- @objc deinit
-}
-public typealias Radians = Swift.Double
-public typealias AccuracyRange = (best: CoreLocation.CLLocationAccuracy, worst: CoreLocation.CLLocationAccuracy)
-extension Array where Element : CoreLocation.CLLocation {
- public var dateInterval: Foundation.DateInterval? {
- get
- }
-}
-infix operator ≅ : ComparisonPrecedence
-infix operator • : DefaultPrecedence
-public struct LocoKitService {
- public static var apiKey: Swift.String? {
- get
- set(key)
- }
- public static var deviceToken: Foundation.Data?
- public static var requestedWakeupCall: Foundation.Date? {
- get
- }
- public static var requestingWakeupCall: Swift.Bool {
- get
- }
- @discardableResult
- public static func requestWakeup(at requestedDate: Foundation.Date) -> Swift.Bool
- public static func fetchModelsFor(coordinate: CoreLocation.CLLocationCoordinate2D, depth: Swift.Int, completion: @escaping ([Swift.String : Any]?) -> Swift.Void)
-}
-extension Date {
- public var startOfDay: Foundation.Date {
- get
- }
- public var sinceStartOfDay: Foundation.TimeInterval {
- get
- }
- public var age: Foundation.TimeInterval {
- get
- }
-}
-extension LocoKitCore.CoreMotionActivityTypeName : Swift.Equatable {}
-extension LocoKitCore.CoreMotionActivityTypeName : Swift.Hashable {}
-extension LocoKitCore.CoreMotionActivityTypeName : Swift.RawRepresentable {}
-extension LocoKitCore.MovingState : Swift.Hashable {}
-extension LocoKitCore.MovingState : Swift.RawRepresentable {}
-extension LocoKitCore.PThreadMutex.PThreadMutexType : Swift.Equatable {}
-extension LocoKitCore.PThreadMutex.PThreadMutexType : Swift.Hashable {}
diff --git a/LocoKitCore.framework/Modules/LocoKitCore.swiftmodule/arm64.swiftmodule b/LocoKitCore.framework/Modules/LocoKitCore.swiftmodule/arm64.swiftmodule
deleted file mode 100644
index 05aa1e72..00000000
Binary files a/LocoKitCore.framework/Modules/LocoKitCore.swiftmodule/arm64.swiftmodule and /dev/null differ
diff --git a/LocoKitCore.framework/Modules/LocoKitCore.swiftmodule/armv7-apple-ios.swiftdoc b/LocoKitCore.framework/Modules/LocoKitCore.swiftmodule/armv7-apple-ios.swiftdoc
deleted file mode 100644
index e3084f08..00000000
Binary files a/LocoKitCore.framework/Modules/LocoKitCore.swiftmodule/armv7-apple-ios.swiftdoc and /dev/null differ
diff --git a/LocoKitCore.framework/Modules/LocoKitCore.swiftmodule/armv7-apple-ios.swiftinterface b/LocoKitCore.framework/Modules/LocoKitCore.swiftmodule/armv7-apple-ios.swiftinterface
deleted file mode 100644
index a4b4557f..00000000
--- a/LocoKitCore.framework/Modules/LocoKitCore.swiftmodule/armv7-apple-ios.swiftinterface
+++ /dev/null
@@ -1,207 +0,0 @@
-// swift-interface-format-version: 1.0
-// swift-compiler-version: Apple Swift version 5.1.3 effective-4.2 (swiftlang-1100.0.282.1 clang-1100.0.33.15)
-// 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
-import Accelerate
-import CoreLocation
-import CoreMotion
-import Darwin
-import Foundation
-@_exported import LocoKitCore
-import Swift
-import os.log
-import os
-public enum CoreMotionActivityTypeName : Swift.String, Swift.Codable {
- case unknown
- case stationary
- case automotive
- case walking
- case running
- case cycling
- public static let allTypes: [LocoKitCore.CoreMotionActivityTypeName]
- public typealias RawValue = Swift.String
- public init?(rawValue: Swift.String)
- public var rawValue: Swift.String {
- get
- }
-}
-public class ActivityBrain {
- public var processHistoricalLocations: Swift.Bool
- public static let highlander: LocoKitCore.ActivityBrain
- public var presentSample: LocoKitCore.ActivityBrainSample {
- get
- set
- }
- public var stationaryPeriodStart: Foundation.Date?
- @objc deinit
-}
-extension ActivityBrain {
- public static var historicalLocationsBrain: LocoKitCore.ActivityBrain {
- get
- }
- public func add(rawLocation location: CoreLocation.CLLocation, trustFactor: Swift.Double? = nil)
- public func update()
- public func freezeTheBrain()
- public var movingState: LocoKitCore.MovingState {
- get
- }
- public var horizontalAccuracy: Swift.Double {
- get
- }
- public var kalmanLocation: CoreLocation.CLLocation? {
- get
- }
- public func resetKalmans()
- public var kalmanRequiredN: Swift.Double {
- get
- }
- public var speedRequiredN: Swift.Double {
- get
- }
- public var requiredN: Swift.Int {
- get
- }
- public var dynamicMinimumConfidenceN: Swift.Int {
- get
- }
- public func spread(_ locations: [CoreLocation.CLLocation]) -> Foundation.TimeInterval
- public func add(pedoData: CoreMotion.CMPedometerData)
- public func add(deviceMotion: CoreMotion.CMDeviceMotion)
- public func add(cmMotionActivity activity: CoreMotion.CMMotionActivity)
-}
-public enum MovingState : Swift.String, Swift.Codable {
- case moving
- case stationary
- case uncertain
- public typealias RawValue = Swift.String
- public init?(rawValue: Swift.String)
- public var rawValue: Swift.String {
- get
- }
-}
-public class ActivityBrainSample {
- public var movingState: LocoKitCore.MovingState
- public var rawLocations: [CoreLocation.CLLocation] {
- get
- }
- public var filteredLocations: [CoreLocation.CLLocation] {
- get
- }
- public var date: Foundation.Date {
- get
- }
- public var location: CoreLocation.CLLocation? {
- get
- }
- public var stepHz: Swift.Double? {
- get
- }
- public var coreMotionActivityType: LocoKitCore.CoreMotionActivityTypeName? {
- get
- }
- public var speed: CoreLocation.CLLocationSpeed {
- get
- }
- public var course: CoreLocation.CLLocationDirection {
- get
- }
- public var courseVariance: Swift.Double? {
- get
- }
- public var xyAcceleration: Swift.Double? {
- get
- }
- public var zAcceleration: Swift.Double? {
- get
- }
- @objc deinit
-}
-postfix operator ′
-public protocol ScopedMutex {
- @discardableResult
- func sync(execute work: () throws -> R) rethrows -> R
- @discardableResult
- func trySync(execute work: () throws -> R) rethrows -> R?
-}
-public protocol RawMutex : LocoKitCore.ScopedMutex {
- associatedtype MutexPrimitive
- var unsafeMutex: Self.MutexPrimitive { get set }
- func unbalancedLock()
- func unbalancedTryLock() -> Swift.Bool
- func unbalancedUnlock()
-}
-extension RawMutex {
- @discardableResult
- public func sync(execute work: () throws -> R) rethrows -> R
- @discardableResult
- public func trySync(execute work: () throws -> R) rethrows -> R?
-}
-final public class PThreadMutex : LocoKitCore.RawMutex {
- public typealias MutexPrimitive = Darwin.pthread_mutex_t
- public enum PThreadMutexType {
- case normal
- case recursive
- public static func == (a: LocoKitCore.PThreadMutex.PThreadMutexType, b: LocoKitCore.PThreadMutex.PThreadMutexType) -> Swift.Bool
- public var hashValue: Swift.Int {
- get
- }
- public func hash(into hasher: inout Swift.Hasher)
- }
- final public var unsafeMutex: Darwin.pthread_mutex_t
- public init(type: LocoKitCore.PThreadMutex.PThreadMutexType = .normal)
- @objc deinit
- final public func unbalancedLock()
- final public func unbalancedTryLock() -> Swift.Bool
- final public func unbalancedUnlock()
-}
-final public class UnfairLock : LocoKitCore.RawMutex {
- public typealias MutexPrimitive = Darwin.os_unfair_lock
- public init()
- final public var unsafeMutex: Darwin.os_unfair_lock
- final public func unbalancedLock()
- final public func unbalancedTryLock() -> Swift.Bool
- final public func unbalancedUnlock()
- @objc deinit
-}
-public typealias Radians = Swift.Double
-public typealias AccuracyRange = (best: CoreLocation.CLLocationAccuracy, worst: CoreLocation.CLLocationAccuracy)
-extension Array where Element : CoreLocation.CLLocation {
- public var dateInterval: Foundation.DateInterval? {
- get
- }
-}
-infix operator ≅ : ComparisonPrecedence
-infix operator • : DefaultPrecedence
-public struct LocoKitService {
- public static var apiKey: Swift.String? {
- get
- set(key)
- }
- public static var deviceToken: Foundation.Data?
- public static var requestedWakeupCall: Foundation.Date? {
- get
- }
- public static var requestingWakeupCall: Swift.Bool {
- get
- }
- @discardableResult
- public static func requestWakeup(at requestedDate: Foundation.Date) -> Swift.Bool
- public static func fetchModelsFor(coordinate: CoreLocation.CLLocationCoordinate2D, depth: Swift.Int, completion: @escaping ([Swift.String : Any]?) -> Swift.Void)
-}
-extension Date {
- public var startOfDay: Foundation.Date {
- get
- }
- public var sinceStartOfDay: Foundation.TimeInterval {
- get
- }
- public var age: Foundation.TimeInterval {
- get
- }
-}
-extension LocoKitCore.CoreMotionActivityTypeName : Swift.Equatable {}
-extension LocoKitCore.CoreMotionActivityTypeName : Swift.Hashable {}
-extension LocoKitCore.CoreMotionActivityTypeName : Swift.RawRepresentable {}
-extension LocoKitCore.MovingState : Swift.Hashable {}
-extension LocoKitCore.MovingState : Swift.RawRepresentable {}
-extension LocoKitCore.PThreadMutex.PThreadMutexType : Swift.Equatable {}
-extension LocoKitCore.PThreadMutex.PThreadMutexType : Swift.Hashable {}
diff --git a/LocoKitCore.framework/Modules/LocoKitCore.swiftmodule/armv7-apple-ios.swiftmodule b/LocoKitCore.framework/Modules/LocoKitCore.swiftmodule/armv7-apple-ios.swiftmodule
deleted file mode 100644
index 336c45f0..00000000
Binary files a/LocoKitCore.framework/Modules/LocoKitCore.swiftmodule/armv7-apple-ios.swiftmodule and /dev/null differ
diff --git a/LocoKitCore.framework/Modules/LocoKitCore.swiftmodule/armv7.swiftdoc b/LocoKitCore.framework/Modules/LocoKitCore.swiftmodule/armv7.swiftdoc
deleted file mode 100644
index e3084f08..00000000
Binary files a/LocoKitCore.framework/Modules/LocoKitCore.swiftmodule/armv7.swiftdoc and /dev/null differ
diff --git a/LocoKitCore.framework/Modules/LocoKitCore.swiftmodule/armv7.swiftinterface b/LocoKitCore.framework/Modules/LocoKitCore.swiftmodule/armv7.swiftinterface
deleted file mode 100644
index a4b4557f..00000000
--- a/LocoKitCore.framework/Modules/LocoKitCore.swiftmodule/armv7.swiftinterface
+++ /dev/null
@@ -1,207 +0,0 @@
-// swift-interface-format-version: 1.0
-// swift-compiler-version: Apple Swift version 5.1.3 effective-4.2 (swiftlang-1100.0.282.1 clang-1100.0.33.15)
-// 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
-import Accelerate
-import CoreLocation
-import CoreMotion
-import Darwin
-import Foundation
-@_exported import LocoKitCore
-import Swift
-import os.log
-import os
-public enum CoreMotionActivityTypeName : Swift.String, Swift.Codable {
- case unknown
- case stationary
- case automotive
- case walking
- case running
- case cycling
- public static let allTypes: [LocoKitCore.CoreMotionActivityTypeName]
- public typealias RawValue = Swift.String
- public init?(rawValue: Swift.String)
- public var rawValue: Swift.String {
- get
- }
-}
-public class ActivityBrain {
- public var processHistoricalLocations: Swift.Bool
- public static let highlander: LocoKitCore.ActivityBrain
- public var presentSample: LocoKitCore.ActivityBrainSample {
- get
- set
- }
- public var stationaryPeriodStart: Foundation.Date?
- @objc deinit
-}
-extension ActivityBrain {
- public static var historicalLocationsBrain: LocoKitCore.ActivityBrain {
- get
- }
- public func add(rawLocation location: CoreLocation.CLLocation, trustFactor: Swift.Double? = nil)
- public func update()
- public func freezeTheBrain()
- public var movingState: LocoKitCore.MovingState {
- get
- }
- public var horizontalAccuracy: Swift.Double {
- get
- }
- public var kalmanLocation: CoreLocation.CLLocation? {
- get
- }
- public func resetKalmans()
- public var kalmanRequiredN: Swift.Double {
- get
- }
- public var speedRequiredN: Swift.Double {
- get
- }
- public var requiredN: Swift.Int {
- get
- }
- public var dynamicMinimumConfidenceN: Swift.Int {
- get
- }
- public func spread(_ locations: [CoreLocation.CLLocation]) -> Foundation.TimeInterval
- public func add(pedoData: CoreMotion.CMPedometerData)
- public func add(deviceMotion: CoreMotion.CMDeviceMotion)
- public func add(cmMotionActivity activity: CoreMotion.CMMotionActivity)
-}
-public enum MovingState : Swift.String, Swift.Codable {
- case moving
- case stationary
- case uncertain
- public typealias RawValue = Swift.String
- public init?(rawValue: Swift.String)
- public var rawValue: Swift.String {
- get
- }
-}
-public class ActivityBrainSample {
- public var movingState: LocoKitCore.MovingState
- public var rawLocations: [CoreLocation.CLLocation] {
- get
- }
- public var filteredLocations: [CoreLocation.CLLocation] {
- get
- }
- public var date: Foundation.Date {
- get
- }
- public var location: CoreLocation.CLLocation? {
- get
- }
- public var stepHz: Swift.Double? {
- get
- }
- public var coreMotionActivityType: LocoKitCore.CoreMotionActivityTypeName? {
- get
- }
- public var speed: CoreLocation.CLLocationSpeed {
- get
- }
- public var course: CoreLocation.CLLocationDirection {
- get
- }
- public var courseVariance: Swift.Double? {
- get
- }
- public var xyAcceleration: Swift.Double? {
- get
- }
- public var zAcceleration: Swift.Double? {
- get
- }
- @objc deinit
-}
-postfix operator ′
-public protocol ScopedMutex {
- @discardableResult
- func sync(execute work: () throws -> R) rethrows -> R
- @discardableResult
- func trySync(execute work: () throws -> R) rethrows -> R?
-}
-public protocol RawMutex : LocoKitCore.ScopedMutex {
- associatedtype MutexPrimitive
- var unsafeMutex: Self.MutexPrimitive { get set }
- func unbalancedLock()
- func unbalancedTryLock() -> Swift.Bool
- func unbalancedUnlock()
-}
-extension RawMutex {
- @discardableResult
- public func sync(execute work: () throws -> R) rethrows -> R
- @discardableResult
- public func trySync(execute work: () throws -> R) rethrows -> R?
-}
-final public class PThreadMutex : LocoKitCore.RawMutex {
- public typealias MutexPrimitive = Darwin.pthread_mutex_t
- public enum PThreadMutexType {
- case normal
- case recursive
- public static func == (a: LocoKitCore.PThreadMutex.PThreadMutexType, b: LocoKitCore.PThreadMutex.PThreadMutexType) -> Swift.Bool
- public var hashValue: Swift.Int {
- get
- }
- public func hash(into hasher: inout Swift.Hasher)
- }
- final public var unsafeMutex: Darwin.pthread_mutex_t
- public init(type: LocoKitCore.PThreadMutex.PThreadMutexType = .normal)
- @objc deinit
- final public func unbalancedLock()
- final public func unbalancedTryLock() -> Swift.Bool
- final public func unbalancedUnlock()
-}
-final public class UnfairLock : LocoKitCore.RawMutex {
- public typealias MutexPrimitive = Darwin.os_unfair_lock
- public init()
- final public var unsafeMutex: Darwin.os_unfair_lock
- final public func unbalancedLock()
- final public func unbalancedTryLock() -> Swift.Bool
- final public func unbalancedUnlock()
- @objc deinit
-}
-public typealias Radians = Swift.Double
-public typealias AccuracyRange = (best: CoreLocation.CLLocationAccuracy, worst: CoreLocation.CLLocationAccuracy)
-extension Array where Element : CoreLocation.CLLocation {
- public var dateInterval: Foundation.DateInterval? {
- get
- }
-}
-infix operator ≅ : ComparisonPrecedence
-infix operator • : DefaultPrecedence
-public struct LocoKitService {
- public static var apiKey: Swift.String? {
- get
- set(key)
- }
- public static var deviceToken: Foundation.Data?
- public static var requestedWakeupCall: Foundation.Date? {
- get
- }
- public static var requestingWakeupCall: Swift.Bool {
- get
- }
- @discardableResult
- public static func requestWakeup(at requestedDate: Foundation.Date) -> Swift.Bool
- public static func fetchModelsFor(coordinate: CoreLocation.CLLocationCoordinate2D, depth: Swift.Int, completion: @escaping ([Swift.String : Any]?) -> Swift.Void)
-}
-extension Date {
- public var startOfDay: Foundation.Date {
- get
- }
- public var sinceStartOfDay: Foundation.TimeInterval {
- get
- }
- public var age: Foundation.TimeInterval {
- get
- }
-}
-extension LocoKitCore.CoreMotionActivityTypeName : Swift.Equatable {}
-extension LocoKitCore.CoreMotionActivityTypeName : Swift.Hashable {}
-extension LocoKitCore.CoreMotionActivityTypeName : Swift.RawRepresentable {}
-extension LocoKitCore.MovingState : Swift.Hashable {}
-extension LocoKitCore.MovingState : Swift.RawRepresentable {}
-extension LocoKitCore.PThreadMutex.PThreadMutexType : Swift.Equatable {}
-extension LocoKitCore.PThreadMutex.PThreadMutexType : Swift.Hashable {}
diff --git a/LocoKitCore.framework/Modules/LocoKitCore.swiftmodule/armv7.swiftmodule b/LocoKitCore.framework/Modules/LocoKitCore.swiftmodule/armv7.swiftmodule
deleted file mode 100644
index 336c45f0..00000000
Binary files a/LocoKitCore.framework/Modules/LocoKitCore.swiftmodule/armv7.swiftmodule and /dev/null differ
diff --git a/LocoKitCore.framework/Modules/LocoKitCore.swiftmodule/i386-apple-ios-simulator.swiftdoc b/LocoKitCore.framework/Modules/LocoKitCore.swiftmodule/i386-apple-ios-simulator.swiftdoc
deleted file mode 100644
index af8b0c6a..00000000
Binary files a/LocoKitCore.framework/Modules/LocoKitCore.swiftmodule/i386-apple-ios-simulator.swiftdoc and /dev/null differ
diff --git a/LocoKitCore.framework/Modules/LocoKitCore.swiftmodule/i386-apple-ios-simulator.swiftinterface b/LocoKitCore.framework/Modules/LocoKitCore.swiftmodule/i386-apple-ios-simulator.swiftinterface
deleted file mode 100644
index a12bd94b..00000000
--- a/LocoKitCore.framework/Modules/LocoKitCore.swiftmodule/i386-apple-ios-simulator.swiftinterface
+++ /dev/null
@@ -1,207 +0,0 @@
-// swift-interface-format-version: 1.0
-// swift-compiler-version: Apple Swift version 5.1.3 effective-4.2 (swiftlang-1100.0.282.1 clang-1100.0.33.15)
-// 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
-import Accelerate
-import CoreLocation
-import CoreMotion
-import Darwin
-import Foundation
-@_exported import LocoKitCore
-import Swift
-import os.log
-import os
-public enum CoreMotionActivityTypeName : Swift.String, Swift.Codable {
- case unknown
- case stationary
- case automotive
- case walking
- case running
- case cycling
- public static let allTypes: [LocoKitCore.CoreMotionActivityTypeName]
- public typealias RawValue = Swift.String
- public init?(rawValue: Swift.String)
- public var rawValue: Swift.String {
- get
- }
-}
-public class ActivityBrain {
- public var processHistoricalLocations: Swift.Bool
- public static let highlander: LocoKitCore.ActivityBrain
- public var presentSample: LocoKitCore.ActivityBrainSample {
- get
- set
- }
- public var stationaryPeriodStart: Foundation.Date?
- @objc deinit
-}
-extension ActivityBrain {
- public static var historicalLocationsBrain: LocoKitCore.ActivityBrain {
- get
- }
- public func add(rawLocation location: CoreLocation.CLLocation, trustFactor: Swift.Double? = nil)
- public func update()
- public func freezeTheBrain()
- public var movingState: LocoKitCore.MovingState {
- get
- }
- public var horizontalAccuracy: Swift.Double {
- get
- }
- public var kalmanLocation: CoreLocation.CLLocation? {
- get
- }
- public func resetKalmans()
- public var kalmanRequiredN: Swift.Double {
- get
- }
- public var speedRequiredN: Swift.Double {
- get
- }
- public var requiredN: Swift.Int {
- get
- }
- public var dynamicMinimumConfidenceN: Swift.Int {
- get
- }
- public func spread(_ locations: [CoreLocation.CLLocation]) -> Foundation.TimeInterval
- public func add(pedoData: CoreMotion.CMPedometerData)
- public func add(deviceMotion: CoreMotion.CMDeviceMotion)
- public func add(cmMotionActivity activity: CoreMotion.CMMotionActivity)
-}
-public enum MovingState : Swift.String, Swift.Codable {
- case moving
- case stationary
- case uncertain
- public typealias RawValue = Swift.String
- public init?(rawValue: Swift.String)
- public var rawValue: Swift.String {
- get
- }
-}
-public class ActivityBrainSample {
- public var movingState: LocoKitCore.MovingState
- public var rawLocations: [CoreLocation.CLLocation] {
- get
- }
- public var filteredLocations: [CoreLocation.CLLocation] {
- get
- }
- public var date: Foundation.Date {
- get
- }
- public var location: CoreLocation.CLLocation? {
- get
- }
- public var stepHz: Swift.Double? {
- get
- }
- public var coreMotionActivityType: LocoKitCore.CoreMotionActivityTypeName? {
- get
- }
- public var speed: CoreLocation.CLLocationSpeed {
- get
- }
- public var course: CoreLocation.CLLocationDirection {
- get
- }
- public var courseVariance: Swift.Double? {
- get
- }
- public var xyAcceleration: Swift.Double? {
- get
- }
- public var zAcceleration: Swift.Double? {
- get
- }
- @objc deinit
-}
-postfix operator ′
-public protocol ScopedMutex {
- @discardableResult
- func sync(execute work: () throws -> R) rethrows -> R
- @discardableResult
- func trySync(execute work: () throws -> R) rethrows -> R?
-}
-public protocol RawMutex : LocoKitCore.ScopedMutex {
- associatedtype MutexPrimitive
- var unsafeMutex: Self.MutexPrimitive { get set }
- func unbalancedLock()
- func unbalancedTryLock() -> Swift.Bool
- func unbalancedUnlock()
-}
-extension RawMutex {
- @discardableResult
- public func sync(execute work: () throws -> R) rethrows -> R
- @discardableResult
- public func trySync(execute work: () throws -> R) rethrows -> R?
-}
-final public class PThreadMutex : LocoKitCore.RawMutex {
- public typealias MutexPrimitive = Darwin.pthread_mutex_t
- public enum PThreadMutexType {
- case normal
- case recursive
- public static func == (a: LocoKitCore.PThreadMutex.PThreadMutexType, b: LocoKitCore.PThreadMutex.PThreadMutexType) -> Swift.Bool
- public var hashValue: Swift.Int {
- get
- }
- public func hash(into hasher: inout Swift.Hasher)
- }
- final public var unsafeMutex: Darwin.pthread_mutex_t
- public init(type: LocoKitCore.PThreadMutex.PThreadMutexType = .normal)
- @objc deinit
- final public func unbalancedLock()
- final public func unbalancedTryLock() -> Swift.Bool
- final public func unbalancedUnlock()
-}
-final public class UnfairLock : LocoKitCore.RawMutex {
- public typealias MutexPrimitive = Darwin.os_unfair_lock
- public init()
- final public var unsafeMutex: Darwin.os_unfair_lock
- final public func unbalancedLock()
- final public func unbalancedTryLock() -> Swift.Bool
- final public func unbalancedUnlock()
- @objc deinit
-}
-public typealias Radians = Swift.Double
-public typealias AccuracyRange = (best: CoreLocation.CLLocationAccuracy, worst: CoreLocation.CLLocationAccuracy)
-extension Array where Element : CoreLocation.CLLocation {
- public var dateInterval: Foundation.DateInterval? {
- get
- }
-}
-infix operator ≅ : ComparisonPrecedence
-infix operator • : DefaultPrecedence
-public struct LocoKitService {
- public static var apiKey: Swift.String? {
- get
- set(key)
- }
- public static var deviceToken: Foundation.Data?
- public static var requestedWakeupCall: Foundation.Date? {
- get
- }
- public static var requestingWakeupCall: Swift.Bool {
- get
- }
- @discardableResult
- public static func requestWakeup(at requestedDate: Foundation.Date) -> Swift.Bool
- public static func fetchModelsFor(coordinate: CoreLocation.CLLocationCoordinate2D, depth: Swift.Int, completion: @escaping ([Swift.String : Any]?) -> Swift.Void)
-}
-extension Date {
- public var startOfDay: Foundation.Date {
- get
- }
- public var sinceStartOfDay: Foundation.TimeInterval {
- get
- }
- public var age: Foundation.TimeInterval {
- get
- }
-}
-extension LocoKitCore.CoreMotionActivityTypeName : Swift.Equatable {}
-extension LocoKitCore.CoreMotionActivityTypeName : Swift.Hashable {}
-extension LocoKitCore.CoreMotionActivityTypeName : Swift.RawRepresentable {}
-extension LocoKitCore.MovingState : Swift.Hashable {}
-extension LocoKitCore.MovingState : Swift.RawRepresentable {}
-extension LocoKitCore.PThreadMutex.PThreadMutexType : Swift.Equatable {}
-extension LocoKitCore.PThreadMutex.PThreadMutexType : Swift.Hashable {}
diff --git a/LocoKitCore.framework/Modules/LocoKitCore.swiftmodule/i386-apple-ios-simulator.swiftmodule b/LocoKitCore.framework/Modules/LocoKitCore.swiftmodule/i386-apple-ios-simulator.swiftmodule
deleted file mode 100644
index 281d2fb5..00000000
Binary files a/LocoKitCore.framework/Modules/LocoKitCore.swiftmodule/i386-apple-ios-simulator.swiftmodule and /dev/null differ
diff --git a/LocoKitCore.framework/Modules/LocoKitCore.swiftmodule/i386.swiftdoc b/LocoKitCore.framework/Modules/LocoKitCore.swiftmodule/i386.swiftdoc
deleted file mode 100644
index af8b0c6a..00000000
Binary files a/LocoKitCore.framework/Modules/LocoKitCore.swiftmodule/i386.swiftdoc and /dev/null differ
diff --git a/LocoKitCore.framework/Modules/LocoKitCore.swiftmodule/i386.swiftinterface b/LocoKitCore.framework/Modules/LocoKitCore.swiftmodule/i386.swiftinterface
deleted file mode 100644
index a12bd94b..00000000
--- a/LocoKitCore.framework/Modules/LocoKitCore.swiftmodule/i386.swiftinterface
+++ /dev/null
@@ -1,207 +0,0 @@
-// swift-interface-format-version: 1.0
-// swift-compiler-version: Apple Swift version 5.1.3 effective-4.2 (swiftlang-1100.0.282.1 clang-1100.0.33.15)
-// 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
-import Accelerate
-import CoreLocation
-import CoreMotion
-import Darwin
-import Foundation
-@_exported import LocoKitCore
-import Swift
-import os.log
-import os
-public enum CoreMotionActivityTypeName : Swift.String, Swift.Codable {
- case unknown
- case stationary
- case automotive
- case walking
- case running
- case cycling
- public static let allTypes: [LocoKitCore.CoreMotionActivityTypeName]
- public typealias RawValue = Swift.String
- public init?(rawValue: Swift.String)
- public var rawValue: Swift.String {
- get
- }
-}
-public class ActivityBrain {
- public var processHistoricalLocations: Swift.Bool
- public static let highlander: LocoKitCore.ActivityBrain
- public var presentSample: LocoKitCore.ActivityBrainSample {
- get
- set
- }
- public var stationaryPeriodStart: Foundation.Date?
- @objc deinit
-}
-extension ActivityBrain {
- public static var historicalLocationsBrain: LocoKitCore.ActivityBrain {
- get
- }
- public func add(rawLocation location: CoreLocation.CLLocation, trustFactor: Swift.Double? = nil)
- public func update()
- public func freezeTheBrain()
- public var movingState: LocoKitCore.MovingState {
- get
- }
- public var horizontalAccuracy: Swift.Double {
- get
- }
- public var kalmanLocation: CoreLocation.CLLocation? {
- get
- }
- public func resetKalmans()
- public var kalmanRequiredN: Swift.Double {
- get
- }
- public var speedRequiredN: Swift.Double {
- get
- }
- public var requiredN: Swift.Int {
- get
- }
- public var dynamicMinimumConfidenceN: Swift.Int {
- get
- }
- public func spread(_ locations: [CoreLocation.CLLocation]) -> Foundation.TimeInterval
- public func add(pedoData: CoreMotion.CMPedometerData)
- public func add(deviceMotion: CoreMotion.CMDeviceMotion)
- public func add(cmMotionActivity activity: CoreMotion.CMMotionActivity)
-}
-public enum MovingState : Swift.String, Swift.Codable {
- case moving
- case stationary
- case uncertain
- public typealias RawValue = Swift.String
- public init?(rawValue: Swift.String)
- public var rawValue: Swift.String {
- get
- }
-}
-public class ActivityBrainSample {
- public var movingState: LocoKitCore.MovingState
- public var rawLocations: [CoreLocation.CLLocation] {
- get
- }
- public var filteredLocations: [CoreLocation.CLLocation] {
- get
- }
- public var date: Foundation.Date {
- get
- }
- public var location: CoreLocation.CLLocation? {
- get
- }
- public var stepHz: Swift.Double? {
- get
- }
- public var coreMotionActivityType: LocoKitCore.CoreMotionActivityTypeName? {
- get
- }
- public var speed: CoreLocation.CLLocationSpeed {
- get
- }
- public var course: CoreLocation.CLLocationDirection {
- get
- }
- public var courseVariance: Swift.Double? {
- get
- }
- public var xyAcceleration: Swift.Double? {
- get
- }
- public var zAcceleration: Swift.Double? {
- get
- }
- @objc deinit
-}
-postfix operator ′
-public protocol ScopedMutex {
- @discardableResult
- func sync(execute work: () throws -> R) rethrows -> R
- @discardableResult
- func trySync(execute work: () throws -> R) rethrows -> R?
-}
-public protocol RawMutex : LocoKitCore.ScopedMutex {
- associatedtype MutexPrimitive
- var unsafeMutex: Self.MutexPrimitive { get set }
- func unbalancedLock()
- func unbalancedTryLock() -> Swift.Bool
- func unbalancedUnlock()
-}
-extension RawMutex {
- @discardableResult
- public func sync(execute work: () throws -> R) rethrows -> R
- @discardableResult
- public func trySync(execute work: () throws -> R) rethrows -> R?
-}
-final public class PThreadMutex : LocoKitCore.RawMutex {
- public typealias MutexPrimitive = Darwin.pthread_mutex_t
- public enum PThreadMutexType {
- case normal
- case recursive
- public static func == (a: LocoKitCore.PThreadMutex.PThreadMutexType, b: LocoKitCore.PThreadMutex.PThreadMutexType) -> Swift.Bool
- public var hashValue: Swift.Int {
- get
- }
- public func hash(into hasher: inout Swift.Hasher)
- }
- final public var unsafeMutex: Darwin.pthread_mutex_t
- public init(type: LocoKitCore.PThreadMutex.PThreadMutexType = .normal)
- @objc deinit
- final public func unbalancedLock()
- final public func unbalancedTryLock() -> Swift.Bool
- final public func unbalancedUnlock()
-}
-final public class UnfairLock : LocoKitCore.RawMutex {
- public typealias MutexPrimitive = Darwin.os_unfair_lock
- public init()
- final public var unsafeMutex: Darwin.os_unfair_lock
- final public func unbalancedLock()
- final public func unbalancedTryLock() -> Swift.Bool
- final public func unbalancedUnlock()
- @objc deinit
-}
-public typealias Radians = Swift.Double
-public typealias AccuracyRange = (best: CoreLocation.CLLocationAccuracy, worst: CoreLocation.CLLocationAccuracy)
-extension Array where Element : CoreLocation.CLLocation {
- public var dateInterval: Foundation.DateInterval? {
- get
- }
-}
-infix operator ≅ : ComparisonPrecedence
-infix operator • : DefaultPrecedence
-public struct LocoKitService {
- public static var apiKey: Swift.String? {
- get
- set(key)
- }
- public static var deviceToken: Foundation.Data?
- public static var requestedWakeupCall: Foundation.Date? {
- get
- }
- public static var requestingWakeupCall: Swift.Bool {
- get
- }
- @discardableResult
- public static func requestWakeup(at requestedDate: Foundation.Date) -> Swift.Bool
- public static func fetchModelsFor(coordinate: CoreLocation.CLLocationCoordinate2D, depth: Swift.Int, completion: @escaping ([Swift.String : Any]?) -> Swift.Void)
-}
-extension Date {
- public var startOfDay: Foundation.Date {
- get
- }
- public var sinceStartOfDay: Foundation.TimeInterval {
- get
- }
- public var age: Foundation.TimeInterval {
- get
- }
-}
-extension LocoKitCore.CoreMotionActivityTypeName : Swift.Equatable {}
-extension LocoKitCore.CoreMotionActivityTypeName : Swift.Hashable {}
-extension LocoKitCore.CoreMotionActivityTypeName : Swift.RawRepresentable {}
-extension LocoKitCore.MovingState : Swift.Hashable {}
-extension LocoKitCore.MovingState : Swift.RawRepresentable {}
-extension LocoKitCore.PThreadMutex.PThreadMutexType : Swift.Equatable {}
-extension LocoKitCore.PThreadMutex.PThreadMutexType : Swift.Hashable {}
diff --git a/LocoKitCore.framework/Modules/LocoKitCore.swiftmodule/i386.swiftmodule b/LocoKitCore.framework/Modules/LocoKitCore.swiftmodule/i386.swiftmodule
deleted file mode 100644
index 281d2fb5..00000000
Binary files a/LocoKitCore.framework/Modules/LocoKitCore.swiftmodule/i386.swiftmodule and /dev/null differ
diff --git a/LocoKitCore.framework/Modules/LocoKitCore.swiftmodule/x86_64-apple-ios-simulator.swiftdoc b/LocoKitCore.framework/Modules/LocoKitCore.swiftmodule/x86_64-apple-ios-simulator.swiftdoc
deleted file mode 100644
index 7bbc9f57..00000000
Binary files a/LocoKitCore.framework/Modules/LocoKitCore.swiftmodule/x86_64-apple-ios-simulator.swiftdoc and /dev/null differ
diff --git a/LocoKitCore.framework/Modules/LocoKitCore.swiftmodule/x86_64-apple-ios-simulator.swiftinterface b/LocoKitCore.framework/Modules/LocoKitCore.swiftmodule/x86_64-apple-ios-simulator.swiftinterface
deleted file mode 100644
index e78a5978..00000000
--- a/LocoKitCore.framework/Modules/LocoKitCore.swiftmodule/x86_64-apple-ios-simulator.swiftinterface
+++ /dev/null
@@ -1,207 +0,0 @@
-// swift-interface-format-version: 1.0
-// swift-compiler-version: Apple Swift version 5.1.3 effective-4.2 (swiftlang-1100.0.282.1 clang-1100.0.33.15)
-// swift-module-flags: -target x86_64-apple-ios10.0-simulator -enable-objc-interop -enable-library-evolution -swift-version 4.2 -enforce-exclusivity=checked -O -module-name LocoKitCore
-import Accelerate
-import CoreLocation
-import CoreMotion
-import Darwin
-import Foundation
-@_exported import LocoKitCore
-import Swift
-import os.log
-import os
-public enum CoreMotionActivityTypeName : Swift.String, Swift.Codable {
- case unknown
- case stationary
- case automotive
- case walking
- case running
- case cycling
- public static let allTypes: [LocoKitCore.CoreMotionActivityTypeName]
- public typealias RawValue = Swift.String
- public init?(rawValue: Swift.String)
- public var rawValue: Swift.String {
- get
- }
-}
-public class ActivityBrain {
- public var processHistoricalLocations: Swift.Bool
- public static let highlander: LocoKitCore.ActivityBrain
- public var presentSample: LocoKitCore.ActivityBrainSample {
- get
- set
- }
- public var stationaryPeriodStart: Foundation.Date?
- @objc deinit
-}
-extension ActivityBrain {
- public static var historicalLocationsBrain: LocoKitCore.ActivityBrain {
- get
- }
- public func add(rawLocation location: CoreLocation.CLLocation, trustFactor: Swift.Double? = nil)
- public func update()
- public func freezeTheBrain()
- public var movingState: LocoKitCore.MovingState {
- get
- }
- public var horizontalAccuracy: Swift.Double {
- get
- }
- public var kalmanLocation: CoreLocation.CLLocation? {
- get
- }
- public func resetKalmans()
- public var kalmanRequiredN: Swift.Double {
- get
- }
- public var speedRequiredN: Swift.Double {
- get
- }
- public var requiredN: Swift.Int {
- get
- }
- public var dynamicMinimumConfidenceN: Swift.Int {
- get
- }
- public func spread(_ locations: [CoreLocation.CLLocation]) -> Foundation.TimeInterval
- public func add(pedoData: CoreMotion.CMPedometerData)
- public func add(deviceMotion: CoreMotion.CMDeviceMotion)
- public func add(cmMotionActivity activity: CoreMotion.CMMotionActivity)
-}
-public enum MovingState : Swift.String, Swift.Codable {
- case moving
- case stationary
- case uncertain
- public typealias RawValue = Swift.String
- public init?(rawValue: Swift.String)
- public var rawValue: Swift.String {
- get
- }
-}
-public class ActivityBrainSample {
- public var movingState: LocoKitCore.MovingState
- public var rawLocations: [CoreLocation.CLLocation] {
- get
- }
- public var filteredLocations: [CoreLocation.CLLocation] {
- get
- }
- public var date: Foundation.Date {
- get
- }
- public var location: CoreLocation.CLLocation? {
- get
- }
- public var stepHz: Swift.Double? {
- get
- }
- public var coreMotionActivityType: LocoKitCore.CoreMotionActivityTypeName? {
- get
- }
- public var speed: CoreLocation.CLLocationSpeed {
- get
- }
- public var course: CoreLocation.CLLocationDirection {
- get
- }
- public var courseVariance: Swift.Double? {
- get
- }
- public var xyAcceleration: Swift.Double? {
- get
- }
- public var zAcceleration: Swift.Double? {
- get
- }
- @objc deinit
-}
-postfix operator ′
-public protocol ScopedMutex {
- @discardableResult
- func sync(execute work: () throws -> R) rethrows -> R
- @discardableResult
- func trySync(execute work: () throws -> R) rethrows -> R?
-}
-public protocol RawMutex : LocoKitCore.ScopedMutex {
- associatedtype MutexPrimitive
- var unsafeMutex: Self.MutexPrimitive { get set }
- func unbalancedLock()
- func unbalancedTryLock() -> Swift.Bool
- func unbalancedUnlock()
-}
-extension RawMutex {
- @discardableResult
- public func sync(execute work: () throws -> R) rethrows -> R
- @discardableResult
- public func trySync(execute work: () throws -> R) rethrows -> R?
-}
-final public class PThreadMutex : LocoKitCore.RawMutex {
- public typealias MutexPrimitive = Darwin.pthread_mutex_t
- public enum PThreadMutexType {
- case normal
- case recursive
- public static func == (a: LocoKitCore.PThreadMutex.PThreadMutexType, b: LocoKitCore.PThreadMutex.PThreadMutexType) -> Swift.Bool
- public var hashValue: Swift.Int {
- get
- }
- public func hash(into hasher: inout Swift.Hasher)
- }
- final public var unsafeMutex: Darwin.pthread_mutex_t
- public init(type: LocoKitCore.PThreadMutex.PThreadMutexType = .normal)
- @objc deinit
- final public func unbalancedLock()
- final public func unbalancedTryLock() -> Swift.Bool
- final public func unbalancedUnlock()
-}
-final public class UnfairLock : LocoKitCore.RawMutex {
- public typealias MutexPrimitive = Darwin.os_unfair_lock
- public init()
- final public var unsafeMutex: Darwin.os_unfair_lock
- final public func unbalancedLock()
- final public func unbalancedTryLock() -> Swift.Bool
- final public func unbalancedUnlock()
- @objc deinit
-}
-public typealias Radians = Swift.Double
-public typealias AccuracyRange = (best: CoreLocation.CLLocationAccuracy, worst: CoreLocation.CLLocationAccuracy)
-extension Array where Element : CoreLocation.CLLocation {
- public var dateInterval: Foundation.DateInterval? {
- get
- }
-}
-infix operator ≅ : ComparisonPrecedence
-infix operator • : DefaultPrecedence
-public struct LocoKitService {
- public static var apiKey: Swift.String? {
- get
- set(key)
- }
- public static var deviceToken: Foundation.Data?
- public static var requestedWakeupCall: Foundation.Date? {
- get
- }
- public static var requestingWakeupCall: Swift.Bool {
- get
- }
- @discardableResult
- public static func requestWakeup(at requestedDate: Foundation.Date) -> Swift.Bool
- public static func fetchModelsFor(coordinate: CoreLocation.CLLocationCoordinate2D, depth: Swift.Int, completion: @escaping ([Swift.String : Any]?) -> Swift.Void)
-}
-extension Date {
- public var startOfDay: Foundation.Date {
- get
- }
- public var sinceStartOfDay: Foundation.TimeInterval {
- get
- }
- public var age: Foundation.TimeInterval {
- get
- }
-}
-extension LocoKitCore.CoreMotionActivityTypeName : Swift.Equatable {}
-extension LocoKitCore.CoreMotionActivityTypeName : Swift.Hashable {}
-extension LocoKitCore.CoreMotionActivityTypeName : Swift.RawRepresentable {}
-extension LocoKitCore.MovingState : Swift.Hashable {}
-extension LocoKitCore.MovingState : Swift.RawRepresentable {}
-extension LocoKitCore.PThreadMutex.PThreadMutexType : Swift.Equatable {}
-extension LocoKitCore.PThreadMutex.PThreadMutexType : Swift.Hashable {}
diff --git a/LocoKitCore.framework/Modules/LocoKitCore.swiftmodule/x86_64-apple-ios-simulator.swiftmodule b/LocoKitCore.framework/Modules/LocoKitCore.swiftmodule/x86_64-apple-ios-simulator.swiftmodule
deleted file mode 100644
index aa41fcb5..00000000
Binary files a/LocoKitCore.framework/Modules/LocoKitCore.swiftmodule/x86_64-apple-ios-simulator.swiftmodule and /dev/null differ
diff --git a/LocoKitCore.framework/Modules/LocoKitCore.swiftmodule/x86_64.swiftdoc b/LocoKitCore.framework/Modules/LocoKitCore.swiftmodule/x86_64.swiftdoc
deleted file mode 100644
index 7bbc9f57..00000000
Binary files a/LocoKitCore.framework/Modules/LocoKitCore.swiftmodule/x86_64.swiftdoc and /dev/null differ
diff --git a/LocoKitCore.framework/Modules/LocoKitCore.swiftmodule/x86_64.swiftinterface b/LocoKitCore.framework/Modules/LocoKitCore.swiftmodule/x86_64.swiftinterface
deleted file mode 100644
index e78a5978..00000000
--- a/LocoKitCore.framework/Modules/LocoKitCore.swiftmodule/x86_64.swiftinterface
+++ /dev/null
@@ -1,207 +0,0 @@
-// swift-interface-format-version: 1.0
-// swift-compiler-version: Apple Swift version 5.1.3 effective-4.2 (swiftlang-1100.0.282.1 clang-1100.0.33.15)
-// swift-module-flags: -target x86_64-apple-ios10.0-simulator -enable-objc-interop -enable-library-evolution -swift-version 4.2 -enforce-exclusivity=checked -O -module-name LocoKitCore
-import Accelerate
-import CoreLocation
-import CoreMotion
-import Darwin
-import Foundation
-@_exported import LocoKitCore
-import Swift
-import os.log
-import os
-public enum CoreMotionActivityTypeName : Swift.String, Swift.Codable {
- case unknown
- case stationary
- case automotive
- case walking
- case running
- case cycling
- public static let allTypes: [LocoKitCore.CoreMotionActivityTypeName]
- public typealias RawValue = Swift.String
- public init?(rawValue: Swift.String)
- public var rawValue: Swift.String {
- get
- }
-}
-public class ActivityBrain {
- public var processHistoricalLocations: Swift.Bool
- public static let highlander: LocoKitCore.ActivityBrain
- public var presentSample: LocoKitCore.ActivityBrainSample {
- get
- set
- }
- public var stationaryPeriodStart: Foundation.Date?
- @objc deinit
-}
-extension ActivityBrain {
- public static var historicalLocationsBrain: LocoKitCore.ActivityBrain {
- get
- }
- public func add(rawLocation location: CoreLocation.CLLocation, trustFactor: Swift.Double? = nil)
- public func update()
- public func freezeTheBrain()
- public var movingState: LocoKitCore.MovingState {
- get
- }
- public var horizontalAccuracy: Swift.Double {
- get
- }
- public var kalmanLocation: CoreLocation.CLLocation? {
- get
- }
- public func resetKalmans()
- public var kalmanRequiredN: Swift.Double {
- get
- }
- public var speedRequiredN: Swift.Double {
- get
- }
- public var requiredN: Swift.Int {
- get
- }
- public var dynamicMinimumConfidenceN: Swift.Int {
- get
- }
- public func spread(_ locations: [CoreLocation.CLLocation]) -> Foundation.TimeInterval
- public func add(pedoData: CoreMotion.CMPedometerData)
- public func add(deviceMotion: CoreMotion.CMDeviceMotion)
- public func add(cmMotionActivity activity: CoreMotion.CMMotionActivity)
-}
-public enum MovingState : Swift.String, Swift.Codable {
- case moving
- case stationary
- case uncertain
- public typealias RawValue = Swift.String
- public init?(rawValue: Swift.String)
- public var rawValue: Swift.String {
- get
- }
-}
-public class ActivityBrainSample {
- public var movingState: LocoKitCore.MovingState
- public var rawLocations: [CoreLocation.CLLocation] {
- get
- }
- public var filteredLocations: [CoreLocation.CLLocation] {
- get
- }
- public var date: Foundation.Date {
- get
- }
- public var location: CoreLocation.CLLocation? {
- get
- }
- public var stepHz: Swift.Double? {
- get
- }
- public var coreMotionActivityType: LocoKitCore.CoreMotionActivityTypeName? {
- get
- }
- public var speed: CoreLocation.CLLocationSpeed {
- get
- }
- public var course: CoreLocation.CLLocationDirection {
- get
- }
- public var courseVariance: Swift.Double? {
- get
- }
- public var xyAcceleration: Swift.Double? {
- get
- }
- public var zAcceleration: Swift.Double? {
- get
- }
- @objc deinit
-}
-postfix operator ′
-public protocol ScopedMutex {
- @discardableResult
- func sync(execute work: () throws -> R) rethrows -> R
- @discardableResult
- func trySync(execute work: () throws -> R) rethrows -> R?
-}
-public protocol RawMutex : LocoKitCore.ScopedMutex {
- associatedtype MutexPrimitive
- var unsafeMutex: Self.MutexPrimitive { get set }
- func unbalancedLock()
- func unbalancedTryLock() -> Swift.Bool
- func unbalancedUnlock()
-}
-extension RawMutex {
- @discardableResult
- public func sync(execute work: () throws -> R) rethrows -> R
- @discardableResult
- public func trySync(execute work: () throws -> R) rethrows -> R?
-}
-final public class PThreadMutex : LocoKitCore.RawMutex {
- public typealias MutexPrimitive = Darwin.pthread_mutex_t
- public enum PThreadMutexType {
- case normal
- case recursive
- public static func == (a: LocoKitCore.PThreadMutex.PThreadMutexType, b: LocoKitCore.PThreadMutex.PThreadMutexType) -> Swift.Bool
- public var hashValue: Swift.Int {
- get
- }
- public func hash(into hasher: inout Swift.Hasher)
- }
- final public var unsafeMutex: Darwin.pthread_mutex_t
- public init(type: LocoKitCore.PThreadMutex.PThreadMutexType = .normal)
- @objc deinit
- final public func unbalancedLock()
- final public func unbalancedTryLock() -> Swift.Bool
- final public func unbalancedUnlock()
-}
-final public class UnfairLock : LocoKitCore.RawMutex {
- public typealias MutexPrimitive = Darwin.os_unfair_lock
- public init()
- final public var unsafeMutex: Darwin.os_unfair_lock
- final public func unbalancedLock()
- final public func unbalancedTryLock() -> Swift.Bool
- final public func unbalancedUnlock()
- @objc deinit
-}
-public typealias Radians = Swift.Double
-public typealias AccuracyRange = (best: CoreLocation.CLLocationAccuracy, worst: CoreLocation.CLLocationAccuracy)
-extension Array where Element : CoreLocation.CLLocation {
- public var dateInterval: Foundation.DateInterval? {
- get
- }
-}
-infix operator ≅ : ComparisonPrecedence
-infix operator • : DefaultPrecedence
-public struct LocoKitService {
- public static var apiKey: Swift.String? {
- get
- set(key)
- }
- public static var deviceToken: Foundation.Data?
- public static var requestedWakeupCall: Foundation.Date? {
- get
- }
- public static var requestingWakeupCall: Swift.Bool {
- get
- }
- @discardableResult
- public static func requestWakeup(at requestedDate: Foundation.Date) -> Swift.Bool
- public static func fetchModelsFor(coordinate: CoreLocation.CLLocationCoordinate2D, depth: Swift.Int, completion: @escaping ([Swift.String : Any]?) -> Swift.Void)
-}
-extension Date {
- public var startOfDay: Foundation.Date {
- get
- }
- public var sinceStartOfDay: Foundation.TimeInterval {
- get
- }
- public var age: Foundation.TimeInterval {
- get
- }
-}
-extension LocoKitCore.CoreMotionActivityTypeName : Swift.Equatable {}
-extension LocoKitCore.CoreMotionActivityTypeName : Swift.Hashable {}
-extension LocoKitCore.CoreMotionActivityTypeName : Swift.RawRepresentable {}
-extension LocoKitCore.MovingState : Swift.Hashable {}
-extension LocoKitCore.MovingState : Swift.RawRepresentable {}
-extension LocoKitCore.PThreadMutex.PThreadMutexType : Swift.Equatable {}
-extension LocoKitCore.PThreadMutex.PThreadMutexType : Swift.Hashable {}
diff --git a/LocoKitCore.framework/Modules/LocoKitCore.swiftmodule/x86_64.swiftmodule b/LocoKitCore.framework/Modules/LocoKitCore.swiftmodule/x86_64.swiftmodule
deleted file mode 100644
index aa41fcb5..00000000
Binary files a/LocoKitCore.framework/Modules/LocoKitCore.swiftmodule/x86_64.swiftmodule and /dev/null differ
diff --git a/LocoKitCore.framework/Modules/module.modulemap b/LocoKitCore.framework/Modules/module.modulemap
deleted file mode 100644
index 7b6da710..00000000
--- a/LocoKitCore.framework/Modules/module.modulemap
+++ /dev/null
@@ -1,11 +0,0 @@
-framework module LocoKitCore {
- umbrella header "LocoKitCore.h"
-
- export *
- module * { export * }
-}
-
-module LocoKitCore.Swift {
- header "LocoKitCore-Swift.h"
- requires objc
-}
diff --git a/LocoKitCore.podspec b/LocoKitCore.podspec
deleted file mode 100644
index 64da609f..00000000
--- a/LocoKitCore.podspec
+++ /dev/null
@@ -1,13 +0,0 @@
-Pod::Spec.new do |s|
- s.name = "LocoKitCore"
- s.version = "7.0.0"
- s.summary = "Location and activity recording framework"
- s.homepage = "https://www.bigpaua.com/locokit/"
- s.author = { "Matt Greenfield" => "matt@bigpaua.com" }
- s.license = { :text => "Copyright 2018 Matt Greenfield. All rights reserved.",
- :type => "Commercial" }
- s.source = { :git => 'https://github.com/sobri909/LocoKit.git', :tag => '7.0.0' }
- s.frameworks = 'CoreLocation', 'CoreMotion'
- s.ios.deployment_target = '10.0'
- s.ios.vendored_frameworks = 'LocoKitCore.framework'
-end
diff --git a/Package.swift b/Package.swift
index 8d5cf98a..ec9d342c 100644
--- a/Package.swift
+++ b/Package.swift
@@ -1,23 +1,23 @@
-// swift-tools-version:5.3
+// swift-tools-version:5.5
// The swift-tools-version declares the minimum version of Swift required to build this package.
import PackageDescription
let package = Package(
name: "LocoKit",
- platforms: [.iOS(.v13)],
+ defaultLocalization: "en",
+ platforms: [.iOS(.v13), .macOS(.v10_15)],
products: [
.library(name: "LocoKit", targets: ["LocoKit"])
],
dependencies: [
- .package(url: "https://github.com/alejandro-isaza/Upsurge.git", from: "0.11.0"),
- .package(name: "GRDB", url: "https://github.com/groue/GRDB.swift.git", from: "4.0.0")
],
targets: [
.target(
name: "LocoKit",
- dependencies: ["Upsurge", "GRDB"],
- path: "LocoKit"
+ dependencies: [],
+ path: "LocoKit",
+ exclude: ["Base/Strings"]
)
]
)
diff --git a/Podfile b/Podfile
deleted file mode 100644
index f48ab33c..00000000
--- a/Podfile
+++ /dev/null
@@ -1,7 +0,0 @@
-target 'LocoKit Demo App'
-platform :ios, '10.0'
-use_frameworks!
-
-pod 'LocoKit'
-pod 'SwiftNotes'
-pod 'Anchorage'
diff --git a/Podfile.lock b/Podfile.lock
deleted file mode 100644
index ca9c3f04..00000000
--- a/Podfile.lock
+++ /dev/null
@@ -1,40 +0,0 @@
-PODS:
- - Anchorage (4.4.0)
- - GRDB.swift (4.11.0):
- - GRDB.swift/standard (= 4.11.0)
- - GRDB.swift/standard (4.11.0)
- - LocoKit (7.0.0):
- - LocoKit/Base (= 7.0.0)
- - LocoKit/Base (7.0.0):
- - GRDB.swift (~> 4)
- - LocoKitCore (= 7.0.0)
- - Upsurge (~> 0.10)
- - LocoKitCore (7.0.0)
- - SwiftNotes (1.1.0)
- - Upsurge (0.10.2)
-
-DEPENDENCIES:
- - Anchorage
- - LocoKit
- - SwiftNotes
-
-SPEC REPOS:
- trunk:
- - Anchorage
- - GRDB.swift
- - LocoKit
- - LocoKitCore
- - SwiftNotes
- - Upsurge
-
-SPEC CHECKSUMS:
- Anchorage: d7f02c4f9425b537053237aab11ae97e59b55f36
- GRDB.swift: 22e9d04cb732dfa9fa4440bc0bbb069ee8195183
- LocoKit: 8a06074e90dfd24ee10e94c9f5274e300f6b9d2f
- LocoKitCore: 30cff6a1e4ac5a32eefe232c19e24fd79485661d
- SwiftNotes: 51d71568d7515c5c82aa791b88900cf8059b8beb
- Upsurge: 5866beadc3da27f91c5df4ac795deb3f3238d678
-
-PODFILE CHECKSUM: c9cb1714b431c31888efb12c1a62351506e334e2
-
-COCOAPODS: 1.9.0
diff --git a/TimelineItemDescription.md b/TimelineItemDescription.md
deleted file mode 100644
index 71451796..00000000
--- a/TimelineItemDescription.md
+++ /dev/null
@@ -1,11 +0,0 @@
-# TimelineItems
-
-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).
-
-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.
-
-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.
-
-The maximum frequency is configurable, with [TimelineManager.samplesPerMinute](https://www.bigpaua.com/locokit/docs/Classes/TimelineManager.html#/Settings)
-
-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.
\ No newline at end of file