diff --git a/.claude/settings.json b/.claude/settings.json
new file mode 100644
index 0000000..72c82ef
--- /dev/null
+++ b/.claude/settings.json
@@ -0,0 +1,45 @@
+{
+ "permissions": {
+ "allow": [
+ "Read(//C:/GitHub/**)",
+ "Read(//C:/Program Files/Microsoft Visual Studio/**)",
+
+ "Edit(/GeoMagGUI/**)",
+ "Edit(/GeoMagSharp/**)",
+ "Edit(/GeoMagSharp-UnitTests/**)",
+ "Edit(/Installer/**)",
+ "Edit(/docs/**)",
+ "Edit(/.github/**)",
+ "Edit(/.claude/**)",
+ "Edit(/*.md)",
+ "Edit(/*.sln)",
+ "Edit(/*.props)",
+ "Edit(/*.json)",
+ "Edit(/.gitignore)",
+
+ "Bash(msbuild *)",
+ "Bash(\"C:/Program Files/Microsoft Visual Studio/2022/Community/MSBuild/Current/Bin/MSBuild.exe\" *)",
+ "Bash(vstest.console.exe *)",
+ "Bash(\"C:/Program Files/Microsoft Visual Studio/2022/Community/Common7/IDE/CommonExtensions/Microsoft/TestWindow/vstest.console.exe\" *)",
+ "Bash(nuget *)",
+ "Bash(git status *)",
+ "Bash(git log *)",
+ "Bash(git diff *)",
+ "Bash(git branch *)",
+ "Bash(git checkout *)",
+ "Bash(git stash *)",
+ "Bash(git ls-tree *)",
+ "Bash(git remote *)",
+ "Bash(gh pr *)",
+ "Bash(gh issue *)",
+ "Bash(gh api *)",
+ "Bash(gh run *)"
+ ],
+ "deny": [
+ "Bash(git push --force *)",
+ "Bash(git reset --hard *)",
+ "Bash(rm -rf *)",
+ "Bash(del /s *)"
+ ]
+ }
+}
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index fb2c048..bee52f0 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -1,23 +1,19 @@
-name: Build and Package
+name: Build and Test
on:
push:
- branches: [ master, preview ]
+ branches: [ development, 'feature/**' ]
pull_request:
- branches: [ master, preview ]
+ branches: [ development ]
workflow_dispatch:
env:
SOLUTION_FILE: GeoMagGUI.sln
- MAIN_PROJECT: GeoMagGUI
CONFIGURATION: Release
jobs:
- build:
+ build-and-test:
runs-on: windows-latest
- outputs:
- full_version: ${{ steps.version.outputs.full_version }}
- artifact_suffix: ${{ steps.version.outputs.artifact_suffix }}
steps:
- name: Checkout code
@@ -29,41 +25,20 @@ jobs:
id: version
shell: pwsh
run: |
- # Read base version from Version.props
[xml]$versionProps = Get-Content "Version.props"
$major = $versionProps.Project.PropertyGroup.MajorVersion
$minor = $versionProps.Project.PropertyGroup.MinorVersion
$patch = $versionProps.Project.PropertyGroup.PatchVersion
-
$baseVersion = "$major.$minor.$patch"
-
- # Determine channel and build number based on branch
- $branch = "${{ github.ref_name }}"
$runNumber = "${{ github.run_number }}"
+ $fullVersion = "$baseVersion.$runNumber"
+ $version = "$baseVersion-dev.$runNumber"
- if ($branch -eq "master") {
- $channel = "release"
- $fullVersion = "$baseVersion.0"
- $artifactSuffix = ""
- } elseif ($branch -eq "preview") {
- $channel = "preview"
- $fullVersion = "$baseVersion.$runNumber"
- $artifactSuffix = "-preview.$runNumber"
- } else {
- $channel = "dev"
- $fullVersion = "$baseVersion.$runNumber"
- $artifactSuffix = "-dev.$runNumber"
- }
-
- Write-Host "Branch: $branch"
- Write-Host "Channel: $channel"
+ Write-Host "Version: $version"
Write-Host "Full Version: $fullVersion"
- Write-Host "Artifact Suffix: $artifactSuffix"
- echo "base_version=$baseVersion" >> $env:GITHUB_OUTPUT
- echo "full_version=$fullVersion" >> $env:GITHUB_OUTPUT
- echo "channel=$channel" >> $env:GITHUB_OUTPUT
- echo "artifact_suffix=$artifactSuffix" >> $env:GITHUB_OUTPUT
+ echo "VERSION=$version" >> $env:GITHUB_OUTPUT
+ echo "FULL_VERSION=$fullVersion" >> $env:GITHUB_OUTPUT
- name: Setup MSBuild
uses: microsoft/setup-msbuild@v2
@@ -77,16 +52,14 @@ jobs:
- name: Update AssemblyInfo versions
shell: pwsh
run: |
- $version = "${{ steps.version.outputs.full_version }}"
+ $version = "${{ steps.version.outputs.FULL_VERSION }}"
- # Update GeoMagGUI AssemblyInfo
$guiAssemblyInfo = "GeoMagGUI\Properties\AssemblyInfo.cs"
$content = Get-Content $guiAssemblyInfo -Raw
$content = $content -replace 'AssemblyVersion\("[^"]+"\)', "AssemblyVersion(`"$version`")"
$content = $content -replace 'AssemblyFileVersion\("[^"]+"\)', "AssemblyFileVersion(`"$version`")"
Set-Content $guiAssemblyInfo $content -NoNewline
- # Update GeoMagSharp AssemblyInfo
$sharpAssemblyInfo = "GeoMagSharp\Properties\AssemblyInfo.cs"
$content = Get-Content $sharpAssemblyInfo -Raw
$content = $content -replace 'AssemblyVersion\("[^"]+"\)', "AssemblyVersion(`"$version`")"
@@ -99,13 +72,10 @@ jobs:
run: msbuild ${{ env.SOLUTION_FILE }} /p:Configuration=${{ env.CONFIGURATION }} /p:Platform="Mixed Platforms" /m
- name: Run tests
- continue-on-error: true
shell: pwsh
run: |
- # Find VSTest console
$vsTestPath = & "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vswhere.exe" -latest -products * -requires Microsoft.VisualStudio.Workload.ManagedDesktop -find Common7\IDE\CommonExtensions\Microsoft\TestWindow\vstest.console.exe
if (-not $vsTestPath) {
- # Fallback to common paths
$vsTestPath = Get-ChildItem -Path "${env:ProgramFiles}\Microsoft Visual Studio" -Recurse -Filter "vstest.console.exe" -ErrorAction SilentlyContinue | Select-Object -First 1 -ExpandProperty FullName
}
@@ -115,118 +85,11 @@ jobs:
Write-Host "VSTest not found, skipping tests"
}
- - name: Upload test results
+ - name: Upload test results (only on failure)
uses: actions/upload-artifact@v4
- if: always()
+ if: failure()
with:
- name: test-results${{ steps.version.outputs.artifact_suffix }}
+ name: test-results-dev-${{ steps.version.outputs.VERSION }}
path: TestResults/*.trx
if-no-files-found: ignore
-
- # Installer steps only run on push events (not pull requests)
- - name: Install WiX Toolset
- if: github.event_name == 'push'
- shell: pwsh
- run: |
- dotnet tool install --global wix --version 5.0.2
-
- - name: Generate WiX file components
- if: github.event_name == 'push'
- shell: pwsh
- run: |
- $buildOutput = "GeoMagGUI\bin\Release"
- $version = "${{ steps.version.outputs.full_version }}"
-
- # Run the component generation script
- & "Installer\Generate-FileComponents.ps1" -BuildOutputPath $buildOutput -Version $version
-
- - name: Build MSI installer
- if: github.event_name == 'push'
- shell: pwsh
- run: |
- $version = "${{ steps.version.outputs.full_version }}"
- $channel = "${{ steps.version.outputs.channel }}"
-
- cd Installer
-
- # Build the MSI
- wix build -arch x86 `
- -d Version=$version `
- -d Channel=$channel `
- -d BuildOutput=..\GeoMagGUI\bin\Release `
- -out ..\artifacts\GeoMagGUI${{ steps.version.outputs.artifact_suffix }}.msi `
- Product.wxs `
- FileComponents.wxs
-
- - name: Upload MSI artifact
- if: github.event_name == 'push'
- uses: actions/upload-artifact@v4
- with:
- name: GeoMagGUI${{ steps.version.outputs.artifact_suffix }}-msi
- path: artifacts/*.msi
-
- - name: Upload build output
- if: github.event_name == 'push'
- uses: actions/upload-artifact@v4
- with:
- name: GeoMagGUI${{ steps.version.outputs.artifact_suffix }}-bin
- path: |
- GeoMagGUI/bin/Release/
- !GeoMagGUI/bin/Release/**/*.pdb
-
- create-release:
- needs: build
- runs-on: ubuntu-latest
- if: github.ref == 'refs/heads/master' && github.event_name == 'push'
-
- steps:
- - name: Checkout code
- uses: actions/checkout@v4
- with:
- fetch-depth: 0
-
- - name: Download MSI artifact
- uses: actions/download-artifact@v4
- with:
- name: GeoMagGUI-msi
- path: artifacts
-
- - name: Generate release notes
- shell: bash
- run: |
- cat > release_notes.md << EOF
- ## What's New in v${{ needs.build.outputs.full_version }}
-
- ### Features & Improvements
- - Added support for WMM2020+ coefficient file format (Issue #1)
- - Upgraded to .NET Framework 4.8
- - Added CI/CD pipeline with automated MSI installer generation
- - Added comprehensive unit tests for Calculator and ModelReader classes
-
- ### Bug Fixes
- - Fixed critical latitude/longitude type mismatch in DMS input (Issue #2)
- - Fixed cursor management with try-finally blocks (Issue #4)
- - Fixed decimal date to DateTime conversion rounding issue
- - Added input validation to ModelReader with detailed error messages (Issue #5)
-
- ### Issues Resolved
- - #1 - NOAA WMM2020 coefficient file support
- - #2 - Type mismatch: Longitude constructor used for Latitude
- - #3 - Add unit tests for Calculator class
- - #4 - Add try-finally for cursor management
- - #5 - Add input validation to ModelReader
-
- ---
- *Full changelog available in the [commit history](https://github.com/${{ github.repository }}/commits/master)*
- EOF
-
- - name: Create Release
- uses: softprops/action-gh-release@v2
- with:
- tag_name: v${{ needs.build.outputs.full_version }}
- name: GeoMag # ${{ needs.build.outputs.full_version }}
- body_path: release_notes.md
- draft: false
- files: artifacts/*.msi
- env:
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ retention-days: 3
diff --git a/.github/workflows/preview-release.yml b/.github/workflows/preview-release.yml
new file mode 100644
index 0000000..c324d60
--- /dev/null
+++ b/.github/workflows/preview-release.yml
@@ -0,0 +1,122 @@
+name: Preview Release
+
+on:
+ push:
+ branches: [ preview ]
+ workflow_dispatch:
+
+permissions:
+ contents: write
+
+env:
+ SOLUTION_FILE: GeoMagGUI.sln
+ CONFIGURATION: Release
+
+jobs:
+ build-and-release:
+ runs-on: windows-latest
+
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+
+ - name: Calculate version
+ id: version
+ shell: pwsh
+ run: |
+ [xml]$versionProps = Get-Content "Version.props"
+ $major = $versionProps.Project.PropertyGroup.MajorVersion
+ $minor = $versionProps.Project.PropertyGroup.MinorVersion
+ $patch = $versionProps.Project.PropertyGroup.PatchVersion
+ $baseVersion = "$major.$minor.$patch"
+ $runNumber = "${{ github.run_number }}"
+ $fullVersion = "$baseVersion.$runNumber"
+ $version = "$baseVersion-preview.$runNumber"
+ Write-Host "Version: $version"
+ echo "VERSION=$version" >> $env:GITHUB_OUTPUT
+ echo "FULL_VERSION=$fullVersion" >> $env:GITHUB_OUTPUT
+
+ - name: Setup MSBuild
+ uses: microsoft/setup-msbuild@v2
+
+ - name: Setup NuGet
+ uses: NuGet/setup-nuget@v2
+
+ - name: Restore NuGet packages
+ run: nuget restore ${{ env.SOLUTION_FILE }}
+
+ - name: Update AssemblyInfo versions
+ shell: pwsh
+ run: |
+ $version = "${{ steps.version.outputs.FULL_VERSION }}"
+ foreach ($path in @("GeoMagGUI\Properties\AssemblyInfo.cs", "GeoMagSharp\Properties\AssemblyInfo.cs")) {
+ $content = Get-Content $path -Raw
+ $content = $content -replace 'AssemblyVersion\("[^"]+"\)', "AssemblyVersion(`"$version`")"
+ $content = $content -replace 'AssemblyFileVersion\("[^"]+"\)', "AssemblyFileVersion(`"$version`")"
+ Set-Content $path $content -NoNewline
+ }
+ Write-Host "Updated assembly versions to $version"
+
+ - name: Build solution
+ run: msbuild ${{ env.SOLUTION_FILE }} /p:Configuration=${{ env.CONFIGURATION }} /p:Platform="Mixed Platforms" /m
+
+ - name: Run tests
+ shell: pwsh
+ run: |
+ $vsTestPath = & "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vswhere.exe" -latest -products * -requires Microsoft.VisualStudio.Workload.ManagedDesktop -find Common7\IDE\CommonExtensions\Microsoft\TestWindow\vstest.console.exe
+ if (-not $vsTestPath) {
+ $vsTestPath = Get-ChildItem -Path "${env:ProgramFiles}\Microsoft Visual Studio" -Recurse -Filter "vstest.console.exe" -ErrorAction SilentlyContinue | Select-Object -First 1 -ExpandProperty FullName
+ }
+ if ($vsTestPath) {
+ & $vsTestPath "GeoMagSharp-UnitTests\bin\Release\GeoMagSharp-UnitTests.dll" --logger:trx --ResultsDirectory:TestResults
+ } else {
+ Write-Host "VSTest not found, skipping tests"
+ }
+
+ - name: Install WiX Toolset
+ shell: pwsh
+ run: dotnet tool install --global wix --version 5.0.2
+
+ - name: Generate WiX file components
+ shell: pwsh
+ run: |
+ $buildOutput = "GeoMagGUI\bin\Release"
+ $version = "${{ steps.version.outputs.FULL_VERSION }}"
+ & "Installer\Generate-FileComponents.ps1" -BuildOutputPath $buildOutput -Version $version
+
+ - name: Build MSI installer
+ shell: pwsh
+ run: |
+ $version = "${{ steps.version.outputs.FULL_VERSION }}"
+ cd Installer
+ wix build -arch x86 -d Version=$version -d Channel=preview -d BuildOutput=..\GeoMagGUI\bin\Release -out ..\artifacts\GeoMagGUI-${{ steps.version.outputs.VERSION }}.msi Product.wxs FileComponents.wxs
+
+ - name: Generate release notes
+ shell: pwsh
+ run: |
+ $lastTag = $null
+ try { $lastTag = git describe --tags --match "v*-preview.*" --abbrev=0 2>$null } catch { }
+ if ($lastTag) { $commits = git log --oneline "$lastTag..HEAD" 2>$null }
+ else { $commits = git log --oneline -20 2>$null }
+ $version = "${{ steps.version.outputs.VERSION }}"
+ $notes = "Preview Release $version`n`n### Changes`n"
+ if ($commits) { $commits -split "`n" | ForEach-Object { $l = $_.Trim(); if ($l) { $notes += "- $l`n" } } }
+ else { $notes += "- Build from latest preview branch`n" }
+ Set-Content -Path "release-notes.md" -Value $notes -Encoding UTF8
+
+ - name: Create GitHub Pre-release
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ shell: pwsh
+ run: |
+ $version = "${{ steps.version.outputs.VERSION }}"
+ gh release create "v$version" "artifacts\GeoMagGUI-$version.msi" --title "GeoMag GUI $version" --notes-file "release-notes.md" --prerelease
+
+ - name: Upload MSI artifact
+ uses: actions/upload-artifact@v4
+ with:
+ name: GeoMagGUI-preview-${{ steps.version.outputs.VERSION }}-msi
+ path: artifacts/*.msi
+ retention-days: 90
diff --git a/.github/workflows/production-release.yml b/.github/workflows/production-release.yml
new file mode 100644
index 0000000..4aea57c
--- /dev/null
+++ b/.github/workflows/production-release.yml
@@ -0,0 +1,125 @@
+name: Production Release
+
+on:
+ push:
+ branches: [ master ]
+ workflow_dispatch:
+
+permissions:
+ contents: write
+
+env:
+ SOLUTION_FILE: GeoMagGUI.sln
+ CONFIGURATION: Release
+
+jobs:
+ build-and-release:
+ runs-on: windows-latest
+
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+
+ - name: Calculate version
+ id: version
+ shell: pwsh
+ run: |
+ [xml]$versionProps = Get-Content "Version.props"
+ $major = $versionProps.Project.PropertyGroup.MajorVersion
+ $minor = $versionProps.Project.PropertyGroup.MinorVersion
+ $patch = $versionProps.Project.PropertyGroup.PatchVersion
+ $baseVersion = "$major.$minor.$patch"
+ $runNumber = "${{ github.run_number }}"
+ $fullVersion = "$baseVersion.$runNumber"
+ $version = "$baseVersion"
+ Write-Host "Version: $version"
+ echo "VERSION=$version" >> $env:GITHUB_OUTPUT
+ echo "FULL_VERSION=$fullVersion" >> $env:GITHUB_OUTPUT
+
+ - name: Setup MSBuild
+ uses: microsoft/setup-msbuild@v2
+
+ - name: Setup NuGet
+ uses: NuGet/setup-nuget@v2
+
+ - name: Restore NuGet packages
+ run: nuget restore ${{ env.SOLUTION_FILE }}
+
+ - name: Update AssemblyInfo versions
+ shell: pwsh
+ run: |
+ $version = "${{ steps.version.outputs.FULL_VERSION }}"
+ foreach ($path in @("GeoMagGUI\Properties\AssemblyInfo.cs", "GeoMagSharp\Properties\AssemblyInfo.cs")) {
+ $content = Get-Content $path -Raw
+ $content = $content -replace 'AssemblyVersion\("[^"]+"\)', "AssemblyVersion(`"$version`")"
+ $content = $content -replace 'AssemblyFileVersion\("[^"]+"\)', "AssemblyFileVersion(`"$version`")"
+ Set-Content $path $content -NoNewline
+ }
+ Write-Host "Updated assembly versions to $version"
+
+ - name: Build solution
+ run: msbuild ${{ env.SOLUTION_FILE }} /p:Configuration=${{ env.CONFIGURATION }} /p:Platform="Mixed Platforms" /m
+
+ - name: Run tests
+ shell: pwsh
+ run: |
+ $vsTestPath = & "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vswhere.exe" -latest -products * -requires Microsoft.VisualStudio.Workload.ManagedDesktop -find Common7\IDE\CommonExtensions\Microsoft\TestWindow\vstest.console.exe
+ if (-not $vsTestPath) {
+ $vsTestPath = Get-ChildItem -Path "${env:ProgramFiles}\Microsoft Visual Studio" -Recurse -Filter "vstest.console.exe" -ErrorAction SilentlyContinue | Select-Object -First 1 -ExpandProperty FullName
+ }
+ if ($vsTestPath) {
+ & $vsTestPath "GeoMagSharp-UnitTests\bin\Release\GeoMagSharp-UnitTests.dll" --logger:trx --ResultsDirectory:TestResults
+ } else {
+ Write-Host "VSTest not found, skipping tests"
+ }
+
+ - name: Install WiX Toolset
+ shell: pwsh
+ run: dotnet tool install --global wix --version 5.0.2
+
+ - name: Generate WiX file components
+ shell: pwsh
+ run: |
+ $buildOutput = "GeoMagGUI\bin\Release"
+ $version = "${{ steps.version.outputs.FULL_VERSION }}"
+ & "Installer\Generate-FileComponents.ps1" -BuildOutputPath $buildOutput -Version $version
+
+ - name: Build MSI installer
+ shell: pwsh
+ run: |
+ $version = "${{ steps.version.outputs.FULL_VERSION }}"
+ cd Installer
+ wix build -arch x86 -d Version=$version -d Channel=stable -d BuildOutput=..\GeoMagGUI\bin\Release -out ..\artifacts\GeoMagGUI-${{ steps.version.outputs.VERSION }}.msi Product.wxs FileComponents.wxs
+
+ - name: Generate release notes
+ shell: pwsh
+ run: |
+ $lastTag = $null
+ try {
+ $tags = git tag --list "v[0-9]*" --sort=-version:refname 2>$null
+ foreach ($t in ($tags -split "`n")) { $t = $t.Trim(); if ($t -and $t -notmatch '-') { $lastTag = $t; break } }
+ } catch { }
+ if ($lastTag) { $commits = git log --oneline "$lastTag..HEAD" 2>$null }
+ else { $commits = git log --oneline -30 2>$null }
+ $version = "${{ steps.version.outputs.VERSION }}"
+ $notes = "Release $version`n`n### Changes`n"
+ if ($commits) { $commits -split "`n" | ForEach-Object { $l = $_.Trim(); if ($l) { $notes += "- $l`n" } } }
+ else { $notes += "- Stable release from preview testing`n" }
+ Set-Content -Path "release-notes.md" -Value $notes -Encoding UTF8
+
+ - name: Create GitHub Release
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ shell: pwsh
+ run: |
+ $version = "${{ steps.version.outputs.VERSION }}"
+ gh release create "v$version" "artifacts\GeoMagGUI-$version.msi" --title "GeoMag GUI $version" --notes-file "release-notes.md"
+
+ - name: Upload MSI artifact
+ uses: actions/upload-artifact@v4
+ with:
+ name: GeoMagGUI-stable-${{ steps.version.outputs.VERSION }}-msi
+ path: artifacts/*.msi
+ retention-days: 90
diff --git a/.gitignore b/.gitignore
index d14ac0f..baa6778 100644
--- a/.gitignore
+++ b/.gitignore
@@ -184,3 +184,8 @@ appsettings.local.json
secrets.json
.env
.env.*
+
+# Claude Code local files (allow settings.json to be tracked)
+.claude/*
+!.claude/settings.json
+*.local.md
diff --git a/GeoMagGUI.sln b/GeoMagGUI.sln
index 4694969..c50f204 100644
--- a/GeoMagGUI.sln
+++ b/GeoMagGUI.sln
@@ -1,120 +1,120 @@
-
-Microsoft Visual Studio Solution File, Format Version 12.00
-# Visual Studio 14
-VisualStudioVersion = 14.0.25420.1
-MinimumVisualStudioVersion = 10.0.40219.1
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GeoMagGUI", "GeoMagGUI\GeoMagGUI.csproj", "{F4F726CC-6FE9-4C5A-8C07-7D120EFD957E}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GeoMagSharp", "GeoMagSharp\GeoMagSharp.csproj", "{AE04340D-E45E-4BDC-942A-58BD0424CEFC}"
-EndProject
-Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{63EFDF46-1284-4180-8E56-6580CC47E280}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GeoMagSharp-UnitTests", "GeoMagSharp-UnitTests\GeoMagSharp-UnitTests.csproj", "{EA296E5F-B205-461B-8CB9-659F31869DD6}"
-EndProject
-Global
- GlobalSection(SolutionConfigurationPlatforms) = preSolution
- CD_ROM|Any CPU = CD_ROM|Any CPU
- CD_ROM|Mixed Platforms = CD_ROM|Mixed Platforms
- CD_ROM|x86 = CD_ROM|x86
- Debug|Any CPU = Debug|Any CPU
- Debug|Mixed Platforms = Debug|Mixed Platforms
- Debug|x86 = Debug|x86
- DVD-5|Any CPU = DVD-5|Any CPU
- DVD-5|Mixed Platforms = DVD-5|Mixed Platforms
- DVD-5|x86 = DVD-5|x86
- Release|Any CPU = Release|Any CPU
- Release|Mixed Platforms = Release|Mixed Platforms
- Release|x86 = Release|x86
- SingleImage|Any CPU = SingleImage|Any CPU
- SingleImage|Mixed Platforms = SingleImage|Mixed Platforms
- SingleImage|x86 = SingleImage|x86
- EndGlobalSection
- GlobalSection(ProjectConfigurationPlatforms) = postSolution
- {F4F726CC-6FE9-4C5A-8C07-7D120EFD957E}.CD_ROM|Any CPU.ActiveCfg = Release|x86
- {F4F726CC-6FE9-4C5A-8C07-7D120EFD957E}.CD_ROM|Mixed Platforms.ActiveCfg = Release|x86
- {F4F726CC-6FE9-4C5A-8C07-7D120EFD957E}.CD_ROM|Mixed Platforms.Build.0 = Release|x86
- {F4F726CC-6FE9-4C5A-8C07-7D120EFD957E}.CD_ROM|x86.ActiveCfg = Release|x86
- {F4F726CC-6FE9-4C5A-8C07-7D120EFD957E}.CD_ROM|x86.Build.0 = Release|x86
- {F4F726CC-6FE9-4C5A-8C07-7D120EFD957E}.Debug|Any CPU.ActiveCfg = Debug|x86
- {F4F726CC-6FE9-4C5A-8C07-7D120EFD957E}.Debug|Mixed Platforms.ActiveCfg = Debug|x86
- {F4F726CC-6FE9-4C5A-8C07-7D120EFD957E}.Debug|Mixed Platforms.Build.0 = Debug|x86
- {F4F726CC-6FE9-4C5A-8C07-7D120EFD957E}.Debug|x86.ActiveCfg = Debug|x86
- {F4F726CC-6FE9-4C5A-8C07-7D120EFD957E}.Debug|x86.Build.0 = Debug|x86
- {F4F726CC-6FE9-4C5A-8C07-7D120EFD957E}.DVD-5|Any CPU.ActiveCfg = Debug|x86
- {F4F726CC-6FE9-4C5A-8C07-7D120EFD957E}.DVD-5|Mixed Platforms.ActiveCfg = Debug|x86
- {F4F726CC-6FE9-4C5A-8C07-7D120EFD957E}.DVD-5|Mixed Platforms.Build.0 = Debug|x86
- {F4F726CC-6FE9-4C5A-8C07-7D120EFD957E}.DVD-5|x86.ActiveCfg = Debug|x86
- {F4F726CC-6FE9-4C5A-8C07-7D120EFD957E}.DVD-5|x86.Build.0 = Debug|x86
- {F4F726CC-6FE9-4C5A-8C07-7D120EFD957E}.Release|Any CPU.ActiveCfg = Release|x86
- {F4F726CC-6FE9-4C5A-8C07-7D120EFD957E}.Release|Mixed Platforms.ActiveCfg = Release|x86
- {F4F726CC-6FE9-4C5A-8C07-7D120EFD957E}.Release|Mixed Platforms.Build.0 = Release|x86
- {F4F726CC-6FE9-4C5A-8C07-7D120EFD957E}.Release|x86.ActiveCfg = Release|x86
- {F4F726CC-6FE9-4C5A-8C07-7D120EFD957E}.Release|x86.Build.0 = Release|x86
- {F4F726CC-6FE9-4C5A-8C07-7D120EFD957E}.SingleImage|Any CPU.ActiveCfg = Release|x86
- {F4F726CC-6FE9-4C5A-8C07-7D120EFD957E}.SingleImage|Mixed Platforms.ActiveCfg = Release|x86
- {F4F726CC-6FE9-4C5A-8C07-7D120EFD957E}.SingleImage|Mixed Platforms.Build.0 = Release|x86
- {F4F726CC-6FE9-4C5A-8C07-7D120EFD957E}.SingleImage|x86.ActiveCfg = Release|x86
- {F4F726CC-6FE9-4C5A-8C07-7D120EFD957E}.SingleImage|x86.Build.0 = Release|x86
- {AE04340D-E45E-4BDC-942A-58BD0424CEFC}.CD_ROM|Any CPU.ActiveCfg = Release|Any CPU
- {AE04340D-E45E-4BDC-942A-58BD0424CEFC}.CD_ROM|Any CPU.Build.0 = Release|Any CPU
- {AE04340D-E45E-4BDC-942A-58BD0424CEFC}.CD_ROM|Mixed Platforms.ActiveCfg = Release|Any CPU
- {AE04340D-E45E-4BDC-942A-58BD0424CEFC}.CD_ROM|Mixed Platforms.Build.0 = Release|Any CPU
- {AE04340D-E45E-4BDC-942A-58BD0424CEFC}.CD_ROM|x86.ActiveCfg = Release|Any CPU
- {AE04340D-E45E-4BDC-942A-58BD0424CEFC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {AE04340D-E45E-4BDC-942A-58BD0424CEFC}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {AE04340D-E45E-4BDC-942A-58BD0424CEFC}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
- {AE04340D-E45E-4BDC-942A-58BD0424CEFC}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
- {AE04340D-E45E-4BDC-942A-58BD0424CEFC}.Debug|x86.ActiveCfg = Debug|Any CPU
- {AE04340D-E45E-4BDC-942A-58BD0424CEFC}.DVD-5|Any CPU.ActiveCfg = Debug|Any CPU
- {AE04340D-E45E-4BDC-942A-58BD0424CEFC}.DVD-5|Any CPU.Build.0 = Debug|Any CPU
- {AE04340D-E45E-4BDC-942A-58BD0424CEFC}.DVD-5|Mixed Platforms.ActiveCfg = Debug|Any CPU
- {AE04340D-E45E-4BDC-942A-58BD0424CEFC}.DVD-5|Mixed Platforms.Build.0 = Debug|Any CPU
- {AE04340D-E45E-4BDC-942A-58BD0424CEFC}.DVD-5|x86.ActiveCfg = Debug|Any CPU
- {AE04340D-E45E-4BDC-942A-58BD0424CEFC}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {AE04340D-E45E-4BDC-942A-58BD0424CEFC}.Release|Any CPU.Build.0 = Release|Any CPU
- {AE04340D-E45E-4BDC-942A-58BD0424CEFC}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
- {AE04340D-E45E-4BDC-942A-58BD0424CEFC}.Release|Mixed Platforms.Build.0 = Release|Any CPU
- {AE04340D-E45E-4BDC-942A-58BD0424CEFC}.Release|x86.ActiveCfg = Release|Any CPU
- {AE04340D-E45E-4BDC-942A-58BD0424CEFC}.SingleImage|Any CPU.ActiveCfg = Release|Any CPU
- {AE04340D-E45E-4BDC-942A-58BD0424CEFC}.SingleImage|Any CPU.Build.0 = Release|Any CPU
- {AE04340D-E45E-4BDC-942A-58BD0424CEFC}.SingleImage|Mixed Platforms.ActiveCfg = Release|Any CPU
- {AE04340D-E45E-4BDC-942A-58BD0424CEFC}.SingleImage|Mixed Platforms.Build.0 = Release|Any CPU
- {AE04340D-E45E-4BDC-942A-58BD0424CEFC}.SingleImage|x86.ActiveCfg = Release|Any CPU
- {EA296E5F-B205-461B-8CB9-659F31869DD6}.CD_ROM|Any CPU.ActiveCfg = Release|Any CPU
- {EA296E5F-B205-461B-8CB9-659F31869DD6}.CD_ROM|Any CPU.Build.0 = Release|Any CPU
- {EA296E5F-B205-461B-8CB9-659F31869DD6}.CD_ROM|Mixed Platforms.ActiveCfg = Release|Any CPU
- {EA296E5F-B205-461B-8CB9-659F31869DD6}.CD_ROM|Mixed Platforms.Build.0 = Release|Any CPU
- {EA296E5F-B205-461B-8CB9-659F31869DD6}.CD_ROM|x86.ActiveCfg = Release|Any CPU
- {EA296E5F-B205-461B-8CB9-659F31869DD6}.CD_ROM|x86.Build.0 = Release|Any CPU
- {EA296E5F-B205-461B-8CB9-659F31869DD6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {EA296E5F-B205-461B-8CB9-659F31869DD6}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {EA296E5F-B205-461B-8CB9-659F31869DD6}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
- {EA296E5F-B205-461B-8CB9-659F31869DD6}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
- {EA296E5F-B205-461B-8CB9-659F31869DD6}.Debug|x86.ActiveCfg = Debug|Any CPU
- {EA296E5F-B205-461B-8CB9-659F31869DD6}.Debug|x86.Build.0 = Debug|Any CPU
- {EA296E5F-B205-461B-8CB9-659F31869DD6}.DVD-5|Any CPU.ActiveCfg = Debug|Any CPU
- {EA296E5F-B205-461B-8CB9-659F31869DD6}.DVD-5|Any CPU.Build.0 = Debug|Any CPU
- {EA296E5F-B205-461B-8CB9-659F31869DD6}.DVD-5|Mixed Platforms.ActiveCfg = Debug|Any CPU
- {EA296E5F-B205-461B-8CB9-659F31869DD6}.DVD-5|Mixed Platforms.Build.0 = Debug|Any CPU
- {EA296E5F-B205-461B-8CB9-659F31869DD6}.DVD-5|x86.ActiveCfg = Debug|Any CPU
- {EA296E5F-B205-461B-8CB9-659F31869DD6}.DVD-5|x86.Build.0 = Debug|Any CPU
- {EA296E5F-B205-461B-8CB9-659F31869DD6}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {EA296E5F-B205-461B-8CB9-659F31869DD6}.Release|Any CPU.Build.0 = Release|Any CPU
- {EA296E5F-B205-461B-8CB9-659F31869DD6}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
- {EA296E5F-B205-461B-8CB9-659F31869DD6}.Release|Mixed Platforms.Build.0 = Release|Any CPU
- {EA296E5F-B205-461B-8CB9-659F31869DD6}.Release|x86.ActiveCfg = Release|Any CPU
- {EA296E5F-B205-461B-8CB9-659F31869DD6}.Release|x86.Build.0 = Release|Any CPU
- {EA296E5F-B205-461B-8CB9-659F31869DD6}.SingleImage|Any CPU.ActiveCfg = Release|Any CPU
- {EA296E5F-B205-461B-8CB9-659F31869DD6}.SingleImage|Any CPU.Build.0 = Release|Any CPU
- {EA296E5F-B205-461B-8CB9-659F31869DD6}.SingleImage|Mixed Platforms.ActiveCfg = Release|Any CPU
- {EA296E5F-B205-461B-8CB9-659F31869DD6}.SingleImage|Mixed Platforms.Build.0 = Release|Any CPU
- {EA296E5F-B205-461B-8CB9-659F31869DD6}.SingleImage|x86.ActiveCfg = Release|Any CPU
- {EA296E5F-B205-461B-8CB9-659F31869DD6}.SingleImage|x86.Build.0 = Release|Any CPU
- EndGlobalSection
- GlobalSection(SolutionProperties) = preSolution
- HideSolutionNode = FALSE
- EndGlobalSection
- GlobalSection(NestedProjects) = preSolution
- {EA296E5F-B205-461B-8CB9-659F31869DD6} = {63EFDF46-1284-4180-8E56-6580CC47E280}
- EndGlobalSection
-EndGlobal
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio 14
+VisualStudioVersion = 14.0.25420.1
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GeoMagGUI", "GeoMagGUI\GeoMagGUI.csproj", "{F4F726CC-6FE9-4C5A-8C07-7D120EFD957E}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GeoMagSharp", "GeoMagSharp\GeoMagSharp.csproj", "{AE04340D-E45E-4BDC-942A-58BD0424CEFC}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{63EFDF46-1284-4180-8E56-6580CC47E280}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GeoMagSharp-UnitTests", "GeoMagSharp-UnitTests\GeoMagSharp-UnitTests.csproj", "{EA296E5F-B205-461B-8CB9-659F31869DD6}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ CD_ROM|Any CPU = CD_ROM|Any CPU
+ CD_ROM|Mixed Platforms = CD_ROM|Mixed Platforms
+ CD_ROM|x86 = CD_ROM|x86
+ Debug|Any CPU = Debug|Any CPU
+ Debug|Mixed Platforms = Debug|Mixed Platforms
+ Debug|x86 = Debug|x86
+ DVD-5|Any CPU = DVD-5|Any CPU
+ DVD-5|Mixed Platforms = DVD-5|Mixed Platforms
+ DVD-5|x86 = DVD-5|x86
+ Release|Any CPU = Release|Any CPU
+ Release|Mixed Platforms = Release|Mixed Platforms
+ Release|x86 = Release|x86
+ SingleImage|Any CPU = SingleImage|Any CPU
+ SingleImage|Mixed Platforms = SingleImage|Mixed Platforms
+ SingleImage|x86 = SingleImage|x86
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {F4F726CC-6FE9-4C5A-8C07-7D120EFD957E}.CD_ROM|Any CPU.ActiveCfg = Release|x86
+ {F4F726CC-6FE9-4C5A-8C07-7D120EFD957E}.CD_ROM|Mixed Platforms.ActiveCfg = Release|x86
+ {F4F726CC-6FE9-4C5A-8C07-7D120EFD957E}.CD_ROM|Mixed Platforms.Build.0 = Release|x86
+ {F4F726CC-6FE9-4C5A-8C07-7D120EFD957E}.CD_ROM|x86.ActiveCfg = Release|x86
+ {F4F726CC-6FE9-4C5A-8C07-7D120EFD957E}.CD_ROM|x86.Build.0 = Release|x86
+ {F4F726CC-6FE9-4C5A-8C07-7D120EFD957E}.Debug|Any CPU.ActiveCfg = Debug|x86
+ {F4F726CC-6FE9-4C5A-8C07-7D120EFD957E}.Debug|Mixed Platforms.ActiveCfg = Debug|x86
+ {F4F726CC-6FE9-4C5A-8C07-7D120EFD957E}.Debug|Mixed Platforms.Build.0 = Debug|x86
+ {F4F726CC-6FE9-4C5A-8C07-7D120EFD957E}.Debug|x86.ActiveCfg = Debug|x86
+ {F4F726CC-6FE9-4C5A-8C07-7D120EFD957E}.Debug|x86.Build.0 = Debug|x86
+ {F4F726CC-6FE9-4C5A-8C07-7D120EFD957E}.DVD-5|Any CPU.ActiveCfg = Debug|x86
+ {F4F726CC-6FE9-4C5A-8C07-7D120EFD957E}.DVD-5|Mixed Platforms.ActiveCfg = Debug|x86
+ {F4F726CC-6FE9-4C5A-8C07-7D120EFD957E}.DVD-5|Mixed Platforms.Build.0 = Debug|x86
+ {F4F726CC-6FE9-4C5A-8C07-7D120EFD957E}.DVD-5|x86.ActiveCfg = Debug|x86
+ {F4F726CC-6FE9-4C5A-8C07-7D120EFD957E}.DVD-5|x86.Build.0 = Debug|x86
+ {F4F726CC-6FE9-4C5A-8C07-7D120EFD957E}.Release|Any CPU.ActiveCfg = Release|x86
+ {F4F726CC-6FE9-4C5A-8C07-7D120EFD957E}.Release|Mixed Platforms.ActiveCfg = Release|x86
+ {F4F726CC-6FE9-4C5A-8C07-7D120EFD957E}.Release|Mixed Platforms.Build.0 = Release|x86
+ {F4F726CC-6FE9-4C5A-8C07-7D120EFD957E}.Release|x86.ActiveCfg = Release|x86
+ {F4F726CC-6FE9-4C5A-8C07-7D120EFD957E}.Release|x86.Build.0 = Release|x86
+ {F4F726CC-6FE9-4C5A-8C07-7D120EFD957E}.SingleImage|Any CPU.ActiveCfg = Release|x86
+ {F4F726CC-6FE9-4C5A-8C07-7D120EFD957E}.SingleImage|Mixed Platforms.ActiveCfg = Release|x86
+ {F4F726CC-6FE9-4C5A-8C07-7D120EFD957E}.SingleImage|Mixed Platforms.Build.0 = Release|x86
+ {F4F726CC-6FE9-4C5A-8C07-7D120EFD957E}.SingleImage|x86.ActiveCfg = Release|x86
+ {F4F726CC-6FE9-4C5A-8C07-7D120EFD957E}.SingleImage|x86.Build.0 = Release|x86
+ {AE04340D-E45E-4BDC-942A-58BD0424CEFC}.CD_ROM|Any CPU.ActiveCfg = Release|Any CPU
+ {AE04340D-E45E-4BDC-942A-58BD0424CEFC}.CD_ROM|Any CPU.Build.0 = Release|Any CPU
+ {AE04340D-E45E-4BDC-942A-58BD0424CEFC}.CD_ROM|Mixed Platforms.ActiveCfg = Release|Any CPU
+ {AE04340D-E45E-4BDC-942A-58BD0424CEFC}.CD_ROM|Mixed Platforms.Build.0 = Release|Any CPU
+ {AE04340D-E45E-4BDC-942A-58BD0424CEFC}.CD_ROM|x86.ActiveCfg = Release|Any CPU
+ {AE04340D-E45E-4BDC-942A-58BD0424CEFC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {AE04340D-E45E-4BDC-942A-58BD0424CEFC}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {AE04340D-E45E-4BDC-942A-58BD0424CEFC}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
+ {AE04340D-E45E-4BDC-942A-58BD0424CEFC}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
+ {AE04340D-E45E-4BDC-942A-58BD0424CEFC}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {AE04340D-E45E-4BDC-942A-58BD0424CEFC}.DVD-5|Any CPU.ActiveCfg = Debug|Any CPU
+ {AE04340D-E45E-4BDC-942A-58BD0424CEFC}.DVD-5|Any CPU.Build.0 = Debug|Any CPU
+ {AE04340D-E45E-4BDC-942A-58BD0424CEFC}.DVD-5|Mixed Platforms.ActiveCfg = Debug|Any CPU
+ {AE04340D-E45E-4BDC-942A-58BD0424CEFC}.DVD-5|Mixed Platforms.Build.0 = Debug|Any CPU
+ {AE04340D-E45E-4BDC-942A-58BD0424CEFC}.DVD-5|x86.ActiveCfg = Debug|Any CPU
+ {AE04340D-E45E-4BDC-942A-58BD0424CEFC}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {AE04340D-E45E-4BDC-942A-58BD0424CEFC}.Release|Any CPU.Build.0 = Release|Any CPU
+ {AE04340D-E45E-4BDC-942A-58BD0424CEFC}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
+ {AE04340D-E45E-4BDC-942A-58BD0424CEFC}.Release|Mixed Platforms.Build.0 = Release|Any CPU
+ {AE04340D-E45E-4BDC-942A-58BD0424CEFC}.Release|x86.ActiveCfg = Release|Any CPU
+ {AE04340D-E45E-4BDC-942A-58BD0424CEFC}.SingleImage|Any CPU.ActiveCfg = Release|Any CPU
+ {AE04340D-E45E-4BDC-942A-58BD0424CEFC}.SingleImage|Any CPU.Build.0 = Release|Any CPU
+ {AE04340D-E45E-4BDC-942A-58BD0424CEFC}.SingleImage|Mixed Platforms.ActiveCfg = Release|Any CPU
+ {AE04340D-E45E-4BDC-942A-58BD0424CEFC}.SingleImage|Mixed Platforms.Build.0 = Release|Any CPU
+ {AE04340D-E45E-4BDC-942A-58BD0424CEFC}.SingleImage|x86.ActiveCfg = Release|Any CPU
+ {EA296E5F-B205-461B-8CB9-659F31869DD6}.CD_ROM|Any CPU.ActiveCfg = Release|Any CPU
+ {EA296E5F-B205-461B-8CB9-659F31869DD6}.CD_ROM|Any CPU.Build.0 = Release|Any CPU
+ {EA296E5F-B205-461B-8CB9-659F31869DD6}.CD_ROM|Mixed Platforms.ActiveCfg = Release|Any CPU
+ {EA296E5F-B205-461B-8CB9-659F31869DD6}.CD_ROM|Mixed Platforms.Build.0 = Release|Any CPU
+ {EA296E5F-B205-461B-8CB9-659F31869DD6}.CD_ROM|x86.ActiveCfg = Release|Any CPU
+ {EA296E5F-B205-461B-8CB9-659F31869DD6}.CD_ROM|x86.Build.0 = Release|Any CPU
+ {EA296E5F-B205-461B-8CB9-659F31869DD6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {EA296E5F-B205-461B-8CB9-659F31869DD6}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {EA296E5F-B205-461B-8CB9-659F31869DD6}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
+ {EA296E5F-B205-461B-8CB9-659F31869DD6}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
+ {EA296E5F-B205-461B-8CB9-659F31869DD6}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {EA296E5F-B205-461B-8CB9-659F31869DD6}.Debug|x86.Build.0 = Debug|Any CPU
+ {EA296E5F-B205-461B-8CB9-659F31869DD6}.DVD-5|Any CPU.ActiveCfg = Debug|Any CPU
+ {EA296E5F-B205-461B-8CB9-659F31869DD6}.DVD-5|Any CPU.Build.0 = Debug|Any CPU
+ {EA296E5F-B205-461B-8CB9-659F31869DD6}.DVD-5|Mixed Platforms.ActiveCfg = Debug|Any CPU
+ {EA296E5F-B205-461B-8CB9-659F31869DD6}.DVD-5|Mixed Platforms.Build.0 = Debug|Any CPU
+ {EA296E5F-B205-461B-8CB9-659F31869DD6}.DVD-5|x86.ActiveCfg = Debug|Any CPU
+ {EA296E5F-B205-461B-8CB9-659F31869DD6}.DVD-5|x86.Build.0 = Debug|Any CPU
+ {EA296E5F-B205-461B-8CB9-659F31869DD6}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {EA296E5F-B205-461B-8CB9-659F31869DD6}.Release|Any CPU.Build.0 = Release|Any CPU
+ {EA296E5F-B205-461B-8CB9-659F31869DD6}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
+ {EA296E5F-B205-461B-8CB9-659F31869DD6}.Release|Mixed Platforms.Build.0 = Release|Any CPU
+ {EA296E5F-B205-461B-8CB9-659F31869DD6}.Release|x86.ActiveCfg = Release|Any CPU
+ {EA296E5F-B205-461B-8CB9-659F31869DD6}.Release|x86.Build.0 = Release|Any CPU
+ {EA296E5F-B205-461B-8CB9-659F31869DD6}.SingleImage|Any CPU.ActiveCfg = Release|Any CPU
+ {EA296E5F-B205-461B-8CB9-659F31869DD6}.SingleImage|Any CPU.Build.0 = Release|Any CPU
+ {EA296E5F-B205-461B-8CB9-659F31869DD6}.SingleImage|Mixed Platforms.ActiveCfg = Release|Any CPU
+ {EA296E5F-B205-461B-8CB9-659F31869DD6}.SingleImage|Mixed Platforms.Build.0 = Release|Any CPU
+ {EA296E5F-B205-461B-8CB9-659F31869DD6}.SingleImage|x86.ActiveCfg = Release|Any CPU
+ {EA296E5F-B205-461B-8CB9-659F31869DD6}.SingleImage|x86.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(NestedProjects) = preSolution
+ {EA296E5F-B205-461B-8CB9-659F31869DD6} = {63EFDF46-1284-4180-8E56-6580CC47E280}
+ EndGlobalSection
+EndGlobal
diff --git a/GeoMagGUI/AboutBoxGeoMag.Designer.cs b/GeoMagGUI/AboutBoxGeoMag.Designer.cs
index 4609918..8175e35 100644
--- a/GeoMagGUI/AboutBoxGeoMag.Designer.cs
+++ b/GeoMagGUI/AboutBoxGeoMag.Designer.cs
@@ -1,156 +1,156 @@
-namespace GeoMagGUI
-{
- partial class AboutBoxGeoMag
- {
- ///
- /// Required designer variable.
- ///
- private System.ComponentModel.IContainer components = null;
-
- ///
- /// Clean up any resources being used.
- ///
- protected override void Dispose(bool disposing)
- {
- if (disposing && (components != null))
- {
- components.Dispose();
- }
- base.Dispose(disposing);
- }
-
- #region Windows Form Designer generated code
-
- ///
- /// Required method for Designer support - do not modify
- /// the contents of this method with the code editor.
- ///
- private void InitializeComponent()
- {
- System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(AboutBoxGeoMag));
- this.tableLayoutPanel = new System.Windows.Forms.TableLayoutPanel();
- this.logoPictureBox = new System.Windows.Forms.PictureBox();
- this.labelProductName = new System.Windows.Forms.Label();
- this.labelVersion = new System.Windows.Forms.Label();
- this.textBoxDescription = new System.Windows.Forms.TextBox();
- this.okButton = new System.Windows.Forms.Button();
- this.tableLayoutPanel.SuspendLayout();
- ((System.ComponentModel.ISupportInitialize)(this.logoPictureBox)).BeginInit();
- this.SuspendLayout();
- //
- // tableLayoutPanel
- //
- this.tableLayoutPanel.ColumnCount = 2;
- this.tableLayoutPanel.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 20.83333F));
- this.tableLayoutPanel.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 79.16666F));
- this.tableLayoutPanel.Controls.Add(this.logoPictureBox, 0, 0);
- this.tableLayoutPanel.Controls.Add(this.labelProductName, 1, 0);
- this.tableLayoutPanel.Controls.Add(this.labelVersion, 1, 1);
- this.tableLayoutPanel.Controls.Add(this.textBoxDescription, 1, 2);
- this.tableLayoutPanel.Controls.Add(this.okButton, 1, 5);
- this.tableLayoutPanel.Dock = System.Windows.Forms.DockStyle.Fill;
- this.tableLayoutPanel.Location = new System.Drawing.Point(9, 9);
- this.tableLayoutPanel.Name = "tableLayoutPanel";
- this.tableLayoutPanel.RowCount = 6;
- this.tableLayoutPanel.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 10F));
- this.tableLayoutPanel.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 10F));
- this.tableLayoutPanel.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 10F));
- this.tableLayoutPanel.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 10F));
- this.tableLayoutPanel.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 50F));
- this.tableLayoutPanel.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 10F));
- this.tableLayoutPanel.Size = new System.Drawing.Size(624, 287);
- this.tableLayoutPanel.TabIndex = 0;
- //
- // logoPictureBox
- //
- this.logoPictureBox.Dock = System.Windows.Forms.DockStyle.Fill;
- this.logoPictureBox.Image = ((System.Drawing.Image)(resources.GetObject("logoPictureBox.Image")));
- this.logoPictureBox.Location = new System.Drawing.Point(3, 3);
- this.logoPictureBox.Name = "logoPictureBox";
- this.tableLayoutPanel.SetRowSpan(this.logoPictureBox, 5);
- this.logoPictureBox.Size = new System.Drawing.Size(124, 249);
- this.logoPictureBox.TabIndex = 12;
- this.logoPictureBox.TabStop = false;
- //
- // labelProductName
- //
- this.labelProductName.Dock = System.Windows.Forms.DockStyle.Fill;
- this.labelProductName.Location = new System.Drawing.Point(136, 0);
- this.labelProductName.Margin = new System.Windows.Forms.Padding(6, 0, 3, 0);
- this.labelProductName.MaximumSize = new System.Drawing.Size(0, 17);
- this.labelProductName.Name = "labelProductName";
- this.labelProductName.Size = new System.Drawing.Size(485, 17);
- this.labelProductName.TabIndex = 19;
- this.labelProductName.Text = "Product Name";
- this.labelProductName.TextAlign = System.Drawing.ContentAlignment.MiddleLeft;
- //
- // labelVersion
- //
- this.labelVersion.Dock = System.Windows.Forms.DockStyle.Fill;
- this.labelVersion.Location = new System.Drawing.Point(136, 28);
- this.labelVersion.Margin = new System.Windows.Forms.Padding(6, 0, 3, 0);
- this.labelVersion.MaximumSize = new System.Drawing.Size(0, 17);
- this.labelVersion.Name = "labelVersion";
- this.labelVersion.Size = new System.Drawing.Size(485, 17);
- this.labelVersion.TabIndex = 0;
- this.labelVersion.Text = "Version";
- this.labelVersion.TextAlign = System.Drawing.ContentAlignment.MiddleLeft;
- //
- // textBoxDescription
- //
- this.textBoxDescription.Dock = System.Windows.Forms.DockStyle.Fill;
- this.textBoxDescription.Location = new System.Drawing.Point(136, 59);
- this.textBoxDescription.Margin = new System.Windows.Forms.Padding(6, 3, 3, 3);
- this.textBoxDescription.Multiline = true;
- this.textBoxDescription.Name = "textBoxDescription";
- this.textBoxDescription.ReadOnly = true;
- this.tableLayoutPanel.SetRowSpan(this.textBoxDescription, 3);
- this.textBoxDescription.ScrollBars = System.Windows.Forms.ScrollBars.Both;
- this.textBoxDescription.Size = new System.Drawing.Size(485, 193);
- this.textBoxDescription.TabIndex = 23;
- this.textBoxDescription.TabStop = false;
- this.textBoxDescription.Text = resources.GetString("textBoxDescription.Text");
- //
- // okButton
- //
- this.okButton.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right)));
- this.okButton.DialogResult = System.Windows.Forms.DialogResult.Cancel;
- this.okButton.Location = new System.Drawing.Point(546, 261);
- this.okButton.Name = "okButton";
- this.okButton.Size = new System.Drawing.Size(75, 23);
- this.okButton.TabIndex = 24;
- this.okButton.Text = "&OK";
- //
- // AboutBoxGeoMag
- //
- this.AcceptButton = this.okButton;
- this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
- this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
- this.ClientSize = new System.Drawing.Size(642, 305);
- this.Controls.Add(this.tableLayoutPanel);
- this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedDialog;
- this.MaximizeBox = false;
- this.MinimizeBox = false;
- this.Name = "AboutBoxGeoMag";
- this.Padding = new System.Windows.Forms.Padding(9, 9, 9, 9);
- this.ShowIcon = false;
- this.ShowInTaskbar = false;
- this.StartPosition = System.Windows.Forms.FormStartPosition.CenterParent;
- this.Text = "About...";
- this.tableLayoutPanel.ResumeLayout(false);
- this.tableLayoutPanel.PerformLayout();
- ((System.ComponentModel.ISupportInitialize)(this.logoPictureBox)).EndInit();
- this.ResumeLayout(false);
-
- }
-
- #endregion
-
- private System.Windows.Forms.TableLayoutPanel tableLayoutPanel;
- private System.Windows.Forms.PictureBox logoPictureBox;
- private System.Windows.Forms.Label labelProductName;
- private System.Windows.Forms.Label labelVersion;
- private System.Windows.Forms.TextBox textBoxDescription;
- private System.Windows.Forms.Button okButton;
- }
-}
+namespace GeoMagGUI
+{
+ partial class AboutBoxGeoMag
+ {
+ ///
+ /// Required designer variable.
+ ///
+ private System.ComponentModel.IContainer components = null;
+
+ ///
+ /// Clean up any resources being used.
+ ///
+ protected override void Dispose(bool disposing)
+ {
+ if (disposing && (components != null))
+ {
+ components.Dispose();
+ }
+ base.Dispose(disposing);
+ }
+
+ #region Windows Form Designer generated code
+
+ ///
+ /// Required method for Designer support - do not modify
+ /// the contents of this method with the code editor.
+ ///
+ private void InitializeComponent()
+ {
+ System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(AboutBoxGeoMag));
+ this.tableLayoutPanel = new System.Windows.Forms.TableLayoutPanel();
+ this.logoPictureBox = new System.Windows.Forms.PictureBox();
+ this.labelProductName = new System.Windows.Forms.Label();
+ this.labelVersion = new System.Windows.Forms.Label();
+ this.textBoxDescription = new System.Windows.Forms.TextBox();
+ this.okButton = new System.Windows.Forms.Button();
+ this.tableLayoutPanel.SuspendLayout();
+ ((System.ComponentModel.ISupportInitialize)(this.logoPictureBox)).BeginInit();
+ this.SuspendLayout();
+ //
+ // tableLayoutPanel
+ //
+ this.tableLayoutPanel.ColumnCount = 2;
+ this.tableLayoutPanel.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 20.83333F));
+ this.tableLayoutPanel.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 79.16666F));
+ this.tableLayoutPanel.Controls.Add(this.logoPictureBox, 0, 0);
+ this.tableLayoutPanel.Controls.Add(this.labelProductName, 1, 0);
+ this.tableLayoutPanel.Controls.Add(this.labelVersion, 1, 1);
+ this.tableLayoutPanel.Controls.Add(this.textBoxDescription, 1, 2);
+ this.tableLayoutPanel.Controls.Add(this.okButton, 1, 5);
+ this.tableLayoutPanel.Dock = System.Windows.Forms.DockStyle.Fill;
+ this.tableLayoutPanel.Location = new System.Drawing.Point(9, 9);
+ this.tableLayoutPanel.Name = "tableLayoutPanel";
+ this.tableLayoutPanel.RowCount = 6;
+ this.tableLayoutPanel.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 10F));
+ this.tableLayoutPanel.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 10F));
+ this.tableLayoutPanel.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 10F));
+ this.tableLayoutPanel.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 10F));
+ this.tableLayoutPanel.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 50F));
+ this.tableLayoutPanel.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 10F));
+ this.tableLayoutPanel.Size = new System.Drawing.Size(624, 287);
+ this.tableLayoutPanel.TabIndex = 0;
+ //
+ // logoPictureBox
+ //
+ this.logoPictureBox.Dock = System.Windows.Forms.DockStyle.Fill;
+ this.logoPictureBox.Image = ((System.Drawing.Image)(resources.GetObject("logoPictureBox.Image")));
+ this.logoPictureBox.Location = new System.Drawing.Point(3, 3);
+ this.logoPictureBox.Name = "logoPictureBox";
+ this.tableLayoutPanel.SetRowSpan(this.logoPictureBox, 5);
+ this.logoPictureBox.Size = new System.Drawing.Size(124, 249);
+ this.logoPictureBox.TabIndex = 12;
+ this.logoPictureBox.TabStop = false;
+ //
+ // labelProductName
+ //
+ this.labelProductName.Dock = System.Windows.Forms.DockStyle.Fill;
+ this.labelProductName.Location = new System.Drawing.Point(136, 0);
+ this.labelProductName.Margin = new System.Windows.Forms.Padding(6, 0, 3, 0);
+ this.labelProductName.MaximumSize = new System.Drawing.Size(0, 17);
+ this.labelProductName.Name = "labelProductName";
+ this.labelProductName.Size = new System.Drawing.Size(485, 17);
+ this.labelProductName.TabIndex = 19;
+ this.labelProductName.Text = "Product Name";
+ this.labelProductName.TextAlign = System.Drawing.ContentAlignment.MiddleLeft;
+ //
+ // labelVersion
+ //
+ this.labelVersion.Dock = System.Windows.Forms.DockStyle.Fill;
+ this.labelVersion.Location = new System.Drawing.Point(136, 28);
+ this.labelVersion.Margin = new System.Windows.Forms.Padding(6, 0, 3, 0);
+ this.labelVersion.MaximumSize = new System.Drawing.Size(0, 17);
+ this.labelVersion.Name = "labelVersion";
+ this.labelVersion.Size = new System.Drawing.Size(485, 17);
+ this.labelVersion.TabIndex = 0;
+ this.labelVersion.Text = "Version";
+ this.labelVersion.TextAlign = System.Drawing.ContentAlignment.MiddleLeft;
+ //
+ // textBoxDescription
+ //
+ this.textBoxDescription.Dock = System.Windows.Forms.DockStyle.Fill;
+ this.textBoxDescription.Location = new System.Drawing.Point(136, 59);
+ this.textBoxDescription.Margin = new System.Windows.Forms.Padding(6, 3, 3, 3);
+ this.textBoxDescription.Multiline = true;
+ this.textBoxDescription.Name = "textBoxDescription";
+ this.textBoxDescription.ReadOnly = true;
+ this.tableLayoutPanel.SetRowSpan(this.textBoxDescription, 3);
+ this.textBoxDescription.ScrollBars = System.Windows.Forms.ScrollBars.Both;
+ this.textBoxDescription.Size = new System.Drawing.Size(485, 193);
+ this.textBoxDescription.TabIndex = 23;
+ this.textBoxDescription.TabStop = false;
+ this.textBoxDescription.Text = resources.GetString("textBoxDescription.Text");
+ //
+ // okButton
+ //
+ this.okButton.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right)));
+ this.okButton.DialogResult = System.Windows.Forms.DialogResult.Cancel;
+ this.okButton.Location = new System.Drawing.Point(546, 261);
+ this.okButton.Name = "okButton";
+ this.okButton.Size = new System.Drawing.Size(75, 23);
+ this.okButton.TabIndex = 24;
+ this.okButton.Text = "&OK";
+ //
+ // AboutBoxGeoMag
+ //
+ this.AcceptButton = this.okButton;
+ this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
+ this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
+ this.ClientSize = new System.Drawing.Size(642, 305);
+ this.Controls.Add(this.tableLayoutPanel);
+ this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedDialog;
+ this.MaximizeBox = false;
+ this.MinimizeBox = false;
+ this.Name = "AboutBoxGeoMag";
+ this.Padding = new System.Windows.Forms.Padding(9, 9, 9, 9);
+ this.ShowIcon = false;
+ this.ShowInTaskbar = false;
+ this.StartPosition = System.Windows.Forms.FormStartPosition.CenterParent;
+ this.Text = "About...";
+ this.tableLayoutPanel.ResumeLayout(false);
+ this.tableLayoutPanel.PerformLayout();
+ ((System.ComponentModel.ISupportInitialize)(this.logoPictureBox)).EndInit();
+ this.ResumeLayout(false);
+
+ }
+
+ #endregion
+
+ private System.Windows.Forms.TableLayoutPanel tableLayoutPanel;
+ private System.Windows.Forms.PictureBox logoPictureBox;
+ private System.Windows.Forms.Label labelProductName;
+ private System.Windows.Forms.Label labelVersion;
+ private System.Windows.Forms.TextBox textBoxDescription;
+ private System.Windows.Forms.Button okButton;
+ }
+}
diff --git a/GeoMagGUI/AboutBoxGeoMag.cs b/GeoMagGUI/AboutBoxGeoMag.cs
index 054967f..cc8f428 100644
--- a/GeoMagGUI/AboutBoxGeoMag.cs
+++ b/GeoMagGUI/AboutBoxGeoMag.cs
@@ -1,104 +1,104 @@
-using System;
-using System.Collections.Generic;
-using System.ComponentModel;
-using System.Drawing;
-using System.Linq;
-using System.Reflection;
-using System.Windows.Forms;
-
-namespace GeoMagGUI
-{
- partial class AboutBoxGeoMag : Form
- {
- public AboutBoxGeoMag()
- {
- InitializeComponent();
- this.Text = String.Format("About {0}", AssemblyTitle);
- this.labelProductName.Text = AssemblyProduct;
- this.labelVersion.Text = String.Format("Version {0}", AssemblyVersion);
- //this.labelCopyright.Text = AssemblyCopyright;
- //this.labelCompanyName.Text = AssemblyCompany;
- //this.textBoxDescription.Text = AssemblyDescription;
- }
-
- #region Assembly Attribute Accessors
-
- public string AssemblyTitle
- {
- get
- {
- object[] attributes = Assembly.GetExecutingAssembly().GetCustomAttributes(typeof(AssemblyTitleAttribute), false);
- if (attributes.Length > 0)
- {
- AssemblyTitleAttribute titleAttribute = (AssemblyTitleAttribute)attributes[0];
- if (titleAttribute.Title != "")
- {
- return titleAttribute.Title;
- }
- }
- return System.IO.Path.GetFileNameWithoutExtension(Assembly.GetExecutingAssembly().CodeBase);
- }
- }
-
- public string AssemblyVersion
- {
- get
- {
- return Assembly.GetExecutingAssembly().GetName().Version.ToString();
- }
- }
-
- public string AssemblyDescription
- {
- get
- {
- object[] attributes = Assembly.GetExecutingAssembly().GetCustomAttributes(typeof(AssemblyDescriptionAttribute), false);
- if (attributes.Length == 0)
- {
- return "";
- }
- return ((AssemblyDescriptionAttribute)attributes[0]).Description;
- }
- }
-
- public string AssemblyProduct
- {
- get
- {
- object[] attributes = Assembly.GetExecutingAssembly().GetCustomAttributes(typeof(AssemblyProductAttribute), false);
- if (attributes.Length == 0)
- {
- return "";
- }
- return ((AssemblyProductAttribute)attributes[0]).Product;
- }
- }
-
- public string AssemblyCopyright
- {
- get
- {
- object[] attributes = Assembly.GetExecutingAssembly().GetCustomAttributes(typeof(AssemblyCopyrightAttribute), false);
- if (attributes.Length == 0)
- {
- return "";
- }
- return ((AssemblyCopyrightAttribute)attributes[0]).Copyright;
- }
- }
-
- public string AssemblyCompany
- {
- get
- {
- object[] attributes = Assembly.GetExecutingAssembly().GetCustomAttributes(typeof(AssemblyCompanyAttribute), false);
- if (attributes.Length == 0)
- {
- return "";
- }
- return ((AssemblyCompanyAttribute)attributes[0]).Company;
- }
- }
- #endregion
- }
-}
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Drawing;
+using System.Linq;
+using System.Reflection;
+using System.Windows.Forms;
+
+namespace GeoMagGUI
+{
+ partial class AboutBoxGeoMag : Form
+ {
+ public AboutBoxGeoMag()
+ {
+ InitializeComponent();
+ this.Text = String.Format("About {0}", AssemblyTitle);
+ this.labelProductName.Text = AssemblyProduct;
+ this.labelVersion.Text = String.Format("Version {0}", AssemblyVersion);
+ //this.labelCopyright.Text = AssemblyCopyright;
+ //this.labelCompanyName.Text = AssemblyCompany;
+ //this.textBoxDescription.Text = AssemblyDescription;
+ }
+
+ #region Assembly Attribute Accessors
+
+ public string AssemblyTitle
+ {
+ get
+ {
+ object[] attributes = Assembly.GetExecutingAssembly().GetCustomAttributes(typeof(AssemblyTitleAttribute), false);
+ if (attributes.Length > 0)
+ {
+ AssemblyTitleAttribute titleAttribute = (AssemblyTitleAttribute)attributes[0];
+ if (titleAttribute.Title != "")
+ {
+ return titleAttribute.Title;
+ }
+ }
+ return System.IO.Path.GetFileNameWithoutExtension(Assembly.GetExecutingAssembly().CodeBase);
+ }
+ }
+
+ public string AssemblyVersion
+ {
+ get
+ {
+ return Assembly.GetExecutingAssembly().GetName().Version.ToString();
+ }
+ }
+
+ public string AssemblyDescription
+ {
+ get
+ {
+ object[] attributes = Assembly.GetExecutingAssembly().GetCustomAttributes(typeof(AssemblyDescriptionAttribute), false);
+ if (attributes.Length == 0)
+ {
+ return "";
+ }
+ return ((AssemblyDescriptionAttribute)attributes[0]).Description;
+ }
+ }
+
+ public string AssemblyProduct
+ {
+ get
+ {
+ object[] attributes = Assembly.GetExecutingAssembly().GetCustomAttributes(typeof(AssemblyProductAttribute), false);
+ if (attributes.Length == 0)
+ {
+ return "";
+ }
+ return ((AssemblyProductAttribute)attributes[0]).Product;
+ }
+ }
+
+ public string AssemblyCopyright
+ {
+ get
+ {
+ object[] attributes = Assembly.GetExecutingAssembly().GetCustomAttributes(typeof(AssemblyCopyrightAttribute), false);
+ if (attributes.Length == 0)
+ {
+ return "";
+ }
+ return ((AssemblyCopyrightAttribute)attributes[0]).Copyright;
+ }
+ }
+
+ public string AssemblyCompany
+ {
+ get
+ {
+ object[] attributes = Assembly.GetExecutingAssembly().GetCustomAttributes(typeof(AssemblyCompanyAttribute), false);
+ if (attributes.Length == 0)
+ {
+ return "";
+ }
+ return ((AssemblyCompanyAttribute)attributes[0]).Company;
+ }
+ }
+ #endregion
+ }
+}
diff --git a/GeoMagGUI/Helper.cs b/GeoMagGUI/Helper.cs
index f74471c..08005cb 100644
--- a/GeoMagGUI/Helper.cs
+++ b/GeoMagGUI/Helper.cs
@@ -1,51 +1,51 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-using System.Windows.Forms;
-
-namespace GeoMagGUI
-{
- public static class Helper
- {
- public static bool IsNumeric(Object expression)
- {
- if (expression == null || expression is DateTime)
- return false;
-
- if (expression is Int16 || expression is Int32 || expression is Decimal || expression is Single || expression is Double || expression is Boolean)
- return true;
-
- try
- {
- if (expression is string)
- Double.Parse(expression as string);
- else
- Double.Parse(expression.ToString());
- return true;
- }
- catch (Exception)
- { } // just dismiss errors but return false
- return false;
- }
-
- public static Int32 GetColumnID(String columnName, DataGridView inDataGrid)
- {
-
- for (Int32 i = 0; i < inDataGrid.ColumnCount; i++)
- {
- if (inDataGrid.Columns[i].Name.Equals(columnName, StringComparison.OrdinalIgnoreCase))
- {
-
- return i;
-
- }
-
- }
-
- return -1;
-
- }
-
- }
-}
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Windows.Forms;
+
+namespace GeoMagGUI
+{
+ public static class Helper
+ {
+ public static bool IsNumeric(Object expression)
+ {
+ if (expression == null || expression is DateTime)
+ return false;
+
+ if (expression is Int16 || expression is Int32 || expression is Decimal || expression is Single || expression is Double || expression is Boolean)
+ return true;
+
+ try
+ {
+ if (expression is string)
+ Double.Parse(expression as string);
+ else
+ Double.Parse(expression.ToString());
+ return true;
+ }
+ catch (Exception)
+ { } // just dismiss errors but return false
+ return false;
+ }
+
+ public static Int32 GetColumnID(String columnName, DataGridView inDataGrid)
+ {
+
+ for (Int32 i = 0; i < inDataGrid.ColumnCount; i++)
+ {
+ if (inDataGrid.Columns[i].Name.Equals(columnName, StringComparison.OrdinalIgnoreCase))
+ {
+
+ return i;
+
+ }
+
+ }
+
+ return -1;
+
+ }
+
+ }
+}
diff --git a/GeoMagGUI/Program.cs b/GeoMagGUI/Program.cs
index 297349b..1a4af60 100644
--- a/GeoMagGUI/Program.cs
+++ b/GeoMagGUI/Program.cs
@@ -1,21 +1,21 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Windows.Forms;
-
-namespace GeoMagGUI
-{
- static class Program
- {
- ///
- /// The main entry point for the application.
- ///
- [STAThread]
- static void Main()
- {
- Application.EnableVisualStyles();
- Application.SetCompatibleTextRenderingDefault(false);
- Application.Run(new FrmMain());
- }
- }
-}
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Windows.Forms;
+
+namespace GeoMagGUI
+{
+ static class Program
+ {
+ ///
+ /// The main entry point for the application.
+ ///
+ [STAThread]
+ static void Main()
+ {
+ Application.EnableVisualStyles();
+ Application.SetCompatibleTextRenderingDefault(false);
+ Application.Run(new FrmMain());
+ }
+ }
+}
diff --git a/GeoMagGUI/frmAddModel.cs b/GeoMagGUI/frmAddModel.cs
index f84bcd7..c1d703a 100644
--- a/GeoMagGUI/frmAddModel.cs
+++ b/GeoMagGUI/frmAddModel.cs
@@ -5,6 +5,8 @@
using System.Drawing;
using System.Linq;
using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
using System.Windows.Forms;
using GeoMagSharp;
@@ -24,20 +26,44 @@ public MagneticModelSet Model
}
}
+ ///
+ /// Gets the file path selected by the user in the open file dialog.
+ /// Empty string if user cancelled.
+ ///
+ public string SelectedFilePath { get; private set; }
+
public frmAddModel()
{
InitializeComponent();
- var modelFile = AddFile();
-
- LoadModelData(modelFile);
-
+ SelectedFilePath = AddFile();
}
private void LoadModelData(string modelFile)
{
_Model = ModelReader.Read(modelFile);
+ DisplayModelData();
+ }
+
+ ///
+ /// Asynchronously loads model data from a coefficient file.
+ ///
+ /// Path to the coefficient file.
+ /// Optional progress reporter.
+ /// Optional cancellation token.
+ public async Task LoadModelDataAsync(string modelFile,
+ IProgress progress = null,
+ CancellationToken cancellationToken = default)
+ {
+ _Model = await ModelReader.ReadAsync(modelFile, progress, cancellationToken)
+ .ConfigureAwait(true);
+
+ DisplayModelData();
+ }
+
+ private void DisplayModelData()
+ {
if(_Model != null)
{
_Model.Name = Path.GetFileNameWithoutExtension(Model.FileNames.First());
@@ -54,9 +80,9 @@ private void LoadModelData(string modelFile)
}
else
{
- MessageBox.Show(this, "", "", MessageBoxButtons.OK, MessageBoxIcon.Error);
+ MessageBox.Show(this, "Failed to load model data from the selected file.",
+ "Model Load Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
-
}
private string AddFile()
@@ -89,6 +115,7 @@ private void buttonAddFile_Click(object sender, EventArgs e)
private void buttonOK_Click(object sender, EventArgs e)
{
+ DialogResult = DialogResult.OK;
Hide();
}
diff --git a/GeoMagGUI/frmMain.Designer.cs b/GeoMagGUI/frmMain.Designer.cs
index 5db6072..b6ff3ef 100644
--- a/GeoMagGUI/frmMain.Designer.cs
+++ b/GeoMagGUI/frmMain.Designer.cs
@@ -13,9 +13,23 @@ partial class FrmMain
/// true if managed resources should be disposed; otherwise, false.
protected override void Dispose(bool disposing)
{
- if (disposing && (components != null))
+ if (disposing)
{
- components.Dispose();
+ _calculationCts?.Cancel();
+ _calculationCts?.Dispose();
+ _calculationCts = null;
+
+ Watcher?.Dispose();
+ Watcher = null;
+
+ _statusClearTimer?.Stop();
+ _statusClearTimer?.Dispose();
+ _statusClearTimer = null;
+
+ if (components != null)
+ {
+ components.Dispose();
+ }
}
base.Dispose(disposing);
}
@@ -80,11 +94,16 @@ private void InitializeComponent()
this.comboBoxAltitudeUnits = new System.Windows.Forms.ComboBox();
this.ComboBoxLongDir = new System.Windows.Forms.ComboBox();
this.ComboBoxLatDir = new System.Windows.Forms.ComboBox();
+ this.statusStrip1 = new System.Windows.Forms.StatusStrip();
+ this.toolStripStatusLabel1 = new System.Windows.Forms.ToolStripStatusLabel();
+ this.toolStripProgressBar1 = new System.Windows.Forms.ToolStripProgressBar();
+ this.toolStripButtonCancel = new System.Windows.Forms.ToolStripButton();
((System.ComponentModel.ISupportInitialize)(this.numericUpDownStepSize)).BeginInit();
this.menuStrip1.SuspendLayout();
((System.ComponentModel.ISupportInitialize)(this.errorProviderCheck)).BeginInit();
((System.ComponentModel.ISupportInitialize)(this.dataGridViewResults)).BeginInit();
this.tableLayoutPanel1.SuspendLayout();
+ this.statusStrip1.SuspendLayout();
this.SuspendLayout();
//
// dateTimePicker1
@@ -359,40 +378,40 @@ private void InitializeComponent()
this.fileToolStripMenuItem.Text = "File";
//
// addModelToolStripMenuItem
- //
+ //
this.addModelToolStripMenuItem.Name = "addModelToolStripMenuItem";
this.addModelToolStripMenuItem.ShortcutKeys = ((System.Windows.Forms.Keys)((System.Windows.Forms.Keys.Control | System.Windows.Forms.Keys.M)));
- this.addModelToolStripMenuItem.Size = new System.Drawing.Size(137, 22);
+ this.addModelToolStripMenuItem.Size = new System.Drawing.Size(180, 22);
this.addModelToolStripMenuItem.Text = "Add Model";
this.addModelToolStripMenuItem.Click += new System.EventHandler(this.addModelToolStripMenuItem_Click);
//
// loadModelToolStripMenuItem
- //
+ //
this.loadModelToolStripMenuItem.Name = "loadModelToolStripMenuItem";
this.loadModelToolStripMenuItem.ShortcutKeys = ((System.Windows.Forms.Keys)((System.Windows.Forms.Keys.Control | System.Windows.Forms.Keys.O)));
- this.loadModelToolStripMenuItem.Size = new System.Drawing.Size(137, 22);
+ this.loadModelToolStripMenuItem.Size = new System.Drawing.Size(180, 22);
this.loadModelToolStripMenuItem.Text = "Load Model";
this.loadModelToolStripMenuItem.Click += new System.EventHandler(this.loadModelToolStripMenuItem_Click);
//
// toolStripSeparator1
//
this.toolStripSeparator1.Name = "toolStripSeparator1";
- this.toolStripSeparator1.Size = new System.Drawing.Size(134, 6);
+ this.toolStripSeparator1.Size = new System.Drawing.Size(177, 6);
//
// saveToolStripMenuItem
- //
+ //
this.saveToolStripMenuItem.Enabled = false;
this.saveToolStripMenuItem.Name = "saveToolStripMenuItem";
this.saveToolStripMenuItem.ShortcutKeys = ((System.Windows.Forms.Keys)((System.Windows.Forms.Keys.Control | System.Windows.Forms.Keys.S)));
- this.saveToolStripMenuItem.Size = new System.Drawing.Size(137, 22);
+ this.saveToolStripMenuItem.Size = new System.Drawing.Size(180, 22);
this.saveToolStripMenuItem.Text = "Save";
this.saveToolStripMenuItem.Click += new System.EventHandler(this.saveToolStripMenuItem_Click);
//
// exitToolStripMenuItem
- //
+ //
this.exitToolStripMenuItem.Name = "exitToolStripMenuItem";
this.exitToolStripMenuItem.ShortcutKeys = ((System.Windows.Forms.Keys)((System.Windows.Forms.Keys.Alt | System.Windows.Forms.Keys.F4)));
- this.exitToolStripMenuItem.Size = new System.Drawing.Size(137, 22);
+ this.exitToolStripMenuItem.Size = new System.Drawing.Size(180, 22);
this.exitToolStripMenuItem.Text = "Exit";
this.exitToolStripMenuItem.Click += new System.EventHandler(this.exitToolStripMenuItem_Click);
//
@@ -407,7 +426,7 @@ private void InitializeComponent()
this.settingsToolStripMenuItem.Text = "Settings";
//
// preferencesToolStripMenuItem
- //
+ //
this.preferencesToolStripMenuItem.Name = "preferencesToolStripMenuItem";
this.preferencesToolStripMenuItem.ShortcutKeys = ((System.Windows.Forms.Keys)((System.Windows.Forms.Keys.Control | System.Windows.Forms.Keys.P)));
this.preferencesToolStripMenuItem.Size = new System.Drawing.Size(206, 22);
@@ -436,10 +455,10 @@ private void InitializeComponent()
this.helpToolStripMenuItem.Text = "Help";
//
// aboutGeoMagToolStripMenuItem
- //
+ //
this.aboutGeoMagToolStripMenuItem.Name = "aboutGeoMagToolStripMenuItem";
this.aboutGeoMagToolStripMenuItem.ShortcutKeys = System.Windows.Forms.Keys.F1;
- this.aboutGeoMagToolStripMenuItem.Size = new System.Drawing.Size(116, 22);
+ this.aboutGeoMagToolStripMenuItem.Size = new System.Drawing.Size(135, 22);
this.aboutGeoMagToolStripMenuItem.Text = "About...";
this.aboutGeoMagToolStripMenuItem.Click += new System.EventHandler(this.aboutGeoMagToolStripMenuItem_Click);
//
@@ -470,7 +489,7 @@ private void InitializeComponent()
this.dataGridViewResults.Name = "dataGridViewResults";
this.dataGridViewResults.ReadOnly = true;
this.tableLayoutPanel1.SetRowSpan(this.dataGridViewResults, 2);
- this.dataGridViewResults.Size = new System.Drawing.Size(808, 117);
+ this.dataGridViewResults.Size = new System.Drawing.Size(808, 95);
this.dataGridViewResults.TabIndex = 19;
//
// ColumnDate
@@ -599,7 +618,7 @@ private void InitializeComponent()
this.tableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Absolute, 16F));
this.tableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Absolute, 16F));
this.tableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Absolute, 16F));
- this.tableLayoutPanel1.Size = new System.Drawing.Size(844, 267);
+ this.tableLayoutPanel1.Size = new System.Drawing.Size(844, 245);
this.tableLayoutPanel1.TabIndex = 23;
//
// label3
@@ -683,12 +702,52 @@ private void InitializeComponent()
this.ComboBoxLatDir.Validating += new System.ComponentModel.CancelEventHandler(this.TextBoxLatitude_Validating);
this.ComboBoxLatDir.Validated += new System.EventHandler(this.TextBoxLatitude_Validated);
//
+ // statusStrip1
+ //
+ this.statusStrip1.Items.AddRange(new System.Windows.Forms.ToolStripItem[] {
+ this.toolStripStatusLabel1,
+ this.toolStripProgressBar1,
+ this.toolStripButtonCancel});
+ this.statusStrip1.Location = new System.Drawing.Point(0, 269);
+ this.statusStrip1.Name = "statusStrip1";
+ this.statusStrip1.Size = new System.Drawing.Size(844, 22);
+ this.statusStrip1.TabIndex = 24;
+ this.statusStrip1.Text = "statusStrip1";
+ //
+ // toolStripStatusLabel1
+ //
+ this.toolStripStatusLabel1.AccessibleName = "Status";
+ this.toolStripStatusLabel1.Name = "toolStripStatusLabel1";
+ this.toolStripStatusLabel1.Size = new System.Drawing.Size(829, 17);
+ this.toolStripStatusLabel1.Spring = true;
+ this.toolStripStatusLabel1.Text = "Ready";
+ this.toolStripStatusLabel1.TextAlign = System.Drawing.ContentAlignment.MiddleLeft;
+ //
+ // toolStripProgressBar1
+ //
+ this.toolStripProgressBar1.AccessibleName = "Calculation progress";
+ this.toolStripProgressBar1.Name = "toolStripProgressBar1";
+ this.toolStripProgressBar1.Size = new System.Drawing.Size(100, 16);
+ this.toolStripProgressBar1.Visible = false;
+ //
+ // toolStripButtonCancel
+ //
+ this.toolStripButtonCancel.AccessibleName = "Cancel calculation";
+ this.toolStripButtonCancel.DisplayStyle = System.Windows.Forms.ToolStripItemDisplayStyle.Text;
+ this.toolStripButtonCancel.Name = "toolStripButtonCancel";
+ this.toolStripButtonCancel.Size = new System.Drawing.Size(47, 20);
+ this.toolStripButtonCancel.Text = "Cancel";
+ this.toolStripButtonCancel.ToolTipText = "Cancel the current operation (Esc)";
+ this.toolStripButtonCancel.Visible = false;
+ this.toolStripButtonCancel.Click += new System.EventHandler(this.toolStripButtonCancel_Click);
+ //
// FrmMain
//
this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.ClientSize = new System.Drawing.Size(844, 291);
this.Controls.Add(this.tableLayoutPanel1);
+ this.Controls.Add(this.statusStrip1);
this.Controls.Add(this.menuStrip1);
this.Icon = ((System.Drawing.Icon)(resources.GetObject("$this.Icon")));
this.KeyPreview = true;
@@ -704,6 +763,8 @@ private void InitializeComponent()
((System.ComponentModel.ISupportInitialize)(this.dataGridViewResults)).EndInit();
this.tableLayoutPanel1.ResumeLayout(false);
this.tableLayoutPanel1.PerformLayout();
+ this.statusStrip1.ResumeLayout(false);
+ this.statusStrip1.PerformLayout();
this.ResumeLayout(false);
this.PerformLayout();
@@ -761,6 +822,10 @@ private void InitializeComponent()
private System.Windows.Forms.DataGridViewTextBoxColumn ColumnEastComp;
private System.Windows.Forms.DataGridViewTextBoxColumn ColumnVerticalComp;
private System.Windows.Forms.DataGridViewTextBoxColumn ColumnTotalField;
+ private System.Windows.Forms.StatusStrip statusStrip1;
+ private System.Windows.Forms.ToolStripStatusLabel toolStripStatusLabel1;
+ private System.Windows.Forms.ToolStripProgressBar toolStripProgressBar1;
+ private System.Windows.Forms.ToolStripButton toolStripButtonCancel;
}
}
diff --git a/GeoMagGUI/frmMain.cs b/GeoMagGUI/frmMain.cs
index a876af3..5b83f8d 100644
--- a/GeoMagGUI/frmMain.cs
+++ b/GeoMagGUI/frmMain.cs
@@ -4,6 +4,8 @@
using System.Device.Location;
using System.IO;
using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
using System.Windows.Forms;
namespace GeoMagGUI
@@ -20,6 +22,10 @@ public partial class FrmMain : Form
private GeoMag _MagCalculator;
+ private CancellationTokenSource _calculationCts;
+
+ private System.Windows.Forms.Timer _statusClearTimer;
+
#region Getters & Setters
public string ModelFolder
@@ -123,10 +129,58 @@ private void FrmMain_KeyDown(object sender, KeyEventArgs e)
buttonMyLocation_Click(sender, e);
e.Handled = true;
}
+ // Escape - Cancel active operation (only when a calculation is running)
+ else if (e.KeyCode == Keys.Escape && _calculationCts != null)
+ {
+ _calculationCts.Cancel();
+ e.Handled = true;
+ }
+ }
+
+ private void SetUIBusy(bool busy)
+ {
+ buttonCalculate.Enabled = !busy;
+ addModelToolStripMenuItem.Enabled = !busy;
+ loadModelToolStripMenuItem.Enabled = !busy;
+ toolStripProgressBar1.Visible = busy;
+ toolStripButtonCancel.Visible = busy;
+ UseWaitCursor = busy;
+
+ if (!busy)
+ {
+ toolStripProgressBar1.Value = 0;
+ }
}
- private void buttonCalculate_Click(object sender, EventArgs e)
+ private void SetStatusTemporary(string message, int milliseconds = 5000)
{
+ toolStripStatusLabel1.Text = message;
+
+ if (_statusClearTimer != null)
+ {
+ _statusClearTimer.Stop();
+ _statusClearTimer.Dispose();
+ }
+
+ _statusClearTimer = new System.Windows.Forms.Timer { Interval = milliseconds };
+ _statusClearTimer.Tick += (s, args) =>
+ {
+ _statusClearTimer.Stop();
+ toolStripStatusLabel1.Text = "Ready";
+ };
+ _statusClearTimer.Start();
+ }
+
+ private void toolStripButtonCancel_Click(object sender, EventArgs e)
+ {
+ _calculationCts?.Cancel();
+ }
+
+ private async void buttonCalculate_Click(object sender, EventArgs e)
+ {
+ // Re-entrancy guard: ignore if already calculating
+ if (_calculationCts != null) return;
+
_MagCalculator = null;
saveToolStripMenuItem.Enabled = false;
@@ -146,20 +200,6 @@ private void buttonCalculate_Click(object sender, EventArgs e)
if (selectedModel != null)
{
- //if (DBNull.Value.Equals(dRow.First()["FileName"]))
- //{
- // this.errorProviderCheck.SetError(comboBoxModels, @"No file name was found for the model you selected");
- // return;
- //}
-
- //string modelFile = dRow.First()["FileName"].ToString();
-
- //if (!File.Exists(modelFile))
- //{
- // this.errorProviderCheck.SetError(comboBoxModels, string.Format("The model file {0} could not be found", Path.GetFileName(modelFile)));
- // return;
- //}
-
if (comboBoxAltitudeUnits.SelectedItem == null)
{
this.errorProviderCheck.SetError(comboBoxAltitudeUnits, @"No Units have been selected");
@@ -188,9 +228,12 @@ private void buttonCalculate_Click(object sender, EventArgs e)
return;
}
+ _calculationCts = new CancellationTokenSource();
+
try
{
- Cursor = Cursors.WaitCursor;
+ SetUIBusy(true);
+ toolStripStatusLabel1.Text = "Calculating...";
var calcOptions = new CalculationOptions
{
@@ -210,7 +253,13 @@ private void buttonCalculate_Click(object sender, EventArgs e)
if (toolStripMenuItemUseRangeOfDates.Checked) calcOptions.EndDate = dateTimePicker2.Value;
- _MagCalculator.MagneticCalculations(calcOptions);
+ var progress = new Progress(info =>
+ {
+ toolStripStatusLabel1.Text = info.StatusMessage;
+ toolStripProgressBar1.Value = Math.Min((int)info.PercentComplete, 100);
+ });
+
+ await _MagCalculator.MagneticCalculationsAsync(calcOptions, progress, _calculationCts.Token);
if (_MagCalculator.ResultsOfCalculation == null || !_MagCalculator.ResultsOfCalculation.Any())
{
@@ -269,15 +318,26 @@ private void buttonCalculate_Click(object sender, EventArgs e)
dataGridViewResults.Rows[dataGridViewResults.Rows.Count - 1].Cells["ColumnTotalField"].Style.BackColor = System.Drawing.Color.LightBlue;
saveToolStripMenuItem.Enabled = true;
+ SetStatusTemporary("Calculation complete");
+ }
+ catch (OperationCanceledException)
+ {
+ dataGridViewResults.Rows.Clear();
+ SetStatusTemporary("Calculation cancelled");
+ _MagCalculator = null;
}
catch (Exception ex)
{
+ dataGridViewResults.Rows.Clear();
MessageBox.Show(ex.Message, "Error: Calculating Magnetics", MessageBoxButtons.OK, MessageBoxIcon.Error);
+ toolStripStatusLabel1.Text = "Ready";
_MagCalculator = null;
}
finally
{
- Cursor = Cursors.Default;
+ SetUIBusy(false);
+ _calculationCts?.Dispose();
+ _calculationCts = null;
}
}
}
@@ -297,31 +357,66 @@ private void LoadModels(string selected = null)
if(selectedIdx != Guid.Empty) comboBoxModels.SelectedValue = selectedIdx;
}
- private void addModelToolStripMenuItem_Click(object sender, EventArgs e)
+ private async void addModelToolStripMenuItem_Click(object sender, EventArgs e)
{
+ if (_calculationCts != null) return;
+
using (var fAddModel = new frmAddModel())
{
+ if (string.IsNullOrEmpty(fAddModel.SelectedFilePath))
+ return;
+
+ _calculationCts = new CancellationTokenSource();
try
{
- this.Cursor = Cursors.WaitCursor;
+ SetUIBusy(true);
+ toolStripStatusLabel1.Text = "Reading model file...";
- fAddModel.ShowDialog(this);
+ var progress = new Progress(info =>
+ {
+ toolStripStatusLabel1.Text = info.StatusMessage;
+ toolStripProgressBar1.Value = Math.Min((int)info.PercentComplete, 100);
+ });
- Models.AddOrReplace(fAddModel.Model);
+ await fAddModel.LoadModelDataAsync(fAddModel.SelectedFilePath, progress, _calculationCts.Token);
- Models.Save(Path.Combine(ModelFolder, Resources.File_Name_Magnetic_Model_JSON));
+ SetUIBusy(false);
+ toolStripStatusLabel1.Text = "Ready";
- LoadModels();
+ if (fAddModel.ShowDialog(this) != DialogResult.OK)
+ return;
+
+ SetUIBusy(true);
+ toolStripStatusLabel1.Text = "Saving model...";
+
+ Models.AddOrReplace(fAddModel.Model);
+ await Models.SaveAsync(ModelJson, _calculationCts.Token);
+
+ LoadModels(fAddModel.Model?.ID.ToString());
+ SetStatusTemporary(string.Format("Model added: {0}", fAddModel.Model?.Name));
+ }
+ catch (OperationCanceledException)
+ {
+ SetStatusTemporary("Model loading cancelled");
+ }
+ catch (Exception ex)
+ {
+ MessageBox.Show(ex.Message, "Error: Adding Model", MessageBoxButtons.OK, MessageBoxIcon.Error);
+ toolStripStatusLabel1.Text = "Ready";
}
finally
{
- this.Cursor = Cursors.Default;
+ SetUIBusy(false);
+ _calculationCts?.Dispose();
+ _calculationCts = null;
}
}
}
- private void loadModelToolStripMenuItem_Click(object sender, EventArgs e)
+ private async void loadModelToolStripMenuItem_Click(object sender, EventArgs e)
{
+ if (_calculationCts != null) return;
+
var fDlg = new OpenFileDialog
{
Title = @"Select a Model Data File",
@@ -329,13 +424,48 @@ private void loadModelToolStripMenuItem_Click(object sender, EventArgs e)
Multiselect = false
};
- if (fDlg.ShowDialog() != DialogResult.Cancel)
+ if (fDlg.ShowDialog() == DialogResult.Cancel) return;
+
+ var copyToLocation = Path.Combine(ModelFolder, Path.GetFileName(fDlg.FileName));
+
+ _calculationCts = new CancellationTokenSource();
+ try
{
- var copyToLocation = string.Format("{0}{1}", ModelFolder, Path.GetFileName(fDlg.FileName));
+ SetUIBusy(true);
+ toolStripStatusLabel1.Text = "Copying model file...";
File.Copy(fDlg.FileName, copyToLocation, overwrite: true);
- LoadModels(copyToLocation);
+ toolStripStatusLabel1.Text = "Reading model file...";
+ var progress = new Progress(info =>
+ {
+ toolStripStatusLabel1.Text = info.StatusMessage;
+ toolStripProgressBar1.Value = Math.Min((int)info.PercentComplete, 100);
+ });
+
+ var model = await ModelReader.ReadAsync(copyToLocation, progress, _calculationCts.Token);
+
+ toolStripStatusLabel1.Text = "Saving model collection...";
+ Models.AddOrReplace(model);
+ await Models.SaveAsync(ModelJson, _calculationCts.Token);
+
+ LoadModels(model.ID.ToString());
+ SetStatusTemporary(string.Format("Model loaded: {0}", model.Name));
+ }
+ catch (OperationCanceledException)
+ {
+ SetStatusTemporary("Model loading cancelled");
+ }
+ catch (Exception ex)
+ {
+ MessageBox.Show(ex.Message, "Error: Loading Model", MessageBoxButtons.OK, MessageBoxIcon.Error);
+ toolStripStatusLabel1.Text = "Ready";
+ }
+ finally
+ {
+ SetUIBusy(false);
+ _calculationCts?.Dispose();
+ _calculationCts = null;
}
}
@@ -680,8 +810,12 @@ private void preferencesToolStripMenuItem_Click(object sender, EventArgs e)
SetElevationDisplay();
}
- private void saveToolStripMenuItem_Click(object sender, EventArgs e)
+ private bool _isSaving;
+
+ private async void saveToolStripMenuItem_Click(object sender, EventArgs e)
{
+ if (_isSaving || _MagCalculator == null) return;
+
var fileName = @"Results";
var fldlg = new SaveFileDialog
@@ -694,14 +828,27 @@ private void saveToolStripMenuItem_Click(object sender, EventArgs e)
if (fldlg.ShowDialog() == DialogResult.OK)
{
+ _isSaving = true;
try
{
- Cursor = Cursors.WaitCursor;
- _MagCalculator.SaveResults(fldlg.FileName);
+ buttonCalculate.Enabled = false;
+ saveToolStripMenuItem.Enabled = false;
+ UseWaitCursor = true;
+ toolStripStatusLabel1.Text = "Saving results...";
+ await _MagCalculator.SaveResultsAsync(fldlg.FileName);
+ SetStatusTemporary("Results saved");
+ }
+ catch (Exception ex)
+ {
+ MessageBox.Show(ex.Message, "Error: Saving Results", MessageBoxButtons.OK, MessageBoxIcon.Error);
+ toolStripStatusLabel1.Text = "Error saving";
}
finally
{
- Cursor = Cursors.Default;
+ buttonCalculate.Enabled = true;
+ saveToolStripMenuItem.Enabled = true;
+ UseWaitCursor = false;
+ _isSaving = false;
}
}
}
diff --git a/GeoMagGUI/frmMain.resx b/GeoMagGUI/frmMain.resx
index f25e845..19b62a9 100644
--- a/GeoMagGUI/frmMain.resx
+++ b/GeoMagGUI/frmMain.resx
@@ -189,18 +189,21 @@
XTesb3QPLh88M+QwdP6m681Lt7xuXbu94vbgcOjwnZHokdE77DtTd1PuvriXeW/h/sYH6AdFD6UeVjxS
fNTws+7PbaOWo6fHXMf6Hwc/vj/OGn/2S8Yv7ycKnpCfVEyqTDZPmU2dmnafvvF05dOJZ+nPFmYKf5X+
tfa5zvMffnP8rX82YnbiBf/Fp99LXsq/PPRq2aueuYC5R69TXy/MF72Rf3P4LeNt37vwd5MLWe+x7ys/
- 6H7o/ujz8cGn1E+f/gUDmPP8usTo0wAAAAlwSFlzAAALEAAACxABrSO9dQAAAL5JREFUOE+lkzEOwjAQ
- BF0i8QHoqRC/CDUS1DwCet6DQgkt1HQ0tNDwE3Z8NjIEiJOMNNJFWTt2fHY1zOXCyuaM5DlInc1EHuRW
- XoPUe8m7vyzlXa5kT26C1Gt5k2S+MpYPOfVPxlAOrPQUkgzZCkfJV+ogQ/aNviytzIIsYzwzeZIXyX7T
- JX/COzJkGcPY7hMAy9lZmQXZ1xYizJj7E8lWiMfIUUUaHSPQJDRLq0aKdGrllNaXKYWrzJX+gXNPps0t
- u/MJS48AAAAASUVORK5CYII=
+ 6H7o/ujz8cGn1E+f/gUDmPP8usTo0wAAAAlwSFlzAAALDgAACw4BQL7hQQAAAL1JREFUOE+lk7EOAUEQ
+ hr9S4gXoVeItqCXUHoLe8wglLbVOo6XxJvLJruzdsc7dl0wyO/vP3u7NDOSZAfNysC4D4BxMvzYj4ABs
+ gGsw/X3Yy7IA7sAS6ADrYPor4BY0HxkCD2CSxPpAL1mPg0ZthWP4yi/UqC3QBbblYAa15ryYAifgEt6b
+ XrmMe2rUmmNu+wPE6+yK2ixq30+IeGLdn6i2QiyjpYr8VUaxSWyWRo0UadXKKY2HKcVRdqS/8gSmzS27
+ no2knAAAAABJRU5ErkJggg==
- 17, 17
+ 132, 17
- 132, 17
+ 247, 17
+
+
+ 17, 17
@@ -1334,28 +1337,4 @@
//////////////////////////////////8=
-
- True
-
-
- True
-
-
- True
-
-
- True
-
-
- True
-
-
- True
-
-
- True
-
-
- True
-
\ No newline at end of file
diff --git a/GeoMagSharp-UnitTests/AsyncOperationsUnitTest.cs b/GeoMagSharp-UnitTests/AsyncOperationsUnitTest.cs
new file mode 100644
index 0000000..277d9be
--- /dev/null
+++ b/GeoMagSharp-UnitTests/AsyncOperationsUnitTest.cs
@@ -0,0 +1,763 @@
+/****************************************************************************
+ * File: AsyncOperationsUnitTest.cs
+ * Description: Unit tests for async operations (ReadAsync,
+ * MagneticCalculationsAsync, SaveResultsAsync,
+ * LoadAsync, SaveAsync)
+ * Author: Christopher Strecker
+ * Website: https://github.com/StreckerCM/GeoMagSharpGUI
+ ****************************************************************************/
+
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+using GeoMagSharp;
+
+namespace GeoMagSharp_UnitTests
+{
+ [TestClass]
+ public class AsyncOperationsUnitTest
+ {
+ private static string TestDataPath;
+
+ [ClassInitialize]
+ public static void ClassInit(TestContext _)
+ {
+ var possiblePaths = new[]
+ {
+ Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "TestData"),
+ Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "..", "..", "TestData"),
+ Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "..", "..", "..", "GeoMagSharp-UnitTests", "TestData"),
+ @"C:\GitHub\GeoMagSharpGUI\GeoMagSharp-UnitTests\TestData"
+ };
+
+ foreach (var path in possiblePaths)
+ {
+ var fullPath = Path.GetFullPath(path);
+ if (Directory.Exists(fullPath))
+ {
+ TestDataPath = fullPath;
+ break;
+ }
+ }
+
+ if (string.IsNullOrEmpty(TestDataPath))
+ {
+ throw new DirectoryNotFoundException("Could not find TestData directory");
+ }
+ }
+
+ #region ModelReader.ReadAsync Tests
+
+ [TestMethod]
+ public async Task ReadAsync_ValidCofFile_ReturnsModel()
+ {
+ // Arrange
+ string filePath = Path.Combine(TestDataPath, "WMM2025.COF");
+ if (!File.Exists(filePath))
+ Assert.Inconclusive("WMM2025.COF not found in TestData folder");
+
+ // Act
+ var modelSet = await ModelReader.ReadAsync(filePath);
+
+ // Assert
+ Assert.IsNotNull(modelSet);
+ Assert.AreEqual(knownModels.WMM, modelSet.Type, "Model type should be WMM");
+ Assert.AreEqual(2025.0, modelSet.MinDate, 0.01, "Model year should be 2025.0");
+ Assert.IsTrue(modelSet.NumberOfModels >= 2, "Should have at least M and S models");
+ }
+
+ [TestMethod]
+ public async Task ReadAsync_MatchesSyncRead()
+ {
+ // Arrange
+ string filePath = Path.Combine(TestDataPath, "WMM2025.COF");
+ if (!File.Exists(filePath))
+ Assert.Inconclusive("WMM2025.COF not found in TestData folder");
+
+ // Act
+ var syncResult = ModelReader.Read(filePath);
+ var asyncResult = await ModelReader.ReadAsync(filePath);
+
+ // Assert - async should produce same result as sync
+ Assert.AreEqual(syncResult.Type, asyncResult.Type, "Type should match");
+ Assert.AreEqual(syncResult.MinDate, asyncResult.MinDate, 0.001, "MinDate should match");
+ Assert.AreEqual(syncResult.MaxDate, asyncResult.MaxDate, 0.001, "MaxDate should match");
+ Assert.AreEqual(syncResult.NumberOfModels, asyncResult.NumberOfModels, "NumberOfModels should match");
+
+ // Compare coefficient counts
+ var syncModels = syncResult.GetModels;
+ var asyncModels = asyncResult.GetModels;
+ Assert.AreEqual(syncModels.Count, asyncModels.Count, "Model count should match");
+
+ for (int i = 0; i < syncModels.Count; i++)
+ {
+ Assert.AreEqual(syncModels[i].SharmCoeff.Count, asyncModels[i].SharmCoeff.Count,
+ string.Format("Coefficient count should match for model {0}", i));
+ }
+ }
+
+ [TestMethod]
+ public async Task ReadAsync_CancelledToken_ThrowsOperationCancelled()
+ {
+ // Arrange
+ string filePath = Path.Combine(TestDataPath, "WMM2025.COF");
+ if (!File.Exists(filePath))
+ Assert.Inconclusive("WMM2025.COF not found in TestData folder");
+
+ var cts = new CancellationTokenSource();
+ cts.Cancel(); // Pre-cancel
+
+ // Act & Assert
+ await AssertThrowsAsync(async () =>
+ await ModelReader.ReadAsync(filePath, null, cts.Token));
+ }
+
+ [TestMethod]
+ public async Task ReadAsync_InvalidFile_ThrowsException()
+ {
+ // Act & Assert
+ await AssertThrowsAsync(async () =>
+ await ModelReader.ReadAsync(null));
+
+ await AssertThrowsAsync(async () =>
+ await ModelReader.ReadAsync(string.Empty));
+ }
+
+ [TestMethod]
+ public async Task ReadAsync_NonExistentFile_ThrowsFileNotFoundException()
+ {
+ // Act & Assert
+ await AssertThrowsAsync(async () =>
+ await ModelReader.ReadAsync(@"C:\NonExistent\Path\File.COF"));
+ }
+
+ [TestMethod]
+ public async Task ReadAsync_ReportsProgress()
+ {
+ // Arrange
+ string filePath = Path.Combine(TestDataPath, "WMM2025.COF");
+ if (!File.Exists(filePath))
+ Assert.Inconclusive("WMM2025.COF not found in TestData folder");
+
+ var progressReports = new List();
+ var progress = new SynchronousProgress(progressReports);
+
+ // Act
+ var modelSet = await ModelReader.ReadAsync(filePath, progress);
+
+ // Assert
+ Assert.IsNotNull(modelSet);
+ Assert.AreEqual(2, progressReports.Count, "Should have received 2 progress reports (start and complete)");
+ Assert.AreEqual(1, progressReports[0].CurrentStep, "First report should be step 1");
+ Assert.AreEqual(2, progressReports[1].CurrentStep, "Second report should be step 2");
+ Assert.AreEqual("Model loaded successfully", progressReports[1].StatusMessage);
+ }
+
+ [TestMethod]
+ public async Task ReadAsync_ValidDatFile_ReturnsModel()
+ {
+ // Arrange - Use a DAT file if available in TestData
+ string filePath = Path.Combine(TestDataPath, "IGRF13.DAT");
+ if (!File.Exists(filePath))
+ {
+ var datFiles = Directory.GetFiles(TestDataPath, "*.DAT");
+ if (datFiles.Length == 0)
+ Assert.Inconclusive("No DAT files found in TestData folder");
+ filePath = datFiles[0];
+ }
+
+ // Act
+ var modelSet = await ModelReader.ReadAsync(filePath);
+
+ // Assert
+ Assert.IsNotNull(modelSet);
+ Assert.IsTrue(modelSet.NumberOfModels > 0, "Should have at least one model");
+ }
+
+ #endregion
+
+ #region GeoMag.MagneticCalculationsAsync Tests
+
+ [TestMethod]
+ public async Task MagneticCalculationsAsync_ValidInput_ReturnsResults()
+ {
+ // Arrange
+ string filePath = Path.Combine(TestDataPath, "WMM2025.COF");
+ if (!File.Exists(filePath))
+ Assert.Inconclusive("WMM2025.COF not found in TestData folder");
+
+ var geoMag = new GeoMag();
+ geoMag.LoadModel(filePath);
+
+ var calcOptions = new CalculationOptions
+ {
+ Latitude = 45.0,
+ Longitude = 0.0,
+ StartDate = new DateTime(2025, 7, 1),
+ SecularVariation = true,
+ CalculationMethod = Algorithm.BGS
+ };
+ calcOptions.SetElevation(0, Distance.Unit.meter, true);
+
+ // Act
+ await geoMag.MagneticCalculationsAsync(calcOptions);
+
+ // Assert
+ Assert.IsNotNull(geoMag.ResultsOfCalculation);
+ Assert.IsTrue(geoMag.ResultsOfCalculation.Count > 0, "Should have calculation results");
+
+ var result = geoMag.ResultsOfCalculation[0];
+ Assert.AreNotEqual(0, result.TotalField.Value, "Total field should not be zero");
+ }
+
+ [TestMethod]
+ public async Task MagneticCalculationsAsync_MatchesSyncResults()
+ {
+ // Arrange
+ string filePath = Path.Combine(TestDataPath, "WMM2025.COF");
+ if (!File.Exists(filePath))
+ Assert.Inconclusive("WMM2025.COF not found in TestData folder");
+
+ var calcOptions = new CalculationOptions
+ {
+ Latitude = 45.0,
+ Longitude = 0.0,
+ StartDate = new DateTime(2025, 7, 1),
+ SecularVariation = true,
+ CalculationMethod = Algorithm.BGS
+ };
+ calcOptions.SetElevation(0, Distance.Unit.meter, true);
+
+ // Sync calculation
+ var syncGeoMag = new GeoMag();
+ syncGeoMag.LoadModel(filePath);
+ syncGeoMag.MagneticCalculations(new CalculationOptions(calcOptions));
+
+ // Async calculation
+ var asyncGeoMag = new GeoMag();
+ asyncGeoMag.LoadModel(filePath);
+ await asyncGeoMag.MagneticCalculationsAsync(new CalculationOptions(calcOptions));
+
+ // Assert - results should match
+ Assert.AreEqual(syncGeoMag.ResultsOfCalculation.Count, asyncGeoMag.ResultsOfCalculation.Count,
+ "Result count should match");
+
+ for (int i = 0; i < syncGeoMag.ResultsOfCalculation.Count; i++)
+ {
+ var syncResult = syncGeoMag.ResultsOfCalculation[i];
+ var asyncResult = asyncGeoMag.ResultsOfCalculation[i];
+
+ Assert.AreEqual(syncResult.Declination.Value, asyncResult.Declination.Value, 0.001,
+ string.Format("Declination should match at step {0}", i));
+ Assert.AreEqual(syncResult.Inclination.Value, asyncResult.Inclination.Value, 0.001,
+ string.Format("Inclination should match at step {0}", i));
+ Assert.AreEqual(syncResult.TotalField.Value, asyncResult.TotalField.Value, 0.1,
+ string.Format("TotalField should match at step {0}", i));
+ }
+ }
+
+ [TestMethod]
+ public async Task MagneticCalculationsAsync_CancelledToken_ThrowsOperationCancelled()
+ {
+ // Arrange
+ string filePath = Path.Combine(TestDataPath, "WMM2025.COF");
+ if (!File.Exists(filePath))
+ Assert.Inconclusive("WMM2025.COF not found in TestData folder");
+
+ var geoMag = new GeoMag();
+ geoMag.LoadModel(filePath);
+
+ var calcOptions = new CalculationOptions
+ {
+ Latitude = 45.0,
+ Longitude = 0.0,
+ StartDate = new DateTime(2025, 1, 1),
+ EndDate = new DateTime(2025, 12, 31),
+ StepInterval = 1,
+ SecularVariation = true,
+ CalculationMethod = Algorithm.BGS
+ };
+ calcOptions.SetElevation(0, Distance.Unit.meter, true);
+
+ var cts = new CancellationTokenSource();
+ cts.Cancel(); // Pre-cancel
+
+ // Act & Assert
+ await AssertThrowsAsync(async () =>
+ await geoMag.MagneticCalculationsAsync(calcOptions, null, cts.Token));
+ }
+
+ [TestMethod]
+ public async Task MagneticCalculationsAsync_ReportsProgress()
+ {
+ // Arrange
+ string filePath = Path.Combine(TestDataPath, "WMM2025.COF");
+ if (!File.Exists(filePath))
+ Assert.Inconclusive("WMM2025.COF not found in TestData folder");
+
+ var geoMag = new GeoMag();
+ geoMag.LoadModel(filePath);
+
+ var calcOptions = new CalculationOptions
+ {
+ Latitude = 45.0,
+ Longitude = 0.0,
+ StartDate = new DateTime(2025, 1, 1),
+ EndDate = new DateTime(2025, 3, 1),
+ StepInterval = 30,
+ SecularVariation = true,
+ CalculationMethod = Algorithm.BGS
+ };
+ calcOptions.SetElevation(0, Distance.Unit.meter, true);
+
+ var progressReports = new List();
+ var progress = new SynchronousProgress(progressReports);
+
+ // Act
+ await geoMag.MagneticCalculationsAsync(calcOptions, progress);
+
+ // Assert
+ Assert.IsNotNull(geoMag.ResultsOfCalculation);
+ Assert.IsTrue(progressReports.Count >= 2, "Should have received at least 2 progress reports");
+
+ // Verify progress steps are monotonically non-decreasing
+ for (int i = 1; i < progressReports.Count; i++)
+ {
+ Assert.IsTrue(progressReports[i].CurrentStep >= progressReports[i - 1].CurrentStep,
+ string.Format("Progress steps should be non-decreasing: step {0} vs {1}",
+ progressReports[i - 1].CurrentStep, progressReports[i].CurrentStep));
+ }
+
+ // Last progress report should indicate completion
+ var lastReport = progressReports[progressReports.Count - 1];
+ Assert.AreEqual("Calculation complete", lastReport.StatusMessage,
+ "Last progress report should say 'Calculation complete'");
+ }
+
+ [TestMethod]
+ public async Task MagneticCalculationsAsync_DateRange_ReturnsMultipleResults()
+ {
+ // Arrange
+ string filePath = Path.Combine(TestDataPath, "WMM2025.COF");
+ if (!File.Exists(filePath))
+ Assert.Inconclusive("WMM2025.COF not found in TestData folder");
+
+ var geoMag = new GeoMag();
+ geoMag.LoadModel(filePath);
+
+ var calcOptions = new CalculationOptions
+ {
+ Latitude = 45.0,
+ Longitude = 0.0,
+ StartDate = new DateTime(2025, 1, 1),
+ EndDate = new DateTime(2025, 4, 1),
+ StepInterval = 30,
+ SecularVariation = false,
+ CalculationMethod = Algorithm.BGS
+ };
+ calcOptions.SetElevation(0, Distance.Unit.meter, true);
+
+ // Act
+ await geoMag.MagneticCalculationsAsync(calcOptions);
+
+ // Assert
+ Assert.IsNotNull(geoMag.ResultsOfCalculation);
+ Assert.IsTrue(geoMag.ResultsOfCalculation.Count > 1,
+ "Date range calculation should return multiple results");
+ }
+
+ #endregion
+
+ #region GeoMag.SaveResultsAsync Tests
+
+ [TestMethod]
+ public async Task SaveResultsAsync_ValidResults_SavesFile()
+ {
+ // Arrange
+ string filePath = Path.Combine(TestDataPath, "WMM2025.COF");
+ if (!File.Exists(filePath))
+ Assert.Inconclusive("WMM2025.COF not found in TestData folder");
+
+ var geoMag = new GeoMag();
+ geoMag.LoadModel(filePath);
+
+ var calcOptions = new CalculationOptions
+ {
+ Latitude = 45.0,
+ Longitude = 0.0,
+ StartDate = new DateTime(2025, 7, 1),
+ SecularVariation = true,
+ CalculationMethod = Algorithm.BGS
+ };
+ calcOptions.SetElevation(0, Distance.Unit.meter, true);
+
+ await geoMag.MagneticCalculationsAsync(calcOptions);
+
+ string outputFile = Path.Combine(Path.GetTempPath(), "async_test_output.txt");
+
+ try
+ {
+ // Act
+ await geoMag.SaveResultsAsync(outputFile);
+
+ // Assert
+ Assert.IsTrue(File.Exists(outputFile), "Output file should exist");
+ var content = File.ReadAllText(outputFile);
+ Assert.IsTrue(content.Length > 0, "Output file should have content");
+ }
+ finally
+ {
+ if (File.Exists(outputFile))
+ File.Delete(outputFile);
+ }
+ }
+
+ [TestMethod]
+ public async Task SaveResultsAsync_NoResults_ThrowsException()
+ {
+ // Arrange
+ var geoMag = new GeoMag();
+ string outputFile = Path.Combine(Path.GetTempPath(), "async_test_no_results.txt");
+
+ // Act & Assert
+ await AssertThrowsAsync(async () =>
+ await geoMag.SaveResultsAsync(outputFile));
+ }
+
+ #endregion
+
+ #region MagneticModelCollection LoadAsync/SaveAsync Tests
+
+ [TestMethod]
+ public async Task LoadAsync_SaveAsync_RoundTrip()
+ {
+ // Arrange
+ string filePath = Path.Combine(TestDataPath, "WMM2025.COF");
+ if (!File.Exists(filePath))
+ Assert.Inconclusive("WMM2025.COF not found in TestData folder");
+
+ var model = ModelReader.Read(filePath);
+ var collection = new MagneticModelCollection();
+ collection.Add(model);
+
+ string tempFile = Path.Combine(Path.GetTempPath(), "async_collection_test.json");
+
+ try
+ {
+ // Act - Save
+ bool saveResult = await collection.SaveAsync(tempFile);
+ Assert.IsTrue(saveResult, "Save should succeed");
+ Assert.IsTrue(File.Exists(tempFile), "Saved file should exist");
+
+ // Act - Load
+ var loadedCollection = await MagneticModelCollection.LoadAsync(tempFile);
+
+ // Assert
+ Assert.IsNotNull(loadedCollection);
+ Assert.AreEqual(1, loadedCollection.TList.Count, "Should have one model");
+
+ var loadedModel = loadedCollection.TList[0];
+ Assert.AreEqual(model.Name, loadedModel.Name, "Model name should match");
+ Assert.AreEqual(model.MinDate, loadedModel.MinDate, 0.001, "MinDate should match");
+ Assert.AreEqual(model.MaxDate, loadedModel.MaxDate, 0.001, "MaxDate should match");
+ Assert.AreEqual(model.Type, loadedModel.Type, "Type should match");
+ }
+ finally
+ {
+ if (File.Exists(tempFile))
+ File.Delete(tempFile);
+ }
+ }
+
+ [TestMethod]
+ public async Task LoadAsync_NonExistentFile_ReturnsEmptyCollection()
+ {
+ // Act
+ var collection = await MagneticModelCollection.LoadAsync(@"C:\NonExistent\Path\File.json");
+
+ // Assert
+ Assert.IsNotNull(collection);
+ Assert.AreEqual(0, collection.TList.Count, "Should return empty collection");
+ }
+
+ [TestMethod]
+ public async Task SaveAsync_CancelledToken_ThrowsOperationCancelled()
+ {
+ // Arrange
+ var collection = new MagneticModelCollection();
+ string tempFile = Path.Combine(Path.GetTempPath(), "async_cancel_test.json");
+ var cts = new CancellationTokenSource();
+ cts.Cancel(); // Pre-cancel
+
+ // Act & Assert
+ await AssertThrowsAsync(async () =>
+ await collection.SaveAsync(tempFile, cts.Token));
+ }
+
+ [TestMethod]
+ public async Task LoadAsync_EmptyFilename_ReturnsEmptyCollection()
+ {
+ // Act
+ var collection = await MagneticModelCollection.LoadAsync(string.Empty);
+
+ // Assert
+ Assert.IsNotNull(collection);
+ Assert.AreEqual(0, collection.TList.Count);
+ }
+
+ #endregion
+
+ #region CalculationProgressInfo Tests
+
+ [TestMethod]
+ public void CalculationProgressInfo_PercentComplete_CalculatesCorrectly()
+ {
+ // Arrange & Act
+ var info = new CalculationProgressInfo
+ {
+ CurrentStep = 5,
+ TotalSteps = 10,
+ StatusMessage = "Testing"
+ };
+
+ // Assert
+ Assert.AreEqual(50.0, info.PercentComplete, 0.001, "50% complete");
+ }
+
+ [TestMethod]
+ public void CalculationProgressInfo_ZeroTotalSteps_ReturnsZeroPercent()
+ {
+ // Arrange & Act
+ var info = new CalculationProgressInfo
+ {
+ CurrentStep = 5,
+ TotalSteps = 0
+ };
+
+ // Assert
+ Assert.AreEqual(0.0, info.PercentComplete, 0.001, "Should return 0 when TotalSteps is 0");
+ }
+
+ #endregion
+
+ #region Additional Edge Case Tests
+
+ [TestMethod]
+ public async Task MagneticCalculationsAsync_NoModelLoaded_ThrowsException()
+ {
+ // Arrange
+ var geoMag = new GeoMag();
+ var calcOptions = new CalculationOptions
+ {
+ Latitude = 45.0,
+ Longitude = 0.0,
+ StartDate = new DateTime(2025, 7, 1),
+ CalculationMethod = Algorithm.BGS
+ };
+ calcOptions.SetElevation(0, Distance.Unit.meter, true);
+
+ // Act & Assert
+ await AssertThrowsAsync(async () =>
+ await geoMag.MagneticCalculationsAsync(calcOptions));
+ }
+
+ [TestMethod]
+ public async Task MagneticCalculationsAsync_DateOutOfRange_ThrowsException()
+ {
+ // Arrange
+ string filePath = Path.Combine(TestDataPath, "WMM2025.COF");
+ if (!File.Exists(filePath))
+ Assert.Inconclusive("WMM2025.COF not found in TestData folder");
+
+ var geoMag = new GeoMag();
+ geoMag.LoadModel(filePath);
+
+ var calcOptions = new CalculationOptions
+ {
+ Latitude = 45.0,
+ Longitude = 0.0,
+ StartDate = new DateTime(1900, 1, 1), // Well outside WMM2025 range
+ CalculationMethod = Algorithm.BGS
+ };
+ calcOptions.SetElevation(0, Distance.Unit.meter, true);
+
+ // Act & Assert
+ await AssertThrowsAsync(async () =>
+ await geoMag.MagneticCalculationsAsync(calcOptions));
+ }
+
+ [TestMethod]
+ public async Task ReadAsync_UnsupportedExtension_ThrowsModelNotLoaded()
+ {
+ // Arrange - Create a temp file with unsupported extension
+ string tempFile = Path.Combine(Path.GetTempPath(), "test_async.xyz");
+ try
+ {
+ File.WriteAllText(tempFile, "test content");
+
+ // Act & Assert
+ await AssertThrowsAsync(async () =>
+ await ModelReader.ReadAsync(tempFile));
+ }
+ finally
+ {
+ if (File.Exists(tempFile))
+ File.Delete(tempFile);
+ }
+ }
+
+ [TestMethod]
+ public async Task SaveResultsAsync_CancelledToken_ThrowsOperationCancelled()
+ {
+ // Arrange
+ string filePath = Path.Combine(TestDataPath, "WMM2025.COF");
+ if (!File.Exists(filePath))
+ Assert.Inconclusive("WMM2025.COF not found in TestData folder");
+
+ var geoMag = new GeoMag();
+ geoMag.LoadModel(filePath);
+
+ var calcOptions = new CalculationOptions
+ {
+ Latitude = 45.0,
+ Longitude = 0.0,
+ StartDate = new DateTime(2025, 7, 1),
+ SecularVariation = true,
+ CalculationMethod = Algorithm.BGS
+ };
+ calcOptions.SetElevation(0, Distance.Unit.meter, true);
+
+ await geoMag.MagneticCalculationsAsync(calcOptions);
+
+ var cts = new CancellationTokenSource();
+ cts.Cancel(); // Pre-cancel
+
+ string outputFile = Path.Combine(Path.GetTempPath(), "async_cancel_save_test.txt");
+
+ // Act & Assert
+ await AssertThrowsAsync(async () =>
+ await geoMag.SaveResultsAsync(outputFile, false, cts.Token));
+ }
+
+ [TestMethod]
+ public async Task LoadAsync_CancelledToken_ThrowsOperationCancelled()
+ {
+ // Arrange
+ var cts = new CancellationTokenSource();
+ cts.Cancel(); // Pre-cancel
+
+ string tempFile = Path.Combine(Path.GetTempPath(), "async_cancel_load_test.json");
+ try
+ {
+ File.WriteAllText(tempFile, "[]");
+
+ // Act & Assert
+ await AssertThrowsAsync(async () =>
+ await MagneticModelCollection.LoadAsync(tempFile, cts.Token));
+ }
+ finally
+ {
+ if (File.Exists(tempFile))
+ File.Delete(tempFile);
+ }
+ }
+
+ [TestMethod]
+ public async Task SaveResultsAsync_MatchesSyncSave()
+ {
+ // Arrange
+ string filePath = Path.Combine(TestDataPath, "WMM2025.COF");
+ if (!File.Exists(filePath))
+ Assert.Inconclusive("WMM2025.COF not found in TestData folder");
+
+ var calcOptions = new CalculationOptions
+ {
+ Latitude = 45.0,
+ Longitude = 0.0,
+ StartDate = new DateTime(2025, 7, 1),
+ SecularVariation = true,
+ CalculationMethod = Algorithm.BGS
+ };
+ calcOptions.SetElevation(0, Distance.Unit.meter, true);
+
+ // Sync save
+ var syncGeoMag = new GeoMag();
+ syncGeoMag.LoadModel(filePath);
+ syncGeoMag.MagneticCalculations(new CalculationOptions(calcOptions));
+
+ string syncFile = Path.Combine(Path.GetTempPath(), "sync_save_test.txt");
+ syncGeoMag.SaveResults(syncFile);
+
+ // Async save
+ var asyncGeoMag = new GeoMag();
+ asyncGeoMag.LoadModel(filePath);
+ await asyncGeoMag.MagneticCalculationsAsync(new CalculationOptions(calcOptions));
+
+ string asyncFile = Path.Combine(Path.GetTempPath(), "async_save_test.txt");
+ await asyncGeoMag.SaveResultsAsync(asyncFile);
+
+ try
+ {
+ // Assert - file contents should be identical
+ string syncContent = File.ReadAllText(syncFile);
+ string asyncContent = File.ReadAllText(asyncFile);
+ Assert.AreEqual(syncContent, asyncContent, "Sync and async save should produce identical output");
+ }
+ finally
+ {
+ if (File.Exists(syncFile)) File.Delete(syncFile);
+ if (File.Exists(asyncFile)) File.Delete(asyncFile);
+ }
+ }
+
+ #endregion
+
+ #region Helper Methods
+
+ ///
+ /// Helper to assert that an async operation throws the expected exception type.
+ /// MSTest v1 [ExpectedException] does not work with async Task methods reliably.
+ ///
+ private static async Task AssertThrowsAsync(Func action) where TException : Exception
+ {
+ try
+ {
+ await action();
+ Assert.Fail("Expected {0} but no exception was thrown", typeof(TException).Name);
+ }
+ catch (TException)
+ {
+ // Expected
+ }
+ catch (AggregateException ex) when (ex.InnerException is TException)
+ {
+ // Also acceptable - Task.Run can wrap in AggregateException
+ }
+ }
+
+ ///
+ /// Synchronous IProgress implementation that captures reports immediately
+ /// without posting to SynchronizationContext. Avoids race conditions in tests.
+ ///
+ private class SynchronousProgress : IProgress
+ {
+ private readonly List _reports;
+
+ public SynchronousProgress(List reports)
+ {
+ _reports = reports;
+ }
+
+ public void Report(CalculationProgressInfo value)
+ {
+ _reports.Add(value);
+ }
+ }
+
+ #endregion
+ }
+}
diff --git a/GeoMagSharp-UnitTests/GeoMagSharp-UnitTests.csproj b/GeoMagSharp-UnitTests/GeoMagSharp-UnitTests.csproj
index 4d6eec4..bcad670 100644
--- a/GeoMagSharp-UnitTests/GeoMagSharp-UnitTests.csproj
+++ b/GeoMagSharp-UnitTests/GeoMagSharp-UnitTests.csproj
@@ -50,6 +50,7 @@
+
diff --git a/GeoMagSharp/GeoMag.cs b/GeoMagSharp/GeoMag.cs
index aaef4e0..cee07ae 100644
--- a/GeoMagSharp/GeoMag.cs
+++ b/GeoMagSharp/GeoMag.cs
@@ -10,6 +10,8 @@
using System.Linq;
using System.Text;
using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
namespace GeoMagSharp
@@ -259,10 +261,214 @@ public void SaveResults(string fileName, bool loadAfterSave = false)
{
outFile.Write(tabStrRight.ToString());
}
-
-
+
+
+ }
+
+ ///
+ /// Asynchronously performs magnetic field calculations over the specified date range and location.
+ /// Results are stored in .
+ ///
+ /// The calculation parameters including location, dates, and elevation.
+ /// Optional progress reporter for UI updates.
+ /// Optional cancellation token to cancel the operation.
+ /// Thrown when no model has been loaded.
+ /// Thrown when the start or end date is outside the model's valid range.
+ /// Thrown when the operation is cancelled.
+ public async Task MagneticCalculationsAsync(CalculationOptions inCalculationOptions,
+ IProgress progress = null,
+ CancellationToken cancellationToken = default)
+ {
+ _CalculationOptions = null;
+ ResultsOfCalculation = null;
+
+ if (_Models == null || _Models.NumberOfModels.Equals(0))
+ throw new GeoMagExceptionModelNotLoaded("Error: No models avaliable for calculation");
+
+ if (!_Models.IsDateInRange(inCalculationOptions.StartDate))
+ {
+ throw new GeoMagExceptionOutOfRange(string.Format("Error: the date {0} is out of range for this model{1}The valid date range for the is {2} to {3}",
+ inCalculationOptions.StartDate.ToShortDateString(), Environment.NewLine, _Models.MinDate.ToDateTime().ToShortDateString(),
+ _Models.MaxDate.ToDateTime().ToShortDateString()));
+ }
+
+ if (inCalculationOptions.EndDate.Equals(DateTime.MinValue)) inCalculationOptions.EndDate = inCalculationOptions.StartDate;
+
+ if (!_Models.IsDateInRange(inCalculationOptions.EndDate))
+ {
+ throw new GeoMagExceptionOutOfRange(string.Format("Error: the date {0} is out of range for this model{1}The valid date range for the is {2} to {3}",
+ inCalculationOptions.EndDate.ToShortDateString(), Environment.NewLine, _Models.MinDate.ToDateTime().ToShortDateString(),
+ _Models.MaxDate.ToDateTime().ToShortDateString()));
+ }
+
+ TimeSpan timespan = (inCalculationOptions.EndDate.Date - inCalculationOptions.StartDate.Date);
+
+ double dayInc = inCalculationOptions.StepInterval < 1 ? 1 : inCalculationOptions.StepInterval;
+
+ // Count total steps for progress reporting
+ int totalSteps = 0;
+ double tempIdx = 0;
+ while (tempIdx <= timespan.Days)
+ {
+ totalSteps++;
+ tempIdx = ((tempIdx < timespan.Days) && ((tempIdx + dayInc) > timespan.Days))
+ ? timespan.Days
+ : tempIdx + dayInc;
+ }
+
+ double dateIdx = 0;
+ int currentStep = 0;
+
+ ResultsOfCalculation = new List();
+
+ _CalculationOptions = new CalculationOptions(inCalculationOptions);
+
+ while (dateIdx <= timespan.Days)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ DateTime intervalDate = _CalculationOptions.StartDate.AddDays(dateIdx);
+
+ currentStep++;
+ progress?.Report(new CalculationProgressInfo
+ {
+ CurrentStep = currentStep,
+ TotalSteps = totalSteps,
+ StatusMessage = string.Format("Calculating for {0}...", intervalDate.ToString("yyyy-MM-dd"))
+ });
+
+ var internalSH = new Coefficients();
+ var externalSH = new Coefficients();
+
+ _Models.GetIntExt(intervalDate.ToDecimal(), out internalSH, out externalSH);
+
+ var models = _Models;
+ var calcOptions = _CalculationOptions;
+
+ var magCalcDate = await Task.Run(() =>
+ Calculator.SpotCalculation(calcOptions, intervalDate, models, internalSH, externalSH, models.EarthRadius),
+ cancellationToken).ConfigureAwait(false);
+
+ if (magCalcDate != null) ResultsOfCalculation.Add(magCalcDate);
+
+ dateIdx = ((dateIdx < timespan.Days) && ((dateIdx + dayInc) > timespan.Days))
+ ? timespan.Days
+ : dateIdx + dayInc;
+ }
+
+ progress?.Report(new CalculationProgressInfo
+ {
+ CurrentStep = totalSteps,
+ TotalSteps = totalSteps,
+ StatusMessage = "Calculation complete"
+ });
+ }
+
+ ///
+ /// Asynchronously saves the calculation results to a tab-separated text file.
+ ///
+ /// The output file path.
+ /// Reserved for future use.
+ /// Optional cancellation token to cancel the operation.
+ /// Thrown when no calculation results are available.
+ /// Thrown when the file is locked or cannot be deleted.
+ /// Thrown when the operation is cancelled.
+ public async Task SaveResultsAsync(string fileName, bool loadAfterSave = false,
+ CancellationToken cancellationToken = default)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ if (ResultsOfCalculation == null)
+ throw new GeoMagExceptionModelNotLoaded("Error: No calculation results to save");
+
+ if (_Models == null || _CalculationOptions == null)
+ throw new GeoMagExceptionModelNotLoaded("Error: Model and calculation options must be set before saving results");
+
+ // Build the output string on the current thread (fast), write to file on background thread
+ Int32 lineCount = 0;
+ var tabStrRight = new StringBuilder();
+
+ tabStrRight.AppendFormat("{0}:\t{1}{2}", "Model".PadLeft(15, ' '), Path.GetFileNameWithoutExtension(_Models.Name).ToUpper(), Environment.NewLine);
+ lineCount++;
+
+ tabStrRight.AppendFormat("{0}:\t{1}{2}", "latitude".PadLeft(15, ' '), _CalculationOptions.Latitude.ToString("F7"), Environment.NewLine);
+ lineCount++;
+
+ tabStrRight.AppendFormat("{0}:\t{1}{2}", "longitude".PadLeft(15, ' '), _CalculationOptions.Longitude.ToString("F7"), Environment.NewLine);
+ lineCount++;
+
+ var elevation = _CalculationOptions.GetElevation;
+
+ tabStrRight.AppendFormat("{0}:\t{1}\t{2}{3}", string.Format("{0}", elevation[0]).PadLeft(15, ' '), Convert.ToDouble(elevation[1]).ToString("F4"), elevation[2], Environment.NewLine);
+ lineCount++;
+
+ tabStrRight.AppendFormat("{0}", Environment.NewLine);
+ lineCount++;
+
+ const Int32 padlen = 25;
+ const string rowFormat = "{0}\t{1}\t{2}\t{3}\t{4}\t{5}\t{6}\t{7}{8}";
+
+ tabStrRight.AppendFormat(rowFormat,
+ "Date".PadRight(padlen, ' '), "Declination (+E/W)".PadRight(padlen, ' '), "Inclination (+D/-U)".PadRight(padlen, ' '),
+ "Horizontal Intensity".PadRight(padlen, ' '), "North Comp (+N/-S)".PadRight(padlen, ' '), "East Comp (+E/-W)".PadRight(padlen, ' '),
+ "Vertical Comp (+D/-U)".PadRight(padlen, ' '), "Total Field".PadRight(padlen, ' '), Environment.NewLine);
+ lineCount++;
+
+ tabStrRight.AppendFormat(rowFormat,
+ "".PadRight(padlen, ' '), "deg".PadRight(padlen, ' '), "deg".PadRight(padlen, ' '),
+ "nT".PadRight(padlen, ' '), "nT".PadRight(padlen, ' '), "nT".PadRight(padlen, ' '),
+ "nT".PadRight(padlen, ' '), "nT".PadRight(padlen, ' '), Environment.NewLine);
+ lineCount++;
+
+ tabStrRight.AppendFormat("{0}", Environment.NewLine);
+ lineCount++;
+
+ foreach (var result in ResultsOfCalculation)
+ {
+ tabStrRight.AppendFormat(rowFormat,
+ result.Date.ToString("MM/dd/yyyy").PadRight(padlen, ' '), result.Declination.Value.ToString("F3").PadRight(padlen, ' '),
+ result.Inclination.Value.ToString("F3").PadRight(padlen, ' '), result.HorizontalIntensity.Value.ToString("F2").PadRight(padlen, ' '),
+ result.NorthComp.Value.ToString("F2").PadRight(padlen, ' '), result.EastComp.Value.ToString("F2").PadRight(padlen, ' '),
+ result.VerticalComp.Value.ToString("F2").PadRight(padlen, ' '), result.TotalField.Value.ToString("F2").PadRight(padlen, ' '),
+ Environment.NewLine);
+
+ lineCount++;
+ }
+
+ tabStrRight.AppendFormat(rowFormat,
+ "Change Per year".PadRight(padlen, ' '), ResultsOfCalculation.First().Declination.ChangePerYear.ToString("F3").PadRight(padlen, ' '),
+ ResultsOfCalculation.First().Inclination.ChangePerYear.ToString("F3").PadRight(padlen, ' '), ResultsOfCalculation.First().HorizontalIntensity.ChangePerYear.ToString("F2").PadRight(padlen, ' '),
+ ResultsOfCalculation.First().NorthComp.ChangePerYear.ToString("F2").PadRight(padlen, ' '), ResultsOfCalculation.First().EastComp.ChangePerYear.ToString("F2").PadRight(padlen, ' '),
+ ResultsOfCalculation.First().VerticalComp.ChangePerYear.ToString("F2").PadRight(padlen, ' '), ResultsOfCalculation.First().TotalField.ChangePerYear.ToString("F2").PadRight(padlen, ' '),
+ Environment.NewLine);
+
+ var content = tabStrRight.ToString();
+
+ await Task.Run(() =>
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ if (ModelReader.IsFileLocked(fileName))
+ throw new GeoMagExceptionOpenError(string.Format("Error: The file '{0}' is locked by another user or application",
+ Path.GetFileName(fileName)));
+
+ if (File.Exists(fileName))
+ {
+ try
+ {
+ File.Delete(fileName);
+ }
+ catch (Exception e)
+ {
+ throw new GeoMagExceptionOpenError(string.Format("Error: The file '{0}' could not be deleted",
+ Path.GetFileName(fileName)), e);
+ }
+ }
+
+ File.WriteAllText(fileName, content);
+ }, cancellationToken).ConfigureAwait(false);
}
}
diff --git a/GeoMagSharp/GeoMagSharp.csproj b/GeoMagSharp/GeoMagSharp.csproj
index 30a0589..41d9808 100644
--- a/GeoMagSharp/GeoMagSharp.csproj
+++ b/GeoMagSharp/GeoMagSharp.csproj
@@ -63,6 +63,8 @@
+
+
diff --git a/GeoMagSharp/ModelReader.cs b/GeoMagSharp/ModelReader.cs
index 4db164c..33cc50d 100644
--- a/GeoMagSharp/ModelReader.cs
+++ b/GeoMagSharp/ModelReader.cs
@@ -10,6 +10,8 @@
using System.Globalization;
using System.Linq;
using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
namespace GeoMagSharp
{
@@ -98,6 +100,71 @@ public static MagneticModelSet Read(string modelFile, string svFile)
Path.GetExtension(modelFile).ToUpper())));
}
+ ///
+ /// Asynchronously reads a magnetic model from a coefficient file.
+ ///
+ /// Path to the coefficient file (.COF or .DAT)
+ /// Optional progress reporter
+ /// Optional cancellation token
+ /// A MagneticModelSet containing the parsed model data
+ /// File does not exist
+ /// File is locked by another process
+ /// File type not supported or no models found
+ /// File contains invalid or malformed data
+ /// The operation was cancelled
+ public static async Task ReadAsync(string modelFile,
+ IProgress progress = null,
+ CancellationToken cancellationToken = default)
+ {
+ if (string.IsNullOrWhiteSpace(modelFile))
+ throw new ArgumentNullException(nameof(modelFile), "Model file path cannot be null or empty");
+
+ if (!File.Exists(modelFile))
+ throw new GeoMagExceptionFileNotFound(string.Format("Error: The file '{0}' was not found",
+ modelFile));
+
+ cancellationToken.ThrowIfCancellationRequested();
+
+ if (IsFileLocked(modelFile))
+ throw new GeoMagExceptionOpenError(string.Format("Error: The file '{0}' is locked by another user or application",
+ Path.GetFileName(modelFile)));
+
+ progress?.Report(new CalculationProgressInfo
+ {
+ CurrentStep = 1,
+ TotalSteps = 2,
+ StatusMessage = "Reading coefficient file..."
+ });
+
+ var extension = Path.GetExtension(modelFile).ToUpper();
+
+ MagneticModelSet result = await Task.Run(() =>
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ switch (extension)
+ {
+ case ".COF":
+ return COFreader(modelFile);
+
+ case ".DAT":
+ return DATreader(modelFile);
+ }
+
+ throw new GeoMagExceptionModelNotLoaded(string.Format("Error: The file type '{0}' is not supported",
+ extension));
+ }, cancellationToken).ConfigureAwait(false);
+
+ progress?.Report(new CalculationProgressInfo
+ {
+ CurrentStep = 2,
+ TotalSteps = 2,
+ StatusMessage = "Model loaded successfully"
+ });
+
+ return result;
+ }
+
///
/// Detects whether a COF file header uses the new format (year first) or old format (model name first).
/// New format (WMM2020+): " 2020.0 WMM-2020 12/10/2019"
diff --git a/GeoMagSharp/Models/Magnetic/MagneticModelCollection.cs b/GeoMagSharp/Models/Magnetic/MagneticModelCollection.cs
index d6b5741..ae7ecf9 100644
--- a/GeoMagSharp/Models/Magnetic/MagneticModelCollection.cs
+++ b/GeoMagSharp/Models/Magnetic/MagneticModelCollection.cs
@@ -11,6 +11,8 @@
using System.Data;
using System.IO;
using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
namespace GeoMagSharp
{
@@ -214,6 +216,95 @@ public static MagneticModelCollection Load(string filename)
return outData;
}
+ ///
+ /// Asynchronously serializes the collection to a JSON file.
+ ///
+ /// The file path to save to.
+ /// Optional cancellation token.
+ /// true if the save was successful; otherwise, false.
+ public async Task SaveAsync(string filename, CancellationToken cancellationToken = default)
+ {
+ if (string.IsNullOrEmpty(filename)) return false;
+
+ cancellationToken.ThrowIfCancellationRequested();
+
+ var inData = this;
+
+ return await Task.Run(() =>
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ try
+ {
+ JsonSerializer serializer = new JsonSerializer
+ {
+ NullValueHandling = NullValueHandling.Ignore,
+ Formatting = Newtonsoft.Json.Formatting.Indented
+ };
+
+ using (StreamWriter sw = new StreamWriter(filename))
+ using (JsonWriter writer = new JsonTextWriter(sw))
+ {
+ serializer.Serialize(writer, inData);
+ }
+
+ return true;
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine("MagneticModelCollection Error: {0}", ex.ToString());
+ return false;
+ }
+ }, cancellationToken).ConfigureAwait(false);
+ }
+
+ ///
+ /// Asynchronously deserializes a collection from a JSON file.
+ ///
+ /// The file path to load from.
+ /// Optional cancellation token.
+ /// The loaded collection, or a new empty collection if the file is missing or invalid.
+ public static async Task LoadAsync(string filename, CancellationToken cancellationToken = default)
+ {
+ if (string.IsNullOrEmpty(filename)) return new MagneticModelCollection();
+
+ if (!System.IO.File.Exists(filename)) return new MagneticModelCollection();
+
+ cancellationToken.ThrowIfCancellationRequested();
+
+ return await Task.Run(() =>
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ MagneticModelCollection outData = null;
+
+ try
+ {
+ JsonSerializer serializer = new JsonSerializer();
+
+ serializer.NullValueHandling = NullValueHandling.Ignore;
+ serializer.Formatting = Newtonsoft.Json.Formatting.Indented;
+
+ using (var sr = new System.IO.StreamReader(filename))
+ using (var reader = new JsonTextReader(sr))
+ {
+ var deserializeList = JsonConvert.DeserializeObject>(serializer.Deserialize(reader).ToString());
+
+ outData = new MagneticModelCollection();
+
+ outData.AddRange(deserializeList);
+ }
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine("MagneticModelCollection Error: {0}", ex.ToString());
+ outData = null;
+ }
+
+ return outData;
+ }, cancellationToken).ConfigureAwait(false);
+ }
+
#endregion
#region getters & setters
diff --git a/GeoMagSharp/Models/Progress/CalculationProgressInfo.cs b/GeoMagSharp/Models/Progress/CalculationProgressInfo.cs
new file mode 100644
index 0000000..2531e49
--- /dev/null
+++ b/GeoMagSharp/Models/Progress/CalculationProgressInfo.cs
@@ -0,0 +1,45 @@
+/****************************************************************************
+ * File: CalculationProgressInfo.cs
+ * Description: Progress reporting data for async operations
+ * Author: Christopher Strecker
+ * Website: https://github.com/StreckerCM/GeoMagSharpGUI
+ ****************************************************************************/
+
+namespace GeoMagSharp
+{
+ ///
+ /// Provides progress information for long-running async operations
+ /// such as model loading and magnetic field calculations.
+ ///
+ public class CalculationProgressInfo
+ {
+ ///
+ /// The current step number in the operation.
+ ///
+ public int CurrentStep { get; set; }
+
+ ///
+ /// The total number of steps in the operation.
+ ///
+ public int TotalSteps { get; set; }
+
+ ///
+ /// A human-readable status message describing the current operation.
+ ///
+ public string StatusMessage { get; set; }
+
+ ///
+ /// Gets the percentage complete (0-100) based on CurrentStep and TotalSteps.
+ /// Returns 0 if TotalSteps is 0 or negative.
+ ///
+ public double PercentComplete
+ {
+ get
+ {
+ return TotalSteps > 0
+ ? (CurrentStep * 100.0 / TotalSteps)
+ : 0;
+ }
+ }
+ }
+}
diff --git a/docs/features/03-async-operations/tasks.md b/docs/features/03-async-operations/tasks.md
new file mode 100644
index 0000000..8ea5766
--- /dev/null
+++ b/docs/features/03-async-operations/tasks.md
@@ -0,0 +1,48 @@
+# Feature: Async Model Reader and Calculations
+Issue: #24
+Branch: feature/issue-24-async-operations
+
+## Tasks
+
+### Library - GeoMagSharp
+- [x] Create `CalculationProgressInfo` class (`GeoMagSharp/Models/Progress/CalculationProgressInfo.cs`)
+- [x] Add `ModelReader.ReadAsync()` with progress and cancellation support
+- [x] Add `GeoMag.MagneticCalculationsAsync()` with progress and cancellation support
+- [x] Add `GeoMag.SaveResultsAsync()` with background file write
+- [x] Add `MagneticModelCollection.LoadAsync()` for async JSON deserialization
+- [x] Add `MagneticModelCollection.SaveAsync()` for async JSON serialization
+- [x] Use `ConfigureAwait(false)` in all library async methods
+- [x] Keep synchronous methods for backward compatibility
+
+### UI - GeoMagGUI
+- [x] Add StatusStrip with progress bar and Cancel button to `frmMain.Designer.cs`
+- [x] Convert `buttonCalculate_Click` to async with progress reporting
+- [x] Convert `saveToolStripMenuItem_Click` to async
+- [x] Add `CancellationTokenSource` field and `SetUIBusy()` helper to `frmMain.cs`
+- [x] Add Cancel button click handler
+- [x] Add Escape key cancellation support
+- [x] Add `LoadModelDataAsync()` to `frmAddModel.cs`
+- [x] Extract `DisplayModelData()` from `LoadModelData()` in `frmAddModel.cs`
+
+### Testing
+- [x] Create `AsyncOperationsUnitTest.cs` with async unit tests
+- [x] Add test file to `GeoMagSharp-UnitTests.csproj`
+- [x] All 83 tests pass (81 passed, 2 skipped for missing DAT files)
+
+### Design Notes
+- `Calculator.SpotCalculationAsync()` from the spec was intentionally not created as a standalone method.
+ Each `SpotCalculation()` call is wrapped in `Task.Run` within `MagneticCalculationsAsync()`, avoiding
+ the "async-over-sync" anti-pattern while still achieving off-UI-thread execution with cancellation.
+
+### Ralph Loop Fixes Applied
+- [x] [REVIEWER] Re-entrancy guard, Dispose cleanup, ConfigureAwait(true) in UI, progress step fix
+- [x] [TESTER] SynchronousProgress helper, 6 additional tests for edge cases
+- [x] [UI_UX_DESIGNER] Escape key guard, grid clear on cancel/error, accessibility names, tooltip
+- [x] [SECURITY_AUDITOR] TOCTOU fix in SaveResultsAsync, WriteAllText, info disclosure fix
+- [x] [REVIEWER-2] Save re-entrancy guard (_isSaving), null check for _MagCalculator, _Models/_CalculationOptions null guard
+
+## Completion Criteria
+- [x] All tasks checked
+- [x] Build succeeds
+- [x] Tests pass (83 total: 81 passed, 2 skipped)
+- [x] 2 clean Ralph Loop cycles (Cycle 3: Iterations 12-17, Cycle 4: Iterations 18-23)