init commit

This commit is contained in:
zhaowei.huang
2026-06-14 11:19:15 +08:00
parent e79a3815a5
commit c8e540d272
174 changed files with 60830 additions and 39 deletions
@@ -0,0 +1,413 @@
using SimpleCore.Traffic;
using System;
using System.Collections.Generic;
using System.Linq;
using SimpleLite.RCS;
using SimpleLite.RCS.CarTypes;
using SimpleCore.Library;
using SimpleCore.PropType;
using SimpleCore;
using System.Threading;
using SimpleLite.CADTools;
using SimpleLite.Props;
using SimpleLite.UI;
using SimpleLite;
using System.Windows.Forms;
using Newtonsoft.Json;
namespace StandardScene.InterLock
{
public class MyDictionary<TKey, TItem> where TItem : new()
{
private readonly Dictionary<TKey, TItem> dictionary = new();
public TItem this[TKey key]
{
get
{
if (dictionary.TryGetValue(key, out var item)) return item;
var newItem = new TItem();
dictionary[key] = newItem;
return newItem;
}
set => dictionary[key] = value;
}
public ICollection<TKey> Keys
{
get => dictionary.Keys;
}
}
public class AbstractInterlockMission : Mission
{
public bool GetAskEnter(int siteId)
{
if (!SiteFilter(siteId)) return false;
lock (sync) return askEnter[siteId];
}
public void SetAllowEnter(int siteId, bool value)
{
if (!SiteFilter(siteId)) return;
lock (sync) allowEnter[siteId] = value;
}
public bool GetAskExit(int siteId)
{
if (!SiteFilter(siteId)) return false;
lock (sync) return askExit[siteId];
}
public void SetAllowExit(int siteId, bool value)
{
if (!SiteFilter(siteId)) return;
lock (sync) allowExit[siteId] = value;
}
public bool GetReportLeave(int siteId)
{
if (!SiteFilter(siteId)) return false;
lock (sync) return reportLeave[siteId];
}
public void SetAcknowledgeLeave(int siteId, bool value)
{
if (!SiteFilter(siteId)) return;
lock (sync) acknowledgeLeave[siteId] = value;
}
public int GetInSiteCarId(int siteId)
{
if (!SiteFilter(siteId)) return -1;
lock (sync) return inSiteCarId[siteId];
}
// 请求进站(写)
[JsonIgnore] private readonly MyDictionary<int, bool> askEnter = new();
// 允许进站(读)
[JsonIgnore] private readonly MyDictionary<int, bool> allowEnter = new();
// 请求离站(写)
[JsonIgnore] private readonly MyDictionary<int, bool> askExit = new();
// 允许离站(读)
[JsonIgnore] private readonly MyDictionary<int, bool> allowExit = new();
// 报告离开(写)
[JsonIgnore] private readonly MyDictionary<int, bool> reportLeave = new();
// 确认离开(读)
[JsonIgnore] private readonly MyDictionary<int, bool> acknowledgeLeave = new();
// 站点上所在的小车(状态)
[JsonIgnore] private readonly MyDictionary<int, int> inSiteCarId = new();
[JsonIgnore] private object sync = new();
public virtual string GetSiteDisplay(int siteId)
{
return $"{siteId}";
}
/// <summary>
/// 返回为true的站点,是当前互锁机制关注的站点
/// </summary>
/// <param name="siteId"></param>
/// <returns></returns>
public virtual bool SiteFilter(int siteId)
{
return true;
}
/// <summary>
/// 互锁站点进站条件
/// </summary>
/// <param name="car"></param>
/// <param name="siteId"></param>
/// <returns></returns>
private bool EnterCondition(AbstractCar car, int siteId)
{
// todo: 是否考虑提前更多申请进入,加快节拍?
if (car.status.pendingLocks.First() != siteId) return false;
//DateTime startTime = DateTime.Now;
//bool validTime = false;
lock (sync)
{
// if (askEnter[siteId] == false)
// {
// Diagnosis.Post($"{car.name}({car.id}) 申请进站 {GetSiteDisplay(siteId)}", "intertime", true);
// //startTime = DateTime.Now;
// //validTime = true;
// }
askEnter[siteId] = true;
if (reportLeave[siteId])
{
Diagnosis.Post($"{car.name}({car.id})未被准许进站{GetSiteDisplay(siteId)},前车离开信号未被确认", "interlock", true);
return false;
}
if (allowEnter[siteId])
{
Diagnosis.Post($"{car.name}({car.id})准许进站{GetSiteDisplay(siteId)}", "interlock", true);
// Diagnosis.Post($"{car.name}({car.id}) 准许进站 {GetSiteDisplay(siteId)}", "intertime", true);
lock (sync) askEnter[siteId] = false;
return true;
}
Diagnosis.Post($"{car.name}({car.id})未被准许进站{GetSiteDisplay(siteId)}", "interlock", true);
return false;
}
}
/// <summary>
/// 互锁站点离站条件
/// </summary>
/// <param name="car"></param>
/// <param name="siteId"></param>
/// <returns></returns>
private bool ExitCondition(AbstractCar car, int siteId)
{
lock (sync)
{
askExit[siteId] = true;
if (allowExit[siteId])
{
Diagnosis.Post($"{car.name}({car.id})准许离站{GetSiteDisplay(siteId)}", "interlock", true);
askExit[siteId] = false;
return true;
}
Diagnosis.Post($"{car.name}({car.id})未被准许离站{GetSiteDisplay(siteId)}", "interlock", true);
return false;
}
}
/// <summary>
/// 告知对方设备小车离开,直到对方设备确认收到离开信号
/// </summary>
/// <param name="car"></param>
/// <param name="siteId"></param>
private void LeaveEvent(AbstractCar car, int siteId)
{
new Thread(() =>
{
while (true)
{
Thread.Sleep(100);
lock (sync)
{
if (acknowledgeLeave[siteId]) // 已经确认小车离开
{
Diagnosis.Post($"{GetSiteDisplay(siteId)}确认{car.name}({car.id})离开", "interlock", true);
break;
}
}
Diagnosis.Post($"{car.name}({car.id})告知离开{GetSiteDisplay(siteId)}", "interlock", true);
lock (sync) reportLeave[siteId] = true;
}
lock (sync) reportLeave[siteId] = false;
}).Start();
}
[JsonIgnore] private bool enableSimulation = false;
[JsonIgnore] public bool started = false;
[JsonIgnore] public Thread showT;
[MethodMember(Name = "启动进程", Description = "处理安全互锁")]
public override void Execute()
{
if (started) return;
// started = true;
status.status = "已启动";
showT = new Thread(() =>
{
while (true)
{
Thread.Sleep(300);
try
{
if (fields.TryGetValue("enableSimulation", out var userEnableSimulation))
enableSimulation = bool.Parse(userEnableSimulation);
foreach (var site in SimpleLib.GetAllSites().Where(ss => SiteFilter(ss.id)))
{
var carId = -1;
foreach (var car in SimpleLib.GetAllCars())
{
lock (TrafficControl.syncTrafficSequence)
{
if (car.status.holdingLocks.Length == 1 && car.status.holdingLocks[0] == site.id)
{
carId = car.id;
break;
}
}
}
lock (sync) inSiteCarId[site.id] = carId;
}
// if (!onDisplay) continue;
var painter = SimpleMonitor.getPainter("DemoInterlockMission");
painter.clear();
if (!onDisplay) continue;
//string str;
//lock (sync)
//{
// var keys = askEnter.Keys.Concat(askExit.Keys).Concat(reportLeave.Keys).ToHashSet()
// .OrderBy(kk => int.Parse(SimpleLib.GetSite(kk).fields["ASNub"])).ToList();
// str = $"站点\t\t请求进站\t允许进站\t请求离站\t允许离站\t上报离开\t确认离开\t小车id\n" +
// $"{string.Join("\n", keys.Select(key =>
// $"{GetSiteDisplay(key)}\t{askEnter[key]}\t\t{allowEnter[key]}\t\t" +
// $"{askExit[key]}\t\t{allowExit[key]}\t\t" +
// $"{reportLeave[key]}\t\t{acknowledgeLeave[key]}\t\t{inSiteCarId[key]}"))}";
//}
//painter.drawTextFixed(str, new SolidBrush(Color.Black), VirtualPainter.DrawPosition.RightTop, Color.AliceBlue);
}
catch (Exception ex)
{
Diagnosis.Post(ExceptionFormatter.FormatEx(ex), "interlock", true);
}
}
}) { Name = "InterlockMission" };
showT.Start();
}
[JsonIgnore] private bool onDisplay = true;
[MethodMember(Name = "切换显示", Description = "是否在右下角显示")]
public void SwitchAlwaysOnDisplay()
{
onDisplay = !onDisplay;
}
private async void SimButton(string title, MyDictionary<int, bool> toManipulate)
{
//if (!enableSimulation) return;
G.pushStatus("请选择站点");
var pt = await Program.UI.getPoint(new UIOps.getPointOptions() { site = true });
var site = SimpleLib.GetSite(pt.site);
if (SiteFilter(site.id))
{
var vv = await Program.UI.Input("请输入true/false", title, "true");
if (vv == null || !bool.TryParse(vv, out var allow))
{
MessageBox.Show("输入错误");
return;
}
lock (sync) toManipulate[site.id] = allow;
}
}
[MethodMember(Name = "模拟准许进站")]
public void SimAllowEnter()
{
SimButton("模拟准许进入状态", allowEnter);
}
[MethodMember(Name = "模拟准许离站")]
public void SimAllowExit()
{
SimButton("模拟准许离站状态", allowExit);
}
[MethodMember(Name = "模拟确认离开")]
public void SimLeaveNoted()
{
SimButton("模拟确认离开状态", acknowledgeLeave);
}
[MethodMember(Name = "模拟发送离开")]
public async void SimReportLeave()
{
// if (!enableSimulation) return;
G.pushStatus("请选择站点");
var pt = await Program.UI.getPoint(new UIOps.getPointOptions() { site = true });
var site = SimpleLib.GetSite(pt.site);
if (SiteFilter(site.id))
{
lock (sync) reportLeave[site.id] = true;
}
new Thread(() =>
{
var t1 = DateTime.Now;
while (true)
{
Thread.Sleep(100);
lock (sync)
{
var deltaT = (DateTime.Now - t1).TotalSeconds;
if (acknowledgeLeave[site.id]||deltaT>5) // 已经确认小车离开
{
Diagnosis.Post($"{GetSiteDisplay(site.id)} 停止发送离开", "simlikai", true);
break;
}
}
Diagnosis.Post($"模拟告知离开{GetSiteDisplay(site.id)}", "simlikai", true);
lock (sync) reportLeave[site.id] = true;
}
lock (sync) reportLeave[site.id] = false;
}).Start();
// SimButton("模拟确认离开状态", reportLeave);
}
public AbstractInterlockMission()
{
Diagnosis.Post($"InterlockMechanism loaded", "interlock", true);
TrafficControl.BeforeLock += (car, siteId) =>
{
if (SiteFilter(siteId)) // EnterCondition
return EnterCondition(car, siteId);
var site = SimpleLib.GetSite(siteId);
if (site.fields.ContainsKey("PreAskEnterSite") && int.TryParse(site.fields["PreAskEnterSite"], out int PreAskEnterSiteId))
{
var presite = SimpleLib.GetSite(PreAskEnterSiteId);
if (presite != null && SiteFilter(PreAskEnterSiteId))
{
Diagnosis.Post($"{car.name}({car.id})在{siteId}-{GetSiteDisplay(siteId)},提前申请 {PreAskEnterSiteId} 的请求进入", "interlock", true);
askEnter[PreAskEnterSiteId] = true;
}
}
// ExitCondition
lock (TrafficControl.syncTrafficSequence)
if (!car.status.holdingLocks.Any(ss => SiteFilter(ss)))
return true;
var isNeighbor = false;
var lockSiteId = -1;
foreach (var trackId in site.relatedTracks)
{
var track = SimpleLib.GetTrack(trackId);
if (SiteFilter(track.siteA))
{
isNeighbor = true;
lockSiteId = track.siteA;
break;
}
if (SiteFilter(track.siteB))
{
isNeighbor = true;
lockSiteId = track.siteB;
break;
}
}
return !isNeighbor || ExitCondition(car, lockSiteId);
};
TrafficControl.AfterLeave += (car, siteId) =>
{
if (SiteFilter(siteId)) LeaveEvent(car, siteId);
};
}
}
}
@@ -0,0 +1,167 @@
using LessokajiWeaverUtilities.Utilities;
using LoopViewerApp;
using Newtonsoft.Json;
using SimpleLite.RCS;
using SimpleLite.RCS.CarTypes;
using SimpleCore;
using SimpleCore.PropType;
using SimpleCore.Traffic;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Windows.Forms;
namespace StandardScene.InterLock
{
[MissionType(Name = "交通管制", editor = typeof(TrafficInterlockMission))]
[I18N.DocumentTranslation(Name = "Traffic Mission", locale = "en")]
public class TrafficInterlockMission : Mission
{
public static List<TrafficArea> TrafficAreaList { get; set; } = new List<TrafficArea>();
public TrafficInterlockMission()
{
TrafficControl.BeforeLock += (car, siteId) =>
{
return EnterArea(car, siteId);
};
TrafficControl.AfterLeave += (car, siteId) =>
{
LeaveArea(car, siteId);
};
}
[MethodMember(Name = "启动进程", Description = "开始交通监控")]
public void StartTrafficControl()
{
if (InitTrafficConfig()) { status.status = "已启动"; }
}
[MethodMember(Name = "停止进程", Description = "停止交通监控")]
public void StopTrafficControl()
{
status.status = "已停止";
}
[MethodMember(Name = "查看管制区")]
public void ShowViewer()
{
var viewer = new TrafficInterlockViewer();
viewer.Show();
}
/// <summary>
/// 初始化交管配置文件
/// </summary>
/// <returns></returns>
private bool InitTrafficConfig()
{
try
{
string ConfigPath = Path.Combine(Application.StartupPath, "Config/traffic.json");
if (File.Exists(ConfigPath))
{
TrafficAreaList = JsonConvert.DeserializeObject<List<TrafficArea>>(File.ReadAllText(ConfigPath));
return true;
}
return false;
}
catch (Exception ex)
{
Console.WriteLine("初始化交管配置文件失败: " + ex.Message); return false;
}
}
/// <summary>
/// 判断小车是否可以进入区域
/// </summary>
/// <param name="car"></param>
/// <param name="siteId"></param>
/// <returns></returns>
public bool EnterArea(AbstractCar car, int siteId)
{
var TrafficAreas = TrafficAreaList.FindAll(area => area.SiteList.Contains(siteId) && area.IsEnable);
if (TrafficAreas.Count == 0) { return true; }
if (!TrafficAreas.Exists(t => t.IsOccupy && t.ControllerName != car.id.ToString()))
{
lock (TrafficAreaList)
{
TrafficAreas.ForEach(t => { t.IsOccupy = true; t.ControllerName = car.id.ToString(); });
}
Console.WriteLine($"小车 {car.id} 进入了交管区域 {string.Join(",", TrafficAreas.Select(t => t.AreaName))}");
return true;
}
return false;
}
/// <summary>
/// 离开区域
/// </summary>
/// <param name="car"></param>
/// <param name="siteId"></param>
/// <returns></returns>
public bool LeaveArea(AbstractCar car, int siteId)
{
var TrafficAreas = TrafficAreaList.FindAll(area => area.SiteList.Contains(siteId) && area.IsEnable);
if (TrafficAreas.Count == 0) { return true; }
foreach (var item in TrafficAreas)
{
var Sites = SimpleLib.GetAllSites().Where(s => item.SiteList.Contains(s.id)).ToList();
if (!Sites.Exists(t => t.status.owner == car.id))
{
lock (TrafficAreaList)
{
item.ControllerName = string.Empty; item.IsOccupy = false;
}
Console.WriteLine($"小车 {car.id} 离开了交管区域 {item.AreaName}");
}
}
return true;
}
}
public class TrafficArea
{
/// <summary>
/// 区域名称
/// </summary>
public string AreaName { get; set; }
/// <summary>
/// 区域站点集合
/// </summary>
public List<int> SiteList { get; set; }
/// <summary>
/// 控制权
/// </summary>
[JsonIgnore]
public string ControllerName { get; set; }
/// <summary>
/// 是否被占用
/// </summary>
[JsonIgnore]
public bool IsOccupy { get; set; }
/// <summary>
/// 是否启用
/// </summary>
public bool IsEnable { get; set; }
}
}
@@ -0,0 +1,325 @@
using System;
using System.Drawing;
using System.Windows.Forms;
namespace LoopViewerApp
{
partial class TrafficInterlockViewer
{
private System.ComponentModel.IContainer components = null;
private ListView lstTasks;
private GroupBox grpEdit;
private ColumnHeader colAreaName;
private ColumnHeader colSites;
private ColumnHeader colControlRight;
private ColumnHeader colIsOccupied;
private ColumnHeader colIsEnabled;
private Label lblAreaName;
private TextBox txtAreaName;
private Label lblStationIds;
private TextBox txtStationIds;
private Label lblControlRight;
private TextBox txtControlRight;
private Label lblIsOccupied;
private CheckBox chkIsOccupied;
private Label lblIsEnabled;
private CheckBox chkIsEnabled;
private Label lblEditingHint;
private Button btnSave;
private Button btnRefresh;
private Button btnNew;
private Button btnDelete;
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}
private void InitializeComponent()
{
this.lstTasks = new System.Windows.Forms.ListView();
this.colAreaName = ((System.Windows.Forms.ColumnHeader)(new System.Windows.Forms.ColumnHeader()));
this.colSites = ((System.Windows.Forms.ColumnHeader)(new System.Windows.Forms.ColumnHeader()));
this.colControlRight = ((System.Windows.Forms.ColumnHeader)(new System.Windows.Forms.ColumnHeader()));
this.colIsOccupied = ((System.Windows.Forms.ColumnHeader)(new System.Windows.Forms.ColumnHeader()));
this.colIsEnabled = ((System.Windows.Forms.ColumnHeader)(new System.Windows.Forms.ColumnHeader()));
this.grpEdit = new System.Windows.Forms.GroupBox();
this.lblEditingHint = new System.Windows.Forms.Label();
this.lblAreaName = new System.Windows.Forms.Label();
this.txtAreaName = new System.Windows.Forms.TextBox();
this.lblStationIds = new System.Windows.Forms.Label();
this.txtStationIds = new System.Windows.Forms.TextBox();
this.lblControlRight = new System.Windows.Forms.Label();
this.txtControlRight = new System.Windows.Forms.TextBox();
this.lblIsOccupied = new System.Windows.Forms.Label();
this.chkIsOccupied = new System.Windows.Forms.CheckBox();
this.lblIsEnabled = new System.Windows.Forms.Label();
this.chkIsEnabled = new System.Windows.Forms.CheckBox();
this.btnSave = new System.Windows.Forms.Button();
this.btnRefresh = new System.Windows.Forms.Button();
this.btnNew = new System.Windows.Forms.Button();
this.btnDelete = new System.Windows.Forms.Button();
this.grpEdit.SuspendLayout();
this.SuspendLayout();
//
// lstTasks
//
this.lstTasks.BackColor = System.Drawing.Color.White;
this.lstTasks.Columns.AddRange(new System.Windows.Forms.ColumnHeader[] {
this.colAreaName,
this.colSites,
this.colControlRight,
this.colIsOccupied,
this.colIsEnabled});
this.lstTasks.ForeColor = System.Drawing.Color.FromArgb(((int)(((byte)(33)))), ((int)(((byte)(33)))), ((int)(((byte)(33)))));
this.lstTasks.FullRowSelect = true;
this.lstTasks.HideSelection = false;
this.lstTasks.Location = new System.Drawing.Point(12, 12);
this.lstTasks.Name = "lstTasks";
this.lstTasks.OwnerDraw = true;
this.lstTasks.Size = new System.Drawing.Size(760, 320);
this.lstTasks.TabIndex = 0;
this.lstTasks.UseCompatibleStateImageBehavior = false;
this.lstTasks.View = System.Windows.Forms.View.Details;
this.lstTasks.DrawColumnHeader += new System.Windows.Forms.DrawListViewColumnHeaderEventHandler(this.lstTasks_DrawColumnHeader);
this.lstTasks.DrawItem += new System.Windows.Forms.DrawListViewItemEventHandler(this.lstTasks_DrawItem);
this.lstTasks.DrawSubItem += new System.Windows.Forms.DrawListViewSubItemEventHandler(this.lstTasks_DrawSubItem);
this.lstTasks.SelectedIndexChanged += new System.EventHandler(this.lstTasks_SelectedIndexChanged);
//
// colAreaName
//
this.colAreaName.Text = "区域名称";
this.colAreaName.Width = 140;
//
// colSites
//
this.colSites.Text = "区域站点集合";
this.colSites.TextAlign = System.Windows.Forms.HorizontalAlignment.Center;
this.colSites.Width = 280;
//
// colControlRight
//
this.colControlRight.Text = "控制权";
this.colControlRight.TextAlign = System.Windows.Forms.HorizontalAlignment.Center;
this.colControlRight.Width = 120;
//
// colIsOccupied
//
this.colIsOccupied.Text = "是否被占用";
this.colIsOccupied.TextAlign = System.Windows.Forms.HorizontalAlignment.Center;
this.colIsOccupied.Width = 100;
//
// colIsEnabled
//
this.colIsEnabled.Text = "是否启用";
this.colIsEnabled.TextAlign = System.Windows.Forms.HorizontalAlignment.Center;
this.colIsEnabled.Width = 100;
//
// grpEdit
//
this.grpEdit.Controls.Add(this.lblEditingHint);
this.grpEdit.Controls.Add(this.lblAreaName);
this.grpEdit.Controls.Add(this.txtAreaName);
this.grpEdit.Controls.Add(this.lblStationIds);
this.grpEdit.Controls.Add(this.txtStationIds);
this.grpEdit.Controls.Add(this.lblControlRight);
this.grpEdit.Controls.Add(this.txtControlRight);
this.grpEdit.Controls.Add(this.lblIsOccupied);
this.grpEdit.Controls.Add(this.chkIsOccupied);
this.grpEdit.Controls.Add(this.lblIsEnabled);
this.grpEdit.Controls.Add(this.chkIsEnabled);
this.grpEdit.Controls.Add(this.btnSave);
this.grpEdit.Controls.Add(this.btnRefresh);
this.grpEdit.Controls.Add(this.btnNew);
this.grpEdit.Controls.Add(this.btnDelete);
this.grpEdit.Font = new System.Drawing.Font("微软雅黑", 10F, System.Drawing.FontStyle.Bold);
this.grpEdit.Location = new System.Drawing.Point(12, 345);
this.grpEdit.Name = "grpEdit";
this.grpEdit.Size = new System.Drawing.Size(760, 165);
this.grpEdit.TabIndex = 1;
this.grpEdit.TabStop = false;
this.grpEdit.Text = "数据新增/编辑(点击表格行可在此查看并编辑该行数据)";
//
// lblEditingHint
//
this.lblEditingHint.AutoSize = true;
this.lblEditingHint.Font = new System.Drawing.Font("微软雅黑", 10F, System.Drawing.FontStyle.Bold);
this.lblEditingHint.ForeColor = System.Drawing.Color.FromArgb(((int)(((byte)(0)))), ((int)(((byte)(120)))), ((int)(((byte)(215)))));
this.lblEditingHint.Location = new System.Drawing.Point(12, 125);
this.lblEditingHint.Name = "lblEditingHint";
this.lblEditingHint.Size = new System.Drawing.Size(65, 19);
this.lblEditingHint.TabIndex = 0;
this.lblEditingHint.Text = "新增区域";
//
// lblAreaName
//
this.lblAreaName.AutoSize = true;
this.lblAreaName.Font = new System.Drawing.Font("微软雅黑", 10F);
this.lblAreaName.Location = new System.Drawing.Point(12, 28);
this.lblAreaName.Name = "lblAreaName";
this.lblAreaName.Size = new System.Drawing.Size(79, 20);
this.lblAreaName.TabIndex = 1;
this.lblAreaName.Text = "区域名称:";
//
// txtAreaName
//
this.txtAreaName.Font = new System.Drawing.Font("微软雅黑", 10F);
this.txtAreaName.Location = new System.Drawing.Point(100, 24);
this.txtAreaName.Name = "txtAreaName";
this.txtAreaName.Size = new System.Drawing.Size(200, 25);
this.txtAreaName.TabIndex = 2;
//
// lblStationIds
//
this.lblStationIds.AutoSize = true;
this.lblStationIds.Font = new System.Drawing.Font("微软雅黑", 10F);
this.lblStationIds.Location = new System.Drawing.Point(320, 28);
this.lblStationIds.Name = "lblStationIds";
this.lblStationIds.Size = new System.Drawing.Size(79, 20);
this.lblStationIds.TabIndex = 3;
this.lblStationIds.Text = "站点集合:";
//
// txtStationIds
//
this.txtStationIds.Font = new System.Drawing.Font("微软雅黑", 10F);
this.txtStationIds.Location = new System.Drawing.Point(418, 24);
this.txtStationIds.Name = "txtStationIds";
this.txtStationIds.Size = new System.Drawing.Size(320, 25);
this.txtStationIds.TabIndex = 4;
//
// lblControlRight
//
this.lblControlRight.AutoSize = true;
this.lblControlRight.Font = new System.Drawing.Font("微软雅黑", 10F);
this.lblControlRight.Location = new System.Drawing.Point(12, 58);
this.lblControlRight.Name = "lblControlRight";
this.lblControlRight.Size = new System.Drawing.Size(65, 20);
this.lblControlRight.TabIndex = 5;
this.lblControlRight.Text = "控制权:";
//
// txtControlRight
//
this.txtControlRight.Font = new System.Drawing.Font("微软雅黑", 10F);
this.txtControlRight.Location = new System.Drawing.Point(100, 54);
this.txtControlRight.Name = "txtControlRight";
this.txtControlRight.Size = new System.Drawing.Size(200, 25);
this.txtControlRight.TabIndex = 6;
//
// lblIsOccupied
//
this.lblIsOccupied.AutoSize = true;
this.lblIsOccupied.Font = new System.Drawing.Font("微软雅黑", 10F);
this.lblIsOccupied.Location = new System.Drawing.Point(320, 58);
this.lblIsOccupied.Name = "lblIsOccupied";
this.lblIsOccupied.Size = new System.Drawing.Size(93, 20);
this.lblIsOccupied.TabIndex = 7;
this.lblIsOccupied.Text = "是否被占用:";
//
// chkIsOccupied
//
this.chkIsOccupied.AutoSize = true;
this.chkIsOccupied.Font = new System.Drawing.Font("Segoe UI", 9F);
this.chkIsOccupied.Location = new System.Drawing.Point(418, 59);
this.chkIsOccupied.Name = "chkIsOccupied";
this.chkIsOccupied.Size = new System.Drawing.Size(39, 19);
this.chkIsOccupied.TabIndex = 8;
this.chkIsOccupied.Text = "是";
//
// lblIsEnabled
//
this.lblIsEnabled.AutoSize = true;
this.lblIsEnabled.Font = new System.Drawing.Font("微软雅黑", 10F);
this.lblIsEnabled.Location = new System.Drawing.Point(500, 58);
this.lblIsEnabled.Name = "lblIsEnabled";
this.lblIsEnabled.Size = new System.Drawing.Size(79, 20);
this.lblIsEnabled.TabIndex = 9;
this.lblIsEnabled.Text = "是否启用:";
//
// chkIsEnabled
//
this.chkIsEnabled.AutoSize = true;
this.chkIsEnabled.Checked = true;
this.chkIsEnabled.CheckState = System.Windows.Forms.CheckState.Checked;
this.chkIsEnabled.Font = new System.Drawing.Font("Segoe UI", 9F);
this.chkIsEnabled.Location = new System.Drawing.Point(585, 59);
this.chkIsEnabled.Name = "chkIsEnabled";
this.chkIsEnabled.Size = new System.Drawing.Size(39, 19);
this.chkIsEnabled.TabIndex = 10;
this.chkIsEnabled.Text = "是";
//
// btnSave
//
this.btnSave.BackColor = System.Drawing.Color.LightBlue;
this.btnSave.FlatStyle = System.Windows.Forms.FlatStyle.Flat;
this.btnSave.Font = new System.Drawing.Font("微软雅黑", 11F, System.Drawing.FontStyle.Bold);
this.btnSave.Location = new System.Drawing.Point(260, 118);
this.btnSave.Name = "btnSave";
this.btnSave.Size = new System.Drawing.Size(140, 40);
this.btnSave.TabIndex = 12;
this.btnSave.Text = "保存";
this.btnSave.UseVisualStyleBackColor = false;
this.btnSave.Click += new System.EventHandler(this.btnSave_Click);
//
// btnRefresh
//
this.btnRefresh.BackColor = System.Drawing.SystemColors.Control;
this.btnRefresh.FlatStyle = System.Windows.Forms.FlatStyle.Flat;
this.btnRefresh.Font = new System.Drawing.Font("微软雅黑", 11F);
this.btnRefresh.Location = new System.Drawing.Point(410, 118);
this.btnRefresh.Name = "btnRefresh";
this.btnRefresh.Size = new System.Drawing.Size(140, 40);
this.btnRefresh.TabIndex = 13;
this.btnRefresh.Text = "刷新";
this.btnRefresh.UseVisualStyleBackColor = false;
this.btnRefresh.Click += new System.EventHandler(this.btnRefresh_Click);
//
// btnNew
//
this.btnNew.BackColor = System.Drawing.SystemColors.Control;
this.btnNew.FlatStyle = System.Windows.Forms.FlatStyle.Flat;
this.btnNew.Font = new System.Drawing.Font("微软雅黑", 11F);
this.btnNew.Location = new System.Drawing.Point(560, 118);
this.btnNew.Name = "btnNew";
this.btnNew.Size = new System.Drawing.Size(140, 40);
this.btnNew.TabIndex = 14;
this.btnNew.Text = "新增";
this.btnNew.UseVisualStyleBackColor = false;
this.btnNew.Click += new System.EventHandler(this.btnNew_Click);
//
// btnDelete
//
this.btnDelete.BackColor = System.Drawing.SystemColors.Control;
this.btnDelete.FlatStyle = System.Windows.Forms.FlatStyle.Flat;
this.btnDelete.Font = new System.Drawing.Font("微软雅黑", 11F);
this.btnDelete.Location = new System.Drawing.Point(110, 118);
this.btnDelete.Name = "btnDelete";
this.btnDelete.Size = new System.Drawing.Size(140, 40);
this.btnDelete.TabIndex = 11;
this.btnDelete.Text = "删除";
this.btnDelete.UseVisualStyleBackColor = false;
this.btnDelete.Click += new System.EventHandler(this.btnDelete_Click);
//
// TrafficInterlockViewer
//
this.ClientSize = new System.Drawing.Size(784, 521);
this.Controls.Add(this.lstTasks);
this.Controls.Add(this.grpEdit);
this.Font = new System.Drawing.Font("微软雅黑", 9F);
this.MinimumSize = new System.Drawing.Size(700, 450);
this.Name = "TrafficInterlockViewer";
this.StartPosition = System.Windows.Forms.FormStartPosition.CenterScreen;
this.Text = "交通联锁区域管理";
this.grpEdit.ResumeLayout(false);
this.grpEdit.PerformLayout();
this.ResumeLayout(false);
}
}
}
@@ -0,0 +1,370 @@
using Newtonsoft.Json;
using StandardScene.InterLock; // 数据类型采用 TrafficInterlockMission 中的 TrafficArea
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Drawing;
using System.IO;
using System.Linq;
using System.Windows.Forms;
namespace LoopViewerApp
{
public partial class TrafficInterlockViewer : Form
{
/// <summary>-1 表示新增模式;>=0 表示正在编辑对应索引</summary>
private int _editingIndex = -1;
/// <summary>选中行变化时是否允许加载到编辑区(避免在保存/取消时重复刷新)</summary>
private bool _allowLoadFromSelection = true;
public TrafficInterlockViewer()
{
InitializeComponent();
if (LicenseManager.UsageMode == LicenseUsageMode.Designtime)
return;
try
{
RenderListView();
ClearPanelInputs();
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"TrafficInterlockViewer init error: {ex}");
}
}
#region
private void lstTasks_DrawColumnHeader(object sender, DrawListViewColumnHeaderEventArgs e)
{
try
{
// 与 LoopViewer 一致:深蓝表头 + 白色加粗字体
using (var backBrush = new SolidBrush(Color.FromArgb(63, 81, 181)))
using (var textBrush = new SolidBrush(Color.White))
using (var font = new Font("微软雅黑", 9, FontStyle.Bold))
{
e.Graphics.FillRectangle(backBrush, e.Bounds);
var sf = new StringFormat { LineAlignment = StringAlignment.Center, Alignment = StringAlignment.Near };
var rect = e.Bounds;
rect.Inflate(-8, 0);
e.Graphics.DrawString(e.Header.Text, font, textBrush, rect, sf);
using (var pen = new Pen(Color.FromArgb(200, 200, 200)))
e.Graphics.DrawLine(pen, e.Bounds.Left, e.Bounds.Bottom - 1, e.Bounds.Right, e.Bounds.Bottom - 1);
}
}
catch
{
e.DrawBackground();
e.DrawText();
}
}
private void lstTasks_DrawItem(object sender, DrawListViewItemEventArgs e)
{
// 由 DrawSubItem 统一绘制
}
private void lstTasks_DrawSubItem(object sender, DrawListViewSubItemEventArgs e)
{
try
{
bool selected = e.Item.Selected;
Rectangle bounds = e.Bounds;
// 与 LoopViewer 一致:选中行蓝色强调,交替行背景,深灰文字
Color selectedBack = Color.FromArgb(0, 120, 215);
Color selectedFore = Color.White;
Color evenBack = Color.White;
Color oddBack = Color.FromArgb(250, 251, 253);
Color normalFore = Color.FromArgb(33, 33, 33);
if (selected)
{
using (var selBrush = new SolidBrush(selectedBack))
e.Graphics.FillRectangle(selBrush, bounds);
}
else
{
using (var back = new SolidBrush(e.ItemIndex % 2 == 0 ? evenBack : oddBack))
e.Graphics.FillRectangle(back, bounds);
}
string text = e.SubItem?.Text ?? string.Empty;
Color fore = selected ? selectedFore : normalFore;
var textRect = bounds;
textRect.Inflate(-6, 0);
using (var font = new Font("微软雅黑", 9))
TextRenderer.DrawText(e.Graphics, text, font, textRect, fore, TextFormatFlags.Left | TextFormatFlags.VerticalCenter);
}
catch
{
e.DrawBackground();
e.DrawText();
}
}
#endregion
#region
private void RenderListView()
{
try
{
if (lstTasks == null) return;
_allowLoadFromSelection = false;
lstTasks.BeginUpdate();
lstTasks.Items.Clear();
foreach (var a in TrafficInterlockMission.TrafficAreaList)
{
var stationStr = a.SiteList != null && a.SiteList.Count > 0 ? string.Join(", ", a.SiteList) : "";
var lvi = new ListViewItem(new[]
{
a.AreaName ?? "",
stationStr,
a.ControllerName ?? "",
a.IsOccupy ? "是" : "否",
a.IsEnable ? "是" : "否"
});
lstTasks.Items.Add(lvi);
}
lstTasks.EndUpdate();
_allowLoadFromSelection = true;
}
catch (Exception ex)
{
_allowLoadFromSelection = true;
System.Diagnostics.Debug.WriteLine($"RenderListView failed: {ex}");
}
}
private void SaveToConfig()
{
try
{
string configPath = Path.Combine(Application.StartupPath, "Config", "traffic.json");
string dir = Path.GetDirectoryName(configPath);
if (!string.IsNullOrEmpty(dir) && !Directory.Exists(dir))
Directory.CreateDirectory(dir);
File.WriteAllText(configPath, JsonConvert.SerializeObject(TrafficInterlockMission.TrafficAreaList, Formatting.Indented));
}
catch (Exception ex)
{
MessageBox.Show("保存失败:" + ex.Message);
}
}
#endregion
#region
private void lstTasks_SelectedIndexChanged(object sender, EventArgs e)
{
if (!_allowLoadFromSelection || lstTasks == null || lstTasks.SelectedIndices.Count == 0) return;
int idx = lstTasks.SelectedIndices[0];
if (idx < 0 || idx >= TrafficInterlockMission.TrafficAreaList.Count) return;
_editingIndex = idx;
LoadAreaToPanel(TrafficInterlockMission.TrafficAreaList[idx]);
}
#endregion
#region /
private void LoadAreaToPanel(TrafficArea a)
{
if (a == null) return;
try
{
if (lblEditingHint != null)
lblEditingHint.Text = $"编辑:{a.AreaName}";
if (txtAreaName != null)
txtAreaName.Text = a.AreaName ?? "";
if (txtStationIds != null)
txtStationIds.Text = a.SiteList != null && a.SiteList.Count > 0
? string.Join(", ", a.SiteList)
: "";
if (txtControlRight != null)
txtControlRight.Text = a.ControllerName ?? "";
if (chkIsOccupied != null)
chkIsOccupied.Checked = a.IsOccupy;
if (chkIsEnabled != null)
chkIsEnabled.Checked = a.IsEnable;
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"LoadAreaToPanel error: {ex}");
}
}
private void ClearPanelInputs()
{
try
{
_editingIndex = -1;
if (lblEditingHint != null)
lblEditingHint.Text = "新增区域";
if (txtAreaName != null)
txtAreaName.Text = "";
if (txtStationIds != null)
txtStationIds.Text = "";
if (txtControlRight != null)
txtControlRight.Text = "";
if (chkIsOccupied != null)
chkIsOccupied.Checked = false;
if (chkIsEnabled != null)
chkIsEnabled.Checked = true;
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"ClearPanelInputs error: {ex}");
}
}
#endregion
#region "1,2,3" -> List<int>
private static List<int> ParseStationIds(string text)
{
var list = new List<int>();
if (string.IsNullOrWhiteSpace(text)) return list;
foreach (var part in text.Split(new[] { ',', ';', ' ', '\t' }, StringSplitOptions.RemoveEmptyEntries))
{
if (int.TryParse(part.Trim(), out int id))
list.Add(id);
}
return list;
}
#endregion
#region / / /
private void btnSave_Click(object sender, EventArgs e)
{
try
{
string areaName = txtAreaName?.Text?.Trim() ?? "";
if (string.IsNullOrEmpty(areaName))
{
MessageBox.Show("请输入区域名称。");
return;
}
var stationIds = ParseStationIds(txtStationIds?.Text ?? "");
string controlRight = txtControlRight?.Text?.Trim() ?? "";
bool isOccupied = chkIsOccupied?.Checked ?? false;
bool isEnabled = chkIsEnabled?.Checked ?? true;
if (_editingIndex >= 0 && _editingIndex < TrafficInterlockMission.TrafficAreaList.Count)
{
lock (TrafficInterlockMission.TrafficAreaList)
{
var existing = TrafficInterlockMission.TrafficAreaList[_editingIndex];
existing.AreaName = areaName;
existing.SiteList = stationIds;
existing.ControllerName = controlRight;
existing.IsOccupy = isOccupied;
existing.IsEnable = isEnabled;
}
}
else
{
lock (TrafficInterlockMission.TrafficAreaList)
{
TrafficInterlockMission.TrafficAreaList.Add(new TrafficArea
{
AreaName = areaName,
SiteList = stationIds,
ControllerName = controlRight,
IsOccupy = isOccupied,
IsEnable = isEnabled
});
}
}
SaveToConfig();
RenderListView();
ClearPanelInputs();
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"btnSave_Click error: {ex}");
MessageBox.Show("操作失败:" + ex.Message);
}
}
private void btnRefresh_Click(object sender, EventArgs e)
{
try
{
RenderListView();
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"btnRefresh_Click error: {ex}");
}
}
private void btnNew_Click(object sender, EventArgs e)
{
if (lstTasks != null)
lstTasks.SelectedIndices.Clear();
ClearPanelInputs();
// 进入新增模式:填写下方编辑区后点击“保存”即可新增一条数据
}
private void btnDelete_Click(object sender, EventArgs e)
{
try
{
if (lstTasks == null || lstTasks.SelectedIndices.Count == 0)
{
MessageBox.Show("请先在上方列表中选择要删除的区域。");
return;
}
var dialogResult = MessageBox.Show(
"确定要删除选中的区域吗?",
"确认删除",
MessageBoxButtons.YesNo,
MessageBoxIcon.Warning);
if (dialogResult != DialogResult.Yes)
return;
var indices = lstTasks.SelectedIndices.Cast<int>()
.OrderByDescending(i => i)
.ToList();
lock (TrafficInterlockMission.TrafficAreaList)
{
foreach (var idx in indices)
{
if (idx >= 0 && idx < TrafficInterlockMission.TrafficAreaList.Count)
{
TrafficInterlockMission.TrafficAreaList.RemoveAt(idx);
}
}
}
SaveToConfig();
RenderListView();
ClearPanelInputs();
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"btnDelete_Click error: {ex}");
MessageBox.Show("删除失败:" + ex.Message);
}
}
#endregion
}
}
@@ -0,0 +1,120 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
</root>