using System; using System.Collections.Generic; using System.Linq; using System.Security.Cryptography; using System.Text; using System.Threading; using MultiWheelC.TrajectoryPlanning.Utils; namespace MultiWheelC.TrajectoryPlanning.Mapping; /// /// 规划地图的唯一公开创建入口。 /// /// 注意: /// - 应在规划服务生命周期内长期复用同一实例,才能命中输入和占据两级缓存。 /// - 本类只消费请求中的纯障碍物快照,不读取传感器、定位、UI 或系统时钟。 /// public sealed class PlanningMapFactory { private readonly EnvironmentMapBuilder _builder = new EnvironmentMapBuilder(); private readonly PlanningMapCache _cache = new PlanningMapCache(); private readonly object _createGate = new object(); private long _nextSnapshotId; /// /// 创建规划地图快照。 /// /// 参数: /// - request:完整建图请求;包含世界范围、栅格分辨率、障碍物来源和空图策略,不能为空。 /// /// 返回: /// - PlanningMapBuildResult:成功时包含不可变 PlanningGridMap、每个来源的投影结果和缓存命中类型; /// 失败时包含失败原因,不提供地图。 /// /// 注意: /// - 完全相同的输入返回同一个快照对象;版本变化但占据相同则复用底层栅格并生成新快照编号。 /// public PlanningMapBuildResult Create(PlanningMapRequest request) { return Create(request, PlanningOperationBudget.Unlimited(CancellationToken.None)); } /// 使用共享预算创建地图;取消或超时时不发布快照或缓存条目。 internal PlanningMapBuildResult Create(PlanningMapRequest request, PlanningOperationBudget budget) { if (budget == null) throw new ArgumentNullException(nameof(budget)); PlanningOperationStopReason stopReason = budget.GetStopReason(); if (stopReason != PlanningOperationStopReason.None) return PlanningMapBuildResult.Stopped(stopReason, null); bool enteredCreateGate = false; try { while (!Monitor.TryEnter(_createGate, 16)) { stopReason = budget.GetStopReason(); if (stopReason != PlanningOperationStopReason.None) return PlanningMapBuildResult.Stopped(stopReason, null); } enteredCreateGate = true; stopReason = budget.GetStopReason(); if (stopReason != PlanningOperationStopReason.None) return PlanningMapBuildResult.Stopped(stopReason, null); if (request == null || request.Bounds == null) return PlanningMapBuildResult.Failure("Planning map request and bounds are required.", null); if (request.ObstacleSources == null) return PlanningMapBuildResult.Failure("Obstacle source collection is required.", null); string requestKey = BuildRequestKey(request); stopReason = budget.GetStopReason(); if (stopReason != PlanningOperationStopReason.None) return PlanningMapBuildResult.Stopped(stopReason, null); if (_cache.TryGetInput(requestKey, out PlanningGridMap exactMap, out IReadOnlyList cachedResults)) { stopReason = budget.GetStopReason(); return stopReason == PlanningOperationStopReason.None ? PlanningMapBuildResult.Success(exactMap, cachedResults, PlanningMapCacheHit.Input) : PlanningMapBuildResult.Stopped(stopReason, cachedResults); } EnvironmentMapBuildResult environment = _builder.Build( new MapBuildRequest { Bounds = request.Bounds, ResolutionMm = request.ResolutionMm, ObstacleSources = request.ObstacleSources }, budget); if (!environment.Succeeded) return environment.StopReason == PlanningOperationStopReason.None ? PlanningMapBuildResult.Failure(environment.FailureReason, environment.SourceResults) : PlanningMapBuildResult.Stopped(environment.StopReason, environment.SourceResults); stopReason = budget.GetStopReason(); if (stopReason != PlanningOperationStopReason.None) return PlanningMapBuildResult.Stopped(stopReason, environment.SourceResults); string descriptor = BuildInputDescriptor(request, environment.SourceResults); string inputFingerprint = Sha256(descriptor); int applied = environment.SourceResults.Count(x => x.Status == ObstacleSourceStatus.Applied); bool ready = applied > 0 || request.AllowExplicitEmptyMap; string blockReason = ready ? string.Empty : "No source supplied obstacle geometry; set AllowExplicitEmptyMap only when an intentionally empty map is safe."; if (!PlanningMapAdapter.TryCreate(environment.Map, budget, out PlanningGridMap candidate, out stopReason)) return PlanningMapBuildResult.Stopped(stopReason, environment.SourceResults); if (!TryComputeOccupancyHash(candidate, budget, out string occupancyHash, out stopReason)) return PlanningMapBuildResult.Stopped(stopReason, environment.SourceResults); PlanningGridMap map; PlanningMapCacheHit cacheHit; if (_cache.TryGetOccupancy(occupancyHash, candidate, out PlanningGridMap canonical)) { map = canonical.WithMetadata(NextSnapshotId(), ready, blockReason, inputFingerprint, occupancyHash); cacheHit = PlanningMapCacheHit.Occupancy; } else { map = candidate.WithMetadata(NextSnapshotId(), ready, blockReason, inputFingerprint, occupancyHash); stopReason = budget.GetStopReason(); if (stopReason != PlanningOperationStopReason.None) return PlanningMapBuildResult.Stopped(stopReason, environment.SourceResults); _cache.AddOccupancy(occupancyHash, map); cacheHit = PlanningMapCacheHit.None; } stopReason = budget.GetStopReason(); if (stopReason != PlanningOperationStopReason.None) return PlanningMapBuildResult.Stopped(stopReason, environment.SourceResults); _cache.AddInput(requestKey, map, environment.SourceResults); return PlanningMapBuildResult.Success(map, environment.SourceResults, cacheHit); } finally { if (enteredCreateGate) Monitor.Exit(_createGate); } } private long NextSnapshotId() { return Interlocked.Increment(ref _nextSnapshotId); } private static string BuildInputDescriptor(PlanningMapRequest request, IReadOnlyList results) { var builder = new StringBuilder(); AppendFloat(builder, request.Bounds.XMin); AppendFloat(builder, request.Bounds.XMax); AppendFloat(builder, request.Bounds.YMin); AppendFloat(builder, request.Bounds.YMax); AppendFloat(builder, request.ResolutionMm); builder.Append(request.AllowExplicitEmptyMap ? '1' : '0'); var sources = request.ObstacleSources.OrderBy(s => s == null ? string.Empty : s.SourceId, StringComparer.Ordinal).ToArray(); for (int i = 0; i < sources.Length; i++) { IMapObstacleSource source = sources[i]; builder.Append('|').Append(source == null ? "" : source.SourceId).Append('|').Append(source == null ? -1 : source.SourceVersion).Append('|').Append(source != null && source.IsRequired ? '1' : '0'); if (i < results.Count) AppendProjection(builder, results[i]); } return builder.ToString(); } private static string BuildRequestKey(PlanningMapRequest request) { var builder = new StringBuilder(); AppendFloat(builder, request.Bounds.XMin); AppendFloat(builder, request.Bounds.XMax); AppendFloat(builder, request.Bounds.YMin); AppendFloat(builder, request.Bounds.YMax); AppendFloat(builder, request.ResolutionMm); builder.Append(request.AllowExplicitEmptyMap ? '1' : '0'); foreach (IMapObstacleSource source in request.ObstacleSources.OrderBy(s => s == null ? string.Empty : s.SourceId, StringComparer.Ordinal)) builder.Append('|').Append(source == null ? "" : source.SourceId).Append('|').Append(source == null ? -1 : source.SourceVersion).Append('|').Append(source != null && source.IsRequired ? '1' : '0'); return builder.ToString(); } private static void AppendProjection(StringBuilder builder, ObstacleProjectionResult result) { builder.Append('|').Append((int)result.Status); for (int index = 0; index < result.Obstacles.Count; index++) { var circle = result.Obstacles[index] as CircleObstacle; if (circle != null) { builder.Append("|C"); AppendFloat(builder, circle.CenterX); AppendFloat(builder, circle.CenterY); AppendFloat(builder, circle.RadiusMm); continue; } var rectangle = result.Obstacles[index] as AxisAlignedRectangleObstacle; if (rectangle != null) { builder.Append("|R"); AppendFloat(builder, rectangle.XMin); AppendFloat(builder, rectangle.XMax); AppendFloat(builder, rectangle.YMin); AppendFloat(builder, rectangle.YMax); } } } private static void AppendFloat(StringBuilder builder, float value) { builder.Append(BitConverter.ToInt32(BitConverter.GetBytes(value), 0).ToString("X8")); } private static bool TryComputeOccupancyHash(PlanningGridMap map, PlanningOperationBudget budget, out string occupancyHash, out PlanningOperationStopReason stopReason) { byte[] occupied = map.CopyOccupied(); stopReason = budget.GetStopReason(); if (stopReason != PlanningOperationStopReason.None) { occupancyHash = string.Empty; return false; } using (var sha = SHA256.Create()) { const int blockLength = 4096; int offset = 0; while (occupied.Length - offset > blockLength) { stopReason = budget.GetStopReason(); if (stopReason != PlanningOperationStopReason.None) { occupancyHash = string.Empty; return false; } sha.TransformBlock(occupied, offset, blockLength, occupied, offset); offset += blockLength; } stopReason = budget.GetStopReason(); if (stopReason != PlanningOperationStopReason.None) { occupancyHash = string.Empty; return false; } byte[] hash = sha.TransformFinalBlock(occupied, offset, occupied.Length - offset); occupancyHash = BitConverter.ToString(hash).Replace("-", string.Empty); stopReason = PlanningOperationStopReason.None; return true; } } private static string Sha256(string text) { using (var sha = SHA256.Create()) return BitConverter.ToString(sha.ComputeHash(Encoding.UTF8.GetBytes(text))).Replace("-", string.Empty); } }