磁导航1.0内部交管和信号交互

This commit is contained in:
ykkokluo
2026-08-24 14:55:35 +08:00
parent cacc5fea5c
commit 244b0af6c8
60 changed files with 6395 additions and 294 deletions
@@ -0,0 +1,479 @@
using System;
using System.Collections.Generic;
using System.Linq;
using SimpleCore.Library;
using SimpleLite.RCS.CarTypes;
using StandardScene.Signal.Model;
using StandardScene.Signal.Plc;
namespace StandardScene.Signal.Logic
{
/// <summary>
/// 磁条交管运行态:按管控区名称分组排队;放行按「本线 + 冲突对向线」站点并集查当前占用。
/// 不维护 pending 在途占额。
/// </summary>
public sealed class MagControlRuntime
{
private readonly object _lock = new object();
private Dictionary<int, MagControlAreaModel> _byTrigger = new Dictionary<int, MagControlAreaModel>();
private readonly Dictionary<int, DateTime> _releaseLog = new Dictionary<int, DateTime>();
private readonly Dictionary<int, Arrival> _arrivals = new Dictionary<int, Arrival>();
public int RuleCount
{
get
{
lock (_lock)
return _byTrigger.Count;
}
}
/// <summary>已不再使用 pending;保留属性避免旧 UI 绑定报错,恒为 0。</summary>
public int PendingCount => 0;
public void Replace(IEnumerable<MagControlAreaModel> rows)
{
var map = (rows ?? Array.Empty<MagControlAreaModel>())
.Where(x => x != null && x.TriggerSit > 0 && x.IsUse)
.GroupBy(x => x.TriggerSit)
.ToDictionary(g => g.Key, g => g.First());
lock (_lock)
{
_byTrigger = map;
_arrivals.Clear();
}
}
/// <summary>
/// 同名区排队;放行前查本线 + 冲突对向线站点并集上的当前占用。
/// 另计:已放行仍停在冲突触发点上的运行中车辆(避免 site 未进区时空窗连放)。
/// </summary>
public (int Waiting, int Released) ScanAndRelease()
{
List<MagControlAreaModel> rules;
lock (_lock)
rules = _byTrigger.Values.ToList();
if (rules.Count == 0)
return (0, 0);
var snapshots = SnapshotCars();
var occupancies = snapshots
.Select(s => (s.CarId, s.OccupiedSiteIds))
.ToList();
var groups = rules
.GroupBy(AreaKey, StringComparer.OrdinalIgnoreCase)
.Select(g => BuildGroup(g.Key, g.ToList()))
.ToList();
lock (_lock)
SweepArrivals(snapshots);
var waiting = 0;
var released = 0;
foreach (var group in groups)
{
var waiters = CollectWaiters(group, snapshots);
waiting += waiters.Count;
if (waiters.Count == 0)
continue;
// 停稳排队的车不计入区内占用;本拍已放行的车单独记冲突占额
var exclude = new HashSet<int>(waiters.Select(w => w.CarId));
var releasedThisScan = new List<Waiter>();
waiters.Sort((a, b) => CompareWaiters(a, b, group.WaitSeconds));
foreach (var waiter in waiters)
{
var checkSites = BuildConflictSites(group, waiter);
var capacity = MagControlAreaGate.NormalizeCapacity(waiter.Capacity);
var others = CountConflictingOccupancy(
group, waiter, checkSites, occupancies, snapshots, exclude, releasedThisScan);
if (others >= capacity)
continue;
SignalCarAdapter.StopCharge(waiter.Car);
SignalCarAdapter.StartMagCar(waiter.Car);
released++;
exclude.Add(waiter.CarId);
releasedThisScan.Add(waiter);
NoteRelease(waiter, group, others, capacity, checkSites);
}
}
return (waiting, released);
}
/// <summary>
/// 冲突占用 = 并集站点上的当前车
/// + 本区冲突触发点上仍「运行中」的车(刚放行、site 尚未进 ControlArea
/// + 本拍已放行且与本线冲突的车。
/// </summary>
private static int CountConflictingOccupancy(
AreaGroup group,
Waiter waiter,
IReadOnlyList<int> checkSites,
List<(int CarId, IReadOnlyList<int> OccupiedSiteIds)> occupancies,
List<CarSnapshot> snapshots,
HashSet<int> exclude,
List<Waiter> releasedThisScan)
{
var others = MagControlAreaGate.CountOccupyingCars(occupancies, checkSites, exclude);
var counted = new HashSet<int>();
foreach (var snap in snapshots)
{
if (snap == null || exclude.Contains(snap.CarId) || counted.Contains(snap.CarId))
continue;
if (!snap.IsMagCar)
continue;
// 已在并集站点上:CountOccupyingCars 已计,这里补「运行中仍在冲突触发点」
if (snap.IsStopped)
continue;
if (snap.Sit <= 0 || !group.Triggers.Contains(snap.Sit))
continue;
if (!group.RouteByTrigger.TryGetValue(snap.Sit, out var route))
continue;
if (!Conflicts(waiter.CompatGroup, waiter.AreaSites, route.CompatGroup, route.AreaSites))
continue;
// 若 site 已在 checkSites 里,上面 others 可能已计(触发点一般不在列表)
if (MagControlAreaGate.OccupiesListedSites(snap.OccupiedSiteIds, checkSites))
continue;
counted.Add(snap.CarId);
others++;
}
foreach (var releasedCar in releasedThisScan)
{
if (releasedCar == null || counted.Contains(releasedCar.CarId))
continue;
if (!Conflicts(
waiter.CompatGroup, waiter.AreaSites,
releasedCar.CompatGroup, releasedCar.AreaSites))
continue;
counted.Add(releasedCar.CarId);
others++;
}
return others;
}
/// <summary>单车判定:本线 + 同名区内冲突对向线站点并集的当前占用。</summary>
public MagControlReleaseDecision Decide(int carId, int triggerSit)
{
MagControlAreaModel rule;
List<MagControlAreaModel> sameArea;
lock (_lock)
{
if (triggerSit <= 0 || !_byTrigger.TryGetValue(triggerSit, out rule) || rule == null)
{
return new MagControlReleaseDecision
{
RuleFound = false,
Reason = "no mag-control rule"
};
}
var key = AreaKey(rule);
sameArea = _byTrigger.Values
.Where(r => string.Equals(AreaKey(r), key, StringComparison.OrdinalIgnoreCase))
.ToList();
}
var group = BuildGroup(AreaKey(rule), sameArea);
if (!group.RouteByTrigger.TryGetValue(triggerSit, out var route))
{
return new MagControlReleaseDecision
{
RuleFound = false,
Reason = "no mag-control route"
};
}
var waiter = new Waiter
{
CarId = carId,
TriggerSit = triggerSit,
CompatGroup = route.CompatGroup,
AreaSites = route.AreaSites,
Capacity = route.Capacity
};
var checkSites = BuildConflictSites(group, waiter);
var occupancies = new List<(int CarId, IReadOnlyList<int> OccupiedSiteIds)>();
foreach (var car in SignalCarAdapter.All())
{
if (car == null)
continue;
occupancies.Add((car.id, MagControlAreaGate.CollectOccupiedSiteIds(car)));
}
return MagControlAreaGate.Evaluate(
carId,
triggerSit,
checkSites,
rule.ControlCapacity,
rule.ControlReleaseStartStop,
occupancies);
}
internal static string AreaKey(MagControlAreaModel rule)
{
if (rule == null)
return "#sit:0";
var name = (rule.AreaName ?? string.Empty).Trim();
return name.Length > 0 ? name : "#sit:" + rule.TriggerSit;
}
internal static int CompareWaiters(Waiter a, Waiter b, int waitSeconds)
{
if (waitSeconds > 0)
{
if (a.Priority < b.Priority &&
(b.ArrivedAt - a.ArrivedAt).TotalSeconds > waitSeconds)
return -1;
if (b.Priority < a.Priority &&
(a.ArrivedAt - b.ArrivedAt).TotalSeconds > waitSeconds)
return 1;
}
var byPriority = b.Priority.CompareTo(a.Priority);
if (byPriority != 0)
return byPriority;
var byArrival = a.ArrivedAt.CompareTo(b.ArrivedAt);
if (byArrival != 0)
return byArrival;
return a.CarId.CompareTo(b.CarId);
}
internal static bool Conflicts(
string compatA,
IReadOnlyList<int> sitesA,
string compatB,
IReadOnlyList<int> sitesB)
{
if (string.Equals(compatA ?? "", compatB ?? "", StringComparison.OrdinalIgnoreCase))
return true;
return MagControlAreaGate.SitesIntersect(sitesA, sitesB);
}
/// <summary>
/// 本线站点 + 同名区内所有与本线冲突的对向/交叉线路站点并集。
/// 例如西环 NS-W 会并入东/北/南列表,100/101/104 有车时都能挡住触发点 4。
/// </summary>
private static IReadOnlyList<int> BuildConflictSites(AreaGroup group, Waiter waiter)
{
var set = new HashSet<int>();
if (waiter?.AreaSites != null)
{
foreach (var site in waiter.AreaSites)
{
if (site > 0)
set.Add(site);
}
}
if (group?.RouteByTrigger == null)
return set.OrderBy(x => x).ToList();
foreach (var route in group.RouteByTrigger.Values)
{
if (route?.AreaSites == null)
continue;
if (!Conflicts(waiter.CompatGroup, waiter.AreaSites, route.CompatGroup, route.AreaSites))
continue;
foreach (var site in route.AreaSites)
{
if (site > 0)
set.Add(site);
}
}
return set.OrderBy(x => x).ToList();
}
private static AreaGroup BuildGroup(string key, List<MagControlAreaModel> rows)
{
var ordered = rows.OrderBy(r => r.TriggerSit).ToList();
var triggers = new HashSet<int>(ordered.Select(r => r.TriggerSit));
var waitSeconds = 0;
var routeByTrigger = new Dictionary<int, RouteInfo>();
foreach (var row in ordered)
{
waitSeconds = Math.Max(waitSeconds, row.WaitSeconds);
var sites = MagControlAreaGate.ParseSiteIds(row.ControlArea, row.TriggerSit);
routeByTrigger[row.TriggerSit] = new RouteInfo
{
CompatGroup = MagControlAreaGate.NormalizeCompatGroup(row.CompatGroup),
Capacity = MagControlAreaGate.NormalizeCapacity(row.ControlCapacity),
AreaSites = sites,
Priority = row.Priority
};
}
return new AreaGroup
{
Key = key,
WaitSeconds = waitSeconds,
Triggers = triggers,
RouteByTrigger = routeByTrigger
};
}
private List<CarSnapshot> SnapshotCars()
{
var list = new List<CarSnapshot>();
foreach (var car in SignalCarAdapter.All())
{
if (car == null)
continue;
list.Add(new CarSnapshot
{
Car = car,
CarId = car.id,
OccupiedSiteIds = MagControlAreaGate.CollectOccupiedSiteIds(car),
IsMagCar = SignalCarAdapter.IsMagCar(car),
IsStopped = SignalCarAdapter.IsStopped(car),
Sit = SignalCarAdapter.PositionId(car)
});
}
return list;
}
private List<Waiter> CollectWaiters(AreaGroup group, List<CarSnapshot> snapshots)
{
var waiters = new List<Waiter>();
var now = DateTime.Now;
foreach (var snap in snapshots)
{
if (!snap.IsMagCar || !snap.IsStopped)
continue;
if (snap.Sit <= 0 || !group.Triggers.Contains(snap.Sit))
continue;
DateTime arrivedAt;
lock (_lock)
{
if (!_arrivals.TryGetValue(snap.CarId, out var arrival) || arrival.TriggerSit != snap.Sit)
{
arrival = new Arrival { TriggerSit = snap.Sit, ArrivedAt = now };
_arrivals[snap.CarId] = arrival;
}
arrivedAt = arrival.ArrivedAt;
}
if (!group.RouteByTrigger.TryGetValue(snap.Sit, out var route))
continue;
waiters.Add(new Waiter
{
Car = snap.Car,
CarId = snap.CarId,
TriggerSit = snap.Sit,
AreaKey = group.Key,
CompatGroup = route.CompatGroup,
Capacity = route.Capacity,
AreaSites = route.AreaSites,
Priority = route.Priority,
ArrivedAt = arrivedAt
});
}
return waiters;
}
private void SweepArrivals(List<CarSnapshot> snapshots)
{
var present = new HashSet<int>();
foreach (var snap in snapshots)
{
if (!snap.IsMagCar || !snap.IsStopped || snap.Sit <= 0)
continue;
if (_arrivals.TryGetValue(snap.CarId, out var arrival) && arrival.TriggerSit == snap.Sit)
present.Add(snap.CarId);
}
var stale = _arrivals.Keys.Where(id => !present.Contains(id)).ToList();
foreach (var id in stale)
_arrivals.Remove(id);
}
private void NoteRelease(
Waiter waiter,
AreaGroup group,
int others,
int capacity,
IReadOnlyList<int> checkSites)
{
var groupName = string.IsNullOrEmpty(waiter.CompatGroup) ? "-" : waiter.CompatGroup;
var carName = waiter.Car?.name;
var namePart = string.IsNullOrWhiteSpace(carName) ? "" : $" name={carName}";
var sites = waiter.AreaSites == null || waiter.AreaSites.Count == 0
? "-"
: string.Join(",", waiter.AreaSites);
var conflict = checkSites == null || checkSites.Count == 0
? "-"
: string.Join(",", checkSites);
var message =
$"放行 MagCar={waiter.CarId}{namePart} 区={group.Key} 组={groupName} 触发点={waiter.TriggerSit} " +
$"本线={sites} 冲突并集={conflict} 当前占用={others}/{capacity}";
MagTrafficFileLogger.Write(carName, waiter.CarId, message);
if (_releaseLog.TryGetValue(waiter.CarId, out var last) && (DateTime.Now - last).TotalSeconds < 5)
return;
_releaseLog[waiter.CarId] = DateTime.Now;
Diagnosis.Log($"磁条交管{message}", "MagTraffic", true);
}
internal sealed class Waiter
{
public Car Car;
public int CarId;
public int TriggerSit;
public string AreaKey;
public string CompatGroup;
public int Capacity;
public IReadOnlyList<int> AreaSites;
public int Priority;
public DateTime ArrivedAt;
}
private sealed class Arrival
{
public int TriggerSit;
public DateTime ArrivedAt;
}
private sealed class RouteInfo
{
public string CompatGroup;
public int Capacity;
public IReadOnlyList<int> AreaSites;
public int Priority;
}
private sealed class AreaGroup
{
public string Key;
public int WaitSeconds;
public HashSet<int> Triggers;
public Dictionary<int, RouteInfo> RouteByTrigger;
}
private sealed class CarSnapshot
{
public Car Car;
public int CarId;
public IReadOnlyList<int> OccupiedSiteIds;
public bool IsMagCar;
public bool IsStopped;
public int Sit;
}
}
}