From 080dd56170c17285e92d7b40e16cb57fc101478a Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Tue, 16 Apr 2019 14:51:36 -0500 Subject: [PATCH 001/147] Add stub classes for ArcGISARView and ArcGISARSensorView --- .../ArcGISToolkit.xcodeproj/project.pbxproj | 16 +++++ .../ArcGISToolkit/AR/ArcGISARSensorView.swift | 58 +++++++++++++++ Toolkit/ArcGISToolkit/AR/ArcGISARView.swift | 71 +++++++++++++++++++ 3 files changed, 145 insertions(+) create mode 100644 Toolkit/ArcGISToolkit/AR/ArcGISARSensorView.swift create mode 100644 Toolkit/ArcGISToolkit/AR/ArcGISARView.swift diff --git a/Toolkit/ArcGISToolkit.xcodeproj/project.pbxproj b/Toolkit/ArcGISToolkit.xcodeproj/project.pbxproj index 71b64e03..07dca98a 100644 --- a/Toolkit/ArcGISToolkit.xcodeproj/project.pbxproj +++ b/Toolkit/ArcGISToolkit.xcodeproj/project.pbxproj @@ -22,6 +22,8 @@ 88DBC29F1FE83D4400255921 /* JobManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88DBC29E1FE83D4400255921 /* JobManager.swift */; }; 88DBC2A31FE83DB800255921 /* CancelGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88DBC2A21FE83DB800255921 /* CancelGroup.swift */; }; 88ECCC931DF92F22000C967E /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 88ECCC921DF92F22000C967E /* Assets.xcassets */; }; + E447A1262266629600578C0B /* ArcGISARView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E447A1252266629600578C0B /* ArcGISARView.swift */; }; + E447A1282266630400578C0B /* ArcGISARSensorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E447A1272266630400578C0B /* ArcGISARSensorView.swift */; }; E46893291FEDAE36008ADA79 /* Compass.swift in Sources */ = {isa = PBXBuildFile; fileRef = E46893281FEDAE36008ADA79 /* Compass.swift */; }; E48405731E9BE7B700927208 /* LegendViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E48405721E9BE7B700927208 /* LegendViewController.swift */; }; E484057A1E9C262D00927208 /* Legend.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = E48405791E9C262D00927208 /* Legend.storyboard */; }; @@ -45,6 +47,8 @@ 88DBC29E1FE83D4400255921 /* JobManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JobManager.swift; sourceTree = ""; }; 88DBC2A21FE83DB800255921 /* CancelGroup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CancelGroup.swift; sourceTree = ""; }; 88ECCC921DF92F22000C967E /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = ArcGISToolkit/Assets.xcassets; sourceTree = ""; }; + E447A1252266629600578C0B /* ArcGISARView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArcGISARView.swift; sourceTree = ""; }; + E447A1272266630400578C0B /* ArcGISARSensorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArcGISARSensorView.swift; sourceTree = ""; }; E46893281FEDAE36008ADA79 /* Compass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Compass.swift; sourceTree = ""; }; E48405721E9BE7B700927208 /* LegendViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LegendViewController.swift; sourceTree = ""; }; E48405791E9C262D00927208 /* Legend.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = Legend.storyboard; sourceTree = ""; }; @@ -116,6 +120,7 @@ 88B689EE1E96EF3300B67FAB /* Components */ = { isa = PBXGroup; children = ( + E447A1242266628300578C0B /* AR */, E46893281FEDAE36008ADA79 /* Compass.swift */, E48405721E9BE7B700927208 /* LegendViewController.swift */, 88B689F41E96EFD700B67FAB /* MeasureToolbar.swift */, @@ -128,6 +133,15 @@ name = Components; sourceTree = ""; }; + E447A1242266628300578C0B /* AR */ = { + isa = PBXGroup; + children = ( + E447A1252266629600578C0B /* ArcGISARView.swift */, + E447A1272266630400578C0B /* ArcGISARSensorView.swift */, + ); + path = AR; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXHeadersBuildPhase section */ @@ -215,6 +229,8 @@ 88B68A041E96EFD700B67FAB /* Scalebar.swift in Sources */, 88B68A081E96EFD700B67FAB /* UnitsViewController.swift in Sources */, E48405731E9BE7B700927208 /* LegendViewController.swift in Sources */, + E447A1282266630400578C0B /* ArcGISARSensorView.swift in Sources */, + E447A1262266629600578C0B /* ArcGISARView.swift in Sources */, 883EA74F20741B9C006D6F72 /* TemplatePickerViewController.swift in Sources */, 883EA74920741A4C006D6F72 /* PopupController.swift in Sources */, 88B68A011E96EFD700B67FAB /* MeasureToolbar.swift in Sources */, diff --git a/Toolkit/ArcGISToolkit/AR/ArcGISARSensorView.swift b/Toolkit/ArcGISToolkit/AR/ArcGISARSensorView.swift new file mode 100644 index 00000000..0f8b5a04 --- /dev/null +++ b/Toolkit/ArcGISToolkit/AR/ArcGISARSensorView.swift @@ -0,0 +1,58 @@ +// +// Copyright 2019 Esri. + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 + +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import UIKit +import ArcGIS + +public enum LocationType { + case anglesOnly + case positionOnly + case anglesAndPosition +} + +class ArcGISARSensorView: UIView { + + public var locationType: LocationType = .anglesAndPosition + + public var sceneView: AGSSceneView? + + public var useAbsoluteHeading: Bool = true + + override init(frame: CGRect) { + super.init(frame: frame) + sharedInitialization() + } + + required public init?(coder aDecoder: NSCoder) { + super.init(coder: aDecoder) + sharedInitialization() + } + + required public init(renderVideoFeed: Bool){ + super.init(frame: CGRect.zero) + sharedInitialization() + } + + private func sharedInitialization(){ + + } + + public func startTracking() { + + } + + public func stopTracking() { + + } +} diff --git a/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift b/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift new file mode 100644 index 00000000..aea49805 --- /dev/null +++ b/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift @@ -0,0 +1,71 @@ +// +// Copyright 2019 Esri. + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 + +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import UIKit +import ARKit +import ArcGIS + +class ArcGISARView: UIView { + + public private(set) var session: ARSession? + + public private(set) var sceneView: AGSSceneView? + + public var originCamera: AGSCamera? + + public var translationTransformationFactor: Double = 1.0 + + override init(frame: CGRect) { + super.init(frame: frame) + sharedInitialization() + } + + required public init?(coder aDecoder: NSCoder) { + super.init(coder: aDecoder) + sharedInitialization() + } + + required public init(renderVideoFeed: Bool){ + super.init(frame: CGRect.zero) + sharedInitialization() + } + + private func sharedInitialization(){ + + } + + public func arScreenToLocation(screenPoint: AGSPoint) -> AGSPoint { + return AGSPoint(x: 0.0, y: 0.0, spatialReference: nil) + } + + public func resetTracking() { + + } + + public func resetUsingLocationServices() -> Bool { + return false + } + + public func resetUsingSpatialAnchor() -> Bool { + return false + } + + public func startTracking() { + + } + + public func stopTracking() { + + } +} From 2d41a441bb19b011d998d7b46585776f5840bffc Mon Sep 17 00:00:00 2001 From: alpascual Date: Wed, 17 Apr 2019 16:51:28 -0700 Subject: [PATCH 002/147] adding support for carthage --- .travis.yml | 21 +++++++++++++++++++++ README.md | 4 ++++ 2 files changed, 25 insertions(+) create mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 00000000..3a86ff9f --- /dev/null +++ b/.travis.yml @@ -0,0 +1,21 @@ +language: objective-c +osx_image: xcode10.2 +xcode_project: Toolkit/ArcGISToolkit.xcodeproj +xcode_scheme: ArcGISToolkit +xcode_sdk: iphonesimulator11.0 +env: + global: + - FRAMEWORK_NAME=ArcGISToolkit +before_install: +- brew update +- brew outdated carthage || brew upgrade carthage +before_script: +- carthage bootstrap +before_deploy: +- carthage build --no-skip-current +- carthage archive $FRAMEWORK_NAME +deploy: + provider: releases + api_key: + secure: WJsVpdw2uEi8t4nLWeTKvX28peKyO0GryObV59DDFDmhzMzzGHVcS/umQTfA0X8KD6/gTl66TsmOh61Z6s1sEYSfHD0nK4ug4Da753kwZsL8wGGDbmGOAf2hDtSik3GcMAbPVVRc3tDP8j6HKTd71xYr4rUgM+oFRPzy0SSeXOt61K+kxJuZea8qrFpIBHD/9T75c+m6enP3pRs+AqfvEIbOs1lMs/+IYvSRIlXBYd5PBKMUBCo911VukUGvY0iwbXhgDgXImTkw0izqMYEKTGFS5YYK3hn3cP9066Wa4Zyl0l6hnCyT/+HL9cHW8i1+D/5RXjjq0RSWsIJ0aD/2nURLFu2b+zAKa2fyUB5oVzmGpoLxE98YNoUQLdF5HL4wH5fnjAbp0QVMCzSgWaF1+CfA/ghGiXog4je6+jduxOq2//nR4nkHar9VREyFKH4nf8svExDJQbOGs082VKWYJXMQLNaty+cg99F+5aT0wz7s/cszIumi+glgG6A/fBRhsi763a5/PxFp+CbRttdh97zF2uDcPtVILOLi6SaJHdM1XTuaCO5l9dbMJ6uUVKJMH3D7Fd28WM7GAx+7ds3JZcWr4px0/S12gy71GcjdumVxrF41swD69eJ6W4NBwwAJamEa7ykYVtHZl2D+6hZRqsOoeP8wJIXQLzGXwrBI7XE= + file: ArcGISToolkit.framework diff --git a/README.md b/README.md index bc4c5482..03edb5ab 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,10 @@ The *ArcGIS Runtime Toolkit for iOS* has a *Target SDK* version of *11.0*, meani New to cocoapods? Visit [cocoapods.org](https://cocoapods.org/) +### Carthage + +[![Carthage compatible](https://img.shields.io/badge/Carthage-compatible-4BC51D.svg?style=flat)](https://github.com/Carthage/Carthage) + ### Manual 1. Ensure you have downloaded and installed __ArcGIS Runtime SDK for iOS__ as described [here](https://developers.arcgis.com/ios/latest/swift/guide/install.htm#ESRI_SECTION1_D57435A2BEBC4D29AFA3A4CAA722506A) 2. Clone or download this repo. From 2a96ec9e46a8a7542ef9e591af7acab5951fda3f Mon Sep 17 00:00:00 2001 From: alpascual Date: Wed, 17 Apr 2019 16:55:49 -0700 Subject: [PATCH 003/147] updated README file --- README.md | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 03edb5ab..99966851 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,14 @@ The *ArcGIS Runtime Toolkit for iOS* has a *Target SDK* version of *11.0*, meani New to cocoapods? Visit [cocoapods.org](https://cocoapods.org/) -### Carthage +### [Carthage](https://github.com/Carthage/Carthage) + +- Add the following to your Cartfile: `github "Esri/arcgis-runtime-toolkit-ios"` +- Then run `carthage update` +- Follow the current instructions in [Carthage's README][carthage-installation] +for up to date installation instructions. + +[carthage-installation]: https://github.com/Carthage/Carthage#adding-frameworks-to-an-application [![Carthage compatible](https://img.shields.io/badge/Carthage-compatible-4BC51D.svg?style=flat)](https://github.com/Carthage/Carthage) From ea37646e9d748cb6572078ab581dcc5dd57ede8e Mon Sep 17 00:00:00 2001 From: alpascual Date: Thu, 18 Apr 2019 09:08:32 -0700 Subject: [PATCH 004/147] changes to the README file --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 99966851..0173c7a1 100644 --- a/README.md +++ b/README.md @@ -30,10 +30,10 @@ The *ArcGIS Runtime Toolkit for iOS* has a *Target SDK* version of *11.0*, meani ### [Carthage](https://github.com/Carthage/Carthage) +Carthage is a decentralized dependency manager that builds your dependencies and provides you with binary frameworks. + - Add the following to your Cartfile: `github "Esri/arcgis-runtime-toolkit-ios"` - Then run `carthage update` -- Follow the current instructions in [Carthage's README][carthage-installation] -for up to date installation instructions. [carthage-installation]: https://github.com/Carthage/Carthage#adding-frameworks-to-an-application From 26a12da80b27f6a4b87af8348266eaeecb25b1e5 Mon Sep 17 00:00:00 2001 From: alpascual Date: Thu, 18 Apr 2019 09:19:22 -0700 Subject: [PATCH 005/147] added shared data for carthage --- .../xcschemes/ArcGISToolkit.xcscheme | 80 +++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 Toolkit/ArcGISToolkit.xcodeproj/xcshareddata/xcschemes/ArcGISToolkit.xcscheme diff --git a/Toolkit/ArcGISToolkit.xcodeproj/xcshareddata/xcschemes/ArcGISToolkit.xcscheme b/Toolkit/ArcGISToolkit.xcodeproj/xcshareddata/xcschemes/ArcGISToolkit.xcscheme new file mode 100644 index 00000000..e9e79808 --- /dev/null +++ b/Toolkit/ArcGISToolkit.xcodeproj/xcshareddata/xcschemes/ArcGISToolkit.xcscheme @@ -0,0 +1,80 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From 77027e1c3e97075fac76984187a23c67545e18d7 Mon Sep 17 00:00:00 2001 From: Al Pascual Date: Thu, 18 Apr 2019 12:38:19 -0700 Subject: [PATCH 006/147] Improved Carthage instructions --- README.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/README.md b/README.md index 0173c7a1..8958aac5 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,14 @@ The *ArcGIS Runtime Toolkit for iOS* has a *Target SDK* version of *11.0*, meani Carthage is a decentralized dependency manager that builds your dependencies and provides you with binary frameworks. +If you don't have carthage installed, first run: +``` +brew update +brew install carthage +``` + +Then: + - Add the following to your Cartfile: `github "Esri/arcgis-runtime-toolkit-ios"` - Then run `carthage update` From 5b1095f0a5dfa353a9979fe6a1b6b3aa636a0625 Mon Sep 17 00:00:00 2001 From: Al Pascual Date: Fri, 19 Apr 2019 10:16:56 -0700 Subject: [PATCH 007/147] no need for a travis file --- .travis.yml | 21 --------------------- 1 file changed, 21 deletions(-) delete mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 3a86ff9f..00000000 --- a/.travis.yml +++ /dev/null @@ -1,21 +0,0 @@ -language: objective-c -osx_image: xcode10.2 -xcode_project: Toolkit/ArcGISToolkit.xcodeproj -xcode_scheme: ArcGISToolkit -xcode_sdk: iphonesimulator11.0 -env: - global: - - FRAMEWORK_NAME=ArcGISToolkit -before_install: -- brew update -- brew outdated carthage || brew upgrade carthage -before_script: -- carthage bootstrap -before_deploy: -- carthage build --no-skip-current -- carthage archive $FRAMEWORK_NAME -deploy: - provider: releases - api_key: - secure: WJsVpdw2uEi8t4nLWeTKvX28peKyO0GryObV59DDFDmhzMzzGHVcS/umQTfA0X8KD6/gTl66TsmOh61Z6s1sEYSfHD0nK4ug4Da753kwZsL8wGGDbmGOAf2hDtSik3GcMAbPVVRc3tDP8j6HKTd71xYr4rUgM+oFRPzy0SSeXOt61K+kxJuZea8qrFpIBHD/9T75c+m6enP3pRs+AqfvEIbOs1lMs/+IYvSRIlXBYd5PBKMUBCo911VukUGvY0iwbXhgDgXImTkw0izqMYEKTGFS5YYK3hn3cP9066Wa4Zyl0l6hnCyT/+HL9cHW8i1+D/5RXjjq0RSWsIJ0aD/2nURLFu2b+zAKa2fyUB5oVzmGpoLxE98YNoUQLdF5HL4wH5fnjAbp0QVMCzSgWaF1+CfA/ghGiXog4je6+jduxOq2//nR4nkHar9VREyFKH4nf8svExDJQbOGs082VKWYJXMQLNaty+cg99F+5aT0wz7s/cszIumi+glgG6A/fBRhsi763a5/PxFp+CbRttdh97zF2uDcPtVILOLi6SaJHdM1XTuaCO5l9dbMJ6uUVKJMH3D7Fd28WM7GAx+7ds3JZcWr4px0/S12gy71GcjdumVxrF41swD69eJ6W4NBwwAJamEa7ykYVtHZl2D+6hZRqsOoeP8wJIXQLzGXwrBI7XE= - file: ArcGISToolkit.framework From ee7e525c99480f9c899d940d7c9fb49443cd1bc6 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Wed, 24 Apr 2019 10:04:59 -0500 Subject: [PATCH 008/147] Add ARExamples class; add basic ARKit and Location/Motion code to ArcGISARView --- .../project.pbxproj | 4 + .../ArcGISToolkitExamples/ARExample.swift | 40 ++ .../ExamplesViewController.swift | 3 +- .../ArcGISToolkit/AR/ArcGISARSensorView.swift | 4 +- Toolkit/ArcGISToolkit/AR/ArcGISARView.swift | 422 +++++++++++++++++- 5 files changed, 460 insertions(+), 13 deletions(-) create mode 100644 Examples/ArcGISToolkitExamples/ARExample.swift diff --git a/Examples/ArcGISToolkitExamples.xcodeproj/project.pbxproj b/Examples/ArcGISToolkitExamples.xcodeproj/project.pbxproj index 5c878bc2..b16c7d07 100644 --- a/Examples/ArcGISToolkitExamples.xcodeproj/project.pbxproj +++ b/Examples/ArcGISToolkitExamples.xcodeproj/project.pbxproj @@ -23,6 +23,7 @@ 88B689CE1E96EDF400B67FAB /* ScalebarExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88B689C41E96EDF400B67FAB /* ScalebarExample.swift */; }; 88B689D11E96EDF400B67FAB /* VCListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88B689C71E96EDF400B67FAB /* VCListViewController.swift */; }; 88DBC2A11FE83D6000255921 /* JobManagerExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88DBC2A01FE83D6000255921 /* JobManagerExample.swift */; }; + E447A12B2267BB9500578C0B /* ARExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = E447A12A2267BB9500578C0B /* ARExample.swift */; }; E46893271FEDAE29008ADA79 /* CompassExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = E46893261FEDAE29008ADA79 /* CompassExample.swift */; }; E48405751E9BE7E600927208 /* LegendExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = E48405741E9BE7E600927208 /* LegendExample.swift */; }; /* End PBXBuildFile section */ @@ -83,6 +84,7 @@ 88B689C41E96EDF400B67FAB /* ScalebarExample.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ScalebarExample.swift; sourceTree = ""; }; 88B689C71E96EDF400B67FAB /* VCListViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VCListViewController.swift; sourceTree = ""; }; 88DBC2A01FE83D6000255921 /* JobManagerExample.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JobManagerExample.swift; sourceTree = ""; }; + E447A12A2267BB9500578C0B /* ARExample.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ARExample.swift; sourceTree = ""; }; E46893261FEDAE29008ADA79 /* CompassExample.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompassExample.swift; sourceTree = ""; }; E48405741E9BE7E600927208 /* LegendExample.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LegendExample.swift; sourceTree = ""; }; /* End PBXFileReference section */ @@ -152,6 +154,7 @@ 2140781D209B629000FBFDCC /* TimeSliderExample.swift */, 883EA74A20741A56006D6F72 /* PopupExample.swift */, 8800656D2228577A00F76945 /* TemplatePickerExample.swift */, + E447A12A2267BB9500578C0B /* ARExample.swift */, ); name = Examples; sourceTree = ""; @@ -279,6 +282,7 @@ 8800656E2228577A00F76945 /* TemplatePickerExample.swift in Sources */, 88B689CE1E96EDF400B67FAB /* ScalebarExample.swift in Sources */, 88B689C91E96EDF400B67FAB /* ExamplesViewController.swift in Sources */, + E447A12B2267BB9500578C0B /* ARExample.swift in Sources */, E48405751E9BE7E600927208 /* LegendExample.swift in Sources */, E46893271FEDAE29008ADA79 /* CompassExample.swift in Sources */, ); diff --git a/Examples/ArcGISToolkitExamples/ARExample.swift b/Examples/ArcGISToolkitExamples/ARExample.swift new file mode 100644 index 00000000..bf73198d --- /dev/null +++ b/Examples/ArcGISToolkitExamples/ARExample.swift @@ -0,0 +1,40 @@ +// Copyright 2019 Esri. + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 + +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import UIKit +//import ArcGISToolkit +//import ArcGIS + +open class ARExample: UIViewController { + + public let arView = ArcGISARView(frame: CGRect.zero) + + override open func viewDidLoad() { + super.viewDidLoad() + + arView.frame = view.bounds + arView.autoresizingMask = [.flexibleWidth, .flexibleHeight] + view.addSubview(arView) + + arView.sceneView.scene = AGSScene(basemapType: .streets) + } + + override open func viewDidAppear(_ animated: Bool) { + arView.startTracking() + } + + override open func viewDidDisappear(_ animated: Bool) { + arView.stopTracking() + } +} + diff --git a/Examples/ArcGISToolkitExamples/ExamplesViewController.swift b/Examples/ArcGISToolkitExamples/ExamplesViewController.swift index cd7530d6..265dc096 100644 --- a/Examples/ArcGISToolkitExamples/ExamplesViewController.swift +++ b/Examples/ArcGISToolkitExamples/ExamplesViewController.swift @@ -29,7 +29,8 @@ class ExamplesViewController: VCListViewController { ("Job Manager", JobManagerExample.self, nil), ("Time Slider", TimeSliderExample.self, nil), ("Popup Controller", PopupExample.self, nil), - ("Template Picker", TemplatePickerExample.self, nil) + ("Template Picker", TemplatePickerExample.self, nil), + ("AR", ARExample.self, nil) ] } diff --git a/Toolkit/ArcGISToolkit/AR/ArcGISARSensorView.swift b/Toolkit/ArcGISToolkit/AR/ArcGISARSensorView.swift index 0f8b5a04..cd9ee067 100644 --- a/Toolkit/ArcGISToolkit/AR/ArcGISARSensorView.swift +++ b/Toolkit/ArcGISToolkit/AR/ArcGISARSensorView.swift @@ -13,7 +13,7 @@ // limitations under the License. import UIKit -import ArcGIS +//import ArcGIS public enum LocationType { case anglesOnly @@ -21,7 +21,7 @@ public enum LocationType { case anglesAndPosition } -class ArcGISARSensorView: UIView { +public class ArcGISARSensorView: UIView { public var locationType: LocationType = .anglesAndPosition diff --git a/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift b/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift index aea49805..eab4d9b5 100644 --- a/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift +++ b/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift @@ -14,19 +14,47 @@ import UIKit import ARKit -import ArcGIS +//import ArcGIS -class ArcGISARView: UIView { +public class ArcGISARView: UIView { - public private(set) var session: ARSession? + // MARK: public properties - public private(set) var sceneView: AGSSceneView? - - public var originCamera: AGSCamera? + public lazy private(set) var arSCNView = ARSCNView(frame: .zero) + public lazy private(set) var sceneView = AGSSceneView(frame: .zero) + public var arConfiguration: ARConfiguration = ARWorldTrackingConfiguration() { + didSet { + //start tracking using the new configuration + startTracking() + } + } + public var originCamera: AGSCamera? public var translationTransformationFactor: Double = 1.0 - override init(frame: CGRect) { + // we intercept these methods first, but will use `delegate` to forward them to clients + weak open var delegate: ARSessionDelegate? + + // MARK: private properties + + private var renderVideoFeed = true + + private lazy var locationManager: CLLocationManager = { + let lm = CLLocationManager() + lm.desiredAccuracy = kCLLocationAccuracyBest + lm.delegate = self + return lm + }() + + // is ARKit supported on this device? + private var isSupported = false + + // has the client been notfiied of start/failure + private var notifiedStartOrFailure = false + + // MARK: Initializers + + public override init(frame: CGRect) { super.init(frame: frame) sharedInitialization() } @@ -36,14 +64,32 @@ class ArcGISARView: UIView { sharedInitialization() } - required public init(renderVideoFeed: Bool){ - super.init(frame: CGRect.zero) - sharedInitialization() + public convenience init(renderVideoFeed: Bool){ + self.init(frame: CGRect.zero) + self.renderVideoFeed = renderVideoFeed } private func sharedInitialization(){ + // + // ARKit initialization + isSupported = ARWorldTrackingConfiguration.isSupported + + addSubviewWithConstraints(arSCNView) + arSCNView.session.delegate = self + + // + // add sceneView to view and setup constraints + addSubviewWithConstraints(sceneView) + locationManager.delegate = self + + // + // make our sceneView's background transparent + sceneView.isBackgroundTransparent = true + sceneView.atmosphereEffect = .none } + + // MARK: Public public func arScreenToLocation(screenPoint: AGSPoint) -> AGSPoint { return AGSPoint(x: 0.0, y: 0.0, spatialReference: nil) @@ -62,10 +108,366 @@ class ArcGISARView: UIView { } public func startTracking() { + notifiedStartOrFailure = false + + if !isSupported { + didStartOrFailWithError(ArcGISARView.notSupportedError()) + return + } + // TODO: look at original beta code and grab locationmanager started stuff + if let origin = originCamera { + //set origin on sceneView??? + sceneView.setViewpointCamera(origin) + finalizeStart() + } + else { + let authStatus = CLLocationManager.authorizationStatus() + switch authStatus { + case .notDetermined: + self.startWithAccessNotDetermined() + case .restricted, .denied: + self.startWithAccessDenied() + case .authorizedAlways, .authorizedWhenInUse: + self.startWithAccessAuthorized() + } + } } public func stopTracking() { + arSCNView.session.pause() + locationManager.stopUpdatingLocation() + if CLLocationManager.headingAvailable() { + locationManager.stopUpdatingHeading() + } + } + + // MARK: Private + fileprivate func finalizeStart() { + + arSCNView.session.run(arConfiguration, options:.resetTracking) + didStartOrFailWithError(nil) + } + + fileprivate func addSubviewWithConstraints(_ subview: UIView) { + // add arSCNView to view and setup constraints + self.addSubview(subview) + subview.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([ + subview.leadingAnchor.constraint(equalTo: self.leadingAnchor), + subview.trailingAnchor.constraint(equalTo: self.trailingAnchor), + subview.topAnchor.constraint(equalTo: self.topAnchor), + subview.bottomAnchor.constraint(equalTo: self.bottomAnchor) + ]) + } + + fileprivate func startWithAccessNotDetermined() { + if (Bundle.main.object(forInfoDictionaryKey: "NSLocationWhenInUseUsageDescription") != nil) { + locationManager.requestWhenInUseAuthorization() + } + if (Bundle.main.object(forInfoDictionaryKey: "NSLocationAlwaysUsageDescription") != nil) { + locationManager.requestAlwaysAuthorization() + } + else{ + didStartOrFailWithError(ArcGISARView.missingPListKeyError()) + } + } + + fileprivate func startUpdatingLocationAndHeading() { + locationManager.startUpdatingLocation() + if CLLocationManager.headingAvailable() { + locationManager.startUpdatingHeading() + } + } + + fileprivate func startWithAccessDenied() { + didStartOrFailWithError(ArcGISARView.accessDeniedError()) + } + + fileprivate func startWithAccessAuthorized() { + startUpdatingLocationAndHeading() + } + + fileprivate func didStartOrFailWithError(_ error: Error?) { + // TODO: present error to user... + + notifiedStartOrFailure = true; + } + + fileprivate func handleAuthStatusChangedAccessDenied() { + // auth status changed to denied + if !notifiedStartOrFailure { + stopTracking() + // we were waiting for user prompt to come back, so notify + didStartOrFailWithError(ArcGISARView.accessDeniedError()) + } + } + + fileprivate func handleAuthStatusChangedAccessAuthorized() { + // auth status changed to authorized + if !notifiedStartOrFailure { + // we were waiting for status to come in to start the datasource + // now that we have authorization - start it + didStartOrFailWithError(nil) + + // need to start location manager updates + startUpdatingLocationAndHeading() + } + } + + // MARK: Errors + class func notSupportedError() -> NSError { + let userInfo = [NSLocalizedDescriptionKey : "The device does not support ARKit functionality."] + return NSError(domain: AGSErrorDomain, code: 0, userInfo: userInfo) + } + + class func accessDeniedError() -> NSError{ + let userInfo = [NSLocalizedDescriptionKey : "Access to the device location is denied."] + return NSError(domain: kCLErrorDomain, code: CLError.Code.denied.rawValue, userInfo: userInfo) + } + + class func missingPListKeyError() -> NSError{ + let userInfo = [NSLocalizedDescriptionKey : "You must specify a location usage description key (NSLocationWhenInUseUsageDescription or NSLocationAlwaysUsageDescription) in your plist."] + return NSError(domain: kCLErrorDomain, code: CLError.Code.denied.rawValue, userInfo: userInfo) + } +} + +// MARK: - ARSessionDelegate +extension ArcGISARView: ARSessionDelegate { + // AR session delegate methods + + /** + This is called when a new frame has been updated. + + @param session The session being run. + @param frame The frame that has been updated. + */ + public func session(_ session: ARSession, didUpdate frame: ARFrame) { + // TODO: updateCamera()..... + + delegate?.session?(session, didUpdate: frame) + } + + /** + This is called when new anchors are added to the session. + + @param session The session being run. + @param anchors An array of added anchors. + */ + public func session(_ session: ARSession, didAdd anchors: [ARAnchor]) { + delegate?.session?(session, didAdd: anchors) + } + + /** + This is called when anchors are updated. + + @param session The session being run. + @param anchors An array of updated anchors. + */ + public func session(_ session: ARSession, didUpdate anchors: [ARAnchor]) { + delegate?.session?(session, didUpdate: anchors) + } + + /** + This is called when anchors are removed from the session. + + @param session The session being run. + @param anchors An array of removed anchors. + */ + public func session(_ session: ARSession, didRemove anchors: [ARAnchor]) { + delegate?.session?(session, didRemove: anchors) + } +} + +// MARK: - ARSessionObserver +extension ArcGISARView: ARSessionObserver { + // AR session methods + + /** + This is called when a session fails. + + @discussion On failure the session will be paused. + @param session The session that failed. + @param error The error being reported (see ARError.h). + */ + public func session(_ session: ARSession, didFailWithError error: Error) { + guard error is ARError else { return } + + let errorWithInfo = error as NSError + let messages = [ + errorWithInfo.localizedDescription, + errorWithInfo.localizedFailureReason, + errorWithInfo.localizedRecoverySuggestion + ] + + // Remove optional error messages. + let errorMessage = messages.compactMap({ $0 }).joined(separator: "\n") + + DispatchQueue.main.async { + // Present an alert describing the error. + let alertController = UIAlertController(title: "Could not start tracking.", message: errorMessage, preferredStyle: .alert) + let restartAction = UIAlertAction(title: "Restart Tracking", style: .default) { _ in + alertController.dismiss(animated: true, completion: nil) + self.startTracking() + } + alertController.addAction(restartAction) + + guard let rootController = UIApplication.shared.keyWindow?.rootViewController else { return } + rootController.present(alertController, animated: true, completion: nil) + } + + delegate?.session?(session, didFailWithError: error) + } + + /** + This is called when the camera’s tracking state has changed. + + @param session The session being run. + @param camera The camera that changed tracking states. + */ + public func session(_ session: ARSession, cameraDidChangeTrackingState camera: ARCamera) { + delegate?.session?(session, cameraDidChangeTrackingState: camera) + } + + /** + This is called when a session is interrupted. + + @discussion A session will be interrupted and no longer able to track when + it fails to receive required sensor data. This happens when video capture is interrupted, + for example when the application is sent to the background or when there are + multiple foreground applications (see AVCaptureSessionInterruptionReason). + No additional frame updates will be delivered until the interruption has ended. + @param session The session that was interrupted. + */ + public func sessionWasInterrupted(_ session: ARSession) { + delegate?.sessionWasInterrupted?(session) + } + + /** + This is called when a session interruption has ended. + + @discussion A session will continue running from the last known state once + the interruption has ended. If the device has moved, anchors will be misaligned. + To avoid this, some applications may want to reset tracking (see ARSessionRunOptions) + or attempt to relocalize (see `-[ARSessionObserver sessionShouldAttemptRelocalization:]`). + @param session The session that was interrupted. + */ + public func sessionInterruptionEnded(_ session: ARSession) { + delegate?.sessionWasInterrupted?(session) + } + + /** + This is called after a session resumes from a pause or interruption to determine + whether or not the session should attempt to relocalize. + + @discussion To avoid misaligned anchors, apps may wish to attempt a relocalization after + a session pause or interruption. If YES is returned: the session will begin relocalizing + and tracking state will switch to limited with reason relocalizing. If successful, the + session's tracking state will return to normal. Because relocalization depends on + the user's location, it can run indefinitely. Apps that wish to give up on relocalization + may call run with `ARSessionRunOptionResetTracking` at any time. + @param session The session to relocalize. + @return Return YES to begin relocalizing. + */ + @available(iOS 11.3, *) + public func sessionShouldAttemptRelocalization(_ session: ARSession) -> Bool { + if let result = delegate?.sessionShouldAttemptRelocalization?(session) { + return result + } + return false + } + + /** + This is called when the session outputs a new audio sample buffer. + + @param session The session being run. + @param audioSampleBuffer The captured audio sample buffer. + */ + public func session(_ session: ARSession, didOutputAudioSampleBuffer audioSampleBuffer: CMSampleBuffer) { + delegate?.session?(session, didOutputAudioSampleBuffer: audioSampleBuffer) + } +} + +// MARK: - CLLocationManagerDelegate +extension ArcGISARView: CLLocationManagerDelegate { + /* + * locationManager:didUpdateLocations: + * + * Discussion: + * Invoked when new locations are available. Required for delivery of + * deferred locations. If implemented, updates will + * not be delivered to locationManager:didUpdateToLocation:fromLocation: + * + * locations is an array of CLLocation objects in chronological order. + */ + public func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) { + guard let location = locations.last, location.horizontalAccuracy >= 0.0 else { return } + let locationPoint = AGSPoint(x: location.coordinate.longitude, + y: location.coordinate.latitude, + z: location.altitude, + spatialReference: .wgs84()) + let camera = AGSCamera(location: locationPoint, heading: 0.0, pitch: 0.0, roll: 0.0) + sceneView.setViewpointCamera(camera) + + // TODO: original code tested new location for a better horizontal accuracy... + } + + /* + * locationManager:didUpdateHeading: + * + * Discussion: + * Invoked when a new heading is available. + */ + @available(iOS 3.0, *) + public func locationManager(_ manager: CLLocationManager, didUpdateHeading newHeading: CLHeading) { + + } + + /* + * locationManager:didFailWithError: + * + * Discussion: + * Invoked when an error has occurred. Error types are defined in "CLError.h". + */ + public func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) { + didStartOrFailWithError(error) + } + + /* + * locationManager:didChangeAuthorizationStatus: + * + * Discussion: + * Invoked when the authorization status changes for this application. + */ + public func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) { + let authStatus = CLLocationManager.authorizationStatus() + switch authStatus { + case .notDetermined: + break + case .restricted, .denied: + self.handleAuthStatusChangedAccessDenied() + case .authorizedAlways, .authorizedWhenInUse: + self.handleAuthStatusChangedAccessAuthorized() + } + } + + /* + * Discussion: + * Invoked when location updates are automatically paused. + */ + public func locationManagerDidPauseLocationUpdates(_ manager: CLLocationManager) { + + } + + /* + * Discussion: + * Invoked when location updates are automatically resumed. + * + * In the event that your application is terminated while suspended, you will + * not receive this notification. + */ + @available(iOS 6.0, *) + public func locationManagerDidResumeLocationUpdates(_ manager: CLLocationManager) { } } From a4d482759a653b336f31fbeac72f8a92bf54d89d Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Mon, 29 Apr 2019 15:00:26 -0500 Subject: [PATCH 009/147] Add location and motion stuff; add video to ArcGISARSensorView --- .../ArcGISToolkitExamples/ARExample.swift | 10 +- .../ArcGISToolkit/AR/ArcGISARSensorView.swift | 510 +++++++++++++++++- Toolkit/ArcGISToolkit/AR/ArcGISARView.swift | 55 +- 3 files changed, 542 insertions(+), 33 deletions(-) diff --git a/Examples/ArcGISToolkitExamples/ARExample.swift b/Examples/ArcGISToolkitExamples/ARExample.swift index bf73198d..abfec545 100644 --- a/Examples/ArcGISToolkitExamples/ARExample.swift +++ b/Examples/ArcGISToolkitExamples/ARExample.swift @@ -12,13 +12,15 @@ // limitations under the License. import UIKit -//import ArcGISToolkit -//import ArcGIS +import ArcGISToolkit +import ArcGIS open class ARExample: UIViewController { - public let arView = ArcGISARView(frame: CGRect.zero) - +// public let arView = ArcGISARView(frame: CGRect.zero) +// public let arView = ArcGISARView(renderVideoFeed: false) + public let arView = ArcGISARSensorView(renderVideoFeed: false) + override open func viewDidLoad() { super.viewDidLoad() diff --git a/Toolkit/ArcGISToolkit/AR/ArcGISARSensorView.swift b/Toolkit/ArcGISToolkit/AR/ArcGISARSensorView.swift index cd9ee067..72947e19 100644 --- a/Toolkit/ArcGISToolkit/AR/ArcGISARSensorView.swift +++ b/Toolkit/ArcGISToolkit/AR/ArcGISARSensorView.swift @@ -13,7 +13,9 @@ // limitations under the License. import UIKit -//import ArcGIS +import AVFoundation +import CoreMotion +import ArcGIS public enum LocationType { case anglesOnly @@ -23,12 +25,68 @@ public enum LocationType { public class ArcGISARSensorView: UIView { - public var locationType: LocationType = .anglesAndPosition - - public var sceneView: AGSSceneView? + public var locationType: LocationType = .anglesAndPosition { + didSet { + // need to update location and heading updates to account for new LocationType + } + } + public var sceneView = AGSSceneView(frame: .zero) { + willSet(newSceneview) { + removeSubviewAndConstraints(sceneView) + } + didSet { + addSubviewWithConstraints(sceneView) + + // make our sceneView's background transparent + sceneView.isBackgroundTransparent = true + sceneView.atmosphereEffect = .none + } + } + public var useAbsoluteHeading: Bool = true + // MARK: private properties + + public var renderVideoFeed = true + + // has the client been notfiied of start/failure + private var notifiedStartOrFailure = false + + private lazy var locationManager: CLLocationManager = { + let lm = CLLocationManager() + lm.desiredAccuracy = kCLLocationAccuracyBest + lm.delegate = self + return lm + }() + + private lazy var motionManager: CMMotionManager = { + let mm = CMMotionManager() + mm.deviceMotionUpdateInterval = 1.0 / 60.0 + mm.showsDeviceMovementDisplay = true + return mm + }() + + // MARK: Capture session + + private enum SessionSetupResult { + case success + case notAuthorized + case configurationFailed + } + + private lazy var session = AVCaptureSession() + + // Communicate with the capture session and other session objects on this queue. + private let sessionQueue = DispatchQueue(label: "session queue", attributes: [], target: nil) + private var setupResult: SessionSetupResult = .success + var videoDeviceInput: AVCaptureDeviceInput! + private lazy var cameraView = CameraView(frame:CGRect.zero) + + private var orientationQuat: simd_quatf = simd_quaternion(Float(0), Float(0), Float(0), Float(1)) + + // MARK: intializers + override init(frame: CGRect) { super.init(frame: frame) sharedInitialization() @@ -39,20 +97,454 @@ public class ArcGISARSensorView: UIView { sharedInitialization() } - required public init(renderVideoFeed: Bool){ - super.init(frame: CGRect.zero) - sharedInitialization() + required public convenience init(renderVideoFeed: Bool){ + self.init(frame: CGRect.zero) + self.renderVideoFeed = renderVideoFeed + if renderVideoFeed { + // Set up the video preview view. + addSubviewWithConstraints(cameraView, index: 0) + cameraView.session = session + + prepVideoFeed() + } } private func sharedInitialization(){ + + // + // make our sceneView's background transparent + sceneView.isBackgroundTransparent = true + sceneView.atmosphereEffect = .none + // add sceneView to our view + addSubviewWithConstraints(sceneView) } - + public func startTracking() { - + notifiedStartOrFailure = false + + // determine status of location manager + let authStatus = CLLocationManager.authorizationStatus() + switch authStatus { + case .notDetermined: + startWithAccessNotDetermined() + case .restricted, .denied: + startWithAccessDenied() + case .authorizedAlways, .authorizedWhenInUse: + startWithAccessAuthorized() + } + + // start motion manager + startUpdatingLocationAndHeading() + + if renderVideoFeed { + setupSession() + } } public func stopTracking() { + locationManager.stopUpdatingLocation() + if CLLocationManager.headingAvailable() { + locationManager.stopUpdatingHeading() + } + + motionManager.stopDeviceMotionUpdates() + + sessionQueue.async { [weak self] in + guard let strongSelf = self else { return } + if strongSelf.setupResult == .success { + strongSelf.session.stopRunning() + + UIDevice.current.endGeneratingDeviceOrientationNotifications() + NotificationCenter.default.removeObserver(strongSelf) + } + } + } + + // Called when device orientation changes + @objc func orientationChanged(notification: Notification) { + // handle rotation here + updateCameraViewOrientation() + } + + private func addSubviewWithConstraints(_ subview: UIView, index: Int = -1) { + // add subView to view and setup constraints + if index >= 0 { + insertSubview(subview, at: index) + } + else { + addSubview(subview) + } + subview.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([ + subview.leadingAnchor.constraint(equalTo: self.leadingAnchor), + subview.trailingAnchor.constraint(equalTo: self.trailingAnchor), + subview.topAnchor.constraint(equalTo: self.topAnchor), + subview.bottomAnchor.constraint(equalTo: self.bottomAnchor) + ]) + } + + private func removeSubviewAndConstraints(_ subview: UIView) { + // remove subView from view along with constraints + subview.removeFromSuperview() + removeConstraints(subview.constraints) + } + + private func startWithAccessNotDetermined() { + if (Bundle.main.object(forInfoDictionaryKey: "NSLocationWhenInUseUsageDescription") != nil) { + locationManager.requestWhenInUseAuthorization() + } + if (Bundle.main.object(forInfoDictionaryKey: "NSLocationAlwaysUsageDescription") != nil) { + locationManager.requestAlwaysAuthorization() + } + else{ + didStartOrFailWithError(ArcGISARView.missingPListKeyError()) + } + } + + private func startUpdatingLocationAndHeading() { + locationManager.startUpdatingLocation() + if CLLocationManager.headingAvailable() { + locationManager.startUpdatingHeading() + } + + startUpdatingAngles() + } + + private func startWithAccessDenied() { + didStartOrFailWithError(ArcGISARView.accessDeniedError()) + } + + private func startWithAccessAuthorized() { + startUpdatingLocationAndHeading() + } + + private func didStartOrFailWithError(_ error: Error?) { + // TODO: present error to user... + + notifiedStartOrFailure = true; + } + + private func handleAuthStatusChangedAccessDenied() { + // auth status changed to denied + if !notifiedStartOrFailure { + stopTracking() + // we were waiting for user prompt to come back, so notify + didStartOrFailWithError(ArcGISARView.accessDeniedError()) + } + } + + private func handleAuthStatusChangedAccessAuthorized() { + // auth status changed to authorized + if !notifiedStartOrFailure { + // we were waiting for status to come in to start the datasource + // now that we have authorization - start it + didStartOrFailWithError(nil) + + // need to start location manager updates + startUpdatingLocationAndHeading() + } + } + + private func finalizeStart() { + // TODO: is there anything to do here? + } + + private func startUpdatingAngles() { + let motionQueue = OperationQueue.init() + motionQueue.qualityOfService = .userInteractive + motionQueue.maxConcurrentOperationCount = 1 + + motionManager.startDeviceMotionUpdates(to: motionQueue) { [weak self] (motion, error) in + guard let quat = self?.motionManager.deviceMotion?.attitude.quaternion, + let orientationQuat = self?.orientationQuat else { return } + let currentQuat = simd_quaternion(Float(quat.x), Float(quat.y), Float(quat.z), Float(quat.w)) + let finalQuat = simd_mul(currentQuat, orientationQuat) + + print("updating device motion: \(finalQuat)") + //use `finalQuat` to update position/orientation of camera + } + } + + // MARK: Video + + func updateCameraViewOrientation() { + if let videoPreviewLayerConnection = cameraView.videoPreviewLayer.connection { + let deviceOrientation = UIDevice.current.orientation + guard let newVideoOrientation = AVCaptureVideoOrientation(rawValue: deviceOrientation.rawValue), + deviceOrientation.isPortrait || deviceOrientation.isLandscape else { + return + } + + videoPreviewLayerConnection.videoOrientation = newVideoOrientation + } + } + + private func prepVideoFeed() { + // + // Check video authorization status. Video access is required and audio + // access is optional. If audio access is denied, audio is not recorded + // during movie recording. + // + switch AVCaptureDevice.authorizationStatus(for: .video) { + case .authorized: + // The user has previously granted access to the camera. + setupResult = .success + break + + case .notDetermined: + // The user has not yet been presented with the option to grant + // video access. We suspend the session queue to delay session + // setup until the access request has completed. + // + // Note that audio access will be implicitly requested when we + // create an AVCaptureDeviceInput for audio during session setup. + sessionQueue.suspend() + AVCaptureDevice.requestAccess(for: .video) { [weak self] (accessGranted) in + if !accessGranted { + self?.setupResult = .notAuthorized + } + else { + self?.setupResult = .success + self?.sessionQueue.resume() + } + } + + default: + // The user has previously denied access. + setupResult = .notAuthorized + } + + // Setup the capture session. + // In general it is not safe to mutate an AVCaptureSession or any of its + // inputs, outputs, or connections from multiple threads at the same time. + // + // Why not do all of this on the main queue? + // Because AVCaptureSession.startRunning() is a blocking call which can + // take a long time. We dispatch session setup to the sessionQueue so + // that the main queue isn't blocked, which keeps the UI responsive. + sessionQueue.async { [weak self] in + self?.configureSession() + } + } + + private func setupSession() { + + // session setup + sessionQueue.async { + switch self.setupResult { + case .success: + self.session.startRunning() + DispatchQueue.main.async { + + self.updateCameraViewOrientation() + + // add observer to catch orientation changes + UIDevice.current.beginGeneratingDeviceOrientationNotifications() + NotificationCenter.default.addObserver( + self, + selector: #selector(self.orientationChanged(notification:)), + name: UIDevice.orientationDidChangeNotification, + object: nil + ) + } + + case .notAuthorized: + DispatchQueue.main.async { + let message = NSLocalizedString("ArcGISARSensorView does not have permission to use the camera, please change privacy settings", comment: "Alert message when the user has denied access to the camera") + let alertController = UIAlertController(title: "ArcGISARSensorView", message: message, preferredStyle: .alert) + alertController.addAction(UIAlertAction(title: NSLocalizedString("OK", comment: "Alert OK button"), style: .cancel, handler: nil)) + alertController.addAction(UIAlertAction(title: NSLocalizedString("Settings", comment: "Alert button to open Settings"), style: .`default`, handler: { action in + UIApplication.shared.open(URL(string: UIApplication.openSettingsURLString)!, options: [:], completionHandler: nil) + })) + + guard let rootController = UIApplication.shared.keyWindow?.rootViewController else { return } + rootController.present(alertController, animated: true, completion: nil) + } + + case .configurationFailed: + DispatchQueue.main.async { + let message = NSLocalizedString("Unable to capture media", comment: "Alert message when something goes wrong during capture session configuration") + let alertController = UIAlertController(title: "ArcGISARSensorView", message: message, preferredStyle: .alert) + alertController.addAction(UIAlertAction(title: NSLocalizedString("OK", comment: "Alert OK button"), style: .cancel, handler: nil)) + + guard let rootController = UIApplication.shared.keyWindow?.rootViewController else { return } + rootController.present(alertController, animated: true, completion: nil) + } + } + } + } + + // MARK: Session Management + + // Call this on the session queue. + private func configureSession() { + if setupResult != .success { + return + } + + session.beginConfiguration() + session.sessionPreset = .high + + // Add video input. + do { + var defaultVideoDevice: AVCaptureDevice? + + // Choose the back dual camera if available, otherwise default to a wide angle camera. + // if let dualCameraDevice = AVCaptureDevice.defaultDevice(withDeviceType: .builtInDuoCamera, mediaType: AVMediaTypeVideo, position: .back) { + // defaultVideoDevice = dualCameraDevice + // } + // else if let backCameraDevice = AVCaptureDevice.defaultDevice(withDeviceType: .builtInWideAngleCamera, mediaType: AVMediaTypeVideo, position: .back) { + // // If the back dual camera is not available, default to the back wide angle camera. + // defaultVideoDevice = backCameraDevice + // } + // else if let frontCameraDevice = AVCaptureDevice.defaultDevice(withDeviceType: .builtInWideAngleCamera, mediaType: AVMediaTypeVideo, position: .front) { + // In some cases where users break their phones, the back wide angle camera is not available. In this case, we should default to the front wide angle camera. + defaultVideoDevice = AVCaptureDevice.default(for: .video) + // } + + let videoDeviceInput = try AVCaptureDeviceInput(device: defaultVideoDevice!) + + if session.canAddInput(videoDeviceInput) { + session.addInput(videoDeviceInput) + self.videoDeviceInput = videoDeviceInput +// +// DispatchQueue.main.async { [weak self] in +// self?.cameraView.videoPreviewLayer.connection!.videoOrientation = .landscapeLeft +// } + } + else { + print("Could not add video device input to the session") + setupResult = .configurationFailed + session.commitConfiguration() + return + } + } + catch { + print("Could not create video device input: \(error)") + setupResult = .configurationFailed + session.commitConfiguration() + return + } + + session.commitConfiguration() + } + + // MARK: Errors + class func notSupportedError() -> NSError { + let userInfo = [NSLocalizedDescriptionKey : "The device does not support ARKit functionality."] + return NSError(domain: AGSErrorDomain, code: 0, userInfo: userInfo) + } + + class func accessDeniedError() -> NSError{ + let userInfo = [NSLocalizedDescriptionKey : "Access to the device location is denied."] + return NSError(domain: kCLErrorDomain, code: CLError.Code.denied.rawValue, userInfo: userInfo) + } + + class func missingPListKeyError() -> NSError{ + let userInfo = [NSLocalizedDescriptionKey : "You must specify a location usage description key (NSLocationWhenInUseUsageDescription or NSLocationAlwaysUsageDescription) in your plist."] + return NSError(domain: kCLErrorDomain, code: CLError.Code.denied.rawValue, userInfo: userInfo) + } + +} + +// MARK: - CLLocationManagerDelegate + +extension ArcGISARSensorView: CLLocationManagerDelegate { + /* + * locationManager:didUpdateLocations: + * + * Discussion: + * Invoked when new locations are available. Required for delivery of + * deferred locations. If implemented, updates will + * not be delivered to locationManager:didUpdateToLocation:fromLocation: + * + * locations is an array of CLLocation objects in chronological order. + */ + public func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) { + guard let location = locations.last, location.horizontalAccuracy >= 0.0 else { return } + + let locationPoint = AGSPoint(x: location.coordinate.longitude, + y: location.coordinate.latitude, + z: location.altitude, + spatialReference: .wgs84()) +// let camera = AGSCamera(location: locationPoint, heading: 0.0, pitch: 0.0, roll: 0.0) +// sceneView.setViewpointCamera(camera) + +// finalizeStart() // is this needed? + + print("updating location: \(locationPoint)") + } + + /* + * locationManager:didFailWithError: + * + * Discussion: + * Invoked when an error has occurred. Error types are defined in "CLError.h". + */ + public func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) { + didStartOrFailWithError(error) + } + + /* + * locationManager:didChangeAuthorizationStatus: + * + * Discussion: + * Invoked when the authorization status changes for this application. + */ + public func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) { + let authStatus = CLLocationManager.authorizationStatus() + switch authStatus { + case .notDetermined: + break + case .restricted, .denied: + self.handleAuthStatusChangedAccessDenied() + case .authorizedAlways, .authorizedWhenInUse: + self.handleAuthStatusChangedAccessAuthorized() + } + } + + /* + * Discussion: + * Invoked when location updates are automatically paused. + */ + public func locationManagerDidPauseLocationUpdates(_ manager: CLLocationManager) { + + } + + /* + * Discussion: + * Invoked when location updates are automatically resumed. + * + * In the event that your application is terminated while suspended, you will + * not receive this notification. + */ + @available(iOS 6.0, *) + public func locationManagerDidResumeLocationUpdates(_ manager: CLLocationManager) { + + } +} + +// MARK: CameraView + +/// CameraView - view which displays the live camera image +class CameraView: UIView { + var videoPreviewLayer: AVCaptureVideoPreviewLayer { + return layer as! AVCaptureVideoPreviewLayer + } + + var session: AVCaptureSession? { + get { + return videoPreviewLayer.session + } + set { + videoPreviewLayer.session = newValue + } + } + + // MARK: UIView + override class var layerClass: AnyClass { + return AVCaptureVideoPreviewLayer.self } } diff --git a/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift b/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift index eab4d9b5..9a73b979 100644 --- a/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift +++ b/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift @@ -14,7 +14,7 @@ import UIKit import ARKit -//import ArcGIS +import ArcGIS public class ArcGISARView: UIView { @@ -45,6 +45,10 @@ public class ArcGISARView: UIView { lm.delegate = self return lm }() + + // initial location from locationManager + private var initialLocation: CLLocation? + private var horizontalAccuracy: CLLocationAccuracy = .greatestFiniteMagnitude; // is ARKit supported on this device? private var isSupported = false @@ -73,20 +77,19 @@ public class ArcGISARView: UIView { // // ARKit initialization isSupported = ARWorldTrackingConfiguration.isSupported - + addSubviewWithConstraints(arSCNView) arSCNView.session.delegate = self // // add sceneView to view and setup constraints addSubviewWithConstraints(sceneView) - - locationManager.delegate = self // // make our sceneView's background transparent sceneView.isBackgroundTransparent = true sceneView.atmosphereEffect = .none + } // MARK: Public @@ -125,11 +128,11 @@ public class ArcGISARView: UIView { let authStatus = CLLocationManager.authorizationStatus() switch authStatus { case .notDetermined: - self.startWithAccessNotDetermined() + startWithAccessNotDetermined() case .restricted, .denied: - self.startWithAccessDenied() + startWithAccessDenied() case .authorizedAlways, .authorizedWhenInUse: - self.startWithAccessAuthorized() + startWithAccessAuthorized() } } } @@ -144,14 +147,14 @@ public class ArcGISARView: UIView { // MARK: Private fileprivate func finalizeStart() { - + arSCNView.isHidden = !renderVideoFeed arSCNView.session.run(arConfiguration, options:.resetTracking) didStartOrFailWithError(nil) } fileprivate func addSubviewWithConstraints(_ subview: UIView) { - // add arSCNView to view and setup constraints - self.addSubview(subview) + // add subview to view and setup constraints + addSubview(subview) subview.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ subview.leadingAnchor.constraint(equalTo: self.leadingAnchor), @@ -244,7 +247,7 @@ extension ArcGISARView: ARSessionDelegate { */ public func session(_ session: ARSession, didUpdate frame: ARFrame) { // TODO: updateCamera()..... - + print("didUpdateFrame...") delegate?.session?(session, didUpdate: frame) } @@ -402,14 +405,26 @@ extension ArcGISARView: CLLocationManagerDelegate { */ public func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) { guard let location = locations.last, location.horizontalAccuracy >= 0.0 else { return } - let locationPoint = AGSPoint(x: location.coordinate.longitude, - y: location.coordinate.latitude, - z: location.altitude, - spatialReference: .wgs84()) - let camera = AGSCamera(location: locationPoint, heading: 0.0, pitch: 0.0, roll: 0.0) - sceneView.setViewpointCamera(camera) - // TODO: original code tested new location for a better horizontal accuracy... + if initialLocation == nil { + initialLocation = location + horizontalAccuracy = location.horizontalAccuracy + + let locationPoint = AGSPoint(x: location.coordinate.longitude, + y: location.coordinate.latitude, + z: location.altitude, + spatialReference: .wgs84()) + let camera = AGSCamera(location: locationPoint, heading: 0.0, pitch: 0.0, roll: 0.0) + sceneView.setViewpointCamera(camera) + + finalizeStart() + } + else if location.horizontalAccuracy < horizontalAccuracy { + horizontalAccuracy = location.horizontalAccuracy + // TODO: update current location??? + } + + print("didUpdateLocations...") } /* @@ -445,9 +460,9 @@ extension ArcGISARView: CLLocationManagerDelegate { case .notDetermined: break case .restricted, .denied: - self.handleAuthStatusChangedAccessDenied() + handleAuthStatusChangedAccessDenied() case .authorizedAlways, .authorizedWhenInUse: - self.handleAuthStatusChangedAccessAuthorized() + handleAuthStatusChangedAccessAuthorized() } } From 0e10ca518caf48274e84540f3789a190c3876204 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Fri, 3 May 2019 10:14:54 -0500 Subject: [PATCH 010/147] location manager startup changes --- .../ArcGISToolkitExamples/ARExample.swift | 4 +- Toolkit/ArcGISToolkit/AR/ArcGISARView.swift | 46 +++++++++++-------- 2 files changed, 28 insertions(+), 22 deletions(-) diff --git a/Examples/ArcGISToolkitExamples/ARExample.swift b/Examples/ArcGISToolkitExamples/ARExample.swift index abfec545..e6ab36e8 100644 --- a/Examples/ArcGISToolkitExamples/ARExample.swift +++ b/Examples/ArcGISToolkitExamples/ARExample.swift @@ -17,9 +17,9 @@ import ArcGIS open class ARExample: UIViewController { -// public let arView = ArcGISARView(frame: CGRect.zero) + public let arView = ArcGISARView(frame: CGRect.zero) // public let arView = ArcGISARView(renderVideoFeed: false) - public let arView = ArcGISARSensorView(renderVideoFeed: false) +// public let arView = ArcGISARSensorView(renderVideoFeed: false) override open func viewDidLoad() { super.viewDidLoad() diff --git a/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift b/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift index 9a73b979..bbde7c23 100644 --- a/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift +++ b/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift @@ -90,6 +90,7 @@ public class ArcGISARView: UIView { sceneView.isBackgroundTransparent = true sceneView.atmosphereEffect = .none + notifiedStartOrFailure = false } // MARK: Public @@ -99,7 +100,9 @@ public class ArcGISARView: UIView { } public func resetTracking() { - + // reset initial location, so we're sure to set it from the LocationManager (provided originCamera == nil) + initialLocation = nil + startTracking() } public func resetUsingLocationServices() -> Bool { @@ -111,7 +114,6 @@ public class ArcGISARView: UIView { } public func startTracking() { - notifiedStartOrFailure = false if !isSupported { didStartOrFailWithError(ArcGISARView.notSupportedError()) @@ -139,10 +141,7 @@ public class ArcGISARView: UIView { public func stopTracking() { arSCNView.session.pause() - locationManager.stopUpdatingLocation() - if CLLocationManager.headingAvailable() { - locationManager.stopUpdatingHeading() - } + stopUpdatingLocationAndHeading() } // MARK: Private @@ -182,6 +181,13 @@ public class ArcGISARView: UIView { locationManager.startUpdatingHeading() } } + + fileprivate func stopUpdatingLocationAndHeading() { + locationManager.stopUpdatingLocation() + if CLLocationManager.headingAvailable() { + locationManager.stopUpdatingHeading() + } + } fileprivate func startWithAccessDenied() { didStartOrFailWithError(ArcGISARView.accessDeniedError()) @@ -192,30 +198,30 @@ public class ArcGISARView: UIView { } fileprivate func didStartOrFailWithError(_ error: Error?) { - // TODO: present error to user... + if !notifiedStartOrFailure, let error = error { + // TODO: present error to user... + print("didStartOrFailWithError: \(String(reflecting:error))") + } notifiedStartOrFailure = true; } fileprivate func handleAuthStatusChangedAccessDenied() { // auth status changed to denied - if !notifiedStartOrFailure { - stopTracking() - // we were waiting for user prompt to come back, so notify - didStartOrFailWithError(ArcGISARView.accessDeniedError()) - } + stopUpdatingLocationAndHeading() + + // we were waiting for user prompt to come back, so notify + didStartOrFailWithError(ArcGISARView.accessDeniedError()) } fileprivate func handleAuthStatusChangedAccessAuthorized() { // auth status changed to authorized - if !notifiedStartOrFailure { - // we were waiting for status to come in to start the datasource - // now that we have authorization - start it - didStartOrFailWithError(nil) - - // need to start location manager updates - startUpdatingLocationAndHeading() - } + // we were waiting for status to come in to start the datasource + // now that we have authorization - start it + didStartOrFailWithError(nil) + + // need to start location manager updates + startUpdatingLocationAndHeading() } // MARK: Errors From fcde4fc7299992ecb989e49e6053c4cbd23d0043 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Wed, 8 May 2019 09:49:39 -0500 Subject: [PATCH 011/147] Move location of video feed setup. --- Toolkit/ArcGISToolkit/AR/ArcGISARSensorView.swift | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/Toolkit/ArcGISToolkit/AR/ArcGISARSensorView.swift b/Toolkit/ArcGISToolkit/AR/ArcGISARSensorView.swift index 72947e19..5c9bde64 100644 --- a/Toolkit/ArcGISToolkit/AR/ArcGISARSensorView.swift +++ b/Toolkit/ArcGISToolkit/AR/ArcGISARSensorView.swift @@ -100,13 +100,6 @@ public class ArcGISARSensorView: UIView { required public convenience init(renderVideoFeed: Bool){ self.init(frame: CGRect.zero) self.renderVideoFeed = renderVideoFeed - if renderVideoFeed { - // Set up the video preview view. - addSubviewWithConstraints(cameraView, index: 0) - cameraView.session = session - - prepVideoFeed() - } } private func sharedInitialization(){ @@ -118,6 +111,14 @@ public class ArcGISARSensorView: UIView { // add sceneView to our view addSubviewWithConstraints(sceneView) + + if renderVideoFeed { + // Set up the video preview view. + addSubviewWithConstraints(cameraView, index: 0) + cameraView.session = session + + prepVideoFeed() + } } public func startTracking() { From 6dc9c6178529a3f328e04ea00ffb50e7ce15f535 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Wed, 8 May 2019 10:00:14 -0500 Subject: [PATCH 012/147] Add LocationWhenInUseUsageDescription --- Examples/ArcGISToolkitExamples/Info.plist | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Examples/ArcGISToolkitExamples/Info.plist b/Examples/ArcGISToolkitExamples/Info.plist index 24d7c13d..7d1d698f 100644 --- a/Examples/ArcGISToolkitExamples/Info.plist +++ b/Examples/ArcGISToolkitExamples/Info.plist @@ -2,6 +2,8 @@ + NSLocationWhenInUseUsageDescription + For showing the current location in a map CFBundleDevelopmentRegion en CFBundleExecutable From 271f0dbd912304719731e02e909bb7333111072c Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Wed, 8 May 2019 16:39:12 -0500 Subject: [PATCH 013/147] renderFrame() and framerate stuff. --- Toolkit/ArcGISToolkit/AR/ArcGISARView.swift | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift b/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift index bbde7c23..55ccc498 100644 --- a/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift +++ b/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift @@ -56,6 +56,10 @@ public class ArcGISARView: UIView { // has the client been notfiied of start/failure private var notifiedStartOrFailure = false + // for calculating framerate + var frameCount:Int = 0 + var frameCountTimer: Timer? + // MARK: Initializers public override init(frame: CGRect) { @@ -89,7 +93,8 @@ public class ArcGISARView: UIView { // make our sceneView's background transparent sceneView.isBackgroundTransparent = true sceneView.atmosphereEffect = .none - + sceneView.isManualRendering = true + notifiedStartOrFailure = false } @@ -137,11 +142,18 @@ public class ArcGISARView: UIView { startWithAccessAuthorized() } } + + frameCountTimer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true, block: { [weak self] (timer) in + print("Frame rate = \(self?.frameCount)") + self?.frameCount = 0 + }) } public func stopTracking() { arSCNView.session.pause() stopUpdatingLocationAndHeading() + + frameCountTimer?.invalidate() } // MARK: Private @@ -253,8 +265,10 @@ extension ArcGISARView: ARSessionDelegate { */ public func session(_ session: ARSession, didUpdate frame: ARFrame) { // TODO: updateCamera()..... - print("didUpdateFrame...") +// print("didUpdateFrame...") delegate?.session?(session, didUpdate: frame) + frameCount = frameCount + 1 + self.sceneView.renderFrame() } /** From 61f47d59bca172836c0f75930e5c681579749ef8 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Mon, 13 May 2019 11:10:55 -0500 Subject: [PATCH 014/147] don't print debug out in SensorView (for now). --- Toolkit/ArcGISToolkit/AR/ArcGISARSensorView.swift | 4 ++-- Toolkit/ArcGISToolkit/AR/ArcGISARView.swift | 6 ++++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/Toolkit/ArcGISToolkit/AR/ArcGISARSensorView.swift b/Toolkit/ArcGISToolkit/AR/ArcGISARSensorView.swift index 5c9bde64..96e110cf 100644 --- a/Toolkit/ArcGISToolkit/AR/ArcGISARSensorView.swift +++ b/Toolkit/ArcGISToolkit/AR/ArcGISARSensorView.swift @@ -262,7 +262,7 @@ public class ArcGISARSensorView: UIView { let currentQuat = simd_quaternion(Float(quat.x), Float(quat.y), Float(quat.z), Float(quat.w)) let finalQuat = simd_mul(currentQuat, orientationQuat) - print("updating device motion: \(finalQuat)") +// print("updating device motion: \(finalQuat)") //use `finalQuat` to update position/orientation of camera } } @@ -474,7 +474,7 @@ extension ArcGISARSensorView: CLLocationManagerDelegate { // finalizeStart() // is this needed? - print("updating location: \(locationPoint)") +// print("updating location: \(locationPoint)") } /* diff --git a/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift b/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift index 55ccc498..aa5fe880 100644 --- a/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift +++ b/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift @@ -143,8 +143,10 @@ public class ArcGISARView: UIView { } } + // reset frameCount and start timer to capture frame rate + frameCount = 0 frameCountTimer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true, block: { [weak self] (timer) in - print("Frame rate = \(self?.frameCount)") + print("Frame rate = \(String(reflecting: self?.frameCount))") self?.frameCount = 0 }) } @@ -267,8 +269,8 @@ extension ArcGISARView: ARSessionDelegate { // TODO: updateCamera()..... // print("didUpdateFrame...") delegate?.session?(session, didUpdate: frame) - frameCount = frameCount + 1 self.sceneView.renderFrame() + frameCount = frameCount + 1 } /** From 824cdbbce22b40b2d1447bdaadc7084dbc80051f Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Thu, 23 May 2019 13:34:21 -0500 Subject: [PATCH 015/147] Lots of new code and a bunch of test stuff that needs cleaning --- .../ArcGISToolkitExamples/ARExample.swift | 121 +++++++- .../ArcGISToolkit/AR/ArcGISARSensorView.swift | 105 ++++++- Toolkit/ArcGISToolkit/AR/ArcGISARView.swift | 263 +++++++++++++++++- 3 files changed, 471 insertions(+), 18 deletions(-) diff --git a/Examples/ArcGISToolkitExamples/ARExample.swift b/Examples/ArcGISToolkitExamples/ARExample.swift index e6ab36e8..dcdfd0ae 100644 --- a/Examples/ArcGISToolkitExamples/ARExample.swift +++ b/Examples/ArcGISToolkitExamples/ARExample.swift @@ -12,14 +12,19 @@ // limitations under the License. import UIKit -import ArcGISToolkit -import ArcGIS +//import ArcGISToolkit +//import ArcGIS open class ARExample: UIViewController { + var featureLayer: AGSFeatureLayer? + let graphicsOverlay = AGSGraphicsOverlay() + let basemapSwitch = UISwitch(frame: .zero) + let graphicsSwitch = UISwitch(frame: .zero) + public let arView = ArcGISARView(frame: CGRect.zero) // public let arView = ArcGISARView(renderVideoFeed: false) -// public let arView = ArcGISARSensorView(renderVideoFeed: false) +// public let arView = ArcGISARSensorView(renderVideoFeed: true) override open func viewDidLoad() { super.viewDidLoad() @@ -28,7 +33,33 @@ open class ARExample: UIViewController { arView.autoresizingMask = [.flexibleWidth, .flexibleHeight] view.addSubview(arView) - arView.sceneView.scene = AGSScene(basemapType: .streets) + arView.sceneView.scene = scene() +// arView.sceneView.alpha = 0.5 + + // option to turn background on/off + basemapSwitch.isOn = true + + basemapSwitch.translatesAutoresizingMaskIntoConstraints = false + arView.sceneView.addSubview(basemapSwitch) + NSLayoutConstraint.activate([ + basemapSwitch.trailingAnchor.constraint(equalTo: arView.sceneView.trailingAnchor, constant: -24), + basemapSwitch.topAnchor.constraint(equalTo: arView.sceneView.topAnchor, constant: 88) + ]) + basemapSwitch.isOn = arView.sceneView.isBackgroundTransparent + basemapSwitch.addTarget(self, action: #selector(switchBasemap), for: .valueChanged) + + // option to turn background on/off + graphicsSwitch.isOn = false + + graphicsSwitch.translatesAutoresizingMaskIntoConstraints = false + arView.sceneView.addSubview(graphicsSwitch) + NSLayoutConstraint.activate([ + graphicsSwitch.trailingAnchor.constraint(equalTo: arView.sceneView.trailingAnchor, constant: -24), + graphicsSwitch.topAnchor.constraint(equalTo: basemapSwitch.bottomAnchor, constant: 12) + ]) + graphicsSwitch.isOn = arView.sceneView.isBackgroundTransparent + graphicsSwitch.addTarget(self, action: #selector(switchGraphics), for: .valueChanged) + } override open func viewDidAppear(_ animated: Bool) { @@ -38,5 +69,87 @@ open class ARExample: UIViewController { override open func viewDidDisappear(_ animated: Bool) { arView.stopTracking() } + + private func scene() -> AGSScene { + + // create scene + let scene = AGSScene(basemapType: .streets) +// let scene = AGSScene() + + // create elevation surface + let elevationSource = AGSArcGISTiledElevationSource(url: URL(string: "http://elevation3d.arcgis.com/arcgis/rest/services/WorldElevation3D/Terrain3D/ImageServer")!) + let surface = AGSSurface() + surface.elevationSources = [elevationSource] + surface.name = "baseSurface" + surface.isEnabled = true + surface.backgroundGrid.isVisible = false + scene.baseSurface = surface + + // add data #1 +// let ft = AGSServiceFeatureTable(url: URL(string: "https://services2.arcgis.com/2B0gmGCMCH3iKkax/arcgis/rest/services/MinneapolisStPaulPOI/FeatureServer/0")!) +// featureLayer = AGSFeatureLayer(featureTable: ft) +//// arView.sceneView.scene?.operationalLayers.add(featureLayer!) +// addGraphicsToOverlay() + + // add data #2 +// let sceneLayer = AGSArcGISSceneLayer(name: "sandiegostage") +// arView.sceneView.scene?.operationalLayers.add(sceneLayer) + + return scene + } + + func addGraphicsToOverlay() { + // add graphics overlay to scene + + arView.sceneView.graphicsOverlays.add(graphicsOverlay) + let qp = AGSQueryParameters() + + // upperleft: 44.950751; -93.323193 + // lr: 44.929669, -93.287659 + + let envelope = AGSEnvelope(xMin: -93.323193, yMin: 44.929669, xMax: -93.287659, yMax: 44.950751, spatialReference: .wgs84()) + qp.geometry = envelope + qp.spatialRelationship = .contains + + // self.featureLayer?.selectFeatures(withQuery: qp, mode: .new, completion: { (queryResult, error) in + self.featureLayer?.featureTable?.load(completion: { (error) in + self.featureLayer?.featureTable?.queryFeatures(with: qp, completion: { (queryResult, error) in + if let queryResult = queryResult { + let markerSymbol = AGSSimpleMarkerSceneSymbol(style: .diamond, color: .blue, height: 50, width: 50, depth: 50, anchorPosition: .bottom) + for feature in queryResult.featureEnumerator() { + if let feature = feature as? AGSArcGISFeature { + feature.load(completion: { (error) in + guard error == nil else { return } + let compositeSymbol = AGSCompositeSymbol() + let text = feature.attributes.object(forKey: "Name") as? String ?? "" + let textSymbol = AGSTextSymbol(text: text, color: .red, size: 36.0, horizontalAlignment: .center, verticalAlignment: .bottom) + + compositeSymbol.symbols = [markerSymbol, textSymbol] + if let featurePoint = feature.geometry as? AGSPoint { + let point = AGSPoint(x: featurePoint.x, y: featurePoint.y, z: 100, spatialReference: feature.geometry?.spatialReference) + let graphic = AGSGraphic(geometry: point, symbol: compositeSymbol, attributes: feature.attributes as? [String : Any]) + self.graphicsOverlay.graphics.add(graphic) + } + }) + } + } + } + }) + }) + } + + @objc func switchBasemap() { + arView.sceneView.scene?.basemap?.baseLayers.forEach({ (baseLayer) in + guard let layer = baseLayer as? AGSLayer else { return } + layer.isVisible = basemapSwitch.isOn + arView.sceneView.scene?.baseSurface?.backgroundGrid.isVisible = basemapSwitch.isOn + }) + // sceneView.atmosphereEffect = backgroundSwitch.isOn ? .none : .realistic + } + + @objc func switchGraphics() { + guard let overlay = arView.sceneView.graphicsOverlays.firstObject as? AGSGraphicsOverlay else { return } + overlay.isVisible = graphicsSwitch.isOn + } } diff --git a/Toolkit/ArcGISToolkit/AR/ArcGISARSensorView.swift b/Toolkit/ArcGISToolkit/AR/ArcGISARSensorView.swift index 96e110cf..bf62658c 100644 --- a/Toolkit/ArcGISToolkit/AR/ArcGISARSensorView.swift +++ b/Toolkit/ArcGISToolkit/AR/ArcGISARSensorView.swift @@ -15,7 +15,7 @@ import UIKit import AVFoundation import CoreMotion -import ArcGIS +//import ArcGIS public enum LocationType { case anglesOnly @@ -67,6 +67,11 @@ public class ArcGISARSensorView: UIView { return mm }() + private var initialLocation: AGSPoint? + private var currentCamera: AGSCamera = AGSCamera(latitude: 0, longitude: 0, altitude: 0, heading: 0, pitch: 0, roll: 0) + + var updateTimer: Timer? + // MARK: Capture session private enum SessionSetupResult { @@ -135,8 +140,8 @@ public class ArcGISARSensorView: UIView { startWithAccessAuthorized() } - // start motion manager - startUpdatingLocationAndHeading() +// // start motion manager +// startUpdatingLocationAndHeading() if renderVideoFeed { setupSession() @@ -249,6 +254,12 @@ public class ArcGISARSensorView: UIView { private func finalizeStart() { // TODO: is there anything to do here? +// updateTimer = Timer.scheduledTimer(withTimeInterval: 1 / 10, repeats: true, block: { [weak self] (timer) in +// guard let strongSelf = self else { return } +// print("currentCamera = \(strongSelf.currentCamera)") +// strongSelf.sceneView.setViewpointCamera(strongSelf.currentCamera) +// }) + } private func startUpdatingAngles() { @@ -257,13 +268,60 @@ public class ArcGISARSensorView: UIView { motionQueue.maxConcurrentOperationCount = 1 motionManager.startDeviceMotionUpdates(to: motionQueue) { [weak self] (motion, error) in - guard let quat = self?.motionManager.deviceMotion?.attitude.quaternion, - let orientationQuat = self?.orientationQuat else { return } - let currentQuat = simd_quaternion(Float(quat.x), Float(quat.y), Float(quat.z), Float(quat.w)) - let finalQuat = simd_mul(currentQuat, orientationQuat) + guard let strongSelf = self else { return } +// guard let quat = self?.motionManager.deviceMotion?.attitude.quaternion, +// let orientationQuat = self?.orientationQuat else { return } +// let currentQuat = simd_quaternion(Float(quat.x), Float(quat.y), Float(quat.z), Float(quat.w)) +// let finalQuat = simd_mul(currentQuat, orientationQuat) + + + guard let attitude = strongSelf.motionManager.deviceMotion?.attitude else { return } + + // Landscape + let heading = 360 - attitude.yaw * 180 / .pi + var pitch = -attitude.roll * 180.0 / .pi + pitch = pitch < 0 ? pitch + 180.0 : pitch + let roll = 360 - attitude.pitch * 180.0 / .pi + + // portrait - this doesn't work +// let heading = (atan2(motion!.gravity.x, motion!.gravity.y) - .pi) * 180.0 / .pi //0.0 //360 - attitude.yaw * 180 / .pi +// var pitch = 90.0//360 - attitude.pitch * 180.0 / .pi +//// pitch = pitch < 0 ? pitch + 180.0 : pitch +// let roll = 0.0 //-attitude.roll * 180.0 / .pi//-attitude.yaw * 180 / .pi//360 - attitude.pitch * 180.0 / .pi + + let camera = strongSelf.sceneView.currentViewpointCamera() + //landscape + strongSelf.currentCamera = camera.rotate(toHeading: heading, pitch: pitch, roll: roll) + + strongSelf.sceneView.setViewpointCamera(strongSelf.currentCamera) + print("attitude = \(attitude)") + print("currentCamera = \(strongSelf.currentCamera)") +// let newCamera = camera.rotate(toHeading: attitude.yaw, pitch: attitude.roll + 90, roll: attitude.pitch) +// sceneView.setViewpointCamera(newCamera) + // print("updating device motion: \(finalQuat)") //use `finalQuat` to update position/orientation of camera + + + /* + [self.motionManager startDeviceMotionUpdatesToQueue: motionQueue withHandler: ^(CMDeviceMotion *motion, NSError *error) { + + CMQuaternion quat = weakSelf.motionManager.deviceMotion.attitude.quaternion; + simd_quatf curentQuat = simd_quaternion((float)quat.x, (float)quat.y, (float)quat.z, (float)quat.w); + + simd_quatf finalQuat = simd_mul(curentQuat, _orientationQuat); + + [weakSelf didUpdateRelativePositionWithDeltaX:0 + deltaY:0 + deltaZ:0 + deltaRotationX:finalQuat.vector.x + deltaRotationY:finalQuat.vector.y + deltaRotationZ:finalQuat.vector.z + deltaRotationW:finalQuat.vector.w + ignoreInitialHeading:NO]; + }]; +*/ } } @@ -467,14 +525,45 @@ extension ArcGISARSensorView: CLLocationManagerDelegate { let locationPoint = AGSPoint(x: location.coordinate.longitude, y: location.coordinate.latitude, - z: location.altitude, + z: location.altitude + 100, spatialReference: .wgs84()) + + if initialLocation == nil { + initialLocation = locationPoint + currentCamera = AGSCamera(location: locationPoint, heading: 0, pitch: 0, roll: 0) + sceneView.setViewpointCamera(currentCamera) + finalizeStart() + } else { + let camera = sceneView.currentViewpointCamera() + currentCamera = camera.move(toLocation: locationPoint) +// sceneView.setViewpointCamera(currentCamera) + } // let camera = AGSCamera(location: locationPoint, heading: 0.0, pitch: 0.0, roll: 0.0) // sceneView.setViewpointCamera(camera) // finalizeStart() // is this needed? // print("updating location: \(locationPoint)") + + /* + -(void)locationManager:(CLLocationManager *)manager didUpdateLocations:(NSArray *)locations { + CLLocation *newLocation = [locations lastObject]; + + // invalid location if hAcc negative + if (newLocation.horizontalAccuracy < 0 || !newLocation) { + return; + } + if (newLocation.verticalAccuracy >= 0) + [self moveInitialPositionToLatitude:newLocation.coordinate.latitude longitude:newLocation.coordinate.longitude altitude:newLocation.altitude]; + else + [self moveInitialPositionToLatitude:newLocation.coordinate.latitude longitude:newLocation.coordinate.longitude]; + + if (!_initialLocation) { + _initialLocation = YES; + [self finalizeStart]; + } + } +*/ } /* diff --git a/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift b/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift index aa5fe880..60e6a245 100644 --- a/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift +++ b/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift @@ -14,7 +14,7 @@ import UIKit import ARKit -import ArcGIS +//import ArcGIS public class ArcGISARView: UIView { @@ -49,7 +49,8 @@ public class ArcGISARView: UIView { // initial location from locationManager private var initialLocation: CLLocation? private var horizontalAccuracy: CLLocationAccuracy = .greatestFiniteMagnitude; - + private var initialTransformationMatrix: AGSTransformationMatrix = AGSTransformationMatrix() + // is ARKit supported on this device? private var isSupported = false @@ -60,6 +61,10 @@ public class ArcGISARView: UIView { var frameCount:Int = 0 var frameCountTimer: Timer? + // compensate the pitch beeing 90 degrees on ARKit + let compensationQuat:simd_quatf = simd_quaternion(Float(sin(45 / (180 / Float.pi))), 0, 0, Float(cos(45 / (180 / Float.pi)))) + var orientationQuat:simd_quatf = simd_quaternion(0, 0, 0, 0) + // MARK: Initializers public override init(frame: CGRect) { @@ -93,9 +98,14 @@ public class ArcGISARView: UIView { // make our sceneView's background transparent sceneView.isBackgroundTransparent = true sceneView.atmosphereEffect = .none - sceneView.isManualRendering = true + sceneView.isManualRendering = false notifiedStartOrFailure = false + + orientationChanged(notification: nil) + + //figure out how to do this better: + arConfiguration.worldAlignment = .gravityAndHeading } // MARK: Public @@ -143,6 +153,14 @@ public class ArcGISARView: UIView { } } + UIDevice.current.beginGeneratingDeviceOrientationNotifications() + NotificationCenter.default.addObserver( + self, + selector: #selector(self.orientationChanged(notification:)), + name: UIDevice.orientationDidChangeNotification, + object: nil + ) + // reset frameCount and start timer to capture frame rate frameCount = 0 frameCountTimer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true, block: { [weak self] (timer) in @@ -155,6 +173,9 @@ public class ArcGISARView: UIView { arSCNView.session.pause() stopUpdatingLocationAndHeading() + UIDevice.current.endGeneratingDeviceOrientationNotifications() + NotificationCenter.default.removeObserver(self) + frameCountTimer?.invalidate() } @@ -237,7 +258,56 @@ public class ArcGISARView: UIView { // need to start location manager updates startUpdatingLocationAndHeading() } + + /* + //set initial orientation + [self didChangeOrientation:nil]; + + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(didChangeOrientation:) + name:UIApplicationDidChangeStatusBarOrientationNotification + object:nil]; + - (void)didChangeOrientation:(NSNotification *)note{ + switch ([[UIApplication sharedApplication]statusBarOrientation]) { + case UIInterfaceOrientationPortraitUpsideDown: + _orientationQuat = simd_quaternion(0, 0, -(float)sqrt(0.5), (float)sqrt(0.5)); + break; + case UIInterfaceOrientationPortrait: + _orientationQuat = simd_quaternion(0, 0, (float)sqrt(0.5), (float)sqrt(0.5)); + break; + case UIInterfaceOrientationLandscapeLeft: + #warning - this is different than the Xamarin version + _orientationQuat = simd_quaternion(0, 0, (float)1, 0); + break; + case UIInterfaceOrientationLandscapeRight: + #warning - this is different than the Xamarin version + _orientationQuat = simd_quaternion(0, 0, 0, (float)1); + break; + default: + break; + } + _needResetFOV = YES; + } +*/ + + // Called when device orientation changes + @objc func orientationChanged(notification: Notification?) { + // handle rotation here + switch UIApplication.shared.statusBarOrientation { + case .landscapeLeft: + orientationQuat = simd_quaternion(0, 0, 1.0, 0); + case .landscapeRight: + orientationQuat = simd_quaternion(0, 0, 0, 1.0); + case .portrait: + orientationQuat = simd_quaternion(0, 0, sqrt(0.5), sqrt(0.5)); + case .portraitUpsideDown: + orientationQuat = simd_quaternion(0, 0, -sqrt(0.5), sqrt(0.5)); + default: + break + } + } + // MARK: Errors class func notSupportedError() -> NSError { let userInfo = [NSLocalizedDescriptionKey : "The device does not support ARKit functionality."] @@ -255,6 +325,9 @@ public class ArcGISARView: UIView { } } +var once = true +var compensationApplied = false + // MARK: - ARSessionDelegate extension ArcGISARView: ARSessionDelegate { // AR session delegate methods @@ -267,12 +340,159 @@ extension ArcGISARView: ARSessionDelegate { */ public func session(_ session: ARSession, didUpdate frame: ARFrame) { // TODO: updateCamera()..... -// print("didUpdateFrame...") + // print("didUpdateFrame...") delegate?.session?(session, didUpdate: frame) - self.sceneView.renderFrame() + + /* + if (currentFrame != nil) { + matrix_float4x4 transform = currentFrame.camera.transform; + simd_quatf finalQuat = simd_mul(simd_mul(_compensationQuat, simd_quaternion(transform)), _orientationQuat); + + [self didUpdateRelativePositionWithDeltaX:transform.columns[3].x + deltaY:-transform.columns[3].z + deltaZ:transform.columns[3].y + deltaRotationX:finalQuat.vector.x + deltaRotationY:finalQuat.vector.y + deltaRotationZ:finalQuat.vector.z + deltaRotationW:finalQuat.vector.w + ignoreInitialHeading:NO]; + } + */ + + guard let currentFrame = session.currentFrame else { return } + + let timeDiff = currentFrame.timestamp - frame.timestamp + print("timeDiff between currentFrame and didUpdate frame: \(timeDiff)") + + // create transformation matrix + let cameraTransform = currentFrame.camera.transform +// let cameraTransform = frame.camera.transform + + // set FOV from ARKit camera.projectionMatrix + let projectionMatrix = currentFrame.camera.projectionMatrix + let verticalElement = projectionMatrix.columns.0.x + let horizontalElement = projectionMatrix.columns.1.y + sceneView.setFieldOfViewFromProjection(Double(verticalElement), horizontalElement: Double(horizontalElement)) + + let finalQuat:simd_quatf = simd_mul(simd_mul(compensationQuat, simd_quaternion(cameraTransform)), orientationQuat) + + + + var transformationMatrix = AGSTransformationMatrix(quaternionX: Double(finalQuat.vector.x), + quaternionY: Double(finalQuat.vector.y), + quaternionZ: Double(finalQuat.vector.z), + quaternionW: Double(finalQuat.vector.w), + translationX: Double(cameraTransform.columns.3.x), + translationY: Double(-cameraTransform.columns.3.z), + translationZ: Double(cameraTransform.columns.3.y)) + + transformationMatrix = initialTransformationMatrix.addTransformation(transformationMatrix) + + // let currentTransformationMatrix = sceneView.currentViewpointCamera().transformationMatrix + // transformationMatrix = currentTransformationMatrix.addTransformation(transformationMatrix) +// print("transformation values: tX = \(transformationMatrix.translationX); tY = \(transformationMatrix.translationY); tZ = \(transformationMatrix.translationZ); qX = \(transformationMatrix.quaternionX); qY = \(transformationMatrix.quaternionY); qZ = \(transformationMatrix.quaternionZ); qW = = \(transformationMatrix.quaternionW)") + let camera = AGSCamera(transformationMatrix: transformationMatrix) + print("camera heading: \(camera.heading), pitch = \(camera.pitch), roll = \(camera.roll), location = \(camera.location)") + + sceneView.setViewpointCamera(camera) + + // let svCamera = sceneView.currentViewpointCamera() + // print("sceneView.Camera heading: \(svCamera.heading), pitch = \(svCamera.pitch), roll = \(svCamera.roll), location = \(svCamera.location)") + +// Thread.sleep(forTimeInterval: 0.25) + sceneView.renderFrame() frameCount = frameCount + 1 +// Thread.sleep(forTimeInterval: 0.25) + +// sleep(5) } - + /* + public func session(_ session: ARSession, didUpdate frame: ARFrame) { + // TODO: updateCamera()..... + // print("didUpdateFrame...") + delegate?.session?(session, didUpdate: frame) + + /* + if (currentFrame != nil) { + matrix_float4x4 transform = currentFrame.camera.transform; + simd_quatf finalQuat = simd_mul(simd_mul(_compensationQuat, simd_quaternion(transform)), _orientationQuat); + + [self didUpdateRelativePositionWithDeltaX:transform.columns[3].x + deltaY:-transform.columns[3].z + deltaZ:transform.columns[3].y + deltaRotationX:finalQuat.vector.x + deltaRotationY:finalQuat.vector.y + deltaRotationZ:finalQuat.vector.z + deltaRotationW:finalQuat.vector.w + ignoreInitialHeading:NO]; + } + */ + + // create transformation matrix + let cameraTransform = frame.camera.transform + // let finalQuat:simd_quatf = simd_mul(simd_mul(compensationQuat, simd_quaternion(cameraTransform)), orientationQuat) + let finalQuat:simd_quatf = simd_quaternion(cameraTransform) + + var transformationMatrix = AGSTransformationMatrix(translationX: Double(cameraTransform.columns.3.x), + translationY: Double(-cameraTransform.columns.3.z), + translationZ: Double(cameraTransform.columns.3.y), + quaternionX: Double(finalQuat.vector.x), + quaternionY: Double(finalQuat.vector.y), + quaternionZ: Double(finalQuat.vector.z), + quaternionW: Double(finalQuat.vector.w)) + print("transformation values: tX = \(Double(cameraTransform.columns.3.x)); tY = \(Double(-cameraTransform.columns.3.z)); tZ = \(Double(cameraTransform.columns.3.y)); qX = \(Double(finalQuat.vector.x)); qY = \(Double(finalQuat.vector.y)); qZ = \(Double(finalQuat.vector.z)); qW = = \(Double(finalQuat.vector.w))") + + // //this gives the same location, no matter what... + // var transformationMatrix = AGSTransformationMatrix(translationX: 0.0, + // translationY: 0.0, + // translationZ: 0.0, + // quaternionX: 0.0, + // quaternionY: 0.0, + // quaternionZ: 0.0, + // quaternionW: 1.0) + + + if !compensationApplied { + compensationApplied = true + let adjustmentQuat:simd_quatf = simd_mul(compensationQuat, orientationQuat); + + var compensationMatrix = AGSTransformationMatrix(translationX: 0.0, + translationY: 0.0, + translationZ: 0.0, + quaternionX: Double(adjustmentQuat.vector.x), + quaternionY: Double(adjustmentQuat.vector.y), + quaternionZ: Double(adjustmentQuat.vector.z), + quaternionW: Double(adjustmentQuat.vector.w)) + + let currentTransformationMatrix = sceneView.currentViewpointCamera().transformationMatrix + compensationMatrix = currentTransformationMatrix.addTransformation(compensationMatrix) + let camera = AGSCamera(transformationMatrix: compensationMatrix) + // print("camera heading: \(camera.heading), pitch = \(camera.pitch), roll = \(camera.roll), location = \(camera.location)") + sceneView.setViewpointCamera(camera) + } + + // var transformationMatrix = AGSTransformationMatrix(translationX: Double(cameraTransform.columns.3.x), + // translationY: Double(-cameraTransform.columns.3.z), + // translationZ: Double(cameraTransform.columns.3.y), + // quaternionX: Double(finalQuat.vector.x), + // quaternionY: Double(finalQuat.vector.y), + // quaternionZ: Double(finalQuat.vector.z), + // quaternionW: Double(finalQuat.vector.w)) + + let currentTransformationMatrix = sceneView.currentViewpointCamera().transformationMatrix + transformationMatrix = currentTransformationMatrix.addTransformation(transformationMatrix) + let camera = AGSCamera(transformationMatrix: transformationMatrix) + print("camera heading: \(camera.heading), pitch = \(camera.pitch), roll = \(camera.roll), location = \(camera.location)") + + sceneView.setViewpointCamera(camera) + + // let svCamera = sceneView.currentViewpointCamera() + // print("sceneView.Camera heading: \(svCamera.heading), pitch = \(svCamera.pitch), roll = \(svCamera.roll), location = \(svCamera.location)") + + sceneView.renderFrame() + frameCount = frameCount + 1 + } +*/ /** This is called when new anchors are added to the session. @@ -413,6 +633,8 @@ extension ArcGISARView: ARSessionObserver { } } +var pointTimer: Timer? + // MARK: - CLLocationManagerDelegate extension ArcGISARView: CLLocationManagerDelegate { /* @@ -437,8 +659,13 @@ extension ArcGISARView: CLLocationManagerDelegate { z: location.altitude, spatialReference: .wgs84()) let camera = AGSCamera(location: locationPoint, heading: 0.0, pitch: 0.0, roll: 0.0) + initialTransformationMatrix = camera.transformationMatrix sceneView.setViewpointCamera(camera) + pointTimer = Timer.scheduledTimer(withTimeInterval: 5.0, repeats: false, block: { [weak self] (timer) in + self?.addPointToScene(camera: (self?.sceneView.currentViewpointCamera())!) + }) + finalizeStart() } else if location.horizontalAccuracy < horizontalAccuracy { @@ -449,6 +676,30 @@ extension ArcGISARView: CLLocationManagerDelegate { print("didUpdateLocations...") } + private func addPointToScene(camera: AGSCamera) { + + +// let location = AGSPoint(x: camera.location.x, y: camera.location.y, z: camera.location.z + 10, spatialReference: camera.location.spatialReference) +// // get camera forward 5 meters + let newerCamera = camera.moveForward(withDistance: 1.0) + let location = newerCamera.location + +// var builder = AGSPointBuilder(point: camera.location) +// builder.offsetBy(x: -0.0001, y: 0.0).z = camera.location.z + 150 +// let location = builder.toGeometry() +// +// let distance = AGSLocationDistanceMeasurement(startLocation: camera.location, endLocation: location).directDistance +// print("distance = \(String(reflecting: distance))") + let go = AGSGraphicsOverlay() + go.sceneProperties = AGSLayerSceneProperties(surfacePlacement: .absolute) + sceneView.graphicsOverlays.add(go) + + let markerSymbol = AGSSimpleMarkerSceneSymbol(style: .diamond, color: .blue, height: 0.1, width: 0.1, depth: 0.1, anchorPosition: .bottom) + + let graphic = AGSGraphic(geometry: location, symbol: markerSymbol, attributes: nil) + go.graphics.add(graphic) + } + /* * locationManager:didUpdateHeading: * From 90cbdf453f493a3dee5fe4232e280fef3c4e1395 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Thu, 23 May 2019 15:18:45 -0500 Subject: [PATCH 016/147] Updates to ArcGISARView for testing renderFrame --- .../ArcGISToolkitExamples/ARExample.swift | 93 -------- .../ArcGISToolkit/AR/ArcGISARSensorView.swift | 102 ++------- Toolkit/ArcGISToolkit/AR/ArcGISARView.swift | 198 +++--------------- 3 files changed, 49 insertions(+), 344 deletions(-) diff --git a/Examples/ArcGISToolkitExamples/ARExample.swift b/Examples/ArcGISToolkitExamples/ARExample.swift index dcdfd0ae..b9a4cfd3 100644 --- a/Examples/ArcGISToolkitExamples/ARExample.swift +++ b/Examples/ArcGISToolkitExamples/ARExample.swift @@ -17,11 +17,6 @@ import UIKit open class ARExample: UIViewController { - var featureLayer: AGSFeatureLayer? - let graphicsOverlay = AGSGraphicsOverlay() - let basemapSwitch = UISwitch(frame: .zero) - let graphicsSwitch = UISwitch(frame: .zero) - public let arView = ArcGISARView(frame: CGRect.zero) // public let arView = ArcGISARView(renderVideoFeed: false) // public let arView = ArcGISARSensorView(renderVideoFeed: true) @@ -36,30 +31,6 @@ open class ARExample: UIViewController { arView.sceneView.scene = scene() // arView.sceneView.alpha = 0.5 - // option to turn background on/off - basemapSwitch.isOn = true - - basemapSwitch.translatesAutoresizingMaskIntoConstraints = false - arView.sceneView.addSubview(basemapSwitch) - NSLayoutConstraint.activate([ - basemapSwitch.trailingAnchor.constraint(equalTo: arView.sceneView.trailingAnchor, constant: -24), - basemapSwitch.topAnchor.constraint(equalTo: arView.sceneView.topAnchor, constant: 88) - ]) - basemapSwitch.isOn = arView.sceneView.isBackgroundTransparent - basemapSwitch.addTarget(self, action: #selector(switchBasemap), for: .valueChanged) - - // option to turn background on/off - graphicsSwitch.isOn = false - - graphicsSwitch.translatesAutoresizingMaskIntoConstraints = false - arView.sceneView.addSubview(graphicsSwitch) - NSLayoutConstraint.activate([ - graphicsSwitch.trailingAnchor.constraint(equalTo: arView.sceneView.trailingAnchor, constant: -24), - graphicsSwitch.topAnchor.constraint(equalTo: basemapSwitch.bottomAnchor, constant: 12) - ]) - graphicsSwitch.isOn = arView.sceneView.isBackgroundTransparent - graphicsSwitch.addTarget(self, action: #selector(switchGraphics), for: .valueChanged) - } override open func viewDidAppear(_ animated: Bool) { @@ -84,72 +55,8 @@ open class ARExample: UIViewController { surface.isEnabled = true surface.backgroundGrid.isVisible = false scene.baseSurface = surface - - // add data #1 -// let ft = AGSServiceFeatureTable(url: URL(string: "https://services2.arcgis.com/2B0gmGCMCH3iKkax/arcgis/rest/services/MinneapolisStPaulPOI/FeatureServer/0")!) -// featureLayer = AGSFeatureLayer(featureTable: ft) -//// arView.sceneView.scene?.operationalLayers.add(featureLayer!) -// addGraphicsToOverlay() - - // add data #2 -// let sceneLayer = AGSArcGISSceneLayer(name: "sandiegostage") -// arView.sceneView.scene?.operationalLayers.add(sceneLayer) return scene } - - func addGraphicsToOverlay() { - // add graphics overlay to scene - - arView.sceneView.graphicsOverlays.add(graphicsOverlay) - let qp = AGSQueryParameters() - - // upperleft: 44.950751; -93.323193 - // lr: 44.929669, -93.287659 - - let envelope = AGSEnvelope(xMin: -93.323193, yMin: 44.929669, xMax: -93.287659, yMax: 44.950751, spatialReference: .wgs84()) - qp.geometry = envelope - qp.spatialRelationship = .contains - - // self.featureLayer?.selectFeatures(withQuery: qp, mode: .new, completion: { (queryResult, error) in - self.featureLayer?.featureTable?.load(completion: { (error) in - self.featureLayer?.featureTable?.queryFeatures(with: qp, completion: { (queryResult, error) in - if let queryResult = queryResult { - let markerSymbol = AGSSimpleMarkerSceneSymbol(style: .diamond, color: .blue, height: 50, width: 50, depth: 50, anchorPosition: .bottom) - for feature in queryResult.featureEnumerator() { - if let feature = feature as? AGSArcGISFeature { - feature.load(completion: { (error) in - guard error == nil else { return } - let compositeSymbol = AGSCompositeSymbol() - let text = feature.attributes.object(forKey: "Name") as? String ?? "" - let textSymbol = AGSTextSymbol(text: text, color: .red, size: 36.0, horizontalAlignment: .center, verticalAlignment: .bottom) - - compositeSymbol.symbols = [markerSymbol, textSymbol] - if let featurePoint = feature.geometry as? AGSPoint { - let point = AGSPoint(x: featurePoint.x, y: featurePoint.y, z: 100, spatialReference: feature.geometry?.spatialReference) - let graphic = AGSGraphic(geometry: point, symbol: compositeSymbol, attributes: feature.attributes as? [String : Any]) - self.graphicsOverlay.graphics.add(graphic) - } - }) - } - } - } - }) - }) - } - - @objc func switchBasemap() { - arView.sceneView.scene?.basemap?.baseLayers.forEach({ (baseLayer) in - guard let layer = baseLayer as? AGSLayer else { return } - layer.isVisible = basemapSwitch.isOn - arView.sceneView.scene?.baseSurface?.backgroundGrid.isVisible = basemapSwitch.isOn - }) - // sceneView.atmosphereEffect = backgroundSwitch.isOn ? .none : .realistic - } - - @objc func switchGraphics() { - guard let overlay = arView.sceneView.graphicsOverlays.firstObject as? AGSGraphicsOverlay else { return } - overlay.isVisible = graphicsSwitch.isOn - } } diff --git a/Toolkit/ArcGISToolkit/AR/ArcGISARSensorView.swift b/Toolkit/ArcGISToolkit/AR/ArcGISARSensorView.swift index bf62658c..ef0cef50 100644 --- a/Toolkit/ArcGISToolkit/AR/ArcGISARSensorView.swift +++ b/Toolkit/ArcGISToolkit/AR/ArcGISARSensorView.swift @@ -15,7 +15,7 @@ import UIKit import AVFoundation import CoreMotion -//import ArcGIS +import ArcGIS public enum LocationType { case anglesOnly @@ -140,9 +140,6 @@ public class ArcGISARSensorView: UIView { startWithAccessAuthorized() } -// // start motion manager -// startUpdatingLocationAndHeading() - if renderVideoFeed { setupSession() } @@ -254,12 +251,6 @@ public class ArcGISARSensorView: UIView { private func finalizeStart() { // TODO: is there anything to do here? -// updateTimer = Timer.scheduledTimer(withTimeInterval: 1 / 10, repeats: true, block: { [weak self] (timer) in -// guard let strongSelf = self else { return } -// print("currentCamera = \(strongSelf.currentCamera)") -// strongSelf.sceneView.setViewpointCamera(strongSelf.currentCamera) -// }) - } private func startUpdatingAngles() { @@ -270,58 +261,21 @@ public class ArcGISARSensorView: UIView { motionManager.startDeviceMotionUpdates(to: motionQueue) { [weak self] (motion, error) in guard let strongSelf = self else { return } -// guard let quat = self?.motionManager.deviceMotion?.attitude.quaternion, -// let orientationQuat = self?.orientationQuat else { return } -// let currentQuat = simd_quaternion(Float(quat.x), Float(quat.y), Float(quat.z), Float(quat.w)) -// let finalQuat = simd_mul(currentQuat, orientationQuat) - - - guard let attitude = strongSelf.motionManager.deviceMotion?.attitude else { return } - - // Landscape - let heading = 360 - attitude.yaw * 180 / .pi - var pitch = -attitude.roll * 180.0 / .pi - pitch = pitch < 0 ? pitch + 180.0 : pitch - let roll = 360 - attitude.pitch * 180.0 / .pi - - // portrait - this doesn't work -// let heading = (atan2(motion!.gravity.x, motion!.gravity.y) - .pi) * 180.0 / .pi //0.0 //360 - attitude.yaw * 180 / .pi -// var pitch = 90.0//360 - attitude.pitch * 180.0 / .pi -//// pitch = pitch < 0 ? pitch + 180.0 : pitch -// let roll = 0.0 //-attitude.roll * 180.0 / .pi//-attitude.yaw * 180 / .pi//360 - attitude.pitch * 180.0 / .pi - - let camera = strongSelf.sceneView.currentViewpointCamera() - //landscape - strongSelf.currentCamera = camera.rotate(toHeading: heading, pitch: pitch, roll: roll) + guard let quat = self?.motionManager.deviceMotion?.attitude.quaternion, + let orientationQuat = self?.orientationQuat else { return } + let currentQuat = simd_quaternion(Float(quat.x), Float(quat.y), Float(quat.z), Float(quat.w)) + let finalQuat = simd_mul(currentQuat, orientationQuat) - strongSelf.sceneView.setViewpointCamera(strongSelf.currentCamera) - print("attitude = \(attitude)") - print("currentCamera = \(strongSelf.currentCamera)") -// let newCamera = camera.rotate(toHeading: attitude.yaw, pitch: attitude.roll + 90, roll: attitude.pitch) -// sceneView.setViewpointCamera(newCamera) - -// print("updating device motion: \(finalQuat)") - //use `finalQuat` to update position/orientation of camera - - - /* - [self.motionManager startDeviceMotionUpdatesToQueue: motionQueue withHandler: ^(CMDeviceMotion *motion, NSError *error) { - - CMQuaternion quat = weakSelf.motionManager.deviceMotion.attitude.quaternion; - simd_quatf curentQuat = simd_quaternion((float)quat.x, (float)quat.y, (float)quat.z, (float)quat.w); - - simd_quatf finalQuat = simd_mul(curentQuat, _orientationQuat); - - [weakSelf didUpdateRelativePositionWithDeltaX:0 - deltaY:0 - deltaZ:0 - deltaRotationX:finalQuat.vector.x - deltaRotationY:finalQuat.vector.y - deltaRotationZ:finalQuat.vector.z - deltaRotationW:finalQuat.vector.w - ignoreInitialHeading:NO]; - }]; -*/ + // Old beta code to update heading... +// [weakSelf didUpdateRelativePositionWithDeltaX:0 +// deltaY:0 +// deltaZ:0 +// deltaRotationX:finalQuat.vector.x +// deltaRotationY:finalQuat.vector.y +// deltaRotationZ:finalQuat.vector.z +// deltaRotationW:finalQuat.vector.w +// ignoreInitialHeading:NO]; +// }]; } } @@ -538,32 +492,6 @@ extension ArcGISARSensorView: CLLocationManagerDelegate { currentCamera = camera.move(toLocation: locationPoint) // sceneView.setViewpointCamera(currentCamera) } -// let camera = AGSCamera(location: locationPoint, heading: 0.0, pitch: 0.0, roll: 0.0) -// sceneView.setViewpointCamera(camera) - -// finalizeStart() // is this needed? - -// print("updating location: \(locationPoint)") - - /* - -(void)locationManager:(CLLocationManager *)manager didUpdateLocations:(NSArray *)locations { - CLLocation *newLocation = [locations lastObject]; - - // invalid location if hAcc negative - if (newLocation.horizontalAccuracy < 0 || !newLocation) { - return; - } - if (newLocation.verticalAccuracy >= 0) - [self moveInitialPositionToLatitude:newLocation.coordinate.latitude longitude:newLocation.coordinate.longitude altitude:newLocation.altitude]; - else - [self moveInitialPositionToLatitude:newLocation.coordinate.latitude longitude:newLocation.coordinate.longitude]; - - if (!_initialLocation) { - _initialLocation = YES; - [self finalizeStart]; - } - } -*/ } /* diff --git a/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift b/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift index 60e6a245..e0450f1c 100644 --- a/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift +++ b/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift @@ -14,7 +14,7 @@ import UIKit import ARKit -//import ArcGIS +import ArcGIS public class ArcGISARView: UIView { @@ -259,38 +259,6 @@ public class ArcGISARView: UIView { startUpdatingLocationAndHeading() } - /* - //set initial orientation - [self didChangeOrientation:nil]; - - [[NSNotificationCenter defaultCenter] addObserver:self - selector:@selector(didChangeOrientation:) - name:UIApplicationDidChangeStatusBarOrientationNotification - object:nil]; - - - (void)didChangeOrientation:(NSNotification *)note{ - switch ([[UIApplication sharedApplication]statusBarOrientation]) { - case UIInterfaceOrientationPortraitUpsideDown: - _orientationQuat = simd_quaternion(0, 0, -(float)sqrt(0.5), (float)sqrt(0.5)); - break; - case UIInterfaceOrientationPortrait: - _orientationQuat = simd_quaternion(0, 0, (float)sqrt(0.5), (float)sqrt(0.5)); - break; - case UIInterfaceOrientationLandscapeLeft: - #warning - this is different than the Xamarin version - _orientationQuat = simd_quaternion(0, 0, (float)1, 0); - break; - case UIInterfaceOrientationLandscapeRight: - #warning - this is different than the Xamarin version - _orientationQuat = simd_quaternion(0, 0, 0, (float)1); - break; - default: - break; - } - _needResetFOV = YES; - } -*/ - // Called when device orientation changes @objc func orientationChanged(notification: Notification?) { // handle rotation here @@ -339,45 +307,35 @@ extension ArcGISARView: ARSessionDelegate { @param frame The frame that has been updated. */ public func session(_ session: ARSession, didUpdate frame: ARFrame) { - // TODO: updateCamera()..... - // print("didUpdateFrame...") + delegate?.session?(session, didUpdate: frame) - /* - if (currentFrame != nil) { - matrix_float4x4 transform = currentFrame.camera.transform; - simd_quatf finalQuat = simd_mul(simd_mul(_compensationQuat, simd_quaternion(transform)), _orientationQuat); - - [self didUpdateRelativePositionWithDeltaX:transform.columns[3].x - deltaY:-transform.columns[3].z - deltaZ:transform.columns[3].y - deltaRotationX:finalQuat.vector.x - deltaRotationY:finalQuat.vector.y - deltaRotationZ:finalQuat.vector.z - deltaRotationW:finalQuat.vector.w - ignoreInitialHeading:NO]; - } - */ - - guard let currentFrame = session.currentFrame else { return } - - let timeDiff = currentFrame.timestamp - frame.timestamp - print("timeDiff between currentFrame and didUpdate frame: \(timeDiff)") + // + // Debug - here's the switch between currentFrame and frame... + // // create transformation matrix + guard let currentFrame = session.currentFrame else { return } let cameraTransform = currentFrame.camera.transform // let cameraTransform = frame.camera.transform + // + // Debug - calculate and display time difference between frame and currentFrame + // +// let timeDiff = currentFrame.timestamp - frame.timestamp +// print("timeDiff between currentFrame and didUpdate frame: \(timeDiff)") + + // + // Future... + // // set FOV from ARKit camera.projectionMatrix - let projectionMatrix = currentFrame.camera.projectionMatrix - let verticalElement = projectionMatrix.columns.0.x - let horizontalElement = projectionMatrix.columns.1.y - sceneView.setFieldOfViewFromProjection(Double(verticalElement), horizontalElement: Double(horizontalElement)) + // +// let projectionMatrix = currentFrame.camera.projectionMatrix +// let verticalElement = projectionMatrix.columns.0.x +// let horizontalElement = projectionMatrix.columns.1.y +// sceneView.setFieldOfViewFromProjection(Double(verticalElement), horizontalElement: Double(horizontalElement)) let finalQuat:simd_quatf = simd_mul(simd_mul(compensationQuat, simd_quaternion(cameraTransform)), orientationQuat) - - - var transformationMatrix = AGSTransformationMatrix(quaternionX: Double(finalQuat.vector.x), quaternionY: Double(finalQuat.vector.y), quaternionZ: Double(finalQuat.vector.z), @@ -403,96 +361,8 @@ extension ArcGISARView: ARSessionDelegate { sceneView.renderFrame() frameCount = frameCount + 1 // Thread.sleep(forTimeInterval: 0.25) - -// sleep(5) } - /* - public func session(_ session: ARSession, didUpdate frame: ARFrame) { - // TODO: updateCamera()..... - // print("didUpdateFrame...") - delegate?.session?(session, didUpdate: frame) - - /* - if (currentFrame != nil) { - matrix_float4x4 transform = currentFrame.camera.transform; - simd_quatf finalQuat = simd_mul(simd_mul(_compensationQuat, simd_quaternion(transform)), _orientationQuat); - - [self didUpdateRelativePositionWithDeltaX:transform.columns[3].x - deltaY:-transform.columns[3].z - deltaZ:transform.columns[3].y - deltaRotationX:finalQuat.vector.x - deltaRotationY:finalQuat.vector.y - deltaRotationZ:finalQuat.vector.z - deltaRotationW:finalQuat.vector.w - ignoreInitialHeading:NO]; - } - */ - - // create transformation matrix - let cameraTransform = frame.camera.transform - // let finalQuat:simd_quatf = simd_mul(simd_mul(compensationQuat, simd_quaternion(cameraTransform)), orientationQuat) - let finalQuat:simd_quatf = simd_quaternion(cameraTransform) - - var transformationMatrix = AGSTransformationMatrix(translationX: Double(cameraTransform.columns.3.x), - translationY: Double(-cameraTransform.columns.3.z), - translationZ: Double(cameraTransform.columns.3.y), - quaternionX: Double(finalQuat.vector.x), - quaternionY: Double(finalQuat.vector.y), - quaternionZ: Double(finalQuat.vector.z), - quaternionW: Double(finalQuat.vector.w)) - print("transformation values: tX = \(Double(cameraTransform.columns.3.x)); tY = \(Double(-cameraTransform.columns.3.z)); tZ = \(Double(cameraTransform.columns.3.y)); qX = \(Double(finalQuat.vector.x)); qY = \(Double(finalQuat.vector.y)); qZ = \(Double(finalQuat.vector.z)); qW = = \(Double(finalQuat.vector.w))") - - // //this gives the same location, no matter what... - // var transformationMatrix = AGSTransformationMatrix(translationX: 0.0, - // translationY: 0.0, - // translationZ: 0.0, - // quaternionX: 0.0, - // quaternionY: 0.0, - // quaternionZ: 0.0, - // quaternionW: 1.0) - - - if !compensationApplied { - compensationApplied = true - let adjustmentQuat:simd_quatf = simd_mul(compensationQuat, orientationQuat); - - var compensationMatrix = AGSTransformationMatrix(translationX: 0.0, - translationY: 0.0, - translationZ: 0.0, - quaternionX: Double(adjustmentQuat.vector.x), - quaternionY: Double(adjustmentQuat.vector.y), - quaternionZ: Double(adjustmentQuat.vector.z), - quaternionW: Double(adjustmentQuat.vector.w)) - - let currentTransformationMatrix = sceneView.currentViewpointCamera().transformationMatrix - compensationMatrix = currentTransformationMatrix.addTransformation(compensationMatrix) - let camera = AGSCamera(transformationMatrix: compensationMatrix) - // print("camera heading: \(camera.heading), pitch = \(camera.pitch), roll = \(camera.roll), location = \(camera.location)") - sceneView.setViewpointCamera(camera) - } - - // var transformationMatrix = AGSTransformationMatrix(translationX: Double(cameraTransform.columns.3.x), - // translationY: Double(-cameraTransform.columns.3.z), - // translationZ: Double(cameraTransform.columns.3.y), - // quaternionX: Double(finalQuat.vector.x), - // quaternionY: Double(finalQuat.vector.y), - // quaternionZ: Double(finalQuat.vector.z), - // quaternionW: Double(finalQuat.vector.w)) - - let currentTransformationMatrix = sceneView.currentViewpointCamera().transformationMatrix - transformationMatrix = currentTransformationMatrix.addTransformation(transformationMatrix) - let camera = AGSCamera(transformationMatrix: transformationMatrix) - print("camera heading: \(camera.heading), pitch = \(camera.pitch), roll = \(camera.roll), location = \(camera.location)") - - sceneView.setViewpointCamera(camera) - - // let svCamera = sceneView.currentViewpointCamera() - // print("sceneView.Camera heading: \(svCamera.heading), pitch = \(svCamera.pitch), roll = \(svCamera.roll), location = \(svCamera.location)") - - sceneView.renderFrame() - frameCount = frameCount + 1 - } -*/ + /** This is called when new anchors are added to the session. @@ -633,6 +503,9 @@ extension ArcGISARView: ARSessionObserver { } } +// +// Debug +// var pointTimer: Timer? // MARK: - CLLocationManagerDelegate @@ -662,6 +535,9 @@ extension ArcGISARView: CLLocationManagerDelegate { initialTransformationMatrix = camera.transformationMatrix sceneView.setViewpointCamera(camera) + // + // Debug - schedule timer to add point to the scene after 5 seconds (so we're sure the camera is set up)... + // pointTimer = Timer.scheduledTimer(withTimeInterval: 5.0, repeats: false, block: { [weak self] (timer) in self?.addPointToScene(camera: (self?.sceneView.currentViewpointCamera())!) }) @@ -675,27 +551,21 @@ extension ArcGISARView: CLLocationManagerDelegate { print("didUpdateLocations...") } - + + // + // Debug - show point in front of camera... + // private func addPointToScene(camera: AGSCamera) { - -// let location = AGSPoint(x: camera.location.x, y: camera.location.y, z: camera.location.z + 10, spatialReference: camera.location.spatialReference) -// // get camera forward 5 meters - let newerCamera = camera.moveForward(withDistance: 1.0) - let location = newerCamera.location - -// var builder = AGSPointBuilder(point: camera.location) -// builder.offsetBy(x: -0.0001, y: 0.0).z = camera.location.z + 150 -// let location = builder.toGeometry() -// -// let distance = AGSLocationDistanceMeasurement(startLocation: camera.location, endLocation: location).directDistance -// print("distance = \(String(reflecting: distance))") let go = AGSGraphicsOverlay() go.sceneProperties = AGSLayerSceneProperties(surfacePlacement: .absolute) sceneView.graphicsOverlays.add(go) let markerSymbol = AGSSimpleMarkerSceneSymbol(style: .diamond, color: .blue, height: 0.1, width: 0.1, depth: 0.1, anchorPosition: .bottom) + // move camera forward 1 meters and get location + let location = camera.moveForward(withDistance: 1.0).location + let graphic = AGSGraphic(geometry: location, symbol: markerSymbol, attributes: nil) go.graphics.add(graphic) } From ee572db8a671687abefde635ac288991afb590ff Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Thu, 23 May 2019 16:07:53 -0500 Subject: [PATCH 017/147] Uncomment Toolkit and ArcGIS imports --- Examples/ArcGISToolkitExamples/ARExample.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Examples/ArcGISToolkitExamples/ARExample.swift b/Examples/ArcGISToolkitExamples/ARExample.swift index b9a4cfd3..25bcd895 100644 --- a/Examples/ArcGISToolkitExamples/ARExample.swift +++ b/Examples/ArcGISToolkitExamples/ARExample.swift @@ -12,8 +12,8 @@ // limitations under the License. import UIKit -//import ArcGISToolkit -//import ArcGIS +import ArcGISToolkit +import ArcGIS open class ARExample: UIViewController { From 2b35ea5e9b96e944f94e83894dbc1e5b809766e6 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Fri, 24 May 2019 10:28:47 -0500 Subject: [PATCH 018/147] use SCNView.pointOfView.transform to control AGSSceneView camera movement. --- Toolkit/ArcGISToolkit/AR/ArcGISARView.swift | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift b/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift index e0450f1c..76e95683 100644 --- a/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift +++ b/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift @@ -310,15 +310,28 @@ extension ArcGISARView: ARSessionDelegate { delegate?.session?(session, didUpdate: frame) + // // Debug - here's the switch between currentFrame and frame... // // create transformation matrix guard let currentFrame = session.currentFrame else { return } - let cameraTransform = currentFrame.camera.transform +// let cameraTransform = currentFrame.camera.transform // let cameraTransform = frame.camera.transform + // + // SCNRenderer point of View stuff + // + guard let pointOfView = arSCNView.pointOfView else { return } + let transform = pointOfView.transform + // let orientation = SCNVector3(-transform.m31, -transform.m32, transform.m33) + // let location = SCNVector3(transform.m41, transform.m42, transform.m43) + // let currentPositionOfCamera = orientation + location + // print(currentPositionOfCamera) + let cameraTransform = float4x4.init(transform) + + // // Debug - calculate and display time difference between frame and currentFrame // @@ -357,10 +370,10 @@ extension ArcGISARView: ARSessionDelegate { // let svCamera = sceneView.currentViewpointCamera() // print("sceneView.Camera heading: \(svCamera.heading), pitch = \(svCamera.pitch), roll = \(svCamera.roll), location = \(svCamera.location)") -// Thread.sleep(forTimeInterval: 0.25) +// Thread.sleep(forTimeInterval: 0.1) sceneView.renderFrame() frameCount = frameCount + 1 -// Thread.sleep(forTimeInterval: 0.25) +// Thread.sleep(forTimeInterval: 0.1) } /** From 1d722c5c69d9f207505ef44389c864729d769184 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Fri, 24 May 2019 16:02:06 -0500 Subject: [PATCH 019/147] Use willRenderScene method of SCNSceneRendererDelegate to control drawing of AGSSceneView (via renderFrame()). Move adding graphics layer and graphic to ARExample class. --- .../ArcGISToolkitExamples/ARExample.swift | 25 ++ Toolkit/ArcGISToolkit/AR/ArcGISARView.swift | 236 +++++++++++------- 2 files changed, 172 insertions(+), 89 deletions(-) diff --git a/Examples/ArcGISToolkitExamples/ARExample.swift b/Examples/ArcGISToolkitExamples/ARExample.swift index 25bcd895..c66b101c 100644 --- a/Examples/ArcGISToolkitExamples/ARExample.swift +++ b/Examples/ArcGISToolkitExamples/ARExample.swift @@ -31,6 +31,13 @@ open class ARExample: UIViewController { arView.sceneView.scene = scene() // arView.sceneView.alpha = 0.5 +// camera heading: 318.9702215288517, pitch = 52.69900468516913, roll = 0.6234908971981902, location = AGSPoint: (-93.298481, 44.940544, 274.055704, nan), sr: 4326 + + let originCamera = AGSCamera(latitude: 44.940544, longitude: -93.298481, altitude: 274.055704, heading: 270.0, pitch: 0.0, roll: 0.0) + arView.originCamera = originCamera + + let camera = AGSCamera(latitude: 44.940544, longitude: -93.298481, altitude: 274.055704, heading: 270.0, pitch: 90.0, roll: 0.0) + addPointToScene(camera: camera) } override open func viewDidAppear(_ animated: Bool) { @@ -58,5 +65,23 @@ open class ARExample: UIViewController { return scene } + + // + // Debug - show point in front of camera... + // + private func addPointToScene(camera: AGSCamera) { + + let go = AGSGraphicsOverlay() + go.sceneProperties = AGSLayerSceneProperties(surfacePlacement: .absolute) + arView.sceneView.graphicsOverlays.add(go) + + let markerSymbol = AGSSimpleMarkerSceneSymbol(style: .diamond, color: .blue, height: 0.1, width: 0.1, depth: 0.1, anchorPosition: .bottom) + + // move camera forward 1 meters and get location + let location = camera.moveForward(withDistance: 5.0).location + + let graphic = AGSGraphic(geometry: location, symbol: markerSymbol, attributes: nil) + go.graphics.add(graphic) + } } diff --git a/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift b/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift index 76e95683..f4971ade 100644 --- a/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift +++ b/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift @@ -89,6 +89,7 @@ public class ArcGISARView: UIView { addSubviewWithConstraints(arSCNView) arSCNView.session.delegate = self + (arSCNView as SCNSceneRenderer).delegate = self // // add sceneView to view and setup constraints @@ -138,6 +139,7 @@ public class ArcGISARView: UIView { // TODO: look at original beta code and grab locationmanager started stuff if let origin = originCamera { //set origin on sceneView??? + initialTransformationMatrix = origin.transformationMatrix sceneView.setViewpointCamera(origin) finalizeStart() } @@ -310,70 +312,70 @@ extension ArcGISARView: ARSessionDelegate { delegate?.session?(session, didUpdate: frame) - - // - // Debug - here's the switch between currentFrame and frame... - // - - // create transformation matrix - guard let currentFrame = session.currentFrame else { return } -// let cameraTransform = currentFrame.camera.transform -// let cameraTransform = frame.camera.transform - - // - // SCNRenderer point of View stuff - // - guard let pointOfView = arSCNView.pointOfView else { return } - let transform = pointOfView.transform - // let orientation = SCNVector3(-transform.m31, -transform.m32, transform.m33) - // let location = SCNVector3(transform.m41, transform.m42, transform.m43) - // let currentPositionOfCamera = orientation + location - // print(currentPositionOfCamera) - let cameraTransform = float4x4.init(transform) - - - // - // Debug - calculate and display time difference between frame and currentFrame - // -// let timeDiff = currentFrame.timestamp - frame.timestamp -// print("timeDiff between currentFrame and didUpdate frame: \(timeDiff)") - - // - // Future... - // - // set FOV from ARKit camera.projectionMatrix - // -// let projectionMatrix = currentFrame.camera.projectionMatrix -// let verticalElement = projectionMatrix.columns.0.x -// let horizontalElement = projectionMatrix.columns.1.y -// sceneView.setFieldOfViewFromProjection(Double(verticalElement), horizontalElement: Double(horizontalElement)) - - let finalQuat:simd_quatf = simd_mul(simd_mul(compensationQuat, simd_quaternion(cameraTransform)), orientationQuat) - var transformationMatrix = AGSTransformationMatrix(quaternionX: Double(finalQuat.vector.x), - quaternionY: Double(finalQuat.vector.y), - quaternionZ: Double(finalQuat.vector.z), - quaternionW: Double(finalQuat.vector.w), - translationX: Double(cameraTransform.columns.3.x), - translationY: Double(-cameraTransform.columns.3.z), - translationZ: Double(cameraTransform.columns.3.y)) - - transformationMatrix = initialTransformationMatrix.addTransformation(transformationMatrix) - - // let currentTransformationMatrix = sceneView.currentViewpointCamera().transformationMatrix - // transformationMatrix = currentTransformationMatrix.addTransformation(transformationMatrix) -// print("transformation values: tX = \(transformationMatrix.translationX); tY = \(transformationMatrix.translationY); tZ = \(transformationMatrix.translationZ); qX = \(transformationMatrix.quaternionX); qY = \(transformationMatrix.quaternionY); qZ = \(transformationMatrix.quaternionZ); qW = = \(transformationMatrix.quaternionW)") - let camera = AGSCamera(transformationMatrix: transformationMatrix) - print("camera heading: \(camera.heading), pitch = \(camera.pitch), roll = \(camera.roll), location = \(camera.location)") - - sceneView.setViewpointCamera(camera) - - // let svCamera = sceneView.currentViewpointCamera() - // print("sceneView.Camera heading: \(svCamera.heading), pitch = \(svCamera.pitch), roll = \(svCamera.roll), location = \(svCamera.location)") - -// Thread.sleep(forTimeInterval: 0.1) - sceneView.renderFrame() - frameCount = frameCount + 1 -// Thread.sleep(forTimeInterval: 0.1) +// +// // +// // Debug - here's the switch between currentFrame and frame... +// // +// +// // create transformation matrix +// guard let currentFrame = session.currentFrame else { return } +//// let cameraTransform = currentFrame.camera.transform +//// let cameraTransform = frame.camera.transform +// +// // +// // SCNRenderer point of View stuff +// // +// guard let pointOfView = arSCNView.pointOfView else { return } +// let transform = pointOfView.transform +// // let orientation = SCNVector3(-transform.m31, -transform.m32, transform.m33) +// // let location = SCNVector3(transform.m41, transform.m42, transform.m43) +// // let currentPositionOfCamera = orientation + location +// // print(currentPositionOfCamera) +// let cameraTransform = float4x4.init(transform) +// +// +// // +// // Debug - calculate and display time difference between frame and currentFrame +// // +//// let timeDiff = currentFrame.timestamp - frame.timestamp +//// print("timeDiff between currentFrame and didUpdate frame: \(timeDiff)") +// +// // +// // Future... +// // +// // set FOV from ARKit camera.projectionMatrix +// // +//// let projectionMatrix = currentFrame.camera.projectionMatrix +//// let verticalElement = projectionMatrix.columns.0.x +//// let horizontalElement = projectionMatrix.columns.1.y +//// sceneView.setFieldOfViewFromProjection(Double(verticalElement), horizontalElement: Double(horizontalElement)) +// +// let finalQuat:simd_quatf = simd_mul(simd_mul(compensationQuat, simd_quaternion(cameraTransform)), orientationQuat) +// var transformationMatrix = AGSTransformationMatrix(quaternionX: Double(finalQuat.vector.x), +// quaternionY: Double(finalQuat.vector.y), +// quaternionZ: Double(finalQuat.vector.z), +// quaternionW: Double(finalQuat.vector.w), +// translationX: Double(cameraTransform.columns.3.x), +// translationY: Double(-cameraTransform.columns.3.z), +// translationZ: Double(cameraTransform.columns.3.y)) +// +// transformationMatrix = initialTransformationMatrix.addTransformation(transformationMatrix) +// +// // let currentTransformationMatrix = sceneView.currentViewpointCamera().transformationMatrix +// // transformationMatrix = currentTransformationMatrix.addTransformation(transformationMatrix) +//// print("transformation values: tX = \(transformationMatrix.translationX); tY = \(transformationMatrix.translationY); tZ = \(transformationMatrix.translationZ); qX = \(transformationMatrix.quaternionX); qY = \(transformationMatrix.quaternionY); qZ = \(transformationMatrix.quaternionZ); qW = = \(transformationMatrix.quaternionW)") +// let camera = AGSCamera(transformationMatrix: transformationMatrix) +// print("camera heading: \(camera.heading), pitch = \(camera.pitch), roll = \(camera.roll), location = \(camera.location)") +// +// sceneView.setViewpointCamera(camera) +// +// // let svCamera = sceneView.currentViewpointCamera() +// // print("sceneView.Camera heading: \(svCamera.heading), pitch = \(svCamera.pitch), roll = \(svCamera.roll), location = \(svCamera.location)") +// +//// Thread.sleep(forTimeInterval: 0.1) +// sceneView.renderFrame() +// frameCount = frameCount + 1 +//// Thread.sleep(forTimeInterval: 0.1) } /** @@ -548,13 +550,6 @@ extension ArcGISARView: CLLocationManagerDelegate { initialTransformationMatrix = camera.transformationMatrix sceneView.setViewpointCamera(camera) - // - // Debug - schedule timer to add point to the scene after 5 seconds (so we're sure the camera is set up)... - // - pointTimer = Timer.scheduledTimer(withTimeInterval: 5.0, repeats: false, block: { [weak self] (timer) in - self?.addPointToScene(camera: (self?.sceneView.currentViewpointCamera())!) - }) - finalizeStart() } else if location.horizontalAccuracy < horizontalAccuracy { @@ -564,24 +559,6 @@ extension ArcGISARView: CLLocationManagerDelegate { print("didUpdateLocations...") } - - // - // Debug - show point in front of camera... - // - private func addPointToScene(camera: AGSCamera) { - - let go = AGSGraphicsOverlay() - go.sceneProperties = AGSLayerSceneProperties(surfacePlacement: .absolute) - sceneView.graphicsOverlays.add(go) - - let markerSymbol = AGSSimpleMarkerSceneSymbol(style: .diamond, color: .blue, height: 0.1, width: 0.1, depth: 0.1, anchorPosition: .bottom) - - // move camera forward 1 meters and get location - let location = camera.moveForward(withDistance: 1.0).location - - let graphic = AGSGraphic(geometry: location, symbol: markerSymbol, attributes: nil) - go.graphics.add(graphic) - } /* * locationManager:didUpdateHeading: @@ -642,3 +619,84 @@ extension ArcGISARView: CLLocationManagerDelegate { } } + +// MARK: - SCNSceneRendererDelegate +extension ArcGISARView: SCNSceneRendererDelegate { + + public func renderer(_ renderer: SCNSceneRenderer, willRenderScene scene: SCNScene, atTime time: TimeInterval) { + + // + // Debug - here's the switch between currentFrame and frame... + // + + // create transformation matrix +// guard let currentFrame = session.currentFrame else { return } + // let cameraTransform = currentFrame.camera.transform + // let cameraTransform = frame.camera.transform + + // + // SCNRenderer point of View stuff + // + guard let pointOfView = arSCNView.pointOfView else { return } + let transform = pointOfView.transform + // let orientation = SCNVector3(-transform.m31, -transform.m32, transform.m33) + // let location = SCNVector3(transform.m41, transform.m42, transform.m43) + // let currentPositionOfCamera = orientation + location + // print(currentPositionOfCamera) + let cameraTransform = float4x4.init(transform) + + + // + // Debug - calculate and display time difference between frame and currentFrame + // + // let timeDiff = currentFrame.timestamp - frame.timestamp + // print("timeDiff between currentFrame and didUpdate frame: \(timeDiff)") + + // + // Future... + // + // set FOV from ARKit camera.projectionMatrix + // + // let projectionMatrix = currentFrame.camera.projectionMatrix + // let verticalElement = projectionMatrix.columns.0.x + // let horizontalElement = projectionMatrix.columns.1.y + // sceneView.setFieldOfViewFromProjection(Double(verticalElement), horizontalElement: Double(horizontalElement)) + + let finalQuat:simd_quatf = simd_mul(simd_mul(compensationQuat, simd_quaternion(cameraTransform)), orientationQuat) + var transformationMatrix = AGSTransformationMatrix(quaternionX: Double(finalQuat.vector.x), + quaternionY: Double(finalQuat.vector.y), + quaternionZ: Double(finalQuat.vector.z), + quaternionW: Double(finalQuat.vector.w), + translationX: Double(cameraTransform.columns.3.x), + translationY: Double(-cameraTransform.columns.3.z), + translationZ: Double(cameraTransform.columns.3.y)) + + transformationMatrix = initialTransformationMatrix.addTransformation(transformationMatrix) + + // let currentTransformationMatrix = sceneView.currentViewpointCamera().transformationMatrix + // transformationMatrix = currentTransformationMatrix.addTransformation(transformationMatrix) + // print("transformation values: tX = \(transformationMatrix.translationX); tY = \(transformationMatrix.translationY); tZ = \(transformationMatrix.translationZ); qX = \(transformationMatrix.quaternionX); qY = \(transformationMatrix.quaternionY); qZ = \(transformationMatrix.quaternionZ); qW = = \(transformationMatrix.quaternionW)") + let camera = AGSCamera(transformationMatrix: transformationMatrix) +// print("camera heading: \(camera.heading), pitch = \(camera.pitch), roll = \(camera.roll), location = \(camera.location)") + + print("go props = \((sceneView.graphicsOverlays.firstObject as? AGSGraphicsOverlay)?.sceneProperties?.surfacePlacement.rawValue)") + + guard let graphic = (sceneView.graphicsOverlays.firstObject as? AGSGraphicsOverlay)?.graphics.firstObject as? AGSGraphic else { return } + + print("graphic location = \(graphic.geometry)") + + sceneView.setViewpointCamera(camera) + + // let svCamera = sceneView.currentViewpointCamera() + // print("sceneView.Camera heading: \(svCamera.heading), pitch = \(svCamera.pitch), roll = \(svCamera.roll), location = \(svCamera.location)") + + // Thread.sleep(forTimeInterval: 0.1) + sceneView.renderFrame() + frameCount = frameCount + 1 + // Thread.sleep(forTimeInterval: 0.1) + } + + public func renderer(_ renderer: SCNSceneRenderer, didRenderScene scene: SCNScene, atTime time: TimeInterval) { + + } +} From 090505f6c56ad5ba3d72a72225505e8a6fbe237c Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Fri, 24 May 2019 16:14:49 -0500 Subject: [PATCH 020/147] remove print statements --- Toolkit/ArcGISToolkit/AR/ArcGISARView.swift | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift b/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift index f4971ade..7c800f84 100644 --- a/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift +++ b/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift @@ -657,10 +657,11 @@ extension ArcGISARView: SCNSceneRendererDelegate { // // set FOV from ARKit camera.projectionMatrix // - // let projectionMatrix = currentFrame.camera.projectionMatrix - // let verticalElement = projectionMatrix.columns.0.x - // let horizontalElement = projectionMatrix.columns.1.y - // sceneView.setFieldOfViewFromProjection(Double(verticalElement), horizontalElement: Double(horizontalElement)) +// guard let currentFrame = arSCNView.session.currentFrame else { return } +// let projectionMatrix = currentFrame.camera.projectionMatrix +// let verticalElement = projectionMatrix.columns.0.x +// let horizontalElement = projectionMatrix.columns.1.y +// sceneView.setFieldOfViewFromProjection(Double(verticalElement), horizontalElement: Double(horizontalElement)) let finalQuat:simd_quatf = simd_mul(simd_mul(compensationQuat, simd_quaternion(cameraTransform)), orientationQuat) var transformationMatrix = AGSTransformationMatrix(quaternionX: Double(finalQuat.vector.x), @@ -679,12 +680,6 @@ extension ArcGISARView: SCNSceneRendererDelegate { let camera = AGSCamera(transformationMatrix: transformationMatrix) // print("camera heading: \(camera.heading), pitch = \(camera.pitch), roll = \(camera.roll), location = \(camera.location)") - print("go props = \((sceneView.graphicsOverlays.firstObject as? AGSGraphicsOverlay)?.sceneProperties?.surfacePlacement.rawValue)") - - guard let graphic = (sceneView.graphicsOverlays.firstObject as? AGSGraphicsOverlay)?.graphics.firstObject as? AGSGraphic else { return } - - print("graphic location = \(graphic.geometry)") - sceneView.setViewpointCamera(camera) // let svCamera = sceneView.currentViewpointCamera() From 41db90a238d218fb3cc561a977c2aef0fd40e45c Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Thu, 30 May 2019 14:23:27 -0500 Subject: [PATCH 021/147] FOV testing --- .../project.pbxproj | 5 +- .../ArcGISToolkitExamples/ARExample.swift | 67 +++++++++++++++++-- .../ArcGISToolkit/AR/ArcGISARSensorView.swift | 2 +- Toolkit/ArcGISToolkit/AR/ArcGISARView.swift | 13 ++-- 4 files changed, 74 insertions(+), 13 deletions(-) diff --git a/Examples/ArcGISToolkitExamples.xcodeproj/project.pbxproj b/Examples/ArcGISToolkitExamples.xcodeproj/project.pbxproj index b16c7d07..5bcc1018 100644 --- a/Examples/ArcGISToolkitExamples.xcodeproj/project.pbxproj +++ b/Examples/ArcGISToolkitExamples.xcodeproj/project.pbxproj @@ -195,6 +195,7 @@ TargetAttributes = { 8839043A1DF6022A001F3188 = { CreatedOnToolsVersion = 8.0; + DevelopmentTeam = P8HGHS7JQ8; LastSwiftMigration = 1010; ProvisioningStyle = Automatic; SystemCapabilities = { @@ -442,7 +443,7 @@ ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; - DEVELOPMENT_TEAM = ""; + DEVELOPMENT_TEAM = P8HGHS7JQ8; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", "$(USER_LIBRARY_DIR)/SDKs/ArcGIS/iOS/Frameworks/Dynamic", @@ -462,7 +463,7 @@ ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; - DEVELOPMENT_TEAM = ""; + DEVELOPMENT_TEAM = P8HGHS7JQ8; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", "$(USER_LIBRARY_DIR)/SDKs/ArcGIS/iOS/Frameworks/Dynamic", diff --git a/Examples/ArcGISToolkitExamples/ARExample.swift b/Examples/ArcGISToolkitExamples/ARExample.swift index c66b101c..2a270e05 100644 --- a/Examples/ArcGISToolkitExamples/ARExample.swift +++ b/Examples/ArcGISToolkitExamples/ARExample.swift @@ -12,8 +12,10 @@ // limitations under the License. import UIKit -import ArcGISToolkit -import ArcGIS +import ARKit + +//import ArcGISToolkit +//import ArcGIS open class ARExample: UIViewController { @@ -21,12 +23,57 @@ open class ARExample: UIViewController { // public let arView = ArcGISARView(renderVideoFeed: false) // public let arView = ArcGISARSensorView(renderVideoFeed: true) + let fovLabel: UILabel = UILabel(frame: .zero) override open func viewDidLoad() { super.viewDidLoad() - arView.frame = view.bounds - arView.autoresizingMask = [.flexibleWidth, .flexibleHeight] + arView.delegate = self + + // + // Short and fat + // + view.addSubview(arView) + arView.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([ + arView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 0), + arView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: 0), + arView.topAnchor.constraint(equalTo: view.topAnchor, constant: 200), +// arView.widthAnchor.constraint(equalToConstant: 200.0), + arView.heightAnchor.constraint(equalToConstant:200.0) +// arView.bottomAnchor.constraint(equalTo: view.bottomAnchor) + ]) + + view.addSubview(fovLabel) + fovLabel.translatesAutoresizingMaskIntoConstraints = false + fovLabel.textColor = .blue + NSLayoutConstraint.activate([ + fovLabel.centerXAnchor.constraint(equalTo: arView.centerXAnchor, constant: 0), + fovLabel.topAnchor.constraint(equalTo: arView.bottomAnchor, constant: 16), + ]) + + // + // Tall and skinny + // + +// view.addSubview(arView) +// arView.translatesAutoresizingMaskIntoConstraints = false +// NSLayoutConstraint.activate([ +// arView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 200), +// // arView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: 0), +// arView.topAnchor.constraint(equalTo: view.topAnchor, constant: 0), +// arView.widthAnchor.constraint(equalToConstant: 200.0), +// // arView.heightAnchor.constraint(equalToConstant:200.0) +// arView.bottomAnchor.constraint(equalTo: view.bottomAnchor) +// ]) +// +// view.addSubview(fovLabel) +// fovLabel.translatesAutoresizingMaskIntoConstraints = false +// fovLabel.textColor = .blue +// NSLayoutConstraint.activate([ +// fovLabel.leadingAnchor.constraint(equalTo: arView.trailingAnchor, constant: 0), +// fovLabel.centerYAnchor.constraint(equalTo: arView.centerYAnchor, constant: 16), +// ]) arView.sceneView.scene = scene() // arView.sceneView.alpha = 0.5 @@ -85,3 +132,15 @@ open class ARExample: UIViewController { } } +extension ARExample: ARSessionDelegate { + + public func session(_ session: ARSession, didUpdate frame: ARFrame) { + guard let currentFrame = session.currentFrame else { return } + let projectionMatrix = currentFrame.camera.projectionMatrix + let verticalElement = projectionMatrix.columns.0.x + let horizontalElement = projectionMatrix.columns.1.y + fovLabel.text = String("FOV-vert: \(verticalElement) horiz: \(horizontalElement)") +// fovLabel.text = String("sceneView.fieldOfView = horizontalElement: \(horizontalElement) fieldOfViewDistortionRatio = \(arView.sceneView.fieldOfViewDistortionRatio)") + + } +} diff --git a/Toolkit/ArcGISToolkit/AR/ArcGISARSensorView.swift b/Toolkit/ArcGISToolkit/AR/ArcGISARSensorView.swift index ef0cef50..2b36b0b3 100644 --- a/Toolkit/ArcGISToolkit/AR/ArcGISARSensorView.swift +++ b/Toolkit/ArcGISToolkit/AR/ArcGISARSensorView.swift @@ -15,7 +15,7 @@ import UIKit import AVFoundation import CoreMotion -import ArcGIS +//import ArcGIS public enum LocationType { case anglesOnly diff --git a/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift b/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift index 7c800f84..6d93dd6a 100644 --- a/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift +++ b/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift @@ -14,7 +14,7 @@ import UIKit import ARKit -import ArcGIS +//import ArcGIS public class ArcGISARView: UIView { @@ -657,11 +657,12 @@ extension ArcGISARView: SCNSceneRendererDelegate { // // set FOV from ARKit camera.projectionMatrix // -// guard let currentFrame = arSCNView.session.currentFrame else { return } -// let projectionMatrix = currentFrame.camera.projectionMatrix -// let verticalElement = projectionMatrix.columns.0.x -// let horizontalElement = projectionMatrix.columns.1.y -// sceneView.setFieldOfViewFromProjection(Double(verticalElement), horizontalElement: Double(horizontalElement)) + guard let currentFrame = arSCNView.session.currentFrame else { return } + let projectionMatrix = currentFrame.camera.projectionMatrix + let verticalElement = projectionMatrix.columns.0.x + let horizontalElement = projectionMatrix.columns.1.y + sceneView.setFieldOfViewFromProjection(Double(verticalElement), horizontalElement: Double(horizontalElement)) + print("FOV-vert: \(verticalElement) horiz: \(horizontalElement)") let finalQuat:simd_quatf = simd_mul(simd_mul(compensationQuat, simd_quaternion(cameraTransform)), orientationQuat) var transformationMatrix = AGSTransformationMatrix(quaternionX: Double(finalQuat.vector.x), From 19ab25c95825e8b563e72690cb68f828236bb164 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Thu, 30 May 2019 14:59:25 -0500 Subject: [PATCH 022/147] Cleanup --- .../ArcGISToolkitExamples/ARExample.swift | 89 ++------- .../ArcGISToolkit/AR/ArcGISARSensorView.swift | 2 +- Toolkit/ArcGISToolkit/AR/ArcGISARView.swift | 179 +++++------------- 3 files changed, 57 insertions(+), 213 deletions(-) diff --git a/Examples/ArcGISToolkitExamples/ARExample.swift b/Examples/ArcGISToolkitExamples/ARExample.swift index 2a270e05..9dde8941 100644 --- a/Examples/ArcGISToolkitExamples/ARExample.swift +++ b/Examples/ArcGISToolkitExamples/ARExample.swift @@ -13,9 +13,8 @@ import UIKit import ARKit - -//import ArcGISToolkit -//import ArcGIS +import ArcGISToolkit +import ArcGIS open class ARExample: UIViewController { @@ -23,68 +22,24 @@ open class ARExample: UIViewController { // public let arView = ArcGISARView(renderVideoFeed: false) // public let arView = ArcGISARSensorView(renderVideoFeed: true) - let fovLabel: UILabel = UILabel(frame: .zero) override open func viewDidLoad() { super.viewDidLoad() - arView.delegate = self - // - // Short and fat + // Example of how to get ARSessionDelegate methods from the ArcGISARView // + arView.sessionDelegate = self view.addSubview(arView) arView.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ - arView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 0), - arView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: 0), - arView.topAnchor.constraint(equalTo: view.topAnchor, constant: 200), -// arView.widthAnchor.constraint(equalToConstant: 200.0), - arView.heightAnchor.constraint(equalToConstant:200.0) -// arView.bottomAnchor.constraint(equalTo: view.bottomAnchor) - ]) - - view.addSubview(fovLabel) - fovLabel.translatesAutoresizingMaskIntoConstraints = false - fovLabel.textColor = .blue - NSLayoutConstraint.activate([ - fovLabel.centerXAnchor.constraint(equalTo: arView.centerXAnchor, constant: 0), - fovLabel.topAnchor.constraint(equalTo: arView.bottomAnchor, constant: 16), + arView.leadingAnchor.constraint(equalTo: view.leadingAnchor), + arView.trailingAnchor.constraint(equalTo: view.trailingAnchor), + arView.topAnchor.constraint(equalTo: view.topAnchor), + arView.bottomAnchor.constraint(equalTo: view.bottomAnchor) ]) - // - // Tall and skinny - // - -// view.addSubview(arView) -// arView.translatesAutoresizingMaskIntoConstraints = false -// NSLayoutConstraint.activate([ -// arView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 200), -// // arView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: 0), -// arView.topAnchor.constraint(equalTo: view.topAnchor, constant: 0), -// arView.widthAnchor.constraint(equalToConstant: 200.0), -// // arView.heightAnchor.constraint(equalToConstant:200.0) -// arView.bottomAnchor.constraint(equalTo: view.bottomAnchor) -// ]) -// -// view.addSubview(fovLabel) -// fovLabel.translatesAutoresizingMaskIntoConstraints = false -// fovLabel.textColor = .blue -// NSLayoutConstraint.activate([ -// fovLabel.leadingAnchor.constraint(equalTo: arView.trailingAnchor, constant: 0), -// fovLabel.centerYAnchor.constraint(equalTo: arView.centerYAnchor, constant: 16), -// ]) - arView.sceneView.scene = scene() -// arView.sceneView.alpha = 0.5 - -// camera heading: 318.9702215288517, pitch = 52.69900468516913, roll = 0.6234908971981902, location = AGSPoint: (-93.298481, 44.940544, 274.055704, nan), sr: 4326 - - let originCamera = AGSCamera(latitude: 44.940544, longitude: -93.298481, altitude: 274.055704, heading: 270.0, pitch: 0.0, roll: 0.0) - arView.originCamera = originCamera - - let camera = AGSCamera(latitude: 44.940544, longitude: -93.298481, altitude: 274.055704, heading: 270.0, pitch: 90.0, roll: 0.0) - addPointToScene(camera: camera) } override open func viewDidAppear(_ animated: Bool) { @@ -112,35 +67,13 @@ open class ARExample: UIViewController { return scene } - - // - // Debug - show point in front of camera... - // - private func addPointToScene(camera: AGSCamera) { - - let go = AGSGraphicsOverlay() - go.sceneProperties = AGSLayerSceneProperties(surfacePlacement: .absolute) - arView.sceneView.graphicsOverlays.add(go) - - let markerSymbol = AGSSimpleMarkerSceneSymbol(style: .diamond, color: .blue, height: 0.1, width: 0.1, depth: 0.1, anchorPosition: .bottom) - - // move camera forward 1 meters and get location - let location = camera.moveForward(withDistance: 5.0).location - - let graphic = AGSGraphic(geometry: location, symbol: markerSymbol, attributes: nil) - go.graphics.add(graphic) - } } extension ARExample: ARSessionDelegate { public func session(_ session: ARSession, didUpdate frame: ARFrame) { - guard let currentFrame = session.currentFrame else { return } - let projectionMatrix = currentFrame.camera.projectionMatrix - let verticalElement = projectionMatrix.columns.0.x - let horizontalElement = projectionMatrix.columns.1.y - fovLabel.text = String("FOV-vert: \(verticalElement) horiz: \(horizontalElement)") -// fovLabel.text = String("sceneView.fieldOfView = horizontalElement: \(horizontalElement) fieldOfViewDistortionRatio = \(arView.sceneView.fieldOfViewDistortionRatio)") - + // + // Example of how to get ARSessionDelegate methods from the ArcGISARView + // } } diff --git a/Toolkit/ArcGISToolkit/AR/ArcGISARSensorView.swift b/Toolkit/ArcGISToolkit/AR/ArcGISARSensorView.swift index 2b36b0b3..ef0cef50 100644 --- a/Toolkit/ArcGISToolkit/AR/ArcGISARSensorView.swift +++ b/Toolkit/ArcGISToolkit/AR/ArcGISARSensorView.swift @@ -15,7 +15,7 @@ import UIKit import AVFoundation import CoreMotion -//import ArcGIS +import ArcGIS public enum LocationType { case anglesOnly diff --git a/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift b/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift index 6d93dd6a..702a92fa 100644 --- a/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift +++ b/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift @@ -14,7 +14,7 @@ import UIKit import ARKit -//import ArcGIS +import ArcGIS public class ArcGISARView: UIView { @@ -32,8 +32,11 @@ public class ArcGISARView: UIView { public var originCamera: AGSCamera? public var translationTransformationFactor: Double = 1.0 - // we intercept these methods first, but will use `delegate` to forward them to clients - weak open var delegate: ARSessionDelegate? + // we intercept these ARSessionDelegate methods first, but will use `sessionDelegate` to forward them to clients + weak open var sessionDelegate: ARSessionDelegate? + + // we intercept these SCNSceneRendererDelegate methods first, but will use `scnSceneRendererDelegate` to forward them to clients + weak open var scnSceneRendererDelegate: SCNSceneRendererDelegate? // MARK: private properties @@ -99,7 +102,7 @@ public class ArcGISARView: UIView { // make our sceneView's background transparent sceneView.isBackgroundTransparent = true sceneView.atmosphereEffect = .none - sceneView.isManualRendering = false + sceneView.isManualRendering = true notifiedStartOrFailure = false @@ -309,73 +312,7 @@ extension ArcGISARView: ARSessionDelegate { @param frame The frame that has been updated. */ public func session(_ session: ARSession, didUpdate frame: ARFrame) { - - delegate?.session?(session, didUpdate: frame) - -// -// // -// // Debug - here's the switch between currentFrame and frame... -// // -// -// // create transformation matrix -// guard let currentFrame = session.currentFrame else { return } -//// let cameraTransform = currentFrame.camera.transform -//// let cameraTransform = frame.camera.transform -// -// // -// // SCNRenderer point of View stuff -// // -// guard let pointOfView = arSCNView.pointOfView else { return } -// let transform = pointOfView.transform -// // let orientation = SCNVector3(-transform.m31, -transform.m32, transform.m33) -// // let location = SCNVector3(transform.m41, transform.m42, transform.m43) -// // let currentPositionOfCamera = orientation + location -// // print(currentPositionOfCamera) -// let cameraTransform = float4x4.init(transform) -// -// -// // -// // Debug - calculate and display time difference between frame and currentFrame -// // -//// let timeDiff = currentFrame.timestamp - frame.timestamp -//// print("timeDiff between currentFrame and didUpdate frame: \(timeDiff)") -// -// // -// // Future... -// // -// // set FOV from ARKit camera.projectionMatrix -// // -//// let projectionMatrix = currentFrame.camera.projectionMatrix -//// let verticalElement = projectionMatrix.columns.0.x -//// let horizontalElement = projectionMatrix.columns.1.y -//// sceneView.setFieldOfViewFromProjection(Double(verticalElement), horizontalElement: Double(horizontalElement)) -// -// let finalQuat:simd_quatf = simd_mul(simd_mul(compensationQuat, simd_quaternion(cameraTransform)), orientationQuat) -// var transformationMatrix = AGSTransformationMatrix(quaternionX: Double(finalQuat.vector.x), -// quaternionY: Double(finalQuat.vector.y), -// quaternionZ: Double(finalQuat.vector.z), -// quaternionW: Double(finalQuat.vector.w), -// translationX: Double(cameraTransform.columns.3.x), -// translationY: Double(-cameraTransform.columns.3.z), -// translationZ: Double(cameraTransform.columns.3.y)) -// -// transformationMatrix = initialTransformationMatrix.addTransformation(transformationMatrix) -// -// // let currentTransformationMatrix = sceneView.currentViewpointCamera().transformationMatrix -// // transformationMatrix = currentTransformationMatrix.addTransformation(transformationMatrix) -//// print("transformation values: tX = \(transformationMatrix.translationX); tY = \(transformationMatrix.translationY); tZ = \(transformationMatrix.translationZ); qX = \(transformationMatrix.quaternionX); qY = \(transformationMatrix.quaternionY); qZ = \(transformationMatrix.quaternionZ); qW = = \(transformationMatrix.quaternionW)") -// let camera = AGSCamera(transformationMatrix: transformationMatrix) -// print("camera heading: \(camera.heading), pitch = \(camera.pitch), roll = \(camera.roll), location = \(camera.location)") -// -// sceneView.setViewpointCamera(camera) -// -// // let svCamera = sceneView.currentViewpointCamera() -// // print("sceneView.Camera heading: \(svCamera.heading), pitch = \(svCamera.pitch), roll = \(svCamera.roll), location = \(svCamera.location)") -// -//// Thread.sleep(forTimeInterval: 0.1) -// sceneView.renderFrame() -// frameCount = frameCount + 1 -//// Thread.sleep(forTimeInterval: 0.1) + sessionDelegate?.session?(session, didUpdate: frame) } /** @@ -385,7 +322,7 @@ extension ArcGISARView: ARSessionDelegate { @param anchors An array of added anchors. */ public func session(_ session: ARSession, didAdd anchors: [ARAnchor]) { - delegate?.session?(session, didAdd: anchors) + sessionDelegate?.session?(session, didAdd: anchors) } /** @@ -395,7 +332,7 @@ extension ArcGISARView: ARSessionDelegate { @param anchors An array of updated anchors. */ public func session(_ session: ARSession, didUpdate anchors: [ARAnchor]) { - delegate?.session?(session, didUpdate: anchors) + sessionDelegate?.session?(session, didUpdate: anchors) } /** @@ -405,7 +342,7 @@ extension ArcGISARView: ARSessionDelegate { @param anchors An array of removed anchors. */ public func session(_ session: ARSession, didRemove anchors: [ARAnchor]) { - delegate?.session?(session, didRemove: anchors) + sessionDelegate?.session?(session, didRemove: anchors) } } @@ -446,7 +383,7 @@ extension ArcGISARView: ARSessionObserver { rootController.present(alertController, animated: true, completion: nil) } - delegate?.session?(session, didFailWithError: error) + sessionDelegate?.session?(session, didFailWithError: error) } /** @@ -456,7 +393,7 @@ extension ArcGISARView: ARSessionObserver { @param camera The camera that changed tracking states. */ public func session(_ session: ARSession, cameraDidChangeTrackingState camera: ARCamera) { - delegate?.session?(session, cameraDidChangeTrackingState: camera) + sessionDelegate?.session?(session, cameraDidChangeTrackingState: camera) } /** @@ -470,7 +407,7 @@ extension ArcGISARView: ARSessionObserver { @param session The session that was interrupted. */ public func sessionWasInterrupted(_ session: ARSession) { - delegate?.sessionWasInterrupted?(session) + sessionDelegate?.sessionWasInterrupted?(session) } /** @@ -483,7 +420,7 @@ extension ArcGISARView: ARSessionObserver { @param session The session that was interrupted. */ public func sessionInterruptionEnded(_ session: ARSession) { - delegate?.sessionWasInterrupted?(session) + sessionDelegate?.sessionWasInterrupted?(session) } /** @@ -501,7 +438,7 @@ extension ArcGISARView: ARSessionObserver { */ @available(iOS 11.3, *) public func sessionShouldAttemptRelocalization(_ session: ARSession) -> Bool { - if let result = delegate?.sessionShouldAttemptRelocalization?(session) { + if let result = sessionDelegate?.sessionShouldAttemptRelocalization?(session) { return result } return false @@ -514,15 +451,10 @@ extension ArcGISARView: ARSessionObserver { @param audioSampleBuffer The captured audio sample buffer. */ public func session(_ session: ARSession, didOutputAudioSampleBuffer audioSampleBuffer: CMSampleBuffer) { - delegate?.session?(session, didOutputAudioSampleBuffer: audioSampleBuffer) + sessionDelegate?.session?(session, didOutputAudioSampleBuffer: audioSampleBuffer) } } -// -// Debug -// -var pointTimer: Timer? - // MARK: - CLLocationManagerDelegate extension ArcGISARView: CLLocationManagerDelegate { /* @@ -551,13 +483,13 @@ extension ArcGISARView: CLLocationManagerDelegate { sceneView.setViewpointCamera(camera) finalizeStart() + print("didUpdateLocations - initialLocation...") } else if location.horizontalAccuracy < horizontalAccuracy { horizontalAccuracy = location.horizontalAccuracy - // TODO: update current location??? + print("didUpdateLocations - accuracy improved...") } - print("didUpdateLocations...") } /* @@ -623,47 +555,31 @@ extension ArcGISARView: CLLocationManagerDelegate { // MARK: - SCNSceneRendererDelegate extension ArcGISARView: SCNSceneRendererDelegate { + public func renderer(_ renderer: SCNSceneRenderer, updateAtTime time: TimeInterval) { + scnSceneRendererDelegate?.renderer?(renderer, updateAtTime: time) + } + + public func renderer(_ renderer: SCNSceneRenderer, didApplyAnimationsAtTime time: TimeInterval) { + scnSceneRendererDelegate?.renderer?(renderer, didApplyConstraintsAtTime: time) + } + + public func renderer(_ renderer: SCNSceneRenderer, didSimulatePhysicsAtTime time: TimeInterval) { + scnSceneRendererDelegate?.renderer?(renderer, didSimulatePhysicsAtTime: time) + } + + @available(iOS 11.0, *) + public func renderer(_ renderer: SCNSceneRenderer, didApplyConstraintsAtTime time: TimeInterval) { + scnSceneRendererDelegate?.renderer?(renderer, didApplyConstraintsAtTime: time) + } + public func renderer(_ renderer: SCNSceneRenderer, willRenderScene scene: SCNScene, atTime time: TimeInterval) { // - // Debug - here's the switch between currentFrame and frame... - // - - // create transformation matrix -// guard let currentFrame = session.currentFrame else { return } - // let cameraTransform = currentFrame.camera.transform - // let cameraTransform = frame.camera.transform - - // - // SCNRenderer point of View stuff + // get transform from SCNView.pointOfView // - guard let pointOfView = arSCNView.pointOfView else { return } - let transform = pointOfView.transform - // let orientation = SCNVector3(-transform.m31, -transform.m32, transform.m33) - // let location = SCNVector3(transform.m41, transform.m42, transform.m43) - // let currentPositionOfCamera = orientation + location - // print(currentPositionOfCamera) + guard let transform = arSCNView.pointOfView?.transform else { return } let cameraTransform = float4x4.init(transform) - - // - // Debug - calculate and display time difference between frame and currentFrame - // - // let timeDiff = currentFrame.timestamp - frame.timestamp - // print("timeDiff between currentFrame and didUpdate frame: \(timeDiff)") - - // - // Future... - // - // set FOV from ARKit camera.projectionMatrix - // - guard let currentFrame = arSCNView.session.currentFrame else { return } - let projectionMatrix = currentFrame.camera.projectionMatrix - let verticalElement = projectionMatrix.columns.0.x - let horizontalElement = projectionMatrix.columns.1.y - sceneView.setFieldOfViewFromProjection(Double(verticalElement), horizontalElement: Double(horizontalElement)) - print("FOV-vert: \(verticalElement) horiz: \(horizontalElement)") - let finalQuat:simd_quatf = simd_mul(simd_mul(compensationQuat, simd_quaternion(cameraTransform)), orientationQuat) var transformationMatrix = AGSTransformationMatrix(quaternionX: Double(finalQuat.vector.x), quaternionY: Double(finalQuat.vector.y), @@ -675,24 +591,19 @@ extension ArcGISARView: SCNSceneRendererDelegate { transformationMatrix = initialTransformationMatrix.addTransformation(transformationMatrix) - // let currentTransformationMatrix = sceneView.currentViewpointCamera().transformationMatrix - // transformationMatrix = currentTransformationMatrix.addTransformation(transformationMatrix) - // print("transformation values: tX = \(transformationMatrix.translationX); tY = \(transformationMatrix.translationY); tZ = \(transformationMatrix.translationZ); qX = \(transformationMatrix.quaternionX); qY = \(transformationMatrix.quaternionY); qZ = \(transformationMatrix.quaternionZ); qW = = \(transformationMatrix.quaternionW)") let camera = AGSCamera(transformationMatrix: transformationMatrix) -// print("camera heading: \(camera.heading), pitch = \(camera.pitch), roll = \(camera.roll), location = \(camera.location)") - sceneView.setViewpointCamera(camera) - // let svCamera = sceneView.currentViewpointCamera() - // print("sceneView.Camera heading: \(svCamera.heading), pitch = \(svCamera.pitch), roll = \(svCamera.roll), location = \(svCamera.location)") - - // Thread.sleep(forTimeInterval: 0.1) sceneView.renderFrame() frameCount = frameCount + 1 - // Thread.sleep(forTimeInterval: 0.1) + + // + // call our scnSceneRendererDelegate + // + scnSceneRendererDelegate?.renderer?(renderer, willRenderScene: scene, atTime: time) } - + public func renderer(_ renderer: SCNSceneRenderer, didRenderScene scene: SCNScene, atTime time: TimeInterval) { - + scnSceneRendererDelegate?.renderer?(renderer, didRenderScene: scene, atTime: time) } } From b6e0efe00bb485bae66ae46c48b601560c609726 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Thu, 30 May 2019 15:02:20 -0500 Subject: [PATCH 023/147] Cleanup, fix originCamera bug, move renderFrame() call to SCNRendererDelegate method --- Examples/ArcGISToolkitExamples.xcodeproj/project.pbxproj | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/Examples/ArcGISToolkitExamples.xcodeproj/project.pbxproj b/Examples/ArcGISToolkitExamples.xcodeproj/project.pbxproj index 5bcc1018..b16c7d07 100644 --- a/Examples/ArcGISToolkitExamples.xcodeproj/project.pbxproj +++ b/Examples/ArcGISToolkitExamples.xcodeproj/project.pbxproj @@ -195,7 +195,6 @@ TargetAttributes = { 8839043A1DF6022A001F3188 = { CreatedOnToolsVersion = 8.0; - DevelopmentTeam = P8HGHS7JQ8; LastSwiftMigration = 1010; ProvisioningStyle = Automatic; SystemCapabilities = { @@ -443,7 +442,7 @@ ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; - DEVELOPMENT_TEAM = P8HGHS7JQ8; + DEVELOPMENT_TEAM = ""; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", "$(USER_LIBRARY_DIR)/SDKs/ArcGIS/iOS/Frameworks/Dynamic", @@ -463,7 +462,7 @@ ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; - DEVELOPMENT_TEAM = P8HGHS7JQ8; + DEVELOPMENT_TEAM = ""; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", "$(USER_LIBRARY_DIR)/SDKs/ArcGIS/iOS/Frameworks/Dynamic", From 5642d967d9d81af4b85a85c4e8b10a640633cac0 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Fri, 31 May 2019 15:55:57 -0500 Subject: [PATCH 024/147] Cleanup and doc --- .../ArcGISToolkitExamples/ARExample.swift | 5 +- .../ArcGISToolkit/AR/ArcGISARSensorView.swift | 104 ++++++++------ Toolkit/ArcGISToolkit/AR/ArcGISARView.swift | 133 ++++++++++++++---- 3 files changed, 165 insertions(+), 77 deletions(-) diff --git a/Examples/ArcGISToolkitExamples/ARExample.swift b/Examples/ArcGISToolkitExamples/ARExample.swift index 9dde8941..0ffdd76d 100644 --- a/Examples/ArcGISToolkitExamples/ARExample.swift +++ b/Examples/ArcGISToolkitExamples/ARExample.swift @@ -19,8 +19,6 @@ import ArcGIS open class ARExample: UIViewController { public let arView = ArcGISARView(frame: CGRect.zero) -// public let arView = ArcGISARView(renderVideoFeed: false) -// public let arView = ArcGISARSensorView(renderVideoFeed: true) override open func viewDidLoad() { super.viewDidLoad() @@ -52,9 +50,8 @@ open class ARExample: UIViewController { private func scene() -> AGSScene { - // create scene + // create scene with the streets basemap let scene = AGSScene(basemapType: .streets) -// let scene = AGSScene() // create elevation surface let elevationSource = AGSArcGISTiledElevationSource(url: URL(string: "http://elevation3d.arcgis.com/arcgis/rest/services/WorldElevation3D/Terrain3D/ImageServer")!) diff --git a/Toolkit/ArcGISToolkit/AR/ArcGISARSensorView.swift b/Toolkit/ArcGISToolkit/AR/ArcGISARSensorView.swift index ef0cef50..ce01a6dc 100644 --- a/Toolkit/ArcGISToolkit/AR/ArcGISARSensorView.swift +++ b/Toolkit/ArcGISToolkit/AR/ArcGISARSensorView.swift @@ -25,34 +25,35 @@ public enum LocationType { public class ArcGISARSensorView: UIView { + /// Location type specifying what information we are interested in public var locationType: LocationType = .anglesAndPosition { didSet { // need to update location and heading updates to account for new LocationType } } + /// The view used to display ArcGIS 3D content public var sceneView = AGSSceneView(frame: .zero) { willSet(newSceneview) { removeSubviewAndConstraints(sceneView) } didSet { addSubviewWithConstraints(sceneView) - - // make our sceneView's background transparent - sceneView.isBackgroundTransparent = true - sceneView.atmosphereEffect = .none } } + /// Determines whether to use an absolution heading as opposed to a heading relative to the original heading public var useAbsoluteHeading: Bool = true // MARK: private properties + /// Whether to display the camera image or not public var renderVideoFeed = true - // has the client been notfiied of start/failure + /// Whether the client has been notfiied of start/failure private var notifiedStartOrFailure = false + /// Used to determine the device location private lazy var locationManager: CLLocationManager = { let lm = CLLocationManager() lm.desiredAccuracy = kCLLocationAccuracyBest @@ -60,6 +61,7 @@ public class ArcGISARSensorView: UIView { return lm }() + /// Used to determine the device motion private lazy var motionManager: CMMotionManager = { let mm = CMMotionManager() mm.deviceMotionUpdateInterval = 1.0 / 60.0 @@ -67,27 +69,30 @@ public class ArcGISARSensorView: UIView { return mm }() + /// Initial location from locationManager private var initialLocation: AGSPoint? - private var currentCamera: AGSCamera = AGSCamera(latitude: 0, longitude: 0, altitude: 0, heading: 0, pitch: 0, roll: 0) - - var updateTimer: Timer? // MARK: Capture session + /// Capture session setup results private enum SessionSetupResult { case success case notAuthorized case configurationFailed } + /// The current AVCaptureSession private lazy var session = AVCaptureSession() // Communicate with the capture session and other session objects on this queue. private let sessionQueue = DispatchQueue(label: "session queue", attributes: [], target: nil) private var setupResult: SessionSetupResult = .success var videoDeviceInput: AVCaptureDeviceInput! + + /// The view the camera image is displayed in private lazy var cameraView = CameraView(frame:CGRect.zero) + /// The quaternion used to represent the device orientation; used to calculate the current device transformation for each frame; defaults to landcape-left private var orientationQuat: simd_quatf = simd_quaternion(Float(0), Float(0), Float(0), Float(1)) // MARK: intializers @@ -102,11 +107,15 @@ public class ArcGISARSensorView: UIView { sharedInitialization() } + /// Initializer used to denote whether to display the live camera image + /// + /// - Parameter renderVideoFeed: whether to dispaly the live camera image required public convenience init(renderVideoFeed: Bool){ self.init(frame: CGRect.zero) self.renderVideoFeed = renderVideoFeed } + /// Initialization code shared between all initializers private func sharedInitialization(){ // @@ -118,7 +127,7 @@ public class ArcGISARSensorView: UIView { addSubviewWithConstraints(sceneView) if renderVideoFeed { - // Set up the video preview view. + // set up the video preview view. addSubviewWithConstraints(cameraView, index: 0) cameraView.session = session @@ -126,6 +135,7 @@ public class ArcGISARSensorView: UIView { } } + /// Starts device tracking public func startTracking() { notifiedStartOrFailure = false @@ -145,6 +155,7 @@ public class ArcGISARSensorView: UIView { } } + /// Suspends device tracking public func stopTracking() { locationManager.stopUpdatingLocation() if CLLocationManager.headingAvailable() { @@ -164,12 +175,17 @@ public class ArcGISARSensorView: UIView { } } - // Called when device orientation changes + /// Called when device orientation changes + /// + /// - Parameter notification: the notification @objc func orientationChanged(notification: Notification) { // handle rotation here updateCameraViewOrientation() } + /// Adds subView to superView with appropriate constraints + /// + /// - Parameter subview: the subView to add private func addSubviewWithConstraints(_ subview: UIView, index: Int = -1) { // add subView to view and setup constraints if index >= 0 { @@ -187,12 +203,15 @@ public class ArcGISARSensorView: UIView { ]) } + /// Removes subView and constraints from superView + /// + /// - Parameter subview: the subView to add private func removeSubviewAndConstraints(_ subview: UIView) { - // remove subView from view along with constraints subview.removeFromSuperview() removeConstraints(subview.constraints) } + /// Start the locationManager with undetermined access private func startWithAccessNotDetermined() { if (Bundle.main.object(forInfoDictionaryKey: "NSLocationWhenInUseUsageDescription") != nil) { locationManager.requestWhenInUseAuthorization() @@ -201,10 +220,11 @@ public class ArcGISARSensorView: UIView { locationManager.requestAlwaysAuthorization() } else{ - didStartOrFailWithError(ArcGISARView.missingPListKeyError()) + didStartOrFailWithError(ArcGISARSensorView.missingPListKeyError()) } } + /// Start updating the location and heading via the locationManager private func startUpdatingLocationAndHeading() { locationManager.startUpdatingLocation() if CLLocationManager.headingAvailable() { @@ -215,7 +235,7 @@ public class ArcGISARSensorView: UIView { } private func startWithAccessDenied() { - didStartOrFailWithError(ArcGISARView.accessDeniedError()) + didStartOrFailWithError(ArcGISARSensorView.accessDeniedError()) } private func startWithAccessAuthorized() { @@ -233,7 +253,7 @@ public class ArcGISARSensorView: UIView { if !notifiedStartOrFailure { stopTracking() // we were waiting for user prompt to come back, so notify - didStartOrFailWithError(ArcGISARView.accessDeniedError()) + didStartOrFailWithError(ArcGISARSensorView.accessDeniedError()) } } @@ -253,35 +273,35 @@ public class ArcGISARSensorView: UIView { // TODO: is there anything to do here? } + /// Start tracking device motion updates to get device orientation private func startUpdatingAngles() { let motionQueue = OperationQueue.init() motionQueue.qualityOfService = .userInteractive motionQueue.maxConcurrentOperationCount = 1 motionManager.startDeviceMotionUpdates(to: motionQueue) { [weak self] (motion, error) in - guard let strongSelf = self else { return } - guard let quat = self?.motionManager.deviceMotion?.attitude.quaternion, let orientationQuat = self?.orientationQuat else { return } + let currentQuat = simd_quaternion(Float(quat.x), Float(quat.y), Float(quat.z), Float(quat.w)) let finalQuat = simd_mul(currentQuat, orientationQuat) - // Old beta code to update heading... -// [weakSelf didUpdateRelativePositionWithDeltaX:0 -// deltaY:0 -// deltaZ:0 -// deltaRotationX:finalQuat.vector.x -// deltaRotationY:finalQuat.vector.y -// deltaRotationZ:finalQuat.vector.z -// deltaRotationW:finalQuat.vector.w -// ignoreInitialHeading:NO]; -// }]; + let transformationMatrix = AGSTransformationMatrix(quaternionX: Double(finalQuat.vector.x), + quaternionY: Double(finalQuat.vector.y), + quaternionZ: Double(finalQuat.vector.z), + quaternionW: Double(finalQuat.vector.w), + translationX: 0, + translationY: 0, + translationZ: 0) + let camera = AGSCamera(transformationMatrix: transformationMatrix) + self?.sceneView.setViewpointCamera(camera) } } // MARK: Video - func updateCameraViewOrientation() { + /// Udpate the orientation of the camera view + private func updateCameraViewOrientation() { if let videoPreviewLayerConnection = cameraView.videoPreviewLayer.connection { let deviceOrientation = UIDevice.current.orientation guard let newVideoOrientation = AVCaptureVideoOrientation(rawValue: deviceOrientation.rawValue), @@ -293,6 +313,7 @@ public class ArcGISARSensorView: UIView { } } + /// Prepare the video feed private func prepVideoFeed() { // // Check video authorization status. Video access is required and audio @@ -341,8 +362,8 @@ public class ArcGISARSensorView: UIView { } } + /// Setup the capture session private func setupSession() { - // session setup sessionQueue.async { switch self.setupResult { @@ -421,10 +442,6 @@ public class ArcGISARSensorView: UIView { if session.canAddInput(videoDeviceInput) { session.addInput(videoDeviceInput) self.videoDeviceInput = videoDeviceInput -// -// DispatchQueue.main.async { [weak self] in -// self?.cameraView.videoPreviewLayer.connection!.videoOrientation = .landscapeLeft -// } } else { print("Could not add video device input to the session") @@ -444,17 +461,19 @@ public class ArcGISARSensorView: UIView { } // MARK: Errors - class func notSupportedError() -> NSError { - let userInfo = [NSLocalizedDescriptionKey : "The device does not support ARKit functionality."] - return NSError(domain: AGSErrorDomain, code: 0, userInfo: userInfo) - } - class func accessDeniedError() -> NSError{ + /// Error used when access to the device location is denied + /// + /// - Returns: error stating access to location information is denied + fileprivate class func accessDeniedError() -> NSError{ let userInfo = [NSLocalizedDescriptionKey : "Access to the device location is denied."] return NSError(domain: kCLErrorDomain, code: CLError.Code.denied.rawValue, userInfo: userInfo) } - class func missingPListKeyError() -> NSError{ + /// Error used when required plist information is missing + /// + /// - Returns: error stating plist information is missing + fileprivate class func missingPListKeyError() -> NSError{ let userInfo = [NSLocalizedDescriptionKey : "You must specify a location usage description key (NSLocationWhenInUseUsageDescription or NSLocationAlwaysUsageDescription) in your plist."] return NSError(domain: kCLErrorDomain, code: CLError.Code.denied.rawValue, userInfo: userInfo) } @@ -484,13 +503,12 @@ extension ArcGISARSensorView: CLLocationManagerDelegate { if initialLocation == nil { initialLocation = locationPoint - currentCamera = AGSCamera(location: locationPoint, heading: 0, pitch: 0, roll: 0) - sceneView.setViewpointCamera(currentCamera) + let camera = AGSCamera(location: locationPoint, heading: 0, pitch: 0, roll: 0) + sceneView.setViewpointCamera(camera) finalizeStart() } else { - let camera = sceneView.currentViewpointCamera() - currentCamera = camera.move(toLocation: locationPoint) -// sceneView.setViewpointCamera(currentCamera) + let camera = sceneView.currentViewpointCamera().move(toLocation: locationPoint) + sceneView.setViewpointCamera(camera) } } diff --git a/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift b/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift index 702a92fa..09b37078 100644 --- a/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift +++ b/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift @@ -20,28 +20,42 @@ public class ArcGISARView: UIView { // MARK: public properties + /// The view used to display the ARKit camera image and 3D SceneKit content public lazy private(set) var arSCNView = ARSCNView(frame: .zero) + + /// The view used to display ArcGIS 3D content public lazy private(set) var sceneView = AGSSceneView(frame: .zero) - public var arConfiguration: ARConfiguration = ARWorldTrackingConfiguration() { + + /// The world tracking information used by ARKit + public var arConfiguration: ARConfiguration = { + let config = ARWorldTrackingConfiguration() + config.worldAlignment = .gravityAndHeading + return config + }() { didSet { //start tracking using the new configuration startTracking() } } + /// The viewpoint camera used to set the initial view of the sceneView instead of the devices GPS location via the locationManager public var originCamera: AGSCamera? + + /// The translation factor used to support a table top AR experience public var translationTransformationFactor: Double = 1.0 - // we intercept these ARSessionDelegate methods first, but will use `sessionDelegate` to forward them to clients + // We implement ARSessionDelegate methods, but will use `sessionDelegate` to forward them to clients weak open var sessionDelegate: ARSessionDelegate? - // we intercept these SCNSceneRendererDelegate methods first, but will use `scnSceneRendererDelegate` to forward them to clients + // We implement SCNSceneRendererDelegate methods, but will use `scnSceneRendererDelegate` to forward them to clients weak open var scnSceneRendererDelegate: SCNSceneRendererDelegate? // MARK: private properties + /// Whether to display the camera image or not private var renderVideoFeed = true + /// Used to determine the device location when originCamera is not set private lazy var locationManager: CLLocationManager = { let lm = CLLocationManager() lm.desiredAccuracy = kCLLocationAccuracyBest @@ -49,24 +63,30 @@ public class ArcGISARView: UIView { return lm }() - // initial location from locationManager + /// Initial location from locationManager private var initialLocation: CLLocation? + + /// Current horizontal accuracy of the device private var horizontalAccuracy: CLLocationAccuracy = .greatestFiniteMagnitude; - private var initialTransformationMatrix: AGSTransformationMatrix = AGSTransformationMatrix() - // is ARKit supported on this device? + /// The intial camera position and orientation whether it was set via originCamera or the locationManager + private var initialTransformationMatrix = AGSTransformationMatrix() + + /// Whether ARKit supported on this device? private var isSupported = false - // has the client been notfiied of start/failure + /// Whether the client has been notfiied of start/failure private var notifiedStartOrFailure = false - // for calculating framerate + /// These are used when calculating framerate var frameCount:Int = 0 var frameCountTimer: Timer? - // compensate the pitch beeing 90 degrees on ARKit + // A quaternion used to compensate for the pitch beeing 90 degrees on ARKit; used to calculate the current device transformation for each frame let compensationQuat:simd_quatf = simd_quaternion(Float(sin(45 / (180 / Float.pi))), 0, 0, Float(cos(45 / (180 / Float.pi)))) - var orientationQuat:simd_quatf = simd_quaternion(0, 0, 0, 0) + + /// The quaternion used to represent the device orientation; used to calculate the current device transformation for each frame; defaults to landcape-left + var orientationQuat:simd_quatf = simd_quaternion(0, 0, 1, 0) // MARK: Initializers @@ -80,11 +100,15 @@ public class ArcGISARView: UIView { sharedInitialization() } + /// Initializer used to denote whether to display the live camera image + /// + /// - Parameter renderVideoFeed: whether to dispaly the live camera image public convenience init(renderVideoFeed: Bool){ self.init(frame: CGRect.zero) self.renderVideoFeed = renderVideoFeed } + /// Initialization code shared between all initializers private func sharedInitialization(){ // // ARKit initialization @@ -99,39 +123,54 @@ public class ArcGISARView: UIView { addSubviewWithConstraints(sceneView) // - // make our sceneView's background transparent + // make our sceneView's background transparent, no atmosphereEffect sceneView.isBackgroundTransparent = true sceneView.atmosphereEffect = .none + + // + // tell the sceneView we will be calling `renderFrame()` manually sceneView.isManualRendering = true + // + // we haven't yet notified user of start or failure notifiedStartOrFailure = false + // + // set intitial orientationQuat orientationChanged(notification: nil) - - //figure out how to do this better: - arConfiguration.worldAlignment = .gravityAndHeading } - + // MARK: Public + /// Determines the map point for the given screen point + /// + /// - Parameter screenPoint: the point in screen coordinates + /// - Returns: the map point corresponding to screenPoint public func arScreenToLocation(screenPoint: AGSPoint) -> AGSPoint { return AGSPoint(x: 0.0, y: 0.0, spatialReference: nil) } + /// Resets the device tracking, using originCamera if it's not nil or the device's GPS location via the locationManager public func resetTracking() { - // reset initial location, so we're sure to set it from the LocationManager (provided originCamera == nil) initialLocation = nil startTracking() } + /// Resets the device tracking, using the device's GPS location via the locationManager + /// + /// - Returns: reset operation success or failure public func resetUsingLocationServices() -> Bool { return false } + /// Resets the device tracking using a spacial anchor + /// + /// - Returns: reset operation success or failure public func resetUsingSpatialAnchor() -> Bool { return false } + /// Starts device tracking public func startTracking() { if !isSupported { @@ -139,14 +178,16 @@ public class ArcGISARView: UIView { return } - // TODO: look at original beta code and grab locationmanager started stuff if let origin = originCamera { - //set origin on sceneView??? + // + // we have a starting camera initialTransformationMatrix = origin.transformationMatrix sceneView.setViewpointCamera(origin) finalizeStart() } else { + // + // no starting camera, use location manger to get initial location let authStatus = CLLocationManager.authorizationStatus() switch authStatus { case .notDetermined: @@ -158,6 +199,8 @@ public class ArcGISARView: UIView { } } + // + // we need to know when the device orientation changes in order to update the Camera transformation UIDevice.current.beginGeneratingDeviceOrientationNotifications() NotificationCenter.default.addObserver( self, @@ -166,6 +209,7 @@ public class ArcGISARView: UIView { object: nil ) + // // reset frameCount and start timer to capture frame rate frameCount = 0 frameCountTimer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true, block: { [weak self] (timer) in @@ -174,6 +218,7 @@ public class ArcGISARView: UIView { }) } + /// Suspends device tracking public func stopTracking() { arSCNView.session.pause() stopUpdatingLocationAndHeading() @@ -185,12 +230,22 @@ public class ArcGISARView: UIView { } // MARK: Private + + /// Operations that happen after device tracking has started fileprivate func finalizeStart() { + // + // hide the camera image if necessary arSCNView.isHidden = !renderVideoFeed + + // + // run the ARSession arSCNView.session.run(arConfiguration, options:.resetTracking) didStartOrFailWithError(nil) } + /// Adds subView to superView with appropriate constraints + /// + /// - Parameter subview: the subView to add fileprivate func addSubviewWithConstraints(_ subview: UIView) { // add subview to view and setup constraints addSubview(subview) @@ -203,6 +258,7 @@ public class ArcGISARView: UIView { ]) } + /// Start the locationManager with undetermined access fileprivate func startWithAccessNotDetermined() { if (Bundle.main.object(forInfoDictionaryKey: "NSLocationWhenInUseUsageDescription") != nil) { locationManager.requestWhenInUseAuthorization() @@ -215,6 +271,7 @@ public class ArcGISARView: UIView { } } + /// Start updating the location and heading via the locationManager fileprivate func startUpdatingLocationAndHeading() { locationManager.startUpdatingLocation() if CLLocationManager.headingAvailable() { @@ -222,6 +279,7 @@ public class ArcGISARView: UIView { } } + /// Stop updating location and heading fileprivate func stopUpdatingLocationAndHeading() { locationManager.stopUpdatingLocation() if CLLocationManager.headingAvailable() { @@ -229,14 +287,19 @@ public class ArcGISARView: UIView { } } + /// Start the locationManager with denied access fileprivate func startWithAccessDenied() { didStartOrFailWithError(ArcGISARView.accessDeniedError()) } + /// Start the locationManager with authorized access fileprivate func startWithAccessAuthorized() { startUpdatingLocationAndHeading() } + /// Potential notification to the user of an error starting device tracking + /// + /// - Parameter error: error that ocurred when starting tracking fileprivate func didStartOrFailWithError(_ error: Error?) { if !notifiedStartOrFailure, let error = error { // TODO: present error to user... @@ -246,6 +309,7 @@ public class ArcGISARView: UIView { notifiedStartOrFailure = true; } + /// Handle a change in authorization status to "denied" fileprivate func handleAuthStatusChangedAccessDenied() { // auth status changed to denied stopUpdatingLocationAndHeading() @@ -254,6 +318,7 @@ public class ArcGISARView: UIView { didStartOrFailWithError(ArcGISARView.accessDeniedError()) } + /// Handle a change in authorization status to "authorized" fileprivate func handleAuthStatusChangedAccessAuthorized() { // auth status changed to authorized // we were waiting for status to come in to start the datasource @@ -264,7 +329,9 @@ public class ArcGISARView: UIView { startUpdatingLocationAndHeading() } - // Called when device orientation changes + /// Called when device orientation changes + /// + /// - Parameter notification: the notification @objc func orientationChanged(notification: Notification?) { // handle rotation here switch UIApplication.shared.statusBarOrientation { @@ -282,28 +349,35 @@ public class ArcGISARView: UIView { } // MARK: Errors - class func notSupportedError() -> NSError { + + /// Error used when ARKit is not supported on the current device + /// + /// - Returns: error stating ARKit not supported + fileprivate class func notSupportedError() -> NSError { let userInfo = [NSLocalizedDescriptionKey : "The device does not support ARKit functionality."] return NSError(domain: AGSErrorDomain, code: 0, userInfo: userInfo) } - class func accessDeniedError() -> NSError{ + /// Error used when access to the device location is denied + /// + /// - Returns: error stating access to location information is denied + fileprivate class func accessDeniedError() -> NSError{ let userInfo = [NSLocalizedDescriptionKey : "Access to the device location is denied."] return NSError(domain: kCLErrorDomain, code: CLError.Code.denied.rawValue, userInfo: userInfo) } - class func missingPListKeyError() -> NSError{ + /// Error used when required plist information is missing + /// + /// - Returns: error stating plist information is missing + fileprivate class func missingPListKeyError() -> NSError{ let userInfo = [NSLocalizedDescriptionKey : "You must specify a location usage description key (NSLocationWhenInUseUsageDescription or NSLocationAlwaysUsageDescription) in your plist."] return NSError(domain: kCLErrorDomain, code: CLError.Code.denied.rawValue, userInfo: userInfo) } } -var once = true -var compensationApplied = false - // MARK: - ARSessionDelegate + extension ArcGISARView: ARSessionDelegate { - // AR session delegate methods /** This is called when a new frame has been updated. @@ -567,7 +641,6 @@ extension ArcGISARView: SCNSceneRendererDelegate { scnSceneRendererDelegate?.renderer?(renderer, didSimulatePhysicsAtTime: time) } - @available(iOS 11.0, *) public func renderer(_ renderer: SCNSceneRenderer, didApplyConstraintsAtTime time: TimeInterval) { scnSceneRendererDelegate?.renderer?(renderer, didApplyConstraintsAtTime: time) } @@ -585,9 +658,9 @@ extension ArcGISARView: SCNSceneRendererDelegate { quaternionY: Double(finalQuat.vector.y), quaternionZ: Double(finalQuat.vector.z), quaternionW: Double(finalQuat.vector.w), - translationX: Double(cameraTransform.columns.3.x), - translationY: Double(-cameraTransform.columns.3.z), - translationZ: Double(cameraTransform.columns.3.y)) + translationX: Double(cameraTransform.columns.3.x) * translationTransformationFactor, + translationY: Double(-cameraTransform.columns.3.z) * translationTransformationFactor, + translationZ: Double(cameraTransform.columns.3.y) * translationTransformationFactor) transformationMatrix = initialTransformationMatrix.addTransformation(transformationMatrix) From a4b1185f9390152ad27dd43dc6efac2fbcfefaa4 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Mon, 3 Jun 2019 10:32:31 -0500 Subject: [PATCH 025/147] Remove ArcGISARSensorView --- .../ArcGISToolkit.xcodeproj/project.pbxproj | 4 - .../ArcGISToolkit/AR/ArcGISARSensorView.swift | 586 ------------------ 2 files changed, 590 deletions(-) delete mode 100644 Toolkit/ArcGISToolkit/AR/ArcGISARSensorView.swift diff --git a/Toolkit/ArcGISToolkit.xcodeproj/project.pbxproj b/Toolkit/ArcGISToolkit.xcodeproj/project.pbxproj index 07dca98a..431ea566 100644 --- a/Toolkit/ArcGISToolkit.xcodeproj/project.pbxproj +++ b/Toolkit/ArcGISToolkit.xcodeproj/project.pbxproj @@ -23,7 +23,6 @@ 88DBC2A31FE83DB800255921 /* CancelGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88DBC2A21FE83DB800255921 /* CancelGroup.swift */; }; 88ECCC931DF92F22000C967E /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 88ECCC921DF92F22000C967E /* Assets.xcassets */; }; E447A1262266629600578C0B /* ArcGISARView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E447A1252266629600578C0B /* ArcGISARView.swift */; }; - E447A1282266630400578C0B /* ArcGISARSensorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E447A1272266630400578C0B /* ArcGISARSensorView.swift */; }; E46893291FEDAE36008ADA79 /* Compass.swift in Sources */ = {isa = PBXBuildFile; fileRef = E46893281FEDAE36008ADA79 /* Compass.swift */; }; E48405731E9BE7B700927208 /* LegendViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E48405721E9BE7B700927208 /* LegendViewController.swift */; }; E484057A1E9C262D00927208 /* Legend.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = E48405791E9C262D00927208 /* Legend.storyboard */; }; @@ -48,7 +47,6 @@ 88DBC2A21FE83DB800255921 /* CancelGroup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CancelGroup.swift; sourceTree = ""; }; 88ECCC921DF92F22000C967E /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = ArcGISToolkit/Assets.xcassets; sourceTree = ""; }; E447A1252266629600578C0B /* ArcGISARView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArcGISARView.swift; sourceTree = ""; }; - E447A1272266630400578C0B /* ArcGISARSensorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArcGISARSensorView.swift; sourceTree = ""; }; E46893281FEDAE36008ADA79 /* Compass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Compass.swift; sourceTree = ""; }; E48405721E9BE7B700927208 /* LegendViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LegendViewController.swift; sourceTree = ""; }; E48405791E9C262D00927208 /* Legend.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = Legend.storyboard; sourceTree = ""; }; @@ -137,7 +135,6 @@ isa = PBXGroup; children = ( E447A1252266629600578C0B /* ArcGISARView.swift */, - E447A1272266630400578C0B /* ArcGISARSensorView.swift */, ); path = AR; sourceTree = ""; @@ -229,7 +226,6 @@ 88B68A041E96EFD700B67FAB /* Scalebar.swift in Sources */, 88B68A081E96EFD700B67FAB /* UnitsViewController.swift in Sources */, E48405731E9BE7B700927208 /* LegendViewController.swift in Sources */, - E447A1282266630400578C0B /* ArcGISARSensorView.swift in Sources */, E447A1262266629600578C0B /* ArcGISARView.swift in Sources */, 883EA74F20741B9C006D6F72 /* TemplatePickerViewController.swift in Sources */, 883EA74920741A4C006D6F72 /* PopupController.swift in Sources */, diff --git a/Toolkit/ArcGISToolkit/AR/ArcGISARSensorView.swift b/Toolkit/ArcGISToolkit/AR/ArcGISARSensorView.swift deleted file mode 100644 index ce01a6dc..00000000 --- a/Toolkit/ArcGISToolkit/AR/ArcGISARSensorView.swift +++ /dev/null @@ -1,586 +0,0 @@ -// -// Copyright 2019 Esri. - -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// http://www.apache.org/licenses/LICENSE-2.0 - -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -import UIKit -import AVFoundation -import CoreMotion -import ArcGIS - -public enum LocationType { - case anglesOnly - case positionOnly - case anglesAndPosition -} - -public class ArcGISARSensorView: UIView { - - /// Location type specifying what information we are interested in - public var locationType: LocationType = .anglesAndPosition { - didSet { - // need to update location and heading updates to account for new LocationType - } - } - - /// The view used to display ArcGIS 3D content - public var sceneView = AGSSceneView(frame: .zero) { - willSet(newSceneview) { - removeSubviewAndConstraints(sceneView) - } - didSet { - addSubviewWithConstraints(sceneView) - } - } - - /// Determines whether to use an absolution heading as opposed to a heading relative to the original heading - public var useAbsoluteHeading: Bool = true - - // MARK: private properties - - /// Whether to display the camera image or not - public var renderVideoFeed = true - - /// Whether the client has been notfiied of start/failure - private var notifiedStartOrFailure = false - - /// Used to determine the device location - private lazy var locationManager: CLLocationManager = { - let lm = CLLocationManager() - lm.desiredAccuracy = kCLLocationAccuracyBest - lm.delegate = self - return lm - }() - - /// Used to determine the device motion - private lazy var motionManager: CMMotionManager = { - let mm = CMMotionManager() - mm.deviceMotionUpdateInterval = 1.0 / 60.0 - mm.showsDeviceMovementDisplay = true - return mm - }() - - /// Initial location from locationManager - private var initialLocation: AGSPoint? - - // MARK: Capture session - - /// Capture session setup results - private enum SessionSetupResult { - case success - case notAuthorized - case configurationFailed - } - - /// The current AVCaptureSession - private lazy var session = AVCaptureSession() - - // Communicate with the capture session and other session objects on this queue. - private let sessionQueue = DispatchQueue(label: "session queue", attributes: [], target: nil) - private var setupResult: SessionSetupResult = .success - var videoDeviceInput: AVCaptureDeviceInput! - - /// The view the camera image is displayed in - private lazy var cameraView = CameraView(frame:CGRect.zero) - - /// The quaternion used to represent the device orientation; used to calculate the current device transformation for each frame; defaults to landcape-left - private var orientationQuat: simd_quatf = simd_quaternion(Float(0), Float(0), Float(0), Float(1)) - - // MARK: intializers - - override init(frame: CGRect) { - super.init(frame: frame) - sharedInitialization() - } - - required public init?(coder aDecoder: NSCoder) { - super.init(coder: aDecoder) - sharedInitialization() - } - - /// Initializer used to denote whether to display the live camera image - /// - /// - Parameter renderVideoFeed: whether to dispaly the live camera image - required public convenience init(renderVideoFeed: Bool){ - self.init(frame: CGRect.zero) - self.renderVideoFeed = renderVideoFeed - } - - /// Initialization code shared between all initializers - private func sharedInitialization(){ - - // - // make our sceneView's background transparent - sceneView.isBackgroundTransparent = true - sceneView.atmosphereEffect = .none - - // add sceneView to our view - addSubviewWithConstraints(sceneView) - - if renderVideoFeed { - // set up the video preview view. - addSubviewWithConstraints(cameraView, index: 0) - cameraView.session = session - - prepVideoFeed() - } - } - - /// Starts device tracking - public func startTracking() { - notifiedStartOrFailure = false - - // determine status of location manager - let authStatus = CLLocationManager.authorizationStatus() - switch authStatus { - case .notDetermined: - startWithAccessNotDetermined() - case .restricted, .denied: - startWithAccessDenied() - case .authorizedAlways, .authorizedWhenInUse: - startWithAccessAuthorized() - } - - if renderVideoFeed { - setupSession() - } - } - - /// Suspends device tracking - public func stopTracking() { - locationManager.stopUpdatingLocation() - if CLLocationManager.headingAvailable() { - locationManager.stopUpdatingHeading() - } - - motionManager.stopDeviceMotionUpdates() - - sessionQueue.async { [weak self] in - guard let strongSelf = self else { return } - if strongSelf.setupResult == .success { - strongSelf.session.stopRunning() - - UIDevice.current.endGeneratingDeviceOrientationNotifications() - NotificationCenter.default.removeObserver(strongSelf) - } - } - } - - /// Called when device orientation changes - /// - /// - Parameter notification: the notification - @objc func orientationChanged(notification: Notification) { - // handle rotation here - updateCameraViewOrientation() - } - - /// Adds subView to superView with appropriate constraints - /// - /// - Parameter subview: the subView to add - private func addSubviewWithConstraints(_ subview: UIView, index: Int = -1) { - // add subView to view and setup constraints - if index >= 0 { - insertSubview(subview, at: index) - } - else { - addSubview(subview) - } - subview.translatesAutoresizingMaskIntoConstraints = false - NSLayoutConstraint.activate([ - subview.leadingAnchor.constraint(equalTo: self.leadingAnchor), - subview.trailingAnchor.constraint(equalTo: self.trailingAnchor), - subview.topAnchor.constraint(equalTo: self.topAnchor), - subview.bottomAnchor.constraint(equalTo: self.bottomAnchor) - ]) - } - - /// Removes subView and constraints from superView - /// - /// - Parameter subview: the subView to add - private func removeSubviewAndConstraints(_ subview: UIView) { - subview.removeFromSuperview() - removeConstraints(subview.constraints) - } - - /// Start the locationManager with undetermined access - private func startWithAccessNotDetermined() { - if (Bundle.main.object(forInfoDictionaryKey: "NSLocationWhenInUseUsageDescription") != nil) { - locationManager.requestWhenInUseAuthorization() - } - if (Bundle.main.object(forInfoDictionaryKey: "NSLocationAlwaysUsageDescription") != nil) { - locationManager.requestAlwaysAuthorization() - } - else{ - didStartOrFailWithError(ArcGISARSensorView.missingPListKeyError()) - } - } - - /// Start updating the location and heading via the locationManager - private func startUpdatingLocationAndHeading() { - locationManager.startUpdatingLocation() - if CLLocationManager.headingAvailable() { - locationManager.startUpdatingHeading() - } - - startUpdatingAngles() - } - - private func startWithAccessDenied() { - didStartOrFailWithError(ArcGISARSensorView.accessDeniedError()) - } - - private func startWithAccessAuthorized() { - startUpdatingLocationAndHeading() - } - - private func didStartOrFailWithError(_ error: Error?) { - // TODO: present error to user... - - notifiedStartOrFailure = true; - } - - private func handleAuthStatusChangedAccessDenied() { - // auth status changed to denied - if !notifiedStartOrFailure { - stopTracking() - // we were waiting for user prompt to come back, so notify - didStartOrFailWithError(ArcGISARSensorView.accessDeniedError()) - } - } - - private func handleAuthStatusChangedAccessAuthorized() { - // auth status changed to authorized - if !notifiedStartOrFailure { - // we were waiting for status to come in to start the datasource - // now that we have authorization - start it - didStartOrFailWithError(nil) - - // need to start location manager updates - startUpdatingLocationAndHeading() - } - } - - private func finalizeStart() { - // TODO: is there anything to do here? - } - - /// Start tracking device motion updates to get device orientation - private func startUpdatingAngles() { - let motionQueue = OperationQueue.init() - motionQueue.qualityOfService = .userInteractive - motionQueue.maxConcurrentOperationCount = 1 - - motionManager.startDeviceMotionUpdates(to: motionQueue) { [weak self] (motion, error) in - guard let quat = self?.motionManager.deviceMotion?.attitude.quaternion, - let orientationQuat = self?.orientationQuat else { return } - - let currentQuat = simd_quaternion(Float(quat.x), Float(quat.y), Float(quat.z), Float(quat.w)) - let finalQuat = simd_mul(currentQuat, orientationQuat) - - let transformationMatrix = AGSTransformationMatrix(quaternionX: Double(finalQuat.vector.x), - quaternionY: Double(finalQuat.vector.y), - quaternionZ: Double(finalQuat.vector.z), - quaternionW: Double(finalQuat.vector.w), - translationX: 0, - translationY: 0, - translationZ: 0) - let camera = AGSCamera(transformationMatrix: transformationMatrix) - self?.sceneView.setViewpointCamera(camera) - } - } - - // MARK: Video - - /// Udpate the orientation of the camera view - private func updateCameraViewOrientation() { - if let videoPreviewLayerConnection = cameraView.videoPreviewLayer.connection { - let deviceOrientation = UIDevice.current.orientation - guard let newVideoOrientation = AVCaptureVideoOrientation(rawValue: deviceOrientation.rawValue), - deviceOrientation.isPortrait || deviceOrientation.isLandscape else { - return - } - - videoPreviewLayerConnection.videoOrientation = newVideoOrientation - } - } - - /// Prepare the video feed - private func prepVideoFeed() { - // - // Check video authorization status. Video access is required and audio - // access is optional. If audio access is denied, audio is not recorded - // during movie recording. - // - switch AVCaptureDevice.authorizationStatus(for: .video) { - case .authorized: - // The user has previously granted access to the camera. - setupResult = .success - break - - case .notDetermined: - // The user has not yet been presented with the option to grant - // video access. We suspend the session queue to delay session - // setup until the access request has completed. - // - // Note that audio access will be implicitly requested when we - // create an AVCaptureDeviceInput for audio during session setup. - sessionQueue.suspend() - AVCaptureDevice.requestAccess(for: .video) { [weak self] (accessGranted) in - if !accessGranted { - self?.setupResult = .notAuthorized - } - else { - self?.setupResult = .success - self?.sessionQueue.resume() - } - } - - default: - // The user has previously denied access. - setupResult = .notAuthorized - } - - // Setup the capture session. - // In general it is not safe to mutate an AVCaptureSession or any of its - // inputs, outputs, or connections from multiple threads at the same time. - // - // Why not do all of this on the main queue? - // Because AVCaptureSession.startRunning() is a blocking call which can - // take a long time. We dispatch session setup to the sessionQueue so - // that the main queue isn't blocked, which keeps the UI responsive. - sessionQueue.async { [weak self] in - self?.configureSession() - } - } - - /// Setup the capture session - private func setupSession() { - // session setup - sessionQueue.async { - switch self.setupResult { - case .success: - self.session.startRunning() - DispatchQueue.main.async { - - self.updateCameraViewOrientation() - - // add observer to catch orientation changes - UIDevice.current.beginGeneratingDeviceOrientationNotifications() - NotificationCenter.default.addObserver( - self, - selector: #selector(self.orientationChanged(notification:)), - name: UIDevice.orientationDidChangeNotification, - object: nil - ) - } - - case .notAuthorized: - DispatchQueue.main.async { - let message = NSLocalizedString("ArcGISARSensorView does not have permission to use the camera, please change privacy settings", comment: "Alert message when the user has denied access to the camera") - let alertController = UIAlertController(title: "ArcGISARSensorView", message: message, preferredStyle: .alert) - alertController.addAction(UIAlertAction(title: NSLocalizedString("OK", comment: "Alert OK button"), style: .cancel, handler: nil)) - alertController.addAction(UIAlertAction(title: NSLocalizedString("Settings", comment: "Alert button to open Settings"), style: .`default`, handler: { action in - UIApplication.shared.open(URL(string: UIApplication.openSettingsURLString)!, options: [:], completionHandler: nil) - })) - - guard let rootController = UIApplication.shared.keyWindow?.rootViewController else { return } - rootController.present(alertController, animated: true, completion: nil) - } - - case .configurationFailed: - DispatchQueue.main.async { - let message = NSLocalizedString("Unable to capture media", comment: "Alert message when something goes wrong during capture session configuration") - let alertController = UIAlertController(title: "ArcGISARSensorView", message: message, preferredStyle: .alert) - alertController.addAction(UIAlertAction(title: NSLocalizedString("OK", comment: "Alert OK button"), style: .cancel, handler: nil)) - - guard let rootController = UIApplication.shared.keyWindow?.rootViewController else { return } - rootController.present(alertController, animated: true, completion: nil) - } - } - } - } - - // MARK: Session Management - - // Call this on the session queue. - private func configureSession() { - if setupResult != .success { - return - } - - session.beginConfiguration() - session.sessionPreset = .high - - // Add video input. - do { - var defaultVideoDevice: AVCaptureDevice? - - // Choose the back dual camera if available, otherwise default to a wide angle camera. - // if let dualCameraDevice = AVCaptureDevice.defaultDevice(withDeviceType: .builtInDuoCamera, mediaType: AVMediaTypeVideo, position: .back) { - // defaultVideoDevice = dualCameraDevice - // } - // else if let backCameraDevice = AVCaptureDevice.defaultDevice(withDeviceType: .builtInWideAngleCamera, mediaType: AVMediaTypeVideo, position: .back) { - // // If the back dual camera is not available, default to the back wide angle camera. - // defaultVideoDevice = backCameraDevice - // } - // else if let frontCameraDevice = AVCaptureDevice.defaultDevice(withDeviceType: .builtInWideAngleCamera, mediaType: AVMediaTypeVideo, position: .front) { - // In some cases where users break their phones, the back wide angle camera is not available. In this case, we should default to the front wide angle camera. - defaultVideoDevice = AVCaptureDevice.default(for: .video) - // } - - let videoDeviceInput = try AVCaptureDeviceInput(device: defaultVideoDevice!) - - if session.canAddInput(videoDeviceInput) { - session.addInput(videoDeviceInput) - self.videoDeviceInput = videoDeviceInput - } - else { - print("Could not add video device input to the session") - setupResult = .configurationFailed - session.commitConfiguration() - return - } - } - catch { - print("Could not create video device input: \(error)") - setupResult = .configurationFailed - session.commitConfiguration() - return - } - - session.commitConfiguration() - } - - // MARK: Errors - - /// Error used when access to the device location is denied - /// - /// - Returns: error stating access to location information is denied - fileprivate class func accessDeniedError() -> NSError{ - let userInfo = [NSLocalizedDescriptionKey : "Access to the device location is denied."] - return NSError(domain: kCLErrorDomain, code: CLError.Code.denied.rawValue, userInfo: userInfo) - } - - /// Error used when required plist information is missing - /// - /// - Returns: error stating plist information is missing - fileprivate class func missingPListKeyError() -> NSError{ - let userInfo = [NSLocalizedDescriptionKey : "You must specify a location usage description key (NSLocationWhenInUseUsageDescription or NSLocationAlwaysUsageDescription) in your plist."] - return NSError(domain: kCLErrorDomain, code: CLError.Code.denied.rawValue, userInfo: userInfo) - } - -} - -// MARK: - CLLocationManagerDelegate - -extension ArcGISARSensorView: CLLocationManagerDelegate { - /* - * locationManager:didUpdateLocations: - * - * Discussion: - * Invoked when new locations are available. Required for delivery of - * deferred locations. If implemented, updates will - * not be delivered to locationManager:didUpdateToLocation:fromLocation: - * - * locations is an array of CLLocation objects in chronological order. - */ - public func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) { - guard let location = locations.last, location.horizontalAccuracy >= 0.0 else { return } - - let locationPoint = AGSPoint(x: location.coordinate.longitude, - y: location.coordinate.latitude, - z: location.altitude + 100, - spatialReference: .wgs84()) - - if initialLocation == nil { - initialLocation = locationPoint - let camera = AGSCamera(location: locationPoint, heading: 0, pitch: 0, roll: 0) - sceneView.setViewpointCamera(camera) - finalizeStart() - } else { - let camera = sceneView.currentViewpointCamera().move(toLocation: locationPoint) - sceneView.setViewpointCamera(camera) - } - } - - /* - * locationManager:didFailWithError: - * - * Discussion: - * Invoked when an error has occurred. Error types are defined in "CLError.h". - */ - public func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) { - didStartOrFailWithError(error) - } - - /* - * locationManager:didChangeAuthorizationStatus: - * - * Discussion: - * Invoked when the authorization status changes for this application. - */ - public func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) { - let authStatus = CLLocationManager.authorizationStatus() - switch authStatus { - case .notDetermined: - break - case .restricted, .denied: - self.handleAuthStatusChangedAccessDenied() - case .authorizedAlways, .authorizedWhenInUse: - self.handleAuthStatusChangedAccessAuthorized() - } - } - - /* - * Discussion: - * Invoked when location updates are automatically paused. - */ - public func locationManagerDidPauseLocationUpdates(_ manager: CLLocationManager) { - - } - - /* - * Discussion: - * Invoked when location updates are automatically resumed. - * - * In the event that your application is terminated while suspended, you will - * not receive this notification. - */ - @available(iOS 6.0, *) - public func locationManagerDidResumeLocationUpdates(_ manager: CLLocationManager) { - - } -} - -// MARK: CameraView - -/// CameraView - view which displays the live camera image -class CameraView: UIView { - var videoPreviewLayer: AVCaptureVideoPreviewLayer { - return layer as! AVCaptureVideoPreviewLayer - } - - var session: AVCaptureSession? { - get { - return videoPreviewLayer.session - } - set { - videoPreviewLayer.session = newValue - } - } - - // MARK: UIView - - override class var layerClass: AnyClass { - return AVCaptureVideoPreviewLayer.self - } -} From e24645a70d3daea880eb35f6109942df50d89e1c Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Tue, 4 Jun 2019 13:48:34 -0500 Subject: [PATCH 026/147] PR review changes; doc updates, new frameRate calculation, etc. --- .../ArcGISToolkitExamples/ARExample.swift | 43 +-- Toolkit/ArcGISToolkit/AR/ArcGISARView.swift | 333 +++++------------- 2 files changed, 108 insertions(+), 268 deletions(-) diff --git a/Examples/ArcGISToolkitExamples/ARExample.swift b/Examples/ArcGISToolkitExamples/ARExample.swift index 0ffdd76d..90b56fbe 100644 --- a/Examples/ArcGISToolkitExamples/ARExample.swift +++ b/Examples/ArcGISToolkitExamples/ARExample.swift @@ -19,13 +19,27 @@ import ArcGIS open class ARExample: UIViewController { public let arView = ArcGISARView(frame: CGRect.zero) + + private let scene: AGSScene = { + // Creates a scene with the streets basemap. + let scene = AGSScene(basemapType: .streets) + + // create elevation surface + let elevationSource = AGSArcGISTiledElevationSource(url: URL(string: "https://elevation3d.arcgis.com/arcgis/rest/services/WorldElevation3D/Terrain3D/ImageServer")!) + let surface = AGSSurface() + surface.elevationSources = [elevationSource] + surface.name = "baseSurface" + surface.isEnabled = true + surface.backgroundGrid.isVisible = false + scene.baseSurface = surface + + return scene + }() override open func viewDidLoad() { super.viewDidLoad() - // - // Example of how to get ARSessionDelegate methods from the ArcGISARView - // + // Example of how to get ARSessionDelegate methods from the ArcGISARView. arView.sessionDelegate = self view.addSubview(arView) @@ -37,7 +51,7 @@ open class ARExample: UIViewController { arView.bottomAnchor.constraint(equalTo: view.bottomAnchor) ]) - arView.sceneView.scene = scene() + arView.sceneView.scene = scene } override open func viewDidAppear(_ animated: Bool) { @@ -47,30 +61,11 @@ open class ARExample: UIViewController { override open func viewDidDisappear(_ animated: Bool) { arView.stopTracking() } - - private func scene() -> AGSScene { - - // create scene with the streets basemap - let scene = AGSScene(basemapType: .streets) - - // create elevation surface - let elevationSource = AGSArcGISTiledElevationSource(url: URL(string: "http://elevation3d.arcgis.com/arcgis/rest/services/WorldElevation3D/Terrain3D/ImageServer")!) - let surface = AGSSurface() - surface.elevationSources = [elevationSource] - surface.name = "baseSurface" - surface.isEnabled = true - surface.backgroundGrid.isVisible = false - scene.baseSurface = surface - - return scene - } } extension ARExample: ARSessionDelegate { public func session(_ session: ARSession, didUpdate frame: ARFrame) { - // - // Example of how to get ARSessionDelegate methods from the ArcGISARView - // + // Example of how to get ARSessionDelegate methods from the ArcGISARView. } } diff --git a/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift b/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift index 09b37078..875c3683 100644 --- a/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift +++ b/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift @@ -20,42 +20,42 @@ public class ArcGISARView: UIView { // MARK: public properties - /// The view used to display the ARKit camera image and 3D SceneKit content + /// The view used to display the `ARKit` camera image and 3D `SceneKit` content. public lazy private(set) var arSCNView = ARSCNView(frame: .zero) - /// The view used to display ArcGIS 3D content + /// The view used to display ArcGIS 3D content. public lazy private(set) var sceneView = AGSSceneView(frame: .zero) - /// The world tracking information used by ARKit + /// The world tracking information used by `ARKit`. public var arConfiguration: ARConfiguration = { let config = ARWorldTrackingConfiguration() config.worldAlignment = .gravityAndHeading return config }() { didSet { - //start tracking using the new configuration + // Start tracking using the new configuration. startTracking() } } - /// The viewpoint camera used to set the initial view of the sceneView instead of the devices GPS location via the locationManager + /// The viewpoint camera used to set the initial view of the sceneView instead of the devices GPS location via the locationManager. public var originCamera: AGSCamera? - /// The translation factor used to support a table top AR experience + /// The translation factor used to support a table top AR experience. public var translationTransformationFactor: Double = 1.0 - // We implement ARSessionDelegate methods, but will use `sessionDelegate` to forward them to clients + // We implement ARSessionDelegate methods, but will use `sessionDelegate` to forward them to clients. weak open var sessionDelegate: ARSessionDelegate? - // We implement SCNSceneRendererDelegate methods, but will use `scnSceneRendererDelegate` to forward them to clients + // We implement SCNSceneRendererDelegate methods, but will use `scnSceneRendererDelegate` to forward them to clients. weak open var scnSceneRendererDelegate: SCNSceneRendererDelegate? // MARK: private properties - /// Whether to display the camera image or not + /// Whether to display the camera image or not. private var renderVideoFeed = true - /// Used to determine the device location when originCamera is not set + /// Used to determine the device location when originCamera is not set. private lazy var locationManager: CLLocationManager = { let lm = CLLocationManager() lm.desiredAccuracy = kCLLocationAccuracyBest @@ -63,30 +63,29 @@ public class ArcGISARView: UIView { return lm }() - /// Initial location from locationManager + /// Initial location from locationManager. private var initialLocation: CLLocation? - /// Current horizontal accuracy of the device + /// Current horizontal accuracy of the device. private var horizontalAccuracy: CLLocationAccuracy = .greatestFiniteMagnitude; - /// The intial camera position and orientation whether it was set via originCamera or the locationManager + /// The intial camera position and orientation whether it was set via originCamera or the locationManager. private var initialTransformationMatrix = AGSTransformationMatrix() - /// Whether ARKit supported on this device? + /// Whether `ARKit` is supported on this device. private var isSupported = false - /// Whether the client has been notfiied of start/failure + /// Whether the client has been notfiied of start/failure. private var notifiedStartOrFailure = false - /// These are used when calculating framerate - var frameCount:Int = 0 - var frameCountTimer: Timer? + /// Used when calculating framerate. + private var lastUpdateTime: TimeInterval = 0 - // A quaternion used to compensate for the pitch beeing 90 degrees on ARKit; used to calculate the current device transformation for each frame - let compensationQuat:simd_quatf = simd_quaternion(Float(sin(45 / (180 / Float.pi))), 0, 0, Float(cos(45 / (180 / Float.pi)))) + // A quaternion used to compensate for the pitch beeing 90 degrees on `ARKit`; used to calculate the current device transformation for each frame. + let compensationQuat:simd_quatd = simd_quaternion((sin(45 / (180 / .pi))), 0, 0, (cos(45 / (180 / .pi)))) - /// The quaternion used to represent the device orientation; used to calculate the current device transformation for each frame; defaults to landcape-left - var orientationQuat:simd_quatf = simd_quaternion(0, 0, 1, 0) + /// The quaternion used to represent the device orientation; used to calculate the current device transformation for each frame; defaults to landcape-left. + var orientationQuat:simd_quatd = simd_quaternion(0, 0, 1, 0) // MARK: Initializers @@ -100,94 +99,85 @@ public class ArcGISARView: UIView { sharedInitialization() } - /// Initializer used to denote whether to display the live camera image + /// Initializer used to denote whether to display the live camera image. /// - /// - Parameter renderVideoFeed: whether to dispaly the live camera image + /// - Parameter renderVideoFeed: whether to dispaly the live camera image. public convenience init(renderVideoFeed: Bool){ - self.init(frame: CGRect.zero) + self.init(frame: .zero) self.renderVideoFeed = renderVideoFeed } - /// Initialization code shared between all initializers + /// Initialization code shared between all initializers. private func sharedInitialization(){ - // - // ARKit initialization + // `ARKit` initialization. isSupported = ARWorldTrackingConfiguration.isSupported addSubviewWithConstraints(arSCNView) arSCNView.session.delegate = self (arSCNView as SCNSceneRenderer).delegate = self - // - // add sceneView to view and setup constraints + // Add sceneView to view and setup constraints. addSubviewWithConstraints(sceneView) - // - // make our sceneView's background transparent, no atmosphereEffect + // Make our sceneView's background transparent, no atmosphereEffect. sceneView.isBackgroundTransparent = true sceneView.atmosphereEffect = .none - // - // tell the sceneView we will be calling `renderFrame()` manually + // Tell the sceneView we will be calling `renderFrame()` manually. sceneView.isManualRendering = true - // - // we haven't yet notified user of start or failure + // We haven't yet notified user of start or failure. notifiedStartOrFailure = false - // - // set intitial orientationQuat + // Set intitial orientationQuat. orientationChanged(notification: nil) } // MARK: Public - /// Determines the map point for the given screen point + /// Determines the map point for the given screen point. /// - /// - Parameter screenPoint: the point in screen coordinates - /// - Returns: the map point corresponding to screenPoint + /// - Parameter screenPoint: the point in screen coordinates. + /// - Returns: The map point corresponding to screenPoint. public func arScreenToLocation(screenPoint: AGSPoint) -> AGSPoint { return AGSPoint(x: 0.0, y: 0.0, spatialReference: nil) } - /// Resets the device tracking, using originCamera if it's not nil or the device's GPS location via the locationManager + /// Resets the device tracking, using originCamera if it's not nil or the device's GPS location via the locationManager. public func resetTracking() { initialLocation = nil startTracking() } - /// Resets the device tracking, using the device's GPS location via the locationManager + /// Resets the device tracking, using the device's GPS location via the locationManager. /// - /// - Returns: reset operation success or failure + /// - Returns: Reset operation success or failure. public func resetUsingLocationServices() -> Bool { return false } - /// Resets the device tracking using a spacial anchor + /// Resets the device tracking using a spacial anchor. /// - /// - Returns: reset operation success or failure + /// - Returns: Reset operation success or failure. public func resetUsingSpatialAnchor() -> Bool { return false } - /// Starts device tracking + /// Starts device tracking. public func startTracking() { - if !isSupported { didStartOrFailWithError(ArcGISARView.notSupportedError()) return } if let origin = originCamera { - // - // we have a starting camera + // We have a starting camera. initialTransformationMatrix = origin.transformationMatrix sceneView.setViewpointCamera(origin) finalizeStart() } else { - // - // no starting camera, use location manger to get initial location + // No starting camera, use location manger to get initial location. let authStatus = CLLocationManager.authorizationStatus() switch authStatus { case .notDetermined: @@ -199,8 +189,7 @@ public class ArcGISARView: UIView { } } - // - // we need to know when the device orientation changes in order to update the Camera transformation + // We need to know when the device orientation changes in order to update the Camera transformation. UIDevice.current.beginGeneratingDeviceOrientationNotifications() NotificationCenter.default.addObserver( self, @@ -208,46 +197,34 @@ public class ArcGISARView: UIView { name: UIDevice.orientationDidChangeNotification, object: nil ) - - // - // reset frameCount and start timer to capture frame rate - frameCount = 0 - frameCountTimer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true, block: { [weak self] (timer) in - print("Frame rate = \(String(reflecting: self?.frameCount))") - self?.frameCount = 0 - }) } - /// Suspends device tracking + /// Suspends device tracking. public func stopTracking() { arSCNView.session.pause() stopUpdatingLocationAndHeading() UIDevice.current.endGeneratingDeviceOrientationNotifications() NotificationCenter.default.removeObserver(self) - - frameCountTimer?.invalidate() } // MARK: Private - /// Operations that happen after device tracking has started + /// Operations that happen after device tracking has started. fileprivate func finalizeStart() { - // - // hide the camera image if necessary + // Hide the camera image if necessary. arSCNView.isHidden = !renderVideoFeed - // - // run the ARSession + // Run the ARSession. arSCNView.session.run(arConfiguration, options:.resetTracking) didStartOrFailWithError(nil) } - /// Adds subView to superView with appropriate constraints + /// Adds subView to superView with appropriate constraints. /// - /// - Parameter subview: the subView to add + /// - Parameter subview: the subView to add. fileprivate func addSubviewWithConstraints(_ subview: UIView) { - // add subview to view and setup constraints + // Add subview to view and setup constraints. addSubview(subview) subview.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ @@ -258,7 +235,7 @@ public class ArcGISARView: UIView { ]) } - /// Start the locationManager with undetermined access + /// Start the locationManager with undetermined access. fileprivate func startWithAccessNotDetermined() { if (Bundle.main.object(forInfoDictionaryKey: "NSLocationWhenInUseUsageDescription") != nil) { locationManager.requestWhenInUseAuthorization() @@ -271,7 +248,7 @@ public class ArcGISARView: UIView { } } - /// Start updating the location and heading via the locationManager + /// Start updating the location and heading via the locationManager. fileprivate func startUpdatingLocationAndHeading() { locationManager.startUpdatingLocation() if CLLocationManager.headingAvailable() { @@ -279,7 +256,7 @@ public class ArcGISARView: UIView { } } - /// Stop updating location and heading + /// Stop updating location and heading. fileprivate func stopUpdatingLocationAndHeading() { locationManager.stopUpdatingLocation() if CLLocationManager.headingAvailable() { @@ -287,19 +264,19 @@ public class ArcGISARView: UIView { } } - /// Start the locationManager with denied access + /// Start the locationManager with denied access. fileprivate func startWithAccessDenied() { didStartOrFailWithError(ArcGISARView.accessDeniedError()) } - /// Start the locationManager with authorized access + /// Start the locationManager with authorized access. fileprivate func startWithAccessAuthorized() { startUpdatingLocationAndHeading() } - /// Potential notification to the user of an error starting device tracking + /// Potential notification to the user of an error starting device tracking. /// - /// - Parameter error: error that ocurred when starting tracking + /// - Parameter error: error that ocurred when starting tracking. fileprivate func didStartOrFailWithError(_ error: Error?) { if !notifiedStartOrFailure, let error = error { // TODO: present error to user... @@ -309,31 +286,27 @@ public class ArcGISARView: UIView { notifiedStartOrFailure = true; } - /// Handle a change in authorization status to "denied" + /// Handle a change in authorization status to "denied". fileprivate func handleAuthStatusChangedAccessDenied() { // auth status changed to denied stopUpdatingLocationAndHeading() - // we were waiting for user prompt to come back, so notify + // We were waiting for user prompt to come back, so notify. didStartOrFailWithError(ArcGISARView.accessDeniedError()) } - /// Handle a change in authorization status to "authorized" + /// Handle a change in authorization status to "authorized". fileprivate func handleAuthStatusChangedAccessAuthorized() { - // auth status changed to authorized - // we were waiting for status to come in to start the datasource - // now that we have authorization - start it + // Auth status changed to authorized; now that we have authorization - start updating location and heading. didStartOrFailWithError(nil) - - // need to start location manager updates startUpdatingLocationAndHeading() } - /// Called when device orientation changes + /// Called when device orientation changes. /// - /// - Parameter notification: the notification + /// - Parameter notification: the notification. @objc func orientationChanged(notification: Notification?) { - // handle rotation here + // Handle rotation here. switch UIApplication.shared.statusBarOrientation { case .landscapeLeft: orientationQuat = simd_quaternion(0, 0, 1.0, 0); @@ -350,25 +323,25 @@ public class ArcGISARView: UIView { // MARK: Errors - /// Error used when ARKit is not supported on the current device + /// Error used when `ARKit` is not supported on the current device. /// - /// - Returns: error stating ARKit not supported + /// - Returns: Error stating `ARKit` not supported. fileprivate class func notSupportedError() -> NSError { let userInfo = [NSLocalizedDescriptionKey : "The device does not support ARKit functionality."] - return NSError(domain: AGSErrorDomain, code: 0, userInfo: userInfo) + return NSError(domain: AGSErrorDomain, code: ARError.unsupportedConfiguration.rawValue, userInfo: userInfo) } - /// Error used when access to the device location is denied + /// Error used when access to the device location is denied. /// - /// - Returns: error stating access to location information is denied + /// - Returns: Error stating access to location information is denied. fileprivate class func accessDeniedError() -> NSError{ let userInfo = [NSLocalizedDescriptionKey : "Access to the device location is denied."] return NSError(domain: kCLErrorDomain, code: CLError.Code.denied.rawValue, userInfo: userInfo) } - /// Error used when required plist information is missing + /// Error used when required plist information is missing. /// - /// - Returns: error stating plist information is missing + /// - Returns: Error stating plist information is missing. fileprivate class func missingPListKeyError() -> NSError{ let userInfo = [NSLocalizedDescriptionKey : "You must specify a location usage description key (NSLocationWhenInUseUsageDescription or NSLocationAlwaysUsageDescription) in your plist."] return NSError(domain: kCLErrorDomain, code: CLError.Code.denied.rawValue, userInfo: userInfo) @@ -379,42 +352,18 @@ public class ArcGISARView: UIView { extension ArcGISARView: ARSessionDelegate { - /** - This is called when a new frame has been updated. - - @param session The session being run. - @param frame The frame that has been updated. - */ public func session(_ session: ARSession, didUpdate frame: ARFrame) { sessionDelegate?.session?(session, didUpdate: frame) } - /** - This is called when new anchors are added to the session. - - @param session The session being run. - @param anchors An array of added anchors. - */ public func session(_ session: ARSession, didAdd anchors: [ARAnchor]) { sessionDelegate?.session?(session, didAdd: anchors) } - /** - This is called when anchors are updated. - - @param session The session being run. - @param anchors An array of updated anchors. - */ public func session(_ session: ARSession, didUpdate anchors: [ARAnchor]) { sessionDelegate?.session?(session, didUpdate: anchors) } - /** - This is called when anchors are removed from the session. - - @param session The session being run. - @param anchors An array of removed anchors. - */ public func session(_ session: ARSession, didRemove anchors: [ARAnchor]) { sessionDelegate?.session?(session, didRemove: anchors) } @@ -422,15 +371,7 @@ extension ArcGISARView: ARSessionDelegate { // MARK: - ARSessionObserver extension ArcGISARView: ARSessionObserver { - // AR session methods - - /** - This is called when a session fails. - - @discussion On failure the session will be paused. - @param session The session that failed. - @param error The error being reported (see ARError.h). - */ + public func session(_ session: ARSession, didFailWithError error: Error) { guard error is ARError else { return } @@ -460,56 +401,18 @@ extension ArcGISARView: ARSessionObserver { sessionDelegate?.session?(session, didFailWithError: error) } - /** - This is called when the camera’s tracking state has changed. - - @param session The session being run. - @param camera The camera that changed tracking states. - */ public func session(_ session: ARSession, cameraDidChangeTrackingState camera: ARCamera) { sessionDelegate?.session?(session, cameraDidChangeTrackingState: camera) } - /** - This is called when a session is interrupted. - - @discussion A session will be interrupted and no longer able to track when - it fails to receive required sensor data. This happens when video capture is interrupted, - for example when the application is sent to the background or when there are - multiple foreground applications (see AVCaptureSessionInterruptionReason). - No additional frame updates will be delivered until the interruption has ended. - @param session The session that was interrupted. - */ public func sessionWasInterrupted(_ session: ARSession) { sessionDelegate?.sessionWasInterrupted?(session) } - /** - This is called when a session interruption has ended. - - @discussion A session will continue running from the last known state once - the interruption has ended. If the device has moved, anchors will be misaligned. - To avoid this, some applications may want to reset tracking (see ARSessionRunOptions) - or attempt to relocalize (see `-[ARSessionObserver sessionShouldAttemptRelocalization:]`). - @param session The session that was interrupted. - */ public func sessionInterruptionEnded(_ session: ARSession) { sessionDelegate?.sessionWasInterrupted?(session) } - /** - This is called after a session resumes from a pause or interruption to determine - whether or not the session should attempt to relocalize. - - @discussion To avoid misaligned anchors, apps may wish to attempt a relocalization after - a session pause or interruption. If YES is returned: the session will begin relocalizing - and tracking state will switch to limited with reason relocalizing. If successful, the - session's tracking state will return to normal. Because relocalization depends on - the user's location, it can run indefinitely. Apps that wish to give up on relocalization - may call run with `ARSessionRunOptionResetTracking` at any time. - @param session The session to relocalize. - @return Return YES to begin relocalizing. - */ @available(iOS 11.3, *) public func sessionShouldAttemptRelocalization(_ session: ARSession) -> Bool { if let result = sessionDelegate?.sessionShouldAttemptRelocalization?(session) { @@ -518,12 +421,6 @@ extension ArcGISARView: ARSessionObserver { return false } - /** - This is called when the session outputs a new audio sample buffer. - - @param session The session being run. - @param audioSampleBuffer The captured audio sample buffer. - */ public func session(_ session: ARSession, didOutputAudioSampleBuffer audioSampleBuffer: CMSampleBuffer) { sessionDelegate?.session?(session, didOutputAudioSampleBuffer: audioSampleBuffer) } @@ -531,16 +428,7 @@ extension ArcGISARView: ARSessionObserver { // MARK: - CLLocationManagerDelegate extension ArcGISARView: CLLocationManagerDelegate { - /* - * locationManager:didUpdateLocations: - * - * Discussion: - * Invoked when new locations are available. Required for delivery of - * deferred locations. If implemented, updates will - * not be delivered to locationManager:didUpdateToLocation:fromLocation: - * - * locations is an array of CLLocation objects in chronological order. - */ + public func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) { guard let location = locations.last, location.horizontalAccuracy >= 0.0 else { return } @@ -557,42 +445,17 @@ extension ArcGISARView: CLLocationManagerDelegate { sceneView.setViewpointCamera(camera) finalizeStart() - print("didUpdateLocations - initialLocation...") } else if location.horizontalAccuracy < horizontalAccuracy { horizontalAccuracy = location.horizontalAccuracy - print("didUpdateLocations - accuracy improved...") } } - /* - * locationManager:didUpdateHeading: - * - * Discussion: - * Invoked when a new heading is available. - */ - @available(iOS 3.0, *) - public func locationManager(_ manager: CLLocationManager, didUpdateHeading newHeading: CLHeading) { - - } - - /* - * locationManager:didFailWithError: - * - * Discussion: - * Invoked when an error has occurred. Error types are defined in "CLError.h". - */ public func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) { didStartOrFailWithError(error) } - /* - * locationManager:didChangeAuthorizationStatus: - * - * Discussion: - * Invoked when the authorization status changes for this application. - */ public func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) { let authStatus = CLLocationManager.authorizationStatus() switch authStatus { @@ -604,26 +467,6 @@ extension ArcGISARView: CLLocationManagerDelegate { handleAuthStatusChangedAccessAuthorized() } } - - /* - * Discussion: - * Invoked when location updates are automatically paused. - */ - public func locationManagerDidPauseLocationUpdates(_ manager: CLLocationManager) { - - } - - /* - * Discussion: - * Invoked when location updates are automatically resumed. - * - * In the event that your application is terminated while suspended, you will - * not receive this notification. - */ - @available(iOS 6.0, *) - public func locationManagerDidResumeLocationUpdates(_ manager: CLLocationManager) { - - } } // MARK: - SCNSceneRendererDelegate @@ -647,20 +490,18 @@ extension ArcGISARView: SCNSceneRendererDelegate { public func renderer(_ renderer: SCNSceneRenderer, willRenderScene scene: SCNScene, atTime time: TimeInterval) { - // - // get transform from SCNView.pointOfView - // + // Get transform from SCNView.pointOfView. guard let transform = arSCNView.pointOfView?.transform else { return } - let cameraTransform = float4x4.init(transform) + let cameraTransform = simd_double4x4(transform) - let finalQuat:simd_quatf = simd_mul(simd_mul(compensationQuat, simd_quaternion(cameraTransform)), orientationQuat) - var transformationMatrix = AGSTransformationMatrix(quaternionX: Double(finalQuat.vector.x), - quaternionY: Double(finalQuat.vector.y), - quaternionZ: Double(finalQuat.vector.z), - quaternionW: Double(finalQuat.vector.w), - translationX: Double(cameraTransform.columns.3.x) * translationTransformationFactor, - translationY: Double(-cameraTransform.columns.3.z) * translationTransformationFactor, - translationZ: Double(cameraTransform.columns.3.y) * translationTransformationFactor) + let finalQuat:simd_quatd = simd_mul(simd_mul(compensationQuat, simd_quaternion(cameraTransform)), orientationQuat) + var transformationMatrix = AGSTransformationMatrix(quaternionX: finalQuat.vector.x, + quaternionY: finalQuat.vector.y, + quaternionZ: finalQuat.vector.z, + quaternionW: finalQuat.vector.w, + translationX: (cameraTransform.columns.3.x) * translationTransformationFactor, + translationY: (-cameraTransform.columns.3.z) * translationTransformationFactor, + translationZ: (cameraTransform.columns.3.y) * translationTransformationFactor) transformationMatrix = initialTransformationMatrix.addTransformation(transformationMatrix) @@ -668,10 +509,14 @@ extension ArcGISARView: SCNSceneRendererDelegate { sceneView.setViewpointCamera(camera) sceneView.renderFrame() - frameCount = frameCount + 1 + + // Calculate frame rate. + let frametime = time - lastUpdateTime + print("Frame rate = \(String(reflecting: Int((1.0 / frametime).rounded())))") + lastUpdateTime = time // - // call our scnSceneRendererDelegate + // Call our scnSceneRendererDelegate. // scnSceneRendererDelegate?.renderer?(renderer, willRenderScene: scene, atTime: time) } From 4169390a7a7c6cb954372b8689ea530b0484826c Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Tue, 4 Jun 2019 14:12:24 -0500 Subject: [PATCH 027/147] Scene no longer a property; better name for Scenes() method. --- .../ArcGISToolkitExamples/ARExample.swift | 35 ++++++++++--------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/Examples/ArcGISToolkitExamples/ARExample.swift b/Examples/ArcGISToolkitExamples/ARExample.swift index 90b56fbe..3008a6fe 100644 --- a/Examples/ArcGISToolkitExamples/ARExample.swift +++ b/Examples/ArcGISToolkitExamples/ARExample.swift @@ -19,22 +19,6 @@ import ArcGIS open class ARExample: UIViewController { public let arView = ArcGISARView(frame: CGRect.zero) - - private let scene: AGSScene = { - // Creates a scene with the streets basemap. - let scene = AGSScene(basemapType: .streets) - - // create elevation surface - let elevationSource = AGSArcGISTiledElevationSource(url: URL(string: "https://elevation3d.arcgis.com/arcgis/rest/services/WorldElevation3D/Terrain3D/ImageServer")!) - let surface = AGSSurface() - surface.elevationSources = [elevationSource] - surface.name = "baseSurface" - surface.isEnabled = true - surface.backgroundGrid.isVisible = false - scene.baseSurface = surface - - return scene - }() override open func viewDidLoad() { super.viewDidLoad() @@ -51,7 +35,7 @@ open class ARExample: UIViewController { arView.bottomAnchor.constraint(equalTo: view.bottomAnchor) ]) - arView.sceneView.scene = scene + arView.sceneView.scene = makeStreetsScene() } override open func viewDidAppear(_ animated: Bool) { @@ -61,6 +45,23 @@ open class ARExample: UIViewController { override open func viewDidDisappear(_ animated: Bool) { arView.stopTracking() } + + private func makeStreetsScene() -> AGSScene { + + // create scene with the streets basemap + let scene = AGSScene(basemapType: .streets) + + // create elevation surface + let elevationSource = AGSArcGISTiledElevationSource(url: URL(string: "http://elevation3d.arcgis.com/arcgis/rest/services/WorldElevation3D/Terrain3D/ImageServer")!) + let surface = AGSSurface() + surface.elevationSources = [elevationSource] + surface.name = "baseSurface" + surface.isEnabled = true + surface.backgroundGrid.isVisible = false + scene.baseSurface = surface + + return scene + } } extension ARExample: ARSessionDelegate { From 2088dfd4d0601408ec36b95c3ed39ab41f7f7548 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Tue, 4 Jun 2019 14:39:46 -0500 Subject: [PATCH 028/147] PR review changes: [unowned self], more doc changes; just .zero. --- .../ArcGISToolkitExamples/ARExample.swift | 2 +- Toolkit/ArcGISToolkit/AR/ArcGISARView.swift | 26 +++++++++---------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/Examples/ArcGISToolkitExamples/ARExample.swift b/Examples/ArcGISToolkitExamples/ARExample.swift index 3008a6fe..95146c2f 100644 --- a/Examples/ArcGISToolkitExamples/ARExample.swift +++ b/Examples/ArcGISToolkitExamples/ARExample.swift @@ -18,7 +18,7 @@ import ArcGIS open class ARExample: UIViewController { - public let arView = ArcGISARView(frame: CGRect.zero) + public let arView = ArcGISARView(frame: .zero) override open func viewDidLoad() { super.viewDidLoad() diff --git a/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift b/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift index 875c3683..2a4f8327 100644 --- a/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift +++ b/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift @@ -56,7 +56,7 @@ public class ArcGISARView: UIView { private var renderVideoFeed = true /// Used to determine the device location when originCamera is not set. - private lazy var locationManager: CLLocationManager = { + private lazy var locationManager: CLLocationManager = { [unowned self] in let lm = CLLocationManager() lm.desiredAccuracy = kCLLocationAccuracyBest lm.delegate = self @@ -67,7 +67,7 @@ public class ArcGISARView: UIView { private var initialLocation: CLLocation? /// Current horizontal accuracy of the device. - private var horizontalAccuracy: CLLocationAccuracy = .greatestFiniteMagnitude; + private var horizontalAccuracy: CLLocationAccuracy = .greatestFiniteMagnitude /// The intial camera position and orientation whether it was set via originCamera or the locationManager. private var initialTransformationMatrix = AGSTransformationMatrix() @@ -101,7 +101,7 @@ public class ArcGISARView: UIView { /// Initializer used to denote whether to display the live camera image. /// - /// - Parameter renderVideoFeed: whether to dispaly the live camera image. + /// - Parameter renderVideoFeed: Whether to display the live camera image. public convenience init(renderVideoFeed: Bool){ self.init(frame: .zero) self.renderVideoFeed = renderVideoFeed @@ -137,7 +137,7 @@ public class ArcGISARView: UIView { /// Determines the map point for the given screen point. /// - /// - Parameter screenPoint: the point in screen coordinates. + /// - Parameter screenPoint: The point in screen coordinates. /// - Returns: The map point corresponding to screenPoint. public func arScreenToLocation(screenPoint: AGSPoint) -> AGSPoint { return AGSPoint(x: 0.0, y: 0.0, spatialReference: nil) @@ -222,7 +222,7 @@ public class ArcGISARView: UIView { /// Adds subView to superView with appropriate constraints. /// - /// - Parameter subview: the subView to add. + /// - Parameter subview: The subView to add. fileprivate func addSubviewWithConstraints(_ subview: UIView) { // Add subview to view and setup constraints. addSubview(subview) @@ -276,14 +276,14 @@ public class ArcGISARView: UIView { /// Potential notification to the user of an error starting device tracking. /// - /// - Parameter error: error that ocurred when starting tracking. + /// - Parameter error: The error that occurred when starting tracking. fileprivate func didStartOrFailWithError(_ error: Error?) { if !notifiedStartOrFailure, let error = error { // TODO: present error to user... print("didStartOrFailWithError: \(String(reflecting:error))") } - notifiedStartOrFailure = true; + notifiedStartOrFailure = true } /// Handle a change in authorization status to "denied". @@ -304,18 +304,18 @@ public class ArcGISARView: UIView { /// Called when device orientation changes. /// - /// - Parameter notification: the notification. + /// - Parameter notification: The notification. @objc func orientationChanged(notification: Notification?) { // Handle rotation here. switch UIApplication.shared.statusBarOrientation { case .landscapeLeft: - orientationQuat = simd_quaternion(0, 0, 1.0, 0); + orientationQuat = simd_quaternion(0, 0, 1.0, 0) case .landscapeRight: - orientationQuat = simd_quaternion(0, 0, 0, 1.0); + orientationQuat = simd_quaternion(0, 0, 0, 1.0) case .portrait: - orientationQuat = simd_quaternion(0, 0, sqrt(0.5), sqrt(0.5)); + orientationQuat = simd_quaternion(0, 0, sqrt(0.5), sqrt(0.5)) case .portraitUpsideDown: - orientationQuat = simd_quaternion(0, 0, -sqrt(0.5), sqrt(0.5)); + orientationQuat = simd_quaternion(0, 0, -sqrt(0.5), sqrt(0.5)) default: break } @@ -328,7 +328,7 @@ public class ArcGISARView: UIView { /// - Returns: Error stating `ARKit` not supported. fileprivate class func notSupportedError() -> NSError { let userInfo = [NSLocalizedDescriptionKey : "The device does not support ARKit functionality."] - return NSError(domain: AGSErrorDomain, code: ARError.unsupportedConfiguration.rawValue, userInfo: userInfo) + return NSError(domain: ARErrorDomain, code: ARError.unsupportedConfiguration.rawValue, userInfo: userInfo) } /// Error used when access to the device location is denied. From 3c8f0c1ace58e5ce1c4d4d730a863d38f7935e14 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Wed, 5 Jun 2019 13:18:51 -0500 Subject: [PATCH 029/147] PR review changes; fatalError for not-yet-supported methods; doc comments; isSupported now computed property --- Toolkit/ArcGISToolkit/AR/ArcGISARView.swift | 24 ++++++++++++--------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift b/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift index 2a4f8327..70ff65d3 100644 --- a/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift +++ b/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift @@ -73,7 +73,9 @@ public class ArcGISARView: UIView { private var initialTransformationMatrix = AGSTransformationMatrix() /// Whether `ARKit` is supported on this device. - private var isSupported = false + private var isSupported = { + return ARWorldTrackingConfiguration.isSupported + }() /// Whether the client has been notfiied of start/failure. private var notifiedStartOrFailure = false @@ -107,11 +109,13 @@ public class ArcGISARView: UIView { self.renderVideoFeed = renderVideoFeed } + deinit { + stopTracking() + } + /// Initialization code shared between all initializers. private func sharedInitialization(){ - // `ARKit` initialization. - isSupported = ARWorldTrackingConfiguration.isSupported - + // Add the ARSCNView to our view. addSubviewWithConstraints(arSCNView) arSCNView.session.delegate = self (arSCNView as SCNSceneRenderer).delegate = self @@ -137,13 +141,13 @@ public class ArcGISARView: UIView { /// Determines the map point for the given screen point. /// - /// - Parameter screenPoint: The point in screen coordinates. + /// - Parameter toLocation: The point in screen coordinates. /// - Returns: The map point corresponding to screenPoint. - public func arScreenToLocation(screenPoint: AGSPoint) -> AGSPoint { - return AGSPoint(x: 0.0, y: 0.0, spatialReference: nil) + public func arScreen(toLocation: AGSPoint) -> AGSPoint { + fatalError("arScreen(toLocation:) has not been implemented") } - /// Resets the device tracking, using originCamera if it's not nil or the device's GPS location via the locationManager. + /// Resets the device tracking, using `originCamera` if it's not nil or the device's GPS location via the locationManager. public func resetTracking() { initialLocation = nil startTracking() @@ -153,14 +157,14 @@ public class ArcGISARView: UIView { /// /// - Returns: Reset operation success or failure. public func resetUsingLocationServices() -> Bool { - return false + fatalError("resetUsingLocationServices() has not been implemented") } /// Resets the device tracking using a spacial anchor. /// /// - Returns: Reset operation success or failure. public func resetUsingSpatialAnchor() -> Bool { - return false + fatalError("resetUsingSpatialAnchor() has not been implemented") } /// Starts device tracking. From 8e7f8c84be204bfea39a34687df4d2f560437420 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Fri, 7 Jun 2019 13:36:43 -0500 Subject: [PATCH 030/147] PR review changes: updates to error creation, orientationQuat construction, delegate stuff and more. --- .../ArcGISToolkitExamples/ARExample.swift | 30 +++ Toolkit/ArcGISToolkit/AR/ArcGISARView.swift | 193 +++++++++--------- 2 files changed, 130 insertions(+), 93 deletions(-) diff --git a/Examples/ArcGISToolkitExamples/ARExample.swift b/Examples/ArcGISToolkitExamples/ARExample.swift index 95146c2f..7e9473aa 100644 --- a/Examples/ArcGISToolkitExamples/ARExample.swift +++ b/Examples/ArcGISToolkitExamples/ARExample.swift @@ -25,6 +25,7 @@ open class ARExample: UIViewController { // Example of how to get ARSessionDelegate methods from the ArcGISARView. arView.sessionDelegate = self + arView.arSCNViewDelegate = self view.addSubview(arView) arView.translatesAutoresizingMaskIntoConstraints = false @@ -70,3 +71,32 @@ extension ARExample: ARSessionDelegate { // Example of how to get ARSessionDelegate methods from the ArcGISARView. } } + +extension ARExample: ARSCNViewDelegate { + + public func session(_ session: ARSession, didFailWithError error: Error) { + guard error is ARError else { return } + + let errorWithInfo = error as NSError + let messages = [ + errorWithInfo.localizedDescription, + errorWithInfo.localizedFailureReason, + errorWithInfo.localizedRecoverySuggestion + ] + + // Remove optional error messages. + let errorMessage = messages.compactMap({ $0 }).joined(separator: "\n") + + DispatchQueue.main.async { [unowned self] in + // Present an alert describing the error. + let alertController = UIAlertController(title: "Could not start tracking.", message: errorMessage, preferredStyle: .alert) + let restartAction = UIAlertAction(title: "Restart Tracking", style: .default) { _ in + alertController.dismiss(animated: true) + self.arView.startTracking() + } + alertController.addAction(restartAction) + + self.present(alertController, animated: true) + } + } +} diff --git a/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift b/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift index 70ff65d3..85868a40 100644 --- a/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift +++ b/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift @@ -16,8 +16,57 @@ import UIKit import ARKit import ArcGIS +extension simd_quatd { + init(statusBarOrientation: UIDeviceOrientation) { + switch statusBarOrientation { + case .landscapeLeft: + self.init(ix: 0, iy: 0, iz: 0, r: 1) + case .landscapeRight: + self.init(ix: 0, iy: 0, iz: 1, r: 0) + case .portrait: + let squareRootOfOneHalf = (0.5 as Double).squareRoot() + self.init(ix: 0, iy: 0, iz: squareRootOfOneHalf, r: squareRootOfOneHalf) + case .portraitUpsideDown: + let squareRootOfOneHalf = (0.5 as Double).squareRoot() + self.init(ix: 0, iy: 0, iz: -squareRootOfOneHalf, r: squareRootOfOneHalf) + default: + // default to landscapeLeft + self.init(ix: 0, iy: 0, iz: 0, r: 1) + } + } +} + +extension ArcGISARView.clError: CustomNSError { + static var errorDomain: String { + return kCLErrorDomain + } + + var errorCode: Int { + switch self { + case .accessDenied: + return CLError.Code.denied.rawValue + case .missingPListKey: + return CLError.Code.denied.rawValue + } + } + + var errorDescription: String? { + switch self { + case .accessDenied: + return "Access to the device location is denied." + case .missingPListKey: + return "You must specify a location usage description key (NSLocationWhenInUseUsageDescription or NSLocationAlwaysUsageDescription) in your plist." + } + } +} + public class ArcGISARView: UIView { + enum clError { + case accessDenied + case missingPListKey + } + // MARK: public properties /// The view used to display the `ARKit` camera image and 3D `SceneKit` content. @@ -47,8 +96,8 @@ public class ArcGISARView: UIView { // We implement ARSessionDelegate methods, but will use `sessionDelegate` to forward them to clients. weak open var sessionDelegate: ARSessionDelegate? - // We implement SCNSceneRendererDelegate methods, but will use `scnSceneRendererDelegate` to forward them to clients. - weak open var scnSceneRendererDelegate: SCNSceneRendererDelegate? + // We implement ARSCNViewDelegate methods, but will use `arSCNViewDelegate` to forward them to clients. + weak open var arSCNViewDelegate: ARSCNViewDelegate? // MARK: private properties @@ -84,10 +133,10 @@ public class ArcGISARView: UIView { private var lastUpdateTime: TimeInterval = 0 // A quaternion used to compensate for the pitch beeing 90 degrees on `ARKit`; used to calculate the current device transformation for each frame. - let compensationQuat:simd_quatd = simd_quaternion((sin(45 / (180 / .pi))), 0, 0, (cos(45 / (180 / .pi)))) + let compensationQuat:simd_quatd = simd_quatd(ix: (sin(45 / (180 / .pi))), iy: 0, iz: 0, r: (cos(45 / (180 / .pi)))) /// The quaternion used to represent the device orientation; used to calculate the current device transformation for each frame; defaults to landcape-left. - var orientationQuat:simd_quatd = simd_quaternion(0, 0, 1, 0) + var orientationQuat:simd_quatd = simd_quatd(statusBarOrientation: .landscapeLeft) // MARK: Initializers @@ -117,8 +166,8 @@ public class ArcGISARView: UIView { private func sharedInitialization(){ // Add the ARSCNView to our view. addSubviewWithConstraints(arSCNView) + arSCNView.delegate = self arSCNView.session.delegate = self - (arSCNView as SCNSceneRenderer).delegate = self // Add sceneView to view and setup constraints. addSubviewWithConstraints(sceneView) @@ -143,7 +192,7 @@ public class ArcGISARView: UIView { /// /// - Parameter toLocation: The point in screen coordinates. /// - Returns: The map point corresponding to screenPoint. - public func arScreen(toLocation: AGSPoint) -> AGSPoint { + public func arScreenToLocation(screenPoint: AGSPoint) -> AGSPoint { fatalError("arScreen(toLocation:) has not been implemented") } @@ -170,7 +219,7 @@ public class ArcGISARView: UIView { /// Starts device tracking. public func startTracking() { if !isSupported { - didStartOrFailWithError(ArcGISARView.notSupportedError()) + didStartOrFailWithError(ARError(.unsupportedConfiguration)) return } @@ -193,6 +242,7 @@ public class ArcGISARView: UIView { } } + //TODO: reloook at the mechanism by which we're getting notified of orientation changed events... // We need to know when the device orientation changes in order to update the Camera transformation. UIDevice.current.beginGeneratingDeviceOrientationNotifications() NotificationCenter.default.addObserver( @@ -248,7 +298,7 @@ public class ArcGISARView: UIView { locationManager.requestAlwaysAuthorization() } else{ - didStartOrFailWithError(ArcGISARView.missingPListKeyError()) + didStartOrFailWithError(ArcGISARView.clError.missingPListKey) } } @@ -270,7 +320,7 @@ public class ArcGISARView: UIView { /// Start the locationManager with denied access. fileprivate func startWithAccessDenied() { - didStartOrFailWithError(ArcGISARView.accessDeniedError()) + didStartOrFailWithError(ArcGISARView.clError.accessDenied) } /// Start the locationManager with authorized access. @@ -296,7 +346,7 @@ public class ArcGISARView: UIView { stopUpdatingLocationAndHeading() // We were waiting for user prompt to come back, so notify. - didStartOrFailWithError(ArcGISARView.accessDeniedError()) + didStartOrFailWithError(ArcGISARView.clError.accessDenied) } /// Handle a change in authorization status to "authorized". @@ -311,49 +361,11 @@ public class ArcGISARView: UIView { /// - Parameter notification: The notification. @objc func orientationChanged(notification: Notification?) { // Handle rotation here. - switch UIApplication.shared.statusBarOrientation { - case .landscapeLeft: - orientationQuat = simd_quaternion(0, 0, 1.0, 0) - case .landscapeRight: - orientationQuat = simd_quaternion(0, 0, 0, 1.0) - case .portrait: - orientationQuat = simd_quaternion(0, 0, sqrt(0.5), sqrt(0.5)) - case .portraitUpsideDown: - orientationQuat = simd_quaternion(0, 0, -sqrt(0.5), sqrt(0.5)) - default: - break - } - } - - // MARK: Errors - - /// Error used when `ARKit` is not supported on the current device. - /// - /// - Returns: Error stating `ARKit` not supported. - fileprivate class func notSupportedError() -> NSError { - let userInfo = [NSLocalizedDescriptionKey : "The device does not support ARKit functionality."] - return NSError(domain: ARErrorDomain, code: ARError.unsupportedConfiguration.rawValue, userInfo: userInfo) - } - - /// Error used when access to the device location is denied. - /// - /// - Returns: Error stating access to location information is denied. - fileprivate class func accessDeniedError() -> NSError{ - let userInfo = [NSLocalizedDescriptionKey : "Access to the device location is denied."] - return NSError(domain: kCLErrorDomain, code: CLError.Code.denied.rawValue, userInfo: userInfo) - } - - /// Error used when required plist information is missing. - /// - /// - Returns: Error stating plist information is missing. - fileprivate class func missingPListKeyError() -> NSError{ - let userInfo = [NSLocalizedDescriptionKey : "You must specify a location usage description key (NSLocationWhenInUseUsageDescription or NSLocationAlwaysUsageDescription) in your plist."] - return NSError(domain: kCLErrorDomain, code: CLError.Code.denied.rawValue, userInfo: userInfo) + orientationQuat = simd_quatd(statusBarOrientation: UIDevice.current.orientation) } } // MARK: - ARSessionDelegate - extension ArcGISARView: ARSessionDelegate { public func session(_ session: ARSession, didUpdate frame: ARFrame) { @@ -373,60 +385,55 @@ extension ArcGISARView: ARSessionDelegate { } } -// MARK: - ARSessionObserver +// MARK: - ARSCNViewDelegate +extension ArcGISARView: ARSCNViewDelegate { + public func renderer(_ renderer: SCNSceneRenderer, nodeFor anchor: ARAnchor) -> SCNNode? { + return arSCNViewDelegate?.renderer?(renderer, nodeFor: anchor) + } + + public func renderer(_ renderer: SCNSceneRenderer, didAdd node: SCNNode, for anchor: ARAnchor) { + arSCNViewDelegate?.renderer?(renderer, didAdd: node, for: anchor) + } + + public func renderer(_ renderer: SCNSceneRenderer, willUpdate node: SCNNode, for anchor: ARAnchor) { + arSCNViewDelegate?.renderer?(renderer, willUpdate: node, for: anchor) + } + + public func renderer(_ renderer: SCNSceneRenderer, didUpdate node: SCNNode, for anchor: ARAnchor) { + arSCNViewDelegate?.renderer?(renderer, didUpdate: node, for: anchor) + } + + public func renderer(_ renderer: SCNSceneRenderer, didRemove node: SCNNode, for anchor: ARAnchor) { + arSCNViewDelegate?.renderer?(renderer, didRemove: node, for: anchor) + } +} + +// MARK: - ARSessionObserver (via ARSCNViewDelegate) extension ArcGISARView: ARSessionObserver { public func session(_ session: ARSession, didFailWithError error: Error) { - guard error is ARError else { return } - - let errorWithInfo = error as NSError - let messages = [ - errorWithInfo.localizedDescription, - errorWithInfo.localizedFailureReason, - errorWithInfo.localizedRecoverySuggestion - ] - - // Remove optional error messages. - let errorMessage = messages.compactMap({ $0 }).joined(separator: "\n") - - DispatchQueue.main.async { - // Present an alert describing the error. - let alertController = UIAlertController(title: "Could not start tracking.", message: errorMessage, preferredStyle: .alert) - let restartAction = UIAlertAction(title: "Restart Tracking", style: .default) { _ in - alertController.dismiss(animated: true, completion: nil) - self.startTracking() - } - alertController.addAction(restartAction) - - guard let rootController = UIApplication.shared.keyWindow?.rootViewController else { return } - rootController.present(alertController, animated: true, completion: nil) - } - - sessionDelegate?.session?(session, didFailWithError: error) + arSCNViewDelegate?.session?(session, didFailWithError: error) } public func session(_ session: ARSession, cameraDidChangeTrackingState camera: ARCamera) { - sessionDelegate?.session?(session, cameraDidChangeTrackingState: camera) + arSCNViewDelegate?.session?(session, cameraDidChangeTrackingState: camera) } public func sessionWasInterrupted(_ session: ARSession) { - sessionDelegate?.sessionWasInterrupted?(session) + arSCNViewDelegate?.sessionWasInterrupted?(session) } public func sessionInterruptionEnded(_ session: ARSession) { - sessionDelegate?.sessionWasInterrupted?(session) + arSCNViewDelegate?.sessionWasInterrupted?(session) } @available(iOS 11.3, *) public func sessionShouldAttemptRelocalization(_ session: ARSession) -> Bool { - if let result = sessionDelegate?.sessionShouldAttemptRelocalization?(session) { - return result - } - return false + return sessionDelegate?.sessionShouldAttemptRelocalization?(session) ?? false } public func session(_ session: ARSession, didOutputAudioSampleBuffer audioSampleBuffer: CMSampleBuffer) { - sessionDelegate?.session?(session, didOutputAudioSampleBuffer: audioSampleBuffer) + arSCNViewDelegate?.session?(session, didOutputAudioSampleBuffer: audioSampleBuffer) } } @@ -473,23 +480,23 @@ extension ArcGISARView: CLLocationManagerDelegate { } } -// MARK: - SCNSceneRendererDelegate +// MARK: - SCNSceneRendererDelegate (via ARSCNViewDelegate) extension ArcGISARView: SCNSceneRendererDelegate { public func renderer(_ renderer: SCNSceneRenderer, updateAtTime time: TimeInterval) { - scnSceneRendererDelegate?.renderer?(renderer, updateAtTime: time) + arSCNViewDelegate?.renderer?(renderer, updateAtTime: time) } public func renderer(_ renderer: SCNSceneRenderer, didApplyAnimationsAtTime time: TimeInterval) { - scnSceneRendererDelegate?.renderer?(renderer, didApplyConstraintsAtTime: time) + arSCNViewDelegate?.renderer?(renderer, didApplyConstraintsAtTime: time) } public func renderer(_ renderer: SCNSceneRenderer, didSimulatePhysicsAtTime time: TimeInterval) { - scnSceneRendererDelegate?.renderer?(renderer, didSimulatePhysicsAtTime: time) + arSCNViewDelegate?.renderer?(renderer, didSimulatePhysicsAtTime: time) } public func renderer(_ renderer: SCNSceneRenderer, didApplyConstraintsAtTime time: TimeInterval) { - scnSceneRendererDelegate?.renderer?(renderer, didApplyConstraintsAtTime: time) + arSCNViewDelegate?.renderer?(renderer, didApplyConstraintsAtTime: time) } public func renderer(_ renderer: SCNSceneRenderer, willRenderScene scene: SCNScene, atTime time: TimeInterval) { @@ -498,7 +505,7 @@ extension ArcGISARView: SCNSceneRendererDelegate { guard let transform = arSCNView.pointOfView?.transform else { return } let cameraTransform = simd_double4x4(transform) - let finalQuat:simd_quatd = simd_mul(simd_mul(compensationQuat, simd_quaternion(cameraTransform)), orientationQuat) + let finalQuat:simd_quatd = simd_mul(simd_mul(compensationQuat, simd_quatd(cameraTransform)), orientationQuat) var transformationMatrix = AGSTransformationMatrix(quaternionX: finalQuat.vector.x, quaternionY: finalQuat.vector.y, quaternionZ: finalQuat.vector.z, @@ -520,12 +527,12 @@ extension ArcGISARView: SCNSceneRendererDelegate { lastUpdateTime = time // - // Call our scnSceneRendererDelegate. + // Call our arSCNViewDelegate. // - scnSceneRendererDelegate?.renderer?(renderer, willRenderScene: scene, atTime: time) + arSCNViewDelegate?.renderer?(renderer, willRenderScene: scene, atTime: time) } public func renderer(_ renderer: SCNSceneRenderer, didRenderScene scene: SCNScene, atTime time: TimeInterval) { - scnSceneRendererDelegate?.renderer?(renderer, didRenderScene: scene, atTime: time) + arSCNViewDelegate?.renderer?(renderer, didRenderScene: scene, atTime: time) } } From 645aea1af3438e81f37607f4088fce56ee4ed4c3 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Mon, 10 Jun 2019 14:51:33 -0500 Subject: [PATCH 031/147] Remove unneeded delegate methods/properties, use default class and method modifiers in ARExample --- .../ArcGISToolkitExamples/ARExample.swift | 29 ++--- Toolkit/ArcGISToolkit/AR/ArcGISARView.swift | 116 +++++++----------- 2 files changed, 55 insertions(+), 90 deletions(-) diff --git a/Examples/ArcGISToolkitExamples/ARExample.swift b/Examples/ArcGISToolkitExamples/ARExample.swift index 7e9473aa..32aa9b21 100644 --- a/Examples/ArcGISToolkitExamples/ARExample.swift +++ b/Examples/ArcGISToolkitExamples/ARExample.swift @@ -16,17 +16,16 @@ import ARKit import ArcGISToolkit import ArcGIS -open class ARExample: UIViewController { +class ARExample: UIViewController { public let arView = ArcGISARView(frame: .zero) - override open func viewDidLoad() { + override func viewDidLoad() { super.viewDidLoad() - // Example of how to get ARSessionDelegate methods from the ArcGISARView. - arView.sessionDelegate = self + // Set ourself as delegate so we can get ARSCNViewDelegate method calls. arView.arSCNViewDelegate = self - + view.addSubview(arView) arView.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ @@ -39,11 +38,11 @@ open class ARExample: UIViewController { arView.sceneView.scene = makeStreetsScene() } - override open func viewDidAppear(_ animated: Bool) { + override func viewDidAppear(_ animated: Bool) { arView.startTracking() } - override open func viewDidDisappear(_ animated: Bool) { + override func viewDidDisappear(_ animated: Bool) { arView.stopTracking() } @@ -65,15 +64,8 @@ open class ARExample: UIViewController { } } -extension ARExample: ARSessionDelegate { - - public func session(_ session: ARSession, didUpdate frame: ARFrame) { - // Example of how to get ARSessionDelegate methods from the ArcGISARView. - } -} - extension ARExample: ARSCNViewDelegate { - + public func session(_ session: ARSession, didFailWithError error: Error) { guard error is ARError else { return } @@ -87,16 +79,15 @@ extension ARExample: ARSCNViewDelegate { // Remove optional error messages. let errorMessage = messages.compactMap({ $0 }).joined(separator: "\n") - DispatchQueue.main.async { [unowned self] in + DispatchQueue.main.async { [weak self] in // Present an alert describing the error. let alertController = UIAlertController(title: "Could not start tracking.", message: errorMessage, preferredStyle: .alert) let restartAction = UIAlertAction(title: "Restart Tracking", style: .default) { _ in - alertController.dismiss(animated: true) - self.arView.startTracking() + self?.arView.startTracking() } alertController.addAction(restartAction) - self.present(alertController, animated: true) + self?.present(alertController, animated: true) } } } diff --git a/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift b/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift index 85868a40..84ae05f8 100644 --- a/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift +++ b/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift @@ -93,9 +93,6 @@ public class ArcGISARView: UIView { /// The translation factor used to support a table top AR experience. public var translationTransformationFactor: Double = 1.0 - // We implement ARSessionDelegate methods, but will use `sessionDelegate` to forward them to clients. - weak open var sessionDelegate: ARSessionDelegate? - // We implement ARSCNViewDelegate methods, but will use `arSCNViewDelegate` to forward them to clients. weak open var arSCNViewDelegate: ARSCNViewDelegate? @@ -167,7 +164,6 @@ public class ArcGISARView: UIView { // Add the ARSCNView to our view. addSubviewWithConstraints(arSCNView) arSCNView.delegate = self - arSCNView.session.delegate = self // Add sceneView to view and setup constraints. addSubviewWithConstraints(sceneView) @@ -365,26 +361,6 @@ public class ArcGISARView: UIView { } } -// MARK: - ARSessionDelegate -extension ArcGISARView: ARSessionDelegate { - - public func session(_ session: ARSession, didUpdate frame: ARFrame) { - sessionDelegate?.session?(session, didUpdate: frame) - } - - public func session(_ session: ARSession, didAdd anchors: [ARAnchor]) { - sessionDelegate?.session?(session, didAdd: anchors) - } - - public func session(_ session: ARSession, didUpdate anchors: [ARAnchor]) { - sessionDelegate?.session?(session, didUpdate: anchors) - } - - public func session(_ session: ARSession, didRemove anchors: [ARAnchor]) { - sessionDelegate?.session?(session, didRemove: anchors) - } -} - // MARK: - ARSCNViewDelegate extension ArcGISARView: ARSCNViewDelegate { public func renderer(_ renderer: SCNSceneRenderer, nodeFor anchor: ARAnchor) -> SCNNode? { @@ -429,7 +405,7 @@ extension ArcGISARView: ARSessionObserver { @available(iOS 11.3, *) public func sessionShouldAttemptRelocalization(_ session: ARSession) -> Bool { - return sessionDelegate?.sessionShouldAttemptRelocalization?(session) ?? false + return arSCNViewDelegate?.sessionShouldAttemptRelocalization?(session) ?? false } public func session(_ session: ARSession, didOutputAudioSampleBuffer audioSampleBuffer: CMSampleBuffer) { @@ -437,49 +413,6 @@ extension ArcGISARView: ARSessionObserver { } } -// MARK: - CLLocationManagerDelegate -extension ArcGISARView: CLLocationManagerDelegate { - - public func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) { - guard let location = locations.last, location.horizontalAccuracy >= 0.0 else { return } - - if initialLocation == nil { - initialLocation = location - horizontalAccuracy = location.horizontalAccuracy - - let locationPoint = AGSPoint(x: location.coordinate.longitude, - y: location.coordinate.latitude, - z: location.altitude, - spatialReference: .wgs84()) - let camera = AGSCamera(location: locationPoint, heading: 0.0, pitch: 0.0, roll: 0.0) - initialTransformationMatrix = camera.transformationMatrix - sceneView.setViewpointCamera(camera) - - finalizeStart() - } - else if location.horizontalAccuracy < horizontalAccuracy { - horizontalAccuracy = location.horizontalAccuracy - } - - } - - public func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) { - didStartOrFailWithError(error) - } - - public func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) { - let authStatus = CLLocationManager.authorizationStatus() - switch authStatus { - case .notDetermined: - break - case .restricted, .denied: - handleAuthStatusChangedAccessDenied() - case .authorizedAlways, .authorizedWhenInUse: - handleAuthStatusChangedAccessAuthorized() - } - } -} - // MARK: - SCNSceneRendererDelegate (via ARSCNViewDelegate) extension ArcGISARView: SCNSceneRendererDelegate { @@ -526,9 +459,7 @@ extension ArcGISARView: SCNSceneRendererDelegate { print("Frame rate = \(String(reflecting: Int((1.0 / frametime).rounded())))") lastUpdateTime = time - // - // Call our arSCNViewDelegate. - // + // Call our arSCNViewDelegate method. arSCNViewDelegate?.renderer?(renderer, willRenderScene: scene, atTime: time) } @@ -536,3 +467,46 @@ extension ArcGISARView: SCNSceneRendererDelegate { arSCNViewDelegate?.renderer?(renderer, didRenderScene: scene, atTime: time) } } + +// MARK: - CLLocationManagerDelegate +extension ArcGISARView: CLLocationManagerDelegate { + + public func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) { + guard let location = locations.last, location.horizontalAccuracy >= 0.0 else { return } + + if initialLocation == nil { + initialLocation = location + horizontalAccuracy = location.horizontalAccuracy + + let locationPoint = AGSPoint(x: location.coordinate.longitude, + y: location.coordinate.latitude, + z: location.altitude, + spatialReference: .wgs84()) + let camera = AGSCamera(location: locationPoint, heading: 0.0, pitch: 0.0, roll: 0.0) + initialTransformationMatrix = camera.transformationMatrix + sceneView.setViewpointCamera(camera) + + finalizeStart() + } + else if location.horizontalAccuracy < horizontalAccuracy { + horizontalAccuracy = location.horizontalAccuracy + } + + } + + public func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) { + didStartOrFailWithError(error) + } + + public func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) { + let authStatus = CLLocationManager.authorizationStatus() + switch authStatus { + case .notDetermined: + break + case .restricted, .denied: + handleAuthStatusChangedAccessDenied() + case .authorizedAlways, .authorizedWhenInUse: + handleAuthStatusChangedAccessAuthorized() + } + } +} From 06d3eb24598f59dea3215c54c90b06f0d5304c76 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Mon, 10 Jun 2019 15:10:23 -0500 Subject: [PATCH 032/147] Update Examples/ArcGISToolkitExamples/ARExample.swift Co-Authored-By: Philip Ridgeway --- Examples/ArcGISToolkitExamples/ARExample.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Examples/ArcGISToolkitExamples/ARExample.swift b/Examples/ArcGISToolkitExamples/ARExample.swift index 32aa9b21..8cd41576 100644 --- a/Examples/ArcGISToolkitExamples/ARExample.swift +++ b/Examples/ArcGISToolkitExamples/ARExample.swift @@ -18,7 +18,7 @@ import ArcGIS class ARExample: UIViewController { - public let arView = ArcGISARView(frame: .zero) + let arView = ArcGISARView(frame: .zero) override func viewDidLoad() { super.viewDidLoad() From 38a30e6397067e9f41d38f54687e61cc0eba90f8 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Mon, 10 Jun 2019 15:10:40 -0500 Subject: [PATCH 033/147] Update Examples/ArcGISToolkitExamples/ARExample.swift Co-Authored-By: Philip Ridgeway --- Examples/ArcGISToolkitExamples/ARExample.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Examples/ArcGISToolkitExamples/ARExample.swift b/Examples/ArcGISToolkitExamples/ARExample.swift index 8cd41576..1fec9f6c 100644 --- a/Examples/ArcGISToolkitExamples/ARExample.swift +++ b/Examples/ArcGISToolkitExamples/ARExample.swift @@ -66,7 +66,7 @@ class ARExample: UIViewController { extension ARExample: ARSCNViewDelegate { - public func session(_ session: ARSession, didFailWithError error: Error) { + func session(_ session: ARSession, didFailWithError error: Error) { guard error is ARError else { return } let errorWithInfo = error as NSError From 1bff7e971e6d4448e8a89de81cbeb78f6b565514 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Mon, 10 Jun 2019 15:10:55 -0500 Subject: [PATCH 034/147] Update Toolkit/ArcGISToolkit/AR/ArcGISARView.swift Co-Authored-By: Philip Ridgeway --- Toolkit/ArcGISToolkit/AR/ArcGISARView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift b/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift index 84ae05f8..4ccefd64 100644 --- a/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift +++ b/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift @@ -498,7 +498,7 @@ extension ArcGISARView: CLLocationManagerDelegate { didStartOrFailWithError(error) } - public func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) { + public func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) { let authStatus = CLLocationManager.authorizationStatus() switch authStatus { case .notDetermined: From f8d3844205706e4559cbce02ebb5759f2b303efe Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Mon, 10 Jun 2019 15:17:27 -0500 Subject: [PATCH 035/147] use https --- Examples/ArcGISToolkitExamples/ARExample.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Examples/ArcGISToolkitExamples/ARExample.swift b/Examples/ArcGISToolkitExamples/ARExample.swift index 32aa9b21..ef1ed898 100644 --- a/Examples/ArcGISToolkitExamples/ARExample.swift +++ b/Examples/ArcGISToolkitExamples/ARExample.swift @@ -52,7 +52,7 @@ class ARExample: UIViewController { let scene = AGSScene(basemapType: .streets) // create elevation surface - let elevationSource = AGSArcGISTiledElevationSource(url: URL(string: "http://elevation3d.arcgis.com/arcgis/rest/services/WorldElevation3D/Terrain3D/ImageServer")!) + let elevationSource = AGSArcGISTiledElevationSource(url: URL(string: "https://elevation3d.arcgis.com/arcgis/rest/services/WorldElevation3D/Terrain3D/ImageServer")!) let surface = AGSSurface() surface.elevationSources = [elevationSource] surface.name = "baseSurface" From 318691af5b32affdf2d68ce4b9c18564143ff86c Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Tue, 11 Jun 2019 15:41:54 -0500 Subject: [PATCH 036/147] PR review changes; Error updates; no lazy on lets; make sure we don't remove a not-yet-added observer. --- Toolkit/ArcGISToolkit/AR/ArcGISARView.swift | 42 ++++++++++----------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift b/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift index 4ccefd64..bc76408d 100644 --- a/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift +++ b/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift @@ -36,18 +36,13 @@ extension simd_quatd { } } -extension ArcGISARView.clError: CustomNSError { +extension ArcGISARView.CoreLocationError: CustomNSError { static var errorDomain: String { return kCLErrorDomain } var errorCode: Int { - switch self { - case .accessDenied: - return CLError.Code.denied.rawValue - case .missingPListKey: - return CLError.Code.denied.rawValue - } + return CLError.Code.denied.rawValue } var errorDescription: String? { @@ -62,7 +57,7 @@ extension ArcGISARView.clError: CustomNSError { public class ArcGISARView: UIView { - enum clError { + enum CoreLocationError: Swift.Error { case accessDenied case missingPListKey } @@ -70,10 +65,10 @@ public class ArcGISARView: UIView { // MARK: public properties /// The view used to display the `ARKit` camera image and 3D `SceneKit` content. - public lazy private(set) var arSCNView = ARSCNView(frame: .zero) + public let arSCNView = ARSCNView(frame: .zero) /// The view used to display ArcGIS 3D content. - public lazy private(set) var sceneView = AGSSceneView(frame: .zero) + public let sceneView = AGSSceneView(frame: .zero) /// The world tracking information used by `ARKit`. public var arConfiguration: ARConfiguration = { @@ -130,10 +125,13 @@ public class ArcGISARView: UIView { private var lastUpdateTime: TimeInterval = 0 // A quaternion used to compensate for the pitch beeing 90 degrees on `ARKit`; used to calculate the current device transformation for each frame. - let compensationQuat:simd_quatd = simd_quatd(ix: (sin(45 / (180 / .pi))), iy: 0, iz: 0, r: (cos(45 / (180 / .pi)))) + let compensationQuat: simd_quatd = simd_quatd(ix: (sin(45 / (180 / .pi))), iy: 0, iz: 0, r: (cos(45 / (180 / .pi)))) /// The quaternion used to represent the device orientation; used to calculate the current device transformation for each frame; defaults to landcape-left. - var orientationQuat:simd_quatd = simd_quatd(statusBarOrientation: .landscapeLeft) + var orientationQuat: simd_quatd = simd_quatd(statusBarOrientation: .landscapeLeft) + + // Denotes whether the orientation observer has been added to the NotificationCenter; used to prevent removing a not-yet-added observer. + private var orientationObserverAdded = false // MARK: Initializers @@ -238,7 +236,6 @@ public class ArcGISARView: UIView { } } - //TODO: reloook at the mechanism by which we're getting notified of orientation changed events... // We need to know when the device orientation changes in order to update the Camera transformation. UIDevice.current.beginGeneratingDeviceOrientationNotifications() NotificationCenter.default.addObserver( @@ -247,6 +244,7 @@ public class ArcGISARView: UIView { name: UIDevice.orientationDidChangeNotification, object: nil ) + orientationObserverAdded = true } /// Suspends device tracking. @@ -255,7 +253,10 @@ public class ArcGISARView: UIView { stopUpdatingLocationAndHeading() UIDevice.current.endGeneratingDeviceOrientationNotifications() - NotificationCenter.default.removeObserver(self) + if (orientationObserverAdded) { + NotificationCenter.default.removeObserver(self, name: UIDevice.orientationDidChangeNotification, object: nil) + orientationObserverAdded = false + } } // MARK: Private @@ -294,7 +295,7 @@ public class ArcGISARView: UIView { locationManager.requestAlwaysAuthorization() } else{ - didStartOrFailWithError(ArcGISARView.clError.missingPListKey) + didStartOrFailWithError(ArcGISARView.CoreLocationError.missingPListKey) } } @@ -316,7 +317,7 @@ public class ArcGISARView: UIView { /// Start the locationManager with denied access. fileprivate func startWithAccessDenied() { - didStartOrFailWithError(ArcGISARView.clError.accessDenied) + didStartOrFailWithError(ArcGISARView.CoreLocationError.accessDenied) } /// Start the locationManager with authorized access. @@ -330,7 +331,6 @@ public class ArcGISARView: UIView { fileprivate func didStartOrFailWithError(_ error: Error?) { if !notifiedStartOrFailure, let error = error { // TODO: present error to user... - print("didStartOrFailWithError: \(String(reflecting:error))") } notifiedStartOrFailure = true @@ -342,7 +342,7 @@ public class ArcGISARView: UIView { stopUpdatingLocationAndHeading() // We were waiting for user prompt to come back, so notify. - didStartOrFailWithError(ArcGISARView.clError.accessDenied) + didStartOrFailWithError(ArcGISARView.CoreLocationError.accessDenied) } /// Handle a change in authorization status to "authorized". @@ -455,9 +455,9 @@ extension ArcGISARView: SCNSceneRendererDelegate { sceneView.renderFrame() // Calculate frame rate. - let frametime = time - lastUpdateTime - print("Frame rate = \(String(reflecting: Int((1.0 / frametime).rounded())))") - lastUpdateTime = time +// let frametime = time - lastUpdateTime +// print("Frame rate = \(String(reflecting: Int((1.0 / frametime).rounded())))") +// lastUpdateTime = time // Call our arSCNViewDelegate method. arSCNViewDelegate?.renderer?(renderer, willRenderScene: scene, atTime: time) From 82a4e4b36434d15506838fd43256dd77e81f5f39 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Wed, 12 Jun 2019 09:30:53 -0500 Subject: [PATCH 037/147] Error qualifier updates. --- Toolkit/ArcGISToolkit/AR/ArcGISARView.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift b/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift index bc76408d..4c06ceb9 100644 --- a/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift +++ b/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift @@ -295,7 +295,7 @@ public class ArcGISARView: UIView { locationManager.requestAlwaysAuthorization() } else{ - didStartOrFailWithError(ArcGISARView.CoreLocationError.missingPListKey) + didStartOrFailWithError(CoreLocationError.missingPListKey) } } @@ -317,7 +317,7 @@ public class ArcGISARView: UIView { /// Start the locationManager with denied access. fileprivate func startWithAccessDenied() { - didStartOrFailWithError(ArcGISARView.CoreLocationError.accessDenied) + didStartOrFailWithError(CoreLocationError.accessDenied) } /// Start the locationManager with authorized access. @@ -342,7 +342,7 @@ public class ArcGISARView: UIView { stopUpdatingLocationAndHeading() // We were waiting for user prompt to come back, so notify. - didStartOrFailWithError(ArcGISARView.CoreLocationError.accessDenied) + didStartOrFailWithError(CoreLocationError.accessDenied) } /// Handle a change in authorization status to "authorized". From 5458a408974562959d4d23d3487f85ee67b99e52 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Mon, 17 Jun 2019 13:11:15 -0500 Subject: [PATCH 038/147] Add TransformationMatrixCameraController; update to using spaceEffect; remove now-unnecessary device orientation matrix and code --- Toolkit/ArcGISToolkit/AR/ArcGISARView.swift | 85 ++++++--------------- 1 file changed, 23 insertions(+), 62 deletions(-) diff --git a/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift b/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift index 4c06ceb9..ef1793c5 100644 --- a/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift +++ b/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift @@ -16,26 +16,6 @@ import UIKit import ARKit import ArcGIS -extension simd_quatd { - init(statusBarOrientation: UIDeviceOrientation) { - switch statusBarOrientation { - case .landscapeLeft: - self.init(ix: 0, iy: 0, iz: 0, r: 1) - case .landscapeRight: - self.init(ix: 0, iy: 0, iz: 1, r: 0) - case .portrait: - let squareRootOfOneHalf = (0.5 as Double).squareRoot() - self.init(ix: 0, iy: 0, iz: squareRootOfOneHalf, r: squareRootOfOneHalf) - case .portraitUpsideDown: - let squareRootOfOneHalf = (0.5 as Double).squareRoot() - self.init(ix: 0, iy: 0, iz: -squareRootOfOneHalf, r: squareRootOfOneHalf) - default: - // default to landscapeLeft - self.init(ix: 0, iy: 0, iz: 0, r: 1) - } - } -} - extension ArcGISARView.CoreLocationError: CustomNSError { static var errorDomain: String { return kCLErrorDomain @@ -70,6 +50,9 @@ public class ArcGISARView: UIView { /// The view used to display ArcGIS 3D content. public let sceneView = AGSSceneView(frame: .zero) + /// The camera controller used to control the Scene + private let cameraController = AGSTransformationMatrixCameraController() + /// The world tracking information used by `ARKit`. public var arConfiguration: ARConfiguration = { let config = ARWorldTrackingConfiguration() @@ -83,12 +66,18 @@ public class ArcGISARView: UIView { } /// The viewpoint camera used to set the initial view of the sceneView instead of the devices GPS location via the locationManager. - public var originCamera: AGSCamera? + public var originCamera: AGSCamera? { + didSet { + if let newCamera = originCamera { + cameraController.originCamera = newCamera + } + } + } /// The translation factor used to support a table top AR experience. public var translationTransformationFactor: Double = 1.0 - // We implement ARSCNViewDelegate methods, but will use `arSCNViewDelegate` to forward them to clients. + /// We implement ARSCNViewDelegate methods, but will use `arSCNViewDelegate` to forward them to clients. weak open var arSCNViewDelegate: ARSCNViewDelegate? // MARK: private properties @@ -124,14 +113,8 @@ public class ArcGISARView: UIView { /// Used when calculating framerate. private var lastUpdateTime: TimeInterval = 0 - // A quaternion used to compensate for the pitch beeing 90 degrees on `ARKit`; used to calculate the current device transformation for each frame. - let compensationQuat: simd_quatd = simd_quatd(ix: (sin(45 / (180 / .pi))), iy: 0, iz: 0, r: (cos(45 / (180 / .pi)))) - - /// The quaternion used to represent the device orientation; used to calculate the current device transformation for each frame; defaults to landcape-left. - var orientationQuat: simd_quatd = simd_quatd(statusBarOrientation: .landscapeLeft) - - // Denotes whether the orientation observer has been added to the NotificationCenter; used to prevent removing a not-yet-added observer. - private var orientationObserverAdded = false + /// A quaternion used to compensate for the pitch beeing 90 degrees on `ARKit`; used to calculate the current device transformation for each frame. + private let compensationQuat: simd_quatd = simd_quatd(ix: (sin(45 / (180 / .pi))), iy: 0, iz: 0, r: (cos(45 / (180 / .pi)))) // MARK: Initializers @@ -166,18 +149,17 @@ public class ArcGISARView: UIView { // Add sceneView to view and setup constraints. addSubviewWithConstraints(sceneView) - // Make our sceneView's background transparent, no atmosphereEffect. - sceneView.isBackgroundTransparent = true + // Make our sceneView's space effect be transparent, no atmosphereEffect. + sceneView.spaceEffect = .transparent sceneView.atmosphereEffect = .none + sceneView.cameraController = cameraController + // Tell the sceneView we will be calling `renderFrame()` manually. sceneView.isManualRendering = true // We haven't yet notified user of start or failure. notifiedStartOrFailure = false - - // Set intitial orientationQuat. - orientationChanged(notification: nil) } // MARK: Public @@ -235,28 +217,12 @@ public class ArcGISARView: UIView { startWithAccessAuthorized() } } - - // We need to know when the device orientation changes in order to update the Camera transformation. - UIDevice.current.beginGeneratingDeviceOrientationNotifications() - NotificationCenter.default.addObserver( - self, - selector: #selector(self.orientationChanged(notification:)), - name: UIDevice.orientationDidChangeNotification, - object: nil - ) - orientationObserverAdded = true } /// Suspends device tracking. public func stopTracking() { arSCNView.session.pause() stopUpdatingLocationAndHeading() - - UIDevice.current.endGeneratingDeviceOrientationNotifications() - if (orientationObserverAdded) { - NotificationCenter.default.removeObserver(self, name: UIDevice.orientationDidChangeNotification, object: nil) - orientationObserverAdded = false - } } // MARK: Private @@ -351,14 +317,6 @@ public class ArcGISARView: UIView { didStartOrFailWithError(nil) startUpdatingLocationAndHeading() } - - /// Called when device orientation changes. - /// - /// - Parameter notification: The notification. - @objc func orientationChanged(notification: Notification?) { - // Handle rotation here. - orientationQuat = simd_quatd(statusBarOrientation: UIDevice.current.orientation) - } } // MARK: - ARSCNViewDelegate @@ -438,7 +396,8 @@ extension ArcGISARView: SCNSceneRendererDelegate { guard let transform = arSCNView.pointOfView?.transform else { return } let cameraTransform = simd_double4x4(transform) - let finalQuat:simd_quatd = simd_mul(simd_mul(compensationQuat, simd_quatd(cameraTransform)), orientationQuat) + // Calculate our final quaternion and create the new transformation matrix. + let finalQuat:simd_quatd = simd_mul(compensationQuat, simd_quatd(cameraTransform)) var transformationMatrix = AGSTransformationMatrix(quaternionX: finalQuat.vector.x, quaternionY: finalQuat.vector.y, quaternionZ: finalQuat.vector.z, @@ -447,11 +406,13 @@ extension ArcGISARView: SCNSceneRendererDelegate { translationY: (-cameraTransform.columns.3.z) * translationTransformationFactor, translationZ: (cameraTransform.columns.3.y) * translationTransformationFactor) + // Add the new trasformation matrix to the initial matrix. transformationMatrix = initialTransformationMatrix.addTransformation(transformationMatrix) - let camera = AGSCamera(transformationMatrix: transformationMatrix) - sceneView.setViewpointCamera(camera) + // Set the matrix on the camera controller. + cameraController.transformationMatrix = transformationMatrix + // Render the Scene with the new transformation. sceneView.renderFrame() // Calculate frame rate. From e2e33295e169ad854557d1de7e24549e1e4c0612 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Mon, 17 Jun 2019 14:28:04 -0500 Subject: [PATCH 039/147] remove now-unnecessary initialTransformationMatrix; remove sceneView.setViewpointCamera as it's now all handled by the camera controller --- Toolkit/ArcGISToolkit/AR/ArcGISARView.swift | 23 ++++++++------------- 1 file changed, 9 insertions(+), 14 deletions(-) diff --git a/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift b/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift index ef1793c5..10546fb4 100644 --- a/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift +++ b/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift @@ -69,7 +69,9 @@ public class ArcGISARView: UIView { public var originCamera: AGSCamera? { didSet { if let newCamera = originCamera { + // Set the camera as the originCamera on the cameraController and reset tracking. cameraController.originCamera = newCamera + resetTracking() } } } @@ -99,9 +101,6 @@ public class ArcGISARView: UIView { /// Current horizontal accuracy of the device. private var horizontalAccuracy: CLLocationAccuracy = .greatestFiniteMagnitude - /// The intial camera position and orientation whether it was set via originCamera or the locationManager. - private var initialTransformationMatrix = AGSTransformationMatrix() - /// Whether `ARKit` is supported on this device. private var isSupported = { return ARWorldTrackingConfiguration.isSupported @@ -199,10 +198,8 @@ public class ArcGISARView: UIView { return } - if let origin = originCamera { - // We have a starting camera. - initialTransformationMatrix = origin.transformationMatrix - sceneView.setViewpointCamera(origin) + if let _ = originCamera { + // We have a starting camera, so no need to start the location manager, just finalizeStart(). finalizeStart() } else { @@ -391,6 +388,8 @@ extension ArcGISARView: SCNSceneRendererDelegate { } public func renderer(_ renderer: SCNSceneRenderer, willRenderScene scene: SCNScene, atTime time: TimeInterval) { + // If we haven't started yet, return. + guard notifiedStartOrFailure else { return } // Get transform from SCNView.pointOfView. guard let transform = arSCNView.pointOfView?.transform else { return } @@ -398,7 +397,7 @@ extension ArcGISARView: SCNSceneRendererDelegate { // Calculate our final quaternion and create the new transformation matrix. let finalQuat:simd_quatd = simd_mul(compensationQuat, simd_quatd(cameraTransform)) - var transformationMatrix = AGSTransformationMatrix(quaternionX: finalQuat.vector.x, + let transformationMatrix = AGSTransformationMatrix(quaternionX: finalQuat.vector.x, quaternionY: finalQuat.vector.y, quaternionZ: finalQuat.vector.z, quaternionW: finalQuat.vector.w, @@ -406,9 +405,6 @@ extension ArcGISARView: SCNSceneRendererDelegate { translationY: (-cameraTransform.columns.3.z) * translationTransformationFactor, translationZ: (cameraTransform.columns.3.y) * translationTransformationFactor) - // Add the new trasformation matrix to the initial matrix. - transformationMatrix = initialTransformationMatrix.addTransformation(transformationMatrix) - // Set the matrix on the camera controller. cameraController.transformationMatrix = transformationMatrix @@ -443,10 +439,9 @@ extension ArcGISARView: CLLocationManagerDelegate { y: location.coordinate.latitude, z: location.altitude, spatialReference: .wgs84()) - let camera = AGSCamera(location: locationPoint, heading: 0.0, pitch: 0.0, roll: 0.0) - initialTransformationMatrix = camera.transformationMatrix - sceneView.setViewpointCamera(camera) + // Create a new camera based on our location and set it on the cameraController. + cameraController.originCamera = AGSCamera(location: locationPoint, heading: 0.0, pitch: 0.0, roll: 0.0) finalizeStart() } else if location.horizontalAccuracy < horizontalAccuracy { From e98b0eb5483b9da5bcd19fefd116138ea9698293 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Tue, 18 Jun 2019 11:41:29 -0500 Subject: [PATCH 040/147] PR review changes; use guard & != nil --- Toolkit/ArcGISToolkit/AR/ArcGISARView.swift | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift b/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift index 10546fb4..f6b51634 100644 --- a/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift +++ b/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift @@ -68,18 +68,17 @@ public class ArcGISARView: UIView { /// The viewpoint camera used to set the initial view of the sceneView instead of the devices GPS location via the locationManager. public var originCamera: AGSCamera? { didSet { - if let newCamera = originCamera { - // Set the camera as the originCamera on the cameraController and reset tracking. - cameraController.originCamera = newCamera - resetTracking() - } + guard let newCamera = originCamera else { return } + // Set the camera as the originCamera on the cameraController and reset tracking. + cameraController.originCamera = newCamera + resetTracking() } } /// The translation factor used to support a table top AR experience. public var translationTransformationFactor: Double = 1.0 - /// We implement ARSCNViewDelegate methods, but will use `arSCNViewDelegate` to forward them to clients. + /// We implement `ARSCNViewDelegate` methods, but will use `arSCNViewDelegate` to forward them to clients. weak open var arSCNViewDelegate: ARSCNViewDelegate? // MARK: private properties @@ -198,7 +197,7 @@ public class ArcGISARView: UIView { return } - if let _ = originCamera { + if originCamera != nil { // We have a starting camera, so no need to start the location manager, just finalizeStart(). finalizeStart() } From 0dbdc7fedffad9d7f4bd3789b9aa474615f21587 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Tue, 18 Jun 2019 12:32:40 -0500 Subject: [PATCH 041/147] Fix typo --- Toolkit/ArcGISToolkit/AR/ArcGISARView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift b/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift index f6b51634..5ce8ea8a 100644 --- a/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift +++ b/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift @@ -111,7 +111,7 @@ public class ArcGISARView: UIView { /// Used when calculating framerate. private var lastUpdateTime: TimeInterval = 0 - /// A quaternion used to compensate for the pitch beeing 90 degrees on `ARKit`; used to calculate the current device transformation for each frame. + /// A quaternion used to compensate for the pitch being 90 degrees on `ARKit`; used to calculate the current device transformation for each frame. private let compensationQuat: simd_quatd = simd_quatd(ix: (sin(45 / (180 / .pi))), iy: 0, iz: 0, r: (cos(45 / (180 / .pi)))) // MARK: Initializers From 220303883d4a956e8d6dd09281d8fd6de79a0411 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Thu, 20 Jun 2019 16:40:30 -0500 Subject: [PATCH 042/147] Set FOV on SceneView at each new frame from ARKit --- Toolkit/ArcGISToolkit/AR/ArcGISARView.swift | 32 +++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift b/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift index 5ce8ea8a..128eddb9 100644 --- a/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift +++ b/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift @@ -16,6 +16,24 @@ import UIKit import ARKit import ArcGIS +extension AGSDeviceOrientation { + init?(statusBarOrientation: UIDeviceOrientation) { + switch statusBarOrientation { + case .landscapeLeft: + self.init(rawValue: AGSDeviceOrientation.landscapeRight.rawValue) + case .landscapeRight: + self.init(rawValue: AGSDeviceOrientation.landscapeLeft.rawValue) + case .portrait: + self.init(rawValue: AGSDeviceOrientation.portrait.rawValue) + case .portraitUpsideDown: + self.init(rawValue: AGSDeviceOrientation.reversePortrait.rawValue) + default: + // default to landscapeLeft + self.init(rawValue: AGSDeviceOrientation.landscapeLeft.rawValue) + } + } +} + extension ArcGISARView.CoreLocationError: CustomNSError { static var errorDomain: String { return kCLErrorDomain @@ -407,6 +425,20 @@ extension ArcGISARView: SCNSceneRendererDelegate { // Set the matrix on the camera controller. cameraController.transformationMatrix = transformationMatrix + // Set FOV on camera. + if let camera = arSCNView.session.currentFrame?.camera { + let intrinsics = camera.intrinsics + let imageResolution = camera.imageResolution + sceneView.setFieldOfViewFromLensIntrinsicsWithXFocalLength(intrinsics[0][0], + yFocalLength: intrinsics[1][1], + xPrincipal: intrinsics[2][0], + yPrincipal: intrinsics[2][1], + xImageSize: Float(imageResolution.width), + yImageSize: Float(imageResolution.height), + deviceOrientation: AGSDeviceOrientation.init(statusBarOrientation: UIDevice.current.orientation) ?? .landscapeRight) + } + // print("FOV: \(sceneView.fieldOfView); distortion = \(sceneView.fieldOfViewDistortionRatio)") + // Render the Scene with the new transformation. sceneView.renderFrame() From 27c7c508485c1a31173b3dee9441456cb8980fbc Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Tue, 25 Jun 2019 17:23:51 -0500 Subject: [PATCH 043/147] Remove commented out print statement --- Toolkit/ArcGISToolkit/AR/ArcGISARView.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift b/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift index 128eddb9..0ad275df 100644 --- a/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift +++ b/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift @@ -437,7 +437,6 @@ extension ArcGISARView: SCNSceneRendererDelegate { yImageSize: Float(imageResolution.height), deviceOrientation: AGSDeviceOrientation.init(statusBarOrientation: UIDevice.current.orientation) ?? .landscapeRight) } - // print("FOV: \(sceneView.fieldOfView); distortion = \(sceneView.fieldOfViewDistortionRatio)") // Render the Scene with the new transformation. sceneView.renderFrame() From 7d1955ddf51072e6f1b02cf019ea1f370b415b23 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Tue, 25 Jun 2019 17:24:39 -0500 Subject: [PATCH 044/147] exportOptions.plist allows building/archiving of the .ipa from the command-line --- Examples/exportOptions.plist | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 Examples/exportOptions.plist diff --git a/Examples/exportOptions.plist b/Examples/exportOptions.plist new file mode 100644 index 00000000..6631ffa6 --- /dev/null +++ b/Examples/exportOptions.plist @@ -0,0 +1,6 @@ + + + + + + From e702ce44aeb5b515b904322941d850f8308e9e52 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Tue, 25 Jun 2019 17:37:56 -0500 Subject: [PATCH 045/147] Add doc for AGSDeviceOrientation extension init method. --- Toolkit/ArcGISToolkit/AR/ArcGISARView.swift | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift b/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift index 0ad275df..827aecaa 100644 --- a/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift +++ b/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift @@ -17,6 +17,9 @@ import ARKit import ArcGIS extension AGSDeviceOrientation { + /// Allows creation of an `AGSDeviceOrientation` from a `UIDeviceOrientation`. + /// + /// - Parameter statusBarOrientation: The `UIDeviceOrientation` to create the `AGSDeviceOrientation` from. init?(statusBarOrientation: UIDeviceOrientation) { switch statusBarOrientation { case .landscapeLeft: From 2f38ab43dc810a3d1b77c2a4525a39142b746b5c Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Thu, 27 Jun 2019 10:22:25 -0500 Subject: [PATCH 046/147] Update Toolkit/ArcGISToolkit/AR/ArcGISARView.swift Co-Authored-By: Philip Ridgeway --- Toolkit/ArcGISToolkit/AR/ArcGISARView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift b/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift index 827aecaa..56b36004 100644 --- a/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift +++ b/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift @@ -438,7 +438,7 @@ extension ArcGISARView: SCNSceneRendererDelegate { yPrincipal: intrinsics[2][1], xImageSize: Float(imageResolution.width), yImageSize: Float(imageResolution.height), - deviceOrientation: AGSDeviceOrientation.init(statusBarOrientation: UIDevice.current.orientation) ?? .landscapeRight) + deviceOrientation: AGSDeviceOrientation(statusBarOrientation: UIDevice.current.orientation) ?? .landscapeRight) } // Render the Scene with the new transformation. From 0b987aa089a5bae6bfcff8e0f46371087c22593b Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Thu, 27 Jun 2019 11:44:14 -0500 Subject: [PATCH 047/147] Make initializer non-optional; default to .landscapeRight --- Toolkit/ArcGISToolkit/AR/ArcGISARView.swift | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift b/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift index 56b36004..dc8884d1 100644 --- a/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift +++ b/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift @@ -20,19 +20,18 @@ extension AGSDeviceOrientation { /// Allows creation of an `AGSDeviceOrientation` from a `UIDeviceOrientation`. /// /// - Parameter statusBarOrientation: The `UIDeviceOrientation` to create the `AGSDeviceOrientation` from. - init?(statusBarOrientation: UIDeviceOrientation) { + init(statusBarOrientation: UIDeviceOrientation) { switch statusBarOrientation { case .landscapeLeft: - self.init(rawValue: AGSDeviceOrientation.landscapeRight.rawValue) + self = .landscapeRight case .landscapeRight: - self.init(rawValue: AGSDeviceOrientation.landscapeLeft.rawValue) + self = .landscapeLeft case .portrait: - self.init(rawValue: AGSDeviceOrientation.portrait.rawValue) + self = .portrait case .portraitUpsideDown: - self.init(rawValue: AGSDeviceOrientation.reversePortrait.rawValue) + self = .reversePortrait default: - // default to landscapeLeft - self.init(rawValue: AGSDeviceOrientation.landscapeLeft.rawValue) + self = .landscapeRight } } } @@ -438,7 +437,7 @@ extension ArcGISARView: SCNSceneRendererDelegate { yPrincipal: intrinsics[2][1], xImageSize: Float(imageResolution.width), yImageSize: Float(imageResolution.height), - deviceOrientation: AGSDeviceOrientation(statusBarOrientation: UIDevice.current.orientation) ?? .landscapeRight) + deviceOrientation: AGSDeviceOrientation(statusBarOrientation: UIDevice.current.orientation)) } // Render the Scene with the new transformation. From 49e6afefd0b82835c3a7b7589ed59aa584059e6d Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Fri, 28 Jun 2019 16:22:49 -0500 Subject: [PATCH 048/147] Replace AGSDeviceOrientation with UIDeviceOrientation to match SDK API --- Toolkit/ArcGISToolkit/AR/ArcGISARView.swift | 22 +-------------------- 1 file changed, 1 insertion(+), 21 deletions(-) diff --git a/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift b/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift index dc8884d1..7c159ccd 100644 --- a/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift +++ b/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift @@ -16,26 +16,6 @@ import UIKit import ARKit import ArcGIS -extension AGSDeviceOrientation { - /// Allows creation of an `AGSDeviceOrientation` from a `UIDeviceOrientation`. - /// - /// - Parameter statusBarOrientation: The `UIDeviceOrientation` to create the `AGSDeviceOrientation` from. - init(statusBarOrientation: UIDeviceOrientation) { - switch statusBarOrientation { - case .landscapeLeft: - self = .landscapeRight - case .landscapeRight: - self = .landscapeLeft - case .portrait: - self = .portrait - case .portraitUpsideDown: - self = .reversePortrait - default: - self = .landscapeRight - } - } -} - extension ArcGISARView.CoreLocationError: CustomNSError { static var errorDomain: String { return kCLErrorDomain @@ -437,7 +417,7 @@ extension ArcGISARView: SCNSceneRendererDelegate { yPrincipal: intrinsics[2][1], xImageSize: Float(imageResolution.width), yImageSize: Float(imageResolution.height), - deviceOrientation: AGSDeviceOrientation(statusBarOrientation: UIDevice.current.orientation)) + deviceOrientation: UIDevice.current.orientation) } // Render the Scene with the new transformation. From c30ac81dcf850a7b6502db6d8d96906141b41e6a Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Mon, 15 Jul 2019 14:53:56 -0500 Subject: [PATCH 049/147] Update ArcGISARView to use the newly updated LocationDataSource class and subclasses. --- .../ArcGISToolkitExamples/ARExample.swift | 7 +- Toolkit/ArcGISToolkit/AR/ArcGISARView.swift | 360 +++++++----------- 2 files changed, 150 insertions(+), 217 deletions(-) diff --git a/Examples/ArcGISToolkitExamples/ARExample.swift b/Examples/ArcGISToolkitExamples/ARExample.swift index b1dcc483..9475fb71 100644 --- a/Examples/ArcGISToolkitExamples/ARExample.swift +++ b/Examples/ArcGISToolkitExamples/ARExample.swift @@ -18,7 +18,7 @@ import ArcGIS class ARExample: UIViewController { - let arView = ArcGISARView(frame: .zero) + let arView = ArcGISARView(renderVideoFeed: true, tryUsingARKit: true) override func viewDidLoad() { super.viewDidLoad() @@ -36,10 +36,13 @@ class ARExample: UIViewController { ]) arView.sceneView.scene = makeStreetsScene() + arView.locationDataSource = AGSCLLocationDataSource() } override func viewDidAppear(_ animated: Bool) { - arView.startTracking() + arView.startTracking { (error) in + print("Error starting ArcGISARView tracking: \(String(describing: error))") + } } override func viewDidDisappear(_ animated: Bool) { diff --git a/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift b/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift index 7c159ccd..6b28fd77 100644 --- a/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift +++ b/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift @@ -16,57 +16,31 @@ import UIKit import ARKit import ArcGIS -extension ArcGISARView.CoreLocationError: CustomNSError { - static var errorDomain: String { - return kCLErrorDomain - } - - var errorCode: Int { - return CLError.Code.denied.rawValue - } - - var errorDescription: String? { - switch self { - case .accessDenied: - return "Access to the device location is denied." - case .missingPListKey: - return "You must specify a location usage description key (NSLocationWhenInUseUsageDescription or NSLocationAlwaysUsageDescription) in your plist." - } - } -} - public class ArcGISARView: UIView { - - enum CoreLocationError: Swift.Error { - case accessDenied - case missingPListKey - } // MARK: public properties /// The view used to display the `ARKit` camera image and 3D `SceneKit` content. public let arSCNView = ARSCNView(frame: .zero) - /// The view used to display ArcGIS 3D content. - public let sceneView = AGSSceneView(frame: .zero) + /// The initial transformation used for a table top experience. Defaults to the Identity Matrix. + public private(set) var initialTransformation = AGSTransformationMatrix.identity - /// The camera controller used to control the Scene - private let cameraController = AGSTransformationMatrixCameraController() + /// Denotes whether tracking location and angles has started. + public private(set) var isTracking: Bool = false - /// The world tracking information used by `ARKit`. - public var arConfiguration: ARConfiguration = { - let config = ARWorldTrackingConfiguration() - config.worldAlignment = .gravityAndHeading - return config - }() { + /// Denotes whether ARKit is being used to track location and angles. + public private(set) var isUsingARKit: Bool = true + + /// The data source used to get device location. Used either in conjuction with ARKit data or when ARKit is not present or not being used. + public var locationDataSource: AGSCLLocationDataSource? { didSet { - // Start tracking using the new configuration. - startTracking() + locationDataSource?.locationChangeHandlerDelegate = self } } - /// The viewpoint camera used to set the initial view of the sceneView instead of the devices GPS location via the locationManager. - public var originCamera: AGSCamera? { + /// The viewpoint camera used to set the initial view of the sceneView instead of the device's GPS location via the location data source. You can use Key-Value Observing to track changes to the origin camera. + @objc public dynamic var originCamera: AGSCamera? { didSet { guard let newCamera = originCamera else { return } // Set the camera as the originCamera on the cameraController and reset tracking. @@ -74,39 +48,42 @@ public class ArcGISARView: UIView { resetTracking() } } + + /// The view used to display ArcGIS 3D content. + public let sceneView = AGSSceneView(frame: .zero) /// The translation factor used to support a table top AR experience. - public var translationTransformationFactor: Double = 1.0 + public var translationFactor: Double = 1.0 { + didSet { + cameraController.translationFactor = translationFactor + } + } + + /// The world tracking information used by `ARKit`. + public var arConfiguration: ARConfiguration = { + let config = ARWorldTrackingConfiguration() + config.worldAlignment = .gravityAndHeading + config.planeDetection = [.horizontal] + return config + }() { + didSet { + // If we're already tracking, reset tracking to use the new configuration. + if isTracking { + resetTracking() + } + } + } /// We implement `ARSCNViewDelegate` methods, but will use `arSCNViewDelegate` to forward them to clients. - weak open var arSCNViewDelegate: ARSCNViewDelegate? + weak public var arSCNViewDelegate: ARSCNViewDelegate? - // MARK: private properties - - /// Whether to display the camera image or not. - private var renderVideoFeed = true - - /// Used to determine the device location when originCamera is not set. - private lazy var locationManager: CLLocationManager = { [unowned self] in - let lm = CLLocationManager() - lm.desiredAccuracy = kCLLocationAccuracyBest - lm.delegate = self - return lm - }() - - /// Initial location from locationManager. - private var initialLocation: CLLocation? + // MARK: Private properties - /// Current horizontal accuracy of the device. - private var horizontalAccuracy: CLLocationAccuracy = .greatestFiniteMagnitude - - /// Whether `ARKit` is supported on this device. - private var isSupported = { - return ARWorldTrackingConfiguration.isSupported - }() + /// The camera controller used to control the Scene. + private let cameraController = AGSTransformationMatrixCameraController() - /// Whether the client has been notfiied of start/failure. - private var notifiedStartOrFailure = false + /// Initial location from location data source. + private var initialLocation: AGSPoint? /// Used when calculating framerate. private var lastUpdateTime: TimeInterval = 0 @@ -114,6 +91,11 @@ public class ArcGISARView: UIView { /// A quaternion used to compensate for the pitch being 90 degrees on `ARKit`; used to calculate the current device transformation for each frame. private let compensationQuat: simd_quatd = simd_quatd(ix: (sin(45 / (180 / .pi))), iy: 0, iz: 0, r: (cos(45 / (180 / .pi)))) + /// Whether `ARKit` is supported on this device. + private let deviceSupportsARKit: Bool = { + return ARWorldTrackingConfiguration.isSupported + }() + // MARK: Initializers public override init(frame: CGRect) { @@ -128,10 +110,24 @@ public class ArcGISARView: UIView { /// Initializer used to denote whether to display the live camera image. /// - /// - Parameter renderVideoFeed: Whether to display the live camera image. - public convenience init(renderVideoFeed: Bool){ + /// - Parameters: + /// - renderVideoFeed: Whether to display the live camera image. + /// - tryUsingARKit: Whether or not to use ARKit, regardless if it's available. + public convenience init(renderVideoFeed: Bool, tryUsingARKit: Bool){ self.init(frame: .zero) - self.renderVideoFeed = renderVideoFeed + + // This overrides the `sharedInitialization()` isUsingARKit code + isUsingARKit = tryUsingARKit && deviceSupportsARKit + + if !isUsingARKit || !renderVideoFeed { + // User is not using ARKit, or they don't want to see video, so remove the arSCNView from the superView (it was added in sharedInitialization()). + // This overrides the `sharedInitialization()` arSCNView code + arSCNView.removeFromSuperview() + } + + // Tell the sceneView we will be calling `renderFrame()` manually if we're using ARKit. + // This overrides the `sharedInitialization()` `isManualRendering` code + sceneView.isManualRendering = isUsingARKit } deinit { @@ -141,23 +137,26 @@ public class ArcGISARView: UIView { /// Initialization code shared between all initializers. private func sharedInitialization(){ // Add the ARSCNView to our view. - addSubviewWithConstraints(arSCNView) - arSCNView.delegate = self - + if deviceSupportsARKit { + addSubviewWithConstraints(arSCNView) + arSCNView.delegate = self + } + + // Always use ARKit if device supports it. + isUsingARKit = deviceSupportsARKit + // Add sceneView to view and setup constraints. addSubviewWithConstraints(sceneView) - // Make our sceneView's space effect be transparent, no atmosphereEffect. + // Make our sceneView's spaceEffect be transparent, no atmosphereEffect. sceneView.spaceEffect = .transparent sceneView.atmosphereEffect = .none + // Set the camera controller on the sceneView sceneView.cameraController = cameraController - // Tell the sceneView we will be calling `renderFrame()` manually. - sceneView.isManualRendering = true - - // We haven't yet notified user of start or failure. - notifiedStartOrFailure = false + // Tell the sceneView we will be calling `renderFrame()` manually if we're using ARKit. + sceneView.isManualRendering = isUsingARKit } // MARK: Public @@ -166,71 +165,73 @@ public class ArcGISARView: UIView { /// /// - Parameter toLocation: The point in screen coordinates. /// - Returns: The map point corresponding to screenPoint. - public func arScreenToLocation(screenPoint: AGSPoint) -> AGSPoint { + public func arScreenToLocation(screenPoint: CGPoint) -> AGSPoint { fatalError("arScreen(toLocation:) has not been implemented") } - /// Resets the device tracking, using `originCamera` if it's not nil or the device's GPS location via the locationManager. + /// Resets the device tracking, using `originCamera` if it's not nil or the device's GPS location via the location data source. public func resetTracking() { initialLocation = nil startTracking() } - - /// Resets the device tracking, using the device's GPS location via the locationManager. + + /// Sets the initial transformation used to offset the originCamera. /// - /// - Returns: Reset operation success or failure. - public func resetUsingLocationServices() -> Bool { - fatalError("resetUsingLocationServices() has not been implemented") + /// - Parameter initialTransformation: The initial transformation for originCamera offset. + /// - Returns: Whether setting the `initialTransformation` succeeded or failed. + public func setInitialTransformation(initialTransformation: AGSTransformationMatrix) -> Bool { + fatalError("setInitialTransformation(initialTransformation:) has not been implemented") } - /// Resets the device tracking using a spacial anchor. + /// Sets the initial transformation used to offset the originCamera. The initial transformation is based on an AR point determined via existing plan hit detection from `screenPoint`. If an AR point cannot be determined, this method will return `false`. /// - /// - Returns: Reset operation success or failure. - public func resetUsingSpatialAnchor() -> Bool { - fatalError("resetUsingSpatialAnchor() has not been implemented") + /// - Parameter screenPoint: The screen point to determine the `initialTransformation` from.. + /// - Returns: Whether setting the `initialTransformation` succeeded or failed. + public func setInitialTransformation(screenPoint: CGPoint) -> Bool { + fatalError("setInitialTransformationPlacingOriginOnPlane(screenPoint:) has not been implemented") } - + /// Starts device tracking. - public func startTracking() { - if !isSupported { - didStartOrFailWithError(ARError(.unsupportedConfiguration)) - return - } - - if originCamera != nil { - // We have a starting camera, so no need to start the location manager, just finalizeStart(). - finalizeStart() + public func startTracking(_ completion: ((_ error: Error?) -> Void)? = nil) { + // We have a location data source that needs to be started. + if let locationDataSource = self.locationDataSource { + locationDataSource.start { [weak self] (error) in + if let error = error { + completion?(error) + } + else { + self?.finalizeStart() + completion?(nil) + } + } } else { - // No starting camera, use location manger to get initial location. - let authStatus = CLLocationManager.authorizationStatus() - switch authStatus { - case .notDetermined: - startWithAccessNotDetermined() - case .restricted, .denied: - startWithAccessDenied() - case .authorizedAlways, .authorizedWhenInUse: - startWithAccessAuthorized() - } + // No data source, continue with defaults. + finalizeStart() + completion?(nil) } } /// Suspends device tracking. public func stopTracking() { arSCNView.session.pause() - stopUpdatingLocationAndHeading() + locationDataSource?.stop() + isTracking = false } // MARK: Private /// Operations that happen after device tracking has started. fileprivate func finalizeStart() { - // Hide the camera image if necessary. - arSCNView.isHidden = !renderVideoFeed - - // Run the ARSession. - arSCNView.session.run(arConfiguration, options:.resetTracking) - didStartOrFailWithError(nil) + DispatchQueue.main.async { [weak self] in + guard let strongSelf = self else { return } + // Run the ARSession. + if strongSelf.isUsingARKit { + strongSelf.arSCNView.session.run(strongSelf.arConfiguration, options:.resetTracking) + } + + strongSelf.isTracking = true + } } /// Adds subView to superView with appropriate constraints. @@ -247,72 +248,6 @@ public class ArcGISARView: UIView { subview.bottomAnchor.constraint(equalTo: self.bottomAnchor) ]) } - - /// Start the locationManager with undetermined access. - fileprivate func startWithAccessNotDetermined() { - if (Bundle.main.object(forInfoDictionaryKey: "NSLocationWhenInUseUsageDescription") != nil) { - locationManager.requestWhenInUseAuthorization() - } - if (Bundle.main.object(forInfoDictionaryKey: "NSLocationAlwaysUsageDescription") != nil) { - locationManager.requestAlwaysAuthorization() - } - else{ - didStartOrFailWithError(CoreLocationError.missingPListKey) - } - } - - /// Start updating the location and heading via the locationManager. - fileprivate func startUpdatingLocationAndHeading() { - locationManager.startUpdatingLocation() - if CLLocationManager.headingAvailable() { - locationManager.startUpdatingHeading() - } - } - - /// Stop updating location and heading. - fileprivate func stopUpdatingLocationAndHeading() { - locationManager.stopUpdatingLocation() - if CLLocationManager.headingAvailable() { - locationManager.stopUpdatingHeading() - } - } - - /// Start the locationManager with denied access. - fileprivate func startWithAccessDenied() { - didStartOrFailWithError(CoreLocationError.accessDenied) - } - - /// Start the locationManager with authorized access. - fileprivate func startWithAccessAuthorized() { - startUpdatingLocationAndHeading() - } - - /// Potential notification to the user of an error starting device tracking. - /// - /// - Parameter error: The error that occurred when starting tracking. - fileprivate func didStartOrFailWithError(_ error: Error?) { - if !notifiedStartOrFailure, let error = error { - // TODO: present error to user... - } - - notifiedStartOrFailure = true - } - - /// Handle a change in authorization status to "denied". - fileprivate func handleAuthStatusChangedAccessDenied() { - // auth status changed to denied - stopUpdatingLocationAndHeading() - - // We were waiting for user prompt to come back, so notify. - didStartOrFailWithError(CoreLocationError.accessDenied) - } - - /// Handle a change in authorization status to "authorized". - fileprivate func handleAuthStatusChangedAccessAuthorized() { - // Auth status changed to authorized; now that we have authorization - start updating location and heading. - didStartOrFailWithError(nil) - startUpdatingLocationAndHeading() - } } // MARK: - ARSCNViewDelegate @@ -387,8 +322,8 @@ extension ArcGISARView: SCNSceneRendererDelegate { } public func renderer(_ renderer: SCNSceneRenderer, willRenderScene scene: SCNScene, atTime time: TimeInterval) { - // If we haven't started yet, return. - guard notifiedStartOrFailure else { return } + // If we aren't tracking yet, return. + guard isTracking else { return } // Get transform from SCNView.pointOfView. guard let transform = arSCNView.pointOfView?.transform else { return } @@ -400,9 +335,9 @@ extension ArcGISARView: SCNSceneRendererDelegate { quaternionY: finalQuat.vector.y, quaternionZ: finalQuat.vector.z, quaternionW: finalQuat.vector.w, - translationX: (cameraTransform.columns.3.x) * translationTransformationFactor, - translationY: (-cameraTransform.columns.3.z) * translationTransformationFactor, - translationZ: (cameraTransform.columns.3.y) * translationTransformationFactor) + translationX: (cameraTransform.columns.3.x), + translationY: (-cameraTransform.columns.3.z), + translationZ: (cameraTransform.columns.3.y)) // Set the matrix on the camera controller. cameraController.transformationMatrix = transformationMatrix @@ -437,44 +372,39 @@ extension ArcGISARView: SCNSceneRendererDelegate { } } -// MARK: - CLLocationManagerDelegate -extension ArcGISARView: CLLocationManagerDelegate { +// MARK: - AGSLocationChangeHandlerDelegate +extension ArcGISARView: AGSLocationChangeHandlerDelegate { + + public func locationDataSource(_ locationDataSource: AGSLocationDataSource, headingDidChange heading: Double) { + // Heading changed. + if !isUsingARKit { + // Not using ARKit, so update heading on the camera directly; otherwise, let ARKit handle heading changes. + let currentCamera = sceneView.currentViewpointCamera() + let camera = currentCamera.rotate(toHeading: heading, pitch: currentCamera.pitch, roll: currentCamera.roll) + sceneView.setViewpointCamera(camera) +// print("heading changed: \(heading)") + } + } - public func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) { - guard let location = locations.last, location.horizontalAccuracy >= 0.0 else { return } + public func locationDataSource(_ locationDataSource: AGSLocationDataSource, locationDidChange location: AGSLocation) { + // Location changed. + guard let locationPoint = location.position else { return } if initialLocation == nil { - initialLocation = location - horizontalAccuracy = location.horizontalAccuracy - - let locationPoint = AGSPoint(x: location.coordinate.longitude, - y: location.coordinate.latitude, - z: location.altitude, - spatialReference: .wgs84()) + initialLocation = locationPoint // Create a new camera based on our location and set it on the cameraController. cameraController.originCamera = AGSCamera(location: locationPoint, heading: 0.0, pitch: 0.0, roll: 0.0) - finalizeStart() } - else if location.horizontalAccuracy < horizontalAccuracy { - horizontalAccuracy = location.horizontalAccuracy + else if !isUsingARKit { + let camera = sceneView.currentViewpointCamera().move(toLocation: locationPoint) + sceneView.setViewpointCamera(camera) +// print("location changed: \(locationPoint), accuracy: \(location.horizontalAccuracy)") } - - } - - public func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) { - didStartOrFailWithError(error) } - - public func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) { - let authStatus = CLLocationManager.authorizationStatus() - switch authStatus { - case .notDetermined: - break - case .restricted, .denied: - handleAuthStatusChangedAccessDenied() - case .authorizedAlways, .authorizedWhenInUse: - handleAuthStatusChangedAccessAuthorized() - } + + public func locationDataSource(_ locationDataSource: AGSLocationDataSource, statusDidChange status: AGSLocationDataSourceStatus) { + // Status changed. +// print("locationDataSource status changed: \(status.rawValue)") } } From c1289cb080995a2fdb00d83f55c172c30c150049 Mon Sep 17 00:00:00 2001 From: Al Pascual Date: Mon, 15 Jul 2019 17:45:15 -0700 Subject: [PATCH 050/147] Improved doc --- README.md | 17 ++++------------- 1 file changed, 4 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 8958aac5..0fefe335 100644 --- a/README.md +++ b/README.md @@ -30,22 +30,13 @@ The *ArcGIS Runtime Toolkit for iOS* has a *Target SDK* version of *11.0*, meani ### [Carthage](https://github.com/Carthage/Carthage) -Carthage is a decentralized dependency manager that builds your dependencies and provides you with binary frameworks. - -If you don't have carthage installed, first run: -``` -brew update -brew install carthage -``` - -Then: +[![Carthage compatible](https://img.shields.io/badge/Carthage-compatible-4BC51D.svg?style=flat)](https://github.com/Carthage/Carthage) -- Add the following to your Cartfile: `github "Esri/arcgis-runtime-toolkit-ios"` -- Then run `carthage update` +Carthage is a decentralized dependency manager that builds your dependencies and provides you with binary frameworks. To integrate ArcGIS Runtime Toolkit for iOS into your Xcode project using Carthage, Add into your `Cartfile` or create a `Cartfile`, specify it in your Cartfile: -[carthage-installation]: https://github.com/Carthage/Carthage#adding-frameworks-to-an-application +`github "esri/ArcGISToolkit"` -[![Carthage compatible](https://img.shields.io/badge/Carthage-compatible-4BC51D.svg?style=flat)](https://github.com/Carthage/Carthage) +Run `carthage update` ### Manual 1. Ensure you have downloaded and installed __ArcGIS Runtime SDK for iOS__ as described [here](https://developers.arcgis.com/ios/latest/swift/guide/install.htm#ESRI_SECTION1_D57435A2BEBC4D29AFA3A4CAA722506A) From 7e4354e1811c0fe05a00811bb258022f8ffc944c Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Tue, 16 Jul 2019 11:14:03 -0500 Subject: [PATCH 051/147] Update Toolkit/ArcGISToolkit/AR/ArcGISARView.swift Co-Authored-By: Philip Ridgeway --- Toolkit/ArcGISToolkit/AR/ArcGISARView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift b/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift index 6b28fd77..496778b5 100644 --- a/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift +++ b/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift @@ -335,7 +335,7 @@ extension ArcGISARView: SCNSceneRendererDelegate { quaternionY: finalQuat.vector.y, quaternionZ: finalQuat.vector.z, quaternionW: finalQuat.vector.w, - translationX: (cameraTransform.columns.3.x), + translationX: cameraTransform.columns.3.x, translationY: (-cameraTransform.columns.3.z), translationZ: (cameraTransform.columns.3.y)) From 2aaa568a73aee9f482947e3e937bc42ed9a3b2b8 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Tue, 16 Jul 2019 11:14:24 -0500 Subject: [PATCH 052/147] Update Toolkit/ArcGISToolkit/AR/ArcGISARView.swift Co-Authored-By: Philip Ridgeway --- Toolkit/ArcGISToolkit/AR/ArcGISARView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift b/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift index 496778b5..200efbeb 100644 --- a/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift +++ b/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift @@ -336,7 +336,7 @@ extension ArcGISARView: SCNSceneRendererDelegate { quaternionZ: finalQuat.vector.z, quaternionW: finalQuat.vector.w, translationX: cameraTransform.columns.3.x, - translationY: (-cameraTransform.columns.3.z), + translationY: -cameraTransform.columns.3.z, translationZ: (cameraTransform.columns.3.y)) // Set the matrix on the camera controller. From 36847bae5d7092991077f8e2a4ec746d354f1734 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Tue, 16 Jul 2019 11:14:35 -0500 Subject: [PATCH 053/147] Update Toolkit/ArcGISToolkit/AR/ArcGISARView.swift Co-Authored-By: Philip Ridgeway --- Toolkit/ArcGISToolkit/AR/ArcGISARView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift b/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift index 200efbeb..dde6b609 100644 --- a/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift +++ b/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift @@ -337,7 +337,7 @@ extension ArcGISARView: SCNSceneRendererDelegate { quaternionW: finalQuat.vector.w, translationX: cameraTransform.columns.3.x, translationY: -cameraTransform.columns.3.z, - translationZ: (cameraTransform.columns.3.y)) + translationZ: cameraTransform.columns.3.y) // Set the matrix on the camera controller. cameraController.transformationMatrix = transformationMatrix From 8bcb98b47103ea89b3994b70b4e174aed6570d27 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Tue, 16 Jul 2019 11:36:31 -0500 Subject: [PATCH 054/147] PR Review changes. --- Examples/ArcGISToolkitExamples/ARExample.swift | 2 ++ Toolkit/ArcGISToolkit/AR/ArcGISARView.swift | 9 +++------ 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/Examples/ArcGISToolkitExamples/ARExample.swift b/Examples/ArcGISToolkitExamples/ARExample.swift index 9475fb71..ac280503 100644 --- a/Examples/ArcGISToolkitExamples/ARExample.swift +++ b/Examples/ArcGISToolkitExamples/ARExample.swift @@ -40,12 +40,14 @@ class ARExample: UIViewController { } override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) arView.startTracking { (error) in print("Error starting ArcGISARView tracking: \(String(describing: error))") } } override func viewDidDisappear(_ animated: Bool) { + super.viewDidDisappear(animated) arView.stopTracking() } diff --git a/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift b/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift index 6b28fd77..7b2c2f7d 100644 --- a/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift +++ b/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift @@ -196,13 +196,10 @@ public class ArcGISARView: UIView { // We have a location data source that needs to be started. if let locationDataSource = self.locationDataSource { locationDataSource.start { [weak self] (error) in - if let error = error { - completion?(error) - } - else { + if error == nil { self?.finalizeStart() - completion?(nil) } + completion?(error) } } else { @@ -227,7 +224,7 @@ public class ArcGISARView: UIView { guard let strongSelf = self else { return } // Run the ARSession. if strongSelf.isUsingARKit { - strongSelf.arSCNView.session.run(strongSelf.arConfiguration, options:.resetTracking) + strongSelf.arSCNView.session.run(strongSelf.arConfiguration, options: .resetTracking) } strongSelf.isTracking = true From 11484e9c28f1c58027a8ba774b0eeaf6bc0e40b9 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Tue, 16 Jul 2019 12:24:40 -0500 Subject: [PATCH 055/147] PR review changes. Remove duplicate period; check for error. --- Examples/ArcGISToolkitExamples/ARExample.swift | 4 +++- Toolkit/ArcGISToolkit/AR/ArcGISARView.swift | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/Examples/ArcGISToolkitExamples/ARExample.swift b/Examples/ArcGISToolkitExamples/ARExample.swift index ac280503..c1fbf467 100644 --- a/Examples/ArcGISToolkitExamples/ARExample.swift +++ b/Examples/ArcGISToolkitExamples/ARExample.swift @@ -42,7 +42,9 @@ class ARExample: UIViewController { override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) arView.startTracking { (error) in - print("Error starting ArcGISARView tracking: \(String(describing: error))") + if error != nil { + print("Error starting ArcGISARView tracking: \(String(describing: error))") + } } } diff --git a/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift b/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift index d269ab89..95a39dab 100644 --- a/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift +++ b/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift @@ -185,7 +185,7 @@ public class ArcGISARView: UIView { /// Sets the initial transformation used to offset the originCamera. The initial transformation is based on an AR point determined via existing plan hit detection from `screenPoint`. If an AR point cannot be determined, this method will return `false`. /// - /// - Parameter screenPoint: The screen point to determine the `initialTransformation` from.. + /// - Parameter screenPoint: The screen point to determine the `initialTransformation` from. /// - Returns: Whether setting the `initialTransformation` succeeded or failed. public func setInitialTransformation(screenPoint: CGPoint) -> Bool { fatalError("setInitialTransformationPlacingOriginOnPlane(screenPoint:) has not been implemented") From 40c1f53096a45aad9d25d3be135a14826d11a456 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Tue, 16 Jul 2019 13:07:39 -0500 Subject: [PATCH 056/147] Bind error before using. --- Examples/ArcGISToolkitExamples/ARExample.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Examples/ArcGISToolkitExamples/ARExample.swift b/Examples/ArcGISToolkitExamples/ARExample.swift index c1fbf467..f2e33267 100644 --- a/Examples/ArcGISToolkitExamples/ARExample.swift +++ b/Examples/ArcGISToolkitExamples/ARExample.swift @@ -42,8 +42,8 @@ class ARExample: UIViewController { override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) arView.startTracking { (error) in - if error != nil { - print("Error starting ArcGISARView tracking: \(String(describing: error))") + if let error = error { + print("Error starting ArcGISARView tracking: \(error)") } } } From cfe3b78752782c0027a91e9a72f2fbbf310e9995 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Thu, 18 Jul 2019 16:48:37 -0500 Subject: [PATCH 057/147] initialTransformation and arScreenToLocation implementations. --- Toolkit/ArcGISToolkit/AR/ArcGISARView.swift | 80 +++++++++++++++++---- 1 file changed, 65 insertions(+), 15 deletions(-) diff --git a/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift b/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift index 95a39dab..0642df60 100644 --- a/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift +++ b/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift @@ -24,7 +24,11 @@ public class ArcGISARView: UIView { public let arSCNView = ARSCNView(frame: .zero) /// The initial transformation used for a table top experience. Defaults to the Identity Matrix. - public private(set) var initialTransformation = AGSTransformationMatrix.identity + public var initialTransformation: AGSTransformationMatrix { + get { + return _initialTransformation + } + } /// Denotes whether tracking location and angles has started. public private(set) var isTracking: Bool = false @@ -45,7 +49,9 @@ public class ArcGISARView: UIView { guard let newCamera = originCamera else { return } // Set the camera as the originCamera on the cameraController and reset tracking. cameraController.originCamera = newCamera - resetTracking() + if isTracking { + resetTracking() + } } } @@ -76,7 +82,7 @@ public class ArcGISARView: UIView { /// We implement `ARSCNViewDelegate` methods, but will use `arSCNViewDelegate` to forward them to clients. weak public var arSCNViewDelegate: ARSCNViewDelegate? - + // MARK: Private properties /// The camera controller used to control the Scene. @@ -90,11 +96,14 @@ public class ArcGISARView: UIView { /// A quaternion used to compensate for the pitch being 90 degrees on `ARKit`; used to calculate the current device transformation for each frame. private let compensationQuat: simd_quatd = simd_quatd(ix: (sin(45 / (180 / .pi))), iy: 0, iz: 0, r: (cos(45 / (180 / .pi)))) - + /// Whether `ARKit` is supported on this device. private let deviceSupportsARKit: Bool = { return ARWorldTrackingConfiguration.isSupported }() + + /// Internal readwrite initialTransformation property + private var _initialTransformation = AGSTransformationMatrix.identity // MARK: Initializers @@ -165,10 +174,18 @@ public class ArcGISARView: UIView { /// /// - Parameter toLocation: The point in screen coordinates. /// - Returns: The map point corresponding to screenPoint. - public func arScreenToLocation(screenPoint: CGPoint) -> AGSPoint { - fatalError("arScreen(toLocation:) has not been implemented") + public func arScreenToLocation(screenPoint: CGPoint) -> AGSPoint? { + // Use the `internalHitTest` method to get the matrix of `screenPoint`. + guard let matrix = internalHitTest(screenPoint: screenPoint) else { return nil } + + // Get the TransformationMatrix from the sceneView.currentViewpointCamera and add the hit test matrix to it. + let currentCamera = sceneView.currentViewpointCamera() + let transformationMatrix = currentCamera.transformationMatrix.addTransformation(matrix) + + // Create a camera from transformationMatrix and return it's location. + return AGSCamera(transformationMatrix: transformationMatrix).location } - + /// Resets the device tracking, using `originCamera` if it's not nil or the device's GPS location via the location data source. public func resetTracking() { initialLocation = nil @@ -180,17 +197,24 @@ public class ArcGISARView: UIView { /// - Parameter initialTransformation: The initial transformation for originCamera offset. /// - Returns: Whether setting the `initialTransformation` succeeded or failed. public func setInitialTransformation(initialTransformation: AGSTransformationMatrix) -> Bool { - fatalError("setInitialTransformation(initialTransformation:) has not been implemented") + _initialTransformation = initialTransformation + return true } /// Sets the initial transformation used to offset the originCamera. The initial transformation is based on an AR point determined via existing plan hit detection from `screenPoint`. If an AR point cannot be determined, this method will return `false`. /// /// - Parameter screenPoint: The screen point to determine the `initialTransformation` from. /// - Returns: Whether setting the `initialTransformation` succeeded or failed. - public func setInitialTransformation(screenPoint: CGPoint) -> Bool { - fatalError("setInitialTransformationPlacingOriginOnPlane(screenPoint:) has not been implemented") - } + public func setInitialTransformation(_ screenPoint: CGPoint) -> Bool { + // Use the `internalHitTest` method to get the matrix of `screenPoint`. + guard let matrix = internalHitTest(screenPoint: screenPoint) else { return false } + + // Set the `initialTransformation` as the AGSTransformationMatrix.identity - hit test matrix. + _initialTransformation = AGSTransformationMatrix.identity.subtractTransformation(matrix) + return true + } + /// Starts device tracking. public func startTracking(_ completion: ((_ error: Error?) -> Void)? = nil) { // We have a location data source that needs to be started. @@ -245,14 +269,40 @@ public class ArcGISARView: UIView { subview.bottomAnchor.constraint(equalTo: self.bottomAnchor) ]) } + + /// Internal method to perform a hit test operation to get the transformation matrix representing the corresponding real-world point for `screenPoint`. + /// + /// - Parameter screenPoint: screenPoint: The screen point to determine the real world transformation matrix from. + /// - Returns: An `AGSTransformationMatrix` representing the real-world point corresponding to `screenPoint`. + fileprivate func internalHitTest(screenPoint: CGPoint) -> AGSTransformationMatrix? { + // Use the `hitTest` method on ARSCNView to get the location of `screenPoint`. + let results = arSCNView.hitTest(screenPoint, types: [.existingPlane, .estimatedHorizontalPlane]) + + // Get the worldTransform from the first result; if there's no worldTransform, return nil. + guard let worldTransform = results.first?.worldTransform else { return nil } + + // Create our hit test matrix based on the worldTransform location. + let hitTestMatrix = AGSTransformationMatrix(quaternionX: 0, + quaternionY: 0, + quaternionZ: 0.0, + quaternionW: 1.0, + translationX: Double(worldTransform.columns.3.x), + translationY: Double(-worldTransform.columns.3.z), + translationZ: Double(worldTransform.columns.3.y)) + + return hitTestMatrix + } } // MARK: - ARSCNViewDelegate extension ArcGISARView: ARSCNViewDelegate { - public func renderer(_ renderer: SCNSceneRenderer, nodeFor anchor: ARAnchor) -> SCNNode? { - return arSCNViewDelegate?.renderer?(renderer, nodeFor: anchor) - } + // This is not implemnted as we are letting ARKit create and manage nodes. + // If you want to manage your own nodes, uncomment this and implement it in your code. +// public func renderer(_ renderer: SCNSceneRenderer, nodeFor anchor: ARAnchor) -> SCNNode? { +// return arSCNViewDelegate?.renderer?(renderer, nodeFor: anchor) +// } + public func renderer(_ renderer: SCNSceneRenderer, didAdd node: SCNNode, for anchor: ARAnchor) { arSCNViewDelegate?.renderer?(renderer, didAdd: node, for: anchor) } @@ -337,7 +387,7 @@ extension ArcGISARView: SCNSceneRendererDelegate { translationZ: cameraTransform.columns.3.y) // Set the matrix on the camera controller. - cameraController.transformationMatrix = transformationMatrix + cameraController.transformationMatrix = initialTransformation.addTransformation(transformationMatrix) // Set FOV on camera. if let camera = arSCNView.session.currentFrame?.camera { From 00663867ebfa6debfab85edbfa46c65d92e6f38a Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Thu, 18 Jul 2019 16:58:49 -0500 Subject: [PATCH 058/147] Use 0.0 and fix typo. --- Toolkit/ArcGISToolkit/AR/ArcGISARView.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift b/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift index 0642df60..7b68102f 100644 --- a/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift +++ b/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift @@ -282,8 +282,8 @@ public class ArcGISARView: UIView { guard let worldTransform = results.first?.worldTransform else { return nil } // Create our hit test matrix based on the worldTransform location. - let hitTestMatrix = AGSTransformationMatrix(quaternionX: 0, - quaternionY: 0, + let hitTestMatrix = AGSTransformationMatrix(quaternionX: 0.0, + quaternionY: 0.0, quaternionZ: 0.0, quaternionW: 1.0, translationX: Double(worldTransform.columns.3.x), @@ -297,7 +297,7 @@ public class ArcGISARView: UIView { // MARK: - ARSCNViewDelegate extension ArcGISARView: ARSCNViewDelegate { - // This is not implemnted as we are letting ARKit create and manage nodes. + // This is not implemented as we are letting ARKit create and manage nodes. // If you want to manage your own nodes, uncomment this and implement it in your code. // public func renderer(_ renderer: SCNSceneRenderer, nodeFor anchor: ARAnchor) -> SCNNode? { // return arSCNViewDelegate?.renderer?(renderer, nodeFor: anchor) From 7b451a75bfc06eb3bf44fe13d28b52b2b56fc559 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Fri, 19 Jul 2019 10:44:49 -0500 Subject: [PATCH 059/147] Update Toolkit/ArcGISToolkit/AR/ArcGISARView.swift Co-Authored-By: Philip Ridgeway --- Toolkit/ArcGISToolkit/AR/ArcGISARView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift b/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift index 7b68102f..ec7b9373 100644 --- a/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift +++ b/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift @@ -196,7 +196,7 @@ public class ArcGISARView: UIView { /// /// - Parameter initialTransformation: The initial transformation for originCamera offset. /// - Returns: Whether setting the `initialTransformation` succeeded or failed. - public func setInitialTransformation(initialTransformation: AGSTransformationMatrix) -> Bool { + public func setInitialTransformation(_ initialTransformation: AGSTransformationMatrix) -> Bool { _initialTransformation = initialTransformation return true } From 960e83d48ac835610df079823f83b09229927e2c Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Fri, 19 Jul 2019 10:45:14 -0500 Subject: [PATCH 060/147] Update Toolkit/ArcGISToolkit/AR/ArcGISARView.swift Co-Authored-By: Philip Ridgeway --- Toolkit/ArcGISToolkit/AR/ArcGISARView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift b/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift index ec7b9373..d77807c1 100644 --- a/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift +++ b/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift @@ -201,7 +201,7 @@ public class ArcGISARView: UIView { return true } - /// Sets the initial transformation used to offset the originCamera. The initial transformation is based on an AR point determined via existing plan hit detection from `screenPoint`. If an AR point cannot be determined, this method will return `false`. + /// Sets the initial transformation used to offset the originCamera. The initial transformation is based on an AR point determined via existing plane hit detection from `screenPoint`. If an AR point cannot be determined, this method will return `false`. /// /// - Parameter screenPoint: The screen point to determine the `initialTransformation` from. /// - Returns: Whether setting the `initialTransformation` succeeded or failed. From 4a673e50e28e86de3b3ebce10de81d2132c81d49 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Fri, 19 Jul 2019 10:46:16 -0500 Subject: [PATCH 061/147] Update Toolkit/ArcGISToolkit/AR/ArcGISARView.swift Co-Authored-By: Philip Ridgeway --- Toolkit/ArcGISToolkit/AR/ArcGISARView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift b/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift index d77807c1..ecf48ff4 100644 --- a/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift +++ b/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift @@ -205,7 +205,7 @@ public class ArcGISARView: UIView { /// /// - Parameter screenPoint: The screen point to determine the `initialTransformation` from. /// - Returns: Whether setting the `initialTransformation` succeeded or failed. - public func setInitialTransformation(_ screenPoint: CGPoint) -> Bool { + public func setInitialTransformation(using screenPoint: CGPoint) -> Bool { // Use the `internalHitTest` method to get the matrix of `screenPoint`. guard let matrix = internalHitTest(screenPoint: screenPoint) else { return false } From e92e2b7d220aac539d8b064bff3c51ddb9b529a4 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Fri, 19 Jul 2019 10:47:01 -0500 Subject: [PATCH 062/147] Update Toolkit/ArcGISToolkit/AR/ArcGISARView.swift Co-Authored-By: Philip Ridgeway --- Toolkit/ArcGISToolkit/AR/ArcGISARView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift b/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift index ecf48ff4..31cdca3e 100644 --- a/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift +++ b/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift @@ -172,7 +172,7 @@ public class ArcGISARView: UIView { /// Determines the map point for the given screen point. /// - /// - Parameter toLocation: The point in screen coordinates. + /// - Parameter screenPoint: The point in screen coordinates. /// - Returns: The map point corresponding to screenPoint. public func arScreenToLocation(screenPoint: CGPoint) -> AGSPoint? { // Use the `internalHitTest` method to get the matrix of `screenPoint`. From cdb4fc624c724fb1aa69d6b43732f63b7ce36626 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Fri, 19 Jul 2019 10:47:50 -0500 Subject: [PATCH 063/147] Update Toolkit/ArcGISToolkit/AR/ArcGISARView.swift Co-Authored-By: Philip Ridgeway --- Toolkit/ArcGISToolkit/AR/ArcGISARView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift b/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift index 31cdca3e..6fc4f628 100644 --- a/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift +++ b/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift @@ -272,7 +272,7 @@ public class ArcGISARView: UIView { /// Internal method to perform a hit test operation to get the transformation matrix representing the corresponding real-world point for `screenPoint`. /// - /// - Parameter screenPoint: screenPoint: The screen point to determine the real world transformation matrix from. + /// - Parameter screenPoint: The screen point to determine the real world transformation matrix from. /// - Returns: An `AGSTransformationMatrix` representing the real-world point corresponding to `screenPoint`. fileprivate func internalHitTest(screenPoint: CGPoint) -> AGSTransformationMatrix? { // Use the `hitTest` method on ARSCNView to get the location of `screenPoint`. From 54525c8d815442a55b96a5c84df4031e9a6cebbe Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Mon, 22 Jul 2019 11:05:16 -0500 Subject: [PATCH 064/147] Fix setInitialTransformation and use of private property. --- Toolkit/ArcGISToolkit/AR/ArcGISARView.swift | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift b/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift index 6fc4f628..2e7b3633 100644 --- a/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift +++ b/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift @@ -24,11 +24,7 @@ public class ArcGISARView: UIView { public let arSCNView = ARSCNView(frame: .zero) /// The initial transformation used for a table top experience. Defaults to the Identity Matrix. - public var initialTransformation: AGSTransformationMatrix { - get { - return _initialTransformation - } - } + public var initialTransformation: AGSTransformationMatrix = AGSTransformationMatrix.identity /// Denotes whether tracking location and angles has started. public private(set) var isTracking: Bool = false @@ -101,9 +97,6 @@ public class ArcGISARView: UIView { private let deviceSupportsARKit: Bool = { return ARWorldTrackingConfiguration.isSupported }() - - /// Internal readwrite initialTransformation property - private var _initialTransformation = AGSTransformationMatrix.identity // MARK: Initializers @@ -197,7 +190,7 @@ public class ArcGISARView: UIView { /// - Parameter initialTransformation: The initial transformation for originCamera offset. /// - Returns: Whether setting the `initialTransformation` succeeded or failed. public func setInitialTransformation(_ initialTransformation: AGSTransformationMatrix) -> Bool { - _initialTransformation = initialTransformation + self.initialTransformation = initialTransformation return true } @@ -210,7 +203,7 @@ public class ArcGISARView: UIView { guard let matrix = internalHitTest(screenPoint: screenPoint) else { return false } // Set the `initialTransformation` as the AGSTransformationMatrix.identity - hit test matrix. - _initialTransformation = AGSTransformationMatrix.identity.subtractTransformation(matrix) + initialTransformation = AGSTransformationMatrix.identity.subtractTransformation(matrix) return true } From 6125ab65a608f00ba5785a29112e8039a0627a33 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Mon, 22 Jul 2019 11:37:47 -0500 Subject: [PATCH 065/147] Add @discardableResult for setInitialTransformation method. --- Toolkit/ArcGISToolkit/AR/ArcGISARView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift b/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift index 2e7b3633..34a665b9 100644 --- a/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift +++ b/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift @@ -189,7 +189,7 @@ public class ArcGISARView: UIView { /// /// - Parameter initialTransformation: The initial transformation for originCamera offset. /// - Returns: Whether setting the `initialTransformation` succeeded or failed. - public func setInitialTransformation(_ initialTransformation: AGSTransformationMatrix) -> Bool { + @discardableResult public func setInitialTransformation(_ initialTransformation: AGSTransformationMatrix) -> Bool { self.initialTransformation = initialTransformation return true } From 50ef2262081ac4b15d2fd572167f2652d04734d2 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Mon, 22 Jul 2019 13:33:44 -0500 Subject: [PATCH 066/147] Update Toolkit/ArcGISToolkit/AR/ArcGISARView.swift Make private(set) & remove duplicate type Co-Authored-By: Philip Ridgeway --- Toolkit/ArcGISToolkit/AR/ArcGISARView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift b/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift index 34a665b9..991fce04 100644 --- a/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift +++ b/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift @@ -24,7 +24,7 @@ public class ArcGISARView: UIView { public let arSCNView = ARSCNView(frame: .zero) /// The initial transformation used for a table top experience. Defaults to the Identity Matrix. - public var initialTransformation: AGSTransformationMatrix = AGSTransformationMatrix.identity + public private(set) var initialTransformation: AGSTransformationMatrix = .identity /// Denotes whether tracking location and angles has started. public private(set) var isTracking: Bool = false From 81ac80c45ac30caee1f164ceae895168ae6d2f9b Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Mon, 22 Jul 2019 15:22:06 -0500 Subject: [PATCH 067/147] Add ability to change scenes and ArcGISARView settings. --- .../project.pbxproj | 16 ++ .../ArcGISToolkitExamples/ARExample.swift | 230 +++++++++++++++++- .../Misc/OptionsTableViewController.swift | 86 +++++++ .../ArcGISToolkitExamples/Misc/Plane.swift | 47 ++++ 4 files changed, 369 insertions(+), 10 deletions(-) create mode 100644 Examples/ArcGISToolkitExamples/Misc/OptionsTableViewController.swift create mode 100644 Examples/ArcGISToolkitExamples/Misc/Plane.swift diff --git a/Examples/ArcGISToolkitExamples.xcodeproj/project.pbxproj b/Examples/ArcGISToolkitExamples.xcodeproj/project.pbxproj index b16c7d07..40e6c136 100644 --- a/Examples/ArcGISToolkitExamples.xcodeproj/project.pbxproj +++ b/Examples/ArcGISToolkitExamples.xcodeproj/project.pbxproj @@ -24,6 +24,8 @@ 88B689D11E96EDF400B67FAB /* VCListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88B689C71E96EDF400B67FAB /* VCListViewController.swift */; }; 88DBC2A11FE83D6000255921 /* JobManagerExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88DBC2A01FE83D6000255921 /* JobManagerExample.swift */; }; E447A12B2267BB9500578C0B /* ARExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = E447A12A2267BB9500578C0B /* ARExample.swift */; }; + E464AA9122E62DC600969DBA /* Plane.swift in Sources */ = {isa = PBXBuildFile; fileRef = E464AA9022E62DC600969DBA /* Plane.swift */; }; + E464AA9322E633C500969DBA /* OptionsTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E464AA9222E633C500969DBA /* OptionsTableViewController.swift */; }; E46893271FEDAE29008ADA79 /* CompassExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = E46893261FEDAE29008ADA79 /* CompassExample.swift */; }; E48405751E9BE7E600927208 /* LegendExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = E48405741E9BE7E600927208 /* LegendExample.swift */; }; /* End PBXBuildFile section */ @@ -85,6 +87,8 @@ 88B689C71E96EDF400B67FAB /* VCListViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VCListViewController.swift; sourceTree = ""; }; 88DBC2A01FE83D6000255921 /* JobManagerExample.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JobManagerExample.swift; sourceTree = ""; }; E447A12A2267BB9500578C0B /* ARExample.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ARExample.swift; sourceTree = ""; }; + E464AA9022E62DC600969DBA /* Plane.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Plane.swift; sourceTree = ""; }; + E464AA9222E633C500969DBA /* OptionsTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OptionsTableViewController.swift; sourceTree = ""; }; E46893261FEDAE29008ADA79 /* CompassExample.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompassExample.swift; sourceTree = ""; }; E48405741E9BE7E600927208 /* LegendExample.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LegendExample.swift; sourceTree = ""; }; /* End PBXFileReference section */ @@ -123,6 +127,7 @@ 8839043D1DF6022A001F3188 /* ArcGISToolkitExamples */ = { isa = PBXGroup; children = ( + E464AA8F22E62D5B00969DBA /* Misc */, 883904421DF6022A001F3188 /* Main.storyboard */, 883904451DF6022A001F3188 /* Assets.xcassets */, 883904471DF6022A001F3188 /* LaunchScreen.storyboard */, @@ -159,6 +164,15 @@ name = Examples; sourceTree = ""; }; + E464AA8F22E62D5B00969DBA /* Misc */ = { + isa = PBXGroup; + children = ( + E464AA9222E633C500969DBA /* OptionsTableViewController.swift */, + E464AA9022E62DC600969DBA /* Plane.swift */, + ); + path = Misc; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -276,6 +290,8 @@ 883EA74B20741A56006D6F72 /* PopupExample.swift in Sources */, 88B689C81E96EDF400B67FAB /* AppDelegate.swift in Sources */, 2140781E209B629000FBFDCC /* TimeSliderExample.swift in Sources */, + E464AA9122E62DC600969DBA /* Plane.swift in Sources */, + E464AA9322E633C500969DBA /* OptionsTableViewController.swift in Sources */, 88B689CB1E96EDF400B67FAB /* MeasureExample.swift in Sources */, 88DBC2A11FE83D6000255921 /* JobManagerExample.swift in Sources */, 88B689D11E96EDF400B67FAB /* VCListViewController.swift in Sources */, diff --git a/Examples/ArcGISToolkitExamples/ARExample.swift b/Examples/ArcGISToolkitExamples/ARExample.swift index f2e33267..a9c9a9bd 100644 --- a/Examples/ArcGISToolkitExamples/ARExample.swift +++ b/Examples/ArcGISToolkitExamples/ARExample.swift @@ -18,14 +18,30 @@ import ArcGIS class ARExample: UIViewController { + typealias sceneInitFunction = () -> AGSScene + + /// The scene creation functions plus labels. The functions create a new scene and perform any necessary `ArcGISARView` initialization. This allows for changing the scene and AR "mode" (table top or full-scale). + private var sceneInfo: [(sceneFunction: sceneInitFunction, label: String)] = [] + + /// The current scene info. + private var currentSceneInfo: (sceneFunction: sceneInitFunction, label: String)? + + /// The `ArcGISARView` that displays the camera feed and handles ARKit functionality. let arView = ArcGISARView(renderVideoFeed: true, tryUsingARKit: true) - + + /// Denotes whether we've performed a hit test yet. + var didHitTest: Bool = false + override func viewDidLoad() { super.viewDidLoad() // Set ourself as delegate so we can get ARSCNViewDelegate method calls. arView.arSCNViewDelegate = self + // Set ourself as touch delegate so we can get touch events. + arView.sceneView.touchDelegate = self + + // Add arView to the view and setup the constraints. view.addSubview(arView) arView.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ @@ -35,8 +51,32 @@ class ARExample: UIViewController { arView.bottomAnchor.constraint(equalTo: view.bottomAnchor) ]) - arView.sceneView.scene = makeStreetsScene() - arView.locationDataSource = AGSCLLocationDataSource() + + // Create a toolbar and add it to the arView. + let toolbar = UIToolbar(frame: .zero) + arView.addSubview(toolbar) + toolbar.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([ + toolbar.leadingAnchor.constraint(equalTo: arView.sceneView.leadingAnchor), + toolbar.trailingAnchor.constraint(equalTo: arView.sceneView.trailingAnchor), + toolbar.bottomAnchor.constraint(equalTo: arView.sceneView.attributionTopAnchor) + ]) + + // Add a toolbar button to change the current scene. + let sceneItem = UIBarButtonItem(title: "Change Scene", style: .plain, target: self, action: #selector(changeScene(_:))) + toolbar.setItems([UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil), + sceneItem, + UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil)], animated: false) + + // Set up the `sceneInfo` array with our scene init functions and labels. + sceneInfo.append(contentsOf: [(sceneFunction: streetsScene, label: "Streets"), + (sceneFunction: everestScene, label: "Everest"), + (sceneFunction: broncosStadiumScene, label: "Broncos Stadium"), + (sceneFunction: emptyScene, label: "Empty")]) + + // Use the first sceneInfo to create and set the scene. + currentSceneInfo = sceneInfo.first + arView.sceneView.scene = currentSceneInfo?.sceneFunction() } override func viewDidAppear(_ animated: Bool) { @@ -52,13 +92,125 @@ class ARExample: UIViewController { super.viewDidDisappear(animated) arView.stopTracking() } - - private func makeStreetsScene() -> AGSScene { + + /// Changes the scene to a newly selected scene. + /// + /// - Parameter sender: The bar button item tapped on. + @objc func changeScene(_ sender: UIBarButtonItem){ + guard let label = currentSceneInfo?.label, + // Get the index of the scene currently shown in the sceneView. + let selectedIndex = sceneInfo.firstIndex(where: { $0.label == label }) else { + return + } + + // Create the array of labels for the options table view controller. + let sceneLabels = sceneInfo.map { $0.label } - // create scene with the streets basemap + // A view controller allowing the user to select the scene to show. + // Note: the `OptionsTableViewController` is copied from the "ArcGIS Runtime SDK for iOS Samples" code, found here: https://github.com/Esri/arcgis-runtime-samples-ios + let controller = OptionsTableViewController(labels: sceneLabels, selectedIndex: selectedIndex) { [weak self] (newIndex) in + if let self = self { + // Dismiss the popover. + self.dismiss(animated: true, completion: nil) + + // Set currentSceneInfo to the selected scene. + self.currentSceneInfo = self.sceneInfo[newIndex] + + // Stop tracking, update the scene with the selected Scene and start tracking again. + self.arView.stopTracking() + self.arView.sceneView.scene = self.sceneInfo[newIndex].sceneFunction() + self.arView.startTracking() + } + } + + // Configure the options controller as a popover. + controller.modalPresentationStyle = .popover + controller.presentationController?.delegate = self + controller.preferredContentSize = CGSize(width: 300, height: 300) + controller.popoverPresentationController?.barButtonItem = sender + controller.popoverPresentationController?.passthroughViews?.append(arView) + + // Show the popover. + present(controller, animated: true) + } + + // MARK: Scene Init Functions + + /// Creates a scene based on the Streets base map. + /// + /// - Returns: The new scene. + private func streetsScene() -> AGSScene { + + // Create scene with the streets basemap. let scene = AGSScene(basemapType: .streets) + addElevationSource(toScene: scene) + + // Set the location data source so we use our GPS location as the originCamera. + arView.locationDataSource = AGSCLLocationDataSource() + return scene + } + + /// Creates a scene based on the Mount Everest web scene. + /// + /// - Returns: The new scene. + private func everestScene() -> AGSScene { + // Create scene using the Everest web scene. + let portal = AGSPortal.arcGISOnline(withLoginRequired: false) + let portalItem = AGSPortalItem(portal: portal, itemID: "27f76008eeb04765b8a94d998aaa46c7") + let scene = AGSScene(item: portalItem) + + // Set camera to Everest summit. + arView.originCamera = AGSCamera(latitude: 27.988153, longitude: 86.925174, altitude: 8868.069399, heading: 159.56, pitch: 0.00, roll: 0.00) + arView.translationFactor = 1000 + + // Clear the location data source, as we're setting the originCamera directly. + arView.locationDataSource = nil + return scene + } + + /// Creates a scene based on the Broncos stadium web scene. + /// + /// - Returns: The new scene. + private func broncosStadiumScene() -> AGSScene { + // Create scene using WebScene of the Broncos stadium + let portal = AGSPortal.arcGISOnline(withLoginRequired: false) + let portalItem = AGSPortalItem(portal: portal, itemID: "72460f2c5b4048339433afedcb2369e1") + let scene = AGSScene(item: portalItem) - // create elevation surface + scene.load { [weak self] (error) in + if let error = error { + print("Error loading scene: \(error)") + return + } + // Set the originCamera to be the initial viewpoint of the web scene. + self?.arView.originCamera = scene.initialViewpoint.camera + self?.arView.translationFactor = 1000 + } + + // Turn off background grid. + scene.baseSurface?.backgroundGrid.isVisible = false + + // Clear the location data source, as we're setting the originCamera directly. + arView.locationDataSource = nil + return scene + } + + /// Creates an empty scene with an elevation source. + /// + /// - Returns: The new scene. + private func emptyScene() -> AGSScene { + let scene = AGSScene() + addElevationSource(toScene: scene) + + // Set the location data source so we use our GPS location as the originCamera. + arView.locationDataSource = AGSCLLocationDataSource() + return scene + } + + /// Adds an elevation source to the given `scene`. + /// + /// - Parameter scene: The scene to add the elevation source to. + private func addElevationSource(toScene scene: AGSScene) { let elevationSource = AGSArcGISTiledElevationSource(url: URL(string: "https://elevation3d.arcgis.com/arcgis/rest/services/WorldElevation3D/Terrain3D/ImageServer")!) let surface = AGSSurface() surface.elevationSources = [elevationSource] @@ -66,13 +218,37 @@ class ARExample: UIViewController { surface.isEnabled = true surface.backgroundGrid.isVisible = false scene.baseSurface = surface - - return scene } } extension ARExample: ARSCNViewDelegate { - + + func renderer(_ renderer: SCNSceneRenderer, didAdd node: SCNNode, for anchor: ARAnchor) { + // Place content only for anchors found by plane detection. + guard let planeAnchor = anchor as? ARPlaneAnchor else { return } + + // Create a custom object to visualize the plane geometry and extent. + let plane = Plane(anchor: planeAnchor, in: arView.arSCNView) + + // Add the visualization to the ARKit-managed node so that it tracks + // changes in the plane anchor as plane estimation continues. + node.addChildNode(plane) + } + + func renderer(_ renderer: SCNSceneRenderer, didUpdate node: SCNNode, for anchor: ARAnchor) { + // Update only anchors and nodes set up by `renderer(_:didAdd:for:)`. + guard let planeAnchor = anchor as? ARPlaneAnchor, + let plane = node.childNodes.first as? Plane + else { return } + + // Update extent visualization to the anchor's new bounding rectangle. + if let extentGeometry = plane.extentNode.geometry as? SCNPlane { + extentGeometry.width = CGFloat(planeAnchor.extent.x) + extentGeometry.height = CGFloat(planeAnchor.extent.z) + plane.extentNode.simdPosition = planeAnchor.center + } + } + func session(_ session: ARSession, didFailWithError error: Error) { guard error is ARError else { return } @@ -98,3 +274,37 @@ extension ARExample: ARSCNViewDelegate { } } } + +extension ARExample: AGSGeoViewTouchDelegate { + public func geoView(_ geoView: AGSGeoView, didTapAtScreenPoint screenPoint: CGPoint, mapPoint: AGSPoint) { + guard !didHitTest else { return } + + if arView.setInitialTransformation(screenPoint: screenPoint) { + didHitTest = true + } + + // guard let point = arView.arScreenToLocation(screenPoint: screenPoint) else { return } + // + // let newpoint = AGSPointMake3D(point.x, point.y, 0, 0, point.spatialReference) + // let sym = AGSModelSceneSymbol(name:"Bristol", extension: "dae", scale: 50.0) + // sym.load { (error) in + // print("error loading sym: \(String(describing: error))") + // } + // let graphic = AGSGraphic(geometry: newpoint, symbol: sym, attributes: nil) + // graphicsOverlay.graphics.add(graphic) + // print("mapPoint: \(mapPoint)") + // print("point: \(point)") + + // guard let initialCamera = initialCamera else { print("No initial camera"); return } + // var initialTransformation = arView.initialTransformation + // initialTransformation = initialTransformation.subtractTransformation(initialCamera.transformationMatrix) + // let _ = arView.setInitialTransformation(initialTransformation: initialTransformation) + } +} + +extension ARExample: UIAdaptivePresentationControllerDelegate { + func adaptivePresentationStyle(for controller: UIPresentationController, traitCollection: UITraitCollection) -> UIModalPresentationStyle { + // show presented controller as popovers even on small displays + return .none + } +} diff --git a/Examples/ArcGISToolkitExamples/Misc/OptionsTableViewController.swift b/Examples/ArcGISToolkitExamples/Misc/OptionsTableViewController.swift new file mode 100644 index 00000000..96d5b757 --- /dev/null +++ b/Examples/ArcGISToolkitExamples/Misc/OptionsTableViewController.swift @@ -0,0 +1,86 @@ +// Copyright 2018 Esri. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import UIKit + +/// A basic interface for selecting one options from a list, +/// showing the checkmark accessory for the selected cell. +class OptionsTableViewController: UITableViewController { + struct Option { + let label: String + let image: UIImage? + + init(label: String, image: UIImage? = nil) { + self.label = label + self.image = image + } + } + + private let options: [Option] + private var selectedIndex: Int + private let onChange: (Int) -> Void + + convenience init(labels: [String], selectedIndex: Int, onChange: @escaping (Int) -> Void) { + let options = labels.map { Option(label: $0) } + self.init(options: options, selectedIndex: selectedIndex, onChange: onChange) + } + + init(options: [Option], selectedIndex: Int, onChange: @escaping (Int) -> Void) { + self.options = options + self.selectedIndex = selectedIndex + self.onChange = onChange + super.init(nibName: nil, bundle: nil) + } + + @available(*, unavailable) + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidLoad() { + super.viewDidLoad() + tableView.register(OptionCell.self, forCellReuseIdentifier: "OptionCell") + } + + // UITableViewDataSource + + override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + return options.count + } + + override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let cell = tableView.dequeueReusableCell(withIdentifier: "OptionCell", for: indexPath) + cell.selectionStyle = .none + let option = options[indexPath.row] + cell.textLabel?.text = option.label + cell.imageView?.image = option.image + if selectedIndex == indexPath.row { + tableView.selectRow(at: indexPath, animated: false, scrollPosition: .none) + } + return cell + } + + // UITableViewDelegate + + override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + selectedIndex = indexPath.row + onChange(indexPath.row) + } +} + +private class OptionCell: UITableViewCell { + override func setSelected(_ selected: Bool, animated: Bool) { + accessoryType = selected ? .checkmark : .none + } +} diff --git a/Examples/ArcGISToolkitExamples/Misc/Plane.swift b/Examples/ArcGISToolkitExamples/Misc/Plane.swift new file mode 100644 index 00000000..540ed742 --- /dev/null +++ b/Examples/ArcGISToolkitExamples/Misc/Plane.swift @@ -0,0 +1,47 @@ +/* +See LICENSE folder for this sample’s licensing information. + +Abstract: +Convenience class for visualizing Plane extent and geometry +*/ + +import ARKit + +class Plane: SCNNode { + + let extentNode: SCNNode + var classificationNode: SCNNode? + + /// - Tag: VisualizePlane + init(anchor: ARPlaneAnchor, in sceneView: ARSCNView) { + // Create a node to visualize the plane's bounding rectangle. + let extentPlane: SCNPlane = SCNPlane(width: CGFloat(anchor.extent.x), height: CGFloat(anchor.extent.z)) + extentNode = SCNNode(geometry: extentPlane) + extentNode.simdPosition = anchor.center + + // `SCNPlane` is vertically oriented in its local coordinate space, so + // rotate it to match the orientation of `ARPlaneAnchor`. + extentNode.eulerAngles.x = -.pi / 2 + + super.init() + + self.setupExtentVisualStyle() + + // Add the plane extent as child node so they appear in the scene. + addChildNode(extentNode) + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func setupExtentVisualStyle() { + // Make the extent visualization semitransparent to clearly show real-world placement. + extentNode.opacity = 0.6 + + guard let material = extentNode.geometry?.firstMaterial + else { fatalError("SCNPlane always has one material") } + + material.diffuse.contents = UIColor.blue + } +} From 284be53bab5ca69646cf0031d32818c4aefeb9cc Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Mon, 22 Jul 2019 15:28:04 -0500 Subject: [PATCH 068/147] Initial StatusViewController --- .../ArcGISToolkitExamples/ARExample.swift | 36 +++++++++++++++++-- 1 file changed, 34 insertions(+), 2 deletions(-) diff --git a/Examples/ArcGISToolkitExamples/ARExample.swift b/Examples/ArcGISToolkitExamples/ARExample.swift index a9c9a9bd..333ece31 100644 --- a/Examples/ArcGISToolkitExamples/ARExample.swift +++ b/Examples/ArcGISToolkitExamples/ARExample.swift @@ -31,7 +31,15 @@ class ARExample: UIViewController { /// Denotes whether we've performed a hit test yet. var didHitTest: Bool = false - + private let statusViewController: ARStatusViewController? = { + let storyBoard = UIStoryboard(name: "ARStatusViewController", bundle: nil) + let vc = storyBoard.instantiateInitialViewController() as? ARStatusViewController + vc?.modalPresentationStyle = .popover + return vc + }() + /// Used when calculating framerate. + private var lastUpdateTime: TimeInterval = 0 + override func viewDidLoad() { super.viewDidLoad() @@ -133,7 +141,17 @@ class ARExample: UIViewController { // Show the popover. present(controller, animated: true) } - + @objc func showStatus(){ + guard let statusVC = statusViewController else { return } + statusVC.popoverPresentationController?.barButtonItem = navigationItem.rightBarButtonItem + statusVC.preferredContentSize = { + let height: CGFloat = CGFloat(statusVC.tableView.numberOfRows(inSection: 0)) * statusVC.tableView.rowHeight + return CGSize(width: 375, height: height) + }() + + navigationController?.present(statusVC, animated: true, completion: nil) + } + // MARK: Scene Init Functions /// Creates a scene based on the Streets base map. @@ -262,6 +280,9 @@ extension ARExample: ARSCNViewDelegate { // Remove optional error messages. let errorMessage = messages.compactMap({ $0 }).joined(separator: "\n") + // Set the error message on the status vc. + statusViewController?.errorMessage = errorMessage + DispatchQueue.main.async { [weak self] in // Present an alert describing the error. let alertController = UIAlertController(title: "Could not start tracking.", message: errorMessage, preferredStyle: .alert) @@ -273,6 +294,17 @@ extension ARExample: ARSCNViewDelegate { self?.present(alertController, animated: true) } } + func session(_ session: ARSession, cameraDidChangeTrackingState camera: ARCamera) { + // Set the tracking state on the status vc. + statusViewController?.trackingState = camera.trackingState + } + + func renderer(_ renderer: SCNSceneRenderer, willRenderScene scene: SCNScene, atTime time: TimeInterval) { + // Calculate frame rate and set on the statuc vc. + let frametime = time - lastUpdateTime + statusViewController?.frameRate = Int((1.0 / frametime).rounded()) + lastUpdateTime = time + } } extension ARExample: AGSGeoViewTouchDelegate { From 3638e13bad080161a9842e8dfe74c43ea8b97e62 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Mon, 22 Jul 2019 16:25:40 -0500 Subject: [PATCH 069/147] Add StatusViewController with Scene info. --- .../project.pbxproj | 8 + .../ArcGISToolkitExamples/ARExample.swift | 51 ++++-- .../Misc/ARStatusViewController.storyboard | 163 ++++++++++++++++++ .../Misc/ARStatusViewController.swift | 94 ++++++++++ 4 files changed, 302 insertions(+), 14 deletions(-) create mode 100644 Examples/ArcGISToolkitExamples/Misc/ARStatusViewController.storyboard create mode 100644 Examples/ArcGISToolkitExamples/Misc/ARStatusViewController.swift diff --git a/Examples/ArcGISToolkitExamples.xcodeproj/project.pbxproj b/Examples/ArcGISToolkitExamples.xcodeproj/project.pbxproj index 40e6c136..20d400a7 100644 --- a/Examples/ArcGISToolkitExamples.xcodeproj/project.pbxproj +++ b/Examples/ArcGISToolkitExamples.xcodeproj/project.pbxproj @@ -26,6 +26,8 @@ E447A12B2267BB9500578C0B /* ARExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = E447A12A2267BB9500578C0B /* ARExample.swift */; }; E464AA9122E62DC600969DBA /* Plane.swift in Sources */ = {isa = PBXBuildFile; fileRef = E464AA9022E62DC600969DBA /* Plane.swift */; }; E464AA9322E633C500969DBA /* OptionsTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E464AA9222E633C500969DBA /* OptionsTableViewController.swift */; }; + E464AA9922E6542E00969DBA /* ARStatusViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E464AA9722E6542E00969DBA /* ARStatusViewController.swift */; }; + E464AA9A22E6542E00969DBA /* ARStatusViewController.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = E464AA9822E6542E00969DBA /* ARStatusViewController.storyboard */; }; E46893271FEDAE29008ADA79 /* CompassExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = E46893261FEDAE29008ADA79 /* CompassExample.swift */; }; E48405751E9BE7E600927208 /* LegendExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = E48405741E9BE7E600927208 /* LegendExample.swift */; }; /* End PBXBuildFile section */ @@ -89,6 +91,8 @@ E447A12A2267BB9500578C0B /* ARExample.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ARExample.swift; sourceTree = ""; }; E464AA9022E62DC600969DBA /* Plane.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Plane.swift; sourceTree = ""; }; E464AA9222E633C500969DBA /* OptionsTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OptionsTableViewController.swift; sourceTree = ""; }; + E464AA9722E6542E00969DBA /* ARStatusViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ARStatusViewController.swift; sourceTree = ""; }; + E464AA9822E6542E00969DBA /* ARStatusViewController.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = ARStatusViewController.storyboard; sourceTree = ""; }; E46893261FEDAE29008ADA79 /* CompassExample.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompassExample.swift; sourceTree = ""; }; E48405741E9BE7E600927208 /* LegendExample.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LegendExample.swift; sourceTree = ""; }; /* End PBXFileReference section */ @@ -169,6 +173,8 @@ children = ( E464AA9222E633C500969DBA /* OptionsTableViewController.swift */, E464AA9022E62DC600969DBA /* Plane.swift */, + E464AA9822E6542E00969DBA /* ARStatusViewController.storyboard */, + E464AA9722E6542E00969DBA /* ARStatusViewController.swift */, ); path = Misc; sourceTree = ""; @@ -260,6 +266,7 @@ files = ( 883904491DF6022A001F3188 /* LaunchScreen.storyboard in Resources */, 883904461DF6022A001F3188 /* Assets.xcassets in Resources */, + E464AA9A22E6542E00969DBA /* ARStatusViewController.storyboard in Resources */, 883904441DF6022A001F3188 /* Main.storyboard in Resources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -297,6 +304,7 @@ 88B689D11E96EDF400B67FAB /* VCListViewController.swift in Sources */, 8800656E2228577A00F76945 /* TemplatePickerExample.swift in Sources */, 88B689CE1E96EDF400B67FAB /* ScalebarExample.swift in Sources */, + E464AA9922E6542E00969DBA /* ARStatusViewController.swift in Sources */, 88B689C91E96EDF400B67FAB /* ExamplesViewController.swift in Sources */, E447A12B2267BB9500578C0B /* ARExample.swift in Sources */, E48405751E9BE7E600927208 /* LegendExample.swift in Sources */, diff --git a/Examples/ArcGISToolkitExamples/ARExample.swift b/Examples/ArcGISToolkitExamples/ARExample.swift index 333ece31..19a95967 100644 --- a/Examples/ArcGISToolkitExamples/ARExample.swift +++ b/Examples/ArcGISToolkitExamples/ARExample.swift @@ -24,19 +24,27 @@ class ARExample: UIViewController { private var sceneInfo: [(sceneFunction: sceneInitFunction, label: String)] = [] /// The current scene info. - private var currentSceneInfo: (sceneFunction: sceneInitFunction, label: String)? + private var currentSceneInfo: (sceneFunction: sceneInitFunction, label: String)? { + didSet { + guard let label = currentSceneInfo?.label else { return } + statusViewController?.currentScene = label + } + } /// The `ArcGISARView` that displays the camera feed and handles ARKit functionality. - let arView = ArcGISARView(renderVideoFeed: true, tryUsingARKit: true) + private let arView = ArcGISARView(renderVideoFeed: true, tryUsingARKit: true) /// Denotes whether we've performed a hit test yet. - var didHitTest: Bool = false - private let statusViewController: ARStatusViewController? = { + private var didHitTest: Bool = false + + // View controller displaying current status of `ARExample`. + private let statusViewController: ARStatusViewController? = { let storyBoard = UIStoryboard(name: "ARStatusViewController", bundle: nil) let vc = storyBoard.instantiateInitialViewController() as? ARStatusViewController vc?.modalPresentationStyle = .popover return vc }() + /// Used when calculating framerate. private var lastUpdateTime: TimeInterval = 0 @@ -70,11 +78,16 @@ class ARExample: UIViewController { toolbar.bottomAnchor.constraint(equalTo: arView.sceneView.attributionTopAnchor) ]) - // Add a toolbar button to change the current scene. + // Create a toolbar button to change the current scene. let sceneItem = UIBarButtonItem(title: "Change Scene", style: .plain, target: self, action: #selector(changeScene(_:))) + + // Create a toolbar button to display the status. + let statusItem = UIBarButtonItem(title: "Status", style: .plain, target: self, action: #selector(showStatus(_:))) + toolbar.setItems([UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil), sceneItem, - UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil)], animated: false) + UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil), + statusItem], animated: false) // Set up the `sceneInfo` array with our scene init functions and labels. sceneInfo.append(contentsOf: [(sceneFunction: streetsScene, label: "Streets"), @@ -141,15 +154,17 @@ class ARExample: UIViewController { // Show the popover. present(controller, animated: true) } - @objc func showStatus(){ - guard let statusVC = statusViewController else { return } - statusVC.popoverPresentationController?.barButtonItem = navigationItem.rightBarButtonItem - statusVC.preferredContentSize = { - let height: CGFloat = CGFloat(statusVC.tableView.numberOfRows(inSection: 0)) * statusVC.tableView.rowHeight + + /// Dislays the status view controller + @objc func showStatus(_ sender: UIBarButtonItem){ + guard let controller = statusViewController else { return } + controller.popoverPresentationController?.barButtonItem = sender + controller.preferredContentSize = { + let height: CGFloat = CGFloat(controller.tableView.numberOfRows(inSection: 0)) * controller.tableView.rowHeight return CGSize(width: 375, height: height) }() - navigationController?.present(statusVC, animated: true, completion: nil) + present(controller, animated: true) } // MARK: Scene Init Functions @@ -311,8 +326,16 @@ extension ARExample: AGSGeoViewTouchDelegate { public func geoView(_ geoView: AGSGeoView, didTapAtScreenPoint screenPoint: CGPoint, mapPoint: AGSPoint) { guard !didHitTest else { return } - if arView.setInitialTransformation(screenPoint: screenPoint) { - didHitTest = true + if let _ = arView.locationDataSource { + // We have a location data source, so we're in full-scale AR mode. + // Get the real world location for screen point from arView. + guard let point = arView.arScreenToLocation(screenPoint: screenPoint) else { return } + } + else { + // We do not have a location data source, so we're in table-top mode. + if arView.setInitialTransformation(screenPoint: screenPoint) { + didHitTest = true + } } // guard let point = arView.arScreenToLocation(screenPoint: screenPoint) else { return } diff --git a/Examples/ArcGISToolkitExamples/Misc/ARStatusViewController.storyboard b/Examples/ArcGISToolkitExamples/Misc/ARStatusViewController.storyboard new file mode 100644 index 00000000..aa468867 --- /dev/null +++ b/Examples/ArcGISToolkitExamples/Misc/ARStatusViewController.storyboard @@ -0,0 +1,163 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Examples/ArcGISToolkitExamples/Misc/ARStatusViewController.swift b/Examples/ArcGISToolkitExamples/Misc/ARStatusViewController.swift new file mode 100644 index 00000000..f0269201 --- /dev/null +++ b/Examples/ArcGISToolkitExamples/Misc/ARStatusViewController.swift @@ -0,0 +1,94 @@ +// Copyright 2019 Esri. + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 + +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import UIKit +import ARKit + +extension ARCamera.TrackingState { + var description: String { + switch self { + case .normal: + return "Normal" + case .notAvailable: + return "Tracking unavailable" + case .limited(.excessiveMotion): + return "Limited - Excessive Motion" + case .limited(.insufficientFeatures): + return "Limited - Insufficient Features" + case .limited(.initializing): + return "Limited - Initializing" + default: + return "" + } + } +} + +class ARStatusViewController: UITableViewController { + + @IBOutlet var trackingStateLabel: UILabel! + @IBOutlet var frameRateLabel: UILabel! + @IBOutlet var errorDescriptionLabel: UILabel! + @IBOutlet var sceneLabel: UILabel! + + public var trackingState: ARCamera.TrackingState = .notAvailable { + didSet { + guard trackingStateLabel != nil else { return } + DispatchQueue.main.async{ [weak self] in + guard let self = self else { return } + self.trackingStateLabel.text = self.trackingState.description + } + } + } + + public var frameRate: Int = 0 { + didSet { + guard frameRateLabel != nil else { return } + DispatchQueue.main.async{ [weak self] in + guard let self = self else { return } + self.frameRateLabel.text = "\(self.frameRate)" + } + } + } + + public var errorMessage: String = "None" { + didSet { + DispatchQueue.main.async{ [weak self] in + guard let self = self else { return } + self.errorDescriptionLabel.text = self.errorMessage + } + } + } + + public var currentScene: String = "None" { + didSet { + DispatchQueue.main.async{ [weak self] in + guard let self = self else { return } + self.sceneLabel.text = self.currentScene + } + } + } + + override func viewDidLoad() { + super.viewDidLoad() + } + + // MARK: - Table view data source + + override func numberOfSections(in tableView: UITableView) -> Int { + return 1 + } + + override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + return 4 + } +} From 7cf3b64e650a95f138afa7cfb5f1a1f43af1dbdd Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Mon, 22 Jul 2019 16:45:58 -0500 Subject: [PATCH 070/147] Hit test for both full-scale and tabletop AR. --- .../ArcGISToolkitExamples/ARExample.swift | 37 ++++++++++--------- 1 file changed, 19 insertions(+), 18 deletions(-) diff --git a/Examples/ArcGISToolkitExamples/ARExample.swift b/Examples/ArcGISToolkitExamples/ARExample.swift index 19a95967..01b3fa76 100644 --- a/Examples/ArcGISToolkitExamples/ARExample.swift +++ b/Examples/ArcGISToolkitExamples/ARExample.swift @@ -47,6 +47,13 @@ class ARExample: UIViewController { /// Used when calculating framerate. private var lastUpdateTime: TimeInterval = 0 + + /// Overlay used to display user-placed graphics. + private let graphicsOverlay: AGSGraphicsOverlay = { + let overlay = AGSGraphicsOverlay() + let properties = AGSLayerSceneProperties(surfacePlacement: .relative) + return overlay + }() override func viewDidLoad() { super.viewDidLoad() @@ -57,6 +64,9 @@ class ARExample: UIViewController { // Set ourself as touch delegate so we can get touch events. arView.sceneView.touchDelegate = self + // Add our graphics overlay to the sceneView. + arView.sceneView.graphicsOverlays.add(graphicsOverlay) + // Add arView to the view and setup the constraints. view.addSubview(arView) arView.translatesAutoresizingMaskIntoConstraints = false @@ -170,6 +180,7 @@ class ARExample: UIViewController { // MARK: Scene Init Functions /// Creates a scene based on the Streets base map. + /// Mode: Full-Scale AR /// /// - Returns: The new scene. private func streetsScene() -> AGSScene { @@ -184,6 +195,7 @@ class ARExample: UIViewController { } /// Creates a scene based on the Mount Everest web scene. + /// Mode: Tabletop AR /// /// - Returns: The new scene. private func everestScene() -> AGSScene { @@ -202,6 +214,7 @@ class ARExample: UIViewController { } /// Creates a scene based on the Broncos stadium web scene. + /// Mode: Tabletop AR /// /// - Returns: The new scene. private func broncosStadiumScene() -> AGSScene { @@ -229,6 +242,7 @@ class ARExample: UIViewController { } /// Creates an empty scene with an elevation source. + /// Mode: Full-Scale AR /// /// - Returns: The new scene. private func emptyScene() -> AGSScene { @@ -330,30 +344,17 @@ extension ARExample: AGSGeoViewTouchDelegate { // We have a location data source, so we're in full-scale AR mode. // Get the real world location for screen point from arView. guard let point = arView.arScreenToLocation(screenPoint: screenPoint) else { return } + + let sym = AGSSimpleMarkerSceneSymbol(style: .sphere, color: .yellow, height: 1.0, width: 1.0, depth: 1.0, anchorPosition: .bottom) + let graphic = AGSGraphic(geometry: point, symbol: sym, attributes: nil) + graphicsOverlay.graphics.add(graphic) } else { // We do not have a location data source, so we're in table-top mode. - if arView.setInitialTransformation(screenPoint: screenPoint) { + if arView.setInitialTransformation(using: screenPoint) { didHitTest = true } } - - // guard let point = arView.arScreenToLocation(screenPoint: screenPoint) else { return } - // - // let newpoint = AGSPointMake3D(point.x, point.y, 0, 0, point.spatialReference) - // let sym = AGSModelSceneSymbol(name:"Bristol", extension: "dae", scale: 50.0) - // sym.load { (error) in - // print("error loading sym: \(String(describing: error))") - // } - // let graphic = AGSGraphic(geometry: newpoint, symbol: sym, attributes: nil) - // graphicsOverlay.graphics.add(graphic) - // print("mapPoint: \(mapPoint)") - // print("point: \(point)") - - // guard let initialCamera = initialCamera else { print("No initial camera"); return } - // var initialTransformation = arView.initialTransformation - // initialTransformation = initialTransformation.subtractTransformation(initialCamera.transformationMatrix) - // let _ = arView.setInitialTransformation(initialTransformation: initialTransformation) } } From 3c859b38a2ab02bf06508946d284c7cdc085fd2c Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Tue, 23 Jul 2019 10:47:49 -0500 Subject: [PATCH 071/147] Use status view controller for errors. --- Examples/ArcGISToolkitExamples/ARExample.swift | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/Examples/ArcGISToolkitExamples/ARExample.swift b/Examples/ArcGISToolkitExamples/ARExample.swift index 01b3fa76..975c7a44 100644 --- a/Examples/ArcGISToolkitExamples/ARExample.swift +++ b/Examples/ArcGISToolkitExamples/ARExample.swift @@ -112,9 +112,9 @@ class ARExample: UIViewController { override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) - arView.startTracking { (error) in + arView.startTracking { [weak self] (error) in if let error = error { - print("Error starting ArcGISARView tracking: \(error)") + self?.statusViewController?.errorMessage = error.localizedDescription } } } @@ -204,9 +204,15 @@ class ARExample: UIViewController { let portalItem = AGSPortalItem(portal: portal, itemID: "27f76008eeb04765b8a94d998aaa46c7") let scene = AGSScene(item: portalItem) - // Set camera to Everest summit. - arView.originCamera = AGSCamera(latitude: 27.988153, longitude: 86.925174, altitude: 8868.069399, heading: 159.56, pitch: 0.00, roll: 0.00) - arView.translationFactor = 1000 + scene.load { [weak self] (error) in + if let error = error { + self?.statusViewController?.errorMessage = error.localizedDescription + return + } + // Set camera to Everest summit. + self?.arView.originCamera = AGSCamera(latitude: 27.988153, longitude: 86.925174, altitude: 8868.069399, heading: 159.56, pitch: 0.00, roll: 0.00) + self?.arView.translationFactor = 1000 + } // Clear the location data source, as we're setting the originCamera directly. arView.locationDataSource = nil @@ -225,7 +231,7 @@ class ARExample: UIViewController { scene.load { [weak self] (error) in if let error = error { - print("Error loading scene: \(error)") + self?.statusViewController?.errorMessage = error.localizedDescription return } // Set the originCamera to be the initial viewpoint of the web scene. From c87fec4cdb12ee4fc0d7a58d869160d970573f45 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Tue, 23 Jul 2019 17:09:23 -0500 Subject: [PATCH 072/147] Add Tabletop/Full Scale to scene labels; reset didHitTest when switching scenes. --- Examples/ArcGISToolkitExamples/ARExample.swift | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/Examples/ArcGISToolkitExamples/ARExample.swift b/Examples/ArcGISToolkitExamples/ARExample.swift index 975c7a44..a1fa563c 100644 --- a/Examples/ArcGISToolkitExamples/ARExample.swift +++ b/Examples/ArcGISToolkitExamples/ARExample.swift @@ -100,10 +100,10 @@ class ARExample: UIViewController { statusItem], animated: false) // Set up the `sceneInfo` array with our scene init functions and labels. - sceneInfo.append(contentsOf: [(sceneFunction: streetsScene, label: "Streets"), - (sceneFunction: everestScene, label: "Everest"), - (sceneFunction: broncosStadiumScene, label: "Broncos Stadium"), - (sceneFunction: emptyScene, label: "Empty")]) + sceneInfo.append(contentsOf: [(sceneFunction: streetsScene, label: "Streets - Full Scale"), + (sceneFunction: everestScene, label: "Everest - Tabletop"), + (sceneFunction: broncosStadiumScene, label: "Broncos Stadium - Tabletop"), + (sceneFunction: emptyScene, label: "Empty - Full Scale")]) // Use the first sceneInfo to create and set the scene. currentSceneInfo = sceneInfo.first @@ -151,6 +151,9 @@ class ARExample: UIViewController { self.arView.stopTracking() self.arView.sceneView.scene = self.sceneInfo[newIndex].sceneFunction() self.arView.startTracking() + + // Reset didHitTest variable + self.didHitTest = false } } From ecb21e76847c888efd03534ad95e498b051d36eb Mon Sep 17 00:00:00 2001 From: Philip Ridgeway Date: Wed, 24 Jul 2019 12:50:07 -0700 Subject: [PATCH 073/147] Make VCListViewController subclass UITableViewController Fixes issue https://github.com/Esri/arcgis-runtime-toolkit-ios/issues/64 by using the default behavior provided by `UITableViewController`. --- .../Base.lproj/Main.storyboard | 57 +++++++++++-------- .../VCListViewController.swift | 19 +------ 2 files changed, 37 insertions(+), 39 deletions(-) diff --git a/Examples/ArcGISToolkitExamples/Base.lproj/Main.storyboard b/Examples/ArcGISToolkitExamples/Base.lproj/Main.storyboard index d658c310..9a9afb56 100644 --- a/Examples/ArcGISToolkitExamples/Base.lproj/Main.storyboard +++ b/Examples/ArcGISToolkitExamples/Base.lproj/Main.storyboard @@ -1,47 +1,58 @@ - + - + + - - - - - - - - - - - - - - - - - - - - + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Examples/ArcGISToolkitExamples/VCListViewController.swift b/Examples/ArcGISToolkitExamples/VCListViewController.swift index 8b611946..6df65512 100644 --- a/Examples/ArcGISToolkitExamples/VCListViewController.swift +++ b/Examples/ArcGISToolkitExamples/VCListViewController.swift @@ -14,7 +14,7 @@ import UIKit import ArcGISToolkit -open class VCListViewController: TableViewController { +open class VCListViewController: UITableViewController { public var storyboardName: String? @@ -25,30 +25,17 @@ open class VCListViewController: TableViewController { } } - override open func viewDidLoad() { - super.viewDidLoad() - } - - required public init?(coder aDecoder: NSCoder) { - super.init(coder: aDecoder) - } - - override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) { - super.init(nibName: nil, bundle: nil) - } - override open func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return viewControllerInfos.count } override open func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - let cell = tableView.dequeueReusableCell(withIdentifier: cellReuseIdentifier)! + let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) cell.textLabel?.text = viewControllerInfos[indexPath.row].vcName return cell } - public func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - + override open func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { let t = viewControllerInfos[indexPath.row].viewControllerType let nibName = viewControllerInfos[indexPath.row].nibName var vcOpt: UIViewController? = nil From d7180e4a94012b6ce9b16437fe714ff132a68dda Mon Sep 17 00:00:00 2001 From: Nicholas Furness Date: Wed, 24 Jul 2019 16:05:55 -0400 Subject: [PATCH 074/147] JobManager clean up (#63) * Remove the odd $0.1 reference to a dictionary tuple's value and replace with $0.value. Also rename some non-descriptive variable names. * Tidy up the coding style around opening block statements to be more consistent with other Swift code. * Refactor some iterations to be more Swifty. * Add comment to clarify how paused state works for jobs restored from JSON. * Focus on the keyedJobs dictionary. * Make jobs a calculated property (essentially a convenience accessor). * Refactor all UserDefaults writing to happen in keyedJobs didSet. * Refactor changes to keyedJobs to be single reassignments. Keeps the writing to UserDefaults at the same minimum, while simplifying the code. * Remove internal references to `jobs` property and settle on consistent access to `keyedJobs.values`. * Fix up the inline documentation comments. --- Toolkit/ArcGISToolkit/JobManager.swift | 256 ++++++++++++------------- 1 file changed, 123 insertions(+), 133 deletions(-) diff --git a/Toolkit/ArcGISToolkit/JobManager.swift b/Toolkit/ArcGISToolkit/JobManager.swift index dc893294..abe5ad41 100644 --- a/Toolkit/ArcGISToolkit/JobManager.swift +++ b/Toolkit/ArcGISToolkit/JobManager.swift @@ -46,89 +46,70 @@ public class JobManager: NSObject { return _jobManagerSharedInstance } - public private(set) var jobManagerID: String + /// The JobManager ID, provided during initialization. + public let jobManagerID: String - // Flag to signify that we shouldn't write to defaults - // Maybe we are currently reading from the defaults so it's pointless to write to them. - // Or maybe we are waiting until a group of modifications are made before writing to the defaults. + /// Flag to signify that we shouldn't write to User Defaults. + /// + /// Used internally when reading stored `AGSJob`s from the User Defaults during init(). private var suppressSaveToUserDefaults = false private var kvoContext = 0 - deinit { - jobs.forEach { unObserveJobStatus(job: $0) } - } - - public private(set) var keyedJobs = [String: AGSJob](){ - didSet{ - self.updateJobsArray() - saveJobsToUserDefaults() + /// A dictionary of Unique IDs and `AGSJob`s that the `JobManager` is managing. + public private(set) var keyedJobs = [String: AGSJob]() { + willSet { + // Need `self` because of a Swift bug. + self.keyedJobs.values.forEach { unObserveJobStatus(job: $0) } + } + didSet { + keyedJobs.values.forEach { observeJobStatus(job: $0) } + + // If there was a change, then re-store the serialized AGSJobs in UserDefaults + if keyedJobs != oldValue { + saveJobsToUserDefaults() + } } } - public private(set) var jobs = [AGSJob]() - private func updateJobsArray(){ - - // when our jobs array changes we need to observe the jobs' status - // that we aren't currently observing. The best way to do that is to - // just unObserve all, then re-observe all job status events - - // so first un-observe all current jobs - jobs.forEach { unObserveJobStatus(job: $0) } - - // set new jobs array - jobs = keyedJobs.map{ $0.1 } - - // now observe all jobs - jobs.forEach { observeJobStatus(job: $0) } + + /// A convenience accessor to the `AGSJob`s that the `JobManager` is managing. + public var jobs: [AGSJob] { + return Array(keyedJobs.values) } - private func toJSON() -> JSONDictionary{ - var d = [String: Any]() - for (jobID, job) in self.keyedJobs{ - if let json = try? job.toJSON(){ - d[jobID] = json - } - } - return d + private var jobsDefaultsKey: String { + return "com.esri.arcgis.runtime.toolkit.jobManager.\(jobManagerID).jobs" } /// Create a JobManager with an ID. - public required init(jobManagerID: String){ + /// + /// - Parameter jobManagerID: An arbitrary identifier for this JobManager. + public required init(jobManagerID: String) { self.jobManagerID = jobManagerID super.init() - if let d = UserDefaults.standard.dictionary(forKey: self.jobsDefaultsKey){ - suppressSaveToUserDefaults = true - self.instantiateStateFromJSON(json: d) - suppressSaveToUserDefaults = false - } + loadJobsFromUserDefaults() } - private func instantiateStateFromJSON(json: JSONDictionary){ - for (jobID, value) in json{ - if let jobJSON = value as? JSONDictionary{ - if let job = (try? AGSJob.fromJSON(jobJSON)) as? AGSJob{ - self.keyedJobs[jobID] = job - } - } - } + deinit { + keyedJobs.values.forEach { unObserveJobStatus(job: $0) } } - private var jobsDefaultsKey: String { - return "com.esri.arcgis.runtime.toolkit.jobManager.\(jobManagerID).jobs" + private func toJSON() -> JSONDictionary { + return keyedJobs.compactMapValues { try? $0.toJSON() } } - // observing job status code - - private func observeJobStatus(job: AGSJob){ - job.addObserver(self, forKeyPath: #keyPath(AGSJob.status), options: [], context: &kvoContext) + // Observing job status code + private func observeJobStatus(job: AGSJob) { + job.addObserver(self, forKeyPath: #keyPath(AGSJob.status), context: &kvoContext) } - private func unObserveJobStatus(job: AGSJob){ + + private func unObserveJobStatus(job: AGSJob) { job.removeObserver(self, forKeyPath: #keyPath(AGSJob.status)) } override public func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey: Any]?, context: UnsafeMutableRawPointer?) { - if context == &kvoContext{ - if keyPath == #keyPath(AGSJob.status){ + if context == &kvoContext { + if keyPath == #keyPath(AGSJob.status) { // when a job's status changes we need to save to user defaults again // so that the correct job state is reflected in our saved state saveJobsToUserDefaults() @@ -138,70 +119,61 @@ public class JobManager: NSObject { super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context) } } - - /** - Register a Job with the JobManager. - Returns a uniqueID for the Job. - */ - @discardableResult public func register(job: AGSJob) -> String{ + + /// Register an `AGSJob` with the `JobManager`. + /// + /// - Parameter job: The AGSJob to register. + /// - Returns: A unique ID for the AGSJob's registration which can be used to unregister the job. + @discardableResult public func register(job: AGSJob) -> String { let jobUniqueID = NSUUID().uuidString keyedJobs[jobUniqueID] = job return jobUniqueID } - /** - Unregister a Job with the JobManager - Returns true if it found the Job and was able to unregister it. - */ - @discardableResult public func unregister(job: AGSJob) -> Bool{ - for (key, value) in keyedJobs{ - if value === job{ - keyedJobs.removeValue(forKey: key) - return true - } + /// Unregister an `AGSJob` from the `JobManager`. + /// + /// - Parameter job: The job to unregister. + /// - Returns: `true` if the job was found, `false` otherwise. + @discardableResult public func unregister(job: AGSJob) -> Bool { + if let jobUniqueID = keyedJobs.first(where: { $0.value === job })?.key { + keyedJobs[jobUniqueID] = nil + return true } return false } - /** - Unregister a Job with the JobManager, using the Job's unique ID. - Returns true if it found the Job and was able to unregister it. - */ - @discardableResult public func unregister(jobUniqueID: String) -> Bool{ + /// Unregister an `AGSJob` from the `JobManager`. + /// + /// - Parameter jobUniqueID: The job's unique ID, returned from calling `register()`. + /// - Returns: `true` if the Job was found, `false` otherwise. + @discardableResult public func unregister(jobUniqueID: String) -> Bool { let removed = keyedJobs.removeValue(forKey: jobUniqueID) != nil return removed } - /// Clears the finished Jobs from the Job manager. - public func clearFinishedJobs(){ - - suppressSaveToUserDefaults = true - for (jobUniqueID, job) in keyedJobs{ - if job.status == .failed || job.status == .succeeded{ - keyedJobs.removeValue(forKey: jobUniqueID) - } + /// Clears the finished `AGSJob`s from the `JobManager`. + public func clearFinishedJobs() { + keyedJobs = keyedJobs.filter { + let status = $0.value.status + return !(status == .failed || status == .succeeded) } - suppressSaveToUserDefaults = false - saveJobsToUserDefaults() - } - /** - Checks the status for all Jobs and returns when completed. - */ - @discardableResult public func checkStatusForAllJobs(completion: @escaping (Bool)->Void) -> AGSCancelable{ - - + /// Checks the status for all `AGSJob`s calling a completion block when completed. + /// + /// - Parameter completion: A completion block that is called when the status of all `AGSJob`s has been checked. Passed `true` if all statuses were retrieves successfully, or `false` otherwise. + /// - Returns: An `AGSCancelable` group that can be used to cancel the status checks. + @discardableResult public func checkStatusForAllJobs(completion: @escaping (Bool)->Void) -> AGSCancelable { let cancelGroup = CancelGroup() let group = DispatchGroup() var completedWithoutErrors = true - keyedJobs.forEach{ + keyedJobs.forEach { group.enter() - let cancellable = $0.1.checkStatus{ error in - if error != nil{ + let cancellable = $0.value.checkStatus { error in + if error != nil { completedWithoutErrors = false } group.leave() @@ -209,22 +181,32 @@ public class JobManager: NSObject { cancelGroup.children.append(cancellable) } - group.notify(queue: DispatchQueue.main){ + group.notify(queue: .main) { completion(completedWithoutErrors) } return cancelGroup } - /** - Checks the status for all Jobs and calls the completion handler when done. - this method can be called from: - `func application(_ application: UIApplication, performFetchWithCompletionHandler completionHandler: @escaping` - */ + /// A helper function to call from a UIApplication's delegate when using iOS's Background Fetch capabilities. + /// + /// Checks the status for all `AGSJob`s and calls the completion handler when done. + /// + /// This method can be called from: + /// `func application(_ application: UIApplication, performFetchWithCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void))` + /// + /// See [Apple's documentation](https://developer.apple.com/documentation/uikit/uiapplicationdelegate/1623125-application) + /// for more details. + /// + /// - Parameters: + /// - application: See [Apple's documentation](https://developer.apple.com/documentation/uikit/uiapplicationdelegate/1623125-application) + /// - completionHandler: See [Apple's documentation](https://developer.apple.com/documentation/uikit/uiapplicationdelegate/1623125-application) public func application(application: UIApplication, performFetchWithCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) { - if self.jobs.count > 0{ - self.checkStatusForAllJobs{ completedWithoutErrors in - if completedWithoutErrors{ + if keyedJobs.isEmpty { + return completionHandler(.noData) + } else { + checkStatusForAllJobs { completedWithoutErrors in + if completedWithoutErrors { completionHandler(.newData) } else{ @@ -232,38 +214,46 @@ public class JobManager: NSObject { } } } - else{ - completionHandler(.noData) - } } - /// Resume all paused and not-started jobs. - public func resumeAllPausedJobs(statusHandler: @escaping JobStatusHandler, completion: @escaping JobCompletionHandler){ - keyedJobs.filter{ $0.1.status == .paused || $0.1.status == .notStarted}.forEach{ - $0.1.start(statusHandler: statusHandler, completion:completion) + /// Resume all paused and not-started `AGSJob`s. + /// + /// An `AGSJob`'s status is `.paused` when it is created from JSON. So any `AGSJob`s that have been reloaded from User Defaults will be in the `.paused` state. + /// + /// See the [Tasks and Jobs](https://developers.arcgis.com/ios/latest/swift/guide/tasks-and-jobs.htm#ESRI_SECTION1_BA1D597878F049278CC787A1C04F9734) + /// guide topic for more details. + /// + /// - Parameters: + /// - statusHandler: A callback block that is called by each active `AGSJob` when the `AGSJob`'s status changes or its messages array is updated. + /// - completion: A callback block that is called by each `AGSJob` when it has completed. + public func resumeAllPausedJobs(statusHandler: @escaping JobStatusHandler, completion: @escaping JobCompletionHandler) { + keyedJobs.lazy.filter({ $0.value.status == .paused || $0.value.status == .notStarted }).forEach { + $0.value.start(statusHandler: statusHandler, completion:completion) } } - /** - Saves all Jobs to User Defaults. - This happens automatically when the jobs are registered/unregistered. - It also happens when job status changes. - */ - private func saveJobsToUserDefaults(){ + /// Saves all managed `AGSJob`s to User Defaults. + /// + /// This happens automatically when the `AGSJob`s are registered/unregistered. + /// It also happens when an `AGSJob`'s status changes. + private func saveJobsToUserDefaults() { + guard !suppressSaveToUserDefaults else { return } - if suppressSaveToUserDefaults{ - return + UserDefaults.standard.set(self.toJSON(), forKey: jobsDefaultsKey) + } + + /// Load any `AGSJob`s that have been saved to User Defaults. + /// + /// This happens when the `JobManager` is initialized. All `AGSJob`s will be in the `.paused` state when first restored from JSON. + /// + /// See the [Tasks and Jobs](https://developers.arcgis.com/ios/latest/swift/guide/tasks-and-jobs.htm#ESRI_SECTION1_BA1D597878F049278CC787A1C04F9734) + /// guide topic for more details. + private func loadJobsFromUserDefaults() { + if let storedJobsJSON = UserDefaults.standard.dictionary(forKey: jobsDefaultsKey) { + suppressSaveToUserDefaults = true + keyedJobs = storedJobsJSON.compactMapValues { $0 is JSONDictionary ? (try? AGSJob.fromJSON($0)) as? AGSJob : nil } + suppressSaveToUserDefaults = false } - - let d = self.toJSON() - UserDefaults.standard.set(d, forKey: self.jobsDefaultsKey) } } - - - - - - - From 618744b62090bf47a76658b9d6fd65769ac6c6a9 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Wed, 24 Jul 2019 15:28:45 -0500 Subject: [PATCH 075/147] Call resetTracking after changing the scene instead of startTracking in order to reset location/originCamera. Update doc. --- Examples/ArcGISToolkitExamples/ARExample.swift | 4 ++-- Toolkit/ArcGISToolkit/AR/ArcGISARView.swift | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/Examples/ArcGISToolkitExamples/ARExample.swift b/Examples/ArcGISToolkitExamples/ARExample.swift index a1fa563c..73a6ea1f 100644 --- a/Examples/ArcGISToolkitExamples/ARExample.swift +++ b/Examples/ArcGISToolkitExamples/ARExample.swift @@ -147,10 +147,10 @@ class ARExample: UIViewController { // Set currentSceneInfo to the selected scene. self.currentSceneInfo = self.sceneInfo[newIndex] - // Stop tracking, update the scene with the selected Scene and start tracking again. + // Stop tracking, update the scene with the selected Scene and reset tracking. self.arView.stopTracking() self.arView.sceneView.scene = self.sceneInfo[newIndex].sceneFunction() - self.arView.startTracking() + self.arView.resetTracking() // Reset didHitTest variable self.didHitTest = false diff --git a/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift b/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift index 991fce04..73b79d1d 100644 --- a/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift +++ b/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift @@ -209,6 +209,8 @@ public class ArcGISARView: UIView { } /// Starts device tracking. + /// + /// - Parameter completion: The completion handler called when start tracking completes. If it tracking starts successfully, the `error` property will be nil; if tracking fails to start, the error will be non-nil and contain the reason for failure. public func startTracking(_ completion: ((_ error: Error?) -> Void)? = nil) { // We have a location data source that needs to be started. if let locationDataSource = self.locationDataSource { From 02c4e3b91e2b131727d786612f10b8e9c6136c17 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Fri, 26 Jul 2019 10:19:03 -0500 Subject: [PATCH 076/147] Display statusVC as a view, not a popover; add translationFactor to statusView --- .../ArcGISToolkitExamples/ARExample.swift | 42 ++++++++++++---- .../Misc/ARStatusViewController.storyboard | 49 +++++++++++++++---- .../Misc/ARStatusViewController.swift | 12 ++++- Toolkit/ArcGISToolkit/AR/ArcGISARView.swift | 7 ++- 4 files changed, 89 insertions(+), 21 deletions(-) diff --git a/Examples/ArcGISToolkitExamples/ARExample.swift b/Examples/ArcGISToolkitExamples/ARExample.swift index 73a6ea1f..4ddf7e40 100644 --- a/Examples/ArcGISToolkitExamples/ARExample.swift +++ b/Examples/ArcGISToolkitExamples/ARExample.swift @@ -41,7 +41,7 @@ class ARExample: UIViewController { private let statusViewController: ARStatusViewController? = { let storyBoard = UIStoryboard(name: "ARStatusViewController", bundle: nil) let vc = storyBoard.instantiateInitialViewController() as? ARStatusViewController - vc?.modalPresentationStyle = .popover +// vc?.modalPresentationStyle = .popover return vc }() @@ -54,6 +54,9 @@ class ARExample: UIViewController { let properties = AGSLayerSceneProperties(surfacePlacement: .relative) return overlay }() + + /// The observer for the `SceneView`'s `translationFactor` property + private var cameraControllerObservation: NSKeyValueObservation? override func viewDidLoad() { super.viewDidLoad() @@ -67,6 +70,13 @@ class ARExample: UIViewController { // Add our graphics overlay to the sceneView. arView.sceneView.graphicsOverlays.add(graphicsOverlay) + // Set up observer for the camera controller's translationFactor. + if let cameraController = arView.sceneView.cameraController as? AGSTransformationMatrixCameraController { + cameraControllerObservation = cameraController.observe(\AGSTransformationMatrixCameraController.translationFactor, options: [.initial, .new]){[weak self] tmcc, change in + self?.statusViewController?.translationFactor = tmcc.translationFactor + } + } + // Add arView to the view and setup the constraints. view.addSubview(arView) arView.translatesAutoresizingMaskIntoConstraints = false @@ -77,7 +87,6 @@ class ARExample: UIViewController { arView.bottomAnchor.constraint(equalTo: view.bottomAnchor) ]) - // Create a toolbar and add it to the arView. let toolbar = UIToolbar(frame: .zero) arView.addSubview(toolbar) @@ -99,6 +108,24 @@ class ARExample: UIViewController { UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil), statusItem], animated: false) + // Add the status view and setup constraints. + if let controller = statusViewController { + view.addSubview(controller.view) + controller.view.translatesAutoresizingMaskIntoConstraints = false + controller.preferredContentSize = { + let height: CGFloat = CGFloat(controller.tableView.numberOfRows(inSection: 0)) * controller.tableView.rowHeight + return CGSize(width: 350, height: height) + }() + NSLayoutConstraint.activate([ + controller.view.heightAnchor.constraint(equalToConstant: 175), + controller.view.widthAnchor.constraint(equalToConstant: 350), + controller.view.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -8), + controller.view.bottomAnchor.constraint(equalTo: toolbar.topAnchor, constant: -8) + ]) + + controller.view.alpha = 0.0 + } + // Set up the `sceneInfo` array with our scene init functions and labels. sceneInfo.append(contentsOf: [(sceneFunction: streetsScene, label: "Streets - Full Scale"), (sceneFunction: everestScene, label: "Everest - Tabletop"), @@ -171,13 +198,9 @@ class ARExample: UIViewController { /// Dislays the status view controller @objc func showStatus(_ sender: UIBarButtonItem){ guard let controller = statusViewController else { return } - controller.popoverPresentationController?.barButtonItem = sender - controller.preferredContentSize = { - let height: CGFloat = CGFloat(controller.tableView.numberOfRows(inSection: 0)) * controller.tableView.rowHeight - return CGSize(width: 375, height: height) - }() - - present(controller, animated: true) + UIView.animate(withDuration: 0.25) { + controller.view.alpha = controller.view.alpha == 1.0 ? 0.0 : 1.0 + } } // MARK: Scene Init Functions @@ -332,6 +355,7 @@ extension ARExample: ARSCNViewDelegate { self?.present(alertController, animated: true) } } + func session(_ session: ARSession, cameraDidChangeTrackingState camera: ARCamera) { // Set the tracking state on the status vc. statusViewController?.trackingState = camera.trackingState diff --git a/Examples/ArcGISToolkitExamples/Misc/ARStatusViewController.storyboard b/Examples/ArcGISToolkitExamples/Misc/ARStatusViewController.storyboard index aa468867..3798ecc1 100644 --- a/Examples/ArcGISToolkitExamples/Misc/ARStatusViewController.storyboard +++ b/Examples/ArcGISToolkitExamples/Misc/ARStatusViewController.storyboard @@ -13,7 +13,7 @@ - + @@ -21,10 +21,10 @@ - + - + - + - + - + - + - + - + + + + + + + + + + + + + + + + + + + @@ -125,6 +148,7 @@ + @@ -156,6 +180,13 @@ + diff --git a/Examples/ArcGISToolkitExamples/Misc/ARStatusViewController.swift b/Examples/ArcGISToolkitExamples/Misc/ARStatusViewController.swift index f0269201..5fab3891 100644 --- a/Examples/ArcGISToolkitExamples/Misc/ARStatusViewController.swift +++ b/Examples/ArcGISToolkitExamples/Misc/ARStatusViewController.swift @@ -39,6 +39,7 @@ class ARStatusViewController: UITableViewController { @IBOutlet var frameRateLabel: UILabel! @IBOutlet var errorDescriptionLabel: UILabel! @IBOutlet var sceneLabel: UILabel! + @IBOutlet var translationFactorLabel: UILabel! public var trackingState: ARCamera.TrackingState = .notAvailable { didSet { @@ -78,6 +79,15 @@ class ARStatusViewController: UITableViewController { } } + public var translationFactor: Double = 1.0 { + didSet { + DispatchQueue.main.async{ [weak self] in + guard let self = self else { return } + self.translationFactorLabel.text = String(format: "%.2f", self.translationFactor) + } + } + } + override func viewDidLoad() { super.viewDidLoad() } @@ -89,6 +99,6 @@ class ARStatusViewController: UITableViewController { } override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - return 4 + return 5 } } diff --git a/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift b/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift index 73b79d1d..983f4d2e 100644 --- a/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift +++ b/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift @@ -55,8 +55,11 @@ public class ArcGISARView: UIView { public let sceneView = AGSSceneView(frame: .zero) /// The translation factor used to support a table top AR experience. - public var translationFactor: Double = 1.0 { - didSet { + public var translationFactor: Double { + get { + return cameraController.translationFactor + } + set { cameraController.translationFactor = translationFactor } } From 8eedf70abacb8126f431112a5688bbe3b9e18165 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Mon, 29 Jul 2019 14:41:35 -0500 Subject: [PATCH 077/147] Fix translationFactor KVO. --- .../ArcGISToolkitExamples/ARExample.swift | 18 ++++++++------- Toolkit/ArcGISToolkit/AR/ArcGISARView.swift | 23 +++++++++++++++---- 2 files changed, 29 insertions(+), 12 deletions(-) diff --git a/Examples/ArcGISToolkitExamples/ARExample.swift b/Examples/ArcGISToolkitExamples/ARExample.swift index 4ddf7e40..97ef0486 100644 --- a/Examples/ArcGISToolkitExamples/ARExample.swift +++ b/Examples/ArcGISToolkitExamples/ARExample.swift @@ -56,7 +56,7 @@ class ARExample: UIViewController { }() /// The observer for the `SceneView`'s `translationFactor` property - private var cameraControllerObservation: NSKeyValueObservation? + private var translationFactorObservation: NSKeyValueObservation? override func viewDidLoad() { super.viewDidLoad() @@ -70,11 +70,9 @@ class ARExample: UIViewController { // Add our graphics overlay to the sceneView. arView.sceneView.graphicsOverlays.add(graphicsOverlay) - // Set up observer for the camera controller's translationFactor. - if let cameraController = arView.sceneView.cameraController as? AGSTransformationMatrixCameraController { - cameraControllerObservation = cameraController.observe(\AGSTransformationMatrixCameraController.translationFactor, options: [.initial, .new]){[weak self] tmcc, change in - self?.statusViewController?.translationFactor = tmcc.translationFactor - } + // Observe the `cameraController.translationFactor` property and update status when it changes. + translationFactorObservation = arView.observe(\ArcGISARView.translationFactor, options: [.initial, .new]){ [weak self] arView, change in + self?.statusViewController?.translationFactor = arView.translationFactor } // Add arView to the view and setup the constraints. @@ -150,7 +148,7 @@ class ARExample: UIViewController { super.viewDidDisappear(animated) arView.stopTracking() } - + /// Changes the scene to a newly selected scene. /// /// - Parameter sender: The bar button item tapped on. @@ -217,6 +215,8 @@ class ARExample: UIViewController { // Set the location data source so we use our GPS location as the originCamera. arView.locationDataSource = AGSCLLocationDataSource() + arView.translationFactor = 1 + arView.originCamera = nil return scene } @@ -283,9 +283,11 @@ class ARExample: UIViewController { // Set the location data source so we use our GPS location as the originCamera. arView.locationDataSource = AGSCLLocationDataSource() + arView.translationFactor = 1 + arView.originCamera = nil return scene } - + /// Adds an elevation source to the given `scene`. /// /// - Parameter scene: The scene to add the elevation source to. diff --git a/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift b/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift index 983f4d2e..333806e3 100644 --- a/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift +++ b/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift @@ -40,7 +40,7 @@ public class ArcGISARView: UIView { } /// The viewpoint camera used to set the initial view of the sceneView instead of the device's GPS location via the location data source. You can use Key-Value Observing to track changes to the origin camera. - @objc public dynamic var originCamera: AGSCamera? { + @objc dynamic public var originCamera: AGSCamera? { didSet { guard let newCamera = originCamera else { return } // Set the camera as the originCamera on the cameraController and reset tracking. @@ -55,12 +55,12 @@ public class ArcGISARView: UIView { public let sceneView = AGSSceneView(frame: .zero) /// The translation factor used to support a table top AR experience. - public var translationFactor: Double { + @objc dynamic public var translationFactor: Double { get { return cameraController.translationFactor } set { - cameraController.translationFactor = translationFactor + cameraController.translationFactor = newValue } } @@ -85,7 +85,7 @@ public class ArcGISARView: UIView { // MARK: Private properties /// The camera controller used to control the Scene. - private let cameraController = AGSTransformationMatrixCameraController() + @objc private let cameraController = AGSTransformationMatrixCameraController() /// Initial location from location data source. private var initialLocation: AGSPoint? @@ -164,6 +164,21 @@ public class ArcGISARView: UIView { sceneView.isManualRendering = isUsingARKit } + /// Implementing this method will allow the computed `translationFactor` property to generate KVO events when the `cameraController.translationFactor` value changes. + /// + /// - Parameter key: The key we want to observe. + /// - Returns: A set of key paths for properties whose values affect the value of the specified key. + public override class func keyPathsForValuesAffectingValue(forKey key: String) -> Set + { + var set = super.keyPathsForValuesAffectingValue(forKey: key) + if key == "translationFactor" { + // Get the key paths for super and append our key path to it. + set = set.union(Set(["cameraController.translationFactor"])) + } + + return set + } + // MARK: Public /// Determines the map point for the given screen point. From 3147e7634abffce7bed75538bc0d8c0a4dac39cd Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Mon, 29 Jul 2019 15:51:05 -0500 Subject: [PATCH 078/147] Remove commented out code. --- Examples/ArcGISToolkitExamples/ARExample.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/Examples/ArcGISToolkitExamples/ARExample.swift b/Examples/ArcGISToolkitExamples/ARExample.swift index 97ef0486..6209e0ca 100644 --- a/Examples/ArcGISToolkitExamples/ARExample.swift +++ b/Examples/ArcGISToolkitExamples/ARExample.swift @@ -41,7 +41,6 @@ class ARExample: UIViewController { private let statusViewController: ARStatusViewController? = { let storyBoard = UIStoryboard(name: "ARStatusViewController", bundle: nil) let vc = storyBoard.instantiateInitialViewController() as? ARStatusViewController -// vc?.modalPresentationStyle = .popover return vc }() From e326875e63ba686f87f659d241fbb9b22973f618 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Tue, 30 Jul 2019 13:03:50 -0500 Subject: [PATCH 079/147] Limit device orientation to landcape/portrait values. --- Toolkit/ArcGISToolkit/AR/ArcGISARView.swift | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift b/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift index 333806e3..14406439 100644 --- a/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift +++ b/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift @@ -101,6 +101,9 @@ public class ArcGISARView: UIView { return ARWorldTrackingConfiguration.isSupported }() + /// The last portrait or landscape orientation value. + private var lastGoodDeviceOrientation = UIDeviceOrientation.portrait + // MARK: Initializers public override init(frame: CGRect) { @@ -406,13 +409,22 @@ extension ArcGISARView: SCNSceneRendererDelegate { if let camera = arSCNView.session.currentFrame?.camera { let intrinsics = camera.intrinsics let imageResolution = camera.imageResolution + + // Get the device orientation, but don't allow non-landscape/portrait values. + var deviceOrientation = UIDevice.current.orientation + if deviceOrientation.isValidInterfaceOrientation { + lastGoodDeviceOrientation = deviceOrientation + } + else { + deviceOrientation = lastGoodDeviceOrientation + } sceneView.setFieldOfViewFromLensIntrinsicsWithXFocalLength(intrinsics[0][0], yFocalLength: intrinsics[1][1], xPrincipal: intrinsics[2][0], yPrincipal: intrinsics[2][1], xImageSize: Float(imageResolution.width), yImageSize: Float(imageResolution.height), - deviceOrientation: UIDevice.current.orientation) + deviceOrientation: deviceOrientation) } // Render the Scene with the new transformation. From 3458b6ae0c2c48017ac9eb788c10bece7f5e46fd Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Tue, 30 Jul 2019 14:56:54 -0500 Subject: [PATCH 080/147] Remove setInitialTransformation method as it conflicts with the private setter in an Obj-C app. --- Toolkit/ArcGISToolkit/AR/ArcGISARView.swift | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift b/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift index 14406439..31b87885 100644 --- a/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift +++ b/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift @@ -24,7 +24,7 @@ public class ArcGISARView: UIView { public let arSCNView = ARSCNView(frame: .zero) /// The initial transformation used for a table top experience. Defaults to the Identity Matrix. - public private(set) var initialTransformation: AGSTransformationMatrix = .identity + public var initialTransformation: AGSTransformationMatrix = .identity /// Denotes whether tracking location and angles has started. public private(set) var isTracking: Bool = false @@ -206,15 +206,6 @@ public class ArcGISARView: UIView { startTracking() } - /// Sets the initial transformation used to offset the originCamera. - /// - /// - Parameter initialTransformation: The initial transformation for originCamera offset. - /// - Returns: Whether setting the `initialTransformation` succeeded or failed. - @discardableResult public func setInitialTransformation(_ initialTransformation: AGSTransformationMatrix) -> Bool { - self.initialTransformation = initialTransformation - return true - } - /// Sets the initial transformation used to offset the originCamera. The initial transformation is based on an AR point determined via existing plane hit detection from `screenPoint`. If an AR point cannot be determined, this method will return `false`. /// /// - Parameter screenPoint: The screen point to determine the `initialTransformation` from. From 09a285caad3fa5afbd96d0739943c8ad38c8986e Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Wed, 31 Jul 2019 09:09:08 -0500 Subject: [PATCH 081/147] Update examples and move them to an extension. --- .../project.pbxproj | 16 +- .../ArcGISToolkitExamples/ARExample.swift | 285 +++++++++++------- 2 files changed, 191 insertions(+), 110 deletions(-) diff --git a/Examples/ArcGISToolkitExamples.xcodeproj/project.pbxproj b/Examples/ArcGISToolkitExamples.xcodeproj/project.pbxproj index 20d400a7..28e41aac 100644 --- a/Examples/ArcGISToolkitExamples.xcodeproj/project.pbxproj +++ b/Examples/ArcGISToolkitExamples.xcodeproj/project.pbxproj @@ -26,10 +26,10 @@ E447A12B2267BB9500578C0B /* ARExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = E447A12A2267BB9500578C0B /* ARExample.swift */; }; E464AA9122E62DC600969DBA /* Plane.swift in Sources */ = {isa = PBXBuildFile; fileRef = E464AA9022E62DC600969DBA /* Plane.swift */; }; E464AA9322E633C500969DBA /* OptionsTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E464AA9222E633C500969DBA /* OptionsTableViewController.swift */; }; - E464AA9922E6542E00969DBA /* ARStatusViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E464AA9722E6542E00969DBA /* ARStatusViewController.swift */; }; - E464AA9A22E6542E00969DBA /* ARStatusViewController.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = E464AA9822E6542E00969DBA /* ARStatusViewController.storyboard */; }; E46893271FEDAE29008ADA79 /* CompassExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = E46893261FEDAE29008ADA79 /* CompassExample.swift */; }; E48405751E9BE7E600927208 /* LegendExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = E48405741E9BE7E600927208 /* LegendExample.swift */; }; + E4CDFD0D22EF844F002B2C66 /* ARStatusViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4CDFD0C22EF844F002B2C66 /* ARStatusViewController.swift */; }; + E4CDFD0F22EF8462002B2C66 /* ARStatusViewController.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = E4CDFD0E22EF8462002B2C66 /* ARStatusViewController.storyboard */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -91,10 +91,10 @@ E447A12A2267BB9500578C0B /* ARExample.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ARExample.swift; sourceTree = ""; }; E464AA9022E62DC600969DBA /* Plane.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Plane.swift; sourceTree = ""; }; E464AA9222E633C500969DBA /* OptionsTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OptionsTableViewController.swift; sourceTree = ""; }; - E464AA9722E6542E00969DBA /* ARStatusViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ARStatusViewController.swift; sourceTree = ""; }; - E464AA9822E6542E00969DBA /* ARStatusViewController.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = ARStatusViewController.storyboard; sourceTree = ""; }; E46893261FEDAE29008ADA79 /* CompassExample.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompassExample.swift; sourceTree = ""; }; E48405741E9BE7E600927208 /* LegendExample.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LegendExample.swift; sourceTree = ""; }; + E4CDFD0C22EF844F002B2C66 /* ARStatusViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ARStatusViewController.swift; sourceTree = ""; }; + E4CDFD0E22EF8462002B2C66 /* ARStatusViewController.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = ARStatusViewController.storyboard; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -173,8 +173,8 @@ children = ( E464AA9222E633C500969DBA /* OptionsTableViewController.swift */, E464AA9022E62DC600969DBA /* Plane.swift */, - E464AA9822E6542E00969DBA /* ARStatusViewController.storyboard */, - E464AA9722E6542E00969DBA /* ARStatusViewController.swift */, + E4CDFD0C22EF844F002B2C66 /* ARStatusViewController.swift */, + E4CDFD0E22EF8462002B2C66 /* ARStatusViewController.storyboard */, ); path = Misc; sourceTree = ""; @@ -266,7 +266,7 @@ files = ( 883904491DF6022A001F3188 /* LaunchScreen.storyboard in Resources */, 883904461DF6022A001F3188 /* Assets.xcassets in Resources */, - E464AA9A22E6542E00969DBA /* ARStatusViewController.storyboard in Resources */, + E4CDFD0F22EF8462002B2C66 /* ARStatusViewController.storyboard in Resources */, 883904441DF6022A001F3188 /* Main.storyboard in Resources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -304,7 +304,7 @@ 88B689D11E96EDF400B67FAB /* VCListViewController.swift in Sources */, 8800656E2228577A00F76945 /* TemplatePickerExample.swift in Sources */, 88B689CE1E96EDF400B67FAB /* ScalebarExample.swift in Sources */, - E464AA9922E6542E00969DBA /* ARStatusViewController.swift in Sources */, + E4CDFD0D22EF844F002B2C66 /* ARStatusViewController.swift in Sources */, 88B689C91E96EDF400B67FAB /* ExamplesViewController.swift in Sources */, E447A12B2267BB9500578C0B /* ARExample.swift in Sources */, E48405751E9BE7E600927208 /* LegendExample.swift in Sources */, diff --git a/Examples/ArcGISToolkitExamples/ARExample.swift b/Examples/ArcGISToolkitExamples/ARExample.swift index 6209e0ca..691d67b0 100644 --- a/Examples/ArcGISToolkitExamples/ARExample.swift +++ b/Examples/ArcGISToolkitExamples/ARExample.swift @@ -125,8 +125,9 @@ class ARExample: UIViewController { // Set up the `sceneInfo` array with our scene init functions and labels. sceneInfo.append(contentsOf: [(sceneFunction: streetsScene, label: "Streets - Full Scale"), - (sceneFunction: everestScene, label: "Everest - Tabletop"), - (sceneFunction: broncosStadiumScene, label: "Broncos Stadium - Tabletop"), + (sceneFunction: pointCloudScene, label: "Point Cloud - Tabletop"), + (sceneFunction: yosemiteScene, label: "Yosemite - Tabletop"), + (sceneFunction: borderScene, label: "US - Mexico Border - Tabletop"), (sceneFunction: emptyScene, label: "Empty - Full Scale")]) // Use the first sceneInfo to create and set the scene. @@ -199,106 +200,6 @@ class ARExample: UIViewController { controller.view.alpha = controller.view.alpha == 1.0 ? 0.0 : 1.0 } } - - // MARK: Scene Init Functions - - /// Creates a scene based on the Streets base map. - /// Mode: Full-Scale AR - /// - /// - Returns: The new scene. - private func streetsScene() -> AGSScene { - - // Create scene with the streets basemap. - let scene = AGSScene(basemapType: .streets) - addElevationSource(toScene: scene) - - // Set the location data source so we use our GPS location as the originCamera. - arView.locationDataSource = AGSCLLocationDataSource() - arView.translationFactor = 1 - arView.originCamera = nil - return scene - } - - /// Creates a scene based on the Mount Everest web scene. - /// Mode: Tabletop AR - /// - /// - Returns: The new scene. - private func everestScene() -> AGSScene { - // Create scene using the Everest web scene. - let portal = AGSPortal.arcGISOnline(withLoginRequired: false) - let portalItem = AGSPortalItem(portal: portal, itemID: "27f76008eeb04765b8a94d998aaa46c7") - let scene = AGSScene(item: portalItem) - - scene.load { [weak self] (error) in - if let error = error { - self?.statusViewController?.errorMessage = error.localizedDescription - return - } - // Set camera to Everest summit. - self?.arView.originCamera = AGSCamera(latitude: 27.988153, longitude: 86.925174, altitude: 8868.069399, heading: 159.56, pitch: 0.00, roll: 0.00) - self?.arView.translationFactor = 1000 - } - - // Clear the location data source, as we're setting the originCamera directly. - arView.locationDataSource = nil - return scene - } - - /// Creates a scene based on the Broncos stadium web scene. - /// Mode: Tabletop AR - /// - /// - Returns: The new scene. - private func broncosStadiumScene() -> AGSScene { - // Create scene using WebScene of the Broncos stadium - let portal = AGSPortal.arcGISOnline(withLoginRequired: false) - let portalItem = AGSPortalItem(portal: portal, itemID: "72460f2c5b4048339433afedcb2369e1") - let scene = AGSScene(item: portalItem) - - scene.load { [weak self] (error) in - if let error = error { - self?.statusViewController?.errorMessage = error.localizedDescription - return - } - // Set the originCamera to be the initial viewpoint of the web scene. - self?.arView.originCamera = scene.initialViewpoint.camera - self?.arView.translationFactor = 1000 - } - - // Turn off background grid. - scene.baseSurface?.backgroundGrid.isVisible = false - - // Clear the location data source, as we're setting the originCamera directly. - arView.locationDataSource = nil - return scene - } - - /// Creates an empty scene with an elevation source. - /// Mode: Full-Scale AR - /// - /// - Returns: The new scene. - private func emptyScene() -> AGSScene { - let scene = AGSScene() - addElevationSource(toScene: scene) - - // Set the location data source so we use our GPS location as the originCamera. - arView.locationDataSource = AGSCLLocationDataSource() - arView.translationFactor = 1 - arView.originCamera = nil - return scene - } - - /// Adds an elevation source to the given `scene`. - /// - /// - Parameter scene: The scene to add the elevation source to. - private func addElevationSource(toScene scene: AGSScene) { - let elevationSource = AGSArcGISTiledElevationSource(url: URL(string: "https://elevation3d.arcgis.com/arcgis/rest/services/WorldElevation3D/Terrain3D/ImageServer")!) - let surface = AGSSurface() - surface.elevationSources = [elevationSource] - surface.name = "baseSurface" - surface.isEnabled = true - surface.backgroundGrid.isVisible = false - scene.baseSurface = surface - } } extension ARExample: ARSCNViewDelegate { @@ -398,3 +299,183 @@ extension ARExample: UIAdaptivePresentationControllerDelegate { return .none } } + +extension ARExample { + // + // These methods create the scenes and perform other intitialization required to set up the AR experiences. + // + + /// Creates a scene based on the Streets base map. + /// Mode: Full-Scale AR + /// + /// - Returns: The new scene. + private func streetsScene() -> AGSScene { + + // Create scene with the streets basemap. + let scene = AGSScene(basemapType: .streets) + addElevationSource(toScene: scene) + + // Set the location data source so we use our GPS location as the originCamera. + arView.locationDataSource = AGSCLLocationDataSource() + arView.originCamera = nil + arView.translationFactor = 1 + return scene + } + + /// Creates a scene based on a point cloud layer. + /// Mode: Tabletop AR + /// + /// - Returns: The new scene. + private func pointCloudScene() -> AGSScene { + // Create scene using a portalItem of the point cloud layer. + let portal = AGSPortal.arcGISOnline(withLoginRequired: false) + let portalItem = AGSPortalItem(portal: portal, itemID: "fc3f4a4919394808830cd11df4631a54") + let layer = AGSPointCloudLayer(item: portalItem) + let scene = AGSScene() + addElevationSource(toScene: scene) + scene.operationalLayers.add(layer) + + layer.load { [weak self] (error) in + if let error = error { + self?.statusViewController?.errorMessage = error.localizedDescription + return + } + + guard let extent = layer.fullExtent else { return } + let center = extent.center + + // Create the origin camera at the center point of the data. This will ensure the data is anchored to the table. + let camera = AGSCamera(latitude: center.y, longitude: center.x, altitude: 0, heading: 0, pitch: 0, roll: 0) + self?.arView.originCamera = camera + self?.arView.translationFactor = 2000 + } + + // Clear the location data source, as we're setting the originCamera directly. + arView.locationDataSource = nil + return scene + } + + /// Creates a scene centered on Yosemite National Park. + /// Mode: Tabletop AR + /// + /// - Returns: The new scene. + private func yosemiteScene() -> AGSScene { + let scene = AGSScene() + addElevationSource(toScene: scene) + + // Create the Yosemite layer. + let layer = AGSIntegratedMeshLayer(url: URL(string:"https://tiles.arcgis.com/tiles/FQD0rKU8X5sAQfh8/arcgis/rest/services/VRICON_Yosemite_Sample_Integrated_Mesh_scene_layer/SceneServer")!) + scene.operationalLayers.add(layer) + scene.load { [weak self, weak scene] (error) in + if let error = error { + self?.statusViewController?.errorMessage = error.localizedDescription + return + } + + // Get the center point of the layer's extent. + guard let layer = scene?.operationalLayers.firstObject as? AGSLayer else { return } + guard let extent = layer.fullExtent else { return } + let center = extent.center + + scene?.baseSurface?.elevationSources.first?.load { (error) in + if let error = error { + self?.statusViewController?.errorMessage = error.localizedDescription + return + } + + // Find the elevation of the layer at the center point. + scene?.baseSurface?.elevation(for: center, completion: { (elevation, error) in + if let error = error { + self?.statusViewController?.errorMessage = error.localizedDescription + return + } + + // Create the origin camera at the center point and elevation of the data. This will ensure the data is anchored to the table. + let camera = AGSCamera(latitude: center.y, longitude: center.x, altitude: elevation, heading: 0, pitch: 0, roll: 0) + self?.arView.originCamera = camera + self?.arView.translationFactor = 1000 + }) + } + } + + // Clear the location data source, as we're setting the originCamera directly. + arView.locationDataSource = nil + return scene + } + + /// Creates a scene centered the US-Mexico border. + /// Mode: Tabletop AR + /// + /// - Returns: The new scene. + private func borderScene() -> AGSScene { + let scene = AGSScene() + addElevationSource(toScene: scene) + + // Create the border layer. + let layer = AGSIntegratedMeshLayer(url: URL(string:"https://tiles.arcgis.com/tiles/FQD0rKU8X5sAQfh8/arcgis/rest/services/VRICON_SW_US_Sample_Integrated_Mesh_scene_layer/SceneServer")!) + scene.operationalLayers.add(layer) + scene.load { [weak self, weak scene] (error) in + if let error = error { + self?.statusViewController?.errorMessage = error.localizedDescription + return + } + + // Get the center point of the layer's extent. + guard let layer = scene?.operationalLayers.firstObject as? AGSLayer else { return } + guard let extent = layer.fullExtent else { return } + let center = extent.center + + scene?.baseSurface?.elevationSources.first?.load { (error) in + if let error = error { + self?.statusViewController?.errorMessage = error.localizedDescription + return + } + + // Find the elevation of the layer at the center point. + scene?.baseSurface?.elevation(for: center, completion: { (elevation, error) in + if let error = error { + self?.statusViewController?.errorMessage = error.localizedDescription + return + } + + // Create the origin camera at the center point and elevation of the data. This will ensure the data is anchored to the table. + let camera = AGSCamera(latitude: center.y, longitude: center.x, altitude: elevation, heading: 0, pitch: 0, roll: 0) + self?.arView.originCamera = camera + self?.arView.translationFactor = 1000 + }) + } + } + + // Clear the location data source, as we're setting the originCamera directly. + arView.locationDataSource = nil + return scene + } + + /// Creates an empty scene with an elevation source. + /// Mode: Full-Scale AR + /// + /// - Returns: The new scene. + private func emptyScene() -> AGSScene { + let scene = AGSScene() + addElevationSource(toScene: scene) + + // Set the location data source so we use our GPS location as the originCamera. + arView.locationDataSource = AGSCLLocationDataSource() + arView.originCamera = nil + arView.translationFactor = 1 + return scene + } + + /// Adds an elevation source to the given `scene`. + /// + /// - Parameter scene: The scene to add the elevation source to. + private func addElevationSource(toScene scene: AGSScene) { + let elevationSource = AGSArcGISTiledElevationSource(url: URL(string: "https://elevation3d.arcgis.com/arcgis/rest/services/WorldElevation3D/Terrain3D/ImageServer")!) + let surface = AGSSurface() + surface.elevationSources = [elevationSource] + surface.name = "baseSurface" + surface.isEnabled = true + surface.backgroundGrid.isVisible = false + scene.baseSurface = surface + } +} From 3aaee17cf5cde16037580c02d888b9df77e1e106 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Wed, 31 Jul 2019 09:12:54 -0500 Subject: [PATCH 082/147] KVO and Orientation fixes; remove unnecessary setInitialTransformation method and make property a var. --- Toolkit/ArcGISToolkit/AR/ArcGISARView.swift | 55 +++++++++++++++------ 1 file changed, 39 insertions(+), 16 deletions(-) diff --git a/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift b/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift index 991fce04..31b87885 100644 --- a/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift +++ b/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift @@ -24,7 +24,7 @@ public class ArcGISARView: UIView { public let arSCNView = ARSCNView(frame: .zero) /// The initial transformation used for a table top experience. Defaults to the Identity Matrix. - public private(set) var initialTransformation: AGSTransformationMatrix = .identity + public var initialTransformation: AGSTransformationMatrix = .identity /// Denotes whether tracking location and angles has started. public private(set) var isTracking: Bool = false @@ -40,7 +40,7 @@ public class ArcGISARView: UIView { } /// The viewpoint camera used to set the initial view of the sceneView instead of the device's GPS location via the location data source. You can use Key-Value Observing to track changes to the origin camera. - @objc public dynamic var originCamera: AGSCamera? { + @objc dynamic public var originCamera: AGSCamera? { didSet { guard let newCamera = originCamera else { return } // Set the camera as the originCamera on the cameraController and reset tracking. @@ -55,9 +55,12 @@ public class ArcGISARView: UIView { public let sceneView = AGSSceneView(frame: .zero) /// The translation factor used to support a table top AR experience. - public var translationFactor: Double = 1.0 { - didSet { - cameraController.translationFactor = translationFactor + @objc dynamic public var translationFactor: Double { + get { + return cameraController.translationFactor + } + set { + cameraController.translationFactor = newValue } } @@ -82,7 +85,7 @@ public class ArcGISARView: UIView { // MARK: Private properties /// The camera controller used to control the Scene. - private let cameraController = AGSTransformationMatrixCameraController() + @objc private let cameraController = AGSTransformationMatrixCameraController() /// Initial location from location data source. private var initialLocation: AGSPoint? @@ -98,6 +101,9 @@ public class ArcGISARView: UIView { return ARWorldTrackingConfiguration.isSupported }() + /// The last portrait or landscape orientation value. + private var lastGoodDeviceOrientation = UIDeviceOrientation.portrait + // MARK: Initializers public override init(frame: CGRect) { @@ -161,6 +167,21 @@ public class ArcGISARView: UIView { sceneView.isManualRendering = isUsingARKit } + /// Implementing this method will allow the computed `translationFactor` property to generate KVO events when the `cameraController.translationFactor` value changes. + /// + /// - Parameter key: The key we want to observe. + /// - Returns: A set of key paths for properties whose values affect the value of the specified key. + public override class func keyPathsForValuesAffectingValue(forKey key: String) -> Set + { + var set = super.keyPathsForValuesAffectingValue(forKey: key) + if key == "translationFactor" { + // Get the key paths for super and append our key path to it. + set = set.union(Set(["cameraController.translationFactor"])) + } + + return set + } + // MARK: Public /// Determines the map point for the given screen point. @@ -185,15 +206,6 @@ public class ArcGISARView: UIView { startTracking() } - /// Sets the initial transformation used to offset the originCamera. - /// - /// - Parameter initialTransformation: The initial transformation for originCamera offset. - /// - Returns: Whether setting the `initialTransformation` succeeded or failed. - @discardableResult public func setInitialTransformation(_ initialTransformation: AGSTransformationMatrix) -> Bool { - self.initialTransformation = initialTransformation - return true - } - /// Sets the initial transformation used to offset the originCamera. The initial transformation is based on an AR point determined via existing plane hit detection from `screenPoint`. If an AR point cannot be determined, this method will return `false`. /// /// - Parameter screenPoint: The screen point to determine the `initialTransformation` from. @@ -209,6 +221,8 @@ public class ArcGISARView: UIView { } /// Starts device tracking. + /// + /// - Parameter completion: The completion handler called when start tracking completes. If it tracking starts successfully, the `error` property will be nil; if tracking fails to start, the error will be non-nil and contain the reason for failure. public func startTracking(_ completion: ((_ error: Error?) -> Void)? = nil) { // We have a location data source that needs to be started. if let locationDataSource = self.locationDataSource { @@ -386,13 +400,22 @@ extension ArcGISARView: SCNSceneRendererDelegate { if let camera = arSCNView.session.currentFrame?.camera { let intrinsics = camera.intrinsics let imageResolution = camera.imageResolution + + // Get the device orientation, but don't allow non-landscape/portrait values. + var deviceOrientation = UIDevice.current.orientation + if deviceOrientation.isValidInterfaceOrientation { + lastGoodDeviceOrientation = deviceOrientation + } + else { + deviceOrientation = lastGoodDeviceOrientation + } sceneView.setFieldOfViewFromLensIntrinsicsWithXFocalLength(intrinsics[0][0], yFocalLength: intrinsics[1][1], xPrincipal: intrinsics[2][0], yPrincipal: intrinsics[2][1], xImageSize: Float(imageResolution.width), yImageSize: Float(imageResolution.height), - deviceOrientation: UIDevice.current.orientation) + deviceOrientation: deviceOrientation) } // Render the Scene with the new transformation. From 987bbae48355c90e613806a7b8a9ecc17421c852 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Wed, 31 Jul 2019 10:46:27 -0500 Subject: [PATCH 083/147] Update Toolkit/ArcGISToolkit/AR/ArcGISARView.swift Use #keyPath and set.insert. Co-Authored-By: Philip Ridgeway --- Toolkit/ArcGISToolkit/AR/ArcGISARView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift b/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift index 31b87885..64dd8fe2 100644 --- a/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift +++ b/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift @@ -176,7 +176,7 @@ public class ArcGISARView: UIView { var set = super.keyPathsForValuesAffectingValue(forKey: key) if key == "translationFactor" { // Get the key paths for super and append our key path to it. - set = set.union(Set(["cameraController.translationFactor"])) + set.insert(#keyPath(cameraController.translationFactor)) } return set From 937cd552dc0f3397c8f89418b510f99512de3bf2 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Wed, 31 Jul 2019 10:49:21 -0500 Subject: [PATCH 084/147] Simplify if statement and use lastGoodDeviceOrientation. --- Toolkit/ArcGISToolkit/AR/ArcGISARView.swift | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift b/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift index 31b87885..65ed4f46 100644 --- a/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift +++ b/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift @@ -402,20 +402,17 @@ extension ArcGISARView: SCNSceneRendererDelegate { let imageResolution = camera.imageResolution // Get the device orientation, but don't allow non-landscape/portrait values. - var deviceOrientation = UIDevice.current.orientation + let deviceOrientation = UIDevice.current.orientation if deviceOrientation.isValidInterfaceOrientation { lastGoodDeviceOrientation = deviceOrientation } - else { - deviceOrientation = lastGoodDeviceOrientation - } sceneView.setFieldOfViewFromLensIntrinsicsWithXFocalLength(intrinsics[0][0], yFocalLength: intrinsics[1][1], xPrincipal: intrinsics[2][0], yPrincipal: intrinsics[2][1], xImageSize: Float(imageResolution.width), yImageSize: Float(imageResolution.height), - deviceOrientation: deviceOrientation) + deviceOrientation: lastGoodDeviceOrientation) } // Render the Scene with the new transformation. From f98bb23f8a1fc53e3224f0d4b499f358575af295 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Wed, 31 Jul 2019 10:51:28 -0500 Subject: [PATCH 085/147] Update Toolkit/ArcGISToolkit/AR/ArcGISARView.swift Use #keyPath() Co-Authored-By: Philip Ridgeway --- Toolkit/ArcGISToolkit/AR/ArcGISARView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift b/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift index 9d4838ea..c629f394 100644 --- a/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift +++ b/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift @@ -174,7 +174,7 @@ public class ArcGISARView: UIView { public override class func keyPathsForValuesAffectingValue(forKey key: String) -> Set { var set = super.keyPathsForValuesAffectingValue(forKey: key) - if key == "translationFactor" { + if key == #keyPath(translationFactor) { // Get the key paths for super and append our key path to it. set.insert(#keyPath(cameraController.translationFactor)) } From 143c5148848150d61cb9d7f391dfc1697de408a8 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Wed, 31 Jul 2019 12:44:17 -0500 Subject: [PATCH 086/147] Fix doc typo. --- Toolkit/ArcGISToolkit/AR/ArcGISARView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift b/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift index c629f394..2cf9554e 100644 --- a/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift +++ b/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift @@ -222,7 +222,7 @@ public class ArcGISARView: UIView { /// Starts device tracking. /// - /// - Parameter completion: The completion handler called when start tracking completes. If it tracking starts successfully, the `error` property will be nil; if tracking fails to start, the error will be non-nil and contain the reason for failure. + /// - Parameter completion: The completion handler called when start tracking completes. If tracking starts successfully, the `error` property will be nil; if tracking fails to start, the error will be non-nil and contain the reason for failure. public func startTracking(_ completion: ((_ error: Error?) -> Void)? = nil) { // We have a location data source that needs to be started. if let locationDataSource = self.locationDataSource { From 3613bcc2b5311232890c48e81e05637047aefba1 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Wed, 31 Jul 2019 16:48:20 -0500 Subject: [PATCH 087/147] Clean up and simplifiy. --- .../ArcGISToolkitExamples/Misc/Plane.swift | 51 +++++++++---------- 1 file changed, 25 insertions(+), 26 deletions(-) diff --git a/Examples/ArcGISToolkitExamples/Misc/Plane.swift b/Examples/ArcGISToolkitExamples/Misc/Plane.swift index 540ed742..9f02c489 100644 --- a/Examples/ArcGISToolkitExamples/Misc/Plane.swift +++ b/Examples/ArcGISToolkitExamples/Misc/Plane.swift @@ -1,47 +1,46 @@ -/* -See LICENSE folder for this sample’s licensing information. +// +// Copyright 2019 Esri. -Abstract: -Convenience class for visualizing Plane extent and geometry -*/ +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 + +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. import ARKit +/// Helper class to visualize a plane found by ARKit class Plane: SCNNode { - let extentNode: SCNNode - var classificationNode: SCNNode? - - /// - Tag: VisualizePlane + let node: SCNNode init(anchor: ARPlaneAnchor, in sceneView: ARSCNView) { // Create a node to visualize the plane's bounding rectangle. - let extentPlane: SCNPlane = SCNPlane(width: CGFloat(anchor.extent.x), height: CGFloat(anchor.extent.z)) - extentNode = SCNNode(geometry: extentPlane) - extentNode.simdPosition = anchor.center + let extent: SCNPlane = SCNPlane(width: CGFloat(anchor.extent.x), height: CGFloat(anchor.extent.z)) + node = SCNNode(geometry: extent) + node.simdPosition = anchor.center // `SCNPlane` is vertically oriented in its local coordinate space, so // rotate it to match the orientation of `ARPlaneAnchor`. - extentNode.eulerAngles.x = -.pi / 2 + node.eulerAngles.x = -.pi / 2 super.init() - self.setupExtentVisualStyle() + node.opacity = 0.6 + guard let material = node.geometry?.firstMaterial + else { fatalError("SCNPlane always has one material") } + + material.diffuse.contents = UIColor.blue - // Add the plane extent as child node so they appear in the scene. - addChildNode(extentNode) + // Add the plane node as child node so they appear in the scene. + addChildNode(node) } required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } - - private func setupExtentVisualStyle() { - // Make the extent visualization semitransparent to clearly show real-world placement. - extentNode.opacity = 0.6 - - guard let material = extentNode.geometry?.firstMaterial - else { fatalError("SCNPlane always has one material") } - - material.diffuse.contents = UIColor.blue - } } From c0f53bc853cda50e8598eca639e7bbe2392df2f8 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Wed, 31 Jul 2019 17:06:37 -0500 Subject: [PATCH 088/147] Change extentNode to simply node. --- Examples/ArcGISToolkitExamples/ARExample.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Examples/ArcGISToolkitExamples/ARExample.swift b/Examples/ArcGISToolkitExamples/ARExample.swift index 691d67b0..a0f2617d 100644 --- a/Examples/ArcGISToolkitExamples/ARExample.swift +++ b/Examples/ArcGISToolkitExamples/ARExample.swift @@ -223,10 +223,10 @@ extension ARExample: ARSCNViewDelegate { else { return } // Update extent visualization to the anchor's new bounding rectangle. - if let extentGeometry = plane.extentNode.geometry as? SCNPlane { + if let extentGeometry = plane.node.geometry as? SCNPlane { extentGeometry.width = CGFloat(planeAnchor.extent.x) extentGeometry.height = CGFloat(planeAnchor.extent.z) - plane.extentNode.simdPosition = planeAnchor.center + plane.node.simdPosition = planeAnchor.center } } From 3be3761b1357ba26b9e7e23c9ff944470fdae2ec Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Wed, 31 Jul 2019 17:09:35 -0500 Subject: [PATCH 089/147] Add class doc for StatusViewController --- Examples/ArcGISToolkitExamples/Misc/ARStatusViewController.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Examples/ArcGISToolkitExamples/Misc/ARStatusViewController.swift b/Examples/ArcGISToolkitExamples/Misc/ARStatusViewController.swift index 5fab3891..24db3a0a 100644 --- a/Examples/ArcGISToolkitExamples/Misc/ARStatusViewController.swift +++ b/Examples/ArcGISToolkitExamples/Misc/ARStatusViewController.swift @@ -33,6 +33,7 @@ extension ARCamera.TrackingState { } } +/// A view controller for display AR-related status information. class ARStatusViewController: UITableViewController { @IBOutlet var trackingStateLabel: UILabel! From 9c24d5ffd95c7a36d0fbfdd6421f3f648170106a Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Thu, 1 Aug 2019 11:51:16 -0500 Subject: [PATCH 090/147] Update Examples/ArcGISToolkitExamples/ARExample.swift Use initWithFrame method. Co-Authored-By: Philip Ridgeway --- Examples/ArcGISToolkitExamples/ARExample.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Examples/ArcGISToolkitExamples/ARExample.swift b/Examples/ArcGISToolkitExamples/ARExample.swift index a0f2617d..0966f997 100644 --- a/Examples/ArcGISToolkitExamples/ARExample.swift +++ b/Examples/ArcGISToolkitExamples/ARExample.swift @@ -85,7 +85,7 @@ class ARExample: UIViewController { ]) // Create a toolbar and add it to the arView. - let toolbar = UIToolbar(frame: .zero) + let toolbar = UIToolbar() arView.addSubview(toolbar) toolbar.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ From 7f3cf6737f0d8723bbbb5e39ac87e1386a2cfaf4 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Fri, 2 Aug 2019 14:18:31 -0500 Subject: [PATCH 091/147] Remove OptionsTableViewController and use an action sheet instead; move ARStatusViewController resources to a .xib file. --- .../project.pbxproj | 20 +- .../ArcGISToolkitExamples/ARExample.swift | 115 ++++------- .../Misc/ARStatusTableViewController.swift | 146 +++++++++++++ .../Misc/ARStatusTableViewController.xib | 30 +++ .../Misc/ARStatusViewController.storyboard | 194 ------------------ .../Misc/ARStatusViewController.swift | 105 ---------- .../Misc/OptionsTableViewController.swift | 86 -------- 7 files changed, 229 insertions(+), 467 deletions(-) create mode 100644 Examples/ArcGISToolkitExamples/Misc/ARStatusTableViewController.swift create mode 100644 Examples/ArcGISToolkitExamples/Misc/ARStatusTableViewController.xib delete mode 100644 Examples/ArcGISToolkitExamples/Misc/ARStatusViewController.storyboard delete mode 100644 Examples/ArcGISToolkitExamples/Misc/ARStatusViewController.swift delete mode 100644 Examples/ArcGISToolkitExamples/Misc/OptionsTableViewController.swift diff --git a/Examples/ArcGISToolkitExamples.xcodeproj/project.pbxproj b/Examples/ArcGISToolkitExamples.xcodeproj/project.pbxproj index 28e41aac..6518e66b 100644 --- a/Examples/ArcGISToolkitExamples.xcodeproj/project.pbxproj +++ b/Examples/ArcGISToolkitExamples.xcodeproj/project.pbxproj @@ -23,13 +23,12 @@ 88B689CE1E96EDF400B67FAB /* ScalebarExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88B689C41E96EDF400B67FAB /* ScalebarExample.swift */; }; 88B689D11E96EDF400B67FAB /* VCListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88B689C71E96EDF400B67FAB /* VCListViewController.swift */; }; 88DBC2A11FE83D6000255921 /* JobManagerExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88DBC2A01FE83D6000255921 /* JobManagerExample.swift */; }; + E42D375622F38F6F00CB6EA3 /* ARStatusTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E42D375422F38F6F00CB6EA3 /* ARStatusTableViewController.swift */; }; + E42D375722F38F6F00CB6EA3 /* ARStatusTableViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = E42D375522F38F6F00CB6EA3 /* ARStatusTableViewController.xib */; }; E447A12B2267BB9500578C0B /* ARExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = E447A12A2267BB9500578C0B /* ARExample.swift */; }; E464AA9122E62DC600969DBA /* Plane.swift in Sources */ = {isa = PBXBuildFile; fileRef = E464AA9022E62DC600969DBA /* Plane.swift */; }; - E464AA9322E633C500969DBA /* OptionsTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E464AA9222E633C500969DBA /* OptionsTableViewController.swift */; }; E46893271FEDAE29008ADA79 /* CompassExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = E46893261FEDAE29008ADA79 /* CompassExample.swift */; }; E48405751E9BE7E600927208 /* LegendExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = E48405741E9BE7E600927208 /* LegendExample.swift */; }; - E4CDFD0D22EF844F002B2C66 /* ARStatusViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4CDFD0C22EF844F002B2C66 /* ARStatusViewController.swift */; }; - E4CDFD0F22EF8462002B2C66 /* ARStatusViewController.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = E4CDFD0E22EF8462002B2C66 /* ARStatusViewController.storyboard */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -88,13 +87,12 @@ 88B689C41E96EDF400B67FAB /* ScalebarExample.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ScalebarExample.swift; sourceTree = ""; }; 88B689C71E96EDF400B67FAB /* VCListViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VCListViewController.swift; sourceTree = ""; }; 88DBC2A01FE83D6000255921 /* JobManagerExample.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JobManagerExample.swift; sourceTree = ""; }; + E42D375422F38F6F00CB6EA3 /* ARStatusTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ARStatusTableViewController.swift; sourceTree = ""; }; + E42D375522F38F6F00CB6EA3 /* ARStatusTableViewController.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = ARStatusTableViewController.xib; sourceTree = ""; }; E447A12A2267BB9500578C0B /* ARExample.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ARExample.swift; sourceTree = ""; }; E464AA9022E62DC600969DBA /* Plane.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Plane.swift; sourceTree = ""; }; - E464AA9222E633C500969DBA /* OptionsTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OptionsTableViewController.swift; sourceTree = ""; }; E46893261FEDAE29008ADA79 /* CompassExample.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompassExample.swift; sourceTree = ""; }; E48405741E9BE7E600927208 /* LegendExample.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LegendExample.swift; sourceTree = ""; }; - E4CDFD0C22EF844F002B2C66 /* ARStatusViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ARStatusViewController.swift; sourceTree = ""; }; - E4CDFD0E22EF8462002B2C66 /* ARStatusViewController.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = ARStatusViewController.storyboard; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -171,10 +169,9 @@ E464AA8F22E62D5B00969DBA /* Misc */ = { isa = PBXGroup; children = ( - E464AA9222E633C500969DBA /* OptionsTableViewController.swift */, E464AA9022E62DC600969DBA /* Plane.swift */, - E4CDFD0C22EF844F002B2C66 /* ARStatusViewController.swift */, - E4CDFD0E22EF8462002B2C66 /* ARStatusViewController.storyboard */, + E42D375422F38F6F00CB6EA3 /* ARStatusTableViewController.swift */, + E42D375522F38F6F00CB6EA3 /* ARStatusTableViewController.xib */, ); path = Misc; sourceTree = ""; @@ -266,7 +263,7 @@ files = ( 883904491DF6022A001F3188 /* LaunchScreen.storyboard in Resources */, 883904461DF6022A001F3188 /* Assets.xcassets in Resources */, - E4CDFD0F22EF8462002B2C66 /* ARStatusViewController.storyboard in Resources */, + E42D375722F38F6F00CB6EA3 /* ARStatusTableViewController.xib in Resources */, 883904441DF6022A001F3188 /* Main.storyboard in Resources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -298,13 +295,12 @@ 88B689C81E96EDF400B67FAB /* AppDelegate.swift in Sources */, 2140781E209B629000FBFDCC /* TimeSliderExample.swift in Sources */, E464AA9122E62DC600969DBA /* Plane.swift in Sources */, - E464AA9322E633C500969DBA /* OptionsTableViewController.swift in Sources */, 88B689CB1E96EDF400B67FAB /* MeasureExample.swift in Sources */, 88DBC2A11FE83D6000255921 /* JobManagerExample.swift in Sources */, + E42D375622F38F6F00CB6EA3 /* ARStatusTableViewController.swift in Sources */, 88B689D11E96EDF400B67FAB /* VCListViewController.swift in Sources */, 8800656E2228577A00F76945 /* TemplatePickerExample.swift in Sources */, 88B689CE1E96EDF400B67FAB /* ScalebarExample.swift in Sources */, - E4CDFD0D22EF844F002B2C66 /* ARStatusViewController.swift in Sources */, 88B689C91E96EDF400B67FAB /* ExamplesViewController.swift in Sources */, E447A12B2267BB9500578C0B /* ARExample.swift in Sources */, E48405751E9BE7E600927208 /* LegendExample.swift in Sources */, diff --git a/Examples/ArcGISToolkitExamples/ARExample.swift b/Examples/ArcGISToolkitExamples/ARExample.swift index a0f2617d..4e476763 100644 --- a/Examples/ArcGISToolkitExamples/ARExample.swift +++ b/Examples/ArcGISToolkitExamples/ARExample.swift @@ -27,7 +27,7 @@ class ARExample: UIViewController { private var currentSceneInfo: (sceneFunction: sceneInitFunction, label: String)? { didSet { guard let label = currentSceneInfo?.label else { return } - statusViewController?.currentScene = label + statusViewController.currentScene = label } } @@ -38,10 +38,8 @@ class ARExample: UIViewController { private var didHitTest: Bool = false // View controller displaying current status of `ARExample`. - private let statusViewController: ARStatusViewController? = { - let storyBoard = UIStoryboard(name: "ARStatusViewController", bundle: nil) - let vc = storyBoard.instantiateInitialViewController() as? ARStatusViewController - return vc + private let statusViewController: ARStatusTableViewController = { + return ARStatusTableViewController(nibName: "ARStatusTableViewController", bundle: nil) }() /// Used when calculating framerate. @@ -50,10 +48,10 @@ class ARExample: UIViewController { /// Overlay used to display user-placed graphics. private let graphicsOverlay: AGSGraphicsOverlay = { let overlay = AGSGraphicsOverlay() - let properties = AGSLayerSceneProperties(surfacePlacement: .relative) + overlay.sceneProperties = AGSLayerSceneProperties(surfacePlacement: .relative) return overlay }() - + /// The observer for the `SceneView`'s `translationFactor` property private var translationFactorObservation: NSKeyValueObservation? @@ -71,7 +69,7 @@ class ARExample: UIViewController { // Observe the `cameraController.translationFactor` property and update status when it changes. translationFactorObservation = arView.observe(\ArcGISARView.translationFactor, options: [.initial, .new]){ [weak self] arView, change in - self?.statusViewController?.translationFactor = arView.translationFactor + self?.statusViewController.translationFactor = arView.translationFactor } // Add arView to the view and setup the constraints. @@ -106,22 +104,17 @@ class ARExample: UIViewController { statusItem], animated: false) // Add the status view and setup constraints. - if let controller = statusViewController { - view.addSubview(controller.view) - controller.view.translatesAutoresizingMaskIntoConstraints = false - controller.preferredContentSize = { - let height: CGFloat = CGFloat(controller.tableView.numberOfRows(inSection: 0)) * controller.tableView.rowHeight - return CGSize(width: 350, height: height) - }() - NSLayoutConstraint.activate([ - controller.view.heightAnchor.constraint(equalToConstant: 175), - controller.view.widthAnchor.constraint(equalToConstant: 350), - controller.view.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -8), - controller.view.bottomAnchor.constraint(equalTo: toolbar.topAnchor, constant: -8) - ]) - - controller.view.alpha = 0.0 - } + addChild(statusViewController) + view.addSubview(statusViewController.view) + statusViewController.view.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([ + statusViewController.view.heightAnchor.constraint(equalToConstant: statusViewController.height()), + statusViewController.view.widthAnchor.constraint(equalToConstant: 350), + statusViewController.view.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -8), + statusViewController.view.bottomAnchor.constraint(equalTo: toolbar.topAnchor, constant: -8) + ]) + + statusViewController.view.alpha = 0.0 // Set up the `sceneInfo` array with our scene init functions and labels. sceneInfo.append(contentsOf: [(sceneFunction: streetsScene, label: "Streets - Full Scale"), @@ -139,7 +132,7 @@ class ARExample: UIViewController { super.viewDidAppear(animated) arView.startTracking { [weak self] (error) in if let error = error { - self?.statusViewController?.errorMessage = error.localizedDescription + self?.statusViewController.errorMessage = error.localizedDescription } } } @@ -153,51 +146,33 @@ class ARExample: UIViewController { /// /// - Parameter sender: The bar button item tapped on. @objc func changeScene(_ sender: UIBarButtonItem){ - guard let label = currentSceneInfo?.label, - // Get the index of the scene currently shown in the sceneView. - let selectedIndex = sceneInfo.firstIndex(where: { $0.label == label }) else { - return - } - - // Create the array of labels for the options table view controller. - let sceneLabels = sceneInfo.map { $0.label } - - // A view controller allowing the user to select the scene to show. - // Note: the `OptionsTableViewController` is copied from the "ArcGIS Runtime SDK for iOS Samples" code, found here: https://github.com/Esri/arcgis-runtime-samples-ios - let controller = OptionsTableViewController(labels: sceneLabels, selectedIndex: selectedIndex) { [weak self] (newIndex) in - if let self = self { - // Dismiss the popover. - self.dismiss(animated: true, completion: nil) - + // Display an alert controller displaying the scenes to choose from. + let alertController = UIAlertController(title: nil, message: nil, preferredStyle: UIAlertController.Style.actionSheet) + alertController.popoverPresentationController?.barButtonItem = sender + sceneInfo.forEach { (sceneFunction, label) in + let action = UIAlertAction(title: label, style: .default, handler: { [weak self] (action) in // Set currentSceneInfo to the selected scene. - self.currentSceneInfo = self.sceneInfo[newIndex] + self?.currentSceneInfo = (sceneFunction, label) // Stop tracking, update the scene with the selected Scene and reset tracking. - self.arView.stopTracking() - self.arView.sceneView.scene = self.sceneInfo[newIndex].sceneFunction() - self.arView.resetTracking() + self?.arView.stopTracking() + self?.arView.sceneView.scene = sceneFunction() + self?.arView.resetTracking() // Reset didHitTest variable - self.didHitTest = false - } + self?.didHitTest = false + }) + // Display current scene as disabled. + action.isEnabled = !(label == currentSceneInfo?.label) + alertController.addAction(action) } - - // Configure the options controller as a popover. - controller.modalPresentationStyle = .popover - controller.presentationController?.delegate = self - controller.preferredContentSize = CGSize(width: 300, height: 300) - controller.popoverPresentationController?.barButtonItem = sender - controller.popoverPresentationController?.passthroughViews?.append(arView) - - // Show the popover. - present(controller, animated: true) + present(alertController, animated: true) } /// Dislays the status view controller @objc func showStatus(_ sender: UIBarButtonItem){ - guard let controller = statusViewController else { return } - UIView.animate(withDuration: 0.25) { - controller.view.alpha = controller.view.alpha == 1.0 ? 0.0 : 1.0 + UIView.animate(withDuration: 0.25) { [weak self] in + self?.statusViewController.view.alpha = self?.statusViewController.view.alpha == 1.0 ? 0.0 : 1.0 } } } @@ -244,7 +219,7 @@ extension ARExample: ARSCNViewDelegate { let errorMessage = messages.compactMap({ $0 }).joined(separator: "\n") // Set the error message on the status vc. - statusViewController?.errorMessage = errorMessage + statusViewController.errorMessage = errorMessage DispatchQueue.main.async { [weak self] in // Present an alert describing the error. @@ -260,13 +235,13 @@ extension ARExample: ARSCNViewDelegate { func session(_ session: ARSession, cameraDidChangeTrackingState camera: ARCamera) { // Set the tracking state on the status vc. - statusViewController?.trackingState = camera.trackingState + statusViewController.trackingState = camera.trackingState } func renderer(_ renderer: SCNSceneRenderer, willRenderScene scene: SCNScene, atTime time: TimeInterval) { // Calculate frame rate and set on the statuc vc. let frametime = time - lastUpdateTime - statusViewController?.frameRate = Int((1.0 / frametime).rounded()) + statusViewController.frameRate = Int((1.0 / frametime).rounded()) lastUpdateTime = time } } @@ -337,7 +312,7 @@ extension ARExample { layer.load { [weak self] (error) in if let error = error { - self?.statusViewController?.errorMessage = error.localizedDescription + self?.statusViewController.errorMessage = error.localizedDescription return } @@ -368,7 +343,7 @@ extension ARExample { scene.operationalLayers.add(layer) scene.load { [weak self, weak scene] (error) in if let error = error { - self?.statusViewController?.errorMessage = error.localizedDescription + self?.statusViewController.errorMessage = error.localizedDescription return } @@ -379,14 +354,14 @@ extension ARExample { scene?.baseSurface?.elevationSources.first?.load { (error) in if let error = error { - self?.statusViewController?.errorMessage = error.localizedDescription + self?.statusViewController.errorMessage = error.localizedDescription return } // Find the elevation of the layer at the center point. scene?.baseSurface?.elevation(for: center, completion: { (elevation, error) in if let error = error { - self?.statusViewController?.errorMessage = error.localizedDescription + self?.statusViewController.errorMessage = error.localizedDescription return } @@ -416,7 +391,7 @@ extension ARExample { scene.operationalLayers.add(layer) scene.load { [weak self, weak scene] (error) in if let error = error { - self?.statusViewController?.errorMessage = error.localizedDescription + self?.statusViewController.errorMessage = error.localizedDescription return } @@ -427,14 +402,14 @@ extension ARExample { scene?.baseSurface?.elevationSources.first?.load { (error) in if let error = error { - self?.statusViewController?.errorMessage = error.localizedDescription + self?.statusViewController.errorMessage = error.localizedDescription return } // Find the elevation of the layer at the center point. scene?.baseSurface?.elevation(for: center, completion: { (elevation, error) in if let error = error { - self?.statusViewController?.errorMessage = error.localizedDescription + self?.statusViewController.errorMessage = error.localizedDescription return } diff --git a/Examples/ArcGISToolkitExamples/Misc/ARStatusTableViewController.swift b/Examples/ArcGISToolkitExamples/Misc/ARStatusTableViewController.swift new file mode 100644 index 00000000..2969cd87 --- /dev/null +++ b/Examples/ArcGISToolkitExamples/Misc/ARStatusTableViewController.swift @@ -0,0 +1,146 @@ +// Copyright 2019 Esri. + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 + +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import UIKit +import ARKit + +extension ARCamera.TrackingState { + var description: String { + switch self { + case .normal: + return "Normal" + case .notAvailable: + return "Tracking unavailable" + case .limited(.excessiveMotion): + return "Limited - Excessive Motion" + case .limited(.insufficientFeatures): + return "Limited - Insufficient Features" + case .limited(.initializing): + return "Limited - Initializing" + default: + return "" + } + } +} + +/// A view controller for display AR-related status information. +class ARStatusTableViewController: UITableViewController { + + /// The `ARKit` camera tracking state. + public var trackingState: ARCamera.TrackingState = .notAvailable { + didSet { + DispatchQueue.main.async{ [weak self] in + self?.tableView.reloadData() + } + } + } + + /// The calculated frame rate of the `SceneView` and `ARKit` display. + public var frameRate: Int = 0 { + didSet { + DispatchQueue.main.async{ [weak self] in + self?.tableView.reloadData() + } + } + } + + /// The current error message. + public var errorMessage: String = "None" { + didSet { + DispatchQueue.main.async{ [weak self] in + self?.tableView.reloadData() + } + } + } + + /// The label for the currently selected scene. + public var currentScene: String = "None" { + didSet { + DispatchQueue.main.async{ [weak self] in + self?.tableView.reloadData() + } + } + } + + /// The translation factor applied to the current scene. + public var translationFactor: Double = 1.0 { + didSet { + DispatchQueue.main.async{ [weak self] in + self?.tableView.reloadData() + } + } + } + + /// The labels for each status item. + private let cellLabels = ["Tracking State", "Frame Rate", "Error", "Scene", "Translation Factor"] + + /// The height of our rows. + private let rowHeight: CGFloat = 24.0 + + override func viewDidLoad() { + super.viewDidLoad() + + // Set the rowHeight of our table view. + tableView.rowHeight = rowHeight + + // Add a blur effect behind the table view. + tableView.backgroundView = UIVisualEffectView(effect: UIBlurEffect(style: .regular)) + } + + /// Calculates and returns the height of the table view based on the row height and number of rows. + /// + /// - Returns: The calculated height of the table view. + public func height() -> CGFloat { + return CGFloat(cellLabels.count) * rowHeight + } + + // MARK: - Table view data source + + override func numberOfSections(in tableView: UITableView) -> Int { + return 1 + } + + override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + return (section == 0) ? cellLabels.count : 0 + } + + override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + // Don't reuse cells, as our table is essentially static. + let statusCell = UITableViewCell(style: .value1, reuseIdentifier: nil) + statusCell.backgroundColor = .clear + statusCell.textLabel?.font = UIFont.systemFont(ofSize: 12.0) + statusCell.detailTextLabel?.font = UIFont.boldSystemFont(ofSize: 12.0) + statusCell.detailTextLabel?.textColor = .black + + statusCell.textLabel?.text = cellLabels[indexPath.row] + + var detailString = "" + switch indexPath.row { + case 0: + detailString = trackingState.description + case 1: + detailString = "\(self.frameRate)" + case 2: + detailString = errorMessage + case 3: + detailString = currentScene + case 4: + detailString = String(format: "%.2f", self.translationFactor) + default: + detailString = "" + } + statusCell.detailTextLabel?.text = detailString + + return statusCell + } +} diff --git a/Examples/ArcGISToolkitExamples/Misc/ARStatusTableViewController.xib b/Examples/ArcGISToolkitExamples/Misc/ARStatusTableViewController.xib new file mode 100644 index 00000000..13fb8130 --- /dev/null +++ b/Examples/ArcGISToolkitExamples/Misc/ARStatusTableViewController.xib @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Examples/ArcGISToolkitExamples/Misc/ARStatusViewController.storyboard b/Examples/ArcGISToolkitExamples/Misc/ARStatusViewController.storyboard deleted file mode 100644 index 3798ecc1..00000000 --- a/Examples/ArcGISToolkitExamples/Misc/ARStatusViewController.storyboard +++ /dev/null @@ -1,194 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Examples/ArcGISToolkitExamples/Misc/ARStatusViewController.swift b/Examples/ArcGISToolkitExamples/Misc/ARStatusViewController.swift deleted file mode 100644 index 24db3a0a..00000000 --- a/Examples/ArcGISToolkitExamples/Misc/ARStatusViewController.swift +++ /dev/null @@ -1,105 +0,0 @@ -// Copyright 2019 Esri. - -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// http://www.apache.org/licenses/LICENSE-2.0 - -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -import UIKit -import ARKit - -extension ARCamera.TrackingState { - var description: String { - switch self { - case .normal: - return "Normal" - case .notAvailable: - return "Tracking unavailable" - case .limited(.excessiveMotion): - return "Limited - Excessive Motion" - case .limited(.insufficientFeatures): - return "Limited - Insufficient Features" - case .limited(.initializing): - return "Limited - Initializing" - default: - return "" - } - } -} - -/// A view controller for display AR-related status information. -class ARStatusViewController: UITableViewController { - - @IBOutlet var trackingStateLabel: UILabel! - @IBOutlet var frameRateLabel: UILabel! - @IBOutlet var errorDescriptionLabel: UILabel! - @IBOutlet var sceneLabel: UILabel! - @IBOutlet var translationFactorLabel: UILabel! - - public var trackingState: ARCamera.TrackingState = .notAvailable { - didSet { - guard trackingStateLabel != nil else { return } - DispatchQueue.main.async{ [weak self] in - guard let self = self else { return } - self.trackingStateLabel.text = self.trackingState.description - } - } - } - - public var frameRate: Int = 0 { - didSet { - guard frameRateLabel != nil else { return } - DispatchQueue.main.async{ [weak self] in - guard let self = self else { return } - self.frameRateLabel.text = "\(self.frameRate)" - } - } - } - - public var errorMessage: String = "None" { - didSet { - DispatchQueue.main.async{ [weak self] in - guard let self = self else { return } - self.errorDescriptionLabel.text = self.errorMessage - } - } - } - - public var currentScene: String = "None" { - didSet { - DispatchQueue.main.async{ [weak self] in - guard let self = self else { return } - self.sceneLabel.text = self.currentScene - } - } - } - - public var translationFactor: Double = 1.0 { - didSet { - DispatchQueue.main.async{ [weak self] in - guard let self = self else { return } - self.translationFactorLabel.text = String(format: "%.2f", self.translationFactor) - } - } - } - - override func viewDidLoad() { - super.viewDidLoad() - } - - // MARK: - Table view data source - - override func numberOfSections(in tableView: UITableView) -> Int { - return 1 - } - - override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - return 5 - } -} diff --git a/Examples/ArcGISToolkitExamples/Misc/OptionsTableViewController.swift b/Examples/ArcGISToolkitExamples/Misc/OptionsTableViewController.swift deleted file mode 100644 index 96d5b757..00000000 --- a/Examples/ArcGISToolkitExamples/Misc/OptionsTableViewController.swift +++ /dev/null @@ -1,86 +0,0 @@ -// Copyright 2018 Esri. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -import UIKit - -/// A basic interface for selecting one options from a list, -/// showing the checkmark accessory for the selected cell. -class OptionsTableViewController: UITableViewController { - struct Option { - let label: String - let image: UIImage? - - init(label: String, image: UIImage? = nil) { - self.label = label - self.image = image - } - } - - private let options: [Option] - private var selectedIndex: Int - private let onChange: (Int) -> Void - - convenience init(labels: [String], selectedIndex: Int, onChange: @escaping (Int) -> Void) { - let options = labels.map { Option(label: $0) } - self.init(options: options, selectedIndex: selectedIndex, onChange: onChange) - } - - init(options: [Option], selectedIndex: Int, onChange: @escaping (Int) -> Void) { - self.options = options - self.selectedIndex = selectedIndex - self.onChange = onChange - super.init(nibName: nil, bundle: nil) - } - - @available(*, unavailable) - required init?(coder aDecoder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - override func viewDidLoad() { - super.viewDidLoad() - tableView.register(OptionCell.self, forCellReuseIdentifier: "OptionCell") - } - - // UITableViewDataSource - - override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - return options.count - } - - override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - let cell = tableView.dequeueReusableCell(withIdentifier: "OptionCell", for: indexPath) - cell.selectionStyle = .none - let option = options[indexPath.row] - cell.textLabel?.text = option.label - cell.imageView?.image = option.image - if selectedIndex == indexPath.row { - tableView.selectRow(at: indexPath, animated: false, scrollPosition: .none) - } - return cell - } - - // UITableViewDelegate - - override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - selectedIndex = indexPath.row - onChange(indexPath.row) - } -} - -private class OptionCell: UITableViewCell { - override func setSelected(_ selected: Bool, animated: Bool) { - accessoryType = selected ? .checkmark : .none - } -} From 81a487d03cc4926403baaa014078cdb9610553aa Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Mon, 5 Aug 2019 16:58:11 -0500 Subject: [PATCH 092/147] PR review changes for status VC creation and parent structure. --- Examples/ArcGISToolkitExamples/ARExample.swift | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/Examples/ArcGISToolkitExamples/ARExample.swift b/Examples/ArcGISToolkitExamples/ARExample.swift index ea3cc05e..d7cee3d8 100644 --- a/Examples/ArcGISToolkitExamples/ARExample.swift +++ b/Examples/ArcGISToolkitExamples/ARExample.swift @@ -38,9 +38,7 @@ class ARExample: UIViewController { private var didHitTest: Bool = false // View controller displaying current status of `ARExample`. - private let statusViewController: ARStatusTableViewController = { - return ARStatusTableViewController(nibName: "ARStatusTableViewController", bundle: nil) - }() + private let statusViewController: ARStatusTableViewController = ARStatusTableViewController() /// Used when calculating framerate. private var lastUpdateTime: TimeInterval = 0 @@ -106,6 +104,7 @@ class ARExample: UIViewController { // Add the status view and setup constraints. addChild(statusViewController) view.addSubview(statusViewController.view) + statusViewController.didMove(toParent: self) statusViewController.view.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ statusViewController.view.heightAnchor.constraint(equalToConstant: statusViewController.height()), From f4402c70961d7f3abb5493969b965d97a38cf527 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Mon, 5 Aug 2019 17:27:59 -0500 Subject: [PATCH 093/147] Go back to Storyboard for ARStatusViewController; other PR review tweaks. --- .../project.pbxproj | 16 +- .../ArcGISToolkitExamples/ARExample.swift | 61 +++--- .../Misc/ARStatusTableViewController.xib | 30 --- .../Misc/ARStatusViewController.storyboard | 199 ++++++++++++++++++ ...ler.swift => ARStatusViewController.swift} | 85 +++----- 5 files changed, 267 insertions(+), 124 deletions(-) delete mode 100644 Examples/ArcGISToolkitExamples/Misc/ARStatusTableViewController.xib create mode 100644 Examples/ArcGISToolkitExamples/Misc/ARStatusViewController.storyboard rename Examples/ArcGISToolkitExamples/Misc/{ARStatusTableViewController.swift => ARStatusViewController.swift} (56%) diff --git a/Examples/ArcGISToolkitExamples.xcodeproj/project.pbxproj b/Examples/ArcGISToolkitExamples.xcodeproj/project.pbxproj index 6518e66b..2f4b9d8a 100644 --- a/Examples/ArcGISToolkitExamples.xcodeproj/project.pbxproj +++ b/Examples/ArcGISToolkitExamples.xcodeproj/project.pbxproj @@ -23,11 +23,11 @@ 88B689CE1E96EDF400B67FAB /* ScalebarExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88B689C41E96EDF400B67FAB /* ScalebarExample.swift */; }; 88B689D11E96EDF400B67FAB /* VCListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88B689C71E96EDF400B67FAB /* VCListViewController.swift */; }; 88DBC2A11FE83D6000255921 /* JobManagerExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88DBC2A01FE83D6000255921 /* JobManagerExample.swift */; }; - E42D375622F38F6F00CB6EA3 /* ARStatusTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E42D375422F38F6F00CB6EA3 /* ARStatusTableViewController.swift */; }; - E42D375722F38F6F00CB6EA3 /* ARStatusTableViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = E42D375522F38F6F00CB6EA3 /* ARStatusTableViewController.xib */; }; E447A12B2267BB9500578C0B /* ARExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = E447A12A2267BB9500578C0B /* ARExample.swift */; }; E464AA9122E62DC600969DBA /* Plane.swift in Sources */ = {isa = PBXBuildFile; fileRef = E464AA9022E62DC600969DBA /* Plane.swift */; }; E46893271FEDAE29008ADA79 /* CompassExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = E46893261FEDAE29008ADA79 /* CompassExample.swift */; }; + E47B16FA22F8DECC000C9C8B /* ARStatusViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E47B16F822F8DECC000C9C8B /* ARStatusViewController.swift */; }; + E47B16FB22F8DECC000C9C8B /* ARStatusViewController.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = E47B16F922F8DECC000C9C8B /* ARStatusViewController.storyboard */; }; E48405751E9BE7E600927208 /* LegendExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = E48405741E9BE7E600927208 /* LegendExample.swift */; }; /* End PBXBuildFile section */ @@ -87,11 +87,11 @@ 88B689C41E96EDF400B67FAB /* ScalebarExample.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ScalebarExample.swift; sourceTree = ""; }; 88B689C71E96EDF400B67FAB /* VCListViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VCListViewController.swift; sourceTree = ""; }; 88DBC2A01FE83D6000255921 /* JobManagerExample.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JobManagerExample.swift; sourceTree = ""; }; - E42D375422F38F6F00CB6EA3 /* ARStatusTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ARStatusTableViewController.swift; sourceTree = ""; }; - E42D375522F38F6F00CB6EA3 /* ARStatusTableViewController.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = ARStatusTableViewController.xib; sourceTree = ""; }; E447A12A2267BB9500578C0B /* ARExample.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ARExample.swift; sourceTree = ""; }; E464AA9022E62DC600969DBA /* Plane.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Plane.swift; sourceTree = ""; }; E46893261FEDAE29008ADA79 /* CompassExample.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompassExample.swift; sourceTree = ""; }; + E47B16F822F8DECC000C9C8B /* ARStatusViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ARStatusViewController.swift; sourceTree = ""; }; + E47B16F922F8DECC000C9C8B /* ARStatusViewController.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = ARStatusViewController.storyboard; sourceTree = ""; }; E48405741E9BE7E600927208 /* LegendExample.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LegendExample.swift; sourceTree = ""; }; /* End PBXFileReference section */ @@ -169,9 +169,9 @@ E464AA8F22E62D5B00969DBA /* Misc */ = { isa = PBXGroup; children = ( + E47B16F922F8DECC000C9C8B /* ARStatusViewController.storyboard */, + E47B16F822F8DECC000C9C8B /* ARStatusViewController.swift */, E464AA9022E62DC600969DBA /* Plane.swift */, - E42D375422F38F6F00CB6EA3 /* ARStatusTableViewController.swift */, - E42D375522F38F6F00CB6EA3 /* ARStatusTableViewController.xib */, ); path = Misc; sourceTree = ""; @@ -263,7 +263,7 @@ files = ( 883904491DF6022A001F3188 /* LaunchScreen.storyboard in Resources */, 883904461DF6022A001F3188 /* Assets.xcassets in Resources */, - E42D375722F38F6F00CB6EA3 /* ARStatusTableViewController.xib in Resources */, + E47B16FB22F8DECC000C9C8B /* ARStatusViewController.storyboard in Resources */, 883904441DF6022A001F3188 /* Main.storyboard in Resources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -297,8 +297,8 @@ E464AA9122E62DC600969DBA /* Plane.swift in Sources */, 88B689CB1E96EDF400B67FAB /* MeasureExample.swift in Sources */, 88DBC2A11FE83D6000255921 /* JobManagerExample.swift in Sources */, - E42D375622F38F6F00CB6EA3 /* ARStatusTableViewController.swift in Sources */, 88B689D11E96EDF400B67FAB /* VCListViewController.swift in Sources */, + E47B16FA22F8DECC000C9C8B /* ARStatusViewController.swift in Sources */, 8800656E2228577A00F76945 /* TemplatePickerExample.swift in Sources */, 88B689CE1E96EDF400B67FAB /* ScalebarExample.swift in Sources */, 88B689C91E96EDF400B67FAB /* ExamplesViewController.swift in Sources */, diff --git a/Examples/ArcGISToolkitExamples/ARExample.swift b/Examples/ArcGISToolkitExamples/ARExample.swift index d7cee3d8..7365bea4 100644 --- a/Examples/ArcGISToolkitExamples/ARExample.swift +++ b/Examples/ArcGISToolkitExamples/ARExample.swift @@ -27,7 +27,7 @@ class ARExample: UIViewController { private var currentSceneInfo: (sceneFunction: sceneInitFunction, label: String)? { didSet { guard let label = currentSceneInfo?.label else { return } - statusViewController.currentScene = label + statusViewController?.currentScene = label } } @@ -38,7 +38,11 @@ class ARExample: UIViewController { private var didHitTest: Bool = false // View controller displaying current status of `ARExample`. - private let statusViewController: ARStatusTableViewController = ARStatusTableViewController() + private let statusViewController: ARStatusViewController? = { + let storyBoard = UIStoryboard(name: "ARStatusViewController", bundle: nil) + let vc = storyBoard.instantiateInitialViewController() as? ARStatusViewController + return vc + }() /// Used when calculating framerate. private var lastUpdateTime: TimeInterval = 0 @@ -67,7 +71,7 @@ class ARExample: UIViewController { // Observe the `cameraController.translationFactor` property and update status when it changes. translationFactorObservation = arView.observe(\ArcGISARView.translationFactor, options: [.initial, .new]){ [weak self] arView, change in - self?.statusViewController.translationFactor = arView.translationFactor + self?.statusViewController?.translationFactor = arView.translationFactor } // Add arView to the view and setup the constraints. @@ -81,7 +85,7 @@ class ARExample: UIViewController { ]) // Create a toolbar and add it to the arView. - let toolbar = UIToolbar() + let toolbar = UIToolbar(frame: .zero) arView.addSubview(toolbar) toolbar.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ @@ -102,19 +106,22 @@ class ARExample: UIViewController { statusItem], animated: false) // Add the status view and setup constraints. - addChild(statusViewController) - view.addSubview(statusViewController.view) - statusViewController.didMove(toParent: self) - statusViewController.view.translatesAutoresizingMaskIntoConstraints = false - NSLayoutConstraint.activate([ - statusViewController.view.heightAnchor.constraint(equalToConstant: statusViewController.height()), - statusViewController.view.widthAnchor.constraint(equalToConstant: 350), - statusViewController.view.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -8), - statusViewController.view.bottomAnchor.constraint(equalTo: toolbar.topAnchor, constant: -8) - ]) - statusViewController.view.alpha = 0.0 + if let statusVC = statusViewController { + addChild(statusVC) + view.addSubview(statusVC.view) + statusVC.didMove(toParent: self) + statusVC.view.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([ + statusVC.view.heightAnchor.constraint(equalToConstant: 110), + statusVC.view.widthAnchor.constraint(equalToConstant: 350), + statusVC.view.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -8), + statusVC.view.bottomAnchor.constraint(equalTo: toolbar.topAnchor, constant: -8) + ]) + statusVC.view.alpha = 0.0 + } + // Set up the `sceneInfo` array with our scene init functions and labels. sceneInfo.append(contentsOf: [(sceneFunction: streetsScene, label: "Streets - Full Scale"), (sceneFunction: pointCloudScene, label: "Point Cloud - Tabletop"), @@ -131,7 +138,7 @@ class ARExample: UIViewController { super.viewDidAppear(animated) arView.startTracking { [weak self] (error) in if let error = error { - self?.statusViewController.errorMessage = error.localizedDescription + self?.statusViewController?.errorMessage = error.localizedDescription } } } @@ -171,7 +178,7 @@ class ARExample: UIViewController { /// Dislays the status view controller @objc func showStatus(_ sender: UIBarButtonItem){ UIView.animate(withDuration: 0.25) { [weak self] in - self?.statusViewController.view.alpha = self?.statusViewController.view.alpha == 1.0 ? 0.0 : 1.0 + self?.statusViewController?.view.alpha = self?.statusViewController?.view.alpha == 1.0 ? 0.0 : 1.0 } } } @@ -218,7 +225,7 @@ extension ARExample: ARSCNViewDelegate { let errorMessage = messages.compactMap({ $0 }).joined(separator: "\n") // Set the error message on the status vc. - statusViewController.errorMessage = errorMessage + statusViewController?.errorMessage = errorMessage DispatchQueue.main.async { [weak self] in // Present an alert describing the error. @@ -234,13 +241,13 @@ extension ARExample: ARSCNViewDelegate { func session(_ session: ARSession, cameraDidChangeTrackingState camera: ARCamera) { // Set the tracking state on the status vc. - statusViewController.trackingState = camera.trackingState + statusViewController?.trackingState = camera.trackingState } func renderer(_ renderer: SCNSceneRenderer, willRenderScene scene: SCNScene, atTime time: TimeInterval) { // Calculate frame rate and set on the statuc vc. let frametime = time - lastUpdateTime - statusViewController.frameRate = Int((1.0 / frametime).rounded()) + statusViewController?.frameRate = Int((1.0 / frametime).rounded()) lastUpdateTime = time } } @@ -311,7 +318,7 @@ extension ARExample { layer.load { [weak self] (error) in if let error = error { - self?.statusViewController.errorMessage = error.localizedDescription + self?.statusViewController?.errorMessage = error.localizedDescription return } @@ -342,7 +349,7 @@ extension ARExample { scene.operationalLayers.add(layer) scene.load { [weak self, weak scene] (error) in if let error = error { - self?.statusViewController.errorMessage = error.localizedDescription + self?.statusViewController?.errorMessage = error.localizedDescription return } @@ -353,14 +360,14 @@ extension ARExample { scene?.baseSurface?.elevationSources.first?.load { (error) in if let error = error { - self?.statusViewController.errorMessage = error.localizedDescription + self?.statusViewController?.errorMessage = error.localizedDescription return } // Find the elevation of the layer at the center point. scene?.baseSurface?.elevation(for: center, completion: { (elevation, error) in if let error = error { - self?.statusViewController.errorMessage = error.localizedDescription + self?.statusViewController?.errorMessage = error.localizedDescription return } @@ -390,7 +397,7 @@ extension ARExample { scene.operationalLayers.add(layer) scene.load { [weak self, weak scene] (error) in if let error = error { - self?.statusViewController.errorMessage = error.localizedDescription + self?.statusViewController?.errorMessage = error.localizedDescription return } @@ -401,14 +408,14 @@ extension ARExample { scene?.baseSurface?.elevationSources.first?.load { (error) in if let error = error { - self?.statusViewController.errorMessage = error.localizedDescription + self?.statusViewController?.errorMessage = error.localizedDescription return } // Find the elevation of the layer at the center point. scene?.baseSurface?.elevation(for: center, completion: { (elevation, error) in if let error = error { - self?.statusViewController.errorMessage = error.localizedDescription + self?.statusViewController?.errorMessage = error.localizedDescription return } diff --git a/Examples/ArcGISToolkitExamples/Misc/ARStatusTableViewController.xib b/Examples/ArcGISToolkitExamples/Misc/ARStatusTableViewController.xib deleted file mode 100644 index 13fb8130..00000000 --- a/Examples/ArcGISToolkitExamples/Misc/ARStatusTableViewController.xib +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Examples/ArcGISToolkitExamples/Misc/ARStatusViewController.storyboard b/Examples/ArcGISToolkitExamples/Misc/ARStatusViewController.storyboard new file mode 100644 index 00000000..0cd52976 --- /dev/null +++ b/Examples/ArcGISToolkitExamples/Misc/ARStatusViewController.storyboard @@ -0,0 +1,199 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Examples/ArcGISToolkitExamples/Misc/ARStatusTableViewController.swift b/Examples/ArcGISToolkitExamples/Misc/ARStatusViewController.swift similarity index 56% rename from Examples/ArcGISToolkitExamples/Misc/ARStatusTableViewController.swift rename to Examples/ArcGISToolkitExamples/Misc/ARStatusViewController.swift index 2969cd87..cdbe2a8d 100644 --- a/Examples/ArcGISToolkitExamples/Misc/ARStatusTableViewController.swift +++ b/Examples/ArcGISToolkitExamples/Misc/ARStatusViewController.swift @@ -34,13 +34,21 @@ extension ARCamera.TrackingState { } /// A view controller for display AR-related status information. -class ARStatusTableViewController: UITableViewController { +class ARStatusViewController: UITableViewController { + + @IBOutlet var trackingStateLabel: UILabel! + @IBOutlet var frameRateLabel: UILabel! + @IBOutlet var errorDescriptionLabel: UILabel! + @IBOutlet var sceneLabel: UILabel! + @IBOutlet var translationFactorLabel: UILabel! /// The `ARKit` camera tracking state. public var trackingState: ARCamera.TrackingState = .notAvailable { didSet { + guard trackingStateLabel != nil else { return } DispatchQueue.main.async{ [weak self] in - self?.tableView.reloadData() + guard let self = self else { return } + self.trackingStateLabel.text = self.trackingState.description } } } @@ -48,17 +56,20 @@ class ARStatusTableViewController: UITableViewController { /// The calculated frame rate of the `SceneView` and `ARKit` display. public var frameRate: Int = 0 { didSet { + guard frameRateLabel != nil else { return } DispatchQueue.main.async{ [weak self] in - self?.tableView.reloadData() + guard let self = self else { return } + self.frameRateLabel.text = "\(self.frameRate)" } } } - + /// The current error message. public var errorMessage: String = "None" { didSet { DispatchQueue.main.async{ [weak self] in - self?.tableView.reloadData() + guard let self = self else { return } + self.errorDescriptionLabel.text = self.errorMessage } } } @@ -67,80 +78,36 @@ class ARStatusTableViewController: UITableViewController { public var currentScene: String = "None" { didSet { DispatchQueue.main.async{ [weak self] in - self?.tableView.reloadData() + guard let self = self else { return } + self.sceneLabel.text = self.currentScene } } } - + /// The translation factor applied to the current scene. public var translationFactor: Double = 1.0 { didSet { DispatchQueue.main.async{ [weak self] in - self?.tableView.reloadData() + guard let self = self else { return } + self.translationFactorLabel.text = String(format: "%.2f", self.translationFactor) } } } - - /// The labels for each status item. - private let cellLabels = ["Tracking State", "Frame Rate", "Error", "Scene", "Translation Factor"] - - /// The height of our rows. - private let rowHeight: CGFloat = 24.0 - + override func viewDidLoad() { super.viewDidLoad() - // Set the rowHeight of our table view. - tableView.rowHeight = rowHeight - // Add a blur effect behind the table view. tableView.backgroundView = UIVisualEffectView(effect: UIBlurEffect(style: .regular)) } - - /// Calculates and returns the height of the table view based on the row height and number of rows. - /// - /// - Returns: The calculated height of the table view. - public func height() -> CGFloat { - return CGFloat(cellLabels.count) * rowHeight - } - + // MARK: - Table view data source - + override func numberOfSections(in tableView: UITableView) -> Int { return 1 } - + override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - return (section == 0) ? cellLabels.count : 0 - } - - override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - // Don't reuse cells, as our table is essentially static. - let statusCell = UITableViewCell(style: .value1, reuseIdentifier: nil) - statusCell.backgroundColor = .clear - statusCell.textLabel?.font = UIFont.systemFont(ofSize: 12.0) - statusCell.detailTextLabel?.font = UIFont.boldSystemFont(ofSize: 12.0) - statusCell.detailTextLabel?.textColor = .black - - statusCell.textLabel?.text = cellLabels[indexPath.row] - - var detailString = "" - switch indexPath.row { - case 0: - detailString = trackingState.description - case 1: - detailString = "\(self.frameRate)" - case 2: - detailString = errorMessage - case 3: - detailString = currentScene - case 4: - detailString = String(format: "%.2f", self.translationFactor) - default: - detailString = "" - } - statusCell.detailTextLabel?.text = detailString - - return statusCell + return 5 } } From 2756f05923590912ece02856807f2ea75ce099da Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Tue, 6 Aug 2019 11:17:31 -0500 Subject: [PATCH 094/147] Remove unnecessary table data source delgate methods. --- .../Misc/ARStatusViewController.swift | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/Examples/ArcGISToolkitExamples/Misc/ARStatusViewController.swift b/Examples/ArcGISToolkitExamples/Misc/ARStatusViewController.swift index cdbe2a8d..893414c8 100644 --- a/Examples/ArcGISToolkitExamples/Misc/ARStatusViewController.swift +++ b/Examples/ArcGISToolkitExamples/Misc/ARStatusViewController.swift @@ -100,14 +100,4 @@ class ARStatusViewController: UITableViewController { // Add a blur effect behind the table view. tableView.backgroundView = UIVisualEffectView(effect: UIBlurEffect(style: .regular)) } - - // MARK: - Table view data source - - override func numberOfSections(in tableView: UITableView) -> Int { - return 1 - } - - override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - return 5 - } } From 463fca61db5edf139929fc22a196ac4135e641cd Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Tue, 6 Aug 2019 12:30:32 -0500 Subject: [PATCH 095/147] Fix constraints/margins for notched phones --- .../ArcGISToolkitExamples/ARExample.swift | 2 +- .../Misc/ARStatusViewController.storyboard | 150 +++++++----------- .../Misc/ARStatusViewController.swift | 3 + 3 files changed, 64 insertions(+), 91 deletions(-) diff --git a/Examples/ArcGISToolkitExamples/ARExample.swift b/Examples/ArcGISToolkitExamples/ARExample.swift index 7365bea4..874fd9e4 100644 --- a/Examples/ArcGISToolkitExamples/ARExample.swift +++ b/Examples/ArcGISToolkitExamples/ARExample.swift @@ -115,7 +115,7 @@ class ARExample: UIViewController { NSLayoutConstraint.activate([ statusVC.view.heightAnchor.constraint(equalToConstant: 110), statusVC.view.widthAnchor.constraint(equalToConstant: 350), - statusVC.view.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -8), + statusVC.view.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -8), statusVC.view.bottomAnchor.constraint(equalTo: toolbar.topAnchor, constant: -8) ]) diff --git a/Examples/ArcGISToolkitExamples/Misc/ARStatusViewController.storyboard b/Examples/ArcGISToolkitExamples/Misc/ARStatusViewController.storyboard index 0cd52976..97db8d0e 100644 --- a/Examples/ArcGISToolkitExamples/Misc/ARStatusViewController.storyboard +++ b/Examples/ArcGISToolkitExamples/Misc/ARStatusViewController.storyboard @@ -20,125 +20,130 @@ - + - - - - - - - - - + - - - - - - - - - + - - - - - - - - - + - - - - - - - - - + - - - - - - - - @@ -149,49 +154,14 @@ - - - - - + + + + + - - - - - diff --git a/Examples/ArcGISToolkitExamples/Misc/ARStatusViewController.swift b/Examples/ArcGISToolkitExamples/Misc/ARStatusViewController.swift index 893414c8..b98e378b 100644 --- a/Examples/ArcGISToolkitExamples/Misc/ARStatusViewController.swift +++ b/Examples/ArcGISToolkitExamples/Misc/ARStatusViewController.swift @@ -67,6 +67,7 @@ class ARStatusViewController: UITableViewController { /// The current error message. public var errorMessage: String = "None" { didSet { + guard errorDescriptionLabel != nil else { return } DispatchQueue.main.async{ [weak self] in guard let self = self else { return } self.errorDescriptionLabel.text = self.errorMessage @@ -77,6 +78,7 @@ class ARStatusViewController: UITableViewController { /// The label for the currently selected scene. public var currentScene: String = "None" { didSet { + guard sceneLabel != nil else { return } DispatchQueue.main.async{ [weak self] in guard let self = self else { return } self.sceneLabel.text = self.currentScene @@ -87,6 +89,7 @@ class ARStatusViewController: UITableViewController { /// The translation factor applied to the current scene. public var translationFactor: Double = 1.0 { didSet { + guard translationFactorLabel != nil else { return } DispatchQueue.main.async{ [weak self] in guard let self = self else { return } self.translationFactorLabel.text = String(format: "%.2f", self.translationFactor) From f4bae65e9d327a15d8e9471cb204e864602e31c7 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Tue, 13 Aug 2019 15:25:45 -0500 Subject: [PATCH 096/147] Altitude change. --- Toolkit/ArcGISToolkit/AR/ArcGISARView.swift | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift b/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift index 2cf9554e..c20c5600 100644 --- a/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift +++ b/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift @@ -448,7 +448,14 @@ extension ArcGISARView: AGSLocationChangeHandlerDelegate { public func locationDataSource(_ locationDataSource: AGSLocationDataSource, locationDidChange location: AGSLocation) { // Location changed. - guard let locationPoint = location.position else { return } + guard var locationPoint = location.position else { return } + + // The AGSCLLocationDataSource does not include altitude information from the CLLocation when + // creating the `AGSLocation` geometry, so grab the altitude directly from the CLLocationManager. + if let clLocationDataSource = locationDataSource as? AGSCLLocationDataSource, + let altitude = clLocationDataSource.locationManager.location?.altitude { + locationPoint = AGSPoint(x: locationPoint.x, y: locationPoint.y, z: altitude, spatialReference: locationPoint.spatialReference) + } if initialLocation == nil { initialLocation = locationPoint From c75b7a4309e9ea3dc062f836c2d2648e993c1c20 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Wed, 14 Aug 2019 15:48:53 -0500 Subject: [PATCH 097/147] Transformation and hit test changes; add UserDirections and CalibrationView code. --- .../ArcGISToolkitExamples/ARExample.swift | 359 +++++++++++++++--- Toolkit/ArcGISToolkit/AR/ArcGISARView.swift | 49 ++- 2 files changed, 330 insertions(+), 78 deletions(-) diff --git a/Examples/ArcGISToolkitExamples/ARExample.swift b/Examples/ArcGISToolkitExamples/ARExample.swift index 874fd9e4..875d4a9d 100644 --- a/Examples/ArcGISToolkitExamples/ARExample.swift +++ b/Examples/ArcGISToolkitExamples/ARExample.swift @@ -19,12 +19,13 @@ import ArcGIS class ARExample: UIViewController { typealias sceneInitFunction = () -> AGSScene + typealias sceneInfoType = (sceneFunction: sceneInitFunction, label: String, tableTop: Bool) - /// The scene creation functions plus labels. The functions create a new scene and perform any necessary `ArcGISARView` initialization. This allows for changing the scene and AR "mode" (table top or full-scale). - private var sceneInfo: [(sceneFunction: sceneInitFunction, label: String)] = [] + /// The scene creation functions plus labels and whehter it represents a table top experience. The functions create a new scene and perform any necessary `ArcGISARView` initialization. This allows for changing the scene and AR "mode" (table top or full-scale). + private var sceneInfo: [sceneInfoType] = [] /// The current scene info. - private var currentSceneInfo: (sceneFunction: sceneInitFunction, label: String)? { + private var currentSceneInfo: sceneInfoType? { didSet { guard let label = currentSceneInfo?.label else { return } statusViewController?.currentScene = label @@ -50,13 +51,21 @@ class ARExample: UIViewController { /// Overlay used to display user-placed graphics. private let graphicsOverlay: AGSGraphicsOverlay = { let overlay = AGSGraphicsOverlay() - overlay.sceneProperties = AGSLayerSceneProperties(surfacePlacement: .relative) + overlay.sceneProperties = AGSLayerSceneProperties(surfacePlacement: .absolute) return overlay }() - /// The observer for the `SceneView`'s `translationFactor` property + /// View for displaying directions to the user. + private let userDirectionsView = UserDirectionsView(effect: UIBlurEffect(style: .light)) + + /// The observer for the `SceneView`'s `translationFactor` property. private var translationFactorObservation: NSKeyValueObservation? + /// Denotes whether we're in calibration mode. + private var isCalibrating = false + private var calibrationView: CalibrationView? + + private var toolbar = UIToolbar(frame: .zero) override func viewDidLoad() { super.viewDidLoad() @@ -66,6 +75,12 @@ class ARExample: UIViewController { // Set ourself as touch delegate so we can get touch events. arView.sceneView.touchDelegate = self + // Disble user interactions on the sceneView. + arView.sceneView.interactionOptions.isEnabled = false + + // Set ourself as the ARKit session delegate. + arView.arSCNView.session.delegate = self + // Add our graphics overlay to the sceneView. arView.sceneView.graphicsOverlays.add(graphicsOverlay) @@ -84,29 +99,10 @@ class ARExample: UIViewController { arView.bottomAnchor.constraint(equalTo: view.bottomAnchor) ]) - // Create a toolbar and add it to the arView. - let toolbar = UIToolbar(frame: .zero) - arView.addSubview(toolbar) - toolbar.translatesAutoresizingMaskIntoConstraints = false - NSLayoutConstraint.activate([ - toolbar.leadingAnchor.constraint(equalTo: arView.sceneView.leadingAnchor), - toolbar.trailingAnchor.constraint(equalTo: arView.sceneView.trailingAnchor), - toolbar.bottomAnchor.constraint(equalTo: arView.sceneView.attributionTopAnchor) - ]) - - // Create a toolbar button to change the current scene. - let sceneItem = UIBarButtonItem(title: "Change Scene", style: .plain, target: self, action: #selector(changeScene(_:))) - - // Create a toolbar button to display the status. - let statusItem = UIBarButtonItem(title: "Status", style: .plain, target: self, action: #selector(showStatus(_:))) - - toolbar.setItems([UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil), - sceneItem, - UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil), - statusItem], animated: false) + // Add a Toolbar for changing the scene and showing the status view. + toolbar = addToolbar() // Add the status view and setup constraints. - if let statusVC = statusViewController { addChild(statusVC) view.addSubview(statusVC.view) @@ -123,11 +119,18 @@ class ARExample: UIViewController { } // Set up the `sceneInfo` array with our scene init functions and labels. - sceneInfo.append(contentsOf: [(sceneFunction: streetsScene, label: "Streets - Full Scale"), - (sceneFunction: pointCloudScene, label: "Point Cloud - Tabletop"), - (sceneFunction: yosemiteScene, label: "Yosemite - Tabletop"), - (sceneFunction: borderScene, label: "US - Mexico Border - Tabletop"), - (sceneFunction: emptyScene, label: "Empty - Full Scale")]) + sceneInfo.append(contentsOf: [(sceneFunction: streetsScene, label: "Streets - Full Scale", tableTop: false), + (sceneFunction: imageryScene, label: "Imagery - Full Scale", tableTop: false), + (sceneFunction: pointCloudScene, label: "Point Cloud - Tabletop", tableTop: true), + (sceneFunction: yosemiteScene, label: "Yosemite - Tabletop", tableTop: true), + (sceneFunction: borderScene, label: "US - Mexico Border - Tabletop", tableTop: true), + (sceneFunction: emptyScene, label: "Empty - Full Scale", tableTop: false)]) + + // Add the UserDirectionsView. + addUserDirectionsView() + + // Add the CalibrationView. + addCalibrationView() // Use the first sceneInfo to create and set the scene. currentSceneInfo = sceneInfo.first @@ -148,6 +151,40 @@ class ARExample: UIViewController { arView.stopTracking() } + var originalGestures: [UIGestureRecognizer]? + + /// Initiatest scene location calibration. + /// + /// - Parameter sender: The bar button item tapped on. + @objc func calibration(_ sender: UIBarButtonItem) { + isCalibrating = !isCalibrating + + arView.sceneView.interactionOptions.isEnabled = isCalibrating + userDirectionsView.updateUserDirections(/*isCalibrating ? "Calibrating...." : */"") + + // Do calibration work... + UIView.animate(withDuration: 0.25) { [weak self] in + self?.calibrationView?.alpha = (self?.isCalibrating ?? false) ? 1.0 : 0.0 + } + + // Do calibration work... + UIView.animate(withDuration: 0.25) { [weak self] in + self?.arView.sceneView.alpha = (self?.isCalibrating ?? false) ? 0.65 : 1.0 + } + +// if isCalibrating { +// arView.stopTracking() +// } +// else { +// // Done calibrating, start tracking again. +// arView.startTracking { [weak self] (error) in +// if let error = error { +// self?.statusViewController?.errorMessage = error.localizedDescription +// } +// } +// } + } + /// Changes the scene to a newly selected scene. /// /// - Parameter sender: The bar button item tapped on. @@ -155,34 +192,70 @@ class ARExample: UIViewController { // Display an alert controller displaying the scenes to choose from. let alertController = UIAlertController(title: nil, message: nil, preferredStyle: UIAlertController.Style.actionSheet) alertController.popoverPresentationController?.barButtonItem = sender - sceneInfo.forEach { (sceneFunction, label) in - let action = UIAlertAction(title: label, style: .default, handler: { [weak self] (action) in + sceneInfo.forEach { info in + let action = UIAlertAction(title: info.label, style: .default, handler: { [weak self] (action) in // Set currentSceneInfo to the selected scene. - self?.currentSceneInfo = (sceneFunction, label) + self?.currentSceneInfo = info // Stop tracking, update the scene with the selected Scene and reset tracking. self?.arView.stopTracking() - self?.arView.sceneView.scene = sceneFunction() + self?.arView.sceneView.scene = info.sceneFunction() + if info.tableTop { + // Dim the SceneView until the user taps on a surface. + self?.arView.sceneView.alpha = 0.5 + } self?.arView.resetTracking() // Reset didHitTest variable self?.didHitTest = false }) // Display current scene as disabled. - action.isEnabled = !(label == currentSceneInfo?.label) + action.isEnabled = !(info.label == currentSceneInfo?.label) alertController.addAction(action) } present(alertController, animated: true) } /// Dislays the status view controller + /// + /// - Parameter sender: The bar button item tapped on. @objc func showStatus(_ sender: UIBarButtonItem){ UIView.animate(withDuration: 0.25) { [weak self] in self?.statusViewController?.view.alpha = self?.statusViewController?.view.alpha == 1.0 ? 0.0 : 1.0 } } + + private func addToolbar() -> UIToolbar { + // Create a toolbar and add it to the arView. + let toolbar = UIToolbar(frame: .zero) + arView.addSubview(toolbar) + toolbar.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([ + toolbar.leadingAnchor.constraint(equalTo: arView.sceneView.leadingAnchor), + toolbar.trailingAnchor.constraint(equalTo: arView.sceneView.trailingAnchor), + toolbar.bottomAnchor.constraint(equalTo: arView.sceneView.attributionTopAnchor) + ]) + + // Create a toolbar button for calibration. + let calibrationItem = UIBarButtonItem(title: "Calibration", style: .plain, target: self, action: #selector(calibration(_:))) + + // Create a toolbar button to change the current scene. + let sceneItem = UIBarButtonItem(title: "Change Scene", style: .plain, target: self, action: #selector(changeScene(_:))) + + // Create a toolbar button to display the status. + let statusItem = UIBarButtonItem(title: "Status", style: .plain, target: self, action: #selector(showStatus(_:))) + + toolbar.setItems([calibrationItem, + UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil), + sceneItem, + UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil), + statusItem], animated: false) + + return toolbar + } } +// MARK: ARSCNViewDelegate extension ARExample: ARSCNViewDelegate { func renderer(_ renderer: SCNSceneRenderer, didAdd node: SCNNode, for anchor: ARAnchor) { @@ -242,9 +315,10 @@ extension ARExample: ARSCNViewDelegate { func session(_ session: ARSession, cameraDidChangeTrackingState camera: ARCamera) { // Set the tracking state on the status vc. statusViewController?.trackingState = camera.trackingState + updateUserDirections(session.currentFrame!, trackingState: camera.trackingState) } - - func renderer(_ renderer: SCNSceneRenderer, willRenderScene scene: SCNScene, atTime time: TimeInterval) { + xxx need willrenderScene + func renderer(_ renderer: SCNSceneRenderer, didRenderScene scene: SCNScene, atTime time: TimeInterval) { // Calculate frame rate and set on the statuc vc. let frametime = time - lastUpdateTime statusViewController?.frameRate = Int((1.0 / frametime).rounded()) @@ -252,28 +326,61 @@ extension ARExample: ARSCNViewDelegate { } } +// MARK: ARSessionDelegate +extension ARExample: ARSessionDelegate { + func session(_ session: ARSession, didAdd anchors: [ARAnchor]) { + guard let frame = session.currentFrame else { return } + updateUserDirections(frame, trackingState: frame.camera.trackingState) + } + + func session(_ session: ARSession, didRemove anchors: [ARAnchor]) { + guard let frame = session.currentFrame else { return } + updateUserDirections(frame, trackingState: frame.camera.trackingState) + } +} + +// MARK: AGSGeoViewTouchDelegate extension ARExample: AGSGeoViewTouchDelegate { public func geoView(_ geoView: AGSGeoView, didTapAtScreenPoint screenPoint: CGPoint, mapPoint: AGSPoint) { - guard !didHitTest else { return } - - if let _ = arView.locationDataSource { - // We have a location data source, so we're in full-scale AR mode. - // Get the real world location for screen point from arView. - guard let point = arView.arScreenToLocation(screenPoint: screenPoint) else { return } + guard let sceneInfo = currentSceneInfo, !didHitTest else { return } - let sym = AGSSimpleMarkerSceneSymbol(style: .sphere, color: .yellow, height: 1.0, width: 1.0, depth: 1.0, anchorPosition: .bottom) - let graphic = AGSGraphic(geometry: point, symbol: sym, attributes: nil) - graphicsOverlay.graphics.add(graphic) - } - else { - // We do not have a location data source, so we're in table-top mode. + let colors:[UIColor] = [.red, .blue, .yellow, .green] + if sceneInfo.tableTop { + // We're in table-top mode. Place the scene at the given point by setting the initial transformation. if arView.setInitialTransformation(using: screenPoint) { + // Show the SceneView now that the user has tapped on the surface. + UIView.animate(withDuration: 0.5) { [weak self] in + self?.arView.sceneView.alpha = 1.0 + } + userDirectionsView.updateUserDirections(nil) didHitTest = true } } + else { + // We're in full-scale AR mode. Get the real world location for screen point from arView. + guard let point = arView.arScreenToLocation(screenPoint: screenPoint) else { return } + + + print("point = \(point)") + + // Create and place a graphic at the real world location. + let sphere = AGSSimpleMarkerSceneSymbol(style: .sphere, color: colors[hitCount], height: 0.25, width: 0.25, depth: 0.25, anchorPosition: .bottom) + let shadow = AGSSimpleMarkerSceneSymbol(style: .sphere, color: .lightGray, height: 0.01, width: 0.25, depth: 0.25, anchorPosition: .center) + let sphereGraphic = AGSGraphic(geometry: point, symbol: sphere, attributes: nil) + let shadowGraphic = AGSGraphic(geometry: point, symbol: shadow, attributes: nil) + graphicsOverlay.graphics.add(shadowGraphic + + ) + graphicsOverlay.graphics.add(sphereGraphic) + hitCount += 1 + if hitCount > 3 { + hitCount = 0 + } + } } } +// MARK: UIAdaptivePresentationControllerDelegate extension ARExample: UIAdaptivePresentationControllerDelegate { func adaptivePresentationStyle(for controller: UIPresentationController, traitCollection: UITraitCollection) -> UIModalPresentationStyle { // show presented controller as popovers even on small displays @@ -281,6 +388,114 @@ extension ARExample: UIAdaptivePresentationControllerDelegate { } } +// MARK: User Directions View +extension ARExample { + + func addUserDirectionsView() { + // Add userDirectionsView to superView and setup constraints. + view.addSubview(userDirectionsView) + userDirectionsView.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([ + userDirectionsView.centerXAnchor.constraint(equalTo: view.centerXAnchor), + userDirectionsView.topAnchor.constraint(equalTo: view.topAnchor, constant: 88.0) + ]) + } + + private func updateUserDirections(_ frame: ARFrame, trackingState: ARCamera.TrackingState) { + var message = "" + + switch trackingState { + case .normal where frame.anchors.isEmpty: + if let sceneInfo = currentSceneInfo, sceneInfo.tableTop, !didHitTest { + message = "Move the device around to detect horizontal surfaces." + } + break + case .normal where !frame.anchors.isEmpty: + if let sceneInfo = currentSceneInfo, sceneInfo.tableTop, !didHitTest { + message = "Tap to place the Scene on a surface." + } + break + case .notAvailable: + message = "Location not available." + break + case .limited(let reason): + switch(reason){ + case .excessiveMotion: + message = "Try moving your device more slowly." + break + case .initializing: + message = "Keep moving your device." + break + case .insufficientFeatures: + message = "Try turning on more lights and moving around." + break + default: + break + } + default: + break + } + + userDirectionsView.updateUserDirections(message) + } +} + +// MARK: Calibration View +extension ARExample { + + func addCalibrationView() { + // Add calibrationView to superView and setup constraints. + guard let cc = arView.sceneView.cameraController as? AGSTransformationMatrixCameraController else { return } + calibrationView = CalibrationView(sceneView: arView.sceneView, cameraController: cc) + guard let calibrationView = calibrationView else { return } + view.addSubview(calibrationView) + calibrationView.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([ + calibrationView.leadingAnchor.constraint(equalTo: view.leadingAnchor), + calibrationView.trailingAnchor.constraint(equalTo: view.trailingAnchor), + calibrationView.topAnchor.constraint(equalTo: view.topAnchor), + calibrationView.bottomAnchor.constraint(equalTo: toolbar.topAnchor) + ]) + + calibrationView.alpha = 0.0 + +// let elevationSlider: UISlider = { +// let slider = UISlider(frame: .zero) +// slider.minimumValue = -100.0 +// slider.maximumValue = 100.0 +// return slider +// }() +// +// let headingSlider: UISlider = { +// let slider = UISlider(frame: .zero) +// slider.minimumValue = -180.0 +// slider.maximumValue = 180.0 +// return slider +// }() +// +// addSubview(elevationSlider) +// elevationSlider.addTarget(self, action: #selector(elevationChanged(_:)), for: .valueChanged) +// elevationSlider.translatesAutoresizingMaskIntoConstraints = false +// NSLayoutConstraint.activate([ +// elevationSlider.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -8), +// // elevationSlider.topAnchor.constraint(equalTo: topAnchor, constant: 8), +// elevationSlider.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -8) +// ]) +// +// addSubview(headingSlider) +// headingSlider.addTarget(self, action: #selector(headingChanged(_:)), for: .valueChanged) +// headingSlider.translatesAutoresizingMaskIntoConstraints = false +// NSLayoutConstraint.activate([ +// headingSlider.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 8), +// headingSlider.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -8), +// // elevationSlider.topAnchor.constraint(equalTo: topAnchor, constant: 8), +// headingSlider.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -8) +// ]) + + } +} + +// MARK: Scene creation methods extension ARExample { // // These methods create the scenes and perform other intitialization required to set up the AR experiences. @@ -294,7 +509,24 @@ extension ARExample { // Create scene with the streets basemap. let scene = AGSScene(basemapType: .streets) - addElevationSource(toScene: scene) + scene.addElevationSource() + + // Set the location data source so we use our GPS location as the originCamera. + arView.locationDataSource = AGSCLLocationDataSource() + arView.originCamera = nil + arView.translationFactor = 1 + return scene + } + + /// Creates a scene based on the ImageryWithLabels base map. + /// Mode: Full-Scale AR + /// + /// - Returns: The new scene. + private func imageryScene() -> AGSScene { + + // Create scene with the streets basemap. + let scene = AGSScene(basemapType: .imageryWithLabels) + scene.addElevationSource() // Set the location data source so we use our GPS location as the originCamera. arView.locationDataSource = AGSCLLocationDataSource() @@ -313,7 +545,7 @@ extension ARExample { let portalItem = AGSPortalItem(portal: portal, itemID: "fc3f4a4919394808830cd11df4631a54") let layer = AGSPointCloudLayer(item: portalItem) let scene = AGSScene() - addElevationSource(toScene: scene) + scene.addElevationSource() scene.operationalLayers.add(layer) layer.load { [weak self] (error) in @@ -326,7 +558,7 @@ extension ARExample { let center = extent.center // Create the origin camera at the center point of the data. This will ensure the data is anchored to the table. - let camera = AGSCamera(latitude: center.y, longitude: center.x, altitude: 0, heading: 0, pitch: 0, roll: 0) + let camera = AGSCamera(latitude: center.y, longitude: center.x, altitude: 0, heading: 0, pitch: 90.0, roll: 0) self?.arView.originCamera = camera self?.arView.translationFactor = 2000 } @@ -342,7 +574,7 @@ extension ARExample { /// - Returns: The new scene. private func yosemiteScene() -> AGSScene { let scene = AGSScene() - addElevationSource(toScene: scene) + scene.addElevationSource() // Create the Yosemite layer. let layer = AGSIntegratedMeshLayer(url: URL(string:"https://tiles.arcgis.com/tiles/FQD0rKU8X5sAQfh8/arcgis/rest/services/VRICON_Yosemite_Sample_Integrated_Mesh_scene_layer/SceneServer")!) @@ -372,9 +604,9 @@ extension ARExample { } // Create the origin camera at the center point and elevation of the data. This will ensure the data is anchored to the table. - let camera = AGSCamera(latitude: center.y, longitude: center.x, altitude: elevation, heading: 0, pitch: 0, roll: 0) + let camera = AGSCamera(latitude: center.y, longitude: center.x, altitude: elevation, heading: 0, pitch: 90, roll: 0) self?.arView.originCamera = camera - self?.arView.translationFactor = 1000 + self?.arView.translationFactor = 18000 }) } } @@ -390,7 +622,7 @@ extension ARExample { /// - Returns: The new scene. private func borderScene() -> AGSScene { let scene = AGSScene() - addElevationSource(toScene: scene) + scene.addElevationSource() // Create the border layer. let layer = AGSIntegratedMeshLayer(url: URL(string:"https://tiles.arcgis.com/tiles/FQD0rKU8X5sAQfh8/arcgis/rest/services/VRICON_SW_US_Sample_Integrated_Mesh_scene_layer/SceneServer")!) @@ -420,7 +652,7 @@ extension ARExample { } // Create the origin camera at the center point and elevation of the data. This will ensure the data is anchored to the table. - let camera = AGSCamera(latitude: center.y, longitude: center.x, altitude: elevation, heading: 0, pitch: 0, roll: 0) + let camera = AGSCamera(latitude: center.y, longitude: center.x, altitude: elevation, heading: 0, pitch: 90.0, roll: 0) self?.arView.originCamera = camera self?.arView.translationFactor = 1000 }) @@ -438,7 +670,7 @@ extension ARExample { /// - Returns: The new scene. private func emptyScene() -> AGSScene { let scene = AGSScene() - addElevationSource(toScene: scene) + scene.addElevationSource() // Set the location data source so we use our GPS location as the originCamera. arView.locationDataSource = AGSCLLocationDataSource() @@ -446,17 +678,22 @@ extension ARExample { arView.translationFactor = 1 return scene } +} +// MARK: AGSScene extension. +extension AGSScene { /// Adds an elevation source to the given `scene`. /// /// - Parameter scene: The scene to add the elevation source to. - private func addElevationSource(toScene scene: AGSScene) { + public func addElevationSource() { let elevationSource = AGSArcGISTiledElevationSource(url: URL(string: "https://elevation3d.arcgis.com/arcgis/rest/services/WorldElevation3D/Terrain3D/ImageServer")!) let surface = AGSSurface() surface.elevationSources = [elevationSource] surface.name = "baseSurface" surface.isEnabled = true surface.backgroundGrid.isVisible = false - scene.baseSurface = surface + surface.navigationConstraint = .none + baseSurface = surface } } + diff --git a/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift b/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift index c20c5600..c6ecd93b 100644 --- a/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift +++ b/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift @@ -93,9 +93,6 @@ public class ArcGISARView: UIView { /// Used when calculating framerate. private var lastUpdateTime: TimeInterval = 0 - /// A quaternion used to compensate for the pitch being 90 degrees on `ARKit`; used to calculate the current device transformation for each frame. - private let compensationQuat: simd_quatd = simd_quatd(ix: (sin(45 / (180 / .pi))), iy: 0, iz: 0, r: (cos(45 / (180 / .pi)))) - /// Whether `ARKit` is supported on this device. private let deviceSupportsARKit: Bool = { return ARWorldTrackingConfiguration.isSupported @@ -190,19 +187,34 @@ public class ArcGISARView: UIView { /// - Returns: The map point corresponding to screenPoint. public func arScreenToLocation(screenPoint: CGPoint) -> AGSPoint? { // Use the `internalHitTest` method to get the matrix of `screenPoint`. - guard let matrix = internalHitTest(screenPoint: screenPoint) else { return nil } + guard let localOffsetMatrix = internalHitTest(screenPoint: screenPoint) else { return nil } - // Get the TransformationMatrix from the sceneView.currentViewpointCamera and add the hit test matrix to it. - let currentCamera = sceneView.currentViewpointCamera() - let transformationMatrix = currentCamera.transformationMatrix.addTransformation(matrix) + print("Local offset XYZ, World origin XYZ, Combined world coordinate XYZ") + + //TODO: generalize the debug print function + // print("lqx: \(localOffsetMatrix.quaternionX); lqy: \(localOffsetMatrix.quaternionY); lqz: \(localOffsetMatrix.quaternionZ); lqw: \(localOffsetMatrix.quaternionW); ltx: \(localOffsetMatrix.translationX); lty: \(localOffsetMatrix.translationY); ltz: \(localOffsetMatrix.translationZ)") + print("\(localOffsetMatrix.translationX) \(localOffsetMatrix.translationY) \(localOffsetMatrix.translationZ)") + + let currOriginCamera = cameraController.originCamera + let currOriginMatrix = currOriginCamera.transformationMatrix + + // print("oqx: \(currOriginMatrix.quaternionX); oqy: \(currOriginMatrix.quaternionY); oqz: \(currOriginMatrix.quaternionZ); oqw: \(currOriginMatrix.quaternionW); otx: \(currOriginMatrix.translationX); oty: \(currOriginMatrix.translationY); otz: \(currOriginMatrix.translationZ)") + print("\(currOriginMatrix.translationX) \(currOriginMatrix.translationY) \(currOriginMatrix.translationZ)") + + //TODO: for tabletop application scale translation by TranslationFactor + let mapPointMatrix = currOriginMatrix.addTransformation(localOffsetMatrix) + + // print("cqx: \(mapPointMatrix.quaternionX); cqy: \(mapPointMatrix.quaternionY); cqz: \(mapPointMatrix.quaternionZ); cqw: \(mapPointMatrix.quaternionW); ctx: \(mapPointMatrix.translationX); cty: \(mapPointMatrix.translationY); ctz: \(mapPointMatrix.translationZ)") + print("\(mapPointMatrix.translationX) \(mapPointMatrix.translationY) \(mapPointMatrix.translationZ)") // Create a camera from transformationMatrix and return it's location. - return AGSCamera(transformationMatrix: transformationMatrix).location + return AGSCamera(transformationMatrix: mapPointMatrix).location } /// Resets the device tracking, using `originCamera` if it's not nil or the device's GPS location via the location data source. public func resetTracking() { initialLocation = nil + initialTransformation = .identity startTracking() } @@ -255,7 +267,7 @@ public class ArcGISARView: UIView { guard let strongSelf = self else { return } // Run the ARSession. if strongSelf.isUsingARKit { - strongSelf.arSCNView.session.run(strongSelf.arConfiguration, options: .resetTracking) + strongSelf.arSCNView.session.run(strongSelf.arConfiguration, options: [.resetTracking]) } strongSelf.isTracking = true @@ -289,6 +301,9 @@ public class ArcGISARView: UIView { guard let worldTransform = results.first?.worldTransform else { return nil } // Create our hit test matrix based on the worldTransform location. + // right now we ignore the orientation of the plane that was hit to find the point + // since we only use horizontal planes, when we will start using vertical planes + // we should stop suppressing the quternion rotation to a null rotation (0,0,0,1) let hitTestMatrix = AGSTransformationMatrix(quaternionX: 0.0, quaternionY: 0.0, quaternionZ: 0.0, @@ -383,15 +398,14 @@ extension ArcGISARView: SCNSceneRendererDelegate { guard let transform = arSCNView.pointOfView?.transform else { return } let cameraTransform = simd_double4x4(transform) - // Calculate our final quaternion and create the new transformation matrix. - let finalQuat:simd_quatd = simd_mul(compensationQuat, simd_quatd(cameraTransform)) - let transformationMatrix = AGSTransformationMatrix(quaternionX: finalQuat.vector.x, - quaternionY: finalQuat.vector.y, - quaternionZ: finalQuat.vector.z, - quaternionW: finalQuat.vector.w, + let cameraQuat:simd_quatd = simd_quatd(cameraTransform) + let transformationMatrix = AGSTransformationMatrix(quaternionX: cameraQuat.vector.x, + quaternionY: cameraQuat.vector.y, + quaternionZ: cameraQuat.vector.z, + quaternionW: cameraQuat.vector.w, translationX: cameraTransform.columns.3.x, - translationY: -cameraTransform.columns.3.z, - translationZ: cameraTransform.columns.3.y) + translationY: cameraTransform.columns.3.y, + translationZ: cameraTransform.columns.3.z) // Set the matrix on the camera controller. cameraController.transformationMatrix = initialTransformation.addTransformation(transformationMatrix) @@ -475,3 +489,4 @@ extension ArcGISARView: AGSLocationChangeHandlerDelegate { // print("locationDataSource status changed: \(status.rawValue)") } } + From 992854a3487e770a00b9565573e556f116f10100 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Wed, 14 Aug 2019 15:50:43 -0500 Subject: [PATCH 098/147] All CalibrationView and UserDirectionsView files. --- .../project.pbxproj | 8 + .../Misc/CalibrationView.swift | 367 ++++++++++++++++++ .../Misc/UserDirectionsView.swift | 59 +++ 3 files changed, 434 insertions(+) create mode 100644 Examples/ArcGISToolkitExamples/Misc/CalibrationView.swift create mode 100644 Examples/ArcGISToolkitExamples/Misc/UserDirectionsView.swift diff --git a/Examples/ArcGISToolkitExamples.xcodeproj/project.pbxproj b/Examples/ArcGISToolkitExamples.xcodeproj/project.pbxproj index 2f4b9d8a..d0f55ead 100644 --- a/Examples/ArcGISToolkitExamples.xcodeproj/project.pbxproj +++ b/Examples/ArcGISToolkitExamples.xcodeproj/project.pbxproj @@ -28,6 +28,8 @@ E46893271FEDAE29008ADA79 /* CompassExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = E46893261FEDAE29008ADA79 /* CompassExample.swift */; }; E47B16FA22F8DECC000C9C8B /* ARStatusViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E47B16F822F8DECC000C9C8B /* ARStatusViewController.swift */; }; E47B16FB22F8DECC000C9C8B /* ARStatusViewController.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = E47B16F922F8DECC000C9C8B /* ARStatusViewController.storyboard */; }; + E47B17362304AB7D000C9C8B /* UserDirectionsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E47B17342304AB7D000C9C8B /* UserDirectionsView.swift */; }; + E47B17372304AB7D000C9C8B /* CalibrationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E47B17352304AB7D000C9C8B /* CalibrationView.swift */; }; E48405751E9BE7E600927208 /* LegendExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = E48405741E9BE7E600927208 /* LegendExample.swift */; }; /* End PBXBuildFile section */ @@ -92,6 +94,8 @@ E46893261FEDAE29008ADA79 /* CompassExample.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompassExample.swift; sourceTree = ""; }; E47B16F822F8DECC000C9C8B /* ARStatusViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ARStatusViewController.swift; sourceTree = ""; }; E47B16F922F8DECC000C9C8B /* ARStatusViewController.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = ARStatusViewController.storyboard; sourceTree = ""; }; + E47B17342304AB7D000C9C8B /* UserDirectionsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UserDirectionsView.swift; sourceTree = ""; }; + E47B17352304AB7D000C9C8B /* CalibrationView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CalibrationView.swift; sourceTree = ""; }; E48405741E9BE7E600927208 /* LegendExample.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LegendExample.swift; sourceTree = ""; }; /* End PBXFileReference section */ @@ -169,6 +173,8 @@ E464AA8F22E62D5B00969DBA /* Misc */ = { isa = PBXGroup; children = ( + E47B17352304AB7D000C9C8B /* CalibrationView.swift */, + E47B17342304AB7D000C9C8B /* UserDirectionsView.swift */, E47B16F922F8DECC000C9C8B /* ARStatusViewController.storyboard */, E47B16F822F8DECC000C9C8B /* ARStatusViewController.swift */, E464AA9022E62DC600969DBA /* Plane.swift */, @@ -294,12 +300,14 @@ 883EA74B20741A56006D6F72 /* PopupExample.swift in Sources */, 88B689C81E96EDF400B67FAB /* AppDelegate.swift in Sources */, 2140781E209B629000FBFDCC /* TimeSliderExample.swift in Sources */, + E47B17372304AB7D000C9C8B /* CalibrationView.swift in Sources */, E464AA9122E62DC600969DBA /* Plane.swift in Sources */, 88B689CB1E96EDF400B67FAB /* MeasureExample.swift in Sources */, 88DBC2A11FE83D6000255921 /* JobManagerExample.swift in Sources */, 88B689D11E96EDF400B67FAB /* VCListViewController.swift in Sources */, E47B16FA22F8DECC000C9C8B /* ARStatusViewController.swift in Sources */, 8800656E2228577A00F76945 /* TemplatePickerExample.swift in Sources */, + E47B17362304AB7D000C9C8B /* UserDirectionsView.swift in Sources */, 88B689CE1E96EDF400B67FAB /* ScalebarExample.swift in Sources */, 88B689C91E96EDF400B67FAB /* ExamplesViewController.swift in Sources */, E447A12B2267BB9500578C0B /* ARExample.swift in Sources */, diff --git a/Examples/ArcGISToolkitExamples/Misc/CalibrationView.swift b/Examples/ArcGISToolkitExamples/Misc/CalibrationView.swift new file mode 100644 index 00000000..07e0d1fe --- /dev/null +++ b/Examples/ArcGISToolkitExamples/Misc/CalibrationView.swift @@ -0,0 +1,367 @@ +// +// CalibrationView.swift +// ArcGISToolkitExamples +// +// Created by Mark Dostal on 8/13/19. +// Copyright © 2019 Esri. All rights reserved. +// + +import UIKit +import ArcGIS + +class CalibrationView: UIView, UIGestureRecognizerDelegate { + + public var cameraController: AGSTransformationMatrixCameraController! + public var sceneView: AGSSceneView! + + private let calibrationDirectionsLabel: UILabel = { + let label = UILabel(frame: .zero) + label.textAlignment = .center + label.font = UIFont.systemFont(ofSize: 24.0) + label.textColor = .darkText + label.numberOfLines = 0 + label.text = "Calibration..." + return label + }() + + private let elevationSlider: UISlider = { + let slider = UISlider(frame: .zero) + slider.minimumValue = -100.0 + slider.maximumValue = 100.0 + + // Rotate the slider so it slides up/down. + slider.transform = CGAffineTransform(rotationAngle: -CGFloat.pi/2) + return slider + }() + + private let headingSlider: UISlider = { + let slider = UISlider(frame: .zero) + slider.minimumValue = -180.0 + slider.maximumValue = 180.0 + return slider + }() + + init(sceneView: AGSSceneView, cameraController: AGSTransformationMatrixCameraController) { + super.init(frame: .zero) + + self.cameraController = cameraController + self.sceneView = sceneView + + // Set a corner radius on the directions label + calibrationDirectionsLabel.layer.cornerRadius = 8.0 + calibrationDirectionsLabel.layer.masksToBounds = true + + let labelView = UIVisualEffectView(effect: UIBlurEffect(style: .light)) + labelView.layer.cornerRadius = 8.0 + labelView.layer.masksToBounds = true + + labelView.contentView.addSubview(calibrationDirectionsLabel) + calibrationDirectionsLabel.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([ + calibrationDirectionsLabel.leadingAnchor.constraint(equalTo: labelView.leadingAnchor, constant: 8), + calibrationDirectionsLabel.trailingAnchor.constraint(equalTo: labelView.trailingAnchor, constant: -8), + calibrationDirectionsLabel.topAnchor.constraint(equalTo: labelView.topAnchor, constant: 8), + calibrationDirectionsLabel.bottomAnchor.constraint(equalTo: labelView.bottomAnchor, constant: -8) + ]) + + addSubview(labelView) + labelView.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([ + labelView.centerXAnchor.constraint(equalTo: centerXAnchor), + labelView.topAnchor.constraint(equalTo: topAnchor, constant: 88.0) + ]) + + addSubview(elevationSlider) + elevationSlider.addTarget(self, action: #selector(elevationChanged(_:)), for: .valueChanged) + elevationSlider.translatesAutoresizingMaskIntoConstraints = false + let width: CGFloat = 500.0 + NSLayoutConstraint.activate([ +// elevationSlider.centerYAnchor.constraint(equalTo: centerYAnchor), +// elevationSlider.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -12), +// elevationSlider.widthAnchor.constraint(greaterThanOrEqualToConstant: 250) + + + + +// elevationSlider.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -12), +// elevationSlider.leadingAnchor.constraint(equalTo: leadingAnchor, constant: -12), + elevationSlider.centerYAnchor.constraint(equalTo: centerYAnchor), +// elevationSlider.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -36), + elevationSlider.widthAnchor.constraint(greaterThanOrEqualToConstant: width), + elevationSlider.trailingAnchor.constraint(equalTo: trailingAnchor, constant: width / 2.0 - 36) + + + + + +// elevationSlider.widthAnchor.constraint(greaterThanOrEqualToConstant: 500.0) +// elevationSlider.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -12), +// elevationSlider.topAnchor.constraint(equalTo: topAnchor, constant: 36), +// elevationSlider.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -36), +// elevationSlider.widthAnchor.constraint(greaterThanOrEqualToConstant: 500.0) + ]) + + addSubview(headingSlider) + headingSlider.addTarget(self, action: #selector(headingChanged(_:)), for: .valueChanged) + headingSlider.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([ + headingSlider.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 24), + headingSlider.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -24), + // elevationSlider.topAnchor.constraint(equalTo: topAnchor, constant: 8), + headingSlider.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -24) + ]) + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + let hitView = super.hitTest(point, with: event) + if hitView == self { + return nil; + } else { + return hitView; + } + } + + var lastElevationValue: Float = 0 + @objc func elevationChanged(_ sender: UISlider){ + print("elevationChanged...") + let camera = cameraController.originCamera + cameraController.originCamera = camera.elevate(withDeltaAltitude: Double(sender.value - lastElevationValue)) + lastElevationValue = sender.value + } + + var lastHeadingValue: Float = 0 + @objc func headingChanged(_ sender: UISlider){ + print("headingChanged...") + let camera = cameraController.originCamera + let newHeading = Float(camera.heading) + sender.value - lastHeadingValue + cameraController.originCamera = camera.rotate(toHeading: Double(newHeading), pitch: camera.pitch, roll: camera.roll) + lastHeadingValue = sender.value + } + +// private var lastTouchPoint = CGPoint.zero +// +// @objc func panGesture() { +// guard let sceneView = sceneView, let cameraController = cameraController else { return } +// switch panGR.state { +// case .began: +// lastTouchPoint = panGR.location(in: self) +// break +// case .changed: +// let newTouchPoint = panGR.location(in: self) +// let lastPoint = sceneView.screen(toBaseSurface: lastTouchPoint) +// let newPoint = sceneView.screen(toBaseSurface: newTouchPoint) +// let dx = newPoint.x - lastPoint.x +// let dy = newPoint.y - lastPoint.y +// print("dx = \(dx); dy = \(dy)") +// let originCamera = cameraController.originCamera +// cameraController.originCamera = AGSCamera(latitude: originCamera.location.y - dy, +// longitude: originCamera.location.x - dx, +// altitude: originCamera.location.z, +// heading: originCamera.heading, +// pitch: originCamera.pitch, +// roll: originCamera.roll) +// lastTouchPoint = newTouchPoint +// break +// default: +// break +// } +// +// } + // -(void)panGesture { + // + // // + // // touchPt represents 1 of 2 possible point values + // // + // // If numTouches == 1: Then it's the actual point touched + // // If numTouches == 2: Then it's the center point of the two touches + // + // CGPoint touchPt = CGPointZero; + // + // NSInteger numTouches = [self.panGR numberOfTouches]; + // + // // "pinching" is YES until both fingers are let up, + // // which is good + // if (self.userPinching && + // [self ags_anchoredOnLocationDisplay]){ + // return; + // } + // + // if (numTouches == 2) { + // _twoFingersDownAtOnePointDuringPanning = YES; + // // + // // make sure user wants 2 finger panning + // if (!_allowTwoFingerPanning) { + // // NOTE: even though we are not actually going to pan + // // we need to update _lastPanTouchCount. In the case + // // of a person panning with 1 finger, then adding a second, then + // // letting up, we don't get a new recognizer event, this one just + // // changes state..in StateChanged handling we update the last(Center|Touch)Loc + // // when the user changes from 1 finger to 2, but ONLY if the touchCount is different. + // _lastPanTouchCount = numTouches; + // return; + // } + // + // CGPoint t1 = [self.panGR locationOfTouch:0 inView:self]; + // CGPoint t2 = [self.panGR locationOfTouch:1 inView:self]; + // touchPt = CGPointMake((t1.x + t2.x) / 2, (t1.y + t2.y)/2); + // } + // else if (numTouches == 3){ + // touchPt = [self.panGR locationOfTouch:0 inView:self]; + // } + // else { + // touchPt = [self.panGR locationInView:self]; + // } + // + // if (_twoFingersDownAtOnePointDuringPanning && + // [self ags_anchoredOnLocationDisplay]){ + // if (self.panGR.state == UIGestureRecognizerStateEnded){ + // _twoFingersDownAtOnePointDuringPanning = NO; + // } + // return; + // } + // + // // + // // if our recognizer is just beginning, set our baseline + // // locations + // if (self.panGR.state == UIGestureRecognizerStateBegan) { + // + // self.userDragging = YES; + // + // if (numTouches == 1) { + // _lastTouchLoc = touchPt; + // [self setOrigin:touchPt]; + // } + // else if (numTouches == 2) { + // _lastCenterLoc = touchPt; + // [self setOrigin:touchPt]; + // } + // else if (numTouches == 3){ + // _lastThreeFingerLoc = touchPt; + // // for pitch we use center + // [self setOrigin:self.center]; + // } + // return; + // } + // // + // // fired when pan recognizer changes state: + // // -either we panned, or changed from 1 to 2 touches, or vice versa + // // + // // We update our _lastLoc positions and update the _lastPanTouchCount + // else if (self.panGR.state == UIGestureRecognizerStateChanged) { + // if (numTouches != _lastPanTouchCount) { + // + // if (numTouches == 1){ + // _lastTouchLoc = touchPt; + // } + // else if (numTouches == 2) { + // _lastCenterLoc = touchPt; + // } + // else if (numTouches == 3){ + // _lastThreeFingerLoc = touchPt; + // } + // _lastPanTouchCount = numTouches; + // } + // + // float dx = 0.0; + // float dy = 0.0; + // + // + // if (numTouches == 1) { + // dx = touchPt.x - _lastTouchLoc.x; + // dy = touchPt.y - _lastTouchLoc.y; + // _lastTouchLoc = touchPt; + // } + // else if (numTouches == 2) { + // dx = touchPt.x - _lastCenterLoc.x; + // dy = touchPt.y - _lastCenterLoc.y; + // _lastCenterLoc = touchPt; + // } + // else if (numTouches == 3){ + // dy = touchPt.y - _lastThreeFingerLoc.y; + // _lastThreeFingerLoc = touchPt; + // } + // + // // panning for 1 touch, tilting for 2 + // if (numTouches == 1){ + // [self.rtcSceneView interactionUpdatePanOrigin:dx screenYDelta:dy error:nil]; + // } + // else if (numTouches == 2){ + // double pitch = (dy / self.frame.size.height) * -90.0; + // //NSLog(@"dy: %f", dy); + // [self.rtcSceneView interactionUpdateRotateAroundOrigin:0 pitchDeltaDegrees:pitch error:nil]; + // } + // } + // else if (self.panGR.state == UIGestureRecognizerStateEnded) { + // // + // // If flick isn't allowed, return + // if (!self.interactionOptions.isFlickEnabled){ + // self.userDragging = NO; + // return; + // } + // [self.rtcSceneView interactionActivateFlick:nil]; + // + // // needs to happen after animation gets kicked off + // self.userDragging = NO; + // } + // else if (self.panGR.state == UIGestureRecognizerStateCancelled) { + // self.userDragging = NO; + // } + // } + + // + // when a pinch starts...get the resolution so we can use it + // as the baseline for zoom + @objc func pinchGesture() { + + } +// - (void)pinchGesture { +// +// if (self.pinchGR.state == UIGestureRecognizerStateBegan) { +// self.userPinching = YES; +// _lastPinchScale = 1.0; +// [self setOrigin:[self.pinchGR locationInView:self]]; +// } +// else if (self.pinchGR.state == UIGestureRecognizerStateChanged) { +// double targetScale = self.pinchGR.scale / _lastPinchScale; +// [self.rtcSceneView interactionUpdateZoomToOrigin:targetScale error:nil]; +// _lastPinchScale = self.pinchGR.scale; +// //NSLog(@"pinch scale; %f, %f", self.pinchGR.scale, targetScale); +// } +// else if (self.pinchGR.state == UIGestureRecognizerStateEnded || +// self.pinchGR.state == UIGestureRecognizerStateCancelled){ +// self.userPinching = NO; +// } +// } + + @objc func rotateGesture() { + + } + +// - (void)rotateGesture { +// +// if (self.rotateGR.state == UIGestureRecognizerStateBegan) { +// self.userRotating = YES; +// CGPoint pt = [self.rotateGR locationInView:self]; +// [self setOrigin:pt]; +// } +// else if (self.rotateGR.state == UIGestureRecognizerStateChanged) { +// // +// double angle = AGS_RAD2DEG(self.rotateGR.rotation); +// [self.rtcSceneView interactionUpdateRotateAroundOrigin:angle pitchDeltaDegrees:0 error:nil]; +// +// // +// // reset the rotation so we keep getting a delta's from the recognizer +// self.rotateGR.rotation = 0.0f; +// } +// else if (self.rotateGR.state == UIGestureRecognizerStateEnded || +// self.rotateGR.state == UIGestureRecognizerStateCancelled) { +// self.userRotating = NO; +// } +// } +// + +} diff --git a/Examples/ArcGISToolkitExamples/Misc/UserDirectionsView.swift b/Examples/ArcGISToolkitExamples/Misc/UserDirectionsView.swift new file mode 100644 index 00000000..863c54fd --- /dev/null +++ b/Examples/ArcGISToolkitExamples/Misc/UserDirectionsView.swift @@ -0,0 +1,59 @@ +// +// Copyright 2019 Esri. + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 + +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import UIKit + +/// A custom view for dislaying directions to the user. +class UserDirectionsView: UIVisualEffectView { + + private let userDirectionsLabel: UILabel = { + let label = UILabel(frame: .zero) + label.textAlignment = .center + label.font = UIFont.systemFont(ofSize: 24.0) + label.textColor = .darkText + label.numberOfLines = 0 + label.text = "Initializing ARKit..." + return label + }() + + override init(effect: UIVisualEffect?) { + super.init(effect: effect) + // Set a corner radius. + layer.cornerRadius = 8.0 + layer.masksToBounds = true + + contentView.addSubview(userDirectionsLabel) + userDirectionsLabel.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([ + userDirectionsLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 8), + userDirectionsLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -8), + userDirectionsLabel.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 8), + userDirectionsLabel.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -8) + ]) + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + /// Updates the displayed user directions string. If `message` is nil or empty, this will hide the view. If `message` is not empty, it will display the view. + /// + /// - Parameter message: the new string to display. + public func updateUserDirections(_ message: String?) { + UIView.animate(withDuration: 0.25) { [weak self] in + self?.alpha = (message?.isEmpty ?? true) ? 0.0 : 1.0 + self?.userDirectionsLabel.text = message + } + } +} From 9a1cd8cb2b6976217b885c3661406f67e94c4b46 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Wed, 14 Aug 2019 16:28:03 -0500 Subject: [PATCH 099/147] LocationDataSource changes. --- .../ArcGISToolkitExamples/ARExample.swift | 14 +++-- .../ArcGISToolkitExamples/Misc/Plane.swift | 2 +- Toolkit/ArcGISToolkit/AR/ArcGISARView.swift | 52 ++++++++++++------- 3 files changed, 43 insertions(+), 25 deletions(-) diff --git a/Examples/ArcGISToolkitExamples/ARExample.swift b/Examples/ArcGISToolkitExamples/ARExample.swift index 875d4a9d..0fef6c3c 100644 --- a/Examples/ArcGISToolkitExamples/ARExample.swift +++ b/Examples/ArcGISToolkitExamples/ARExample.swift @@ -18,6 +18,8 @@ import ArcGIS class ARExample: UIViewController { + var hitCount = 0 + typealias sceneInitFunction = () -> AGSScene typealias sceneInfoType = (sceneFunction: sceneInitFunction, label: String, tableTop: Bool) @@ -140,9 +142,7 @@ class ARExample: UIViewController { override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) arView.startTracking { [weak self] (error) in - if let error = error { - self?.statusViewController?.errorMessage = error.localizedDescription - } + self?.statusViewController?.errorMessage = error?.localizedDescription ?? "" } } @@ -204,7 +204,11 @@ class ARExample: UIViewController { // Dim the SceneView until the user taps on a surface. self?.arView.sceneView.alpha = 0.5 } + // Reset AR tracking and then start tracking. self?.arView.resetTracking() + self?.arView.startTracking { [weak self] (error) in + self?.statusViewController?.errorMessage = error?.localizedDescription ?? "" + } // Reset didHitTest variable self?.didHitTest = false @@ -304,7 +308,9 @@ extension ARExample: ARSCNViewDelegate { // Present an alert describing the error. let alertController = UIAlertController(title: "Could not start tracking.", message: errorMessage, preferredStyle: .alert) let restartAction = UIAlertAction(title: "Restart Tracking", style: .default) { _ in - self?.arView.startTracking() + self?.arView.startTracking { [weak self] (error) in + self?.statusViewController?.errorMessage = error?.localizedDescription ?? "" + } } alertController.addAction(restartAction) diff --git a/Examples/ArcGISToolkitExamples/Misc/Plane.swift b/Examples/ArcGISToolkitExamples/Misc/Plane.swift index 9f02c489..84787574 100644 --- a/Examples/ArcGISToolkitExamples/Misc/Plane.swift +++ b/Examples/ArcGISToolkitExamples/Misc/Plane.swift @@ -34,7 +34,7 @@ class Plane: SCNNode { guard let material = node.geometry?.firstMaterial else { fatalError("SCNPlane always has one material") } - material.diffuse.contents = UIColor.blue + material.diffuse.contents = UIColor.white // Add the plane node as child node so they appear in the scene. addChildNode(node) diff --git a/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift b/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift index c6ecd93b..afc0a031 100644 --- a/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift +++ b/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift @@ -45,9 +45,6 @@ public class ArcGISARView: UIView { guard let newCamera = originCamera else { return } // Set the camera as the originCamera on the cameraController and reset tracking. cameraController.originCamera = newCamera - if isTracking { - resetTracking() - } } } @@ -73,8 +70,8 @@ public class ArcGISARView: UIView { }() { didSet { // If we're already tracking, reset tracking to use the new configuration. - if isTracking { - resetTracking() + if isTracking, isUsingARKit { + arSCNView.session.run(arConfiguration, options: [.resetTracking]) } } } @@ -87,12 +84,12 @@ public class ArcGISARView: UIView { /// The camera controller used to control the Scene. @objc private let cameraController = AGSTransformationMatrixCameraController() - /// Initial location from location data source. - private var initialLocation: AGSPoint? - /// Used when calculating framerate. private var lastUpdateTime: TimeInterval = 0 + /// A quaternion used to compensate for the pitch being 90 degrees on `ARKit`; used to calculate the current device transformation for each frame. + private let compensationQuat: simd_quatd = simd_quatd(ix: (sin(45 / (180 / .pi))), iy: 0, iz: 0, r: (cos(45 / (180 / .pi)))) + /// Whether `ARKit` is supported on this device. private let deviceSupportsARKit: Bool = { return ARWorldTrackingConfiguration.isSupported @@ -101,6 +98,9 @@ public class ArcGISARView: UIView { /// The last portrait or landscape orientation value. private var lastGoodDeviceOrientation = UIDeviceOrientation.portrait + /// Are we using only the first location provided by the LocationDataSource? + private var useLocationDataSourceOnce = false + // MARK: Initializers public override init(frame: CGRect) { @@ -207,15 +207,18 @@ public class ArcGISARView: UIView { // print("cqx: \(mapPointMatrix.quaternionX); cqy: \(mapPointMatrix.quaternionY); cqz: \(mapPointMatrix.quaternionZ); cqw: \(mapPointMatrix.quaternionW); ctx: \(mapPointMatrix.translationX); cty: \(mapPointMatrix.translationY); ctz: \(mapPointMatrix.translationZ)") print("\(mapPointMatrix.translationX) \(mapPointMatrix.translationY) \(mapPointMatrix.translationZ)") - // Create a camera from transformationMatrix and return it's location. + // Create a camera from transformationMatrix and return its location. return AGSCamera(transformationMatrix: mapPointMatrix).location } - /// Resets the device tracking, using `originCamera` if it's not nil or the device's GPS location via the location data source. + /// Resets the device tracking and related properties. public func resetTracking() { - initialLocation = nil initialTransformation = .identity - startTracking() + if isUsingARKit { + arSCNView.session.run(arConfiguration, options: [.resetTracking]) + } + + cameraController.transformationMatrix = .identity } /// Sets the initial transformation used to offset the originCamera. The initial transformation is based on an AR point determined via existing plane hit detection from `screenPoint`. If an AR point cannot be determined, this method will return `false`. @@ -235,8 +238,9 @@ public class ArcGISARView: UIView { /// Starts device tracking. /// /// - Parameter completion: The completion handler called when start tracking completes. If tracking starts successfully, the `error` property will be nil; if tracking fails to start, the error will be non-nil and contain the reason for failure. - public func startTracking(_ completion: ((_ error: Error?) -> Void)? = nil) { + public func startTracking(useLocationDataSourceOnce: Bool = false, completion: ((_ error: Error?) -> Void)? = nil) { // We have a location data source that needs to be started. + self.useLocationDataSourceOnce = useLocationDataSourceOnce if let locationDataSource = self.locationDataSource { locationDataSource.start { [weak self] (error) in if error == nil { @@ -270,6 +274,7 @@ public class ArcGISARView: UIView { strongSelf.arSCNView.session.run(strongSelf.arConfiguration, options: [.resetTracking]) } + strongSelf.cameraController.transformationMatrix = .identity strongSelf.isTracking = true } } @@ -471,16 +476,23 @@ extension ArcGISARView: AGSLocationChangeHandlerDelegate { locationPoint = AGSPoint(x: locationPoint.x, y: locationPoint.y, z: altitude, spatialReference: locationPoint.spatialReference) } - if initialLocation == nil { - initialLocation = locationPoint + // Always set originCamera; then reset ARKit + let oldCamera = cameraController.originCamera // Create a new camera based on our location and set it on the cameraController. - cameraController.originCamera = AGSCamera(location: locationPoint, heading: 0.0, pitch: 0.0, roll: 0.0) + cameraController.originCamera = AGSCamera(location: locationPoint, heading: oldCamera.heading, pitch: oldCamera.pitch, roll: oldCamera.roll) + + // If we're using ARKit, reset its tracking. + if isUsingARKit { + arSCNView.session.run(arConfiguration, options: [.resetTracking]) } - else if !isUsingARKit { - let camera = sceneView.currentViewpointCamera().move(toLocation: locationPoint) - sceneView.setViewpointCamera(camera) -// print("location changed: \(locationPoint), accuracy: \(location.horizontalAccuracy)") + + // Reset the camera controller's transformationMatrix to its initial state, the Idenity matrix. + cameraController.transformationMatrix = .identity + + if (useLocationDataSourceOnce) { + // If we are only using the intitial data source location, stop the data source. + locationDataSource.stop() } } From 4f79f8b976fa4488b8aef1f60cd865441a8eb647 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Wed, 14 Aug 2019 16:29:50 -0500 Subject: [PATCH 100/147] Remove unnecessary comment. --- Examples/ArcGISToolkitExamples/ARExample.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Examples/ArcGISToolkitExamples/ARExample.swift b/Examples/ArcGISToolkitExamples/ARExample.swift index 0fef6c3c..0dc49f56 100644 --- a/Examples/ArcGISToolkitExamples/ARExample.swift +++ b/Examples/ArcGISToolkitExamples/ARExample.swift @@ -323,7 +323,7 @@ extension ARExample: ARSCNViewDelegate { statusViewController?.trackingState = camera.trackingState updateUserDirections(session.currentFrame!, trackingState: camera.trackingState) } - xxx need willrenderScene + func renderer(_ renderer: SCNSceneRenderer, didRenderScene scene: SCNScene, atTime time: TimeInterval) { // Calculate frame rate and set on the statuc vc. let frametime = time - lastUpdateTime From 856cc044bc5b9cc9fffb4cf0dcf665a58da3f924 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Thu, 15 Aug 2019 14:30:33 -0500 Subject: [PATCH 101/147] Latest and greatest; merge from all unmerged branches. --- .../ArcGISToolkitExamples/ARExample.swift | 242 ++++++--------- .../Misc/ARStatusViewController.swift | 2 +- .../Misc/CalibrationView.swift | 290 +++--------------- Toolkit/ArcGISToolkit/AR/ArcGISARView.swift | 59 ++-- 4 files changed, 170 insertions(+), 423 deletions(-) diff --git a/Examples/ArcGISToolkitExamples/ARExample.swift b/Examples/ArcGISToolkitExamples/ARExample.swift index 0dc49f56..3894de74 100644 --- a/Examples/ArcGISToolkitExamples/ARExample.swift +++ b/Examples/ArcGISToolkitExamples/ARExample.swift @@ -18,8 +18,6 @@ import ArcGIS class ARExample: UIViewController { - var hitCount = 0 - typealias sceneInitFunction = () -> AGSScene typealias sceneInfoType = (sceneFunction: sceneInitFunction, label: String, tableTop: Bool) @@ -37,8 +35,8 @@ class ARExample: UIViewController { /// The `ArcGISARView` that displays the camera feed and handles ARKit functionality. private let arView = ArcGISARView(renderVideoFeed: true, tryUsingARKit: true) - /// Denotes whether we've performed a hit test yet. - private var didHitTest: Bool = false + /// Denotes whether we've placed the scene in table top experiences. + private var didPlaceScene: Bool = false // View controller displaying current status of `ARExample`. private let statusViewController: ARStatusViewController? = { @@ -63,11 +61,14 @@ class ARExample: UIViewController { /// The observer for the `SceneView`'s `translationFactor` property. private var translationFactorObservation: NSKeyValueObservation? - /// Denotes whether we're in calibration mode. - private var isCalibrating = false + /// View for displaying calibration controls to the user. private var calibrationView: CalibrationView? + /// The toolbar used to display controls for calibration, changing scenes, and status. private var toolbar = UIToolbar(frame: .zero) + + // MARK: Initialization + override func viewDidLoad() { super.viewDidLoad() @@ -86,7 +87,7 @@ class ARExample: UIViewController { // Add our graphics overlay to the sceneView. arView.sceneView.graphicsOverlays.add(graphicsOverlay) - // Observe the `cameraController.translationFactor` property and update status when it changes. + // Observe the `arView.translationFactor` property and update status when it changes. translationFactorObservation = arView.observe(\ArcGISARView.translationFactor, options: [.initial, .new]){ [weak self] arView, change in self?.statusViewController?.translationFactor = arView.translationFactor } @@ -101,24 +102,17 @@ class ARExample: UIViewController { arView.bottomAnchor.constraint(equalTo: view.bottomAnchor) ]) - // Add a Toolbar for changing the scene and showing the status view. - toolbar = addToolbar() + // Add a Toolbar for displaying user controls. + addToolbar() // Add the status view and setup constraints. - if let statusVC = statusViewController { - addChild(statusVC) - view.addSubview(statusVC.view) - statusVC.didMove(toParent: self) - statusVC.view.translatesAutoresizingMaskIntoConstraints = false - NSLayoutConstraint.activate([ - statusVC.view.heightAnchor.constraint(equalToConstant: 110), - statusVC.view.widthAnchor.constraint(equalToConstant: 350), - statusVC.view.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -8), - statusVC.view.bottomAnchor.constraint(equalTo: toolbar.topAnchor, constant: -8) - ]) - - statusVC.view.alpha = 0.0 - } + addStatusViewController() + + // Add the UserDirectionsView. + addUserDirectionsView() + + // Add the CalibrationView. + addCalibrationView() // Set up the `sceneInfo` array with our scene init functions and labels. sceneInfo.append(contentsOf: [(sceneFunction: streetsScene, label: "Streets - Full Scale", tableTop: false), @@ -127,13 +121,7 @@ class ARExample: UIViewController { (sceneFunction: yosemiteScene, label: "Yosemite - Tabletop", tableTop: true), (sceneFunction: borderScene, label: "US - Mexico Border - Tabletop", tableTop: true), (sceneFunction: emptyScene, label: "Empty - Full Scale", tableTop: false)]) - - // Add the UserDirectionsView. - addUserDirectionsView() - - // Add the CalibrationView. - addCalibrationView() - + // Use the first sceneInfo to create and set the scene. currentSceneInfo = sceneInfo.first arView.sceneView.scene = currentSceneInfo?.sceneFunction() @@ -142,7 +130,7 @@ class ARExample: UIViewController { override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) arView.startTracking { [weak self] (error) in - self?.statusViewController?.errorMessage = error?.localizedDescription ?? "" + self?.statusViewController?.errorMessage = error?.localizedDescription } } @@ -151,47 +139,39 @@ class ARExample: UIViewController { arView.stopTracking() } - var originalGestures: [UIGestureRecognizer]? + // MARK: Toolbar button actions - /// Initiatest scene location calibration. + /// Initialize scene location/heading/elevation calibration. /// /// - Parameter sender: The bar button item tapped on. - @objc func calibration(_ sender: UIBarButtonItem) { - isCalibrating = !isCalibrating + @objc func displayCalibration(_ sender: UIBarButtonItem) { + // If the sceneView's alpha is 0.0, that means we are not in calibration mode and we need to start calibrating. + let startCalibrating = (calibrationView?.alpha == 0.0) - arView.sceneView.interactionOptions.isEnabled = isCalibrating - userDirectionsView.updateUserDirections(/*isCalibrating ? "Calibrating...." : */"") + // Enable/disable sceneView touch interactions. + arView.sceneView.interactionOptions.isEnabled = startCalibrating + userDirectionsView.updateUserDirections(nil) - // Do calibration work... + // Display calibration view. UIView.animate(withDuration: 0.25) { [weak self] in - self?.calibrationView?.alpha = (self?.isCalibrating ?? false) ? 1.0 : 0.0 + self?.calibrationView?.alpha = startCalibrating ? 1.0 : 0.0 } - // Do calibration work... + // Dim the sceneView if we're calibrating. UIView.animate(withDuration: 0.25) { [weak self] in - self?.arView.sceneView.alpha = (self?.isCalibrating ?? false) ? 0.65 : 1.0 + self?.arView.sceneView.alpha = startCalibrating ? 0.65 : 1.0 } - -// if isCalibrating { -// arView.stopTracking() -// } -// else { -// // Done calibrating, start tracking again. -// arView.startTracking { [weak self] (error) in -// if let error = error { -// self?.statusViewController?.errorMessage = error.localizedDescription -// } -// } -// } } - /// Changes the scene to a newly selected scene. + /// Allow users to change the current scene. /// /// - Parameter sender: The bar button item tapped on. @objc func changeScene(_ sender: UIBarButtonItem){ // Display an alert controller displaying the scenes to choose from. let alertController = UIAlertController(title: nil, message: nil, preferredStyle: UIAlertController.Style.actionSheet) alertController.popoverPresentationController?.barButtonItem = sender + + // Loop through all sceneInfos and add `UIAlertActions` for each. sceneInfo.forEach { info in let action = UIAlertAction(title: info.label, style: .default, handler: { [weak self] (action) in // Set currentSceneInfo to the selected scene. @@ -206,12 +186,12 @@ class ARExample: UIViewController { } // Reset AR tracking and then start tracking. self?.arView.resetTracking() - self?.arView.startTracking { [weak self] (error) in - self?.statusViewController?.errorMessage = error?.localizedDescription ?? "" - } + self?.arView.startTracking(useLocationDataSourceOnce: true, completion: { [weak self] (error) in + self?.statusViewController?.errorMessage = error?.localizedDescription + }) - // Reset didHitTest variable - self?.didHitTest = false + // Reset didPlaceScene variable + self?.didPlaceScene = false }) // Display current scene as disabled. action.isEnabled = !(info.label == currentSceneInfo?.label) @@ -220,7 +200,7 @@ class ARExample: UIViewController { present(alertController, animated: true) } - /// Dislays the status view controller + /// Dislays the status view controller. /// /// - Parameter sender: The bar button item tapped on. @objc func showStatus(_ sender: UIBarButtonItem){ @@ -229,9 +209,9 @@ class ARExample: UIViewController { } } - private func addToolbar() -> UIToolbar { - // Create a toolbar and add it to the arView. - let toolbar = UIToolbar(frame: .zero) + /// Sets up the toolbar and add it to the view. + private func addToolbar() { + // Add it to the arView and set up constraints. arView.addSubview(toolbar) toolbar.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ @@ -241,7 +221,7 @@ class ARExample: UIViewController { ]) // Create a toolbar button for calibration. - let calibrationItem = UIBarButtonItem(title: "Calibration", style: .plain, target: self, action: #selector(calibration(_:))) + let calibrationItem = UIBarButtonItem(title: "Calibration", style: .plain, target: self, action: #selector(displayCalibration(_:))) // Create a toolbar button to change the current scene. let sceneItem = UIBarButtonItem(title: "Change Scene", style: .plain, target: self, action: #selector(changeScene(_:))) @@ -254,8 +234,24 @@ class ARExample: UIViewController { sceneItem, UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil), statusItem], animated: false) - - return toolbar + } + + /// Set up the status view controller and adds it to the view. + private func addStatusViewController() { + if let statusVC = statusViewController { + addChild(statusVC) + view.addSubview(statusVC.view) + statusVC.didMove(toParent: self) + statusVC.view.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([ + statusVC.view.heightAnchor.constraint(equalToConstant: 110), + statusVC.view.widthAnchor.constraint(equalToConstant: 350), + statusVC.view.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -8), + statusVC.view.bottomAnchor.constraint(equalTo: toolbar.topAnchor, constant: -8) + ]) + + statusVC.view.alpha = 0.0 + } } } @@ -309,7 +305,7 @@ extension ARExample: ARSCNViewDelegate { let alertController = UIAlertController(title: "Could not start tracking.", message: errorMessage, preferredStyle: .alert) let restartAction = UIAlertAction(title: "Restart Tracking", style: .default) { _ in self?.arView.startTracking { [weak self] (error) in - self?.statusViewController?.errorMessage = error?.localizedDescription ?? "" + self?.statusViewController?.errorMessage = error?.localizedDescription } } alertController.addAction(restartAction) @@ -348,40 +344,32 @@ extension ARExample: ARSessionDelegate { // MARK: AGSGeoViewTouchDelegate extension ARExample: AGSGeoViewTouchDelegate { public func geoView(_ geoView: AGSGeoView, didTapAtScreenPoint screenPoint: CGPoint, mapPoint: AGSPoint) { - guard let sceneInfo = currentSceneInfo, !didHitTest else { return } - - let colors:[UIColor] = [.red, .blue, .yellow, .green] - if sceneInfo.tableTop { - // We're in table-top mode. Place the scene at the given point by setting the initial transformation. + if let sceneInfo = currentSceneInfo, sceneInfo.tableTop, !didPlaceScene { + // We're in table-top mode and haven't placed the scene yet. Place the scene at the given point by setting the initial transformation. if arView.setInitialTransformation(using: screenPoint) { // Show the SceneView now that the user has tapped on the surface. UIView.animate(withDuration: 0.5) { [weak self] in self?.arView.sceneView.alpha = 1.0 } + + // Clear the user directions. userDirectionsView.updateUserDirections(nil) - didHitTest = true + didPlaceScene = true } } else { - // We're in full-scale AR mode. Get the real world location for screen point from arView. + // We're in full-scale AR mode or have already placed the scene. Get the real world location for screen point from arView. guard let point = arView.arScreenToLocation(screenPoint: screenPoint) else { return } - - - print("point = \(point)") - // Create and place a graphic at the real world location. - let sphere = AGSSimpleMarkerSceneSymbol(style: .sphere, color: colors[hitCount], height: 0.25, width: 0.25, depth: 0.25, anchorPosition: .bottom) - let shadow = AGSSimpleMarkerSceneSymbol(style: .sphere, color: .lightGray, height: 0.01, width: 0.25, depth: 0.25, anchorPosition: .center) - let sphereGraphic = AGSGraphic(geometry: point, symbol: sphere, attributes: nil) + // Create and place a graphic and shadown at the real world location. + let shadowColor = UIColor.lightGray.withAlphaComponent(0.5) + let shadow = AGSSimpleMarkerSceneSymbol(style: .sphere, color: shadowColor, height: 0.01, width: 0.25, depth: 0.25, anchorPosition: .center) let shadowGraphic = AGSGraphic(geometry: point, symbol: shadow, attributes: nil) - graphicsOverlay.graphics.add(shadowGraphic - - ) + graphicsOverlay.graphics.add(shadowGraphic) + + let sphere = AGSSimpleMarkerSceneSymbol(style: .sphere, color: .red, height: 0.25, width: 0.25, depth: 0.25, anchorPosition: .bottom) + let sphereGraphic = AGSGraphic(geometry: point, symbol: sphere, attributes: nil) graphicsOverlay.graphics.add(sphereGraphic) - hitCount += 1 - if hitCount > 3 { - hitCount = 0 - } } } } @@ -397,8 +385,8 @@ extension ARExample: UIAdaptivePresentationControllerDelegate { // MARK: User Directions View extension ARExample { + /// Add user directions view to view and setup constraints. func addUserDirectionsView() { - // Add userDirectionsView to superView and setup constraints. view.addSubview(userDirectionsView) userDirectionsView.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ @@ -407,17 +395,22 @@ extension ARExample { ]) } + /// Update the displayed message in the user directions view for the current frame and tracking state. + /// + /// - Parameters: + /// - frame: The current ARKit frame. + /// - trackingState: The current ARKit tracking state. private func updateUserDirections(_ frame: ARFrame, trackingState: ARCamera.TrackingState) { var message = "" switch trackingState { case .normal where frame.anchors.isEmpty: - if let sceneInfo = currentSceneInfo, sceneInfo.tableTop, !didHitTest { + if let sceneInfo = currentSceneInfo, sceneInfo.tableTop, !didPlaceScene { message = "Move the device around to detect horizontal surfaces." } break case .normal where !frame.anchors.isEmpty: - if let sceneInfo = currentSceneInfo, sceneInfo.tableTop, !didHitTest { + if let sceneInfo = currentSceneInfo, sceneInfo.tableTop, !didPlaceScene { message = "Tap to place the Scene on a surface." } break @@ -449,10 +442,9 @@ extension ARExample { // MARK: Calibration View extension ARExample { + /// Add the calibration view to the view and setup constraints. func addCalibrationView() { - // Add calibrationView to superView and setup constraints. - guard let cc = arView.sceneView.cameraController as? AGSTransformationMatrixCameraController else { return } - calibrationView = CalibrationView(sceneView: arView.sceneView, cameraController: cc) + calibrationView = CalibrationView(sceneView: arView.sceneView, cameraController: arView.cameraController) guard let calibrationView = calibrationView else { return } view.addSubview(calibrationView) calibrationView.translatesAutoresizingMaskIntoConstraints = false @@ -464,40 +456,6 @@ extension ARExample { ]) calibrationView.alpha = 0.0 - -// let elevationSlider: UISlider = { -// let slider = UISlider(frame: .zero) -// slider.minimumValue = -100.0 -// slider.maximumValue = 100.0 -// return slider -// }() -// -// let headingSlider: UISlider = { -// let slider = UISlider(frame: .zero) -// slider.minimumValue = -180.0 -// slider.maximumValue = 180.0 -// return slider -// }() -// -// addSubview(elevationSlider) -// elevationSlider.addTarget(self, action: #selector(elevationChanged(_:)), for: .valueChanged) -// elevationSlider.translatesAutoresizingMaskIntoConstraints = false -// NSLayoutConstraint.activate([ -// elevationSlider.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -8), -// // elevationSlider.topAnchor.constraint(equalTo: topAnchor, constant: 8), -// elevationSlider.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -8) -// ]) -// -// addSubview(headingSlider) -// headingSlider.addTarget(self, action: #selector(headingChanged(_:)), for: .valueChanged) -// headingSlider.translatesAutoresizingMaskIntoConstraints = false -// NSLayoutConstraint.activate([ -// headingSlider.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 8), -// headingSlider.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -8), -// // elevationSlider.topAnchor.constraint(equalTo: topAnchor, constant: 8), -// headingSlider.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -8) -// ]) - } } @@ -555,8 +513,8 @@ extension ARExample { scene.operationalLayers.add(layer) layer.load { [weak self] (error) in - if let error = error { - self?.statusViewController?.errorMessage = error.localizedDescription + self?.statusViewController?.errorMessage = error?.localizedDescription + if let _ = error { return } @@ -586,8 +544,8 @@ extension ARExample { let layer = AGSIntegratedMeshLayer(url: URL(string:"https://tiles.arcgis.com/tiles/FQD0rKU8X5sAQfh8/arcgis/rest/services/VRICON_Yosemite_Sample_Integrated_Mesh_scene_layer/SceneServer")!) scene.operationalLayers.add(layer) scene.load { [weak self, weak scene] (error) in - if let error = error { - self?.statusViewController?.errorMessage = error.localizedDescription + self?.statusViewController?.errorMessage = error?.localizedDescription + if let _ = error { return } @@ -597,15 +555,15 @@ extension ARExample { let center = extent.center scene?.baseSurface?.elevationSources.first?.load { (error) in - if let error = error { - self?.statusViewController?.errorMessage = error.localizedDescription + self?.statusViewController?.errorMessage = error?.localizedDescription + if let _ = error { return } // Find the elevation of the layer at the center point. scene?.baseSurface?.elevation(for: center, completion: { (elevation, error) in - if let error = error { - self?.statusViewController?.errorMessage = error.localizedDescription + self?.statusViewController?.errorMessage = error?.localizedDescription + if let _ = error { return } @@ -634,8 +592,8 @@ extension ARExample { let layer = AGSIntegratedMeshLayer(url: URL(string:"https://tiles.arcgis.com/tiles/FQD0rKU8X5sAQfh8/arcgis/rest/services/VRICON_SW_US_Sample_Integrated_Mesh_scene_layer/SceneServer")!) scene.operationalLayers.add(layer) scene.load { [weak self, weak scene] (error) in - if let error = error { - self?.statusViewController?.errorMessage = error.localizedDescription + self?.statusViewController?.errorMessage = error?.localizedDescription + if let _ = error { return } @@ -645,15 +603,15 @@ extension ARExample { let center = extent.center scene?.baseSurface?.elevationSources.first?.load { (error) in - if let error = error { - self?.statusViewController?.errorMessage = error.localizedDescription + self?.statusViewController?.errorMessage = error?.localizedDescription + if let _ = error { return } // Find the elevation of the layer at the center point. scene?.baseSurface?.elevation(for: center, completion: { (elevation, error) in - if let error = error { - self?.statusViewController?.errorMessage = error.localizedDescription + self?.statusViewController?.errorMessage = error?.localizedDescription + if let _ = error { return } diff --git a/Examples/ArcGISToolkitExamples/Misc/ARStatusViewController.swift b/Examples/ArcGISToolkitExamples/Misc/ARStatusViewController.swift index b98e378b..d45e1a83 100644 --- a/Examples/ArcGISToolkitExamples/Misc/ARStatusViewController.swift +++ b/Examples/ArcGISToolkitExamples/Misc/ARStatusViewController.swift @@ -65,7 +65,7 @@ class ARStatusViewController: UITableViewController { } /// The current error message. - public var errorMessage: String = "None" { + public var errorMessage: String? { didSet { guard errorDescriptionLabel != nil else { return } DispatchQueue.main.async{ [weak self] in diff --git a/Examples/ArcGISToolkitExamples/Misc/CalibrationView.swift b/Examples/ArcGISToolkitExamples/Misc/CalibrationView.swift index 07e0d1fe..9f4f5507 100644 --- a/Examples/ArcGISToolkitExamples/Misc/CalibrationView.swift +++ b/Examples/ArcGISToolkitExamples/Misc/CalibrationView.swift @@ -9,21 +9,27 @@ import UIKit import ArcGIS +/// A view displaying controls for adjusting a scene view's location, heading, and elevation. Used to calibrate an AR session. class CalibrationView: UIView, UIGestureRecognizerDelegate { - public var cameraController: AGSTransformationMatrixCameraController! - public var sceneView: AGSSceneView! + // The scene view displaying the scene. + private var sceneView: AGSSceneView! + /// The camera controller used to adjust user interactions. + private var cameraController: AGSTransformationMatrixCameraController! + + /// The label displaying calibration directions. private let calibrationDirectionsLabel: UILabel = { let label = UILabel(frame: .zero) label.textAlignment = .center label.font = UIFont.systemFont(ofSize: 24.0) label.textColor = .darkText label.numberOfLines = 0 - label.text = "Calibration..." + label.text = "Calibrating..." return label }() + /// The UISlider used to adjust elevation. private let elevationSlider: UISlider = { let slider = UISlider(frame: .zero) slider.minimumValue = -100.0 @@ -34,27 +40,41 @@ class CalibrationView: UIView, UIGestureRecognizerDelegate { return slider }() + /// The UISlider used to adjust heading. private let headingSlider: UISlider = { let slider = UISlider(frame: .zero) slider.minimumValue = -180.0 slider.maximumValue = 180.0 return slider }() + + /// The last elevation slider value. + var lastElevationValue: Float = 0 + + // The last heading slider value. + var lastHeadingValue: Float = 0 + /// Initialized a new calibration view with the given scene view and camera controller. + /// + /// - Parameters: + /// - sceneView: The scene view displaying the scene. + /// - cameraController: The camera controller used to adjust user interactions. init(sceneView: AGSSceneView, cameraController: AGSTransformationMatrixCameraController) { super.init(frame: .zero) self.cameraController = cameraController self.sceneView = sceneView - // Set a corner radius on the directions label - calibrationDirectionsLabel.layer.cornerRadius = 8.0 - calibrationDirectionsLabel.layer.masksToBounds = true + // Set a corner radius on the directions label. +// calibrationDirectionsLabel.layer.cornerRadius = 8.0 +// calibrationDirectionsLabel.layer.masksToBounds = true + // Create visual effects view to show the label on a blurred background. let labelView = UIVisualEffectView(effect: UIBlurEffect(style: .light)) labelView.layer.cornerRadius = 8.0 labelView.layer.masksToBounds = true + // Add the label to our label view and set up constraints. labelView.contentView.addSubview(calibrationDirectionsLabel) calibrationDirectionsLabel.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ @@ -64,6 +84,7 @@ class CalibrationView: UIView, UIGestureRecognizerDelegate { calibrationDirectionsLabel.bottomAnchor.constraint(equalTo: labelView.bottomAnchor, constant: -8) ]) + // Add the label view to our view and set up constraints. addSubview(labelView) labelView.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ @@ -71,43 +92,24 @@ class CalibrationView: UIView, UIGestureRecognizerDelegate { labelView.topAnchor.constraint(equalTo: topAnchor, constant: 88.0) ]) + // Add the elevation slider. addSubview(elevationSlider) elevationSlider.addTarget(self, action: #selector(elevationChanged(_:)), for: .valueChanged) elevationSlider.translatesAutoresizingMaskIntoConstraints = false let width: CGFloat = 500.0 NSLayoutConstraint.activate([ -// elevationSlider.centerYAnchor.constraint(equalTo: centerYAnchor), -// elevationSlider.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -12), -// elevationSlider.widthAnchor.constraint(greaterThanOrEqualToConstant: 250) - - - - -// elevationSlider.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -12), -// elevationSlider.leadingAnchor.constraint(equalTo: leadingAnchor, constant: -12), elevationSlider.centerYAnchor.constraint(equalTo: centerYAnchor), -// elevationSlider.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -36), elevationSlider.widthAnchor.constraint(greaterThanOrEqualToConstant: width), elevationSlider.trailingAnchor.constraint(equalTo: trailingAnchor, constant: width / 2.0 - 36) - - - - - -// elevationSlider.widthAnchor.constraint(greaterThanOrEqualToConstant: 500.0) -// elevationSlider.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -12), -// elevationSlider.topAnchor.constraint(equalTo: topAnchor, constant: 36), -// elevationSlider.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -36), -// elevationSlider.widthAnchor.constraint(greaterThanOrEqualToConstant: 500.0) ]) + // Add the heading slider. addSubview(headingSlider) headingSlider.addTarget(self, action: #selector(headingChanged(_:)), for: .valueChanged) headingSlider.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ headingSlider.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 24), headingSlider.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -24), - // elevationSlider.topAnchor.constraint(equalTo: topAnchor, constant: 8), headingSlider.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -24) ]) } @@ -117,6 +119,7 @@ class CalibrationView: UIView, UIGestureRecognizerDelegate { } override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + // If the user tapped in the view (and not in the sliders), do not handle the event. let hitView = super.hitTest(point, with: event) if hitView == self { return nil; @@ -125,243 +128,22 @@ class CalibrationView: UIView, UIGestureRecognizerDelegate { } } - var lastElevationValue: Float = 0 + /// Handle an elevation slider value-changed event. + /// + /// - Parameter sender: The slider tapped on. @objc func elevationChanged(_ sender: UISlider){ - print("elevationChanged...") let camera = cameraController.originCamera cameraController.originCamera = camera.elevate(withDeltaAltitude: Double(sender.value - lastElevationValue)) lastElevationValue = sender.value } - var lastHeadingValue: Float = 0 + /// Handle an heading slider value-changed event. + /// + /// - Parameter sender: The slider tapped on. @objc func headingChanged(_ sender: UISlider){ - print("headingChanged...") let camera = cameraController.originCamera let newHeading = Float(camera.heading) + sender.value - lastHeadingValue cameraController.originCamera = camera.rotate(toHeading: Double(newHeading), pitch: camera.pitch, roll: camera.roll) lastHeadingValue = sender.value } - -// private var lastTouchPoint = CGPoint.zero -// -// @objc func panGesture() { -// guard let sceneView = sceneView, let cameraController = cameraController else { return } -// switch panGR.state { -// case .began: -// lastTouchPoint = panGR.location(in: self) -// break -// case .changed: -// let newTouchPoint = panGR.location(in: self) -// let lastPoint = sceneView.screen(toBaseSurface: lastTouchPoint) -// let newPoint = sceneView.screen(toBaseSurface: newTouchPoint) -// let dx = newPoint.x - lastPoint.x -// let dy = newPoint.y - lastPoint.y -// print("dx = \(dx); dy = \(dy)") -// let originCamera = cameraController.originCamera -// cameraController.originCamera = AGSCamera(latitude: originCamera.location.y - dy, -// longitude: originCamera.location.x - dx, -// altitude: originCamera.location.z, -// heading: originCamera.heading, -// pitch: originCamera.pitch, -// roll: originCamera.roll) -// lastTouchPoint = newTouchPoint -// break -// default: -// break -// } -// -// } - // -(void)panGesture { - // - // // - // // touchPt represents 1 of 2 possible point values - // // - // // If numTouches == 1: Then it's the actual point touched - // // If numTouches == 2: Then it's the center point of the two touches - // - // CGPoint touchPt = CGPointZero; - // - // NSInteger numTouches = [self.panGR numberOfTouches]; - // - // // "pinching" is YES until both fingers are let up, - // // which is good - // if (self.userPinching && - // [self ags_anchoredOnLocationDisplay]){ - // return; - // } - // - // if (numTouches == 2) { - // _twoFingersDownAtOnePointDuringPanning = YES; - // // - // // make sure user wants 2 finger panning - // if (!_allowTwoFingerPanning) { - // // NOTE: even though we are not actually going to pan - // // we need to update _lastPanTouchCount. In the case - // // of a person panning with 1 finger, then adding a second, then - // // letting up, we don't get a new recognizer event, this one just - // // changes state..in StateChanged handling we update the last(Center|Touch)Loc - // // when the user changes from 1 finger to 2, but ONLY if the touchCount is different. - // _lastPanTouchCount = numTouches; - // return; - // } - // - // CGPoint t1 = [self.panGR locationOfTouch:0 inView:self]; - // CGPoint t2 = [self.panGR locationOfTouch:1 inView:self]; - // touchPt = CGPointMake((t1.x + t2.x) / 2, (t1.y + t2.y)/2); - // } - // else if (numTouches == 3){ - // touchPt = [self.panGR locationOfTouch:0 inView:self]; - // } - // else { - // touchPt = [self.panGR locationInView:self]; - // } - // - // if (_twoFingersDownAtOnePointDuringPanning && - // [self ags_anchoredOnLocationDisplay]){ - // if (self.panGR.state == UIGestureRecognizerStateEnded){ - // _twoFingersDownAtOnePointDuringPanning = NO; - // } - // return; - // } - // - // // - // // if our recognizer is just beginning, set our baseline - // // locations - // if (self.panGR.state == UIGestureRecognizerStateBegan) { - // - // self.userDragging = YES; - // - // if (numTouches == 1) { - // _lastTouchLoc = touchPt; - // [self setOrigin:touchPt]; - // } - // else if (numTouches == 2) { - // _lastCenterLoc = touchPt; - // [self setOrigin:touchPt]; - // } - // else if (numTouches == 3){ - // _lastThreeFingerLoc = touchPt; - // // for pitch we use center - // [self setOrigin:self.center]; - // } - // return; - // } - // // - // // fired when pan recognizer changes state: - // // -either we panned, or changed from 1 to 2 touches, or vice versa - // // - // // We update our _lastLoc positions and update the _lastPanTouchCount - // else if (self.panGR.state == UIGestureRecognizerStateChanged) { - // if (numTouches != _lastPanTouchCount) { - // - // if (numTouches == 1){ - // _lastTouchLoc = touchPt; - // } - // else if (numTouches == 2) { - // _lastCenterLoc = touchPt; - // } - // else if (numTouches == 3){ - // _lastThreeFingerLoc = touchPt; - // } - // _lastPanTouchCount = numTouches; - // } - // - // float dx = 0.0; - // float dy = 0.0; - // - // - // if (numTouches == 1) { - // dx = touchPt.x - _lastTouchLoc.x; - // dy = touchPt.y - _lastTouchLoc.y; - // _lastTouchLoc = touchPt; - // } - // else if (numTouches == 2) { - // dx = touchPt.x - _lastCenterLoc.x; - // dy = touchPt.y - _lastCenterLoc.y; - // _lastCenterLoc = touchPt; - // } - // else if (numTouches == 3){ - // dy = touchPt.y - _lastThreeFingerLoc.y; - // _lastThreeFingerLoc = touchPt; - // } - // - // // panning for 1 touch, tilting for 2 - // if (numTouches == 1){ - // [self.rtcSceneView interactionUpdatePanOrigin:dx screenYDelta:dy error:nil]; - // } - // else if (numTouches == 2){ - // double pitch = (dy / self.frame.size.height) * -90.0; - // //NSLog(@"dy: %f", dy); - // [self.rtcSceneView interactionUpdateRotateAroundOrigin:0 pitchDeltaDegrees:pitch error:nil]; - // } - // } - // else if (self.panGR.state == UIGestureRecognizerStateEnded) { - // // - // // If flick isn't allowed, return - // if (!self.interactionOptions.isFlickEnabled){ - // self.userDragging = NO; - // return; - // } - // [self.rtcSceneView interactionActivateFlick:nil]; - // - // // needs to happen after animation gets kicked off - // self.userDragging = NO; - // } - // else if (self.panGR.state == UIGestureRecognizerStateCancelled) { - // self.userDragging = NO; - // } - // } - - // - // when a pinch starts...get the resolution so we can use it - // as the baseline for zoom - @objc func pinchGesture() { - - } -// - (void)pinchGesture { -// -// if (self.pinchGR.state == UIGestureRecognizerStateBegan) { -// self.userPinching = YES; -// _lastPinchScale = 1.0; -// [self setOrigin:[self.pinchGR locationInView:self]]; -// } -// else if (self.pinchGR.state == UIGestureRecognizerStateChanged) { -// double targetScale = self.pinchGR.scale / _lastPinchScale; -// [self.rtcSceneView interactionUpdateZoomToOrigin:targetScale error:nil]; -// _lastPinchScale = self.pinchGR.scale; -// //NSLog(@"pinch scale; %f, %f", self.pinchGR.scale, targetScale); -// } -// else if (self.pinchGR.state == UIGestureRecognizerStateEnded || -// self.pinchGR.state == UIGestureRecognizerStateCancelled){ -// self.userPinching = NO; -// } -// } - - @objc func rotateGesture() { - - } - -// - (void)rotateGesture { -// -// if (self.rotateGR.state == UIGestureRecognizerStateBegan) { -// self.userRotating = YES; -// CGPoint pt = [self.rotateGR locationInView:self]; -// [self setOrigin:pt]; -// } -// else if (self.rotateGR.state == UIGestureRecognizerStateChanged) { -// // -// double angle = AGS_RAD2DEG(self.rotateGR.rotation); -// [self.rtcSceneView interactionUpdateRotateAroundOrigin:angle pitchDeltaDegrees:0 error:nil]; -// -// // -// // reset the rotation so we keep getting a delta's from the recognizer -// self.rotateGR.rotation = 0.0f; -// } -// else if (self.rotateGR.state == UIGestureRecognizerStateEnded || -// self.rotateGR.state == UIGestureRecognizerStateCancelled) { -// self.userRotating = NO; -// } -// } -// - } diff --git a/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift b/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift index afc0a031..b87205af 100644 --- a/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift +++ b/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift @@ -38,6 +38,9 @@ public class ArcGISARView: UIView { locationDataSource?.locationChangeHandlerDelegate = self } } + + /// The `AGSTransformationMatrixCameraController` used to control the Scene. + @objc public let cameraController = AGSTransformationMatrixCameraController() /// The viewpoint camera used to set the initial view of the sceneView instead of the device's GPS location via the location data source. You can use Key-Value Observing to track changes to the origin camera. @objc dynamic public var originCamera: AGSCamera? { @@ -75,21 +78,15 @@ public class ArcGISARView: UIView { } } } - + /// We implement `ARSCNViewDelegate` methods, but will use `arSCNViewDelegate` to forward them to clients. weak public var arSCNViewDelegate: ARSCNViewDelegate? // MARK: Private properties - /// The camera controller used to control the Scene. - @objc private let cameraController = AGSTransformationMatrixCameraController() - /// Used when calculating framerate. private var lastUpdateTime: TimeInterval = 0 - /// A quaternion used to compensate for the pitch being 90 degrees on `ARKit`; used to calculate the current device transformation for each frame. - private let compensationQuat: simd_quatd = simd_quatd(ix: (sin(45 / (180 / .pi))), iy: 0, iz: 0, r: (cos(45 / (180 / .pi)))) - /// Whether `ARKit` is supported on this device. private let deviceSupportsARKit: Bool = { return ARWorldTrackingConfiguration.isSupported @@ -172,10 +169,12 @@ public class ArcGISARView: UIView { { var set = super.keyPathsForValuesAffectingValue(forKey: key) if key == #keyPath(translationFactor) { - // Get the key paths for super and append our key path to it. set.insert(#keyPath(cameraController.translationFactor)) } - + else if key == #keyPath(originCamera) { + set.insert(#keyPath(cameraController.originCamera)) + } + return set } @@ -189,23 +188,23 @@ public class ArcGISARView: UIView { // Use the `internalHitTest` method to get the matrix of `screenPoint`. guard let localOffsetMatrix = internalHitTest(screenPoint: screenPoint) else { return nil } - print("Local offset XYZ, World origin XYZ, Combined world coordinate XYZ") - - //TODO: generalize the debug print function - // print("lqx: \(localOffsetMatrix.quaternionX); lqy: \(localOffsetMatrix.quaternionY); lqz: \(localOffsetMatrix.quaternionZ); lqw: \(localOffsetMatrix.quaternionW); ltx: \(localOffsetMatrix.translationX); lty: \(localOffsetMatrix.translationY); ltz: \(localOffsetMatrix.translationZ)") - print("\(localOffsetMatrix.translationX) \(localOffsetMatrix.translationY) \(localOffsetMatrix.translationZ)") +// print("Local offset XYZ, World origin XYZ, Combined world coordinate XYZ") +// +// //TODO: generalize the debug print function +// // print("lqx: \(localOffsetMatrix.quaternionX); lqy: \(localOffsetMatrix.quaternionY); lqz: \(localOffsetMatrix.quaternionZ); lqw: \(localOffsetMatrix.quaternionW); ltx: \(localOffsetMatrix.translationX); lty: \(localOffsetMatrix.translationY); ltz: \(localOffsetMatrix.translationZ)") +// print("\(localOffsetMatrix.translationX) \(localOffsetMatrix.translationY) \(localOffsetMatrix.translationZ)") let currOriginCamera = cameraController.originCamera let currOriginMatrix = currOriginCamera.transformationMatrix - - // print("oqx: \(currOriginMatrix.quaternionX); oqy: \(currOriginMatrix.quaternionY); oqz: \(currOriginMatrix.quaternionZ); oqw: \(currOriginMatrix.quaternionW); otx: \(currOriginMatrix.translationX); oty: \(currOriginMatrix.translationY); otz: \(currOriginMatrix.translationZ)") - print("\(currOriginMatrix.translationX) \(currOriginMatrix.translationY) \(currOriginMatrix.translationZ)") +// +// // print("oqx: \(currOriginMatrix.quaternionX); oqy: \(currOriginMatrix.quaternionY); oqz: \(currOriginMatrix.quaternionZ); oqw: \(currOriginMatrix.quaternionW); otx: \(currOriginMatrix.translationX); oty: \(currOriginMatrix.translationY); otz: \(currOriginMatrix.translationZ)") +// print("\(currOriginMatrix.translationX) \(currOriginMatrix.translationY) \(currOriginMatrix.translationZ)") //TODO: for tabletop application scale translation by TranslationFactor let mapPointMatrix = currOriginMatrix.addTransformation(localOffsetMatrix) - - // print("cqx: \(mapPointMatrix.quaternionX); cqy: \(mapPointMatrix.quaternionY); cqz: \(mapPointMatrix.quaternionZ); cqw: \(mapPointMatrix.quaternionW); ctx: \(mapPointMatrix.translationX); cty: \(mapPointMatrix.translationY); ctz: \(mapPointMatrix.translationZ)") - print("\(mapPointMatrix.translationX) \(mapPointMatrix.translationY) \(mapPointMatrix.translationZ)") +// +// // print("cqx: \(mapPointMatrix.quaternionX); cqy: \(mapPointMatrix.quaternionY); cqz: \(mapPointMatrix.quaternionZ); cqw: \(mapPointMatrix.quaternionW); ctx: \(mapPointMatrix.translationX); cty: \(mapPointMatrix.translationY); ctz: \(mapPointMatrix.translationZ)") +// print("\(mapPointMatrix.translationX) \(mapPointMatrix.translationY) \(mapPointMatrix.translationZ)") // Create a camera from transformationMatrix and return its location. return AGSCamera(transformationMatrix: mapPointMatrix).location @@ -213,9 +212,10 @@ public class ArcGISARView: UIView { /// Resets the device tracking and related properties. public func resetTracking() { + originCamera = nil initialTransformation = .identity if isUsingARKit { - arSCNView.session.run(arConfiguration, options: [.resetTracking]) + arSCNView.session.run(arConfiguration, options: [.resetTracking, .removeExistingAnchors]) } cameraController.transformationMatrix = .identity @@ -274,7 +274,6 @@ public class ArcGISARView: UIView { strongSelf.arSCNView.session.run(strongSelf.arConfiguration, options: [.resetTracking]) } - strongSelf.cameraController.transformationMatrix = .identity strongSelf.isTracking = true } } @@ -314,8 +313,8 @@ public class ArcGISARView: UIView { quaternionZ: 0.0, quaternionW: 1.0, translationX: Double(worldTransform.columns.3.x), - translationY: Double(-worldTransform.columns.3.z), - translationZ: Double(worldTransform.columns.3.y)) + translationY: Double(worldTransform.columns.3.y), + translationZ: Double(worldTransform.columns.3.z)) return hitTestMatrix } @@ -469,6 +468,8 @@ extension ArcGISARView: AGSLocationChangeHandlerDelegate { // Location changed. guard var locationPoint = location.position else { return } + print("New Location") + // The AGSCLLocationDataSource does not include altitude information from the CLLocation when // creating the `AGSLocation` geometry, so grab the altitude directly from the CLLocationManager. if let clLocationDataSource = locationDataSource as? AGSCLLocationDataSource, @@ -479,8 +480,14 @@ extension ArcGISARView: AGSLocationChangeHandlerDelegate { // Always set originCamera; then reset ARKit let oldCamera = cameraController.originCamera - // Create a new camera based on our location and set it on the cameraController. - cameraController.originCamera = AGSCamera(location: locationPoint, heading: oldCamera.heading, pitch: oldCamera.pitch, roll: oldCamera.roll) + // Create a new camera based on our location and set it on the cameraController. + if originCamera == nil { + let newCamera = AGSCamera(location: locationPoint, heading: 0.0, pitch: 90.0, roll: 0.0) + originCamera = newCamera + } + else { + cameraController.originCamera = AGSCamera(location: locationPoint, heading: oldCamera.heading, pitch: oldCamera.pitch, roll: oldCamera.roll) + } // If we're using ARKit, reset its tracking. if isUsingARKit { From 5c450c79e0921067bc0bee0b639da293370f661b Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Thu, 15 Aug 2019 14:53:16 -0500 Subject: [PATCH 102/147] Use LocationDataSource continuously. --- Examples/ArcGISToolkitExamples/ARExample.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Examples/ArcGISToolkitExamples/ARExample.swift b/Examples/ArcGISToolkitExamples/ARExample.swift index 3894de74..e090350c 100644 --- a/Examples/ArcGISToolkitExamples/ARExample.swift +++ b/Examples/ArcGISToolkitExamples/ARExample.swift @@ -186,9 +186,9 @@ class ARExample: UIViewController { } // Reset AR tracking and then start tracking. self?.arView.resetTracking() - self?.arView.startTracking(useLocationDataSourceOnce: true, completion: { [weak self] (error) in + self?.arView.startTracking { [weak self] (error) in self?.statusViewController?.errorMessage = error?.localizedDescription - }) + } // Reset didPlaceScene variable self?.didPlaceScene = false From d981cbd984f59f24cccc3affeff54714e912179e Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Mon, 19 Aug 2019 13:46:39 -0500 Subject: [PATCH 103/147] Add altitude check when adding elevation to location; use Esri copyright. --- .../Misc/CalibrationView.swift | 18 ++++++++++++------ Toolkit/ArcGISToolkit/AR/ArcGISARView.swift | 4 +++- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/Examples/ArcGISToolkitExamples/Misc/CalibrationView.swift b/Examples/ArcGISToolkitExamples/Misc/CalibrationView.swift index 9f4f5507..89ccaa51 100644 --- a/Examples/ArcGISToolkitExamples/Misc/CalibrationView.swift +++ b/Examples/ArcGISToolkitExamples/Misc/CalibrationView.swift @@ -1,10 +1,16 @@ // -// CalibrationView.swift -// ArcGISToolkitExamples -// -// Created by Mark Dostal on 8/13/19. -// Copyright © 2019 Esri. All rights reserved. -// +// Copyright 2019 Esri. + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 + +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. import UIKit import ArcGIS diff --git a/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift b/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift index b87205af..4ab09f47 100644 --- a/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift +++ b/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift @@ -473,7 +473,9 @@ extension ArcGISARView: AGSLocationChangeHandlerDelegate { // The AGSCLLocationDataSource does not include altitude information from the CLLocation when // creating the `AGSLocation` geometry, so grab the altitude directly from the CLLocationManager. if let clLocationDataSource = locationDataSource as? AGSCLLocationDataSource, - let altitude = clLocationDataSource.locationManager.location?.altitude { + let location = clLocationDataSource.locationManager.location, + location.verticalAccuracy >= 0 { + let altitude = location.altitude, locationPoint = AGSPoint(x: locationPoint.x, y: locationPoint.y, z: altitude, spatialReference: locationPoint.spatialReference) } From 12eba1b49da02cf9a8217e61b26244d7cc3fc960 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Mon, 19 Aug 2019 15:11:28 -0500 Subject: [PATCH 104/147] Fix adding/removing calibration view. --- .../ArcGISToolkitExamples/ARExample.swift | 32 +++++++++++++------ 1 file changed, 23 insertions(+), 9 deletions(-) diff --git a/Examples/ArcGISToolkitExamples/ARExample.swift b/Examples/ArcGISToolkitExamples/ARExample.swift index e090350c..80161b76 100644 --- a/Examples/ArcGISToolkitExamples/ARExample.swift +++ b/Examples/ArcGISToolkitExamples/ARExample.swift @@ -110,10 +110,11 @@ class ARExample: UIViewController { // Add the UserDirectionsView. addUserDirectionsView() - - // Add the CalibrationView. - addCalibrationView() - + + // Create the CalibrationView. + calibrationView = CalibrationView(sceneView: arView.sceneView, cameraController: arView.cameraController) + calibrationView?.alpha = 0.0 + // Set up the `sceneInfo` array with our scene init functions and labels. sceneInfo.append(contentsOf: [(sceneFunction: streetsScene, label: "Streets - Full Scale", tableTop: false), (sceneFunction: imageryScene, label: "Imagery - Full Scale", tableTop: false), @@ -145,16 +146,26 @@ class ARExample: UIViewController { /// /// - Parameter sender: The bar button item tapped on. @objc func displayCalibration(_ sender: UIBarButtonItem) { + // If the sceneView's alpha is 0.0, that means we are not in calibration mode and we need to start calibrating. let startCalibrating = (calibrationView?.alpha == 0.0) - + // Enable/disable sceneView touch interactions. arView.sceneView.interactionOptions.isEnabled = startCalibrating userDirectionsView.updateUserDirections(nil) // Display calibration view. - UIView.animate(withDuration: 0.25) { [weak self] in + UIView.animate(withDuration: 0.25, animations: { [weak self] in + if startCalibrating { + self?.arView.sceneView.isAttributionTextVisible = false + self?.addCalibrationView() + } self?.calibrationView?.alpha = startCalibrating ? 1.0 : 0.0 + }) { [weak self] (_) in + if !startCalibrating { + self?.removeCalibrationView() + self?.arView.sceneView.isAttributionTextVisible = true + } } // Dim the sceneView if we're calibrating. @@ -444,7 +455,6 @@ extension ARExample { /// Add the calibration view to the view and setup constraints. func addCalibrationView() { - calibrationView = CalibrationView(sceneView: arView.sceneView, cameraController: arView.cameraController) guard let calibrationView = calibrationView else { return } view.addSubview(calibrationView) calibrationView.translatesAutoresizingMaskIntoConstraints = false @@ -454,8 +464,12 @@ extension ARExample { calibrationView.topAnchor.constraint(equalTo: view.topAnchor), calibrationView.bottomAnchor.constraint(equalTo: toolbar.topAnchor) ]) - - calibrationView.alpha = 0.0 + } + + /// Add the calibration view to the view and setup constraints. + func removeCalibrationView() { + guard let calibrationView = calibrationView else { return } + calibrationView.removeFromSuperview() } } From 241e7990cd8b5573ad347ab6ed7275bcd9e8e0c1 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Tue, 20 Aug 2019 10:36:04 -0500 Subject: [PATCH 105/147] Finalize calibration sliders; default to only use GPS once. --- .../Misc/CalibrationView.swift | 161 ++++++++++++++---- Toolkit/ArcGISToolkit/AR/ArcGISARView.swift | 4 +- 2 files changed, 130 insertions(+), 35 deletions(-) diff --git a/Examples/ArcGISToolkitExamples/Misc/CalibrationView.swift b/Examples/ArcGISToolkitExamples/Misc/CalibrationView.swift index 89ccaa51..d3e60a61 100644 --- a/Examples/ArcGISToolkitExamples/Misc/CalibrationView.swift +++ b/Examples/ArcGISToolkitExamples/Misc/CalibrationView.swift @@ -16,7 +16,7 @@ import UIKit import ArcGIS /// A view displaying controls for adjusting a scene view's location, heading, and elevation. Used to calibrate an AR session. -class CalibrationView: UIView, UIGestureRecognizerDelegate { +class CalibrationView: UIView { // The scene view displaying the scene. private var sceneView: AGSSceneView! @@ -38,19 +38,16 @@ class CalibrationView: UIView, UIGestureRecognizerDelegate { /// The UISlider used to adjust elevation. private let elevationSlider: UISlider = { let slider = UISlider(frame: .zero) - slider.minimumValue = -100.0 - slider.maximumValue = 100.0 - - // Rotate the slider so it slides up/down. - slider.transform = CGAffineTransform(rotationAngle: -CGFloat.pi/2) + slider.minimumValue = -50.0 + slider.maximumValue = 50.0 return slider }() /// The UISlider used to adjust heading. private let headingSlider: UISlider = { let slider = UISlider(frame: .zero) - slider.minimumValue = -180.0 - slider.maximumValue = 180.0 + slider.minimumValue = -10.0 + slider.maximumValue = 10.0 return slider }() @@ -70,10 +67,6 @@ class CalibrationView: UIView, UIGestureRecognizerDelegate { self.cameraController = cameraController self.sceneView = sceneView - - // Set a corner radius on the directions label. -// calibrationDirectionsLabel.layer.cornerRadius = 8.0 -// calibrationDirectionsLabel.layer.masksToBounds = true // Create visual effects view to show the label on a blurred background. let labelView = UIVisualEffectView(effect: UIBlurEffect(style: .light)) @@ -98,26 +91,54 @@ class CalibrationView: UIView, UIGestureRecognizerDelegate { labelView.topAnchor.constraint(equalTo: topAnchor, constant: 88.0) ]) - // Add the elevation slider. - addSubview(elevationSlider) - elevationSlider.addTarget(self, action: #selector(elevationChanged(_:)), for: .valueChanged) - elevationSlider.translatesAutoresizingMaskIntoConstraints = false - let width: CGFloat = 500.0 + // Add the heading label and slider. + let headingLabel = UILabel(frame: .zero) + headingLabel.text = "Heading" + headingLabel.textColor = .yellow + addSubview(headingLabel) + headingLabel.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ - elevationSlider.centerYAnchor.constraint(equalTo: centerYAnchor), - elevationSlider.widthAnchor.constraint(greaterThanOrEqualToConstant: width), - elevationSlider.trailingAnchor.constraint(equalTo: trailingAnchor, constant: width / 2.0 - 36) + headingLabel.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 16), + headingLabel.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -16) ]) - // Add the heading slider. addSubview(headingSlider) - headingSlider.addTarget(self, action: #selector(headingChanged(_:)), for: .valueChanged) headingSlider.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ - headingSlider.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 24), - headingSlider.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -24), - headingSlider.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -24) + headingSlider.leadingAnchor.constraint(equalTo: headingLabel.trailingAnchor, constant: 16), + headingSlider.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -16), + headingSlider.centerYAnchor.constraint(equalTo: headingLabel.centerYAnchor) + ]) + + // Add the elevation label and slider. + let elevationLabel = UILabel(frame: .zero) + elevationLabel.text = "Elevation" + elevationLabel.textColor = .yellow + addSubview(elevationLabel) + elevationLabel.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([ + elevationLabel.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 16), + elevationLabel.bottomAnchor.constraint(equalTo: headingLabel.topAnchor, constant: -24) + ]) + + addSubview(elevationSlider) + elevationSlider.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([ + elevationSlider.leadingAnchor.constraint(equalTo: elevationLabel.trailingAnchor, constant: 16), + elevationSlider.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -16), + elevationSlider.centerYAnchor.constraint(equalTo: elevationLabel.centerYAnchor) ]) + + // Setup actions for the two sliders. The sliders operate as "joysticks", where moving the slider thumb will start a timer + // which roates or elevates the current camera when the timer fires. The elevation and heading delta + // values increase the further you move away from center. Moving and holding the thumb a little bit from center + // will roate/elevate just a little bit, but get progressively more the further from center the thumb is moved. + headingSlider.addTarget(self, action: #selector(headingChanged(_:)), for: .valueChanged) + headingSlider.addTarget(self, action: #selector(touchUpHeading(_:)), for: .touchUpInside) + + elevationSlider.addTarget(self, action: #selector(elevationChanged(_:)), for: .valueChanged) + elevationSlider.addTarget(self, action: #selector(touchUpElevation(_:)), for: .touchUpInside) + } required init?(coder aDecoder: NSCoder) { @@ -126,6 +147,8 @@ class CalibrationView: UIView, UIGestureRecognizerDelegate { override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { // If the user tapped in the view (and not in the sliders), do not handle the event. + // This allows the view below the calibration view to handle touch events. In this case, + // that view is the SceneView. let hitView = super.hitTest(point, with: event) if hitView == self { return nil; @@ -134,22 +157,94 @@ class CalibrationView: UIView, UIGestureRecognizerDelegate { } } + // The timers for the "joystick" behavior. + private var elevationTimer: Timer? + private var headingTimer: Timer? + /// Handle an elevation slider value-changed event. /// /// - Parameter sender: The slider tapped on. - @objc func elevationChanged(_ sender: UISlider){ - let camera = cameraController.originCamera - cameraController.originCamera = camera.elevate(withDeltaAltitude: Double(sender.value - lastElevationValue)) - lastElevationValue = sender.value + @objc func elevationChanged(_ sender: UISlider) { + if elevationTimer == nil { + // Create a timer which elevates the camera when fired. + elevationTimer = Timer(timeInterval: 0.25, repeats: true, block: { [weak self] (timer) in + let delta = self?.joystickElevation() ?? 0.0 + print("elevate delta = \(delta)") + self?.elevate(delta) + }) + + // Add the timer to the main run loop. + guard let timer = elevationTimer else { return } + RunLoop.main.add(timer, forMode: .default) + } } /// Handle an heading slider value-changed event. /// /// - Parameter sender: The slider tapped on. - @objc func headingChanged(_ sender: UISlider){ + @objc func headingChanged(_ sender: UISlider) { + if headingTimer == nil { + // Create a timer which rotates the camera when fired. + headingTimer = Timer(timeInterval: 0.25, repeats: true, block: { [weak self] (timer) in + let delta = self?.joystickHeading() ?? 0.0 + print("rotate delta = \(delta)") + self?.rotate(delta) + }) + + // Add the timer to the main run loop. + guard let timer = headingTimer else { return } + RunLoop.main.add(timer, forMode: .default) + } + } + + /// Handle an elevation slider touchUp event. This will stop the timer. + /// + /// - Parameter sender: The slider tapped on. + @objc func touchUpElevation(_ sender: UISlider) { + elevationTimer?.invalidate() + elevationTimer = nil + sender.value = 0.0 + } + + /// Handle a heading slider touchUp event. This will stop the timer. + /// + /// - Parameter sender: The slider tapped on. + @objc func touchUpHeading(_ sender: UISlider) { + headingTimer?.invalidate() + headingTimer = nil + sender.value = 0.0 + } + + /// Rotates the camera by `deltaHeading`. + /// + /// - Parameter deltaHeading: The amount to rotate the camera. + private func rotate(_ deltaHeading: Double) { + let camera = cameraController.originCamera + let newHeading = camera.heading + deltaHeading + cameraController.originCamera = camera.rotate(toHeading: newHeading, pitch: camera.pitch, roll: camera.roll) + } + + /// Change the cameras altitude by `deltaAltitude`. + /// + /// - Parameter deltaAltitude: The amount to elevate the camera. + private func elevate(_ deltaAltitude: Double) { let camera = cameraController.originCamera - let newHeading = Float(camera.heading) + sender.value - lastHeadingValue - cameraController.originCamera = camera.rotate(toHeading: Double(newHeading), pitch: camera.pitch, roll: camera.roll) - lastHeadingValue = sender.value + cameraController.originCamera = camera.elevate(withDeltaAltitude: deltaAltitude) + } + + /// Calculates the elevation delta amount based on the elevation slider value. + /// + /// - Returns: The elevation delta. + private func joystickElevation() -> Double { + let deltaElevation = Double(elevationSlider.value) + return pow(deltaElevation, 2) / 10.0 * (deltaElevation < 0 ? -1.0 : 1.0) + } + + /// Calculates the heading delta amount based on the heading slider value. + /// + /// - Returns: The heading delta. + private func joystickHeading() -> Double { + let deltaHeading = Double(headingSlider.value) + return pow(deltaHeading, 2) / 10.0 * (deltaHeading < 0 ? -1.0 : 1.0) } } diff --git a/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift b/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift index 4ab09f47..478fe3bd 100644 --- a/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift +++ b/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift @@ -96,7 +96,7 @@ public class ArcGISARView: UIView { private var lastGoodDeviceOrientation = UIDeviceOrientation.portrait /// Are we using only the first location provided by the LocationDataSource? - private var useLocationDataSourceOnce = false + private var useLocationDataSourceOnce = true // MARK: Initializers @@ -238,7 +238,7 @@ public class ArcGISARView: UIView { /// Starts device tracking. /// /// - Parameter completion: The completion handler called when start tracking completes. If tracking starts successfully, the `error` property will be nil; if tracking fails to start, the error will be non-nil and contain the reason for failure. - public func startTracking(useLocationDataSourceOnce: Bool = false, completion: ((_ error: Error?) -> Void)? = nil) { + public func startTracking(useLocationDataSourceOnce: Bool = true, completion: ((_ error: Error?) -> Void)? = nil) { // We have a location data source that needs to be started. self.useLocationDataSourceOnce = useLocationDataSourceOnce if let locationDataSource = self.locationDataSource { From 0141631f45d663aa76ddf1b1c7c76dc7037e3e58 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Tue, 20 Aug 2019 12:18:46 -0500 Subject: [PATCH 106/147] PR review changes; bug fixes; hide directions view when calibrating. --- .../ArcGISToolkitExamples/ARExample.swift | 136 ++++++++---------- .../Misc/CalibrationView.swift | 17 +-- .../Misc/UserDirectionsView.swift | 7 +- Toolkit/ArcGISToolkit/AR/ArcGISARView.swift | 10 +- 4 files changed, 77 insertions(+), 93 deletions(-) diff --git a/Examples/ArcGISToolkitExamples/ARExample.swift b/Examples/ArcGISToolkitExamples/ARExample.swift index 80161b76..3a222e62 100644 --- a/Examples/ArcGISToolkitExamples/ARExample.swift +++ b/Examples/ArcGISToolkitExamples/ARExample.swift @@ -18,14 +18,14 @@ import ArcGIS class ARExample: UIViewController { - typealias sceneInitFunction = () -> AGSScene - typealias sceneInfoType = (sceneFunction: sceneInitFunction, label: String, tableTop: Bool) + typealias SceneInitFunction = () -> AGSScene + typealias SceneInfoType = (sceneFunction: SceneInitFunction, label: String, tableTop: Bool, continuousLocation: Bool) - /// The scene creation functions plus labels and whehter it represents a table top experience. The functions create a new scene and perform any necessary `ArcGISARView` initialization. This allows for changing the scene and AR "mode" (table top or full-scale). - private var sceneInfo: [sceneInfoType] = [] + /// The scene creation functions plus labels and whether it represents a table top experience. The functions create a new scene and perform any necessary `ArcGISARView` initialization. This allows for changing the scene and AR "mode" (table top or full-scale). + private var sceneInfo: [SceneInfoType] = [] /// The current scene info. - private var currentSceneInfo: sceneInfoType? { + private var currentSceneInfo: SceneInfoType? { didSet { guard let label = currentSceneInfo?.label else { return } statusViewController?.currentScene = label @@ -58,7 +58,7 @@ class ARExample: UIViewController { /// View for displaying directions to the user. private let userDirectionsView = UserDirectionsView(effect: UIBlurEffect(style: .light)) - /// The observer for the `SceneView`'s `translationFactor` property. + /// The observation for the `SceneView`'s `translationFactor` property. private var translationFactorObservation: NSKeyValueObservation? /// View for displaying calibration controls to the user. @@ -88,7 +88,7 @@ class ARExample: UIViewController { arView.sceneView.graphicsOverlays.add(graphicsOverlay) // Observe the `arView.translationFactor` property and update status when it changes. - translationFactorObservation = arView.observe(\ArcGISARView.translationFactor, options: [.initial, .new]){ [weak self] arView, change in + translationFactorObservation = arView.observe(\ArcGISARView.translationFactor, options: [.initial, .new]) { [weak self] arView, change in self?.statusViewController?.translationFactor = arView.translationFactor } @@ -116,12 +116,12 @@ class ARExample: UIViewController { calibrationView?.alpha = 0.0 // Set up the `sceneInfo` array with our scene init functions and labels. - sceneInfo.append(contentsOf: [(sceneFunction: streetsScene, label: "Streets - Full Scale", tableTop: false), - (sceneFunction: imageryScene, label: "Imagery - Full Scale", tableTop: false), - (sceneFunction: pointCloudScene, label: "Point Cloud - Tabletop", tableTop: true), - (sceneFunction: yosemiteScene, label: "Yosemite - Tabletop", tableTop: true), - (sceneFunction: borderScene, label: "US - Mexico Border - Tabletop", tableTop: true), - (sceneFunction: emptyScene, label: "Empty - Full Scale", tableTop: false)]) + sceneInfo.append(contentsOf: [(sceneFunction: streetsScene, label: "Streets - Full Scale", tableTop: false, continuousLocation: true), + (sceneFunction: imageryScene, label: "Imagery - Full Scale", tableTop: false, continuousLocation: true), + (sceneFunction: pointCloudScene, label: "Point Cloud - Tabletop", tableTop: true, continuousLocation: false), + (sceneFunction: yosemiteScene, label: "Yosemite - Tabletop", tableTop: true, continuousLocation: false), + (sceneFunction: borderScene, label: "US - Mexico Border - Tabletop", tableTop: true, continuousLocation: false), + (sceneFunction: emptyScene, label: "Empty - Full Scale", tableTop: false, continuousLocation: false)]) // Use the first sceneInfo to create and set the scene. currentSceneInfo = sceneInfo.first @@ -130,9 +130,9 @@ class ARExample: UIViewController { override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) - arView.startTracking { [weak self] (error) in + arView.startTracking(useLocationDataSourceOnce: currentSceneInfo?.continuousLocation ?? false, completion: { [weak self] (error) in self?.statusViewController?.errorMessage = error?.localizedDescription - } + }) } override func viewDidDisappear(_ animated: Bool) { @@ -155,16 +155,16 @@ class ARExample: UIViewController { userDirectionsView.updateUserDirections(nil) // Display calibration view. - UIView.animate(withDuration: 0.25, animations: { [weak self] in + UIView.animate(withDuration: 0.25, animations: { if startCalibrating { - self?.arView.sceneView.isAttributionTextVisible = false - self?.addCalibrationView() + self.arView.sceneView.isAttributionTextVisible = false + self.addCalibrationView() } - self?.calibrationView?.alpha = startCalibrating ? 1.0 : 0.0 - }) { [weak self] (_) in + self.calibrationView?.alpha = startCalibrating ? 1.0 : 0.0 + }) { (_) in if !startCalibrating { - self?.removeCalibrationView() - self?.arView.sceneView.isAttributionTextVisible = true + self.removeCalibrationView() + self.arView.sceneView.isAttributionTextVisible = true } } @@ -172,40 +172,43 @@ class ARExample: UIViewController { UIView.animate(withDuration: 0.25) { [weak self] in self?.arView.sceneView.alpha = startCalibrating ? 0.65 : 1.0 } + + // Hide directions view if we're calibrating. + userDirectionsView.isHidden = startCalibrating } /// Allow users to change the current scene. /// /// - Parameter sender: The bar button item tapped on. - @objc func changeScene(_ sender: UIBarButtonItem){ + @objc func changeScene(_ sender: UIBarButtonItem) { // Display an alert controller displaying the scenes to choose from. let alertController = UIAlertController(title: nil, message: nil, preferredStyle: UIAlertController.Style.actionSheet) alertController.popoverPresentationController?.barButtonItem = sender // Loop through all sceneInfos and add `UIAlertActions` for each. sceneInfo.forEach { info in - let action = UIAlertAction(title: info.label, style: .default, handler: { [weak self] (action) in + let action = UIAlertAction(title: info.label, style: .default, handler: { (action) in // Set currentSceneInfo to the selected scene. - self?.currentSceneInfo = info + self.currentSceneInfo = info // Stop tracking, update the scene with the selected Scene and reset tracking. - self?.arView.stopTracking() - self?.arView.sceneView.scene = info.sceneFunction() + self.arView.stopTracking() + self.arView.sceneView.scene = info.sceneFunction() if info.tableTop { // Dim the SceneView until the user taps on a surface. - self?.arView.sceneView.alpha = 0.5 + self.arView.sceneView.alpha = 0.5 } // Reset AR tracking and then start tracking. - self?.arView.resetTracking() - self?.arView.startTracking { [weak self] (error) in - self?.statusViewController?.errorMessage = error?.localizedDescription - } + self.arView.resetTracking() + self.arView.startTracking(useLocationDataSourceOnce: info.continuousLocation, completion: { [weak self] (error) in + self?.statusViewController?.errorMessage = error?.localizedDescription + }) // Reset didPlaceScene variable - self?.didPlaceScene = false + self.didPlaceScene = false }) // Display current scene as disabled. - action.isEnabled = !(info.label == currentSceneInfo?.label) + action.isEnabled = (info.label != currentSceneInfo?.label) alertController.addAction(action) } present(alertController, animated: true) @@ -214,7 +217,7 @@ class ARExample: UIViewController { /// Dislays the status view controller. /// /// - Parameter sender: The bar button item tapped on. - @objc func showStatus(_ sender: UIBarButtonItem){ + @objc func showStatus(_ sender: UIBarButtonItem) { UIView.animate(withDuration: 0.25) { [weak self] in self?.statusViewController?.view.alpha = self?.statusViewController?.view.alpha == 1.0 ? 0.0 : 1.0 } @@ -223,11 +226,11 @@ class ARExample: UIViewController { /// Sets up the toolbar and add it to the view. private func addToolbar() { // Add it to the arView and set up constraints. - arView.addSubview(toolbar) + view.addSubview(toolbar) toolbar.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ - toolbar.leadingAnchor.constraint(equalTo: arView.sceneView.leadingAnchor), - toolbar.trailingAnchor.constraint(equalTo: arView.sceneView.trailingAnchor), + toolbar.leadingAnchor.constraint(equalTo: view.leadingAnchor), + toolbar.trailingAnchor.constraint(equalTo: view.trailingAnchor), toolbar.bottomAnchor.constraint(equalTo: arView.sceneView.attributionTopAnchor) ]) @@ -315,9 +318,9 @@ extension ARExample: ARSCNViewDelegate { // Present an alert describing the error. let alertController = UIAlertController(title: "Could not start tracking.", message: errorMessage, preferredStyle: .alert) let restartAction = UIAlertAction(title: "Restart Tracking", style: .default) { _ in - self?.arView.startTracking { [weak self] (error) in + self?.arView.startTracking(useLocationDataSourceOnce: self?.currentSceneInfo?.continuousLocation ?? false, completion: { (error) in self?.statusViewController?.errorMessage = error?.localizedDescription - } + }) } alertController.addAction(restartAction) @@ -375,24 +378,16 @@ extension ARExample: AGSGeoViewTouchDelegate { // Create and place a graphic and shadown at the real world location. let shadowColor = UIColor.lightGray.withAlphaComponent(0.5) let shadow = AGSSimpleMarkerSceneSymbol(style: .sphere, color: shadowColor, height: 0.01, width: 0.25, depth: 0.25, anchorPosition: .center) - let shadowGraphic = AGSGraphic(geometry: point, symbol: shadow, attributes: nil) + let shadowGraphic = AGSGraphic(geometry: point, symbol: shadow) graphicsOverlay.graphics.add(shadowGraphic) let sphere = AGSSimpleMarkerSceneSymbol(style: .sphere, color: .red, height: 0.25, width: 0.25, depth: 0.25, anchorPosition: .bottom) - let sphereGraphic = AGSGraphic(geometry: point, symbol: sphere, attributes: nil) + let sphereGraphic = AGSGraphic(geometry: point, symbol: sphere) graphicsOverlay.graphics.add(sphereGraphic) } } } -// MARK: UIAdaptivePresentationControllerDelegate -extension ARExample: UIAdaptivePresentationControllerDelegate { - func adaptivePresentationStyle(for controller: UIPresentationController, traitCollection: UITraitCollection) -> UIModalPresentationStyle { - // show presented controller as popovers even on small displays - return .none - } -} - // MARK: User Directions View extension ARExample { @@ -402,7 +397,7 @@ extension ARExample { userDirectionsView.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ userDirectionsView.centerXAnchor.constraint(equalTo: view.centerXAnchor), - userDirectionsView.topAnchor.constraint(equalTo: view.topAnchor, constant: 88.0) + userDirectionsView.topAnchor.constraint(equalToSystemSpacingBelow: view.safeAreaLayoutGuide.topAnchor, multiplier: 1) ]) } @@ -415,35 +410,27 @@ extension ARExample { var message = "" switch trackingState { - case .normal where frame.anchors.isEmpty: - if let sceneInfo = currentSceneInfo, sceneInfo.tableTop, !didPlaceScene { - message = "Move the device around to detect horizontal surfaces." - } - break - case .normal where !frame.anchors.isEmpty: + case .normal: if let sceneInfo = currentSceneInfo, sceneInfo.tableTop, !didPlaceScene { - message = "Tap to place the Scene on a surface." + if frame.anchors.isEmpty { + message = "Move the device around to detect horizontal surfaces." + } else { + message = "Tap to place the Scene on a surface." + } } - break case .notAvailable: message = "Location not available." - break case .limited(let reason): - switch(reason){ + switch(reason) { case .excessiveMotion: message = "Try moving your device more slowly." - break case .initializing: message = "Keep moving your device." - break case .insufficientFeatures: message = "Try turning on more lights and moving around." - break default: break } - default: - break } userDirectionsView.updateUserDirections(message) @@ -528,17 +515,14 @@ extension ARExample { layer.load { [weak self] (error) in self?.statusViewController?.errorMessage = error?.localizedDescription - if let _ = error { - return + if let extent = layer.fullExtent, error == nil { + let center = extent.center + + // Create the origin camera at the center point of the data. This will ensure the data is anchored to the table. + let camera = AGSCamera(latitude: center.y, longitude: center.x, altitude: 0, heading: 0, pitch: 90.0, roll: 0) + self?.arView.originCamera = camera + self?.arView.translationFactor = 2000 } - - guard let extent = layer.fullExtent else { return } - let center = extent.center - - // Create the origin camera at the center point of the data. This will ensure the data is anchored to the table. - let camera = AGSCamera(latitude: center.y, longitude: center.x, altitude: 0, heading: 0, pitch: 90.0, roll: 0) - self?.arView.originCamera = camera - self?.arView.translationFactor = 2000 } // Clear the location data source, as we're setting the originCamera directly. diff --git a/Examples/ArcGISToolkitExamples/Misc/CalibrationView.swift b/Examples/ArcGISToolkitExamples/Misc/CalibrationView.swift index d3e60a61..49c1d57d 100644 --- a/Examples/ArcGISToolkitExamples/Misc/CalibrationView.swift +++ b/Examples/ArcGISToolkitExamples/Misc/CalibrationView.swift @@ -19,10 +19,10 @@ import ArcGIS class CalibrationView: UIView { // The scene view displaying the scene. - private var sceneView: AGSSceneView! + private let sceneView: AGSSceneView /// The camera controller used to adjust user interactions. - private var cameraController: AGSTransformationMatrixCameraController! + private let cameraController: AGSTransformationMatrixCameraController /// The label displaying calibration directions. private let calibrationDirectionsLabel: UILabel = { @@ -63,10 +63,11 @@ class CalibrationView: UIView { /// - sceneView: The scene view displaying the scene. /// - cameraController: The camera controller used to adjust user interactions. init(sceneView: AGSSceneView, cameraController: AGSTransformationMatrixCameraController) { - super.init(frame: .zero) - self.cameraController = cameraController self.sceneView = sceneView + + super.init(frame: .zero) + // Create visual effects view to show the label on a blurred background. let labelView = UIVisualEffectView(effect: UIBlurEffect(style: .light)) @@ -151,9 +152,9 @@ class CalibrationView: UIView { // that view is the SceneView. let hitView = super.hitTest(point, with: event) if hitView == self { - return nil; + return nil } else { - return hitView; + return hitView } } @@ -169,7 +170,7 @@ class CalibrationView: UIView { // Create a timer which elevates the camera when fired. elevationTimer = Timer(timeInterval: 0.25, repeats: true, block: { [weak self] (timer) in let delta = self?.joystickElevation() ?? 0.0 - print("elevate delta = \(delta)") +// print("elevate delta = \(delta)") self?.elevate(delta) }) @@ -187,7 +188,7 @@ class CalibrationView: UIView { // Create a timer which rotates the camera when fired. headingTimer = Timer(timeInterval: 0.25, repeats: true, block: { [weak self] (timer) in let delta = self?.joystickHeading() ?? 0.0 - print("rotate delta = \(delta)") +// print("rotate delta = \(delta)") self?.rotate(delta) }) diff --git a/Examples/ArcGISToolkitExamples/Misc/UserDirectionsView.swift b/Examples/ArcGISToolkitExamples/Misc/UserDirectionsView.swift index 863c54fd..7dc54f46 100644 --- a/Examples/ArcGISToolkitExamples/Misc/UserDirectionsView.swift +++ b/Examples/ArcGISToolkitExamples/Misc/UserDirectionsView.swift @@ -29,9 +29,10 @@ class UserDirectionsView: UIVisualEffectView { override init(effect: UIVisualEffect?) { super.init(effect: effect) - // Set a corner radius. - layer.cornerRadius = 8.0 - layer.masksToBounds = true + + // Set a corner radius. + layer.cornerRadius = 8.0 + layer.masksToBounds = true contentView.addSubview(userDirectionsLabel) userDirectionsLabel.translatesAutoresizingMaskIntoConstraints = false diff --git a/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift b/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift index 478fe3bd..c5d81c22 100644 --- a/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift +++ b/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift @@ -74,7 +74,7 @@ public class ArcGISARView: UIView { didSet { // If we're already tracking, reset tracking to use the new configuration. if isTracking, isUsingARKit { - arSCNView.session.run(arConfiguration, options: [.resetTracking]) + arSCNView.session.run(arConfiguration, options: .resetTracking) } } } @@ -271,7 +271,7 @@ public class ArcGISARView: UIView { guard let strongSelf = self else { return } // Run the ARSession. if strongSelf.isUsingARKit { - strongSelf.arSCNView.session.run(strongSelf.arConfiguration, options: [.resetTracking]) + strongSelf.arSCNView.session.run(strongSelf.arConfiguration, options: .resetTracking) } strongSelf.isTracking = true @@ -468,14 +468,12 @@ extension ArcGISARView: AGSLocationChangeHandlerDelegate { // Location changed. guard var locationPoint = location.position else { return } - print("New Location") - // The AGSCLLocationDataSource does not include altitude information from the CLLocation when // creating the `AGSLocation` geometry, so grab the altitude directly from the CLLocationManager. if let clLocationDataSource = locationDataSource as? AGSCLLocationDataSource, let location = clLocationDataSource.locationManager.location, location.verticalAccuracy >= 0 { - let altitude = location.altitude, + let altitude = location.altitude locationPoint = AGSPoint(x: locationPoint.x, y: locationPoint.y, z: altitude, spatialReference: locationPoint.spatialReference) } @@ -493,7 +491,7 @@ extension ArcGISARView: AGSLocationChangeHandlerDelegate { // If we're using ARKit, reset its tracking. if isUsingARKit { - arSCNView.session.run(arConfiguration, options: [.resetTracking]) + arSCNView.session.run(arConfiguration, options: .resetTracking) } // Reset the camera controller's transformationMatrix to its initial state, the Idenity matrix. From ff4e4b1088203f020ec802ab2f114bb4636fbf11 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Wed, 21 Aug 2019 11:48:04 -0500 Subject: [PATCH 107/147] Fix constraints to safeArea; handle .touchUpOutside as well as .touchUpInside. --- .../Misc/CalibrationView.swift | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/Examples/ArcGISToolkitExamples/Misc/CalibrationView.swift b/Examples/ArcGISToolkitExamples/Misc/CalibrationView.swift index 49c1d57d..e1c5bb34 100644 --- a/Examples/ArcGISToolkitExamples/Misc/CalibrationView.swift +++ b/Examples/ArcGISToolkitExamples/Misc/CalibrationView.swift @@ -89,7 +89,7 @@ class CalibrationView: UIView { labelView.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ labelView.centerXAnchor.constraint(equalTo: centerXAnchor), - labelView.topAnchor.constraint(equalTo: topAnchor, constant: 88.0) + labelView.topAnchor.constraint(equalTo: safeAreaLayoutGuide.topAnchor, constant: 8.0) ]) // Add the heading label and slider. @@ -99,15 +99,15 @@ class CalibrationView: UIView { addSubview(headingLabel) headingLabel.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ - headingLabel.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 16), - headingLabel.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -16) + headingLabel.leadingAnchor.constraint(equalTo: safeAreaLayoutGuide.leadingAnchor, constant: 16), + headingLabel.bottomAnchor.constraint(equalTo: safeAreaLayoutGuide.bottomAnchor, constant: -16) ]) addSubview(headingSlider) headingSlider.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ headingSlider.leadingAnchor.constraint(equalTo: headingLabel.trailingAnchor, constant: 16), - headingSlider.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -16), + headingSlider.trailingAnchor.constraint(equalTo: safeAreaLayoutGuide.trailingAnchor, constant: -16), headingSlider.centerYAnchor.constraint(equalTo: headingLabel.centerYAnchor) ]) @@ -118,7 +118,7 @@ class CalibrationView: UIView { addSubview(elevationLabel) elevationLabel.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ - elevationLabel.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 16), + elevationLabel.leadingAnchor.constraint(equalTo: safeAreaLayoutGuide.leadingAnchor, constant: 16), elevationLabel.bottomAnchor.constraint(equalTo: headingLabel.topAnchor, constant: -24) ]) @@ -126,7 +126,7 @@ class CalibrationView: UIView { elevationSlider.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ elevationSlider.leadingAnchor.constraint(equalTo: elevationLabel.trailingAnchor, constant: 16), - elevationSlider.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -16), + elevationSlider.trailingAnchor.constraint(equalTo: safeAreaLayoutGuide.trailingAnchor, constant: -16), elevationSlider.centerYAnchor.constraint(equalTo: elevationLabel.centerYAnchor) ]) @@ -135,10 +135,10 @@ class CalibrationView: UIView { // values increase the further you move away from center. Moving and holding the thumb a little bit from center // will roate/elevate just a little bit, but get progressively more the further from center the thumb is moved. headingSlider.addTarget(self, action: #selector(headingChanged(_:)), for: .valueChanged) - headingSlider.addTarget(self, action: #selector(touchUpHeading(_:)), for: .touchUpInside) + headingSlider.addTarget(self, action: #selector(touchUpHeading(_:)), for: [.touchUpInside, .touchUpOutside]) elevationSlider.addTarget(self, action: #selector(elevationChanged(_:)), for: .valueChanged) - elevationSlider.addTarget(self, action: #selector(touchUpElevation(_:)), for: .touchUpInside) + elevationSlider.addTarget(self, action: #selector(touchUpElevation(_:)), for: [.touchUpInside, .touchUpOutside]) } From 2784f0b617a8a18899fe7b8047a8ba50b5ab938b Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Wed, 21 Aug 2019 14:44:11 -0500 Subject: [PATCH 108/147] Adjust slider delta speed and values; fix `continuousGPS` issue. --- .../ArcGISToolkitExamples/ARExample.swift | 20 +++++++++---------- .../Misc/CalibrationView.swift | 6 +++--- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/Examples/ArcGISToolkitExamples/ARExample.swift b/Examples/ArcGISToolkitExamples/ARExample.swift index 3a222e62..0bacd5ba 100644 --- a/Examples/ArcGISToolkitExamples/ARExample.swift +++ b/Examples/ArcGISToolkitExamples/ARExample.swift @@ -19,7 +19,7 @@ import ArcGIS class ARExample: UIViewController { typealias SceneInitFunction = () -> AGSScene - typealias SceneInfoType = (sceneFunction: SceneInitFunction, label: String, tableTop: Bool, continuousLocation: Bool) + typealias SceneInfoType = (sceneFunction: SceneInitFunction, label: String, tableTop: Bool, useLocationDataSourceOnce: Bool) /// The scene creation functions plus labels and whether it represents a table top experience. The functions create a new scene and perform any necessary `ArcGISARView` initialization. This allows for changing the scene and AR "mode" (table top or full-scale). private var sceneInfo: [SceneInfoType] = [] @@ -116,12 +116,12 @@ class ARExample: UIViewController { calibrationView?.alpha = 0.0 // Set up the `sceneInfo` array with our scene init functions and labels. - sceneInfo.append(contentsOf: [(sceneFunction: streetsScene, label: "Streets - Full Scale", tableTop: false, continuousLocation: true), - (sceneFunction: imageryScene, label: "Imagery - Full Scale", tableTop: false, continuousLocation: true), - (sceneFunction: pointCloudScene, label: "Point Cloud - Tabletop", tableTop: true, continuousLocation: false), - (sceneFunction: yosemiteScene, label: "Yosemite - Tabletop", tableTop: true, continuousLocation: false), - (sceneFunction: borderScene, label: "US - Mexico Border - Tabletop", tableTop: true, continuousLocation: false), - (sceneFunction: emptyScene, label: "Empty - Full Scale", tableTop: false, continuousLocation: false)]) + sceneInfo.append(contentsOf: [(sceneFunction: streetsScene, label: "Streets - Full Scale", tableTop: false, useLocationDataSourceOnce: false), + (sceneFunction: imageryScene, label: "Imagery - Full Scale", tableTop: false, useLocationDataSourceOnce: false), + (sceneFunction: pointCloudScene, label: "Point Cloud - Tabletop", tableTop: true, useLocationDataSourceOnce: true), + (sceneFunction: yosemiteScene, label: "Yosemite - Tabletop", tableTop: true, useLocationDataSourceOnce: true), + (sceneFunction: borderScene, label: "US - Mexico Border - Tabletop", tableTop: true, useLocationDataSourceOnce: true), + (sceneFunction: emptyScene, label: "Empty - Full Scale", tableTop: false, useLocationDataSourceOnce: true)]) // Use the first sceneInfo to create and set the scene. currentSceneInfo = sceneInfo.first @@ -130,7 +130,7 @@ class ARExample: UIViewController { override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) - arView.startTracking(useLocationDataSourceOnce: currentSceneInfo?.continuousLocation ?? false, completion: { [weak self] (error) in + arView.startTracking(useLocationDataSourceOnce: currentSceneInfo?.useLocationDataSourceOnce ?? false, completion: { [weak self] (error) in self?.statusViewController?.errorMessage = error?.localizedDescription }) } @@ -200,7 +200,7 @@ class ARExample: UIViewController { } // Reset AR tracking and then start tracking. self.arView.resetTracking() - self.arView.startTracking(useLocationDataSourceOnce: info.continuousLocation, completion: { [weak self] (error) in + self.arView.startTracking(useLocationDataSourceOnce: info.useLocationDataSourceOnce, completion: { [weak self] (error) in self?.statusViewController?.errorMessage = error?.localizedDescription }) @@ -318,7 +318,7 @@ extension ARExample: ARSCNViewDelegate { // Present an alert describing the error. let alertController = UIAlertController(title: "Could not start tracking.", message: errorMessage, preferredStyle: .alert) let restartAction = UIAlertAction(title: "Restart Tracking", style: .default) { _ in - self?.arView.startTracking(useLocationDataSourceOnce: self?.currentSceneInfo?.continuousLocation ?? false, completion: { (error) in + self?.arView.startTracking(useLocationDataSourceOnce: self?.currentSceneInfo?.useLocationDataSourceOnce ?? false, completion: { (error) in self?.statusViewController?.errorMessage = error?.localizedDescription }) } diff --git a/Examples/ArcGISToolkitExamples/Misc/CalibrationView.swift b/Examples/ArcGISToolkitExamples/Misc/CalibrationView.swift index e1c5bb34..14cb1944 100644 --- a/Examples/ArcGISToolkitExamples/Misc/CalibrationView.swift +++ b/Examples/ArcGISToolkitExamples/Misc/CalibrationView.swift @@ -186,7 +186,7 @@ class CalibrationView: UIView { @objc func headingChanged(_ sender: UISlider) { if headingTimer == nil { // Create a timer which rotates the camera when fired. - headingTimer = Timer(timeInterval: 0.25, repeats: true, block: { [weak self] (timer) in + headingTimer = Timer(timeInterval: 0.1, repeats: true, block: { [weak self] (timer) in let delta = self?.joystickHeading() ?? 0.0 // print("rotate delta = \(delta)") self?.rotate(delta) @@ -238,7 +238,7 @@ class CalibrationView: UIView { /// - Returns: The elevation delta. private func joystickElevation() -> Double { let deltaElevation = Double(elevationSlider.value) - return pow(deltaElevation, 2) / 10.0 * (deltaElevation < 0 ? -1.0 : 1.0) + return pow(deltaElevation, 2) / 50.0 * (deltaElevation < 0 ? -1.0 : 1.0) } /// Calculates the heading delta amount based on the heading slider value. @@ -246,6 +246,6 @@ class CalibrationView: UIView { /// - Returns: The heading delta. private func joystickHeading() -> Double { let deltaHeading = Double(headingSlider.value) - return pow(deltaHeading, 2) / 10.0 * (deltaHeading < 0 ? -1.0 : 1.0) + return pow(deltaHeading, 2) / 25.0 * (deltaHeading < 0 ? -1.0 : 1.0) } } From b31b141bc378e2d9c3d89ed2718ff6293b7194ae Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Fri, 23 Aug 2019 11:09:51 -0500 Subject: [PATCH 109/147] Fix typo. --- Toolkit/ArcGISToolkit/AR/ArcGISARView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift b/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift index c5d81c22..bc4095ef 100644 --- a/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift +++ b/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift @@ -307,7 +307,7 @@ public class ArcGISARView: UIView { // Create our hit test matrix based on the worldTransform location. // right now we ignore the orientation of the plane that was hit to find the point // since we only use horizontal planes, when we will start using vertical planes - // we should stop suppressing the quternion rotation to a null rotation (0,0,0,1) + // we should stop suppressing the quaternion rotation to a null rotation (0,0,0,1) let hitTestMatrix = AGSTransformationMatrix(quaternionX: 0.0, quaternionY: 0.0, quaternionZ: 0.0, From 58379534aeb4ae72e4930a75355bfa0bc5fcc852 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Fri, 23 Aug 2019 11:33:50 -0500 Subject: [PATCH 110/147] Add cancel action for Change Scene alert. --- Examples/ArcGISToolkitExamples/ARExample.swift | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Examples/ArcGISToolkitExamples/ARExample.swift b/Examples/ArcGISToolkitExamples/ARExample.swift index 0bacd5ba..67616fe2 100644 --- a/Examples/ArcGISToolkitExamples/ARExample.swift +++ b/Examples/ArcGISToolkitExamples/ARExample.swift @@ -211,6 +211,11 @@ class ARExample: UIViewController { action.isEnabled = (info.label != currentSceneInfo?.label) alertController.addAction(action) } + + // Add "cancel" action. + let cancelAction = UIAlertAction(title: "Cancel", style: .cancel, handler: nil) + alertController.addAction(cancelAction) + present(alertController, animated: true) } From 5559efb5d8d9fd8ac2229757a3485214c5ddabef Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Mon, 26 Aug 2019 15:16:01 -0500 Subject: [PATCH 111/147] Add data source status, horizontal and vertical accuracy to status view; add locationDataSource delegate. --- .../ArcGISToolkitExamples/ARExample.swift | 21 ++++- .../Misc/ARStatusViewController.storyboard | 80 ++++++++++++++++++- .../Misc/ARStatusViewController.swift | 55 ++++++++++++- Toolkit/ArcGISToolkit/AR/ArcGISARView.swift | 31 +++++-- 4 files changed, 175 insertions(+), 12 deletions(-) diff --git a/Examples/ArcGISToolkitExamples/ARExample.swift b/Examples/ArcGISToolkitExamples/ARExample.swift index 67616fe2..3574c2a7 100644 --- a/Examples/ArcGISToolkitExamples/ARExample.swift +++ b/Examples/ArcGISToolkitExamples/ARExample.swift @@ -78,6 +78,9 @@ class ARExample: UIViewController { // Set ourself as touch delegate so we can get touch events. arView.sceneView.touchDelegate = self + // Set ourself as touch delegate so we can get touch events. + arView.locationChangeHandlerDelegate = self + // Disble user interactions on the sceneView. arView.sceneView.interactionOptions.isEnabled = false @@ -117,7 +120,7 @@ class ARExample: UIViewController { // Set up the `sceneInfo` array with our scene init functions and labels. sceneInfo.append(contentsOf: [(sceneFunction: streetsScene, label: "Streets - Full Scale", tableTop: false, useLocationDataSourceOnce: false), - (sceneFunction: imageryScene, label: "Imagery - Full Scale", tableTop: false, useLocationDataSourceOnce: false), + (sceneFunction: imageryScene, label: "Imagery - Full Scale", tableTop: false, useLocationDataSourceOnce: true), (sceneFunction: pointCloudScene, label: "Point Cloud - Tabletop", tableTop: true, useLocationDataSourceOnce: true), (sceneFunction: yosemiteScene, label: "Yosemite - Tabletop", tableTop: true, useLocationDataSourceOnce: true), (sceneFunction: borderScene, label: "US - Mexico Border - Tabletop", tableTop: true, useLocationDataSourceOnce: true), @@ -263,7 +266,7 @@ class ARExample: UIViewController { statusVC.didMove(toParent: self) statusVC.view.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ - statusVC.view.heightAnchor.constraint(equalToConstant: 110), + statusVC.view.heightAnchor.constraint(equalToConstant: 176), statusVC.view.widthAnchor.constraint(equalToConstant: 350), statusVC.view.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -8), statusVC.view.bottomAnchor.constraint(equalTo: toolbar.topAnchor, constant: -8) @@ -664,3 +667,17 @@ extension AGSScene { } } +// MARK: AGSLocationChangeHandlerDelegate methods +extension ARExample: AGSLocationChangeHandlerDelegate { + public func locationDataSource(_ locationDataSource: AGSLocationDataSource, locationDidChange location: AGSLocation) { + // When we get a new location, update the status view controller with the new horizontal and vertical accuracy. + statusViewController?.horizontalAccuracy = location.horizontalAccuracy + statusViewController?.verticalAccuracy = location.verticalAccuracy + } + + func locationDataSource(_ locationDataSource: AGSLocationDataSource, statusDidChange status: AGSLocationDataSourceStatus) { + // Update the data source status. + statusViewController?.locationDataSourceStatus = status + } +} + diff --git a/Examples/ArcGISToolkitExamples/Misc/ARStatusViewController.storyboard b/Examples/ArcGISToolkitExamples/Misc/ARStatusViewController.storyboard index 97db8d0e..52daece8 100644 --- a/Examples/ArcGISToolkitExamples/Misc/ARStatusViewController.storyboard +++ b/Examples/ArcGISToolkitExamples/Misc/ARStatusViewController.storyboard @@ -145,6 +145,81 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -156,14 +231,17 @@ + + + - + diff --git a/Examples/ArcGISToolkitExamples/Misc/ARStatusViewController.swift b/Examples/ArcGISToolkitExamples/Misc/ARStatusViewController.swift index d45e1a83..7c329726 100644 --- a/Examples/ArcGISToolkitExamples/Misc/ARStatusViewController.swift +++ b/Examples/ArcGISToolkitExamples/Misc/ARStatusViewController.swift @@ -13,7 +13,7 @@ import UIKit import ARKit - +import ArcGIS extension ARCamera.TrackingState { var description: String { switch self { @@ -33,6 +33,21 @@ extension ARCamera.TrackingState { } } +extension AGSLocationDataSourceStatus { + var description: String { + switch self { + case .stopped: + return "Stopped" + case .starting: + return "Starting" + case .started: + return "Started" + case .failedToStart: + return "Failed to start" + } + } +} + /// A view controller for display AR-related status information. class ARStatusViewController: UITableViewController { @@ -41,7 +56,10 @@ class ARStatusViewController: UITableViewController { @IBOutlet var errorDescriptionLabel: UILabel! @IBOutlet var sceneLabel: UILabel! @IBOutlet var translationFactorLabel: UILabel! - + @IBOutlet var horizontalAccuracyLabel: UILabel! + @IBOutlet var verticalAccuracyLabel: UILabel! + @IBOutlet var locationDataSourceStatusLabel: UILabel! + /// The `ARKit` camera tracking state. public var trackingState: ARCamera.TrackingState = .notAvailable { didSet { @@ -97,6 +115,39 @@ class ARStatusViewController: UITableViewController { } } + /// The horizontal accuracy of the last location. + public var horizontalAccuracy: Double = 1.0 { + didSet { + guard horizontalAccuracyLabel != nil else { return } + DispatchQueue.main.async{ [weak self] in + guard let self = self else { return } + self.horizontalAccuracyLabel.text = String(format: "%.0f", self.horizontalAccuracy) + } + } + } + + /// The vertical accuracy of the last location. + public var verticalAccuracy: Double = 1.0 { + didSet { + guard verticalAccuracyLabel != nil else { return } + DispatchQueue.main.async{ [weak self] in + guard let self = self else { return } + self.verticalAccuracyLabel.text = String(format: "%.0f", self.verticalAccuracy) + } + } + } + + /// The status of the location data source. + public var locationDataSourceStatus: AGSLocationDataSourceStatus = .stopped { + didSet { + guard locationDataSourceStatusLabel != nil else { return } + DispatchQueue.main.async{ [weak self] in + guard let self = self else { return } + self.locationDataSourceStatusLabel.text = self.locationDataSourceStatus.description + } + } + } + override func viewDidLoad() { super.viewDidLoad() diff --git a/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift b/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift index bc4095ef..85be776e 100644 --- a/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift +++ b/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift @@ -81,7 +81,10 @@ public class ArcGISARView: UIView { /// We implement `ARSCNViewDelegate` methods, but will use `arSCNViewDelegate` to forward them to clients. weak public var arSCNViewDelegate: ARSCNViewDelegate? - + + /// We implement `AGSLocationChangeHandlerDelegate` methods, but will use `locationChangeHandlerDelegate` to forward them to clients. + weak public var locationChangeHandlerDelegate: AGSLocationChangeHandlerDelegate? + // MARK: Private properties /// Used when calculating framerate. @@ -462,19 +465,29 @@ extension ArcGISARView: AGSLocationChangeHandlerDelegate { sceneView.setViewpointCamera(camera) // print("heading changed: \(heading)") } + + locationChangeHandlerDelegate?.locationDataSource?(locationDataSource, headingDidChange: heading) } public func locationDataSource(_ locationDataSource: AGSLocationDataSource, locationDidChange location: AGSLocation) { // Location changed. guard var locationPoint = location.position else { return } - +// print("AGS Horizontal Accuracy = \(location.horizontalAccuracy)") + // The AGSCLLocationDataSource does not include altitude information from the CLLocation when // creating the `AGSLocation` geometry, so grab the altitude directly from the CLLocationManager. - if let clLocationDataSource = locationDataSource as? AGSCLLocationDataSource, - let location = clLocationDataSource.locationManager.location, - location.verticalAccuracy >= 0 { - let altitude = location.altitude - locationPoint = AGSPoint(x: locationPoint.x, y: locationPoint.y, z: altitude, spatialReference: locationPoint.spatialReference) + if let clLocationDataSource = locationDataSource as? AGSCLLocationDataSource { + if let location = clLocationDataSource.locationManager.location, + location.verticalAccuracy >= 0 { + let altitude = location.altitude + locationPoint = AGSPoint(x: locationPoint.x, y: locationPoint.y, z: altitude, spatialReference: locationPoint.spatialReference) + print("Horizontal Accuracy = \(location.horizontalAccuracy)") + } + else { + // We don't have a valid altitude, so use the old altitude. + let oldLocationPoint = cameraController.originCamera.location + locationPoint = AGSPoint(x: locationPoint.x, y: locationPoint.y, z: oldLocationPoint.z, spatialReference: locationPoint.spatialReference) + } } // Always set originCamera; then reset ARKit @@ -501,11 +514,15 @@ extension ArcGISARView: AGSLocationChangeHandlerDelegate { // If we are only using the intitial data source location, stop the data source. locationDataSource.stop() } + + locationChangeHandlerDelegate?.locationDataSource?(locationDataSource, locationDidChange: location) + } public func locationDataSource(_ locationDataSource: AGSLocationDataSource, statusDidChange status: AGSLocationDataSourceStatus) { // Status changed. // print("locationDataSource status changed: \(status.rawValue)") + locationChangeHandlerDelegate?.locationDataSource?(locationDataSource, statusDidChange: status) } } From 80dc2851d78a7fa233b7a1b5e3eb70cf9f93b9f2 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Wed, 28 Aug 2019 10:08:55 -0500 Subject: [PATCH 112/147] Make ArcGISARView.cameraController private; for CalibrationView, just pass the ArcGISARView in the constructor. --- .../ArcGISToolkitExamples/ARExample.swift | 4 +-- .../Misc/CalibrationView.swift | 27 ++++++++----------- Toolkit/ArcGISToolkit/AR/ArcGISARView.swift | 8 +++--- 3 files changed, 17 insertions(+), 22 deletions(-) diff --git a/Examples/ArcGISToolkitExamples/ARExample.swift b/Examples/ArcGISToolkitExamples/ARExample.swift index 3574c2a7..3dc69f13 100644 --- a/Examples/ArcGISToolkitExamples/ARExample.swift +++ b/Examples/ArcGISToolkitExamples/ARExample.swift @@ -17,7 +17,7 @@ import ArcGISToolkit import ArcGIS class ARExample: UIViewController { - + typealias SceneInitFunction = () -> AGSScene typealias SceneInfoType = (sceneFunction: SceneInitFunction, label: String, tableTop: Bool, useLocationDataSourceOnce: Bool) @@ -115,7 +115,7 @@ class ARExample: UIViewController { addUserDirectionsView() // Create the CalibrationView. - calibrationView = CalibrationView(sceneView: arView.sceneView, cameraController: arView.cameraController) + calibrationView = CalibrationView(arView) calibrationView?.alpha = 0.0 // Set up the `sceneInfo` array with our scene init functions and labels. diff --git a/Examples/ArcGISToolkitExamples/Misc/CalibrationView.swift b/Examples/ArcGISToolkitExamples/Misc/CalibrationView.swift index 14cb1944..cd1a6d2a 100644 --- a/Examples/ArcGISToolkitExamples/Misc/CalibrationView.swift +++ b/Examples/ArcGISToolkitExamples/Misc/CalibrationView.swift @@ -14,15 +14,13 @@ import UIKit import ArcGIS +import ArcGISToolkit /// A view displaying controls for adjusting a scene view's location, heading, and elevation. Used to calibrate an AR session. class CalibrationView: UIView { - - // The scene view displaying the scene. - private let sceneView: AGSSceneView - /// The camera controller used to adjust user interactions. - private let cameraController: AGSTransformationMatrixCameraController + /// The `ArcGISARView` containing the origin camera we will be updating. + private var arcgisARView: ArcGISARView! /// The label displaying calibration directions. private let calibrationDirectionsLabel: UILabel = { @@ -57,18 +55,15 @@ class CalibrationView: UIView { // The last heading slider value. var lastHeadingValue: Float = 0 - /// Initialized a new calibration view with the given scene view and camera controller. + /// Initialized a new calibration view with the `ArcGISARView`. /// /// - Parameters: - /// - sceneView: The scene view displaying the scene. - /// - cameraController: The camera controller used to adjust user interactions. - init(sceneView: AGSSceneView, cameraController: AGSTransformationMatrixCameraController) { - self.cameraController = cameraController - self.sceneView = sceneView + /// - arcgisARView: The `ArcGISARView` containing the originCamera we're updating. + init(_ arcgisARView: ArcGISARView) { + self.arcgisARView = arcgisARView super.init(frame: .zero) - // Create visual effects view to show the label on a blurred background. let labelView = UIVisualEffectView(effect: UIBlurEffect(style: .light)) labelView.layer.cornerRadius = 8.0 @@ -220,17 +215,17 @@ class CalibrationView: UIView { /// /// - Parameter deltaHeading: The amount to rotate the camera. private func rotate(_ deltaHeading: Double) { - let camera = cameraController.originCamera + guard let camera = arcgisARView.originCamera else { return } let newHeading = camera.heading + deltaHeading - cameraController.originCamera = camera.rotate(toHeading: newHeading, pitch: camera.pitch, roll: camera.roll) + arcgisARView.originCamera = camera.rotate(toHeading: newHeading, pitch: camera.pitch, roll: camera.roll) } /// Change the cameras altitude by `deltaAltitude`. /// /// - Parameter deltaAltitude: The amount to elevate the camera. private func elevate(_ deltaAltitude: Double) { - let camera = cameraController.originCamera - cameraController.originCamera = camera.elevate(withDeltaAltitude: deltaAltitude) + guard let camera = arcgisARView.originCamera else { return } + arcgisARView.originCamera = camera.elevate(withDeltaAltitude: deltaAltitude) } /// Calculates the elevation delta amount based on the elevation slider value. diff --git a/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift b/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift index 85be776e..7162a94b 100644 --- a/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift +++ b/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift @@ -38,9 +38,6 @@ public class ArcGISARView: UIView { locationDataSource?.locationChangeHandlerDelegate = self } } - - /// The `AGSTransformationMatrixCameraController` used to control the Scene. - @objc public let cameraController = AGSTransformationMatrixCameraController() /// The viewpoint camera used to set the initial view of the sceneView instead of the device's GPS location via the location data source. You can use Key-Value Observing to track changes to the origin camera. @objc dynamic public var originCamera: AGSCamera? { @@ -86,7 +83,10 @@ public class ArcGISARView: UIView { weak public var locationChangeHandlerDelegate: AGSLocationChangeHandlerDelegate? // MARK: Private properties - + + /// The `AGSTransformationMatrixCameraController` used to control the Scene. + @objc private let cameraController = AGSTransformationMatrixCameraController() + /// Used when calculating framerate. private var lastUpdateTime: TimeInterval = 0 From 1140c5b46860a14b1442997c8c1050262c24d75e Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Wed, 28 Aug 2019 13:59:47 -0500 Subject: [PATCH 113/147] Allow user to place tabletop scene more than once; only display ARKit initializing message if we're not in Continuous GPS model --- .../ArcGISToolkitExamples/ARExample.swift | 49 ++++++++++--------- 1 file changed, 25 insertions(+), 24 deletions(-) diff --git a/Examples/ArcGISToolkitExamples/ARExample.swift b/Examples/ArcGISToolkitExamples/ARExample.swift index 3dc69f13..a4bcc638 100644 --- a/Examples/ArcGISToolkitExamples/ARExample.swift +++ b/Examples/ArcGISToolkitExamples/ARExample.swift @@ -17,7 +17,7 @@ import ArcGISToolkit import ArcGIS class ARExample: UIViewController { - + typealias SceneInitFunction = () -> AGSScene typealias SceneInfoType = (sceneFunction: SceneInitFunction, label: String, tableTop: Bool, useLocationDataSourceOnce: Bool) @@ -54,19 +54,19 @@ class ARExample: UIViewController { overlay.sceneProperties = AGSLayerSceneProperties(surfacePlacement: .absolute) return overlay }() - + /// View for displaying directions to the user. private let userDirectionsView = UserDirectionsView(effect: UIBlurEffect(style: .light)) /// The observation for the `SceneView`'s `translationFactor` property. private var translationFactorObservation: NSKeyValueObservation? - + /// View for displaying calibration controls to the user. private var calibrationView: CalibrationView? - + /// The toolbar used to display controls for calibration, changing scenes, and status. private var toolbar = UIToolbar(frame: .zero) - + // MARK: Initialization override func viewDidLoad() { @@ -80,7 +80,7 @@ class ARExample: UIViewController { // Set ourself as touch delegate so we can get touch events. arView.locationChangeHandlerDelegate = self - + // Disble user interactions on the sceneView. arView.sceneView.interactionOptions.isEnabled = false @@ -113,11 +113,11 @@ class ARExample: UIViewController { // Add the UserDirectionsView. addUserDirectionsView() - + // Create the CalibrationView. calibrationView = CalibrationView(arView) calibrationView?.alpha = 0.0 - + // Set up the `sceneInfo` array with our scene init functions and labels. sceneInfo.append(contentsOf: [(sceneFunction: streetsScene, label: "Streets - Full Scale", tableTop: false, useLocationDataSourceOnce: false), (sceneFunction: imageryScene, label: "Imagery - Full Scale", tableTop: false, useLocationDataSourceOnce: true), @@ -125,7 +125,7 @@ class ARExample: UIViewController { (sceneFunction: yosemiteScene, label: "Yosemite - Tabletop", tableTop: true, useLocationDataSourceOnce: true), (sceneFunction: borderScene, label: "US - Mexico Border - Tabletop", tableTop: true, useLocationDataSourceOnce: true), (sceneFunction: emptyScene, label: "Empty - Full Scale", tableTop: false, useLocationDataSourceOnce: true)]) - + // Use the first sceneInfo to create and set the scene. currentSceneInfo = sceneInfo.first arView.sceneView.scene = currentSceneInfo?.sceneFunction() @@ -142,7 +142,7 @@ class ARExample: UIViewController { super.viewDidDisappear(animated) arView.stopTracking() } - + // MARK: Toolbar button actions /// Initialize scene location/heading/elevation calibration. @@ -152,7 +152,7 @@ class ARExample: UIViewController { // If the sceneView's alpha is 0.0, that means we are not in calibration mode and we need to start calibrating. let startCalibrating = (calibrationView?.alpha == 0.0) - + // Enable/disable sceneView touch interactions. arView.sceneView.interactionOptions.isEnabled = startCalibrating userDirectionsView.updateUserDirections(nil) @@ -179,7 +179,7 @@ class ARExample: UIViewController { // Hide directions view if we're calibrating. userDirectionsView.isHidden = startCalibrating } - + /// Allow users to change the current scene. /// /// - Parameter sender: The bar button item tapped on. @@ -204,7 +204,7 @@ class ARExample: UIViewController { // Reset AR tracking and then start tracking. self.arView.resetTracking() self.arView.startTracking(useLocationDataSourceOnce: info.useLocationDataSourceOnce, completion: { [weak self] (error) in - self?.statusViewController?.errorMessage = error?.localizedDescription + self?.statusViewController?.errorMessage = error?.localizedDescription }) // Reset didPlaceScene variable @@ -214,11 +214,11 @@ class ARExample: UIViewController { action.isEnabled = (info.label != currentSceneInfo?.label) alertController.addAction(action) } - + // Add "cancel" action. let cancelAction = UIAlertAction(title: "Cancel", style: .cancel, handler: nil) alertController.addAction(cancelAction) - + present(alertController, animated: true) } @@ -244,20 +244,20 @@ class ARExample: UIViewController { // Create a toolbar button for calibration. let calibrationItem = UIBarButtonItem(title: "Calibration", style: .plain, target: self, action: #selector(displayCalibration(_:))) - + // Create a toolbar button to change the current scene. let sceneItem = UIBarButtonItem(title: "Change Scene", style: .plain, target: self, action: #selector(changeScene(_:))) // Create a toolbar button to display the status. let statusItem = UIBarButtonItem(title: "Status", style: .plain, target: self, action: #selector(showStatus(_:))) - + toolbar.setItems([calibrationItem, UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil), sceneItem, UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil), statusItem], animated: false) } - + /// Set up the status view controller and adds it to the view. private func addStatusViewController() { if let statusVC = statusViewController { @@ -366,7 +366,7 @@ extension ARExample: ARSessionDelegate { // MARK: AGSGeoViewTouchDelegate extension ARExample: AGSGeoViewTouchDelegate { public func geoView(_ geoView: AGSGeoView, didTapAtScreenPoint screenPoint: CGPoint, mapPoint: AGSPoint) { - if let sceneInfo = currentSceneInfo, sceneInfo.tableTop, !didPlaceScene { + if let sceneInfo = currentSceneInfo, sceneInfo.tableTop { // We're in table-top mode and haven't placed the scene yet. Place the scene at the given point by setting the initial transformation. if arView.setInitialTransformation(using: screenPoint) { // Show the SceneView now that the user has tapped on the surface. @@ -382,13 +382,13 @@ extension ARExample: AGSGeoViewTouchDelegate { else { // We're in full-scale AR mode or have already placed the scene. Get the real world location for screen point from arView. guard let point = arView.arScreenToLocation(screenPoint: screenPoint) else { return } - + // Create and place a graphic and shadown at the real world location. let shadowColor = UIColor.lightGray.withAlphaComponent(0.5) let shadow = AGSSimpleMarkerSceneSymbol(style: .sphere, color: shadowColor, height: 0.01, width: 0.25, depth: 0.25, anchorPosition: .center) let shadowGraphic = AGSGraphic(geometry: point, symbol: shadow) graphicsOverlay.graphics.add(shadowGraphic) - + let sphere = AGSSimpleMarkerSceneSymbol(style: .sphere, color: .red, height: 0.25, width: 0.25, depth: 0.25, anchorPosition: .bottom) let sphereGraphic = AGSGraphic(geometry: point, symbol: sphere) graphicsOverlay.graphics.add(sphereGraphic) @@ -433,7 +433,8 @@ extension ARExample { case .excessiveMotion: message = "Try moving your device more slowly." case .initializing: - message = "Keep moving your device." + // Because ARKit gets reset often when using continuous GPS, only dipslay initializing message if we're using the initial GPS. + message = (currentSceneInfo?.useLocationDataSourceOnce ?? false) ? "Keep moving your device." : "" case .insufficientFeatures: message = "Try turning on more lights and moving around." default: @@ -633,7 +634,7 @@ extension ARExample { arView.locationDataSource = nil return scene } - + /// Creates an empty scene with an elevation source. /// Mode: Full-Scale AR /// @@ -649,7 +650,7 @@ extension ARExample { return scene } } - + // MARK: AGSScene extension. extension AGSScene { /// Adds an elevation source to the given `scene`. From 1e38e734eed42d38b8f0256a7c78f55213966ef4 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Thu, 29 Aug 2019 13:14:53 -0500 Subject: [PATCH 114/147] Add "Since" tag to public API; remove commented out lines and dead code; update ScreenToLocation to deal with translationFactor. --- Toolkit/ArcGISToolkit/AR/ArcGISARView.swift | 57 ++++++++++----------- 1 file changed, 28 insertions(+), 29 deletions(-) diff --git a/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift b/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift index 7162a94b..b25c24f4 100644 --- a/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift +++ b/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift @@ -21,18 +21,23 @@ public class ArcGISARView: UIView { // MARK: public properties /// The view used to display the `ARKit` camera image and 3D `SceneKit` content. + /// - Since: 100.6.0 public let arSCNView = ARSCNView(frame: .zero) /// The initial transformation used for a table top experience. Defaults to the Identity Matrix. + /// - Since: 100.6.0 public var initialTransformation: AGSTransformationMatrix = .identity /// Denotes whether tracking location and angles has started. + /// - Since: 100.6.0 public private(set) var isTracking: Bool = false /// Denotes whether ARKit is being used to track location and angles. + /// - Since: 100.6.0 public private(set) var isUsingARKit: Bool = true /// The data source used to get device location. Used either in conjuction with ARKit data or when ARKit is not present or not being used. + /// - Since: 100.6.0 public var locationDataSource: AGSCLLocationDataSource? { didSet { locationDataSource?.locationChangeHandlerDelegate = self @@ -40,6 +45,7 @@ public class ArcGISARView: UIView { } /// The viewpoint camera used to set the initial view of the sceneView instead of the device's GPS location via the location data source. You can use Key-Value Observing to track changes to the origin camera. + /// - Since: 100.6.0 @objc dynamic public var originCamera: AGSCamera? { didSet { guard let newCamera = originCamera else { return } @@ -49,9 +55,11 @@ public class ArcGISARView: UIView { } /// The view used to display ArcGIS 3D content. + /// - Since: 100.6.0 public let sceneView = AGSSceneView(frame: .zero) /// The translation factor used to support a table top AR experience. + /// - Since: 100.6.0 @objc dynamic public var translationFactor: Double { get { return cameraController.translationFactor @@ -62,6 +70,7 @@ public class ArcGISARView: UIView { } /// The world tracking information used by `ARKit`. + /// - Since: 100.6.0 public var arConfiguration: ARConfiguration = { let config = ARWorldTrackingConfiguration() config.worldAlignment = .gravityAndHeading @@ -77,18 +86,17 @@ public class ArcGISARView: UIView { } /// We implement `ARSCNViewDelegate` methods, but will use `arSCNViewDelegate` to forward them to clients. + /// - Since: 100.6.0 weak public var arSCNViewDelegate: ARSCNViewDelegate? /// We implement `AGSLocationChangeHandlerDelegate` methods, but will use `locationChangeHandlerDelegate` to forward them to clients. + /// - Since: 100.6.0 weak public var locationChangeHandlerDelegate: AGSLocationChangeHandlerDelegate? // MARK: Private properties /// The `AGSTransformationMatrixCameraController` used to control the Scene. @objc private let cameraController = AGSTransformationMatrixCameraController() - - /// Used when calculating framerate. - private var lastUpdateTime: TimeInterval = 0 /// Whether `ARKit` is supported on this device. private let deviceSupportsARKit: Bool = { @@ -118,6 +126,7 @@ public class ArcGISARView: UIView { /// - Parameters: /// - renderVideoFeed: Whether to display the live camera image. /// - tryUsingARKit: Whether or not to use ARKit, regardless if it's available. + /// - Since: 100.6.0 public convenience init(renderVideoFeed: Bool, tryUsingARKit: Bool){ self.init(frame: .zero) @@ -187,33 +196,29 @@ public class ArcGISARView: UIView { /// /// - Parameter screenPoint: The point in screen coordinates. /// - Returns: The map point corresponding to screenPoint. + /// - Since: 100.6.0 public func arScreenToLocation(screenPoint: CGPoint) -> AGSPoint? { // Use the `internalHitTest` method to get the matrix of `screenPoint`. guard let localOffsetMatrix = internalHitTest(screenPoint: screenPoint) else { return nil } - -// print("Local offset XYZ, World origin XYZ, Combined world coordinate XYZ") -// -// //TODO: generalize the debug print function -// // print("lqx: \(localOffsetMatrix.quaternionX); lqy: \(localOffsetMatrix.quaternionY); lqz: \(localOffsetMatrix.quaternionZ); lqw: \(localOffsetMatrix.quaternionW); ltx: \(localOffsetMatrix.translationX); lty: \(localOffsetMatrix.translationY); ltz: \(localOffsetMatrix.translationZ)") -// print("\(localOffsetMatrix.translationX) \(localOffsetMatrix.translationY) \(localOffsetMatrix.translationZ)") - let currOriginCamera = cameraController.originCamera - let currOriginMatrix = currOriginCamera.transformationMatrix -// -// // print("oqx: \(currOriginMatrix.quaternionX); oqy: \(currOriginMatrix.quaternionY); oqz: \(currOriginMatrix.quaternionZ); oqw: \(currOriginMatrix.quaternionW); otx: \(currOriginMatrix.translationX); oty: \(currOriginMatrix.translationY); otz: \(currOriginMatrix.translationZ)") -// print("\(currOriginMatrix.translationX) \(currOriginMatrix.translationY) \(currOriginMatrix.translationZ)") - - //TODO: for tabletop application scale translation by TranslationFactor - let mapPointMatrix = currOriginMatrix.addTransformation(localOffsetMatrix) -// -// // print("cqx: \(mapPointMatrix.quaternionX); cqy: \(mapPointMatrix.quaternionY); cqz: \(mapPointMatrix.quaternionZ); cqw: \(mapPointMatrix.quaternionW); ctx: \(mapPointMatrix.translationX); cty: \(mapPointMatrix.translationY); ctz: \(mapPointMatrix.translationZ)") -// print("\(mapPointMatrix.translationX) \(mapPointMatrix.translationY) \(mapPointMatrix.translationZ)") + let currOriginMatrix = cameraController.originCamera.transformationMatrix + + // Scale translation by translationFactor. + let translatedMatrix = AGSTransformationMatrix(quaternionX: localOffsetMatrix.quaternionX, + quaternionY: localOffsetMatrix.quaternionY, + quaternionZ: localOffsetMatrix.quaternionZ, + quaternionW: localOffsetMatrix.quaternionW, + translationX: localOffsetMatrix.translationX * translationFactor, + translationY: localOffsetMatrix.translationY * translationFactor, + translationZ: localOffsetMatrix.translationZ * translationFactor) + let mapPointMatrix = currOriginMatrix.addTransformation(translatedMatrix) // Create a camera from transformationMatrix and return its location. return AGSCamera(transformationMatrix: mapPointMatrix).location } /// Resets the device tracking and related properties. + /// - Since: 100.6.0 public func resetTracking() { originCamera = nil initialTransformation = .identity @@ -228,6 +233,7 @@ public class ArcGISARView: UIView { /// /// - Parameter screenPoint: The screen point to determine the `initialTransformation` from. /// - Returns: Whether setting the `initialTransformation` succeeded or failed. + /// - Since: 100.6.0 public func setInitialTransformation(using screenPoint: CGPoint) -> Bool { // Use the `internalHitTest` method to get the matrix of `screenPoint`. guard let matrix = internalHitTest(screenPoint: screenPoint) else { return false } @@ -241,6 +247,7 @@ public class ArcGISARView: UIView { /// Starts device tracking. /// /// - Parameter completion: The completion handler called when start tracking completes. If tracking starts successfully, the `error` property will be nil; if tracking fails to start, the error will be non-nil and contain the reason for failure. + /// - Since: 100.6.0 public func startTracking(useLocationDataSourceOnce: Bool = true, completion: ((_ error: Error?) -> Void)? = nil) { // We have a location data source that needs to be started. self.useLocationDataSourceOnce = useLocationDataSourceOnce @@ -260,6 +267,7 @@ public class ArcGISARView: UIView { } /// Suspends device tracking. + /// - Since: 100.6.0 public func stopTracking() { arSCNView.session.pause() locationDataSource?.stop() @@ -438,11 +446,6 @@ extension ArcGISARView: SCNSceneRendererDelegate { // Render the Scene with the new transformation. sceneView.renderFrame() - - // Calculate frame rate. -// let frametime = time - lastUpdateTime -// print("Frame rate = \(String(reflecting: Int((1.0 / frametime).rounded())))") -// lastUpdateTime = time // Call our arSCNViewDelegate method. arSCNViewDelegate?.renderer?(renderer, willRenderScene: scene, atTime: time) @@ -463,7 +466,6 @@ extension ArcGISARView: AGSLocationChangeHandlerDelegate { let currentCamera = sceneView.currentViewpointCamera() let camera = currentCamera.rotate(toHeading: heading, pitch: currentCamera.pitch, roll: currentCamera.roll) sceneView.setViewpointCamera(camera) -// print("heading changed: \(heading)") } locationChangeHandlerDelegate?.locationDataSource?(locationDataSource, headingDidChange: heading) @@ -472,7 +474,6 @@ extension ArcGISARView: AGSLocationChangeHandlerDelegate { public func locationDataSource(_ locationDataSource: AGSLocationDataSource, locationDidChange location: AGSLocation) { // Location changed. guard var locationPoint = location.position else { return } -// print("AGS Horizontal Accuracy = \(location.horizontalAccuracy)") // The AGSCLLocationDataSource does not include altitude information from the CLLocation when // creating the `AGSLocation` geometry, so grab the altitude directly from the CLLocationManager. @@ -481,7 +482,6 @@ extension ArcGISARView: AGSLocationChangeHandlerDelegate { location.verticalAccuracy >= 0 { let altitude = location.altitude locationPoint = AGSPoint(x: locationPoint.x, y: locationPoint.y, z: altitude, spatialReference: locationPoint.spatialReference) - print("Horizontal Accuracy = \(location.horizontalAccuracy)") } else { // We don't have a valid altitude, so use the old altitude. @@ -521,7 +521,6 @@ extension ArcGISARView: AGSLocationChangeHandlerDelegate { public func locationDataSource(_ locationDataSource: AGSLocationDataSource, statusDidChange status: AGSLocationDataSourceStatus) { // Status changed. -// print("locationDataSource status changed: \(status.rawValue)") locationChangeHandlerDelegate?.locationDataSource?(locationDataSource, statusDidChange: status) } } From 7956f048c66de6419298301bf55ae518afb15f13 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Thu, 29 Aug 2019 13:33:52 -0500 Subject: [PATCH 115/147] Use actual size of plane for hit detection; add ARSCNView debug options but leave them commented out. --- Examples/ArcGISToolkitExamples/ARExample.swift | 3 +++ Toolkit/ArcGISToolkit/AR/ArcGISARView.swift | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/Examples/ArcGISToolkitExamples/ARExample.swift b/Examples/ArcGISToolkitExamples/ARExample.swift index a4bcc638..f85ae8e9 100644 --- a/Examples/ArcGISToolkitExamples/ARExample.swift +++ b/Examples/ArcGISToolkitExamples/ARExample.swift @@ -129,6 +129,9 @@ class ARExample: UIViewController { // Use the first sceneInfo to create and set the scene. currentSceneInfo = sceneInfo.first arView.sceneView.scene = currentSceneInfo?.sceneFunction() + + // Debug options for showing world origin and point cloud scene analysis points. +// arView.arSCNView.debugOptions = [.showWorldOrigin, .showFeaturePoints] } override func viewDidAppear(_ animated: Bool) { diff --git a/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift b/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift index b25c24f4..ac0a0947 100644 --- a/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift +++ b/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift @@ -310,7 +310,7 @@ public class ArcGISARView: UIView { /// - Returns: An `AGSTransformationMatrix` representing the real-world point corresponding to `screenPoint`. fileprivate func internalHitTest(screenPoint: CGPoint) -> AGSTransformationMatrix? { // Use the `hitTest` method on ARSCNView to get the location of `screenPoint`. - let results = arSCNView.hitTest(screenPoint, types: [.existingPlane, .estimatedHorizontalPlane]) + let results = arSCNView.hitTest(screenPoint, types: .existingPlaneUsingExtent) // Get the worldTransform from the first result; if there's no worldTransform, return nil. guard let worldTransform = results.first?.worldTransform else { return nil } From 66c30632c27d96bdca770f91413083238311ff9c Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Fri, 30 Aug 2019 15:35:33 -0500 Subject: [PATCH 116/147] Final Example app pieces from #205 --- .../ArcGISToolkitExamples/ARExample.swift | 21 +++++++++++++------ .../Misc/CalibrationView.swift | 18 +++++++++++++--- 2 files changed, 30 insertions(+), 9 deletions(-) diff --git a/Examples/ArcGISToolkitExamples/ARExample.swift b/Examples/ArcGISToolkitExamples/ARExample.swift index f85ae8e9..e3a4c5db 100644 --- a/Examples/ArcGISToolkitExamples/ARExample.swift +++ b/Examples/ArcGISToolkitExamples/ARExample.swift @@ -67,6 +67,12 @@ class ARExample: UIViewController { /// The toolbar used to display controls for calibration, changing scenes, and status. private var toolbar = UIToolbar(frame: .zero) + /// Button used to display the `CalibrationView`. + private let calibrationItem = UIBarButtonItem(title: "Calibration", style: .plain, target: self, action: #selector(displayCalibration(_:))) + + /// Button used to change the current scene. + private let sceneItem = UIBarButtonItem(title: "Change Scene", style: .plain, target: self, action: #selector(changeScene(_:))) + // MARK: Initialization override func viewDidLoad() { @@ -181,6 +187,9 @@ class ARExample: UIViewController { // Hide directions view if we're calibrating. userDirectionsView.isHidden = startCalibrating + + // Disable changing scenes if we're calibrating. + sceneItem.isEnabled = !startCalibrating } /// Allow users to change the current scene. @@ -210,6 +219,12 @@ class ARExample: UIViewController { self?.statusViewController?.errorMessage = error?.localizedDescription }) + // Disable elevation control if we're using continuous GPS. + self.calibrationView?.elevationControlVisibility = info.useLocationDataSourceOnce + + // Disable calibration if we're in table top + self.calibrationItem.isEnabled = !info.tableTop + // Reset didPlaceScene variable self.didPlaceScene = false }) @@ -245,12 +260,6 @@ class ARExample: UIViewController { toolbar.bottomAnchor.constraint(equalTo: arView.sceneView.attributionTopAnchor) ]) - // Create a toolbar button for calibration. - let calibrationItem = UIBarButtonItem(title: "Calibration", style: .plain, target: self, action: #selector(displayCalibration(_:))) - - // Create a toolbar button to change the current scene. - let sceneItem = UIBarButtonItem(title: "Change Scene", style: .plain, target: self, action: #selector(changeScene(_:))) - // Create a toolbar button to display the status. let statusItem = UIBarButtonItem(title: "Status", style: .plain, target: self, action: #selector(showStatus(_:))) diff --git a/Examples/ArcGISToolkitExamples/Misc/CalibrationView.swift b/Examples/ArcGISToolkitExamples/Misc/CalibrationView.swift index cd1a6d2a..7486bac7 100644 --- a/Examples/ArcGISToolkitExamples/Misc/CalibrationView.swift +++ b/Examples/ArcGISToolkitExamples/Misc/CalibrationView.swift @@ -19,6 +19,14 @@ import ArcGISToolkit /// A view displaying controls for adjusting a scene view's location, heading, and elevation. Used to calibrate an AR session. class CalibrationView: UIView { + /// Denotes whether to show the elevation control and label; defaults to `true`. + public var elevationControlVisibility: Bool = true { + didSet { + elevationSlider.isHidden = !elevationControlVisibility + elevationLabel.isHidden = !elevationControlVisibility + } + } + /// The `ArcGISARView` containing the origin camera we will be updating. private var arcgisARView: ArcGISARView! @@ -50,10 +58,13 @@ class CalibrationView: UIView { }() /// The last elevation slider value. - var lastElevationValue: Float = 0 + private var lastElevationValue: Float = 0 // The last heading slider value. - var lastHeadingValue: Float = 0 + private var lastHeadingValue: Float = 0 + + /// The elevation label.. + private let elevationLabel = UILabel(frame: .zero) /// Initialized a new calibration view with the `ArcGISARView`. /// @@ -107,7 +118,6 @@ class CalibrationView: UIView { ]) // Add the elevation label and slider. - let elevationLabel = UILabel(frame: .zero) elevationLabel.text = "Elevation" elevationLabel.textColor = .yellow addSubview(elevationLabel) @@ -135,6 +145,8 @@ class CalibrationView: UIView { elevationSlider.addTarget(self, action: #selector(elevationChanged(_:)), for: .valueChanged) elevationSlider.addTarget(self, action: #selector(touchUpElevation(_:)), for: [.touchUpInside, .touchUpOutside]) + elevationSlider.isHidden = !elevationControlVisibility + elevationLabel.isHidden = !elevationControlVisibility } required init?(coder aDecoder: NSCoder) { From 9f1c25db36b87f9aba66d273330c52ea11371ac2 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Thu, 5 Sep 2019 11:25:49 -0500 Subject: [PATCH 117/147] Add back in empty line and use optional chaining for labels. --- .../Misc/ARStatusViewController.swift | 25 +++++++------------ 1 file changed, 9 insertions(+), 16 deletions(-) diff --git a/Examples/ArcGISToolkitExamples/Misc/ARStatusViewController.swift b/Examples/ArcGISToolkitExamples/Misc/ARStatusViewController.swift index 7c329726..d69f4abe 100644 --- a/Examples/ArcGISToolkitExamples/Misc/ARStatusViewController.swift +++ b/Examples/ArcGISToolkitExamples/Misc/ARStatusViewController.swift @@ -14,6 +14,7 @@ import UIKit import ARKit import ArcGIS + extension ARCamera.TrackingState { var description: String { switch self { @@ -63,10 +64,9 @@ class ARStatusViewController: UITableViewController { /// The `ARKit` camera tracking state. public var trackingState: ARCamera.TrackingState = .notAvailable { didSet { - guard trackingStateLabel != nil else { return } DispatchQueue.main.async{ [weak self] in guard let self = self else { return } - self.trackingStateLabel.text = self.trackingState.description + self.trackingStateLabel?.text = self.trackingState.description } } } @@ -74,10 +74,9 @@ class ARStatusViewController: UITableViewController { /// The calculated frame rate of the `SceneView` and `ARKit` display. public var frameRate: Int = 0 { didSet { - guard frameRateLabel != nil else { return } DispatchQueue.main.async{ [weak self] in guard let self = self else { return } - self.frameRateLabel.text = "\(self.frameRate)" + self.frameRateLabel?.text = "\(self.frameRate)" } } } @@ -85,10 +84,9 @@ class ARStatusViewController: UITableViewController { /// The current error message. public var errorMessage: String? { didSet { - guard errorDescriptionLabel != nil else { return } DispatchQueue.main.async{ [weak self] in guard let self = self else { return } - self.errorDescriptionLabel.text = self.errorMessage + self.errorDescriptionLabel?.text = self.errorMessage } } } @@ -96,10 +94,9 @@ class ARStatusViewController: UITableViewController { /// The label for the currently selected scene. public var currentScene: String = "None" { didSet { - guard sceneLabel != nil else { return } DispatchQueue.main.async{ [weak self] in guard let self = self else { return } - self.sceneLabel.text = self.currentScene + self.sceneLabel?.text = self.currentScene } } } @@ -107,10 +104,9 @@ class ARStatusViewController: UITableViewController { /// The translation factor applied to the current scene. public var translationFactor: Double = 1.0 { didSet { - guard translationFactorLabel != nil else { return } DispatchQueue.main.async{ [weak self] in guard let self = self else { return } - self.translationFactorLabel.text = String(format: "%.2f", self.translationFactor) + self.translationFactorLabel?.text = String(format: "%.2f", self.translationFactor) } } } @@ -118,10 +114,9 @@ class ARStatusViewController: UITableViewController { /// The horizontal accuracy of the last location. public var horizontalAccuracy: Double = 1.0 { didSet { - guard horizontalAccuracyLabel != nil else { return } DispatchQueue.main.async{ [weak self] in guard let self = self else { return } - self.horizontalAccuracyLabel.text = String(format: "%.0f", self.horizontalAccuracy) + self.horizontalAccuracyLabel?.text = String(format: "%.0f", self.horizontalAccuracy) } } } @@ -129,10 +124,9 @@ class ARStatusViewController: UITableViewController { /// The vertical accuracy of the last location. public var verticalAccuracy: Double = 1.0 { didSet { - guard verticalAccuracyLabel != nil else { return } DispatchQueue.main.async{ [weak self] in guard let self = self else { return } - self.verticalAccuracyLabel.text = String(format: "%.0f", self.verticalAccuracy) + self.verticalAccuracyLabel?.text = String(format: "%.0f", self.verticalAccuracy) } } } @@ -140,10 +134,9 @@ class ARStatusViewController: UITableViewController { /// The status of the location data source. public var locationDataSourceStatus: AGSLocationDataSourceStatus = .stopped { didSet { - guard locationDataSourceStatusLabel != nil else { return } DispatchQueue.main.async{ [weak self] in guard let self = self else { return } - self.locationDataSourceStatusLabel.text = self.locationDataSourceStatus.description + self.locationDataSourceStatusLabel?.text = self.locationDataSourceStatus.description } } } From 095ae7f46f1f13c74bcf21ba1abe84f94c2bb4d1 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Thu, 5 Sep 2019 11:45:04 -0500 Subject: [PATCH 118/147] Use Measurements instead of double for accuracy values; tweak display of frame rate and translation factor. --- Examples/ArcGISToolkitExamples/ARExample.swift | 4 ++-- .../Misc/ARStatusViewController.swift | 18 ++++++++++++------ 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/Examples/ArcGISToolkitExamples/ARExample.swift b/Examples/ArcGISToolkitExamples/ARExample.swift index e3a4c5db..31302fe5 100644 --- a/Examples/ArcGISToolkitExamples/ARExample.swift +++ b/Examples/ArcGISToolkitExamples/ARExample.swift @@ -684,8 +684,8 @@ extension AGSScene { extension ARExample: AGSLocationChangeHandlerDelegate { public func locationDataSource(_ locationDataSource: AGSLocationDataSource, locationDidChange location: AGSLocation) { // When we get a new location, update the status view controller with the new horizontal and vertical accuracy. - statusViewController?.horizontalAccuracy = location.horizontalAccuracy - statusViewController?.verticalAccuracy = location.verticalAccuracy + statusViewController?.horizontalAccuracyMeasurement = Measurement(value: location.horizontalAccuracy, unit: UnitLength.meters) + statusViewController?.verticalAccuracyMeasurement = Measurement(value: location.verticalAccuracy, unit: UnitLength.meters) } func locationDataSource(_ locationDataSource: AGSLocationDataSource, statusDidChange status: AGSLocationDataSourceStatus) { diff --git a/Examples/ArcGISToolkitExamples/Misc/ARStatusViewController.swift b/Examples/ArcGISToolkitExamples/Misc/ARStatusViewController.swift index d69f4abe..eab062dd 100644 --- a/Examples/ArcGISToolkitExamples/Misc/ARStatusViewController.swift +++ b/Examples/ArcGISToolkitExamples/Misc/ARStatusViewController.swift @@ -76,7 +76,7 @@ class ARStatusViewController: UITableViewController { didSet { DispatchQueue.main.async{ [weak self] in guard let self = self else { return } - self.frameRateLabel?.text = "\(self.frameRate)" + self.frameRateLabel?.text = "\(self.frameRate) fps" } } } @@ -106,27 +106,27 @@ class ARStatusViewController: UITableViewController { didSet { DispatchQueue.main.async{ [weak self] in guard let self = self else { return } - self.translationFactorLabel?.text = String(format: "%.2f", self.translationFactor) + self.translationFactorLabel?.text = String(format: "%.1f", self.translationFactor) } } } /// The horizontal accuracy of the last location. - public var horizontalAccuracy: Double = 1.0 { + public var horizontalAccuracyMeasurement = Measurement(value: 1, unit: UnitLength.meters) { didSet { DispatchQueue.main.async{ [weak self] in guard let self = self else { return } - self.horizontalAccuracyLabel?.text = String(format: "%.0f", self.horizontalAccuracy) + self.horizontalAccuracyLabel?.text = self.measurementFormatter.string(from: self.horizontalAccuracyMeasurement) } } } /// The vertical accuracy of the last location. - public var verticalAccuracy: Double = 1.0 { + public var verticalAccuracyMeasurement = Measurement(value: 1, unit: UnitLength.meters) { didSet { DispatchQueue.main.async{ [weak self] in guard let self = self else { return } - self.verticalAccuracyLabel?.text = String(format: "%.0f", self.verticalAccuracy) + self.verticalAccuracyLabel?.text = self.measurementFormatter.string(from: self.verticalAccuracyMeasurement) } } } @@ -141,6 +141,12 @@ class ARStatusViewController: UITableViewController { } } + private let measurementFormatter: MeasurementFormatter = { + let formatter = MeasurementFormatter() + formatter.unitOptions = [.naturalScale, .providedUnit] + return formatter + }() + override func viewDidLoad() { super.viewDidLoad() From 3052160fcd2decbc7bbc8766da2a6dd04670563f Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Thu, 5 Sep 2019 14:03:40 -0500 Subject: [PATCH 119/147] Add ARLocationTrackingMode enum; update StartTracking method to take new enum; update originCamera property/code now that we have a proper enum. --- .../ArcGISToolkitExamples/ARExample.swift | 98 +++++++++++-------- .../Misc/CalibrationView.swift | 5 +- Toolkit/ArcGISToolkit/AR/ArcGISARView.swift | 73 ++++++++------ 3 files changed, 100 insertions(+), 76 deletions(-) diff --git a/Examples/ArcGISToolkitExamples/ARExample.swift b/Examples/ArcGISToolkitExamples/ARExample.swift index 31302fe5..bf864a2b 100644 --- a/Examples/ArcGISToolkitExamples/ARExample.swift +++ b/Examples/ArcGISToolkitExamples/ARExample.swift @@ -19,7 +19,7 @@ import ArcGIS class ARExample: UIViewController { typealias SceneInitFunction = () -> AGSScene - typealias SceneInfoType = (sceneFunction: SceneInitFunction, label: String, tableTop: Bool, useLocationDataSourceOnce: Bool) + typealias SceneInfoType = (sceneFunction: SceneInitFunction, label: String, tableTop: Bool, trackingMode: ARLocationTrackingMode) /// The scene creation functions plus labels and whether it represents a table top experience. The functions create a new scene and perform any necessary `ArcGISARView` initialization. This allows for changing the scene and AR "mode" (table top or full-scale). private var sceneInfo: [SceneInfoType] = [] @@ -33,7 +33,7 @@ class ARExample: UIViewController { } /// The `ArcGISARView` that displays the camera feed and handles ARKit functionality. - private let arView = ArcGISARView(renderVideoFeed: true, tryUsingARKit: true) + private let arView = ArcGISARView(renderVideoFeed: true) /// Denotes whether we've placed the scene in table top experiences. private var didPlaceScene: Bool = false @@ -72,7 +72,7 @@ class ARExample: UIViewController { /// Button used to change the current scene. private let sceneItem = UIBarButtonItem(title: "Change Scene", style: .plain, target: self, action: #selector(changeScene(_:))) - + // MARK: Initialization override func viewDidLoad() { @@ -125,16 +125,15 @@ class ARExample: UIViewController { calibrationView?.alpha = 0.0 // Set up the `sceneInfo` array with our scene init functions and labels. - sceneInfo.append(contentsOf: [(sceneFunction: streetsScene, label: "Streets - Full Scale", tableTop: false, useLocationDataSourceOnce: false), - (sceneFunction: imageryScene, label: "Imagery - Full Scale", tableTop: false, useLocationDataSourceOnce: true), - (sceneFunction: pointCloudScene, label: "Point Cloud - Tabletop", tableTop: true, useLocationDataSourceOnce: true), - (sceneFunction: yosemiteScene, label: "Yosemite - Tabletop", tableTop: true, useLocationDataSourceOnce: true), - (sceneFunction: borderScene, label: "US - Mexico Border - Tabletop", tableTop: true, useLocationDataSourceOnce: true), - (sceneFunction: emptyScene, label: "Empty - Full Scale", tableTop: false, useLocationDataSourceOnce: true)]) + sceneInfo.append(contentsOf: [(sceneFunction: streetsScene, label: "Streets - Full Scale", tableTop: false, trackingMode: .continuous), + (sceneFunction: imageryScene, label: "Imagery - Full Scale", tableTop: false, trackingMode: .initial), + (sceneFunction: pointCloudScene, label: "Point Cloud - Tabletop", tableTop: true, trackingMode: .ignore), + (sceneFunction: yosemiteScene, label: "Yosemite - Tabletop", tableTop: true, trackingMode: .ignore), + (sceneFunction: borderScene, label: "US - Mexico Border - Tabletop", tableTop: true, trackingMode: .ignore), + (sceneFunction: emptyScene, label: "Empty - Full Scale", tableTop: false, trackingMode: .initial)]) // Use the first sceneInfo to create and set the scene. - currentSceneInfo = sceneInfo.first - arView.sceneView.scene = currentSceneInfo?.sceneFunction() + selectSceneInfo(sceneInfo.first) // Debug options for showing world origin and point cloud scene analysis points. // arView.arSCNView.debugOptions = [.showWorldOrigin, .showFeaturePoints] @@ -142,7 +141,7 @@ class ARExample: UIViewController { override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) - arView.startTracking(useLocationDataSourceOnce: currentSceneInfo?.useLocationDataSourceOnce ?? false, completion: { [weak self] (error) in + arView.startTracking(currentSceneInfo?.trackingMode ?? .ignore, completion: { [weak self] (error) in self?.statusViewController?.errorMessage = error?.localizedDescription }) } @@ -192,6 +191,39 @@ class ARExample: UIViewController { sceneItem.isEnabled = !startCalibrating } + /// Sets up the required functionality in order to display the scene and AR experience represented by `sceneInfo`. + /// + /// - Parameter sceneInfo: The sceneInfo used to set up the scene and AR experience. + fileprivate func selectSceneInfo(_ sceneInfo: SceneInfoType?) { + guard let info = sceneInfo else { return } + + // Set currentSceneInfo to the selected scene info. + self.currentSceneInfo = info + + // Stop tracking, update the scene with the selected Scene and reset tracking. + self.arView.stopTracking() + self.arView.sceneView.scene = info.sceneFunction() + if info.tableTop { + // Dim the SceneView until the user taps on a surface. + self.arView.sceneView.alpha = 0.5 + } + + // Reset AR tracking and then start tracking. + self.arView.resetTracking() + self.arView.startTracking(info.trackingMode, completion: { [weak self] (error) in + self?.statusViewController?.errorMessage = error?.localizedDescription + }) + + // Disable elevation control if we're using continuous GPS. + self.calibrationView?.elevationControlVisibility = (info.trackingMode != .continuous) + + // Disable calibration if we're in table top + self.calibrationItem.isEnabled = !info.tableTop + + // Reset didPlaceScene variable + self.didPlaceScene = false + } + /// Allow users to change the current scene. /// /// - Parameter sender: The bar button item tapped on. @@ -202,32 +234,10 @@ class ARExample: UIViewController { // Loop through all sceneInfos and add `UIAlertActions` for each. sceneInfo.forEach { info in - let action = UIAlertAction(title: info.label, style: .default, handler: { (action) in - // Set currentSceneInfo to the selected scene. - self.currentSceneInfo = info - - // Stop tracking, update the scene with the selected Scene and reset tracking. - self.arView.stopTracking() - self.arView.sceneView.scene = info.sceneFunction() - if info.tableTop { - // Dim the SceneView until the user taps on a surface. - self.arView.sceneView.alpha = 0.5 - } - // Reset AR tracking and then start tracking. - self.arView.resetTracking() - self.arView.startTracking(useLocationDataSourceOnce: info.useLocationDataSourceOnce, completion: { [weak self] (error) in - self?.statusViewController?.errorMessage = error?.localizedDescription - }) - - // Disable elevation control if we're using continuous GPS. - self.calibrationView?.elevationControlVisibility = info.useLocationDataSourceOnce - - // Disable calibration if we're in table top - self.calibrationItem.isEnabled = !info.tableTop - - // Reset didPlaceScene variable - self.didPlaceScene = false + let action = UIAlertAction(title: info.label, style: .default, handler: { [weak self] (action) in + self?.selectSceneInfo(info) }) + // Display current scene as disabled. action.isEnabled = (info.label != currentSceneInfo?.label) alertController.addAction(action) @@ -338,7 +348,7 @@ extension ARExample: ARSCNViewDelegate { // Present an alert describing the error. let alertController = UIAlertController(title: "Could not start tracking.", message: errorMessage, preferredStyle: .alert) let restartAction = UIAlertAction(title: "Restart Tracking", style: .default) { _ in - self?.arView.startTracking(useLocationDataSourceOnce: self?.currentSceneInfo?.useLocationDataSourceOnce ?? false, completion: { (error) in + self?.arView.startTracking(self?.currentSceneInfo?.trackingMode ?? .ignore, completion: { (error) in self?.statusViewController?.errorMessage = error?.localizedDescription }) } @@ -445,8 +455,13 @@ extension ARExample { case .excessiveMotion: message = "Try moving your device more slowly." case .initializing: - // Because ARKit gets reset often when using continuous GPS, only dipslay initializing message if we're using the initial GPS. - message = (currentSceneInfo?.useLocationDataSourceOnce ?? false) ? "Keep moving your device." : "" + // Because ARKit gets reset often when using continuous GPS, only dipslay initializing message if we're not in continuous tracking mode. + if let sceneInfo = currentSceneInfo, sceneInfo.trackingMode != .continuous { + message = "Keep moving your device." + } + else { + message = "" + } case .insufficientFeatures: message = "Try turning on more lights and moving around." default: @@ -499,7 +514,6 @@ extension ARExample { // Set the location data source so we use our GPS location as the originCamera. arView.locationDataSource = AGSCLLocationDataSource() - arView.originCamera = nil arView.translationFactor = 1 return scene } @@ -516,7 +530,6 @@ extension ARExample { // Set the location data source so we use our GPS location as the originCamera. arView.locationDataSource = AGSCLLocationDataSource() - arView.originCamera = nil arView.translationFactor = 1 return scene } @@ -657,7 +670,6 @@ extension ARExample { // Set the location data source so we use our GPS location as the originCamera. arView.locationDataSource = AGSCLLocationDataSource() - arView.originCamera = nil arView.translationFactor = 1 return scene } diff --git a/Examples/ArcGISToolkitExamples/Misc/CalibrationView.swift b/Examples/ArcGISToolkitExamples/Misc/CalibrationView.swift index 7486bac7..1490d6f0 100644 --- a/Examples/ArcGISToolkitExamples/Misc/CalibrationView.swift +++ b/Examples/ArcGISToolkitExamples/Misc/CalibrationView.swift @@ -227,7 +227,7 @@ class CalibrationView: UIView { /// /// - Parameter deltaHeading: The amount to rotate the camera. private func rotate(_ deltaHeading: Double) { - guard let camera = arcgisARView.originCamera else { return } + let camera = arcgisARView.originCamera let newHeading = camera.heading + deltaHeading arcgisARView.originCamera = camera.rotate(toHeading: newHeading, pitch: camera.pitch, roll: camera.roll) } @@ -236,8 +236,7 @@ class CalibrationView: UIView { /// /// - Parameter deltaAltitude: The amount to elevate the camera. private func elevate(_ deltaAltitude: Double) { - guard let camera = arcgisARView.originCamera else { return } - arcgisARView.originCamera = camera.elevate(withDeltaAltitude: deltaAltitude) + arcgisARView.originCamera = arcgisARView.originCamera.elevate(withDeltaAltitude: deltaAltitude) } /// Calculates the elevation delta amount based on the elevation slider value. diff --git a/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift b/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift index ac0a0947..3dadf4aa 100644 --- a/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift +++ b/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift @@ -16,6 +16,17 @@ import UIKit import ARKit import ArcGIS +/// Controls how the locations generated from the location data source are used during AR tracking. +/// +/// - ignore: Ignore all location data source locations. +/// - initial: Use only the initial location from the location data source and ignore all subsequent locations. +/// - continuous: Use all locations from the location data source. +public enum ARLocationTrackingMode { + case ignore + case initial + case continuous +} + public class ArcGISARView: UIView { // MARK: public properties @@ -46,11 +57,12 @@ public class ArcGISARView: UIView { /// The viewpoint camera used to set the initial view of the sceneView instead of the device's GPS location via the location data source. You can use Key-Value Observing to track changes to the origin camera. /// - Since: 100.6.0 - @objc dynamic public var originCamera: AGSCamera? { - didSet { - guard let newCamera = originCamera else { return } - // Set the camera as the originCamera on the cameraController and reset tracking. - cameraController.originCamera = newCamera + @objc dynamic public var originCamera: AGSCamera { + get { + return cameraController.originCamera + } + set { + cameraController.originCamera = newValue } } @@ -103,12 +115,15 @@ public class ArcGISARView: UIView { return ARWorldTrackingConfiguration.isSupported }() + /// Denotes whether we've received our initial location from the data source. + private var didSetInitialLocation: Bool = false + /// The last portrait or landscape orientation value. private var lastGoodDeviceOrientation = UIDeviceOrientation.portrait - /// Are we using only the first location provided by the LocationDataSource? - private var useLocationDataSourceOnce = true - + /// The tracking mode controlling how the locations generated from the location data source are used during AR tracking. + private var locationTrackingMode: ARLocationTrackingMode = .ignore + // MARK: Initializers public override init(frame: CGRect) { @@ -125,14 +140,10 @@ public class ArcGISARView: UIView { /// /// - Parameters: /// - renderVideoFeed: Whether to display the live camera image. - /// - tryUsingARKit: Whether or not to use ARKit, regardless if it's available. /// - Since: 100.6.0 - public convenience init(renderVideoFeed: Bool, tryUsingARKit: Bool){ + public convenience init(renderVideoFeed: Bool){ self.init(frame: .zero) - // This overrides the `sharedInitialization()` isUsingARKit code - isUsingARKit = tryUsingARKit && deviceSupportsARKit - if !isUsingARKit || !renderVideoFeed { // User is not using ARKit, or they don't want to see video, so remove the arSCNView from the superView (it was added in sharedInitialization()). // This overrides the `sharedInitialization()` arSCNView code @@ -201,7 +212,7 @@ public class ArcGISARView: UIView { // Use the `internalHitTest` method to get the matrix of `screenPoint`. guard let localOffsetMatrix = internalHitTest(screenPoint: screenPoint) else { return nil } - let currOriginMatrix = cameraController.originCamera.transformationMatrix + let currOriginMatrix = originCamera.transformationMatrix // Scale translation by translationFactor. let translatedMatrix = AGSTransformationMatrix(quaternionX: localOffsetMatrix.quaternionX, @@ -220,7 +231,7 @@ public class ArcGISARView: UIView { /// Resets the device tracking and related properties. /// - Since: 100.6.0 public func resetTracking() { - originCamera = nil + didSetInitialLocation = false initialTransformation = .identity if isUsingARKit { arSCNView.session.run(arConfiguration, options: [.resetTracking, .removeExistingAnchors]) @@ -248,10 +259,11 @@ public class ArcGISARView: UIView { /// /// - Parameter completion: The completion handler called when start tracking completes. If tracking starts successfully, the `error` property will be nil; if tracking fails to start, the error will be non-nil and contain the reason for failure. /// - Since: 100.6.0 - public func startTracking(useLocationDataSourceOnce: Bool = true, completion: ((_ error: Error?) -> Void)? = nil) { + public func startTracking(_ locationTrackingMode: ARLocationTrackingMode, completion: ((_ error: Error?) -> Void)? = nil) { // We have a location data source that needs to be started. - self.useLocationDataSourceOnce = useLocationDataSourceOnce - if let locationDataSource = self.locationDataSource { + self.locationTrackingMode = locationTrackingMode + if locationTrackingMode != .ignore, + let locationDataSource = self.locationDataSource { locationDataSource.start { [weak self] (error) in if error == nil { self?.finalizeStart() @@ -260,7 +272,7 @@ public class ArcGISARView: UIView { } } else { - // No data source, continue with defaults. + // We're either ignoring the data source or there is no data source so continue with defaults. finalizeStart() completion?(nil) } @@ -473,7 +485,7 @@ extension ArcGISARView: AGSLocationChangeHandlerDelegate { public func locationDataSource(_ locationDataSource: AGSLocationDataSource, locationDidChange location: AGSLocation) { // Location changed. - guard var locationPoint = location.position else { return } + guard locationTrackingMode != .ignore, var locationPoint = location.position else { return } // The AGSCLLocationDataSource does not include altitude information from the CLLocation when // creating the `AGSLocation` geometry, so grab the altitude directly from the CLLocationManager. @@ -485,21 +497,23 @@ extension ArcGISARView: AGSLocationChangeHandlerDelegate { } else { // We don't have a valid altitude, so use the old altitude. - let oldLocationPoint = cameraController.originCamera.location + let oldLocationPoint = originCamera.location locationPoint = AGSPoint(x: locationPoint.x, y: locationPoint.y, z: oldLocationPoint.z, spatialReference: locationPoint.spatialReference) } } - // Always set originCamera; then reset ARKit - let oldCamera = cameraController.originCamera - + // Always set originCamera; then reset ARKit // Create a new camera based on our location and set it on the cameraController. - if originCamera == nil { + // Note for the .initial tracking mode (or if we've yet to set an initial locatin), + // we create a new camera with the location and defaults for heading, pitch, roll. + // For .continuous mode, we use the location and the old camera's heading, pitch, roll. + if locationTrackingMode == .initial || !didSetInitialLocation { let newCamera = AGSCamera(location: locationPoint, heading: 0.0, pitch: 90.0, roll: 0.0) originCamera = newCamera + didSetInitialLocation = true } - else { - cameraController.originCamera = AGSCamera(location: locationPoint, heading: oldCamera.heading, pitch: oldCamera.pitch, roll: oldCamera.roll) + else if locationTrackingMode == .continuous { + originCamera = AGSCamera(location: locationPoint, heading: originCamera.heading, pitch: originCamera.pitch, roll: originCamera.roll) } // If we're using ARKit, reset its tracking. @@ -510,13 +524,12 @@ extension ArcGISARView: AGSLocationChangeHandlerDelegate { // Reset the camera controller's transformationMatrix to its initial state, the Idenity matrix. cameraController.transformationMatrix = .identity - if (useLocationDataSourceOnce) { - // If we are only using the intitial data source location, stop the data source. + if (locationTrackingMode != .continuous) { + // Stop the data source if the tracking mode is not continuous. locationDataSource.stop() } locationChangeHandlerDelegate?.locationDataSource?(locationDataSource, locationDidChange: location) - } public func locationDataSource(_ locationDataSource: AGSLocationDataSource, statusDidChange status: AGSLocationDataSourceStatus) { From 8cc485596a6729d398780dee86e5a37ab4e5b6a8 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Thu, 5 Sep 2019 14:07:33 -0500 Subject: [PATCH 120/147] PR review changes; update comment and remove obsolete properties. --- Examples/ArcGISToolkitExamples/ARExample.swift | 2 +- Examples/ArcGISToolkitExamples/Misc/CalibrationView.swift | 6 ------ 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/Examples/ArcGISToolkitExamples/ARExample.swift b/Examples/ArcGISToolkitExamples/ARExample.swift index bf864a2b..390e5f83 100644 --- a/Examples/ArcGISToolkitExamples/ARExample.swift +++ b/Examples/ArcGISToolkitExamples/ARExample.swift @@ -84,7 +84,7 @@ class ARExample: UIViewController { // Set ourself as touch delegate so we can get touch events. arView.sceneView.touchDelegate = self - // Set ourself as touch delegate so we can get touch events. + // Set ourself as location change delegate so we can get location data source events. arView.locationChangeHandlerDelegate = self // Disble user interactions on the sceneView. diff --git a/Examples/ArcGISToolkitExamples/Misc/CalibrationView.swift b/Examples/ArcGISToolkitExamples/Misc/CalibrationView.swift index 1490d6f0..46f05a34 100644 --- a/Examples/ArcGISToolkitExamples/Misc/CalibrationView.swift +++ b/Examples/ArcGISToolkitExamples/Misc/CalibrationView.swift @@ -57,12 +57,6 @@ class CalibrationView: UIView { return slider }() - /// The last elevation slider value. - private var lastElevationValue: Float = 0 - - // The last heading slider value. - private var lastHeadingValue: Float = 0 - /// The elevation label.. private let elevationLabel = UILabel(frame: .zero) From f15b68a7cc2d366bc4f78a193fce1fb5b739258f Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Fri, 6 Sep 2019 08:44:40 -0500 Subject: [PATCH 121/147] PR review changes; don't need self now that code is not in a block; use measurement.value to set a measurement; make sceneInfo argument non-optional. --- .../ArcGISToolkitExamples/ARExample.swift | 31 ++++++++++--------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/Examples/ArcGISToolkitExamples/ARExample.swift b/Examples/ArcGISToolkitExamples/ARExample.swift index 390e5f83..bcca4ff9 100644 --- a/Examples/ArcGISToolkitExamples/ARExample.swift +++ b/Examples/ArcGISToolkitExamples/ARExample.swift @@ -133,7 +133,9 @@ class ARExample: UIViewController { (sceneFunction: emptyScene, label: "Empty - Full Scale", tableTop: false, trackingMode: .initial)]) // Use the first sceneInfo to create and set the scene. - selectSceneInfo(sceneInfo.first) + if let info = sceneInfo.first { + selectSceneInfo(info) + } // Debug options for showing world origin and point cloud scene analysis points. // arView.arSCNView.debugOptions = [.showWorldOrigin, .showFeaturePoints] @@ -194,34 +196,33 @@ class ARExample: UIViewController { /// Sets up the required functionality in order to display the scene and AR experience represented by `sceneInfo`. /// /// - Parameter sceneInfo: The sceneInfo used to set up the scene and AR experience. - fileprivate func selectSceneInfo(_ sceneInfo: SceneInfoType?) { - guard let info = sceneInfo else { return } + fileprivate func selectSceneInfo(_ sceneInfo: SceneInfoType) { // Set currentSceneInfo to the selected scene info. - self.currentSceneInfo = info + currentSceneInfo = sceneInfo // Stop tracking, update the scene with the selected Scene and reset tracking. - self.arView.stopTracking() - self.arView.sceneView.scene = info.sceneFunction() - if info.tableTop { + arView.stopTracking() + arView.sceneView.scene = sceneInfo.sceneFunction() + if sceneInfo.tableTop { // Dim the SceneView until the user taps on a surface. - self.arView.sceneView.alpha = 0.5 + arView.sceneView.alpha = 0.5 } // Reset AR tracking and then start tracking. - self.arView.resetTracking() - self.arView.startTracking(info.trackingMode, completion: { [weak self] (error) in + arView.resetTracking() + arView.startTracking(sceneInfo.trackingMode, completion: { [weak self] (error) in self?.statusViewController?.errorMessage = error?.localizedDescription }) // Disable elevation control if we're using continuous GPS. - self.calibrationView?.elevationControlVisibility = (info.trackingMode != .continuous) + calibrationView?.elevationControlVisibility = (sceneInfo.trackingMode != .continuous) // Disable calibration if we're in table top - self.calibrationItem.isEnabled = !info.tableTop + calibrationItem.isEnabled = !sceneInfo.tableTop // Reset didPlaceScene variable - self.didPlaceScene = false + didPlaceScene = false } /// Allow users to change the current scene. @@ -696,8 +697,8 @@ extension AGSScene { extension ARExample: AGSLocationChangeHandlerDelegate { public func locationDataSource(_ locationDataSource: AGSLocationDataSource, locationDidChange location: AGSLocation) { // When we get a new location, update the status view controller with the new horizontal and vertical accuracy. - statusViewController?.horizontalAccuracyMeasurement = Measurement(value: location.horizontalAccuracy, unit: UnitLength.meters) - statusViewController?.verticalAccuracyMeasurement = Measurement(value: location.verticalAccuracy, unit: UnitLength.meters) + statusViewController?.horizontalAccuracyMeasurement.value = location.horizontalAccuracy + statusViewController?.verticalAccuracyMeasurement.value = location.verticalAccuracy } func locationDataSource(_ locationDataSource: AGSLocationDataSource, statusDidChange status: AGSLocationDataSourceStatus) { From f64a1cb2b9eda057e6830a29050a04a1ada60daa Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Thu, 12 Sep 2019 15:52:06 -0500 Subject: [PATCH 122/147] Readme updates --- Documentation/AR/README.md | 50 ++++++++++++++++++++++++++++++++++++++ Documentation/README.md | 1 + README.md | 3 ++- 3 files changed, 53 insertions(+), 1 deletion(-) create mode 100644 Documentation/AR/README.md diff --git a/Documentation/AR/README.md b/Documentation/AR/README.md new file mode 100644 index 00000000..cf37d9d0 --- /dev/null +++ b/Documentation/AR/README.md @@ -0,0 +1,50 @@ +# AR + +Augmented reality experiences are designed to "augment" the physical world with virtual content that respects real world scale, position, and orientation of a device. In the case of Runtime, a SceneView contains 3D geographic data as virtual content on top of a camera feed which represents the real, physical world. + +The Augmented Reality (alias AR) toolkit component allows quick and easy integration of AR into your application for a wide variety of scenarios. The toolkit recognizes the following common patterns for AR:  +• Flyover – Flyover AR is a kind of AR scenario that allows you to explore a scene using your device as a window into the virtual world. A typical flyover AR scenario will start with the scene’s virtual camera positioned over an area of interest. You can walk around and reorient the device to focus on specific content in the scene.  +• Tabletop – A kind of AR scenario where scene content is anchored to a physical surface, as if it were a 3D-printed model.  +• Real-scale – A kind of AR scenario where scene content is rendered exactly where it would be in the physical world. A camera feed is shown and GIS content is rendered on top of that feed. This is used in scenarios ranging from viewing hidden infrastructure to displaying waypoints for navigation.  + +The AR toolkit component is comprised of one class: `ArcGISARView`. This is a subclass of `UIView` that contains the functionality needed to display an AR experience in your application. It uses `ARKit`, Apple's augmented reality framework to display the live camera feed and handle real world tracking and synchronization with the Runtime SDK's `AGSSceneView`. The `ArcGISARView` is responsible for starting and managing an `ARKit` session. It uses a user-provided `AGSLocationDataSource` for getting an initial GPS location and when continuous GPS tracking is required. + +### Features of the AR component + +- Allows display of live camera feed +- Manages `ARKit` `ARSession` lifecycle +- Ability to track users location and device orientation through a combination of `ARKit` and the device GPS +- Provides access to an `AGSSceneView` to display your GIS 3D data over the live camera feed +- `ARScreenToLocation` method to convert a screen point to a real-world coordinate +- Easy access to all `ARKit` and `AGSLocationDataSource` delegate methods + +### Usage + +```swift +let arView = ArcGISARView(renderVideoFeed: true) +view.addSubview(arView) +arView.translatesAutoresizingMaskIntoConstraints = false +NSLayoutConstraint.activate([ + arView.leadingAnchor.constraint(equalTo: view.leadingAnchor), + arView.trailingAnchor.constraint(equalTo: view.trailingAnchor), + arView.topAnchor.constraint(equalTo: view.topAnchor), + arView.bottomAnchor.constraint(equalTo: view.bottomAnchor) + ]) + + +// Create a simple scene. +arView.sceneView.scene = AGSScene(basemapType: .imagery) + +// Set a AGSCLLocationDataSource, used to get our initial real-world location. +arView.locationDataSource = AGSCLLocationDataSource() + +// Start tracking our location and device orientation +arView.startTracking(.initial) + +``` + +To see it in action, try out the [Examples](../../Examples) and refer to [ARExample.swift](../../Examples/ArcGISToolkitExamples/AR/ArExample.swift) in the project. + + + + diff --git a/Documentation/README.md b/Documentation/README.md index 077d7263..bbd6a561 100644 --- a/Documentation/README.md +++ b/Documentation/README.md @@ -6,3 +6,4 @@ * [Measure Toolbar](MeasureToolbar) * [Scalebar](Scalebar) * [TimeSlider](TimeSlider) +* [AR](AR) diff --git a/README.md b/README.md index bc4c5482..8ad5cad1 100644 --- a/README.md +++ b/README.md @@ -12,9 +12,10 @@ Toolkit components that will simplify your iOS app development with ArcGIS Runti * [TimeSlider](Documentation/TimeSlider) * [PopupController](Documentation/PopupController) * [TemplatePickerViewController](Documentation/TemplatePicker) +* [AR](Documentation/AR) ## Requirements -* [ArcGIS Runtime SDK for iOS](https://developers.arcgis.com/en/ios/) 100.5.0 (or higher) +* [ArcGIS Runtime SDK for iOS](https://developers.arcgis.com/en/ios/) 100.6.0 (or higher) * Xcode 10.1 (or higher) The *ArcGIS Runtime Toolkit for iOS* has a *Target SDK* version of *11.0*, meaning that it can run on devices with *iOS 11.0* or newer. From 95c066253e710745703138d6017304c65e1c0b6e Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Thu, 12 Sep 2019 16:28:26 -0500 Subject: [PATCH 123/147] Update README.md Formatting changes --- Documentation/AR/README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Documentation/AR/README.md b/Documentation/AR/README.md index cf37d9d0..d15179fe 100644 --- a/Documentation/AR/README.md +++ b/Documentation/AR/README.md @@ -2,10 +2,10 @@ Augmented reality experiences are designed to "augment" the physical world with virtual content that respects real world scale, position, and orientation of a device. In the case of Runtime, a SceneView contains 3D geographic data as virtual content on top of a camera feed which represents the real, physical world. -The Augmented Reality (alias AR) toolkit component allows quick and easy integration of AR into your application for a wide variety of scenarios. The toolkit recognizes the following common patterns for AR:  -• Flyover – Flyover AR is a kind of AR scenario that allows you to explore a scene using your device as a window into the virtual world. A typical flyover AR scenario will start with the scene’s virtual camera positioned over an area of interest. You can walk around and reorient the device to focus on specific content in the scene.  -• Tabletop – A kind of AR scenario where scene content is anchored to a physical surface, as if it were a 3D-printed model.  -• Real-scale – A kind of AR scenario where scene content is rendered exactly where it would be in the physical world. A camera feed is shown and GIS content is rendered on top of that feed. This is used in scenarios ranging from viewing hidden infrastructure to displaying waypoints for navigation.  +The Augmented Reality (AR) toolkit component allows quick and easy integration of AR into your application for a wide variety of scenarios. The toolkit recognizes the following common patterns for AR:  +* Flyover – Flyover AR is a kind of AR scenario that allows you to explore a scene using your device as a window into the virtual world. A typical flyover AR scenario will start with the scene’s virtual camera positioned over an area of interest. You can walk around and reorient the device to focus on specific content in the scene.  +* Tabletop – A kind of AR scenario where scene content is anchored to a physical surface, as if it were a 3D-printed model.  +* Real-scale – A kind of AR scenario where scene content is rendered exactly where it would be in the physical world. A camera feed is shown and GIS content is rendered on top of that feed. This is used in scenarios ranging from viewing hidden infrastructure to displaying waypoints for navigation.  The AR toolkit component is comprised of one class: `ArcGISARView`. This is a subclass of `UIView` that contains the functionality needed to display an AR experience in your application. It uses `ARKit`, Apple's augmented reality framework to display the live camera feed and handle real world tracking and synchronization with the Runtime SDK's `AGSSceneView`. The `ArcGISARView` is responsible for starting and managing an `ARKit` session. It uses a user-provided `AGSLocationDataSource` for getting an initial GPS location and when continuous GPS tracking is required. From aaeb334348222aafddd19b757a34fbf0f0a2de87 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Thu, 12 Sep 2019 16:29:35 -0500 Subject: [PATCH 124/147] Update README.md Fix ARExample.swift link. --- Documentation/AR/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Documentation/AR/README.md b/Documentation/AR/README.md index d15179fe..054f65de 100644 --- a/Documentation/AR/README.md +++ b/Documentation/AR/README.md @@ -43,7 +43,7 @@ arView.startTracking(.initial) ``` -To see it in action, try out the [Examples](../../Examples) and refer to [ARExample.swift](../../Examples/ArcGISToolkitExamples/AR/ArExample.swift) in the project. +To see it in action, try out the [Examples](../../Examples) and refer to [ARExample.swift](../../Examples/ArcGISToolkitExamples/ArExample.swift) in the project. From 0fc1d72b7b2c024a1d437679c37f652d371682ce Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Thu, 12 Sep 2019 16:30:36 -0500 Subject: [PATCH 125/147] Update README.md Fix link, again... --- Documentation/AR/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Documentation/AR/README.md b/Documentation/AR/README.md index 054f65de..589d4118 100644 --- a/Documentation/AR/README.md +++ b/Documentation/AR/README.md @@ -43,7 +43,7 @@ arView.startTracking(.initial) ``` -To see it in action, try out the [Examples](../../Examples) and refer to [ARExample.swift](../../Examples/ArcGISToolkitExamples/ArExample.swift) in the project. +To see it in action, try out the [Examples](../../Examples) and refer to [ARExample.swift](../../Examples/ArcGISToolkitExamples/ARExample.swift) in the project. From d79e4fe586c1edc69ae94e1e62bf9196ff9d6f8f Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Fri, 13 Sep 2019 11:37:06 -0500 Subject: [PATCH 126/147] Update code and add info about plist entries. --- Documentation/AR/README.md | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/Documentation/AR/README.md b/Documentation/AR/README.md index 589d4118..b431f79f 100644 --- a/Documentation/AR/README.md +++ b/Documentation/AR/README.md @@ -39,10 +39,17 @@ arView.sceneView.scene = AGSScene(basemapType: .imagery) arView.locationDataSource = AGSCLLocationDataSource() // Start tracking our location and device orientation -arView.startTracking(.initial) +arView.startTracking(.initial) { (error) in + print("Start tracking error: \(String(describing: error))") +} ``` +You must also add the following entries to your application's Info.plist file to support use of the camera (for the live video feed) and, when using the `AGSCLLocationDataSource`, the GPS (for determining your device's location): + +Privacy – Camera Usage Description +Privacy – Location When In Use Usage Description + To see it in action, try out the [Examples](../../Examples) and refer to [ARExample.swift](../../Examples/ArcGISToolkitExamples/ARExample.swift) in the project. From 0fe5123d4ebf641777c873e1f21594fe010b5047 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Fri, 13 Sep 2019 11:37:52 -0500 Subject: [PATCH 127/147] Update README.md Update formatting --- Documentation/AR/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Documentation/AR/README.md b/Documentation/AR/README.md index b431f79f..92802f69 100644 --- a/Documentation/AR/README.md +++ b/Documentation/AR/README.md @@ -47,8 +47,8 @@ arView.startTracking(.initial) { (error) in You must also add the following entries to your application's Info.plist file to support use of the camera (for the live video feed) and, when using the `AGSCLLocationDataSource`, the GPS (for determining your device's location): -Privacy – Camera Usage Description -Privacy – Location When In Use Usage Description +* Privacy – Camera Usage Description +* Privacy – Location When In Use Usage Description To see it in action, try out the [Examples](../../Examples) and refer to [ARExample.swift](../../Examples/ArcGISToolkitExamples/ARExample.swift) in the project. From ce176d80e1ad3d2ef1f67244d06e7b80daaff705 Mon Sep 17 00:00:00 2001 From: Nicholas Furness Date: Fri, 13 Sep 2019 13:36:36 -0400 Subject: [PATCH 128/147] Suggested edits A couple of other thoughts: 1. You mention "It uses a user-provided AGSLocationDataSource for getting an initial GPS location and when continuous GPS tracking is required" but should we mention that this is the default? It sounds as if the user will need to create one of these and provide it (I assume that's not usually the case). 2. This seems like it could be restructured to be a little clearer: "You must also add the following entries to your application's Info.plist file to support use of the camera (for the live video feed) and, when using the AGSCLLocationDataSource, the GPS (for determining your device's location)". 3. For the `info.plist` keys, should we also list the actual literals (like `NSLocationWhenInUseUsageDescription`) and link to the [Apple doc](https://developer.apple.com/documentation/bundleresources/information_property_list/nslocationwheninuseusagedescription)? --- Documentation/AR/README.md | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/Documentation/AR/README.md b/Documentation/AR/README.md index 92802f69..c9055695 100644 --- a/Documentation/AR/README.md +++ b/Documentation/AR/README.md @@ -3,17 +3,17 @@ Augmented reality experiences are designed to "augment" the physical world with virtual content that respects real world scale, position, and orientation of a device. In the case of Runtime, a SceneView contains 3D geographic data as virtual content on top of a camera feed which represents the real, physical world. The Augmented Reality (AR) toolkit component allows quick and easy integration of AR into your application for a wide variety of scenarios. The toolkit recognizes the following common patterns for AR:  -* Flyover – Flyover AR is a kind of AR scenario that allows you to explore a scene using your device as a window into the virtual world. A typical flyover AR scenario will start with the scene’s virtual camera positioned over an area of interest. You can walk around and reorient the device to focus on specific content in the scene.  -* Tabletop – A kind of AR scenario where scene content is anchored to a physical surface, as if it were a 3D-printed model.  -* Real-scale – A kind of AR scenario where scene content is rendered exactly where it would be in the physical world. A camera feed is shown and GIS content is rendered on top of that feed. This is used in scenarios ranging from viewing hidden infrastructure to displaying waypoints for navigation.  +* **Flyover**: Flyover AR allows you to explore a scene using your device as a window into the virtual world. A typical flyover AR scenario will start with the scene’s virtual camera positioned over an area of interest. You can walk around and reorient the device to focus on specific content in the scene.  +* **Tabletop**: Scene content is anchored to a physical surface, as if it were a 3D-printed model.  +* **Real-scale**: Scene content is rendered exactly where it would be in the physical world. A camera feed is shown and GIS content is rendered on top of that feed. This is used in scenarios ranging from viewing hidden infrastructure to displaying waypoints for navigation. The AR toolkit component is comprised of one class: `ArcGISARView`. This is a subclass of `UIView` that contains the functionality needed to display an AR experience in your application. It uses `ARKit`, Apple's augmented reality framework to display the live camera feed and handle real world tracking and synchronization with the Runtime SDK's `AGSSceneView`. The `ArcGISARView` is responsible for starting and managing an `ARKit` session. It uses a user-provided `AGSLocationDataSource` for getting an initial GPS location and when continuous GPS tracking is required. ### Features of the AR component -- Allows display of live camera feed +- Allows display of the live camera feed - Manages `ARKit` `ARSession` lifecycle -- Ability to track users location and device orientation through a combination of `ARKit` and the device GPS +- Tracks user location and device orientation through a combination of `ARKit` and the device GPS - Provides access to an `AGSSceneView` to display your GIS 3D data over the live camera feed - `ARScreenToLocation` method to convert a screen point to a real-world coordinate - Easy access to all `ARKit` and `AGSLocationDataSource` delegate methods @@ -51,7 +51,3 @@ You must also add the following entries to your application's Info.plist file to * Privacy – Location When In Use Usage Description To see it in action, try out the [Examples](../../Examples) and refer to [ARExample.swift](../../Examples/ArcGISToolkitExamples/ARExample.swift) in the project. - - - - From 443f51488e2260b6fff4e2f92e65bc49a34b3036 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Fri, 13 Sep 2019 15:23:22 -0500 Subject: [PATCH 129/147] Add scene info label as title of VC. --- Examples/ArcGISToolkitExamples/ARExample.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Examples/ArcGISToolkitExamples/ARExample.swift b/Examples/ArcGISToolkitExamples/ARExample.swift index bcca4ff9..2d73b1e1 100644 --- a/Examples/ArcGISToolkitExamples/ARExample.swift +++ b/Examples/ArcGISToolkitExamples/ARExample.swift @@ -198,6 +198,8 @@ class ARExample: UIViewController { /// - Parameter sceneInfo: The sceneInfo used to set up the scene and AR experience. fileprivate func selectSceneInfo(_ sceneInfo: SceneInfoType) { + title = sceneInfo.label + // Set currentSceneInfo to the selected scene info. currentSceneInfo = sceneInfo From e49a9b9c7f52f515b60e3ac0dc3d1356ffd54b22 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Fri, 13 Sep 2019 15:33:28 -0500 Subject: [PATCH 130/147] Doc updates for Info.plist entries. --- Documentation/AR/README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Documentation/AR/README.md b/Documentation/AR/README.md index c9055695..73239f7f 100644 --- a/Documentation/AR/README.md +++ b/Documentation/AR/README.md @@ -45,9 +45,9 @@ arView.startTracking(.initial) { (error) in ``` -You must also add the following entries to your application's Info.plist file to support use of the camera (for the live video feed) and, when using the `AGSCLLocationDataSource`, the GPS (for determining your device's location): +You must also add the following entries to your application's `Info.plist` file. These are required to allow access to the camera (for the live video feed) and to allow access to location services (when using the `AGSCLLocationDataSource`): -* Privacy – Camera Usage Description -* Privacy – Location When In Use Usage Description +* Privacy – Camera Usage Description ([NSCameraUsageDescription](https://developer.apple.com/documentation/bundleresources/information_property_list/nscamerausagedescription)) +* Privacy – Location When In Use Usage Description ([NSLocationWhenInUseUsageDescription](https://developer.apple.com/documentation/bundleresources/information_property_list/nslocationwheninuseusagedescription)) To see it in action, try out the [Examples](../../Examples) and refer to [ARExample.swift](../../Examples/ArcGISToolkitExamples/ARExample.swift) in the project. From dd68c9123ae0939e6b2765878dc05b758991f90b Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Fri, 13 Sep 2019 16:13:17 -0500 Subject: [PATCH 131/147] Don't remove arSCNView from superview if not rendering the video feed; instead, set it's alpha to 0.0. --- Toolkit/ArcGISToolkit/AR/ArcGISARView.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift b/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift index 3dadf4aa..8daee738 100644 --- a/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift +++ b/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift @@ -145,9 +145,9 @@ public class ArcGISARView: UIView { self.init(frame: .zero) if !isUsingARKit || !renderVideoFeed { - // User is not using ARKit, or they don't want to see video, so remove the arSCNView from the superView (it was added in sharedInitialization()). - // This overrides the `sharedInitialization()` arSCNView code - arSCNView.removeFromSuperview() + // User is not using ARKit, or they don't want to see video, + // set the arSCNView.alpha to 0.0 so it doesn't display. + arSCNView.alpha = 0.0 } // Tell the sceneView we will be calling `renderFrame()` manually if we're using ARKit. From c3390e2054900971a4204b3018fde52846a6caec Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Fri, 13 Sep 2019 16:14:47 -0500 Subject: [PATCH 132/147] Fix wording of 3D data display. Co-Authored-By: Philip Ridgeway --- Documentation/AR/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Documentation/AR/README.md b/Documentation/AR/README.md index 73239f7f..5841ab45 100644 --- a/Documentation/AR/README.md +++ b/Documentation/AR/README.md @@ -1,6 +1,6 @@ # AR -Augmented reality experiences are designed to "augment" the physical world with virtual content that respects real world scale, position, and orientation of a device. In the case of Runtime, a SceneView contains 3D geographic data as virtual content on top of a camera feed which represents the real, physical world. +Augmented reality experiences are designed to "augment" the physical world with virtual content that respects real world scale, position, and orientation of a device. In the case of Runtime, a SceneView displays 3D geographic data as virtual content on top of a camera feed which represents the real, physical world. The Augmented Reality (AR) toolkit component allows quick and easy integration of AR into your application for a wide variety of scenarios. The toolkit recognizes the following common patterns for AR:  * **Flyover**: Flyover AR allows you to explore a scene using your device as a window into the virtual world. A typical flyover AR scenario will start with the scene’s virtual camera positioned over an area of interest. You can walk around and reorient the device to focus on specific content in the scene.  From 4cfbc7df50c4e7325fc03bc4bc644c2c9b7a78a5 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Mon, 16 Sep 2019 14:28:06 -0500 Subject: [PATCH 133/147] Update Carthage doc, version and copyright info. --- README.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 0fefe335..ea0f9066 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ Toolkit components that will simplify your iOS app development with ArcGIS Runti * [TemplatePickerViewController](Documentation/TemplatePicker) ## Requirements -* [ArcGIS Runtime SDK for iOS](https://developers.arcgis.com/en/ios/) 100.5.0 (or higher) +* [ArcGIS Runtime SDK for iOS](https://developers.arcgis.com/en/ios/) 100.6.0 (or higher) * Xcode 10.1 (or higher) The *ArcGIS Runtime Toolkit for iOS* has a *Target SDK* version of *11.0*, meaning that it can run on devices with *iOS 11.0* or newer. @@ -32,12 +32,14 @@ The *ArcGIS Runtime Toolkit for iOS* has a *Target SDK* version of *11.0*, meani [![Carthage compatible](https://img.shields.io/badge/Carthage-compatible-4BC51D.svg?style=flat)](https://github.com/Carthage/Carthage) -Carthage is a decentralized dependency manager that builds your dependencies and provides you with binary frameworks. To integrate ArcGIS Runtime Toolkit for iOS into your Xcode project using Carthage, Add into your `Cartfile` or create a `Cartfile`, specify it in your Cartfile: +Carthage is a decentralized dependency manager that builds your dependencies and provides you with binary frameworks. To integrate ArcGIS Runtime Toolkit for iOS into your Xcode project using Carthage, add into your `Cartfile` or create a new `Cartfile` with the following: `github "esri/ArcGISToolkit"` Run `carthage update` +Finally, drag the `ArcGISToolkit.framework ` from the `Carthage/Build ` folder to the "TARGETS" settings for your application and drop it in the "Embedded Binaries" section in the "General" tab + ### Manual 1. Ensure you have downloaded and installed __ArcGIS Runtime SDK for iOS__ as described [here](https://developers.arcgis.com/ios/latest/swift/guide/install.htm#ESRI_SECTION1_D57435A2BEBC4D29AFA3A4CAA722506A) 2. Clone or download this repo. @@ -61,7 +63,7 @@ Find a bug or want to request a new feature? Please let us know by submitting a Esri welcomes contributions from anyone and everyone. Please see our [guidelines for contributing](https://github.com/esri/contributing). ## Licensing -Copyright 2017 Esri +Copyright 2017 - 2019 Esri Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. From 2e64b77a6e38c8cfec795ee8e762efca3991af46 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Mon, 16 Sep 2019 14:35:30 -0500 Subject: [PATCH 134/147] Update README.md Add additional info on installing Runtime SDK in Carthage instructions. --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index ea0f9066..db9886c1 100644 --- a/README.md +++ b/README.md @@ -40,6 +40,8 @@ Run `carthage update` Finally, drag the `ArcGISToolkit.framework ` from the `Carthage/Build ` folder to the "TARGETS" settings for your application and drop it in the "Embedded Binaries" section in the "General" tab +Note that you must also have the __ArcGIS Runtime SDK for iOS__ installed and your project set up as per the instructions [here](#manual). + ### Manual 1. Ensure you have downloaded and installed __ArcGIS Runtime SDK for iOS__ as described [here](https://developers.arcgis.com/ios/latest/swift/guide/install.htm#ESRI_SECTION1_D57435A2BEBC4D29AFA3A4CAA722506A) 2. Clone or download this repo. From c33028ccde4a6a2cd2723b5e17ca04d2ed80ce48 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Tue, 17 Sep 2019 08:00:42 -0500 Subject: [PATCH 135/147] Fix toolkit repo name. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index db9886c1..10c9df12 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,7 @@ The *ArcGIS Runtime Toolkit for iOS* has a *Target SDK* version of *11.0*, meani Carthage is a decentralized dependency manager that builds your dependencies and provides you with binary frameworks. To integrate ArcGIS Runtime Toolkit for iOS into your Xcode project using Carthage, add into your `Cartfile` or create a new `Cartfile` with the following: -`github "esri/ArcGISToolkit"` +`github "esri/arcgis-runtime-toolkit-ios"` Run `carthage update` From 103f6c00de628d3f45bb3d32b465762b4f4ceb28 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Tue, 17 Sep 2019 08:12:15 -0500 Subject: [PATCH 136/147] Update README.md Format Carthage section to match other instructions; add link to Carthage GitHub page; update Runtime SDK install link. --- README.md | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 10c9df12..a900d2d8 100644 --- a/README.md +++ b/README.md @@ -32,15 +32,16 @@ The *ArcGIS Runtime Toolkit for iOS* has a *Target SDK* version of *11.0*, meani [![Carthage compatible](https://img.shields.io/badge/Carthage-compatible-4BC51D.svg?style=flat)](https://github.com/Carthage/Carthage) -Carthage is a decentralized dependency manager that builds your dependencies and provides you with binary frameworks. To integrate ArcGIS Runtime Toolkit for iOS into your Xcode project using Carthage, add into your `Cartfile` or create a new `Cartfile` with the following: +Carthage is a decentralized dependency manager that builds your dependencies and provides you with binary frameworks. -`github "esri/arcgis-runtime-toolkit-ios"` + 1. Add `github "esri/arcgis-runtime-toolkit-ios"` to your `Cartfile` + 2. Run `carthage update` + 3. Drag the `ArcGISToolkit.framework ` from the `Carthage/Build ` folder to the "TARGETS" settings for your application and drop it in the "Embedded Binaries" section in the "General" tab + 4. Add `import ArcGISToolkit` in your source code and start using the toolkit components -Run `carthage update` +New to Carthage? Visit the Carthage [GitHub](https://github.com/Carthage/Carthage) page. -Finally, drag the `ArcGISToolkit.framework ` from the `Carthage/Build ` folder to the "TARGETS" settings for your application and drop it in the "Embedded Binaries" section in the "General" tab - -Note that you must also have the __ArcGIS Runtime SDK for iOS__ installed and your project set up as per the instructions [here](#manual). +Note that you must also have the __ArcGIS Runtime SDK for iOS__ installed and your project set up as per the instructions [here](https://developers.arcgis.com/ios/latest/swift/guide/install.htm#ESRI_SECTION1_D57435A2BEBC4D29AFA3A4CAA722506A). ### Manual 1. Ensure you have downloaded and installed __ArcGIS Runtime SDK for iOS__ as described [here](https://developers.arcgis.com/ios/latest/swift/guide/install.htm#ESRI_SECTION1_D57435A2BEBC4D29AFA3A4CAA722506A) From c89db1c6ac25539031b2c67c01f807a72ceb9899 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Tue, 17 Sep 2019 08:13:52 -0500 Subject: [PATCH 137/147] Update README.md Format tweaks. --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index a900d2d8..49533e99 100644 --- a/README.md +++ b/README.md @@ -28,13 +28,13 @@ The *ArcGIS Runtime Toolkit for iOS* has a *Target SDK* version of *11.0*, meani New to cocoapods? Visit [cocoapods.org](https://cocoapods.org/) -### [Carthage](https://github.com/Carthage/Carthage) +### Carthage [![Carthage compatible](https://img.shields.io/badge/Carthage-compatible-4BC51D.svg?style=flat)](https://github.com/Carthage/Carthage) Carthage is a decentralized dependency manager that builds your dependencies and provides you with binary frameworks. - 1. Add `github "esri/arcgis-runtime-toolkit-ios"` to your `Cartfile` + 1. Add `github "esri/arcgis-runtime-toolkit-ios"` to your Cartfile 2. Run `carthage update` 3. Drag the `ArcGISToolkit.framework ` from the `Carthage/Build ` folder to the "TARGETS" settings for your application and drop it in the "Embedded Binaries" section in the "General" tab 4. Add `import ArcGISToolkit` in your source code and start using the toolkit components From c4a4b8c1b162829eef19f34305a36a342a3f4f96 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Tue, 24 Sep 2019 16:27:26 -0500 Subject: [PATCH 138/147] SwiftLint changes for Toolkit project. --- Toolkit/.swiftlint.yml | 69 +++ .../ArcGISToolkit.xcodeproj/project.pbxproj | 22 + Toolkit/ArcGISToolkit/CancelGroup.swift | 10 +- Toolkit/ArcGISToolkit/Coalescer.swift | 28 +- Toolkit/ArcGISToolkit/Compass.swift | 17 +- Toolkit/ArcGISToolkit/Extensions.swift | 2 - Toolkit/ArcGISToolkit/JobManager.swift | 44 +- .../ArcGISToolkit/LegendViewController.swift | 98 ++-- Toolkit/ArcGISToolkit/MapViewController.swift | 3 - Toolkit/ArcGISToolkit/MeasureToolbar.swift | 207 ++++---- Toolkit/ArcGISToolkit/PopupController.swift | 137 ++---- Toolkit/ArcGISToolkit/Scalebar.swift | 464 +++++++----------- .../ArcGISToolkit/TableViewController.swift | 12 +- .../TemplatePickerViewController.swift | 91 ++-- Toolkit/ArcGISToolkit/TimeSlider.swift | 353 ++++++------- .../ArcGISToolkit/UnitsViewController.swift | 13 +- 16 files changed, 718 insertions(+), 852 deletions(-) create mode 100644 Toolkit/.swiftlint.yml diff --git a/Toolkit/.swiftlint.yml b/Toolkit/.swiftlint.yml new file mode 100644 index 00000000..248ca5fd --- /dev/null +++ b/Toolkit/.swiftlint.yml @@ -0,0 +1,69 @@ +included: +opt_in_rules: + - anyobject_protocol + - array_init + - attributes + - block_based_kvo + - closure_end_indentation + - closure_spacing + - collection_alignment + - contains_over_filter_count + - contains_over_filter_is_empty + - contains_over_first_not_nil + - convenience_type + - discouraged_direct_init + - discouraged_optional_boolean + - empty_collection_literal + - empty_count + - empty_string + - empty_xctest_method + - explicit_init + - extension_access_modifier + - fatal_error_message + - first_where + - function_default_parameter_at_end + - identical_operands + - joined_default_parameter + - legacy_random + - let_var_whitespace + - literal_expression_end_indentation + - lower_acl_than_parent + - modifier_order + - multiline_arguments + - multiline_function_chains + - multiline_parameters + - operator_usage_whitespace + - operator_whitespace + - overridden_super_call + - override_in_extension + - prohibited_super_call + - redundant_nil_coalescing + - redundant_type_annotation + - sorted_first_last + - static_operator + - toggle_bool + - trailing_closure + - untyped_error_in_catch + - vertical_parameter_alignment_on_call + - vertical_whitespace_closing_braces + - vertical_whitespace_opening_braces + - xctfail_message + - yoda_condition +disabled_rules: + - file_length + - for_where + - force_cast + - function_body_length + - function_parameter_count + - identifier_name + - large_tuple + - line_length + - nesting + - todo + - trailing_whitespace + - type_body_length +custom_rules: + assert_equal_to_nil: + name: Assert Equal to Nil + regex: XCTAssertEqual\([^\n]+,\s*nil\) + message: Use 'XCTAssertNil' instead. diff --git a/Toolkit/ArcGISToolkit.xcodeproj/project.pbxproj b/Toolkit/ArcGISToolkit.xcodeproj/project.pbxproj index 71b64e03..42750ea8 100644 --- a/Toolkit/ArcGISToolkit.xcodeproj/project.pbxproj +++ b/Toolkit/ArcGISToolkit.xcodeproj/project.pbxproj @@ -150,6 +150,7 @@ 8812336C1DF601A700B2EA8E /* Frameworks */, 8812336D1DF601A700B2EA8E /* Headers */, 8812336E1DF601A700B2EA8E /* Resources */, + E47ED065233AA6110032440E /* Run Linter */, ); buildRules = ( ); @@ -206,6 +207,27 @@ }; /* End PBXResourcesBuildPhase section */ +/* Begin PBXShellScriptBuildPhase section */ + E47ED065233AA6110032440E /* Run Linter */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + name = "Run Linter"; + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "if which swiftlint >/dev/null; then\nswiftlint\nelse\necho \"warning: SwiftLint not installed, download from https://github.com/realm/SwiftLint\"\nfi\n"; + }; +/* End PBXShellScriptBuildPhase section */ + /* Begin PBXSourcesBuildPhase section */ 8812336B1DF601A700B2EA8E /* Sources */ = { isa = PBXSourcesBuildPhase; diff --git a/Toolkit/ArcGISToolkit/CancelGroup.swift b/Toolkit/ArcGISToolkit/CancelGroup.swift index ed5324d4..46f138c7 100644 --- a/Toolkit/ArcGISToolkit/CancelGroup.swift +++ b/Toolkit/ArcGISToolkit/CancelGroup.swift @@ -18,22 +18,20 @@ import Foundation Wraps multiple AGSCancelables into a single cancelable object. */ @objc -public class CancelGroup: NSObject, AGSCancelable{ - +public class CancelGroup: NSObject, AGSCancelable { /// Cancels all the AGSCancelables in the group. - public func cancel(){ - children.forEach{ $0.cancel() } + public func cancel() { + children.forEach { $0.cancel() } _canceled = true } private var _canceled: Bool = false /// Whether or not the group is canceled. - public func isCanceled() -> Bool{ + public func isCanceled() -> Bool { return _canceled } /// The children associated with this group. public var children: [AGSCancelable] = [AGSCancelable]() - } diff --git a/Toolkit/ArcGISToolkit/Coalescer.swift b/Toolkit/ArcGISToolkit/Coalescer.swift index 26a42b52..3d27d7e3 100644 --- a/Toolkit/ArcGISToolkit/Coalescer.swift +++ b/Toolkit/ArcGISToolkit/Coalescer.swift @@ -12,7 +12,6 @@ // limitations under the License. internal class Coalescer { - // Class to coalesce actions into intervals. // This is helpful for the Scalebar because we get updates to the visibleArea up to 60hz and we // don't need to redraw the Scalebar that often @@ -21,7 +20,7 @@ internal class Coalescer { var interval: DispatchTimeInterval var action: (() -> Void) - init (dispatchQueue: DispatchQueue, interval: DispatchTimeInterval, action: @escaping (()->Void)){ + init (dispatchQueue: DispatchQueue, interval: DispatchTimeInterval, action: @escaping (() -> Void)) { self.dispatchQueue = dispatchQueue self.interval = interval self.action = action @@ -29,11 +28,10 @@ internal class Coalescer { private var count = 0 - func ping(){ - + func ping() { // synchronize to a serial queue, in this case main thread - if !Thread.isMainThread{ - DispatchQueue.main.async{ self.ping() } + if !Thread.isMainThread { + DispatchQueue.main.async { self.ping() } return } @@ -41,9 +39,8 @@ internal class Coalescer { count += 1 // the first time the count is incremented, it dispatches the action - if count == 1{ - dispatchQueue.asyncAfter(deadline: DispatchTime.now() + interval){ - + if count == 1 { + dispatchQueue.asyncAfter(deadline: DispatchTime.now() + interval) { // call the action self.action() @@ -51,19 +48,14 @@ internal class Coalescer { self.resetCount() } } - } - private func resetCount(){ - + private func resetCount() { // synchronize to a serial queue, in this case main thread - if !Thread.isMainThread{ - DispatchQueue.main.async{ self.count = 0 } - } - else{ + if !Thread.isMainThread { + DispatchQueue.main.async { self.count = 0 } + } else { self.count = 0 } } - } - diff --git a/Toolkit/ArcGISToolkit/Compass.swift b/Toolkit/ArcGISToolkit/Compass.swift index 3bb07305..62391040 100644 --- a/Toolkit/ArcGISToolkit/Compass.swift +++ b/Toolkit/ArcGISToolkit/Compass.swift @@ -15,7 +15,6 @@ import UIKit import ArcGIS public class Compass: UIImageView { - public var heading: Double = 0.0 { // Rotation - bound to MapView.MapRotation didSet { mapView.setViewpointRotation(heading, completion: nil) @@ -67,16 +66,14 @@ public class Compass: UIImageView { animateCompass() // Add Compass as an observer of the mapView's rotation. - rotationObservation = mapView.observe(\.rotation, options: .new) {[weak self] (mapView, change) in - - guard let rotation = change.newValue else{ + rotationObservation = mapView.observe(\.rotation, options: .new) {[weak self] (_, change) in + guard let rotation = change.newValue else { return } // make sure that UI changes are made on the main thread - DispatchQueue.main.async{ - - guard let self = self else{ + DispatchQueue.main.async { + guard let self = self else { return } @@ -87,14 +84,14 @@ public class Compass: UIImageView { self.animateCompass() } } - } - required public init?(coder aDecoder: NSCoder) { + public required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } - @objc func compassTapped(){ + @objc + func compassTapped() { mapView.setViewpointRotation(0, completion: nil) } diff --git a/Toolkit/ArcGISToolkit/Extensions.swift b/Toolkit/ArcGISToolkit/Extensions.swift index e331e837..6db6113b 100644 --- a/Toolkit/ArcGISToolkit/Extensions.swift +++ b/Toolkit/ArcGISToolkit/Extensions.swift @@ -29,5 +29,3 @@ extension UIApplication { return controller } } - - diff --git a/Toolkit/ArcGISToolkit/JobManager.swift b/Toolkit/ArcGISToolkit/JobManager.swift index abe5ad41..4ff67898 100644 --- a/Toolkit/ArcGISToolkit/JobManager.swift +++ b/Toolkit/ArcGISToolkit/JobManager.swift @@ -21,7 +21,6 @@ public typealias JobCompletionHandler = (Any?, Error?) -> Void // // MARK: JobManager - private let _jobManagerSharedInstance = JobManager(jobManagerID: "shared") /** @@ -40,7 +39,6 @@ private let _jobManagerSharedInstance = JobManager(jobManagerID: "shared") method. */ public class JobManager: NSObject { - /// Default shared instance of the JobManager. public class var shared: JobManager { return _jobManagerSharedInstance @@ -81,6 +79,8 @@ public class JobManager: NSObject { return "com.esri.arcgis.runtime.toolkit.jobManager.\(jobManagerID).jobs" } + private var jobStatusObservation: NSKeyValueObservation? + /// Create a JobManager with an ID. /// /// - Parameter jobManagerID: An arbitrary identifier for this JobManager. @@ -100,31 +100,22 @@ public class JobManager: NSObject { // Observing job status code private func observeJobStatus(job: AGSJob) { - job.addObserver(self, forKeyPath: #keyPath(AGSJob.status), context: &kvoContext) + jobStatusObservation = job.observe(\.status, options: [.new]) { [weak self] (_, _) in + self?.saveJobsToUserDefaults() + } } private func unObserveJobStatus(job: AGSJob) { - job.removeObserver(self, forKeyPath: #keyPath(AGSJob.status)) - } - - override public func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey: Any]?, context: UnsafeMutableRawPointer?) { - if context == &kvoContext { - if keyPath == #keyPath(AGSJob.status) { - // when a job's status changes we need to save to user defaults again - // so that the correct job state is reflected in our saved state - saveJobsToUserDefaults() - } - } - else { - super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context) - } + jobStatusObservation?.invalidate() + jobStatusObservation = nil } /// Register an `AGSJob` with the `JobManager`. /// /// - Parameter job: The AGSJob to register. /// - Returns: A unique ID for the AGSJob's registration which can be used to unregister the job. - @discardableResult public func register(job: AGSJob) -> String { + @discardableResult + public func register(job: AGSJob) -> String { let jobUniqueID = NSUUID().uuidString keyedJobs[jobUniqueID] = job return jobUniqueID @@ -134,7 +125,8 @@ public class JobManager: NSObject { /// /// - Parameter job: The job to unregister. /// - Returns: `true` if the job was found, `false` otherwise. - @discardableResult public func unregister(job: AGSJob) -> Bool { + @discardableResult + public func unregister(job: AGSJob) -> Bool { if let jobUniqueID = keyedJobs.first(where: { $0.value === job })?.key { keyedJobs[jobUniqueID] = nil return true @@ -146,7 +138,8 @@ public class JobManager: NSObject { /// /// - Parameter jobUniqueID: The job's unique ID, returned from calling `register()`. /// - Returns: `true` if the Job was found, `false` otherwise. - @discardableResult public func unregister(jobUniqueID: String) -> Bool { + @discardableResult + public func unregister(jobUniqueID: String) -> Bool { let removed = keyedJobs.removeValue(forKey: jobUniqueID) != nil return removed } @@ -163,7 +156,8 @@ public class JobManager: NSObject { /// /// - Parameter completion: A completion block that is called when the status of all `AGSJob`s has been checked. Passed `true` if all statuses were retrieves successfully, or `false` otherwise. /// - Returns: An `AGSCancelable` group that can be used to cancel the status checks. - @discardableResult public func checkStatusForAllJobs(completion: @escaping (Bool)->Void) -> AGSCancelable { + @discardableResult + public func checkStatusForAllJobs(completion: @escaping (Bool) -> Void) -> AGSCancelable { let cancelGroup = CancelGroup() let group = DispatchGroup() @@ -208,14 +202,12 @@ public class JobManager: NSObject { checkStatusForAllJobs { completedWithoutErrors in if completedWithoutErrors { completionHandler(.newData) - } - else{ + } else { completionHandler(.failed) } } } } - /// Resume all paused and not-started `AGSJob`s. /// @@ -228,8 +220,8 @@ public class JobManager: NSObject { /// - statusHandler: A callback block that is called by each active `AGSJob` when the `AGSJob`'s status changes or its messages array is updated. /// - completion: A callback block that is called by each `AGSJob` when it has completed. public func resumeAllPausedJobs(statusHandler: @escaping JobStatusHandler, completion: @escaping JobCompletionHandler) { - keyedJobs.lazy.filter({ $0.value.status == .paused || $0.value.status == .notStarted }).forEach { - $0.value.start(statusHandler: statusHandler, completion:completion) + keyedJobs.lazy.filter { $0.value.status == .paused || $0.value.status == .notStarted }.forEach { + $0.value.start(statusHandler: statusHandler, completion: completion) } } diff --git a/Toolkit/ArcGISToolkit/LegendViewController.swift b/Toolkit/ArcGISToolkit/LegendViewController.swift index 2af22155..d23f698a 100644 --- a/Toolkit/ArcGISToolkit/LegendViewController.swift +++ b/Toolkit/ArcGISToolkit/LegendViewController.swift @@ -15,24 +15,21 @@ import UIKit import ArcGIS public class LegendViewController: UIViewController, UITableViewDelegate, UITableViewDataSource { - - public var geoView: AGSGeoView?{ - didSet - { + public var geoView: AGSGeoView? { + didSet { if geoView != nil { if let mapView = geoView as? AGSMapView { - mapView.map?.load(completion: { [weak self] (error) in + mapView.map?.load { [weak self] (_) in if let basemap = mapView.map?.basemap { - basemap.load(completion: { (error) in + basemap.load { (_) in self?.updateLayerData() - }) + } } - }) - } - else if let sceneView = geoView as? AGSSceneView { - sceneView.scene?.load(completion: {[weak self] (error) in + } + } else if let sceneView = geoView as? AGSSceneView { + sceneView.scene?.load(completion: {[weak self] (_) in if let basemap = sceneView.scene?.basemap { - basemap.load(completion: { (error) in + basemap.load(completion: { (_) in self?.updateLayerData() }) } @@ -42,7 +39,7 @@ public class LegendViewController: UIViewController, UITableViewDelegate, UITabl //set layerViewStateChangedHandler if let geoView = geoView { geoView.layerViewStateChangedHandler = { [weak self] (layer: AGSLayer, layerViewState: AGSLayerViewState) in - DispatchQueue.main.async{ + DispatchQueue.main.async { self?.updateLegendArray() } } @@ -52,14 +49,12 @@ public class LegendViewController: UIViewController, UITableViewDelegate, UITabl } public var respectScaleRange: Bool = true { - didSet - { + didSet { updateLayerData() } } public var reverseLayerOrder: Bool = false { - didSet - { + didSet { updateLayerData() } } @@ -68,10 +63,10 @@ public class LegendViewController: UIViewController, UITableViewDelegate, UITabl @IBOutlet private var tableView: UITableView? // dictionary of legend infos; keys are AGSLayerContent objectIdentifier values - private var legendInfos = [UInt:[AGSLegendInfo]]() + private var legendInfos = [UInt: [AGSLegendInfo]]() // dictionary of symbol swatches (images); keys are the symbol used to create the swatch - private var symbolSwatches = [AGSSymbol:UIImage]() + private var symbolSwatches = [AGSSymbol: UIImage]() // the array of all layers in the map, including basemap layers private var layerArray = [AGSLayer]() @@ -93,14 +88,14 @@ public class LegendViewController: UIViewController, UITableViewDelegate, UITabl fatalError("use the method `makeLegendViewController` instead") } - required public init?(coder aDecoder: NSCoder) { + public required init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) } // use this static method to instantiate the view controller from our storyboard - static public func makeLegendViewController(geoView: AGSGeoView? = nil) -> LegendViewController? { + public static func makeLegendViewController(geoView: AGSGeoView? = nil) -> LegendViewController? { // get the bundle and then the storyboard - let bundle = Bundle.init(for: LegendViewController.self) + let bundle = Bundle(for: LegendViewController.self) let storyboard = UIStoryboard(name: "Legend", bundle: bundle) // create the legend VC from the storyboard @@ -124,21 +119,19 @@ public class LegendViewController: UIViewController, UITableViewDelegate, UITabl var cell: UITableViewCell! // configure the cell... - let rowItem:AnyObject = legendArray[indexPath.row] + let rowItem: AnyObject = legendArray[indexPath.row] if let layer = rowItem as? AGSLayer { // item is a layer cell = tableView.dequeueReusableCell(withIdentifier: LegendViewController.layerTitleCellID)! let textLabel = cell.viewWithTag(LegendViewController.labelTag) as? UILabel textLabel?.text = layer.name - } - else if let layerContent = rowItem as? AGSLayerContent { + } else if let layerContent = rowItem as? AGSLayerContent { // item is not a layer, but still implements AGSLayerContent // so it's a sublayer cell = tableView.dequeueReusableCell(withIdentifier: LegendViewController.sublayerTitleCellID)! let textLabel = cell.viewWithTag(LegendViewController.labelTag) as? UILabel textLabel?.text = layerContent.name - } - else if let legendInfo = rowItem as? AGSLegendInfo { + } else if let legendInfo = rowItem as? AGSLegendInfo { // item is a legendInfo cell = tableView.dequeueReusableCell(withIdentifier: LegendViewController.legendInfoCellID)! let textLabel = cell.viewWithTag(LegendViewController.labelTag) as? UILabel @@ -152,18 +145,17 @@ public class LegendViewController: UIViewController, UITableViewDelegate, UITabl // we have a swatch, so set it into the imageView and stop the activity indicator imageview?.image = swatch activityIndicator.stopAnimating() - } - else { + } else { // tag the cell so we know what index path it's being used for cell.tag = indexPath.hashValue // we don't have a swatch for the given symbol, start the activity indicator // and create the swatch activityIndicator.startAnimating() - symbol.createSwatch(completion: { [weak self] (image, error) -> Void in + symbol.createSwatch(completion: { [weak self] (image, _) -> Void in // make sure this is the cell we still care about and that it // wasn't already recycled by the time we get the swatch - if cell.tag != indexPath.hashValue{ + if cell.tag != indexPath.hashValue { return } @@ -182,7 +174,6 @@ public class LegendViewController: UIViewController, UITableViewDelegate, UITabl // update the legend data for all layers and sublayers private func updateLayerData() { - // remove all saved data legendInfos.removeAll() symbolSwatches.removeAll() @@ -196,7 +187,7 @@ public class LegendViewController: UIViewController, UITableViewDelegate, UITabl private func populateLayerArray() { layerArray.removeAll() - var basemap:AGSBasemap? + var basemap: AGSBasemap? // Because the layers in the map's operationalLayers property // are drawn from the bottom up (the first layer in the array is @@ -208,8 +199,7 @@ public class LegendViewController: UIViewController, UITableViewDelegate, UITabl if let layers = mapView.map?.operationalLayers as AnyObject as? [AGSLayer] { reversedLayerArray.append(contentsOf: layers) } - } - else if let sceneView = geoView as? AGSSceneView { + } else if let sceneView = geoView as? AGSSceneView { basemap = sceneView.scene?.basemap if let layers = sceneView.scene?.operationalLayers as AnyObject as? [AGSLayer] { reversedLayerArray.append(contentsOf: layers) @@ -236,10 +226,9 @@ public class LegendViewController: UIViewController, UITableViewDelegate, UITabl // This is "!reverseLayerOrder" because the layers are by default reversed // and will only NOT be reversed here if reverseLayerOrder == true. - if !reverseLayerOrder && reversedLayerArray.count > 0 { + if !reverseLayerOrder && !reversedLayerArray.isEmpty { layerArray.append(contentsOf: reversedLayerArray.reversed()) - } - else { + } else { // we are reversing the order, so just use the original reversedLayerArray layerArray.append(contentsOf: reversedLayerArray) } @@ -257,11 +246,10 @@ public class LegendViewController: UIViewController, UITableViewDelegate, UITabl private func loadIndividualLayer(_ layerContent: AGSLayerContent) { if let layer = layerContent as? AGSLayer { // we have an AGSLayer, so make sure it's loaded - layer.load { [weak self] (error) in + layer.load { [weak self] (_) in self?.loadSublayersOrLegendInfos(layerContent) } - } - else { + } else { self.loadSublayersOrLegendInfos(layerContent) } } @@ -271,22 +259,21 @@ public class LegendViewController: UIViewController, UITableViewDelegate, UITabl // the AGSLayer is loaded for this layer/sublayer, so // set the contents changed handler. layerContent.subLayerContentsChangedHandler = { [weak self] () in - DispatchQueue.main.async{ + DispatchQueue.main.async { self?.updateLegendArray() } } // if we have sublayer contents, load those as well - if layerContent.subLayerContents.count > 0 { + if !layerContent.subLayerContents.isEmpty { layerContent.subLayerContents.forEach { self.loadIndividualLayer($0) } - } - else { + } else { // fetch the legend infos - layerContent.fetchLegendInfos(completion: { [weak self] (legendInfos, error) in + layerContent.fetchLegendInfos { [weak self] (legendInfos, _) in //handle legendInfos self?.legendInfos[LegendViewController.objectIdentifierFor(layerContent)] = legendInfos self?.updateLegendArray() - }) + } } } @@ -295,7 +282,6 @@ public class LegendViewController: UIViewController, UITableViewDelegate, UITabl // items once layers load. Updating everything here will make // implementing the table view data source methods much easier. private func updateLegendArray() { - legendArray.removeAll() // filter any layers which are not visible or not showInLegend @@ -320,8 +306,7 @@ public class LegendViewController: UIViewController, UITableViewDelegate, UITabl if featureCollectionLayer.layers.count > 1 { legendArray.append(layerContent) } - } - else { + } else { legendArray.append(layerContent) } updateLayerLegend(layerContent) @@ -334,17 +319,16 @@ public class LegendViewController: UIViewController, UITableViewDelegate, UITabl // Handle subLayerContents and legend infos; this method assumes that // the incoming layerContent argument is visible and showInLegend == true. private func updateLayerLegend(_ layerContent: AGSLayerContent) { - if layerContent.subLayerContents.count > 0 { + if !layerContent.subLayerContents.isEmpty { // filter any sublayers which are not visible or not showInLegend let sublayerContents = layerContent.subLayerContents.filter { $0.isVisible && $0.showInLegend } - sublayerContents.forEach({ (layerContent) in + sublayerContents.forEach { (layerContent) in legendArray.append(layerContent) updateLayerLegend(layerContent) - }) - } - else { - if let internalLegendInfos:[AGSLegendInfo] = legendInfos[LegendViewController.objectIdentifierFor(layerContent as AnyObject)] { - legendArray = legendArray + internalLegendInfos + } + } else { + if let internalLegendInfos: [AGSLegendInfo] = legendInfos[LegendViewController.objectIdentifierFor(layerContent as AnyObject)] { + legendArray += internalLegendInfos } } } diff --git a/Toolkit/ArcGISToolkit/MapViewController.swift b/Toolkit/ArcGISToolkit/MapViewController.swift index e4e744a3..0643a481 100644 --- a/Toolkit/ArcGISToolkit/MapViewController.swift +++ b/Toolkit/ArcGISToolkit/MapViewController.swift @@ -15,7 +15,6 @@ import UIKit import ArcGIS open class MapViewController: UIViewController { - public let mapView = AGSMapView(frame: CGRect.zero) override open func viewDidLoad() { @@ -25,6 +24,4 @@ open class MapViewController: UIViewController { mapView.autoresizingMask = [.flexibleWidth, .flexibleHeight] view.addSubview(mapView) } - } - diff --git a/Toolkit/ArcGISToolkit/MeasureToolbar.swift b/Toolkit/ArcGISToolkit/MeasureToolbar.swift index 64039924..baae9de5 100644 --- a/Toolkit/ArcGISToolkit/MeasureToolbar.swift +++ b/Toolkit/ArcGISToolkit/MeasureToolbar.swift @@ -19,11 +19,10 @@ struct Measurement { let unit: AGSUnit } -class MeasureResultView: UIView{ - +class MeasureResultView: UIView { var measurement: Measurement? { - didSet{ - if let measurement = measurement{ + didSet { + if let measurement = measurement { valueLabel.text = valueString() unitButton.setTitle(stringForUnit(measurement.unit), for: .normal) unitButton.isHidden = false @@ -32,9 +31,9 @@ class MeasureResultView: UIView{ } } - var helpText: String?{ - didSet{ - if let helpText = helpText{ + var helpText: String? { + didSet { + if let helpText = helpText { valueLabel.text = helpText unitButton.isHidden = true unitButton.setTitle(nil, for: .normal) @@ -47,14 +46,13 @@ class MeasureResultView: UIView{ var stackView: UIStackView let numberFormatter = NumberFormatter() - var buttonTapHandler: (()->(Void))? + var buttonTapHandler: (() -> Void)? - override var intrinsicContentSize: CGSize{ + override var intrinsicContentSize: CGSize { return stackView.systemLayoutSizeFitting(CGSize(width: 0, height: 0), withHorizontalFittingPriority: .fittingSizeLevel, verticalFittingPriority: .fittingSizeLevel) } override init(frame: CGRect) { - numberFormatter.numberStyle = .decimal numberFormatter.minimumFractionDigits = 0 numberFormatter.maximumFractionDigits = 2 @@ -114,29 +112,28 @@ class MeasureResultView: UIView{ fatalError("init(coder:) has not been implemented") } - override public class var requiresConstraintBasedLayout: Bool { + override class var requiresConstraintBasedLayout: Bool { return true } - @objc func buttonTap(){ - guard unitButton.isHidden == false else{ + @objc + func buttonTap() { + guard unitButton.isHidden == false else { return } buttonTapHandler?() } - func valueString() -> String?{ - - guard let measurement = measurement else{ + func valueString() -> String? { + guard let measurement = measurement else { return "" } // if number greater than some value then don't show fraction - if measurement.value > 1_000{ + if measurement.value > 1_000 { numberFormatter.maximumFractionDigits = 0 - } - else{ + } else { numberFormatter.maximumFractionDigits = 2 } @@ -147,24 +144,21 @@ class MeasureResultView: UIView{ return measurementValueString } - func stringForUnit(_ unit: AGSUnit?) -> String?{ + func stringForUnit(_ unit: AGSUnit?) -> String? { guard let unit = unit else { return "" } return unit.pluralDisplayName } - } -private enum MeasureToolbarMode{ +private enum MeasureToolbarMode { case length case area case feature } public class MeasureToolbar: UIToolbar, AGSGeoViewTouchDelegate { - - // Exposed so that the user can customize the sketch editor styles. // Consumers of the MeasureToolbar should not mutate the sketch editor state // other than it's style. @@ -180,7 +174,7 @@ public class MeasureToolbar: UIToolbar, AGSGeoViewTouchDelegate { } public var mapView: AGSMapView? { - didSet{ + didSet { guard mapView != oldValue else { return } unbindFromMapView(mapView: oldValue) bindToMapView(mapView: mapView) @@ -221,7 +215,7 @@ public class MeasureToolbar: UIToolbar, AGSGeoViewTouchDelegate { } } - private let resultView: MeasureResultView = MeasureResultView() + private let resultView = MeasureResultView() private var undoButton: UIBarButtonItem! private var redoButton: UIBarButtonItem! @@ -255,12 +249,12 @@ public class MeasureToolbar: UIToolbar, AGSGeoViewTouchDelegate { sharedInitialization() } - required public init?(coder aDecoder: NSCoder) { + public required init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) sharedInitialization() } - convenience public init(mapView: AGSMapView){ + public convenience init(mapView: AGSMapView) { self.init(frame: .zero) self.mapView = mapView // because didSet doesn't happen in constructors @@ -270,8 +264,7 @@ public class MeasureToolbar: UIToolbar, AGSGeoViewTouchDelegate { private var sketchModeButtons: [UIBarButtonItem] = [] private var selectModeButtons: [UIBarButtonItem] = [] - private func sharedInitialization(){ - + private func sharedInitialization() { let bundle = Bundle(for: type(of: self)) let measureLengthImage = UIImage(named: "MeasureLength", in: bundle, compatibleWith: nil) let measureAreaImage = UIImage(named: "MeasureArea", in: bundle, compatibleWith: nil) @@ -281,7 +274,7 @@ public class MeasureToolbar: UIToolbar, AGSGeoViewTouchDelegate { undoButton = UIBarButtonItem(image: undoImage, style: .plain, target: nil, action: nil) redoButton = UIBarButtonItem(image: redoImage, style: .plain, target: nil, action: nil) - clearButton = UIBarButtonItem(barButtonSystemItem: .trash, target: nil, action:nil) + clearButton = UIBarButtonItem(barButtonSystemItem: .trash, target: nil, action: nil) segControl = UISegmentedControl(items: ["Length", "Area", "Select"]) segControl.setImage(measureLengthImage, forSegmentAt: 0) @@ -319,10 +312,10 @@ public class MeasureToolbar: UIToolbar, AGSGeoViewTouchDelegate { NotificationCenter.default.addObserver(self, selector: #selector(sketchEditorGeometryDidChange(_:)), name: .AGSSketchEditorGeometryDidChange, object: nil) } - private func bindToMapView(mapView: AGSMapView?){ + private func bindToMapView(mapView: AGSMapView?) { mapView?.touchDelegate = self - if let mapView = mapView{ + if let mapView = mapView { // defaults for symbology selectionLineSymbol = lineSketchEditor.style.lineSymbol let fillColor = mapView.selectionProperties.color.withAlphaComponent(0.25) @@ -339,22 +332,21 @@ public class MeasureToolbar: UIToolbar, AGSGeoViewTouchDelegate { } } - private func unbindFromMapView(mapView: AGSMapView?){ + private func unbindFromMapView(mapView: AGSMapView?) { mapView?.sketchEditor = nil mapView?.touchDelegate = nil - if let mapView = mapView, let selectionOverlay = selectionOverlay{ + if let mapView = mapView, let selectionOverlay = selectionOverlay { mapView.graphicsOverlays.remove(selectionOverlay) } } private var didSetConstraints: Bool = false - public override func updateConstraints() { - + override public func updateConstraints() { super.updateConstraints() - guard !didSetConstraints else{ + guard !didSetConstraints else { return } @@ -395,22 +387,19 @@ public class MeasureToolbar: UIToolbar, AGSGeoViewTouchDelegate { return true } - @objc private func segmentControlValueChanged(){ - - if segControl.selectedSegmentIndex == 0{ + @objc + private func segmentControlValueChanged() { + if segControl.selectedSegmentIndex == 0 { startLineMode() - } - else if segControl.selectedSegmentIndex == 1{ + } else if segControl.selectedSegmentIndex == 1 { startAreaMode() - } - else if segControl.selectedSegmentIndex == 2{ + } else if segControl.selectedSegmentIndex == 2 { startFeatureMode() } } - private func startLineMode(){ - - guard mode != MeasureToolbarMode.length else{ + private func startLineMode() { + guard mode != MeasureToolbarMode.length else { return } @@ -419,14 +408,13 @@ public class MeasureToolbar: UIToolbar, AGSGeoViewTouchDelegate { self.items = sketchModeButtons mapView?.sketchEditor = lineSketchEditor - if !lineSketchEditor.isStarted{ + if !lineSketchEditor.isStarted { lineSketchEditor.start(with: AGSSketchCreationMode.polyline) } } - private func startAreaMode(){ - - guard mode != MeasureToolbarMode.area else{ + private func startAreaMode() { + guard mode != MeasureToolbarMode.area else { return } @@ -435,14 +423,13 @@ public class MeasureToolbar: UIToolbar, AGSGeoViewTouchDelegate { self.items = sketchModeButtons mapView?.sketchEditor = areaSketchEditor - if !areaSketchEditor.isStarted{ + if !areaSketchEditor.isStarted { areaSketchEditor.start(with: AGSSketchCreationMode.polygon) } } - private func startFeatureMode(){ - - guard mode != MeasureToolbarMode.feature else{ + private func startFeatureMode() { + guard mode != MeasureToolbarMode.feature else { return } @@ -452,31 +439,32 @@ public class MeasureToolbar: UIToolbar, AGSGeoViewTouchDelegate { mapView?.sketchEditor = nil } - @objc private func undoButtonTap(){ + @objc + private func undoButtonTap() { mapView?.sketchEditor?.undoManager.undo() } - @objc private func redoButtonTap(){ + @objc + private func redoButtonTap() { mapView?.sketchEditor?.undoManager.redo() } - @objc private func clearButtonTap(){ + @objc + private func clearButtonTap() { mapView?.sketchEditor?.clearGeometry() } - private func unitsButtonTap(){ + private func unitsButtonTap() { let units: [AGSUnit] let selectedUnit: AGSUnit if mapView?.sketchEditor == lineSketchEditor || selectedGeometry?.geometryType == .polyline { - let linearUnitIDs: [AGSLinearUnitID] = [.centimeters, .feet, .inches, .kilometers, .meters, .miles, .millimeters, .nauticalMiles, .yards] units = linearUnitIDs.compactMap { AGSLinearUnit(unitID: $0) } selectedUnit = selectedLinearUnit } else if mapView?.sketchEditor == areaSketchEditor || selectedGeometry?.geometryType == .envelope || selectedGeometry?.geometryType == .polygon { - let areaUnitIDs: [AGSAreaUnitID] = [.acres, .hectares, .squareCentimeters, .squareDecimeters, .squareFeet, .squareKilometers, .squareMeters, .squareMillimeters, .squareMiles, .squareYards] units = areaUnitIDs.compactMap { AGSAreaUnit(unitID: $0) } selectedUnit = selectedAreaUnit @@ -499,7 +487,8 @@ public class MeasureToolbar: UIToolbar, AGSGeoViewTouchDelegate { /// `Notification.Name.AGSSketchEditorGeometryDidChange` being posted. /// /// - Parameter notification: The posted notification. - @objc private func sketchEditorGeometryDidChange(_ notification: Notification) { + @objc + private func sketchEditorGeometryDidChange(_ notification: Notification) { guard let sketchEditor = notification.object as? AGSSketchEditor, sketchEditor == lineSketchEditor || sketchEditor == areaSketchEditor else { return @@ -521,28 +510,26 @@ public class MeasureToolbar: UIToolbar, AGSGeoViewTouchDelegate { if let geometry = selectedGeometry { let measurement = Measurement(value: calculateMeasurement(of: geometry), unit: unit(for: geometry)) resultView.measurement = measurement - } else{ + } else { resultView.helpText = "Tap a feature" } } } - private func calculateSketchLength() -> Double{ - - guard mapView?.sketchEditor?.isSketchValid == true, let geom = mapView?.sketchEditor?.geometry else{ + private func calculateSketchLength() -> Double { + guard mapView?.sketchEditor?.isSketchValid == true, let geom = mapView?.sketchEditor?.geometry else { return 0 } return calculateLength(of: geom) } - private func calculateLength(of geom: AGSGeometry) -> Double{ - + private func calculateLength(of geom: AGSGeometry) -> Double { // if planar is very large then just return that, geodetic might take too long - if let linearUnit = geom.spatialReference?.unit as? AGSLinearUnit{ + if let linearUnit = geom.spatialReference?.unit as? AGSLinearUnit { var planar = AGSGeometryEngine.length(of: geom) planar = linearUnit.convert(toMeters: planar) - if planar > planarLengthMetersThreshold{ + if planar > planarLengthMetersThreshold { let planarDisplay = AGSLinearUnit.meters().convert(planar, to: selectedLinearUnit) //`print("returning planar length... \(planar) sq meters") return planarDisplay @@ -553,22 +540,20 @@ public class MeasureToolbar: UIToolbar, AGSGeoViewTouchDelegate { return AGSGeometryEngine.geodeticLength(of: geom, lengthUnit: selectedLinearUnit, curveType: geodeticCurveType) } - private func calculateSketchArea() -> Double{ - - guard mapView?.sketchEditor?.isSketchValid == true, let geom = mapView?.sketchEditor?.geometry else{ + private func calculateSketchArea() -> Double { + guard mapView?.sketchEditor?.isSketchValid == true, let geom = mapView?.sketchEditor?.geometry else { return 0 } return calculateArea(of: geom) } - private func calculateArea(of geom: AGSGeometry) -> Double{ - + private func calculateArea(of geom: AGSGeometry) -> Double { // if planar is very large then just return that, geodetic might take too long - if let linearUnit = geom.spatialReference?.unit as? AGSLinearUnit{ + if let linearUnit = geom.spatialReference?.unit as? AGSLinearUnit { let planar = AGSGeometryEngine.area(of: geom) if let planarMiles = linearUnit.toAreaUnit()?.convert(planar, to: AGSAreaUnit.squareMiles()), - planarMiles > planarAreaSquareMilesThreshold{ + planarMiles > planarAreaSquareMilesThreshold { let planarDisplay = AGSAreaUnit.squareMiles().convert(planarMiles, to: selectedAreaUnit) //print("returning planar area... \(planarMiles) sq miles") return planarDisplay @@ -579,7 +564,7 @@ public class MeasureToolbar: UIToolbar, AGSGeoViewTouchDelegate { return AGSGeometryEngine.geodeticArea(of: geom, areaUnit: selectedAreaUnit, curveType: geodeticCurveType) } - private func calculateMeasurement(of geom: AGSGeometry) -> Double{ + private func calculateMeasurement(of geom: AGSGeometry) -> Double { switch geom.geometryType { case .polyline: return calculateLength(of: geom) @@ -591,8 +576,7 @@ public class MeasureToolbar: UIToolbar, AGSGeoViewTouchDelegate { } } - private func unit(for geom: AGSGeometry) -> AGSUnit{ - + private func unit(for geom: AGSGeometry) -> AGSUnit { switch geom.geometryType { case .polyline: return selectedLinearUnit @@ -603,8 +587,7 @@ public class MeasureToolbar: UIToolbar, AGSGeoViewTouchDelegate { } } - private func selectionSymbol(for geom: AGSGeometry) -> AGSSymbol?{ - + private func selectionSymbol(for geom: AGSGeometry) -> AGSSymbol? { switch geom.geometryType { case .polyline: return selectionLineSymbol @@ -617,36 +600,32 @@ public class MeasureToolbar: UIToolbar, AGSGeoViewTouchDelegate { private var lastIdentify: AGSCancelable? - public func geoView(_ geoView: AGSGeoView, didTapAtScreenPoint screenPoint: CGPoint, mapPoint: AGSPoint){ - + public func geoView(_ geoView: AGSGeoView, didTapAtScreenPoint screenPoint: CGPoint, mapPoint: AGSPoint) { lastIdentify?.cancel() - lastIdentify = geoView.identifyGraphicsOverlays(atScreenPoint: screenPoint, tolerance: MeasureToolbar.identifyTolerance, returnPopupsOnly: false){ [weak self] results, error in - - guard let self = self else{ + lastIdentify = geoView.identifyGraphicsOverlays(atScreenPoint: screenPoint, tolerance: MeasureToolbar.identifyTolerance, returnPopupsOnly: false) { [weak self] results, error in + guard let self = self else { return } - if let error = error{ - guard (error as NSError).domain != NSCocoaErrorDomain && (error as NSError).code != NSUserCancelledError else{ + if let error = error { + guard (error as NSError).domain != NSCocoaErrorDomain && (error as NSError).code != NSUserCancelledError else { return } } - if let geom = self.firstOverlayPolyResult(in: results){ + if let geom = self.firstOverlayPolyResult(in: results) { // display graphic result self.select(geom: geom) - } - else{ + } else { // otherwise identify layers to try to find a feature - self.lastIdentify = geoView.identifyLayers(atScreenPoint: screenPoint, tolerance: MeasureToolbar.identifyTolerance, returnPopupsOnly: false){ [weak self] results, error in - - guard let self = self else{ + self.lastIdentify = geoView.identifyLayers(atScreenPoint: screenPoint, tolerance: MeasureToolbar.identifyTolerance, returnPopupsOnly: false) { [weak self] results, error in + guard let self = self else { return } - if let error = error{ - guard (error as NSError).domain != NSCocoaErrorDomain && (error as NSError).code != NSUserCancelledError else{ + if let error = error { + guard (error as NSError).domain != NSCocoaErrorDomain && (error as NSError).code != NSUserCancelledError else { return } } @@ -655,21 +634,19 @@ public class MeasureToolbar: UIToolbar, AGSGeoViewTouchDelegate { self.select(geom: geom) } } - } } - private func clearGeometrySelection(){ + private func clearGeometrySelection() { selectionOverlay?.clearSelection() selectionOverlay?.graphics.removeAllObjects() selectedGeometry = nil } - private func select(geom: AGSGeometry?){ - + private func select(geom: AGSGeometry?) { clearGeometrySelection() - guard let geom = geom else{ + guard let geom = geom else { return } @@ -680,15 +657,14 @@ public class MeasureToolbar: UIToolbar, AGSGeoViewTouchDelegate { selectedGeometry = geom } - private func firstOverlayPolyResult(in identifyResults: [AGSIdentifyGraphicsOverlayResult]?) -> AGSGeometry?{ - - guard let results = identifyResults else{ + private func firstOverlayPolyResult(in identifyResults: [AGSIdentifyGraphicsOverlayResult]?) -> AGSGeometry? { + guard let results = identifyResults else { return nil } - for result in results{ - for ge in result.graphics{ - if ge.geometry?.geometryType == .polyline || ge.geometry?.geometryType == .polygon || ge.geometry?.geometryType == .envelope{ + for result in results { + for ge in result.graphics { + if ge.geometry?.geometryType == .polyline || ge.geometry?.geometryType == .polygon || ge.geometry?.geometryType == .envelope { return ge.geometry! } } @@ -696,19 +672,18 @@ public class MeasureToolbar: UIToolbar, AGSGeoViewTouchDelegate { return nil } - private func firstLayerPolyResult(in identifyResults: [AGSIdentifyLayerResult]?) -> AGSGeometry?{ - - guard let results = identifyResults else{ + private func firstLayerPolyResult(in identifyResults: [AGSIdentifyLayerResult]?) -> AGSGeometry? { + guard let results = identifyResults else { return nil } - for result in results{ - for ge in result.geoElements{ - if ge.geometry?.geometryType == .polyline || ge.geometry?.geometryType == .polygon || ge.geometry?.geometryType == .envelope{ + for result in results { + for ge in result.geoElements { + if ge.geometry?.geometryType == .polyline || ge.geometry?.geometryType == .polygon || ge.geometry?.geometryType == .envelope { return ge.geometry! } } - if let subGeom = firstLayerPolyResult(in: result.sublayerResults){ + if let subGeom = firstLayerPolyResult(in: result.sublayerResults) { return subGeom } } diff --git a/Toolkit/ArcGISToolkit/PopupController.swift b/Toolkit/ArcGISToolkit/PopupController.swift index 95764223..e197e0e7 100644 --- a/Toolkit/ArcGISToolkit/PopupController.swift +++ b/Toolkit/ArcGISToolkit/PopupController.swift @@ -18,7 +18,6 @@ import ArcGIS /// Through its use of the `AGSPopupsViewController`, it provides a complete /// feature editing and collecting experience. public class PopupController: NSObject, AGSPopupsViewControllerDelegate, AGSGeoViewTouchDelegate { - private var lastPopupQueries = [AGSCancelable]() private var popupsViewController: AGSPopupsViewController? private let sketchEditor = AGSSketchEditor() @@ -42,8 +41,7 @@ public class PopupController: NSObject, AGSPopupsViewControllerDelegate, AGSGeoV /// - takeOverTouchDelegate: Whether or not the `PopupController` will take over the `AGSGeoView's` `touchDelegate`. /// If `false` then you must forward calls from the `AGSGeoViewTouchDelegate` to the `PopupController`. Defaults to `true`. /// - showAddFeatureButton: If `true` then a `UIBarButtonItem` will be added to the `navigationItem` as a right-hand button. - public init(geoViewController: UIViewController, geoView: AGSGeoView, takeOverTouchDelegate: Bool = true, showAddFeatureButton: Bool = true){ - + public init(geoViewController: UIViewController, geoView: AGSGeoView, takeOverTouchDelegate: Bool = true, showAddFeatureButton: Bool = true) { self.geoViewController = geoViewController self.geoView = geoView self.addNewFeatureButtonItem = UIBarButtonItem(barButtonSystemItem: .add, target: nil, action: nil) @@ -53,33 +51,32 @@ public class PopupController: NSObject, AGSPopupsViewControllerDelegate, AGSGeoV self.addNewFeatureButtonItem.target = self self.addNewFeatureButtonItem.action = #selector(addNewFeatureTap) - if showAddFeatureButton{ - if let items = geoViewController.navigationItem.rightBarButtonItems{ + if showAddFeatureButton { + if let items = geoViewController.navigationItem.rightBarButtonItems { geoViewController.navigationItem.rightBarButtonItems = [self.addNewFeatureButtonItem] + items - } - else{ + } else { geoViewController.navigationItem.rightBarButtonItem = self.addNewFeatureButtonItem } } - if takeOverTouchDelegate{ + if takeOverTouchDelegate { self.geoView.touchDelegate = self } sketchEditor.isVisible = true - if let mapView = geoView as? AGSMapView{ + if let mapView = geoView as? AGSMapView { mapView.sketchEditor = sketchEditor } } private var addingNewFeature: Bool = false - @objc private func addNewFeatureTap(){ - + @objc + private func addNewFeatureTap() { // if old pvc is being shown still for some reason, dismiss it self.cleanupLastPopupsViewController() - guard let map = (geoView as? AGSMapView)?.map else{ + guard let map = (geoView as? AGSMapView)?.map else { return } @@ -91,54 +88,48 @@ public class PopupController: NSObject, AGSPopupsViewControllerDelegate, AGSGeoV geoViewController?.present(navigationController, animated: true) } - - private func cleanupLastPopupsViewController(){ + private func cleanupLastPopupsViewController() { unselectLastSelectedFeature() // if old pvc is being shown still for some reason, dismiss it if popupsViewController?.view?.window != nil { - if popupsViewController == geoViewController?.navigationController?.topViewController{ + if popupsViewController == geoViewController?.navigationController?.topViewController { geoViewController?.navigationController?.popToViewController(geoViewController!, animated: true) - } - else if popupsViewController == geoViewController?.presentedViewController{ + } else if popupsViewController == geoViewController?.presentedViewController { popupsViewController?.dismiss(animated: true) } } // cleanup last time - lastPopupQueries.forEach{ $0.cancel() } - popupsViewController = nil; + lastPopupQueries.forEach { $0.cancel() } + popupsViewController = nil lastPopupQueries.removeAll() } public func geoView(_ geoView: AGSGeoView, didTapAtScreenPoint screenPoint: CGPoint, mapPoint: AGSPoint) { - self.cleanupLastPopupsViewController() - guard let mapView = geoView as? AGSMapView , mapView.map != nil else{ + guard let mapView = geoView as? AGSMapView, mapView.map != nil else { return } let c = mapView.identifyLayers(atScreenPoint: screenPoint, tolerance: 10, returnPopupsOnly: true, maximumResultsPerLayer: 12) { [weak self] (identifyResults, error) -> Void in - if let identifyResults = identifyResults { - let popups = identifyResults.flatMap({ $0.allPopups }) + let popups = identifyResults.flatMap { $0.allPopups } self?.showPopups(popups) - } - else if let error = error { + } else if let error = error { print("error identifying popups \(error)") } } lastPopupQueries.append(c) } - private func showPopups(_ popups: [AGSPopup]){ - - guard !popups.isEmpty else{ + private func showPopups(_ popups: [AGSPopup]) { + guard !popups.isEmpty else { return } - if let popupsViewController = self.popupsViewController{ + if let popupsViewController = self.popupsViewController { // If we already have a popupsViewController, then show additional popupsViewController.showAdditionalPopups(popups) return @@ -154,7 +145,7 @@ public class PopupController: NSObject, AGSPopupsViewControllerDelegate, AGSGeoV popupsViewController.customDoneButton = nil popupsViewController.delegate = self - if containerStyle == .navigationController{ + if containerStyle == .navigationController { // set a back button for the pvc in the nav controller, showing modally, this is handled for us // need to do this so we can clean up (unselect feature, etc) when `back` is tapped let doneViewingBbi = UIBarButtonItem(title: "Back", style: .plain, target: self, action: #selector(doneViewingInNavController)) @@ -162,24 +153,22 @@ public class PopupController: NSObject, AGSPopupsViewControllerDelegate, AGSGeoV popupsViewController.navigationItem.leftBarButtonItem = doneViewingBbi geoViewController?.navigationController?.pushViewController(popupsViewController, animated: true) - } - else{ + } else { geoViewController?.present(popupsViewController, animated: true) } - } - @objc private func doneViewingInNavController(){ + @objc + private func doneViewingInNavController() { guard let popupsViewController = popupsViewController else { return } popupsViewControllerDidFinishViewingPopups(popupsViewController) } - private func unselectLastSelectedFeature(){ - + private func unselectLastSelectedFeature() { guard let feature = lastSelectedFeature, - let layer = lastSelectedFeatureLayer else{ + let layer = lastSelectedFeatureLayer else { return } @@ -192,65 +181,56 @@ public class PopupController: NSObject, AGSPopupsViewControllerDelegate, AGSGeoV private var editingGeometry: Bool = false private func navigateToMapActionForGeometryEditing() { - editingGeometry = true - if let geoViewController = geoViewController, let nc = geoViewController.navigationController{ + if let geoViewController = geoViewController, let nc = geoViewController.navigationController { // if there is a navigationController available add button to go back to popups when done editing geometry geoViewControllerOriginalRightBarButtonItems = geoViewController.navigationItem.rightBarButtonItems let backToPvcButton = UIBarButtonItem(title: "Done", style: .done, target: self, action: #selector(navigateBackToPopupsFromGeometryEditing)) geoViewController.navigationItem.rightBarButtonItem = backToPvcButton - if useNavigationControllerIfAvailable{ + if useNavigationControllerIfAvailable { nc.popToViewController(geoViewController, animated: true) - } - else{ + } else { popupsViewController?.dismiss(animated: true) } - } - else{ + } else { // in this case developer needs to have a button that calls `navigateBackToPopupsFromGeometryEditing` popupsViewController?.dismiss(animated: true) } - } - @objc private func navigateBackToPopupsFromGeometryEditing(){ - - guard let popupsViewController = popupsViewController else{ + @objc + private func navigateBackToPopupsFromGeometryEditing() { + guard let popupsViewController = popupsViewController else { return } editingGeometry = false - if let geoViewController = geoViewController, let nc = geoViewController.navigationController{ + if let geoViewController = geoViewController, let nc = geoViewController.navigationController { // if there is a navigationController available reset to original buttons geoViewController.navigationItem.rightBarButtonItems = geoViewControllerOriginalRightBarButtonItems geoViewControllerOriginalRightBarButtonItems = nil - if useNavigationControllerIfAvailable{ + if useNavigationControllerIfAvailable { nc.pushViewController(popupsViewController, animated: true) - } - else{ + } else { geoViewController.present(popupsViewController, animated: true) } - } - else{ + } else { geoViewController?.present(popupsViewController, animated: true) } } public func popupsViewController(_ popupsViewController: AGSPopupsViewController, sketchEditorFor popup: AGSPopup) -> AGSSketchEditor? { - // give the popupsViewController the sketchEditor - if let g = popup.geoElement.geometry{ + if let g = popup.geoElement.geometry { self.sketchEditor.start(with: g) - } - else if let f = popup.geoElement as? AGSFeature, let ft = f.featureTable as? AGSArcGISFeatureTable{ + } else if let f = popup.geoElement as? AGSFeature, let ft = f.featureTable as? AGSArcGISFeatureTable { self.sketchEditor.start(with: ft.geometryType) - } - else{ + } else { self.sketchEditor.start(with: AGSSketchCreationMode.polygon) } @@ -263,10 +243,9 @@ public class PopupController: NSObject, AGSPopupsViewControllerDelegate, AGSGeoV } public func popupsViewController(_ popupsViewController: AGSPopupsViewController, didChangeToCurrentPopup popup: AGSPopup) { - guard let f = popup.geoElement as? AGSArcGISFeature, let ft = f.featureTable as? AGSServiceFeatureTable, - let fl = ft.featureLayer else{ + let fl = ft.featureLayer else { return } @@ -278,30 +257,25 @@ public class PopupController: NSObject, AGSPopupsViewControllerDelegate, AGSGeoV } public func popupsViewController(_ popupsViewController: AGSPopupsViewController, didFinishEditingFor popup: AGSPopup) { - // geometry editing has ended self.sketchEditor.stop() // apply edits for service feature table - if let f = popup.geoElement as? AGSArcGISFeature, let ft = f.featureTable as? AGSServiceFeatureTable{ + if let f = popup.geoElement as? AGSArcGISFeature, let ft = f.featureTable as? AGSServiceFeatureTable { ft.applyEdits { (results, error) in - - if let error = error{ + if let error = error { // In this case it is a service level error print("error applying edits: \(error)") } - if let results = results{ - - let editErrors = results.flatMap({ self.checkFeatureEditResult($0) }) - if editErrors.isEmpty{ + if let results = results { + let editErrors = results.flatMap { self.checkFeatureEditResult($0) } + if editErrors.isEmpty { print("applied all edits successfully") - } - else{ + } else { // These would be feature level edit errors print("apply edits failed: \(editErrors)") } - } } } @@ -311,12 +285,12 @@ public class PopupController: NSObject, AGSPopupsViewControllerDelegate, AGSGeoV } /// This pulls out any nested errors from a feature edit result - private func checkFeatureEditResult(_ featureEditResult: AGSFeatureEditResult) -> [Error]{ + private func checkFeatureEditResult(_ featureEditResult: AGSFeatureEditResult) -> [Error] { var errors = [Error]() - if let error = featureEditResult.error{ + if let error = featureEditResult.error { errors.append(error) } - errors.append(contentsOf: featureEditResult.attachmentResults.compactMap({ $0.error })) + errors.append(contentsOf: featureEditResult.attachmentResults.compactMap { $0.error }) return errors } @@ -324,7 +298,7 @@ public class PopupController: NSObject, AGSPopupsViewControllerDelegate, AGSGeoV // geometry editing has ended self.sketchEditor.stop() - if addingNewFeature{ + if addingNewFeature { // if was adding new feature, then hide the popup, don't show viewing mode self.cleanupLastPopupsViewController() } @@ -336,19 +310,16 @@ public class PopupController: NSObject, AGSPopupsViewControllerDelegate, AGSGeoV public func popupsViewControllerDidFinishViewingPopups(_ popupsViewController: AGSPopupsViewController) { self.cleanupLastPopupsViewController() } - } extension PopupController: TemplatePickerViewControllerDelegate { - public func templatePickerViewControllerDidCancel(_ templatePickerViewController: TemplatePickerViewController) { templatePickerViewController.dismiss(animated: true) } - public func templatePickerViewController(_ templatePickerViewController: TemplatePickerViewController, didSelect featureTemplateInfo: FeatureTemplateInfo){ - templatePickerViewController.dismiss(animated: true){ - - guard let feature = featureTemplateInfo.featureTable.createFeature(with: featureTemplateInfo.featureTemplate) else{ + public func templatePickerViewController(_ templatePickerViewController: TemplatePickerViewController, didSelect featureTemplateInfo: FeatureTemplateInfo) { + templatePickerViewController.dismiss(animated: true) { + guard let feature = featureTemplateInfo.featureTable.createFeature(with: featureTemplateInfo.featureTemplate) else { return } diff --git a/Toolkit/ArcGISToolkit/Scalebar.swift b/Toolkit/ArcGISToolkit/Scalebar.swift index 2e775ec5..092669ee 100644 --- a/Toolkit/ArcGISToolkit/Scalebar.swift +++ b/Toolkit/ArcGISToolkit/Scalebar.swift @@ -14,15 +14,15 @@ import UIKit import ArcGIS -public enum ScalebarUnits{ +public enum ScalebarUnits { case imperial case metric - internal func baseUnits()->AGSLinearUnit{ + internal func baseUnits() -> AGSLinearUnit { return self == .imperial ? AGSLinearUnit.feet() : AGSLinearUnit.meters() } - private static func multiplierAndMagnitudeForDistance(distance: Double) -> (multiplier: Double, magnitude: Double){ + private static func multiplierAndMagnitudeForDistance(distance: Double) -> (multiplier: Double, magnitude: Double) { // get multiplier let magnitude = pow(10, floor(log10(distance))) @@ -31,8 +31,7 @@ public enum ScalebarUnits{ return (multiplier, magnitude) } - internal func closestDistanceWithoutGoingOver(to distance: Double, units: AGSLinearUnit) -> Double{ - + internal func closestDistanceWithoutGoingOver(to distance: Double, units: AGSLinearUnit) -> Double { let mm = ScalebarUnits.multiplierAndMagnitudeForDistance(distance: distance) let roundNumber = mm.multiplier * mm.magnitude @@ -51,7 +50,9 @@ public enum ScalebarUnits{ // this table must begin with 1 and end with 10 private static let roundNumberMultipliers: [Double] = [1, 1.2, 1.25, 1.5, 1.75, 2, 2.4, 2.5, 3, 3.75, 4, 5, 6, 7.5, 8, 9, 10] - private static func segmentOptionsForMultiplier(multiplier: Double) -> [Int]{ + + // swiftlint:disable cyclomatic_complexity + private static func segmentOptionsForMultiplier(multiplier: Double) -> [Int] { switch multiplier { case 1: return [1, 2, 4, 5] @@ -91,9 +92,9 @@ public enum ScalebarUnits{ return [1] } } - - internal static func numSegmentsForDistance(distance: Double, maxNumSegments: Int) -> Int{ - + // swiftlint:enable cyclomatic_complexity + + internal static func numSegmentsForDistance(distance: Double, maxNumSegments: Int) -> Int { // this function returns the best number of segments so that we get relatively round // numbers when the distance is divided up. @@ -103,36 +104,33 @@ public enum ScalebarUnits{ return num } - internal func linearUnitsForDistance(distance: Double) -> AGSLinearUnit{ - + internal func linearUnitsForDistance(distance: Double) -> AGSLinearUnit { switch self { case .imperial: - if distance >= 2640{ + if distance >= 2640 { return AGSLinearUnit.miles() } return AGSLinearUnit.feet() case .metric: - if distance >= 1000{ + if distance >= 1000 { return AGSLinearUnit.kilometers() } return AGSLinearUnit.meters() } - } - } -public enum ScalebarStyle{ +public enum ScalebarStyle { case line case bar case graduatedLine case alternatingBar case dualUnitLine - fileprivate func rendererForScalebar(scalebar: Scalebar) -> ScalebarRenderer{ + fileprivate func rendererForScalebar(scalebar: Scalebar) -> ScalebarRenderer { switch self { case .line: return ScalebarLineStyleRenderer(scalebar: scalebar) @@ -148,85 +146,76 @@ public enum ScalebarStyle{ } } -public enum ScalebarAlignment{ +public enum ScalebarAlignment { case left case right case center } - public class Scalebar: UIView { - // // public properties - public var units: ScalebarUnits = .imperial{ - didSet{ + public var units: ScalebarUnits = .imperial { + didSet { updateScaleDisplay(forceRedraw: true) } } - public var style: ScalebarStyle = .line{ - didSet{ + public var style: ScalebarStyle = .line { + didSet { renderer = style.rendererForScalebar(scalebar: self) updateScaleDisplay(forceRedraw: true) } } - @IBInspectable - public var fillColor: UIColor? = UIColor.lightGray.withAlphaComponent(0.5){ - didSet{ + @IBInspectable public var fillColor: UIColor? = UIColor.lightGray.withAlphaComponent(0.5) { + didSet { setNeedsDisplay() } } - @IBInspectable - public var alternateFillColor: UIColor? = UIColor.black{ - didSet{ + @IBInspectable public var alternateFillColor: UIColor? = UIColor.black { + didSet { setNeedsDisplay() } } - @IBInspectable - public var lineColor: UIColor = UIColor.white{ - didSet{ + @IBInspectable public var lineColor: UIColor = UIColor.white { + didSet { setNeedsDisplay() } } - @IBInspectable - public var shadowColor: UIColor? = UIColor.black.withAlphaComponent(0.65){ - didSet{ + @IBInspectable public var shadowColor: UIColor? = UIColor.black.withAlphaComponent(0.65) { + didSet { setNeedsDisplay() } } - @IBInspectable - public var textColor: UIColor? = UIColor.black{ - didSet{ + @IBInspectable public var textColor: UIColor? = UIColor.black { + didSet { setNeedsDisplay() } } - @IBInspectable - public var textShadowColor: UIColor? = UIColor.white{ - didSet{ + @IBInspectable public var textShadowColor: UIColor? = UIColor.white { + didSet { setNeedsDisplay() } } // Set this to a value greater than 0 if you don't specify constraints for width and want to rely // on intrinsic content size for the width when using autolayout. Only applicable for autolayout. - @IBInspectable - public var maximumIntrinsicWidth: CGFloat = 0 { - didSet{ + @IBInspectable public var maximumIntrinsicWidth: CGFloat = 0 { + didSet { // this will invalidate the intrinsicContentSize and also redraw updateScaleDisplay(forceRedraw: true) } } - public var alignment: ScalebarAlignment = .left{ - didSet{ + public var alignment: ScalebarAlignment = .left { + didSet { updateScaleDisplay(forceRedraw: true) } } @@ -235,15 +224,15 @@ public class Scalebar: UIView { public var useGeodeticCalculations = true public var mapView: AGSMapView? { - didSet{ + didSet { unbindFromMapView(mapView: oldValue) bindToMapView(mapView: mapView) updateScaleDisplay(forceRedraw: true) } } - public var font: UIFont = UIFont.systemFont(ofSize: 9.0, weight: UIFont.Weight.semibold){ - didSet{ + public var font = UIFont.systemFont(ofSize: 9.0, weight: UIFont.Weight.semibold) { + didSet { recalculateFontProperties() updateScaleDisplay(forceRedraw: true) } @@ -259,9 +248,9 @@ public class Scalebar: UIView { internal static let labelYPad: CGFloat = 2.0 internal static let labelXPad: CGFloat = 4.0 - internal static let tickHeight: CGFloat = 6.0 - internal static let tick2Height: CGFloat = 4.5 - internal static let notchHeight: CGFloat = 6.0 + internal static let tickHeight: CGFloat = 6.0 + internal static let tick2Height: CGFloat = 4.5 + internal static let notchHeight: CGFloat = 6.0 internal static var numberFormatter: NumberFormatter = { let numberFormatter = NumberFormatter() numberFormatter.numberStyle = .decimal @@ -271,16 +260,14 @@ public class Scalebar: UIView { return numberFormatter }() - internal static let showFrameDebugColors = false - internal static let lineCap: CGLineCap = CGLineCap.round + internal static let lineCap = CGLineCap.round internal var fontHeight: CGFloat = 0 internal var zeroStringWidth: CGFloat = 0 internal var maxRightUnitsPad: CGFloat = 0 - private func recalculateFontProperties(){ - + private func recalculateFontProperties() { let attributes: [NSAttributedString.Key: Any] = [.font: font] let zeroText = "0" @@ -298,7 +285,7 @@ public class Scalebar: UIView { // accurate for the center of the map on smaller scales (when zoomed way out). // A minScale of 0 means it will always be visible private let minScale: Double = 0 - private var updateCoalescer: Coalescer? = nil + private var updateCoalescer: Coalescer? private var renderer: ScalebarRenderer? @@ -311,12 +298,12 @@ public class Scalebar: UIView { sharedInitialization() } - required public init?(coder aDecoder: NSCoder) { + public required init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) sharedInitialization() } - required public init(mapView: AGSMapView){ + public required init(mapView: AGSMapView) { super.init(frame: CGRect.zero) sharedInitialization() self.mapView = mapView @@ -324,8 +311,7 @@ public class Scalebar: UIView { bindToMapView(mapView: mapView) } - private func sharedInitialization(){ - + private func sharedInitialization() { self.updateCoalescer = Coalescer(dispatchQueue: DispatchQueue.main, interval: DispatchTimeInterval.milliseconds(500), action: updateScaleDisplayIfNecessary) self.isUserInteractionEnabled = false @@ -340,18 +326,18 @@ public class Scalebar: UIView { private var mapObservation: NSKeyValueObservation? private var visibleAreaObservation: NSKeyValueObservation? - private func bindToMapView(mapView: AGSMapView?){ - mapObservation = mapView?.observe(\.map, options: .new){[weak self] mapView, change in + private func bindToMapView(mapView: AGSMapView?) { + mapObservation = mapView?.observe(\.map, options: .new) {[weak self] _, _ in self?.updateScaleDisplay(forceRedraw: false) } - visibleAreaObservation = mapView?.observe(\.visibleArea, options: .new){ [weak self] mapView, change in + visibleAreaObservation = mapView?.observe(\.visibleArea, options: .new) { [weak self] _, _ in // since we get updates so often, we don't need to redraw that often // so use the coalescer to filter the events on a time interval self?.updateCoalescer?.ping() } } - private func unbindFromMapView(mapView: AGSMapView?){ + private func unbindFromMapView(mapView: AGSMapView?) { // invalidate observations and set to nil mapObservation?.invalidate() mapObservation = nil @@ -359,37 +345,37 @@ public class Scalebar: UIView { visibleAreaObservation = nil } - private func updateScaleDisplayIfNecessary(){ + private func updateScaleDisplayIfNecessary() { updateScaleDisplay(forceRedraw: false) } - private func updateScaleDisplay(forceRedraw: Bool){ - - guard var renderer = renderer else{ + // swiftlint:disable cyclomatic_complexity + private func updateScaleDisplay(forceRedraw: Bool) { + guard var renderer = renderer else { // this should never happen, should always have a renderer setNeedsDisplay() return } - guard let mapView = mapView else{ + guard let mapView = mapView else { renderer.currentScaleDisplay = nil setNeedsDisplay() return } - guard mapView.map != nil else{ + guard mapView.map != nil else { renderer.currentScaleDisplay = nil setNeedsDisplay() return } - guard let sr = mapView.spatialReference else{ + guard let sr = mapView.spatialReference else { renderer.currentScaleDisplay = nil setNeedsDisplay() return } - guard let visibleArea = mapView.visibleArea else{ + guard let visibleArea = mapView.visibleArea else { renderer.currentScaleDisplay = nil setNeedsDisplay() return @@ -397,7 +383,7 @@ public class Scalebar: UIView { //print("current scale: \(mapView.mapScale)") - guard minScale <= 0 || mapView.mapScale < minScale else{ + guard minScale <= 0 || mapView.mapScale < minScale else { //print("current scale: \(mapView.mapScale), minScale \(minScale)") renderer.currentScaleDisplay = nil setNeedsDisplay() @@ -416,19 +402,18 @@ public class Scalebar: UIView { let lineDisplayLength: CGFloat // bail early if we can because the last time we drew was good - if let csd = renderer.currentScaleDisplay, forceRedraw == false{ + if let csd = renderer.currentScaleDisplay, forceRedraw == false { var needsRedraw = false - if csd.mapScale != mapScale{ needsRedraw = true } + if csd.mapScale != mapScale { needsRedraw = true } let dependsOnMapCenter = sr.unit is AGSAngularUnit || useGeodeticCalculations - if dependsOnMapCenter && !mapCenter.isEqual(to: csd.mapCenter){ needsRedraw = true } - if !needsRedraw{ + if dependsOnMapCenter && !mapCenter.isEqual(to: csd.mapCenter) { needsRedraw = true } + if !needsRedraw { // no need to redraw - nothing significant changed return } } if useGeodeticCalculations || sr.unit is AGSAngularUnit { - let maxLengthPlanar = unitsPerPoint * Double(maxLength) let p1 = AGSPoint(x: mapCenter.x - (maxLengthPlanar * 0.5), y: mapCenter.y, spatialReference: sr) let p2 = AGSPoint(x: mapCenter.x + (maxLengthPlanar * 0.5), y: mapCenter.y, spatialReference: sr) @@ -440,10 +425,8 @@ public class Scalebar: UIView { lineDisplayLength = CGFloat( (roundNumberDistance * planarToGeodeticFactor) / unitsPerPoint ) displayUnit = units.linearUnitsForDistance(distance: roundNumberDistance) lineMapLength = baseUnits.convert(roundNumberDistance, to: displayUnit) - } - else { - - guard let srUnit = sr.unit as? AGSLinearUnit else{ + } else { + guard let srUnit = sr.unit as? AGSLinearUnit else { renderer.currentScaleDisplay = nil setNeedsDisplay() return @@ -458,7 +441,7 @@ public class Scalebar: UIView { lineMapLength = baseUnits.convert(closestLen, to: displayUnit) } - guard lineDisplayLength.isFinite, !lineDisplayLength.isNaN else{ + guard lineDisplayLength.isFinite, !lineDisplayLength.isNaN else { renderer.currentScaleDisplay = nil setNeedsDisplay() return @@ -475,25 +458,21 @@ public class Scalebar: UIView { // tell view we need to redraw setNeedsDisplay() } - - public override var intrinsicContentSize: CGSize{ - get{ - if let renderer = renderer { - if maximumIntrinsicWidth > 0{ - return CGSize(width: renderer.currentMaxDisplayWidth, height: renderer.displayHeight) - } - else{ - return CGSize(width: UIView.noIntrinsicMetric, height: renderer.displayHeight) - } - } - else{ - return CGSize(width: UIView.noIntrinsicMetric, height: UIView.noIntrinsicMetric) + // swiftlint:enable cyclomatic_complexity + + override public var intrinsicContentSize: CGSize { + if let renderer = renderer { + if maximumIntrinsicWidth > 0 { + return CGSize(width: renderer.currentMaxDisplayWidth, height: renderer.displayHeight) + } else { + return CGSize(width: UIView.noIntrinsicMetric, height: renderer.displayHeight) } + } else { + return CGSize(width: UIView.noIntrinsicMetric, height: UIView.noIntrinsicMetric) } } - private func offsetRectForDisplaySize(displaySize: CGSize) -> CGRect{ - + private func offsetRectForDisplaySize(displaySize: CGSize) -> CGRect { // center on y axis let offsetY = (bounds.height - displaySize.height) / 2 @@ -512,10 +491,9 @@ public class Scalebar: UIView { } override public func draw(_ rect: CGRect) { - super.draw(rect) - guard let renderer = self.renderer, renderer.currentScaleDisplay != nil else{ + guard let renderer = self.renderer, renderer.currentScaleDisplay != nil else { return } @@ -523,11 +501,11 @@ public class Scalebar: UIView { let odr = offsetRectForDisplaySize(displaySize: displaySize) - guard !odr.isEmpty else{ + guard !odr.isEmpty else { return } - if Scalebar.showFrameDebugColors, let context = UIGraphicsGetCurrentContext(){ + if Scalebar.showFrameDebugColors, let context = UIGraphicsGetCurrentContext() { context.saveGState() context.setFillColor(UIColor.yellow.cgColor) @@ -543,19 +521,17 @@ public class Scalebar: UIView { renderer.draw(rect: odr) } - private func calculateDisplaySize() -> CGSize{ + private func calculateDisplaySize() -> CGSize { if let renderer = renderer { let displaySize = CGSize(width: renderer.currentMaxDisplayWidth, height: renderer.displayHeight) return displaySize - } - else{ + } else { return CGSize(width: UIView.noIntrinsicMetric, height: UIView.noIntrinsicMetric) } } } - -internal struct ScaleDisplay{ +internal struct ScaleDisplay { var mapScale: Double = 0 var unitsPerPoint: Double = 0 var lineMapLength: Double = 0 @@ -565,7 +541,7 @@ internal struct ScaleDisplay{ var mapLengthString: String } -internal struct SegmentInfo{ +internal struct SegmentInfo { var index: Int var segmentScreenLength: CGFloat var xOffset: CGFloat @@ -574,11 +550,10 @@ internal struct SegmentInfo{ var textWidth: CGFloat } -internal protocol ScalebarRenderer{ - - var scalebar: Scalebar? {get} +internal protocol ScalebarRenderer { + var scalebar: Scalebar? { get } var currentScaleDisplay: ScaleDisplay? { get set } - var displayHeight: CGFloat {get} + var displayHeight: CGFloat { get } var currentMaxDisplayWidth: CGFloat { get } init(scalebar: Scalebar) @@ -587,27 +562,21 @@ internal protocol ScalebarRenderer{ func draw(rect: CGRect) } -internal extension ScalebarRenderer{ - - var shadowOffset: CGPoint{ +internal extension ScalebarRenderer { + var shadowOffset: CGPoint { return CGPoint(x: 0.5, y: 0.5) } var lineWidth: CGFloat { - get { - return 2 - } + return 2 } var halfLineWidth: CGFloat { - get{ - return 1 - } + return 1 } - - func calculateSegmentInfos() -> [SegmentInfo]?{ - - guard let scaleDisplay = currentScaleDisplay, let scalebar = scalebar else{ + + func calculateSegmentInfos() -> [SegmentInfo]? { + guard let scaleDisplay = currentScaleDisplay, let scalebar = scalebar else { return nil } @@ -619,7 +588,7 @@ internal extension ScalebarRenderer{ let minSegmentTestString = (scaleDisplay.mapLengthString.count > 3) ? scaleDisplay.mapLengthString : "9.9" // use 1.5 because the last segment, the text is right justified insted of center, which makes it harder to squeeze text in let minSegmentWidth = (minSegmentTestString.size(withAttributes: [.font: scalebar.font]).width * 1.5) + (Scalebar.labelXPad * 2) - var maxNumSegments: Int = Int(lineDisplayLength / minSegmentWidth) + var maxNumSegments = Int(lineDisplayLength / minSegmentWidth) maxNumSegments = min(maxNumSegments, 4) // cap it at 4 let numSegments: Int = ScalebarUnits.numSegmentsForDistance(distance: scaleDisplay.lineMapLength, maxNumSegments: maxNumSegments) @@ -630,7 +599,6 @@ internal extension ScalebarRenderer{ var segmentInfos = [SegmentInfo]() for index in 0.. CGFloat{ + func availableLineDisplayLength(totalDisplayWidth: CGFloat) -> CGFloat { return totalDisplayWidth - lineWidth } - func draw(rect: CGRect){ - - guard let scaleDisplay = currentScaleDisplay else{ + func draw(rect: CGRect) { + guard let scaleDisplay = currentScaleDisplay else { return } - guard let scalebar = self.scalebar else{ + guard let scalebar = self.scalebar else { return } - guard let context = UIGraphicsGetCurrentContext() else{ + guard let context = UIGraphicsGetCurrentContext() else { return } @@ -803,20 +758,19 @@ internal class ScalebarLineStyleRenderer: ScalebarRenderer{ let lineBottom = lineTop + Scalebar.tickHeight path.move(to: CGPoint(x: lineX, y: lineTop)) - path.addLine(to: CGPoint(x: lineX, y:lineBottom)) + path.addLine(to: CGPoint(x: lineX, y: lineBottom)) path.addLine(to: CGPoint(x: lineX + lineScreenLength, y: lineBottom)) path.addLine(to: CGPoint(x: lineX + lineScreenLength, y: lineTop)) - // // draw paths context.setLineCap(Scalebar.lineCap) context.setLineJoin(CGLineJoin.bevel) - if let shadowColor = scalebar.shadowColor{ + if let shadowColor = scalebar.shadowColor { var t = CGAffineTransform(translationX: shadowOffset.x, y: shadowOffset.y) - if let shadowPath = path.copy(using: &t){ + if let shadowPath = path.copy(using: &t) { context.setLineWidth(lineWidth) context.setStrokeColor(shadowColor.cgColor) context.addPath(shadowPath) @@ -845,8 +799,7 @@ internal class ScalebarLineStyleRenderer: ScalebarRenderer{ } } -internal class ScalebarGraduatedLineStyleRenderer: ScalebarRenderer{ - +internal class ScalebarGraduatedLineStyleRenderer: ScalebarRenderer { weak var scalebar: Scalebar? required init(scalebar: Scalebar) { @@ -856,41 +809,36 @@ internal class ScalebarGraduatedLineStyleRenderer: ScalebarRenderer{ var currentScaleDisplay: ScaleDisplay? var displayHeight: CGFloat { - get{ - guard let scalebar = scalebar else { return 0 } - return halfLineWidth + Scalebar.tickHeight + Scalebar.labelYPad + scalebar.fontHeight + shadowOffset.y - } + guard let scalebar = scalebar else { return 0 } + return halfLineWidth + Scalebar.tickHeight + Scalebar.labelYPad + scalebar.fontHeight + shadowOffset.y } var currentMaxDisplayWidth: CGFloat { - get{ - guard let scalebar = scalebar, let scaleDisplay = currentScaleDisplay else { - return 0 - } - return halfLineWidth + scaleDisplay.lineDisplayLength + halfLineWidth + scalebar.maxRightUnitsPad + shadowOffset.x + guard let scalebar = scalebar, let scaleDisplay = currentScaleDisplay else { + return 0 } + return halfLineWidth + scaleDisplay.lineDisplayLength + halfLineWidth + scalebar.maxRightUnitsPad + shadowOffset.x } - func availableLineDisplayLength(totalDisplayWidth: CGFloat) -> CGFloat{ + func availableLineDisplayLength(totalDisplayWidth: CGFloat) -> CGFloat { guard let scalebar = scalebar else { return 0 } return totalDisplayWidth - halfLineWidth - scalebar.maxRightUnitsPad } - func draw(rect: CGRect){ - - guard let scaleDisplay = currentScaleDisplay else{ + func draw(rect: CGRect) { + guard let scaleDisplay = currentScaleDisplay else { return } - guard let scalebar = self.scalebar else{ + guard let scalebar = self.scalebar else { return } - guard let segmentInfos = calculateSegmentInfos() else{ + guard let segmentInfos = calculateSegmentInfos() else { return } - guard let context = UIGraphicsGetCurrentContext() else{ + guard let context = UIGraphicsGetCurrentContext() else { return } @@ -916,14 +864,13 @@ internal class ScalebarGraduatedLineStyleRenderer: ScalebarRenderer{ let lineX = x + halfLineWidth path.move(to: CGPoint(x: lineX, y: lineTop)) - path.addLine(to: CGPoint(x: lineX, y:lineBottom)) + path.addLine(to: CGPoint(x: lineX, y: lineBottom)) path.addLine(to: CGPoint(x: lineX + lineScreenLength, y: lineBottom)) path.addLine(to: CGPoint(x: lineX + lineScreenLength, y: lineTop)) - // draw segment ticks - for si in segmentInfos{ - if si.index == segmentInfos.last?.index{ + for si in segmentInfos { + if si.index == segmentInfos.last?.index { // skip last segment continue } @@ -938,9 +885,9 @@ internal class ScalebarGraduatedLineStyleRenderer: ScalebarRenderer{ context.setLineCap(Scalebar.lineCap) context.setLineJoin(CGLineJoin.bevel) - if let shadowColor = scalebar.shadowColor{ + if let shadowColor = scalebar.shadowColor { var t = CGAffineTransform(translationX: shadowOffset.x, y: shadowOffset.y) - if let shadowPath = path.copy(using: &t){ + if let shadowPath = path.copy(using: &t) { context.setLineWidth(lineWidth) context.setStrokeColor(shadowColor.cgColor) context.addPath(shadowPath) @@ -960,14 +907,12 @@ internal class ScalebarGraduatedLineStyleRenderer: ScalebarRenderer{ let textY = lineBottom + Scalebar.labelYPad drawSegmentsText(segmentInfos: segmentInfos, scaleDisplay: scaleDisplay, startingX: lineX, textY: textY) - // reset the state context.restoreGState() } } -internal class ScalebarBarStyleRenderer: ScalebarRenderer{ - +internal class ScalebarBarStyleRenderer: ScalebarRenderer { weak var scalebar: Scalebar? required init(scalebar: Scalebar) { @@ -977,36 +922,31 @@ internal class ScalebarBarStyleRenderer: ScalebarRenderer{ var currentScaleDisplay: ScaleDisplay? var displayHeight: CGFloat { - get{ - guard let scalebar = scalebar else { return 0 } - return Scalebar.notchHeight + Scalebar.labelYPad + scalebar.fontHeight + shadowOffset.y - } + guard let scalebar = scalebar else { return 0 } + return Scalebar.notchHeight + Scalebar.labelYPad + scalebar.fontHeight + shadowOffset.y } var currentMaxDisplayWidth: CGFloat { - get{ - guard let scaleDisplay = currentScaleDisplay else{ - return 0 - } - return halfLineWidth + scaleDisplay.lineDisplayLength + halfLineWidth + shadowOffset.x + guard let scaleDisplay = currentScaleDisplay else { + return 0 } + return halfLineWidth + scaleDisplay.lineDisplayLength + halfLineWidth + shadowOffset.x } - func availableLineDisplayLength(totalDisplayWidth: CGFloat) -> CGFloat{ + func availableLineDisplayLength(totalDisplayWidth: CGFloat) -> CGFloat { return totalDisplayWidth - lineWidth } - func draw(rect: CGRect){ - - guard let scaleDisplay = currentScaleDisplay else{ + func draw(rect: CGRect) { + guard let scaleDisplay = currentScaleDisplay else { return } - guard let scalebar = self.scalebar else{ + guard let scalebar = self.scalebar else { return } - guard let context = UIGraphicsGetCurrentContext() else{ + guard let context = UIGraphicsGetCurrentContext() else { return } @@ -1020,7 +960,6 @@ internal class ScalebarBarStyleRenderer: ScalebarRenderer{ let path = CGMutablePath() - // set path for bar style /* =================== @@ -1043,9 +982,9 @@ internal class ScalebarBarStyleRenderer: ScalebarRenderer{ context.setLineCap(Scalebar.lineCap) context.setLineJoin(CGLineJoin.bevel) - if let shadowColor = scalebar.shadowColor{ + if let shadowColor = scalebar.shadowColor { var t = CGAffineTransform(translationX: shadowOffset.x, y: shadowOffset.y) - if let shadowPath = path.copy(using: &t){ + if let shadowPath = path.copy(using: &t) { context.setLineWidth(lineWidth) context.setStrokeColor(shadowColor.cgColor) context.addPath(shadowPath) @@ -1053,7 +992,7 @@ internal class ScalebarBarStyleRenderer: ScalebarRenderer{ } } - if let fillColor = scalebar.fillColor{ + if let fillColor = scalebar.fillColor { context.setFillColor(fillColor.cgColor) context.addPath(path) context.drawPath(using: .fill) @@ -1080,8 +1019,7 @@ internal class ScalebarBarStyleRenderer: ScalebarRenderer{ } } -internal class ScalebarAlternatingBarStyleRenderer: ScalebarRenderer{ - +internal class ScalebarAlternatingBarStyleRenderer: ScalebarRenderer { weak var scalebar: Scalebar? required init(scalebar: Scalebar) { @@ -1091,44 +1029,40 @@ internal class ScalebarAlternatingBarStyleRenderer: ScalebarRenderer{ var currentScaleDisplay: ScaleDisplay? var displayHeight: CGFloat { - get{ - guard let scalebar = scalebar else { return 0 } - return halfLineWidth + Scalebar.notchHeight + Scalebar.labelYPad + scalebar.fontHeight + shadowOffset.y - } + guard let scalebar = scalebar else { return 0 } + return halfLineWidth + Scalebar.notchHeight + Scalebar.labelYPad + scalebar.fontHeight + shadowOffset.y } var currentMaxDisplayWidth: CGFloat { - get{ - guard let scalebar = scalebar, let scaleDisplay = currentScaleDisplay else { - return 0 - } - return halfLineWidth + scaleDisplay.lineDisplayLength + halfLineWidth + scalebar.maxRightUnitsPad + shadowOffset.x + guard let scalebar = scalebar, let scaleDisplay = currentScaleDisplay else { + return 0 } + return halfLineWidth + scaleDisplay.lineDisplayLength + halfLineWidth + scalebar.maxRightUnitsPad + shadowOffset.x } // can change this if you want to see quarter graduation private let showQuarters = false - func availableLineDisplayLength(totalDisplayWidth: CGFloat) -> CGFloat{ + func availableLineDisplayLength(totalDisplayWidth: CGFloat) -> CGFloat { guard let scalebar = scalebar else { return 0 } return totalDisplayWidth - halfLineWidth - scalebar.maxRightUnitsPad } - func draw(rect: CGRect){ - - guard let scaleDisplay = currentScaleDisplay else{ + // swiftlint:disable cyclomatic_complexity + func draw(rect: CGRect) { + guard let scaleDisplay = currentScaleDisplay else { return } - guard let scalebar = self.scalebar else{ + guard let scalebar = self.scalebar else { return } - guard let segmentInfos = calculateSegmentInfos() else{ + guard let segmentInfos = calculateSegmentInfos() else { return } - guard let context = UIGraphicsGetCurrentContext() else{ + guard let context = UIGraphicsGetCurrentContext() else { return } @@ -1142,14 +1076,12 @@ internal class ScalebarAlternatingBarStyleRenderer: ScalebarRenderer{ let pathStroke = CGMutablePath() - // set path for bar style /* =========~~~~~~~~~~ 0 100 200km */ - let lineTop = y + halfLineWidth let lineBottom = lineTop + Scalebar.notchHeight let lineX = x + halfLineWidth @@ -1164,8 +1096,8 @@ internal class ScalebarAlternatingBarStyleRenderer: ScalebarRenderer{ pathStroke.closeSubpath() // add all segment ticks - for si in segmentInfos{ - if si.index == segmentInfos.last?.index{ + for si in segmentInfos { + if si.index == segmentInfos.last?.index { // skip last segment continue } @@ -1181,8 +1113,7 @@ internal class ScalebarAlternatingBarStyleRenderer: ScalebarRenderer{ var lastPathX = lineX - for si in segmentInfos{ - + for si in segmentInfos { let fillPath = (si.index % 2) == 0 ? fillPath2 : fillPath1 let pathX = lineX + si.xOffset @@ -1196,7 +1127,6 @@ internal class ScalebarAlternatingBarStyleRenderer: ScalebarRenderer{ lastPathX = pathX } - // // draw paths @@ -1204,9 +1134,9 @@ internal class ScalebarAlternatingBarStyleRenderer: ScalebarRenderer{ context.setLineJoin(CGLineJoin.bevel) // stroke shadow - if let shadowColor = scalebar.shadowColor{ + if let shadowColor = scalebar.shadowColor { var t = CGAffineTransform(translationX: shadowOffset.x, y: shadowOffset.y) - if let shadowPath = pathStroke.copy(using: &t){ + if let shadowPath = pathStroke.copy(using: &t) { context.setLineWidth(lineWidth) context.setStrokeColor(shadowColor.cgColor) context.addPath(shadowPath) @@ -1215,14 +1145,14 @@ internal class ScalebarAlternatingBarStyleRenderer: ScalebarRenderer{ } // fill in odd segments - if let fillColor = scalebar.fillColor{ + if let fillColor = scalebar.fillColor { context.setFillColor(fillColor.cgColor) context.addPath(fillPath1) context.drawPath(using: .fill) } // fill in even segments - if let alternateFillColor = scalebar.alternateFillColor{ + if let alternateFillColor = scalebar.alternateFillColor { context.setFillColor(alternateFillColor.cgColor) context.addPath(fillPath2) context.drawPath(using: .fill) @@ -1243,12 +1173,10 @@ internal class ScalebarAlternatingBarStyleRenderer: ScalebarRenderer{ // reset the state context.restoreGState() } - + // swiftlint:enable cyclomatic_complexity } - -internal class ScalebarDualUnitLineStyleRenderer: ScalebarRenderer{ - +internal class ScalebarDualUnitLineStyleRenderer: ScalebarRenderer { weak var scalebar: Scalebar? required init(scalebar: Scalebar) { @@ -1258,37 +1186,32 @@ internal class ScalebarDualUnitLineStyleRenderer: ScalebarRenderer{ var currentScaleDisplay: ScaleDisplay? var displayHeight: CGFloat { - get{ - guard let scalebar = scalebar else { return 0 } - return scalebar.fontHeight + Scalebar.labelYPad + Scalebar.tick2Height + Scalebar.tick2Height + Scalebar.labelYPad + scalebar.fontHeight + shadowOffset.y - } + guard let scalebar = scalebar else { return 0 } + return scalebar.fontHeight + Scalebar.labelYPad + Scalebar.tick2Height + Scalebar.tick2Height + Scalebar.labelYPad + scalebar.fontHeight + shadowOffset.y } var currentMaxDisplayWidth: CGFloat { - get{ - guard let scalebar = scalebar, let scaleDisplay = currentScaleDisplay else { - return 0 - } - return halfLineWidth + scaleDisplay.lineDisplayLength + halfLineWidth + scalebar.maxRightUnitsPad + shadowOffset.x + guard let scalebar = scalebar, let scaleDisplay = currentScaleDisplay else { + return 0 } + return halfLineWidth + scaleDisplay.lineDisplayLength + halfLineWidth + scalebar.maxRightUnitsPad + shadowOffset.x } - func availableLineDisplayLength(totalDisplayWidth: CGFloat) -> CGFloat{ + func availableLineDisplayLength(totalDisplayWidth: CGFloat) -> CGFloat { guard let scalebar = scalebar else { return 0 } return totalDisplayWidth - halfLineWidth - scalebar.maxRightUnitsPad } - func draw(rect: CGRect){ - - guard let scaleDisplay = currentScaleDisplay else{ + func draw(rect: CGRect) { + guard let scaleDisplay = currentScaleDisplay else { return } - guard let scalebar = self.scalebar else{ + guard let scalebar = self.scalebar else { return } - guard let context = UIGraphicsGetCurrentContext() else{ + guard let context = UIGraphicsGetCurrentContext() else { return } @@ -1319,7 +1242,7 @@ internal class ScalebarDualUnitLineStyleRenderer: ScalebarRenderer{ // top unit line path.move(to: CGPoint(x: lineX, y: lineTop)) path.addLine(to: CGPoint(x: lineX, y: lineBottom)) - path.move(to: CGPoint(x: lineX, y:lineY)) + path.move(to: CGPoint(x: lineX, y: lineY)) path.addLine(to: CGPoint(x: lineX + lineScreenLength, y: lineY)) path.addLine(to: CGPoint(x: lineX + lineScreenLength, y: lineTop)) @@ -1343,9 +1266,9 @@ internal class ScalebarDualUnitLineStyleRenderer: ScalebarRenderer{ context.setLineCap(Scalebar.lineCap) context.setLineJoin(CGLineJoin.bevel) - if let shadowColor = scalebar.shadowColor{ + if let shadowColor = scalebar.shadowColor { var t = CGAffineTransform(translationX: shadowOffset.x, y: shadowOffset.y) - if let shadowPath = path.copy(using: &t){ + if let shadowPath = path.copy(using: &t) { context.setLineWidth(lineWidth) context.setStrokeColor(shadowColor.cgColor) context.addPath(shadowPath) @@ -1376,8 +1299,7 @@ internal class ScalebarDualUnitLineStyleRenderer: ScalebarRenderer{ self.drawText(text: topText, frame: topTextFrame, alignment: .right) // draw bottom text - if let numberString = Scalebar.numberFormatter.string(from: NSNumber(value: otherLineMapLength)){ - + if let numberString = Scalebar.numberFormatter.string(from: NSNumber(value: otherLineMapLength)) { let bottomUnitsText = " \(otherDisplayUnits.abbreviation)" let bottomUnitsTextWidth = bottomUnitsText.size(withAttributes: [.font: scalebar.font]).width @@ -1397,9 +1319,3 @@ internal class ScalebarDualUnitLineStyleRenderer: ScalebarRenderer{ context.restoreGState() } } - - - - - - diff --git a/Toolkit/ArcGISToolkit/TableViewController.swift b/Toolkit/ArcGISToolkit/TableViewController.swift index ff5c7031..c3f3f99a 100644 --- a/Toolkit/ArcGISToolkit/TableViewController.swift +++ b/Toolkit/ArcGISToolkit/TableViewController.swift @@ -14,7 +14,6 @@ import UIKit open class TableViewController: UIViewController, UITableViewDataSource, UITableViewDelegate { - public var cellReuseIdentifier = "cell" public var tableView = UITableView(frame: .zero) @@ -29,7 +28,7 @@ open class TableViewController: UIViewController, UITableViewDataSource, UITable tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor), tableView.topAnchor.constraint(equalTo: view.topAnchor), tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor) - ]) + ]) tableView.delegate = self tableView.dataSource = self @@ -44,13 +43,12 @@ open class TableViewController: UIViewController, UITableViewDataSource, UITable return UITableViewCell() } - public func goBack(_ completion: (()->Void)? ){ - if let nc = navigationController{ + public func goBack(_ completion: (() -> Void)? ) { + if let nc = navigationController { nc.popViewController(animated: true) completion?() - } - else{ - self.dismiss(animated: true){ + } else { + self.dismiss(animated: true) { completion?() } } diff --git a/Toolkit/ArcGISToolkit/TemplatePickerViewController.swift b/Toolkit/ArcGISToolkit/TemplatePickerViewController.swift index 13ff129e..2b463def 100644 --- a/Toolkit/ArcGISToolkit/TemplatePickerViewController.swift +++ b/Toolkit/ArcGISToolkit/TemplatePickerViewController.swift @@ -14,7 +14,7 @@ import ArcGIS /// An object that encapsulates information related to a feature template -public class FeatureTemplateInfo{ +public class FeatureTemplateInfo { /// The feature layer that the template is from public let featureLayer: AGSFeatureLayer /// The feature table that the template is from @@ -24,7 +24,7 @@ public class FeatureTemplateInfo{ /// The swatch for the feature template public var swatch: UIImage? - fileprivate init(featureLayer: AGSFeatureLayer, featureTable: AGSArcGISFeatureTable, featureTemplate: AGSFeatureTemplate, swatch: UIImage? = nil){ + fileprivate init(featureLayer: AGSFeatureLayer, featureTable: AGSArcGISFeatureTable, featureTemplate: AGSFeatureTemplate, swatch: UIImage? = nil) { self.featureLayer = featureLayer self.featureTable = featureTable self.featureTemplate = featureTemplate @@ -51,7 +51,6 @@ public protocol TemplatePickerViewControllerDelegate: AnyObject { /// and allowing them to choose one. /// This view controller is meant to be embedded in a navigation controller. public class TemplatePickerViewController: TableViewController { - /// The map which this view controller will display the feature templates from public let map: AGSMap? @@ -59,21 +58,21 @@ public class TemplatePickerViewController: TableViewController { private var currentDatasource = [String: [FeatureTemplateInfo]]() private var isFiltering: Bool = false private var unfilteredInfos = [FeatureTemplateInfo]() - private var currentInfos = [FeatureTemplateInfo](){ - didSet{ - tables = Set(self.currentInfos.map { $0.featureTable }).sorted(by: {$0.tableName < $1.tableName}) - currentDatasource = Dictionary(grouping: currentInfos, by: { $0.featureTable.tableName }) + private var currentInfos = [FeatureTemplateInfo]() { + didSet { + tables = Set(self.currentInfos.map { $0.featureTable }).sorted { $0.tableName < $1.tableName } + currentDatasource = Dictionary(grouping: currentInfos) { $0.featureTable.tableName } self.tableView.reloadData() } } /// Initializes a `TemplatePickerViewController` with a map. - public init(map: AGSMap){ + public init(map: AGSMap) { self.map = map super.init(nibName: nil, bundle: nil) } - required public init?(coder aDecoder: NSCoder) { + public required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } @@ -92,7 +91,7 @@ public class TemplatePickerViewController: TableViewController { navigationItem.leftBarButtonItem = UIBarButtonItem(barButtonSystemItem: .cancel, target: self, action: #selector(TemplatePickerViewController.cancelAction)) // get the templates from the map and load them as the datasource - if let map = map{ + if let map = map { getTemplateInfos(map: map, completion: loadInfosAndCreateSwatches) } } @@ -112,58 +111,53 @@ public class TemplatePickerViewController: TableViewController { } /// Gets the templates out of a map. - private func getTemplateInfos(map: AGSMap, completion: @escaping (([FeatureTemplateInfo])->Void) ){ - - map.load{ [weak self] error in - + private func getTemplateInfos(map: AGSMap, completion: @escaping (([FeatureTemplateInfo]) -> Void) ) { + map.load { [weak self] error in guard let self = self else { return } - guard error == nil else{ return } + guard error == nil else { return } - let allLayers : [AGSLayer] = (map.operationalLayers as Array + map.basemap.baseLayers as Array + map.basemap.referenceLayers as Array) as! [AGSLayer] + let allLayers: [AGSLayer] = (map.operationalLayers as Array + map.basemap.baseLayers as Array + map.basemap.referenceLayers as Array) as! [AGSLayer] let featureLayers = allLayers - .compactMap({ $0 as? AGSFeatureLayer }) - .filter({ $0.featureTable is AGSArcGISFeatureTable }) + .compactMap { $0 as? AGSFeatureLayer } + .filter { $0.featureTable is AGSArcGISFeatureTable } - AGSLoadObjects(featureLayers){ [weak self] _ in + AGSLoadObjects(featureLayers) { [weak self] _ in guard let self = self else { return } - let templates = featureLayers.flatMap({ return self.getTemplateInfos(featureLayer: $0) }) + let templates = featureLayers.flatMap { return self.getTemplateInfos(featureLayer: $0) } completion(templates) } } - } /// Gets the templates out of a feature layer and associated table. /// This should only be called once the feature layer is loaded. - private func getTemplateInfos(featureLayer: AGSFeatureLayer) -> [FeatureTemplateInfo]{ - - guard let table = featureLayer.featureTable as? AGSArcGISFeatureTable else{ + private func getTemplateInfos(featureLayer: AGSFeatureLayer) -> [FeatureTemplateInfo] { + guard let table = featureLayer.featureTable as? AGSArcGISFeatureTable else { return [] } - guard let popupDef = featureLayer.popupDefinition, popupDef.allowEdit || table.canAddFeature else{ + guard let popupDef = featureLayer.popupDefinition, popupDef.allowEdit || table.canAddFeature else { return [] } - let tableTemplates = table.featureTemplates.map({ - FeatureTemplateInfo(featureLayer:featureLayer, featureTable:table, featureTemplate:$0) - }) + let tableTemplates = table.featureTemplates.map { + FeatureTemplateInfo(featureLayer: featureLayer, featureTable: table, featureTemplate: $0) + } let typeTemplates = table.featureTypes .lazy - .flatMap({ $0.templates }) - .map({ FeatureTemplateInfo(featureLayer:featureLayer, featureTable:table, featureTemplate:$0) }) + .flatMap { $0.templates } + .map { FeatureTemplateInfo(featureLayer: featureLayer, featureTable: table, featureTemplate: $0) } return tableTemplates + typeTemplates } /// Loads the template infos as the current datasource /// and creates swatches for them - private func loadInfosAndCreateSwatches(infos: [FeatureTemplateInfo]){ - + private func loadInfosAndCreateSwatches(infos: [FeatureTemplateInfo]) { // if filtering, need to disable it - if isFiltering{ + if isFiltering { navigationItem.searchController?.isActive = false } @@ -174,14 +168,13 @@ public class TemplatePickerViewController: TableViewController { currentInfos = unfilteredInfos // generate swatches for the layer infos - for index in infos.indices{ + for index in infos.indices { let info = infos[index] - if let feature = info.featureTable.createFeature(with: info.featureTemplate){ + if let feature = info.featureTable.createFeature(with: info.featureTemplate) { let sym = info.featureLayer.renderer?.symbol(for: feature) - sym?.createSwatch{ [weak self] image, error in - + sym?.createSwatch { [weak self] image, error in guard let self = self else { return } - guard error == nil else{ return } + guard error == nil else { return } // update info with swatch infos[index].swatch = image @@ -196,7 +189,7 @@ public class TemplatePickerViewController: TableViewController { // MARK: TableView delegate/datasource methods - public func numberOfSectionsInTableView(_ tableView: UITableView) -> Int{ + public func numberOfSectionsInTableView(_ tableView: UITableView) -> Int { return tables.count } @@ -206,7 +199,6 @@ public class TemplatePickerViewController: TableViewController { } public func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - // when the user taps on a feature type // first get the selected object @@ -221,7 +213,7 @@ public class TemplatePickerViewController: TableViewController { // Only do this if not being presented from a nav controller // as in that case, it causes problems when the delegate that pushed this VC // tries to pop it off the stack. - if presentingViewController != nil{ + if presentingViewController != nil { navigationItem.searchController?.isActive = false } @@ -244,14 +236,15 @@ public class TemplatePickerViewController: TableViewController { // MARK: go back, cancel methods - @objc private func cancelAction(){ + @objc + private func cancelAction() { // If the search controller is still active, the delegate will not be // able to dismiss this if they showed this modally. // (or wrapped it in a navigation controller and showed that modally) // Only do this if not being presented from a nav controller // as in that case, it causes problems when the delegate that pushed this VC // tries to pop it off the stack. - if presentingViewController != nil{ + if presentingViewController != nil { navigationItem.searchController?.isActive = false } delegate?.templatePickerViewControllerDidCancel(self) @@ -259,14 +252,13 @@ public class TemplatePickerViewController: TableViewController { // MARK: IndexPath -> Info - private func info(for indexPath: IndexPath) -> FeatureTemplateInfo{ + private func info(for indexPath: IndexPath) -> FeatureTemplateInfo { let tableName = tables[indexPath.section].tableName let infos = self.currentDatasource[tableName]! return infos[indexPath.row] } - private func indexPath(for info: FeatureTemplateInfo) -> IndexPath{ - + private func indexPath(for info: FeatureTemplateInfo) -> IndexPath { let tableIndex = tables.index { $0.tableName == info.featureTable.tableName }! let infos = self.currentDatasource[info.featureTable.tableName]! let infoIndex = infos.index { $0 === info }! @@ -282,19 +274,18 @@ extension TemplatePickerViewController: UISearchResultsUpdating { isFiltering = true DispatchQueue.global(qos: DispatchQoS.QoSClass.userInitiated).async { [weak self] in guard let self = self else { return } - let filtered = self.unfilteredInfos.filter{ + let filtered = self.unfilteredInfos.filter { $0.featureTemplate.name.range(of: text, options: .caseInsensitive) != nil } DispatchQueue.main.async { [weak self] in guard let self = self else { return } // Make sure we are still filtering - if self.isFiltering{ + if self.isFiltering { self.currentInfos = filtered } } } - } - else { + } else { isFiltering = false self.currentInfos = self.unfilteredInfos } diff --git a/Toolkit/ArcGISToolkit/TimeSlider.swift b/Toolkit/ArcGISToolkit/TimeSlider.swift index 81252c9e..a51b20cc 100644 --- a/Toolkit/ArcGISToolkit/TimeSlider.swift +++ b/Toolkit/ArcGISToolkit/TimeSlider.swift @@ -19,7 +19,6 @@ import ArcGIS // MARK: - Time Slider Control public class TimeSlider: UIControl { - // MARK: - Enumerations /** @@ -106,7 +105,7 @@ public class TimeSlider: UIControl { isRangeEnabled = (startTime != endTime) } } - // This means there is only one thumb needs to be displayed and current extent start and end times are same. + // This means there is only one thumb needs to be displayed and current extent start and end times are same. else if let startTime = currentExtent?.startTime, currentExtent?.endTime == nil { // // Only one thumb should be displayed @@ -119,7 +118,7 @@ public class TimeSlider: UIControl { // Start and end time must be same. currentExtentEndTime = currentExtentStartTime } - // This means there is only one thumb needs to be displayed and current extent start and end times are same. + // This means there is only one thumb needs to be displayed and current extent start and end times are same. else if let endTime = currentExtent?.endTime, currentExtent?.startTime == nil { // // Only one thumb should be displayed @@ -132,8 +131,8 @@ public class TimeSlider: UIControl { // Start and end time must be same. currentExtentEndTime = currentExtentStartTime } - // Set start and end time to nil if current extent is nil - // or it's start and end times are nil + // Set start and end time to nil if current extent is nil + // or it's start and end times are nil else if currentExtent == nil || (currentExtent?.startTime == nil && currentExtent?.endTime == nil) { currentExtentStartTime = nil currentExtentEndTime = nil @@ -202,8 +201,7 @@ public class TimeSlider: UIControl { didSet { if isRangeEnabled { upperThumbLayer.isPinned = isEndTimePinned - } - else { + } else { isStartTimePinned = isEndTimePinned lowerThumbLayer.isPinned = isEndTimePinned upperThumbLayer.isPinned = isEndTimePinned @@ -222,14 +220,12 @@ public class TimeSlider: UIControl { // Set current extent if it's nil. if currentExtent == nil, let fullExtent = fullExtent { currentExtent = fullExtent - } - else if fullExtent == nil { + } else if fullExtent == nil { timeSteps?.removeAll() tickMarks.removeAll() removeTickMarkLabels() currentExtent = fullExtent - } - else { + } else { // // It is possible that the current extent times are outside of the range of // new full extent times. Adjust and sanp them to the tick marks. @@ -526,8 +522,7 @@ public class TimeSlider: UIControl { // Start the timer with specified playback interval timer = Timer.scheduledTimer(timeInterval: playbackInterval, target: self, selector: #selector(timerAction), userInfo: nil, repeats: true) - } - else { + } else { // // Set the button state playPauseButton.isSelected = false @@ -553,8 +548,7 @@ public class TimeSlider: UIControl { didSet { if observeGeoView { addObservers() - } - else { + } else { removeObservers() } } @@ -584,9 +578,9 @@ public class TimeSlider: UIControl { fullExtentEndTimeLabel.isHidden = !isSliderVisible currentExtentStartTimeLabel.isHidden = !isSliderVisible currentExtentEndTimeLabel.isHidden = !isSliderVisible - tickMarkLabels.forEach({ (tickMarkLabel) in + tickMarkLabels.forEach { (tickMarkLabel) in tickMarkLabel.isHidden = !isSliderVisible - }) + } invalidateIntrinsicContentSize() setNeedsLayout() } @@ -669,10 +663,10 @@ public class TimeSlider: UIControl { private let tickMarkLayer = TimeSliderTickMarkLayer() private let lowerThumbLayer = TimeSliderThumbLayer() private let upperThumbLayer = TimeSliderThumbLayer() - private let fullExtentStartTimeLabel: CATextLayer = CATextLayer() - private let fullExtentEndTimeLabel: CATextLayer = CATextLayer() - private let currentExtentStartTimeLabel: CATextLayer = CATextLayer() - private let currentExtentEndTimeLabel: CATextLayer = CATextLayer() + private let fullExtentStartTimeLabel = CATextLayer() + private let fullExtentEndTimeLabel = CATextLayer() + private let currentExtentStartTimeLabel = CATextLayer() + private let currentExtentEndTimeLabel = CATextLayer() private let minimumFrameWidth: CGFloat = 250.0 private let maximumThumbSize: CGFloat = 50.0 @@ -688,7 +682,7 @@ public class TimeSlider: UIControl { private let forwardButton = UIButton(type: .custom) private let backButton = UIButton(type: .custom) - fileprivate var pinnedThumbFillColor: UIColor = UIColor.black + fileprivate var pinnedThumbFillColor = UIColor.black // If set to True, it will show two thumbs, otherwise only one. Default is True. fileprivate var isRangeEnabled: Bool = true { @@ -707,10 +701,10 @@ public class TimeSlider: UIControl { private var mapLayersObservation: NSKeyValueObservation? private var sceneLayersObservation: NSKeyValueObservation? private var timeExtentObservation: NSKeyValueObservation? - + // MARK: - Override Functions - required public init?(coder: NSCoder) { + public required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } @@ -759,7 +753,7 @@ public class TimeSlider: UIControl { return lowerThumbLayer.isHighlighted || upperThumbLayer.isHighlighted } - open override func continueTracking(_ touch: UITouch, with event: UIEvent?) -> Bool { + override public func continueTracking(_ touch: UITouch, with event: UIEvent?) -> Bool { // // Get the touch location let location = touch.location(in: self) @@ -770,8 +764,7 @@ public class TimeSlider: UIControl { // Set values based on selected thumb if lowerThumbLayer.isHighlighted { updateCurrentExtentStartTime(Date(timeIntervalSince1970: selectedValue)) - } - else if upperThumbLayer.isHighlighted { + } else if upperThumbLayer.isHighlighted { updateCurrentExtentEndTime(Date(timeIntervalSince1970: selectedValue)) } @@ -805,7 +798,7 @@ public class TimeSlider: UIControl { } // Refresh the slider when requried - public override func layoutSubviews() { + override public func layoutSubviews() { // // Calculate time steps if timeSteps == nil || timeSteps?.isEmpty == true { @@ -821,7 +814,7 @@ public class TimeSlider: UIControl { } // Set intrinsic content size - public override var intrinsicContentSize: CGSize { + override public var intrinsicContentSize: CGSize { let intrinsicHeight: CGFloat if isSliderVisible { if playbackButtonsVisible { @@ -847,7 +840,8 @@ public class TimeSlider: UIControl { to initialize slider's fullExtent, currentExtent and timeStepInterval properties. Setting observeGeoView to true will observe changes in operational layers and time extent of geoView. */ - public func initializeTimeProperties(geoView: AGSGeoView, observeGeoView: Bool, completion: @escaping (Error?)->Void) { + // swiftlint:disable cyclomatic_complexity + public func initializeTimeProperties(geoView: AGSGeoView, observeGeoView: Bool, completion: @escaping (Error?) -> Void) { // // Set operational layers guard let operationalLayers = geoView.operationalLayers, !operationalLayers.isEmpty else { @@ -862,8 +856,7 @@ public class TimeSlider: UIControl { // Set map/scene if let mapView = geoView as? AGSMapView { map = mapView.map - } - else if let sceneView = geoView as? AGSSceneView { + } else if let sceneView = geoView as? AGSSceneView { scene = sceneView.scene } @@ -900,7 +893,7 @@ public class TimeSlider: UIControl { // Once, all layers are loaded, // loop through all of them. - operationalLayers.forEach({ (layer) in + operationalLayers.forEach { (layer) in // // The layer must be time aware, supports time filtering and time filtering is enabled. guard let timeAwareLayer = layer as? AGSTimeAware, timeAwareLayer.supportsTimeFiltering, timeAwareLayer.isTimeFilteringEnabled else { @@ -916,7 +909,7 @@ public class TimeSlider: UIControl { // This is an async operation to find out time step interval and // whether range time filtering is supported by the layer. dispatchGroup.enter() - self.findTimeStepIntervalAndIsRangeTimeFilteringSupported(for: timeAwareLayer, completion: { (timeInterval, supportsRangeFiltering) in + self.findTimeStepIntervalAndIsRangeTimeFilteringSupported(for: timeAwareLayer) { (timeInterval, supportsRangeFiltering) in // // Set the range filtering value supportsRangeTimeFiltering = supportsRangeFiltering @@ -927,8 +920,7 @@ public class TimeSlider: UIControl { if timeInterval > layersTimeInterval { timeAwareLayersStepInterval = timeInterval } - } - else { + } else { timeAwareLayersStepInterval = timeInterval } } @@ -937,10 +929,10 @@ public class TimeSlider: UIControl { // Leave the group so we can set time // properties and notify dispatchGroup.leave() - }) - }) + } + } - dispatchGroup.notify(queue: DispatchQueue.main, execute: { + dispatchGroup.notify(queue: DispatchQueue.main) { // // If full extent or time step interval is not available then // we cannot initialize the slider. Finish with error. @@ -960,8 +952,7 @@ public class TimeSlider: UIControl { // calculate using default timeStepCount if let timeStepInterval = timeAwareLayersStepInterval { self.timeStepInterval = timeStepInterval - } - else { + } else { self.timeStepInterval = self.calculateTimeStepInterval(for: layersFullExtent, timeStepCount: 0) } @@ -969,22 +960,22 @@ public class TimeSlider: UIControl { // current extent to either the full extent's start (if range filtering is not supported), or to the entire full extent. if let geoViewTimeExtent = self.geoView?.timeExtent, !self.reInitializeTimeProperties { self.currentExtent = geoViewTimeExtent - } - else { + } else { if let fullExtentStartTime = self.fullExtent?.startTime, let fullExtentEndTime = self.fullExtent?.endTime { self.currentExtent = supportsRangeTimeFiltering ? AGSTimeExtent(startTime: fullExtentStartTime, endTime: fullExtentEndTime) : AGSTimeExtent(timeInstant: fullExtentStartTime) } } completion(nil) - }) + } } } - + // swiftlint:enable cyclomatic_complexity + /** This will initialize slider's fullExtent, currentExtent and timeStepInterval properties if the layer is visible and participate in time based filtering. */ - public func initializeTimeProperties(timeAwareLayer: AGSTimeAware, completion: @escaping (Error?)->Void) { + public func initializeTimeProperties(timeAwareLayer: AGSTimeAware, completion: @escaping (Error?) -> Void) { // // The layer must be loadable. guard let layer = timeAwareLayer as? AGSLoadable else { @@ -992,7 +983,7 @@ public class TimeSlider: UIControl { return } - layer.load(completion: { [weak self] (error) in + layer.load { [weak self] (error) in // // If layer fails to load then // return with an error. @@ -1016,7 +1007,7 @@ public class TimeSlider: UIControl { // // This is an async operation to find out time step interval and // whether range time filtering is supported by the layer. - self.findTimeStepIntervalAndIsRangeTimeFilteringSupported(for: timeAwareLayer, completion: { [weak self] (timeInterval, supportsRangeTimeFiltering) in + self.findTimeStepIntervalAndIsRangeTimeFilteringSupported(for: timeAwareLayer) { [weak self] (timeInterval, supportsRangeTimeFiltering) in // // Make sure self is around guard let self = self else { @@ -1041,8 +1032,7 @@ public class TimeSlider: UIControl { // calculate using default timeStepCount if let timeInterval = timeInterval { self.timeStepInterval = timeInterval - } - else { + } else { self.timeStepInterval = self.calculateTimeStepInterval(for: fullTimeExtent, timeStepCount: 0) } @@ -1053,15 +1043,15 @@ public class TimeSlider: UIControl { // Slider is loaded successfully. completion(nil) - }) - }) + } + } } /** This will initialize slider's fullExtent, currentExtent and timeStepInterval properties based on provided step count and full extent. The current extent will be set to a time instant. */ - public func initializeTimeSteps(timeStepCount: Int, fullExtent: AGSTimeExtent, completion: @escaping (Error?)->Void) { + public func initializeTimeSteps(timeStepCount: Int, fullExtent: AGSTimeExtent, completion: @escaping (Error?) -> Void) { // // There should be at least two time steps // for time slider to work correctly. @@ -1071,7 +1061,7 @@ public class TimeSlider: UIControl { } // Full extent's start and end time must be available for time slider to work correctly. - guard let fullExtentStartTime = fullExtent.startTime, let _ = fullExtent.endTime else { + guard let fullExtentStartTime = fullExtent.startTime, fullExtent.endTime != nil else { completion(NSError(domain: AGSErrorDomain, code: AGSErrorCode.commonNoData.rawValue, userInfo: [NSLocalizedDescriptionKey: "fullExtent is not available to calculate time steps."])) return } @@ -1092,7 +1082,8 @@ public class TimeSlider: UIControl { /** Moves the slider thumbs forward with provided time steps. */ - @discardableResult public func stepForward(timeSteps: Int) -> Bool { + @discardableResult + public func stepForward(timeSteps: Int) -> Bool { // // Time steps must be greater than 0 if timeSteps > 0 { @@ -1104,7 +1095,8 @@ public class TimeSlider: UIControl { /** Moves the slider thumbs back with provided time steps. */ - @discardableResult public func stepBack(timeSteps: Int) -> Bool { + @discardableResult + public func stepBack(timeSteps: Int) -> Bool { // // Time steps must be greater than 0 if timeSteps > 0 { @@ -1115,21 +1107,25 @@ public class TimeSlider: UIControl { // MARK: - Actions - @objc private func forwardAction(_ sender: UIButton) { + @objc + private func forwardAction(_ sender: UIButton) { isPlaying = false stepForward(timeSteps: 1) } - @objc private func backAction(_ sender: UIButton) { + @objc + private func backAction(_ sender: UIButton) { isPlaying = false stepBack(timeSteps: 1) } - @objc private func playPauseAction(_ sender: UIButton) { - isPlaying = !isPlaying + @objc + private func playPauseAction(_ sender: UIButton) { + isPlaying.toggle() } - @discardableResult private func moveTimeStep(timeSteps: Int) -> Bool { + @discardableResult + private func moveTimeStep(timeSteps: Int) -> Bool { // // Time steps must be between 1 and count of calculated time steps if let ts = self.timeSteps, timeSteps < ts.count, let startTime = currentExtentStartTime, let endTime = currentExtentEndTime { @@ -1142,7 +1138,7 @@ public class TimeSlider: UIControl { // Set the start time step index if it's not set if startTimeStepIndex <= 0 { - if let (index, date) = closestTimeStep(for: startTime) { + if let (index, date) = closestTimeStep(for: startTime) { currentExtentStartTime = date startTimeStepIndex = index } @@ -1158,16 +1154,15 @@ public class TimeSlider: UIControl { // Get the minimum and maximum allowable time step indexes. This is not necessarily the end of the time slider since // the start and end times may be pinned. - let minTimeStepIndex = !isStartTimePinned ? 0 : startTimeStepIndex; - let maxTimeStepIndex = !isEndTimePinned ? ts.count - 1 : endTimeStepIndex; + let minTimeStepIndex = !isStartTimePinned ? 0 : startTimeStepIndex + let maxTimeStepIndex = !isEndTimePinned ? ts.count - 1 : endTimeStepIndex // Get the number of steps by which to move the current time. If the number specified in the method call would move the current time extent // beyond the valid range, clamp the number of steps to the maximum number that the extent can move in the specified direction. var validTimeStepDelta = 0 if timeSteps > 0 { validTimeStepDelta = startTimeStepIndex + timeSteps <= maxTimeStepIndex ? timeSteps : maxTimeStepIndex - startTimeStepIndex - } - else { + } else { validTimeStepDelta = endTimeStepIndex + timeSteps >= minTimeStepIndex ? timeSteps : minTimeStepIndex - endTimeStepIndex } @@ -1184,16 +1179,15 @@ public class TimeSlider: UIControl { endTimeStepIndex + validTimeStepDelta : endTimeStepIndex // Evaluate how many time steps the start and end were moved by and whether they were able to be moved by the requested number of steps - let startDelta = newStartTimeStepIndex - startTimeStepIndex; - let endDelta = newEndTimeStepIndex - endTimeStepIndex; - let canMoveStartAndEndByTimeSteps = startDelta == timeSteps && endDelta == timeSteps; - let canMoveStartOrEndByTimeSteps = startDelta == timeSteps || endDelta == timeSteps; + let startDelta = newStartTimeStepIndex - startTimeStepIndex + let endDelta = newEndTimeStepIndex - endTimeStepIndex + let canMoveStartAndEndByTimeSteps = startDelta == timeSteps && endDelta == timeSteps + let canMoveStartOrEndByTimeSteps = startDelta == timeSteps || endDelta == timeSteps - let isRequestedMoveValid = canMoveStartAndEndByTimeSteps || canMoveStartOrEndByTimeSteps; + let isRequestedMoveValid = canMoveStartAndEndByTimeSteps || canMoveStartOrEndByTimeSteps // Apply the new extent if the new time indexes represent a valid change if isRequestedMoveValid && newStartTimeStepIndex < ts.count && newEndTimeStepIndex < ts.count { - // Set new times and time step indexes currentExtentStartTime = ts[newStartTimeStepIndex] startTimeStepIndex = newStartTimeStepIndex @@ -1217,13 +1211,13 @@ public class TimeSlider: UIControl { return false } - @objc private func timerAction() { + @objc + private func timerAction() { if let geoView = self.geoView { if geoView.drawStatus == .completed { handlePlaying() } - } - else { + } else { handlePlaying() } } @@ -1403,21 +1397,19 @@ public class TimeSlider: UIControl { lowerThumbLayer.isHidden = !isSliderVisible lowerThumbLayer.frame = lowerThumbFrame lowerThumbLayer.setNeedsDisplay() - } - else { + } else { lowerThumbLayer.isHidden = true } // Set upper thumb layer frame - if let endTime = currentExtentEndTime, isRangeEnabled, fullExtent != nil { + if let endTime = currentExtentEndTime, isRangeEnabled, fullExtent != nil { let upperThumbCenter = CGFloat(position(for: endTime.timeIntervalSince1970)) let upperThumbOrigin = CGPoint(x: trackLayerSidePadding + upperThumbCenter - thumbSize.width / 2.0, y: trackLayerFrame.midY - thumbSize.height / 2.0) let upperThumbFrame = CGRect(origin: upperThumbOrigin, size: thumbSize) upperThumbLayer.isHidden = !isSliderVisible upperThumbLayer.frame = upperThumbFrame upperThumbLayer.setNeedsDisplay() - } - else { + } else { upperThumbLayer.isHidden = true } } @@ -1480,8 +1472,7 @@ public class TimeSlider: UIControl { let tickLayerStartTimeLabelY = tickMarkLayer.frame.maxY + labelPadding let startTimeLabelY = max(thumbStartTimeLabelY, tickLayerStartTimeLabelY) fullExtentStartTimeLabel.frame = CGRect(x: startTimeLabelX, y: startTimeLabelY, width: startTimeLabelSize.width, height: startTimeLabelSize.height) - } - else { + } else { fullExtentStartTimeLabel.string = "" fullExtentStartTimeLabel.isHidden = true } @@ -1502,8 +1493,7 @@ public class TimeSlider: UIControl { let tickLayerEndTimeLabelY = tickMarkLayer.frame.maxY + labelPadding let endTimeLabelY = max(thumbEndTimeLabelY, tickLayerEndTimeLabelY) fullExtentEndTimeLabel.frame = CGRect(x: endTimeLabelX, y: endTimeLabelY, width: endTimeLabelSize.width, height: endTimeLabelSize.height) - } - else { + } else { fullExtentEndTimeLabel.string = "" fullExtentEndTimeLabel.isHidden = true } @@ -1512,6 +1502,7 @@ public class TimeSlider: UIControl { CATransaction.commit() } + // swiftlint:disable cyclomatic_complexity private func updateCurrentExtentLabelFrames() { // // If label mode is not thumbs then @@ -1530,7 +1521,7 @@ public class TimeSlider: UIControl { // // Update current extent start time label - if let startTime = currentExtentStartTime, fullExtent != nil { + if let startTime = currentExtentStartTime, fullExtent != nil { let startTimeString = string(for: startTime, style: currentExtentLabelDateStyle) currentExtentStartTimeLabel.string = startTimeString let startTimeLabelSize: CGSize = startTimeString.size(withAttributes: [kCTFontAttributeName as NSAttributedString.Key: currentExtentLabelFont]) @@ -1541,14 +1532,12 @@ public class TimeSlider: UIControl { if let fullExtentStartTime = fullExtent?.startTime, fullExtentStartTime == startTime { currentExtentStartTimeLabel.isHidden = true } - } - else if startTimeLabelX + startTimeLabelSize.width > bounds.maxX - labelSidePadding { + } else if startTimeLabelX + startTimeLabelSize.width > bounds.maxX - labelSidePadding { startTimeLabelX = bounds.maxX - startTimeLabelSize.width - labelSidePadding if let fullExtentEndTime = fullExtent?.endTime, fullExtentEndTime == startTime { currentExtentStartTimeLabel.isHidden = true } - } - else if !currentExtentEndTimeLabel.isHidden && currentExtentEndTimeLabel.frame.origin.x >= 0.0 && startTimeLabelX + startTimeLabelSize.width > currentExtentEndTimeLabel.frame.origin.x { + } else if !currentExtentEndTimeLabel.isHidden && currentExtentEndTimeLabel.frame.origin.x >= 0.0 && startTimeLabelX + startTimeLabelSize.width > currentExtentEndTimeLabel.frame.origin.x { startTimeLabelX = currentExtentEndTimeLabel.frame.origin.x - startTimeLabelSize.width - paddingBetweenLabels } @@ -1556,14 +1545,13 @@ public class TimeSlider: UIControl { let tickLayerStartTimeLabelY = tickMarkLayer.frame.minY - currentExtentStartTimeLabel.frame.height - labelPadding let startTimeLabelY = min(thumbStartTimeLabelY, tickLayerStartTimeLabelY) currentExtentStartTimeLabel.frame = CGRect(x: startTimeLabelX, y: startTimeLabelY, width: startTimeLabelSize.width, height: startTimeLabelSize.height) - } - else { + } else { currentExtentStartTimeLabel.string = "" currentExtentStartTimeLabel.isHidden = true } // Update current extent end time label - if let endTime = currentExtentEndTime, isRangeEnabled, fullExtent != nil { + if let endTime = currentExtentEndTime, isRangeEnabled, fullExtent != nil { let endTimeString = string(for: endTime, style: currentExtentLabelDateStyle) currentExtentEndTimeLabel.string = endTimeString let endTimeLabelSize: CGSize = endTimeString.size(withAttributes: [kCTFontAttributeName as NSAttributedString.Key: currentExtentLabelFont]) @@ -1574,14 +1562,12 @@ public class TimeSlider: UIControl { if let fullExtentStartTime = fullExtent?.startTime, fullExtentStartTime == endTime { currentExtentEndTimeLabel.isHidden = true } - } - else if endTimeLabelX + endTimeLabelSize.width > bounds.maxX - labelSidePadding { + } else if endTimeLabelX + endTimeLabelSize.width > bounds.maxX - labelSidePadding { endTimeLabelX = bounds.maxX - endTimeLabelSize.width - labelSidePadding if let fullExtentEndTime = fullExtent?.endTime, fullExtentEndTime == endTime { currentExtentEndTimeLabel.isHidden = true } - } - else if !currentExtentStartTimeLabel.isHidden && endTimeLabelX < currentExtentStartTimeLabel.frame.origin.x + currentExtentStartTimeLabel.frame.width { + } else if !currentExtentStartTimeLabel.isHidden && endTimeLabelX < currentExtentStartTimeLabel.frame.origin.x + currentExtentStartTimeLabel.frame.width { endTimeLabelX = currentExtentStartTimeLabel.frame.origin.x + currentExtentStartTimeLabel.frame.width + paddingBetweenLabels } @@ -1589,8 +1575,7 @@ public class TimeSlider: UIControl { let tickLayerEndTimeLabelY = tickMarkLayer.frame.minY - currentExtentEndTimeLabel.frame.height - labelPadding let endTimeLabelY = min(thumbEndTimeLabelY, tickLayerEndTimeLabelY) currentExtentEndTimeLabel.frame = CGRect(x: endTimeLabelX, y: endTimeLabelY, width: endTimeLabelSize.width, height: endTimeLabelSize.height) - } - else { + } else { currentExtentEndTimeLabel.string = "" currentExtentEndTimeLabel.isHidden = true } @@ -1598,7 +1583,9 @@ public class TimeSlider: UIControl { // Commit the transaction CATransaction.commit() } - + // swiftlint:enable cyclomatic_complexity + + // swiftlint:disable cyclomatic_complexity private func positionTickMarks() { // // Bail out if time steps are not available @@ -1632,7 +1619,7 @@ public class TimeSlider: UIControl { if maxMajorTickInterval >= majorTickInterval { // // Calculate the number of ticks between each major tick and the index of the first major tick - for i in majorTickInterval.. (index: Int, date: Date)? { + private func closestTimeStep(for date: Date) -> (index: Int, date: Date)? { // // Return nil if not able to find the closest time step for the provided date. - guard let closest = timeSteps?.enumerated().min( by: { abs($0.1.timeIntervalSince1970 - date.timeIntervalSince1970) < abs($1.1.timeIntervalSince1970 - date.timeIntervalSince1970)} ) else { + guard let closest = timeSteps?.enumerated().min( by: { abs($0.1.timeIntervalSince1970 - date.timeIntervalSince1970) < abs($1.1.timeIntervalSince1970 - date.timeIntervalSince1970) }) else { return nil } @@ -1951,9 +1935,9 @@ public class TimeSlider: UIControl { private func removeTickMarkLabels() { // // Remove layers from the view - tickMarkLabels.forEach({ (tickMarkLabel) in + tickMarkLabels.forEach { (tickMarkLabel) in tickMarkLabel.removeFromSuperlayer() - }) + } // Clear the array tickMarkLabels.removeAll() @@ -1963,7 +1947,7 @@ public class TimeSlider: UIControl { private func notifyChangeOfCurrentExtent() { // // Notify only if current date are different than previous dates. - if (previousCurrentExtentStartTime != currentExtentStartTime || previousCurrentExtentEndTime != currentExtentEndTime) { + if previousCurrentExtentStartTime != currentExtentStartTime || previousCurrentExtentEndTime != currentExtentEndTime { // // Update previous values previousCurrentExtentStartTime = currentExtentStartTime @@ -1995,13 +1979,12 @@ public class TimeSlider: UIControl { let startTime = Date(timeIntervalSince1970: boundCurrentExtentStartTime(value: startTime.timeIntervalSince1970)) // If time steps are available then snap it to the closest time step and set the index. - if let ts = timeSteps, ts.count > 0 { + if let ts = timeSteps, !ts.isEmpty { if let (index, date) = closestTimeStep(for: startTime) { currentExtentStartTime = date startTimeStepIndex = index } - } - else { + } else { currentExtentStartTime = startTime startTimeStepIndex = -1 } @@ -2013,20 +1996,20 @@ public class TimeSlider: UIControl { let endTime = Date(timeIntervalSince1970: boundCurrentExtentEndTime(value: endTime.timeIntervalSince1970)) // If time steps are available then snap it to the closest time step and set the index. - if let ts = timeSteps, ts.count > 0 { + if let ts = timeSteps, !ts.isEmpty { if let (index, date) = closestTimeStep(for: endTime) { currentExtentEndTime = date endTimeStepIndex = index } - } - else { + } else { currentExtentEndTime = endTime endTimeStepIndex = -1 } } // This function returns time step interval and whether given layer supports range time filtering or not. - private func findTimeStepIntervalAndIsRangeTimeFilteringSupported(for timeAwareLayer: AGSTimeAware, completion: @escaping ((timeStepInterval: AGSTimeValue?, supportsRangeTimeFiltering: Bool))->Void) { + // swiftlint:disable cyclomatic_complexity + private func findTimeStepIntervalAndIsRangeTimeFilteringSupported(for timeAwareLayer: AGSTimeAware, completion: @escaping ((timeStepInterval: AGSTimeValue?, supportsRangeTimeFiltering: Bool)) -> Void) { // // The default is false var supportsRangeTimeFiltering = false @@ -2034,13 +2017,11 @@ public class TimeSlider: UIControl { // Get the time interval of the layer var timeStepInterval = timeAwareLayer.timeInterval - // If the layer is map image layer then we need to find out details from the // sublayers. Let's load all sublayers and check whether sub layers supports // range time filtering and largets time step interval. if let mapImageLayer = timeAwareLayer as? AGSArcGISMapImageLayer { - AGSLoadObjects(mapImageLayer.mapImageSublayers as! [AGSLoadable], { [weak self] (loaded) in - + AGSLoadObjects(mapImageLayer.mapImageSublayers as! [AGSLoadable]) { [weak self] (loaded) in // Make sure self is around guard let self = self else { return @@ -2053,7 +2034,7 @@ public class TimeSlider: UIControl { // // If either start or end time field name is not available then // set supportsRangeTimeFiltering to false - if timeInfo.startTimeField.count <= 0 || timeInfo.endTimeField.count <= 0 { + if timeInfo.startTimeField.isEmpty || timeInfo.endTimeField.isEmpty { supportsRangeTimeFiltering = false } @@ -2064,8 +2045,7 @@ public class TimeSlider: UIControl { if interval1 > interval2 { timeInterval = interval1 } - } - else { + } else { timeInterval = interval1 } } @@ -2079,22 +2059,22 @@ public class TimeSlider: UIControl { } } completion((timeStepInterval, supportsRangeTimeFiltering)) - }) - } - else { + } + } else { // // If layer is not map image layer then find layer supports // range time filtering or not and set time step interval // from timeInfo if not available on the layer. if let timeAwareLayer = timeAwareLayer as? AGSLoadable, let timeInfo = timeInfo(for: timeAwareLayer) { - if timeInfo.startTimeField.count <= 0 || timeInfo.endTimeField.count <= 0 { + if timeInfo.startTimeField.isEmpty || timeInfo.endTimeField.isEmpty { supportsRangeTimeFiltering = false } } completion((timeStepInterval, supportsRangeTimeFiltering)) } } - + // swiftlint:enable cyclomatic_complexity + // Returns layer's time info if available. The parameter cannot be of type AGSLayer because // ArcGISSublayer does not inherit from AGSLayer. It is expected that this function is // called on already loaded object @@ -2107,11 +2087,9 @@ public class TimeSlider: UIControl { if layer.loadStatus == .loaded { if let sublayer = layer as? AGSArcGISSublayer { return sublayer.mapServiceSublayerInfo?.timeInfo - } - else if let featureLayer = layer as? AGSFeatureLayer, let featureTable = featureLayer.featureTable as? AGSArcGISFeatureTable { + } else if let featureLayer = layer as? AGSFeatureLayer, let featureTable = featureLayer.featureTable as? AGSArcGISFeatureTable { return featureTable.layerInfo?.timeInfo - } - else if let rasterLayer = layer as? AGSRasterLayer, let imageServiceRaster = rasterLayer.raster as? AGSImageServiceRaster { + } else if let rasterLayer = layer as? AGSRasterLayer, let imageServiceRaster = rasterLayer.raster as? AGSImageServiceRaster { return imageServiceRaster.serviceInfo?.timeInfo } } @@ -2129,7 +2107,7 @@ public class TimeSlider: UIControl { // Re initialize time slider reInitializeTimeProperties = true - initializeTimeProperties(geoView: geoView, observeGeoView: observeGeoView, completion: { [weak self] (error) in + initializeTimeProperties(geoView: geoView, observeGeoView: observeGeoView) { [weak self] (error) in // // Bail out if there is an error guard error == nil else { @@ -2138,7 +2116,7 @@ public class TimeSlider: UIControl { // Set the flag self?.reInitializeTimeProperties = false - }) + } } // This function checks whether the observed value of operationalLayers @@ -2166,14 +2144,14 @@ public class TimeSlider: UIControl { } // This function returns a string for the given date and date style + // swiftlint:disable cyclomatic_complexity private func string(for date: Date, style: DateStyle) -> String { // // Create the date formatter to get the string for a date - let dateFormatter: DateFormatter = DateFormatter() + let dateFormatter = DateFormatter() dateFormatter.timeZone = timeZone switch style { - case .dayShortMonthYear: dateFormatter.setLocalizedDateFormatFromTemplate("d MMM y") case .longDate: @@ -2202,7 +2180,8 @@ public class TimeSlider: UIControl { return dateFormatter.string(from: date) } - + // swiftlint:enable cyclomatic_complexity + // Calculates time step interval based on provided time extent and time step count private func calculateTimeStepInterval(for timeExtent: AGSTimeExtent, timeStepCount: Int) -> AGSTimeValue? { if let startTime = timeExtent.startTime, let endTime = timeExtent.endTime { @@ -2217,8 +2196,7 @@ public class TimeSlider: UIControl { if let (duration, component) = timeIntervalDate.offset(from: startTime) { return AGSTimeValue.fromCalenderComponents(duration: Double(duration), component: component) } - } - else { + } else { if let startTime = timeExtent.startTime, let endTime = timeExtent.endTime { // // Since the time step count is 0 we'll use default duration 1 @@ -2230,7 +2208,6 @@ public class TimeSlider: UIControl { } return nil } - } // MARK: - Time Slider Thumb Layer @@ -2371,12 +2348,11 @@ private class TimeSliderTickMarkLayer: CALayer { // Render tick marks tickMarksOriginX.forEach { (tickX) in ctx.beginPath() - ctx.move(to: CGPoint(x: CGFloat(tickX), y:bounds.midY - (slider.trackHeight / 2.0))) + ctx.move(to: CGPoint(x: CGFloat(tickX), y: bounds.midY - (slider.trackHeight / 2.0))) ctx.addLine(to: CGPoint(x: CGFloat(tickX), y: bounds.midY + bounds.height / 2.0)) ctx.strokePath() } - } - else { + } else { // Loop through all tick marks // and render them. for i in 0.. (duration: Int, component: Calendar.Component)? { - if years(from: date) > 0 { return (years(from: date), .year) } - if months(from: date) > 0 { return (months(from: date), .month) } + if years(from: date) > 0 { return (years(from: date), .year) } + if months(from: date) > 0 { return (months(from: date), .month) } if seconds(from: date) > 0 { return (seconds(from: date), .second) } if nanoseconds(from: date) > 0 { return (nanoseconds(from: date), .nanosecond) } return nil @@ -2550,23 +2524,16 @@ fileprivate extension Date { // MARK: - Color Extension extension UIColor { - class var oceanBlue: UIColor { - get { - return UIColor(red: 0.0, green: 0.475, blue: 0.757, alpha: 1) - } + return UIColor(red: 0.0, green: 0.475, blue: 0.757, alpha: 1) } class var customBlue: UIColor { - get { - return UIColor(red: 0.0, green: 0.45, blue: 0.94, alpha: 1.0) - } + return UIColor(red: 0.0, green: 0.45, blue: 0.94, alpha: 1.0) } class var lightSkyBlue: UIColor { - get { - return UIColor(red: 0.529, green: 0.807, blue: 0.980, alpha: 1.0) - } + return UIColor(red: 0.529, green: 0.807, blue: 0.980, alpha: 1.0) } } @@ -2619,6 +2586,7 @@ extension AGSTimeValue: Comparable { } // Converts time value to the calender component values. + // swiftlint:disable cyclomatic_complexity public func toCalenderComponents() -> (duration: Double, component: Calendar.Component)? { switch unit { case .unknown: @@ -2645,7 +2613,8 @@ extension AGSTimeValue: Comparable { return (duration, Calendar.Component.year) } } - + // swiftlint:enable cyclomatic_complexity + // Returns time value generated from calender component and duration class func fromCalenderComponents(duration: Double, component: Calendar.Component) -> AGSTimeValue? { switch component { @@ -2697,21 +2666,17 @@ extension AGSTimeValue: Comparable { // MARK: - GeoView Extension -extension AGSGeoView { - - fileprivate var operationalLayers: [AGSLayer]? { - get { - if let mapView = self as? AGSMapView { - if let layers = mapView.map?.operationalLayers as AnyObject as? [AGSLayer] { - return layers - } +fileprivate extension AGSGeoView { + var operationalLayers: [AGSLayer]? { + if let mapView = self as? AGSMapView { + if let layers = mapView.map?.operationalLayers as AnyObject as? [AGSLayer] { + return layers } - else if let sceneView = self as? AGSSceneView { - if let layers = sceneView.scene?.operationalLayers as AnyObject as? [AGSLayer] { - return layers - } + } else if let sceneView = self as? AGSSceneView { + if let layers = sceneView.scene?.operationalLayers as AnyObject as? [AGSLayer] { + return layers } - return nil } + return nil } } diff --git a/Toolkit/ArcGISToolkit/UnitsViewController.swift b/Toolkit/ArcGISToolkit/UnitsViewController.swift index 18ec687f..8187699c 100644 --- a/Toolkit/ArcGISToolkit/UnitsViewController.swift +++ b/Toolkit/ArcGISToolkit/UnitsViewController.swift @@ -16,7 +16,7 @@ import class ArcGIS.AGSUnit /// The protocol you implement to respond as the user interacts with the units /// view controller. -public protocol UnitsViewControllerDelegate: class { +public protocol UnitsViewControllerDelegate: AnyObject { /// Tells the delegate that the user has cancelled selecting a unit. /// /// - Parameter unitsViewController: The current units view controller. @@ -59,14 +59,15 @@ public class UnitsViewController: TableViewController { } /// Called in response to the Cancel button being tapped. - @objc private func cancel() { + @objc + private func cancel() { // If the search controller is still active, the delegate will not be // able to dismiss this if they showed this modally. // (or wrapped it in a navigation controller and showed that modally) // Only do this if not being presented from a nav controller // as in that case, it causes problems when the delegate that pushed this VC // tries to pop it off the stack. - if presentingViewController != nil{ + if presentingViewController != nil { navigationItem.searchController?.isActive = false } delegate?.unitsViewControllerDidCancel(self) @@ -88,12 +89,12 @@ public class UnitsViewController: TableViewController { tableView.reloadRows(at: indexPaths, with: .automatic) } - override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) { + override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) { super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil) sharedInitialization() } - required public init?(coder aDecoder: NSCoder) { + public required init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) sharedInitialization() } @@ -141,7 +142,7 @@ public class UnitsViewController: TableViewController { // Only do this if not being presented from a nav controller // as in that case, it causes problems when the delegate that pushed this VC // tries to pop it off the stack. - if presentingViewController != nil{ + if presentingViewController != nil { navigationItem.searchController?.isActive = false } delegate?.unitsViewControllerDidSelectUnit(self) From eeecb7b29aea5e0ef6edd5a71be50531ac2d2b78 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Wed, 25 Sep 2019 09:12:41 -0500 Subject: [PATCH 139/147] SwiftLint changes for Examples app. --- Examples/.swiftlint.yml | 69 ++++++++++ .../project.pbxproj | 21 ++- .../ArcGISToolkitExamples/AppDelegate.swift | 5 - .../CompassExample.swift | 2 - .../ExamplesViewController.swift | 3 - .../JobManagerExample.swift | 125 +++++++----------- .../ArcGISToolkitExamples/LegendExample.swift | 8 +- .../MeasureExample.swift | 5 +- .../ArcGISToolkitExamples/PopupExample.swift | 9 +- .../ScalebarExample.swift | 3 - .../TemplatePickerExample.swift | 18 +-- .../TimeSliderExample.swift | 18 +-- .../VCListViewController.swift | 20 ++- Toolkit/ArcGISToolkit/JobManager.swift | 11 +- 14 files changed, 175 insertions(+), 142 deletions(-) create mode 100644 Examples/.swiftlint.yml diff --git a/Examples/.swiftlint.yml b/Examples/.swiftlint.yml new file mode 100644 index 00000000..248ca5fd --- /dev/null +++ b/Examples/.swiftlint.yml @@ -0,0 +1,69 @@ +included: +opt_in_rules: + - anyobject_protocol + - array_init + - attributes + - block_based_kvo + - closure_end_indentation + - closure_spacing + - collection_alignment + - contains_over_filter_count + - contains_over_filter_is_empty + - contains_over_first_not_nil + - convenience_type + - discouraged_direct_init + - discouraged_optional_boolean + - empty_collection_literal + - empty_count + - empty_string + - empty_xctest_method + - explicit_init + - extension_access_modifier + - fatal_error_message + - first_where + - function_default_parameter_at_end + - identical_operands + - joined_default_parameter + - legacy_random + - let_var_whitespace + - literal_expression_end_indentation + - lower_acl_than_parent + - modifier_order + - multiline_arguments + - multiline_function_chains + - multiline_parameters + - operator_usage_whitespace + - operator_whitespace + - overridden_super_call + - override_in_extension + - prohibited_super_call + - redundant_nil_coalescing + - redundant_type_annotation + - sorted_first_last + - static_operator + - toggle_bool + - trailing_closure + - untyped_error_in_catch + - vertical_parameter_alignment_on_call + - vertical_whitespace_closing_braces + - vertical_whitespace_opening_braces + - xctfail_message + - yoda_condition +disabled_rules: + - file_length + - for_where + - force_cast + - function_body_length + - function_parameter_count + - identifier_name + - large_tuple + - line_length + - nesting + - todo + - trailing_whitespace + - type_body_length +custom_rules: + assert_equal_to_nil: + name: Assert Equal to Nil + regex: XCTAssertEqual\([^\n]+,\s*nil\) + message: Use 'XCTAssertNil' instead. diff --git a/Examples/ArcGISToolkitExamples.xcodeproj/project.pbxproj b/Examples/ArcGISToolkitExamples.xcodeproj/project.pbxproj index 5c878bc2..68f05115 100644 --- a/Examples/ArcGISToolkitExamples.xcodeproj/project.pbxproj +++ b/Examples/ArcGISToolkitExamples.xcodeproj/project.pbxproj @@ -168,6 +168,7 @@ 883904391DF6022A001F3188 /* Resources */, 883904531DF60296001F3188 /* Embed Frameworks */, 88AE77111EFC267A00AFC80A /* ShellScript */, + E47ED066233AC27B0032440E /* Run Linter */, ); buildRules = ( ); @@ -261,7 +262,25 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "bash \"${BUILT_PRODUCTS_DIR}/${FRAMEWORKS_FOLDER_PATH}/ArcGIS.framework/strip-frameworks.sh\""; + shellScript = "bash \"${BUILT_PRODUCTS_DIR}/${FRAMEWORKS_FOLDER_PATH}/ArcGIS.framework/strip-frameworks.sh\"\n"; + }; + E47ED066233AC27B0032440E /* Run Linter */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + name = "Run Linter"; + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "if which swiftlint >/dev/null; then\nswiftlint\nelse\necho \"warning: SwiftLint not installed, download from https://github.com/realm/SwiftLint\"\nfi\n"; }; /* End PBXShellScriptBuildPhase section */ diff --git a/Examples/ArcGISToolkitExamples/AppDelegate.swift b/Examples/ArcGISToolkitExamples/AppDelegate.swift index 006d7a72..9dce979a 100644 --- a/Examples/ArcGISToolkitExamples/AppDelegate.swift +++ b/Examples/ArcGISToolkitExamples/AppDelegate.swift @@ -16,10 +16,8 @@ import ArcGISToolkit @UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate { - var window: UIWindow? - func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool { // Override point for customization after application launch. return true @@ -50,9 +48,6 @@ class AppDelegate: UIResponder, UIApplicationDelegate { // Here is where we forward background fetch to the JobManager // so that jobs can be updated in the background func application(_ application: UIApplication, performFetchWithCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) { - JobManager.shared.application(application: application, performFetchWithCompletionHandler: completionHandler) } - } - diff --git a/Examples/ArcGISToolkitExamples/CompassExample.swift b/Examples/ArcGISToolkitExamples/CompassExample.swift index b3cfe8a9..bcd0725b 100644 --- a/Examples/ArcGISToolkitExamples/CompassExample.swift +++ b/Examples/ArcGISToolkitExamples/CompassExample.swift @@ -16,7 +16,6 @@ import ArcGISToolkit import ArcGIS class CompassExample: MapViewController { - var map: AGSMap? override func viewDidLoad() { @@ -39,5 +38,4 @@ class CompassExample: MapViewController { compass.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 12.0).isActive = true compass.trailingAnchor.constraint(equalTo: margins.trailingAnchor).isActive = true } - } diff --git a/Examples/ArcGISToolkitExamples/ExamplesViewController.swift b/Examples/ArcGISToolkitExamples/ExamplesViewController.swift index cd7530d6..fa0b1ef8 100644 --- a/Examples/ArcGISToolkitExamples/ExamplesViewController.swift +++ b/Examples/ArcGISToolkitExamples/ExamplesViewController.swift @@ -15,7 +15,6 @@ import UIKit import ArcGISToolkit class ExamplesViewController: VCListViewController { - override func viewDidLoad() { super.viewDidLoad() @@ -31,7 +30,5 @@ class ExamplesViewController: VCListViewController { ("Popup Controller", PopupExample.self, nil), ("Template Picker", TemplatePickerExample.self, nil) ] - } - } diff --git a/Examples/ArcGISToolkitExamples/JobManagerExample.swift b/Examples/ArcGISToolkitExamples/JobManagerExample.swift index 7399e503..0ae6df04 100644 --- a/Examples/ArcGISToolkitExamples/JobManagerExample.swift +++ b/Examples/ArcGISToolkitExamples/JobManagerExample.swift @@ -30,8 +30,7 @@ import UserNotifications // We forward that call to the shared JobManager so that it can perform the background fetch. // -class JobTableViewCell: UITableViewCell{ - +class JobTableViewCell: UITableViewCell { var job: AGSJob? var statusObservation: NSKeyValueObservation? @@ -43,8 +42,7 @@ class JobTableViewCell: UITableViewCell{ fatalError("init(coder:) has not been implemented") } - func configureWithJob(job: AGSJob?){ - + func configureWithJob(job: AGSJob?) { // invalidate previous observation statusObservation?.invalidate() statusObservation = nil @@ -54,16 +52,15 @@ class JobTableViewCell: UITableViewCell{ self.updateUI() // observe job status - statusObservation = self.job?.observe(\.status, options: .new) { [weak self] (job, changes) in + statusObservation = self.job?.observe(\.status, options: .new) { [weak self] (_, _) in DispatchQueue.main.async { self?.updateUI() } } } - func updateUI(){ - - guard let job = job else{ + func updateUI() { + guard let job = job else { return } @@ -74,46 +71,36 @@ class JobTableViewCell: UITableViewCell{ } override func prepareForReuse() { + super.prepareForReuse() self.textLabel?.text = "" self.detailTextLabel?.text = "" } - - class func jobTypeString(_ job: AGSJob)->String{ - if job is AGSGenerateGeodatabaseJob{ + class func jobTypeString(_ job: AGSJob) -> String { + if job is AGSGenerateGeodatabaseJob { return "Generate GDB" - } - else if job is AGSSyncGeodatabaseJob{ + } else if job is AGSSyncGeodatabaseJob { return "Sync GDB" - } - else if job is AGSExportTileCacheJob{ + } else if job is AGSExportTileCacheJob { return "Export Tiles" - } - else if job is AGSEstimateTileCacheSizeJob{ + } else if job is AGSEstimateTileCacheSizeJob { return "Estimate Tile Cache Size" - } - else if job is AGSGenerateOfflineMapJob{ + } else if job is AGSGenerateOfflineMapJob { return "Offline Map" - } - else if job is AGSOfflineMapSyncJob{ + } else if job is AGSOfflineMapSyncJob { return "Offline Map Sync" - } - else if job is AGSGeoprocessingJob{ + } else if job is AGSGeoprocessingJob { return "Geoprocessing" - } - else if job is AGSExportVectorTilesJob{ + } else if job is AGSExportVectorTilesJob { return "Export Vector Tiles" - } - else if job is AGSDownloadPreplannedOfflineMapJob{ + } else if job is AGSDownloadPreplannedOfflineMapJob { return "Download Preplanned Offline Map" } return "Other" } - } class JobManagerExample: TableViewController { - // array to hold onto tasks while they are loading var tasks = [AGSGeodatabaseSyncTask]() @@ -123,7 +110,7 @@ class JobManagerExample: TableViewController { var toolbar: UIToolbar? - override open func viewDidLoad() { + override func viewDidLoad() { super.viewDidLoad() // create a Toolbar and add it to the view controller @@ -158,7 +145,7 @@ class JobManagerExample: TableViewController { // request authorization for user notifications, this way we can notify user in bg when job complete let center = UNUserNotificationCenter.current() center.requestAuthorization(options: [.alert, .badge, .sound]) { (granted, _) in - if !granted{ + if !granted { print("You must grant access for user notifications for all the features of this sample to work") } } @@ -167,26 +154,26 @@ class JobManagerExample: TableViewController { tableView.register(JobTableViewCell.self, forCellReuseIdentifier: "JobCell") } - @objc func resumeAllPausedJobs(){ + @objc + func resumeAllPausedJobs() { JobManager.shared.resumeAllPausedJobs(statusHandler: self.jobStatusHandler, completion: self.jobCompletionHandler) } - @objc func clearFinishedJobs(){ + @objc + func clearFinishedJobs() { JobManager.shared.clearFinishedJobs() tableView.reloadData() } var i = 0 - @objc func kickOffJob(){ - - if (i % 2) == 0{ + @objc + func kickOffJob() { + if (i % 2) == 0 { let url = URL(string: "https://sampleserver6.arcgisonline.com/arcgis/rest/services/Sync/WildfireSync/FeatureServer")! generateGDB(URL: url, syncModel: .layer, extent: nil) - } - else{ - - let portalItem = AGSPortalItem(url: URL(string:"https://www.arcgis.com/home/item.html?id=acc027394bc84c2fb04d1ed317aac674")!)! + } else { + let portalItem = AGSPortalItem(url: URL(string: "https://www.arcgis.com/home/item.html?id=acc027394bc84c2fb04d1ed317aac674")!)! let map = AGSMap(item: portalItem) // naperville let env = AGSEnvelope(xMin: -9825684.031125, yMin: 5102237.935062, xMax: -9798254.961608, yMax: 5151000.725314, spatialReference: AGSSpatialReference.webMercator()) @@ -196,7 +183,7 @@ class JobManagerExample: TableViewController { i += 1 } - required public init?(coder aDecoder: NSCoder) { + required init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) } @@ -204,11 +191,11 @@ class JobManagerExample: TableViewController { super.init(nibName: nil, bundle: nil) } - override open func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return jobs.count } - override open func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCell(withIdentifier: "JobCell") as! JobTableViewCell let job = jobs[indexPath.row] cell.configureWithJob(job: job) @@ -219,58 +206,55 @@ class JobManagerExample: TableViewController { tableView.deselectRow(at: indexPath, animated: true) } - var documentsPath: String{ + var documentsPath: String { return NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0] } - func generateGDB(URL: URL, syncModel: AGSSyncModel, extent: AGSEnvelope?){ - + func generateGDB(URL: URL, syncModel: AGSSyncModel, extent: AGSEnvelope?) { let task = AGSGeodatabaseSyncTask(url: URL) // hold on to task so that it stays retained while it's loading self.tasks.append(task) - task.load{ [weak self, weak task] error in - + task.load { [weak self, weak task] error in // make sure we are still around... guard let self = self else { return } - guard let strongTask = task else{ + guard let strongTask = task else { return } // remove task from array now that it's loaded - if let index = self.tasks.index(where: {return $0 === strongTask}){ + if let index = self.tasks.index(where: { return $0 === strongTask }) { self.tasks.remove(at: index) } // return if error or no featureServiceInfo - guard error == nil else{ + guard error == nil else { return } - guard let fsi = strongTask.featureServiceInfo else{ + guard let fsi = strongTask.featureServiceInfo else { return } let params = AGSGenerateGeodatabaseParameters() params.extent = extent - if params.extent == nil{ + if params.extent == nil { params.extent = fsi.fullExtent } params.outSpatialReference = AGSSpatialReference.webMercator() - if syncModel == .geodatabase{ + if syncModel == .geodatabase { params.syncModel = .geodatabase - } - else{ + } else { params.syncModel = .layer var options = [AGSGenerateLayerOption]() - for li in fsi.layerInfos{ + for li in fsi.layerInfos { let option = AGSGenerateLayerOption(layerID: li.id) options.append(option) } @@ -294,21 +278,19 @@ class JobManagerExample: TableViewController { } } - func takeOffline(map: AGSMap, extent: AGSEnvelope){ - + func takeOffline(map: AGSMap, extent: AGSEnvelope) { let task = AGSOfflineMapTask(onlineMap: map) let uuid = NSUUID() let offlineMapURL = URL(fileURLWithPath: "\(self.documentsPath)/\(uuid.uuidString)") as URL - task.defaultGenerateOfflineMapParameters(withAreaOfInterest: extent){ [weak self] params, error in - + task.defaultGenerateOfflineMapParameters(withAreaOfInterest: extent) { [weak self] params, error in // make sure we are still around... guard let self = self else { return } - if let params = params{ + if let params = params { let job = task.generateOfflineMapJob(with: params, downloadDirectory: offlineMapURL) // register the job with our JobManager shared instance @@ -319,25 +301,22 @@ class JobManagerExample: TableViewController { // refresh the tableview self.tableView.reloadData() - } - else{ + } else { // if could not get default parameters, then fire completion with the error self.jobCompletionHandler(result: nil, error: error) } } - } - func jobStatusHandler(status: AGSJobStatus){ + func jobStatusHandler(status: AGSJobStatus) { print("status: \(status.asString())") } - func jobCompletionHandler(result: Any?, error: Error?){ + func jobCompletionHandler(result: Any?, error: Error?) { print("job completed") - if let error = error{ + if let error = error { print(" - error: \(error)") - } - else if let result = result{ + } else if let result = result { print(" - result: \(result)") } @@ -351,9 +330,8 @@ class JobManagerExample: TableViewController { } } - -extension AGSJobStatus{ - func asString() -> String{ +extension AGSJobStatus { + func asString() -> String { switch self { case .failed: return "Failed" @@ -368,4 +346,3 @@ extension AGSJobStatus{ } } } - diff --git a/Examples/ArcGISToolkitExamples/LegendExample.swift b/Examples/ArcGISToolkitExamples/LegendExample.swift index 107cb69e..77ea95b0 100644 --- a/Examples/ArcGISToolkitExamples/LegendExample.swift +++ b/Examples/ArcGISToolkitExamples/LegendExample.swift @@ -16,7 +16,6 @@ import ArcGISToolkit import ArcGIS class LegendExample: MapViewController { - let portal = AGSPortal.arcGISOnline(withLoginRequired: false) var portalItem: AGSPortalItem? var map: AGSMap? @@ -37,11 +36,10 @@ class LegendExample: MapViewController { navigationItem.rightBarButtonItem = bbi } - @objc func showLegendAction(){ - if let legendVC = legendVC{ + @objc + func showLegendAction() { + if let legendVC = legendVC { navigationController?.pushViewController(legendVC, animated: true) } } - } - diff --git a/Examples/ArcGISToolkitExamples/MeasureExample.swift b/Examples/ArcGISToolkitExamples/MeasureExample.swift index 5ddbc1e6..3df3393c 100644 --- a/Examples/ArcGISToolkitExamples/MeasureExample.swift +++ b/Examples/ArcGISToolkitExamples/MeasureExample.swift @@ -15,8 +15,7 @@ import UIKit import ArcGISToolkit import ArcGIS -class MeasureExample: MapViewController{ - +class MeasureExample: MapViewController { var measureToolbar: MeasureToolbar! override func viewDidLoad() { @@ -35,7 +34,6 @@ class MeasureExample: MapViewController{ measureToolbar.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true measureToolbar.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor).isActive = true measureToolbar.heightAnchor.constraint(equalToConstant: 44).isActive = true - } override func viewDidLayoutSubviews() { @@ -44,5 +42,4 @@ class MeasureExample: MapViewController{ // update content inset for mapview mapView.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: measureToolbar.frame.height, right: 0) } - } diff --git a/Examples/ArcGISToolkitExamples/PopupExample.swift b/Examples/ArcGISToolkitExamples/PopupExample.swift index ff00c205..4ed99ad6 100644 --- a/Examples/ArcGISToolkitExamples/PopupExample.swift +++ b/Examples/ArcGISToolkitExamples/PopupExample.swift @@ -16,7 +16,6 @@ import ArcGISToolkit import ArcGIS class PopupExample: MapViewController { - var map: AGSMap? var popupController: PopupController? @@ -34,7 +33,7 @@ class PopupExample: MapViewController { // We have to load it first to create a default popup definition. // If you create the map from a portal item, you can define the popup definition // in the webmap and avoid this step. - featureLayer.load{ _ in + featureLayer.load { _ in featureLayer.popupDefinition = AGSPopupDefinition(popupSource: featureLayer) } @@ -47,8 +46,8 @@ class PopupExample: MapViewController { mapView.map = map // Log if there is any error loading the map - map?.load{ error in - if let error = error{ + map?.load { error in + if let error = error { print("error loading map: \(error)") } } @@ -56,6 +55,4 @@ class PopupExample: MapViewController { // instantiate the popup controller popupController = PopupController(geoViewController: self, geoView: mapView) } - } - diff --git a/Examples/ArcGISToolkitExamples/ScalebarExample.swift b/Examples/ArcGISToolkitExamples/ScalebarExample.swift index 4f179a36..322b493a 100644 --- a/Examples/ArcGISToolkitExamples/ScalebarExample.swift +++ b/Examples/ArcGISToolkitExamples/ScalebarExample.swift @@ -16,7 +16,6 @@ import ArcGISToolkit import ArcGIS class ScalebarExample: MapViewController, AGSGeoViewTouchDelegate { - var map: AGSMap? var scalebar: Scalebar? @@ -44,6 +43,4 @@ class ScalebarExample: MapViewController, AGSGeoViewTouchDelegate { sb.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: xMargin).isActive = true scalebar = sb } - } - diff --git a/Examples/ArcGISToolkitExamples/TemplatePickerExample.swift b/Examples/ArcGISToolkitExamples/TemplatePickerExample.swift index e2605196..728084ee 100644 --- a/Examples/ArcGISToolkitExamples/TemplatePickerExample.swift +++ b/Examples/ArcGISToolkitExamples/TemplatePickerExample.swift @@ -16,7 +16,6 @@ import ArcGISToolkit import ArcGIS class TemplatePickerExample: MapViewController { - var map: AGSMap? override func viewDidLoad() { @@ -33,7 +32,7 @@ class TemplatePickerExample: MapViewController { // We have to load it first to create a default popup definition. // If you create the map from a portal item, you can define the popup definition // in the webmap and avoid this step. - featureLayer.load{ _ in + featureLayer.load { _ in featureLayer.popupDefinition = AGSPopupDefinition(popupSource: featureLayer) } @@ -41,8 +40,8 @@ class TemplatePickerExample: MapViewController { mapView.map = map // Log if there is any error loading the map - map?.load{ error in - if let error = error{ + map?.load { error in + if let error = error { print("error loading map: \(error)") } } @@ -52,8 +51,8 @@ class TemplatePickerExample: MapViewController { navigationItem.rightBarButtonItem = bbi } - @objc private func showTemplates(){ - + @objc + private func showTemplates() { guard let map = map else { return } // Instantiate the TemplatePickerViewController @@ -65,13 +64,10 @@ class TemplatePickerExample: MapViewController { // Present the template picker self.navigationController?.pushViewController(templatePicker, animated: true) } - } extension TemplatePickerExample: TemplatePickerViewControllerDelegate { - public func templatePickerViewControllerDidCancel(_ templatePickerViewController: TemplatePickerViewController) { - // This is where you handle the user canceling the template picker // dismiss the template picker @@ -85,7 +81,6 @@ extension TemplatePickerExample: TemplatePickerViewControllerDelegate { } public func templatePickerViewController(_ templatePickerViewController: TemplatePickerViewController, didSelect featureTemplateInfo: FeatureTemplateInfo) { - // This is where you handle the user making a selection with the template picker // dismiss the template picker @@ -98,6 +93,3 @@ extension TemplatePickerExample: TemplatePickerViewControllerDelegate { present(alert, animated: true) } } - - - diff --git a/Examples/ArcGISToolkitExamples/TimeSliderExample.swift b/Examples/ArcGISToolkitExamples/TimeSliderExample.swift index ddc2a8a9..6c51866a 100644 --- a/Examples/ArcGISToolkitExamples/TimeSliderExample.swift +++ b/Examples/ArcGISToolkitExamples/TimeSliderExample.swift @@ -17,7 +17,6 @@ import ArcGISToolkit import ArcGIS class TimeSliderExample: MapViewController { - private var map = AGSMap(basemap: AGSBasemap.topographic()) private var timeSlider = TimeSlider() @@ -44,8 +43,7 @@ class TimeSliderExample: MapViewController { // Add layer let mapImageLayer = AGSArcGISMapImageLayer(url: URL(string: "https://sampleserver6.arcgisonline.com/arcgis/rest/services/911CallsHotspot/MapServer")!) mapView.map?.operationalLayers.add(mapImageLayer) - mapImageLayer.load(completion: { [weak self] (error) in - + mapImageLayer.load { [weak self] (error) in // Make sure self is around guard let self = self else { return @@ -63,8 +61,7 @@ class TimeSliderExample: MapViewController { self.mapView.setViewpoint(AGSViewpoint(targetExtent: fullExtent), completion: nil) } - self.timeSlider.initializeTimeProperties(geoView: self.mapView, observeGeoView: true, completion: { [weak self] (error) in - + self.timeSlider.initializeTimeProperties(geoView: self.mapView, observeGeoView: true) { [weak self] (error) in // Make sure self is around guard let self = self else { return @@ -79,17 +76,18 @@ class TimeSliderExample: MapViewController { // Show the time slider self.timeSlider.isHidden = false - }) - }) + } + } } - @objc func timeSliderValueChanged(timeSlider: TimeSlider) { + @objc + func timeSliderValueChanged(timeSlider: TimeSlider) { if mapView.timeExtent != timeSlider.currentExtent { mapView.timeExtent = timeSlider.currentExtent } } - //MARK: - Show Error + // MARK: - Show Error private func showError(_ error: Error) { let alertController = UIAlertController(title: "Error", message: error.localizedDescription, preferredStyle: .alert) @@ -97,5 +95,3 @@ class TimeSliderExample: MapViewController { present(alertController, animated: true) } } - - diff --git a/Examples/ArcGISToolkitExamples/VCListViewController.swift b/Examples/ArcGISToolkitExamples/VCListViewController.swift index 6df65512..52f79559 100644 --- a/Examples/ArcGISToolkitExamples/VCListViewController.swift +++ b/Examples/ArcGISToolkitExamples/VCListViewController.swift @@ -15,12 +15,11 @@ import UIKit import ArcGISToolkit open class VCListViewController: UITableViewController { - public var storyboardName: String? public var viewControllerInfos: [(vcName: String, viewControllerType: UIViewController.Type, nibName: String?)] = [ - ]{ - didSet{ + ] { + didSet { self.tableView.reloadData() } } @@ -38,28 +37,27 @@ open class VCListViewController: UITableViewController { override open func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { let t = viewControllerInfos[indexPath.row].viewControllerType let nibName = viewControllerInfos[indexPath.row].nibName - var vcOpt: UIViewController? = nil + var vcOpt: UIViewController? // first check storyboard - if let storyboardName = self.storyboardName{ + if let storyboardName = self.storyboardName { let sb = UIStoryboard(name: storyboardName, bundle: nil) - if let nibName = nibName{ + if let nibName = nibName { // this is how you can check to see if that identifier is in the nib, based on http://stackoverflow.com/a/34650505/1687195 - if let dictionary = sb.value(forKey: "identifierToNibNameMap") as? NSDictionary{ - if dictionary.value(forKey: nibName) != nil{ + if let dictionary = sb.value(forKey: "identifierToNibNameMap") as? NSDictionary { + if dictionary.value(forKey: nibName) != nil { vcOpt = sb.instantiateViewController(withIdentifier: nibName) } } } } - if vcOpt == nil{ + if vcOpt == nil { vcOpt = t.init(nibName: nibName, bundle: nil) } - if let vc = vcOpt{ + if let vc = vcOpt { navigationController?.pushViewController(vc, animated: true) } } - } diff --git a/Toolkit/ArcGISToolkit/JobManager.swift b/Toolkit/ArcGISToolkit/JobManager.swift index 4ff67898..c4384d12 100644 --- a/Toolkit/ArcGISToolkit/JobManager.swift +++ b/Toolkit/ArcGISToolkit/JobManager.swift @@ -79,7 +79,7 @@ public class JobManager: NSObject { return "com.esri.arcgis.runtime.toolkit.jobManager.\(jobManagerID).jobs" } - private var jobStatusObservation: NSKeyValueObservation? + private var jobStatusObservations = [String: NSKeyValueObservation]() /// Create a JobManager with an ID. /// @@ -100,14 +100,17 @@ public class JobManager: NSObject { // Observing job status code private func observeJobStatus(job: AGSJob) { - jobStatusObservation = job.observe(\.status, options: [.new]) { [weak self] (_, _) in + let observer = job.observe(\.status, options: [.new]) { [weak self] (_, _) in self?.saveJobsToUserDefaults() } + jobStatusObservations[job.serverJobID] = observer } private func unObserveJobStatus(job: AGSJob) { - jobStatusObservation?.invalidate() - jobStatusObservation = nil + if let observer = jobStatusObservations[job.serverJobID] { + observer.invalidate() + jobStatusObservations.removeValue(forKey: job.serverJobID) + } } /// Register an `AGSJob` with the `JobManager`. From 0db70ad50d90e18076da0bb9fb0343d4bed1e87f Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Wed, 25 Sep 2019 10:23:38 -0500 Subject: [PATCH 140/147] Add SwiftLint information --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 49533e99..b555e539 100644 --- a/README.md +++ b/README.md @@ -50,6 +50,8 @@ Note that you must also have the __ArcGIS Runtime SDK for iOS__ installed and yo 4. Drag the `ArcGISToolkit.framework` from the `ArcGISToolkit.xcodeproj/ArcGISToolkit/Products` folder to the "TARGETS" settings for your application and drop it in the "Embedded Binaries" section in the "General" tab 5. Add `import ArcGISToolkit` in your source code and start using the toolkit components +## SwiftLint +New in the 100.6.0 release is SwiftLint support for both the Toolkit and Examples app. You will need to install SwiftLint from [here](https://github.com/realm/SwiftLint) in order to build. The specific rules the linter uses can be found in the `swiftlint.yml` files in the `Toolkit` and `Examples` directories. ## Additional Resources From 8a3766f5d2fedb2d266f43240e0297216eb3c6b1 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Wed, 25 Sep 2019 10:52:13 -0500 Subject: [PATCH 141/147] move .swiftlint.yml up to parent dir and use for both projects --- Examples/.swiftlint.yml => .swiftlint.yml | 0 Toolkit/.swiftlint.yml | 69 ----------------------- 2 files changed, 69 deletions(-) rename Examples/.swiftlint.yml => .swiftlint.yml (100%) delete mode 100644 Toolkit/.swiftlint.yml diff --git a/Examples/.swiftlint.yml b/.swiftlint.yml similarity index 100% rename from Examples/.swiftlint.yml rename to .swiftlint.yml diff --git a/Toolkit/.swiftlint.yml b/Toolkit/.swiftlint.yml deleted file mode 100644 index 248ca5fd..00000000 --- a/Toolkit/.swiftlint.yml +++ /dev/null @@ -1,69 +0,0 @@ -included: -opt_in_rules: - - anyobject_protocol - - array_init - - attributes - - block_based_kvo - - closure_end_indentation - - closure_spacing - - collection_alignment - - contains_over_filter_count - - contains_over_filter_is_empty - - contains_over_first_not_nil - - convenience_type - - discouraged_direct_init - - discouraged_optional_boolean - - empty_collection_literal - - empty_count - - empty_string - - empty_xctest_method - - explicit_init - - extension_access_modifier - - fatal_error_message - - first_where - - function_default_parameter_at_end - - identical_operands - - joined_default_parameter - - legacy_random - - let_var_whitespace - - literal_expression_end_indentation - - lower_acl_than_parent - - modifier_order - - multiline_arguments - - multiline_function_chains - - multiline_parameters - - operator_usage_whitespace - - operator_whitespace - - overridden_super_call - - override_in_extension - - prohibited_super_call - - redundant_nil_coalescing - - redundant_type_annotation - - sorted_first_last - - static_operator - - toggle_bool - - trailing_closure - - untyped_error_in_catch - - vertical_parameter_alignment_on_call - - vertical_whitespace_closing_braces - - vertical_whitespace_opening_braces - - xctfail_message - - yoda_condition -disabled_rules: - - file_length - - for_where - - force_cast - - function_body_length - - function_parameter_count - - identifier_name - - large_tuple - - line_length - - nesting - - todo - - trailing_whitespace - - type_body_length -custom_rules: - assert_equal_to_nil: - name: Assert Equal to Nil - regex: XCTAssertEqual\([^\n]+,\s*nil\) - message: Use 'XCTAssertNil' instead. From ed459a87cb3d43d0b5e2417b152060fc6bab1bea Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Wed, 25 Sep 2019 10:53:15 -0500 Subject: [PATCH 142/147] add symbolic links to single .swiftlint.yml file. --- Examples/.swiftlint.yml | 1 + Toolkit/.swiftlint.yml | 1 + 2 files changed, 2 insertions(+) create mode 120000 Examples/.swiftlint.yml create mode 120000 Toolkit/.swiftlint.yml diff --git a/Examples/.swiftlint.yml b/Examples/.swiftlint.yml new file mode 120000 index 00000000..9e225e41 --- /dev/null +++ b/Examples/.swiftlint.yml @@ -0,0 +1 @@ +../.swiftlint.yml \ No newline at end of file diff --git a/Toolkit/.swiftlint.yml b/Toolkit/.swiftlint.yml new file mode 120000 index 00000000..9e225e41 --- /dev/null +++ b/Toolkit/.swiftlint.yml @@ -0,0 +1 @@ +../.swiftlint.yml \ No newline at end of file From 9d9a9444295bcbaa7a59c187507fd13b82590eac Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Thu, 26 Sep 2019 09:57:50 -0500 Subject: [PATCH 143/147] PR review changes --- .swiftlint.yml | 6 ------ Examples/ArcGISToolkitExamples/VCListViewController.swift | 3 +-- README.md | 2 +- 3 files changed, 2 insertions(+), 9 deletions(-) diff --git a/.swiftlint.yml b/.swiftlint.yml index 248ca5fd..a85ee760 100644 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -1,4 +1,3 @@ -included: opt_in_rules: - anyobject_protocol - array_init @@ -62,8 +61,3 @@ disabled_rules: - todo - trailing_whitespace - type_body_length -custom_rules: - assert_equal_to_nil: - name: Assert Equal to Nil - regex: XCTAssertEqual\([^\n]+,\s*nil\) - message: Use 'XCTAssertNil' instead. diff --git a/Examples/ArcGISToolkitExamples/VCListViewController.swift b/Examples/ArcGISToolkitExamples/VCListViewController.swift index 52f79559..f080ce49 100644 --- a/Examples/ArcGISToolkitExamples/VCListViewController.swift +++ b/Examples/ArcGISToolkitExamples/VCListViewController.swift @@ -17,8 +17,7 @@ import ArcGISToolkit open class VCListViewController: UITableViewController { public var storyboardName: String? - public var viewControllerInfos: [(vcName: String, viewControllerType: UIViewController.Type, nibName: String?)] = [ - ] { + public var viewControllerInfos: [(vcName: String, viewControllerType: UIViewController.Type, nibName: String?)] = [] { didSet { self.tableView.reloadData() } diff --git a/README.md b/README.md index b555e539..2cb08fab 100644 --- a/README.md +++ b/README.md @@ -51,7 +51,7 @@ Note that you must also have the __ArcGIS Runtime SDK for iOS__ installed and yo 5. Add `import ArcGISToolkit` in your source code and start using the toolkit components ## SwiftLint -New in the 100.6.0 release is SwiftLint support for both the Toolkit and Examples app. You will need to install SwiftLint from [here](https://github.com/realm/SwiftLint) in order to build. The specific rules the linter uses can be found in the `swiftlint.yml` files in the `Toolkit` and `Examples` directories. +New in the 100.6.0 release is SwiftLint support for both the Toolkit and Examples app. You can install SwiftLint from [here](https://github.com/realm/SwiftLint). It is not necessary to have it installed in order to build, but you will get a warning without it. The specific rules the linter uses can be found in the `swiftlint.yml` files in the `Toolkit` and `Examples` directories. ## Additional Resources From 281683e9b50a47490bcf69728e86b478e1b86b06 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Thu, 26 Sep 2019 10:28:59 -0500 Subject: [PATCH 144/147] Lint AR files. --- .../ArcGISToolkitExamples/ARExample.swift | 109 ++++++++---------- .../Misc/ARStatusViewController.swift | 33 +++--- .../Misc/CalibrationView.swift | 35 +++--- .../ArcGISToolkitExamples/Misc/Plane.swift | 4 +- .../Misc/UserDirectionsView.swift | 5 +- Toolkit/ArcGISToolkit/AR/ArcGISARView.swift | 43 +++---- 6 files changed, 106 insertions(+), 123 deletions(-) diff --git a/Examples/ArcGISToolkitExamples/ARExample.swift b/Examples/ArcGISToolkitExamples/ARExample.swift index 2d73b1e1..a6ddf9fd 100644 --- a/Examples/ArcGISToolkitExamples/ARExample.swift +++ b/Examples/ArcGISToolkitExamples/ARExample.swift @@ -17,7 +17,6 @@ import ArcGISToolkit import ArcGIS class ARExample: UIViewController { - typealias SceneInitFunction = () -> AGSScene typealias SceneInfoType = (sceneFunction: SceneInitFunction, label: String, tableTop: Bool, trackingMode: ARLocationTrackingMode) @@ -97,7 +96,7 @@ class ARExample: UIViewController { arView.sceneView.graphicsOverlays.add(graphicsOverlay) // Observe the `arView.translationFactor` property and update status when it changes. - translationFactorObservation = arView.observe(\ArcGISARView.translationFactor, options: [.initial, .new]) { [weak self] arView, change in + translationFactorObservation = arView.observe(\ArcGISARView.translationFactor, options: [.initial, .new]) { [weak self] arView, _ in self?.statusViewController?.translationFactor = arView.translationFactor } @@ -109,7 +108,7 @@ class ARExample: UIViewController { arView.trailingAnchor.constraint(equalTo: view.trailingAnchor), arView.topAnchor.constraint(equalTo: view.topAnchor), arView.bottomAnchor.constraint(equalTo: view.bottomAnchor) - ]) + ]) // Add a Toolbar for displaying user controls. addToolbar() @@ -143,9 +142,9 @@ class ARExample: UIViewController { override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) - arView.startTracking(currentSceneInfo?.trackingMode ?? .ignore, completion: { [weak self] (error) in + arView.startTracking(currentSceneInfo?.trackingMode ?? .ignore) { [weak self] (error) in self?.statusViewController?.errorMessage = error?.localizedDescription - }) + } } override func viewDidDisappear(_ animated: Bool) { @@ -158,8 +157,8 @@ class ARExample: UIViewController { /// Initialize scene location/heading/elevation calibration. /// /// - Parameter sender: The bar button item tapped on. - @objc func displayCalibration(_ sender: UIBarButtonItem) { - + @objc + func displayCalibration(_ sender: UIBarButtonItem) { // If the sceneView's alpha is 0.0, that means we are not in calibration mode and we need to start calibrating. let startCalibrating = (calibrationView?.alpha == 0.0) @@ -168,18 +167,19 @@ class ARExample: UIViewController { userDirectionsView.updateUserDirections(nil) // Display calibration view. - UIView.animate(withDuration: 0.25, animations: { - if startCalibrating { - self.arView.sceneView.isAttributionTextVisible = false - self.addCalibrationView() - } - self.calibrationView?.alpha = startCalibrating ? 1.0 : 0.0 - }) { (_) in - if !startCalibrating { - self.removeCalibrationView() - self.arView.sceneView.isAttributionTextVisible = true - } - } + UIView.animate(withDuration: 0.25, + animations: { + if startCalibrating { + self.arView.sceneView.isAttributionTextVisible = false + self.addCalibrationView() + } + }, + completion: { (_) in + if !startCalibrating { + self.removeCalibrationView() + self.arView.sceneView.isAttributionTextVisible = true + } + }) // Dim the sceneView if we're calibrating. UIView.animate(withDuration: 0.25) { [weak self] in @@ -197,7 +197,6 @@ class ARExample: UIViewController { /// /// - Parameter sceneInfo: The sceneInfo used to set up the scene and AR experience. fileprivate func selectSceneInfo(_ sceneInfo: SceneInfoType) { - title = sceneInfo.label // Set currentSceneInfo to the selected scene info. @@ -213,9 +212,9 @@ class ARExample: UIViewController { // Reset AR tracking and then start tracking. arView.resetTracking() - arView.startTracking(sceneInfo.trackingMode, completion: { [weak self] (error) in + arView.startTracking(sceneInfo.trackingMode) { [weak self] (error) in self?.statusViewController?.errorMessage = error?.localizedDescription - }) + } // Disable elevation control if we're using continuous GPS. calibrationView?.elevationControlVisibility = (sceneInfo.trackingMode != .continuous) @@ -230,16 +229,17 @@ class ARExample: UIViewController { /// Allow users to change the current scene. /// /// - Parameter sender: The bar button item tapped on. - @objc func changeScene(_ sender: UIBarButtonItem) { + @objc + func changeScene(_ sender: UIBarButtonItem) { // Display an alert controller displaying the scenes to choose from. let alertController = UIAlertController(title: nil, message: nil, preferredStyle: UIAlertController.Style.actionSheet) alertController.popoverPresentationController?.barButtonItem = sender // Loop through all sceneInfos and add `UIAlertActions` for each. sceneInfo.forEach { info in - let action = UIAlertAction(title: info.label, style: .default, handler: { [weak self] (action) in + let action = UIAlertAction(title: info.label, style: .default) { [weak self] (_) in self?.selectSceneInfo(info) - }) + } // Display current scene as disabled. action.isEnabled = (info.label != currentSceneInfo?.label) @@ -256,7 +256,8 @@ class ARExample: UIViewController { /// Dislays the status view controller. /// /// - Parameter sender: The bar button item tapped on. - @objc func showStatus(_ sender: UIBarButtonItem) { + @objc + func showStatus(_ sender: UIBarButtonItem) { UIView.animate(withDuration: 0.25) { [weak self] in self?.statusViewController?.view.alpha = self?.statusViewController?.view.alpha == 1.0 ? 0.0 : 1.0 } @@ -271,7 +272,7 @@ class ARExample: UIViewController { toolbar.leadingAnchor.constraint(equalTo: view.leadingAnchor), toolbar.trailingAnchor.constraint(equalTo: view.trailingAnchor), toolbar.bottomAnchor.constraint(equalTo: arView.sceneView.attributionTopAnchor) - ]) + ]) // Create a toolbar button to display the status. let statusItem = UIBarButtonItem(title: "Status", style: .plain, target: self, action: #selector(showStatus(_:))) @@ -295,7 +296,7 @@ class ARExample: UIViewController { statusVC.view.widthAnchor.constraint(equalToConstant: 350), statusVC.view.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -8), statusVC.view.bottomAnchor.constraint(equalTo: toolbar.topAnchor, constant: -8) - ]) + ]) statusVC.view.alpha = 0.0 } @@ -304,7 +305,6 @@ class ARExample: UIViewController { // MARK: ARSCNViewDelegate extension ARExample: ARSCNViewDelegate { - func renderer(_ renderer: SCNSceneRenderer, didAdd node: SCNNode, for anchor: ARAnchor) { // Place content only for anchors found by plane detection. guard let planeAnchor = anchor as? ARPlaneAnchor else { return } @@ -342,7 +342,7 @@ extension ARExample: ARSCNViewDelegate { ] // Remove optional error messages. - let errorMessage = messages.compactMap({ $0 }).joined(separator: "\n") + let errorMessage = messages.compactMap { $0 }.joined(separator: "\n") // Set the error message on the status vc. statusViewController?.errorMessage = errorMessage @@ -351,9 +351,9 @@ extension ARExample: ARSCNViewDelegate { // Present an alert describing the error. let alertController = UIAlertController(title: "Could not start tracking.", message: errorMessage, preferredStyle: .alert) let restartAction = UIAlertAction(title: "Restart Tracking", style: .default) { _ in - self?.arView.startTracking(self?.currentSceneInfo?.trackingMode ?? .ignore, completion: { (error) in + self?.arView.startTracking(self?.currentSceneInfo?.trackingMode ?? .ignore) { (error) in self?.statusViewController?.errorMessage = error?.localizedDescription - }) + } } alertController.addAction(restartAction) @@ -403,8 +403,7 @@ extension ARExample: AGSGeoViewTouchDelegate { userDirectionsView.updateUserDirections(nil) didPlaceScene = true } - } - else { + } else { // We're in full-scale AR mode or have already placed the scene. Get the real world location for screen point from arView. guard let point = arView.arScreenToLocation(screenPoint: screenPoint) else { return } @@ -423,7 +422,6 @@ extension ARExample: AGSGeoViewTouchDelegate { // MARK: User Directions View extension ARExample { - /// Add user directions view to view and setup constraints. func addUserDirectionsView() { view.addSubview(userDirectionsView) @@ -431,7 +429,7 @@ extension ARExample { NSLayoutConstraint.activate([ userDirectionsView.centerXAnchor.constraint(equalTo: view.centerXAnchor), userDirectionsView.topAnchor.constraint(equalToSystemSpacingBelow: view.safeAreaLayoutGuide.topAnchor, multiplier: 1) - ]) + ]) } /// Update the displayed message in the user directions view for the current frame and tracking state. @@ -454,15 +452,14 @@ extension ARExample { case .notAvailable: message = "Location not available." case .limited(let reason): - switch(reason) { + switch reason { case .excessiveMotion: message = "Try moving your device more slowly." case .initializing: // Because ARKit gets reset often when using continuous GPS, only dipslay initializing message if we're not in continuous tracking mode. if let sceneInfo = currentSceneInfo, sceneInfo.trackingMode != .continuous { message = "Keep moving your device." - } - else { + } else { message = "" } case .insufficientFeatures: @@ -478,7 +475,6 @@ extension ARExample { // MARK: Calibration View extension ARExample { - /// Add the calibration view to the view and setup constraints. func addCalibrationView() { guard let calibrationView = calibrationView else { return } @@ -489,7 +485,7 @@ extension ARExample { calibrationView.trailingAnchor.constraint(equalTo: view.trailingAnchor), calibrationView.topAnchor.constraint(equalTo: view.topAnchor), calibrationView.bottomAnchor.constraint(equalTo: toolbar.topAnchor) - ]) + ]) } /// Add the calibration view to the view and setup constraints. @@ -510,7 +506,6 @@ extension ARExample { /// /// - Returns: The new scene. private func streetsScene() -> AGSScene { - // Create scene with the streets basemap. let scene = AGSScene(basemapType: .streets) scene.addElevationSource() @@ -526,7 +521,6 @@ extension ARExample { /// /// - Returns: The new scene. private func imageryScene() -> AGSScene { - // Create scene with the streets basemap. let scene = AGSScene(basemapType: .imageryWithLabels) scene.addElevationSource() @@ -576,11 +570,11 @@ extension ARExample { scene.addElevationSource() // Create the Yosemite layer. - let layer = AGSIntegratedMeshLayer(url: URL(string:"https://tiles.arcgis.com/tiles/FQD0rKU8X5sAQfh8/arcgis/rest/services/VRICON_Yosemite_Sample_Integrated_Mesh_scene_layer/SceneServer")!) + let layer = AGSIntegratedMeshLayer(url: URL(string: "https://tiles.arcgis.com/tiles/FQD0rKU8X5sAQfh8/arcgis/rest/services/VRICON_Yosemite_Sample_Integrated_Mesh_scene_layer/SceneServer")!) scene.operationalLayers.add(layer) scene.load { [weak self, weak scene] (error) in self?.statusViewController?.errorMessage = error?.localizedDescription - if let _ = error { + if error != nil { return } @@ -591,14 +585,14 @@ extension ARExample { scene?.baseSurface?.elevationSources.first?.load { (error) in self?.statusViewController?.errorMessage = error?.localizedDescription - if let _ = error { + if error != nil { return } // Find the elevation of the layer at the center point. - scene?.baseSurface?.elevation(for: center, completion: { (elevation, error) in + scene?.baseSurface?.elevation(for: center) { (elevation, error) in self?.statusViewController?.errorMessage = error?.localizedDescription - if let _ = error { + if error != nil { return } @@ -606,7 +600,7 @@ extension ARExample { let camera = AGSCamera(latitude: center.y, longitude: center.x, altitude: elevation, heading: 0, pitch: 90, roll: 0) self?.arView.originCamera = camera self?.arView.translationFactor = 18000 - }) + } } } @@ -624,11 +618,11 @@ extension ARExample { scene.addElevationSource() // Create the border layer. - let layer = AGSIntegratedMeshLayer(url: URL(string:"https://tiles.arcgis.com/tiles/FQD0rKU8X5sAQfh8/arcgis/rest/services/VRICON_SW_US_Sample_Integrated_Mesh_scene_layer/SceneServer")!) + let layer = AGSIntegratedMeshLayer(url: URL(string: "https://tiles.arcgis.com/tiles/FQD0rKU8X5sAQfh8/arcgis/rest/services/VRICON_SW_US_Sample_Integrated_Mesh_scene_layer/SceneServer")!) scene.operationalLayers.add(layer) scene.load { [weak self, weak scene] (error) in self?.statusViewController?.errorMessage = error?.localizedDescription - if let _ = error { + if error != nil { return } @@ -639,14 +633,14 @@ extension ARExample { scene?.baseSurface?.elevationSources.first?.load { (error) in self?.statusViewController?.errorMessage = error?.localizedDescription - if let _ = error { + if error != nil { return } // Find the elevation of the layer at the center point. - scene?.baseSurface?.elevation(for: center, completion: { (elevation, error) in + scene?.baseSurface?.elevation(for: center) { (elevation, error) in self?.statusViewController?.errorMessage = error?.localizedDescription - if let _ = error { + if error != nil { return } @@ -654,7 +648,7 @@ extension ARExample { let camera = AGSCamera(latitude: center.y, longitude: center.x, altitude: elevation, heading: 0, pitch: 90.0, roll: 0) self?.arView.originCamera = camera self?.arView.translationFactor = 1000 - }) + } } } @@ -683,7 +677,7 @@ extension AGSScene { /// Adds an elevation source to the given `scene`. /// /// - Parameter scene: The scene to add the elevation source to. - public func addElevationSource() { + func addElevationSource() { let elevationSource = AGSArcGISTiledElevationSource(url: URL(string: "https://elevation3d.arcgis.com/arcgis/rest/services/WorldElevation3D/Terrain3D/ImageServer")!) let surface = AGSSurface() surface.elevationSources = [elevationSource] @@ -697,7 +691,7 @@ extension AGSScene { // MARK: AGSLocationChangeHandlerDelegate methods extension ARExample: AGSLocationChangeHandlerDelegate { - public func locationDataSource(_ locationDataSource: AGSLocationDataSource, locationDidChange location: AGSLocation) { + func locationDataSource(_ locationDataSource: AGSLocationDataSource, locationDidChange location: AGSLocation) { // When we get a new location, update the status view controller with the new horizontal and vertical accuracy. statusViewController?.horizontalAccuracyMeasurement.value = location.horizontalAccuracy statusViewController?.verticalAccuracyMeasurement.value = location.verticalAccuracy @@ -708,4 +702,3 @@ extension ARExample: AGSLocationChangeHandlerDelegate { statusViewController?.locationDataSourceStatus = status } } - diff --git a/Examples/ArcGISToolkitExamples/Misc/ARStatusViewController.swift b/Examples/ArcGISToolkitExamples/Misc/ARStatusViewController.swift index eab062dd..f3ea06ac 100644 --- a/Examples/ArcGISToolkitExamples/Misc/ARStatusViewController.swift +++ b/Examples/ArcGISToolkitExamples/Misc/ARStatusViewController.swift @@ -51,7 +51,6 @@ extension AGSLocationDataSourceStatus { /// A view controller for display AR-related status information. class ARStatusViewController: UITableViewController { - @IBOutlet var trackingStateLabel: UILabel! @IBOutlet var frameRateLabel: UILabel! @IBOutlet var errorDescriptionLabel: UILabel! @@ -62,9 +61,9 @@ class ARStatusViewController: UITableViewController { @IBOutlet var locationDataSourceStatusLabel: UILabel! /// The `ARKit` camera tracking state. - public var trackingState: ARCamera.TrackingState = .notAvailable { + var trackingState: ARCamera.TrackingState = .notAvailable { didSet { - DispatchQueue.main.async{ [weak self] in + DispatchQueue.main.async { [weak self] in guard let self = self else { return } self.trackingStateLabel?.text = self.trackingState.description } @@ -72,9 +71,9 @@ class ARStatusViewController: UITableViewController { } /// The calculated frame rate of the `SceneView` and `ARKit` display. - public var frameRate: Int = 0 { + var frameRate: Int = 0 { didSet { - DispatchQueue.main.async{ [weak self] in + DispatchQueue.main.async { [weak self] in guard let self = self else { return } self.frameRateLabel?.text = "\(self.frameRate) fps" } @@ -82,9 +81,9 @@ class ARStatusViewController: UITableViewController { } /// The current error message. - public var errorMessage: String? { + var errorMessage: String? { didSet { - DispatchQueue.main.async{ [weak self] in + DispatchQueue.main.async { [weak self] in guard let self = self else { return } self.errorDescriptionLabel?.text = self.errorMessage } @@ -92,9 +91,9 @@ class ARStatusViewController: UITableViewController { } /// The label for the currently selected scene. - public var currentScene: String = "None" { + var currentScene: String = "None" { didSet { - DispatchQueue.main.async{ [weak self] in + DispatchQueue.main.async { [weak self] in guard let self = self else { return } self.sceneLabel?.text = self.currentScene } @@ -102,9 +101,9 @@ class ARStatusViewController: UITableViewController { } /// The translation factor applied to the current scene. - public var translationFactor: Double = 1.0 { + var translationFactor: Double = 1.0 { didSet { - DispatchQueue.main.async{ [weak self] in + DispatchQueue.main.async { [weak self] in guard let self = self else { return } self.translationFactorLabel?.text = String(format: "%.1f", self.translationFactor) } @@ -112,9 +111,9 @@ class ARStatusViewController: UITableViewController { } /// The horizontal accuracy of the last location. - public var horizontalAccuracyMeasurement = Measurement(value: 1, unit: UnitLength.meters) { + var horizontalAccuracyMeasurement = Measurement(value: 1, unit: UnitLength.meters) { didSet { - DispatchQueue.main.async{ [weak self] in + DispatchQueue.main.async { [weak self] in guard let self = self else { return } self.horizontalAccuracyLabel?.text = self.measurementFormatter.string(from: self.horizontalAccuracyMeasurement) } @@ -122,9 +121,9 @@ class ARStatusViewController: UITableViewController { } /// The vertical accuracy of the last location. - public var verticalAccuracyMeasurement = Measurement(value: 1, unit: UnitLength.meters) { + var verticalAccuracyMeasurement = Measurement(value: 1, unit: UnitLength.meters) { didSet { - DispatchQueue.main.async{ [weak self] in + DispatchQueue.main.async { [weak self] in guard let self = self else { return } self.verticalAccuracyLabel?.text = self.measurementFormatter.string(from: self.verticalAccuracyMeasurement) } @@ -132,9 +131,9 @@ class ARStatusViewController: UITableViewController { } /// The status of the location data source. - public var locationDataSourceStatus: AGSLocationDataSourceStatus = .stopped { + var locationDataSourceStatus: AGSLocationDataSourceStatus = .stopped { didSet { - DispatchQueue.main.async{ [weak self] in + DispatchQueue.main.async { [weak self] in guard let self = self else { return } self.locationDataSourceStatusLabel?.text = self.locationDataSourceStatus.description } diff --git a/Examples/ArcGISToolkitExamples/Misc/CalibrationView.swift b/Examples/ArcGISToolkitExamples/Misc/CalibrationView.swift index 46f05a34..97541e50 100644 --- a/Examples/ArcGISToolkitExamples/Misc/CalibrationView.swift +++ b/Examples/ArcGISToolkitExamples/Misc/CalibrationView.swift @@ -18,9 +18,8 @@ import ArcGISToolkit /// A view displaying controls for adjusting a scene view's location, heading, and elevation. Used to calibrate an AR session. class CalibrationView: UIView { - /// Denotes whether to show the elevation control and label; defaults to `true`. - public var elevationControlVisibility: Bool = true { + var elevationControlVisibility: Bool = true { didSet { elevationSlider.isHidden = !elevationControlVisibility elevationLabel.isHidden = !elevationControlVisibility @@ -82,7 +81,7 @@ class CalibrationView: UIView { calibrationDirectionsLabel.trailingAnchor.constraint(equalTo: labelView.trailingAnchor, constant: -8), calibrationDirectionsLabel.topAnchor.constraint(equalTo: labelView.topAnchor, constant: 8), calibrationDirectionsLabel.bottomAnchor.constraint(equalTo: labelView.bottomAnchor, constant: -8) - ]) + ]) // Add the label view to our view and set up constraints. addSubview(labelView) @@ -90,7 +89,7 @@ class CalibrationView: UIView { NSLayoutConstraint.activate([ labelView.centerXAnchor.constraint(equalTo: centerXAnchor), labelView.topAnchor.constraint(equalTo: safeAreaLayoutGuide.topAnchor, constant: 8.0) - ]) + ]) // Add the heading label and slider. let headingLabel = UILabel(frame: .zero) @@ -101,7 +100,7 @@ class CalibrationView: UIView { NSLayoutConstraint.activate([ headingLabel.leadingAnchor.constraint(equalTo: safeAreaLayoutGuide.leadingAnchor, constant: 16), headingLabel.bottomAnchor.constraint(equalTo: safeAreaLayoutGuide.bottomAnchor, constant: -16) - ]) + ]) addSubview(headingSlider) headingSlider.translatesAutoresizingMaskIntoConstraints = false @@ -109,7 +108,7 @@ class CalibrationView: UIView { headingSlider.leadingAnchor.constraint(equalTo: headingLabel.trailingAnchor, constant: 16), headingSlider.trailingAnchor.constraint(equalTo: safeAreaLayoutGuide.trailingAnchor, constant: -16), headingSlider.centerYAnchor.constraint(equalTo: headingLabel.centerYAnchor) - ]) + ]) // Add the elevation label and slider. elevationLabel.text = "Elevation" @@ -119,7 +118,7 @@ class CalibrationView: UIView { NSLayoutConstraint.activate([ elevationLabel.leadingAnchor.constraint(equalTo: safeAreaLayoutGuide.leadingAnchor, constant: 16), elevationLabel.bottomAnchor.constraint(equalTo: headingLabel.topAnchor, constant: -24) - ]) + ]) addSubview(elevationSlider) elevationSlider.translatesAutoresizingMaskIntoConstraints = false @@ -127,7 +126,7 @@ class CalibrationView: UIView { elevationSlider.leadingAnchor.constraint(equalTo: elevationLabel.trailingAnchor, constant: 16), elevationSlider.trailingAnchor.constraint(equalTo: safeAreaLayoutGuide.trailingAnchor, constant: -16), elevationSlider.centerYAnchor.constraint(equalTo: elevationLabel.centerYAnchor) - ]) + ]) // Setup actions for the two sliders. The sliders operate as "joysticks", where moving the slider thumb will start a timer // which roates or elevates the current camera when the timer fires. The elevation and heading delta @@ -166,14 +165,15 @@ class CalibrationView: UIView { /// Handle an elevation slider value-changed event. /// /// - Parameter sender: The slider tapped on. - @objc func elevationChanged(_ sender: UISlider) { + @objc + func elevationChanged(_ sender: UISlider) { if elevationTimer == nil { // Create a timer which elevates the camera when fired. - elevationTimer = Timer(timeInterval: 0.25, repeats: true, block: { [weak self] (timer) in + elevationTimer = Timer(timeInterval: 0.25, repeats: true) { [weak self] (_) in let delta = self?.joystickElevation() ?? 0.0 // print("elevate delta = \(delta)") self?.elevate(delta) - }) + } // Add the timer to the main run loop. guard let timer = elevationTimer else { return } @@ -184,14 +184,15 @@ class CalibrationView: UIView { /// Handle an heading slider value-changed event. /// /// - Parameter sender: The slider tapped on. - @objc func headingChanged(_ sender: UISlider) { + @objc + func headingChanged(_ sender: UISlider) { if headingTimer == nil { // Create a timer which rotates the camera when fired. - headingTimer = Timer(timeInterval: 0.1, repeats: true, block: { [weak self] (timer) in + headingTimer = Timer(timeInterval: 0.1, repeats: true) { [weak self] (_) in let delta = self?.joystickHeading() ?? 0.0 // print("rotate delta = \(delta)") self?.rotate(delta) - }) + } // Add the timer to the main run loop. guard let timer = headingTimer else { return } @@ -202,7 +203,8 @@ class CalibrationView: UIView { /// Handle an elevation slider touchUp event. This will stop the timer. /// /// - Parameter sender: The slider tapped on. - @objc func touchUpElevation(_ sender: UISlider) { + @objc + func touchUpElevation(_ sender: UISlider) { elevationTimer?.invalidate() elevationTimer = nil sender.value = 0.0 @@ -211,7 +213,8 @@ class CalibrationView: UIView { /// Handle a heading slider touchUp event. This will stop the timer. /// /// - Parameter sender: The slider tapped on. - @objc func touchUpHeading(_ sender: UISlider) { + @objc + func touchUpHeading(_ sender: UISlider) { headingTimer?.invalidate() headingTimer = nil sender.value = 0.0 diff --git a/Examples/ArcGISToolkitExamples/Misc/Plane.swift b/Examples/ArcGISToolkitExamples/Misc/Plane.swift index 84787574..3d65a572 100644 --- a/Examples/ArcGISToolkitExamples/Misc/Plane.swift +++ b/Examples/ArcGISToolkitExamples/Misc/Plane.swift @@ -16,11 +16,11 @@ import ARKit /// Helper class to visualize a plane found by ARKit class Plane: SCNNode { - let node: SCNNode + init(anchor: ARPlaneAnchor, in sceneView: ARSCNView) { // Create a node to visualize the plane's bounding rectangle. - let extent: SCNPlane = SCNPlane(width: CGFloat(anchor.extent.x), height: CGFloat(anchor.extent.z)) + let extent = SCNPlane(width: CGFloat(anchor.extent.x), height: CGFloat(anchor.extent.z)) node = SCNNode(geometry: extent) node.simdPosition = anchor.center diff --git a/Examples/ArcGISToolkitExamples/Misc/UserDirectionsView.swift b/Examples/ArcGISToolkitExamples/Misc/UserDirectionsView.swift index 7dc54f46..abbe5160 100644 --- a/Examples/ArcGISToolkitExamples/Misc/UserDirectionsView.swift +++ b/Examples/ArcGISToolkitExamples/Misc/UserDirectionsView.swift @@ -16,7 +16,6 @@ import UIKit /// A custom view for dislaying directions to the user. class UserDirectionsView: UIVisualEffectView { - private let userDirectionsLabel: UILabel = { let label = UILabel(frame: .zero) label.textAlignment = .center @@ -41,7 +40,7 @@ class UserDirectionsView: UIVisualEffectView { userDirectionsLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -8), userDirectionsLabel.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 8), userDirectionsLabel.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -8) - ]) + ]) } required init?(coder aDecoder: NSCoder) { @@ -51,7 +50,7 @@ class UserDirectionsView: UIVisualEffectView { /// Updates the displayed user directions string. If `message` is nil or empty, this will hide the view. If `message` is not empty, it will display the view. /// /// - Parameter message: the new string to display. - public func updateUserDirections(_ message: String?) { + func updateUserDirections(_ message: String?) { UIView.animate(withDuration: 0.25) { [weak self] in self?.alpha = (message?.isEmpty ?? true) ? 0.0 : 1.0 self?.userDirectionsLabel.text = message diff --git a/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift b/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift index 8daee738..b3d2958e 100644 --- a/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift +++ b/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift @@ -28,7 +28,6 @@ public enum ARLocationTrackingMode { } public class ArcGISARView: UIView { - // MARK: public properties /// The view used to display the `ARKit` camera image and 3D `SceneKit` content. @@ -57,7 +56,7 @@ public class ArcGISARView: UIView { /// The viewpoint camera used to set the initial view of the sceneView instead of the device's GPS location via the location data source. You can use Key-Value Observing to track changes to the origin camera. /// - Since: 100.6.0 - @objc dynamic public var originCamera: AGSCamera { + @objc public dynamic var originCamera: AGSCamera { get { return cameraController.originCamera } @@ -72,7 +71,7 @@ public class ArcGISARView: UIView { /// The translation factor used to support a table top AR experience. /// - Since: 100.6.0 - @objc dynamic public var translationFactor: Double { + @objc public dynamic var translationFactor: Double { get { return cameraController.translationFactor } @@ -99,11 +98,11 @@ public class ArcGISARView: UIView { /// We implement `ARSCNViewDelegate` methods, but will use `arSCNViewDelegate` to forward them to clients. /// - Since: 100.6.0 - weak public var arSCNViewDelegate: ARSCNViewDelegate? + public weak var arSCNViewDelegate: ARSCNViewDelegate? /// We implement `AGSLocationChangeHandlerDelegate` methods, but will use `locationChangeHandlerDelegate` to forward them to clients. /// - Since: 100.6.0 - weak public var locationChangeHandlerDelegate: AGSLocationChangeHandlerDelegate? + public weak var locationChangeHandlerDelegate: AGSLocationChangeHandlerDelegate? // MARK: Private properties @@ -126,12 +125,12 @@ public class ArcGISARView: UIView { // MARK: Initializers - public override init(frame: CGRect) { + override public init(frame: CGRect) { super.init(frame: frame) sharedInitialization() } - required public init?(coder aDecoder: NSCoder) { + public required init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) sharedInitialization() } @@ -141,7 +140,7 @@ public class ArcGISARView: UIView { /// - Parameters: /// - renderVideoFeed: Whether to display the live camera image. /// - Since: 100.6.0 - public convenience init(renderVideoFeed: Bool){ + public convenience init(renderVideoFeed: Bool) { self.init(frame: .zero) if !isUsingARKit || !renderVideoFeed { @@ -160,7 +159,7 @@ public class ArcGISARView: UIView { } /// Initialization code shared between all initializers. - private func sharedInitialization(){ + private func sharedInitialization() { // Add the ARSCNView to our view. if deviceSupportsARKit { addSubviewWithConstraints(arSCNView) @@ -188,13 +187,11 @@ public class ArcGISARView: UIView { /// /// - Parameter key: The key we want to observe. /// - Returns: A set of key paths for properties whose values affect the value of the specified key. - public override class func keyPathsForValuesAffectingValue(forKey key: String) -> Set - { + override public class func keyPathsForValuesAffectingValue(forKey key: String) -> Set { var set = super.keyPathsForValuesAffectingValue(forKey: key) if key == #keyPath(translationFactor) { set.insert(#keyPath(cameraController.translationFactor)) - } - else if key == #keyPath(originCamera) { + } else if key == #keyPath(originCamera) { set.insert(#keyPath(cameraController.originCamera)) } @@ -270,8 +267,7 @@ public class ArcGISARView: UIView { } completion?(error) } - } - else { + } else { // We're either ignoring the data source or there is no data source so continue with defaults. finalizeStart() completion?(nil) @@ -313,7 +309,7 @@ public class ArcGISARView: UIView { subview.trailingAnchor.constraint(equalTo: self.trailingAnchor), subview.topAnchor.constraint(equalTo: self.topAnchor), subview.bottomAnchor.constraint(equalTo: self.bottomAnchor) - ]) + ]) } /// Internal method to perform a hit test operation to get the transformation matrix representing the corresponding real-world point for `screenPoint`. @@ -345,7 +341,6 @@ public class ArcGISARView: UIView { // MARK: - ARSCNViewDelegate extension ArcGISARView: ARSCNViewDelegate { - // This is not implemented as we are letting ARKit create and manage nodes. // If you want to manage your own nodes, uncomment this and implement it in your code. // public func renderer(_ renderer: SCNSceneRenderer, nodeFor anchor: ARAnchor) -> SCNNode? { @@ -371,7 +366,6 @@ extension ArcGISARView: ARSCNViewDelegate { // MARK: - ARSessionObserver (via ARSCNViewDelegate) extension ArcGISARView: ARSessionObserver { - public func session(_ session: ARSession, didFailWithError error: Error) { arSCNViewDelegate?.session?(session, didFailWithError: error) } @@ -400,7 +394,6 @@ extension ArcGISARView: ARSessionObserver { // MARK: - SCNSceneRendererDelegate (via ARSCNViewDelegate) extension ArcGISARView: SCNSceneRendererDelegate { - public func renderer(_ renderer: SCNSceneRenderer, updateAtTime time: TimeInterval) { arSCNViewDelegate?.renderer?(renderer, updateAtTime: time) } @@ -425,7 +418,7 @@ extension ArcGISARView: SCNSceneRendererDelegate { guard let transform = arSCNView.pointOfView?.transform else { return } let cameraTransform = simd_double4x4(transform) - let cameraQuat:simd_quatd = simd_quatd(cameraTransform) + let cameraQuat = simd_quatd(cameraTransform) let transformationMatrix = AGSTransformationMatrix(quaternionX: cameraQuat.vector.x, quaternionY: cameraQuat.vector.y, quaternionZ: cameraQuat.vector.z, @@ -470,7 +463,6 @@ extension ArcGISARView: SCNSceneRendererDelegate { // MARK: - AGSLocationChangeHandlerDelegate extension ArcGISARView: AGSLocationChangeHandlerDelegate { - public func locationDataSource(_ locationDataSource: AGSLocationDataSource, headingDidChange heading: Double) { // Heading changed. if !isUsingARKit { @@ -494,8 +486,7 @@ extension ArcGISARView: AGSLocationChangeHandlerDelegate { location.verticalAccuracy >= 0 { let altitude = location.altitude locationPoint = AGSPoint(x: locationPoint.x, y: locationPoint.y, z: altitude, spatialReference: locationPoint.spatialReference) - } - else { + } else { // We don't have a valid altitude, so use the old altitude. let oldLocationPoint = originCamera.location locationPoint = AGSPoint(x: locationPoint.x, y: locationPoint.y, z: oldLocationPoint.z, spatialReference: locationPoint.spatialReference) @@ -511,8 +502,7 @@ extension ArcGISARView: AGSLocationChangeHandlerDelegate { let newCamera = AGSCamera(location: locationPoint, heading: 0.0, pitch: 90.0, roll: 0.0) originCamera = newCamera didSetInitialLocation = true - } - else if locationTrackingMode == .continuous { + } else if locationTrackingMode == .continuous { originCamera = AGSCamera(location: locationPoint, heading: originCamera.heading, pitch: originCamera.pitch, roll: originCamera.roll) } @@ -524,7 +514,7 @@ extension ArcGISARView: AGSLocationChangeHandlerDelegate { // Reset the camera controller's transformationMatrix to its initial state, the Idenity matrix. cameraController.transformationMatrix = .identity - if (locationTrackingMode != .continuous) { + if locationTrackingMode != .continuous { // Stop the data source if the tracking mode is not continuous. locationDataSource.stop() } @@ -537,4 +527,3 @@ extension ArcGISARView: AGSLocationChangeHandlerDelegate { locationChangeHandlerDelegate?.locationDataSource?(locationDataSource, statusDidChange: status) } } - From ed902b63308934a9bc82fa9c7172f69cbce3669f Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Thu, 26 Sep 2019 11:32:05 -0500 Subject: [PATCH 145/147] Update version to 100.6 in plist --- Examples/ArcGISToolkitExamples/Info.plist | 6 +++--- Toolkit/ArcGISToolkit/Info.plist | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Examples/ArcGISToolkitExamples/Info.plist b/Examples/ArcGISToolkitExamples/Info.plist index 7d1d698f..39f45da8 100644 --- a/Examples/ArcGISToolkitExamples/Info.plist +++ b/Examples/ArcGISToolkitExamples/Info.plist @@ -2,8 +2,6 @@ - NSLocationWhenInUseUsageDescription - For showing the current location in a map CFBundleDevelopmentRegion en CFBundleExecutable @@ -17,7 +15,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 100.5 + 100.6 CFBundleVersion 1 LSRequiresIPhoneOS @@ -29,6 +27,8 @@ NSCameraUsageDescription For collecting attachments in popups + NSLocationWhenInUseUsageDescription + For showing the current location in a map NSPhotoLibraryUsageDescription For collecting attachments in popups UIBackgroundModes diff --git a/Toolkit/ArcGISToolkit/Info.plist b/Toolkit/ArcGISToolkit/Info.plist index 9cdfeaeb..aa1b0165 100644 --- a/Toolkit/ArcGISToolkit/Info.plist +++ b/Toolkit/ArcGISToolkit/Info.plist @@ -15,7 +15,7 @@ CFBundlePackageType FMWK CFBundleShortVersionString - 100.5 + 100.6 CFBundleVersion $(CURRENT_PROJECT_VERSION) NSPrincipalClass From 5414b0a4cc72678a22b7fb84155903ef467af9fb Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Thu, 3 Oct 2019 09:50:29 -0500 Subject: [PATCH 146/147] Fix calibration view animation, inadvertently broken during SwiftLint changes. --- Examples/ArcGISToolkitExamples/ARExample.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Examples/ArcGISToolkitExamples/ARExample.swift b/Examples/ArcGISToolkitExamples/ARExample.swift index a6ddf9fd..7d0483c1 100644 --- a/Examples/ArcGISToolkitExamples/ARExample.swift +++ b/Examples/ArcGISToolkitExamples/ARExample.swift @@ -173,6 +173,7 @@ class ARExample: UIViewController { self.arView.sceneView.isAttributionTextVisible = false self.addCalibrationView() } + self.calibrationView?.alpha = startCalibrating ? 1.0 : 0.0 }, completion: { (_) in if !startCalibrating { From 4bab2a957d0a0054649e0323f921a6e7a9db45cd Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Thu, 3 Oct 2019 15:30:55 -0500 Subject: [PATCH 147/147] strongSelf -> self for consistency. --- Toolkit/ArcGISToolkit/AR/ArcGISARView.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift b/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift index b3d2958e..dc954c15 100644 --- a/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift +++ b/Toolkit/ArcGISToolkit/AR/ArcGISARView.swift @@ -287,13 +287,13 @@ public class ArcGISARView: UIView { /// Operations that happen after device tracking has started. fileprivate func finalizeStart() { DispatchQueue.main.async { [weak self] in - guard let strongSelf = self else { return } + guard let self = self else { return } // Run the ARSession. - if strongSelf.isUsingARKit { - strongSelf.arSCNView.session.run(strongSelf.arConfiguration, options: .resetTracking) + if self.isUsingARKit { + self.arSCNView.session.run(self.arConfiguration, options: .resetTracking) } - strongSelf.isTracking = true + self.isTracking = true } }