胡佳骏 1 year ago
commit
9d4f400adf
100 changed files with 26032 additions and 0 deletions
  1. 41 0
      .gitignore
  2. 8 0
      Assets/ImmersalSDK.meta
  3. 8 0
      Assets/ImmersalSDK/Core.meta
  4. 8 0
      Assets/ImmersalSDK/Core/Plugins.meta
  5. 9 0
      Assets/ImmersalSDK/Core/Plugins/Android.meta
  6. BIN
      Assets/ImmersalSDK/Core/Plugins/Android/nativebindings.aar
  7. 32 0
      Assets/ImmersalSDK/Core/Plugins/Android/nativebindings.aar.meta
  8. BIN
      Assets/ImmersalSDK/Core/Plugins/Android/poseplugin.aar
  9. 32 0
      Assets/ImmersalSDK/Core/Plugins/Android/poseplugin.aar.meta
  10. 8 0
      Assets/ImmersalSDK/Core/Plugins/Android/x86_64.meta
  11. BIN
      Assets/ImmersalSDK/Core/Plugins/Android/x86_64/libPosePlugin.so
  12. 81 0
      Assets/ImmersalSDK/Core/Plugins/Android/x86_64/libPosePlugin.so.meta
  13. 129 0
      Assets/ImmersalSDK/Core/Plugins/NativeBindings.cs
  14. 11 0
      Assets/ImmersalSDK/Core/Plugins/NativeBindings.cs.meta
  15. 8 0
      Assets/ImmersalSDK/Core/Plugins/WSA.meta
  16. 8 0
      Assets/ImmersalSDK/Core/Plugins/WSA/ARM64.meta
  17. BIN
      Assets/ImmersalSDK/Core/Plugins/WSA/ARM64/PosePlugin.dll
  18. 92 0
      Assets/ImmersalSDK/Core/Plugins/WSA/ARM64/PosePlugin.dll.meta
  19. BIN
      Assets/ImmersalSDK/Core/Plugins/WSA/ARM64/opencv_world453.dll
  20. 92 0
      Assets/ImmersalSDK/Core/Plugins/WSA/ARM64/opencv_world453.dll.meta
  21. 9 0
      Assets/ImmersalSDK/Core/Plugins/iOS.meta
  22. 18 0
      Assets/ImmersalSDK/Core/Plugins/iOS/NativeLocation.h
  23. 33 0
      Assets/ImmersalSDK/Core/Plugins/iOS/NativeLocation.h.meta
  24. 132 0
      Assets/ImmersalSDK/Core/Plugins/iOS/NativeLocation.mm
  25. 33 0
      Assets/ImmersalSDK/Core/Plugins/iOS/NativeLocation.mm.meta
  26. BIN
      Assets/ImmersalSDK/Core/Plugins/iOS/libPosePlugin.a
  27. 90 0
      Assets/ImmersalSDK/Core/Plugins/iOS/libPosePlugin.a.meta
  28. 9 0
      Assets/ImmersalSDK/Core/Plugins/x86_64.meta
  29. 129 0
      Assets/ImmersalSDK/Core/Plugins/x86_64/PosePlugin.bundle.meta
  30. 8 0
      Assets/ImmersalSDK/Core/Plugins/x86_64/PosePlugin.bundle/Contents.meta
  31. 50 0
      Assets/ImmersalSDK/Core/Plugins/x86_64/PosePlugin.bundle/Contents/Info.plist
  32. 7 0
      Assets/ImmersalSDK/Core/Plugins/x86_64/PosePlugin.bundle/Contents/Info.plist.meta
  33. 8 0
      Assets/ImmersalSDK/Core/Plugins/x86_64/PosePlugin.bundle/Contents/MacOS.meta
  34. BIN
      Assets/ImmersalSDK/Core/Plugins/x86_64/PosePlugin.bundle/Contents/MacOS/PosePlugin
  35. 7 0
      Assets/ImmersalSDK/Core/Plugins/x86_64/PosePlugin.bundle/Contents/MacOS/PosePlugin.meta
  36. 8 0
      Assets/ImmersalSDK/Core/Plugins/x86_64/PosePlugin.bundle/Contents/_CodeSignature.meta
  37. 115 0
      Assets/ImmersalSDK/Core/Plugins/x86_64/PosePlugin.bundle/Contents/_CodeSignature/CodeResources
  38. 7 0
      Assets/ImmersalSDK/Core/Plugins/x86_64/PosePlugin.bundle/Contents/_CodeSignature/CodeResources.meta
  39. BIN
      Assets/ImmersalSDK/Core/Plugins/x86_64/PosePlugin.dll
  40. 116 0
      Assets/ImmersalSDK/Core/Plugins/x86_64/PosePlugin.dll.meta
  41. BIN
      Assets/ImmersalSDK/Core/Plugins/x86_64/opencv_world453.dll
  42. 81 0
      Assets/ImmersalSDK/Core/Plugins/x86_64/opencv_world453.dll.meta
  43. 8 0
      Assets/ImmersalSDK/Core/Prefabs.meta
  44. 47 0
      Assets/ImmersalSDK/Core/Prefabs/ImmersalSDK.prefab
  45. 8 0
      Assets/ImmersalSDK/Core/Prefabs/ImmersalSDK.prefab.meta
  46. 8 0
      Assets/ImmersalSDK/Core/Resources.meta
  47. 8 0
      Assets/ImmersalSDK/Core/Resources/Shaders.meta
  48. 71 0
      Assets/ImmersalSDK/Core/Resources/Shaders/pointCloud.shader
  49. 9 0
      Assets/ImmersalSDK/Core/Resources/Shaders/pointCloud.shader.meta
  50. 8 0
      Assets/ImmersalSDK/Core/Scripts.meta
  51. 8 0
      Assets/ImmersalSDK/Core/Scripts/AR.meta
  52. 216 0
      Assets/ImmersalSDK/Core/Scripts/AR/ARHelper.cs
  53. 11 0
      Assets/ImmersalSDK/Core/Scripts/AR/ARHelper.cs.meta
  54. 404 0
      Assets/ImmersalSDK/Core/Scripts/AR/ARLocalizer.cs
  55. 11 0
      Assets/ImmersalSDK/Core/Scripts/AR/ARLocalizer.cs.meta
  56. 546 0
      Assets/ImmersalSDK/Core/Scripts/AR/ARMap.cs
  57. 14 0
      Assets/ImmersalSDK/Core/Scripts/AR/ARMap.cs.meta
  58. 256 0
      Assets/ImmersalSDK/Core/Scripts/AR/ARSpace.cs
  59. 11 0
      Assets/ImmersalSDK/Core/Scripts/AR/ARSpace.cs.meta
  60. 8 0
      Assets/ImmersalSDK/Core/Scripts/AR/Editor.meta
  61. 312 0
      Assets/ImmersalSDK/Core/Scripts/AR/Editor/ARMapEditor.cs
  62. 11 0
      Assets/ImmersalSDK/Core/Scripts/AR/Editor/ARMapEditor.cs.meta
  63. 27 0
      Assets/ImmersalSDK/Core/Scripts/AR/Editor/ARSpaceEditor.cs
  64. 11 0
      Assets/ImmersalSDK/Core/Scripts/AR/Editor/ARSpaceEditor.cs.meta
  65. 350 0
      Assets/ImmersalSDK/Core/Scripts/AR/LocalizerBase.cs
  66. 11 0
      Assets/ImmersalSDK/Core/Scripts/AR/LocalizerBase.cs.meta
  67. 92 0
      Assets/ImmersalSDK/Core/Scripts/AR/PoseFilter.cs
  68. 11 0
      Assets/ImmersalSDK/Core/Scripts/AR/PoseFilter.cs.meta
  69. 379 0
      Assets/ImmersalSDK/Core/Scripts/Core.cs
  70. 12 0
      Assets/ImmersalSDK/Core/Scripts/Core.cs.meta
  71. 8 0
      Assets/ImmersalSDK/Core/Scripts/Editor.meta
  72. 280 0
      Assets/ImmersalSDK/Core/Scripts/Editor/ARMapDownloader.cs
  73. 11 0
      Assets/ImmersalSDK/Core/Scripts/Editor/ARMapDownloader.cs.meta
  74. 103 0
      Assets/ImmersalSDK/Core/Scripts/Editor/ImmersalSDKEditor.cs
  75. 11 0
      Assets/ImmersalSDK/Core/Scripts/Editor/ImmersalSDKEditor.cs.meta
  76. 32 0
      Assets/ImmersalSDK/Core/Scripts/Editor/ImmersalSDKInfo.cs
  77. 11 0
      Assets/ImmersalSDK/Core/Scripts/Editor/ImmersalSDKInfo.cs.meta
  78. 34 0
      Assets/ImmersalSDK/Core/Scripts/Editor/ReadOnlyDrawer.cs
  79. 11 0
      Assets/ImmersalSDK/Core/Scripts/Editor/ReadOnlyDrawer.cs.meta
  80. 401 0
      Assets/ImmersalSDK/Core/Scripts/ImmersalSDK.cs
  81. 11 0
      Assets/ImmersalSDK/Core/Scripts/ImmersalSDK.cs.meta
  82. 475 0
      Assets/ImmersalSDK/Core/Scripts/REST.cs
  83. 11 0
      Assets/ImmersalSDK/Core/Scripts/REST.cs.meta
  84. 893 0
      Assets/ImmersalSDK/Core/Scripts/RESTJobsAsync.cs
  85. 11 0
      Assets/ImmersalSDK/Core/Scripts/RESTJobsAsync.cs.meta
  86. 6 0
      Assets/ImmersalSDK/Core/Scripts/ReadOnlyAttribute.cs
  87. 11 0
      Assets/ImmersalSDK/Core/Scripts/ReadOnlyAttribute.cs.meta
  88. 21 0
      Assets/ImmersalSDK/ImmersalSDK.asmdef
  89. 7 0
      Assets/ImmersalSDK/ImmersalSDK.asmdef.meta
  90. 8 0
      Assets/ImmersalSDK/Samples.meta
  91. 8 0
      Assets/ImmersalSDK/Samples/Fonts.meta
  92. 8 0
      Assets/ImmersalSDK/Samples/Fonts/LiberationSans.meta
  93. 7826 0
      Assets/ImmersalSDK/Samples/Fonts/LiberationSans/LiberationSans SDF DebugConsole.asset
  94. 8 0
      Assets/ImmersalSDK/Samples/Fonts/LiberationSans/LiberationSans SDF DebugConsole.asset.meta
  95. 8 0
      Assets/ImmersalSDK/Samples/Fonts/Roboto Mono.meta
  96. 43 0
      Assets/ImmersalSDK/Samples/Fonts/Roboto Mono/RobotoMono-Light SDF.asset
  97. 8 0
      Assets/ImmersalSDK/Samples/Fonts/Roboto Mono/RobotoMono-Light SDF.asset.meta
  98. 5681 0
      Assets/ImmersalSDK/Samples/Fonts/Roboto Mono/RobotoMono-Medium SDF.asset
  99. 8 0
      Assets/ImmersalSDK/Samples/Fonts/Roboto Mono/RobotoMono-Medium SDF.asset.meta
  100. 5783 0
      Assets/ImmersalSDK/Samples/Fonts/Roboto Mono/RobotoMono-Regular SDF.asset

+ 41 - 0
.gitignore

@@ -0,0 +1,41 @@
+/*.csproj
+/Library
+/Logs
+/obj
+/Temp
+/UserSettings
+/vrlauncher.sln
+/Assembly-CSharp.csproj
+/Assembly-CSharp-Editor.csproj
+/Assembly-CSharp-Editor-firstpass.csproj
+/Assembly-CSharp-firstpass.csproj
+/ControllerSample.csproj
+/Demos.StandardShader.Inspectors.csproj
+/Lenovo.XR.OSK.csproj
+/Lenovo.XR.OSK.Sample.csproj
+/Pico.Platform.csproj
+/Pico.Spatializer.csproj
+/Pico.Spatializer.Editor.csproj
+/Pico.Spatializer.Example.csproj
+/QCHT.Core.csproj
+/QCHT.Core.Editor.csproj
+/QCHT.Interactions.csproj
+/QCHT.Interactions.Editor.csproj
+/Snapdragon.Spaces.Editor.csproj
+/Snapdragon.Spaces.Runtime.csproj
+/Unity.XR.PICO.csproj
+/Unity.XR.PICO.Editor.csproj
+/.vsconfig
+/*.apk
+/libc++_shared.so
+/libc++_shared.so.meta
+/libopenxr_loader.so
+/libopenxr_loader.so.meta
+/QCAR
+/*.sln
+/.vs
+/.vscode
+/Assets/Samples/*
+/Assets/Samples.meta
+/Packages/*
+/Build/*

+ 8 - 0
Assets/ImmersalSDK.meta

@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: e117e2b8a7ccb40a5991f97d64cb428a
+folderAsset: yes
+DefaultImporter:
+  externalObjects: {}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 8 - 0
Assets/ImmersalSDK/Core.meta

@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 13d95319a60c0b5439397aa481fa749c
+folderAsset: yes
+DefaultImporter:
+  externalObjects: {}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 8 - 0
Assets/ImmersalSDK/Core/Plugins.meta

@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: d4e1b058addf948b589443857632c8e7
+folderAsset: yes
+DefaultImporter:
+  externalObjects: {}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 9 - 0
Assets/ImmersalSDK/Core/Plugins/Android.meta

@@ -0,0 +1,9 @@
+fileFormatVersion: 2
+guid: f192b64343523994e999fa8a5c964120
+folderAsset: yes
+timeCreated: 1466076008
+licenseType: Free
+DefaultImporter:
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

BIN
Assets/ImmersalSDK/Core/Plugins/Android/nativebindings.aar


+ 32 - 0
Assets/ImmersalSDK/Core/Plugins/Android/nativebindings.aar.meta

@@ -0,0 +1,32 @@
+fileFormatVersion: 2
+guid: c0353ebb74a494fc38f3854047d3a2cd
+PluginImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  iconMap: {}
+  executionOrder: {}
+  defineConstraints: []
+  isPreloaded: 0
+  isOverridable: 0
+  isExplicitlyReferenced: 0
+  validateReferences: 1
+  platformData:
+  - first:
+      Android: Android
+    second:
+      enabled: 1
+      settings: {}
+  - first:
+      Any: 
+    second:
+      enabled: 0
+      settings: {}
+  - first:
+      Editor: Editor
+    second:
+      enabled: 0
+      settings:
+        DefaultValueInitialized: true
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

BIN
Assets/ImmersalSDK/Core/Plugins/Android/poseplugin.aar


+ 32 - 0
Assets/ImmersalSDK/Core/Plugins/Android/poseplugin.aar.meta

@@ -0,0 +1,32 @@
+fileFormatVersion: 2
+guid: 6ac8052c3b76d46219a17796c730a4b2
+PluginImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  iconMap: {}
+  executionOrder: {}
+  defineConstraints: []
+  isPreloaded: 0
+  isOverridable: 0
+  isExplicitlyReferenced: 0
+  validateReferences: 1
+  platformData:
+  - first:
+      Android: Android
+    second:
+      enabled: 1
+      settings: {}
+  - first:
+      Any: 
+    second:
+      enabled: 0
+      settings: {}
+  - first:
+      Editor: Editor
+    second:
+      enabled: 0
+      settings:
+        DefaultValueInitialized: true
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 8 - 0
Assets/ImmersalSDK/Core/Plugins/Android/x86_64.meta

@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 3d740e69729e84014acb4e50398f6a9d
+folderAsset: yes
+DefaultImporter:
+  externalObjects: {}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

BIN
Assets/ImmersalSDK/Core/Plugins/Android/x86_64/libPosePlugin.so


+ 81 - 0
Assets/ImmersalSDK/Core/Plugins/Android/x86_64/libPosePlugin.so.meta

@@ -0,0 +1,81 @@
+fileFormatVersion: 2
+guid: 547ffb2831da54ff19fc3fef3067a560
+PluginImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  iconMap: {}
+  executionOrder: {}
+  defineConstraints: []
+  isPreloaded: 1
+  isOverridable: 0
+  isExplicitlyReferenced: 0
+  validateReferences: 1
+  platformData:
+  - first:
+      : Any
+    second:
+      enabled: 0
+      settings:
+        Exclude Android: 0
+        Exclude Editor: 1
+        Exclude Linux64: 1
+        Exclude OSXUniversal: 1
+        Exclude Win: 1
+        Exclude Win64: 1
+        Exclude iOS: 1
+  - first:
+      Android: Android
+    second:
+      enabled: 1
+      settings:
+        AndroidSharedLibraryType: Executable
+        CPU: X86_64
+  - first:
+      Any: 
+    second:
+      enabled: 0
+      settings: {}
+  - first:
+      Editor: Editor
+    second:
+      enabled: 0
+      settings:
+        CPU: AnyCPU
+        DefaultValueInitialized: true
+        OS: AnyOS
+  - first:
+      Standalone: Linux64
+    second:
+      enabled: 0
+      settings:
+        CPU: AnyCPU
+  - first:
+      Standalone: OSXUniversal
+    second:
+      enabled: 0
+      settings:
+        CPU: None
+  - first:
+      Standalone: Win
+    second:
+      enabled: 0
+      settings:
+        CPU: x86
+  - first:
+      Standalone: Win64
+    second:
+      enabled: 0
+      settings:
+        CPU: x86_64
+  - first:
+      iPhone: iOS
+    second:
+      enabled: 0
+      settings:
+        AddToEmbeddedBinaries: false
+        CPU: AnyCPU
+        CompileFlags: 
+        FrameworkDependencies: 
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 129 - 0
Assets/ImmersalSDK/Core/Plugins/NativeBindings.cs

@@ -0,0 +1,129 @@
+/*===============================================================================
+Copyright (C) 2023 Immersal - Part of Hexagon. All Rights Reserved.
+
+This file is part of the Immersal SDK.
+
+The Immersal SDK cannot be copied, distributed, or made available to
+third-parties for commercial purposes without written permission of Immersal Ltd.
+
+Contact sales@immersal.com for licensing requests.
+===============================================================================*/
+
+#if (UNITY_IOS || PLATFORM_ANDROID) && !UNITY_EDITOR
+using System.Runtime.InteropServices;
+using UnityEngine;
+
+namespace Immersal
+{
+	public class NativeBindings
+	{
+		#if UNITY_IOS
+		[DllImport("__Internal")]
+		public static extern void startLocation();
+
+		[DllImport("__Internal")]
+		public static extern void stopLocation();
+
+		[DllImport("__Internal")]
+		public static extern double getLatitude();
+
+		[DllImport("__Internal")]
+		public static extern double getLongitude();
+
+		[DllImport("__Internal")]
+		public static extern double getAltitude();
+
+		[DllImport("__Internal")]
+		public static extern double getHorizontalAccuracy();
+
+		[DllImport("__Internal")]
+		public static extern double getVerticalAccuracy();
+
+		[DllImport("__Internal")]
+		public static extern bool locationServicesEnabled();
+
+		#elif PLATFORM_ANDROID
+		static AndroidJavaClass obj = new AndroidJavaClass("com.immersal.nativebindings.Main");
+		#endif
+
+		public static bool StartLocation()
+		{
+			if (!Input.location.isEnabledByUser)
+			{
+				return false;
+			}
+
+			#if UNITY_IOS
+			startLocation();
+			#elif PLATFORM_ANDROID
+			obj.CallStatic("startLocation");
+			#endif
+
+			return true;
+		}
+
+		public static void StopLocation()
+		{
+			#if UNITY_IOS
+			stopLocation();
+			#elif PLATFORM_ANDROID
+			obj.CallStatic("stopLocation");
+			#endif
+		}
+
+		public static double GetLatitude()
+		{
+			#if UNITY_IOS
+			return getLatitude();
+			#elif PLATFORM_ANDROID
+			return obj.CallStatic<double>("getLatitude");
+			#endif
+		}
+
+		public static double GetLongitude()
+		{
+			#if UNITY_IOS
+			return getLongitude();
+			#elif PLATFORM_ANDROID
+			return obj.CallStatic<double>("getLongitude");
+			#endif
+		}
+
+		public static double GetAltitude()
+		{
+			#if UNITY_IOS
+			return getAltitude();
+			#elif PLATFORM_ANDROID
+			return obj.CallStatic<double>("getAltitude");
+			#endif
+		}
+
+		public static double GetHorizontalAccuracy()
+		{
+			#if UNITY_IOS
+			return getHorizontalAccuracy();
+			#elif PLATFORM_ANDROID
+			return obj.CallStatic<double>("getHorizontalAccuracy");
+			#endif
+		}
+
+		public static double GetVerticalAccuracy()
+		{
+			#if UNITY_IOS
+			return getVerticalAccuracy();
+			#elif PLATFORM_ANDROID
+			return obj.CallStatic<double>("getVerticalAccuracy");
+			#endif
+		}
+
+		public static bool LocationServicesEnabled()
+		{
+			#if UNITY_IOS
+			return locationServicesEnabled();
+			#elif PLATFORM_ANDROID
+			return obj.CallStatic<bool>("locationServicesEnabled");
+			#endif
+		}
+	}
+}
+#endif

+ 11 - 0
Assets/ImmersalSDK/Core/Plugins/NativeBindings.cs.meta

@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 1f141c749ad8c4a4dbdd7b392e91a9c3
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 8 - 0
Assets/ImmersalSDK/Core/Plugins/WSA.meta

@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 67eed6f859603464a82ba3e935402f7c
+folderAsset: yes
+DefaultImporter:
+  externalObjects: {}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 8 - 0
Assets/ImmersalSDK/Core/Plugins/WSA/ARM64.meta

@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 631bfc40bf2454bbab8186d0cb601a42
+folderAsset: yes
+DefaultImporter:
+  externalObjects: {}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

BIN
Assets/ImmersalSDK/Core/Plugins/WSA/ARM64/PosePlugin.dll


+ 92 - 0
Assets/ImmersalSDK/Core/Plugins/WSA/ARM64/PosePlugin.dll.meta

@@ -0,0 +1,92 @@
+fileFormatVersion: 2
+guid: 0be4d5a4a319b8143bd8cf96ce5a83d5
+PluginImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  iconMap: {}
+  executionOrder: {}
+  defineConstraints: []
+  isPreloaded: 1
+  isOverridable: 0
+  isExplicitlyReferenced: 0
+  validateReferences: 1
+  platformData:
+  - first:
+      : Any
+    second:
+      enabled: 0
+      settings:
+        Exclude Android: 1
+        Exclude Editor: 1
+        Exclude Linux64: 1
+        Exclude Lumin: 1
+        Exclude OSXUniversal: 1
+        Exclude Win: 1
+        Exclude Win64: 1
+        Exclude WindowsStoreApps: 0
+        Exclude iOS: 1
+  - first:
+      Android: Android
+    second:
+      enabled: 0
+      settings:
+        CPU: ARMv7
+  - first:
+      Any: 
+    second:
+      enabled: 0
+      settings: {}
+  - first:
+      Editor: Editor
+    second:
+      enabled: 0
+      settings:
+        CPU: AnyCPU
+        DefaultValueInitialized: true
+        OS: AnyOS
+  - first:
+      Standalone: Linux64
+    second:
+      enabled: 0
+      settings:
+        CPU: None
+  - first:
+      Standalone: OSXUniversal
+    second:
+      enabled: 0
+      settings:
+        CPU: None
+  - first:
+      Standalone: Win
+    second:
+      enabled: 0
+      settings:
+        CPU: None
+  - first:
+      Standalone: Win64
+    second:
+      enabled: 0
+      settings:
+        CPU: None
+  - first:
+      Windows Store Apps: WindowsStoreApps
+    second:
+      enabled: 1
+      settings:
+        CPU: ARM64
+        DontProcess: false
+        PlaceholderPath: 
+        SDK: UWP
+        ScriptingBackend: AnyScriptingBackend
+  - first:
+      iPhone: iOS
+    second:
+      enabled: 0
+      settings:
+        AddToEmbeddedBinaries: false
+        CPU: AnyCPU
+        CompileFlags: 
+        FrameworkDependencies: 
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

BIN
Assets/ImmersalSDK/Core/Plugins/WSA/ARM64/opencv_world453.dll


+ 92 - 0
Assets/ImmersalSDK/Core/Plugins/WSA/ARM64/opencv_world453.dll.meta

@@ -0,0 +1,92 @@
+fileFormatVersion: 2
+guid: f3ebd60d649e2ef4e9df5620d73c0ad8
+PluginImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  iconMap: {}
+  executionOrder: {}
+  defineConstraints: []
+  isPreloaded: 0
+  isOverridable: 0
+  isExplicitlyReferenced: 0
+  validateReferences: 1
+  platformData:
+  - first:
+      : Any
+    second:
+      enabled: 0
+      settings:
+        Exclude Android: 1
+        Exclude Editor: 1
+        Exclude Linux64: 1
+        Exclude Lumin: 1
+        Exclude OSXUniversal: 1
+        Exclude Win: 1
+        Exclude Win64: 1
+        Exclude WindowsStoreApps: 0
+        Exclude iOS: 1
+  - first:
+      Android: Android
+    second:
+      enabled: 0
+      settings:
+        CPU: ARMv7
+  - first:
+      Any: 
+    second:
+      enabled: 0
+      settings: {}
+  - first:
+      Editor: Editor
+    second:
+      enabled: 0
+      settings:
+        CPU: AnyCPU
+        DefaultValueInitialized: true
+        OS: AnyOS
+  - first:
+      Standalone: Linux64
+    second:
+      enabled: 0
+      settings:
+        CPU: None
+  - first:
+      Standalone: OSXUniversal
+    second:
+      enabled: 0
+      settings:
+        CPU: None
+  - first:
+      Standalone: Win
+    second:
+      enabled: 0
+      settings:
+        CPU: None
+  - first:
+      Standalone: Win64
+    second:
+      enabled: 0
+      settings:
+        CPU: None
+  - first:
+      Windows Store Apps: WindowsStoreApps
+    second:
+      enabled: 1
+      settings:
+        CPU: ARM64
+        DontProcess: false
+        PlaceholderPath: 
+        SDK: UWP
+        ScriptingBackend: AnyScriptingBackend
+  - first:
+      iPhone: iOS
+    second:
+      enabled: 0
+      settings:
+        AddToEmbeddedBinaries: false
+        CPU: AnyCPU
+        CompileFlags: 
+        FrameworkDependencies: 
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 9 - 0
Assets/ImmersalSDK/Core/Plugins/iOS.meta

@@ -0,0 +1,9 @@
+fileFormatVersion: 2
+guid: 91c9d6ab115d24e3585c90021f3f6983
+folderAsset: yes
+timeCreated: 1470226487
+licenseType: Free
+DefaultImporter:
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 18 - 0
Assets/ImmersalSDK/Core/Plugins/iOS/NativeLocation.h

@@ -0,0 +1,18 @@
+//
+//  NativeLocation.h
+//  Immersal SDK
+//
+//  Created by Mikko on 29/05/2020.
+//
+//
+
+#import <Foundation/Foundation.h>
+#import <CoreLocation/CoreLocation.h>
+#import <UIKit/UIKit.h>
+
+@interface NativeLocation : NSObject <CLLocationManagerDelegate>
+
+- (NativeLocation *)init;
+- (void)locationManager:(CLLocationManager *)manager didUpdateLocations:(NSArray *)locations;
+
+@end

+ 33 - 0
Assets/ImmersalSDK/Core/Plugins/iOS/NativeLocation.h.meta

