/*=============================================================================== Copyright (C) 2024 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.Collections.Generic; using System.Linq; using System.Linq.Expressions; using System.Runtime.InteropServices; using System.Threading; using System.Threading.Tasks; using Immersal.REST; using UnityEditor; using UnityEngine; using UnityEngine.Events; using UnityEngine.Serialization; using Object = UnityEngine.Object; namespace Immersal.XR { public struct LocalizationResults : ILocalizationResults { public ILocalizationResult[] Results { get; set; } } public struct LocalizationResult : ILocalizationResult { public bool Success { get; set; } public int MapId { get; set; } public LocalizeInfo LocalizeInfo { get; set; } } public class Localizer : MonoBehaviour, ILocalizer { // Localization Methods // Serialized as Objects like the components in ImmersalSDK [SerializeField, Interface(typeof(ILocalizationMethod))] private Object[] m_LocalizationMethodObjects; private ILocalizationMethod[] m_CachedLocalizationMethods; // cache deserialized methods public ILocalizationMethod[] AvailableLocalizationMethods { get { // Return if already cached if (m_CachedLocalizationMethods != null) return m_CachedLocalizationMethods; if (m_LocalizationMethodObjects == null) return Array.Empty(); // Deserialize and cache the localization methods m_CachedLocalizationMethods = m_LocalizationMethodObjects.OfType().ToArray(); return m_CachedLocalizationMethods; } } private List m_ConfiguredLocalizationMethods = new List(); // Keep references to running LocalizationTasks in a dictionary // Each type of ILocalizationMethod has it's own LocalizationTask private Dictionary m_RunningLocalizationTasks; // Events [Header("Events"), Space] // Invoked when localization is successful for the first time (since start/reset) public UnityEvent OnFirstSuccessfulLocalization; // Invoked once per Localize call if any localization was successful // int[] includes mapIds of successful localizations public UnityEvent OnSuccessfulLocalizations; public UnityEvent OnLocalizationResult; // Invoked once per Localize call if all localization attempts failed public UnityEvent OnFailedLocalizations; // Other private bool m_IsLocalizing = false; private bool m_HasLocalizedSuccessfully = false; // Configuration // Note: this can get called again after the initial configuration if new LocalizationMethods need to be added public async Task ConfigureLocalizer(ILocalizerConfiguration configuration) { ImmersalLogger.Log("Configuring Localizer"); // Check if tasks are running and stop if requested if (configuration.StopRunningTasks && m_RunningLocalizationTasks is { Count: > 0 }) await StopRunningLocalizationTasks(); // Default to failure ILocalizerConfigurationResult r = new LocalizerConfigurationResult { Success = false }; List configuredMethods = new List(); // Add new configurations if requested if (configuration.ConfigurationsToAdd != null) { foreach (ILocalizationMethod localizationMethod in AvailableLocalizationMethods) { bool isNecessary = configuration.ConfigurationsToAdd.TryGetValue(localizationMethod, out XRMap[] maps); bool configureMethod = localizationMethod.ConfigurationMode switch { ConfigurationMode.WhenNecessary => isNecessary, ConfigurationMode.Always => true, _ => false }; if (configureMethod) { // Try to configure method with associated mapIds if (!await ConfigureLocalizationMethod(localizationMethod, maps)) { // Configure failed, bail out return r; } configuredMethods.Add(localizationMethod); } } } // Refresh our localization method cache to only include configured methods m_ConfiguredLocalizationMethods.AddRange(configuredMethods); // Remove configurations if requested if (configuration.ConfigurationsToRemove != null) { foreach (KeyValuePair keyValuePair in configuration.ConfigurationsToRemove) { await RemoveLocalizationMethodConfiguration(keyValuePair.Key, keyValuePair.Value); } } // Initialize running tasks Dictionary if (m_RunningLocalizationTasks == null) m_RunningLocalizationTasks = new Dictionary(); r.Success = true; return r; } private async Task ConfigureLocalizationMethod(ILocalizationMethod localizationMethod, XRMap[] maps) { ImmersalLogger.Log($"Configuring localization method: {localizationMethod.GetType().Name}"); // Ensure we have the requested localization method available if (!AvailableLocalizationMethods.Contains(localizationMethod)) { ImmersalLogger.LogError("Trying to configure unavailable localization method."); return false; } DefaultLocalizationMethodConfiguration config = new DefaultLocalizationMethodConfiguration { MapsToAdd = maps }; if (await localizationMethod.Configure(config)) { return true; } ImmersalLogger.LogError($"Could not configure localization method: {localizationMethod.GetType().Name}."); return false; } private async Task RemoveLocalizationMethodConfiguration(ILocalizationMethod localizationMethod, XRMap[] maps) { // Check if configured if (!m_ConfiguredLocalizationMethods.Contains(localizationMethod)) { ImmersalLogger.LogError("Trying to remove configurations from a non-configured localization method."); return false; } DefaultLocalizationMethodConfiguration config = new DefaultLocalizationMethodConfiguration { MapsToRemove = maps }; ImmersalLogger.Log($"Removing {maps.Length} maps from {localizationMethod.GetType().Name} configuration"); // Configure will return false if the method does not have any maps configured after removal // => should remove the configuration entirely if set to WhenNecessary if (!await localizationMethod.Configure(config) && localizationMethod.ConfigurationMode == ConfigurationMode.WhenNecessary) { ImmersalLogger.Log($"Removing {localizationMethod.GetType().Name} configuration"); // Cancel possible running task if (m_RunningLocalizationTasks.TryGetValue(localizationMethod, out LocalizationTask task)) { task.CancellationTokenSource.Cancel(); await task.LocalizationMethodTask; } m_ConfiguredLocalizationMethods.Remove(localizationMethod); } return true; } // This ILocalizer implementation can run multiple asynchronous localization tasks (one per method) // The Localize task itself always awaits for one of the internal localization tasks to finish // before providing results. Remaining localization tasks will propagate to the next cycle. public async Task Localize(ICameraData cameraData) { // Localization is already running -> bail out if (m_IsLocalizing) { cameraData.CheckReferences(); return new LocalizationResults { Results = Array.Empty() }; } m_IsLocalizing = true; List results = new List(); // Make sure all LocalizationTasks are running foreach (ILocalizationMethod localizationMethod in m_ConfiguredLocalizationMethods) { // If a task is already running, we check if has completed since last localization cycle if (m_RunningLocalizationTasks.TryGetValue(localizationMethod, out LocalizationTask task)) { if (task.LocalizationMethodTask.IsCompleted) { // Add results and remove so we can start again results.Add(task.LocalizationMethodTask.Result); m_RunningLocalizationTasks.Remove(localizationMethod); task.LocalizationMethodTask.Dispose(); } else { // Skip unfinished tasks continue; } } // Start new localization task StartNewLocalizationTask(localizationMethod, cameraData); } // Wait for any of the currently running localization tasks to finish Task anyTask = Task.WhenAny( m_RunningLocalizationTasks.Values.Select(runningTask => runningTask.LocalizationMethodTask)); try { await anyTask; } catch (OperationCanceledException) { CleanUpLocalizationTasks(); return new LocalizationResults { Results = Array.Empty() }; } ImmersalLogger.Log("Localization task completed"); // Collect results for this cycle and combine with previous results.AddRange(CollectLocalizationResults()); CheckForEvents(results); LocalizationResults localizationResults = new LocalizationResults { Results = results.ToArray() }; m_IsLocalizing = false; OnLocalizationResult?.Invoke(localizationResults); return localizationResults; } public async Task> CreateLocalizationTasks(ICameraData cameraData) { List tasks = new List(); foreach (ILocalizationMethod localizationMethod in m_ConfiguredLocalizationMethods) { // Create new localization task tasks.Add(CreateNewLocalizationTask(localizationMethod, cameraData)); } return tasks; } public async Task LocalizeAllMethods(ICameraData cameraData) { List results = new List(); List tasks = await CreateLocalizationTasks(cameraData); await Task.WhenAll(tasks.Select(t => t.LocalizationMethodTask)); foreach (Task t in tasks.Select(t => t.LocalizationMethodTask)) { if (t.Status != TaskStatus.RanToCompletion) continue; results.Add(t.Result); t.Dispose(); } return new LocalizationResults { Results = results.ToArray() }; } private void StartNewLocalizationTask(ILocalizationMethod localizationMethod, ICameraData cameraData) { CancellationTokenSource cts = new CancellationTokenSource(); Task localizationMethodTask = localizationMethod.Localize(cameraData, cts.Token); LocalizationTask task = new LocalizationTask(localizationMethodTask, cts); m_RunningLocalizationTasks.Add(localizationMethod, task); } private LocalizationTask CreateNewLocalizationTask(ILocalizationMethod localizationMethod, ICameraData cameraData) { CancellationTokenSource cts = new CancellationTokenSource(); Task localizationMethodTask = localizationMethod.Localize(cameraData, cts.Token); LocalizationTask task = new LocalizationTask(localizationMethodTask, cts); return task; } private ILocalizationResult[] CollectLocalizationResults() { // Combine all currently finished results List resultList = new List(); foreach (ILocalizationMethod localizationMethod in m_ConfiguredLocalizationMethods) { if (m_RunningLocalizationTasks.TryGetValue(localizationMethod, out LocalizationTask task)) { if (task.LocalizationMethodTask.IsCompleted) { resultList.Add(task.LocalizationMethodTask.Result); m_RunningLocalizationTasks.Remove(localizationMethod); task.LocalizationMethodTask.Dispose(); } } } return resultList.ToArray(); } private void CleanUpLocalizationTasks() { // remove cancelled tasks foreach (ILocalizationMethod localizationMethod in m_ConfiguredLocalizationMethods) { if (m_RunningLocalizationTasks.TryGetValue(localizationMethod, out LocalizationTask task)) { if (task.LocalizationMethodTask.IsCanceled) { m_RunningLocalizationTasks.Remove(localizationMethod); } } } } // Event firing logic private void CheckForEvents(List results) { int[] ids = results.Where(r => r.Success).Select(r => r.MapId).ToArray(); if (ids.Length > 0) { if (!m_HasLocalizedSuccessfully) { OnFirstSuccessfulLocalization?.Invoke(); } OnSuccessfulLocalizations?.Invoke(ids); m_HasLocalizedSuccessfully = true; } else { OnFailedLocalizations?.Invoke(); } } private async Task StopRunningLocalizationTasks() { if (m_RunningLocalizationTasks is not { Count: > 0 }) return; List> tasks = new List>(); foreach (LocalizationTask localizationTask in m_RunningLocalizationTasks.Values) { localizationTask.CancellationTokenSource.Cancel(); tasks.Add(localizationTask.LocalizationMethodTask); } // Wait for task to finish await Task.WhenAll(tasks); // Clean up tasks m_RunningLocalizationTasks.Clear(); } public async Task StopLocalizationForMethod(ILocalizationMethod localizationMethod) { if (m_RunningLocalizationTasks is not { Count: > 0 }) return; if (m_RunningLocalizationTasks.TryGetValue(localizationMethod, out LocalizationTask task)) { task.CancellationTokenSource.Cancel(); await task.LocalizationMethodTask; m_RunningLocalizationTasks.Remove(localizationMethod); } } public bool TryGetLocalizationTask(ILocalizationMethod localizationMethod, out LocalizationTask task) { return m_RunningLocalizationTasks.TryGetValue(localizationMethod, out task); } public async Task StopAndCleanUp() { // Cancel all running tasks await StopRunningLocalizationTasks(); // Clean up methods await Task.WhenAll(m_ConfiguredLocalizationMethods.Select(method => method.StopAndCleanUp())); m_ConfiguredLocalizationMethods.Clear(); m_HasLocalizedSuccessfully = false; } } public struct LocalizerConfigurationResult : ILocalizerConfigurationResult { public bool Success { get; set; } } public struct DefaultLocalizerConfiguration : ILocalizerConfiguration { public Dictionary ConfigurationsToAdd { get; set; } public Dictionary ConfigurationsToRemove { get; set; } public bool StopRunningTasks { get; set; } } public struct DefaultLocalizationMethodConfiguration : ILocalizationMethodConfiguration { public XRMap[] MapsToAdd { get; set; } public XRMap[] MapsToRemove { get; set; } public SolverType? SolverType { get; set; } [ObsoleteAttribute("PriorNNCount is obsolete. Use PriorNNCountMin/Max instead.", false)] public int? PriorNNCount { get; set; } public int? PriorNNCountMin { get; set; } public int? PriorNNCountMax { get; set; } public Vector3? PriorScale { get; set; } public float? PriorRadius { get; set; } } }