dsh-astro-plan 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +150 -2
- package/cordis.patch.yml +10 -0
- package/docs/icon.svg +43 -0
- package/lib/api.js +84 -0
- package/lib/astro.js +242 -0
- package/lib/aurora.js +106 -0
- package/lib/catalog.js +214 -0
- package/lib/client.js +1863 -0
- package/lib/comets.js +211 -0
- package/lib/data/comets.json +1660 -0
- package/lib/data/db-stamp.json +7 -0
- package/lib/data/events.json +22 -0
- package/lib/data/foreground-models.json +13 -0
- package/lib/data/horizon.json +1 -0
- package/lib/data/params.json +69 -0
- package/lib/data/sites-core.json +2274 -0
- package/lib/data/sites-province.json +52 -0
- package/lib/data/sky.json +1 -0
- package/lib/data/targets.json +1536 -0
- package/lib/database.js +350 -0
- package/lib/events.js +414 -0
- package/lib/index.js +248 -0
- package/lib/planner.js +1979 -0
- package/lib/scene.js +243 -0
- package/lib/skyview.js +1574 -0
- package/lib/vendor/astronomy-engine.js +9675 -0
- package/lib/weather.js +274 -0
- package/lib/webui.js +950 -0
- package/package.json +55 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Sirius YANG
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,151 @@
|
|
|
1
|
-
#
|
|
1
|
+
# dsh-astro-plan · 星野计划
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://www.npmjs.com/package/dsh-astro-plan)
|
|
4
|
+
[](LICENSE)
|
|
5
|
+
[](https://github.com/SiriusYANG91/dsh-repository/blob/main/docs/user-guide.md)
|
|
6
|
+
|
|
7
|
+
星野计划(by Sirius YANG):按 18 类天象与深空目标、全国 173 站、最长一年检索,结合光污染、方位、月相、星历事件与 7 天天气,给出推荐排序、地景、焦段与曝光;每条结果附 Stellarium 风格 3D 天球画面模拟(真实银河等值线、真实月相、实测天际线)。
|
|
8
|
+
|
|
9
|
+

|
|
10
|
+
|
|
11
|
+
## 功能速览
|
|
12
|
+
|
|
13
|
+
| 能力 | 规模 / 说明 |
|
|
14
|
+
|---|---|
|
|
15
|
+
| 目标类型 | **18 类**,两级菜单:星野 6(银河/流星雨/彗星/星轨/黄道光/极光)· 日月 2 · 行星 4(单个行星/冲日/大距/合)· 深空 5(全部深空/星系/星云/星团/行星状星云) |
|
|
16
|
+
| 站点库 | **173 个**,覆盖 **34 个省级行政区**、8 个地区,含海拔、波特尔指数、实测天际线 |
|
|
17
|
+
| 时间跨度 | 单次最长 **365 天**(夜的定义:当日 15:00 至次日 09:00) |
|
|
18
|
+
| 天气 | 未来 **7 天内**接入 Open-Meteo 逐时预报,更远只按天文条件评估 |
|
|
19
|
+
| 评分 | 五维加权:天象 0.4 · 天气 0.2 · 地景 0.2 · 光污染 0.1 · 月相 0.1,缺维度按参与权重归一 |
|
|
20
|
+
| 画面模拟 | 立体投影 3D 天球、真实银河等值线、真实日月大小与月相、实测天际线、5°–180° 缩放、地平/赤道双坐标 |
|
|
21
|
+
| 运行时依赖 | **零**(星历用 vendored astronomy-engine,星表/银河为构建期快照) |
|
|
22
|
+
|
|
23
|
+
## 安装
|
|
24
|
+
|
|
25
|
+
三种方式任选其一,装完**重启 DSH 桌面应用**。
|
|
26
|
+
|
|
27
|
+
**1)插件市场**:侧边栏「插件」→「发现」→ 添加索引
|
|
28
|
+
|
|
29
|
+
```
|
|
30
|
+
https://raw.githubusercontent.com/SiriusYANG91/dsh-repository/main/docs/market-index.json
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
**2)npm**
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
dsh plugin --profile desktop add dsh-astro-plan@0.1.0
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
**3)本地目录(开发)**
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
dsh plugin --profile desktop add "<本目录绝对路径>"
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
验证:`curl http://127.0.0.1:<端口>/astro-plan/api/meta`
|
|
46
|
+
|
|
47
|
+
## 打开
|
|
48
|
+
|
|
49
|
+
| 入口 | 位置 |
|
|
50
|
+
|---|---|
|
|
51
|
+
| 会话 Tab | 侧边栏「星野计划」 |
|
|
52
|
+
| 独立页面 | `http://127.0.0.1:<端口>/astro-plan/` |
|
|
53
|
+
| 工具 | 对话里让模型调用 `astro_plan` |
|
|
54
|
+
|
|
55
|
+
页面参数:`?theme=light|dark` 强制配色、`?withSim=0` 关闭画面模拟。
|
|
56
|
+
|
|
57
|
+
## 用法(对话里)
|
|
58
|
+
|
|
59
|
+
1. 「这周末海坨山能拍什么?」
|
|
60
|
+
2. 「今年 8 月英仙座流星雨,华北哪个站点最好?」
|
|
61
|
+
3. 「明年 3 月去西藏拍深空,推荐 3 个目标,要星系」
|
|
62
|
+
4. 「10 月 14 日北京日食能看到吗?」
|
|
63
|
+
5. 「下个月有没有适合拍银河的暗夜窗口?」
|
|
64
|
+
6. 「M42 和 M31 哪个更适合我这周去拍?」
|
|
65
|
+
7. 「给我一条青海冷湖的整夜时间轴」
|
|
66
|
+
8. 「这几个目标的焦距和曝光分别是多少?」
|
|
67
|
+
|
|
68
|
+
| action | 作用 |
|
|
69
|
+
|---|---|
|
|
70
|
+
| `search` | 按条件检索推荐(默认) |
|
|
71
|
+
| `window` | 某站某夜逐小时时间轴 |
|
|
72
|
+
| `meta` | 数据概览(类型、地区、站点数、天气覆盖) |
|
|
73
|
+
| `events` | 天象事件(流星雨、银河季节窗口等) |
|
|
74
|
+
| `params` | 某目标的拍摄参数(`targetId`,如 `m42`) |
|
|
75
|
+
|
|
76
|
+
常用参数:`eventType`、`location`(站点/城市/省份/`lat,lon`/「全国」)、`region`、`province`、`dateStart`、`dateEnd`、`minAltitude`、`maxBortle`、`limit`、`originLat`/`originLon`。
|
|
77
|
+
|
|
78
|
+
## 界面
|
|
79
|
+
|
|
80
|
+
**查询区**:三行。拍摄日期(跨度上限一年,前端校验);目的地与出发地(地区→省份→站点三级联动,出发地默认「目前所在地」,定位失败先复用上次结果再按北京估算);拍摄目标(18 类两级菜单、高度角 ≥ 默认 0°、波特尔 ≤ 默认 6)。「查询」「重置」,打开页面不自动检索。
|
|
81
|
+
|
|
82
|
+
**结果列表**:八列 —— `#`、推荐度、目标类型、目标、拍摄地点、日期、最佳时段、最高高度;按推荐度排序,**20 条/页**,序号跨页连续,可跳页。
|
|
83
|
+
|
|
84
|
+
**便签详情**:点行展开,七个区块 —— 目标、时间与月相、地点、方位与构图、拍摄参数、天气、推荐度明细与推荐理由。维度随题材变化(日食只算天象/天气/地景;月食不算月相;行星与深空不算地景),缺维度不扣分。**窄屏(< 1100px)时画面模拟排在最上面一行,其余便签移到它下方**,宽屏仍是「左七块 + 右模拟」。
|
|
85
|
+
|
|
86
|
+
**画面模拟**:便签右侧整列(或窄屏时置顶)。立体投影投满整幅画面,无圆形取景框;银河按 5 级等值线加色绘制,暗尘带是真空洞;日月按真实视直径、月球按真实相位(晨昏线椭圆、亮面朝太阳);天际线来自实测高程(36 方位、1.5–140 km);缩放 5°–180°,地平/赤道网格切换,HUD 给时间、坐标系、视野、高度/方位(赤道模式另给赤经赤纬);播放覆盖最佳时段(最多 72 帧),带跟踪开关。
|
|
87
|
+
|
|
88
|
+
## 评分模型
|
|
89
|
+
|
|
90
|
+
| 维度 | 权重 | 说明 |
|
|
91
|
+
|---|---|---|
|
|
92
|
+
| 天象 | **0.4** | 可见性(高度、时长)与事件分值合并 |
|
|
93
|
+
| 天气 | **0.2** | 7 天内来自 Open-Meteo;更远不参与 |
|
|
94
|
+
| 地景 | **0.2** | 前景与目标的配合度、不可替代性 |
|
|
95
|
+
| 光污染 | **0.1** | 波特尔指数,越低越好 |
|
|
96
|
+
| 月相 | **0.1** | 月照度越低越好;月食/日食不参与 |
|
|
97
|
+
|
|
98
|
+
缺哪个维度就把它的权重从分母里去掉(不会把缺失当 0 分),最后换算成百分制:**≥80** 条件很好、**60–80** 值得去、**<60** 建议换目标或日期。
|
|
99
|
+
|
|
100
|
+
## 数据与更新
|
|
101
|
+
|
|
102
|
+
| 数据 | 规模 | 来源 |
|
|
103
|
+
|---|---|---|
|
|
104
|
+
| 站点 | 173 个 / 34 省 / 8 地区 | 自建(海拔、波特尔、天际线、地景标签) |
|
|
105
|
+
| 目标 | 71 个(深空 47:星系 12 / 星云 18 / 星团 11 / 行星状星云 6) | 自建 + 通用星表 |
|
|
106
|
+
| 彗星 | 103 颗 | MPC 轨道根数,**每周首次启动自动重建** |
|
|
107
|
+
| 天球 | 632 颗亮星(≤ 4.2 等)、76 星座、5 级银河、11 场流星雨 | d3-celestial(BSD-3-Clause),构建期快照 |
|
|
108
|
+
|
|
109
|
+
页面第一行小字显示彗星库最近一次成功重建时间;除天气外,插件运行时不联网。
|
|
110
|
+
|
|
111
|
+
## 截图
|
|
112
|
+
|
|
113
|
+