@@ -0,0 +1,33 @@
+fileFormatVersion: 2
+guid: 0a3a48cdb55ec4cd092e73a175c19327
+PluginImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  iconMap: {}
+  executionOrder: {}
+  defineConstraints: []
+  isPreloaded: 0
+  isOverridable: 0
+  isExplicitlyReferenced: 0
+  validateReferences: 1
+  platformData:
+  - first:
+      Any: 
+    second:
+      enabled: 0
+      settings: {}
+  - first:
+      Editor: Editor
+    second:
+      enabled: 0
+      settings:
+        DefaultValueInitialized: true
+  - first:
+      iPhone: iOS
+    second:
+      enabled: 1
+      settings:
+        AddToEmbeddedBinaries: false
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 132 - 0
Assets/ImmersalSDK/Core/Plugins/iOS/NativeLocation.mm

@@ -0,0 +1,132 @@
+//
+//  NativeLocation.m
+//  Immersal SDK
+//
+//  Created by Mikko on 29/05/2020.
+//
+//
+
+#import "NativeLocation.h"
+
+double latitude;
+double longitude;
+double altitude;
+double haccuracy;
+double vaccuracy;
+
+@implementation NativeLocation
+
+CLLocationManager *locationManager;
+static bool isEnabled = NO;
+
+- (NativeLocation *)init
+{
+    locationManager = [[CLLocationManager alloc] init];
+    locationManager.delegate = self;
+    locationManager.distanceFilter = kCLDistanceFilterNone;
+    locationManager.desiredAccuracy = kCLLocationAccuracyBest;
+    
+    if ([[[UIDevice currentDevice] systemVersion] floatValue] >= 8.0)
+        [locationManager requestWhenInUseAuthorization];
+        
+    return self;
+}
+
+- (void)locationManager:(CLLocationManager *)manager didChangeAuthorizationStatus:(CLAuthorizationStatus)status;
+{
+/*    switch (status) {
+        case kCLAuthorizationStatusAuthorizedWhenInUse:
+        case kCLAuthorizationStatusAuthorizedAlways:
+            isEnabled = YES; break;
+        default:
+            isEnabled = NO; break;
+    }*/
+}
+
+- (void)locationManager:(CLLocationManager*)manager didFailWithError:(NSError*)error;
+{
+    isEnabled = NO;
+}
+
+- (void)locationManager:(CLLocationManager *)manager didUpdateLocations:(NSArray *)locations;
+{
+    CLLocation *location = [locations lastObject];
+    latitude = location.coordinate.latitude;
+    longitude = location.coordinate.longitude;
+    altitude = location.altitude;
+    haccuracy = location.horizontalAccuracy;
+    vaccuracy = location.verticalAccuracy;
+    
+    isEnabled = YES;
+    
+    //NSLog(@"lat: %f long: %f alt: %f", latitude, longitude, altitude);
+}
+
+- (void)start
+{
+    if (locationManager != NULL) {
+        [locationManager startUpdatingLocation];
+    }
+}
+
+- (void)stop
+{
+    if (locationManager != NULL) {
+        [locationManager stopUpdatingLocation];
+    }
+    
+    isEnabled = NO;
+}
+
+@end
+
+static NativeLocation* locationDelegate = NULL;
+
+extern "C"
+{
+    void startLocation()
+    {
+        if (locationDelegate == NULL) {
+            locationDelegate = [[NativeLocation alloc] init];
+        }
+        
+        [locationDelegate start];
+    }
+
+    void stopLocation()
+    {
+        if (locationDelegate != NULL) {
+            [locationDelegate stop];
+        }
+    }
+        
+    double getLatitude()
+    {
+        return latitude;
+    }
+
+    double getLongitude()
+    {
+        return longitude;
+    }
+
+    double getAltitude()
+    {
+        return altitude;
+    }
+
+    double getHorizontalAccuracy()
+    {
+        return haccuracy;
+    }
+
+    double getVerticalAccuracy()
+    {
+        return vaccuracy;
+    }
+
+    bool locationServicesEnabled()
+    {
+        return isEnabled;
+    }
+}

+ 33 - 0
Assets/ImmersalSDK/Core/Plugins/iOS/NativeLocation.mm.meta

@@ -0,0 +1,33 @@
+fileFormatVersion: 2
+guid: f3ab833245d3148879a818fd6485d2ce
+PluginImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  iconMap: {}
+  executionOrder: {}
+  defineConstraints: []
+  isPreloaded: 0
+  isOverridable: 0
+  isExplicitlyReferenced: 0
+  validateReferences: 1
+  platformData:
+  - first:
+      Any: 
+    second:
+      enabled: 0
+      settings: {}
+  - first:
+      Editor: Editor
+    second:
+      enabled: 0
+      settings:
+        DefaultValueInitialized: true
+  - first:
+      iPhone: iOS
+    second:
+      enabled: 1
+      settings:
+        AddToEmbeddedBinaries: false
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

BIN
Assets/ImmersalSDK/Core/Plugins/iOS/libPosePlugin.a


+ 90 - 0
Assets/ImmersalSDK/Core/Plugins/iOS/libPosePlugin.a.meta

@@ -0,0 +1,90 @@
+fileFormatVersion: 2
+guid: f134d0207a63f7c4696a67f9fa9398c9
+PluginImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  iconMap: {}
+  executionOrder: {}
+  defineConstraints: []
+  isPreloaded: 0
+  isOverridable: 0
+  isExplicitlyReferenced: 0
+  validateReferences: 1
+  platformData:
+  - first:
+      : OSXIntel
+    second:
+      enabled: 0
+      settings:
+        CPU: AnyCPU
+  - first:
+      : OSXIntel64
+    second:
+      enabled: 0
+      settings:
+        CPU: AnyCPU
+  - first:
+      Android: Android
+    second:
+      enabled: 0
+      settings:
+        CPU: AnyCPU
+  - first:
+      Any: 
+    second:
+      enabled: 0
+      settings: {}
+  - first:
+      Editor: Editor
+    second:
+      enabled: 0
+      settings:
+        CPU: AnyCPU
+        DefaultValueInitialized: true
+        OS: AnyOS
+  - first:
+      Facebook: Win
+    second:
+      enabled: 0
+      settings:
+        CPU: AnyCPU
+  - first:
+      Facebook: Win64
+    second:
+      enabled: 0
+      settings:
+        CPU: AnyCPU
+  - first:
+      Standalone: Linux
+    second:
+      enabled: 0
+      settings:
+        CPU: x86
+  - first:
+      Standalone: Linux64
+    second:
+      enabled: 0
+      settings:
+        CPU: AnyCPU
+  - first:
+      Standalone: Win
+    second:
+      enabled: 0
+      settings:
+        CPU: AnyCPU
+  - first:
+      Standalone: Win64
+    second:
+      enabled: 0
+      settings:
+        CPU: AnyCPU
+  - first:
+      iPhone: iOS
+    second:
+      enabled: 1
+      settings:
+        CompileFlags: 
+        FrameworkDependencies: Security;AssetsLibrary;
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 9 - 0
Assets/ImmersalSDK/Core/Plugins/x86_64.meta

@@ -0,0 +1,9 @@
+fileFormatVersion: 2
+guid: 867bc362f511875419b00825c35adb62
+folderAsset: yes
+timeCreated: 1469455304
+licenseType: Free
+DefaultImporter:
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 129 - 0
Assets/ImmersalSDK/Core/Plugins/x86_64/PosePlugin.bundle.meta

@@ -0,0 +1,129 @@
+fileFormatVersion: 2
+guid: 0a60313de0e704fd682e181e56ca94a7
+folderAsset: yes
+PluginImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  iconMap: {}
+  executionOrder: {}
+  defineConstraints: []
+  isPreloaded: 1
+  isOverridable: 0
+  isExplicitlyReferenced: 0
+  validateReferences: 1
+  platformData:
+  - first:
+      : Any
+    second:
+      enabled: 0
+      settings:
+        Exclude Android: 1
+        Exclude Editor: 0
+        Exclude Linux: 1
+        Exclude Linux64: 0
+        Exclude LinuxUniversal: 0
+        Exclude Lumin: 1
+        Exclude OSXIntel: 1
+        Exclude OSXIntel64: 0
+        Exclude OSXUniversal: 0
+        Exclude Win: 1
+        Exclude Win64: 0
+        Exclude iOS: 1
+  - first:
+      : Editor
+    second:
+      enabled: 0
+      settings:
+        CPU: x86_64
+        OS: OSX
+  - first:
+      Android: Android
+    second:
+      enabled: 0
+      settings:
+        CPU: ARMv7
+  - first:
+      Any: 
+    second:
+      enabled: 0
+      settings: {}
+  - first:
+      Editor: Editor
+    second:
+      enabled: 1
+      settings:
+        CPU: AnyCPU
+        DefaultValueInitialized: true
+        OS: OSX
+  - first:
+      Facebook: Win
+    second:
+      enabled: 0
+      settings:
+        CPU: None
+  - first:
+      Facebook: Win64
+    second:
+      enabled: 1
+      settings:
+        CPU: AnyCPU
+  - first:
+      Standalone: Linux
+    second:
+      enabled: 0
+      settings:
+        CPU: None
+  - first:
+      Standalone: Linux64
+    second:
+      enabled: 1
+      settings:
+        CPU: AnyCPU
+  - first:
+      Standalone: LinuxUniversal
+    second:
+      enabled: 1
+      settings:
+        CPU: x86_64
+  - first:
+      Standalone: OSXIntel
+    second:
+      enabled: 0
+      settings:
+        CPU: None
+  - first:
+      Standalone: OSXIntel64
+    second:
+      enabled: 1
+      settings:
+        CPU: AnyCPU
+  - first:
+      Standalone: OSXUniversal
+    second:
+      enabled: 1
+      settings:
+        CPU: AnyCPU
+  - first:
+      Standalone: Win
+    second:
+      enabled: 0
+      settings:
+        CPU: None
+  - first:
+      Standalone: Win64
+    second:
+      enabled: 1
+      settings:
+        CPU: AnyCPU
+  - first:
+      iPhone: iOS
+    second:
+      enabled: 0
+      settings:
+        AddToEmbeddedBinaries: false
+        CPU: AnyCPU
+        CompileFlags: 
+        FrameworkDependencies: 
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 8 - 0
Assets/ImmersalSDK/Core/Plugins/x86_64/PosePlugin.bundle/Contents.meta

@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 5c29455e40ce42d4395230475d19e709
+folderAsset: yes
+DefaultImporter:
+  externalObjects: {}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 50 - 0
Assets/ImmersalSDK/Core/Plugins/x86_64/PosePlugin.bundle/Contents/Info.plist

@@ -0,0 +1,50 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+	<key>BuildMachineOSBuild</key>
+	<string>22G120</string>
+	<key>CFBundleDevelopmentRegion</key>
+	<string>en</string>
+	<key>CFBundleExecutable</key>
+	<string>PosePlugin</string>
+	<key>CFBundleIdentifier</key>
+	<string>com.immersal.PosePlugin</string>
+	<key>CFBundleInfoDictionaryVersion</key>
+	<string>6.0</string>
+	<key>CFBundleName</key>
+	<string>PosePlugin</string>
+	<key>CFBundlePackageType</key>
+	<string>BNDL</string>
+	<key>CFBundleShortVersionString</key>
+	<string>1.0</string>
+	<key>CFBundleSignature</key>
+	<string>????</string>
+	<key>CFBundleSupportedPlatforms</key>
+	<array>
+		<string>MacOSX</string>
+	</array>
+	<key>CFBundleVersion</key>
+	<string>1</string>
+	<key>DTCompiler</key>
+	<string>com.apple.compilers.llvm.clang.1_0</string>
+	<key>DTPlatformBuild</key>
+	<string></string>
+	<key>DTPlatformName</key>
+	<string>macosx</string>
+	<key>DTPlatformVersion</key>
+	<string>14.0</string>
+	<key>DTSDKBuild</key>
+	<string>23A334</string>
+	<key>DTSDKName</key>
+	<string>macosx14.0</string>
+	<key>DTXcode</key>
+	<string>1500</string>
+	<key>DTXcodeBuild</key>
+	<string>15A240d</string>
+	<key>LSMinimumSystemVersion</key>
+	<string>12.0</string>
+	<key>NSHumanReadableCopyright</key>
+	<string>Copyright © 2022 Immersal - Part of Hexagon. All rights reserved.</string>
+</dict>
+</plist>

+ 7 - 0
Assets/ImmersalSDK/Core/Plugins/x86_64/PosePlugin.bundle/Contents/Info.plist.meta

@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: bcff9374b1d7dbf44a041a0ea5fe4e20
+DefaultImporter:
+  externalObjects: {}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 8 - 0
Assets/ImmersalSDK/Core/Plugins/x86_64/PosePlugin.bundle/Contents/MacOS.meta

@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: a182975a61632564e9fc3b8bc2d4414d
+folderAsset: yes
+DefaultImporter:
+  externalObjects: {}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

BIN
Assets/ImmersalSDK/Core/Plugins/x86_64/PosePlugin.bundle/Contents/MacOS/PosePlugin


+ 7 - 0
Assets/ImmersalSDK/Core/Plugins/x86_64/PosePlugin.bundle/Contents/MacOS/PosePlugin.meta

@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: 773ed4041bf9d844ea4c12cb5e1ed402
+DefaultImporter:
+  externalObjects: {}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 8 - 0
Assets/ImmersalSDK/Core/Plugins/x86_64/PosePlugin.bundle/Contents/_CodeSignature.meta

@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 59d7c0c97d02b694ab0a938ac465604a
+folderAsset: yes
+DefaultImporter:
+  externalObjects: {}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 115 - 0
Assets/ImmersalSDK/Core/Plugins/x86_64/PosePlugin.bundle/Contents/_CodeSignature/CodeResources

@@ -0,0 +1,115 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+	<key>files</key>
+	<dict/>
+	<key>files2</key>
+	<dict/>
+	<key>rules</key>
+	<dict>
+		<key>^Resources/</key>
+		<true/>
+		<key>^Resources/.*\.lproj/</key>
+		<dict>
+			<key>optional</key>
+			<true/>
+			<key>weight</key>
+			<real>1000</real>
+		</dict>
+		<key>^Resources/.*\.lproj/locversion.plist$</key>
+		<dict>
+			<key>omit</key>
+			<true/>
+			<key>weight</key>
+			<real>1100</real>
+		</dict>
+		<key>^Resources/Base\.lproj/</key>
+		<dict>
+			<key>weight</key>
+			<real>1010</real>
+		</dict>
+		<key>^version.plist$</key>
+		<true/>
+	</dict>
+	<key>rules2</key>
+	<dict>
+		<key>.*\.dSYM($|/)</key>
+		<dict>
+			<key>weight</key>
+			<real>11</real>
+		</dict>
+		<key>^(.*/)?\.DS_Store$</key>
+		<dict>
+			<key>omit</key>
+			<true/>
+			<key>weight</key>
+			<real>2000</real>
+		</dict>
+		<key>^(Frameworks|SharedFrameworks|PlugIns|Plug-ins|XPCServices|Helpers|MacOS|Library/(Automator|Spotlight|LoginItems))/</key>
+		<dict>
+			<key>nested</key>
+			<true/>
+			<key>weight</key>
+			<real>10</real>
+		</dict>
+		<key>^.*</key>
+		<true/>
+		<key>^Info\.plist$</key>
+		<dict>
+			<key>omit</key>
+			<true/>
+			<key>weight</key>
+			<real>20</real>
+		</dict>
+		<key>^PkgInfo$</key>
+		<dict>
+			<key>omit</key>
+			<true/>
+			<key>weight</key>
+			<real>20</real>
+		</dict>
+		<key>^Resources/</key>
+		<dict>
+			<key>weight</key>
+			<real>20</real>
+		</dict>
+		<key>^Resources/.*\.lproj/</key>
+		<dict>
+			<key>optional</key>
+			<true/>
+			<key>weight</key>
+			<real>1000</real>
+		</dict>
+		<key>^Resources/.*\.lproj/locversion.plist$</key>
+		<dict>
+			<key>omit</key>
+			<true/>
+			<key>weight</key>
+			<real>1100</real>
+		</dict>
+		<key>^Resources/Base\.lproj/</key>
+		<dict>
+			<key>weight</key>
+			<real>1010</real>
+		</dict>
+		<key>^[^/]+$</key>
+		<dict>
+			<key>nested</key>
+			<true/>
+			<key>weight</key>
+			<real>10</real>
+		</dict>
+		<key>^embedded\.provisionprofile$</key>
+		<dict>
+			<key>weight</key>
+			<real>20</real>
+		</dict>
+		<key>^version\.plist$</key>
+		<dict>
+			<key>weight</key>
+			<real>20</real>
+		</dict>
+	</dict>
+</dict>
+</plist>

+ 7 - 0
Assets/ImmersalSDK/Core/Plugins/x86_64/PosePlugin.bundle/Contents/_CodeSignature/CodeResources.meta

@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: b72eea44116af524bac3ab1797b791eb
+DefaultImporter:
+  externalObjects: {}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

BIN
Assets/ImmersalSDK/Core/Plugins/x86_64/PosePlugin.dll


+ 116 - 0
Assets/ImmersalSDK/Core/Plugins/x86_64/PosePlugin.dll.meta

@@ -0,0 +1,116 @@
+fileFormatVersion: 2
+guid: b3ad48f0b80387d47808cea0b43f73fb
+PluginImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  iconMap: {}
+  executionOrder: {}
+  defineConstraints: []
+  isPreloaded: 1
+  isOverridable: 0
+  isExplicitlyReferenced: 0
+  validateReferences: 1
+  platformData:
+  - first:
+      : Any
+    second:
+      enabled: 0
+      settings:
+        Exclude Android: 1
+        Exclude Editor: 0
+        Exclude Linux64: 0
+        Exclude OSXUniversal: 1
+        Exclude Win: 1
+        Exclude Win64: 0
+        Exclude iOS: 1
+  - first:
+      : Linux
+    second:
+      enabled: 0
+      settings:
+        CPU: None
+  - first:
+      : LinuxUniversal
+    second:
+      enabled: 0
+      settings:
+        CPU: x86_64
+  - first:
+      : OSXIntel
+    second:
+      enabled: 0
+      settings:
+        CPU: None
+  - first:
+      : OSXIntel64
+    second:
+      enabled: 1
+      settings:
+        CPU: AnyCPU
+  - first:
+      Android: Android
+    second:
+      enabled: 0
+      settings:
+        CPU: ARMv7
+  - first:
+      Any: 
+    second:
+      enabled: 0
+      settings: {}
+  - first:
+      Editor: Editor
+    second:
+      enabled: 1
+      settings:
+        CPU: x86_64
+        DefaultValueInitialized: true
+        OS: Windows
+  - first:
+      Facebook: Win
+    second:
+      enabled: 0
+      settings:
+        CPU: None
+  - first:
+      Facebook: Win64
+    second:
+      enabled: 1
+      settings:
+        CPU: AnyCPU
+  - first:
+      Standalone: Linux64
+    second:
+      enabled: 1
+      settings:
+        CPU: AnyCPU
+  - first:
+      Standalone: OSXUniversal
+    second:
+      enabled: 0
+      settings:
+        CPU: x86_64
+  - first:
+      Standalone: Win
+    second:
+      enabled: 0
+      settings:
+        CPU: None
+  - first:
+      Standalone: Win64
+    second:
+      enabled: 1
+      settings:
+        CPU: AnyCPU
+  - first:
+      iPhone: iOS
+    second:
+      enabled: 0
+      settings:
+        AddToEmbeddedBinaries: false
+        CPU: AnyCPU
+        CompileFlags: 
+        FrameworkDependencies: 
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

BIN
Assets/ImmersalSDK/Core/Plugins/x86_64/opencv_world453.dll


+ 81 - 0
Assets/ImmersalSDK/Core/Plugins/x86_64/opencv_world453.dll.meta

@@ -0,0 +1,81 @@
+fileFormatVersion: 2
+guid: 9af32ce34e3c09740a2296f38904f864
+PluginImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  iconMap: {}
+  executionOrder: {}
+  defineConstraints: []
+  isPreloaded: 0
+  isOverridable: 0
+  isExplicitlyReferenced: 0
+  validateReferences: 1
+  platformData:
+  - first:
+      : Any
+    second:
+      enabled: 0
+      settings:
+        Exclude Android: 1
+        Exclude Editor: 0
+        Exclude Linux64: 0
+        Exclude Lumin: 1
+        Exclude OSXUniversal: 0
+        Exclude Win: 0
+        Exclude Win64: 0
+        Exclude iOS: 1
+  - first:
+      Android: Android
+    second:
+      enabled: 0
+      settings:
+        CPU: ARMv7
+  - first:
+      Any: 
+    second:
+      enabled: 0
+      settings: {}
+  - first:
+      Editor: Editor
+    second:
+      enabled: 1
+      settings:
+        CPU: x86_64
+        DefaultValueInitialized: true
+        OS: Windows
+  - first:
+      Standalone: Linux64
+    second:
+      enabled: 1
+      settings:
+        CPU: AnyCPU
+  - first:
+      Standalone: OSXUniversal
+    second:
+      enabled: 1
+      settings:
+        CPU: x86_64
+  - first:
+      Standalone: Win
+    second:
+      enabled: 1
+      settings:
+        CPU: None
+  - first:
+      Standalone: Win64
+    second:
+      enabled: 1
+      settings:
+        CPU: AnyCPU
+  - first:
+      iPhone: iOS
+    second:
+      enabled: 0
+      settings:
+        AddToEmbeddedBinaries: false
+        CPU: AnyCPU
+        CompileFlags: 
+        FrameworkDependencies: 
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 8 - 0
Assets/ImmersalSDK/Core/Prefabs.meta

@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: bba6e78353c194c489388f40a4f2bd6c
+folderAsset: yes
+DefaultImporter:
+  externalObjects: {}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 47 - 0
Assets/ImmersalSDK/Core/Prefabs/ImmersalSDK.prefab

@@ -0,0 +1,47 @@
+%YAML 1.1
+%TAG !u! tag:unity3d.com,2011:
+--- !u!1 &1434895627878410
+GameObject:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  serializedVersion: 6
+  m_Component:
+  - component: {fileID: 4682760393691964}
+  - component: {fileID: 8869940821398010349}
+  m_Layer: 0
+  m_Name: ImmersalARCloudSDK
+  m_TagString: Untagged
+  m_Icon: {fileID: 0}
+  m_NavMeshLayer: 0
+  m_StaticEditorFlags: 0
+  m_IsActive: 1
+--- !u!4 &4682760393691964
+Transform:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  m_GameObject: {fileID: 1434895627878410}
+  m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
+  m_LocalPosition: {x: 0, y: 0, z: 0}
+  m_LocalScale: {x: 1, y: 1, z: 1}
+  m_Children: []
+  m_Father: {fileID: 0}
+  m_RootOrder: 0
+  m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+--- !u!114 &8869940821398010349
+MonoBehaviour:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  m_GameObject: {fileID: 1434895627878410}
+  m_Enabled: 1
+  m_EditorHideFlags: 0
+  m_Script: {fileID: 11500000, guid: bc609bf82f8e346d593bc1a41c2b7cb8, type: 3}
+  m_Name: 
+  m_EditorClassIdentifier: 
+  developerToken: 
+  m_TargetFrameRate: 60

+ 8 - 0
Assets/ImmersalSDK/Core/Prefabs/ImmersalSDK.prefab.meta

@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 181e478384bef4862b94db5a060df1b6
+NativeFormatImporter:
+  externalObjects: {}
+  mainObjectFileID: 100100000
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 8 - 0
Assets/ImmersalSDK/Core/Resources.meta

@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 9e40124e0b172426ba7c8c93d75c788b
+folderAsset: yes
+DefaultImporter:
+  externalObjects: {}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 8 - 0
Assets/ImmersalSDK/Core/Resources/Shaders.meta

@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 30df7aac5e0027a46a0a8d0a3afc1f6a
+folderAsset: yes
+DefaultImporter:
+  externalObjects: {}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 71 - 0
Assets/ImmersalSDK/Core/Resources/Shaders/pointCloud.shader

@@ -0,0 +1,71 @@
+Shader "Immersal/Point Cloud"
+{
+	Properties
+	{
+
+	}
+
+		SubShader
+	{
+		Cull Off
+		Tags{ "RenderType" = "Opaque" }
+		LOD 100
+
+		Pass
+		{
+			CGPROGRAM
+			#pragma vertex vert
+			#pragma fragment frag
+			#pragma target 3.0
+
+			#include "UnityCG.cginc"
+
+			float _PointSize;
+			fixed _PerspectiveEnabled;
+			fixed4 _PointColor;
+
+			struct Vertex
+			{
+				float3 vertex : POSITION;
+			};
+
+			struct VertexOut
+			{
+				float psize : PSIZE;
+				float4 center : TEXCOORD0;
+				half size : TEXCOORD1;
+				UNITY_FOG_COORDS(0)
+			};
+
+			VertexOut vert(Vertex vertex, out float4 outpos : SV_POSITION)
+			{
+				VertexOut o;
+				outpos = UnityObjectToClipPos(vertex.vertex);
+
+                o.psize = lerp(_PointSize, _PointSize / outpos.w * _ScreenParams.y, step(0.5, _PerspectiveEnabled));
+				o.size = o.psize;
+
+				o.center = ComputeScreenPos(outpos);
+				UNITY_TRANSFER_FOG(o, o.position);
+				return o;
+			}
+            
+			fixed4 frag(VertexOut i, UNITY_VPOS_TYPE vpos : VPOS) : SV_Target
+			{
+				fixed4 c = _PointColor;
+				float4 center = i.center;
+				center.xy /= center.w;
+				center.xy *= _ScreenParams.xy;
+				float d = distance(vpos.xy, center.xy);
+
+				if (d > i.size * 0.5) {
+                    discard;
+				}
+
+				UNITY_APPLY_FOG(input.fogCoord, c);
+				return c;
+			}
+			ENDCG
+		}
+	}
+}

+ 9 - 0
Assets/ImmersalSDK/Core/Resources/Shaders/pointCloud.shader.meta

@@ -0,0 +1,9 @@
+fileFormatVersion: 2
+guid: 80bb3c180be837f4daba8ccfbd06b6eb
+ShaderImporter:
+  externalObjects: {}
+  defaultTextures: []
+  nonModifiableTextures: []
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 8 - 0
Assets/ImmersalSDK/Core/Scripts.meta

@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: eee6937ee2c707d42a0b601979052c50
+folderAsset: yes
+DefaultImporter:
+  externalObjects: {}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 8 - 0
Assets/ImmersalSDK/Core/Scripts/AR.meta

@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 09263de2dbfc4a743bdd4c5ff73574ef
+folderAsset: yes
+DefaultImporter:
+  externalObjects: {}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 216 - 0
Assets/ImmersalSDK/Core/Scripts/AR/ARHelper.cs

