diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..99bf87d --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,5 @@ +# These owners will be the default owners for everything in +# the repo. Unless a later match takes precedence, +# @global-owner1 and @global-owner2 will be requested for +# review when someone opens a pull request. +* @SimonDarksideJ @metaColin \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/01_bug_report.yml b/.github/ISSUE_TEMPLATE/01_bug_report.yml new file mode 100644 index 0000000..6dbdf97 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/01_bug_report.yml @@ -0,0 +1,84 @@ +name: Bug Report +description: Create a report to help us improve +labels: [] +body: + - type: markdown + attributes: + value: | + We welcome bug reports! Please see our [contribution guidelines](https://github.com/EtharInc/Ethar.GeoPose/blob/main/CONTRIBUTING.md#writing-a-good-bug-report) for more information on writing a good bug report. This template will help us gather the information we need to start the triage process. + - type: textarea + id: background + attributes: + label: Description + description: Please share a clear and concise description of the problem. + placeholder: Description + validations: + required: true + - type: textarea + id: repro-steps + attributes: + label: Reproduction Steps + description: | + Please include minimal steps to reproduce the problem if possible. E.g.: the smallest possible code snippet; or a small project, with steps to run it. If possible include text as text rather than screenshots (so it shows up in searches). + placeholder: Minimal Reproduction + validations: + required: true + - type: textarea + id: expected-behavior + attributes: + label: Expected behavior + description: | + Provide a description of the expected behavior. + placeholder: Expected behavior + validations: + required: true + - type: textarea + id: actual-behavior + attributes: + label: Actual behavior + description: | + Provide a description of the actual behavior observed. If applicable please include any error messages, exception stacktraces or memory dumps. + placeholder: Actual behavior + validations: + required: true + - type: textarea + id: regression + attributes: + label: Regression? + description: | + Did this work in a previous build or release of Ethar GeoPose? If you can try a previous release or build to find out, that can help us narrow down the problem. If you don't know, that's OK. + placeholder: Regression? + validations: + required: false + - type: textarea + id: known-workarounds + attributes: + label: Known Workarounds + description: | + Please provide a description of any known workarounds. + placeholder: Known Workarounds + validations: + required: false + - type: textarea + id: configuration + attributes: + label: Configuration + description: | + Please provide more information on your .NET configuration: + * Which version of .NET is the code running on? + * What OS and version, and what distro if applicable? + * What is the architecture (x64, x86, ARM, ARM64)? + * Do you know whether it is specific to that configuration? + * If you're using Unity or other platform, which condition(s) do you see this issue in? + placeholder: Configuration + validations: + required: false + - type: textarea + id: other-info + attributes: + label: Other information + description: | + If you have an idea where the problem might lie, let us know that here. Please include any pointers to code, relevant changes, or related issues you know of. + placeholder: Other information + validations: + required: false \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/02_api_proposal.yml b/.github/ISSUE_TEMPLATE/02_api_proposal.yml new file mode 100644 index 0000000..f53a5b8 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/02_api_proposal.yml @@ -0,0 +1,73 @@ +name: API Suggestion +description: Propose a change to the public API surface +title: "[API Proposal]: " +labels: [api-suggestion] +body: + - type: markdown + attributes: + value: | + We welcome API proposals! We have a process to evaluate the value and shape of new API. Although recognise the majority of the library is governed by the [GeoPose Standard](https://docs.ogc.org/dis/21-056r10/21-056r10.html) and some API changes will need to be rattified by the standards body before being accepted. + - type: textarea + id: background + attributes: + label: Background and motivation + description: Please describe the purpose and value of the new API here. + placeholder: Purpose + validations: + required: true + - type: textarea + id: api-proposal + attributes: + label: API Proposal + description: | + Please provide the specific public API signature diff that you are proposing. + + placeholder: API declaration (no method bodies) + value: | + ```csharp + namespace System.Collections.Generic; + + public class MyFancyCollection : IEnumerable + { + public void Fancy(T item); + } + ``` + validations: + required: true + - type: textarea + id: api-usage + attributes: + label: API Usage + description: | + Please provide code examples that highlight how the proposed API additions are meant to be consumed. This will help suggest whether the API has the right shape to be functional, performant and usable. + placeholder: API usage + value: | + ```csharp + // Fancy the value + var c = new MyFancyCollection(); + c.Fancy(42); + + // Getting the values out + foreach (var v in c) + Console.WriteLine(v); + ``` + validations: + required: true + - type: textarea + id: alternative-designs + attributes: + label: Alternative Designs + description: | + Please provide alternative designs. This might not be APIs; for example instead of providing new APIs an option might be to change the behavior of an existing API. + placeholder: Alternative designs + validations: + required: false + - type: textarea + id: risks + attributes: + label: Risks + description: | + Please mention any risks that to your knowledge the API proposal might entail, such as breaking changes, performance regressions, etc. + placeholder: Risks + validations: + required: false \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/03_blank_issue.md b/.github/ISSUE_TEMPLATE/03_blank_issue.md new file mode 100644 index 0000000..7b9b9f9 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/03_blank_issue.md @@ -0,0 +1,8 @@ +--- +name: Blank issue +about: Something that doesn't fit the other categories +title: '' +labels: '' +assignees: '' + +--- \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/04_request_for_information.md b/.github/ISSUE_TEMPLATE/04_request_for_information.md new file mode 100644 index 0000000..3dce5e1 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/04_request_for_information.md @@ -0,0 +1,30 @@ +--- +name: Request for Information +about: Not sure how to do something, just ask. +title: '' +labels: Question +assignees: '' +--- + +## Ethar Inc. - Ethar GeoPose Request for Information + + + +## What are you trying to achieve? + + + +## What have you already tried + + + +## What material have you referenced that didn't answer your question + + \ No newline at end of file diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..8c7e4b0 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,35 @@ +# Ethar Inc. - Ethar GeoPose Pull Request + +## Overview + + + +## Changes + + + +- Fixes: + +## Breaking Changes + + + +- Breaks + +## Related Submodule Changes + + +- URL + +## Testing status + + +- No tests have been added. +- Includes unit tests. +- Includes performance tests. +- Includes integration tests. + +### Manual testing status + + + \ No newline at end of file diff --git a/.github/ReleaseNotes.md b/.github/ReleaseNotes.md new file mode 100644 index 0000000..e48d96f --- /dev/null +++ b/.github/ReleaseNotes.md @@ -0,0 +1 @@ +* \ No newline at end of file diff --git a/.github/workflows/develop-buildandtest.yml b/.github/workflows/develop-buildandtest.yml new file mode 100644 index 0000000..2504a7a --- /dev/null +++ b/.github/workflows/develop-buildandtest.yml @@ -0,0 +1,41 @@ +name: Development dotnet build and test + +on: + push: + branches-ignore: + - main + - upm + +jobs: + build: + + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Setup dotnet + uses: actions/setup-dotnet@v3 + with: + dotnet-version: | + 5.x + 6.x + 7.x + - uses: actions/cache@v3 + with: + path: ~/.nuget/packages + # Look to see if there is a cache hit for the corresponding requirements file + key: ${{ runner.os }}-nuget-${{ hashFiles('**/packages.lock.json') }} + restore-keys: | + ${{ runner.os }}-nuget + - name: Install dependencies + run: dotnet restore + - name: Build + run: dotnet build --configuration Release + - name: Test with dotnet + run: dotnet test --logger trx --results-directory "TestResults-dotnet-6" + - name: Upload dotnet test results + uses: actions/upload-artifact@v3 + with: + name: dotnet-results-dotnet-6 + path: TestResults-dotnet-6 + # Use always() to always run this step to publish test results when there are test failures + if: ${{ always() }} diff --git a/.github/workflows/main-release.yml b/.github/workflows/main-release.yml new file mode 100644 index 0000000..d9ed64d --- /dev/null +++ b/.github/workflows/main-release.yml @@ -0,0 +1,84 @@ +name: CI Release + +on: + workflow_dispatch: + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Setup dotnet + uses: actions/setup-dotnet@v3 + with: + dotnet-version: | + 5.x + 6.x + 7.x + - uses: actions/cache@v3 + with: + path: .nuget/packages + # Look to see if there is a cache hit for the corresponding requirements file + key: ${{ runner.os }}-nuget-${{ hashFiles('**/packages.lock.json') }} + restore-keys: | + ${{ runner.os }}-nuget + - name: Install dependencies + run: dotnet restore + - name: Build + run: dotnet build --configuration Release + - name: Create packages # This would actually build your project, using zip for an example artifact + run: | + zip -r Ethar.GeoPose.zip Ethar.GeoPose/bin/Release + zip -r Ethar.GeoPose.Authority.zip Ethar.GeoPose.Authority/bin/Release + - name: Tag Release + run: | + git config user.email "github-action@users.noreply.github.com" + git config user.name "GitHub Action" + git tag v1.0.0 + git add . + git push origin v1.0.0 + - name: Create Release + id: create_release + uses: marvinpinto/action-automatic-releases@latest + with: + repo_token: ${{ secrets.CI_TOKEN }} + automatic_release_tag: "latest" + prerelease: false + title: "Release ${{ github.job }}" + draft: false + files: | + ./Ethar.GeoPose.zip + ./Ethar.GeoPose.Authority.zip + Ethar.GeoPose/bin/Release/Ethar.GeoPose.1.0.0.nupkg + Ethar.GeoPose.Authority/bin/Release/Ethar.GeoPose.Authority.1.0.0.nupkg + - name: Clone UPM branch + uses: actions/checkout@v3 + with: + ref: upm + path: upm + token: ${{ secrets.CI_TOKEN }} + - name: Copy Releases to upm + run: | + Copy-Item ./Ethar.GeoPose/bin/Release/net48/Ethar.GeoPose*.* -Destination ./upm/Runtime/Plugins/Ethar.GeoPose -Recurse -Force + Copy-Item ./Ethar.GeoPose.Authority/bin/Release/net48/Ethar.GeoPose.Authority*.* -Destination ./upm/Runtime/Plugins/Ethar.GeoPose.Authority -Recurse -Force + Copy-Item ./Ethar.GeoPose.Examples/Example_BasicSerialization.cs -Destination ./upm/Samples~/BasicSerialization -Force + Copy-Item ./Ethar.GeoPose.Examples/Example_AuthorityImplementation.cs -Destination ./upm/Samples~/AuthorityImplementation -Force + Copy-Item ./Ethar.GeoPose.UnitTests/*.cs -Destination ./upm/Samples~/Tests/Editor/GeoPose -Force + Copy-Item ./Ethar.GeoPose.Authority.UnitTests/*.cs -Destination ./upm/Samples~/Tests/Editor/Authority -Force + shell: pwsh + - name: Publish upm changes + run: | + cd upm + git config user.email "github-action@users.noreply.github.com" + git config user.name "GitHub Action" + git add . + git commit -m "upm package updated to ${{ steps.create_release.outputs.automatic_releases_tag }} [skip ci]" + git push origin upm + git tag upm-v1.0.0 + git add . + git push origin upm-v1.0.0 + shell: pwsh + - name: Push GeoPose generated package to GitHub registry + run: dotnet nuget push Ethar.GeoPose/bin/Release/Ethar.GeoPose.1.0.0.nupkg --api-key ${{ secrets.NUGET_TOKEN }} --source https://api.nuget.org/v3/index.json --skip-duplicate + - name: Push Ethar Authority generated package to GitHub registry + run: dotnet nuget push Ethar.GeoPose.Authority/bin/Release/Ethar.GeoPose.Authority.1.0.0.nupkg --api-key ${{ secrets.NUGET_TOKEN }} --source https://api.nuget.org/v3/index.json --skip-duplicate diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5f9dd72 --- /dev/null +++ b/.gitignore @@ -0,0 +1,410 @@ +# globs +Makefile.in +*.userprefs +*.usertasks +config.make +config.status +aclocal.m4 +install-sh +autom4te.cache/ +*.tar.gz +tarballs/ +test-results/ + +# Mac bundle stuff +*.dmg +*.app + +# content below from: https://github.com/github/gitignore/blob/main/Global/macOS.gitignore +# General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +# content below from: https://github.com/github/gitignore/blob/main/Global/Windows.gitignore +# Windows thumbnail cache files +Thumbs.db +ehthumbs.db +ehthumbs_vista.db + +# Dump file +*.stackdump + +# Folder config file +[Dd]esktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msix +*.msm +*.msp + +# Windows shortcuts +*.lnk + +# content below from: https://github.com/github/gitignore/blob/master/VisualStudio.gitignore +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore + +# User-specific files +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ + +# Visual Studio 2015/2017 cache/options directory +.vs/ +.vscode/ + +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUNIT +*.VisualState.xml +TestResult.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_h.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*_wpftmp.csproj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding add-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# JetBrains Rider +.idea/ +*.sln.iml + +# CodeRush personal settings +.cr/personal + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ + +# Local History for Visual Studio +.localhistory/ + +# docFX generated site +_site \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..8637846 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,107 @@ +# Contribution Guidelines + +The Ethar GeoPose library is under the [Apache2.0 License](https://github.com/EtharInc/Ethar.GeoPose/blob/main/LICENSE.txt). By contributing to the Ethar GeoPose library, you assert that: + +* The contribution is your own original work. +* The contribution adheres to the [Coding Guidelines](articles/appendices/A01-CodingGuidelines.md) +* You have the right to assign the copyright for the work (it is not owned by your employer, or + you have been given copyright assignment in writing). + +## Finding Existing Issues + +Before filing a new issue, please search our [open issues](https://github.com/dotnet/runtime/issues) to check if it already exists. + +If you do find an existing issue, please include your own feedback in the discussion. Do consider upvoting (👍 reaction) the original post, as this helps us prioritize popular issues in our backlog. + +## Writing a Good Bug Report + +Good bug reports make it easier for maintainers to verify and root cause the underlying problem. The better a bug report, the faster the problem will be resolved. Ideally, a bug report should contain the following information: + +* A high-level description of the problem. +* A _minimal reproduction_, i.e. the smallest size of code/configuration required to reproduce the wrong behavior. +* A description of the _expected behavior_, contrasted with the _actual behavior_ observed. +* Information on the environment: OS/distro, CPU arch, SDK version, etc. +* Additional information, e.g. is it a regression from previous versions? are there any known workarounds? + +When ready to submit a bug report, please use the [Bug Report issue template](https://github.com/dotnet/runtime/issues/new?assignees=&labels=&template=01_bug_report.yml). + +### Why are Minimal Reproductions Important? + +A reproduction lets maintainers verify the presence of a bug, and diagnose the issue using a debugger. A _minimal_ reproduction is the smallest possible console application demonstrating that bug. Minimal reproductions are generally preferable since they: + +1. Focus debugging efforts on a simple code snippet, +2. Ensure that the problem is not caused by unrelated dependencies/configuration, +3. Avoid the need to share production codebases. + +### Are Minimal Reproductions Required? + +In certain cases, creating a minimal reproduction might not be practical (e.g. due to nondeterministic factors, external dependencies). In such cases you would be asked to provide as much information as possible, for example by sharing a memory dump of the failing application. If maintainers are unable to root cause the problem, they might still close the issue as not actionable. While not required, minimal reproductions are strongly encouraged and will significantly improve the chances of your issue being prioritized and fixed by the maintainers. + +### How to Create a Minimal Reproduction + +The best way to create a minimal reproduction is gradually removing code and dependencies from a reproducing app, until the problem no longer occurs. A good minimal reproduction: + +* Excludes all unnecessary types, methods, code blocks, source files, NuGet dependencies and project configurations. +* Contains documentation or code comments illustrating expected vs actual behavior. +* If possible, avoids performing any unneeded IO or system calls. For example, can the ASP.NET based reproduction be converted to a plain old console app? + +## DOs and DON'Ts + +Please do: + +* **DO** follow our [coding style](https://learn.microsoft.com/en-us/dotnet/csharp/fundamentals/coding-style/coding-conventions) (C# code-specific). +* **DO** give priority to the current style of the project or file you're changing even if it diverges from the general guidelines. +* **DO** include tests when adding new features. When fixing bugs, start with + adding a test that highlights how the current behavior is broken. +* **DO** keep the discussions focused. When a new or related topic comes up + it's often better to create new issue than to side track the discussion. +* **DO** clearly state on an issue that you are going to take on implementing it. +* **DO** blog and tweet (or whatever) about your contributions, frequently! + +Please do not: + +* **DON'T** make PRs for style changes. +* **DON'T** surprise us with big pull requests. Instead, file an issue and start + a discussion so we can agree on a direction before you invest a large amount + of time. +* **DON'T** commit code that you didn't write. If you find code that you think is a good fit to add to the .NET runtime, file an issue and start a discussion before proceeding. +* **DON'T** submit PRs that alter licensing related files or headers. If you believe there's a problem with them, file an issue and we'll be happy to discuss it. +* **DON'T** add API additions without filing an issue and discussing with us first. The project is governed by the [GeoPose standard](https://docs.ogc.org/dis/21-056r10/21-056r10.html) and changes need to be ratified first. + +## How to contribute + +### Prerequisites + +* A GitHub Account +* Familiarization with projects with Git Source control versioning. Atlassian has a wonderful guide for [getting started with Git](https://www.atlassian.com/git). +* Install Git on your local machine and have git assigned as an environment variable. +* Install a Git client like [Fork](https://git-fork.com/) or [GitHub for Desktop](https://desktop.github.com/) for staging and committing code to source control +* Follow any [Getting Started Guidelines](articles/00-GettingStarted.md#prerequisites) for setting up your development environment not covered here. + +### Steps + +1. Fork the repository to open a pull request for. +2. Clone or sync any changes from the source repository to your local disk. + > **Note:** When initially cloning the repository be sure to recursively check out all submodules! +3. Create a new branch based on the last source development commit. +4. Make the changes you'd like to contribute. +5. Stage and commit your changes with thoughtful messages detailing the work. +6. Push your local changes to your fork's remote server. +7. Navigate to the repository's source repository on GitHub. + > **Note:** by now a prompt to open a new pull request should be available on the repository's main landing page. +8. Open a pull request detailing the changes and fill out the Pull Request Template. +9. Typically Code Reviews are performed in around 24-48 hours. +10. Iterate on any feedback from reviews. + > **Note:** Typically you can push the changes directly to the branch you've opened the pull request for. +11. Once the pull request is accepted and the build validation passes, changes are then squashed and merged into the target branch, and the process is repeated. + +### Branching Strategies + +The main, development, and any feature branches are all protected by branch policies. Each branch must be up to date and pass test and build validation before it can be merged. + +* All merges to the feature and development branches are squashed and merged into a single commit to keep the history clean and focused on specific pull request changes. +* All merges from the main and development branches are just traditionally merged together to ensure they stay in sync and share the same histories. + +--- + +If there is anything not mentioned in this document or you simply want to know more, raise an [RFI (Request for Information) request here](https://github.com/EtharInc/Ethar.GeoPose/issues/new?assignees=&labels=question&template=04_request_for_information.md&title=). diff --git a/Ethar.GeoPose.Authority.UnitTests/Ethar.GeoPose.Authority.UnitTests.csproj b/Ethar.GeoPose.Authority.UnitTests/Ethar.GeoPose.Authority.UnitTests.csproj new file mode 100644 index 0000000..d2e0d88 --- /dev/null +++ b/Ethar.GeoPose.Authority.UnitTests/Ethar.GeoPose.Authority.UnitTests.csproj @@ -0,0 +1,23 @@ + + + + net6.0 + enable + disable + + false + + + + + + + + + + + + + + + diff --git a/Ethar.GeoPose.Authority.UnitTests/LtpEnuSpecificationTests.cs b/Ethar.GeoPose.Authority.UnitTests/LtpEnuSpecificationTests.cs new file mode 100644 index 0000000..9e1df6c --- /dev/null +++ b/Ethar.GeoPose.Authority.UnitTests/LtpEnuSpecificationTests.cs @@ -0,0 +1,57 @@ +using Ethar.GeoPose.Authority.FrameSpecifications; +using Ethar.GeoPose.DataTypes; +using Newtonsoft.Json; +using NUnit.Framework; + +namespace Ethar.GeoPose.Authority.UnitTests +{ + [TestFixture] + public class LtpEnuSpecificationTests : UnitTestBase + { + [TestCase(16534234327, "\"/Ethar.GeoPose/1.0\"", "\"LTP-ENU\"", "\"longitude=-122.0000000&latitude=48.0000000&heightInMeters=5.000\"")] + public void CanCorrectlyDeserializeLtpEnuSpecification(long validTime, string authority, string id, string parameters) + { + var json = + "{" + + $"\"authority\": {authority}," + + $"\"id\": {id}," + + $"\"parameters\": {parameters}" + + "}"; + + var ltpEnuSpec = JsonConvert.DeserializeObject(json); + + Assert.That(ltpEnuSpec.Position.Latitude, Is.EqualTo(48)); + Assert.That(ltpEnuSpec.Position.Longitude, Is.EqualTo(-122)); + Assert.That(ltpEnuSpec.Position.HeightInMeters, Is.EqualTo(5)); + } + + [TestCase(16534234327, "/Ethar.GeoPose/1.0", "LTP-ENU", "\"latitude=48&longitude=-122&heightInMeters=5\"")] + public void CanCorrectlySerializeLtpEnuSpecification(long validTime, string authority, string id, string parameters) + { + var ltpEnuSpec = new LtpEnuSpecification(new TangentPointPosition() + { Latitude = 48, Longitude = -122, HeightInMeters = 5 }); + + var expected = + "{" + + $"\"authority\":\"{authority}\"," + + $"\"id\":\"{id}\"," + + $"\"parameters\":{parameters}" + + "}"; + + var json = JsonConvert.SerializeObject(ltpEnuSpec); + + Assert.That(json, Is.EqualTo(expected)); + } + + [TestCase(16534234327, "/Ethar.GeoPose/1.0", "LTP-ENU", "\"latitude=48&longitude=-122&heightInMeters=5\"")] + public void CanCorrectlyMakeARoundTripSerialization(long validTime, string authority, string id, string parameters) + { + var ltpEnuSpec = new LtpEnuSpecification(new TangentPointPosition() + { Latitude = 48, Longitude = -122, HeightInMeters = 5 }); + + var json = JsonConvert.SerializeObject(ltpEnuSpec); + var converted = JsonConvert.DeserializeObject(json); + Assert.That(ltpEnuSpec, Is.EqualTo(converted)); + } + } +} diff --git a/Ethar.GeoPose.Authority.UnitTests/LtpNedSpecificationTests.cs b/Ethar.GeoPose.Authority.UnitTests/LtpNedSpecificationTests.cs new file mode 100644 index 0000000..de403ed --- /dev/null +++ b/Ethar.GeoPose.Authority.UnitTests/LtpNedSpecificationTests.cs @@ -0,0 +1,57 @@ +using Ethar.GeoPose.Authority.FrameSpecifications; +using Ethar.GeoPose.DataTypes; +using Newtonsoft.Json; +using NUnit.Framework; + +namespace Ethar.GeoPose.Authority.UnitTests +{ + [TestFixture] + public class LtpNedSpecificationTests : UnitTestBase + { + [TestCase(16534234327, "\"/Ethar.GeoPose/1.0\"", "\"LTP-NED\"", "\"longitude=-122.0000000&latitude=48.0000000&heightInMeters=5.000\"")] + public void CanCorrectlyDeserializeLtpNedSpecification(long validTime, string authority, string id, string parameters) + { + var json = + "{" + + $"\"authority\": {authority}," + + $"\"id\": {id}," + + $"\"parameters\": {parameters}" + + "}"; + + var ltpEnuSpec = JsonConvert.DeserializeObject(json); + + Assert.That(ltpEnuSpec.Position.Latitude, Is.EqualTo(48)); + Assert.That(ltpEnuSpec.Position.Longitude, Is.EqualTo(-122)); + Assert.That(ltpEnuSpec.Position.HeightInMeters, Is.EqualTo(5)); + } + + [TestCase(16534234327, "/Ethar.GeoPose/1.0", "LTP-NED", "\"latitude=48&longitude=-122&heightInMeters=5\"")] + public void CanCorrectlySerializeLtpNedSpecification(long validTime, string authority, string id, string parameters) + { + var ltpEnuSpec = new LtpNedSpecification(new TangentPointPosition() + { Latitude = 48, Longitude = -122, HeightInMeters = 5 }); + + var expected = + "{" + + $"\"authority\":\"{authority}\"," + + $"\"id\":\"{id}\"," + + $"\"parameters\":{parameters}" + + "}"; + + var json = JsonConvert.SerializeObject(ltpEnuSpec); + + Assert.That(json, Is.EqualTo(expected)); + } + + [TestCase(16534234327, "/Ethar.GeoPose/1.0", "LTP-NED", "\"latitude=48&longitude=-122&heightInMeters=5\"")] + public void CanCorrectlyMakeARoundTripSerialization(long validTime, string authority, string id, string parameters) + { + var ltpEnuSpec = new LtpNedSpecification(new TangentPointPosition() + { Latitude = 48, Longitude = -122, HeightInMeters = 5 }); + + var json = JsonConvert.SerializeObject(ltpEnuSpec); + var converted = JsonConvert.DeserializeObject(json); + Assert.That(ltpEnuSpec, Is.EqualTo(converted)); + } + } +} diff --git a/Ethar.GeoPose.Authority.UnitTests/QuaternionOrientedLtpEnuSpecificationTests.cs b/Ethar.GeoPose.Authority.UnitTests/QuaternionOrientedLtpEnuSpecificationTests.cs new file mode 100644 index 0000000..2d88067 --- /dev/null +++ b/Ethar.GeoPose.Authority.UnitTests/QuaternionOrientedLtpEnuSpecificationTests.cs @@ -0,0 +1,61 @@ +using Ethar.GeoPose.Authority.FrameSpecifications; +using Ethar.GeoPose.DataTypes; +using Newtonsoft.Json; +using NUnit.Framework; + +namespace Ethar.GeoPose.Authority.UnitTests +{ + [TestFixture] + internal class QuaternionOrientedLtpEnuSpecificationTests : UnitTestBase + { + [TestCase("\"/Ethar.GeoPose/1.0\"", "\"Quaternion-LTP-ENU\"", "\"longitude=-122&latitude=48&heightInMeters=5&orientation.x=0.692&orientation.y=0.691&orientation.z=0.141&orientation.w=0.14\"")] + public void CanCorrectlyDeserializeYprLtpEnuSpecification(string authority, string id, string parameters) + { + var json = + "{" + + $"\"authority\": {authority}," + + $"\"id\": {id}," + + $"\"parameters\": {parameters}" + + "}"; + + var ltpEnuSpec = JsonConvert.DeserializeObject(json); + + Assert.That(ltpEnuSpec.Position.Latitude, Is.EqualTo(48)); + Assert.That(ltpEnuSpec.Position.Longitude, Is.EqualTo(-122)); + Assert.That(ltpEnuSpec.Position.HeightInMeters, Is.EqualTo(5)); + Assert.That(ltpEnuSpec.Orientation.X, Is.EqualTo(0.692f)); + Assert.That(ltpEnuSpec.Orientation.Y, Is.EqualTo(0.691f)); + Assert.That(ltpEnuSpec.Orientation.Z, Is.EqualTo(0.141f)); + Assert.That(ltpEnuSpec.Orientation.W, Is.EqualTo(0.14f)); + } + + [TestCase("/Ethar.GeoPose/1.0", "Quaternion-LTP-ENU", "\"latitude=48&longitude=-122&heightInMeters=5&orientation.x=0.692&orientation.y=0.691&orientation.z=0.141&orientation.w=0.14\"")] + public void CanCorrectlySerializeYprLtpEnuSpecification(string authority, string id, string parameters) + { + var ltpEnuSpec = new QuaternionOrientedLtpEnuSpecification(new TangentPointPosition() + { Latitude = 48, Longitude = -122, HeightInMeters = 5 }, new UnitQuaternion() { X = 0.692f, Y = 0.691f, Z = 0.141f, W = 0.14f }); + + var expected = + "{" + + $"\"authority\":\"{authority}\"," + + $"\"id\":\"{id}\"," + + $"\"parameters\":{parameters}" + + "}"; + + var json = JsonConvert.SerializeObject(ltpEnuSpec); + + Assert.That(json, Is.EqualTo(expected)); + } + + [Test] + public void CanCorrectlyMakeARoundTripSerialization() + { + var ltpEnuSpec = new QuaternionOrientedLtpEnuSpecification(new TangentPointPosition() + { Latitude = 48, Longitude = -122, HeightInMeters = 5 }, new UnitQuaternion() { X = 0.692f, Y = 0.691f, Z = 0.141f, W = 0.14f }); + + var json = JsonConvert.SerializeObject(ltpEnuSpec); + var converted = JsonConvert.DeserializeObject(json); + Assert.That(ltpEnuSpec, Is.EqualTo(converted)); + } + } +} diff --git a/Ethar.GeoPose.Authority.UnitTests/TransitionModelJsonConverterTests.cs b/Ethar.GeoPose.Authority.UnitTests/TransitionModelJsonConverterTests.cs new file mode 100644 index 0000000..b2e855e --- /dev/null +++ b/Ethar.GeoPose.Authority.UnitTests/TransitionModelJsonConverterTests.cs @@ -0,0 +1,90 @@ +using Ethar.GeoPose.Authority.TransitionModels; +using Newtonsoft.Json; +using NUnit.Framework; + +namespace Ethar.GeoPose.Authority.UnitTests +{ + [TestFixture] + internal class TransitionModelJsonConverterTests : UnitTestBase + { + [TestCase("/Ethar.GeoPose/1.0", "none")] + public void CanCorrectlyDeserializeNoneTransitionModel(string authority, string id) + { + var json = + "{" + + $"\"authority\":\"{authority}\"," + + $"\"id\":\"{id}\"," + + $"\"parameters\":\"\"" + + "}"; + + var transitionModel = JsonConvert.DeserializeObject(json); + Assert.IsFalse(transitionModel is null); + } + + [TestCase("/Ethar.GeoPose/1.0", "none")] + public void CanCorrectlySerializeNoneTransitionModel(string authority, string id) + { + var transitionModel = new NoneTransitionModel(); + var expected = + "{" + + $"\"authority\":\"{authority}\"," + + $"\"id\":\"{id}\"," + + $"\"parameters\":\"\"" + + "}"; + + var json = JsonConvert.SerializeObject(transitionModel); + Assert.That(json, Is.EqualTo(expected)); + } + + [TestCase("/Ethar.GeoPose/1.0", "none")] + public void CanCorrectlyMakeARoundTripConversionForNoneTransitionModel(string authority, string id) + { + var transitionModel = new NoneTransitionModel(); + + var json = JsonConvert.SerializeObject(transitionModel); + var converted = JsonConvert.DeserializeObject(json); + + Assert.That(converted, Is.EqualTo(transitionModel)); + } + + [TestCase("/Ethar.GeoPose/1.0", "interpolated")] + public void CanCorrectlyDeserializeInterpolatedTransitionModel(string authority, string id) + { + var json = + "{" + + $"\"authority\":\"{authority}\"," + + $"\"id\":\"{id}\"," + + $"\"parameters\":\"\"" + + "}"; + + var transitionModel = JsonConvert.DeserializeObject(json); + Assert.That(transitionModel is null, Is.False); + } + + [TestCase("/Ethar.GeoPose/1.0", "interpolated")] + public void CanCorrectlySerializeInterpolatedTransitionModel(string authority, string id) + { + var transitionModel = new InterpolatedTransitionModel(); + var expected = + "{" + + $"\"authority\":\"{authority}\"," + + $"\"id\":\"{id}\"," + + $"\"parameters\":\"\"" + + "}"; + + var json = JsonConvert.SerializeObject(transitionModel); + Assert.That(json, Is.EqualTo(expected)); + } + + [TestCase("/Ethar.GeoPose/1.0", "interpolated")] + public void CanCorrectlyMakeARoundTripConversionForInterpolatedTransitionModel(string authority, string id) + { + var transitionModel = new InterpolatedTransitionModel(); + + var json = JsonConvert.SerializeObject(transitionModel); + var converted = JsonConvert.DeserializeObject(json); + + Assert.That(converted, Is.EqualTo(transitionModel)); + } + } +} diff --git a/Ethar.GeoPose.Authority.UnitTests/TranslateRotateSpecificationTests.cs b/Ethar.GeoPose.Authority.UnitTests/TranslateRotateSpecificationTests.cs new file mode 100644 index 0000000..d237f37 --- /dev/null +++ b/Ethar.GeoPose.Authority.UnitTests/TranslateRotateSpecificationTests.cs @@ -0,0 +1,61 @@ +using Ethar.GeoPose.Authority.FrameSpecifications; +using Newtonsoft.Json; +using NUnit.Framework; + +namespace Ethar.GeoPose.Authority.UnitTests +{ + [TestFixture] + internal class TranslateRotateSpecificationTests : UnitTestBase + { + [TestCase("\"/Ethar.GeoPose/1.0\"", "\"Translate-Rotate\"", "\"translation.x=0.1&translation.y=0.2&translation.z=0.3&rotation.x=0.692&rotation.y=0.691&rotation.z=0.141&rotation.w=0.14\"")] + public void CanCorrectlyDeserializeTranslateRotateSpecification(string authority, string id, string parameters) + { + var json = + "{" + + $"\"authority\": {authority}," + + $"\"id\": {id}," + + $"\"parameters\": {parameters}" + + "}"; + + var translateRotateSpec = JsonConvert.DeserializeObject(json); + Assert.That(translateRotateSpec.Translation.X, Is.EqualTo(0.1f)); + Assert.That(translateRotateSpec.Translation.Y, Is.EqualTo(0.2f)); + Assert.That(translateRotateSpec.Translation.Z, Is.EqualTo(0.3f)); + Assert.That(translateRotateSpec.Rotation.X, Is.EqualTo(0.692f)); + Assert.That(translateRotateSpec.Rotation.Y, Is.EqualTo(0.691f)); + Assert.That(translateRotateSpec.Rotation.Z, Is.EqualTo(0.141f)); + Assert.That(translateRotateSpec.Rotation.W, Is.EqualTo(0.14f)); + } + + [TestCase("/Ethar.GeoPose/1.0", "Translate-Rotate", "\"translation.x=0.1&translation.y=0.2&translation.z=0.3&rotation.x=0.692&rotation.y=0.691&rotation.z=0.141&rotation.w=0.14\"")] + public void CanCorrectlySerializeTranslateRotateSpecification(string authority, string id, string parameters) + { + var frameSpec = new TranslateRotateSpecification( + new DataTypes.UnitVector3(0.1f, 0.2f, 0.3f), + new DataTypes.UnitQuaternion() { X = 0.692f, Y = 0.691f, Z = 0.141f, W = 0.14f }); + + var expected = + "{" + + $"\"authority\":\"{authority}\"," + + $"\"id\":\"{id}\"," + + $"\"parameters\":{parameters}" + + "}"; + + var json = JsonConvert.SerializeObject(frameSpec); + Assert.That(json, Is.EqualTo(expected)); + } + + [TestCase("/Ethar.GeoPose/1.0", "Translate-Rotate")] + public void CanCorrectlyMakeARoundTripSerialization(string authority, string id) + { + var frameSpec = new TranslateRotateSpecification( + new DataTypes.UnitVector3(0.1f, 0.2f, 0.3f), + new DataTypes.UnitQuaternion() { X = 0.692f, Y = 0.691f, Z = 0.141f, W = 0.14f }); + + var json = JsonConvert.SerializeObject(frameSpec); + + var converted = JsonConvert.DeserializeObject(json); + Assert.That(frameSpec, Is.EqualTo(converted)); + } + } +} diff --git a/Ethar.GeoPose.Authority.UnitTests/UnitTestBase.cs b/Ethar.GeoPose.Authority.UnitTests/UnitTestBase.cs new file mode 100644 index 0000000..e41b76d --- /dev/null +++ b/Ethar.GeoPose.Authority.UnitTests/UnitTestBase.cs @@ -0,0 +1,18 @@ +using Ethar.GeoPose.Authority; +using NUnit.Framework; +using System.Net.Http; + +namespace Ethar.GeoPose.Authority.UnitTests +{ + public class UnitTestBase + { + protected HttpClient Client; + + [OneTimeSetUp] + public void Setup() + { + this.Client = new HttpClient(); + AuthorityProvider.RegisterAuthority(new EtharGeoPoseAuthority()); + } + } +} \ No newline at end of file diff --git a/Ethar.GeoPose.Authority.UnitTests/YprOrientedLtpEnuSpecificationTests.cs b/Ethar.GeoPose.Authority.UnitTests/YprOrientedLtpEnuSpecificationTests.cs new file mode 100644 index 0000000..8abafa0 --- /dev/null +++ b/Ethar.GeoPose.Authority.UnitTests/YprOrientedLtpEnuSpecificationTests.cs @@ -0,0 +1,60 @@ +using Ethar.GeoPose.Authority.FrameSpecifications; +using Ethar.GeoPose.DataTypes; +using Newtonsoft.Json; +using NUnit.Framework; + +namespace Ethar.GeoPose.Authority.UnitTests +{ + [TestFixture] + public class YprOrientedLtpEnuSpecificationTests : UnitTestBase + { + [TestCase("\"/Ethar.GeoPose/1.0\"", "\"YPR-LTP-ENU\"", "\"longitude=-122&latitude=48&heightInMeters=5&orientation.yaw=0.1&orientation.pitch=0.2&orientation.roll=0.3\"")] + public void CanCorrectlyDeserializeYprLtpEnuSpecification(string authority, string id, string parameters) + { + var json = + "{" + + $"\"authority\": {authority}," + + $"\"id\": {id}," + + $"\"parameters\": {parameters}" + + "}"; + + var ltpEnuSpec = JsonConvert.DeserializeObject(json); + + Assert.That(ltpEnuSpec.Position.Latitude, Is.EqualTo(48)); + Assert.That(ltpEnuSpec.Position.Longitude, Is.EqualTo(-122)); + Assert.That(ltpEnuSpec.Position.HeightInMeters, Is.EqualTo(5)); + Assert.That(ltpEnuSpec.Orientation.Yaw, Is.EqualTo(0.1f)); + Assert.That(ltpEnuSpec.Orientation.Pitch, Is.EqualTo(0.2f)); + Assert.That(ltpEnuSpec.Orientation.Roll, Is.EqualTo(0.3f)); + } + + [TestCase("/Ethar.GeoPose/1.0", "YPR-LTP-ENU", "\"latitude=48&longitude=-122&heightInMeters=5&orientation.yaw=0.1&orientation.pitch=0.2&orientation.roll=0.3\"")] + public void CanCorrectlySerializeYprLtpEnuSpecification(string authority, string id, string parameters) + { + var ltpEnuSpec = new YawPitchRollOrientedLtpEnuSpecification(new TangentPointPosition() + { Latitude = 48, Longitude = -122, HeightInMeters = 5 }, new YawPitchRollAngles() { Yaw = 0.1f, Pitch = 0.2f, Roll = 0.3f }); + + var expected = + "{" + + $"\"authority\":\"{authority}\"," + + $"\"id\":\"{id}\"," + + $"\"parameters\":{parameters}" + + "}"; + + var json = JsonConvert.SerializeObject(ltpEnuSpec); + + Assert.That(json, Is.EqualTo(expected)); + } + + [Test] + public void CanCorrectlyMakeARoundTripSerialization() + { + var ltpEnuSpec = new YawPitchRollOrientedLtpEnuSpecification(new TangentPointPosition() + { Latitude = 48, Longitude = -122, HeightInMeters = 5 }, new YawPitchRollAngles() { Yaw = 0.1f, Pitch = 0.2f, Roll = 0.3f }); + + var json = JsonConvert.SerializeObject(ltpEnuSpec); + var converted = JsonConvert.DeserializeObject(json); + Assert.That(ltpEnuSpec, Is.EqualTo(converted)); + } + } +} diff --git a/Ethar.GeoPose.Authority/Constants.cs b/Ethar.GeoPose.Authority/Constants.cs new file mode 100644 index 0000000..2955101 --- /dev/null +++ b/Ethar.GeoPose.Authority/Constants.cs @@ -0,0 +1,17 @@ +// +// Copyright (c) Ethar. All rights reserved. +// + +namespace Ethar.GeoPose.Authority +{ + /// + /// A class that contains constants for the /Ethar.GeoPose/1.0 authority. + /// + public static class Constants + { + /// + /// The authority name. + /// + public const string AuthorityName = "/Ethar.GeoPose/1.0"; + } +} \ No newline at end of file diff --git a/Ethar.GeoPose.Authority/Ethar.GeoPose.Authority.csproj b/Ethar.GeoPose.Authority/Ethar.GeoPose.Authority.csproj new file mode 100644 index 0000000..d4434a5 --- /dev/null +++ b/Ethar.GeoPose.Authority/Ethar.GeoPose.Authority.csproj @@ -0,0 +1,80 @@ + + + + net452;net462;net48;netstandard2.0;netstandard2.1;netcoreapp3.1;net5.0;net6.0;net7.0 + True + latest + true + 1701;1702;8765;NETSDK1138 + true + readme.md + nuget.png + Ethar.GeoPose.Authority + Ethar.GeoPose.Authority + Ethar Inc. + Ethar.GeoPose.Authority + Ethar's authority implementation of version 1.0.0 of the GeoPose spec http://www.opengis.net/doc/DIS/geopose/1.0. + Ethar Inc. + Copyright (c) Ethar Inc. and Contributors + Ethar.GeoPose.Authority + true + true + (LGPL-2.0-only WITH FLTK-exception OR Apache-2.0+) + https://github.com/EtharInc/Ethar.GeoPose.Unity + https://github.com/EtharInc/Ethar.GeoPose.Unity.git + git + GeoPose Location GeoLocation Unity Positioning Authority + en-US + True + false + false + true + snupkg + false + false + true + + + + Full + + + + false + + + + + + + + + True + \ + + + True + \ + + + True + \ + + + + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; + + + diff --git a/Ethar.GeoPose.Authority/EtharGeoPoseAuthority.cs b/Ethar.GeoPose.Authority/EtharGeoPoseAuthority.cs new file mode 100644 index 0000000..e648029 --- /dev/null +++ b/Ethar.GeoPose.Authority/EtharGeoPoseAuthority.cs @@ -0,0 +1,149 @@ +// +// Copyright (c) Ethar. All rights reserved. +// + +namespace Ethar.GeoPose.Authority +{ + using System; + using Ethar.GeoPose.Authority.FrameSpecifications; + using Ethar.GeoPose.Authority.JsonConversion; + using Ethar.GeoPose.Authority.TransitionModels; + using Ethar.GeoPose.Authority.Validation; + using Ethar.GeoPose.Exceptions; + using Ethar.GeoPose.Interfaces; + using Ethar.GeoPose.TransitionModels; + using Ethar.GeoPose.Validation; + using Newtonsoft.Json.Linq; + + /// + /// A class that implements the "/Ethar.GeoPose/1.0" authority. + /// + public class EtharGeoPoseAuthority : IAuthority + { + /// + /// Initializes a new instance of the class. + /// + public EtharGeoPoseAuthority() + { + this.Validator = new EtharGeoPoseAuthorityExplicitFrameSpecificationValidator(); + } + + /// + public string AuthorityName => Constants.AuthorityName; + + /// + /// Gets the frame specification validator. + /// + public IExplicitFrameSpecificationValidator Validator { get; } + + /// + public JObject ConvertFrameSpecToJson(IFrameSpecification frameSpec) + { + switch (frameSpec) + { + case YawPitchRollOrientedLtpEnuSpecification yprLtpEnu: + return EtharFrameSpecificationJsonConverter.ConvertYprOrientedLtpEnuFrameSpecificationToJson(yprLtpEnu); + case QuaternionOrientedLtpEnuSpecification quatLtpEnu: + return EtharFrameSpecificationJsonConverter.ConvertQuaternionOrientedLtpEnuFrameSpecificationToJson(quatLtpEnu); + case TranslateRotateSpecification translateRotate: + return EtharFrameSpecificationJsonConverter.ConvertTranslateRotateFrameSpecificationToJson(translateRotate); + case LtpEnuSpecification ltpEnu: + return EtharFrameSpecificationJsonConverter.ConvertLtpEnuFrameSpecificationToJson(ltpEnu); + case LtpNedSpecification ltpNed: + return EtharFrameSpecificationJsonConverter.ConvertLtpNedFrameSpecificationToJson(ltpNed); + default: + throw new NotImplementedException($"The frame specification does not exist in the authority {this.AuthorityName}"); + } + } + + /// + public IFrameSpecification ConvertJsonToFrameSpec(JObject jsonObject) + { + var id = (string)jsonObject["id"]; + if (string.IsNullOrEmpty(id)) + { + // TODO: create exception type for this + throw new Exception(); + } + + IFrameSpecification frameSpec; + FrameSpecificationValidationResult validationResult; + + switch (id) + { + case EtharFrameSpecificationTypes.LtpEnuSpec: + frameSpec = EtharFrameSpecificationJsonConverter.ConvertJsonToLtpEnuFrameSpecification(jsonObject); + validationResult = this.Validator.Validate(frameSpec); + return validationResult.IsValid ? frameSpec : throw new FrameSpecificationInvalidException(validationResult.Message); + case EtharFrameSpecificationTypes.LtpNedSpec: + frameSpec = EtharFrameSpecificationJsonConverter.ConvertJsonToLtpNedFrameSpecification(jsonObject); + validationResult = this.Validator.Validate(frameSpec); + return validationResult.IsValid ? frameSpec : throw new FrameSpecificationInvalidException(validationResult.Message); + case EtharFrameSpecificationTypes.YprOrientedLtpEnuSpec: + frameSpec = EtharFrameSpecificationJsonConverter.ConvertJsonToYprOrientedLtpEnuFrameSpecification(jsonObject); + validationResult = this.Validator.Validate(frameSpec); + return validationResult.IsValid ? frameSpec : throw new FrameSpecificationInvalidException(validationResult.Message); + case EtharFrameSpecificationTypes.QuaternionOrientedLtpEnuSpec: + frameSpec = EtharFrameSpecificationJsonConverter.ConvertJsonToQuaternionOrientedLtpEnuFrameSpecification(jsonObject); + validationResult = this.Validator.Validate(frameSpec); + return validationResult.IsValid ? frameSpec : throw new FrameSpecificationInvalidException(validationResult.Message); + case EtharFrameSpecificationTypes.TranslateRotateSpec: + frameSpec = EtharFrameSpecificationJsonConverter.ConvertJsonToTranslateRotateFrameSpecification(jsonObject); + validationResult = this.Validator.Validate(frameSpec); + return validationResult.IsValid ? frameSpec : throw new FrameSpecificationInvalidException(validationResult.Message); + default: + throw new NotImplementedException(); + } + } + + /// + public TransitionModel ConvertJsonToTransitionModel(JObject jsonObject) + { + var id = (string)jsonObject["id"]; + if (string.IsNullOrEmpty(id)) + { + // TODO: create exception type for this + throw new Exception(); + } + + switch (id) + { + case EtharTransitionModelTypes.None: + return EtharTransitionModelJsonConverter.ConvertJsonToNoneTransitionModel(jsonObject); + case EtharTransitionModelTypes.Interpolated: + return EtharTransitionModelJsonConverter.ConvertJsonToInterpolatedTransitionModel(jsonObject); + default: + throw new NotImplementedException(); + } + } + + /// + public JObject ConvertTransitionModelToJson(TransitionModel transitionModel) + { + switch (transitionModel) + { + case NoneTransitionModel none: + return EtharTransitionModelJsonConverter.ConvertNoneTransitionModelToJson(none); + case InterpolatedTransitionModel interpolated: + return EtharTransitionModelJsonConverter.ConvertInterpolatedTransitionModelToJson(interpolated); + default: + throw new NotImplementedException(); + } + } + + /// + public bool IsFrameSpecificationExtrinsic(IFrameSpecification frameSpec) + { + switch (frameSpec) + { + case YawPitchRollOrientedLtpEnuSpecification _: + case QuaternionOrientedLtpEnuSpecification _: + case LtpEnuSpecification _: + case LtpNedSpecification _: + return true; + default: + return false; + } + } + } +} \ No newline at end of file diff --git a/Ethar.GeoPose.Authority/FrameSpecifications/EtharFrameSpecificationTypes.cs b/Ethar.GeoPose.Authority/FrameSpecifications/EtharFrameSpecificationTypes.cs new file mode 100644 index 0000000..84cd817 --- /dev/null +++ b/Ethar.GeoPose.Authority/FrameSpecifications/EtharFrameSpecificationTypes.cs @@ -0,0 +1,37 @@ +// +// Copyright (c) Ethar. All rights reserved. +// + +namespace Ethar.GeoPose.Authority.FrameSpecifications +{ + /// + /// A class that contains the names of the frame specification types included in the /Ethar.GeoPose/1.0 authority. + /// + public static class EtharFrameSpecificationTypes + { + /// + /// Identifier for an . + /// + public const string LtpEnuSpec = "LTP-ENU"; + + /// + /// Identifier for an . + /// + public const string LtpNedSpec = "LTP-NED"; + + /// + /// Identifier for a . + /// + public const string YprOrientedLtpEnuSpec = "YPR-LTP-ENU"; + + /// + /// Identifier for a . + /// + public const string QuaternionOrientedLtpEnuSpec = "Quaternion-LTP-ENU"; + + /// + /// Identifier for a . + /// + public const string TranslateRotateSpec = "Translate-Rotate"; + } +} diff --git a/Ethar.GeoPose.Authority/FrameSpecifications/LtpEnuSpecification.cs b/Ethar.GeoPose.Authority/FrameSpecifications/LtpEnuSpecification.cs new file mode 100644 index 0000000..2257e19 --- /dev/null +++ b/Ethar.GeoPose.Authority/FrameSpecifications/LtpEnuSpecification.cs @@ -0,0 +1,79 @@ +// +// Copyright (c) Ethar. All rights reserved. +// + +namespace Ethar.GeoPose.Authority.FrameSpecifications +{ + using System; + using Ethar.GeoPose.DataTypes; + using Ethar.GeoPose.FrameSpecifications; + using Newtonsoft.Json; + + /// + /// A frame specification with a local tangent plane coordinate system specialized to an east-north-up system, where the X axis is aligned toward east and the Y axis toward north. + /// + /// Derived from Figure 8 from section 7.2.1 of version 1.0.0 of the GeoPose spec http://www.opengis.net/doc/DIS/geopose/1.0. + /// + public class LtpEnuSpecification : BaseFrameSpecification, IEquatable + { + /// + /// Initializes a new instance of the class. + /// + public LtpEnuSpecification() + : base(EtharFrameSpecificationTypes.LtpEnuSpec, Constants.AuthorityName) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The position in latitude and longitude in decimal degrees, and height in meters. + public LtpEnuSpecification(TangentPointPosition position) + : base(EtharFrameSpecificationTypes.LtpEnuSpec, Constants.AuthorityName) + { + this.Position = position; + } + + /// + /// Initializes a new instance of the class. + /// + /// An ID that uniquely defines the frame within the authority. + /// A string uniquely specifying a source of reference frame specifications. + /// The position in latitude and longitude in decimal degrees, and height in meters. + protected LtpEnuSpecification(string id, string authority, TangentPointPosition position) + : base(id, authority) + { + this.Position = position; + } + + /// + /// Gets the position in latitude and longitude in decimal degrees, and height in meters. + /// + [JsonProperty("position")] + public TangentPointPosition Position { get; } + + /// + public bool Equals(LtpEnuSpecification other) + { + return this.Position == other.Position + && base.Equals(other); + } + + /// + public override bool Equals(object obj) + { + return obj is LtpEnuSpecification equatable && this.Equals(equatable); + } + + /// + public override int GetHashCode() + { + unchecked + { + var hashcode = base.GetHashCode(); + hashcode = (hashcode * 397) ^ this.Position.GetHashCode(); + return hashcode; + } + } + } +} diff --git a/Ethar.GeoPose.Authority/FrameSpecifications/LtpNedSpecification.cs b/Ethar.GeoPose.Authority/FrameSpecifications/LtpNedSpecification.cs new file mode 100644 index 0000000..c83c02f --- /dev/null +++ b/Ethar.GeoPose.Authority/FrameSpecifications/LtpNedSpecification.cs @@ -0,0 +1,67 @@ +// +// Copyright (c) Ethar. All rights reserved. +// + +namespace Ethar.GeoPose.Authority.FrameSpecifications +{ + using System; + using Ethar.GeoPose.DataTypes; + using Ethar.GeoPose.FrameSpecifications; + using Newtonsoft.Json; + + /// + /// A frame specification with a local tangent plane coordinate system specialized to an north-east-down system, where the X axis is aligned toward north and the Y axis toward east. + /// + /// Derived from Figure 8 from section 7.2.1 of version 1.0.0 of the GeoPose spec http://www.opengis.net/doc/DIS/geopose/1.0. + /// + public class LtpNedSpecification : BaseFrameSpecification, IEquatable + { + /// + /// Initializes a new instance of the class. + /// + public LtpNedSpecification() + : base(EtharFrameSpecificationTypes.LtpNedSpec, Constants.AuthorityName) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The position in latitude and longitude in decimal degrees, and height in meters. + public LtpNedSpecification(TangentPointPosition position) + : base(EtharFrameSpecificationTypes.LtpNedSpec, Constants.AuthorityName) + { + this.Position = position; + } + + /// + /// Gets the position in latitude and longitude in decimal degrees, and height in meters. + /// + [JsonProperty("position")] + public TangentPointPosition Position { get; } + + /// + public bool Equals(LtpNedSpecification other) + { + return this.Position == other.Position + && base.Equals(other); + } + + /// + public override bool Equals(object obj) + { + return obj is LtpNedSpecification equatable && this.Equals(equatable); + } + + /// + public override int GetHashCode() + { + unchecked + { + var hashcode = base.GetHashCode(); + hashcode = (hashcode * 397) ^ this.Position.GetHashCode(); + return hashcode; + } + } + } +} diff --git a/Ethar.GeoPose.Authority/FrameSpecifications/QuaternionOrientedLtpEnuSpecification.cs b/Ethar.GeoPose.Authority/FrameSpecifications/QuaternionOrientedLtpEnuSpecification.cs new file mode 100644 index 0000000..a64d3c8 --- /dev/null +++ b/Ethar.GeoPose.Authority/FrameSpecifications/QuaternionOrientedLtpEnuSpecification.cs @@ -0,0 +1,68 @@ +// +// Copyright (c) Ethar. All rights reserved. +// + +namespace Ethar.GeoPose.Authority.FrameSpecifications +{ + using System; + using Ethar.GeoPose.DataTypes; + using Newtonsoft.Json; + + /// + /// A frame specification with a local tangent plane coordinate system specialized to an east-north-up system, where the X axis is aligned toward east and the Y axis toward north. + /// Also contains a quaternion describing an orientation. + /// + /// Derived from Figure 8 from section 7.2.1 of version 1.0.0 of the GeoPose spec http://www.opengis.net/doc/DIS/geopose/1.0. + /// + public class QuaternionOrientedLtpEnuSpecification : LtpEnuSpecification, IEquatable + { + /// + /// Initializes a new instance of the class. + /// + public QuaternionOrientedLtpEnuSpecification() + : base(EtharFrameSpecificationTypes.QuaternionOrientedLtpEnuSpec, Constants.AuthorityName, default) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The position in latitude, longitude, and height in meters. + /// A quaternion representing an orientation. + public QuaternionOrientedLtpEnuSpecification(TangentPointPosition position, UnitQuaternion orientation) + : base(EtharFrameSpecificationTypes.QuaternionOrientedLtpEnuSpec, Constants.AuthorityName, position) + { + this.Orientation = orientation; + } + + /// + /// Gets the orientation. + /// + [JsonProperty("orientation")] + public UnitQuaternion Orientation { get; } + + /// + public bool Equals(QuaternionOrientedLtpEnuSpecification other) + { + return this.Orientation == other.Orientation + && base.Equals(other); + } + + /// + public override bool Equals(object obj) + { + return obj is QuaternionOrientedLtpEnuSpecification equatable && this.Equals(equatable); + } + + /// + public override int GetHashCode() + { + unchecked + { + var hashcode = base.GetHashCode(); + hashcode = (hashcode * 397) ^ this.Orientation.GetHashCode(); + return hashcode; + } + } + } +} diff --git a/Ethar.GeoPose.Authority/FrameSpecifications/TranslateRotateSpecification.cs b/Ethar.GeoPose.Authority/FrameSpecifications/TranslateRotateSpecification.cs new file mode 100644 index 0000000..c6db7fc --- /dev/null +++ b/Ethar.GeoPose.Authority/FrameSpecifications/TranslateRotateSpecification.cs @@ -0,0 +1,78 @@ +// +// Copyright (c) Ethar. All rights reserved. +// + +namespace Ethar.GeoPose.Authority.FrameSpecifications +{ + using System; + using Ethar.GeoPose.DataTypes; + using Ethar.GeoPose.FrameSpecifications; + using Newtonsoft.Json; + + /// + /// A frame specification that specifies a translation and rotation relative to another frame specification. + /// This type of frame specification cannot be the outermost frame in a chain or graph. + /// + /// Derived from Figure 8 from section 7.2.1 of version 1.0.0 of the GeoPose spec http://www.opengis.net/doc/DIS/geopose/1.0. + /// + public class TranslateRotateSpecification : BaseFrameSpecification, IEquatable + { + /// + /// Initializes a new instance of the class. + /// + public TranslateRotateSpecification() + : base(EtharFrameSpecificationTypes.TranslateRotateSpec, Constants.AuthorityName) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The translation. + /// The rotation. + public TranslateRotateSpecification(UnitVector3 translation, UnitQuaternion rotation) + : base(EtharFrameSpecificationTypes.TranslateRotateSpec, Constants.AuthorityName) + { + this.Translation = translation; + this.Rotation = rotation; + } + + /// + /// Gets the translation. + /// + [JsonProperty("translation")] + public UnitVector3 Translation { get; } + + /// + /// Gets the rotation. + /// + [JsonProperty("rotation")] + public UnitQuaternion Rotation { get; } + + /// + public bool Equals(TranslateRotateSpecification other) + { + return this.Translation == other.Translation + && this.Rotation == other.Rotation + && base.Equals(other); + } + + /// + public override bool Equals(object obj) + { + return obj is TranslateRotateSpecification equatable && this.Equals(equatable); + } + + /// + public override int GetHashCode() + { + unchecked + { + var hashcode = base.GetHashCode(); + hashcode = (hashcode * 397) ^ this.Translation.GetHashCode(); + hashcode = (hashcode * 397) ^ this.Rotation.GetHashCode(); + return hashcode; + } + } + } +} diff --git a/Ethar.GeoPose.Authority/FrameSpecifications/YawPitchRollOrientedLtpEnuSpecification.cs b/Ethar.GeoPose.Authority/FrameSpecifications/YawPitchRollOrientedLtpEnuSpecification.cs new file mode 100644 index 0000000..df61907 --- /dev/null +++ b/Ethar.GeoPose.Authority/FrameSpecifications/YawPitchRollOrientedLtpEnuSpecification.cs @@ -0,0 +1,68 @@ +// +// Copyright (c) Ethar. All rights reserved. +// + +namespace Ethar.GeoPose.Authority.FrameSpecifications +{ + using System; + using Ethar.GeoPose.DataTypes; + using Newtonsoft.Json; + + /// + /// A frame specification with a local tangent plane coordinate system specialized to an east-north-up system, where the X axis is aligned toward east and the Y axis toward north. + /// Also contains a yaw, pitch, and roll describing an orientation. + /// + /// Derived from Figure 8 from section 7.2.1 of version 1.0.0 of the GeoPose spec http://www.opengis.net/doc/DIS/geopose/1.0. + /// + public class YawPitchRollOrientedLtpEnuSpecification : LtpEnuSpecification, IEquatable + { + /// + /// Initializes a new instance of the class. + /// + public YawPitchRollOrientedLtpEnuSpecification() + : base(EtharFrameSpecificationTypes.YprOrientedLtpEnuSpec, Constants.AuthorityName, default) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The position in latitude, longitude, and height in meters. + /// Yaw, pitch, and roll representing an orientation. + public YawPitchRollOrientedLtpEnuSpecification(TangentPointPosition position, YawPitchRollAngles orientation) + : base(EtharFrameSpecificationTypes.YprOrientedLtpEnuSpec, Constants.AuthorityName, position) + { + this.Orientation = orientation; + } + + /// + /// Gets the orientation. + /// + [JsonProperty("orientation")] + public YawPitchRollAngles Orientation { get; } + + /// + public bool Equals(YawPitchRollOrientedLtpEnuSpecification other) + { + return this.Orientation == other.Orientation + && base.Equals(other); + } + + /// + public override bool Equals(object obj) + { + return obj is YawPitchRollOrientedLtpEnuSpecification equatable && this.Equals(equatable); + } + + /// + public override int GetHashCode() + { + unchecked + { + var hashcode = base.GetHashCode(); + hashcode = (hashcode * 397) ^ this.Orientation.GetHashCode(); + return hashcode; + } + } + } +} diff --git a/Ethar.GeoPose.Authority/JsonConversion/EtharFrameSpecificationJsonConverter.cs b/Ethar.GeoPose.Authority/JsonConversion/EtharFrameSpecificationJsonConverter.cs new file mode 100644 index 0000000..459458c --- /dev/null +++ b/Ethar.GeoPose.Authority/JsonConversion/EtharFrameSpecificationJsonConverter.cs @@ -0,0 +1,223 @@ +// +// Copyright (c) Ethar. All rights reserved. +// + +namespace Ethar.GeoPose.Authority.JsonConversion +{ + using Ethar.GeoPose.Authority.FrameSpecifications; + using Ethar.GeoPose.DataTypes; + using Ethar.GeoPose.Extensions; + using Ethar.GeoPose.Validation; + using Newtonsoft.Json; + using Newtonsoft.Json.Linq; + + /// + /// A utility class that converts JObjects to frame specifications. + /// + internal class EtharFrameSpecificationJsonConverter + { + /// + /// Converts json to a . + /// + /// The json to convert. + /// A . + internal static LtpEnuSpecification ConvertJsonToLtpEnuFrameSpecification(JObject jObject) + { + if (ValidationUtilities.ValidateJsonObjectParameters(jObject, out var queryString)) + { + var lat = float.Parse(queryString.GetParameter("latitude")); + var lon = float.Parse(queryString.GetParameter("longitude")); + var height = float.Parse(queryString.GetParameter("heightInMeters")); + + return new LtpEnuSpecification(new TangentPointPosition() { Latitude = lat, Longitude = lon, HeightInMeters = height }); + } + + return null; + } + + /// + /// Converts a to json. + /// + /// The to convert. + /// A json representation of the . + internal static JObject ConvertLtpEnuFrameSpecificationToJson(LtpEnuSpecification spec) + { + var paramString = spec.Position.BuildParamString(); + + var jObj = new JObject + { + { "authority", spec.Authority }, + { "id", spec.Id }, + { "parameters", paramString }, + }; + + return jObj; + } + + /// + /// Converts json to a . + /// + /// The json to convert. + /// A . + internal static LtpNedSpecification ConvertJsonToLtpNedFrameSpecification(JObject jObject) + { + if (ValidationUtilities.ValidateJsonObjectParameters(jObject, out var queryString)) + { + var lat = float.Parse(queryString.GetParameter("latitude")); + var lon = float.Parse(queryString.GetParameter("longitude")); + var height = float.Parse(queryString.GetParameter("heightInMeters")); + + return new LtpNedSpecification(new TangentPointPosition() { Latitude = lat, Longitude = lon, HeightInMeters = height }); + } + + return null; + } + + /// + /// Converts a to json. + /// + /// The to convert. + /// A json representation of the . + internal static JObject ConvertLtpNedFrameSpecificationToJson(LtpNedSpecification spec) + { + var paramString = spec.Position.BuildParamString(); + + var jObj = new JObject + { + { "authority", spec.Authority }, + { "id", spec.Id }, + { "parameters", paramString }, + }; + + return jObj; + } + + /// + /// Converts json to a . + /// + /// The json to convert. + /// A . + internal static YawPitchRollOrientedLtpEnuSpecification ConvertJsonToYprOrientedLtpEnuFrameSpecification(JObject jObject) + { + if (ValidationUtilities.ValidateJsonObjectParameters(jObject, out var queryString)) + { + var lat = float.Parse(queryString.GetParameter("latitude")); + var lon = float.Parse(queryString.GetParameter("longitude")); + var height = float.Parse(queryString.GetParameter("heightInMeters")); + var yaw = float.Parse(queryString.GetParameter("orientation.yaw")); + var pitch = float.Parse(queryString.GetParameter("orientation.pitch")); + var roll = float.Parse(queryString.GetParameter("orientation.roll")); + + return new YawPitchRollOrientedLtpEnuSpecification(new TangentPointPosition() { Latitude = lat, Longitude = lon, HeightInMeters = height }, new YawPitchRollAngles() { Yaw = yaw, Pitch = pitch, Roll = roll }); + } + + return null; + } + + /// + /// Converts a to json. + /// + /// The to convert. + /// A json representation of the . + internal static JObject ConvertYprOrientedLtpEnuFrameSpecificationToJson(YawPitchRollOrientedLtpEnuSpecification spec) + { + var positionParamString = spec.Position.BuildParamString(); + var orientationParamString = spec.Orientation.BuildOrientationParamString(); + + var jObj = new JObject + { + { "authority", spec.Authority }, + { "id", spec.Id }, + { "parameters", string.Concat(positionParamString, "&", orientationParamString) }, + }; + + return jObj; + } + + /// + /// Converts json to a . + /// + /// The json to convert. + /// A . + internal static QuaternionOrientedLtpEnuSpecification ConvertJsonToQuaternionOrientedLtpEnuFrameSpecification(JObject jObject) + { + if (ValidationUtilities.ValidateJsonObjectParameters(jObject, out var queryString)) + { + var lat = float.Parse(queryString.GetParameter("latitude")); + var lon = float.Parse(queryString.GetParameter("longitude")); + var height = float.Parse(queryString.GetParameter("heightInMeters")); + var x = float.Parse(queryString.GetParameter("orientation.x")); + var y = float.Parse(queryString.GetParameter("orientation.y")); + var z = float.Parse(queryString.GetParameter("orientation.z")); + var w = float.Parse(queryString.GetParameter("orientation.w")); + + return new QuaternionOrientedLtpEnuSpecification(new TangentPointPosition() { Latitude = lat, Longitude = lon, HeightInMeters = height }, new UnitQuaternion() { X = x, Y = y, Z = z, W = w }); + } + + return null; + } + + /// + /// Converts a to json. + /// + /// The to convert. + /// A json representation of the . + internal static JObject ConvertQuaternionOrientedLtpEnuFrameSpecificationToJson(QuaternionOrientedLtpEnuSpecification spec) + { + var positionParamString = spec.Position.BuildParamString(); + var orientationParamString = spec.Orientation.BuildOrientationParamString(); + + var jObj = new JObject + { + { "authority", spec.Authority }, + { "id", spec.Id }, + { "parameters", string.Concat(positionParamString, "&", orientationParamString) }, + }; + + return jObj; + } + + /// + /// Converts json to a . + /// + /// The json to convert. + /// A . + internal static TranslateRotateSpecification ConvertJsonToTranslateRotateFrameSpecification(JObject jObject) + { + if (ValidationUtilities.ValidateJsonObjectParameters(jObject, out var queryString)) + { + var translationX = JsonConvert.DeserializeObject(queryString.GetParameter("translation.x")); + var translationY = JsonConvert.DeserializeObject(queryString.GetParameter("translation.y")); + var translationZ = JsonConvert.DeserializeObject(queryString.GetParameter("translation.z")); + var rotationX = JsonConvert.DeserializeObject(queryString.GetParameter("rotation.x")); + var rotationY = JsonConvert.DeserializeObject(queryString.GetParameter("rotation.y")); + var rotationZ = JsonConvert.DeserializeObject(queryString.GetParameter("rotation.z")); + var rotationW = JsonConvert.DeserializeObject(queryString.GetParameter("rotation.w")); + + return new TranslateRotateSpecification(new UnitVector3() { X = translationX, Y = translationY, Z = translationZ }, new UnitQuaternion() { X = rotationX, Y = rotationY, Z = rotationZ, W = rotationW }); + } + + return null; + } + + /// + /// Converts a to json. + /// + /// The to convert. + /// A json representation of the . + internal static JObject ConvertTranslateRotateFrameSpecificationToJson(TranslateRotateSpecification spec) + { + var positionParamString = spec.Translation.BuildTranslationParamString(); + var orientationParamString = spec.Rotation.BuildRotationParamString(); + + var jObj = new JObject + { + { "authority", spec.Authority }, + { "id", spec.Id }, + { "parameters", string.Concat(positionParamString, "&", orientationParamString) }, + }; + + return jObj; + } + } +} \ No newline at end of file diff --git a/Ethar.GeoPose.Authority/JsonConversion/EtharTransitionModelJsonConverter.cs b/Ethar.GeoPose.Authority/JsonConversion/EtharTransitionModelJsonConverter.cs new file mode 100644 index 0000000..d81ec33 --- /dev/null +++ b/Ethar.GeoPose.Authority/JsonConversion/EtharTransitionModelJsonConverter.cs @@ -0,0 +1,63 @@ +// +// Copyright (c) Ethar. All rights reserved. +// + +namespace Ethar.GeoPose.Authority.JsonConversion +{ + using Ethar.GeoPose.Authority.TransitionModels; + using Newtonsoft.Json.Linq; + + /// + /// A utility class that converts JObjects to transition models. + /// + internal class EtharTransitionModelJsonConverter + { + /// + /// Converts json to a . + /// + /// The json to convert. + /// A . + internal static NoneTransitionModel ConvertJsonToNoneTransitionModel(JObject jObject) => new NoneTransitionModel(); + + /// + /// Converts a to json. + /// + /// The to convert. + /// A json representation of the . + internal static JObject ConvertNoneTransitionModelToJson(NoneTransitionModel model) + { + var jObj = new JObject + { + { "authority", model.Authority }, + { "id", model.Id }, + { "parameters", string.Empty }, + }; + + return jObj; + } + + /// + /// Converts json to a . + /// + /// The json to convert. + /// A . + internal static InterpolatedTransitionModel ConvertJsonToInterpolatedTransitionModel(JObject jObject) => new InterpolatedTransitionModel(); + + /// + /// Converts a to json. + /// + /// The to convert. + /// A json representation of the . + internal static JObject ConvertInterpolatedTransitionModelToJson(InterpolatedTransitionModel model) + { + var jObj = new JObject + { + { "authority", model.Authority }, + { "id", model.Id }, + { "parameters", string.Empty }, + }; + + return jObj; + } + } +} diff --git a/Ethar.GeoPose.Authority/LICENSE b/Ethar.GeoPose.Authority/LICENSE new file mode 100644 index 0000000..2dc7c55 --- /dev/null +++ b/Ethar.GeoPose.Authority/LICENSE @@ -0,0 +1,203 @@ +# Ethar GeoPose Apache 2.0 License + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. \ No newline at end of file diff --git a/Ethar.GeoPose.Authority/TransitionModels/EtharTransitionModelTypes.cs b/Ethar.GeoPose.Authority/TransitionModels/EtharTransitionModelTypes.cs new file mode 100644 index 0000000..f7f807d --- /dev/null +++ b/Ethar.GeoPose.Authority/TransitionModels/EtharTransitionModelTypes.cs @@ -0,0 +1,24 @@ +// +// Copyright (c) Ethar. All rights reserved. +// + +namespace Ethar.GeoPose.Authority.TransitionModels +{ + using Ethar.GeoPose.TransitionModels; + + /// + /// A class that contains the names of the types of in the /Ethar.GeoPose/1.0 authority. + /// + public static class EtharTransitionModelTypes + { + /// + /// Identifier for no transition model. + /// + public const string None = "none"; + + /// + /// Identifier for an interpolated transition model. + /// + public const string Interpolated = "interpolated"; + } +} diff --git a/Ethar.GeoPose.Authority/TransitionModels/InterpolatedTransitionModel.cs b/Ethar.GeoPose.Authority/TransitionModels/InterpolatedTransitionModel.cs new file mode 100644 index 0000000..0abe3b3 --- /dev/null +++ b/Ethar.GeoPose.Authority/TransitionModels/InterpolatedTransitionModel.cs @@ -0,0 +1,22 @@ +// +// Copyright (c) Ethar. All rights reserved. +// + +namespace Ethar.GeoPose.Authority.TransitionModels +{ + using Ethar.GeoPose.TransitionModels; + + /// + /// A class that represents an interpolated transition model. + /// + public class InterpolatedTransitionModel : TransitionModel + { + /// + /// Initializes a new instance of the class. + /// + public InterpolatedTransitionModel() + : base("interpolated", Constants.AuthorityName) + { + } + } +} diff --git a/Ethar.GeoPose.Authority/TransitionModels/NoneTransitionModel.cs b/Ethar.GeoPose.Authority/TransitionModels/NoneTransitionModel.cs new file mode 100644 index 0000000..0e0a9fa --- /dev/null +++ b/Ethar.GeoPose.Authority/TransitionModels/NoneTransitionModel.cs @@ -0,0 +1,22 @@ +// +// Copyright (c) Ethar. All rights reserved. +// + +namespace Ethar.GeoPose.Authority.TransitionModels +{ + using Ethar.GeoPose.TransitionModels; + + /// + /// A class that represents an empty transition model as it is not required. + /// + public class NoneTransitionModel : TransitionModel + { + /// + /// Initializes a new instance of the class. + /// + public NoneTransitionModel() + : base("none", Constants.AuthorityName) + { + } + } +} diff --git a/Ethar.GeoPose.Authority/Validation/EtharGeoPoseAuthorityExplicitFrameSpecificationValidator.cs b/Ethar.GeoPose.Authority/Validation/EtharGeoPoseAuthorityExplicitFrameSpecificationValidator.cs new file mode 100644 index 0000000..1253a01 --- /dev/null +++ b/Ethar.GeoPose.Authority/Validation/EtharGeoPoseAuthorityExplicitFrameSpecificationValidator.cs @@ -0,0 +1,31 @@ +// +// Copyright (c) Ethar. All rights reserved. +// + +namespace Ethar.GeoPose.Authority.Validation +{ + using Ethar.GeoPose.Authority.FrameSpecifications; + using Ethar.GeoPose.Extensions; + using Ethar.GeoPose.Interfaces; + using Ethar.GeoPose.Validation; + + /// + /// A utility class that validates frame specifications from the /Ethar.GeoPose/1.0 authority. + /// + public class EtharGeoPoseAuthorityExplicitFrameSpecificationValidator : IExplicitFrameSpecificationValidator + { + /// + public FrameSpecificationValidationResult Validate(IFrameSpecification frame) + { + switch (frame) + { + case LtpNedSpecification ltpNedSpec: + return ltpNedSpec.Position.Validate(); + case LtpEnuSpecification ltpEnuSpec: + return ltpEnuSpec.Position.Validate(); + default: + return FrameSpecificationValidationResult.Valid; + } + } + } +} \ No newline at end of file diff --git a/Ethar.GeoPose.Authority/readme.md b/Ethar.GeoPose.Authority/readme.md new file mode 100644 index 0000000..dc66a21 --- /dev/null +++ b/Ethar.GeoPose.Authority/readme.md @@ -0,0 +1,224 @@ +

+ GeoPose Logo +

+ +# GeoPose Authority Implementation by [Ethar, Inc.](https://www.ethar.com/) + +[![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](https://github.com/EtharInc/Ethar.GeoPose/blob/main/LICENSE) + +The [GeoPose standard](https://docs.ogc.org/dis/21-056r10/21-056r10.html) enables the easy integration of digital elements on and in relation to the surface of the planet. + +Ethar.GeoPose.Authority is a C# implementation of a GeoPose authority whose responsibility is to manage, convert and maintain Frame Specifications used within a GeoPose definition. + +## Table of Contents + +- [Introduction](#introduction) +- [Who is Ethar](#who-is-ethar) +- [Features](#ethar-geopose-authority-implementation-features) +- [Installation](#installation) +- [Usage](#usage) +- [Contributing](#contributing) +- [Terms of Use](#terms-of-use) +- [License](#license) + +## Introduction + +Ethar.GeoPose is a convenient library written in C# for performing GeoPose calculations and integrating them into your applications, the Ethar.GeoPose.Authority provides a full Authority implementation based on the GeoPose standard and implements the 5 main Frame Specifications outlined by the specification. + +GeoPose is a standard stewarded by the [Open Geospatial Consortium](https://www.ogc.org/) & supported by members of [Open AR Cloud](https://www.openarcloud.org/) which defines the encodings for the real world position and orientation of a real or a digital object in a machine-readable form. + +The GeoPose standard describes a geographically-anchored pose (GeoPose) with 6 degrees of freedom referenced to one or more standardized Coordinate Reference Systems (CRSs). This provides an interoperable way to seamlessly express, record, and share the GeoPose of objects in an entirely consistent manner across different applications, users, devices, services, and platforms which adopt the standard or are able to translate/exchange the GeoPose into another CRS. + +## Who is Ethar? + +[Ethar, Inc.](https://www.ethar.com/) is a spatial computing company focused on delivering tools for the entire life-cycle of XR content. We believe the future of spatial computing is open and cooperative, we strive to incorporate open and interoperable technology standards into the very foundation of our products. + +Along the way we have built some useful tooling, that we want to share with the community, and encourage the adoption of open standards. This is our implementation of GeoPose. We think it is one of the most important building blocks of an open, interoperable & decentralized spatial web. + +## Ethar GeoPose Authority Implementation Features + +- Implements 5 core Frame Specifications. +- Provides JSON conversion routines to receive and transmit GeoPose data utilising the Frame Specifications within the GeoPose SDU's. +- Validation functions to qualify incoming Frame Specifications. +- An example Interpolated Transition Model and a fallback None Transition Model. +- Full Authority implementation ready for use, including helper functions. +- Full Unit Testing of GeoPose Authority concepts and elements. (available via the [GitHub site](https://github.com/EtharInc/Ethar.GeoPose/tree/main/Ethar.GeoPose.Authority.UnitTests), not included with npm package) + +> Full documentation on the implementation specification and helper docs, including common guides for C# and Unity can be found at: +> +> [https://etharinc.github.io/Ethar.GeoPose.Docs](https://etharinc.github.io/Ethar.GeoPose.Docs) + +## Installation + +The Ethar GeoPose library and the corresponding Ethar Authority implementation have been provided in as many places as possible, including: + +- NuGet - for cross platform C# development. +- OpenUPM - for versioned Unity deployment. +- Git - Fully open sourced on GitHub. + +> For comments, questions and queries, please log a request on the [Ethar GeoPose GitHub site here](https://github.com/EtharInc/Ethar.GeoPose/issues). + +### Install via NuGet + +```xml + + + +``` + +### Install via OpenUPM + +```text + openupm add com.ethar.GeoPose +``` + +### Git Source + +Being open source, all the code, examples and features are available via the GitHub page for the Ethar GeoPose Library here: + +``` text +https://github.com/EtharInc/Ethar.GeoPose +``` + +## Usage + +Beyond what is outlined in the GeoPose standard, use of the Ethar GeoPose authority is documented on the [Ethar GeoPose documentation site here](https://etharinc.github.io/Ethar.GeoPose.Docs). + +## Concepts + +In summary, the following concepts are crucial to understanding the [GeoPose specification](https://docs.ogc.org/dis/21-056r10/21-056r10.html) defined by the OGC, namely: + +- [A Pose, or fixed position for an object.](#pose) +- [The Orientation or direction of a posed object.](#orientation) +- [GeoPose Structural Data Units (SDU) to describe locational metadata.](#structural-data-units) +- [A Frame Specification for GeoPose Data.](#frame-specifications) +- [A GeoPose Authority](#geopose-authorities). + +### Pose + +At the core of the GeoPose definition is the concept of a Pose, that being a position or place where an object is located. A Pose may be determined by a coordinate system relative to a physical location, which may be a GPS, cartesian, image tracked or SLAM position. + +The position is absolute at the time of creation and updated as the need arises. + +### Orientation + +To define the actual direction in which a GeoPosed object is placed, a direction is needed to denote the physical orientation of a placed object. The orientation of objects relative to their position is one of the key factors that sets the GeoPose standard apart from other positioning solutions. + +### Structural Data Units + +The base definition of a GeoPose construct is its ```Structural Data Unit``` definition, which outlines the serialized data that is shared between entities to communicate GeoPosed data. By default, there are 8 Structural Data Units defined within the [GeoPose standard](https://docs.ogc.org/dis/21-056r10/21-056r10.html#toc45), which are: + +- Basic YawPitchRoll - Basic positioning using WGS84 coordinates for position and Euler angles for orientation. +- Basic Quaternion - Basic positioning using WGS84 coordinates for position and a Quaternion for orientation. +- Advanced - An advanced concept utilizing a [Frame Specification](#frame-specifications) that defines a reference frame for an object. +- Graph - An SDU that contains a directed acyclic graph representation of the transformational relationships between reference frames defined by [Frame Specifications](#frame-specifications). +- Chain - An SDU that represents a linear sequence of poses linked by full 6DoF transformations, with the first frame in the sequence being extrinsic. +- Regular Series - An ordered set of operations to perform on a GeoPosed object, complete with timed events. +- Irregular Series - An unordered set of operations for use on a GeoPosed Object. +- Stream - Another advanced use case whereby complex operations can be structured, such as animation. + +Structural Data Units consist of base GeoPose Data Types and can contain one or more [Frame Specifications](#frame-specifications) for extending a GeoPosed Object. Which type of SDU you use will largely depend on your use case, and there is always the option of creating your own (at the cost of interoperability). + +> See the [GeoPose standard](https://docs.ogc.org/dis/21-056r10/21-056r10.html#toc45) section on Structural Data Units for more information. + +### Frame Specifications + +A ```Frame Specification``` can take on multiple roles or responsibilities for objects placed using a GeoPose, these can range from Transform Animations, waypoints, relativistic placement and structure. Which frame specifications are used will be largely determined by the use case required and the [SDU](#structural-data-units) that has been implemented. + +At its most simplistic level, Frame Specifications are references used to co-locate a GeoPose object in relation to another physical entity (such as the Earth) or another GeoPose. In advanced cases they can be used to infer animation, or the bounds of a GeoPosed object. + +Unlike SDU's however, Frame Specifications require an authority who is responsible for orchestrating the content of the specification and ultimately, controls how the data is assembled and disassembled for transport. (Different organizations may implement different authorities for managing how they interpret and expose GeoPosed data.) + +> See the [GeoPose standard](https://docs.ogc.org/dis/21-056r10/21-056r10.html#term-frame-specification) section on Frame Specifications for more information. + +### GeoPose Authorities + +An ```Authority``` in the GeoPose standard is the entity responsible for the understanding, conversion and transformation of any Frame Specification. It is defined in the Ethar GeoPose library through an Interface designed to enforce a specific pattern for Authority Implementations, as shown below: + +![IAuthority Interface](https://raw.githubusercontent.com/EtharInc/Ethar.GeoPose/main/Images/architecture/IAuthority-Interface.png)
+*figure 1: IAuthority Interface.* + +The interface defines a single property and several methods required by an Authority for operation, namely: + +- Authority Name - The unique name/identifier for the authority in the form of ```"/GeoPose/1.0"``` +- ConvertJsonToFrameSpec - Method to take in a GeoPose Frame Specification JSON string and output a Frame Specification definition. +- ConvertFrameSpecToJson - Method to take a Frame Specification object and turn it into serialized GeoPose Frame Specification JSON string. +- ConvertJsonToTransitionModel - Method to take a [Transition Model](https://docs.ogc.org/dis/21-056r10/21-056r10.html#toc17) JSON string and output a Transition Model definition. +- ConvertTransitionModelToJson - Method to serialize a [Transition model](https://docs.ogc.org/dis/21-056r10/21-056r10.html#toc17) into a specific GeoPose Transition Model JSON string + +> Additionally, it is recommended to also implement a ```FrameSpecificationValidator``` as part of any Authority implementation, to validate any Frame Specifications and handle any irregularities with incoming data. + +You can see a fully implemented authority implementation [here](https://github.com/EtharInc/Ethar.GeoPose/blob/main/Ethar.GeoPose.Authority/EtharGeoPoseAuthority.cs) or refer to the ```Ethar GeoPose Authority Sample``` included with this package. + +It is Critical, when implementing your own authority, to define the frame specification that the authority is responsible for, including the data types (either c# native or GeoPose elements) and then write ```Convertors``` to extract and understand the Frame specifications being handled by the authority, with specific attention to use the ```ValidationUtilities``` provided by the Ethar GeoPose library, for example: + +```csharp +public static ExampleExtrinsicFrameSpec ConvertJObjectToExampleExtrinsicFrameSpec(JObject jObject) +{ + // Validated the incoming json object string and checks that it has the required values and also + // checks if this is the authority mentioned in the incoming data that handles the frame specification. + if (ValidationUtilities.ValidateJsonObjectParameters(jObject, Constants.AuthorityName, out var queryString)) + { + // Retrieves the required data from the json to construct the Frame Specification. + var lat = float.Parse(queryString.GetParameter("latitude")); + var lon = float.Parse(queryString.GetParameter("longitude")); + + // Returns a new instance of the Frame Specification with the data populated. + return new ExampleExtrinsicFrameSpec { Latitude = lat, Longitude = lon }; + } + + return null; +} +``` + +*figure 2: Authority Frame Specification conversion.* + +This ensures that all serialization and deserialization is handled automatically by the authority whenever a Frame Specification (or SDU containing a Frame Specification) is used. + +> For a more detailed explanation, check the [Ethar GeoPose documentation](https://etharinc.github.io/Ethar.GeoPose.Docs). + +### Authority Provider + +To ensure the successful use and access to any implemented GeoPose Authorities, an ```AuthorityProvider``` class is provided as part of this package to Register, Request and remove active Authorities in your solution, this provides a "Single Path" for querying authorities when evaluating incoming data. + +Under the hood, the Authority Provider is simply a Safe Dictionary implemented within a Static class which has proven useful for API-level access within a project. + +The surface of the Authority Provider is as follows: + +![Authority Provider](https://raw.githubusercontent.com/EtharInc/Ethar.GeoPose/main/Images/architecture/AuthorityProvider-utility.png)
+*figure 3: Authority Provider.* + +The utility defines a single exposed property and several methods to safely access the dictionary, namely: + +- Authorities - Read only list of registered authorities. +- RegisterAuthority - Used to register an Authority instance as active. +- UnregisterAuthority - Used to remove an Authority from active use and dispose of it. +- GetAuthority - Safe method for retrieving an Authority by its Name, returns null if not found. + +Use of the AuthorityProvider to manage access to Authorities is recommended when handling incoming GeoPose data to ensure quick and safe access. + +> For more detailed examples of authority registration and use, check the [Ethar GeoPose documentation](https://etharinc.github.io/Ethar.GeoPose.Docs). + +## Contributing + +Ethar.GeoPose is made possible by the excellent work of the Ethar Team: + + + + + + + + + + + +
The Masked Coder??????
Simon JacksonGitHub/SimonDarksideJLinkedIn/xrconsultant
Connor DavisGitHub/john-connor-davisLinkedIn/John-Connor-Davis
Colin SteinmannGitHub/metaColinLinkedIn/colinsteinmann
+ +## Terms of Use + +For full terms of use please refer our [documentation site](https://etharinc.github.io/Ethar.GeoPose.Docs/termsofuse.html). + +## License + +Ethar.GeoPose is distributed under [Apache 2.0 License](https://etharinc.github.io/Ethar.GeoPose.Docs/license.html) diff --git a/Ethar.GeoPose.Authority/stylecop.json b/Ethar.GeoPose.Authority/stylecop.json new file mode 100644 index 0000000..a3b11d2 --- /dev/null +++ b/Ethar.GeoPose.Authority/stylecop.json @@ -0,0 +1,14 @@ +{ + // ACTION REQUIRED: This file was automatically added to your project, but it + // will not take effect until additional steps are taken to enable it. See the + // following page for additional information: + // + // https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/EnableConfiguration.md + + "$schema": "https://raw.githubusercontent.com/DotNetAnalyzers/StyleCopAnalyzers/master/StyleCop.Analyzers/StyleCop.Analyzers/Settings/stylecop.schema.json", + "settings": { + "documentationRules": { + "companyName": "Ethar" + } + } +} diff --git a/Ethar.GeoPose.Examples/Ethar.GeoPose.Examples.csproj b/Ethar.GeoPose.Examples/Ethar.GeoPose.Examples.csproj new file mode 100644 index 0000000..8c4bc41 --- /dev/null +++ b/Ethar.GeoPose.Examples/Ethar.GeoPose.Examples.csproj @@ -0,0 +1,15 @@ + + + + Exe + net6.0 + enable + disable + + + + + + + + diff --git a/Ethar.GeoPose.Examples/Example_AuthorityImplementation.cs b/Ethar.GeoPose.Examples/Example_AuthorityImplementation.cs new file mode 100644 index 0000000..a9629b4 --- /dev/null +++ b/Ethar.GeoPose.Examples/Example_AuthorityImplementation.cs @@ -0,0 +1,273 @@ +using System; +using Ethar.GeoPose.Authority; +using Ethar.GeoPose.DataTypes; +using Ethar.GeoPose.Exceptions; +using Ethar.GeoPose.FrameSpecifications; +using Ethar.GeoPose.Interfaces; +using Ethar.GeoPose.StructuralDataUnits; +using Ethar.GeoPose.TransitionModels; +using Ethar.GeoPose.Validation; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace Ethar.GeoPose.Examples +{ + /// + /// This example shows how to implement the various classes required for a developer to implement their own custom frame specifications managed by a custom Authority. + /// + public class Example_AuthorityImplementation + { + public static string SerializeExample() + { + var auth = new ExampleAuthority(); + + // We have to register the authority for serial/deserialization to work for custom frame specifications. + AuthorityProvider.RegisterAuthority(auth); + + var sdu = new AdvancedSdu(1, + new UnitQuaternion(1, 2, 1, 2), + new ExampleExtrinsicFrameSpec()); + + // Now that the authority is implemented and registered we can serialize/deserialize custom frame specs. + return JsonConvert.SerializeObject(sdu); + } + + public static AdvancedSdu DeserializeExample() + { + var auth = new ExampleAuthority(); + + // We have to register the authority for serial/deserialization to work for custom frame specifications. + AuthorityProvider.RegisterAuthority(auth); + + var json = + "{" + + "\"frameSpecification\":" + + "{" + + $"\"authority\": \"{Constants.AuthorityName}\"," + + $"\"id\": \"ExampleExtrinsicFrameSpec\"," + + $"\"parameters\": \"latitude=1&longitude=2\"" + + "}," + + "\"quaternion\":" + + "{" + + $"\"x\": 1," + + $"\"y\": 0," + + $"\"z\": 0," + + $"\"w\": 1" + + "}," + + $"\"validTime\": 1" + + "}"; + + // Now that the authority is implemented and registered we can serialize/deserialize custom frame specs. + return JsonConvert.DeserializeObject(json); + } + } + + /// + /// This class is an example implementation of an authority. An authority is a GeoPose concept wherein individuals/groups can implement their + /// own frame specification definitions. An implementor may want to implement their own authority if the basic SDUs do not satisfy the + /// implementors geospatial data requirements. + /// + internal class ExampleAuthority : IAuthority + { + private IExplicitFrameSpecificationValidator validator = new ExampleAuthorityFrameSpecificationValidator(); + + public string AuthorityName => Constants.AuthorityName; + + public JObject ConvertFrameSpecToJson(IFrameSpecification frameSpec) + { + switch (frameSpec) + { + case ExampleExtrinsicFrameSpec extrinsic: + return ExampleFrameSpecificationConverter.ConvertExampleExtrinsicSpecToJObject(extrinsic); + case ExampleIntrinsicFrameSpec intrinsic: + return ExampleFrameSpecificationConverter.ConvertExampleIntrinsicSpecToJObject(intrinsic); ; + default: + throw new NotImplementedException($"The frame specification does not exist in the authority {AuthorityName}"); + } + } + + public IFrameSpecification ConvertJsonToFrameSpec(JObject jsonObject) + { + var id = (string)jsonObject["id"]; + if (string.IsNullOrEmpty(id)) + { + throw new NotImplementedException(); + } + + IFrameSpecification frameSpec; + FrameSpecificationValidationResult validationResult; + + switch (id) + { + // Implementors could add optional validation here to ensure that an invalid frame specification does not get created. + // This is not required but an example is shown below. + case "ExampleExtrinsicFrameSpec": + frameSpec = ExampleFrameSpecificationConverter.ConvertJObjectToExampleExtrinsicFrameSpec(jsonObject); + validationResult = this.validator.Validate(frameSpec); + return validationResult.IsValid ? frameSpec : throw new FrameSpecificationInvalidException(validationResult.Message); + case "ExampleIntrinsicFrameSpec": + frameSpec = ExampleFrameSpecificationConverter.ConvertJObjectToExampleIntrinsicFrameSpec(jsonObject); + validationResult = this.validator.Validate(frameSpec); + return validationResult.IsValid ? frameSpec : throw new FrameSpecificationInvalidException(validationResult.Message); + default: + throw new NotImplementedException(); + } + } + + /// + /// This example does not have transition models, though the conversion could be implemented in the same way as the frame specifications. + /// > + public TransitionModel ConvertJsonToTransitionModel(JObject jsonObject) + { + throw new NotImplementedException(); + } + + public JObject ConvertTransitionModelToJson(TransitionModel transitionModel) + { + throw new NotImplementedException(); + } + + public bool IsFrameSpecificationExtrinsic(IFrameSpecification frameSpec) + { + switch (frameSpec) + { + case ExampleExtrinsicFrameSpec _: + return true; + case ExampleIntrinsicFrameSpec _: + return false; + default: + throw new NotImplementedException("Frame specification does not exist in the authority."); + } + } + } + + /// + /// This class represents an example frame specification that has a latitude and longitude. This is different than one of the basic SDUs because + /// it has no rotation. This specification is extrinsic because it is referenced from the earth rather than from another frame specification. + /// + /// + /// { + /// id = "ExampleExtrinsicFrameSpec", + /// authority = "ExampleAuthority/1.0", + /// parameters = "latitude=1&longitude=2" + /// } + /// + internal class ExampleExtrinsicFrameSpec : BaseFrameSpecification + { + public ExampleExtrinsicFrameSpec() + : base("ExampleExtrinsicFrameSpecification", Constants.AuthorityName) + { + } + + public float Latitude { get; set; } + public float Longitude { get; set; } + } + + /// + /// This class represents an example frame specification that has a translation relative to another frame specification in meters + /// with the x value representing east, the y value representing north, and z representing up. This type of frame specification could + /// be useful if you have an object that should always be positioned relative to another. + /// + /// + /// { + /// id = "ExampleIntrinsicFrameSpec", + /// authority = "ExampleAuthority/1.0", + /// parameters = "translation.x=1&translation.y=2&translation.z=3" + /// } + /// + internal class ExampleIntrinsicFrameSpec : BaseFrameSpecification + { + public ExampleIntrinsicFrameSpec() + : base("ExampleIntrinsicFrameSpecification", Constants.AuthorityName) + { + } + + public UnitVector3 Translation { get; set; } + } + + /// + /// This class converts the frame specifications that this authority implements to and from json. It is not required to use this + /// pattern for your conversion, this is just one of many ways that it can be implemented. + /// + internal class ExampleFrameSpecificationConverter + { + public static ExampleExtrinsicFrameSpec ConvertJObjectToExampleExtrinsicFrameSpec(JObject jObject) + { + if (ValidationUtilities.ValidateJsonObjectParameters(jObject, out var queryString)) + { + var lat = float.Parse(queryString.GetParameter("latitude")); + var lon = float.Parse(queryString.GetParameter("longitude")); + + return new ExampleExtrinsicFrameSpec { Latitude = lat, Longitude = lon }; + } + + return null; + } + + public static JObject ConvertExampleExtrinsicSpecToJObject(ExampleExtrinsicFrameSpec spec) + { + var paramString = $"latitude={spec.Latitude}&longitude={spec.Longitude}"; + + var jObj = new JObject + { + { "authority", spec.Authority }, + { "id", spec.Id }, + { "parameters", paramString }, + }; + + return jObj; + } + + public static ExampleIntrinsicFrameSpec ConvertJObjectToExampleIntrinsicFrameSpec(JObject jObject) + { + if (ValidationUtilities.ValidateJsonObjectParameters(jObject, out var queryString)) + { + var x = float.Parse(queryString.GetParameter("translation.x")); + var y = float.Parse(queryString.GetParameter("translation.y")); + var z = float.Parse(queryString.GetParameter("translation.z")); + var vector = new UnitVector3(x, y, z); + + return new ExampleIntrinsicFrameSpec { Translation = vector }; + } + + return null; + } + + public static JObject ConvertExampleIntrinsicSpecToJObject(ExampleIntrinsicFrameSpec spec) + { + var paramString = $"translation.x={spec.Translation.X}&translation.y={spec.Translation.Y}&translation.z={spec.Translation.Z}"; + + var jObj = new JObject + { + { "authority", spec.Authority }, + { "id", spec.Id }, + { "parameters", paramString }, + }; + + return jObj; + } + } + + /// + /// This example shows a basic validator. This class is not required to implement and is given as a reference in case implementors desire validation for frame specifications. + /// + internal class ExampleAuthorityFrameSpecificationValidator : IExplicitFrameSpecificationValidator + { + public FrameSpecificationValidationResult Validate(IFrameSpecification frame) + { + switch (frame) + { + case ExampleExtrinsicFrameSpec extrinsic: + if (extrinsic.Latitude > 90 || extrinsic.Latitude < -90) return new FrameSpecificationValidationResult(false, "Latitude must be between -90 and 90"); + return FrameSpecificationValidationResult.Valid; + default: + return FrameSpecificationValidationResult.Valid; + } + } + } + + internal static class Constants + { + public static string AuthorityName => "ExampleAuthority/1.0"; + } +} diff --git a/Ethar.GeoPose.Examples/Example_BasicSerialization.cs b/Ethar.GeoPose.Examples/Example_BasicSerialization.cs new file mode 100644 index 0000000..011e513 --- /dev/null +++ b/Ethar.GeoPose.Examples/Example_BasicSerialization.cs @@ -0,0 +1,43 @@ +using System.Net.Http; +using System.Text; +using System.Threading.Tasks; +using Ethar.GeoPose.DataTypes; +using Ethar.GeoPose.StructuralDataUnits; +using Newtonsoft.Json; + +namespace Ethar.GeoPose.Examples +{ + /// + /// This example shows how to serialize and deserialize a basic SDU. + /// + internal class Example_BasicSerialization + { + internal static async Task SerialiseGeoPose() + { + var sdu = new BasicYawPitchRollSdu(new YawPitchRollAngles(1f, 2f, 3f), + new TangentPointPosition(1f, 2f, 3f)); + + var converted = JsonConvert.SerializeObject(sdu); + var content = new StringContent(converted, Encoding.UTF8, "application/json"); + + var client = new HttpClient(); + + // Replace the endpoint with the endpoint you want to send the data to. + var response = await client.PostAsync("YourEndpoint", content); + } + + internal static async Task DeSerialiseGeoPose() + { + var client = new HttpClient(); + + var lat = 1f; + var lon = 2f; + var height = 3f; + + var response = await client.GetAsync($"https://service.geopose.io/solar/solarpose/Ypr?longitude={lon}&latitude={lat}&height={height}"); + var json = await response.Content.ReadAsStringAsync(); + var sdu = JsonConvert.DeserializeObject(json); + return sdu; + } + } +} \ No newline at end of file diff --git a/Ethar.GeoPose.Examples/Program.cs b/Ethar.GeoPose.Examples/Program.cs new file mode 100644 index 0000000..ec17ed0 --- /dev/null +++ b/Ethar.GeoPose.Examples/Program.cs @@ -0,0 +1,29 @@ +// Runtime execution of the GeoPose examples +// +// 1. Basic GeoPose data deserialization from Solarpose data source. +// 2. Serialzing GeoPose SDU through an Authority for posting. +// 3. Deserializing GeoPose JSON and constucting an SDU. +// +// See https://etharinc.github.io/Ethar.GeoPose.Docs/ for more information + +using Ethar.GeoPose.Examples; + +Console.WriteLine("-----Start-----"); + +Console.WriteLine("Getting GeoPose data and deserializing"); +var result = await Example_BasicSerialization.DeSerialiseGeoPose(); +Console.WriteLine($"Geopose data - position {result.Position} - angles {result.Angles}"); + +Console.WriteLine("----------"); + +Console.WriteLine("Serializing SDU"); +var jsonResult = Example_AuthorityImplementation.SerializeExample(); +Console.WriteLine($"Serialised Data {jsonResult}"); + +Console.WriteLine("----------"); + +Console.WriteLine("Deserializing SDU"); +var sduResult = Example_AuthorityImplementation.DeserializeExample(); +Console.WriteLine($"Deserialized Data {sduResult}"); + +Console.WriteLine("-----End-----"); diff --git a/Ethar.GeoPose.UnitTests/AdvancedSduTests.cs b/Ethar.GeoPose.UnitTests/AdvancedSduTests.cs new file mode 100644 index 0000000..286ce3b --- /dev/null +++ b/Ethar.GeoPose.UnitTests/AdvancedSduTests.cs @@ -0,0 +1,253 @@ +using Ethar.GeoPose.Authority.FrameSpecifications; +using Ethar.GeoPose.StructuralDataUnits; +using Newtonsoft.Json; +using NUnit.Framework; + +namespace Ethar.GeoPose.UnitTests +{ + internal class AdvancedSduTests : UnitTestBase + { + [Test] + [TestCase(16534234327, "\"/Ethar.GeoPose/1.0\"", "\"LTP-ENU\"", "\"longitude=-122.0000000&latitude=48.0000000&heightInMeters=5.000\"", 0.207f, 0.218f, 0.655f, -0.692f)] + public void CanCorrectlyDeserializeAdvancedSduWithLtpEnuSpecification(long validTime, string authority, string id, string parameters, float x, float y, float z, float w) + { + var json = + "{" + + "\"frameSpecification\":" + + "{" + + $"\"authority\": {authority}," + + $"\"id\": {id}," + + $"\"parameters\": {parameters}" + + "}," + + "\"quaternion\":" + + "{" + + $"\"x\": {x}," + + $"\"y\": {y}," + + $"\"z\": {z}," + + $"\"w\": {w}" + + "}," + + $"\"validTime\": {validTime}" + + "}"; + + var sdu = JsonConvert.DeserializeObject(json); + Assert.That(sdu.ValidTime, Is.EqualTo(validTime)); + Assert.That(sdu.Quaternion.X, Is.EqualTo(x)); + Assert.That(sdu.Quaternion.Y, Is.EqualTo(y)); + Assert.That(sdu.Quaternion.Z, Is.EqualTo(z)); + Assert.That(sdu.Quaternion.W, Is.EqualTo(w)); + + Assert.That(sdu.FrameSpecification is LtpEnuSpecification); + + var ltpEnuSpec = sdu.FrameSpecification as LtpEnuSpecification; + Assert.That(ltpEnuSpec.Position.Latitude, Is.EqualTo(48)); + Assert.That(ltpEnuSpec.Position.Longitude, Is.EqualTo(-122)); + Assert.That(ltpEnuSpec.Position.HeightInMeters, Is.EqualTo(5)); + } + + [Test] + [TestCase(16534234327, "\"/Ethar.GeoPose/1.0\"", "\"LTP-NED\"", "\"longitude=-122.0000000&latitude=48.0000000&heightInMeters=5.000\"", 0.207f, 0.218f, 0.655f, -0.692f)] + public void CanCorrectlyDeserializeAdvancedSduWithLtpNedSpecification(long validTime, string authority, string id, string parameters, float x, float y, float z, float w) + { + var json = + "{" + + "\"frameSpecification\":" + + "{" + + $"\"authority\": {authority}," + + $"\"id\": {id}," + + $"\"parameters\": {parameters}" + + "}," + + "\"quaternion\":" + + "{" + + $"\"x\": {x}," + + $"\"y\": {y}," + + $"\"z\": {z}," + + $"\"w\": {w}" + + "}," + + $"\"validTime\": {validTime}" + + "}"; + + var sdu = JsonConvert.DeserializeObject(json); + Assert.That(sdu.ValidTime, Is.EqualTo(validTime)); + Assert.That(sdu.Quaternion.X, Is.EqualTo(x)); + Assert.That(sdu.Quaternion.Y, Is.EqualTo(y)); + Assert.That(sdu.Quaternion.Z, Is.EqualTo(z)); + Assert.That(sdu.Quaternion.W, Is.EqualTo(w)); + + Assert.That(sdu.FrameSpecification is LtpNedSpecification); + + var ltpEnuSpec = sdu.FrameSpecification as LtpNedSpecification; + Assert.That(ltpEnuSpec.Position.Latitude, Is.EqualTo(48)); + Assert.That(ltpEnuSpec.Position.Longitude, Is.EqualTo(-122)); + Assert.That(ltpEnuSpec.Position.HeightInMeters, Is.EqualTo(5)); + } + + [Test] + [TestCase(16534234327, "\"/Ethar.GeoPose/1.0\"", "\"YPR-LTP-ENU\"", "\"longitude=-122.0000000&latitude=48.0000000&heightInMeters=5.000&orientation.yaw=10&orientation.pitch=11&orientation.roll=12\"", 0.207f, 0.218f, 0.655f, -0.692f)] + public void CanCorrectlyDeserializeAdvancedSduWithYprOrientedLtpEnuSpecification(long validTime, string authority, string id, string parameters, float x, float y, float z, float w) + { + var json = + "{" + + "\"frameSpecification\":" + + "{" + + $"\"authority\": {authority}," + + $"\"id\": {id}," + + $"\"parameters\": {parameters}" + + "}," + + "\"quaternion\":" + + "{" + + $"\"x\": {x}," + + $"\"y\": {y}," + + $"\"z\": {z}," + + $"\"w\": {w}" + + "}," + + $"\"validTime\": {validTime}" + + "}"; + + var sdu = JsonConvert.DeserializeObject(json); + Assert.That(sdu.ValidTime, Is.EqualTo(validTime)); + Assert.That(sdu.Quaternion.X, Is.EqualTo(x)); + Assert.That(sdu.Quaternion.Y, Is.EqualTo(y)); + Assert.That(sdu.Quaternion.Z, Is.EqualTo(z)); + Assert.That(sdu.Quaternion.W, Is.EqualTo(w)); + + Assert.That(sdu.FrameSpecification is YawPitchRollOrientedLtpEnuSpecification); + + var ltpEnuSpec = sdu.FrameSpecification as YawPitchRollOrientedLtpEnuSpecification; + Assert.That(ltpEnuSpec.Position.Latitude, Is.EqualTo(48)); + Assert.That(ltpEnuSpec.Position.Longitude, Is.EqualTo(-122)); + Assert.That(ltpEnuSpec.Position.HeightInMeters, Is.EqualTo(5)); + Assert.That(ltpEnuSpec.Orientation.Yaw, Is.EqualTo(10)); + Assert.That(ltpEnuSpec.Orientation.Pitch, Is.EqualTo(11)); + Assert.That(ltpEnuSpec.Orientation.Roll, Is.EqualTo(12)); + } + + [Test] + [TestCase(16534234327, "\"/Ethar.GeoPose/1.0\"", "\"Quaternion-LTP-ENU\"", "\"longitude=-122.0000000&latitude=48.0000000&heightInMeters=5.000&orientation.x=0.5&orientation.y=0.25&orientation.z=0.25&orientation.w=-0.5\"", 0.207f, 0.218f, 0.655f, -0.692f)] + public void CanCorrectlyDeserializeAdvancedSduWithQuaternionOrientedLtpEnuSpecification(long validTime, string authority, string id, string parameters, float x, float y, float z, float w) + { + var json = + "{" + + "\"frameSpecification\":" + + "{" + + $"\"authority\": {authority}," + + $"\"id\": {id}," + + $"\"parameters\": {parameters}" + + "}," + + "\"quaternion\":" + + "{" + + $"\"x\": {x}," + + $"\"y\": {y}," + + $"\"z\": {z}," + + $"\"w\": {w}" + + "}," + + $"\"validTime\": {validTime}" + + "}"; + + var sdu = JsonConvert.DeserializeObject(json); + Assert.That(sdu.ValidTime, Is.EqualTo(validTime)); + Assert.That(sdu.Quaternion.X, Is.EqualTo(x)); + Assert.That(sdu.Quaternion.Y, Is.EqualTo(y)); + Assert.That(sdu.Quaternion.Z, Is.EqualTo(z)); + Assert.That(sdu.Quaternion.W, Is.EqualTo(w)); + + Assert.That(sdu.FrameSpecification is QuaternionOrientedLtpEnuSpecification); + + var ltpEnuSpec = sdu.FrameSpecification as QuaternionOrientedLtpEnuSpecification; + Assert.That(ltpEnuSpec.Position.Latitude, Is.EqualTo(48)); + Assert.That(ltpEnuSpec.Position.Longitude, Is.EqualTo(-122)); + Assert.That(ltpEnuSpec.Position.HeightInMeters, Is.EqualTo(5)); + Assert.That(ltpEnuSpec.Orientation.X, Is.EqualTo(0.5)); + Assert.That(ltpEnuSpec.Orientation.Y, Is.EqualTo(0.25)); + Assert.That(ltpEnuSpec.Orientation.Z, Is.EqualTo(0.25)); + Assert.That(ltpEnuSpec.Orientation.W, Is.EqualTo(-0.5)); + } + + [Test] + [TestCase(16534234327, "\"/Ethar.GeoPose/1.0\"", "\"Translate-Rotate\"", "\"translation.x=0.1&translation.y=0.2&translation.z=0.3&rotation.x=0.692&rotation.y=0.691&rotation.z=0.141&rotation.w=0.14\"", 0.207f, 0.218f, 0.655f, -0.692f)] + public void CanCorrectlyDeserializeAdvancedSduWithTranslateRotateSpecification(long validTime, string authority, string id, string parameters, float x, float y, float z, float w) + { + var json = + "{" + + "\"frameSpecification\":" + + "{" + + $"\"authority\": {authority}," + + $"\"id\": {id}," + + $"\"parameters\": {parameters}" + + "}," + + "\"quaternion\":" + + "{" + + $"\"x\": {x}," + + $"\"y\": {y}," + + $"\"z\": {z}," + + $"\"w\": {w}" + + "}," + + $"\"validTime\": {validTime}" + + "}"; + + var sdu = JsonConvert.DeserializeObject(json); + Assert.That(sdu.ValidTime, Is.EqualTo(validTime)); + Assert.That(sdu.Quaternion.X, Is.EqualTo(x)); + Assert.That(sdu.Quaternion.Y, Is.EqualTo(y)); + Assert.That(sdu.Quaternion.Z, Is.EqualTo(z)); + Assert.That(sdu.Quaternion.W, Is.EqualTo(w)); + + Assert.That(sdu.FrameSpecification is TranslateRotateSpecification); + + var translateRotateSpec = sdu.FrameSpecification as TranslateRotateSpecification; + Assert.That(translateRotateSpec.Translation.X, Is.EqualTo(0.1f)); + Assert.That(translateRotateSpec.Translation.Y, Is.EqualTo(0.2f)); + Assert.That(translateRotateSpec.Translation.Z, Is.EqualTo(0.3f)); + Assert.That(translateRotateSpec.Rotation.X, Is.EqualTo(0.692f)); + Assert.That(translateRotateSpec.Rotation.Y, Is.EqualTo(0.691f)); + Assert.That(translateRotateSpec.Rotation.Z, Is.EqualTo(0.141f)); + Assert.That(translateRotateSpec.Rotation.W, Is.EqualTo(0.14f)); + } + + [Test] + [TestCase(16534234327, "\"/Ethar.GeoPose/1.0\"", "\"Translate-Rotate\"", "\"translation.x=0.1&translation.y=0.2&translation.z=0.3&rotation.x=0.692&rotation.y=0.691&rotation.z=0.141&rotation.w=0.14\"", 0.207f, 0.218f, 0.655f, -0.692f)] + public void CanConvertAdvancedSDUToString(long validTime, string authority, string id, string parameters, float x, float y, float z, float w) + { + var json = + "{" + + "\"frameSpecification\":" + + "{" + + $"\"authority\": {authority}," + + $"\"id\": {id}," + + $"\"parameters\": {parameters}" + + "}," + + "\"quaternion\":" + + "{" + + $"\"x\": {x}," + + $"\"y\": {y}," + + $"\"z\": {z}," + + $"\"w\": {w}" + + "}," + + $"\"validTime\": {validTime}" + + "}"; + + var sdu = JsonConvert.DeserializeObject(json); + var result = sdu.ToString(); + Assert.That(result, !Is.Empty); + Assert.That(result, Is.EqualTo("ValidTime:16534234327, Quaternion:[X:0.207, Y:0.218, Z:0.655, W:-0.692], FrameSpecification:[Authority:/Ethar.GeoPose/1.0, Id:Translate-Rotate]")); + } + + [Test] + [TestCase(16534234327, 0.207f, 0.218f, 0.655f, -0.692f)] + public void CanConvertAdvancedSDUToStringWithNoFrameSpecification(long validTime, float x, float y, float z, float w) + { + var sdu = new AdvancedSdu(validTime,new DataTypes.UnitQuaternion(x, y, z, w),null); + var result = sdu.ToString(); + Assert.That(result, !Is.Empty); + Assert.That(result, Is.EqualTo("ValidTime:16534234327, Quaternion:[X:0.207, Y:0.218, Z:0.655, W:-0.692], FrameSpecification:[]")); + } + + [Test] + public void CanConvertAdvancedSDUToStringNew() + { + var sdu = new AdvancedSdu(); + var result = sdu.ToString(); + Assert.That(result, !Is.Empty); + Assert.That(result, Is.EqualTo("ValidTime:0, Quaternion:[X:0, Y:0, Z:0, W:0], FrameSpecification:[]")); + } + } +} diff --git a/Ethar.GeoPose.UnitTests/ApiEndpoints.cs b/Ethar.GeoPose.UnitTests/ApiEndpoints.cs new file mode 100644 index 0000000..ad59129 --- /dev/null +++ b/Ethar.GeoPose.UnitTests/ApiEndpoints.cs @@ -0,0 +1,8 @@ +namespace Ethar.GeoPose.UnitTests +{ + public static class ApiEndpoints + { + public const string BaseUrl = "https://service.geopose.io/solar"; + public const string BasicYprGet = "https://service.geopose.io/solar/solarpose/YPR"; + } +} \ No newline at end of file diff --git a/Ethar.GeoPose.UnitTests/AuthorityProviderUnitTests.cs b/Ethar.GeoPose.UnitTests/AuthorityProviderUnitTests.cs new file mode 100644 index 0000000..d3fcc07 --- /dev/null +++ b/Ethar.GeoPose.UnitTests/AuthorityProviderUnitTests.cs @@ -0,0 +1,68 @@ +#if !UNITY_EDITOR +using Ethar.GeoPose.Authority; +using Moq; +using NUnit.Framework; + +namespace Ethar.GeoPose.UnitTests +{ + [TestFixture] + internal class AuthorityProviderUnitTests + { + [Test] + public void CanProperlyRegisterAuthorities() + { + var auth1 = new Mock(); + auth1.Setup(x => x.AuthorityName).Returns("test 1"); + + var auth2 = new Mock(); + auth2.Setup(x => x.AuthorityName).Returns("test 2"); + + var auth3 = new Mock(); + auth3.Setup(x => x.AuthorityName).Returns("test 3"); + + AuthorityProvider.RegisterAuthority(auth1.Object); + AuthorityProvider.RegisterAuthority(auth2.Object); + AuthorityProvider.RegisterAuthority(auth3.Object); + + Assert.That(AuthorityProvider.Authorities.Count, Is.EqualTo(3)); + Assert.That(AuthorityProvider.Authorities.Any(a => string.Equals(a.AuthorityName, "test 1"))); + Assert.That(AuthorityProvider.Authorities.Any(a => string.Equals(a.AuthorityName, "test 2"))); + Assert.That(AuthorityProvider.Authorities.Any(a => string.Equals(a.AuthorityName, "test 3"))); + } + + [Test] + public void CanProperlyUnregisterAuthorities() + { + var auth1 = new Mock(); + auth1.Setup(x => x.AuthorityName).Returns("test 1"); + + var auth2 = new Mock(); + auth2.Setup(x => x.AuthorityName).Returns("test 2"); + + var auth3 = new Mock(); + auth3.Setup(x => x.AuthorityName).Returns("test 3"); + + AuthorityProvider.RegisterAuthority(auth1.Object); + AuthorityProvider.RegisterAuthority(auth2.Object); + AuthorityProvider.RegisterAuthority(auth3.Object); + + Assert.That(AuthorityProvider.Authorities.Count, Is.EqualTo(3)); + + AuthorityProvider.UnregisterAuthority("test 1"); + + Assert.That(AuthorityProvider.Authorities.Count, Is.EqualTo(2)); + Assert.That(AuthorityProvider.Authorities.Any(a => string.Equals(a.AuthorityName, "test 2"))); + Assert.That(AuthorityProvider.Authorities.Any(a => string.Equals(a.AuthorityName, "test 3"))); + + AuthorityProvider.UnregisterAuthority("test 2"); + + Assert.That(AuthorityProvider.Authorities.Count, Is.EqualTo(1)); + Assert.That(AuthorityProvider.Authorities.Any(a => string.Equals(a.AuthorityName, "test 3"))); + + AuthorityProvider.UnregisterAuthority("test 3"); + + Assert.That(AuthorityProvider.Authorities.Count, Is.EqualTo(0)); + } + } +} +#endif \ No newline at end of file diff --git a/Ethar.GeoPose.UnitTests/BasicQuaternionSduTests.cs b/Ethar.GeoPose.UnitTests/BasicQuaternionSduTests.cs new file mode 100644 index 0000000..db0f384 --- /dev/null +++ b/Ethar.GeoPose.UnitTests/BasicQuaternionSduTests.cs @@ -0,0 +1,96 @@ +using Ethar.GeoPose.StructuralDataUnits; +using Newtonsoft.Json; +using NUnit.Framework; + +namespace Ethar.GeoPose.UnitTests +{ + internal class BasicQuaternionSduTests : UnitTestBase + { + [TestCase(45, 45, 5, 0.207f, 0.218f, 0.655f, -0.692f)] + public void CanCorrectlyDeserializeBasicQuaternionSdu(float lat, float lon, float h, float x, float y, float z, float w) + { + var json = + "{" + + "\"position\":" + + "{" + + $"\"lat\": {lat}," + + $"\"lon\": {lon}," + + $"\"h\": {h}" + + "}," + + "\"quaternion\":" + + "{" + + $"\"x\": {x}," + + $"\"y\": {y}," + + $"\"z\": {z}," + + $"\"w\": {w}" + + "}" + + "}"; + + var sdu = JsonConvert.DeserializeObject(json); + Assert.That(sdu.Position.Longitude, Is.EqualTo(lon)); + Assert.That(sdu.Position.Latitude, Is.EqualTo(lat)); + Assert.That(sdu.Position.HeightInMeters, Is.EqualTo(h)); + Assert.That(sdu.Quaternion.X, Is.EqualTo(x)); + Assert.That(sdu.Quaternion.Y, Is.EqualTo(y)); + Assert.That(sdu.Quaternion.Z, Is.EqualTo(z)); + Assert.That(sdu.Quaternion.W, Is.EqualTo(w)); + } + + [TestCase(45, 45, 5, 0.207f, 0.218f, 0.655f, -0.692f)] + public void CanCorrectlySerializeBasicQuaternionSdu(float lat, float lon, float h, float x, float y, float z, float w) + { + var sdu = new BasicQuaternionSdu(new DataTypes.TangentPointPosition() { HeightInMeters = h, Latitude = lat, Longitude = lon }, new DataTypes.UnitQuaternion() { X = x, Y = y, Z = z, W = w }); + + var json = JsonConvert.SerializeObject(sdu); + + var expected = + "{" + + "\"position\":" + + "{" + + $"\"lat\":{lat}.0," + + $"\"lon\":{lon}.0," + + $"\"h\":{h}.0" + + "}," + + "\"quaternion\":" + + "{" + + $"\"x\":{x}," + + $"\"y\":{y}," + + $"\"z\":{z}," + + $"\"w\":{w}" + + "}" + + "}"; + + Assert.That(json, Is.EqualTo(expected)); + } + + [TestCase(45, 45, 5, 12, 16, 8)] + public void CanCorrectlyMakeARoundTripConversion(float lat, float lon, float h, float yaw, float pitch, float roll) + { + var sdu = new BasicYawPitchRollSdu(new DataTypes.YawPitchRollAngles() { Yaw = yaw, Pitch = pitch, Roll = roll }, new DataTypes.TangentPointPosition() { HeightInMeters = h, Latitude = lat, Longitude = lon }); + + var json = JsonConvert.SerializeObject(sdu); + + var converted = JsonConvert.DeserializeObject(json); + + Assert.That(sdu, Is.EqualTo(converted)); + } + + [TestCase(45, 45, 5, 0.207f, 0.218f, 0.655f, -0.692f)] + public void CanConvertBasicQuaternionSduToString(float lat, float lon, float h, float x, float y, float z, float w) + { + var sdu = new BasicQuaternionSdu(new DataTypes.TangentPointPosition() { HeightInMeters = h, Latitude = lat, Longitude = lon }, new DataTypes.UnitQuaternion() { X = x, Y = y, Z = z, W = w }); + var result = sdu.ToString(); + Assert.That(result, !Is.Empty); + Assert.That(result, Is.EqualTo("Position:[Latitude:45, Longitude:45, HeightInMeters:5], Quaternion:[X:0.207, Y:0.218, Z:0.655, W:-0.692]")); + } + + [Test] + public void CanConvertBasicQuaternionSduToStringNew() + { + var sdu = new BasicQuaternionSdu(); + var result = sdu.ToString(); + Assert.That(result, !Is.Empty); + Assert.That(result, Is.EqualTo("Position:[Latitude:0, Longitude:0, HeightInMeters:0], Quaternion:[X:0, Y:0, Z:0, W:0]")); + } + } +} diff --git a/Ethar.GeoPose.UnitTests/BasicYawPitchRollSduTests.cs b/Ethar.GeoPose.UnitTests/BasicYawPitchRollSduTests.cs new file mode 100644 index 0000000..3164cc1 --- /dev/null +++ b/Ethar.GeoPose.UnitTests/BasicYawPitchRollSduTests.cs @@ -0,0 +1,109 @@ +using Ethar.GeoPose.StructuralDataUnits; +using Newtonsoft.Json; +using NUnit.Framework; + +namespace Ethar.GeoPose.UnitTests +{ + [TestFixture] + internal class BasicYawPitchRollSduTests : UnitTestBase + { +#if !UNITY_EDITOR && APITEST + [TestCase(45, 45, 10)] + [TestCase(0, 90, 100)] + [TestCase(45, 45, 10.12f)] + [TestCase(-15, -45, -10)] + public async Task CanCorrectlyParseBasicYawPitchRollSdu(float lat, float lon, float height) + { + var sdu = await this.Client.ReadAsJsonAsync($"{ApiEndpoints.BasicYprGet}?longitude={lon}&latitude={lat}&heightInMeters={height}"); + Assert.That(sdu.Position.Longitude, Is.EqualTo(lon)); + Assert.That(sdu.Position.Latitude, Is.EqualTo(lat)); + Assert.That(sdu.Position.HeightInMeters, Is.EqualTo(height)); + } +#endif + + [TestCase(45f, 45f, 5f, 12, 16, 8)] + public void CanCorrectlyDeserializeBasicYawPitchRollSdu(float lat, float lon, float h, float yaw, float pitch, float roll) + { + var json = + "{" + + "\"position\":" + + "{" + + $"\"lat\": {lat}," + + $"\"lon\": {lon}," + + $"\"h\": {h}" + + "}," + + "\"angles\":" + + "{" + + $"\"yaw\": {yaw}," + + $"\"pitch\": {pitch}," + + $"\"roll\": {roll}" + + "}" + + "}"; + + var sdu = JsonConvert.DeserializeObject(json); + + Assert.That(sdu.Position.Longitude, Is.EqualTo(lon)); + Assert.That(sdu.Position.Latitude, Is.EqualTo(lat)); + Assert.That(sdu.Position.HeightInMeters, Is.EqualTo(h)); + Assert.That(sdu.Angles.Yaw, Is.EqualTo(yaw)); + Assert.That(sdu.Angles.Pitch, Is.EqualTo(pitch)); + Assert.That(sdu.Angles.Roll, Is.EqualTo(roll)); + } + + [TestCase(45.0f, 45.0f, 5, 12, 16, 8)] + public void CanCorrectlySerializeBasicYawPitchRollSdu(float lat, float lon, float h, float yaw, float pitch, float roll) + { + var sdu = new BasicYawPitchRollSdu(new DataTypes.YawPitchRollAngles() { Yaw = yaw, Pitch = pitch, Roll = roll }, new DataTypes.TangentPointPosition() { HeightInMeters = h, Latitude = lat, Longitude = lon }); + + var json = JsonConvert.SerializeObject(sdu); + + var expected = + "{" + + "\"position\":" + + "{" + + $"\"lat\":{lat}.0," + + $"\"lon\":{lon}.0," + + $"\"h\":{h}.0" + + "}," + + "\"angles\":" + + "{" + + $"\"yaw\":{yaw}.0," + + $"\"pitch\":{pitch}.0," + + $"\"roll\":{roll}.0" + + "}" + + "}"; + + Assert.That(json, Is.EqualTo(expected)); + } + + [TestCase(45, 45, 5, 12, 16, 8)] + public void CanCorrectlyMakeARoundTripConversion(float lat, float lon, float h, float yaw, float pitch, float roll) + { + var sdu = new BasicYawPitchRollSdu(new DataTypes.YawPitchRollAngles() { Yaw = yaw, Pitch = pitch, Roll = roll }, new DataTypes.TangentPointPosition() { HeightInMeters = h, Latitude = lat, Longitude = lon }); + + var json = JsonConvert.SerializeObject(sdu); + + var converted = JsonConvert.DeserializeObject(json); + + Assert.That(sdu, Is.EqualTo(converted)); + } + + [TestCase(45f, 45f, 5f, 12, 16, 8)] + public void CanConvertBasicYawPitchRollSduToString(float lat, float lon, float h, float yaw, float pitch, float roll) + { + var sdu = new BasicYawPitchRollSdu(new DataTypes.YawPitchRollAngles() { Yaw = yaw, Pitch = pitch, Roll = roll }, new DataTypes.TangentPointPosition() { HeightInMeters = h, Latitude = lat, Longitude = lon }); + var result = sdu.ToString(); + Assert.That(result, !Is.Empty); + Assert.That(result, Is.EqualTo("Position:[Latitude:45, Longitude:45, HeightInMeters:5], Angles:[Yaw:12, Pitch:16, Roll:8]")); + } + + [Test] + public void CanConvertBasicYawPitchRollSduToStringNew() + { + var sdu = new BasicYawPitchRollSdu(); + var result = sdu.ToString(); + Assert.That(result, !Is.Empty); + Assert.That(result, Is.EqualTo("Position:[Latitude:0, Longitude:0, HeightInMeters:0], Angles:[Yaw:0, Pitch:0, Roll:0]")); + } + } +} diff --git a/Ethar.GeoPose.UnitTests/ChainSduTests.cs b/Ethar.GeoPose.UnitTests/ChainSduTests.cs new file mode 100644 index 0000000..7564269 --- /dev/null +++ b/Ethar.GeoPose.UnitTests/ChainSduTests.cs @@ -0,0 +1,194 @@ +using System.Collections.Generic; +using System.Linq; +using Ethar.GeoPose.Authority.FrameSpecifications; +using Ethar.GeoPose.FrameSpecifications; +using Ethar.GeoPose.StructuralDataUnits; +using Newtonsoft.Json; +using NUnit.Framework; + +namespace Ethar.GeoPose.UnitTests +{ + [TestFixture] + public class ChainSduTests : UnitTestBase + { + [TestCase(16534234327, "\"/Ethar.GeoPose/1.0\"", "\"LTP-ENU\"", "\"longitude=-122.0000000&latitude=48.0000000&heightInMeters=5.000\"", "\"Translate-Rotate\"", + "\"translation.x=0.1&translation.y=0.2&translation.z=0.3&rotation.x=0.692&rotation.y=0.691&rotation.z=0.141&rotation.w=0.14\"", + "\"translation.x=0.4&translation.y=0.5&translation.z=0.6&rotation.x=0.692&rotation.y=0.691&rotation.z=0.141&rotation.w=0.14\"", + "\"translation.x=0.7&translation.y=0.8&translation.z=0.9&rotation.x=0.692&rotation.y=0.691&rotation.z=0.141&rotation.w=0.14\"")] + public void CanCorrectlyDeserializeChainSdu(long validTime, string authority, string outerFrameId, string outerFrameParams, string innerFrameId, string innerFrameParameters1, string innerFrameParameters2, string innerFrameParameters3) + { + var json = + "{" + + "\"frameChain\": [" + + "{" + + $"\"authority\": {authority}," + + $"\"id\": {innerFrameId}," + + $"\"parameters\": {innerFrameParameters1}" + + "}," + + "{" + + $"\"authority\": {authority}," + + $"\"id\": {innerFrameId}," + + $"\"parameters\": {innerFrameParameters2}" + + "}," + + "{" + + $"\"authority\": {authority}," + + $"\"id\": {innerFrameId}," + + $"\"parameters\": {innerFrameParameters3}" + + "}" + + "]," + + "\"outerFrame\":" + + "{" + + $"\"authority\": {authority}," + + $"\"id\": {outerFrameId}," + + $"\"parameters\": {outerFrameParams}" + + "}," + + $"\"validTime\": {validTime}" + + "}"; + + var sdu = JsonConvert.DeserializeObject(json); + Assert.That(sdu.ValidTime, Is.EqualTo(validTime)); + + Assert.That(sdu.OuterFrame is LtpEnuSpecification); + var outerFrame = (LtpEnuSpecification)sdu.OuterFrame; + + Assert.That(outerFrame.Position.Latitude, Is.EqualTo(48)); + Assert.That(outerFrame.Position.Longitude, Is.EqualTo(-122)); + Assert.That(outerFrame.Position.HeightInMeters, Is.EqualTo(5)); + + Assert.That(sdu.FrameChain.All(x => x is TranslateRotateSpecification)); + + var translateRotateList = sdu.FrameChain.Select(x => x as TranslateRotateSpecification).ToList(); + Assert.That(translateRotateList.Count, Is.EqualTo(3)); + + Assert.That(translateRotateList.ElementAt(0).Translation.X, Is.EqualTo(0.1f)); + Assert.That(translateRotateList.ElementAt(0).Translation.Y, Is.EqualTo(0.2f)); + Assert.That(translateRotateList.ElementAt(0).Translation.Z, Is.EqualTo(0.3f)); + Assert.That(translateRotateList.ElementAt(0).Rotation.X, Is.EqualTo(0.692f)); + Assert.That(translateRotateList.ElementAt(0).Rotation.Y, Is.EqualTo(0.691f)); + Assert.That(translateRotateList.ElementAt(0).Rotation.Z, Is.EqualTo(0.141f)); + Assert.That(translateRotateList.ElementAt(0).Rotation.W, Is.EqualTo(0.14f)); + + Assert.That(translateRotateList.ElementAt(1).Translation.X, Is.EqualTo(0.4f)); + Assert.That(translateRotateList.ElementAt(1).Translation.Y, Is.EqualTo(0.5f)); + Assert.That(translateRotateList.ElementAt(1).Translation.Z, Is.EqualTo(0.6f)); + Assert.That(translateRotateList.ElementAt(1).Rotation.X, Is.EqualTo(0.692f)); + Assert.That(translateRotateList.ElementAt(1).Rotation.Y, Is.EqualTo(0.691f)); + Assert.That(translateRotateList.ElementAt(1).Rotation.Z, Is.EqualTo(0.141f)); + Assert.That(translateRotateList.ElementAt(1).Rotation.W, Is.EqualTo(0.14f)); + + Assert.That(translateRotateList.ElementAt(2).Translation.X, Is.EqualTo(0.7f)); + Assert.That(translateRotateList.ElementAt(2).Translation.Y, Is.EqualTo(0.8f)); + Assert.That(translateRotateList.ElementAt(2).Translation.Z, Is.EqualTo(0.9f)); + Assert.That(translateRotateList.ElementAt(2).Rotation.X, Is.EqualTo(0.692f)); + Assert.That(translateRotateList.ElementAt(2).Rotation.Y, Is.EqualTo(0.691f)); + Assert.That(translateRotateList.ElementAt(2).Rotation.Z, Is.EqualTo(0.141f)); + Assert.That(translateRotateList.ElementAt(2).Rotation.W, Is.EqualTo(0.14f)); + } + + [TestCase(16534234327, "/Ethar.GeoPose/1.0", "LTP-ENU", "\"latitude=48&longitude=-122&heightInMeters=5\"", "Translate-Rotate", + "\"translation.x=0.1&translation.y=0.2&translation.z=0.3&rotation.x=0.692&rotation.y=0.691&rotation.z=0.141&rotation.w=0.14\"", + "\"translation.x=0.4&translation.y=0.5&translation.z=0.6&rotation.x=0.692&rotation.y=0.691&rotation.z=0.141&rotation.w=0.14\"", + "\"translation.x=0.7&translation.y=0.8&translation.z=0.9&rotation.x=0.692&rotation.y=0.691&rotation.z=0.141&rotation.w=0.14\"")] + public void CanCorrectlySerializeChainSdu(long validTime, string authority, string outerFrameId, string outerFrameParams, string innerFrameId, string innerFrameParameters1, string innerFrameParameters2, string innerFrameParameters3) + { + var outerFrame = new LtpEnuSpecification(new DataTypes.TangentPointPosition() { Latitude = 48, Longitude = -122, HeightInMeters = 5 }); + + var frameList = new List() + { + new TranslateRotateSpecification(new DataTypes.UnitVector3(0.1f, 0.2f, 0.3f), new DataTypes.UnitQuaternion() {X = 0.692f, Y = 0.691f, Z = 0.141f, W = 0.14f}), + new TranslateRotateSpecification(new DataTypes.UnitVector3(0.4f, 0.5f, 0.6f), new DataTypes.UnitQuaternion() {X = 0.692f, Y = 0.691f, Z = 0.141f, W = 0.14f}), + new TranslateRotateSpecification(new DataTypes.UnitVector3(0.7f, 0.8f, 0.9f), new DataTypes.UnitQuaternion() {X = 0.692f, Y = 0.691f, Z = 0.141f, W = 0.14f}) + }; + + var sdu = new ChainSdu(validTime, outerFrame, frameList); + + var expected = + "{" + + $"\"validTime\":{validTime}," + + "\"outerFrame\":" + + "{" + + $"\"authority\":\"{authority}\"," + + $"\"id\":\"{outerFrameId}\"," + + $"\"parameters\":{outerFrameParams}" + + "}," + + "\"frameChain\":[" + + "{" + + $"\"authority\":\"{authority}\"," + + $"\"id\":\"{innerFrameId}\"," + + $"\"parameters\":{innerFrameParameters1}" + + "}," + + "{" + + $"\"authority\":\"{authority}\"," + + $"\"id\":\"{innerFrameId}\"," + + $"\"parameters\":{innerFrameParameters2}" + + "}," + + "{" + + $"\"authority\":\"{authority}\"," + + $"\"id\":\"{innerFrameId}\"," + + $"\"parameters\":{innerFrameParameters3}" + + "}" + + "]" + + "}"; + + var json = JsonConvert.SerializeObject(sdu); + Assert.That(json, Is.EqualTo(expected)); + } + + [TestCase(16534234327)] + public void CanCorrectlyMakeRoundTripSerialization(long validTime) + { + var outerFrame = new LtpEnuSpecification(new DataTypes.TangentPointPosition() { Latitude = 48, Longitude = -122, HeightInMeters = 5 }); + + var frameList = new List() + { + new TranslateRotateSpecification(new DataTypes.UnitVector3(0.1f, 0.2f, 0.3f), new DataTypes.UnitQuaternion() {X = 0.692f, Y = 0.691f, Z = 0.141f, W = 0.14f}), + new TranslateRotateSpecification(new DataTypes.UnitVector3(0.4f, 0.5f, 0.6f), new DataTypes.UnitQuaternion() {X = 0.692f, Y = 0.691f, Z = 0.141f, W = 0.14f}), + new TranslateRotateSpecification(new DataTypes.UnitVector3(0.7f, 0.8f, 0.9f), new DataTypes.UnitQuaternion() {X = 0.692f, Y = 0.691f, Z = 0.141f, W = 0.14f}) + }; + + var sdu = new ChainSdu(validTime, outerFrame, frameList); + var json = JsonConvert.SerializeObject(sdu); + var converted = JsonConvert.DeserializeObject(json); + Assert.That(sdu, Is.EqualTo(converted)); + } + + [TestCase(16534234327, "\"/Ethar.GeoPose/1.0\"", "\"LTP-ENU\"", "\"longitude=-122.0000000&latitude=48.0000000&heightInMeters=5.000\"", "\"Translate-Rotate\"", + "\"translation.x=0.1&translation.y=0.2&translation.z=0.3&rotation.x=0.692&rotation.y=0.691&rotation.z=0.141&rotation.w=0.14\"", + "\"translation.x=0.4&translation.y=0.5&translation.z=0.6&rotation.x=0.692&rotation.y=0.691&rotation.z=0.141&rotation.w=0.14\"", + "\"translation.x=0.7&translation.y=0.8&translation.z=0.9&rotation.x=0.692&rotation.y=0.691&rotation.z=0.141&rotation.w=0.14\"")] + public void CanConvertChainSduToString(long validTime, string authority, string outerFrameId, string outerFrameParams, string innerFrameId, string innerFrameParameters1, string innerFrameParameters2, string innerFrameParameters3) + { + var outerFrame = new LtpEnuSpecification(new DataTypes.TangentPointPosition() { Latitude = 48, Longitude = -122, HeightInMeters = 5 }); + + var frameList = new List() + { + new TranslateRotateSpecification(new DataTypes.UnitVector3(0.1f, 0.2f, 0.3f), new DataTypes.UnitQuaternion() {X = 0.692f, Y = 0.691f, Z = 0.141f, W = 0.14f}), + new TranslateRotateSpecification(new DataTypes.UnitVector3(0.4f, 0.5f, 0.6f), new DataTypes.UnitQuaternion() {X = 0.692f, Y = 0.691f, Z = 0.141f, W = 0.14f}), + new TranslateRotateSpecification(new DataTypes.UnitVector3(0.7f, 0.8f, 0.9f), new DataTypes.UnitQuaternion() {X = 0.692f, Y = 0.691f, Z = 0.141f, W = 0.14f}) + }; + + var sdu = new ChainSdu(validTime, outerFrame, frameList); + var result = sdu.ToString(); + Assert.That(result, !Is.Empty); + Assert.That(result, Is.EqualTo("ValidTime:16534234327, OuterFrame:[Authority:/Ethar.GeoPose/1.0, Id:LTP-ENU], FrameChainCount:3")); + } + + [TestCase(16534234327)] + public void CanConvertChainSduToStringWithNull(long validTime) + { + var sdu = new ChainSdu(validTime, null, null); + var result = sdu.ToString(); + Assert.That(result, !Is.Empty); + Assert.That(result, Is.EqualTo("ValidTime:16534234327, OuterFrame:[], FrameChainCount:0")); + } + + [Test] + public void CanConvertChainSduToStringNew() + { + var sdu = new ChainSdu(); + var result = sdu.ToString(); + Assert.That(result, !Is.Empty); + Assert.That(result, Is.EqualTo("ValidTime:0, OuterFrame:[], FrameChainCount:0")); + } + } +} diff --git a/Ethar.GeoPose.UnitTests/Ethar.GeoPose.UnitTests.csproj b/Ethar.GeoPose.UnitTests/Ethar.GeoPose.UnitTests.csproj new file mode 100644 index 0000000..658bfff --- /dev/null +++ b/Ethar.GeoPose.UnitTests/Ethar.GeoPose.UnitTests.csproj @@ -0,0 +1,25 @@ + + + + net6.0 + enable + disable + + false + + + + + + + + + + + + + + + + + diff --git a/Ethar.GeoPose.UnitTests/FrameTransformIndexPairJsonConverterUnitTests.cs b/Ethar.GeoPose.UnitTests/FrameTransformIndexPairJsonConverterUnitTests.cs new file mode 100644 index 0000000..e883084 --- /dev/null +++ b/Ethar.GeoPose.UnitTests/FrameTransformIndexPairJsonConverterUnitTests.cs @@ -0,0 +1,78 @@ +using Ethar.GeoPose.DataTypes; +using Newtonsoft.Json; +using NUnit.Framework; + +namespace Ethar.GeoPose.UnitTests +{ + [TestFixture] + public class FrameTransformIndexPairJsonConverterUnitTests + { + [Test] + public void WriteJson_CorrectlyConvertsFrameTransformIndexPair() + { + var pair = new FrameTransformIndexPair() + { + OuterFrameIndex = 1, + InnerFrameIndex = 2 + }; + + var json = JsonConvert.SerializeObject(pair); + + var expected = "{" + + "\"link\":[" + + "1," + + "2" + + "]" + + "}"; + + Assert.That(json, Is.EqualTo(expected)); + } + + [Test] + public void ReadJson_CorrectlyConvertsFrameTransformIndexPair() + { + var json = "{" + + "\"link\":[" + + "1," + + "2" + + "]" + + "}"; + + var pair = JsonConvert.DeserializeObject(json); + + Assert.That(pair.OuterFrameIndex, Is.EqualTo(1)); + Assert.That(pair.InnerFrameIndex, Is.EqualTo(2)); + } + + [Test] + public void CanCorrectlyMakeARoundTripConversion() + { + var pair = new FrameTransformIndexPair() + { + OuterFrameIndex = 1, + InnerFrameIndex = 2 + }; + + var json = JsonConvert.SerializeObject(pair); + var converted = JsonConvert.DeserializeObject(json); + + Assert.That(converted, Is.EqualTo(pair)); + } + + [Test] + public void CanConvertFrameTransformIndexPairToString() + { + var json = "{" + + "\"link\":[" + + "1," + + "2" + + "]" + + "}"; + + var pair = JsonConvert.DeserializeObject(json); + var result = pair.ToString(); + Assert.That(result, !Is.Empty); + Assert.That(result, Is.EqualTo("OuterFrameIndex:1, InnerFrameIndex:2")); + } + } +} diff --git a/Ethar.GeoPose.UnitTests/GraphSduExtensionsUnitTests.cs b/Ethar.GeoPose.UnitTests/GraphSduExtensionsUnitTests.cs new file mode 100644 index 0000000..b1aabf2 --- /dev/null +++ b/Ethar.GeoPose.UnitTests/GraphSduExtensionsUnitTests.cs @@ -0,0 +1,221 @@ +using Ethar.GeoPose.Authority.FrameSpecifications; +using Ethar.GeoPose.DataTypes; +using Ethar.GeoPose.Extensions; +using Ethar.GeoPose.FrameSpecifications; +using Ethar.GeoPose.StructuralDataUnits; +using NUnit.Framework; +using System.Collections.Generic; + +namespace Ethar.GeoPose.UnitTests +{ + [TestFixture] + public class GraphSduExtensionsUnitTests : UnitTestBase + { + [TestCaseSource(nameof(IndexValidationTestCaseData))] + public bool CanCorrectlyValidateGraphSdu(List frameList, + List transformList) + { + var uut = new GraphSdu( + 1, + frameList, transformList); + return uut.Validate(); + } + + private static IEnumerable IndexValidationTestCaseData + { + get + { + yield return new TestCaseData(new List() + { + new LtpEnuSpecification(new TangentPointPosition() + { Latitude = 48, Longitude = -122, HeightInMeters = 5 }), + new LtpEnuSpecification(new TangentPointPosition() + { Latitude = 48, Longitude = -122, HeightInMeters = 5 }), + }, + new List() + { + new FrameTransformIndexPair() { OuterFrameIndex = 0, InnerFrameIndex = 1 }, + }).Returns(true); + yield return new TestCaseData(new List() + { + new LtpEnuSpecification(new TangentPointPosition() + { Latitude = 48, Longitude = -122, HeightInMeters = 5 }), + new LtpEnuSpecification(new TangentPointPosition() + { Latitude = 48, Longitude = -122, HeightInMeters = 5 }), + }, + new List() + { + new FrameTransformIndexPair() { OuterFrameIndex = 0, InnerFrameIndex = 2 }, + }).Returns(false); + yield return new TestCaseData(new List() + { + new LtpEnuSpecification(new TangentPointPosition() + { Latitude = 48, Longitude = -122, HeightInMeters = 5 }), + new LtpEnuSpecification(new TangentPointPosition() + { Latitude = 48, Longitude = -122, HeightInMeters = 5 }), + }, + new List() + { + new FrameTransformIndexPair() { OuterFrameIndex = 2, InnerFrameIndex = 0 }, + }).Returns(false); + yield return new TestCaseData(new List() + { + new LtpEnuSpecification(new TangentPointPosition() + { Latitude = 48, Longitude = -122, HeightInMeters = 5 }), + new TranslateRotateSpecification( + new DataTypes.UnitVector3(0.1f, 0.2f, 0.3f), + new DataTypes.UnitQuaternion() { X = 0.692f, Y = 0.691f, Z = 0.141f, W = 0.14f }), + new TranslateRotateSpecification( + new DataTypes.UnitVector3(0.4f, 0.5f, 0.6f), + new DataTypes.UnitQuaternion() { X = 0.692f, Y = 0.691f, Z = 0.141f, W = 0.14f }), + new TranslateRotateSpecification( + new DataTypes.UnitVector3(0.7f, 0.8f, 0.9f), + new DataTypes.UnitQuaternion() { X = 0.692f, Y = 0.691f, Z = 0.141f, W = 0.14f }) + }, + new List() + { + new FrameTransformIndexPair() { OuterFrameIndex = 0, InnerFrameIndex = 1 }, + new FrameTransformIndexPair() { OuterFrameIndex = 1, InnerFrameIndex = 2 }, + new FrameTransformIndexPair() { OuterFrameIndex = 2, InnerFrameIndex = 3 }, + }).Returns(true); + yield return new TestCaseData(new List() + { + new LtpEnuSpecification(new TangentPointPosition() + { Latitude = 48, Longitude = -122, HeightInMeters = 5 }), + new TranslateRotateSpecification( + new DataTypes.UnitVector3(0.1f, 0.2f, 0.3f), + new DataTypes.UnitQuaternion() { X = 0.692f, Y = 0.691f, Z = 0.141f, W = 0.14f }), + new TranslateRotateSpecification( + new DataTypes.UnitVector3(0.4f, 0.5f, 0.6f), + new DataTypes.UnitQuaternion() { X = 0.692f, Y = 0.691f, Z = 0.141f, W = 0.14f }), + new TranslateRotateSpecification( + new DataTypes.UnitVector3(0.7f, 0.8f, 0.9f), + new DataTypes.UnitQuaternion() { X = 0.692f, Y = 0.691f, Z = 0.141f, W = 0.14f }) + }, + new List() + { + new FrameTransformIndexPair() { OuterFrameIndex = 0, InnerFrameIndex = 1 }, + new FrameTransformIndexPair() { OuterFrameIndex = 0, InnerFrameIndex = 2 }, + new FrameTransformIndexPair() { OuterFrameIndex = 0, InnerFrameIndex = 3 }, + }).Returns(true); + yield return new TestCaseData(new List() + { + new LtpEnuSpecification(new DataTypes.TangentPointPosition() + { Latitude = 48, Longitude = -122, HeightInMeters = 5 }), + new TranslateRotateSpecification( + new DataTypes.UnitVector3(0.1f, 0.2f, 0.3f), + new UnitQuaternion() { X = 0.692f, Y = 0.691f, Z = 0.141f, W = 0.14f }), + new TranslateRotateSpecification( + new DataTypes.UnitVector3(0.4f, 0.5f, 0.6f), + new UnitQuaternion() { X = 0.692f, Y = 0.691f, Z = 0.141f, W = 0.14f }), + new TranslateRotateSpecification( + new DataTypes.UnitVector3(0.7f, 0.8f, 0.9f), + new UnitQuaternion() { X = 0.692f, Y = 0.691f, Z = 0.141f, W = 0.14f }) + }, + new List() + { + new FrameTransformIndexPair() { OuterFrameIndex = 2, InnerFrameIndex = 3 }, + new FrameTransformIndexPair() { OuterFrameIndex = 1, InnerFrameIndex = 2 }, + new FrameTransformIndexPair() { OuterFrameIndex = 0, InnerFrameIndex = 1 }, + }).Returns(true); + yield return new TestCaseData(new List() + { + new LtpEnuSpecification(new TangentPointPosition() + { Latitude = 48, Longitude = -122, HeightInMeters = 5 }), + new TranslateRotateSpecification( + new DataTypes.UnitVector3(0.1f, 0.2f, 0.3f), + new UnitQuaternion() { X = 0.692f, Y = 0.691f, Z = 0.141f, W = 0.14f }), + new TranslateRotateSpecification( + new DataTypes.UnitVector3(0.4f, 0.5f, 0.6f), + new UnitQuaternion() { X = 0.692f, Y = 0.691f, Z = 0.141f, W = 0.14f }), + new TranslateRotateSpecification( + new DataTypes.UnitVector3(0.7f, 0.8f, 0.9f), + new UnitQuaternion() { X = 0.692f, Y = 0.691f, Z = 0.141f, W = 0.14f }) + }, + new List() + { + new FrameTransformIndexPair() { OuterFrameIndex = 2, InnerFrameIndex = 3 }, + new FrameTransformIndexPair() { OuterFrameIndex = 3, InnerFrameIndex = 0 }, + new FrameTransformIndexPair() { OuterFrameIndex = 0, InnerFrameIndex = 1 }, + }).Returns(false); + yield return new TestCaseData(new List() + { + new LtpEnuSpecification(new TangentPointPosition() + { Latitude = 48, Longitude = -122, HeightInMeters = 5 }), + new LtpEnuSpecification(new TangentPointPosition() + { Latitude = 48, Longitude = -122, HeightInMeters = 5 }), + new LtpEnuSpecification(new TangentPointPosition() + { Latitude = 48, Longitude = -122, HeightInMeters = 5 }), + new LtpEnuSpecification(new TangentPointPosition() + { Latitude = 48, Longitude = -122, HeightInMeters = 5 }), + }, + new List()).Returns(true); + yield return new TestCaseData(new List() + { + new TranslateRotateSpecification( + new DataTypes.UnitVector3(0.1f, 0.2f, 0.3f), + new UnitQuaternion() { X = 0.692f, Y = 0.691f, Z = 0.141f, W = 0.14f }), + new TranslateRotateSpecification( + new DataTypes.UnitVector3(0.1f, 0.2f, 0.3f), + new UnitQuaternion() { X = 0.692f, Y = 0.691f, Z = 0.141f, W = 0.14f }), + new TranslateRotateSpecification( + new DataTypes.UnitVector3(0.4f, 0.5f, 0.6f), + new UnitQuaternion() { X = 0.692f, Y = 0.691f, Z = 0.141f, W = 0.14f }), + new TranslateRotateSpecification( + new DataTypes.UnitVector3(0.7f, 0.8f, 0.9f), + new UnitQuaternion() { X = 0.692f, Y = 0.691f, Z = 0.141f, W = 0.14f }) + }, + new List()).Returns(false); + yield return new TestCaseData(new List() + { + new LtpEnuSpecification(new DataTypes.TangentPointPosition() + { Latitude = 48, Longitude = -122, HeightInMeters = 5 }), + new TranslateRotateSpecification( + new DataTypes.UnitVector3(0.1f, 0.2f, 0.3f), + new UnitQuaternion() { X = 0.692f, Y = 0.691f, Z = 0.141f, W = 0.14f }), + new TranslateRotateSpecification( + new DataTypes.UnitVector3(0.4f, 0.5f, 0.6f), + new UnitQuaternion() { X = 0.692f, Y = 0.691f, Z = 0.141f, W = 0.14f }), + new TranslateRotateSpecification( + new DataTypes.UnitVector3(0.7f, 0.8f, 0.9f), + new UnitQuaternion() { X = 0.692f, Y = 0.691f, Z = 0.141f, W = 0.14f }), + new TranslateRotateSpecification( + new DataTypes.UnitVector3(0.7f, 0.8f, 0.9f), + new UnitQuaternion() { X = 0.692f, Y = 0.691f, Z = 0.141f, W = 0.14f }) + }, + new List() + { + new FrameTransformIndexPair() { OuterFrameIndex = 0, InnerFrameIndex = 1 }, + new FrameTransformIndexPair() { OuterFrameIndex = 1, InnerFrameIndex = 2 }, + new FrameTransformIndexPair() { OuterFrameIndex = 2, InnerFrameIndex = 3 }, + new FrameTransformIndexPair() { OuterFrameIndex = 3, InnerFrameIndex = 1 }, + }).Returns(false); + yield return new TestCaseData(new List() + { + new LtpEnuSpecification(new DataTypes.TangentPointPosition() + { Latitude = 48, Longitude = -122, HeightInMeters = 5 }), + new TranslateRotateSpecification( + new DataTypes.UnitVector3(0.1f, 0.2f, 0.3f), + new UnitQuaternion() { X = 0.692f, Y = 0.691f, Z = 0.141f, W = 0.14f }), + new TranslateRotateSpecification( + new DataTypes.UnitVector3(0.4f, 0.5f, 0.6f), + new UnitQuaternion() { X = 0.692f, Y = 0.691f, Z = 0.141f, W = 0.14f }), + new TranslateRotateSpecification( + new DataTypes.UnitVector3(0.7f, 0.8f, 0.9f), + new UnitQuaternion() { X = 0.692f, Y = 0.691f, Z = 0.141f, W = 0.14f }), + new TranslateRotateSpecification( + new DataTypes.UnitVector3(0.7f, 0.8f, 0.9f), + new UnitQuaternion() { X = 0.692f, Y = 0.691f, Z = 0.141f, W = 0.14f }) + }, + new List() + { + new FrameTransformIndexPair() { OuterFrameIndex = 0, InnerFrameIndex = 1 }, + new FrameTransformIndexPair() { OuterFrameIndex = 1, InnerFrameIndex = 2 }, + new FrameTransformIndexPair() { OuterFrameIndex = 2, InnerFrameIndex = 3 }, + new FrameTransformIndexPair() { OuterFrameIndex = 3, InnerFrameIndex = 4 }, + new FrameTransformIndexPair() { OuterFrameIndex = 4, InnerFrameIndex = 3 }, + }).Returns(true); + } + } + } +} \ No newline at end of file diff --git a/Ethar.GeoPose.UnitTests/GraphSduTests.cs b/Ethar.GeoPose.UnitTests/GraphSduTests.cs new file mode 100644 index 0000000..370b2af --- /dev/null +++ b/Ethar.GeoPose.UnitTests/GraphSduTests.cs @@ -0,0 +1,259 @@ +using System.Collections.Generic; +using System.Linq; +using Ethar.GeoPose.Authority.FrameSpecifications; +using Ethar.GeoPose.DataTypes; +using Ethar.GeoPose.FrameSpecifications; +using Ethar.GeoPose.StructuralDataUnits; +using Newtonsoft.Json; +using NUnit.Framework; + +namespace Ethar.GeoPose.UnitTests +{ + [TestFixture] + internal class GraphSduTests : UnitTestBase + { + [TestCase(16534234327, "\"/Ethar.GeoPose/1.0\"", "\"LTP-ENU\"", "\"longitude=-122.0000000&latitude=48.0000000&heightInMeters=5.000\"", "\"Translate-Rotate\"", "\"translation.x=0.1&translation.y=0.2&translation.z=0.3&rotation.x=0.692&rotation.y=0.691&rotation.z=0.141&rotation.w=0.14\"", "\"translation.x=0.4&translation.y=0.5&translation.z=0.6&rotation.x=0.692&rotation.y=0.691&rotation.z=0.141&rotation.w=0.14\"", "\"translation.x=0.7&translation.y=0.8&translation.z=0.9&rotation.x=0.692&rotation.y=0.691&rotation.z=0.141&rotation.w=0.14\"")] + public void CanCorrectlyDeserializeGraphSdu(long validTime, string authority, string outerFrameId, string outerFrameParams, string innerFrameId, string innerFrameParameters1, string innerFrameParameters2, string innerFrameParameters3) + { + var json = + "{" + + "\"frameList\": [" + + "{" + + $"\"authority\": {authority}," + + $"\"id\": {outerFrameId}," + + $"\"parameters\": {outerFrameParams}" + + "}," + + "{" + + $"\"authority\": {authority}," + + $"\"id\": {innerFrameId}," + + $"\"parameters\": {innerFrameParameters1}" + + "}," + + "{" + + $"\"authority\": {authority}," + + $"\"id\": {innerFrameId}," + + $"\"parameters\": {innerFrameParameters2}" + + "}," + + "{" + + $"\"authority\": {authority}," + + $"\"id\": {innerFrameId}," + + $"\"parameters\": {innerFrameParameters3}" + + "}" + + "]," + + "\"transformList\": [" + + "{" + + $"\"link\": [" + + "0,1" + + "]" + + "}," + + "{" + + $"\"link\": [" + + "1,2" + + "]" + + "}," + + "{" + + $"\"link\": [" + + "2,3" + + "]" + + "}," + + "{" + + $"\"link\": [" + + "0,3" + + "]" + + "}" + + "]," + + $"\"validTime\": {validTime}" + + "}"; + + var sdu = JsonConvert.DeserializeObject(json); + Assert.That(sdu.ValidTime, Is.EqualTo(validTime)); + + Assert.That(sdu.FrameList.OfType().Count(), Is.EqualTo(1)); + var ltpEnuFrame = sdu.FrameList.OfType().Cast().Single(); + + Assert.That(ltpEnuFrame.Position.Latitude, Is.EqualTo(48)); + Assert.That(ltpEnuFrame.Position.Longitude, Is.EqualTo(-122)); + Assert.That(ltpEnuFrame.Position.HeightInMeters, Is.EqualTo(5)); + + Assert.That(sdu.FrameList.OfType().Count(), Is.EqualTo(3)); + + var translateRotateList = sdu.FrameList.OfType().ToList(); + + Assert.That(translateRotateList.ElementAt(0).Translation.X, Is.EqualTo(0.1f)); + Assert.That(translateRotateList.ElementAt(0).Translation.Y, Is.EqualTo(0.2f)); + Assert.That(translateRotateList.ElementAt(0).Translation.Z, Is.EqualTo(0.3f)); + Assert.That(translateRotateList.ElementAt(0).Rotation.X, Is.EqualTo(0.692f)); + Assert.That(translateRotateList.ElementAt(0).Rotation.Y, Is.EqualTo(0.691f)); + Assert.That(translateRotateList.ElementAt(0).Rotation.Z, Is.EqualTo(0.141f)); + Assert.That(translateRotateList.ElementAt(0).Rotation.W, Is.EqualTo(0.14f)); + + Assert.That(translateRotateList.ElementAt(1).Translation.X, Is.EqualTo(0.4f)); + Assert.That(translateRotateList.ElementAt(1).Translation.Y, Is.EqualTo(0.5f)); + Assert.That(translateRotateList.ElementAt(1).Translation.Z, Is.EqualTo(0.6f)); + Assert.That(translateRotateList.ElementAt(1).Rotation.X, Is.EqualTo(0.692f)); + Assert.That(translateRotateList.ElementAt(1).Rotation.Y, Is.EqualTo(0.691f)); + Assert.That(translateRotateList.ElementAt(1).Rotation.Z, Is.EqualTo(0.141f)); + Assert.That(translateRotateList.ElementAt(1).Rotation.W, Is.EqualTo(0.14f)); + + Assert.That(translateRotateList.ElementAt(2).Translation.X, Is.EqualTo(0.7f)); + Assert.That(translateRotateList.ElementAt(2).Translation.Y, Is.EqualTo(0.8f)); + Assert.That(translateRotateList.ElementAt(2).Translation.Z, Is.EqualTo(0.9f)); + Assert.That(translateRotateList.ElementAt(2).Rotation.X, Is.EqualTo(0.692f)); + Assert.That(translateRotateList.ElementAt(2).Rotation.Y, Is.EqualTo(0.691f)); + Assert.That(translateRotateList.ElementAt(2).Rotation.Z, Is.EqualTo(0.141f)); + Assert.That(translateRotateList.ElementAt(2).Rotation.W, Is.EqualTo(0.14f)); + + Assert.That(sdu.TransformList.Count, Is.EqualTo(4)); + Assert.That(sdu.TransformList.Any(x => x.OuterFrameIndex == 0 && x.InnerFrameIndex == 1), Is.True); + Assert.That(sdu.TransformList.Any(x => x.OuterFrameIndex == 1 && x.InnerFrameIndex == 2), Is.True); + Assert.That(sdu.TransformList.Any(x => x.OuterFrameIndex == 2 && x.InnerFrameIndex == 3), Is.True); + Assert.That(sdu.TransformList.Any(x => x.OuterFrameIndex == 0 && x.InnerFrameIndex == 3), Is.True); + } + + [TestCase(16534234327, "/Ethar.GeoPose/1.0", "LTP-ENU", "\"latitude=48&longitude=-122&heightInMeters=5\"", "Translate-Rotate", "\"translation.x=0.1&translation.y=0.2&translation.z=0.3&rotation.x=0.692&rotation.y=0.691&rotation.z=0.141&rotation.w=0.14\"", "\"translation.x=0.4&translation.y=0.5&translation.z=0.6&rotation.x=0.692&rotation.y=0.691&rotation.z=0.141&rotation.w=0.14\"", "\"translation.x=0.7&translation.y=0.8&translation.z=0.9&rotation.x=0.692&rotation.y=0.691&rotation.z=0.141&rotation.w=0.14\"")] + public void CanCorrectlySerializeGraphSdu(long validTime, string authority, string outerFrameId, string outerFrameParams, string innerFrameId, string innerFrameParameters1, string innerFrameParameters2, string innerFrameParameters3) + { + var frameList = new List() + { + new LtpEnuSpecification(new DataTypes.TangentPointPosition() {Latitude = 48, Longitude = -122, HeightInMeters = 5}), + new TranslateRotateSpecification(new DataTypes.UnitVector3(0.1f, 0.2f, 0.3f), new DataTypes.UnitQuaternion() {X = 0.692f, Y = 0.691f, Z = 0.141f, W = 0.14f}), + new TranslateRotateSpecification(new DataTypes.UnitVector3(0.4f, 0.5f, 0.6f), new DataTypes.UnitQuaternion() {X = 0.692f, Y = 0.691f, Z = 0.141f, W = 0.14f}), + new TranslateRotateSpecification(new DataTypes.UnitVector3(0.7f, 0.8f, 0.9f), new DataTypes.UnitQuaternion() {X = 0.692f, Y = 0.691f, Z = 0.141f, W = 0.14f}) + }; + + var transformList = new List() + { + new FrameTransformIndexPair() { OuterFrameIndex = 0, InnerFrameIndex = 1 }, + new FrameTransformIndexPair() { OuterFrameIndex = 1, InnerFrameIndex = 2 }, + new FrameTransformIndexPair() { OuterFrameIndex = 2, InnerFrameIndex = 3 }, + new FrameTransformIndexPair() { OuterFrameIndex = 0, InnerFrameIndex = 3 } + }; + + var sdu = new GraphSdu(validTime, frameList, transformList); + + var expected = + "{" + + $"\"validTime\":{validTime}," + + "\"frameList\":[" + + "{" + + $"\"authority\":\"{authority}\"," + + $"\"id\":\"{outerFrameId}\"," + + $"\"parameters\":{outerFrameParams}" + + "}," + + "{" + + $"\"authority\":\"{authority}\"," + + $"\"id\":\"{innerFrameId}\"," + + $"\"parameters\":{innerFrameParameters1}" + + "}," + + "{" + + $"\"authority\":\"{authority}\"," + + $"\"id\":\"{innerFrameId}\"," + + $"\"parameters\":{innerFrameParameters2}" + + "}," + + "{" + + $"\"authority\":\"{authority}\"," + + $"\"id\":\"{innerFrameId}\"," + + $"\"parameters\":{innerFrameParameters3}" + + "}" + + "]," + + "\"transformList\":[" + + "{" + + $"\"link\":[" + + "0,1" + + "]" + + "}," + + "{" + + $"\"link\":[" + + "1,2" + + "]" + + "}," + + "{" + + $"\"link\":[" + + "2,3" + + "]" + + "}," + + "{" + + $"\"link\":[" + + "0,3" + + "]" + + "}" + + "]" + + "}"; + + var json = JsonConvert.SerializeObject(sdu); + Assert.That(json, Is.EqualTo(expected)); + } + + [TestCase(16534234327, "/Ethar.GeoPose/1.0", "LTP-ENU", "\"latitude=48&longitude=-122&heightInMeters=5\"", "Translate-Rotate", + "\"translation.x=0.1&translation.y=0.2&translation.z=0.3&rotation.x=0.692&rotation.y=0.691&rotation.z=0.141&rotation.w=0.14\"", + "\"translation.x=0.4&translation.y=0.5&translation.z=0.6&rotation.x=0.692&rotation.y=0.691&rotation.z=0.141&rotation.w=0.14\"", + "\"translation.x=0.7&translation.y=0.8&translation.z=0.9&rotation.x=0.692&rotation.y=0.691&rotation.z=0.141&rotation.w=0.14\"")] + public void CanCorrectlyMakeARoundTripSerialization(long validTime, string authority, string outerFrameId, string outerFrameParams, string innerFrameId, string innerFrameParameters1, string innerFrameParameters2, string innerFrameParameters3) + { + var frameList = new List() + { + new LtpEnuSpecification(new DataTypes.TangentPointPosition() {Latitude = 48, Longitude = -122, HeightInMeters = 5}), + new TranslateRotateSpecification(new DataTypes.UnitVector3(0.1f, 0.2f, 0.3f), new DataTypes.UnitQuaternion() {X = 0.692f, Y = 0.691f, Z = 0.141f, W = 0.14f}), + new TranslateRotateSpecification(new DataTypes.UnitVector3(0.4f, 0.5f, 0.6f), new DataTypes.UnitQuaternion() {X = 0.692f, Y = 0.691f, Z = 0.141f, W = 0.14f}), + new TranslateRotateSpecification(new DataTypes.UnitVector3(0.7f, 0.8f, 0.9f), new DataTypes.UnitQuaternion() {X = 0.692f, Y = 0.691f, Z = 0.141f, W = 0.14f}) + }; + + var transformList = new List() + { + new FrameTransformIndexPair() { OuterFrameIndex = 0, InnerFrameIndex = 1 }, + new FrameTransformIndexPair() { OuterFrameIndex = 1, InnerFrameIndex = 2 }, + new FrameTransformIndexPair() { OuterFrameIndex = 2, InnerFrameIndex = 3 }, + new FrameTransformIndexPair() { OuterFrameIndex = 0, InnerFrameIndex = 3 } + }; + + var sdu = new GraphSdu(validTime, frameList, transformList); + + var json = JsonConvert.SerializeObject(sdu); + var converted = JsonConvert.DeserializeObject(json); + + Assert.That(sdu, Is.EqualTo(converted)); + } + + [TestCase(16534234327, "\"/Ethar.GeoPose/1.0\"", "\"LTP-ENU\"", "\"longitude=-122.0000000&latitude=48.0000000&heightInMeters=5.000\"", "\"Translate-Rotate\"", "\"translation.x=0.1&translation.y=0.2&translation.z=0.3&rotation.x=0.692&rotation.y=0.691&rotation.z=0.141&rotation.w=0.14\"", "\"translation.x=0.4&translation.y=0.5&translation.z=0.6&rotation.x=0.692&rotation.y=0.691&rotation.z=0.141&rotation.w=0.14\"", "\"translation.x=0.7&translation.y=0.8&translation.z=0.9&rotation.x=0.692&rotation.y=0.691&rotation.z=0.141&rotation.w=0.14\"")] + public void CanConvertGraphSduToString(long validTime, string authority, string outerFrameId, string outerFrameParams, string innerFrameId, string innerFrameParameters1, string innerFrameParameters2, string innerFrameParameters3) + { + var frameList = new List() + { + new LtpEnuSpecification(new DataTypes.TangentPointPosition() {Latitude = 48, Longitude = -122, HeightInMeters = 5}), + new TranslateRotateSpecification(new DataTypes.UnitVector3(0.1f, 0.2f, 0.3f), new DataTypes.UnitQuaternion() {X = 0.692f, Y = 0.691f, Z = 0.141f, W = 0.14f}), + new TranslateRotateSpecification(new DataTypes.UnitVector3(0.4f, 0.5f, 0.6f), new DataTypes.UnitQuaternion() {X = 0.692f, Y = 0.691f, Z = 0.141f, W = 0.14f}), + new TranslateRotateSpecification(new DataTypes.UnitVector3(0.7f, 0.8f, 0.9f), new DataTypes.UnitQuaternion() {X = 0.692f, Y = 0.691f, Z = 0.141f, W = 0.14f}) + }; + + var transformList = new List() + { + new FrameTransformIndexPair() { OuterFrameIndex = 0, InnerFrameIndex = 1 }, + new FrameTransformIndexPair() { OuterFrameIndex = 1, InnerFrameIndex = 2 }, + new FrameTransformIndexPair() { OuterFrameIndex = 2, InnerFrameIndex = 3 }, + new FrameTransformIndexPair() { OuterFrameIndex = 0, InnerFrameIndex = 3 } + }; + + var sdu = new GraphSdu(validTime, frameList, transformList); + var result = sdu.ToString(); + Assert.That(result, !Is.Empty); + Assert.That(result, Is.EqualTo("ValidTime:16534234327, FrameListCount:4, TransformListCount:4")); + } + + [TestCase(16534234327)] + public void CanConvertGraphSduToStringWithNull(long validTime) + { + var sdu = new GraphSdu(validTime, null, null); + var result = sdu.ToString(); + Assert.That(result, !Is.Empty); + Assert.That(result, Is.EqualTo("ValidTime:16534234327, FrameListCount:0, TransformListCount:0")); + } + + [Test] + public void CanConvertGraphSduToStringNew() + { + var sdu = new GraphSdu(); + var result = sdu.ToString(); + Assert.That(result, !Is.Empty); + Assert.That(result, Is.EqualTo("ValidTime:0, FrameListCount:0, TransformListCount:0")); + } + } +} diff --git a/Ethar.GeoPose.UnitTests/HttpClientExtensions.cs b/Ethar.GeoPose.UnitTests/HttpClientExtensions.cs new file mode 100644 index 0000000..3039a87 --- /dev/null +++ b/Ethar.GeoPose.UnitTests/HttpClientExtensions.cs @@ -0,0 +1,16 @@ +using Newtonsoft.Json; +using System.Net.Http; +using System.Threading.Tasks; + +namespace Ethar.GeoPose.UnitTests +{ + public static class HttpClientExtensions + { + public static async Task ReadAsJsonAsync(this HttpClient client, string address) + { + var response = await client.GetAsync(address); + var json = await response.Content.ReadAsStringAsync(); + return JsonConvert.DeserializeObject(json); + } + } +} \ No newline at end of file diff --git a/Ethar.GeoPose.UnitTests/IrregularSeriesSduUnitTests.cs b/Ethar.GeoPose.UnitTests/IrregularSeriesSduUnitTests.cs new file mode 100644 index 0000000..09130e8 --- /dev/null +++ b/Ethar.GeoPose.UnitTests/IrregularSeriesSduUnitTests.cs @@ -0,0 +1,366 @@ +using Ethar.GeoPose.Authority.FrameSpecifications; +using Ethar.GeoPose.Authority.TransitionModels; +using Ethar.GeoPose.DataTypes; +using Ethar.GeoPose.StructuralDataUnits; +using Newtonsoft.Json; +using NUnit.Framework; +using System.Collections.Generic; +using System.Linq; + +namespace Ethar.GeoPose.UnitTests +{ + [TestFixture] + internal class IrregularSeriesSduUnitTests : UnitTestBase + { + [TestCase(16534234327, "\"/Ethar.GeoPose/1.0\"", "\"LTP-ENU\"", "\"longitude=-122.0000000&latitude=48.0000000&heightInMeters=5.000\"", "\"Translate-Rotate\"", + "\"translation.x=0.1&translation.y=0.2&translation.z=0.3&rotation.x=0.692&rotation.y=0.691&rotation.z=0.141&rotation.w=0.14\"", + "\"translation.x=0.4&translation.y=0.5&translation.z=0.6&rotation.x=0.692&rotation.y=0.691&rotation.z=0.141&rotation.w=0.14\"", + "\"translation.x=0.7&translation.y=0.8&translation.z=0.9&rotation.x=0.692&rotation.y=0.691&rotation.z=0.141&rotation.w=0.14\"")] + public void CanCorrectlyDeserializeIrregularSeriesSdu(long validTime, string authority, string outerFrameId, string outerFrameParams, string innerFrameId, string innerFrameParameters1, string innerFrameParameters2, string innerFrameParameters3) + { + var json = + "{" + + "\"header\":" + + "{" + + $"\"transitionModel\":" + + "{" + + $"\"authority\": {authority}," + + $"\"id\": \"none\"," + + $"\"parameters\": \"\"" + + "}," + + $"\"poseCount\": 2," + + $"\"integrityCheck\": \"{{\\\"SHA256\\\": \\\"5556fb65f8bf9eddb3ace1329c9a6aeedd4833409965aeee3e6b61ed21f47858\\\"}}\"," + + $"\"startInstant\": {validTime}," + + $"\"stopInstant\": {validTime}" + + "}," + + "\"innerFrameAndTimeSeries\": [" + + "{" + + "\"frame\":" + + "{" + + $"\"authority\": {authority}," + + $"\"id\": {innerFrameId}," + + $"\"parameters\": {innerFrameParameters1}" + + "}," + + $"\"validTime\": {validTime}," + + "}," + + "{" + + "\"frame\":" + + "{" + + $"\"authority\": {authority}," + + $"\"id\": {innerFrameId}," + + $"\"parameters\": {innerFrameParameters2}" + + "}," + + $"\"validTime\": {validTime}," + + "}," + + "{" + + "\"frame\":" + + "{" + + $"\"authority\": {authority}," + + $"\"id\": {innerFrameId}," + + $"\"parameters\": {innerFrameParameters3}" + + "}," + + $"\"validTime\": {validTime}," + + "}" + + "]," + + "\"outerFrame\":" + + "{" + + $"\"authority\": {authority}," + + $"\"id\": {outerFrameId}," + + $"\"parameters\": {outerFrameParams}" + + "}," + + $"\"trailer\":" + + "{" + + $"\"poseCount\": 2," + + $"\"integrityCheck\": \"{{\\\"SHA256\\\": \\\"5556fb65f8bf9eddb3ace1329c9a6aeedd4833409965aeee3e6b61ed21f47858\\\"}}\"," + + "}" + + "}"; + + var sdu = JsonConvert.DeserializeObject(json); + + Assert.That(sdu.Header.TransitionModel is NoneTransitionModel); + + Assert.That(sdu.OuterFrame is LtpEnuSpecification); + var outerFrame = (LtpEnuSpecification)sdu.OuterFrame; + + Assert.That(outerFrame.Position.Latitude, Is.EqualTo(48)); + Assert.That(outerFrame.Position.Longitude, Is.EqualTo(-122)); + Assert.That(outerFrame.Position.HeightInMeters, Is.EqualTo(5)); + + Assert.That(sdu.InnerFrameAndTimeSeries.All(x => x.Frame is TranslateRotateSpecification)); + + var translateRotateList = sdu.InnerFrameAndTimeSeries.Select(x => x.Frame as TranslateRotateSpecification).ToList(); + Assert.That(translateRotateList.Count, Is.EqualTo(3)); + + Assert.That(translateRotateList.ElementAt(0).Translation.X, Is.EqualTo(0.1f)); + Assert.That(translateRotateList.ElementAt(0).Translation.Y, Is.EqualTo(0.2f)); + Assert.That(translateRotateList.ElementAt(0).Translation.Z, Is.EqualTo(0.3f)); + Assert.That(translateRotateList.ElementAt(0).Rotation.X, Is.EqualTo(0.692f)); + Assert.That(translateRotateList.ElementAt(0).Rotation.Y, Is.EqualTo(0.691f)); + Assert.That(translateRotateList.ElementAt(0).Rotation.Z, Is.EqualTo(0.141f)); + Assert.That(translateRotateList.ElementAt(0).Rotation.W, Is.EqualTo(0.14f)); + + Assert.That(translateRotateList.ElementAt(1).Translation.X, Is.EqualTo(0.4f)); + Assert.That(translateRotateList.ElementAt(1).Translation.Y, Is.EqualTo(0.5f)); + Assert.That(translateRotateList.ElementAt(1).Translation.Z, Is.EqualTo(0.6f)); + Assert.That(translateRotateList.ElementAt(1).Rotation.X, Is.EqualTo(0.692f)); + Assert.That(translateRotateList.ElementAt(1).Rotation.Y, Is.EqualTo(0.691f)); + Assert.That(translateRotateList.ElementAt(1).Rotation.Z, Is.EqualTo(0.141f)); + Assert.That(translateRotateList.ElementAt(1).Rotation.W, Is.EqualTo(0.14f)); + + Assert.That(translateRotateList.ElementAt(2).Translation.X, Is.EqualTo(0.7f)); + Assert.That(translateRotateList.ElementAt(2).Translation.Y, Is.EqualTo(0.8f)); + Assert.That(translateRotateList.ElementAt(2).Translation.Z, Is.EqualTo(0.9f)); + Assert.That(translateRotateList.ElementAt(2).Rotation.X, Is.EqualTo(0.692f)); + Assert.That(translateRotateList.ElementAt(2).Rotation.Y, Is.EqualTo(0.691f)); + Assert.That(translateRotateList.ElementAt(2).Rotation.Z, Is.EqualTo(0.141f)); + Assert.That(translateRotateList.ElementAt(2).Rotation.W, Is.EqualTo(0.14f)); + } + + [TestCase(16534234327, "/Ethar.GeoPose/1.0", "LTP-ENU", "\"latitude=48&longitude=-122&heightInMeters=5\"", "Translate-Rotate", + "\"translation.x=0.1&translation.y=0.2&translation.z=0.3&rotation.x=0.692&rotation.y=0.691&rotation.z=0.141&rotation.w=0.14\"", + "\"translation.x=0.4&translation.y=0.5&translation.z=0.6&rotation.x=0.692&rotation.y=0.691&rotation.z=0.141&rotation.w=0.14\"", + "\"translation.x=0.7&translation.y=0.8&translation.z=0.9&rotation.x=0.692&rotation.y=0.691&rotation.z=0.141&rotation.w=0.14\"")] + public void CanCorrectlySerializeIrregularSeriesSdu(long validTime, string authority, string outerFrameId, string outerFrameParams, string innerFrameId, string innerFrameParameters1, string innerFrameParameters2, string innerFrameParameters3) + { + var header = new SeriesHeader() + { + TransitionModel = new NoneTransitionModel(), + IntegrityCheck = + "SHA256: 5556fb65f8bf9eddb3ace1329c9a6aeedd4833409965aeee3e6b61ed21f47858", + PoseCount = 2, + StartInstant = validTime, + StopInstant = validTime + }; + + var outerFrame = new LtpEnuSpecification(new TangentPointPosition() { Latitude = 48, Longitude = -122, HeightInMeters = 5 }); + + var trailer = new SeriesTrailer() + { + IntegrityCheck = + "SHA256: 5556fb65f8bf9eddb3ace1329c9a6aeedd4833409965aeee3e6b61ed21f47859", + PoseCount = 2 + }; + + var innerFrames = new List + { + new FrameAndTimeElement() + { + Frame = new TranslateRotateSpecification(new DataTypes.UnitVector3(0.1f, 0.2f, 0.3f), new UnitQuaternion() { X = 0.692f, Y = 0.691f, Z = 0.141f, W = 0.14f }), + ValidTime = validTime, + }, + new FrameAndTimeElement() + { + Frame = new TranslateRotateSpecification(new DataTypes.UnitVector3(0.4f, 0.5f, 0.6f), new UnitQuaternion() { X = 0.692f, Y = 0.691f, Z = 0.141f, W = 0.14f }), + ValidTime = validTime, + }, + new FrameAndTimeElement() + { + Frame = new TranslateRotateSpecification(new DataTypes.UnitVector3(0.7f, 0.8f, 0.9f), new UnitQuaternion() { X = 0.692f, Y = 0.691f, Z = 0.141f, W = 0.14f }), + ValidTime = validTime, + } + }; + + var sdu = new IrregularSeriesSdu(header, outerFrame, trailer, innerFrames); + + var expected = + "{" + + "\"header\":" + + "{" + + $"\"transitionModel\":" + + "{" + + $"\"authority\":\"{authority}\"," + + $"\"id\":\"none\"," + + $"\"parameters\":\"\"" + + "}," + + $"\"poseCount\":{sdu.Header.PoseCount}," + + $"\"startInstant\":{validTime}," + + $"\"stopInstant\":{validTime}," + + $"\"integrityCheck\":\"SHA256: 5556fb65f8bf9eddb3ace1329c9a6aeedd4833409965aeee3e6b61ed21f47858\"" + + "}," + + "\"outerFrame\":" + + "{" + + $"\"authority\":\"{authority}\"," + + $"\"id\":\"{outerFrameId}\"," + + $"\"parameters\":{outerFrameParams}" + + "}," + + "\"innerFrameAndTimeSeries\":[" + + "{" + + "\"frame\":" + + "{" + + $"\"authority\":\"{authority}\"," + + $"\"id\":\"{innerFrameId}\"," + + $"\"parameters\":{innerFrameParameters1}" + + "}," + + $"\"validTime\":{validTime}" + + "}," + + "{" + + "\"frame\":" + + "{" + + $"\"authority\":\"{authority}\"," + + $"\"id\":\"{innerFrameId}\"," + + $"\"parameters\":{innerFrameParameters2}" + + "}," + + $"\"validTime\":{validTime}" + + "}," + + "{" + + "\"frame\":" + + "{" + + $"\"authority\":\"{authority}\"," + + $"\"id\":\"{innerFrameId}\"," + + $"\"parameters\":{innerFrameParameters3}" + + "}," + + $"\"validTime\":{validTime}" + + "}" + + "]," + + $"\"trailer\":" + + "{" + + $"\"poseCount\":{sdu.Trailer.PoseCount}," + + $"\"integrityCheck\":\"SHA256: 5556fb65f8bf9eddb3ace1329c9a6aeedd4833409965aeee3e6b61ed21f47859\"" + + "}" + + "}"; + + var json = JsonConvert.SerializeObject(sdu); + Assert.That(json, Is.EqualTo(expected)); + } + + [TestCase(16534234327, "/Ethar.GeoPose/1.0", "LTP-ENU", "\"latitude=48&longitude=-122&heightInMeters=5\"", + "Translate-Rotate", + "\"translation.x=0.1&translation.y=0.2&translation.z=0.3&rotation.x=0.692&rotation.y=0.691&rotation.z=0.141&rotation.w=0.14\"", + "\"translation.x=0.4&translation.y=0.5&translation.z=0.6&rotation.x=0.692&rotation.y=0.691&rotation.z=0.141&rotation.w=0.14\"", + "\"translation.x=0.7&translation.y=0.8&translation.z=0.9&rotation.x=0.692&rotation.y=0.691&rotation.z=0.141&rotation.w=0.14\"")] + public void CanCorrectlyMakeARoundTripConversion(long validTime, string authority, string outerFrameId, + string outerFrameParams, string innerFrameId, string innerFrameParameters1, string innerFrameParameters2, + string innerFrameParameters3) + { + var header = new SeriesHeader() + { + TransitionModel = new NoneTransitionModel(), + IntegrityCheck = + "SHA256: 5556fb65f8bf9eddb3ace1329c9a6aeedd4833409965aeee3e6b61ed21f47858", + PoseCount = 2, + StartInstant = validTime, + StopInstant = validTime + }; + + var outerFrame = new LtpEnuSpecification(new TangentPointPosition() { Latitude = 48, Longitude = -122, HeightInMeters = 5 }); + + var trailer = new SeriesTrailer() + { + IntegrityCheck = + "SHA256: 5556fb65f8bf9eddb3ace1329c9a6aeedd4833409965aeee3e6b61ed21f47859", + PoseCount = 2 + }; + + var innerFrames = new List + { + new FrameAndTimeElement() + { + Frame = new TranslateRotateSpecification(new DataTypes.UnitVector3(0.1f, 0.2f, 0.3f), new UnitQuaternion() { X = 0.692f, Y = 0.691f, Z = 0.141f, W = 0.14f }), + ValidTime = validTime, + }, + new FrameAndTimeElement() + { + Frame = new TranslateRotateSpecification(new DataTypes.UnitVector3(0.4f, 0.5f, 0.6f), new UnitQuaternion() { X = 0.692f, Y = 0.691f, Z = 0.141f, W = 0.14f }), + ValidTime = validTime, + }, + new FrameAndTimeElement() + { + Frame = new TranslateRotateSpecification(new DataTypes.UnitVector3(0.7f, 0.8f, 0.9f), new UnitQuaternion() { X = 0.692f, Y = 0.691f, Z = 0.141f, W = 0.14f }), + ValidTime = validTime, + } + }; + + var sdu = new IrregularSeriesSdu(header, outerFrame, trailer, innerFrames); + + var json = JsonConvert.SerializeObject(sdu); + var converted = JsonConvert.DeserializeObject(json); + + Assert.That(sdu, Is.EqualTo(converted)); + } + + [TestCase(16534234327, "\"/Ethar.GeoPose/1.0\"", "\"LTP-ENU\"", "\"longitude=-122.0000000&latitude=48.0000000&heightInMeters=5.000\"", "\"Translate-Rotate\"", + "\"translation.x=0.1&translation.y=0.2&translation.z=0.3&rotation.x=0.692&rotation.y=0.691&rotation.z=0.141&rotation.w=0.14\"", + "\"translation.x=0.4&translation.y=0.5&translation.z=0.6&rotation.x=0.692&rotation.y=0.691&rotation.z=0.141&rotation.w=0.14\"", + "\"translation.x=0.7&translation.y=0.8&translation.z=0.9&rotation.x=0.692&rotation.y=0.691&rotation.z=0.141&rotation.w=0.14\"")] + public void CanConvertIrregularSeriesSduToString(long validTime, string authority, string outerFrameId, string outerFrameParams, string innerFrameId, string innerFrameParameters1, string innerFrameParameters2, string innerFrameParameters3) + { + var header = new SeriesHeader() + { + TransitionModel = new NoneTransitionModel(), + IntegrityCheck = + "SHA256: 5556fb65f8bf9eddb3ace1329c9a6aeedd4833409965aeee3e6b61ed21f47858", + PoseCount = 2, + StartInstant = validTime, + StopInstant = validTime + }; + + var outerFrame = new LtpEnuSpecification(new TangentPointPosition() { Latitude = 48, Longitude = -122, HeightInMeters = 5 }); + + var trailer = new SeriesTrailer() + { + IntegrityCheck = + "SHA256: 5556fb65f8bf9eddb3ace1329c9a6aeedd4833409965aeee3e6b61ed21f47859", + PoseCount = 2 + }; + + var innerFrames = new List + { + new FrameAndTimeElement() + { + Frame = new TranslateRotateSpecification(new DataTypes.UnitVector3(0.1f, 0.2f, 0.3f), new UnitQuaternion() { X = 0.692f, Y = 0.691f, Z = 0.141f, W = 0.14f }), + ValidTime = validTime, + }, + new FrameAndTimeElement() + { + Frame = new TranslateRotateSpecification(new DataTypes.UnitVector3(0.4f, 0.5f, 0.6f), new UnitQuaternion() { X = 0.692f, Y = 0.691f, Z = 0.141f, W = 0.14f }), + ValidTime = validTime, + }, + new FrameAndTimeElement() + { + Frame = new TranslateRotateSpecification(new DataTypes.UnitVector3(0.7f, 0.8f, 0.9f), new UnitQuaternion() { X = 0.692f, Y = 0.691f, Z = 0.141f, W = 0.14f }), + ValidTime = validTime, + } + }; + + var sdu = new IrregularSeriesSdu(header, outerFrame, trailer, innerFrames); + var result = sdu.ToString(); + Assert.That(result, !Is.Empty); + Assert.That(result, Is.EqualTo("Header:[TransitionModel:[Authority:/Ethar.GeoPose/1.0, Id:none], PoseCount:2, StartInstant:16534234327, StopInstant:16534234327, IntegrityCheck:SHA256: 5556fb65f8bf9eddb3ace1329c9a6aeedd4833409965aeee3e6b61ed21f47858], OuterFrame:[Authority:/Ethar.GeoPose/1.0, Id:LTP-ENU], InnerFrameAndTimeSeriesCount:3, Trailer:[PoseCount:2, IntegrityCheck:SHA256: 5556fb65f8bf9eddb3ace1329c9a6aeedd4833409965aeee3e6b61ed21f47859]")); + } + + [TestCase(16534234327)] + public void CanConvertIrregularSeriesSduToStringWithNull(long validTime) + { + var header = new SeriesHeader() + { + TransitionModel = new NoneTransitionModel(), + IntegrityCheck = + "SHA256: 5556fb65f8bf9eddb3ace1329c9a6aeedd4833409965aeee3e6b61ed21f47858", + PoseCount = 2, + StartInstant = validTime, + StopInstant = validTime + }; + + var trailer = new SeriesTrailer() + { + IntegrityCheck = + "SHA256: 5556fb65f8bf9eddb3ace1329c9a6aeedd4833409965aeee3e6b61ed21f47859", + PoseCount = 2 + }; + + var sdu = new IrregularSeriesSdu(header, null, trailer, null); + var result = sdu.ToString(); + Assert.That(result, !Is.Empty); + Assert.That(result, Is.EqualTo("Header:[TransitionModel:[Authority:/Ethar.GeoPose/1.0, Id:none], PoseCount:2, StartInstant:16534234327, StopInstant:16534234327, IntegrityCheck:SHA256: 5556fb65f8bf9eddb3ace1329c9a6aeedd4833409965aeee3e6b61ed21f47858], OuterFrame:[], InnerFrameAndTimeSeriesCount:0, Trailer:[PoseCount:2, IntegrityCheck:SHA256: 5556fb65f8bf9eddb3ace1329c9a6aeedd4833409965aeee3e6b61ed21f47859]")); + } + + [Test] + public void CanConvertIrregularSeriesSduToStringNew() + { + var sdu = new IrregularSeriesSdu(); + var result = sdu.ToString(); + Assert.That(result, !Is.Empty); + Assert.That(result, Is.EqualTo("Header:[TransitionModel:[], PoseCount:0, StartInstant:0, StopInstant:0, IntegrityCheck:], OuterFrame:[], InnerFrameAndTimeSeriesCount:0, Trailer:[PoseCount:0, IntegrityCheck:]")); + } + } +} \ No newline at end of file diff --git a/Ethar.GeoPose.UnitTests/RegularSeriesSduUnitTests.cs b/Ethar.GeoPose.UnitTests/RegularSeriesSduUnitTests.cs new file mode 100644 index 0000000..36148e3 --- /dev/null +++ b/Ethar.GeoPose.UnitTests/RegularSeriesSduUnitTests.cs @@ -0,0 +1,278 @@ +using System.Collections.Generic; +using System.Linq; +using Ethar.GeoPose.Authority.FrameSpecifications; +using Ethar.GeoPose.Authority.TransitionModels; +using Ethar.GeoPose.DataTypes; +using Ethar.GeoPose.FrameSpecifications; +using Ethar.GeoPose.StructuralDataUnits; +using Newtonsoft.Json; +using NUnit.Framework; + +namespace Ethar.GeoPose.UnitTests +{ + [TestFixture] + internal class RegularSeriesSduUnitTests : UnitTestBase + { + [TestCase(16534234327, "\"/Ethar.GeoPose/1.0\"", "\"LTP-ENU\"", "\"longitude=-122.0000000&latitude=48.0000000&heightInMeters=5.000\"", "\"Translate-Rotate\"", + "\"translation.x=0.1&translation.y=0.2&translation.z=0.3&rotation.x=0.692&rotation.y=0.691&rotation.z=0.141&rotation.w=0.14\"", + "\"translation.x=0.4&translation.y=0.5&translation.z=0.6&rotation.x=0.692&rotation.y=0.691&rotation.z=0.141&rotation.w=0.14\"", + "\"translation.x=0.7&translation.y=0.8&translation.z=0.9&rotation.x=0.692&rotation.y=0.691&rotation.z=0.141&rotation.w=0.14\"")] + public void CanCorrectlyDeserializeRegularSeriesSdu(long validTime, string authority, string outerFrameId, string outerFrameParams, string innerFrameId, string innerFrameParameters1, string innerFrameParameters2, string innerFrameParameters3) + { + var json = + "{" + + "\"header\":" + + "{" + + $"\"transitionModel\":" + + "{" + + $"\"authority\": {authority}," + + $"\"id\": \"none\"," + + $"\"parameters\": \"\"" + + "}," + + $"\"poseCount\": 2," + + $"\"integrityCheck\": \"{{\\\"SHA256\\\": \\\"5556fb65f8bf9eddb3ace1329c9a6aeedd4833409965aeee3e6b61ed21f47858\\\"}}\"," + + $"\"startInstant\": {validTime}," + + $"\"stopInstant\": {validTime}" + + "}," + + "\"innerFrameSeries\": [" + + "{" + + $"\"authority\": {authority}," + + $"\"id\": {innerFrameId}," + + $"\"parameters\": {innerFrameParameters1}" + + "}," + + "{" + + $"\"authority\": {authority}," + + $"\"id\": {innerFrameId}," + + $"\"parameters\": {innerFrameParameters2}" + + "}," + + "{" + + $"\"authority\": {authority}," + + $"\"id\": {innerFrameId}," + + $"\"parameters\": {innerFrameParameters3}" + + "}" + + "]," + + "\"outerFrame\":" + + "{" + + $"\"authority\": {authority}," + + $"\"id\": {outerFrameId}," + + $"\"parameters\": {outerFrameParams}" + + "}," + + $"\"interPoseDuration\": 1000," + + $"\"trailer\":" + + "{" + + $"\"poseCount\": 2," + + $"\"integrityCheck\": \"{{\\\"SHA256\\\": \\\"5556fb65f8bf9eddb3ace1329c9a6aeedd4833409965aeee3e6b61ed21f47858\\\"}}\"," + + "}" + + "}"; + + var sdu = JsonConvert.DeserializeObject(json); + Assert.That(sdu.InterPoseDuration.NumericDuration, Is.EqualTo(1000)); + + Assert.That(sdu.OuterFrame is LtpEnuSpecification); + var outerFrame = (LtpEnuSpecification)sdu.OuterFrame; + + Assert.That(outerFrame.Position.Latitude, Is.EqualTo(48)); + Assert.That(outerFrame.Position.Longitude, Is.EqualTo(-122)); + Assert.That(outerFrame.Position.HeightInMeters, Is.EqualTo(5)); + + Assert.That(sdu.InnerFrameSeries.All(x => x is TranslateRotateSpecification)); + + var translateRotateList = sdu.InnerFrameSeries.Select(x => x as TranslateRotateSpecification).ToList(); + Assert.That(translateRotateList.Count, Is.EqualTo(3)); + + Assert.That(translateRotateList.ElementAt(0).Translation.X, Is.EqualTo(0.1f)); + Assert.That(translateRotateList.ElementAt(0).Translation.Y, Is.EqualTo(0.2f)); + Assert.That(translateRotateList.ElementAt(0).Translation.Z, Is.EqualTo(0.3f)); + Assert.That(translateRotateList.ElementAt(0).Rotation.X, Is.EqualTo(0.692f)); + Assert.That(translateRotateList.ElementAt(0).Rotation.Y, Is.EqualTo(0.691f)); + Assert.That(translateRotateList.ElementAt(0).Rotation.Z, Is.EqualTo(0.141f)); + Assert.That(translateRotateList.ElementAt(0).Rotation.W, Is.EqualTo(0.14f)); + + Assert.That(translateRotateList.ElementAt(1).Translation.X, Is.EqualTo(0.4f)); + Assert.That(translateRotateList.ElementAt(1).Translation.Y, Is.EqualTo(0.5f)); + Assert.That(translateRotateList.ElementAt(1).Translation.Z, Is.EqualTo(0.6f)); + Assert.That(translateRotateList.ElementAt(1).Rotation.X, Is.EqualTo(0.692f)); + Assert.That(translateRotateList.ElementAt(1).Rotation.Y, Is.EqualTo(0.691f)); + Assert.That(translateRotateList.ElementAt(1).Rotation.Z, Is.EqualTo(0.141f)); + Assert.That(translateRotateList.ElementAt(1).Rotation.W, Is.EqualTo(0.14f)); + + Assert.That(translateRotateList.ElementAt(2).Translation.X, Is.EqualTo(0.7f)); + Assert.That(translateRotateList.ElementAt(2).Translation.Y, Is.EqualTo(0.8f)); + Assert.That(translateRotateList.ElementAt(2).Translation.Z, Is.EqualTo(0.9f)); + Assert.That(translateRotateList.ElementAt(2).Rotation.X, Is.EqualTo(0.692f)); + Assert.That(translateRotateList.ElementAt(2).Rotation.Y, Is.EqualTo(0.691f)); + Assert.That(translateRotateList.ElementAt(2).Rotation.Z, Is.EqualTo(0.141f)); + Assert.That(translateRotateList.ElementAt(2).Rotation.W, Is.EqualTo(0.14f)); + } + + [TestCase(16534234327, "/Ethar.GeoPose/1.0", "LTP-ENU", "\"latitude=48&longitude=-122&heightInMeters=5\"", "Translate-Rotate", + "\"translation.x=0.1&translation.y=0.2&translation.z=0.3&rotation.x=0.692&rotation.y=0.691&rotation.z=0.141&rotation.w=0.14\"", + "\"translation.x=0.4&translation.y=0.5&translation.z=0.6&rotation.x=0.692&rotation.y=0.691&rotation.z=0.141&rotation.w=0.14\"", + "\"translation.x=0.7&translation.y=0.8&translation.z=0.9&rotation.x=0.692&rotation.y=0.691&rotation.z=0.141&rotation.w=0.14\"")] + public void CanCorrectlySerializeRegularSeriesSdu(long validTime, string authority, string outerFrameId, string outerFrameParams, string innerFrameId, string innerFrameParameters1, string innerFrameParameters2, string innerFrameParameters3) + { + var header = new SeriesHeader() + { + TransitionModel = new NoneTransitionModel(), + IntegrityCheck = + "SHA256: 5556fb65f8bf9eddb3ace1329c9a6aeedd4833409965aeee3e6b61ed21f47858", + PoseCount = 2, + StartInstant = validTime, + StopInstant = validTime + }; + + var outerFrame = new LtpEnuSpecification(new TangentPointPosition() { Latitude = 48, Longitude = -122, HeightInMeters = 5 }); + + var trailer = new SeriesTrailer() + { + IntegrityCheck = + "SHA256: 5556fb65f8bf9eddb3ace1329c9a6aeedd4833409965aeee3e6b61ed21f47859", + PoseCount = 2 + }; + + var innerFrames = new List() + { + new TranslateRotateSpecification(new DataTypes.UnitVector3(0.1f, 0.2f, 0.3f), new UnitQuaternion() {X = 0.692f, Y = 0.691f, Z = 0.141f, W = 0.14f}), + new TranslateRotateSpecification(new DataTypes.UnitVector3(0.4f, 0.5f, 0.6f), new UnitQuaternion() {X = 0.692f, Y = 0.691f, Z = 0.141f, W = 0.14f}), + new TranslateRotateSpecification(new DataTypes.UnitVector3(0.7f, 0.8f, 0.9f), new UnitQuaternion() {X = 0.692f, Y = 0.691f, Z = 0.141f, W = 0.14f}), + }; + + var sdu = new RegularSeriesSdu(header, new GeoPoseDuration() { NumericDuration = 1000 }, outerFrame, innerFrames, trailer); + + var expected = + "{" + + "\"header\":" + + "{" + + $"\"transitionModel\":" + + "{" + + $"\"authority\":\"{authority}\"," + + $"\"id\":\"none\"," + + $"\"parameters\":\"\"" + + "}," + + $"\"poseCount\":2," + + $"\"startInstant\":{validTime}," + + $"\"stopInstant\":{validTime}," + + $"\"integrityCheck\":\"SHA256: 5556fb65f8bf9eddb3ace1329c9a6aeedd4833409965aeee3e6b61ed21f47858\"" + + "}," + + $"\"interPoseDuration\":1000," + + "\"outerFrame\":" + + "{" + + $"\"authority\":\"{authority}\"," + + $"\"id\":\"{outerFrameId}\"," + + $"\"parameters\":{outerFrameParams}" + + "}," + + "\"innerFrameSeries\":[" + + "{" + + $"\"authority\":\"{authority}\"," + + $"\"id\":\"{innerFrameId}\"," + + $"\"parameters\":{innerFrameParameters1}" + + "}," + + "{" + + $"\"authority\":\"{authority}\"," + + $"\"id\":\"{innerFrameId}\"," + + $"\"parameters\":{innerFrameParameters2}" + + "}," + + "{" + + $"\"authority\":\"{authority}\"," + + $"\"id\":\"{innerFrameId}\"," + + $"\"parameters\":{innerFrameParameters3}" + + "}" + + "]," + + $"\"trailer\":" + + "{" + + $"\"poseCount\":2," + + $"\"integrityCheck\":\"SHA256: 5556fb65f8bf9eddb3ace1329c9a6aeedd4833409965aeee3e6b61ed21f47859\"" + + "}" + + "}"; + + var json = JsonConvert.SerializeObject(sdu); + Assert.That(json, Is.EqualTo(expected)); + } + + [TestCase(16534234327, "/Ethar.GeoPose/1.0", "LTP-ENU", "\"latitude=48&longitude=-122&heightInMeters=5\"", "Translate-Rotate", + "\"translation.x=0.1&translation.y=0.2&translation.z=0.3&rotation.x=0.692&rotation.y=0.691&rotation.z=0.141&rotation.w=0.14\"", + "\"translation.x=0.4&translation.y=0.5&translation.z=0.6&rotation.x=0.692&rotation.y=0.691&rotation.z=0.141&rotation.w=0.14\"", + "\"translation.x=0.7&translation.y=0.8&translation.z=0.9&rotation.x=0.692&rotation.y=0.691&rotation.z=0.141&rotation.w=0.14\"")] + public void CanCorrectlyMakeARoundTripConversion(long validTime, string authority, string outerFrameId, string outerFrameParams, string innerFrameId, string innerFrameParameters1, string innerFrameParameters2, string innerFrameParameters3) + { + var header = new SeriesHeader() + { + TransitionModel = new NoneTransitionModel(), + IntegrityCheck = + "SHA256: 5556fb65f8bf9eddb3ace1329c9a6aeedd4833409965aeee3e6b61ed21f47858", + PoseCount = 2, + StartInstant = validTime, + StopInstant = validTime + }; + + var outerFrame = new LtpEnuSpecification(new TangentPointPosition() { Latitude = 48, Longitude = -122, HeightInMeters = 5 }); + + var trailer = new SeriesTrailer() + { + IntegrityCheck = + "SHA256: 5556fb65f8bf9eddb3ace1329c9a6aeedd4833409965aeee3e6b61ed21f47859", + PoseCount = 2 + }; + + var innerFrames = new List() + { + new TranslateRotateSpecification(new DataTypes.UnitVector3(0.1f, 0.2f, 0.3f), new UnitQuaternion() {X = 0.692f, Y = 0.691f, Z = 0.141f, W = 0.14f}), + new TranslateRotateSpecification(new DataTypes.UnitVector3(0.4f, 0.5f, 0.6f), new UnitQuaternion() {X = 0.692f, Y = 0.691f, Z = 0.141f, W = 0.14f}), + new TranslateRotateSpecification(new DataTypes.UnitVector3(0.7f, 0.8f, 0.9f), new UnitQuaternion() {X = 0.692f, Y = 0.691f, Z = 0.141f, W = 0.14f}), + }; + + var sdu = new RegularSeriesSdu(header, new GeoPoseDuration() { NumericDuration = 1000 }, outerFrame, innerFrames, trailer); + + var json = JsonConvert.SerializeObject(sdu); + var converted = JsonConvert.DeserializeObject(json); + Assert.That(converted, Is.EqualTo(sdu)); + } + + [TestCase(16534234327, "\"/Ethar.GeoPose/1.0\"", "\"LTP-ENU\"", "\"longitude=-122.0000000&latitude=48.0000000&heightInMeters=5.000\"", "\"Translate-Rotate\"", + "\"translation.x=0.1&translation.y=0.2&translation.z=0.3&rotation.x=0.692&rotation.y=0.691&rotation.z=0.141&rotation.w=0.14\"", + "\"translation.x=0.4&translation.y=0.5&translation.z=0.6&rotation.x=0.692&rotation.y=0.691&rotation.z=0.141&rotation.w=0.14\"", + "\"translation.x=0.7&translation.y=0.8&translation.z=0.9&rotation.x=0.692&rotation.y=0.691&rotation.z=0.141&rotation.w=0.14\"")] + public void CanConvertRegularSeriesSduToString(long validTime, string authority, string outerFrameId, string outerFrameParams, string innerFrameId, string innerFrameParameters1, string innerFrameParameters2, string innerFrameParameters3) + { + var header = new SeriesHeader() + { + TransitionModel = new NoneTransitionModel(), + IntegrityCheck = + "SHA256: 5556fb65f8bf9eddb3ace1329c9a6aeedd4833409965aeee3e6b61ed21f47858", + PoseCount = 2, + StartInstant = validTime, + StopInstant = validTime + }; + + var outerFrame = new LtpEnuSpecification(new TangentPointPosition() { Latitude = 48, Longitude = -122, HeightInMeters = 5 }); + + var trailer = new SeriesTrailer() + { + IntegrityCheck = + "SHA256: 5556fb65f8bf9eddb3ace1329c9a6aeedd4833409965aeee3e6b61ed21f47859", + PoseCount = 2 + }; + + var innerFrames = new List() + { + new TranslateRotateSpecification(new DataTypes.UnitVector3(0.1f, 0.2f, 0.3f), new UnitQuaternion() {X = 0.692f, Y = 0.691f, Z = 0.141f, W = 0.14f}), + new TranslateRotateSpecification(new DataTypes.UnitVector3(0.4f, 0.5f, 0.6f), new UnitQuaternion() {X = 0.692f, Y = 0.691f, Z = 0.141f, W = 0.14f}), + new TranslateRotateSpecification(new DataTypes.UnitVector3(0.7f, 0.8f, 0.9f), new UnitQuaternion() {X = 0.692f, Y = 0.691f, Z = 0.141f, W = 0.14f}), + }; + + var sdu = new RegularSeriesSdu(header, new GeoPoseDuration() { NumericDuration = 1000 }, outerFrame, innerFrames, trailer); + var result = sdu.ToString(); + Assert.That(result, !Is.Empty); + Assert.That(result, Is.EqualTo("Header:[TransitionModel:[Authority:/Ethar.GeoPose/1.0, Id:none], PoseCount:2, StartInstant:16534234327, StopInstant:16534234327, IntegrityCheck:SHA256: 5556fb65f8bf9eddb3ace1329c9a6aeedd4833409965aeee3e6b61ed21f47858], InterPoseDuration:[NumericDuration:1000], OuterFrame:[Authority:/Ethar.GeoPose/1.0, Id:LTP-ENU], InnerFrameSeriesCount:3, Trailer:[PoseCount:2, IntegrityCheck:SHA256: 5556fb65f8bf9eddb3ace1329c9a6aeedd4833409965aeee3e6b61ed21f47859]")); + } + + [Test] + public void CanConvertRegularSeriesSduNew() + { + var sdu = new RegularSeriesSdu(); + var result = sdu.ToString(); + Assert.That(result, !Is.Empty); + Assert.That(result, Is.EqualTo("Header:[TransitionModel:[], PoseCount:0, StartInstant:0, StopInstant:0, IntegrityCheck:], InterPoseDuration:[NumericDuration:0], OuterFrame:[], InnerFrameSeriesCount:0, Trailer:[PoseCount:0, IntegrityCheck:]")); + } + } +} diff --git a/Ethar.GeoPose.UnitTests/StreamElementSduTests.cs b/Ethar.GeoPose.UnitTests/StreamElementSduTests.cs new file mode 100644 index 0000000..07250ac --- /dev/null +++ b/Ethar.GeoPose.UnitTests/StreamElementSduTests.cs @@ -0,0 +1,124 @@ +using Ethar.GeoPose.Authority.FrameSpecifications; +using Ethar.GeoPose.DataTypes; +using Ethar.GeoPose.StructuralDataUnits; +using Newtonsoft.Json; +using NUnit.Framework; + +namespace Ethar.GeoPose.UnitTests +{ + [TestFixture] + public class StreamElementSduTests : UnitTestBase + { + [TestCase(16534234327, "\"/Ethar.GeoPose/1.0\"", "\"Translate-Rotate\"", + "\"translation.x=0.1&translation.y=0.2&translation.z=0.3&rotation.x=0.692&rotation.y=0.691&rotation.z=0.141&rotation.w=0.14\"")] + public void CanCorrectlyDeserializeStreamElementSdu(long validTime, string authority, string innerFrameId, string innerFrameParameters) + { + var json = + "{" + + "\"streamElement\":" + + "{" + + "\"frame\":" + + "{" + + $"\"authority\": {authority}," + + $"\"id\": {innerFrameId}," + + $"\"parameters\": {innerFrameParameters}" + + "}," + + $"\"validTime\": {validTime}," + + "}" + + "}"; + + var sdu = JsonConvert.DeserializeObject(json); + + Assert.That(sdu.StreamElement.ValidTime, Is.EqualTo(validTime)); + + Assert.That(sdu.StreamElement.Frame is TranslateRotateSpecification); + + var translateRotate = sdu.StreamElement.Frame as TranslateRotateSpecification; + + Assert.That(translateRotate.Translation.X, Is.EqualTo(0.1f)); + Assert.That(translateRotate.Translation.Y, Is.EqualTo(0.2f)); + Assert.That(translateRotate.Translation.Z, Is.EqualTo(0.3f)); + Assert.That(translateRotate.Rotation.X, Is.EqualTo(0.692f)); + Assert.That(translateRotate.Rotation.Y, Is.EqualTo(0.691f)); + Assert.That(translateRotate.Rotation.Z, Is.EqualTo(0.141f)); + Assert.That(translateRotate.Rotation.W, Is.EqualTo(0.14f)); + } + + [TestCase(16534234327, "/Ethar.GeoPose/1.0", "Translate-Rotate", + "\"translation.x=0.1&translation.y=0.2&translation.z=0.3&rotation.x=0.692&rotation.y=0.691&rotation.z=0.141&rotation.w=0.14\"")] + public void CanCorrectlySerializeStreamElementSdu(long validTime, string authority, string innerFrameId, string innerFrameParameters) + { + var frameAndTime = new FrameAndTimeElement() + { + Frame = new TranslateRotateSpecification(new DataTypes.UnitVector3(0.1f, 0.2f, 0.3f), + new UnitQuaternion() { X = 0.692f, Y = 0.691f, Z = 0.141f, W = 0.14f }), + ValidTime = validTime + }; + + var sdu = new StreamElementSdu(frameAndTime); + + var expected = + "{" + + "\"streamElement\":" + + "{" + + "\"frame\":" + + "{" + + $"\"authority\":\"{authority}\"," + + $"\"id\":\"{innerFrameId}\"," + + $"\"parameters\":{innerFrameParameters}" + + "}," + + $"\"validTime\":{validTime}" + + "}" + + "}"; + + var json = JsonConvert.SerializeObject(sdu); + + Assert.That(json, Is.EqualTo(expected)); + } + + [TestCase(16534234327)] + public void CanCorrectlyMakeARoundTripConversion(long validTime) + { + var frameAndTime = new FrameAndTimeElement() + { + Frame = new TranslateRotateSpecification(new DataTypes.UnitVector3(0.1f, 0.2f, 0.3f), + new UnitQuaternion() { X = 0.692f, Y = 0.691f, Z = 0.141f, W = 0.14f }), + ValidTime = validTime + }; + + var sdu = new StreamElementSdu(frameAndTime); + + var json = JsonConvert.SerializeObject(sdu); + + var converted = JsonConvert.DeserializeObject(json); + + Assert.That(converted, Is.EqualTo(sdu)); + } + + [TestCase(16534234327, "/Ethar.GeoPose/1.0", "Translate-Rotate", + "\"translation.x=0.1&translation.y=0.2&translation.z=0.3&rotation.x=0.692&rotation.y=0.691&rotation.z=0.141&rotation.w=0.14\"")] + public void CanConvertStreamElementSduToString(long validTime, string authority, string innerFrameId, string innerFrameParameters) + { + var frameAndTime = new FrameAndTimeElement() + { + Frame = new TranslateRotateSpecification(new DataTypes.UnitVector3(0.1f, 0.2f, 0.3f), + new UnitQuaternion() { X = 0.692f, Y = 0.691f, Z = 0.141f, W = 0.14f }), + ValidTime = validTime + }; + + var sdu = new StreamElementSdu(frameAndTime); + var result = sdu.ToString(); + Assert.That(result, !Is.Empty); + Assert.That(result, Is.EqualTo("StreamElement:[Frame:[Authority:/Ethar.GeoPose/1.0, Id:Translate-Rotate], ValidTime:16534234327]")); + } + + [Test] + public void CanConvertStreamElementSduNew() + { + var sdu = new StreamElementSdu(); + var result = sdu.ToString(); + Assert.That(result, !Is.Empty); + Assert.That(result, Is.EqualTo("StreamElement:[Frame:[], ValidTime:0]")); + } + } +} diff --git a/Ethar.GeoPose.UnitTests/StreamHeaderSduTests.cs b/Ethar.GeoPose.UnitTests/StreamHeaderSduTests.cs new file mode 100644 index 0000000..4ccd368 --- /dev/null +++ b/Ethar.GeoPose.UnitTests/StreamHeaderSduTests.cs @@ -0,0 +1,103 @@ +using Ethar.GeoPose.Authority.FrameSpecifications; +using Ethar.GeoPose.Authority.TransitionModels; +using Ethar.GeoPose.DataTypes; +using Ethar.GeoPose.StructuralDataUnits; +using Newtonsoft.Json; +using NUnit.Framework; + +namespace Ethar.GeoPose.UnitTests +{ + [TestFixture] + public class StreamHeaderSduTests : UnitTestBase + { + [TestCase("\"/Ethar.GeoPose/1.0\"", "\"LTP-ENU\"", "\"longitude=-122.0000000&latitude=48.0000000&heightInMeters=5.000\"")] + public void CanCorrectlyDeserializeStreamHeaderSdu(string authority, string outerFrameId, string outerFrameParams) + { + var json = + "{" + + $"\"transitionModel\":" + + "{" + + $"\"authority\": {authority}," + + $"\"id\": \"none\"," + + $"\"parameters\": \"\"" + + "}," + + "\"outerFrame\":" + + "{" + + $"\"authority\": {authority}," + + $"\"id\": {outerFrameId}," + + $"\"parameters\": {outerFrameParams}" + + "}," + + "}"; + + var sdu = JsonConvert.DeserializeObject(json); + + Assert.That(sdu.TransitionModel is NoneTransitionModel); + + Assert.That(sdu.OuterFrame is LtpEnuSpecification); + var outerFrame = (LtpEnuSpecification)sdu.OuterFrame; + + Assert.That(outerFrame.Position.Latitude, Is.EqualTo(48)); + Assert.That(outerFrame.Position.Longitude, Is.EqualTo(-122)); + Assert.That(outerFrame.Position.HeightInMeters, Is.EqualTo(5)); + } + + [TestCase("/Ethar.GeoPose/1.0", "LTP-ENU", "\"latitude=48&longitude=-122&heightInMeters=5\"")] + public void CanCorrectlySerializeStreamHeaderSdu(string authority, string outerFrameId, string outerFrameParams) + { + var outerFrame = new LtpEnuSpecification(new TangentPointPosition() { Latitude = 48, Longitude = -122, HeightInMeters = 5 }); + var sdu = new StreamHeaderSdu(new NoneTransitionModel(), outerFrame); + + var expected = + "{" + + $"\"transitionModel\":" + + "{" + + $"\"authority\":\"{authority}\"," + + $"\"id\":\"none\"," + + $"\"parameters\":\"\"" + + "}," + + "\"outerFrame\":" + + "{" + + $"\"authority\":\"{authority}\"," + + $"\"id\":\"{outerFrameId}\"," + + $"\"parameters\":{outerFrameParams}" + + "}" + + "}"; + + var json = JsonConvert.SerializeObject(sdu); + Assert.That(json, Is.EqualTo(expected)); + } + + [TestCase("/Ethar.GeoPose/1.0", "LTP-ENU", "{\"latitude\":48,\"longitude\":-122,\"heightInMeters\":5}")] + public void CanCorrectlyMakeARoundTripConversion(string authority, string outerFrameId, string outerFrameParams) + { + var position = JsonConvert.DeserializeObject(outerFrameParams); + var outerFrame = new LtpEnuSpecification(position); + var sdu = new StreamHeaderSdu(new NoneTransitionModel(), outerFrame); + var json = JsonConvert.SerializeObject(sdu); + + var converted = JsonConvert.DeserializeObject(json); + Assert.That(converted, Is.EqualTo(sdu)); + } + + [TestCase("/Ethar.GeoPose/1.0", "LTP-ENU", "{\"latitude\":48,\"longitude\":-122,\"heightInMeters\":5}")] + public void CanConvertStreamHeaderSduToString(string authority, string outerFrameId, string outerFrameParams) + { + var position = JsonConvert.DeserializeObject(outerFrameParams); + var outerFrame = new LtpEnuSpecification(position); + var sdu = new StreamHeaderSdu(new NoneTransitionModel(), outerFrame); + + var result = sdu.ToString(); + Assert.That(result, !Is.Empty); + Assert.That(result, Is.EqualTo("TransitionModel:[Authority:/Ethar.GeoPose/1.0, Id:none], OuterFrame:[Authority:/Ethar.GeoPose/1.0, Id:LTP-ENU]")); + } + + [Test] + public void CanConvertStreamHeaderSduNew() + { + var sdu = new StreamHeaderSdu(); + var result = sdu.ToString(); + Assert.That(result, !Is.Empty); + Assert.That(result, Is.EqualTo("TransitionModel:[], OuterFrame:[]")); + } + } +} diff --git a/Ethar.GeoPose.UnitTests/UnitTestBase.cs b/Ethar.GeoPose.UnitTests/UnitTestBase.cs new file mode 100644 index 0000000..07d4364 --- /dev/null +++ b/Ethar.GeoPose.UnitTests/UnitTestBase.cs @@ -0,0 +1,25 @@ +using System.Net.Http; +using Ethar.GeoPose.Authority; +using NUnit.Framework; + +namespace Ethar.GeoPose.UnitTests +{ + public class UnitTestBase + { + protected HttpClient Client; + + [SetUp] + public void Setup() + { + this.Client = new HttpClient(); + AuthorityProvider.RegisterAuthority(new EtharGeoPoseAuthority()); + } + + + [TearDown] + public void Teardown() + { + AuthorityProvider.UnregisterAuthority("/Ethar.GeoPose/1.0"); + } + } +} \ No newline at end of file diff --git a/Ethar.GeoPose.sln b/Ethar.GeoPose.sln new file mode 100644 index 0000000..84a87d1 --- /dev/null +++ b/Ethar.GeoPose.sln @@ -0,0 +1,49 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.3.32929.385 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ethar.GeoPose", "Ethar.GeoPose\Ethar.GeoPose.csproj", "{FC0BE663-94B3-4BCC-8B32-15C28A0F2EAE}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ethar.GeoPose.UnitTests", "Ethar.GeoPose.UnitTests\Ethar.GeoPose.UnitTests.csproj", "{900D6AB2-A765-4B6A-82AB-AC26B006A427}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ethar.GeoPose.Authority.UnitTests", "Ethar.GeoPose.Authority.UnitTests\Ethar.GeoPose.Authority.UnitTests.csproj", "{73475777-CE6F-4EAA-9899-E69D5A139E98}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ethar.GeoPose.Authority", "Ethar.GeoPose.Authority\Ethar.GeoPose.Authority.csproj", "{6A8617A3-0F45-4305-A775-F8040B763C18}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ethar.GeoPose.Examples", "Ethar.GeoPose.Examples\Ethar.GeoPose.Examples.csproj", "{83B92AB0-1E28-43C0-AC2A-03B8FF4B0A5D}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {FC0BE663-94B3-4BCC-8B32-15C28A0F2EAE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FC0BE663-94B3-4BCC-8B32-15C28A0F2EAE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FC0BE663-94B3-4BCC-8B32-15C28A0F2EAE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FC0BE663-94B3-4BCC-8B32-15C28A0F2EAE}.Release|Any CPU.Build.0 = Release|Any CPU + {900D6AB2-A765-4B6A-82AB-AC26B006A427}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {900D6AB2-A765-4B6A-82AB-AC26B006A427}.Debug|Any CPU.Build.0 = Debug|Any CPU + {900D6AB2-A765-4B6A-82AB-AC26B006A427}.Release|Any CPU.ActiveCfg = Release|Any CPU + {900D6AB2-A765-4B6A-82AB-AC26B006A427}.Release|Any CPU.Build.0 = Release|Any CPU + {73475777-CE6F-4EAA-9899-E69D5A139E98}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {73475777-CE6F-4EAA-9899-E69D5A139E98}.Debug|Any CPU.Build.0 = Debug|Any CPU + {73475777-CE6F-4EAA-9899-E69D5A139E98}.Release|Any CPU.ActiveCfg = Release|Any CPU + {73475777-CE6F-4EAA-9899-E69D5A139E98}.Release|Any CPU.Build.0 = Release|Any CPU + {6A8617A3-0F45-4305-A775-F8040B763C18}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6A8617A3-0F45-4305-A775-F8040B763C18}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6A8617A3-0F45-4305-A775-F8040B763C18}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6A8617A3-0F45-4305-A775-F8040B763C18}.Release|Any CPU.Build.0 = Release|Any CPU + {83B92AB0-1E28-43C0-AC2A-03B8FF4B0A5D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {83B92AB0-1E28-43C0-AC2A-03B8FF4B0A5D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {83B92AB0-1E28-43C0-AC2A-03B8FF4B0A5D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {83B92AB0-1E28-43C0-AC2A-03B8FF4B0A5D}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {23152CAB-B1BA-453E-AD34-9F4534E80E44} + EndGlobalSection +EndGlobal diff --git a/Ethar.GeoPose/Authorities/AuthorityProvider.cs b/Ethar.GeoPose/Authorities/AuthorityProvider.cs new file mode 100644 index 0000000..eb11bb3 --- /dev/null +++ b/Ethar.GeoPose/Authorities/AuthorityProvider.cs @@ -0,0 +1,53 @@ +// +// Copyright (c) Ethar. All rights reserved. +// + +namespace Ethar.GeoPose.Authority +{ + using System.Collections.Generic; + using System.Linq; + using Ethar.GeoPose.Exceptions; + using Ethar.GeoPose.Extensions; + + /// + /// A static management class that provides the application with information about which authorities are implemented, including registration management. + /// + public static class AuthorityProvider + { + private static readonly Dictionary AuthorityDictionary; + + static AuthorityProvider() + { + AuthorityDictionary = new Dictionary(); + } + + /// + /// Gets a list that contains the supported authorities. + /// + public static IList Authorities => AuthorityDictionary.Values.ToList(); + + /// + /// Adds an to the list of supported authorities. + /// + /// The authority to add. + public static void RegisterAuthority(IAuthority authority) => AuthorityDictionary.TryAdd(authority.AuthorityName, authority); + + /// + /// Removes an from the list of supported authorities. + /// + /// The name authority to remove. + public static void UnregisterAuthority(string authorityName) => AuthorityDictionary.Remove(authorityName); + + /// + /// Gets the authority associated with the identifier, if it exists in the supported authorities. + /// + /// The name of the authority to get. + /// The authority if it exists. + /// Thrown if the authority does not exist in the supported authorities. + public static IAuthority GetAuthority(string authorityName) + { + AuthorityDictionary.TryGetValue(authorityName, out var authority); + return authority ?? throw new AuthorityNotSupportedException($"The authority {authorityName} is not supported. Consider adding it with the UseAuthority method."); + } + } +} diff --git a/Ethar.GeoPose/Authorities/IAuthority.cs b/Ethar.GeoPose/Authorities/IAuthority.cs new file mode 100644 index 0000000..ec627be --- /dev/null +++ b/Ethar.GeoPose/Authorities/IAuthority.cs @@ -0,0 +1,76 @@ +// +// Copyright (c) Ethar. All rights reserved. +// + +namespace Ethar.GeoPose.Authority +{ + using System; + using Ethar.GeoPose.Exceptions; + using Ethar.GeoPose.Interfaces; + using Ethar.GeoPose.TransitionModels; + using Newtonsoft.Json.Linq; + + /// + /// Interface that represents a GeoPose authority. + /// + public interface IAuthority + { + /// + /// Gets the name of the authority. + /// + string AuthorityName { get; } + + /// + /// Converts json to an from the authority. + /// + /// The json to convert. + /// An from the authority. + /// + /// Thrown when the frame specification has not been implemented or does not exist in the authority. + /// + IFrameSpecification ConvertJsonToFrameSpec(JObject jsonObject); + + /// + /// Converts an from the authority to json. + /// + /// The to convert. + /// A json representation of an from the authority. + /// + /// Thrown when the frame specification has not been implemented or does not exist in the authority. + /// + /// + /// May be thrown if the frame specification contains invalid parameters. + /// + JObject ConvertFrameSpecToJson(IFrameSpecification frameSpec); + + /// + /// Converts json to an from the authority. + /// + /// The json to convert. + /// An from the authority. + /// + /// Thrown when the transition model has not been implemented or does not exist in the authority. + /// + TransitionModel ConvertJsonToTransitionModel(JObject jsonObject); + + /// + /// Converts an from the authority to json. + /// + /// The to convert. + /// A json representation of an from the authority. + /// + /// Thrown when the transition model has not been implemented or does not exist in the authority. + /// + /// + /// May be thrown if the transition model contains invalid parameters. + /// + JObject ConvertTransitionModelToJson(TransitionModel transitionModel); + + /// + /// Method to check if the is extrinsic. + /// + /// The to check. + /// True if the is extrinsic, false otherwise. + bool IsFrameSpecificationExtrinsic(IFrameSpecification frameSpec); + } +} \ No newline at end of file diff --git a/Ethar.GeoPose/DataTypes/FrameAndTimeElement.cs b/Ethar.GeoPose/DataTypes/FrameAndTimeElement.cs new file mode 100644 index 0000000..a361ec3 --- /dev/null +++ b/Ethar.GeoPose/DataTypes/FrameAndTimeElement.cs @@ -0,0 +1,87 @@ +// +// Copyright (c) Ethar. All rights reserved. +// + +namespace Ethar.GeoPose.DataTypes +{ + using System; + using Ethar.GeoPose.FrameSpecifications; + using Newtonsoft.Json; + + /// + /// A construct that represents a frame and time used in a chain of poses. + /// + /// Requirements derived from Figure 13 in section 7.2.4 of version 1.0.0 of the GeoPose spec http://www.opengis.net/doc/DIS/geopose/1.0. + /// + public struct FrameAndTimeElement : IEquatable + { + /// + /// Initializes a new instance of the struct. + /// + /// The . + /// The valid time. + public FrameAndTimeElement(BaseFrameSpecification frame, long validTime) + { + this.Frame = frame; + this.ValidTime = validTime; + } + + /// + /// Gets or sets the frame specification. + /// + [JsonProperty("frame")] + public BaseFrameSpecification Frame { get; set; } + + /// + /// Gets or sets the valid time. + /// + [JsonProperty("validTime")] + public long ValidTime { get; set; } + + /// + /// Performs an equality comparison on two objects of type . + /// + /// The first item. + /// The second item. + /// Whether the two items are equal. + public static bool operator ==(FrameAndTimeElement a, FrameAndTimeElement b) => a.Equals(b); + + /// + /// Performs an equality comparison on two objects of type . + /// + /// The first item. + /// The second item. + /// Whether the two items are not equal. + public static bool operator !=(FrameAndTimeElement a, FrameAndTimeElement b) => !(a == b); + + /// + public bool Equals(FrameAndTimeElement other) + { + return this.ValidTime.Equals(other.ValidTime) + && this.Frame.Equals(other.Frame); + } + + /// + public override bool Equals(object obj) + { + return obj is FrameAndTimeElement equatable && this.Equals(equatable); + } + + /// + public override int GetHashCode() + { + unchecked + { + var hashcode = this.ValidTime.GetHashCode(); + hashcode = (hashcode * 397) ^ this.Frame.GetHashCode(); + return hashcode; + } + } + + /// + public override string ToString() + { + return $"Frame:[{this.Frame}], ValidTime:{this.ValidTime}"; + } + } +} \ No newline at end of file diff --git a/Ethar.GeoPose/DataTypes/FrameTransformIndexPair.cs b/Ethar.GeoPose/DataTypes/FrameTransformIndexPair.cs new file mode 100644 index 0000000..8d824ab --- /dev/null +++ b/Ethar.GeoPose/DataTypes/FrameTransformIndexPair.cs @@ -0,0 +1,89 @@ +// +// Copyright (c) Ethar. All rights reserved. +// + +namespace Ethar.GeoPose.DataTypes +{ + using System; + using System.Collections.Generic; + using System.Linq; + using Ethar.GeoPose.JsonConversion; + using Newtonsoft.Json; + + /// + /// A construct that represents a outer frame index and inner frame index pair in a graph. + /// + /// Requirements derived from Figure 12 of section 7.2.4 of version 1.0.0 of the GeoPose spec http://www.opengis.net/doc/DIS/geopose/1.0. + /// + [JsonConverter(typeof(FrameTransformIndexPairJsonConverter))] + public struct FrameTransformIndexPair : IEquatable + { + /// + /// Initializes a new instance of the struct. + /// + /// Array where the first item is the outer frame index and the last item is the inner frame index. + public FrameTransformIndexPair(IEnumerable link) + { + this.OuterFrameIndex = link.First(); + this.InnerFrameIndex = link.Last(); + } + + /// + /// Gets or sets the outer frame index. + /// + [JsonProperty("outerFrameIndex")] + public int OuterFrameIndex { get; set; } + + /// + /// Gets or sets the inner frame index. + /// + [JsonProperty("innerFrameIndex")] + public int InnerFrameIndex { get; set; } + + /// + /// Performs an equality comparison on two objects of type . + /// + /// The first item. + /// The second item. + /// Whether the two items are equal. + public static bool operator ==(FrameTransformIndexPair a, FrameTransformIndexPair b) => a.Equals(b); + + /// + /// Performs an equality comparison on two objects of type . + /// + /// The first item. + /// The second item. + /// Whether the two items are not equal. + public static bool operator !=(FrameTransformIndexPair a, FrameTransformIndexPair b) => !(a == b); + + /// + public bool Equals(FrameTransformIndexPair other) + { + return this.OuterFrameIndex.Equals(other.OuterFrameIndex) + && this.InnerFrameIndex.Equals(other.InnerFrameIndex); + } + + /// + public override bool Equals(object obj) + { + return obj is FrameTransformIndexPair equatable && this.Equals(equatable); + } + + /// + public override int GetHashCode() + { + unchecked + { + var hashcode = this.OuterFrameIndex.GetHashCode(); + hashcode = (hashcode * 397) ^ this.InnerFrameIndex.GetHashCode(); + return hashcode; + } + } + + /// + public override string ToString() + { + return $"OuterFrameIndex:{this.OuterFrameIndex}, InnerFrameIndex:{this.InnerFrameIndex}"; + } + } +} diff --git a/Ethar.GeoPose/DataTypes/GeoposeDuration.cs b/Ethar.GeoPose/DataTypes/GeoposeDuration.cs new file mode 100644 index 0000000..8dc8902 --- /dev/null +++ b/Ethar.GeoPose/DataTypes/GeoposeDuration.cs @@ -0,0 +1,69 @@ +// +// Copyright (c) Ethar. All rights reserved. +// + +namespace Ethar.GeoPose.DataTypes +{ + using System; + using Ethar.GeoPose.JsonConversion; + using Newtonsoft.Json; + + /// + /// A construct that represents a duration of time in milliseconds. + /// + /// Requirements derived from Requirement 11 in section 8.3.3 of version 1.0.0 of the GeoPose spec http://www.opengis.net/doc/DIS/geopose/1.0. + /// + [JsonConverter(typeof(GeoposeDurationJsonConverter))] + public struct GeoPoseDuration : IEquatable + { + /// + /// Gets or sets the duration in milliseconds. + /// + [JsonProperty("numericDuration")] + public long NumericDuration { get; set; } + + /// + /// Performs an equality comparison on two objects of type . + /// + /// The first item. + /// The second item. + /// Whether the two items are equal. + public static bool operator ==(GeoPoseDuration a, GeoPoseDuration b) => a.Equals(b); + + /// + /// Performs an equality comparison on two objects of type . + /// + /// The first item. + /// The second item. + /// Whether the two items are not equal. + public static bool operator !=(GeoPoseDuration a, GeoPoseDuration b) => !(a == b); + + /// + public bool Equals(GeoPoseDuration other) + { + return this.NumericDuration.Equals(other.NumericDuration); + } + + /// + public override bool Equals(object obj) + { + return obj is GeoPoseDuration equatable && this.Equals(equatable); + } + + /// + public override int GetHashCode() + { + unchecked + { + var hashcode = this.NumericDuration.GetHashCode(); + return hashcode; + } + } + + /// + public override string ToString() + { + return $"NumericDuration:{this.NumericDuration}"; + } + } +} \ No newline at end of file diff --git a/Ethar.GeoPose/DataTypes/SeriesHeader.cs b/Ethar.GeoPose/DataTypes/SeriesHeader.cs new file mode 100644 index 0000000..b95bbf4 --- /dev/null +++ b/Ethar.GeoPose/DataTypes/SeriesHeader.cs @@ -0,0 +1,117 @@ +// +// Copyright (c) Ethar. All rights reserved. +// + +namespace Ethar.GeoPose.DataTypes +{ + using System; + using Ethar.GeoPose.TransitionModels; + using Newtonsoft.Json; + + /// + /// A construct that represents the series trailer. + /// + /// Requirements derived from figure 13 in section 7.2.4 of version 1.0.0 of the GeoPose spec http://www.opengis.net/doc/DIS/geopose/1.0. + /// + public struct SeriesHeader : IEquatable + { + /// + /// Initializes a new instance of the struct. + /// + /// The transition model. + /// The pose count. + /// The Unix timestamp that represents the start instant. + /// The Unix timestamp that represents the stop instant. + /// The integrity check. + public SeriesHeader(TransitionModel transitionModel, int poseCount, long startInstant, long stopInstant, string integrityCheck) + { + this.TransitionModel = transitionModel; + this.PoseCount = poseCount; + this.StartInstant = startInstant; + this.StopInstant = stopInstant; + this.IntegrityCheck = integrityCheck; + } + + /// + /// Gets or sets the transition model. + /// + [JsonProperty("transitionModel")] + public TransitionModel TransitionModel { get; set; } + + /// + /// Gets or sets the number of poses in the series. + /// + [JsonProperty("poseCount")] + public int PoseCount { get; set; } + + /// + /// Gets or sets the Unix timestamp that represents the start instant. + /// + [JsonProperty("startInstant")] + public long StartInstant { get; set; } + + /// + /// Gets or sets the Unix timestamp that represents the stop instant. + /// + [JsonProperty("stopInstant")] + public long StopInstant { get; set; } + + /// + /// Gets or sets the integrity check. + /// + [JsonProperty("integrityCheck")] + public string IntegrityCheck { get; set; } + + /// + /// Performs an equality comparison on two objects of type . + /// + /// The first item. + /// The second item. + /// Whether the two items are equal. + public static bool operator ==(SeriesHeader a, SeriesHeader b) => a.Equals(b); + + /// + /// Performs an equality comparison on two objects of type . + /// + /// The first item. + /// The second item. + /// Whether the two items are not equal. + public static bool operator !=(SeriesHeader a, SeriesHeader b) => !(a == b); + + /// + public bool Equals(SeriesHeader other) + { + return this.TransitionModel.Equals(other.TransitionModel) + && this.PoseCount.Equals(other.PoseCount) + && this.StartInstant.Equals(other.StartInstant) + && this.StopInstant.Equals(other.StopInstant) + && this.IntegrityCheck.Equals(other.IntegrityCheck); + } + + /// + public override bool Equals(object obj) + { + return obj is SeriesHeader equatable && this.Equals(equatable); + } + + /// + public override int GetHashCode() + { + unchecked + { + var hashcode = this.PoseCount.GetHashCode(); + hashcode = (hashcode * 397) ^ this.IntegrityCheck.GetHashCode(); + hashcode = (hashcode * 397) ^ this.StartInstant.GetHashCode(); + hashcode = (hashcode * 397) ^ this.StopInstant.GetHashCode(); + hashcode = (hashcode * 397) ^ this.TransitionModel.GetHashCode(); + return hashcode; + } + } + + /// + public override string ToString() + { + return $"TransitionModel:[{this.TransitionModel}], PoseCount:{this.PoseCount}, StartInstant:{this.StartInstant}, StopInstant:{this.StopInstant}, IntegrityCheck:{this.IntegrityCheck}"; + } + } +} diff --git a/Ethar.GeoPose/DataTypes/SeriesTrailer.cs b/Ethar.GeoPose/DataTypes/SeriesTrailer.cs new file mode 100644 index 0000000..325a82a --- /dev/null +++ b/Ethar.GeoPose/DataTypes/SeriesTrailer.cs @@ -0,0 +1,86 @@ +// +// Copyright (c) Ethar. All rights reserved. +// + +namespace Ethar.GeoPose.DataTypes +{ + using System; + using Newtonsoft.Json; + + /// + /// A construct that represents the series trailer. + /// + /// Requirements derived from figure 13 in section 7.2.4 of version 1.0.0 of the GeoPose spec http://www.opengis.net/doc/DIS/geopose/1.0. + /// + public struct SeriesTrailer : IEquatable + { + /// + /// Initializes a new instance of the struct. + /// + /// The pose count. + /// The integrity check. + public SeriesTrailer(int poseCount, string integrityCheck) + { + this.PoseCount = poseCount; + this.IntegrityCheck = integrityCheck; + } + + /// + /// Gets or sets the number of poses in the series. + /// + [JsonProperty("poseCount")] + public int PoseCount { get; set; } + + /// + /// Gets or sets the integrity check. + /// + [JsonProperty("integrityCheck")] + public string IntegrityCheck { get; set; } + + /// + /// Performs an equality comparison on two objects of type . + /// + /// The first item. + /// The second item. + /// Whether the two items are equal. + public static bool operator ==(SeriesTrailer a, SeriesTrailer b) => a.Equals(b); + + /// + /// Performs an equality comparison on two objects of type . + /// + /// The first item. + /// The second item. + /// Whether the two items are not equal. + public static bool operator !=(SeriesTrailer a, SeriesTrailer b) => !(a == b); + + /// + public bool Equals(SeriesTrailer other) + { + return this.PoseCount.Equals(other.PoseCount) + && this.IntegrityCheck.Equals(other.IntegrityCheck); + } + + /// + public override bool Equals(object obj) + { + return obj is SeriesTrailer equatable && this.Equals(equatable); + } + + /// + public override int GetHashCode() + { + unchecked + { + var hashcode = this.PoseCount.GetHashCode(); + hashcode = (hashcode * 397) ^ this.IntegrityCheck.GetHashCode(); + return hashcode; + } + } + + /// + public override string ToString() + { + return $"PoseCount:{this.PoseCount}, IntegrityCheck:{this.IntegrityCheck}"; + } + } +} diff --git a/Ethar.GeoPose/DataTypes/TangentPointPosition.cs b/Ethar.GeoPose/DataTypes/TangentPointPosition.cs new file mode 100644 index 0000000..3bf664d --- /dev/null +++ b/Ethar.GeoPose/DataTypes/TangentPointPosition.cs @@ -0,0 +1,96 @@ +// +// Copyright (c) Ethar. All rights reserved. +// + +namespace Ethar.GeoPose.DataTypes +{ + using System; + using Newtonsoft.Json; + + /// + /// A construct that represents a location represented by latitude and longitude in decimal degrees and a height in meters. + /// + /// Requirements derived from figure 13 in section 7.2.4 of version 1.0.0 of the GeoPose spec http://www.opengis.net/doc/DIS/geopose/1.0. + /// + public struct TangentPointPosition : IEquatable + { + /// + /// Initializes a new instance of the struct. + /// + /// The latitude in decimal degrees. Valid range is from -90 to 90. + /// The longitude in decimal degrees. Valid range is from -180 to 180. + /// The height in meters. + public TangentPointPosition(float latitude, float longitude, float heightInMeters) + { + this.Latitude = latitude; + this.Longitude = longitude; + this.HeightInMeters = heightInMeters; + } + + /// + /// Gets or sets the latitude in decimal degrees. + /// + [JsonProperty("lat")] + public float Latitude { get; set; } + + /// + /// Gets or sets the longitude in decimal degrees. + /// + [JsonProperty("lon")] + public float Longitude { get; set; } + + /// + /// Gets or sets the height in meters. + /// + [JsonProperty("h")] + public float HeightInMeters { get; set; } + + /// + /// Performs an equality comparison on two objects of type . + /// + /// The first item. + /// The second item. + /// Whether the two items are equal. + public static bool operator ==(TangentPointPosition a, TangentPointPosition b) => a.Equals(b); + + /// + /// Performs an equality comparison on two objects of type . + /// + /// The first item. + /// The second item. + /// Whether the two items are not equal. + public static bool operator !=(TangentPointPosition a, TangentPointPosition b) => !(a == b); + + /// + public bool Equals(TangentPointPosition other) + { + return this.Latitude.Equals(other.Latitude) + && this.Longitude.Equals(other.Longitude) + && this.HeightInMeters.Equals(other.HeightInMeters); + } + + /// + public override bool Equals(object obj) + { + return obj is TangentPointPosition equatable && this.Equals(equatable); + } + + /// + public override int GetHashCode() + { + unchecked + { + var hashcode = this.Longitude.GetHashCode(); + hashcode = (hashcode * 397) ^ this.Latitude.GetHashCode(); + hashcode = (hashcode * 397) ^ this.HeightInMeters.GetHashCode(); + return hashcode; + } + } + + /// + public override string ToString() + { + return $"Latitude:{this.Latitude}, Longitude:{this.Longitude}, HeightInMeters:{this.HeightInMeters}"; + } + } +} \ No newline at end of file diff --git a/Ethar.GeoPose/DataTypes/UnitQuaternion.cs b/Ethar.GeoPose/DataTypes/UnitQuaternion.cs new file mode 100644 index 0000000..02149e0 --- /dev/null +++ b/Ethar.GeoPose/DataTypes/UnitQuaternion.cs @@ -0,0 +1,125 @@ +// +// Copyright (c) Ethar. All rights reserved. +// + +namespace Ethar.GeoPose.DataTypes +{ + using System; + using Newtonsoft.Json; + + /// + /// A data type that represents a quaternion for serialization. + /// + public struct UnitQuaternion : IEquatable + { + /// + /// Initializes a new instance of the struct. + /// + /// The x value. + /// The y value. + /// The z value. + /// The w value. + public UnitQuaternion(float x, float y, float z, float w) + { + this.X = x; + this.Y = y; + this.Z = z; + this.W = w; + } + + /// + /// Initializes a new instance of the struct from the given vector and rotation parts. + /// + /// The vector part of the Quaternion. + /// The rotation part of the Quaternion. + public UnitQuaternion(UnitVector3 vectorPart, float scalarPart) + { + this.X = vectorPart.X; + this.Y = vectorPart.Y; + this.Z = vectorPart.Z; + this.W = scalarPart; + } + + /// + /// Gets a Quaternion representing no rotation. + /// + public static UnitQuaternion Identity + { + get { return new UnitQuaternion(0, 0, 0, 1); } + } + + /// + /// Gets or sets the x value. + /// + [JsonProperty("x")] + public float X { get; set; } + + /// + /// Gets or sets the y value. + /// + [JsonProperty("y")] + public float Y { get; set; } + + /// + /// Gets or sets the z value. + /// + [JsonProperty("z")] + public float Z { get; set; } + + /// + /// Gets or sets the w value. + /// + [JsonProperty("w")] + public float W { get; set; } + + /// + /// Performs an equality comparison on two objects of type . + /// + /// The first item. + /// The second item. + /// Whether the two items are equal. + public static bool operator ==(UnitQuaternion a, UnitQuaternion b) => a.Equals(b); + + /// + /// Performs an equality comparison on two objects of type . + /// + /// The first item. + /// The second item. + /// Whether the two items are not equal. + public static bool operator !=(UnitQuaternion a, UnitQuaternion b) => !(a == b); + + /// + public bool Equals(UnitQuaternion other) + { + return this.X.Equals(other.X) + && this.Y.Equals(other.Y) + && this.Z.Equals(other.Z) + && this.W.Equals(other.W); + } + + /// + public override bool Equals(object obj) + { + return obj is UnitQuaternion equatable && this.Equals(equatable); + } + + /// + public override int GetHashCode() + { + unchecked + { + var hashcode = this.X.GetHashCode(); + hashcode = (hashcode * 397) ^ this.Y.GetHashCode(); + hashcode = (hashcode * 397) ^ this.Z.GetHashCode(); + hashcode = (hashcode * 397) ^ this.W.GetHashCode(); + return hashcode; + } + } + + /// + public override string ToString() + { + return $"X:{this.X}, Y:{this.Y}, Z:{this.Z}, W:{this.W}"; + } + } +} diff --git a/Ethar.GeoPose/DataTypes/UnitVector3.cs b/Ethar.GeoPose/DataTypes/UnitVector3.cs new file mode 100644 index 0000000..690d59f --- /dev/null +++ b/Ethar.GeoPose/DataTypes/UnitVector3.cs @@ -0,0 +1,171 @@ +// +// Copyright (c) Ethar. All rights reserved. +// + +namespace Ethar.GeoPose.DataTypes +{ + using System; + using Newtonsoft.Json; + + /// + /// A data type that represents a vector3 for serialization. + /// + public struct UnitVector3 : IEquatable + { + /// + /// Initializes a new instance of the struct. + /// + /// The x value. + /// The y value. + /// The z value. + public UnitVector3(float x, float y, float z) + { + this.X = x; + this.Y = y; + this.Z = z; + } + + /// + /// Gets a vector whose 3 elements are equal to zero. + /// + /// + /// A vector whose three elements are equal to zero (that is, it returns the vector (0,0,0) ). + /// + public static UnitVector3 Zero + { + get + { + return default; + } + } + + /// + /// Gets a vector whose 3 elements are equal to one. + /// + /// + /// A vector whose three elements are equal to one (that is, it returns the vector (1,1,1) ). + /// + public static UnitVector3 One + { + get + { + return new UnitVector3(1f, 1f, 1f); + } + } + + /// + /// Gets the vector (1,0,0). + /// + /// + /// The vector (1,0,0). + /// + public static UnitVector3 UnitX + { + get + { + return new UnitVector3(1f, 0f, 0f); + } + } + + /// + /// Gets the vector (0,1,0). + /// + /// + /// The vector (0,1,0). + /// + public static UnitVector3 UnitY + { + get + { + return new UnitVector3(0f, 1f, 0f); + } + } + + /// + /// Gets the vector (0,0,1). + /// + /// + /// The vector (0,0,1). + /// + public static UnitVector3 UnitZ + { + get + { + return new UnitVector3(0f, 0f, 1f); + } + } + + /// + /// Gets or sets the x value. + /// + [JsonProperty("x")] + public float X { get; set; } + + /// + /// Gets or sets the y value. + /// + [JsonProperty("y")] + public float Y { get; set; } + + /// + /// Gets or sets the z value. + /// + [JsonProperty("z")] + public float Z { get; set; } + + /// + /// Performs an equality comparison on two objects of type . + /// + /// The first item. + /// The second item. + /// Whether the two items are equal. + public static bool operator ==(UnitVector3 a, UnitVector3 b) => a.Equals(b); + + /// + /// Performs an equality comparison on two objects of type . + /// + /// The first item. + /// The second item. + /// Whether the two items are not equal. + public static bool operator !=(UnitVector3 a, UnitVector3 b) => !(a == b); + + /// + /// Combines two hash codes, useful for combining hash codes of individual vector elements. + /// + /// First Hash. + /// Second Hash to add. + /// Combined Hashcode. + public static int CombineHashCodes(int h1, int h2) + { + return ((h1 << 5) + h1) ^ h2; + } + + /// + public bool Equals(UnitVector3 other) + { + return this.X.Equals(other.X) + && this.Y.Equals(other.Y) + && this.Z.Equals(other.Z); + } + + /// + public override bool Equals(object obj) + { + return obj is UnitVector3 equatable && this.Equals(equatable); + } + + /// + public override int GetHashCode() + { + int hashCode = this.X.GetHashCode(); + hashCode = CombineHashCodes(hashCode, this.Y.GetHashCode()); + return CombineHashCodes(hashCode, this.Z.GetHashCode()); + } + + /// + public override string ToString() + { + return $"X:{this.X}, Y:{this.Y}, Z:{this.Z}"; + } + } +} \ No newline at end of file diff --git a/Ethar.GeoPose/DataTypes/YawPitchRollAngles.cs b/Ethar.GeoPose/DataTypes/YawPitchRollAngles.cs new file mode 100644 index 0000000..429c0a3 --- /dev/null +++ b/Ethar.GeoPose/DataTypes/YawPitchRollAngles.cs @@ -0,0 +1,96 @@ +// +// Copyright (c) Ethar. All rights reserved. +// + +namespace Ethar.GeoPose.DataTypes +{ + using System; + using Newtonsoft.Json; + + /// + /// A construct that represents yaw, pitch, and roll angles specified in decimal degrees. + /// + /// Requirements derived from figure 8 in section 7.2.1 of version 1.0.0 of the GeoPose spec http://www.opengis.net/doc/DIS/geopose/1.0. + /// + public struct YawPitchRollAngles : IEquatable + { + /// + /// Initializes a new instance of the struct. + /// + /// The yaw in decimal degrees. + /// The pitch in decimal degrees. + /// The roll in decimal degrees. + public YawPitchRollAngles(float yaw, float pitch, float roll) + { + this.Yaw = yaw; + this.Pitch = pitch; + this.Roll = roll; + } + + /// + /// Gets or sets yaw specified in decimal degrees. + /// + [JsonProperty("yaw")] + public float Yaw { get; set; } + + /// + /// Gets or sets pitch specified in decimal degrees. + /// + [JsonProperty("pitch")] + public float Pitch { get; set; } + + /// + /// Gets or sets roll specified in decimal degrees. + /// + [JsonProperty("roll")] + public float Roll { get; set; } + + /// + /// Performs an equality comparison on two objects of type . + /// + /// The first item. + /// The second item. + /// Whether the two items are equal. + public static bool operator ==(YawPitchRollAngles a, YawPitchRollAngles b) => a.Equals(b); + + /// + /// Performs an equality comparison on two objects of type . + /// + /// The first item. + /// The second item. + /// Whether the two items are not equal. + public static bool operator !=(YawPitchRollAngles a, YawPitchRollAngles b) => !(a == b); + + /// + public bool Equals(YawPitchRollAngles other) + { + return this.Yaw.Equals(other.Yaw) + && this.Pitch.Equals(other.Pitch) + && this.Roll.Equals(other.Roll); + } + + /// + public override bool Equals(object obj) + { + return obj is YawPitchRollAngles equatable && this.Equals(equatable); + } + + /// + public override int GetHashCode() + { + unchecked + { + var hashcode = this.Yaw.GetHashCode(); + hashcode = (hashcode * 397) ^ this.Pitch.GetHashCode(); + hashcode = (hashcode * 397) ^ this.Roll.GetHashCode(); + return hashcode; + } + } + + /// + public override string ToString() + { + return $"Yaw:{this.Yaw}, Pitch:{this.Pitch}, Roll:{this.Roll}"; + } + } +} diff --git a/Ethar.GeoPose/Ethar.GeoPose.csproj b/Ethar.GeoPose/Ethar.GeoPose.csproj new file mode 100644 index 0000000..52d69aa --- /dev/null +++ b/Ethar.GeoPose/Ethar.GeoPose.csproj @@ -0,0 +1,85 @@ + + + + + + + + @(ReleaseNotes, '%0a') + + + + + net452;net462;net48;netstandard2.0;netstandard2.1;netcoreapp3.1;net5.0;net6.0;net7.0 + 7.3 + Ethar.GeoPose + Ethar.GeoPose + True + Ethar Inc. + Ethar.GeoPose + true + Ethar's implementation of version 1.0.0 of the GeoPose spec http://www.opengis.net/doc/DIS/geopose/1.0. + Ethar Inc. + Ethar.GeoPose + false + false + true + true + snupkg + Copyright (c) Ethar Inc. and Contributors + https://github.com/EtharInc/Ethar.GeoPose.Unity + https://github.com/EtharInc/Ethar.GeoPose.Unity.git + git + GeoPose Location GeoLocation Unity Positioning + en-US + false + false + nuget.png + readme.md + true + true + (LGPL-2.0-only WITH FLTK-exception OR Apache-2.0+) + 1701;1702;8765;NETSDK1138 + true + latest + + + + Full + + + + false + + + + + + + + + True + \ + + + True + \ + + + True + \ + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; + + + diff --git a/Ethar.GeoPose/Exceptions/AuthorityNotSupportedException.cs b/Ethar.GeoPose/Exceptions/AuthorityNotSupportedException.cs new file mode 100644 index 0000000..b07e3a1 --- /dev/null +++ b/Ethar.GeoPose/Exceptions/AuthorityNotSupportedException.cs @@ -0,0 +1,24 @@ +// +// Copyright (c) Ethar. All rights reserved. +// + +namespace Ethar.GeoPose.Exceptions +{ + using System; + using Ethar.GeoPose.Authority; + + /// + /// Exception that is thrown when a user attempts to get information about an that is not supported. + /// + public class AuthorityNotSupportedException : Exception + { + /// + /// Initializes a new instance of the class. + /// + /// The error message. + public AuthorityNotSupportedException(string message) + : base(message) + { + } + } +} diff --git a/Ethar.GeoPose/Exceptions/FrameSpecificationInvalidException.cs b/Ethar.GeoPose/Exceptions/FrameSpecificationInvalidException.cs new file mode 100644 index 0000000..629460e --- /dev/null +++ b/Ethar.GeoPose/Exceptions/FrameSpecificationInvalidException.cs @@ -0,0 +1,24 @@ +// +// Copyright (c) Ethar. All rights reserved. +// + +namespace Ethar.GeoPose.Exceptions +{ + using System; + using Ethar.GeoPose.Interfaces; + + /// + /// Exception thrown when a has invalid parameters. + /// + public class FrameSpecificationInvalidException : Exception + { + /// + /// Initializes a new instance of the class. + /// + /// The error message. + public FrameSpecificationInvalidException(string message) + : base(message) + { + } + } +} diff --git a/Ethar.GeoPose/Exceptions/TransitionModelInvalidException.cs b/Ethar.GeoPose/Exceptions/TransitionModelInvalidException.cs new file mode 100644 index 0000000..839dda8 --- /dev/null +++ b/Ethar.GeoPose/Exceptions/TransitionModelInvalidException.cs @@ -0,0 +1,24 @@ +// +// Copyright (c) Ethar. All rights reserved. +// + +namespace Ethar.GeoPose.Exceptions +{ + using System; + using Ethar.GeoPose.TransitionModels; + + /// + /// Exception thrown when a has invalid parameters. + /// + public class TransitionModelInvalidException : Exception + { + /// + /// Initializes a new instance of the class. + /// + /// The error message. + public TransitionModelInvalidException(string message) + : base(message) + { + } + } +} diff --git a/Ethar.GeoPose/Extensions/ChainSduExtensions.cs b/Ethar.GeoPose/Extensions/ChainSduExtensions.cs new file mode 100644 index 0000000..3c8c02d --- /dev/null +++ b/Ethar.GeoPose/Extensions/ChainSduExtensions.cs @@ -0,0 +1,27 @@ +// +// Copyright (c) Ethar. All rights reserved. +// + +namespace Ethar.GeoPose.Extensions +{ + using Ethar.GeoPose.Authority; + using Ethar.GeoPose.StructuralDataUnits; + + /// + /// Extensions for the . + /// + public static class ChainSduExtensions + { + /// + /// Validates that the outer frame of the is extrinsic. + /// + /// The to validate. + /// True if the sdu is valid, false otherwise. + public static bool Validate(this ChainSdu sdu) + { + var outerFrame = sdu.OuterFrame; + var authority = AuthorityProvider.GetAuthority(outerFrame.Authority); + return authority.IsFrameSpecificationExtrinsic(outerFrame); + } + } +} diff --git a/Ethar.GeoPose/Extensions/DictionaryExtensions.cs b/Ethar.GeoPose/Extensions/DictionaryExtensions.cs new file mode 100644 index 0000000..6289db5 --- /dev/null +++ b/Ethar.GeoPose/Extensions/DictionaryExtensions.cs @@ -0,0 +1,37 @@ +// +// Copyright (c) Ethar. All rights reserved. +// + +namespace Ethar.GeoPose.Extensions +{ + using System; + using System.Collections.Generic; + + /// + /// Extensions for the Dictionary,]]> class to allow safer addition. + /// + public static class DictionaryExtensions + { + /// + /// TryAdd extension to safely add an element to a dictionary. + /// + /// Key value for the Dictionary. + /// Value to add to the Key in the dictionary. + /// Input dictionary for the extension. + /// Dictionary index to try and add. + /// Dictionary item to add for Key. + /// Input dictionary is null. + public static void TryAdd(this IDictionary input, TKey key, TValue value) + { + if (input == null) + { + throw new ArgumentNullException(nameof(input)); + } + + if (!input.ContainsKey(key)) + { + input.Add(key, value); + } + } + } +} \ No newline at end of file diff --git a/Ethar.GeoPose/Extensions/GraphSduExtensions.cs b/Ethar.GeoPose/Extensions/GraphSduExtensions.cs new file mode 100644 index 0000000..717908f --- /dev/null +++ b/Ethar.GeoPose/Extensions/GraphSduExtensions.cs @@ -0,0 +1,89 @@ +// +// Copyright (c) Ethar. All rights reserved. +// + +namespace Ethar.GeoPose.Extensions +{ + using System.Collections.Generic; + using System.Linq; + using Ethar.GeoPose.Authority; + using Ethar.GeoPose.StructuralDataUnits; + + /// + /// Extensions for the . + /// + public static class GraphSduExtensions + { + /// + /// Validates that the has does not have any transform indices out of bounds. Also validates that every node is + /// extrinsic or reachable from an extrinsic node. + /// + /// The to validate. + /// True if the sdu is valid, false otherwise. + public static bool Validate(this GraphSdu sdu) + { + var nodeCount = sdu.FrameList.Count; + + if (sdu.TransformList.Any(f => f.InnerFrameIndex >= nodeCount || f.OuterFrameIndex >= nodeCount)) + { + return false; + } + + var extrinsicOrReachableNodeIndices = sdu.FrameList.Where((f, i) => + { + var authority = AuthorityProvider.GetAuthority(f.Authority); + return authority.IsFrameSpecificationExtrinsic(f); + }).Select((f, i) => i).ToList(); + + if (!extrinsicOrReachableNodeIndices.Any()) + { + return false; + } + + var graph = new Dictionary>(); + foreach (var pair in sdu.TransformList) + { + if (graph.ContainsKey(pair.OuterFrameIndex)) + { + graph[pair.OuterFrameIndex].Add(pair.InnerFrameIndex); + } + else + { + graph.Add(pair.OuterFrameIndex, new List { pair.InnerFrameIndex }); + } + } + + foreach (var edge in graph) + { + var outerFrame = edge.Key; + foreach (var innerFrame in edge.Value) + { + VisitEdge(graph, extrinsicOrReachableNodeIndices, outerFrame, innerFrame); + } + } + + return extrinsicOrReachableNodeIndices.Count == nodeCount; + } + + private static void VisitEdge(Dictionary> graph, List reachableNodes, int outerFrame, int innerFrame) + { + if (reachableNodes.Contains(innerFrame) || !reachableNodes.Contains(outerFrame)) + { + return; + } + + if (reachableNodes.Contains(outerFrame)) + { + reachableNodes.Add(innerFrame); + } + + if (graph.TryGetValue(innerFrame, out var toVisit)) + { + foreach (var index in toVisit) + { + VisitEdge(graph, reachableNodes, innerFrame, index); + } + } + } + } +} diff --git a/Ethar.GeoPose/Extensions/IEnumerableTExtensions.cs b/Ethar.GeoPose/Extensions/IEnumerableTExtensions.cs new file mode 100644 index 0000000..6f4e0a2 --- /dev/null +++ b/Ethar.GeoPose/Extensions/IEnumerableTExtensions.cs @@ -0,0 +1,35 @@ +// +// Copyright (c) Ethar. All rights reserved. +// + +namespace Ethar.GeoPose.Extensions +{ + using System.Collections.Generic; + + /// + /// Extensions for IEnumerable]]> to generate hash codes. + /// + public static class IEnumerableTExtensions + { + /// + /// Gets a hashcode for a collection of items. + /// + /// The type of object in the collection. + /// The collection to generate a hashcode for. + /// A hashcode for a collection of items. + /// This method will currently not work properly for a collection of collections. + public static int GetHashCodeForCollection(this IEnumerable enumerable) + { + unchecked + { + var hashcode = 397; + foreach (var item in enumerable) + { + hashcode = (hashcode * 397) ^ item?.GetHashCode() ?? 0; + } + + return hashcode; + } + } + } +} diff --git a/Ethar.GeoPose/Extensions/TangentPointPositionExtensions.cs b/Ethar.GeoPose/Extensions/TangentPointPositionExtensions.cs new file mode 100644 index 0000000..9d8aa85 --- /dev/null +++ b/Ethar.GeoPose/Extensions/TangentPointPositionExtensions.cs @@ -0,0 +1,49 @@ +// +// Copyright (c) Ethar. All rights reserved. +// + +namespace Ethar.GeoPose.Extensions +{ + using System; + using System.Text; + using Ethar.GeoPose.DataTypes; + using Ethar.GeoPose.Validation; + + /// + /// Extensions for the struct. + /// + public static class TangentPointPositionExtensions + { + /// + /// Validates that the contains a valid latitude and longitude. + /// + /// The position to validate. + /// A with information on the validity of the . + public static FrameSpecificationValidationResult Validate(this TangentPointPosition position) + { + var isValid = true; + var message = new StringBuilder(); + + if (position.Latitude < -90 || position.Latitude > 90) + { + isValid = false; + message.Append($"Latitude value {position.Latitude} is outside of the valid range.{Environment.NewLine}"); + } + + if (position.Longitude < -180 || position.Longitude > 180) + { + isValid = false; + message.Append($"Longitude value {position.Longitude} is outside of the valid range."); + } + + return isValid ? FrameSpecificationValidationResult.Valid : new FrameSpecificationValidationResult(isValid, message.ToString()); + } + + /// + /// Builds a parameter string for various frame specification types. + /// + /// The position to build the parameter string for. + /// A parameter string representation of the . + public static string BuildParamString(this TangentPointPosition position) => $"latitude={position.Latitude}&longitude={position.Longitude}&heightInMeters={position.HeightInMeters}"; + } +} diff --git a/Ethar.GeoPose/Extensions/UnitQuaternionExtensions.cs b/Ethar.GeoPose/Extensions/UnitQuaternionExtensions.cs new file mode 100644 index 0000000..d83abec --- /dev/null +++ b/Ethar.GeoPose/Extensions/UnitQuaternionExtensions.cs @@ -0,0 +1,28 @@ +// +// Copyright (c) Ethar. All rights reserved. +// + +namespace Ethar.GeoPose.Extensions +{ + using Ethar.GeoPose.DataTypes; + + /// + /// Extensions for the data type. + /// + public static class UnitQuaternionExtensions + { + /// + /// Builds a parameter string for a . The string will have "rotation" prepended to the members. + /// + /// The to generate the parameter string for. + /// A string representing the . + public static string BuildRotationParamString(this UnitQuaternion quat) => $"rotation.x={quat.X}&rotation.y={quat.Y}&rotation.z={quat.Z}&rotation.w={quat.W}"; + + /// + /// Builds a parameter string for a . The string will have "orientation" prepended to the members. + /// + /// The to generate the parameter string for. + /// A string representing the . + public static string BuildOrientationParamString(this UnitQuaternion quat) => $"orientation.x={quat.X}&orientation.y={quat.Y}&orientation.z={quat.Z}&orientation.w={quat.W}"; + } +} diff --git a/Ethar.GeoPose/Extensions/UnitVector3Extensions.cs b/Ethar.GeoPose/Extensions/UnitVector3Extensions.cs new file mode 100644 index 0000000..461b751 --- /dev/null +++ b/Ethar.GeoPose/Extensions/UnitVector3Extensions.cs @@ -0,0 +1,22 @@ +// +// Copyright (c) Ethar. All rights reserved. +// + +namespace Ethar.GeoPose.Extensions +{ + using Ethar.GeoPose.DataTypes; + + /// + /// Extensions for the data type. + /// + public static class UnitVector3Extensions + { + /// + /// Builds a parameter string for the . + /// + /// The to generate a parameter string for. + /// A string representation of the . + /// Convenience method used to generate parameter strings for frame specifications in the /Ethar.GeoPose/1.0 authority. + public static string BuildTranslationParamString(this UnitVector3 vector) => $"translation.x={vector.X}&translation.y={vector.Y}&translation.z={vector.Z}"; + } +} diff --git a/Ethar.GeoPose/Extensions/YawPitchRollAnglesExtensions.cs b/Ethar.GeoPose/Extensions/YawPitchRollAnglesExtensions.cs new file mode 100644 index 0000000..e4e0125 --- /dev/null +++ b/Ethar.GeoPose/Extensions/YawPitchRollAnglesExtensions.cs @@ -0,0 +1,21 @@ +// +// Copyright (c) Ethar. All rights reserved. +// + +namespace Ethar.GeoPose.Extensions +{ + using Ethar.GeoPose.DataTypes; + + /// + /// Extensions for the data type. + /// + public static class YawPitchRollAnglesExtensions + { + /// + /// Builds a parameter string for a . + /// + /// The to generate the parameter string for. + /// A string representing the . + public static string BuildOrientationParamString(this YawPitchRollAngles ypr) => $"orientation.yaw={ypr.Yaw}&orientation.pitch={ypr.Pitch}&orientation.roll={ypr.Roll}"; + } +} diff --git a/Ethar.GeoPose/FrameSpecifications/BaseFrameSpecification.cs b/Ethar.GeoPose/FrameSpecifications/BaseFrameSpecification.cs new file mode 100644 index 0000000..c8e09ad --- /dev/null +++ b/Ethar.GeoPose/FrameSpecifications/BaseFrameSpecification.cs @@ -0,0 +1,67 @@ +// +// Copyright (c) Ethar. All rights reserved. +// + +namespace Ethar.GeoPose.FrameSpecifications +{ + using System; + using Ethar.GeoPose.Interfaces; + using Ethar.GeoPose.JsonConversion; + using Newtonsoft.Json; + + /// + /// A construct that completely and uniquely defines a reference frame. + /// + /// Derived from Figure 8 from section 7.2.1 of version 1.0.0 of the GeoPose spec http://www.opengis.net/doc/DIS/geopose/1.0. + /// + [JsonConverter(typeof(BaseFrameSpecificationJsonConverter))] + public class BaseFrameSpecification : IFrameSpecification, IEquatable + { + /// + /// Initializes a new instance of the class. + /// + /// An ID that uniquely defines the frame within the authority. + /// A string uniquely specifying a source of reference frame specifications. + protected BaseFrameSpecification(string id, string authority) + { + this.Authority = authority; + this.Id = id; + } + + /// + public string Authority { get; } + + /// + public string Id { get; } + + /// + public bool Equals(BaseFrameSpecification other) + { + return string.Equals(this.Authority, other.Authority) + && string.Equals(this.Id, other.Id); + } + + /// + public override bool Equals(object obj) + { + return obj != null && obj is BaseFrameSpecification equatable && this.Equals(equatable); + } + + /// + public override int GetHashCode() + { + unchecked + { + var hashcode = this.Authority.GetHashCode(); + hashcode = (hashcode * 397) ^ this.Id.GetHashCode(); + return hashcode; + } + } + + /// + public override string ToString() + { + return $"Authority:{this.Authority}, Id:{this.Id}"; + } + } +} \ No newline at end of file diff --git a/Ethar.GeoPose/Interfaces/IFrameSpecification.cs b/Ethar.GeoPose/Interfaces/IFrameSpecification.cs new file mode 100644 index 0000000..646a86c --- /dev/null +++ b/Ethar.GeoPose/Interfaces/IFrameSpecification.cs @@ -0,0 +1,28 @@ +// +// Copyright (c) Ethar. All rights reserved. +// + +namespace Ethar.GeoPose.Interfaces +{ + using Newtonsoft.Json; + + /// + /// A Frame Specificaton interface that completely and uniquely defines a reference frame. + /// + /// Derived from Figure 8 from section 7.2.1 of version 1.0.0 of the GeoPose spec http://www.opengis.net/doc/DIS/geopose/1.0. + /// + public interface IFrameSpecification + { + /// + /// Gets the frame specification's authority. + /// + [JsonProperty("authority")] + string Authority { get; } + + /// + /// Gets the frame specification's ID. + /// + [JsonProperty("id")] + string Id { get; } + } +} \ No newline at end of file diff --git a/Ethar.GeoPose/Interfaces/IStructuralDataUnit.cs b/Ethar.GeoPose/Interfaces/IStructuralDataUnit.cs new file mode 100644 index 0000000..91a1ab2 --- /dev/null +++ b/Ethar.GeoPose/Interfaces/IStructuralDataUnit.cs @@ -0,0 +1,13 @@ +// +// Copyright (c) Ethar. All rights reserved. +// + +namespace Ethar.GeoPose.Interfaces +{ + /// + /// Base interface that represents a Structural Data Unit. Reserved for future use. + /// + public interface IStructuralDataUnit + { + } +} \ No newline at end of file diff --git a/Ethar.GeoPose/JsonConversion/BaseFrameSpecificationJsonConverter.cs b/Ethar.GeoPose/JsonConversion/BaseFrameSpecificationJsonConverter.cs new file mode 100644 index 0000000..8579ba7 --- /dev/null +++ b/Ethar.GeoPose/JsonConversion/BaseFrameSpecificationJsonConverter.cs @@ -0,0 +1,45 @@ +// +// Copyright (c) Ethar. All rights reserved. +// + +namespace Ethar.GeoPose.JsonConversion +{ + using System; + using Ethar.GeoPose.Authority; + using Ethar.GeoPose.Exceptions; + using Ethar.GeoPose.FrameSpecifications; + using Newtonsoft.Json; + using Newtonsoft.Json.Linq; + + /// + /// A utility class that converts json into and vice versa. + /// + public class BaseFrameSpecificationJsonConverter : JsonConverter + { + /// + /// Thrown when the authority that the json belongs to is not supported. + public override BaseFrameSpecification ReadJson(JsonReader reader, Type objectType, BaseFrameSpecification existingValue, bool hasExistingValue, JsonSerializer serializer) + { + var jsonObject = JObject.Load(reader); + var authorityName = (string)jsonObject["authority"] ?? string.Empty; + + if (string.IsNullOrEmpty(authorityName) || jsonObject == null) + { + // TODO: create exception type for this + throw new Exception(); + } + + var authority = AuthorityProvider.GetAuthority(authorityName); + return authority.ConvertJsonToFrameSpec(jsonObject) as BaseFrameSpecification; + } + + /// + public override void WriteJson(JsonWriter writer, BaseFrameSpecification value, JsonSerializer serializer) + { + var authorityName = value.Authority ?? throw new NullReferenceException(nameof(value)); + var authority = AuthorityProvider.GetAuthority(authorityName); + var jObj = authority.ConvertFrameSpecToJson(value); + jObj.WriteTo(writer); + } + } +} diff --git a/Ethar.GeoPose/JsonConversion/FrameTransformIndexPairJsonConverter.cs b/Ethar.GeoPose/JsonConversion/FrameTransformIndexPairJsonConverter.cs new file mode 100644 index 0000000..9edb173 --- /dev/null +++ b/Ethar.GeoPose/JsonConversion/FrameTransformIndexPairJsonConverter.cs @@ -0,0 +1,45 @@ +// +// Copyright (c) Ethar. All rights reserved. +// + +namespace Ethar.GeoPose.JsonConversion +{ + using System; + using System.Linq; + using Ethar.GeoPose.DataTypes; + using Newtonsoft.Json; + using Newtonsoft.Json.Linq; + + /// + /// A utility class that converts json into a and vice versa. + /// + public class FrameTransformIndexPairJsonConverter : JsonConverter + { + /// + public override FrameTransformIndexPair ReadJson(JsonReader reader, Type objectType, FrameTransformIndexPair existingValue, bool hasExistingValue, JsonSerializer serializer) + { + var jsonObject = JObject.Load(reader); + var link = (JArray)jsonObject["link"]; + + var links = link.Select(x => (int)x); + + return new FrameTransformIndexPair(links); + } + + /// + public override void WriteJson(JsonWriter writer, FrameTransformIndexPair value, JsonSerializer serializer) + { + var jObj = new JObject(); + + var arr = new JArray + { + value.OuterFrameIndex, + value.InnerFrameIndex, + }; + + jObj.Add("link", arr); + + jObj.WriteTo(writer); + } + } +} diff --git a/Ethar.GeoPose/JsonConversion/GeoposeDurationJsonConverter.cs b/Ethar.GeoPose/JsonConversion/GeoposeDurationJsonConverter.cs new file mode 100644 index 0000000..06107a2 --- /dev/null +++ b/Ethar.GeoPose/JsonConversion/GeoposeDurationJsonConverter.cs @@ -0,0 +1,34 @@ +// +// Copyright (c) Ethar. All rights reserved. +// + +namespace Ethar.GeoPose.JsonConversion +{ + using System; + using Ethar.GeoPose.DataTypes; + using Newtonsoft.Json; + + /// + /// A utility class that converts json into and vice versa. + /// + public class GeoposeDurationJsonConverter : JsonConverter + { + /// + public override GeoPoseDuration ReadJson(JsonReader reader, Type objectType, GeoPoseDuration existingValue, bool hasExistingValue, JsonSerializer serializer) + { + var time = (long)(reader.Value ?? throw new NullReferenceException(nameof(reader.Value))); + + return new GeoPoseDuration() + { + NumericDuration = time, + }; + } + + /// + public override void WriteJson(JsonWriter writer, GeoPoseDuration value, JsonSerializer serializer) + { + var toWrite = value.NumericDuration; + writer.WriteValue(toWrite); + } + } +} diff --git a/Ethar.GeoPose/JsonConversion/ParamStringBuilder.cs b/Ethar.GeoPose/JsonConversion/ParamStringBuilder.cs new file mode 100644 index 0000000..f92ed6b --- /dev/null +++ b/Ethar.GeoPose/JsonConversion/ParamStringBuilder.cs @@ -0,0 +1,57 @@ +// +// Copyright (c) Ethar. All rights reserved. +// + +namespace Ethar.GeoPose.JsonConversion +{ + using System.Linq; + using System.Reflection; + using Newtonsoft.Json; + + /// + /// A utility class that is used to build generic parameter strings. + /// + public class ParamStringBuilder + { + /// + /// Builds a parameter string for the object. + /// + /// The type of object. + /// The object to build a parameter string for. + /// A parameter string representation of the object. + /// + /// This works by using reflection to determine which properties have the and generating a parameter + /// string from those properties. + /// + /// + /// Assume a class with the following member. + /// + /// class TestClass + /// { + /// [JsonProperty("p1")] + /// int Param1 + /// } + /// + /// This method would generate the parameter string: "p1={value of Param1}". + /// + public static string BuildParamString(T obj) + { + var props = typeof(T) + .GetProperties() + .Where(p => p.GetCustomAttribute() != null) + .Select(p => + { + if (p.PropertyType.IsPrimitive || p.PropertyType == typeof(string)) + { + return $"{p.GetCustomAttribute().PropertyName}={p.GetValue(obj)}"; + } + else + { + return BuildParamString(p.GetValue(obj)); + } + }).ToList(); + var paramString = string.Join("&", props); + return paramString; + } + } +} diff --git a/Ethar.GeoPose/JsonConversion/TransitionModelJsonConverter.cs b/Ethar.GeoPose/JsonConversion/TransitionModelJsonConverter.cs new file mode 100644 index 0000000..e811efa --- /dev/null +++ b/Ethar.GeoPose/JsonConversion/TransitionModelJsonConverter.cs @@ -0,0 +1,43 @@ +// +// Copyright (c) Ethar. All rights reserved. +// + +namespace Ethar.GeoPose.JsonConversion +{ + using System; + using Ethar.GeoPose.Authority; + using Ethar.GeoPose.TransitionModels; + using Newtonsoft.Json; + using Newtonsoft.Json.Linq; + + /// + /// A utility class that converts transition models to json and vice versa. + /// + public class TransitionModelJsonConverter : JsonConverter + { + /// + public override TransitionModel ReadJson(JsonReader reader, Type objectType, TransitionModel existingValue, bool hasExistingValue, JsonSerializer serializer) + { + var jsonObject = JObject.Load(reader); + var authorityName = (string)jsonObject["authority"] ?? string.Empty; + + if (string.IsNullOrEmpty(authorityName) || jsonObject == null) + { + // TODO: create exception type for this + throw new Exception(); + } + + var authority = AuthorityProvider.GetAuthority(authorityName); + return authority.ConvertJsonToTransitionModel(jsonObject); + } + + /// + public override void WriteJson(JsonWriter writer, TransitionModel value, JsonSerializer serializer) + { + var authorityName = value.Authority ?? throw new NullReferenceException(nameof(value)); + var authority = AuthorityProvider.GetAuthority(authorityName); + var jObj = authority.ConvertTransitionModelToJson(value); + jObj.WriteTo(writer); + } + } +} diff --git a/Ethar.GeoPose/LICENSE b/Ethar.GeoPose/LICENSE new file mode 100644 index 0000000..2dc7c55 --- /dev/null +++ b/Ethar.GeoPose/LICENSE @@ -0,0 +1,203 @@ +# Ethar GeoPose Apache 2.0 License + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. \ No newline at end of file diff --git a/Ethar.GeoPose/StructuralDataUnits/AdvancedSdu.cs b/Ethar.GeoPose/StructuralDataUnits/AdvancedSdu.cs new file mode 100644 index 0000000..e765499 --- /dev/null +++ b/Ethar.GeoPose/StructuralDataUnits/AdvancedSdu.cs @@ -0,0 +1,100 @@ +// +// Copyright (c) Ethar. All rights reserved. +// + +namespace Ethar.GeoPose.StructuralDataUnits +{ + using System; + using Ethar.GeoPose.DataTypes; + using Ethar.GeoPose.FrameSpecifications; + using Ethar.GeoPose.Interfaces; + using Newtonsoft.Json; + + /// + /// A construct that represents an Advanced SDU. + /// The Advanced Target has a more general structure than Basic-YPR and Basic-Quaternion, supporting flexible specification of Outer Frame and a Valid Time. + /// + /// Requirements derived from section 8.4.3 of version 1.0.0 of the GeoPose spec http://www.opengis.net/doc/DIS/geopose/1.0. + /// + public struct AdvancedSdu : IStructuralDataUnit, IEquatable + { + /// + /// Initializes a new instance of the struct. + /// + /// The Unix Timestamp representing the valid time. + /// The quaternion to apply to the .. + /// The reference frame for the SDU, must be an extrinsic frame specification. + public AdvancedSdu(long validTime, UnitQuaternion quaternion, BaseFrameSpecification frameSpecification) + { + this.ValidTime = validTime; + this.Quaternion = quaternion; + this.FrameSpecification = frameSpecification; + } + + /// + /// Gets the Unix Timestamp representing the valid time. + /// + [JsonProperty("validTime")] + public long ValidTime { get; } + + /// + /// Gets the quaternion to apply to the . + /// + [JsonProperty("quaternion")] + public UnitQuaternion Quaternion { get; } + + /// + /// Gets the reference frame specification for the SDU. + /// + [JsonProperty("frameSpecification")] + public BaseFrameSpecification FrameSpecification { get; } + + /// + /// Performs an equality comparison on two objects of type . + /// + /// The first item. + /// The second item. + /// Whether the two items are equal. + public static bool operator ==(AdvancedSdu a, AdvancedSdu b) => a.Equals(b); + + /// + /// Performs an equality comparison on two objects of type . + /// + /// The first item. + /// The second item. + /// Whether the two items are not equal. + public static bool operator !=(AdvancedSdu a, AdvancedSdu b) => !(a == b); + + /// + public bool Equals(AdvancedSdu other) + { + return this.ValidTime.Equals(other.ValidTime) + && this.Quaternion.Equals(other.Quaternion) + && this.FrameSpecification.Equals(other.FrameSpecification); + } + + /// + public override bool Equals(object obj) + { + return obj is AdvancedSdu equatable && this.Equals(equatable); + } + + /// + public override int GetHashCode() + { + unchecked + { + var hashcode = this.ValidTime.GetHashCode(); + hashcode = (hashcode * 397) ^ this.Quaternion.GetHashCode(); + hashcode = (hashcode * 397) ^ this.FrameSpecification.GetHashCode(); + return hashcode; + } + } + + /// + public override string ToString() + { + return $"ValidTime:{this.ValidTime}, Quaternion:[{this.Quaternion}], FrameSpecification:[{this.FrameSpecification}]"; + } + } +} \ No newline at end of file diff --git a/Ethar.GeoPose/StructuralDataUnits/BasicQuaternionSdu.cs b/Ethar.GeoPose/StructuralDataUnits/BasicQuaternionSdu.cs new file mode 100644 index 0000000..123d0ce --- /dev/null +++ b/Ethar.GeoPose/StructuralDataUnits/BasicQuaternionSdu.cs @@ -0,0 +1,89 @@ +// +// Copyright (c) Ethar. All rights reserved. +// + +namespace Ethar.GeoPose.StructuralDataUnits +{ + using System; + using Ethar.GeoPose.DataTypes; + using Ethar.GeoPose.Interfaces; + using Newtonsoft.Json; + + /// + /// A construct that represents a Basic Quaternion SDU. + /// The Basic-Quaternion Target has a simple structure with no options. Position is specified as a point in an LTP-ENU frame and rotation is specified as a unit quaternion. + /// + /// Requirements derived from section 8.4.2 of version 1.0.0 of the GeoPose spec http://www.opengis.net/doc/DIS/geopose/1.0. + /// + public struct BasicQuaternionSdu : IStructuralDataUnit, IEquatable + { + /// + /// Initializes a new instance of the struct. + /// + /// The position in latitude and longitude in decimal degrees, and height in meters. + /// A quaternion representing the orientation. + public BasicQuaternionSdu(TangentPointPosition position, UnitQuaternion quaternion) + { + this.Position = position; + this.Quaternion = quaternion; + } + + /// + /// Gets the position in latitude and longitude in decimal degrees, and height in meters. + /// + [JsonProperty("position")] + public TangentPointPosition Position { get; } + + /// + /// Gets a quaternion representing the orientation. + /// + [JsonProperty("quaternion")] + public UnitQuaternion Quaternion { get; } + + /// + /// Performs an equality comparison on two objects of type . + /// + /// The first item. + /// The second item. + /// Whether the two items are equal. + public static bool operator ==(BasicQuaternionSdu left, BasicQuaternionSdu right) => left.Equals(right); + + /// + /// Performs an equality comparison on two objects of type . + /// + /// The first item. + /// The second item. + /// Whether the two items are not equal. + public static bool operator !=(BasicQuaternionSdu left, BasicQuaternionSdu right) => !(left == right); + + /// + public bool Equals(BasicQuaternionSdu other) + { + return this.Position.Equals(other.Position) + && this.Quaternion.Equals(other.Quaternion); + } + + /// + public override bool Equals(object obj) + { + return obj is BasicQuaternionSdu equatable && this.Equals(equatable); + } + + /// + public override int GetHashCode() + { + unchecked + { + var hashcode = this.Position.GetHashCode(); + hashcode = (hashcode * 397) ^ this.Quaternion.GetHashCode(); + return hashcode; + } + } + + /// + public override string ToString() + { + return $"Position:[{this.Position}], Quaternion:[{this.Quaternion}]"; + } + } +} \ No newline at end of file diff --git a/Ethar.GeoPose/StructuralDataUnits/BasicYawPitchRollSdu.cs b/Ethar.GeoPose/StructuralDataUnits/BasicYawPitchRollSdu.cs new file mode 100644 index 0000000..5d6e583 --- /dev/null +++ b/Ethar.GeoPose/StructuralDataUnits/BasicYawPitchRollSdu.cs @@ -0,0 +1,89 @@ +// +// Copyright (c) Ethar. All rights reserved. +// + +namespace Ethar.GeoPose.StructuralDataUnits +{ + using System; + using Ethar.GeoPose.DataTypes; + using Ethar.GeoPose.Interfaces; + using Newtonsoft.Json; + + /// + /// A construct that represents a Basic YPR SDU. + /// The Basic-YPR Target has a simple structure with no options. Position is specified as a point in an LTP-ENU frame and rotation is specified by yaw, pitch, and roll angles specified in decimal degrees. + /// + /// Requirements derived from section 8.4.1 of version 1.0.0 of the GeoPose spec http://www.opengis.net/doc/DIS/geopose/1.0. + /// + public struct BasicYawPitchRollSdu : IStructuralDataUnit, IEquatable + { + /// + /// Initializes a new instance of the struct. + /// + /// A representation of the orientation in yaw, pitch, and roll. + /// The position in latitude and longitude in decimal degrees, and height in meters. + public BasicYawPitchRollSdu(YawPitchRollAngles angles, TangentPointPosition position) + { + this.Angles = angles; + this.Position = position; + } + + /// + /// Gets the position in latitude and longitude in decimal degrees, and height in meters. + /// + [JsonProperty("position")] + public TangentPointPosition Position { get; } + + /// + /// Gets the orientation represented in yaw, pitch, and roll in decimal degrees. + /// + [JsonProperty("angles")] + public YawPitchRollAngles Angles { get; } + + /// + /// Performs an equality comparison on two objects of type . + /// + /// The first item. + /// The second item. + /// Whether the two items are equal. + public static bool operator ==(BasicYawPitchRollSdu a, BasicYawPitchRollSdu b) => a.Equals(b); + + /// + /// Performs an equality comparison on two objects of type . + /// + /// The first item. + /// The second item. + /// Whether the two items are not equal. + public static bool operator !=(BasicYawPitchRollSdu a, BasicYawPitchRollSdu b) => !(a == b); + + /// + public bool Equals(BasicYawPitchRollSdu other) + { + return this.Position.Equals(other.Position) + && this.Angles.Equals(other.Angles); + } + + /// + public override bool Equals(object obj) + { + return obj is BasicYawPitchRollSdu equatable && this.Equals(equatable); + } + + /// + public override int GetHashCode() + { + unchecked + { + var hashcode = this.Position.GetHashCode(); + hashcode = (hashcode * 397) ^ this.Angles.GetHashCode(); + return hashcode; + } + } + + /// + public override string ToString() + { + return $"Position:[{this.Position}], Angles:[{this.Angles}]"; + } + } +} \ No newline at end of file diff --git a/Ethar.GeoPose/StructuralDataUnits/ChainSdu.cs b/Ethar.GeoPose/StructuralDataUnits/ChainSdu.cs new file mode 100644 index 0000000..7a3d385 --- /dev/null +++ b/Ethar.GeoPose/StructuralDataUnits/ChainSdu.cs @@ -0,0 +1,102 @@ +// +// Copyright (c) Ethar. All rights reserved. +// + +namespace Ethar.GeoPose.StructuralDataUnits +{ + using System; + using System.Collections.Generic; + using System.Linq; + using Ethar.GeoPose.DataTypes; + using Ethar.GeoPose.Extensions; + using Ethar.GeoPose.FrameSpecifications; + using Ethar.GeoPose.Interfaces; + using Newtonsoft.Json; + + /// + /// A construct that represents a Chain SDU. + /// + /// Requirements derived from section 8.4.5 of version 1.0.0 of the GeoPose spec http://www.opengis.net/doc/DIS/geopose/1.0. + /// + public struct ChainSdu : IStructuralDataUnit, IEquatable + { + /// + /// Initializes a new instance of the struct. + /// + /// The Unix Timestamp representing the valid time. + /// The extrinsic frame specification for this SDU. + /// Ordered chain of . + public ChainSdu(long validTime, BaseFrameSpecification outerFrame, IList frameChain) + { + this.ValidTime = validTime; + this.OuterFrame = outerFrame; + this.FrameChain = frameChain; + } + + /// + /// Gets the Unix Timestamp representing the valid time. + /// + [JsonProperty("validTime")] + public long ValidTime { get; } + + /// + /// Gets the outer frame. + /// + [JsonProperty("outerFrame")] + public BaseFrameSpecification OuterFrame { get; } + + /// + /// Gets the frame chain. + /// + [JsonProperty("frameChain")] + public IList FrameChain { get; } + + /// + /// Performs an equality comparison on two objects of type . + /// + /// The first item. + /// The second item. + /// Whether the two items are equal. + public static bool operator ==(ChainSdu a, ChainSdu b) => a.Equals(b); + + /// + /// Performs an equality comparison on two objects of type . + /// + /// The first item. + /// The second item. + /// Whether the two items are not equal. + public static bool operator !=(ChainSdu a, ChainSdu b) => !(a == b); + + /// + public bool Equals(ChainSdu other) + { + return this.ValidTime.Equals(other.ValidTime) + && this.OuterFrame.Equals(other.OuterFrame) + && this.FrameChain.SequenceEqual(other.FrameChain); + } + + /// + public override bool Equals(object obj) + { + return obj is ChainSdu equatable && this.Equals(equatable); + } + + /// + public override int GetHashCode() + { + unchecked + { + var hashcode = this.ValidTime.GetHashCode(); + hashcode = (hashcode * 397) ^ this.FrameChain.GetHashCodeForCollection(); + hashcode = (hashcode * 397) ^ this.OuterFrame.GetHashCode(); + return hashcode; + } + } + + /// + public override string ToString() + { + return $"ValidTime:{this.ValidTime}, OuterFrame:[{this.OuterFrame}], FrameChainCount:{this.FrameChain?.Count ?? 0}"; + } + } +} \ No newline at end of file diff --git a/Ethar.GeoPose/StructuralDataUnits/GraphSdu.cs b/Ethar.GeoPose/StructuralDataUnits/GraphSdu.cs new file mode 100644 index 0000000..0ac1afd --- /dev/null +++ b/Ethar.GeoPose/StructuralDataUnits/GraphSdu.cs @@ -0,0 +1,103 @@ +// +// Copyright (c) Ethar. All rights reserved. +// + +namespace Ethar.GeoPose.StructuralDataUnits +{ + using System; + using System.Collections.Generic; + using System.Linq; + using Ethar.GeoPose.DataTypes; + using Ethar.GeoPose.Extensions; + using Ethar.GeoPose.FrameSpecifications; + using Ethar.GeoPose.Interfaces; + using Newtonsoft.Json; + + /// + /// A construct that represents a Graph SDU. + /// The Graph Target supports a network of object relative poses. The graph is a directed acyclic graph, each node must either be an Extrinsic Frame or reachable from an Extrinsic Frame. + /// + /// Requirements derived from section 8.4.4 of version 1.0.0 of the GeoPose spec http://www.opengis.net/doc/DIS/geopose/1.0. + /// + public struct GraphSdu : IStructuralDataUnit, IEquatable + { + /// + /// Initializes a new instance of the struct. + /// + /// The Unix Timestamp representing the valid time. + /// List of frame specifications in the graph. + /// List of that represent edges in the graph. + public GraphSdu(long validTime, IList frameList, IList transformList) + { + this.ValidTime = validTime; + this.FrameList = frameList; + this.TransformList = transformList; + } + + /// + /// Gets the Unix Timestamp representing the valid time. + /// + [JsonProperty("validTime")] + public long ValidTime { get; } + + /// + /// Gets a list of frame specifications in the graph. + /// + [JsonProperty("frameList")] + public IList FrameList { get; } + + /// + /// Gets a list of that represent edges in the graph. + /// + [JsonProperty("transformList")] + public IList TransformList { get; } + + /// + /// Performs an equality comparison on two objects of type . + /// + /// The first item. + /// The second item. + /// Whether the two items are equal. + public static bool operator ==(GraphSdu a, GraphSdu b) => a.Equals(b); + + /// + /// Performs an equality comparison on two objects of type . + /// + /// The first item. + /// The second item. + /// Whether the two items are not equal. + public static bool operator !=(GraphSdu a, GraphSdu b) => !(a == b); + + /// + public bool Equals(GraphSdu other) + { + return this.ValidTime.Equals(other.ValidTime) + && this.FrameList.SequenceEqual(other.FrameList) + && this.TransformList.SequenceEqual(other.TransformList); + } + + /// + public override bool Equals(object obj) + { + return obj is GraphSdu equatable && this.Equals(equatable); + } + + /// + public override int GetHashCode() + { + unchecked + { + var hashcode = this.ValidTime.GetHashCode(); + hashcode = (hashcode * 397) ^ this.FrameList.GetHashCodeForCollection(); + hashcode = (hashcode * 397) ^ this.TransformList.GetHashCodeForCollection(); + return hashcode; + } + } + + /// + public override string ToString() + { + return $"ValidTime:{this.ValidTime}, FrameListCount:{this.FrameList?.Count ?? 0}, TransformListCount:{this.TransformList?.Count ?? 0}"; + } + } +} \ No newline at end of file diff --git a/Ethar.GeoPose/StructuralDataUnits/IrregularSeriesSdu.cs b/Ethar.GeoPose/StructuralDataUnits/IrregularSeriesSdu.cs new file mode 100644 index 0000000..ecb9a7d --- /dev/null +++ b/Ethar.GeoPose/StructuralDataUnits/IrregularSeriesSdu.cs @@ -0,0 +1,112 @@ +// +// Copyright (c) Ethar. All rights reserved. +// + +namespace Ethar.GeoPose.StructuralDataUnits +{ + using System; + using System.Collections.Generic; + using System.Linq; + using Ethar.GeoPose.DataTypes; + using Ethar.GeoPose.Extensions; + using Ethar.GeoPose.FrameSpecifications; + using Ethar.GeoPose.Interfaces; + using Newtonsoft.Json; + + /// + /// A construct that represents an Irregular Series SDU. + /// + /// Requirements derived from section 8.4.7 of version 1.0.0 of the GeoPose spec http://www.opengis.net/doc/DIS/geopose/1.0. + /// + public struct IrregularSeriesSdu : IStructuralDataUnit, IEquatable + { + /// + /// Initializes a new instance of the struct. + /// + /// The series header. + /// The outer frame for the series. + /// The series trailer. + /// The inner frames and valid times associated with them. + public IrregularSeriesSdu(SeriesHeader header, BaseFrameSpecification outerFrame, SeriesTrailer trailer, IEnumerable innerFrameAndTimeSeries) + { + this.Header = header; + this.OuterFrame = outerFrame; + this.Trailer = trailer; + this.InnerFrameAndTimeSeries = innerFrameAndTimeSeries; + } + + /// + /// Gets the series header. + /// + [JsonProperty("header")] + public SeriesHeader Header { get; } + + /// + /// Gets the outer frame. + /// + [JsonProperty("outerFrame")] + public BaseFrameSpecification OuterFrame { get; } + + /// + /// Gets the inner frames and their valid times. + /// + [JsonProperty("innerFrameAndTimeSeries")] + public IEnumerable InnerFrameAndTimeSeries { get; } + + /// + /// Gets the series trailer. + /// + [JsonProperty("trailer")] + public SeriesTrailer Trailer { get; } + + /// + /// Performs an equality comparison on two objects of type . + /// + /// The first item. + /// The second item. + /// Whether the two items are equal. + public static bool operator ==(IrregularSeriesSdu a, IrregularSeriesSdu b) => a.Equals(b); + + /// + /// Performs an equality comparison on two objects of type . + /// + /// The first item. + /// The second item. + /// Whether the two items are not equal. + public static bool operator !=(IrregularSeriesSdu a, IrregularSeriesSdu b) => !(a == b); + + /// + public bool Equals(IrregularSeriesSdu other) + { + return this.Header.Equals(other.Header) + && this.OuterFrame.Equals(other.OuterFrame) + && this.InnerFrameAndTimeSeries.SequenceEqual(other.InnerFrameAndTimeSeries) + && this.Trailer.Equals(other.Trailer); + } + + /// + public override bool Equals(object obj) + { + return obj is IrregularSeriesSdu equatable && this.Equals(equatable); + } + + /// + public override int GetHashCode() + { + unchecked + { + var hashcode = this.Header.GetHashCode(); + hashcode = (hashcode * 397) ^ this.OuterFrame.GetHashCode(); + hashcode = (hashcode * 397) ^ this.InnerFrameAndTimeSeries.GetHashCodeForCollection(); + hashcode = (hashcode * 397) ^ this.Trailer.GetHashCode(); + return hashcode; + } + } + + /// + public override string ToString() + { + return $"Header:[{this.Header}], OuterFrame:[{this.OuterFrame}], InnerFrameAndTimeSeriesCount:{this.InnerFrameAndTimeSeries?.Count() ?? 0}, Trailer:[{this.Trailer}]"; + } + } +} \ No newline at end of file diff --git a/Ethar.GeoPose/StructuralDataUnits/RegularSeriesSdu.cs b/Ethar.GeoPose/StructuralDataUnits/RegularSeriesSdu.cs new file mode 100644 index 0000000..5ab369e --- /dev/null +++ b/Ethar.GeoPose/StructuralDataUnits/RegularSeriesSdu.cs @@ -0,0 +1,122 @@ +// +// Copyright (c) Ethar. All rights reserved. +// + +namespace Ethar.GeoPose.StructuralDataUnits +{ + using System; + using System.Collections.Generic; + using System.Linq; + using Ethar.GeoPose.DataTypes; + using Ethar.GeoPose.Extensions; + using Ethar.GeoPose.FrameSpecifications; + using Ethar.GeoPose.Interfaces; + using Newtonsoft.Json; + + /// + /// A construct that represents a Regular Series SDU. + /// + /// Requirements derived from section 8.4.6 of version 1.0.0 of the GeoPose spec http://www.opengis.net/doc/DIS/geopose/1.0. + /// + public struct RegularSeriesSdu : IStructuralDataUnit, IEquatable + { + /// + /// Initializes a new instance of the struct. + /// + /// The series header. + /// The inter pose duration. + /// The outer frame for the series. + /// The inner frames. + /// The series trailer. + public RegularSeriesSdu(SeriesHeader header, GeoPoseDuration interPoseDuration, BaseFrameSpecification outerFrame, IEnumerable innerFrameSeries, SeriesTrailer trailer) + { + this.Header = header; + this.InterPoseDuration = interPoseDuration; + this.OuterFrame = outerFrame; + this.InnerFrameSeries = innerFrameSeries; + this.Trailer = trailer; + } + + /// + /// Gets the series header. + /// + [JsonProperty("header")] + public SeriesHeader Header { get; } + + /// + /// Gets the inter pose duration. + /// + [JsonProperty("interPoseDuration")] + public GeoPoseDuration InterPoseDuration { get; } + + /// + /// Gets the outer frame for the series. + /// + [JsonProperty("outerFrame")] + public BaseFrameSpecification OuterFrame { get; } + + /// + /// Gets the inner frame series. + /// + [JsonProperty("innerFrameSeries")] + public IEnumerable InnerFrameSeries { get; } + + /// + /// Gets the series trailer. + /// + [JsonProperty("trailer")] + public SeriesTrailer Trailer { get; } + + /// + /// Performs an equality comparison on two objects of type . + /// + /// The first item. + /// The second item. + /// Whether the two items are equal. + public static bool operator ==(RegularSeriesSdu a, RegularSeriesSdu b) => a.Equals(b); + + /// + /// Performs an equality comparison on two objects of type . + /// + /// The first item. + /// The second item. + /// Whether the two items are not equal. + public static bool operator !=(RegularSeriesSdu a, RegularSeriesSdu b) => !(a == b); + + /// + public bool Equals(RegularSeriesSdu other) + { + return this.Header.Equals(other.Header) + && this.InterPoseDuration.Equals(other.InterPoseDuration) + && this.OuterFrame.Equals(other.OuterFrame) + && this.InnerFrameSeries.SequenceEqual(other.InnerFrameSeries) + && this.Trailer.Equals(other.Trailer); + } + + /// + public override bool Equals(object obj) + { + return obj is RegularSeriesSdu equatable && this.Equals(equatable); + } + + /// + public override int GetHashCode() + { + unchecked + { + var hashcode = this.Header.GetHashCode(); + hashcode = (hashcode * 397) ^ this.OuterFrame.GetHashCode(); + hashcode = (hashcode * 397) ^ this.InnerFrameSeries.GetHashCodeForCollection(); + hashcode = (hashcode * 397) ^ this.InterPoseDuration.GetHashCode(); + hashcode = (hashcode * 397) ^ this.Trailer.GetHashCode(); + return hashcode; + } + } + + /// + public override string ToString() + { + return $"Header:[{this.Header}], InterPoseDuration:[{this.InterPoseDuration}], OuterFrame:[{this.OuterFrame}], InnerFrameSeriesCount:{this.InnerFrameSeries?.Count() ?? 0}, Trailer:[{this.Trailer}]"; + } + } +} \ No newline at end of file diff --git a/Ethar.GeoPose/StructuralDataUnits/StreamElementSdu.cs b/Ethar.GeoPose/StructuralDataUnits/StreamElementSdu.cs new file mode 100644 index 0000000..e125d5e --- /dev/null +++ b/Ethar.GeoPose/StructuralDataUnits/StreamElementSdu.cs @@ -0,0 +1,78 @@ +// +// Copyright (c) Ethar. All rights reserved. +// + +namespace Ethar.GeoPose.StructuralDataUnits +{ + using System; + using Ethar.GeoPose.DataTypes; + using Ethar.GeoPose.Interfaces; + using Newtonsoft.Json; + + /// + /// A construct that represents a Stream Element SDU. + /// + /// Requirements derived from section 8.4.8 of version 1.0.0 of the GeoPose spec http://www.opengis.net/doc/DIS/geopose/1.0. + /// + public struct StreamElementSdu : IStructuralDataUnit, IEquatable + { + /// + /// Initializes a new instance of the struct. + /// + /// The stream element. + public StreamElementSdu(FrameAndTimeElement streamElement) + { + this.StreamElement = streamElement; + } + + /// + /// Gets the stream element. + /// + [JsonProperty("streamElement")] + public FrameAndTimeElement StreamElement { get; } + + /// + /// Performs an equality comparison on two objects of type . + /// + /// The first item. + /// The second item. + /// Whether the two items are equal. + public static bool operator ==(StreamElementSdu a, StreamElementSdu b) => a.Equals(b); + + /// + /// Performs an equality comparison on two objects of type . + /// + /// The first item. + /// The second item. + /// Whether the two items are not equal. + public static bool operator !=(StreamElementSdu a, StreamElementSdu b) => !(a == b); + + /// + public bool Equals(StreamElementSdu other) + { + return this.StreamElement.Equals(other.StreamElement); + } + + /// + public override bool Equals(object obj) + { + return obj is StreamElementSdu equatable && this.Equals(equatable); + } + + /// + public override int GetHashCode() + { + unchecked + { + var hashcode = this.StreamElement.GetHashCode(); + return hashcode; + } + } + + /// + public override string ToString() + { + return $"StreamElement:[{this.StreamElement}]"; + } + } +} \ No newline at end of file diff --git a/Ethar.GeoPose/StructuralDataUnits/StreamHeaderSdu.cs b/Ethar.GeoPose/StructuralDataUnits/StreamHeaderSdu.cs new file mode 100644 index 0000000..4db5f64 --- /dev/null +++ b/Ethar.GeoPose/StructuralDataUnits/StreamHeaderSdu.cs @@ -0,0 +1,89 @@ +// +// Copyright (c) Ethar. All rights reserved. +// + +namespace Ethar.GeoPose.StructuralDataUnits +{ + using System; + using Ethar.GeoPose.FrameSpecifications; + using Ethar.GeoPose.Interfaces; + using Ethar.GeoPose.TransitionModels; + using Newtonsoft.Json; + + /// + /// A construct that represents a Stream Header SDU. + /// + /// Requirements derived from section 8.4.8 of version 1.0.0 of the GeoPose spec http://www.opengis.net/doc/DIS/geopose/1.0. + /// + public struct StreamHeaderSdu : IStructuralDataUnit, IEquatable + { + /// + /// Initializes a new instance of the struct. + /// + /// The transition model. + /// The outer frame for the stream. + public StreamHeaderSdu(TransitionModel transitionModel, BaseFrameSpecification outerFrame) + { + this.TransitionModel = transitionModel; + this.OuterFrame = outerFrame; + } + + /// + /// Gets the transition model. + /// + [JsonProperty("transitionModel")] + public TransitionModel TransitionModel { get; } + + /// + /// Gets the outer frame. + /// + [JsonProperty("outerFrame")] + public BaseFrameSpecification OuterFrame { get; } + + /// + /// Performs an equality comparison on two objects of type . + /// + /// The first item. + /// The second item. + /// Whether the two items are equal. + public static bool operator ==(StreamHeaderSdu a, StreamHeaderSdu b) => a.Equals(b); + + /// + /// Performs an equality comparison on two objects of type . + /// + /// The first item. + /// The second item. + /// Whether the two items are not equal. + public static bool operator !=(StreamHeaderSdu a, StreamHeaderSdu b) => !(a == b); + + /// + public bool Equals(StreamHeaderSdu other) + { + return this.TransitionModel.Equals(other.TransitionModel) + && this.OuterFrame.Equals(other.OuterFrame); + } + + /// + public override bool Equals(object obj) + { + return obj is StreamHeaderSdu equatable && this.Equals(equatable); + } + + /// + public override int GetHashCode() + { + unchecked + { + var hashcode = this.TransitionModel.GetHashCode(); + hashcode = (hashcode * 397) ^ this.OuterFrame.GetHashCode(); + return hashcode; + } + } + + /// + public override string ToString() + { + return $"TransitionModel:[{this.TransitionModel}], OuterFrame:[{this.OuterFrame}]"; + } + } +} \ No newline at end of file diff --git a/Ethar.GeoPose/StructuralDataUnits/StreamRecordSdu.cs b/Ethar.GeoPose/StructuralDataUnits/StreamRecordSdu.cs new file mode 100644 index 0000000..b7e6f44 --- /dev/null +++ b/Ethar.GeoPose/StructuralDataUnits/StreamRecordSdu.cs @@ -0,0 +1,90 @@ +// +// Copyright (c) Ethar. All rights reserved. +// + +namespace Ethar.GeoPose.StructuralDataUnits +{ + using System; + using System.Collections.Generic; + using System.Linq; + using Ethar.GeoPose.Extensions; + using Ethar.GeoPose.Interfaces; + using Newtonsoft.Json; + + /// + /// A construct that represents an Stream Record SDU. + /// + /// Requirements derived from section 9.2.9 of version 1.0.0 of the GeoPose spec http://www.opengis.net/doc/DIS/geopose/1.0. + /// + public struct StreamRecordSdu : IStructuralDataUnit, IEquatable + { + /// + /// Initializes a new instance of the struct. + /// + /// The stream header. + /// The stream elements. + public StreamRecordSdu(StreamHeaderSdu streamHeader, IEnumerable streamElements) + { + this.StreamElements = streamElements; + this.StreamHeader = streamHeader; + } + + /// + /// Gets the stream header. + /// + [JsonProperty("header")] + public StreamHeaderSdu StreamHeader { get; } + + /// + /// Gets the stream elements. + /// + [JsonProperty("streamElements")] + public IEnumerable StreamElements { get; } + + /// + /// Performs an equality comparison on two objects of type . + /// + /// The first item. + /// The second item. + /// Whether the two items are equal. + public static bool operator ==(StreamRecordSdu a, StreamRecordSdu b) => a.Equals(b); + + /// + /// Performs an equality comparison on two objects of type . + /// + /// The first item. + /// The second item. + /// Whether the two items are not equal. + public static bool operator !=(StreamRecordSdu a, StreamRecordSdu b) => !(a == b); + + /// + public bool Equals(StreamRecordSdu other) + { + return this.StreamHeader.Equals(other.StreamHeader) + && this.StreamElements.All(x => other.StreamElements.Any(y => y.Equals(x))); + } + + /// + public override bool Equals(object obj) + { + return obj is StreamRecordSdu equatable && this.Equals(equatable); + } + + /// + public override int GetHashCode() + { + unchecked + { + var hashcode = this.StreamHeader.GetHashCode(); + hashcode = (hashcode * 397) ^ this.StreamElements.GetHashCodeForCollection(); + return hashcode; + } + } + + /// + public override string ToString() + { + return $"StreamHeader:[{this.StreamHeader}], StreamElementsCount:{this.StreamElements?.Count() ?? 0}"; + } + } +} \ No newline at end of file diff --git a/Ethar.GeoPose/TransitionModels/TransitionModel.cs b/Ethar.GeoPose/TransitionModels/TransitionModel.cs new file mode 100644 index 0000000..35bd62e --- /dev/null +++ b/Ethar.GeoPose/TransitionModels/TransitionModel.cs @@ -0,0 +1,95 @@ +// +// Copyright (c) Ethar. All rights reserved. +// + +namespace Ethar.GeoPose.TransitionModels +{ + using System; + using Ethar.GeoPose.JsonConversion; + using Newtonsoft.Json; + + /// + /// An abstract class that providss a base a GeoPose model, a specific model is required for implementation. + /// + [JsonConverter(typeof(TransitionModelJsonConverter))] + public abstract class TransitionModel : IEquatable + { + /// + /// Initializes a new instance of the class. + /// + /// The id of the . + /// The authority for the . + protected TransitionModel(string id, string authority) + { + this.Authority = authority; + this.Id = id; + } + + /// + /// Gets the authority. + /// + [JsonProperty("authority")] + public string Authority { get; } + + /// + /// Gets the ID. + /// + [JsonProperty("id")] + public string Id { get; } + + /// + /// Performs an equality comparison on two objects of type . + /// + /// The first item. + /// The second item. + /// Whether the two items are equal. + public static bool operator ==(TransitionModel a, TransitionModel b) + { + if (a is null) + { + return b is null; + } + + return b is null ? false : a.Equals(b); + } + + /// + /// Performs an equality comparison on two objects of type . + /// + /// The first item. + /// The second item. + /// Whether the two items are not equal. + public static bool operator !=(TransitionModel a, TransitionModel b) => !(a == b); + + /// + public bool Equals(TransitionModel other) + { + return string.Equals(this.Id, other.Id) + && string.Equals(this.Authority, other.Authority); + } + + /// + public override bool Equals(object obj) + { + var item = obj as TransitionModel; + return item != null && this.Equals(item); + } + + /// + public override int GetHashCode() + { + unchecked + { + var hashcode = this.Id.GetHashCode(); + hashcode = (hashcode * 397) ^ this.Authority.GetHashCode(); + return hashcode; + } + } + + /// + public override string ToString() + { + return $"Authority:{this.Authority}, Id:{this.Id}"; + } + } +} diff --git a/Ethar.GeoPose/Validation/FrameSpecificationValidationResult.cs b/Ethar.GeoPose/Validation/FrameSpecificationValidationResult.cs new file mode 100644 index 0000000..5bf88e0 --- /dev/null +++ b/Ethar.GeoPose/Validation/FrameSpecificationValidationResult.cs @@ -0,0 +1,40 @@ +// +// Copyright (c) Ethar. All rights reserved. +// + +namespace Ethar.GeoPose.Validation +{ + using Ethar.GeoPose.Interfaces; + + /// + /// Struct that contains data pertaining to the validity of an . + /// + public struct FrameSpecificationValidationResult + { + /// + /// Initializes a new instance of the struct. + /// + /// Whether the associated is valid. + /// The error message, if any. + public FrameSpecificationValidationResult(bool isValid, string message) + { + this.IsValid = isValid; + this.Message = message; + } + + /// + /// Gets a that represents a valid . + /// + public static FrameSpecificationValidationResult Valid => new FrameSpecificationValidationResult(true, string.Empty); + + /// + /// Gets a value indicating whether the associated is valid. + /// + public bool IsValid { get; } + + /// + /// Gets the error message, if any. + /// + public string Message { get; } + } +} \ No newline at end of file diff --git a/Ethar.GeoPose/Validation/IExplicitFrameSpecificationValidator.cs b/Ethar.GeoPose/Validation/IExplicitFrameSpecificationValidator.cs new file mode 100644 index 0000000..b70c8c7 --- /dev/null +++ b/Ethar.GeoPose/Validation/IExplicitFrameSpecificationValidator.cs @@ -0,0 +1,21 @@ +// +// Copyright (c) Ethar. All rights reserved. +// + +namespace Ethar.GeoPose.Validation +{ + using Ethar.GeoPose.Interfaces; + + /// + /// Interface that represents an validator. + /// + public interface IExplicitFrameSpecificationValidator + { + /// + /// Determines if the is valid. + /// + /// The to validate. + /// A with details on the validity of the . + FrameSpecificationValidationResult Validate(IFrameSpecification frame); + } +} \ No newline at end of file diff --git a/Ethar.GeoPose/Validation/ValidationUtilities.cs b/Ethar.GeoPose/Validation/ValidationUtilities.cs new file mode 100644 index 0000000..3f80c8a --- /dev/null +++ b/Ethar.GeoPose/Validation/ValidationUtilities.cs @@ -0,0 +1,138 @@ +// +// Copyright (c) Ethar. All rights reserved. +// + +namespace Ethar.GeoPose.Validation +{ + using System; + using System.Collections.Specialized; + using Ethar.GeoPose.Exceptions; + using Ethar.GeoPose.Interfaces; + using Newtonsoft.Json.Linq; + + /// + /// A collection of GeoPose validation routines. + /// + public static class ValidationUtilities + { + /// + /// Validate the inbound json definition for expected parameters. + /// + /// A Frame Specification implementing the IFrameSpecification interface, likely derrived from BaseFrameSpecification. + /// The inbound JSON object in format. + /// A of the json Object values. + /// Returns true if the inbound JSON was valid. + /// Raises a on input values that are missing. + /// Raises a if the Frame specification ID is incorrect. + /// Raises a if the specified authority was not found in the incoming frame. + public static bool ValidateJsonObjectParameters(JObject jObject, out NameValueCollection queryString) + where T : IFrameSpecification, new() + { + if (jObject is null) + { + throw new ArgumentNullException(nameof(jObject)); + } + + var frameSpecification = new T(); + + if (frameSpecification == null) + { + throw new FrameSpecificationInvalidException( + $"Frame specification could not be validated, check it has an overload with no constructor parameters"); + } + + if (string.IsNullOrEmpty(frameSpecification.Id)) + { + throw new ArgumentException($"Frame Specification ID is missing in the FrameSpecification, is it initialized by default?"); + } + + if (string.IsNullOrEmpty((string)jObject["id"])) + { + throw new NullReferenceException($"Missing value for id."); + } + + if (!string.IsNullOrEmpty(frameSpecification.Id) && !string.Equals((string)jObject["id"], frameSpecification.Id, StringComparison.OrdinalIgnoreCase)) + { + throw new FrameSpecificationInvalidException( + $"Frame specification ID should be {frameSpecification.Id}"); + } + + if (string.IsNullOrEmpty(frameSpecification.Authority)) + { + throw new ArgumentException($"The Authority Name is missing in the FrameSpecification, is it initialized by default?"); + } + + if (string.IsNullOrEmpty((string)jObject["authority"])) + { + throw new NullReferenceException($"Missing value for authority."); + } + + if (!string.Equals((string)jObject["authority"], frameSpecification.Authority, StringComparison.OrdinalIgnoreCase)) + { + throw new AuthorityNotSupportedException($"Authority must be {frameSpecification.Authority}."); + } + + if (string.IsNullOrEmpty((string)jObject["parameters"])) + { + throw new NullReferenceException($"Missing value for parameters."); + } + + queryString = GetQueryParameters((string)jObject["parameters"]); + + return true; + } + + /// + /// Resolve the parameters in a URL Querystring. + /// + /// input query string. + /// A of Query string parameters and values. + public static NameValueCollection GetQueryParameters(string queryString) + { + NameValueCollection queryParameters = new NameValueCollection(); + string[] querySegments = queryString.Split('&'); + foreach (string segment in querySegments) + { + string[] parts = segment.Split('='); + if (parts.Length > 0) + { + string key = parts[0].Trim(new char[] { '?', ' ' }); + string val = parts[1].Trim(); + + queryParameters.Add(key, val); + } + } + + return queryParameters; + } + + /// + /// Utility to retrieve Frame Specification Parameter data from a deserialised . + /// + /// The input containing deserialised parameter data. + /// The name of the parameter to retrieve from the collection. + /// Returns the string value from the collection if found. + /// Raises a on input values that are missing. + /// Raises a if the specified Parameter Name could not be retrieved from the Frame Specification parameters. + public static string GetParameter(this NameValueCollection collection, string parameterName) + { + if (collection == null || collection.Count == 0) + { + throw new ArgumentException($"Specified collection is null or not initialised"); + } + + if (string.IsNullOrEmpty(parameterName)) + { + throw new ArgumentException($"'{nameof(parameterName)}' cannot be null or empty.", nameof(parameterName)); + } + + if (collection[parameterName] == null) + { + throw new FrameSpecificationInvalidException( + $"Parameter {parameterName} was not found in the input collection, is the Frame Specification invalid?"); + } + + return collection[parameterName]; + } + } +} \ No newline at end of file diff --git a/Ethar.GeoPose/readme.md b/Ethar.GeoPose/readme.md new file mode 100644 index 0000000..856f7e5 --- /dev/null +++ b/Ethar.GeoPose/readme.md @@ -0,0 +1,242 @@ +

+ GeoPose Logo +

+ +# GeoPose Library by [Ethar, Inc.](https://www.ethar.com/) + +[![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](https://github.com/EtharInc/Ethar.GeoPose/blob/main/LICENSE) + +The [GeoPose standard](https://docs.ogc.org/dis/21-056r10/21-056r10.html) enables the easy integration of digital elements on and in relation to the surface of the planet. + +Ethar.GeoPose is a C# library that implements GeoPose, allowing you to assign precise 3D position and orientation of objects (virtual or real) in the real world. + +## Table of Contents + +- [Introduction](#introduction) +- [Who is Ethar](#who-is-ethar) +- [Features](#ethar-geopose-implementation-features) +- [Installation](#installation) +- [Usage](#usage) +- [Contributing](#contributing) +- [Terms of Use](#terms-of-use) +- [License](#license) + +## Introduction + +Ethar.GeoPose is a convenient library written in C# for performing GeoPose calculations and integrating them into your applications. + +GeoPose is a standard stewarded by the [Open Geospatial Consortium](https://www.ogc.org/) & supported by members of [Open AR Cloud](https://www.openarcloud.org/) which defines the encodings for the real world position and orientation of a real or a digital object in a machine-readable form. + +This standard describes a geographically-anchored pose (GeoPose) with 6 degrees of freedom referenced to one or more standardized Coordinate Reference Systems (CRSs). This provides an interoperable way to seamlessly express, record, and share the GeoPose of objects in an entirely consistent manner across different applications, users, devices, services, and platforms which adopt the standard or are able to translate/exchange the GeoPose into another CRS. + +## Who is Ethar? + +[Ethar, Inc.](https://www.ethar.com/) is a spatial computing company focused on delivering tools for the entire life-cycle of XR content. We believe the future of spatial computing is open and cooperative, we strive to incorporate open and interoperable technology standards into the very foundation of our products. + +Along the way we have built some useful tooling, that we want to share with the community, and encourage the adoption of open standards. This is our implementation of GeoPose. We think it is one of the most important building blocks of an open, interoperable & decentralized spatial web. + +## Ethar GeoPose Implementation Features + +- Provides an "out of the box" implementation of the [OGC GeoPose specification](https://docs.ogc.org/dis/21-056r10/21-056r10.html). +- Extensions and implementation helpers for various implementations, including C# and Unity. +- Two Code Examples (that demonstrate the transmission of GeoPose data, and creating an authority to transposition GeoPose data). +- Ethar Core Authority implementation (Which implements the most common frame specifications). +- Full Unit Testing of GeoPose concepts and elements. (available via the [GitHub site](https://github.com/EtharInc/Ethar.GeoPose/tree/main/Ethar.GeoPose.UnitTests), not included with npm package) + +> Full documentation on the implementation specification and helper docs, including common guides for C# and Unity can be found at: +> +> [https://etharinc.github.io/Ethar.GeoPose.Docs](https://etharinc.github.io/Ethar.GeoPose.Docs) + +## Installation + +The Ethar GeoPose library and the corresponding Ethar Authority implementation have been provided in as many places as possible, including: + +- NuGet - for cross platform C# development. +- OpenUPM - for versioned Unity deployment. +- Git - Fully open sourced on GitHub. + +> For comments, questions and queries, please log a request on the [Ethar GeoPose GitHub site here](https://github.com/EtharInc/Ethar.GeoPose/issues). + +### Install via NuGet + +```xml + + + +``` + +### Install via OpenUPM + +```text + openupm add com.ethar.GeoPose +``` + +### Unity UPM Git package source + +Using the Unity Package Manager, add a new "Package from Git URL..." using the following path: + +```text + https://github.com/EtharInc/Ethar.GeoPose.git#upm +``` + +### Git Source + +Being open source, all the code, examples and features are available via the GitHub page for the Ethar GeoPose Library here: + +``` text +https://github.com/EtharInc/Ethar.GeoPose +``` + +## Usage + +There are several patterns available when utilizing the GeoPose standard for communicating positional data, ranging from: + +- Basic serialization of GeoPose data types in local storage. +- Consumption of standard SDU definitions for sharing positional data, including an Authority implementation as appropriate. +- Use and the extension of Frame Specifications to compose positional data in more advanced ways. + +The examples provided in the package show basic working patterns in alignment with the first two operations: + +- Example_BasicSerialization.cs - Shows basic data type serialization techniques and consumption of GeoPose data from online sources. +- Example_AuthorityImplementation.cs - A basic example of a working authority implementing two standard Frame Specifications and the authorities management of them. + +> For a more detailed Authority implementation, check the Ethar GeoPose Authority package via [NPM](https://www.npmjs.com/package/ethar.GeoPose.authority), OpenUPM or on [GitHub](https://github.com/EtharInc/Ethar.GeoPose/tree/main/Ethar.GeoPose.Authority). + +## Concepts + +In summary, the following concepts are crucial to understanding the [GeoPose specification](https://docs.ogc.org/dis/21-056r10/21-056r10.html) defined by the OGC, namely: + +- [A Pose, or fixed position for an object.](#pose) +- [The Orientation or direction of a posed object.](#orientation) +- [GeoPose Structural Data Units (SDU) to describe locational metadata.](#structural-data-units) +- [A Frame Specification for GeoPose Data.](#frame-specifications) +- [A GeoPose Authority](#geopose-authorities). + +### Pose + +At the core of the GeoPose definition is the concept of a Pose, that being a position or place where an object is located. A Pose may be determined by a coordinate system relative to a physical location, which may be a GPS, cartesian, image tracked or SLAM position. + +The position is absolute at the time of creation and updated as the need arises. + +### Orientation + +To define the actual direction in which a GeoPosed object is placed, a direction is needed to denote the physical orientation of a placed object. The orientation of objects relative to their position is one of the key factors that sets the GeoPose standard apart from other positioning solutions. + +### Structural Data Units + +The base definition of a GeoPose construct is its ```Structural Data Unit``` definition, which outlines the serialized data that is shared between entities to communicate GeoPosed data. By default, there are 8 Structural Data Units defined within the [GeoPose standard](https://docs.ogc.org/dis/21-056r10/21-056r10.html#toc45), which are: + +- Basic YawPitchRoll - Basic positioning using WGS84 coordinates for position and Euler angles for orientation. +- Basic Quaternion - Basic positioning using WGS84 coordinates for position and a Quaternion for orientation. +- Advanced - An advanced concept utilizing a [Frame Specification](#frame-specifications) that defines a reference frame for an object. +- Graph - An SDU that contains a directed acyclic graph representation of the transformational relationships between reference frames defined by [Frame Specifications](#frame-specifications). +- Chain - An SDU that represents a linear sequence of poses linked by full 6DoF transformations, with the first frame in the sequence being extrinsic. +- Regular Series - An ordered set of operations to perform on a GeoPosed object, complete with timed events. +- Irregular Series - An unordered set of operations for use on a GeoPosed Object. +- Stream - Another advanced use case whereby complex operations can be structured, such as animation. + +Structural Data Units consist of base GeoPose Data Types and can contain one or more [Frame Specifications](#frame-specifications) for extending a GeoPosed Object. Which type of SDU you use will largely depend on your use case, and there is always the option of creating your own (at the cost of interoperability). + +> See the [GeoPose standard](https://docs.ogc.org/dis/21-056r10/21-056r10.html#toc45) section on Structural Data Units for more information. + +### Frame Specifications + +A ```Frame Specification``` can take on multiple roles or responsibilities for objects placed using a GeoPose, these can range from Transform Animations, waypoints, relativistic placement and structure. Which frame specifications are used will be largely determined by the use case required and the [SDU](#structural-data-units) that has been implemented. + +At its most simplistic level, Frame Specifications are references used to co-locate a GeoPose object in relation to another physical entity (such as the Earth) or another GeoPose. In advanced cases they can be used to infer animation, or the bounds of a GeoPosed object. + +Unlike SDU's however, Frame Specifications require an authority who is responsible for orchestrating the content of the specification and ultimately, controls how the data is assembled and disassembled for transport. (Different organizations may implement different authorities for managing how they interpret and expose GeoPosed data.) + +> See the [GeoPose standard](https://docs.ogc.org/dis/21-056r10/21-056r10.html#term-frame-specification) section on Frame Specifications for more information. + +### GeoPose Authorities + +An ```Authority``` in the GeoPose standard is the entity responsible for the understanding, conversion and transformation of any Frame Specification. It is defined in the Ethar GeoPose library through an Interface designed to enforce a specific pattern for Authority Implementations, as shown below: + +![IAuthority Interface](https://raw.githubusercontent.com/EtharInc/Ethar.GeoPose/main/Images/architecture/IAuthority-Interface.png)
+*figure 1: IAuthority Interface.* + +The interface defines a single property and several methods required by an Authority for operation, namely: + +- Authority Name - The unique name/identifier for the authority in the form of ```"/GeoPose/1.0"``` +- ConvertJsonToFrameSpec - Method to take in a GeoPose Frame Specification JSON string and output a Frame Specification definition. +- ConvertFrameSpecToJson - Method to take a Frame Specification object and turn it into serialized GeoPose Frame Specification JSON string. +- ConvertJsonToTransitionModel - Method to take a [Transition Model](https://docs.ogc.org/dis/21-056r10/21-056r10.html#toc17) JSON string and output a Transition Model definition. +- ConvertTransitionModelToJson - Method to serialize a [Transition model](https://docs.ogc.org/dis/21-056r10/21-056r10.html#toc17) into a specific GeoPose Transition Model JSON string + +> Additionally, it is recommended to also implement a ```FrameSpecificationValidator``` as part of any Authority implementation, to validate any Frame Specifications and handle any irregularities with incoming data. + +You can see a fully implemented authority implementation [here](https://github.com/EtharInc/Ethar.GeoPose/blob/main/Ethar.GeoPose.Authority/EtharGeoPoseAuthority.cs) or refer to the ```Ethar GeoPose Authority Sample``` included with this package. + +It is Critical, when implementing your own authority, to define the frame specification that the authority is responsible for, including the data types (either c# native or GeoPose elements) and then write ```Converters``` to extract and understand the Frame specifications being handled by the authority, with specific attention to use the ```ValidationUtilities``` provided by the Ethar GeoPose library, for example: + +```csharp +public static ExampleExtrinsicFrameSpec ConvertJObjectToExampleExtrinsicFrameSpec(JObject jObject) +{ + // Validated the incoming json object string and checks that it has the required values and also + // checks if this is the authority mentioned in the incoming data that handles the frame specification. + if (ValidationUtilities.ValidateJsonObjectParameters(jObject, Constants.AuthorityName, out var queryString)) + { + // Retrieves the required data from the json to construct the Frame Specification. + var lat = float.Parse(queryString.GetParameter("latitude")); + var lon = float.Parse(queryString.GetParameter("longitude")); + + // Returns a new instance of the Frame Specification with the data populated. + return new ExampleExtrinsicFrameSpec { Latitude = lat, Longitude = lon }; + } + + return null; +} +``` + +*figure 2: Authority Frame Specification conversion.* + +This ensures that all serialization and deserialization is handled automatically by the authority whenever a Frame Specification (or SDU containing a Frame Specification) is used. + +> For a more detailed explanation, check the [Ethar GeoPose documentation](https://etharinc.github.io/Ethar.GeoPose.Docs). + +### Authority Provider + +To ensure the successful use and access to any implemented GeoPose Authorities, an ```AuthorityProvider``` class is provided as part of this package to Register, Request and remove active Authorities in your solution, this provides a "Single Path" for querying authorities when evaluating incoming data. + +Under the hood, the Authority Provider is simply a Safe Dictionary implemented within a Static class which has proven useful for API-level access within a project. + +The surface of the Authority Provider is as follows: + +![Authority Provider](https://raw.githubusercontent.com/EtharInc/Ethar.GeoPose/main/Images/architecture/AuthorityProvider-utility.png)
+*figure 3: Authority Provider.* + +The utility defines a single exposed property and several methods to safely access the dictionary, namely: + +- Authorities - Read only list of registered authorities. +- RegisterAuthority - Used to register an Authority instance as active. +- UnregisterAuthority - Used to remove an Authority from active use and dispose of it. +- GetAuthority - Safe method for retrieving an Authority by its Name, returns null if not found. + +Use of the AuthorityProvider to manage access to Authorities is recommended when handling incoming GeoPose data to ensure quick and safe access. + +> For more detailed examples of authority registration and use, check the [Ethar GeoPose documentation](https://etharinc.github.io/Ethar.GeoPose.Docs). + +## Contributing + +Ethar.GeoPose is made possible by the excellent work of the Ethar Team: + + + + + + + + + + + +
The Masked Coder??????
Simon JacksonGitHub/SimonDarksideJLinkedIn/xrconsultant
Connor DavisGitHub/john-connor-davisLinkedIn/John-Connor-Davis
Colin SteinmannGitHub/metaColinLinkedIn/colinsteinmann
+ +## Terms of Use + +For full terms of use please refer our [documentation site](https://etharinc.github.io/Ethar.GeoPose.Docs/termsofuse.html). + +## License + +Ethar.GeoPose is distributed under [Apache 2.0 License](https://etharinc.github.io/Ethar.GeoPose.Docs/license.html) diff --git a/Ethar.GeoPose/stylecop.json b/Ethar.GeoPose/stylecop.json new file mode 100644 index 0000000..a3b11d2 --- /dev/null +++ b/Ethar.GeoPose/stylecop.json @@ -0,0 +1,14 @@ +{ + // ACTION REQUIRED: This file was automatically added to your project, but it + // will not take effect until additional steps are taken to enable it. See the + // following page for additional information: + // + // https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/EnableConfiguration.md + + "$schema": "https://raw.githubusercontent.com/DotNetAnalyzers/StyleCopAnalyzers/master/StyleCop.Analyzers/StyleCop.Analyzers/Settings/stylecop.schema.json", + "settings": { + "documentationRules": { + "companyName": "Ethar" + } + } +} diff --git a/Images/architecture/AuthorityProvider-utility.png b/Images/architecture/AuthorityProvider-utility.png new file mode 100644 index 0000000..c14b72e Binary files /dev/null and b/Images/architecture/AuthorityProvider-utility.png differ diff --git a/Images/architecture/IAuthority-Interface.png b/Images/architecture/IAuthority-Interface.png new file mode 100644 index 0000000..7499c52 Binary files /dev/null and b/Images/architecture/IAuthority-Interface.png differ diff --git a/Images/branding/GeoPoseLogoWhiteRGB.svg b/Images/branding/GeoPoseLogoWhiteRGB.svg new file mode 100644 index 0000000..ae7edaf --- /dev/null +++ b/Images/branding/GeoPoseLogoWhiteRGB.svg @@ -0,0 +1,191 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Images/nuget.png b/Images/nuget.png new file mode 100644 index 0000000..9aac5ef Binary files /dev/null and b/Images/nuget.png differ diff --git a/README.md b/README.md new file mode 100644 index 0000000..4ca345e --- /dev/null +++ b/README.md @@ -0,0 +1,249 @@ +

+ GeoPose Logo +

+ +[![NuGet Badge](https://buildstats.info/nuget/Ethar.GeoPose)](https://www.nuget.org/packages/Ethar.GeoPose) +[![CI](https://github.com/etharinc/Ethar.GeoPose/actions/workflows/main-release.yml/badge.svg?branch=main)](https://github.com/etharinc/MQTTnet/actions/workflows/main-release.yml) +[![NPM](https://badgen.net/npm/v/Ethar.GeoPose)](https://npmjs.org/package/Ethar.GeoPose) +![Size](https://img.shields.io/github/repo-size/etharinc/MQTTnet.svg) +[![License: Apache 2.0](https://img.shields.io/badge/License-APACHE2.0-green.svg)](https://raw.githubusercontent.com/etharinc/Ethar.GeoPose/main/LICENSE) + +# GeoPose Library by [Ethar, Inc.](https://www.ethar.com/) + +[![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](https://github.com/EtharInc/Ethar.GeoPose/blob/main/LICENSE) + +The [GeoPose standard](https://docs.ogc.org/dis/21-056r10/21-056r10.html) enables the easy integration of digital elements on and in relation to the surface of the planet. + +Ethar.GeoPose is a C# library that implements GeoPose, allowing you to assign precise 3D position and orientation of objects (virtual or real) in the real world. + +## Table of Contents + +- [Introduction](#introduction) +- [Who is Ethar](#who-is-ethar) +- [Features](#ethar-geopose-implementation-features) +- [Installation](#installation) +- [Usage](#usage) +- [Contributing](#contributing) +- [Terms of Use](#terms-of-use) +- [License](#license) + +## Introduction + +Ethar.GeoPose is a convenient library written in C# for performing GeoPose calculations and integrating them into your applications. + +GeoPose is a standard stewarded by the [Open Geospatial Consortium](https://www.ogc.org/) & supported by members of [Open AR Cloud](https://www.openarcloud.org/) which defines the encodings for the real world position and orientation of a real or a digital object in a machine-readable form. + +This standard describes a geographically-anchored pose (GeoPose) with 6 degrees of freedom referenced to one or more standardized Coordinate Reference Systems (CRSs). This provides an interoperable way to seamlessly express, record, and share the GeoPose of objects in an entirely consistent manner across different applications, users, devices, services, and platforms which adopt the standard or are able to translate/exchange the GeoPose into another CRS. + +## Who is Ethar? + +[Ethar, Inc.](https://www.ethar.com/) is a spatial computing company focused on delivering tools for the entire life-cycle of XR content. We believe the future of spatial computing is open and cooperative, we strive to incorporate open and interoperable technology standards into the very foundation of our products. + +Along the way we have built some useful tooling, that we want to share with the community, and encourage the adoption of open standards. This is our implementation of GeoPose. We think it is one of the most important building blocks of an open, interoperable & decentralized spatial web. + +## Ethar GeoPose Implementation Features + +- Provides an "out of the box" implementation of the [OGC GeoPose specification](https://docs.ogc.org/dis/21-056r10/21-056r10.html). +- Extensions and implementation helpers for various implementations, including C# and Unity. +- Two Code Examples (that demonstrate the transmission of GeoPose data, and creating an authority to transposition GeoPose data). +- Ethar Core Authority implementation (Which implements the most common frame specifications). +- Full Unit Testing of GeoPose concepts and elements. + +> Full documentation on the implementation specification and helper docs, including common guides for C# and Unity can be found at: +> +> [https://etharinc.github.io/Ethar.GeoPose.Docs](https://etharinc.github.io/Ethar.GeoPose.Docs) + +## Installation + +The Ethar GeoPose library and the corresponding Ethar Authority implementation have been provided in as many places as possible, including: + +- NuGet - for cross platform C# development. +- OpenUPM - for versioned Unity deployment. +- Git - Fully open sourced on GitHub. + +> For comments, questions and queries, please log a request on the [Ethar GeoPose GitHub site here](https://github.com/EtharInc/Ethar.GeoPose/issues). + +### Install via NuGet + +```xml + + + + +``` + +### Install via OpenUPM + +```text + openupm add com.ethar.GeoPose +``` + +### Unity UPM Git package source + +Using the Unity Package Manager, add a new "Package from Git URL..." using the following path: + +```text + https://github.com/EtharInc/Ethar.GeoPose.git#upm +``` + +### Git Source + +Being open source, all the code, examples and features are available via the GitHub page for the Ethar GeoPose Library here: + +``` text +https://github.com/EtharInc/Ethar.GeoPose +``` + +## Usage + +There are several patterns available when utilizing the GeoPose standard for communicating positional data, ranging from: + +- Basic serialization of GeoPose data types in local storage. +- Consumption of standard SDU definitions for sharing positional data, including an Authority implementation as appropriate. +- Use and the extension of Frame Specifications to compose positional data in more advanced ways. + +The examples provided in the package show basic working patterns in alignment with the first two operations: + +- Example_BasicSerialization.cs - Shows basic data type serialization techniques and consumption of GeoPose data from online sources. +- Example_AuthorityImplementation.cs - A basic example of a working authority implementing two standard Frame Specifications and the authorities management of them. + +> For a more detailed Authority implementation, check the Ethar GeoPose Authority package via [NPM](https://www.npmjs.com/package/ethar.GeoPose.authority), OpenUPM or on [GitHub](https://github.com/EtharInc/Ethar.GeoPose/tree/main/Ethar.GeoPose.Authority). + +## Concepts + +In summary, the following concepts are crucial to understanding the [GeoPose specification](https://docs.ogc.org/dis/21-056r10/21-056r10.html) defined by the OGC, namely: + +- [A Pose, or fixed position for an object.](#pose) +- [The Orientation or direction of a posed object.](#orientation) +- [GeoPose Structural Data Units (SDU) to describe locational metadata.](#structural-data-units) +- [A Frame Specification for GeoPose Data.](#frame-specifications) +- [A GeoPose Authority](#geopose-authorities). + +### Pose + +At the core of the GeoPose definition is the concept of a Pose, that being a position or place where an object is located. A Pose may be determined by a coordinate system relative to a physical location, which may be a GPS, cartesian, image tracked or SLAM position. + +The position is absolute at the time of creation and updated as the need arises. + +### Orientation + +To define the actual direction in which a GeoPosed object is placed, a direction is needed to denote the physical orientation of a placed object. The orientation of objects relative to their position is one of the key factors that sets the GeoPose standard apart from other positioning solutions. + +### Structural Data Units + +The base definition of a GeoPose construct is its ```Structural Data Unit``` definition, which outlines the serialized data that is shared between entities to communicate GeoPosed data. By default, there are 8 Structural Data Units defined within the [GeoPose standard](https://docs.ogc.org/dis/21-056r10/21-056r10.html#toc45), which are: + +- Basic YawPitchRoll - Basic positioning using WGS84 coordinates for position and Euler angles for orientation. +- Basic Quaternion - Basic positioning using WGS84 coordinates for position and a Quaternion for orientation. +- Advanced - An advanced concept utilizing a [Frame Specification](#frame-specifications) that defines a reference frame for an object. +- Graph - An SDU that contains a directed acyclic graph representation of the transformational relationships between reference frames defined by [Frame Specifications](#frame-specifications). +- Chain - An SDU that represents a linear sequence of poses linked by full 6DoF transformations, with the first frame in the sequence being extrinsic. +- Regular Series - An ordered set of operations to perform on a GeoPosed object, complete with timed events. +- Irregular Series - An unordered set of operations for use on a GeoPosed Object. +- Stream - Another advanced use case whereby complex operations can be structured, such as animation. + +Structural Data Units consist of base GeoPose Data Types and can contain one or more [Frame Specifications](#frame-specifications) for extending a GeoPosed Object. Which type of SDU you use will largely depend on your use case, and there is always the option of creating your own (at the cost of interoperability). + +> See the [GeoPose standard](https://docs.ogc.org/dis/21-056r10/21-056r10.html#toc45) section on Structural Data Units for more information. + +### Frame Specifications + +A ```Frame Specification``` can take on multiple roles or responsibilities for objects placed using a GeoPose, these can range from Transform Animations, waypoints, relativistic placement and structure. Which frame specifications are used will be largely determined by the use case required and the [SDU](#structural-data-units) that has been implemented. + +At its most simplistic level, Frame Specifications are references used to co-locate a GeoPose object in relation to another physical entity (such as the Earth) or another GeoPose. In advanced cases they can be used to infer animation, or the bounds of a GeoPosed object. + +Unlike SDU's however, Frame Specifications require an authority who is responsible for orchestrating the content of the specification and ultimately, controls how the data is assembled and disassembled for transport. (Different organizations may implement different authorities for managing how they interpret and expose GeoPosed data.) + +> See the [GeoPose standard](https://docs.ogc.org/dis/21-056r10/21-056r10.html#term-frame-specification) section on Frame Specifications for more information. + +### GeoPose Authorities + +An ```Authority``` in the GeoPose standard is the entity responsible for the understanding, conversion and transformation of any Frame Specification. It is defined in the Ethar GeoPose library through an Interface designed to enforce a specific pattern for Authority Implementations, as shown below: + +![IAuthority Interface](https://raw.githubusercontent.com/EtharInc/Ethar.GeoPose/main/Images/architecture/IAuthority-Interface.png?raw=true)
+*figure 1: IAuthority Interface.* + +The interface defines a single property and several methods required by an Authority for operation, namely: + +- Authority Name - The unique name/identifier for the authority in the form of ```"/GeoPose/1.0"``` +- ConvertJsonToFrameSpec - Method to take in a GeoPose Frame Specification JSON string and output a Frame Specification definition. +- ConvertFrameSpecToJson - Method to take a Frame Specification object and turn it into serialized GeoPose Frame Specification JSON string. +- ConvertJsonToTransitionModel - Method to take a [Transition Model](https://docs.ogc.org/dis/21-056r10/21-056r10.html#toc17) JSON string and output a Transition Model definition. +- ConvertTransitionModelToJson - Method to serialize a [Transition model](https://docs.ogc.org/dis/21-056r10/21-056r10.html#toc17) into a specific GeoPose Transition Model JSON string + +> Additionally, it is recommended to also implement a ```FrameSpecificationValidator``` as part of any Authority implementation, to validate any Frame Specifications and handle any irregularities with incoming data. + +You can see a fully implemented authority implementation [here](https://github.com/EtharInc/Ethar.GeoPose/blob/main/Ethar.GeoPose.Authority/EtharGeoPoseAuthority.cs) or refer to the ```Ethar GeoPose Authority Sample``` included with this package. + +It is Critical, when implementing your own authority, to define the frame specification that the authority is responsible for, including the data types (either c# native or GeoPose elements) and then write ```Converters``` to extract and understand the Frame specifications being handled by the authority, with specific attention to use the ```ValidationUtilities``` provided by the Ethar GeoPose library, for example: + +```csharp +public static ExampleExtrinsicFrameSpec ConvertJObjectToExampleExtrinsicFrameSpec(JObject jObject) +{ + // Validated the incoming json object string and checks that it has the required values and also + // checks if this is the authority mentioned in the incoming data that handles the frame specification. + if (ValidationUtilities.ValidateJsonObjectParameters(jObject, Constants.AuthorityName, out var queryString)) + { + // Retrieves the required data from the json to construct the Frame Specification. + var lat = float.Parse(queryString.Get("latitude")); + var lon = float.Parse(queryString.Get("longitude")); + + // Returns a new instance of the Frame Specification with the data populated. + return new ExampleExtrinsicFrameSpec { Latitude = lat, Longitude = lon }; + } + + return null; +} +``` + +*figure 2: Authority Frame Specification conversion.* + +This ensures that all serialization and deserialization is handled automatically by the authority whenever a Frame Specification (or SDU containing a Frame Specification) is used. + +> For a more detailed explanation, check the [Ethar GeoPose documentation](https://etharinc.github.io/Ethar.GeoPose.Docs). + +### Authority Provider + +To ensure the successful use and access to any implemented GeoPose Authorities, an ```AuthorityProvider``` class is provided as part of this package to Register, Request and remove active Authorities in your solution, this provides a "Single Path" for querying authorities when evaluating incoming data. + +Under the hood, the Authority Provider is simply a Safe Dictionary implemented within a Static class which has proven useful for API-level access within a project. + +The surface of the Authority Provider is as follows: + +![Authority Provider](https://raw.githubusercontent.com/EtharInc/Ethar.GeoPose/main/Images/architecture/AuthorityProvider-utility.png)
+*figure 3: Authority Provider.* + +The utility defines a single exposed property and several methods to safely access the dictionary, namely: + +- Authorities - Read only list of registered authorities. +- RegisterAuthority - Used to register an Authority instance as active. +- UnregisterAuthority - Used to remove an Authority from active use and dispose of it. +- GetAuthority - Safe method for retrieving an Authority by its Name, returns null if not found. + +Use of the AuthorityProvider to manage access to Authorities is recommended when handling incoming GeoPose data to ensure quick and safe access. + +> For more detailed examples of authority registration and use, check the [Ethar GeoPose documentation](https://etharinc.github.io/Ethar.GeoPose.Docs). + +## Contributing + +Ethar.GeoPose is made possible by the excellent work of the Ethar Team: + + + + + + + + + + + +
The Masked Coder??????
Simon JacksonGitHub/SimonDarksideJLinkedIn/xrconsultant
Connor DavisGitHub/john-connor-davisLinkedIn/John-Connor-Davis
Colin SteinmannGitHub/metaColinLinkedIn/colinsteinmann
+ +## Terms of Use + +For full terms of use please refer our [documentation site](https://etharinc.github.io/Ethar.GeoPose.Docs/termsofuse.html). + +## License + +Ethar.GeoPose is distributed under [Apache 2.0 License](https://etharinc.github.io/Ethar.GeoPose.Docs/license.html)