using CartographerSharp.Mapping; using CartographerSharp.Models.Mapping; using RobotNet10.RobotApp.SLAM.Cartographer; using RobotNet10.Shared.Geometry; namespace RobotNet10.RobotApp.SLAM.Cartographer.Helpers; /// /// Helper class for creating and configuring MapBuilder instances /// Factory methods for MapBuilder and related configurations /// Extension methods are now in MapBuilderExtensions class /// public static class MapBuilderHelper { /// /// Create a new MapBuilder with configuration /// public static MapBuilder CreateMapBuilder(CartographerConfiguration config) { ArgumentNullException.ThrowIfNull(config); var use2D = config.MapBuilder.UseTrajectoryBuilder2D ?? config.TrajectoryBuilder.Use2D; var use3D = config.MapBuilder.UseTrajectoryBuilder3D ?? !config.TrajectoryBuilder.Use2D; var poseGraphOptions = TrajectoryBuilderOptionsFactory.CreatePoseGraphOptions(config.MapBuilder); var mapBuilderOptions = new MapBuilderOptions( useTrajectoryBuilder2D: use2D, useTrajectoryBuilder3D: use3D, numBackgroundThreads: config.MapBuilder.NumBackgroundThreads, poseGraphOptions: poseGraphOptions, collateByTrajectory: config.MapBuilder.CollateByTrajectory); return new MapBuilder(mapBuilderOptions); } /// /// Create MapBuilder from MapLoadResult (pbstream path and saved config). /// Uses saved config if available, otherwise uses current config. /// Then loads state from pbstream file with frozen trajectories for localization. /// public static MapBuilder CreateMapBuilderFromLoadResult( MapCartographerLoadResult loadResult, CartographerConfiguration currentConfig, ILogger? logger = null, bool loadFrozenState = true) { ArgumentNullException.ThrowIfNull(loadResult); ArgumentNullException.ThrowIfNull(currentConfig); if (string.IsNullOrEmpty(loadResult.PbstreamPath)) { throw new ArgumentException("PbstreamPath cannot be null or empty", nameof(loadResult)); } if (!File.Exists(loadResult.PbstreamPath)) { throw new FileNotFoundException("Pbstream file not found", loadResult.PbstreamPath); } // Get MapBuilder options from saved config or current config bool use2D; bool use3D; int numBackgroundThreads; PoseGraphOptions poseGraphOptions; if (loadResult.SavedConfig != null) { // Use saved config use2D = loadResult.SavedConfig.MapBuilder.UseTrajectoryBuilder2D ?? loadResult.SavedConfig.Use2D; use3D = loadResult.SavedConfig.MapBuilder.UseTrajectoryBuilder3D ?? !loadResult.SavedConfig.Use2D; numBackgroundThreads = loadResult.SavedConfig.MapBuilder.NumBackgroundThreads; poseGraphOptions = CreatePoseGraphOptionsFromSnapshot(loadResult.SavedConfig); // Warn if config mismatch var currentUse2D = currentConfig.MapBuilder.UseTrajectoryBuilder2D ?? currentConfig.TrajectoryBuilder.Use2D; if (currentUse2D != use2D) { logger?.LogWarning( "MapBuilderHelper: Map config mismatch detected. Using saved config (2D={Saved2D}) instead of current config (2D={Current2D}).", use2D, currentUse2D); } } else { // Use current config use2D = currentConfig.MapBuilder.UseTrajectoryBuilder2D ?? currentConfig.TrajectoryBuilder.Use2D; use3D = currentConfig.MapBuilder.UseTrajectoryBuilder3D ?? !currentConfig.TrajectoryBuilder.Use2D; numBackgroundThreads = currentConfig.MapBuilder.NumBackgroundThreads; poseGraphOptions = TrajectoryBuilderOptionsFactory.CreatePoseGraphOptions(currentConfig.MapBuilder); } var mapBuilderOptions = new MapBuilderOptions( useTrajectoryBuilder2D: use2D, useTrajectoryBuilder3D: use3D, numBackgroundThreads: numBackgroundThreads, poseGraphOptions: poseGraphOptions ); var mapBuilder = new MapBuilder(mapBuilderOptions); // Load state from pbstream file // loadFrozenState: true freezes trajectories in the loaded map for localization // This prevents modifying the map during localization (as per xloc reference implementation) _ = mapBuilder.LoadStateFromFile(loadResult.PbstreamPath, loadFrozenState: loadFrozenState); logger?.LogInformation( "MapBuilderHelper: Created MapBuilder from pbstream file: {PbstreamPath}, loadFrozenState={LoadFrozenState}", loadResult.PbstreamPath, loadFrozenState); return mapBuilder; } /// /// Creates PoseGraphOptions from saved config snapshot /// private static PoseGraphOptions CreatePoseGraphOptionsFromSnapshot(MapCartographerConfigSnapshot savedConfig) { // Create temporary MapBuilderConfiguration from snapshot var tempMapBuilderConfig = new MapBuilderConfiguration { UseTrajectoryBuilder2D = savedConfig.MapBuilder.UseTrajectoryBuilder2D, UseTrajectoryBuilder3D = savedConfig.MapBuilder.UseTrajectoryBuilder3D, NumBackgroundThreads = savedConfig.MapBuilder.NumBackgroundThreads, OptimizeEveryNNodes = savedConfig.MapBuilder.OptimizeEveryNNodes, MatcherTranslationWeight = savedConfig.MapBuilder.MatcherTranslationWeight, MatcherRotationWeight = savedConfig.MapBuilder.MatcherRotationWeight, MaxNumFinalIterations = savedConfig.MapBuilder.MaxNumFinalIterations, GlobalSamplingRatio = savedConfig.MapBuilder.GlobalSamplingRatio, LogResidualHistograms = savedConfig.MapBuilder.LogResidualHistograms, GlobalConstraintSearchAfterNSeconds = savedConfig.MapBuilder.GlobalConstraintSearchAfterNSeconds, EnableSingleTrajectoryLoopClosure = savedConfig.MapBuilder.EnableSingleTrajectoryLoopClosure, SingleTrajectoryLoopClosureDistanceThreshold = savedConfig.MapBuilder.SingleTrajectoryLoopClosureDistanceThreshold, PoseGraphOptimizationProblemOptions = new OptimizationProblemOptions { HuberScale = savedConfig.MapBuilder.OptimizationProblemOptions.HuberScale, AccelerationWeight = savedConfig.MapBuilder.OptimizationProblemOptions.AccelerationWeight, RotationWeight = savedConfig.MapBuilder.OptimizationProblemOptions.RotationWeight, LocalSlamPoseTranslationWeight = savedConfig.MapBuilder.OptimizationProblemOptions.LocalSlamPoseTranslationWeight, LocalSlamPoseRotationWeight = savedConfig.MapBuilder.OptimizationProblemOptions.LocalSlamPoseRotationWeight, OdometryTranslationWeight = savedConfig.MapBuilder.OptimizationProblemOptions.OdometryTranslationWeight, OdometryRotationWeight = savedConfig.MapBuilder.OptimizationProblemOptions.OdometryRotationWeight, FixedFramePoseTranslationWeight = savedConfig.MapBuilder.OptimizationProblemOptions.FixedFramePoseTranslationWeight, FixedFramePoseRotationWeight = savedConfig.MapBuilder.OptimizationProblemOptions.FixedFramePoseRotationWeight, FixedFramePoseUseTolerantLoss = savedConfig.MapBuilder.OptimizationProblemOptions.FixedFramePoseUseTolerantLoss, FixedFramePoseTolerantLossParamA = savedConfig.MapBuilder.OptimizationProblemOptions.FixedFramePoseTolerantLossParamA, FixedFramePoseTolerantLossParamB = savedConfig.MapBuilder.OptimizationProblemOptions.FixedFramePoseTolerantLossParamB, LogSolverSummary = savedConfig.MapBuilder.OptimizationProblemOptions.LogSolverSummary, MaxNumIterations = savedConfig.MapBuilder.OptimizationProblemOptions.MaxNumIterations } }; return TrajectoryBuilderOptionsFactory.CreatePoseGraphOptions(tempMapBuilderConfig); } /// /// Tạo config snapshot từ configuration hiện tại (để lưu vào map.json khi save). /// public static MapCartographerConfigSnapshot CreateConfigSnapshot(CartographerConfiguration config) { ArgumentNullException.ThrowIfNull(config); var use2D = config.MapBuilder.UseTrajectoryBuilder2D ?? config.TrajectoryBuilder.Use2D; return new MapCartographerConfigSnapshot { Use2D = use2D, MapBuilder = new MapCarographerBuilderConfigSnapshot { UseTrajectoryBuilder2D = config.MapBuilder.UseTrajectoryBuilder2D, UseTrajectoryBuilder3D = config.MapBuilder.UseTrajectoryBuilder3D, NumBackgroundThreads = config.MapBuilder.NumBackgroundThreads, OptimizeEveryNNodes = config.MapBuilder.OptimizeEveryNNodes, MatcherTranslationWeight = config.MapBuilder.MatcherTranslationWeight, MatcherRotationWeight = config.MapBuilder.MatcherRotationWeight, MaxNumFinalIterations = config.MapBuilder.MaxNumFinalIterations, GlobalSamplingRatio = config.MapBuilder.GlobalSamplingRatio, LogResidualHistograms = config.MapBuilder.LogResidualHistograms, GlobalConstraintSearchAfterNSeconds = config.MapBuilder.GlobalConstraintSearchAfterNSeconds, EnableSingleTrajectoryLoopClosure = config.MapBuilder.EnableSingleTrajectoryLoopClosure, SingleTrajectoryLoopClosureDistanceThreshold = config.MapBuilder.SingleTrajectoryLoopClosureDistanceThreshold, OptimizationProblemOptions = new OptimizationProblemOptionsSnapshot { HuberScale = config.MapBuilder.PoseGraphOptimizationProblemOptions.HuberScale, AccelerationWeight = config.MapBuilder.PoseGraphOptimizationProblemOptions.AccelerationWeight, RotationWeight = config.MapBuilder.PoseGraphOptimizationProblemOptions.RotationWeight, LocalSlamPoseTranslationWeight = config.MapBuilder.PoseGraphOptimizationProblemOptions.LocalSlamPoseTranslationWeight, LocalSlamPoseRotationWeight = config.MapBuilder.PoseGraphOptimizationProblemOptions.LocalSlamPoseRotationWeight, OdometryTranslationWeight = config.MapBuilder.PoseGraphOptimizationProblemOptions.OdometryTranslationWeight, OdometryRotationWeight = config.MapBuilder.PoseGraphOptimizationProblemOptions.OdometryRotationWeight, FixedFramePoseTranslationWeight = config.MapBuilder.PoseGraphOptimizationProblemOptions.FixedFramePoseTranslationWeight, FixedFramePoseRotationWeight = config.MapBuilder.PoseGraphOptimizationProblemOptions.FixedFramePoseRotationWeight, FixedFramePoseUseTolerantLoss = config.MapBuilder.PoseGraphOptimizationProblemOptions.FixedFramePoseUseTolerantLoss, FixedFramePoseTolerantLossParamA = config.MapBuilder.PoseGraphOptimizationProblemOptions.FixedFramePoseTolerantLossParamA, FixedFramePoseTolerantLossParamB = config.MapBuilder.PoseGraphOptimizationProblemOptions.FixedFramePoseTolerantLossParamB, LogSolverSummary = config.MapBuilder.PoseGraphOptimizationProblemOptions.LogSolverSummary, MaxNumIterations = config.MapBuilder.PoseGraphOptimizationProblemOptions.MaxNumIterations } }, TrajectoryBuilder = new TrajectoryBuilderConfigSnapshot { Use2D = config.TrajectoryBuilder.Use2D, MinRange = config.TrajectoryBuilder.MinRange, MaxRange = config.TrajectoryBuilder.MaxRange, MinZ = config.TrajectoryBuilder.MinZ, MaxZ = config.TrajectoryBuilder.MaxZ, MissingDataRayLength = config.TrajectoryBuilder.MissingDataRayLength, VoxelFilterSize = config.TrajectoryBuilder.VoxelFilterSize, NumAccumulatedRangeData = config.TrajectoryBuilder.NumAccumulatedRangeData, UseImuData = config.TrajectoryBuilder.UseImuData, UseOnlineCorrelativeScanMatching = config.TrajectoryBuilder.UseOnlineCorrelativeScanMatching } }; } }