diff --git a/CLAUDE.md b/CLAUDE.md index 057242b..4766ffe 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -4,77 +4,233 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co ## Project Overview -GeoMagSharpGUI is a Windows desktop application for geomagnetic field calculations using spherical harmonic models (IGRF, WMM, EMM). It provides a GUI for calculating magnetic declination, inclination, and field strength at any location and date. +GeoMagSharpGUI is a Windows desktop application for calculating geomagnetic field values using spherical harmonic models. It provides a graphical interface for computing magnetic declination, inclination, and field intensity at any location and date. **Tech Stack:** .NET Framework 4.0 WinForms application (C#), x86 platform ## Build Commands ```bash -# Release build -msbuild GeoMagGUI.sln /p:Configuration=Release /p:Platform="x86" - -# Debug build +# Debug build (x86) msbuild GeoMagGUI.sln /p:Configuration=Debug /p:Platform="x86" +# Release build (x86) +msbuild GeoMagGUI.sln /p:Configuration=Release /p:Platform="x86" + # Run unit tests vstest.console.exe GeoMagSharp-UnitTests\bin\Debug\GeoMagSharp-UnitTests.dll + +# Run the application (after build) +GeoMagGUI\bin\Debug\GeoMagGUI.exe +``` + +Note: Use Developer Command Prompt for Visual Studio. This is a legacy .NET Framework project. + +## Branching Strategy + +``` +master ←──────── Stable releases + ↑ + │ PR merge + │ +preview ←─────── Pre-release testing and development + ↑ + │ PR merge + │ +feature/* ─────── Feature development work ``` -## Session Start Protocol +### Branch Guidelines -At the start of every session: -1. Read `docs/features/ACTIVE_WORK.md` if it exists -2. Check auto memory (`MEMORY.md`) for prior context -3. Run `git log --oneline -10` and `git status` to understand current state -4. Ask the user what they'd like to work on before making assumptions +| Branch | Purpose | Description | +|--------|---------|-------------| +| `master` | Production releases | Stable release builds | +| `preview` | Development | Integration testing before release | +| `feature/*` | Feature work | Development branches for new features | -Before ending a session or when context is getting long: -1. Update `docs/features/ACTIVE_WORK.md` with current progress, decisions made, and next steps -2. Save any important patterns or learnings to auto memory +### Workflow +1. Create feature branches from `preview` +2. PR feature branches to `preview` for integration +3. PR `preview` to `master` for releases +4. See `docs/RELEASE_PROCESS.md` for detailed release instructions -## Branching Strategy +### Branch Protection Rules — NEVER VIOLATE -4-branch flow: `feature/*` -> `development` -> `preview` -> `master` +- **NEVER commit directly to `master`.** All changes to `master` must come through reviewed and approved PRs from `preview`. +- **NEVER commit directly to `preview`.** All changes to `preview` must come through PRs from `feature/*` branches. +- **NEVER push directly to protected branches.** No force-pushes, no direct commits, no exceptions. +- **NEVER create or merge a PR without explicit user confirmation.** Always ask the user before creating a PR and before merging one. Draft PRs are acceptable without confirmation, but converting to ready-for-review or merging requires approval. +- **All development work happens on `feature/*` branches.** This is the only place where direct commits are allowed. -| Branch | Purpose | Version Format | Workflow | -|--------|---------|----------------|----------| -| `master` | Production releases | `X.Y.Z` | `production-release.yml` | -| `preview` | Pre-release testing | `X.Y.Z-preview.N` | `preview-release.yml` | -| `development` | Integration | `X.Y.Z-dev.N` | `build.yml` | -| `feature/*` | Development work | `X.Y.Z-dev.N` | `build.yml` | +## Architecture -Feature branches are created from `development`. PRs flow: `feature/*` -> `development` -> `preview` -> `master`. +### Solution Structure -### Branch Protection Rules +- **GeoMagGUI** (`GeoMagGUI/`) - WinForms application (.NET Framework 4.0 Client) +- **GeoMagSharp** (`GeoMagSharp/`) - Core calculation library (.NET Framework 4.0) +- **GeoMagSharp-UnitTests** (`GeoMagSharp-UnitTests/`) - MSTest unit tests (.NET Framework 4.5.2) -- **NEVER** commit directly to `master`, `preview`, or `development`. All changes via PRs only. -- **NEVER** force-push to protected branches. -- **NEVER** create or merge a PR without explicit user confirmation. Draft PRs are acceptable without confirmation. -- All development work happens on `feature/*` branches (only place direct commits are allowed). +### Key Source Files + +| File | Purpose | +|------|--------| +| `GeoMagSharp/GeoMag.cs` | Main calculation orchestrator | +| `GeoMagSharp/Calculator.cs` | Spherical harmonic calculations | +| `GeoMagSharp/ModelReader.cs` | Coefficient file parser (.COF, .DAT) | +| `GeoMagSharp/DataModel.cs` | Data structures and model classes | +| `GeoMagSharp/Units.cs` | Unit conversion utilities | +| `GeoMagGUI/frmMain.cs` | Main application window | +| `GeoMagGUI/frmPreferences.cs` | User preferences dialog | +| `GeoMagGUI/frmAddModel.cs` | Add model dialog | + +### Data Directories + +| Directory | Purpose | +|-----------|--------| +| `GeoMagGUI/coefficient/` | Magnetic model files (.COF, .DAT) and MagneticModels.json | +| `GeoMagGUI/assets/` | Icons and images | +| `GeoMagGUI/documentation/` | License and docs | + +### Supported Magnetic Models + +- **IGRF** (International Geomagnetic Reference Field) +- **WMM** (World Magnetic Model) +- **DGRF** (Definitive Geomagnetic Reference Field) +- **EMM** (Enhanced Magnetic Model) ## Development Workflow -**MANDATORY:** Every `feature/*` branch MUST use Ralph Loop (`/ralph-loop`) with rotating personas before any code is written. +### MANDATORY: Ralph Loop for ALL Feature Work + +**This is NON-NEGOTIABLE.** Every feature branch (`feature/*`) MUST use the Ralph Wiggum loop (`/ralph-loop`) with rotating personas. There are NO exceptions to this rule, regardless of how simple the task appears. -**Before writing ANY code on a feature branch:** -1. Ensure a GitHub issue exists -2. Create `docs/features//tasks.md` with task breakdown -3. Start a Ralph Loop with rotating persona pattern +**Before writing ANY code on a feature branch, you MUST:** -See `docs/prompts/` for full Ralph Loop documentation and persona definitions. +1. Verify a `docs/features//tasks.md` file exists +2. If it doesn't exist, create one before proceeding +3. Start a Ralph Loop with the rotating persona pattern + +**If you find yourself on a feature branch writing code without an active Ralph Loop, STOP and start one.** + +### Step 1: Create a GitHub Issue (if one doesn't exist) + +Every feature must have a corresponding GitHub issue before work begins. + +### Step 2: Create and Switch to a Feature Branch + +```bash +git checkout preview +git pull origin preview +git checkout -b feature/- +``` + +### Step 3: Create or Verify tasks.md (GATE - Required Before Any Code) + +Every feature MUST have a `docs/features//tasks.md` file. This file is the **single source of truth** for what work needs to be done. No code should be written until this file exists and has been reviewed. + +**tasks.md format:** +```markdown +# Feature: +Issue: # +Branch: feature/- + +## Tasks +- [ ] Task 1 description +- [ ] Task 2 description +- [ ] Task 3 description + +## Completion Criteria +- [ ] All tasks checked +- [ ] Build succeeds +- [ ] Tests pass +- [ ] 2 clean Ralph Loop cycles +``` + +### Step 4: Start a Ralph Loop (MANDATORY) + +Use the Ralph Wiggum loop with the rotating persona pattern defined in `docs/prompts/PERSONAS.md`. See the "Ralph Loop / Iterative Development" section below for the full pattern and completion criteria. + +## Key Patterns + +- WinForms with minimal code-behind +- Keep calculation logic in GeoMagSharp library +- UI code in GeoMagGUI stays lightweight +- JSON configuration via Newtonsoft.Json +- Coefficient files in fixed 80-character record format +- Use existing `ExtensionMethods` and `Helper` utilities + +## Naming Conventions + +| Element | Convention | Example | +|---------|------------|---------| +| Forms | `frm*` prefix | `frmMain`, `frmPreferences` | +| Types | PascalCase | `MagneticModelSet` | +| Methods | PascalCase | `SpotCalculation()` | +| Private fields | _camelCase | `_modelCollection` | +| Parameters | camelCase | `latitude` | ## Platform Constraints -- x86 architecture (set in project files) -- Windows-only (.NET Framework 4.0) -- Requires Visual Studio Developer Command Prompt for builds +- .NET Framework 4.0 required +- Windows-only (WinForms) +- x86 platform target +- Visual Studio 2019 or later recommended + +## Dependencies + +| Package | Purpose | +|---------|---------| +| Newtonsoft.Json | JSON serialization | +| System.Device | GPS location services (Windows) | + +## Ralph Loop / Iterative Development + +**MANDATORY — NO EXCEPTIONS:** ALL feature branch work (`feature/*`) MUST use Ralph loops with rotating personas. This applies regardless of feature size, complexity, or urgency. Skipping the Ralph Loop is never acceptable. + +### Pre-Flight Checklist (Before Starting Ralph Loop) -## Extended Documentation +Before starting any Ralph Loop, verify: -For detailed information, read these files on demand (not loaded every session): +- [ ] GitHub issue exists for this feature +- [ ] Feature branch created from `preview` +- [ ] `docs/features//tasks.md` exists with task breakdown +- [ ] PR created (draft is fine) to track work + +If any of these are missing, create them first. **Do NOT start coding without tasks.md.** + +### Required Persona Rotation + +``` +Iteration % 6 determines the current persona: + +[0] #5 IMPLEMENTER - Complete tasks, write code +[1] #9 REVIEWER - Review for bugs, code quality +[2] #7 TESTER - Verify functionality, add tests +[3] #3 UI_UX_DESIGNER - Review UI/UX, accessibility +[4] #10 SECURITY - Security review, input validation +[5] #2 PROJECT_MGR - Check requirements, update tasks +``` -- **Architecture & Conventions:** `@AGENTS.md` -- Solution structure, coding conventions, key classes -- **Ralph Loop:** `@docs/prompts/README.md` -- Iterative development workflow with rotating personas -- **Personas:** `@docs/prompts/PERSONAS.md` -- 11 development personas for Ralph Loop rotation -- **Feature Templates:** `@docs/prompts/templates/ROTATING_FEATURE.md` -- Ralph Loop prompt templates +### Each Iteration Must: +1. Identify the current persona based on iteration number +2. Follow that persona's mindset and output format from `docs/prompts/PERSONAS.md` +3. Commit with persona prefix: `[IMPLEMENTER]`, `[REVIEWER]`, etc. +4. Reference the feature's `tasks.md` file and mark tasks complete +5. Post a PR comment summarizing findings and changes + +### Completion Criteria +- All tasks in `docs/features/[feature]/tasks.md` marked complete +- Build succeeds with no errors +- Tests pass +- **2 clean cycles** (all 6 personas find no issues twice) + +### Why This Matters + +The Ralph Loop ensures: +- Multiple perspectives review every change (code quality, security, UX, testing) +- Issues are caught early through systematic rotation +- Progress is tracked via tasks.md +- An audit trail exists via PR comments from each persona +- Features meet a consistent quality bar before merging + +See `docs/prompts/README.md` and `docs/prompts/templates/ROTATING_FEATURE.md` for full documentation. 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-UnitTests/ModelReaderUnitTest.cs b/GeoMagSharp-UnitTests/ModelReaderUnitTest.cs index a46cc70..df432ff 100644 --- a/GeoMagSharp-UnitTests/ModelReaderUnitTest.cs +++ b/GeoMagSharp-UnitTests/ModelReaderUnitTest.cs @@ -301,6 +301,280 @@ public void Read_NonExistentFile_ThrowsFileNotFoundException() #endregion + #region DAT File Format Tests + + [TestMethod] + public void Read_ValidDATFile_ReturnsCorrectModel() + { + // Arrange - Use a DAT file if available in TestData + string filePath = Path.Combine(TestDataPath, "IGRF13.DAT"); + if (!File.Exists(filePath)) + { + // Try alternative DAT files + 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 = ModelReader.Read(filePath); + + // Assert + Assert.IsNotNull(modelSet); + Assert.IsTrue(modelSet.NumberOfModels > 0, "Should have at least one model"); + } + + #endregion + + #region Edge Case Tests + + [TestMethod] + public void Read_COFFileWithWhitespace_ParsesCorrectly() + { + // Arrange + string filePath = Path.Combine(TestDataPath, "WMM2025.COF"); + if (!File.Exists(filePath)) + Assert.Inconclusive("WMM2025.COF not found in TestData folder"); + + // Act - COF files have whitespace-separated fields + var modelSet = ModelReader.Read(filePath); + + // Assert - Should parse without errors + Assert.IsNotNull(modelSet); + Assert.IsTrue(modelSet.NumberOfModels >= 2, "Should have parsed M and S models"); + } + + [TestMethod] + public void Read_ModelSetDateRange_IsValid() + { + // Arrange + string filePath = Path.Combine(TestDataPath, "WMM2025.COF"); + if (!File.Exists(filePath)) + Assert.Inconclusive("WMM2025.COF not found in TestData folder"); + + // Act + var modelSet = ModelReader.Read(filePath); + + // Assert - MaxDate should be 5 years after MinDate for WMM models + Assert.IsTrue(modelSet.MaxDate > modelSet.MinDate, "MaxDate should be greater than MinDate"); + Assert.AreEqual(2030.0, modelSet.MaxDate, 0.01, "MaxDate should be MinDate + 5 for WMM models"); + } + + [TestMethod] + public void Read_ModelHasBothMAndSTypes() + { + // Arrange + string filePath = Path.Combine(TestDataPath, "WMM2025.COF"); + if (!File.Exists(filePath)) + Assert.Inconclusive("WMM2025.COF not found in TestData folder"); + + // Act + var modelSet = ModelReader.Read(filePath); + var models = modelSet.GetModels; + + // Assert - Should have both M (Main) and S (Secular variation) models + bool hasM = false; + bool hasS = false; + foreach (var model in models) + { + if (model.Type == "M") hasM = true; + if (model.Type == "S") hasS = true; + } + + Assert.IsTrue(hasM, "Should have M (Main) model"); + Assert.IsTrue(hasS, "Should have S (Secular variation) model"); + } + + [TestMethod] + public void Read_FileNameIsStored() + { + // Arrange + string filePath = Path.Combine(TestDataPath, "WMM2025.COF"); + if (!File.Exists(filePath)) + Assert.Inconclusive("WMM2025.COF not found in TestData folder"); + + // Act + var modelSet = ModelReader.Read(filePath); + + // Assert + Assert.IsNotNull(modelSet.FileNames); + Assert.IsTrue(modelSet.FileNames.Count > 0, "Should store filename"); + Assert.AreEqual("WMM2025.COF", modelSet.FileNames[0], "Should store correct filename"); + } + + #endregion + + #region IsFileLocked Tests + + [TestMethod] + public void IsFileLocked_UnlockedFile_ReturnsFalse() + { + // Arrange + string filePath = Path.Combine(TestDataPath, "WMM2025.COF"); + if (!File.Exists(filePath)) + Assert.Inconclusive("WMM2025.COF not found in TestData folder"); + + // Act + bool isLocked = ModelReader.IsFileLocked(filePath); + + // Assert + Assert.IsFalse(isLocked, "Unlocked file should return false"); + } + + [TestMethod] + public void IsFileLocked_EmptyPath_ReturnsFalse() + { + // Act + bool isLocked = ModelReader.IsFileLocked(string.Empty); + + // Assert + Assert.IsFalse(isLocked, "Empty path should return false"); + } + + #endregion + + #region Unsupported File Type Tests + + [TestMethod] + [ExpectedException(typeof(GeoMagExceptionModelNotLoaded))] + public void Read_UnsupportedExtension_ThrowsModelNotLoaded() + { + // Arrange - Create a temp file with unsupported extension + string tempFile = Path.Combine(Path.GetTempPath(), "test.xyz"); + try + { + File.WriteAllText(tempFile, "test content"); + + // Act + ModelReader.Read(tempFile); + } + finally + { + if (File.Exists(tempFile)) + File.Delete(tempFile); + } + } + + #endregion + + #region Coefficient Validation Tests + + [TestMethod] + public void Read_WMM2025_CoefficientsAreReasonable() + { + // Arrange + string filePath = Path.Combine(TestDataPath, "WMM2025.COF"); + if (!File.Exists(filePath)) + Assert.Inconclusive("WMM2025.COF not found in TestData folder"); + + // Act + var modelSet = ModelReader.Read(filePath); + var models = modelSet.GetModels; + + // Assert - WMM models have degree 12, which means n*(n+2) = 168 coefficients + // The main field (M) model should have coefficients in reasonable ranges + var mModel = models.Find(m => m.Type == "M"); + Assert.IsNotNull(mModel, "Should have M model"); + + // g(1,0) coefficient is around -29000 nT for recent epochs + // This is the first coefficient and should be a large negative number + Assert.IsTrue(mModel.SharmCoeff.Count >= 168, "Degree 12 model should have at least 168 coefficients"); + Assert.IsTrue(mModel.SharmCoeff[0] < -20000, "g(1,0) coefficient should be large negative"); + } + + [TestMethod] + public void Read_ModelDegree_CalculatesCorrectly() + { + // Arrange + string filePath = Path.Combine(TestDataPath, "WMM2025.COF"); + if (!File.Exists(filePath)) + Assert.Inconclusive("WMM2025.COF not found in TestData folder"); + + // Act + var modelSet = ModelReader.Read(filePath); + var models = modelSet.GetModels; + var mModel = models.Find(m => m.Type == "M"); + + // Assert - Standard WMM is degree 12 + Assert.AreEqual(12, mModel.Max_Degree, "WMM should be degree 12"); + } + + [TestMethod] + public void Read_WMMHR_HigherDegree() + { + // Arrange + string filePath = Path.Combine(TestDataPath, "WMMHR.COF"); + if (!File.Exists(filePath)) + Assert.Inconclusive("WMMHR.COF not found in TestData folder"); + + // Act + var modelSet = ModelReader.Read(filePath); + var models = modelSet.GetModels; + var mModel = models.Find(m => m.Type == "M"); + + // Assert - WMMHR is degree 18 + Assert.IsTrue(mModel.Max_Degree > 12, "WMMHR should have degree higher than 12"); + } + + #endregion + + #region Spherical Harmonic Validation Tests + + [TestMethod] + public void Read_WMM2025_DegreeAndOrderAreValid() + { + // Arrange + string filePath = Path.Combine(TestDataPath, "WMM2025.COF"); + if (!File.Exists(filePath)) + Assert.Inconclusive("WMM2025.COF not found in TestData folder"); + + // Act - This should parse without throwing validation errors + var modelSet = ModelReader.Read(filePath); + + // Assert - If we got here, all degree/order values were valid + Assert.IsNotNull(modelSet); + Assert.IsTrue(modelSet.NumberOfModels >= 2, "Should have M and S models"); + } + + [TestMethod] + public void Read_WMMHR_DegreeAndOrderAreValid() + { + // Arrange - WMMHR has higher degree coefficients + string filePath = Path.Combine(TestDataPath, "WMMHR.COF"); + if (!File.Exists(filePath)) + Assert.Inconclusive("WMMHR.COF not found in TestData folder"); + + // Act - This should parse without throwing validation errors + var modelSet = ModelReader.Read(filePath); + + // Assert - If we got here, all degree/order values were valid + Assert.IsNotNull(modelSet); + } + + [TestMethod] + public void Read_AllWMMFiles_HaveValidSphericalHarmonicCoefficients() + { + // Arrange - Get all WMM COF files in TestData + string[] wmmFiles = Directory.GetFiles(TestDataPath, "WMM*.COF"); + if (wmmFiles.Length == 0) + Assert.Inconclusive("No WMM COF files found in TestData folder"); + + // Act & Assert - Each file should parse without validation errors + foreach (var filePath in wmmFiles) + { + var modelSet = ModelReader.Read(filePath); + Assert.IsNotNull(modelSet, $"Failed to parse {Path.GetFileName(filePath)}"); + + // Verify M model exists and has reasonable coefficients + var mModel = modelSet.GetModels.Find(m => m.Type == "M"); + Assert.IsNotNull(mModel, $"{Path.GetFileName(filePath)} should have M model"); + Assert.IsTrue(mModel.Max_Degree >= 12, $"{Path.GetFileName(filePath)} should have degree >= 12"); + } + } + + #endregion + #region CheckStringForModel Extension Method Tests [TestMethod] diff --git a/GeoMagSharp/Calculator.cs b/GeoMagSharp/Calculator.cs index 8b3aeb9..516de98 100644 --- a/GeoMagSharp/Calculator.cs +++ b/GeoMagSharp/Calculator.cs @@ -1,20 +1,24 @@ /**************************************************************************** - * File: GeoMagBGGM.cs - * Description: routines to handle bggm coefficients file and calculate - * field values - * Akowlegements: Ported from the C++ model code created by the British Geological Survey - * Website: http://www.geomag.bgs.ac.uk/data_service/directionaldrilling/bggm.html - * Warnings: This code can be used with the IGRF, WMM, or BGGM coeficent file. The BGGM - * coeficient file is Commerically avalable from the British Geological Survey - * and is not distributed with this project. Please contcact the BGS for more information. - * + * File: Calculator.cs + * Description: Routines to calculate magnetic field values from + * spherical harmonic coefficient files + * Reference: Based on the NOAA World Magnetic Model (WMM) algorithm + * https://www.ncei.noaa.gov/products/world-magnetic-model + * Compatibility: This code can be used with IGRF, WMM, or other standard + * coefficient files in COF or DAT format. * Current version: 2.21 - * ****************************************************************************/ + * Author: Christopher Strecker + * Website: https://github.com/StreckerCM/GeoMagSharpGUI + ****************************************************************************/ using System; namespace GeoMagSharp { + /// + /// Static calculator for magnetic field values using spherical harmonic coefficients. + /// Based on the NOAA World Magnetic Model (WMM) algorithm. + /// public static class Calculator { diff --git a/GeoMagSharp/Enums/GeoMagEnums.cs b/GeoMagSharp/Enums/GeoMagEnums.cs index abb9ede..a57a8ba 100644 --- a/GeoMagSharp/Enums/GeoMagEnums.cs +++ b/GeoMagSharp/Enums/GeoMagEnums.cs @@ -29,7 +29,7 @@ public enum CoordinateSystem public enum Algorithm { /// - /// British Geological Survey algorithm + /// Default spherical harmonic algorithm /// BGS = 1, diff --git a/GeoMagSharp/ExtensionMethods.cs b/GeoMagSharp/ExtensionMethods.cs index 4566deb..92bf64c 100644 --- a/GeoMagSharp/ExtensionMethods.cs +++ b/GeoMagSharp/ExtensionMethods.cs @@ -11,6 +11,9 @@ namespace GeoMagSharp { + /// + /// Extension methods for date/time conversion, angle conversion, and model identification. + /// public static class ExtensionMethods { /// @@ -167,11 +170,21 @@ public static DateTime ToDateTime(this double decDate) return new DateTime(yearInt, monthInt, dayInt); } + /// + /// Determines whether the year of the specified date falls within the valid range (1900 to DateTime.MaxValue.Year). + /// + /// The date to validate. + /// true if the year is within the valid range; otherwise, false. public static bool IsValidYear(this DateTime date) { return (1900 <= date.Year && date.Year <= DateTime.MaxValue.Year); } + /// + /// Determines whether the specified decimal year falls within the valid range (1900 to max DateTime as decimal). + /// + /// The decimal year to validate. + /// true if the decimal year is within the valid range; otherwise, false. public static bool IsValidYear(this double decDate) { return (1900D <= decDate && decDate <= DateTime.MaxValue.ToDecimal()); diff --git a/GeoMagSharp/GeoMag.cs b/GeoMagSharp/GeoMag.cs index f6131e2..cee07ae 100644 --- a/GeoMagSharp/GeoMag.cs +++ b/GeoMagSharp/GeoMag.cs @@ -1,231 +1,475 @@ -/**************************************************************************** - * File: GeoMag.cs - * Description: Routines to provide an interface to the calculation methods - * Author: Christopher Strecker - * Website: https://github.com/StreckerCM/GeoMagSharpGUI - * Warnings: - * Current version: - * ****************************************************************************/ - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.IO; - - -namespace GeoMagSharp -{ - public class GeoMag - { - public List ResultsOfCalculation; - - private MagneticModelSet _Models; - - private CalculationOptions _CalculationOptions; - - public GeoMag() - { - _Models = null; - } - - public void LoadModel(string modelFile) - { - _Models = null; - - if (string.IsNullOrEmpty(modelFile)) - throw new GeoMagExceptionFileNotFound("Error coefficient file name not specified"); - - _Models = ModelReader.Read(modelFile); - - } - - public void LoadModel(MagneticModelSet modelSet) - { - _Models = null; - - if (modelSet == null) - throw new GeoMagExceptionFileNotFound("Error coefficient file name not specified"); - - _Models = modelSet; - - } - - public void LoadModel(string modelFile, string svFile) - { - _Models = null; - - if (string.IsNullOrEmpty(modelFile)) - throw new GeoMagExceptionFileNotFound("Error coefficient file name not specified"); - - _Models = ModelReader.Read(modelFile, svFile); - - } - - public void MagneticCalculations(CalculationOptions inCalculationOptions) - { - _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; - - double dateIdx = 0; - - ResultsOfCalculation = new List(); - - _CalculationOptions = new CalculationOptions(inCalculationOptions); - - while (dateIdx <= timespan.Days) - { - - var internalSH = new Coefficients(); - - var externalSH = new Coefficients(); - - DateTime intervalDate = _CalculationOptions.StartDate.AddDays(dateIdx); - - _Models.GetIntExt(intervalDate.ToDecimal(), out internalSH, out externalSH); - - var magCalcDate = Calculator.SpotCalculation(_CalculationOptions, intervalDate, _Models, internalSH, externalSH, _Models.EarthRadius); - - if (magCalcDate != null) ResultsOfCalculation.Add(magCalcDate); - - dateIdx = ((dateIdx < timespan.Days) && ((dateIdx + dayInc) > timespan.Days)) - ? timespan.Days - : dateIdx + dayInc; - - } - - } - - - public void SaveResults(string fileName, bool loadAfterSave = false) - { - if (ResultsOfCalculation == null) - throw new GeoMagExceptionModelNotLoaded("Error: No calculation results to save"); - - 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: {1}", - System.IO.Path.GetFileName(fileName), e.ToString())); - } - - } - - Int32 lineCount = 0; - - //Int32 lineNumColIdx = -1; - - var tabStrRight = new StringBuilder(); - - //Build header - - - - 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}"; - - //Build Column Header - - 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++; - - //Build result rows - - foreach(var result in ResultsOfCalculation) - { - //Date - 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); - - // Write the stream contents to a text fle - using (StreamWriter outFile = File.AppendText(fileName)) - { - outFile.Write(tabStrRight.ToString()); - } - - - - - } - - } -} +/**************************************************************************** + * File: GeoMag.cs + * Description: Routines to provide an interface to the calculation methods + * Author: Christopher Strecker + * Website: https://github.com/StreckerCM/GeoMagSharpGUI + ****************************************************************************/ + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + + +namespace GeoMagSharp +{ + /// + /// Provides an interface to magnetic field calculation methods. + /// Handles model loading, magnetic field computation, and result export. + /// + public class GeoMag + { + /// + /// The results of the most recent magnetic field calculation. + /// + public List ResultsOfCalculation; + + private MagneticModelSet _Models; + + private CalculationOptions _CalculationOptions; + + /// + /// Initializes a new instance of the class. + /// + public GeoMag() + { + _Models = null; + } + + /// + /// Loads a magnetic model from a coefficient file. + /// + /// Path to the coefficient file (.COF or .DAT format) + /// Thrown when is null or empty. + public void LoadModel(string modelFile) + { + _Models = null; + + if (string.IsNullOrEmpty(modelFile)) + throw new GeoMagExceptionFileNotFound("Error coefficient file name not specified"); + + _Models = ModelReader.Read(modelFile); + + } + + /// + /// Loads a magnetic model from a pre-built . + /// + /// The magnetic model set to use for calculations. + /// Thrown when is null. + public void LoadModel(MagneticModelSet modelSet) + { + _Models = null; + + if (modelSet == null) + throw new GeoMagExceptionFileNotFound("Error coefficient file name not specified"); + + _Models = modelSet; + + } + + /// + /// Loads a magnetic model from separate main field and secular variation coefficient files. + /// + /// Path to the main field coefficient file. + /// Path to the secular variation coefficient file. + /// Thrown when is null or empty. + public void LoadModel(string modelFile, string svFile) + { + _Models = null; + + if (string.IsNullOrEmpty(modelFile)) + throw new GeoMagExceptionFileNotFound("Error coefficient file name not specified"); + + _Models = ModelReader.Read(modelFile, svFile); + + } + + /// + /// Performs magnetic field calculations over the specified date range and location. + /// Results are stored in . + /// + /// The calculation parameters including location, dates, and elevation. + /// Thrown when no model has been loaded. + /// Thrown when the start or end date is outside the model's valid range. + public void MagneticCalculations(CalculationOptions inCalculationOptions) + { + _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; + + double dateIdx = 0; + + ResultsOfCalculation = new List(); + + _CalculationOptions = new CalculationOptions(inCalculationOptions); + + while (dateIdx <= timespan.Days) + { + + var internalSH = new Coefficients(); + + var externalSH = new Coefficients(); + + DateTime intervalDate = _CalculationOptions.StartDate.AddDays(dateIdx); + + _Models.GetIntExt(intervalDate.ToDecimal(), out internalSH, out externalSH); + + var magCalcDate = Calculator.SpotCalculation(_CalculationOptions, intervalDate, _Models, internalSH, externalSH, _Models.EarthRadius); + + if (magCalcDate != null) ResultsOfCalculation.Add(magCalcDate); + + dateIdx = ((dateIdx < timespan.Days) && ((dateIdx + dayInc) > timespan.Days)) + ? timespan.Days + : dateIdx + dayInc; + + } + + } + + + /// + /// Saves the calculation results to a tab-separated text file. + /// + /// The output file path. + /// Reserved for future use. + /// Thrown when no calculation results are available. + /// Thrown when the file is locked or cannot be deleted. + public void SaveResults(string fileName, bool loadAfterSave = false) + { + if (ResultsOfCalculation == null) + throw new GeoMagExceptionModelNotLoaded("Error: No calculation results to save"); + + 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: {1}", + System.IO.Path.GetFileName(fileName), e.ToString())); + } + + } + + Int32 lineCount = 0; + + //Int32 lineNumColIdx = -1; + + var tabStrRight = new StringBuilder(); + + //Build header + + + + 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}"; + + //Build Column Header + + 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++; + + //Build result rows + + foreach(var result in ResultsOfCalculation) + { + //Date + 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); + + // Write the stream contents to a text fle + using (StreamWriter outFile = File.AppendText(fileName)) + { + 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/GeoMagException.cs b/GeoMagSharp/GeoMagException.cs index d9d12dd..2d43f4d 100644 --- a/GeoMagSharp/GeoMagException.cs +++ b/GeoMagSharp/GeoMagException.cs @@ -48,9 +48,9 @@ protected GeoMagException( } } - /// - /// Thrown when file is not found - /// + /// + /// Thrown when a magnetic model has not been loaded before performing calculations. + /// [Serializable] public class GeoMagExceptionModelNotLoaded : GeoMagException { @@ -75,9 +75,9 @@ public GeoMagExceptionModelNotLoaded(SerializationInfo info, StreamingContext co } - /// - /// Thrown when file is not found - /// + /// + /// Thrown when a coefficient file cannot be found at the specified path. + /// [Serializable] public class GeoMagExceptionFileNotFound : GeoMagException { @@ -101,9 +101,9 @@ public GeoMagExceptionFileNotFound(SerializationInfo info, StreamingContext cont } } - /// - /// Thrown when file has an invalid charater in a line - /// + /// + /// Thrown when a coefficient file contains an invalid character in a data line. + /// [Serializable] public class GeoMagExceptionBadCharacter : GeoMagException { @@ -128,9 +128,9 @@ public GeoMagExceptionBadCharacter(SerializationInfo info, StreamingContext cont } - /// - /// Thrown when file has an invalid number of coefficients - /// + /// + /// Thrown when a coefficient file contains an unexpected number of coefficients. + /// [Serializable] public class GeoMagExceptionBadNumberOfCoefficients : GeoMagException { @@ -154,9 +154,9 @@ public GeoMagExceptionBadNumberOfCoefficients(SerializationInfo info, StreamingC } } - /// - /// Thrown when file has an invalid number of coefficients - /// + /// + /// Thrown when a coefficient file cannot be opened or read. + /// [Serializable] public class GeoMagExceptionOpenError : GeoMagException { @@ -180,9 +180,9 @@ public GeoMagExceptionOpenError(SerializationInfo info, StreamingContext context } } - /// - /// Thrown when file has an invalid number of coefficients - /// + /// + /// Thrown when a calculation date or parameter is outside the valid range. + /// [Serializable] public class GeoMagExceptionOutOfRange : GeoMagException { @@ -206,9 +206,9 @@ public GeoMagExceptionOutOfRange(SerializationInfo info, StreamingContext contex } } - /// - /// Thrown when file has an invalid number of coefficients - /// + /// + /// Thrown when a calculation or model loading operation exceeds available memory. + /// [Serializable] public class GeoMagExceptionOutOfMemory : GeoMagException { 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 8fdb40d..33cc50d 100644 --- a/GeoMagSharp/ModelReader.cs +++ b/GeoMagSharp/ModelReader.cs @@ -1,20 +1,23 @@ /**************************************************************************** - * File: FileReader.cs - * Description: Routines read a given model file into the model structure - * to be used for calculation - * Author: Christopher Strecker - * Website: https://github.com/StreckerCM/GeoMagSharpGUI - * Warnings: - * Current version: - * ****************************************************************************/ + * File: ModelReader.cs + * Description: Routines to read magnetic model coefficient files into + * the model structure for calculation + * Author: Christopher Strecker + * Website: https://github.com/StreckerCM/GeoMagSharpGUI + ****************************************************************************/ using System; using System.Globalization; using System.Linq; using System.IO; +using System.Threading; +using System.Threading.Tasks; namespace GeoMagSharp { + /// + /// Reads and parses magnetic model coefficient files (COF/DAT) into model structures. + /// public static class ModelReader { /// @@ -97,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" @@ -229,58 +297,8 @@ private static MagneticModelSet COFreader(string modelFile) } else if (mModelIdx > -1) { - if(outModels.Type.Equals(knownModels.EMM)) - { - #region EMM File Line Reader - // EMM format requires at least 6 columns: degree, order, g1, h1, g2, h2 - ValidateArrayLength(lineParase, 6, lineNumber); - - Int32 lineDegree = ParseInt(lineParase[0], lineNumber, "degree"); - Int32 lineOrder = ParseInt(lineParase[1], lineNumber, "order"); - - // g1 coefficient - tempDbl = ParseDouble(lineParase[2], lineNumber, "g1 coefficient"); - outModels.AddCoefficients(mModelIdx, tempDbl); - - // h1 coefficient (only if order > 0) - tempDbl = ParseDouble(lineParase[3], lineNumber, "h1 coefficient"); - if (lineOrder > 0) outModels.AddCoefficients(mModelIdx, tempDbl); - - // g2 coefficient - tempDbl = ParseDouble(lineParase[4], lineNumber, "g2 coefficient"); - outModels.AddCoefficients(eModelIdx, tempDbl); - - // h2 coefficient (only if order > 0) - tempDbl = ParseDouble(lineParase[5], lineNumber, "h2 coefficient"); - if (lineOrder > 0) outModels.AddCoefficients(eModelIdx, tempDbl); - #endregion - } - else - { - #region Standard COF File Line Reader - // Standard COF format requires at least 6 columns: degree, order, g1, h1, g2, h2 - ValidateArrayLength(lineParase, 6, lineNumber); - - Int32 lineDegree = ParseInt(lineParase[0], lineNumber, "degree"); - Int32 lineOrder = ParseInt(lineParase[1], lineNumber, "order"); - - // g1 coefficient - tempDbl = ParseDouble(lineParase[2], lineNumber, "g1 coefficient"); - outModels.AddCoefficients(mModelIdx, tempDbl); - - // h1 coefficient (only if order > 0) - tempDbl = ParseDouble(lineParase[3], lineNumber, "h1 coefficient"); - if (lineOrder > 0) outModels.AddCoefficients(mModelIdx, tempDbl); - - // g2 coefficient - tempDbl = ParseDouble(lineParase[4], lineNumber, "g2 coefficient"); - outModels.AddCoefficients(eModelIdx, tempDbl); - - // h2 coefficient (only if order > 0) - tempDbl = ParseDouble(lineParase[5], lineNumber, "h2 coefficient"); - if (lineOrder > 0) outModels.AddCoefficients(eModelIdx, tempDbl); - #endregion - } + // Parse coefficient line - both EMM and standard COF use identical format + ParseCOFCoefficients(lineParase, lineNumber, outModels, mModelIdx, eModelIdx); } } } @@ -404,6 +422,51 @@ public static bool IsFileLocked(string filePath) return false; } + /// + /// Parses a COF coefficient data line and adds coefficients to the model. + /// This method handles both EMM and standard COF formats, which share identical parsing logic. + /// + /// The whitespace-split fields of the data line + /// Line number for error reporting + /// The MagneticModelSet to add coefficients to + /// Index of the main (M) model + /// Index of the secular variation (S) model + private static void ParseCOFCoefficients(string[] lineParase, int lineNumber, + MagneticModelSet outModels, int mModelIdx, int eModelIdx) + { + // Both EMM and standard COF format require at least 6 columns: degree, order, g1, h1, g2, h2 + ValidateArrayLength(lineParase, 6, lineNumber); + + // Parse degree and order for validation and coefficient placement logic + int lineDegree = ParseInt(lineParase[0], lineNumber, "degree"); + int lineOrder = ParseInt(lineParase[1], lineNumber, "order"); + + // Validate spherical harmonic constraints: degree >= 1, order >= 0, order <= degree + if (lineDegree < 1) + throw new GeoMagExceptionBadCharacter(string.Format( + "Error: Invalid degree {0} at line {1}. Degree must be >= 1", lineDegree, lineNumber)); + if (lineOrder < 0 || lineOrder > lineDegree) + throw new GeoMagExceptionBadCharacter(string.Format( + "Error: Invalid order {0} at line {1}. Order must be between 0 and degree ({2})", + lineOrder, lineNumber, lineDegree)); + + // g1 coefficient (main field) + double g1 = ParseDouble(lineParase[2], lineNumber, "g1 coefficient"); + outModels.AddCoefficients(mModelIdx, g1); + + // h1 coefficient (main field, only if order > 0) + double h1 = ParseDouble(lineParase[3], lineNumber, "h1 coefficient"); + if (lineOrder > 0) outModels.AddCoefficients(mModelIdx, h1); + + // g2 coefficient (secular variation) + double g2 = ParseDouble(lineParase[4], lineNumber, "g2 coefficient"); + outModels.AddCoefficients(eModelIdx, g2); + + // h2 coefficient (secular variation, only if order > 0) + double h2 = ParseDouble(lineParase[5], lineNumber, "h2 coefficient"); + if (lineOrder > 0) outModels.AddCoefficients(eModelIdx, h2); + } + /// /// Safely parses a double value with validation. /// diff --git a/GeoMagSharp/Models/Configuration/CalculationOptions.cs b/GeoMagSharp/Models/Configuration/CalculationOptions.cs index f28622c..30c254d 100644 --- a/GeoMagSharp/Models/Configuration/CalculationOptions.cs +++ b/GeoMagSharp/Models/Configuration/CalculationOptions.cs @@ -17,6 +17,9 @@ public class CalculationOptions { #region Constructors + /// + /// Initializes a new instance with default values (origin point, current date, default algorithm). + /// public CalculationOptions() { Latitude = 0; @@ -32,6 +35,10 @@ public CalculationOptions() ElevationIsAltitude = true; } + /// + /// Initializes a new instance by copying values from another . + /// + /// The source options to copy from. public CalculationOptions(CalculationOptions other) { Latitude = other.Latitude; @@ -49,16 +56,35 @@ public CalculationOptions(CalculationOptions other) #endregion + /// Geographic latitude in decimal degrees (-90 to +90). public double Latitude { get; set; } + + /// Geographic longitude in decimal degrees (-180 to +180). public double Longitude { get; set; } + + /// Start date for the calculation range. public DateTime StartDate { get; set; } + + /// End date for the calculation range. If equal to , defaults to . public DateTime EndDate { get; set; } + + /// Step interval in days between calculations in a date range. public double StepInterval { get; set; } + + /// Whether to calculate secular variation (annual rate of change). public bool SecularVariation { get; set; } + + /// The calculation algorithm to use. public Algorithm CalculationMethod { get; set; } #region Getters & Setters + /// + /// Sets the elevation value, unit, and type (altitude or depth). + /// + /// The elevation value. + /// The unit of measurement. + /// true for altitude above sea level; false for depth below sea level. public void SetElevation(double value, Distance.Unit unit, bool isAltitude = true) { ElevationValue = value; @@ -66,6 +92,9 @@ public void SetElevation(double value, Distance.Unit unit, bool isAltitude = tru ElevationIsAltitude = isAltitude; } + /// + /// Gets the elevation converted to depth in meters. Positive for depth, negative for altitude. + /// public double DepthInM { get @@ -93,6 +122,9 @@ public double DepthInM } } + /// + /// Gets the elevation converted to altitude in kilometers. Positive for altitude, negative for depth. + /// public double AltitudeInKm { get @@ -120,6 +152,9 @@ public double AltitudeInKm } } + /// + /// Gets the elevation as a list of [label, value, unit abbreviation] for display purposes. + /// public List GetElevation { get @@ -133,6 +168,9 @@ public List GetElevation } } + /// + /// Gets the geographic co-latitude (90 - latitude) in degrees. + /// public double CoLatitude { get diff --git a/GeoMagSharp/Models/Configuration/Preferences.cs b/GeoMagSharp/Models/Configuration/Preferences.cs index af9b0b7..1db5a06 100644 --- a/GeoMagSharp/Models/Configuration/Preferences.cs +++ b/GeoMagSharp/Models/Configuration/Preferences.cs @@ -25,36 +25,42 @@ public class Preferences #region Getters & Setters + /// Whether to display coordinates in decimal degrees (true) or degrees/minutes/seconds (false). public bool UseDecimalDegrees { get { return _UseDecimalDegrees; } set { _UseDecimalDegrees = value; } } + /// Whether to use altitude (true) or depth (false) for elevation input. public bool UseAltitude { get { return _UseAltitude; } set { _UseAltitude = value; } } + /// The unit for magnetic field intensity display (e.g., "nT"). public string FieldUnit { get { return _FieldUnit; } set { _FieldUnit = value; } } + /// The unit for altitude/depth display (e.g., "ft", "m"). public string AltitudeUnits { get { return _AltitudeUnits; } set { _AltitudeUnits = value; } } + /// Default latitude hemisphere ("N" or "S"). public string LatitudeHemisphere { get { return _LatitudeHemisphere; } set { _LatitudeHemisphere = value; } } + /// Default longitude hemisphere ("E" or "W"). public string LongitudeHemisphere { get { return _LongitudeHemisphere; } @@ -65,10 +71,17 @@ public string LongitudeHemisphere #region Constructors + /// + /// Initializes a new instance with default preferences. + /// public Preferences() { } + /// + /// Initializes a new instance by copying values from another . + /// + /// The source preferences to copy from. public Preferences(Preferences other) { UseDecimalDegrees = other.UseDecimalDegrees; @@ -80,6 +93,11 @@ public Preferences(Preferences other) #region Object Serializers + /// + /// Serializes the preferences to a JSON file. + /// + /// The file path to save to. + /// true if the save was successful; otherwise, false. public bool Save(string filename) { if (string.IsNullOrEmpty(filename)) return false; @@ -113,6 +131,11 @@ public bool Save(string filename) return wasSucessful; } + /// + /// Deserializes preferences from a JSON file. + /// + /// The file path to load from. + /// The loaded , or a new default instance if the file is missing or invalid. public static Preferences Load(string filename) { if (string.IsNullOrEmpty(filename)) return new Preferences(); diff --git a/GeoMagSharp/Models/Coordinates/Latitude.cs b/GeoMagSharp/Models/Coordinates/Latitude.cs index 7aa6117..beb816b 100644 --- a/GeoMagSharp/Models/Coordinates/Latitude.cs +++ b/GeoMagSharp/Models/Coordinates/Latitude.cs @@ -19,21 +19,39 @@ public class Latitude : Coordinate #region Constructors + /// + /// Initializes a new instance at the equator (0 degrees). + /// public Latitude() { Decimal = 0; } + /// + /// Initializes a new instance with the specified decimal degree value. + /// + /// Latitude in decimal degrees (-90 to +90). public Latitude(double inDecimal) { Decimal = inDecimal; } + /// + /// Initializes a new instance by copying another . + /// + /// The source latitude to copy. public Latitude(Latitude other) { Decimal = other.Decimal; } + /// + /// Initializes a new instance from degrees, minutes, seconds, and hemisphere direction. + /// + /// Degrees component. + /// Minutes component. + /// Seconds component. + /// Hemisphere direction ("N" or "S"). public Latitude(double inDegrees, double inMinutes, double inSeconds, string inDirection) { bool isPositive = inDirection.Equals("N", StringComparison.OrdinalIgnoreCase); diff --git a/GeoMagSharp/Models/Coordinates/Longitude.cs b/GeoMagSharp/Models/Coordinates/Longitude.cs index 0feb0b9..2e28de3 100644 --- a/GeoMagSharp/Models/Coordinates/Longitude.cs +++ b/GeoMagSharp/Models/Coordinates/Longitude.cs @@ -19,21 +19,39 @@ public class Longitude : Coordinate #region Constructors + /// + /// Initializes a new instance at the prime meridian (0 degrees). + /// public Longitude() { Decimal = 0; } + /// + /// Initializes a new instance with the specified decimal degree value. + /// + /// Longitude in decimal degrees (-180 to +180). public Longitude(double inDecimal) { Decimal = inDecimal; } - public Longitude(Latitude other) + /// + /// Initializes a new instance by copying another . + /// + /// The source longitude to copy. + public Longitude(Longitude other) { Decimal = other.Decimal; } + /// + /// Initializes a new instance from degrees, minutes, seconds, and hemisphere direction. + /// + /// Degrees component. + /// Minutes component. + /// Seconds component. + /// Hemisphere direction ("E" or "W"). public Longitude(double inDegrees, double inMinutes, double inSeconds, string inDirection) { bool isPositive = inDirection.Equals("E", StringComparison.OrdinalIgnoreCase); diff --git a/GeoMagSharp/Models/Magnetic/Coefficients.cs b/GeoMagSharp/Models/Magnetic/Coefficients.cs index c8223f2..545f3b6 100644 --- a/GeoMagSharp/Models/Magnetic/Coefficients.cs +++ b/GeoMagSharp/Models/Magnetic/Coefficients.cs @@ -18,12 +18,19 @@ public class Coefficients { #region Constructors + /// + /// Initializes a new instance with an empty coefficient list and zero max degree. + /// public Coefficients() { coeffs = new List(); MaxDegree = 0; } + /// + /// Initializes a new instance by copying coefficients from another . + /// + /// The source coefficients to copy. public Coefficients(Coefficients other) { coeffs = new List(); @@ -34,7 +41,10 @@ public Coefficients(Coefficients other) #endregion + /// The list of spherical harmonic coefficient values. public List coeffs { get; set; } + + /// The maximum spherical harmonic degree represented by these coefficients. public Int32 MaxDegree { get; set; } } } diff --git a/GeoMagSharp/Models/Magnetic/MagneticModel.cs b/GeoMagSharp/Models/Magnetic/MagneticModel.cs index 03381b0..1792e2b 100644 --- a/GeoMagSharp/Models/Magnetic/MagneticModel.cs +++ b/GeoMagSharp/Models/Magnetic/MagneticModel.cs @@ -18,6 +18,9 @@ public class MagneticModel { #region Constructors + /// + /// Initializes a new instance with default values. + /// public MagneticModel() { Type = string.Empty; @@ -26,13 +29,17 @@ public MagneticModel() SharmCoeff = new List(); } + /// + /// Initializes a new instance by copying values from another . + /// + /// The source model to copy. public MagneticModel(MagneticModel other) { Type = other.Type; Year = other.Year; SharmCoeff = new List(); - if (other.SharmCoeff.Any()) SharmCoeff.AddRange(SharmCoeff); + if (other.SharmCoeff.Any()) SharmCoeff.AddRange(other.SharmCoeff); } #endregion diff --git a/GeoMagSharp/Models/Magnetic/MagneticModelCollection.cs b/GeoMagSharp/Models/Magnetic/MagneticModelCollection.cs index b47bb5b..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 { @@ -19,11 +21,15 @@ namespace GeoMagSharp /// public class MagneticModelCollection : IEnumerable { + /// The internal list of magnetic model sets. [JsonProperty(TypeNameHandling = TypeNameHandling.None)] public List TList { get; private set; } #region Constructors + /// + /// Initializes a new instance with an empty model list. + /// public MagneticModelCollection() { TList = new List(); @@ -33,21 +39,39 @@ public MagneticModelCollection() #region Base Class Methods + /// + /// Adds a model set to the collection. + /// + /// The model set to add. public void Add(MagneticModelSet item) { TList.Add(item); } + /// + /// Adds a range of model sets to the collection. + /// + /// The model sets to add. public void AddRange(IEnumerable collection) { TList.AddRange(collection); } + /// + /// Searches for a model set that matches the specified predicate. + /// + /// The predicate to match against. + /// The first matching model set, or null if not found. public MagneticModelSet Find(Predicate match) { return TList.Find(match); } + /// + /// Searches for all model sets that match the specified predicate. + /// + /// The predicate to match against. + /// A list of all matching model sets. public List FindAll(Predicate match) { return TList.FindAll(match); @@ -97,6 +121,10 @@ public bool AddOrReplace(MagneticModelSet item) return false; } + /// + /// Returns an enumerator that iterates through the model sets. + /// + /// An enumerator for the collection. public IEnumerator GetEnumerator() { return TList.GetEnumerator(); @@ -111,6 +139,11 @@ System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() #region Object Serializers + /// + /// Serializes the collection to a JSON file. + /// + /// The file path to save to. + /// true if the save was successful; otherwise, false. public bool Save(string filename) { if (string.IsNullOrEmpty(filename)) return false; @@ -144,6 +177,11 @@ public bool Save(string filename) return wasSucessful; } + /// + /// Deserializes a collection from a JSON file. + /// + /// The file path to load from. + /// The loaded collection, or a new empty collection if the file is missing or invalid. public static MagneticModelCollection Load(string filename) { if (string.IsNullOrEmpty(filename)) return new MagneticModelCollection(); @@ -178,10 +216,103 @@ 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 + /// + /// Gets a DataTable representation of all models for UI data binding. + /// Columns: ID, ModelName, FileNames, DateMin, DateMax, NumberOfModels, Type. + /// public DataTable GetDataTable { get diff --git a/GeoMagSharp/Models/Magnetic/MagneticModelSet.cs b/GeoMagSharp/Models/Magnetic/MagneticModelSet.cs index 8f0131f..ffac2e4 100644 --- a/GeoMagSharp/Models/Magnetic/MagneticModelSet.cs +++ b/GeoMagSharp/Models/Magnetic/MagneticModelSet.cs @@ -20,11 +20,18 @@ public class MagneticModelSet { #region Constructors + /// + /// Initializes a new instance with an empty model list. + /// public MagneticModelSet() { Models = new List(); } + /// + /// Initializes a new instance by copying values from another . + /// + /// The source model set to copy. public MagneticModelSet(MagneticModelSet other) { ID = other.ID; @@ -44,6 +51,10 @@ public MagneticModelSet(MagneticModelSet other) #region Public Methods + /// + /// Adds a magnetic model and updates the date range to include the model's epoch. + /// + /// The magnetic model to add. public void AddModel(MagneticModel newModel) { if (newModel == null) return; @@ -57,6 +68,11 @@ public void AddModel(MagneticModel newModel) MaxDate = newModel.Year; } + /// + /// Adds a spherical harmonic coefficient to the model at the specified index. + /// + /// The index of the model to add the coefficient to. + /// The coefficient value. public void AddCoefficients(Int32 modelIdx, double coeff) { if (modelIdx.Equals(-1)) return; @@ -290,6 +306,7 @@ subtract what is needed at start and end of time span #region getters & setters + /// Unique identifier for this model set. public Guid ID { get @@ -304,6 +321,7 @@ public Guid ID } } + /// Display name of the model (e.g., "WMM2025"). public string Name { get @@ -318,6 +336,7 @@ public string Name } } + /// List of coefficient file paths used to build this model set. public List FileNames { get @@ -332,6 +351,7 @@ public List FileNames } } + /// The known model type (WMM, IGRF, EMM, etc.). public knownModels Type { get @@ -346,6 +366,7 @@ public knownModels Type } } + /// Minimum valid date (decimal year) for this model set. Only accepts values earlier than the current minimum. public double MinDate { get @@ -362,6 +383,7 @@ public double MinDate } } + /// Maximum valid date (decimal year) for this model set. Only accepts values later than the current maximum. public double MaxDate { get @@ -378,6 +400,7 @@ public double MaxDate } } + /// Earth's radius in kilometers used by this model. public double EarthRadius { get @@ -392,6 +415,7 @@ public double EarthRadius } } + /// Gets a copy of the internal models list. [JsonIgnore] public List GetModels { @@ -401,6 +425,7 @@ public List GetModels } } + /// Gets the number of models in the set, or -1 if models are null. [JsonIgnore] public Int32 NumberOfModels { 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/GeoMagSharp/Models/Results/GeoMagVector.cs b/GeoMagSharp/Models/Results/GeoMagVector.cs index ff46387..61a0edb 100644 --- a/GeoMagSharp/Models/Results/GeoMagVector.cs +++ b/GeoMagSharp/Models/Results/GeoMagVector.cs @@ -14,6 +14,9 @@ public class GeoMagVector { #region Constructors + /// + /// Initializes a new instance with all components set to zero. + /// public GeoMagVector() { d = 0; @@ -25,6 +28,10 @@ public GeoMagVector() f = 0; } + /// + /// Initializes a new instance by copying all components from another . + /// + /// The source vector to copy. public GeoMagVector(GeoMagVector other) { d = other.d; diff --git a/GeoMagSharp/Models/Results/MagneticCalculations.cs b/GeoMagSharp/Models/Results/MagneticCalculations.cs index 592e8a7..aa5eac5 100644 --- a/GeoMagSharp/Models/Results/MagneticCalculations.cs +++ b/GeoMagSharp/Models/Results/MagneticCalculations.cs @@ -16,6 +16,9 @@ public class MagneticCalculations { #region Constructors + /// + /// Initializes a new instance with the current date and zero-valued components. + /// public MagneticCalculations() { Date = DateTime.Now; @@ -28,6 +31,10 @@ public MagneticCalculations() TotalField = new MagneticValue(); } + /// + /// Initializes a new instance by copying values from another . + /// + /// The source calculation results to copy. public MagneticCalculations(MagneticCalculations other) { Date = other.Date; @@ -40,6 +47,12 @@ public MagneticCalculations(MagneticCalculations other) TotalField = new MagneticValue(other.TotalField); } + /// + /// Initializes a new instance from field and secular variation vectors. + /// + /// The date of the calculation. + /// The main magnetic field vector. + /// The secular variation vector, or null if not computed. public MagneticCalculations(DateTime inDate, GeoMagVector fieldCalculations, GeoMagVector SecVarCalculations = null) { Date = inDate; diff --git a/GeoMagSharp/Models/Results/MagneticValue.cs b/GeoMagSharp/Models/Results/MagneticValue.cs index 76934b2..c5267d1 100644 --- a/GeoMagSharp/Models/Results/MagneticValue.cs +++ b/GeoMagSharp/Models/Results/MagneticValue.cs @@ -14,12 +14,19 @@ public class MagneticValue { #region Constructors + /// + /// Initializes a new instance with zero value and zero change per year. + /// public MagneticValue() { Value = 0.0; ChangePerYear = 0.0; } + /// + /// Initializes a new instance by copying values from another . + /// + /// The source value to copy. public MagneticValue(MagneticValue other) { Value = other.Value; diff --git a/GeoMagSharp/Units.cs b/GeoMagSharp/Units.cs index 99e97b7..23ba0d9 100644 --- a/GeoMagSharp/Units.cs +++ b/GeoMagSharp/Units.cs @@ -13,17 +13,33 @@ namespace GeoMagSharp { + /// + /// Provides distance and angle unit types with conversion between string and enum representations. + /// public static class Distance { + /// + /// Distance measurement units. + /// public enum Unit { + /// Unknown or unspecified unit. unknown = 0, + /// Meters. meter = 1, + /// Kilometers. kilometer = 2, + /// Feet. foot = 3, + /// Miles. mile = 4 } + /// + /// Converts a enum value to its abbreviation string. + /// + /// The distance unit to convert. + /// The unit abbreviation (e.g., "m", "ft", "mi"), or empty string for unknown. public static string ToString(Unit inUnit) { switch (inUnit) @@ -32,7 +48,7 @@ public static string ToString(Unit inUnit) return @"m"; case Distance.Unit.kilometer: - return @"mi"; + return @"km"; case Distance.Unit.foot: return @"ft"; @@ -45,6 +61,11 @@ public static string ToString(Unit inUnit) return string.Empty; } + /// + /// Parses a string to the corresponding enum value. + /// + /// The unit string (e.g., "m", "km", "ft", "meter"). + /// The matching , or if not recognized. public static Unit FromString(string unitString) { switch (unitString.ToLower()) @@ -73,15 +94,29 @@ public static Unit FromString(string unitString) return Distance.Unit.unknown; } + /// + /// Provides angle unit types with conversion between string and enum representations. + /// public static class Angle { + /// + /// Angle measurement units. + /// public enum Unit { + /// Unknown or unspecified unit. unknown = 0, + /// Degrees. Degree = 1, + /// Radians. Radian = 2 } + /// + /// Converts an enum value to its symbol string. + /// + /// The angle unit to convert. + /// The unit symbol (e.g., "°", "g"), or empty string for unknown. public static string ToString(Unit inUnit) { switch (inUnit) @@ -97,6 +132,11 @@ public static string ToString(Unit inUnit) return string.Empty; } + /// + /// Parses a string to the corresponding enum value. + /// + /// The unit string (e.g., "degree", "deg", "°", "radian"). + /// The matching , or if not recognized. public static Unit FromString(string unitString) { switch (unitString.ToLower()) diff --git a/README.md b/README.md index fa29d5a..02c1f2a 100644 --- a/README.md +++ b/README.md @@ -138,6 +138,5 @@ See [GeoMagGUI/documentation/LICENSE](./GeoMagGUI/documentation/LICENSE) ## Credits -- Original NOAA Geomag 7.0 software -- British Geological Survey (BGS) calculation algorithms +- NOAA World Magnetic Model (WMM) and Geomag 7.0 software - Port to C# with GUI by StreckerCM 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) diff --git a/docs/prompts/PERSONAS.md b/docs/prompts/PERSONAS.md index a5a8410..defb521 100644 --- a/docs/prompts/PERSONAS.md +++ b/docs/prompts/PERSONAS.md @@ -193,7 +193,7 @@ As a [user type], I want [goal] so that [benefit]. ### Visual States | State | Appearance | Trigger | -|-------|------------|---------| +|-------|------------|--------| | Default | [description] | Initial load | | Hover | [description] | Mouse over | | Disabled | [description] | [condition] | @@ -502,6 +502,141 @@ As a [user type], I want [goal] so that [benefit]. --- +## Rotating Persona Pattern + +The most effective way to use personas is **rotation** - cycling through different perspectives each iteration using `ITERATION MOD N`. + +### Standard 6-Persona Rotation + +``` +[0] #5 IMPLEMENTER - Complete tasks, write code +[1] #9 REVIEWER - Review for bugs, issues +[2] #7 TESTER - Verify functionality, add tests +[3] #3 UI_UX_DESIGNER - Review UI/UX (if applicable) +[4] #10 SECURITY - Security review +[5] #2 PROJECT_MGR - Check requirements, update tasks +``` + +### Example Rotating Loop + +```bash +/ralph-loop " +Feature: emm-model-support + +PHASE 1 - TASKS: +- Read docs/features/emm-model-support/tasks.md +- Complete unchecked tasks, mark done with [x] +- Run: msbuild GeoMagGUI.sln /p:Configuration=Debug /p:Platform=\"x86\" + +PHASE 2 - ROTATING REVIEW (ITERATION MOD 6): + +[0] #5 IMPLEMENTER: Complete next task, follow patterns +[1] #9 REVIEWER: Review code for bugs/issues, fix problems +[2] #7 TESTER: Verify functionality, check edge cases +[3] #3 UI_UX_DESIGNER: Review WinForms UI, check usability +[4] #10 SECURITY: Check for vulnerabilities, validate inputs +[5] #2 PROJECT_MANAGER: Verify requirements met, update tasks + +EACH ITERATION: +1. Run current persona's checks (Iteration % 6) +2. Make fixes/improvements +3. Commit: '[PERSONA] description' +4. Post PR comment with findings/changes (see PR Comment Format below) + +OUTPUT FEATURE COMPLETE when all tasks done and 2 clean cycles. +" --completion-promise "FEATURE COMPLETE" --max-iterations 30 +``` + +### PR Comment Format + +Each persona should post a comment to the PR summarizing their findings and changes. This creates an audit trail and helps human reviewers understand the AI's reasoning. + +```markdown +## [PERSONA] Review - Iteration N + +### Summary +[Brief description of what was reviewed/implemented] + +### Findings +- [Finding 1 - issue found or observation] +- [Finding 2] + +### Changes Made +- [Change 1 - file:line - description] +- [Change 2] + +### Status +- [ ] Issues found requiring follow-up +- [x] Clean pass - no issues found +``` + +**Example PR Comments:** + +```markdown +## [IMPLEMENTER] Review - Iteration 0 + +### Summary +Implemented EMM coefficient file reader. + +### Changes Made +- GeoMagSharp/ModelReader.cs:150-200 - Added EMM file parsing +- GeoMagSharp/GeoMag.cs:50-75 - Updated model loading + +### Status +- [x] Implementation complete, ready for review +``` + +```markdown +## [REVIEWER] Review - Iteration 1 + +### Summary +Code review of EMM model implementation. + +### Findings +- Missing bounds check in coefficient array access +- Date validation doesn't handle EMM date ranges + +### Changes Made +- GeoMagSharp/ModelReader.cs:175 - Added bounds check +- GeoMagSharp/GeoMag.cs:62 - Fixed date range validation + +### Status +- [x] Issues fixed, clean pass +``` + +```markdown +## [SECURITY_AUDITOR] Review - Iteration 4 + +### Summary +Security review of EMM file parsing feature. + +### Findings +- No path traversal vulnerabilities found +- Coefficient parsing is safe from buffer overflow +- No hardcoded paths or credentials + +### Changes Made +- None required + +### Status +- [x] Clean pass - no security issues found +``` + +See [templates/ROTATING_FEATURE.md](./templates/ROTATING_FEATURE.md) for full templates. + +--- + +## Persona Combinations (Sequential) + +For simpler tasks, use personas in sequence: + +```bash +# Implementation with self-review +/ralph-loop "Using persona #5 (IMPLEMENTER), implement Feature X. Then switch to persona #9 (REVIEWER) and review your own code. Fix any issues found. Output COMPLETE when implementation is done and review passes." --completion-promise "COMPLETE" +``` + +--- + ## Custom Persona Template ```markdown diff --git a/docs/prompts/templates/ROTATING_FEATURE.md b/docs/prompts/templates/ROTATING_FEATURE.md index b2a03f3..6dbe15d 100644 --- a/docs/prompts/templates/ROTATING_FEATURE.md +++ b/docs/prompts/templates/ROTATING_FEATURE.md @@ -1,9 +1,22 @@ # Rotating Persona Feature Implementation -Each iteration, the persona changes based on `iteration % N` — ensuring every perspective reviews the work multiple times. +This template uses rotating personas that cycle through different perspectives on each iteration, ensuring comprehensive review from multiple angles. + +## How It Works + +Each iteration, the persona changes based on `iteration % N`: +- Iteration 0, 6, 12... → Persona 0 +- Iteration 1, 7, 13... → Persona 1 +- And so on... + +This ensures every perspective reviews the work multiple times. + +--- ## Standard 6-Persona Rotation +Best for most feature implementations: + ```bash /ralph-loop " Feature: [FEATURE_NUMBER]-[feature-name] @@ -18,19 +31,49 @@ PHASE 1 - TASK COMPLETION: PHASE 2 - ROTATING PERSONA REVIEW (cycle each iteration): Current Persona = ITERATION MOD 6: -[0] #5 IMPLEMENTER: Complete next task, follow patterns, run build -[1] #9 CODE REVIEWER: Review for bugs/edge cases, check patterns, fix issues -[2] #7 TESTER: Run vstest.console.exe, check coverage, write missing tests -[3] #3 UI_UX_DESIGNER: Review WinForms consistency, tab order, visual states -[4] #10 SECURITY_AUDITOR: Check config values, input validation, file I/O -[5] #2 PROJECT_MANAGER: Verify tasks complete, check requirements, update tasks.md +[0] #5 IMPLEMENTER: +- Check tasks.md for incomplete items +- Implement next unchecked task +- Follow existing code patterns +- Run build after changes + +[1] #9 CODE REVIEWER: +- Review recent changes for bugs, edge cases +- Check error handling and null safety +- Verify code follows project patterns +- Fix any issues found + +[2] #7 TESTER: +- Run: vstest.console.exe GeoMagSharp-UnitTests\\bin\\Debug\\GeoMagSharp-UnitTests.dll +- Check test coverage for new code +- Write missing unit tests +- Verify edge cases are covered + +[3] #3 UI_UX_DESIGNER (if UI changes): +- Review WinForms layout and controls +- Check tab order and keyboard navigation +- Verify consistent styling +- Check visual states (enabled, disabled, error) + +[4] #10 SECURITY_AUDITOR: +- Check for hardcoded values that should be config +- Verify input validation for coordinates +- Look for potential security issues +- Review any new file I/O code + +[5] #2 PROJECT_MANAGER: +- Verify all tasks in tasks.md are checked +- Check spec.md requirements are met +- Document any gaps or issues found +- Update tasks.md if new work discovered EACH ITERATION: 1. Identify current persona (Iteration % 6) 2. Perform that persona's review/work -3. Commit: '[persona] description' -4. Post PR comment with findings/changes -5. If all tasks complete AND 2 clean cycles (12 iterations), output completion +3. Make improvements or fixes as needed +4. Commit changes with message: '[persona] description' +5. Post PR comment with findings/changes (see PR Comment Format below) +6. If all tasks complete AND no issues found by ANY persona for 2 full cycles (12 iterations), output completion OUTPUT FEATURE COMPLETE when: - All tasks in tasks.md are checked [x] @@ -39,44 +82,218 @@ OUTPUT FEATURE COMPLETE when: " --completion-promise "FEATURE COMPLETE" --max-iterations 30 ``` +--- + +## Example: Add New Magnetic Model Support + +```bash +/ralph-loop " +Feature: emm-model-support +Branch: feature/emm-model-support + +PHASE 1 - TASK COMPLETION: +- Read docs/features/emm-model-support/tasks.md +- If any tasks unchecked (- [ ]), complete them first +- Mark completed tasks (- [x]) as you finish them +- Run: msbuild GeoMagGUI.sln /p:Configuration=Debug /p:Platform=\"x86\" + +PHASE 2 - ROTATING PERSONA REVIEW (cycle each iteration): +Current Persona = ITERATION MOD 6: + +[0] #5 IMPLEMENTER: +- Check tasks.md for incomplete items +- Implement next unchecked task +- Follow existing ModelReader patterns +- Run build after changes + +[1] #9 CODE REVIEWER: +- Review recent changes for bugs, edge cases +- Check coefficient parsing is correct +- Verify error handling for malformed files +- Fix any issues found + +[2] #7 TESTER: +- Run unit tests +- Add tests for EMM file parsing +- Verify calculations match reference values +- Test edge cases (min/max dates, boundaries) + +[3] #3 UI_UX_DESIGNER: +- Verify EMM appears correctly in model dropdown +- Check model info display is consistent +- Verify date range validation UI + +[4] #10 SECURITY_AUDITOR: +- Check EMM file parsing is safe +- Verify no path traversal vulnerabilities +- Review coefficient array bounds + +[5] #2 PROJECT_MANAGER: +- Verify all tasks in tasks.md are checked +- Ensure backward compatibility maintained +- Document any remaining work + +EACH ITERATION: +1. Identify current persona (Iteration % 6) +2. Perform that persona's review/work +3. Make improvements or fixes as needed +4. Commit changes with message: '[persona] description' +5. Post PR comment with findings/changes (see PR Comment Format below) +6. If all tasks complete AND no issues found for 2 full cycles, output completion + +OUTPUT FEATURE COMPLETE when: +- All tasks in tasks.md are checked [x] +- Build succeeds with no errors +- All personas report no issues for 2 consecutive cycles +" --completion-promise "FEATURE COMPLETE" --max-iterations 30 +``` + +--- + ## Compact 4-Persona Rotation (Faster) -For simpler features: +For simpler features or when speed is preferred: ```bash /ralph-loop " Feature: [FEATURE_NUMBER]-[feature-name] ROTATING PERSONA (ITERATION MOD 4): + [0] #5 IMPLEMENTER: Complete next task from tasks.md, run build [1] #9 REVIEWER: Review code for bugs/issues, fix problems [2] #7 TESTER: Verify functionality, add tests if needed [3] #2 PROJECT_MANAGER: Check all requirements met, update tasks.md -EACH ITERATION: Run persona checks, commit '[persona] description', post PR comment. +EACH ITERATION: +1. Run current persona's checks +2. Make one fix/improvement +3. Commit: '[persona] description' +4. Post PR comment with findings/changes OUTPUT DONE when all tasks complete and 2 clean cycles. " --completion-promise "DONE" --max-iterations 20 ``` -## Commit & PR Format +--- + +## Full 11-Persona Rotation (Comprehensive) -**Commits:** `[IMPLEMENTER] Add feature X` / `[REVIEWER] Fix null check` / `[TESTER] Add unit test for Y` +For critical features requiring maximum scrutiny: + +```bash +/ralph-loop " +Feature: [FEATURE_NUMBER]-[feature-name] + +ROTATING PERSONA (ITERATION MOD 11): + +[0] #1 BUSINESS_ANALYST: Verify requirements clarity, check acceptance criteria +[1] #2 PROJECT_MANAGER: Check progress, identify blockers, update tasks +[2] #3 UI_UX_DESIGNER: Review UI design, user flows, accessibility +[3] #4 UI_IMPLEMENTER: Check WinForms quality, bindings, control layout +[4] #5 IMPLEMENTER: Complete next task, follow patterns +[5] #6 REFACTORER: Clean up code, improve organization +[6] #7 TESTER: Run tests, add coverage, verify edge cases +[7] #8 DEBUGGER: Look for potential bugs, add defensive code +[8] #9 REVIEWER: Full code review, check quality +[9] #10 SECURITY_AUDITOR: Security review, check for vulnerabilities +[10] #11 DOCUMENTER: Update comments, check documentation + +OUTPUT FEATURE COMPLETE when all tasks done and clean cycle. +" --completion-promise "FEATURE COMPLETE" --max-iterations 44 +``` + +--- + +## Commit Message Format + +Each commit should indicate which persona made the change: + +``` +[IMPLEMENTER] Add EMM coefficient file reader +[REVIEWER] Fix array bounds check in ModelReader +[TESTER] Add unit tests for EMM date validation +[UI_UX_DESIGNER] Update model dropdown styling +[SECURITY_AUDITOR] Add input validation for model paths +[PROJECT_MANAGER] Mark EMM support tasks complete +``` + +--- + +## PR Comment Format + +Each persona should post a comment to the PR summarizing their findings and changes. This creates an audit trail and helps human reviewers understand the AI's reasoning. -**PR Comments:** ```markdown ## [PERSONA] Review - Iteration N ### Summary -[Brief description] +[Brief description of what was reviewed/implemented] ### Findings -- [Issues or observations] +- [Finding 1 - issue found or observation] +- [Finding 2] ### Changes Made -- [file:line - description] +- [Change 1 - file:line - description] +- [Change 2] ### Status - [ ] Issues found requiring follow-up - [x] Clean pass - no issues found ``` + +### Example PR Comments + +**IMPLEMENTER:** +```markdown +## [IMPLEMENTER] Review - Iteration 0 + +### Summary +Implemented EMM coefficient file reader. + +### Changes Made +- GeoMagSharp/ModelReader.cs:150-200 - Added EMM file parsing +- GeoMagSharp/GeoMag.cs:50-75 - Updated model loading + +### Status +- [x] Implementation complete, ready for review +``` + +**REVIEWER:** +```markdown +## [REVIEWER] Review - Iteration 1 + +### Summary +Code review of EMM model implementation. + +### Findings +- Missing bounds check in coefficient array access +- Date validation doesn't handle EMM date ranges + +### Changes Made +- GeoMagSharp/ModelReader.cs:175 - Added bounds check +- GeoMagSharp/GeoMag.cs:62 - Fixed date range validation + +### Status +- [x] Issues fixed, clean pass +``` + +**SECURITY_AUDITOR:** +```markdown +## [SECURITY_AUDITOR] Review - Iteration 4 + +### Summary +Security review of EMM file parsing feature. + +### Findings +- No path traversal vulnerabilities found +- Coefficient parsing is safe from buffer overflow +- No hardcoded paths or credentials + +### Changes Made +- None required + +### Status +- [x] Clean pass - no security issues found +```