Skip to content

refactor: shadowfinder + exif — symmetric metric, inlined sun math, WMM declination#4

Merged
nickFalcone merged 1 commit into
mainfrom
code-cleanup
May 23, 2026
Merged

refactor: shadowfinder + exif — symmetric metric, inlined sun math, WMM declination#4
nickFalcone merged 1 commit into
mainfrom
code-cleanup

Conversation

@nickFalcone
Copy link
Copy Markdown
Owner

Summary

Follow-up to #3 addressing the deeper correctness, accuracy, and perf issues surfaced during code review. Each bullet maps to one item from the review.

src/lib/shadowfinder.ts

  • Symmetric likelihood metric (Gps coords #2) — replace asymmetric `|ratio/tan(alt) − 1|` with angular error `|atan(h/s) − sunAlt|` in radians. Band thresholds re-tuned and exported (`ULTRA_TIGHT_BAND_RAD`, `TIGHT_BAND_RAD`, `MAIN_BAND_RAD`, `VISIBLE_BAND_RAD`) so the visualization shares them.
  • Inlined sun-position math (chore: code cleanup — CLAUDE.md, test suite, conventions, perf #3) — time-only terms precomputed once, longitude trig cached per column in a `Float64Array`, integer-indexed loops, preallocated points array. ~2.5× speedup vs. the previous SunCalc-per-cell loop (74.8 ms → 29.7 ms with constraint, measured over 12 runs).
  • Antimeridian + bimodal posteriors (Feature/leaflet map #1) — `estimateBestLocation` splits at the largest latitude gap, takes a circular mean of longitudes (correct across ±180°), and reports a 68th-percentile great-circle distance as accuracy (robust to outliers, floors at half-cell ~28 km). `mainBandCoordinates` gained `lngWraps` to flag wrap-around bands.
  • Constraint-aware grid path (#8) — `generateShadowFinderGrid` now takes an optional `azimuthConstraint` that lets a caller skip `Math.asin` for cells outside tolerance. Always skip `Math.atan2` on night cells (NaN-azimuth night cells are correctly excluded by `applyAzimuthConstraint`). Production callsite intentionally does not pass the constraint so the visualization's no-match fallback continues to work.

src/lib/exif.ts

  • WMM2025 magnetic-bearing correction (refactor: shadowfinder + exif — symmetric metric, inlined sun math, WMM declination #4) — when EXIF reports a magnetic bearing alongside GPS coords, apply the declination from the bundled `geomagnetism` package (Apache-2.0, valid through 2029). Stores the applied declination as `magneticDeclination`. When no GPS is available, the magnetic bearing is left untouched and existing consumers continue to ignore it.
  • `localTime` footgun fixed (#5) — replaced `localTime: Date` (where the Date's UTC fields encoded the wall-clock and `.getHours()` lied) with `localWallClock: string` (ISO 8601 without `Z`). `applyUtcOffset` rewritten to accept the string. `formatDateInput` / `formatTimeInput` are now polymorphic over `Date | string`.
  • Misc correctness (#9–#11) — preserve fractional GPS seconds as ms; `computeFovDeg` honors portrait orientation (24 mm sensor); dev `console.log` gated by `import.meta.env.DEV` (verified stripped from prod bundle); UTC offset parser now accepts `+HH:MM`, `+HHMM`, and `+HH`; `-0` normalized to `+0`.

src/lib/shadowfinder.ts + src/lib/exif.ts — named constants (#10)

Hoisted magic numbers: `KM_PER_DEG_LAT`, `ACCURACY_PERCENTILE`, `DEFAULT_AZIMUTH_TOLERANCE_DEG`, `SENSOR_LONG_DIM_MM`/`SENSOR_SHORT_DIM_MM`, `DEFAULT_FOV_DEG`. `AnalysisPanel` now imports the tolerance constant rather than inlining `10`.

Test plan

  • `npx tsc --noEmit`: clean.
  • `npm test`: 80/80 pass (was 51 before this PR — 29 new tests for SunCalc parity, antimeridian centroid, bimodal clustering, WMM declination, constraint-aware grid, polymorphic formatters, etc.).
  • `npm run lint`: only the 3 pre-existing warnings in `ui/badge.tsx`, `ui/button.tsx`, `tailwind.config.ts` (verified by stashing the diff and re-running).
  • `npm run build`: succeeds; gzipped bundle 177.6 kB (+~5 kB net from the bundled WMM2025 coefficients).
  • Manual: `grep -c '[exif]' dist/assets/*.js` returns 0, confirming the dev-only log is tree-shaken from prod.

🤖 Co-authored with Claude Opus 4.7

Made with Cursor

…nomics

shadowfinder:
- Symmetric angular error |atan(h/s) − sunAlt| as the likelihood metric;
  exported band thresholds (ULTRA_TIGHT/TIGHT/MAIN/VISIBLE_BAND_RAD).
- Inline sun-position math: time-only terms precomputed once, lng trig
  cached per column, integer-indexed loops, preallocated array. ~2.5×
  speedup over SunCalc-per-cell (74.8ms → 29.7ms with constraint).
- estimateBestLocation: split bimodal lat-clusters at the largest gap,
  use circular mean for longitude (correct across the antimeridian), and
  68th-percentile great-circle distance as the accuracy radius.
- mainBandCoordinates gains lngWraps to flag antimeridian-crossing bands.
- Optional azimuth constraint on generateShadowFinderGrid skips Math.asin
  for cells outside tolerance; always skip Math.atan2 on night cells.
- Hoist magic numbers (KM_PER_DEG_LAT, ACCURACY_PERCENTILE,
  DEFAULT_AZIMUTH_TOLERANCE_DEG, etc.) to named constants.

exif:
- WMM2025 magnetic-bearing correction when EXIF reports a magnetic
  bearing alongside GPS coords (new geomagnetism dep, Apache-2.0).
  Stores the applied declination as magneticDeclination.
- localTime: Date → localWallClock: string (ISO 8601 without Z) to fix
  the wall-clock-encoded-as-UTC-Date footgun. applyUtcOffset rewritten
  to accept the string; formatDateInput/formatTimeInput now accept
  Date | string.
- Preserve fractional GPS seconds as milliseconds.
- computeFovDeg honors portrait orientation (24mm short sensor dim).
- Gate dev console.log behind import.meta.env.DEV (verified stripped
  from the prod bundle).
- More forgiving UTC offset parsing (+HH:MM, +HHMM, +HH); -0 normalized
  to +0.

Tests grow 51 → 80, including SunCalc parity at six sample points,
antimeridian centroid handling, WMM declination samples, and
constraint-aware grid behavior. CLAUDE.md updated to reflect the new
metric and inlined sun math.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR is a follow-up refactor that hardens correctness, accuracy, and performance of the shadow-analysis and EXIF pipelines. It replaces the asymmetric likelihood metric with a symmetric angular error in radians, inlines and caches sun-position math for ~2.5× speedup, makes location estimation robust across the antimeridian and bimodal posteriors, applies WMM2025 declination to magnetic bearings when GPS is available, and replaces the localTime: Date footgun with a wall-clock ISO string. Several magic numbers were also hoisted into named constants and FOV calculation now honors portrait orientation.

Changes:

  • shadowfinder.ts: symmetric angular-error likelihood, inlined sun math with column-cached lng trig, antimeridian-safe circular-mean centroid with bimodal lat-cluster splitting, and an optional generation-time azimuth constraint (production callsite intentionally omits it to preserve visualization fallback).
  • exif.ts: localTime: DatelocalWallClock: string; magnetic bearings corrected via bundled geomagnetism (WMM2025) when GPS is present; parseUtcOffset accepts +HH:MM, +HHMM, +HH; portrait-aware computeFovDeg; dev log gated by import.meta.env.DEV; fractional GPS seconds preserved.
  • Constants/visualization: VISIBLE_BAND_RAD, NIGHT_LIKELIHOOD, DEFAULT_AZIMUTH_TOLERANCE_DEG exported and consumed by ShadowFinderVisualization and AnalysisPanel; CLAUDE.md updated; 29 new tests including SunCalc parity, antimeridian centroid, bimodal clustering, and WMM declination.

Reviewed changes

Copilot reviewed 8 out of 9 changed files in this pull request and generated no comments.

Show a summary per file
File Description
src/lib/shadowfinder.ts New symmetric angular-error metric, inlined sun math, antimeridian/bimodal-safe estimateBestLocation, optional azimuth constraint, exported band/tolerance constants.
src/lib/shadowfinder.test.ts Updated tests for new metric, added SunCalc parity, dominantLatCluster, circular-mean longitude, constraint-aware grid, and lngWraps coverage.
src/lib/exif.ts Replaced localTime with localWallClock, added parseUtcOffset/correctMagneticBearing, portrait-aware FOV, dev-gated logging, fractional GPS seconds.
src/lib/exif.test.ts New tests for parseUtcOffset, correctMagneticBearing, portrait FOV, string-overloaded formatters, day-boundary applyUtcOffset.
src/components/ShadowFinderVisualization.tsx Uses exported VISIBLE_BAND_RAD/NIGHT_LIKELIHOOD and LIKELIHOOD_CUTOFF for intersection normalization.
src/components/AnalysisPanel.tsx Migrated to localWallClock and DEFAULT_AZIMUTH_TOLERANCE_DEG.
package.json / package-lock.json Adds geomagnetism@^0.2.0 runtime dependency; lockfile cleanup.
CLAUDE.md Documents inlined sun math, symmetric error metric, bimodal/antimeridian handling.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@nickFalcone nickFalcone merged commit 31c4004 into main May 23, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants