@caoguo/maplibre 0.0.1
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/dist/chunk-DNEF4DHG.js +48 -0
- package/dist/chunk-DNEF4DHG.js.map +1 -0
- package/dist/chunk-EZOV5O2M.js +96 -0
- package/dist/chunk-EZOV5O2M.js.map +1 -0
- package/dist/chunk-MNUQZUQA.js +372 -0
- package/dist/chunk-MNUQZUQA.js.map +1 -0
- package/dist/index.cjs +1154 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +459 -0
- package/dist/index.d.ts +459 -0
- package/dist/index.js +587 -0
- package/dist/index.js.map +1 -0
- package/dist/offline/index.cjs +397 -0
- package/dist/offline/index.cjs.map +1 -0
- package/dist/offline/index.d.cts +300 -0
- package/dist/offline/index.d.ts +300 -0
- package/dist/offline/index.js +3 -0
- package/dist/offline/index.js.map +1 -0
- package/dist/sources/index.cjs +102 -0
- package/dist/sources/index.cjs.map +1 -0
- package/dist/sources/index.d.cts +83 -0
- package/dist/sources/index.d.ts +83 -0
- package/dist/sources/index.js +3 -0
- package/dist/sources/index.js.map +1 -0
- package/dist/styles.cjs +53 -0
- package/dist/styles.cjs.map +1 -0
- package/dist/styles.d.cts +19 -0
- package/dist/styles.d.ts +19 -0
- package/dist/styles.js +3 -0
- package/dist/styles.js.map +1 -0
- package/package.json +52 -0
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { buildStyle } from '@caoguo/theme';
|
|
2
|
+
|
|
3
|
+
// src/styles.ts
|
|
4
|
+
var WUHAN_CENTER = [114.3055, 30.5928];
|
|
5
|
+
var WUHAN_ZOOM = 11;
|
|
6
|
+
function caoguoStyle(theme = "caoguo-dark") {
|
|
7
|
+
return buildStyle(theme);
|
|
8
|
+
}
|
|
9
|
+
function osmRasterStyle() {
|
|
10
|
+
return {
|
|
11
|
+
version: 8,
|
|
12
|
+
glyphs: "https://demotiles.maplibre.org/font/{fontstack}/{range}.pbf",
|
|
13
|
+
sources: {
|
|
14
|
+
"osm-raster": {
|
|
15
|
+
type: "raster",
|
|
16
|
+
tiles: [
|
|
17
|
+
"https://a.tile.openstreetmap.org/{z}/{x}/{y}.png",
|
|
18
|
+
"https://b.tile.openstreetmap.org/{z}/{x}/{y}.png",
|
|
19
|
+
"https://c.tile.openstreetmap.org/{z}/{x}/{y}.png"
|
|
20
|
+
],
|
|
21
|
+
tileSize: 256,
|
|
22
|
+
attribution: "\xA9 OpenStreetMap contributors"
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
layers: [
|
|
26
|
+
{
|
|
27
|
+
id: "background",
|
|
28
|
+
type: "background",
|
|
29
|
+
paint: { "background-color": "#0a0f1e" }
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
id: "osm-raster",
|
|
33
|
+
type: "raster",
|
|
34
|
+
source: "osm-raster",
|
|
35
|
+
paint: {
|
|
36
|
+
"raster-opacity": 0.85,
|
|
37
|
+
"raster-saturation": -0.6,
|
|
38
|
+
"raster-brightness-max": 0.7,
|
|
39
|
+
"raster-contrast": 0.1
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
]
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export { WUHAN_CENTER, WUHAN_ZOOM, caoguoStyle, osmRasterStyle };
|
|
47
|
+
//# sourceMappingURL=chunk-DNEF4DHG.js.map
|
|
48
|
+
//# sourceMappingURL=chunk-DNEF4DHG.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/styles.ts"],"names":[],"mappings":";;;AAWO,IAAM,YAAA,GAAiC,CAAC,QAAA,EAAU,OAAO;AACzD,IAAM,UAAA,GAAa;AAGnB,SAAS,WAAA,CAAY,QAAmB,aAAA,EAAmC;AAChF,EAAA,OAAO,WAAW,KAAK,CAAA;AACzB;AAGO,SAAS,cAAA,GAAqC;AACnD,EAAA,OAAO;AAAA,IACL,OAAA,EAAS,CAAA;AAAA,IACT,MAAA,EAAQ,6DAAA;AAAA,IACR,OAAA,EAAS;AAAA,MACP,YAAA,EAAc;AAAA,QACZ,IAAA,EAAM,QAAA;AAAA,QACN,KAAA,EAAO;AAAA,UACL,kDAAA;AAAA,UACA,kDAAA;AAAA,UACA;AAAA,SACF;AAAA,QACA,QAAA,EAAU,GAAA;AAAA,QACV,WAAA,EAAa;AAAA;AACf,KACF;AAAA,IACA,MAAA,EAAQ;AAAA,MACN;AAAA,QACE,EAAA,EAAI,YAAA;AAAA,QACJ,IAAA,EAAM,YAAA;AAAA,QACN,KAAA,EAAO,EAAE,kBAAA,EAAoB,SAAA;AAAU,OACzC;AAAA,MACA;AAAA,QACE,EAAA,EAAI,YAAA;AAAA,QACJ,IAAA,EAAM,QAAA;AAAA,QACN,MAAA,EAAQ,YAAA;AAAA,QACR,KAAA,EAAO;AAAA,UACL,gBAAA,EAAkB,IAAA;AAAA,UAClB,mBAAA,EAAqB,IAAA;AAAA,UACrB,uBAAA,EAAyB,GAAA;AAAA,UACzB,iBAAA,EAAmB;AAAA;AACrB;AACF;AACF,GACF;AACF","file":"chunk-DNEF4DHG.js","sourcesContent":["/**\n * 草果地图内置样式。\n *\n * - `caoguoStyle()`:官方矢量主题(caoguo-dark / caoguo-light),\n * 由 @caoguo/theme 生成,底层使用 MapLibre 免费 demo 矢量瓦片,无需 token。\n * - `osmRasterStyle()`:OSM 栅格兜底样式,矢量主题加载失败时使用。\n */\n\nimport type { StyleSpecification } from 'maplibre-gl';\nimport { buildStyle, type ThemeName } from '@caoguo/theme';\n\nexport const WUHAN_CENTER: [number, number] = [114.3055, 30.5928];\nexport const WUHAN_ZOOM = 11;\n\n/** 草果官方矢量主题(默认暗色)。 */\nexport function caoguoStyle(theme: ThemeName = 'caoguo-dark'): StyleSpecification {\n return buildStyle(theme);\n}\n\n/** 暗色 OSM 栅格兜底底图(无需 key)。 */\nexport function osmRasterStyle(): StyleSpecification {\n return {\n version: 8,\n glyphs: 'https://demotiles.maplibre.org/font/{fontstack}/{range}.pbf',\n sources: {\n 'osm-raster': {\n type: 'raster',\n tiles: [\n 'https://a.tile.openstreetmap.org/{z}/{x}/{y}.png',\n 'https://b.tile.openstreetmap.org/{z}/{x}/{y}.png',\n 'https://c.tile.openstreetmap.org/{z}/{x}/{y}.png',\n ],\n tileSize: 256,\n attribution: '© OpenStreetMap contributors',\n },\n },\n layers: [\n {\n id: 'background',\n type: 'background',\n paint: { 'background-color': '#0a0f1e' },\n },\n {\n id: 'osm-raster',\n type: 'raster',\n source: 'osm-raster',\n paint: {\n 'raster-opacity': 0.85,\n 'raster-saturation': -0.6,\n 'raster-brightness-max': 0.7,\n 'raster-contrast': 0.1,\n },\n },\n ],\n };\n}\n"]}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
// src/sources/tianditu.ts
|
|
2
|
+
var LAYER_MAP = {
|
|
3
|
+
vector: { base: "vec", label: "cva" },
|
|
4
|
+
satellite: { base: "img", label: "cia" },
|
|
5
|
+
terrain: { base: "ter", label: "cta" }
|
|
6
|
+
};
|
|
7
|
+
var LAYER_LABEL_EN = {
|
|
8
|
+
cva: "eva",
|
|
9
|
+
cia: "eia",
|
|
10
|
+
cta: "eta"
|
|
11
|
+
};
|
|
12
|
+
function tiandituTileUrls(layer, opts) {
|
|
13
|
+
const sub = opts.subdomains ?? [0, 1, 2, 3, 4, 5, 6, 7];
|
|
14
|
+
const labelLayer = opts.lang === "en" ? LAYER_LABEL_EN[layer] ?? layer : layer;
|
|
15
|
+
return sub.map(
|
|
16
|
+
(s) => `https://t${s}.tianditu.gov.cn/${labelLayer}_w/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=${labelLayer}&STYLE=default&TILEMATRIXSET=w&FORMAT=tiles&TILEMATRIX={z}&TILEROW={y}&TILECOL={x}&tk=${encodeURIComponent(opts.token)}`
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
function buildTiandituSources(type, opts) {
|
|
20
|
+
const { base, label } = LAYER_MAP[type];
|
|
21
|
+
const tileSize = opts.tileSize ?? 256;
|
|
22
|
+
const maxzoom = opts.maxzoom ?? 18;
|
|
23
|
+
const attr = "\xA9 \u5929\u5730\u56FE GS(2023)3295\u53F7";
|
|
24
|
+
return [
|
|
25
|
+
{
|
|
26
|
+
id: `tianditu-${base}`,
|
|
27
|
+
isLabel: false,
|
|
28
|
+
source: {
|
|
29
|
+
type: "raster",
|
|
30
|
+
tiles: tiandituTileUrls(base, opts),
|
|
31
|
+
tileSize,
|
|
32
|
+
maxzoom,
|
|
33
|
+
attribution: attr
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
id: `tianditu-${label}`,
|
|
38
|
+
isLabel: true,
|
|
39
|
+
source: {
|
|
40
|
+
type: "raster",
|
|
41
|
+
tiles: tiandituTileUrls(label, opts),
|
|
42
|
+
tileSize,
|
|
43
|
+
maxzoom,
|
|
44
|
+
attribution: attr
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
];
|
|
48
|
+
}
|
|
49
|
+
var MissingTokenError = class extends Error {
|
|
50
|
+
constructor() {
|
|
51
|
+
super("\u5929\u5730\u56FE token \u7F3A\u5931\uFF1A\u8C03\u7528 addTiandituBaseMap \u5FC5\u987B\u4F20\u5165 tianditu: { token } \u9009\u9879\u3002");
|
|
52
|
+
this.name = "MissingTokenError";
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
// src/sources/addTianditu.ts
|
|
57
|
+
function tiandituStyle(type, opts) {
|
|
58
|
+
if (!opts.token) throw new MissingTokenError();
|
|
59
|
+
const sources = buildTiandituSources(type, opts);
|
|
60
|
+
return {
|
|
61
|
+
version: 8,
|
|
62
|
+
glyphs: "https://demotiles.maplibre.org/font/{fontstack}/{range}.pbf",
|
|
63
|
+
sources: Object.fromEntries(
|
|
64
|
+
sources.map((s) => [s.id, s.source])
|
|
65
|
+
),
|
|
66
|
+
layers: sources.map((s) => ({
|
|
67
|
+
id: s.id,
|
|
68
|
+
type: "raster",
|
|
69
|
+
source: s.id,
|
|
70
|
+
// 注记层置于底图之上
|
|
71
|
+
...s.isLabel ? { minzoom: 0 } : {}
|
|
72
|
+
}))
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
function addTiandituBaseMap(opts) {
|
|
76
|
+
if (!opts.token) throw new MissingTokenError();
|
|
77
|
+
const type = opts.type ?? "vector";
|
|
78
|
+
const sources = buildTiandituSources(type, opts);
|
|
79
|
+
const map = opts.map;
|
|
80
|
+
for (const s of sources) {
|
|
81
|
+
if (!map.getSource(s.id)) {
|
|
82
|
+
map.addSource(s.id, s.source);
|
|
83
|
+
}
|
|
84
|
+
if (!map.getLayer(s.id)) {
|
|
85
|
+
map.addLayer({
|
|
86
|
+
id: s.id,
|
|
87
|
+
type: "raster",
|
|
88
|
+
source: s.id
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export { MissingTokenError, addTiandituBaseMap, buildTiandituSources, tiandituStyle, tiandituTileUrls };
|
|
95
|
+
//# sourceMappingURL=chunk-EZOV5O2M.js.map
|
|
96
|
+
//# sourceMappingURL=chunk-EZOV5O2M.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/sources/tianditu.ts","../src/sources/addTianditu.ts"],"names":[],"mappings":";AA4CA,IAAM,SAAA,GAAiF;AAAA,EACrF,MAAA,EAAQ,EAAE,IAAA,EAAM,KAAA,EAAO,OAAO,KAAA,EAAM;AAAA,EACpC,SAAA,EAAW,EAAE,IAAA,EAAM,KAAA,EAAO,OAAO,KAAA,EAAM;AAAA,EACvC,OAAA,EAAS,EAAE,IAAA,EAAM,KAAA,EAAO,OAAO,KAAA;AACjC,CAAA;AAEA,IAAM,cAAA,GAAgE;AAAA,EACpE,GAAA,EAAK,KAAA;AAAA,EACL,GAAA,EAAK,KAAA;AAAA,EACL,GAAA,EAAK;AACP,CAAA;AAGO,SAAS,gBAAA,CACd,OACA,IAAA,EACU;AACV,EAAA,MAAM,GAAA,GAAM,IAAA,CAAK,UAAA,IAAc,CAAC,CAAA,EAAG,CAAA,EAAG,CAAA,EAAG,CAAA,EAAG,CAAA,EAAG,CAAA,EAAG,CAAA,EAAG,CAAC,CAAA;AACtD,EAAA,MAAM,aAAa,IAAA,CAAK,IAAA,KAAS,OAAO,cAAA,CAAe,KAAK,KAAK,KAAA,GAAQ,KAAA;AACzE,EAAA,OAAO,GAAA,CAAI,GAAA;AAAA,IACT,CAAC,CAAA,KACC,CAAA,SAAA,EAAY,CAAC,CAAA,iBAAA,EAAoB,UAAU,CAAA,yDAAA,EAA4D,UAAU,CAAA,sFAAA,EAAyF,kBAAA,CAAmB,IAAA,CAAK,KAAK,CAAC,CAAA;AAAA,GAC5O;AACF;AAgBO,SAAS,oBAAA,CACd,MACA,IAAA,EACsB;AACtB,EAAA,MAAM,EAAE,IAAA,EAAM,KAAA,EAAM,GAAI,UAAU,IAAI,CAAA;AACtC,EAAA,MAAM,QAAA,GAAW,KAAK,QAAA,IAAY,GAAA;AAClC,EAAA,MAAM,OAAA,GAAU,KAAK,OAAA,IAAW,EAAA;AAChC,EAAA,MAAM,IAAA,GAAO,4CAAA;AACb,EAAA,OAAO;AAAA,IACL;AAAA,MACE,EAAA,EAAI,YAAY,IAAI,CAAA,CAAA;AAAA,MACpB,OAAA,EAAS,KAAA;AAAA,MACT,MAAA,EAAQ;AAAA,QACN,IAAA,EAAM,QAAA;AAAA,QACN,KAAA,EAAO,gBAAA,CAAiB,IAAA,EAAM,IAAI,CAAA;AAAA,QAClC,QAAA;AAAA,QACA,OAAA;AAAA,QACA,WAAA,EAAa;AAAA;AACf,KACF;AAAA,IACA;AAAA,MACE,EAAA,EAAI,YAAY,KAAK,CAAA,CAAA;AAAA,MACrB,OAAA,EAAS,IAAA;AAAA,MACT,MAAA,EAAQ;AAAA,QACN,IAAA,EAAM,QAAA;AAAA,QACN,KAAA,EAAO,gBAAA,CAAiB,KAAA,EAAO,IAAI,CAAA;AAAA,QACnC,QAAA;AAAA,QACA,OAAA;AAAA,QACA,WAAA,EAAa;AAAA;AACf;AACF,GACF;AACF;AAEO,IAAM,iBAAA,GAAN,cAAgC,KAAA,CAAM;AAAA,EAC3C,WAAA,GAAc;AACZ,IAAA,KAAA,CAAM,4IAAiE,CAAA;AACvE,IAAA,IAAA,CAAK,IAAA,GAAO,mBAAA;AAAA,EACd;AACF;;;ACjGO,SAAS,aAAA,CACd,MACA,IAAA,EACoB;AACpB,EAAA,IAAI,CAAC,IAAA,CAAK,KAAA,EAAO,MAAM,IAAI,iBAAA,EAAkB;AAC7C,EAAA,MAAM,OAAA,GAAU,oBAAA,CAAqB,IAAA,EAAM,IAAI,CAAA;AAC/C,EAAA,OAAO;AAAA,IACL,OAAA,EAAS,CAAA;AAAA,IACT,MAAA,EAAQ,6DAAA;AAAA,IACR,SAAS,MAAA,CAAO,WAAA;AAAA,MACd,OAAA,CAAQ,IAAI,CAAC,CAAA,KAAM,CAAC,CAAA,CAAE,EAAA,EAAI,CAAA,CAAE,MAAM,CAAC;AAAA,KACrC;AAAA,IACA,MAAA,EAAQ,OAAA,CAAQ,GAAA,CAAI,CAAC,CAAA,MAAO;AAAA,MAC1B,IAAI,CAAA,CAAE,EAAA;AAAA,MACN,IAAA,EAAM,QAAA;AAAA,MACN,QAAQ,CAAA,CAAE,EAAA;AAAA;AAAA,MAEV,GAAI,CAAA,CAAE,OAAA,GAAU,EAAE,OAAA,EAAS,CAAA,KAAM;AAAC,KACpC,CAAE;AAAA,GACJ;AACF;AAGO,SAAS,mBAAmB,IAAA,EAAgC;AACjE,EAAA,IAAI,CAAC,IAAA,CAAK,KAAA,EAAO,MAAM,IAAI,iBAAA,EAAkB;AAC7C,EAAA,MAAM,IAAA,GAAO,KAAK,IAAA,IAAQ,QAAA;AAC1B,EAAA,MAAM,OAAA,GAAU,oBAAA,CAAqB,IAAA,EAAM,IAAI,CAAA;AAC/C,EAAA,MAAM,MAAM,IAAA,CAAK,GAAA;AACjB,EAAA,KAAA,MAAW,KAAK,OAAA,EAAS;AACvB,IAAA,IAAI,CAAC,GAAA,CAAI,SAAA,CAAU,CAAA,CAAE,EAAE,CAAA,EAAG;AACxB,MAAA,GAAA,CAAI,SAAA,CAAU,CAAA,CAAE,EAAA,EAAI,CAAA,CAAE,MAAe,CAAA;AAAA,IACvC;AACA,IAAA,IAAI,CAAC,GAAA,CAAI,QAAA,CAAS,CAAA,CAAE,EAAE,CAAA,EAAG;AACvB,MAAA,GAAA,CAAI,QAAA,CAAS;AAAA,QACX,IAAI,CAAA,CAAE,EAAA;AAAA,QACN,IAAA,EAAM,QAAA;AAAA,QACN,QAAQ,CAAA,CAAE;AAAA,OACF,CAAA;AAAA,IACZ;AAAA,EACF;AACF","file":"chunk-EZOV5O2M.js","sourcesContent":["/**\n * 天地图(Tianditu)WMTS 底图源(F-1.2)。\n *\n * 天地图为国内权威底图,坐标系为 CGCS2000 / Web Mercator(显示尺度等价于 WGS84)。\n * token 必须由调用方在运行时注入,**绝不硬编码**;缺失时抛出明确错误。\n *\n * 文档:https://lbs.tianditu.gov.cn/server/MapService.html\n */\n\nexport type TiandituLayer =\n /** 矢量底图(含道路/注记基础) */\n | 'vec'\n /** 影像底图 */\n | 'img'\n /** 地形底图 */\n | 'ter'\n /** 矢量注记(道路/地名) */\n | 'cva'\n /** 影像注记 */\n | 'cia'\n /** 地形注记 */\n | 'cta'\n /** 矢量注记(英文) */\n | 'eva'\n /** 影像注记(英文) */\n | 'eia'\n /** 地形注记(英文) */\n | 'eta';\n\nexport type TiandituType = 'vector' | 'satellite' | 'terrain';\n\nexport interface TiandituOptions {\n /** 天地图授权 token(必填,运行时注入) */\n token: string;\n /** 语言:中文 cva/cia/cta 或英文 eva/eia/eta */\n lang?: 'zh' | 'en';\n /** 瓦片服务子域,默认 0-7 */\n subdomains?: number[];\n /** 瓦片尺寸,默认 256 */\n tileSize?: number;\n /** 最大层级,默认 18 */\n maxzoom?: number;\n}\n\nconst LAYER_MAP: Record<TiandituType, { base: TiandituLayer; label: TiandituLayer }> = {\n vector: { base: 'vec', label: 'cva' },\n satellite: { base: 'img', label: 'cia' },\n terrain: { base: 'ter', label: 'cta' },\n};\n\nconst LAYER_LABEL_EN: Partial<Record<TiandituLayer, TiandituLayer>> = {\n cva: 'eva',\n cia: 'eia',\n cta: 'eta',\n};\n\n/** 构建单张 WMTS 栅格源的 tiles URL 列表(含子域与 token) */\nexport function tiandituTileUrls(\n layer: TiandituLayer,\n opts: TiandituOptions\n): string[] {\n const sub = opts.subdomains ?? [0, 1, 2, 3, 4, 5, 6, 7];\n const labelLayer = opts.lang === 'en' ? LAYER_LABEL_EN[layer] ?? layer : layer;\n return sub.map(\n (s) =>\n `https://t${s}.tianditu.gov.cn/${labelLayer}_w/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=${labelLayer}&STYLE=default&TILEMATRIXSET=w&FORMAT=tiles&TILEMATRIX={z}&TILEROW={y}&TILECOL={x}&tk=${encodeURIComponent(opts.token)}`\n );\n}\n\nexport interface TiandituSourceSpec {\n id: string;\n source: {\n type: 'raster';\n tiles: string[];\n tileSize: number;\n maxzoom: number;\n attribution: string;\n };\n /** 是否注记层(绘制于底图之上) */\n isLabel: boolean;\n}\n\n/** 生成一组天地图源(底图 + 注记) */\nexport function buildTiandituSources(\n type: TiandituType,\n opts: TiandituOptions\n): TiandituSourceSpec[] {\n const { base, label } = LAYER_MAP[type];\n const tileSize = opts.tileSize ?? 256;\n const maxzoom = opts.maxzoom ?? 18;\n const attr = '© 天地图 GS(2023)3295号';\n return [\n {\n id: `tianditu-${base}`,\n isLabel: false,\n source: {\n type: 'raster',\n tiles: tiandituTileUrls(base, opts),\n tileSize,\n maxzoom,\n attribution: attr,\n },\n },\n {\n id: `tianditu-${label}`,\n isLabel: true,\n source: {\n type: 'raster',\n tiles: tiandituTileUrls(label, opts),\n tileSize,\n maxzoom,\n attribution: attr,\n },\n },\n ];\n}\n\nexport class MissingTokenError extends Error {\n constructor() {\n super('天地图 token 缺失:调用 addTiandituBaseMap 必须传入 tianditu: { token } 选项。');\n this.name = 'MissingTokenError';\n }\n}\n","/**\n * 把天地图底图(底图 + 注记)作为源/图层添加到 MapLibre 地图实例。\n *\n * 由于天地图为栅格底图,推荐作为「无 style 背景」的地图底层:\n * 直接通过 setStyle 注入最小 style,或先 addSource + addLayer。\n * 这里提供低侵入的「注入已有 map」方式,业务可叠加自有矢量图层。\n */\n\nimport type { Map as MlMap, StyleSpecification } from 'maplibre-gl';\nimport {\n buildTiandituSources,\n MissingTokenError,\n type TiandituOptions,\n type TiandituType,\n} from './tianditu';\n\nexport interface AddTiandituOptions extends TiandituOptions {\n type?: TiandituType;\n /** 添加到指定地图实例 */\n map: MlMap;\n /** 注入为 style 的源/图层还是 addSource(默认 addSource + addLayer) */\n asStyle?: false;\n}\n\n/** 生成可直接作为 Map style 的天地图 Style JSON(含底图+注记两层) */\nexport function tiandituStyle(\n type: TiandituType,\n opts: TiandituOptions\n): StyleSpecification {\n if (!opts.token) throw new MissingTokenError();\n const sources = buildTiandituSources(type, opts);\n return {\n version: 8,\n glyphs: 'https://demotiles.maplibre.org/font/{fontstack}/{range}.pbf',\n sources: Object.fromEntries(\n sources.map((s) => [s.id, s.source])\n ) as StyleSpecification['sources'],\n layers: sources.map((s) => ({\n id: s.id,\n type: 'raster' as const,\n source: s.id,\n // 注记层置于底图之上\n ...(s.isLabel ? { minzoom: 0 } : {}),\n })),\n };\n}\n\n/** 向已存在的地图实例注入天地图底图(底图 + 注记) */\nexport function addTiandituBaseMap(opts: AddTiandituOptions): void {\n if (!opts.token) throw new MissingTokenError();\n const type = opts.type ?? 'vector';\n const sources = buildTiandituSources(type, opts);\n const map = opts.map;\n for (const s of sources) {\n if (!map.getSource(s.id)) {\n map.addSource(s.id, s.source as never);\n }\n if (!map.getLayer(s.id)) {\n map.addLayer({\n id: s.id,\n type: 'raster',\n source: s.id,\n } as never);\n }\n }\n}\n"]}
|
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
// src/offline/storage.ts
|
|
2
|
+
var MemoryTileStore = class {
|
|
3
|
+
constructor() {
|
|
4
|
+
this.map = /* @__PURE__ */ new Map();
|
|
5
|
+
}
|
|
6
|
+
async get(key) {
|
|
7
|
+
const t = this.map.get(key);
|
|
8
|
+
if (!t) return void 0;
|
|
9
|
+
if (t.expires && t.expires < Date.now()) {
|
|
10
|
+
this.map.delete(key);
|
|
11
|
+
return void 0;
|
|
12
|
+
}
|
|
13
|
+
return t;
|
|
14
|
+
}
|
|
15
|
+
async put(key, tile) {
|
|
16
|
+
this.map.set(key, tile);
|
|
17
|
+
}
|
|
18
|
+
async has(key) {
|
|
19
|
+
return await this.get(key) !== void 0;
|
|
20
|
+
}
|
|
21
|
+
async delete(key) {
|
|
22
|
+
this.map.delete(key);
|
|
23
|
+
}
|
|
24
|
+
async clear(prefix) {
|
|
25
|
+
if (!prefix) {
|
|
26
|
+
this.map.clear();
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
for (const k of [...this.map.keys()]) {
|
|
30
|
+
if (k.startsWith(prefix)) this.map.delete(k);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
async size() {
|
|
34
|
+
return this.map.size;
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
function tileKey(sourceId, z, x, y) {
|
|
38
|
+
return `${sourceId}:${z}:${x}:${y}`;
|
|
39
|
+
}
|
|
40
|
+
var OFFLINE_PROTOCOL = "caoguo-offline";
|
|
41
|
+
function parseOfflineUrl(url) {
|
|
42
|
+
const m = /^caoguo-offline:\/\/([^/]+)\/(\d+)\/(\d+)\/(\d+)$/.exec(url);
|
|
43
|
+
if (!m) return null;
|
|
44
|
+
return {
|
|
45
|
+
sourceId: decodeURIComponent(m[1]),
|
|
46
|
+
z: Number(m[2]),
|
|
47
|
+
x: Number(m[3]),
|
|
48
|
+
y: Number(m[4])
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// src/offline/protocol.ts
|
|
53
|
+
function offlineTileUrl(sourceId, z, x, y) {
|
|
54
|
+
return `${OFFLINE_PROTOCOL}://${encodeURIComponent(sourceId)}/${z}/${x}/${y}`;
|
|
55
|
+
}
|
|
56
|
+
function offlineSourceTiles(sourceId) {
|
|
57
|
+
return [`${OFFLINE_PROTOCOL}://${encodeURIComponent(sourceId)}/{z}/{x}/{y}`];
|
|
58
|
+
}
|
|
59
|
+
function toArrayBuffer(data) {
|
|
60
|
+
if (data instanceof Uint8Array) {
|
|
61
|
+
const out2 = new ArrayBuffer(data.byteLength);
|
|
62
|
+
new Uint8Array(out2).set(data);
|
|
63
|
+
return out2;
|
|
64
|
+
}
|
|
65
|
+
const src = data;
|
|
66
|
+
const out = new ArrayBuffer(src.byteLength);
|
|
67
|
+
new Uint8Array(out).set(new Uint8Array(src));
|
|
68
|
+
return out;
|
|
69
|
+
}
|
|
70
|
+
function createOfflineLoader(ctx) {
|
|
71
|
+
return (params) => {
|
|
72
|
+
const url = params.url;
|
|
73
|
+
const parsed = parseOfflineUrl(url);
|
|
74
|
+
if (!parsed) {
|
|
75
|
+
return Promise.reject(new Error(`\u975E\u6CD5\u79BB\u7EBF URL: ${url}`));
|
|
76
|
+
}
|
|
77
|
+
const key = tileKey(parsed.sourceId, parsed.z, parsed.x, parsed.y);
|
|
78
|
+
return ctx.store.get(key).then((tile) => {
|
|
79
|
+
if (!tile) {
|
|
80
|
+
throw new Error(`\u79BB\u7EBF\u74E6\u7247\u7F3A\u5931: ${key}`);
|
|
81
|
+
}
|
|
82
|
+
return { data: toArrayBuffer(tile.data) };
|
|
83
|
+
});
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
function registerOfflineProtocol(maplibregl, ctx) {
|
|
87
|
+
maplibregl.addProtocol(OFFLINE_PROTOCOL, createOfflineLoader(ctx));
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// src/offline/packer.ts
|
|
91
|
+
var MAX_ZOOM_DEFAULT = 14;
|
|
92
|
+
function lon2x(lon, z) {
|
|
93
|
+
return Math.floor((lon + 180) / 360 * Math.pow(2, z));
|
|
94
|
+
}
|
|
95
|
+
function lat2y(lat, z) {
|
|
96
|
+
const rad = lat * Math.PI / 180;
|
|
97
|
+
return Math.floor(
|
|
98
|
+
(1 - Math.log(Math.tan(rad) + 1 / Math.cos(rad)) / Math.PI) / 2 * Math.pow(2, z)
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
function tileBounds(z, x, y) {
|
|
102
|
+
const n = Math.pow(2, z);
|
|
103
|
+
const lonWest = x / n * 360 - 180;
|
|
104
|
+
const lonEast = (x + 1) / n * 360 - 180;
|
|
105
|
+
const latNorth = Math.atan(Math.sinh(Math.PI * (1 - 2 * y / n))) * 180 / Math.PI;
|
|
106
|
+
const latSouth = Math.atan(Math.sinh(Math.PI * (1 - 2 * (y + 1) / n))) * 180 / Math.PI;
|
|
107
|
+
return [lonWest, latSouth, lonEast, latNorth];
|
|
108
|
+
}
|
|
109
|
+
function coordInTile(lon, lat, z, x, y) {
|
|
110
|
+
const [w, s, e, n] = tileBounds(z, x, y);
|
|
111
|
+
return lon >= w && lon <= e && lat >= s && lat <= n;
|
|
112
|
+
}
|
|
113
|
+
async function packGeoJSONToStore(store, opts) {
|
|
114
|
+
const maxZoom = opts.maxZoom ?? MAX_ZOOM_DEFAULT;
|
|
115
|
+
const expires = opts.expires ?? 0;
|
|
116
|
+
const buckets = /* @__PURE__ */ new Map();
|
|
117
|
+
for (const f of opts.geojson.features) {
|
|
118
|
+
const g = f.geometry;
|
|
119
|
+
const coords = [];
|
|
120
|
+
if (g.type === "Point" || g.type === "Circle") {
|
|
121
|
+
coords.push(g.coordinates);
|
|
122
|
+
} else if (g.type === "LineString" || g.type === "MultiPoint") {
|
|
123
|
+
coords.push(...g.coordinates);
|
|
124
|
+
} else if (g.type === "Polygon" || g.type === "MultiLineString") {
|
|
125
|
+
for (const ring of g.coordinates) coords.push(...ring);
|
|
126
|
+
} else if (g.type === "MultiPolygon") {
|
|
127
|
+
for (const poly of g.coordinates) {
|
|
128
|
+
for (const ring of poly) coords.push(...ring);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
for (const [lon, lat] of coords) {
|
|
132
|
+
for (let z = 0; z <= maxZoom; z++) {
|
|
133
|
+
const x = lon2x(lon, z);
|
|
134
|
+
const y = lat2y(lat, z);
|
|
135
|
+
if (coordInTile(lon, lat, z, x, y)) {
|
|
136
|
+
const k = tileKey(opts.sourceId, z, x, y);
|
|
137
|
+
if (!buckets.has(k)) buckets.set(k, []);
|
|
138
|
+
buckets.get(k).push(f);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
const total = buckets.size;
|
|
144
|
+
let done = 0;
|
|
145
|
+
for (const [k, features] of buckets) {
|
|
146
|
+
const fc = { type: "FeatureCollection", features };
|
|
147
|
+
const tile = {
|
|
148
|
+
data: new TextEncoder().encode(JSON.stringify(fc)),
|
|
149
|
+
format: "geojson",
|
|
150
|
+
expires
|
|
151
|
+
};
|
|
152
|
+
await store.put(k, tile);
|
|
153
|
+
done++;
|
|
154
|
+
opts.onProgress?.(done, total);
|
|
155
|
+
}
|
|
156
|
+
return total;
|
|
157
|
+
}
|
|
158
|
+
function offlineGeoJSONSource(sourceId) {
|
|
159
|
+
return {
|
|
160
|
+
type: "geojson",
|
|
161
|
+
data: void 0,
|
|
162
|
+
tiles: [`caoguo-offline://${encodeURIComponent(sourceId)}/{z}/{x}/{y}`],
|
|
163
|
+
_caoguoOffline: true,
|
|
164
|
+
_sourceId: sourceId
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// src/offline/serviceWorker.ts
|
|
169
|
+
var CACHEABLE_HOSTS = ["t0.tianditu.gov.cn", "t7.tianditu.gov.cn", "tianditu.gov.cn"];
|
|
170
|
+
var CACHE_NAME = "caoguo-offline-tiles-v1";
|
|
171
|
+
var MSG_AIRGAP = "caoguo:airgap";
|
|
172
|
+
function shouldCache(request) {
|
|
173
|
+
if (request.method !== "GET") return false;
|
|
174
|
+
try {
|
|
175
|
+
const u = new URL(request.url);
|
|
176
|
+
if (u.protocol === `${OFFLINE_PROTOCOL}:`) return false;
|
|
177
|
+
return CACHEABLE_HOSTS.some((h) => u.host === h || u.host.endsWith(`.${h}`));
|
|
178
|
+
} catch {
|
|
179
|
+
return false;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
function resolveCacheKey(request) {
|
|
183
|
+
return request.url;
|
|
184
|
+
}
|
|
185
|
+
async function resolveFromStore(store, request) {
|
|
186
|
+
if (!store) return void 0;
|
|
187
|
+
const z = /TILEMATRIX=(\d+)/.exec(request.url)?.[1];
|
|
188
|
+
const x = /TILECOL=(\d+)/.exec(request.url)?.[1];
|
|
189
|
+
const y = /TILEROW=(\d+)/.exec(request.url)?.[1];
|
|
190
|
+
if (!z || !x || !y) return void 0;
|
|
191
|
+
const layer = /LAYER=([a-z]+)/.exec(request.url)?.[1] ?? "tianditu";
|
|
192
|
+
const key = tileKey(`td-${layer}`, Number(z), Number(x), Number(y));
|
|
193
|
+
const tile = await store.get(key);
|
|
194
|
+
if (!tile) return void 0;
|
|
195
|
+
const buf = tile.data instanceof Uint8Array ? tile.data : new Uint8Array(tile.data);
|
|
196
|
+
return { data: buf.buffer, contentType: "image/png" };
|
|
197
|
+
}
|
|
198
|
+
async function resolveResponse(request, deps) {
|
|
199
|
+
if (!shouldCache(request)) return null;
|
|
200
|
+
const key = resolveCacheKey(request);
|
|
201
|
+
const cached = await deps.cache.match(key);
|
|
202
|
+
if (cached && cached.ok) {
|
|
203
|
+
return {
|
|
204
|
+
data: await cached.arrayBuffer(),
|
|
205
|
+
contentType: cached.headers.get("content-type") ?? "image/png",
|
|
206
|
+
source: "cache",
|
|
207
|
+
cacheable: false
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
if (deps.airgap.enabled) {
|
|
211
|
+
const fromStore = await resolveFromStore(deps.store, request);
|
|
212
|
+
if (fromStore) {
|
|
213
|
+
return { ...fromStore, source: "store", cacheable: false };
|
|
214
|
+
}
|
|
215
|
+
return null;
|
|
216
|
+
}
|
|
217
|
+
try {
|
|
218
|
+
const resp = await deps.fetchImpl(request.url);
|
|
219
|
+
if (resp.ok) {
|
|
220
|
+
const data = await resp.arrayBuffer();
|
|
221
|
+
const contentType = resp.headers.get("content-type") ?? "image/png";
|
|
222
|
+
void deps.cache.put(key, {
|
|
223
|
+
ok: true,
|
|
224
|
+
status: resp.status,
|
|
225
|
+
arrayBuffer: async () => data,
|
|
226
|
+
headers: { get: (k) => k.toLowerCase() === "content-type" ? contentType : null }
|
|
227
|
+
}).catch(() => void 0);
|
|
228
|
+
return { data, contentType, source: "network", cacheable: true };
|
|
229
|
+
}
|
|
230
|
+
} catch {
|
|
231
|
+
const fromStore = await resolveFromStore(deps.store, request);
|
|
232
|
+
if (fromStore) {
|
|
233
|
+
return { ...fromStore, source: "store", cacheable: false };
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
return null;
|
|
237
|
+
}
|
|
238
|
+
function createFetchHandler(opts) {
|
|
239
|
+
const airgap = opts.airgap ?? { enabled: false };
|
|
240
|
+
return (event) => {
|
|
241
|
+
const request = event.request;
|
|
242
|
+
event.respondWith(
|
|
243
|
+
(async () => {
|
|
244
|
+
const deps = {
|
|
245
|
+
fetchImpl: (u) => fetch(u),
|
|
246
|
+
cache: caches,
|
|
247
|
+
airgap,
|
|
248
|
+
store: opts.store
|
|
249
|
+
};
|
|
250
|
+
const res = await resolveResponse(request, deps);
|
|
251
|
+
if (res) {
|
|
252
|
+
return new Response(res.data, {
|
|
253
|
+
status: 200,
|
|
254
|
+
headers: { "content-type": res.contentType, "x-caoguo-source": res.source }
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
return fetch(request.url);
|
|
258
|
+
})()
|
|
259
|
+
);
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
function installServiceWorker(sw) {
|
|
263
|
+
const airgap = { enabled: false };
|
|
264
|
+
sw.addEventListener("install", () => {
|
|
265
|
+
sw.skipWaiting?.();
|
|
266
|
+
});
|
|
267
|
+
sw.addEventListener("activate", () => {
|
|
268
|
+
sw.clients?.claim?.();
|
|
269
|
+
});
|
|
270
|
+
sw.addEventListener("message", (ev) => {
|
|
271
|
+
const msg = ev;
|
|
272
|
+
if (msg.data?.type === MSG_AIRGAP) {
|
|
273
|
+
airgap.enabled = !!msg.data.enabled;
|
|
274
|
+
}
|
|
275
|
+
});
|
|
276
|
+
sw.addEventListener("fetch", createFetchHandler({ store: sw.store, airgap }));
|
|
277
|
+
return { airgap };
|
|
278
|
+
}
|
|
279
|
+
async function registerOfflineServiceWorker(scriptUrl) {
|
|
280
|
+
if (typeof navigator === "undefined" || !("serviceWorker" in navigator)) return null;
|
|
281
|
+
try {
|
|
282
|
+
return await navigator.serviceWorker.register(scriptUrl);
|
|
283
|
+
} catch {
|
|
284
|
+
return null;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
function setAirgap(registration, enabled) {
|
|
288
|
+
const ctrl = registration.active ?? registration.installing ?? registration.waiting;
|
|
289
|
+
ctrl?.postMessage({ type: MSG_AIRGAP, enabled });
|
|
290
|
+
}
|
|
291
|
+
var SW_TEMPLATE_HINT = "importScripts('caoguo-offline-sw.bundle.js'); installServiceWorker(self);";
|
|
292
|
+
|
|
293
|
+
// src/offline/idbStore.ts
|
|
294
|
+
var dbPromise = null;
|
|
295
|
+
function getDb() {
|
|
296
|
+
if (!dbPromise) {
|
|
297
|
+
dbPromise = (async () => {
|
|
298
|
+
const { openDB } = await import('idb');
|
|
299
|
+
return openDB("caoguo-offline", 1, {
|
|
300
|
+
upgrade(db) {
|
|
301
|
+
if (!db.objectStoreNames.contains("tiles")) {
|
|
302
|
+
const store = db.createObjectStore("tiles", { keyPath: "key" });
|
|
303
|
+
store.createIndex("sourceId", "sourceId");
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
});
|
|
307
|
+
})();
|
|
308
|
+
}
|
|
309
|
+
return dbPromise;
|
|
310
|
+
}
|
|
311
|
+
var IdbTileStore = class {
|
|
312
|
+
async get(key) {
|
|
313
|
+
const db = await getDb();
|
|
314
|
+
const row = await db.get("tiles", key);
|
|
315
|
+
if (!row) return void 0;
|
|
316
|
+
if (row.expires && row.expires < Date.now()) {
|
|
317
|
+
await this.delete(key);
|
|
318
|
+
return void 0;
|
|
319
|
+
}
|
|
320
|
+
return { data: row.data, format: row.format, expires: row.expires };
|
|
321
|
+
}
|
|
322
|
+
async put(key, tile) {
|
|
323
|
+
const db = await getDb();
|
|
324
|
+
const sourceId = key.split(":")[0];
|
|
325
|
+
await db.put("tiles", {
|
|
326
|
+
key,
|
|
327
|
+
sourceId,
|
|
328
|
+
data: tile.data,
|
|
329
|
+
format: tile.format,
|
|
330
|
+
expires: tile.expires
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
async has(key) {
|
|
334
|
+
return await this.get(key) !== void 0;
|
|
335
|
+
}
|
|
336
|
+
async delete(key) {
|
|
337
|
+
const db = await getDb();
|
|
338
|
+
await db.delete("tiles", key);
|
|
339
|
+
}
|
|
340
|
+
async clear(prefix) {
|
|
341
|
+
const db = await getDb();
|
|
342
|
+
if (!prefix) {
|
|
343
|
+
await db.clear("tiles");
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
const tx = db.transaction("tiles", "readwrite");
|
|
347
|
+
const idx = tx.store.index("sourceId");
|
|
348
|
+
let cursor = await idx.openCursor(prefix);
|
|
349
|
+
while (cursor) {
|
|
350
|
+
await cursor.delete();
|
|
351
|
+
cursor = await cursor.continue();
|
|
352
|
+
}
|
|
353
|
+
await tx.done;
|
|
354
|
+
}
|
|
355
|
+
async size() {
|
|
356
|
+
const db = await getDb();
|
|
357
|
+
return db.count("tiles");
|
|
358
|
+
}
|
|
359
|
+
};
|
|
360
|
+
function createDefaultStore() {
|
|
361
|
+
if (typeof indexedDB !== "undefined") {
|
|
362
|
+
try {
|
|
363
|
+
return new IdbTileStore();
|
|
364
|
+
} catch {
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
return new MemoryTileStore();
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
export { CACHEABLE_HOSTS, CACHE_NAME, IdbTileStore, MSG_AIRGAP, MemoryTileStore, OFFLINE_PROTOCOL, SW_TEMPLATE_HINT, createDefaultStore, createFetchHandler, createOfflineLoader, installServiceWorker, offlineGeoJSONSource, offlineSourceTiles, offlineTileUrl, packGeoJSONToStore, parseOfflineUrl, registerOfflineProtocol, registerOfflineServiceWorker, resolveCacheKey, resolveFromStore, resolveResponse, setAirgap, shouldCache, tileKey };
|
|
371
|
+
//# sourceMappingURL=chunk-MNUQZUQA.js.map
|
|
372
|
+
//# sourceMappingURL=chunk-MNUQZUQA.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/offline/storage.ts","../src/offline/protocol.ts","../src/offline/packer.ts","../src/offline/serviceWorker.ts","../src/offline/idbStore.ts"],"names":["out"],"mappings":"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","file":"chunk-MNUQZUQA.js","sourcesContent":["/**\n * 离线瓦片存储抽象层(F-1.4)。\n *\n * 设计:存储后端可插拔。浏览器用 IndexedDB(idb),Node/测试用内存实现。\n * 瓦片 key 统一为 `${sourceId}:${z}:${x}:${y}`,value 为原始字节 + 过期时间。\n */\n\nexport type TileFormat = 'pbf' | 'geojson' | 'png' | 'mvt' | 'json';\n\nexport interface StoredTile {\n data: ArrayBuffer | Uint8Array;\n format: TileFormat;\n /** 过期时间戳(ms);0 表示永不过期 */\n expires: number;\n}\n\nexport interface TileStoreBackend {\n get(key: string): Promise<StoredTile | undefined>;\n put(key: string, tile: StoredTile): Promise<void>;\n has(key: string): Promise<boolean>;\n delete(key: string): Promise<void>;\n clear(prefix?: string): Promise<void>;\n size(): Promise<number>;\n}\n\n/** 内存实现(测试 / 无 IndexedDB 环境) */\nexport class MemoryTileStore implements TileStoreBackend {\n private map = new Map<string, StoredTile>();\n async get(key: string) {\n const t = this.map.get(key);\n if (!t) return undefined;\n if (t.expires && t.expires < Date.now()) {\n this.map.delete(key);\n return undefined;\n }\n return t;\n }\n async put(key: string, tile: StoredTile) {\n this.map.set(key, tile);\n }\n async has(key: string) {\n return (await this.get(key)) !== undefined;\n }\n async delete(key: string) {\n this.map.delete(key);\n }\n async clear(prefix?: string) {\n if (!prefix) {\n this.map.clear();\n return;\n }\n for (const k of [...this.map.keys()]) {\n if (k.startsWith(prefix)) this.map.delete(k);\n }\n }\n async size() {\n return this.map.size;\n }\n}\n\nexport function tileKey(sourceId: string, z: number, x: number, y: number): string {\n return `${sourceId}:${z}:${x}:${y}`;\n}\n\n/** 离线协议名(caoguo-offline://...)。集中定义,供 protocol 与 serviceWorker 共用。 */\nexport const OFFLINE_PROTOCOL = 'caoguo-offline';\n\n/**\n * 解析离线协议 URL:`caoguo-offline://{sourceId}/{z}/{x}/{y}`\n * 返回 sourceId 与瓦片坐标,便于 ProtocolHandler 取瓦片。\n */\nexport function parseOfflineUrl(\n url: string\n): { sourceId: string; z: number; x: number; y: number } | null {\n const m = /^caoguo-offline:\\/\\/([^/]+)\\/(\\d+)\\/(\\d+)\\/(\\d+)$/.exec(url);\n if (!m) return null;\n return {\n sourceId: decodeURIComponent(m[1]),\n z: Number(m[2]),\n x: Number(m[3]),\n y: Number(m[4]),\n };\n}\n","/**\n * 离线协议加载器(F-1.4)。\n *\n * 通过 MapLibre `addProtocol('caoguo-offline', loader)` 注册自定义协议,\n * 瓦片源可声明 `tiles: ['caoguo-offline://{sourceId}/{z}/{x}/{y}']`,\n * 引擎即从离线存储读取瓦片,实现「空气隔离」离线底图。\n *\n * MapLibre v4 的 protocol handler 签名:\n * (requestParameters, abortController) => Promise<{ data: ArrayBuffer }>\n */\n\nimport type { AddProtocolAction, RequestParameters } from 'maplibre-gl';\nimport { parseOfflineUrl, tileKey, OFFLINE_PROTOCOL, type TileStoreBackend } from './storage';\n\nexport { OFFLINE_PROTOCOL };\n\nexport interface OfflineProtocolContext {\n store: TileStoreBackend;\n}\n\n/** 构造离线协议 URL(供 source.tiles 使用) */\nexport function offlineTileUrl(sourceId: string, z: number, x: number, y: number): string {\n return `${OFFLINE_PROTOCOL}://${encodeURIComponent(sourceId)}/${z}/${x}/${y}`;\n}\n\n/** 生成离线栅格/矢量源的 tiles 数组 */\nexport function offlineSourceTiles(sourceId: string): string[] {\n return [`${OFFLINE_PROTOCOL}://${encodeURIComponent(sourceId)}/{z}/{x}/{y}`];\n}\n\n/** 把存储中的瓦片数据规整为 ArrayBuffer */\nfunction toArrayBuffer(data: ArrayBuffer | Uint8Array): ArrayBuffer {\n if (data instanceof Uint8Array) {\n const out = new ArrayBuffer(data.byteLength);\n new Uint8Array(out).set(data);\n return out;\n }\n // ArrayBufferLike 可能含 SharedArrayBuffer,复制为独立 ArrayBuffer\n const src = data as ArrayBuffer;\n const out = new ArrayBuffer(src.byteLength);\n new Uint8Array(out).set(new Uint8Array(src));\n return out;\n}\n\n/**\n * 创建离线协议 loader(返回可直接传给 addProtocol 的异步函数)。\n * 未命中瓦片时 reject,由 MapLibre 回退到线上源(若配置)。\n */\nexport function createOfflineLoader(ctx: OfflineProtocolContext): AddProtocolAction {\n return (params: RequestParameters) => {\n const url = params.url;\n const parsed = parseOfflineUrl(url);\n if (!parsed) {\n return Promise.reject(new Error(`非法离线 URL: ${url}`));\n }\n const key = tileKey(parsed.sourceId, parsed.z, parsed.x, parsed.y);\n return ctx.store.get(key).then((tile) => {\n if (!tile) {\n throw new Error(`离线瓦片缺失: ${key}`);\n }\n return { data: toArrayBuffer(tile.data) };\n });\n };\n}\n\n/** 在指定地图实例上注册离线协议 */\nexport function registerOfflineProtocol(\n maplibregl: { addProtocol: (name: string, action: AddProtocolAction) => void },\n ctx: OfflineProtocolContext\n): void {\n maplibregl.addProtocol(OFFLINE_PROTOCOL, createOfflineLoader(ctx));\n}\n","/**\n * 离线瓦片打包器(F-1.4)。\n *\n * 把本地瓦片导入离线存储:\n * - 原始 {z}/{x}/{y}.pbf|png 目录结构\n * - 单张 GeoJSON(按 z/x/y 网格切片为矢量瓦片缓冲,供 `caoguo-offline` 协议读取)\n *\n * 这里的「切片」为简化实现:GeoJSON 按瓦片网格空间包含关系分桶,\n * 仅把要素写入对应瓦片(不做 MVT 编码),存储为 UTF-8 JSON,\n * 由 protocol loader 以 `geojson` 格式返回,source type 用 geojson。\n * 生产级 MVT 编码(tippecanoe / @mapbox/vector-tile)可后续接入。\n */\n\nimport { tileKey, type StoredTile, type TileStoreBackend, type TileFormat } from './storage';\n\nconst MAX_ZOOM_DEFAULT = 14;\n\nfunction lon2x(lon: number, z: number): number {\n return Math.floor(((lon + 180) / 360) * Math.pow(2, z));\n}\nfunction lat2y(lat: number, z: number): number {\n const rad = (lat * Math.PI) / 180;\n return Math.floor(\n ((1 - Math.log(Math.tan(rad) + 1 / Math.cos(rad)) / Math.PI) / 2) * Math.pow(2, z)\n );\n}\n\nfunction tileBounds(z: number, x: number, y: number): [number, number, number, number] {\n const n = Math.pow(2, z);\n const lonWest = (x / n) * 360 - 180;\n const lonEast = ((x + 1) / n) * 360 - 180;\n const latNorth = (Math.atan(Math.sinh(Math.PI * (1 - (2 * y) / n))) * 180) / Math.PI;\n const latSouth = (Math.atan(Math.sinh(Math.PI * (1 - (2 * (y + 1)) / n))) * 180) / Math.PI;\n return [lonWest, latSouth, lonEast, latNorth];\n}\n\nfunction coordInTile(lon: number, lat: number, z: number, x: number, y: number): boolean {\n const [w, s, e, n] = tileBounds(z, x, y);\n return lon >= w && lon <= e && lat >= s && lat <= n;\n}\n\nexport interface PackGeoJSONOptions {\n sourceId: string;\n geojson: {\n type: 'FeatureCollection';\n features: Array<{\n geometry: { type: string; coordinates: unknown };\n properties: Record<string, unknown>;\n }>;\n };\n maxZoom?: number;\n /** 过期时间(ms 时间戳),0=永不过期 */\n expires?: number;\n onProgress?: (done: number, total: number) => void;\n}\n\n/**\n * 将 GeoJSON 按瓦片网格分桶写入离线存储。\n * 返回写入的瓦片数量。\n */\nexport async function packGeoJSONToStore(\n store: TileStoreBackend,\n opts: PackGeoJSONOptions\n): Promise<number> {\n const maxZoom = opts.maxZoom ?? MAX_ZOOM_DEFAULT;\n const expires = opts.expires ?? 0;\n const buckets = new Map<string, unknown[]>();\n\n for (const f of opts.geojson.features) {\n const g = f.geometry;\n const coords: number[][] = [];\n if (g.type === 'Point' || g.type === 'Circle') {\n coords.push(g.coordinates as number[]);\n } else if (g.type === 'LineString' || g.type === 'MultiPoint') {\n coords.push(...(g.coordinates as number[][]));\n } else if (g.type === 'Polygon' || g.type === 'MultiLineString') {\n for (const ring of g.coordinates as number[][][]) coords.push(...ring);\n } else if (g.type === 'MultiPolygon') {\n for (const poly of g.coordinates as number[][][][]) {\n for (const ring of poly) coords.push(...ring);\n }\n }\n for (const [lon, lat] of coords) {\n // 在每个缩放层级把要素写入其命中的瓦片(父瓦片亦包含,保证任意层级请求命中)\n for (let z = 0; z <= maxZoom; z++) {\n const x = lon2x(lon, z);\n const y = lat2y(lat, z);\n if (coordInTile(lon, lat, z, x, y)) {\n const k = tileKey(opts.sourceId, z, x, y);\n if (!buckets.has(k)) buckets.set(k, []);\n (buckets.get(k) as unknown[]).push(f);\n }\n }\n }\n }\n\n const total = buckets.size;\n let done = 0;\n for (const [k, features] of buckets) {\n const fc = { type: 'FeatureCollection', features };\n const tile: StoredTile = {\n data: new TextEncoder().encode(JSON.stringify(fc)),\n format: 'geojson' as TileFormat,\n expires,\n };\n await store.put(k, tile);\n done++;\n opts.onProgress?.(done, total);\n }\n return total;\n}\n\n/**\n * 由离线存储构造一个 geojson 源(用于 MapLibre source.tiles)。\n * 注意:geojson 协议瓦片由 protocol loader 直接返回 FeatureCollection。\n */\nexport function offlineGeoJSONSource(\n sourceId: string\n): { type: 'geojson'; data: never } & { tiles: string[]; _caoguoOffline: true; _sourceId: string } {\n return {\n type: 'geojson',\n data: undefined as never,\n tiles: [`caoguo-offline://${encodeURIComponent(sourceId)}/{z}/{x}/{y}`],\n _caoguoOffline: true,\n _sourceId: sourceId,\n } as never;\n}\n","/**\n * Service Worker 离线拦截(T5 / F-1.6)。\n *\n * 目标:在 T4 离线瓦片(IndexedDB + caoguo-offline 协议)之上,增加一层\n * **网络级二级缓存**——用 Cache API 缓存线上请求的瓦片(天地图等),并在\n * 「空气隔离(air-gap)」模式下拦截所有请求、用 Cache/离线存储兜底,\n * 实现断网后地图仍可平移缩放。\n *\n * 设计:\n * - `resolveCacheKey` / `shouldCache` / `resolveResponse` 是**纯函数 core**,\n * 不依赖真实 ServiceWorker 全局,可在 Node/vitest 中直接测试。\n * - `createFetchHandler` 把 core 包装成标准 `fetch` 事件处理器(注入 cache 与 store)。\n * - 浏览器侧用 `registerServiceWorker` 注册脚本,并通过 postMessage 切换空气隔离模式。\n *\n * 与 T4 的边界:T4 负责「业务离线瓦片 → IndexedDB → maplibre 协议」,\n * 本模块负责「线上瓦片请求 → Cache API 二级缓存 + 空气隔离拦截」,\n * 二者互补:T4 命中时根本不发网络请求(协议层短路),未命中时才走到 SW 缓存。\n */\n\nimport { OFFLINE_PROTOCOL, tileKey, type TileStoreBackend } from './storage';\n\n/** 需要二级缓存的瓦片来源(host 白名单,避免缓存无关资源) */\nexport const CACHEABLE_HOSTS = ['t0.tianditu.gov.cn', 't7.tianditu.gov.cn', 'tianditu.gov.cn'];\n\n/** 缓存名称(版本化,便于失效升级) */\nexport const CACHE_NAME = 'caoguo-offline-tiles-v1';\n\n/** 空气隔离模式消息协议 */\nexport const MSG_AIRGAP = 'caoguo:airgap' as const;\n\nexport interface AirgapMessage {\n type: typeof MSG_AIRGAP;\n /** true=开启空气隔离(断网模式),false=恢复在线优先 */\n enabled: boolean;\n}\n\n/**\n * 判断请求是否应进入离线二级缓存。\n * 只缓存 GET 请求 & 白名单 host & 图片/瓦片类型(保守策略)。\n */\nexport function shouldCache(request: { method: string; url: string }): boolean {\n if (request.method !== 'GET') return false;\n try {\n const u = new URL(request.url);\n // 离线协议请求(caoguo-offline://)由 T4 处理,不在此缓存\n if (u.protocol === `${OFFLINE_PROTOCOL}:`) return false;\n return CACHEABLE_HOSTS.some((h) => u.host === h || u.host.endsWith(`.${h}`));\n } catch {\n return false;\n }\n}\n\n/**\n * 计算 Cache Storage 的 key。优先用 URL(含 query,token 不同视为不同资源),\n * 否则回退到 request 对象本身。\n */\nexport function resolveCacheKey(request: { url: string }): string {\n return request.url;\n}\n\n/**\n * 从存储(IndexedDB/T4 离线)尝试取得瓦片兜底。\n * 用于空气隔离模式下网络不可达时返回上次离线打包的瓦片。\n */\nexport async function resolveFromStore(\n store: TileStoreBackend | undefined,\n request: { url: string }\n): Promise<{ data: ArrayBuffer; contentType: string } | undefined> {\n if (!store) return undefined;\n const z = /TILEMATRIX=(\\d+)/.exec(request.url)?.[1];\n const x = /TILECOL=(\\d+)/.exec(request.url)?.[1];\n const y = /TILEROW=(\\d+)/.exec(request.url)?.[1];\n if (!z || !x || !y) return undefined;\n // 天地图瓦片 key 以 sourceId 前缀;此处用 url 中的 layer 推断 sourceId\n const layer = /LAYER=([a-z]+)/.exec(request.url)?.[1] ?? 'tianditu';\n const key = tileKey(`td-${layer}`, Number(z), Number(x), Number(y));\n const tile = await store.get(key);\n if (!tile) return undefined;\n const buf = tile.data instanceof Uint8Array ? tile.data : new Uint8Array(tile.data);\n return { data: buf.buffer as ArrayBuffer, contentType: 'image/png' };\n}\n\nexport interface ResolveResponseDeps {\n /** 实际的 fetch 实现(浏览器为全局 fetch,测试可注入) */\n fetchImpl: (input: string) => Promise<{ ok: boolean; status: number; arrayBuffer: () => Promise<ArrayBuffer>; headers: { get: (k: string) => string | null } }>;\n /** Cache 接口(浏览器为全局 caches,测试可注入内存实现) */\n cache: {\n match: (key: string) => Promise<CacheEntry | undefined>;\n put: (key: string, resp: unknown) => Promise<void>;\n };\n /** 空气隔离开关(闭包状态,由消息控制) */\n airgap: { enabled: boolean };\n /** T4 离线存储(可选,空气隔离兜底) */\n store?: TileStoreBackend;\n}\n\ninterface CacheEntry {\n ok: boolean;\n status: number;\n arrayBuffer: () => Promise<ArrayBuffer>;\n headers: { get: (k: string) => string | null };\n}\n\nexport interface ResolvedResponse {\n /** 最终响应主体 */\n data: ArrayBuffer;\n /** 内容类型 */\n contentType: string;\n /** 来源:cache=命中缓存 / network=线上最新 / store=离线存储兜底 */\n source: 'cache' | 'network' | 'store';\n /** 该响应是否应写回缓存 */\n cacheable: boolean;\n}\n\n/**\n * 核心解析逻辑(纯函数,可测试)。\n * 两种模式:\n * - 在线优先(airgap=false):cache → network(写回 cache) → store 兜底\n * - 空气隔离(airgap=true):cache → store 兜底(不发网络请求)\n */\nexport async function resolveResponse(\n request: { method: string; url: string },\n deps: ResolveResponseDeps\n): Promise<ResolvedResponse | null> {\n if (!shouldCache(request)) return null;\n\n const key = resolveCacheKey(request);\n\n // 1) Cache 优先\n const cached = await deps.cache.match(key);\n if (cached && cached.ok) {\n return {\n data: await cached.arrayBuffer(),\n contentType: cached.headers.get('content-type') ?? 'image/png',\n source: 'cache',\n cacheable: false,\n };\n }\n\n // 2) 空气隔离模式:禁止网络,直接走离线存储兜底\n if (deps.airgap.enabled) {\n const fromStore = await resolveFromStore(deps.store, request);\n if (fromStore) {\n return { ...fromStore, source: 'store', cacheable: false };\n }\n return null; // 离线且无任何缓存:放弃(交由上层 404/透明恢复)\n }\n\n // 3) 在线优先:fetch 并写回 Cache\n try {\n const resp = await deps.fetchImpl(request.url);\n if (resp.ok) {\n const data = await resp.arrayBuffer();\n const contentType = resp.headers.get('content-type') ?? 'image/png';\n // 异步写回缓存(不阻塞响应)\n void deps.cache\n .put(key, {\n ok: true,\n status: resp.status,\n arrayBuffer: async () => data,\n headers: { get: (k: string) => (k.toLowerCase() === 'content-type' ? contentType : null) },\n })\n .catch(() => undefined);\n return { data, contentType, source: 'network', cacheable: true };\n }\n } catch {\n // 网络失败:回退 store\n const fromStore = await resolveFromStore(deps.store, request);\n if (fromStore) {\n return { ...fromStore, source: 'store', cacheable: false };\n }\n }\n return null;\n}\n\n/**\n * 包装为 Service Worker 的 fetch 事件处理器。\n * 注入真实的 caches / fetch / store。在浏览器 SW 全局中调用:\n * self.addEventListener('fetch', createFetchHandler({ store }));\n */\nexport function createFetchHandler(opts: {\n store?: TileStoreBackend;\n airgap?: { enabled: boolean };\n}): (event: { request: { method: string; url: string }; respondWith: (r: Promise<Response> | Response) => void }) => void {\n const airgap = opts.airgap ?? { enabled: false };\n return (event) => {\n const request = event.request;\n event.respondWith(\n (async () => {\n const deps: ResolveResponseDeps = {\n fetchImpl: (u) => fetch(u) as unknown as ResolveResponseDeps['fetchImpl'] extends (a: string) => infer R ? R : never,\n cache: caches as unknown as ResolveResponseDeps['cache'],\n airgap,\n store: opts.store,\n };\n const res = await resolveResponse(request, deps);\n if (res) {\n return new Response(res.data, {\n status: 200,\n headers: { 'content-type': res.contentType, 'x-caoguo-source': res.source },\n });\n }\n // 非瓦片请求 / 空气隔离缺失:透明放行\n return fetch(request.url);\n })()\n );\n };\n}\n\n/**\n * 在 Service Worker 全局上安装离线拦截逻辑(浏览器 SW 脚本入口)。\n * 处理 install/activate 生命周期、airgap 消息、fetch 拦截。\n */\nexport function installServiceWorker(sw: {\n addEventListener: (type: string, handler: (ev: unknown) => void) => void;\n caches: unknown;\n store?: TileStoreBackend;\n}): { airgap: { enabled: boolean } } {\n const airgap = { enabled: false };\n\n sw.addEventListener('install', () => {\n // @ts-expect-error 浏览器全局\n sw.skipWaiting?.();\n });\n sw.addEventListener('activate', () => {\n // @ts-expect-error 浏览器全局\n sw.clients?.claim?.();\n });\n sw.addEventListener('message', (ev: unknown) => {\n const msg = ev as { data?: AirgapMessage };\n if (msg.data?.type === MSG_AIRGAP) {\n airgap.enabled = !!msg.data.enabled;\n }\n });\n sw.addEventListener('fetch', createFetchHandler({ store: sw.store, airgap }) as never);\n\n return { airgap };\n}\n\n/**\n * 浏览器端注册离线 Service Worker(主线程调用)。\n * 需要项目提供 SW 脚本文件(见 `sw-template` 注释),并部署到与地图同域根路径。\n *\n * @param scriptUrl SW 脚本 URL(如 '/caoguo-sw.js'),需调用方自行构建/放置\n * @returns 注册成功的 ServiceWorker 实例;不支持时返回 null\n */\nexport async function registerOfflineServiceWorker(\n scriptUrl: string\n): Promise<ServiceWorkerRegistration | null> {\n if (typeof navigator === 'undefined' || !('serviceWorker' in navigator)) return null;\n try {\n return await navigator.serviceWorker.register(scriptUrl);\n } catch {\n return null;\n }\n}\n\n/**\n * 向已注册的 Service Worker 发送消息,切换「空气隔离」模式。\n * enabled=true 时地图进入断网可用状态(仅用 Cache + T4 离线存储)。\n */\nexport function setAirgap(registration: ServiceWorkerRegistration, enabled: boolean): void {\n const ctrl = registration.active ?? registration.installing ?? registration.waiting;\n ctrl?.postMessage({ type: MSG_AIRGAP, enabled } satisfies AirgapMessage as unknown);\n}\n\n/**\n * SW 脚本模板(供调用方写入部署文件,如 public/caoguo-sw.js)。\n * 该脚本通过 importScripts 复用本包编译产物,或直接内联 installServiceWorker。\n *\n * ```js\n * // caoguo-sw.js\n * importScripts('/caoguo-offline-sw.bundle.js'); // 含 installServiceWorker\n * installServiceWorker(self);\n * ```\n */\nexport const SW_TEMPLATE_HINT =\n \"importScripts('caoguo-offline-sw.bundle.js'); installServiceWorker(self);\";\n","/**\n * IndexedDB 离线瓦片后端(浏览器环境,F-1.4)。\n * 延迟依赖 idb;仅当运行在浏览器时加载,避免 Node/测试环境报错。\n */\n\nimport { MemoryTileStore, type StoredTile, type TileStoreBackend } from './storage';\n\ninterface RawRow {\n key: string;\n data: ArrayBuffer | Uint8Array;\n format: StoredTile['format'];\n expires: number;\n}\n\nlet dbPromise: Promise<unknown> | null = null;\n\nfunction getDb(): Promise<unknown> {\n if (!dbPromise) {\n dbPromise = (async () => {\n const { openDB } = await import('idb');\n return openDB('caoguo-offline', 1, {\n upgrade(db: any) {\n if (!db.objectStoreNames.contains('tiles')) {\n const store = db.createObjectStore('tiles', { keyPath: 'key' });\n store.createIndex('sourceId', 'sourceId');\n }\n },\n });\n })();\n }\n return dbPromise;\n}\n\nexport class IdbTileStore implements TileStoreBackend {\n async get(key: string): Promise<StoredTile | undefined> {\n const db: any = await getDb();\n const row: RawRow | undefined = await db.get('tiles', key);\n if (!row) return undefined;\n if (row.expires && row.expires < Date.now()) {\n await this.delete(key);\n return undefined;\n }\n return { data: row.data, format: row.format, expires: row.expires };\n }\n async put(key: string, tile: StoredTile): Promise<void> {\n const db: any = await getDb();\n const sourceId = key.split(':')[0];\n await db.put('tiles', {\n key,\n sourceId,\n data: tile.data,\n format: tile.format,\n expires: tile.expires,\n });\n }\n async has(key: string): Promise<boolean> {\n return (await this.get(key)) !== undefined;\n }\n async delete(key: string): Promise<void> {\n const db: any = await getDb();\n await db.delete('tiles', key);\n }\n async clear(prefix?: string): Promise<void> {\n const db: any = await getDb();\n if (!prefix) {\n await db.clear('tiles');\n return;\n }\n const tx = db.transaction('tiles', 'readwrite');\n const idx = tx.store.index('sourceId');\n let cursor = await idx.openCursor(prefix);\n while (cursor) {\n await cursor.delete();\n cursor = await cursor.continue();\n }\n await tx.done;\n }\n async size(): Promise<number> {\n const db: any = await getDb();\n return db.count('tiles');\n }\n}\n\n/** 浏览器环境默认用 IndexedDB,否则回退内存 */\nexport function createDefaultStore(): TileStoreBackend {\n if (typeof indexedDB !== 'undefined') {\n try {\n return new IdbTileStore();\n } catch {\n /* fallthrough */\n }\n }\n return new MemoryTileStore();\n}\n"]}
|