|
|
114
|
+
|
|
115
|
+

|
|
116
|
+
|
|
117
|
+
完整图文说明(14 章、9 张截图)见 **[用户指南](docs/user-guide.md)**。
|
|
118
|
+
|
|
119
|
+
## 开发
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
node --test --test-isolation=none "tests/*.test.js" # 全部测试
|
|
123
|
+
node scripts/validate-sites.mjs # 站点库校验
|
|
124
|
+
node tools/build-comets.mjs # 重建彗星库(需联网)
|
|
125
|
+
node tools/build-sky.mjs # 重建星表/星座/银河(需联网)
|
|
126
|
+
node tools/sync-sky-renderer.mjs # 改完 skyview.js 的 RENDER_SRC 后同步到 Tab
|
|
127
|
+
node tools/make-guide-images.mjs # 重出用户指南配图(需无头 Edge)
|
|
128
|
+
node tools/publish.mjs --check|--publish # 发布 npm 并刷新市场索引
|
|
129
|
+
node tools/push-repo.mjs # 把代码推到 GitHub
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
> 沙箱环境里 `node --test` 会为每个测试文件 spawn 子进程并被拒绝(EPERM),用 `--test-isolation=none` 单进程跑。
|
|
133
|
+
|
|
134
|
+
## 目录
|
|
135
|
+
|
|
136
|
+
```
|
|
137
|
+
lib/index.js 宿主入口:astro_plan 工具 + /astro-plan 路由
|
|
138
|
+
lib/planner.js 检索 → 可见窗口 → 五维评分 → 排序 / 时间轴
|
|
139
|
+
lib/skyview.js 画面模拟:星表岁差、meta.sky、sim 数据与 3D 渲染器源码
|
|
140
|
+
lib/astro.js 星历(高度方位/升落/暮光/月相,含岁差与时角)
|
|
141
|
+
lib/events.js 天象事件现算(食/冲/大距/合·合月 + 站级可见性)
|
|
142
|
+
lib/comets.js MPC 轨道根数 → 二体传播 + 亮度模型
|
|
143
|
+
lib/weather.js Open-Meteo 抓取与评分(10 分钟缓存)
|
|
144
|
+
lib/webui.js 自包含页面 lib/client.js 会话 Tab(零构建)
|
|
145
|
+
lib/data/ 站点 / 目标 / 参数 / 彗星 / 天际线 / 星表(+ NOTICE.md)
|
|
146
|
+
tools/ scripts/ 构建、发布、配图与校验脚本 tests/ 测试
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
## 许可
|
|
150
|
+
|
|
151
|
+
插件代码 MIT。`lib/vendor/astronomy-engine.js` 来自 [cosinekitty/astronomy](https://github.com/cosinekitty/astronomy)(MIT,Don Cross);`lib/data/sky.json` 由 [d3-celestial](https://github.com/ofrohn/d3-celestial)(BSD-3-Clause,Olaf Frohn)的公开星表裁剪生成,详见 `lib/data/NOTICE.md`。
|
package/cordis.patch.yml
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# dsh-astro-plan bundle patch: inserts the host row into the profile's plugin
|
|
2
|
+
# roster. The browser half rides the package's `dsh.client` manifest (the client
|
|
3
|
+
# registry serves lib/client.js), and this row materializes the astro-planning
|
|
4
|
+
# host plugin on boot.
|
|
5
|
+
#
|
|
6
|
+
# 包名 / 行 id / client id 三者保持一致(都是 dsh-astro-plan),这是 DSH 客户端
|
|
7
|
+
# 模块系统能稳定识别的形态;改成 scoped 名会让同一条 bundle 被注册两次。
|
|
8
|
+
- insert:
|
|
9
|
+
- id: dsh-astro-plan
|
|
10
|
+
name: 'dsh-astro-plan'
|
package/docs/icon.svg
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="256" height="256" viewBox="0 0 256 256" role="img" aria-label="Astro Plan">
|
|
2
|
+
<defs>
|
|
3
|
+
<linearGradient id="sky" x1="0" y1="0" x2="0" y2="1">
|
|
4
|
+
<stop offset="0" stop-color="#05070f"/>
|
|
5
|
+
<stop offset="0.62" stop-color="#0d1526"/>
|
|
6
|
+
<stop offset="1" stop-color="#16233a"/>
|
|
7
|
+
</linearGradient>
|
|
8
|
+
<linearGradient id="mw" x1="0" y1="0" x2="1" y2="1">
|
|
9
|
+
<stop offset="0" stop-color="#cfe0f7" stop-opacity="0"/>
|
|
10
|
+
<stop offset="0.5" stop-color="#cfe0f7" stop-opacity="0.55"/>
|
|
11
|
+
<stop offset="1" stop-color="#cfe0f7" stop-opacity="0"/>
|
|
12
|
+
</linearGradient>
|
|
13
|
+
<linearGradient id="frame" x1="0" y1="0" x2="1" y2="1">
|
|
14
|
+
<stop offset="0" stop-color="#d8b46a"/>
|
|
15
|
+
<stop offset="1" stop-color="#f0d79a"/>
|
|
16
|
+
</linearGradient>
|
|
17
|
+
</defs>
|
|
18
|
+
<rect width="256" height="256" rx="52" fill="url(#sky)"/>
|
|
19
|
+
<g>
|
|
20
|
+
<circle cx="46" cy="52" r="1.7" fill="#e8f0ff" opacity="0.85"/>
|
|
21
|
+
<circle cx="74" cy="30" r="2.2" fill="#e8f0ff" opacity="0.75"/>
|
|
22
|
+
<circle cx="196" cy="46" r="1.5" fill="#e8f0ff" opacity="0.8"/>
|
|
23
|
+
<circle cx="212" cy="86" r="2" fill="#e8f0ff" opacity="0.7"/>
|
|
24
|
+
<circle cx="34" cy="122" r="1.4" fill="#e8f0ff" opacity="0.65"/>
|
|
25
|
+
<circle cx="222" cy="150" r="1.6" fill="#e8f0ff" opacity="0.6"/>
|
|
26
|
+
<circle cx="64" cy="196" r="1.8" fill="#e8f0ff" opacity="0.55"/>
|
|
27
|
+
<circle cx="188" cy="206" r="1.3" fill="#e8f0ff" opacity="0.5"/>
|
|
28
|
+
</g>
|
|
29
|
+
<path d="M14 190 C 66 132, 132 108, 242 128" fill="none" stroke="url(#mw)" stroke-width="26" stroke-linecap="round" opacity="0.75"/>
|
|
30
|
+
<path d="M14 190 C 66 132, 132 108, 242 128" fill="none" stroke="#e8f0ff" stroke-width="4" stroke-linecap="round" opacity="0.30"/>
|
|
31
|
+
<g fill="none" stroke="url(#frame)" stroke-width="6" stroke-linecap="round">
|
|
32
|
+
<path d="M62 88 V72 H82"/>
|
|
33
|
+
<path d="M174 72 H194 V88"/>
|
|
34
|
+
<path d="M194 176 V192 H174"/>
|
|
35
|
+
<path d="M82 192 H62 V176"/>
|
|
36
|
+
</g>
|
|
37
|
+
<g fill="url(#frame)">
|
|
38
|
+
<circle cx="128" cy="132" r="7"/>
|
|
39
|
+
</g>
|
|
40
|
+
<g fill="none" stroke="url(#frame)" stroke-width="2.4" opacity="0.7">
|
|
41
|
+
<circle cx="128" cy="132" r="18"/>
|
|
42
|
+
</g>
|
|
43
|
+
</svg>
|
package/lib/api.js
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-astro-plan — HTTP API。
|
|
3
|
+
*
|
|
4
|
+
* 前缀 /astro-plan:
|
|
5
|
+
* GET /api/meta 元数据(事件类型/区域/省份/站点/目标/天气覆盖)
|
|
6
|
+
* GET /api/search?... 与工具同参的检索
|
|
7
|
+
* GET /api/night?site=&date= 单站单夜时间轴
|
|
8
|
+
* GET /astro-plan/ 自包含页面
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { meta, search, timeline } from './planner.js'
|
|
12
|
+
import { page } from './webui.js'
|
|
13
|
+
|
|
14
|
+
function sendJson(res, status, payload) {
|
|
15
|
+
const body = JSON.stringify(payload)
|
|
16
|
+
res.writeHead(status, {
|
|
17
|
+
'content-type': 'application/json; charset=utf-8',
|
|
18
|
+
'cache-control': 'no-store',
|
|
19
|
+
'content-length': Buffer.byteLength(body),
|
|
20
|
+
})
|
|
21
|
+
res.end(body)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function sendHtml(res, status, html) {
|
|
25
|
+
res.writeHead(status, {
|
|
26
|
+
'content-type': 'text/html; charset=utf-8',
|
|
27
|
+
'cache-control': 'no-store',
|
|
28
|
+
'content-length': Buffer.byteLength(html),
|
|
29
|
+
})
|
|
30
|
+
res.end(html)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function queryToOptions(url) {
|
|
34
|
+
const q = url.searchParams
|
|
35
|
+
const opt = {}
|
|
36
|
+
for (const key of ['action', 'eventType', 'location', 'region', 'province', 'dateStart', 'dateEnd',
|
|
37
|
+
'timeFrom', 'timeTo', 'format']) {
|
|
38
|
+
if (q.get(key)) opt[key] = q.get(key)
|
|
39
|
+
}
|
|
40
|
+
for (const key of ['minAltitude', 'maxBortle', 'limit', 'page', 'pageSize', 'siteLimit', 'maxDriveHours', 'originLat', 'originLon']) {
|
|
41
|
+
if (q.get(key) !== null && q.get(key) !== '') opt[key] = Number(q.get(key))
|
|
42
|
+
}
|
|
43
|
+
if (q.get('tracker') !== null) opt.tracker = q.get('tracker') === 'true' || q.get('tracker') === '1'
|
|
44
|
+
// 页面需要「画面模拟」数据:默认开启,?withSim=0 可关(工具直连 search 时默认不算)
|
|
45
|
+
opt.withSim = !(q.get('withSim') === '0' || q.get('withSim') === 'false')
|
|
46
|
+
return opt
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function installApi(ctx) {
|
|
50
|
+
const handler = async (req, res) => {
|
|
51
|
+
const url = new URL(req.url ?? '/', 'http://localhost')
|
|
52
|
+
const path = url.pathname
|
|
53
|
+
try {
|
|
54
|
+
if (path === '/astro-plan' || path === '/astro-plan/' || path === '/astro-plan/index.html') {
|
|
55
|
+
sendHtml(res, 200, page())
|
|
56
|
+
return
|
|
57
|
+
}
|
|
58
|
+
if (req.method === 'GET' && path === '/astro-plan/api/meta') {
|
|
59
|
+
sendJson(res, 200, { ok: true, ...meta() })
|
|
60
|
+
return
|
|
61
|
+
}
|
|
62
|
+
if (req.method === 'GET' && path === '/astro-plan/api/search') {
|
|
63
|
+
const result = await search(queryToOptions(url))
|
|
64
|
+
sendJson(res, result.ok ? 200 : 400, result)
|
|
65
|
+
return
|
|
66
|
+
}
|
|
67
|
+
if (req.method === 'GET' && path === '/astro-plan/api/night') {
|
|
68
|
+
const site = url.searchParams.get('site')
|
|
69
|
+
const date = url.searchParams.get('date')
|
|
70
|
+
if (!site || !date) {
|
|
71
|
+
sendJson(res, 400, { ok: false, error: 'bad_request', message: '需要 site 与 date 参数' })
|
|
72
|
+
return
|
|
73
|
+
}
|
|
74
|
+
const result = timeline(site, date)
|
|
75
|
+
sendJson(res, result.ok ? 200 : 404, result)
|
|
76
|
+
return
|
|
77
|
+
}
|
|
78
|
+
sendJson(res, 404, { ok: false, error: 'not_found', message: `未知路径 ${path}` })
|
|
79
|
+
} catch (error) {
|
|
80
|
+
sendJson(res, 500, { ok: false, error: 'internal', message: String(error?.message ?? error) })
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return ctx.webServer.register({ kind: 'prefix', path: '/astro-plan', handler })
|
|
84
|
+
}
|
package/lib/astro.js
ADDED
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-astro-plan — 天文计算适配层。
|
|
3
|
+
*
|
|
4
|
+
* 底层星历使用 vendored 的 astronomy-engine v2.1.19(MIT,见 lib/vendor)。
|
|
5
|
+
* 本层只做三件事:统一时区(中国 UTC+8)、统一站点观测者、把「地平坐标 /
|
|
6
|
+
* 升落 / 暮光 / 月相」包装成规划器好用的形状。
|
|
7
|
+
*
|
|
8
|
+
* 精度约定(与 astropy 基准对照,见 tests/astro.test.js):
|
|
9
|
+
* - 太阳/行星高度方位:≤0.05°
|
|
10
|
+
* - 月亮(含视差):≤0.4°(受月球理论差异限制,规划足够)
|
|
11
|
+
* - 暮光 / 月升落:≤2 min
|
|
12
|
+
* - 月照度:≤2%
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import * as A from './vendor/astronomy-engine.js'
|
|
16
|
+
|
|
17
|
+
const D2R = Math.PI / 180
|
|
18
|
+
const R2D = 180 / Math.PI
|
|
19
|
+
export const TZ_OFFSET_HOURS = 8
|
|
20
|
+
|
|
21
|
+
export { A }
|
|
22
|
+
|
|
23
|
+
/** Date | ISO 字符串 → AstroTime。 */
|
|
24
|
+
export function makeTime(value) {
|
|
25
|
+
const date = value instanceof Date ? value : new Date(value)
|
|
26
|
+
if (Number.isNaN(date.getTime())) throw new Error(`invalid date: ${value}`)
|
|
27
|
+
return A.MakeTime(date)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** 站点 → astronomy-engine Observer。 */
|
|
31
|
+
export function observerOf(site) {
|
|
32
|
+
return new A.Observer(site.lat, site.lon, site.altM ?? 0)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** 把 "YYYY-MM-DD" + "HH:MM" 解析成 UTC+8 的 Date。 */
|
|
36
|
+
export function localDate(dateStr, hhmm = '00:00') {
|
|
37
|
+
const [y, m, d] = String(dateStr).split('-').map(Number)
|
|
38
|
+
const [hh, mm] = String(hhmm).split(':').map(Number)
|
|
39
|
+
return new Date(Date.UTC(y, m - 1, d, hh - TZ_OFFSET_HOURS, mm || 0))
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Date → UTC+8 本地 "YYYY-MM-DD"。 */
|
|
43
|
+
export function localDateString(date) {
|
|
44
|
+
const t = new Date(date.getTime() + TZ_OFFSET_HOURS * 3600 * 1000)
|
|
45
|
+
return t.toISOString().slice(0, 10)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Date → UTC+8 本地 "HH:MM"。 */
|
|
49
|
+
export function localHHMM(date) {
|
|
50
|
+
const t = new Date(date.getTime() + TZ_OFFSET_HOURS * 3600 * 1000)
|
|
51
|
+
return t.toISOString().slice(11, 16)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Date → UTC+8 本地 "YYYY-MM-DD HH:MM"(跨零点的观测窗口用这个才不歧义)。 */
|
|
55
|
+
export function localDateTime(date) {
|
|
56
|
+
const t = new Date(date.getTime() + TZ_OFFSET_HOURS * 3600 * 1000)
|
|
57
|
+
return `${t.toISOString().slice(0, 10)} ${t.toISOString().slice(11, 16)}`
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Date → UTC+8 本地 "MM-DD HH:MM"(表格里用短格式,省宽度)。 */
|
|
61
|
+
export function localShortDateTime(date) {
|
|
62
|
+
const t = new Date(date.getTime() + TZ_OFFSET_HOURS * 3600 * 1000)
|
|
63
|
+
return `${t.toISOString().slice(5, 10)} ${t.toISOString().slice(11, 16)}`
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** 地方恒星时(小时,0–24),lon 为东经度。 */
|
|
67
|
+
export function lstHours(time, lonDeg) {
|
|
68
|
+
const gast = A.SiderealTime(time) // 小时
|
|
69
|
+
return ((gast + lonDeg / 15) % 24 + 24) % 24
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** 大气折射(度),地平线以上才加,避免地平线下出现假抬升。 */
|
|
73
|
+
function refracted(altDeg) {
|
|
74
|
+
if (altDeg < -1) return altDeg
|
|
75
|
+
return altDeg + A.Refraction('normal', altDeg)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* 时角法高度方位(快,供固定恒星批量计算)。
|
|
80
|
+
* @param {number} raDeg 赤经(J2000,度)
|
|
81
|
+
* @param {number} decDeg 赤纬(度)
|
|
82
|
+
* @param {number} lstH 地方恒星时(小时)
|
|
83
|
+
* @param {number} latDeg 站点纬度
|
|
84
|
+
*/
|
|
85
|
+
export function altAzFromHA(raDeg, decDeg, lstH, latDeg) {
|
|
86
|
+
const H = ((((lstH * 15 - raDeg) % 360) + 540) % 360 - 180) * D2R
|
|
87
|
+
const phi = latDeg * D2R
|
|
88
|
+
const dec = decDeg * D2R
|
|
89
|
+
const sinAlt = Math.sin(phi) * Math.sin(dec) + Math.cos(phi) * Math.cos(dec) * Math.cos(H)
|
|
90
|
+
const altGeo = Math.asin(Math.max(-1, Math.min(1, sinAlt)))
|
|
91
|
+
// 方位必须用「几何高度」求,折射只影响高度,否则会引入约 0.5° 的方位偏差
|
|
92
|
+
const cosA = (Math.sin(dec) - Math.sin(altGeo) * Math.sin(phi)) / (Math.cos(altGeo) * Math.cos(phi) || 1e-9)
|
|
93
|
+
let az = Math.acos(Math.max(-1, Math.min(1, cosA)))
|
|
94
|
+
if (Math.sin(H) > 0) az = 2 * Math.PI - az
|
|
95
|
+
const altDeg = altGeo * R2D
|
|
96
|
+
return { alt: altDeg + (altDeg > -1 ? A.Refraction('normal', altDeg) : 0), az: (az * R2D + 360) % 360 }
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** J2000 星表坐标 → 当日赤道坐标(岁差),用于时角快速算法。 */
|
|
100
|
+
export function ofDateRADec(raDeg, decDeg, time) {
|
|
101
|
+
const v = {
|
|
102
|
+
x: Math.cos(decDeg * D2R) * Math.cos(raDeg * D2R),
|
|
103
|
+
y: Math.cos(decDeg * D2R) * Math.sin(raDeg * D2R),
|
|
104
|
+
z: Math.sin(decDeg * D2R),
|
|
105
|
+
t: time,
|
|
106
|
+
}
|
|
107
|
+
const eqd = A.RotateVector(A.Rotation_EQJ_EQD(time), v)
|
|
108
|
+
const sph = A.EquatorFromVector(eqd)
|
|
109
|
+
return { raDeg: sph.ra * 15, dec: sph.dec }
|
|
110
|
+
}
|
|
111
|
+
/** 固定恒星(J2000 坐标)→ 地平坐标,矩阵法(更准,单点用)。 */
|
|
112
|
+
export function starAltAz(raDeg, decDeg, time, site) {
|
|
113
|
+
const obs = observerOf(site)
|
|
114
|
+
const v = { x: Math.cos(decDeg * D2R) * Math.cos(raDeg * D2R), y: Math.cos(decDeg * D2R) * Math.sin(raDeg * D2R), z: Math.sin(decDeg * D2R) }
|
|
115
|
+
const hor = A.RotateVector(A.Rotation_EQJ_HOR(time, obs), v)
|
|
116
|
+
const sph = A.HorizonFromVector(hor, null)
|
|
117
|
+
return { alt: refracted(sph.lat), az: (sph.lon + 360) % 360 }
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** 太阳/月亮/行星 → 地平坐标(含视差与折射)。 */
|
|
121
|
+
export function bodyAltAz(bodyName, time, site) {
|
|
122
|
+
const body = A.Body[bodyName]
|
|
123
|
+
if (body === undefined) throw new Error(`unknown body: ${bodyName}`)
|
|
124
|
+
const obs = observerOf(site)
|
|
125
|
+
const v = A.GeoVector(body, time, true)
|
|
126
|
+
const ov = A.ObserverVector(time, obs, false)
|
|
127
|
+
const topo = { x: v.x - ov.x, y: v.y - ov.y, z: v.z - ov.z, t: v.t }
|
|
128
|
+
const hor = A.RotateVector(A.Rotation_EQJ_HOR(time, obs), topo)
|
|
129
|
+
const sph = A.HorizonFromVector(hor, null)
|
|
130
|
+
return { alt: refracted(sph.lat), az: (sph.lon + 360) % 360 }
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* 任意「地心赤道 J2000 矢量」→ 地平坐标(含视差与折射)。
|
|
135
|
+
* 彗星等没有内建星历的天体走这里,与 bodyAltAz 完全同一条几何链路。
|
|
136
|
+
*/
|
|
137
|
+
export function vectorAltAz(vec, time, site) {
|
|
138
|
+
const obs = observerOf(site)
|
|
139
|
+
const ov = A.ObserverVector(time, obs, false)
|
|
140
|
+
const topo = { x: vec.x - ov.x, y: vec.y - ov.y, z: vec.z - ov.z, t: vec.t ?? time }
|
|
141
|
+
const hor = A.RotateVector(A.Rotation_EQJ_HOR(time, obs), topo)
|
|
142
|
+
const sph = A.HorizonFromVector(hor, null)
|
|
143
|
+
return { alt: refracted(sph.lat), az: (sph.lon + 360) % 360 }
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** 目标(星表或行星)→ 地平坐标,统一入口。 */
|
|
147
|
+
export function targetAltAz(target, time, site) {
|
|
148
|
+
if (target.body) return bodyAltAz(target.body, time, site)
|
|
149
|
+
return starAltAz(target.raH * 15, target.dec, time, site)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** 下一次升/落(direction: +1 升, -1 落),返回 Date 或 null。 */
|
|
153
|
+
export function riseSet(bodyName, time, site, direction, limitDays = 2) {
|
|
154
|
+
const body = A.Body[bodyName]
|
|
155
|
+
if (body === undefined) throw new Error(`unknown body: ${bodyName}`)
|
|
156
|
+
const found = A.SearchRiseSet(body, observerOf(site), direction, time, limitDays, 0)
|
|
157
|
+
return found ? found.date : null
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** 太阳高度 → 下一次穿越目标高度的时刻。 */
|
|
161
|
+
export function searchSunAltitude(time, site, altitudeDeg, direction, limitDays = 1) {
|
|
162
|
+
const found = A.SearchAltitude(A.Body.Sun, observerOf(site), direction, time, limitDays, altitudeDeg)
|
|
163
|
+
return found ? found.date : null
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** 月相角(0=新月, 180=满月)。 */
|
|
167
|
+
export function moonPhaseAngle(time) {
|
|
168
|
+
return A.MoonPhase(time)
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** 月面照度百分比。 */
|
|
172
|
+
export function moonIllumPercent(time) {
|
|
173
|
+
const angle = A.MoonPhase(time)
|
|
174
|
+
return ((1 - Math.cos(angle * D2R)) / 2) * 100
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** 月相名(中文)。 */
|
|
178
|
+
export function moonPhaseName(illum, waxing) {
|
|
179
|
+
if (illum < 1.5) return '新月'
|
|
180
|
+
if (illum < 12) return waxing ? '娥眉月' : '残月'
|
|
181
|
+
if (illum < 45) return waxing ? '上弦前' : '下弦后'
|
|
182
|
+
if (illum < 55) return '弦月'
|
|
183
|
+
if (illum < 88) return waxing ? '盈凸月' : '亏凸月'
|
|
184
|
+
if (illum < 98.5) return '近满月'
|
|
185
|
+
return '满月'
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** 两个地平坐标的角距(度)。 */
|
|
189
|
+
export function separation(alt1, az1, alt2, az2) {
|
|
190
|
+
const a1 = alt1 * D2R; const a2 = alt2 * D2R
|
|
191
|
+
const dz = (az1 - az2) * D2R
|
|
192
|
+
const c = Math.sin(a1) * Math.sin(a2) + Math.cos(a1) * Math.cos(a2) * Math.cos(dz)
|
|
193
|
+
return Math.acos(Math.max(-1, Math.min(1, c))) * R2D
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** 罗盘方位名。 */
|
|
197
|
+
export function azName(az) {
|
|
198
|
+
const names = ['北', '东北', '东', '东南', '南', '西南', '西', '西北']
|
|
199
|
+
return names[Math.round(((az % 360) + 360) % 360 / 45) % 8]
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* 一"夜"的上下文:夜定义为 D 日 15:00 → D+1 09:00(UTC+8)。
|
|
204
|
+
* @param {object} site 站点
|
|
205
|
+
* @param {string} dateStr "YYYY-MM-DD"
|
|
206
|
+
* @param {{fast?: boolean}} [options] fast=true 时只算暮光与月相(检索路径用,
|
|
207
|
+
* 省掉 4 次太阳升落搜索与 2 次月升落搜索,长跨度检索可快近一倍)
|
|
208
|
+
*/
|
|
209
|
+
export function nightContext(site, dateStr, options = {}) {
|
|
210
|
+
const fast = options.fast === true
|
|
211
|
+
const start = makeTime(localDate(dateStr, '15:00'))
|
|
212
|
+
const endDate = localDate(dateStr, '09:00')
|
|
213
|
+
endDate.setUTCDate(endDate.getUTCDate() + 1)
|
|
214
|
+
const end = makeTime(endDate)
|
|
215
|
+
|
|
216
|
+
const sunset = fast ? null : searchSunAltitude(start, site, -0.833, -1)
|
|
217
|
+
const dusk = searchSunAltitude(start, site, -18, -1)
|
|
218
|
+
const dawn = dusk ? searchSunAltitude(makeTime(dusk), site, -18, +1) : null
|
|
219
|
+
const sunrise = fast || !dawn ? null : searchSunAltitude(makeTime(dawn), site, -0.833, +1)
|
|
220
|
+
|
|
221
|
+
const moonrise = fast ? null : riseSet('Moon', start, site, +1)
|
|
222
|
+
const moonset = fast ? null : riseSet('Moon', start, site, -1)
|
|
223
|
+
const noon = makeTime(localDate(dateStr, '22:00'))
|
|
224
|
+
const illum = moonIllumPercent(noon)
|
|
225
|
+
const phaseAngle = moonPhaseAngle(noon)
|
|
226
|
+
const waxing = phaseAngle < 180
|
|
227
|
+
|
|
228
|
+
return {
|
|
229
|
+
date: dateStr,
|
|
230
|
+
start: start.date,
|
|
231
|
+
end: end.date,
|
|
232
|
+
sunset: sunset ? sunset : null,
|
|
233
|
+
dusk: dusk ? dusk : null,
|
|
234
|
+
dawn: dawn ? dawn : null,
|
|
235
|
+
sunrise: sunrise ? sunrise : null,
|
|
236
|
+
moonrise: moonrise && moonrise <= end.date ? moonrise : null,
|
|
237
|
+
moonset: moonset && moonset <= end.date ? moonset : null,
|
|
238
|
+
moonIllum: illum,
|
|
239
|
+
moonPhase: moonPhaseName(illum, waxing),
|
|
240
|
+
waxing,
|
|
241
|
+
}
|
|
242
|
+
}
|
package/lib/aurora.js
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-astro-plan — 极光可见性判据。
|
|
3
|
+
*
|
|
4
|
+
* 国内只有北纬 42° 以上的站点在强地磁暴时可能看到极光,仅靠「季节 + 站点纬度」
|
|
5
|
+
* 会给出永远可拍的假结果(此前每次查询都固定 3 条)。这里用两件事判可见性:
|
|
6
|
+
* 1. 站点地磁纬度(偶极近似,地磁北极 80.65°N / 72.68°W);
|
|
7
|
+
* 2. NOAA SWPC 的 Kp 预报(3 小时分辨率,含观测 + 未来约 3 天预测)。
|
|
8
|
+
* Kp 达不到所需阈值 → 该站该夜不产生结果;预报没覆盖到的日期 → 不产生结果,
|
|
9
|
+
* 由 planner 给出「超出 Kp 预报范围」的说明。
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const KP_URL = 'https://services.swpc.noaa.gov/products/noaa-planetary-k-index-forecast.json'
|
|
13
|
+
const CACHE_TTL_MS = 30 * 60 * 1000
|
|
14
|
+
const D2R = Math.PI / 180
|
|
15
|
+
const R2D = 180 / Math.PI
|
|
16
|
+
|
|
17
|
+
/** 地磁北极(IGRF 近似,2020 年代)。 */
|
|
18
|
+
const MAG_POLE = { lat: 80.65, lon: -72.68 }
|
|
19
|
+
|
|
20
|
+
let cache = { at: 0, data: null }
|
|
21
|
+
|
|
22
|
+
/** 站点纬经度 → 地磁纬度(度)。 */
|
|
23
|
+
export function geomagneticLatitude(lat, lon) {
|
|
24
|
+
const p = MAG_POLE.lat * D2R
|
|
25
|
+
const l0 = MAG_POLE.lon * D2R
|
|
26
|
+
const phi = lat * D2R
|
|
27
|
+
const lambda = lon * D2R
|
|
28
|
+
const sinM = Math.sin(phi) * Math.sin(p) + Math.cos(phi) * Math.cos(p) * Math.cos(lambda - l0)
|
|
29
|
+
return Math.asin(Math.max(-1, Math.min(1, sinM))) * R2D
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* 该站看到极光所需的最低 Kp。
|
|
34
|
+
* 经验式:极光卵赤道侧边界随 Kp 下降约 3°/级,Kp 0 ≈ 66.5° 地磁纬度。
|
|
35
|
+
* 漠河(地磁纬度 ~46°)需要 Kp ≥ 6.8;长白山(~40°)需要 Kp ≥ 8.8。
|
|
36
|
+
*/
|
|
37
|
+
export function requiredKp(magLat) {
|
|
38
|
+
return Math.max(0, Math.min(9, (66.5 - magLat) / 3))
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function parseRows(rows) {
|
|
42
|
+
const out = []
|
|
43
|
+
for (const row of rows) {
|
|
44
|
+
const iso = String(row.time_tag ?? '').replace(' ', 'T')
|
|
45
|
+
const kp = Number(row.kp)
|
|
46
|
+
if (!iso || !Number.isFinite(kp)) continue
|
|
47
|
+
const time = new Date(`${iso}Z`)
|
|
48
|
+
if (Number.isNaN(time.getTime())) continue
|
|
49
|
+
out.push({ time, kp })
|
|
50
|
+
}
|
|
51
|
+
return out.sort((a, b) => a.time - b.time)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* 取 Kp 序列(30 分钟缓存)。失败返回 null,调用方据此判定「无法确认」。
|
|
56
|
+
*/
|
|
57
|
+
export async function kpForecast() {
|
|
58
|
+
if (cache.data && Date.now() - cache.at < CACHE_TTL_MS) return cache.data
|
|
59
|
+
try {
|
|
60
|
+
const controller = new AbortController()
|
|
61
|
+
const timer = setTimeout(() => controller.abort(), 12000)
|
|
62
|
+
const res = await fetch(KP_URL, { signal: controller.signal })
|
|
63
|
+
clearTimeout(timer)
|
|
64
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`)
|
|
65
|
+
const rows = await res.json()
|
|
66
|
+
const data = parseRows(Array.isArray(rows) ? rows : [])
|
|
67
|
+
if (data.length === 0) throw new Error('empty kp payload')
|
|
68
|
+
cache = { at: Date.now(), data }
|
|
69
|
+
return data
|
|
70
|
+
} catch (error) {
|
|
71
|
+
return null
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* 每个「夜」(D 15:00 → D+1 09:00,UTC+8)的 Kp 峰值。
|
|
77
|
+
* 对应 UTC 窗口 D 07:00 → D+1 01:00。
|
|
78
|
+
* @returns {Map<string, number>}
|
|
79
|
+
*/
|
|
80
|
+
export function kpByNight(forecast) {
|
|
81
|
+
const map = new Map()
|
|
82
|
+
if (!forecast) return map
|
|
83
|
+
for (const { time, kp } of forecast) {
|
|
84
|
+
// 夜 D(UTC+8 15:00 → 次日 09:00)= UTC D 07:00 → D+1 01:00
|
|
85
|
+
const utcHour = time.getUTCHours()
|
|
86
|
+
let dateKey
|
|
87
|
+
if (utcHour >= 7) dateKey = time.toISOString().slice(0, 10)
|
|
88
|
+
else if (utcHour <= 1) dateKey = new Date(time.getTime() - 86400000).toISOString().slice(0, 10)
|
|
89
|
+
else continue
|
|
90
|
+
const prev = map.get(dateKey)
|
|
91
|
+
if (prev === undefined || kp > prev) map.set(dateKey, kp)
|
|
92
|
+
}
|
|
93
|
+
return map
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** 预报覆盖的日期范围(给提示用)。 */
|
|
97
|
+
export function kpCoverage(forecast) {
|
|
98
|
+
if (!forecast || forecast.length === 0) return { from: null, to: null }
|
|
99
|
+
const first = forecast[0].time.toISOString().slice(0, 10)
|
|
100
|
+
const last = forecast[forecast.length - 1].time.toISOString().slice(0, 10)
|
|
101
|
+
return { from: first, to: last }
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function clearAuroraCache() {
|
|
105
|
+
cache = { at: 0, data: null }
|
|
106
|
+
}
|