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"
+ }
+}