@@ -0,0 +1,216 @@
+/*===============================================================================
+Copyright (C) 2023 Immersal - Part of Hexagon. All Rights Reserved.
+
+This file is part of the Immersal SDK.
+
+The Immersal SDK cannot be copied, distributed, or made available to
+third-parties for commercial purposes without written permission of Immersal Ltd.
+
+Contact sales@immersal.com for licensing requests.
+===============================================================================*/
+
+using UnityEngine;
+using UnityEngine.XR.ARFoundation;
+using UnityEngine.XR.ARSubsystems;
+using System;
+using System.Runtime.InteropServices;
+using Unity.Collections.LowLevel.Unsafe;
+
+namespace Immersal.AR
+{
+	public class ARHelper {
+		public static Matrix4x4 SwitchHandedness(Matrix4x4 b)
+		{
+			Matrix4x4 D = Matrix4x4.identity;
+			D.m00 = -1;
+			return D * b * D;
+		}
+
+		public static Quaternion SwitchHandedness(Quaternion b)
+		{
+			Matrix4x4 m = SwitchHandedness(Matrix4x4.Rotate(b));
+			return m.rotation;
+		}
+
+		public static Vector3 SwitchHandedness(Vector3 b)
+		{
+			Matrix4x4 m = SwitchHandedness(Matrix4x4.TRS(b, Quaternion.identity, Vector3.one));
+			return m.GetColumn(3);
+		}
+
+        public static void DoubleQuaternionToDoubleMatrix3x3(out double[] m, double[] q)
+        {
+            m = new double [] {1, 0, 0, 0, 1, 0, 0, 0, 1}; //identity matrix
+            
+            // input quaternion should be in WXYZ order
+			double w = q[0];
+			double x = q[1];
+			double y = q[2];
+			double z = q[3];
+			
+			double ww = w * w;
+            double xx = x * x;
+            double yy = y * y;
+            double zz = z * z;
+            
+            double xy = x * y;
+            double zw = z * w;
+			double xz = x * z;
+            double yw = y * w;
+			double yz = y * z;
+            double xw = x * w;
+
+			double inv = 1.0 / (xx + yy + zz + ww);
+
+            m[0] = ( xx - yy - zz + ww) * inv;
+            m[1] = 2.0 * (xy - zw) * inv;
+            m[2] = 2.0 * (xz + yw) * inv;
+            m[3] = 2.0 * (xy + zw) * inv;
+            m[4] = (-xx + yy - zz + ww) * inv;
+            m[5] = 2.0 * (yz - xw) * inv;
+            m[6] = 2.0 * (xz - yw) * inv;
+            m[7] = 2.0 * (yz + xw) * inv;
+            m[8] = (-xx - yy + zz + ww) * inv;
+        }
+
+		public static void GetIntrinsics(out Vector4 intrinsics)
+        {
+            intrinsics = Vector4.zero;
+			XRCameraIntrinsics intr;
+			ARCameraManager manager = ImmersalSDK.Instance?.cameraManager;
+
+			if (manager != null && manager.TryGetIntrinsics(out intr))
+			{
+				intrinsics.x = intr.focalLength.x;
+				intrinsics.y = intr.focalLength.y;
+				intrinsics.z = intr.principalPoint.x;
+				intrinsics.w = intr.principalPoint.y;
+            }
+        }
+
+		public static void GetRotation(ref Quaternion rot)
+		{
+			float angle = 0f;
+			switch (Screen.orientation)
+			{
+				case ScreenOrientation.Portrait:
+					angle = 90f;
+					break;
+				case ScreenOrientation.LandscapeLeft:
+					angle = 180f;
+					break;
+				case ScreenOrientation.LandscapeRight:
+					angle = 0f;
+					break;
+				case ScreenOrientation.PortraitUpsideDown:
+					angle = -90f;
+					break;
+				default:
+					angle = 0f;
+					break;
+			}
+
+			rot *= Quaternion.Euler(0f, 0f, angle);
+		}
+
+		public static void GetPlaneDataFast(ref IntPtr pixels, XRCpuImage image)
+		{
+			XRCpuImage.Plane plane = image.GetPlane(0);	// use the Y plane
+			int width = image.width, height = image.height;
+
+			if (width == plane.rowStride)
+			{
+				unsafe
+				{
+					pixels = (IntPtr)NativeArrayUnsafeUtility.GetUnsafeBufferPointerWithoutChecks(plane.data);
+				}
+			}
+			else
+			{
+				byte[] data = new byte[width * height];
+
+				unsafe
+				{
+					fixed (byte* dstPtr = data)
+					{
+						byte* srcPtr = (byte*)NativeArrayUnsafeUtility.GetUnsafeBufferPointerWithoutChecks(plane.data);
+						if (width > 0 && height > 0) {
+							UnsafeUtility.MemCpyStride(dstPtr, width, srcPtr, plane.rowStride, width, height);
+						}
+						pixels = (IntPtr)dstPtr;
+					}
+				}
+			}
+		}
+		
+		public static void GetPlaneData(out byte[] pixels, XRCpuImage image)
+		{
+			XRCpuImage.Plane plane = image.GetPlane(0);	// use the Y plane
+			int width = image.width, height = image.height;
+			pixels = new byte[width * height];
+
+			if (width == plane.rowStride)
+			{
+				plane.data.CopyTo(pixels);
+			}
+			else
+			{
+				unsafe
+				{
+					fixed (byte* dstPtr = pixels)
+					{
+						byte* srcPtr = (byte*)NativeArrayUnsafeUtility.GetUnsafeReadOnlyPtr(plane.data);
+						if (width > 0 && height > 0) {
+							UnsafeUtility.MemCpyStride(dstPtr, width, srcPtr, plane.rowStride, width, height);
+						}
+					}
+				}
+			}
+		}
+
+		public static void GetPlaneDataRGB(out byte[] pixels, XRCpuImage image)
+		{
+			var conversionParams = new XRCpuImage.ConversionParams
+			{
+				inputRect = new RectInt(0, 0, image.width, image.height),
+				outputDimensions = new Vector2Int(image.width, image.height),
+				outputFormat = TextureFormat.RGB24,
+				transformation = XRCpuImage.Transformation.None
+			};
+
+			int size = image.GetConvertedDataSize(conversionParams);
+			pixels = new byte[size];
+			GCHandle bufferHandle = GCHandle.Alloc(pixels, GCHandleType.Pinned);
+			image.Convert(conversionParams, bufferHandle.AddrOfPinnedObject(), pixels.Length);
+			bufferHandle.Free();
+		}
+
+		public static bool TryGetTrackingQuality(out int quality)
+		{
+			quality = default;
+
+			if (ImmersalSDK.Instance?.arSession == null)
+				return false;
+						
+			var arSubsystem = ImmersalSDK.Instance?.arSession.subsystem;
+			
+			if (arSubsystem != null && arSubsystem.running)
+			{
+				switch (arSubsystem.trackingState)
+				{
+					case TrackingState.Tracking:
+						quality = 4;
+						break;
+					case TrackingState.Limited:
+						quality = 1;
+						break;
+					case TrackingState.None:
+						quality = 0;
+						break;
+				}
+			}
+
+			return true;
+		}
+	}
+}

+ 11 - 0
Assets/ImmersalSDK/Core/Scripts/AR/ARHelper.cs.meta

@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: f78d5a866c99746cdb1556667e77111c
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 404 - 0
Assets/ImmersalSDK/Core/Scripts/AR/ARLocalizer.cs

@@ -0,0 +1,404 @@
+/*===============================================================================
+Copyright (C) 2023 Immersal - Part of Hexagon. All Rights Reserved.
+
+This file is part of the Immersal SDK.
+
+The Immersal SDK cannot be copied, distributed, or made available to
+third-parties for commercial purposes without written permission of Immersal Ltd.
+
+Contact sales@immersal.com for licensing requests.
+===============================================================================*/
+
+using UnityEngine;
+using System;
+using System.Collections.Generic;
+using UnityEngine.XR.ARFoundation;
+using UnityEngine.XR.ARSubsystems;
+using System.Threading.Tasks;
+using Immersal.REST;
+
+namespace Immersal.AR
+{
+    public class ARLocalizer : LocalizerBase
+	{
+		private static ARLocalizer instance = null;
+		
+		private void ARSessionStateChanged(ARSessionStateChangedEventArgs args)
+		{
+			CheckTrackingState(args.state);
+		}
+
+		private void CheckTrackingState(ARSessionState newState)
+		{
+			isTracking = newState == ARSessionState.SessionTracking;
+
+			if (!isTracking)
+			{
+				foreach (KeyValuePair<Transform, SpaceContainer> item in ARSpace.transformToSpace)
+					item.Value.filter.InvalidateHistory();
+			}
+		}
+
+		public static ARLocalizer Instance
+		{
+			get
+			{
+#if UNITY_EDITOR
+				if (instance == null && !Application.isPlaying)
+				{
+					instance = UnityEngine.Object.FindObjectOfType<ARLocalizer>();
+				}
+#endif
+				if (instance == null)
+				{
+					Debug.LogError("No ARLocalizer instance found. Ensure one exists in the scene.");
+				}
+				return instance;
+			}
+		}
+
+		void Awake()
+		{
+			if (instance == null)
+			{
+				instance = this;
+			}
+			if (instance != this)
+			{
+				Debug.LogError("There must be only one ARLocalizer object in a scene.");
+				UnityEngine.Object.DestroyImmediate(this);
+				return;
+			}
+		}
+
+        public override void Start()
+        {
+            base.Start();
+            m_Sdk.RegisterLocalizer(instance);
+        }
+
+		public override void OnEnable()
+		{
+			base.OnEnable();
+#if !UNITY_EDITOR
+			CheckTrackingState(ARSession.state);
+			ARSession.stateChanged += ARSessionStateChanged;
+#endif
+		}
+
+		public override void OnDisable()
+		{
+#if !UNITY_EDITOR
+			ARSession.stateChanged -= ARSessionStateChanged;
+#endif
+			base.OnDisable();
+		}
+
+        public override async void LocalizeServer(SDKMapId[] mapIds)
+        {
+			if (m_Sdk.cameraManager.TryAcquireLatestCpuImage(out XRCpuImage image))
+            {
+				using (image)
+				{
+					stats.localizationAttemptCount++;
+
+					JobLocalizeServerAsync j = new JobLocalizeServerAsync();
+
+					byte[] pixels;
+					Vector3 camPos = m_Cam.transform.position;
+					Quaternion camRot = m_Cam.transform.rotation;
+					Vector4 intrinsics;
+					int channels = 1;
+					int width = image.width;
+					int height = image.height;
+
+					ARHelper.GetIntrinsics(out intrinsics);
+					ARHelper.GetPlaneData(out pixels, image);
+
+					float startTime = Time.realtimeSinceStartup;
+
+					Task<(byte[], CaptureInfo)> t = Task.Run(() =>
+					{
+						byte[] capture = new byte[channels * width * height + 8192];
+						CaptureInfo info = Immersal.Core.CaptureImage(capture, capture.Length, pixels, width, height, channels);
+						Array.Resize(ref capture, info.captureSize);
+						return (capture, info);
+					});
+
+					await t;
+
+					j.image = t.Result.Item1;
+					j.intrinsics = intrinsics;
+					j.mapIds = mapIds;
+
+					j.OnResult += (SDKLocalizeResult result) =>
+					{
+						float elapsedTime = Time.realtimeSinceStartup - startTime;
+
+						if (result.success)
+						{
+							LocalizerDebugLog(string.Format("Relocalized in {0} seconds", elapsedTime));
+
+							int mapId = result.map;
+
+							if (mapId > 0 && ARSpace.mapIdToMap.ContainsKey(mapId))
+							{
+								ARMap map = ARSpace.mapIdToMap[mapId];
+
+								if (mapId != lastLocalizedMapId)
+								{
+									if (resetOnMapChange)
+									{
+										Reset();
+									}
+									
+									lastLocalizedMapId = mapId;
+
+									OnMapChanged?.Invoke(mapId);
+								}
+
+								MapOffset mo = ARSpace.mapIdToOffset[mapId];
+								stats.localizationSuccessCount++;
+								
+								Matrix4x4 responseMatrix = Matrix4x4.identity;
+								responseMatrix.m00 = result.r00; responseMatrix.m01 = result.r01; responseMatrix.m02 = result.r02; responseMatrix.m03 = result.px;
+								responseMatrix.m10 = result.r10; responseMatrix.m11 = result.r11; responseMatrix.m12 = result.r12; responseMatrix.m13 = result.py;
+								responseMatrix.m20 = result.r20; responseMatrix.m21 = result.r21; responseMatrix.m22 = result.r22; responseMatrix.m23 = result.pz;
+								
+								Vector3 pos = responseMatrix.GetColumn(3);
+								Quaternion rot = responseMatrix.rotation;
+								ARHelper.GetRotation(ref rot);
+								pos = ARHelper.SwitchHandedness(pos);
+								rot = ARHelper.SwitchHandedness(rot);
+
+								Matrix4x4 offsetNoScale = Matrix4x4.TRS(mo.position, mo.rotation, Vector3.one);
+								Vector3 scaledPos = Vector3.Scale(pos, mo.scale);
+								Matrix4x4 cloudSpace = offsetNoScale * Matrix4x4.TRS(scaledPos, rot, Vector3.one);
+								Matrix4x4 trackerSpace = Matrix4x4.TRS(camPos, camRot, Vector3.one);
+								Matrix4x4 m = trackerSpace * (cloudSpace.inverse);
+
+								if (useFiltering)
+									mo.space.filter.RefinePose(m);
+								else
+									ARSpace.UpdateSpace(mo.space, m.GetColumn(3), m.rotation);
+
+								double[] ecef = map.MapToEcefGet();
+								LocalizerBase.GetLocalizerPose(out lastLocalizedPose, mapId, pos, rot, m.inverse, ecef);
+								map.NotifySuccessfulLocalization(mapId);
+								OnPoseFound?.Invoke(lastLocalizedPose);
+							}
+						}
+						else
+						{
+							LocalizerDebugLog(string.Format("Localization attempt failed after {0} seconds", elapsedTime));
+						}
+					};
+
+					await j.RunJobAsync();
+				}
+            }
+
+			base.LocalizeServer(mapIds);
+        }
+
+        public override async void LocalizeGeoPose(SDKMapId[] mapIds)
+        {
+			if (m_Sdk.cameraManager.TryAcquireLatestCpuImage(out XRCpuImage image))
+			{
+				using (image)
+				{
+					stats.localizationAttemptCount++;
+
+					JobGeoPoseAsync j = new JobGeoPoseAsync();
+
+					byte[] pixels;
+					Vector3 camPos = m_Cam.transform.position;
+					Quaternion camRot = m_Cam.transform.rotation;
+					int channels = 1;
+					int width = image.width;
+					int height = image.height;
+
+					j.mapIds = mapIds;
+
+					ARHelper.GetIntrinsics(out j.intrinsics);
+					ARHelper.GetPlaneData(out pixels, image);
+
+					float startTime = Time.realtimeSinceStartup;
+
+					Task<(byte[], CaptureInfo)> t = Task.Run(() =>
+					{
+						byte[] capture = new byte[channels * width * height + 8192];
+						CaptureInfo info = Immersal.Core.CaptureImage(capture, capture.Length, pixels, width, height, channels);
+						Array.Resize(ref capture, info.captureSize);
+						return (capture, info);
+					});
+
+					await t;
+
+					j.image = t.Result.Item1;
+
+					j.OnResult += (SDKGeoPoseResult result) =>
+					{
+						float elapsedTime = Time.realtimeSinceStartup - startTime;
+
+						if (result.success)
+						{
+							LocalizerDebugLog(string.Format("Relocalized in {0} seconds", elapsedTime));
+
+							int mapId = result.map;
+							double latitude = result.latitude;
+							double longitude = result.longitude;
+							double ellipsoidHeight = result.ellipsoidHeight;
+							Quaternion rot = new Quaternion(result.quaternion[1], result.quaternion[2], result.quaternion[3], result.quaternion[0]);
+							LocalizerDebugLog(string.Format("GeoPose returned latitude: {0}, longitude: {1}, ellipsoidHeight: {2}, quaternion: {3}", latitude, longitude, ellipsoidHeight, rot));
+
+							double[] ecef = new double[3];
+							double[] wgs84 = new double[3] { latitude, longitude, ellipsoidHeight };
+							Core.PosWgs84ToEcef(ecef, wgs84);
+
+							if (ARSpace.mapIdToMap.ContainsKey(mapId))
+							{
+								ARMap map = ARSpace.mapIdToMap[mapId];
+
+								if (mapId != lastLocalizedMapId)
+								{
+									if (resetOnMapChange)
+									{
+										Reset();
+									}
+									
+									lastLocalizedMapId = mapId;
+
+									OnMapChanged?.Invoke(mapId);
+								}
+
+								MapOffset mo = ARSpace.mapIdToOffset[mapId];
+								stats.localizationSuccessCount++;
+
+								double[] mapToEcef = map.MapToEcefGet();
+								Vector3 mapPos;
+								Quaternion mapRot;
+								Core.PosEcefToMap(out mapPos, ecef, mapToEcef);
+								Core.RotEcefToMap(out mapRot, rot, mapToEcef);
+								ARHelper.GetRotation(ref mapRot);
+								mapPos = ARHelper.SwitchHandedness(mapPos);
+								mapRot = ARHelper.SwitchHandedness(mapRot);
+
+								Matrix4x4 offsetNoScale = Matrix4x4.TRS(mo.position, mo.rotation, Vector3.one);
+								Vector3 scaledPos = Vector3.Scale(mapPos, mo.scale);
+								Matrix4x4 cloudSpace = offsetNoScale * Matrix4x4.TRS(scaledPos, mapRot, Vector3.one);
+								Matrix4x4 trackerSpace = Matrix4x4.TRS(camPos, camRot, Vector3.one);
+								Matrix4x4 m = trackerSpace*(cloudSpace.inverse);
+								
+								if (useFiltering)
+									mo.space.filter.RefinePose(m);
+								else
+									ARSpace.UpdateSpace(mo.space, m.GetColumn(3), m.rotation);
+
+								LocalizerBase.GetLocalizerPose(out lastLocalizedPose, mapId, cloudSpace.GetColumn(3), cloudSpace.rotation, m.inverse, mapToEcef);
+								map.NotifySuccessfulLocalization(mapId);
+								OnPoseFound?.Invoke(lastLocalizedPose);
+							}
+						}
+						else
+						{
+							LocalizerDebugLog(string.Format("GeoPose localization attempt failed after {0} seconds", elapsedTime));
+						}
+					};
+
+					await j.RunJobAsync();
+				}
+			}
+
+			base.LocalizeGeoPose(mapIds);
+        }
+ 
+        public override async void Localize()
+		{
+			if (m_Sdk.cameraManager.TryAcquireLatestCpuImage(out XRCpuImage image))
+			{
+				using (image)
+				{
+					stats.localizationAttemptCount++;
+					Vector4 intrinsics;
+					Vector3 camPos = m_Cam.transform.position;
+					Quaternion camRot = m_Cam.transform.rotation;
+					ARHelper.GetIntrinsics(out intrinsics);
+					ARHelper.GetPlaneDataFast(ref m_PixelBuffer, image);
+
+					if (m_PixelBuffer != IntPtr.Zero)
+					{
+						float startTime = Time.realtimeSinceStartup;
+
+						Task<LocalizeInfo> t = Task.Run(() =>
+						{
+							return Immersal.Core.LocalizeImage(image.width, image.height, ref intrinsics, m_PixelBuffer);
+						});
+
+						await t;
+
+						LocalizeInfo locInfo = t.Result;
+
+						Matrix4x4 resultMatrix = Matrix4x4.identity;
+						resultMatrix.m00 = locInfo.r00; resultMatrix.m01 = locInfo.r01; resultMatrix.m02 = locInfo.r02; resultMatrix.m03 = locInfo.px;
+						resultMatrix.m10 = locInfo.r10; resultMatrix.m11 = locInfo.r11; resultMatrix.m12 = locInfo.r12; resultMatrix.m13 = locInfo.py;
+						resultMatrix.m20 = locInfo.r20; resultMatrix.m21 = locInfo.r21; resultMatrix.m22 = locInfo.r22; resultMatrix.m23 = locInfo.pz;
+
+						Vector3 pos = resultMatrix.GetColumn(3);
+						Quaternion rot = resultMatrix.rotation;
+
+						int mapHandle = locInfo.handle;
+						int mapId = ARMap.MapHandleToId(mapHandle);
+						float elapsedTime = Time.realtimeSinceStartup - startTime;
+
+						if (mapId > 0 && ARSpace.mapIdToOffset.ContainsKey(mapId))
+						{
+							ARHelper.GetRotation(ref rot);
+							pos = ARHelper.SwitchHandedness(pos);
+							rot = ARHelper.SwitchHandedness(rot);
+
+							LocalizerDebugLog(string.Format("Relocalized in {0} seconds", elapsedTime));
+							stats.localizationSuccessCount++;
+
+							if (mapId != lastLocalizedMapId)
+							{
+								if (resetOnMapChange)
+								{
+									Reset();
+								}
+								
+								lastLocalizedMapId = mapId;
+
+								OnMapChanged?.Invoke(mapId);
+							}
+							
+							MapOffset mo = ARSpace.mapIdToOffset[mapId];
+							Matrix4x4 offsetNoScale = Matrix4x4.TRS(mo.position, mo.rotation, Vector3.one);
+							Vector3 scaledPos = Vector3.Scale(pos, mo.scale);
+							Matrix4x4 cloudSpace = offsetNoScale * Matrix4x4.TRS(scaledPos, rot, Vector3.one);
+							Matrix4x4 trackerSpace = Matrix4x4.TRS(camPos, camRot, Vector3.one);
+							Matrix4x4 m = trackerSpace * (cloudSpace.inverse);
+
+							if (useFiltering)
+								mo.space.filter.RefinePose(m);
+							else
+								ARSpace.UpdateSpace(mo.space, m.GetColumn(3), m.rotation);
+
+							GetLocalizerPose(out lastLocalizedPose, mapId, pos, rot, m.inverse);
+							OnPoseFound?.Invoke(lastLocalizedPose);
+
+							ARMap map = ARSpace.mapIdToMap[mapId];
+							map.NotifySuccessfulLocalization(mapId);
+						}
+						else
+						{
+							LocalizerDebugLog(string.Format("Localization attempt failed after {0} seconds", elapsedTime));
+						}
+					}
+				}
+			}
+
+			base.Localize();
+		}
+	}
+}

+ 11 - 0
Assets/ImmersalSDK/Core/Scripts/AR/ARLocalizer.cs.meta

@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 69bf98867974d4a4ba48cc87a2fde8be
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 546 - 0
Assets/ImmersalSDK/Core/Scripts/AR/ARMap.cs

