Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions Quantum.Debug/BankingProfileBrowserCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -101,10 +101,15 @@ private static string BuildHtml(
string outputHtmlPath,
JsonElement artifactRoot)
{
string? outputDirectory = Path.GetDirectoryName(outputHtmlPath);
string payloadPathBase = string.IsNullOrEmpty(outputDirectory)
? Environment.CurrentDirectory
: outputDirectory;

var payload = new BankingProfileBrowserPayload
{
SourcePath = ToDisplayPath(Path.GetRelativePath(Environment.CurrentDirectory, diagnosticsJsonPath)),
OutputPath = ToDisplayPath(Path.GetRelativePath(Environment.CurrentDirectory, outputHtmlPath)),
SourcePath = ToDisplayPath(Path.GetRelativePath(payloadPathBase, diagnosticsJsonPath)),
OutputPath = ToDisplayPath(Path.GetRelativePath(payloadPathBase, outputHtmlPath)),
Artifact = artifactRoot
};

Expand Down
72 changes: 72 additions & 0 deletions Quantum.IO/TrackLayout/V2/TrackLayoutPackageV2Json.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Text.Json;
using System.Text.Json.Serialization;

Expand Down Expand Up @@ -48,6 +50,76 @@ public static TrackLayoutPackageV2Dto Deserialize(string json)
return dto;
}

public static TrackLayoutPackageV2ImportResult Import(string json)
{
if (json == null)
{
throw new ArgumentNullException(nameof(json));
}

try
{
return TrackLayoutPackageV2Mapper.Import(Deserialize(json));
}
catch (JsonException ex)
{
return TrackLayoutPackageV2ImportResult.Failure(
new[]
{
new TrackLayoutPackageV2ValidationDiagnostic(
TrackLayoutPackageV2ValidationCode.MalformedJson,
"json",
CreateMalformedJsonMessage(ex))
});
}
}

private static string CreateMalformedJsonMessage(JsonException exception)
{
JsonException detail = exception.InnerException as JsonException ?? exception;
string message = exception.Message;
if (!ReferenceEquals(detail, exception))
{
message += " JSON parser detail: " + detail.Message;
}

string context = CreateJsonExceptionContext(detail);
if (context.Length != 0)
{
message += " " + context;
}

return message;
}

private static string CreateJsonExceptionContext(JsonException exception)
{
var parts = new List<string>();
if (!string.IsNullOrEmpty(exception.Path))
{
parts.Add("path '" + exception.Path + "'");
}

if (exception.LineNumber.HasValue)
{
parts.Add("line " + exception.LineNumber.Value.ToString(CultureInfo.InvariantCulture));
}

if (exception.BytePositionInLine.HasValue)
{
parts.Add(
"byte position " +
exception.BytePositionInLine.Value.ToString(CultureInfo.InvariantCulture));
}

if (parts.Count == 0)
{
return string.Empty;
}

return "Context: " + string.Join(", ", parts) + ".";
}

