using System.Globalization;
using System.Text.Json;
using XFEEDToolBox.Core.Models;
using XFEEDToolBox.Core.Navigation;
namespace XFEEDToolBox.Core.Galaxy;
public sealed record SystemMapCatalogue(GalacticSystem System,CurrentSystemSnapshot Preview,IReadOnlyDictionary<string,Station> Stations)
{
public const string PreviewDescription="公开目录示意 · 非实时位置 · 抵达后核对游戏坐标";
}
/// <summary>One selected system only. Public catalogue positions are presentation data and never navigation coordinates.</summary>
public sealed class SystemMapCatalogueClient(HttpClient http)
{
public async Task<SystemMapCatalogue> ReadAsync(GalacticSystem system,CancellationToken token=default)
{
var query=Uri.EscapeDataString(system.Name);
var stationsTask=ReadJsonAsync("stations",query,token);
var bodiesTask=ReadJsonAsync("bodies",query,token);
await Task.WhenAll(stationsTask,bodiesTask);
using var stations=await stationsTask;using var bodies=await bodiesTask;
return Parse(system,bodies.RootElement,stations.RootElement);
}
private async Task<JsonDocument> ReadJsonAsync(string kind,string query,CancellationToken token)
{
// EDSM System API: https://www.edsm.net/en/api-system-v1 . No region or database dump is requested.
using var response=await http.GetAsync($"https://www.edsm.net/api-system-v1/{kind}?systemName={query}",HttpCompletionOption.ResponseHeadersRead,token);
response.EnsureSuccessStatusCode();
await using var stream=await response.Content.ReadAsStreamAsync(token);using var buffer=new MemoryStream();
var block=new byte[16384];int count;
while((count=await stream.ReadAsync(block,token))!=0)
{if(buffer.Length+count>16*1024*1024)throw new InvalidDataException("本地星系目录响应超出大小限制。");await buffer.WriteAsync(block.AsMemory(0,count),token);}
return JsonDocument.Parse(buffer.ToArray());
}
public static SystemMapCatalogue Parse(GalacticSystem system,JsonElement bodies,JsonElement stations)
{
string Text(JsonElement row,string key)=>row.ValueKind==JsonValueKind.Object&&row.TryGetProperty(key,out var value)&&value.ValueKind==JsonValueKind.String?value.GetString()??"":"";
long Number(JsonElement row,string key)=>row.ValueKind==JsonValueKind.Object&&row.TryGetProperty(key,out var value)&&value.ValueKind==JsonValueKind.Number&&value.TryGetInt64(out var number)?number:0;
double? Decimal(JsonElement row,string key)=>row.ValueKind==JsonValueKind.Object&&row.TryGetProperty(key,out var value)&&value.ValueKind==JsonValueKind.Number&&value.TryGetDouble(out var number)&&double.IsFinite(number)&&number>=0?number:null;
void RequireSystem(JsonElement root)
{
if(!Text(root,"name").Equals(system.Name,StringComparison.OrdinalIgnoreCase))throw new InvalidDataException("公开目录返回的星系名称不一致。");
var address=Number(root,"id64");if(system.SystemAddress is >0&&address>0&&address!=system.SystemAddress)throw new InvalidDataException("公开目录返回的星系身份不一致。");
}
RequireSystem(bodies);RequireSystem(stations);
var systemAddress=system.SystemAddress??Number(bodies,"id64");
if(systemAddress<=0)systemAddress=Number(stations,"id64");
if(systemAddress<=0)throw new InvalidDataException("本地星系目录缺少星系身份。");
var rows=new List<SystemLocation>();var bodyPositions=new Dictionary<string,Vector3d>(StringComparer.OrdinalIgnoreCase);
// A stable catalogue diagram deliberately supplies no orbit or native world identity.
// Unknown orbital planes/epochs must never be labelled as a real-time system map.
var index=0;
if(bodies.TryGetProperty("bodies",out var bodyArray)&&bodyArray.ValueKind==JsonValueKind.Array)
foreach(var body in bodyArray.EnumerateArray())
{
var name=Text(body,"name");if(name.Length==0)continue;
var star=Text(body,"type")=="Star";var landable=body.TryGetProperty("isLandable",out var land)&&land.ValueKind==JsonValueKind.True;
var distance=Decimal(body,"distanceToArrival");var phase=index*2.399963229728653;
var radius=index==0&&star?0:1+Math.Log10(1+(distance??index*20));
var position=new Vector3d(Math.Cos(phase)*radius,Math.Sin(phase*.7)*radius*.25,Math.Sin(phase)*radius)*1e9;
var identity=$"catalogue-body:{systemAddress}:{Number(body,"id")}";
rows.Add(new(index++,0,name,Text(body,"subType"),star?1:3,star?SystemLocationCategory.Star:landable?SystemLocationCategory.LandablePlanet:SystemLocationCategory.Planet,
null,position,distance*299792458,false,identity));bodyPositions.TryAdd(name,position);
}
var stationMap=new Dictionary<string,Station>(StringComparer.Ordinal);
if(stations.TryGetProperty("stations",out var stationArray)&&stationArray.ValueKind==JsonValueKind.Array)
foreach(var row in stationArray.EnumerateArray())
{
var name=Text(row,"name");var market=Number(row,"marketId");if(name.Length==0||market<=0)continue;
var type=Text(row,"type");var compact=string.Concat(type.Where(char.IsLetterOrDigit)).ToLowerInvariant();
var planetary=compact.Contains("planetary")||compact.Contains("settlement");var carrier=compact.Contains("carrier");
var pads=row.TryGetProperty("maxLandingPad",out var pad)&&pad.ValueKind==JsonValueKind.String?pad.GetString():null;
var maxPad=pads?.ToLowerInvariant() switch{"large"=>LandingPad.Large,"medium"=>LandingPad.Medium,"small"=>LandingPad.Small,_=>LandingPad.Unknown};
// These original station forms have fixed large docks; unknown forms stay unknown.
if(maxPad==LandingPad.Unknown&&compact is "coriolisstarport" or "orbisstarport" or "ocellusstarport" or "asteroidbase" or "fleetcarrier")maxPad=LandingPad.Large;
if(maxPad==LandingPad.Unknown&&compact=="outpost")maxPad=LandingPad.Medium;
var at=DateTimeOffset.MinValue;
if(row.TryGetProperty("updateTime",out var update))DateTimeOffset.TryParse(Text(update,"information"),CultureInfo.InvariantCulture,DateTimeStyles.AssumeUniversal,out at);
var station=new Station{MarketId=market,Name=name,SystemName=system.Name,Position=system.Position,Type=type,IsPlanetary=planetary,IsFleetCarrier=carrier,
DistanceToArrivalLs=Decimal(row,"distanceToArrival"),MaxLandingPad=maxPad,UpdatedAt=at};
var identity=$"catalogue-station:{systemAddress}:{market}";
if(stationMap.ContainsKey(identity))throw new InvalidDataException("本地星系目录包含重复市场身份。");
stationMap.Add(identity,station);
var phase=index*2.399963229728653;var radius=1+Math.Log10(1+(station.DistanceToArrivalLs??20));
var position=new Vector3d(Math.Cos(phase)*radius,0,Math.Sin(phase)*radius)*1e9;
if(row.TryGetProperty("body",out var parent)&&bodyPositions.TryGetValue(Text(parent,"name"),out var parentPoint))
position=parentPoint+new Vector3d(Math.Cos(phase),.2,Math.Sin(phase))*2e8;
rows.Add(new(index++,0,name,type,0,carrier?SystemLocationCategory.Carrier:planetary?SystemLocationCategory.Settlement:SystemLocationCategory.Station,
market,position,station.DistanceToArrivalLs*299792458,false,identity));
}
return new(system,new(systemAddress,system.Name,0,0,DateTimeOffset.UtcNow,rows){DisplayOnly=true},stationMap);
}
}
using System.Globalization;
using System.Text.Json;
using XFEEDToolBox.Core.Models;
using XFEEDToolBox.Core.Navigation;
namespace XFEEDToolBox.Core.Galaxy;
public sealed record SystemMapCatalogue(GalacticSystem System,CurrentSystemSnapshot Preview,IReadOnlyDictionary<string,Station> Stations)
{
public const string PreviewDescription="公开目录示意 · 非实时位置 · 抵达后核对游戏坐标";
}
/// <summary>One selected system only. Public catalogue positions are presentation data and never navigation coordinates.</summary>
public sealed class SystemMapCatalogueClient(HttpClient http)
{
public async Task<SystemMapCatalogue> ReadAsync(GalacticSystem system,CancellationToken token=default)
{
var query=Uri.EscapeDataString(system.Name);
var stationsTask=ReadJsonAsync("stations",query,token);
var bodiesTask=ReadJsonAsync("bodies",query,token);
await Task.WhenAll(stationsTask,bodiesTask);
using var stations=await stationsTask;using var bodies=await bodiesTask;
return Parse(system,bodies.RootElement,stations.RootElement);
}
private async Task<JsonDocument> ReadJsonAsync(string kind,string query,CancellationToken token)
{
// EDSM System API: https://www.edsm.net/en/api-system-v1 . No region or database dump is requested.
using var response=await http.GetAsync($"https://www.edsm.net/api-system-v1/{kind}?systemName={query}",HttpCompletionOption.ResponseHeadersRead,token);
response.EnsureSuccessStatusCode();
await using var stream=await response.Content.ReadAsStreamAsync(token);using var buffer=new MemoryStream();
var block=new byte[16384];int count;
while((count=await stream.ReadAsync(block,token))!=0)
{if(buffer.Length+count>16*1024*1024)throw new InvalidDataException("本地星系目录响应超出大小限制。");await buffer.WriteAsync(block.AsMemory(0,count),token);}
return JsonDocument.Parse(buffer.ToArray());
}
public static SystemMapCatalogue Parse(GalacticSystem system,JsonElement bodies,JsonElement stations)
{
string Text(JsonElement row,string key)=>row.ValueKind==JsonValueKind.Object&&row.TryGetProperty(key,out var value)&&value.ValueKind==JsonValueKind.String?value.GetString()??"":"";
long Number(JsonElement row,string key)=>row.ValueKind==JsonValueKind.Object&&row.TryGetProperty(key,out var value)&&value.ValueKind==JsonValueKind.Number&&value.TryGetInt64(out var number)?number:0;
double? Decimal(JsonElement row,string key)=>row.ValueKind==JsonValueKind.Object&&row.TryGetProperty(key,out var value)&&value.ValueKind==JsonValueKind.Number&&value.TryGetDouble(out var number)&&double.IsFinite(number)&&number>=0?number:null;
void RequireSystem(JsonElement root)
{
if(!Text(root,"name").Equals(system.Name,StringComparison.OrdinalIgnoreCase))throw new InvalidDataException("公开目录返回的星系名称不一致。");
var address=Number(root,"id64");if(system.SystemAddress is >0&&address>0&&address!=system.SystemAddress)throw new InvalidDataException("公开目录返回的星系身份不一致。");
}
RequireSystem(bodies);RequireSystem(stations);
var systemAddress=system.SystemAddress??Number(bodies,"id64");
if(systemAddress<=0)systemAddress=Number(stations,"id64");
if(systemAddress<=0)throw new InvalidDataException("本地星系目录缺少星系身份。");
var rows=new List<SystemLocation>();var bodyPositions=new Dictionary<string,Vector3d>(StringComparer.OrdinalIgnoreCase);
// A stable catalogue diagram deliberately supplies no orbit or native world identity.
// Unknown orbital planes/epochs must never be labelled as a real-time system map.
var index=0;
if(bodies.TryGetProperty("bodies",out var bodyArray)&&bodyArray.ValueKind==JsonValueKind.Array)
foreach(var body in bodyArray.EnumerateArray())
{
var name=Text(body,"name");if(name.Length==0)continue;
var star=Text(body,"type")=="Star";var landable=body.TryGetProperty("isLandable",out var land)&&land.ValueKind==JsonValueKind.True;
var distance=Decimal(body,"distanceToArrival");var phase=index*2.399963229728653;
var radius=index==0&&star?0:1+Math.Log10(1+(distance??index*20));
var position=new Vector3d(Math.Cos(phase)*radius,Math.Sin(phase*.7)*radius*.25,Math.Sin(phase)*radius)*1e9;
var identity=$"catalogue-body:{systemAddress}:{Number(body,"id")}";
rows.Add(new(index++,0,name,Text(body,"subType"),star?1:3,star?SystemLocationCategory.Star:landable?SystemLocationCategory.LandablePlanet:SystemLocationCategory.Planet,
null,position,distance*299792458,false,identity));bodyPositions.TryAdd(name,position);
}
var stationMap=new Dictionary<string,Station>(StringComparer.Ordinal);
if(stations.TryGetProperty("stations",out var stationArray)&&stationArray.ValueKind==JsonValueKind.Array)
foreach(var row in stationArray.EnumerateArray())
{
var name=Text(row,"name");var market=Number(row,"marketId");if(name.Length==0||market<=0)continue;
var type=Text(row,"type");var compact=string.Concat(type.Where(char.IsLetterOrDigit)).ToLowerInvariant();
var planetary=compact.Contains("planetary")||compact.Contains("settlement");var carrier=compact.Contains("carrier");
var pads=row.TryGetProperty("maxLandingPad",out var pad)&&pad.ValueKind==JsonValueKind.String?pad.GetString():null;
var maxPad=pads?.ToLowerInvariant() switch{"large"=>LandingPad.Large,"medium"=>LandingPad.Medium,"small"=>LandingPad.Small,_=>LandingPad.Unknown};
// These original station forms have fixed large docks; unknown forms stay unknown.
if(maxPad==LandingPad.Unknown&&compact is "coriolisstarport" or "orbisstarport" or "ocellusstarport" or "asteroidbase" or "fleetcarrier")maxPad=LandingPad.Large;
if(maxPad==LandingPad.Unknown&&compact=="outpost")maxPad=LandingPad.Medium;
var at=DateTimeOffset.MinValue;
if(row.TryGetProperty("updateTime",out var update))DateTimeOffset.TryParse(Text(update,"information"),CultureInfo.InvariantCulture,DateTimeStyles.AssumeUniversal,out at);
var station=new Station{MarketId=market,Name=name,SystemName=system.Name,Position=system.Position,Type=type,IsPlanetary=planetary,IsFleetCarrier=carrier,
DistanceToArrivalLs=Decimal(row,"distanceToArrival"),MaxLandingPad=maxPad,UpdatedAt=at};
var identity=$"catalogue-station:{systemAddress}:{market}";
if(stationMap.ContainsKey(identity))throw new InvalidDataException("本地星系目录包含重复市场身份。");
stationMap.Add(identity,station);
var phase=index*2.399963229728653;var radius=1+Math.Log10(1+(station.DistanceToArrivalLs??20));
var position=new Vector3d(Math.Cos(phase)*radius,0,Math.Sin(phase)*radius)*1e9;
if(row.TryGetProperty("body",out var parent)&&bodyPositions.TryGetValue(Text(parent,"name"),out var parentPoint))
position=parentPoint+new Vector3d(Math.Cos(phase),.2,Math.Sin(phase))*2e8;
rows.Add(new(index++,0,name,type,0,carrier?SystemLocationCategory.Carrier:planetary?SystemLocationCategory.Settlement:SystemLocationCategory.Station,
market,position,station.DistanceToArrivalLs*299792458,false,identity));
}
return new(system,new(systemAddress,system.Name,0,0,DateTimeOffset.UtcNow,rows){DisplayOnly=true},stationMap);
}
}