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 { /// /// 磁条交管运行态:按管控区名称分组排队;放行按「本线 + 冲突对向线」站点并集查当前占用。 /// 不维护 pending 在途占额。 /// public sealed class MagControlRuntime { private readonly object _lock = new object(); private Dictionary _byTrigger = new Dictionary(); private readonly Dictionary _releaseLog = new Dictionary(); private readonly Dictionary _arrivals = new Dictionary(); public int RuleCount { get { lock (_lock) return _byTrigger.Count; } } /// 已不再使用 pending;保留属性避免旧 UI 绑定报错,恒为 0。 public int PendingCount => 0; public void Replace(IEnumerable rows) { var map = (rows ?? Array.Empty()) .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(); } } /// /// 同名区排队;放行前查本线 + 冲突对向线站点并集上的当前占用。 /// 另计:已放行仍停在冲突触发点上的运行中车辆(避免 site 未进区时空窗连放)。 /// public (int Waiting, int Released) ScanAndRelease() { List 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(waiters.Select(w => w.CarId)); var releasedThisScan = new List(); 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); } /// /// 冲突占用 = 并集站点上的当前车 /// + 本区冲突触发点上仍「运行中」的车(刚放行、site 尚未进 ControlArea) /// + 本拍已放行且与本线冲突的车。 /// private static int CountConflictingOccupancy( AreaGroup group, Waiter waiter, IReadOnlyList checkSites, List<(int CarId, IReadOnlyList OccupiedSiteIds)> occupancies, List snapshots, HashSet exclude, List releasedThisScan) { var others = MagControlAreaGate.CountOccupyingCars(occupancies, checkSites, exclude); var counted = new HashSet(); 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; } /// 单车判定:本线 + 同名区内冲突对向线站点并集的当前占用。 public MagControlReleaseDecision Decide(int carId, int triggerSit) { MagControlAreaModel rule; List 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 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 sitesA, string compatB, IReadOnlyList sitesB) { if (string.Equals(compatA ?? "", compatB ?? "", StringComparison.OrdinalIgnoreCase)) return true; return MagControlAreaGate.SitesIntersect(sitesA, sitesB); } /// /// 本线站点 + 同名区内所有与本线冲突的对向/交叉线路站点并集。 /// 例如西环 NS-W 会并入东/北/南列表,100/101/104 有车时都能挡住触发点 4。 /// private static IReadOnlyList BuildConflictSites(AreaGroup group, Waiter waiter) { var set = new HashSet(); 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 rows) { var ordered = rows.OrderBy(r => r.TriggerSit).ToList(); var triggers = new HashSet(ordered.Select(r => r.TriggerSit)); var waitSeconds = 0; var routeByTrigger = new Dictionary(); 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 SnapshotCars() { var list = new List(); 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 CollectWaiters(AreaGroup group, List snapshots) { var waiters = new List(); 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 snapshots) { var present = new HashSet(); 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 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 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 AreaSites; public int Priority; } private sealed class AreaGroup { public string Key; public int WaitSeconds; public HashSet Triggers; public Dictionary RouteByTrigger; } private sealed class CarSnapshot { public Car Car; public int CarId; public IReadOnlyList OccupiedSiteIds; public bool IsMagCar; public bool IsStopped; public int Sit; } } }