diff --git a/.devops/config.ps1 b/.devops/config.ps1 deleted file mode 100644 index 34e1621..0000000 --- a/.devops/config.ps1 +++ /dev/null @@ -1,42 +0,0 @@ -$devopsExtensions = @( - @{ - Name = "Endjin.RecommendedPractices.Build" - Version = "[1.5.10,2.0)" - Process = "tasks/build.process.ps1" - } -) - -# Load the tasks and process -. endjin-devops.tasks - -# -# Build process configuration -# -$SolutionToBuild = (Resolve-Path (Join-Path $here "./Solutions/Ais.Net.Models.sln")).Path -$SkipBuildModuleVersionCheck = $true # currently doesn't work properly with endjin-devops - -# Set default build task -task . FullBuild - -# -# Build Process Extensibility Points - uncomment and implement as required -# - -# task RunFirst {} -# task PreInit {} -# task PostInit {} -# task PreVersion {} -# task PostVersion {} -# task PreBuild {} -# task PostBuild {} -# task PreTest {} -# task PostTest {} -# task PreTestReport {} -# task PostTestReport {} -# task PreAnalysis {} -# task PostAnalysis {} -# task PrePackage {} -# task PostPackage {} -# task PrePublish {} -# task PostPublish {} -# task RunLast {} \ No newline at end of file diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..98909cf --- /dev/null +++ b/.gitattributes @@ -0,0 +1,50 @@ +# Auto detect text files and perform LF normalization +* text=auto + +# Explicitly declare text files you want to always be normalized and converted +# to native line endings on checkout. +*.cs text +*.csx text +*.csproj text +*.sln text +*.xaml text +*.config text +*.xml text +*.json text +*.md text +*.txt text +*.yml text +*.yaml text +*.sh text eol=lf +*.ps1 text eol=crlf +*.cmd text eol=crlf +*.bat text eol=crlf + +# Declare files that will always have LF line endings on checkout. +.gitattributes text eol=lf +.gitignore text eol=lf +*.sh text eol=lf + +# Declare files that will always have CRLF line endings on checkout. +*.sln text eol=crlf +*.slnx text eol=crlf +*.cmd text eol=crlf +*.bat text eol=crlf +*.ps1 text eol=crlf + +# Denote all files that are truly binary and should not be modified. +*.png binary +*.jpg binary +*.jpeg binary +*.gif binary +*.ico binary +*.dll binary +*.exe binary +*.pdb binary +*.zip binary +*.gz binary +*.tar binary +*.7z binary +*.nupkg binary +*.snk binary +*.pfx binary \ No newline at end of file diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 1354ad0..85444a4 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,58 +1,98 @@ -name: build -on: - push: - branches: - - main - tags: - - '*' - pull_request: - branches: - - main - workflow_dispatch: - inputs: - forcePublish: - description: When true the Publish stage will always be run, otherwise it only runs for tagged versions. - required: false - default: false - type: boolean - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -permissions: - checks: write # enable test result annotations - contents: write # enable creating releases - issues: read - packages: write # enable publishing packages - pull-requests: write # enable test result annotations - -jobs: - prepareConfig: - name: Prepare Configuration - runs-on: ubuntu-latest - outputs: - RESOLVED_ENV_VARS: ${{ steps.prepareEnvVarsAndSecrets.outputs.environmentVariablesYamlBase64 }} - RESOLVED_SECRETS: ${{ steps.prepareEnvVarsAndSecrets.outputs.secretsYamlBase64 }} - steps: - # Declare any environment variables and/or secrets that need to be available inside the build process - - uses: endjin/Endjin.RecommendedPractices.GitHubActions/actions/prepare-env-vars-and-secrets@main - id: prepareEnvVarsAndSecrets - with: - environmentVariablesYaml: | - BUILDVAR_NuGetPublishSource: "${{ startsWith(github.ref, 'refs/tags/') && 'https://api.nuget.org/v3/index.json' || 'https://nuget.pkg.github.com/ais-dotnet/index.json' }}" - BUILDVAR_Configuration: "Release" - secretsYaml: | - NUGET_API_KEY: "${{ startsWith(github.ref, 'refs/tags/') && secrets.NUGET_APIKEY || secrets.BUILD_PUBLISHER_PAT }}" - - build: - needs: prepareConfig - uses: endjin/Endjin.RecommendedPractices.GitHubActions/.github/workflows/scripted-build-single-job-pipeline.yml@main - with: - netSdkVersion: '8.0.x' - # workflow_dispatch inputs are always strings, the type property is just for the UI - forcePublish: ${{ github.event.inputs.forcePublish == 'true' }} - buildEnv: ${{ needs.prepareConfig.outputs.RESOLVED_ENV_VARS }} - secrets: - buildAzureCredentials: ${{ secrets.AZURE_READER_CREDENTIALS }} - buildSecrets: ${{ needs.prepareConfig.outputs.RESOLVED_SECRETS }} +name: build +on: + push: + branches: + - main + tags: + - '*' + pull_request: + branches: + - main + workflow_dispatch: + inputs: + forcePublish: + description: When true the Publish stage will always be run, otherwise it only runs for tagged versions. + required: false + default: false + type: boolean + forcePublicNugetDestination: + description: When true the NuGet Publish destination will always be set to nuget.org. (Normally, force publishing publishes to the private feed on GitHub.) + required: false + default: false + type: boolean + skipCleanup: + description: When true the pipeline clean-up stage will not be run. For example, the cache used between pipeline stages will be retained. + required: false + default: false + type: boolean + +concurrency: + group: ${{ github.workflow }}-${{ github.sha }} + cancel-in-progress: true + +permissions: + actions: write # enable cache clean-up + checks: write # enable test result annotations + contents: write # enable creating releases + issues: read + packages: write # enable publishing packages + pull-requests: write # enable test result annotations + +jobs: + prepareConfig: + name: Prepare Configuration + runs-on: ubuntu-latest + outputs: + RESOLVED_ENV_VARS: ${{ steps.prepareEnvVarsAndSecrets.outputs.environmentVariablesYamlBase64 }} + RESOLVED_SECRETS: ${{ steps.prepareEnvVarsAndSecrets.outputs.secretsYamlBase64 }} + steps: + # Declare any environment variables and/or secrets that need to be available inside the build process + - uses: endjin/Endjin.RecommendedPractices.GitHubActions/actions/prepare-env-vars-and-secrets@main + id: prepareEnvVarsAndSecrets + with: + environmentVariablesYaml: | + ZF_NUGET_PUBLISH_SOURCE: ${{ (startsWith(github.ref, 'refs/tags/') || github.event.inputs.forcePublicNugetDestination == 'true') && 'https://api.nuget.org/v3/index.json' || format('https://nuget.pkg.github.com/{0}/index.json', github.repository_owner) }} + secretsYaml: | + NUGET_API_KEY: "${{ startsWith(github.ref, 'refs/tags/') && secrets.NUGET_APIKEY || secrets.BUILD_PUBLISHER_PAT }}" + secretsEncryptionKey: ${{ secrets.SHARED_WORKFLOW_KEY }} + + build: + needs: prepareConfig + uses: endjin/Endjin.RecommendedPractices.GitHubActions/.github/workflows/scripted-build-matrix-pipeline.yml@main + with: + netSdkVersion: '10.x' + # Install the .NET 8 SDK too so the net8.0 targeting pack/runtime is available for + # building, testing and packing the multi-targeted (net8.0;net10.0) projects. + additionalNetSdkVersion: '8.x' + # workflow_dispatch inputs are always strings, the type property is just for the UI + forcePublish: ${{ github.event.inputs.forcePublish == 'true' }} + skipCleanup: ${{ github.event.inputs.skipCleanup == 'true' }} + enableCrossOsCaching: true + compilePhaseRunnerOs: ubuntu-latest + packagePhaseRunnerOs: ubuntu-latest + # Tests run only on ubuntu-latest. Microsoft.Testing.Platform test projects build an + # OS-specific executable, and this pipeline compiles once (compilePhaseRunnerOs) then shares + # the binaries cross-OS, so a Windows test job cannot launch the Linux-built test host. The + # library is pure managed code with no OS-specific behaviour, so testing on the compile OS is + # sufficient. (Restoring Windows test coverage would require compiling per-OS.) + testPhaseMatrixJson: | + { + "os": ["ubuntu-latest"], + "dotnetFramework": ["net8.0", "net10.0"] + } + # testArtifactName: '' + # testArtifactPath: '' + compilePhaseEnv: ${{ needs.prepareConfig.outputs.RESOLVED_ENV_VARS }} + testPhaseEnv: ${{ needs.prepareConfig.outputs.RESOLVED_ENV_VARS }} + packagePhaseEnv: ${{ needs.prepareConfig.outputs.RESOLVED_ENV_VARS }} + publishPhaseEnv: ${{ needs.prepareConfig.outputs.RESOLVED_ENV_VARS }} + secrets: + compilePhaseAzureCredentials: ${{ secrets.AZURE_READER_CREDENTIALS }} + # testPhaseAzureCredentials: ${{ secrets.TESTS_KV_READER_CREDENTIALS }} + # packagePhaseAzureCredentials: ${{ secrets.AZURE_PUBLISH_CREDENTIALS }} + # publishPhaseAzureCredentials: ${{ secrets.AZURE_PUBLISH_CREDENTIALS }} + # compilePhaseSecrets: ${{ needs.prepareConfig.outputs.RESOLVED_SECRETS }} + # testPhaseSecrets: ${{ needs.prepareConfig.outputs.RESOLVED_SECRETS }} + # packagePhaseSecrets: ${{ needs.prepareConfig.outputs.RESOLVED_SECRETS }} + publishPhaseSecrets: ${{ needs.prepareConfig.outputs.RESOLVED_SECRETS }} + secretsEncryptionKey: ${{ secrets.SHARED_WORKFLOW_KEY }} diff --git a/.gitignore b/.gitignore index 8ee7fb9..89be2bb 100644 --- a/.gitignore +++ b/.gitignore @@ -401,4 +401,7 @@ FodyWeavers.xsd # Build outputs _codeCoverage/ _packages/ -*.sbom.* \ No newline at end of file +.zf/extensions/ +*.sbom.* +.endjin/ +BenchmarkDotNet.Artifacts/ \ No newline at end of file diff --git a/.zf/config.ps1 b/.zf/config.ps1 new file mode 100644 index 0000000..620121d --- /dev/null +++ b/.zf/config.ps1 @@ -0,0 +1,68 @@ +<# +This example demonstrates a software build process using the 'ZeroFailed.Build.DotNet' extension +to provide the features needed when building a .NET solutions. +#> + +$zerofailedExtensions = @( + @{ + # References the extension from its GitHub repository. If not already installed, use latest version from 'main' will be downloaded. + Name = "ZeroFailed.Build.DotNet" + GitRepository = "https://github.com/zerofailed/ZeroFailed.Build.DotNet" + GitRef = "main" + } +) + +# Load the tasks and process +. ZeroFailed.tasks -ZfPath $here/.zf + + +# +# Build process control options +# +$SkipInit = $false +$SkipVersion = $false +$SkipBuild = $false +$CleanBuild = $Clean +$SkipTest = $false +$SkipTestReport = $false +$SkipAnalysis = $false +$SkipPackage = $false + +# +# Build process configuration +# +$SolutionToBuild = (Resolve-Path (Join-Path $here "./Solutions/Ais.Net.Models.slnx")).Path +$ProjectsToPublish = @() +$NuSpecFilesToPackage = @() +$NugetPublishSource = property ZF_NUGET_PUBLISH_SOURCE "$here/_local-nuget-feed" +$IncludeAssembliesInCodeCoverage = "Ais.Net.Models" +$ExcludeAssembliesInCodeCoverage = "" + + +# Customise the build process + +task . FullBuild + + +# +# Build Process Extensibility Points - uncomment and implement as required +# + +# task RunFirst {} +# task PreInit {} +# task PostInit {} +# task PreVersion {} +# task PostVersion {} +# task PreBuild {} +# task PostBuild {} +# task PreTest {} +# task PostTest {} +# task PreTestReport {} +# task PostTestReport {} +# task PreAnalysis {} +# task PostAnalysis {} +# task PrePackage {} +# task PostPackage {} +# task PrePublish {} +# task PostPublish {} +# task RunLast {} diff --git a/Solutions/Ais.Net.Models.Benchmarks/Ais.Net.Models.Benchmarks.csproj b/Solutions/Ais.Net.Models.Benchmarks/Ais.Net.Models.Benchmarks.csproj new file mode 100644 index 0000000..9289de2 --- /dev/null +++ b/Solutions/Ais.Net.Models.Benchmarks/Ais.Net.Models.Benchmarks.csproj @@ -0,0 +1,25 @@ + + + + + net10.0 + latest + enable + Exe + false + + true + false + + CS1591 + + + + + + + + + + + diff --git a/Solutions/Ais.Net.Models.Benchmarks/ConversionBenchmarks.cs b/Solutions/Ais.Net.Models.Benchmarks/ConversionBenchmarks.cs new file mode 100644 index 0000000..0b09c8a --- /dev/null +++ b/Solutions/Ais.Net.Models.Benchmarks/ConversionBenchmarks.cs @@ -0,0 +1,42 @@ +namespace Ais.Net.Models.Benchmarks; + +using Ais.Net.Models; +using Ais.Net.Models.Abstractions; + +using BenchmarkDotNet.Attributes; + +/// +/// Measures the pure numeric/enum helpers on the decode path. These are expected to be +/// branch-free-ish, sub-nanosecond, and allocation-free; the benchmark exists to prove that +/// and to catch regressions. +/// +[MemoryDiagnoser] +public class ConversionBenchmarks +{ + private const int LatitudeIn10000thMins = 30_000_000; // 50 degrees + private const int LatitudeIn10thMins = 30_000; // 50 degrees + private const uint SpeedRaw = 123; // 12.3 knots + private const uint CourseRaw = 1234; // 123.4 degrees + private const int RawShipType = 60; // Passenger, all ships + + [Benchmark] + public double From10000thMinsToDegrees() => LatitudeIn10000thMins.From10000thMinsToDegrees(); + + [Benchmark] + public double From10thMinsToDegrees() => LatitudeIn10thMins.From10thMinsToDegrees(); + + [Benchmark] + public float? FromTenths() => SpeedRaw.FromTenths(); + + [Benchmark] + public float? FromTenthsToDegrees() => CourseRaw.FromTenthsToDegrees(); + + [Benchmark] + public ShipTypeGroup ToShipTypeGroup() => RawShipType.ToShipTypeGroup(); + + [Benchmark] + public ShipTypeCategory ToShipTypeCategory() => ((ShipType)RawShipType).ToShipTypeCategory(); + + [Benchmark] + public string ToShipTypeDescription() => RawShipType.ToShipTypeDescription(); +} diff --git a/Solutions/Ais.Net.Models.Benchmarks/MessageConstructionBenchmarks.cs b/Solutions/Ais.Net.Models.Benchmarks/MessageConstructionBenchmarks.cs new file mode 100644 index 0000000..1fc1fc2 --- /dev/null +++ b/Solutions/Ais.Net.Models.Benchmarks/MessageConstructionBenchmarks.cs @@ -0,0 +1,42 @@ +namespace Ais.Net.Models.Benchmarks; + +using Ais.Net.Models; +using Ais.Net.Models.Abstractions; + +using BenchmarkDotNet.Attributes; + +/// +/// Measures the cost of materialising a model record for a decoded message. Records are +/// reference types, so each message is at least one heap allocation (plus a nested +/// allocation for positional message types). +/// +[MemoryDiagnoser] +public class MessageConstructionBenchmarks +{ + [Benchmark] + public AisMessageType1Through3 ClassAPositionReport() => new( + CourseOverGround: 123.4f, + ManoeuvreIndicator: ManoeuvreIndicator.NotAvailable, + MessageType: 1, + Mmsi: 235009802, + NavigationStatus: NavigationStatus.UnderwayUsingEngine, + Position: new Position(50.12345, -1.98765), + PositionAccuracy: true, + RadioSlotTimeout: 1, + RadioSubMessage: 2, + RadioSyncState: RadioSyncState.UtcDirect, + RateOfTurn: 0, + RaimFlag: false, + RepeatIndicator: 0, + SpareBits145: 0, + SpeedOverGround: 12.3f, + TimeStampSecond: 42, + TrueHeadingDegrees: 181); + + [Benchmark] + public AisMessageType24Part0 StaticDataReportPartA() => new( + Mmsi: 235009802, + PartNumber: 0, + RepeatIndicator: 0, + Spare160: 0); +} diff --git a/Solutions/Ais.Net.Models.Benchmarks/PositionBenchmarks.cs b/Solutions/Ais.Net.Models.Benchmarks/PositionBenchmarks.cs new file mode 100644 index 0000000..e8ffaa1 --- /dev/null +++ b/Solutions/Ais.Net.Models.Benchmarks/PositionBenchmarks.cs @@ -0,0 +1,26 @@ +namespace Ais.Net.Models.Benchmarks; + +using Ais.Net.Models.Abstractions; + +using BenchmarkDotNet.Attributes; + +/// +/// Quantifies the cost of being a record (reference type). Every +/// decoded position is a heap allocation. The struct variant models the proposed +/// readonly record struct change to show the allocation saving on the decode path. +/// +[MemoryDiagnoser] +public class PositionBenchmarks +{ + private const double Latitude = 50.12345; + private const double Longitude = -1.98765; + + [Benchmark(Baseline = true)] + public Position RecordClass() => new(Latitude, Longitude); + + [Benchmark] + public PositionValue RecordStruct() => new(Latitude, Longitude); + + /// Models reworked as a value type. + public readonly record struct PositionValue(double Latitude, double Longitude); +} diff --git a/Solutions/Ais.Net.Models.Benchmarks/Program.cs b/Solutions/Ais.Net.Models.Benchmarks/Program.cs new file mode 100644 index 0000000..0d8d0a4 --- /dev/null +++ b/Solutions/Ais.Net.Models.Benchmarks/Program.cs @@ -0,0 +1,9 @@ +using BenchmarkDotNet.Running; + +// Use the switcher so individual benchmark classes/methods can be selected via --filter, +// e.g. dotnet run -c Release -- --filter *VesselName* +BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args); + +public partial class Program +{ +} diff --git a/Solutions/Ais.Net.Models.Benchmarks/VesselNameBenchmarks.cs b/Solutions/Ais.Net.Models.Benchmarks/VesselNameBenchmarks.cs new file mode 100644 index 0000000..ee804f7 --- /dev/null +++ b/Solutions/Ais.Net.Models.Benchmarks/VesselNameBenchmarks.cs @@ -0,0 +1,33 @@ +namespace Ais.Net.Models.Benchmarks; + +using System; + +using Ais.Net.Models; + +using BenchmarkDotNet.Attributes; + +/// +/// Compares vessel-name cleaning strategies. The production method currently uses +/// Trim(...).Split(...).Join(...) which allocates a temporary trimmed string, an array, +/// and one substring per word. The span-based variant produces exactly one string (the result) +/// using a stack buffer, and is the proposed replacement. +/// +[MemoryDiagnoser] +public class VesselNameBenchmarks +{ + // A raw 20-character AIS name field: text plus '@' padding (the common case). + [Params("EVER GIVEN@@@@@@@@@@", "QUEEN MARY 2@@@@@", "@@@@@@@@@@@@@@@@@@@@")] + public string RawName { get; set; } = string.Empty; + + // The previous implementation, kept as the baseline to show the improvement. + [Benchmark(Baseline = true)] + public string Old_SplitJoin() + { + string trimmed = this.RawName.Trim('@', ' '); + return string.Join(' ', trimmed.Split(' ', StringSplitOptions.RemoveEmptyEntries)); + } + + // The shipped implementation. + [Benchmark] + public string Current_Span() => this.RawName.CleanVesselName(); +} diff --git a/Solutions/Ais.Net.Models.Specs/Ais.Net.Models.Specs.csproj b/Solutions/Ais.Net.Models.Specs/Ais.Net.Models.Specs.csproj index 1eea670..8c17bd9 100644 --- a/Solutions/Ais.Net.Models.Specs/Ais.Net.Models.Specs.csproj +++ b/Solutions/Ais.Net.Models.Specs/Ais.Net.Models.Specs.csproj @@ -1,17 +1,13 @@ - + + - net8.0 + net8.0;net10.0 latest - false - - RCS1029;SA0001;SA1204;SA1600;SA1602;CS1591 enable + false + + CS1591 @@ -20,26 +16,12 @@ - - - contentfiles; analyzers - - - - - + + - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - + diff --git a/Solutions/Ais.Net.Models.Specs/AisMessageExtensionsTests.cs b/Solutions/Ais.Net.Models.Specs/AisMessageExtensionsTests.cs new file mode 100644 index 0000000..0488954 --- /dev/null +++ b/Solutions/Ais.Net.Models.Specs/AisMessageExtensionsTests.cs @@ -0,0 +1,107 @@ +namespace Ais.Net.Models.Specs; + +using System; + +using Ais.Net; +using Ais.Net.Models; + +using Microsoft.VisualStudio.TestTools.UnitTesting; + +using Shouldly; + +[TestClass] +public class AisMessageExtensionsTests +{ + [TestMethod] + [DataRow(0u, 0.0f)] + [DataRow(123u, 12.3f)] + [DataRow(1022u, 102.2f)] // one below the sentinel is still a real value + public void FromTenthsScalesByTenth(uint raw, float expected) + { + raw.FromTenths().ShouldBe(expected); + } + + [TestMethod] + public void FromTenthsTreats1023AsNotAvailable() + { + // 1023 is the AIS sentinel meaning "speed over ground not available". + 1023u.FromTenths().ShouldBeNull(); + } + + [TestMethod] + [DataRow(0u, 0.0f)] + [DataRow(1234u, 123.4f)] + [DataRow(3599u, 359.9f)] // one below the sentinel is still a real value + public void FromTenthsToDegreesScalesByTenth(uint raw, float expected) + { + raw.FromTenthsToDegrees().ShouldBe(expected); + } + + [TestMethod] + public void FromTenthsToDegreesTreats3600AsNotAvailable() + { + // 3600 (== 360.0 degrees) is the AIS sentinel meaning "course over ground not available". + 3600u.FromTenthsToDegrees().ShouldBeNull(); + } + + [TestMethod] + [DataRow("VESSEL", "VESSEL")] + [DataRow("VESSEL@@@@@", "VESSEL")] // trailing '@' padding is removed + [DataRow("@@@@@VESSEL", "VESSEL")] // leading '@' padding is removed + [DataRow("VESSEL NAME ", "VESSEL NAME")] // trailing whitespace is trimmed, interior single space kept + [DataRow("QUEEN MARY 2", "QUEEN MARY 2")] // legitimate interior single spaces are preserved + [DataRow("TWO WORDS", "TWO WORDS")] // a run of interior spaces collapses to one + [DataRow("A B", "A B")] // three-plus interior spaces collapse fully (not just paired) + [DataRow("QUEEN MARY 2", "QUEEN MARY 2")] // multiple interior runs each collapse + [DataRow("NAME@ ", "NAME")] // '@' followed by a space is still stripped (padding order-independent) + [DataRow(" @@ VESSEL @@ ", "VESSEL")] // mixed leading/trailing '@' and spaces are all stripped + [DataRow("AB@CD", "AB@CD")] // an interior '@' is left untouched (only end padding is stripped) + [DataRow("@@@@@", "")] // an all-padding name becomes empty + [DataRow("", "")] // an empty name stays empty + public void CleanVesselNameStripsPaddingAndCollapsesInteriorSpaceRuns(string raw, string expected) + { + raw.CleanVesselName().ShouldBe(expected); + } + + [TestMethod] + public void GetStringDecodesAsciiBytes() + { + ReadOnlySpan ascii = "Hello"u8; + + ascii.GetString().ShouldBe("Hello"); + } + + [TestMethod] + public void TextFieldToStringDecodesSixBitAsciiField() + { + // AIS packs text as 6-bit characters where 'H' == 8 and 'I' == 9. Those values are + // carried in the ASCII-armoured payload as (value + 48), i.e. '8' and '9'. + ReadOnlySpan armoured = "89"u8; + NmeaAisBitVectorParser bits = new(armoured, padding: 0); + NmeaAisTextFieldParser field = new(bits, bitLength: 12, bitOffset: 0); + + field.TextFieldToString().ShouldBe("HI"); + } + + [TestMethod] + public void CleanVesselNameHandlesNamesLongerThanTheStackBuffer() + { + // 260 characters (> 256) forces the heap-buffer fallback path. + string longName = new('A', 260); + + longName.CleanVesselName().ShouldBe(longName); + } + + [TestMethod] + public void TextFieldToStringHandlesFieldsLongerThanTheStackBuffer() + { + // 300 characters (> 256) forces the heap-buffer fallback. Each armoured '1' is 6-bit + // value 1, which decodes to 'A'. + byte[] armoured = new byte[300]; + armoured.AsSpan().Fill((byte)'1'); + NmeaAisBitVectorParser bits = new(armoured, padding: 0); + NmeaAisTextFieldParser field = new(bits, bitLength: (uint)(armoured.Length * 6), bitOffset: 0); + + field.TextFieldToString().ShouldBe(new string('A', 300)); + } +} diff --git a/Solutions/Ais.Net.Models.Specs/AisMessageType18StepDefinitions.cs b/Solutions/Ais.Net.Models.Specs/AisMessageType18StepDefinitions.cs deleted file mode 100644 index aa37e8a..0000000 --- a/Solutions/Ais.Net.Models.Specs/AisMessageType18StepDefinitions.cs +++ /dev/null @@ -1,104 +0,0 @@ -namespace Ais.Net.Models.Specs; - -using Abstractions; -using System; - -using Reqnroll; -using Shouldly; - -[Binding] -public class AisMessageType18StepDefinitions -{ - private AisMessageType18 sut; - private AisMessageType18Data data; - - [Given("a new AisMessageType18 record with the following properties:")] - public void GivenANewAisMessageTypeRecordWithTheFollowingProperties(DataTable table) - { - PositionData position = new() - { - Latitude = Convert.ToInt32(table.Rows[0]["Position_Latitude"]), - Longitude = Convert.ToInt32(table.Rows[0]["Position_Longitude"]) - }; - - this.data = table.CreateInstance(); - this.data.Position = position; - } - - [When("the AisMessageType is created")] - public void WhenTheAisMessageTypeIsCreated() - { - this.sut = new AisMessageType18( - CanAcceptMessage22ChannelAssignment: this.data.CanAcceptMessage22ChannelAssignment, - CanSwitchBands: this.data.CanSwitchBands, - CourseOverGround: this.data.CourseOverGroundDegrees, - CsUnit: Enum.Parse(this.data.CsUnit), - HasDisplay: this.data.HasDisplay, - IsAssigned: this.data.IsAssigned, - IsDscAttached: this.data.IsDscAttached, - Mmsi: this.data.Mmsi, - Position: new Position(this.data.Position.Latitude, this.data.Position.Longitude), - PositionAccuracy: this.data.PositionAccuracy, - RadioStatusType: Enum.Parse(this.data.RadioStatusType), - RaimFlag: this.data.RaimFlag, - RegionalReserved139: this.data.RegionalReserved139, - RegionalReserved38: this.data.RegionalReserved38, - RepeatIndicator: this.data.RepeatIndicator, - SpeedOverGround: this.data.SpeedOverGround, - TimeStampSecond: this.data.TimeStampSecond, - TrueHeadingDegrees: this.data.TrueHeadingDegrees - ); - } - - [Then("the properties should be set correctly")] - public void ThenThePropertiesShouldBeSetCorrectly() - { - this.sut.ShouldNotBeNull(); - this.sut.CanAcceptMessage22ChannelAssignment.ShouldBeTrue(); - this.sut.CanSwitchBands.ShouldBeTrue(); - this.sut.CourseOverGround.ShouldBe(123.45f); - this.sut.CsUnit.ShouldBe(ClassBUnit.Cstdma); - this.sut.HasDisplay.ShouldBeTrue(); - this.sut.IsAssigned.ShouldBeTrue(); - this.sut.IsDscAttached.ShouldBeTrue(); - this.sut.Mmsi.ShouldBe(12345u); - this.sut.Position.ShouldBe(new Position(1.0, 2.0)); - this.sut.PositionAccuracy.ShouldBeTrue(); - this.sut.RadioStatusType.ShouldBe(ClassBRadioStatusType.Itdma); - this.sut.RaimFlag.ShouldBeTrue(); - this.sut.RegionalReserved139.ShouldBe(1); - this.sut.RegionalReserved38.ShouldBe(2); - this.sut.RepeatIndicator.ShouldBe(3u); - this.sut.SpeedOverGround.ShouldBe(12.34f); - this.sut.TimeStampSecond.ShouldBe(56u); - this.sut.TrueHeadingDegrees.ShouldBe(78u); - } - - private class AisMessageType18Data - { - public bool CanAcceptMessage22ChannelAssignment { get; set; } - public bool CanSwitchBands { get; set; } - public float? CourseOverGroundDegrees { get; set; } - public string CsUnit { get; set; } - public bool HasDisplay { get; set; } - public bool IsAssigned { get; set; } - public bool IsDscAttached { get; set; } - public uint Mmsi { get; set; } - public PositionData Position { get; set; } - public bool PositionAccuracy { get; set; } - public string RadioStatusType { get; set; } - public bool RaimFlag { get; set; } - public int RegionalReserved139 { get; set; } - public int RegionalReserved38 { get; set; } - public uint RepeatIndicator { get; set; } - public float? SpeedOverGround { get; set; } - public uint TimeStampSecond { get; set; } - public uint TrueHeadingDegrees { get; set; } - } - - private class PositionData - { - public int Latitude { get; set; } - public int Longitude { get; set; } - } -} \ No newline at end of file diff --git a/Solutions/Ais.Net.Models.Specs/AisMessageType18Tests.cs b/Solutions/Ais.Net.Models.Specs/AisMessageType18Tests.cs new file mode 100644 index 0000000..dd956a3 --- /dev/null +++ b/Solutions/Ais.Net.Models.Specs/AisMessageType18Tests.cs @@ -0,0 +1,56 @@ +namespace Ais.Net.Models.Specs; + +using Ais.Net.Models; +using Ais.Net.Models.Abstractions; + +using Microsoft.VisualStudio.TestTools.UnitTesting; + +using Shouldly; + +[TestClass] +public class AisMessageType18Tests +{ + [TestMethod] + public void ClassBCsPositionReportExposesSuppliedValues() + { + AisMessageType18 message = new( + CanAcceptMessage22ChannelAssignment: true, + CanSwitchBands: true, + CourseOverGround: 123.45f, + CsUnit: ClassBUnit.Cstdma, + HasDisplay: true, + IsAssigned: true, + IsDscAttached: true, + Mmsi: 12345, + Position: new Position(1.0, 2.0), + PositionAccuracy: true, + RadioStatusType: ClassBRadioStatusType.Itdma, + RaimFlag: true, + RegionalReserved139: 1, + RegionalReserved38: 2, + RepeatIndicator: 3, + SpeedOverGround: 12.34f, + TimeStampSecond: 56, + TrueHeadingDegrees: 78); + + message.MessageType.ShouldBe(18); + message.CanAcceptMessage22ChannelAssignment.ShouldBe(true); + message.CanSwitchBands.ShouldBe(true); + message.CourseOverGround.ShouldBe(123.45f); + message.CsUnit.ShouldBe(ClassBUnit.Cstdma); + message.HasDisplay.ShouldBe(true); + message.IsAssigned.ShouldBe(true); + message.IsDscAttached.ShouldBe(true); + message.Mmsi.ShouldBe(12345u); + message.Position.ShouldBe(new Position(1.0, 2.0)); + message.PositionAccuracy.ShouldBe(true); + message.RadioStatusType.ShouldBe(ClassBRadioStatusType.Itdma); + message.RaimFlag.ShouldBe(true); + message.RegionalReserved139.ShouldBe(1); + message.RegionalReserved38.ShouldBe(2); + message.RepeatIndicator.ShouldBe(3u); + message.SpeedOverGround.ShouldBe(12.34f); + message.TimeStampSecond.ShouldBe(56u); + message.TrueHeadingDegrees.ShouldBe(78u); + } +} diff --git a/Solutions/Ais.Net.Models.Specs/AisMessageType19Tests.cs b/Solutions/Ais.Net.Models.Specs/AisMessageType19Tests.cs new file mode 100644 index 0000000..13b971e --- /dev/null +++ b/Solutions/Ais.Net.Models.Specs/AisMessageType19Tests.cs @@ -0,0 +1,62 @@ +namespace Ais.Net.Models.Specs; + +using Ais.Net.Models; +using Ais.Net.Models.Abstractions; + +using Microsoft.VisualStudio.TestTools.UnitTesting; + +using Shouldly; + +[TestClass] +public class AisMessageType19Tests +{ + [TestMethod] + public void ClassBSoPositionReportExposesSuppliedValues() + { + AisMessageType19 message = new( + CourseOverGround: 123.45f, + DimensionToBow: 1, + DimensionToPort: 2, + DimensionToStarboard: 3, + DimensionToStern: 4, + IsAssigned: true, + IsDteNotReady: true, + Mmsi: 12345, + Position: new Position(1.0, 2.0), + PositionAccuracy: true, + PositionFixType: EpfdFixType.Gps, + RaimFlag: true, + RegionalReserved139: 1, + RegionalReserved38: 2, + RepeatIndicator: 3, + ShipName: "SHIP", + ShipType: (ShipType)60, + Spare308: 1, + SpeedOverGround: 12.34f, + TimeStampSecond: 56, + TrueHeadingDegrees: 78); + + message.MessageType.ShouldBe(19); + message.CourseOverGround.ShouldBe(123.45f); + message.DimensionToBow.ShouldBe(1u); + message.DimensionToPort.ShouldBe(2u); + message.DimensionToStarboard.ShouldBe(3u); + message.DimensionToStern.ShouldBe(4u); + message.IsAssigned.ShouldBe(true); + message.IsDteNotReady.ShouldBe(true); + message.Mmsi.ShouldBe(12345u); + message.Position.ShouldBe(new Position(1.0, 2.0)); + message.PositionAccuracy.ShouldBe(true); + message.PositionFixType.ShouldBe(EpfdFixType.Gps); + message.RaimFlag.ShouldBe(true); + message.RegionalReserved139.ShouldBe(1); + message.RegionalReserved38.ShouldBe(2); + message.RepeatIndicator.ShouldBe(3u); + message.ShipName.ShouldBe("SHIP"); + message.ShipType.ShouldBe((ShipType)60); + message.Spare308.ShouldBe(1u); + message.SpeedOverGround.ShouldBe(12.34f); + message.TimeStampSecond.ShouldBe(56u); + message.TrueHeadingDegrees.ShouldBe(78u); + } +} diff --git a/Solutions/Ais.Net.Models.Specs/AisMessageType1Through3Tests.cs b/Solutions/Ais.Net.Models.Specs/AisMessageType1Through3Tests.cs new file mode 100644 index 0000000..a4a7503 --- /dev/null +++ b/Solutions/Ais.Net.Models.Specs/AisMessageType1Through3Tests.cs @@ -0,0 +1,61 @@ +namespace Ais.Net.Models.Specs; + +using Ais.Net.Models; +using Ais.Net.Models.Abstractions; + +using Microsoft.VisualStudio.TestTools.UnitTesting; + +using Shouldly; + +[TestClass] +public class AisMessageType1Through3Tests +{ + [TestMethod] + [DataRow(123.45f, NavigationStatus.UnderwayUsingEngine, 12345u, 1.0, 2.0, 12.34f)] + [DataRow(234.56f, NavigationStatus.AtAnchor, 54321u, 3.0, 4.0, 0.0f)] + public void ClassAVesselPositionReportExposesSuppliedValues( + float courseOverGround, + NavigationStatus navigationStatus, + uint mmsi, + double latitude, + double longitude, + float speedOverGround) + { + AisMessageType1Through3 message = new( + CourseOverGround: courseOverGround, + ManoeuvreIndicator: ManoeuvreIndicator.NotAvailable, + MessageType: 1, + Mmsi: mmsi, + NavigationStatus: navigationStatus, + Position: new Position(latitude, longitude), + PositionAccuracy: true, + RadioSlotTimeout: 1, + RadioSubMessage: 2, + RadioSyncState: RadioSyncState.UtcDirect, + RateOfTurn: 1, + RaimFlag: true, + RepeatIndicator: 3, + SpareBits145: 4, + SpeedOverGround: speedOverGround, + TimeStampSecond: 56, + TrueHeadingDegrees: 78); + + message.CourseOverGround.ShouldBe(courseOverGround); + message.ManoeuvreIndicator.ShouldBe(ManoeuvreIndicator.NotAvailable); + message.MessageType.ShouldBe(1); + message.Mmsi.ShouldBe(mmsi); + message.NavigationStatus.ShouldBe(navigationStatus); + message.Position.ShouldBe(new Position(latitude, longitude)); + message.PositionAccuracy.ShouldBe(true); + message.RadioSlotTimeout.ShouldBe(1u); + message.RadioSubMessage.ShouldBe(2u); + message.RadioSyncState.ShouldBe(RadioSyncState.UtcDirect); + message.RateOfTurn.ShouldBe(1); + message.RaimFlag.ShouldBe(true); + message.RepeatIndicator.ShouldBe(3u); + message.SpareBits145.ShouldBe(4u); + message.SpeedOverGround.ShouldBe(speedOverGround); + message.TimeStampSecond.ShouldBe(56u); + message.TrueHeadingDegrees.ShouldBe(78u); + } +} diff --git a/Solutions/Ais.Net.Models.Specs/AisMessageType24Part0Tests.cs b/Solutions/Ais.Net.Models.Specs/AisMessageType24Part0Tests.cs new file mode 100644 index 0000000..a696c11 --- /dev/null +++ b/Solutions/Ais.Net.Models.Specs/AisMessageType24Part0Tests.cs @@ -0,0 +1,27 @@ +namespace Ais.Net.Models.Specs; + +using Ais.Net.Models; + +using Microsoft.VisualStudio.TestTools.UnitTesting; + +using Shouldly; + +[TestClass] +public class AisMessageType24Part0Tests +{ + [TestMethod] + public void StaticDataReportPartAExposesSuppliedValues() + { + AisMessageType24Part0 message = new( + Mmsi: 12345, + PartNumber: 0, + RepeatIndicator: 3, + Spare160: 1); + + message.MessageType.ShouldBe(24); // both parts of a type 24 report share message type 24 + message.Mmsi.ShouldBe(12345u); + message.PartNumber.ShouldBe(0u); + message.RepeatIndicator.ShouldBe(3u); + message.Spare160.ShouldBe(1u); + } +} diff --git a/Solutions/Ais.Net.Models.Specs/AisMessageType24Part1Tests.cs b/Solutions/Ais.Net.Models.Specs/AisMessageType24Part1Tests.cs new file mode 100644 index 0000000..4153435 --- /dev/null +++ b/Solutions/Ais.Net.Models.Specs/AisMessageType24Part1Tests.cs @@ -0,0 +1,50 @@ +namespace Ais.Net.Models.Specs; + +using Ais.Net.Models; +using Ais.Net.Models.Abstractions; + +using Microsoft.VisualStudio.TestTools.UnitTesting; + +using Shouldly; + +[TestClass] +public class AisMessageType24Part1Tests +{ + [TestMethod] + public void StaticDataReportPartBExposesSuppliedValues() + { + AisMessageType24Part1 message = new( + CallSign: "CALL", + DimensionToBow: 1, + DimensionToPort: 2, + DimensionToStarboard: 3, + DimensionToStern: 4, + Mmsi: 12345, + MothershipMmsi: 54321, + PartNumber: 1, + RepeatIndicator: 3, + SerialNumber: 123, + Spare162: 1, + ShipType: (ShipType)60, + UnitModelCode: 1, + VendorIdRev3: "VEN", + VendorIdRev4: "DOR"); + + message.MessageType.ShouldBe(24); // both parts of a type 24 report share message type 24 + message.CallSign.ShouldBe("CALL"); + message.DimensionToBow.ShouldBe(1u); + message.DimensionToPort.ShouldBe(2u); + message.DimensionToStarboard.ShouldBe(3u); + message.DimensionToStern.ShouldBe(4u); + message.Mmsi.ShouldBe(12345u); + message.MothershipMmsi.ShouldBe(54321u); + message.PartNumber.ShouldBe(1u); + message.RepeatIndicator.ShouldBe(3u); + message.SerialNumber.ShouldBe(123u); + message.Spare162.ShouldBe(1u); + message.ShipType.ShouldBe((ShipType)60); + message.UnitModelCode.ShouldBe(1u); + message.VendorIdRev3.ShouldBe("VEN"); + message.VendorIdRev4.ShouldBe("DOR"); + } +} diff --git a/Solutions/Ais.Net.Models.Specs/AisMessageType27Tests.cs b/Solutions/Ais.Net.Models.Specs/AisMessageType27Tests.cs new file mode 100644 index 0000000..14ae355 --- /dev/null +++ b/Solutions/Ais.Net.Models.Specs/AisMessageType27Tests.cs @@ -0,0 +1,38 @@ +namespace Ais.Net.Models.Specs; + +using Ais.Net.Models; +using Ais.Net.Models.Abstractions; + +using Microsoft.VisualStudio.TestTools.UnitTesting; + +using Shouldly; + +[TestClass] +public class AisMessageType27Tests +{ + [TestMethod] + public void LongRangeAisBroadcastMessageExposesSuppliedValues() + { + AisMessageType27 message = new( + CourseOverGround: 123.45f, + GnssPositionStatus: true, + Mmsi: 12345, + NavigationStatus: NavigationStatus.UnderwayUsingEngine, + Position: new Position(1.0, 2.0), + PositionAccuracy: true, + RaimFlag: true, + RepeatIndicator: 3, + SpeedOverGround: 12.34f); + + message.MessageType.ShouldBe(27); + message.CourseOverGround.ShouldBe(123.45f); + message.GnssPositionStatus.ShouldBe(true); + message.Mmsi.ShouldBe(12345u); + message.NavigationStatus.ShouldBe(NavigationStatus.UnderwayUsingEngine); + message.Position.ShouldBe(new Position(1.0, 2.0)); + message.PositionAccuracy.ShouldBe(true); + message.RaimFlag.ShouldBe(true); + message.RepeatIndicator.ShouldBe(3u); + message.SpeedOverGround.ShouldBe(12.34f); + } +} diff --git a/Solutions/Ais.Net.Models.Specs/AisMessageType5Tests.cs b/Solutions/Ais.Net.Models.Specs/AisMessageType5Tests.cs new file mode 100644 index 0000000..998bf3e --- /dev/null +++ b/Solutions/Ais.Net.Models.Specs/AisMessageType5Tests.cs @@ -0,0 +1,60 @@ +namespace Ais.Net.Models.Specs; + +using Ais.Net.Models; +using Ais.Net.Models.Abstractions; + +using Microsoft.VisualStudio.TestTools.UnitTesting; + +using Shouldly; + +[TestClass] +public class AisMessageType5Tests +{ + [TestMethod] + public void StaticAndVoyageRelatedDataExposesSuppliedValues() + { + AisMessageType5 message = new( + AisVersion: 1, + CallSign: "CALL", + Destination: "DEST", + DimensionToBow: 1, + DimensionToPort: 2, + DimensionToStarboard: 3, + DimensionToStern: 4, + Draught10thMetres: 5, + EtaDay: 6, + EtaHour: 7, + EtaMinute: 8, + EtaMonth: 9, + IsDteNotReady: true, + ImoNumber: 123, + Mmsi: 12345, + PositionFixType: EpfdFixType.Gps, + RepeatIndicator: 3, + ShipType: (ShipType)60, + Spare423: 1, + VesselName: "VESSEL"); + + message.MessageType.ShouldBe(5); + message.AisVersion.ShouldBe(1u); + message.CallSign.ShouldBe("CALL"); + message.Destination.ShouldBe("DEST"); + message.DimensionToBow.ShouldBe(1u); + message.DimensionToPort.ShouldBe(2u); + message.DimensionToStarboard.ShouldBe(3u); + message.DimensionToStern.ShouldBe(4u); + message.Draught10thMetres.ShouldBe(5u); + message.EtaDay.ShouldBe(6u); + message.EtaHour.ShouldBe(7u); + message.EtaMinute.ShouldBe(8u); + message.EtaMonth.ShouldBe(9u); + message.IsDteNotReady.ShouldBe(true); + message.ImoNumber.ShouldBe(123u); + message.Mmsi.ShouldBe(12345u); + message.PositionFixType.ShouldBe(EpfdFixType.Gps); + message.RepeatIndicator.ShouldBe(3u); + message.ShipType.ShouldBe((ShipType)60); + message.Spare423.ShouldBe(1u); + message.VesselName.ShouldBe("VESSEL"); + } +} diff --git a/Solutions/Ais.Net.Models.Specs/AssemblyInfo.cs b/Solutions/Ais.Net.Models.Specs/AssemblyInfo.cs new file mode 100644 index 0000000..15e991d --- /dev/null +++ b/Solutions/Ais.Net.Models.Specs/AssemblyInfo.cs @@ -0,0 +1,5 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; + +// MSTest 4.0 requires explicit parallelization configuration (MSTEST0001). +// Workers = 0 uses all available cores; MethodLevel allows tests to run concurrently. +[assembly: Parallelize(Workers = 0, Scope = ExecutionScope.MethodLevel)] diff --git a/Solutions/Ais.Net.Models.Specs/Features/AisMessageType18.feature b/Solutions/Ais.Net.Models.Specs/Features/AisMessageType18.feature deleted file mode 100644 index f01dd46..0000000 --- a/Solutions/Ais.Net.Models.Specs/Features/AisMessageType18.feature +++ /dev/null @@ -1,8 +0,0 @@ -Feature: AisMessageType18 - -Scenario: Create AisMessageType18 record - Given a new AisMessageType18 record with the following properties: - | CanAcceptMessage22ChannelAssignment | CanSwitchBands | CourseOverGroundDegrees | CsUnit | HasDisplay | IsAssigned | IsDscAttached | Mmsi | Position_Latitude | Position_Longitude | PositionAccuracy | RadioStatusType | RaimFlag | RegionalReserved139 | RegionalReserved38 | RepeatIndicator | SpeedOverGround | TimeStampSecond | TrueHeadingDegrees | - | true | true | 123.45 | Cstdma | true | true | true | 12345 | 1 | 2 | true | Itdma | true | 1 | 2 | 3 | 12.34 | 56 | 78 | - When the AisMessageType is created - Then the properties should be set correctly \ No newline at end of file diff --git a/Solutions/Ais.Net.Models.Specs/Features/AisMessageType18.feature.cs b/Solutions/Ais.Net.Models.Specs/Features/AisMessageType18.feature.cs deleted file mode 100644 index aab3790..0000000 --- a/Solutions/Ais.Net.Models.Specs/Features/AisMessageType18.feature.cs +++ /dev/null @@ -1,149 +0,0 @@ -// ------------------------------------------------------------------------------ -// -// This code was generated by Reqnroll (https://www.reqnroll.net/). -// Reqnroll Version:2.0.0.0 -// Reqnroll Generator Version:2.0.0.0 -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -// ------------------------------------------------------------------------------ -#region Designer generated code -#pragma warning disable -namespace Ais.Net.Models.Specs.Features -{ - using Reqnroll; - using System; - using System.Linq; - - - [System.CodeDom.Compiler.GeneratedCodeAttribute("Reqnroll", "2.0.0.0")] - [System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - [NUnit.Framework.TestFixtureAttribute()] - [NUnit.Framework.DescriptionAttribute("AisMessageType18")] - public partial class AisMessageType18Feature - { - - private global::Reqnroll.ITestRunner testRunner; - - private static string[] featureTags = ((string[])(null)); - -#line 1 "AisMessageType18.feature" -#line hidden - - [NUnit.Framework.OneTimeSetUpAttribute()] - public virtual async System.Threading.Tasks.Task FeatureSetupAsync() - { - testRunner = global::Reqnroll.TestRunnerManager.GetTestRunnerForAssembly(); - global::Reqnroll.FeatureInfo featureInfo = new global::Reqnroll.FeatureInfo(new System.Globalization.CultureInfo("en-US"), "Features", "AisMessageType18", null, global::Reqnroll.ProgrammingLanguage.CSharp, featureTags); - await testRunner.OnFeatureStartAsync(featureInfo); - } - - [NUnit.Framework.OneTimeTearDownAttribute()] - public virtual async System.Threading.Tasks.Task FeatureTearDownAsync() - { - await testRunner.OnFeatureEndAsync(); - global::Reqnroll.TestRunnerManager.ReleaseTestRunner(testRunner); - testRunner = null; - } - - [NUnit.Framework.SetUpAttribute()] - public async System.Threading.Tasks.Task TestInitializeAsync() - { - } - - [NUnit.Framework.TearDownAttribute()] - public async System.Threading.Tasks.Task TestTearDownAsync() - { - await testRunner.OnScenarioEndAsync(); - } - - public void ScenarioInitialize(global::Reqnroll.ScenarioInfo scenarioInfo) - { - testRunner.OnScenarioInitialize(scenarioInfo); - testRunner.ScenarioContext.ScenarioContainer.RegisterInstanceAs(NUnit.Framework.TestContext.CurrentContext); - } - - public async System.Threading.Tasks.Task ScenarioStartAsync() - { - await testRunner.OnScenarioStartAsync(); - } - - public async System.Threading.Tasks.Task ScenarioCleanupAsync() - { - await testRunner.CollectScenarioErrorsAsync(); - } - - [NUnit.Framework.TestAttribute()] - [NUnit.Framework.DescriptionAttribute("Create AisMessageType18 record")] - public async System.Threading.Tasks.Task CreateAisMessageType18Record() - { - string[] tagsOfScenario = ((string[])(null)); - System.Collections.Specialized.OrderedDictionary argumentsOfScenario = new System.Collections.Specialized.OrderedDictionary(); - global::Reqnroll.ScenarioInfo scenarioInfo = new global::Reqnroll.ScenarioInfo("Create AisMessageType18 record", null, tagsOfScenario, argumentsOfScenario, featureTags); -#line 3 -this.ScenarioInitialize(scenarioInfo); -#line hidden - if ((global::Reqnroll.TagHelper.ContainsIgnoreTag(scenarioInfo.CombinedTags) || global::Reqnroll.TagHelper.ContainsIgnoreTag(featureTags))) - { - testRunner.SkipScenario(); - } - else - { - await this.ScenarioStartAsync(); - global::Reqnroll.Table table1 = new global::Reqnroll.Table(new string[] { - "CanAcceptMessage22ChannelAssignment", - "CanSwitchBands", - "CourseOverGroundDegrees", - "CsUnit", - "HasDisplay", - "IsAssigned", - "IsDscAttached", - "Mmsi", - "Position_Latitude", - "Position_Longitude", - "PositionAccuracy", - "RadioStatusType", - "RaimFlag", - "RegionalReserved139", - "RegionalReserved38", - "RepeatIndicator", - "SpeedOverGround", - "TimeStampSecond", - "TrueHeadingDegrees"}); - table1.AddRow(new string[] { - "true", - "true", - "123.45", - "Cstdma", - "true", - "true", - "true", - "12345", - "1", - "2", - "true", - "Itdma", - "true", - "1", - "2", - "3", - "12.34", - "56", - "78"}); -#line 4 - await testRunner.GivenAsync("a new AisMessageType18 record with the following properties:", ((string)(null)), table1, "Given "); -#line hidden -#line 7 - await testRunner.WhenAsync("the AisMessageType is created", ((string)(null)), ((global::Reqnroll.Table)(null)), "When "); -#line hidden -#line 8 - await testRunner.ThenAsync("the properties should be set correctly", ((string)(null)), ((global::Reqnroll.Table)(null)), "Then "); -#line hidden - } - await this.ScenarioCleanupAsync(); - } - } -} -#pragma warning restore -#endregion diff --git a/Solutions/Ais.Net.Models.Specs/PositionTests.cs b/Solutions/Ais.Net.Models.Specs/PositionTests.cs new file mode 100644 index 0000000..d8b884a --- /dev/null +++ b/Solutions/Ais.Net.Models.Specs/PositionTests.cs @@ -0,0 +1,71 @@ +namespace Ais.Net.Models.Specs; + +using Ais.Net.Models; +using Ais.Net.Models.Abstractions; + +using Microsoft.VisualStudio.TestTools.UnitTesting; + +using Shouldly; + +[TestClass] +public class PositionTests +{ + [TestMethod] + [DataRow(600000, 1.0)] // 1 degree == 60 minutes == 600,000 ten-thousandths of a minute + [DataRow(-600000, -1.0)] + [DataRow(300000, 0.5)] + [DataRow(0, 0.0)] + public void From10000thMinsToDegreesScalesBySixHundredThousand(int tenThousandthsOfMinutes, double expectedDegrees) + { + tenThousandthsOfMinutes.From10000thMinsToDegrees().ShouldBe(expectedDegrees); + } + + [TestMethod] + [DataRow(600, 1.0)] // 1 degree == 60 minutes == 600 tenths of a minute + [DataRow(-600, -1.0)] + [DataRow(300, 0.5)] + [DataRow(0, 0.0)] + public void From10thMinsToDegreesScalesBySixHundred(int tenthsOfMinutes, double expectedDegrees) + { + tenthsOfMinutes.From10thMinsToDegrees().ShouldBe(expectedDegrees); + } + + [TestMethod] + public void From10000thMinsBuildsPositionFromScaledLatitudeAndLongitude() + { + Position position = Position.From10000thMins(600000, 1200000); + + position.ShouldBe(new Position(1.0, 2.0)); + } + + [TestMethod] + public void From10thMinsBuildsPositionFromScaledLatitudeAndLongitude() + { + Position position = Position.From10thMins(600, 1200); + + position.ShouldBe(new Position(1.0, 2.0)); + } + + [TestMethod] + [DataRow(54600000, 91.0)] // AIS "latitude not available" sentinel (91 degrees) + [DataRow(108600000, 181.0)] // AIS "longitude not available" sentinel (181 degrees) + public void From10000thMinsToDegreesDoesNotSpecialCaseNotAvailableSentinels(int tenThousandthsOfMinutes, double expectedDegrees) + { + // Unlike speed/course, the coordinate helpers do not null out the "not available" + // sentinels; the raw out-of-range degrees pass straight through (any such filtering + // is the caller's / decoder's responsibility). + tenThousandthsOfMinutes.From10000thMinsToDegrees().ShouldBe(expectedDegrees); + } + + [TestMethod] + public void PositionsWithTheSameCoordinatesAreEqual() + { + new Position(51.5, -0.12).ShouldBe(new Position(51.5, -0.12)); + } + + [TestMethod] + public void PositionsWithDifferentCoordinatesAreNotEqual() + { + new Position(51.5, -0.12).ShouldNotBe(new Position(51.5, 0.12)); + } +} diff --git a/Solutions/Ais.Net.Models.Specs/ShipTypeClassificationTests.cs b/Solutions/Ais.Net.Models.Specs/ShipTypeClassificationTests.cs new file mode 100644 index 0000000..67450ad --- /dev/null +++ b/Solutions/Ais.Net.Models.Specs/ShipTypeClassificationTests.cs @@ -0,0 +1,242 @@ +namespace Ais.Net.Models.Specs; + +using Ais.Net.Models; +using Ais.Net.Models.Abstractions; + +using Microsoft.VisualStudio.TestTools.UnitTesting; + +using Shouldly; + +[TestClass] +public class ShipTypeClassificationTests +{ + [TestMethod] + [DataRow(0, ShipTypeGroup.NotAvailable)] + [DataRow(1, ShipTypeGroup.Reserved)] + [DataRow(19, ShipTypeGroup.Reserved)] + [DataRow(20, ShipTypeGroup.WingInGroundAll)] + [DataRow(21, ShipTypeGroup.WingInGroundHazardous)] + [DataRow(24, ShipTypeGroup.WingInGroundHazardous)] + [DataRow(25, ShipTypeGroup.WingInGroundReserved)] + [DataRow(29, ShipTypeGroup.WingInGroundReserved)] + [DataRow(30, ShipTypeGroup.Fishing)] + [DataRow(31, ShipTypeGroup.Towing)] + [DataRow(32, ShipTypeGroup.TowingLengthOver200MOrBreadthOver25M)] + [DataRow(33, ShipTypeGroup.DredgingOrUnderwaterOps)] + [DataRow(34, ShipTypeGroup.DivingOps)] + [DataRow(35, ShipTypeGroup.MilitaryOps)] + [DataRow(36, ShipTypeGroup.Sailing)] + [DataRow(37, ShipTypeGroup.PleasureCraft)] + [DataRow(38, ShipTypeGroup.Reserved)] + [DataRow(39, ShipTypeGroup.Reserved)] + [DataRow(40, ShipTypeGroup.HighSpeedCraftAll)] + [DataRow(41, ShipTypeGroup.HighSpeedCraftHazardous)] + [DataRow(44, ShipTypeGroup.HighSpeedCraftHazardous)] + [DataRow(45, ShipTypeGroup.HighSpeedCraftReserved)] + [DataRow(48, ShipTypeGroup.HighSpeedCraftReserved)] + [DataRow(49, ShipTypeGroup.HighSpeedCraftNoAdditionalInformation)] // '9' == "no additional information", matching 69/79/89/99 + [DataRow(50, ShipTypeGroup.PilotVessel)] + [DataRow(51, ShipTypeGroup.SearchAndRescueVessel)] + [DataRow(52, ShipTypeGroup.Tug)] + [DataRow(53, ShipTypeGroup.PortTender)] + [DataRow(54, ShipTypeGroup.AntiPollutionEquipment)] + [DataRow(55, ShipTypeGroup.LawEnforcement)] + [DataRow(56, ShipTypeGroup.SpareLocalVessel)] + [DataRow(57, ShipTypeGroup.SpareLocalVessel)] + [DataRow(58, ShipTypeGroup.MedicalTransport)] + [DataRow(59, ShipTypeGroup.NoncombatantShip)] + [DataRow(60, ShipTypeGroup.PassengerAll)] + [DataRow(61, ShipTypeGroup.PassengerHazardous)] + [DataRow(64, ShipTypeGroup.PassengerHazardous)] + [DataRow(65, ShipTypeGroup.PassengerReserved)] + [DataRow(68, ShipTypeGroup.PassengerReserved)] + [DataRow(69, ShipTypeGroup.PassengerNoAdditionalInformation)] + [DataRow(70, ShipTypeGroup.CargoAll)] + [DataRow(71, ShipTypeGroup.CargoHazardous)] + [DataRow(74, ShipTypeGroup.CargoHazardous)] + [DataRow(75, ShipTypeGroup.CargoReserved)] + [DataRow(78, ShipTypeGroup.CargoReserved)] + [DataRow(79, ShipTypeGroup.CargoNoAdditionalInformation)] + [DataRow(80, ShipTypeGroup.TankerAll)] + [DataRow(81, ShipTypeGroup.TankerHazardous)] + [DataRow(84, ShipTypeGroup.TankerHazardous)] + [DataRow(85, ShipTypeGroup.TankerReserved)] + [DataRow(88, ShipTypeGroup.TankerReserved)] + [DataRow(89, ShipTypeGroup.TankerNoAdditionalInformation)] + [DataRow(90, ShipTypeGroup.OtherAll)] + [DataRow(91, ShipTypeGroup.OtherHazardous)] + [DataRow(94, ShipTypeGroup.OtherHazardous)] + [DataRow(95, ShipTypeGroup.OtherReserved)] + [DataRow(98, ShipTypeGroup.OtherReserved)] + [DataRow(99, ShipTypeGroup.OtherNoAdditionalInformation)] + [DataRow(100, ShipTypeGroup.NotAvailable)] + [DataRow(-1, ShipTypeGroup.NotAvailable)] + public void ToShipTypeGroupClassifiesRawShipTypeCode(int shipType, ShipTypeGroup expected) + { + shipType.ToShipTypeGroup().ShouldBe(expected); + } + + [TestMethod] + [DataRow(ShipTypeGroup.NotAvailable, ShipTypeCategory.NotAvailable)] + [DataRow(ShipTypeGroup.Reserved, ShipTypeCategory.Reserved)] + [DataRow(ShipTypeGroup.WingInGroundAll, ShipTypeCategory.WingInGround)] + [DataRow(ShipTypeGroup.WingInGroundHazardous, ShipTypeCategory.WingInGround)] + [DataRow(ShipTypeGroup.WingInGroundReserved, ShipTypeCategory.WingInGround)] + [DataRow(ShipTypeGroup.Fishing, ShipTypeCategory.SpecialCategory3)] + [DataRow(ShipTypeGroup.Towing, ShipTypeCategory.SpecialCategory3)] + [DataRow(ShipTypeGroup.TowingLengthOver200MOrBreadthOver25M, ShipTypeCategory.SpecialCategory3)] + [DataRow(ShipTypeGroup.DredgingOrUnderwaterOps, ShipTypeCategory.SpecialCategory3)] + [DataRow(ShipTypeGroup.DivingOps, ShipTypeCategory.SpecialCategory3)] + [DataRow(ShipTypeGroup.MilitaryOps, ShipTypeCategory.SpecialCategory3)] + [DataRow(ShipTypeGroup.Sailing, ShipTypeCategory.SpecialCategory3)] + [DataRow(ShipTypeGroup.PleasureCraft, ShipTypeCategory.SpecialCategory3)] + [DataRow(ShipTypeGroup.HighSpeedCraftAll, ShipTypeCategory.HighSpeedCraft)] + [DataRow(ShipTypeGroup.HighSpeedCraftHazardous, ShipTypeCategory.HighSpeedCraft)] + [DataRow(ShipTypeGroup.HighSpeedCraftReserved, ShipTypeCategory.HighSpeedCraft)] + [DataRow(ShipTypeGroup.HighSpeedCraftNoAdditionalInformation, ShipTypeCategory.HighSpeedCraft)] + [DataRow(ShipTypeGroup.PilotVessel, ShipTypeCategory.SpecialCategory5)] + [DataRow(ShipTypeGroup.SearchAndRescueVessel, ShipTypeCategory.SpecialCategory5)] + [DataRow(ShipTypeGroup.Tug, ShipTypeCategory.SpecialCategory5)] + [DataRow(ShipTypeGroup.PortTender, ShipTypeCategory.SpecialCategory5)] + [DataRow(ShipTypeGroup.AntiPollutionEquipment, ShipTypeCategory.SpecialCategory5)] + [DataRow(ShipTypeGroup.LawEnforcement, ShipTypeCategory.SpecialCategory5)] + [DataRow(ShipTypeGroup.SpareLocalVessel, ShipTypeCategory.SpecialCategory5)] + [DataRow(ShipTypeGroup.MedicalTransport, ShipTypeCategory.SpecialCategory5)] + [DataRow(ShipTypeGroup.NoncombatantShip, ShipTypeCategory.SpecialCategory5)] + [DataRow(ShipTypeGroup.PassengerAll, ShipTypeCategory.Passenger)] + [DataRow(ShipTypeGroup.PassengerHazardous, ShipTypeCategory.Passenger)] + [DataRow(ShipTypeGroup.PassengerReserved, ShipTypeCategory.Passenger)] + [DataRow(ShipTypeGroup.PassengerNoAdditionalInformation, ShipTypeCategory.Passenger)] + [DataRow(ShipTypeGroup.CargoAll, ShipTypeCategory.Cargo)] + [DataRow(ShipTypeGroup.CargoHazardous, ShipTypeCategory.Cargo)] + [DataRow(ShipTypeGroup.CargoReserved, ShipTypeCategory.Cargo)] + [DataRow(ShipTypeGroup.CargoNoAdditionalInformation, ShipTypeCategory.Cargo)] + [DataRow(ShipTypeGroup.TankerAll, ShipTypeCategory.Tanker)] + [DataRow(ShipTypeGroup.TankerHazardous, ShipTypeCategory.Tanker)] + [DataRow(ShipTypeGroup.TankerReserved, ShipTypeCategory.Tanker)] + [DataRow(ShipTypeGroup.TankerNoAdditionalInformation, ShipTypeCategory.Tanker)] + [DataRow(ShipTypeGroup.OtherAll, ShipTypeCategory.Other)] + [DataRow(ShipTypeGroup.OtherHazardous, ShipTypeCategory.Other)] + [DataRow(ShipTypeGroup.OtherReserved, ShipTypeCategory.Other)] + [DataRow(ShipTypeGroup.OtherNoAdditionalInformation, ShipTypeCategory.Other)] + [DataRow((ShipTypeGroup)999, ShipTypeCategory.NotAvailable)] // an unrecognised group falls back to NotAvailable + public void ToShipTypeCategoryMapsGroupToCategory(ShipTypeGroup group, ShipTypeCategory expected) + { + group.ToShipTypeCategory().ShouldBe(expected); + } + + [TestMethod] + [DataRow(0, ShipTypeCategory.NotAvailable)] + [DataRow(30, ShipTypeCategory.SpecialCategory3)] + [DataRow(40, ShipTypeCategory.HighSpeedCraft)] + [DataRow(50, ShipTypeCategory.SpecialCategory5)] + [DataRow(60, ShipTypeCategory.Passenger)] + [DataRow(70, ShipTypeCategory.Cargo)] + [DataRow(80, ShipTypeCategory.Tanker)] + [DataRow(90, ShipTypeCategory.Other)] + public void ToShipTypeCategoryClassifiesShipTypeEnumViaItsGroup(int shipType, ShipTypeCategory expected) + { + ((ShipType)shipType).ToShipTypeCategory().ShouldBe(expected); + } + + [TestMethod] + [DataRow(-5, "Not available")] // below the valid range + [DataRow(0, "Not available")] + [DataRow(1, "Reserved for future use")] + [DataRow(10, "Reserved for future use")] + [DataRow(19, "Reserved for future use")] + [DataRow(20, "Wing in ground(WIG), all ships of this type")] + [DataRow(21, "Wing in ground(WIG), Hazardous category A")] + [DataRow(22, "Wing in ground(WIG), Hazardous category B")] + [DataRow(23, "Wing in ground(WIG), Hazardous category C")] + [DataRow(24, "Wing in ground(WIG), Hazardous category D")] + [DataRow(25, "Wing in ground(WIG), Reserved for future use")] + [DataRow(29, "Wing in ground(WIG), Reserved for future use")] + [DataRow(30, "Fishing")] + [DataRow(31, "Towing")] + [DataRow(32, "Towing: length exceeds 200m or breadth exceeds 25m")] + [DataRow(33, "Dredging or underwater ops")] + [DataRow(34, "Diving ops")] + [DataRow(35, "Military ops")] + [DataRow(36, "Sailing")] + [DataRow(37, "Pleasure Craft")] + [DataRow(38, "Reserved")] + [DataRow(39, "Reserved")] + [DataRow(40, "High speed craft(HSC), all ships of this type")] + [DataRow(41, "High speed craft(HSC), Hazardous category A")] + [DataRow(42, "High speed craft(HSC), Hazardous category B")] + [DataRow(43, "High speed craft(HSC), Hazardous category C")] + [DataRow(44, "High speed craft(HSC), Hazardous category D")] + [DataRow(45, "High speed craft(HSC), Reserved for future use")] + [DataRow(48, "High speed craft(HSC), Reserved for future use")] + [DataRow(49, "High speed craft(HSC), No additional information")] + [DataRow(50, "Pilot Vessel")] + [DataRow(51, "Search and Rescue vessel")] + [DataRow(52, "Tug")] + [DataRow(53, "Port Tender")] + [DataRow(54, "Anti - pollution equipment")] + [DataRow(55, "Law Enforcement")] + [DataRow(56, "Spare - Local Vessel")] + [DataRow(57, "Spare - Local Vessel")] + [DataRow(58, "Medical Transport")] + [DataRow(59, "Noncombatant ship according to RR Resolution No. 18")] + [DataRow(60, "Passenger, all ships of this type")] + [DataRow(61, "Passenger, Hazardous category A")] + [DataRow(62, "Passenger, Hazardous category B")] + [DataRow(63, "Passenger, Hazardous category C")] + [DataRow(64, "Passenger, Hazardous category D")] + [DataRow(65, "Passenger, Reserved for future use")] + [DataRow(68, "Passenger, Reserved for future use")] + [DataRow(69, "Passenger, No additional information")] + [DataRow(70, "Cargo, all ships of this type")] + [DataRow(71, "Cargo, Hazardous category A")] + [DataRow(72, "Cargo, Hazardous category B")] + [DataRow(73, "Cargo, Hazardous category C")] + [DataRow(74, "Cargo, Hazardous category D")] + [DataRow(75, "Cargo, Reserved for future use")] + [DataRow(78, "Cargo, Reserved for future use")] + [DataRow(79, "Cargo, No additional information")] + [DataRow(80, "Tanker, all ships of this type")] + [DataRow(81, "Tanker, Hazardous category A")] + [DataRow(82, "Tanker, Hazardous category B")] + [DataRow(83, "Tanker, Hazardous category C")] + [DataRow(84, "Tanker, Hazardous category D")] + [DataRow(85, "Tanker, Reserved for future use")] + [DataRow(88, "Tanker, Reserved for future use")] + [DataRow(89, "Tanker, No additional information")] + [DataRow(90, "Other Type, all ships of this type")] + [DataRow(91, "Other Type, Hazardous category A")] + [DataRow(92, "Other Type, Hazardous category B")] + [DataRow(93, "Other Type, Hazardous category C")] + [DataRow(94, "Other Type, Hazardous category D")] + [DataRow(95, "Other Type, Reserved for future use")] + [DataRow(98, "Other Type, Reserved for future use")] + [DataRow(99, "Other Type, no additional information")] + [DataRow(100, "Not available")] // above the valid range + + // Remaining interior values of each "Reserved for future use" band — every code has its + // own arm in the source, so these are included to exercise all of them. + [DataRow(26, "Wing in ground(WIG), Reserved for future use")] + [DataRow(27, "Wing in ground(WIG), Reserved for future use")] + [DataRow(28, "Wing in ground(WIG), Reserved for future use")] + [DataRow(46, "High speed craft(HSC), Reserved for future use")] + [DataRow(47, "High speed craft(HSC), Reserved for future use")] + [DataRow(66, "Passenger, Reserved for future use")] + [DataRow(67, "Passenger, Reserved for future use")] + [DataRow(76, "Cargo, Reserved for future use")] + [DataRow(77, "Cargo, Reserved for future use")] + [DataRow(86, "Tanker, Reserved for future use")] + [DataRow(87, "Tanker, Reserved for future use")] + [DataRow(96, "Other Type, Reserved for future use")] + [DataRow(97, "Other Type, Reserved for future use")] + public void ToShipTypeDescriptionReturnsHumanReadableText(int shipType, string expected) + { + shipType.ToShipTypeDescription().ShouldBe(expected); + } + + [TestMethod] + public void ToShipTypeGroupAcceptsTheShipTypeEnum() + { + ((ShipType)60).ToShipTypeGroup().ShouldBe(ShipTypeGroup.PassengerAll); + } +} diff --git a/Solutions/Ais.Net.Models.Specs/packages.lock.json b/Solutions/Ais.Net.Models.Specs/packages.lock.json index e2b9594..ba3248a 100644 --- a/Solutions/Ais.Net.Models.Specs/packages.lock.json +++ b/Solutions/Ais.Net.Models.Specs/packages.lock.json @@ -1,284 +1,394 @@ -{ - "version": 1, - "dependencies": { - "net8.0": { - "coverlet.msbuild": { - "type": "Direct", - "requested": "[6.0.2, )", - "resolved": "6.0.2", - "contentHash": "8b4jBNH7mcQy1otyQErjjIUuGD74XxKZ1wvDufbY7jhWwckl7wIa+icjwdPYeI0aYMS4Tp63LIZvyMFjWwOMDw==" - }, - "Microsoft.NET.Test.Sdk": { - "type": "Direct", - "requested": "[17.11.1, )", - "resolved": "17.11.1", - "contentHash": "U3Ty4BaGoEu+T2bwSko9tWqWUOU16WzSFkq6U8zve75oRBMSLTBdMAZrVNNz1Tq12aCdDom9fcOcM9QZaFHqFg==", - "dependencies": { - "Microsoft.CodeCoverage": "17.11.1", - "Microsoft.TestPlatform.TestHost": "17.11.1" - } - }, - "NUnit3TestAdapter": { - "type": "Direct", - "requested": "[4.6.0, )", - "resolved": "4.6.0", - "contentHash": "R7e1+a4vuV/YS+ItfL7f//rG+JBvVeVLX4mHzFEZo4W1qEKl8Zz27AqvQSAqo+BtIzUCo4aAJMYa56VXS4hudw==" - }, - "Reqnroll.NUnit": { - "type": "Direct", - "requested": "[2.1.1, )", - "resolved": "2.1.1", - "contentHash": "VcacexjyMd32HmbiYX/QD5vynid+xLjXym835KR68q0vVAA2PWCCRY+bZkLoGb0k/wsddsTQ+w2lCRq1+U2Z7A==", - "dependencies": { - "NUnit": "3.13.1", - "Reqnroll": "[2.1.1]", - "Reqnroll.Tools.MsBuild.Generation": "[2.1.1]" - } - }, - "Shouldly": { - "type": "Direct", - "requested": "[4.2.1, )", - "resolved": "4.2.1", - "contentHash": "dKAKiSuhLKqD2TXwLKtqNg1nwzJcIKOOMncZjk9LYe4W+h+SCftpWdxwR79YZUIHMH+3Vu9s0s0UHNrgICLwRQ==", - "dependencies": { - "DiffEngine": "11.3.0", - "EmptyFiles": "4.4.0" - } - }, - "Ais.Net": { - "type": "Transitive", - "resolved": "0.4.2", - "contentHash": "+wtx8g0YwxEaDbilf4MiED9S4Yxr9J7RHAAMEN48+yB0it10KYdhmtB6dxt7qdCOvdoDnBOumggcGYefWMz+WA==", - "dependencies": { - "System.IO.Pipelines": "4.7.4" - } - }, - "Cucumber.CucumberExpressions": { - "type": "Transitive", - "resolved": "17.1.0", - "contentHash": "IZFDLLwrUCKuGDXQzmAWZa+kQFpVmVwWD8wEl0UWGOR83UQfzqg8hZeSSNUOXlW0Kh3vMO6kEm3FpNC4amKMaw==" - }, - "DiffEngine": { - "type": "Transitive", - "resolved": "11.3.0", - "contentHash": "k0ZgZqd09jLZQjR8FyQbSQE86Q7QZnjEzq1LPHtj1R2AoWO8sjV5x+jlSisL7NZAbUOI4y+7Bog8gkr9WIRBGw==", - "dependencies": { - "EmptyFiles": "4.4.0", - "System.Management": "6.0.1" - } - }, - "EmptyFiles": { - "type": "Transitive", - "resolved": "4.4.0", - "contentHash": "gwJEfIGS7FhykvtZoscwXj/XwW+mJY6UbAZk+qtLKFUGWC95kfKXnj8VkxsZQnWBxJemM/q664rGLN5nf+OHZw==" - }, - "Gherkin": { - "type": "Transitive", - "resolved": "29.0.0", - "contentHash": "az9zmD5V72UZUzYcDyHoZHFRT5eSLOg1Co2r3AtlshPe41Am7iMwOJG+xHxZ1Pfn6agGoWw1dhm6Pn8Dg6QBUg==" - }, - "Microsoft.CodeCoverage": { - "type": "Transitive", - "resolved": "17.11.1", - "contentHash": "nPJqrcA5iX+Y0kqoT3a+pD/8lrW/V7ayqnEJQsTonSoPz59J8bmoQhcSN4G8+UJ64Hkuf0zuxnfuj2lkHOq4cA==" - }, - "Microsoft.Extensions.DependencyModel": { - "type": "Transitive", - "resolved": "8.0.1", - "contentHash": "5Ou6varcxLBzQ+Agfm0k0pnH7vrEITYlXMDuE6s7ZHlZHz6/G8XJ3iISZDr5rfwfge6RnXJ1+Wc479mMn52vjA==", - "dependencies": { - "System.Text.Encodings.Web": "8.0.0", - "System.Text.Json": "8.0.4" - } - }, - "Microsoft.NETCore.Platforms": { - "type": "Transitive", - "resolved": "1.1.0", - "contentHash": "kz0PEW2lhqygehI/d6XsPCQzD7ff7gUJaVGPVETX611eadGsA3A877GdSlU0LRVMCTH/+P3o2iDTak+S08V2+A==" - }, - "Microsoft.NETCore.Targets": { - "type": "Transitive", - "resolved": "1.1.0", - "contentHash": "aOZA3BWfz9RXjpzt0sRJJMjAscAUm3Hoa4UWAfceV9UTYxgwZ1lZt5nO2myFf+/jetYQo4uTP7zS8sJY67BBxg==" - }, - "Microsoft.TestPlatform.ObjectModel": { - "type": "Transitive", - "resolved": "17.11.1", - "contentHash": "E2jZqAU6JeWEVsyOEOrSW1o1bpHLgb25ypvKNB/moBXPVsFYBPd/Jwi7OrYahG50J83LfHzezYI+GaEkpAotiA==", - "dependencies": { - "System.Reflection.Metadata": "1.6.0" - } - }, - "Microsoft.TestPlatform.TestHost": { - "type": "Transitive", - "resolved": "17.11.1", - "contentHash": "DnG+GOqJXO/CkoqlJWeDFTgPhqD/V6VqUIL3vINizCWZ3X+HshCtbbyDdSHQQEjrc2Sl/K3yaxX6s+5LFEdYuw==", - "dependencies": { - "Microsoft.TestPlatform.ObjectModel": "17.11.1", - "Newtonsoft.Json": "13.0.1" - } - }, - "NETStandard.Library": { - "type": "Transitive", - "resolved": "2.0.0", - "contentHash": "7jnbRU+L08FXKMxqUflxEXtVymWvNOrS8yHgu9s6EM8Anr6T/wIX4nZ08j/u3Asz+tCufp3YVwFSEvFTPYmBPA==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0" - } - }, - "Newtonsoft.Json": { - "type": "Transitive", - "resolved": "13.0.1", - "contentHash": "ppPFpBcvxdsfUonNcvITKqLl3bqxWbDCZIzDWHzjpdAHRFfZe0Dw9HmA0+za13IdyrgJwpkDTDA9fHaxOrt20A==" - }, - "NUnit": { - "type": "Transitive", - "resolved": "3.13.1", - "contentHash": "vWBvrSelmTYwqHWvO3dA63z7cOpaFR/3nJ9MMVLBkWeaWa7oiglPPm5g1h96B4i2XXqjFexxhR5MyMjmIJYPfg==", - "dependencies": { - "NETStandard.Library": "2.0.0" - } - }, - "Reqnroll": { - "type": "Transitive", - "resolved": "2.1.1", - "contentHash": "RhV5B0lxVXzP+0U9OUkWf3nMGfSlWnxQdUp0mcatvuQISxB8ZPjdhJB3T4Vjo3bFMiQb/0LUXUmNZ0IqBPZldw==", - "dependencies": { - "Cucumber.CucumberExpressions": "17.1.0", - "Gherkin": "29.0.0", - "Microsoft.Extensions.DependencyModel": "8.0.1", - "SpecFlow.Internal.Json": "1.0.8", - "System.Runtime.Loader": "4.3.0" - } - }, - "Reqnroll.Tools.MsBuild.Generation": { - "type": "Transitive", - "resolved": "2.1.1", - "contentHash": "xjv2ihoRHpAAjhZX9d964/0R8mhgHiquPBjnJmhc2sYQnemDCkHIsb4d2VAMKS/HeiXLi6NAxujjgAYFBcxDKg==", - "dependencies": { - "Reqnroll": "[2.1.1]" - } - }, - "SpecFlow.Internal.Json": { - "type": "Transitive", - "resolved": "1.0.8", - "contentHash": "lVCC/Rie7N5rFoc7YxPS0nneLfsWSTIMMlkndwxhaD8MxBp3Bsv1HeiVjVwXCjWaQeoqZcvIy52fF5Xit00ZLw==" - }, - "System.CodeDom": { - "type": "Transitive", - "resolved": "6.0.0", - "contentHash": "CPc6tWO1LAer3IzfZufDBRL+UZQcj5uS207NHALQzP84Vp/z6wF0Aa0YZImOQY8iStY0A2zI/e3ihKNPfUm8XA==" - }, - "System.IO": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "3qjaHvxQPDpSOYICjUoTsmoq5u6QJAFRUITgeT/4gqkF1bajbSmb1kwSxEA8AHlofqgcKJcM8udgieRNhaJ5Cg==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "Microsoft.NETCore.Targets": "1.1.0", - "System.Runtime": "4.3.0", - "System.Text.Encoding": "4.3.0", - "System.Threading.Tasks": "4.3.0" - } - }, - "System.IO.Pipelines": { - "type": "Transitive", - "resolved": "4.7.4", - "contentHash": "TQqNjDcTVu6eTDB2S0vjSUhn0udEugL93D9zgsOsSiJO1kv1UWDM2o0+79H95I5X+S0VPEzm4ihj6KPwhNEb7g==" - }, - "System.Management": { - "type": "Transitive", - "resolved": "6.0.1", - "contentHash": "10J1D0h/lioojphfJ4Fuh5ZUThT/xOVHdV9roGBittKKNP2PMjrvibEdbVTGZcPra1399Ja3tqIJLyQrc5Wmhg==", - "dependencies": { - "System.CodeDom": "6.0.0" - } - }, - "System.Reflection": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "KMiAFoW7MfJGa9nDFNcfu+FpEdiHpWgTcS2HdMpDvt9saK3y/G4GwprPyzqjFH9NTaGPQeWNHU+iDlDILj96aQ==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "Microsoft.NETCore.Targets": "1.1.0", - "System.IO": "4.3.0", - "System.Reflection.Primitives": "4.3.0", - "System.Runtime": "4.3.0" - } - }, - "System.Reflection.Metadata": { - "type": "Transitive", - "resolved": "1.6.0", - "contentHash": "COC1aiAJjCoA5GBF+QKL2uLqEBew4JsCkQmoHKbN3TlOZKa2fKLz5CpiRQKDz0RsAOEGsVKqOD5bomsXq/4STQ==" - }, - "System.Reflection.Primitives": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "5RXItQz5As4xN2/YUDxdpsEkMhvw3e6aNveFXUn4Hl/udNTCNhnKp8lT9fnc3MhvGKh1baak5CovpuQUXHAlIA==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "Microsoft.NETCore.Targets": "1.1.0", - "System.Runtime": "4.3.0" - } - }, - "System.Runtime": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "JufQi0vPQ0xGnAczR13AUFglDyVYt4Kqnz1AZaiKZ5+GICq0/1MH/mO/eAJHt/mHW1zjKBJd7kV26SrxddAhiw==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "Microsoft.NETCore.Targets": "1.1.0" - } - }, - "System.Runtime.Loader": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "DHMaRn8D8YCK2GG2pw+UzNxn/OHVfaWx7OTLBD/hPegHZZgcZh3H6seWegrC4BYwsfuGrywIuT+MQs+rPqRLTQ==", - "dependencies": { - "System.IO": "4.3.0", - "System.Reflection": "4.3.0", - "System.Runtime": "4.3.0" - } - }, - "System.Text.Encoding": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "BiIg+KWaSDOITze6jGQynxg64naAPtqGHBwDrLaCtixsa5bKiR8dpPOHA7ge3C0JJQizJE+sfkz1wV+BAKAYZw==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "Microsoft.NETCore.Targets": "1.1.0", - "System.Runtime": "4.3.0" - } - }, - "System.Text.Encodings.Web": { - "type": "Transitive", - "resolved": "8.0.0", - "contentHash": "yev/k9GHAEGx2Rg3/tU6MQh4HGBXJs70y7j1LaM1i/ER9po+6nnQ6RRqTJn1E7Xu0fbIFK80Nh5EoODxrbxwBQ==" - }, - "System.Text.Json": { - "type": "Transitive", - "resolved": "8.0.4", - "contentHash": "bAkhgDJ88XTsqczoxEMliSrpijKZHhbJQldhAmObj/RbrN3sU5dcokuXmWJWsdQAhiMJ9bTayWsL1C9fbbCRhw==", - "dependencies": { - "System.Text.Encodings.Web": "8.0.0" - } - }, - "System.Threading.Tasks": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "LbSxKEdOUhVe8BezB/9uOGGppt+nZf6e1VFyw6v3DN6lqitm0OSn2uXMOdtP0M3W4iMcqcivm2J6UgqiwwnXiA==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "Microsoft.NETCore.Targets": "1.1.0", - "System.Runtime": "4.3.0" - } - }, - "ais.net.models": { - "type": "Project", - "dependencies": { - "Ais.Net": "[0.4.2, )" - } - } - } - } +{ + "version": 2, + "dependencies": { + "net10.0": { + "Microsoft.Testing.Extensions.CodeCoverage": { + "type": "Direct", + "requested": "[18.9.0, )", + "resolved": "18.9.0", + "contentHash": "Odp8djWEUFTJoRV02y0zJaAwAHF31CtNaDgnKKM+8dBHbbaH7Ee0oo9gweLpbzjI7UrtMUzH6xLJSrICZw+Nwg==", + "dependencies": { + "Microsoft.DiaSymReader": "2.2.9", + "Microsoft.Extensions.DependencyModel": "10.0.8", + "Microsoft.Testing.Platform": "2.3.0" + } + }, + "Microsoft.Testing.Extensions.TrxReport": { + "type": "Direct", + "requested": "[2.3.2, )", + "resolved": "2.3.2", + "contentHash": "JzCtiOAmIfkPP2PpAKZeiH1hVYIVC9jMhsPWrd6PnH/Wtuct1UMuxpKi2F1cmXgfokc/HOadC6HOO1qzI23tnA==", + "dependencies": { + "Microsoft.Testing.Extensions.TrxReport.Abstractions": "2.3.2", + "Microsoft.Testing.Platform": "2.3.2" + } + }, + "MSTest.TestAdapter": { + "type": "Direct", + "requested": "[4.3.2, )", + "resolved": "4.3.2", + "contentHash": "uqpjJqQ3ws5z7Pq6BfMFpuqdrN2YIfe0Gfww91LRVXwxBdLqUaJd4JbHUImOSQvmK5m3PqW285kPjKbb69QFoA==", + "dependencies": { + "MSTest.TestFramework": "4.3.2", + "Microsoft.Testing.Extensions.VSTestBridge": "2.3.2", + "Microsoft.Testing.Platform.MSBuild": "2.3.2" + } + }, + "MSTest.TestFramework": { + "type": "Direct", + "requested": "[4.3.2, )", + "resolved": "4.3.2", + "contentHash": "Vlj3THt/BcOV/93k0Cdq29RNEsrY3TvUV6mF13rOuY8Gx7Ltd+LrOYId8AYu0cH9ioCYY/Sx1hppfI9p/vP+aw==", + "dependencies": { + "MSTest.Analyzers": "4.3.2" + } + }, + "NSubstitute": { + "type": "Direct", + "requested": "[6.0.0, )", + "resolved": "6.0.0", + "contentHash": "0gvKMbiJ+/WrfbcfBfqRZZrvfLJcd3rqkqVMjjlY5dtmLRVzMY+o/K/rJUStofQ2haSr9Vd04YDfvZtVVGS3/A==", + "dependencies": { + "Castle.Core": "5.1.1" + } + }, + "Shouldly": { + "type": "Direct", + "requested": "[4.3.0, )", + "resolved": "4.3.0", + "contentHash": "sDetrWXrl6YXZ4HeLsdBoNk3uIa7K+V4uvIJ+cqdRa5DrFxeTED7VkjoxCuU1kJWpUuBDZz2QXFzSxBtVXLwRQ==", + "dependencies": { + "DiffEngine": "11.3.0", + "EmptyFiles": "4.4.0" + } + }, + "Castle.Core": { + "type": "Transitive", + "resolved": "5.1.1", + "contentHash": "rpYtIczkzGpf+EkZgDr9CClTdemhsrwA/W5hMoPjLkRFnXzH44zDLoovXeKtmxb1ykXK9aJVODSpiJml8CTw2g==", + "dependencies": { + "System.Diagnostics.EventLog": "6.0.0" + } + }, + "DiffEngine": { + "type": "Transitive", + "resolved": "11.3.0", + "contentHash": "k0ZgZqd09jLZQjR8FyQbSQE86Q7QZnjEzq1LPHtj1R2AoWO8sjV5x+jlSisL7NZAbUOI4y+7Bog8gkr9WIRBGw==", + "dependencies": { + "EmptyFiles": "4.4.0", + "System.Management": "6.0.1" + } + }, + "EmptyFiles": { + "type": "Transitive", + "resolved": "4.4.0", + "contentHash": "gwJEfIGS7FhykvtZoscwXj/XwW+mJY6UbAZk+qtLKFUGWC95kfKXnj8VkxsZQnWBxJemM/q664rGLN5nf+OHZw==" + }, + "Microsoft.ApplicationInsights": { + "type": "Transitive", + "resolved": "2.23.0", + "contentHash": "nWArUZTdU7iqZLycLKWe0TDms48KKGE6pONH2terYNa8REXiqixrMOkf1sk5DHGMaUTqONU2YkS4SAXBhLStgw==" + }, + "Microsoft.DiaSymReader": { + "type": "Transitive", + "resolved": "2.2.9", + "contentHash": "WLbDc/W9XkHpGVtd1L6Vml8W6+PaaYZZTJASHjHF7W2WX/hld6bfNK102Ui+I3a6zIdx4+oKRJtMgHz44JmBfw==" + }, + "Microsoft.Extensions.DependencyModel": { + "type": "Transitive", + "resolved": "10.0.8", + "contentHash": "vLyZVpxmduO2jx+76ggqnsA3m81kwMY3NkWciNTj5E+Nvqb0VihqCvQP89QsGONWp0AJwMZG+u9GzaCjDdFGNw==" + }, + "Microsoft.Testing.Extensions.Telemetry": { + "type": "Transitive", + "resolved": "2.3.2", + "contentHash": "Q1iapTk1DwFFT9aPtv4wxJHe22++NLxsg7IekEujac9RDAugxGQdCN0SQ1b9ryF78cdidhSBB17pHdCANurOyQ==", + "dependencies": { + "Microsoft.ApplicationInsights": "2.23.0", + "Microsoft.Testing.Platform": "2.3.2" + } + }, + "Microsoft.Testing.Extensions.TrxReport.Abstractions": { + "type": "Transitive", + "resolved": "2.3.2", + "contentHash": "frw+Qa6gUxOZSw2Mw6SPtJT9gPzcP4nS9TfQhS0cHXKL6Rcp/CqLG+o4Psc1bHAhxW04rpzTBhjpNMnw2L7BuQ==", + "dependencies": { + "Microsoft.Testing.Platform": "2.3.2" + } + }, + "Microsoft.Testing.Extensions.VSTestBridge": { + "type": "Transitive", + "resolved": "2.3.2", + "contentHash": "DXJIaLlwt+6GnPurlf7eVMI0wAZV5KCZrgGPibhx9i9LNxAkwEOK2jVzdVjLvvZ/SX9MUUaGJB9yXuF/VoT4/w==", + "dependencies": { + "Microsoft.TestPlatform.ObjectModel": "18.4.0", + "Microsoft.Testing.Extensions.Telemetry": "2.3.2", + "Microsoft.Testing.Extensions.TrxReport.Abstractions": "2.3.2", + "Microsoft.Testing.Platform": "2.3.2" + } + }, + "Microsoft.Testing.Platform": { + "type": "Transitive", + "resolved": "2.3.2", + "contentHash": "FuJgIEfNU+rFHJYGhuqA8uup+3cryJ1N67Tijqo01Jd5xAr28ptz0aXkFfSqK3MhAUmGsrPpgQ92+3+0oXv8AA==" + }, + "Microsoft.Testing.Platform.MSBuild": { + "type": "Transitive", + "resolved": "2.3.2", + "contentHash": "guFvvaE8FGUlB6h24jN+P+NBz/P5x4On9sk4n5UapUAPq4fr0y1AgUe4sXmItp7r2NMe3rhZYLE1teP9T9sVbg==", + "dependencies": { + "Microsoft.Testing.Platform": "2.3.2" + } + }, + "Microsoft.TestPlatform.ObjectModel": { + "type": "Transitive", + "resolved": "18.4.0", + "contentHash": "4L6m2kS2pY5uJ9cpeRxzW22opr6ttScIRqsOpMDQpgENp/ZwxkkQCcmc6LRSURo2dFaaSW5KVflQZvroiJ7Wzg==" + }, + "MSTest.Analyzers": { + "type": "Transitive", + "resolved": "4.3.2", + "contentHash": "GNvrVI6/Y5pNoKnK1/VELBls3XiHNZaj+zh14LtwlCqFI8zXTW3vuBe0Jv2qEr9wxHlOAQQBTJPRoNRjneGrKQ==" + }, + "System.CodeDom": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "CPc6tWO1LAer3IzfZufDBRL+UZQcj5uS207NHALQzP84Vp/z6wF0Aa0YZImOQY8iStY0A2zI/e3ihKNPfUm8XA==" + }, + "System.Diagnostics.EventLog": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "lcyUiXTsETK2ALsZrX+nWuHSIQeazhqPphLfaRxzdGaG93+0kELqpgEHtwWOlQe7+jSFnKwaCAgL4kjeZCQJnw==" + }, + "System.Management": { + "type": "Transitive", + "resolved": "6.0.1", + "contentHash": "10J1D0h/lioojphfJ4Fuh5ZUThT/xOVHdV9roGBittKKNP2PMjrvibEdbVTGZcPra1399Ja3tqIJLyQrc5Wmhg==", + "dependencies": { + "System.CodeDom": "6.0.0" + } + }, + "ais.net.models": { + "type": "Project", + "dependencies": { + "Ais.Net": "[0.4.2, )" + } + }, + "Ais.Net": { + "type": "CentralTransitive", + "requested": "[0.4.2, )", + "resolved": "0.4.2", + "contentHash": "+wtx8g0YwxEaDbilf4MiED9S4Yxr9J7RHAAMEN48+yB0it10KYdhmtB6dxt7qdCOvdoDnBOumggcGYefWMz+WA==" + } + }, + "net8.0": { + "Microsoft.Testing.Extensions.CodeCoverage": { + "type": "Direct", + "requested": "[18.9.0, )", + "resolved": "18.9.0", + "contentHash": "Odp8djWEUFTJoRV02y0zJaAwAHF31CtNaDgnKKM+8dBHbbaH7Ee0oo9gweLpbzjI7UrtMUzH6xLJSrICZw+Nwg==", + "dependencies": { + "Microsoft.DiaSymReader": "2.2.9", + "Microsoft.Extensions.DependencyModel": "10.0.8", + "Microsoft.Testing.Platform": "2.3.0", + "System.IO.Pipelines": "10.0.8", + "System.Text.Encodings.Web": "10.0.8", + "System.Text.Json": "10.0.8" + } + }, + "Microsoft.Testing.Extensions.TrxReport": { + "type": "Direct", + "requested": "[2.3.2, )", + "resolved": "2.3.2", + "contentHash": "JzCtiOAmIfkPP2PpAKZeiH1hVYIVC9jMhsPWrd6PnH/Wtuct1UMuxpKi2F1cmXgfokc/HOadC6HOO1qzI23tnA==", + "dependencies": { + "Microsoft.Testing.Extensions.TrxReport.Abstractions": "2.3.2", + "Microsoft.Testing.Platform": "2.3.2" + } + }, + "MSTest.TestAdapter": { + "type": "Direct", + "requested": "[4.3.2, )", + "resolved": "4.3.2", + "contentHash": "uqpjJqQ3ws5z7Pq6BfMFpuqdrN2YIfe0Gfww91LRVXwxBdLqUaJd4JbHUImOSQvmK5m3PqW285kPjKbb69QFoA==", + "dependencies": { + "MSTest.TestFramework": "4.3.2", + "Microsoft.Testing.Extensions.VSTestBridge": "2.3.2", + "Microsoft.Testing.Platform.MSBuild": "2.3.2" + } + }, + "MSTest.TestFramework": { + "type": "Direct", + "requested": "[4.3.2, )", + "resolved": "4.3.2", + "contentHash": "Vlj3THt/BcOV/93k0Cdq29RNEsrY3TvUV6mF13rOuY8Gx7Ltd+LrOYId8AYu0cH9ioCYY/Sx1hppfI9p/vP+aw==", + "dependencies": { + "MSTest.Analyzers": "4.3.2" + } + }, + "NSubstitute": { + "type": "Direct", + "requested": "[6.0.0, )", + "resolved": "6.0.0", + "contentHash": "0gvKMbiJ+/WrfbcfBfqRZZrvfLJcd3rqkqVMjjlY5dtmLRVzMY+o/K/rJUStofQ2haSr9Vd04YDfvZtVVGS3/A==", + "dependencies": { + "Castle.Core": "5.1.1" + } + }, + "Shouldly": { + "type": "Direct", + "requested": "[4.3.0, )", + "resolved": "4.3.0", + "contentHash": "sDetrWXrl6YXZ4HeLsdBoNk3uIa7K+V4uvIJ+cqdRa5DrFxeTED7VkjoxCuU1kJWpUuBDZz2QXFzSxBtVXLwRQ==", + "dependencies": { + "DiffEngine": "11.3.0", + "EmptyFiles": "4.4.0" + } + }, + "Castle.Core": { + "type": "Transitive", + "resolved": "5.1.1", + "contentHash": "rpYtIczkzGpf+EkZgDr9CClTdemhsrwA/W5hMoPjLkRFnXzH44zDLoovXeKtmxb1ykXK9aJVODSpiJml8CTw2g==", + "dependencies": { + "System.Diagnostics.EventLog": "6.0.0" + } + }, + "DiffEngine": { + "type": "Transitive", + "resolved": "11.3.0", + "contentHash": "k0ZgZqd09jLZQjR8FyQbSQE86Q7QZnjEzq1LPHtj1R2AoWO8sjV5x+jlSisL7NZAbUOI4y+7Bog8gkr9WIRBGw==", + "dependencies": { + "EmptyFiles": "4.4.0", + "System.Management": "6.0.1" + } + }, + "EmptyFiles": { + "type": "Transitive", + "resolved": "4.4.0", + "contentHash": "gwJEfIGS7FhykvtZoscwXj/XwW+mJY6UbAZk+qtLKFUGWC95kfKXnj8VkxsZQnWBxJemM/q664rGLN5nf+OHZw==" + }, + "Microsoft.ApplicationInsights": { + "type": "Transitive", + "resolved": "2.23.0", + "contentHash": "nWArUZTdU7iqZLycLKWe0TDms48KKGE6pONH2terYNa8REXiqixrMOkf1sk5DHGMaUTqONU2YkS4SAXBhLStgw==" + }, + "Microsoft.DiaSymReader": { + "type": "Transitive", + "resolved": "2.2.9", + "contentHash": "WLbDc/W9XkHpGVtd1L6Vml8W6+PaaYZZTJASHjHF7W2WX/hld6bfNK102Ui+I3a6zIdx4+oKRJtMgHz44JmBfw==" + }, + "Microsoft.Extensions.DependencyModel": { + "type": "Transitive", + "resolved": "10.0.8", + "contentHash": "vLyZVpxmduO2jx+76ggqnsA3m81kwMY3NkWciNTj5E+Nvqb0VihqCvQP89QsGONWp0AJwMZG+u9GzaCjDdFGNw==", + "dependencies": { + "System.Text.Encodings.Web": "10.0.8", + "System.Text.Json": "10.0.8" + } + }, + "Microsoft.Testing.Extensions.Telemetry": { + "type": "Transitive", + "resolved": "2.3.2", + "contentHash": "Q1iapTk1DwFFT9aPtv4wxJHe22++NLxsg7IekEujac9RDAugxGQdCN0SQ1b9ryF78cdidhSBB17pHdCANurOyQ==", + "dependencies": { + "Microsoft.ApplicationInsights": "2.23.0", + "Microsoft.Testing.Platform": "2.3.2" + } + }, + "Microsoft.Testing.Extensions.TrxReport.Abstractions": { + "type": "Transitive", + "resolved": "2.3.2", + "contentHash": "frw+Qa6gUxOZSw2Mw6SPtJT9gPzcP4nS9TfQhS0cHXKL6Rcp/CqLG+o4Psc1bHAhxW04rpzTBhjpNMnw2L7BuQ==", + "dependencies": { + "Microsoft.Testing.Platform": "2.3.2" + } + }, + "Microsoft.Testing.Extensions.VSTestBridge": { + "type": "Transitive", + "resolved": "2.3.2", + "contentHash": "DXJIaLlwt+6GnPurlf7eVMI0wAZV5KCZrgGPibhx9i9LNxAkwEOK2jVzdVjLvvZ/SX9MUUaGJB9yXuF/VoT4/w==", + "dependencies": { + "Microsoft.TestPlatform.ObjectModel": "18.4.0", + "Microsoft.Testing.Extensions.Telemetry": "2.3.2", + "Microsoft.Testing.Extensions.TrxReport.Abstractions": "2.3.2", + "Microsoft.Testing.Platform": "2.3.2" + } + }, + "Microsoft.Testing.Platform": { + "type": "Transitive", + "resolved": "2.3.2", + "contentHash": "FuJgIEfNU+rFHJYGhuqA8uup+3cryJ1N67Tijqo01Jd5xAr28ptz0aXkFfSqK3MhAUmGsrPpgQ92+3+0oXv8AA==" + }, + "Microsoft.Testing.Platform.MSBuild": { + "type": "Transitive", + "resolved": "2.3.2", + "contentHash": "guFvvaE8FGUlB6h24jN+P+NBz/P5x4On9sk4n5UapUAPq4fr0y1AgUe4sXmItp7r2NMe3rhZYLE1teP9T9sVbg==", + "dependencies": { + "Microsoft.Testing.Platform": "2.3.2" + } + }, + "Microsoft.TestPlatform.ObjectModel": { + "type": "Transitive", + "resolved": "18.4.0", + "contentHash": "4L6m2kS2pY5uJ9cpeRxzW22opr6ttScIRqsOpMDQpgENp/ZwxkkQCcmc6LRSURo2dFaaSW5KVflQZvroiJ7Wzg==" + }, + "MSTest.Analyzers": { + "type": "Transitive", + "resolved": "4.3.2", + "contentHash": "GNvrVI6/Y5pNoKnK1/VELBls3XiHNZaj+zh14LtwlCqFI8zXTW3vuBe0Jv2qEr9wxHlOAQQBTJPRoNRjneGrKQ==" + }, + "System.CodeDom": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "CPc6tWO1LAer3IzfZufDBRL+UZQcj5uS207NHALQzP84Vp/z6wF0Aa0YZImOQY8iStY0A2zI/e3ihKNPfUm8XA==" + }, + "System.Diagnostics.EventLog": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "lcyUiXTsETK2ALsZrX+nWuHSIQeazhqPphLfaRxzdGaG93+0kELqpgEHtwWOlQe7+jSFnKwaCAgL4kjeZCQJnw==" + }, + "System.IO.Pipelines": { + "type": "Transitive", + "resolved": "10.0.8", + "contentHash": "STVNTIVd+UrYvSo31D2tOOTs19IThdjGDN14FS3/NZb4PWsVakAg/VT4sq+JYWGP/GtazzvXwebWL6LPBAwnFQ==" + }, + "System.Management": { + "type": "Transitive", + "resolved": "6.0.1", + "contentHash": "10J1D0h/lioojphfJ4Fuh5ZUThT/xOVHdV9roGBittKKNP2PMjrvibEdbVTGZcPra1399Ja3tqIJLyQrc5Wmhg==", + "dependencies": { + "System.CodeDom": "6.0.0" + } + }, + "System.Text.Encodings.Web": { + "type": "Transitive", + "resolved": "10.0.8", + "contentHash": "HvFIJXM/CMTRu6PBQmjukQZ/O32Vx5fOEYZs0kq0OD5s9vJQ19KHLWCVmnfh3gNC7pbYomm06tzOTgJBflr/nQ==" + }, + "System.Text.Json": { + "type": "Transitive", + "resolved": "10.0.8", + "contentHash": "/fysUDkD7oFGaRPoA7IaFs0wRoO3GlwlCNq2P+xWZqxLy1R4cktRSKfMjJDy9ymS4grL7IDVdt8de8L9a0z55Q==", + "dependencies": { + "System.IO.Pipelines": "10.0.8", + "System.Text.Encodings.Web": "10.0.8" + } + }, + "ais.net.models": { + "type": "Project", + "dependencies": { + "Ais.Net": "[0.4.2, )" + } + }, + "Ais.Net": { + "type": "CentralTransitive", + "requested": "[0.4.2, )", + "resolved": "0.4.2", + "contentHash": "+wtx8g0YwxEaDbilf4MiED9S4Yxr9J7RHAAMEN48+yB0it10KYdhmtB6dxt7qdCOvdoDnBOumggcGYefWMz+WA==", + "dependencies": { + "System.IO.Pipelines": "4.7.4" + } + } + } + } } \ No newline at end of file diff --git a/Solutions/Ais.Net.Models.sln b/Solutions/Ais.Net.Models.sln deleted file mode 100644 index 476f7e4..0000000 --- a/Solutions/Ais.Net.Models.sln +++ /dev/null @@ -1,38 +0,0 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 17 -VisualStudioVersion = 17.0.32126.317 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{6C0840FD-F495-41BA-9D68-2B691313D2DC}" - ProjectSection(SolutionItems) = preProject - .editorconfig = .editorconfig - ..\GitVersion.yml = ..\GitVersion.yml - ..\README.md = ..\README.md - EndProjectSection -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ais.Net.Models", "Ais.Net.Models\Ais.Net.Models.csproj", "{5B090F5E-28EA-4394-83DB-8C2764970FDC}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ais.Net.Models.Specs", "Ais.Net.Models.Specs\Ais.Net.Models.Specs.csproj", "{AA6F29A1-3F00-4796-A7C7-9EDFD6C677F8}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {5B090F5E-28EA-4394-83DB-8C2764970FDC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {5B090F5E-28EA-4394-83DB-8C2764970FDC}.Debug|Any CPU.Build.0 = Debug|Any CPU - {5B090F5E-28EA-4394-83DB-8C2764970FDC}.Release|Any CPU.ActiveCfg = Release|Any CPU - {5B090F5E-28EA-4394-83DB-8C2764970FDC}.Release|Any CPU.Build.0 = Release|Any CPU - {AA6F29A1-3F00-4796-A7C7-9EDFD6C677F8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {AA6F29A1-3F00-4796-A7C7-9EDFD6C677F8}.Debug|Any CPU.Build.0 = Debug|Any CPU - {AA6F29A1-3F00-4796-A7C7-9EDFD6C677F8}.Release|Any CPU.ActiveCfg = Release|Any CPU - {AA6F29A1-3F00-4796-A7C7-9EDFD6C677F8}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {B807F890-1DEB-4C00-A4FC-67EC55DE21F5} - EndGlobalSection -EndGlobal diff --git a/Solutions/Ais.Net.Models.slnx b/Solutions/Ais.Net.Models.slnx new file mode 100644 index 0000000..830c3a3 --- /dev/null +++ b/Solutions/Ais.Net.Models.slnx @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/Solutions/Ais.Net.Models/Ais.Net.Models.csproj b/Solutions/Ais.Net.Models/Ais.Net.Models.csproj index debd8fd..a0e40c7 100644 --- a/Solutions/Ais.Net.Models/Ais.Net.Models.csproj +++ b/Solutions/Ais.Net.Models/Ais.Net.Models.csproj @@ -3,10 +3,12 @@ - net8.0 + net8.0;net10.0 latest enable True + + true @@ -17,12 +19,12 @@ - RCS1029;SA1313;SA1009;SA1600;SA1591;CS1591; + RCS1029;SA1313;SA1009; - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/Solutions/Ais.Net.Models/Ais/Net/Models/Abstractions/IAisIsAssigned.cs b/Solutions/Ais.Net.Models/Ais/Net/Models/Abstractions/IAisIsAssigned.cs index 3eb08c4..8e1c6e0 100644 --- a/Solutions/Ais.Net.Models/Ais/Net/Models/Abstractions/IAisIsAssigned.cs +++ b/Solutions/Ais.Net.Models/Ais/Net/Models/Abstractions/IAisIsAssigned.cs @@ -4,7 +4,13 @@ namespace Ais.Net.Models.Abstractions; +/// +/// Exposes the AIS assigned-mode status flag. +/// public interface IAisIsAssigned { + /// + /// Gets a value indicating whether the station is operating in assigned mode. + /// bool IsAssigned { get; } } \ No newline at end of file diff --git a/Solutions/Ais.Net.Models/Ais/Net/Models/Abstractions/IAisIsDteNotReady.cs b/Solutions/Ais.Net.Models/Ais/Net/Models/Abstractions/IAisIsDteNotReady.cs index 05f42ce..01bb786 100644 --- a/Solutions/Ais.Net.Models/Ais/Net/Models/Abstractions/IAisIsDteNotReady.cs +++ b/Solutions/Ais.Net.Models/Ais/Net/Models/Abstractions/IAisIsDteNotReady.cs @@ -4,7 +4,13 @@ namespace Ais.Net.Models.Abstractions; +/// +/// Exposes the AIS data terminal equipment (DTE) readiness status flag. +/// public interface IAisIsDteNotReady { + /// + /// Gets a value indicating whether the data terminal equipment (DTE) is not ready. + /// bool IsDteNotReady { get; } } \ No newline at end of file diff --git a/Solutions/Ais.Net.Models/Ais/Net/Models/Abstractions/IAisMessage.cs b/Solutions/Ais.Net.Models/Ais/Net/Models/Abstractions/IAisMessage.cs index 345d16b..3b87794 100644 --- a/Solutions/Ais.Net.Models/Ais/Net/Models/Abstractions/IAisMessage.cs +++ b/Solutions/Ais.Net.Models/Ais/Net/Models/Abstractions/IAisMessage.cs @@ -4,6 +4,9 @@ namespace Ais.Net.Models.Abstractions; +/// +/// Common abstraction for an AIS message, combining vessel identity with the message type identifier. +/// public interface IAisMessage : IVesselIdentity, IAisMessageType { } \ No newline at end of file diff --git a/Solutions/Ais.Net.Models/Ais/Net/Models/Abstractions/IAisMessageType.cs b/Solutions/Ais.Net.Models/Ais/Net/Models/Abstractions/IAisMessageType.cs index 8681284..f70c7c7 100644 --- a/Solutions/Ais.Net.Models/Ais/Net/Models/Abstractions/IAisMessageType.cs +++ b/Solutions/Ais.Net.Models/Ais/Net/Models/Abstractions/IAisMessageType.cs @@ -4,7 +4,13 @@ namespace Ais.Net.Models.Abstractions; +/// +/// Exposes the numeric AIS message type identifier. +/// public interface IAisMessageType { + /// + /// Gets the numeric AIS message type identifier. + /// int MessageType { get; } } \ No newline at end of file diff --git a/Solutions/Ais.Net.Models/Ais/Net/Models/Abstractions/IAisMessageType18.cs b/Solutions/Ais.Net.Models/Ais/Net/Models/Abstractions/IAisMessageType18.cs index bbb01d1..ca3c33a 100644 --- a/Solutions/Ais.Net.Models/Ais/Net/Models/Abstractions/IAisMessageType18.cs +++ b/Solutions/Ais.Net.Models/Ais/Net/Models/Abstractions/IAisMessageType18.cs @@ -4,21 +4,48 @@ namespace Ais.Net.Models.Abstractions; +/// +/// Describes the properties specific to an AIS message type 18 (Class B "CS" position report). +/// public interface IAisMessageType18 { + /// + /// Gets a value indicating whether the unit can accept a channel assignment via a message type 22. + /// bool CanAcceptMessage22ChannelAssignment { get; } + /// + /// Gets a value indicating whether the unit can switch frequency bands. + /// bool CanSwitchBands { get; } + /// + /// Gets the Class B carrier sense (CS) unit type. + /// ClassBUnit CsUnit { get; } + /// + /// Gets a value indicating whether the unit has a display. + /// bool HasDisplay { get; } + /// + /// Gets a value indicating whether a digital selective calling (DSC) transceiver is attached. + /// bool IsDscAttached { get; } + /// + /// Gets the Class B radio status type indicating how the radio status field is encoded. + /// ClassBRadioStatusType RadioStatusType { get; } + /// + /// Gets the regional reserved value held in the bits at offset 139. + /// int RegionalReserved139 { get; } + /// + /// Gets the regional reserved value held in the bits at offset 38. + /// int RegionalReserved38 { get; } } \ No newline at end of file diff --git a/Solutions/Ais.Net.Models/Ais/Net/Models/Abstractions/IAisMessageType19.cs b/Solutions/Ais.Net.Models/Ais/Net/Models/Abstractions/IAisMessageType19.cs index 1693bbf..38df203 100644 --- a/Solutions/Ais.Net.Models/Ais/Net/Models/Abstractions/IAisMessageType19.cs +++ b/Solutions/Ais.Net.Models/Ais/Net/Models/Abstractions/IAisMessageType19.cs @@ -4,13 +4,28 @@ namespace Ais.Net.Models.Abstractions; +/// +/// Describes the properties specific to an AIS message type 19 (Class B "SO" extended position report). +/// public interface IAisMessageType19 { + /// + /// Gets the regional reserved value held in the bits at offset 139. + /// int RegionalReserved139 { get; } + /// + /// Gets the regional reserved value held in the bits at offset 38. + /// int RegionalReserved38 { get; } + /// + /// Gets the name of the ship. + /// string ShipName { get; } + /// + /// Gets the spare value held in the bits at offset 308. + /// uint Spare308 { get; } } \ No newline at end of file diff --git a/Solutions/Ais.Net.Models/Ais/Net/Models/Abstractions/IAisMessageType1to3.cs b/Solutions/Ais.Net.Models/Ais/Net/Models/Abstractions/IAisMessageType1to3.cs index 7c98287..6c65c84 100644 --- a/Solutions/Ais.Net.Models/Ais/Net/Models/Abstractions/IAisMessageType1to3.cs +++ b/Solutions/Ais.Net.Models/Ais/Net/Models/Abstractions/IAisMessageType1to3.cs @@ -4,17 +4,38 @@ namespace Ais.Net.Models.Abstractions; +/// +/// Describes the properties specific to an AIS message type 1, 2 or 3 (Class A position report). +/// public interface IAisMessageType1to3 { + /// + /// Gets the special manoeuvre indicator for the vessel. + /// ManoeuvreIndicator ManoeuvreIndicator { get; } + /// + /// Gets the number of frames remaining until the transmission slot times out. + /// uint RadioSlotTimeout { get; } + /// + /// Gets the radio sub-message, whose meaning depends on the slot timeout value. + /// uint RadioSubMessage { get; } + /// + /// Gets the radio synchronisation state. + /// RadioSyncState RadioSyncState { get; } + /// + /// Gets the rate of turn of the vessel. + /// int RateOfTurn { get; } + /// + /// Gets the spare value held in the bits at offset 145. + /// uint SpareBits145 { get; } } \ No newline at end of file diff --git a/Solutions/Ais.Net.Models/Ais/Net/Models/Abstractions/IAisMessageType24Part0.cs b/Solutions/Ais.Net.Models/Ais/Net/Models/Abstractions/IAisMessageType24Part0.cs index eb3d8cc..040599e 100644 --- a/Solutions/Ais.Net.Models/Ais/Net/Models/Abstractions/IAisMessageType24Part0.cs +++ b/Solutions/Ais.Net.Models/Ais/Net/Models/Abstractions/IAisMessageType24Part0.cs @@ -4,7 +4,13 @@ namespace Ais.Net.Models.Abstractions; +/// +/// Describes the properties specific to an AIS message type 24 Part A (static data report carrying the vessel name). +/// public interface IAisMessageType24Part0 { + /// + /// Gets the spare value held in the bits at offset 160. + /// uint Spare160 { get; } } \ No newline at end of file diff --git a/Solutions/Ais.Net.Models/Ais/Net/Models/Abstractions/IAisMessageType24Part1.cs b/Solutions/Ais.Net.Models/Ais/Net/Models/Abstractions/IAisMessageType24Part1.cs index 0d08ae9..4fd1247 100644 --- a/Solutions/Ais.Net.Models/Ais/Net/Models/Abstractions/IAisMessageType24Part1.cs +++ b/Solutions/Ais.Net.Models/Ais/Net/Models/Abstractions/IAisMessageType24Part1.cs @@ -4,17 +4,38 @@ namespace Ais.Net.Models.Abstractions; +/// +/// Describes the properties specific to an AIS message type 24 Part B (static data report carrying vessel dimensions and equipment details). +/// public interface IAisMessageType24Part1 { + /// + /// Gets the MMSI of the mothership associated with this unit. + /// uint MothershipMmsi { get; } + /// + /// Gets the manufacturer-assigned serial number of the unit. + /// uint SerialNumber { get; } + /// + /// Gets the spare value held in the bits at offset 162. + /// uint Spare162 { get; } + /// + /// Gets the manufacturer-assigned unit model code. + /// uint UnitModelCode { get; } + /// + /// Gets the vendor identifier as interpreted under ITU-R M.1371 revision 3. + /// string VendorIdRev3 { get; } + /// + /// Gets the vendor identifier as interpreted under ITU-R M.1371 revision 4. + /// string VendorIdRev4 { get; } } \ No newline at end of file diff --git a/Solutions/Ais.Net.Models/Ais/Net/Models/Abstractions/IAisMessageType27.cs b/Solutions/Ais.Net.Models/Ais/Net/Models/Abstractions/IAisMessageType27.cs index efc086b..8836ba2 100644 --- a/Solutions/Ais.Net.Models/Ais/Net/Models/Abstractions/IAisMessageType27.cs +++ b/Solutions/Ais.Net.Models/Ais/Net/Models/Abstractions/IAisMessageType27.cs @@ -1,12 +1,21 @@ -// -// Copyright (c) Endjin Limited. All rights reserved. -// - -namespace Ais.Net.Models.Abstractions; - -public interface IAisMessageType27 -{ - bool GnssPositionStatus { get; } - - Position? Position { get; } +// +// Copyright (c) Endjin Limited. All rights reserved. +// + +namespace Ais.Net.Models.Abstractions; + +/// +/// Describes the properties specific to an AIS message type 27 (long-range broadcast position report). +/// +public interface IAisMessageType27 +{ + /// + /// Gets a value indicating whether the GNSS position is being reported by the position system. + /// + bool GnssPositionStatus { get; } + + /// + /// Gets the reported position of the vessel, or if no position is available. + /// + Position? Position { get; } } \ No newline at end of file diff --git a/Solutions/Ais.Net.Models/Ais/Net/Models/Abstractions/IAisMessageType5.cs b/Solutions/Ais.Net.Models/Ais/Net/Models/Abstractions/IAisMessageType5.cs index 4683a7c..c7c7a7e 100644 --- a/Solutions/Ais.Net.Models/Ais/Net/Models/Abstractions/IAisMessageType5.cs +++ b/Solutions/Ais.Net.Models/Ais/Net/Models/Abstractions/IAisMessageType5.cs @@ -4,23 +4,53 @@ namespace Ais.Net.Models.Abstractions; +/// +/// Describes the properties specific to an AIS message type 5 (static and voyage related data). +/// public interface IAisMessageType5 { + /// + /// Gets the AIS version indicator supported by the station. + /// uint AisVersion { get; } + /// + /// Gets the destination of the voyage. + /// string Destination { get; } + /// + /// Gets the maximum present static draught, expressed in tenths of a metre. + /// uint Draught10thMetres { get; } + /// + /// Gets the month component of the estimated time of arrival (ETA). + /// uint EtaMonth { get; } + /// + /// Gets the day component of the estimated time of arrival (ETA). + /// uint EtaDay { get; } + /// + /// Gets the hour component of the estimated time of arrival (ETA). + /// uint EtaHour { get; } + /// + /// Gets the minute component of the estimated time of arrival (ETA). + /// uint EtaMinute { get; } + /// + /// Gets the IMO number identifying the vessel. + /// uint ImoNumber { get; } + /// + /// Gets the spare value held in the bits at offset 423. + /// uint Spare423 { get; } } \ No newline at end of file diff --git a/Solutions/Ais.Net.Models/Ais/Net/Models/Abstractions/IAisMultipartMessage.cs b/Solutions/Ais.Net.Models/Ais/Net/Models/Abstractions/IAisMultipartMessage.cs index b116ed4..88466ce 100644 --- a/Solutions/Ais.Net.Models/Ais/Net/Models/Abstractions/IAisMultipartMessage.cs +++ b/Solutions/Ais.Net.Models/Ais/Net/Models/Abstractions/IAisMultipartMessage.cs @@ -4,7 +4,13 @@ namespace Ais.Net.Models.Abstractions; +/// +/// Marks an AIS message that is split across multiple parts. +/// public interface IAisMultipartMessage { + /// + /// Gets the zero-based part number identifying this part of the multipart message. + /// uint PartNumber { get; } } \ No newline at end of file diff --git a/Solutions/Ais.Net.Models/Ais/Net/Models/Abstractions/IAisPositionFixType.cs b/Solutions/Ais.Net.Models/Ais/Net/Models/Abstractions/IAisPositionFixType.cs index b6d3b52..93ba542 100644 --- a/Solutions/Ais.Net.Models/Ais/Net/Models/Abstractions/IAisPositionFixType.cs +++ b/Solutions/Ais.Net.Models/Ais/Net/Models/Abstractions/IAisPositionFixType.cs @@ -4,7 +4,13 @@ namespace Ais.Net.Models.Abstractions; +/// +/// Exposes the electronic position fixing device (EPFD) type used by the vessel. +/// public interface IAisPositionFixType { + /// + /// Gets the type of electronic position fixing device (EPFD) used to obtain the position fix. + /// EpfdFixType PositionFixType { get; } } \ No newline at end of file diff --git a/Solutions/Ais.Net.Models/Ais/Net/Models/Abstractions/ICallSign.cs b/Solutions/Ais.Net.Models/Ais/Net/Models/Abstractions/ICallSign.cs index 44fc845..7938a6e 100644 --- a/Solutions/Ais.Net.Models/Ais/Net/Models/Abstractions/ICallSign.cs +++ b/Solutions/Ais.Net.Models/Ais/Net/Models/Abstractions/ICallSign.cs @@ -4,7 +4,13 @@ namespace Ais.Net.Models.Abstractions; +/// +/// Exposes the vessel's radio call sign. +/// public interface ICallSign { + /// + /// Gets the vessel's radio call sign. + /// string CallSign { get; } } \ No newline at end of file diff --git a/Solutions/Ais.Net.Models/Ais/Net/Models/Abstractions/IRaimFlag.cs b/Solutions/Ais.Net.Models/Ais/Net/Models/Abstractions/IRaimFlag.cs index 1c1a6fb..e05f44f 100644 --- a/Solutions/Ais.Net.Models/Ais/Net/Models/Abstractions/IRaimFlag.cs +++ b/Solutions/Ais.Net.Models/Ais/Net/Models/Abstractions/IRaimFlag.cs @@ -4,7 +4,13 @@ namespace Ais.Net.Models.Abstractions; +/// +/// Exposes whether Receiver Autonomous Integrity Monitoring (RAIM) is in use. +/// public interface IRaimFlag { + /// + /// Gets a value indicating whether Receiver Autonomous Integrity Monitoring (RAIM) is in use by the position fixing device. + /// bool RaimFlag { get; } } \ No newline at end of file diff --git a/Solutions/Ais.Net.Models/Ais/Net/Models/Abstractions/IRepeatIndicator.cs b/Solutions/Ais.Net.Models/Ais/Net/Models/Abstractions/IRepeatIndicator.cs index a0a50b1..b1286f2 100644 --- a/Solutions/Ais.Net.Models/Ais/Net/Models/Abstractions/IRepeatIndicator.cs +++ b/Solutions/Ais.Net.Models/Ais/Net/Models/Abstractions/IRepeatIndicator.cs @@ -4,7 +4,13 @@ namespace Ais.Net.Models.Abstractions; +/// +/// Exposes how many times the message has been repeated by a repeater. +/// public interface IRepeatIndicator { + /// + /// Gets the number of times the message has been repeated by a repeater. + /// uint RepeatIndicator { get; } } \ No newline at end of file diff --git a/Solutions/Ais.Net.Models/Ais/Net/Models/Abstractions/IShipType.cs b/Solutions/Ais.Net.Models/Ais/Net/Models/Abstractions/IShipType.cs index eb1b85a..fb5013a 100644 --- a/Solutions/Ais.Net.Models/Ais/Net/Models/Abstractions/IShipType.cs +++ b/Solutions/Ais.Net.Models/Ais/Net/Models/Abstractions/IShipType.cs @@ -4,7 +4,13 @@ namespace Ais.Net.Models.Abstractions; +/// +/// Exposes the ship-and-cargo type. +/// public interface IShipType { + /// + /// Gets the ship-and-cargo type of the vessel. + /// ShipType ShipType { get; } } \ No newline at end of file diff --git a/Solutions/Ais.Net.Models/Ais/Net/Models/Abstractions/IVesselCourseOverGround.cs b/Solutions/Ais.Net.Models/Ais/Net/Models/Abstractions/IVesselCourseOverGround.cs index f3e5bf2..6b30b55 100644 --- a/Solutions/Ais.Net.Models/Ais/Net/Models/Abstractions/IVesselCourseOverGround.cs +++ b/Solutions/Ais.Net.Models/Ais/Net/Models/Abstractions/IVesselCourseOverGround.cs @@ -1,10 +1,16 @@ -// -// Copyright (c) Endjin Limited. All rights reserved. -// - -namespace Ais.Net.Models.Abstractions; - -public interface IVesselCourseOverGround -{ - float? CourseOverGround { get; } +// +// Copyright (c) Endjin Limited. All rights reserved. +// + +namespace Ais.Net.Models.Abstractions; + +/// +/// Exposes the vessel's course over ground. +/// +public interface IVesselCourseOverGround +{ + /// + /// Gets the course over ground in degrees, or null when not available. + /// + float? CourseOverGround { get; } } \ No newline at end of file diff --git a/Solutions/Ais.Net.Models/Ais/Net/Models/Abstractions/IVesselDimensions.cs b/Solutions/Ais.Net.Models/Ais/Net/Models/Abstractions/IVesselDimensions.cs index 8e4388f..a7f8f35 100644 --- a/Solutions/Ais.Net.Models/Ais/Net/Models/Abstractions/IVesselDimensions.cs +++ b/Solutions/Ais.Net.Models/Ais/Net/Models/Abstractions/IVesselDimensions.cs @@ -4,13 +4,28 @@ namespace Ais.Net.Models.Abstractions; +/// +/// Exposes the vessel's dimensions, measured in metres from the position reference point. +/// public interface IVesselDimensions { + /// + /// Gets the distance in metres from the position reference point to the bow of the vessel. + /// uint DimensionToBow { get; } + /// + /// Gets the distance in metres from the position reference point to the port side of the vessel. + /// uint DimensionToPort { get; } + /// + /// Gets the distance in metres from the position reference point to the starboard side of the vessel. + /// uint DimensionToStarboard { get; } + /// + /// Gets the distance in metres from the position reference point to the stern of the vessel. + /// uint DimensionToStern { get; } } \ No newline at end of file diff --git a/Solutions/Ais.Net.Models/Ais/Net/Models/Abstractions/IVesselIdentity.cs b/Solutions/Ais.Net.Models/Ais/Net/Models/Abstractions/IVesselIdentity.cs index b2bed8d..baa02c2 100644 --- a/Solutions/Ais.Net.Models/Ais/Net/Models/Abstractions/IVesselIdentity.cs +++ b/Solutions/Ais.Net.Models/Ais/Net/Models/Abstractions/IVesselIdentity.cs @@ -4,7 +4,13 @@ namespace Ais.Net.Models.Abstractions; +/// +/// Exposes the vessel's identity. +/// public interface IVesselIdentity { + /// + /// Gets the vessel's Maritime Mobile Service Identity (MMSI). + /// uint Mmsi { get; } } \ No newline at end of file diff --git a/Solutions/Ais.Net.Models/Ais/Net/Models/Abstractions/IVesselName.cs b/Solutions/Ais.Net.Models/Ais/Net/Models/Abstractions/IVesselName.cs index 3aa233f..a66bfa7 100644 --- a/Solutions/Ais.Net.Models/Ais/Net/Models/Abstractions/IVesselName.cs +++ b/Solutions/Ais.Net.Models/Ais/Net/Models/Abstractions/IVesselName.cs @@ -4,7 +4,13 @@ namespace Ais.Net.Models.Abstractions; +/// +/// Exposes the vessel's name. +/// public interface IVesselName { + /// + /// Gets the vessel's name. + /// string VesselName { get; } } \ No newline at end of file diff --git a/Solutions/Ais.Net.Models/Ais/Net/Models/Abstractions/IVesselNavigation.cs b/Solutions/Ais.Net.Models/Ais/Net/Models/Abstractions/IVesselNavigation.cs index 4acc947..f186ee4 100644 --- a/Solutions/Ais.Net.Models/Ais/Net/Models/Abstractions/IVesselNavigation.cs +++ b/Solutions/Ais.Net.Models/Ais/Net/Models/Abstractions/IVesselNavigation.cs @@ -4,11 +4,23 @@ namespace Ais.Net.Models.Abstractions; +/// +/// Exposes the vessel's navigation data, including position, course, speed and heading. +/// public interface IVesselNavigation: IVesselPositionAccuracy, IVesselSpeedOverGround, IVesselCourseOverGround { + /// + /// Gets the vessel's position, or null when not available. + /// Position? Position { get; } + /// + /// Gets the UTC second at which the position was reported. + /// uint TimeStampSecond { get; } + /// + /// Gets the vessel's true heading in degrees. + /// uint TrueHeadingDegrees { get; } } \ No newline at end of file diff --git a/Solutions/Ais.Net.Models/Ais/Net/Models/Abstractions/IVesselNavigationStatus.cs b/Solutions/Ais.Net.Models/Ais/Net/Models/Abstractions/IVesselNavigationStatus.cs index 58558e7..7fb86a6 100644 --- a/Solutions/Ais.Net.Models/Ais/Net/Models/Abstractions/IVesselNavigationStatus.cs +++ b/Solutions/Ais.Net.Models/Ais/Net/Models/Abstractions/IVesselNavigationStatus.cs @@ -1,10 +1,16 @@ -// -// Copyright (c) Endjin Limited. All rights reserved. -// - -namespace Ais.Net.Models.Abstractions; - -public interface IVesselNavigationStatus -{ - NavigationStatus NavigationStatus { get; } +// +// Copyright (c) Endjin Limited. All rights reserved. +// + +namespace Ais.Net.Models.Abstractions; + +/// +/// Exposes the vessel's navigation status. +/// +public interface IVesselNavigationStatus +{ + /// + /// Gets the vessel's navigation status. + /// + NavigationStatus NavigationStatus { get; } } \ No newline at end of file diff --git a/Solutions/Ais.Net.Models/Ais/Net/Models/Abstractions/IVesselPositionAccuracy.cs b/Solutions/Ais.Net.Models/Ais/Net/Models/Abstractions/IVesselPositionAccuracy.cs index a47eb94..8b83811 100644 --- a/Solutions/Ais.Net.Models/Ais/Net/Models/Abstractions/IVesselPositionAccuracy.cs +++ b/Solutions/Ais.Net.Models/Ais/Net/Models/Abstractions/IVesselPositionAccuracy.cs @@ -1,10 +1,16 @@ -// -// Copyright (c) Endjin Limited. All rights reserved. -// - -namespace Ais.Net.Models.Abstractions; - -public interface IVesselPositionAccuracy -{ - bool PositionAccuracy { get; } +// +// Copyright (c) Endjin Limited. All rights reserved. +// + +namespace Ais.Net.Models.Abstractions; + +/// +/// Exposes whether a high-accuracy (DGPS) position fix is indicated. +/// +public interface IVesselPositionAccuracy +{ + /// + /// Gets a value indicating whether a high-accuracy (DGPS) position fix is indicated. + /// + bool PositionAccuracy { get; } } \ No newline at end of file diff --git a/Solutions/Ais.Net.Models/Ais/Net/Models/Abstractions/IVesselSpeedOverGround.cs b/Solutions/Ais.Net.Models/Ais/Net/Models/Abstractions/IVesselSpeedOverGround.cs index 66a4243..ded9689 100644 --- a/Solutions/Ais.Net.Models/Ais/Net/Models/Abstractions/IVesselSpeedOverGround.cs +++ b/Solutions/Ais.Net.Models/Ais/Net/Models/Abstractions/IVesselSpeedOverGround.cs @@ -1,10 +1,16 @@ -// -// Copyright (c) Endjin Limited. All rights reserved. -// - -namespace Ais.Net.Models.Abstractions; - -public interface IVesselSpeedOverGround -{ - float? SpeedOverGround { get; } +// +// Copyright (c) Endjin Limited. All rights reserved. +// + +namespace Ais.Net.Models.Abstractions; + +/// +/// Exposes the vessel's speed over ground. +/// +public interface IVesselSpeedOverGround +{ + /// + /// Gets the speed over ground in knots, or null when not available. + /// + float? SpeedOverGround { get; } } \ No newline at end of file diff --git a/Solutions/Ais.Net.Models/Ais/Net/Models/Abstractions/Position.cs b/Solutions/Ais.Net.Models/Ais/Net/Models/Abstractions/Position.cs index d692a41..edb6cf6 100644 --- a/Solutions/Ais.Net.Models/Ais/Net/Models/Abstractions/Position.cs +++ b/Solutions/Ais.Net.Models/Ais/Net/Models/Abstractions/Position.cs @@ -1,14 +1,33 @@ -// -// Copyright (c) Endjin Limited. All rights reserved. -// - -namespace Ais.Net.Models.Abstractions; - -public record Position(double Latitude, double Longitude) -{ - public static Position From10000thMins(int latitude, int longitude) => - new (latitude.From10000thMinsToDegrees(), longitude.From10000thMinsToDegrees()); - - public static Position From10thMins(int latitude, int longitude) => - new(latitude.From10thMinsToDegrees(), longitude.From10thMinsToDegrees()); -} \ No newline at end of file +// +// Copyright (c) Endjin Limited. All rights reserved. +// + +namespace Ais.Net.Models.Abstractions; + +/// +/// A geographic position expressed as latitude and longitude in degrees. +/// +/// The latitude in degrees (positive north of the equator). +/// The longitude in degrees (positive east of the prime meridian). +public record Position(double Latitude, double Longitude) +{ + /// + /// Creates a from coordinates expressed in ten-thousandths of a minute + /// (the encoding used by most AIS position reports). + /// + /// The latitude in units of 1/10000 minute. + /// The longitude in units of 1/10000 minute. + /// The position in degrees. + public static Position From10000thMins(int latitude, int longitude) => + new (latitude.From10000thMinsToDegrees(), longitude.From10000thMinsToDegrees()); + + /// + /// Creates a from coordinates expressed in tenths of a minute (the + /// lower-precision encoding used by the long-range message type 27). + /// + /// The latitude in units of 1/10 minute. + /// The longitude in units of 1/10 minute. + /// The position in degrees. + public static Position From10thMins(int latitude, int longitude) => + new(latitude.From10thMinsToDegrees(), longitude.From10thMinsToDegrees()); +} diff --git a/Solutions/Ais.Net.Models/Ais/Net/Models/Abstractions/ShipTypeCategory.cs b/Solutions/Ais.Net.Models/Ais/Net/Models/Abstractions/ShipTypeCategory.cs index 97545ac..9cc3e81 100644 --- a/Solutions/Ais.Net.Models/Ais/Net/Models/Abstractions/ShipTypeCategory.cs +++ b/Solutions/Ais.Net.Models/Ais/Net/Models/Abstractions/ShipTypeCategory.cs @@ -4,16 +4,58 @@ namespace Ais.Net.Models.Abstractions; +/// +/// Coarse classification of a vessel derived from the AIS ship type code. +/// public enum ShipTypeCategory { + /// + /// No ship type category is available. + /// NotAvailable = 0, + + /// + /// Reserved for future use. + /// Reserved = 1, + + /// + /// Wing in ground (WIG) craft. + /// WingInGround = 2, + + /// + /// Special category reserved for the ship type code range beginning with 3. + /// SpecialCategory3 = 3, + + /// + /// High speed craft (HSC). + /// HighSpeedCraft = 4, + + /// + /// Special category reserved for the ship type code range beginning with 5. + /// SpecialCategory5 = 5, + + /// + /// Passenger vessel. + /// Passenger = 6, + + /// + /// Cargo vessel. + /// Cargo = 7, + + /// + /// Tanker. + /// Tanker = 8, + + /// + /// Other type of vessel. + /// Other = 9 } \ No newline at end of file diff --git a/Solutions/Ais.Net.Models/Ais/Net/Models/Abstractions/ShipTypeGroup.cs b/Solutions/Ais.Net.Models/Ais/Net/Models/Abstractions/ShipTypeGroup.cs index 1062b7b..1968510 100644 --- a/Solutions/Ais.Net.Models/Ais/Net/Models/Abstractions/ShipTypeGroup.cs +++ b/Solutions/Ais.Net.Models/Ais/Net/Models/Abstractions/ShipTypeGroup.cs @@ -4,55 +4,218 @@ namespace Ais.Net.Models.Abstractions; +/// +/// Fine-grained grouping of a vessel derived from the AIS ship type code. +/// public enum ShipTypeGroup { + /// + /// No ship type group is available. + /// NotAvailable, + + /// + /// Reserved for future use. + /// Reserved, + + /// + /// Wing in ground craft, all ships of this type. + /// WingInGroundAll, + + /// + /// Wing in ground craft carrying hazardous or polluting cargo. + /// WingInGroundHazardous, + + /// + /// Wing in ground craft, reserved for future use. + /// WingInGroundReserved, + /// + /// Fishing vessel. + /// Fishing, + + /// + /// Vessel engaged in towing. + /// Towing, + + /// + /// Vessel engaged in towing where the tow exceeds 200 metres in length or 25 metres in breadth. + /// TowingLengthOver200MOrBreadthOver25M, + + /// + /// Vessel engaged in dredging or underwater operations. + /// DredgingOrUnderwaterOps, + + /// + /// Vessel engaged in diving operations. + /// DivingOps, + + /// + /// Vessel engaged in military operations. + /// MilitaryOps, + + /// + /// Sailing vessel. + /// Sailing, + + /// + /// Pleasure craft. + /// PleasureCraft, + /// + /// High speed craft, all ships of this type. + /// HighSpeedCraftAll, + + /// + /// High speed craft carrying hazardous or polluting cargo. + /// HighSpeedCraftHazardous, + + /// + /// High speed craft, reserved for future use. + /// HighSpeedCraftReserved, + + /// + /// High speed craft with no additional information. + /// HighSpeedCraftNoAdditionalInformation, + /// + /// Pilot vessel. + /// PilotVessel, + + /// + /// Search and rescue vessel. + /// SearchAndRescueVessel, + + /// + /// Tug. + /// Tug, + + /// + /// Port tender. + /// PortTender, + + /// + /// Vessel carrying anti-pollution equipment. + /// AntiPollutionEquipment, + + /// + /// Law enforcement vessel. + /// LawEnforcement, + + /// + /// Spare, reserved for local or regional vessel assignments. + /// SpareLocalVessel, + + /// + /// Medical transport. + /// MedicalTransport, + + /// + /// Ship or aircraft not party to an armed conflict (noncombatant). + /// NoncombatantShip, + /// + /// Passenger vessel, all ships of this type. + /// PassengerAll, + + /// + /// Passenger vessel carrying hazardous or polluting cargo. + /// PassengerHazardous, + + /// + /// Passenger vessel, reserved for future use. + /// PassengerReserved, + + /// + /// Passenger vessel with no additional information. + /// PassengerNoAdditionalInformation, + /// + /// Cargo vessel, all ships of this type. + /// CargoAll, + + /// + /// Cargo vessel carrying hazardous or polluting cargo. + /// CargoHazardous, + + /// + /// Cargo vessel, reserved for future use. + /// CargoReserved, + + /// + /// Cargo vessel with no additional information. + /// CargoNoAdditionalInformation, + /// + /// Tanker, all ships of this type. + /// TankerAll, + + /// + /// Tanker carrying hazardous or polluting cargo. + /// TankerHazardous, + + /// + /// Tanker, reserved for future use. + /// TankerReserved, + + /// + /// Tanker with no additional information. + /// TankerNoAdditionalInformation, + /// + /// Other type of vessel, all ships of this type. + /// OtherAll, + + /// + /// Other type of vessel carrying hazardous or polluting cargo. + /// OtherHazardous, + + /// + /// Other type of vessel, reserved for future use. + /// OtherReserved, + + /// + /// Other type of vessel with no additional information. + /// OtherNoAdditionalInformation, } \ No newline at end of file diff --git a/Solutions/Ais.Net.Models/Ais/Net/Models/AisMessageBase.cs b/Solutions/Ais.Net.Models/Ais/Net/Models/AisMessageBase.cs index 900152c..836e0c7 100644 --- a/Solutions/Ais.Net.Models/Ais/Net/Models/AisMessageBase.cs +++ b/Solutions/Ais.Net.Models/Ais/Net/Models/AisMessageBase.cs @@ -6,4 +6,9 @@ namespace Ais.Net.Models; +/// +/// The abstract base for all AIS messages, carrying the fields common to every message type. +/// +/// The AIS message type identifier. +/// The Maritime Mobile Service Identity of the transmitting station. public record AisMessageBase(int MessageType, uint Mmsi) : IAisMessage; \ No newline at end of file diff --git a/Solutions/Ais.Net.Models/Ais/Net/Models/AisMessageExtensions.cs b/Solutions/Ais.Net.Models/Ais/Net/Models/AisMessageExtensions.cs index 212173f..0d1f5a8 100644 --- a/Solutions/Ais.Net.Models/Ais/Net/Models/AisMessageExtensions.cs +++ b/Solutions/Ais.Net.Models/Ais/Net/Models/AisMessageExtensions.cs @@ -1,4 +1,4 @@ -// +// // Copyright (c) Endjin Limited. All rights reserved. // @@ -8,51 +8,138 @@ namespace Ais.Net.Models; +/// +/// Helper methods for converting raw AIS field values into the units and types exposed by the +/// model records, and for classifying ship-type codes. +/// public static class AisMessageExtensions { + /// + /// Converts a coordinate expressed in ten-thousandths of a minute (the encoding used by most + /// AIS position reports) to degrees. + /// + /// The coordinate in units of 1/10000 minute. + /// The coordinate in degrees. public static double From10000thMinsToDegrees(this int value) { return value / 600000.0; } + /// + /// Converts a coordinate expressed in tenths of a minute (the lower-precision encoding used by + /// the long-range message type 27) to degrees. + /// + /// The coordinate in units of 1/10 minute. + /// The coordinate in degrees. public static double From10thMinsToDegrees(this int value) { return value / 600.0; } + /// + /// Converts a value expressed in tenths (for example speed over ground in 1/10 knot) to its + /// real value, treating the sentinel 1023 as "not available". + /// + /// The raw value in tenths. + /// The scaled value, or when the value is the 1023 "not available" sentinel. public static float? FromTenths(this uint value) { return value == 1023 ? null : (value / 10.0f); } + /// + /// Converts a value expressed in tenths of a degree (for example course over ground) to + /// degrees, treating the sentinel 3600 as "not available". + /// + /// The raw value in tenths of a degree. + /// The value in degrees, or when the value is the 3600 "not available" sentinel. public static float? FromTenthsToDegrees(this uint value) { return value == 3600 ? null : (value / 10.0f); } + /// + /// Removes the '@' and space padding that AIS applies to text name fields and collapses any + /// run of interior spaces to a single space, preserving significant single spaces (such as in + /// "QUEEN MARY 2"). + /// + /// The raw, padded vessel name. + /// The cleaned vessel name, or an empty string when the input is entirely padding. public static string CleanVesselName(this string value) { - return value.Trim('@').Trim().Replace(" ", " "); + // AIS pads names with '@' (6-bit value 0) and/or spaces. Strip that padding from both + // ends (order-independently) and collapse any run of interior spaces to a single space + // (a single interior space is significant, e.g. "QUEEN MARY 2"). The result is built + // through a stack buffer so that only the returned string is allocated. + ReadOnlySpan trimmed = value.AsSpan().Trim("@ "); + if (trimmed.IsEmpty) + { + return string.Empty; + } + + // AIS text fields are short (<= 20 chars); the heap fallback guards against a pathological + // caller passing an over-long string to what is otherwise a stackalloc. + Span buffer = trimmed.Length <= 256 ? stackalloc char[256] : new char[trimmed.Length]; + int length = 0; + bool previousWasSpace = false; + foreach (char c in trimmed) + { + bool isSpace = c == ' '; + if (isSpace && previousWasSpace) + { + continue; + } + + buffer[length++] = c; + previousWasSpace = isSpace; + } + + return new string(buffer[..length]); } - public static string GetString(this Span value) + /// + /// Decodes a span of ASCII bytes to a string. + /// + /// The ASCII bytes to decode. + /// The decoded string. + public static string GetString(this ReadOnlySpan value) { return Encoding.ASCII.GetString(value); } + /// + /// Decodes an AIS 6-bit ASCII text field to a string. + /// + /// The parser positioned over the text field. + /// The decoded text. public static string TextFieldToString(this NmeaAisTextFieldParser field) { - Span ascii = stackalloc byte[(int)field.CharacterCount]; - field.WriteAsAscii(ascii); + int characterCount = (int)field.CharacterCount; - return Encoding.ASCII.GetString(ascii); + // AIS text fields are short; the heap fallback guards against a field that reports a + // pathologically large character count from overflowing the stack. + Span ascii = characterCount <= 256 ? stackalloc byte[256] : new byte[characterCount]; + Span field6BitAscii = ascii[..characterCount]; + field.WriteAsAscii(field6BitAscii); + + return Encoding.ASCII.GetString(field6BitAscii); } + /// + /// Maps a ship-and-cargo type to its coarse . + /// + /// The ship type. + /// The category the ship type belongs to. public static ShipTypeCategory ToShipTypeCategory(this ShipType shipType) { return shipType.ToShipTypeGroup().ToShipTypeCategory(); } + /// + /// Maps a to its coarse . + /// + /// The ship-type group. + /// The category the group belongs to. public static ShipTypeCategory ToShipTypeCategory(this ShipTypeGroup shipTypeGroup) { return shipTypeGroup switch @@ -103,148 +190,133 @@ public static ShipTypeCategory ToShipTypeCategory(this ShipTypeGroup shipTypeGro }; } + /// + /// Returns the human-readable description for a raw ship-and-cargo type code. + /// + /// The raw ship-type code (0-99). + /// The description, or "Not available" when the code is out of range. public static string ToShipTypeDescription(this int shipType) { - switch (shipType) - { - case 0: goto default; - case >= 1 and <= 19: return "Reserved for future use"; - case 20: return "Wing in ground(WIG), all ships of this type"; - case 21: return "Wing in ground(WIG), Hazardous category A"; - case 22: return "Wing in ground(WIG), Hazardous category B"; - case 23: return "Wing in ground(WIG), Hazardous category C"; - case 24: return "Wing in ground(WIG), Hazardous category D"; - case 25: return "Wing in ground(WIG), Reserved for future use"; - case 26: return "Wing in ground(WIG), Reserved for future use"; - case 27: return "Wing in ground(WIG), Reserved for future use"; - case 28: return "Wing in ground(WIG), Reserved for future use"; - case 29: return "Wing in ground(WIG), Reserved for future use"; - case 30: return "Fishing"; - case 31: return "Towing"; - case 32: return "Towing: length exceeds 200m or breadth exceeds 25m"; - case 33: return "Dredging or underwater ops"; - case 34: return "Diving ops"; - case 35: return "Military ops"; - case 36: return "Sailing"; - case 37: return "Pleasure Craft"; - case 38: return "Reserved"; - case 39: return "Reserved"; - case 40: return "High speed craft(HSC), all ships of this type"; - case 41: return "High speed craft(HSC), Hazardous category A"; - case 42: return "High speed craft(HSC), Hazardous category B"; - case 43: return "High speed craft(HSC), Hazardous category C"; - case 44: return "High speed craft(HSC), Hazardous category D"; - case 45: return "High speed craft(HSC), Reserved for future use"; - case 46: return "High speed craft(HSC), Reserved for future use"; - case 47: return "High speed craft(HSC), Reserved for future use"; - case 48: return "High speed craft(HSC), Reserved for future use"; - case 49: return "High speed craft(HSC), No additional information"; - case 50: return "Pilot Vessel"; - case 51: return "Search and Rescue vessel"; - case 52: return "Tug"; - case 53: return "Port Tender"; - case 54: return "Anti - pollution equipment"; - case 55: return "Law Enforcement"; - case 56: return "Spare - Local Vessel"; - case 57: return "Spare - Local Vessel"; - case 58: return "Medical Transport"; - case 59: return "Noncombatant ship according to RR Resolution No. 18"; - case 60: return "Passenger, all ships of this type"; - case 61: return "Passenger, Hazardous category A"; - case 62: return "Passenger, Hazardous category B"; - case 63: return "Passenger, Hazardous category C"; - case 64: return "Passenger, Hazardous category D"; - case 65: return "Passenger, Reserved for future use"; - case 66: return "Passenger, Reserved for future use"; - case 67: return "Passenger, Reserved for future use"; - case 68: return "Passenger, Reserved for future use"; - case 69: return "Passenger, No additional information"; - case 70: return "Cargo, all ships of this type"; - case 71: return "Cargo, Hazardous category A"; - case 72: return "Cargo, Hazardous category B"; - case 73: return "Cargo, Hazardous category C"; - case 74: return "Cargo, Hazardous category D"; - case 75: return "Cargo, Reserved for future use"; - case 76: return "Cargo, Reserved for future use"; - case 77: return "Cargo, Reserved for future use"; - case 78: return "Cargo, Reserved for future use"; - case 79: return "Cargo, No additional information"; - case 80: return "Tanker, all ships of this type"; - case 81: return "Tanker, Hazardous category A"; - case 82: return "Tanker, Hazardous category B"; - case 83: return "Tanker, Hazardous category C"; - case 84: return "Tanker, Hazardous category D"; - case 85: return "Tanker, Reserved for future use"; - case 86: return "Tanker, Reserved for future use"; - case 87: return "Tanker, Reserved for future use"; - case 88: return "Tanker, Reserved for future use"; - case 89: return "Tanker, No additional information"; - case 90: return "Other Type, all ships of this type"; - case 91: return "Other Type, Hazardous category A"; - case 92: return "Other Type, Hazardous category B"; - case 93: return "Other Type, Hazardous category C"; - case 94: return "Other Type, Hazardous category D"; - case 95: return "Other Type, Reserved for future use"; - case 96: return "Other Type, Reserved for future use"; - case 97: return "Other Type, Reserved for future use"; - case 98: return "Other Type, Reserved for future use"; - case 99: return "Other Type, no additional information"; - default: return "Not available"; - } + return (uint)shipType < (uint)ShipTypeTable.Length + ? ShipTypeTable[shipType].Description + : NotAvailableDescription; } + /// + /// Maps a ship-and-cargo type to its . + /// + /// The ship type. + /// The group the ship type belongs to. public static ShipTypeGroup ToShipTypeGroup(this ShipType shipType) { return ToShipTypeGroup((int)shipType); } + /// + /// Maps a raw ship-and-cargo type code to its . + /// + /// The raw ship-type code (0-99). + /// The group the code belongs to, or when the code is out of range. public static ShipTypeGroup ToShipTypeGroup(this int shipType) { - switch (shipType) + return (uint)shipType < (uint)ShipTypeTable.Length + ? ShipTypeTable[shipType].Group + : ShipTypeGroup.NotAvailable; + } + + private const string NotAvailableDescription = "Not available"; + + // Single source of truth for ship-type classification: each raw code (0-99) is mapped to its + // group AND its human-readable description in exactly one place, so they can never disagree. + // (A previous bug had code 49 classified as "reserved" by the group switch while the separate + // description switch said "no additional information".) Category is derived from the group, so + // all three facets flow from this one table. + private static readonly ShipTypeInfo[] ShipTypeTable = BuildShipTypeTable(); + + private static ShipTypeInfo[] BuildShipTypeTable() + { + ShipTypeInfo[] table = new ShipTypeInfo[100]; + for (int i = 0; i < table.Length; i++) + { + table[i] = new ShipTypeInfo(ShipTypeGroup.NotAvailable, NotAvailableDescription); + } + + SetRange(table, 1, 19, ShipTypeGroup.Reserved, "Reserved for future use"); + + // Wing in ground (20-29): note this band has no "no additional information" code. + table[20] = new ShipTypeInfo(ShipTypeGroup.WingInGroundAll, "Wing in ground(WIG), all ships of this type"); + SetHazardousCategories(table, 21, ShipTypeGroup.WingInGroundHazardous, "Wing in ground(WIG)"); + SetRange(table, 25, 29, ShipTypeGroup.WingInGroundReserved, "Wing in ground(WIG), Reserved for future use"); + + table[30] = new ShipTypeInfo(ShipTypeGroup.Fishing, "Fishing"); + table[31] = new ShipTypeInfo(ShipTypeGroup.Towing, "Towing"); + table[32] = new ShipTypeInfo(ShipTypeGroup.TowingLengthOver200MOrBreadthOver25M, "Towing: length exceeds 200m or breadth exceeds 25m"); + table[33] = new ShipTypeInfo(ShipTypeGroup.DredgingOrUnderwaterOps, "Dredging or underwater ops"); + table[34] = new ShipTypeInfo(ShipTypeGroup.DivingOps, "Diving ops"); + table[35] = new ShipTypeInfo(ShipTypeGroup.MilitaryOps, "Military ops"); + table[36] = new ShipTypeInfo(ShipTypeGroup.Sailing, "Sailing"); + table[37] = new ShipTypeInfo(ShipTypeGroup.PleasureCraft, "Pleasure Craft"); + SetRange(table, 38, 39, ShipTypeGroup.Reserved, "Reserved"); + + // High speed craft (40-49): code 49 is "no additional information", not reserved. + table[40] = new ShipTypeInfo(ShipTypeGroup.HighSpeedCraftAll, "High speed craft(HSC), all ships of this type"); + SetHazardousCategories(table, 41, ShipTypeGroup.HighSpeedCraftHazardous, "High speed craft(HSC)"); + SetRange(table, 45, 48, ShipTypeGroup.HighSpeedCraftReserved, "High speed craft(HSC), Reserved for future use"); + table[49] = new ShipTypeInfo(ShipTypeGroup.HighSpeedCraftNoAdditionalInformation, "High speed craft(HSC), No additional information"); + + table[50] = new ShipTypeInfo(ShipTypeGroup.PilotVessel, "Pilot Vessel"); + table[51] = new ShipTypeInfo(ShipTypeGroup.SearchAndRescueVessel, "Search and Rescue vessel"); + table[52] = new ShipTypeInfo(ShipTypeGroup.Tug, "Tug"); + table[53] = new ShipTypeInfo(ShipTypeGroup.PortTender, "Port Tender"); + table[54] = new ShipTypeInfo(ShipTypeGroup.AntiPollutionEquipment, "Anti - pollution equipment"); + table[55] = new ShipTypeInfo(ShipTypeGroup.LawEnforcement, "Law Enforcement"); + SetRange(table, 56, 57, ShipTypeGroup.SpareLocalVessel, "Spare - Local Vessel"); + table[58] = new ShipTypeInfo(ShipTypeGroup.MedicalTransport, "Medical Transport"); + table[59] = new ShipTypeInfo(ShipTypeGroup.NoncombatantShip, "Noncombatant ship according to RR Resolution No. 18"); + + // Passenger (60-69). + table[60] = new ShipTypeInfo(ShipTypeGroup.PassengerAll, "Passenger, all ships of this type"); + SetHazardousCategories(table, 61, ShipTypeGroup.PassengerHazardous, "Passenger"); + SetRange(table, 65, 68, ShipTypeGroup.PassengerReserved, "Passenger, Reserved for future use"); + table[69] = new ShipTypeInfo(ShipTypeGroup.PassengerNoAdditionalInformation, "Passenger, No additional information"); + + // Cargo (70-79). + table[70] = new ShipTypeInfo(ShipTypeGroup.CargoAll, "Cargo, all ships of this type"); + SetHazardousCategories(table, 71, ShipTypeGroup.CargoHazardous, "Cargo"); + SetRange(table, 75, 78, ShipTypeGroup.CargoReserved, "Cargo, Reserved for future use"); + table[79] = new ShipTypeInfo(ShipTypeGroup.CargoNoAdditionalInformation, "Cargo, No additional information"); + + // Tanker (80-89). + table[80] = new ShipTypeInfo(ShipTypeGroup.TankerAll, "Tanker, all ships of this type"); + SetHazardousCategories(table, 81, ShipTypeGroup.TankerHazardous, "Tanker"); + SetRange(table, 85, 88, ShipTypeGroup.TankerReserved, "Tanker, Reserved for future use"); + table[89] = new ShipTypeInfo(ShipTypeGroup.TankerNoAdditionalInformation, "Tanker, No additional information"); + + // Other (90-99): code 99 uses lowercase "no additional information" in the source data. + table[90] = new ShipTypeInfo(ShipTypeGroup.OtherAll, "Other Type, all ships of this type"); + SetHazardousCategories(table, 91, ShipTypeGroup.OtherHazardous, "Other Type"); + SetRange(table, 95, 98, ShipTypeGroup.OtherReserved, "Other Type, Reserved for future use"); + table[99] = new ShipTypeInfo(ShipTypeGroup.OtherNoAdditionalInformation, "Other Type, no additional information"); + + return table; + } + + private static void SetHazardousCategories(ShipTypeInfo[] table, int firstCode, ShipTypeGroup group, string descriptionPrefix) + { + // The four consecutive codes map to "Hazardous category A" through "Hazardous category D". + for (int i = 0; i < 4; i++) { - case 0: goto default; - case >= 1 and <= 19: return ShipTypeGroup.Reserved; - case 20: return ShipTypeGroup.WingInGroundAll; - case >= 21 and <= 24: return ShipTypeGroup.WingInGroundHazardous; - case >= 25 and <= 29: return ShipTypeGroup.WingInGroundReserved; - case 30: return ShipTypeGroup.Fishing; - case 31: return ShipTypeGroup.Towing; - case 32: return ShipTypeGroup.TowingLengthOver200MOrBreadthOver25M; - case 33: return ShipTypeGroup.DredgingOrUnderwaterOps; - case 34: return ShipTypeGroup.DivingOps; - case 35: return ShipTypeGroup.MilitaryOps; - case 36: return ShipTypeGroup.Sailing; - case 37: return ShipTypeGroup.PleasureCraft; - case >= 38 and <= 39: return ShipTypeGroup.Reserved; - case 40: return ShipTypeGroup.HighSpeedCraftAll; - case >= 41 and <= 44: return ShipTypeGroup.HighSpeedCraftHazardous; - case >= 45 and <= 49: return ShipTypeGroup.HighSpeedCraftReserved; - case 50: return ShipTypeGroup.PilotVessel; - case 51: return ShipTypeGroup.SearchAndRescueVessel; - case 52: return ShipTypeGroup.Tug; - case 53: return ShipTypeGroup.PortTender; - case 54: return ShipTypeGroup.AntiPollutionEquipment; - case 55: return ShipTypeGroup.LawEnforcement; - case >= 56 and <= 57: return ShipTypeGroup.SpareLocalVessel; - case 58: return ShipTypeGroup.MedicalTransport; - case 59: return ShipTypeGroup.NoncombatantShip; - case 60: return ShipTypeGroup.PassengerAll; - case >= 61 and <= 64: return ShipTypeGroup.PassengerHazardous; - case >= 65 and <= 68: return ShipTypeGroup.PassengerReserved; - case 69: return ShipTypeGroup.PassengerNoAdditionalInformation; - case 70: return ShipTypeGroup.CargoAll; - case >= 71 and <= 74: return ShipTypeGroup.CargoHazardous; - case >= 75 and <= 78: return ShipTypeGroup.CargoReserved; - case 79: return ShipTypeGroup.CargoNoAdditionalInformation; - case 80: return ShipTypeGroup.TankerAll; - case >= 81 and <= 84: return ShipTypeGroup.TankerHazardous; - case >= 85 and <= 88: return ShipTypeGroup.TankerReserved; - case 89: return ShipTypeGroup.TankerNoAdditionalInformation; - case 90: return ShipTypeGroup.OtherAll; - case >= 91 and <= 94: return ShipTypeGroup.OtherHazardous; - case >= 95 and <= 98: return ShipTypeGroup.OtherReserved; - case 99: return ShipTypeGroup.OtherNoAdditionalInformation; - default: return ShipTypeGroup.NotAvailable; + table[firstCode + i] = new ShipTypeInfo(group, $"{descriptionPrefix}, Hazardous category {(char)('A' + i)}"); } } -} \ No newline at end of file + + private static void SetRange(ShipTypeInfo[] table, int firstCode, int lastCode, ShipTypeGroup group, string description) + { + for (int i = firstCode; i <= lastCode; i++) + { + table[i] = new ShipTypeInfo(group, description); + } + } + + private readonly record struct ShipTypeInfo(ShipTypeGroup Group, string Description); +} diff --git a/Solutions/Ais.Net.Models/Ais/Net/Models/AisMessageType18.cs b/Solutions/Ais.Net.Models/Ais/Net/Models/AisMessageType18.cs index ccc70c7..9d32279 100644 --- a/Solutions/Ais.Net.Models/Ais/Net/Models/AisMessageType18.cs +++ b/Solutions/Ais.Net.Models/Ais/Net/Models/AisMessageType18.cs @@ -6,6 +6,27 @@ namespace Ais.Net.Models; +/// +/// A Class B "CS" (carrier-sense) position report (AIS message type 18). +/// +/// Whether the unit can accept a channel assignment via message type 22. +/// Whether the unit can switch frequency bands. +/// The course over ground in degrees, or null when not available. +/// The Class B unit type (carrier-sense or SOTDMA). +/// Whether the unit is equipped with a display. +/// Whether the station is operating in assigned mode. +/// Whether the unit is attached to a Digital Selective Calling (DSC) transceiver. +/// The Maritime Mobile Service Identity of the transmitting station. +/// The reported latitude/longitude, or null when not available. +/// Whether a high-accuracy (DGPS) fix is indicated. +/// The type of radio status information carried by the message. +/// Whether Receiver Autonomous Integrity Monitoring is in use. +/// The value of the regional reserved bits at bit offset 139. +/// The value of the regional reserved bits at bit offset 38. +/// The number of times the message has been repeated. +/// The speed over ground in knots, or null when not available. +/// The UTC second when the report was generated. +/// The true heading in degrees. public record AisMessageType18( bool CanAcceptMessage22ChannelAssignment, bool CanSwitchBands, diff --git a/Solutions/Ais.Net.Models/Ais/Net/Models/AisMessageType19.cs b/Solutions/Ais.Net.Models/Ais/Net/Models/AisMessageType19.cs index 12884b7..587acf4 100644 --- a/Solutions/Ais.Net.Models/Ais/Net/Models/AisMessageType19.cs +++ b/Solutions/Ais.Net.Models/Ais/Net/Models/AisMessageType19.cs @@ -6,6 +6,30 @@ namespace Ais.Net.Models; +/// +/// A Class B "SO" extended position report (AIS message type 19). +/// +/// The course over ground in degrees, or null when not available. +/// The distance in metres from the position reference point to the bow. +/// The distance in metres from the position reference point to the port side. +/// The distance in metres from the position reference point to the starboard side. +/// The distance in metres from the position reference point to the stern. +/// Whether the station is operating in assigned mode. +/// Whether the Data Terminal Equipment is not ready. +/// The Maritime Mobile Service Identity of the transmitting station. +/// The reported latitude/longitude, or null when not available. +/// Whether a high-accuracy (DGPS) fix is indicated. +/// The electronic position fixing device type. +/// Whether Receiver Autonomous Integrity Monitoring is in use. +/// The value of the regional reserved bits at bit offset 139. +/// The value of the regional reserved bits at bit offset 38. +/// The number of times the message has been repeated. +/// The name of the vessel. +/// The ship-and-cargo type code. +/// The value of the spare bits at bit offset 308. +/// The speed over ground in knots, or null when not available. +/// The UTC second when the report was generated. +/// The true heading in degrees. public record AisMessageType19( float? CourseOverGround, uint DimensionToBow, diff --git a/Solutions/Ais.Net.Models/Ais/Net/Models/AisMessageType1Through3.cs b/Solutions/Ais.Net.Models/Ais/Net/Models/AisMessageType1Through3.cs index c945de3..f7bcb82 100644 --- a/Solutions/Ais.Net.Models/Ais/Net/Models/AisMessageType1Through3.cs +++ b/Solutions/Ais.Net.Models/Ais/Net/Models/AisMessageType1Through3.cs @@ -6,6 +6,26 @@ namespace Ais.Net.Models; +/// +/// A Class A vessel position report (AIS message types 1, 2 and 3). +/// +/// The course over ground in degrees, or null when not available. +/// The special manoeuvre indicator for the vessel. +/// The AIS message type identifier. +/// The Maritime Mobile Service Identity of the transmitting station. +/// The reported navigation status of the vessel. +/// The reported latitude/longitude, or null when not available. +/// Whether a high-accuracy (DGPS) fix is indicated. +/// The number of remaining transmission slots before the radio slot timeout. +/// The radio sub-message value, whose meaning depends on the radio slot timeout. +/// The synchronisation state of the radio. +/// The rate of turn of the vessel. +/// Whether Receiver Autonomous Integrity Monitoring is in use. +/// The number of times the message has been repeated. +/// The value of the spare bits at bit offset 145. +/// The speed over ground in knots, or null when not available. +/// The UTC second when the report was generated. +/// The true heading in degrees. public record AisMessageType1Through3( float? CourseOverGround, ManoeuvreIndicator ManoeuvreIndicator, diff --git a/Solutions/Ais.Net.Models/Ais/Net/Models/AisMessageType24Part0.cs b/Solutions/Ais.Net.Models/Ais/Net/Models/AisMessageType24Part0.cs index 716bec1..622f239 100644 --- a/Solutions/Ais.Net.Models/Ais/Net/Models/AisMessageType24Part0.cs +++ b/Solutions/Ais.Net.Models/Ais/Net/Models/AisMessageType24Part0.cs @@ -6,6 +6,13 @@ namespace Ais.Net.Models; +/// +/// A static data report, Part A - the vessel name (AIS message type 24, part 0). +/// +/// The Maritime Mobile Service Identity of the transmitting station. +/// The part number identifying this as Part A of the static data report. +/// The number of times the message has been repeated. +/// The value of the spare bits at bit offset 160. public record AisMessageType24Part0( uint Mmsi, uint PartNumber, diff --git a/Solutions/Ais.Net.Models/Ais/Net/Models/AisMessageType24Part1.cs b/Solutions/Ais.Net.Models/Ais/Net/Models/AisMessageType24Part1.cs index aa445b9..ce2d5ac 100644 --- a/Solutions/Ais.Net.Models/Ais/Net/Models/AisMessageType24Part1.cs +++ b/Solutions/Ais.Net.Models/Ais/Net/Models/AisMessageType24Part1.cs @@ -6,6 +6,24 @@ namespace Ais.Net.Models; +/// +/// A static data report, Part B - type, dimensions and identifiers (AIS message type 24, part 1). +/// +/// The call sign of the vessel. +/// The distance in metres from the position reference point to the bow. +/// The distance in metres from the position reference point to the port side. +/// The distance in metres from the position reference point to the starboard side. +/// The distance in metres from the position reference point to the stern. +/// The Maritime Mobile Service Identity of the transmitting station. +/// The MMSI of the mothership for an auxiliary craft. +/// The part number identifying this as Part B of the static data report. +/// The number of times the message has been repeated. +/// The equipment serial number of the Class B unit. +/// The value of the spare bits at bit offset 162. +/// The ship-and-cargo type code. +/// The unit model code of the Class B equipment. +/// The vendor identifier as encoded under revision 3 of the specification. +/// The vendor identifier as encoded under revision 4 of the specification. public record AisMessageType24Part1( string CallSign, uint DimensionToBow, diff --git a/Solutions/Ais.Net.Models/Ais/Net/Models/AisMessageType27.cs b/Solutions/Ais.Net.Models/Ais/Net/Models/AisMessageType27.cs index 15076ff..0cfe8e0 100644 --- a/Solutions/Ais.Net.Models/Ais/Net/Models/AisMessageType27.cs +++ b/Solutions/Ais.Net.Models/Ais/Net/Models/AisMessageType27.cs @@ -1,25 +1,37 @@ -// -// Copyright (c) Endjin Limited. All rights reserved. -// - -using Ais.Net.Models.Abstractions; - -namespace Ais.Net.Models; - -public record AisMessageType27( - float? CourseOverGround, - bool GnssPositionStatus, - uint Mmsi, - NavigationStatus NavigationStatus, - Position? Position, - bool PositionAccuracy, - bool RaimFlag, - uint RepeatIndicator, - float? SpeedOverGround) : - AisMessageBase(MessageType: 27, Mmsi), - IAisMessageType27, - IRaimFlag, - IRepeatIndicator, - IVesselCourseOverGround, - IVesselNavigationStatus, +// +// Copyright (c) Endjin Limited. All rights reserved. +// + +using Ais.Net.Models.Abstractions; + +namespace Ais.Net.Models; + +/// +/// A long-range AIS broadcast position message (AIS message type 27). +/// +/// The course over ground in degrees, or null when not available. +/// Whether the position was obtained from the on-board GNSS receiver. +/// The Maritime Mobile Service Identity of the transmitting station. +/// The reported navigation status of the vessel. +/// The reported latitude/longitude, or null when not available. +/// Whether a high-accuracy (DGPS) fix is indicated. +/// Whether Receiver Autonomous Integrity Monitoring is in use. +/// The number of times the message has been repeated. +/// The speed over ground in knots, or null when not available. +public record AisMessageType27( + float? CourseOverGround, + bool GnssPositionStatus, + uint Mmsi, + NavigationStatus NavigationStatus, + Position? Position, + bool PositionAccuracy, + bool RaimFlag, + uint RepeatIndicator, + float? SpeedOverGround) : + AisMessageBase(MessageType: 27, Mmsi), + IAisMessageType27, + IRaimFlag, + IRepeatIndicator, + IVesselCourseOverGround, + IVesselNavigationStatus, IVesselPositionAccuracy; \ No newline at end of file diff --git a/Solutions/Ais.Net.Models/Ais/Net/Models/AisMessageType5.cs b/Solutions/Ais.Net.Models/Ais/Net/Models/AisMessageType5.cs index a5439ca..eb53894 100644 --- a/Solutions/Ais.Net.Models/Ais/Net/Models/AisMessageType5.cs +++ b/Solutions/Ais.Net.Models/Ais/Net/Models/AisMessageType5.cs @@ -6,6 +6,29 @@ namespace Ais.Net.Models; +/// +/// Static and voyage related data (AIS message type 5). +/// +/// The AIS station compliance version. +/// The call sign of the vessel. +/// The destination of the current voyage. +/// The distance in metres from the position reference point to the bow. +/// The distance in metres from the position reference point to the port side. +/// The distance in metres from the position reference point to the starboard side. +/// The distance in metres from the position reference point to the stern. +/// The maximum present static draught in 1/10 metre. +/// The day component of the estimated time of arrival. +/// The hour component of the estimated time of arrival. +/// The minute component of the estimated time of arrival. +/// The month component of the estimated time of arrival. +/// Whether the Data Terminal Equipment is not ready. +/// The IMO ship identification number. +/// The Maritime Mobile Service Identity of the transmitting station. +/// The electronic position fixing device type. +/// The number of times the message has been repeated. +/// The ship-and-cargo type code. +/// The value of the spare bits at bit offset 423. +/// The name of the vessel. public record AisMessageType5( uint AisVersion, string CallSign, diff --git a/Solutions/Directory.Packages.props b/Solutions/Directory.Packages.props new file mode 100644 index 0000000..f446a11 --- /dev/null +++ b/Solutions/Directory.Packages.props @@ -0,0 +1,21 @@ + + + + true + + + + + + + + + + + + + + + + + diff --git a/build.ps1 b/build.ps1 index aa78da4..bb95a74 100644 --- a/build.ps1 +++ b/build.ps1 @@ -3,11 +3,11 @@ .SYNOPSIS Runs a .NET flavoured build process. .DESCRIPTION - This script was scaffolded using a template from the Endjin.RecommendedPractices.Build PowerShell module. - It uses the InvokeBuild module to orchestrate an opinonated software build process for .NET solutions. + This script was scaffolded using a template from the ZeroFailed project. + It uses the InvokeBuild module to orchestrate an opinionated software build process for .NET solutions. .EXAMPLE PS C:\> ./build.ps1 - Downloads any missing module dependencies (Endjin.RecommendedPractices.Build & InvokeBuild) and executes + Downloads any missing module dependencies (ZeroFailed & InvokeBuild) and executes the build process. .PARAMETER Tasks Optionally override the default task executed as the entry-point of the build. @@ -27,14 +27,12 @@ The logging verbosity. .PARAMETER Clean When true, the .NET solution will be cleaned and all output/intermediate folders deleted. -.PARAMETER BuildModulePath - The path to import the Endjin.RecommendedPractices.Build module from. This is useful when - testing pre-release versions of the Endjin.RecommendedPractices.Build that are not yet - available in the PowerShell Gallery. -.PARAMETER BuildModuleVersion - The version of the Endjin.RecommendedPractices.Build module to import. This is useful when - testing pre-release versions of the Endjin.RecommendedPractices.Build that are not yet - available in the PowerShell Gallery. +.PARAMETER ZfModulePath + The path to import the ZeroFailed module from. This is useful when testing pre-release + versions of ZeroFailed that are not yet available in the PowerShell Gallery. +.PARAMETER ZfModuleVersion + The version of the ZeroFailed module to import. This is useful when testing pre-release + versions of ZeroFailed that are not yet available in the PowerShell Gallery. .PARAMETER InvokeBuildModuleVersion The version of the InvokeBuild module to be used. #> @@ -69,13 +67,13 @@ param ( [switch] $Clean, [Parameter()] - [string] $BuildModulePath, + [string] $ZfModulePath, [Parameter()] - [string] $BuildModuleVersion = "1.0.0-preview0020", + [string] $ZfModuleVersion = "1.0.5", [Parameter()] - [version] $InvokeBuildModuleVersion = "5.11.3" + [version] $InvokeBuildModuleVersion = "5.12.1" ) $ErrorActionPreference = 'Stop' $here = Split-Path -Parent $PSCommandPath @@ -89,14 +87,6 @@ if ($MyInvocation.ScriptName -notlike '*Invoke-Build.ps1') { Invoke-Build $Tasks $MyInvocation.MyCommand.Path @PSBoundParameters } catch { - if ($env:GITHUB_ACTIONS) { - Write-Host ("::error file={0},line={1},col={2}::{3}" -f ` - $_.InvocationInfo.ScriptName, - $_.InvocationInfo.ScriptLineNumber, - $_.InvocationInfo.OffsetInLine, - $_.Exception.Message - ) - } Write-Host -f Yellow "`n`n***`n*** Build Failure Summary - check previous logs for more details`n***" Write-Host -f Yellow $_.Exception.Message Write-Host -f Yellow $_.ScriptStackTrace @@ -106,21 +96,26 @@ if ($MyInvocation.ScriptName -notlike '*Invoke-Build.ps1') { } #endregion -#region Import shared tasks and initialise build framework +#region Initialise build framework $splat = @{ Force = $true; Verbose = $false} -if (!($BuildModulePath)) { - Install-PSResource endjin-devops -Version $BuildModuleVersion -Scope CurrentUser -TrustRepository -Reinstall:$($BuildModuleVersion -match '-') | Out-Null - $BuildModulePath = "endjin-devops" - $splat.Add("RequiredVersion", ($BuildModuleVersion -split '-')[0]) +Import-Module Microsoft.PowerShell.PSResourceGet +if (!($ZfModulePath)) { + Install-PSResource ZeroFailed -Version $ZfModuleVersion -Scope CurrentUser -TrustRepository | Out-Null + $ZfModulePath = "ZeroFailed" + $splat.Add("RequiredVersion", ($ZfModuleVersion -split '-')[0]) } else { - Write-Host "BuildModulePath: $BuildModulePath" + Write-Host "ZfModulePath: $ZfModulePath" } -$splat.Add("Name", $BuildModulePath) +$splat.Add("Name", $ZfModulePath) +# Ensure only 1 version of the module is loaded +Get-Module ZeroFailed | Remove-Module Import-Module @splat -$ver = "{0} {1}" -f (Get-Module endjin-devops).Version, (Get-Module endjin-devops).PrivateData.PsData.PreRelease -Write-Host "Using Build module version: $ver" +$ver = "{0} {1}" -f (Get-Module ZeroFailed).Version, (Get-Module ZeroFailed).PrivateData.PsData.PreRelease +Write-Host "Using ZeroFailed module version: $ver" #endregion +$PSModuleAutoloadingPreference = 'none' + # Load the build configuration -. $here/.devops/config.ps1 +. $here/.zf/config.ps1 diff --git a/global.json b/global.json new file mode 100644 index 0000000..cbef943 --- /dev/null +++ b/global.json @@ -0,0 +1,12 @@ +{ + "sdk": { + "version": "10.0.100", + "rollForward": "latestMinor" + }, + "test": { + "runner": "Microsoft.Testing.Platform" + }, + "msbuild-sdks": { + "MSTest.Sdk": "4.3.2" + } +}