@@ -0,0 +1,546 @@
+/*===============================================================================
+Copyright (C) 2023 Immersal - Part of Hexagon. All Rights Reserved.
+
+This file is part of the Immersal SDK.
+
+The Immersal SDK cannot be copied, distributed, or made available to
+third-parties for commercial purposes without written permission of Immersal Ltd.
+
+Contact sales@immersal.com for licensing requests.
+===============================================================================*/
+
+using UnityEngine;
+using UnityEngine.Events;
+using System;
+using System.IO;
+using System.Collections.Generic;
+using System.Text.RegularExpressions;
+using System.Threading.Tasks;
+using UnityEngine.Rendering;
+
+namespace Immersal.AR
+{
+    [System.Serializable]
+    public class MapLocalizedEvent : UnityEvent<int>
+    {
+    }
+
+    [ExecuteAlways]
+    public class ARMap : MonoBehaviour
+    {
+        public static readonly Color[] pointCloudColors = new Color[]	{	new Color(0.22f,    1f,     0.46f), 
+																            new Color(0.96f,    0.14f,  0.14f),
+																            new Color(0.16f,    0.69f,  0.95f),
+																            new Color(0.93f,    0.84f,  0.12f),
+																            new Color(0.57f,    0.93f,  0.12f),
+																            new Color(1f,       0.38f,  0.78f),
+																            new Color(0.4f,     0f,     0.9f),
+																            new Color(0.89f,    0.4f,   0f)
+															            };
+
+        public enum RenderMode { DoNotRender, EditorOnly, EditorAndRuntime }
+
+        public static Dictionary<int, ARMap> mapHandleToMap = new Dictionary<int, ARMap>();
+		public static bool pointCloudVisible = true;
+
+        public RenderMode renderMode = RenderMode.EditorOnly;
+        public TextAsset mapFile;
+        
+        [SerializeField]
+        private Color m_PointColor = new Color(0.57f, 0.93f, 0.12f);
+
+        [Space(10)]
+        [Header("Map Metadata")]
+
+        [SerializeField][ReadOnly]
+        private int m_MapId = -1;
+        [SerializeField][ReadOnly]
+        private string m_MapName = null;
+        [ReadOnly]
+        public int privacy;
+        [ReadOnly]
+        public MapAlignment mapAlignment;
+        [ReadOnly]
+        public WGS84 wgs84;
+
+        [Space(10)]
+        [Header("Events")]
+
+        public MapLocalizedEvent OnFirstLocalization = null;
+        protected ARSpace m_ARSpace = null;
+        private bool m_LocalizedOnce = false;
+
+        public Color pointColor
+        {
+            get { return m_PointColor; }
+            set { m_PointColor = value; }
+        }
+
+        public static float pointSize = 0.33f;
+        // public static bool isRenderable = true;
+        public static bool renderAs3dPoints = true;
+
+        [System.Serializable]
+        public struct MapAlignment
+        {
+            public double tx;
+            public double ty;
+            public double tz;
+            public double qx;
+            public double qy;
+            public double qz;
+            public double qw;
+            public double scale;
+        }
+
+        [System.Serializable]
+        public struct WGS84
+        {
+            public double latitude;
+            public double longitude;
+            public double altitude;
+        }
+
+        private Shader m_Shader;
+        private Material m_Material;
+        private Mesh m_Mesh;
+        private MeshFilter m_MeshFilter;
+        private MeshRenderer m_MeshRenderer;
+
+        public Transform root { get; protected set; }
+        public int mapHandle { get; private set; } = -1;
+
+        public int mapId
+        {
+            get => m_MapId;
+            private set => m_MapId = value;
+        }
+
+        public string mapName
+        {
+            get => m_MapName;
+            set => m_MapName = value;
+        }
+
+        public static int MapHandleToId(int handle)
+        {
+            if (mapHandleToMap.ContainsKey(handle))
+            {
+                return mapHandleToMap[handle].mapId;
+            }
+            return -1;
+        }
+
+        public static int MapIdToHandle(int id)
+        {
+            if (ARSpace.mapIdToMap.ContainsKey(id))
+            {
+                return ARSpace.mapIdToMap[id].mapHandle;
+            }
+            return -1;
+        }
+
+        public double[] MapToEcefGet()
+        {
+            double[] q = new double[] {this.mapAlignment.qw, this.mapAlignment.qx, this.mapAlignment.qy, this.mapAlignment.qz};
+            double[] m = new double[9];
+            ARHelper.DoubleQuaternionToDoubleMatrix3x3(out m, q);
+
+            double[] mapToEcef = new double[] {this.mapAlignment.tx, this.mapAlignment.ty, this.mapAlignment.tz, m[0], m[1], m[2], m[3], m[4], m[5], m[6], m[7], m[8], this.mapAlignment.scale};
+
+            return mapToEcef;
+        }
+
+        public virtual void FreeMap(bool destroy = false)
+        {
+            if (mapHandle >= 0)
+            {
+                Immersal.Core.FreeMap(mapHandle);
+
+                if (mapHandleToMap.ContainsKey(mapHandle))
+                {
+                    mapHandleToMap.Remove(mapHandle);
+                }
+            }
+
+            mapHandle = -1;
+            ClearMesh();
+            Reset();
+
+            if (this.mapId > 0)
+            {
+                ARSpace.UnregisterSpace(root, this.mapId);
+                this.mapId = -1;
+            }
+
+            if (destroy)
+            {
+                GameObject.Destroy(gameObject);
+            }
+        }
+
+        public virtual void Reset()
+        {
+            m_LocalizedOnce = false;
+        }
+
+        public virtual async Task<int> LoadMap(byte[] mapBytes = null, int mapId = -1)
+        {
+            if (mapBytes == null)
+            {
+                mapBytes = (mapFile != null) ? mapFile.bytes : null;
+            }
+
+            if (mapBytes != null)
+            {
+                Task<int> t = Task.Run(() =>
+                {
+                    return Immersal.Core.LoadMap(mapBytes);
+                });
+
+                await t;
+
+                mapHandle = t.Result;
+
+                if (this == null)
+                {
+                    FreeMap();
+                    return -1;
+                }
+            }
+
+            if (mapId > 0)
+            {
+                this.mapId = mapId;
+            }
+            else
+            {
+                ParseMapIdAndName();
+            }
+
+            if (mapHandle >= 0)
+            {
+                int pointCloudSize = Immersal.Core.GetPointCloudSize(mapHandle);
+                Vector3[] points = new Vector3[pointCloudSize];
+                Immersal.Core.GetPointCloud(mapHandle, points);
+                for (int i = 0; i < pointCloudSize; i++)
+                {
+                    points[i] = ARHelper.SwitchHandedness(points[i]);
+                }
+                mapHandleToMap[mapHandle] = this;
+                InitializeMesh(points);
+            }
+
+            //Debug.LogFormat("LoadMap() maphandle: {0}, mapID: {1}, name: {2}", mapHandle, this.mapId, mapName);
+
+            if (this.mapId > 0 && m_ARSpace != null)
+            {
+                root = m_ARSpace.transform;
+                ARSpace.RegisterSpace(root, this, transform.localPosition, transform.localRotation, transform.localScale);
+            }
+
+            return mapHandle;
+        }
+
+        private void InitializeMesh(Vector3[] pointPositions)
+        {
+            if (this == null) return;
+
+            if (m_Shader == null)
+            {
+                m_Shader = Shader.Find("Immersal/Point Cloud");
+            }
+
+            if (m_Material == null)
+            {
+                m_Material = new Material(m_Shader);
+                m_Material.hideFlags = HideFlags.DontSave;
+            }
+
+            if (m_Mesh == null)
+            {
+                m_Mesh = new Mesh();
+                m_Mesh.indexFormat = IndexFormat.UInt32;
+            }
+
+            int numPoints = pointPositions.Length;
+
+            int[] indices = new int[numPoints];
+            Vector3[] pts = new Vector3[numPoints];
+            Color32[] col = new Color32[numPoints];
+
+            for (int i = 0; i < numPoints; ++i)
+            {
+                indices[i] = i;
+                pts[i] = pointPositions[i];
+            }
+
+            m_Mesh.Clear();
+            m_Mesh.vertices = pts;
+            m_Mesh.colors32 = col;
+            m_Mesh.SetIndices(indices, MeshTopology.Points, 0);
+            m_Mesh.bounds = new Bounds(transform.position, new Vector3(float.MaxValue, float.MaxValue, float.MaxValue));
+
+            if (m_MeshFilter == null)
+            {
+                m_MeshFilter = gameObject.GetComponent<MeshFilter>();
+                if (m_MeshFilter == null)
+                {
+                    m_MeshFilter = gameObject.AddComponent<MeshFilter>();
+                }
+            }
+
+            if (m_MeshRenderer == null)
+            {
+                m_MeshRenderer = gameObject.GetComponent<MeshRenderer>();
+                if (m_MeshRenderer == null)
+                {
+                    m_MeshRenderer = gameObject.AddComponent<MeshRenderer>();
+                }
+            }
+
+            m_MeshFilter.mesh = m_Mesh;
+            m_MeshRenderer.material = m_Material;
+
+            m_MeshRenderer.shadowCastingMode = UnityEngine.Rendering.ShadowCastingMode.Off;
+            m_MeshRenderer.lightProbeUsage = UnityEngine.Rendering.LightProbeUsage.Off;
+            m_MeshRenderer.reflectionProbeUsage = UnityEngine.Rendering.ReflectionProbeUsage.Off;
+        }
+
+        private void InitializeMesh()
+        {
+            InitializeMesh(new Vector3[0]);
+        }
+
+        private void ClearMesh()
+        {
+            if (m_Mesh != null)
+            {
+                m_Mesh.Clear();
+            }
+        }
+
+        public void NotifySuccessfulLocalization(int mapId)
+        {
+            if (m_LocalizedOnce)
+                return;
+            
+            OnFirstLocalization?.Invoke(mapId);
+            m_LocalizedOnce = true;
+        }
+
+        private void Awake()
+        {
+            m_ARSpace = gameObject.GetComponentInParent<ARSpace>();
+            if (!m_ARSpace)
+            {
+                GameObject go = new GameObject("AR Space");
+                m_ARSpace = go.AddComponent<ARSpace>();
+                transform.SetParent(go.transform);
+            }
+
+            ParseMapIdAndName();
+            InitializeMesh();
+        }
+
+        private void ParseMapIdAndName()
+        {
+            int id;
+            if (GetMapId(out id))
+            {
+                this.mapId = id;
+                this.mapName = mapFile.name.Substring(id.ToString().Length + 1);
+            }
+
+            if (Application.isEditor)
+            {
+                if (mapFile != null)
+                {
+                    try
+                    {
+                        string destinationFolder = Path.Combine("Assets", "Map Data");
+                        string jsonFilePath = Path.Combine(destinationFolder, string.Format("{0}-metadata.json", mapFile.name));
+
+                        MetadataFile metadataFile = JsonUtility.FromJson<MetadataFile>(File.ReadAllText(jsonFilePath));
+                        
+                        this.mapAlignment.tx = metadataFile.tx;
+                        this.mapAlignment.ty = metadataFile.ty;
+                        this.mapAlignment.tz = metadataFile.tz;
+
+                        this.mapAlignment.qx = metadataFile.qx;
+                        this.mapAlignment.qy = metadataFile.qy;
+                        this.mapAlignment.qz = metadataFile.qz;
+                        this.mapAlignment.qw = metadataFile.qw;
+
+                        this.mapAlignment.scale = metadataFile.scale;
+                        
+                        this.wgs84.latitude = metadataFile.latitude;
+                        this.wgs84.longitude = metadataFile.longitude;
+                        this.wgs84.altitude = metadataFile.altitude;
+                        
+                        this.privacy = metadataFile.privacy;
+                    }
+                    catch (FileNotFoundException e)
+                    {
+                        Debug.LogWarningFormat("{0}\nCould not find {1}-metadata.json", e.Message, mapFile.name);
+                        // set default values in case metadata is not available
+                        
+                        this.mapAlignment.tx = 0.0;
+                        this.mapAlignment.ty = 0.0;
+                        this.mapAlignment.tz = 0.0;
+
+                        this.mapAlignment.qx = 0.0;
+                        this.mapAlignment.qy = 0.0;
+                        this.mapAlignment.qz = 0.0;
+                        this.mapAlignment.qw = 1.0;
+
+                        this.mapAlignment.scale = 1.0;
+                        
+                        this.wgs84.latitude = 0.0;
+                        this.wgs84.longitude = 0.0;
+                        this.wgs84.altitude = 0.0;
+
+                        this.privacy = 0;
+                    }
+                }
+            }
+        }
+
+        [System.Serializable]
+        public struct MetadataFile
+        {
+            public string error;
+            public int id;
+            public int type;
+            public string created;
+            public string version;
+            public int user;
+            public int creator;
+            public string name;
+            public int size;
+            public string status;
+            public int privacy;
+            public double latitude;
+            public double longitude;
+            public double altitude;
+            public double tx;
+            public double ty;
+            public double tz;
+            public double qw;
+            public double qx;
+            public double qy;
+            public double qz;
+            public double scale;
+            public string sha256_al;
+            public string sha256_sparse;
+            public string sha256_dense;
+            public string sha256_tex;
+        }
+
+        private bool GetMapId(out int mapId)
+        {
+            if (mapFile == null)
+            {
+                mapId = -1;
+                return false;
+            }
+
+            string mapFileName = mapFile.name;
+            Regex rx = new Regex(@"^\d+");
+            Match match = rx.Match(mapFileName);
+            if (match.Success)
+            {
+                mapId = Int32.Parse(match.Value);
+                return true;
+            }
+            else
+            {
+                mapId = -1;
+                return false;
+            }
+        }
+
+        private async void OnEnable()
+        {
+            if (mapFile != null)
+            {
+                await LoadMap();
+            }
+        }
+
+        private void OnDisable()
+        {
+            FreeMap();
+        }
+
+        private void OnDestroy()
+        {
+            FreeMap();
+
+            if (m_Material != null)
+            {
+                if (Application.isPlaying)
+                {
+                    Destroy(m_Mesh);
+                    Destroy(m_Material);
+                }
+                else
+                {
+                    DestroyImmediate(m_Mesh);
+                    DestroyImmediate(m_Material);
+                }
+            }
+        }
+
+        private bool IsRenderable()
+        {
+            if (pointCloudVisible)
+            {
+                switch (renderMode)
+                {
+                    case RenderMode.DoNotRender:
+                        return false;
+                    case RenderMode.EditorOnly:
+                        if (Application.isEditor)
+                        {
+                            return true;
+                        }
+                        else
+                        {
+                            return false;
+                        }
+                    case RenderMode.EditorAndRuntime:
+                        return true;
+                    default:
+                        return false;
+                }
+            }
+            return false;
+        }
+
+
+        private void OnRenderObject()
+        {
+            if (IsRenderable() && m_Material != null)
+            {
+                m_MeshRenderer.enabled = true;
+
+                if (renderAs3dPoints)
+                {
+                    m_Material.SetFloat("_PerspectiveEnabled", 1f);
+                    m_Material.SetFloat("_PointSize", Mathf.Lerp(0.002f, 0.14f, Mathf.Max(0, Mathf.Pow(pointSize, 3f))));
+                }
+                else
+                {
+                    m_Material.SetFloat("_PerspectiveEnabled", 0f);
+                    m_Material.SetFloat("_PointSize", Mathf.Lerp(1.5f, 40f, Mathf.Max(0, pointSize)));
+                }
+                m_Material.SetColor("_PointColor", m_PointColor);
+            }
+            else
+            {
+                m_MeshRenderer.enabled = false;
+            }
+        }
+    }
+}

+ 14 - 0
Assets/ImmersalSDK/Core/Scripts/AR/ARMap.cs.meta

@@ -0,0 +1,14 @@
+fileFormatVersion: 2
+guid: 6a7ab799a64cb7941b7b41a8e1211b12
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences:
+  - mapFile: {instanceID: 0}
+  - m_Shader: {fileID: 4800000, guid: 9cee2ca6ff2bbdc458a11fe108bb2e5b, type: 3}
+  - m_Sorter: {fileID: 7200000, guid: 333809191cc59694fb5772e17740acfe, type: 3}
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 256 - 0
Assets/ImmersalSDK/Core/Scripts/AR/ARSpace.cs

@@ -0,0 +1,256 @@
+/*===============================================================================
+Copyright (C) 2023 Immersal - Part of Hexagon. All Rights Reserved.
+
+This file is part of the Immersal SDK.
+
+The Immersal SDK cannot be copied, distributed, or made available to
+third-parties for commercial purposes without written permission of Immersal Ltd.
+
+Contact sales@immersal.com for licensing requests.
+===============================================================================*/
+
+using UnityEngine;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using Immersal.REST;
+
+namespace Immersal.AR
+{
+	public class SpaceContainer
+	{
+        public int mapCount = 0;
+		public Vector3 targetPosition = Vector3.zero;
+		public Quaternion targetRotation = Quaternion.identity;
+		public PoseFilter filter = new PoseFilter();
+	}
+
+    public class MapOffset
+    {
+        public Vector3 position;
+        public Quaternion rotation;
+        public Vector3 scale;
+        public SpaceContainer space;
+    }
+
+    public class ARSpace : MonoBehaviour
+    {
+        public static Dictionary<Transform, SpaceContainer> transformToSpace = new Dictionary<Transform, SpaceContainer>();
+        public static Dictionary<SpaceContainer, Transform> spaceToTransform = new Dictionary<SpaceContainer, Transform>();
+        public static Dictionary<int, MapOffset> mapIdToOffset = new Dictionary<int, MapOffset>();
+        public static Dictionary<int, ARMap> mapIdToMap = new Dictionary<int, ARMap>();
+
+        private Matrix4x4 m_InitialOffset = Matrix4x4.identity;
+
+        public Matrix4x4 initialOffset
+        {
+            get { return m_InitialOffset; }
+        }
+
+        void Awake()
+		{
+			Vector3 pos = transform.position;
+			Quaternion rot = transform.rotation;
+			Matrix4x4 offset = Matrix4x4.TRS(pos, rot, Vector3.one);
+
+			m_InitialOffset = offset;
+		}
+
+        public void OnDestroy()
+        {
+            transformToSpace.Clear();
+            spaceToTransform.Clear();
+            mapIdToOffset.Clear();
+            mapIdToMap.Clear();
+        }
+
+        public Pose ToCloudSpace(Vector3 camPos, Quaternion camRot)
+		{
+			Matrix4x4 trackerSpace = Matrix4x4.TRS(camPos, camRot, Vector3.one);
+			Matrix4x4 trackerToCloudSpace = Matrix4x4.TRS(transform.position, transform.rotation, Vector3.one);
+			Matrix4x4 cloudSpace = trackerToCloudSpace.inverse * trackerSpace;
+
+			return new Pose(cloudSpace.GetColumn(3), cloudSpace.rotation);
+		}
+
+		public Pose FromCloudSpace(Vector3 camPos, Quaternion camRot)
+		{
+			Matrix4x4 cloudSpace = Matrix4x4.TRS(transform.position, transform.rotation, Vector3.one);
+			Matrix4x4 trackerSpace = Matrix4x4.TRS(camPos, camRot, Vector3.one);
+			Matrix4x4 m = trackerSpace * (cloudSpace.inverse);
+
+			return new Pose(m.GetColumn(3), m.rotation);
+		}
+
+		public static async Task<ARMap> LoadAndInstantiateARMap(Transform root, SDKMapResult map, ARMap.RenderMode renderMode = ARMap.RenderMode.DoNotRender, Color pointCloudColor = default, bool applyAlignment = false)
+		{
+			GameObject go = new GameObject(string.Format("AR Map {0}-{1}", map.metadata.id, map.metadata.name));
+            if (root != null)
+            {
+                go.transform.SetParent(root, false);
+            }
+
+            if (applyAlignment)
+            {
+                Matrix4x4 b = Matrix4x4.TRS(new Vector3((float)map.metadata.tx, (float)map.metadata.ty, (float)map.metadata.tz), 
+                    new Quaternion((float)map.metadata.qx, (float)map.metadata.qy, (float)map.metadata.qz, (float)map.metadata.qw), 
+                    new Vector3((float)map.metadata.scale, (float)map.metadata.scale, (float)map.metadata.scale)
+                );
+                Matrix4x4 a = ARHelper.SwitchHandedness(b);
+                go.transform.localPosition = a.GetColumn(3);
+                go.transform.localRotation = a.rotation;
+                go.transform.localScale = new Vector3(1.0f, 1.0f, 1.0f);
+            }
+
+			ARMap arMap = go.AddComponent<ARMap>();
+            arMap.mapName = map.metadata.name;
+            arMap.privacy = map.metadata.privacy;
+
+            arMap.mapAlignment.tx = map.metadata.tx;
+            arMap.mapAlignment.ty = map.metadata.ty;
+            arMap.mapAlignment.tz = map.metadata.tz;
+            arMap.mapAlignment.qx = map.metadata.qx;
+            arMap.mapAlignment.qy = map.metadata.qy;
+            arMap.mapAlignment.qz = map.metadata.qz;
+            arMap.mapAlignment.qw = map.metadata.qw;
+
+            // TODO: Scale support
+            arMap.mapAlignment.scale = 1.0;
+
+            arMap.wgs84.latitude = map.metadata.latitude;
+            arMap.wgs84.longitude = map.metadata.longitude;
+            arMap.wgs84.altitude = map.metadata.altitude;
+            
+            arMap.pointColor = pointCloudColor;
+            arMap.renderMode = renderMode;
+
+            await arMap.LoadMap(map.mapData, map.metadata.id);
+
+            return arMap;
+		}
+
+		public static async Task<ARMap> LoadAndInstantiateARMap(Transform root, SDKJob map, byte[] mapData = null, ARMap.RenderMode renderMode = ARMap.RenderMode.DoNotRender, Color pointCloudColor = default, bool applyAlignment = false)
+		{
+			GameObject go = new GameObject(string.Format("AR Map {0}-{1}", map.id, map.name));
+            if (root != null)
+            {
+                go.transform.SetParent(root, false);
+            }
+
+			ARMap arMap = go.AddComponent<ARMap>();
+            arMap.mapName = map.name;
+            arMap.privacy = map.privacy;
+            arMap.pointColor = pointCloudColor;
+            arMap.renderMode = renderMode;
+
+            JobMapMetadataGetAsync j = new JobMapMetadataGetAsync();
+            j.id = map.id;
+            j.token = map.privacy == (int)SDKJobPrivacy.Private ? ImmersalSDK.Instance.developerToken : "";
+            j.OnResult += (SDKMapMetadataGetResult metadata) => 
+            {
+                if (metadata.error == "none")
+                {
+                    arMap.mapAlignment.tx = metadata.tx;
+                    arMap.mapAlignment.ty = metadata.ty;
+                    arMap.mapAlignment.tz = metadata.tz;
+                    arMap.mapAlignment.qx = metadata.qx;
+                    arMap.mapAlignment.qy = metadata.qy;
+                    arMap.mapAlignment.qz = metadata.qz;
+                    arMap.mapAlignment.qw = metadata.qw;
+
+                    // TODO: Scale support
+                    arMap.mapAlignment.scale = 1.0;
+
+                    arMap.wgs84.latitude = metadata.latitude;
+                    arMap.wgs84.longitude = metadata.longitude;
+                    arMap.wgs84.altitude = metadata.altitude;
+
+                    if (applyAlignment)
+                    {
+                        Matrix4x4 b = Matrix4x4.TRS(new Vector3((float)metadata.tx, (float)metadata.ty, (float)metadata.tz), 
+                            new Quaternion((float)metadata.qx, (float)metadata.qy, (float)metadata.qz, (float)metadata.qw), 
+                            new Vector3((float)metadata.scale, (float)metadata.scale, (float)metadata.scale)
+                        );
+                        Matrix4x4 a = ARHelper.SwitchHandedness(b);
+                        go.transform.localPosition = a.GetColumn(3);
+                        go.transform.localRotation = a.rotation;
+                        go.transform.localScale = new Vector3(1.0f, 1.0f, 1.0f);
+                    }
+                }
+            };
+
+            await j.RunJobAsync();
+
+            await arMap.LoadMap(mapData, map.id);
+
+            return arMap;
+		}
+
+        public static void RegisterSpace(Transform tr, ARMap map, Vector3 offsetPosition, Quaternion offsetRotation, Vector3 offsetScale)
+		{
+			if (tr == null)
+				return;
+			
+            SpaceContainer sc;
+
+            if (!transformToSpace.ContainsKey(tr))
+            {
+                sc = new SpaceContainer();
+                transformToSpace[tr] = sc;
+            }
+            else
+            {
+                sc = transformToSpace[tr];
+            }
+
+            spaceToTransform[sc] = tr;
+
+            sc.mapCount++;
+
+            MapOffset mo = new MapOffset();
+            mo.position = offsetPosition;
+            mo.rotation = offsetRotation;
+            mo.scale = offsetScale;
+            mo.space = sc;
+
+            mapIdToOffset[map.mapId] = mo;
+            mapIdToMap[map.mapId] = map;
+		}
+
+        public static void RegisterSpace(Transform tr, ARMap map)
+        {
+            RegisterSpace(tr, map, Vector3.zero, Quaternion.identity, Vector3.one);
+        }
+
+        public static void UnregisterSpace(Transform tr, int mapId)
+		{
+			if (tr == null)
+				return;
+			
+			if (transformToSpace.ContainsKey(tr))
+			{
+				SpaceContainer sc = transformToSpace[tr];
+				if (--sc.mapCount == 0)
+                {
+					transformToSpace.Remove(tr);
+                    spaceToTransform.Remove(sc);
+                }
+				if (mapIdToOffset.ContainsKey(mapId))
+					mapIdToOffset.Remove(mapId);
+                if (mapIdToMap.ContainsKey(mapId))
+                    mapIdToMap.Remove(mapId);
+			}
+		}
+
+		public static void UpdateSpace(SpaceContainer space, Vector3 pos, Quaternion rot)
+        {
+	        if (space == null)
+		        return;
+	        
+            if (spaceToTransform.ContainsKey(space))
+            {
+                Transform tr = spaceToTransform[space];
+        		tr.SetPositionAndRotation(pos, rot);
+            }
+		}
+    }
+}

+ 11 - 0
Assets/ImmersalSDK/Core/Scripts/AR/ARSpace.cs.meta

@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 53a086897bd294848aa39db1a6b2e6ed
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 8 - 0
Assets/ImmersalSDK/Core/Scripts/AR/Editor.meta

@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: d5323759a599bec4780d2cd0291d140c
+folderAsset: yes
+DefaultImporter:
+  externalObjects: {}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 312 - 0
Assets/ImmersalSDK/Core/Scripts/AR/Editor/ARMapEditor.cs

@@ -0,0 +1,312 @@
+/*===============================================================================
+Copyright (C) 2023 Immersal - Part of Hexagon. All Rights Reserved.
+
+This file is part of the Immersal SDK.
+
+The Immersal SDK cannot be copied, distributed, or made available to
+third-parties for commercial purposes without written permission of Immersal Ltd.
+
+Contact sales@immersal.com for licensing requests.
+===============================================================================*/
+
+#if UNITY_EDITOR
+using UnityEngine;
+using UnityEditor;
+using UnityEngine.Networking;
+using System.IO;
+using System.Collections;
+using System.Threading.Tasks;
+using Unity.EditorCoroutines.Editor;
+using Immersal.REST;
+
+namespace Immersal.AR
+{
+    [CustomEditor(typeof(ARMap))]
+    public class ARMapEditor : Editor
+    {
+        private ImmersalSDK m_Sdk = null;
+
+        private static float pointSizeSliderValue = 0.33f;
+        private static bool renderAs3dPointsToggle = true;
+
+        private TextAsset currentMapFile = null;
+        private TextAsset prevMapFile = null;
+
+        private void OnEnable()
+        {
+            pointSizeSliderValue = EditorPrefs.GetFloat("pointSizeSliderValue", pointSizeSliderValue);
+            renderAs3dPointsToggle = EditorPrefs.GetBool("pointSizeSliderValue", renderAs3dPointsToggle);
+        }
+
+        public override void OnInspectorGUI()
+        {
+            ARMap obj = (ARMap)target;
+
+            // reload map file without using OnValidate() on ARMap.cs
+            currentMapFile = obj.mapFile;
+            if (currentMapFile != prevMapFile || prevMapFile == null)
+            {
+                if (currentMapFile != null)
+                {
+                    if (AssetDatabase.GetAssetPath(currentMapFile).EndsWith(".bytes"))
+                    {
+                        EditorCoroutineUtility.StartCoroutine(DoLoadMap(), this);
+                        prevMapFile = currentMapFile;
+                        UnityEditorInternal.InternalEditorUtility.RepaintAllViews();
+                    }
+                    else
+                    {
+                        Debug.LogFormat("{0} is not a valid map file", AssetDatabase.GetAssetPath(currentMapFile));
+                    }
+                }
+            }
+
+            EditorGUI.BeginChangeCheck();
+            pointSizeSliderValue = EditorGUILayout.Slider("Point Size", pointSizeSliderValue, 0f, 1f);
+            renderAs3dPointsToggle = EditorGUILayout.Toggle("Render as 3D Points", renderAs3dPointsToggle);
+            if (EditorGUI.EndChangeCheck())
+            {
+                ARMap.pointSize = pointSizeSliderValue;
+                ARMap.renderAs3dPoints = renderAs3dPointsToggle;
+
+                EditorPrefs.SetFloat("pointSizeSliderValue", pointSizeSliderValue);
+                EditorPrefs.SetBool("pointSizeSliderValue", renderAs3dPointsToggle);
+
+                UnityEditorInternal.InternalEditorUtility.RepaintAllViews();
+            }
+
+            base.OnInspectorGUI();
+
+            EditorGUILayout.HelpBox("Alignment metadata stored in right-handed coordinate system. Captured (default) alignment is in ECEF coordinates", MessageType.Info);
+
+            GUILayout.BeginHorizontal();
+            if (GUILayout.Button(new GUIContent("Load Alignment", "Loads alignment from map metadata. Coordinate system is unknown (ECEF or Unity's)")))
+            {
+                EditorCoroutineUtility.StartCoroutine(MapAlignmentLoad(), this);
+            }
+
+            if (GUILayout.Button(new GUIContent("Save Alignment", "Saves current (local transform) alignment to map metadata")))
+            {
+                EditorCoroutineUtility.StartCoroutine(MapAlignmentSave(), this);
+            }
+
+            Color oldColor = GUI.backgroundColor;
+            GUI.backgroundColor = new Color(1f, 0.5f, 0.6f);
+
+            if (GUILayout.Button(new GUIContent("Reset Alignment", "Fetches the original captured alignment metadata in ECEF coordinates")))
+            {
+                EditorCoroutineUtility.StartCoroutine(MapAlignmentReset(), this);
+            }
+
+            GUI.backgroundColor = oldColor;
+
+            GUILayout.EndHorizontal();
+        }
+
+        private IEnumerator DoLoadMap()
+        {
+            ARMap obj = (ARMap)target;
+            Task task = obj.LoadMap();
+            yield return new WaitUntil(() => task.IsCompleted);
+        }
+
+        private IEnumerator MapAlignmentLoad()
+        {
+            //
+            // Loads map metadata, updates AR Map metadata info, extracts the alignment, converts it to Unity's coordinate system and sets the map transform
+            //
+
+            ARMap obj = (ARMap)target;
+            m_Sdk = ImmersalSDK.Instance;
+
+            // Load map metadata from Immersal Cloud Service
+            SDKMapMetadataGetRequest r = new SDKMapMetadataGetRequest();
+            r.token = m_Sdk.developerToken;
+            r.id = obj.mapId;
+
+            string jsonString = JsonUtility.ToJson(r);
+            UnityWebRequest request = UnityWebRequest.Put(string.Format(ImmersalHttp.URL_FORMAT, ImmersalSDK.Instance.localizationServer, SDKMapMetadataGetRequest.endpoint), jsonString);
+            request.method = UnityWebRequest.kHttpVerbPOST;
+            request.useHttpContinue = false;
+            request.SetRequestHeader("Content-Type", "application/json");
+            request.SetRequestHeader("Accept", "application/json");
+            request.SendWebRequest();
+
+            while (!request.isDone)
+            {
+                yield return null;
+            }
+
+#if UNITY_2020_1_OR_NEWER
+            if (request.result != UnityWebRequest.Result.Success)
+#else
+            if (request.isNetworkError || request.isHttpError)
+#endif
+            {
+                Debug.LogError(string.Format("Failed to load alignment metadata for map id {0}\n{1}", obj.mapId, request.error));
+            }
+            else
+            {
+                SDKMapMetadataGetResult result = JsonUtility.FromJson<SDKMapMetadataGetResult>(request.downloadHandler.text);
+                if (result.error == "none")
+                {
+                    // Save metadata file on disk, overwrite existing file
+                    string destinationFolder = Path.Combine("Assets", "Map Data");
+                    string jsonFilePath = Path.Combine(destinationFolder, string.Format("{0}-{1}-metadata.json", result.id, result.name));
+                    WriteJson(jsonFilePath, request.downloadHandler.text);
+
+                    Vector3 posMetadata = new Vector3((float)result.tx, (float)result.ty, (float)result.tz);
+                    Quaternion rotMetadata = new Quaternion((float)result.qx, (float)result.qy, (float)result.qz, (float)result.qw);
+                    float scaleMetadata = (float)result.scale; // Only uniform scale metadata is supported
+
+                    // Update metadata information on AR Map
+                    obj.mapAlignment.tx = posMetadata.x;
+                    obj.mapAlignment.ty = posMetadata.y;
+                    obj.mapAlignment.tz = posMetadata.z;
+                    obj.mapAlignment.qx = rotMetadata.x;
+                    obj.mapAlignment.qy = rotMetadata.y;
+                    obj.mapAlignment.qz = rotMetadata.z;
+                    obj.mapAlignment.qw = rotMetadata.w;
+                    obj.mapAlignment.scale = scaleMetadata;
+
+                    obj.wgs84.latitude = result.latitude;
+                    obj.wgs84.longitude = result.longitude;
+                    obj.wgs84.altitude = result.altitude;
+
+                    obj.privacy = result.privacy;
+
+                    // IMPORTANT
+                    // Switch coordinate system handedness back from Immersal Cloud Service's default right-handed system to Unity's left-handed system
+                    Matrix4x4 b = Matrix4x4.TRS(posMetadata, rotMetadata, new Vector3(scaleMetadata, scaleMetadata, scaleMetadata));
+                    Matrix4x4 a = ARHelper.SwitchHandedness(b);
+                    Vector3 pos = a.GetColumn(3);
+                    Quaternion rot = a.rotation;
+                    Vector3 scl = new Vector3(scaleMetadata, scaleMetadata, scaleMetadata); // Only uniform scale metadata is supported
+
+                    // Set AR Map local transform from the converted metadata
+                    obj.transform.localPosition = pos;
+                    obj.transform.localRotation = rot;
+                    obj.transform.localScale = scl;
+                }
+            }
+        }
+
+        private IEnumerator MapAlignmentSave()
+        {
+            //
+            // Updates map metadata to the Cloud Service and reloads to keep local files in sync
+            //
+
+            ARMap obj = (ARMap)target;
+            m_Sdk = ImmersalSDK.Instance;
+
+            Vector3 pos = obj.transform.localPosition;
+            Quaternion rot = obj.transform.localRotation;
+            float scl = (obj.transform.localScale.x + obj.transform.localScale.y + obj.transform.localScale.z) / 3f; // Only uniform scale metadata is supported
+
+            // IMPORTANT
+            // Switching coordinate system handedness from Unity's left-handed system to Immersal Cloud Service's default right-handed system
+            Matrix4x4 b = Matrix4x4.TRS(pos, rot, obj.transform.localScale);
+            Matrix4x4 a = ARHelper.SwitchHandedness(b);
+            pos = a.GetColumn(3);
+            rot = a.rotation;
+
+            // Update map alignment metadata to Immersal Cloud Service
+            SDKMapAlignmentSetRequest r = new SDKMapAlignmentSetRequest();
+            r.token = m_Sdk.developerToken;
+            r.id = obj.mapId;
+            r.tx = pos.x;
+            r.ty = pos.y;
+            r.tz = pos.z;
+            r.qx = rot.x;
+            r.qy = rot.y;
+            r.qz = rot.z;
+            r.qw = rot.w;
+            r.scale = scl;
+
+            string jsonString = JsonUtility.ToJson(r);
+            UnityWebRequest request = UnityWebRequest.Put(string.Format(ImmersalHttp.URL_FORMAT, ImmersalSDK.Instance.localizationServer, SDKMapAlignmentSetRequest.endpoint), jsonString);
+            request.method = UnityWebRequest.kHttpVerbPOST;
+            request.useHttpContinue = false;
+            request.SetRequestHeader("Content-Type", "application/json");
+            request.SetRequestHeader("Accept", "application/json");
+            request.SendWebRequest();
+
+            while (!request.isDone)
+            {
+                yield return null;
+            }
+
+#if UNITY_2020_1_OR_NEWER
+            if (request.result != UnityWebRequest.Result.Success)
+#else
+            if (request.isNetworkError || request.isHttpError)
+#endif
+            {
+                Debug.LogError(string.Format("Failed to save alignment for map id {0}\n{1}", obj.mapId, request.error));
+            }
+            else
+            {
+                SDKMapAlignmentSetResult result = JsonUtility.FromJson<SDKMapAlignmentSetResult>(request.downloadHandler.text);
+                if (result.error == "none")
+                {
+                    // Reload the metadata from Immersal Cloud Service to keep local files in sync
+                    EditorCoroutineUtility.StartCoroutine(MapAlignmentLoad(), this);
+                }
+            }
+        }
+
+        private IEnumerator MapAlignmentReset()
+        {
+            //
+            // Reset map alignment to the original captured data and reload metadata from the Immersal Cloud Service to keep local files in sync
+            //
+
+            ARMap obj = (ARMap)target;
+            m_Sdk = ImmersalSDK.Instance;
+
+            // Reset alignment on Immersal Cloud Service
+            SDKMapAlignmentResetRequest r = new SDKMapAlignmentResetRequest();
+            r.token = m_Sdk.developerToken;
+            r.id = obj.mapId;
+
+            string jsonString = JsonUtility.ToJson(r);
+            UnityWebRequest request = UnityWebRequest.Put(string.Format(ImmersalHttp.URL_FORMAT, ImmersalSDK.Instance.localizationServer, SDKMapAlignmentResetRequest.endpoint), jsonString);
+            request.method = UnityWebRequest.kHttpVerbPOST;
+            request.useHttpContinue = false;
+            request.SetRequestHeader("Content-Type", "application/json");
+            request.SetRequestHeader("Accept", "application/json");
+            request.SendWebRequest();
+
+            while (!request.isDone)
+            {
+                yield return null;
+            }
+
+#if UNITY_2020_1_OR_NEWER
+            if (request.result != UnityWebRequest.Result.Success)
+#else
+            if (request.isNetworkError || request.isHttpError)
+#endif
+            {
+                Debug.LogError(string.Format("Failed to reset alignment for map id {0}\n{1}", obj.mapId, request.error));
+            }
+            else
+            {
+                SDKMapAlignmentResetResult result = JsonUtility.FromJson<SDKMapAlignmentResetResult>(request.downloadHandler.text);
+                if (result.error == "none")
+                {
+                    // Reload the metadata from Immersal Cloud Service to keep local files in sync
+                    EditorCoroutineUtility.StartCoroutine(MapAlignmentLoad(), this);
+                }
+            }
+        }
+
+        private void WriteJson(string jsonFilepath, string data)
+        {
+            File.WriteAllText(jsonFilepath, data);
+            AssetDatabase.ImportAsset(jsonFilepath);
+        }
+    }
+}
+#endif

