EDSM: first version of Systems API

This commit is contained in:
alterNERDtive 2022-02-15 21:16:20 +01:00
parent 6f8144a7be
commit 301f21a42e
5 changed files with 632 additions and 9 deletions

View file

@ -59,6 +59,14 @@ namespace alterNERDtive.Edna
=> (this.X, this.Y, this.Z, this.Precision)
= ((int)edtsSystem.Position.X, (int)edtsSystem.Position.Y, (int)edtsSystem.Position.Z, (int)edtsSystem.Uncertainty);
/// <summary>
/// Initializes a new instance of the <see cref="Coordinates"/> struct.
/// </summary>
/// <param name="edsmSystem">An EDSM system to convert.</param>
public Coordinates(Edsm.ApiSystem edsmSystem)
=> (this.X, this.Y, this.Z, this.Precision)
= (edsmSystem.Coords.Value.X, edsmSystem.Coords.Value.Y, edsmSystem.Coords.Value.Z, 0);
/// <summary>
/// Gets the x coordinate.
/// </summary>

View file

@ -6,6 +6,7 @@
<ItemGroup>
<PackageReference Include="Microsoft.AspNet.WebApi.Client" Version="5.2.7" />
<PackageReference Include="RestSharp" Version="107.3.0" />
</ItemGroup>
<ItemGroup>

View file

@ -17,16 +17,33 @@
// along with EDNA. If not, see &lt;https://www.gnu.org/licenses/&gt;.
// </copyright>
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace EDSM
namespace Edsm
{
public class EdsmApi
/// <summary>
/// Coordinates represent a location in space.
/// </summary>
[System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1649:File name should match first type name", Justification = "Stupid rule :)")]
public struct Coordinates
{
private readonly string url = "https://www.edsm.net/api-";
/// <summary>
/// Gets or sets the X coordinate.
/// </summary>
public double X { get; set; }
/// <summary>
/// Gets or sets the Y coordinate.
/// </summary>
public double Y { get; set; }
/// <summary>
/// Gets or sets the Z coordinate.
/// </summary>
public double Z { get; set; }
/// <inheritdoc/>
public override string ToString()
{
return $"({this.X},{this.Y},{this.Z})";
}
}
}

404
EDSM/SystemsApi.cs Normal file
View file

