480 lines
18 KiB
C#
480 lines
18 KiB
C#
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;
|
||
}
|
||
}
|
||
}
|