+ 11 - 0
Assets/ImmersalSDK/Core/Scripts/AR/Editor/ARMapEditor.cs.meta

@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 44c3933e9650d5d4ab8a280c8c8ee305
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 27 - 0
Assets/ImmersalSDK/Core/Scripts/AR/Editor/ARSpaceEditor.cs

@@ -0,0 +1,27 @@
+/*===============================================================================
+Copyright (C) 2023 Immersal - Part of Hexagon. All Rights Reserved.
+
+This file is part of the Immersal SDK.
+
+The Immersal SDK cannot be copied, distributed, or made available to
+third-parties for commercial purposes without written permission of Immersal Ltd.
+
+Contact sales@immersal.com for licensing requests.
+===============================================================================*/
+
+#if UNITY_EDITOR
+using UnityEditor;
+
+namespace Immersal.AR
+{
+    [CustomEditor(typeof(ARSpace))]
+    public class ARSpaceEditor : Editor
+    {
+        public override void OnInspectorGUI()
+        {
+            EditorGUILayout.HelpBox("Place AR Maps and all content under this object", MessageType.Info);
+            DrawDefaultInspector();
+        }
+    }
+}
+#endif

+ 11 - 0
Assets/ImmersalSDK/Core/Scripts/AR/Editor/ARSpaceEditor.cs.meta

@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 8f310f75bcad8f446a8339c427df60ad
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 350 - 0
Assets/ImmersalSDK/Core/Scripts/AR/LocalizerBase.cs

@@ -0,0 +1,350 @@
+/*===============================================================================
+Copyright (C) 2023 Immersal - Part of Hexagon. All Rights Reserved.
+
+This file is part of the Immersal SDK.
+
+The Immersal SDK cannot be copied, distributed, or made available to
+third-parties for commercial purposes without written permission of Immersal Ltd.
+
+Contact sales@immersal.com for licensing requests.
+===============================================================================*/
+
+using UnityEngine;
+using System;
+using System.Collections.Generic;
+using Immersal.REST;
+
+namespace Immersal.AR
+{
+	public class LocalizerStats
+	{
+		public int localizationAttemptCount = 0;
+		public int localizationSuccessCount = 0;
+	}
+
+	public struct LocalizerPose
+	{
+		public bool valid;
+		public double[] mapToEcef;
+		public Matrix4x4 matrix;
+		public Pose lastUpdatedPose;
+		public double vLatitude;
+		public double vLongitude;
+		public double vAltitude;
+	}
+
+    public abstract class LocalizerBase : MonoBehaviour
+	{
+		[Tooltip("Start localizing at app startup")]
+		[SerializeField]
+		protected bool m_AutoStart = true;
+		[Tooltip("Time between localization requests in seconds")]
+		public float localizationInterval = 2.0f;
+		[Tooltip("Filter localizer poses for smoother results")]
+		[SerializeField]
+		protected bool m_UseFiltering = true;
+		[Tooltip("Reset localizer filtering when relocalized against a different map than the previous time")]
+		[SerializeField]
+		protected bool m_ResetOnMapChange = false;
+		[Tooltip("Try to localize at maximum speed at app startup / resume")]
+		[SerializeField]
+		protected bool m_BurstMode = true;
+		[Tooltip("Use the on-server GeoPose localizer")]
+		[SerializeField]
+		protected bool m_UseGeoPoseLocalizer = false;
+		[Tooltip("Use the on-server localizer")]
+		[SerializeField]
+		protected bool m_UseServerLocalizer = false;
+		[Tooltip("Optional server map IDs when the on-server localizer is used")]
+		[SerializeField]
+		protected SDKMapId[] m_MapIds = new SDKMapId[] { };
+		[SerializeField]
+		protected bool m_EnableLogging;
+
+		public LocalizerStats stats { get; protected set; } = new LocalizerStats();
+		public int lastLocalizedMapId { get; protected set; }
+		public LocalizerPose lastLocalizedPose = default;
+        public bool isTracking { get; protected set; }
+        public bool isLocalizing { get; protected set; }
+
+		public Action<LocalizerPose> OnPoseFound;
+		public Action<int> OnMapChanged;
+		public Action OnReset;
+		
+		protected ImmersalSDK m_Sdk = null;
+		protected IntPtr m_PixelBuffer = IntPtr.Zero;
+		protected float m_LastLocalizeTime = 0.0f;
+		protected float m_BurstStartTime = 0.0f;
+		protected bool m_BurstModeActive = false;
+		protected bool m_LocalizeContinuously = false;
+		protected Camera m_Cam = null;
+		protected float m_WarpThresholdDistSq = 5.0f * 5.0f;
+		protected float m_WarpThresholdCosAngle = Mathf.Cos(20.0f * Mathf.PI / 180.0f);
+
+		public bool burstMode
+		{
+			get { return m_BurstMode; }
+			set
+			{
+				SetBurstMode(value);
+			}
+		}
+
+		public bool useFiltering
+		{
+			get { return m_UseFiltering; }
+			set { m_UseFiltering = value; }
+		}
+
+		public bool resetOnMapChange
+		{
+			get { return m_ResetOnMapChange; }
+			set { m_ResetOnMapChange = value; }
+		}
+
+		public bool autoStart
+		{
+			get { return m_AutoStart; }
+			set
+			{
+				m_AutoStart = value;
+				SetContinuousLocalization(value);
+			}
+		}
+
+		public bool useServerLocalizer
+		{
+			get { return m_UseServerLocalizer; }
+			set { m_UseServerLocalizer = value; }
+		}
+
+		public bool useGeoPoseLocalizer
+		{
+			get { return m_UseGeoPoseLocalizer; }
+			set { m_UseGeoPoseLocalizer = value; }
+		}
+
+		public SDKMapId[] mapIds
+		{
+			get { return m_MapIds; }
+			set { m_MapIds = value; }
+		}
+
+        #region Virtual methods
+
+		public virtual void Start()
+		{
+			m_Sdk = ImmersalSDK.Instance;
+			lastLocalizedMapId = -1;
+			SetBurstMode(burstMode);
+			SetContinuousLocalization(autoStart);
+		}
+
+		public virtual void OnEnable()
+		{
+			m_Cam = Camera.main;
+		}
+
+		public virtual void OnDisable()
+		{
+			isTracking = false;
+		}
+
+		public virtual void OnDestroy()
+		{
+			m_PixelBuffer = IntPtr.Zero;
+		}
+
+		public virtual void OnApplicationPause(bool pauseStatus)
+		{
+			Reset();
+			
+			if (!pauseStatus)
+				SetBurstMode(burstMode);
+		}
+
+		public virtual void Localize()
+		{
+			LocalizerDebugLog(string.Format("Successful localizations: {0}/{1}", stats.localizationSuccessCount, stats.localizationAttemptCount));
+			isLocalizing = false;
+		}
+
+		public virtual void LocalizeServer(SDKMapId[] mapIds)
+		{
+			LocalizerDebugLog(string.Format("Successful localizations: {0}/{1}", stats.localizationSuccessCount, stats.localizationAttemptCount));
+			isLocalizing = false;
+		}
+
+		public virtual void LocalizeGeoPose(SDKMapId[] mapIds)
+		{
+			LocalizerDebugLog(string.Format("Successful localizations: {0}/{1}", stats.localizationSuccessCount, stats.localizationAttemptCount));
+			isLocalizing = false;
+		}
+
+		public virtual void Reset()
+		{
+			lastLocalizedMapId = -1;
+			
+			stats.localizationAttemptCount = stats.localizationSuccessCount = 0;
+			SetBurstMode(burstMode);
+
+			foreach (KeyValuePair<Transform, SpaceContainer> item in ARSpace.transformToSpace)
+				item.Value.filter.ResetFiltering();
+			
+			OnReset?.Invoke();
+		}
+
+		public virtual void StartLocalizing()
+		{
+			Reset();
+			SetContinuousLocalization(autoStart);
+		}
+
+		public virtual void StopLocalizing()
+		{
+			SetContinuousLocalization(false);
+			Reset();
+		}
+
+		public virtual void Pause()
+		{
+			SetContinuousLocalization(false);
+		}
+
+		public virtual void Resume()
+		{
+			SetContinuousLocalization(true);
+		}
+
+		protected virtual void LocalizerDebugLog(string message)
+		{
+			if (m_EnableLogging)
+			{
+				Debug.LogFormat("[{0}]: {1}", this.GetType().Name, message);
+			}
+		}
+
+		protected virtual void Update()
+		{
+			if (!m_LocalizeContinuously)
+				return;
+			
+			if (ARSpace.transformToSpace.Count == 0)
+			{
+				m_BurstStartTime = Time.unscaledTime;
+				return;
+			}
+			
+			if (useFiltering)
+			{
+				foreach (KeyValuePair<Transform, SpaceContainer> item in ARSpace.transformToSpace)
+				{
+					float distSq = (item.Value.filter.position - item.Value.targetPosition).sqrMagnitude;
+					float cosAngle = Quaternion.Dot(item.Value.filter.rotation, item.Value.targetRotation);
+					if (item.Value.filter.SampleCount() == 1 || distSq > m_WarpThresholdDistSq || cosAngle < m_WarpThresholdCosAngle)
+					{
+						item.Value.targetPosition = item.Value.filter.position;
+						item.Value.targetRotation = item.Value.filter.rotation;
+					}
+					else
+					{
+						float smoothing = 0.025f;
+						float steps = Time.deltaTime / (1.0f / 60.0f);
+						if (steps < 1.0f)
+							steps = 1.0f;
+						else if (steps > 6.0f)
+							steps = 6.0f;
+						float alpha = 1.0f - Mathf.Pow(1.0f - smoothing, steps);
+
+						item.Value.targetRotation = Quaternion.Slerp(item.Value.targetRotation, item.Value.filter.rotation, alpha);
+						item.Value.targetPosition = Vector3.Lerp(item.Value.targetPosition, item.Value.filter.position, alpha);
+					}
+					ARSpace.UpdateSpace(item.Value, item.Value.targetPosition, item.Value.targetRotation);
+				}
+			}
+
+			float curTime = Time.unscaledTime;
+			if (m_BurstModeActive)	// try to localize at max speed during app start/resume
+			{
+				if (!isLocalizing && isTracking)
+				{
+					float elapsedTime = curTime - m_BurstStartTime;
+					isLocalizing = true;
+
+					if (useGeoPoseLocalizer && mapIds.Length > 0)
+					{
+						LocalizeGeoPose(mapIds);
+					}
+					else if (useServerLocalizer && mapIds.Length > 0)
+					{
+						LocalizeServer(mapIds);
+					}
+					else
+					{
+						Localize();
+					}
+					if (stats.localizationSuccessCount == 10 || elapsedTime >= 15f)
+					{
+						m_BurstModeActive = false;
+					}
+				}
+			}
+
+			if (!isLocalizing && isTracking && (curTime - m_LastLocalizeTime) >= localizationInterval)
+			{
+				m_LastLocalizeTime = curTime;
+				isLocalizing = true;
+
+				if (useGeoPoseLocalizer && mapIds.Length > 0)
+				{
+					LocalizeGeoPose(mapIds);
+				}
+				else if (useServerLocalizer && mapIds.Length > 0)
+				{
+					LocalizeServer(mapIds);
+				}
+				else
+				{
+					Localize();
+				}
+			}
+		}
+		
+        #endregion
+
+		private void SetBurstMode(bool on)
+		{
+			m_BurstStartTime = Time.unscaledTime;
+			m_BurstModeActive = on;
+		}
+
+		private void SetContinuousLocalization(bool on)
+		{
+			m_LocalizeContinuously = on;
+		}
+
+		public static void GetLocalizerPose(out LocalizerPose localizerPose, int mapId, Vector3 pos, Quaternion rot, Matrix4x4 m, double[] mapToEcef = null)
+		{
+			localizerPose = default;
+
+			if (mapToEcef == null)
+			{
+				mapToEcef = ARSpace.mapIdToMap[mapId].MapToEcefGet();
+			}
+
+			double[] wgs84 = new double[3];
+			int r = Immersal.Core.PosMapToWgs84(wgs84, ARHelper.SwitchHandedness(pos), mapToEcef);
+
+			if (r == 0)
+			{
+				localizerPose.valid = true;
+				localizerPose.mapToEcef = mapToEcef;
+				localizerPose.matrix = m;
+				localizerPose.lastUpdatedPose = new Pose(pos, rot);
+				localizerPose.vLatitude = wgs84[0];
+				localizerPose.vLongitude = wgs84[1];
+				localizerPose.vAltitude = wgs84[2];
+			}
+		}
+ 	}
+}

+ 11 - 0
Assets/ImmersalSDK/Core/Scripts/AR/LocalizerBase.cs.meta

@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 06ec00cf50a16443a92eafce8161c1a1
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 92 - 0
Assets/ImmersalSDK/Core/Scripts/AR/PoseFilter.cs

@@ -0,0 +1,92 @@
+/*===============================================================================
+Copyright (C) 2023 Immersal - Part of Hexagon. All Rights Reserved.
+
+This file is part of the Immersal SDK.
+
+The Immersal SDK cannot be copied, distributed, or made available to
+third-parties for commercial purposes without written permission of Immersal Ltd.
+
+Contact sales@immersal.com for licensing requests.
+===============================================================================*/
+
+using UnityEngine;
+
+namespace Immersal.AR
+{
+	public class PoseFilter
+	{
+		public Vector3 position = Vector3.zero;
+		public Quaternion rotation = Quaternion.identity;
+
+		private static uint m_HistorySize = 8;
+		private Vector3[] m_P = new Vector3[m_HistorySize];
+		private Vector3[] m_X = new Vector3[m_HistorySize];
+		private Vector3[] m_Z = new Vector3[m_HistorySize];
+		private uint m_Samples = 0;
+
+		public uint SampleCount()
+		{
+			return m_Samples;
+		}
+
+		public void InvalidateHistory()
+		{
+			m_Samples = 0;
+		}
+
+		public void ResetFiltering()
+		{
+			position = Vector3.zero;
+			rotation = Quaternion.identity;
+			InvalidateHistory();
+		}
+
+		public void RefinePose(Matrix4x4 r)
+		{
+			uint idx = m_Samples% m_HistorySize;
+			m_P[idx] = r.GetColumn(3);
+			m_X[idx] = r.GetColumn(0);
+			m_Z[idx] = r.GetColumn(2);
+			m_Samples++;
+			uint n = m_Samples > m_HistorySize ? m_HistorySize : m_Samples;
+			position = FilterAVT(m_P, n);
+			Vector3 x = Vector3.Normalize(FilterAVT(m_X, n));
+			Vector3 z = Vector3.Normalize(FilterAVT(m_Z, n));
+			Vector3 up = Vector3.Normalize(Vector3.Cross(z, x));
+			rotation = Quaternion.LookRotation(z, up);
+		}
+
+		private Vector3 FilterAVT(Vector3[] buf, uint n)
+		{
+			Vector3 mean = Vector3.zero;
+			for (uint i = 0; i < n; i++)
+				mean += buf[i];
+			mean /= (float)n;
+			if (n <= 2)
+				return mean;
+			float s = 0;
+			for (uint i = 0; i < n; i++)
+			{
+				s += Vector3.SqrMagnitude(buf[i] - mean);
+			}
+			s /= (float)n;
+			Vector3 avg = Vector3.zero;
+			int ib = 0;
+			for (uint i = 0; i < n; i++)
+			{
+				float d = Vector3.SqrMagnitude(buf[i] - mean);
+				if (d <= s)
+				{
+					avg += buf[i];
+					ib++;
+				}
+			}
+			if (ib > 0)
+			{
+				avg /= (float)ib;
+				return avg;
+			}
+			return mean;
+		}
+	}
+}

+ 11 - 0
Assets/ImmersalSDK/Core/Scripts/AR/PoseFilter.cs.meta

@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 3081ca6de5f736847b8e96b9683e38d7
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 379 - 0
Assets/ImmersalSDK/Core/Scripts/Core.cs

