mirror of
https://github.com/acamarata/moon-sighting.git
synced 2026-06-30 19:04:24 +00:00
refactor: code quality improvements and fix arcvMinimum constant
Fix critical bug: arcvMinimum polynomial constant was 7.1651 (wrong) instead of 11.8371 (Odeh 2006) in getMoonVisibilityEstimate. Now imports the canonical arcvMinimum() from visibility module. Deduplicate shared code across modules: - arcvMinimum polynomial: single source in visibility/index.ts - dot/norm vector helpers: use vdot/vnorm from math/index.ts - DEG constant: use DEG2RAD from math/index.ts - jdToJSDate: use jdToDate from time/index.ts Add input validation to all public API functions (lat/lon range, valid Date instances). Add ESLint + Prettier with TypeScript support. Convert tests to node:test runner. Fix package.json exports to use nested types-first format. Pin devDependencies to caret ranges. Add noImplicitReturns and noFallthroughCasesInSwitch to tsconfig. Replace .markdownlint.json with .vscode/settings.json. Update CI workflow with lint job. Expand .gitignore coverage.
This commit is contained in:
parent
3b666c6465
commit
8bf34fb696
21 changed files with 1887 additions and 1031 deletions
29
.github/workflows/ci.yml
vendored
29
.github/workflows/ci.yml
vendored
|
|
@ -22,10 +22,26 @@ jobs:
|
||||||
with:
|
with:
|
||||||
node-version: ${{ matrix.node }}
|
node-version: ${{ matrix.node }}
|
||||||
cache: pnpm
|
cache: pnpm
|
||||||
- run: pnpm install
|
- run: pnpm install --frozen-lockfile
|
||||||
- run: pnpm build
|
- run: pnpm build
|
||||||
- run: node test.mjs
|
- run: node --test test.mjs
|
||||||
- run: node test-cjs.cjs
|
- run: node --test test-cjs.cjs
|
||||||
|
|
||||||
|
lint:
|
||||||
|
name: Lint
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: pnpm/action-setup@v4
|
||||||
|
with:
|
||||||
|
version: 10
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 24
|
||||||
|
cache: pnpm
|
||||||
|
- run: pnpm install --frozen-lockfile
|
||||||
|
- run: pnpm run lint
|
||||||
|
- run: pnpm run format:check
|
||||||
|
|
||||||
typecheck:
|
typecheck:
|
||||||
name: TypeScript
|
name: TypeScript
|
||||||
|
|
@ -39,7 +55,7 @@ jobs:
|
||||||
with:
|
with:
|
||||||
node-version: 24
|
node-version: 24
|
||||||
cache: pnpm
|
cache: pnpm
|
||||||
- run: pnpm install
|
- run: pnpm install --frozen-lockfile
|
||||||
- run: pnpm run typecheck
|
- run: pnpm run typecheck
|
||||||
|
|
||||||
pack-check:
|
pack-check:
|
||||||
|
|
@ -54,17 +70,16 @@ jobs:
|
||||||
with:
|
with:
|
||||||
node-version: 24
|
node-version: 24
|
||||||
cache: pnpm
|
cache: pnpm
|
||||||
- run: pnpm install
|
- run: pnpm install --frozen-lockfile
|
||||||
- run: pnpm build
|
- run: pnpm build
|
||||||
- name: Verify pack contents
|
- name: Verify pack contents
|
||||||
run: |
|
run: |
|
||||||
npm pack --dry-run 2>&1 | tee pack-output.txt
|
npm pack --dry-run 2>&1 | tee pack-output.txt
|
||||||
# Verify expected files are present
|
|
||||||
grep -q "dist/index.cjs" pack-output.txt || (echo "Missing dist/index.cjs" && exit 1)
|
grep -q "dist/index.cjs" pack-output.txt || (echo "Missing dist/index.cjs" && exit 1)
|
||||||
grep -q "dist/index.mjs" pack-output.txt || (echo "Missing dist/index.mjs" && exit 1)
|
grep -q "dist/index.mjs" pack-output.txt || (echo "Missing dist/index.mjs" && exit 1)
|
||||||
grep -q "dist/index.d.ts" pack-output.txt || (echo "Missing dist/index.d.ts" && exit 1)
|
grep -q "dist/index.d.ts" pack-output.txt || (echo "Missing dist/index.d.ts" && exit 1)
|
||||||
|
grep -q "dist/index.d.mts" pack-output.txt || (echo "Missing dist/index.d.mts" && exit 1)
|
||||||
grep -q "README.md" pack-output.txt || (echo "Missing README.md" && exit 1)
|
grep -q "README.md" pack-output.txt || (echo "Missing README.md" && exit 1)
|
||||||
# Verify no test files or .gitignore items are included
|
|
||||||
! grep -q "test.mjs" pack-output.txt || (echo "test.mjs should not be in pack" && exit 1)
|
! grep -q "test.mjs" pack-output.txt || (echo "test.mjs should not be in pack" && exit 1)
|
||||||
! grep -q "node_modules" pack-output.txt || (echo "node_modules should not be in pack" && exit 1)
|
! grep -q "node_modules" pack-output.txt || (echo "node_modules should not be in pack" && exit 1)
|
||||||
echo "Pack contents verified."
|
echo "Pack contents verified."
|
||||||
|
|
|
||||||
25
.gitignore
vendored
25
.gitignore
vendored
|
|
@ -1,7 +1,10 @@
|
||||||
node_modules/
|
node_modules/
|
||||||
dist/
|
dist/
|
||||||
|
build/
|
||||||
|
out/
|
||||||
*.tgz
|
*.tgz
|
||||||
*.log
|
*.log
|
||||||
|
*.tsbuildinfo
|
||||||
.DS_Store
|
.DS_Store
|
||||||
.env
|
.env
|
||||||
.env.*
|
.env.*
|
||||||
|
|
@ -11,8 +14,28 @@ dist/
|
||||||
*.bsp
|
*.bsp
|
||||||
*.tls
|
*.tls
|
||||||
|
|
||||||
|
# PnP
|
||||||
|
.pnp
|
||||||
|
.pnp.js
|
||||||
|
|
||||||
|
# Coverage
|
||||||
|
coverage/
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
*.swp
|
||||||
|
|
||||||
# AI agent directories
|
# AI agent directories
|
||||||
.claude/
|
.claude/
|
||||||
.cursor/
|
.cursor/
|
||||||
.aider/
|
.copilot/
|
||||||
|
.aider*
|
||||||
.continue/
|
.continue/
|
||||||
|
.codex/
|
||||||
|
.gemini/
|
||||||
|
.vscode/*
|
||||||
|
.aider/
|
||||||
|
.aider.chat.history.md
|
||||||
|
.windsurf/
|
||||||
|
.codeium/
|
||||||
|
|
|
||||||
|
|
@ -1,4 +0,0 @@
|
||||||
{
|
|
||||||
"MD013": false,
|
|
||||||
"MD024": { "siblings_only": true }
|
|
||||||
}
|
|
||||||
7
.prettierrc
Normal file
7
.prettierrc
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
{
|
||||||
|
"semi": false,
|
||||||
|
"singleQuote": true,
|
||||||
|
"trailingComma": "all",
|
||||||
|
"printWidth": 100,
|
||||||
|
"tabWidth": 2
|
||||||
|
}
|
||||||
12
eslint.config.mjs
Normal file
12
eslint.config.mjs
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
import eslint from '@eslint/js'
|
||||||
|
import tseslint from 'typescript-eslint'
|
||||||
|
import prettier from 'eslint-config-prettier'
|
||||||
|
|
||||||
|
export default tseslint.config(
|
||||||
|
eslint.configs.recommended,
|
||||||
|
...tseslint.configs.recommended,
|
||||||
|
prettier,
|
||||||
|
{
|
||||||
|
ignores: ['dist/', 'node_modules/', 'test.mjs', 'test-cjs.cjs'],
|
||||||
|
},
|
||||||
|
)
|
||||||
27
package.json
27
package.json
|
|
@ -9,9 +9,14 @@
|
||||||
"types": "./dist/index.d.ts",
|
"types": "./dist/index.d.ts",
|
||||||
"exports": {
|
"exports": {
|
||||||
".": {
|
".": {
|
||||||
"types": "./dist/index.d.ts",
|
"import": {
|
||||||
"import": "./dist/index.mjs",
|
"types": "./dist/index.d.mts",
|
||||||
"require": "./dist/index.cjs"
|
"default": "./dist/index.mjs"
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"types": "./dist/index.d.ts",
|
||||||
|
"default": "./dist/index.cjs"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
|
|
@ -31,14 +36,22 @@
|
||||||
"build": "tsup",
|
"build": "tsup",
|
||||||
"typecheck": "tsc --noEmit",
|
"typecheck": "tsc --noEmit",
|
||||||
"pretest": "tsup",
|
"pretest": "tsup",
|
||||||
"test": "node test.mjs && node test-cjs.cjs",
|
"test": "node --test test.mjs test-cjs.cjs",
|
||||||
|
"lint": "eslint src/",
|
||||||
|
"format": "prettier --write src/",
|
||||||
|
"format:check": "prettier --check src/",
|
||||||
"prepublishOnly": "tsup",
|
"prepublishOnly": "tsup",
|
||||||
"cli": "node dist/cli/index.cjs"
|
"cli": "node dist/cli/index.cjs"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "latest",
|
"@eslint/js": "^10.0.1",
|
||||||
"tsup": "latest",
|
"@types/node": "^25.3.0",
|
||||||
"typescript": "latest"
|
"eslint": "^10.0.3",
|
||||||
|
"eslint-config-prettier": "^10.1.8",
|
||||||
|
"prettier": "^3.8.1",
|
||||||
|
"tsup": "^8.5.1",
|
||||||
|
"typescript": "^5.9.3",
|
||||||
|
"typescript-eslint": "^8.56.1"
|
||||||
},
|
},
|
||||||
"publishConfig": {
|
"publishConfig": {
|
||||||
"access": "public",
|
"access": "public",
|
||||||
|
|
|
||||||
739
pnpm-lock.yaml
739
pnpm-lock.yaml
|
|
@ -8,15 +8,30 @@ importers:
|
||||||
|
|
||||||
.:
|
.:
|
||||||
devDependencies:
|
devDependencies:
|
||||||
|
'@eslint/js':
|
||||||
|
specifier: ^10.0.1
|
||||||
|
version: 10.0.1(eslint@10.0.3)
|
||||||
'@types/node':
|
'@types/node':
|
||||||
specifier: latest
|
specifier: ^25.3.0
|
||||||
version: 25.3.0
|
version: 25.3.0
|
||||||
|
eslint:
|
||||||
|
specifier: ^10.0.3
|
||||||
|
version: 10.0.3
|
||||||
|
eslint-config-prettier:
|
||||||
|
specifier: ^10.1.8
|
||||||
|
version: 10.1.8(eslint@10.0.3)
|
||||||
|
prettier:
|
||||||
|
specifier: ^3.8.1
|
||||||
|
version: 3.8.1
|
||||||
tsup:
|
tsup:
|
||||||
specifier: latest
|
specifier: ^8.5.1
|
||||||
version: 8.5.1(typescript@5.9.3)
|
version: 8.5.1(typescript@5.9.3)
|
||||||
typescript:
|
typescript:
|
||||||
specifier: latest
|
specifier: ^5.9.3
|
||||||
version: 5.9.3
|
version: 5.9.3
|
||||||
|
typescript-eslint:
|
||||||
|
specifier: ^8.56.1
|
||||||
|
version: 8.56.1(eslint@10.0.3)(typescript@5.9.3)
|
||||||
|
|
||||||
packages:
|
packages:
|
||||||
|
|
||||||
|
|
@ -176,6 +191,61 @@ packages:
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [win32]
|
os: [win32]
|
||||||
|
|
||||||
|
'@eslint-community/eslint-utils@4.9.1':
|
||||||
|
resolution: {integrity: sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==}
|
||||||
|
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
|
||||||
|
peerDependencies:
|
||||||
|
eslint: ^6.0.0 || ^7.0.0 || >=8.0.0
|
||||||
|
|
||||||
|
'@eslint-community/regexpp@4.12.2':
|
||||||
|
resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==}
|
||||||
|
engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0}
|
||||||
|
|
||||||
|
'@eslint/config-array@0.23.3':
|
||||||
|
resolution: {integrity: sha512-j+eEWmB6YYLwcNOdlwQ6L2OsptI/LO6lNBuLIqe5R7RetD658HLoF+Mn7LzYmAWWNNzdC6cqP+L6r8ujeYXWLw==}
|
||||||
|
engines: {node: ^20.19.0 || ^22.13.0 || >=24}
|
||||||
|
|
||||||
|
'@eslint/config-helpers@0.5.3':
|
||||||
|
resolution: {integrity: sha512-lzGN0onllOZCGroKJmRwY6QcEHxbjBw1gwB8SgRSqK8YbbtEXMvKynsXc3553ckIEBxsbMBU7oOZXKIPGZNeZw==}
|
||||||
|
engines: {node: ^20.19.0 || ^22.13.0 || >=24}
|
||||||
|
|
||||||
|
'@eslint/core@1.1.1':
|
||||||
|
resolution: {integrity: sha512-QUPblTtE51/7/Zhfv8BDwO0qkkzQL7P/aWWbqcf4xWLEYn1oKjdO0gglQBB4GAsu7u6wjijbCmzsUTy6mnk6oQ==}
|
||||||
|
engines: {node: ^20.19.0 || ^22.13.0 || >=24}
|
||||||
|
|
||||||
|
'@eslint/js@10.0.1':
|
||||||
|
resolution: {integrity: sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==}
|
||||||
|
engines: {node: ^20.19.0 || ^22.13.0 || >=24}
|
||||||
|
peerDependencies:
|
||||||
|
eslint: ^10.0.0
|
||||||
|
peerDependenciesMeta:
|
||||||
|
eslint:
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@eslint/object-schema@3.0.3':
|
||||||
|
resolution: {integrity: sha512-iM869Pugn9Nsxbh/YHRqYiqd23AmIbxJOcpUMOuWCVNdoQJ5ZtwL6h3t0bcZzJUlC3Dq9jCFCESBZnX0GTv7iQ==}
|
||||||
|
engines: {node: ^20.19.0 || ^22.13.0 || >=24}
|
||||||
|
|
||||||
|
'@eslint/plugin-kit@0.6.1':
|
||||||
|
resolution: {integrity: sha512-iH1B076HoAshH1mLpHMgwdGeTs0CYwL0SPMkGuSebZrwBp16v415e9NZXg2jtrqPVQjf6IANe2Vtlr5KswtcZQ==}
|
||||||
|
engines: {node: ^20.19.0 || ^22.13.0 || >=24}
|
||||||
|
|
||||||
|
'@humanfs/core@0.19.1':
|
||||||
|
resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==}
|
||||||
|
engines: {node: '>=18.18.0'}
|
||||||
|
|
||||||
|
'@humanfs/node@0.16.7':
|
||||||
|
resolution: {integrity: sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==}
|
||||||
|
engines: {node: '>=18.18.0'}
|
||||||
|
|
||||||
|
'@humanwhocodes/module-importer@1.0.1':
|
||||||
|
resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==}
|
||||||
|
engines: {node: '>=12.22'}
|
||||||
|
|
||||||
|
'@humanwhocodes/retry@0.4.3':
|
||||||
|
resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==}
|
||||||
|
engines: {node: '>=18.18'}
|
||||||
|
|
||||||
'@jridgewell/gen-mapping@0.3.13':
|
'@jridgewell/gen-mapping@0.3.13':
|
||||||
resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==}
|
resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==}
|
||||||
|
|
||||||
|
|
@ -327,20 +397,101 @@ packages:
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [win32]
|
os: [win32]
|
||||||
|
|
||||||
|
'@types/esrecurse@4.3.1':
|
||||||
|
resolution: {integrity: sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==}
|
||||||
|
|
||||||
'@types/estree@1.0.8':
|
'@types/estree@1.0.8':
|
||||||
resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==}
|
resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==}
|
||||||
|
|
||||||
|
'@types/json-schema@7.0.15':
|
||||||
|
resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
|
||||||
|
|
||||||
'@types/node@25.3.0':
|
'@types/node@25.3.0':
|
||||||
resolution: {integrity: sha512-4K3bqJpXpqfg2XKGK9bpDTc6xO/xoUP/RBWS7AtRMug6zZFaRekiLzjVtAoZMquxoAbzBvy5nxQ7veS5eYzf8A==}
|
resolution: {integrity: sha512-4K3bqJpXpqfg2XKGK9bpDTc6xO/xoUP/RBWS7AtRMug6zZFaRekiLzjVtAoZMquxoAbzBvy5nxQ7veS5eYzf8A==}
|
||||||
|
|
||||||
|
'@typescript-eslint/eslint-plugin@8.56.1':
|
||||||
|
resolution: {integrity: sha512-Jz9ZztpB37dNC+HU2HI28Bs9QXpzCz+y/twHOwhyrIRdbuVDxSytJNDl6z/aAKlaRIwC7y8wJdkBv7FxYGgi0A==}
|
||||||
|
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||||
|
peerDependencies:
|
||||||
|
'@typescript-eslint/parser': ^8.56.1
|
||||||
|
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
|
||||||
|
typescript: '>=4.8.4 <6.0.0'
|
||||||
|
|
||||||
|
'@typescript-eslint/parser@8.56.1':
|
||||||
|
resolution: {integrity: sha512-klQbnPAAiGYFyI02+znpBRLyjL4/BrBd0nyWkdC0s/6xFLkXYQ8OoRrSkqacS1ddVxf/LDyODIKbQ5TgKAf/Fg==}
|
||||||
|
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||||
|
peerDependencies:
|
||||||
|
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
|
||||||
|
typescript: '>=4.8.4 <6.0.0'
|
||||||
|
|
||||||
|
'@typescript-eslint/project-service@8.56.1':
|
||||||
|
resolution: {integrity: sha512-TAdqQTzHNNvlVFfR+hu2PDJrURiwKsUvxFn1M0h95BB8ah5jejas08jUWG4dBA68jDMI988IvtfdAI53JzEHOQ==}
|
||||||
|
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||||
|
peerDependencies:
|
||||||
|
typescript: '>=4.8.4 <6.0.0'
|
||||||
|
|
||||||
|
'@typescript-eslint/scope-manager@8.56.1':
|
||||||
|
resolution: {integrity: sha512-YAi4VDKcIZp0O4tz/haYKhmIDZFEUPOreKbfdAN3SzUDMcPhJ8QI99xQXqX+HoUVq8cs85eRKnD+rne2UAnj2w==}
|
||||||
|
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||||
|
|
||||||
|
'@typescript-eslint/tsconfig-utils@8.56.1':
|
||||||
|
resolution: {integrity: sha512-qOtCYzKEeyr3aR9f28mPJqBty7+DBqsdd63eO0yyDwc6vgThj2UjWfJIcsFeSucYydqcuudMOprZ+x1SpF3ZuQ==}
|
||||||
|
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||||
|
peerDependencies:
|
||||||
|
typescript: '>=4.8.4 <6.0.0'
|
||||||
|
|
||||||
|
'@typescript-eslint/type-utils@8.56.1':
|
||||||
|
resolution: {integrity: sha512-yB/7dxi7MgTtGhZdaHCemf7PuwrHMenHjmzgUW1aJpO+bBU43OycnM3Wn+DdvDO/8zzA9HlhaJ0AUGuvri4oGg==}
|
||||||
|
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||||
|
peerDependencies:
|
||||||
|
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
|
||||||
|
typescript: '>=4.8.4 <6.0.0'
|
||||||
|
|
||||||
|
'@typescript-eslint/types@8.56.1':
|
||||||
|
resolution: {integrity: sha512-dbMkdIUkIkchgGDIv7KLUpa0Mda4IYjo4IAMJUZ+3xNoUXxMsk9YtKpTHSChRS85o+H9ftm51gsK1dZReY9CVw==}
|
||||||
|
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||||
|
|
||||||
|
'@typescript-eslint/typescript-estree@8.56.1':
|
||||||
|
resolution: {integrity: sha512-qzUL1qgalIvKWAf9C1HpvBjif+Vm6rcT5wZd4VoMb9+Km3iS3Cv9DY6dMRMDtPnwRAFyAi7YXJpTIEXLvdfPxg==}
|
||||||
|
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||||
|
peerDependencies:
|
||||||
|
typescript: '>=4.8.4 <6.0.0'
|
||||||
|
|
||||||
|
'@typescript-eslint/utils@8.56.1':
|
||||||
|
resolution: {integrity: sha512-HPAVNIME3tABJ61siYlHzSWCGtOoeP2RTIaHXFMPqjrQKCGB9OgUVdiNgH7TJS2JNIQ5qQ4RsAUDuGaGme/KOA==}
|
||||||
|
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||||
|
peerDependencies:
|
||||||
|
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
|
||||||
|
typescript: '>=4.8.4 <6.0.0'
|
||||||
|
|
||||||
|
'@typescript-eslint/visitor-keys@8.56.1':
|
||||||
|
resolution: {integrity: sha512-KiROIzYdEV85YygXw6BI/Dx4fnBlFQu6Mq4QE4MOH9fFnhohw6wX/OAvDY2/C+ut0I3RSPKenvZJIVYqJNkhEw==}
|
||||||
|
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||||
|
|
||||||
|
acorn-jsx@5.3.2:
|
||||||
|
resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==}
|
||||||
|
peerDependencies:
|
||||||
|
acorn: ^6.0.0 || ^7.0.0 || ^8.0.0
|
||||||
|
|
||||||
acorn@8.16.0:
|
acorn@8.16.0:
|
||||||
resolution: {integrity: sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==}
|
resolution: {integrity: sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==}
|
||||||
engines: {node: '>=0.4.0'}
|
engines: {node: '>=0.4.0'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
ajv@6.14.0:
|
||||||
|
resolution: {integrity: sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==}
|
||||||
|
|
||||||
any-promise@1.3.0:
|
any-promise@1.3.0:
|
||||||
resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==}
|
resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==}
|
||||||
|
|
||||||
|
balanced-match@4.0.4:
|
||||||
|
resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==}
|
||||||
|
engines: {node: 18 || 20 || >=22}
|
||||||
|
|
||||||
|
brace-expansion@5.0.4:
|
||||||
|
resolution: {integrity: sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==}
|
||||||
|
engines: {node: 18 || 20 || >=22}
|
||||||
|
|
||||||
bundle-require@5.1.0:
|
bundle-require@5.1.0:
|
||||||
resolution: {integrity: sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA==}
|
resolution: {integrity: sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA==}
|
||||||
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
|
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
|
||||||
|
|
@ -366,6 +517,10 @@ packages:
|
||||||
resolution: {integrity: sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==}
|
resolution: {integrity: sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==}
|
||||||
engines: {node: ^14.18.0 || >=16.10.0}
|
engines: {node: ^14.18.0 || >=16.10.0}
|
||||||
|
|
||||||
|
cross-spawn@7.0.6:
|
||||||
|
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
|
||||||
|
engines: {node: '>= 8'}
|
||||||
|
|
||||||
debug@4.4.3:
|
debug@4.4.3:
|
||||||
resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==}
|
resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==}
|
||||||
engines: {node: '>=6.0'}
|
engines: {node: '>=6.0'}
|
||||||
|
|
@ -375,11 +530,75 @@ packages:
|
||||||
supports-color:
|
supports-color:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
deep-is@0.1.4:
|
||||||
|
resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==}
|
||||||
|
|
||||||
esbuild@0.27.3:
|
esbuild@0.27.3:
|
||||||
resolution: {integrity: sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==}
|
resolution: {integrity: sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
escape-string-regexp@4.0.0:
|
||||||
|
resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==}
|
||||||
|
engines: {node: '>=10'}
|
||||||
|
|
||||||
|
eslint-config-prettier@10.1.8:
|
||||||
|
resolution: {integrity: sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==}
|
||||||
|
hasBin: true
|
||||||
|
peerDependencies:
|
||||||
|
eslint: '>=7.0.0'
|
||||||
|
|
||||||
|
eslint-scope@9.1.2:
|
||||||
|
resolution: {integrity: sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==}
|
||||||
|
engines: {node: ^20.19.0 || ^22.13.0 || >=24}
|
||||||
|
|
||||||
|
eslint-visitor-keys@3.4.3:
|
||||||
|
resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==}
|
||||||
|
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
|
||||||
|
|
||||||
|
eslint-visitor-keys@5.0.1:
|
||||||
|
resolution: {integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==}
|
||||||
|
engines: {node: ^20.19.0 || ^22.13.0 || >=24}
|
||||||
|
|
||||||
|
eslint@10.0.3:
|
||||||
|
resolution: {integrity: sha512-COV33RzXZkqhG9P2rZCFl9ZmJ7WL+gQSCRzE7RhkbclbQPtLAWReL7ysA0Sh4c8Im2U9ynybdR56PV0XcKvqaQ==}
|
||||||
|
engines: {node: ^20.19.0 || ^22.13.0 || >=24}
|
||||||
|
hasBin: true
|
||||||
|
peerDependencies:
|
||||||
|
jiti: '*'
|
||||||
|
peerDependenciesMeta:
|
||||||
|
jiti:
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
espree@11.2.0:
|
||||||
|
resolution: {integrity: sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==}
|
||||||
|
engines: {node: ^20.19.0 || ^22.13.0 || >=24}
|
||||||
|
|
||||||
|
esquery@1.7.0:
|
||||||
|
resolution: {integrity: sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==}
|
||||||
|
engines: {node: '>=0.10'}
|
||||||
|
|
||||||
|
esrecurse@4.3.0:
|
||||||
|
resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==}
|
||||||
|
engines: {node: '>=4.0'}
|
||||||
|
|
||||||
|
estraverse@5.3.0:
|
||||||
|
resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==}
|
||||||
|
engines: {node: '>=4.0'}
|
||||||
|
|
||||||
|
esutils@2.0.3:
|
||||||
|
resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==}
|
||||||
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
||||||
|
fast-deep-equal@3.1.3:
|
||||||
|
resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
|
||||||
|
|
||||||
|
fast-json-stable-stringify@2.1.0:
|
||||||
|
resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==}
|
||||||
|
|
||||||
|
fast-levenshtein@2.0.6:
|
||||||
|
resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==}
|
||||||
|
|
||||||
fdir@6.5.0:
|
fdir@6.5.0:
|
||||||
resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==}
|
resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==}
|
||||||
engines: {node: '>=12.0.0'}
|
engines: {node: '>=12.0.0'}
|
||||||
|
|
@ -389,18 +608,76 @@ packages:
|
||||||
picomatch:
|
picomatch:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
file-entry-cache@8.0.0:
|
||||||
|
resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==}
|
||||||
|
engines: {node: '>=16.0.0'}
|
||||||
|
|
||||||
|
find-up@5.0.0:
|
||||||
|
resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==}
|
||||||
|
engines: {node: '>=10'}
|
||||||
|
|
||||||
fix-dts-default-cjs-exports@1.0.1:
|
fix-dts-default-cjs-exports@1.0.1:
|
||||||
resolution: {integrity: sha512-pVIECanWFC61Hzl2+oOCtoJ3F17kglZC/6N94eRWycFgBH35hHx0Li604ZIzhseh97mf2p0cv7vVrOZGoqhlEg==}
|
resolution: {integrity: sha512-pVIECanWFC61Hzl2+oOCtoJ3F17kglZC/6N94eRWycFgBH35hHx0Li604ZIzhseh97mf2p0cv7vVrOZGoqhlEg==}
|
||||||
|
|
||||||
|
flat-cache@4.0.1:
|
||||||
|
resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==}
|
||||||
|
engines: {node: '>=16'}
|
||||||
|
|
||||||
|
flatted@3.3.4:
|
||||||
|
resolution: {integrity: sha512-3+mMldrTAPdta5kjX2G2J7iX4zxtnwpdA8Tr2ZSjkyPSanvbZAcy6flmtnXbEybHrDcU9641lxrMfFuUxVz9vA==}
|
||||||
|
|
||||||
fsevents@2.3.3:
|
fsevents@2.3.3:
|
||||||
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
|
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
|
||||||
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
|
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
|
||||||
os: [darwin]
|
os: [darwin]
|
||||||
|
|
||||||
|
glob-parent@6.0.2:
|
||||||
|
resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==}
|
||||||
|
engines: {node: '>=10.13.0'}
|
||||||
|
|
||||||
|
ignore@5.3.2:
|
||||||
|
resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==}
|
||||||
|
engines: {node: '>= 4'}
|
||||||
|
|
||||||
|
ignore@7.0.5:
|
||||||
|
resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==}
|
||||||
|
engines: {node: '>= 4'}
|
||||||
|
|
||||||
|
imurmurhash@0.1.4:
|
||||||
|
resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==}
|
||||||
|
engines: {node: '>=0.8.19'}
|
||||||
|
|
||||||
|
is-extglob@2.1.1:
|
||||||
|
resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==}
|
||||||
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
||||||
|
is-glob@4.0.3:
|
||||||
|
resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==}
|
||||||
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
||||||
|
isexe@2.0.0:
|
||||||
|
resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
|
||||||
|
|
||||||
joycon@3.1.1:
|
joycon@3.1.1:
|
||||||
resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==}
|
resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
|
|
||||||
|
json-buffer@3.0.1:
|
||||||
|
resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==}
|
||||||
|
|
||||||
|
json-schema-traverse@0.4.1:
|
||||||
|
resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==}
|
||||||
|
|
||||||
|
json-stable-stringify-without-jsonify@1.0.1:
|
||||||
|
resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==}
|
||||||
|
|
||||||
|
keyv@4.5.4:
|
||||||
|
resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==}
|
||||||
|
|
||||||
|
levn@0.4.1:
|
||||||
|
resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==}
|
||||||
|
engines: {node: '>= 0.8.0'}
|
||||||
|
|
||||||
lilconfig@3.1.3:
|
lilconfig@3.1.3:
|
||||||
resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==}
|
resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==}
|
||||||
engines: {node: '>=14'}
|
engines: {node: '>=14'}
|
||||||
|
|
@ -412,9 +689,17 @@ packages:
|
||||||
resolution: {integrity: sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==}
|
resolution: {integrity: sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==}
|
||||||
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
|
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
|
||||||
|
|
||||||
|
locate-path@6.0.0:
|
||||||
|
resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==}
|
||||||
|
engines: {node: '>=10'}
|
||||||
|
|
||||||
magic-string@0.30.21:
|
magic-string@0.30.21:
|
||||||
resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
|
resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
|
||||||
|
|
||||||
|
minimatch@10.2.4:
|
||||||
|
resolution: {integrity: sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==}
|
||||||
|
engines: {node: 18 || 20 || >=22}
|
||||||
|
|
||||||
mlly@1.8.0:
|
mlly@1.8.0:
|
||||||
resolution: {integrity: sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g==}
|
resolution: {integrity: sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g==}
|
||||||
|
|
||||||
|
|
@ -424,10 +709,33 @@ packages:
|
||||||
mz@2.7.0:
|
mz@2.7.0:
|
||||||
resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==}
|
resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==}
|
||||||
|
|
||||||
|
natural-compare@1.4.0:
|
||||||
|
resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
|
||||||
|
|
||||||
object-assign@4.1.1:
|
object-assign@4.1.1:
|
||||||
resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==}
|
resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
||||||
|
optionator@0.9.4:
|
||||||
|
resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==}
|
||||||
|
engines: {node: '>= 0.8.0'}
|
||||||
|
|
||||||
|
p-limit@3.1.0:
|
||||||
|
resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==}
|
||||||
|
engines: {node: '>=10'}
|
||||||
|
|
||||||
|
p-locate@5.0.0:
|
||||||
|
resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==}
|
||||||
|
engines: {node: '>=10'}
|
||||||
|
|
||||||
|
path-exists@4.0.0:
|
||||||
|
resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
|
path-key@3.1.1:
|
||||||
|
resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
pathe@2.0.3:
|
pathe@2.0.3:
|
||||||
resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==}
|
resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==}
|
||||||
|
|
||||||
|
|
@ -463,6 +771,19 @@ packages:
|
||||||
yaml:
|
yaml:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
prelude-ls@1.2.1:
|
||||||
|
resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
|
||||||
|
engines: {node: '>= 0.8.0'}
|
||||||
|
|
||||||
|
prettier@3.8.1:
|
||||||
|
resolution: {integrity: sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==}
|
||||||
|
engines: {node: '>=14'}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
|
punycode@2.3.1:
|
||||||
|
resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
|
||||||
|
engines: {node: '>=6'}
|
||||||
|
|
||||||
readdirp@4.1.2:
|
readdirp@4.1.2:
|
||||||
resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==}
|
resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==}
|
||||||
engines: {node: '>= 14.18.0'}
|
engines: {node: '>= 14.18.0'}
|
||||||
|
|
@ -476,6 +797,19 @@ packages:
|
||||||
engines: {node: '>=18.0.0', npm: '>=8.0.0'}
|
engines: {node: '>=18.0.0', npm: '>=8.0.0'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
semver@7.7.4:
|
||||||
|
resolution: {integrity: sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==}
|
||||||
|
engines: {node: '>=10'}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
|
shebang-command@2.0.0:
|
||||||
|
resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
|
shebang-regex@3.0.0:
|
||||||
|
resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
source-map@0.7.6:
|
source-map@0.7.6:
|
||||||
resolution: {integrity: sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==}
|
resolution: {integrity: sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==}
|
||||||
engines: {node: '>= 12'}
|
engines: {node: '>= 12'}
|
||||||
|
|
@ -503,6 +837,12 @@ packages:
|
||||||
resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==}
|
resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
ts-api-utils@2.4.0:
|
||||||
|
resolution: {integrity: sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==}
|
||||||
|
engines: {node: '>=18.12'}
|
||||||
|
peerDependencies:
|
||||||
|
typescript: '>=4.8.4'
|
||||||
|
|
||||||
ts-interface-checker@0.1.13:
|
ts-interface-checker@0.1.13:
|
||||||
resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==}
|
resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==}
|
||||||
|
|
||||||
|
|
@ -525,6 +865,17 @@ packages:
|
||||||
typescript:
|
typescript:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
type-check@0.4.0:
|
||||||
|
resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==}
|
||||||
|
engines: {node: '>= 0.8.0'}
|
||||||
|
|
||||||
|
typescript-eslint@8.56.1:
|
||||||
|
resolution: {integrity: sha512-U4lM6pjmBX7J5wk4szltF7I1cGBHXZopnAXCMXb3+fZ3B/0Z3hq3wS/CCUB2NZBNAExK92mCU2tEohWuwVMsDQ==}
|
||||||
|
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||||
|
peerDependencies:
|
||||||
|
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
|
||||||
|
typescript: '>=4.8.4 <6.0.0'
|
||||||
|
|
||||||
typescript@5.9.3:
|
typescript@5.9.3:
|
||||||
resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==}
|
resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==}
|
||||||
engines: {node: '>=14.17'}
|
engines: {node: '>=14.17'}
|
||||||
|
|
@ -536,6 +887,22 @@ packages:
|
||||||
undici-types@7.18.2:
|
undici-types@7.18.2:
|
||||||
resolution: {integrity: sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==}
|
resolution: {integrity: sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==}
|
||||||
|
|
||||||
|
uri-js@4.4.1:
|
||||||
|
resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
|
||||||
|
|
||||||
|
which@2.0.2:
|
||||||
|
resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==}
|
||||||
|
engines: {node: '>= 8'}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
|
word-wrap@1.2.5:
|
||||||
|
resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==}
|
||||||
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
||||||
|
yocto-queue@0.1.0:
|
||||||
|
resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
|
||||||
|
engines: {node: '>=10'}
|
||||||
|
|
||||||
snapshots:
|
snapshots:
|
||||||
|
|
||||||
'@esbuild/aix-ppc64@0.27.3':
|
'@esbuild/aix-ppc64@0.27.3':
|
||||||
|
|
@ -616,6 +983,51 @@ snapshots:
|
||||||
'@esbuild/win32-x64@0.27.3':
|
'@esbuild/win32-x64@0.27.3':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
'@eslint-community/eslint-utils@4.9.1(eslint@10.0.3)':
|
||||||
|
dependencies:
|
||||||
|
eslint: 10.0.3
|
||||||
|
eslint-visitor-keys: 3.4.3
|
||||||
|
|
||||||
|
'@eslint-community/regexpp@4.12.2': {}
|
||||||
|
|
||||||
|
'@eslint/config-array@0.23.3':
|
||||||
|
dependencies:
|
||||||
|
'@eslint/object-schema': 3.0.3
|
||||||
|
debug: 4.4.3
|
||||||
|
minimatch: 10.2.4
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- supports-color
|
||||||
|
|
||||||
|
'@eslint/config-helpers@0.5.3':
|
||||||
|
dependencies:
|
||||||
|
'@eslint/core': 1.1.1
|
||||||
|
|
||||||
|
'@eslint/core@1.1.1':
|
||||||
|
dependencies:
|
||||||
|
'@types/json-schema': 7.0.15
|
||||||
|
|
||||||
|
'@eslint/js@10.0.1(eslint@10.0.3)':
|
||||||
|
optionalDependencies:
|
||||||
|
eslint: 10.0.3
|
||||||
|
|
||||||
|
'@eslint/object-schema@3.0.3': {}
|
||||||
|
|
||||||
|
'@eslint/plugin-kit@0.6.1':
|
||||||
|
dependencies:
|
||||||
|
'@eslint/core': 1.1.1
|
||||||
|
levn: 0.4.1
|
||||||
|
|
||||||
|
'@humanfs/core@0.19.1': {}
|
||||||
|
|
||||||
|
'@humanfs/node@0.16.7':
|
||||||
|
dependencies:
|
||||||
|
'@humanfs/core': 0.19.1
|
||||||
|
'@humanwhocodes/retry': 0.4.3
|
||||||
|
|
||||||
|
'@humanwhocodes/module-importer@1.0.1': {}
|
||||||
|
|
||||||
|
'@humanwhocodes/retry@0.4.3': {}
|
||||||
|
|
||||||
'@jridgewell/gen-mapping@0.3.13':
|
'@jridgewell/gen-mapping@0.3.13':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@jridgewell/sourcemap-codec': 1.5.5
|
'@jridgewell/sourcemap-codec': 1.5.5
|
||||||
|
|
@ -705,16 +1117,128 @@ snapshots:
|
||||||
'@rollup/rollup-win32-x64-msvc@4.59.0':
|
'@rollup/rollup-win32-x64-msvc@4.59.0':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
'@types/esrecurse@4.3.1': {}
|
||||||
|
|
||||||
'@types/estree@1.0.8': {}
|
'@types/estree@1.0.8': {}
|
||||||
|
|
||||||
|
'@types/json-schema@7.0.15': {}
|
||||||
|
|
||||||
'@types/node@25.3.0':
|
'@types/node@25.3.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
undici-types: 7.18.2
|
undici-types: 7.18.2
|
||||||
|
|
||||||
|
'@typescript-eslint/eslint-plugin@8.56.1(@typescript-eslint/parser@8.56.1(eslint@10.0.3)(typescript@5.9.3))(eslint@10.0.3)(typescript@5.9.3)':
|
||||||
|
dependencies:
|
||||||
|
'@eslint-community/regexpp': 4.12.2
|
||||||
|
'@typescript-eslint/parser': 8.56.1(eslint@10.0.3)(typescript@5.9.3)
|
||||||
|
'@typescript-eslint/scope-manager': 8.56.1
|
||||||
|
'@typescript-eslint/type-utils': 8.56.1(eslint@10.0.3)(typescript@5.9.3)
|
||||||
|
'@typescript-eslint/utils': 8.56.1(eslint@10.0.3)(typescript@5.9.3)
|
||||||
|
'@typescript-eslint/visitor-keys': 8.56.1
|
||||||
|
eslint: 10.0.3
|
||||||
|
ignore: 7.0.5
|
||||||
|
natural-compare: 1.4.0
|
||||||
|
ts-api-utils: 2.4.0(typescript@5.9.3)
|
||||||
|
typescript: 5.9.3
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- supports-color
|
||||||
|
|
||||||
|
'@typescript-eslint/parser@8.56.1(eslint@10.0.3)(typescript@5.9.3)':
|
||||||
|
dependencies:
|
||||||
|
'@typescript-eslint/scope-manager': 8.56.1
|
||||||
|
'@typescript-eslint/types': 8.56.1
|
||||||
|
'@typescript-eslint/typescript-estree': 8.56.1(typescript@5.9.3)
|
||||||
|
'@typescript-eslint/visitor-keys': 8.56.1
|
||||||
|
debug: 4.4.3
|
||||||
|
eslint: 10.0.3
|
||||||
|
typescript: 5.9.3
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- supports-color
|
||||||
|
|
||||||
|
'@typescript-eslint/project-service@8.56.1(typescript@5.9.3)':
|
||||||
|
dependencies:
|
||||||
|
'@typescript-eslint/tsconfig-utils': 8.56.1(typescript@5.9.3)
|
||||||
|
'@typescript-eslint/types': 8.56.1
|
||||||
|
debug: 4.4.3
|
||||||
|
typescript: 5.9.3
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- supports-color
|
||||||
|
|
||||||
|
'@typescript-eslint/scope-manager@8.56.1':
|
||||||
|
dependencies:
|
||||||
|
'@typescript-eslint/types': 8.56.1
|
||||||
|
'@typescript-eslint/visitor-keys': 8.56.1
|
||||||
|
|
||||||
|
'@typescript-eslint/tsconfig-utils@8.56.1(typescript@5.9.3)':
|
||||||
|
dependencies:
|
||||||
|
typescript: 5.9.3
|
||||||
|
|
||||||
|
'@typescript-eslint/type-utils@8.56.1(eslint@10.0.3)(typescript@5.9.3)':
|
||||||
|
dependencies:
|
||||||
|
'@typescript-eslint/types': 8.56.1
|
||||||
|
'@typescript-eslint/typescript-estree': 8.56.1(typescript@5.9.3)
|
||||||
|
'@typescript-eslint/utils': 8.56.1(eslint@10.0.3)(typescript@5.9.3)
|
||||||
|
debug: 4.4.3
|
||||||
|
eslint: 10.0.3
|
||||||
|
ts-api-utils: 2.4.0(typescript@5.9.3)
|
||||||
|
typescript: 5.9.3
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- supports-color
|
||||||
|
|
||||||
|
'@typescript-eslint/types@8.56.1': {}
|
||||||
|
|
||||||
|
'@typescript-eslint/typescript-estree@8.56.1(typescript@5.9.3)':
|
||||||
|
dependencies:
|
||||||
|
'@typescript-eslint/project-service': 8.56.1(typescript@5.9.3)
|
||||||
|
'@typescript-eslint/tsconfig-utils': 8.56.1(typescript@5.9.3)
|
||||||
|
'@typescript-eslint/types': 8.56.1
|
||||||
|
'@typescript-eslint/visitor-keys': 8.56.1
|
||||||
|
debug: 4.4.3
|
||||||
|
minimatch: 10.2.4
|
||||||
|
semver: 7.7.4
|
||||||
|
tinyglobby: 0.2.15
|
||||||
|
ts-api-utils: 2.4.0(typescript@5.9.3)
|
||||||
|
typescript: 5.9.3
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- supports-color
|
||||||
|
|
||||||
|
'@typescript-eslint/utils@8.56.1(eslint@10.0.3)(typescript@5.9.3)':
|
||||||
|
dependencies:
|
||||||
|
'@eslint-community/eslint-utils': 4.9.1(eslint@10.0.3)
|
||||||
|
'@typescript-eslint/scope-manager': 8.56.1
|
||||||
|
'@typescript-eslint/types': 8.56.1
|
||||||
|
'@typescript-eslint/typescript-estree': 8.56.1(typescript@5.9.3)
|
||||||
|
eslint: 10.0.3
|
||||||
|
typescript: 5.9.3
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- supports-color
|
||||||
|
|
||||||
|
'@typescript-eslint/visitor-keys@8.56.1':
|
||||||
|
dependencies:
|
||||||
|
'@typescript-eslint/types': 8.56.1
|
||||||
|
eslint-visitor-keys: 5.0.1
|
||||||
|
|
||||||
|
acorn-jsx@5.3.2(acorn@8.16.0):
|
||||||
|
dependencies:
|
||||||
|
acorn: 8.16.0
|
||||||
|
|
||||||
acorn@8.16.0: {}
|
acorn@8.16.0: {}
|
||||||
|
|
||||||
|
ajv@6.14.0:
|
||||||
|
dependencies:
|
||||||
|
fast-deep-equal: 3.1.3
|
||||||
|
fast-json-stable-stringify: 2.1.0
|
||||||
|
json-schema-traverse: 0.4.1
|
||||||
|
uri-js: 4.4.1
|
||||||
|
|
||||||
any-promise@1.3.0: {}
|
any-promise@1.3.0: {}
|
||||||
|
|
||||||
|
balanced-match@4.0.4: {}
|
||||||
|
|
||||||
|
brace-expansion@5.0.4:
|
||||||
|
dependencies:
|
||||||
|
balanced-match: 4.0.4
|
||||||
|
|
||||||
bundle-require@5.1.0(esbuild@0.27.3):
|
bundle-require@5.1.0(esbuild@0.27.3):
|
||||||
dependencies:
|
dependencies:
|
||||||
esbuild: 0.27.3
|
esbuild: 0.27.3
|
||||||
|
|
@ -732,10 +1256,18 @@ snapshots:
|
||||||
|
|
||||||
consola@3.4.2: {}
|
consola@3.4.2: {}
|
||||||
|
|
||||||
|
cross-spawn@7.0.6:
|
||||||
|
dependencies:
|
||||||
|
path-key: 3.1.1
|
||||||
|
shebang-command: 2.0.0
|
||||||
|
which: 2.0.2
|
||||||
|
|
||||||
debug@4.4.3:
|
debug@4.4.3:
|
||||||
dependencies:
|
dependencies:
|
||||||
ms: 2.1.3
|
ms: 2.1.3
|
||||||
|
|
||||||
|
deep-is@0.1.4: {}
|
||||||
|
|
||||||
esbuild@0.27.3:
|
esbuild@0.27.3:
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@esbuild/aix-ppc64': 0.27.3
|
'@esbuild/aix-ppc64': 0.27.3
|
||||||
|
|
@ -765,31 +1297,164 @@ snapshots:
|
||||||
'@esbuild/win32-ia32': 0.27.3
|
'@esbuild/win32-ia32': 0.27.3
|
||||||
'@esbuild/win32-x64': 0.27.3
|
'@esbuild/win32-x64': 0.27.3
|
||||||
|
|
||||||
|
escape-string-regexp@4.0.0: {}
|
||||||
|
|
||||||
|
eslint-config-prettier@10.1.8(eslint@10.0.3):
|
||||||
|
dependencies:
|
||||||
|
eslint: 10.0.3
|
||||||
|
|
||||||
|
eslint-scope@9.1.2:
|
||||||
|
dependencies:
|
||||||
|
'@types/esrecurse': 4.3.1
|
||||||
|
'@types/estree': 1.0.8
|
||||||
|
esrecurse: 4.3.0
|
||||||
|
estraverse: 5.3.0
|
||||||
|
|
||||||
|
eslint-visitor-keys@3.4.3: {}
|
||||||
|
|
||||||
|
eslint-visitor-keys@5.0.1: {}
|
||||||
|
|
||||||
|
eslint@10.0.3:
|
||||||
|
dependencies:
|
||||||
|
'@eslint-community/eslint-utils': 4.9.1(eslint@10.0.3)
|
||||||
|
'@eslint-community/regexpp': 4.12.2
|
||||||
|
'@eslint/config-array': 0.23.3
|
||||||
|
'@eslint/config-helpers': 0.5.3
|
||||||
|
'@eslint/core': 1.1.1
|
||||||
|
'@eslint/plugin-kit': 0.6.1
|
||||||
|
'@humanfs/node': 0.16.7
|
||||||
|
'@humanwhocodes/module-importer': 1.0.1
|
||||||
|
'@humanwhocodes/retry': 0.4.3
|
||||||
|
'@types/estree': 1.0.8
|
||||||
|
ajv: 6.14.0
|
||||||
|
cross-spawn: 7.0.6
|
||||||
|
debug: 4.4.3
|
||||||
|
escape-string-regexp: 4.0.0
|
||||||
|
eslint-scope: 9.1.2
|
||||||
|
eslint-visitor-keys: 5.0.1
|
||||||
|
espree: 11.2.0
|
||||||
|
esquery: 1.7.0
|
||||||
|
esutils: 2.0.3
|
||||||
|
fast-deep-equal: 3.1.3
|
||||||
|
file-entry-cache: 8.0.0
|
||||||
|
find-up: 5.0.0
|
||||||
|
glob-parent: 6.0.2
|
||||||
|
ignore: 5.3.2
|
||||||
|
imurmurhash: 0.1.4
|
||||||
|
is-glob: 4.0.3
|
||||||
|
json-stable-stringify-without-jsonify: 1.0.1
|
||||||
|
minimatch: 10.2.4
|
||||||
|
natural-compare: 1.4.0
|
||||||
|
optionator: 0.9.4
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- supports-color
|
||||||
|
|
||||||
|
espree@11.2.0:
|
||||||
|
dependencies:
|
||||||
|
acorn: 8.16.0
|
||||||
|
acorn-jsx: 5.3.2(acorn@8.16.0)
|
||||||
|
eslint-visitor-keys: 5.0.1
|
||||||
|
|
||||||
|
esquery@1.7.0:
|
||||||
|
dependencies:
|
||||||
|
estraverse: 5.3.0
|
||||||
|
|
||||||
|
esrecurse@4.3.0:
|
||||||
|
dependencies:
|
||||||
|
estraverse: 5.3.0
|
||||||
|
|
||||||
|
estraverse@5.3.0: {}
|
||||||
|
|
||||||
|
esutils@2.0.3: {}
|
||||||
|
|
||||||
|
fast-deep-equal@3.1.3: {}
|
||||||
|
|
||||||
|
fast-json-stable-stringify@2.1.0: {}
|
||||||
|
|
||||||
|
fast-levenshtein@2.0.6: {}
|
||||||
|
|
||||||
fdir@6.5.0(picomatch@4.0.3):
|
fdir@6.5.0(picomatch@4.0.3):
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
picomatch: 4.0.3
|
picomatch: 4.0.3
|
||||||
|
|
||||||
|
file-entry-cache@8.0.0:
|
||||||
|
dependencies:
|
||||||
|
flat-cache: 4.0.1
|
||||||
|
|
||||||
|
find-up@5.0.0:
|
||||||
|
dependencies:
|
||||||
|
locate-path: 6.0.0
|
||||||
|
path-exists: 4.0.0
|
||||||
|
|
||||||
fix-dts-default-cjs-exports@1.0.1:
|
fix-dts-default-cjs-exports@1.0.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
magic-string: 0.30.21
|
magic-string: 0.30.21
|
||||||
mlly: 1.8.0
|
mlly: 1.8.0
|
||||||
rollup: 4.59.0
|
rollup: 4.59.0
|
||||||
|
|
||||||
|
flat-cache@4.0.1:
|
||||||
|
dependencies:
|
||||||
|
flatted: 3.3.4
|
||||||
|
keyv: 4.5.4
|
||||||
|
|
||||||
|
flatted@3.3.4: {}
|
||||||
|
|
||||||
fsevents@2.3.3:
|
fsevents@2.3.3:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
glob-parent@6.0.2:
|
||||||
|
dependencies:
|
||||||
|
is-glob: 4.0.3
|
||||||
|
|
||||||
|
ignore@5.3.2: {}
|
||||||
|
|
||||||
|
ignore@7.0.5: {}
|
||||||
|
|
||||||
|
imurmurhash@0.1.4: {}
|
||||||
|
|
||||||
|
is-extglob@2.1.1: {}
|
||||||
|
|
||||||
|
is-glob@4.0.3:
|
||||||
|
dependencies:
|
||||||
|
is-extglob: 2.1.1
|
||||||
|
|
||||||
|
isexe@2.0.0: {}
|
||||||
|
|
||||||
joycon@3.1.1: {}
|
joycon@3.1.1: {}
|
||||||
|
|
||||||
|
json-buffer@3.0.1: {}
|
||||||
|
|
||||||
|
json-schema-traverse@0.4.1: {}
|
||||||
|
|
||||||
|
json-stable-stringify-without-jsonify@1.0.1: {}
|
||||||
|
|
||||||
|
keyv@4.5.4:
|
||||||
|
dependencies:
|
||||||
|
json-buffer: 3.0.1
|
||||||
|
|
||||||
|
levn@0.4.1:
|
||||||
|
dependencies:
|
||||||
|
prelude-ls: 1.2.1
|
||||||
|
type-check: 0.4.0
|
||||||
|
|
||||||
lilconfig@3.1.3: {}
|
lilconfig@3.1.3: {}
|
||||||
|
|
||||||
lines-and-columns@1.2.4: {}
|
lines-and-columns@1.2.4: {}
|
||||||
|
|
||||||
load-tsconfig@0.2.5: {}
|
load-tsconfig@0.2.5: {}
|
||||||
|
|
||||||
|
locate-path@6.0.0:
|
||||||
|
dependencies:
|
||||||
|
p-locate: 5.0.0
|
||||||
|
|
||||||
magic-string@0.30.21:
|
magic-string@0.30.21:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@jridgewell/sourcemap-codec': 1.5.5
|
'@jridgewell/sourcemap-codec': 1.5.5
|
||||||
|
|
||||||
|
minimatch@10.2.4:
|
||||||
|
dependencies:
|
||||||
|
brace-expansion: 5.0.4
|
||||||
|
|
||||||
mlly@1.8.0:
|
mlly@1.8.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
acorn: 8.16.0
|
acorn: 8.16.0
|
||||||
|
|
@ -805,8 +1470,31 @@ snapshots:
|
||||||
object-assign: 4.1.1
|
object-assign: 4.1.1
|
||||||
thenify-all: 1.6.0
|
thenify-all: 1.6.0
|
||||||
|
|
||||||
|
natural-compare@1.4.0: {}
|
||||||
|
|
||||||
object-assign@4.1.1: {}
|
object-assign@4.1.1: {}
|
||||||
|
|
||||||
|
optionator@0.9.4:
|
||||||
|
dependencies:
|
||||||
|
deep-is: 0.1.4
|
||||||
|
fast-levenshtein: 2.0.6
|
||||||
|
levn: 0.4.1
|
||||||
|
prelude-ls: 1.2.1
|
||||||
|
type-check: 0.4.0
|
||||||
|
word-wrap: 1.2.5
|
||||||
|
|
||||||
|
p-limit@3.1.0:
|
||||||
|
dependencies:
|
||||||
|
yocto-queue: 0.1.0
|
||||||
|
|
||||||
|
p-locate@5.0.0:
|
||||||
|
dependencies:
|
||||||
|
p-limit: 3.1.0
|
||||||
|
|
||||||
|
path-exists@4.0.0: {}
|
||||||
|
|
||||||
|
path-key@3.1.1: {}
|
||||||
|
|
||||||
pathe@2.0.3: {}
|
pathe@2.0.3: {}
|
||||||
|
|
||||||
picocolors@1.1.1: {}
|
picocolors@1.1.1: {}
|
||||||
|
|
@ -825,6 +1513,12 @@ snapshots:
|
||||||
dependencies:
|
dependencies:
|
||||||
lilconfig: 3.1.3
|
lilconfig: 3.1.3
|
||||||
|
|
||||||
|
prelude-ls@1.2.1: {}
|
||||||
|
|
||||||
|
prettier@3.8.1: {}
|
||||||
|
|
||||||
|
punycode@2.3.1: {}
|
||||||
|
|
||||||
readdirp@4.1.2: {}
|
readdirp@4.1.2: {}
|
||||||
|
|
||||||
resolve-from@5.0.0: {}
|
resolve-from@5.0.0: {}
|
||||||
|
|
@ -860,6 +1554,14 @@ snapshots:
|
||||||
'@rollup/rollup-win32-x64-msvc': 4.59.0
|
'@rollup/rollup-win32-x64-msvc': 4.59.0
|
||||||
fsevents: 2.3.3
|
fsevents: 2.3.3
|
||||||
|
|
||||||
|
semver@7.7.4: {}
|
||||||
|
|
||||||
|
shebang-command@2.0.0:
|
||||||
|
dependencies:
|
||||||
|
shebang-regex: 3.0.0
|
||||||
|
|
||||||
|
shebang-regex@3.0.0: {}
|
||||||
|
|
||||||
source-map@0.7.6: {}
|
source-map@0.7.6: {}
|
||||||
|
|
||||||
sucrase@3.35.1:
|
sucrase@3.35.1:
|
||||||
|
|
@ -889,6 +1591,10 @@ snapshots:
|
||||||
|
|
||||||
tree-kill@1.2.2: {}
|
tree-kill@1.2.2: {}
|
||||||
|
|
||||||
|
ts-api-utils@2.4.0(typescript@5.9.3):
|
||||||
|
dependencies:
|
||||||
|
typescript: 5.9.3
|
||||||
|
|
||||||
ts-interface-checker@0.1.13: {}
|
ts-interface-checker@0.1.13: {}
|
||||||
|
|
||||||
tsup@8.5.1(typescript@5.9.3):
|
tsup@8.5.1(typescript@5.9.3):
|
||||||
|
|
@ -918,8 +1624,35 @@ snapshots:
|
||||||
- tsx
|
- tsx
|
||||||
- yaml
|
- yaml
|
||||||
|
|
||||||
|
type-check@0.4.0:
|
||||||
|
dependencies:
|
||||||
|
prelude-ls: 1.2.1
|
||||||
|
|
||||||
|
typescript-eslint@8.56.1(eslint@10.0.3)(typescript@5.9.3):
|
||||||
|
dependencies:
|
||||||
|
'@typescript-eslint/eslint-plugin': 8.56.1(@typescript-eslint/parser@8.56.1(eslint@10.0.3)(typescript@5.9.3))(eslint@10.0.3)(typescript@5.9.3)
|
||||||
|
'@typescript-eslint/parser': 8.56.1(eslint@10.0.3)(typescript@5.9.3)
|
||||||
|
'@typescript-eslint/typescript-estree': 8.56.1(typescript@5.9.3)
|
||||||
|
'@typescript-eslint/utils': 8.56.1(eslint@10.0.3)(typescript@5.9.3)
|
||||||
|
eslint: 10.0.3
|
||||||
|
typescript: 5.9.3
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- supports-color
|
||||||
|
|
||||||
typescript@5.9.3: {}
|
typescript@5.9.3: {}
|
||||||
|
|
||||||
ufo@1.6.3: {}
|
ufo@1.6.3: {}
|
||||||
|
|
||||||
undici-types@7.18.2: {}
|
undici-types@7.18.2: {}
|
||||||
|
|
||||||
|
uri-js@4.4.1:
|
||||||
|
dependencies:
|
||||||
|
punycode: 2.3.1
|
||||||
|
|
||||||
|
which@2.0.2:
|
||||||
|
dependencies:
|
||||||
|
isexe: 2.0.0
|
||||||
|
|
||||||
|
word-wrap@1.2.5: {}
|
||||||
|
|
||||||
|
yocto-queue@0.1.0: {}
|
||||||
|
|
|
||||||
221
src/api/index.ts
221
src/api/index.ts
|
|
@ -31,11 +31,7 @@ import type {
|
||||||
} from '../types.js'
|
} from '../types.js'
|
||||||
import { ODEH_THRESHOLDS, ODEH_DESCRIPTIONS } from '../types.js'
|
import { ODEH_THRESHOLDS, ODEH_DESCRIPTIONS } from '../types.js'
|
||||||
import { SpkKernel } from '../spk/index.js'
|
import { SpkKernel } from '../spk/index.js'
|
||||||
import {
|
import { computeTimeScales, jdTTtoET, jdToDate, J2000 } from '../time/index.js'
|
||||||
computeTimeScales,
|
|
||||||
jdTTtoET,
|
|
||||||
J2000,
|
|
||||||
} from '../time/index.js'
|
|
||||||
import {
|
import {
|
||||||
getMoonGeocentricState,
|
getMoonGeocentricState,
|
||||||
getSunGeocentricState,
|
getSunGeocentricState,
|
||||||
|
|
@ -44,10 +40,7 @@ import {
|
||||||
getMoonSunApproximate,
|
getMoonSunApproximate,
|
||||||
nearestNewMoon,
|
nearestNewMoon,
|
||||||
} from '../bodies/index.js'
|
} from '../bodies/index.js'
|
||||||
import {
|
import { geodeticToECEF, computeAzAlt } from '../observer/index.js'
|
||||||
geodeticToECEF,
|
|
||||||
computeAzAlt,
|
|
||||||
} from '../observer/index.js'
|
|
||||||
import { itrsToGcrs, computeERA } from '../frames/index.js'
|
import { itrsToGcrs, computeERA } from '../frames/index.js'
|
||||||
import {
|
import {
|
||||||
getSunMoonEvents as eventsGetSunMoonEvents,
|
getSunMoonEvents as eventsGetSunMoonEvents,
|
||||||
|
|
@ -60,7 +53,34 @@ import {
|
||||||
computeYallop,
|
computeYallop,
|
||||||
computeOdeh,
|
computeOdeh,
|
||||||
buildGuidanceText,
|
buildGuidanceText,
|
||||||
|
arcvMinimum,
|
||||||
} from '../visibility/index.js'
|
} from '../visibility/index.js'
|
||||||
|
import { DEG2RAD } from '../math/index.js'
|
||||||
|
|
||||||
|
// ─── Input validation ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
function validateDate(date: Date, label: string): void {
|
||||||
|
if (!(date instanceof Date) || isNaN(date.getTime())) {
|
||||||
|
throw new RangeError(`${label}: expected a valid Date instance`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateLatitude(lat: number, label: string): void {
|
||||||
|
if (!isFinite(lat) || lat < -90 || lat > 90) {
|
||||||
|
throw new RangeError(`${label}: latitude must be a finite number in [-90, 90], got ${lat}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateLongitude(lon: number, label: string): void {
|
||||||
|
if (!isFinite(lon) || lon < -180 || lon > 180) {
|
||||||
|
throw new RangeError(`${label}: longitude must be a finite number in [-180, 180], got ${lon}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateObserver(observer: Observer, label: string): void {
|
||||||
|
validateLatitude(observer.lat, label)
|
||||||
|
validateLongitude(observer.lon, label)
|
||||||
|
}
|
||||||
|
|
||||||
// ─── Module-level kernel singleton ─────────────────────────────────────────────
|
// ─── Module-level kernel singleton ─────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
@ -83,7 +103,7 @@ function resolveCacheDir(override?: string): string {
|
||||||
// ─── Download sources ─────────────────────────────────────────────────────────
|
// ─── Download sources ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
const NAIF_DE442S_URL = 'https://naif.jpl.nasa.gov/pub/naif/generic_kernels/spk/planets/de442s.bsp'
|
const NAIF_DE442S_URL = 'https://naif.jpl.nasa.gov/pub/naif/generic_kernels/spk/planets/de442s.bsp'
|
||||||
const NAIF_LSK_URL = 'https://naif.jpl.nasa.gov/pub/naif/generic_kernels/lsk/naif0012.tls'
|
const NAIF_LSK_URL = 'https://naif.jpl.nasa.gov/pub/naif/generic_kernels/lsk/naif0012.tls'
|
||||||
|
|
||||||
// ─── Kernel lifecycle ─────────────────────────────────────────────────────────
|
// ─── Kernel lifecycle ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
@ -109,7 +129,8 @@ export async function initKernels(config?: KernelConfig): Promise<void> {
|
||||||
buffer = source.data
|
buffer = source.data
|
||||||
} else if (source.type === 'url') {
|
} else if (source.type === 'url') {
|
||||||
const res = await fetch(source.url)
|
const res = await fetch(source.url)
|
||||||
if (!res.ok) throw new Error(`Failed to fetch kernel from ${source.url}: ${res.status} ${res.statusText}`)
|
if (!res.ok)
|
||||||
|
throw new Error(`Failed to fetch kernel from ${source.url}: ${res.status} ${res.statusText}`)
|
||||||
buffer = await res.arrayBuffer()
|
buffer = await res.arrayBuffer()
|
||||||
} else {
|
} else {
|
||||||
// auto: download to local cache, then load
|
// auto: download to local cache, then load
|
||||||
|
|
@ -146,7 +167,7 @@ export async function downloadKernels(config?: KernelConfig): Promise<{
|
||||||
|
|
||||||
await mkdir(cacheDir, { recursive: true })
|
await mkdir(cacheDir, { recursive: true })
|
||||||
|
|
||||||
const planetaryPath = join(cacheDir, 'de442s.bsp')
|
const planetaryPath = join(cacheDir, 'de442s.bsp')
|
||||||
const leapSecondsPath = join(cacheDir, 'naif0012.tls')
|
const leapSecondsPath = join(cacheDir, 'naif0012.tls')
|
||||||
|
|
||||||
if (!existsSync(planetaryPath)) {
|
if (!existsSync(planetaryPath)) {
|
||||||
|
|
@ -206,7 +227,7 @@ export async function verifyKernels(config?: KernelConfig): Promise<{
|
||||||
const { join } = await import('node:path')
|
const { join } = await import('node:path')
|
||||||
const errors: string[] = []
|
const errors: string[] = []
|
||||||
|
|
||||||
const planetaryPath = join(cacheDir, 'de442s.bsp')
|
const planetaryPath = join(cacheDir, 'de442s.bsp')
|
||||||
const leapSecondsPath = join(cacheDir, 'naif0012.tls')
|
const leapSecondsPath = join(cacheDir, 'naif0012.tls')
|
||||||
|
|
||||||
if (!existsSync(planetaryPath)) {
|
if (!existsSync(planetaryPath)) {
|
||||||
|
|
@ -251,7 +272,8 @@ async function resolveKernel(config?: KernelConfig): Promise<SpkKernel> {
|
||||||
|
|
||||||
// auto-init as last resort
|
// auto-init as last resort
|
||||||
await initKernels(config)
|
await initKernels(config)
|
||||||
if (!activeKernel) throw new Error('Kernel failed to initialize. Call initKernels() before computing.')
|
if (!activeKernel)
|
||||||
|
throw new Error('Kernel failed to initialize. Call initKernels() before computing.')
|
||||||
return activeKernel
|
return activeKernel
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -286,6 +308,8 @@ export async function getMoonSightingReport(
|
||||||
observer: Observer,
|
observer: Observer,
|
||||||
options?: SightingOptions,
|
options?: SightingOptions,
|
||||||
): Promise<MoonSightingReport> {
|
): Promise<MoonSightingReport> {
|
||||||
|
validateDate(date, 'getMoonSightingReport')
|
||||||
|
validateObserver(observer, 'getMoonSightingReport')
|
||||||
const kernel = await resolveKernel(options?.kernels)
|
const kernel = await resolveKernel(options?.kernels)
|
||||||
|
|
||||||
// Event times (sunset, moonset, twilight, rise)
|
// Event times (sunset, moonset, twilight, rise)
|
||||||
|
|
@ -321,7 +345,7 @@ export async function getMoonSightingReport(
|
||||||
|
|
||||||
// Body positions in GCRS (geocentric)
|
// Body positions in GCRS (geocentric)
|
||||||
const moonGCRS = getMoonGeocentricState(kernel, et).position
|
const moonGCRS = getMoonGeocentricState(kernel, et).position
|
||||||
const sunGCRS = getSunGeocentricState(kernel, et).position
|
const sunGCRS = getSunGeocentricState(kernel, et).position
|
||||||
|
|
||||||
// Observer ITRS position (km) from geodetic coordinates
|
// Observer ITRS position (km) from geodetic coordinates
|
||||||
const obsECEF = geodeticToECEF(observer.lat, observer.lon, observer.elevation)
|
const obsECEF = geodeticToECEF(observer.lat, observer.lon, observer.elevation)
|
||||||
|
|
@ -333,7 +357,7 @@ export async function getMoonSightingReport(
|
||||||
|
|
||||||
// Airless alt/az — required by Yallop/Odeh criteria
|
// Airless alt/az — required by Yallop/Odeh criteria
|
||||||
const moonAirless = computeAzAlt(moonGCRS, observer, ts, true)
|
const moonAirless = computeAzAlt(moonGCRS, observer, ts, true)
|
||||||
const sunAirless = computeAzAlt(sunGCRS, observer, ts, true)
|
const sunAirless = computeAzAlt(sunGCRS, observer, ts, true)
|
||||||
// Apparent alt/az (with refraction) — for guidance text
|
// Apparent alt/az (with refraction) — for guidance text
|
||||||
const moonApparent = computeAzAlt(moonGCRS, observer, ts, false)
|
const moonApparent = computeAzAlt(moonGCRS, observer, ts, false)
|
||||||
|
|
||||||
|
|
@ -349,11 +373,7 @@ export async function getMoonSightingReport(
|
||||||
moonGCRS[1] - obsGCRS[1],
|
moonGCRS[1] - obsGCRS[1],
|
||||||
moonGCRS[2] - obsGCRS[2],
|
moonGCRS[2] - obsGCRS[2],
|
||||||
]
|
]
|
||||||
const sunTopo: Vec3 = [
|
const sunTopo: Vec3 = [sunGCRS[0] - obsGCRS[0], sunGCRS[1] - obsGCRS[1], sunGCRS[2] - obsGCRS[2]]
|
||||||
sunGCRS[0] - obsGCRS[0],
|
|
||||||
sunGCRS[1] - obsGCRS[1],
|
|
||||||
sunGCRS[2] - obsGCRS[2],
|
|
||||||
]
|
|
||||||
|
|
||||||
const geometry = computeCrescentGeometry(
|
const geometry = computeCrescentGeometry(
|
||||||
moonAirless,
|
moonAirless,
|
||||||
|
|
@ -366,7 +386,7 @@ export async function getMoonSightingReport(
|
||||||
|
|
||||||
const { Wprime } = computeCrescentWidth(moonTopo, geometry.ARCL)
|
const { Wprime } = computeCrescentWidth(moonTopo, geometry.ARCL)
|
||||||
const yallop = computeYallop(geometry, Wprime)
|
const yallop = computeYallop(geometry, Wprime)
|
||||||
const odeh = computeOdeh(geometry)
|
const odeh = computeOdeh(geometry)
|
||||||
|
|
||||||
const moonAboveHorizon = moonAirless.altitude > 0
|
const moonAboveHorizon = moonAirless.altitude > 0
|
||||||
const sightingPossible = moonAboveHorizon && lagMinutes > 0
|
const sightingPossible = moonAboveHorizon && lagMinutes > 0
|
||||||
|
|
@ -413,7 +433,7 @@ function buildNullReport(
|
||||||
return {
|
return {
|
||||||
date,
|
date,
|
||||||
observer,
|
observer,
|
||||||
sunsetUTC: events.sunsetUTC,
|
sunsetUTC: events.sunsetUTC,
|
||||||
moonsetUTC: events.moonsetUTC,
|
moonsetUTC: events.moonsetUTC,
|
||||||
lagMinutes: null,
|
lagMinutes: null,
|
||||||
bestTimeUTC: null,
|
bestTimeUTC: null,
|
||||||
|
|
@ -425,7 +445,8 @@ function buildNullReport(
|
||||||
geometry: null,
|
geometry: null,
|
||||||
yallop: null,
|
yallop: null,
|
||||||
odeh: null,
|
odeh: null,
|
||||||
guidance: 'Sighting not possible: sunset or moonset could not be determined for this date and location.',
|
guidance:
|
||||||
|
'Sighting not possible: sunset or moonset could not be determined for this date and location.',
|
||||||
ephemerisSource: source,
|
ephemerisSource: source,
|
||||||
moonAboveHorizon: null,
|
moonAboveHorizon: null,
|
||||||
sightingPossible,
|
sightingPossible,
|
||||||
|
|
@ -435,14 +456,14 @@ function buildNullReport(
|
||||||
// ─── Phase display lookup ──────────────────────────────────────────────────────
|
// ─── Phase display lookup ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
const PHASE_DISPLAY: Record<MoonPhaseName, { name: string; symbol: string }> = {
|
const PHASE_DISPLAY: Record<MoonPhaseName, { name: string; symbol: string }> = {
|
||||||
'new-moon': { name: 'New Moon', symbol: '🌑' },
|
'new-moon': { name: 'New Moon', symbol: '🌑' },
|
||||||
'waxing-crescent': { name: 'Waxing Crescent', symbol: '🌒' },
|
'waxing-crescent': { name: 'Waxing Crescent', symbol: '🌒' },
|
||||||
'first-quarter': { name: 'First Quarter', symbol: '🌓' },
|
'first-quarter': { name: 'First Quarter', symbol: '🌓' },
|
||||||
'waxing-gibbous': { name: 'Waxing Gibbous', symbol: '🌔' },
|
'waxing-gibbous': { name: 'Waxing Gibbous', symbol: '🌔' },
|
||||||
'full-moon': { name: 'Full Moon', symbol: '🌕' },
|
'full-moon': { name: 'Full Moon', symbol: '🌕' },
|
||||||
'waning-gibbous': { name: 'Waning Gibbous', symbol: '🌖' },
|
'waning-gibbous': { name: 'Waning Gibbous', symbol: '🌖' },
|
||||||
'last-quarter': { name: 'Last Quarter', symbol: '🌗' },
|
'last-quarter': { name: 'Last Quarter', symbol: '🌗' },
|
||||||
'waning-crescent': { name: 'Waning Crescent', symbol: '🌘' },
|
'waning-crescent': { name: 'Waning Crescent', symbol: '🌘' },
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -464,6 +485,7 @@ const PHASE_DISPLAY: Record<MoonPhaseName, { name: string; symbol: string }> = {
|
||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
export function getMoonPhase(date = new Date()): MoonPhaseResult {
|
export function getMoonPhase(date = new Date()): MoonPhaseResult {
|
||||||
|
validateDate(date, 'getMoonPhase')
|
||||||
const ts = computeTimeScales(date)
|
const ts = computeTimeScales(date)
|
||||||
const { moonGCRS, sunGCRS } = getMoonSunApproximate(ts.jdTT)
|
const { moonGCRS, sunGCRS } = getMoonSunApproximate(ts.jdTT)
|
||||||
|
|
||||||
|
|
@ -478,7 +500,7 @@ export function getMoonPhase(date = new Date()): MoonPhaseResult {
|
||||||
const phaseKey = elongationToPhase(elongationDeg, isWaxing)
|
const phaseKey = elongationToPhase(elongationDeg, isWaxing)
|
||||||
const { name: phaseName, symbol: phaseSymbol } = PHASE_DISPLAY[phaseKey]
|
const { name: phaseName, symbol: phaseSymbol } = PHASE_DISPLAY[phaseKey]
|
||||||
|
|
||||||
const nextNewMoonJD = nearestNewMoon(ts.jdTT + 15)
|
const nextNewMoonJD = nearestNewMoon(ts.jdTT + 15)
|
||||||
const nextFullMoonJD = nearestFullMoon(ts.jdTT)
|
const nextFullMoonJD = nearestFullMoon(ts.jdTT)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
@ -489,9 +511,9 @@ export function getMoonPhase(date = new Date()): MoonPhaseResult {
|
||||||
age,
|
age,
|
||||||
elongationDeg,
|
elongationDeg,
|
||||||
isWaxing,
|
isWaxing,
|
||||||
nextNewMoon: jdToJSDate(nextNewMoonJD),
|
nextNewMoon: jdToDate(nextNewMoonJD),
|
||||||
nextFullMoon: jdToJSDate(nextFullMoonJD),
|
nextFullMoon: jdToDate(nextFullMoonJD),
|
||||||
prevNewMoon: jdToJSDate(prevNewMoonJD),
|
prevNewMoon: jdToDate(prevNewMoonJD),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -520,7 +542,9 @@ export function getMoonPosition(
|
||||||
lon: number,
|
lon: number,
|
||||||
elevation = 0,
|
elevation = 0,
|
||||||
): MoonPosition {
|
): MoonPosition {
|
||||||
const DEG = Math.PI / 180
|
validateDate(date, 'getMoonPosition')
|
||||||
|
validateLatitude(lat, 'getMoonPosition')
|
||||||
|
validateLongitude(lon, 'getMoonPosition')
|
||||||
const ts = computeTimeScales(date)
|
const ts = computeTimeScales(date)
|
||||||
const { moonGCRS } = getMoonSunApproximate(ts.jdTT)
|
const { moonGCRS } = getMoonSunApproximate(ts.jdTT)
|
||||||
|
|
||||||
|
|
@ -532,17 +556,17 @@ export function getMoonPosition(
|
||||||
const distance = Math.sqrt(moonGCRS[0] ** 2 + moonGCRS[1] ** 2 + moonGCRS[2] ** 2)
|
const distance = Math.sqrt(moonGCRS[0] ** 2 + moonGCRS[1] ** 2 + moonGCRS[2] ** 2)
|
||||||
|
|
||||||
// Equatorial coordinates for parallactic angle
|
// Equatorial coordinates for parallactic angle
|
||||||
const RA_moon = Math.atan2(moonGCRS[1], moonGCRS[0])
|
const RA_moon = Math.atan2(moonGCRS[1], moonGCRS[0])
|
||||||
const dec_moon = Math.asin(Math.max(-1, Math.min(1, moonGCRS[2] / distance)))
|
const dec_moon = Math.asin(Math.max(-1, Math.min(1, moonGCRS[2] / distance)))
|
||||||
|
|
||||||
// Hour angle: ERA(UT1) + longitude − right ascension
|
// Hour angle: ERA(UT1) + longitude − right ascension
|
||||||
const era = computeERA(ts.jdUT1)
|
const era = computeERA(ts.jdUT1)
|
||||||
const HA = era + lon * DEG - RA_moon
|
const HA = era + lon * DEG2RAD - RA_moon
|
||||||
|
|
||||||
// Parallactic angle: signed angle between zenith and north pole as seen from the Moon
|
// Parallactic angle: signed angle between zenith and north pole as seen from the Moon
|
||||||
const parallacticAngle = Math.atan2(
|
const parallacticAngle = Math.atan2(
|
||||||
Math.sin(HA),
|
Math.sin(HA),
|
||||||
Math.cos(lat * DEG) * Math.tan(dec_moon) - Math.sin(lat * DEG) * Math.cos(HA),
|
Math.cos(lat * DEG2RAD) * Math.tan(dec_moon) - Math.sin(lat * DEG2RAD) * Math.cos(HA),
|
||||||
)
|
)
|
||||||
|
|
||||||
return { azimuth: azAlt.azimuth, altitude: azAlt.altitude, distance, parallacticAngle }
|
return { azimuth: azAlt.azimuth, altitude: azAlt.altitude, distance, parallacticAngle }
|
||||||
|
|
@ -566,6 +590,7 @@ export function getMoonPosition(
|
||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
export function getMoonIllumination(date: Date = new Date()): MoonIlluminationResult {
|
export function getMoonIllumination(date: Date = new Date()): MoonIlluminationResult {
|
||||||
|
validateDate(date, 'getMoonIllumination')
|
||||||
const ts = computeTimeScales(date)
|
const ts = computeTimeScales(date)
|
||||||
const { moonGCRS, sunGCRS } = getMoonSunApproximate(ts.jdTT)
|
const { moonGCRS, sunGCRS } = getMoonSunApproximate(ts.jdTT)
|
||||||
|
|
||||||
|
|
@ -578,12 +603,12 @@ export function getMoonIllumination(date: Date = new Date()): MoonIlluminationRe
|
||||||
// PA = atan2(cos(dec_sun) * sin(RA_sun - RA_moon),
|
// PA = atan2(cos(dec_sun) * sin(RA_sun - RA_moon),
|
||||||
// sin(dec_sun) * cos(dec_moon) - cos(dec_sun) * sin(dec_moon) * cos(RA_sun - RA_moon))
|
// sin(dec_sun) * cos(dec_moon) - cos(dec_sun) * sin(dec_moon) * cos(RA_sun - RA_moon))
|
||||||
const moonDist = Math.sqrt(moonGCRS[0] ** 2 + moonGCRS[1] ** 2 + moonGCRS[2] ** 2)
|
const moonDist = Math.sqrt(moonGCRS[0] ** 2 + moonGCRS[1] ** 2 + moonGCRS[2] ** 2)
|
||||||
const sunDist = Math.sqrt(sunGCRS[0] ** 2 + sunGCRS[1] ** 2 + sunGCRS[2] ** 2)
|
const sunDist = Math.sqrt(sunGCRS[0] ** 2 + sunGCRS[1] ** 2 + sunGCRS[2] ** 2)
|
||||||
|
|
||||||
const RA_moon = Math.atan2(moonGCRS[1], moonGCRS[0])
|
const RA_moon = Math.atan2(moonGCRS[1], moonGCRS[0])
|
||||||
const dec_moon = Math.asin(Math.max(-1, Math.min(1, moonGCRS[2] / moonDist)))
|
const dec_moon = Math.asin(Math.max(-1, Math.min(1, moonGCRS[2] / moonDist)))
|
||||||
const RA_sun = Math.atan2(sunGCRS[1], sunGCRS[0])
|
const RA_sun = Math.atan2(sunGCRS[1], sunGCRS[0])
|
||||||
const dec_sun = Math.asin(Math.max(-1, Math.min(1, sunGCRS[2] / sunDist)))
|
const dec_sun = Math.asin(Math.max(-1, Math.min(1, sunGCRS[2] / sunDist)))
|
||||||
|
|
||||||
const dRA = RA_sun - RA_moon
|
const dRA = RA_sun - RA_moon
|
||||||
const angle = Math.atan2(
|
const angle = Math.atan2(
|
||||||
|
|
@ -625,13 +650,16 @@ export function getMoonVisibilityEstimate(
|
||||||
lon: number,
|
lon: number,
|
||||||
elevation = 0,
|
elevation = 0,
|
||||||
): MoonVisibilityEstimate {
|
): MoonVisibilityEstimate {
|
||||||
|
validateDate(date, 'getMoonVisibilityEstimate')
|
||||||
|
validateLatitude(lat, 'getMoonVisibilityEstimate')
|
||||||
|
validateLongitude(lon, 'getMoonVisibilityEstimate')
|
||||||
const ts = computeTimeScales(date)
|
const ts = computeTimeScales(date)
|
||||||
const { moonGCRS, sunGCRS } = getMoonSunApproximate(ts.jdTT)
|
const { moonGCRS, sunGCRS } = getMoonSunApproximate(ts.jdTT)
|
||||||
const observer: Observer = { lat, lon, elevation }
|
const observer: Observer = { lat, lon, elevation }
|
||||||
|
|
||||||
// Airless positions — Odeh uses airless altitudes (no refraction)
|
// Airless positions — Odeh uses airless altitudes (no refraction)
|
||||||
const moonAirless = computeAzAlt(moonGCRS, observer, ts, true)
|
const moonAirless = computeAzAlt(moonGCRS, observer, ts, true)
|
||||||
const sunAirless = computeAzAlt(sunGCRS, observer, ts, true)
|
const sunAirless = computeAzAlt(sunGCRS, observer, ts, true)
|
||||||
|
|
||||||
// ARCL = elongation (geocentric, degrees)
|
// ARCL = elongation (geocentric, degrees)
|
||||||
const { elongationDeg } = computeIllumination(moonGCRS, sunGCRS)
|
const { elongationDeg } = computeIllumination(moonGCRS, sunGCRS)
|
||||||
|
|
@ -652,14 +680,11 @@ export function getMoonVisibilityEstimate(
|
||||||
|
|
||||||
const { W } = computeCrescentWidth(moonTopo, ARCL)
|
const { W } = computeCrescentWidth(moonTopo, ARCL)
|
||||||
|
|
||||||
// Odeh 2006: V = ARCV - f(W), where f(W) = arcv_minimum polynomial
|
// Odeh 2006: V = ARCV - arcv_minimum(W)
|
||||||
const arcvMin = -0.1018 * W ** 3 + 0.7319 * W ** 2 - 6.3226 * W + 7.1651
|
const V = ARCV - arcvMinimum(W)
|
||||||
const V = ARCV - arcvMin
|
|
||||||
|
|
||||||
const zone: OdehZone = V >= ODEH_THRESHOLDS.A ? 'A'
|
const zone: OdehZone =
|
||||||
: V >= ODEH_THRESHOLDS.B ? 'B'
|
V >= ODEH_THRESHOLDS.A ? 'A' : V >= ODEH_THRESHOLDS.B ? 'B' : V >= ODEH_THRESHOLDS.C ? 'C' : 'D'
|
||||||
: V >= ODEH_THRESHOLDS.C ? 'C'
|
|
||||||
: 'D'
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
V,
|
V,
|
||||||
|
|
@ -705,21 +730,19 @@ export function getMoon(
|
||||||
lon: number,
|
lon: number,
|
||||||
elevation = 0,
|
elevation = 0,
|
||||||
): MoonSnapshot {
|
): MoonSnapshot {
|
||||||
|
validateDate(date, 'getMoon')
|
||||||
|
validateLatitude(lat, 'getMoon')
|
||||||
|
validateLongitude(lon, 'getMoon')
|
||||||
return {
|
return {
|
||||||
phase: getMoonPhase(date),
|
phase: getMoonPhase(date),
|
||||||
position: getMoonPosition(date, lat, lon, elevation),
|
position: getMoonPosition(date, lat, lon, elevation),
|
||||||
illumination: getMoonIllumination(date),
|
illumination: getMoonIllumination(date),
|
||||||
visibility: getMoonVisibilityEstimate(date, lat, lon, elevation),
|
visibility: getMoonVisibilityEstimate(date, lat, lon, elevation),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Internal helpers ─────────────────────────────────────────────────────────
|
// ─── Internal helpers ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
/** Convert JD to a UTC Date. */
|
|
||||||
function jdToJSDate(jd: number): Date {
|
|
||||||
return new Date((jd - 2440587.5) * 86400000)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Approximate the nearest full moon JD using Meeus Ch. 49 (full moon k = n + 0.5).
|
* Approximate the nearest full moon JD using Meeus Ch. 49 (full moon k = n + 0.5).
|
||||||
* Full moon corrections differ from new moon; these are from Meeus Table 49.A.
|
* Full moon corrections differ from new moon; these are from Meeus Table 49.A.
|
||||||
|
|
@ -740,46 +763,46 @@ function nearestFullMoon(jdTT: number): number {
|
||||||
/** Full moon JDE for a half-integer k (Meeus Ch. 49, Table 49.A). */
|
/** Full moon JDE for a half-integer k (Meeus Ch. 49, Table 49.A). */
|
||||||
function fullMoonJDE(k: number): number {
|
function fullMoonJDE(k: number): number {
|
||||||
const T = k / 1236.85
|
const T = k / 1236.85
|
||||||
const DEG = Math.PI / 180
|
|
||||||
|
|
||||||
let JDE = 2451550.09766
|
let JDE =
|
||||||
+ 29.530588861 * k
|
2451550.09766 +
|
||||||
+ 0.00015437 * T * T
|
29.530588861 * k +
|
||||||
- 0.000000150 * T * T * T
|
0.00015437 * T * T -
|
||||||
+ 0.00000000073 * T * T * T * T
|
0.00000015 * T * T * T +
|
||||||
|
0.00000000073 * T * T * T * T
|
||||||
|
|
||||||
const M = (2.5534 + 29.10535670 * k - 0.0000014 * T * T) * DEG
|
const M = (2.5534 + 29.1053567 * k - 0.0000014 * T * T) * DEG2RAD
|
||||||
const Mp = (201.5643 + 385.81693528 * k + 0.0107582 * T * T) * DEG
|
const Mp = (201.5643 + 385.81693528 * k + 0.0107582 * T * T) * DEG2RAD
|
||||||
const Fc = (160.7108 + 390.67050284 * k - 0.0016118 * T * T) * DEG
|
const Fc = (160.7108 + 390.67050284 * k - 0.0016118 * T * T) * DEG2RAD
|
||||||
const Om = (124.7746 - 1.56375588 * k + 0.0020672 * T * T) * DEG
|
const Om = (124.7746 - 1.56375588 * k + 0.0020672 * T * T) * DEG2RAD
|
||||||
const E = 1 - 0.002516 * T - 0.0000074 * T * T
|
const E = 1 - 0.002516 * T - 0.0000074 * T * T
|
||||||
|
|
||||||
JDE +=
|
JDE +=
|
||||||
-0.40614 * Math.sin(Mp)
|
-0.40614 * Math.sin(Mp) +
|
||||||
+ 0.17302 * E * Math.sin(M)
|
0.17302 * E * Math.sin(M) +
|
||||||
+ 0.01614 * Math.sin(2 * Mp)
|
0.01614 * Math.sin(2 * Mp) +
|
||||||
+ 0.01043 * Math.sin(2 * Fc)
|
0.01043 * Math.sin(2 * Fc) +
|
||||||
+ 0.00734 * E * Math.sin(Mp - M)
|
0.00734 * E * Math.sin(Mp - M) -
|
||||||
- 0.00515 * E * Math.sin(Mp + M)
|
0.00515 * E * Math.sin(Mp + M) +
|
||||||
+ 0.00209 * E * E * Math.sin(2 * M)
|
0.00209 * E * E * Math.sin(2 * M) -
|
||||||
- 0.00111 * Math.sin(Mp - 2 * Fc)
|
0.00111 * Math.sin(Mp - 2 * Fc) -
|
||||||
- 0.00057 * Math.sin(Mp + 2 * Fc)
|
0.00057 * Math.sin(Mp + 2 * Fc) +
|
||||||
+ 0.00056 * E * Math.sin(2 * Mp + M)
|
0.00056 * E * Math.sin(2 * Mp + M) -
|
||||||
- 0.00042 * Math.sin(3 * Mp)
|
0.00042 * Math.sin(3 * Mp) +
|
||||||
+ 0.00042 * E * Math.sin(M + 2 * Fc)
|
0.00042 * E * Math.sin(M + 2 * Fc) +
|
||||||
+ 0.00038 * E * Math.sin(M - 2 * Fc)
|
0.00038 * E * Math.sin(M - 2 * Fc) -
|
||||||
- 0.00024 * E * Math.sin(2 * Mp - M)
|
0.00024 * E * Math.sin(2 * Mp - M) -
|
||||||
- 0.00017 * Math.sin(Om)
|
0.00017 * Math.sin(Om) -
|
||||||
- 0.00007 * Math.sin(Mp + 2 * M)
|
0.00007 * Math.sin(Mp + 2 * M) +
|
||||||
+ 0.00004 * Math.sin(2 * Mp - 2 * Fc)
|
0.00004 * Math.sin(2 * Mp - 2 * Fc) +
|
||||||
+ 0.00004 * Math.sin(3 * M)
|
0.00004 * Math.sin(3 * M) +
|
||||||
+ 0.00003 * Math.sin(Mp + M - 2 * Fc)
|
0.00003 * Math.sin(Mp + M - 2 * Fc) +
|
||||||
+ 0.00003 * Math.sin(2 * Mp + 2 * Fc)
|
0.00003 * Math.sin(2 * Mp + 2 * Fc) -
|
||||||
- 0.00003 * Math.sin(Mp + M + 2 * Fc)
|
0.00003 * Math.sin(Mp + M + 2 * Fc) +
|
||||||
+ 0.00003 * Math.sin(Mp - M + 2 * Fc)
|
0.00003 * Math.sin(Mp - M + 2 * Fc) -
|
||||||
- 0.00002 * Math.sin(Mp - M - 2 * Fc)
|
0.00002 * Math.sin(Mp - M - 2 * Fc) -
|
||||||
- 0.00002 * Math.sin(3 * Mp + M)
|
0.00002 * Math.sin(3 * Mp + M) +
|
||||||
+ 0.00002 * Math.sin(4 * Mp)
|
0.00002 * Math.sin(4 * Mp)
|
||||||
|
|
||||||
return JDE
|
return JDE
|
||||||
}
|
}
|
||||||
|
|
@ -790,10 +813,10 @@ function fullMoonJDE(k: number): number {
|
||||||
*/
|
*/
|
||||||
function elongationToPhase(elongationDeg: number, isWaxing: boolean): MoonPhaseName {
|
function elongationToPhase(elongationDeg: number, isWaxing: boolean): MoonPhaseName {
|
||||||
const e = elongationDeg
|
const e = elongationDeg
|
||||||
if (e < 5) return 'new-moon'
|
if (e < 5) return 'new-moon'
|
||||||
if (e > 175) return 'full-moon'
|
if (e > 175) return 'full-moon'
|
||||||
if (e < 85) return isWaxing ? 'waxing-crescent' : 'waning-crescent'
|
if (e < 85) return isWaxing ? 'waxing-crescent' : 'waning-crescent'
|
||||||
if (e < 95) return isWaxing ? 'first-quarter' : 'last-quarter'
|
if (e < 95) return isWaxing ? 'first-quarter' : 'last-quarter'
|
||||||
return isWaxing ? 'waxing-gibbous' : 'waning-gibbous'
|
return isWaxing ? 'waxing-gibbous' : 'waning-gibbous'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -812,6 +835,8 @@ export async function getSunMoonEvents(
|
||||||
observer: Observer,
|
observer: Observer,
|
||||||
options?: Pick<SightingOptions, 'kernels'>,
|
options?: Pick<SightingOptions, 'kernels'>,
|
||||||
): Promise<SunMoonEvents> {
|
): Promise<SunMoonEvents> {
|
||||||
|
validateDate(date, 'getSunMoonEvents')
|
||||||
|
validateObserver(observer, 'getSunMoonEvents')
|
||||||
const kernel = await resolveKernel(options?.kernels)
|
const kernel = await resolveKernel(options?.kernels)
|
||||||
return eventsGetSunMoonEvents(date, observer, kernel)
|
return eventsGetSunMoonEvents(date, observer, kernel)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -20,17 +20,17 @@ import type { StateVector, Vec3 } from '../types.js'
|
||||||
import type { SpkKernel } from '../spk/index.js'
|
import type { SpkKernel } from '../spk/index.js'
|
||||||
import { NAIF_IDS } from '../spk/index.js'
|
import { NAIF_IDS } from '../spk/index.js'
|
||||||
import { J2000, DAYS_PER_JULIAN_CENTURY } from '../time/index.js'
|
import { J2000, DAYS_PER_JULIAN_CENTURY } from '../time/index.js'
|
||||||
|
import { DEG2RAD, vdot, vnorm } from '../math/index.js'
|
||||||
|
|
||||||
// ─── Constants ────────────────────────────────────────────────────────────────
|
// ─── Constants ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
const DEG = Math.PI / 180
|
|
||||||
const AU_KM = 149597870.7
|
const AU_KM = 149597870.7
|
||||||
|
|
||||||
/** Mean radius of the Moon in km (IAU 2015 nominal value) */
|
/** Mean radius of the Moon in km (IAU 2015 nominal value) */
|
||||||
const MOON_RADIUS_KM = 1737.4
|
const MOON_RADIUS_KM = 1737.4
|
||||||
|
|
||||||
/** Mean radius of the Sun in km */
|
/** Mean radius of the Sun in km */
|
||||||
const SUN_RADIUS_KM = 696000.0
|
const _SUN_RADIUS_KM = 696000.0
|
||||||
|
void _SUN_RADIUS_KM // reserved for future solar semi-diameter calculations
|
||||||
|
|
||||||
// ─── Geocentric state ─────────────────────────────────────────────────────────
|
// ─── Geocentric state ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
@ -85,15 +85,12 @@ export function computeIllumination(
|
||||||
moonGCRS: Vec3,
|
moonGCRS: Vec3,
|
||||||
sunGCRS: Vec3,
|
sunGCRS: Vec3,
|
||||||
): { illumination: number; phaseAngleDeg: number; elongationDeg: number; isWaxing: boolean } {
|
): { illumination: number; phaseAngleDeg: number; elongationDeg: number; isWaxing: boolean } {
|
||||||
const dot = (a: Vec3, b: Vec3) => a[0]*b[0] + a[1]*b[1] + a[2]*b[2]
|
const rMoon = vnorm(moonGCRS)
|
||||||
const norm = (v: Vec3) => Math.sqrt(dot(v, v))
|
const rSun = vnorm(sunGCRS)
|
||||||
|
|
||||||
const rMoon = norm(moonGCRS)
|
|
||||||
const rSun = norm(sunGCRS)
|
|
||||||
|
|
||||||
// Elongation ψ: angle at Earth between Moon and Sun
|
// Elongation ψ: angle at Earth between Moon and Sun
|
||||||
const cosElong = dot(moonGCRS, sunGCRS) / (rMoon * rSun)
|
const cosElong = vdot(moonGCRS, sunGCRS) / (rMoon * rSun)
|
||||||
const elongationDeg = Math.acos(Math.max(-1, Math.min(1, cosElong))) / DEG
|
const elongationDeg = Math.acos(Math.max(-1, Math.min(1, cosElong))) / DEG2RAD
|
||||||
|
|
||||||
// Phase angle i: angle at Moon between Earth and Sun
|
// Phase angle i: angle at Moon between Earth and Sun
|
||||||
// Vector from Moon to Earth: -moonGCRS
|
// Vector from Moon to Earth: -moonGCRS
|
||||||
|
|
@ -104,16 +101,16 @@ export function computeIllumination(
|
||||||
sunGCRS[2] - moonGCRS[2],
|
sunGCRS[2] - moonGCRS[2],
|
||||||
]
|
]
|
||||||
const moonToEarth: Vec3 = [-moonGCRS[0], -moonGCRS[1], -moonGCRS[2]]
|
const moonToEarth: Vec3 = [-moonGCRS[0], -moonGCRS[1], -moonGCRS[2]]
|
||||||
const rMoonToSun = norm(moonToSun)
|
const rMoonToSun = vnorm(moonToSun)
|
||||||
|
|
||||||
const cosPhase = dot(moonToEarth, moonToSun) / (rMoon * rMoonToSun)
|
const cosPhase = vdot(moonToEarth, moonToSun) / (rMoon * rMoonToSun)
|
||||||
const phaseAngleDeg = Math.acos(Math.max(-1, Math.min(1, cosPhase))) / DEG
|
const phaseAngleDeg = Math.acos(Math.max(-1, Math.min(1, cosPhase))) / DEG2RAD
|
||||||
|
|
||||||
const illumination = (1 + Math.cos(phaseAngleDeg * DEG)) / 2
|
const illumination = (1 + Math.cos(phaseAngleDeg * DEG2RAD)) / 2
|
||||||
|
|
||||||
// Moon is waxing when it is east of the Sun (elongation increasing).
|
// Moon is waxing when it is east of the Sun (elongation increasing).
|
||||||
// Cross product sunGCRS × moonGCRS z-component: positive when Moon is east of Sun.
|
// Cross product sunGCRS × moonGCRS z-component: positive when Moon is east of Sun.
|
||||||
const crossZ = sunGCRS[0]*moonGCRS[1] - sunGCRS[1]*moonGCRS[0]
|
const crossZ = sunGCRS[0] * moonGCRS[1] - sunGCRS[1] * moonGCRS[0]
|
||||||
const isWaxing = crossZ > 0
|
const isWaxing = crossZ > 0
|
||||||
|
|
||||||
return { illumination, phaseAngleDeg, elongationDeg, isWaxing }
|
return { illumination, phaseAngleDeg, elongationDeg, isWaxing }
|
||||||
|
|
@ -143,16 +140,13 @@ export function computeCrescentWidth(
|
||||||
moonTopoVec: Vec3,
|
moonTopoVec: Vec3,
|
||||||
ARCL: number,
|
ARCL: number,
|
||||||
): { W: number; Wprime: number } {
|
): { W: number; Wprime: number } {
|
||||||
|
const rMoon = Math.sqrt(moonTopoVec[0] ** 2 + moonTopoVec[1] ** 2 + moonTopoVec[2] ** 2)
|
||||||
const rMoon = Math.sqrt(
|
|
||||||
moonTopoVec[0]**2 + moonTopoVec[1]**2 + moonTopoVec[2]**2,
|
|
||||||
)
|
|
||||||
|
|
||||||
// Topocentric semi-diameter in arc minutes
|
// Topocentric semi-diameter in arc minutes
|
||||||
const SDmoon_arcmin = Math.atan(MOON_RADIUS_KM / rMoon) / DEG * 60
|
const SDmoon_arcmin = (Math.atan(MOON_RADIUS_KM / rMoon) / DEG2RAD) * 60
|
||||||
|
|
||||||
// Crescent width in arc minutes
|
// Crescent width in arc minutes
|
||||||
const ARCL_rad = ARCL * DEG
|
const ARCL_rad = ARCL * DEG2RAD
|
||||||
const W = SDmoon_arcmin * (1 - Math.cos(ARCL_rad))
|
const W = SDmoon_arcmin * (1 - Math.cos(ARCL_rad))
|
||||||
|
|
||||||
// Wprime ≡ W for both Odeh and Yallop in this formulation
|
// Wprime ≡ W for both Odeh and Yallop in this formulation
|
||||||
|
|
@ -185,30 +179,32 @@ export function getMoonSunApproximate(jdTT: number): {
|
||||||
// Mean longitude L0 and mean anomaly M (degrees)
|
// Mean longitude L0 and mean anomaly M (degrees)
|
||||||
const L0 = 280.46646 + 36000.76983 * T + 0.0003032 * T * T
|
const L0 = 280.46646 + 36000.76983 * T + 0.0003032 * T * T
|
||||||
const M_sun = 357.52911 + 35999.05029 * T - 0.0001537 * T * T
|
const M_sun = 357.52911 + 35999.05029 * T - 0.0001537 * T * T
|
||||||
const M_sun_rad = (M_sun % 360) * DEG
|
const M_sun_rad = (M_sun % 360) * DEG2RAD
|
||||||
|
|
||||||
const e_sun = 0.016708634 - 0.000042037 * T - 0.0000001267 * T * T
|
const e_sun = 0.016708634 - 0.000042037 * T - 0.0000001267 * T * T
|
||||||
|
|
||||||
// Equation of center (degrees)
|
// Equation of center (degrees)
|
||||||
const C = (1.914602 - 0.004817 * T - 0.000014 * T * T) * Math.sin(M_sun_rad)
|
const C =
|
||||||
+ (0.019993 - 0.000101 * T) * Math.sin(2 * M_sun_rad)
|
(1.914602 - 0.004817 * T - 0.000014 * T * T) * Math.sin(M_sun_rad) +
|
||||||
+ 0.000289 * Math.sin(3 * M_sun_rad)
|
(0.019993 - 0.000101 * T) * Math.sin(2 * M_sun_rad) +
|
||||||
|
0.000289 * Math.sin(3 * M_sun_rad)
|
||||||
|
|
||||||
// True longitude and anomaly
|
// True longitude and anomaly
|
||||||
const sunLonDeg = L0 + C
|
const sunLonDeg = L0 + C
|
||||||
const nu_rad = M_sun_rad + C * DEG
|
const nu_rad = M_sun_rad + C * DEG2RAD
|
||||||
|
|
||||||
// Geometric distance in AU
|
// Geometric distance in AU
|
||||||
const R_AU = 1.000001018 * (1 - e_sun * e_sun) / (1 + e_sun * Math.cos(nu_rad))
|
const R_AU = (1.000001018 * (1 - e_sun * e_sun)) / (1 + e_sun * Math.cos(nu_rad))
|
||||||
const R_km = R_AU * AU_KM
|
const R_km = R_AU * AU_KM
|
||||||
|
|
||||||
// Nutation correction for apparent longitude (simplified)
|
// Nutation correction for apparent longitude (simplified)
|
||||||
const omega = (125.04 - 1934.136 * T) * DEG
|
const omega = (125.04 - 1934.136 * T) * DEG2RAD
|
||||||
const sunLonApp = sunLonDeg - 0.00569 - 0.00478 * Math.sin(omega)
|
const sunLonApp = sunLonDeg - 0.00569 - 0.00478 * Math.sin(omega)
|
||||||
const sunLon_rad = sunLonApp * DEG
|
const sunLon_rad = sunLonApp * DEG2RAD
|
||||||
|
|
||||||
// Mean obliquity of the ecliptic (IAU 1980 approximation, degrees)
|
// Mean obliquity of the ecliptic (IAU 1980 approximation, degrees)
|
||||||
const eps = (23.439291111 - 0.013004167 * T - 0.0000001638 * T * T + 0.0000005036 * T * T * T) * DEG
|
const eps =
|
||||||
|
(23.439291111 - 0.013004167 * T - 0.0000001638 * T * T + 0.0000005036 * T * T * T) * DEG2RAD
|
||||||
|
|
||||||
const sunGCRS: Vec3 = [
|
const sunGCRS: Vec3 = [
|
||||||
R_km * Math.cos(sunLon_rad),
|
R_km * Math.cos(sunLon_rad),
|
||||||
|
|
@ -219,121 +215,151 @@ export function getMoonSunApproximate(jdTT: number): {
|
||||||
// ── Moon (Meeus Ch. 47) ─────────────────────────────────────────────────────
|
// ── Moon (Meeus Ch. 47) ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
// Fundamental arguments (degrees)
|
// Fundamental arguments (degrees)
|
||||||
const Lp = 218.3164477 + 481267.88123421 * T - 0.0015786 * T * T + T * T * T / 538841 - T * T * T * T / 65194000
|
const Lp =
|
||||||
const D = 297.8501921 + 445267.1114034 * T - 0.0018819 * T * T + T * T * T / 545868 - T * T * T * T / 113065000
|
218.3164477 +
|
||||||
const M = 357.5291092 + 35999.0502909 * T - 0.0001536 * T * T + T * T * T / 24490000
|
481267.88123421 * T -
|
||||||
const Mp = 134.9633964 + 477198.8675055 * T + 0.0087414 * T * T + T * T * T / 69699 - T * T * T * T / 14712000
|
0.0015786 * T * T +
|
||||||
const F = 93.2720950 + 483202.0175233 * T - 0.0036539 * T * T - T * T * T / 3526000 + T * T * T * T / 863310000
|
(T * T * T) / 538841 -
|
||||||
|
(T * T * T * T) / 65194000
|
||||||
|
const D =
|
||||||
|
297.8501921 +
|
||||||
|
445267.1114034 * T -
|
||||||
|
0.0018819 * T * T +
|
||||||
|
(T * T * T) / 545868 -
|
||||||
|
(T * T * T * T) / 113065000
|
||||||
|
const M = 357.5291092 + 35999.0502909 * T - 0.0001536 * T * T + (T * T * T) / 24490000
|
||||||
|
const Mp =
|
||||||
|
134.9633964 +
|
||||||
|
477198.8675055 * T +
|
||||||
|
0.0087414 * T * T +
|
||||||
|
(T * T * T) / 69699 -
|
||||||
|
(T * T * T * T) / 14712000
|
||||||
|
const F =
|
||||||
|
93.272095 +
|
||||||
|
483202.0175233 * T -
|
||||||
|
0.0036539 * T * T -
|
||||||
|
(T * T * T) / 3526000 +
|
||||||
|
(T * T * T * T) / 863310000
|
||||||
|
|
||||||
// Additive terms for longitude/latitude
|
// Additive terms for longitude/latitude
|
||||||
const A1 = (119.75 + 131.849 * T) * DEG
|
const A1 = (119.75 + 131.849 * T) * DEG2RAD
|
||||||
const A2 = ( 53.09 + 479264.290 * T) * DEG
|
const A2 = (53.09 + 479264.29 * T) * DEG2RAD
|
||||||
const A3 = (313.45 + 481266.484 * T) * DEG
|
const A3 = (313.45 + 481266.484 * T) * DEG2RAD
|
||||||
|
|
||||||
// Convert to radians for accumulation
|
// Convert to radians for accumulation
|
||||||
const D_r = (D % 360) * DEG
|
const D_r = (D % 360) * DEG2RAD
|
||||||
const M_r = (M % 360) * DEG
|
const M_r = (M % 360) * DEG2RAD
|
||||||
const Mp_r = (Mp % 360) * DEG
|
const Mp_r = (Mp % 360) * DEG2RAD
|
||||||
const F_r = (F % 360) * DEG
|
const F_r = (F % 360) * DEG2RAD
|
||||||
|
|
||||||
// Eccentricity correction for terms involving M (Earth's orbital eccentricity)
|
// Eccentricity correction for terms involving M (Earth's orbital eccentricity)
|
||||||
const E = 1 - 0.002516 * T - 0.0000074 * T * T
|
const E = 1 - 0.002516 * T - 0.0000074 * T * T
|
||||||
|
|
||||||
// Longitude and distance accumulation — 30 main terms from Meeus Table 47.A
|
// Longitude and distance accumulation — 30 main terms from Meeus Table 47.A
|
||||||
// [d, m, mp, f, Σl (0.000001°), Σr (0.001 km)]
|
// [d, m, mp, f, Σl (0.000001°), Σr (0.001 km)]
|
||||||
const LD: ReadonlyArray<readonly [number,number,number,number,number,number]> = [
|
const LD: ReadonlyArray<readonly [number, number, number, number, number, number]> = [
|
||||||
[ 0, 0, 1, 0, 6288774, -20905355],
|
[0, 0, 1, 0, 6288774, -20905355],
|
||||||
[ 2, 0,-1, 0, 1274027, -3699111],
|
[2, 0, -1, 0, 1274027, -3699111],
|
||||||
[ 2, 0, 0, 0, 658314, -2955968],
|
[2, 0, 0, 0, 658314, -2955968],
|
||||||
[ 0, 0, 2, 0, 213618, -569925],
|
[0, 0, 2, 0, 213618, -569925],
|
||||||
[ 0, 1, 0, 0, -185116, 48888],
|
[0, 1, 0, 0, -185116, 48888],
|
||||||
[ 0, 0, 0, 2, -114332, -3149],
|
[0, 0, 0, 2, -114332, -3149],
|
||||||
[ 2, 0,-2, 0, 58793, 246158],
|
[2, 0, -2, 0, 58793, 246158],
|
||||||
[ 2,-1,-1, 0, 57066, -152138],
|
[2, -1, -1, 0, 57066, -152138],
|
||||||
[ 2, 0, 1, 0, 53322, -170733],
|
[2, 0, 1, 0, 53322, -170733],
|
||||||
[ 2,-1, 0, 0, 45758, -204586],
|
[2, -1, 0, 0, 45758, -204586],
|
||||||
[ 0, 1,-1, 0, -40923, -129620],
|
[0, 1, -1, 0, -40923, -129620],
|
||||||
[ 1, 0, 0, 0, -34720, 108743],
|
[1, 0, 0, 0, -34720, 108743],
|
||||||
[ 0, 1, 1, 0, -30383, 104755],
|
[0, 1, 1, 0, -30383, 104755],
|
||||||
[ 2, 0, 0,-2, 15327, 10321],
|
[2, 0, 0, -2, 15327, 10321],
|
||||||
[ 0, 0, 1, 2, -12528, 0],
|
[0, 0, 1, 2, -12528, 0],
|
||||||
[ 0, 0, 1,-2, 10980, 79661],
|
[0, 0, 1, -2, 10980, 79661],
|
||||||
[ 4, 0,-1, 0, 10675, -34782],
|
[4, 0, -1, 0, 10675, -34782],
|
||||||
[ 0, 0, 3, 0, 10034, -23210],
|
[0, 0, 3, 0, 10034, -23210],
|
||||||
[ 4, 0,-2, 0, 8548, -21636],
|
[4, 0, -2, 0, 8548, -21636],
|
||||||
[ 2, 1,-1, 0, -7888, 24208],
|
[2, 1, -1, 0, -7888, 24208],
|
||||||
[ 2, 1, 0, 0, -6766, 30824],
|
[2, 1, 0, 0, -6766, 30824],
|
||||||
[ 1, 0,-1, 0, -5163, -8379],
|
[1, 0, -1, 0, -5163, -8379],
|
||||||
[ 1, 1, 0, 0, 4987, -16675],
|
[1, 1, 0, 0, 4987, -16675],
|
||||||
[ 2,-1, 1, 0, 4036, -12831],
|
[2, -1, 1, 0, 4036, -12831],
|
||||||
[ 2, 0, 2, 0, 3994, -10445],
|
[2, 0, 2, 0, 3994, -10445],
|
||||||
[ 4, 0, 0, 0, 3861, -11650],
|
[4, 0, 0, 0, 3861, -11650],
|
||||||
[ 2, 0,-3, 0, 3665, 14403],
|
[2, 0, -3, 0, 3665, 14403],
|
||||||
[ 0, 1,-2, 0, -2689, -7003],
|
[0, 1, -2, 0, -2689, -7003],
|
||||||
[ 2, 0,-1, 2, -2602, 0],
|
[2, 0, -1, 2, -2602, 0],
|
||||||
[ 2,-1,-2, 0, 2390, 10056],
|
[2, -1, -2, 0, 2390, 10056],
|
||||||
]
|
]
|
||||||
|
|
||||||
let Sl = 0, Sr = 0
|
let Sl = 0,
|
||||||
|
Sr = 0
|
||||||
for (const [d, m, mp, f, sl, sr] of LD) {
|
for (const [d, m, mp, f, sl, sr] of LD) {
|
||||||
const arg = d*D_r + m*M_r + mp*Mp_r + f*F_r
|
const arg = d * D_r + m * M_r + mp * Mp_r + f * F_r
|
||||||
const eCorr = Math.abs(m) === 2 ? E*E : Math.abs(m) === 1 ? E : 1
|
const eCorr = Math.abs(m) === 2 ? E * E : Math.abs(m) === 1 ? E : 1
|
||||||
Sl += sl * eCorr * Math.sin(arg)
|
Sl += sl * eCorr * Math.sin(arg)
|
||||||
Sr += sr * eCorr * Math.cos(arg)
|
Sr += sr * eCorr * Math.cos(arg)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Additive longitude corrections (Meeus §47)
|
// Additive longitude corrections (Meeus §47)
|
||||||
Sl += 3958 * Math.sin(A1) + 1962 * Math.sin((Lp - F) * DEG) + 318 * Math.sin(A2)
|
Sl += 3958 * Math.sin(A1) + 1962 * Math.sin((Lp - F) * DEG2RAD) + 318 * Math.sin(A2)
|
||||||
|
|
||||||
// Latitude accumulation — 20 main terms from Meeus Table 47.B
|
// Latitude accumulation — 20 main terms from Meeus Table 47.B
|
||||||
// [d, m, mp, f, Σb (0.000001°)]
|
// [d, m, mp, f, Σb (0.000001°)]
|
||||||
const FB: ReadonlyArray<readonly [number,number,number,number,number]> = [
|
const FB: ReadonlyArray<readonly [number, number, number, number, number]> = [
|
||||||
[ 0, 0, 0, 1, 5128122],
|
[0, 0, 0, 1, 5128122],
|
||||||
[ 0, 0, 1, 1, 280602],
|
[0, 0, 1, 1, 280602],
|
||||||
[ 0, 0, 1,-1, 277693],
|
[0, 0, 1, -1, 277693],
|
||||||
[ 2, 0, 0,-1, 173237],
|
[2, 0, 0, -1, 173237],
|
||||||
[ 2, 0,-1, 1, 55413],
|
[2, 0, -1, 1, 55413],
|
||||||
[ 2, 0,-1,-1, 46271],
|
[2, 0, -1, -1, 46271],
|
||||||
[ 2, 0, 0, 1, 32573],
|
[2, 0, 0, 1, 32573],
|
||||||
[ 0, 0, 2, 1, 17198],
|
[0, 0, 2, 1, 17198],
|
||||||
[ 2, 0, 1,-1, 9266],
|
[2, 0, 1, -1, 9266],
|
||||||
[ 0, 0, 2,-1, 8822],
|
[0, 0, 2, -1, 8822],
|
||||||
[ 2,-1, 0,-1, 8216],
|
[2, -1, 0, -1, 8216],
|
||||||
[ 2, 0,-2,-1, 4324],
|
[2, 0, -2, -1, 4324],
|
||||||
[ 2, 0, 1, 1, 4200],
|
[2, 0, 1, 1, 4200],
|
||||||
[ 2, 1, 0,-1, -3359],
|
[2, 1, 0, -1, -3359],
|
||||||
[ 2,-1,-1, 1, 2463],
|
[2, -1, -1, 1, 2463],
|
||||||
[ 2,-1, 0, 1, 2211],
|
[2, -1, 0, 1, 2211],
|
||||||
[ 2,-1,-1,-1, 2065],
|
[2, -1, -1, -1, 2065],
|
||||||
[ 0, 1,-1,-1, -1870],
|
[0, 1, -1, -1, -1870],
|
||||||
[ 4, 0,-1,-1, 1828],
|
[4, 0, -1, -1, 1828],
|
||||||
[ 0, 1, 0, 1, -1794],
|
[0, 1, 0, 1, -1794],
|
||||||
]
|
]
|
||||||
|
|
||||||
let Sb = 0
|
let Sb = 0
|
||||||
for (const [d, m, mp, f, sb] of FB) {
|
for (const [d, m, mp, f, sb] of FB) {
|
||||||
const arg = d*D_r + m*M_r + mp*Mp_r + f*F_r
|
const arg = d * D_r + m * M_r + mp * Mp_r + f * F_r
|
||||||
const eCorr = Math.abs(m) === 2 ? E*E : Math.abs(m) === 1 ? E : 1
|
const eCorr = Math.abs(m) === 2 ? E * E : Math.abs(m) === 1 ? E : 1
|
||||||
Sb += sb * eCorr * Math.sin(arg)
|
Sb += sb * eCorr * Math.sin(arg)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Additive latitude corrections
|
// Additive latitude corrections
|
||||||
Sb += -2235 * Math.sin(Lp * DEG) + 382 * Math.sin(A3) + 175 * Math.sin(A1 - F_r)
|
Sb +=
|
||||||
+ 175 * Math.sin(A1 + F_r) + 127 * Math.sin((Lp - Mp) * DEG) - 115 * Math.sin((Lp + Mp) * DEG)
|
-2235 * Math.sin(Lp * DEG2RAD) +
|
||||||
|
382 * Math.sin(A3) +
|
||||||
|
175 * Math.sin(A1 - F_r) +
|
||||||
|
175 * Math.sin(A1 + F_r) +
|
||||||
|
127 * Math.sin((Lp - Mp) * DEG2RAD) -
|
||||||
|
115 * Math.sin((Lp + Mp) * DEG2RAD)
|
||||||
|
|
||||||
// Moon ecliptic coordinates
|
// Moon ecliptic coordinates
|
||||||
const moonLonDeg = Lp + Sl * 1e-6
|
const moonLonDeg = Lp + Sl * 1e-6
|
||||||
const moonLatDeg = Sb * 1e-6
|
const moonLatDeg = Sb * 1e-6
|
||||||
const moonDistKm = 385000.56 + Sr * 0.001
|
const moonDistKm = 385000.56 + Sr * 0.001
|
||||||
|
|
||||||
const moonLon_rad = moonLonDeg * DEG
|
const moonLon_rad = moonLonDeg * DEG2RAD
|
||||||
const moonLat_rad = moonLatDeg * DEG
|
const moonLat_rad = moonLatDeg * DEG2RAD
|
||||||
|
|
||||||
// Ecliptic to equatorial (GCRS ≈ J2000 equatorial for this accuracy level)
|
// Ecliptic to equatorial (GCRS ≈ J2000 equatorial for this accuracy level)
|
||||||
const moonGCRS: Vec3 = [
|
const moonGCRS: Vec3 = [
|
||||||
moonDistKm * Math.cos(moonLat_rad) * Math.cos(moonLon_rad),
|
moonDistKm * Math.cos(moonLat_rad) * Math.cos(moonLon_rad),
|
||||||
moonDistKm * (Math.cos(eps) * Math.cos(moonLat_rad) * Math.sin(moonLon_rad) - Math.sin(eps) * Math.sin(moonLat_rad)),
|
moonDistKm *
|
||||||
moonDistKm * (Math.sin(eps) * Math.cos(moonLat_rad) * Math.sin(moonLon_rad) + Math.cos(eps) * Math.sin(moonLat_rad)),
|
(Math.cos(eps) * Math.cos(moonLat_rad) * Math.sin(moonLon_rad) -
|
||||||
|
Math.sin(eps) * Math.sin(moonLat_rad)),
|
||||||
|
moonDistKm *
|
||||||
|
(Math.sin(eps) * Math.cos(moonLat_rad) * Math.sin(moonLon_rad) +
|
||||||
|
Math.cos(eps) * Math.sin(moonLat_rad)),
|
||||||
]
|
]
|
||||||
|
|
||||||
return { moonGCRS, sunGCRS }
|
return { moonGCRS, sunGCRS }
|
||||||
|
|
@ -355,48 +381,49 @@ export function nearestNewMoon(jdTT: number): number {
|
||||||
const T = k / 1236.85
|
const T = k / 1236.85
|
||||||
|
|
||||||
// JDE of mean new moon (Meeus Eq. 49.1)
|
// JDE of mean new moon (Meeus Eq. 49.1)
|
||||||
let JDE = 2451550.09766
|
let JDE =
|
||||||
+ 29.530588861 * k
|
2451550.09766 +
|
||||||
+ 0.00015437 * T * T
|
29.530588861 * k +
|
||||||
- 0.000000150 * T * T * T
|
0.00015437 * T * T -
|
||||||
+ 0.00000000073 * T * T * T * T
|
0.00000015 * T * T * T +
|
||||||
|
0.00000000073 * T * T * T * T
|
||||||
|
|
||||||
// Fundamental arguments for the corrections (degrees → radians)
|
// Fundamental arguments for the corrections (degrees → radians)
|
||||||
const M = (2.5534 + 29.10535670 * k - 0.0000014 * T * T - 0.00000011 * T * T * T) * DEG
|
const M = (2.5534 + 29.1053567 * k - 0.0000014 * T * T - 0.00000011 * T * T * T) * DEG2RAD
|
||||||
const Mp = (201.5643 + 385.81693528 * k + 0.0107582 * T * T + 0.00001238 * T * T * T) * DEG
|
const Mp = (201.5643 + 385.81693528 * k + 0.0107582 * T * T + 0.00001238 * T * T * T) * DEG2RAD
|
||||||
const Fc = (160.7108 + 390.67050284 * k - 0.0016118 * T * T - 0.00000227 * T * T * T) * DEG
|
const Fc = (160.7108 + 390.67050284 * k - 0.0016118 * T * T - 0.00000227 * T * T * T) * DEG2RAD
|
||||||
const Om = (124.7746 - 1.56375588 * k + 0.0020672 * T * T + 0.00000215 * T * T * T) * DEG
|
const Om = (124.7746 - 1.56375588 * k + 0.0020672 * T * T + 0.00000215 * T * T * T) * DEG2RAD
|
||||||
|
|
||||||
// Eccentricity of Earth's orbit
|
// Eccentricity of Earth's orbit
|
||||||
const E = 1 - 0.002516 * T - 0.0000074 * T * T
|
const E = 1 - 0.002516 * T - 0.0000074 * T * T
|
||||||
|
|
||||||
// Corrections from Meeus Table 49.A (new moon)
|
// Corrections from Meeus Table 49.A (new moon)
|
||||||
JDE +=
|
JDE +=
|
||||||
-0.40720 * Math.sin(Mp)
|
-0.4072 * Math.sin(Mp) +
|
||||||
+ 0.17241 * E * Math.sin(M)
|
0.17241 * E * Math.sin(M) +
|
||||||
+ 0.01608 * Math.sin(2 * Mp)
|
0.01608 * Math.sin(2 * Mp) +
|
||||||
+ 0.01039 * Math.sin(2 * Fc)
|
0.01039 * Math.sin(2 * Fc) +
|
||||||
+ 0.00739 * E * Math.sin(Mp - M)
|
0.00739 * E * Math.sin(Mp - M) -
|
||||||
- 0.00514 * E * Math.sin(Mp + M)
|
0.00514 * E * Math.sin(Mp + M) +
|
||||||
+ 0.00208 * E * E * Math.sin(2 * M)
|
0.00208 * E * E * Math.sin(2 * M) -
|
||||||
- 0.00111 * Math.sin(Mp - 2 * Fc)
|
0.00111 * Math.sin(Mp - 2 * Fc) -
|
||||||
- 0.00057 * Math.sin(Mp + 2 * Fc)
|
0.00057 * Math.sin(Mp + 2 * Fc) +
|
||||||
+ 0.00056 * E * Math.sin(2 * Mp + M)
|
0.00056 * E * Math.sin(2 * Mp + M) -
|
||||||
- 0.00042 * Math.sin(3 * Mp)
|
0.00042 * Math.sin(3 * Mp) +
|
||||||
+ 0.00042 * E * Math.sin(M + 2 * Fc)
|
0.00042 * E * Math.sin(M + 2 * Fc) +
|
||||||
+ 0.00038 * E * Math.sin(M - 2 * Fc)
|
0.00038 * E * Math.sin(M - 2 * Fc) -
|
||||||
- 0.00024 * E * Math.sin(2 * Mp - M)
|
0.00024 * E * Math.sin(2 * Mp - M) -
|
||||||
- 0.00017 * Math.sin(Om)
|
0.00017 * Math.sin(Om) -
|
||||||
- 0.00007 * Math.sin(Mp + 2 * M)
|
0.00007 * Math.sin(Mp + 2 * M) +
|
||||||
+ 0.00004 * Math.sin(2 * Mp - 2 * Fc)
|
0.00004 * Math.sin(2 * Mp - 2 * Fc) +
|
||||||
+ 0.00004 * Math.sin(3 * M)
|
0.00004 * Math.sin(3 * M) +
|
||||||
+ 0.00003 * Math.sin(Mp + M - 2 * Fc)
|
0.00003 * Math.sin(Mp + M - 2 * Fc) +
|
||||||
+ 0.00003 * Math.sin(2 * Mp + 2 * Fc)
|
0.00003 * Math.sin(2 * Mp + 2 * Fc) -
|
||||||
- 0.00003 * Math.sin(Mp + M + 2 * Fc)
|
0.00003 * Math.sin(Mp + M + 2 * Fc) +
|
||||||
+ 0.00003 * Math.sin(Mp - M + 2 * Fc)
|
0.00003 * Math.sin(Mp - M + 2 * Fc) -
|
||||||
- 0.00002 * Math.sin(Mp - M - 2 * Fc)
|
0.00002 * Math.sin(Mp - M - 2 * Fc) -
|
||||||
- 0.00002 * Math.sin(3 * Mp + M)
|
0.00002 * Math.sin(3 * Mp + M) +
|
||||||
+ 0.00002 * Math.sin(4 * Mp)
|
0.00002 * Math.sin(4 * Mp)
|
||||||
|
|
||||||
return JDE
|
return JDE
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -100,7 +100,9 @@ async function cmdSighting(cmdArgs: string[]) {
|
||||||
console.log(`Sunset: ${fmtDate(report.sunsetUTC)}`)
|
console.log(`Sunset: ${fmtDate(report.sunsetUTC)}`)
|
||||||
console.log(`Moonset: ${fmtDate(report.moonsetUTC)}`)
|
console.log(`Moonset: ${fmtDate(report.moonsetUTC)}`)
|
||||||
console.log(`Best time: ${fmtDate(report.bestTimeUTC)}`)
|
console.log(`Best time: ${fmtDate(report.bestTimeUTC)}`)
|
||||||
console.log(`Lag: ${report.lagMinutes !== null ? Math.round(report.lagMinutes) + ' min' : 'N/A'}`)
|
console.log(
|
||||||
|
`Lag: ${report.lagMinutes !== null ? Math.round(report.lagMinutes) + ' min' : 'N/A'}`,
|
||||||
|
)
|
||||||
console.log('')
|
console.log('')
|
||||||
|
|
||||||
if (report.geometry) {
|
if (report.geometry) {
|
||||||
|
|
@ -151,7 +153,9 @@ async function cmdBenchmark() {
|
||||||
getMoonPhase(new Date(Date.UTC(2025, 2, 1 + (i % 28))))
|
getMoonPhase(new Date(Date.UTC(2025, 2, 1 + (i % 28))))
|
||||||
}
|
}
|
||||||
const phaseMs = performance.now() - phaseStart
|
const phaseMs = performance.now() - phaseStart
|
||||||
console.log(`getMoonPhase × ${N_PHASE}: ${phaseMs.toFixed(1)} ms (${(phaseMs / N_PHASE * 1000).toFixed(1)} µs/call)`)
|
console.log(
|
||||||
|
`getMoonPhase × ${N_PHASE}: ${phaseMs.toFixed(1)} ms (${((phaseMs / N_PHASE) * 1000).toFixed(1)} µs/call)`,
|
||||||
|
)
|
||||||
|
|
||||||
// Benchmark 2: kernel load
|
// Benchmark 2: kernel load
|
||||||
const loadStart = performance.now()
|
const loadStart = performance.now()
|
||||||
|
|
@ -170,10 +174,13 @@ async function cmdBenchmark() {
|
||||||
/** Format a nullable Date as a short UTC string. */
|
/** Format a nullable Date as a short UTC string. */
|
||||||
function fmtDate(d: Date | null): string {
|
function fmtDate(d: Date | null): string {
|
||||||
if (!d) return 'N/A'
|
if (!d) return 'N/A'
|
||||||
return d.toISOString().replace('T', ' ').replace(/\.\d+Z$/, ' UTC')
|
return d
|
||||||
|
.toISOString()
|
||||||
|
.replace('T', ' ')
|
||||||
|
.replace(/\.\d+Z$/, ' UTC')
|
||||||
}
|
}
|
||||||
|
|
||||||
main().catch(err => {
|
main().catch((err) => {
|
||||||
console.error(err instanceof Error ? err.message : String(err))
|
console.error(err instanceof Error ? err.message : String(err))
|
||||||
process.exit(1)
|
process.exit(1)
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,8 @@
|
||||||
import type { Vec3, Observer, SunMoonEvents, TimeScales } from '../types.js'
|
import type { Vec3, Observer, SunMoonEvents, TimeScales } from '../types.js'
|
||||||
import type { SpkKernel } from '../spk/index.js'
|
import type { SpkKernel } from '../spk/index.js'
|
||||||
import { NAIF_IDS } from '../spk/index.js'
|
import { NAIF_IDS } from '../spk/index.js'
|
||||||
import { brentRoot } from '../math/index.js'
|
import { brentRoot, vdot, vnorm } from '../math/index.js'
|
||||||
|
import { arcvMinimum } from '../visibility/index.js'
|
||||||
import {
|
import {
|
||||||
J2000,
|
J2000,
|
||||||
SECONDS_PER_DAY,
|
SECONDS_PER_DAY,
|
||||||
|
|
@ -33,7 +34,11 @@ import {
|
||||||
getDeltaAT,
|
getDeltaAT,
|
||||||
TT_MINUS_TAI,
|
TT_MINUS_TAI,
|
||||||
} from '../time/index.js'
|
} from '../time/index.js'
|
||||||
import { getMoonGeocentricState, getSunGeocentricState, computeCrescentWidth } from '../bodies/index.js'
|
import {
|
||||||
|
getMoonGeocentricState,
|
||||||
|
getSunGeocentricState,
|
||||||
|
computeCrescentWidth,
|
||||||
|
} from '../bodies/index.js'
|
||||||
import { geodeticToECEF, computeAzAlt } from '../observer/index.js'
|
import { geodeticToECEF, computeAzAlt } from '../observer/index.js'
|
||||||
import { itrsToGcrs } from '../frames/index.js'
|
import { itrsToGcrs } from '../frames/index.js'
|
||||||
|
|
||||||
|
|
@ -51,7 +56,7 @@ export const SUN_ALTITUDE_THRESHOLD = -0.8333
|
||||||
* Accounts for: standard refraction at horizon (34') + lunar semi-diameter (~16')
|
* Accounts for: standard refraction at horizon (34') + lunar semi-diameter (~16')
|
||||||
* Note: Moon's SD varies with distance (14.7'–16.8'). Use 0.2725° as mean.
|
* Note: Moon's SD varies with distance (14.7'–16.8'). Use 0.2725° as mean.
|
||||||
*/
|
*/
|
||||||
export const MOON_ALTITUDE_THRESHOLD = -0.8333 // refined per actual distance in implementation
|
export const MOON_ALTITUDE_THRESHOLD = -0.8333 // refined per actual distance in implementation
|
||||||
|
|
||||||
// ─── Internal helpers ─────────────────────────────────────────────────────────
|
// ─── Internal helpers ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
@ -130,7 +135,7 @@ export function findAltitudeCrossing(
|
||||||
|
|
||||||
const f = (et: number) => altitudeMinusThreshold(kernel, naifId, observer, et, threshold)
|
const f = (et: number) => altitudeMinusThreshold(kernel, naifId, observer, et, threshold)
|
||||||
|
|
||||||
const STEP_S = 600 // 10-minute coarse sampling
|
const STEP_S = 600 // 10-minute coarse sampling
|
||||||
const nSteps = Math.ceil((endET - startET) / STEP_S)
|
const nSteps = Math.ceil((endET - startET) / STEP_S)
|
||||||
|
|
||||||
let prevET = startET
|
let prevET = startET
|
||||||
|
|
@ -140,11 +145,11 @@ export function findAltitudeCrossing(
|
||||||
const currET = Math.min(startET + i * STEP_S, endET)
|
const currET = Math.min(startET + i * STEP_S, endET)
|
||||||
const currF = f(currET)
|
const currF = f(currET)
|
||||||
|
|
||||||
const isRisingCross = rising && prevF < 0 && currF >= 0
|
const isRisingCross = rising && prevF < 0 && currF >= 0
|
||||||
const isSettingCross = !rising && prevF >= 0 && currF < 0
|
const isSettingCross = !rising && prevF >= 0 && currF < 0
|
||||||
|
|
||||||
if (isRisingCross || isSettingCross) {
|
if (isRisingCross || isSettingCross) {
|
||||||
const etCross = brentRoot(f, prevET, currET, 0.5) // 0.5s precision
|
const etCross = brentRoot(f, prevET, currET, 0.5) // 0.5s precision
|
||||||
if (etCross !== null) {
|
if (etCross !== null) {
|
||||||
const tsCross = etToTS(etCross)
|
const tsCross = etToTS(etCross)
|
||||||
return tsCross.utc
|
return tsCross.utc
|
||||||
|
|
@ -169,45 +174,90 @@ export function findAltitudeCrossing(
|
||||||
* @param kernel - DE442S kernel
|
* @param kernel - DE442S kernel
|
||||||
* @returns SunMoonEvents with all event times in UTC, or null if event doesn't occur
|
* @returns SunMoonEvents with all event times in UTC, or null if event doesn't occur
|
||||||
*/
|
*/
|
||||||
export function getSunMoonEvents(
|
export function getSunMoonEvents(date: Date, observer: Observer, kernel: SpkKernel): SunMoonEvents {
|
||||||
date: Date,
|
|
||||||
observer: Observer,
|
|
||||||
kernel: SpkKernel,
|
|
||||||
): SunMoonEvents {
|
|
||||||
// Anchor search at UTC midnight of the input date
|
// Anchor search at UTC midnight of the input date
|
||||||
const midnight = new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate()))
|
const midnight = new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate()))
|
||||||
const jdMidnight = dateToJD(midnight)
|
const jdMidnight = dateToJD(midnight)
|
||||||
// Approximate ET at midnight
|
// Approximate ET at midnight
|
||||||
const etStart = jdTTtoET(jdMidnight + 70.0 / SECONDS_PER_DAY) // rough TT≈UTC+70s
|
const etStart = jdTTtoET(jdMidnight + 70.0 / SECONDS_PER_DAY) // rough TT≈UTC+70s
|
||||||
const etEnd = etStart + 28 * 3600 // 28-hour window
|
const etEnd = etStart + 28 * 3600 // 28-hour window
|
||||||
|
|
||||||
const ts0 = computeTimeScales(midnight)
|
const ts0 = computeTimeScales(midnight)
|
||||||
|
|
||||||
// Sun events
|
// Sun events
|
||||||
const sunriseUTC = findAltitudeCrossing(
|
const sunriseUTC = findAltitudeCrossing(
|
||||||
kernel, NAIF_IDS.SUN, observer, ts0, etStart, etEnd, SUN_ALTITUDE_THRESHOLD, true,
|
kernel,
|
||||||
|
NAIF_IDS.SUN,
|
||||||
|
observer,
|
||||||
|
ts0,
|
||||||
|
etStart,
|
||||||
|
etEnd,
|
||||||
|
SUN_ALTITUDE_THRESHOLD,
|
||||||
|
true,
|
||||||
)
|
)
|
||||||
const sunsetUTC = findAltitudeCrossing(
|
const sunsetUTC = findAltitudeCrossing(
|
||||||
kernel, NAIF_IDS.SUN, observer, ts0, etStart, etEnd, SUN_ALTITUDE_THRESHOLD, false,
|
kernel,
|
||||||
|
NAIF_IDS.SUN,
|
||||||
|
observer,
|
||||||
|
ts0,
|
||||||
|
etStart,
|
||||||
|
etEnd,
|
||||||
|
SUN_ALTITUDE_THRESHOLD,
|
||||||
|
false,
|
||||||
)
|
)
|
||||||
|
|
||||||
// Twilight events (Sun setting below -6°, -12°, -18°)
|
// Twilight events (Sun setting below -6°, -12°, -18°)
|
||||||
const civilTwilightEndUTC = findAltitudeCrossing(
|
const civilTwilightEndUTC = findAltitudeCrossing(
|
||||||
kernel, NAIF_IDS.SUN, observer, ts0, etStart, etEnd, -6, false,
|
kernel,
|
||||||
|
NAIF_IDS.SUN,
|
||||||
|
observer,
|
||||||
|
ts0,
|
||||||
|
etStart,
|
||||||
|
etEnd,
|
||||||
|
-6,
|
||||||
|
false,
|
||||||
)
|
)
|
||||||
const nauticalTwilightEndUTC = findAltitudeCrossing(
|
const nauticalTwilightEndUTC = findAltitudeCrossing(
|
||||||
kernel, NAIF_IDS.SUN, observer, ts0, etStart, etEnd, -12, false,
|
kernel,
|
||||||
|
NAIF_IDS.SUN,
|
||||||
|
observer,
|
||||||
|
ts0,
|
||||||
|
etStart,
|
||||||
|
etEnd,
|
||||||
|
-12,
|
||||||
|
false,
|
||||||
)
|
)
|
||||||
const astronomicalTwilightEndUTC = findAltitudeCrossing(
|
const astronomicalTwilightEndUTC = findAltitudeCrossing(
|
||||||
kernel, NAIF_IDS.SUN, observer, ts0, etStart, etEnd, -18, false,
|
kernel,
|
||||||
|
NAIF_IDS.SUN,
|
||||||
|
observer,
|
||||||
|
ts0,
|
||||||
|
etStart,
|
||||||
|
etEnd,
|
||||||
|
-18,
|
||||||
|
false,
|
||||||
)
|
)
|
||||||
|
|
||||||
// Moon events
|
// Moon events
|
||||||
const moonriseUTC = findAltitudeCrossing(
|
const moonriseUTC = findAltitudeCrossing(
|
||||||
kernel, NAIF_IDS.MOON, observer, ts0, etStart, etEnd, MOON_ALTITUDE_THRESHOLD, true,
|
kernel,
|
||||||
|
NAIF_IDS.MOON,
|
||||||
|
observer,
|
||||||
|
ts0,
|
||||||
|
etStart,
|
||||||
|
etEnd,
|
||||||
|
MOON_ALTITUDE_THRESHOLD,
|
||||||
|
true,
|
||||||
)
|
)
|
||||||
const moonsetUTC = findAltitudeCrossing(
|
const moonsetUTC = findAltitudeCrossing(
|
||||||
kernel, NAIF_IDS.MOON, observer, ts0, etStart, etEnd, MOON_ALTITUDE_THRESHOLD, false,
|
kernel,
|
||||||
|
NAIF_IDS.MOON,
|
||||||
|
observer,
|
||||||
|
ts0,
|
||||||
|
etStart,
|
||||||
|
etEnd,
|
||||||
|
MOON_ALTITUDE_THRESHOLD,
|
||||||
|
false,
|
||||||
)
|
)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
@ -241,7 +291,7 @@ export function bestTimeHeuristic(
|
||||||
moonsetUTC: Date,
|
moonsetUTC: Date,
|
||||||
): { bestTimeUTC: Date; lagMinutes: number } | null {
|
): { bestTimeUTC: Date; lagMinutes: number } | null {
|
||||||
const lagMs = moonsetUTC.getTime() - sunsetUTC.getTime()
|
const lagMs = moonsetUTC.getTime() - sunsetUTC.getTime()
|
||||||
if (lagMs <= 0) return null // Moon sets before Sun — no sighting possible
|
if (lagMs <= 0) return null // Moon sets before Sun — no sighting possible
|
||||||
|
|
||||||
const lagMinutes = lagMs / 60000
|
const lagMinutes = lagMs / 60000
|
||||||
const bestTimeMs = sunsetUTC.getTime() + (4 / 9) * lagMs
|
const bestTimeMs = sunsetUTC.getTime() + (4 / 9) * lagMs
|
||||||
|
|
@ -252,14 +302,6 @@ export function bestTimeHeuristic(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Odeh arcv minimum polynomial (Odeh 2006, Eq. 1).
|
|
||||||
* Returns the minimum ARCV needed for visibility at crescent width W (arc minutes).
|
|
||||||
*/
|
|
||||||
function odehArcvMin(W: number): number {
|
|
||||||
return 11.8371 - 6.3226 * W + 0.7319 * W * W - 0.1018 * W * W * W
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Find the optimal observation time by maximizing the Odeh V parameter
|
* Find the optimal observation time by maximizing the Odeh V parameter
|
||||||
* over the interval [sunset, moonset].
|
* over the interval [sunset, moonset].
|
||||||
|
|
@ -290,9 +332,6 @@ export function bestTimeOptimized(
|
||||||
const obsECEF = geodeticToECEF(observer.lat, observer.lon, observer.elevation)
|
const obsECEF = geodeticToECEF(observer.lat, observer.lon, observer.elevation)
|
||||||
const obsITRS: Vec3 = [obsECEF[0] / 1000, obsECEF[1] / 1000, obsECEF[2] / 1000]
|
const obsITRS: Vec3 = [obsECEF[0] / 1000, obsECEF[1] / 1000, obsECEF[2] / 1000]
|
||||||
|
|
||||||
const dot = (a: Vec3, b: Vec3) => a[0]*b[0] + a[1]*b[1] + a[2]*b[2]
|
|
||||||
const norm = (v: Vec3) => Math.sqrt(dot(v, v))
|
|
||||||
|
|
||||||
let bestTimeUTC = sunsetUTC
|
let bestTimeUTC = sunsetUTC
|
||||||
let maxV = -Infinity
|
let maxV = -Infinity
|
||||||
|
|
||||||
|
|
@ -302,14 +341,14 @@ export function bestTimeOptimized(
|
||||||
const et = jdTTtoET(ts.jdTT)
|
const et = jdTTtoET(ts.jdTT)
|
||||||
|
|
||||||
const moonGCRS = getMoonGeocentricState(kernel, et).position
|
const moonGCRS = getMoonGeocentricState(kernel, et).position
|
||||||
const sunGCRS = getSunGeocentricState(kernel, et).position
|
const sunGCRS = getSunGeocentricState(kernel, et).position
|
||||||
|
|
||||||
// Convert observer ITRS → GCRS at this timestep (Earth rotation changes per step)
|
// Convert observer ITRS → GCRS at this timestep (Earth rotation changes per step)
|
||||||
const obsGCRS = itrsToGcrs(obsITRS, ts)
|
const obsGCRS = itrsToGcrs(obsITRS, ts)
|
||||||
|
|
||||||
// Airless altitudes via the full pipeline
|
// Airless altitudes via the full pipeline
|
||||||
const moonAzAlt = computeAzAlt(moonGCRS, observer, ts, true)
|
const moonAzAlt = computeAzAlt(moonGCRS, observer, ts, true)
|
||||||
const sunAzAlt = computeAzAlt(sunGCRS, observer, ts, true)
|
const sunAzAlt = computeAzAlt(sunGCRS, observer, ts, true)
|
||||||
|
|
||||||
const ARCV = moonAzAlt.altitude - sunAzAlt.altitude
|
const ARCV = moonAzAlt.altitude - sunAzAlt.altitude
|
||||||
|
|
||||||
|
|
@ -324,11 +363,11 @@ export function bestTimeOptimized(
|
||||||
sunGCRS[1] - obsGCRS[1],
|
sunGCRS[1] - obsGCRS[1],
|
||||||
sunGCRS[2] - obsGCRS[2],
|
sunGCRS[2] - obsGCRS[2],
|
||||||
]
|
]
|
||||||
const cosARCL = dot(moonTopo, sunTopo) / (norm(moonTopo) * norm(sunTopo))
|
const cosARCL = vdot(moonTopo, sunTopo) / (vnorm(moonTopo) * vnorm(sunTopo))
|
||||||
const ARCL = Math.acos(Math.max(-1, Math.min(1, cosARCL))) * (180 / Math.PI)
|
const ARCL = Math.acos(Math.max(-1, Math.min(1, cosARCL))) * (180 / Math.PI)
|
||||||
|
|
||||||
const { W } = computeCrescentWidth(moonTopo, ARCL)
|
const { W } = computeCrescentWidth(moonTopo, ARCL)
|
||||||
const V = ARCV - odehArcvMin(W)
|
const V = ARCV - arcvMinimum(W)
|
||||||
|
|
||||||
if (V > maxV) {
|
if (V > maxV) {
|
||||||
maxV = V
|
maxV = V
|
||||||
|
|
@ -347,13 +386,7 @@ export function bestTimeOptimized(
|
||||||
* @param windowMinutes - Half-width of window in minutes (default 20)
|
* @param windowMinutes - Half-width of window in minutes (default 20)
|
||||||
* @returns [start, end] UTC Date pair
|
* @returns [start, end] UTC Date pair
|
||||||
*/
|
*/
|
||||||
export function computeObservationWindow(
|
export function computeObservationWindow(bestTimeUTC: Date, windowMinutes = 20): [Date, Date] {
|
||||||
bestTimeUTC: Date,
|
|
||||||
windowMinutes = 20,
|
|
||||||
): [Date, Date] {
|
|
||||||
const windowMs = windowMinutes * 60000
|
const windowMs = windowMinutes * 60000
|
||||||
return [
|
return [new Date(bestTimeUTC.getTime() - windowMs), new Date(bestTimeUTC.getTime() + windowMs)]
|
||||||
new Date(bestTimeUTC.getTime() - windowMs),
|
|
||||||
new Date(bestTimeUTC.getTime() + windowMs),
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -50,165 +50,163 @@ const UAS01_TO_ARCSEC = 1e-7
|
||||||
// dpsi += (ps + pst*T)*sin(arg) + pc*cos(arg)
|
// dpsi += (ps + pst*T)*sin(arg) + pc*cos(arg)
|
||||||
// deps += (ec + ect*T)*cos(arg) + es*sin(arg)
|
// deps += (ec + ect*T)*cos(arg) + es*sin(arg)
|
||||||
|
|
||||||
const NUT_2000B: ReadonlyArray<readonly [
|
const NUT_2000B: ReadonlyArray<
|
||||||
number,number,number,number,number,
|
readonly [number, number, number, number, number, number, number, number, number, number, number]
|
||||||
number,number,number,
|
> = [
|
||||||
number,number,number
|
|
||||||
]> = [
|
|
||||||
// 1
|
// 1
|
||||||
[ 0, 0, 0, 0, 1, -172064161.0, -174666.0, 33386.0, 92052331.0, 9086.0, 15377.0],
|
[0, 0, 0, 0, 1, -172064161.0, -174666.0, 33386.0, 92052331.0, 9086.0, 15377.0],
|
||||||
// 2
|
// 2
|
||||||
[ 0, 0, 2,-2, 2, -13170906.0, -13696.0, -13238.0, 5730336.0, -3015.0, -4587.0],
|
[0, 0, 2, -2, 2, -13170906.0, -13696.0, -13238.0, 5730336.0, -3015.0, -4587.0],
|
||||||
// 3
|
// 3
|
||||||
[ 0, 0, 2, 0, 2, -2276413.0, -2353.0, 2796.0, 978459.0, -619.0, 645.0],
|
[0, 0, 2, 0, 2, -2276413.0, -2353.0, 2796.0, 978459.0, -619.0, 645.0],
|
||||||
// 4
|
// 4
|
||||||
[ 0, 0, 0, 0, 2, 2074554.0, 2352.0, -2635.0, -897492.0, 307.0, -187.0],
|
[0, 0, 0, 0, 2, 2074554.0, 2352.0, -2635.0, -897492.0, 307.0, -187.0],
|
||||||
// 5
|
// 5
|
||||||
[ 0, 1, 0, 0, 0, 1475877.0, -11817.0, 11817.0, 73871.0, -184.0, -1924.0],
|
[0, 1, 0, 0, 0, 1475877.0, -11817.0, 11817.0, 73871.0, -184.0, -1924.0],
|
||||||
// 6
|
// 6
|
||||||
[ 0, 1, 2,-2, 2, -516821.0, 1226.0, -524.0, 224386.0, -677.0, -174.0],
|
[0, 1, 2, -2, 2, -516821.0, 1226.0, -524.0, 224386.0, -677.0, -174.0],
|
||||||
// 7
|
// 7
|
||||||
[ 1, 0, 0, 0, 0, 711159.0, 73.0, -872.0, -6750.0, 0.0, 358.0],
|
[1, 0, 0, 0, 0, 711159.0, 73.0, -872.0, -6750.0, 0.0, 358.0],
|
||||||
// 8
|
// 8
|
||||||
[ 0, 0, 2, 0, 1, -387298.0, -367.0, 380.0, 200728.0, 18.0, 318.0],
|
[0, 0, 2, 0, 1, -387298.0, -367.0, 380.0, 200728.0, 18.0, 318.0],
|
||||||
// 9
|
// 9
|
||||||
[ 1, 0, 2, 0, 2, -301461.0, -36.0, 816.0, 129025.0, -63.0, 367.0],
|
[1, 0, 2, 0, 2, -301461.0, -36.0, 816.0, 129025.0, -63.0, 367.0],
|
||||||
// 10
|
// 10
|
||||||
[ 0,-1, 2,-2, 2, 215829.0, -494.0, 111.0, -95929.0, 299.0, 132.0],
|
[0, -1, 2, -2, 2, 215829.0, -494.0, 111.0, -95929.0, 299.0, 132.0],
|
||||||
// 11
|
// 11
|
||||||
[ 0, 0, 2,-2, 1, 128227.0, 137.0, 181.0, -68982.0, -9.0, 39.0],
|
[0, 0, 2, -2, 1, 128227.0, 137.0, 181.0, -68982.0, -9.0, 39.0],
|
||||||
// 12
|
// 12
|
||||||
[-1, 0, 2, 0, 2, 123457.0, 11.0, 19.0, -53311.0, 32.0, -4.0],
|
[-1, 0, 2, 0, 2, 123457.0, 11.0, 19.0, -53311.0, 32.0, -4.0],
|
||||||
// 13
|
// 13
|
||||||
[-1, 0, 0, 2, 0, 156994.0, 10.0, -168.0, -1235.0, 0.0, 82.0],
|
[-1, 0, 0, 2, 0, 156994.0, 10.0, -168.0, -1235.0, 0.0, 82.0],
|
||||||
// 14
|
// 14
|
||||||
[ 1, 0, 0, 0, 1, 63110.0, 63.0, 27.0, -33228.0, 0.0, -9.0],
|
[1, 0, 0, 0, 1, 63110.0, 63.0, 27.0, -33228.0, 0.0, -9.0],
|
||||||
// 15
|
// 15
|
||||||
[-1, 0, 0, 0, 1, -57976.0, -63.0, -189.0, 31429.0, 0.0, -75.0],
|
[-1, 0, 0, 0, 1, -57976.0, -63.0, -189.0, 31429.0, 0.0, -75.0],
|
||||||
// 16
|
// 16
|
||||||
[-1, 0, 2, 2, 2, -59641.0, -11.0, 149.0, 25543.0, -11.0, 66.0],
|
[-1, 0, 2, 2, 2, -59641.0, -11.0, 149.0, 25543.0, -11.0, 66.0],
|
||||||
// 17
|
// 17
|
||||||
[ 1, 0, 2, 0, 1, -51613.0, -42.0, 129.0, 26366.0, 0.0, 78.0],
|
[1, 0, 2, 0, 1, -51613.0, -42.0, 129.0, 26366.0, 0.0, 78.0],
|
||||||
// 18
|
// 18
|
||||||
[-2, 0, 2, 0, 1, 45893.0, 50.0, 31.0, -24236.0, -10.0, 20.0],
|
[-2, 0, 2, 0, 1, 45893.0, 50.0, 31.0, -24236.0, -10.0, 20.0],
|
||||||
// 19
|
// 19
|
||||||
[ 0, 0, 0, 2, 0, 63384.0, 11.0, -150.0, -1220.0, 0.0, 29.0],
|
[0, 0, 0, 2, 0, 63384.0, 11.0, -150.0, -1220.0, 0.0, 29.0],
|
||||||
// 20
|
// 20
|
||||||
[ 0, 0, 2, 2, 2, -38571.0, -1.0, 158.0, 16452.0, -11.0, 68.0],
|
[0, 0, 2, 2, 2, -38571.0, -1.0, 158.0, 16452.0, -11.0, 68.0],
|
||||||
// 21
|
// 21
|
||||||
[ 0,-2, 2,-2, 2, 32481.0, 0.0, 0.0, -13870.0, 0.0, 0.0],
|
[0, -2, 2, -2, 2, 32481.0, 0.0, 0.0, -13870.0, 0.0, 0.0],
|
||||||
// 22
|
// 22
|
||||||
[-2, 0, 0, 2, 0, -47722.0, 0.0, -18.0, 477.0, 0.0, -25.0],
|
[-2, 0, 0, 2, 0, -47722.0, 0.0, -18.0, 477.0, 0.0, -25.0],
|
||||||
// 23
|
// 23
|
||||||
[ 2, 0, 2, 0, 2, -31046.0, -1.0, 131.0, 13238.0, -11.0, 59.0],
|
[2, 0, 2, 0, 2, -31046.0, -1.0, 131.0, 13238.0, -11.0, 59.0],
|
||||||
// 24
|
// 24
|
||||||
[ 1, 0, 2,-2, 2, 28593.0, 0.0, -1.0, -12338.0, 10.0, -3.0],
|
[1, 0, 2, -2, 2, 28593.0, 0.0, -1.0, -12338.0, 10.0, -3.0],
|
||||||
// 25
|
// 25
|
||||||
[-1, 0, 2, 0, 1, 20441.0, 21.0, 10.0, -10758.0, 0.0, -3.0],
|
[-1, 0, 2, 0, 1, 20441.0, 21.0, 10.0, -10758.0, 0.0, -3.0],
|
||||||
// 26
|
// 26
|
||||||
[ 2, 0, 0, 0, 0, 29243.0, 0.0, -74.0, -609.0, 0.0, 13.0],
|
[2, 0, 0, 0, 0, 29243.0, 0.0, -74.0, -609.0, 0.0, 13.0],
|
||||||
// 27
|
// 27
|
||||||
[ 0, 0, 2, 0, 0, 25887.0, 0.0, -66.0, -550.0, 0.0, 11.0],
|
[0, 0, 2, 0, 0, 25887.0, 0.0, -66.0, -550.0, 0.0, 11.0],
|
||||||
// 28
|
// 28
|
||||||
[ 0, 1, 0, 0, 1, -14053.0, -25.0, 79.0, 8551.0, -2.0, -45.0],
|
[0, 1, 0, 0, 1, -14053.0, -25.0, 79.0, 8551.0, -2.0, -45.0],
|
||||||
// 29
|
// 29
|
||||||
[-1, 0, 0, 2, 1, 15164.0, 10.0, 11.0, -8001.0, 0.0, -1.0],
|
[-1, 0, 0, 2, 1, 15164.0, 10.0, 11.0, -8001.0, 0.0, -1.0],
|
||||||
// 30
|
// 30
|
||||||
[ 0, 2, 2,-2, 2, -15794.0, 72.0, -16.0, 6850.0, -42.0, -5.0],
|
[0, 2, 2, -2, 2, -15794.0, 72.0, -16.0, 6850.0, -42.0, -5.0],
|
||||||
// 31
|
// 31
|
||||||
[ 0, 0,-2, 2, 0, 21783.0, 0.0, 13.0, -167.0, 0.0, 13.0],
|
[0, 0, -2, 2, 0, 21783.0, 0.0, 13.0, -167.0, 0.0, 13.0],
|
||||||
// 32
|
// 32
|
||||||
[ 1, 0, 0,-2, 1, -12873.0, -10.0, -37.0, 6953.0, 0.0, -14.0],
|
[1, 0, 0, -2, 1, -12873.0, -10.0, -37.0, 6953.0, 0.0, -14.0],
|
||||||
// 33
|
// 33
|
||||||
[ 0,-1, 0, 0, 1, -12654.0, 11.0, 63.0, 6415.0, 0.0, 26.0],
|
[0, -1, 0, 0, 1, -12654.0, 11.0, 63.0, 6415.0, 0.0, 26.0],
|
||||||
// 34
|
// 34
|
||||||
[-1, 0, 2, 2, 1, -10204.0, 0.0, 25.0, 5222.0, 0.0, 15.0],
|
[-1, 0, 2, 2, 1, -10204.0, 0.0, 25.0, 5222.0, 0.0, 15.0],
|
||||||
// 35
|
// 35
|
||||||
[ 0, 2, 0, 0, 0, 16707.0, -85.0, -10.0, 168.0, -1.0, 10.0],
|
[0, 2, 0, 0, 0, 16707.0, -85.0, -10.0, 168.0, -1.0, 10.0],
|
||||||
// 36
|
// 36
|
||||||
[ 1, 0, 2, 2, 2, -7691.0, 0.0, 44.0, 3268.0, 0.0, 19.0],
|
[1, 0, 2, 2, 2, -7691.0, 0.0, 44.0, 3268.0, 0.0, 19.0],
|
||||||
// 37
|
// 37
|
||||||
[-2, 0, 2, 0, 0, -11024.0, 0.0, -14.0, 104.0, 0.0, 2.0],
|
[-2, 0, 2, 0, 0, -11024.0, 0.0, -14.0, 104.0, 0.0, 2.0],
|
||||||
// 38
|
// 38
|
||||||
[ 0, 1, 2, 0, 2, 7566.0, -21.0, -11.0, -3250.0, 0.0, -5.0],
|
[0, 1, 2, 0, 2, 7566.0, -21.0, -11.0, -3250.0, 0.0, -5.0],
|
||||||
// 39
|
// 39
|
||||||
[ 0, 0, 2, 2, 1, -6637.0, -11.0, 25.0, 3353.0, 0.0, 14.0],
|
[0, 0, 2, 2, 1, -6637.0, -11.0, 25.0, 3353.0, 0.0, 14.0],
|
||||||
// 40
|
// 40
|
||||||
[ 0,-1, 2, 0, 2, -7141.0, 21.0, 8.0, 3070.0, 0.0, 4.0],
|
[0, -1, 2, 0, 2, -7141.0, 21.0, 8.0, 3070.0, 0.0, 4.0],
|
||||||
// 41
|
// 41
|
||||||
[ 0, 0, 0, 2, 1, -6302.0, -11.0, 2.0, 3272.0, 0.0, 4.0],
|
[0, 0, 0, 2, 1, -6302.0, -11.0, 2.0, 3272.0, 0.0, 4.0],
|
||||||
// 42
|
// 42
|
||||||
[ 1, 0, 2,-2, 1, 5800.0, 10.0, 2.0, -3045.0, 0.0, -1.0],
|
[1, 0, 2, -2, 1, 5800.0, 10.0, 2.0, -3045.0, 0.0, -1.0],
|
||||||
// 43
|
// 43
|
||||||
[ 2, 0, 2,-2, 2, 6443.0, 0.0, -7.0, -2768.0, 0.0, -4.0],
|
[2, 0, 2, -2, 2, 6443.0, 0.0, -7.0, -2768.0, 0.0, -4.0],
|
||||||
// 44
|
// 44
|
||||||
[-2, 0, 0, 2, 1, -5774.0, -11.0, -15.0, 3041.0, 0.0, -5.0],
|
[-2, 0, 0, 2, 1, -5774.0, -11.0, -15.0, 3041.0, 0.0, -5.0],
|
||||||
// 45
|
// 45
|
||||||
[ 2, 0, 2, 0, 1, -5350.0, 0.0, 21.0, 2695.0, 0.0, 12.0],
|
[2, 0, 2, 0, 1, -5350.0, 0.0, 21.0, 2695.0, 0.0, 12.0],
|
||||||
// 46
|
// 46
|
||||||
[ 0,-1, 2,-2, 1, -4752.0, -11.0, -3.0, 2719.0, 0.0, -3.0],
|
[0, -1, 2, -2, 1, -4752.0, -11.0, -3.0, 2719.0, 0.0, -3.0],
|
||||||
// 47
|
// 47
|
||||||
[ 0, 0, 0,-2, 1, -4940.0, -11.0, -21.0, 2720.0, 0.0, -9.0],
|
[0, 0, 0, -2, 1, -4940.0, -11.0, -21.0, 2720.0, 0.0, -9.0],
|
||||||
// 48
|
// 48
|
||||||
[-1,-1, 0, 2, 0, 7350.0, 0.0, -8.0, -51.0, 0.0, 4.0],
|
[-1, -1, 0, 2, 0, 7350.0, 0.0, -8.0, -51.0, 0.0, 4.0],
|
||||||
// 49
|
// 49
|
||||||
[ 2, 0, 0,-2, 1, 4065.0, 0.0, 6.0, -2206.0, 0.0, 1.0],
|
[2, 0, 0, -2, 1, 4065.0, 0.0, 6.0, -2206.0, 0.0, 1.0],
|
||||||
// 50
|
// 50
|
||||||
[ 1, 0, 0, 2, 0, 6579.0, 0.0, -24.0, -199.0, 0.0, 2.0],
|
[1, 0, 0, 2, 0, 6579.0, 0.0, -24.0, -199.0, 0.0, 2.0],
|
||||||
// 51
|
// 51
|
||||||
[ 0, 1, 2,-2, 1, 3579.0, 0.0, 5.0, -1900.0, 0.0, 1.0],
|
[0, 1, 2, -2, 1, 3579.0, 0.0, 5.0, -1900.0, 0.0, 1.0],
|
||||||
// 52
|
// 52
|
||||||
[ 1,-1, 0, 0, 0, 4725.0, 0.0, -6.0, -41.0, 0.0, 3.0],
|
[1, -1, 0, 0, 0, 4725.0, 0.0, -6.0, -41.0, 0.0, 3.0],
|
||||||
// 53
|
// 53
|
||||||
[-2, 0, 2, 0, 2, -3075.0, 0.0, -2.0, 1313.0, 0.0, -1.0],
|
[-2, 0, 2, 0, 2, -3075.0, 0.0, -2.0, 1313.0, 0.0, -1.0],
|
||||||
// 54
|
// 54
|
||||||
[ 3, 0, 2, 0, 2, -2904.0, 0.0, 15.0, 1233.0, 0.0, 7.0],
|
[3, 0, 2, 0, 2, -2904.0, 0.0, 15.0, 1233.0, 0.0, 7.0],
|
||||||
// 55
|
// 55
|
||||||
[ 0,-1, 0, 2, 0, 4348.0, 0.0, -10.0, -81.0, 0.0, 2.0],
|
[0, -1, 0, 2, 0, 4348.0, 0.0, -10.0, -81.0, 0.0, 2.0],
|
||||||
// 56
|
// 56
|
||||||
[ 1,-1, 2, 0, 2, -2878.0, 0.0, 8.0, 1232.0, 0.0, 4.0],
|
[1, -1, 2, 0, 2, -2878.0, 0.0, 8.0, 1232.0, 0.0, 4.0],
|
||||||
// 57
|
// 57
|
||||||
[ 0, 0, 0, 1, 0, -4230.0, 0.0, 5.0, -20.0, 0.0, -2.0],
|
[0, 0, 0, 1, 0, -4230.0, 0.0, 5.0, -20.0, 0.0, -2.0],
|
||||||
// 58
|
// 58
|
||||||
[-1,-1, 2, 2, 2, -2819.0, 0.0, 7.0, 1207.0, 0.0, 3.0],
|
[-1, -1, 2, 2, 2, -2819.0, 0.0, 7.0, 1207.0, 0.0, 3.0],
|
||||||
// 59
|
// 59
|
||||||
[-1, 0, 2, 0, 0, -4056.0, 0.0, 5.0, 40.0, 0.0, -2.0],
|
[-1, 0, 2, 0, 0, -4056.0, 0.0, 5.0, 40.0, 0.0, -2.0],
|
||||||
// 60
|
// 60
|
||||||
[ 0,-1, 2, 2, 2, -2647.0, 0.0, 11.0, 1129.0, 0.0, 5.0],
|
[0, -1, 2, 2, 2, -2647.0, 0.0, 11.0, 1129.0, 0.0, 5.0],
|
||||||
// 61
|
// 61
|
||||||
[-2, 0, 0, 0, 1, -2294.0, 0.0, -10.0, 1266.0, 0.0, -4.0],
|
[-2, 0, 0, 0, 1, -2294.0, 0.0, -10.0, 1266.0, 0.0, -4.0],
|
||||||
// 62
|
// 62
|
||||||
[ 1, 1, 2, 0, 2, 2481.0, 0.0, -7.0, -1062.0, 0.0, -3.0],
|
[1, 1, 2, 0, 2, 2481.0, 0.0, -7.0, -1062.0, 0.0, -3.0],
|
||||||
// 63
|
// 63
|
||||||
[ 2, 0, 0, 0, 1, 2179.0, 0.0, -2.0, -1129.0, 0.0, -2.0],
|
[2, 0, 0, 0, 1, 2179.0, 0.0, -2.0, -1129.0, 0.0, -2.0],
|
||||||
// 64
|
// 64
|
||||||
[-1, 1, 0, 1, 0, 3276.0, 0.0, 1.0, -9.0, 0.0, 0.0],
|
[-1, 1, 0, 1, 0, 3276.0, 0.0, 1.0, -9.0, 0.0, 0.0],
|
||||||
// 65
|
// 65
|
||||||
[ 1, 1, 0, 0, 0, -3389.0, 0.0, 5.0, 35.0, 0.0, -2.0],
|
[1, 1, 0, 0, 0, -3389.0, 0.0, 5.0, 35.0, 0.0, -2.0],
|
||||||
// 66
|
// 66
|
||||||
[ 1, 0, 2, 0, 0, 3339.0, 0.0, -13.0, -107.0, 0.0, 1.0],
|
[1, 0, 2, 0, 0, 3339.0, 0.0, -13.0, -107.0, 0.0, 1.0],
|
||||||
// 67
|
// 67
|
||||||
[-1, 0, 2,-2, 1, -1987.0, 0.0, -6.0, 1073.0, 0.0, -2.0],
|
[-1, 0, 2, -2, 1, -1987.0, 0.0, -6.0, 1073.0, 0.0, -2.0],
|
||||||
// 68
|
// 68
|
||||||
[ 1, 0, 0, 0, 2, -1981.0, 0.0, 0.0, 854.0, 0.0, 0.0],
|
[1, 0, 0, 0, 2, -1981.0, 0.0, 0.0, 854.0, 0.0, 0.0],
|
||||||
// 69
|
// 69
|
||||||
[-1, 0, 0, 1, 0, 4026.0, 0.0, -353.0, -553.0, 0.0, -139.0],
|
[-1, 0, 0, 1, 0, 4026.0, 0.0, -353.0, -553.0, 0.0, -139.0],
|
||||||
// 70
|
// 70
|
||||||
[ 0, 0, 2, 1, 2, 1660.0, 0.0, -5.0, -710.0, 0.0, -2.0],
|
[0, 0, 2, 1, 2, 1660.0, 0.0, -5.0, -710.0, 0.0, -2.0],
|
||||||
// 71
|
// 71
|
||||||
[-1, 0, 2, 4, 2, -1521.0, 0.0, 9.0, 647.0, 0.0, 4.0],
|
[-1, 0, 2, 4, 2, -1521.0, 0.0, 9.0, 647.0, 0.0, 4.0],
|
||||||
// 72
|
// 72
|
||||||
[-1, 1, 0, 1, 1, 1464.0, 0.0, -11.0, -527.0, 0.0, -1.0],
|
[-1, 1, 0, 1, 1, 1464.0, 0.0, -11.0, -527.0, 0.0, -1.0],
|
||||||
// 73
|
// 73
|
||||||
[ 0,-2, 2,-2, 1, -1389.0, 0.0, 3.0, 656.0, 0.0, 1.0],
|
[0, -2, 2, -2, 1, -1389.0, 0.0, 3.0, 656.0, 0.0, 1.0],
|
||||||
// 74
|
// 74
|
||||||
[ 1,-1, 2, 2, 2, -1377.0, 0.0, 8.0, 594.0, 0.0, 4.0],
|
[1, -1, 2, 2, 2, -1377.0, 0.0, 8.0, 594.0, 0.0, 4.0],
|
||||||
// 75
|
// 75
|
||||||
[ 3, 0, 2,-2, 2, 1371.0, 0.0, -2.0, -588.0, 0.0, -1.0],
|
[3, 0, 2, -2, 2, 1371.0, 0.0, -2.0, -588.0, 0.0, -1.0],
|
||||||
// 76
|
// 76
|
||||||
[ 0, 0, 4,-2, 4, 1341.0, 0.0, 0.0, -577.0, 0.0, 0.0],
|
[0, 0, 4, -2, 4, 1341.0, 0.0, 0.0, -577.0, 0.0, 0.0],
|
||||||
// 77
|
// 77
|
||||||
[ 0, 0, 2,-2, 4, -1316.0, 0.0, 0.0, 567.0, 0.0, 0.0],
|
[0, 0, 2, -2, 4, -1316.0, 0.0, 0.0, 567.0, 0.0, 0.0],
|
||||||
]
|
]
|
||||||
|
|
||||||
// ─── Fundamental arguments (Delaunay) ────────────────────────────────────────
|
// ─── Fundamental arguments (Delaunay) ────────────────────────────────────────
|
||||||
|
|
@ -224,35 +222,35 @@ function arcsecToRad(arcsec: number): number {
|
||||||
/** Mean anomaly of the Moon l (IAU 2003) */
|
/** Mean anomaly of the Moon l (IAU 2003) */
|
||||||
function fundamentalL(T: number): number {
|
function fundamentalL(T: number): number {
|
||||||
return arcsecToRad(
|
return arcsecToRad(
|
||||||
485868.249036 + T * (1717915923.2178 + T * (31.8792 + T * (0.051635 + T * (-0.00024470))))
|
485868.249036 + T * (1717915923.2178 + T * (31.8792 + T * (0.051635 + T * -0.0002447))),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Mean anomaly of the Sun l' (IAU 2003) */
|
/** Mean anomaly of the Sun l' (IAU 2003) */
|
||||||
function fundamentalLp(T: number): number {
|
function fundamentalLp(T: number): number {
|
||||||
return arcsecToRad(
|
return arcsecToRad(
|
||||||
1287104.793048 + T * (129596581.0481 + T * (-0.5532 + T * (0.000136 + T * (-0.00001149))))
|
1287104.793048 + T * (129596581.0481 + T * (-0.5532 + T * (0.000136 + T * -0.00001149))),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Moon's argument of latitude F = L - Ω (IAU 2003) */
|
/** Moon's argument of latitude F = L - Ω (IAU 2003) */
|
||||||
function fundamentalF(T: number): number {
|
function fundamentalF(T: number): number {
|
||||||
return arcsecToRad(
|
return arcsecToRad(
|
||||||
335779.526232 + T * (1739527262.8478 + T * (-12.7512 + T * (-0.001037 + T * 0.00000417)))
|
335779.526232 + T * (1739527262.8478 + T * (-12.7512 + T * (-0.001037 + T * 0.00000417))),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Mean elongation of the Moon D (IAU 2003) */
|
/** Mean elongation of the Moon D (IAU 2003) */
|
||||||
function fundamentalD(T: number): number {
|
function fundamentalD(T: number): number {
|
||||||
return arcsecToRad(
|
return arcsecToRad(
|
||||||
1072260.703692 + T * (1602961601.2090 + T * (-6.3706 + T * (0.006593 + T * (-0.00003169))))
|
1072260.703692 + T * (1602961601.209 + T * (-6.3706 + T * (0.006593 + T * -0.00003169))),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Longitude of Moon's ascending node Ω (IAU 2003) */
|
/** Longitude of Moon's ascending node Ω (IAU 2003) */
|
||||||
function fundamentalOm(T: number): number {
|
function fundamentalOm(T: number): number {
|
||||||
return arcsecToRad(
|
return arcsecToRad(
|
||||||
450160.398036 + T * (-6962890.5431 + T * (7.4722 + T * (0.007702 + T * (-0.00005939))))
|
450160.398036 + T * (-6962890.5431 + T * (7.4722 + T * (0.007702 + T * -0.00005939))),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -274,24 +272,21 @@ function fundamentalOm(T: number): number {
|
||||||
* @param jdTT - Julian Date in TT
|
* @param jdTT - Julian Date in TT
|
||||||
* @returns { X, Y, s } in radians
|
* @returns { X, Y, s } in radians
|
||||||
*/
|
*/
|
||||||
export function computeCIPXYs(
|
export function computeCIPXYs(jdTT: number): { X: number; Y: number; s: number } {
|
||||||
jdTT: number,
|
|
||||||
): { X: number; Y: number; s: number } {
|
|
||||||
|
|
||||||
const T = (jdTT - J2000) / DAYS_PER_JULIAN_CENTURY
|
const T = (jdTT - J2000) / DAYS_PER_JULIAN_CENTURY
|
||||||
|
|
||||||
// Delaunay fundamental arguments
|
// Delaunay fundamental arguments
|
||||||
const l = fundamentalL(T)
|
const l = fundamentalL(T)
|
||||||
const lp = fundamentalLp(T)
|
const lp = fundamentalLp(T)
|
||||||
const F = fundamentalF(T)
|
const F = fundamentalF(T)
|
||||||
const D = fundamentalD(T)
|
const D = fundamentalD(T)
|
||||||
const Om = fundamentalOm(T)
|
const Om = fundamentalOm(T)
|
||||||
|
|
||||||
// Accumulate nutation in longitude (dpsi) and obliquity (deps) — units: 0.1 uas
|
// Accumulate nutation in longitude (dpsi) and obliquity (deps) — units: 0.1 uas
|
||||||
let dpsi = 0.0
|
let dpsi = 0.0
|
||||||
let deps = 0.0
|
let deps = 0.0
|
||||||
for (const [nl, nlp, nF, nD, nOm, ps, pst, pc, ec, ect, es] of NUT_2000B) {
|
for (const [nl, nlp, nF, nD, nOm, ps, pst, pc, ec, ect, es] of NUT_2000B) {
|
||||||
const arg = nl*l + nlp*lp + nF*F + nD*D + nOm*Om
|
const arg = nl * l + nlp * lp + nF * F + nD * D + nOm * Om
|
||||||
const sinA = Math.sin(arg)
|
const sinA = Math.sin(arg)
|
||||||
const cosA = Math.cos(arg)
|
const cosA = Math.cos(arg)
|
||||||
dpsi += (ps + pst * T) * sinA + pc * cosA
|
dpsi += (ps + pst * T) * sinA + pc * cosA
|
||||||
|
|
@ -304,34 +299,24 @@ export function computeCIPXYs(
|
||||||
|
|
||||||
// Mean obliquity eps0 (IAU 2006, arcseconds → radians)
|
// Mean obliquity eps0 (IAU 2006, arcseconds → radians)
|
||||||
// Reference: IERS Conventions (2010) Table 5.1
|
// Reference: IERS Conventions (2010) Table 5.1
|
||||||
const eps0 = (
|
const eps0 =
|
||||||
84381.406
|
(84381.406 +
|
||||||
+ T * (-46.836769
|
T *
|
||||||
+ T * (-0.0001831
|
(-46.836769 +
|
||||||
+ T * ( 0.00200340
|
T * (-0.0001831 + T * (0.0020034 + T * (-0.000000576 + T * -0.0000000434))))) *
|
||||||
+ T * (-0.000000576
|
ARCSEC_RAD
|
||||||
+ T * (-0.0000000434)))))
|
|
||||||
) * ARCSEC_RAD
|
|
||||||
|
|
||||||
// IAU 2006 precession polynomial for X (arcseconds)
|
// IAU 2006 precession polynomial for X (arcseconds)
|
||||||
// Reference: IERS Conventions (2010) Table 5.2a, polynomial s_X
|
// Reference: IERS Conventions (2010) Table 5.2a, polynomial s_X
|
||||||
const Xarcsec =
|
const Xarcsec =
|
||||||
-0.016617
|
-0.016617 +
|
||||||
+ T * ( 2004.191898
|
T * (2004.191898 + T * (-0.4297829 + T * (-0.19861834 + T * (0.000007578 + T * 0.0000059285))))
|
||||||
+ T * ( -0.4297829
|
|
||||||
+ T * ( -0.19861834
|
|
||||||
+ T * ( 0.000007578
|
|
||||||
+ T * 0.0000059285))))
|
|
||||||
|
|
||||||
// IAU 2006 precession polynomial for Y (arcseconds)
|
// IAU 2006 precession polynomial for Y (arcseconds)
|
||||||
// Reference: IERS Conventions (2010) Table 5.2a, polynomial s_Y
|
// Reference: IERS Conventions (2010) Table 5.2a, polynomial s_Y
|
||||||
const Yarcsec =
|
const Yarcsec =
|
||||||
-0.006951
|
-0.006951 +
|
||||||
+ T * ( -0.025896
|
T * (-0.025896 + T * (-22.4072747 + T * (0.00190059 + T * (0.001112526 + T * 0.0000001358))))
|
||||||
+ T * ( -22.4072747
|
|
||||||
+ T * ( 0.00190059
|
|
||||||
+ T * ( 0.001112526
|
|
||||||
+ T * 0.0000001358))))
|
|
||||||
|
|
||||||
// CIP X, Y: precession polynomial + first-order nutation correction
|
// CIP X, Y: precession polynomial + first-order nutation correction
|
||||||
const X = Xarcsec * ARCSEC_RAD + dpsiRad * Math.sin(eps0)
|
const X = Xarcsec * ARCSEC_RAD + dpsiRad * Math.sin(eps0)
|
||||||
|
|
@ -340,7 +325,7 @@ export function computeCIPXYs(
|
||||||
// CIO locator s ≈ -X·Y/2 + small polynomial (IERS Conventions 2010 Eq. 5.9)
|
// CIO locator s ≈ -X·Y/2 + small polynomial (IERS Conventions 2010 Eq. 5.9)
|
||||||
// Polynomial term: s_poly ≈ -0.041775"·T (arcseconds)
|
// Polynomial term: s_poly ≈ -0.041775"·T (arcseconds)
|
||||||
const sPoly = -0.041775 * T * ARCSEC_RAD
|
const sPoly = -0.041775 * T * ARCSEC_RAD
|
||||||
const s = -X * Y / 2 + sPoly
|
const s = (-X * Y) / 2 + sPoly
|
||||||
|
|
||||||
return { X, Y, s }
|
return { X, Y, s }
|
||||||
}
|
}
|
||||||
|
|
@ -361,7 +346,7 @@ export function computeCIPXYs(
|
||||||
*/
|
*/
|
||||||
export function computeERA(jdUT1: number): number {
|
export function computeERA(jdUT1: number): number {
|
||||||
const Du = jdUT1 - 2451545.0
|
const Du = jdUT1 - 2451545.0
|
||||||
const era = 2 * Math.PI * (0.7790572732640 + 1.00273781191135448 * Du)
|
const era = 2 * Math.PI * (0.779057273264 + 1.0027378119113546 * Du)
|
||||||
return ((era % (2 * Math.PI)) + 2 * Math.PI) % (2 * Math.PI)
|
return ((era % (2 * Math.PI)) + 2 * Math.PI) % (2 * Math.PI)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -422,12 +407,7 @@ export function polarMotionMatrix(xp: number, yp: number): Mat3 {
|
||||||
* @param yp - Polar motion y (radians, default 0)
|
* @param yp - Polar motion y (radians, default 0)
|
||||||
* @returns Vector in ITRS frame (km)
|
* @returns Vector in ITRS frame (km)
|
||||||
*/
|
*/
|
||||||
export function gcrsToItrs(
|
export function gcrsToItrs(gcrsVec: Vec3, ts: TimeScales, xp = 0, yp = 0): Vec3 {
|
||||||
gcrsVec: Vec3,
|
|
||||||
ts: TimeScales,
|
|
||||||
xp = 0,
|
|
||||||
yp = 0,
|
|
||||||
): Vec3 {
|
|
||||||
const { X, Y, s } = computeCIPXYs(ts.jdTT)
|
const { X, Y, s } = computeCIPXYs(ts.jdTT)
|
||||||
const Q = celestialMotionMatrix(X, Y, s)
|
const Q = celestialMotionMatrix(X, Y, s)
|
||||||
const era = computeERA(ts.jdUT1)
|
const era = computeERA(ts.jdUT1)
|
||||||
|
|
@ -449,12 +429,7 @@ export function gcrsToItrs(
|
||||||
* @param yp - Polar motion y (radians, default 0)
|
* @param yp - Polar motion y (radians, default 0)
|
||||||
* @returns Vector in GCRS frame (km)
|
* @returns Vector in GCRS frame (km)
|
||||||
*/
|
*/
|
||||||
export function itrsToGcrs(
|
export function itrsToGcrs(itrsVec: Vec3, ts: TimeScales, xp = 0, yp = 0): Vec3 {
|
||||||
itrsVec: Vec3,
|
|
||||||
ts: TimeScales,
|
|
||||||
xp = 0,
|
|
||||||
yp = 0,
|
|
||||||
): Vec3 {
|
|
||||||
const { X, Y, s } = computeCIPXYs(ts.jdTT)
|
const { X, Y, s } = computeCIPXYs(ts.jdTT)
|
||||||
const Q = celestialMotionMatrix(X, Y, s)
|
const Q = celestialMotionMatrix(X, Y, s)
|
||||||
const era = computeERA(ts.jdUT1)
|
const era = computeERA(ts.jdUT1)
|
||||||
|
|
|
||||||
|
|
@ -36,11 +36,7 @@ export function vnorm(a: Vec3): number {
|
||||||
|
|
||||||
/** Cross product */
|
/** Cross product */
|
||||||
export function vcross(a: Vec3, b: Vec3): Vec3 {
|
export function vcross(a: Vec3, b: Vec3): Vec3 {
|
||||||
return [
|
return [a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0]]
|
||||||
a[1] * b[2] - a[2] * b[1],
|
|
||||||
a[2] * b[0] - a[0] * b[2],
|
|
||||||
a[0] * b[1] - a[1] * b[0],
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Unit vector (normalized) */
|
/** Unit vector (normalized) */
|
||||||
|
|
@ -59,11 +55,7 @@ export function angularSep(a: Vec3, b: Vec3): number {
|
||||||
// ─── 3×3 matrix operations ────────────────────────────────────────────────────
|
// ─── 3×3 matrix operations ────────────────────────────────────────────────────
|
||||||
|
|
||||||
/** 3×3 matrix stored row-major as a 9-element tuple */
|
/** 3×3 matrix stored row-major as a 9-element tuple */
|
||||||
export type Mat3 = [
|
export type Mat3 = [number, number, number, number, number, number, number, number, number]
|
||||||
number, number, number,
|
|
||||||
number, number, number,
|
|
||||||
number, number, number,
|
|
||||||
]
|
|
||||||
|
|
||||||
/** Multiply 3×3 matrix by 3-vector */
|
/** Multiply 3×3 matrix by 3-vector */
|
||||||
export function mvmul(m: Mat3, v: Vec3): Vec3 {
|
export function mvmul(m: Mat3, v: Vec3): Vec3 {
|
||||||
|
|
@ -77,15 +69,15 @@ export function mvmul(m: Mat3, v: Vec3): Vec3 {
|
||||||
/** Multiply two 3×3 matrices */
|
/** Multiply two 3×3 matrices */
|
||||||
export function mmmul(a: Mat3, b: Mat3): Mat3 {
|
export function mmmul(a: Mat3, b: Mat3): Mat3 {
|
||||||
return [
|
return [
|
||||||
a[0]*b[0] + a[1]*b[3] + a[2]*b[6],
|
a[0] * b[0] + a[1] * b[3] + a[2] * b[6],
|
||||||
a[0]*b[1] + a[1]*b[4] + a[2]*b[7],
|
a[0] * b[1] + a[1] * b[4] + a[2] * b[7],
|
||||||
a[0]*b[2] + a[1]*b[5] + a[2]*b[8],
|
a[0] * b[2] + a[1] * b[5] + a[2] * b[8],
|
||||||
a[3]*b[0] + a[4]*b[3] + a[5]*b[6],
|
a[3] * b[0] + a[4] * b[3] + a[5] * b[6],
|
||||||
a[3]*b[1] + a[4]*b[4] + a[5]*b[7],
|
a[3] * b[1] + a[4] * b[4] + a[5] * b[7],
|
||||||
a[3]*b[2] + a[4]*b[5] + a[5]*b[8],
|
a[3] * b[2] + a[4] * b[5] + a[5] * b[8],
|
||||||
a[6]*b[0] + a[7]*b[3] + a[8]*b[6],
|
a[6] * b[0] + a[7] * b[3] + a[8] * b[6],
|
||||||
a[6]*b[1] + a[7]*b[4] + a[8]*b[7],
|
a[6] * b[1] + a[7] * b[4] + a[8] * b[7],
|
||||||
a[6]*b[2] + a[7]*b[5] + a[8]*b[8],
|
a[6] * b[2] + a[7] * b[5] + a[8] * b[8],
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -173,14 +165,18 @@ export function chebyshevEvalWithDerivative(
|
||||||
if (n === 1) return [coeffs[0], 0]
|
if (n === 1) return [coeffs[0], 0]
|
||||||
|
|
||||||
const x2 = 2 * x
|
const x2 = 2 * x
|
||||||
let b2 = 0; let b1 = 0
|
let b2 = 0
|
||||||
let db2 = 0; let db1 = 0
|
let b1 = 0
|
||||||
|
let db2 = 0
|
||||||
|
let db1 = 0
|
||||||
|
|
||||||
for (let k = n - 1; k >= 1; k--) {
|
for (let k = n - 1; k >= 1; k--) {
|
||||||
const b0 = coeffs[k] + x2 * b1 - b2
|
const b0 = coeffs[k] + x2 * b1 - b2
|
||||||
const db0 = 2 * b1 + x2 * db1 - db2
|
const db0 = 2 * b1 + x2 * db1 - db2
|
||||||
b2 = b1; b1 = b0
|
b2 = b1
|
||||||
db2 = db1; db1 = db0
|
b1 = b0
|
||||||
|
db2 = db1
|
||||||
|
db1 = db0
|
||||||
}
|
}
|
||||||
|
|
||||||
const value = coeffs[0] + x * b1 - b2
|
const value = coeffs[0] + x * b1 - b2
|
||||||
|
|
@ -227,7 +223,7 @@ export function brentRoot(
|
||||||
let c = a
|
let c = a
|
||||||
let fc = fa
|
let fc = fa
|
||||||
let mflag = true
|
let mflag = true
|
||||||
let s = 0
|
let s: number
|
||||||
let d = 0
|
let d = 0
|
||||||
|
|
||||||
for (let i = 0; i < maxIter; i++) {
|
for (let i = 0; i < maxIter; i++) {
|
||||||
|
|
@ -289,12 +285,7 @@ export function brentRoot(
|
||||||
* @param steps - Number of initial subdivision steps (default 48 for 30-min resolution over a day)
|
* @param steps - Number of initial subdivision steps (default 48 for 30-min resolution over a day)
|
||||||
* @returns Array of root locations
|
* @returns Array of root locations
|
||||||
*/
|
*/
|
||||||
export function findRoots(
|
export function findRoots(f: (t: number) => number, a: number, b: number, steps = 48): number[] {
|
||||||
f: (t: number) => number,
|
|
||||||
a: number,
|
|
||||||
b: number,
|
|
||||||
steps = 48,
|
|
||||||
): number[] {
|
|
||||||
const dt = (b - a) / steps
|
const dt = (b - a) / steps
|
||||||
const roots: number[] = []
|
const roots: number[] = []
|
||||||
let tPrev = a
|
let tPrev = a
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@
|
||||||
import type { Vec3, Observer, AzAlt, TimeScales } from '../types.js'
|
import type { Vec3, Observer, AzAlt, TimeScales } from '../types.js'
|
||||||
import { WGS84 } from '../types.js'
|
import { WGS84 } from '../types.js'
|
||||||
import { gcrsToItrs } from '../frames/index.js'
|
import { gcrsToItrs } from '../frames/index.js'
|
||||||
|
import { vdot } from '../math/index.js'
|
||||||
|
|
||||||
// ─── Geodetic ↔ ECEF ─────────────────────────────────────────────────────────
|
// ─── Geodetic ↔ ECEF ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
@ -93,12 +94,14 @@ export function ecefToGeodetic(ecef: Vec3): { lat: number; lon: number; h: numbe
|
||||||
export function computeENUBasis(lat: number, lon: number): { east: Vec3; north: Vec3; up: Vec3 } {
|
export function computeENUBasis(lat: number, lon: number): { east: Vec3; north: Vec3; up: Vec3 } {
|
||||||
const phi = (lat * Math.PI) / 180
|
const phi = (lat * Math.PI) / 180
|
||||||
const lam = (lon * Math.PI) / 180
|
const lam = (lon * Math.PI) / 180
|
||||||
const sinPhi = Math.sin(phi), cosPhi = Math.cos(phi)
|
const sinPhi = Math.sin(phi),
|
||||||
const sinLam = Math.sin(lam), cosLam = Math.cos(lam)
|
cosPhi = Math.cos(phi)
|
||||||
|
const sinLam = Math.sin(lam),
|
||||||
|
cosLam = Math.cos(lam)
|
||||||
|
|
||||||
const east: Vec3 = [-sinLam, cosLam, 0]
|
const east: Vec3 = [-sinLam, cosLam, 0]
|
||||||
const north: Vec3 = [-sinPhi * cosLam, -sinPhi * sinLam, cosPhi]
|
const north: Vec3 = [-sinPhi * cosLam, -sinPhi * sinLam, cosPhi]
|
||||||
const up: Vec3 = [ cosPhi * cosLam, cosPhi * sinLam, sinPhi]
|
const up: Vec3 = [cosPhi * cosLam, cosPhi * sinLam, sinPhi]
|
||||||
|
|
||||||
return { east, north, up }
|
return { east, north, up }
|
||||||
}
|
}
|
||||||
|
|
@ -114,8 +117,7 @@ export function computeENUBasis(lat: number, lon: number): { east: Vec3; north:
|
||||||
*/
|
*/
|
||||||
export function ecefToENU(ecefDelta: Vec3, lat: number, lon: number): Vec3 {
|
export function ecefToENU(ecefDelta: Vec3, lat: number, lon: number): Vec3 {
|
||||||
const { east, north, up } = computeENUBasis(lat, lon)
|
const { east, north, up } = computeENUBasis(lat, lon)
|
||||||
const dot = (a: Vec3, b: Vec3) => a[0]*b[0] + a[1]*b[1] + a[2]*b[2]
|
return [vdot(ecefDelta, east), vdot(ecefDelta, north), vdot(ecefDelta, up)]
|
||||||
return [dot(ecefDelta, east), dot(ecefDelta, north), dot(ecefDelta, up)]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -185,7 +187,6 @@ export function computeAzAlt(
|
||||||
ts: TimeScales,
|
ts: TimeScales,
|
||||||
airless: boolean,
|
airless: boolean,
|
||||||
): AzAlt {
|
): AzAlt {
|
||||||
|
|
||||||
// 1. Convert body position from GCRS to ITRS (km)
|
// 1. Convert body position from GCRS to ITRS (km)
|
||||||
const bodyITRS = gcrsToItrs(bodyGCRS, ts)
|
const bodyITRS = gcrsToItrs(bodyGCRS, ts)
|
||||||
|
|
||||||
|
|
@ -194,11 +195,7 @@ export function computeAzAlt(
|
||||||
const obsITRS: Vec3 = [obsECEF[0] / 1000, obsECEF[1] / 1000, obsECEF[2] / 1000]
|
const obsITRS: Vec3 = [obsECEF[0] / 1000, obsECEF[1] / 1000, obsECEF[2] / 1000]
|
||||||
|
|
||||||
// 3. Displacement vector from observer to body in ITRS (km — magnitude doesn't matter)
|
// 3. Displacement vector from observer to body in ITRS (km — magnitude doesn't matter)
|
||||||
const delta: Vec3 = [
|
const delta: Vec3 = [bodyITRS[0] - obsITRS[0], bodyITRS[1] - obsITRS[1], bodyITRS[2] - obsITRS[2]]
|
||||||
bodyITRS[0] - obsITRS[0],
|
|
||||||
bodyITRS[1] - obsITRS[1],
|
|
||||||
bodyITRS[2] - obsITRS[2],
|
|
||||||
]
|
|
||||||
|
|
||||||
// 4. Project onto local ENU basis at the observer's location
|
// 4. Project onto local ENU basis at the observer's location
|
||||||
const enu = ecefToENU(delta, observer.lat, observer.lon)
|
const enu = ecefToENU(delta, observer.lat, observer.lon)
|
||||||
|
|
@ -208,11 +205,7 @@ export function computeAzAlt(
|
||||||
|
|
||||||
// 6. Refraction correction
|
// 6. Refraction correction
|
||||||
if (!airless) {
|
if (!airless) {
|
||||||
azAlt.altitude = applyRefraction(
|
azAlt.altitude = applyRefraction(azAlt.altitude, observer.pressure, observer.temperature)
|
||||||
azAlt.altitude,
|
|
||||||
observer.pressure,
|
|
||||||
observer.temperature,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return azAlt
|
return azAlt
|
||||||
|
|
@ -261,11 +254,7 @@ export function bennettRefraction(
|
||||||
* Apply refraction correction to an airless altitude.
|
* Apply refraction correction to an airless altitude.
|
||||||
* Returns the apparent (observed) altitude.
|
* Returns the apparent (observed) altitude.
|
||||||
*/
|
*/
|
||||||
export function applyRefraction(
|
export function applyRefraction(airlessAlt: number, pressure = 1013.25, temperature = 15): number {
|
||||||
airlessAlt: number,
|
|
||||||
pressure = 1013.25,
|
|
||||||
temperature = 15,
|
|
||||||
): number {
|
|
||||||
return airlessAlt + bennettRefraction(airlessAlt, pressure, temperature)
|
return airlessAlt + bennettRefraction(airlessAlt, pressure, temperature)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -31,10 +31,10 @@ import { chebyshevEvalWithDerivative } from '../math/index.js'
|
||||||
|
|
||||||
/** NAIF integer body IDs used in DE442S segment chaining */
|
/** NAIF integer body IDs used in DE442S segment chaining */
|
||||||
export const NAIF_IDS = {
|
export const NAIF_IDS = {
|
||||||
SSB: 0, // Solar System Barycenter
|
SSB: 0, // Solar System Barycenter
|
||||||
MERCURY_BARYCENTER: 1,
|
MERCURY_BARYCENTER: 1,
|
||||||
VENUS_BARYCENTER: 2,
|
VENUS_BARYCENTER: 2,
|
||||||
EMB: 3, // Earth-Moon Barycenter
|
EMB: 3, // Earth-Moon Barycenter
|
||||||
MARS_BARYCENTER: 4,
|
MARS_BARYCENTER: 4,
|
||||||
JUPITER_BARYCENTER: 5,
|
JUPITER_BARYCENTER: 5,
|
||||||
SATURN_BARYCENTER: 6,
|
SATURN_BARYCENTER: 6,
|
||||||
|
|
@ -107,7 +107,7 @@ export class SpkKernel {
|
||||||
private findSeg(target: number, center: number, et: number): SpkSegment | null {
|
private findSeg(target: number, center: number, et: number): SpkSegment | null {
|
||||||
const candidates = this.index.get(`${target}:${center}`)
|
const candidates = this.index.get(`${target}:${center}`)
|
||||||
if (!candidates) return null
|
if (!candidates) return null
|
||||||
return candidates.find(s => et >= s.startET && et <= s.endET) ?? null
|
return candidates.find((s) => et >= s.startET && et <= s.endET) ?? null
|
||||||
}
|
}
|
||||||
|
|
||||||
private getChained(target: number, center: number, et: number): StateVector {
|
private getChained(target: number, center: number, et: number): StateVector {
|
||||||
|
|
@ -174,7 +174,12 @@ export class SpkKernel {
|
||||||
// ─── DAF parsing ──────────────────────────────────────────────────────────────
|
// ─── DAF parsing ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
function parseDafFileRecord(buffer: ArrayBuffer): {
|
function parseDafFileRecord(buffer: ArrayBuffer): {
|
||||||
nd: number; ni: number; fward: number; bward: number; free: number; le: boolean
|
nd: number
|
||||||
|
ni: number
|
||||||
|
fward: number
|
||||||
|
bward: number
|
||||||
|
free: number
|
||||||
|
le: boolean
|
||||||
} {
|
} {
|
||||||
const dv = new DataView(buffer)
|
const dv = new DataView(buffer)
|
||||||
|
|
||||||
|
|
@ -214,7 +219,7 @@ function parseSummaryRecords(
|
||||||
const nextRecord = dv.getFloat64(recOffset, le)
|
const nextRecord = dv.getFloat64(recOffset, le)
|
||||||
const nSummaries = Math.round(dv.getFloat64(recOffset + 16, le))
|
const nSummaries = Math.round(dv.getFloat64(recOffset + 16, le))
|
||||||
|
|
||||||
let offset = recOffset + 24 // skip 3 control doubles (24 bytes)
|
let offset = recOffset + 24 // skip 3 control doubles (24 bytes)
|
||||||
|
|
||||||
for (let i = 0; i < nSummaries; i++) {
|
for (let i = 0; i < nSummaries; i++) {
|
||||||
if (offset + summaryBytes > buffer.byteLength) break
|
if (offset + summaryBytes > buffer.byteLength) break
|
||||||
|
|
@ -383,8 +388,18 @@ export function parseLsk(text: string): ReadonlyArray<readonly [number, number]>
|
||||||
|
|
||||||
const block = match[1]
|
const block = match[1]
|
||||||
const months: Record<string, number> = {
|
const months: Record<string, number> = {
|
||||||
JAN: 1, FEB: 2, MAR: 3, APR: 4, MAY: 5, JUN: 6,
|
JAN: 1,
|
||||||
JUL: 7, AUG: 8, SEP: 9, OCT: 10, NOV: 11, DEC: 12,
|
FEB: 2,
|
||||||
|
MAR: 3,
|
||||||
|
APR: 4,
|
||||||
|
MAY: 5,
|
||||||
|
JUN: 6,
|
||||||
|
JUL: 7,
|
||||||
|
AUG: 8,
|
||||||
|
SEP: 9,
|
||||||
|
OCT: 10,
|
||||||
|
NOV: 11,
|
||||||
|
DEC: 12,
|
||||||
}
|
}
|
||||||
|
|
||||||
const pairRe = /(-?\d+(?:\.\d+)?)\s*,\s*@(\d{4})-([A-Z]{3})-(\d{1,2})/g
|
const pairRe = /(-?\d+(?:\.\d+)?)\s*,\s*@(\d{4})-([A-Z]{3})-(\d{1,2})/g
|
||||||
|
|
@ -400,8 +415,14 @@ export function parseLsk(text: string): ReadonlyArray<readonly [number, number]>
|
||||||
const a = Math.floor((14 - month) / 12)
|
const a = Math.floor((14 - month) / 12)
|
||||||
const y = year + 4800 - a
|
const y = year + 4800 - a
|
||||||
const mo = month + 12 * a - 3
|
const mo = month + 12 * a - 3
|
||||||
const jdNoon = day + Math.floor((153 * mo + 2) / 5) + 365 * y +
|
const jdNoon =
|
||||||
Math.floor(y / 4) - Math.floor(y / 100) + Math.floor(y / 400) - 32045
|
day +
|
||||||
|
Math.floor((153 * mo + 2) / 5) +
|
||||||
|
365 * y +
|
||||||
|
Math.floor(y / 4) -
|
||||||
|
Math.floor(y / 100) +
|
||||||
|
Math.floor(y / 400) -
|
||||||
|
32045
|
||||||
// Midnight = JD - 0.5
|
// Midnight = JD - 0.5
|
||||||
results.push([jdNoon - 0.5, deltaAT])
|
results.push([jdNoon - 0.5, deltaAT])
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -202,55 +202,74 @@ export function deltaTPolynomial(jdTT: number): number {
|
||||||
} else if (y < 500) {
|
} else if (y < 500) {
|
||||||
const u = y / 100
|
const u = y / 100
|
||||||
return (
|
return (
|
||||||
10583.6 - 1014.41 * u + 33.78311 * u * u - 5.952053 * u * u * u -
|
10583.6 -
|
||||||
0.1798452 * u ** 4 + 0.022174192 * u ** 5 + 0.0090316521 * u ** 6
|
1014.41 * u +
|
||||||
|
33.78311 * u * u -
|
||||||
|
5.952053 * u * u * u -
|
||||||
|
0.1798452 * u ** 4 +
|
||||||
|
0.022174192 * u ** 5 +
|
||||||
|
0.0090316521 * u ** 6
|
||||||
)
|
)
|
||||||
} else if (y < 1600) {
|
} else if (y < 1600) {
|
||||||
const u = (y - 1000) / 100
|
const u = (y - 1000) / 100
|
||||||
return (
|
return (
|
||||||
1574.2 - 556.01 * u + 71.23472 * u * u + 0.319781 * u ** 3 -
|
1574.2 -
|
||||||
0.8503463 * u ** 4 - 0.005050998 * u ** 5 + 0.0083572073 * u ** 6
|
556.01 * u +
|
||||||
|
71.23472 * u * u +
|
||||||
|
0.319781 * u ** 3 -
|
||||||
|
0.8503463 * u ** 4 -
|
||||||
|
0.005050998 * u ** 5 +
|
||||||
|
0.0083572073 * u ** 6
|
||||||
)
|
)
|
||||||
} else if (y < 1700) {
|
} else if (y < 1700) {
|
||||||
const t = y - 1600
|
const t = y - 1600
|
||||||
return 120 - 0.9808 * t - 0.01532 * t * t + t ** 3 / 7129
|
return 120 - 0.9808 * t - 0.01532 * t * t + t ** 3 / 7129
|
||||||
} else if (y < 1800) {
|
} else if (y < 1800) {
|
||||||
const t = y - 1700
|
const t = y - 1700
|
||||||
return (
|
return 8.83 + 0.1603 * t - 0.0059285 * t * t + 0.00013336 * t ** 3 - t ** 4 / 1174000
|
||||||
8.83 + 0.1603 * t - 0.0059285 * t * t + 0.00013336 * t ** 3 - t ** 4 / 1174000
|
|
||||||
)
|
|
||||||
} else if (y < 1860) {
|
} else if (y < 1860) {
|
||||||
const t = y - 1800
|
const t = y - 1800
|
||||||
return (
|
return (
|
||||||
13.72 - 0.332447 * t + 0.0068612 * t * t + 0.0041116 * t ** 3 -
|
13.72 -
|
||||||
0.00037436 * t ** 4 + 0.0000121272 * t ** 5 -
|
0.332447 * t +
|
||||||
0.0000001699 * t ** 6 + 0.000000000875 * t ** 7
|
0.0068612 * t * t +
|
||||||
|
0.0041116 * t ** 3 -
|
||||||
|
0.00037436 * t ** 4 +
|
||||||
|
0.0000121272 * t ** 5 -
|
||||||
|
0.0000001699 * t ** 6 +
|
||||||
|
0.000000000875 * t ** 7
|
||||||
)
|
)
|
||||||
} else if (y < 1900) {
|
} else if (y < 1900) {
|
||||||
const t = y - 1860
|
const t = y - 1860
|
||||||
return (
|
return (
|
||||||
7.62 + 0.5737 * t - 0.251754 * t * t + 0.01680668 * t ** 3 -
|
7.62 +
|
||||||
0.0004473624 * t ** 4 + t ** 5 / 233174
|
0.5737 * t -
|
||||||
|
0.251754 * t * t +
|
||||||
|
0.01680668 * t ** 3 -
|
||||||
|
0.0004473624 * t ** 4 +
|
||||||
|
t ** 5 / 233174
|
||||||
)
|
)
|
||||||
} else if (y < 1920) {
|
} else if (y < 1920) {
|
||||||
const t = y - 1900
|
const t = y - 1900
|
||||||
return (
|
return -2.79 + 1.494119 * t - 0.0598939 * t * t + 0.0061966 * t ** 3 - 0.000197 * t ** 4
|
||||||
-2.79 + 1.494119 * t - 0.0598939 * t * t + 0.0061966 * t ** 3 - 0.000197 * t ** 4
|
|
||||||
)
|
|
||||||
} else if (y < 1941) {
|
} else if (y < 1941) {
|
||||||
const t = y - 1920
|
const t = y - 1920
|
||||||
return 21.20 + 0.84493 * t - 0.076100 * t * t + 0.0020936 * t ** 3
|
return 21.2 + 0.84493 * t - 0.0761 * t * t + 0.0020936 * t ** 3
|
||||||
} else if (y < 1961) {
|
} else if (y < 1961) {
|
||||||
const t = y - 1950
|
const t = y - 1950
|
||||||
return 29.07 + 0.407 * t - t * t / 233 + t ** 3 / 2547
|
return 29.07 + 0.407 * t - (t * t) / 233 + t ** 3 / 2547
|
||||||
} else if (y < 1986) {
|
} else if (y < 1986) {
|
||||||
const t = y - 1975
|
const t = y - 1975
|
||||||
return 45.45 + 1.067 * t - t * t / 260 - t ** 3 / 718
|
return 45.45 + 1.067 * t - (t * t) / 260 - t ** 3 / 718
|
||||||
} else if (y < 2005) {
|
} else if (y < 2005) {
|
||||||
const t = y - 2000
|
const t = y - 2000
|
||||||
return (
|
return (
|
||||||
63.86 + 0.3345 * t - 0.060374 * t * t + 0.0017275 * t ** 3 +
|
63.86 +
|
||||||
0.000651814 * t ** 4 + 0.00002373599 * t ** 5
|
0.3345 * t -
|
||||||
|
0.060374 * t * t +
|
||||||
|
0.0017275 * t ** 3 +
|
||||||
|
0.000651814 * t ** 4 +
|
||||||
|
0.00002373599 * t ** 5
|
||||||
)
|
)
|
||||||
} else if (y < 2050) {
|
} else if (y < 2050) {
|
||||||
const t = y - 2000
|
const t = y - 2000
|
||||||
|
|
|
||||||
18
src/types.ts
18
src/types.ts
|
|
@ -5,8 +5,8 @@ export type Vec3 = [number, number, number]
|
||||||
|
|
||||||
/** Position + velocity state vector from the ephemeris */
|
/** Position + velocity state vector from the ephemeris */
|
||||||
export interface StateVector {
|
export interface StateVector {
|
||||||
position: Vec3 // km, in the frame determined by context
|
position: Vec3 // km, in the frame determined by context
|
||||||
velocity: Vec3 // km/s
|
velocity: Vec3 // km/s
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Azimuth + altitude in degrees */
|
/** Azimuth + altitude in degrees */
|
||||||
|
|
@ -154,7 +154,7 @@ export type YallopCategory = 'A' | 'B' | 'C' | 'D' | 'E' | 'F'
|
||||||
export const YALLOP_THRESHOLDS = {
|
export const YALLOP_THRESHOLDS = {
|
||||||
A: 0.216,
|
A: 0.216,
|
||||||
B: -0.014,
|
B: -0.014,
|
||||||
C: -0.160,
|
C: -0.16,
|
||||||
D: -0.232,
|
D: -0.232,
|
||||||
E: -0.293,
|
E: -0.293,
|
||||||
} as const
|
} as const
|
||||||
|
|
@ -199,7 +199,7 @@ export type OdehZone = 'A' | 'B' | 'C' | 'D'
|
||||||
*/
|
*/
|
||||||
export const ODEH_THRESHOLDS = {
|
export const ODEH_THRESHOLDS = {
|
||||||
A: 5.65,
|
A: 5.65,
|
||||||
B: 2.00,
|
B: 2.0,
|
||||||
C: -0.96,
|
C: -0.96,
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
|
|
@ -390,7 +390,7 @@ export type KernelSource =
|
||||||
| { type: 'file'; path: string }
|
| { type: 'file'; path: string }
|
||||||
| { type: 'buffer'; data: ArrayBuffer; name: string }
|
| { type: 'buffer'; data: ArrayBuffer; name: string }
|
||||||
| { type: 'url'; url: string }
|
| { type: 'url'; url: string }
|
||||||
| { type: 'auto' } // auto-download from NAIF, cache in ~/.cache/moon-sighting
|
| { type: 'auto' } // auto-download from NAIF, cache in ~/.cache/moon-sighting
|
||||||
|
|
||||||
export interface KernelConfig {
|
export interface KernelConfig {
|
||||||
/** Planetary SPK kernel — defaults to de442s.bsp via auto-download */
|
/** Planetary SPK kernel — defaults to de442s.bsp via auto-download */
|
||||||
|
|
@ -434,9 +434,13 @@ export const WGS84 = {
|
||||||
/** Flattening */
|
/** Flattening */
|
||||||
f: 1 / 298.257223563,
|
f: 1 / 298.257223563,
|
||||||
/** Semi-minor axis in meters */
|
/** Semi-minor axis in meters */
|
||||||
get b() { return this.a * (1 - this.f) },
|
get b() {
|
||||||
|
return this.a * (1 - this.f)
|
||||||
|
},
|
||||||
/** First eccentricity squared */
|
/** First eccentricity squared */
|
||||||
get e2() { return 2 * this.f - this.f * this.f },
|
get e2() {
|
||||||
|
return 2 * this.f - this.f * this.f
|
||||||
|
},
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
// ─── Internal ephemeris types ─────────────────────────────────────────────────
|
// ─── Internal ephemeris types ─────────────────────────────────────────────────
|
||||||
|
|
|
||||||
|
|
@ -238,7 +238,10 @@ export function buildGuidanceText(
|
||||||
lagMinutes: number,
|
lagMinutes: number,
|
||||||
): string {
|
): string {
|
||||||
const direction = azimuthToCardinal(moonAz)
|
const direction = azimuthToCardinal(moonAz)
|
||||||
const timeStr = bestTimeUTC.toISOString().replace('T', ' ').replace(/\.\d+Z$/, ' UTC')
|
const timeStr = bestTimeUTC
|
||||||
|
.toISOString()
|
||||||
|
.replace('T', ' ')
|
||||||
|
.replace(/\.\d+Z$/, ' UTC')
|
||||||
const lagStr = `${Math.round(lagMinutes)} min after sunset`
|
const lagStr = `${Math.round(lagMinutes)} min after sunset`
|
||||||
|
|
||||||
let visibility: string
|
let visibility: string
|
||||||
|
|
@ -263,8 +266,24 @@ export function buildGuidanceText(
|
||||||
|
|
||||||
/** Convert azimuth degrees to a cardinal/intercardinal direction label */
|
/** Convert azimuth degrees to a cardinal/intercardinal direction label */
|
||||||
function azimuthToCardinal(az: number): string {
|
function azimuthToCardinal(az: number): string {
|
||||||
const dirs = ['North', 'NNE', 'NE', 'ENE', 'East', 'ESE', 'SE', 'SSE',
|
const dirs = [
|
||||||
'South', 'SSW', 'SW', 'WSW', 'West', 'WNW', 'NW', 'NNW']
|
'North',
|
||||||
|
'NNE',
|
||||||
|
'NE',
|
||||||
|
'ENE',
|
||||||
|
'East',
|
||||||
|
'ESE',
|
||||||
|
'SE',
|
||||||
|
'SSE',
|
||||||
|
'South',
|
||||||
|
'SSW',
|
||||||
|
'SW',
|
||||||
|
'WSW',
|
||||||
|
'West',
|
||||||
|
'WNW',
|
||||||
|
'NW',
|
||||||
|
'NNW',
|
||||||
|
]
|
||||||
const idx = Math.round(az / 22.5) % 16
|
const idx = Math.round(az / 22.5) % 16
|
||||||
return dirs[(idx + 16) % 16]
|
return dirs[(idx + 16) % 16]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
239
test-cjs.cjs
239
test-cjs.cjs
|
|
@ -2,10 +2,11 @@
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* moon-sighting CJS test suite
|
* moon-sighting CJS test suite
|
||||||
* Runs with: node test-cjs.cjs
|
* Runs with: node --test test-cjs.cjs
|
||||||
* Verifies the CommonJS build is functional.
|
* Verifies the CommonJS build is functional.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
const { describe, it } = require('node:test')
|
||||||
const assert = require('node:assert/strict')
|
const assert = require('node:assert/strict')
|
||||||
const {
|
const {
|
||||||
YALLOP_THRESHOLDS,
|
YALLOP_THRESHOLDS,
|
||||||
|
|
@ -25,142 +26,122 @@ const {
|
||||||
getSunMoonEvents,
|
getSunMoonEvents,
|
||||||
} = require('./dist/index.cjs')
|
} = require('./dist/index.cjs')
|
||||||
|
|
||||||
let passed = 0
|
describe('CJS compatibility', () => {
|
||||||
let failed = 0
|
it('require() works', () => {
|
||||||
|
assert.ok(YALLOP_THRESHOLDS !== undefined)
|
||||||
function test(name, fn) {
|
})
|
||||||
try {
|
it('YALLOP_THRESHOLDS.A is 0.216', () => {
|
||||||
fn()
|
assert.equal(YALLOP_THRESHOLDS.A, 0.216)
|
||||||
console.log(` [${name}]... PASS`)
|
})
|
||||||
passed++
|
it('ODEH_THRESHOLDS.A is 5.65', () => {
|
||||||
} catch (err) {
|
assert.equal(ODEH_THRESHOLDS.A, 5.65)
|
||||||
console.error(` [${name}]... FAIL: ${err.message}`)
|
})
|
||||||
failed++
|
it('WGS84.a is 6378137.0', () => {
|
||||||
}
|
assert.equal(WGS84.a, 6378137.0)
|
||||||
}
|
})
|
||||||
|
it('All API functions are exported', () => {
|
||||||
console.log('CJS compatibility:')
|
assert.equal(typeof getMoonPhase, 'function')
|
||||||
|
assert.equal(typeof getMoonPosition, 'function')
|
||||||
test('require() works', () => {
|
assert.equal(typeof getMoonIllumination, 'function')
|
||||||
assert.ok(YALLOP_THRESHOLDS !== undefined)
|
assert.equal(typeof getMoonVisibilityEstimate, 'function')
|
||||||
})
|
assert.equal(typeof getMoon, 'function')
|
||||||
test('YALLOP_THRESHOLDS.A is 0.216', () => {
|
assert.equal(typeof initKernels, 'function')
|
||||||
assert.equal(YALLOP_THRESHOLDS.A, 0.216)
|
assert.equal(typeof downloadKernels, 'function')
|
||||||
})
|
assert.equal(typeof verifyKernels, 'function')
|
||||||
test('ODEH_THRESHOLDS.A is 5.65', () => {
|
assert.equal(typeof getMoonSightingReport, 'function')
|
||||||
assert.equal(ODEH_THRESHOLDS.A, 5.65)
|
assert.equal(typeof getSunMoonEvents, 'function')
|
||||||
})
|
})
|
||||||
test('WGS84.a is 6378137.0', () => {
|
|
||||||
assert.equal(WGS84.a, 6378137.0)
|
|
||||||
})
|
|
||||||
test('All API functions are exported', () => {
|
|
||||||
assert.equal(typeof getMoonPhase, 'function')
|
|
||||||
assert.equal(typeof getMoonPosition, 'function')
|
|
||||||
assert.equal(typeof getMoonIllumination, 'function')
|
|
||||||
assert.equal(typeof getMoonVisibilityEstimate, 'function')
|
|
||||||
assert.equal(typeof getMoon, 'function')
|
|
||||||
assert.equal(typeof initKernels, 'function')
|
|
||||||
assert.equal(typeof downloadKernels, 'function')
|
|
||||||
assert.equal(typeof verifyKernels, 'function')
|
|
||||||
assert.equal(typeof getMoonSightingReport, 'function')
|
|
||||||
assert.equal(typeof getSunMoonEvents, 'function')
|
|
||||||
})
|
})
|
||||||
|
|
||||||
console.log('\nCJS getMoonPhase:')
|
describe('CJS getMoonPhase', () => {
|
||||||
|
it('returns valid phase', () => {
|
||||||
test('getMoonPhase returns valid phase', () => {
|
const valid = new Set([
|
||||||
const valid = new Set([
|
'new-moon', 'waxing-crescent', 'first-quarter', 'waxing-gibbous',
|
||||||
'new-moon', 'waxing-crescent', 'first-quarter', 'waxing-gibbous',
|
'full-moon', 'waning-gibbous', 'last-quarter', 'waning-crescent',
|
||||||
'full-moon', 'waning-gibbous', 'last-quarter', 'waning-crescent',
|
])
|
||||||
])
|
const p = getMoonPhase(new Date('2025-03-14T12:00:00Z'))
|
||||||
const p = getMoonPhase(new Date('2025-03-14T12:00:00Z'))
|
assert.ok(valid.has(p.phase), `got: ${p.phase}`)
|
||||||
assert.ok(valid.has(p.phase), `got: ${p.phase}`)
|
})
|
||||||
})
|
it('illumination in [0, 100]', () => {
|
||||||
test('getMoonPhase illumination in [0, 100]', () => {
|
const p = getMoonPhase(new Date('2025-03-01T12:00:00Z'))
|
||||||
const p = getMoonPhase(new Date('2025-03-01T12:00:00Z'))
|
assert.ok(p.illumination >= 0 && p.illumination <= 100)
|
||||||
assert.ok(p.illumination >= 0 && p.illumination <= 100)
|
})
|
||||||
})
|
it('near full moon has high illumination', () => {
|
||||||
test('getMoonPhase near full moon has high illumination', () => {
|
const p = getMoonPhase(new Date('2025-03-14T12:00:00Z'))
|
||||||
const p = getMoonPhase(new Date('2025-03-14T12:00:00Z'))
|
assert.ok(p.illumination > 85, `illumination=${p.illumination.toFixed(1)}%`)
|
||||||
assert.ok(p.illumination > 85, `illumination=${p.illumination.toFixed(1)}%`)
|
})
|
||||||
})
|
it('Dates are Date objects', () => {
|
||||||
test('getMoonPhase Dates are Date objects', () => {
|
const p = getMoonPhase(new Date('2025-03-01T12:00:00Z'))
|
||||||
const p = getMoonPhase(new Date('2025-03-01T12:00:00Z'))
|
assert.ok(p.nextNewMoon instanceof Date)
|
||||||
assert.ok(p.nextNewMoon instanceof Date)
|
assert.ok(p.prevNewMoon instanceof Date)
|
||||||
assert.ok(p.prevNewMoon instanceof Date)
|
assert.ok(p.nextFullMoon instanceof Date)
|
||||||
assert.ok(p.nextFullMoon instanceof Date)
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
console.log('\nCJS getMoonPosition + getMoonIllumination:')
|
describe('CJS getMoonPosition and getMoonIllumination', () => {
|
||||||
|
it('getMoonPosition returns valid azimuth/altitude', () => {
|
||||||
test('getMoonPosition returns valid azimuth/altitude', () => {
|
const pos = getMoonPosition(new Date('2025-03-14T20:00:00Z'), 51.5074, -0.1278, 10)
|
||||||
const pos = getMoonPosition(new Date('2025-03-14T20:00:00Z'), 51.5074, -0.1278, 10)
|
assert.ok(pos.azimuth >= 0 && pos.azimuth < 360, `azimuth=${pos.azimuth}`)
|
||||||
assert.ok(pos.azimuth >= 0 && pos.azimuth < 360, `azimuth=${pos.azimuth}`)
|
assert.ok(pos.altitude >= -90 && pos.altitude <= 90, `altitude=${pos.altitude}`)
|
||||||
assert.ok(pos.altitude >= -90 && pos.altitude <= 90, `altitude=${pos.altitude}`)
|
assert.ok(pos.distance > 356000 && pos.distance < 407000, `distance=${pos.distance}`)
|
||||||
assert.ok(pos.distance > 356000 && pos.distance < 407000, `distance=${pos.distance}`)
|
assert.ok(isFinite(pos.parallacticAngle))
|
||||||
assert.ok(isFinite(pos.parallacticAngle))
|
})
|
||||||
})
|
it('getMoonIllumination near full moon: fraction > 0.85', () => {
|
||||||
test('getMoonIllumination near full moon: fraction > 0.85', () => {
|
const illum = getMoonIllumination(new Date('2025-03-14T12:00:00Z'))
|
||||||
const illum = getMoonIllumination(new Date('2025-03-14T12:00:00Z'))
|
assert.ok(illum.fraction > 0.85, `fraction=${illum.fraction.toFixed(3)}`)
|
||||||
assert.ok(illum.fraction > 0.85, `fraction=${illum.fraction.toFixed(3)}`)
|
assert.ok(illum.phase > 0.4 && illum.phase < 0.6, `phase=${illum.phase.toFixed(3)}`)
|
||||||
assert.ok(illum.phase > 0.4 && illum.phase < 0.6, `phase=${illum.phase.toFixed(3)}`)
|
assert.ok(isFinite(illum.angle))
|
||||||
assert.ok(isFinite(illum.angle))
|
})
|
||||||
})
|
it('getMoonIllumination waxing: isWaxing = true', () => {
|
||||||
test('getMoonIllumination waxing: isWaxing = true', () => {
|
const illum = getMoonIllumination(new Date('2025-03-05T12:00:00Z'))
|
||||||
const illum = getMoonIllumination(new Date('2025-03-05T12:00:00Z'))
|
assert.equal(illum.isWaxing, true)
|
||||||
assert.equal(illum.isWaxing, true)
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
console.log('\nCJS getMoonPhase phaseName/phaseSymbol:')
|
describe('CJS getMoonPhase phaseName/phaseSymbol', () => {
|
||||||
|
it('phaseName is a non-empty string', () => {
|
||||||
test('getMoonPhase.phaseName is a non-empty string', () => {
|
const p = getMoonPhase(new Date('2025-03-05T12:00:00Z'))
|
||||||
const p = getMoonPhase(new Date('2025-03-05T12:00:00Z'))
|
assert.ok(typeof p.phaseName === 'string' && p.phaseName.length > 0)
|
||||||
assert.ok(typeof p.phaseName === 'string' && p.phaseName.length > 0)
|
})
|
||||||
})
|
it('phaseSymbol is a moon emoji', () => {
|
||||||
test('getMoonPhase.phaseSymbol is a moon emoji', () => {
|
const SYMBOLS = new Set(['🌑', '🌒', '🌓', '🌔', '🌕', '🌖', '🌗', '🌘'])
|
||||||
const SYMBOLS = new Set(['🌑', '🌒', '🌓', '🌔', '🌕', '🌖', '🌗', '🌘'])
|
const p = getMoonPhase(new Date('2025-03-05T12:00:00Z'))
|
||||||
const p = getMoonPhase(new Date('2025-03-05T12:00:00Z'))
|
assert.ok(SYMBOLS.has(p.phaseSymbol), `got: ${p.phaseSymbol}`)
|
||||||
assert.ok(SYMBOLS.has(p.phaseSymbol), `got: ${p.phaseSymbol}`)
|
})
|
||||||
})
|
it('Waxing crescent: correct phaseName and phaseSymbol', () => {
|
||||||
test('Waxing crescent: phaseName = "Waxing Crescent", phaseSymbol = "🌒"', () => {
|
const p = getMoonPhase(new Date('2025-03-05T12:00:00Z'))
|
||||||
const p = getMoonPhase(new Date('2025-03-05T12:00:00Z'))
|
assert.equal(p.phaseName, 'Waxing Crescent')
|
||||||
assert.equal(p.phaseName, 'Waxing Crescent')
|
assert.equal(p.phaseSymbol, '🌒')
|
||||||
assert.equal(p.phaseSymbol, '🌒')
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
console.log('\nCJS getMoonVisibilityEstimate:')
|
describe('CJS getMoonVisibilityEstimate', () => {
|
||||||
|
it('returns valid zone', () => {
|
||||||
test('getMoonVisibilityEstimate returns valid zone', () => {
|
const v = getMoonVisibilityEstimate(new Date('2025-03-02T18:30:00Z'), 51.5074, -0.1278, 10)
|
||||||
const v = getMoonVisibilityEstimate(new Date('2025-03-02T18:30:00Z'), 51.5074, -0.1278, 10)
|
assert.ok(['A', 'B', 'C', 'D'].includes(v.zone), `zone=${v.zone}`)
|
||||||
assert.ok(['A', 'B', 'C', 'D'].includes(v.zone), `zone=${v.zone}`)
|
assert.ok(isFinite(v.V))
|
||||||
assert.ok(isFinite(v.V))
|
assert.equal(v.isApproximate, true)
|
||||||
assert.equal(v.isApproximate, true)
|
})
|
||||||
})
|
it('near new moon: zone C or D', () => {
|
||||||
test('getMoonVisibilityEstimate near new moon: zone C or D', () => {
|
const v = getMoonVisibilityEstimate(new Date('2025-03-29T18:00:00Z'), 21.4225, 39.8262)
|
||||||
const v = getMoonVisibilityEstimate(new Date('2025-03-29T18:00:00Z'), 21.4225, 39.8262)
|
assert.ok(['C', 'D'].includes(v.zone), `zone=${v.zone}`)
|
||||||
assert.ok(['C', 'D'].includes(v.zone), `zone=${v.zone}`)
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
console.log('\nCJS getMoon:')
|
describe('CJS getMoon', () => {
|
||||||
|
it('returns all four sub-results', () => {
|
||||||
test('getMoon returns all four sub-results', () => {
|
const m = getMoon(new Date('2025-03-05T20:00:00Z'), 51.5074, -0.1278, 10)
|
||||||
const m = getMoon(new Date('2025-03-05T20:00:00Z'), 51.5074, -0.1278, 10)
|
assert.ok(typeof m.phase === 'object')
|
||||||
assert.ok(typeof m.phase === 'object')
|
assert.ok(typeof m.position === 'object')
|
||||||
assert.ok(typeof m.position === 'object')
|
assert.ok(typeof m.illumination === 'object')
|
||||||
assert.ok(typeof m.illumination === 'object')
|
assert.ok(typeof m.visibility === 'object')
|
||||||
assert.ok(typeof m.visibility === 'object')
|
})
|
||||||
|
it('phase.phaseName is non-empty', () => {
|
||||||
|
const m = getMoon(new Date('2025-03-05T20:00:00Z'), 51.5074, -0.1278)
|
||||||
|
assert.ok(typeof m.phase.phaseName === 'string' && m.phase.phaseName.length > 0)
|
||||||
|
})
|
||||||
|
it('visibility.isApproximate is true', () => {
|
||||||
|
const m = getMoon(new Date(), 51.5074, -0.1278)
|
||||||
|
assert.equal(m.visibility.isApproximate, true)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
test('getMoon.phase.phaseName is non-empty', () => {
|
|
||||||
const m = getMoon(new Date('2025-03-05T20:00:00Z'), 51.5074, -0.1278)
|
|
||||||
assert.ok(typeof m.phase.phaseName === 'string' && m.phase.phaseName.length > 0)
|
|
||||||
})
|
|
||||||
test('getMoon.visibility.isApproximate is true', () => {
|
|
||||||
const m = getMoon(new Date(), 51.5074, -0.1278)
|
|
||||||
assert.equal(m.visibility.isApproximate, true)
|
|
||||||
})
|
|
||||||
|
|
||||||
console.log(`\n${passed + failed} tests: ${passed} passed, ${failed} failed`)
|
|
||||||
|
|
||||||
if (failed > 0) {
|
|
||||||
process.exit(1)
|
|
||||||
}
|
|
||||||
|
|
|
||||||
713
test.mjs
713
test.mjs
|
|
@ -1,9 +1,9 @@
|
||||||
/**
|
/**
|
||||||
* moon-sighting ESM test suite
|
* moon-sighting ESM test suite
|
||||||
* Runs with: node test.mjs
|
* Runs with: node --test test.mjs
|
||||||
* All tests use plain assert — no test framework.
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { describe, it } from 'node:test'
|
||||||
import assert from 'node:assert/strict'
|
import assert from 'node:assert/strict'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
|
@ -26,429 +26,392 @@ import {
|
||||||
getSunMoonEvents,
|
getSunMoonEvents,
|
||||||
} from './dist/index.mjs'
|
} from './dist/index.mjs'
|
||||||
|
|
||||||
let passed = 0
|
|
||||||
let failed = 0
|
|
||||||
|
|
||||||
function test(name, fn) {
|
|
||||||
try {
|
|
||||||
fn()
|
|
||||||
console.log(` [${name}]... PASS`)
|
|
||||||
passed++
|
|
||||||
} catch (err) {
|
|
||||||
console.error(` [${name}]... FAIL: ${err.message}`)
|
|
||||||
failed++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ─── Constants ────────────────────────────────────────────────────────────────
|
// ─── Constants ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
console.log('Constants:')
|
describe('Constants', () => {
|
||||||
|
it('YALLOP_THRESHOLDS.A is 0.216', () => {
|
||||||
test('YALLOP_THRESHOLDS.A is 0.216', () => {
|
assert.equal(YALLOP_THRESHOLDS.A, 0.216)
|
||||||
assert.equal(YALLOP_THRESHOLDS.A, 0.216)
|
})
|
||||||
})
|
it('YALLOP_THRESHOLDS.E is -0.293', () => {
|
||||||
test('YALLOP_THRESHOLDS.E is -0.293', () => {
|
assert.equal(YALLOP_THRESHOLDS.E, -0.293)
|
||||||
assert.equal(YALLOP_THRESHOLDS.E, -0.293)
|
})
|
||||||
})
|
it('All Yallop thresholds are defined', () => {
|
||||||
test('All Yallop thresholds are defined', () => {
|
for (const key of ['A', 'B', 'C', 'D', 'E']) {
|
||||||
for (const key of ['A', 'B', 'C', 'D', 'E']) {
|
assert.ok(typeof YALLOP_THRESHOLDS[key] === 'number', `${key} should be a number`)
|
||||||
assert.ok(typeof YALLOP_THRESHOLDS[key] === 'number', `${key} should be a number`)
|
}
|
||||||
}
|
})
|
||||||
})
|
it('Yallop thresholds descend A > B > C > D > E', () => {
|
||||||
test('Yallop thresholds descend A > B > C > D > E', () => {
|
assert.ok(YALLOP_THRESHOLDS.A > YALLOP_THRESHOLDS.B)
|
||||||
assert.ok(YALLOP_THRESHOLDS.A > YALLOP_THRESHOLDS.B)
|
assert.ok(YALLOP_THRESHOLDS.B > YALLOP_THRESHOLDS.C)
|
||||||
assert.ok(YALLOP_THRESHOLDS.B > YALLOP_THRESHOLDS.C)
|
assert.ok(YALLOP_THRESHOLDS.C > YALLOP_THRESHOLDS.D)
|
||||||
assert.ok(YALLOP_THRESHOLDS.C > YALLOP_THRESHOLDS.D)
|
assert.ok(YALLOP_THRESHOLDS.D > YALLOP_THRESHOLDS.E)
|
||||||
assert.ok(YALLOP_THRESHOLDS.D > YALLOP_THRESHOLDS.E)
|
})
|
||||||
})
|
it('ODEH_THRESHOLDS.A is 5.65', () => {
|
||||||
test('ODEH_THRESHOLDS.A is 5.65', () => {
|
assert.equal(ODEH_THRESHOLDS.A, 5.65)
|
||||||
assert.equal(ODEH_THRESHOLDS.A, 5.65)
|
})
|
||||||
})
|
it('ODEH_THRESHOLDS.C is -0.96', () => {
|
||||||
test('ODEH_THRESHOLDS.C is -0.96', () => {
|
assert.equal(ODEH_THRESHOLDS.C, -0.96)
|
||||||
assert.equal(ODEH_THRESHOLDS.C, -0.96)
|
})
|
||||||
})
|
it('Odeh thresholds descend A > B > C', () => {
|
||||||
test('Odeh thresholds descend A > B > C', () => {
|
assert.ok(ODEH_THRESHOLDS.A > ODEH_THRESHOLDS.B)
|
||||||
assert.ok(ODEH_THRESHOLDS.A > ODEH_THRESHOLDS.B)
|
assert.ok(ODEH_THRESHOLDS.B > ODEH_THRESHOLDS.C)
|
||||||
assert.ok(ODEH_THRESHOLDS.B > ODEH_THRESHOLDS.C)
|
})
|
||||||
})
|
it('WGS84.a is 6378137.0', () => {
|
||||||
test('WGS84.a is 6378137.0', () => {
|
assert.equal(WGS84.a, 6378137.0)
|
||||||
assert.equal(WGS84.a, 6378137.0)
|
})
|
||||||
})
|
it('WGS84.invF is 298.257223563', () => {
|
||||||
test('WGS84.invF is 298.257223563', () => {
|
assert.equal(WGS84.invF, 298.257223563)
|
||||||
assert.equal(WGS84.invF, 298.257223563)
|
})
|
||||||
})
|
it('WGS84.e2 is positive and < 1', () => {
|
||||||
test('WGS84.e2 is positive and < 1', () => {
|
assert.ok(WGS84.e2 > 0 && WGS84.e2 < 1, `e2=${WGS84.e2}`)
|
||||||
assert.ok(WGS84.e2 > 0 && WGS84.e2 < 1, `e2=${WGS84.e2}`)
|
})
|
||||||
})
|
it('WGS84.b < WGS84.a (oblate spheroid)', () => {
|
||||||
test('WGS84.b < WGS84.a (oblate spheroid)', () => {
|
assert.ok(WGS84.b < WGS84.a)
|
||||||
assert.ok(WGS84.b < WGS84.a)
|
})
|
||||||
})
|
it('Yallop descriptions are non-empty strings', () => {
|
||||||
test('Yallop descriptions are non-empty strings', () => {
|
for (const cat of ['A', 'B', 'C', 'D', 'E', 'F']) {
|
||||||
for (const cat of ['A', 'B', 'C', 'D', 'E', 'F']) {
|
assert.ok(typeof YALLOP_DESCRIPTIONS[cat] === 'string' && YALLOP_DESCRIPTIONS[cat].length > 0)
|
||||||
assert.ok(typeof YALLOP_DESCRIPTIONS[cat] === 'string' && YALLOP_DESCRIPTIONS[cat].length > 0)
|
}
|
||||||
}
|
})
|
||||||
})
|
it('Odeh descriptions are non-empty strings', () => {
|
||||||
test('Odeh descriptions are non-empty strings', () => {
|
for (const zone of ['A', 'B', 'C', 'D']) {
|
||||||
for (const zone of ['A', 'B', 'C', 'D']) {
|
assert.ok(typeof ODEH_DESCRIPTIONS[zone] === 'string' && ODEH_DESCRIPTIONS[zone].length > 0)
|
||||||
assert.ok(typeof ODEH_DESCRIPTIONS[zone] === 'string' && ODEH_DESCRIPTIONS[zone].length > 0)
|
}
|
||||||
}
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
// ─── API function exports ──────────────────────────────────────────────────────
|
// ─── API function exports ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
console.log('\nAPI exports:')
|
describe('API exports', () => {
|
||||||
|
it('getMoonPhase is a function', () => {
|
||||||
test('getMoonPhase is a function', () => {
|
assert.equal(typeof getMoonPhase, 'function')
|
||||||
assert.equal(typeof getMoonPhase, 'function')
|
})
|
||||||
})
|
it('initKernels is a function', () => {
|
||||||
test('initKernels is a function', () => {
|
assert.equal(typeof initKernels, 'function')
|
||||||
assert.equal(typeof initKernels, 'function')
|
})
|
||||||
})
|
it('downloadKernels is a function', () => {
|
||||||
test('downloadKernels is a function', () => {
|
assert.equal(typeof downloadKernels, 'function')
|
||||||
assert.equal(typeof downloadKernels, 'function')
|
})
|
||||||
})
|
it('verifyKernels is a function', () => {
|
||||||
test('verifyKernels is a function', () => {
|
assert.equal(typeof verifyKernels, 'function')
|
||||||
assert.equal(typeof verifyKernels, 'function')
|
})
|
||||||
})
|
it('getMoonSightingReport is a function', () => {
|
||||||
test('getMoonSightingReport is a function', () => {
|
assert.equal(typeof getMoonSightingReport, 'function')
|
||||||
assert.equal(typeof getMoonSightingReport, 'function')
|
})
|
||||||
})
|
it('getSunMoonEvents is a function', () => {
|
||||||
test('getSunMoonEvents is a function', () => {
|
assert.equal(typeof getSunMoonEvents, 'function')
|
||||||
assert.equal(typeof getSunMoonEvents, 'function')
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
// ─── getMoonPhase (synchronous, no kernel) ─────────────────────────────────────
|
// ─── getMoonPhase (synchronous, no kernel) ─────────────────────────────────────
|
||||||
|
|
||||||
console.log('\ngetMoonPhase — structure:')
|
|
||||||
|
|
||||||
const VALID_PHASES = new Set([
|
const VALID_PHASES = new Set([
|
||||||
'new-moon', 'waxing-crescent', 'first-quarter', 'waxing-gibbous',
|
'new-moon', 'waxing-crescent', 'first-quarter', 'waxing-gibbous',
|
||||||
'full-moon', 'waning-gibbous', 'last-quarter', 'waning-crescent',
|
'full-moon', 'waning-gibbous', 'last-quarter', 'waning-crescent',
|
||||||
])
|
])
|
||||||
|
|
||||||
// Test with a known reference date: 2025-03-01 UTC
|
|
||||||
// At this date the Moon was a waxing crescent (~2 days after new moon Feb 28)
|
|
||||||
const DATE_MARCH_1_2025 = new Date('2025-03-01T12:00:00Z')
|
const DATE_MARCH_1_2025 = new Date('2025-03-01T12:00:00Z')
|
||||||
const phase_march1 = getMoonPhase(DATE_MARCH_1_2025)
|
const phase_march1 = getMoonPhase(DATE_MARCH_1_2025)
|
||||||
|
|
||||||
test('getMoonPhase returns an object', () => {
|
describe('getMoonPhase structure', () => {
|
||||||
assert.ok(phase_march1 !== null && typeof phase_march1 === 'object')
|
it('returns an object', () => {
|
||||||
})
|
assert.ok(phase_march1 !== null && typeof phase_march1 === 'object')
|
||||||
test('getMoonPhase.phase is a valid phase name', () => {
|
})
|
||||||
assert.ok(VALID_PHASES.has(phase_march1.phase), `got: ${phase_march1.phase}`)
|
it('phase is a valid phase name', () => {
|
||||||
})
|
assert.ok(VALID_PHASES.has(phase_march1.phase), `got: ${phase_march1.phase}`)
|
||||||
test('getMoonPhase.illumination is in [0, 100]', () => {
|
})
|
||||||
assert.ok(phase_march1.illumination >= 0 && phase_march1.illumination <= 100,
|
it('illumination is in [0, 100]', () => {
|
||||||
`illumination=${phase_march1.illumination}`)
|
assert.ok(phase_march1.illumination >= 0 && phase_march1.illumination <= 100,
|
||||||
})
|
`illumination=${phase_march1.illumination}`)
|
||||||
test('getMoonPhase.age is >= 0', () => {
|
})
|
||||||
assert.ok(phase_march1.age >= 0, `age=${phase_march1.age}`)
|
it('age is >= 0', () => {
|
||||||
})
|
assert.ok(phase_march1.age >= 0, `age=${phase_march1.age}`)
|
||||||
test('getMoonPhase.elongationDeg is in [0, 180]', () => {
|
})
|
||||||
assert.ok(phase_march1.elongationDeg >= 0 && phase_march1.elongationDeg <= 180,
|
it('elongationDeg is in [0, 180]', () => {
|
||||||
`elongationDeg=${phase_march1.elongationDeg}`)
|
assert.ok(phase_march1.elongationDeg >= 0 && phase_march1.elongationDeg <= 180,
|
||||||
})
|
`elongationDeg=${phase_march1.elongationDeg}`)
|
||||||
test('getMoonPhase.isWaxing is a boolean', () => {
|
})
|
||||||
assert.equal(typeof phase_march1.isWaxing, 'boolean')
|
it('isWaxing is a boolean', () => {
|
||||||
})
|
assert.equal(typeof phase_march1.isWaxing, 'boolean')
|
||||||
test('getMoonPhase.nextNewMoon is a Date', () => {
|
})
|
||||||
assert.ok(phase_march1.nextNewMoon instanceof Date)
|
it('nextNewMoon is a Date', () => {
|
||||||
})
|
assert.ok(phase_march1.nextNewMoon instanceof Date)
|
||||||
test('getMoonPhase.prevNewMoon is a Date', () => {
|
})
|
||||||
assert.ok(phase_march1.prevNewMoon instanceof Date)
|
it('prevNewMoon is a Date', () => {
|
||||||
})
|
assert.ok(phase_march1.prevNewMoon instanceof Date)
|
||||||
test('getMoonPhase.nextFullMoon is a Date', () => {
|
})
|
||||||
assert.ok(phase_march1.nextFullMoon instanceof Date)
|
it('nextFullMoon is a Date', () => {
|
||||||
})
|
assert.ok(phase_march1.nextFullMoon instanceof Date)
|
||||||
test('getMoonPhase.prevNewMoon is before reference date', () => {
|
})
|
||||||
assert.ok(phase_march1.prevNewMoon < DATE_MARCH_1_2025,
|
it('prevNewMoon is before reference date', () => {
|
||||||
`prevNewMoon=${phase_march1.prevNewMoon.toISOString()}`)
|
assert.ok(phase_march1.prevNewMoon < DATE_MARCH_1_2025,
|
||||||
})
|
`prevNewMoon=${phase_march1.prevNewMoon.toISOString()}`)
|
||||||
test('getMoonPhase.nextNewMoon is after prevNewMoon', () => {
|
})
|
||||||
assert.ok(phase_march1.nextNewMoon > phase_march1.prevNewMoon)
|
it('nextNewMoon is after prevNewMoon', () => {
|
||||||
|
assert.ok(phase_march1.nextNewMoon > phase_march1.prevNewMoon)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
console.log('\ngetMoonPhase — phase boundaries:')
|
describe('getMoonPhase phase boundaries', () => {
|
||||||
|
const DATE_FULL_MOON = new Date('2025-03-14T12:00:00Z')
|
||||||
|
const phase_full = getMoonPhase(DATE_FULL_MOON)
|
||||||
|
|
||||||
// 2025-03-14 was close to full moon (illumination should be high)
|
it('near full moon: illumination > 85%', () => {
|
||||||
const DATE_FULL_MOON = new Date('2025-03-14T12:00:00Z')
|
assert.ok(phase_full.illumination > 85,
|
||||||
const phase_full = getMoonPhase(DATE_FULL_MOON)
|
`illumination at full moon=${phase_full.illumination.toFixed(1)}%`)
|
||||||
|
})
|
||||||
|
it('near full moon: phase is full-moon or waxing/waning gibbous', () => {
|
||||||
|
const valid = new Set(['full-moon', 'waxing-gibbous', 'waning-gibbous'])
|
||||||
|
assert.ok(valid.has(phase_full.phase), `got: ${phase_full.phase}`)
|
||||||
|
})
|
||||||
|
it('near full moon: elongation > 120 deg', () => {
|
||||||
|
assert.ok(phase_full.elongationDeg > 120, `elongation=${phase_full.elongationDeg}`)
|
||||||
|
})
|
||||||
|
|
||||||
test('Near full moon: illumination > 85%', () => {
|
const DATE_NEW_MOON = new Date('2025-03-29T12:00:00Z')
|
||||||
assert.ok(phase_full.illumination > 85,
|
const phase_new = getMoonPhase(DATE_NEW_MOON)
|
||||||
`illumination at full moon=${phase_full.illumination.toFixed(1)}%`)
|
|
||||||
})
|
it('near new moon: illumination < 10%', () => {
|
||||||
test('Near full moon: phase is full-moon or waxing/waning gibbous', () => {
|
assert.ok(phase_new.illumination < 10,
|
||||||
const valid = new Set(['full-moon', 'waxing-gibbous', 'waning-gibbous'])
|
`illumination at new moon=${phase_new.illumination.toFixed(1)}%`)
|
||||||
assert.ok(valid.has(phase_full.phase), `got: ${phase_full.phase}`)
|
})
|
||||||
})
|
it('near new moon: elongation < 30 deg', () => {
|
||||||
test('Near full moon: elongation > 120°', () => {
|
assert.ok(phase_new.elongationDeg < 30, `elongation=${phase_new.elongationDeg}`)
|
||||||
assert.ok(phase_full.elongationDeg > 120, `elongation=${phase_full.elongationDeg}`)
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
// 2025-03-29 is close to new moon (illumination should be low)
|
describe('getMoonPhase consistency', () => {
|
||||||
const DATE_NEW_MOON = new Date('2025-03-29T12:00:00Z')
|
const DATE_WAXING = new Date('2025-03-05T12:00:00Z')
|
||||||
const phase_new = getMoonPhase(DATE_NEW_MOON)
|
const DATE_WANING = new Date('2025-03-20T12:00:00Z')
|
||||||
|
|
||||||
test('Near new moon: illumination < 10%', () => {
|
it('5 days after new moon: isWaxing = true', () => {
|
||||||
assert.ok(phase_new.illumination < 10,
|
assert.equal(getMoonPhase(DATE_WAXING).isWaxing, true)
|
||||||
`illumination at new moon=${phase_new.illumination.toFixed(1)}%`)
|
})
|
||||||
})
|
it('6 days after full moon: isWaxing = false', () => {
|
||||||
test('Near new moon: elongation < 30°', () => {
|
assert.equal(getMoonPhase(DATE_WANING).isWaxing, false)
|
||||||
assert.ok(phase_new.elongationDeg < 30, `elongation=${phase_new.elongationDeg}`)
|
})
|
||||||
})
|
it('default date (now) returns valid result', () => {
|
||||||
|
const nowPhase = getMoonPhase()
|
||||||
console.log('\ngetMoonPhase — consistency:')
|
assert.ok(VALID_PHASES.has(nowPhase.phase))
|
||||||
|
assert.ok(nowPhase.illumination >= 0 && nowPhase.illumination <= 100)
|
||||||
// Two dates: one clearly waxing, one clearly waning
|
})
|
||||||
const DATE_WAXING = new Date('2025-03-05T12:00:00Z') // ~7 days after new moon
|
it('synodic month duration is ~29.5 days', () => {
|
||||||
const DATE_WANING = new Date('2025-03-20T12:00:00Z') // ~6 days after full moon
|
const synodicMs = phase_march1.nextNewMoon.getTime() - phase_march1.prevNewMoon.getTime()
|
||||||
const phase_waxing = getMoonPhase(DATE_WAXING)
|
const synodicDays = synodicMs / 86400000
|
||||||
const phase_waning = getMoonPhase(DATE_WANING)
|
assert.ok(
|
||||||
|
synodicDays > 29.0 && synodicDays < 30.1,
|
||||||
test('5 days after new moon: isWaxing = true', () => {
|
`synodic month=${synodicDays.toFixed(2)} days`,
|
||||||
assert.equal(phase_waxing.isWaxing, true)
|
)
|
||||||
})
|
})
|
||||||
test('6 days after full moon: isWaxing = false', () => {
|
|
||||||
assert.equal(phase_waning.isWaxing, false)
|
|
||||||
})
|
|
||||||
test('getMoonPhase with default date (now) returns valid result', () => {
|
|
||||||
const nowPhase = getMoonPhase()
|
|
||||||
assert.ok(VALID_PHASES.has(nowPhase.phase))
|
|
||||||
assert.ok(nowPhase.illumination >= 0 && nowPhase.illumination <= 100)
|
|
||||||
})
|
|
||||||
|
|
||||||
// Synodic month duration check: nextNewMoon - prevNewMoon ≈ 29.53 days
|
|
||||||
test('Synodic month duration is ~29.5 days (±0.5)', () => {
|
|
||||||
const synodicMs = phase_march1.nextNewMoon.getTime() - phase_march1.prevNewMoon.getTime()
|
|
||||||
const synodicDays = synodicMs / 86400000
|
|
||||||
assert.ok(
|
|
||||||
synodicDays > 29.0 && synodicDays < 30.1,
|
|
||||||
`synodic month=${synodicDays.toFixed(2)} days`,
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// ─── getMoonPosition ─────────────────────────────────────────────────────────
|
// ─── getMoonPosition ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
console.log('\ngetMoonPosition:')
|
describe('getMoonPosition', () => {
|
||||||
|
const moonPos_london = getMoonPosition(new Date('2025-03-14T20:00:00Z'), 51.5074, -0.1278, 10)
|
||||||
|
|
||||||
// London on 2025-03-14 at noon UTC — Moon should be above the horizon during daytime
|
it('azimuth in [0, 360)', () => {
|
||||||
const moonPos_london = getMoonPosition(new Date('2025-03-14T20:00:00Z'), 51.5074, -0.1278, 10)
|
assert.ok(moonPos_london.azimuth >= 0 && moonPos_london.azimuth < 360,
|
||||||
|
`azimuth=${moonPos_london.azimuth}`)
|
||||||
test('getMoonPosition returns azimuth in [0, 360)', () => {
|
})
|
||||||
assert.ok(
|
it('altitude in [-90, 90]', () => {
|
||||||
moonPos_london.azimuth >= 0 && moonPos_london.azimuth < 360,
|
assert.ok(moonPos_london.altitude >= -90 && moonPos_london.altitude <= 90,
|
||||||
`azimuth=${moonPos_london.azimuth}`,
|
`altitude=${moonPos_london.altitude}`)
|
||||||
)
|
})
|
||||||
})
|
it('distance in lunar orbit range [356000, 407000] km', () => {
|
||||||
test('getMoonPosition returns altitude in [-90, 90]', () => {
|
assert.ok(moonPos_london.distance >= 356000 && moonPos_london.distance <= 407000,
|
||||||
assert.ok(
|
`distance=${moonPos_london.distance.toFixed(0)} km`)
|
||||||
moonPos_london.altitude >= -90 && moonPos_london.altitude <= 90,
|
})
|
||||||
`altitude=${moonPos_london.altitude}`,
|
it('finite parallacticAngle', () => {
|
||||||
)
|
assert.ok(isFinite(moonPos_london.parallacticAngle),
|
||||||
})
|
`parallacticAngle=${moonPos_london.parallacticAngle}`)
|
||||||
test('getMoonPosition returns distance in lunar orbit range [356000, 407000] km', () => {
|
})
|
||||||
assert.ok(
|
it('default date (now) returns valid result', () => {
|
||||||
moonPos_london.distance >= 356000 && moonPos_london.distance <= 407000,
|
const pos = getMoonPosition(new Date(), 21.4225, 39.8262)
|
||||||
`distance=${moonPos_london.distance.toFixed(0)} km`,
|
assert.ok(pos.azimuth >= 0 && pos.azimuth < 360)
|
||||||
)
|
assert.ok(pos.altitude >= -90 && pos.altitude <= 90)
|
||||||
})
|
assert.ok(pos.distance > 350000 && pos.distance < 410000)
|
||||||
test('getMoonPosition returns finite parallacticAngle', () => {
|
})
|
||||||
assert.ok(
|
|
||||||
isFinite(moonPos_london.parallacticAngle),
|
|
||||||
`parallacticAngle=${moonPos_london.parallacticAngle}`,
|
|
||||||
)
|
|
||||||
})
|
|
||||||
test('getMoonPosition default date (now) returns valid result', () => {
|
|
||||||
const pos = getMoonPosition(new Date(), 21.4225, 39.8262) // Mecca
|
|
||||||
assert.ok(pos.azimuth >= 0 && pos.azimuth < 360)
|
|
||||||
assert.ok(pos.altitude >= -90 && pos.altitude <= 90)
|
|
||||||
assert.ok(pos.distance > 350000 && pos.distance < 410000)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// ─── getMoonIllumination ─────────────────────────────────────────────────────
|
// ─── getMoonIllumination ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
console.log('\ngetMoonIllumination:')
|
describe('getMoonIllumination', () => {
|
||||||
|
const illum_full = getMoonIllumination(new Date('2025-03-14T12:00:00Z'))
|
||||||
|
const illum_new = getMoonIllumination(new Date('2025-03-29T12:00:00Z'))
|
||||||
|
const illum_waxing = getMoonIllumination(new Date('2025-03-05T12:00:00Z'))
|
||||||
|
|
||||||
// 2025-03-14 was close to full moon
|
it('near full moon: fraction > 0.85', () => {
|
||||||
const illum_full = getMoonIllumination(new Date('2025-03-14T12:00:00Z'))
|
assert.ok(illum_full.fraction > 0.85, `fraction=${illum_full.fraction.toFixed(3)}`)
|
||||||
// 2025-03-29 was close to new moon
|
})
|
||||||
const illum_new = getMoonIllumination(new Date('2025-03-29T12:00:00Z'))
|
it('near full moon: phase close to 0.5', () => {
|
||||||
// 2025-03-05 was waxing crescent (~7 days after new moon)
|
assert.ok(illum_full.phase > 0.4 && illum_full.phase < 0.6,
|
||||||
const illum_waxing = getMoonIllumination(new Date('2025-03-05T12:00:00Z'))
|
`phase=${illum_full.phase.toFixed(3)}`)
|
||||||
|
})
|
||||||
test('getMoonIllumination near full moon: fraction > 0.85', () => {
|
it('near new moon: fraction < 0.05', () => {
|
||||||
assert.ok(illum_full.fraction > 0.85, `fraction=${illum_full.fraction.toFixed(3)}`)
|
assert.ok(illum_new.fraction < 0.05, `fraction=${illum_new.fraction.toFixed(3)}`)
|
||||||
})
|
})
|
||||||
test('getMoonIllumination near full moon: phase close to 0.5', () => {
|
it('near new moon: phase close to 0 or 1', () => {
|
||||||
assert.ok(
|
const p = illum_new.phase
|
||||||
illum_full.phase > 0.4 && illum_full.phase < 0.6,
|
assert.ok(p < 0.08 || p > 0.92, `phase=${p.toFixed(3)}`)
|
||||||
`phase=${illum_full.phase.toFixed(3)}`,
|
})
|
||||||
)
|
it('waxing: isWaxing = true', () => {
|
||||||
})
|
assert.equal(illum_waxing.isWaxing, true)
|
||||||
test('getMoonIllumination near new moon: fraction < 0.05', () => {
|
})
|
||||||
assert.ok(illum_new.fraction < 0.05, `fraction=${illum_new.fraction.toFixed(3)}`)
|
it('fraction in [0, 1]', () => {
|
||||||
})
|
assert.ok(illum_full.fraction >= 0 && illum_full.fraction <= 1)
|
||||||
test('getMoonIllumination near new moon: phase close to 0 or 1', () => {
|
assert.ok(illum_new.fraction >= 0 && illum_new.fraction <= 1)
|
||||||
const p = illum_new.phase
|
})
|
||||||
assert.ok(p < 0.08 || p > 0.92, `phase=${p.toFixed(3)}`)
|
it('phase in [0, 1)', () => {
|
||||||
})
|
assert.ok(illum_full.phase >= 0 && illum_full.phase < 1)
|
||||||
test('getMoonIllumination waxing: isWaxing = true', () => {
|
assert.ok(illum_new.phase >= 0 && illum_new.phase < 1)
|
||||||
assert.equal(illum_waxing.isWaxing, true)
|
})
|
||||||
})
|
it('angle is finite', () => {
|
||||||
test('getMoonIllumination fraction in [0, 1]', () => {
|
assert.ok(isFinite(illum_full.angle), `angle=${illum_full.angle}`)
|
||||||
assert.ok(illum_full.fraction >= 0 && illum_full.fraction <= 1)
|
})
|
||||||
assert.ok(illum_new.fraction >= 0 && illum_new.fraction <= 1)
|
it('default date (now) returns valid result', () => {
|
||||||
})
|
const illum = getMoonIllumination()
|
||||||
test('getMoonIllumination phase in [0, 1)', () => {
|
assert.ok(illum.fraction >= 0 && illum.fraction <= 1)
|
||||||
assert.ok(illum_full.phase >= 0 && illum_full.phase < 1)
|
assert.ok(illum.phase >= 0 && illum.phase < 1)
|
||||||
assert.ok(illum_new.phase >= 0 && illum_new.phase < 1)
|
assert.equal(typeof illum.isWaxing, 'boolean')
|
||||||
})
|
assert.ok(isFinite(illum.angle))
|
||||||
test('getMoonIllumination angle is finite', () => {
|
})
|
||||||
assert.ok(isFinite(illum_full.angle), `angle=${illum_full.angle}`)
|
|
||||||
})
|
|
||||||
test('getMoonIllumination default date (now) returns valid result', () => {
|
|
||||||
const illum = getMoonIllumination()
|
|
||||||
assert.ok(illum.fraction >= 0 && illum.fraction <= 1)
|
|
||||||
assert.ok(illum.phase >= 0 && illum.phase < 1)
|
|
||||||
assert.equal(typeof illum.isWaxing, 'boolean')
|
|
||||||
assert.ok(isFinite(illum.angle))
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// ─── getMoonPhase phaseName + phaseSymbol ─────────────────────────────────────
|
// ─── getMoonPhase phaseName + phaseSymbol ─────────────────────────────────────
|
||||||
|
|
||||||
console.log('\ngetMoonPhase — phaseName + phaseSymbol:')
|
describe('getMoonPhase phaseName and phaseSymbol', () => {
|
||||||
|
const PHASE_NAMES = new Set([
|
||||||
|
'New Moon', 'Waxing Crescent', 'First Quarter', 'Waxing Gibbous',
|
||||||
|
'Full Moon', 'Waning Gibbous', 'Last Quarter', 'Waning Crescent',
|
||||||
|
])
|
||||||
|
const PHASE_SYMBOLS = new Set(['🌑', '🌒', '🌓', '🌔', '🌕', '🌖', '🌗', '🌘'])
|
||||||
|
|
||||||
const PHASE_NAMES = new Set([
|
it('phaseName is a valid human-readable name', () => {
|
||||||
'New Moon', 'Waxing Crescent', 'First Quarter', 'Waxing Gibbous',
|
const p = getMoonPhase(DATE_MARCH_1_2025)
|
||||||
'Full Moon', 'Waning Gibbous', 'Last Quarter', 'Waning Crescent',
|
assert.ok(PHASE_NAMES.has(p.phaseName), `got: ${p.phaseName}`)
|
||||||
])
|
})
|
||||||
const PHASE_SYMBOLS = new Set(['🌑', '🌒', '🌓', '🌔', '🌕', '🌖', '🌗', '🌘'])
|
it('phaseSymbol is a moon emoji', () => {
|
||||||
|
const p = getMoonPhase(DATE_MARCH_1_2025)
|
||||||
test('getMoonPhase.phaseName is a valid human-readable name', () => {
|
assert.ok(PHASE_SYMBOLS.has(p.phaseSymbol), `got: ${p.phaseSymbol}`)
|
||||||
const p = getMoonPhase(DATE_MARCH_1_2025)
|
})
|
||||||
assert.ok(PHASE_NAMES.has(p.phaseName), `got: ${p.phaseName}`)
|
it('near full moon: phaseName is Full Moon or gibbous', () => {
|
||||||
})
|
const valid = new Set(['Full Moon', 'Waxing Gibbous', 'Waning Gibbous'])
|
||||||
test('getMoonPhase.phaseSymbol is a moon emoji', () => {
|
const p = getMoonPhase(new Date('2025-03-14T12:00:00Z'))
|
||||||
const p = getMoonPhase(DATE_MARCH_1_2025)
|
assert.ok(valid.has(p.phaseName), `got: ${p.phaseName}`)
|
||||||
assert.ok(PHASE_SYMBOLS.has(p.phaseSymbol), `got: ${p.phaseSymbol}`)
|
})
|
||||||
})
|
it('waxing crescent: phaseName is Waxing Crescent', () => {
|
||||||
test('Near full moon: phaseName is "Full Moon" or gibbous', () => {
|
const p = getMoonPhase(new Date('2025-03-05T12:00:00Z'))
|
||||||
const valid = new Set(['Full Moon', 'Waxing Gibbous', 'Waning Gibbous'])
|
assert.equal(p.phaseName, 'Waxing Crescent')
|
||||||
const p = getMoonPhase(DATE_FULL_MOON)
|
})
|
||||||
assert.ok(valid.has(p.phaseName), `got: ${p.phaseName}`)
|
it('waxing crescent: phaseSymbol is correct', () => {
|
||||||
})
|
const p = getMoonPhase(new Date('2025-03-05T12:00:00Z'))
|
||||||
test('Near full moon: phaseSymbol is 🌕 or 🌔 or 🌖', () => {
|
assert.equal(p.phaseSymbol, '🌒')
|
||||||
const valid = new Set(['🌕', '🌔', '🌖'])
|
})
|
||||||
const p = getMoonPhase(DATE_FULL_MOON)
|
|
||||||
assert.ok(valid.has(p.phaseSymbol), `got: ${p.phaseSymbol}`)
|
|
||||||
})
|
|
||||||
test('Waxing crescent: phaseName is "Waxing Crescent"', () => {
|
|
||||||
const p = getMoonPhase(DATE_WAXING)
|
|
||||||
assert.equal(p.phaseName, 'Waxing Crescent')
|
|
||||||
})
|
|
||||||
test('Waxing crescent: phaseSymbol is 🌒', () => {
|
|
||||||
const p = getMoonPhase(DATE_WAXING)
|
|
||||||
assert.equal(p.phaseSymbol, '🌒')
|
|
||||||
})
|
|
||||||
test('phaseName and phaseSymbol are consistent with phase key', () => {
|
|
||||||
// If phase is 'waning-crescent', phaseName should be 'Waning Crescent'
|
|
||||||
const p = getMoonPhase(DATE_WANING)
|
|
||||||
assert.equal(typeof p.phaseName, 'string')
|
|
||||||
assert.ok(p.phaseName.length > 0)
|
|
||||||
assert.ok(PHASE_SYMBOLS.has(p.phaseSymbol))
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// ─── getMoonVisibilityEstimate ─────────────────────────────────────────────────
|
// ─── getMoonVisibilityEstimate ─────────────────────────────────────────────────
|
||||||
|
|
||||||
console.log('\ngetMoonVisibilityEstimate:')
|
describe('getMoonVisibilityEstimate', () => {
|
||||||
|
const DATE_VIS_ESTIMATE = new Date('2025-03-02T18:30:00Z')
|
||||||
|
const vis = getMoonVisibilityEstimate(DATE_VIS_ESTIMATE, 51.5074, -0.1278, 10)
|
||||||
|
|
||||||
// London, 40 min after nominal sunset on 2025-03-01 (day after new moon)
|
it('returns an object', () => {
|
||||||
const DATE_VIS_ESTIMATE = new Date('2025-03-02T18:30:00Z')
|
assert.ok(vis !== null && typeof vis === 'object')
|
||||||
const vis = getMoonVisibilityEstimate(DATE_VIS_ESTIMATE, 51.5074, -0.1278, 10)
|
})
|
||||||
|
it('zone is A, B, C, or D', () => {
|
||||||
test('getMoonVisibilityEstimate returns an object', () => {
|
assert.ok(['A', 'B', 'C', 'D'].includes(vis.zone), `got: ${vis.zone}`)
|
||||||
assert.ok(vis !== null && typeof vis === 'object')
|
})
|
||||||
})
|
it('V is finite', () => {
|
||||||
test('getMoonVisibilityEstimate.zone is A, B, C, or D', () => {
|
assert.ok(isFinite(vis.V), `V=${vis.V}`)
|
||||||
assert.ok(['A', 'B', 'C', 'D'].includes(vis.zone), `got: ${vis.zone}`)
|
})
|
||||||
})
|
it('ARCL is in [0, 180]', () => {
|
||||||
test('getMoonVisibilityEstimate.V is finite', () => {
|
assert.ok(vis.ARCL >= 0 && vis.ARCL <= 180, `ARCL=${vis.ARCL}`)
|
||||||
assert.ok(isFinite(vis.V), `V=${vis.V}`)
|
})
|
||||||
})
|
it('W >= 0', () => {
|
||||||
test('getMoonVisibilityEstimate.ARCL is in [0, 180]', () => {
|
assert.ok(vis.W >= 0, `W=${vis.W}`)
|
||||||
assert.ok(vis.ARCL >= 0 && vis.ARCL <= 180, `ARCL=${vis.ARCL}`)
|
})
|
||||||
})
|
it('isApproximate is true', () => {
|
||||||
test('getMoonVisibilityEstimate.W >= 0', () => {
|
assert.equal(vis.isApproximate, true)
|
||||||
assert.ok(vis.W >= 0, `W=${vis.W}`)
|
})
|
||||||
})
|
it('moonAboveHorizon is a boolean', () => {
|
||||||
test('getMoonVisibilityEstimate.isApproximate is true', () => {
|
assert.equal(typeof vis.moonAboveHorizon, 'boolean')
|
||||||
assert.equal(vis.isApproximate, true)
|
})
|
||||||
})
|
it('isVisibleNakedEye matches zone A', () => {
|
||||||
test('getMoonVisibilityEstimate.moonAboveHorizon is a boolean', () => {
|
assert.equal(vis.isVisibleNakedEye, vis.zone === 'A')
|
||||||
assert.equal(typeof vis.moonAboveHorizon, 'boolean')
|
})
|
||||||
})
|
it('isVisibleWithOpticalAid matches zone A or B', () => {
|
||||||
test('getMoonVisibilityEstimate.isVisibleNakedEye matches zone A', () => {
|
assert.equal(vis.isVisibleWithOpticalAid, vis.zone === 'A' || vis.zone === 'B')
|
||||||
assert.equal(vis.isVisibleNakedEye, vis.zone === 'A')
|
})
|
||||||
})
|
it('description is a non-empty string', () => {
|
||||||
test('getMoonVisibilityEstimate.isVisibleWithOpticalAid matches zone A or B', () => {
|
assert.ok(typeof vis.description === 'string' && vis.description.length > 0)
|
||||||
assert.equal(vis.isVisibleWithOpticalAid, vis.zone === 'A' || vis.zone === 'B')
|
})
|
||||||
})
|
it('default date works', () => {
|
||||||
test('getMoonVisibilityEstimate.description is a non-empty string', () => {
|
const v = getMoonVisibilityEstimate(new Date(), 21.4225, 39.8262)
|
||||||
assert.ok(typeof vis.description === 'string' && vis.description.length > 0)
|
assert.ok(['A', 'B', 'C', 'D'].includes(v.zone))
|
||||||
})
|
assert.ok(isFinite(v.V))
|
||||||
test('getMoonVisibilityEstimate default date works', () => {
|
assert.equal(v.isApproximate, true)
|
||||||
const v = getMoonVisibilityEstimate(new Date(), 21.4225, 39.8262)
|
})
|
||||||
assert.ok(['A', 'B', 'C', 'D'].includes(v.zone))
|
it('near new moon: zone is D or C', () => {
|
||||||
assert.ok(isFinite(v.V))
|
const nearNew = getMoonVisibilityEstimate(new Date('2025-03-29T18:00:00Z'), 21.4225, 39.8262)
|
||||||
assert.equal(v.isApproximate, true)
|
assert.ok(['C', 'D'].includes(nearNew.zone), `zone=${nearNew.zone} V=${nearNew.V.toFixed(2)}`)
|
||||||
})
|
})
|
||||||
// Near new moon: elongation small, W small, crescent should be very thin or invisible
|
|
||||||
test('Near new moon: zone is D or C (not visible or marginal)', () => {
|
|
||||||
const nearNew = getMoonVisibilityEstimate(new Date('2025-03-29T18:00:00Z'), 21.4225, 39.8262)
|
|
||||||
assert.ok(['C', 'D'].includes(nearNew.zone), `zone=${nearNew.zone} V=${nearNew.V.toFixed(2)}`)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// ─── getMoon ──────────────────────────────────────────────────────────────────
|
// ─── getMoon ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
console.log('\ngetMoon:')
|
describe('getMoon', () => {
|
||||||
|
const moon = getMoon(new Date('2025-03-05T20:00:00Z'), 51.5074, -0.1278, 10)
|
||||||
|
|
||||||
const moon = getMoon(new Date('2025-03-05T20:00:00Z'), 51.5074, -0.1278, 10)
|
it('returns object with phase, position, illumination, visibility', () => {
|
||||||
|
assert.ok(typeof moon === 'object')
|
||||||
test('getMoon returns an object with phase, position, illumination, visibility', () => {
|
assert.ok(typeof moon.phase === 'object')
|
||||||
assert.ok(typeof moon === 'object')
|
assert.ok(typeof moon.position === 'object')
|
||||||
assert.ok(typeof moon.phase === 'object')
|
assert.ok(typeof moon.illumination === 'object')
|
||||||
assert.ok(typeof moon.position === 'object')
|
assert.ok(typeof moon.visibility === 'object')
|
||||||
assert.ok(typeof moon.illumination === 'object')
|
})
|
||||||
assert.ok(typeof moon.visibility === 'object')
|
it('phase is consistent with getMoonPhase standalone', () => {
|
||||||
})
|
const standalone = getMoonPhase(new Date('2025-03-05T20:00:00Z'))
|
||||||
test('getMoon.phase is consistent with getMoonPhase standalone', () => {
|
assert.equal(moon.phase.phase, standalone.phase)
|
||||||
const standalone = getMoonPhase(new Date('2025-03-05T20:00:00Z'))
|
assert.equal(moon.phase.phaseName, standalone.phaseName)
|
||||||
assert.equal(moon.phase.phase, standalone.phase)
|
})
|
||||||
assert.equal(moon.phase.phaseName, standalone.phaseName)
|
it('illumination.isWaxing matches phase.isWaxing', () => {
|
||||||
})
|
assert.equal(moon.illumination.isWaxing, moon.phase.isWaxing)
|
||||||
test('getMoon.illumination.isWaxing matches phase.isWaxing', () => {
|
})
|
||||||
assert.equal(moon.illumination.isWaxing, moon.phase.isWaxing)
|
it('visibility.isApproximate is true', () => {
|
||||||
})
|
assert.equal(moon.visibility.isApproximate, true)
|
||||||
test('getMoon.visibility.isApproximate is true', () => {
|
})
|
||||||
assert.equal(moon.visibility.isApproximate, true)
|
it('position has valid azimuth and altitude', () => {
|
||||||
})
|
assert.ok(moon.position.azimuth >= 0 && moon.position.azimuth < 360)
|
||||||
test('getMoon.position has valid azimuth and altitude', () => {
|
assert.ok(moon.position.altitude >= -90 && moon.position.altitude <= 90)
|
||||||
assert.ok(moon.position.azimuth >= 0 && moon.position.azimuth < 360)
|
})
|
||||||
assert.ok(moon.position.altitude >= -90 && moon.position.altitude <= 90)
|
it('default date works', () => {
|
||||||
})
|
const m = getMoon(new Date(), 21.4225, 39.8262)
|
||||||
test('getMoon default date works', () => {
|
assert.ok(isFinite(m.position.azimuth))
|
||||||
const m = getMoon(new Date(), 21.4225, 39.8262)
|
assert.ok(isFinite(m.illumination.fraction))
|
||||||
assert.ok(PHASE_NAMES.has(m.phase.phaseName))
|
assert.ok(['A', 'B', 'C', 'D'].includes(m.visibility.zone))
|
||||||
assert.ok(isFinite(m.position.azimuth))
|
})
|
||||||
assert.ok(isFinite(m.illumination.fraction))
|
|
||||||
assert.ok(['A', 'B', 'C', 'D'].includes(m.visibility.zone))
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// ─── Summary ─────────────────────────────────────────────────────────────────
|
// ─── Input validation ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
console.log(`\n${passed + failed} tests: ${passed} passed, ${failed} failed`)
|
describe('Input validation', () => {
|
||||||
|
it('getMoonPhase rejects invalid date', () => {
|
||||||
if (failed > 0) {
|
assert.throws(() => getMoonPhase(new Date('invalid')), /valid Date/)
|
||||||
process.exit(1)
|
})
|
||||||
}
|
it('getMoonPosition rejects latitude out of range', () => {
|
||||||
|
assert.throws(() => getMoonPosition(new Date(), 91, 0), /latitude/)
|
||||||
|
})
|
||||||
|
it('getMoonPosition rejects longitude out of range', () => {
|
||||||
|
assert.throws(() => getMoonPosition(new Date(), 0, 181), /longitude/)
|
||||||
|
})
|
||||||
|
it('getMoonPosition rejects NaN latitude', () => {
|
||||||
|
assert.throws(() => getMoonPosition(new Date(), NaN, 0), /latitude/)
|
||||||
|
})
|
||||||
|
it('getMoonVisibilityEstimate rejects invalid coordinates', () => {
|
||||||
|
assert.throws(() => getMoonVisibilityEstimate(new Date(), -91, 0), /latitude/)
|
||||||
|
})
|
||||||
|
it('getMoon rejects invalid coordinates', () => {
|
||||||
|
assert.throws(() => getMoon(new Date(), 0, 200), /longitude/)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,9 @@
|
||||||
"module": "ESNext",
|
"module": "ESNext",
|
||||||
"moduleResolution": "bundler",
|
"moduleResolution": "bundler",
|
||||||
"strict": true,
|
"strict": true,
|
||||||
|
"noImplicitReturns": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
"forceConsistentCasingInFileNames": true,
|
"forceConsistentCasingInFileNames": true,
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
"declaration": true,
|
"declaration": true,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue