Files
StandardSence/StandardScene.Signal/Logic/MagControlRuntime.cs
T

480 lines
18 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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;
}
}
}