@@ -0,0 +1,379 @@
+/*===============================================================================
+Copyright (C) 2023 Immersal - Part of Hexagon. All Rights Reserved.
+
+This file is part of the Immersal SDK.
+
+The Immersal SDK cannot be copied, distributed, or made available to
+third-parties for commercial purposes without written permission of Immersal Ltd.
+
+Contact sales@immersal.com for licensing requests.
+===============================================================================*/
+
+using UnityEngine;
+using System;
+using System.Runtime.InteropServices;
+#if UNITY_EDITOR || UNITY_STANDALONE
+using System.Diagnostics;
+#endif
+
+namespace Immersal
+{
+    [StructLayout(LayoutKind.Sequential)]
+    public struct CaptureInfo
+    {
+        public int captureSize;
+        public int connected;
+    }
+
+    [StructLayout(LayoutKind.Sequential)]
+    public struct LocalizeInfo
+    {
+        public int handle;
+        public float px, py, pz;
+        public float r00, r01, r02, r10, r11, r12, r20, r21, r22;
+        public int confidence;
+    };
+
+    [UnmanagedFunctionPointer(CallingConvention.Cdecl)]
+    public delegate void LogCallback(IntPtr msg);
+
+    public static class Core
+    {
+        #region On-Device Mapping
+        public static int MapAddImage(IntPtr pixels, int width, int height, int channels, ref Vector4 intrinsics, ref Vector3 pos, float[] rot) => Native.icvMapAddImage(pixels, width, height, channels, ref intrinsics, ref pos, rot);
+
+        public static int MapImageGetCount() => Native.icvMapImageGetCount();
+
+        public static int MapPrepare(string path) => Native.icvMapPrepare(path);
+
+        public static int MapGet(byte[] map) => Native.icvMapGet(map);
+
+        public static int MapPointsGetCount() => Native.icvMapPointsGetCount();
+
+        public static int MapPointsGet(Vector3[] points)
+        {
+            GCHandle pointsHandle = GCHandle.Alloc(points, GCHandleType.Pinned);
+            int n = Native.icvMapPointsGet(pointsHandle.AddrOfPinnedObject(), points.Length);
+            pointsHandle.Free();
+            return n;
+        }
+
+        public static int MapResourcesFree() => Native.icvMapResourcesFree();
+
+        #endregion
+
+        /// <summary>
+        /// Get a Vector3 point cloud representation of the map data.
+        /// </summary>
+        /// <param name="mapHandle">An integer map handle</param>
+        /// <param name="points">A preallocated Vector3 array for the points</param>
+        /// <returns>Returns the number of points if succeeded, 0 otherwise.</returns>
+        public static int GetPointCloud(int mapHandle, Vector3[] points)
+        {
+            GCHandle vector3ArrayHandle = GCHandle.Alloc(points, GCHandleType.Pinned);
+            int n = Native.icvPointsGet(mapHandle, vector3ArrayHandle.AddrOfPinnedObject(), points.Length);
+            vector3ArrayHandle.Free();
+            return n;
+        }
+
+        /// <summary>
+        /// Get point count of the map's point cloud.
+        /// </summary>
+        /// <param name="mapHandle">An integer map handle</param>
+        /// <returns>Returns the number of points.</returns>
+        public static int GetPointCloudSize(int mapHandle) => Native.icvPointsGetCount(mapHandle);
+        
+        /// <summary>
+        /// Load map data from a .bytes file.
+        /// </summary>
+        /// <param name="buffer">Map data as a byte array</param>
+        /// <returns>An integer map handle.</returns>
+        public static int LoadMap(byte[] buffer) => Native.icvLoadMap(buffer);
+
+        /// <summary>
+        /// Free the map data from memory.
+        /// </summary>
+        /// <param name="mapHandle">An integer map handle</param>
+        /// <returns>Returns 1 if succeeded, 0 otherwise.</returns>
+        public static int FreeMap(int mapHandle) => Native.icvFreeMap(mapHandle);
+
+        /// <summary>
+        /// Capture image into the current map.
+        /// </summary>
+        /// <param name="capture">A preallocated byte array for the captured PNG image</param>
+        /// <param name="captureSizeMax">Int size of the array</param>
+        /// <param name="pixels">Raw pixel buffer data from the camera</param>
+        /// <param name="width">Image width</param>
+        /// <param name="height">Image height</param>
+        /// <param name="channels">1 or 3, monochromatic or RGB capture</param>
+        /// <returns>Int size of the captured PNG bytes</returns>
+        public static CaptureInfo CaptureImage(byte[] capture, int captureSizeMax, byte[] pixels, int width,
+            int height, int channels, int useMatching = 0)
+        {
+            GCHandle captureHandle = GCHandle.Alloc(capture, GCHandleType.Pinned);
+            GCHandle pixelsHandle = GCHandle.Alloc(pixels, GCHandleType.Pinned);
+            CaptureInfo info = Native.icvCaptureImage(captureHandle.AddrOfPinnedObject(), captureSizeMax,
+                pixelsHandle.AddrOfPinnedObject(), width, height, channels, useMatching);
+            captureHandle.Free();
+            pixelsHandle.Free();
+
+            return info;
+        }
+
+        /// <summary>
+        /// Gets the position and orientation of the image within the map.
+        /// </summary>
+        /// <param name="pos">Output Vector3 for the position</param>
+        /// <param name="rot">Output Quaternion for the orientation</param>
+        /// <param name="width">Image width</param>
+        /// <param name="height">Image height</param>
+        /// <param name="intrinsics">Camera intrinsics</param>
+        /// <param name="pixels">Raw pixel buffer data from the camera</param>
+        /// <returns>An integer map ID if succeeded, -1 otherwise</returns>
+        public static LocalizeInfo LocalizeImage(int n, int[] handles, int width,
+            int height, ref Vector4 intrinsics, IntPtr pixels)
+        {
+            GCHandle intHandle = GCHandle.Alloc(handles, GCHandleType.Pinned);
+            LocalizeInfo result = Native.icvLocalize(n, intHandle.AddrOfPinnedObject(), width, height,
+                ref intrinsics, pixels);
+            intHandle.Free();
+            return result;
+        }
+
+        /// <summary>
+        /// Gets the position and orientation of the image within the map.
+        /// </summary>
+        /// <param name="pos">Output Vector3 for the position</param>
+        /// <param name="rot">Output Quaternion for the orientation</param>
+        /// <param name="width">Image width</param>
+        /// <param name="height">Image height</param>
+        /// <param name="intrinsics">Camera intrinsics</param>
+        /// <param name="pixels">Raw pixel buffer data from the camera</param>
+        /// <returns>An integer map ID if succeeded, -1 otherwise</returns>
+        public static LocalizeInfo LocalizeImage(int width, int height,
+            ref Vector4 intrinsics, IntPtr pixels)
+        {
+            int n = 0;
+            int[] handles = new int[1];
+            return LocalizeImage(n, handles, width, height, ref intrinsics, pixels);
+        }
+
+        /// <summary>
+        /// 
+        /// </summary>
+        /// <param name="ecef"></param>
+        /// <param name="map"></param>
+        /// <param name="mapToEcef"></param>
+        /// <returns></returns>
+        public static int PosMapToEcef(double[] ecef, Vector3 map, double[] mapToEcef)
+        {
+            GCHandle ecefHandle = GCHandle.Alloc(ecef, GCHandleType.Pinned);
+            GCHandle mapToEcefHandle = GCHandle.Alloc(mapToEcef, GCHandleType.Pinned);
+            int r = Native.icvPosMapToEcef(ecefHandle.AddrOfPinnedObject(), ref map,
+                mapToEcefHandle.AddrOfPinnedObject());
+            mapToEcefHandle.Free();
+            ecefHandle.Free();
+            return r;
+        }
+
+        /// <summary>
+        /// 
+        /// </summary>
+        /// <param name="wgs84"></param>
+        /// <param name="ecef"></param>
+        /// <returns></returns>
+        public static int PosEcefToWgs84(double[] wgs84, double[] ecef)
+        {
+            GCHandle wgs84Handle = GCHandle.Alloc(wgs84, GCHandleType.Pinned);
+            GCHandle ecefHandle = GCHandle.Alloc(ecef, GCHandleType.Pinned);
+            int r = Native.icvPosEcefToWgs84(wgs84Handle.AddrOfPinnedObject(), ecefHandle.AddrOfPinnedObject());
+            ecefHandle.Free();
+            wgs84Handle.Free();
+            return r;
+        }
+
+        /// <summary>
+        /// 
+        /// </summary>
+        /// <param name="ecef"></param>
+        /// <param name="wgs84"></param>
+        /// <returns></returns>
+        public static int PosWgs84ToEcef(double[] ecef, double[] wgs84)
+        {
+            GCHandle ecefHandle = GCHandle.Alloc(ecef, GCHandleType.Pinned);
+            GCHandle wgs84Handle = GCHandle.Alloc(wgs84, GCHandleType.Pinned);
+            int r = Native.icvPosWgs84ToEcef(ecefHandle.AddrOfPinnedObject(), wgs84Handle.AddrOfPinnedObject());
+            wgs84Handle.Free();
+            ecefHandle.Free();
+            return r;
+        }
+
+        /// <summary>
+        /// 
+        /// </summary>
+        /// <param name="map"></param>
+        /// <param name="ecef"></param>
+        /// <param name="mapToEcef"></param>
+        /// <returns></returns>
+        public static int PosEcefToMap(out Vector3 map, double[] ecef, double[] mapToEcef)
+        {
+            GCHandle ecefHandle = GCHandle.Alloc(ecef, GCHandleType.Pinned);
+            GCHandle mapToEcefHandle = GCHandle.Alloc(mapToEcef, GCHandleType.Pinned);
+            int r = Native.icvPosEcefToMap(out map, ecefHandle.AddrOfPinnedObject(),
+                mapToEcefHandle.AddrOfPinnedObject());
+            mapToEcefHandle.Free();
+            ecefHandle.Free();
+            return r;
+        }
+
+        /// <summary>
+        /// 
+        /// </summary>
+        /// <param name="wgs84"></param>
+        /// <param name="map"></param>
+        /// <param name="mapToEcef"></param>
+        /// <returns></returns>
+        public static int PosMapToWgs84(double[] wgs84, Vector3 map, double[] mapToEcef)
+        {
+            double[] ecef = new double[3];
+            int err = PosMapToEcef(ecef, map, mapToEcef);
+            if (err != 0)
+                return err;
+            return PosEcefToWgs84(wgs84, ecef);
+        }
+
+        /// <summary>
+        /// 
+        /// </summary>
+        /// <param name="ecef"></param>
+        /// <param name="map"></param>
+        /// <param name="mapToEcef"></param>
+        /// <returns></returns>
+        public static int RotMapToEcef(out Quaternion ecef, Quaternion map, double[] mapToEcef)
+        {
+            GCHandle mapToEcefHandle = GCHandle.Alloc(mapToEcef, GCHandleType.Pinned);
+            int r = Native.icvRotMapToEcef(out ecef, ref map, mapToEcefHandle.AddrOfPinnedObject());
+            mapToEcefHandle.Free();
+            return r;
+        }
+
+        /// <summary>
+        /// 
+        /// </summary>
+        /// <param name="map"></param>
+        /// <param name="ecef"></param>
+        /// <param name="mapToEcef"></param>
+        /// <returns></returns>
+        public static int RotEcefToMap(out Quaternion map, Quaternion ecef, double[] mapToEcef)
+        {
+            GCHandle mapToEcefHandle = GCHandle.Alloc(mapToEcef, GCHandleType.Pinned);
+            int r = Native.icvRotEcefToMap(out map, ref ecef, mapToEcefHandle.AddrOfPinnedObject());
+            mapToEcefHandle.Free();
+            return r;
+        }
+
+        /// <summary>
+        /// Get internal plugin parameters.
+        /// </summary>
+        /// <param name="parameter">Parameter name</param>
+        /// <returns>Returns an integer value if set, -1 otherwise.</returns>
+        public static int GetInteger(string parameter) => Native.icvGetInteger(parameter);
+
+        /// <summary>
+        /// Set internal plugin parameters.
+        ///
+        /// Available parameters:
+        /// "LocalizationMaxPixels" - 0 is no limit (the default), 960*720 or higher.
+        /// "NumThreads" - how many CPU cores to use; -1 (system default) or a positive integer.
+        /// "ImageCompressionLevel" - 0 (no compression, fastest) to 9 (slowest). Defaults to 4.
+        /// </summary>
+        /// <param name="parameter">Parameter name</param>
+        /// <param name="value">An integer parameter value</param>
+        /// <returns>Returns 1 if succeeded, -1 otherwise.</returns>
+        public static int SetInteger(string parameter, int value) => Native.icvSetInteger(parameter, value);
+
+        public static int ValidateUser(string token) => Native.icvValidateUser(token);
+    }
+
+    public static class Native
+    {
+        private const string Assembly =
+#if UNITY_IOS && !UNITY_EDITOR
+		"__Internal";
+#else
+        "PosePlugin";
+#endif
+        [DllImport(Assembly, CallingConvention = CallingConvention.Cdecl)]
+        public static extern void PP_RegisterLogCallback(IntPtr callbackDelegate);
+
+        [DllImport(Assembly, CallingConvention = CallingConvention.Cdecl)]
+        public static extern int icvPointsGet(int mapHandle, IntPtr array, int maxCount);
+
+        [DllImport(Assembly, CallingConvention = CallingConvention.Cdecl)]
+        public static extern int icvPointsGetCount(int mapHandle);
+
+        [DllImport(Assembly, CallingConvention = CallingConvention.Cdecl)]
+        public static extern int icvLoadMap(byte[] buffer);
+
+        [DllImport(Assembly, CallingConvention = CallingConvention.Cdecl)]
+        public static extern int icvFreeMap(int mapHandle);
+
+        [DllImport(Assembly, CallingConvention = CallingConvention.Cdecl)]
+        public static extern CaptureInfo icvCaptureImage(IntPtr capture, int captureSizeMax, IntPtr pixels,
+            int width, int height, int channels, int useMatching);
+
+        [DllImport(Assembly, CallingConvention = CallingConvention.Cdecl)]
+        public static extern LocalizeInfo icvLocalize(int n, IntPtr handles, int width,
+            int height, ref Vector4 intrinsics, IntPtr pixels);
+
+        [DllImport(Assembly, CallingConvention = CallingConvention.Cdecl)]
+        public static extern int icvMapToEcefGet(IntPtr mapToEcef, int handle);
+
+        [DllImport(Assembly, CallingConvention = CallingConvention.Cdecl)]
+        public static extern int icvPosMapToEcef(IntPtr ecef, ref Vector3 map, IntPtr mapToEcef);
+
+        [DllImport(Assembly, CallingConvention = CallingConvention.Cdecl)]
+        public static extern int icvPosEcefToWgs84(IntPtr wgs84, IntPtr ecef);
+
+        [DllImport(Assembly, CallingConvention = CallingConvention.Cdecl)]
+        public static extern int icvPosWgs84ToEcef(IntPtr ecef, IntPtr wgs84);
+
+        [DllImport(Assembly, CallingConvention = CallingConvention.Cdecl)]
+        public static extern int icvPosEcefToMap(out Vector3 map, IntPtr ecef, IntPtr mapToEcef);
+
+        [DllImport(Assembly, CallingConvention = CallingConvention.Cdecl)]
+        public static extern int icvRotMapToEcef(out Quaternion ecef, ref Quaternion map, IntPtr mapToEcef);
+
+        [DllImport(Assembly, CallingConvention = CallingConvention.Cdecl)]
+        public static extern int icvRotEcefToMap(out Quaternion map, ref Quaternion ecef, IntPtr mapToEcef);
+
+        [DllImport(Assembly, CallingConvention = CallingConvention.Cdecl)]
+        public static extern int icvSetInteger([MarshalAs(UnmanagedType.LPStr)] string parameter, int value);
+
+        [DllImport(Assembly, CallingConvention = CallingConvention.Cdecl)]
+        public static extern int icvGetInteger([MarshalAs(UnmanagedType.LPStr)] string parameter);
+
+        [DllImport(Assembly, CallingConvention = CallingConvention.Cdecl)]
+        public static extern int icvValidateUser([MarshalAs(UnmanagedType.LPStr)] string token);
+        [DllImport(Assembly, CallingConvention = CallingConvention.Cdecl)]
+        public static extern int icvMapAddImage(IntPtr pixels, int width, int height, int channels, ref Vector4 intrinsics, ref Vector3 pos, float[] rot);
+
+        [DllImport(Assembly, CallingConvention = CallingConvention.Cdecl)]
+        public static extern int icvMapImageGetCount();
+
+        [DllImport(Assembly, CallingConvention = CallingConvention.Cdecl)]
+        public static extern int icvMapPrepare([MarshalAs(UnmanagedType.LPStr)] string path);
+
+        [DllImport(Assembly, CallingConvention = CallingConvention.Cdecl)]
+        public static extern int icvMapGet(byte[] map);
+
+        [DllImport(Assembly, CallingConvention = CallingConvention.Cdecl)]
+        public static extern int icvMapPointsGetCount();
+
+        [DllImport(Assembly, CallingConvention = CallingConvention.Cdecl)]
+        public static extern int icvMapPointsGet(IntPtr points, int countMax);
+
+        [DllImport(Assembly, CallingConvention = CallingConvention.Cdecl)]
+        public static extern int icvMapResourcesFree();
+    }
+}

+ 12 - 0
Assets/ImmersalSDK/Core/Scripts/Core.cs.meta

@@ -0,0 +1,12 @@
+fileFormatVersion: 2
+guid: 2a0cde8ffdea2d24bb36590dbe44cb53
+timeCreated: 1464961012
+licenseType: Free
+MonoImporter:
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 8 - 0
Assets/ImmersalSDK/Core/Scripts/Editor.meta

@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 068ff142a326946a4921c7907dd3368c
+folderAsset: yes
+DefaultImporter:
+  externalObjects: {}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 280 - 0
Assets/ImmersalSDK/Core/Scripts/Editor/ARMapDownloader.cs

@@ -0,0 +1,280 @@
+/*===============================================================================
+Copyright (C) 2023 Immersal - Part of Hexagon. All Rights Reserved.
+
+This file is part of the Immersal SDK.
+
+The Immersal SDK cannot be copied, distributed, or made available to
+third-parties for commercial purposes without written permission of Immersal Ltd.
+
+Contact sales@immersal.com for licensing requests.
+===============================================================================*/
+
+#if UNITY_EDITOR
+using UnityEngine;
+using UnityEngine.Networking;
+using UnityEditor;
+using Unity.EditorCoroutines.Editor;
+using System;
+using System.IO;
+using System.Linq;
+using System.Collections;
+using System.Collections.Generic;
+using Immersal.REST;
+using Immersal.AR;
+
+namespace Immersal
+{
+    public class ARMapDownloader : EditorWindow
+    {
+        private static string m_MapIdsCSV = "";
+        private static string m_Token = "";
+        private static string m_MapDataPath;
+        private ARSpace m_ARSpace = null;
+        private bool m_loadAlignment = false;
+
+        [MenuItem("Immersal SDK/AR Map Downloader")]
+        public static void ShowWindow()
+        {
+            EditorWindow.GetWindow<ARMapDownloader>("AR Map Downloader");
+        }
+
+        private void OnGUI()
+        {
+            if (BuildPipeline.isBuildingPlayer)
+                return;
+            
+            m_Token = ImmersalSDK.Instance.developerToken;
+
+            m_MapIdsCSV = EditorGUILayout.TextField(new GUIContent("Map ids CSV", "Comma-separated values of map ids to download"), m_MapIdsCSV);
+            m_ARSpace = (ARSpace)EditorGUILayout.ObjectField(new GUIContent("AR Space", "AR Space Game Object from the scene under which the maps are created"), m_ARSpace, typeof(ARSpace), true);
+            if (m_Token.Length == 0)
+            {
+                EditorGUILayout.HelpBox("No Developer token specified, please log in in the Immersal SDK Settings or manually input in the ImmersalSDK game object", MessageType.Warning, true);
+            }
+            if (m_ARSpace == null)
+            {
+                EditorGUILayout.HelpBox("No AR Space selected, a new one will be created", MessageType.Info, true);
+            }
+
+            if (GUILayout.Button("Download And Setup Maps"))
+            {
+                DownloadMaps();
+            }
+
+            m_loadAlignment = EditorGUILayout.Toggle(new GUIContent("Load Alignment (Experimental)", "Try to load alignment from map metadata. Coordinate system is unknown (ECEF or Unity's)"), m_loadAlignment);
+        }
+
+        private void DownloadMaps()
+        {
+            m_MapDataPath = Path.Combine("Assets", "Map Data");
+            DirectoryInfo di = Directory.CreateDirectory(m_MapDataPath);
+            AssetDatabase.Refresh();
+
+            List<int> mapIds = GetMapIds(m_MapIdsCSV);
+
+            if (AssetDatabase.IsValidFolder(m_MapDataPath))
+            {
+                if (mapIds.Count > 0)
+                {
+                    foreach (int id in mapIds)
+                    {
+                        EditorCoroutineUtility.StartCoroutine(DownloadMapMetadata(id), this);
+                    }
+                }
+            }
+            else
+            {
+                Debug.LogError("Map Data Folder not valid, aborting");
+            }
+        }
+
+        private IEnumerator DownloadMapMetadata(int id)
+        {
+            //
+            // Downloads map metadata, saves it to disk, downloads the map file and sets up the AR Map game object
+            //
+
+            // Load map metadata from Immersal Cloud Service
+            SDKMapMetadataGetRequest r = new SDKMapMetadataGetRequest();
+            r.token = m_Token;
+            r.id = id;
+
+            string jsonString = JsonUtility.ToJson(r);
+            UnityWebRequest request = UnityWebRequest.Put(string.Format(ImmersalHttp.URL_FORMAT, ImmersalSDK.Instance.localizationServer, SDKMapMetadataGetRequest.endpoint), jsonString);
+            request.method = UnityWebRequest.kHttpVerbPOST;
+            request.useHttpContinue = false;
+            request.SetRequestHeader("Content-Type", "application/json");
+            request.SetRequestHeader("Accept", "application/json");
+            request.SendWebRequest();
+
+            while (!request.isDone)
+            {
+                yield return null;
+            }
+
+#if UNITY_2020_1_OR_NEWER
+            if (request.result != UnityWebRequest.Result.Success)
+#else
+            if (request.isNetworkError || request.isHttpError)
+#endif
+            {
+                Debug.LogError(request.error);
+            }
+            else
+            {
+                SDKMapMetadataGetResult result = JsonUtility.FromJson<SDKMapMetadataGetResult>(request.downloadHandler.text);
+                if (result.error == "none")
+                {
+                    // Save metadata file on disk, overwrite existing file
+                    string jsonFilePath = Path.Combine(m_MapDataPath, string.Format("{0}-{1}-metadata.json", result.id, result.name));
+                    WriteJson(jsonFilePath, request.downloadHandler.text);
+
+                    // Load map file from Immersal Cloud Service
+                    EditorCoroutineUtility.StartCoroutine(DownloadMapFile(id, result), this);
+                }
+            }
+        }
+
+        private IEnumerator DownloadMapFile(int id, SDKMapMetadataGetResult result)
+        {
+            //
+            // Load the map file, write it to disk and set up the AR Map game object
+            //
+
+            // Load map file from Immersal Cloud Service
+            SDKMapDownloadRequest r = new SDKMapDownloadRequest();
+            r.token = m_Token;
+            r.id = id;
+
+            string jsonString = JsonUtility.ToJson(r);
+            UnityWebRequest request = UnityWebRequest.Put(string.Format(ImmersalHttp.URL_FORMAT, ImmersalSDK.Instance.localizationServer, SDKMapDownloadRequest.endpoint), jsonString);
+            request.method = UnityWebRequest.kHttpVerbPOST;
+            request.useHttpContinue = false;
+            request.SetRequestHeader("Content-Type", "application/json");
+            request.SetRequestHeader("Accept", "application/json");
+            request.SendWebRequest();
+
+            while (!request.isDone)
+            {
+                yield return null;
+            }
+
+#if UNITY_2020_1_OR_NEWER
+            if (request.result != UnityWebRequest.Result.Success)
+#else
+            if (request.isNetworkError || request.isHttpError)
+#endif
+            {
+                Debug.LogError(request.error);
+            }
+            else
+            {
+                SDKMapDownloadResult mapDataResult = JsonUtility.FromJson<SDKMapDownloadResult>(request.downloadHandler.text);
+                if (mapDataResult.error == "none")
+                {
+                    // Save map file on disk, overwrite existing file
+                    string mapFilepath = Path.Combine(m_MapDataPath, string.Format("{0}-{1}.bytes", result.id, result.name));
+                    WriteBytes(mapFilepath, mapDataResult.b64);
+
+                    // Set up AR Map game object
+                    SetupARMapInScene(mapFilepath, result);
+                }
+            }
+        }
+
+        private List<int> GetMapIds(string mapIds)
+        {
+            List<int> ids = mapIds.Split(',').Select(int.Parse).ToList();
+            return ids;
+        }
+
+        private void WriteJson(string jsonFilepath, string data)
+        {
+            File.WriteAllText(jsonFilepath, data);
+            AssetDatabase.ImportAsset(jsonFilepath);
+        }
+
+        private void WriteBytes(string mapFilepath, string b64)
+        {
+            if (!File.Exists(mapFilepath))
+            {
+                byte[] data = Convert.FromBase64String(b64);
+                File.WriteAllBytes(mapFilepath, data);
+                AssetDatabase.ImportAsset(mapFilepath);
+            }
+        }
+
+        private void SetupARMapInScene(string mapFilepath, SDKMapMetadataGetResult result)
+        {
+            if(m_ARSpace == null)
+            {
+                GameObject arSpace = new GameObject("AR Space");
+                m_ARSpace = arSpace.AddComponent<ARSpace>();
+            }
+
+            string arMapName = string.Format("AR Map {0}-{1}", result.id, result.name);
+            ARMap[] arMapsInScene = FindObjectsOfType<ARMap>();
+            bool arMapExists = false;
+            ARMap arMap = null;
+            for(int i=0; i<arMapsInScene.Length; i++)
+            {
+                if(arMapsInScene[i].name == arMapName)
+                {
+                    arMapExists = true;
+                    arMap = arMapsInScene[i];
+                    break;
+                }
+            }
+
+            if (!arMapExists)
+            {
+                GameObject go = new GameObject(string.Format("AR Map {0}-{1}", result.id, result.name));
+                go.transform.parent = m_ARSpace.transform;
+
+                Color pointCloudColor = ARMap.pointCloudColors[UnityEngine.Random.Range(0, ARMap.pointCloudColors.Length)];
+
+                arMap = go.AddComponent<ARMap>();
+                arMap.pointColor = pointCloudColor;
+
+                TextAsset mapFile = (TextAsset)AssetDatabase.LoadAssetAtPath(mapFilepath, typeof(TextAsset));
+                arMap.mapFile = mapFile;
+            }
+
+            arMap.mapAlignment.tx = result.tx;
+            arMap.mapAlignment.ty = result.ty;
+            arMap.mapAlignment.tz = result.tz;
+            arMap.mapAlignment.qx = result.qx;
+            arMap.mapAlignment.qy = result.qy;
+            arMap.mapAlignment.qz = result.qz;
+            arMap.mapAlignment.qw = result.qw;
+            arMap.mapAlignment.scale = result.scale;
+
+            arMap.wgs84.latitude = result.latitude;
+            arMap.wgs84.longitude = result.longitude;
+            arMap.wgs84.altitude = result.altitude;
+
+            arMap.privacy = result.privacy;
+
+            if (m_loadAlignment)
+            {
+                Vector3 posMetadata = new Vector3((float)result.tx, (float)result.ty, (float)result.tz);
+                Quaternion rotMetadata = new Quaternion((float)result.qx, (float)result.qy, (float)result.qz, (float)result.qw);
+                float scaleMetadata = (float)result.scale; // Only uniform scale metadata is supported
+
+                // IMPORTANT
+                // Switch coordinate system handedness back from Immersal Cloud Service's default right-handed system to Unity's left-handed system
+                Matrix4x4 b = Matrix4x4.TRS(posMetadata, rotMetadata, new Vector3(scaleMetadata, scaleMetadata, scaleMetadata));
+                Matrix4x4 a = ARHelper.SwitchHandedness(b);
+                Vector3 pos = a.GetColumn(3);
+                Quaternion rot = a.rotation;
+                Vector3 scl = new Vector3(scaleMetadata, scaleMetadata, scaleMetadata); // Only uniform scale metadata is supported
+
+                // Set AR Map local transform from the converted metadata
+                arMap.transform.localPosition = pos;
+                arMap.transform.localRotation = rot;
+                arMap.transform.localScale = scl;
+            }
+        }
+    }
+}
+#endif

+ 11 - 0
Assets/ImmersalSDK/Core/Scripts/Editor/ARMapDownloader.cs.meta

@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 955d276a9ba9e7d4380b01dffcc369ea
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 103 - 0
Assets/ImmersalSDK/Core/Scripts/Editor/ImmersalSDKEditor.cs

@@ -0,0 +1,103 @@
+/*===============================================================================
+Copyright (C) 2023 Immersal - Part of Hexagon. All Rights Reserved.
+
+This file is part of the Immersal SDK.
+
+The Immersal SDK cannot be copied, distributed, or made available to
+third-parties for commercial purposes without written permission of Immersal Ltd.
+
+Contact sales@immersal.com for licensing requests.
+===============================================================================*/
+
+#if UNITY_EDITOR
+using System.Collections;
+using Unity.EditorCoroutines.Editor;
+using UnityEngine;
+using UnityEditor;
+using UnityEditor.SceneManagement;
+using UnityEngine.Networking;
+using Immersal.REST;
+
+namespace Immersal
+{
+    public class ImmersalSDKEditor : EditorWindow
+    {
+        private string myEmail = "";
+        private string myPassword = "";
+        private string myToken = "";
+        private ImmersalSDK sdk = null;
+
+        private static UnityWebRequest request;
+
+        [MenuItem("Immersal SDK/Open Settings")]
+        public static void ShowWindow()
+        {
+            EditorWindow.GetWindow<ImmersalSDKEditor>("Immersal SDK Settings");
+        }
+
+        void OnGUI()
+        {
+            GUILayout.Label("Credentials", EditorStyles.boldLabel);
+            myEmail = EditorGUILayout.TextField("Email", myEmail);
+            myPassword = EditorGUILayout.PasswordField("Password", myPassword);
+
+            if (GUILayout.Button("Login"))
+            {
+                SDKLoginRequest loginRequest = new SDKLoginRequest();
+                loginRequest.login = myEmail;
+                loginRequest.password = myPassword;
+
+                EditorCoroutineUtility.StartCoroutine(Login(loginRequest), this);
+            }
+
+            EditorGUILayout.Separator();
+
+            myToken = EditorGUILayout.TextField("Token", myToken);
+
+            EditorGUILayout.Separator();
+            EditorGUILayout.Separator();
+            EditorGUILayout.Separator();
+
+            EditorGUILayout.LabelField("(C) 2023 Immersal - Part of Hexagon. All Right Reserved.");
+        }
+
+        void OnInspectorUpdate()
+        {
+            Repaint();
+        }
+
+        IEnumerator Login(SDKLoginRequest loginRequest)
+        {
+            string jsonString = JsonUtility.ToJson(loginRequest);
+            sdk = ImmersalSDK.Instance;
+            using (UnityWebRequest request = UnityWebRequest.Put(string.Format(ImmersalHttp.URL_FORMAT, sdk.localizationServer, SDKLoginRequest.endpoint), jsonString))
+            {
+                request.method = UnityWebRequest.kHttpVerbPOST;
+                request.useHttpContinue = false;
+                request.SetRequestHeader("Content-Type", "application/json");
+                request.SetRequestHeader("Accept", "application/json");
+                yield return request.SendWebRequest();
+
+#if UNITY_2020_1_OR_NEWER
+                if (request.result != UnityWebRequest.Result.Success)
+#else
+                if (request.isNetworkError || request.isHttpError)
+#endif
+                {
+                    Debug.LogError(request.error);
+                }
+                else
+                {
+                    SDKLoginResult loginResult = JsonUtility.FromJson<SDKLoginResult>(request.downloadHandler.text);
+                    if (loginResult.error == "none")
+                    {
+                        myToken = loginResult.token;
+                        sdk.developerToken = myToken;
+                        EditorSceneManager.MarkSceneDirty(EditorSceneManager.GetActiveScene());
+                    }
+                }
+            }
+        }
+    }
+}
+#endif

+ 11 - 0
Assets/ImmersalSDK/Core/Scripts/Editor/ImmersalSDKEditor.cs.meta

@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 2d48ee0fc8ac04698a34d148045338c9
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 32 - 0
Assets/ImmersalSDK/Core/Scripts/Editor/ImmersalSDKInfo.cs

@@ -0,0 +1,32 @@
+/*===============================================================================
+Copyright (C) 2023 Immersal - Part of Hexagon. All Rights Reserved.
+
+This file is part of the Immersal SDK.
+
+The Immersal SDK cannot be copied, distributed, or made available to
+third-parties for commercial purposes without written permission of Immersal Ltd.
+
+Contact sales@immersal.com for licensing requests.
+===============================================================================*/
+
+#if UNITY_EDITOR
+using UnityEditor;
+
+namespace Immersal
+{
+    [CustomEditor(typeof(ImmersalSDK))]
+    public class ImmersalSDKInfo : Editor
+    {
+        private ImmersalSDK sdk
+        {
+            get { return target as ImmersalSDK; }
+        }
+
+        public override void OnInspectorGUI()
+        {
+            EditorGUILayout.HelpBox("Immersal SDK v" + ImmersalSDK.sdkVersion, MessageType.Info);
+            base.OnInspectorGUI();
+        }
+    }
+}
+#endif

