metweave 0.1.1 → 0.2.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/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # metweave
2
2
 
3
- 把公开气象数据编织成可嵌入产品的图:机场报文(METAR / SPECI)经「解析 → 标准化 → 渲染」一条管道,变成报文卡片与地图图层。数据直连公开源(IEM currents 端点 CORS 全开,浏览器可直连);解析与渲染全部在浏览器端完成——报文不经过你自己的服务器,无需自建后端。
3
+ 把公开气象数据编织成可嵌入产品的图:机场报文(METAR / SPECI 观测与 TAF 预报)经「解析 → 标准化 → 渲染」一条管道,变成报文卡片与地图图层。数据直连公开源(IEM 实况端点 CORS 全开,浏览器可直连;TAF 上游无 CORS 头,需代理或镜像——见下);解析与渲染全部在浏览器端完成——报文不经过你自己的服务器,无需自建后端。
4
4
 
5
5
  ### 安装
6
6
 
@@ -16,6 +16,7 @@ npm install metweave @metweave/leaflet leaflet
16
16
  ```ts
17
17
  import * as L from "leaflet";
18
18
  import "leaflet/dist/leaflet.css"; // 别漏:不引入 CSS 地图不渲染
19
+ import { CN_STATIONS } from "metweave/stations-cn";
19
20
  import { getMetarReports } from "metweave/sources"; // 取数 → 解析 → 定位
20
21
  import { addMetarLayer } from "@metweave/leaflet";
21
22
 
@@ -28,7 +29,21 @@ L.tileLayer(
28
29
  await addMetarLayer(map, await getMetarReports(), { conditionColors: true });
29
30
  ```
30
31
 
