325 lines
9.9 KiB
C#
325 lines
9.9 KiB
C#
using System;
|
|||
|
|
using System.Collections.Generic;
|
||
|
|
using System.Globalization;
|
||
|
|
using System.IO;
|
||
|
|
using Newtonsoft.Json.Linq;
|
||
|
|
|
||
|
|
namespace StandardScene.MagCarSimulator
|
||
|
|
{
|
||
|
|
public sealed class SimpleLiteSite
|
||
|
|
{
|
||
|
|
public int Id { get; set; }
|
||
|
|
public string Name { get; set; }
|
||
|
|
public double X { get; set; }
|
||
|
|
public double Y { get; set; }
|
||
|
|
public int? TagValue { get; set; }
|
||
|
|
public bool NeedStop { get; set; }
|
||
|
|
public IReadOnlyDictionary<string, string> Fields { get; set; }
|
||
|
|
}
|
||
|
|
|
||
|
|
public sealed class SimpleLiteTrack
|
||
|
|
{
|
||
|
|
public int Id { get; set; }
|
||
|
|
public int SiteA { get; set; }
|
||
|
|
public int SiteB { get; set; }
|
||
|
|
public int Direction { get; set; }
|
||
|
|
}
|
||
|
|
|
||
|
|
/// <summary>解析 SimpleLite <c>maps/*.json</c> 的 Sites / Tracks。</summary>
|
||
|
|
public sealed class SimpleLiteMap
|
||
|
|
{
|
||
|
|
public const string NeedStopField = "Mag_NeedStop";
|
||
|
|
|
||
|
|
public string FilePath { get; private set; }
|
||
|
|
public string FileName { get; private set; }
|
||
|
|
public IReadOnlyDictionary<int, SimpleLiteSite> Sites { get; private set; }
|
||
|
|
public IReadOnlyList<SimpleLiteTrack> Tracks { get; private set; }
|
||
|
|
public IReadOnlyDictionary<int, IReadOnlyList<int>> Adjacency { get; private set; }
|
||
|
|
private readonly HashSet<int> _singleOccupancySites = new HashSet<int>();
|
||
|
|
|
||
|
|
public static SimpleLiteMap Load(string path, bool useTagValueAsNode)
|
||
|
|
{
|
||
|
|
if (string.IsNullOrWhiteSpace(path) || !File.Exists(path))
|
||
|
|
{
|
||
|
|
throw new FileNotFoundException("找不到 SimpleLite 地图文件", path);
|
||
|
|
}
|
||
|
|
|
||
|
|
var root = JObject.Parse(File.ReadAllText(path));
|
||
|
|
var sites = ParseSites(root["Sites"] as JObject);
|
||
|
|
var tracks = ParseTracks(root["Tracks"] as JObject);
|
||
|
|
var adjacency = BuildAdjacency(sites, tracks);
|
||
|
|
|
||
|
|
var map = new SimpleLiteMap
|
||
|
|
{
|
||
|
|
FilePath = Path.GetFullPath(path),
|
||
|
|
FileName = Path.GetFileName(path),
|
||
|
|
Sites = sites,
|
||
|
|
Tracks = tracks,
|
||
|
|
Adjacency = adjacency,
|
||
|
|
UseTagValueAsNode = useTagValueAsNode
|
||
|
|
};
|
||
|
|
map.ResetSingleOccupancyFromStops();
|
||
|
|
return map;
|
||
|
|
}
|
||
|
|
|
||
|
|
public bool UseTagValueAsNode { get; private set; }
|
||
|
|
|
||
|
|
public bool TryGetSite(int siteId, out SimpleLiteSite site)
|
||
|
|
{
|
||
|
|
return Sites.TryGetValue(siteId, out site);
|
||
|
|
}
|
||
|
|
|
||
|
|
public ushort ResolveNode(int siteId)
|
||
|
|
{
|
||
|
|
if (!Sites.TryGetValue(siteId, out var site))
|
||
|
|
{
|
||
|
|
return (ushort)Math.Clamp(siteId, 0, ushort.MaxValue);
|
||
|
|
}
|
||
|
|
|
||
|
|
if (UseTagValueAsNode && site.TagValue.HasValue && site.TagValue.Value >= 0)
|
||
|
|
{
|
||
|
|
return (ushort)Math.Clamp(site.TagValue.Value, 0, ushort.MaxValue);
|
||
|
|
}
|
||
|
|
|
||
|
|
return (ushort)Math.Clamp(site.Id, 0, ushort.MaxValue);
|
||
|
|
}
|
||
|
|
|
||
|
|
public bool NeedStop(int siteId)
|
||
|
|
{
|
||
|
|
return Sites.TryGetValue(siteId, out var site) && site.NeedStop;
|
||
|
|
}
|
||
|
|
|
||
|
|
/// <summary>停止点或交管点:同时只允许一辆。普通站可叠车。</summary>
|
||
|
|
public bool SingleOccupancy(int siteId)
|
||
|
|
{
|
||
|
|
return _singleOccupancySites.Contains(siteId);
|
||
|
|
}
|
||
|
|
|
||
|
|
public IReadOnlyList<int> ListSingleOccupancySiteIds()
|
||
|
|
{
|
||
|
|
var list = new List<int>(_singleOccupancySites);
|
||
|
|
list.Sort();
|
||
|
|
return list;
|
||
|
|
}
|
||
|
|
|
||
|
|
public void ResetSingleOccupancyFromStops()
|
||
|
|
{
|
||
|
|
_singleOccupancySites.Clear();
|
||
|
|
foreach (var site in Sites.Values)
|
||
|
|
{
|
||
|
|
if (site.NeedStop)
|
||
|
|
{
|
||
|
|
_singleOccupancySites.Add(site.Id);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
public void AddSingleOccupancySites(IEnumerable<int> siteIds)
|
||
|
|
{
|
||
|
|
if (siteIds == null)
|
||
|
|
{
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
foreach (var id in siteIds)
|
||
|
|
{
|
||
|
|
if (id > 0 && Sites.ContainsKey(id))
|
||
|
|
{
|
||
|
|
_singleOccupancySites.Add(id);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
public IReadOnlyList<int> ListNeedStopSiteIds()
|
||
|
|
{
|
||
|
|
var list = new List<int>();
|
||
|
|
foreach (var site in Sites.Values)
|
||
|
|
{
|
||
|
|
if (site.NeedStop)
|
||
|
|
{
|
||
|
|
list.Add(site.Id);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
list.Sort();
|
||
|
|
return list;
|
||
|
|
}
|
||
|
|
|
||
|
|
private static Dictionary<int, SimpleLiteSite> ParseSites(JObject sitesNode)
|
||
|
|
{
|
||
|
|
var result = new Dictionary<int, SimpleLiteSite>();
|
||
|
|
if (sitesNode == null)
|
||
|
|
{
|
||
|
|
return result;
|
||
|
|
}
|
||
|
|
|
||
|
|
foreach (var property in sitesNode.Properties())
|
||
|
|
{
|
||
|
|
if (property.Value is not JObject obj)
|
||
|
|
{
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
|
||
|
|
var id = obj.Value<int?>("id") ?? ParseInt(property.Name);
|
||
|
|
if (id <= 0)
|
||
|
|
{
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
|
||
|
|
var fields = ReadFields(obj["fields"] as JObject);
|
||
|
|
var site = new SimpleLiteSite
|
||
|
|
{
|
||
|
|
Id = id,
|
||
|
|
Name = obj.Value<string>("name") ?? "",
|
||
|
|
X = obj.Value<double?>("x") ?? 0,
|
||
|
|
Y = obj.Value<double?>("y") ?? 0,
|
||
|
|
TagValue = TryReadInt(fields, "TagValue"),
|
||
|
|
NeedStop = IsNeedStop(fields),
|
||
|
|
Fields = fields
|
||
|
|
};
|
||
|
|
result[id] = site;
|
||
|
|
}
|
||
|
|
|
||
|
|
return result;
|
||
|
|
}
|
||
|
|
|
||
|
|
private static List<SimpleLiteTrack> ParseTracks(JObject tracksNode)
|
||
|
|
{
|
||
|
|
var result = new List<SimpleLiteTrack>();
|
||
|
|
if (tracksNode == null)
|
||
|
|
{
|
||
|
|
return result;
|
||
|
|
}
|
||
|
|
|
||
|
|
foreach (var property in tracksNode.Properties())
|
||
|
|
{
|
||
|
|
if (property.Value is not JObject obj)
|
||
|
|
{
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
|
||
|
|
var siteA = obj.Value<int?>("siteA") ?? obj.Value<int?>("_siteA") ?? 0;
|
||
|
|
var siteB = obj.Value<int?>("siteB") ?? obj.Value<int?>("_siteB") ?? 0;
|
||
|
|
if (siteA <= 0 || siteB <= 0)
|
||
|
|
{
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
|
||
|
|
result.Add(new SimpleLiteTrack
|
||
|
|
{
|
||
|
|
Id = obj.Value<int?>("id") ?? ParseInt(property.Name),
|
||
|
|
SiteA = siteA,
|
||
|
|
SiteB = siteB,
|
||
|
|
Direction = obj.Value<int?>("direction") ?? 0
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
return result;
|
||
|
|
}
|
||
|
|
|
||
|
|
private static Dictionary<int, IReadOnlyList<int>> BuildAdjacency(
|
||
|
|
Dictionary<int, SimpleLiteSite> sites,
|
||
|
|
List<SimpleLiteTrack> tracks)
|
||
|
|
{
|
||
|
|
var mutable = new Dictionary<int, List<int>>();
|
||
|
|
foreach (var id in sites.Keys)
|
||
|
|
{
|
||
|
|
mutable[id] = new List<int>();
|
||
|
|
}
|
||
|
|
|
||
|
|
foreach (var track in tracks)
|
||
|
|
{
|
||
|
|
AddEdge(mutable, track.SiteA, track.SiteB, track.Direction);
|
||
|
|
}
|
||
|
|
|
||
|
|
var result = new Dictionary<int, IReadOnlyList<int>>();
|
||
|
|
foreach (var pair in mutable)
|
||
|
|
{
|
||
|
|
result[pair.Key] = pair.Value;
|
||
|
|
}
|
||
|
|
|
||
|
|
return result;
|
||
|
|
}
|
||
|
|
|
||
|
|
private static void AddEdge(Dictionary<int, List<int>> graph, int from, int to, int direction)
|
||
|
|
{
|
||
|
|
// 0 双向;1 仅 A→B;2 仅 B→A。其它值按双向处理。
|
||
|
|
var aToB = direction != 2;
|
||
|
|
var bToA = direction != 1;
|
||
|
|
if (aToB)
|
||
|
|
{
|
||
|
|
AddUnique(graph, from, to);
|
||
|
|
}
|
||
|
|
|
||
|
|
if (bToA)
|
||
|
|
{
|
||
|
|
AddUnique(graph, to, from);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
private static void AddUnique(Dictionary<int, List<int>> graph, int from, int to)
|
||
|
|
{
|
||
|
|
if (!graph.TryGetValue(from, out var list))
|
||
|
|
{
|
||
|
|
list = new List<int>();
|
||
|
|
graph[from] = list;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (!list.Contains(to))
|
||
|
|
{
|
||
|
|
list.Add(to);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
private static Dictionary<string, string> ReadFields(JObject fields)
|
||
|
|
{
|
||
|
|
var result = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||
|
|
if (fields == null)
|
||
|
|
{
|
||
|
|
return result;
|
||
|
|
}
|
||
|
|
|
||
|
|
foreach (var property in fields.Properties())
|
||
|
|
{
|
||
|
|
result[property.Name] = property.Value?.Type == JTokenType.Null
|
||
|
|
? ""
|
||
|
|
: property.Value.ToString();
|
||
|
|
}
|
||
|
|
|
||
|
|
return result;
|
||
|
|
}
|
||
|
|
|
||
|
|
private static bool IsNeedStop(Dictionary<string, string> fields)
|
||
|
|
{
|
||
|
|
if (!fields.TryGetValue(NeedStopField, out var raw))
|
||
|
|
{
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
raw = (raw ?? "").Trim();
|
||
|
|
return raw.Equals("true", StringComparison.OrdinalIgnoreCase)
|
||
|
|
|| raw.Equals("1", StringComparison.OrdinalIgnoreCase)
|
||
|
|
|| raw.Equals("yes", StringComparison.OrdinalIgnoreCase);
|
||
|
|
}
|
||
|
|
|
||
|
|
private static int? TryReadInt(Dictionary<string, string> fields, string key)
|
||
|
|
{
|
||
|
|
if (!fields.TryGetValue(key, out var raw) || string.IsNullOrWhiteSpace(raw))
|
||
|
|
{
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
|
||
|
|
return int.TryParse(raw, NumberStyles.Integer, CultureInfo.InvariantCulture, out var value)
|
||
|
|
? value
|
||
|
|
: (int?)null;
|
||
|
|
}
|
||
|
|
|
||
|
|
private static int ParseInt(string text)
|
||
|
|
{
|
||
|
|
return int.TryParse(text, NumberStyles.Integer, CultureInfo.InvariantCulture, out var value)
|
||
|
|
? value
|
||
|
|
: 0;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|