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);
}
}