31
- 本包再导出 `@metweave/core` / `parser` / `render`(整条管道一个入口,含 `toValues` 与机读错误类);`metweave/sources` 提供取数 helper。文档与完整示例见[主仓库](https://github.com/yaojaro/metweave)。
32
+ ### TAF 预报侧(同样三步)
33
+
34
+ ```ts
35
+ import { getTafReports } from "metweave/sources";
36
+ import { addTafLayer } from "@metweave/leaflet";
37
+
38
+ // TAF 端点不提供坐标——stations 联表是定位的唯一来源(中国 39 站元数据随包自带)
39
+ const tafItems = await getTafReports(["ZBAA", "ZBAD"], {
40
+ baseUrl: "/aw-taf", // 上游无 CORS 头:浏览器直连走自建代理/镜像(根因与三条出路见主仓库 README「TAF 取数与 CORS」)
41
+ stations: CN_STATIONS,
42
+ });
43
+ const tafLayer = await addTafLayer(map, tafItems); // 圆点四档色/悬停摘要/TAF 卡弹窗,与实况层同款
44
+ ```
45
+
46
+ 本包再导出 `@metweave/core` / `parser` / `render`(整条管道一个入口,含 `toValues` 与机读错误类):预报侧 `parseTaf`(tolerant 与 `mode: "strict"` 严判)、`expandTaf` 时间线展开、`tafSegments` 分段明细、`validateTaf` 条文判据校验、`renderTafCard` TAF 卡片。`metweave/sources` 提供双线取数 helper(`getMetars` / `getMetarReports` 与 `getTafs` / `getTafReports`,各带 `baseUrl` 换源位);`metweave/stations-cn` 提供中国 39 站元数据。文档与完整示例见[主仓库](https://github.com/yaojaro/metweave)。
32
47
 
33
48
  ## 许可
34
49
 
package/dist/sources.cjs CHANGED
@@ -7,9 +7,10 @@ const IEM_BASE = "https://mesonet.agron.iastate.edu";
7
7
  * The IEM whole-network currents endpoint (CORS-open, direct browser access). Defaults to the China ASOS network (39 stations).
8
8
  * IEM 整网实况端点(CORS 全开,浏览器可直连)。缺省中国 ASOS 网(39 站)。
9
9
  * @param network - IEM network name (e.g. CN__ASOS, RU__ASOS, IA_ASOS). IEM 网络名。
10
+ * @param baseUrl - IEM 站点根覆盖(内网镜像/自建网关——换源口径,见 GetMetarsOptions.baseUrl)。
10
11
  */
11
- function iemCurrentsUrl(network = "CN__ASOS") {
12
- return `${IEM_BASE}/api/1/currents.json?network=${encodeURIComponent(network)}`;
12
+ function iemCurrentsUrl(network = "CN__ASOS", baseUrl = IEM_BASE) {
13
+ return `${baseUrl}/api/1/currents.json?network=${encodeURIComponent(network)}`;
13
14
  }
14
15
  /** 观测记录守卫:station/raw 均须为字符串(只查键不查类型是旧洞)。 */
15
16
  function isObservation(v) {
@@ -39,7 +40,7 @@ async function getMetars(network = "CN__ASOS", options = {}) {
39
40
  try {
40
41
  let res;
41
42
  try {
42
- res = await fetch(iemCurrentsUrl(network), { signal: controller.signal });
43
+ res = await fetch(iemCurrentsUrl(network, options.baseUrl), { signal: controller.signal });
43
44
  } catch (err) {
44
45
  if (controller.signal.aborted) throw err;
45
46
  const reason = err instanceof Error ? err.message : String(err);
@@ -111,7 +112,230 @@ async function getMetarReports(network = "CN__ASOS", options = {}) {
111
112
  }
112
113
  return items;
113
114
  }
115
+ /** aviationweather TAF 数据端点(上游无 CORS 头:浏览器直连须自建代理/镜像——baseUrl 覆盖即为此用) */
116
+ const AW_TAF_ENDPOINT = "https://aviationweather.gov/api/data/taf";
117
+ /**
118
+ * The aviationweather TAF endpoint (raw format). ids joined as given; date serialized when present.
119
+ * aviationweather TAF 端点模板(format=raw)。ids 数组逗号连接;date 存在时序列化为查询参数。
120
+ */
121
+ function awTafUrl(ids, options = {}) {
122
+ const idList = typeof ids === "string" ? ids : ids.join(",");
123
+ const params = new URLSearchParams({
124
+ ids: idList,
125
+ format: "raw"
126
+ });
127
+ const d = options.date;
128
+ if (d !== void 0) params.set("date", d instanceof Date ? d.toISOString() : d);
129
+ return `${options.baseUrl ?? "https://aviationweather.gov/api/data/taf"}?${params.toString()}`;
130
+ }
131
+ /** raw 文本 → 整份报文行(缩进续行并回上一份;与报池语义同源)+ best-effort 站码提取 */
132
+ function mergeTafLines(text) {
133
+ const merged = [];
134
+ for (const line of text.split("\n")) {
135
+ if (line.trim() === "") continue;
136
+ if (/^\s/.test(line) && merged.length > 0) merged[merged.length - 1] += ` ${line.trim()}`;
137
+ else merged.push(line.trim());
138
+ }
139
+ return merged.map((raw) => {
140
+ const toks = raw.split(/\s+/);
141
+ let k = toks[0] === "TAF" ? 1 : 0;
142
+ while (toks[k] === "AMD" || toks[k] === "COR") k += 1;
143
+ const head = toks[k] ?? "";
144
+ return {
145
+ station: /^[A-Z0-9]{4}$/.test(head) ? head : "",
146
+ raw
147
+ };
148
+ });
149
+ }
150
+ /**
151
+ * Fetch aviationweather TAFs (raw format) with station extracted and continuation lines merged.
152
+ * 拉取 aviationweather TAF(raw 格式):续行归并 + 站码提取的取数侧整理。
153
+ * 失败语义与 getMetars 同族(不静默):timeout/network/http-error/empty-data 一律 MetarSourceError
154
+ * 机读码;上游对未知 ids 也返回 200 + 空体,空数据显式 throw 而非空数组静默。
155
+ */
156
+ async function getTafs(ids, options = {}) {
157
+ const controller = new AbortController();
158
+ const external = options.signal;
159
+ const forward = () => {
160
+ controller.abort(external?.reason);
161
+ };
162
+ if (external !== void 0) {
163
+ if (external.aborted) forward();
164
+ else external.addEventListener("abort", forward, { once: true });
165
+ }
166
+ const timer = options.timeoutMs === void 0 ? void 0 : setTimeout(() => controller.abort(new _metweave_core.MetarSourceError("timeout", "aviationweather", `aviationweather TAF 请求超时(>${options.timeoutMs}ms)`)), options.timeoutMs);
167
+ try {
168
+ let res;
169
+ try {
170
+ res = await fetch(awTafUrl(ids, options), { signal: controller.signal });
171
+ } catch (err) {
172
+ if (controller.signal.aborted) throw err;
173
+ const reason = err instanceof Error ? err.message : String(err);
174
+ throw new _metweave_core.MetarSourceError("network", "aviationweather", `网络请求失败(源:aviationweather TAF):请检查网络连通性后重试(${reason})`, { cause: err });
175
+ }
176
+ if (!res.ok) throw new _metweave_core.MetarSourceError("http-error", "aviationweather", `aviationweather TAF HTTP ${res.status}`);
177
+ let text;
178
+ try {
179
+ text = await res.text();
180
+ } catch (err) {
181
+ if (controller.signal.aborted) throw err;
182
+ const reason = err instanceof Error ? err.message : String(err);
183
+ throw new _metweave_core.MetarSourceError("network", "aviationweather", `aviationweather TAF 响应读取失败:${reason}`, { cause: err });
184
+ }
185
+ const rows = mergeTafLines(text);
186
+ if (rows.length === 0) throw new _metweave_core.MetarSourceError("empty-data", "aviationweather", "aviationweather TAF 返回空数据——请核对 ids 站码(如 ZBAA,ZBAD;端点 format=raw)");
187
+ return rows;
188
+ } finally {
189
+ if (timer !== void 0) clearTimeout(timer);
190
+ external?.removeEventListener("abort", forward);
191
+ }
192
+ }
193
+ /**
194
+ * TAF 版的取数 → 解析 → 定位一步到位(契约沿 getMetarReports):stations 命中的站带精确坐标与站名,
195
+ * 未命中跳过(TAF 端点无坐标可回落,与 getMetarReports 的无坐标跳行同规则);解析失败两种模式
196
+ * (缺省聚合抛出 batch-parse-failed、传 onUnparseable 逐行容错)。报池等数据管理逻辑不进本层。
197
+ */
198
+ async function getTafReports(ids, options = {}) {
199
+ const { stations, onUnparseable, spans, ...fetchOptions } = options;
200
+ const table = new Map((stations ?? []).map((s) => [s.icao, s]));
201
+ const observations = await getTafs(ids, fetchOptions);
202
+ const items = [];
203
+ const failures = [];
204
+ for (const obs of observations) try {
205
+ const report = (0, _metweave_parser.parseTaf)(obs.raw, spans === false ? { spans: false } : void 0);
206
+ const station = table.get(report.station);
207
+ if (station === void 0) continue;
208
+ items.push({
209
+ report,
210
+ position: [station.lat, station.lon],
211
+ title: `${report.station}${station.name ? ` ${station.name}` : ""}`
212
+ });
213
+ } catch (err) {
214
+ if (!(err instanceof _metweave_core.MetarParseError)) throw err;
215
+ if (onUnparseable !== void 0) {
216
+ onUnparseable({
217
+ station: obs.station,
218
+ raw: obs.raw,
219
+ code: err.code,
220
+ error: err.message
221
+ });
222
+ continue;
223
+ }
224
+ failures.push(`${obs.station}: ${err.message}`);
225
+ }
226
+ if (failures.length > 0) {
227
+ const summary = `source=aviationweather; failures=${failures.length}; ${failures.join("; ")}`;
228
+ throw new _metweave_core.MetarParseError("batch-parse-failed", summary, `TAF 解析失败 ${failures.length} 条(aviationweather)——${failures.slice(0, 3).join(";")}${failures.length > 3 ? "……" : ""}`);
229
+ }
230
+ return items;
231
+ }
232
+ /** ogimet display_metars2.php 端点(tipo=FT=TAF;无 CORS 头:浏览器直连须代理/镜像——baseUrl 覆盖即为此用) */
233
+ const OGIMET_TAF_ENDPOINT = "https://www.ogimet.com/display_metars2.php";
234
+ /**
235
+ * ogimet TAF 端点模板(配方=私有管线 backfill_ogimet.py 同源):tipo=FT、txt 形、
236
+ * 服务端按 <pre> 区回 HTML(解析见 parseOgimetTafs)。免费公益服务——调用方自行节制频率。
237
+ */
238
+ /** UTC 日期字段补零组(ogimet 查询串的 ano/mes/day/hora/min 族) */
239
+ const ogimetFields = (d) => ({
240
+ ano: `${d.getUTCFullYear()}`,
241
+ mes: `${d.getUTCMonth() + 1}`.padStart(2, "0"),
242
+ day: `${d.getUTCDate()}`.padStart(2, "0"),
243
+ hora: `${d.getUTCHours()}`.padStart(2, "0"),
244
+ min: `${d.getUTCMinutes()}`.padStart(2, "0")
245
+ });
246
+ function ogimetTafUrl(station, window, options = {}) {
247
+ const a = ogimetFields(window.from);
248
+ const b = ogimetFields(window.to);
249
+ const qs = new URLSearchParams({
250
+ lang: "en",
251
+ lugar: station,
252
+ tipo: "FT",
253
+ ord: "DIR",
254
+ nil: "SI",
255
+ fmt: "txt",
256
+ ano: a.ano,
257
+ mes: a.mes,
258
+ day: a.day,
259
+ hora: a.hora,
260
+ min: a.min,
261
+ anof: b.ano,
262
+ mesf: b.mes,
263
+ dayf: b.day,
264
+ horaf: b.hora,
265
+ minf: b.min,
266
+ send: "send",
267
+ nohtmlo: "yes",
268
+ annot: "yes",
269
+ leng: "0"
270
+ });
271
+ return `${options.baseUrl ?? "https://www.ogimet.com/display_metars2.php"}?${qs.toString()}`;
272
+ }
273
+ /** 电头站码 best-effort 提取(TAF/AMD/COR 剥词;剥不出返回空串)——与 mergeTafLines 同判式(aviationweather 取数线) */
274
+ const stationHeadOf = (raw) => {
275
+ const toks = raw.split(/\s+/);
276
+ let k = toks[0] === "TAF" ? 1 : 0;
277
+ while (toks[k] === "AMD" || toks[k] === "COR") k += 1;
278
+ const head = toks[k] ?? "";
279
+ return /^[A-Z0-9]{4}$/.test(head) ? head : "";
280
+ };
281
+ /**
282
+ * ogimet 响应解析(<pre> 区):12 位时间戳前缀行(YYYYMMDDHHmm,收报序)起新记录、无前缀行并入上一条
283
+ * (多行报文并单——与私有管线 parse_tafs 同协议);# 注释/空行跳过;只收 TAF 起头报文。
284
+ * 站码 best-effort 取自电头(query 站名兜底);空模板(无 <pre> 或零报文)返回空数组——
285
+ * 由调用方判空(ogimet 缺数常见,属正常降级而非错误面)。
286
+ */
287
+ function parseOgimetTafs(html, fallbackStation = "") {
288
+ const pre = /<pre>([\s\S]*?)<\/pre>/.exec(html);
289
+ if (pre === null) return [];
290
+ const PREFIX = /^(\d{12})\s+(.*)$/;
291
+ const merged = [];
292
+ for (const line of (pre[1] ?? "").split("\n")) {
293
+ const t = line.trim();
294
+ if (t === "" || t.startsWith("#")) continue;
295
+ const pm = PREFIX.exec(t);
296
+ if (pm !== null) merged.push(pm[2] ?? "");
297
+ else if (merged.length > 0) merged[merged.length - 1] += ` ${t}`;
298
+ }
299
+ return merged.filter((raw) => raw.startsWith("TAF")).map((raw) => ({
300
+ station: stationHeadOf(raw) || fallbackStation,
301
+ raw
302
+ }));
303
+ }
304
+ /**
305
+ * Fetch one station's recent TAFs from ogimet (latest + previous publication cycles).
306
+ * 拉取单站的 ogimet 近窗 TAF(最新与上一发布周期——报池补充线,供与 aviationweather 线合并去重)。
307
+ * 失败语义与 getTafs 同族(timeout/network/http-error 机读码);空数据返回空数组而非抛错
308
+ * (ogimet 站点级缺数常见——它是补充线,空窗属正常降级,不静默丢整批)。
309
+ */
310
+ async function getTafsOgimet(station, window, options = {}) {
311
+ const controller = new AbortController();
312
+ const external = options.signal;
313
+ const forward = () => {
314
+ controller.abort(external?.reason);
315
+ };
316
+ if (external !== void 0) {
317
+ if (external.aborted) forward();
318
+ else external.addEventListener("abort", forward, { once: true });
319
+ }
320
+ const timer = options.timeoutMs === void 0 ? void 0 : setTimeout(() => controller.abort(new _metweave_core.MetarSourceError("timeout", "ogimet", `ogimet TAF 请求超时(>${options.timeoutMs}ms,站=${station})`)), options.timeoutMs);
321
+ try {
322
+ let res;
323
+ try {
324
+ res = await fetch(ogimetTafUrl(station, window, options), { signal: controller.signal });
325
+ } catch (err) {
326
+ if (controller.signal.aborted) throw err;
327
+ const reason = err instanceof Error ? err.message : String(err);
328
+ throw new _metweave_core.MetarSourceError("network", "ogimet", `网络请求失败(源:ogimet TAF,站=${station}):${reason}`, { cause: err });
329
+ }
330
+ if (!res.ok) throw new _metweave_core.MetarSourceError("http-error", "ogimet", `ogimet TAF HTTP ${res.status}(站=${station})`);
331
+ return parseOgimetTafs(await res.text(), station);
332
+ } finally {
333
+ if (timer !== void 0) clearTimeout(timer);
334
+ external?.removeEventListener("abort", forward);
335
+ }
336
+ }
114
337
  //#endregion
338
+ exports.AW_TAF_ENDPOINT = AW_TAF_ENDPOINT;
115
339
  Object.defineProperty(exports, "MetarParseError", {
116
340
  enumerable: true,
117
341
  get: function() {
@@ -124,8 +348,15 @@ Object.defineProperty(exports, "MetarSourceError", {
124
348
  return _metweave_core.MetarSourceError;
125
349
  }
126
350
  });
351
+ exports.OGIMET_TAF_ENDPOINT = OGIMET_TAF_ENDPOINT;
352
+ exports.awTafUrl = awTafUrl;
127
353
  exports.getMetarReports = getMetarReports;
128
354
  exports.getMetars = getMetars;
355
+ exports.getTafReports = getTafReports;
356
+ exports.getTafs = getTafs;
357
+ exports.getTafsOgimet = getTafsOgimet;
129
358
  exports.iemCurrentsUrl = iemCurrentsUrl;
359
+ exports.ogimetTafUrl = ogimetTafUrl;
360
+ exports.parseOgimetTafs = parseOgimetTafs;
130
361
 
131
362
  //# sourceMappingURL=sources.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"sources.cjs","names":["MetarSourceError","parse","MetarParseError"],"sources":["../src/sources.ts"],"sourcesContent":["/**\n * metweave/sources — 取数 helper(伞包子路径)。\n *\n * 纪律:只做端点模板 + fetch + 取数侧整理(解析、定位联表)——缓存/质控/归档等\n * 数据管理逻辑永不进入开源包;核心包(core/parser/render)零网络代码。\n * 不静默:HTTP 失败、响应 schema 不符、报文解析失败一律 throw,绝不吞错返回空数组。\n * 错误面机读化:五路失败一律抛 MetarSourceError(code 稳定契约 + network 网络名字段;\n * message 权威文案为中文;英文用 @metweave/core 导出的 EN_MESSAGES[code] 查表,\n * 或渲染层 locale:\"en\" 整卡切换(card locale 已内置 code→英文映射))。\n */\nimport type { MetarReport } from \"@metweave/core\";\nimport type { MetarParseErrorCode } from \"@metweave/core\";\nimport { MetarParseError, MetarSourceError } from \"@metweave/core\";\nimport { parse } from \"@metweave/parser\";\n\n// 错误类复出(API 完整性):消费方从 sources 子路径 import 即可 catch 两类错误,\n// 不必再从伞包主入口双入口引入(类实体与 core 同一,instanceof 双向成立)。\nexport { MetarParseError, MetarSourceError } from \"@metweave/core\";\n\n/**\n * One fetched observation row: station, verbatim raw report, and source-provided coordinates (city-level for IEM China rows).\n * 单条取数观测行:站名、原样报文、源站坐标(IEM 中国行为城市级粗坐标)。\n */\nexport interface MetarObservation {\n /** 站名(ICAO) */\n station: string;\n /** 报文原文(IEM 通路无类型词,IEM 自带坐标为城市级粗坐标——定位请使用站点元数据) */\n raw: string;\n lat: number | null;\n lon: number | null;\n}\n\n/**\n * Options for getMetars: external abort signal and request timeout (either firing aborts).\n * getMetars 的选项:外部取消信号与请求超时(任一触发即中止)。\n */\nexport interface GetMetarsOptions {\n /** 外部取消信号(与超时组合,任一触发即中止) */\n signal?: AbortSignal;\n /** 请求超时毫秒数(触发即中止并抛出超时错误) */\n timeoutMs?: number;\n}\n\n/**\n * Station metadata for lookup joins: authoritative coordinates overriding the source's city-level ones.\n * 站点元数据(联表用):坐标为权威定位源,覆盖源站的城市级粗坐标。\n */\nexport interface StationRef {\n /** 站名(ICAO) */\n icao: string;\n /** 纬度(WGS-84) */\n lat: number;\n /** 经度(WGS-84) */\n lon: number;\n /** 站点名称(进 tooltip 标题) */\n name?: string;\n}\n\n/**\n * Per-row failure detail (the onUnparseable callback argument): one observation row failed to parse wholesale (missing station/time etc.).\n * 逐行容错的失败详情(onUnparseable 回调入参):一行观测报文解析整体失败(站名/时组缺失等)。\n * `code` is the machine-readable failure code (MetarParseError's stable contract); `error` is the narrative message.\n * code 是机读失败码(MetarParseError 的稳定契约),error 是中文 message。\n */\nexport interface UnparseableReport {\n /** 源站站名(IEM 行的 station 字段) */\n station: string;\n /** 报文原文 */\n raw: string;\n /** 机读失败码(missing-station / missing-time / invalid-time) */\n code: MetarParseErrorCode;\n /** 解析失败原因(Error.message,中文) */\n error: string;\n}\n\n/**\n * Options for getMetarReports: fetch options plus station metadata and the per-row tolerance callback.\n * getMetarReports 的选项:取数选项 + 站点联表元数据 + 逐行容错回调。\n */\nexport interface GetMetarReportsOptions extends GetMetarsOptions {\n /** 按 icao 联表的站点元数据:命中则用其精确坐标与名称,未命中回落源站坐标 */\n stations?: readonly StationRef[];\n /**\n * 逐行容错(缺省关闭):传入时解析失败的行跳过并逐条回调——失败可观测,不静默;\n * 不传时任一失败仍聚合抛出(默认行为不变,全有或全无)。\n */\n onUnparseable?: (failure: UnparseableReport) => void;\n /**\n * 紧凑模式:parse 产物剥除全部 span 字段(存储体积约 -32%,入库供屏场景推荐);\n * 缺省 undefined = 完整模式(span 在位,原文回溯用)。\n */\n spans?: false;\n}\n\n/**\n * A map-ready report item (structurally compatible with MetarLayerItem): the { report, position, title } triple —\n * report is the parsed IR (raw text preserved verbatim on report.raw; field values via toValues(report)),\n * position is WGS-84 [lat, lon], title is the hover title.\n * 可直接喂给地图适配器(如 addMetarLayer)的报文项(与 MetarLayerItem 结构兼容):\n * `{ report, position, title }` 三件套——report 是已解析的 IR(报文原文在 report.raw 原样保真,\n * 需要字段值走 toValues(report)),position 是 WGS-84 [lat, lon],title 是悬浮提示标题。\n */\nexport interface MetarReportItem {\n /** 已解析的报文 IR(原文经 report.raw 原样保真回溯) */\n report: MetarReport;\n /** 站点坐标(WGS-84 [lat, lon]) */\n position: [number, number];\n /** 站点提示(「ICAO 站名」;适配器缺省回落 IR 站名) */\n title: string;\n}\n\nconst IEM_BASE = \"https://mesonet.agron.iastate.edu\";\n\n/**\n * The IEM whole-network currents endpoint (CORS-open, direct browser access). Defaults to the China ASOS network (39 stations).\n * IEM 整网实况端点(CORS 全开,浏览器可直连)。缺省中国 ASOS 网(39 站)。\n * @param network - IEM network name (e.g. CN__ASOS, RU__ASOS, IA_ASOS). IEM 网络名。\n */\nexport function iemCurrentsUrl(network = \"CN__ASOS\"): string {\n return `${IEM_BASE}/api/1/currents.json?network=${encodeURIComponent(network)}`;\n}\n\n/** 观测记录守卫:station/raw 均须为字符串(只查键不查类型是旧洞)。 */\nfunction isObservation(v: unknown): v is MetarObservation {\n if (typeof v !== \"object\" || v === null) return false;\n const rec = v as { station?: unknown; raw?: unknown };\n return typeof rec.station === \"string\" && typeof rec.raw === \"string\";\n}\n\n/**\n * Fetch IEM whole-network current observations (station/raw/coordinates passed through).\n * 拉取 IEM 整网实况观测(station/raw/坐标透传)。\n * No-silent-failure discipline on the fetch side / 不静默纪律的取数面:\n * - 空数组是错误网络名的典型形态(IEM 对未知 network 返回 200 + 空 data,合法网络不会返回空)——显式 throw;\n * - fetch 网络层失败(断网/DNS)包装为中文可读错误(原始 message 保留在文末与 cause);\n * - 超时/外部 AbortSignal 触发的拒绝不包装、原样透传(保持取消语义)。\n */\nexport async function getMetars(\n network = \"CN__ASOS\",\n options: GetMetarsOptions = {},\n): Promise<MetarObservation[]> {\n // 手工组合超时与外部信号(不依赖 AbortSignal.any:兼容 Node 20 全系;setTimeout 可被测试假时钟接管)\n const controller = new AbortController();\n const external = options.signal;\n const forward = (): void => {\n controller.abort(external?.reason);\n };\n if (external !== undefined) {\n if (external.aborted) forward();\n else external.addEventListener(\"abort\", forward, { once: true });\n }\n const timer =\n options.timeoutMs === undefined\n ? undefined\n : setTimeout(\n () =>\n controller.abort(\n // 超时中止原因即最终抛出的错误:code \"timeout\"(外部 signal 取消不在此列,reason 原样透传)\n new MetarSourceError(\n \"timeout\",\n network,\n `IEM 请求超时(>${options.timeoutMs}ms,network=${network})`,\n ),\n ),\n options.timeoutMs,\n );\n try {\n let res: Response;\n try {\n res = await fetch(iemCurrentsUrl(network), { signal: controller.signal });\n } catch (err) {\n // 取消/超时引发的拒绝(controller 已中止)原样透传——保持 AbortSignal 语义\n //(超时场景透传的即上方 MetarSourceError \"timeout\");\n // 其余(断网/DNS 的 TypeError 等)包装为 MetarSourceError \"network\",原始 message 保留在文末与 cause\n if (controller.signal.aborted) throw err;\n const reason = err instanceof Error ? err.message : String(err);\n throw new MetarSourceError(\n \"network\",\n network,\n `网络请求失败(源:IEM,network=${network}):请检查网络连通性后重试(${reason})`,\n { cause: err },\n );\n }\n if (!res.ok) {\n throw new MetarSourceError(\n \"http-error\",\n network,\n `IEM HTTP ${res.status}(network=${network})`,\n );\n }\n let body: unknown;\n try {\n body = await res.json();\n } catch (err) {\n // HTTP 200 但响应体不是 JSON(企业代理/防火墙拦截页的典型形态)——裸 SyntaxError 打穿属错误面缺口;\n // 归入 bad-schema 机读码,人话提示指向代理环境。中止引发的拒绝不在此列(保持取消语义透传)\n if (controller.signal.aborted) throw err;\n const reason = err instanceof Error ? err.message : String(err);\n throw new MetarSourceError(\n \"bad-schema\",\n network,\n `IEM 响应不是合法 JSON(可能被代理/防火墙拦截,network=${network}):${reason}`,\n { cause: err },\n );\n }\n if (\n typeof body !== \"object\" ||\n body === null ||\n !(\"data\" in body) ||\n !Array.isArray(body.data)\n ) {\n throw new MetarSourceError(\n \"bad-schema\",\n network,\n `IEM 响应异常:缺少 data 数组(network=${network},schema 不符)`,\n );\n }\n const rows = body.data;\n if (!rows.every(isObservation)) {\n throw new MetarSourceError(\n \"bad-schema\",\n network,\n `IEM 响应异常:data 存在 station/raw 非字符串的记录(network=${network},schema 不符)`,\n );\n }\n if (rows.length === 0) {\n // 合法网络不会返回空(整网实况端点)——空 data 几乎必然是网络名写错(IEM 对未知网络名返回 200 + 空数组)\n throw new MetarSourceError(\n \"empty-data\",\n network,\n `IEM 返回空数据(network=${network})——请核对 IEM 网络名(如 CN__ASOS/RU__ASOS,参考 https://mesonet.agron.iastate.edu/sites/networks.php)`,\n );\n }\n return rows;\n } finally {\n if (timer !== undefined) clearTimeout(timer);\n external?.removeEventListener(\"abort\", forward);\n }\n}\n\n/**\n * Fetch → parse → locate in one step: returns map-ready report items.\n * 取数 → 解析 → 定位一步到位:返回可直接喂给地图适配器的报文项数组。\n *\n * 定位规则:stations 命中的站用元数据精确坐标(并带站名),未命中的回落源站坐标\n * (IEM 中国站为城市级粗坐标,严肃场景请传站点元数据——见 examples 的 stations.json 用法);\n * 源站坐标为 null 且无元数据的行跳过。\n * 失败语义(不静默,两种模式):缺省任一报文解析整体失败(站名/时组缺失)即聚合抛出,绝不静默丢行;\n * 传 options.onUnparseable 则逐行容错——失败行跳过并逐条回调(可观测),好行照常返回。\n */\nexport async function getMetarReports(\n network = \"CN__ASOS\",\n options: GetMetarReportsOptions = {},\n): Promise<MetarReportItem[]> {\n const { stations, onUnparseable, spans, ...fetchOptions } = options;\n const table = new Map((stations ?? []).map((s) => [s.icao, s] as const));\n const observations = await getMetars(network, fetchOptions);\n const items: MetarReportItem[] = [];\n const failures: string[] = [];\n for (const obs of observations) {\n const station = table.get(obs.station);\n const lat = station?.lat ?? obs.lat;\n const lon = station?.lon ?? obs.lon;\n if (lat === null || lon === null) continue;\n try {\n items.push({\n report: parse(obs.raw, spans === false ? { spans: false } : undefined),\n position: [lat, lon],\n title: `${obs.station}${station?.name ? ` ${station.name}` : \"\"}`,\n });\n } catch (err) {\n // parse 对字符串输入只抛 MetarParseError(code 稳定契约);其他异常属编程错误,直接上抛不降级\n if (!(err instanceof MetarParseError)) throw err;\n if (onUnparseable !== undefined) {\n onUnparseable({ station: obs.station, raw: obs.raw, code: err.code, error: err.message });\n continue;\n }\n failures.push(`${obs.station}: ${err.message}`);\n }\n }\n if (failures.length > 0) {\n // 聚合错误机读化(原裸 Error):code 'batch-parse-failed' 稳定契约;raw 字段在此承载\n // 汇总信息(网络名/条数/逐条站名与原因)而非单条报文原文——语义调整见 core/errors.ts 注释\n const summary = `network=${network}; failures=${failures.length}; ${failures.join(\"; \")}`;\n throw new MetarParseError(\n \"batch-parse-failed\",\n summary,\n `报文解析失败 ${failures.length} 条(network=${network})——${failures.slice(0, 3).join(\";\")}${failures.length > 3 ? \"……\" : \"\"}`,\n );\n }\n return items;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"sources.cjs","names":["MetarSourceError","parse","MetarParseError","parseTaf"],"sources":["../src/sources.ts"],"sourcesContent":["/**\n * metweave/sources — 取数 helper(伞包子路径)。双线:IEM METAR 实况 + aviationweather TAF 预报(主)+ ogimet TAF 报池补充(次)。\n *\n * 纪律:只做端点模板 + fetch + 取数侧整理(解析、定位联表)——缓存/质控/归档等\n * 数据管理逻辑永不进入开源包;核心包(core/parser/render)零网络代码。\n * 换源口径(内网的人切换到自己的数据源」指令):两条线各留 baseUrl 覆盖位\n * (IEM=站点根、TAF=端点根)——内网镜像/自建网关只换根、路径与查询串由本层拼装。\n * 不静默:HTTP 失败、响应 schema 不符、报文解析失败一律 throw,绝不吞错返回空数组。\n * 错误面机读化:五路失败一律抛 MetarSourceError(code 稳定契约 + network 网络名字段;\n * message 权威文案为中文;英文用 @metweave/core 导出的 EN_MESSAGES[code] 查表,\n * 或渲染层 locale:\"en\" 整卡切换(card locale 已内置 code→英文映射))。\n */\nimport type { MetarReport, TafReport } from \"@metweave/core\";\nimport type { MetarParseErrorCode } from \"@metweave/core\";\nimport { MetarParseError, MetarSourceError } from \"@metweave/core\";\nimport { parse, parseTaf } from \"@metweave/parser\";\n\n// 错误类复出(API 完整性):消费方从 sources 子路径 import 即可 catch 两类错误,\n// 不必再从伞包主入口双入口引入(类实体与 core 同一,instanceof 双向成立)。\nexport { MetarParseError, MetarSourceError } from \"@metweave/core\";\n\n/**\n * One fetched observation row: station, verbatim raw report, and source-provided coordinates (city-level for IEM China rows).\n * 单条取数观测行:站名、原样报文、源站坐标(IEM 中国行为城市级粗坐标)。\n */\nexport interface MetarObservation {\n /** 站名(ICAO) */\n station: string;\n /** 报文原文(IEM 通路无类型词,IEM 自带坐标为城市级粗坐标——定位请使用站点元数据) */\n raw: string;\n lat: number | null;\n lon: number | null;\n}\n\n/**\n * Options for getMetars: external abort signal and request timeout (either firing aborts).\n * getMetars 的选项:外部取消信号与请求超时(任一触发即中止)。\n */\nexport interface GetMetarsOptions {\n /** 外部取消信号(与超时组合,任一触发即中止) */\n signal?: AbortSignal;\n /** 请求超时毫秒数(触发即中止并抛出超时错误) */\n timeoutMs?: number;\n /**\n * IEM 站点根覆盖(缺省官方 https://mesonet.agron.iastate.edu):内网镜像/自建网关\n * 只换根——/api/1/currents.json 路径与查询串由本层拼装(换源口径,见文件头)。\n */\n baseUrl?: string;\n}\n\n/**\n * Station metadata for lookup joins: authoritative coordinates overriding the source's city-level ones.\n * 站点元数据(联表用):坐标为权威定位源,覆盖源站的城市级粗坐标。\n */\nexport interface StationRef {\n /** 站名(ICAO) */\n icao: string;\n /** 纬度(WGS-84) */\n lat: number;\n /** 经度(WGS-84) */\n lon: number;\n /** 站点名称(进 tooltip 标题) */\n name?: string;\n}\n\n/**\n * Per-row failure detail (the onUnparseable callback argument): one observation row failed to parse wholesale (missing station/time etc.).\n * 逐行容错的失败详情(onUnparseable 回调入参):一行观测报文解析整体失败(站名/时组缺失等)。\n * `code` is the machine-readable failure code (MetarParseError's stable contract); `error` is the narrative message.\n * code 是机读失败码(MetarParseError 的稳定契约),error 是中文 message。\n */\nexport interface UnparseableReport {\n /** 源站站名(IEM 行的 station 字段) */\n station: string;\n /** 报文原文 */\n raw: string;\n /** 机读失败码(missing-station / missing-time / invalid-time) */\n code: MetarParseErrorCode;\n /** 解析失败原因(Error.message,中文) */\n error: string;\n}\n\n/**\n * Options for getMetarReports: fetch options plus station metadata and the per-row tolerance callback.\n * getMetarReports 的选项:取数选项 + 站点联表元数据 + 逐行容错回调。\n */\nexport interface GetMetarReportsOptions extends GetMetarsOptions {\n /** 按 icao 联表的站点元数据:命中则用其精确坐标与名称,未命中回落源站坐标 */\n stations?: readonly StationRef[];\n /**\n * 逐行容错(缺省关闭):传入时解析失败的行跳过并逐条回调——失败可观测,不静默;\n * 不传时任一失败仍聚合抛出(默认行为不变,全有或全无)。\n */\n onUnparseable?: (failure: UnparseableReport) => void;\n /**\n * 紧凑模式:parse 产物剥除全部 span 字段(存储体积约 -32%,入库供屏场景推荐);\n * 缺省 undefined = 完整模式(span 在位,原文回溯用)。\n */\n spans?: false;\n}\n\n/**\n * A map-ready report item (structurally compatible with MetarLayerItem): the { report, position, title } triple —\n * report is the parsed IR (raw text preserved verbatim on report.raw; field values via toValues(report)),\n * position is WGS-84 [lat, lon], title is the hover title.\n * 可直接喂给地图适配器(如 addMetarLayer)的报文项(与 MetarLayerItem 结构兼容):\n * `{ report, position, title }` 三件套——report 是已解析的 IR(报文原文在 report.raw 原样保真,\n * 需要字段值走 toValues(report)),position 是 WGS-84 [lat, lon],title 是悬浮提示标题。\n */\nexport interface MetarReportItem {\n /** 已解析的报文 IR(原文经 report.raw 原样保真回溯) */\n report: MetarReport;\n /** 站点坐标(WGS-84 [lat, lon]) */\n position: [number, number];\n /** 站点提示(「ICAO 站名」;适配器缺省回落 IR 站名) */\n title: string;\n}\n\nconst IEM_BASE = \"https://mesonet.agron.iastate.edu\";\n\n/**\n * The IEM whole-network currents endpoint (CORS-open, direct browser access). Defaults to the China ASOS network (39 stations).\n * IEM 整网实况端点(CORS 全开,浏览器可直连)。缺省中国 ASOS 网(39 站)。\n * @param network - IEM network name (e.g. CN__ASOS, RU__ASOS, IA_ASOS). IEM 网络名。\n * @param baseUrl - IEM 站点根覆盖(内网镜像/自建网关——换源口径,见 GetMetarsOptions.baseUrl)。\n */\nexport function iemCurrentsUrl(network = \"CN__ASOS\", baseUrl = IEM_BASE): string {\n return `${baseUrl}/api/1/currents.json?network=${encodeURIComponent(network)}`;\n}\n\n/** 观测记录守卫:station/raw 均须为字符串(只查键不查类型是旧洞)。 */\nfunction isObservation(v: unknown): v is MetarObservation {\n if (typeof v !== \"object\" || v === null) return false;\n const rec = v as { station?: unknown; raw?: unknown };\n return typeof rec.station === \"string\" && typeof rec.raw === \"string\";\n}\n\n/**\n * Fetch IEM whole-network current observations (station/raw/coordinates passed through).\n * 拉取 IEM 整网实况观测(station/raw/坐标透传)。\n * No-silent-failure discipline on the fetch side / 不静默纪律的取数面:\n * - 空数组是错误网络名的典型形态(IEM 对未知 network 返回 200 + 空 data,合法网络不会返回空)——显式 throw;\n * - fetch 网络层失败(断网/DNS)包装为中文可读错误(原始 message 保留在文末与 cause);\n * - 超时/外部 AbortSignal 触发的拒绝不包装、原样透传(保持取消语义)。\n */\nexport async function getMetars(\n network = \"CN__ASOS\",\n options: GetMetarsOptions = {},\n): Promise<MetarObservation[]> {\n // 手工组合超时与外部信号(不依赖 AbortSignal.any:兼容 Node 20 全系;setTimeout 可被测试假时钟接管)\n const controller = new AbortController();\n const external = options.signal;\n const forward = (): void => {\n controller.abort(external?.reason);\n };\n if (external !== undefined) {\n if (external.aborted) forward();\n else external.addEventListener(\"abort\", forward, { once: true });\n }\n const timer =\n options.timeoutMs === undefined\n ? undefined\n : setTimeout(\n () =>\n controller.abort(\n // 超时中止原因即最终抛出的错误:code \"timeout\"(外部 signal 取消不在此列,reason 原样透传)\n new MetarSourceError(\n \"timeout\",\n network,\n `IEM 请求超时(>${options.timeoutMs}ms,network=${network})`,\n ),\n ),\n options.timeoutMs,\n );\n try {\n let res: Response;\n try {\n res = await fetch(iemCurrentsUrl(network, options.baseUrl), { signal: controller.signal });\n } catch (err) {\n // 取消/超时引发的拒绝(controller 已中止)原样透传——保持 AbortSignal 语义\n //(超时场景透传的即上方 MetarSourceError \"timeout\");\n // 其余(断网/DNS 的 TypeError 等)包装为 MetarSourceError \"network\",原始 message 保留在文末与 cause\n if (controller.signal.aborted) throw err;\n const reason = err instanceof Error ? err.message : String(err);\n throw new MetarSourceError(\n \"network\",\n network,\n `网络请求失败(源:IEM,network=${network}):请检查网络连通性后重试(${reason})`,\n { cause: err },\n );\n }\n if (!res.ok) {\n throw new MetarSourceError(\n \"http-error\",\n network,\n `IEM HTTP ${res.status}(network=${network})`,\n );\n }\n let body: unknown;\n try {\n body = await res.json();\n } catch (err) {\n // HTTP 200 但响应体不是 JSON(企业代理/防火墙拦截页的典型形态)——裸 SyntaxError 打穿属错误面缺口;\n // 归入 bad-schema 机读码,人话提示指向代理环境。中止引发的拒绝不在此列(保持取消语义透传)\n if (controller.signal.aborted) throw err;\n const reason = err instanceof Error ? err.message : String(err);\n throw new MetarSourceError(\n \"bad-schema\",\n network,\n `IEM 响应不是合法 JSON(可能被代理/防火墙拦截,network=${network}):${reason}`,\n { cause: err },\n );\n }\n if (\n typeof body !== \"object\" ||\n body === null ||\n !(\"data\" in body) ||\n !Array.isArray(body.data)\n ) {\n throw new MetarSourceError(\n \"bad-schema\",\n network,\n `IEM 响应异常:缺少 data 数组(network=${network},schema 不符)`,\n );\n }\n const rows = body.data;\n if (!rows.every(isObservation)) {\n throw new MetarSourceError(\n \"bad-schema\",\n network,\n `IEM 响应异常:data 存在 station/raw 非字符串的记录(network=${network},schema 不符)`,\n );\n }\n if (rows.length === 0) {\n // 合法网络不会返回空(整网实况端点)——空 data 几乎必然是网络名写错(IEM 对未知网络名返回 200 + 空数组)\n throw new MetarSourceError(\n \"empty-data\",\n network,\n `IEM 返回空数据(network=${network})——请核对 IEM 网络名(如 CN__ASOS/RU__ASOS,参考 https://mesonet.agron.iastate.edu/sites/networks.php)`,\n );\n }\n return rows;\n } finally {\n if (timer !== undefined) clearTimeout(timer);\n external?.removeEventListener(\"abort\", forward);\n }\n}\n\n/**\n * Fetch → parse → locate in one step: returns map-ready report items.\n * 取数 → 解析 → 定位一步到位:返回可直接喂给地图适配器的报文项数组。\n *\n * 定位规则:stations 命中的站用元数据精确坐标(并带站名),未命中的回落源站坐标\n * (IEM 中国站为城市级粗坐标,严肃场景请传站点元数据——见 examples 的 stations.json 用法);\n * 源站坐标为 null 且无元数据的行跳过。\n * 失败语义(不静默,两种模式):缺省任一报文解析整体失败(站名/时组缺失)即聚合抛出,绝不静默丢行;\n * 传 options.onUnparseable 则逐行容错——失败行跳过并逐条回调(可观测),好行照常返回。\n */\nexport async function getMetarReports(\n network = \"CN__ASOS\",\n options: GetMetarReportsOptions = {},\n): Promise<MetarReportItem[]> {\n const { stations, onUnparseable, spans, ...fetchOptions } = options;\n const table = new Map((stations ?? []).map((s) => [s.icao, s] as const));\n const observations = await getMetars(network, fetchOptions);\n const items: MetarReportItem[] = [];\n const failures: string[] = [];\n for (const obs of observations) {\n const station = table.get(obs.station);\n const lat = station?.lat ?? obs.lat;\n const lon = station?.lon ?? obs.lon;\n if (lat === null || lon === null) continue;\n try {\n items.push({\n report: parse(obs.raw, spans === false ? { spans: false } : undefined),\n position: [lat, lon],\n title: `${obs.station}${station?.name ? ` ${station.name}` : \"\"}`,\n });\n } catch (err) {\n // parse 对字符串输入只抛 MetarParseError(code 稳定契约);其他异常属编程错误,直接上抛不降级\n if (!(err instanceof MetarParseError)) throw err;\n if (onUnparseable !== undefined) {\n onUnparseable({ station: obs.station, raw: obs.raw, code: err.code, error: err.message });\n continue;\n }\n failures.push(`${obs.station}: ${err.message}`);\n }\n }\n if (failures.length > 0) {\n // 聚合错误机读化(原裸 Error):code 'batch-parse-failed' 稳定契约;raw 字段在此承载\n // 汇总信息(网络名/条数/逐条站名与原因)而非单条报文原文——语义调整见 core/errors.ts 注释\n const summary = `network=${network}; failures=${failures.length}; ${failures.join(\"; \")}`;\n throw new MetarParseError(\n \"batch-parse-failed\",\n summary,\n `报文解析失败 ${failures.length} 条(network=${network})——${failures.slice(0, 3).join(\";\")}${failures.length > 3 ? \"……\" : \"\"}`,\n );\n }\n return items;\n}\n\n// ---------------------------------------------------------------- TAF 取数线(收进 sources」指令)\n\n/** aviationweather TAF 数据端点(上游无 CORS 头:浏览器直连须自建代理/镜像——baseUrl 覆盖即为此用) */\nexport const AW_TAF_ENDPOINT = \"https://aviationweather.gov/api/data/taf\";\n\n/**\n * One fetched TAF row: station (best-effort) and the verbatim report (continuation lines already merged).\n * 单条 TAF 取数行:站码(best-effort)与报文原文(续行已归并)。\n */\nexport interface TafObservation {\n /** 站码:剥 TAF/AMD/COR 电头词后的首枚 4 位码;剥不出为 \"\"(解析层 missing-station 兜底出声) */\n station: string;\n /** 报文原文(上游 raw 格式的缩进续行已并回所属报文;report.raw 语义一致——原文保真) */\n raw: string;\n}\n\n/** Options for getTafs: fetch options plus the TAF endpoint-root override and the as-of date. */\nexport interface GetTafsOptions {\n /** 外部取消信号(与超时组合,任一触发即中止) */\n signal?: AbortSignal;\n /** 请求超时毫秒数(触发即中止并抛出超时错误) */\n timeoutMs?: number;\n /**\n * TAF 端点根覆盖(缺省官方 https://aviationweather.gov/api/data/taf):内网镜像/自建代理\n * 只换根——?ids=&format=raw 查询串由本层拼装(换源口径,见文件头;examples 即用 /aw-taf 代理根)。\n */\n baseUrl?: string;\n /**\n * 「该时刻已发布的最新报」(上游 date 参数语义):回看上一发布周期用(examples 报池即 date=now-4h)。\n * Date 转 ISO;字符串原样透传(上游亦收相对形态)。\n */\n date?: Date | string;\n}\n\n/**\n * The aviationweather TAF endpoint (raw format). ids joined as given; date serialized when present.\n * aviationweather TAF 端点模板(format=raw)。ids 数组逗号连接;date 存在时序列化为查询参数。\n */\nexport function awTafUrl(\n ids: string | readonly string[],\n options: { baseUrl?: string; date?: Date | string } = {},\n): string {\n const idList = typeof ids === \"string\" ? ids : ids.join(\",\");\n const params = new URLSearchParams({ ids: idList, format: \"raw\" });\n const d = options.date;\n if (d !== undefined) params.set(\"date\", d instanceof Date ? d.toISOString() : d);\n return `${options.baseUrl ?? AW_TAF_ENDPOINT}?${params.toString()}`;\n}\n\n/** raw 文本 → 整份报文行(缩进续行并回上一份;与报池语义同源)+ best-effort 站码提取 */\nfunction mergeTafLines(text: string): TafObservation[] {\n const merged: string[] = [];\n for (const line of text.split(\"\\n\")) {\n if (line.trim() === \"\") continue;\n if (/^\\s/.test(line) && merged.length > 0) merged[merged.length - 1] += ` ${line.trim()}`;\n else merged.push(line.trim());\n }\n return merged.map((raw) => {\n const toks = raw.split(/\\s+/);\n let k = toks[0] === \"TAF\" ? 1 : 0;\n while (toks[k] === \"AMD\" || toks[k] === \"COR\") k += 1;\n const head = toks[k] ?? \"\";\n return { station: /^[A-Z0-9]{4}$/.test(head) ? head : \"\", raw };\n });\n}\n\n/**\n * Fetch aviationweather TAFs (raw format) with station extracted and continuation lines merged.\n * 拉取 aviationweather TAF(raw 格式):续行归并 + 站码提取的取数侧整理。\n * 失败语义与 getMetars 同族(不静默):timeout/network/http-error/empty-data 一律 MetarSourceError\n * 机读码;上游对未知 ids 也返回 200 + 空体,空数据显式 throw 而非空数组静默。\n */\nexport async function getTafs(\n ids: string | readonly string[],\n options: GetTafsOptions = {},\n): Promise<TafObservation[]> {\n // 手工组合超时与外部信号(与 getMetars 同款:不依赖 AbortSignal.any,兼容 Node 20;假时钟可测)\n const controller = new AbortController();\n const external = options.signal;\n const forward = (): void => {\n controller.abort(external?.reason);\n };\n if (external !== undefined) {\n if (external.aborted) forward();\n else external.addEventListener(\"abort\", forward, { once: true });\n }\n const timer =\n options.timeoutMs === undefined\n ? undefined\n : setTimeout(\n () =>\n controller.abort(\n new MetarSourceError(\n \"timeout\",\n \"aviationweather\",\n `aviationweather TAF 请求超时(>${options.timeoutMs}ms)`,\n ),\n ),\n options.timeoutMs,\n );\n try {\n let res: Response;\n try {\n res = await fetch(awTafUrl(ids, options), { signal: controller.signal });\n } catch (err) {\n // 取消/超时引发的拒绝原样透传(AbortSignal 语义);断网/DNS 包装为 network(原始 message 留文末与 cause)\n if (controller.signal.aborted) throw err;\n const reason = err instanceof Error ? err.message : String(err);\n throw new MetarSourceError(\n \"network\",\n \"aviationweather\",\n `网络请求失败(源:aviationweather TAF):请检查网络连通性后重试(${reason})`,\n { cause: err },\n );\n }\n if (!res.ok) {\n throw new MetarSourceError(\n \"http-error\",\n \"aviationweather\",\n `aviationweather TAF HTTP ${res.status}`,\n );\n }\n let text: string;\n try {\n text = await res.text();\n } catch (err) {\n if (controller.signal.aborted) throw err;\n const reason = err instanceof Error ? err.message : String(err);\n throw new MetarSourceError(\n \"network\",\n \"aviationweather\",\n `aviationweather TAF 响应读取失败:${reason}`,\n { cause: err },\n );\n }\n const rows = mergeTafLines(text);\n if (rows.length === 0) {\n throw new MetarSourceError(\n \"empty-data\",\n \"aviationweather\",\n \"aviationweather TAF 返回空数据——请核对 ids 站码(如 ZBAA,ZBAD;端点 format=raw)\",\n );\n }\n return rows;\n } finally {\n if (timer !== undefined) clearTimeout(timer);\n external?.removeEventListener(\"abort\", forward);\n }\n}\n\n/** Options for getTafReports: fetch options plus station metadata and the per-row tolerance callback. */\nexport interface GetTafReportsOptions extends GetTafsOptions {\n /** 按 icao 联表的站点元数据(TAF 端点不提供坐标——stations 是定位的唯一来源,缺省将得空数组) */\n stations?: readonly StationRef[];\n /** 逐行容错(缺省关闭):传入时解析失败的行跳过并逐条回调;不传时聚合抛出(全有或全无) */\n onUnparseable?: (failure: UnparseableReport) => void;\n /** 紧凑模式:parseTaf 产物剥除全部 span 字段(存储场景推荐);缺省完整模式 */\n spans?: false;\n}\n\n/**\n * A map-ready TAF item (structurally compatible with TafLayerItem's report/position/title triple).\n * 可直接喂给地图适配器(如 addTafLayer)的预报项:{ report, position, title } 三件套。\n */\nexport interface TafReportItem {\n /** 已解析的 TAF IR(原文经 report.raw 原样保真) */\n report: TafReport;\n /** 站点坐标(WGS-84 [lat, lon];TAF 端点无坐标,来自 stations 联表) */\n position: [number, number];\n /** 站点提示(「ICAO 站名」;适配器缺省回落 IR 站名) */\n title: string;\n}\n\n/**\n * TAF 版的取数 → 解析 → 定位一步到位(契约沿 getMetarReports):stations 命中的站带精确坐标与站名,\n * 未命中跳过(TAF 端点无坐标可回落,与 getMetarReports 的无坐标跳行同规则);解析失败两种模式\n * (缺省聚合抛出 batch-parse-failed、传 onUnparseable 逐行容错)。报池等数据管理逻辑不进本层。\n */\nexport async function getTafReports(\n ids: string | readonly string[],\n options: GetTafReportsOptions = {},\n): Promise<TafReportItem[]> {\n const { stations, onUnparseable, spans, ...fetchOptions } = options;\n const table = new Map((stations ?? []).map((s) => [s.icao, s] as const));\n const observations = await getTafs(ids, fetchOptions);\n const items: TafReportItem[] = [];\n const failures: string[] = [];\n for (const obs of observations) {\n try {\n const report = parseTaf(obs.raw, spans === false ? { spans: false } : undefined);\n const station = table.get(report.station);\n if (station === undefined) continue; // 站表未命中(端点无坐标可回落)——跳过,规则同 getMetarReports\n items.push({\n report,\n position: [station.lat, station.lon],\n title: `${report.station}${station.name ? ` ${station.name}` : \"\"}`,\n });\n } catch (err) {\n // parseTaf 对字符串输入只抛 MetarParseError;其他异常属编程错误直接上抛\n if (!(err instanceof MetarParseError)) throw err;\n if (onUnparseable !== undefined) {\n onUnparseable({ station: obs.station, raw: obs.raw, code: err.code, error: err.message });\n continue;\n }\n failures.push(`${obs.station}: ${err.message}`);\n }\n }\n if (failures.length > 0) {\n const summary = `source=aviationweather; failures=${failures.length}; ${failures.join(\"; \")}`;\n throw new MetarParseError(\n \"batch-parse-failed\",\n summary,\n `TAF 解析失败 ${failures.length} 条(aviationweather)——${failures.slice(0, 3).join(\";\")}${failures.length > 3 ? \"……\" : \"\"}`,\n );\n }\n return items;\n}\n\n// ---------------------------------------------------------------- ogimet TAF 补充线(指令:aviationweather 取最新,ogimet 取最新/次新合并补充)\n\n/** ogimet display_metars2.php 端点(tipo=FT=TAF;无 CORS 头:浏览器直连须代理/镜像——baseUrl 覆盖即为此用) */\nexport const OGIMET_TAF_ENDPOINT = \"https://www.ogimet.com/display_metars2.php\";\n\n/** ogimet 查询窗(UTC 起止) */\nexport interface OgimetTafWindow {\n readonly from: Date;\n readonly to: Date;\n}\n\n/**\n * ogimet TAF 端点模板(配方=私有管线 backfill_ogimet.py 同源):tipo=FT、txt 形、\n * 服务端按 <pre> 区回 HTML(解析见 parseOgimetTafs)。免费公益服务——调用方自行节制频率。\n */\n/** UTC 日期字段补零组(ogimet 查询串的 ano/mes/day/hora/min 族) */\nconst ogimetFields = (\n d: Date,\n): { ano: string; mes: string; day: string; hora: string; min: string } => ({\n ano: `${d.getUTCFullYear()}`,\n mes: `${d.getUTCMonth() + 1}`.padStart(2, \"0\"),\n day: `${d.getUTCDate()}`.padStart(2, \"0\"),\n hora: `${d.getUTCHours()}`.padStart(2, \"0\"),\n min: `${d.getUTCMinutes()}`.padStart(2, \"0\"),\n});\n\nexport function ogimetTafUrl(\n station: string,\n window: OgimetTafWindow,\n options: { baseUrl?: string } = {},\n): string {\n const a = ogimetFields(window.from);\n const b = ogimetFields(window.to);\n const qs = new URLSearchParams({\n lang: \"en\",\n lugar: station,\n tipo: \"FT\",\n ord: \"DIR\",\n nil: \"SI\",\n fmt: \"txt\",\n ano: a.ano,\n mes: a.mes,\n day: a.day,\n hora: a.hora,\n min: a.min,\n anof: b.ano,\n mesf: b.mes,\n dayf: b.day,\n horaf: b.hora,\n minf: b.min,\n send: \"send\",\n nohtmlo: \"yes\",\n annot: \"yes\",\n leng: \"0\",\n });\n return `${options.baseUrl ?? OGIMET_TAF_ENDPOINT}?${qs.toString()}`;\n}\n\n/** 电头站码 best-effort 提取(TAF/AMD/COR 剥词;剥不出返回空串)——与 mergeTafLines 同判式(aviationweather 取数线) */\nconst stationHeadOf = (raw: string): string => {\n const toks = raw.split(/\\s+/);\n let k = toks[0] === \"TAF\" ? 1 : 0;\n while (toks[k] === \"AMD\" || toks[k] === \"COR\") k += 1;\n const head = toks[k] ?? \"\";\n return /^[A-Z0-9]{4}$/.test(head) ? head : \"\";\n};\n\n/**\n * ogimet 响应解析(<pre> 区):12 位时间戳前缀行(YYYYMMDDHHmm,收报序)起新记录、无前缀行并入上一条\n * (多行报文并单——与私有管线 parse_tafs 同协议);# 注释/空行跳过;只收 TAF 起头报文。\n * 站码 best-effort 取自电头(query 站名兜底);空模板(无 <pre> 或零报文)返回空数组——\n * 由调用方判空(ogimet 缺数常见,属正常降级而非错误面)。\n */\nexport function parseOgimetTafs(html: string, fallbackStation = \"\"): TafObservation[] {\n const pre = /<pre>([\\s\\S]*?)<\\/pre>/.exec(html);\n if (pre === null) return [];\n const PREFIX = /^(\\d{12})\\s+(.*)$/;\n const merged: string[] = [];\n for (const line of (pre[1] ?? \"\").split(\"\\n\")) {\n const t = line.trim();\n if (t === \"\" || t.startsWith(\"#\")) continue;\n const pm = PREFIX.exec(t);\n if (pm !== null) merged.push(pm[2] ?? \"\");\n else if (merged.length > 0) merged[merged.length - 1] += ` ${t}`;\n }\n return merged\n .filter((raw) => raw.startsWith(\"TAF\"))\n .map((raw) => ({ station: stationHeadOf(raw) || fallbackStation, raw }));\n}\n\n/** Options for getTafsOgimet: fetch options plus the ogimet endpoint-root override. */\nexport interface GetTafsOgimetOptions {\n /** 外部取消信号(与超时组合,任一触发即中止) */\n signal?: AbortSignal;\n /** 请求超时毫秒数(触发即中止并抛出超时错误) */\n timeoutMs?: number;\n /** ogimet 端点根覆盖(内网镜像/自建代理;examples 走 /ogimet-taf 代理根) */\n baseUrl?: string;\n}\n\n/**\n * Fetch one station's recent TAFs from ogimet (latest + previous publication cycles).\n * 拉取单站的 ogimet 近窗 TAF(最新与上一发布周期——报池补充线,供与 aviationweather 线合并去重)。\n * 失败语义与 getTafs 同族(timeout/network/http-error 机读码);空数据返回空数组而非抛错\n * (ogimet 站点级缺数常见——它是补充线,空窗属正常降级,不静默丢整批)。\n */\nexport async function getTafsOgimet(\n station: string,\n window: OgimetTafWindow,\n options: GetTafsOgimetOptions = {},\n): Promise<TafObservation[]> {\n const controller = new AbortController();\n const external = options.signal;\n const forward = (): void => {\n controller.abort(external?.reason);\n };\n if (external !== undefined) {\n if (external.aborted) forward();\n else external.addEventListener(\"abort\", forward, { once: true });\n }\n const timer =\n options.timeoutMs === undefined\n ? undefined\n : setTimeout(\n () =>\n controller.abort(\n new MetarSourceError(\n \"timeout\",\n \"ogimet\",\n `ogimet TAF 请求超时(>${options.timeoutMs}ms,站=${station})`,\n ),\n ),\n options.timeoutMs,\n );\n try {\n let res: Response;\n try {\n res = await fetch(ogimetTafUrl(station, window, options), { signal: controller.signal });\n } catch (err) {\n if (controller.signal.aborted) throw err;\n const reason = err instanceof Error ? err.message : String(err);\n throw new MetarSourceError(\n \"network\",\n \"ogimet\",\n `网络请求失败(源:ogimet TAF,站=${station}):${reason}`,\n { cause: err },\n );\n }\n if (!res.ok) {\n throw new MetarSourceError(\n \"http-error\",\n \"ogimet\",\n `ogimet TAF HTTP ${res.status}(站=${station})`,\n );\n }\n return parseOgimetTafs(await res.text(), station);\n } finally {\n if (timer !== undefined) clearTimeout(timer);\n external?.removeEventListener(\"abort\", forward);\n }\n}\n"],"mappings":"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"}
@@ -1,4 +1,4 @@
1
- import { MetarParseError, MetarParseErrorCode, MetarReport, MetarSourceError } from "@metweave/core";
1
+ import { MetarParseError, MetarParseErrorCode, MetarReport, MetarSourceError, TafReport } from "@metweave/core";
2
2
  //#region src/sources.d.ts
3
3
  /**
4
4
  * One fetched observation row: station, verbatim raw report, and source-provided coordinates (city-level for IEM China rows).
@@ -21,6 +21,11 @@ export interface GetMetarsOptions {
21
21
  signal?: AbortSignal;
22
22
  /** 请求超时毫秒数(触发即中止并抛出超时错误) */
23
23
  timeoutMs?: number;
24
+ /**
25
+ * IEM 站点根覆盖(缺省官方 https://mesonet.agron.iastate.edu):内网镜像/自建网关
26
+ * 只换根——/api/1/currents.json 路径与查询串由本层拼装(换源口径,见文件头)。
27
+ */
28
+ baseUrl?: string;
24
29
  }
25
30
  /**
26
31
  * Station metadata for lookup joins: authoritative coordinates overriding the source's city-level ones.
@@ -90,8 +95,9 @@ export interface MetarReportItem {
90
95
  * The IEM whole-network currents endpoint (CORS-open, direct browser access). Defaults to the China ASOS network (39 stations).
91
96
  * IEM 整网实况端点(CORS 全开,浏览器可直连)。缺省中国 ASOS 网(39 站)。
92
97
  * @param network - IEM network name (e.g. CN__ASOS, RU__ASOS, IA_ASOS). IEM 网络名。
98
+ * @param baseUrl - IEM 站点根覆盖(内网镜像/自建网关——换源口径,见 GetMetarsOptions.baseUrl)。
93
99
  */
94
- export declare function iemCurrentsUrl(network?: string): string;
100
+ export declare function iemCurrentsUrl(network?: string, baseUrl?: string): string;
95
101
  /**
96
102
  * Fetch IEM whole-network current observations (station/raw/coordinates passed through).
97
103
  * 拉取 IEM 整网实况观测(station/raw/坐标透传)。
@@ -112,6 +118,110 @@ export declare function getMetars(network?: string, options?: GetMetarsOptions):
112
118
  * 传 options.onUnparseable 则逐行容错——失败行跳过并逐条回调(可观测),好行照常返回。
113
119
  */
114
120
  export declare function getMetarReports(network?: string, options?: GetMetarReportsOptions): Promise<MetarReportItem[]>;
121
+ /** aviationweather TAF 数据端点(上游无 CORS 头:浏览器直连须自建代理/镜像——baseUrl 覆盖即为此用) */
122
+ export declare const AW_TAF_ENDPOINT = "https://aviationweather.gov/api/data/taf";
123
+ /**
124
+ * One fetched TAF row: station (best-effort) and the verbatim report (continuation lines already merged).
125
+ * 单条 TAF 取数行:站码(best-effort)与报文原文(续行已归并)。
126
+ */
127
+ export interface TafObservation {
128
+ /** 站码:剥 TAF/AMD/COR 电头词后的首枚 4 位码;剥不出为 ""(解析层 missing-station 兜底出声) */
129
+ station: string;
130
+ /** 报文原文(上游 raw 格式的缩进续行已并回所属报文;report.raw 语义一致——原文保真) */
131
+ raw: string;
132
+ }
133
+ /** Options for getTafs: fetch options plus the TAF endpoint-root override and the as-of date. */
134
+ export interface GetTafsOptions {
135
+ /** 外部取消信号(与超时组合,任一触发即中止) */
136
+ signal?: AbortSignal;
137
+ /** 请求超时毫秒数(触发即中止并抛出超时错误) */
138
+ timeoutMs?: number;
139
+ /**
140
+ * TAF 端点根覆盖(缺省官方 https://aviationweather.gov/api/data/taf):内网镜像/自建代理
141
+ * 只换根——?ids=&format=raw 查询串由本层拼装(换源口径,见文件头;examples 即用 /aw-taf 代理根)。
142
+ */
143
+ baseUrl?: string;
144
+ /**
145
+ * 「该时刻已发布的最新报」(上游 date 参数语义):回看上一发布周期用(examples 报池即 date=now-4h)。
146
+ * Date 转 ISO;字符串原样透传(上游亦收相对形态)。
147
+ */
148
+ date?: Date | string;
149
+ }
150
+ /**
151
+ * The aviationweather TAF endpoint (raw format). ids joined as given; date serialized when present.
152
+ * aviationweather TAF 端点模板(format=raw)。ids 数组逗号连接;date 存在时序列化为查询参数。
153
+ */
154
+ export declare function awTafUrl(ids: string | readonly string[], options?: {
155
+ baseUrl?: string;
156
+ date?: Date | string;
157
+ }): string;
158
+ /**
159
+ * Fetch aviationweather TAFs (raw format) with station extracted and continuation lines merged.
160
+ * 拉取 aviationweather TAF(raw 格式):续行归并 + 站码提取的取数侧整理。
161
+ * 失败语义与 getMetars 同族(不静默):timeout/network/http-error/empty-data 一律 MetarSourceError
162
+ * 机读码;上游对未知 ids 也返回 200 + 空体,空数据显式 throw 而非空数组静默。
163
+ */
164
+ export declare function getTafs(ids: string | readonly string[], options?: GetTafsOptions): Promise<TafObservation[]>;
165
+ /** Options for getTafReports: fetch options plus station metadata and the per-row tolerance callback. */
166
+ export interface GetTafReportsOptions extends GetTafsOptions {
167
+ /** 按 icao 联表的站点元数据(TAF 端点不提供坐标——stations 是定位的唯一来源,缺省将得空数组) */
168
+ stations?: readonly StationRef[];
169
+ /** 逐行容错(缺省关闭):传入时解析失败的行跳过并逐条回调;不传时聚合抛出(全有或全无) */
170
+ onUnparseable?: (failure: UnparseableReport) => void;
171
+ /** 紧凑模式:parseTaf 产物剥除全部 span 字段(存储场景推荐);缺省完整模式 */
172
+ spans?: false;
173
+ }
174
+ /**
175
+ * A map-ready TAF item (structurally compatible with TafLayerItem's report/position/title triple).
176
+ * 可直接喂给地图适配器(如 addTafLayer)的预报项:{ report, position, title } 三件套。
177
+ */
178
+ export interface TafReportItem {
179
+ /** 已解析的 TAF IR(原文经 report.raw 原样保真) */
180
+ report: TafReport;
181
+ /** 站点坐标(WGS-84 [lat, lon];TAF 端点无坐标,来自 stations 联表) */
182
+ position: [number, number];
183
+ /** 站点提示(「ICAO 站名」;适配器缺省回落 IR 站名) */
184
+ title: string;
185
+ }
186
+ /**
187
+ * TAF 版的取数 → 解析 → 定位一步到位(契约沿 getMetarReports):stations 命中的站带精确坐标与站名,
188
+ * 未命中跳过(TAF 端点无坐标可回落,与 getMetarReports 的无坐标跳行同规则);解析失败两种模式
189
+ * (缺省聚合抛出 batch-parse-failed、传 onUnparseable 逐行容错)。报池等数据管理逻辑不进本层。
190
+ */
191
+ export declare function getTafReports(ids: string | readonly string[], options?: GetTafReportsOptions): Promise<TafReportItem[]>;
192
+ /** ogimet display_metars2.php 端点(tipo=FT=TAF;无 CORS 头:浏览器直连须代理/镜像——baseUrl 覆盖即为此用) */
193
+ export declare const OGIMET_TAF_ENDPOINT = "https://www.ogimet.com/display_metars2.php";
194
+ /** ogimet 查询窗(UTC 起止) */
195
+ export interface OgimetTafWindow {
196
+ readonly from: Date;
197
+ readonly to: Date;
198
+ }
199
+ export declare function ogimetTafUrl(station: string, window: OgimetTafWindow, options?: {
200
+ baseUrl?: string;
201
+ }): string;
202
+ /**
203
+ * ogimet 响应解析(<pre> 区):12 位时间戳前缀行(YYYYMMDDHHmm,收报序)起新记录、无前缀行并入上一条
204
+ * (多行报文并单——与私有管线 parse_tafs 同协议);# 注释/空行跳过;只收 TAF 起头报文。
205
+ * 站码 best-effort 取自电头(query 站名兜底);空模板(无 <pre> 或零报文)返回空数组——
206
+ * 由调用方判空(ogimet 缺数常见,属正常降级而非错误面)。
207
+ */
208
+ export declare function parseOgimetTafs(html: string, fallbackStation?: string): TafObservation[];
209
+ /** Options for getTafsOgimet: fetch options plus the ogimet endpoint-root override. */
210
+ export interface GetTafsOgimetOptions {
211
+ /** 外部取消信号(与超时组合,任一触发即中止) */
212
+ signal?: AbortSignal;
213
+ /** 请求超时毫秒数(触发即中止并抛出超时错误) */
214
+ timeoutMs?: number;
215
+ /** ogimet 端点根覆盖(内网镜像/自建代理;examples 走 /ogimet-taf 代理根) */
216
+ baseUrl?: string;
217
+ }
218
+ /**
219
+ * Fetch one station's recent TAFs from ogimet (latest + previous publication cycles).
220
+ * 拉取单站的 ogimet 近窗 TAF(最新与上一发布周期——报池补充线,供与 aviationweather 线合并去重)。
221
+ * 失败语义与 getTafs 同族(timeout/network/http-error 机读码);空数据返回空数组而非抛错
222
+ * (ogimet 站点级缺数常见——它是补充线,空窗属正常降级,不静默丢整批)。
223
+ */
224
+ export declare function getTafsOgimet(station: string, window: OgimetTafWindow, options?: GetTafsOgimetOptions): Promise<TafObservation[]>;
115
225
  //#endregion
116
226
  export { MetarParseError, MetarSourceError };
117
227
  //# sourceMappingURL=sources.d.cts.map
package/dist/sources.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { MetarParseError, MetarParseErrorCode, MetarReport, MetarSourceError } from "@metweave/core";
1
+ import { MetarParseError, MetarParseErrorCode, MetarReport, MetarSourceError, TafReport } from "@metweave/core";
2
2
  //#region src/sources.d.ts
3
3
  /**
4
4
  * One fetched observation row: station, verbatim raw report, and source-provided coordinates (city-level for IEM China rows).
@@ -21,6 +21,11 @@ export interface GetMetarsOptions {
21
21
  signal?: AbortSignal;
22
22
  /** 请求超时毫秒数(触发即中止并抛出超时错误) */
23
23
  timeoutMs?: number;
24
+ /**
25
+ * IEM 站点根覆盖(缺省官方 https://mesonet.agron.iastate.edu):内网镜像/自建网关
26
+ * 只换根——/api/1/currents.json 路径与查询串由本层拼装(换源口径,见文件头)。
27
+ */
28
+ baseUrl?: string;
24
29
  }
25
30
  /**
26
31
  * Station metadata for lookup joins: authoritative coordinates overriding the source's city-level ones.
@@ -90,8 +95,9 @@ export interface MetarReportItem {
90
95
  * The IEM whole-network currents endpoint (CORS-open, direct browser access). Defaults to the China ASOS network (39 stations).
91
96
  * IEM 整网实况端点(CORS 全开,浏览器可直连)。缺省中国 ASOS 网(39 站)。
92
97
  * @param network - IEM network name (e.g. CN__ASOS, RU__ASOS, IA_ASOS). IEM 网络名。
98
+ * @param baseUrl - IEM 站点根覆盖(内网镜像/自建网关——换源口径,见 GetMetarsOptions.baseUrl)。
93
99
  */
94
- export declare function iemCurrentsUrl(network?: string): string;
100
+ export declare function iemCurrentsUrl(network?: string, baseUrl?: string): string;
95
101
  /**
96
102
  * Fetch IEM whole-network current observations (station/raw/coordinates passed through).
97
103
  * 拉取 IEM 整网实况观测(station/raw/坐标透传)。
@@ -112,6 +118,110 @@ export declare function getMetars(network?: string, options?: GetMetarsOptions):
112
118
  * 传 options.onUnparseable 则逐行容错——失败行跳过并逐条回调(可观测),好行照常返回。
113
119
  */
114
120
  export declare function getMetarReports(network?: string, options?: GetMetarReportsOptions): Promise<MetarReportItem[]>;
121
+ /** aviationweather TAF 数据端点(上游无 CORS 头:浏览器直连须自建代理/镜像——baseUrl 覆盖即为此用) */
122
+ export declare const AW_TAF_ENDPOINT = "https://aviationweather.gov/api/data/taf";
123
+ /**
124
+ * One fetched TAF row: station (best-effort) and the verbatim report (continuation lines already merged).
125
+ * 单条 TAF 取数行:站码(best-effort)与报文原文(续行已归并)。
126
+ */
127
+ export interface TafObservation {
128
+ /** 站码:剥 TAF/AMD/COR 电头词后的首枚 4 位码;剥不出为 ""(解析层 missing-station 兜底出声) */
129
+ station: string;
130
+ /** 报文原文(上游 raw 格式的缩进续行已并回所属报文;report.raw 语义一致——原文保真) */
131
+ raw: string;
132
+ }
133
+ /** Options for getTafs: fetch options plus the TAF endpoint-root override and the as-of date. */
134
+ export interface GetTafsOptions {
135
+ /** 外部取消信号(与超时组合,任一触发即中止) */
136
+ signal?: AbortSignal;
137
+ /** 请求超时毫秒数(触发即中止并抛出超时错误) */
138
+ timeoutMs?: number;
139
+ /**
140
+ * TAF 端点根覆盖(缺省官方 https://aviationweather.gov/api/data/taf):内网镜像/自建代理
141
+ * 只换根——?ids=&format=raw 查询串由本层拼装(换源口径,见文件头;examples 即用 /aw-taf 代理根)。
142
+ */
143
+ baseUrl?: string;
144
+ /**
145
+ * 「该时刻已发布的最新报」(上游 date 参数语义):回看上一发布周期用(examples 报池即 date=now-4h)。
146
+ * Date 转 ISO;字符串原样透传(上游亦收相对形态)。
147
+ */
148
+ date?: Date | string;
149
+ }
150
+ /**
151
+ * The aviationweather TAF endpoint (raw format). ids joined as given; date serialized when present.
152
+ * aviationweather TAF 端点模板(format=raw)。ids 数组逗号连接;date 存在时序列化为查询参数。
153
+ */
154
+ export declare function awTafUrl(ids: string | readonly string[], options?: {
155
+ baseUrl?: string;
156
+ date?: Date | string;
157
+ }): string;
158
+ /**
159
+ * Fetch aviationweather TAFs (raw format) with station extracted and continuation lines merged.
160
+ * 拉取 aviationweather TAF(raw 格式):续行归并 + 站码提取的取数侧整理。
161
+ * 失败语义与 getMetars 同族(不静默):timeout/network/http-error/empty-data 一律 MetarSourceError
162
+ * 机读码;上游对未知 ids 也返回 200 + 空体,空数据显式 throw 而非空数组静默。
163
+ */
164
+ export declare function getTafs(ids: string | readonly string[], options?: GetTafsOptions): Promise<TafObservation[]>;
165
+ /** Options for getTafReports: fetch options plus station metadata and the per-row tolerance callback. */
166
+ export interface GetTafReportsOptions extends GetTafsOptions {
167
+ /** 按 icao 联表的站点元数据(TAF 端点不提供坐标——stations 是定位的唯一来源,缺省将得空数组) */
168
+ stations?: readonly StationRef[];
169
+ /** 逐行容错(缺省关闭):传入时解析失败的行跳过并逐条回调;不传时聚合抛出(全有或全无) */
170
+ onUnparseable?: (failure: UnparseableReport) => void;
171
+ /** 紧凑模式:parseTaf 产物剥除全部 span 字段(存储场景推荐);缺省完整模式 */
172
+ spans?: false;
173
+ }
174
+ /**
175
+ * A map-ready TAF item (structurally compatible with TafLayerItem's report/position/title triple).
176
+ * 可直接喂给地图适配器(如 addTafLayer)的预报项:{ report, position, title } 三件套。
177
+ */
178
+ export interface TafReportItem {
179
+ /** 已解析的 TAF IR(原文经 report.raw 原样保真) */
180
+ report: TafReport;
181
+ /** 站点坐标(WGS-84 [lat, lon];TAF 端点无坐标,来自 stations 联表) */
182
+ position: [number, number];
183
+ /** 站点提示(「ICAO 站名」;适配器缺省回落 IR 站名) */
184
+ title: string;
185
+ }
186
+ /**
187
+ * TAF 版的取数 → 解析 → 定位一步到位(契约沿 getMetarReports):stations 命中的站带精确坐标与站名,
188
+ * 未命中跳过(TAF 端点无坐标可回落,与 getMetarReports 的无坐标跳行同规则);解析失败两种模式
189
+ * (缺省聚合抛出 batch-parse-failed、传 onUnparseable 逐行容错)。报池等数据管理逻辑不进本层。
190
+ */
191
+ export declare function getTafReports(ids: string | readonly string[], options?: GetTafReportsOptions): Promise<TafReportItem[]>;
192
+ /** ogimet display_metars2.php 端点(tipo=FT=TAF;无 CORS 头:浏览器直连须代理/镜像——baseUrl 覆盖即为此用) */
193
+ export declare const OGIMET_TAF_ENDPOINT = "https://www.ogimet.com/display_metars2.php";
194
+ /** ogimet 查询窗(UTC 起止) */
195
+ export interface OgimetTafWindow {
196
+ readonly from: Date;
197
+ readonly to: Date;
198
+ }
199
+ export declare function ogimetTafUrl(station: string, window: OgimetTafWindow, options?: {
200
+ baseUrl?: string;
201
+ }): string;
202
+ /**
203
+ * ogimet 响应解析(<pre> 区):12 位时间戳前缀行(YYYYMMDDHHmm,收报序)起新记录、无前缀行并入上一条
204
+ * (多行报文并单——与私有管线 parse_tafs 同协议);# 注释/空行跳过;只收 TAF 起头报文。
205
+ * 站码 best-effort 取自电头(query 站名兜底);空模板(无 <pre> 或零报文)返回空数组——
206
+ * 由调用方判空(ogimet 缺数常见,属正常降级而非错误面)。
207
+ */
208
+ export declare function parseOgimetTafs(html: string, fallbackStation?: string): TafObservation[];
209
+ /** Options for getTafsOgimet: fetch options plus the ogimet endpoint-root override. */
210
+ export interface GetTafsOgimetOptions {
211
+ /** 外部取消信号(与超时组合,任一触发即中止) */
212
+ signal?: AbortSignal;
213
+ /** 请求超时毫秒数(触发即中止并抛出超时错误) */
214
+ timeoutMs?: number;
215
+ /** ogimet 端点根覆盖(内网镜像/自建代理;examples 走 /ogimet-taf 代理根) */
216
+ baseUrl?: string;
217
+ }
218
+ /**
219
+ * Fetch one station's recent TAFs from ogimet (latest + previous publication cycles).
220
+ * 拉取单站的 ogimet 近窗 TAF(最新与上一发布周期——报池补充线,供与 aviationweather 线合并去重)。
221
+ * 失败语义与 getTafs 同族(timeout/network/http-error 机读码);空数据返回空数组而非抛错
222
+ * (ogimet 站点级缺数常见——它是补充线,空窗属正常降级,不静默丢整批)。
223
+ */
224
+ export declare function getTafsOgimet(station: string, window: OgimetTafWindow, options?: GetTafsOgimetOptions): Promise<TafObservation[]>;
115
225
  //#endregion
116
226
  export { MetarParseError, MetarSourceError };
117
227
  //# sourceMappingURL=sources.d.ts.map
package/dist/sources.js CHANGED
@@ -1,14 +1,15 @@
1
1
  import { MetarParseError, MetarParseError as MetarParseError$1, MetarSourceError, MetarSourceError as MetarSourceError$1 } from "@metweave/core";
2
- import { parse } from "@metweave/parser";
2
+ import { parse, parseTaf } from "@metweave/parser";
3
3
  //#region src/sources.ts
4
4
  const IEM_BASE = "https://mesonet.agron.iastate.edu";
5
5
  /**
6
6
  * The IEM whole-network currents endpoint (CORS-open, direct browser access). Defaults to the China ASOS network (39 stations).
7
7
  * IEM 整网实况端点(CORS 全开,浏览器可直连)。缺省中国 ASOS 网(39 站)。
8
8
  * @param network - IEM network name (e.g. CN__ASOS, RU__ASOS, IA_ASOS). IEM 网络名。
9
+ * @param baseUrl - IEM 站点根覆盖(内网镜像/自建网关——换源口径,见 GetMetarsOptions.baseUrl)。
9
10
  */
10
- function iemCurrentsUrl(network = "CN__ASOS") {
11
- return `${IEM_BASE}/api/1/currents.json?network=${encodeURIComponent(network)}`;
11
+ function iemCurrentsUrl(network = "CN__ASOS", baseUrl = IEM_BASE) {
12
+ return `${baseUrl}/api/1/currents.json?network=${encodeURIComponent(network)}`;
12
13
  }
13
14
  /** 观测记录守卫:station/raw 均须为字符串(只查键不查类型是旧洞)。 */
14
15
  function isObservation(v) {
@@ -38,7 +39,7 @@ async function getMetars(network = "CN__ASOS", options = {}) {
38
39
  try {
39
40
  let res;
40
41
  try {
41
- res = await fetch(iemCurrentsUrl(network), { signal: controller.signal });
42
+ res = await fetch(iemCurrentsUrl(network, options.baseUrl), { signal: controller.signal });
42
43
  } catch (err) {
43
44
  if (controller.signal.aborted) throw err;
44
45
  const reason = err instanceof Error ? err.message : String(err);
@@ -110,7 +111,229 @@ async function getMetarReports(network = "CN__ASOS", options = {}) {
110
111
  }
111
112
  return items;
112
113
  }
114
+ /** aviationweather TAF 数据端点(上游无 CORS 头:浏览器直连须自建代理/镜像——baseUrl 覆盖即为此用) */
115
+ const AW_TAF_ENDPOINT = "https://aviationweather.gov/api/data/taf";
116
+ /**
117
+ * The aviationweather TAF endpoint (raw format). ids joined as given; date serialized when present.
118
+ * aviationweather TAF 端点模板(format=raw)。ids 数组逗号连接;date 存在时序列化为查询参数。
119
+ */
120
+ function awTafUrl(ids, options = {}) {
121
+ const idList = typeof ids === "string" ? ids : ids.join(",");
122
+ const params = new URLSearchParams({
123
+ ids: idList,
124
+ format: "raw"
125
+ });
126
+ const d = options.date;
127
+ if (d !== void 0) params.set("date", d instanceof Date ? d.toISOString() : d);
128
+ return `${options.baseUrl ?? "https://aviationweather.gov/api/data/taf"}?${params.toString()}`;
129
+ }
130
+ /** raw 文本 → 整份报文行(缩进续行并回上一份;与报池语义同源)+ best-effort 站码提取 */
131
+ function mergeTafLines(text) {
132
+ const merged = [];
133
+ for (const line of text.split("\n")) {
134
+ if (line.trim() === "") continue;
135
+ if (/^\s/.test(line) && merged.length > 0) merged[merged.length - 1] += ` ${line.trim()}`;
136
+ else merged.push(line.trim());
137
+ }
138
+ return merged.map((raw) => {
139
+ const toks = raw.split(/\s+/);
140
+ let k = toks[0] === "TAF" ? 1 : 0;
141
+ while (toks[k] === "AMD" || toks[k] === "COR") k += 1;
142
+ const head = toks[k] ?? "";
143
+ return {
144
+ station: /^[A-Z0-9]{4}$/.test(head) ? head : "",
145
+ raw
146
+ };
147
+ });
148
+ }
149
+ /**
150
+ * Fetch aviationweather TAFs (raw format) with station extracted and continuation lines merged.
151
+ * 拉取 aviationweather TAF(raw 格式):续行归并 + 站码提取的取数侧整理。
152
+ * 失败语义与 getMetars 同族(不静默):timeout/network/http-error/empty-data 一律 MetarSourceError
153
+ * 机读码;上游对未知 ids 也返回 200 + 空体,空数据显式 throw 而非空数组静默。
154
+ */
155
+ async function getTafs(ids, options = {}) {
156
+ const controller = new AbortController();
157
+ const external = options.signal;
158
+ const forward = () => {
159
+ controller.abort(external?.reason);
160
+ };
161
+ if (external !== void 0) {
162
+ if (external.aborted) forward();
163
+ else external.addEventListener("abort", forward, { once: true });
164
+ }
165
+ const timer = options.timeoutMs === void 0 ? void 0 : setTimeout(() => controller.abort(new MetarSourceError$1("timeout", "aviationweather", `aviationweather TAF 请求超时(>${options.timeoutMs}ms)`)), options.timeoutMs);
166
+ try {
167
+ let res;
168
+ try {
169
+ res = await fetch(awTafUrl(ids, options), { signal: controller.signal });
170
+ } catch (err) {
171
+ if (controller.signal.aborted) throw err;
172
+ const reason = err instanceof Error ? err.message : String(err);
173
+ throw new MetarSourceError$1("network", "aviationweather", `网络请求失败(源:aviationweather TAF):请检查网络连通性后重试(${reason})`, { cause: err });
174
+ }
175
+ if (!res.ok) throw new MetarSourceError$1("http-error", "aviationweather", `aviationweather TAF HTTP ${res.status}`);
176
+ let text;
177
+ try {
178
+ text = await res.text();
179
+ } catch (err) {
180
+ if (controller.signal.aborted) throw err;
181
+ const reason = err instanceof Error ? err.message : String(err);
182
+ throw new MetarSourceError$1("network", "aviationweather", `aviationweather TAF 响应读取失败:${reason}`, { cause: err });
183
+ }
184
+ const rows = mergeTafLines(text);
185
+ if (rows.length === 0) throw new MetarSourceError$1("empty-data", "aviationweather", "aviationweather TAF 返回空数据——请核对 ids 站码(如 ZBAA,ZBAD;端点 format=raw)");
186
+ return rows;
187
+ } finally {
188
+ if (timer !== void 0) clearTimeout(timer);
189
+ external?.removeEventListener("abort", forward);
190
+ }
191
+ }
192
+ /**
193
+ * TAF 版的取数 → 解析 → 定位一步到位(契约沿 getMetarReports):stations 命中的站带精确坐标与站名,
194
+ * 未命中跳过(TAF 端点无坐标可回落,与 getMetarReports 的无坐标跳行同规则);解析失败两种模式
195
+ * (缺省聚合抛出 batch-parse-failed、传 onUnparseable 逐行容错)。报池等数据管理逻辑不进本层。
196
+ */
197
+ async function getTafReports(ids, options = {}) {
198
+ const { stations, onUnparseable, spans, ...fetchOptions } = options;
199
+ const table = new Map((stations ?? []).map((s) => [s.icao, s]));
200
+ const observations = await getTafs(ids, fetchOptions);
201
+ const items = [];
202
+ const failures = [];
203
+ for (const obs of observations) try {
204
+ const report = parseTaf(obs.raw, spans === false ? { spans: false } : void 0);
205
+ const station = table.get(report.station);
206
+ if (station === void 0) continue;
207
+ items.push({
208
+ report,
209
+ position: [station.lat, station.lon],
210
+ title: `${report.station}${station.name ? ` ${station.name}` : ""}`
211
+ });
212
+ } catch (err) {
213
+ if (!(err instanceof MetarParseError$1)) throw err;
214
+ if (onUnparseable !== void 0) {
215
+ onUnparseable({
216
+ station: obs.station,
217
+ raw: obs.raw,
218
+ code: err.code,
219
+ error: err.message
220
+ });
221
+ continue;
222
+ }
223
+ failures.push(`${obs.station}: ${err.message}`);
224
+ }
225
+ if (failures.length > 0) {
226
+ const summary = `source=aviationweather; failures=${failures.length}; ${failures.join("; ")}`;
227
+ throw new MetarParseError$1("batch-parse-failed", summary, `TAF 解析失败 ${failures.length} 条(aviationweather)——${failures.slice(0, 3).join(";")}${failures.length > 3 ? "……" : ""}`);
228
+ }
229
+ return items;
230
+ }
231
+ /** ogimet display_metars2.php 端点(tipo=FT=TAF;无 CORS 头:浏览器直连须代理/镜像——baseUrl 覆盖即为此用) */
232
+ const OGIMET_TAF_ENDPOINT = "https://www.ogimet.com/display_metars2.php";
233
+ /**
234
+ * ogimet TAF 端点模板(配方=私有管线 backfill_ogimet.py 同源):tipo=FT、txt 形、
235
+ * 服务端按 <pre> 区回 HTML(解析见 parseOgimetTafs)。免费公益服务——调用方自行节制频率。
236
+ */
237
+ /** UTC 日期字段补零组(ogimet 查询串的 ano/mes/day/hora/min 族) */
238
+ const ogimetFields = (d) => ({
239
+ ano: `${d.getUTCFullYear()}`,
240
+ mes: `${d.getUTCMonth() + 1}`.padStart(2, "0"),
241
+ day: `${d.getUTCDate()}`.padStart(2, "0"),
242
+ hora: `${d.getUTCHours()}`.padStart(2, "0"),
243
+ min: `${d.getUTCMinutes()}`.padStart(2, "0")
244
+ });
245
+ function ogimetTafUrl(station, window, options = {}) {
246
+ const a = ogimetFields(window.from);
247
+ const b = ogimetFields(window.to);
248
+ const qs = new URLSearchParams({
249
+ lang: "en",
250
+ lugar: station,
251
+ tipo: "FT",
252
+ ord: "DIR",
253
+ nil: "SI",
254
+ fmt: "txt",
255
+ ano: a.ano,
256
+ mes: a.mes,
257
+ day: a.day,
258
+ hora: a.hora,
259
+ min: a.min,
260
+ anof: b.ano,
261
+ mesf: b.mes,
262
+ dayf: b.day,
263
+ horaf: b.hora,
264
+ minf: b.min,
265
+ send: "send",
266
+ nohtmlo: "yes",
267
+ annot: "yes",
268
+ leng: "0"
269
+ });
270
+ return `${options.baseUrl ?? "https://www.ogimet.com/display_metars2.php"}?${qs.toString()}`;
271
+ }
272
+ /** 电头站码 best-effort 提取(TAF/AMD/COR 剥词;剥不出返回空串)——与 mergeTafLines 同判式(aviationweather 取数线) */
273
+ const stationHeadOf = (raw) => {
274
+ const toks = raw.split(/\s+/);
275
+ let k = toks[0] === "TAF" ? 1 : 0;
276
+ while (toks[k] === "AMD" || toks[k] === "COR") k += 1;
277
+ const head = toks[k] ?? "";
278
+ return /^[A-Z0-9]{4}$/.test(head) ? head : "";
279
+ };
280
+ /**
281
+ * ogimet 响应解析(<pre> 区):12 位时间戳前缀行(YYYYMMDDHHmm,收报序)起新记录、无前缀行并入上一条
282
+ * (多行报文并单——与私有管线 parse_tafs 同协议);# 注释/空行跳过;只收 TAF 起头报文。
283
+ * 站码 best-effort 取自电头(query 站名兜底);空模板(无 <pre> 或零报文)返回空数组——
284
+ * 由调用方判空(ogimet 缺数常见,属正常降级而非错误面)。
285
+ */
286
+ function parseOgimetTafs(html, fallbackStation = "") {
287
+ const pre = /<pre>([\s\S]*?)<\/pre>/.exec(html);
288
+ if (pre === null) return [];
289
+ const PREFIX = /^(\d{12})\s+(.*)$/;
290
+ const merged = [];
291
+ for (const line of (pre[1] ?? "").split("\n")) {
292
+ const t = line.trim();
293
+ if (t === "" || t.startsWith("#")) continue;
294
+ const pm = PREFIX.exec(t);
295
+ if (pm !== null) merged.push(pm[2] ?? "");
296
+ else if (merged.length > 0) merged[merged.length - 1] += ` ${t}`;
297
+ }
298
+ return merged.filter((raw) => raw.startsWith("TAF")).map((raw) => ({
299
+ station: stationHeadOf(raw) || fallbackStation,
300
+ raw
301
+ }));
302
+ }
303
+ /**
304
+ * Fetch one station's recent TAFs from ogimet (latest + previous publication cycles).
305
+ * 拉取单站的 ogimet 近窗 TAF(最新与上一发布周期——报池补充线,供与 aviationweather 线合并去重)。
306
+ * 失败语义与 getTafs 同族(timeout/network/http-error 机读码);空数据返回空数组而非抛错
307
+ * (ogimet 站点级缺数常见——它是补充线,空窗属正常降级,不静默丢整批)。
308
+ */
309
+ async function getTafsOgimet(station, window, options = {}) {
310
+ const controller = new AbortController();
311
+ const external = options.signal;
312
+ const forward = () => {
313
+ controller.abort(external?.reason);
314
+ };
315
+ if (external !== void 0) {
316
+ if (external.aborted) forward();
317
+ else external.addEventListener("abort", forward, { once: true });
318
+ }
319
+ const timer = options.timeoutMs === void 0 ? void 0 : setTimeout(() => controller.abort(new MetarSourceError$1("timeout", "ogimet", `ogimet TAF 请求超时(>${options.timeoutMs}ms,站=${station})`)), options.timeoutMs);
320
+ try {
321
+ let res;
322
+ try {
323
+ res = await fetch(ogimetTafUrl(station, window, options), { signal: controller.signal });
324
+ } catch (err) {
325
+ if (controller.signal.aborted) throw err;
326
+ const reason = err instanceof Error ? err.message : String(err);
327
+ throw new MetarSourceError$1("network", "ogimet", `网络请求失败(源:ogimet TAF,站=${station}):${reason}`, { cause: err });
328
+ }
329
+ if (!res.ok) throw new MetarSourceError$1("http-error", "ogimet", `ogimet TAF HTTP ${res.status}(站=${station})`);
330
+ return parseOgimetTafs(await res.text(), station);
331
+ } finally {
332
+ if (timer !== void 0) clearTimeout(timer);
333
+ external?.removeEventListener("abort", forward);
334
+ }
335
+ }
113
336
  //#endregion
114
- export { MetarParseError, MetarSourceError, getMetarReports, getMetars, iemCurrentsUrl };
337
+ export { AW_TAF_ENDPOINT, MetarParseError, MetarSourceError, OGIMET_TAF_ENDPOINT, awTafUrl, getMetarReports, getMetars, getTafReports, getTafs, getTafsOgimet, iemCurrentsUrl, ogimetTafUrl, parseOgimetTafs };
115
338
 
116
339
  //# sourceMappingURL=sources.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"sources.js","names":["MetarSourceError","MetarParseError"],"sources":["../src/sources.ts"],"sourcesContent":["/**\n * metweave/sources — 取数 helper(伞包子路径)。\n *\n * 纪律:只做端点模板 + fetch + 取数侧整理(解析、定位联表)——缓存/质控/归档等\n * 数据管理逻辑永不进入开源包;核心包(core/parser/render)零网络代码。\n * 不静默:HTTP 失败、响应 schema 不符、报文解析失败一律 throw,绝不吞错返回空数组。\n * 错误面机读化:五路失败一律抛 MetarSourceError(code 稳定契约 + network 网络名字段;\n * message 权威文案为中文;英文用 @metweave/core 导出的 EN_MESSAGES[code] 查表,\n * 或渲染层 locale:\"en\" 整卡切换(card locale 已内置 code→英文映射))。\n */\nimport type { MetarReport } from \"@metweave/core\";\nimport type { MetarParseErrorCode } from \"@metweave/core\";\nimport { MetarParseError, MetarSourceError } from \"@metweave/core\";\nimport { parse } from \"@metweave/parser\";\n\n// 错误类复出(API 完整性):消费方从 sources 子路径 import 即可 catch 两类错误,\n// 不必再从伞包主入口双入口引入(类实体与 core 同一,instanceof 双向成立)。\nexport { MetarParseError, MetarSourceError } from \"@metweave/core\";\n\n/**\n * One fetched observation row: station, verbatim raw report, and source-provided coordinates (city-level for IEM China rows).\n * 单条取数观测行:站名、原样报文、源站坐标(IEM 中国行为城市级粗坐标)。\n */\nexport interface MetarObservation {\n /** 站名(ICAO) */\n station: string;\n /** 报文原文(IEM 通路无类型词,IEM 自带坐标为城市级粗坐标——定位请使用站点元数据) */\n raw: string;\n lat: number | null;\n lon: number | null;\n}\n\n/**\n * Options for getMetars: external abort signal and request timeout (either firing aborts).\n * getMetars 的选项:外部取消信号与请求超时(任一触发即中止)。\n */\nexport interface GetMetarsOptions {\n /** 外部取消信号(与超时组合,任一触发即中止) */\n signal?: AbortSignal;\n /** 请求超时毫秒数(触发即中止并抛出超时错误) */\n timeoutMs?: number;\n}\n\n/**\n * Station metadata for lookup joins: authoritative coordinates overriding the source's city-level ones.\n * 站点元数据(联表用):坐标为权威定位源,覆盖源站的城市级粗坐标。\n */\nexport interface StationRef {\n /** 站名(ICAO) */\n icao: string;\n /** 纬度(WGS-84) */\n lat: number;\n /** 经度(WGS-84) */\n lon: number;\n /** 站点名称(进 tooltip 标题) */\n name?: string;\n}\n\n/**\n * Per-row failure detail (the onUnparseable callback argument): one observation row failed to parse wholesale (missing station/time etc.).\n * 逐行容错的失败详情(onUnparseable 回调入参):一行观测报文解析整体失败(站名/时组缺失等)。\n * `code` is the machine-readable failure code (MetarParseError's stable contract); `error` is the narrative message.\n * code 是机读失败码(MetarParseError 的稳定契约),error 是中文 message。\n */\nexport interface UnparseableReport {\n /** 源站站名(IEM 行的 station 字段) */\n station: string;\n /** 报文原文 */\n raw: string;\n /** 机读失败码(missing-station / missing-time / invalid-time) */\n code: MetarParseErrorCode;\n /** 解析失败原因(Error.message,中文) */\n error: string;\n}\n\n/**\n * Options for getMetarReports: fetch options plus station metadata and the per-row tolerance callback.\n * getMetarReports 的选项:取数选项 + 站点联表元数据 + 逐行容错回调。\n */\nexport interface GetMetarReportsOptions extends GetMetarsOptions {\n /** 按 icao 联表的站点元数据:命中则用其精确坐标与名称,未命中回落源站坐标 */\n stations?: readonly StationRef[];\n /**\n * 逐行容错(缺省关闭):传入时解析失败的行跳过并逐条回调——失败可观测,不静默;\n * 不传时任一失败仍聚合抛出(默认行为不变,全有或全无)。\n */\n onUnparseable?: (failure: UnparseableReport) => void;\n /**\n * 紧凑模式:parse 产物剥除全部 span 字段(存储体积约 -32%,入库供屏场景推荐);\n * 缺省 undefined = 完整模式(span 在位,原文回溯用)。\n */\n spans?: false;\n}\n\n/**\n * A map-ready report item (structurally compatible with MetarLayerItem): the { report, position, title } triple —\n * report is the parsed IR (raw text preserved verbatim on report.raw; field values via toValues(report)),\n * position is WGS-84 [lat, lon], title is the hover title.\n * 可直接喂给地图适配器(如 addMetarLayer)的报文项(与 MetarLayerItem 结构兼容):\n * `{ report, position, title }` 三件套——report 是已解析的 IR(报文原文在 report.raw 原样保真,\n * 需要字段值走 toValues(report)),position 是 WGS-84 [lat, lon],title 是悬浮提示标题。\n */\nexport interface MetarReportItem {\n /** 已解析的报文 IR(原文经 report.raw 原样保真回溯) */\n report: MetarReport;\n /** 站点坐标(WGS-84 [lat, lon]) */\n position: [number, number];\n /** 站点提示(「ICAO 站名」;适配器缺省回落 IR 站名) */\n title: string;\n}\n\nconst IEM_BASE = \"https://mesonet.agron.iastate.edu\";\n\n/**\n * The IEM whole-network currents endpoint (CORS-open, direct browser access). Defaults to the China ASOS network (39 stations).\n * IEM 整网实况端点(CORS 全开,浏览器可直连)。缺省中国 ASOS 网(39 站)。\n * @param network - IEM network name (e.g. CN__ASOS, RU__ASOS, IA_ASOS). IEM 网络名。\n */\nexport function iemCurrentsUrl(network = \"CN__ASOS\"): string {\n return `${IEM_BASE}/api/1/currents.json?network=${encodeURIComponent(network)}`;\n}\n\n/** 观测记录守卫:station/raw 均须为字符串(只查键不查类型是旧洞)。 */\nfunction isObservation(v: unknown): v is MetarObservation {\n if (typeof v !== \"object\" || v === null) return false;\n const rec = v as { station?: unknown; raw?: unknown };\n return typeof rec.station === \"string\" && typeof rec.raw === \"string\";\n}\n\n/**\n * Fetch IEM whole-network current observations (station/raw/coordinates passed through).\n * 拉取 IEM 整网实况观测(station/raw/坐标透传)。\n * No-silent-failure discipline on the fetch side / 不静默纪律的取数面:\n * - 空数组是错误网络名的典型形态(IEM 对未知 network 返回 200 + 空 data,合法网络不会返回空)——显式 throw;\n * - fetch 网络层失败(断网/DNS)包装为中文可读错误(原始 message 保留在文末与 cause);\n * - 超时/外部 AbortSignal 触发的拒绝不包装、原样透传(保持取消语义)。\n */\nexport async function getMetars(\n network = \"CN__ASOS\",\n options: GetMetarsOptions = {},\n): Promise<MetarObservation[]> {\n // 手工组合超时与外部信号(不依赖 AbortSignal.any:兼容 Node 20 全系;setTimeout 可被测试假时钟接管)\n const controller = new AbortController();\n const external = options.signal;\n const forward = (): void => {\n controller.abort(external?.reason);\n };\n if (external !== undefined) {\n if (external.aborted) forward();\n else external.addEventListener(\"abort\", forward, { once: true });\n }\n const timer =\n options.timeoutMs === undefined\n ? undefined\n : setTimeout(\n () =>\n controller.abort(\n // 超时中止原因即最终抛出的错误:code \"timeout\"(外部 signal 取消不在此列,reason 原样透传)\n new MetarSourceError(\n \"timeout\",\n network,\n `IEM 请求超时(>${options.timeoutMs}ms,network=${network})`,\n ),\n ),\n options.timeoutMs,\n );\n try {\n let res: Response;\n try {\n res = await fetch(iemCurrentsUrl(network), { signal: controller.signal });\n } catch (err) {\n // 取消/超时引发的拒绝(controller 已中止)原样透传——保持 AbortSignal 语义\n //(超时场景透传的即上方 MetarSourceError \"timeout\");\n // 其余(断网/DNS 的 TypeError 等)包装为 MetarSourceError \"network\",原始 message 保留在文末与 cause\n if (controller.signal.aborted) throw err;\n const reason = err instanceof Error ? err.message : String(err);\n throw new MetarSourceError(\n \"network\",\n network,\n `网络请求失败(源:IEM,network=${network}):请检查网络连通性后重试(${reason})`,\n { cause: err },\n );\n }\n if (!res.ok) {\n throw new MetarSourceError(\n \"http-error\",\n network,\n `IEM HTTP ${res.status}(network=${network})`,\n );\n }\n let body: unknown;\n try {\n body = await res.json();\n } catch (err) {\n // HTTP 200 但响应体不是 JSON(企业代理/防火墙拦截页的典型形态)——裸 SyntaxError 打穿属错误面缺口;\n // 归入 bad-schema 机读码,人话提示指向代理环境。中止引发的拒绝不在此列(保持取消语义透传)\n if (controller.signal.aborted) throw err;\n const reason = err instanceof Error ? err.message : String(err);\n throw new MetarSourceError(\n \"bad-schema\",\n network,\n `IEM 响应不是合法 JSON(可能被代理/防火墙拦截,network=${network}):${reason}`,\n { cause: err },\n );\n }\n if (\n typeof body !== \"object\" ||\n body === null ||\n !(\"data\" in body) ||\n !Array.isArray(body.data)\n ) {\n throw new MetarSourceError(\n \"bad-schema\",\n network,\n `IEM 响应异常:缺少 data 数组(network=${network},schema 不符)`,\n );\n }\n const rows = body.data;\n if (!rows.every(isObservation)) {\n throw new MetarSourceError(\n \"bad-schema\",\n network,\n `IEM 响应异常:data 存在 station/raw 非字符串的记录(network=${network},schema 不符)`,\n );\n }\n if (rows.length === 0) {\n // 合法网络不会返回空(整网实况端点)——空 data 几乎必然是网络名写错(IEM 对未知网络名返回 200 + 空数组)\n throw new MetarSourceError(\n \"empty-data\",\n network,\n `IEM 返回空数据(network=${network})——请核对 IEM 网络名(如 CN__ASOS/RU__ASOS,参考 https://mesonet.agron.iastate.edu/sites/networks.php)`,\n );\n }\n return rows;\n } finally {\n if (timer !== undefined) clearTimeout(timer);\n external?.removeEventListener(\"abort\", forward);\n }\n}\n\n/**\n * Fetch → parse → locate in one step: returns map-ready report items.\n * 取数 → 解析 → 定位一步到位:返回可直接喂给地图适配器的报文项数组。\n *\n * 定位规则:stations 命中的站用元数据精确坐标(并带站名),未命中的回落源站坐标\n * (IEM 中国站为城市级粗坐标,严肃场景请传站点元数据——见 examples 的 stations.json 用法);\n * 源站坐标为 null 且无元数据的行跳过。\n * 失败语义(不静默,两种模式):缺省任一报文解析整体失败(站名/时组缺失)即聚合抛出,绝不静默丢行;\n * 传 options.onUnparseable 则逐行容错——失败行跳过并逐条回调(可观测),好行照常返回。\n */\nexport async function getMetarReports(\n network = \"CN__ASOS\",\n options: GetMetarReportsOptions = {},\n): Promise<MetarReportItem[]> {\n const { stations, onUnparseable, spans, ...fetchOptions } = options;\n const table = new Map((stations ?? []).map((s) => [s.icao, s] as const));\n const observations = await getMetars(network, fetchOptions);\n const items: MetarReportItem[] = [];\n const failures: string[] = [];\n for (const obs of observations) {\n const station = table.get(obs.station);\n const lat = station?.lat ?? obs.lat;\n const lon = station?.lon ?? obs.lon;\n if (lat === null || lon === null) continue;\n try {\n items.push({\n report: parse(obs.raw, spans === false ? { spans: false } : undefined),\n position: [lat, lon],\n title: `${obs.station}${station?.name ? ` ${station.name}` : \"\"}`,\n });\n } catch (err) {\n // parse 对字符串输入只抛 MetarParseError(code 稳定契约);其他异常属编程错误,直接上抛不降级\n if (!(err instanceof MetarParseError)) throw err;\n if (onUnparseable !== undefined) {\n onUnparseable({ station: obs.station, raw: obs.raw, code: err.code, error: err.message });\n continue;\n }\n failures.push(`${obs.station}: ${err.message}`);\n }\n }\n if (failures.length > 0) {\n // 聚合错误机读化(原裸 Error):code 'batch-parse-failed' 稳定契约;raw 字段在此承载\n // 汇总信息(网络名/条数/逐条站名与原因)而非单条报文原文——语义调整见 core/errors.ts 注释\n const summary = `network=${network}; failures=${failures.length}; ${failures.join(\"; \")}`;\n throw new MetarParseError(\n \"batch-parse-failed\",\n summary,\n `报文解析失败 ${failures.length} 条(network=${network})——${failures.slice(0, 3).join(\";\")}${failures.length > 3 ? \"……\" : \"\"}`,\n );\n }\n return items;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"sources.js","names":["MetarSourceError","MetarParseError"],"sources":["../src/sources.ts"],"sourcesContent":["/**\n * metweave/sources — 取数 helper(伞包子路径)。双线:IEM METAR 实况 + aviationweather TAF 预报(主)+ ogimet TAF 报池补充(次)。\n *\n * 纪律:只做端点模板 + fetch + 取数侧整理(解析、定位联表)——缓存/质控/归档等\n * 数据管理逻辑永不进入开源包;核心包(core/parser/render)零网络代码。\n * 换源口径(内网的人切换到自己的数据源」指令):两条线各留 baseUrl 覆盖位\n * (IEM=站点根、TAF=端点根)——内网镜像/自建网关只换根、路径与查询串由本层拼装。\n * 不静默:HTTP 失败、响应 schema 不符、报文解析失败一律 throw,绝不吞错返回空数组。\n * 错误面机读化:五路失败一律抛 MetarSourceError(code 稳定契约 + network 网络名字段;\n * message 权威文案为中文;英文用 @metweave/core 导出的 EN_MESSAGES[code] 查表,\n * 或渲染层 locale:\"en\" 整卡切换(card locale 已内置 code→英文映射))。\n */\nimport type { MetarReport, TafReport } from \"@metweave/core\";\nimport type { MetarParseErrorCode } from \"@metweave/core\";\nimport { MetarParseError, MetarSourceError } from \"@metweave/core\";\nimport { parse, parseTaf } from \"@metweave/parser\";\n\n// 错误类复出(API 完整性):消费方从 sources 子路径 import 即可 catch 两类错误,\n// 不必再从伞包主入口双入口引入(类实体与 core 同一,instanceof 双向成立)。\nexport { MetarParseError, MetarSourceError } from \"@metweave/core\";\n\n/**\n * One fetched observation row: station, verbatim raw report, and source-provided coordinates (city-level for IEM China rows).\n * 单条取数观测行:站名、原样报文、源站坐标(IEM 中国行为城市级粗坐标)。\n */\nexport interface MetarObservation {\n /** 站名(ICAO) */\n station: string;\n /** 报文原文(IEM 通路无类型词,IEM 自带坐标为城市级粗坐标——定位请使用站点元数据) */\n raw: string;\n lat: number | null;\n lon: number | null;\n}\n\n/**\n * Options for getMetars: external abort signal and request timeout (either firing aborts).\n * getMetars 的选项:外部取消信号与请求超时(任一触发即中止)。\n */\nexport interface GetMetarsOptions {\n /** 外部取消信号(与超时组合,任一触发即中止) */\n signal?: AbortSignal;\n /** 请求超时毫秒数(触发即中止并抛出超时错误) */\n timeoutMs?: number;\n /**\n * IEM 站点根覆盖(缺省官方 https://mesonet.agron.iastate.edu):内网镜像/自建网关\n * 只换根——/api/1/currents.json 路径与查询串由本层拼装(换源口径,见文件头)。\n */\n baseUrl?: string;\n}\n\n/**\n * Station metadata for lookup joins: authoritative coordinates overriding the source's city-level ones.\n * 站点元数据(联表用):坐标为权威定位源,覆盖源站的城市级粗坐标。\n */\nexport interface StationRef {\n /** 站名(ICAO) */\n icao: string;\n /** 纬度(WGS-84) */\n lat: number;\n /** 经度(WGS-84) */\n lon: number;\n /** 站点名称(进 tooltip 标题) */\n name?: string;\n}\n\n/**\n * Per-row failure detail (the onUnparseable callback argument): one observation row failed to parse wholesale (missing station/time etc.).\n * 逐行容错的失败详情(onUnparseable 回调入参):一行观测报文解析整体失败(站名/时组缺失等)。\n * `code` is the machine-readable failure code (MetarParseError's stable contract); `error` is the narrative message.\n * code 是机读失败码(MetarParseError 的稳定契约),error 是中文 message。\n */\nexport interface UnparseableReport {\n /** 源站站名(IEM 行的 station 字段) */\n station: string;\n /** 报文原文 */\n raw: string;\n /** 机读失败码(missing-station / missing-time / invalid-time) */\n code: MetarParseErrorCode;\n /** 解析失败原因(Error.message,中文) */\n error: string;\n}\n\n/**\n * Options for getMetarReports: fetch options plus station metadata and the per-row tolerance callback.\n * getMetarReports 的选项:取数选项 + 站点联表元数据 + 逐行容错回调。\n */\nexport interface GetMetarReportsOptions extends GetMetarsOptions {\n /** 按 icao 联表的站点元数据:命中则用其精确坐标与名称,未命中回落源站坐标 */\n stations?: readonly StationRef[];\n /**\n * 逐行容错(缺省关闭):传入时解析失败的行跳过并逐条回调——失败可观测,不静默;\n * 不传时任一失败仍聚合抛出(默认行为不变,全有或全无)。\n */\n onUnparseable?: (failure: UnparseableReport) => void;\n /**\n * 紧凑模式:parse 产物剥除全部 span 字段(存储体积约 -32%,入库供屏场景推荐);\n * 缺省 undefined = 完整模式(span 在位,原文回溯用)。\n */\n spans?: false;\n}\n\n/**\n * A map-ready report item (structurally compatible with MetarLayerItem): the { report, position, title } triple —\n * report is the parsed IR (raw text preserved verbatim on report.raw; field values via toValues(report)),\n * position is WGS-84 [lat, lon], title is the hover title.\n * 可直接喂给地图适配器(如 addMetarLayer)的报文项(与 MetarLayerItem 结构兼容):\n * `{ report, position, title }` 三件套——report 是已解析的 IR(报文原文在 report.raw 原样保真,\n * 需要字段值走 toValues(report)),position 是 WGS-84 [lat, lon],title 是悬浮提示标题。\n */\nexport interface MetarReportItem {\n /** 已解析的报文 IR(原文经 report.raw 原样保真回溯) */\n report: MetarReport;\n /** 站点坐标(WGS-84 [lat, lon]) */\n position: [number, number];\n /** 站点提示(「ICAO 站名」;适配器缺省回落 IR 站名) */\n title: string;\n}\n\nconst IEM_BASE = \"https://mesonet.agron.iastate.edu\";\n\n/**\n * The IEM whole-network currents endpoint (CORS-open, direct browser access). Defaults to the China ASOS network (39 stations).\n * IEM 整网实况端点(CORS 全开,浏览器可直连)。缺省中国 ASOS 网(39 站)。\n * @param network - IEM network name (e.g. CN__ASOS, RU__ASOS, IA_ASOS). IEM 网络名。\n * @param baseUrl - IEM 站点根覆盖(内网镜像/自建网关——换源口径,见 GetMetarsOptions.baseUrl)。\n */\nexport function iemCurrentsUrl(network = \"CN__ASOS\", baseUrl = IEM_BASE): string {\n return `${baseUrl}/api/1/currents.json?network=${encodeURIComponent(network)}`;\n}\n\n/** 观测记录守卫:station/raw 均须为字符串(只查键不查类型是旧洞)。 */\nfunction isObservation(v: unknown): v is MetarObservation {\n if (typeof v !== \"object\" || v === null) return false;\n const rec = v as { station?: unknown; raw?: unknown };\n return typeof rec.station === \"string\" && typeof rec.raw === \"string\";\n}\n\n/**\n * Fetch IEM whole-network current observations (station/raw/coordinates passed through).\n * 拉取 IEM 整网实况观测(station/raw/坐标透传)。\n * No-silent-failure discipline on the fetch side / 不静默纪律的取数面:\n * - 空数组是错误网络名的典型形态(IEM 对未知 network 返回 200 + 空 data,合法网络不会返回空)——显式 throw;\n * - fetch 网络层失败(断网/DNS)包装为中文可读错误(原始 message 保留在文末与 cause);\n * - 超时/外部 AbortSignal 触发的拒绝不包装、原样透传(保持取消语义)。\n */\nexport async function getMetars(\n network = \"CN__ASOS\",\n options: GetMetarsOptions = {},\n): Promise<MetarObservation[]> {\n // 手工组合超时与外部信号(不依赖 AbortSignal.any:兼容 Node 20 全系;setTimeout 可被测试假时钟接管)\n const controller = new AbortController();\n const external = options.signal;\n const forward = (): void => {\n controller.abort(external?.reason);\n };\n if (external !== undefined) {\n if (external.aborted) forward();\n else external.addEventListener(\"abort\", forward, { once: true });\n }\n const timer =\n options.timeoutMs === undefined\n ? undefined\n : setTimeout(\n () =>\n controller.abort(\n // 超时中止原因即最终抛出的错误:code \"timeout\"(外部 signal 取消不在此列,reason 原样透传)\n new MetarSourceError(\n \"timeout\",\n network,\n `IEM 请求超时(>${options.timeoutMs}ms,network=${network})`,\n ),\n ),\n options.timeoutMs,\n );\n try {\n let res: Response;\n try {\n res = await fetch(iemCurrentsUrl(network, options.baseUrl), { signal: controller.signal });\n } catch (err) {\n // 取消/超时引发的拒绝(controller 已中止)原样透传——保持 AbortSignal 语义\n //(超时场景透传的即上方 MetarSourceError \"timeout\");\n // 其余(断网/DNS 的 TypeError 等)包装为 MetarSourceError \"network\",原始 message 保留在文末与 cause\n if (controller.signal.aborted) throw err;\n const reason = err instanceof Error ? err.message : String(err);\n throw new MetarSourceError(\n \"network\",\n network,\n `网络请求失败(源:IEM,network=${network}):请检查网络连通性后重试(${reason})`,\n { cause: err },\n );\n }\n if (!res.ok) {\n throw new MetarSourceError(\n \"http-error\",\n network,\n `IEM HTTP ${res.status}(network=${network})`,\n );\n }\n let body: unknown;\n try {\n body = await res.json();\n } catch (err) {\n // HTTP 200 但响应体不是 JSON(企业代理/防火墙拦截页的典型形态)——裸 SyntaxError 打穿属错误面缺口;\n // 归入 bad-schema 机读码,人话提示指向代理环境。中止引发的拒绝不在此列(保持取消语义透传)\n if (controller.signal.aborted) throw err;\n const reason = err instanceof Error ? err.message : String(err);\n throw new MetarSourceError(\n \"bad-schema\",\n network,\n `IEM 响应不是合法 JSON(可能被代理/防火墙拦截,network=${network}):${reason}`,\n { cause: err },\n );\n }\n if (\n typeof body !== \"object\" ||\n body === null ||\n !(\"data\" in body) ||\n !Array.isArray(body.data)\n ) {\n throw new MetarSourceError(\n \"bad-schema\",\n network,\n `IEM 响应异常:缺少 data 数组(network=${network},schema 不符)`,\n );\n }\n const rows = body.data;\n if (!rows.every(isObservation)) {\n throw new MetarSourceError(\n \"bad-schema\",\n network,\n `IEM 响应异常:data 存在 station/raw 非字符串的记录(network=${network},schema 不符)`,\n );\n }\n if (rows.length === 0) {\n // 合法网络不会返回空(整网实况端点)——空 data 几乎必然是网络名写错(IEM 对未知网络名返回 200 + 空数组)\n throw new MetarSourceError(\n \"empty-data\",\n network,\n `IEM 返回空数据(network=${network})——请核对 IEM 网络名(如 CN__ASOS/RU__ASOS,参考 https://mesonet.agron.iastate.edu/sites/networks.php)`,\n );\n }\n return rows;\n } finally {\n if (timer !== undefined) clearTimeout(timer);\n external?.removeEventListener(\"abort\", forward);\n }\n}\n\n/**\n * Fetch → parse → locate in one step: returns map-ready report items.\n * 取数 → 解析 → 定位一步到位:返回可直接喂给地图适配器的报文项数组。\n *\n * 定位规则:stations 命中的站用元数据精确坐标(并带站名),未命中的回落源站坐标\n * (IEM 中国站为城市级粗坐标,严肃场景请传站点元数据——见 examples 的 stations.json 用法);\n * 源站坐标为 null 且无元数据的行跳过。\n * 失败语义(不静默,两种模式):缺省任一报文解析整体失败(站名/时组缺失)即聚合抛出,绝不静默丢行;\n * 传 options.onUnparseable 则逐行容错——失败行跳过并逐条回调(可观测),好行照常返回。\n */\nexport async function getMetarReports(\n network = \"CN__ASOS\",\n options: GetMetarReportsOptions = {},\n): Promise<MetarReportItem[]> {\n const { stations, onUnparseable, spans, ...fetchOptions } = options;\n const table = new Map((stations ?? []).map((s) => [s.icao, s] as const));\n const observations = await getMetars(network, fetchOptions);\n const items: MetarReportItem[] = [];\n const failures: string[] = [];\n for (const obs of observations) {\n const station = table.get(obs.station);\n const lat = station?.lat ?? obs.lat;\n const lon = station?.lon ?? obs.lon;\n if (lat === null || lon === null) continue;\n try {\n items.push({\n report: parse(obs.raw, spans === false ? { spans: false } : undefined),\n position: [lat, lon],\n title: `${obs.station}${station?.name ? ` ${station.name}` : \"\"}`,\n });\n } catch (err) {\n // parse 对字符串输入只抛 MetarParseError(code 稳定契约);其他异常属编程错误,直接上抛不降级\n if (!(err instanceof MetarParseError)) throw err;\n if (onUnparseable !== undefined) {\n onUnparseable({ station: obs.station, raw: obs.raw, code: err.code, error: err.message });\n continue;\n }\n failures.push(`${obs.station}: ${err.message}`);\n }\n }\n if (failures.length > 0) {\n // 聚合错误机读化(原裸 Error):code 'batch-parse-failed' 稳定契约;raw 字段在此承载\n // 汇总信息(网络名/条数/逐条站名与原因)而非单条报文原文——语义调整见 core/errors.ts 注释\n const summary = `network=${network}; failures=${failures.length}; ${failures.join(\"; \")}`;\n throw new MetarParseError(\n \"batch-parse-failed\",\n summary,\n `报文解析失败 ${failures.length} 条(network=${network})——${failures.slice(0, 3).join(\";\")}${failures.length > 3 ? \"……\" : \"\"}`,\n );\n }\n return items;\n}\n\n// ---------------------------------------------------------------- TAF 取数线(收进 sources」指令)\n\n/** aviationweather TAF 数据端点(上游无 CORS 头:浏览器直连须自建代理/镜像——baseUrl 覆盖即为此用) */\nexport const AW_TAF_ENDPOINT = \"https://aviationweather.gov/api/data/taf\";\n\n/**\n * One fetched TAF row: station (best-effort) and the verbatim report (continuation lines already merged).\n * 单条 TAF 取数行:站码(best-effort)与报文原文(续行已归并)。\n */\nexport interface TafObservation {\n /** 站码:剥 TAF/AMD/COR 电头词后的首枚 4 位码;剥不出为 \"\"(解析层 missing-station 兜底出声) */\n station: string;\n /** 报文原文(上游 raw 格式的缩进续行已并回所属报文;report.raw 语义一致——原文保真) */\n raw: string;\n}\n\n/** Options for getTafs: fetch options plus the TAF endpoint-root override and the as-of date. */\nexport interface GetTafsOptions {\n /** 外部取消信号(与超时组合,任一触发即中止) */\n signal?: AbortSignal;\n /** 请求超时毫秒数(触发即中止并抛出超时错误) */\n timeoutMs?: number;\n /**\n * TAF 端点根覆盖(缺省官方 https://aviationweather.gov/api/data/taf):内网镜像/自建代理\n * 只换根——?ids=&format=raw 查询串由本层拼装(换源口径,见文件头;examples 即用 /aw-taf 代理根)。\n */\n baseUrl?: string;\n /**\n * 「该时刻已发布的最新报」(上游 date 参数语义):回看上一发布周期用(examples 报池即 date=now-4h)。\n * Date 转 ISO;字符串原样透传(上游亦收相对形态)。\n */\n date?: Date | string;\n}\n\n/**\n * The aviationweather TAF endpoint (raw format). ids joined as given; date serialized when present.\n * aviationweather TAF 端点模板(format=raw)。ids 数组逗号连接;date 存在时序列化为查询参数。\n */\nexport function awTafUrl(\n ids: string | readonly string[],\n options: { baseUrl?: string; date?: Date | string } = {},\n): string {\n const idList = typeof ids === \"string\" ? ids : ids.join(\",\");\n const params = new URLSearchParams({ ids: idList, format: \"raw\" });\n const d = options.date;\n if (d !== undefined) params.set(\"date\", d instanceof Date ? d.toISOString() : d);\n return `${options.baseUrl ?? AW_TAF_ENDPOINT}?${params.toString()}`;\n}\n\n/** raw 文本 → 整份报文行(缩进续行并回上一份;与报池语义同源)+ best-effort 站码提取 */\nfunction mergeTafLines(text: string): TafObservation[] {\n const merged: string[] = [];\n for (const line of text.split(\"\\n\")) {\n if (line.trim() === \"\") continue;\n if (/^\\s/.test(line) && merged.length > 0) merged[merged.length - 1] += ` ${line.trim()}`;\n else merged.push(line.trim());\n }\n return merged.map((raw) => {\n const toks = raw.split(/\\s+/);\n let k = toks[0] === \"TAF\" ? 1 : 0;\n while (toks[k] === \"AMD\" || toks[k] === \"COR\") k += 1;\n const head = toks[k] ?? \"\";\n return { station: /^[A-Z0-9]{4}$/.test(head) ? head : \"\", raw };\n });\n}\n\n/**\n * Fetch aviationweather TAFs (raw format) with station extracted and continuation lines merged.\n * 拉取 aviationweather TAF(raw 格式):续行归并 + 站码提取的取数侧整理。\n * 失败语义与 getMetars 同族(不静默):timeout/network/http-error/empty-data 一律 MetarSourceError\n * 机读码;上游对未知 ids 也返回 200 + 空体,空数据显式 throw 而非空数组静默。\n */\nexport async function getTafs(\n ids: string | readonly string[],\n options: GetTafsOptions = {},\n): Promise<TafObservation[]> {\n // 手工组合超时与外部信号(与 getMetars 同款:不依赖 AbortSignal.any,兼容 Node 20;假时钟可测)\n const controller = new AbortController();\n const external = options.signal;\n const forward = (): void => {\n controller.abort(external?.reason);\n };\n if (external !== undefined) {\n if (external.aborted) forward();\n else external.addEventListener(\"abort\", forward, { once: true });\n }\n const timer =\n options.timeoutMs === undefined\n ? undefined\n : setTimeout(\n () =>\n controller.abort(\n new MetarSourceError(\n \"timeout\",\n \"aviationweather\",\n `aviationweather TAF 请求超时(>${options.timeoutMs}ms)`,\n ),\n ),\n options.timeoutMs,\n );\n try {\n let res: Response;\n try {\n res = await fetch(awTafUrl(ids, options), { signal: controller.signal });\n } catch (err) {\n // 取消/超时引发的拒绝原样透传(AbortSignal 语义);断网/DNS 包装为 network(原始 message 留文末与 cause)\n if (controller.signal.aborted) throw err;\n const reason = err instanceof Error ? err.message : String(err);\n throw new MetarSourceError(\n \"network\",\n \"aviationweather\",\n `网络请求失败(源:aviationweather TAF):请检查网络连通性后重试(${reason})`,\n { cause: err },\n );\n }\n if (!res.ok) {\n throw new MetarSourceError(\n \"http-error\",\n \"aviationweather\",\n `aviationweather TAF HTTP ${res.status}`,\n );\n }\n let text: string;\n try {\n text = await res.text();\n } catch (err) {\n if (controller.signal.aborted) throw err;\n const reason = err instanceof Error ? err.message : String(err);\n throw new MetarSourceError(\n \"network\",\n \"aviationweather\",\n `aviationweather TAF 响应读取失败:${reason}`,\n { cause: err },\n );\n }\n const rows = mergeTafLines(text);\n if (rows.length === 0) {\n throw new MetarSourceError(\n \"empty-data\",\n \"aviationweather\",\n \"aviationweather TAF 返回空数据——请核对 ids 站码(如 ZBAA,ZBAD;端点 format=raw)\",\n );\n }\n return rows;\n } finally {\n if (timer !== undefined) clearTimeout(timer);\n external?.removeEventListener(\"abort\", forward);\n }\n}\n\n/** Options for getTafReports: fetch options plus station metadata and the per-row tolerance callback. */\nexport interface GetTafReportsOptions extends GetTafsOptions {\n /** 按 icao 联表的站点元数据(TAF 端点不提供坐标——stations 是定位的唯一来源,缺省将得空数组) */\n stations?: readonly StationRef[];\n /** 逐行容错(缺省关闭):传入时解析失败的行跳过并逐条回调;不传时聚合抛出(全有或全无) */\n onUnparseable?: (failure: UnparseableReport) => void;\n /** 紧凑模式:parseTaf 产物剥除全部 span 字段(存储场景推荐);缺省完整模式 */\n spans?: false;\n}\n\n/**\n * A map-ready TAF item (structurally compatible with TafLayerItem's report/position/title triple).\n * 可直接喂给地图适配器(如 addTafLayer)的预报项:{ report, position, title } 三件套。\n */\nexport interface TafReportItem {\n /** 已解析的 TAF IR(原文经 report.raw 原样保真) */\n report: TafReport;\n /** 站点坐标(WGS-84 [lat, lon];TAF 端点无坐标,来自 stations 联表) */\n position: [number, number];\n /** 站点提示(「ICAO 站名」;适配器缺省回落 IR 站名) */\n title: string;\n}\n\n/**\n * TAF 版的取数 → 解析 → 定位一步到位(契约沿 getMetarReports):stations 命中的站带精确坐标与站名,\n * 未命中跳过(TAF 端点无坐标可回落,与 getMetarReports 的无坐标跳行同规则);解析失败两种模式\n * (缺省聚合抛出 batch-parse-failed、传 onUnparseable 逐行容错)。报池等数据管理逻辑不进本层。\n */\nexport async function getTafReports(\n ids: string | readonly string[],\n options: GetTafReportsOptions = {},\n): Promise<TafReportItem[]> {\n const { stations, onUnparseable, spans, ...fetchOptions } = options;\n const table = new Map((stations ?? []).map((s) => [s.icao, s] as const));\n const observations = await getTafs(ids, fetchOptions);\n const items: TafReportItem[] = [];\n const failures: string[] = [];\n for (const obs of observations) {\n try {\n const report = parseTaf(obs.raw, spans === false ? { spans: false } : undefined);\n const station = table.get(report.station);\n if (station === undefined) continue; // 站表未命中(端点无坐标可回落)——跳过,规则同 getMetarReports\n items.push({\n report,\n position: [station.lat, station.lon],\n title: `${report.station}${station.name ? ` ${station.name}` : \"\"}`,\n });\n } catch (err) {\n // parseTaf 对字符串输入只抛 MetarParseError;其他异常属编程错误直接上抛\n if (!(err instanceof MetarParseError)) throw err;\n if (onUnparseable !== undefined) {\n onUnparseable({ station: obs.station, raw: obs.raw, code: err.code, error: err.message });\n continue;\n }\n failures.push(`${obs.station}: ${err.message}`);\n }\n }\n if (failures.length > 0) {\n const summary = `source=aviationweather; failures=${failures.length}; ${failures.join(\"; \")}`;\n throw new MetarParseError(\n \"batch-parse-failed\",\n summary,\n `TAF 解析失败 ${failures.length} 条(aviationweather)——${failures.slice(0, 3).join(\";\")}${failures.length > 3 ? \"……\" : \"\"}`,\n );\n }\n return items;\n}\n\n// ---------------------------------------------------------------- ogimet TAF 补充线(指令:aviationweather 取最新,ogimet 取最新/次新合并补充)\n\n/** ogimet display_metars2.php 端点(tipo=FT=TAF;无 CORS 头:浏览器直连须代理/镜像——baseUrl 覆盖即为此用) */\nexport const OGIMET_TAF_ENDPOINT = \"https://www.ogimet.com/display_metars2.php\";\n\n/** ogimet 查询窗(UTC 起止) */\nexport interface OgimetTafWindow {\n readonly from: Date;\n readonly to: Date;\n}\n\n/**\n * ogimet TAF 端点模板(配方=私有管线 backfill_ogimet.py 同源):tipo=FT、txt 形、\n * 服务端按 <pre> 区回 HTML(解析见 parseOgimetTafs)。免费公益服务——调用方自行节制频率。\n */\n/** UTC 日期字段补零组(ogimet 查询串的 ano/mes/day/hora/min 族) */\nconst ogimetFields = (\n d: Date,\n): { ano: string; mes: string; day: string; hora: string; min: string } => ({\n ano: `${d.getUTCFullYear()}`,\n mes: `${d.getUTCMonth() + 1}`.padStart(2, \"0\"),\n day: `${d.getUTCDate()}`.padStart(2, \"0\"),\n hora: `${d.getUTCHours()}`.padStart(2, \"0\"),\n min: `${d.getUTCMinutes()}`.padStart(2, \"0\"),\n});\n\nexport function ogimetTafUrl(\n station: string,\n window: OgimetTafWindow,\n options: { baseUrl?: string } = {},\n): string {\n const a = ogimetFields(window.from);\n const b = ogimetFields(window.to);\n const qs = new URLSearchParams({\n lang: \"en\",\n lugar: station,\n tipo: \"FT\",\n ord: \"DIR\",\n nil: \"SI\",\n fmt: \"txt\",\n ano: a.ano,\n mes: a.mes,\n day: a.day,\n hora: a.hora,\n min: a.min,\n anof: b.ano,\n mesf: b.mes,\n dayf: b.day,\n horaf: b.hora,\n minf: b.min,\n send: \"send\",\n nohtmlo: \"yes\",\n annot: \"yes\",\n leng: \"0\",\n });\n return `${options.baseUrl ?? OGIMET_TAF_ENDPOINT}?${qs.toString()}`;\n}\n\n/** 电头站码 best-effort 提取(TAF/AMD/COR 剥词;剥不出返回空串)——与 mergeTafLines 同判式(aviationweather 取数线) */\nconst stationHeadOf = (raw: string): string => {\n const toks = raw.split(/\\s+/);\n let k = toks[0] === \"TAF\" ? 1 : 0;\n while (toks[k] === \"AMD\" || toks[k] === \"COR\") k += 1;\n const head = toks[k] ?? \"\";\n return /^[A-Z0-9]{4}$/.test(head) ? head : \"\";\n};\n\n/**\n * ogimet 响应解析(<pre> 区):12 位时间戳前缀行(YYYYMMDDHHmm,收报序)起新记录、无前缀行并入上一条\n * (多行报文并单——与私有管线 parse_tafs 同协议);# 注释/空行跳过;只收 TAF 起头报文。\n * 站码 best-effort 取自电头(query 站名兜底);空模板(无 <pre> 或零报文)返回空数组——\n * 由调用方判空(ogimet 缺数常见,属正常降级而非错误面)。\n */\nexport function parseOgimetTafs(html: string, fallbackStation = \"\"): TafObservation[] {\n const pre = /<pre>([\\s\\S]*?)<\\/pre>/.exec(html);\n if (pre === null) return [];\n const PREFIX = /^(\\d{12})\\s+(.*)$/;\n const merged: string[] = [];\n for (const line of (pre[1] ?? \"\").split(\"\\n\")) {\n const t = line.trim();\n if (t === \"\" || t.startsWith(\"#\")) continue;\n const pm = PREFIX.exec(t);\n if (pm !== null) merged.push(pm[2] ?? \"\");\n else if (merged.length > 0) merged[merged.length - 1] += ` ${t}`;\n }\n return merged\n .filter((raw) => raw.startsWith(\"TAF\"))\n .map((raw) => ({ station: stationHeadOf(raw) || fallbackStation, raw }));\n}\n\n/** Options for getTafsOgimet: fetch options plus the ogimet endpoint-root override. */\nexport interface GetTafsOgimetOptions {\n /** 外部取消信号(与超时组合,任一触发即中止) */\n signal?: AbortSignal;\n /** 请求超时毫秒数(触发即中止并抛出超时错误) */\n timeoutMs?: number;\n /** ogimet 端点根覆盖(内网镜像/自建代理;examples 走 /ogimet-taf 代理根) */\n baseUrl?: string;\n}\n\n/**\n * Fetch one station's recent TAFs from ogimet (latest + previous publication cycles).\n * 拉取单站的 ogimet 近窗 TAF(最新与上一发布周期——报池补充线,供与 aviationweather 线合并去重)。\n * 失败语义与 getTafs 同族(timeout/network/http-error 机读码);空数据返回空数组而非抛错\n * (ogimet 站点级缺数常见——它是补充线,空窗属正常降级,不静默丢整批)。\n */\nexport async function getTafsOgimet(\n station: string,\n window: OgimetTafWindow,\n options: GetTafsOgimetOptions = {},\n): Promise<TafObservation[]> {\n const controller = new AbortController();\n const external = options.signal;\n const forward = (): void => {\n controller.abort(external?.reason);\n };\n if (external !== undefined) {\n if (external.aborted) forward();\n else external.addEventListener(\"abort\", forward, { once: true });\n }\n const timer =\n options.timeoutMs === undefined\n ? undefined\n : setTimeout(\n () =>\n controller.abort(\n new MetarSourceError(\n \"timeout\",\n \"ogimet\",\n `ogimet TAF 请求超时(>${options.timeoutMs}ms,站=${station})`,\n ),\n ),\n options.timeoutMs,\n );\n try {\n let res: Response;\n try {\n res = await fetch(ogimetTafUrl(station, window, options), { signal: controller.signal });\n } catch (err) {\n if (controller.signal.aborted) throw err;\n const reason = err instanceof Error ? err.message : String(err);\n throw new MetarSourceError(\n \"network\",\n \"ogimet\",\n `网络请求失败(源:ogimet TAF,站=${station}):${reason}`,\n { cause: err },\n );\n }\n if (!res.ok) {\n throw new MetarSourceError(\n \"http-error\",\n \"ogimet\",\n `ogimet TAF HTTP ${res.status}(站=${station})`,\n );\n }\n return parseOgimetTafs(await res.text(), station);\n } finally {\n if (timer !== undefined) clearTimeout(timer);\n external?.removeEventListener(\"abort\", forward);\n }\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "metweave",
3
- "version": "0.1.1",
3
+ "version": "0.2.0",
4
4
  "description": "Show live airport weather on a Leaflet map in ten lines — METAR parsing, report cards and map layers, straight from public sources.",
5
5
  "keywords": [
6
6
  "airport",
@@ -13,6 +13,10 @@
13
13
  "visualization",
14
14
  "weather"
15
15
  ],
16
+ "homepage": "https://github.com/yaojaro/metweave#readme",
17
+ "bugs": {
18
+ "url": "https://github.com/yaojaro/metweave/issues"
19
+ },
16
20
  "license": "MIT",
17
21
  "author": "YaoJaro <yaojaro@metweave.com>",
18
22
  "repository": {
@@ -27,54 +31,74 @@
27
31
  ],
28
32
  "type": "module",
29
33
  "sideEffects": false,
30
- "main": "./dist/index.cjs",
31
- "module": "./dist/index.js",
32
- "types": "./dist/index.d.ts",
34
+ "main": "./src/index.ts",
35
+ "module": "./src/index.ts",
36
+ "types": "./src/index.ts",
33
37
  "exports": {
34
38
  ".": {
35
- "import": {
36
- "types": "./dist/index.d.ts",
37
- "default": "./dist/index.js"
38
- },
39
- "require": {
40
- "types": "./dist/index.d.cts",
41
- "default": "./dist/index.cjs"
42
- }
39
+ "types": "./src/index.ts",
40
+ "import": "./src/index.ts",
41
+ "require": "./src/index.ts"
43
42
  },
44
43
  "./sources": {
45
- "import": {
46
- "types": "./dist/sources.d.ts",
47
- "default": "./dist/sources.js"
48
- },
49
- "require": {
50
- "types": "./dist/sources.d.cts",
51
- "default": "./dist/sources.cjs"
52
- }
44
+ "types": "./src/sources.ts",
45
+ "import": "./src/sources.ts",
46
+ "require": "./src/sources.ts"
53
47
  },
54
48
  "./stations-cn": {
55
- "import": {
56
- "types": "./dist/stations-cn.d.ts",
57
- "default": "./dist/stations-cn.js"
58
- },
59
- "require": {
60
- "types": "./dist/stations-cn.d.cts",
61
- "default": "./dist/stations-cn.cjs"
62
- }
49
+ "types": "./src/stations-cn.ts",
50
+ "import": "./src/stations-cn.ts",
51
+ "require": "./src/stations-cn.ts"
63
52
  }
64
53
  },
65
54
  "publishConfig": {
55
+ "main": "./dist/index.cjs",
56
+ "module": "./dist/index.js",
57
+ "types": "./dist/index.d.ts",
58
+ "exports": {
59
+ ".": {
60
+ "import": {
61
+ "types": "./dist/index.d.ts",
62
+ "default": "./dist/index.js"
63
+ },
64
+ "require": {
65
+ "types": "./dist/index.d.cts",
66
+ "default": "./dist/index.cjs"
67
+ }
68
+ },
69
+ "./sources": {
70
+ "import": {
71
+ "types": "./dist/sources.d.ts",
72
+ "default": "./dist/sources.js"
73
+ },
74
+ "require": {
75
+ "types": "./dist/sources.d.cts",
76
+ "default": "./dist/sources.cjs"
77
+ }
78
+ },
79
+ "./stations-cn": {
80
+ "import": {
81
+ "types": "./dist/stations-cn.d.ts",
82
+ "default": "./dist/stations-cn.js"
83
+ },
84
+ "require": {
85
+ "types": "./dist/stations-cn.d.cts",
86
+ "default": "./dist/stations-cn.cjs"
87
+ }
88
+ }
89
+ },
66
90
  "access": "public"
67
91
  },
92
+ "scripts": {
93
+ "build": "tsdown",
94
+ "typecheck": "tsc --noEmit"
95
+ },
68
96
  "dependencies": {
69
- "@metweave/core": "0.1.1",
70
- "@metweave/parser": "0.1.1",
71
- "@metweave/render": "0.1.1"
97
+ "@metweave/core": "workspace:*",
98
+ "@metweave/parser": "workspace:*",
99
+ "@metweave/render": "workspace:*"
72
100
  },
73
101
  "engines": {
74
102
  "node": ">=20"
75
- },
76
- "scripts": {
77
- "build": "tsdown",
78
- "typecheck": "tsc --noEmit"
79
103
  }
80
- }
104
+ }
package/LICENSE DELETED
@@ -1,21 +0,0 @@
1
- MIT License
2
-
3
- Copyright (c) 2026 YaoJaro
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.