From 88aaf0f320eb0ea2c51d7e3f277505c81bf85c2d Mon Sep 17 00:00:00 2001 From: 2kai2kai2 <2kai2kai2@gmail.com> Date: Sun, 18 Jun 2023 01:47:19 -0400 Subject: [PATCH 01/14] Setup XCTest Basic tests setup. Currently has just one demo test. --- apps/ios/GuideDogs.xcodeproj/project.pbxproj | 206 ++++++++++++++++++ .../xcschemes/Soundscape - Dogfood.xcscheme | 10 + .../Soundscape - Feature Flags.xcscheme | 10 + .../Soundscape - Localization.xcscheme | 10 + .../Soundscape - Screenshots.xcscheme | 10 + .../xcschemes/Soundscape.xcscheme | 22 +- .../contents.xcworkspacedata | 3 + apps/ios/UnitTests.xctestplan | 34 +++ .../App/Helpers/GeometryUtilsTest.swift | 36 +++ docs/ios-client/build-and-test/testing.md | 20 ++ 10 files changed, 358 insertions(+), 3 deletions(-) create mode 100644 apps/ios/UnitTests.xctestplan create mode 100644 apps/ios/UnitTests/App/Helpers/GeometryUtilsTest.swift create mode 100644 docs/ios-client/build-and-test/testing.md diff --git a/apps/ios/GuideDogs.xcodeproj/project.pbxproj b/apps/ios/GuideDogs.xcodeproj/project.pbxproj index 77fbc998..5805e717 100644 --- a/apps/ios/GuideDogs.xcodeproj/project.pbxproj +++ b/apps/ios/GuideDogs.xcodeproj/project.pbxproj @@ -662,6 +662,8 @@ 62F7A30C27B6080900C62390 /* InteractiveBeaconView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62F7A30B27B6080900C62390 /* InteractiveBeaconView.swift */; }; 62F7A30E27B6082A00C62390 /* InteractiveBeaconViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62F7A30D27B6082A00C62390 /* InteractiveBeaconViewModel.swift */; }; 6A4891BB2A5E66DE0002D146 /* ExternalNavigationApps.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A4891BA2A5E66DE0002D146 /* ExternalNavigationApps.swift */; }; + 91DC0CF92A46134600244CC8 /* GeometryUtilsTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 914DEBDC2A3CE901007B161C /* GeometryUtilsTest.swift */; }; + B5499F0EEDE32E16F9814457 /* Pods_Soundscape.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DAEDD72453A8268FBA604D2F /* Pods_Soundscape.framework */; }; B90C27D61EAF81D600007368 /* Sound.swift in Sources */ = {isa = PBXBuildFile; fileRef = B90C27D51EAF81D600007368 /* Sound.swift */; }; B918EE9825100FFF00A5354A /* CalloutRangeContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = B918EE9725100FFF00A5354A /* CalloutRangeContext.swift */; }; B91D3F6427AB5546004159A8 /* UserAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = B91D3F6327AB5546004159A8 /* UserAction.swift */; }; @@ -876,6 +878,16 @@ D395B0F71B41DA15005A6407 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = D395B0F61B41DA15005A6407 /* Images.xcassets */; }; /* End PBXBuildFile section */ +/* Begin PBXContainerItemProxy section */ + 91DC0CD12A423B2200244CC8 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = D3D7CFC01B3D96460020B5E9 /* Project object */; + proxyType = 1; + remoteGlobalIDString = D3D7CFC71B3D96460020B5E9; + remoteInfo = Soundscape; + }; +/* End PBXContainerItemProxy section */ + /* Begin PBXCopyFilesBuildPhase section */ B9B06E971E6483900010936A /* CopyFiles */ = { isa = PBXCopyFilesBuildPhase; @@ -1566,6 +1578,11 @@ 62F7A30D27B6082A00C62390 /* InteractiveBeaconViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InteractiveBeaconViewModel.swift; sourceTree = ""; }; 6A4891BA2A5E66DE0002D146 /* ExternalNavigationApps.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ExternalNavigationApps.swift; path = Code/App/ExternalNavigationApps.swift; sourceTree = ""; }; 815775252832F0650DF9D8D4 /* Pods-Soundscape.adhoc.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Soundscape.adhoc.xcconfig"; path = "Target Support Files/Pods-Soundscape/Pods-Soundscape.adhoc.xcconfig"; sourceTree = ""; }; + 914DEBCD2A3CE6B9007B161C /* UnitTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = UnitTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 914DEBDC2A3CE901007B161C /* GeometryUtilsTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeometryUtilsTest.swift; sourceTree = ""; }; + 91DC0CF32A460FAA00244CC8 /* iOS_GPX_Framework.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = iOS_GPX_Framework.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 91DC0CF52A460FAA00244CC8 /* Pods_Soundscape.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Pods_Soundscape.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 91DC0CF72A460FAA00244CC8 /* TBXML.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = TBXML.framework; sourceTree = BUILT_PRODUCTS_DIR; }; B90C27D51EAF81D600007368 /* Sound.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Sound.swift; path = Code/Audio/Protocols/Sound.swift; sourceTree = ""; }; B918EE9725100FFF00A5354A /* CalloutRangeContext.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CalloutRangeContext.swift; sourceTree = ""; }; B91D3F6327AB5546004159A8 /* UserAction.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UserAction.swift; sourceTree = ""; }; @@ -1803,6 +1820,13 @@ /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ + 914DEBCA2A3CE6B9007B161C /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; D3D7CFC51B3D96460020B5E9 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; @@ -2985,6 +3009,9 @@ 4AD48659F673FA58D2DF2C07 /* Frameworks */ = { isa = PBXGroup; children = ( + 91DC0CF32A460FAA00244CC8 /* iOS_GPX_Framework.framework */, + 91DC0CF52A460FAA00244CC8 /* Pods_Soundscape.framework */, + 91DC0CF72A460FAA00244CC8 /* TBXML.framework */, 280FF2FC26AF6BDF00DE9C5D /* SwiftUI.framework */, 2876EE142433FE5600B0A137 /* CoreServices.framework */, D3E2A6361CAD5CA100A5192A /* Accelerate.framework */, @@ -4252,6 +4279,30 @@ path = "Interactive View"; sourceTree = ""; }; + 914DEBCE2A3CE6B9007B161C /* UnitTests */ = { + isa = PBXGroup; + children = ( + 914DEBD92A3CE7E6007B161C /* App */, + ); + path = UnitTests; + sourceTree = ""; + }; + 914DEBD92A3CE7E6007B161C /* App */ = { + isa = PBXGroup; + children = ( + 914DEBDB2A3CE85D007B161C /* Helpers */, + ); + path = App; + sourceTree = ""; + }; + 914DEBDB2A3CE85D007B161C /* Helpers */ = { + isa = PBXGroup; + children = ( + 914DEBDC2A3CE901007B161C /* GeometryUtilsTest.swift */, + ); + path = Helpers; + sourceTree = ""; + }; 94B9789D44F5F13621A0C9B1 /* Pods */ = { isa = PBXGroup; children = ( @@ -5089,6 +5140,7 @@ children = ( D3D7CFCA1B3D96470020B5E9 /* Soundscape */, B9E5F4CE2891DBAC0019DEED /* Packages */, + 914DEBCE2A3CE6B9007B161C /* UnitTests */, 4AD48659F673FA58D2DF2C07 /* Frameworks */, D3D7CFC91B3D96470020B5E9 /* Products */, 94B9789D44F5F13621A0C9B1 /* Pods */, @@ -5099,6 +5151,7 @@ isa = PBXGroup; children = ( D3D7CFC81B3D96470020B5E9 /* Soundscape.app */, + 914DEBCD2A3CE6B9007B161C /* UnitTests.xctest */, ); name = Products; sourceTree = ""; @@ -5116,6 +5169,24 @@ /* End PBXGroup section */ /* Begin PBXNativeTarget section */ + 914DEBCC2A3CE6B9007B161C /* UnitTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 914DEBD62A3CE6BA007B161C /* Build configuration list for PBXNativeTarget "UnitTests" */; + buildPhases = ( + 914DEBC92A3CE6B9007B161C /* Sources */, + 914DEBCA2A3CE6B9007B161C /* Frameworks */, + 914DEBCB2A3CE6B9007B161C /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 91DC0CD22A423B2200244CC8 /* PBXTargetDependency */, + ); + name = UnitTests; + productName = UnitTests; + productReference = 914DEBCD2A3CE6B9007B161C /* UnitTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; D3D7CFC71B3D96460020B5E9 /* Soundscape */ = { isa = PBXNativeTarget; buildConfigurationList = D3D7CFEE1B3D96470020B5E9 /* Build configuration list for PBXNativeTarget "Soundscape" */; @@ -5161,6 +5232,10 @@ LastUpgradeCheck = 1430; ORGANIZATIONNAME = "Soundscape community"; TargetAttributes = { + 914DEBCC2A3CE6B9007B161C = { + CreatedOnToolsVersion = 13.1; + TestTargetID = D3D7CFC71B3D96460020B5E9; + }; D3D7CFC71B3D96460020B5E9 = { CreatedOnToolsVersion = 6.3.2; LastSwiftMigration = 1020; @@ -5223,11 +5298,19 @@ projectRoot = ""; targets = ( D3D7CFC71B3D96460020B5E9 /* Soundscape */, + 914DEBCC2A3CE6B9007B161C /* UnitTests */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ + 914DEBCB2A3CE6B9007B161C /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; D3D7CFC61B3D96460020B5E9 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; @@ -5428,6 +5511,14 @@ /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ + 914DEBC92A3CE6B9007B161C /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 91DC0CF92A46134600244CC8 /* GeometryUtilsTest.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; D3D7CFC41B3D96460020B5E9 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; @@ -6166,6 +6257,14 @@ }; /* End PBXSourcesBuildPhase section */ +/* Begin PBXTargetDependency section */ + 91DC0CD22A423B2200244CC8 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = D3D7CFC71B3D96460020B5E9 /* Soundscape */; + targetProxy = 91DC0CD12A423B2200244CC8 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + /* Begin PBXVariantGroup section */ 316B3F6E2283924E0025526D /* InfoPlist.strings */ = { isa = PBXVariantGroup; @@ -6341,6 +6440,103 @@ }; name = AdHoc; }; + 914DEBD32A3CE6B9007B161C /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = dwarf; + GCC_C_LANGUAGE_STANDARD = gnu11; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 14.1; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = com.github.rcos.soundscape.UnitTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Soundscape.app/Soundscape"; + }; + name = Debug; + }; + 914DEBD42A3CE6B9007B161C /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GCC_C_LANGUAGE_STANDARD = gnu11; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 14.1; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = com.github.rcos.soundscape.UnitTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Soundscape.app/Soundscape"; + }; + name = Release; + }; + 914DEBD52A3CE6B9007B161C /* AdHoc */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GCC_C_LANGUAGE_STANDARD = gnu11; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 14.1; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = com.github.rcos.soundscape.UnitTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Soundscape.app/Soundscape"; + }; + name = AdHoc; + }; D3D7CFEC1B3D96470020B5E9 /* Debug */ = { isa = XCBuildConfiguration; baseConfigurationReference = 62E2B87C25AE5C0F0008AF5D /* Debug.xcconfig */; @@ -6601,6 +6797,16 @@ /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ + 914DEBD62A3CE6BA007B161C /* Build configuration list for PBXNativeTarget "UnitTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 914DEBD32A3CE6B9007B161C /* Debug */, + 914DEBD42A3CE6B9007B161C /* Release */, + 914DEBD52A3CE6B9007B161C /* AdHoc */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; D3D7CFC31B3D96460020B5E9 /* Build configuration list for PBXProject "GuideDogs" */ = { isa = XCConfigurationList; buildConfigurations = ( diff --git a/apps/ios/GuideDogs.xcodeproj/xcshareddata/xcschemes/Soundscape - Dogfood.xcscheme b/apps/ios/GuideDogs.xcodeproj/xcshareddata/xcschemes/Soundscape - Dogfood.xcscheme index f301a00d..99f1e51d 100644 --- a/apps/ios/GuideDogs.xcodeproj/xcshareddata/xcschemes/Soundscape - Dogfood.xcscheme +++ b/apps/ios/GuideDogs.xcodeproj/xcshareddata/xcschemes/Soundscape - Dogfood.xcscheme @@ -108,6 +108,16 @@ + + + + + + + + + + + + + + + + @@ -56,7 +56,23 @@ isEnabled = "YES"> + + + + + + + + + + diff --git a/apps/ios/UnitTests.xctestplan b/apps/ios/UnitTests.xctestplan new file mode 100644 index 00000000..57c821e9 --- /dev/null +++ b/apps/ios/UnitTests.xctestplan @@ -0,0 +1,34 @@ +{ + "configurations" : [ + { + "id" : "5D2C2CC2-30EC-422E-AE1D-2F61654BFB02", + "name" : "Configuration 1", + "options" : { + + } + } + ], + "defaultOptions" : { + "codeCoverage" : false, + "commandLineArgumentEntries" : [ + { + "argument" : "-TESTING" + } + ], + "targetForVariableExpansion" : { + "containerPath" : "container:GuideDogs.xcodeproj", + "identifier" : "D3D7CFC71B3D96460020B5E9", + "name" : "Soundscape" + } + }, + "testTargets" : [ + { + "target" : { + "containerPath" : "container:GuideDogs.xcodeproj", + "identifier" : "914DEBCC2A3CE6B9007B161C", + "name" : "UnitTests" + } + } + ], + "version" : 1 +} diff --git a/apps/ios/UnitTests/App/Helpers/GeometryUtilsTest.swift b/apps/ios/UnitTests/App/Helpers/GeometryUtilsTest.swift new file mode 100644 index 00000000..4a888798 --- /dev/null +++ b/apps/ios/UnitTests/App/Helpers/GeometryUtilsTest.swift @@ -0,0 +1,36 @@ +// +// GeometryUtilsTest.swift +// UnitTests +// +// Created by Kai on 6/16/23. +// Copyright © 2023 Microsoft. All rights reserved. +// + +import XCTest +import CoreLocation +@testable import Soundscape + +class GeometryUtilsTest: XCTestCase { + + override func setUpWithError() throws { + // Put setup code here. This method is called before the invocation of each test method in the class. + } + + override func tearDownWithError() throws { + // Put teardown code here. This method is called after the invocation of each test method in the class. + } + + func testExample() throws { + // This is an example of a functional test case. + // Use XCTAssert and related functions to verify your tests produce the correct results. + XCTAssert(Soundscape.GeometryUtils.geometryContainsLocation(location: CLLocationCoordinate2D.init(latitude: 1, longitude: 1), coordinates: [CLLocationCoordinate2D.init(latitude: 1, longitude: 1), CLLocationCoordinate2D.init(latitude: 3, longitude: 3)])) + } + + func testPerformanceExample() throws { + // This is an example of a performance test case. + self.measure { + // Put the code you want to measure the time of here. + } + } + +} diff --git a/docs/ios-client/build-and-test/testing.md b/docs/ios-client/build-and-test/testing.md new file mode 100644 index 00000000..c9cb261f --- /dev/null +++ b/docs/ios-client/build-and-test/testing.md @@ -0,0 +1,20 @@ +# Testing +Note that this file may only be relevant to the default `Soundscape` scheme (not necessarily others such as `Soundscape - Dogfood`). + +## Running Tests +### Running Tests via GitHub Actions +*This is currently planned but not yet implemented.* + +### Manually Running Tests +To manually run tests in XCode, go to **Product** → **Test** (`⌘U`) or click and hold down the run button to show more options and select **Test**. This will build the project and run the test plan. + +## Creating Tests +Go to the Tests Navigator tab (`⌘6`) to see all tests. Tests may be added to existing files, and new test classes may be added by right clicking on the Test Navigator or clicking the `+` icon in the bottom-left of the Test Navigator. The organization of tests is detailed below. + +## Test Plan & Organization +Separate test plans are included for each type of test *(currently only unit tests)*: + +### Unit Tests +Settings for the unit test plan are included in `apps/ios/UnitTests.xctestplan`. It runs the `UnitTests` test target which are located in `apps/ios/UnitTests/`. The tests are run on the Soundscape app target. + +Within the unit tests directory, the file structure reflects the `apps/ios/GuideDogs/Code` file structure with tests for the corresponding files. \ No newline at end of file From a5d18d3cb9428b7b44a20d35f4d2f451ba75340e Mon Sep 17 00:00:00 2001 From: 2kai2kai2 <2kai2kai2@gmail.com> Date: Fri, 23 Jun 2023 14:37:31 -0400 Subject: [PATCH 02/14] Automate testing via GitHub Actions --- docs/ios-client/build-and-test/testing.md | 3 ++- docs/ios-client/onboarding.md | 13 ++++++++----- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/docs/ios-client/build-and-test/testing.md b/docs/ios-client/build-and-test/testing.md index c9cb261f..04c0dc78 100644 --- a/docs/ios-client/build-and-test/testing.md +++ b/docs/ios-client/build-and-test/testing.md @@ -3,7 +3,8 @@ Note that this file may only be relevant to the default `Soundscape` scheme (not ## Running Tests ### Running Tests via GitHub Actions -*This is currently planned but not yet implemented.* +GitHub Runners should run the iOS tests on each *push* or *pull request* to `main`. +This is configured in `.github/workflows/ios-tests.yml` ### Manually Running Tests To manually run tests in XCode, go to **Product** → **Test** (`⌘U`) or click and hold down the run button to show more options and select **Test**. This will build the project and run the test plan. diff --git a/docs/ios-client/onboarding.md b/docs/ios-client/onboarding.md index bad3ef29..75ca738b 100644 --- a/docs/ios-client/onboarding.md +++ b/docs/ios-client/onboarding.md @@ -20,18 +20,21 @@ Download Xcode from the [App Store](https://apps.apple.com/us/app/xcode/id497799 ## Install Xcode Command Line Tools -Open Xcode and you should be prompted with installing the command line tools, or run this in a Terminal window: +Open Xcode and you should be prompted with installing the command line tools, or run this in a Terminal window: ```sh xcode-select --install ``` +## Install Ruby + +_Note:_ while macOS comes with a version of Ruby installed, you should install and use a non-system [Ruby](https://www.ruby-lang.org/) +using a version manager like [RVM](https://rvm.io/) + ## Install CocoaPods and CocoaPods-Patch Soundscape uses [CocoaPods](https://cocoapods.org/) as a dependency managers along with [Swift Package Manager](https://www.swift.org/package-manager/), and [CocoaPods-Patch](https://github.com/DoubleSymmetry/cocoapods-patch) to add changes to a third party CocoaPods framework. -_Note:_ before the next step, make sure you have [Ruby](https://www.ruby-lang.org/) installed on your machine. - In the iOS project folder, run the following command to install the dependencies from the `Gemfile`: ```sh @@ -40,7 +43,7 @@ bundle install ## Install CocoaPods Dependencies -Install the CocoaPods dependencies by running the following command in Terminal: +Install the CocoaPods dependencies by running the following command in Terminal from the iOS project folder: ```sh pod install @@ -76,7 +79,7 @@ Soundscape uses a backend service to download map tiles and other information. I ## Building and Running -At this point, you should be able to build and run the `Soundsacpe` target on an iOS simulator. In order to run the app on a real device, you will need to add your Apple Developer account signing info in the _Signing & Capabilities_ section of the project settings. +At this point, you should be able to build and run the `Soundscape` target on an iOS simulator. In order to run the app on a real device, you will need to add your Apple Developer account signing info in the _Signing & Capabilities_ section of the project settings. ## Additional Personalization From 8407c0f4455b35fd05aea2a720e32a1734ada130 Mon Sep 17 00:00:00 2001 From: 2kai2kai2 <2kai2kai2@gmail.com> Date: Tue, 18 Jul 2023 14:25:45 -0400 Subject: [PATCH 03/14] Add GeolocationManagerTest Tests for the main location-related sensor manager class --- apps/ios/GuideDogs.xcodeproj/project.pbxproj | 29 ++++ apps/ios/UnitTests.xctestplan | 3 +- .../ GeolocationManagerTest.swift | 163 ++++++++++++++++++ 3 files changed, 194 insertions(+), 1 deletion(-) create mode 100644 apps/ios/UnitTests/Sensors/Geolocation/Geolocation Manager/ GeolocationManagerTest.swift diff --git a/apps/ios/GuideDogs.xcodeproj/project.pbxproj b/apps/ios/GuideDogs.xcodeproj/project.pbxproj index 5805e717..d1ea2198 100644 --- a/apps/ios/GuideDogs.xcodeproj/project.pbxproj +++ b/apps/ios/GuideDogs.xcodeproj/project.pbxproj @@ -662,6 +662,7 @@ 62F7A30C27B6080900C62390 /* InteractiveBeaconView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62F7A30B27B6080900C62390 /* InteractiveBeaconView.swift */; }; 62F7A30E27B6082A00C62390 /* InteractiveBeaconViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62F7A30D27B6082A00C62390 /* InteractiveBeaconViewModel.swift */; }; 6A4891BB2A5E66DE0002D146 /* ExternalNavigationApps.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A4891BA2A5E66DE0002D146 /* ExternalNavigationApps.swift */; }; + 91C82AAD2A5DCF040086D126 /* GeolocationManagerTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 91C82AAC2A5DCF040086D126 /* GeolocationManagerTest.swift */; }; 91DC0CF92A46134600244CC8 /* GeometryUtilsTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 914DEBDC2A3CE901007B161C /* GeometryUtilsTest.swift */; }; B5499F0EEDE32E16F9814457 /* Pods_Soundscape.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DAEDD72453A8268FBA604D2F /* Pods_Soundscape.framework */; }; B90C27D61EAF81D600007368 /* Sound.swift in Sources */ = {isa = PBXBuildFile; fileRef = B90C27D51EAF81D600007368 /* Sound.swift */; }; @@ -876,6 +877,7 @@ D29832961E4E249700352A5A /* GeoJsonGeometry.swift in Sources */ = {isa = PBXBuildFile; fileRef = D29832951E4E249700352A5A /* GeoJsonGeometry.swift */; }; D30355D31B61401D00E1DDED /* GDAJSONObject.m in Sources */ = {isa = PBXBuildFile; fileRef = D30355D21B61401D00E1DDED /* GDAJSONObject.m */; }; D395B0F71B41DA15005A6407 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = D395B0F61B41DA15005A6407 /* Images.xcassets */; }; + DE65184E7A99BC8A024D5CE8 /* Pods_Soundscape.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 91DC0CF52A460FAA00244CC8 /* Pods_Soundscape.framework */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -1580,6 +1582,7 @@ 815775252832F0650DF9D8D4 /* Pods-Soundscape.adhoc.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Soundscape.adhoc.xcconfig"; path = "Target Support Files/Pods-Soundscape/Pods-Soundscape.adhoc.xcconfig"; sourceTree = ""; }; 914DEBCD2A3CE6B9007B161C /* UnitTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = UnitTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 914DEBDC2A3CE901007B161C /* GeometryUtilsTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeometryUtilsTest.swift; sourceTree = ""; }; + 91C82AAC2A5DCF040086D126 /* GeolocationManagerTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = " GeolocationManagerTest.swift"; path = "UnitTests/Sensors/Geolocation/Geolocation Manager/ GeolocationManagerTest.swift"; sourceTree = SOURCE_ROOT; }; 91DC0CF32A460FAA00244CC8 /* iOS_GPX_Framework.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = iOS_GPX_Framework.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 91DC0CF52A460FAA00244CC8 /* Pods_Soundscape.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Pods_Soundscape.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 91DC0CF72A460FAA00244CC8 /* TBXML.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = TBXML.framework; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -4282,6 +4285,7 @@ 914DEBCE2A3CE6B9007B161C /* UnitTests */ = { isa = PBXGroup; children = ( + 91C82AA62A4F56A70086D126 /* Sensors */, 914DEBD92A3CE7E6007B161C /* App */, ); path = UnitTests; @@ -4303,6 +4307,30 @@ path = Helpers; sourceTree = ""; }; + 91C82AA62A4F56A70086D126 /* Sensors */ = { + isa = PBXGroup; + children = ( + 91C82AA72A4F56F80086D126 /* Geolocation */, + ); + path = Sensors; + sourceTree = ""; + }; + 91C82AA72A4F56F80086D126 /* Geolocation */ = { + isa = PBXGroup; + children = ( + 91C82AAB2A5DCECB0086D126 /* Geolocation Manager */, + ); + path = Geolocation; + sourceTree = ""; + }; + 91C82AAB2A5DCECB0086D126 /* Geolocation Manager */ = { + isa = PBXGroup; + children = ( + 91C82AAC2A5DCF040086D126 /* GeolocationManagerTest.swift */, + ); + path = "Geolocation Manager"; + sourceTree = ""; + }; 94B9789D44F5F13621A0C9B1 /* Pods */ = { isa = PBXGroup; children = ( @@ -5515,6 +5543,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 91C82AAD2A5DCF040086D126 /* GeolocationManagerTest.swift in Sources */, 91DC0CF92A46134600244CC8 /* GeometryUtilsTest.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/apps/ios/UnitTests.xctestplan b/apps/ios/UnitTests.xctestplan index 57c821e9..f8729279 100644 --- a/apps/ios/UnitTests.xctestplan +++ b/apps/ios/UnitTests.xctestplan @@ -19,7 +19,8 @@ "containerPath" : "container:GuideDogs.xcodeproj", "identifier" : "D3D7CFC71B3D96460020B5E9", "name" : "Soundscape" - } + }, + "testExecutionOrdering" : "random" }, "testTargets" : [ { diff --git a/apps/ios/UnitTests/Sensors/Geolocation/Geolocation Manager/ GeolocationManagerTest.swift b/apps/ios/UnitTests/Sensors/Geolocation/Geolocation Manager/ GeolocationManagerTest.swift new file mode 100644 index 00000000..2c080a7f --- /dev/null +++ b/apps/ios/UnitTests/Sensors/Geolocation/Geolocation Manager/ GeolocationManagerTest.swift @@ -0,0 +1,163 @@ +// +// GeolocationManager.swift +// UnitTests +// +// Created by Kai on 7/11/23. +// Copyright © 2023 Microsoft. All rights reserved. +// + +import XCTest +import CoreLocation +@testable import Soundscape + +// As described in `docs/ios-client/overview.md`, the GeolocationManager manages all location-related sensor providers for both the GPS and headphone location +// by default, this uses CoreLocationManager and derivations of it for all providers + +// Gets updated by its providers via its delegate extensions (e.g. `DeviceHeadingProviderDelegate` with event handler `deviceHeadingProvider(_:, didUpdateDeviceHeading:)`) +// When location updates, all `GeolocationManagerUpdateDelegate`s receiving events from it will have `didUpdateLocation(...)` called +// When any heading updates, the event will be sent to `NotificationCenter.default` for distribution to listeners + +class TestLocationProvider: LocationProvider { + var locationDelegate: LocationProviderDelegate? + func sendLocation(_ loc: CLLocation) { + locationDelegate?.locationProvider(self, didUpdateLocation: loc) + } + + func startLocationUpdates() { } + + func stopLocationUpdates() { } + + func startMonitoringSignificantLocationChanges() -> Bool { return false; } + + func stopMonitoringSignificantLocationChanges() { } + + var id: UUID = UUID() +} + +class TestCourseProvider: CourseProvider { + var courseDelegate: CourseProviderDelegate? + + func sendHeading(_ course: HeadingValue) { + courseDelegate?.courseProvider(self, didUpdateCourse: course) + } + + func startCourseProviderUpdates() { } + + func stopCourseProviderUpdates() { } + + var id: UUID = UUID() +} + +class TestUserHeadingProvider: UserHeadingProvider { + var headingDelegate: UserHeadingProviderDelegate? + + func sendHeading(_ userHeading: HeadingValue) { + headingDelegate?.userHeadingProvider(self, didUpdateUserHeading: userHeading) + } + + var accuracy: Double = 0 + + func startUserHeadingUpdates() { } + + func stopUserHeadingUpdates() { } + + var id: UUID = UUID() + + +} + +class TestGeolocationUpdateReceiver: GeolocationManagerUpdateDelegate { + var locations: [CLLocation] = [] + func didUpdateLocation(_ location: CLLocation) { + locations.append(location) + } +} +class TestNotificationCenterReceiver { + var headings: [[String: Any]] = [] + init() { + NotificationCenter.default.addObserver(self, selector: #selector(self.onNotif(_:)), name: Notification.Name.headingTypeDidUpdate, object: nil) + } + @objc + func onNotif(_ notif: NSNotification) { + headings.append(notif.userInfo! as! [String: Any]) + } +} + +class TestCLLocationManager: CLLocationManager { + +} + +class GeolocationManagerTest: XCTestCase { + private var manager: GeolocationManager! + private var locProvider = TestLocationProvider() + private var courseProvider = TestCourseProvider() + private var userHeadingProvider = TestUserHeadingProvider() + private var locReceiver = TestGeolocationUpdateReceiver() + + override func setUp() { + manager = GeolocationManager(isInMotion: false) + manager.updateDelegate = locReceiver + manager.add(locProvider) + manager.add(courseProvider) + manager.add(userHeadingProvider) + } + + override func tearDownWithError() throws { + // Put teardown code here. This method is called after the invocation of each test method in the class. + } + + func testDeviceLocationMocked() throws { + // Using our mock `LocationProvider` + let loc0_0 = CLLocation(latitude: 0, longitude: 0) + locProvider.sendLocation(loc0_0) + XCTAssertEqual(locReceiver.locations.count, 1) + XCTAssertEqual(locReceiver.locations[0], loc0_0) + } + + func testCourseProviderMocked() throws { + // Using our mock `CourseProvider` + let notifReceiver = TestNotificationCenterReceiver() + courseProvider.sendHeading(HeadingValue(0, 0)) + // Filter to only `HeadingType.course` + let courseHeadings = notifReceiver.headings.filter({$0[GeolocationManager.Key.type] as! HeadingType == HeadingType.course}) + XCTAssertEqual(courseHeadings.count, 1) + XCTAssertEqual(courseHeadings[0][GeolocationManager.Key.value] as! Double, 0) + XCTAssertEqual(courseHeadings[0][GeolocationManager.Key.accuracy] as! Double, 0) + } + + func testCourseProviderMockedNoAccuracy() throws { + // Using our mock `CourseProvider` + let notifReceiver = TestNotificationCenterReceiver() + courseProvider.sendHeading(HeadingValue(0, nil)) + // Filter to only `HeadingType.course` + let courseHeadings = notifReceiver.headings.filter({$0[GeolocationManager.Key.type] as! HeadingType == HeadingType.course}) + XCTAssertEqual(courseHeadings.count, 1) + XCTAssertEqual(courseHeadings[0][GeolocationManager.Key.value] as! Double, 0) + XCTAssertFalse(courseHeadings[0].keys.contains(GeolocationManager.Key.accuracy)) + } + + func testUserHeadingProviderMocked() throws { + // Using our mock `CourseProvider` + let notifReceiver = TestNotificationCenterReceiver() + userHeadingProvider.sendHeading(HeadingValue(0, 0)) + // Filter to only `HeadingType.course` + let userHeadings = notifReceiver.headings.filter({$0[GeolocationManager.Key.type] as! HeadingType == HeadingType.user}) + XCTAssertEqual(userHeadings.count, 1) + XCTAssertEqual(userHeadings[0][GeolocationManager.Key.value] as! Double, 0) + XCTAssertEqual(userHeadings[0][GeolocationManager.Key.accuracy] as! Double, 0) + } + + func testUserHeadingProviderMockedNoAccuracy() throws { + // Using our mock `CourseProvider` + let notifReceiver = TestNotificationCenterReceiver() + userHeadingProvider.sendHeading(HeadingValue(0, nil)) + // Filter to only `HeadingType.course` + let userHeadings = notifReceiver.headings.filter({$0[GeolocationManager.Key.type] as! HeadingType == HeadingType.user}) + XCTAssertEqual(userHeadings.count, 1) + XCTAssertEqual(userHeadings[0][GeolocationManager.Key.value] as! Double, 0) + XCTAssertFalse(userHeadings[0].keys.contains(GeolocationManager.Key.accuracy)) + } + + // NOTE: device heading provider is locked to be the default + // but we should probably still add tests +} From 471a84fd95ed1a097c0aa04f3eb2a554dda8264f Mon Sep 17 00:00:00 2001 From: 2kai2kai2 <2kai2kai2@gmail.com> Date: Fri, 4 Aug 2023 13:49:08 -0400 Subject: [PATCH 04/14] Initial RouteGuidance tests --- apps/ios/GuideDogs.xcodeproj/project.pbxproj | 20 ++ .../Route Guidance/RouteGuidance.swift | 11 +- .../Route Guidance/RouteGuidanceTest.swift | 212 ++++++++++++++++++ 3 files changed, 241 insertions(+), 2 deletions(-) create mode 100644 apps/ios/UnitTests/Behaviors/Route Guidance/RouteGuidanceTest.swift diff --git a/apps/ios/GuideDogs.xcodeproj/project.pbxproj b/apps/ios/GuideDogs.xcodeproj/project.pbxproj index d1ea2198..7612e172 100644 --- a/apps/ios/GuideDogs.xcodeproj/project.pbxproj +++ b/apps/ios/GuideDogs.xcodeproj/project.pbxproj @@ -663,6 +663,7 @@ 62F7A30E27B6082A00C62390 /* InteractiveBeaconViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62F7A30D27B6082A00C62390 /* InteractiveBeaconViewModel.swift */; }; 6A4891BB2A5E66DE0002D146 /* ExternalNavigationApps.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A4891BA2A5E66DE0002D146 /* ExternalNavigationApps.swift */; }; 91C82AAD2A5DCF040086D126 /* GeolocationManagerTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 91C82AAC2A5DCF040086D126 /* GeolocationManagerTest.swift */; }; + 91C82ABE2A6B08500086D126 /* RouteGuidanceTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 91C82ABD2A6B08500086D126 /* RouteGuidanceTest.swift */; }; 91DC0CF92A46134600244CC8 /* GeometryUtilsTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 914DEBDC2A3CE901007B161C /* GeometryUtilsTest.swift */; }; B5499F0EEDE32E16F9814457 /* Pods_Soundscape.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DAEDD72453A8268FBA604D2F /* Pods_Soundscape.framework */; }; B90C27D61EAF81D600007368 /* Sound.swift in Sources */ = {isa = PBXBuildFile; fileRef = B90C27D51EAF81D600007368 /* Sound.swift */; }; @@ -1583,6 +1584,7 @@ 914DEBCD2A3CE6B9007B161C /* UnitTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = UnitTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 914DEBDC2A3CE901007B161C /* GeometryUtilsTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeometryUtilsTest.swift; sourceTree = ""; }; 91C82AAC2A5DCF040086D126 /* GeolocationManagerTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = " GeolocationManagerTest.swift"; path = "UnitTests/Sensors/Geolocation/Geolocation Manager/ GeolocationManagerTest.swift"; sourceTree = SOURCE_ROOT; }; + 91C82ABD2A6B08500086D126 /* RouteGuidanceTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = RouteGuidanceTest.swift; path = "UnitTests/Behaviors/Route Guidance/RouteGuidanceTest.swift"; sourceTree = SOURCE_ROOT; }; 91DC0CF32A460FAA00244CC8 /* iOS_GPX_Framework.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = iOS_GPX_Framework.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 91DC0CF52A460FAA00244CC8 /* Pods_Soundscape.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Pods_Soundscape.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 91DC0CF72A460FAA00244CC8 /* TBXML.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = TBXML.framework; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -4285,6 +4287,7 @@ 914DEBCE2A3CE6B9007B161C /* UnitTests */ = { isa = PBXGroup; children = ( + 91C82ABB2A6B03790086D126 /* Behaviors */, 91C82AA62A4F56A70086D126 /* Sensors */, 914DEBD92A3CE7E6007B161C /* App */, ); @@ -4331,6 +4334,22 @@ path = "Geolocation Manager"; sourceTree = ""; }; + 91C82ABB2A6B03790086D126 /* Behaviors */ = { + isa = PBXGroup; + children = ( + 91C82ABF2A6B08630086D126 /* Route Guidance */, + ); + path = Behaviors; + sourceTree = ""; + }; + 91C82ABF2A6B08630086D126 /* Route Guidance */ = { + isa = PBXGroup; + children = ( + 91C82ABD2A6B08500086D126 /* RouteGuidanceTest.swift */, + ); + path = "Route Guidance"; + sourceTree = ""; + }; 94B9789D44F5F13621A0C9B1 /* Pods */ = { isa = PBXGroup; children = ( @@ -5543,6 +5562,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 91C82ABE2A6B08500086D126 /* RouteGuidanceTest.swift in Sources */, 91C82AAD2A5DCF040086D126 /* GeolocationManagerTest.swift in Sources */, 91DC0CF92A46134600244CC8 /* GeometryUtilsTest.swift in Sources */, ); diff --git a/apps/ios/GuideDogs/Code/Behaviors/Route Guidance/RouteGuidance.swift b/apps/ios/GuideDogs/Code/Behaviors/Route Guidance/RouteGuidance.swift index a3ed2dcd..b96bbd69 100644 --- a/apps/ios/GuideDogs/Code/Behaviors/Route Guidance/RouteGuidance.swift +++ b/apps/ios/GuideDogs/Code/Behaviors/Route Guidance/RouteGuidance.swift @@ -51,13 +51,18 @@ class RouteGuidance: BehaviorBase { private(set) var content: RouteDetail private(set) var nearestIntersectionKey: String? + /// If we are transitioning to a waypoint but have not yet arrived, this contains information about the upcoming waypoint beacon. We do this transition instead of just changing if `isArrivingAtWaypoint` is `true`. + /// see: `setOrTransitionBeacon(to waypoint:, enableAudio:, skipAsyncFinish:)` private var pendingBeaconArgs: PendingBeaconArgs? + /// see: `nextWaypoint(automatic:)` private var isArrivingAtWaypoint = false + /// If we have ever visited the final waypoint; is *not* reset by `activate()` private var arrivedAtFinalWaypoint = false + /// Seems related to beacons, somehow. private var isFinished = false + /// Receives `dynamicPlayerFinished` events private var beaconObserver: AnyCancellable? - // Unless `shouldResume = true`, the route's state will be reset each time - // the route is activated + /// Unless `shouldResume = true`, the route's state will be reset each time the route is activated var shouldResume = false private var lastSaveTime: Date? @@ -340,6 +345,8 @@ class RouteGuidance: BehaviorBase { /// Updates the `waypointIndex` property of the state object to be the index of the /// next waypoint and then saves the route guidance's state. + /// - Parameter automatic: If `false`, we play departure callouts immediately instead of queueing. + /// Usually `true` if the cause of this change was automatic, or `false` if the user manually tapped the "Next" button func nextWaypoint(automatic: Bool = false) { if let index = state.waypointIndex { // Next() increases the index until the last index and then stops incrementing diff --git a/apps/ios/UnitTests/Behaviors/Route Guidance/RouteGuidanceTest.swift b/apps/ios/UnitTests/Behaviors/Route Guidance/RouteGuidanceTest.swift new file mode 100644 index 00000000..a20c2b4c --- /dev/null +++ b/apps/ios/UnitTests/Behaviors/Route Guidance/RouteGuidanceTest.swift @@ -0,0 +1,212 @@ +// +// RouteGuidanceTest.swift +// UnitTests +// +// Created by Kai on 7/21/23. +// Copyright © 2023 Microsoft. All rights reserved. +// + +import XCTest +import CoreLocation +@testable import Soundscape + +// Note that while these mocked systems allow for some testing, they still cover a limited number of cases, since some would require that more complex systems be mocked. + + +class RouteGuidanceTest: XCTestCase { + class TestMotionActivity: MotionActivityProtocol { + var isWalking: Bool = true + var isInVehicle: Bool = false + var currentActivity: ActivityType = .walking + func startActivityUpdates() { } + func stopActivityUpdates() { } + } + class TestSpatialData: SpatialDataProtocol { + static var zoomLevel: UInt = SpatialDataContext.zoomLevel + static var cacheDistance: CLLocationDistance = SpatialDataContext.cacheDistance + static var initialPOISearchDistance: CLLocationDistance = SpatialDataContext.initialPOISearchDistance + static var expansionPOISearchDistance: CLLocationDistance = SpatialDataContext.expansionPOISearchDistance + static var refreshTimeInterval: TimeInterval = SpatialDataContext.refreshTimeInterval + static var refreshDistanceInterval: CLLocationDistance = SpatialDataContext.refreshDistanceInterval + + var motionActivityContext: MotionActivityProtocol + var destinationManager: DestinationManagerProtocol + var state: SpatialDataState = SpatialDataState.waitingForLocation + var loadedSpatialData: Bool = false + var currentTiles: [VectorTile] = [] + + init(_ motionActivity: MotionActivityProtocol, _ destination: DestinationManagerProtocol) { + motionActivityContext = motionActivity + destinationManager = destination + } + + func start() { } + func stop() { } + func clearCache() -> Bool { + false + } + + func getDataView(for location: CLLocation, searchDistance: CLLocationDistance) -> SpatialDataViewProtocol? { + nil + } + func getCurrentDataView(searchDistance: CLLocationDistance) -> SpatialDataViewProtocol? { + nil + } + func getCurrentDataView(initialSearchDistance: CLLocationDistance, shouldExpandDataView: @escaping (SpatialDataViewProtocol) -> Bool) -> SpatialDataViewProtocol? { + nil + } + + func updateSpatialData(at location: CLLocation, completion: @escaping () -> Void) -> Progress? { + nil + } + } + + // ---- + + let motion = TestMotionActivity() + let destinationM = DestinationManager(userLocation: nil, audioEngine: AudioEngine(envSettings: TestAudioEnvironmentSettings(), mixWithOthers: true), collectionHeading: Heading(orderedBy: [], course: nil, deviceHeading: nil, userHeading: nil, geolocationManager: nil)) + var spatial: TestSpatialData! + static let loc0_0 = CLLocationCoordinate2D(latitude: 0, longitude: 0) + static let availableInterval = DateInterval(start: Date(timeIntervalSinceNow: TimeInterval(0)), end: Date(timeIntervalSinceNow: TimeInterval(99999999))) + static func activity_single_waypoint() -> AuthoredActivityContent { + AuthoredActivityContent(id: "test1234", type: .guidedTour, name: "test1234", creator: "soundscape_unit_tests", locale: .enUS, availability: availableInterval, expires: false, image: nil, desc: "test description", waypoints: [ActivityWaypoint(coordinate: loc0_0)], pois: []) + } + static func activity_random_waypoints(count: Int) -> AuthoredActivityContent { + var points: [ActivityWaypoint] = [] + for _ in 0.. Date: Mon, 14 Aug 2023 21:52:07 -0400 Subject: [PATCH 05/14] Improve GeometryUtils test class --- .../Code/App/Helpers/GeometryUtils.swift | 34 +- .../App/Helpers/GeometryUtilsTest.swift | 412 +++++++++++++++++- 2 files changed, 420 insertions(+), 26 deletions(-) diff --git a/apps/ios/GuideDogs/Code/App/Helpers/GeometryUtils.swift b/apps/ios/GuideDogs/Code/App/Helpers/GeometryUtils.swift index 171e48cf..c4ed974a 100644 --- a/apps/ios/GuideDogs/Code/App/Helpers/GeometryUtils.swift +++ b/apps/ios/GuideDogs/Code/App/Helpers/GeometryUtils.swift @@ -28,6 +28,10 @@ class GeometryUtils { static let earthRadius = Double(6378137) /// Parses a GeoJSON string and returns the coordinates and type values. + /// + /// See: + /// * https://geojson.org + /// * RFC 7946 static func coordinates(geoJson: String) -> (type: GeometryType?, points: [Any]?) { guard !geoJson.isEmpty, let jsonObject = GDAJSONObject(string: geoJson) else { return (nil, nil) @@ -47,7 +51,7 @@ class GeometryUtils { return (geometryType, geometry) } - /// Returns whether a coordinate lies inside of path. + /// Returns whether a coordinate lies inside of the region contained within the coordinate path. /// The path is always considered closed, regardless of whether the last point equals the first or not. static func geometryContainsLocation(location: CLLocationCoordinate2D, coordinates: [CLLocationCoordinate2D]) -> Bool { guard coordinates.count > 0 else { @@ -82,8 +86,8 @@ class GeometryUtils { /// point on that path which is no further away from the first point than the specified `maxDistance`. /// /// - Parameters: - /// - path: The reference path. - /// - maxDistance: The max distance for calculating the reference coordinate along the path. + /// - for: The reference path. + /// - maxDistance: The max distance for calculating the reference coordinate along the path. /// - Returns: The bearing of a coordinates path. /// /// - note: The path most have more than one coordinate. @@ -116,7 +120,7 @@ class GeometryUtils { /// - path: The reference path. /// - coordinate: The reference coordinate. /// - reversedDirection: If `true` is passed, the returned sub-coordinates will be - /// cauculated from the reference coordinate to the start of the path. + /// calculated from the reference coordinate to the start of the path. /// - Returns: The sub-coordinates from a coordinate on a path until the end or start of the path. static func split(path: [CLLocationCoordinate2D], atCoordinate coordinate: CLLocationCoordinate2D, @@ -165,7 +169,7 @@ class GeometryUtils { /// and there are more than 2 coordinates). /// /// - Parameters: - /// - path: The reference path. + /// - path: The reference path. /// - Returns: `true` if the path is a circular path, `false` otherwise. static func pathIsCircular(_ path: [CLLocationCoordinate2D]) -> Bool { guard path.count > 2, let first = path.first, let last = path.last else { @@ -178,7 +182,7 @@ class GeometryUtils { /// Returns the distance of a coordinate path /// /// - Parameters: - /// - path: The reference path. + /// - path: The reference path. /// - Returns: The distance of a coordinate path static func pathDistance(_ path: [CLLocationCoordinate2D]) -> CLLocationDistance { guard path.count > 1 else { @@ -194,7 +198,7 @@ class GeometryUtils { return distance } - /// Calculates a coordinate on a path at a target distance from the path's first coordinate. + /// Calculates a coordinate on a path at a target distance along the path from the path's first coordinate. /// - note: If the target distance is greater than the path distance, the last path coordinate is returned. /// - note: If the target distance is between two coordinates on the path, a synthesized coordinate between the coordinates is returned. /// - note: If the target distance is smaller or equal to zero, the first path coordinate is returned. @@ -303,6 +307,8 @@ class GeometryUtils { return ((cX - projX) * (cX - projX) + (cY - projY) * (cY - projY), lat, lon) } + /// Finds the closest point on an edge of the polygon (including intermediate points along edges) to the given coordinate. + /// - Returns: `nil` if the polygon is empty (i.e. no edges) static func closestEdge(from coordinate: CLLocationCoordinate2D, on polygon: GAMultiLine) -> CLLocation? { var coordinates: [CLLocationCoordinate2D] = [] @@ -316,7 +322,13 @@ class GeometryUtils { return closestEdge(from: coordinate, on: coordinates) } + /// Finds the closest point on the path (including intermediate points along edges) to the given coordinate. + /// - Returns: `nil` if there are no edges (less than two points in `path`) static func closestEdge(from coordinate: CLLocationCoordinate2D, on path: [CLLocationCoordinate2D]) -> CLLocation? { + guard !path.isEmpty else { + return nil; + } + // Used to calculate distance to a point on a polygon let zoomLevel: UInt = 23 let res: Double = VectorTile.groundResolution(latitude: coordinate.latitude, zoom: zoomLevel) @@ -364,10 +376,9 @@ class GeometryUtils { /// ``` /// /// - Parameters: - /// - start: The start coordinate. - /// - end: The end coordinate. + /// - coordinates: The original coordinate path. /// - distance: The fixed distance to use between the interpolated coodinates. - /// - Returns: An coordinates path including the original coordinates and any coordiantes between + /// - Returns: A coordinate path including the original coordinates and any coordiantes between /// them with a fixed distance of `distance`. static func interpolateToEqualDistance(coordinates: [CLLocationCoordinate2D], distance targetDistance: CLLocationDistance) -> [CLLocationCoordinate2D] { @@ -447,6 +458,7 @@ class GeometryUtils { extension GeometryUtils { /// Returns a generated coordinate representing the mean center of a given array of coordinates. + /// - Note: See `centroid(coordinates: [CLLocationCoordinate2D]) -> CLLocationCoordinate2D?` static func centroid(geoJson: String) -> CLLocationCoordinate2D? { guard let points = GeometryUtils.coordinates(geoJson: geoJson).points else { return nil @@ -472,7 +484,7 @@ extension GeometryUtils { } /// Returns a generated coordinate representing the mean center of a given array of `CLLocation` objects. - /// - Note: See `centroid(locations: [CLLocation]) -> CLLocationCoordinate2D?` + /// - Note: See `centroid(coordinates: [CLLocationCoordinate2D]) -> CLLocationCoordinate2D?` static func centroid(locations: [CLLocation]) -> CLLocationCoordinate2D? { return GeometryUtils.centroid(coordinates: locations.map { (location) -> CLLocationCoordinate2D in return location.coordinate diff --git a/apps/ios/UnitTests/App/Helpers/GeometryUtilsTest.swift b/apps/ios/UnitTests/App/Helpers/GeometryUtilsTest.swift index 4a888798..be1905ad 100644 --- a/apps/ios/UnitTests/App/Helpers/GeometryUtilsTest.swift +++ b/apps/ios/UnitTests/App/Helpers/GeometryUtilsTest.swift @@ -11,26 +11,408 @@ import CoreLocation @testable import Soundscape class GeometryUtilsTest: XCTestCase { - - override func setUpWithError() throws { - // Put setup code here. This method is called before the invocation of each test method in the class. + + // TODO: `GeometryUtils::coordinates(geoJson:)` would be better if the `GeometryType` enum used associated values (coordinates), which would let us avoid the fact that it currently returns a vague `[Any]?` and instead just return a `GeometryType`. According to comments in `GeometryUtils`, the reason for this is compatibility with Objective-C. However, if we can move away from that, we could have much better code. + + // GeoJSON strings taken/adapted from the GeoJSON spec, RFC-7946 + + /// normal test case for `GeometryUtils::coordinates(geoJson:)` + func testGeoJSONCoordinates_Point() throws { + /// `Point`-- coordinates are a `[Double]` + let point = GeometryUtils.coordinates(geoJson: """ +{ + "type": "Point", + "coordinates": [100.0, 0.0] +} +""") + XCTAssertEqual(point.type, GeometryType.point) + XCTAssertEqual(point.points as! [Double], [100.0, 0.0]) } - - override func tearDownWithError() throws { - // Put teardown code here. This method is called after the invocation of each test method in the class. + + /// normal test case for `GeometryUtils::coordinates(geoJson:)` + func testGeoJSONCoordinates_LineString() throws { + /// `LineString`-- coordinates are a `[[Double]]` + let lineString = GeometryUtils.coordinates(geoJson: """ +{ + "type": "LineString", + "coordinates": [ + [100.0, 0.0], + [101.0, 1.0] + ] +} +""") + XCTAssertEqual(lineString.type, GeometryType.lineString) + XCTAssertEqual(lineString.points as! [[Double]], [[100.0, 0.0], [101.0, 1.0]]) } - + /// normal test case for `GeometryUtils::coordinates(geoJson:)` + func testGeoJSONCoordinates_Polygon() throws { + /// `Polygon`-- coordinates are a `[[[Double]]]` + let poly = GeometryUtils.coordinates(geoJson: """ +{ + "type": "Polygon", + "coordinates": [ + [ + [100.0, 0.0], + [101.0, 0.0], + [101.0, 1.0], + [100.0, 1.0], + [100.0, 0.0] + ], + [ + [100.8, 0.8], + [100.8, 0.2], + [100.2, 0.2], + [100.2, 0.8], + [100.8, 0.8] + ] + ] +} +""") + XCTAssertEqual(poly.type, GeometryType.polygon) + XCTAssertEqual(poly.points as! [[[Double]]], [ + [ + [100.0, 0.0], + [101.0, 0.0], + [101.0, 1.0], + [100.0, 1.0], + [100.0, 0.0] + ], + [ + [100.8, 0.8], + [100.8, 0.2], + [100.2, 0.2], + [100.2, 0.8], + [100.8, 0.8] + ] + ]) + } + + // Skipping type `MultiPoint` as equivalent + // Skipping type `MultiLineString` as equivalent + // Skipping type `MultiPolygon` as equivalent + + func testGeoJSONCoordinates_invalidType() throws { + let a = GeometryUtils.coordinates(geoJson: """ +{ + "type": "a", + "coordinates": [100.0, 0.0] +} +""") + // TODO: apparently invalid types become GeometryType.multiPolygon - should it? + XCTAssertEqual(a.type, .multiPolygon) + XCTAssertEqual(a.points as! [Double], [100.0, 0.0]) + } + + /// edge case for `GeometryUtils::coordinates(geoJson:)` with empty input + /// which should result in `(nil, nil)` + func testGeoJSONCoordinates_emptystring() throws { + let emptyString = GeometryUtils.coordinates(geoJson: "") + XCTAssertNil(emptyString.type) + XCTAssertNil(emptyString.points) + } + + /// edge cases for `GeometryUtils::coordinates(geoJson:)` with malformed json + /// which should result in `(nil, nil)` + func testGeoJSONCoordinates_malformed() throws { + let badKey = GeometryUtils.coordinates(geoJson: "{a: 1}"); + XCTAssertNil(badKey.type) + XCTAssertNil(badKey.points) + + let badValue = GeometryUtils.coordinates(geoJson: "{\"a\": asdf}") + XCTAssertNil(badValue.type) + XCTAssertNil(badValue.points) + } + + /// edge cases for `GeometryUtils::coordinates(geoJson:)` with missing keys + /// which should result in one or both return fields being `nil` + func testGeoJSONCoordinates_missing() throws { + let noType = GeometryUtils.coordinates(geoJson: """ +{"coordinates": [100.0, 0.0]} +""") + XCTAssertNil(noType.type) + XCTAssertEqual(noType.points as! [Double], [100.0, 0.0]) + + let noCoords = GeometryUtils.coordinates(geoJson: """ +{"type": "Point"} +""") + XCTAssertEqual(noCoords.type, GeometryType.point) + XCTAssertNil(noCoords.points) + } + // TODO: test `geometryContainsLocation` func testExample() throws { - // This is an example of a functional test case. - // Use XCTAssert and related functions to verify your tests produce the correct results. XCTAssert(Soundscape.GeometryUtils.geometryContainsLocation(location: CLLocationCoordinate2D.init(latitude: 1, longitude: 1), coordinates: [CLLocationCoordinate2D.init(latitude: 1, longitude: 1), CLLocationCoordinate2D.init(latitude: 3, longitude: 3)])) } - - func testPerformanceExample() throws { - // This is an example of a performance test case. - self.measure { - // Put the code you want to measure the time of here. + + // TODO: test `pathBearing` + // TODO: test `split` + + func testRotate() throws { + let p1 = CLLocationCoordinate2DMake(0, 1) + let p2 = CLLocationCoordinate2DMake(0, 2) + let p3 = CLLocationCoordinate2DMake(0, 3) + let p4 = CLLocationCoordinate2DMake(0, 4) + let p5 = CLLocationCoordinate2DMake(0, 5) + + /// Using the example given in the function description for `rotate` + let a = [p1, p2, p3, p4, p5, p1] + let a_rot = GeometryUtils.rotate(circularPath: a, atCoordinate: p3) + XCTAssertEqual(a_rot, [p3, p4, p5, p1, p2, p3]) + let a_rot_reverse = GeometryUtils.rotate(circularPath: a, atCoordinate: p3, reversedDirection: true) + XCTAssertEqual(a_rot_reverse, [p3, p2, p1, p5, p4, p3]) + + /// Rotating from the existing start is trivial: + let a_no_rot = GeometryUtils.rotate(circularPath: a, atCoordinate: p1) + XCTAssertEqual(a_no_rot, a) + let a_no_rot_reverse = GeometryUtils.rotate(circularPath: a, atCoordinate: p1, reversedDirection: true) + XCTAssertEqual(a_no_rot_reverse, a.reversed()) + } + + func testRotate_invalidPath() throws { + let p1 = CLLocationCoordinate2DMake(0, 1) + let p2 = CLLocationCoordinate2DMake(0, 2) + let p3 = CLLocationCoordinate2DMake(0, 3) + let p4 = CLLocationCoordinate2DMake(0, 4) + let p5 = CLLocationCoordinate2DMake(0, 5) + + /// For a non-circular path: + let noncirc = [p1, p2, p3, p4, p5] + let noncirc_rot = GeometryUtils.rotate(circularPath: noncirc, atCoordinate: p3) + XCTAssertTrue(noncirc_rot.isEmpty) + } + + func testRotate_invalidCoord() throws { + let p1 = CLLocationCoordinate2DMake(0, 1) + let p2 = CLLocationCoordinate2DMake(0, 2) + let p3 = CLLocationCoordinate2DMake(0, 3) + let p4 = CLLocationCoordinate2DMake(0, 4) + let p5 = CLLocationCoordinate2DMake(0, 5) + let other_point = CLLocationCoordinate2DMake(90, 0) + + // test with rotation to coordinate not in the path + let a = [p1, p2, p3, p4, p5, p1] + let a_rot = GeometryUtils.rotate(circularPath: a, atCoordinate: other_point) + XCTAssertTrue(a_rot.isEmpty) + } + + func testPathIsCircular() throws { + let a = [CLLocationCoordinate2DMake(0, 0), + CLLocationCoordinate2DMake(1, 1), + CLLocationCoordinate2DMake(0, 0)] + XCTAssertTrue(GeometryUtils.pathIsCircular(a)) + + let b = [CLLocationCoordinate2DMake(0, 0), + CLLocationCoordinate2DMake(1, 1), + CLLocationCoordinate2DMake(2, 2)] + XCTAssertFalse(GeometryUtils.pathIsCircular(b)) + + let c = [CLLocationCoordinate2DMake(0, 0), + CLLocationCoordinate2DMake(1, 1), + CLLocationCoordinate2DMake(0, 0), + CLLocationCoordinate2DMake(1, 1)] + XCTAssertFalse(GeometryUtils.pathIsCircular(c)) + } + + /// Edge cases for `GeometryUtils::pathIsCircular(_:)` with a path size of less than or equal to 2 + func testPathIsCircular_small() throws { + let emptyPath: [CLLocationCoordinate2D] = [] + XCTAssertFalse(GeometryUtils.pathIsCircular(emptyPath)) + + let singlePoint = [CLLocationCoordinate2DMake(0, 0)] + XCTAssertFalse(GeometryUtils.pathIsCircular(singlePoint)) + + let twoPoints_different = [CLLocationCoordinate2DMake(0, 0), + CLLocationCoordinate2DMake(1, 1)] + XCTAssertFalse(GeometryUtils.pathIsCircular(twoPoints_different)) + + let twoPoints_same = [CLLocationCoordinate2DMake(0, 0), + CLLocationCoordinate2DMake(0, 0)] + XCTAssertFalse(GeometryUtils.pathIsCircular(twoPoints_same)) + + } + + func testPathDistance() throws { + let emptyPath: [CLLocationCoordinate2D] = [] + XCTAssertEqual(GeometryUtils.pathDistance(emptyPath), 0) + + let singlePoint = [CLLocationCoordinate2DMake(0, 0)] + XCTAssertEqual(GeometryUtils.pathDistance(singlePoint), 0) + + let twoPoints_same = [CLLocationCoordinate2DMake(0, 0), + CLLocationCoordinate2DMake(0, 0)] + XCTAssertEqual(GeometryUtils.pathDistance(twoPoints_same), 0) + + let twoPoints_1 = [CLLocationCoordinate2DMake(0, 0), + CLLocationCoordinate2DMake(0, 180)] + XCTAssertEqual(GeometryUtils.pathDistance(twoPoints_1), + twoPoints_1[0].distance(from: twoPoints_1[1])) + XCTAssertEqual(GeometryUtils.pathDistance(twoPoints_1), GeometryUtils.earthRadius * Double.pi) + + // It should follow the path, not just the distance from start to end + let circlePath = [CLLocationCoordinate2DMake(0, 0), + CLLocationCoordinate2DMake(1, 0), + CLLocationCoordinate2DMake(0, 1), + CLLocationCoordinate2DMake(0, 0)] + XCTAssertTrue(GeometryUtils.pathIsCircular(circlePath), "This issue is unrelated to `pathDistance`") // assume + XCTAssertNotEqual(GeometryUtils.pathDistance(circlePath), 0) + } + + func testReferenceCoordinate() throws { + // Note that the first three points are a 3-4-5 triangle + let path = [CLLocationCoordinate2DMake(0, 0), + CLLocationCoordinate2DMake(3, 4), + CLLocationCoordinate2DMake(3, 0), + CLLocationCoordinate2DMake(0, 0), + CLLocationCoordinate2DMake(0, 1)] + + let dist1 = path[0].distance(from: path[1]) + let dist2 = path[1].distance(from: path[2]) + let dist3 = path[2].distance(from: path[3]) + let dist4 = path[3].distance(from: path[4]) + + // For some reason these test cases don't work between points. + // Either I don't understand how this should work, or it's a bug. + + //XCTAssertEqual(GeometryUtils.referenceCoordinate(on: path, for: dist1 / 2), CLLocationCoordinate2DMake(1.5, 2)) + XCTAssertEqual(GeometryUtils.referenceCoordinate(on: path, for: dist1), path[1]) + + //XCTAssertEqual(GeometryUtils.referenceCoordinate(on: path, for: dist1 + dist2 / 4), CLLocationCoordinate2DMake(3, 3)) + //XCTAssertEqual(GeometryUtils.referenceCoordinate(on: path, for: dist1 + dist3 * 3.12 / 4), CLLocationCoordinate2DMake(3, 1 - 0.12)) + XCTAssertEqual(GeometryUtils.referenceCoordinate(on: path, for: dist1 + dist2), path[2]) + + //XCTAssertEqual(GeometryUtils.referenceCoordinate(on: path, for: dist1 + dist2 + dist3 / 6), CLLocationCoordinate2DMake(2.5, 0)) + XCTAssertEqual(GeometryUtils.referenceCoordinate(on: path, for: dist1 + dist2 + dist3), path[3]) + + //XCTAssertEqual(GeometryUtils.referenceCoordinate(on: path, for: dist1 + dist2 + dist3 + dist4 * 0.123), CLLocationCoordinate2DMake(0, 0.123)) + XCTAssertEqual(GeometryUtils.referenceCoordinate(on: path, for: dist1 + dist2 + dist3 + dist4), path.last) + } + + /// Edge cases for `GeometryUtils::referenceCoordinate(on:for:)` with a path size of less than 2 + func testReferenceCoordinate_small() throws { + // Empty path returns `nil` + let emptyPath: [CLLocationCoordinate2D] = [] + XCTAssertNil(GeometryUtils.referenceCoordinate(on: emptyPath, for: -1)) + XCTAssertNil(GeometryUtils.referenceCoordinate(on: emptyPath, for: 0)) + XCTAssertNil(GeometryUtils.referenceCoordinate(on: emptyPath, for: 1)) + + // Single point always returns that point + let singlePath = [CLLocationCoordinate2DMake(0, 0)] + XCTAssertEqual(GeometryUtils.referenceCoordinate(on: singlePath, for: -1), singlePath.first) + XCTAssertEqual(GeometryUtils.referenceCoordinate(on: singlePath, for: 0), singlePath.first) + XCTAssertEqual(GeometryUtils.referenceCoordinate(on: singlePath, for: 1), singlePath.first) + } + + /// Edge cases for `GeometryUtils::referenceCoordinate(on:for:)` with a distance before the start or after the end of the path + func testReferenceCoordinate_outOfBounds() throws { + let path = [CLLocationCoordinate2DMake(0, 0), + CLLocationCoordinate2DMake(1, 0), + CLLocationCoordinate2DMake(1, 1), + CLLocationCoordinate2DMake(2, 2)] + let path_len = GeometryUtils.pathDistance(path) + // Any distance before the start returns the first coordinate + XCTAssertEqual(GeometryUtils.referenceCoordinate(on: path, for: -CLLocationDistanceMax), path.first) + XCTAssertEqual(GeometryUtils.referenceCoordinate(on: path, for: -path_len), path.first) + XCTAssertEqual(GeometryUtils.referenceCoordinate(on: path, for: -5.2), path.first) + XCTAssertEqual(GeometryUtils.referenceCoordinate(on: path, for: -1), path.first) + XCTAssertEqual(GeometryUtils.referenceCoordinate(on: path, for: 0), path.first) + + // Any distance after the end returns the last coordinate + XCTAssertEqual(GeometryUtils.referenceCoordinate(on: path, for: CLLocationDistanceMax), path.last) + XCTAssertEqual(GeometryUtils.referenceCoordinate(on: path, for: path_len * 1.2512), path.last) + XCTAssertEqual(GeometryUtils.referenceCoordinate(on: path, for: path_len + 1), path.last) + XCTAssertEqual(GeometryUtils.referenceCoordinate(on: path, for: path_len), path.last) + } + + // TODO: test `squaredDistance` + // TODO: test `closestEdge` on polygon (just calls the other one) + + /// Normal test cases for `GeometryUtils.closestEdge(coordinate:path:)` + func testClosestEdge() throws { + let path: [CLLocationCoordinate2D] = [CLLocationCoordinate2DMake(0, 0), + CLLocationCoordinate2DMake(0, 10), + CLLocationCoordinate2DMake(0, 20)] + + for lon in [0.0, 5.0, 10.0, 15.0, 20.0] { + let on_path = CLLocationCoordinate2DMake(0, lon) + let on_path_closest = GeometryUtils.closestEdge(from: on_path, on: path) + XCTAssertNotNil(on_path_closest) + XCTAssertEqual(on_path_closest!.coordinate, on_path) + + let parallel = CLLocationCoordinate2DMake(10, lon) + let parallel_closest = GeometryUtils.closestEdge(from: parallel, on: path) + XCTAssertNotNil(parallel_closest) + XCTAssertEqual(parallel_closest!.coordinate, on_path) + } + + for lat in [-10.0, -5.0, 0, 5.0, 10.0] { + let before = CLLocationCoordinate2DMake(lat, -10) + let before_closest = GeometryUtils.closestEdge(from: before, on: path) + XCTAssertNotNil(before_closest); + XCTAssertEqual(before_closest!.coordinate, path.first) + + let after = CLLocationCoordinate2DMake(lat, 30) + let after_closest = GeometryUtils.closestEdge(from: after, on: path) + XCTAssertNotNil(after_closest) + XCTAssertEqual(after_closest!.coordinate, path.last) } } - + + /// An edge case for `GeometryUtils.closestEdge(coordinate:path:)` + /// with no points in the path + /// returns `nil` + func testClosestEdge_emptyPath() throws { + let emptyPath: [CLLocationCoordinate2D] = [] + let p0 = CLLocationCoordinate2DMake(0, 0) + let closest = GeometryUtils.closestEdge(from: p0, on: emptyPath) + XCTAssertNil(closest) + } + + /// An edge case for `GeometryUtils.closestEdge(coordinate:path:)` + /// with only one point or multiple identical points + /// which seems to return `nil` + func testClosestEdge_singlePoint() throws { + // TODO: *should* this return `nil`? or should it be the distance to the point? + let p0 = CLLocationCoordinate2DMake(0, 0) + let p1 = CLLocationCoordinate2DMake(0, 1) + + let singlePoint = [p0] + XCTAssertNil(GeometryUtils.closestEdge(from: p0, on: singlePoint)) + XCTAssertNil(GeometryUtils.closestEdge(from: p1, on: singlePoint)) + + let twoIdentical = [p0, p0] + XCTAssertNil(GeometryUtils.closestEdge(from: p0, on: twoIdentical)) + XCTAssertNil(GeometryUtils.closestEdge(from: p1, on: twoIdentical)) + + let manyIdentical = [p0, p0, p0, p0, p0] + XCTAssertNil(GeometryUtils.closestEdge(from: p0, on: manyIdentical)) + XCTAssertNil(GeometryUtils.closestEdge(from: p1, on: manyIdentical)) + } + + /// An edge case for `GeometryUtils.closestEdge(coordinate:path:)` + /// Since we're on a sphere, there can be multiple closest points (e.g. path is the equator, and point is the north pole). + /// it seems to return + func testClosestEdge_equidistant() throws { + let path: [CLLocationCoordinate2D] = [CLLocationCoordinate2DMake(0, 0), + CLLocationCoordinate2DMake(0, 90), + CLLocationCoordinate2DMake(0, 180)] + + let n_pole = CLLocationCoordinate2DMake(90, 0) + let n_pole_closest = GeometryUtils.closestEdge(from: n_pole, on: path) + XCTAssertNotNil(n_pole_closest) + XCTAssertEqual(n_pole_closest!.coordinate, path.first) + + let s_pole = CLLocationCoordinate2DMake(-90, 0) + let s_pole_closest = GeometryUtils.closestEdge(from: s_pole, on: path) + XCTAssertNotNil(s_pole_closest) + XCTAssertEqual(s_pole_closest!.coordinate, path.first) + } + + + // TODO: test `interpolateToEqualDistance` with coordinates + // TODO: test `interpolateToEqualDistance` with start and end + + // TODO: test `centroid` with geoJson + // TODO: test `centroid` with locations + // TODO: test `centroid` with coordinates + + } From d1134d1aea8e46109394484f19c52790638aa03f Mon Sep 17 00:00:00 2001 From: 2kai2kai2 <2kai2kai2@gmail.com> Date: Fri, 6 Oct 2023 18:29:20 -0400 Subject: [PATCH 06/14] Basic DestinationManagerTest Needs audio beacon tests --- apps/ios/DestinationManagerTest.swift | 97 +++++++++++++++++++ apps/ios/GuideDogs.xcodeproj/project.pbxproj | 20 ++++ .../DestinationManager.swift | 6 +- 3 files changed, 122 insertions(+), 1 deletion(-) create mode 100644 apps/ios/DestinationManagerTest.swift diff --git a/apps/ios/DestinationManagerTest.swift b/apps/ios/DestinationManagerTest.swift new file mode 100644 index 00000000..5fb469f7 --- /dev/null +++ b/apps/ios/DestinationManagerTest.swift @@ -0,0 +1,97 @@ +// +// DestinationManagerTest.swift +// UnitTests +// +// Created by Kai on 10/3/23. +// Copyright © 2023 Microsoft. All rights reserved. +// + +import XCTest +import CoreLocation +@testable import Soundscape + +final class DestinationManagerTest: XCTestCase { + + let basic_audio_engine = AudioEngine(envSettings: TestAudioEnvironmentSettings(), mixWithOthers: true) + let empty_heading = Heading(orderedBy: [], course: nil, deviceHeading: nil, userHeading: nil, geolocationManager: nil) + /// The intersection of Sage Ave. and Burdett Ave. in Troy, NY + let sage_burdett_coord = CLLocation(latitude: 42.7290570, longitude: -73.6726370) + /// The 'front' of Barton Hall at RPI in Troy, NY + let barton_front_coord = CLLocation(latitude: 42.7294341, longitude: -73.6740136) + /// The ends of the bridge at the center of RPI, crossing 15th Street, Troy, NY + let rpi_bridge_east = CLLocation(latitude: 42.7292999, longitude: -73.6774054) + let rpi_bridge_west = CLLocation(latitude: 42.7293598, longitude: -73.6778063) + + class TestSearchProvider: POISearchProviderProtocol { + var providerName: String = "testsearchprovider123" + + func search(byKey: String) -> Soundscape.POI? { + return nil + } + + func objects(predicate: NSPredicate) -> [Soundscape.POI] { + return [] + } + } + + let search_provider = TestSearchProvider() + + override func setUp() { + // We have to provide our own POI search provider (turns locations into POIs) + SpatialDataCache.register(provider: search_provider) + } + + override func tearDownWithError() throws { + // Clean up our POI search provider + SpatialDataCache.removeAllProviders() + } + + func test_empty_init() throws { + let dm = DestinationManager(audioEngine: basic_audio_engine, collectionHeading: empty_heading) + // nope? XCTAssertFalse(dm.isDestinationSet) + XCTAssertFalse(dm.isAudioEnabled) + XCTAssertFalse(dm.isBeaconInBounds) + XCTAssertNil(dm.beaconPlayerId) + } + + func testBasicDestination() throws { + let dm = DestinationManager(audioEngine: basic_audio_engine, collectionHeading: empty_heading) + XCTAssertFalse(dm.isDestinationSet) + let ref_entity = try dm.setDestination(location: sage_burdett_coord, address: nil, enableAudio: false, userLocation: barton_front_coord) + XCTAssertFalse(dm.isUserWithinGeofence(barton_front_coord)) // we are not at the destination + XCTAssertTrue(dm.isDestinationSet) + XCTAssertTrue(dm.isDestination(key: ref_entity)) + XCTAssertFalse(dm.isDestination(key: "asdf (:")) + XCTAssertFalse(dm.isDestination(key: ref_entity + "AA")) + + // clear destination + + try dm.clearDestination() + XCTAssertFalse(dm.isDestinationSet) + XCTAssertFalse(dm.isAudioEnabled) + // clearing should delete our temporary destination location, meaning this should error: + XCTAssertThrowsError(try dm.setDestination(referenceID: ref_entity, enableAudio: false, userLocation: nil, logContext: nil)) + } + + /// geofence is within `EnterImmediateVicinityDistance` and `LeaveImmediateVicinityDistance` of the destination + func testDestinationInGeoFence() throws { + let dm = DestinationManager(audioEngine: basic_audio_engine, collectionHeading: empty_heading) + _ = try dm.setDestination(location: rpi_bridge_east, address: nil, enableAudio: true, userLocation: rpi_bridge_east) + XCTAssertFalse(dm.isAudioEnabled) // since we are already at the destination, audio should be disabled + XCTAssertTrue(dm.isUserWithinGeofence(rpi_bridge_east)) // we are at the destination + XCTAssertFalse(dm.isUserWithinGeofence(rpi_bridge_west)) // if we were across the bridge we would not be + // (how close is the other side of the bridge? should it be?) + XCTAssertFalse(dm.isUserWithinGeofence(barton_front_coord)) // definitely not from barton hall + + // clear destination + + try dm.clearDestination() + XCTAssertFalse(dm.isDestinationSet) + XCTAssertFalse(dm.isAudioEnabled) + XCTAssertFalse(dm.isUserWithinGeofence(rpi_bridge_east)) // no longer in geofence as it is gone + XCTAssertFalse(dm.isUserWithinGeofence(rpi_bridge_west)) // and still across the bridge isn't either + } + + + +} diff --git a/apps/ios/GuideDogs.xcodeproj/project.pbxproj b/apps/ios/GuideDogs.xcodeproj/project.pbxproj index 7612e172..c0280b4a 100644 --- a/apps/ios/GuideDogs.xcodeproj/project.pbxproj +++ b/apps/ios/GuideDogs.xcodeproj/project.pbxproj @@ -661,6 +661,7 @@ 62EACA2B26697F4300DBDECC /* WaypointDetailAnnotationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62EACA2A26697F4300DBDECC /* WaypointDetailAnnotationView.swift */; }; 62F7A30C27B6080900C62390 /* InteractiveBeaconView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62F7A30B27B6080900C62390 /* InteractiveBeaconView.swift */; }; 62F7A30E27B6082A00C62390 /* InteractiveBeaconViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62F7A30D27B6082A00C62390 /* InteractiveBeaconViewModel.swift */; }; + 9109FFF62ACCB10B002EC850 /* DestinationManagerTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9109FFF52ACCB10B002EC850 /* DestinationManagerTest.swift */; }; 6A4891BB2A5E66DE0002D146 /* ExternalNavigationApps.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A4891BA2A5E66DE0002D146 /* ExternalNavigationApps.swift */; }; 91C82AAD2A5DCF040086D126 /* GeolocationManagerTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 91C82AAC2A5DCF040086D126 /* GeolocationManagerTest.swift */; }; 91C82ABE2A6B08500086D126 /* RouteGuidanceTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 91C82ABD2A6B08500086D126 /* RouteGuidanceTest.swift */; }; @@ -1579,6 +1580,7 @@ 62EACA2A26697F4300DBDECC /* WaypointDetailAnnotationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WaypointDetailAnnotationView.swift; sourceTree = ""; }; 62F7A30B27B6080900C62390 /* InteractiveBeaconView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InteractiveBeaconView.swift; sourceTree = ""; }; 62F7A30D27B6082A00C62390 /* InteractiveBeaconViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InteractiveBeaconViewModel.swift; sourceTree = ""; }; + 9109FFF52ACCB10B002EC850 /* DestinationManagerTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DestinationManagerTest.swift; sourceTree = SOURCE_ROOT; }; 6A4891BA2A5E66DE0002D146 /* ExternalNavigationApps.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ExternalNavigationApps.swift; path = Code/App/ExternalNavigationApps.swift; sourceTree = ""; }; 815775252832F0650DF9D8D4 /* Pods-Soundscape.adhoc.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Soundscape.adhoc.xcconfig"; path = "Target Support Files/Pods-Soundscape/Pods-Soundscape.adhoc.xcconfig"; sourceTree = ""; }; 914DEBCD2A3CE6B9007B161C /* UnitTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = UnitTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -4284,10 +4286,19 @@ path = "Interactive View"; sourceTree = ""; }; + 9109FFF42ACCB0D5002EC850 /* Destination Manager */ = { + isa = PBXGroup; + children = ( + 9109FFF52ACCB10B002EC850 /* DestinationManagerTest.swift */, + ); + path = "Destination Manager"; + sourceTree = ""; + }; 914DEBCE2A3CE6B9007B161C /* UnitTests */ = { isa = PBXGroup; children = ( 91C82ABB2A6B03790086D126 /* Behaviors */, + 91C82AB52A67182E0086D126 /* Data */, 91C82AA62A4F56A70086D126 /* Sensors */, 914DEBD92A3CE7E6007B161C /* App */, ); @@ -4318,6 +4329,14 @@ path = Sensors; sourceTree = ""; }; + 91C82AB52A67182E0086D126 /* Data */ = { + isa = PBXGroup; + children = ( + 9109FFF42ACCB0D5002EC850 /* Destination Manager */, + ); + path = Data; + sourceTree = ""; + }; 91C82AA72A4F56F80086D126 /* Geolocation */ = { isa = PBXGroup; children = ( @@ -5563,6 +5582,7 @@ buildActionMask = 2147483647; files = ( 91C82ABE2A6B08500086D126 /* RouteGuidanceTest.swift in Sources */, + 9109FFF62ACCB10B002EC850 /* DestinationManagerTest.swift in Sources */, 91C82AAD2A5DCF040086D126 /* GeolocationManagerTest.swift in Sources */, 91DC0CF92A46134600244CC8 /* GeometryUtilsTest.swift in Sources */, ); diff --git a/apps/ios/GuideDogs/Code/Data/Destination Manager/DestinationManager.swift b/apps/ios/GuideDogs/Code/Data/Destination Manager/DestinationManager.swift index f93aba5c..ba599904 100644 --- a/apps/ios/GuideDogs/Code/Data/Destination Manager/DestinationManager.swift +++ b/apps/ios/GuideDogs/Code/Data/Destination Manager/DestinationManager.swift @@ -186,7 +186,7 @@ class DestinationManager: DestinationManagerProtocol { // Don't directly access the beacon POI - we don't want to have to retrieve it from // the database at frequency we receive heading updates. Instead, just check if there is - // currently a beacon playing, and then only update the `isBeaconInBounds` flag is there is. + // currently a beacon playing, and then only update the `isBeaconInBounds` flag if there is. guard self.beaconPlayerId != nil else { return } @@ -211,6 +211,10 @@ class DestinationManager: DestinationManagerProtocol { // MARK: Manage Destination Methods + /// Checks whether the current destination is the specified key. + /// - Parameters: + /// - key: the destination's entity key in the `SpatialDataCache`. Same as the referenceID in `setDestination()`. + /// - Returns: `false` if the destination isn't set or the entity key doesn't match the destination func isDestination(key: String) -> Bool { guard destinationKey == key || destination?.entityKey == key else { // Return false if the destination isn't set or the entityKey doesn't match the destination From 16605800985cbd889201e8766caafd7a1b02c8af Mon Sep 17 00:00:00 2001 From: 2kai2kai2 <2kai2kai2@gmail.com> Date: Thu, 12 Oct 2023 20:21:20 -0400 Subject: [PATCH 07/14] Add basic AudioEngineTest - Also include code coverage --- apps/ios/GuideDogs.xcodeproj/project.pbxproj | 12 +++ apps/ios/UnitTests.xctestplan | 10 +- .../ios/UnitTests/Audio/AudioEngineTest.swift | 101 ++++++++++++++++++ 3 files changed, 122 insertions(+), 1 deletion(-) create mode 100644 apps/ios/UnitTests/Audio/AudioEngineTest.swift diff --git a/apps/ios/GuideDogs.xcodeproj/project.pbxproj b/apps/ios/GuideDogs.xcodeproj/project.pbxproj index c0280b4a..19803a8e 100644 --- a/apps/ios/GuideDogs.xcodeproj/project.pbxproj +++ b/apps/ios/GuideDogs.xcodeproj/project.pbxproj @@ -663,6 +663,7 @@ 62F7A30E27B6082A00C62390 /* InteractiveBeaconViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62F7A30D27B6082A00C62390 /* InteractiveBeaconViewModel.swift */; }; 9109FFF62ACCB10B002EC850 /* DestinationManagerTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9109FFF52ACCB10B002EC850 /* DestinationManagerTest.swift */; }; 6A4891BB2A5E66DE0002D146 /* ExternalNavigationApps.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A4891BA2A5E66DE0002D146 /* ExternalNavigationApps.swift */; }; + 914BAAFD2AD7483300CB2171 /* AudioEngineTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 914BAAFC2AD7483300CB2171 /* AudioEngineTest.swift */; }; 91C82AAD2A5DCF040086D126 /* GeolocationManagerTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 91C82AAC2A5DCF040086D126 /* GeolocationManagerTest.swift */; }; 91C82ABE2A6B08500086D126 /* RouteGuidanceTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 91C82ABD2A6B08500086D126 /* RouteGuidanceTest.swift */; }; 91DC0CF92A46134600244CC8 /* GeometryUtilsTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 914DEBDC2A3CE901007B161C /* GeometryUtilsTest.swift */; }; @@ -1583,6 +1584,7 @@ 9109FFF52ACCB10B002EC850 /* DestinationManagerTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DestinationManagerTest.swift; sourceTree = SOURCE_ROOT; }; 6A4891BA2A5E66DE0002D146 /* ExternalNavigationApps.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ExternalNavigationApps.swift; path = Code/App/ExternalNavigationApps.swift; sourceTree = ""; }; 815775252832F0650DF9D8D4 /* Pods-Soundscape.adhoc.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Soundscape.adhoc.xcconfig"; path = "Target Support Files/Pods-Soundscape/Pods-Soundscape.adhoc.xcconfig"; sourceTree = ""; }; + 914BAAFC2AD7483300CB2171 /* AudioEngineTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AudioEngineTest.swift; sourceTree = ""; }; 914DEBCD2A3CE6B9007B161C /* UnitTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = UnitTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 914DEBDC2A3CE901007B161C /* GeometryUtilsTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeometryUtilsTest.swift; sourceTree = ""; }; 91C82AAC2A5DCF040086D126 /* GeolocationManagerTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = " GeolocationManagerTest.swift"; path = "UnitTests/Sensors/Geolocation/Geolocation Manager/ GeolocationManagerTest.swift"; sourceTree = SOURCE_ROOT; }; @@ -4294,9 +4296,18 @@ path = "Destination Manager"; sourceTree = ""; }; + 914BAAFB2AD747C200CB2171 /* Audio */ = { + isa = PBXGroup; + children = ( + 914BAAFC2AD7483300CB2171 /* AudioEngineTest.swift */, + ); + path = Audio; + sourceTree = ""; + }; 914DEBCE2A3CE6B9007B161C /* UnitTests */ = { isa = PBXGroup; children = ( + 914BAAFB2AD747C200CB2171 /* Audio */, 91C82ABB2A6B03790086D126 /* Behaviors */, 91C82AB52A67182E0086D126 /* Data */, 91C82AA62A4F56A70086D126 /* Sensors */, @@ -5581,6 +5592,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 914BAAFD2AD7483300CB2171 /* AudioEngineTest.swift in Sources */, 91C82ABE2A6B08500086D126 /* RouteGuidanceTest.swift in Sources */, 9109FFF62ACCB10B002EC850 /* DestinationManagerTest.swift in Sources */, 91C82AAD2A5DCF040086D126 /* GeolocationManagerTest.swift in Sources */, diff --git a/apps/ios/UnitTests.xctestplan b/apps/ios/UnitTests.xctestplan index f8729279..c4e2fa5b 100644 --- a/apps/ios/UnitTests.xctestplan +++ b/apps/ios/UnitTests.xctestplan @@ -9,7 +9,15 @@ } ], "defaultOptions" : { - "codeCoverage" : false, + "codeCoverage" : { + "targets" : [ + { + "containerPath" : "container:GuideDogs.xcodeproj", + "identifier" : "D3D7CFC71B3D96460020B5E9", + "name" : "Soundscape" + } + ] + }, "commandLineArgumentEntries" : [ { "argument" : "-TESTING" diff --git a/apps/ios/UnitTests/Audio/AudioEngineTest.swift b/apps/ios/UnitTests/Audio/AudioEngineTest.swift new file mode 100644 index 00000000..9b618c89 --- /dev/null +++ b/apps/ios/UnitTests/Audio/AudioEngineTest.swift @@ -0,0 +1,101 @@ +// +// AudioEngineTest.swift +// +// +// Created by Kai on 10/11/23. +// + +import XCTest +@testable import Soundscape +import AVFAudio + +class TestAudioEnvironmentSettings: EnvironmentSettingsProvider { + var envRenderingAlgorithm: AVAudio3DMixingRenderingAlgorithm = .auto + var envRenderingDistance: Double = 40 + var envRenderingReverbEnable: Bool = false + var envRenderingReverbPreset: AVAudioUnitReverbPreset = .mediumRoom + var envRenderingReverbBlend: Float = 0 + var envRenderingReverbLevel: Float = 0 + var envReverbFilterActive: Bool = false + var envReverbFilterBandwidth: Float = 0 + var envReverbFilterBypass: Bool = true + var envReverbFilterType: AVAudioUnitEQFilterType = .parametric + var envReverbFilterFrequency: Float = 0 + var envReverbFilterGain: Float = 0 +} + +final class AudioEngineTest: XCTestCase { + class TestAudioEngineDelegate: AudioEngineDelegate { + func didFinishPlaying() { + finish_count += 1 + } + + var finish_count = 0 + } + + /// Ensures the initial state is correct + func testInit() throws { + let eng = AudioEngine(envSettings: TestAudioEnvironmentSettings(), mixWithOthers: false) + XCTAssertNil(eng.delegate) + XCTAssertFalse(eng.isInMonoMode) // currently always true as it is not implemented + XCTAssertFalse(eng.isDiscreteAudioPlaying) + XCTAssertFalse(eng.isRecording) + XCTAssertFalse(eng.mixWithOthers) + } + + func testPlayBad() throws { + // TODO: this + } + + /// Just playing a single `Sound` + func testDiscreteAudio2DSimple() throws { + let eng = AudioEngine(envSettings: TestAudioEnvironmentSettings(), mixWithOthers: false) + let delegate = TestAudioEngineDelegate() + eng.delegate = delegate + let expectation = XCTestExpectation() + + XCTAssertEqual(delegate.finish_count, 0) + XCTAssertFalse(eng.isDiscreteAudioPlaying) + eng.play(TTSSound("testing a sound")) { success in + XCTAssertTrue(success) + XCTAssertEqual(delegate.finish_count, 1) + expectation.fulfill() + } + XCTAssertEqual(XCTWaiter.wait(for: [expectation], timeout: 10), .completed) + XCTAssertEqual(delegate.finish_count, 1) + XCTAssertFalse(eng.isDiscreteAudioPlaying) + } + + /// Play a series of queued sounds in sequence + func testDiscreteAudio2DSeveral() throws { + let eng = AudioEngine(envSettings: TestAudioEnvironmentSettings(), mixWithOthers: false) + let delegate = TestAudioEngineDelegate() + eng.delegate = delegate + let expectations = [XCTestExpectation(description: "one"), + XCTestExpectation(description: "two"), + XCTestExpectation(description: "three")] + + XCTAssertEqual(delegate.finish_count, 0) + XCTAssertFalse(eng.isDiscreteAudioPlaying) + eng.play(TTSSound("one one one")) { success in + XCTAssertTrue(success) + XCTAssertEqual(delegate.finish_count, 1) + expectations[0].fulfill() + } + eng.play(TTSSound("two two two")) { success in + XCTAssertTrue(success) + XCTAssertEqual(delegate.finish_count, 2) + expectations[1].fulfill() + } + eng.play(TTSSound("three three three")) { success in + XCTAssertTrue(success) + XCTAssertEqual(delegate.finish_count, 3) + expectations[2].fulfill() + } + + XCTAssertEqual(XCTWaiter.wait(for: expectations, timeout: 10, enforceOrder: true), .completed) + XCTAssertEqual(delegate.finish_count, 3) + XCTAssertFalse(eng.isDiscreteAudioPlaying) + } + +} From 0790b740e3a13994fd254ba3503dd3d6439b0b30 Mon Sep 17 00:00:00 2001 From: 2kai2kai2 <2kai2kai2@gmail.com> Date: Thu, 12 Oct 2023 20:45:06 -0400 Subject: [PATCH 08/14] Move DestinationManagerTest It's currently in the wrong place. --- apps/ios/GuideDogs.xcodeproj/project.pbxproj | 33 +++++++++++-------- .../DestinationManagerTest.swift | 0 2 files changed, 20 insertions(+), 13 deletions(-) rename apps/ios/{ => UnitTests/Data/Destination Manager}/DestinationManagerTest.swift (100%) diff --git a/apps/ios/GuideDogs.xcodeproj/project.pbxproj b/apps/ios/GuideDogs.xcodeproj/project.pbxproj index 19803a8e..b312c7a2 100644 --- a/apps/ios/GuideDogs.xcodeproj/project.pbxproj +++ b/apps/ios/GuideDogs.xcodeproj/project.pbxproj @@ -661,8 +661,8 @@ 62EACA2B26697F4300DBDECC /* WaypointDetailAnnotationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62EACA2A26697F4300DBDECC /* WaypointDetailAnnotationView.swift */; }; 62F7A30C27B6080900C62390 /* InteractiveBeaconView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62F7A30B27B6080900C62390 /* InteractiveBeaconView.swift */; }; 62F7A30E27B6082A00C62390 /* InteractiveBeaconViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62F7A30D27B6082A00C62390 /* InteractiveBeaconViewModel.swift */; }; - 9109FFF62ACCB10B002EC850 /* DestinationManagerTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9109FFF52ACCB10B002EC850 /* DestinationManagerTest.swift */; }; 6A4891BB2A5E66DE0002D146 /* ExternalNavigationApps.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A4891BA2A5E66DE0002D146 /* ExternalNavigationApps.swift */; }; + 914BAAF32AD745E400CB2171 /* DestinationManagerTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 914BAAF22AD745E400CB2171 /* DestinationManagerTest.swift */; }; 914BAAFD2AD7483300CB2171 /* AudioEngineTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 914BAAFC2AD7483300CB2171 /* AudioEngineTest.swift */; }; 91C82AAD2A5DCF040086D126 /* GeolocationManagerTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 91C82AAC2A5DCF040086D126 /* GeolocationManagerTest.swift */; }; 91C82ABE2A6B08500086D126 /* RouteGuidanceTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 91C82ABD2A6B08500086D126 /* RouteGuidanceTest.swift */; }; @@ -1581,9 +1581,9 @@ 62EACA2A26697F4300DBDECC /* WaypointDetailAnnotationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WaypointDetailAnnotationView.swift; sourceTree = ""; }; 62F7A30B27B6080900C62390 /* InteractiveBeaconView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InteractiveBeaconView.swift; sourceTree = ""; }; 62F7A30D27B6082A00C62390 /* InteractiveBeaconViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InteractiveBeaconViewModel.swift; sourceTree = ""; }; - 9109FFF52ACCB10B002EC850 /* DestinationManagerTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DestinationManagerTest.swift; sourceTree = SOURCE_ROOT; }; 6A4891BA2A5E66DE0002D146 /* ExternalNavigationApps.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ExternalNavigationApps.swift; path = Code/App/ExternalNavigationApps.swift; sourceTree = ""; }; 815775252832F0650DF9D8D4 /* Pods-Soundscape.adhoc.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Soundscape.adhoc.xcconfig"; path = "Target Support Files/Pods-Soundscape/Pods-Soundscape.adhoc.xcconfig"; sourceTree = ""; }; + 914BAAF22AD745E400CB2171 /* DestinationManagerTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DestinationManagerTest.swift; sourceTree = ""; }; 914BAAFC2AD7483300CB2171 /* AudioEngineTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AudioEngineTest.swift; sourceTree = ""; }; 914DEBCD2A3CE6B9007B161C /* UnitTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = UnitTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 914DEBDC2A3CE901007B161C /* GeometryUtilsTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeometryUtilsTest.swift; sourceTree = ""; }; @@ -4288,10 +4288,17 @@ path = "Interactive View"; sourceTree = ""; }; - 9109FFF42ACCB0D5002EC850 /* Destination Manager */ = { + 914BAAED2AD745BC00CB2171 /* Services */ = { isa = PBXGroup; children = ( - 9109FFF52ACCB10B002EC850 /* DestinationManagerTest.swift */, + ); + path = Services; + sourceTree = ""; + }; + 914BAAF12AD745E400CB2171 /* Destination Manager */ = { + isa = PBXGroup; + children = ( + 914BAAF22AD745E400CB2171 /* DestinationManagerTest.swift */, ); path = "Destination Manager"; sourceTree = ""; @@ -4340,14 +4347,6 @@ path = Sensors; sourceTree = ""; }; - 91C82AB52A67182E0086D126 /* Data */ = { - isa = PBXGroup; - children = ( - 9109FFF42ACCB0D5002EC850 /* Destination Manager */, - ); - path = Data; - sourceTree = ""; - }; 91C82AA72A4F56F80086D126 /* Geolocation */ = { isa = PBXGroup; children = ( @@ -4364,6 +4363,14 @@ path = "Geolocation Manager"; sourceTree = ""; }; + 91C82AB52A67182E0086D126 /* Data */ = { + isa = PBXGroup; + children = ( + 914BAAF12AD745E400CB2171 /* Destination Manager */, + ); + path = Data; + sourceTree = ""; + }; 91C82ABB2A6B03790086D126 /* Behaviors */ = { isa = PBXGroup; children = ( @@ -5593,8 +5600,8 @@ buildActionMask = 2147483647; files = ( 914BAAFD2AD7483300CB2171 /* AudioEngineTest.swift in Sources */, + 914BAAF32AD745E400CB2171 /* DestinationManagerTest.swift in Sources */, 91C82ABE2A6B08500086D126 /* RouteGuidanceTest.swift in Sources */, - 9109FFF62ACCB10B002EC850 /* DestinationManagerTest.swift in Sources */, 91C82AAD2A5DCF040086D126 /* GeolocationManagerTest.swift in Sources */, 91DC0CF92A46134600244CC8 /* GeometryUtilsTest.swift in Sources */, ); diff --git a/apps/ios/DestinationManagerTest.swift b/apps/ios/UnitTests/Data/Destination Manager/DestinationManagerTest.swift similarity index 100% rename from apps/ios/DestinationManagerTest.swift rename to apps/ios/UnitTests/Data/Destination Manager/DestinationManagerTest.swift From 782cbe83961b68672a44d59b124f52e0241f66ce Mon Sep 17 00:00:00 2001 From: 2kai2kai2 <2kai2kai2@gmail.com> Date: Fri, 13 Oct 2023 23:32:36 -0400 Subject: [PATCH 09/14] Transition from iOS-GPX-Framework to CoreGPX Successfully builds and passes tests (though no tests use the GPX system) This is a major step towards resolving issue #64 - Removed `cocoapods` and `cocoapods-patch` - Replaced the outdated cocoapods dependency `iOS-GPX-Framework` (Objective-C) with `CoreGPX` (Swift, Swift Package Manager) - Rewrote GPX Extensions in Swift from Objective-C (note: if there are GPX bugs, look here first) --- apps/ios/Gemfile | 2 - apps/ios/Gemfile.lock | 69 --- apps/ios/GuideDogs.xcodeproj/project.pbxproj | 99 +--- .../contents.xcworkspacedata | 3 - .../xcshareddata/swiftpm/Package.resolved | 9 + .../Geo Extensions/GPXExtensions.swift | 512 ++++++++++++++++-- .../AuthoredActivityContent.swift | 74 +-- .../AuthoredActivityLoader.swift | 6 +- .../Preview/IntersectionSearchResult.swift | 2 +- .../Data/Preview/RoadAdjacentDataView.swift | 2 +- .../GPX Simulator/GPXSimulator.swift | 75 +-- .../GPX Simulator/GPXTracker.swift | 2 +- apps/ios/Podfile | 25 - apps/ios/Podfile.lock | 20 - 14 files changed, 591 insertions(+), 309 deletions(-) delete mode 100644 apps/ios/Podfile delete mode 100644 apps/ios/Podfile.lock diff --git a/apps/ios/Gemfile b/apps/ios/Gemfile index 0fc019de..e01f77a0 100644 --- a/apps/ios/Gemfile +++ b/apps/ios/Gemfile @@ -1,7 +1,5 @@ source 'https://rubygems.org' -gem 'cocoapods', '1.11.3' -gem 'cocoapods-patch', '1.0.2' gem 'fastlane' plugins_path = File.join(File.dirname(__FILE__), 'fastlane', 'Pluginfile') diff --git a/apps/ios/Gemfile.lock b/apps/ios/Gemfile.lock index c0ce2445..8ecbaa66 100644 --- a/apps/ios/Gemfile.lock +++ b/apps/ios/Gemfile.lock @@ -3,17 +3,8 @@ GEM specs: CFPropertyList (3.0.5) rexml - activesupport (6.1.7) - concurrent-ruby (~> 1.0, >= 1.0.2) - i18n (>= 1.6, < 2) - minitest (>= 5.1) - tzinfo (~> 2.0) - zeitwerk (~> 2.3) addressable (2.8.1) public_suffix (>= 2.0.2, < 6.0) - algoliasearch (1.27.5) - httpclient (~> 2.8, >= 2.8.3) - json (>= 1.5.1) artifactory (3.0.15) atomos (0.1.3) aws-eventstream (1.2.0) @@ -34,50 +25,10 @@ GEM aws-eventstream (~> 1, >= 1.0.2) babosa (1.0.4) claide (1.1.0) - cocoapods (1.11.3) - addressable (~> 2.8) - claide (>= 1.0.2, < 2.0) - cocoapods-core (= 1.11.3) - cocoapods-deintegrate (>= 1.0.3, < 2.0) - cocoapods-downloader (>= 1.4.0, < 2.0) - cocoapods-plugins (>= 1.0.0, < 2.0) - cocoapods-search (>= 1.0.0, < 2.0) - cocoapods-trunk (>= 1.4.0, < 2.0) - cocoapods-try (>= 1.1.0, < 2.0) - colored2 (~> 3.1) - escape (~> 0.0.4) - fourflusher (>= 2.3.0, < 3.0) - gh_inspector (~> 1.0) - molinillo (~> 0.8.0) - nap (~> 1.0) - ruby-macho (>= 1.0, < 3.0) - xcodeproj (>= 1.21.0, < 2.0) - cocoapods-core (1.11.3) - activesupport (>= 5.0, < 7) - addressable (~> 2.8) - algoliasearch (~> 1.0) - concurrent-ruby (~> 1.1) - fuzzy_match (~> 2.0.4) - nap (~> 1.0) - netrc (~> 0.11) - public_suffix (~> 4.0) - typhoeus (~> 1.0) - cocoapods-deintegrate (1.0.5) - cocoapods-downloader (1.6.3) - cocoapods-patch (1.0.2) - cocoapods (~> 1.11.0) - cocoapods-plugins (1.0.0) - nap - cocoapods-search (1.0.1) - cocoapods-trunk (1.6.0) - nap (>= 0.8, < 2.0) - netrc (~> 0.11) - cocoapods-try (1.2.0) colored (1.2) colored2 (3.1.2) commander (4.6.0) highline (~> 2.0.0) - concurrent-ruby (1.1.10) declarative (0.0.20) digest-crc (0.6.5) rake (>= 12.0.0, < 14.0.0) @@ -85,9 +36,6 @@ GEM unf (>= 0.0.5, < 1.0.0) dotenv (2.8.1) emoji_regex (3.2.3) - escape (0.0.4) - ethon (0.15.0) - ffi (>= 1.15.0) excon (0.100.0) faraday (1.10.3) faraday-em_http (~> 1.0) @@ -160,9 +108,6 @@ GEM fastlane-plugin-clean_testflight_testers (0.3.0) fastlane-plugin-sentry (1.15.0) os (~> 1.1, >= 1.1.4) - ffi (1.15.5) - fourflusher (2.3.1) - fuzzy_match (2.0.4) gh_inspector (1.1.3) google-apis-androidpublisher_v3 (0.45.0) google-apis-core (>= 0.11.0, < 2.a) @@ -206,22 +151,16 @@ GEM http-cookie (1.0.5) domain_name (~> 0.5) httpclient (2.8.3) - i18n (1.12.0) - concurrent-ruby (~> 1.0) jmespath (1.6.2) json (2.6.2) jwt (2.7.1) memoist (0.16.2) mini_magick (4.12.0) mini_mime (1.1.2) - minitest (5.16.3) - molinillo (0.8.0) multi_json (1.15.0) multipart-post (2.3.0) nanaimo (0.3.0) - nap (1.1.0) naturally (2.2.1) - netrc (0.11.0) optparse (0.1.1) os (1.1.4) plist (3.7.0) @@ -234,7 +173,6 @@ GEM retriable (3.1.2) rexml (3.2.5) rouge (2.0.7) - ruby-macho (2.5.1) ruby2_keywords (0.0.5) rubyzip (2.3.2) security (0.1.3) @@ -254,10 +192,6 @@ GEM tty-screen (0.8.1) tty-spinner (0.9.3) tty-cursor (~> 0.7) - typhoeus (1.4.0) - ethon (>= 0.9.0) - tzinfo (2.0.5) - concurrent-ruby (~> 1.0) uber (0.1.0) unf (0.1.4) unf_ext @@ -276,15 +210,12 @@ GEM rouge (~> 2.0.7) xcpretty-travis-formatter (1.0.1) xcpretty (~> 0.2, >= 0.0.7) - zeitwerk (2.6.4) PLATFORMS arm64-darwin-21 x86_64-darwin-22 DEPENDENCIES - cocoapods (= 1.11.3) - cocoapods-patch (= 1.0.2) fastlane fastlane-plugin-clean_testflight_testers fastlane-plugin-sentry diff --git a/apps/ios/GuideDogs.xcodeproj/project.pbxproj b/apps/ios/GuideDogs.xcodeproj/project.pbxproj index b312c7a2..eadc41e2 100644 --- a/apps/ios/GuideDogs.xcodeproj/project.pbxproj +++ b/apps/ios/GuideDogs.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 53; + objectVersion = 54; objects = { /* Begin PBXBuildFile section */ @@ -662,12 +662,12 @@ 62F7A30C27B6080900C62390 /* InteractiveBeaconView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62F7A30B27B6080900C62390 /* InteractiveBeaconView.swift */; }; 62F7A30E27B6082A00C62390 /* InteractiveBeaconViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62F7A30D27B6082A00C62390 /* InteractiveBeaconViewModel.swift */; }; 6A4891BB2A5E66DE0002D146 /* ExternalNavigationApps.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A4891BA2A5E66DE0002D146 /* ExternalNavigationApps.swift */; }; + 91172A732AD8D56D00E6E8E9 /* CoreGPX in Frameworks */ = {isa = PBXBuildFile; productRef = 91172A722AD8D56D00E6E8E9 /* CoreGPX */; }; 914BAAF32AD745E400CB2171 /* DestinationManagerTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 914BAAF22AD745E400CB2171 /* DestinationManagerTest.swift */; }; 914BAAFD2AD7483300CB2171 /* AudioEngineTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 914BAAFC2AD7483300CB2171 /* AudioEngineTest.swift */; }; 91C82AAD2A5DCF040086D126 /* GeolocationManagerTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 91C82AAC2A5DCF040086D126 /* GeolocationManagerTest.swift */; }; 91C82ABE2A6B08500086D126 /* RouteGuidanceTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 91C82ABD2A6B08500086D126 /* RouteGuidanceTest.swift */; }; 91DC0CF92A46134600244CC8 /* GeometryUtilsTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 914DEBDC2A3CE901007B161C /* GeometryUtilsTest.swift */; }; - B5499F0EEDE32E16F9814457 /* Pods_Soundscape.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DAEDD72453A8268FBA604D2F /* Pods_Soundscape.framework */; }; B90C27D61EAF81D600007368 /* Sound.swift in Sources */ = {isa = PBXBuildFile; fileRef = B90C27D51EAF81D600007368 /* Sound.swift */; }; B918EE9825100FFF00A5354A /* CalloutRangeContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = B918EE9725100FFF00A5354A /* CalloutRangeContext.swift */; }; B91D3F6427AB5546004159A8 /* UserAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = B91D3F6327AB5546004159A8 /* UserAction.swift */; }; @@ -868,7 +868,6 @@ C3FA2EFD236A3A6A00ADB8BC /* BannerContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3FA2EFC236A3A6A00ADB8BC /* BannerContainer.swift */; }; C3FA2EFF236A4A8000ADB8BC /* AlertContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3FA2EFE236A4A8000ADB8BC /* AlertContainer.swift */; }; C3FA2F01236A4A9500ADB8BC /* ModalViewContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3FA2F00236A4A9500ADB8BC /* ModalViewContainer.swift */; }; - C4F94572D08285E140899289 /* Pods_Soundscape.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0CB77B72F2800A9A8B08A9F0 /* Pods_Soundscape.framework */; }; D225D72E1E493A9700289270 /* LocationUpdateFilter.swift in Sources */ = {isa = PBXBuildFile; fileRef = D225D72D1E493A9700289270 /* LocationUpdateFilter.swift */; }; D22A38351E6100ED00C3AA71 /* ReverseGeocoderContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = D22A38341E6100ED00C3AA71 /* ReverseGeocoderContext.swift */; }; D22E23AF1E27EDE700E66FC6 /* VectorTile.swift in Sources */ = {isa = PBXBuildFile; fileRef = D22E23AE1E27EDE700E66FC6 /* VectorTile.swift */; }; @@ -880,7 +879,6 @@ D29832961E4E249700352A5A /* GeoJsonGeometry.swift in Sources */ = {isa = PBXBuildFile; fileRef = D29832951E4E249700352A5A /* GeoJsonGeometry.swift */; }; D30355D31B61401D00E1DDED /* GDAJSONObject.m in Sources */ = {isa = PBXBuildFile; fileRef = D30355D21B61401D00E1DDED /* GDAJSONObject.m */; }; D395B0F71B41DA15005A6407 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = D395B0F61B41DA15005A6407 /* Images.xcassets */; }; - DE65184E7A99BC8A024D5CE8 /* Pods_Soundscape.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 91DC0CF52A460FAA00244CC8 /* Pods_Soundscape.framework */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -906,7 +904,6 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ - 0CB77B72F2800A9A8B08A9F0 /* Pods_Soundscape.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Soundscape.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 2800ABF02654427300B2622F /* AuthoredActivityCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthoredActivityCell.swift; sourceTree = ""; }; 2800ABF326545F4C00B2622F /* AuthoredActivitiesList.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = AuthoredActivitiesList.swift; path = "GuideDogs/Code/Visual UI/Views/Authored Activities/AuthoredActivitiesList.swift"; sourceTree = SOURCE_ROOT; }; 2800ABF6265460C800B2622F /* RouteDetailsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RouteDetailsView.swift; sourceTree = ""; }; @@ -1255,7 +1252,6 @@ 28F94F762490425F0010D2B3 /* HapticEngine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = HapticEngine.swift; path = Code/Haptics/HapticEngine.swift; sourceTree = ""; }; 28FCD314281C93F0007CEFE7 /* HandledEventAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HandledEventAction.swift; sourceTree = ""; }; 28FD0C46206AAC37000FC731 /* NewFeaturesView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = NewFeaturesView.xib; path = "Code/Visual UI/Controls/New Feature Announcement/NewFeaturesView.xib"; sourceTree = ""; }; - 2CE0A71CF64BDEF1A8FCDF22 /* Pods-Soundscape.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Soundscape.release.xcconfig"; path = "Target Support Files/Pods-Soundscape/Pods-Soundscape.release.xcconfig"; sourceTree = ""; }; 2F4432FF1D36B5A300CA437D /* licenses.html */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.html; path = licenses.html; sourceTree = ""; }; 2F4433011D36B5B300CA437D /* AboutViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = AboutViewController.swift; path = "Code/Visual UI/View Controllers/Settings/AboutViewController.swift"; sourceTree = ""; }; 31013A6420D9ACBE00205D1F /* HomeViewController+RemoteControl.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = "HomeViewController+RemoteControl.swift"; path = "Code/Visual UI/View Controllers/Home/HomeViewController+RemoteControl.swift"; sourceTree = ""; }; @@ -1582,16 +1578,12 @@ 62F7A30B27B6080900C62390 /* InteractiveBeaconView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InteractiveBeaconView.swift; sourceTree = ""; }; 62F7A30D27B6082A00C62390 /* InteractiveBeaconViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InteractiveBeaconViewModel.swift; sourceTree = ""; }; 6A4891BA2A5E66DE0002D146 /* ExternalNavigationApps.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ExternalNavigationApps.swift; path = Code/App/ExternalNavigationApps.swift; sourceTree = ""; }; - 815775252832F0650DF9D8D4 /* Pods-Soundscape.adhoc.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Soundscape.adhoc.xcconfig"; path = "Target Support Files/Pods-Soundscape/Pods-Soundscape.adhoc.xcconfig"; sourceTree = ""; }; 914BAAF22AD745E400CB2171 /* DestinationManagerTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DestinationManagerTest.swift; sourceTree = ""; }; 914BAAFC2AD7483300CB2171 /* AudioEngineTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AudioEngineTest.swift; sourceTree = ""; }; 914DEBCD2A3CE6B9007B161C /* UnitTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = UnitTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 914DEBDC2A3CE901007B161C /* GeometryUtilsTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeometryUtilsTest.swift; sourceTree = ""; }; 91C82AAC2A5DCF040086D126 /* GeolocationManagerTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = " GeolocationManagerTest.swift"; path = "UnitTests/Sensors/Geolocation/Geolocation Manager/ GeolocationManagerTest.swift"; sourceTree = SOURCE_ROOT; }; 91C82ABD2A6B08500086D126 /* RouteGuidanceTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = RouteGuidanceTest.swift; path = "UnitTests/Behaviors/Route Guidance/RouteGuidanceTest.swift"; sourceTree = SOURCE_ROOT; }; - 91DC0CF32A460FAA00244CC8 /* iOS_GPX_Framework.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = iOS_GPX_Framework.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - 91DC0CF52A460FAA00244CC8 /* Pods_Soundscape.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Pods_Soundscape.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - 91DC0CF72A460FAA00244CC8 /* TBXML.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = TBXML.framework; sourceTree = BUILT_PRODUCTS_DIR; }; B90C27D51EAF81D600007368 /* Sound.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Sound.swift; path = Code/Audio/Protocols/Sound.swift; sourceTree = ""; }; B918EE9725100FFF00A5354A /* CalloutRangeContext.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CalloutRangeContext.swift; sourceTree = ""; }; B91D3F6327AB5546004159A8 /* UserAction.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UserAction.swift; sourceTree = ""; }; @@ -1801,7 +1793,6 @@ C3FA2F00236A4A9500ADB8BC /* ModalViewContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ModalViewContainer.swift; path = Code/Notifications/Containers/ModalViewContainer.swift; sourceTree = ""; }; C3FA2F02236B557F00ADB8BC /* sv-SE */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "sv-SE"; path = "sv-SE.lproj/InfoPlist.strings"; sourceTree = ""; }; C3FA2F03236B557F00ADB8BC /* sv-SE */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "sv-SE"; path = "sv-SE.lproj/Localizable.strings"; sourceTree = ""; }; - CA8EE99DA4F34403EFB5AF2B /* Pods-Soundscape.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Soundscape.debug.xcconfig"; path = "Target Support Files/Pods-Soundscape/Pods-Soundscape.debug.xcconfig"; sourceTree = ""; }; D225D72D1E493A9700289270 /* LocationUpdateFilter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = LocationUpdateFilter.swift; path = Code/Sensors/Geolocation/Filters/LocationUpdateFilter.swift; sourceTree = ""; }; D22A38341E6100ED00C3AA71 /* ReverseGeocoderContext.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; name = ReverseGeocoderContext.swift; path = Code/Generators/Geocoding/ReverseGeocoderContext.swift; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; D22E23AE1E27EDE700E66FC6 /* VectorTile.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = VectorTile.swift; path = Code/Data/Models/Helpers/VectorTile.swift; sourceTree = ""; }; @@ -1846,11 +1837,11 @@ 2866A7C92625020600EBA2D8 /* AppCenterCrashes in Frameworks */, 286F409D2624FF6000C3F80B /* NVActivityIndicatorView in Frameworks */, 2866A7D5262502D500EBA2D8 /* CocoaLumberjackSwift in Frameworks */, + 91172A732AD8D56D00E6E8E9 /* CoreGPX in Frameworks */, 286145EB2625099F000E5525 /* SDWebImage in Frameworks */, 281BCBFF262638290031298D /* RealmSwift in Frameworks */, 28F1ECE2262505DC00E964C0 /* Realm in Frameworks */, 2866A7C12625013400EBA2D8 /* DZNEmptyDataSet in Frameworks */, - C4F94572D08285E140899289 /* Pods_Soundscape.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -3018,9 +3009,6 @@ 4AD48659F673FA58D2DF2C07 /* Frameworks */ = { isa = PBXGroup; children = ( - 91DC0CF32A460FAA00244CC8 /* iOS_GPX_Framework.framework */, - 91DC0CF52A460FAA00244CC8 /* Pods_Soundscape.framework */, - 91DC0CF72A460FAA00244CC8 /* TBXML.framework */, 280FF2FC26AF6BDF00DE9C5D /* SwiftUI.framework */, 2876EE142433FE5600B0A137 /* CoreServices.framework */, D3E2A6361CAD5CA100A5192A /* Accelerate.framework */, @@ -3033,7 +3021,6 @@ D334CB751B9F682400546BF1 /* MapKit.framework */, D3CE458E1C1737AE00AC1A25 /* MobileCoreServices.framework */, D384DE871B867AD400884DE5 /* ReplayKit.framework */, - 0CB77B72F2800A9A8B08A9F0 /* Pods_Soundscape.framework */, ); name = Frameworks; sourceTree = ""; @@ -4288,13 +4275,6 @@ path = "Interactive View"; sourceTree = ""; }; - 914BAAED2AD745BC00CB2171 /* Services */ = { - isa = PBXGroup; - children = ( - ); - path = Services; - sourceTree = ""; - }; 914BAAF12AD745E400CB2171 /* Destination Manager */ = { isa = PBXGroup; children = ( @@ -4387,16 +4367,6 @@ path = "Route Guidance"; sourceTree = ""; }; - 94B9789D44F5F13621A0C9B1 /* Pods */ = { - isa = PBXGroup; - children = ( - CA8EE99DA4F34403EFB5AF2B /* Pods-Soundscape.debug.xcconfig */, - 2CE0A71CF64BDEF1A8FCDF22 /* Pods-Soundscape.release.xcconfig */, - 815775252832F0650DF9D8D4 /* Pods-Soundscape.adhoc.xcconfig */, - ); - path = Pods; - sourceTree = ""; - }; B91D3F6227AB5546004159A8 /* User Actions */ = { isa = PBXGroup; children = ( @@ -5227,7 +5197,6 @@ 914DEBCE2A3CE6B9007B161C /* UnitTests */, 4AD48659F673FA58D2DF2C07 /* Frameworks */, D3D7CFC91B3D96470020B5E9 /* Products */, - 94B9789D44F5F13621A0C9B1 /* Pods */, ); sourceTree = ""; }; @@ -5275,13 +5244,11 @@ isa = PBXNativeTarget; buildConfigurationList = D3D7CFEE1B3D96470020B5E9 /* Build configuration list for PBXNativeTarget "Soundscape" */; buildPhases = ( - 7EFF796CAF7E9A7CD6D9070B /* [CP] Check Pods Manifest.lock */, D3D7CFC41B3D96460020B5E9 /* Sources */, D3D7CFC51B3D96460020B5E9 /* Frameworks */, D3D7CFC61B3D96460020B5E9 /* Resources */, B9B06E971E6483900010936A /* CopyFiles */, 31500FEE224CA30000BD0A4E /* Localization Validation */, - 5B0340000E919C2F3805558C /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -5299,6 +5266,7 @@ 286145EA2625099F000E5525 /* SDWebImage */, 281BCBFE262638290031298D /* RealmSwift */, 283B0306264B03920092F74F /* SDWebImageSwiftUI */, + 91172A722AD8D56D00E6E8E9 /* CoreGPX */, ); productName = GuideDogs; productReference = D3D7CFC81B3D96470020B5E9 /* Soundscape.app */; @@ -5376,6 +5344,7 @@ 28F1ECE0262505DC00E964C0 /* XCRemoteSwiftPackageReference "realm-cocoa" */, 286145E92625099F000E5525 /* XCRemoteSwiftPackageReference "SDWebImage" */, 283B0305264B03920092F74F /* XCRemoteSwiftPackageReference "SDWebImageSwiftUI" */, + 91172A712AD8D56D00E6E8E9 /* XCRemoteSwiftPackageReference "CoreGPX" */, ); productRefGroup = D3D7CFC91B3D96470020B5E9 /* Products */; projectDirPath = ""; @@ -5550,48 +5519,6 @@ shellPath = /bin/sh; shellScript = "# Only run in debug type builds (\"Debug\", \"Debug-FF\", etc)\nif [[ ${CONFIGURATION} == \"Debug\"* ]]; then\n ${SRCROOT}/Scripts/LocalizationLinter/main.swift\n echo \"Skipping Localization Validation script (non-debug build)\"\nelse \n echo \"Skipping Localization Validation script (non-debug build)\"\nfi\n"; }; - 5B0340000E919C2F3805558C /* [CP] Embed Pods Frameworks */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Soundscape/Pods-Soundscape-frameworks.sh", - "${BUILT_PRODUCTS_DIR}/TBXML/TBXML.framework", - "${BUILT_PRODUCTS_DIR}/iOS-GPX-Framework/iOS_GPX_Framework.framework", - ); - name = "[CP] Embed Pods Frameworks"; - outputPaths = ( - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/TBXML.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/iOS_GPX_Framework.framework", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Soundscape/Pods-Soundscape-frameworks.sh\"\n"; - showEnvVarsInLog = 0; - }; - 7EFF796CAF7E9A7CD6D9070B /* [CP] Check Pods Manifest.lock */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - ); - inputPaths = ( - "${PODS_PODFILE_DIR_PATH}/Podfile.lock", - "${PODS_ROOT}/Manifest.lock", - ); - name = "[CP] Check Pods Manifest.lock"; - outputFileListPaths = ( - ); - outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-Soundscape-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; - }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ @@ -6469,7 +6396,6 @@ }; 3163C7431DFDB27D00C9E605 /* AdHoc */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 815775252832F0650DF9D8D4 /* Pods-Soundscape.adhoc.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = "AppIcon Beta"; BUNDLE_SPOKEN_NAME = "Soundscape Adhoc"; @@ -6758,7 +6684,6 @@ }; D3D7CFEF1B3D96470020B5E9 /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = CA8EE99DA4F34403EFB5AF2B /* Pods-Soundscape.debug.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = "AppIcon Beta"; BUNDLE_SPOKEN_NAME = "Soundscape Debug"; @@ -6821,7 +6746,6 @@ }; D3D7CFF01B3D96470020B5E9 /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 2CE0A71CF64BDEF1A8FCDF22 /* Pods-Soundscape.release.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; BUNDLE_SPOKEN_NAME = Soundscape; @@ -6982,6 +6906,14 @@ version = 10.11.0; }; }; + 91172A712AD8D56D00E6E8E9 /* XCRemoteSwiftPackageReference "CoreGPX" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/vincentneo/CoreGPX.git"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 0.9.2; + }; + }; /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ @@ -7035,6 +6967,11 @@ package = 28F1ECE0262505DC00E964C0 /* XCRemoteSwiftPackageReference "realm-cocoa" */; productName = Realm; }; + 91172A722AD8D56D00E6E8E9 /* CoreGPX */ = { + isa = XCSwiftPackageProductDependency; + package = 91172A712AD8D56D00E6E8E9 /* XCRemoteSwiftPackageReference "CoreGPX" */; + productName = CoreGPX; + }; /* End XCSwiftPackageProductDependency section */ }; rootObject = D3D7CFC01B3D96460020B5E9 /* Project object */; diff --git a/apps/ios/GuideDogs.xcworkspace/contents.xcworkspacedata b/apps/ios/GuideDogs.xcworkspace/contents.xcworkspacedata index 79181bcb..7e0f82e9 100644 --- a/apps/ios/GuideDogs.xcworkspace/contents.xcworkspacedata +++ b/apps/ios/GuideDogs.xcworkspace/contents.xcworkspacedata @@ -4,9 +4,6 @@ - - diff --git a/apps/ios/GuideDogs.xcworkspace/xcshareddata/swiftpm/Package.resolved b/apps/ios/GuideDogs.xcworkspace/xcshareddata/swiftpm/Package.resolved index 81f15f04..0732e7db 100644 --- a/apps/ios/GuideDogs.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/apps/ios/GuideDogs.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -18,6 +18,15 @@ "version" : "3.7.4" } }, + { + "identity" : "coregpx", + "kind" : "remoteSourceControl", + "location" : "https://github.com/vincentneo/CoreGPX.git", + "state" : { + "revision" : "90dea5dea799e2b3684f5c72f07141d8516ce80b", + "version" : "0.9.2" + } + }, { "identity" : "dznemptydataset", "kind" : "remoteSourceControl", diff --git a/apps/ios/GuideDogs/Code/App/Framework Extensions/Geo Extensions/GPXExtensions.swift b/apps/ios/GuideDogs/Code/App/Framework Extensions/Geo Extensions/GPXExtensions.swift index 98eb54cb..206a3b35 100644 --- a/apps/ios/GuideDogs/Code/App/Framework Extensions/Geo Extensions/GPXExtensions.swift +++ b/apps/ios/GuideDogs/Code/App/Framework Extensions/Geo Extensions/GPXExtensions.swift @@ -7,7 +7,7 @@ // import Foundation -import iOS_GPX_Framework +import CoreGPX import CoreMotion.CMMotionActivity public typealias GPXActivity = String @@ -46,10 +46,8 @@ extension GPXBounds { } } - self.init(minLatitude: minLatitude, - minLongitude: minLongitude, - maxLatitude: maxLatitude, - maxLongitude: maxLongitude) + self.init(minLatitude: minLatitude, maxLatitude: maxLatitude, + minLongitude: minLongitude, maxLongitude: maxLongitude) } } @@ -78,18 +76,438 @@ extension GPXRoot { let trackSegment = GPXTrackSegment() for gpxLocation in trackLocations { - trackSegment.addTrackpoint(GPXTrackPoint(with: gpxLocation)) + trackSegment.add(trackpoint: GPXTrackPoint(with: gpxLocation)) } let track = GPXTrack() - track.addTracksegment(trackSegment) + track.add(trackSegment: trackSegment) - root.addTrack(track) + root.add(track: track) return root } } +// MARK: Implement Custom GPX Extensions + +extension GPXExtensionsElement { + /// Sets the first child tag with the specified name, or creates a new one if it does not exist. + public func set_property(_ name: String, to value: String) { + for child in children { + if child.name == name { + child.text = value + return + } + } + let new_element = GPXExtensionsElement(name: value) + new_element.text = value + children.append(new_element) + } + + /// Gets the first child tag with the specified name, or nil if not found + public func get_property(_ name: String) -> String? { + for child in children { + if child.name == name { + return child.text + } + } + return nil + } +} + +enum GPXExtensionsKeys : String { + case kGPXTrackPointExtensions = "gpxtpx:TrackPointExtension" + case kGPXTrailsTrackExtensions = "trailsio:TrackExtension" + case kGPXTrailsTrackPointExtensions = "trailsio:TrackPointExtension" + case kGPXSoundscapeExtensions = "gpxgd:TrackPointExtension" // why is it called this??????? + case kGPXSoundscapeSharedContentExtensions = "gpxsc:meta" + case kGPXSoundscapeAnnotationExtensions = "gpxsc:annotations" + case kGPXSoundscapeLinkExtensions = "gpxsc:links" + case kGPXSoundscapePOIExtensions = "gpxsc:poi" +} + +/// Allows easy access to be provided to extensions +/// However, the generic version does nothing on its own. +/// Instead, each GPX extension has a (swift) extension that defines it +/// (kinda like a fancy inheritance using generics) +class GPXExtensionView where E.RawValue == String { + private weak var ref: GPXExtensionsElement? + init(_ ref: GPXExtensionsElement) { + // TODO: maybe add a way to assert/ensure we have matching `ref` tag and `E` + self.ref = ref + } + + public var still_valid: Bool { return ref != nil } + + private func get_single(_ key: E) -> String? { + return ref?.get_property(key.rawValue) + } + /// Allows conversion to string-convertible values such as integral numbers + private func get_single(_ key: E) -> T? { + guard let value: String = get_single(key) else { + return nil + } + return T(value) + } + private func set_single(_ key: E, to value: String?) { + guard let value = value else { + return + } + ref?.set_property(key.rawValue, to: value) + } + /// Allows conversion from string-convertible values such as integral numbers + private func set_single(_ key: E, to value: LosslessStringConvertible?) { + guard let value = value else { + return + } + ref?.set_property(key.rawValue, to: String(value)) + } +} +func BuildGPXExtension(_ type: GPXExtensionsKeys) -> GPXExtensionsElement { + return GPXExtensionsElement(name: type.rawValue) +} + +// TODO: Many of the properties in the various extensions are NSNumber in the Objective-C versions. That means they could be any number type, from floating point to integer types to booleans. We should probably figure out what they're actually supposed to be. + +/// child tags within a `GPXTrackPointExtensions` which has tag `gpxtpx:TrackPointExtension` +/// https://www8.garmin.com/xmlschemas/TrackPointExtensionv2.xsd +enum GPXTrackPointExtensionsProperties : String { + case kHeartRate = "gpxtpx:hr" // unsigned int + case kCadence = "gpxtpx:cad" // unsigned int + case kSpeed = "gpxtpx:speed" // double + case kCourse = "gpxtpx:course" // double +} +extension GPXExtensionView { + public var heartRate: UInt? { + get { get_single(.kHeartRate) } + set { set_single(.kHeartRate, to: newValue) } + } + public var cadence: UInt? { + get { get_single(.kCadence) } + set { set_single(.kCadence, to: newValue) } + } + public var speed: Double? { + get { get_single(.kSpeed) } + set { set_single(.kSpeed, to: newValue) } + } + public var course: Double? { + get { get_single(.kCourse) } + set { set_single(.kCourse, to: newValue) } + } +} + +enum GPXTrailsTrackExtensionsProperties : String { + case kElementActivity = "trailsio:activity" +} +extension GPXExtensionView { + // TODO: this +} + +enum GPXTrailsTrackPointExtensionsProperties : String { + case kElementHorizontalAcc = "trailsio:hacc" + case kElementVerticalAcc = "trailsio:vacc" + case kElementSteps = "trailsio:steps" +} +extension GPXExtensionView { + public var horizontalAcceleration: Double? { + get { get_single(.kElementHorizontalAcc) } + set { set_single(.kElementHorizontalAcc, to: newValue) } + } + public var verticalAcceleration: Double? { + get { get_single(.kElementVerticalAcc) } + set { set_single(.kElementVerticalAcc, to: newValue) } + } + public var steps: Double? { + get { get_single(.kElementSteps) } + set { set_single(.kElementSteps, to: newValue) } + } +} + +/// child tags within a `GPXSoundscapeExtensions` which has tag `gpxgd:TrackPointExtension` +enum GPXSoundscapeExtensionsProperties : String { + case kElementHorizontalAccuracy = "gpxgd:hor_acc" + case kElementVerticalAccuracy = "gpxgd:ver_acc" + + case kElementTrueHeading = "gpxgd:hdg_tru" + case kElementMagneticHeading = "gpxgd:hdg_mag" + case kElementHeadingAccuracy = "gpxgd:hdg_acc" + case kElementDeviceHeading = "gpxgd:hdg_dvc" + + case kElementFloorLevel = "gpxgd:flr_lvl" + + case kElementMotionActivity = "gpxgd:activity" +} + +extension GPXExtensionView { + public var horizontalAccuracy: Double? { + get { get_single(.kElementHorizontalAccuracy) } + set { set_single(.kElementHorizontalAccuracy, to: newValue) } + } + public var verticalAccuracy: Double? { + get { get_single(.kElementVerticalAccuracy) } + set { set_single(.kElementVerticalAccuracy, to: newValue) } + } + public var trueHeading: Double? { + get { get_single(.kElementTrueHeading) } + set { set_single(.kElementTrueHeading, to: newValue) } + } + public var magneticHeading: Double? { + get { get_single(.kElementMagneticHeading) } + set { set_single(.kElementMagneticHeading, to: newValue) } + } + public var headingAccuracy: Double? { + get { get_single(.kElementHeadingAccuracy) } + set { set_single(.kElementHeadingAccuracy, to: newValue) } + } + public var deviceHeading: Double? { + get { get_single(.kElementDeviceHeading) } + set { set_single(.kElementDeviceHeading, to: newValue) } + } + public var floorLevel: Double? { + get { get_single(.kElementFloorLevel) } + set { set_single(.kElementFloorLevel, to: newValue) } + } + public var motionActivity: String? { + get { get_single(.kElementMotionActivity) } + set { set_single(.kElementMotionActivity, to: newValue) } + } +} + +/// child tags within a `GPXSoundscapeSharedContentExtensions` which has tag `gpxsc:meta` +enum GPXSoundscapeSharedContentExtensionsProperties : String { + // Experience Meta Tags + case kElementID = "gpxsc:id" // 'required' + case kElementBehavior = "gpxsc:behavior" // 'required' + case kElementVersion = "gpxsc:version" + // Experience Tags + case kElementLocale = "gpxsc:locale" // 'required' +} +/// attribute keys within a `GPXSoundscapeSharedContentExtensions` which has tag `gpxsc:meta` +enum GPXSoundscapeSharedContentExtensionsAttributes : String { + case kAttributeStartDate = "start" + case kAttributeEndDate = "end" + case kAttributeExpires = "expires" +} +extension GPXExtensionView { + public var id: String? { + get { get_single(.kElementID) } + set { set_single(.kElementID, to: newValue) } + } + public var behavior: String? { + get { get_single(.kElementBehavior) } + set { set_single(.kElementBehavior, to: newValue) } + } + public var version: String? { + get { get_single(.kElementVersion) } + set { set_single(.kElementVersion, to: newValue) } + } + // TODO: apparently there may be a GPXSoundscapeRegion in here too? + /// If set to an invalid or unknown value, will not catch that and will save/read back that locale identifier + public var locale: Locale? { + get { + guard let locale_id = get_single(.kElementLocale) else { + return nil + } + return Locale(identifier: locale_id) + } + set { set_single(.kElementLocale, to: newValue?.identifier) } + } + /// This is the correct date formatter for the GPX format + private static let dateFormatter = ISO8601DateFormatter() + /// If nil, then start date is in the distant past + public var startDate: Date? { + get { + guard let startStr = ref?.attributes[GPXSoundscapeSharedContentExtensionsAttributes.kAttributeStartDate.rawValue], + let start = GPXExtensionView.dateFormatter.date(from: startStr) else { + return nil + } + return start + } + set { + var value: String? = nil + if let newValue = newValue, newValue != Date.distantPast { + value = GPXExtensionView.dateFormatter.string(from: newValue) + } + ref?.attributes[GPXSoundscapeSharedContentExtensionsAttributes.kAttributeStartDate.rawValue] = value + } + } + /// If nil, then end date in the distant future + public var endDate: Date? { + get { + guard let endStr = ref?.attributes[GPXSoundscapeSharedContentExtensionsAttributes.kAttributeEndDate.rawValue], + let end = GPXExtensionView.dateFormatter.date(from: endStr) else { + return nil + } + return end + } + set { + var value: String? = nil + if let newValue = newValue, newValue != Date.distantFuture { + value = GPXExtensionView.dateFormatter.string(from: newValue) + } + ref?.attributes[GPXSoundscapeSharedContentExtensionsAttributes.kAttributeEndDate.rawValue] = value + } + } + /// If not present, it does not expire (i.e. false) + public var expires: Bool { + get { ref?.attributes[GPXSoundscapeSharedContentExtensionsAttributes.kAttributeExpires.rawValue] == "true" } + set { ref?.attributes[GPXSoundscapeSharedContentExtensionsAttributes.kAttributeExpires.rawValue] = (newValue ? "true" : "false") } + } + /// Based on the Soundscape patch for the old Objective-C version of iOS-GPX-Framework + /// + /// Uses the `startDate` and `endDate` properties + public var availability: DateInterval { + get { + let start = startDate ?? Date.distantPast + let end = endDate ?? Date.distantFuture + return DateInterval(start: start, end: end) + } + set { + startDate = (newValue.start == Date.distantPast) ? nil : newValue.start + endDate = (newValue.end == Date.distantFuture) ? nil : newValue.end + } + } +} + +/// child tags within a `GPXSoundscapeAnnotationExtensions` which has tag `gpxsc:annotations` +enum GPXSoundscapeAnnotationExtensionsProperties : String { + case kAnnotation = "gpxsc:annotation" +} +/// attribute keys within a `GPXSoundscapeAnnotation` - note this is the single annotation tag, not the extension. +enum GPXSoundscapeAnnotationAttributes : String { + case kAttributeTitle = "title" + case kAttributeType = "type" +} +class GPXAnnotation { + // TODO: make this work better with the GPX system + var title: String? + /// Obj-C patch makes this non-null + var content: String + var type: String? + + init(element: GPXExtensionsElement) { + content = element.text ?? "" + title = element.attributes[GPXSoundscapeAnnotationAttributes.kAttributeTitle.rawValue] + type = element.attributes[GPXSoundscapeAnnotationAttributes.kAttributeType.rawValue] + } +} +extension GPXExtensionView { + public var annotations: [GPXAnnotation] { + get { + // We store stuff as GPXSoundscapeLink which is an empty subclass of GPXLink + return ref?.children.filter({$0.name == E.kAnnotation.rawValue}).compactMap( GPXAnnotation.init ) ?? [] + } + // TODO: a setter maybe? + } +} + +/// child tags within a `GPXSoundscapeAnnotationExtensions` which has tag `gpxsc:annotations` +enum GPXSoundscapeLinkExtensionsProperties : String { + case kLink = "gpxsc:link" +} +extension GPXExtensionView { + public var links: [GPXLink] { + get { + // We store stuff as GPXSoundscapeLink which is an empty subclass of GPXLink + return ref?.children.filter({$0.name == E.kLink.rawValue}).compactMap({ + let link = GPXLink() + link.mimetype = $0.get_property("type") + link.text = $0.get_property("text") + link.href = $0.attributes["href"] + return link + }) ?? [] + } + // TODO: a setter maybe? + } +} + +/// child tags within a `GPXSoundscapePOIExtensions` which has tag `gpxsc:poi` +enum GPXSoundscapePOIExtensionsProperties : String { + case kElementStreetAddress = "gpxsc:street" + case kElementPhone = "gpxsc:phone" + case kElementHomepage = "gpxsc:link" // this is the same type as `GPXSoundscapeLink` +} + +/// CoreGPX seems to prefer to leave things just as `GPXExtensionsElement`s so we'll just use that +/// But, for ease of use add ``ExtensionWrapper`` to allow easy lookup of named tags +extension GPXExtensions { + /// A getter specifically for our extensions + func get_ext(_ name: GPXExtensionsKeys) -> GPXExtensionsElement? { + return children.first { $0.name == name.rawValue } + } + + /// Formerly type `GPXTrackPointExtensions` + var garminExtensions: GPXExtensionView? { + guard let ext = get_ext(.kGPXTrackPointExtensions) else { + return nil + } + return GPXExtensionView(ext) + } + + /// Formerly type `GPXTrailsTrackExtensions` + var trailsTrackExtensions: GPXExtensionView? { + guard let ext = get_ext(.kGPXTrailsTrackExtensions) else { + return nil + } + return GPXExtensionView(ext) + } + + /// Formerly type `GPXTrailsTrackPointExtensions` + var trailsTrackPointExtensions: GPXExtensionView? { + guard let ext = get_ext(.kGPXTrailsTrackPointExtensions) else { + return nil + } + return GPXExtensionView(ext) + } + + /// Formerly type `GPXSoundscapeExtensions` + var soundscapeExtensions: GPXExtensionView? { + guard let ext = get_ext(.kGPXSoundscapeExtensions) else { + return nil + } + return GPXExtensionView(ext) + } + + /// Formerly type `GPXSoundscapeSharedContentExtensions` + /// see: ``GPXSoundscapeSharedContentExtensionsAttributes`` + var soundscapeSCExtensions: GPXExtensionView? { + guard let ext = get_ext(.kGPXSoundscapeSharedContentExtensions) else { + return nil + } + return GPXExtensionView(ext) + } + + /// Formerly type `GPXSoundscapeAnnotationExtensions` + /// Should contain `GPXSoundscapeAnnotation`s: see ``GPXSoundscapeAnnotationAttributes`` + var soundscapeAnnotationExtensions: GPXExtensionView? { + guard let ext = get_ext(.kGPXSoundscapeAnnotationExtensions) else { + return nil + } + return GPXExtensionView(ext) + } + + /// Formerly type `GPXSoundscapeLinkExtensions` + /// Should contain `GPXSoundscapeLink` with tag `gpxsc:link` + var soundscapeLinkExtensions: GPXExtensionView? { + guard let ext = get_ext(.kGPXSoundscapeLinkExtensions) else { + return nil + } + return GPXExtensionView(ext) + } + + /// Formerly type `GPXSoundscapePOIExtensions` + var soundscapePOIExtensions: GPXExtensionView? { + guard let ext = get_ext(.kGPXSoundscapePOIExtensions) else { + return nil + } + return GPXExtensionView(ext) + } + + // GPXSoundscapeRegion ???????? +} + +// MARK: End Implementing Custom GPX Extensions + extension GPXWaypoint { /// This can be used to check if a timestamp of a `CLLocation` created with a waypoint is compared to nil. @@ -112,26 +530,25 @@ extension GPXWaypoint { elevation = location.altitude time = location.timestamp - let garminExtension = GPXTrackPointExtensions() - garminExtension.speed = NSNumber(value: location.speed) - garminExtension.course = NSNumber(value: location.course) + let extensions = GPXExtensions() + + extensions.children.append(BuildGPXExtension(.kGPXTrackPointExtensions)) + let garminExtension = extensions.garminExtensions! + garminExtension.speed = location.speed + garminExtension.course = location.course - let soundscapeExtension = GPXSoundscapeExtensions() - soundscapeExtension.horizontalAccuracy = NSNumber(value: location.horizontalAccuracy) - soundscapeExtension.verticalAccuracy = NSNumber(value: location.verticalAccuracy) + extensions.children.append(BuildGPXExtension(.kGPXSoundscapeExtensions)) + let soundscapeExtension = extensions.soundscapeExtensions! + soundscapeExtension.horizontalAccuracy = location.horizontalAccuracy + soundscapeExtension.verticalAccuracy = location.verticalAccuracy if let heading = gpxLocation.deviceHeading { - soundscapeExtension.deviceHeading = NSNumber(value: heading) + soundscapeExtension.deviceHeading = heading } - if let activity = gpxLocation.activity, activity != ActivityType.unknown.rawValue { - soundscapeExtension.activity = activity + soundscapeExtension.motionActivity = activity } - let extensions = GPXExtensions() - extensions.garminExtensions = garminExtension - extensions.soundscapeExtensions = soundscapeExtension - self.extensions = extensions } @@ -152,47 +569,61 @@ extension GPXWaypoint { var activity: GPXActivity? - // Backwards compatibility: previuosly openscape used the dilution values for accuracy - horizontalAccuracy = horizontalDilution - verticalAccuracy = verticalDilution + // Backwards compatibility: previously openscape used the dilution values for accuracy + horizontalAccuracy = horizontalDilution ?? horizontalAccuracy + verticalAccuracy = verticalDilution ?? verticalAccuracy if let extensions = extensions { - // Backwards compatibility: previuosly openscape used to store speed and course directly in the extensions class - speed = extensions.speed - course = extensions.course + // Backwards compatibility: previously openscape used to store speed and course directly in the extensions class + if let speedText = extensions.children.first(where: { $0.name == "speed" })?.text, + let speedNum = CLLocationSpeed(speedText) { + speed = speedNum + } + if let courseText = extensions.children.first(where: { $0.name == "course" })?.text, + let courseNum = CLLocationDirection(courseText) { + course = courseNum + } if let garminExtensions = extensions.garminExtensions { - if let garminSpeed = garminExtensions.speed, let speedNum = CLLocationSpeed(exactly: garminSpeed) { + if let garminSpeed = garminExtensions.speed, + let speedNum = CLLocationSpeed(exactly: garminSpeed) { speed = speedNum } - if let garminCourse = garminExtensions.course, let courseNum = CLLocationDirection(exactly: garminCourse) { + if let garminCourse = garminExtensions.course, + let courseNum = CLLocationDirection(exactly: garminCourse) { course = courseNum } } if let soundscapeExtensions = extensions.soundscapeExtensions { - if let hAcc = soundscapeExtensions.horizontalAccuracy, let hAccNum = CLLocationAccuracy(exactly: hAcc) { + if let hAcc = soundscapeExtensions.horizontalAccuracy, + let hAccNum = CLLocationAccuracy(exactly: hAcc) { horizontalAccuracy = hAccNum } - if let vAcc = soundscapeExtensions.verticalAccuracy, let vAccNum = CLLocationAccuracy(exactly: vAcc) { + if let vAcc = soundscapeExtensions.verticalAccuracy, + let vAccNum = CLLocationAccuracy(exactly: vAcc) { verticalAccuracy = vAccNum } - if let sTrueHeading = soundscapeExtensions.trueHeading, let sTrueHeadingNum = CLLocationDirection(exactly: sTrueHeading) { + if let sTrueHeading = soundscapeExtensions.trueHeading, + let sTrueHeadingNum = CLLocationDirection(exactly: sTrueHeading) { trueHeading = sTrueHeadingNum } - if let sMagneticHeading = soundscapeExtensions.magneticHeading, let sMagneticHeadingNum = CLLocationDirection(exactly: sMagneticHeading) { + if let sMagneticHeading = soundscapeExtensions.magneticHeading, + let sMagneticHeadingNum = CLLocationDirection(exactly: sMagneticHeading) { magneticHeading = sMagneticHeadingNum } - if let sHeadingAccuracy = soundscapeExtensions.headingAccuracy, let sHeadingAccuracyNum = CLLocationDirection(exactly: sHeadingAccuracy) { + if let sHeadingAccuracy = soundscapeExtensions.headingAccuracy, + let sHeadingAccuracyNum = CLLocationDirection(exactly: sHeadingAccuracy) { headingAccuracy = sHeadingAccuracyNum } - if let sDeviceHeading = soundscapeExtensions.deviceHeading, let sDeviceHeadingNum = CLLocationDirection(exactly: sDeviceHeading) { + if let sDeviceHeading = soundscapeExtensions.deviceHeading, + let sDeviceHeadingNum = CLLocationDirection(exactly: sDeviceHeading) { deviceHeading = sDeviceHeadingNum } @@ -213,12 +644,12 @@ extension GPXWaypoint { } } - activity = soundscapeExtensions.activity + activity = soundscapeExtensions.motionActivity } } - let location = CLLocation(coordinate: CLLocationCoordinate2D(latitude: latitude, longitude: longitude), - altitude: elevation, + let location = CLLocation(coordinate: CLLocationCoordinate2D(latitude: latitude!, longitude: longitude!), + altitude: elevation ?? 0, // default value??? horizontalAccuracy: horizontalAccuracy, verticalAccuracy: verticalAccuracy, course: course, @@ -232,10 +663,9 @@ extension GPXWaypoint { extension Array where Element == CLLocationCoordinate2D { func toGPXRoute() -> GPXRoute { - let routePoints = self.compactMap { GPXRoutePoint.routepoint(withLatitude: CGFloat($0.latitude), - longitude: CGFloat($0.longitude)) } + let routePoints = self.compactMap { GPXRoutePoint(latitude: $0.latitude, longitude: $0.longitude) } let route = GPXRoute() - route.addRoutepoints(routePoints) + route.points = routePoints return route } diff --git a/apps/ios/GuideDogs/Code/Data/Authored Activities/AuthoredActivityContent.swift b/apps/ios/GuideDogs/Code/Data/Authored Activities/AuthoredActivityContent.swift index a53bfb15..fed92634 100644 --- a/apps/ios/GuideDogs/Code/Data/Authored Activities/AuthoredActivityContent.swift +++ b/apps/ios/GuideDogs/Code/Data/Authored Activities/AuthoredActivityContent.swift @@ -7,7 +7,8 @@ // import Foundation -import iOS_GPX_Framework +import CoreLocation +import CoreGPX // MARK: - Data Models @@ -161,7 +162,10 @@ struct AuthoredActivityContent { // MARK: - Parsing GPX Event Data fileprivate extension GPXWaypoint { - var coordinate: CLLocationCoordinate2D { + var coordinate: CLLocationCoordinate2D? { + guard let latitude = latitude, let longitude = longitude else { + return nil + } return CLLocationCoordinate2D(latitude: latitude, longitude: longitude) } } @@ -181,7 +185,11 @@ extension AuthoredActivityContent { return nil } - guard let ext = metadata.extensions?.soundscapeSCExtensions, let actType = AuthoredActivityType.parse(ext.behavior) else { + guard let ext = metadata.extensions?.soundscapeSCExtensions, + let id = ext.id, // required here + let behavior = ext.behavior, // required here + let locale = ext.locale, // required here + let actType = AuthoredActivityType.parse(behavior) else { return nil } @@ -189,7 +197,7 @@ extension AuthoredActivityContent { return nil } - guard let creator = metadata.author.name, !creator.isEmpty else { + guard let creator = metadata.author?.name, !creator.isEmpty else { return nil } @@ -198,8 +206,8 @@ extension AuthoredActivityContent { } var imageURL: URL? - if let image = metadata.link, image.mimetype.hasPrefix("image") { - imageURL = URL(string: image.href) + if let image = metadata.links.first, image.mimetype?.hasPrefix("image") != nil, let href = image.href { + imageURL = URL(string: href) } // Parse the waypoints and POIs based on the file version @@ -212,11 +220,11 @@ extension AuthoredActivityContent { return nil } - return AuthoredActivityContent(id: ext.identifier, + return AuthoredActivityContent(id: id, type: actType, name: name, creator: creator, - locale: ext.locale, + locale: locale, availability: ext.availability, expires: ext.expires, image: imageURL, @@ -225,24 +233,25 @@ extension AuthoredActivityContent { pois: []) case "2": - guard let route = gpx.routes.first, route.routepoints.count > 0 else { + guard let route = gpx.routes.first, route.points.count > 0 else { return nil } - let wpts: [ActivityWaypoint] = waypoints(from: route.routepoints) + let wpts: [ActivityWaypoint] = waypoints(from: route.points) // For waypoints in this experience, require names, descriptions, and street addresses guard !wpts.isEmpty, !wpts.contains(where: { $0.name == nil }) else { return nil } - let pois = gpx.waypoints.map { ActivityPOI(coordinate: $0.coordinate, name: $0.name, description: $0.desc) } + // TODO: maybe don't use ! + let pois = gpx.waypoints.map { ActivityPOI(coordinate: $0.coordinate!, name: $0.name!, description: $0.desc) } - return AuthoredActivityContent(id: ext.identifier, + return AuthoredActivityContent(id: id, type: actType, name: name, creator: creator, - locale: ext.locale, + locale: locale, availability: ext.availability, expires: ext.expires, image: imageURL, @@ -264,37 +273,40 @@ extension AuthoredActivityContent { let audioMimeTypes = Set(["audio/mpeg", "audio/x-m4a"]) return waypoints.map { wpt in - let links = wpt.extensions?.soundscapeLinkExtensions?.links + let links = wpt.extensions?.soundscapeLinkExtensions?.links.filter({ + guard let mimetype = $0.mimetype else { return false } + return imageMimeTypes.contains(mimetype) + }) ?? [] - let parsedImages = links?.filter({ imageMimeTypes.contains($0.mimetype) }) - .compactMap { (link) -> ActivityWaypointImage? in - guard let url = URL(string: link.href) else { - return nil - } - - return ActivityWaypointImage(url: url, altText: link.text) + let parsedImages = links.compactMap { (link) -> ActivityWaypointImage? in + guard let href = link.href, + let url = URL(string: href) else { + return nil } + + return ActivityWaypointImage(url: url, altText: link.text) + } - let parsedAudioClips = links?.filter({ audioMimeTypes.contains($0.mimetype) }) - .compactMap { (link) -> ActivityWaypointAudioClip? in - guard let url = URL(string: link.href) else { - return nil - } - - return ActivityWaypointAudioClip(url: url, description: link.text) + let parsedAudioClips = links.compactMap { (link) -> ActivityWaypointAudioClip? in + guard let href = link.href, + let url = URL(string: href) else { + return nil } + + return ActivityWaypointAudioClip(url: url, description: link.text) + } let allAnnotations = wpt.extensions?.soundscapeAnnotationExtensions?.annotations let departure = allAnnotations?.first(where: { $0.type == "departure" })?.content let arrival = allAnnotations?.first(where: { $0.type == "arrival" })?.content - return ActivityWaypoint(coordinate: wpt.coordinate, + return ActivityWaypoint(coordinate: wpt.coordinate!, // TODO: maybe shouldn't be ! name: wpt.name, description: wpt.desc, departureCallout: departure, arrivalCallout: arrival, - images: parsedImages ?? [], - audioClips: parsedAudioClips ?? []) + images: parsedImages, + audioClips: parsedAudioClips) } } } diff --git a/apps/ios/GuideDogs/Code/Data/Authored Activities/AuthoredActivityLoader.swift b/apps/ios/GuideDogs/Code/Data/Authored Activities/AuthoredActivityLoader.swift index 0a2ad70e..b527325a 100644 --- a/apps/ios/GuideDogs/Code/Data/Authored Activities/AuthoredActivityLoader.swift +++ b/apps/ios/GuideDogs/Code/Data/Authored Activities/AuthoredActivityLoader.swift @@ -11,7 +11,7 @@ import Foundation import CoreLocation -import iOS_GPX_Framework +import CoreGPX enum ActivityLoaderError: Error { case loadingError @@ -123,7 +123,7 @@ class AuthoredActivityLoader { return nil } - guard let gpx = GPXParser.parseGPX(at: contentURL) else { + guard let gpx = GPXParser(withURL: contentURL)?.parsedData() else { return nil } @@ -285,7 +285,7 @@ class AuthoredActivityLoader { } // Validate that we can parse the new data - guard let gpx = GPXParser.parseGPX(with: data) else { + guard let gpx = GPXParser(withData: data).parsedData() else { GDLogWarn(.routeGuidance, "Unable to parse GPX for \(id)") NotificationCenter.default.post(name: .didTryActivityUpdate, object: self, userInfo: [ diff --git a/apps/ios/GuideDogs/Code/Data/Preview/IntersectionSearchResult.swift b/apps/ios/GuideDogs/Code/Data/Preview/IntersectionSearchResult.swift index a448509c..44950860 100644 --- a/apps/ios/GuideDogs/Code/Data/Preview/IntersectionSearchResult.swift +++ b/apps/ios/GuideDogs/Code/Data/Preview/IntersectionSearchResult.swift @@ -8,7 +8,7 @@ import Foundation import CoreLocation -import iOS_GPX_Framework +import CoreGPX /// An object representing a result of an intersection search along a road at a specific coordinate struct IntersectionSearchResult { diff --git a/apps/ios/GuideDogs/Code/Data/Preview/RoadAdjacentDataView.swift b/apps/ios/GuideDogs/Code/Data/Preview/RoadAdjacentDataView.swift index 0ab31143..ebd87e8f 100644 --- a/apps/ios/GuideDogs/Code/Data/Preview/RoadAdjacentDataView.swift +++ b/apps/ios/GuideDogs/Code/Data/Preview/RoadAdjacentDataView.swift @@ -9,7 +9,7 @@ import Foundation import CoreLocation import CocoaLumberjackSwift -import iOS_GPX_Framework +import CoreGPX struct RoadAdjacentDataView: AdjacentDataView, Equatable { diff --git a/apps/ios/GuideDogs/Code/Sensors/Geolocation/GPX Simulator/GPXSimulator.swift b/apps/ios/GuideDogs/Code/Sensors/Geolocation/GPX Simulator/GPXSimulator.swift index d5dd3fb0..26c1d071 100644 --- a/apps/ios/GuideDogs/Code/Sensors/Geolocation/GPX Simulator/GPXSimulator.swift +++ b/apps/ios/GuideDogs/Code/Sensors/Geolocation/GPX Simulator/GPXSimulator.swift @@ -7,7 +7,8 @@ // import Foundation -import iOS_GPX_Framework +import CoreLocation +import CoreGPX struct GPXIndex { static let zero = GPXIndex(track: 0, segment: 0, point: 0) @@ -108,11 +109,11 @@ class GPXSimulator { // MARK: Computed Properties var allTrackPoints: [GPXTrackPoint]? { - var trackPoints = [GPXTrackPoint]() + var trackPoints: [GPXTrackPoint] = [] for track in gpx.tracks { - for segment in track.tracksegments { - for point in segment.trackpoints { + for segment in track.segments { + for point in segment.points { trackPoints.append(point) } } @@ -131,8 +132,8 @@ class GPXSimulator { init?(gpx: GPXRoot) { // Check if file has any data guard let firstTrack = gpx.tracks.first, - let firstSegment = firstTrack.tracksegments.first, - firstSegment.trackpoints.first != nil else { + let firstSegment = firstTrack.segments.first, + firstSegment.points.first != nil else { return nil } @@ -149,7 +150,7 @@ class GPXSimulator { } convenience init?(filepath: String) { - guard let gpx = GPXParser.parseGPX(atPath: filepath) else { + guard let gpx = GPXParser(withPath: filepath)?.parsedData() else { return nil } @@ -539,15 +540,15 @@ class GPXSimulator { /// If the jump lands on the next or previous segment or track, it will return 0 as the point private func indexByJumpingPoints(_ index: GPXIndex, pointsToJump: Int) -> GPXIndex? { let track = gpx.tracks[currentIndex.track] - let segment = track.tracksegments[currentIndex.segment] + let segment = track.segments[currentIndex.segment] if currentIndex.point + pointsToJump < 0 { // Jump back to segment start return GPXIndex(track: currentIndex.track, segment: currentIndex.segment, point: 0) - } else if currentIndex.point + pointsToJump < segment.trackpoints.count { + } else if currentIndex.point + pointsToJump < segment.points.count { // Still inside current segment return currentIndex.indexByAddingPoints(pointsToJump) - } else if currentIndex.segment < track.tracksegments.count-1 { + } else if currentIndex.segment < track.segments.count-1 { // Next segment return currentIndex.nextSegment() } else if currentIndex.track == gpx.tracks.count-1 { @@ -565,16 +566,16 @@ class GPXSimulator { } let track = gpx.tracks[index.track] - guard index.segment >= 0 && index.segment < track.tracksegments.count else { + guard index.segment >= 0 && index.segment < track.segments.count else { return nil } - let segment = track.tracksegments[index.segment] - guard index.point >= 0 && index.point < segment.trackpoints.count else { + let segment = track.segments[index.segment] + guard index.point >= 0 && index.point < segment.points.count else { return nil } - let point = segment.trackpoints[index.point] + let point = segment.points[index.point] return point } @@ -592,8 +593,7 @@ class GPXSimulator { // Synthesize the speed if needed if !gpxLocation.location.speed.isValid && synthesizeSpeed && !trackPoint.hasSoundscapeExtension { - let speed = self.speed(for: index) - if speed.isValid { + if let speed = self.speed(for: index), speed.isValid { gpxLocation.location = gpxLocation.location.with(speed: speed) } } @@ -605,13 +605,16 @@ class GPXSimulator { guard let trackPoint = self.trackPoint(at: index) else { return -1 } // Try to get existing track course - if let trackCourse = trackPoint.course?.doubleValue, (trackCourse as CLLocationDirection).isValid { + if let trackCourseStr = trackPoint.extensions?.children.first(where: { $0.name == "course"})?.text, + let trackCourse = CLLocationDirection(trackCourseStr), + trackCourse.isValid { + // I am like 95% sure the previous code here meant the extensions course, but if that breaks you might want to look here first. return trackCourse } if let extensions = trackPoint.extensions, let garminExtensions = extensions.garminExtensions, - let trackCourse = garminExtensions.course?.doubleValue, + let trackCourse = garminExtensions.course, (trackCourse as CLLocationDirection).isValid { return trackCourse } @@ -639,17 +642,22 @@ class GPXSimulator { return trackPoint.gpxLocation().location.bearing(to: nextTrackPoint.gpxLocation().location) } - private func speed(for index: GPXIndex) -> CLLocationSpeed { - guard let trackPoint = self.trackPoint(at: index) else { return -1 } + private func speed(for index: GPXIndex) -> CLLocationSpeed? { + guard let trackPoint = self.trackPoint(at: index) else { + return nil + } // Try to get the existing speed value - if let trackSpeed = trackPoint.speed?.doubleValue, trackSpeed.isValid { + if let trackSpeedStr = trackPoint.extensions?.children.first(where: {$0.name == "speed"})?.text, + let trackSpeed = CLLocationSpeed(trackSpeedStr), + trackSpeed.isValid { + // I am like 95% sure the previous code here meant the extensions speed, but if that breaks you might want to look here first. return trackSpeed } if let extensions = trackPoint.extensions, let garminExtensions = extensions.garminExtensions, - let trackSpeed = garminExtensions.speed?.doubleValue, + let trackSpeed = garminExtensions.speed, trackSpeed.isValid { return trackSpeed } @@ -661,10 +669,16 @@ class GPXSimulator { } let prevIndex = GPXIndex(track: index.track, segment: index.segment, point: index.point-1) - guard let prevTrackPoint = self.trackPoint(at: prevIndex) else { return -1 } + guard let prevTrackPoint = self.trackPoint(at: prevIndex) else { + return nil + } - let location = CLLocation(latitude: trackPoint.latitude, longitude: trackPoint.longitude) - let prevLocation = CLLocation(latitude: prevTrackPoint.latitude, longitude: prevTrackPoint.longitude) + guard let currentLatitude = trackPoint.latitude, let currentLongitude = trackPoint.longitude, + let prevLatitude = prevTrackPoint.latitude, let prevLongitude = prevTrackPoint.longitude else { + return nil + } + let location = CLLocation(latitude: currentLatitude, longitude: currentLongitude) + let prevLocation = CLLocation(latitude: prevLatitude, longitude: prevLongitude) let distance = location.distance(from: prevLocation) let time = timeIntervalBetweenLocations @@ -674,8 +688,8 @@ class GPXSimulator { func index(for trackPoint: GPXTrackPoint) -> GPXIndex? { for (trackIndex, track) in gpx.tracks.enumerated() { - for (segmentIndex, segment) in track.tracksegments.enumerated() { - for (pointIndex, point) in segment.trackpoints.enumerated() where point === trackPoint { + for (segmentIndex, segment) in track.segments.enumerated() { + for (pointIndex, point) in segment.points.enumerated() where point === trackPoint { return GPXIndex(track: trackIndex, segment: segmentIndex, point: pointIndex) } } @@ -686,12 +700,12 @@ class GPXSimulator { private func trackPoints(trackIndex: Int, segmentIndex: Int? = nil) -> [GPXTrackPoint] { if let segmentIndex = segmentIndex { - return gpx.tracks[trackIndex].tracksegments[segmentIndex].trackpoints + return gpx.tracks[trackIndex].segments[segmentIndex].points } var trackPoints: [GPXTrackPoint] = [] - for trackSegment in gpx.tracks[trackIndex].tracksegments { - trackPoints.append(contentsOf: trackSegment.trackpoints) + for trackSegment in gpx.tracks[trackIndex].segments { + trackPoints.append(contentsOf: trackSegment.points) } return trackPoints @@ -712,7 +726,6 @@ class GPXSimulator { closestTrackPoint = trackPoint closestDistance = distance } - return closestTrackPoint } diff --git a/apps/ios/GuideDogs/Code/Sensors/Geolocation/GPX Simulator/GPXTracker.swift b/apps/ios/GuideDogs/Code/Sensors/Geolocation/GPX Simulator/GPXTracker.swift index 717084c0..0f08e2ec 100644 --- a/apps/ios/GuideDogs/Code/Sensors/Geolocation/GPX Simulator/GPXTracker.swift +++ b/apps/ios/GuideDogs/Code/Sensors/Geolocation/GPX Simulator/GPXTracker.swift @@ -7,7 +7,7 @@ // import Foundation -import iOS_GPX_Framework +import CoreGPX import CoreMotion.CMMotionActivity class GPXTracker { diff --git a/apps/ios/Podfile b/apps/ios/Podfile deleted file mode 100644 index 59ded582..00000000 --- a/apps/ios/Podfile +++ /dev/null @@ -1,25 +0,0 @@ -platform :ios, '14.1' -plugin 'cocoapods-patch' -use_frameworks! - -target 'Soundscape' do - project 'GuideDogs.xcodeproj' - - # When `pod install` is called, the 'cocoapods-patch' plugin - # will patches the pod with the diff file in `/patches`. - pod "iOS-GPX-Framework", '0.0.2' -end - -post_install do |installer| - installer.pods_project.build_configurations.each do |config| - config.build_settings['ARCHS[sdk=iphonesimulator*]'] = `uname -m` - end - - installer.generated_projects.each do |project| - project.targets.each do |target| - target.build_configurations.each do |config| - config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '12.0' - end - end - end -end diff --git a/apps/ios/Podfile.lock b/apps/ios/Podfile.lock deleted file mode 100644 index 33e4e65a..00000000 --- a/apps/ios/Podfile.lock +++ /dev/null @@ -1,20 +0,0 @@ -PODS: - - iOS-GPX-Framework (0.0.2): - - TBXML (~> 1.5) - - TBXML (1.5) - -DEPENDENCIES: - - iOS-GPX-Framework (= 0.0.2) - -SPEC REPOS: - trunk: - - iOS-GPX-Framework - - TBXML - -SPEC CHECKSUMS: - iOS-GPX-Framework: f6d50c6664a1747746b73da6899b1fc9a1f81ff6 - TBXML: 9f9dadb239f2f2af1dadb87fe43a0ce6e479379a - -PODFILE CHECKSUM: 46f7edc36ef0fb9b48f8dc798e182253e40b2adf - -COCOAPODS: 1.11.3 From f20391022dff28fa659ae7295a356d6107cd1f4c Mon Sep 17 00:00:00 2001 From: 2kai2kai2 <2kai2kai2@gmail.com> Date: Sat, 14 Oct 2023 03:16:13 -0400 Subject: [PATCH 10/14] Clean up remaining cocoapods stuff --- .github/workflows/ios-tests.yml | 4 +- .github/workflows/ios.disabled-yml | 7 +- apps/ios/GuideDogs.xcodeproj/project.pbxproj | 6 - .../Geo Extensions/GPXExtensions.swift | 1 + apps/ios/patches/iOS-GPX-Framework+0.0.2.diff | 2513 ----------------- docs/ios-client/onboarding.md | 16 +- 6 files changed, 6 insertions(+), 2541 deletions(-) delete mode 100644 apps/ios/patches/iOS-GPX-Framework+0.0.2.diff diff --git a/.github/workflows/ios-tests.yml b/.github/workflows/ios-tests.yml index 6b1413de..51148bab 100644 --- a/.github/workflows/ios-tests.yml +++ b/.github/workflows/ios-tests.yml @@ -11,9 +11,7 @@ jobs: runs-on: macos-latest steps: - uses: actions/checkout@v3 - - run: cd apps/ios && bundle install && gem uninstall cocoapods --version '>= 1.12.0' - # cocoapods-patch does not support cocoapods 1.12.0 yet - - run: cd apps/ios && pod install + - run: cd apps/ios && bundle install # may be able to skip this if we don't need fastlane - name: Build run: xcodebuild build-for-testing -workspace apps/ios/GuideDogs.xcworkspace -scheme Soundscape -destination 'platform=iOS Simulator,name=iPhone 13' - name: Test diff --git a/.github/workflows/ios.disabled-yml b/.github/workflows/ios.disabled-yml index a92678f0..f1afd471 100644 --- a/.github/workflows/ios.disabled-yml +++ b/.github/workflows/ios.disabled-yml @@ -16,15 +16,10 @@ jobs: steps: - name: Checkout uses: actions/checkout@v3 - - name: Install specific CocoaPods version - uses: maxim-lobanov/setup-cocoapods@v1 - with: - podfile-path: apps/ios/Podfile.lock - - name: Install CocoaPods packages + - name: Install Fastlane working-directory: apps/ios run: | bundle install - pod install - name: Set Default Scheme run: | scheme_list=$(xcodebuild -list -json | tr -d "\n") diff --git a/apps/ios/GuideDogs.xcodeproj/project.pbxproj b/apps/ios/GuideDogs.xcodeproj/project.pbxproj index eadc41e2..c269105f 100644 --- a/apps/ios/GuideDogs.xcodeproj/project.pbxproj +++ b/apps/ios/GuideDogs.xcodeproj/project.pbxproj @@ -6422,8 +6422,6 @@ HEADER_SEARCH_PATHS = ( "$(SDKROOT)/usr/include/libxml2", "$(inherited)", - "\"${PODS_ROOT}/Headers/Public\"", - "\"${PODS_ROOT}/Headers/Public/HockeySDK\"", ); INFOPLIST_FILE = GuideDogs/Assets/PropertyLists/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = Soundscape; @@ -6711,8 +6709,6 @@ HEADER_SEARCH_PATHS = ( "$(SDKROOT)/usr/include/libxml2", "$(inherited)", - "\"${PODS_ROOT}/Headers/Public\"", - "\"${PODS_ROOT}/Headers/Public/HockeySDK\"", ); INFOPLIST_FILE = GuideDogs/Assets/PropertyLists/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = Soundscape; @@ -6773,8 +6769,6 @@ HEADER_SEARCH_PATHS = ( "$(SDKROOT)/usr/include/libxml2", "$(inherited)", - "\"${PODS_ROOT}/Headers/Public\"", - "\"${PODS_ROOT}/Headers/Public/HockeySDK\"", ); INFOPLIST_FILE = GuideDogs/Assets/PropertyLists/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = Soundscape; diff --git a/apps/ios/GuideDogs/Code/App/Framework Extensions/Geo Extensions/GPXExtensions.swift b/apps/ios/GuideDogs/Code/App/Framework Extensions/Geo Extensions/GPXExtensions.swift index 206a3b35..8a990a70 100644 --- a/apps/ios/GuideDogs/Code/App/Framework Extensions/Geo Extensions/GPXExtensions.swift +++ b/apps/ios/GuideDogs/Code/App/Framework Extensions/Geo Extensions/GPXExtensions.swift @@ -7,6 +7,7 @@ // import Foundation +import CoreLocation import CoreGPX import CoreMotion.CMMotionActivity diff --git a/apps/ios/patches/iOS-GPX-Framework+0.0.2.diff b/apps/ios/patches/iOS-GPX-Framework+0.0.2.diff deleted file mode 100644 index a757ccb2..00000000 --- a/apps/ios/patches/iOS-GPX-Framework+0.0.2.diff +++ /dev/null @@ -1,2513 +0,0 @@ -diff --git a/cocoapods-patch-20221103-54518-lfrmhf/iOS-GPX-Framework/GPX/GPX.h b/Pods/iOS-GPX-Framework/GPX/GPX.h -index 1bb6675c9..8429652af 100644 ---- a/cocoapods-patch-20221103-54518-lfrmhf/iOS-GPX-Framework/GPX/GPX.h -+++ b/Pods/iOS-GPX-Framework/GPX/GPX.h -@@ -6,6 +6,7 @@ - // Copyright (c) 2012 NextBusinessSystem Co., Ltd. All rights reserved. - // - -+#import - #import "GPXParser.h" - #import "GPXConst.h" - #import "GPXType.h" -diff --git a/cocoapods-patch-20221103-54518-lfrmhf/iOS-GPX-Framework/GPX/GPXBounds.h b/Pods/iOS-GPX-Framework/GPX/GPXBounds.h -index e3fd09f3b..56baf8f5f 100644 ---- a/cocoapods-patch-20221103-54518-lfrmhf/iOS-GPX-Framework/GPX/GPXBounds.h -+++ b/Pods/iOS-GPX-Framework/GPX/GPXBounds.h -@@ -7,7 +7,7 @@ - // - - #import "GPXElement.h" -- -+@import CoreLocation; - - /** Two lat/lon pairs defining the extent of an element. - */ -@@ -19,16 +19,16 @@ - /// --------------------------------- - - /** The minimum latitude. */ --@property (nonatomic, assign) CGFloat minLatitude; -+@property (nonatomic) CLLocationDegrees minLatitude; - - /** The minimum longitude. */ --@property (nonatomic, assign) CGFloat minLongitude; -+@property (nonatomic) CLLocationDegrees minLongitude; - - /** The maximum latitude. */ --@property (nonatomic, assign) CGFloat maxLatitude; -+@property (nonatomic) CLLocationDegrees maxLatitude; - - /** The maximum longitude. */ --@property (nonatomic, assign) CGFloat maxLongitude; -+@property (nonatomic) CLLocationDegrees maxLongitude; - - - /// --------------------------------- -@@ -42,6 +42,6 @@ - @param maxLongitude The maximum longitude. - @return A newly created bounds element. - */ --+ (GPXBounds *)boundsWithMinLatitude:(CGFloat)minLatitude minLongitude:(CGFloat)minLongitude maxLatitude:(CGFloat)maxLatitude maxLongitude:(CGFloat)maxLongitude; -++ (GPXBounds *)boundsWithMinLatitude:(CLLocationDegrees)minLatitude minLongitude:(CLLocationDegrees)minLongitude maxLatitude:(CLLocationDegrees)maxLatitude maxLongitude:(CLLocationDegrees)maxLongitude; - - @end -diff --git a/cocoapods-patch-20221103-54518-lfrmhf/iOS-GPX-Framework/GPX/GPXBounds.m b/Pods/iOS-GPX-Framework/GPX/GPXBounds.m -index 21c7ad9ad..ebfafa791 100644 ---- a/cocoapods-patch-20221103-54518-lfrmhf/iOS-GPX-Framework/GPX/GPXBounds.m -+++ b/Pods/iOS-GPX-Framework/GPX/GPXBounds.m -@@ -36,7 +36,7 @@ - return self; - } - --+ (GPXBounds *)boundsWithMinLatitude:(CGFloat)minLatitude minLongitude:(CGFloat)minLongitude maxLatitude:(CGFloat)maxLatitude maxLongitude:(CGFloat)maxLongitude -++ (GPXBounds *)boundsWithMinLatitude:(CLLocationDegrees)minLatitude minLongitude:(CLLocationDegrees)minLongitude maxLatitude:(CLLocationDegrees)maxLatitude maxLongitude:(CLLocationDegrees)maxLongitude - { - GPXBounds *bounds = [GPXBounds new]; - bounds.minLatitude = minLatitude; -@@ -49,42 +49,42 @@ - - #pragma mark - Public methods - --- (CGFloat)minLatitude -+- (CLLocationDegrees)minLatitude - { - return [GPXType latitude:_minLatitudeValue]; - } - --- (void)setMinLatitude:(CGFloat)minLatitude -+- (void)setMinLatitude:(CLLocationDegrees)minLatitude - { - _minLatitudeValue = [GPXType valueForLatitude:minLatitude]; - } - --- (CGFloat)minLongitude -+- (CLLocationDegrees)minLongitude - { - return [GPXType longitude:_minLongitudeValue]; - } - --- (void)setMinLongitude:(CGFloat)minLongitude -+- (void)setMinLongitude:(CLLocationDegrees)minLongitude - { - _minLongitudeValue = [GPXType valueForLongitude:minLongitude]; - } - --- (CGFloat)maxLatitude -+- (CLLocationDegrees)maxLatitude - { - return [GPXType latitude:_maxLatitudeValue]; - } - --- (void)setMaxlat:(CGFloat)maxLatitude -+- (void)setMaxLatitude:(CLLocationDegrees)maxLatitude - { - _maxLatitudeValue = [GPXType valueForLatitude:maxLatitude]; - } - --- (CGFloat)maxLongitude -+- (CLLocationDegrees)maxLongitude - { - return [GPXType longitude:_maxLongitudeValue]; - } - --- (void)setMaxlon:(CGFloat)maxLongitude -+- (void)setMaxLongitude:(CLLocationDegrees)maxLongitude - { - _maxLongitudeValue = [GPXType valueForLongitude:maxLongitude]; - } -diff --git a/cocoapods-patch-20221103-54518-lfrmhf/iOS-GPX-Framework/GPX/GPXElement.h b/Pods/iOS-GPX-Framework/GPX/GPXElement.h -index a39f12437..3c9c753bd 100644 ---- a/cocoapods-patch-20221103-54518-lfrmhf/iOS-GPX-Framework/GPX/GPXElement.h -+++ b/Pods/iOS-GPX-Framework/GPX/GPXElement.h -@@ -20,7 +20,7 @@ - - /** A parent GPXElement of the receiver. - */ --@property (unsafe_unretained, nonatomic) GPXElement *parent; -+@property (weak, nonatomic) GPXElement *parent; - - - /// --------------------------------- -diff --git a/cocoapods-patch-20221103-54518-lfrmhf/iOS-GPX-Framework/GPX/GPXElement.m b/Pods/iOS-GPX-Framework/GPX/GPXElement.m -index 1458665ac..27ae7347e 100644 ---- a/cocoapods-patch-20221103-54518-lfrmhf/iOS-GPX-Framework/GPX/GPXElement.m -+++ b/Pods/iOS-GPX-Framework/GPX/GPXElement.m -@@ -170,7 +170,6 @@ - } - } - -- - #pragma mark - GPX - - - (NSString *)gpx { -diff --git a/cocoapods-patch-20221103-54518-lfrmhf/iOS-GPX-Framework/GPX/GPXElementSubclass.h b/Pods/iOS-GPX-Framework/GPX/GPXElementSubclass.h -index 8337e5ece..6ebc1d64d 100644 ---- a/cocoapods-patch-20221103-54518-lfrmhf/iOS-GPX-Framework/GPX/GPXElementSubclass.h -+++ b/Pods/iOS-GPX-Framework/GPX/GPXElementSubclass.h -@@ -7,7 +7,7 @@ - // - - #import "GPXElement.h" --#import "TBXML.h" -+#import - - @interface GPXElement () - -diff --git a/cocoapods-patch-20221103-54518-lfrmhf/iOS-GPX-Framework/GPX/GPXEmail.h b/Pods/iOS-GPX-Framework/GPX/GPXEmail.h -index 88d63441f..c1078fc36 100644 ---- a/cocoapods-patch-20221103-54518-lfrmhf/iOS-GPX-Framework/GPX/GPXEmail.h -+++ b/Pods/iOS-GPX-Framework/GPX/GPXEmail.h -@@ -30,7 +30,7 @@ - /// --------------------------------- - - /** Creates and returns a new email element. -- @param id half of email address (billgates2004) -+ @param emailID half of email address (billgates2004) - @param domain half of email address (hotmail.com) - @return A newly created email element. - */ -diff --git a/cocoapods-patch-20221103-54518-lfrmhf/iOS-GPX-Framework/GPX/GPXExtensions.h b/Pods/iOS-GPX-Framework/GPX/GPXExtensions.h -index 0ebc342b8..71873e191 100644 ---- a/cocoapods-patch-20221103-54518-lfrmhf/iOS-GPX-Framework/GPX/GPXExtensions.h -+++ b/Pods/iOS-GPX-Framework/GPX/GPXExtensions.h -@@ -7,10 +7,32 @@ - // - - #import "GPXElement.h" -+#import "GPXTrackPointExtensions.h" - -+@class GPXTrailsTrackExtensions; -+@class GPXTrailsTrackPointExtensions; -+@class GPXSoundscapeExtensions; -+@class GPXSoundscapeSharedContentExtensions; -+@class GPXSoundscapeAnnotationExtensions; -+@class GPXSoundscapeLinkExtensions; -+@class GPXSoundscapePOIExtensions; - - /** You can add extend GPX by adding your own elements from another schema here. - */ - @interface GPXExtensions : GPXElement - -+@property (strong, nullable) GPXTrackPointExtensions *garminExtensions; -+@property (strong, nullable) GPXTrailsTrackExtensions *trailsTrackExtensions; -+@property (strong, nullable) GPXTrailsTrackPointExtensions *trailsTrackPointExtensions; -+@property (strong, nullable) GPXSoundscapeExtensions *soundscapeExtensions; -+@property (strong, nullable) GPXSoundscapeSharedContentExtensions *soundscapeSCExtensions; -+@property (strong, nullable) GPXSoundscapeAnnotationExtensions * soundscapeAnnotationExtensions; -+@property (strong, nullable) GPXSoundscapeLinkExtensions * soundscapeLinkExtensions; -+@property (strong, nullable) GPXSoundscapePOIExtensions * soundscapePOIExtensions; -+ -+/// Backwards compatibility -+/// Speed and course are now in the Garmin TrackPoint extension -+@property (nonatomic, assign) CLLocationSpeed speed; -+@property (nonatomic, assign) CLLocationDirection course; -+ - @end -diff --git a/cocoapods-patch-20221103-54518-lfrmhf/iOS-GPX-Framework/GPX/GPXExtensions.m b/Pods/iOS-GPX-Framework/GPX/GPXExtensions.m -index 7fed5e039..d36b2f391 100644 ---- a/cocoapods-patch-20221103-54518-lfrmhf/iOS-GPX-Framework/GPX/GPXExtensions.m -+++ b/Pods/iOS-GPX-Framework/GPX/GPXExtensions.m -@@ -8,8 +8,19 @@ - - #import "GPXExtensions.h" - #import "GPXElementSubclass.h" -+#import "GPXTrailsTrackExtensions.h" -+#import "GPXTrailsTrackPointExtensions.h" -+#import "GPXSoundscapeExtensions.h" -+#import "GPXSoundscapeSharedContentExtensions.h" -+#import "GPXSoundscapeRegion.h" -+#import "GPXSoundscapeAnnotationExtensions.h" -+#import "GPXSoundscapeLinkExtensions.h" -+#import "GPXSoundscapePOIExtensions.h" - --@implementation GPXExtensions -+@implementation GPXExtensions { -+ NSString *_speedValue; -+ NSString *_courseValue; -+} - - #pragma mark - Instance - -@@ -17,14 +28,48 @@ - { - self = [super initWithXMLElement:element parent:parent]; - if (self) { -+ self.garminExtensions = (GPXTrackPointExtensions *)[self childElementOfClass:[GPXTrackPointExtensions class] xmlElement:element]; -+ self.trailsTrackExtensions = (GPXTrailsTrackExtensions *)[self childElementOfClass:[GPXTrailsTrackExtensions class] xmlElement:element]; -+ self.trailsTrackPointExtensions = (GPXTrailsTrackPointExtensions *)[self childElementOfClass:[GPXTrailsTrackPointExtensions class] xmlElement:element]; -+ self.soundscapeExtensions = (GPXSoundscapeExtensions *)[self childElementOfClass:[GPXSoundscapeExtensions class] xmlElement:element]; -+ self.soundscapeSCExtensions = (GPXSoundscapeSharedContentExtensions *)[self childElementOfClass:[GPXSoundscapeSharedContentExtensions class] xmlElement:element]; -+ self.soundscapeAnnotationExtensions = (GPXSoundscapeAnnotationExtensions *)[self childElementOfClass:[GPXSoundscapeAnnotationExtensions class] xmlElement:element]; -+ self.soundscapeLinkExtensions = (GPXSoundscapeLinkExtensions *)[self childElementOfClass:[GPXSoundscapeLinkExtensions class] xmlElement:element]; -+ self.soundscapePOIExtensions = (GPXSoundscapePOIExtensions *)[self childElementOfClass:[GPXSoundscapePOIExtensions class] xmlElement:element]; -+ -+ _speedValue = [self textForSingleChildElementNamed:@"speed" xmlElement:element]; -+ _courseValue = [self textForSingleChildElementNamed:@"course" xmlElement:element]; - } - return self; - } - -- - #pragma mark - Public methods - -+- (double)speed -+{ -+ if (!_speedValue) { -+ return -1; -+ } -+ return [GPXType decimal:_speedValue]; -+} -+ -+- (void)setSpeed:(double)speed -+{ -+ _speedValue = [GPXType valueForDecimal:speed]; -+} -+ -+- (double)course -+{ -+ if (!_courseValue) { -+ return -1; -+ } -+ return [GPXType decimal:_courseValue]; -+} - -+- (void)setCourse:(double)course -+{ -+ _courseValue = [GPXType valueForDecimal:course]; -+} - - #pragma mark - tag - -@@ -40,6 +85,33 @@ - { - [super addChildTagToGpx:gpx indentationLevel:indentationLevel]; - -+ if (self.garminExtensions) { -+ [self.garminExtensions gpx:gpx indentationLevel:indentationLevel]; -+ } -+ -+ if (self.trailsTrackExtensions) { -+ [self.trailsTrackExtensions gpx:gpx indentationLevel:indentationLevel]; -+ } -+ -+ if (self.trailsTrackPointExtensions) { -+ [self.trailsTrackPointExtensions gpx:gpx indentationLevel:indentationLevel]; -+ } -+ -+ if (self.soundscapeExtensions) { -+ [self.soundscapeExtensions gpx:gpx indentationLevel:indentationLevel]; -+ } -+ -+ if (self.soundscapeSCExtensions) { -+ [self.soundscapeSCExtensions gpx:gpx indentationLevel:indentationLevel]; -+ } -+ -+ if (self.soundscapeAnnotationExtensions) { -+ [self.soundscapeAnnotationExtensions gpx:gpx indentationLevel:indentationLevel]; -+ } -+ -+ if (self.soundscapePOIExtensions) { -+ [self.soundscapePOIExtensions gpx:gpx indentationLevel:indentationLevel]; -+ } - } - - @end -diff --git a/cocoapods-patch-20221103-54518-lfrmhf/iOS-GPX-Framework/GPX/GPXMetadata.h b/Pods/iOS-GPX-Framework/GPX/GPXMetadata.h -index 21b1f9bc2..1b8f62547 100644 ---- a/cocoapods-patch-20221103-54518-lfrmhf/iOS-GPX-Framework/GPX/GPXMetadata.h -+++ b/Pods/iOS-GPX-Framework/GPX/GPXMetadata.h -@@ -44,12 +44,12 @@ - @property (strong, nonatomic) NSDate *time; - - /** Keywords associated with the file. Search engines or databases can use this information to classify the data. */ --@property (strong, nonatomic) NSString *keyword; -+@property (strong, nonatomic) NSString *keywords; - - /** Minimum and maximum coordinates which describe the extent of the coordinates in the file. */ - @property (strong, nonatomic) GPXBounds *bounds; - - /** You can add extend GPX by adding your own elements from another schema here. */ --@property (strong, nonatomic) GPXExtensions *extensions; -+@property (strong, nonatomic, nullable) GPXExtensions *extensions; - - @end -diff --git a/cocoapods-patch-20221103-54518-lfrmhf/iOS-GPX-Framework/GPX/GPXMetadata.m b/Pods/iOS-GPX-Framework/GPX/GPXMetadata.m -index cba512922..37415835f 100644 ---- a/cocoapods-patch-20221103-54518-lfrmhf/iOS-GPX-Framework/GPX/GPXMetadata.m -+++ b/Pods/iOS-GPX-Framework/GPX/GPXMetadata.m -@@ -24,7 +24,7 @@ - @synthesize copyright = _copyright; - @synthesize link = _link; - @synthesize time = _time; --@synthesize keyword = _keyword; -+@synthesize keywords = _keywords; - @synthesize bounds = _bounds; - @synthesize extensions = _extensions; - -@@ -41,7 +41,7 @@ - _copyright = (GPXCopyright *)[self childElementOfClass:[GPXCopyright class] xmlElement:element]; - _link = (GPXLink *)[self childElementOfClass:[GPXLink class] xmlElement:element]; - _timeValue = [self textForSingleChildElementNamed:@"time" xmlElement:element]; -- _keyword = [self textForSingleChildElementNamed:@"keyword" xmlElement:element]; -+ _keywords = [self textForSingleChildElementNamed:@"keywords" xmlElement:element]; - _bounds = (GPXBounds *)[self childElementOfClass:[GPXBounds class] xmlElement:element]; - _extensions = (GPXExtensions *)[self childElementOfClass:[GPXExtensions class] xmlElement:element]; - } -@@ -92,7 +92,7 @@ - } - - [self gpx:gpx addPropertyForValue:_timeValue defaultValue:@"0" tagName:@"time" indentationLevel:indentationLevel]; -- [self gpx:gpx addPropertyForValue:_keyword tagName:@"keyword" indentationLevel:indentationLevel]; -+ [self gpx:gpx addPropertyForValue:_keywords tagName:@"keywords" indentationLevel:indentationLevel]; - - if (self.bounds) { - [self.bounds gpx:gpx indentationLevel:indentationLevel]; -diff --git a/cocoapods-patch-20221103-54518-lfrmhf/iOS-GPX-Framework/GPX/GPXPoint.h b/Pods/iOS-GPX-Framework/GPX/GPXPoint.h -index 6783495b4..070160ec6 100644 ---- a/cocoapods-patch-20221103-54518-lfrmhf/iOS-GPX-Framework/GPX/GPXPoint.h -+++ b/Pods/iOS-GPX-Framework/GPX/GPXPoint.h -@@ -7,6 +7,7 @@ - // - - #import "GPXElement.h" -+@import CoreLocation; - - - /** A geographic point with optional elevation and time. Available for use by other schemas. -@@ -19,16 +20,16 @@ - /// --------------------------------- - - /** The elevation (in meters) of the point. */ --@property (nonatomic, assign) CGFloat elevation; -+@property (nonatomic) CLLocationDistance elevation; - - /** The time that the point was recorded. */ - @property (strong, nonatomic) NSDate *time; - - /** The latitude of the point. Decimal degrees, WGS84 datum */ --@property (nonatomic, assign) CGFloat latitude; -+@property (nonatomic) CLLocationDegrees latitude; - - /** The longitude of the point. Decimal degrees, WGS84 datum. */ --@property (nonatomic, assign) CGFloat longitude; -+@property (nonatomic) CLLocationDegrees longitude; - - - /// --------------------------------- -@@ -40,6 +41,6 @@ - @param longitude The longitude of the point. - @return A newly created point element. - */ --+ (GPXPoint *)pointWithLatitude:(CGFloat)latitude longitude:(CGFloat)longitude; -++ (GPXPoint *)pointWithLatitude:(CLLocationDegrees)latitude longitude:(CLLocationDegrees)longitude; - - @end -diff --git a/cocoapods-patch-20221103-54518-lfrmhf/iOS-GPX-Framework/GPX/GPXPoint.m b/Pods/iOS-GPX-Framework/GPX/GPXPoint.m -index 3e8eb6bf3..141d99956 100644 ---- a/cocoapods-patch-20221103-54518-lfrmhf/iOS-GPX-Framework/GPX/GPXPoint.m -+++ b/Pods/iOS-GPX-Framework/GPX/GPXPoint.m -@@ -36,7 +36,7 @@ - return self; - } - --+ (GPXPoint *)pointWithLatitude:(CGFloat)latitude longitude:(CGFloat)longitude -++ (GPXPoint *)pointWithLatitude:(CLLocationDegrees)latitude longitude:(CLLocationDegrees)longitude - { - GPXPoint *point = [GPXPoint new]; - point.latitude = latitude; -@@ -47,12 +47,12 @@ - - #pragma mark - Public methods - --- (CGFloat)elevation -+- (CLLocationDistance)elevation - { - return [GPXType decimal:_elevationValue]; - } - --- (void)setElevation:(CGFloat)elevation -+- (void)setElevation:(CLLocationDistance)elevation - { - _elevationValue = [GPXType valueForDecimal:elevation]; - } -@@ -67,22 +67,22 @@ - _timeValue = [GPXType valueForDateTime:time]; - } - --- (CGFloat)latitude -+- (CLLocationDegrees)latitude - { - return [GPXType latitude:_latitudeValue]; - } - --- (void)setLatitude:(CGFloat)latitude -+- (void)setLatitude:(CLLocationDegrees)latitude - { - _latitudeValue = [GPXType valueForLatitude:latitude]; - } - --- (CGFloat)longitude -+- (CLLocationDegrees)longitude - { - return [GPXType longitude:_longitudeValue]; - } - --- (void)setLongitude:(CGFloat)longitude -+- (void)setLongitude:(CLLocationDegrees)longitude - { - _longitudeValue = [GPXType valueForLongitude:longitude]; - } -diff --git a/cocoapods-patch-20221103-54518-lfrmhf/iOS-GPX-Framework/GPX/GPXPointSegment.h b/Pods/iOS-GPX-Framework/GPX/GPXPointSegment.h -index 69376ef1b..6b13e09e2 100644 ---- a/cocoapods-patch-20221103-54518-lfrmhf/iOS-GPX-Framework/GPX/GPXPointSegment.h -+++ b/Pods/iOS-GPX-Framework/GPX/GPXPointSegment.h -@@ -21,7 +21,7 @@ - /// --------------------------------- - - /** Ordered list of geographic points. */ --@property (strong, nonatomic, readonly) NSArray *points; -+@property (strong, nonatomic, readonly) NSArray *points; - - - /// --------------------------------- -@@ -48,7 +48,7 @@ - /** Adds the GPXPoint objects contained in another given array to the end of the point array. - @param array An array of GPXPoint objects to add to the end of the point array. - */ --- (void)addPoints:(NSArray *)array; -+- (void)addPoints:(NSArray *)array; - - - /// --------------------------------- -diff --git a/cocoapods-patch-20221103-54518-lfrmhf/iOS-GPX-Framework/GPX/GPXRoot.h b/Pods/iOS-GPX-Framework/GPX/GPXRoot.h -index 4c47a55fb..4fa2977ac 100644 ---- a/cocoapods-patch-20221103-54518-lfrmhf/iOS-GPX-Framework/GPX/GPXRoot.h -+++ b/Pods/iOS-GPX-Framework/GPX/GPXRoot.h -@@ -14,6 +14,7 @@ - @class GPXTrack; - @class GPXExtensions; - -+NS_ASSUME_NONNULL_BEGIN - - /** GPX is the root element in the XML file. - GPX documents contain a metadata header, followed by waypoints, routes, and tracks. -@@ -30,26 +31,29 @@ - @property (strong, nonatomic, readonly) NSString *schema; - - /** You must include the version number in your GPX document. */ --@property (strong, nonatomic) NSString *version; -+@property (strong, nonatomic, readonly) NSString *version; - - /** You must include the name or URL of the software that created your GPX document. - This allows others to inform the creator of a GPX instance document that fails to validate. */ --@property (strong, nonatomic) NSString *creator; -+@property (strong, nonatomic, readonly) NSString *creator; - - /** Metadata about the file. */ --@property (strong, nonatomic) GPXMetadata *metadata; -+@property (strong, nonatomic, nullable) GPXMetadata *metadata; -+ -+/** Keywords for indexing the GPX file with search engines. Will be comma separated. */ -+@property (strong, nonatomic, nullable, readonly) NSArray *keywords; - - /** A list of waypoints. */ --@property (strong, nonatomic, readonly) NSArray *waypoints; -+@property (strong, nonatomic, readonly) NSArray *waypoints; - - /** A list of routes. */ --@property (strong, nonatomic, readonly) NSArray *routes; -+@property (strong, nonatomic, readonly) NSArray *routes; - - /** A list of tracks. */ --@property (strong, nonatomic, readonly) NSArray *tracks; -+@property (strong, nonatomic, readonly) NSArray *tracks; - --/** You can add extend GPX by adding your own elements from another schema here. */ --@property (strong, nonatomic) GPXExtensions *extensions; -+/** You can extend GPX by adding your own elements from another schema here. */ -+@property (strong, nonatomic, nullable) GPXExtensions *extensions; - - - /// --------------------------------- -@@ -87,7 +91,7 @@ - /** Adds the GPXWaypoint objects contained in another given array to the end of the waypoint array. - @param array An array of GPXWaypoint objects to add to the end of the waypoint array. - */ --- (void)addWaypoints:(NSArray *)array; -+- (void)addWaypoints:(NSArray *)array; - - - /// --------------------------------- -@@ -122,7 +126,7 @@ - /** Adds the GPXRoute objects contained in another given array to the end of the route array. - @param array An array of GPXRoute objects to add to the end of the route array. - */ --- (void)addRoutes:(NSArray *)array; -+- (void)addRoutes:(NSArray *)array; - - - /// --------------------------------- -@@ -157,7 +161,7 @@ - /** Adds the GPXTrack objects contained in another given array to the end of the track array. - @param array An array of GPXTrack objects to add to the end of the track array. - */ --- (void)addTracks:(NSArray *)array; -+- (void)addTracks:(NSArray *)array; - - - /// --------------------------------- -@@ -170,3 +174,5 @@ - - (void)removeTrack:(GPXTrack *)track; - - @end -+ -+NS_ASSUME_NONNULL_END -diff --git a/cocoapods-patch-20221103-54518-lfrmhf/iOS-GPX-Framework/GPX/GPXRoot.m b/Pods/iOS-GPX-Framework/GPX/GPXRoot.m -index fdccd733c..f28fe71a0 100644 ---- a/cocoapods-patch-20221103-54518-lfrmhf/iOS-GPX-Framework/GPX/GPXRoot.m -+++ b/Pods/iOS-GPX-Framework/GPX/GPXRoot.m -@@ -14,6 +14,9 @@ - #import "GPXTrack.h" - #import "GPXExtensions.h" - -+@interface GPXRoot () -+@property (strong, nonatomic, readwrite) NSString *creator; -+@end - - @implementation GPXRoot { - NSMutableArray *_waypoints; -@@ -24,6 +27,7 @@ - @synthesize schema = _schema; - @synthesize version = _version; - @synthesize creator = _creator; -+@synthesize keywords = _keywords; - @synthesize metadata = _metadata; - @synthesize waypoints = _waypoints; - @synthesize routes = _routes; -@@ -40,6 +44,7 @@ - if (self) { - _version = @"1.1"; - _creator = @"http://gpxframework.com"; -+ _keywords = nil; - _waypoints = [NSMutableArray array]; - _routes = [NSMutableArray array]; - _tracks = [NSMutableArray array]; -@@ -51,11 +56,13 @@ - { - self = [super initWithXMLElement:element parent:parent]; - if (self) { -- _version = [self valueOfAttributeNamed:@"version" xmlElement:element required:YES]; -- _creator = [self valueOfAttributeNamed:@"creator" xmlElement:element required:YES]; -- -+ _version = [self valueOfAttributeNamed:@"version" xmlElement:element required:YES] ?: _version; -+ _creator = [self valueOfAttributeNamed:@"creator" xmlElement:element required:YES] ?: _creator; - _metadata = (GPXMetadata *)[self childElementOfClass:[GPXMetadata class] xmlElement:element]; -- -+ _keywords = [GPXRoot keywordsArrayFromString:[self textForSingleChildElementNamed:@"keywords" xmlElement:element]]; -+ if (!_keywords.count && _metadata.keywords.length) { -+ _keywords = [GPXRoot keywordsArrayFromString:_metadata.keywords]; -+ } - NSMutableArray *array1 = [NSMutableArray array]; - [self childElementsOfClass:[GPXWaypoint class] - xmlElement:element -@@ -93,6 +100,24 @@ - return root; - } - -++ (NSArray *)keywordsArrayFromString:(NSString *)keywordString { -+ if (!keywordString) { -+ // return nil only for nil strings, to differentiate between empty and nil keywords -+ return nil; -+ } -+ -+ NSArray *keywords = [keywordString componentsSeparatedByString:@","]; -+ if (!keywords.count) { -+ return @[]; -+ } -+ NSMutableArray *sanitizedKeyWords = [NSMutableArray arrayWithCapacity:keywords.count]; -+ NSCharacterSet *whitespaces = [NSCharacterSet whitespaceAndNewlineCharacterSet]; -+ for (NSString *key in keywords) { -+ [sanitizedKeyWords addObject:[key stringByTrimmingCharactersInSet:whitespaces]]; -+ } -+ -+ return [sanitizedKeyWords copy]; -+} - - #pragma mark - Public methods - -diff --git a/cocoapods-patch-20221103-54518-lfrmhf/iOS-GPX-Framework/GPX/GPXRoute.h b/Pods/iOS-GPX-Framework/GPX/GPXRoute.h -index 513719d61..1cc9864ad 100644 ---- a/cocoapods-patch-20221103-54518-lfrmhf/iOS-GPX-Framework/GPX/GPXRoute.h -+++ b/Pods/iOS-GPX-Framework/GPX/GPXRoute.h -@@ -35,7 +35,7 @@ - @property (strong, nonatomic) NSString *source; - - /** Links to external information about the route. */ --@property (strong, nonatomic, readonly) NSArray *links; -+@property (strong, nonatomic, readonly) NSArray *links; - - /** GPS route number. */ - @property (nonatomic, assign) NSInteger number; -@@ -47,7 +47,7 @@ - @property (strong, nonatomic) GPXExtensions *extensions; - - /** A list of route points. */ --@property (strong, nonatomic, readonly) NSArray *routepoints; -+@property (strong, nonatomic, readonly) NSArray *routepoints; - - - /// --------------------------------- -@@ -73,7 +73,7 @@ - /** Adds the GPXLink objects contained in another given array to the end of the link array. - @param array An array of GPXLink objects to add to the end of the link array. - */ --- (void)addLinks:(NSArray *)array; -+- (void)addLinks:(NSArray *)array; - - - /// --------------------------------- -@@ -110,7 +110,7 @@ - /** Adds the GPXRoutePoint objects contained in another given array to the end of the routepoint array. - @param array An array of GPXRoutePoint objects to add to the end of the routepoint array. - */ --- (void)addRoutepoints:(NSArray *)array; -+- (void)addRoutepoints:(NSArray *)array; - - - /// --------------------------------- -diff --git a/Pods/iOS-GPX-Framework/GPX/GPXSoundscapeAnnotation.h b/Pods/iOS-GPX-Framework/GPX/GPXSoundscapeAnnotation.h -new file mode 100644 -index 000000000..7fd3bf154 ---- /dev/null -+++ b/Pods/iOS-GPX-Framework/GPX/GPXSoundscapeAnnotation.h -@@ -0,0 +1,21 @@ -+// -+// GPXSoundscapeAnnotation.h -+// iOS-GPX-Framework -+// -+// -+ -+#import "GPXElement.h" -+#import "GPXSoundscapeAnnotation.h" -+ -+@interface GPXSoundscapeAnnotation : GPXElement -+ -+/** A title for the annotation */ -+@property (strong, nonatomic, nullable) NSString * title; -+ -+/** An annotation. */ -+@property (strong, nonatomic, nonnull) NSString * content; -+ -+/** Type of the annotation. Used for specifying arrival and departure callouts */ -+@property (strong, nonatomic, nullable) NSString * type; -+ -+@end -diff --git a/Pods/iOS-GPX-Framework/GPX/GPXSoundscapeAnnotation.m b/Pods/iOS-GPX-Framework/GPX/GPXSoundscapeAnnotation.m -new file mode 100644 -index 000000000..ef2580429 ---- /dev/null -+++ b/Pods/iOS-GPX-Framework/GPX/GPXSoundscapeAnnotation.m -@@ -0,0 +1,36 @@ -+// -+// GPXSoundscapeAnnotation.m -+// iOS-GPX-Framework -+// -+// -+ -+#import "GPXSoundscapeAnnotation.h" -+#import "GPXElementSubclass.h" -+ -+NSString *const kGPXSoundscapeAnnotationTagName = @"gpxsc:annotation"; -+ -+NSString *const kAttributeTitle = @"title"; -+ -+NSString *const kAttributeType = @"type"; -+ -+@implementation GPXSoundscapeAnnotation -+ -+- (id)initWithXMLElement:(TBXMLElement *)element parent:(GPXElement *)parent -+{ -+ self = [super initWithXMLElement:element parent:parent]; -+ if (self) { -+ self.title = [self valueOfAttributeNamed:kAttributeTitle xmlElement:element required:NO]; -+ self.type = [self valueOfAttributeNamed:kAttributeType xmlElement:element required:NO]; -+ self.content = [TBXML textForElement:element]; -+ } -+ return self; -+} -+ -+#pragma mark - tag -+ -++ (NSString *)tagName -+{ -+ return kGPXSoundscapeAnnotationTagName; -+} -+ -+@end -diff --git a/Pods/iOS-GPX-Framework/GPX/GPXSoundscapeAnnotationExtensions.h b/Pods/iOS-GPX-Framework/GPX/GPXSoundscapeAnnotationExtensions.h -new file mode 100644 -index 000000000..1f13ae987 ---- /dev/null -+++ b/Pods/iOS-GPX-Framework/GPX/GPXSoundscapeAnnotationExtensions.h -@@ -0,0 +1,15 @@ -+// -+// GPXSoundscapeAnnotationExtensions.h -+// iOS-GPX-Framework -+// -+// -+ -+#import "GPXElement.h" -+#import "GPXSoundscapeAnnotation.h" -+ -+@interface GPXSoundscapeAnnotationExtensions : GPXElement -+ -+/** A list of annotations. */ -+@property (strong, nonatomic, nonnull) NSArray * annotations; -+ -+@end -diff --git a/Pods/iOS-GPX-Framework/GPX/GPXSoundscapeAnnotationExtensions.m b/Pods/iOS-GPX-Framework/GPX/GPXSoundscapeAnnotationExtensions.m -new file mode 100644 -index 000000000..e9860e028 ---- /dev/null -+++ b/Pods/iOS-GPX-Framework/GPX/GPXSoundscapeAnnotationExtensions.m -@@ -0,0 +1,45 @@ -+// -+// GPXSoundscapeAnnotationExtensions.m -+// iOS-GPX-Framework -+// -+// -+ -+#import "GPXSoundscapeAnnotationExtensions.h" -+#import "GPXElementSubclass.h" -+ -+NSString *const kGPXSoundscapeAnnotationExtensionTagName = @"gpxsc:annotations"; -+ -+@implementation GPXSoundscapeAnnotationExtensions -+ -+- (id)init -+{ -+ self = [super init]; -+ if (self) { -+ _annotations = [NSMutableArray array]; -+ } -+ return self; -+} -+ -+- (id)initWithXMLElement:(TBXMLElement *)element parent:(GPXElement *)parent -+{ -+ self = [super initWithXMLElement:element parent:parent]; -+ if (self) { -+ NSMutableArray *array = [NSMutableArray array]; -+ [self childElementsOfClass:[GPXSoundscapeAnnotation class] -+ xmlElement:element -+ eachBlock:^(GPXElement *element) { -+ [array addObject:element]; -+ }]; -+ _annotations = array; -+ } -+ return self; -+} -+ -+#pragma mark - tag -+ -++ (NSString *)tagName -+{ -+ return kGPXSoundscapeAnnotationExtensionTagName; -+} -+ -+@end -diff --git a/Pods/iOS-GPX-Framework/GPX/GPXSoundscapeExtensions.h b/Pods/iOS-GPX-Framework/GPX/GPXSoundscapeExtensions.h -new file mode 100644 -index 000000000..cfbf8ec60 ---- /dev/null -+++ b/Pods/iOS-GPX-Framework/GPX/GPXSoundscapeExtensions.h -@@ -0,0 +1,26 @@ -+// -+// GPXSoundscapeExtensions.h -+// iOS-GPX-Framework -+// -+// -+ -+#import "GPXElement.h" -+@import CoreLocation; -+ -+@interface GPXSoundscapeExtensions : GPXElement -+ -+@property (strong, nonatomic, nullable) NSNumber *horizontalAccuracy; -+@property (strong, nonatomic, nullable) NSNumber *verticalAccuracy; -+ -+@property (strong, nonatomic, nullable) NSNumber *trueHeading; -+@property (strong, nonatomic, nullable) NSNumber *magneticHeading; -+@property (strong, nonatomic, nullable) NSNumber *headingAccuracy; -+ -+@property (strong, nonatomic, nullable) NSNumber *deviceHeading; -+ -+@property (strong, nonatomic, nullable) NSNumber *floorLevel; -+ -+/// examples: stationary, walking, running, automotive, cycling -+@property (strong, nonatomic, nullable) NSString *activity; -+ -+@end -diff --git a/Pods/iOS-GPX-Framework/GPX/GPXSoundscapeExtensions.m b/Pods/iOS-GPX-Framework/GPX/GPXSoundscapeExtensions.m -new file mode 100644 -index 000000000..5757d9ec0 ---- /dev/null -+++ b/Pods/iOS-GPX-Framework/GPX/GPXSoundscapeExtensions.m -@@ -0,0 +1,147 @@ -+// -+// GPXSoundscapeExtensions.m -+// iOS-GPX-Framework -+// -+// -+ -+#import "GPXSoundscapeExtensions.h" -+#import "GPXElementSubclass.h" -+ -+NSString *const kGPXSoundscapeExtensionsTagName = @"gpxgd:TrackPointExtension"; -+ -+NSString *const kElementHorizontalAccuracy = @"gpxgd:hor_acc"; -+NSString *const kElementVerticalAccuracy = @"gpxgd:ver_acc"; -+ -+NSString *const kElementTrueHeading = @"gpxgd:hdg_tru"; -+NSString *const kElementMagneticHeading = @"gpxgd:hdg_mag"; -+NSString *const kElementHeadingAccuracy = @"gpxgd:hdg_acc"; -+ -+NSString *const kElementDeviceHeading = @"gpxgd:hdg_dvc"; -+ -+NSString *const kElementFloorLevel = @"gpxgd:flr_lvl"; -+ -+NSString *const kElementMotionActivity = @"gpxgd:activity"; -+ -+@interface GPXSoundscapeExtensions () -+ -+@property (nonatomic, strong) NSString *horizontalAccuracyString; -+@property (nonatomic, strong) NSString *verticalAccuracyString; -+ -+@property (nonatomic, strong) NSString *trueHeadingString; -+@property (nonatomic, strong) NSString *magneticHeadingString; -+@property (nonatomic, strong) NSString *headingAccuracyString; -+ -+@property (nonatomic, strong) NSString *deviceHeadingString; -+ -+@property (nonatomic, strong) NSString *floorLevelString; -+ -+@end -+ -+@implementation GPXSoundscapeExtensions -+ -+- (id)initWithXMLElement:(TBXMLElement *)element parent:(GPXElement *)parent -+{ -+ self = [super initWithXMLElement:element parent:parent]; -+ if (self) { -+ _horizontalAccuracyString = [self textForSingleChildElementNamed:kElementHorizontalAccuracy xmlElement:element]; -+ _verticalAccuracyString = [self textForSingleChildElementNamed:kElementVerticalAccuracy xmlElement:element]; -+ -+ _trueHeadingString = [self textForSingleChildElementNamed:kElementTrueHeading xmlElement:element]; -+ _magneticHeadingString = [self textForSingleChildElementNamed:kElementMagneticHeading xmlElement:element]; -+ _headingAccuracyString = [self textForSingleChildElementNamed:kElementHeadingAccuracy xmlElement:element]; -+ -+ _deviceHeadingString = [self textForSingleChildElementNamed:kElementDeviceHeading xmlElement:element]; -+ -+ _floorLevelString = [self textForSingleChildElementNamed:kElementFloorLevel xmlElement:element]; -+ -+ _activity = [self textForSingleChildElementNamed:kElementMotionActivity xmlElement:element]; -+ } -+ return self; -+} -+ -+#pragma mark - Public methods -+ -+- (void)setHorizontalAccuracy:(NSNumber *)horizontalAccuracy { -+ _horizontalAccuracyString = horizontalAccuracy ? [GPXType valueForDecimal:horizontalAccuracy.doubleValue] : nil; -+} -+ -+- (NSNumber *)horizontalAccuracy { -+ return _horizontalAccuracyString.length > 0 ? [NSNumber numberWithDouble:[GPXType decimal:_horizontalAccuracyString]] : nil; -+} -+ -+- (void)setVerticalAccuracy:(NSNumber *)verticalAccuracy { -+ _verticalAccuracyString = verticalAccuracy ? [GPXType valueForDecimal:verticalAccuracy.doubleValue] : nil; -+} -+ -+- (NSNumber *)verticalAccuracy { -+ return _verticalAccuracyString.length > 0 ? [NSNumber numberWithDouble:[GPXType decimal:_verticalAccuracyString]] : nil; -+} -+ -+- (void)setTrueHeading:(NSNumber *)trueHeading { -+ _trueHeadingString = trueHeading ? [GPXType valueForDecimal:trueHeading.doubleValue] : nil; -+} -+ -+- (NSNumber *)trueHeading { -+ return _trueHeadingString.length > 0 ? [NSNumber numberWithDouble:[GPXType decimal:_trueHeadingString]] : nil; -+} -+ -+- (void)setMagneticHeading:(NSNumber *)magneticHeading { -+ _magneticHeadingString = magneticHeading ? [GPXType valueForDecimal:magneticHeading.doubleValue] : nil; -+} -+ -+- (NSNumber *)magneticHeading { -+ return _magneticHeadingString.length > 0 ? [NSNumber numberWithDouble:[GPXType decimal:_magneticHeadingString]] : nil; -+} -+ -+- (void)setHeadingAccuracy:(NSNumber *)headingAccuracy { -+ _headingAccuracyString = headingAccuracy ? [GPXType valueForDecimal:headingAccuracy.doubleValue] : nil; -+} -+ -+- (NSNumber *)headingAccuracy { -+ return _headingAccuracyString.length > 0 ? [NSNumber numberWithDouble:[GPXType decimal:_headingAccuracyString]] : nil; -+} -+ -+- (void)setDeviceHeading:(NSNumber *)deviceHeading { -+ _deviceHeadingString = deviceHeading ? [GPXType valueForDecimal:deviceHeading.doubleValue] : nil; -+} -+ -+- (NSNumber *)deviceHeading { -+ return _deviceHeadingString.length > 0 ? [NSNumber numberWithDouble:[GPXType decimal:_deviceHeadingString]] : nil; -+} -+ -+- (void)setFloorLevel:(NSNumber *)floorLevel { -+ _floorLevelString = floorLevel ? floorLevel.stringValue : nil; -+} -+ -+- (NSNumber *)floorLevel { -+ return _floorLevelString.length > 0 ? [NSNumber numberWithInteger:_floorLevelString.integerValue] : nil; -+} -+ -+#pragma mark - tag -+ -++ (NSString *)tagName -+{ -+ return kGPXSoundscapeExtensionsTagName; -+} -+ -+#pragma mark - GPX -+ -+- (void)addChildTagToGpx:(NSMutableString *)gpx indentationLevel:(NSInteger)indentationLevel -+{ -+ [super addChildTagToGpx:gpx indentationLevel:indentationLevel]; -+ -+ if (_horizontalAccuracyString) [self gpx:gpx addPropertyForValue:_horizontalAccuracyString tagName:kElementHorizontalAccuracy indentationLevel:indentationLevel]; -+ if (_verticalAccuracyString) [self gpx:gpx addPropertyForValue:_verticalAccuracyString tagName:kElementVerticalAccuracy indentationLevel:indentationLevel]; -+ -+ if (_trueHeadingString) [self gpx:gpx addPropertyForValue:_trueHeadingString tagName:kElementTrueHeading indentationLevel:indentationLevel]; -+ if (_magneticHeadingString) [self gpx:gpx addPropertyForValue:_magneticHeadingString tagName:kElementMagneticHeading indentationLevel:indentationLevel]; -+ if (_headingAccuracyString) [self gpx:gpx addPropertyForValue:_headingAccuracyString tagName:kElementHeadingAccuracy indentationLevel:indentationLevel]; -+ -+ if(_deviceHeadingString) [self gpx:gpx addPropertyForValue:_deviceHeadingString tagName:kElementDeviceHeading indentationLevel:indentationLevel]; -+ -+ if (_floorLevelString) [self gpx:gpx addPropertyForValue:_floorLevelString tagName:kElementFloorLevel indentationLevel:indentationLevel]; -+ -+ if (_activity) [self gpx:gpx addPropertyForValue:_activity tagName:kElementMotionActivity indentationLevel:indentationLevel]; -+} -+ -+@end -diff --git a/Pods/iOS-GPX-Framework/GPX/GPXSoundscapeLinkExtensions.h b/Pods/iOS-GPX-Framework/GPX/GPXSoundscapeLinkExtensions.h -new file mode 100644 -index 000000000..7ad636d2d ---- /dev/null -+++ b/Pods/iOS-GPX-Framework/GPX/GPXSoundscapeLinkExtensions.h -@@ -0,0 +1,19 @@ -+// -+// GPXSoundscapeLinkExtensions.h -+// Pods -+// -+// -+ -+#import "GPXLink.h" -+ -+@interface GPXSoundscapeLink : GPXLink -+@end -+ -+@interface GPXSoundscapeLinkExtensions : GPXElement -+ -+/** A list of annotations. */ -+@property (strong, nonatomic, nonnull) NSArray * links; -+ -+@end -+ -+ -diff --git a/Pods/iOS-GPX-Framework/GPX/GPXSoundscapeLinkExtensions.m b/Pods/iOS-GPX-Framework/GPX/GPXSoundscapeLinkExtensions.m -new file mode 100644 -index 000000000..8ed711e6e ---- /dev/null -+++ b/Pods/iOS-GPX-Framework/GPX/GPXSoundscapeLinkExtensions.m -@@ -0,0 +1,55 @@ -+// -+// GPXSoundscapeLinkExtensions.m -+// iOS-GPX-Framework -+// -+// -+ -+#import "GPXSoundscapeLinkExtensions.h" -+#import "GPXElementSubclass.h" -+ -+@implementation GPXSoundscapeLink -+ -+// Override the tagName -++ (NSString *)tagName -+{ -+ return @"gpxsc:link"; -+} -+ -+@end -+ -+NSString *const kGPXSoundscapeLinksExtensionTagName = @"gpxsc:links"; -+ -+@implementation GPXSoundscapeLinkExtensions -+ -+- (id)init -+{ -+ self = [super init]; -+ if (self) { -+ _links = [NSMutableArray array]; -+ } -+ return self; -+} -+ -+- (id)initWithXMLElement:(TBXMLElement *)element parent:(GPXElement *)parent -+{ -+ self = [super initWithXMLElement:element parent:parent]; -+ if (self) { -+ NSMutableArray *array = [NSMutableArray array]; -+ [self childElementsOfClass:[GPXSoundscapeLink class] -+ xmlElement:element -+ eachBlock:^(GPXElement *element) { -+ [array addObject:element]; -+ }]; -+ _links = array; -+ } -+ return self; -+} -+ -+#pragma mark - tag -+ -++ (NSString *)tagName -+{ -+ return kGPXSoundscapeLinksExtensionTagName; -+} -+ -+@end -diff --git a/Pods/iOS-GPX-Framework/GPX/GPXSoundscapePOIExtensions.h b/Pods/iOS-GPX-Framework/GPX/GPXSoundscapePOIExtensions.h -new file mode 100644 -index 000000000..baa89c396 ---- /dev/null -+++ b/Pods/iOS-GPX-Framework/GPX/GPXSoundscapePOIExtensions.h -@@ -0,0 +1,22 @@ -+// -+// GPXSoundscapePOIExtensions.h -+// iOS-GPX-Framework -+// -+// -+ -+#import "GPXElement.h" -+#import "GPXLink.h" -+#import "GPXSoundscapeAnnotationExtensions.h" -+ -+@interface GPXSoundscapePOIExtensions : GPXElement -+ -+/** Street address of the POI */ -+@property (strong, nonatomic, nullable) NSString * streetAddress; -+ -+/** The POI's website. */ -+@property (strong, nonatomic, nullable) GPXLink * homepage; -+ -+/** Phone number of the POI */ -+@property (strong, nonatomic, nullable) NSString * phone; -+ -+@end -diff --git a/Pods/iOS-GPX-Framework/GPX/GPXSoundscapePOIExtensions.m b/Pods/iOS-GPX-Framework/GPX/GPXSoundscapePOIExtensions.m -new file mode 100644 -index 000000000..0dc1f3afd ---- /dev/null -+++ b/Pods/iOS-GPX-Framework/GPX/GPXSoundscapePOIExtensions.m -@@ -0,0 +1,45 @@ -+// -+// GPXSoundscapePOIExtensions.h -+// iOS-GPX-Framework -+// -+// -+ -+#import "GPXSoundscapePOIExtensions.h" -+#import "GPXElementSubclass.h" -+ -+NSString *const kGPXSoundscapePOIExtensionTagName = @"gpxsc:poi"; -+ -+NSString *const kElementStreetAddress = @"gpxsc:street"; -+NSString *const kElementPhone = @"gpxsc:phone"; -+ -+@implementation GPXSoundscapePOIExtensions -+ -+- (id)initWithXMLElement:(TBXMLElement *)element parent:(GPXElement *)parent -+{ -+ self = [super initWithXMLElement:element parent:parent]; -+ if (self) { -+ self.streetAddress = [self textForSingleChildElementNamed:kElementStreetAddress xmlElement:element]; -+ self.homepage = [self childElementOfClass:[GPXLink class] xmlElement:element]; -+ self.phone = [self textForSingleChildElementNamed:kElementPhone xmlElement:element]; -+ } -+ return self; -+} -+ -+#pragma mark - tag -+ -++ (NSString *)tagName -+{ -+ return kGPXSoundscapePOIExtensionTagName; -+} -+ -+#pragma mark - GPX -+ -+- (void)addChildTagToGpx:(NSMutableString *)gpx indentationLevel:(NSInteger)indentationLevel -+{ -+ [super addChildTagToGpx:gpx indentationLevel:indentationLevel]; -+ -+ if (self.streetAddress) [self gpx:gpx addPropertyForValue:self.streetAddress tagName:kElementStreetAddress indentationLevel:indentationLevel]; -+ if (self.homepage) [self.homepage gpx:gpx indentationLevel:indentationLevel]; -+ if (self.phone) [self gpx:gpx addPropertyForValue:self.phone tagName:kElementPhone indentationLevel:indentationLevel]; -+} -+@end -diff --git a/Pods/iOS-GPX-Framework/GPX/GPXSoundscapeRegion.h b/Pods/iOS-GPX-Framework/GPX/GPXSoundscapeRegion.h -new file mode 100644 -index 000000000..3b08ea0c4 ---- /dev/null -+++ b/Pods/iOS-GPX-Framework/GPX/GPXSoundscapeRegion.h -@@ -0,0 +1,15 @@ -+// -+// GPXSoundscapeRegion.h -+// iOS-GPX-Framework -+// -+// -+ -+#import "GPXElement.h" -+@import CoreLocation; -+ -+@interface GPXSoundscapeRegion : GPXElement -+ -+/** The region in which a shared content experience is available */ -+@property (nonnull, nonatomic, strong) CLCircularRegion * region; -+ -+@end -diff --git a/Pods/iOS-GPX-Framework/GPX/GPXSoundscapeRegion.m b/Pods/iOS-GPX-Framework/GPX/GPXSoundscapeRegion.m -new file mode 100644 -index 000000000..3256cb69f ---- /dev/null -+++ b/Pods/iOS-GPX-Framework/GPX/GPXSoundscapeRegion.m -@@ -0,0 +1,70 @@ -+// -+// GPXSoundscapeRegion.m -+// iOS-GPX-Framework -+// -+// -+ -+#import "GPXSoundscapeRegion.h" -+#import "GPXElementSubclass.h" -+ -+NSString *const kGPXSoundscapeRegionTagName = @"gpxsc:region"; -+ -+NSString *const kAttributeLatitude = @"lat"; -+NSString *const kAttributeLongitude = @"lon"; -+NSString *const kAttributeRadius = @"radius"; -+ -+NSString *const kRegionIdentifier = @"SoundscapeExperienceRegion"; -+ -+@interface GPXSoundscapeRegion () -+ -+/** The latitude of the center of the region. Decimal degrees, WGS84 datum. */ -+@property (nonatomic) NSString * latitudeString; -+ -+/** The longitude of the center of the region. Decimal degrees, WGS84 datum. */ -+@property (nonatomic) NSString * longitudeString; -+ -+/** The radius (in meters) of the region surrounding the lat/lon. */ -+@property (nonatomic) NSString * radiusString; -+ -+@end -+ -+@implementation GPXSoundscapeRegion -+ -+- (id)initWithXMLElement:(TBXMLElement *)element parent:(GPXElement *)parent -+{ -+ self = [super initWithXMLElement:element parent:parent]; -+ if (self) { -+ self.latitudeString = [self valueOfAttributeNamed:kAttributeLatitude xmlElement:element required:YES]; -+ self.longitudeString = [self valueOfAttributeNamed:kAttributeLongitude xmlElement:element required:YES]; -+ self.radiusString = [self valueOfAttributeNamed:kAttributeRadius xmlElement:element required:YES]; -+ } -+ return self; -+} -+ -+#pragma mark - Public methods -+ -+- (CLCircularRegion *)region -+{ -+ CLLocationDegrees latitude = [GPXType latitude:self.latitudeString]; -+ CLLocationDegrees longitude = [GPXType longitude:self.longitudeString]; -+ CLLocationDistance radius = (CLLocationDistance)[GPXType decimal:self.radiusString]; -+ -+ return [[CLCircularRegion alloc] initWithCenter:CLLocationCoordinate2DMake(latitude, longitude) radius:radius identifier:kRegionIdentifier]; -+} -+ -+- (void)setRegion:(CLCircularRegion *)region -+{ -+ self.latitudeString = [GPXType valueForLatitude:region.center.latitude]; -+ self.longitudeString = [GPXType valueForLongitude:region.center.longitude]; -+ self.radiusString = [GPXType valueForDecimal:region.radius]; -+} -+ -+#pragma mark - tag -+ -++ (NSString *)tagName -+{ -+ return kGPXSoundscapeRegionTagName; -+} -+ -+@end -+ -diff --git a/Pods/iOS-GPX-Framework/GPX/GPXSoundscapeSharedContentExtensions.h b/Pods/iOS-GPX-Framework/GPX/GPXSoundscapeSharedContentExtensions.h -new file mode 100644 -index 000000000..0945e225b ---- /dev/null -+++ b/Pods/iOS-GPX-Framework/GPX/GPXSoundscapeSharedContentExtensions.h -@@ -0,0 +1,20 @@ -+// -+// GPXSoundscapeSharedContentExtensions.m -+// iOS-GPX-Framework -+// -+// -+ -+#import "GPXElement.h" -+#import "GPXSoundscapeRegion.h" -+ -+@interface GPXSoundscapeSharedContentExtensions : GPXElement -+ -+@property (nonnull, nonatomic, strong) NSString * identifier; -+@property (nonnull, nonatomic, strong) NSString * behavior; -+@property (nullable, nonatomic, strong) NSString * version; -+@property (nonatomic, strong) GPXSoundscapeRegion * region; -+@property (nonnull, nonatomic, strong) NSLocale * locale; -+@property (nonnull, nonatomic, strong) NSDateInterval * availability; -+@property (nonatomic) BOOL expires; -+ -+@end -diff --git a/Pods/iOS-GPX-Framework/GPX/GPXSoundscapeSharedContentExtensions.m b/Pods/iOS-GPX-Framework/GPX/GPXSoundscapeSharedContentExtensions.m -new file mode 100644 -index 000000000..97d53ea66 ---- /dev/null -+++ b/Pods/iOS-GPX-Framework/GPX/GPXSoundscapeSharedContentExtensions.m -@@ -0,0 +1,169 @@ -+// -+// GPXSoundscapeSharedContentExtensions.m -+// iOS-GPX-Framework -+// -+// -+ -+#import "GPXSoundscapeSharedContentExtensions.h" -+ -+#import "GPXElementSubclass.h" -+ -+NSString *const kGPXSSSharedContentExtensionsTagName = @"gpxsc:meta"; -+ -+// MARK: Experience Meta Tags -+ -+NSString *const kElementID = @"gpxsc:id"; -+NSString *const kElementBehavior = @"gpxsc:behavior"; -+NSString *const kElementVersion = @"gpxsc:version"; -+ -+// MARK: Experience Tags -+ -+NSString *const kElementLocale = @"gpxsc:locale"; -+NSString *const kAttributeStartDate = @"start"; -+NSString *const kAttributeEndDate = @"end"; -+NSString *const kAttributeExpires = @"expires"; -+ -+@interface GPXSoundscapeSharedContentExtensions () -+ -+@property (nonatomic, strong) NSString * localeString; -+@property (nonatomic, strong) NSString * startDateString; -+@property (nonatomic, strong) NSString * endDateString; -+@property (nullable, nonatomic, strong) NSString * expiresString; -+ -+@end -+ -+@implementation GPXSoundscapeSharedContentExtensions -+ -+- (id)init -+{ -+ self = [super init]; -+ if (self) { -+ // TODO: Initialize required props -+ } -+ return self; -+} -+ -+- (id)initWithXMLElement:(TBXMLElement *)element parent:(GPXElement *)parent -+{ -+ self = [super initWithXMLElement:element parent:parent]; -+ if (self) { -+ self.identifier = [self textForSingleChildElementNamed:kElementID xmlElement:element required:true]; -+ self.behavior = [self textForSingleChildElementNamed:kElementBehavior xmlElement:element required:true]; -+ self.version = [self textForSingleChildElementNamed:kElementVersion xmlElement:element]; -+ -+ self.region = (GPXSoundscapeRegion *)[self childElementOfClass:[GPXSoundscapeRegion class] xmlElement:element]; -+ -+ self.localeString = [self textForSingleChildElementNamed:kElementLocale xmlElement:element required:true]; -+ self.startDateString = [self valueOfAttributeNamed:kAttributeStartDate xmlElement:element]; -+ self.endDateString = [self valueOfAttributeNamed:kAttributeEndDate xmlElement:element]; -+ self.expiresString = [self valueOfAttributeNamed:kAttributeExpires xmlElement:element]; -+ } -+ return self; -+} -+ -+#pragma mark - Public methods -+ -+- (void)setLocale:(NSLocale *)locale { -+ _localeString = locale ? locale.localeIdentifier : nil; -+} -+ -+- (NSLocale *)locale { -+ return self.localeString.length > 0 ? [[NSLocale alloc] initWithLocaleIdentifier:self.localeString] : nil; -+} -+ -+- (void)setAvailability:(NSDateInterval *)availability { -+ if (!availability) { -+ _startDateString = nil; -+ _endDateString = nil; -+ return; -+ } -+ -+ if ([[availability startDate] compare:[NSDate distantPast]] == NSOrderedSame) { -+ _startDateString = nil; -+ } else { -+ _startDateString = [GPXType valueForDateTime:[availability startDate]]; -+ } -+ -+ if ([[availability endDate] compare:[NSDate distantFuture]] == NSOrderedSame) { -+ _endDateString = nil; -+ } else { -+ _endDateString = [GPXType valueForDateTime:[availability endDate]]; -+ } -+} -+ -+- (NSDateInterval *)availability { -+ NSDate * start = _startDateString ? [GPXType dateTime:_startDateString] : [NSDate distantPast]; -+ NSDate * end = _endDateString ? [GPXType dateTime:_endDateString] : [NSDate distantFuture]; -+ -+ // Ensure that if the start or end date have an invalid format, we replace the date with something appropriate -+ if (start == nil) { -+ start = [NSDate distantPast]; -+ } -+ -+ if (end == nil) { -+ end = [NSDate distantFuture]; -+ } -+ -+ return [[NSDateInterval alloc] initWithStartDate:start endDate:end]; -+} -+ -+- (void) setExpires:(BOOL)expires { -+ if (expires) { -+ _expiresString = @"true"; -+ } else { -+ _expiresString = @"false"; -+ } -+} -+ -+- (BOOL) expires { -+ if ([_expiresString isEqualToString:@"true"]) { -+ return TRUE; -+ } else { -+ return FALSE; -+ } -+} -+ -+#pragma mark - tag -+ -++ (NSString *)tagName -+{ -+ return kGPXSSSharedContentExtensionsTagName; -+} -+ -+#pragma mark - GPX -+ -+- (void)addOpenTagToGpx:(NSMutableString *)gpx indentationLevel:(NSInteger)indentationLevel -+{ -+ NSMutableString *attribute = [NSMutableString stringWithString:@""]; -+ if (_startDateString) { -+ [attribute appendFormat:@" start=\"%@\"", _startDateString]; -+ } -+ if (_endDateString) { -+ [attribute appendFormat:@" end=\"%@\"", _endDateString]; -+ } -+ if (_expiresString) { -+ [attribute appendFormat:@" expires=\"%@\"", _expiresString]; -+ } -+ -+ [gpx appendString:[NSString stringWithFormat:@"%@<%@%@>\r\n" -+ , [self indentForIndentationLevel:indentationLevel] -+ , [[self class] tagName] -+ , attribute -+ ] -+ ]; -+} -+ -+- (void)addChildTagToGpx:(NSMutableString *)gpx indentationLevel:(NSInteger)indentationLevel -+{ -+ [super addChildTagToGpx:gpx indentationLevel:indentationLevel]; -+ -+ [self gpx:gpx addPropertyForValue:_identifier tagName:kElementID indentationLevel:indentationLevel]; -+ [self gpx:gpx addPropertyForValue:_behavior tagName:kElementBehavior indentationLevel:indentationLevel]; -+ -+ if (self.region) [self.region gpx:gpx indentationLevel:indentationLevel]; -+ -+ [self gpx:gpx addPropertyForValue:self.localeString tagName:kElementLocale indentationLevel:indentationLevel]; -+} -+ -+@end -+ -diff --git a/cocoapods-patch-20221103-54518-lfrmhf/iOS-GPX-Framework/GPX/GPXTrack.h b/Pods/iOS-GPX-Framework/GPX/GPXTrack.h -index 172a308d7..d0a5cb33c 100644 ---- a/cocoapods-patch-20221103-54518-lfrmhf/iOS-GPX-Framework/GPX/GPXTrack.h -+++ b/Pods/iOS-GPX-Framework/GPX/GPXTrack.h -@@ -36,10 +36,10 @@ - @property (strong, nonatomic) NSString *source; - - /** Links to external information about track. */ --@property (strong, nonatomic, readonly) NSArray *links; -+@property (strong, nonatomic, readonly) NSArray *links; - - /** GPS track number. */ --@property (nonatomic, assign) NSInteger number; -+@property (nonatomic) NSInteger number; - - /** Type (classification) of track. */ - @property (strong, nonatomic) NSString *type; -@@ -50,7 +50,7 @@ - /** A Track Segment holds a list of Track Points which are logically connected in order. - To represent a single GPS track where GPS reception was lost, or the GPS receiver was turned off, - start a new Track Segment for each continuous span of track data. */ --@property (strong, nonatomic, readonly) NSArray *tracksegments; -+@property (strong, nonatomic, readonly) NSArray *tracksegments; - - - /// --------------------------------- -@@ -76,7 +76,7 @@ - /** Adds the GPXLink objects contained in another given array to the end of the link array. - @param array An array of GPXLink objects to add to the end of the link array. - */ --- (void)addLinks:(NSArray *)array; -+- (void)addLinks:(NSArray *)array; - - - /// --------------------------------- -@@ -111,7 +111,7 @@ - /** Adds the GPXTrackSegment objects contained in another given array to the end of the tracksegment array. - @param array An array of GPXTrackSegment objects to add to the end of the tracksegment array. - */ --- (void)addTracksegments:(NSArray *)array; -+- (void)addTracksegments:(NSArray *)array; - - - /// --------------------------------- -diff --git a/cocoapods-patch-20221103-54518-lfrmhf/iOS-GPX-Framework/GPX/GPXTrackPoint.h b/Pods/iOS-GPX-Framework/GPX/GPXTrackPoint.h -index 06ddc756a..bcd2f16b2 100644 ---- a/cocoapods-patch-20221103-54518-lfrmhf/iOS-GPX-Framework/GPX/GPXTrackPoint.h -+++ b/Pods/iOS-GPX-Framework/GPX/GPXTrackPoint.h -@@ -19,6 +19,6 @@ - @param longitude The longitude of the point. - @return A newly created trackpoint element. - */ --+ (GPXTrackPoint *)trackpointWithLatitude:(CGFloat)latitude longitude:(CGFloat)longitude; -++ (GPXTrackPoint *)trackpointWithLatitude:(CLLocationDegrees)latitude longitude:(CLLocationDegrees)longitude; - - @end -diff --git a/cocoapods-patch-20221103-54518-lfrmhf/iOS-GPX-Framework/GPX/GPXTrackPoint.m b/Pods/iOS-GPX-Framework/GPX/GPXTrackPoint.m -index 110522c1b..d540d05d8 100644 ---- a/cocoapods-patch-20221103-54518-lfrmhf/iOS-GPX-Framework/GPX/GPXTrackPoint.m -+++ b/Pods/iOS-GPX-Framework/GPX/GPXTrackPoint.m -@@ -13,7 +13,7 @@ - - #pragma mark - Instance - --+ (GPXTrackPoint *)trackpointWithLatitude:(CGFloat)latitude longitude:(CGFloat)longitude -++ (GPXTrackPoint *)trackpointWithLatitude:(CLLocationDegrees)latitude longitude:(CLLocationDegrees)longitude - { - GPXTrackPoint *trackpoint = [GPXTrackPoint new]; - trackpoint.latitude = latitude; -diff --git a/Pods/iOS-GPX-Framework/GPX/GPXTrackPointExtensions.h b/Pods/iOS-GPX-Framework/GPX/GPXTrackPointExtensions.h -new file mode 100644 -index 000000000..3a8980115 ---- /dev/null -+++ b/Pods/iOS-GPX-Framework/GPX/GPXTrackPointExtensions.h -@@ -0,0 +1,19 @@ -+// -+// GPXTrackPointExtensions.h -+// GPX -+// -+// Created by Felix Schneider on 29.09.2014. -+// -+// -+ -+#import "GPXElement.h" -+ -+@interface GPXTrackPointExtensions : GPXElement -+ -+/* see: http://www8.garmin.com/xmlschemas/TrackPointExtensionv2.xsd */ -+@property (nonatomic, strong) NSNumber *heartRate; -+@property (nonatomic, strong) NSNumber *cadence; -+@property (nonatomic, strong) NSNumber *speed; -+@property (nonatomic, strong) NSNumber *course; -+ -+@end -diff --git a/Pods/iOS-GPX-Framework/GPX/GPXTrackPointExtensions.m b/Pods/iOS-GPX-Framework/GPX/GPXTrackPointExtensions.m -new file mode 100644 -index 000000000..92ae44b31 ---- /dev/null -+++ b/Pods/iOS-GPX-Framework/GPX/GPXTrackPointExtensions.m -@@ -0,0 +1,105 @@ -+// -+// GPXTrackPointExtensions.m -+// GPX -+// -+// Created by Felix Schneider on 29.09.2014. -+// -+// -+ -+#import "GPXTrackPointExtensions.h" -+#import "GPXElementSubclass.h" -+ -+NSString *const kGPXTrackPointExtensionsTagName = @"gpxtpx:TrackPointExtension"; -+ -+@interface GPXTrackPointExtensions () -+ -+@property (nonatomic, strong) NSString *heartRateString; -+@property (nonatomic, strong) NSString *cadenceString; -+@property (nonatomic, strong) NSString *speedString; -+@property (nonatomic, strong) NSString *courseString; -+ -+@end -+ -+@implementation GPXTrackPointExtensions -+ -+- (id)initWithXMLElement:(TBXMLElement *)element parent:(GPXElement *)parent -+{ -+ self = [super initWithXMLElement:element parent:parent]; -+ if (self) { -+ _heartRateString = [self textForSingleChildElementNamed:@"gpxtpx:hr" xmlElement:element]; -+ _cadenceString = [self textForSingleChildElementNamed:@"gpxtpx:cad" xmlElement:element]; -+ _speedString = [self textForSingleChildElementNamed:@"gpxtpx:speed" xmlElement:element]; -+ _courseString = [self textForSingleChildElementNamed:@"gpxtpx:course" xmlElement:element]; -+ } -+ return self; -+} -+ -+#pragma mark - Public methods -+- (void)setHeartRate:(NSNumber *)heartRate -+{ -+ _heartRateString = heartRate? [NSString stringWithFormat:@"%ud", [heartRate unsignedIntValue]]: nil; -+} -+ -+- (void)setCadence:(NSNumber *)cadence -+{ -+ _cadenceString = cadence ? [NSString stringWithFormat:@"%ud", [cadence unsignedIntValue]]: nil; -+} -+ -+- (void)setSpeed:(NSNumber *)speed -+{ -+ _speedString = speed ? [GPXType valueForDecimal:speed.doubleValue] : nil; -+} -+ -+- (void)setCourse:(NSNumber *)course -+{ -+ _courseString = course ? [GPXType valueForDecimal:course.doubleValue]: nil; -+} -+ -+- (NSNumber *)heartRate { -+ if (!_heartRateString.length) { -+ return nil; -+ } -+ return [NSNumber numberWithDouble:[GPXType decimal:_heartRateString]]; -+} -+ -+- (NSNumber *)cadence { -+ if (!_cadenceString.length) { -+ return nil; -+ } -+ return [NSNumber numberWithInteger:[GPXType nonNegativeInteger:_cadenceString]]; -+} -+ -+- (NSNumber *)speed { -+ if (!_speedString.length) { -+ return nil; -+ } -+ return [NSNumber numberWithDouble:[GPXType decimal:_speedString]]; -+} -+ -+- (NSNumber *)course { -+ if (!_courseString.length) { -+ return nil; -+ } -+ return [NSNumber numberWithDouble:[GPXType decimal:_courseString]]; -+} -+ -+#pragma mark - tag -+ -++ (NSString *)tagName -+{ -+ return kGPXTrackPointExtensionsTagName; -+} -+ -+#pragma mark - GPX -+ -+- (void)addChildTagToGpx:(NSMutableString *)gpx indentationLevel:(NSInteger)indentationLevel -+{ -+ [super addChildTagToGpx:gpx indentationLevel:indentationLevel]; -+ [self gpx:gpx addPropertyForValue:_heartRateString tagName:@"gpxtpx:hr" indentationLevel:indentationLevel]; -+ [self gpx:gpx addPropertyForValue:_cadenceString tagName:@"gpxtpx:cad" indentationLevel:indentationLevel]; -+ [self gpx:gpx addPropertyForValue:_speedString tagName:@"gpxtpx:speed" indentationLevel:indentationLevel]; -+ [self gpx:gpx addPropertyForValue:_courseString tagName:@"gpxtpx:course" indentationLevel:indentationLevel]; -+} -+ -+ -+@end -diff --git a/cocoapods-patch-20221103-54518-lfrmhf/iOS-GPX-Framework/GPX/GPXTrackSegment.h b/Pods/iOS-GPX-Framework/GPX/GPXTrackSegment.h -index 5c9c91646..054f8e8b9 100644 ---- a/cocoapods-patch-20221103-54518-lfrmhf/iOS-GPX-Framework/GPX/GPXTrackSegment.h -+++ b/Pods/iOS-GPX-Framework/GPX/GPXTrackSegment.h -@@ -23,7 +23,7 @@ - /// --------------------------------- - - /** A Track Point holds the coordinates, elevation, timestamp, and metadata for a single point in a track. */ --@property (strong, nonatomic, readonly) NSArray *trackpoints; -+@property (strong, nonatomic, readonly) NSArray *trackpoints; - - /** You can add extend GPX by adding your own elements from another schema here. */ - @property (strong, nonatomic) GPXExtensions *extensions; -@@ -53,7 +53,7 @@ - /** Adds the GPXTrackPoint objects contained in another given array to the end of the trackpoint array. - @param array An array of GPXTrackPoint objects to add to the end of the trackpoint array. - */ --- (void)addTrackpoints:(NSArray *)array; -+- (void)addTrackpoints:(NSArray *)array; - - - /// --------------------------------- -diff --git a/Pods/iOS-GPX-Framework/GPX/GPXTrailsTrackExtensions.h b/Pods/iOS-GPX-Framework/GPX/GPXTrailsTrackExtensions.h -new file mode 100644 -index 000000000..eafec5e5c ---- /dev/null -+++ b/Pods/iOS-GPX-Framework/GPX/GPXTrailsTrackExtensions.h -@@ -0,0 +1,16 @@ -+// -+// GPXTrailsTrackExtensions.h -+// GPX -+// -+// Created by Jan Weitz on 27.04.2015 -+// -+// -+ -+#import "GPXElement.h" -+ -+@interface GPXTrailsTrackExtensions : GPXElement -+ -+/* see: https://trails.io/GPX/1/0/trails_1.0.xsd */ -+@property (nonatomic) NSString *activityTypeString; -+ -+@end -diff --git a/Pods/iOS-GPX-Framework/GPX/GPXTrailsTrackExtensions.m b/Pods/iOS-GPX-Framework/GPX/GPXTrailsTrackExtensions.m -new file mode 100644 -index 000000000..3100cc8da ---- /dev/null -+++ b/Pods/iOS-GPX-Framework/GPX/GPXTrailsTrackExtensions.m -@@ -0,0 +1,53 @@ -+// -+// GPXTrailsTrackExtensions.m -+// GPX -+// -+// Created by Jan Weitz on 27.04.2015 -+// -+// -+ -+#import "GPXTrailsTrackExtensions.h" -+#import "GPXElementSubclass.h" -+ -+NSString *const kElementActivity = @"trailsio:activity"; -+NSString *const kTrackExtensionsTagName = @"trailsio:TrackExtension"; -+ -+@interface GPXTrailsTrackExtensions () -+ -+@end -+ -+@implementation GPXTrailsTrackExtensions -+ -+- (id)initWithXMLElement:(TBXMLElement *)element parent:(GPXElement *)parent { -+ self = [super initWithXMLElement:element parent:parent]; -+ -+ if (self) { -+ _activityTypeString = [self textForSingleChildElementNamed:kElementActivity xmlElement:element]; -+ } -+ -+ return self; -+} -+ -+#pragma mark - Public methods -+- (NSString *)activityTypeString { -+ if (!_activityTypeString.length) { -+ return nil; -+ } -+ -+ return _activityTypeString; -+} -+ -+#pragma mark - tag -+ -++ (NSString *)tagName { -+ return kTrackExtensionsTagName; -+} -+ -+#pragma mark - GPX -+ -+- (void)addChildTagToGpx:(NSMutableString *)gpx indentationLevel:(NSInteger)indentationLevel { -+ [super addChildTagToGpx:gpx indentationLevel:indentationLevel]; -+ [self gpx:gpx addPropertyForValue:_activityTypeString tagName:kElementActivity indentationLevel:indentationLevel]; -+} -+ -+@end -diff --git a/Pods/iOS-GPX-Framework/GPX/GPXTrailsTrackPointExtensions.h b/Pods/iOS-GPX-Framework/GPX/GPXTrailsTrackPointExtensions.h -new file mode 100644 -index 000000000..2822c8728 ---- /dev/null -+++ b/Pods/iOS-GPX-Framework/GPX/GPXTrailsTrackPointExtensions.h -@@ -0,0 +1,18 @@ -+// -+// GPXTrailsTrackPointExtensions.h -+// GPX -+// -+// Created by Jan Weitz on 27.04.2015 -+// -+// -+ -+#import "GPXElement.h" -+ -+@interface GPXTrailsTrackPointExtensions : GPXElement -+ -+/* see: https://trails.io/GPX/1/0/trails_1.0.xsd */ -+@property (nonatomic) NSNumber *horizontalAccuracy; -+@property (nonatomic) NSNumber *verticalAccuracy; -+@property (nonatomic) NSNumber *stepCount; -+ -+@end -diff --git a/Pods/iOS-GPX-Framework/GPX/GPXTrailsTrackPointExtensions.m b/Pods/iOS-GPX-Framework/GPX/GPXTrailsTrackPointExtensions.m -new file mode 100644 -index 000000000..054c3ea1b ---- /dev/null -+++ b/Pods/iOS-GPX-Framework/GPX/GPXTrailsTrackPointExtensions.m -@@ -0,0 +1,91 @@ -+// -+// GPXTrailsTrackPointExtensions.m -+// GPX -+// -+// Created by Jan Weitz on 27.04.2015 -+// -+// -+ -+#import "GPXTrailsTrackPointExtensions.h" -+#import "GPXElementSubclass.h" -+ -+NSString *const kElementHorizontalAcc = @"trailsio:hacc"; -+NSString *const kElementVerticalAcc = @"trailsio:vacc"; -+NSString *const kElementSteps = @"trailsio:steps"; -+NSString *const kTrackPointExtensionsTagName = @"trailsio:TrackPointExtension"; -+ -+@interface GPXTrailsTrackPointExtensions () -+ -+@property (nonatomic) NSString *horizontalAccuracyString; -+@property (nonatomic) NSString *verticalAccuracyString; -+@property (nonatomic) NSString *stepCountString; -+ -+@end -+ -+@implementation GPXTrailsTrackPointExtensions -+ -+- (id)initWithXMLElement:(TBXMLElement *)element parent:(GPXElement *)parent { -+ self = [super initWithXMLElement:element parent:parent]; -+ -+ if (self) { -+ _horizontalAccuracyString = [self textForSingleChildElementNamed:kElementHorizontalAcc xmlElement:element]; -+ _verticalAccuracyString = [self textForSingleChildElementNamed:kElementVerticalAcc xmlElement:element]; -+ _stepCountString = [self textForSingleChildElementNamed:kElementSteps xmlElement:element]; -+ } -+ -+ return self; -+} -+ -+#pragma mark - Public methods -+- (void)setHorizontalAccuracy:(NSNumber *)horizontalAccuracy { -+ _horizontalAccuracyString = horizontalAccuracy ? horizontalAccuracy.stringValue : nil; -+} -+ -+- (void)setVerticalAccuracy:(NSNumber *)verticalAccuracy { -+ _verticalAccuracyString = verticalAccuracy ? verticalAccuracy.stringValue : nil; -+} -+ -+- (void)setStepCount:(NSNumber *)stepCount { -+ _stepCountString = stepCount ? stepCount.stringValue : nil; -+} -+ -+- (NSNumber *)horizontalAccuracy { -+ if (!_horizontalAccuracyString.length) { -+ return nil; -+ } -+ -+ return [NSNumber numberWithDouble:[GPXType decimal:_horizontalAccuracyString]]; -+} -+ -+- (NSNumber *)verticalAccuracy { -+ if (!_verticalAccuracyString.length) { -+ return nil; -+ } -+ -+ return [NSNumber numberWithDouble:[GPXType decimal:_verticalAccuracyString]]; -+} -+ -+- (NSNumber *)stepCount { -+ if (!_stepCountString.length) { -+ return nil; -+ } -+ -+ return [NSNumber numberWithDouble:[GPXType decimal:_stepCountString]]; -+} -+ -+#pragma mark - tag -+ -++ (NSString *)tagName { -+ return kTrackPointExtensionsTagName; -+} -+ -+#pragma mark - GPX -+ -+- (void)addChildTagToGpx:(NSMutableString *)gpx indentationLevel:(NSInteger)indentationLevel { -+ [super addChildTagToGpx:gpx indentationLevel:indentationLevel]; -+ [self gpx:gpx addPropertyForValue:_horizontalAccuracyString tagName:kElementHorizontalAcc indentationLevel:indentationLevel]; -+ [self gpx:gpx addPropertyForValue:_verticalAccuracyString tagName:kElementVerticalAcc indentationLevel:indentationLevel]; -+ [self gpx:gpx addPropertyForValue:_stepCountString tagName:kElementSteps indentationLevel:indentationLevel]; -+} -+ -+@end -diff --git a/cocoapods-patch-20221103-54518-lfrmhf/iOS-GPX-Framework/GPX/GPXType.h b/Pods/iOS-GPX-Framework/GPX/GPXType.h -index 6d0c0dfb0..cc24ab754 100644 ---- a/cocoapods-patch-20221103-54518-lfrmhf/iOS-GPX-Framework/GPX/GPXType.h -+++ b/Pods/iOS-GPX-Framework/GPX/GPXType.h -@@ -14,46 +14,49 @@ typedef NS_ENUM(NSInteger, GPXFix) { - GPXFixPps, - }; - -+@import UIKit; -+@import CoreLocation; -+ - - /** Convinience methods for GPX Value types. - */ - @interface GPXType : NSObject - --/** Return the CGFloat object from a given string. -- @param value The string which to convert CGFloat. A value ≥−90 and ≤90. -- @return A CGFloat from a value. -+/** Return the CLLocationDegrees object from a given string. -+ @param value The string which to convert CLLocationDegrees. A value ≥−90 and ≤90. -+ @return A CLLocationDegrees from a value. - */ --+ (CGFloat)latitude:(NSString *)value; -++ (CLLocationDegrees)latitude:(NSString *)value; - --/** Return the NSString object from a given CGFloat. -- @param latitude The CGFloat which to convert NSString. A value ≥−90 and ≤90. -+/** Return the NSString object from a given CLLocationDegrees. -+ @param latitude The CLLocationDegrees which to convert NSString. A value ≥−90 and ≤90. - @return A NSString from a latitude. - */ --+ (NSString *)valueForLatitude:(CGFloat)latitude; -++ (NSString *)valueForLatitude:(CLLocationDegrees)latitude; - --/** Return the CGFloat object from a given string. -+/** Return the CLLocationDegrees object from a given string. - @param value The string which to convert CGFloat. A value ≥−180 and ≤180. -- @return A CGFloat from a value. -+ @return A CLLocationDegrees from a value. - */ --+ (CGFloat)longitude:(NSString *)value; -++ (CLLocationDegrees)longitude:(NSString *)value; - --/** Return the NSString object from a given CGFloat. -- @param longitude The CGFloat which to convert NSString. A value ≥−180 and ≤180. -+/** Return the NSString object from a given CLLocationDegrees. -+ @param longitude The CLLocationDegrees which to convert NSString. A value ≥−180 and ≤180. - @return A NSString from a longitude. - */ --+ (NSString *)valueForLongitude:(CGFloat)longitude; -++ (NSString *)valueForLongitude:(CLLocationDegrees)longitude; - --/** Return the CGFloat object from a given string. -- @param value The string which to convert CGFloat. A value ≥0 and ≤360. -- @return A CGFloat from a value. -+/** Return the CLLocationDegrees object from a given string. -+ @param value The string which to convert CLLocationDegrees. A value ≥0 and ≤360. -+ @return A CLLocationDegrees from a value. - */ --+ (CGFloat)degress:(NSString *)value; -++ (CLLocationDegrees)degress:(NSString *)value; - --/** Return the NSString object from a given CGFloat. -- @param degress The CGFloat which to convert NSString. A value ≥0 and ≤360. -+/** Return the NSString object from a given CLLocationDegrees. -+ @param degress The CLLocationDegrees which to convert NSString. A value ≥0 and ≤360. - @return A NSString from a degress. - */ --+ (NSString *)valueForDegress:(CGFloat)degress; -++ (NSString *)valueForDegress:(CLLocationDegrees)degress; - - /** Return the GPXFix from a given string. - @param value The string which to convert GPXFix. -@@ -79,17 +82,17 @@ typedef NS_ENUM(NSInteger, GPXFix) { - */ - + (NSString *)valueForDgpsStation:(NSInteger)dgpsStation; - --/** Return the CGFloat object from a given string. -+/** Return the double value from a given string. - @param value The string which to convert CGFloat. -- @return A CGFloat from a value. -+ @return A double from a value. - */ --+ (CGFloat)decimal:(NSString *)value; -++ (double)decimal:(NSString *)value; - --/** Return the NSString object from a given CGFloat. -- @param decimal The CGFloat which to convert NSString. -+/** Return the NSString object from a given double. -+ @param decimal The double which to convert NSString. - @return A NSString from a decimal. - */ --+ (NSString *)valueForDecimal:(CGFloat)decimal; -++ (NSString *)valueForDecimal:(double)decimal; - - /** Return the NSDate object from a given string. - -diff --git a/cocoapods-patch-20221103-54518-lfrmhf/iOS-GPX-Framework/GPX/GPXType.m b/Pods/iOS-GPX-Framework/GPX/GPXType.m -index b956fcf60..b512be526 100644 ---- a/cocoapods-patch-20221103-54518-lfrmhf/iOS-GPX-Framework/GPX/GPXType.m -+++ b/Pods/iOS-GPX-Framework/GPX/GPXType.m -@@ -10,21 +10,13 @@ - - @implementation GPXType - --+ (CGFloat)latitude:(NSString *)value -++ (CLLocationDegrees)latitude:(NSString *)value - { -- @try { -- CGFloat f = [value floatValue]; -- if (-90.f <= f && f <= 90.f) { -- return f; -- } -- } -- @catch (NSException *exception) { -- } -- -- return 0.f; -+ CLLocationDegrees f = [value doubleValue]; -+ return f; - } - --+ (NSString *)valueForLatitude:(CGFloat)latitude -++ (NSString *)valueForLatitude:(CLLocationDegrees)latitude - { - if (-90.f <= latitude && latitude <= 90.f) { - return [NSString stringWithFormat:@"%f", latitude]; -@@ -33,21 +25,13 @@ - return @"0"; - } - --+ (CGFloat)longitude:(NSString *)value -++ (CLLocationDegrees)longitude:(NSString *)value - { -- @try { -- CGFloat f = [value floatValue]; -- if (-180.f <= f && f <= 180.f) { -- return f; -- } -- } -- @catch (NSException *exception) { -- } -- -- return 0.f; -+ CLLocationDegrees f = [value doubleValue]; -+ return f; - } - --+ (NSString *)valueForLongitude:(CGFloat)longitude -++ (NSString *)valueForLongitude:(CLLocationDegrees)longitude - { - if (-180.f <= longitude && longitude <= 180.f) { - return [NSString stringWithFormat:@"%f", longitude]; -@@ -56,27 +40,19 @@ - return @"0"; - } - --+ (CGFloat)degress:(NSString *)value -++ (CLLocationDegrees)degress:(NSString *)value - { -- @try { -- CGFloat f = [value floatValue]; -- if (0.f <= f && f <= 360.f) { -- return f; -- } -- } -- @catch (NSException *exception) { -- } -- -- return 0.f; -+ CLLocationDegrees f = [value doubleValue]; -+ return f; - } - --+ (NSString *)valueForDegress:(CGFloat)degress -++ (NSString *)valueForDegress:(CLLocationDegrees)degress - { - if (0.0f <= degress && degress <= 360.f) { - return [NSString stringWithFormat:@"%f", degress]; - } - -- return @"0"; -+ return @"-1"; - } - - + (GPXFix)fix:(NSString *)value -@@ -117,16 +93,11 @@ - - + (NSInteger)dgpsStation:(NSString *)value - { -- @try { -- NSInteger i = [value integerValue]; -- if (0 <= i && i <= 1023) { -- return i; -- } -+ NSInteger i = [value integerValue]; -+ if (0 <= i && i <= 1023) { -+ return i; - } -- @catch (NSException *exception) { -- } -- -- return 0; -+ return 0; - } - - + (NSString *)valueForDgpsStation:(NSInteger)dgpsStation -@@ -138,55 +109,84 @@ - return @"0"; - } - --+ (CGFloat)decimal:(NSString *)value -++ (double)decimal:(NSString *)value - { -- @try { -- CGFloat f = [value floatValue]; -- return f; -- } -- @catch (NSException *exception) { -- } -- -- return 0; -+ double f = [value doubleValue]; -+ return f; - } - --+ (NSString *)valueForDecimal:(CGFloat)decimal -++ (NSString *)valueForDecimal:(double)decimal - { -- return [NSString stringWithFormat:@"%f", decimal]; -- -+ return [NSNumber numberWithDouble:decimal].stringValue; - } - -++ (NSDateFormatter *)newDateFormatterWithFormat:(NSString *)format { -+ if (!format) { -+ return nil; -+ } -+ -+ NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init]; -+ NSLocale *en_US_POSIX = [[NSLocale alloc] initWithLocaleIdentifier:@"en_US_POSIX"]; -+ -+ [dateFormatter setCalendar:[[NSCalendar alloc] initWithCalendarIdentifier:NSCalendarIdentifierGregorian]]; -+ [dateFormatter setLocale:en_US_POSIX]; -+ [dateFormatter setTimeZone:[NSTimeZone timeZoneForSecondsFromGMT:0]]; -+ [dateFormatter setDateFormat:format]; -+ return dateFormatter; -+} - + (NSDate *)dateTime:(NSString *)value - { - NSDate *date; - -- NSDateFormatter *formatter = [[NSDateFormatter alloc] init]; -- formatter.timeZone = [NSTimeZone timeZoneForSecondsFromGMT:0]; -+ static NSDateFormatter *dateFormatter_ssZ = nil; -+ static dispatch_once_t pred; -+ -+ dispatch_once(&pred, ^{ -+ dateFormatter_ssZ = [self newDateFormatterWithFormat:@"yyyy'-'MM'-'dd'T'HH':'mm':'ss'Z'"]; -+ }); - -+ - // dateTime(YYYY-MM-DDThh:mm:ssZ) -- formatter.dateFormat = @"yyyy'-'MM'-'dd'T'HH':'mm':'ss'Z'"; -- date = [formatter dateFromString:value]; -+ date = [dateFormatter_ssZ dateFromString:value]; - if (date) { - return date; - } - -+ static NSDateFormatter *dateFormatter_ss_SSSZ = nil; -+ static dispatch_once_t pred_ss_SSSZ; -+ -+ dispatch_once(&pred_ss_SSSZ, ^{ -+ dateFormatter_ss_SSSZ = [self newDateFormatterWithFormat:@"yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'SSS'Z'"]; -+ }); -+ - // dateTime(YYYY-MM-DDThh:mm:ss.SSSZ) -- formatter.dateFormat = @"yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'SSS'Z'"; -- date = [formatter dateFromString:value]; -+ date = [dateFormatter_ss_SSSZ dateFromString:value]; - if (date) { - return date; - } - -- // dateTime(YYYY-MM-DDThh:mm:sszzzzzz) -- if (value.length >= 22) { -- formatter.dateFormat = @"yyyy'-'MM'-'dd'T'HH':'mm':'sszzzz"; -- NSString *v = [value stringByReplacingOccurrencesOfString:@":" withString:@"" options:0 range:NSMakeRange(22, 1)]; -- date = [formatter dateFromString:v]; -+ -+ // dateTime(YYYY-MM-DDThh:mm:sszzzzzz -+ NSUInteger maxLength = 22; -+ if (value.length >= maxLength) { -+ static NSDateFormatter *dateFormatter_sszzzz = nil; -+ static dispatch_once_t pred_sszzzz; -+ -+ dispatch_once(&pred_sszzzz, ^{ -+ dateFormatter_sszzzz = [self newDateFormatterWithFormat:@"yyyy'-'MM'-'dd'T'HH':'mm':'sszzzz"]; -+ }); -+ -+ NSUInteger remaining = value.length - maxLength; -+ NSString *v = [value stringByReplacingOccurrencesOfString:@":" withString:@"" options:0 range:NSMakeRange(maxLength, remaining)]; -+ date = [dateFormatter_sszzzz dateFromString:v]; - if (date) { - return date; - } - } - -+ NSDateFormatter *formatter = [[NSDateFormatter alloc] init]; -+ formatter.timeZone = [NSTimeZone timeZoneForSecondsFromGMT:0]; -+ - // date - formatter.dateFormat = @"yyyy'-'MM'-'dd'"; - date = [formatter dateFromString:value]; -@@ -224,13 +224,9 @@ - - + (NSInteger)nonNegativeInteger:(NSString *)value - { -- @try { -- NSInteger i = [value integerValue]; -- if (i >= 0) { -- return i; -- } -- } -- @catch (NSException *exception) { -+ NSInteger i = [value integerValue]; -+ if (i > 0) { -+ return i; - } - - return 0; -@@ -241,7 +237,6 @@ - if (integer >= 0) { - return [NSString stringWithFormat:@"%ld", (long)integer]; - } -- - return @"0"; - } - -diff --git a/cocoapods-patch-20221103-54518-lfrmhf/iOS-GPX-Framework/GPX/GPXWaypoint.h b/Pods/iOS-GPX-Framework/GPX/GPXWaypoint.h -index 22fc8f019..2cd2d8ed3 100644 ---- a/cocoapods-patch-20221103-54518-lfrmhf/iOS-GPX-Framework/GPX/GPXWaypoint.h -+++ b/Pods/iOS-GPX-Framework/GPX/GPXWaypoint.h -@@ -22,7 +22,10 @@ - /// --------------------------------- - - /** Elevation (in meters) of the point. */ --@property (nonatomic, assign) CGFloat elevation; -+@property (nonatomic) CLLocationDistance elevation; -+ -+/// Returns nil if elevation is not present in waypoint information. -+@property (nonatomic, readonly) NSNumber *elevationBoxed; - - /** Creation/modification timestamp for element. - Date and time in are in Univeral Coordinated Time (UTC), not local time! -@@ -31,10 +34,10 @@ - @property (strong, nonatomic) NSDate *time; - - /** Magnetic variation (in degrees) at the point */ --@property (nonatomic, assign) CGFloat magneticVariation; -+@property (nonatomic) CGFloat magneticVariation; - - /** Height (in meters) of geoid (mean sea level) above WGS84 earth ellipsoid. As defined in NMEA GGA message. */ --@property (nonatomic, assign) CGFloat geoidHeight; -+@property (nonatomic) CLLocationDistance geoidHeight; - - /** The GPS name of the waypoint. This field will be transferred to and from the GPS. - GPX does not place restrictions on the length of this field or the characters contained in it. -@@ -52,7 +55,7 @@ - @property (strong, nonatomic) NSString *source; - - /** Link to additional information about the waypoint. */ --@property (strong, nonatomic) NSArray *links; -+@property (strong, nonatomic) NSArray *links; - - /** Text of GPS symbol name. For interchange with other programs, use the exact spelling of the symbol as displayed on the GPS. - If the GPS abbreviates words, spell them out. */ -@@ -61,35 +64,41 @@ - /** Type (classification) of the waypoint. */ - @property (strong, nonatomic) NSString *type; - -+/** Instantaneous speed at the point in m/s. */ -+@property (strong, nonatomic) NSNumber *speed; -+ -+/** Instantaneous course at the point. */ -+@property (strong, nonatomic) NSNumber *course; -+ - /** Type of GPX fix. */ --@property (nonatomic, assign) NSInteger fix; -+@property (nonatomic) NSInteger fix; - - /** Number of satellites used to calculate the GPX fix. */ --@property (nonatomic, assign) NSInteger satellites; -+@property (nonatomic) NSInteger satellites; - - /** Horizontal dilution of precision. */ --@property (nonatomic, assign) CGFloat horizontalDilution; -+@property (nonatomic) double horizontalDilution; - - /** Vertical dilution of precision. */ --@property (nonatomic, assign) CGFloat verticalDilution; -+@property (nonatomic) double verticalDilution; - - /** Position dilution of precision. */ --@property (nonatomic, assign) CGFloat positionDilution; -+@property (nonatomic) double positionDilution; - - /** Number of seconds since last DGPS update. */ --@property (nonatomic, assign) CGFloat ageOfDGPSData; -+@property (nonatomic) double ageOfDGPSData; - - /** ID of DGPS station used in differential correction. */ --@property (nonatomic, assign) NSInteger DGPSid; -+@property (nonatomic) NSInteger DGPSid; - - /** You can add extend GPX by adding your own elements from another schema here. */ --@property (strong, nonatomic) GPXExtensions *extensions; -+@property (strong, nonatomic, nullable) GPXExtensions *extensions; - - /** The latitude of the point. Decimal degrees, WGS84 datum. */ --@property (nonatomic, assign) CGFloat latitude; -+@property (nonatomic) CLLocationDegrees latitude; - - /** The longitude of the point. Decimal degrees, WGS84 datum. */ --@property (nonatomic, assign) CGFloat longitude; -+@property (nonatomic) CLLocationDegrees longitude; - - - /// --------------------------------- -@@ -127,7 +136,7 @@ - /** Adds the GPXLink objects contained in another given array to the end of the link array. - @param array An array of GPXLink objects to add to the end of the link array. - */ --- (void)addLinks:(NSArray *)array; -+- (void)addLinks:(NSArray *)array; - - - /// --------------------------------- -diff --git a/cocoapods-patch-20221103-54518-lfrmhf/iOS-GPX-Framework/GPX/GPXWaypoint.m b/Pods/iOS-GPX-Framework/GPX/GPXWaypoint.m -index 6f06aefac..ea261d11e 100644 ---- a/cocoapods-patch-20221103-54518-lfrmhf/iOS-GPX-Framework/GPX/GPXWaypoint.m -+++ b/Pods/iOS-GPX-Framework/GPX/GPXWaypoint.m -@@ -13,6 +13,8 @@ - - @implementation GPXWaypoint { - NSString *_elevationValue; -+ NSString *_speedValue; -+ NSString *_courseValue; - NSString *_timeValue; - NSString *_magneticVariationValue; - NSString *_geoidHeightValue; -@@ -65,7 +67,11 @@ - _comment = [self textForSingleChildElementNamed:@"cmt" xmlElement:element]; - _desc = [self textForSingleChildElementNamed:@"desc" xmlElement:element]; - _source = [self textForSingleChildElementNamed:@"src" xmlElement:element]; -- -+ -+ // speed and course may be direct child tags of waypoints according to http://www.topografix.com/gpx_manual.asp -+ _courseValue = [self textForSingleChildElementNamed:@"course" xmlElement:element]; -+ _speedValue = [self textForSingleChildElementNamed:@"speed" xmlElement:element]; -+ - NSMutableArray *array = [NSMutableArray array]; - [self childElementsOfClass:[GPXLink class] - xmlElement:element -@@ -103,16 +109,23 @@ - - #pragma mark - Public methods - --- (CGFloat)elevation -+- (CLLocationDistance)elevation - { - return [GPXType decimal:_elevationValue]; - } - --- (void)setElevation:(CGFloat)elevation -+- (void)setElevation:(CLLocationDistance)elevation - { - _elevationValue = [GPXType valueForDecimal:elevation]; - } - -+- (NSNumber *)elevationBoxed { -+ if (!_elevationValue.length) { -+ return nil; -+ } -+ return @([self elevation]); -+} -+ - - (NSDate *)time - { - return [GPXType dateTime:_timeValue]; -@@ -123,6 +136,31 @@ - _timeValue = [GPXType valueForDateTime:time]; - } - -+- (NSNumber *)speed { -+ if (!_speedValue) { -+ return nil; -+ } -+ -+ return @([_speedValue doubleValue]); -+} -+ -+ -+- (void)setSpeed:(NSNumber *)speed { -+ _speedValue = [speed stringValue]; -+} -+ -+- (NSNumber *)course { -+ if (!_courseValue) { -+ return nil; -+ } -+ -+ return @([_courseValue doubleValue]); -+} -+ -+- (void)setCourse:(NSNumber *)course { -+ _courseValue = [course stringValue]; -+} -+ - - (CGFloat)magneticVariation - { - return [GPXType degress:_magneticVariationValue]; -@@ -133,12 +171,12 @@ - _magneticVariationValue = [GPXType valueForDegress:magneticVariation]; - } - --- (CGFloat)geoidHeight -+- (CLLocationDistance)geoidHeight - { - return [GPXType decimal:_geoidHeightValue]; - } - --- (void)setGeoidHeight:(CGFloat)geoidHeight -+- (void)setGeoidHeight:(CLLocationDistance)geoidHeight - { - _geoidHeightValue = [GPXType valueForDecimal:geoidHeight]; - } -@@ -197,42 +235,42 @@ - _satellitesValue = [GPXType valueForNonNegativeInteger:satellites]; - } - --- (CGFloat)horizontalDilution -+- (double)horizontalDilution - { - return [GPXType decimal:_horizontalDilutionValue]; - } - --- (void)setHorizontalDilution:(CGFloat)horizontalDilution -+- (void)setHorizontalDilution:(double)horizontalDilution - { - _horizontalDilutionValue = [GPXType valueForDecimal:horizontalDilution]; - } - --- (CGFloat)verticalDilution -+- (double)verticalDilution - { - return [GPXType decimal:_verticalDilutionValue]; - } - --- (void)setVerticalDilution:(CGFloat)verticalDilution -+- (void)setVerticalDilution:(double)verticalDilution - { - _verticalDilutionValue = [GPXType valueForDecimal:verticalDilution]; - } - --- (CGFloat)positionDilution -+- (double)positionDilution - { - return [GPXType decimal:_positionDilutionValue]; - } - --- (void)setPositionDilution:(CGFloat)positionDilution -+- (void)setPositionDilution:(double)positionDilution - { - _positionDilutionValue = [GPXType valueForDecimal:positionDilution]; - } - --- (CGFloat)ageOfDGPSData -+- (double)ageOfDGPSData - { - return [GPXType decimal:_ageOfDGPSDataValue]; - } - --- (void)setAgeOfDGPSData:(CGFloat)ageOfDGPSData -+- (void)setAgeOfDGPSData:(double)ageOfDGPSData - { - _ageOfDGPSDataValue = [GPXType valueForDecimal:ageOfDGPSData]; - } -@@ -247,22 +285,22 @@ - _DGPSidValue = [GPXType valueForDgpsStation:DGPSid]; - } - --- (CGFloat)latitude -+- (CLLocationDegrees)latitude - { - return [GPXType latitude:_latitudeValue]; - } - --- (void)setLatitude:(CGFloat)latitude -+- (void)setLatitude:(CLLocationDegrees)latitude - { - _latitudeValue = [GPXType valueForLatitude:latitude]; - } - --- (CGFloat)longitude -+- (CLLocationDegrees)longitude - { - return [GPXType longitude:_longitudeValue]; - } - --- (void)setLongitude:(CGFloat)longitude -+- (void)setLongitude:(CLLocationDegrees)longitude - { - _longitudeValue = [GPXType valueForLongitude:longitude]; - } diff --git a/docs/ios-client/onboarding.md b/docs/ios-client/onboarding.md index 75ca738b..77bd68be 100644 --- a/docs/ios-client/onboarding.md +++ b/docs/ios-client/onboarding.md @@ -9,8 +9,8 @@ As of Soundscape version 5.3.1 (October 2022): * macOS 12.6.1 * Xcode 13.4.1 * iOS 14.1 -* CocoaPods 1.11.3 -* CocoaPods Patch 1.0.2 +* CocoaPods 1.11.3 (since removed) +* CocoaPods Patch 1.0.2 (since removed) ## Install Xcode @@ -31,9 +31,7 @@ xcode-select --install _Note:_ while macOS comes with a version of Ruby installed, you should install and use a non-system [Ruby](https://www.ruby-lang.org/) using a version manager like [RVM](https://rvm.io/) -## Install CocoaPods and CocoaPods-Patch - -Soundscape uses [CocoaPods](https://cocoapods.org/) as a dependency managers along with [Swift Package Manager](https://www.swift.org/package-manager/), and [CocoaPods-Patch](https://github.com/DoubleSymmetry/cocoapods-patch) to add changes to a third party CocoaPods framework. +## Install Fastlane In the iOS project folder, run the following command to install the dependencies from the `Gemfile`: @@ -41,14 +39,6 @@ In the iOS project folder, run the following command to install the dependencies bundle install ``` -## Install CocoaPods Dependencies - -Install the CocoaPods dependencies by running the following command in Terminal from the iOS project folder: - -```sh -pod install -``` - ## Opening the Project At this point, you can open the `GuideDogs.xcworkspace` file, which is the main entry point to the Xcode project. From c4fa5bb0b2ee2c3e60e7c48899a643f2dc1e8756 Mon Sep 17 00:00:00 2001 From: 2kai2kai2 <2kai2kai2@gmail.com> Date: Tue, 17 Oct 2023 16:17:39 -0400 Subject: [PATCH 11/14] Basic AuthoredActivityContentTest --- apps/ios/GuideDogs.xcodeproj/project.pbxproj | 12 +++ .../Helpers/GDAStateMachine/GDAStateMachine.m | 10 +- .../AuthoredActivityContent.swift | 2 + .../AuthoredActivityContentTest.swift | 100 ++++++++++++++++++ 4 files changed, 119 insertions(+), 5 deletions(-) create mode 100644 apps/ios/UnitTests/Data/Authored Activities/AuthoredActivityContentTest.swift diff --git a/apps/ios/GuideDogs.xcodeproj/project.pbxproj b/apps/ios/GuideDogs.xcodeproj/project.pbxproj index c269105f..eee43114 100644 --- a/apps/ios/GuideDogs.xcodeproj/project.pbxproj +++ b/apps/ios/GuideDogs.xcodeproj/project.pbxproj @@ -665,6 +665,7 @@ 91172A732AD8D56D00E6E8E9 /* CoreGPX in Frameworks */ = {isa = PBXBuildFile; productRef = 91172A722AD8D56D00E6E8E9 /* CoreGPX */; }; 914BAAF32AD745E400CB2171 /* DestinationManagerTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 914BAAF22AD745E400CB2171 /* DestinationManagerTest.swift */; }; 914BAAFD2AD7483300CB2171 /* AudioEngineTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 914BAAFC2AD7483300CB2171 /* AudioEngineTest.swift */; }; + 915FF9F62ADE4BAF002B3690 /* AuthoredActivityContentTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 915FF9F42ADE3F91002B3690 /* AuthoredActivityContentTest.swift */; }; 91C82AAD2A5DCF040086D126 /* GeolocationManagerTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 91C82AAC2A5DCF040086D126 /* GeolocationManagerTest.swift */; }; 91C82ABE2A6B08500086D126 /* RouteGuidanceTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 91C82ABD2A6B08500086D126 /* RouteGuidanceTest.swift */; }; 91DC0CF92A46134600244CC8 /* GeometryUtilsTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 914DEBDC2A3CE901007B161C /* GeometryUtilsTest.swift */; }; @@ -1582,6 +1583,7 @@ 914BAAFC2AD7483300CB2171 /* AudioEngineTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AudioEngineTest.swift; sourceTree = ""; }; 914DEBCD2A3CE6B9007B161C /* UnitTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = UnitTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 914DEBDC2A3CE901007B161C /* GeometryUtilsTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeometryUtilsTest.swift; sourceTree = ""; }; + 915FF9F42ADE3F91002B3690 /* AuthoredActivityContentTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AuthoredActivityContentTest.swift; sourceTree = ""; }; 91C82AAC2A5DCF040086D126 /* GeolocationManagerTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = " GeolocationManagerTest.swift"; path = "UnitTests/Sensors/Geolocation/Geolocation Manager/ GeolocationManagerTest.swift"; sourceTree = SOURCE_ROOT; }; 91C82ABD2A6B08500086D126 /* RouteGuidanceTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = RouteGuidanceTest.swift; path = "UnitTests/Behaviors/Route Guidance/RouteGuidanceTest.swift"; sourceTree = SOURCE_ROOT; }; B90C27D51EAF81D600007368 /* Sound.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Sound.swift; path = Code/Audio/Protocols/Sound.swift; sourceTree = ""; }; @@ -4319,6 +4321,14 @@ path = Helpers; sourceTree = ""; }; + 915FF9F32ADE3F91002B3690 /* Authored Activities */ = { + isa = PBXGroup; + children = ( + 915FF9F42ADE3F91002B3690 /* AuthoredActivityContentTest.swift */, + ); + path = "Authored Activities"; + sourceTree = ""; + }; 91C82AA62A4F56A70086D126 /* Sensors */ = { isa = PBXGroup; children = ( @@ -4346,6 +4356,7 @@ 91C82AB52A67182E0086D126 /* Data */ = { isa = PBXGroup; children = ( + 915FF9F32ADE3F91002B3690 /* Authored Activities */, 914BAAF12AD745E400CB2171 /* Destination Manager */, ); path = Data; @@ -5526,6 +5537,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 915FF9F62ADE4BAF002B3690 /* AuthoredActivityContentTest.swift in Sources */, 914BAAFD2AD7483300CB2171 /* AudioEngineTest.swift in Sources */, 914BAAF32AD745E400CB2171 /* DestinationManagerTest.swift in Sources */, 91C82ABE2A6B08500086D126 /* RouteGuidanceTest.swift in Sources */, diff --git a/apps/ios/GuideDogs/Code/Behaviors/Helpers/GDAStateMachine/GDAStateMachine.m b/apps/ios/GuideDogs/Code/Behaviors/Helpers/GDAStateMachine/GDAStateMachine.m index 48d7604f..31bd2ddd 100644 --- a/apps/ios/GuideDogs/Code/Behaviors/Helpers/GDAStateMachine/GDAStateMachine.m +++ b/apps/ios/GuideDogs/Code/Behaviors/Helpers/GDAStateMachine/GDAStateMachine.m @@ -427,22 +427,22 @@ - (void)transitionToState:(GDAStateMachineState *)state // Schedule timeout processing. dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(timeout * NSEC_PER_SEC)), dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ // Lock. - pthread_mutex_lock(&_mutex); + pthread_mutex_lock(&self->_mutex); // If the operation timed out, change to the default state. - BOOL timedout = _stateNumber == stateNumber; + BOOL timedout = self->_stateNumber == stateNumber; if (timedout) { // Log. - Log(@"%@: State '%@' timed out after %.0f seconds. Transition to state '%@'", _name, [state name], timeout, [_timeoutState name]); + Log(@"%@: State '%@' timed out after %.0f seconds. Transition to state '%@'", self->_name, [state name], timeout, [self->_timeoutState name]); // Transition to the timeout state. - [self transitionToState:_timeoutState + [self transitionToState:self->_timeoutState object:[state name]]; } // Unlock. - pthread_mutex_unlock(&_mutex); + pthread_mutex_unlock(&self->_mutex); // If the operation timed out, notify the delegate. if (timedout && [[self delegate] respondsToSelector:@selector(stateMachine:timedOutWithState:)]) diff --git a/apps/ios/GuideDogs/Code/Data/Authored Activities/AuthoredActivityContent.swift b/apps/ios/GuideDogs/Code/Data/Authored Activities/AuthoredActivityContent.swift index fed92634..7b3aed0e 100644 --- a/apps/ios/GuideDogs/Code/Data/Authored Activities/AuthoredActivityContent.swift +++ b/apps/ios/GuideDogs/Code/Data/Authored Activities/AuthoredActivityContent.swift @@ -213,6 +213,8 @@ extension AuthoredActivityContent { // Parse the waypoints and POIs based on the file version switch ext.version ?? "1" { case "1": + // Version 1 just uses all the top-level waypoints `` defined in the GPX, in order + let wpts: [ActivityWaypoint] = waypoints(from: gpx.waypoints) // For waypoints in this experience, require names, descriptions, and street addresses diff --git a/apps/ios/UnitTests/Data/Authored Activities/AuthoredActivityContentTest.swift b/apps/ios/UnitTests/Data/Authored Activities/AuthoredActivityContentTest.swift new file mode 100644 index 00000000..c3df9275 --- /dev/null +++ b/apps/ios/UnitTests/Data/Authored Activities/AuthoredActivityContentTest.swift @@ -0,0 +1,100 @@ +// +// AuthoredActivityContentTest.swift +// UnitTests +// +// Created by Kai on 10/17/23. +// Copyright © 2023 Soundscape community. All rights reserved. +// + +import XCTest +import CoreLocation +import CoreGPX +@testable import Soundscape + +final class AuthoredActivityContentTest: XCTestCase { + + // MARK: Test GPX Parsing + + /// Tests parsing from GPX + /// Using `GPXSoundscapeSharedContentExtensions` v1 + /// And minimal other details + func testParseGPXContentV1_00() throws { + let text = """ + + + required name123 + required description456 + + required author's name!! + + + + + + + activity_id1234 + + scavengerhunt + + 1 + en_US + + + + + + + + + first waypoint + waypoint0 + + + second waypoint + waypoint1 + + +""" + guard let parser = GPXParser(withRawString: text) else { + XCTFail("Failed to initialize GPXParser") + return + } + guard let root = parser.parsedData() else { + XCTFail("Failed to get parsedData") + return + } + guard let activity = AuthoredActivityContent.parse(gpx: root) else { + XCTFail("Failed to create AuthoredActivityContent from GPXRoot") + return + } + + XCTAssertEqual(activity.id, "activity_id1234") // gpxsc:id + XCTAssertEqual(activity.type, AuthoredActivityType.orienteering) + XCTAssertEqual(activity.name, "required name123") + XCTAssertEqual(activity.creator, "required author's name!!") + XCTAssertEqual(activity.locale.identifier, "en_US") + // TODO: availability???? expiration?? image?? + XCTAssertNotNil(activity.availability) + XCTAssertFalse(activity.expires) + + // Note `activity.desc` and `activity.description` are different + // desc comes from the gpxsc:desc tag, whereas description is a generated text description + XCTAssertEqual(activity.desc, "required description456") + + XCTAssertEqual(activity.waypoints.count, 2) + if let wpt0 = activity.waypoints.first, let wpt1 = activity.waypoints.last { + XCTAssertEqual(wpt0.coordinate, CLLocationCoordinate2DMake(0, 0)) + XCTAssertEqual(wpt0.name, "first waypoint") + XCTAssertEqual(wpt0.description, "waypoint0") + XCTAssertEqual(wpt1.coordinate, CLLocationCoordinate2DMake(1, 0)) + XCTAssertEqual(wpt1.name, "second waypoint") + XCTAssertEqual(wpt1.description, "waypoint1") + // TODO: optional waypoint properties + } + + XCTAssertEqual(activity.pois.count, 0) // v1 has no POIs + } + + // TODO: test other stuff + +} From da4f0f34db3daeeef35755f36428519a0324f271 Mon Sep 17 00:00:00 2001 From: 2kai2kai2 <2kai2kai2@gmail.com> Date: Tue, 17 Oct 2023 17:28:03 -0400 Subject: [PATCH 12/14] AuthoredActivityContent v2 tests --- .../AuthoredActivityContentTest.swift | 93 +++++++++++++++++++ 1 file changed, 93 insertions(+) diff --git a/apps/ios/UnitTests/Data/Authored Activities/AuthoredActivityContentTest.swift b/apps/ios/UnitTests/Data/Authored Activities/AuthoredActivityContentTest.swift index c3df9275..d5afbb3b 100644 --- a/apps/ios/UnitTests/Data/Authored Activities/AuthoredActivityContentTest.swift +++ b/apps/ios/UnitTests/Data/Authored Activities/AuthoredActivityContentTest.swift @@ -95,6 +95,99 @@ final class AuthoredActivityContentTest: XCTestCase { XCTAssertEqual(activity.pois.count, 0) // v1 has no POIs } + /// Tests parsing from GPX + /// Using `GPXSoundscapeSharedContentExtensions` v2 + /// And minimal other details + func testParseGPXContentV2_00() throws { + let text = """ + + + required name234 + required description567 + + required author's name!! + + + + + + + activity_id5678 + + scavengerhunt + + 2 + en_US + + + + + + + + + first point + point0 + + + second point + point1 + + + + + + Cool POI + is optional + + +""" + guard let parser = GPXParser(withRawString: text) else { + XCTFail("Failed to initialize GPXParser") + return + } + guard let root = parser.parsedData() else { + XCTFail("Failed to get parsedData") + return + } + guard let activity = AuthoredActivityContent.parse(gpx: root) else { + XCTFail("Failed to create AuthoredActivityContent from GPXRoot") + return + } + + XCTAssertEqual(activity.id, "activity_id5678") // gpxsc:id + XCTAssertEqual(activity.type, AuthoredActivityType.orienteering) + XCTAssertEqual(activity.name, "required name234") + XCTAssertEqual(activity.creator, "required author's name!!") + XCTAssertEqual(activity.locale.identifier, "en_US") + // TODO: availability???? expiration?? image?? + XCTAssertNotNil(activity.availability) + XCTAssertFalse(activity.expires) + + // Note `activity.desc` and `activity.description` are different + // desc comes from the gpxsc:desc tag, whereas description is a generated text description + XCTAssertEqual(activity.desc, "required description567") + + XCTAssertEqual(activity.waypoints.count, 2) + if let wpt0 = activity.waypoints.first, let wpt1 = activity.waypoints.last { + XCTAssertEqual(wpt0.coordinate, CLLocationCoordinate2DMake(0, 0)) + XCTAssertEqual(wpt0.name, "first point") + XCTAssertEqual(wpt0.description, "point0") + XCTAssertEqual(wpt1.coordinate, CLLocationCoordinate2DMake(1, 0)) + XCTAssertEqual(wpt1.name, "second point") + XCTAssertEqual(wpt1.description, "point1") + // TODO: optional waypoint properties + } + + // POIs are the top-level waypoints + XCTAssertEqual(activity.pois.count, 1) + if let poi0 = activity.pois.first { + XCTAssertEqual(poi0.coordinate, CLLocationCoordinate2DMake(0.5, 0)) + XCTAssertEqual(poi0.name, "Cool POI") + XCTAssertEqual(poi0.description, "is optional") + } + } + // TODO: test other stuff } From 625d73a5a3284a9b7962cd71ded1cf3e052a5b37 Mon Sep 17 00:00:00 2001 From: 2kai2kai2 <2kai2kai2@gmail.com> Date: Tue, 24 Oct 2023 16:21:47 -0400 Subject: [PATCH 13/14] Improve GPX implementation stability and docs --- .../Geo Extensions/GPXExtensions.swift | 139 +++++++++++++++--- .../AuthoredActivityContent.swift | 39 +++-- docs/ios-client/onboarding.md | 6 +- 3 files changed, 146 insertions(+), 38 deletions(-) diff --git a/apps/ios/GuideDogs/Code/App/Framework Extensions/Geo Extensions/GPXExtensions.swift b/apps/ios/GuideDogs/Code/App/Framework Extensions/Geo Extensions/GPXExtensions.swift index 8a990a70..3f03995a 100644 --- a/apps/ios/GuideDogs/Code/App/Framework Extensions/Geo Extensions/GPXExtensions.swift +++ b/apps/ios/GuideDogs/Code/App/Framework Extensions/Geo Extensions/GPXExtensions.swift @@ -93,13 +93,17 @@ extension GPXRoot { extension GPXExtensionsElement { /// Sets the first child tag with the specified name, or creates a new one if it does not exist. - public func set_property(_ name: String, to value: String) { - for child in children { - if child.name == name { - child.text = value - return - } + /// If value is `nil`, removes specified tags + public func set_property(_ name: String, to value: String?) { + guard let value = value else { + children.removeAll(where: { $0.name == name }) + return + } + if let child = children.first(where: { $0.name == name }) { + child.text = value + return } + let new_element = GPXExtensionsElement(name: value) new_element.text = value children.append(new_element) @@ -107,12 +111,7 @@ extension GPXExtensionsElement { /// Gets the first child tag with the specified name, or nil if not found public func get_property(_ name: String) -> String? { - for child in children { - if child.name == name { - return child.text - } - } - return nil + return children.first(where: { $0.name == name })?.text } } @@ -171,7 +170,7 @@ func BuildGPXExtension(_ type: GPXExtensionsKeys) -> GPXExtensionsElement { // TODO: Many of the properties in the various extensions are NSNumber in the Objective-C versions. That means they could be any number type, from floating point to integer types to booleans. We should probably figure out what they're actually supposed to be. /// child tags within a `GPXTrackPointExtensions` which has tag `gpxtpx:TrackPointExtension` -/// https://www8.garmin.com/xmlschemas/TrackPointExtensionv2.xsd +/// - seealso: [](https://www8.garmin.com/xmlschemas/TrackPointExtensionv2.xsd) enum GPXTrackPointExtensionsProperties : String { case kHeartRate = "gpxtpx:hr" // unsigned int case kCadence = "gpxtpx:cad" // unsigned int @@ -197,13 +196,18 @@ extension GPXExtensionView { } } +/// - seealso: [](https://trails.io/GPX/1/0/trails_1.0.xsd) enum GPXTrailsTrackExtensionsProperties : String { case kElementActivity = "trailsio:activity" } extension GPXExtensionView { - // TODO: this + public var activityType: String? { + get { get_single(.kElementActivity) } + set {set_single(.kElementActivity, to: newValue) } + } } +/// - seealso: [](https://trails.io/GPX/1/0/trails_1.0.xsd) enum GPXTrailsTrackPointExtensionsProperties : String { case kElementHorizontalAcc = "trailsio:hacc" case kElementVerticalAcc = "trailsio:vacc" @@ -282,6 +286,8 @@ enum GPXSoundscapeSharedContentExtensionsProperties : String { case kElementVersion = "gpxsc:version" // Experience Tags case kElementLocale = "gpxsc:locale" // 'required' + // ?? + case kElementRegion = "gpxsc:region" } /// attribute keys within a `GPXSoundscapeSharedContentExtensions` which has tag `gpxsc:meta` enum GPXSoundscapeSharedContentExtensionsAttributes : String { @@ -289,6 +295,30 @@ enum GPXSoundscapeSharedContentExtensionsAttributes : String { case kAttributeEndDate = "end" case kAttributeExpires = "expires" } +class GPXSoundscapeRegion { + var latitude: CLLocationDegrees + var longitude: CLLocationDegrees + var radius: CLLocationDistance + + init?(element: GPXExtensionsElement) { + guard element.name == GPXSoundscapeSharedContentExtensionsProperties.kElementRegion.rawValue, + let lat = element.attributes["lat"], + let lat = CLLocationDegrees(lat), + let lon = element.attributes["lon"], + let lon = CLLocationDegrees(lon), + let rad = element.attributes["radius"], + let rad = CLLocationDistance(rad) else { + return nil + } + latitude = lat + longitude = lon + radius = rad + } + + var region: CLCircularRegion { + return CLCircularRegion(center: CLLocationCoordinate2D(latitude: latitude, longitude: longitude), radius: radius, identifier: "SoundscapeExperienceRegion") + } +} extension GPXExtensionView { public var id: String? { get { get_single(.kElementID) } @@ -302,7 +332,16 @@ extension GPXExtensionView { get { get_single(.kElementVersion) } set { set_single(.kElementVersion, to: newValue) } } - // TODO: apparently there may be a GPXSoundscapeRegion in here too? + /// Seems to be unused. + public var region: GPXSoundscapeRegion? { + get { + guard let element = ref?.children.first(where: {$0.name == E.kElementRegion.rawValue}) else { + return nil + } + return GPXSoundscapeRegion(element: element) + } + // TODO: make a setter + } /// If set to an invalid or unknown value, will not catch that and will save/read back that locale identifier public var locale: Locale? { get { @@ -315,7 +354,7 @@ extension GPXExtensionView { } /// This is the correct date formatter for the GPX format private static let dateFormatter = ISO8601DateFormatter() - /// If nil, then start date is in the distant past + /// If `nil`, then start date is in the distant past public var startDate: Date? { get { guard let startStr = ref?.attributes[GPXSoundscapeSharedContentExtensionsAttributes.kAttributeStartDate.rawValue], @@ -332,7 +371,7 @@ extension GPXExtensionView { ref?.attributes[GPXSoundscapeSharedContentExtensionsAttributes.kAttributeStartDate.rawValue] = value } } - /// If nil, then end date in the distant future + /// If `nil`, then end date is in the distant future public var endDate: Date? { get { guard let endStr = ref?.attributes[GPXSoundscapeSharedContentExtensionsAttributes.kAttributeEndDate.rawValue], @@ -379,6 +418,13 @@ enum GPXSoundscapeAnnotationAttributes : String { case kAttributeTitle = "title" case kAttributeType = "type" } + +/// ```xml +/// From the following GPX tags: +/// +/// CONTENT HERE [0..N] +/// +/// ``` class GPXAnnotation { // TODO: make this work better with the GPX system var title: String? @@ -395,11 +441,18 @@ class GPXAnnotation { extension GPXExtensionView { public var annotations: [GPXAnnotation] { get { - // We store stuff as GPXSoundscapeLink which is an empty subclass of GPXLink return ref?.children.filter({$0.name == E.kAnnotation.rawValue}).compactMap( GPXAnnotation.init ) ?? [] } // TODO: a setter maybe? } + + /// Parses and returns the first `gpxsc:annotation` child found with the specified annotation type + public func getFirstAnnotation(withType type: String) -> GPXAnnotation? { + guard let element = ref?.children.first(where: {$0.name == E.kAnnotation.rawValue && $0.attributes[GPXSoundscapeAnnotationAttributes.kAttributeType.rawValue] == type }) else { + return nil + } + return GPXAnnotation(element: element) + } } /// child tags within a `GPXSoundscapeAnnotationExtensions` which has tag `gpxsc:annotations` @@ -426,14 +479,56 @@ extension GPXExtensionView { enum GPXSoundscapePOIExtensionsProperties : String { case kElementStreetAddress = "gpxsc:street" case kElementPhone = "gpxsc:phone" - case kElementHomepage = "gpxsc:link" // this is the same type as `GPXSoundscapeLink` + case kElementHomepage = "link" // I think this is a normal link, and lacks a "gpxsc:" +} +extension GPXExtensionView { + public var street: String? { + get { get_single(.kElementStreetAddress) } + set { set_single(.kElementStreetAddress, to: newValue) } + } + public var phone: String? { + get { get_single(.kElementPhone) } + set { set_single(.kElementPhone, to: newValue) } + } + public var homepage: GPXLink? { + get { + guard let element = ref?.children.first(where: { $0.name == E.kElementHomepage.rawValue }) else { + return nil + } + let link = GPXLink() + link.mimetype = element.get_property("type") + link.text = element.get_property("name") + link.href = element.attributes["href"] + return link + } + set { + // if set to nil, remove all links + guard let newValue = newValue else { + ref?.children.removeAll(where: { $0.name == E.kElementHomepage.rawValue }) + return + } + guard let element = ref?.children.first(where: { $0.name == E.kElementHomepage.rawValue }) else{ + // If there is no existing link element, create a new one + let element = GPXExtensionsElement(name: E.kElementHomepage.rawValue) + element.attributes["href"] = newValue.href + element.set_property("type", to: newValue.mimetype) + element.set_property("name", to: newValue.text) + ref?.children.append(element) + return + } + // otherwise there is an existing one: overwrite it + element.attributes["href"] = newValue.href + element.set_property("type", to: newValue.mimetype) + element.set_property("name", to: newValue.text) + } + } } /// CoreGPX seems to prefer to leave things just as `GPXExtensionsElement`s so we'll just use that -/// But, for ease of use add ``ExtensionWrapper`` to allow easy lookup of named tags +/// But, for ease of use add ``GPXExtensionView`` to allow easy lookup of named tags extension GPXExtensions { /// A getter specifically for our extensions - func get_ext(_ name: GPXExtensionsKeys) -> GPXExtensionsElement? { + private func get_ext(_ name: GPXExtensionsKeys) -> GPXExtensionsElement? { return children.first { $0.name == name.rawValue } } @@ -503,8 +598,6 @@ extension GPXExtensions { } return GPXExtensionView(ext) } - - // GPXSoundscapeRegion ???????? } // MARK: End Implementing Custom GPX Extensions diff --git a/apps/ios/GuideDogs/Code/Data/Authored Activities/AuthoredActivityContent.swift b/apps/ios/GuideDogs/Code/Data/Authored Activities/AuthoredActivityContent.swift index 7b3aed0e..e288a194 100644 --- a/apps/ios/GuideDogs/Code/Data/Authored Activities/AuthoredActivityContent.swift +++ b/apps/ios/GuideDogs/Code/Data/Authored Activities/AuthoredActivityContent.swift @@ -235,10 +235,14 @@ extension AuthoredActivityContent { pois: []) case "2": - guard let route = gpx.routes.first, route.points.count > 0 else { + // Version 2 uses the first route ``, taking the contained route points as its waypoints + // It then uses the top-level GPX waypoints `` as POIs + + guard let route = gpx.routes.first, !route.points.isEmpty else { return nil } + // Waypoints are strict about requiring names and locations let wpts: [ActivityWaypoint] = waypoints(from: route.points) // For waypoints in this experience, require names, descriptions, and street addresses @@ -246,8 +250,14 @@ extension AuthoredActivityContent { return nil } - // TODO: maybe don't use ! - let pois = gpx.waypoints.map { ActivityPOI(coordinate: $0.coordinate!, name: $0.name!, description: $0.desc) } + let pois: [ActivityPOI] = gpx.waypoints.compactMap { + guard let coord = $0.coordinate else { + // skip waypoints without a location (why does GPX allow this waypoints to lack a location?) + // TODO: maybe log a warning + return nil + } + return ActivityPOI(coordinate: coord, name: $0.name ?? "Unlabeled POI", description: $0.desc) + } return AuthoredActivityContent(id: id, type: actType, @@ -274,13 +284,13 @@ extension AuthoredActivityContent { let imageMimeTypes = Set(["image/jpeg", "image/jpg", "image/png"]) let audioMimeTypes = Set(["audio/mpeg", "audio/x-m4a"]) - return waypoints.map { wpt in - let links = wpt.extensions?.soundscapeLinkExtensions?.links.filter({ + return waypoints.compactMap { wpt in + let links: [GPXLink] = wpt.extensions?.soundscapeLinkExtensions?.links.filter({ guard let mimetype = $0.mimetype else { return false } return imageMimeTypes.contains(mimetype) }) ?? [] - let parsedImages = links.compactMap { (link) -> ActivityWaypointImage? in + let parsedImages: [ActivityWaypointImage] = links.compactMap { link in guard let href = link.href, let url = URL(string: href) else { return nil @@ -289,7 +299,7 @@ extension AuthoredActivityContent { return ActivityWaypointImage(url: url, altText: link.text) } - let parsedAudioClips = links.compactMap { (link) -> ActivityWaypointAudioClip? in + let parsedAudioClips: [ActivityWaypointAudioClip] = links.compactMap { link in guard let href = link.href, let url = URL(string: href) else { return nil @@ -298,11 +308,18 @@ extension AuthoredActivityContent { return ActivityWaypointAudioClip(url: url, description: link.text) } - let allAnnotations = wpt.extensions?.soundscapeAnnotationExtensions?.annotations - let departure = allAnnotations?.first(where: { $0.type == "departure" })?.content - let arrival = allAnnotations?.first(where: { $0.type == "arrival" })?.content + let annotations = wpt.extensions?.soundscapeAnnotationExtensions + let departure = annotations?.getFirstAnnotation(withType: "departure")?.content + let arrival = annotations?.getFirstAnnotation(withType: "arrival")?.content + + // Coordinate shouldn't be nil, but CoreGPX allows it to be so. + // For now we just skip those points + // TODO: there's probably a better way to enforce this + guard let coordinate = wpt.coordinate else { + return nil + } - return ActivityWaypoint(coordinate: wpt.coordinate!, // TODO: maybe shouldn't be ! + return ActivityWaypoint(coordinate: coordinate, name: wpt.name, description: wpt.desc, departureCallout: departure, diff --git a/docs/ios-client/onboarding.md b/docs/ios-client/onboarding.md index 77bd68be..5a2229a3 100644 --- a/docs/ios-client/onboarding.md +++ b/docs/ios-client/onboarding.md @@ -4,13 +4,11 @@ This document describes how to build and run the Soundscape iOS app. ## Supported Tooling Versions -As of Soundscape version 5.3.1 (October 2022): +As of Soundscape Community version 1.0.1 (October 2023): * macOS 12.6.1 * Xcode 13.4.1 * iOS 14.1 -* CocoaPods 1.11.3 (since removed) -* CocoaPods Patch 1.0.2 (since removed) ## Install Xcode @@ -31,7 +29,7 @@ xcode-select --install _Note:_ while macOS comes with a version of Ruby installed, you should install and use a non-system [Ruby](https://www.ruby-lang.org/) using a version manager like [RVM](https://rvm.io/) -## Install Fastlane +## Install Fastlane (optional) In the iOS project folder, run the following command to install the dependencies from the `Gemfile`: From af9c0289a9ff0f70ab1f18ef8ac8c5771235d0c3 Mon Sep 17 00:00:00 2001 From: 2kai2kai2 <2kai2kai2@gmail.com> Date: Fri, 27 Oct 2023 15:08:15 -0400 Subject: [PATCH 14/14] Additional Documentation --- .github/workflows/ios-tests.yml | 1 - .../Authored Activities/AuthoredActivityContent.swift | 11 +++++------ docs/Client.md | 4 ++-- docs/ios-client/onboarding.md | 9 ++++----- 4 files changed, 11 insertions(+), 14 deletions(-) diff --git a/.github/workflows/ios-tests.yml b/.github/workflows/ios-tests.yml index 51148bab..04a5cca1 100644 --- a/.github/workflows/ios-tests.yml +++ b/.github/workflows/ios-tests.yml @@ -11,7 +11,6 @@ jobs: runs-on: macos-latest steps: - uses: actions/checkout@v3 - - run: cd apps/ios && bundle install # may be able to skip this if we don't need fastlane - name: Build run: xcodebuild build-for-testing -workspace apps/ios/GuideDogs.xcworkspace -scheme Soundscape -destination 'platform=iOS Simulator,name=iPhone 13' - name: Test diff --git a/apps/ios/GuideDogs/Code/Data/Authored Activities/AuthoredActivityContent.swift b/apps/ios/GuideDogs/Code/Data/Authored Activities/AuthoredActivityContent.swift index e288a194..d829cb01 100644 --- a/apps/ios/GuideDogs/Code/Data/Authored Activities/AuthoredActivityContent.swift +++ b/apps/ios/GuideDogs/Code/Data/Authored Activities/AuthoredActivityContent.swift @@ -177,9 +177,8 @@ extension AuthoredActivityContent { /// Parses a custom GPX file with the data for an adaptive sports event. /// - /// - Parameter url: URL for downloading the GPX data - /// - Throws: SharedContentError if the file cannot be parsed (or required data is missing) - /// - Returns: An AdaptiveSportsEvent struct + /// - Parameter gpx: A parsed GPX file + /// - Returns: An ``AuthoredActivityContent``, or `nil` if parsing failed or required properties were missing. Currently, waypoints or POIs may be skipped if they lack coordinate data. static func parse(gpx: GPXRoot) -> AuthoredActivityContent? { guard let metadata = gpx.metadata else { return nil @@ -276,10 +275,10 @@ extension AuthoredActivityContent { } } - /// Parses a list of GPXWaypoints into POIWaypoints and AnnotationWaypoints. + /// Parses a list of ``GPXWaypoint``s into ``ActivityWaypoint``s /// - /// - Parameter waypoints: an array of GPXWaypoints - /// - Returns: an array of POIWaypoints and an array of AnnotationWaypoints + /// - Parameter waypoints: an array of ``GPXWaypoint``s + /// - Returns: an array of ``ActivityWaypoint``s including annotation data (if applicable) private static func waypoints(from waypoints: [GPXWaypoint]) -> [ActivityWaypoint] { let imageMimeTypes = Set(["image/jpeg", "image/jpg", "image/png"]) let audioMimeTypes = Set(["audio/mpeg", "audio/x-m4a"]) diff --git a/docs/Client.md b/docs/Client.md index afcb8adc..fff9ed58 100644 --- a/docs/Client.md +++ b/docs/Client.md @@ -3,8 +3,8 @@ The Xcode project can be found in [this folder](../apps/ios/). Instructions on how to setup your development environment, build and -run the Soundscape iOS app can be found in the iOS client [onboarding -documentation](./ios-client/onboarding.md). +run the Soundscape iOS app can be found in the iOS client +[onboarding documentation](./ios-client/onboarding.md). Additional useful instructions for development can be found in the [iOS client documents Folder](./ios-client). diff --git a/docs/ios-client/onboarding.md b/docs/ios-client/onboarding.md index 5a2229a3..d57874c5 100644 --- a/docs/ios-client/onboarding.md +++ b/docs/ios-client/onboarding.md @@ -24,14 +24,13 @@ Open Xcode and you should be prompted with installing the command line tools, or xcode-select --install ``` -## Install Ruby +## Install Fastlane (optional) -_Note:_ while macOS comes with a version of Ruby installed, you should install and use a non-system [Ruby](https://www.ruby-lang.org/) -using a version manager like [RVM](https://rvm.io/) +Installing Fastlane requires a [Ruby](https://www.ruby-lang.org/) installation. -## Install Fastlane (optional) +> __Note:__ while macOS comes with a version of Ruby installed, you should install and use a non-system [Ruby](https://www.ruby-lang.org/) using a version manager like [RVM](https://rvm.io/) -In the iOS project folder, run the following command to install the dependencies from the `Gemfile`: +In the iOS project folder `apps/ios`, run the following command to install the dependencies from the `Gemfile`: ```sh bundle install