+ 11 - 0
Assets/ImmersalSDK/Core/Scripts/Editor/ImmersalSDKInfo.cs.meta

@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: a196b3942c8a6ee4d9b0490d6896cc92
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 34 - 0
Assets/ImmersalSDK/Core/Scripts/Editor/ReadOnlyDrawer.cs

@@ -0,0 +1,34 @@
+/*===============================================================================
+Copyright (C) 2023 Immersal - Part of Hexagon. All Rights Reserved.
+
+This file is part of the Immersal SDK.
+
+The Immersal SDK cannot be copied, distributed, or made available to
+third-parties for commercial purposes without written permission of Immersal Ltd.
+
+Contact sales@immersal.com for licensing requests.
+===============================================================================*/
+
+#if UNITY_EDITOR
+using UnityEngine;
+using UnityEditor;
+
+[CustomPropertyDrawer(typeof(ReadOnlyAttribute))]
+public class ReadOnlyDrawer : PropertyDrawer
+{
+    public override float GetPropertyHeight(SerializedProperty property,
+                                            GUIContent label)
+    {
+        return EditorGUI.GetPropertyHeight(property, label, true);
+    }
+
+    public override void OnGUI(Rect position,
+                               SerializedProperty property,
+                               GUIContent label)
+    {
+        GUI.enabled = false;
+        EditorGUI.PropertyField(position, property, label, true);
+        GUI.enabled = true;
+    }
+}
+#endif

+ 11 - 0
Assets/ImmersalSDK/Core/Scripts/Editor/ReadOnlyDrawer.cs.meta

@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 6a04c0995666dab4988bf70980c9d391
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 401 - 0
Assets/ImmersalSDK/Core/Scripts/ImmersalSDK.cs

@@ -0,0 +1,401 @@
+/*===============================================================================
+Copyright (C) 2023 Immersal - Part of Hexagon. All Rights Reserved.
+
+This file is part of the Immersal SDK.
+
+The Immersal SDK cannot be copied, distributed, or made available to
+third-parties for commercial purposes without written permission of Immersal Ltd.
+
+Contact sales@immersal.com for licensing requests.
+===============================================================================*/
+
+using UnityEngine;
+using UnityEngine.Events;
+using Unity.Collections;
+using UnityEngine.XR.ARFoundation;
+using System;
+using System.Net.Http;
+using System.Runtime.InteropServices;
+using Immersal.AR;
+using Immersal.REST;
+using UnityEngine.XR.ARSubsystems;
+using AOT;
+
+namespace Immersal
+{
+	public class ImmersalSDK : MonoBehaviour
+	{
+		public static string sdkVersion = "1.19.2";
+		public static bool isHWAR = false;
+		private static readonly string[] ServerList = new[] {"https://api.immersal.com", "https://immersal.hexagon.com.cn"};
+
+        public enum CameraResolution { Default, HD, FullHD, Max };	// With Huawei AR Engine SDK, only Default (640x480) and Max (1440x1080) are supported.
+        public enum APIServer { DefaultServer, ChinaServer };
+
+		private static ImmersalSDK instance = null;
+
+		[SerializeField]
+		public APIServer defaultServer = APIServer.DefaultServer;
+		[Tooltip("SDK developer token")]
+		public string developerToken;
+		[SerializeField]
+		[Tooltip("Application target frame rate")]
+		private int m_TargetFrameRate = 60;
+		[SerializeField]
+		[Tooltip("Android resolution")]
+		private CameraResolution m_AndroidResolution = CameraResolution.FullHD;
+		[SerializeField]
+		[Tooltip("iOS resolution")]
+		private CameraResolution m_iOSResolution = CameraResolution.Default;
+		[Tooltip("Downsample image to HD resolution")]
+		[SerializeField]
+		private bool m_Downsample = true;
+
+		public UnityEvent onPoseLost = null;
+		public UnityEvent onPoseFound = null;
+
+        public int secondsToDecayPose = 10;
+
+		public LocalizerBase Localizer { get; private set; }
+		public int TrackingQuality { get; private set; }
+
+		private ARCameraManager m_CameraManager;
+		private ARSession m_ARSession;
+        private bool m_bCamConfigDone = false;
+		private string m_LocalizationServer;
+		private int m_PreviousResults = 0;
+		private int m_CurrentResults = 0;
+		private int q = 0;
+		private float m_LatestPoseUpdated = 0f;
+		private bool m_HasPose = false;
+		private XRCameraConfiguration? m_InitialConfig;
+
+        public static HttpClient client;
+
+		public int targetFrameRate
+		{
+			get { return m_TargetFrameRate; }
+			set
+			{
+				m_TargetFrameRate = value;
+				SetFrameRate();
+			}
+		}
+
+		public string defaultServerURL
+		{
+			get {
+				return ServerList[(int)defaultServer];
+			}
+		}
+
+		public CameraResolution androidResolution
+		{
+			get { return m_AndroidResolution; }
+			set
+			{
+				m_AndroidResolution = value;
+				ConfigureCamera();
+			}
+		}
+
+		public CameraResolution iOSResolution
+		{
+			get { return m_iOSResolution; }
+			set
+			{
+				m_iOSResolution = value;
+				ConfigureCamera();
+			}
+		}
+
+        public bool downsample
+		{
+			get { return m_Downsample; }
+			set
+			{
+				m_Downsample = value;
+				SetDownsample();
+			}
+		}
+
+		public string localizationServer
+		{
+			get {
+				if (m_LocalizationServer != null)
+				{
+					return m_LocalizationServer;
+				}
+				return defaultServerURL;
+			}
+			set
+			{
+				m_LocalizationServer = value;
+			}
+		}
+
+		public ARCameraManager cameraManager
+		{
+			get
+			{
+				if (m_CameraManager == null)
+				{
+					m_CameraManager = UnityEngine.Object.FindObjectOfType<ARCameraManager>();
+				}
+				return m_CameraManager;
+			}
+		}
+
+		public ARSession arSession
+		{
+			get
+			{
+				if (m_ARSession == null)
+				{
+					m_ARSession = UnityEngine.Object.FindObjectOfType<ARSession>();
+				}
+				return m_ARSession;
+			}
+		}
+
+		public static ImmersalSDK Instance
+		{
+			get
+			{
+#if UNITY_EDITOR
+				if (instance == null && !Application.isPlaying)
+				{
+					instance = UnityEngine.Object.FindObjectOfType<ImmersalSDK>();
+				}
+#endif
+				if (instance == null)
+				{
+					Debug.LogError("No ImmersalSDK instance found. Ensure one exists in the scene.");
+				}
+				return instance;
+			}
+		}
+
+		async void Awake()
+		{
+			if (instance == null)
+			{
+				instance = this;
+			}
+			if (instance != this)
+			{
+				Debug.LogError("There must be only one ImmersalSDK object in a scene.");
+				UnityEngine.Object.DestroyImmediate(this);
+				return;
+			}
+
+			LogCallback callback_delegate = new LogCallback(Log);
+			IntPtr intptr_delegate = Marshal.GetFunctionPointerForDelegate(callback_delegate);
+			Native.PP_RegisterLogCallback(intptr_delegate);
+
+			HttpClientHandler handler = new HttpClientHandler();
+			handler.ClientCertificateOptions = ClientCertificateOption.Automatic;
+			client = new HttpClient(handler);
+			client.DefaultRequestHeaders.ExpectContinue = false;
+			
+			if (developerToken != null && developerToken.Length > 0)
+			{
+				PlayerPrefs.SetString("token", developerToken);
+
+				if (Application.platform == RuntimePlatform.IPhonePlayer || Application.platform == RuntimePlatform.Android)
+				{
+					int r = Core.ValidateUser(developerToken);
+					Debug.LogFormat("{0} License", r >= 1 ? "Enterprise" : "Free");
+				}
+				else
+				{
+					JobStatusAsync j = new JobStatusAsync();
+					j.OnResult += (SDKStatusResult result) =>
+					{
+						Debug.LogFormat("{0} License", result.level >= 1 ? "Enterprise" : "Free");
+					};
+					await j.RunJobAsync();
+				}
+			}
+		}
+
+		void Start()
+		{
+			SetFrameRate();
+#if !UNITY_EDITOR
+			SetDownsample();
+#endif
+			
+			onPoseLost?.Invoke();
+		}
+
+        [MonoPInvokeCallback(typeof(LogCallback))]
+        public static void Log(IntPtr ansiString)
+		{
+			string msg = Marshal.PtrToStringAnsi(ansiString);
+			Debug.LogFormat("Plugin: {0}", msg);
+        }
+
+		private void SetFrameRate()
+		{
+			Application.targetFrameRate = targetFrameRate;
+		}
+
+		private void SetDownsample()
+		{
+			if (downsample)
+			{
+				Core.SetInteger("LocalizationMaxPixels", 960*720);
+			}
+			else
+			{
+				Core.SetInteger("LocalizationMaxPixels", 0);
+			}
+		}
+
+		private void Update()
+		{
+			if (Localizer != null)
+			{
+				LocalizerStats stats = Localizer.stats;
+				if (stats.localizationAttemptCount > 0)
+				{
+					q = CurrentResults(stats.localizationSuccessCount);
+					
+					if (!m_HasPose && q > 1)
+					{
+						m_HasPose = true;
+						onPoseFound?.Invoke();
+					}
+
+					if (m_HasPose && (q < 1 || !Localizer.isTracking))
+					{
+						m_HasPose = false;
+						Localizer.Reset();
+						m_PreviousResults = 0;
+						m_CurrentResults = 0;
+						onPoseLost?.Invoke();
+					}
+
+					TrackingQuality = q;
+				}
+			}
+			
+			if (!isHWAR)
+			{
+				if (!m_bCamConfigDone && cameraManager != null)
+					ConfigureCamera();
+			}
+		}
+
+		private void ConfigureCamera()
+		{
+#if !UNITY_EDITOR && (UNITY_ANDROID || UNITY_IOS)
+			var cameraSubsystem = cameraManager.subsystem;
+			if (cameraSubsystem == null || !cameraSubsystem.running)
+				return;
+			var configurations = cameraSubsystem.GetConfigurations(Allocator.Temp);
+			if (!configurations.IsCreated || (configurations.Length <= 0))
+				return;
+			int bestError = int.MaxValue;
+			var currentConfig = cameraSubsystem.currentConfiguration;
+			int dw = (int)currentConfig?.width;
+			int dh = (int)currentConfig?.height;
+			if (dw == 0 && dh == 0)
+				return;
+#if UNITY_ANDROID
+			CameraResolution reso = androidResolution;
+#else
+			CameraResolution reso = iOSResolution;
+#endif
+
+			if (!m_bCamConfigDone)
+			{
+				m_InitialConfig = currentConfig;
+			}
+
+			switch (reso)
+			{
+				case CameraResolution.Default:
+					dw = (int)currentConfig?.width;
+					dh = (int)currentConfig?.height;
+					break;
+				case CameraResolution.HD:
+					dw = 1280;
+					dh = 720;
+					break;
+				case CameraResolution.FullHD:
+					dw = 1920;
+					dh = 1080;
+					break;
+				case CameraResolution.Max:
+					dw = 80000;
+					dh = 80000;
+					break;
+			}
+
+			foreach (var config in configurations)
+			{
+				int perror = config.width * config.height - dw * dh;
+				if (Math.Abs(perror) < bestError)
+				{
+					bestError = Math.Abs(perror);
+					currentConfig = config;
+				}
+			}
+
+			if (reso != CameraResolution.Default) {
+				Debug.LogFormat("resolution = {0}x{1}", (int)currentConfig?.width, (int)currentConfig?.height);
+				cameraSubsystem.currentConfiguration = currentConfig;
+			}
+			else
+			{
+				cameraSubsystem.currentConfiguration = m_InitialConfig;
+			}
+#endif
+			m_bCamConfigDone = true;
+		}
+
+		int CurrentResults(int localizationResults) {
+			int diffResults = localizationResults - m_PreviousResults;
+			m_PreviousResults = localizationResults;
+			if (diffResults > 0)
+			{
+				m_LatestPoseUpdated = Time.time;
+				m_CurrentResults += diffResults;
+				if (m_CurrentResults > 3)
+				{
+					m_CurrentResults = 3;
+				}
+			}
+			else if (Time.time - m_LatestPoseUpdated > secondsToDecayPose)
+			{
+				m_LatestPoseUpdated = Time.time;
+				if (m_CurrentResults > 0)
+				{
+					m_CurrentResults--;
+				}
+			}
+				
+			return m_CurrentResults;
+		}
+
+		public void RegisterLocalizer(LocalizerBase localizer)
+		{
+			Localizer = localizer;
+			Localizer.OnReset += OnLocalizerReset;
+		}
+		
+		public void UnRegisterLocalizer()
+		{
+			Localizer.OnReset -= OnLocalizerReset;
+			Localizer = null;
+		}
+
+		private void OnLocalizerReset()
+		{
+			m_CurrentResults = m_PreviousResults = 0;
+			m_HasPose = false;
+		}
+	}
+}

+ 11 - 0
Assets/ImmersalSDK/Core/Scripts/ImmersalSDK.cs.meta

@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: bc609bf82f8e346d593bc1a41c2b7cb8
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 475 - 0
Assets/ImmersalSDK/Core/Scripts/REST.cs

@@ -0,0 +1,475 @@
+/*===============================================================================
+Copyright (C) 2023 Immersal - Part of Hexagon. All Rights Reserved.
+
+This file is part of the Immersal SDK.
+
+The Immersal SDK cannot be copied, distributed, or made available to
+third-parties for commercial purposes without written permission of Immersal Ltd.
+
+Contact sales@immersal.com for licensing requests.
+===============================================================================*/
+
+using System;
+using UnityEngine;
+
+namespace Immersal.REST
+{
+    public struct SDKJobState
+    {
+        public const string Done = "done";
+        public const string Sparse = "sparse";
+        public const string Processing = "processing";
+        public const string Failed = "failed";
+        public const string Pending = "pending";
+    }
+
+    public enum SDKJobType { Map, Stitch, Alignment, Edit };
+    public enum SDKJobPrivacy { Private, Public };
+
+    [Serializable]
+    public struct SDKJob
+    {
+        public int id;
+        public int type;
+        public string version;
+        public int creator;
+        public int size;
+        public string status;
+        public int privacy;
+        public string name;
+        public double latitude;
+        public double longitude;
+        public double altitude;
+        public string created;
+        public string modified;
+        public string sha256_al;
+        public string sha256_sparse;
+        public string sha256_dense;
+        public string sha256_tex;
+    }
+
+    [Serializable]
+    public struct SDKMapId
+    {
+        public int id;
+    }
+
+    [Serializable]
+    public struct SDKLoginRequest
+    {
+        public static string endpoint = "login";
+        public string login;
+        public string password;
+    }
+
+    [Serializable]
+    public struct SDKLoginResult
+    {
+        public string error;
+        public int userId;
+        public string token;
+        public int level;
+    }
+
+    [Serializable]
+    public struct SDKClearRequest
+    {
+        public static string endpoint = "clear";
+        public string token;
+        public bool anchor;
+    }
+
+    [Serializable]
+    public struct SDKClearResult
+    {
+        public string error;
+    }
+
+    [Serializable]
+    public struct SDKConstructRequest
+    {
+        public static string endpoint = "construct";
+        public string token;
+        public string name;
+        public int featureCount;
+        public int featureType;
+        public bool preservePoses;
+        public int windowSize;
+    }
+
+    [Serializable]
+    public struct SDKConstructResult
+    {
+        public string error;
+        public int id;
+        public int size;
+    }
+
+    [Serializable]
+    public struct SDKStatusRequest
+    {
+        public static string endpoint = "status";
+        public string token;
+    }
+
+    [Serializable]
+    public struct SDKStatusResult
+    {
+        public string error;
+        public int userId;
+        public int imageCount;
+        public int imageMax;
+        public bool eulaAccepted;
+        public int level;
+    }
+
+    [Serializable]
+    public struct SDKJobsRequest
+    {
+        public static string endpoint = "list";
+        public string token;
+    }
+
+    [Serializable]
+    public struct SDKGeoJobsRequest
+    {
+        public static string endpoint = "geolist";
+        public string token;
+        public double latitude;
+        public double longitude;
+        public double radius;
+    }
+
+    [Serializable]
+    public struct SDKJobsResult
+    {
+        public string error;
+        public int count;
+        public SDKJob[] jobs;
+    }
+
+    [Serializable]
+    public struct SDKImageRequest
+    {
+        public static string endpoint = "capture";
+        public string token;
+        public int run;
+        public int index;
+        public bool anchor;
+        public double px;
+        public double py;
+        public double pz;
+        public double r00;
+        public double r01;
+        public double r02;
+        public double r10;
+        public double r11;
+        public double r12;
+        public double r20;
+        public double r21;
+        public double r22;
+        public double fx;
+        public double fy;
+        public double ox;
+        public double oy;
+        public double latitude;
+        public double longitude;
+        public double altitude;
+    }
+
+    [Serializable]
+    public struct SDKImageResult
+    {
+        public string error;
+        public string path;
+    }
+    
+    [Serializable]
+    public struct SDKGeoLocalizeRequest
+    {
+        public static string endpoint = "geolocalize";
+        public string token;
+        public double fx;
+        public double fy;
+        public double ox;
+        public double oy;
+        public double latitude;
+        public double longitude;
+        public double radius;
+    }
+
+    [Serializable]
+    public struct SDKLocalizeRequest
+    {
+        public static string endpoint = "localize";
+        public string token;
+        public double fx;
+        public double fy;
+        public double ox;
+        public double oy;
+        public SDKMapId[] mapIds;
+    }
+
+    [Serializable]
+    public struct SDKGeoPoseRequest
+    {
+        public static string endpoint = "geopose";
+        public string token;
+        public double fx;
+        public double fy;
+        public double ox;
+        public double oy;
+        public SDKMapId[] mapIds;
+    }
+
+    [Serializable]
+    public struct SDKLocalizeResult
+    {
+        public string error;
+        public bool success;
+        public int map;
+        public float px;
+        public float py;
+        public float pz;
+        public float r00;
+        public float r01;
+        public float r02;
+        public float r10;
+        public float r11;
+        public float r12;
+        public float r20;
+        public float r21;
+        public float r22;
+        public float time;
+    }
+
+    [Serializable]
+    public struct SDKGeoPoseResult
+    {
+        public string error;
+        public bool success;
+        public int map;
+        public double latitude;
+        public double longitude;
+        public double ellipsoidHeight;
+        public float[] quaternion;
+    }
+
+    [Serializable]
+    public struct SDKEcefRequest
+    {
+        public static string endpoint = "ecef";
+        public string token;
+        public int id;
+    }
+
+    [Serializable]
+    public struct SDKEcefResult
+    {
+        public string error;
+        public double[] ecef;
+    }
+
+    [Serializable]
+    public struct SDKSetMapAccessTokenRequest
+    {
+        public static string endpoint = "setmaptoken";
+        public string token;
+        public int id;
+    }
+    
+    [Serializable]
+    public struct SDKClearMapAccessTokenRequest
+    {
+        public static string endpoint = "clearmaptoken";
+        public string token;
+        public int id;
+    }
+    
+    [Serializable]
+    public struct SDKMapAccessTokenResult
+    {
+        public string error;
+        public int mapId;
+        public string accessToken;
+    }
+    
+    [Serializable]
+    public struct SDKMapBinaryRequest
+    {
+        public static string endpoint = "map";
+        public string token;
+        public int id;
+    }
+    
+    [Serializable]
+    public struct SDKMapRequest
+    {
+        public static string endpoint = "mapb64";
+        public string token;
+        public int id;
+    }
+
+    [Serializable]
+    public struct SDKMapResult
+    {
+        public string error;
+        public string sha256_al;
+        public string b64;
+        public byte[] mapData;
+        public SDKMapMetadataGetResult metadata;
+    }
+
+    [Serializable]
+    public struct SDKDeleteMapRequest
+    {
+        public static string endpoint = "delete";
+        public string token;
+        public int id;
+    }
+
+    [Serializable]
+    public struct SDKDeleteMapResult
+    {
+        public string error;
+    }
+
+    [Serializable]
+    public struct SDKRestoreMapImagesRequest
+    {
+        public static string endpoint = "restore";
+        public string token;
+        public int id;
+        public bool clear;
+    }
+
+    [Serializable]
+    public struct SDKRestoreMapImagesResult
+    {
+        public string error;
+    }
+
+    [Serializable]
+    public struct SDKMapPrivacyRequest
+    {
+        public static string endpoint = "privacy";
+        public string token;
+        public int id;
+        public int privacy;
+    }
+
+    [Serializable]
+    public struct SDKMapPrivacyResult
+    {
+        public string error;
+    }
+
+    [Serializable]
+    public struct SDKMapDownloadRequest
+    {
+        public static string endpoint = "mapb64";
+        public string token;
+        public int id;
+    }
+
+    [Serializable]
+    public struct SDKMapDownloadResult
+    {
+        public string error;
+        public string sha256_al;
+        public string b64;
+    }
+
+    [Serializable]
+    public struct SDKMapUploadRequest
+    {
+        public static string endpoint = "uploadmap";
+        public string token;
+        public string name;
+        public double latitude;
+        public double longitude;
+        public double altitude;
+    }
+
+    [Serializable]
+    public struct SDKMapUploadResult
+    {
+        public string error;
+        public int id;
+    }
+
+    [Serializable]
+    public struct SDKMapMetadataGetRequest
+    {
+        public static string endpoint = "metadataget";
+        public string token;
+        public int id;
+    }
+
+    [Serializable]
+    public struct SDKMapMetadataGetResult
+    {
+        public string error;
+        public int id;
+        public int type;
+        public string created;
+        public string version;
+        public int user;
+        public int creator;
+        public string name;
+        public int size;
+        public string status;
+        public int privacy;
+        public double latitude;
+        public double longitude;
+        public double altitude;
+        public double tx;
+        public double ty;
+        public double tz;
+        public double qw;
+        public double qx;
+        public double qy;
+        public double qz;
+        public double scale;
+        public string sha256_al;
+        public string sha256_sparse;
+        public string sha256_dense;
+        public string sha256_tex;
+    }
+
+    [Serializable]
+    public struct SDKMapAlignmentSetRequest
+    {
+        public static string endpoint = "metadataset";
+        public string token;
+        public int id;
+        public double tx;
+        public double ty;
+        public double tz;
+        public double qw;
+        public double qx;
+        public double qy;
+        public double qz;
+        public double scale;
+    }
+
+    [Serializable]
+    public struct SDKMapAlignmentSetResult
+    {
+        public string error;
+    }
+
+    [Serializable]
+    public struct SDKMapAlignmentResetRequest
+    {
+        public static string endpoint = "reset";
+        public string token;
+        public int id;
+    }
+
+    [Serializable]
+    public struct SDKMapAlignmentResetResult
+    {
+        public string error;
+    }
+}

+ 11 - 0
Assets/ImmersalSDK/Core/Scripts/REST.cs.meta

@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 615bbea05426f4575a109159373ba4f0
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 893 - 0
Assets/ImmersalSDK/Core/Scripts/RESTJobsAsync.cs

