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