From 4f5b29bee0c13154fbd0d0fe9880594204799c95 Mon Sep 17 00:00:00 2001 From: benweru <138494503+benweru@users.noreply.github.com> Date: Tue, 20 Oct 2026 01:46:38 +0300 Subject: [PATCH] feat(web): show a live satellite and Nairobi contact windows Co-authored-by: Cursor --- web/README.md | 6 +- web/package-lock.json | 769 ++++++++++++++++++++++++++++++++++++++ web/package.json | 3 + web/src/App.tsx | 152 ++++---- web/src/Globe.tsx | 145 +++++++ web/src/index.css | 34 +- web/src/orbit/geo.test.ts | 17 + web/src/orbit/geo.ts | 47 +++ web/vite.config.ts | 60 ++- 9 files changed, 1151 insertions(+), 82 deletions(-) create mode 100644 web/src/Globe.tsx create mode 100644 web/src/orbit/geo.test.ts create mode 100644 web/src/orbit/geo.ts diff --git a/web/README.md b/web/README.md index ea6fb72..4f53ed2 100644 --- a/web/README.md +++ b/web/README.md @@ -22,8 +22,10 @@ From the repo root, `make test-web` runs `npm test` here. It is not part of - 12: scaffold - 13: CelesTrak TLEs + SGP4 -- 14: Nairobi contact windows (this branch) -- 15: Cesium globe, picker, credits, Heavens-Above/N2YO check +- 14: Nairobi contact windows +- 15: Cesium globe, picker, credits, Heavens-Above/N2YO check (this branch) + +Optional `VITE_CESIUM_ION_TOKEN` is unused in v1; the globe uses OpenStreetMap tiles so it runs without Ion. `npm run dev` proxies `/api/celestrak` to CelesTrak so the browser can fetch TLEs. Tests use a committed ISS fixture and never hit the network. diff --git a/web/package-lock.json b/web/package-lock.json index ea8e95e..a3759de 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -8,17 +8,20 @@ "name": "orbitlog-web", "version": "0.0.1", "dependencies": { + "cesium": "1.125.0", "react": "19.3.1", "react-dom": "19.3.1", "satellite.js": "7.1.0" }, "devDependencies": { + "@types/node": "22.18.0", "@types/react": "19.3.0", "@types/react-dom": "19.3.0", "@vitejs/plugin-react": "4.7.0", "jsdom": "26.1.0", "typescript": "5.9.3", "vite": "6.4.4", + "vite-plugin-cesium": "1.2.22", "vitest": "3.2.7" }, "engines": { @@ -328,6 +331,79 @@ "node": ">=6.9.0" } }, + "node_modules/@cesium/engine": { + "version": "13.1.0", + "resolved": "https://registry.npmjs.org/@cesium/engine/-/engine-13.1.0.tgz", + "integrity": "sha512-1PX7sMhtgBgaSOtyQ0lTPvG35bMfR1ngtgBZqzNnR67lp+tVv9lNyAg/byBSnijL2b2NJNC2Iec6A6Z/Rj12OA==", + "license": "Apache-2.0", + "dependencies": { + "@tweenjs/tween.js": "^25.0.0", + "@zip.js/zip.js": "^2.7.34", + "autolinker": "^4.0.0", + "bitmap-sdf": "^1.0.3", + "dompurify": "^3.0.2", + "draco3d": "^1.5.1", + "earcut": "^3.0.0", + "grapheme-splitter": "^1.0.4", + "jsep": "^1.3.8", + "kdbush": "^4.0.1", + "ktx-parse": "^0.7.0", + "lerc": "^2.0.0", + "mersenne-twister": "^1.1.0", + "meshoptimizer": "^0.22.0", + "pako": "^2.0.4", + "protobufjs": "^7.1.0", + "rbush": "3.0.1", + "topojson-client": "^3.1.0", + "urijs": "^1.19.7" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@cesium/widgets": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/@cesium/widgets/-/widgets-10.2.0.tgz", + "integrity": "sha512-f0Wrp3MG02P2KSAopVESHWOhF+2eK9cQR9prEYGWuPM3iF6YktKrZqXzNnsOxCw1KPup0aSXsCrFl2asT5jF9g==", + "license": "Apache-2.0", + "dependencies": { + "@cesium/engine": "^14.0.0", + "nosleep.js": "^0.12.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@cesium/widgets/node_modules/@cesium/engine": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/@cesium/engine/-/engine-14.0.0.tgz", + "integrity": "sha512-nmW0uQCyg4CRqi3a8o30gU9S9bFm3TLh2fRO74iv+6a8FFArvZ1xF7IqG2tyRjSXkyH4c5vpavNRT3R51R8NyQ==", + "license": "Apache-2.0", + "dependencies": { + "@tweenjs/tween.js": "^25.0.0", + "@zip.js/zip.js": "^2.7.34", + "autolinker": "^4.0.0", + "bitmap-sdf": "^1.0.3", + "dompurify": "^3.0.2", + "draco3d": "^1.5.1", + "earcut": "^3.0.0", + "grapheme-splitter": "^1.0.4", + "jsep": "^1.3.8", + "kdbush": "^4.0.1", + "ktx-parse": "^0.7.0", + "lerc": "^2.0.0", + "mersenne-twister": "^1.1.0", + "meshoptimizer": "^0.22.0", + "pako": "^2.0.4", + "protobufjs": "^7.1.0", + "rbush": "3.0.1", + "topojson-client": "^3.1.0", + "urijs": "^1.19.7" + }, + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/@csstools/color-helpers": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz", @@ -955,6 +1031,63 @@ "node": "^22.20 || ^24.12 || >=25" } }, + "node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/codegen": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.5.tgz", + "integrity": "sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/eventemitter": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.1.tgz", + "integrity": "sha512-vW1GmwMZNnL+gMRaovlh9yZX74kc+TTU3FObkkurpMaRtBfLP3ldjS9KQWlwZgraRE0+dheEEoAxdzcJQ8eXZg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/fetch": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.1.tgz", + "integrity": "sha512-GpptLrs57adMSuHi3VNj0mAF8dwh36LMaYF6XyJ6JMWlVsc+t42tm1HSEDmOs3A8fC9yyeisgLhsTVQokOZ0zw==", + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.1" + } + }, + "node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/utf8": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.2.tgz", + "integrity": "sha512-b1UQwcEZ4yCnMCD8DAL1VlbvBJE9/IX4FTIp7BG1xYpf29SLazLSrqUkj4w7Y5y7cCVP6E5tcqqcI0xemPkHug==", + "license": "BSD-3-Clause" + }, "node_modules/@rolldown/pluginutils": { "version": "1.0.0-beta.27", "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", @@ -962,6 +1095,40 @@ "dev": true, "license": "MIT" }, + "node_modules/@rollup/pluginutils": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-4.2.1.tgz", + "integrity": "sha512-iKnFXr7NkdZAIHiIWE+BX5ULi/ucVFYWD6TbAV+rZctiRTY2PL6tsIKhoIOaoskiWAkgu+VsbXgUVDNLHf+InQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "estree-walker": "^2.0.1", + "picomatch": "^2.2.2" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/@rollup/pluginutils/node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/pluginutils/node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/@rollup/rollup-android-arm-eabi": { "version": "4.64.10", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.64.10.tgz", @@ -1351,6 +1518,12 @@ "win32" ] }, + "node_modules/@tweenjs/tween.js": { + "version": "25.0.0", + "resolved": "https://registry.npmjs.org/@tweenjs/tween.js/-/tween.js-25.0.0.tgz", + "integrity": "sha512-XKLA6syeBUaPzx4j3qwMqzzq+V4uo72BnlbOjmuljLrRqdsd3qnzvZZoxvMHZ23ndsRS4aufU6JOZYpCbU6T1A==", + "license": "MIT" + }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -1421,6 +1594,15 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/node": { + "version": "22.18.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.18.0.tgz", + "integrity": "sha512-m5ObIqwsUp6BZzyiy4RdZpzWGub9bqLJMvZDD0QMXhxjqMHMENlj+SqF5QxoUwaQNFe+8kz8XM8ZQhqkQPTgMQ==", + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, "node_modules/@types/react": { "version": "19.3.0", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.3.0.tgz", @@ -1441,6 +1623,13 @@ "@types/react": "^19.3.0" } }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "license": "MIT", + "optional": true + }, "node_modules/@vitejs/plugin-react": { "version": "4.7.0", "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", @@ -1577,6 +1766,17 @@ "url": "https://opencollective.com/vitest" } }, + "node_modules/@zip.js/zip.js": { + "version": "2.24.0", + "resolved": "https://registry.npmjs.org/@zip.js/zip.js/-/zip.js-2.24.0.tgz", + "integrity": "sha512-WlaVKzdqBK+RF3bdiXZWbdCnvGB6PdO4IHC1mn/2Yfl3bASgqUNegnU3+PNg61iecwoPfUi5BgLUPqBukXpyWw==", + "license": "BSD-3-Clause", + "engines": { + "bun": ">=0.7.0", + "deno": ">=1.0.0", + "node": ">=18.0.0" + } + }, "node_modules/agent-base": { "version": "7.1.4", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", @@ -1597,6 +1797,28 @@ "node": ">=12" } }, + "node_modules/at-least-node": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", + "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/autolinker": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/autolinker/-/autolinker-4.1.5.tgz", + "integrity": "sha512-vEfYZPmvVOIuE567XBVCsx8SBgOYtjB2+S1iAaJ+HgH+DNjAcrHem2hmAeC9yaNGWayicv4yR+9UaJlkF3pvtw==", + "license": "MIT", + "dependencies": { + "tslib": "^2.8.1" + }, + "engines": { + "pnpm": ">=10.10.0" + } + }, "node_modules/baseline-browser-mapping": { "version": "2.11.30", "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.30.tgz", @@ -1610,6 +1832,12 @@ "node": ">=6.0.0" } }, + "node_modules/bitmap-sdf": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/bitmap-sdf/-/bitmap-sdf-1.0.4.tgz", + "integrity": "sha512-1G3U4n5JE6RAiALMxu0p1XmeZkTeCwGKykzsLTCqVzfSDaN6S7fKnkIkfejogz+iwqBWc0UYAIKnKHNN7pSfDg==", + "license": "MIT" + }, "node_modules/browserslist": { "version": "4.29.4", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.29.4.tgz", @@ -1675,6 +1903,23 @@ ], "license": "CC-BY-4.0" }, + "node_modules/cesium": { + "version": "1.125.0", + "resolved": "https://registry.npmjs.org/cesium/-/cesium-1.125.0.tgz", + "integrity": "sha512-T6V1xKFQOxydJJtG6ammyDOwKMerkGLdxQJLJSLHhhh6VKqpUyp+GXSKjnzlAmpLDjQLF1UkhbJ98WC59am/fA==", + "license": "Apache-2.0", + "workspaces": [ + "packages/engine", + "packages/widgets" + ], + "dependencies": { + "@cesium/engine": "^13.1.0", + "@cesium/widgets": "^10.1.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, "node_modules/chai": { "version": "5.3.3", "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", @@ -1702,6 +1947,12 @@ "node": ">= 16" } }, + "node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "license": "MIT" + }, "node_modules/convert-source-map": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", @@ -1779,6 +2030,55 @@ "node": ">=6" } }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/dompurify": { + "version": "3.4.17", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.17.tgz", + "integrity": "sha512-B/pSlYVHQ58kqISD/yDjuJ1yfzI34m1gF4lAod2bwSRhiN4K5Cl3H8Af7hqB2PmTL911tdEudnnXn5FnhRoz0w==", + "license": "(MPL-2.0 OR Apache-2.0)", + "optionalDependencies": { + "@types/trusted-types": "^2.0.7" + } + }, + "node_modules/draco3d": { + "version": "1.5.9", + "resolved": "https://registry.npmjs.org/draco3d/-/draco3d-1.5.9.tgz", + "integrity": "sha512-MqC0eAfW5gPATGY3GABIyFWpHwdDGAyewTdek5R/e+mhI9ZDUMKb2mTDkTMUW0+Vm7an+ThEMUop3+svS72Uug==", + "license": "Apache-2.0" + }, + "node_modules/earcut": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/earcut/-/earcut-3.2.4.tgz", + "integrity": "sha512-FHc6yNpnTORB0tM5kmZ03iD2mDuNakDE2lvf4Hw1XkfxtEHIGVi01B/GqUSwx+NcC1WGjidAfpTXzeALtPtfpg==", + "license": "ISC" + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "dev": true, + "license": "MIT" + }, "node_modules/electron-to-chromium": { "version": "1.5.459", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.459.tgz", @@ -1786,6 +2086,16 @@ "dev": true, "license": "ISC" }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/entities": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", @@ -1858,6 +2168,13 @@ "node": ">=6" } }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "dev": true, + "license": "MIT" + }, "node_modules/estree-walker": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", @@ -1868,6 +2185,16 @@ "@types/estree": "^1.0.0" } }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/expect-type": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", @@ -1896,6 +2223,32 @@ } } }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -1921,6 +2274,19 @@ "node": ">=6.9.0" } }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/grapheme-splitter": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz", + "integrity": "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==", + "license": "MIT" + }, "node_modules/html-encoding-sniffer": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", @@ -1934,6 +2300,27 @@ "node": ">=18" } }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/http-proxy-agent": { "version": "7.0.2", "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", @@ -1975,6 +2362,13 @@ "node": ">=0.10.0" } }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, "node_modules/is-potential-custom-element-name": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", @@ -1982,6 +2376,16 @@ "dev": true, "license": "MIT" }, + "node_modules/is-reference": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-1.2.1.tgz", + "integrity": "sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "*" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -2029,6 +2433,15 @@ } } }, + "node_modules/jsep": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/jsep/-/jsep-1.6.0.tgz", + "integrity": "sha512-Y8Tr54CX7B5YGAwo7/n8eumidEX3Opx98RyyQVnlHqUaZcPA8uUzL2v1rshr9ypW9Q6hNjfJtg3sL+8kaNE7TA==", + "license": "MIT", + "engines": { + "node": ">= 20.16.0" + } + }, "node_modules/jsesc": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", @@ -2055,6 +2468,43 @@ "node": ">=6" } }, + "node_modules/jsonfile": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/kdbush": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/kdbush/-/kdbush-4.1.0.tgz", + "integrity": "sha512-e9vurzrXJQrFX6ckpHP3bvj5l+9CnYzkxDNnNQ1h2QTqdWsUAJgXiKdGNcOa1EY85dU8KbQ+z/FdQdB7P+9yfQ==", + "license": "ISC" + }, + "node_modules/ktx-parse": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/ktx-parse/-/ktx-parse-0.7.1.tgz", + "integrity": "sha512-FeA3g56ksdFNwjXJJsc1CCc7co+AJYDp6ipIp878zZ2bU8kWROatLYf39TQEd4/XRSUvBXovQ8gaVKWPXsCLEQ==", + "license": "MIT" + }, + "node_modules/lerc": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/lerc/-/lerc-2.0.0.tgz", + "integrity": "sha512-7qo1Mq8ZNmaR4USHHm615nEW2lPeeWJ3bTyoqFbd35DLx0LUH7C6ptt5FDCTAlbIzs3+WKrk5SkJvw8AFDE2hg==", + "license": "Apache-2.0" + }, + "node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "license": "Apache-2.0" + }, "node_modules/loupe": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", @@ -2082,6 +2532,31 @@ "@jridgewell/sourcemap-codec": "^1.5.5" } }, + "node_modules/mersenne-twister": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/mersenne-twister/-/mersenne-twister-1.1.0.tgz", + "integrity": "sha512-mUYWsMKNrm4lfygPkL3OfGzOPTR2DBlTkBNHM//F6hGp8cLThY897crAlk3/Jo17LEOOjQUrNAx6DvgO77QJkA==", + "license": "MIT" + }, + "node_modules/meshoptimizer": { + "version": "0.22.0", + "resolved": "https://registry.npmjs.org/meshoptimizer/-/meshoptimizer-0.22.0.tgz", + "integrity": "sha512-IebiK79sqIy+E4EgOr+CAw+Ke8hAspXKzBd0JdgEmPHiAwmvEj2S4h1rfvo+o/BnfEYd/jAOg5IeeIjzlzSnDg==", + "license": "MIT" + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "dev": true, + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -2118,6 +2593,12 @@ "node": ">=18" } }, + "node_modules/nosleep.js": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/nosleep.js/-/nosleep.js-0.12.0.tgz", + "integrity": "sha512-9d1HbpKLh3sdWlhXMhU6MMH+wQzKkrgfRkYV0EBdvt99YJfj0ilCJrWRDYG2130Tm4GXbEoTCx5b34JSaP+HhA==", + "license": "MIT" + }, "node_modules/nwsapi": { "version": "2.2.28", "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.28.tgz", @@ -2125,6 +2606,35 @@ "dev": true, "license": "MIT" }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/pako": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/pako/-/pako-2.2.0.tgz", + "integrity": "sha512-zJq6RP/5q+TO2OpFV3FHzlPnFjmkb7Nc99a5SNjJE+uu/PkpChs+NIZSSzbBoD+6kjiISXjfYdwj1ZRQ81dz/w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "(MIT AND Zlib)" + }, "node_modules/parse5": { "version": "7.3.0", "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", @@ -2138,6 +2648,16 @@ "url": "https://github.com/inikulin/parse5?sponsor=1" } }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/pathe": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", @@ -2204,6 +2724,29 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/protobufjs": { + "version": "7.6.6", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.6.6.tgz", + "integrity": "sha512-dYDWdjSl5RNb7SgPxGQcRU+GtvP7s2fpkrY0r432PcOIaZ0/rBcxEZnQN67iJhFuQiVw754JDoPruPCNdGsbjg==", + "hasInstallScript": true, + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.5", + "@protobufjs/eventemitter": "^1.1.1", + "@protobufjs/fetch": "^1.1.1", + "@protobufjs/float": "^1.0.2", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.1", + "@types/node": ">=13.7.0", + "long": "^5.3.2" + }, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -2214,6 +2757,31 @@ "node": ">=6" } }, + "node_modules/quickselect": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/quickselect/-/quickselect-2.0.0.tgz", + "integrity": "sha512-RKJ22hX8mHe3Y6wH/N3wCM6BWtjaxIyyUIkpHOvfFnxdI4yD4tBXEBKSbriGujF6jnSVkJrffuo6vxACiSSxIw==", + "license": "ISC" + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/rbush": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/rbush/-/rbush-3.0.1.tgz", + "integrity": "sha512-XRaVO0YecOpEuIvbhbpTrZgoiI6xBlz6hnlr6EHhd+0x9ase6EmeN+hdwwUaJvLcsFFQ8iWVF1GAK1yB0BWi0w==", + "license": "MIT", + "dependencies": { + "quickselect": "^2.0.0" + } + }, "node_modules/react": { "version": "19.3.1", "resolved": "https://registry.npmjs.org/react/-/react-19.3.1.tgz", @@ -2340,6 +2908,71 @@ "semver": "bin/semver.js" } }, + "node_modules/send": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", + "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.1", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "~2.4.1", + "range-parser": "~1.2.1", + "statuses": "~2.0.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/send/node_modules/debug/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, + "license": "MIT" + }, + "node_modules/serve-static": { + "version": "1.16.3", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz", + "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "~0.19.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "dev": true, + "license": "ISC" + }, "node_modules/siginfo": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", @@ -2357,6 +2990,14 @@ "node": ">=0.10.0" } }, + "node_modules/sourcemap-codec": { + "version": "1.4.8", + "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz", + "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==", + "deprecated": "Please use @jridgewell/sourcemap-codec instead", + "dev": true, + "license": "MIT" + }, "node_modules/stackback": { "version": "0.0.2", "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", @@ -2364,6 +3005,16 @@ "dev": true, "license": "MIT" }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/std-env": { "version": "3.10.0", "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", @@ -2479,6 +3130,30 @@ "dev": true, "license": "MIT" }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/topojson-client": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/topojson-client/-/topojson-client-3.1.0.tgz", + "integrity": "sha512-605uxS6bcYxGXw9qi62XyrV6Q3xwbndjachmNxu8HWTtVPxZfEJN9fd/SZS1Q54Sn2y0TMyMxFj/cJINqGHrKw==", + "license": "ISC", + "dependencies": { + "commander": "2" + }, + "bin": { + "topo2geo": "bin/topo2geo", + "topomerge": "bin/topomerge", + "topoquantize": "bin/topoquantize" + } + }, "node_modules/tough-cookie": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.2.tgz", @@ -2505,6 +3180,12 @@ "node": ">=18" } }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, "node_modules/typescript": { "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", @@ -2519,6 +3200,22 @@ "node": ">=14.17" } }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "license": "MIT" + }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, "node_modules/update-browserslist-db": { "version": "1.3.4", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.4.tgz", @@ -2550,6 +3247,12 @@ "browserslist": ">= 4.21.0" } }, + "node_modules/urijs": { + "version": "1.19.11", + "resolved": "https://registry.npmjs.org/urijs/-/urijs-1.19.11.tgz", + "integrity": "sha512-HXgFDgDommxn5/bIv0cnQZsPhHDA90NPHD6+c/v21U5+Sx5hoP8+dP9IZXBU1gIfvdRfhG8cel9QNPeionfcCQ==", + "license": "MIT" + }, "node_modules/vite": { "version": "6.4.4", "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.4.tgz", @@ -2648,6 +3351,72 @@ "url": "https://opencollective.com/vitest" } }, + "node_modules/vite-plugin-cesium": { + "version": "1.2.22", + "resolved": "https://registry.npmjs.org/vite-plugin-cesium/-/vite-plugin-cesium-1.2.22.tgz", + "integrity": "sha512-OnS+VKNGck4kUu4/67Fdfhz0/zF9mDVNUp9hUWtX19C38O0mJsJy2MH1ev2QcrVLf6VieJ7vCGxkLchdB1n1HQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fs-extra": "^9.1.0", + "rollup-plugin-external-globals": "^0.6.1", + "serve-static": "^1.14.1" + }, + "peerDependencies": { + "cesium": "^1.95.0", + "vite": ">=2.7.1" + } + }, + "node_modules/vite-plugin-cesium/node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/vite-plugin-cesium/node_modules/magic-string": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.9.tgz", + "integrity": "sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "sourcemap-codec": "^1.4.8" + } + }, + "node_modules/vite-plugin-cesium/node_modules/rollup": { + "version": "2.80.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.80.0.tgz", + "integrity": "sha512-cIFJOD1DESzpjOBl763Kp1AH7UE/0fcdHe6rZXUdQ9c50uvgigvW97u3IcSeBwOkgqL/PXPBktBCh0KEu5L8XQ==", + "dev": true, + "license": "MIT", + "peer": true, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=10.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/vite-plugin-cesium/node_modules/rollup-plugin-external-globals": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/rollup-plugin-external-globals/-/rollup-plugin-external-globals-0.6.1.tgz", + "integrity": "sha512-mlp3KNa5sE4Sp9UUR2rjBrxjG79OyZAh/QC18RHIjM+iYkbBwNXSo8DHRMZWtzJTrH8GxQ+SJvCTN3i14uMXIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^4.0.0", + "estree-walker": "^2.0.1", + "is-reference": "^1.2.1", + "magic-string": "^0.25.7" + }, + "peerDependencies": { + "rollup": "^2.25.0" + } + }, "node_modules/vitest": { "version": "3.2.7", "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.7.tgz", diff --git a/web/package.json b/web/package.json index dbd1491..8a18042 100644 --- a/web/package.json +++ b/web/package.json @@ -13,17 +13,20 @@ "test": "vitest run" }, "dependencies": { + "cesium": "1.125.0", "react": "19.3.1", "react-dom": "19.3.1", "satellite.js": "7.1.0" }, "devDependencies": { + "@types/node": "22.18.0", "@types/react": "19.3.0", "@types/react-dom": "19.3.0", "@vitejs/plugin-react": "4.7.0", "jsdom": "26.1.0", "typescript": "5.9.3", "vite": "6.4.4", + "vite-plugin-cesium": "1.2.22", "vitest": "3.2.7" } } diff --git a/web/src/App.tsx b/web/src/App.tsx index 7ff96d4..1a19afe 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -1,4 +1,5 @@ import { useEffect, useMemo, useState } from "react"; +import Globe from "./Globe"; import { tleCache, type CachedTle } from "./orbit/cache"; import { SATELLITES } from "./orbit/catalog"; import { countdownSeconds, formatCountdown } from "./orbit/countdown"; @@ -28,6 +29,9 @@ export default function App() { } } setCached(next); + if (!next[SATELLITES[0].catnr]) { + void load(SATELLITES[0].catnr); + } }, []); useEffect(() => { @@ -50,6 +54,7 @@ export default function App() { } const tle = cached[selected]; + const sat = SATELLITES.find((s) => s.catnr === selected) ?? SATELLITES[0]; const passBucket = Math.floor(now.getTime() / 15_000); const pass: Pass | null = useMemo(() => { if (!tle) { @@ -64,79 +69,78 @@ export default function App() { now.getTime() < pass.set.getTime(); return ( -
-

OrbitLog

-

Ride along with a real satellite and watch it think.

-

- A contact window is the few minutes a satellite is - above {DEFAULT_ELEVATION_MASK_DEG}° elevation at {NAIROBI.name}.{" "} - Look angles are azimuth (compass direction) and - elevation (angle above the horizon). The globe comes next. -

- - {error ?

{error}

: null} -
-

- Next pass over {NAIROBI.name} (mask {DEFAULT_ELEVATION_MASK_DEG}°) -

- {!tle ? ( -

Fetch a TLE to predict the next window.

- ) : !pass ? ( -

No pass in the next 36 hours for this TLE.

- ) : ( -
-
Countdown
-
- {inPass - ? `In contact · sets in ${formatCountdown(countdownSeconds(pass.set, now))}` - : formatCountdown(countdownSeconds(pass.rise, now))} -
-
Rise
-
{utc(pass.rise)}
-
Max elevation
-
- {pass.maxElDeg.toFixed(1)}° at {utc(pass.maxElAt)} -
-
Set
-
{utc(pass.set)}
-
- )} -
-

- v2, the onboard diagnosis story, is {V2_STATUS} until - the benchmark writes files under results/. This page will - never invent those numbers. -

-

- Faults in OrbitLog are simulated on a ground Linux box, not live - satellite telemetry. TLE data: CelesTrak. -

-
+
+ + +
); } diff --git a/web/src/Globe.tsx b/web/src/Globe.tsx new file mode 100644 index 0000000..0594ebf --- /dev/null +++ b/web/src/Globe.tsx @@ -0,0 +1,145 @@ +import { + Cartesian2, + Cartesian3, + Color, + ConstantPositionProperty, + EllipsoidTerrainProvider, + ImageryLayer, + OpenStreetMapImageryProvider, + Viewer, +} from "cesium"; +import { useEffect, useRef } from "react"; +import { llaAt, sampleTrack } from "./orbit/geo"; +import type { Pass } from "./orbit/pass"; +import { NAIROBI } from "./orbit/station"; +import type { ParsedTle } from "./orbit/tle"; + +type Props = { + name: string; + tle: ParsedTle | null; + pass: Pass | null; +}; + +const ORBIT_MS = 92 * 60 * 1000; +const ORBIT_STEP_MS = 30_000; +const PASS_STEP_MS = 5_000; + +function cart(lla: { latitudeDeg: number; longitudeDeg: number; heightKm: number }) { + return Cartesian3.fromDegrees( + lla.longitudeDeg, + lla.latitudeDeg, + lla.heightKm * 1000, + ); +} + +export default function Globe({ name, tle, pass }: Props) { + const host = useRef(null); + const viewerRef = useRef(null); + + useEffect(() => { + const el = host.current; + if (!el) { + return; + } + const viewer = new Viewer(el, { + baseLayer: new ImageryLayer( + new OpenStreetMapImageryProvider({ + url: "https://tile.openstreetmap.org/", + }), + ), + baseLayerPicker: false, + geocoder: false, + homeButton: false, + infoBox: false, + sceneModePicker: false, + selectionIndicator: false, + timeline: false, + animation: false, + navigationHelpButton: false, + terrainProvider: new EllipsoidTerrainProvider(), + }); + viewer.scene.globe.enableLighting = true; + viewer.entities.add({ + name: "Nairobi", + position: Cartesian3.fromDegrees( + NAIROBI.longitudeDeg, + NAIROBI.latitudeDeg, + 0, + ), + point: { pixelSize: 10, color: Color.WHITE }, + label: { + text: "Nairobi", + font: "14px sans-serif", + fillColor: Color.WHITE, + pixelOffset: new Cartesian2(0, -16), + showBackground: true, + }, + }); + viewerRef.current = viewer; + return () => { + viewer.destroy(); + viewerRef.current = null; + }; + }, []); + + useEffect(() => { + const viewer = viewerRef.current; + if (!viewer || viewer.isDestroyed()) { + return; + } + const extras = viewer.entities.values.filter((e) => e.name !== "Nairobi"); + for (const entity of extras) { + viewer.entities.remove(entity); + } + if (!tle) { + return; + } + const now = new Date(); + const orbit = sampleTrack(tle, new Date(now.getTime() - 10 * 60_000), ORBIT_MS, ORBIT_STEP_MS); + viewer.entities.add({ + name: "orbit", + polyline: { + positions: orbit.map(cart), + width: 2, + material: Color.CYAN.withAlpha(0.55), + }, + }); + if (pass) { + const windowTrack = sampleTrack( + tle, + pass.rise, + pass.set.getTime() - pass.rise.getTime(), + PASS_STEP_MS, + ); + viewer.entities.add({ + name: "contact", + polyline: { + positions: windowTrack.map(cart), + width: 4, + material: Color.YELLOW, + }, + }); + } + const sat = viewer.entities.add({ + name: "sat", + position: cart(llaAt(tle, now)), + point: { pixelSize: 12, color: Color.YELLOW }, + label: { + text: name, + font: "13px sans-serif", + fillColor: Color.YELLOW, + pixelOffset: new Cartesian2(0, -16), + showBackground: true, + }, + }); + const id = window.setInterval(() => { + if (viewer.isDestroyed()) { + return; + } + sat.position = new ConstantPositionProperty(cart(llaAt(tle, new Date()))); + }, 1000); + return () => window.clearInterval(id); + }, [tle, pass, name]); + + return
; +} diff --git a/web/src/index.css b/web/src/index.css index e0e1a4e..cb5db9c 100644 --- a/web/src/index.css +++ b/web/src/index.css @@ -9,14 +9,38 @@ box-sizing: border-box; } +html, +body, +#root { + height: 100%; +} + body { margin: 0; } -main { - max-width: 40rem; - margin: 0 auto; - padding: 3rem 1.25rem; +.shell { + position: relative; + height: 100%; + overflow: hidden; +} + +.globe { + position: absolute; + inset: 0; +} + +.hud { + position: absolute; + z-index: 1; + top: 1rem; + left: 1rem; + width: min(24rem, calc(100% - 2rem)); + max-height: calc(100% - 2rem); + overflow: auto; + padding: 1rem 1.1rem; + background: #111318f2; + border: 1px solid #2a2e35; } .kicker { @@ -28,7 +52,7 @@ main { } h1 { - font-size: 1.75rem; + font-size: 1.35rem; font-weight: 600; line-height: 1.25; margin: 0 0 1rem; diff --git a/web/src/orbit/geo.test.ts b/web/src/orbit/geo.test.ts new file mode 100644 index 0000000..8ba1892 --- /dev/null +++ b/web/src/orbit/geo.test.ts @@ -0,0 +1,17 @@ +import { describe, expect, it } from "vitest"; +import issTle from "./fixtures/iss.tle?raw"; +import { llaAt } from "./geo"; +import { parseTle } from "./tle"; + +describe("llaAt", () => { + it("keeps ISS within its 51.6° inclination band", () => { + const tle = parseTle(issTle); + const when = new Date(Date.UTC(2026, 9, 19, 10, 26, 22)); + const lla = llaAt(tle, when); + expect(Math.abs(lla.latitudeDeg)).toBeLessThan(52); + expect(lla.longitudeDeg).toBeGreaterThanOrEqual(-180); + expect(lla.longitudeDeg).toBeLessThanOrEqual(180); + expect(lla.heightKm).toBeGreaterThan(300); + expect(lla.heightKm).toBeLessThan(500); + }); +}); diff --git a/web/src/orbit/geo.ts b/web/src/orbit/geo.ts new file mode 100644 index 0000000..9eccf16 --- /dev/null +++ b/web/src/orbit/geo.ts @@ -0,0 +1,47 @@ +/** + * Convert SGP4’s ECI kilometres into geodetic lat/lon/height so Cesium can + * place the satellite on the globe. + */ +import * as satellite from "satellite.js"; +import { satrecFromTle } from "./look"; +import { propagateEci } from "./sgp4"; +import type { ParsedTle } from "./tle"; + +export type Lla = { + latitudeDeg: number; + longitudeDeg: number; + heightKm: number; +}; + +export function eciToLla(eci: { x: number; y: number; z: number }, when: Date): Lla { + const gd = satellite.eciToGeodetic(eci, satellite.gstime(when)); + return { + latitudeDeg: satellite.degreesLat(gd.latitude), + longitudeDeg: satellite.degreesLong(gd.longitude), + heightKm: gd.height, + }; +} + +export function llaAt(tle: ParsedTle, when: Date): Lla { + return eciToLla(propagateEci(tle, when), when); +} + +export function sampleTrack( + tle: ParsedTle, + from: Date, + durationMs: number, + stepMs: number, +): Lla[] { + const satrec = satrecFromTle(tle); + const out: Lla[] = []; + for (let t = from.getTime(); t <= from.getTime() + durationMs; t += stepMs) { + const when = new Date(t); + const result = satellite.propagate(satrec, when); + const position = result?.position; + if (!position || typeof position.x !== "number") { + continue; + } + out.push(eciToLla(position, when)); + } + return out; +} diff --git a/web/vite.config.ts b/web/vite.config.ts index eed018f..5ffb14a 100644 --- a/web/vite.config.ts +++ b/web/vite.config.ts @@ -1,8 +1,66 @@ import react from "@vitejs/plugin-react"; +import cesium from "vite-plugin-cesium"; import { defineConfig } from "vitest/config"; +function cesiumDevGlobal() { + let serve = false; + return { + name: "cesium-dev-global", + enforce: "pre" as const, + config(_cfg: unknown, env: { command: string }) { + serve = env.command === "serve"; + return { + optimizeDeps: { + exclude: ["cesium", "satellite.js"], + }, + }; + }, + resolveId(id: string) { + if (serve && id === "cesium") { + return "\0cesium-global"; + } + return undefined; + }, + load(id: string) { + if (id === "\0cesium-global") { + return ` + const c = window.Cesium; + export const Cartesian2 = c.Cartesian2; + export const Cartesian3 = c.Cartesian3; + export const Color = c.Color; + export const ConstantPositionProperty = c.ConstantPositionProperty; + export const EllipsoidTerrainProvider = c.EllipsoidTerrainProvider; + export const ImageryLayer = c.ImageryLayer; + export const OpenStreetMapImageryProvider = c.OpenStreetMapImageryProvider; + export const Viewer = c.Viewer; + `; + } + const normalized = id.replaceAll("\\", "/"); + if (normalized.includes("satellite.js/wasm-build")) { + return "export default {}"; + } + return undefined; + }, + transformIndexHtml() { + if (!serve) { + return []; + } + return [ + { + tag: "script", + attrs: { src: "/cesium/Cesium.js" }, + }, + ]; + }, + }; +} + export default defineConfig({ - plugins: [react()], + plugins: [ + react(), + cesium(), + cesiumDevGlobal(), + ], server: { proxy: { "/api/celestrak": {