磁导航1.0内部交管和信号交互
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user