@@ -0,0 +1,893 @@
+/*===============================================================================
+Copyright (C) 2023 Immersal - Part of Hexagon. All Rights Reserved.
+
+This file is part of the Immersal SDK.
+
+The Immersal SDK cannot be copied, distributed, or made available to
+third-parties for commercial purposes without written permission of Immersal Ltd.
+
+Contact sales@immersal.com for licensing requests.
+===============================================================================*/
+
+using System;
+using System.Runtime.InteropServices;
+using System.Threading;
+using System.Threading.Tasks;
+using System.Net.Http;
+using System.IO;
+using System.Text;
+using UnityEngine;
+
+namespace Immersal.REST
+{
+    public class ImmersalHttp
+    {
+        public static readonly string URL_FORMAT = "{0}/{1}";
+
+        public static async Task<U> Request<T, U>(T request, IProgress<float> progress)
+        {
+            U result = default(U);
+            string jsonString = JsonUtility.ToJson(request);
+            HttpRequestMessage r = new HttpRequestMessage(HttpMethod.Post, string.Format(URL_FORMAT, ImmersalSDK.Instance.localizationServer, (string)typeof(T).GetField("endpoint").GetValue(null)));
+            r.Content = new StringContent(jsonString);
+
+            try
+            {
+                using (MemoryStream stream = new MemoryStream())
+                {
+                    using (var response = await ImmersalSDK.client.DownloadAsync(r, stream, progress, CancellationToken.None))
+                    {
+                        string responseBody = Encoding.ASCII.GetString(stream.GetBuffer(), 0, (int)stream.Length);
+                        //Debug.Log(responseBody);
+                        result = JsonUtility.FromJson<U>(responseBody);
+                        if (!response.IsSuccessStatusCode)
+                        {
+                            Debug.LogWarningFormat("ImmersalHttp error: {0} ({1}), {2}\nrequest JSON: {3}\nresponse JSON: {4}", (int)response.StatusCode, response.ReasonPhrase, response.RequestMessage, jsonString, responseBody);
+                        }
+                    }
+                }
+            }
+            catch (Exception e)
+            {
+                Debug.LogErrorFormat("ImmersalHttp connection error: {0}", e.Message);
+            }
+
+            return result;
+        }
+
+        public static async Task<byte[]> RequestGet(string uri, IProgress<float> progress)
+        {
+            byte[] result = null;
+            HttpRequestMessage r = new HttpRequestMessage(HttpMethod.Get, uri);
+
+            try
+            {
+                using (MemoryStream stream = new MemoryStream())
+                {
+                    using (var response = await ImmersalSDK.client.DownloadAsync(r, stream, progress, CancellationToken.None))
+                    {
+                        result = stream.GetBuffer();
+                        Array.Resize(ref result, (int)stream.Length);
+
+                        if (!response.IsSuccessStatusCode)
+                        {
+                            Debug.LogWarningFormat("ImmersalHttp error: {0} ({1}), {2}", (int)response.StatusCode, response.ReasonPhrase, response.RequestMessage);
+                        }
+                    }
+                }
+            }
+            catch (Exception e)
+            {
+                Debug.LogErrorFormat("ImmersalHttp connection error: {0}", e.Message);
+            }
+
+            return result;
+        }
+
+        public static async Task<U> RequestUpload<T, U>(T request, byte[] data, IProgress<float> progress)
+        {
+            U result = default(U);
+            string jsonString = JsonUtility.ToJson(request);
+            byte[] jsonBytes = Encoding.ASCII.GetBytes(jsonString);
+            byte[] body = new byte[jsonBytes.Length + 1 + data.Length];
+            Array.Copy(jsonBytes, 0, body, 0, jsonBytes.Length);
+            body[jsonBytes.Length] = 0;
+            Array.Copy(data, 0, body, jsonBytes.Length + 1, data.Length);
+            HttpRequestMessage r = new HttpRequestMessage(HttpMethod.Post, string.Format(URL_FORMAT, ImmersalSDK.Instance.localizationServer, (string)typeof(T).GetField("endpoint").GetValue(null)));
+            var byteStream = new ProgressMemoryStream(body, progress);
+            r.Content = new StreamContent(byteStream);
+
+            try
+            {
+                using (MemoryStream stream = new MemoryStream())
+                {
+                    using (var response = await ImmersalSDK.client.DownloadAsync(r, stream, null, CancellationToken.None))
+                    {
+                        string responseBody = Encoding.ASCII.GetString(stream.GetBuffer(), 0, (int)stream.Length);
+                        //Debug.Log(responseBody);
+                        result = JsonUtility.FromJson<U>(responseBody);
+                        if (!response.IsSuccessStatusCode)
+                        {
+                            Debug.LogWarningFormat("ImmersalHttp error: {0} ({1}), {2}", (int)response.StatusCode, response.ReasonPhrase, response.RequestMessage);
+                        }
+                    }
+                }
+            }
+            catch (Exception e)
+            {
+                Debug.LogErrorFormat("ImmersalHttp connection error: {0}", e.Message);
+            }
+
+            return result;
+        }
+    }
+
+    public static class HttpClientExtensions
+    {
+        public static async Task<HttpResponseMessage> DownloadAsync(this HttpClient client, HttpRequestMessage request, Stream destination, IProgress<float> progress = null, CancellationToken cancellationToken = default) {
+            using (var response = await client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead))
+            {
+                request.Dispose();
+                
+                var contentLength = response.Content.Headers.ContentLength;
+
+                using (var download = await response.Content.ReadAsStreamAsync())
+                {
+                    if (progress == null || !contentLength.HasValue)
+                    {
+                        await download.CopyToAsync(destination);
+                        return response;
+                    }
+
+                    var relativeProgress = new Progress<long>(totalBytes => progress.Report((float)totalBytes / contentLength.Value));
+                    await download.CopyToAsync(destination, 81920, relativeProgress, cancellationToken);
+                }
+
+                return response;
+            }
+        }
+    }
+
+    public class ProgressMemoryStream : MemoryStream
+    {
+        IProgress<float> progress;
+        private int length;
+
+        public ProgressMemoryStream(byte[] buffer, IProgress<float> progress = null)
+            : base(buffer, true) {
+            
+            this.length = buffer.Length;
+            this.progress = progress;
+        }
+
+        public override int Read([In, Out] byte[] buffer, int offset, int count) {
+            int n = base.Read(buffer, offset, count);
+            progress?.Report((float)this.Position / this.length);
+            return n;
+        }
+    }
+
+    public static class StreamExtensions
+    {
+        public static async Task CopyToAsync(this Stream source, Stream destination, int bufferSize, IProgress<long> progress = null, CancellationToken cancellationToken = default) {
+            if (source == null)
+                throw new ArgumentNullException(nameof(source));
+            if (!source.CanRead)
+                throw new ArgumentException("Has to be readable", nameof(source));
+            if (destination == null)
+                throw new ArgumentNullException(nameof(destination));
+            if (!destination.CanWrite)
+                throw new ArgumentException("Has to be writable", nameof(destination));
+            if (bufferSize < 0)
+                throw new ArgumentOutOfRangeException(nameof(bufferSize));
+
+            var buffer = new byte[bufferSize];
+            long totalBytesRead = 0;
+            int bytesRead;
+            while ((bytesRead = await source.ReadAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false)) != 0) {
+                await destination.WriteAsync(buffer, 0, bytesRead, cancellationToken).ConfigureAwait(false);
+                totalBytesRead += bytesRead;
+                progress?.Report(totalBytesRead);
+            }
+        }
+    }
+
+    public class JobAsync
+    {
+        public string token = ImmersalSDK.Instance.developerToken;
+        public Action OnStart;
+        public Action<string> OnError;
+        public Progress<float> Progress = new Progress<float>();
+
+        public virtual async Task RunJobAsync()
+        {
+            await Task.Yield();
+        }
+
+        protected void HandleError(string e)
+        {
+            OnError?.Invoke(e ?? "conn");
+        }
+    }
+
+    public class JobSetMapAccessTokenAsync : JobAsync
+    {
+        public int id;
+        public Action<SDKMapAccessTokenResult> OnResult;
+
+        public override async Task RunJobAsync()
+        {
+            Debug.Log("*************************** JobSetMapAccessTokenAsync ***************************");
+            this.OnStart?.Invoke();
+
+            SDKSetMapAccessTokenRequest r = new SDKSetMapAccessTokenRequest();
+            r.token = this.token;
+            r.id = this.id;
+            SDKMapAccessTokenResult result = await ImmersalHttp.Request<SDKSetMapAccessTokenRequest, SDKMapAccessTokenResult>(r, this.Progress);
+            if (result.error == "none")
+            {
+                this.OnResult?.Invoke(result);
+            }
+            else
+            {
+                HandleError(result.error);
+            }
+        }
+    }
+    
+    public class JobClearMapAccessTokenAsync : JobAsync
+    {
+        public int id;
+        public Action<SDKMapAccessTokenResult> OnResult;
+
+        public override async Task RunJobAsync()
+        {
+            Debug.Log("*************************** JobClearMapAccessTokenAsync ***************************");
+            this.OnStart?.Invoke();
+
+            SDKClearMapAccessTokenRequest r = new SDKClearMapAccessTokenRequest();
+            r.token = this.token;
+            r.id = this.id;
+            SDKMapAccessTokenResult result = await ImmersalHttp.Request<SDKClearMapAccessTokenRequest, SDKMapAccessTokenResult>(r, this.Progress);
+            
+            if (result.error == "none")
+            {
+                this.OnResult?.Invoke(result);
+            }
+            else
+            {
+                HandleError(result.error);
+            }
+        }
+    }
+    
+    public class JobClearAsync : JobAsync
+    {
+        public bool anchor;
+        public Action<SDKClearResult> OnResult;
+
+        public override async Task RunJobAsync()
+        {
+            this.OnStart?.Invoke();
+
+            SDKClearRequest r = new SDKClearRequest();
+            r.token = this.token;
+            r.anchor = this.anchor;
+            SDKClearResult result = await ImmersalHttp.Request<SDKClearRequest, SDKClearResult>(r, this.Progress);
+            if (result.error == "none")
+            {
+                this.OnResult?.Invoke(result);
+            }
+            else
+            {
+                HandleError(result.error);
+            }
+        }
+    }
+
+    public class JobConstructAsync : JobAsync
+    {
+        public string name;
+        public int featureCount = 1024;
+        public int featureType = 2;
+        public int windowSize = 0;
+        public bool preservePoses = false;
+        public Action<SDKConstructResult> OnResult;
+
+        public override async Task RunJobAsync()
+        {
+            this.OnStart?.Invoke();
+
+            SDKConstructRequest r = new SDKConstructRequest();
+            r.token = this.token;
+            r.name = this.name;
+            r.featureCount = this.featureCount;
+            r.featureType = this.featureType;
+            r.windowSize = this.windowSize;
+            r.preservePoses = this.preservePoses;
+            SDKConstructResult result = await ImmersalHttp.Request<SDKConstructRequest, SDKConstructResult>(r, this.Progress);
+            if (result.error == "none")
+            {
+                this.OnResult?.Invoke(result);
+            }
+            else
+            {
+                HandleError(result.error);
+            }
+        }
+    }
+
+    public class JobRestoreMapImagesAsync : JobAsync
+    {
+        public int id;
+        public bool clear;
+        public Action<SDKRestoreMapImagesResult> OnResult;
+
+        public override async Task RunJobAsync()
+        {
+            this.OnStart?.Invoke();
+
+            SDKRestoreMapImagesRequest r = new SDKRestoreMapImagesRequest();
+            r.token = this.token;
+            r.id = this.id;
+            r.clear = this.clear;
+            
+            SDKRestoreMapImagesResult result = await ImmersalHttp.Request<SDKRestoreMapImagesRequest, SDKRestoreMapImagesResult>(r, this.Progress);
+            if (result.error == "none")
+            {
+                this.OnResult?.Invoke(result);
+            }
+            else
+            {
+                HandleError(result.error);
+            }
+        }
+    }
+
+    public class JobDeleteMapAsync : JobAsync
+    {
+        public int id;
+        public Action<SDKDeleteMapResult> OnResult;
+
+        public override async Task RunJobAsync()
+        {
+            this.OnStart?.Invoke();
+
+            SDKDeleteMapRequest r = new SDKDeleteMapRequest();
+            r.token = this.token;
+            r.id = this.id;
+            SDKDeleteMapResult result = await ImmersalHttp.Request<SDKDeleteMapRequest, SDKDeleteMapResult>(r, this.Progress);
+            if (result.error == "none")
+            {
+                this.OnResult?.Invoke(result);
+            }
+            else
+            {
+                HandleError(result.error);
+            }
+        }
+    }
+
+    public class JobStatusAsync : JobAsync
+    {
+        public Action<SDKStatusResult> OnResult;
+
+        public override async Task RunJobAsync()
+        {
+            this.OnStart?.Invoke();
+
+            SDKStatusRequest r = new SDKStatusRequest();
+            r.token = this.token;
+            SDKStatusResult result = await ImmersalHttp.Request<SDKStatusRequest, SDKStatusResult>(r, this.Progress);
+            if (result.error == "none")
+            {
+                this.OnResult?.Invoke(result);
+            }
+            else
+            {
+                HandleError(result.error);
+            }
+        }
+    }
+
+    public class JobMapUploadAsync : JobAsync
+    {
+        public string name;
+        public double latitude = 0.0;
+        public double longitude = 0.0;
+        public double altitude = 0.0;
+        public byte[] mapData;
+        public Action<SDKMapUploadResult> OnResult;
+
+        public override async Task RunJobAsync()
+        {
+            this.OnStart?.Invoke();
+
+            SDKMapUploadRequest r = new SDKMapUploadRequest();
+            r.token = this.token;
+            r.name = this.name;
+            r.latitude = this.latitude;
+            r.longitude = this.longitude;
+            r.altitude = this.altitude;
+            Debug.LogFormat("Uploading map {0} with token {1}", r.name, r.token);
+
+            SDKMapUploadResult result = await ImmersalHttp.RequestUpload<SDKMapUploadRequest, SDKMapUploadResult>(r, mapData, this.Progress);
+            if (result.error == "none")
+            {
+                this.OnResult?.Invoke(result);
+            }
+            else
+            {
+                HandleError(result.error);
+            }
+        }
+    }
+
+    public class JobCaptureAsync : JobAsync
+    {
+        public int run;
+        public int index;
+        public bool anchor;
+        public Vector4 intrinsics;
+        public Matrix4x4 rotation;
+        public Vector3 position;
+        public double latitude;
+        public double longitude;
+        public double altitude;
+        public string encodedImage;
+        public string imagePath;
+        public Action<SDKImageResult> OnResult;
+
+        public override async Task RunJobAsync()
+        {
+            this.OnStart?.Invoke();
+
+            SDKImageRequest r = new SDKImageRequest();
+            r.token = this.token;
+            r.run = this.run;
+            r.index = this.index;
+            r.anchor = this.anchor;
+            r.px = position.x;
+            r.py = position.y;
+            r.pz = position.z;
+            r.r00 = rotation.m00;
+            r.r01 = rotation.m01;
+            r.r02 = rotation.m02;
+            r.r10 = rotation.m10;
+            r.r11 = rotation.m11;
+            r.r12 = rotation.m12;
+            r.r20 = rotation.m20;
+            r.r21 = rotation.m21;
+            r.r22 = rotation.m22;
+            r.fx = intrinsics.x;
+            r.fy = intrinsics.y;
+            r.ox = intrinsics.z;
+            r.oy = intrinsics.w;
+            r.latitude = latitude;
+            r.longitude = longitude;
+            r.altitude = altitude;
+
+            byte[] image = File.ReadAllBytes(imagePath);
+
+            SDKImageResult result = await ImmersalHttp.RequestUpload<SDKImageRequest, SDKImageResult>(r, image, this.Progress);
+            if (result.error == "none")
+            {
+                this.OnResult?.Invoke(result);
+            }
+            else
+            {
+                HandleError(result.error);
+            }
+        }
+    }
+
+    public class JobLocalizeServerAsync : JobAsync
+    {
+        public Vector3 position;
+        public Quaternion rotation;
+        public Vector4 intrinsics;
+        public double latitude = 0.0;
+        public double longitude = 0.0;
+        public double radius = 0.0;
+        public bool useGPS = false;
+        public SDKMapId[] mapIds;
+        public byte[] image;
+        public Action<SDKLocalizeResult> OnResult;
+
+        public override async Task RunJobAsync()
+        {
+            this.OnStart?.Invoke();
+
+            SDKLocalizeResult result = default;
+
+            if (this.useGPS)
+            {
+                SDKGeoLocalizeRequest r = new SDKGeoLocalizeRequest();
+                r.token = this.token;
+                r.fx = intrinsics.x;
+                r.fy = intrinsics.y;
+                r.ox = intrinsics.z;
+                r.oy = intrinsics.w;
+                r.latitude = this.latitude;
+                r.longitude = this.longitude;
+                r.radius = this.radius;
+                result = await ImmersalHttp.RequestUpload<SDKGeoLocalizeRequest, SDKLocalizeResult>(r, this.image, this.Progress);
+            }
+            else
+            {
+                SDKLocalizeRequest r = new SDKLocalizeRequest();
+                r.token = this.token;
+                r.fx = intrinsics.x;
+                r.fy = intrinsics.y;
+                r.ox = intrinsics.z;
+                r.oy = intrinsics.w;
+                r.mapIds = this.mapIds;
+                result = await ImmersalHttp.RequestUpload<SDKLocalizeRequest, SDKLocalizeResult>(r, this.image, this.Progress);
+            }
+
+            if (result.error == "none")
+            {
+                this.OnResult?.Invoke(result);
+            }
+            else
+            {
+                HandleError(result.error);
+            }
+        }
+    }
+
+    public class JobGeoPoseAsync : JobAsync
+    {
+        public Vector3 position;
+        public Quaternion rotation;
+        public Vector4 intrinsics;
+        public SDKMapId[] mapIds;
+        public byte[] image;
+        public Action<SDKGeoPoseResult> OnResult;
+
+        public override async Task RunJobAsync()
+        {
+            this.OnStart?.Invoke();
+
+            SDKGeoPoseRequest r = new SDKGeoPoseRequest();
+            r.token = this.token;
+            r.fx = intrinsics.x;
+            r.fy = intrinsics.y;
+            r.ox = intrinsics.z;
+            r.oy = intrinsics.w;
+            r.mapIds = this.mapIds;
+
+            SDKGeoPoseResult result = await ImmersalHttp.RequestUpload<SDKGeoPoseRequest, SDKGeoPoseResult>(r, this.image, this.Progress);
+            if (result.error == "none")
+            {
+                this.OnResult?.Invoke(result);
+            }
+            else
+            {
+                HandleError(result.error);
+            }
+        }
+    }
+
+    public class JobEcefAsync : JobAsync
+    {
+        public int id;
+        public bool useToken = true;
+        public Action<SDKEcefResult> OnResult;
+
+        public override async Task RunJobAsync()
+        {
+            this.OnStart?.Invoke();
+
+            SDKEcefRequest r = new SDKEcefRequest();
+            r.token = useToken ? this.token : "";
+            r.id = this.id;
+            SDKEcefResult result = await ImmersalHttp.Request<SDKEcefRequest, SDKEcefResult>(r, this.Progress);
+            if (result.error == "none")
+            {
+                this.OnResult?.Invoke(result);
+            }
+            else
+            {
+                HandleError(result.error);
+            }
+        }
+    }
+
+    public class JobListJobsAsync : JobAsync
+    {
+        public double latitude = 0.0;
+        public double longitude = 0.0;
+        public double radius = 0.0;
+        public bool useGPS = false;
+        public bool useToken = true;
+        public Action<SDKJobsResult> OnResult;
+
+        public override async Task RunJobAsync()
+        {
+            this.OnStart?.Invoke();
+
+            SDKJobsResult result = default;
+
+            if (this.useGPS)
+            {
+                SDKGeoJobsRequest r = new SDKGeoJobsRequest();
+                r.token = this.useToken ? this.token : "";
+                r.latitude = this.latitude;
+                r.longitude = this.longitude;
+                r.radius = this.radius;
+                result = await ImmersalHttp.Request<SDKGeoJobsRequest, SDKJobsResult>(r, this.Progress);
+            }
+            else
+            {
+                SDKJobsRequest r = new SDKJobsRequest();
+                r.token = this.useToken ? this.token : "";
+                result = await ImmersalHttp.Request<SDKJobsRequest, SDKJobsResult>(r, this.Progress);
+            }
+
+            if (result.error == "none")
+            {
+                this.OnResult?.Invoke(result);
+            }
+            else
+            {
+                HandleError(result.error);
+            }
+        }
+    }
+
+    public class JobLoadMapBinaryAsync : JobAsync
+    {
+        public int id;
+        public bool useToken = true;
+        public string sha256_al;
+        public Action<SDKMapResult> OnResult;
+
+        public override async Task RunJobAsync()
+        {
+            this.OnStart?.Invoke();
+
+            SDKMapBinaryRequest r = new SDKMapBinaryRequest();
+            r.token = this.useToken ?  this.token : "";
+            r.id = this.id;
+
+            string uri = string.Format(ImmersalHttp.URL_FORMAT, ImmersalSDK.Instance.localizationServer, SDKMapBinaryRequest.endpoint);
+            uri += (r.token != "") ? string.Format("?token={0}&id={1}", r.token, r.id) : string.Format("?id={0}", r.id);
+
+            HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, uri);
+            SDKMapResult result = default;
+            byte[] data = await ImmersalHttp.RequestGet(uri, this.Progress);
+
+            if (data == null || data.Length == 0)
+            {
+                result.error = "no data";
+            }
+            else if (data.Length <= 256) // error
+            {
+                var str = Encoding.Default.GetString(data);
+                result = JsonUtility.FromJson<SDKMapResult>(str);
+            }
+            else
+            {
+                result.error = "none";
+                result.sha256_al = this.sha256_al;
+                result.mapData = data;
+            }
+
+            if (result.error == "none")
+            {
+                JobMapMetadataGetAsync j = new JobMapMetadataGetAsync();
+                j.id = this.id;
+                j.token = r.token;
+                j.OnError += (e) =>
+                {
+                    this.OnResult?.Invoke(result);
+                };
+
+                j.OnResult += (SDKMapMetadataGetResult metadata) => 
+                {
+                    if (metadata.error == "none")
+                    {
+                        result.metadata = metadata;
+                        this.OnResult?.Invoke(result);
+                    }
+                    else
+                    {
+                        HandleError(metadata.error);
+                    }
+                };
+
+                await j.RunJobAsync();
+            }
+            else
+            {
+                HandleError(result.error);
+            }
+        }
+    }
+
+    public class JobLoadMapAsync : JobAsync
+    {
+        public int id;
+        public bool useToken = true;
+        public Action<SDKMapResult> OnResult;
+
+        public override async Task RunJobAsync()
+        {
+            this.OnStart?.Invoke();
+
+            SDKMapRequest r = new SDKMapRequest();
+            r.token = this.useToken ? this.token : "";
+            r.id = this.id;
+            SDKMapResult result = await ImmersalHttp.Request<SDKMapRequest, SDKMapResult>(r, this.Progress);
+            if (result.error == "none")
+            {
+                JobMapMetadataGetAsync j = new JobMapMetadataGetAsync();
+                j.id = this.id;
+                j.token = r.token;
+                j.OnError += (e) =>
+                {
+                    this.OnResult?.Invoke(result);
+                };
+
+                j.OnResult += (SDKMapMetadataGetResult metadata) => 
+                {
+                    if (metadata.error == "none")
+                    {
+                        result.metadata = metadata;
+                        this.OnResult?.Invoke(result);
+                    }
+                    else
+                    {
+                        HandleError(metadata.error);
+                    }
+                };
+
+                await j.RunJobAsync();
+            }
+            else
+            {
+                HandleError(result.error);
+            }
+        }
+    }
+
+    public class JobSetPrivacyAsync : JobAsync
+    {
+        public int id;
+        public int privacy;
+        public Action<SDKMapPrivacyResult> OnResult;
+
+        public override async Task RunJobAsync()
+        {
+            this.OnStart?.Invoke();
+
+            SDKMapPrivacyRequest r = new SDKMapPrivacyRequest();
+            r.token = this.token;
+            r.id = this.id;
+            r.privacy = this.privacy;
+            SDKMapPrivacyResult result = await ImmersalHttp.Request<SDKMapPrivacyRequest, SDKMapPrivacyResult>(r, this.Progress);
+            if (result.error == "none")
+            {
+                this.OnResult?.Invoke(result);
+            }
+            else
+            {
+                HandleError(result.error);
+            }
+        }
+    }
+
+    public class JobLoginAsync : JobAsync
+    {
+        public string username;
+        public string password;
+        public Action<SDKLoginResult> OnResult;
+
+        public override async Task RunJobAsync()
+        {
+            this.OnStart?.Invoke();
+
+            SDKLoginRequest r = new SDKLoginRequest();
+            r.login = this.username;
+            r.password = this.password;
+            SDKLoginResult result = await ImmersalHttp.Request<SDKLoginRequest, SDKLoginResult>(r, this.Progress);
+            if (result.error == "none")
+            {
+                this.OnResult?.Invoke(result);
+            }
+            else
+            {
+                HandleError(result.error);
+            }
+        }
+    }
+
+    public class JobMapDownloadAsync : JobAsync
+    {
+        public int id;
+        public Action<SDKMapDownloadResult> OnResult;
+
+        public override async Task RunJobAsync()
+        {
+            this.OnStart?.Invoke();
+
+            SDKMapDownloadRequest r = new SDKMapDownloadRequest();
+            r.token = this.token;
+            r.id = this.id;
+            SDKMapDownloadResult result = await ImmersalHttp.Request<SDKMapDownloadRequest, SDKMapDownloadResult>(r, this.Progress);
+            if (result.error == "none")
+            {
+                this.OnResult?.Invoke(result);
+            }
+            else
+            {
+                HandleError(result.error);
+            }
+        }
+    }
+
+    public class JobMapMetadataGetAsync : JobAsync
+    {
+        public int id;
+        public Action<SDKMapMetadataGetResult> OnResult;
+
+        public override async Task RunJobAsync()
+        {
+            this.OnStart?.Invoke();
+
+            SDKMapMetadataGetRequest r = new SDKMapMetadataGetRequest();
+            r.token = this.token;
+            r.id = this.id;
+            SDKMapMetadataGetResult result = await ImmersalHttp.Request<SDKMapMetadataGetRequest, SDKMapMetadataGetResult>(r, this.Progress);
+            if (result.error == "none")
+            {
+                this.OnResult?.Invoke(result);
+            }
+            else
+            {
+                HandleError(result.error);
+            }
+        }
+    }
+
+    public class JobMapAlignmentSetAsync : JobAsync
+    {
+        public int id;
+        public double tx;
+        public double ty;
+        public double tz;
+        public double qw;
+        public double qx;
+        public double qy;
+        public double qz;
+        public double scale;
+        public Action<SDKMapAlignmentSetResult> OnResult;
+
+        public override async Task RunJobAsync()
+        {
+            this.OnStart?.Invoke();
+
+            SDKMapAlignmentSetRequest r = new SDKMapAlignmentSetRequest();
+            r.token = this.token;
+            r.id = this.id;
+            r.tx = this.tx;
+            r.ty = this.ty;
+            r.tz = this.tz;
+            r.qw = this.qw;
+            r.qx = this.qx;
+            r.qy = this.qy;
+            r.qz = this.qz;
+            r.scale = this.scale;
+            SDKMapAlignmentSetResult result = await ImmersalHttp.Request<SDKMapAlignmentSetRequest, SDKMapAlignmentSetResult>(r, this.Progress);
+            if (result.error == "none")
+            {
+                this.OnResult?.Invoke(result);
+            }
+            else
+            {
+                HandleError(result.error);
+            }
+        }
+    }
+}

+ 11 - 0
Assets/ImmersalSDK/Core/Scripts/RESTJobsAsync.cs.meta

@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: ec69c2b82779b4f0aaedc149e6983ddb
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 6 - 0
Assets/ImmersalSDK/Core/Scripts/ReadOnlyAttribute.cs

@@ -0,0 +1,6 @@
+using UnityEngine;
+
+public class ReadOnlyAttribute : PropertyAttribute
+{
+
+}

+ 11 - 0
Assets/ImmersalSDK/Core/Scripts/ReadOnlyAttribute.cs.meta

@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 2058786f596e5a34bae16afef4229107
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 21 - 0
Assets/ImmersalSDK/ImmersalSDK.asmdef

@@ -0,0 +1,21 @@
+{
+    "name": "ImmersalSDK",
+    "rootNamespace": "",
+    "references": [
+        "GUID:a9420e37d7990b54abdef6688edbe313",
+        "GUID:92703082f92b41ba80f0d6912de66115",
+        "GUID:478a2357cc57436488a56e564b08d223",
+        "GUID:ba171b3dd2a51234ab864770f99741a5",
+        "GUID:6055be8ebefd69e48b49212b09b47b2f",
+        "GUID:dc960734dc080426fa6612f1c5fe95f3"
+    ],
+    "includePlatforms": [],
+    "excludePlatforms": [],
+    "allowUnsafeCode": true,
+    "overrideReferences": false,
+    "precompiledReferences": [],
+    "autoReferenced": true,
+    "defineConstraints": [],
+    "versionDefines": [],
+    "noEngineReferences": false
+}

+ 7 - 0
Assets/ImmersalSDK/ImmersalSDK.asmdef.meta

@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: 8aa748a3b2aa5c94b829fc71fa739175
+AssemblyDefinitionImporter:
+  externalObjects: {}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 8 - 0
Assets/ImmersalSDK/Samples.meta

@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 6229708e9fe744d689e61385fc836ead
+folderAsset: yes
+DefaultImporter:
+  externalObjects: {}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 8 - 0
Assets/ImmersalSDK/Samples/Fonts.meta

@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 7d667ded3ad954cafb3d39c61759c54b
+folderAsset: yes
+DefaultImporter:
+  externalObjects: {}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 8 - 0
Assets/ImmersalSDK/Samples/Fonts/LiberationSans.meta

@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 6fc14a78783cd194d8cddda28465231e
+folderAsset: yes
+DefaultImporter:
+  externalObjects: {}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

File diff suppressed because it is too large
+ 7826 - 0
Assets/ImmersalSDK/Samples/Fonts/LiberationSans/LiberationSans SDF DebugConsole.asset


+ 8 - 0
Assets/ImmersalSDK/Samples/Fonts/LiberationSans/LiberationSans SDF DebugConsole.asset.meta

@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: faedf4e49540105449e88d992595e85a
+NativeFormatImporter:
+  externalObjects: {}
+  mainObjectFileID: 11400000
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 8 - 0
Assets/ImmersalSDK/Samples/Fonts/Roboto Mono.meta

@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 80e0062821125974db0239650672030a
+folderAsset: yes
+DefaultImporter:
+  externalObjects: {}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

File diff suppressed because it is too large
+ 43 - 0
Assets/ImmersalSDK/Samples/Fonts/Roboto Mono/RobotoMono-Light SDF.asset


+ 8 - 0
Assets/ImmersalSDK/Samples/Fonts/Roboto Mono/RobotoMono-Light SDF.asset.meta

@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: dc48ab5070ccd6f4cb016efa0d873725
+NativeFormatImporter:
+  externalObjects: {}
+  mainObjectFileID: 11400000
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

File diff suppressed because it is too large
+ 5681 - 0
Assets/ImmersalSDK/Samples/Fonts/Roboto Mono/RobotoMono-Medium SDF.asset


+ 8 - 0
Assets/ImmersalSDK/Samples/Fonts/Roboto Mono/RobotoMono-Medium SDF.asset.meta

@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: e5029ad341efd014aadcc68cfe46a302
+NativeFormatImporter:
+  externalObjects: {}
+  mainObjectFileID: 11400000
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

File diff suppressed because it is too large
+ 5783 - 0
Assets/ImmersalSDK/Samples/Fonts/Roboto Mono/RobotoMono-Regular SDF.asset


Some files were not shown because too many files changed in this diff