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)