@ -0,0 +1,404 @@
// <copyright file="SystemsApi.cs" company="alterNERDtive">
// Copyright 20212022 alterNERDtive.
//
// This file is part of EDNA.
//
// EDNA is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// EDNA is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with EDNA. If not, see &lt;https://www.gnu.org/licenses/&gt;.
// </copyright>
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using RestSharp;
namespace Edsm
{
/// <summary>
/// A star system in the galaxy of Elite Dangerous, as returned from EDSMs
/// “Systems” API.
/// </summary>
[System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1649:File name should match first type name", Justification = "Either this or wrong class/struct order 🤷")]
public struct ApiSystem
{
/// <summary>
/// Gets or sets the distance to the reference system. Only used by the
/// Sphere and Cube endpoints of the Systems API.
/// </summary>
public double Distance { get; set; }
/// <summary>
/// Gets or sets the body count of the star system. Only used by the
/// Sphere and Cube endpoints of the Systems API.
/// </summary>
public int? BodyCount { get; set; }
/// <summary>
/// Gets or sets the name of the star system.
/// </summary>
public string Name { get; set; }
/// <summary>
/// Gets or sets the EDSM ID of the star system.
/// </summary>
public int Id { get; set; }
/// <summary>
/// Gets or sets the ID64 of the star system.
/// </summary>
public ulong Id64 { get; set; }
/// <summary>
/// Gets or sets the location of the star system.
/// </summary>
public Coordinates? Coords { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the coordinates of the star
/// system are “locked”.
///
/// FIXXME: I _think_ that means they have been confirmed by multiple
/// people, but since its undocumented I am not sure.
/// </summary>
public bool CoordsLocked { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the star system requires
/// acquisition of a permit.
/// </summary>
public bool RequirePermit { get; set; }
/// <summary>
/// Gets or sets the name of the permit required to visit the star system.
/// </summary>
public string? PermitName { get; set; }
/// <summary>
/// Gets or sets general information about the star system.
/// </summary>
public SystemInformation? Information { get; set; }
/// <summary>
/// Gets or sets information about the primary star of the star system.
/// </summary>
public PrimaryStarInformation? PrimaryStar { get; set; }
/// <summary>
/// Information about the primary star of a star system.
/// </summary>
public struct PrimaryStarInformation
{
/// <summary>
/// Gets or sets the stars name.
/// </summary>
public string Name { get; set; }
/// <summary>
/// Gets or sets the stars star type (full text version).
/// </summary>
public string Type { get; set; }
/// <summary>
/// Gets or sets a value indicating whether or not the star is able
/// to be scooped for fuel.
/// </summary>
public bool IsScoopable { get; set; }
}
/// <summary>
/// General Information about a star system.
/// </summary>
public struct SystemInformation
{
/// <summary>
/// Gets or sets the star systems allegiance.
///
/// Can currently be Federation, Empire, Alliance, Independent, Thargoid.
/// </summary>
public string? Allegiance { get; set; }
/// <summary>
/// Gets or sets the star systems government type.
/// </summary>
public string? Government { get; set; }
/// <summary>
/// Gets or sets the star systems current controlling faction.
/// </summary>
public string? Faction { get; set; }
/// <summary>
/// Gets or sets the star systems current controlling factions state.
/// </summary>
public string? FactionState { get; set; }
/// <summary>
/// Gets or sets the star systems current population.
/// </summary>
public ulong? Population { get; set; }
/// <summary>
/// Gets or sets the star systems current security state.
/// </summary>
public string? Security { get; set; }
/// <summary>
/// Gets or sets the star systems primary economy.
/// </summary>
public string? Economy { get; set; }
/// <summary>
/// Gets or sets the star systems secondary economy.
/// </summary>
public string? SecondEconomy { get; set; }
/// <summary>
/// Gets or sets the star systems reserve level.
/// </summary>
public string? Reserve { get; set; }
}
}
/// <summary>
/// The SystemsApi class is used to pull information about a single or
/// multiple star systems from EDSMs “Systems” API.
///
/// See https://www.edsm.net/en/api-v1.
/// </summary>
public class SystemsApi
{
private static readonly Uri ApiUrl = new Uri("https://www.edsm.net/en/api-v1");
private static readonly RestClient ApiClient = new RestClient(ApiUrl)
.AddDefaultQueryParameter("showId", "1")
.AddDefaultQueryParameter("showCoordinates", "1")
.AddDefaultQueryParameter("showPermit", "1")
.AddDefaultQueryParameter("showInformation", "1")
.AddDefaultQueryParameter("showPrimaryStar", "1");
/// <summary>
/// Retrieves a single star system by name.
/// </summary>
/// <param name="name">The system name.</param>
/// <returns>The star system.</returns>
public static async Task<ApiSystem> FindSystem(string name)
{
RestResponse<ApiSystem> response = await ApiClient.ExecuteAsync<ApiSystem>(
new RestRequest("system").AddQueryParameter("systemName", name));
try
{
CheckResponseStatus(response);
}
catch (ArgumentException)
{
throw new ArgumentException($"System “{name}” not found.", nameof(name));
}
return response.Data;
}
/// <summary>
/// Retrieves multiple star systems by partial name.
/// </summary>
/// <param name="name">The partial name.</param>
/// <returns>A list of star systems beginning with the partial name.</returns>
public static async Task<IEnumerable<ApiSystem>> FindSystems(string name)
{
RestResponse<IEnumerable<ApiSystem>> response = await ApiClient.ExecuteAsync<IEnumerable<ApiSystem>>(
new RestRequest("systems").AddQueryParameter("systemName", name));
try
{
CheckResponseStatus(response);
}
catch (ArgumentException)
{
throw new ArgumentException($"No system found for partial name “{name}”.", nameof(name));
}
return response.Data;
}
/// <summary>
/// Retrieves the star systems to a given set of system names.
/// </summary>
/// <param name="names">The star systems in question.</param>
/// <returns>A <see cref="Task{TResult}"/> representing the result of the asynchronous operation.</returns>
public static async Task<IEnumerable<ApiSystem>> FindSystems(IEnumerable<string> names)
{
RestRequest request = new RestRequest("systems");
foreach (string name in names)
{
request.AddQueryParameter("systemName[]", name);
}
RestResponse<IEnumerable<ApiSystem>> response = await ApiClient.ExecuteAsync<IEnumerable<ApiSystem>>(request);
try
{
CheckResponseStatus(response);
}
catch (ArgumentException)
{
throw new ArgumentException($"No systems found for “{string.Join(", ", names)}”.", nameof(names));
}
return response.Data;
}
/// <summary>
/// Retrieves all star systems contained in a sphere around a given star
/// system.
/// </summary>
/// <param name="name">The name of the star system.</param>
/// <param name="minRadius">The minimum radius within which star systems are
/// excluded from the result set.</param>
/// <param name="radius">The radius of the search sphere.</param>
/// <returns>A <see cref="Task{TResult}"/> representing the result of the asynchronous operation.</returns>
public static async Task<IEnumerable<ApiSystem>> FindSystemsSphere(string name, int minRadius = 0, int radius = 50)
{
int max = 100;
if (radius > max)
{
throw new ArgumentException($"Radius cannot exceed {max}ly.", nameof(radius));
}
RestResponse<IEnumerable<ApiSystem>> response = await ApiClient.ExecuteAsync<IEnumerable<ApiSystem>>(
new RestRequest("sphere-systems")
.AddQueryParameter("systemName", name)
.AddQueryParameter("minRadius", minRadius)
.AddQueryParameter("radius", radius));
try
{
CheckResponseStatus(response);
}
catch (ArgumentException)
{
throw new ArgumentException($"No systems found within {radius}ly of “{name}”.");
}
return response.Data;
}
/// <summary>
/// Retrieves all star systems contained in a sphere around given coordinates.
/// </summary>
/// <param name="coords">The coordinates in question.</param>
/// <param name="minRadius">The minimum radius within which star systems
/// are excluded from the result set.</param>
/// <param name="radius">The radius of the search sphere.</param>
/// <returns>A <see cref="Task{TResult}"/> representing the result of the asynchronous operation.</returns>
public static async Task<IEnumerable<ApiSystem>> FindSystemsSphere(Coordinates coords, int minRadius = 0, int radius = 50)
{
int max = 100;
if (radius > max)
{
throw new ArgumentException($"Radius cannot exceed {max}ly.", nameof(radius));
}
RestResponse<IEnumerable<ApiSystem>> response = await ApiClient.ExecuteAsync<IEnumerable<ApiSystem>>(
new RestRequest("sphere-systems")
.AddQueryParameter("x", coords.X)
.AddQueryParameter("y", coords.Y)
.AddQueryParameter("z", coords.Z)
.AddQueryParameter("minRadius", minRadius)
.AddQueryParameter("radius", radius));
try
{
CheckResponseStatus(response);
}
catch (ArgumentException)
{
throw new ArgumentException($"No systems found within {radius}ly of {coords}.");
}
return response.Data;
}
/// <summary>
/// Retrivese all star systems contained in a cube centered on a given
/// star system.
/// </summary>
/// <param name="name">The name of the system.</param>
/// <param name="boundarySize">The boundary size for the cube.</param>
/// <returns>A <see cref="Task{TResult}"/> representing the result of the asynchronous operation.</returns>
public static async Task<IEnumerable<ApiSystem>> FindSystemsCube(string name, int boundarySize = 100)
{
int max = 200;
if (boundarySize > max)
{
throw new ArgumentException($"Boundary size cannot exceed {max}ly.", nameof(boundarySize));
}
RestResponse<IEnumerable<ApiSystem>> response = await ApiClient.ExecuteAsync<IEnumerable<ApiSystem>>(
new RestRequest("cube-systems")
.AddQueryParameter("systemName", name)
.AddQueryParameter("size", boundarySize));
try
{
CheckResponseStatus(response);
}
catch (ArgumentException)
{
throw new ArgumentException($"No systems found in a cube of {boundarySize}ly boundary size around “{name}”.");
}
return response.Data;
}
/// <summary>
/// Retrieves all star systems contained in a cube centered on a given
/// set of coordinates.
/// </summary>
/// <param name="coords">The coordinates in question.</param>
/// <param name="boundarySize">The boundary size for the cube.</param>
/// <returns>A <see cref="Task{TResult}"/> representing the result of the asynchronous operation.</returns>
public static async Task<IEnumerable<ApiSystem>> FindSystemsCube(Coordinates coords, int boundarySize = 100)
{
int max = 200;
if (boundarySize > max)
{
throw new ArgumentException($"Boundary size cannot exceed {max}ly.", nameof(boundarySize));
}
RestResponse<IEnumerable<ApiSystem>> response = await ApiClient.ExecuteAsync<IEnumerable<ApiSystem>>(
new RestRequest("cube-systems")
.AddQueryParameter("x", coords.X)
.AddQueryParameter("y", coords.Y)
.AddQueryParameter("z", coords.Z)
.AddQueryParameter("size", boundarySize));
try
{
CheckResponseStatus(response);
}
catch (ArgumentException)
{
throw new ArgumentException($"No systems found in a cube of {boundarySize}ly boundary size around {coords}.");
}
return response.Data;
}
private static void CheckResponseStatus(RestResponse response)
{
if (response.ResponseStatus != ResponseStatus.Completed)
{
if (response.ErrorException is System.Text.Json.JsonException && response.Content.Equals("[]"))
{
throw new ArgumentException();
}
else
{
throw response.ErrorException;
}
}
}
}
}

193
Test/EDSM/SystemsApiTest.cs Normal file
View file

@ -0,0 +1,193 @@
// <copyright file="SystemsApiTest.cs" company="alterNERDtive">
// Copyright 20212022 alterNERDtive.
//
// This file is part of EDNA.
//
// EDNA is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// EDNA is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with EDNA. If not, see &lt;https://www.gnu.org/licenses/&gt;.
// </copyright>
#pragma warning disable SA1615 // Element return value should be documented
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Xunit;
using Edsm;
namespace Test.EDSM
{
/// <summary>
/// Tests getting correct System data EDSMs Systems API.
/// </summary>
public class SystemsApiTest
{
/// <summary>
/// Tests getting correct System data from the System endpoint of EDSMs
/// Systems API.
/// </summary>
public class SystemEndpointTest
{
/// <summary>
/// Tests whether the Sol system can be retrieved correctly.
/// </summary>
[Fact]
public async Task SolData()
{
Edsm.ApiSystem sol = await Edsm.SystemsApi.FindSystem(name: "Sol");
Assert.Equal("Sol", sol.Name);
Assert.Equal<int>(27, sol.Id);
Assert.Equal<ulong>(10477373803, sol.Id64);
Edsm.Coordinates coords = sol.Coords.Value;
Assert.Equal<double>(0, coords.X);
Assert.Equal<double>(0, coords.Y);
Assert.Equal<double>(0, coords.Z);
Edsm.ApiSystem.SystemInformation systemInformation = sol.Information.Value;
Assert.Equal("Federation", systemInformation.Allegiance);
Assert.Equal("Democracy", systemInformation.Government);
Assert.Equal("Mother Gaia", systemInformation.Faction);
Assert.NotNull(systemInformation.FactionState);
Assert.Equal<ulong>(22780919531, systemInformation.Population.Value);
Assert.Equal("High", systemInformation.Security);
Assert.Equal("Refinery", systemInformation.Economy);
Assert.Equal("Service", systemInformation.SecondEconomy);
Assert.Equal("Common", systemInformation.Reserve);
Edsm.ApiSystem.PrimaryStarInformation starInformation = sol.PrimaryStar.Value;
Assert.Equal("G (White-Yellow) Star", starInformation.Type);
Assert.Equal("Sol", starInformation.Name);
Assert.True(starInformation.IsScoopable);
}
/// <summary>
/// Tests whether an invalid system name / system not in EDSM throws
/// the proper ArgumentException.
/// </summary>
[Fact]
public async Task InvalidSystemName()
{
await Assert.ThrowsAsync<ArgumentException>(() => Edsm.SystemsApi.FindSystem(name: "Soll"));
}
}
/// <summary>
/// Tests getting correct System data from the Systems endpoint of
/// EDSMs Systems API.
/// </summary>
public class SystemsEndpointTest
{
/// <summary>
/// Tests whether a partial system search for “Beagle Po” correctly
/// retrieves (only) the Beagle Point system.
/// </summary>
[Fact]
public async Task PartialBeaglePoint()
{
List<Edsm.ApiSystem> systems = (await Edsm.SystemsApi.FindSystems(name: "Beagle Po")).ToList();
Assert.Single(systems);
Assert.Equal("Beagle Point", systems.First().Name);
}
/// <summary>
/// Tests retrieving Sol and Beagle, with a random invalid system in
/// there that the API will silently ignore.
/// </summary>
[Fact]
public async Task SolAndBeaglePoint()
{
List<Edsm.ApiSystem> systems = (await Edsm.SystemsApi.FindSystems(names: new[] { "Sol", "Beagle Point", "Random Invalid Name" })).ToList();
Assert.Equal<int>(2, systems.Count);
systems.Find(x => x.Name == "Sol");
systems.Find(x => x.Name == "Beagle Point");
}
}
/// <summary>
/// Tests getting correct star system data from the Sphere-systems
/// endpoint of EDSMs Systems API.
/// </summary>
public class SphereSystemsEndpointTest
{
/// <summary>
/// Tests whether a sphere search between 9 and 10ly around Sol
/// correctly returns the 3 systems in that section of the sphere.
/// </summary>
[Fact]
public async Task SphereAroundSol()
{
List<Edsm.ApiSystem> systems = (await Edsm.SystemsApi.FindSystemsSphere(name: "Sol", minRadius: 9, radius: 10)).ToList();
Assert.Equal<int>(3, systems.Count);
systems.Find(x => x.Name == "Duamta" && x.Distance == 9.88 && x.BodyCount == 12);
systems.Find(x => x.Name == "Ross 154" && x.Distance == 9.69 && x.BodyCount == 9);
systems.Find(x => x.Name == "Yin Sector CL-Y d127" && x.Distance == 9.86 && x.BodyCount == 2);
}
/// <summary>
/// Tests whether a sphere between of 30ly around (1000,1000,1000)
/// correctly returns the star systems in that sphere.
/// </summary>
[Fact]
public async Task SphereAround1000()
{
List<Edsm.ApiSystem> systems =
(await Edsm.SystemsApi.FindSystemsSphere(new Edsm.Coordinates { X = 1000, Y = 1000, Z = 1000 }, minRadius: 20, radius: 30)).ToList();
Assert.Single(systems);
Assert.Equal("Praea Euq ZK-M d8-3", systems.First().Name);
systems = (await Edsm.SystemsApi.FindSystemsSphere(new Edsm.Coordinates { X = 1000, Y = 1000, Z = 1000 }, radius: 30)).ToList();
Assert.Equal(2, systems.Count);
systems.Find(x => x.Name == "Praea Euq ZK-M d8-3");
systems.Find(x => x.Name == "Praea Euq AH-X c17-0");
}
}
/// <summary>
/// Test getting correct star system data from the Cube-systems endpoint
/// of EDSMs Systems API.
/// </summary>
public class CubeSystemsEndpointTest
{
/// <summary>
/// Tests whether a cube search of boundary size 10ly around Sol
/// correctly returns the 3 systems is that cube.
/// </summary>
[Fact]
public async Task CubeAroundSol()
{
List<Edsm.ApiSystem> systems = (await Edsm.SystemsApi.FindSystemsCube(name: "Sol", boundarySize: 10)).ToList();
Assert.Equal<int>(3, systems.Count);
systems.Find(x => x.Name == "Barnard's Star" && x.Distance == 5.95 && x.BodyCount == 16);
systems.Find(x => x.Name == "Sol" && x.Distance == 0 && x.BodyCount == 40);
systems.Find(x => x.Name == "Alpha Centauri" && x.Distance == 4.38 && x.BodyCount == 9);
}
/// <summary>
/// Tests whether a cube of boundary size 42 centered around
/// (1000,1000,1000) correctly returns the star systems in that cube.
/// </summary>
[Fact]
public async Task CubeAround1000()
{
List<Edsm.ApiSystem> systems =
(await Edsm.SystemsApi.FindSystemsCube(new Edsm.Coordinates { X = 1000, Y = 1000, Z = 1000 }, boundarySize: 42)).ToList();
Assert.Equal(2, systems.Count);
systems.Find(x => x.Name == "Praea Euq ZK-M d8-3");
systems.Find(x => x.Name == "Praea Euq AH-X c17-0");
}
}
}
}