private static JsonSerializerOptions CreateOptions(bool indented)
{
return new JsonSerializerOptions
Expand Down
262 changes: 262 additions & 0 deletions Quantum.IO/TrackLayout/V2/TrackLayoutPackageV2Mapper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,262 @@
using System;
using System.Collections.Generic;
using Quantum.Math;
using Quantum.Track;
using Quantum.Track.Authoring;

namespace Quantum.IO.TrackLayout.V2
{
public sealed class TrackLayoutPackageV2ImportResult
{
public TrackLayoutPackageV2ImportResult(
bool success,
TrackAuthoringDefinition? definition,
HeartlineOffset? heartlineOffset,
IReadOnlyList<TrackLayoutPackageV2ValidationDiagnostic> diagnostics)
{
Success = success;
Definition = definition;
HeartlineOffset = heartlineOffset;
Diagnostics = diagnostics ?? throw new ArgumentNullException(nameof(diagnostics));
}

public bool Success { get; }

public TrackAuthoringDefinition? Definition { get; }

public HeartlineOffset? HeartlineOffset { get; }

public IReadOnlyList<TrackLayoutPackageV2ValidationDiagnostic> Diagnostics { get; }

internal static TrackLayoutPackageV2ImportResult Failure(
IReadOnlyList<TrackLayoutPackageV2ValidationDiagnostic> diagnostics)
{
return new TrackLayoutPackageV2ImportResult(false, null, null, diagnostics);
}
}

/// <summary>
/// Maps TrackLayoutPackageV2 DTOs into backend authoring definitions.
/// </summary>
public static class TrackLayoutPackageV2Mapper
{
public static TrackLayoutPackageV2ImportResult Import(TrackLayoutPackageV2Dto dto)
{
if (dto == null)
{
throw new ArgumentNullException(nameof(dto));
}

IReadOnlyList<TrackLayoutPackageV2ValidationDiagnostic> diagnostics =
TrackLayoutPackageV2Validator.Validate(dto);
if (diagnostics.Count != 0)
{
return TrackLayoutPackageV2ImportResult.Failure(diagnostics);
}

try
{
var sections = new List<GeometricSectionDefinition>(dto.Sections.Length);
for (int i = 0; i < dto.Sections.Length; i++)
{
sections.Add(MapSection(dto.Sections[i]));
}

TrackStartPose startPose = MapStartPose(dto.StartPose);
TrackBankingDefinition? banking = MapBanking(dto.Banking);
TrackAuthoringDefinition definition = banking == null
? new TrackAuthoringDefinition(sections, startPose)
: new TrackAuthoringDefinition(sections, startPose, banking);
HeartlineOffset? heartlineOffset = MapHeartline(dto.Heartline);

return new TrackLayoutPackageV2ImportResult(
true,
definition,
heartlineOffset,
Array.Empty<TrackLayoutPackageV2ValidationDiagnostic>());
}
catch (Exception ex) when (
ex is ArgumentException ||
ex is ArgumentOutOfRangeException ||
ex is InvalidOperationException ||
ex is NotSupportedException)
{
return TrackLayoutPackageV2ImportResult.Failure(
new[]
{
new TrackLayoutPackageV2ValidationDiagnostic(
TrackLayoutPackageV2ValidationCode.MappingFailed,
"dto",
"Validated TrackLayoutPackageV2 DTO could not be mapped; this likely indicates " +
"validator/mapper parity drift. Mapper detail: " + ex.Message)
});
}
}

private static GeometricSectionDefinition MapSection(TrackLayoutSectionV2Dto section)
{
switch (section.Kind)
{
case TrackLayoutPackageV2Vocabulary.StraightSectionKind:
return new StraightSectionDefinition(
section.Id,
section.Length,
section.RollRadians);

case TrackLayoutPackageV2Vocabulary.ConstantCurvatureSectionKind:
return new ConstantCurvatureSectionDefinition(
section.Id,
section.Length,
section.Radius!.Value,
section.RollRadians);

case TrackLayoutPackageV2Vocabulary.CurvatureTransitionSectionKind:
return new CurvatureTransitionSectionDefinition(
section.Id,
section.Length,
section.StartCurvature!.Value,
section.EndCurvature!.Value,
MapCurvatureInterpolation(section.InterpolationMode),
section.RollRadians);

case TrackLayoutPackageV2Vocabulary.SpatialSectionKind:
return new SpatialSectionDefinition(
section.Id,
section.Length,
MapVectors(section.ControlPoints!),
section.Degree!.Value,
section.Weights!,
section.RollRadians);

default:
throw new NotSupportedException(
"Unsupported TrackLayoutPackageV2 section kind '" + section.Kind + "'.");
}
}

private static TrackStartPose MapStartPose(TrackStartPoseV2Dto startPose)
{
return new TrackStartPose(
MapVector(startPose.Position),
MapVector(startPose.Tangent),
MapVector(startPose.Normal),
MapVector(startPose.Binormal));
}

private static TrackBankingDefinition? MapBanking(TrackBankingV2Dto? banking)
{
if (banking == null)
{
return null;
}

var keys = new BankingProfileKey[banking.Keys.Length];
for (int i = 0; i < banking.Keys.Length; i++)
{
TrackBankingKeyV2Dto key = banking.Keys[i];
keys[i] = new BankingProfileKey(
key.Distance,
key.RollRadians,
MapBankingInterpolation(key.InterpolationToNext));
}

return new TrackBankingDefinition(keys);
}

private static HeartlineOffset? MapHeartline(TrackHeartlineV2Dto? heartline)
{
if (heartline == null)
{
return null;
}

if (!string.Equals(
heartline.Kind,
TrackLayoutPackageV2Vocabulary.HeartlineKindConstantOffset,
StringComparison.Ordinal) ||
!string.Equals(
heartline.DistanceDomain,
TrackLayoutPackageV2Vocabulary.HeartlineDistanceDomainCenterlineStation,
StringComparison.Ordinal) ||
!string.Equals(
heartline.AxisSource,
TrackLayoutPackageV2Vocabulary.HeartlineAxisSourceSampledFrame,
StringComparison.Ordinal))
{
throw new NotSupportedException(
"Only constantOffset heartline offsets over centerlineStation using sampledFrame axes are supported.");
}

return new HeartlineOffset(heartline.NormalOffset, heartline.LateralOffset);
}

private static CurvatureTransitionInterpolationMode MapCurvatureInterpolation(
string? interpolationMode)
{
switch (interpolationMode)
{
case TrackLayoutPackageV2Vocabulary.CurvatureInterpolationLinear:
return CurvatureTransitionInterpolationMode.Linear;

default:
throw new ArgumentOutOfRangeException(
nameof(interpolationMode),
interpolationMode,
"Unsupported curvature transition interpolation mode.");
}
}

private static BankingProfileInterpolationMode MapBankingInterpolation(
string interpolationToNext)
{
switch (interpolationToNext)
{
case TrackLayoutPackageV2Vocabulary.BankingInterpolationConstant:
return BankingProfileInterpolationMode.Constant;

case TrackLayoutPackageV2Vocabulary.BankingInterpolationLinear:
return BankingProfileInterpolationMode.Linear;

case TrackLayoutPackageV2Vocabulary.BankingInterpolationSmoothStep:
return BankingProfileInterpolationMode.SmoothStep;

case TrackLayoutPackageV2Vocabulary.BankingInterpolationQuadratic:
return BankingProfileInterpolationMode.Quadratic;

case TrackLayoutPackageV2Vocabulary.BankingInterpolationCubic:
return BankingProfileInterpolationMode.Cubic;

case TrackLayoutPackageV2Vocabulary.BankingInterpolationQuartic:
return BankingProfileInterpolationMode.Quartic;

case TrackLayoutPackageV2Vocabulary.BankingInterpolationQuintic:
return BankingProfileInterpolationMode.Quintic;

case TrackLayoutPackageV2Vocabulary.BankingInterpolationSinusoidal:
return BankingProfileInterpolationMode.Sinusoidal;

default:
throw new ArgumentOutOfRangeException(
nameof(interpolationToNext),
interpolationToNext,
"Unsupported banking interpolation mode.");
}
}

private static Vector3d[] MapVectors(TrackLayoutVector3dV2Dto[] vectors)
{
var result = new Vector3d[vectors.Length];
for (int i = 0; i < vectors.Length; i++)
{
result[i] = MapVector(vectors[i]);
}

return result;
}

private static Vector3d MapVector(TrackLayoutVector3dV2Dto vector)
{
return new Vector3d(vector.X, vector.Y, vector.Z);
}
}
}
4 changes: 3 additions & 1 deletion Quantum.IO/TrackLayout/V2/TrackLayoutPackageV2Validator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,9 @@ public enum TrackLayoutPackageV2ValidationCode
InvalidBankingDomain = 24,
InvalidHeartlineKind = 25,
InvalidHeartlineDistanceDomain = 26,
InvalidHeartlineAxisSource = 27
InvalidHeartlineAxisSource = 27,
MalformedJson = 28,
MappingFailed = 29
}

public sealed class TrackLayoutPackageV2ValidationDiagnostic
Expand Down
7 changes: 6 additions & 1 deletion Quantum.Tests/Debug/BankingProfileBrowserCommandTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,12 @@ public void Run_ValidDiagnosticsJson_EmbedsParseableDiagnosticsJsonPayload()
Assert.Equal("Linear", samples[1].GetProperty("interpolationMode").GetString());
Assert.Equal("Constant", samples[3].GetProperty("interpolationMode").GetString());
Assert.True(samples[1].TryGetProperty("approximateRollSlopeRadPerMeter", out _));
Assert.Contains("banking-profile-diagnostics.sample.json", root.GetProperty("sourcePath").GetString());
Assert.Equal(
"banking-profile-diagnostics.sample.json",
root.GetProperty("sourcePath").GetString());
Assert.Equal(
"banking-profile.browser.html",
root.GetProperty("outputPath").GetString());
}
finally
{
Expand Down
Loading
Loading