using CartographerSharp.Mapping;
using Microsoft.AspNetCore.SignalR;
using RobotNet10.RobotApp.Client.Shared.SLAM;
using RobotNet10.RobotApp.Devices;
using RobotNet10.RobotApp.Hubs;
using RobotNet10.RobotApp.Shared;
using RobotNet10.RobotApp.SLAM.Cartographer.Helpers;
using RobotNet10.Shared.Geometry;
using RobotNet10.Shared.Localization;
using RobotNet10.Shared.Numbers;
namespace RobotNet10.RobotApp.SLAM.Cartographer;
public partial class CartographerService
{
#region Map Listing and Info
///
/// Liệt kê các maps có sẵn trong thư mục maps
///
public IReadOnlyList ListMaps()
{
var mapsDirectory = Path.GetFullPath(_config.MapStorage.Directory);
if (!Directory.Exists(mapsDirectory))
return [];
try
{
var maps = new List();
var mapDirectories = Directory.GetDirectories(mapsDirectory);
foreach (var mapDir in mapDirectories)
{
try
{
var metadataPath = MapPathHelper.ResolveMetadataPath(mapDir);
if (metadataPath != null)
{
var jsonContent = File.ReadAllText(metadataPath);
var metadata = System.Text.Json.JsonSerializer.Deserialize(jsonContent);
if (metadata != null)
{
metadata = MapNameHelper.EnsureMapSize(metadata);
maps.Add(metadata);
}
}
else
{
// Create basic metadata from directory name
var mapName = Path.GetFileName(mapDir);
maps.Add(new MapInfo
{
Name = mapName,
FolderPath = mapDir,
CreatedDate = Directory.GetCreationTime(mapDir),
Resolution = _config.MapStorage.OccupancyGridResolution
});
}
}
catch (Exception ex)
{
_logger.LogWarning(ex, "CartographerService: Failed to read metadata for map in: {MapDir}", mapDir);
}
}
return maps;
}
catch (Exception ex)
{
_logger.LogError(ex, "CartographerService: Failed to list maps");
return [];
}
}
///
/// Lấy thông tin chi tiết của một map theo tên
///
public MapInfo? GetMapInfo(string mapName)
{
try
{
var mapPath = MapPathHelper.GetMapPath(_config.MapStorage.Directory, mapName);
if (!Directory.Exists(mapPath))
{
_logger.LogWarning("CartographerService: Map directory not found: {MapPath}", mapPath);
return null;
}
var metadataPath = MapPathHelper.ResolveMetadataPath(mapPath);
if (metadataPath == null)
{
// Create basic metadata from directory name
var sanitizedMapName = MapNameHelper.Sanitize(mapName);
return new MapInfo
{
Name = sanitizedMapName,
FolderPath = mapPath,
CreatedDate = Directory.GetCreationTime(mapPath),
Resolution = _config.MapStorage.OccupancyGridResolution
};
}
var jsonContent = File.ReadAllText(metadataPath);
var metadata = System.Text.Json.JsonSerializer.Deserialize(jsonContent);
if (metadata == null)
return null;
metadata = MapNameHelper.EnsureMapSize(metadata);
return metadata;
}
catch (Exception ex)
{
_logger.LogError(ex, "CartographerService: Failed to get map info: {MapName}", mapName);
return null;
}
}
///
/// Lấy trạng thái xử lý của map
///
/// Tên map
/// True nếu map đang được xử lý, false nếu không
public bool GetMapProcessingStatus(string mapName)
{
return _mapProcessingStatus.TryGetValue(mapName, out var isProcessing) && isProcessing;
}
#endregion
#region Map Operations
///
/// Lấy đường dẫn đến file ảnh PNG của map
///
public string? GetMapImagePath(string mapName)
{
try
{
var mapPath = MapPathHelper.GetMapPath(_config.MapStorage.Directory, mapName);
if (!Directory.Exists(mapPath))
{
_logger.LogWarning("CartographerService: Map directory not found: {MapPath}", mapPath);
return null;
}
var imagePath = MapPathHelper.ResolveImagePath(mapPath);
if (imagePath != null)
return imagePath;
_logger.LogWarning("CartographerService: Map image not found for: {MapName}", mapName);
return null;
}
catch (Exception ex)
{
_logger.LogError(ex, "CartographerService: Failed to get map image path: {MapName}", mapName);
return null;
}
}
///
/// Xóa map folder
///
public Task DeleteMapAsync(string mapName, CancellationToken cancellationToken = default)
{
try
{
var mapPath = MapPathHelper.GetMapPath(_config.MapStorage.Directory, mapName);
if (!Directory.Exists(mapPath))
{
_logger.LogWarning("CartographerService: Map directory not found: {MapPath}", mapPath);
return Task.FromResult(false);
}
Directory.Delete(mapPath, recursive: true);
return Task.FromResult(true);
}
catch (Exception ex)
{
_logger.LogError(ex, "CartographerService: Failed to delete map: {MapName}", mapName);
return Task.FromResult(false);
}
}
///
/// Transform map để chọn lại gốc tọa độ.
/// Kiểm tra nếu map đang được xử lý thì return false.
/// Nếu không, bắt đầu xử lý trên thread riêng và return true ngay lập tức.
///
public Task TransformMapOriginAsync(string mapName, Pose newOrigin, CancellationToken cancellationToken = default)
{
// Check if map is already being processed
if (_mapProcessingStatus.TryGetValue(mapName, out var isProcessing) && isProcessing)
{
_logger.LogWarning("CartographerService: Map {MapName} is already being processed", mapName);
return Task.FromResult(false);
}
// Set processing status to true
_mapProcessingStatus[mapName] = true;
// Notify clients that processing has started
_ = _hubContext.Clients.Group(mapName).SendAsync("OnMapProcessingChanged", mapName, true, cancellationToken);
// Start processing on a separate thread
_ = Task.Run(async () =>
{
try
{
await TransformMapOriginInternalAsync(mapName, newOrigin, cancellationToken);
}
finally
{
// Set processing status to false
_mapProcessingStatus[mapName] = false;
// Notify clients that processing has completed
await _hubContext.Clients.Group(mapName).SendAsync("OnMapProcessingChanged", mapName, false, cancellationToken);
}
}, cancellationToken);
// Return true immediately to indicate processing has started
return Task.FromResult(true);
}
///
/// Internal method that performs the actual map transformation.
/// Based on xloc.cc ChangeMapOrigin logic: updates TransformToMap in the PoseGraph proto
/// while preserving all other data (trajectories, submaps, options, etc.).
///
private async Task TransformMapOriginInternalAsync(string mapName, Pose newOrigin, CancellationToken cancellationToken = default)
{
try
{
var mapPath = MapPathHelper.GetMapPath(_config.MapStorage.Directory, mapName);
var metadataPath = MapPathHelper.ResolveMetadataPath(mapPath);
var pbstreamPath = MapPathHelper.ResolvePbstreamPath(mapPath);
if (metadataPath == null)
{
_logger.LogError("TransformMapOrigin: Metadata file not found in: {MapPath}", mapPath);
return false;
}
if (pbstreamPath == null)
{
_logger.LogError("TransformMapOrigin: Pbstream file not found in: {MapPath}", mapPath);
return false;
}
// Load metadata
var jsonContent = await File.ReadAllTextAsync(metadataPath, cancellationToken);
var metadata = System.Text.Json.JsonSerializer.Deserialize(jsonContent);
if (metadata == null)
{
_logger.LogError("TransformMapOrigin: Failed to deserialize metadata");
return false;
}
// === Update pbstream file using shared helper ===
// This preserves all original data (AllTrajectoryBuilderOptions, submaps, nodes, etc.)
PbstreamTransformHelper.TransformPbstreamOrigin(pbstreamPath, newOrigin, _logger);
// Reload transformed map to regenerate occupancy grid and image files
var transformedLoadResult = LoadMapFromDirectory(mapName);
if (transformedLoadResult == null)
{
_logger.LogError("TransformMapOrigin: Failed to load transformed map for file regeneration");
return false;
}
MapBuilder transformedMapBuilder;
try
{
transformedMapBuilder = MapBuilderHelper.CreateMapBuilderFromLoadResult(
transformedLoadResult,
_config,
_logger,
loadFrozenState: false);
}
catch (Exception ex)
{
_logger.LogError(ex, "TransformMapOrigin: Failed to create MapBuilder from transformed pbstream");
return false;
}
try
{
// Use MapSaveProcessor to regenerate occupancy grid files and update metadata
if (_mapSaveProcessor == null)
{
_logger.LogError("TransformMapOrigin: MapSaveProcessor is not available");
return false;
}
await _mapSaveProcessor.SaveMapMetadataAsync(
mapName,
transformedMapBuilder,
mapPath,
cancellationToken);
return true;
}
finally
{
transformedMapBuilder.Dispose();
}
}
catch (Exception ex)
{
_logger.LogError(ex, "TransformMapOrigin: Failed for map {MapName}", mapName);
return false;
}
}
///
/// Rerender map image files (PNG, JPG, PGM) with custom OccupancyGridConfiguration.
/// Kiểm tra nếu map đang được xử lý thì return false.
/// Nếu không, bắt đầu xử lý trên thread riêng và return true ngay lập tức.
///
public Task RerenderMapWithConfigAsync(string mapName, OccupancyGridConfigurationDto configDto, CancellationToken cancellationToken = default)
{
// Check if map is already being processed
if (_mapProcessingStatus.TryGetValue(mapName, out var isProcessing) && isProcessing)
{
_logger.LogWarning("CartographerService: Map {MapName} is already being processed", mapName);
return Task.FromResult(false);
}
// Set processing status to true
_mapProcessingStatus[mapName] = true;
// Notify clients that processing has started
_ = _hubContext.Clients.Group(mapName).SendAsync("OnMapProcessingChanged", mapName, true, cancellationToken);
// Start processing on a separate thread
_ = Task.Run(async () =>
{
try
{
await RerenderMapWithConfigInternalAsync(mapName, configDto, cancellationToken);
}
finally
{
// Set processing status to false
_mapProcessingStatus[mapName] = false;
// Notify clients that processing has completed
await _hubContext.Clients.Group(mapName).SendAsync("OnMapProcessingChanged", mapName, false, cancellationToken);
}
}, cancellationToken);
// Return true immediately to indicate processing has started
return Task.FromResult(true);
}
///
/// Internal method that performs the actual map rerendering.
/// Loads pbstream, generates occupancy grid with custom config, and saves image files.
///
private async Task RerenderMapWithConfigInternalAsync(string mapName, OccupancyGridConfigurationDto configDto, CancellationToken cancellationToken = default)
{
try
{
var mapPath = MapPathHelper.GetMapPath(_config.MapStorage.Directory, mapName);
var pbstreamPath = MapPathHelper.ResolvePbstreamPath(mapPath);
if (pbstreamPath == null)
{
_logger.LogError("RerenderMapWithConfig: Pbstream file not found in: {MapPath}", mapPath);
return false;
}
// Load map from pbstream
var loadResult = LoadMapFromDirectory(mapName);
if (loadResult == null)
{
_logger.LogError("RerenderMapWithConfig: Failed to load map from directory");
return false;
}
MapBuilder mapBuilder;
try
{
mapBuilder = MapBuilderHelper.CreateMapBuilderFromLoadResult(
loadResult,
_config,
_logger,
loadFrozenState: false);
}
catch (Exception ex)
{
_logger.LogError(ex, "RerenderMapWithConfig: Failed to create MapBuilder from pbstream");
return false;
}
try
{
// === DEBUG: Log PoseGraph state after loading ===
var poseGraph = mapBuilder.PoseGraph;
var transformToMap = poseGraph.GetTransformToMap();
_logger.LogWarning(
"RerenderMapWithConfig DEBUG: TransformToMap = T[{TX:F4}, {TY:F4}, {TZ:F4}], R[{RW:F4}, {RX:F4}, {RY:F4}, {RZ:F4}]",
transformToMap.Translation.X, transformToMap.Translation.Y, transformToMap.Translation.Z,
transformToMap.Rotation.W, transformToMap.Rotation.X, transformToMap.Rotation.Y, transformToMap.Rotation.Z);
var allSubmapData = poseGraph.GetAllSubmapData();
_logger.LogWarning("RerenderMapWithConfig DEBUG: Total submaps = {Count}", allSubmapData?.Count ?? 0);
// Log LocalToGlobalTransform for each trajectory
try
{
var trajectoryStates = poseGraph.GetTrajectoryStates();
_logger.LogWarning("RerenderMapWithConfig DEBUG: Found {Count} trajectories", trajectoryStates?.Count ?? 0);
if (trajectoryStates != null)
{
foreach (var trajState in trajectoryStates)
{
var trajId = trajState.Key;
var localToGlobal = poseGraph.GetLocalToGlobalTransform(trajId);
_logger.LogWarning(
"RerenderMapWithConfig DEBUG: Trajectory[{TrajId}] State={State}, LocalToGlobalTransform = " +
"T[{TX:F4},{TY:F4},{TZ:F4}], R[{RW:F4},{RX:F4},{RY:F4},{RZ:F4}]",
trajId, trajState.Value,
localToGlobal.Translation.X, localToGlobal.Translation.Y, localToGlobal.Translation.Z,
localToGlobal.Rotation.W, localToGlobal.Rotation.X, localToGlobal.Rotation.Y, localToGlobal.Rotation.Z);
}
}
}
catch (Exception ex)
{
_logger.LogWarning("RerenderMapWithConfig DEBUG: Failed to get trajectory states: {Error}", ex.Message);
}
int submapIdx = 0;
foreach (var idDataRef in allSubmapData)
{
var submapId = idDataRef.Id;
var submapData = idDataRef.Data;
var submapPose = submapData.Pose;
// Calculate globalPose as OccupancyGridGenerator does
var transformToMapInverse = transformToMap.Inverse();
var globalPose = transformToMapInverse * submapPose;
if (submapData.Submap is CartographerSharp.Mapping.D2D.Submap2D submap2D)
{
var localPose = submap2D.LocalPose;
var grid = submap2D.Grid;
var gridInfo = grid != null
? $"cells={grid.Limits.CellLimits.NumXCells}x{grid.Limits.CellLimits.NumYCells}, res={grid.Limits.Resolution:F4}"
: "null";
// Calculate yaw from quaternion components
static double GetYawDegreesFromComponents(double qw, double qx, double qy, double qz)
{
var siny_cosp = 2.0 * (qw * qz + qx * qy);
var cosy_cosp = 1.0 - 2.0 * (qy * qy + qz * qz);
return Math.Atan2(siny_cosp, cosy_cosp) * 180.0 / Math.PI;
}
var localYaw = GetYawDegreesFromComponents(
localPose.Rotation.W, localPose.Rotation.X, localPose.Rotation.Y, localPose.Rotation.Z);
var submapPoseYaw = GetYawDegreesFromComponents(
submapPose.Rotation.W, submapPose.Rotation.X, submapPose.Rotation.Y, submapPose.Rotation.Z);
var globalPoseYaw = GetYawDegreesFromComponents(
globalPose.Rotation.W, globalPose.Rotation.X, globalPose.Rotation.Y, globalPose.Rotation.Z);
_logger.LogWarning(
"RerenderMapWithConfig DEBUG: Submap[{Idx}] Id={TrajId}:{SubIdx}, " +
"LocalPose=T[{LPX:F4},{LPY:F4}] Yaw={LPYaw:F2}°, " +
"SubmapData.Pose=T[{SPX:F4},{SPY:F4}] Yaw={SPYaw:F2}°, " +
"GlobalPose=T[{GPX:F4},{GPY:F4}] Yaw={GPYaw:F2}°, " +
"Grid={GridInfo}",
submapIdx, submapId.TrajectoryId, submapId.SubmapIndex,
localPose.Translation.X, localPose.Translation.Y, localYaw,
submapPose.Translation.X, submapPose.Translation.Y, submapPoseYaw,
globalPose.Translation.X, globalPose.Translation.Y, globalPoseYaw,
gridInfo);
}
submapIdx++;
}
// === END DEBUG ===
// Convert DTO to OccupancyGridConfiguration
var config = ConvertDtoToConfig(configDto);
// Generate occupancy grid with custom config
var resolution = _config.MapStorage.OccupancyGridResolution;
var padding = _config.MapStorage.MapPadding;
var occupancyGrid = OccupancyGridGenerator.Generate(mapBuilder, resolution, padding, _logger, config);
if (occupancyGrid == null)
{
_logger.LogError("RerenderMapWithConfig: Failed to generate occupancy grid");
return false;
}
// === DEBUG: Log OccupancyGrid result ===
_logger.LogWarning(
"RerenderMapWithConfig DEBUG: OccupancyGrid size={W}x{H}, resolution={Res:F4}, " +
"Origin=T[{OX:F4},{OY:F4}]",
occupancyGrid.Width, occupancyGrid.Height, occupancyGrid.Resolution,
occupancyGrid.Origin.Position.X, occupancyGrid.Origin.Position.Y);
// === END DEBUG ===
// Save all image formats (PGM, PNG, JPG, YAML)
await OccupancyGridFileHelper.SaveAllFormatsAsync(occupancyGrid, mapPath, cancellationToken, _logger);
_logger.LogInformation("RerenderMapWithConfig: Successfully rerendered map {MapName} with custom config", mapName);
return true;
}
finally
{
mapBuilder.Dispose();
}
}
catch (Exception ex)
{
_logger.LogError(ex, "RerenderMapWithConfig: Failed for map {MapName}", mapName);
return false;
}
}
///
/// Convert OccupancyGridConfigurationDto to OccupancyGridConfiguration
///
private static OccupancyGridConfiguration ConvertDtoToConfig(OccupancyGridConfigurationDto dto)
{
return new OccupancyGridConfiguration
{
MergeStrategy = dto.MergeStrategy switch
{
SubmapMergeStrategyDto.PorterDuff => SubmapMergeStrategy.PorterDuff,
SubmapMergeStrategyDto.LogOddsSum => SubmapMergeStrategy.LogOddsSum,
SubmapMergeStrategyDto.MaxProbability => SubmapMergeStrategy.MaxProbability,
_ => SubmapMergeStrategy.LogOddsSum
},
LogOddsClamp = dto.LogOddsClamp,
UseLogOddsAverage = dto.UseLogOddsAverage,
FreeSpaceThreshold = dto.FreeSpaceThreshold,
OccupiedSpaceThreshold = dto.OccupiedSpaceThreshold,
UseBinaryOutput = dto.UseBinaryOutput,
EnableWallThinning = dto.EnableWallThinning,
WallThinningIterations = dto.WallThinningIterations,
MinWallThicknessPixels = dto.MinWallThicknessPixels,
AmbiguousCellValue = dto.AmbiguousCellValue,
AmbiguousRangeLower = dto.AmbiguousRangeLower,
AmbiguousRangeUpper = dto.AmbiguousRangeUpper,
EnableMedianFilter = dto.EnableMedianFilter,
MedianFilterKernelSize = dto.MedianFilterKernelSize
};
}
#endregion
#region Map Loading
///
/// Load map files directly from map directory
///
private MapCartographerLoadResult? LoadMapFromDirectory(string mapName)
{
try
{
var mapPath = MapPathHelper.GetMapPath(_config.MapStorage.Directory, mapName);
if (!Directory.Exists(mapPath))
{
_logger.LogError("CartographerService: Map directory not found: {MapPath}", mapPath);
return null;
}
// Find pbstream file (map.pbstream or legacy)
var pbstreamPath = MapPathHelper.ResolvePbstreamPath(mapPath);
if (pbstreamPath == null)
{
_logger.LogError("CartographerService: No pbstream file found in: {MapPath}", mapPath);
return null;
}
if (Path.GetFileName(pbstreamPath) != "map.pbstream")
{
_logger.LogWarning("CartographerService: Using legacy pbstream file: {Path}", pbstreamPath);
}
// Load metadata to get saved config (map.json)
MapCartographerConfigSnapshot? savedConfig = null;
var metadataPath = MapPathHelper.ResolveMetadataPath(mapPath);
if (metadataPath != null)
{
if (Path.GetFileName(metadataPath) == "metadata.json")
{
_logger.LogWarning("CartographerService: Using legacy metadata.json file");
}
try
{
var jsonContent = File.ReadAllText(metadataPath);
var metadata = System.Text.Json.JsonSerializer.Deserialize(jsonContent);
savedConfig = metadata?.MapConfig;
}
catch (Exception ex)
{
_logger.LogWarning(ex, "CartographerService: Failed to load metadata, will use current config");
}
}
return new MapCartographerLoadResult
{
PbstreamPath = pbstreamPath,
MapPath = mapPath,
SavedConfig = savedConfig
};
}
catch (Exception ex)
{
_logger.LogError(ex, "CartographerService: Failed to load map: {MapName}", mapName);
return null;
}
}
private bool MapExists(string mapName)
{
try
{
var mapPath = MapPathHelper.GetMapPath(_config.MapStorage.Directory, mapName);
return Directory.Exists(mapPath);
}
catch
{
return false;
}
}
#endregion
#region Wall Alignment
///
/// Calculate wall-aligned pose by detecting walls from lidar scans.
/// This method should be called AFTER sensors are resumed to ensure fresh lidar data.
/// The aligned pose should be set on LocalTrajectoryBuilder2D using SetInitialPose()
/// BEFORE resuming sensors for scan mapping.
///
/// Wall-aligned pose if successful, null otherwise
private Pose CalculateWallAlignedPoseAsync()
{
_logger.LogInformation("CartographerService: Starting wall alignment calculation");
// Step 1: Get all lidar devices
var allDevices = _deviceProvider.GetDevicesByType(RobotNet10.RobotApp.Client.Shared.Devices.DeviceType.Lidar);
var lidarList = allDevices.OfType().ToList();
if (lidarList.Count == 0)
{
_logger.LogWarning("CartographerService: No lidar devices found for wall alignment");
return new Pose();
}
_logger.LogInformation("CartographerService: Found {Count} lidar device(s)", lidarList.Count);
// Step 2: Collect point clouds from all lidars and transform to base_link frame
var allPoints = new List();
foreach (var lidar in lidarList)
{
try
{
var deviceId = (lidar as DeviceBase)?.DeviceId;
if (string.IsNullOrEmpty(deviceId))
{
_logger.LogWarning("CartographerService: Lidar device has no ID, skipping");
continue;
}
var laserScan = lidar.CurrentMeasurementData;
if (!laserScan.HasValue)
{
_logger.LogWarning("CartographerService: No scan data from lidar {DeviceId}", deviceId);
continue;
}
// Get sensor configuration and transform
var sensorConfig = _config.Sensors.Lidars
.FirstOrDefault(cfg => cfg.DeviceId == deviceId && cfg.Enabled);
if (sensorConfig == null)
{
_logger.LogWarning("CartographerService: No configuration found for lidar {DeviceId}", deviceId);
continue;
}
var scan = laserScan.Value;
// Convert LaserScan to point cloud in base_link frame
var (baseLink, _, _, _, _) = SensorDataTransformHelper.ToTimedPointCloudDataBaseAndSensorFrame(
scan.Header.Stamp,
scan,
sensorConfig.Transform,
sensorConfig.AngleMin,
sensorConfig.AngleMax
);
// Extract points in base_link frame (use .Ranges instead of .Points)
int pointsAdded = 0;
foreach (var point in baseLink.Ranges)
{
allPoints.Add(point.Position);
pointsAdded++;
}
_logger.LogInformation(
"CartographerService: Collected {Count} points from lidar {DeviceId}",
pointsAdded, deviceId);
}
catch (Exception ex)
{
_logger.LogError(ex, "CartographerService: Error processing lidar data for wall alignment");
}
}
if (allPoints.Count == 0)
{
_logger.LogWarning("CartographerService: No points collected for wall alignment");
return new Pose();
}
_logger.LogInformation("CartographerService: Total points collected: {Count}", allPoints.Count);
// Step 3: Detect wall and calculate compensation angle
var compensationAngle = WallAlignmentHelper.DetectWallAndCalculateCompensation(allPoints, _logger);
if (!compensationAngle.HasValue)
{
_logger.LogWarning("CartographerService: Could not detect wall for alignment");
return new Pose();
}
// Step 4: Calculate wall-aligned pose
// Start with identity pose (robot at origin, facing along X-axis)
// Then apply the compensation angle to align with detected wall
var alignedOrientation = WallAlignmentHelper.CreateQuaternionFromYaw(compensationAngle.Value);
var alignedPose = new Pose
{
Position = Vector3.Zero, // Start at origin
Orientation = alignedOrientation // Oriented to align map with wall
};
_logger.LogInformation(
"CartographerService: Calculated wall-aligned pose - " +
"Position: [{X:F3}, {Y:F3}, {Z:F3}], Yaw: {Yaw:F3}rad ({YawDeg:F1}°), " +
"Compensation angle: {CompAngle:F3}rad ({CompAngleDeg:F1}°)",
alignedPose.Position.X,
alignedPose.Position.Y,
alignedPose.Position.Z,
WallAlignmentHelper.GetYawFromQuaternion(alignedPose.Orientation),
WallAlignmentHelper.GetYawFromQuaternion(alignedPose.Orientation) * 180.0 / Math.PI,
compensationAngle.Value,
compensationAngle.Value * 180.0 / Math.PI);
return alignedPose;
}
#endregion
}