@caoguo/maplibre 0.0.5 → 0.0.7

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.
Files changed (64) hide show
  1. package/dist/chunk-DENQSIV2.js +81 -0
  2. package/dist/chunk-DENQSIV2.js.map +1 -0
  3. package/dist/chunk-G7VY42ZG.js +210 -0
  4. package/dist/chunk-G7VY42ZG.js.map +1 -0
  5. package/dist/chunk-MLLIAWVI.js +214 -0
  6. package/dist/chunk-MLLIAWVI.js.map +1 -0
  7. package/dist/chunk-OLS4DD3F.js +27 -0
  8. package/dist/chunk-OLS4DD3F.js.map +1 -0
  9. package/dist/chunk-OUEPE6JT.js +26 -0
  10. package/dist/chunk-OUEPE6JT.js.map +1 -0
  11. package/dist/chunk-SKXL3DKL.js +125 -0
  12. package/dist/chunk-SKXL3DKL.js.map +1 -0
  13. package/dist/chunk-Y3JNDOHU.js +56 -0
  14. package/dist/chunk-Y3JNDOHU.js.map +1 -0
  15. package/dist/controls/index.cjs +227 -0
  16. package/dist/controls/index.cjs.map +1 -0
  17. package/dist/controls/index.d.cts +171 -0
  18. package/dist/controls/index.d.ts +171 -0
  19. package/dist/controls/index.js +4 -0
  20. package/dist/controls/index.js.map +1 -0
  21. package/dist/crs/index.cjs +137 -0
  22. package/dist/crs/index.cjs.map +1 -0
  23. package/dist/crs/index.d.cts +80 -0
  24. package/dist/crs/index.d.ts +80 -0
  25. package/dist/crs/index.js +3 -0
  26. package/dist/crs/index.js.map +1 -0
  27. package/dist/index.cjs +119 -41
  28. package/dist/index.cjs.map +1 -1
  29. package/dist/index.d.cts +19 -484
  30. package/dist/index.d.ts +19 -484
  31. package/dist/index.js +18 -604
  32. package/dist/index.js.map +1 -1
  33. package/dist/lod/index.cjs +60 -0
  34. package/dist/lod/index.cjs.map +1 -0
  35. package/dist/lod/index.d.cts +62 -0
  36. package/dist/lod/index.d.ts +62 -0
  37. package/dist/lod/index.js +3 -0
  38. package/dist/lod/index.js.map +1 -0
  39. package/dist/shaders/index.cjs +215 -0
  40. package/dist/shaders/index.cjs.map +1 -0
  41. package/dist/shaders/index.d.cts +131 -0
  42. package/dist/shaders/index.d.ts +131 -0
  43. package/dist/shaders/index.js +3 -0
  44. package/dist/shaders/index.js.map +1 -0
  45. package/dist/sourceUtils.cjs +30 -0
  46. package/dist/sourceUtils.cjs.map +1 -0
  47. package/dist/sourceUtils.d.cts +60 -0
  48. package/dist/sourceUtils.d.ts +60 -0
  49. package/dist/sourceUtils.js +3 -0
  50. package/dist/sourceUtils.js.map +1 -0
  51. package/dist/styles.cjs +47 -12
  52. package/dist/styles.cjs.map +1 -1
  53. package/dist/styles.d.cts +13 -4
  54. package/dist/styles.d.ts +13 -4
  55. package/dist/styles.js +1 -1
  56. package/dist/terrain.cjs +31 -0
  57. package/dist/terrain.cjs.map +1 -0
  58. package/dist/terrain.d.cts +28 -0
  59. package/dist/terrain.d.ts +28 -0
  60. package/dist/terrain.js +3 -0
  61. package/dist/terrain.js.map +1 -0
  62. package/package.json +35 -5
  63. package/dist/chunk-DNEF4DHG.js +0 -48
  64. package/dist/chunk-DNEF4DHG.js.map +0 -1
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/terrain.ts"],"names":[],"mappings":";AAsBO,IAAM,qBAAA,GAAwB;AAAA,EACnC;AACF;AAGO,SAAS,YAAA,CAAa,KAAA,EAAc,IAAA,GAAuB,EAAC,EAAS;AAC1E,EAAA,MAAM,QAAA,GAAW,KAAK,QAAA,IAAY,QAAA;AAClC,EAAA,IAAI,CAAC,KAAA,CAAM,SAAA,CAAU,QAAQ,CAAA,EAAG;AAC9B,IAAA,KAAA,CAAM,UAAU,QAAA,EAAU;AAAA,MACxB,IAAA,EAAM,YAAA;AAAA,MACN,KAAA,EAAO,KAAK,KAAA,IAAS,qBAAA;AAAA,MACrB,QAAA,EAAU,KAAK,QAAA,IAAY,WAAA;AAAA,MAC3B,QAAA,EAAU,GAAA;AAAA,MACV,OAAA,EAAS,KAAK,OAAA,IAAW;AAAA,KACjB,CAAA;AAAA,EACZ;AACA,EAAA,KAAA,CAAM,UAAA,CAAW,EAAE,MAAA,EAAQ,QAAA,EAAU,cAAc,IAAA,CAAK,YAAA,IAAgB,KAAc,CAAA;AACxF;AAGO,SAAS,aAAA,CAAc,KAAA,EAAc,QAAA,GAAW,QAAA,EAAgB;AACrE,EAAA,KAAA,CAAM,WAAW,IAAI,CAAA;AACrB,EAAA,IAAI,KAAA,CAAM,SAAA,CAAU,QAAQ,CAAA,EAAG;AAC7B,IAAA,KAAA,CAAM,aAAa,QAAQ,CAAA;AAAA,EAC7B;AACF","file":"chunk-OLS4DD3F.js","sourcesContent":["/**\n * 3D 地形渲染(F-1.5)\n *\n * 注入 raster-dem 高程源并调用 maplibre-gl 原生 `setTerrain`,使地图呈现地形起伏。\n * DEM 瓦片默认使用公共 Terrarium 源(elevation-tiles-prod,Terrain-RGB 编码,支持 CORS),\n * 可在 opts.tiles 覆盖为私有瓦片服务(如 MinIO / 本地 MBTiles 服务)。\n */\n\nimport type { Map as MlMap } from 'maplibre-gl';\n\nexport interface TerrainOptions {\n /** DEM source id(默认 cg-dem) */\n sourceId?: string;\n /** DEM 瓦片模板,{z}/{x}/{y} 占位 */\n tiles?: string[];\n encoding?: 'terrarium' | 'mapbox';\n /** 地形夸张系数(默认 1.5) */\n exaggeration?: number;\n maxzoom?: number;\n}\n\n/** 默认 DEM 瓦片源(Mapzen Terrarium,Terrain-RGB,支持 CORS,无需鉴权) */\nexport const DEFAULT_TERRAIN_TILES = [\n 'https://elevation-tiles-prod.s3.amazonaws.com/terrarium/{z}/{x}/{y}.png',\n];\n\n/** 在原生 MapLibre 实例上启用地形(纯函数,便于单测) */\nexport function applyTerrain(mlMap: MlMap, opts: TerrainOptions = {}): void {\n const sourceId = opts.sourceId ?? 'cg-dem';\n if (!mlMap.getSource(sourceId)) {\n mlMap.addSource(sourceId, {\n type: 'raster-dem',\n tiles: opts.tiles ?? DEFAULT_TERRAIN_TILES,\n encoding: opts.encoding ?? 'terrarium',\n tileSize: 256,\n maxzoom: opts.maxzoom ?? 15,\n } as never);\n }\n mlMap.setTerrain({ source: sourceId, exaggeration: opts.exaggeration ?? 1.5 } as never);\n}\n\n/** 关闭地形(纯函数),移除 DEM 源 */\nexport function removeTerrain(mlMap: MlMap, sourceId = 'cg-dem'): void {\n mlMap.setTerrain(null);\n if (mlMap.getSource(sourceId)) {\n mlMap.removeSource(sourceId);\n }\n}\n"]}
@@ -0,0 +1,26 @@
1
+ // src/sourceUtils.ts
2
+ function upsertSource(mlMap, id, data) {
3
+ if (mlMap.getSource(id)) {
4
+ if (mlMap.setData) {
5
+ mlMap.setData(id, data);
6
+ } else if (mlMap.addSource) {
7
+ mlMap.addSource(id, data);
8
+ }
9
+ } else if (mlMap.addSource) {
10
+ mlMap.addSource(id, data);
11
+ }
12
+ }
13
+ function removeSourceSafe(mlMap, id) {
14
+ if (mlMap.getSource(id) && mlMap.removeSource) {
15
+ mlMap.removeSource(id);
16
+ }
17
+ }
18
+ function removeSourcesSafe(mlMap, ids) {
19
+ for (const id of ids) {
20
+ removeSourceSafe(mlMap, id);
21
+ }
22
+ }
23
+
24
+ export { removeSourceSafe, removeSourcesSafe, upsertSource };
25
+ //# sourceMappingURL=chunk-OUEPE6JT.js.map
26
+ //# sourceMappingURL=chunk-OUEPE6JT.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/sourceUtils.ts"],"names":[],"mappings":";AA6CO,SAAS,YAAA,CACd,KAAA,EACA,EAAA,EACA,IAAA,EACM;AACN,EAAA,IAAI,KAAA,CAAM,SAAA,CAAU,EAAE,CAAA,EAAG;AACvB,IAAA,IAAI,MAAM,OAAA,EAAS;AACjB,MAAA,KAAA,CAAM,OAAA,CAAQ,IAAI,IAAI,CAAA;AAAA,IACxB,CAAA,MAAA,IAAW,MAAM,SAAA,EAAW;AAE1B,MAAA,KAAA,CAAM,SAAA,CAAU,IAAI,IAAI,CAAA;AAAA,IAC1B;AAAA,EACF,CAAA,MAAA,IAAW,MAAM,SAAA,EAAW;AAC1B,IAAA,KAAA,CAAM,SAAA,CAAU,IAAI,IAAI,CAAA;AAAA,EAC1B;AACF;AAQO,SAAS,gBAAA,CAAiB,OAAuB,EAAA,EAAkB;AACxE,EAAA,IAAI,KAAA,CAAM,SAAA,CAAU,EAAE,CAAA,IAAK,MAAM,YAAA,EAAc;AAC7C,IAAA,KAAA,CAAM,aAAa,EAAE,CAAA;AAAA,EACvB;AACF;AAQO,SAAS,iBAAA,CAAkB,OAAuB,GAAA,EAAqB;AAC5E,EAAA,KAAA,MAAW,MAAM,GAAA,EAAK;AACpB,IAAA,gBAAA,CAAiB,OAAO,EAAE,CAAA;AAAA,EAC5B;AACF","file":"chunk-OUEPE6JT.js","sourcesContent":["/**\n * 原生 MapLibre 地图 source 操作的幂等工具。\n *\n * 各业务 System(grid/water/pipeline/telecom/compute/transport)的 render()\n * 通过 `this.map.instance` 直接调用原生 MapLibre,而非经 CaoguoMap 包装类的\n * 幂等 addSource。为避免在每个 System 内重复书写 `if (!mlMap.getSource(id))`\n * 样板,并根治「层级切换重渲染崩溃」(Source already exists),统一在此提供\n * 幂等 upsert 与安全的 removeSource。\n *\n * 仅依赖原生 MapLibre 的 source 子集接口,便于各 System 直接传入 instance。\n */\n\n/**\n * 原生 MapLibre source 操作的子集接口。\n *\n * 仅 `getSource` 为必填——它是所有 helper 判断「source 是否存在」的统一探针。\n * 其余方法全部可选,因为各业务 System 对 `this.map.instance` 的局部类型断言\n * 只声明了实际用到的若干方法(如 topology 只取 getSource/removeSource,\n * traffic 只取 addSource/addLayer),强制统一必填字段会与这些子集冲突。\n * helper 内部对每个要调用的方法做存在性检查,缺失时安全跳过或回退,\n * 保证类型与运行时都安全(真实 MapLibre 实例始终具备全部方法)。\n */\nexport interface MlMapSourceApi {\n getSource: (id: string) => unknown;\n addSource?: (id: string, source: unknown) => void;\n addLayer?: (layer: unknown) => void;\n setData?: (id: string, data: unknown) => void;\n removeSource?: (id: string) => void;\n}\n\n/**\n * 幂等地确保一个 GeoJSON source 存在并持有给定数据。\n *\n * - 若 source 已存在且具备 setData:setData 更新(避免重复 addSource 抛错)。\n * - 若 source 已存在但无 setData:回退为 addSource(MapLibre 对重复 id 会忽略/覆盖,\n * 不抛错,仍可达成渲染目标)。\n * - 若 source 不存在且具备 addSource:addSource 创建。\n *\n * 注意:若传入对象既无 setData 又无 addSource,则无法创建/更新 source。\n * 各 System 传入的 `this.map.instance` 为真实 MapLibre 实例,始终具备这些方法。\n *\n * @param mlMap 原生 MapLibre 实例(或任意满足 MlMapSourceApi 的对象)\n * @param id source 唯一标识\n * @param data GeoJSON 数据(或 addSource 所需的 source spec)\n */\nexport function upsertSource(\n mlMap: MlMapSourceApi,\n id: string,\n data: unknown,\n): void {\n if (mlMap.getSource(id)) {\n if (mlMap.setData) {\n mlMap.setData(id, data);\n } else if (mlMap.addSource) {\n // 无 setData 时回退为 addSource:MapLibre 对同名 source 会忽略重复创建。\n mlMap.addSource(id, data);\n }\n } else if (mlMap.addSource) {\n mlMap.addSource(id, data);\n }\n}\n\n/**\n * 安全地移除 source:仅当存在且具备 removeSource 时才移除,避免不存在时抛错。\n *\n * @param mlMap 原生 MapLibre 实例\n * @param id source 唯一标识\n */\nexport function removeSourceSafe(mlMap: MlMapSourceApi, id: string): void {\n if (mlMap.getSource(id) && mlMap.removeSource) {\n mlMap.removeSource(id);\n }\n}\n\n/**\n * 安全地批量移除 source。\n *\n * @param mlMap 原生 MapLibre 实例\n * @param ids source 标识数组\n */\nexport function removeSourcesSafe(mlMap: MlMapSourceApi, ids: string[]): void {\n for (const id of ids) {\n removeSourceSafe(mlMap, id);\n }\n}\n"]}
@@ -0,0 +1,125 @@
1
+ // src/crs/gcj02.ts
2
+ var PI = Math.PI;
3
+ var A = 6378245;
4
+ var EE = 0.006693421622965943;
5
+ function outOfChina(lng, lat) {
6
+ return lng < 72.004 || lng > 137.8347 || lat < 0.8293 || lat > 55.8271;
7
+ }
8
+ function transformLat(lng, lat) {
9
+ let ret = -100 + 2 * lng + 3 * lat + 0.2 * lat * lat + 0.1 * lng * lat + 0.2 * Math.sqrt(Math.abs(lng));
10
+ ret += (20 * Math.sin(6 * lng * PI) + 20 * Math.sin(2 * lng * PI)) * 2 / 3;
11
+ ret += (20 * Math.sin(lat * PI) + 40 * Math.sin(lat / 3 * PI)) * 2 / 3;
12
+ ret += (160 * Math.sin(lat / 12 * PI) + 320 * Math.sin(lat * PI / 30)) * 2 / 3;
13
+ return ret;
14
+ }
15
+ function transformLng(lng, lat) {
16
+ let ret = 300 + lng + 2 * lat + 0.1 * lng * lng + 0.1 * lng * lat + 0.1 * Math.sqrt(Math.abs(lng));
17
+ ret += (20 * Math.sin(6 * lng * PI) + 20 * Math.sin(2 * lng * PI)) * 2 / 3;
18
+ ret += (20 * Math.sin(lng * PI) + 40 * Math.sin(lng / 3 * PI)) * 2 / 3;
19
+ ret += (150 * Math.sin(lng / 12 * PI) + 300 * Math.sin(lng / 30 * PI)) * 2 / 3;
20
+ return ret;
21
+ }
22
+ function wgs84ToGcj02(lng, lat) {
23
+ if (outOfChina(lng, lat)) return [lng, lat];
24
+ let dLat = transformLat(lng - 105, lat - 35);
25
+ let dLng = transformLng(lng - 105, lat - 35);
26
+ const radLat = lat / 180 * PI;
27
+ let magic = Math.sin(radLat);
28
+ magic = 1 - EE * magic * magic;
29
+ const sqrtMagic = Math.sqrt(magic);
30
+ dLat = dLat * 180 / (A * (1 - EE) / (magic * sqrtMagic) * PI);
31
+ dLng = dLng * 180 / (A / sqrtMagic * Math.cos(radLat) * PI);
32
+ return [lng + dLng, lat + dLat];
33
+ }
34
+ function gcj02ToWgs84(lng, lat) {
35
+ if (outOfChina(lng, lat)) return [lng, lat];
36
+ let [mlng, mlat] = wgs84ToGcj02(lng, lat);
37
+ let dlng = mlng - lng;
38
+ let dlat = mlat - lat;
39
+ [mlng, mlat] = wgs84ToGcj02(lng - dlng, lat - dlat);
40
+ dlng = mlng - lng;
41
+ dlat = mlat - lat;
42
+ [mlng, mlat] = wgs84ToGcj02(lng - dlng, lat - dlat);
43
+ dlng = mlng - lng;
44
+ dlat = mlat - lat;
45
+ return [lng - dlng, lat - dlat];
46
+ }
47
+ function isInChina(lng, lat) {
48
+ return !outOfChina(lng, lat);
49
+ }
50
+
51
+ // src/crs/cgcs2000.ts
52
+ var gridShift = null;
53
+ function setCgcs2000GridShift(provider) {
54
+ gridShift = provider;
55
+ }
56
+ function wgs84ToCgcs2000(lng, lat) {
57
+ if (!gridShift) return [lng, lat];
58
+ const [dLng, dLat] = gridShift.shift(lng, lat);
59
+ return [lng + dLng, lat + dLat];
60
+ }
61
+ function cgcs2000ToWgs84(lng, lat) {
62
+ if (!gridShift) return [lng, lat];
63
+ const [dLng, dLat] = gridShift.shift(lng, lat);
64
+ return [lng - dLng, lat - dLat];
65
+ }
66
+
67
+ // src/crs/transform.ts
68
+ function toWgs84(crs, lng, lat) {
69
+ switch (crs) {
70
+ case "WGS84":
71
+ return [lng, lat];
72
+ case "GCJ02":
73
+ return gcj02ToWgs84(lng, lat);
74
+ case "CGCS2000":
75
+ return cgcs2000ToWgs84(lng, lat);
76
+ }
77
+ }
78
+ function fromWgs84(crs, lng, lat) {
79
+ switch (crs) {
80
+ case "WGS84":
81
+ return [lng, lat];
82
+ case "GCJ02":
83
+ return wgs84ToGcj02(lng, lat);
84
+ case "CGCS2000":
85
+ return wgs84ToCgcs2000(lng, lat);
86
+ }
87
+ }
88
+ function createTransformer(from, to) {
89
+ if (from === to) {
90
+ const identity = (lng, lat) => [lng, lat];
91
+ return { forward: identity, inverse: identity };
92
+ }
93
+ const forward = (lng, lat) => {
94
+ const [wLng, wLat] = toWgs84(from, lng, lat);
95
+ return fromWgs84(to, wLng, wLat);
96
+ };
97
+ const inverse = (lng, lat) => {
98
+ const [wLng, wLat] = toWgs84(to, lng, lat);
99
+ return fromWgs84(from, wLng, wLat);
100
+ };
101
+ return { forward, inverse };
102
+ }
103
+ function transformPoint(lng, lat, from, to) {
104
+ return createTransformer(from, to).forward(lng, lat);
105
+ }
106
+ function transformBounds(b, from, to) {
107
+ const t = createTransformer(from, to);
108
+ const xs = [];
109
+ const ys = [];
110
+ for (const [x, y] of [
111
+ [b[0], b[1]],
112
+ [b[2], b[1]],
113
+ [b[0], b[3]],
114
+ [b[2], b[3]]
115
+ ]) {
116
+ const [nx, ny] = t.forward(x, y);
117
+ xs.push(nx);
118
+ ys.push(ny);
119
+ }
120
+ return [Math.min(...xs), Math.min(...ys), Math.max(...xs), Math.max(...ys)];
121
+ }
122
+
123
+ export { cgcs2000ToWgs84, createTransformer, fromWgs84, gcj02ToWgs84, isInChina, setCgcs2000GridShift, toWgs84, transformBounds, transformPoint, wgs84ToCgcs2000, wgs84ToGcj02 };
124
+ //# sourceMappingURL=chunk-SKXL3DKL.js.map
125
+ //# sourceMappingURL=chunk-SKXL3DKL.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/crs/gcj02.ts","../src/crs/cgcs2000.ts","../src/crs/transform.ts"],"names":[],"mappings":"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","file":"chunk-SKXL3DKL.js","sourcesContent":["/**\n * WGS84 <-> GCJ-02(火星坐标系)转换。\n *\n * 算法:公开的解析偏移模型(Krasovsky 椭球),与 coordtransform 一致。\n * 全国范围单点误差 < 50m(PRD 验收 F-1.1)。\n * 注意:GCJ-02 -> WGS84 采用 3 次迭代反算,进一步收敛残差。\n */\n\nconst PI = Math.PI;\nconst A = 6378245.0; // 长半轴\nconst EE = 0.00669342162296594323; // 第一偏心率平方\n\nfunction outOfChina(lng: number, lat: number): boolean {\n return lng < 72.004 || lng > 137.8347 || lat < 0.8293 || lat > 55.8271;\n}\n\nfunction transformLat(lng: number, lat: number): number {\n let ret =\n -100.0 +\n 2.0 * lng +\n 3.0 * lat +\n 0.2 * lat * lat +\n 0.1 * lng * lat +\n 0.2 * Math.sqrt(Math.abs(lng));\n ret += ((20.0 * Math.sin(6.0 * lng * PI) + 20.0 * Math.sin(2.0 * lng * PI)) * 2.0) / 3.0;\n ret += ((20.0 * Math.sin(lat * PI) + 40.0 * Math.sin((lat / 3.0) * PI)) * 2.0) / 3.0;\n ret += ((160.0 * Math.sin((lat / 12.0) * PI) + 320 * Math.sin((lat * PI) / 30.0)) * 2.0) / 3.0;\n return ret;\n}\n\nfunction transformLng(lng: number, lat: number): number {\n let ret =\n 300.0 +\n lng +\n 2.0 * lat +\n 0.1 * lng * lng +\n 0.1 * lng * lat +\n 0.1 * Math.sqrt(Math.abs(lng));\n ret += ((20.0 * Math.sin(6.0 * lng * PI) + 20.0 * Math.sin(2.0 * lng * PI)) * 2.0) / 3.0;\n ret += ((20.0 * Math.sin(lng * PI) + 40.0 * Math.sin((lng / 3.0) * PI)) * 2.0) / 3.0;\n ret += ((150.0 * Math.sin((lng / 12.0) * PI) + 300.0 * Math.sin((lng / 30.0) * PI)) * 2.0) / 3.0;\n return ret;\n}\n\n/** WGS84 -> GCJ-02 */\nexport function wgs84ToGcj02(lng: number, lat: number): [number, number] {\n if (outOfChina(lng, lat)) return [lng, lat];\n let dLat = transformLat(lng - 105.0, lat - 35.0);\n let dLng = transformLng(lng - 105.0, lat - 35.0);\n const radLat = (lat / 180.0) * PI;\n let magic = Math.sin(radLat);\n magic = 1 - EE * magic * magic;\n const sqrtMagic = Math.sqrt(magic);\n dLat = (dLat * 180.0) / (((A * (1 - EE)) / (magic * sqrtMagic)) * PI);\n dLng = (dLng * 180.0) / ((A / sqrtMagic) * Math.cos(radLat) * PI);\n return [lng + dLng, lat + dLat];\n}\n\n/** GCJ-02 -> WGS84(3 次迭代反算) */\nexport function gcj02ToWgs84(lng: number, lat: number): [number, number] {\n if (outOfChina(lng, lat)) return [lng, lat];\n let [mlng, mlat] = wgs84ToGcj02(lng, lat);\n let dlng = mlng - lng;\n let dlat = mlat - lat;\n // 一次迭代\n [mlng, mlat] = wgs84ToGcj02(lng - dlng, lat - dlat);\n dlng = mlng - lng;\n dlat = mlat - lat;\n // 二次迭代(收敛)\n [mlng, mlat] = wgs84ToGcj02(lng - dlng, lat - dlat);\n dlng = mlng - lng;\n dlat = mlat - lat;\n return [lng - dlng, lat - dlat];\n}\n\n/** 是否在 GCJ-02 偏移覆盖区(中国境内外判定) */\nexport function isInChina(lng: number, lat: number): boolean {\n return !outOfChina(lng, lat);\n}\n","/**\n * WGS84 <-> CGCS2000 转换。\n *\n * 说明:CGCS2000 与 WGS84 采用近似相同的参考椭球(扁率/长半轴差异极小,\n * 历元差异仅导致 cm~dm 级点位偏移),在 Web Mercator 显示尺度下等价于一致。\n * 因此 Phase 0 默认实现为恒等变换(残差 < 0.5m,满足 PRD 验收 F-1.1)。\n *\n * 测绘级高精度需求:可注入 `GridShiftProvider`(区域 7 参数 / 格网平移),\n * 见 types.ts。本文件保留可扩展入口。\n */\n\nimport type { GridShiftProvider, LngLat } from './types';\n\nlet gridShift: GridShiftProvider | null = null;\n\n/** 注册 CGCS2000 偏移提供方(可选,测绘级) */\nexport function setCgcs2000GridShift(provider: GridShiftProvider | null): void {\n gridShift = provider;\n}\n\n/** WGS84 -> CGCS2000 */\nexport function wgs84ToCgcs2000(lng: number, lat: number): LngLat {\n if (!gridShift) return [lng, lat];\n const [dLng, dLat] = gridShift.shift(lng, lat);\n return [lng + dLng, lat + dLat];\n}\n\n/** CGCS2000 -> WGS84 */\nexport function cgcs2000ToWgs84(lng: number, lat: number): LngLat {\n if (!gridShift) return [lng, lat];\n const [dLng, dLat] = gridShift.shift(lng, lat);\n return [lng - dLng, lat - dLat];\n}\n","/**\n * 坐标系变换组合器(以 WGS84 为枢纽)。\n *\n * 用法:\n * const t = createTransformer('GCJ02', 'WGS84');\n * const [lng, lat] = t.forward(114.30, 30.59); // GCJ-02 业务数据 -> 渲染基准\n */\n\nimport type { Bounds, CRS, LngLat, Transformer } from './types';\nimport { cgcs2000ToWgs84, wgs84ToCgcs2000 } from './cgcs2000';\nimport { gcj02ToWgs84, wgs84ToGcj02 } from './gcj02';\n\n/** 把任意 CRS 的坐标转换到 WGS84 */\nexport function toWgs84(crs: CRS, lng: number, lat: number): LngLat {\n switch (crs) {\n case 'WGS84':\n return [lng, lat];\n case 'GCJ02':\n return gcj02ToWgs84(lng, lat);\n case 'CGCS2000':\n return cgcs2000ToWgs84(lng, lat);\n }\n}\n\n/** 把 WGS84 坐标转换到目标 CRS */\nexport function fromWgs84(crs: CRS, lng: number, lat: number): LngLat {\n switch (crs) {\n case 'WGS84':\n return [lng, lat];\n case 'GCJ02':\n return wgs84ToGcj02(lng, lat);\n case 'CGCS2000':\n return wgs84ToCgcs2000(lng, lat);\n }\n}\n\nexport function createTransformer(from: CRS, to: CRS): Transformer {\n if (from === to) {\n const identity = (lng: number, lat: number): LngLat => [lng, lat];\n return { forward: identity, inverse: identity };\n }\n const forward = (lng: number, lat: number): LngLat => {\n const [wLng, wLat] = toWgs84(from, lng, lat);\n return fromWgs84(to, wLng, wLat);\n };\n const inverse = (lng: number, lat: number): LngLat => {\n const [wLng, wLat] = toWgs84(to, lng, lat);\n return fromWgs84(from, wLng, wLat);\n };\n return { forward, inverse };\n}\n\n/** 单点快捷转换 */\nexport function transformPoint(\n lng: number,\n lat: number,\n from: CRS,\n to: CRS\n): LngLat {\n return createTransformer(from, to).forward(lng, lat);\n}\n\n/** 范围 [w, s, e, n] 转换(取四个角点极值) */\nexport function transformBounds(b: Bounds, from: CRS, to: CRS): Bounds {\n const t = createTransformer(from, to);\n const xs: number[] = [];\n const ys: number[] = [];\n for (const [x, y] of [\n [b[0], b[1]],\n [b[2], b[1]],\n [b[0], b[3]],\n [b[2], b[3]],\n ]) {\n const [nx, ny] = t.forward(x, y);\n xs.push(nx);\n ys.push(ny);\n }\n return [Math.min(...xs), Math.min(...ys), Math.max(...xs), Math.max(...ys)];\n}\n"]}
@@ -0,0 +1,56 @@
1
+ // src/lod/lodController.ts
2
+ function resolveLod(zoom, levels) {
3
+ let best = null;
4
+ for (const lv of levels) {
5
+ const max = lv.maxZoom ?? Infinity;
6
+ if (zoom >= lv.minZoom && zoom <= max) {
7
+ if (!best || lv.minZoom > best.minZoom) best = lv;
8
+ }
9
+ }
10
+ return best;
11
+ }
12
+ function suggestDensity(zoom, basePerTile = 200, tileCountFactor = 1) {
13
+ const z = Math.max(0, zoom);
14
+ return Math.round(basePerTile * Math.pow(2, Math.max(0, z - 8)) * tileCountFactor);
15
+ }
16
+ var LodController = class {
17
+ constructor(map, levels, onChange) {
18
+ this.current = null;
19
+ this.map = map;
20
+ this.levels = levels;
21
+ this.onChange = onChange;
22
+ this.handler = () => this.evaluate(false);
23
+ map.on("zoom", this.handler);
24
+ map.on("move", this.handler);
25
+ }
26
+ /** 立即评估当前 zoom 并(按需)触发回调 */
27
+ evaluate(force = false) {
28
+ const zoom = this.map.getZoom();
29
+ const next = resolveLod(zoom, this.levels);
30
+ if (!next) return this.current;
31
+ const changed = force || next.id !== this.current?.id;
32
+ if (changed || force) {
33
+ this.current = next;
34
+ this.onChange({ level: next, changed, zoom });
35
+ }
36
+ return next;
37
+ }
38
+ /** 当前等级 */
39
+ getLevel() {
40
+ return this.current;
41
+ }
42
+ /** 更新分级配置(如权限/数据就绪后) */
43
+ setLevels(levels) {
44
+ this.levels = levels;
45
+ this.evaluate(false);
46
+ }
47
+ /** 卸载监听 */
48
+ remove() {
49
+ this.map.off("zoom", this.handler);
50
+ this.map.off("move", this.handler);
51
+ }
52
+ };
53
+
54
+ export { LodController, resolveLod, suggestDensity };
55
+ //# sourceMappingURL=chunk-Y3JNDOHU.js.map
56
+ //# sourceMappingURL=chunk-Y3JNDOHU.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/lod/lodController.ts"],"names":[],"mappings":";AAsCO,SAAS,UAAA,CAAc,MAAc,MAAA,EAA2C;AACrF,EAAA,IAAI,IAAA,GAA2B,IAAA;AAC/B,EAAA,KAAA,MAAW,MAAM,MAAA,EAAQ;AACvB,IAAA,MAAM,GAAA,GAAM,GAAG,OAAA,IAAW,QAAA;AAC1B,IAAA,IAAI,IAAA,IAAQ,EAAA,CAAG,OAAA,IAAW,IAAA,IAAQ,GAAA,EAAK;AACrC,MAAA,IAAI,CAAC,IAAA,IAAQ,EAAA,CAAG,OAAA,GAAU,IAAA,CAAK,SAAS,IAAA,GAAO,EAAA;AAAA,IACjD;AAAA,EACF;AACA,EAAA,OAAO,IAAA;AACT;AAMO,SAAS,cAAA,CAAe,IAAA,EAAc,WAAA,GAAc,GAAA,EAAK,kBAAkB,CAAA,EAAW;AAC3F,EAAA,MAAM,CAAA,GAAI,IAAA,CAAK,GAAA,CAAI,CAAA,EAAG,IAAI,CAAA;AAC1B,EAAA,OAAO,IAAA,CAAK,KAAA,CAAM,WAAA,GAAc,IAAA,CAAK,GAAA,CAAI,CAAA,EAAG,IAAA,CAAK,GAAA,CAAI,CAAA,EAAG,CAAA,GAAI,CAAC,CAAC,IAAI,eAAe,CAAA;AACnF;AAKO,IAAM,gBAAN,MAAiC;AAAA,EAMtC,WAAA,CACE,GAAA,EACA,MAAA,EACA,QAAA,EACA;AARF,IAAA,IAAA,CAAQ,OAAA,GAA8B,IAAA;AASpC,IAAA,IAAA,CAAK,GAAA,GAAM,GAAA;AACX,IAAA,IAAA,CAAK,MAAA,GAAS,MAAA;AACd,IAAA,IAAA,CAAK,QAAA,GAAW,QAAA;AAChB,IAAA,IAAA,CAAK,OAAA,GAAU,MAAM,IAAA,CAAK,QAAA,CAAS,KAAK,CAAA;AACxC,IAAA,GAAA,CAAI,EAAA,CAAG,MAAA,EAAQ,IAAA,CAAK,OAAO,CAAA;AAC3B,IAAA,GAAA,CAAI,EAAA,CAAG,MAAA,EAAQ,IAAA,CAAK,OAAO,CAAA;AAAA,EAC7B;AAAA;AAAA,EAKA,QAAA,CAAS,QAAQ,KAAA,EAA2B;AAC1C,IAAA,MAAM,IAAA,GAAO,IAAA,CAAK,GAAA,CAAI,OAAA,EAAQ;AAC9B,IAAA,MAAM,IAAA,GAAO,UAAA,CAAW,IAAA,EAAM,IAAA,CAAK,MAAM,CAAA;AACzC,IAAA,IAAI,CAAC,IAAA,EAAM,OAAO,IAAA,CAAK,OAAA;AACvB,IAAA,MAAM,OAAA,GAAU,KAAA,IAAS,IAAA,CAAK,EAAA,KAAO,KAAK,OAAA,EAAS,EAAA;AACnD,IAAA,IAAI,WAAW,KAAA,EAAO;AACpB,MAAA,IAAA,CAAK,OAAA,GAAU,IAAA;AACf,MAAA,IAAA,CAAK,SAAS,EAAE,KAAA,EAAO,IAAA,EAAM,OAAA,EAAS,MAAM,CAAA;AAAA,IAC9C;AACA,IAAA,OAAO,IAAA;AAAA,EACT;AAAA;AAAA,EAGA,QAAA,GAA+B;AAC7B,IAAA,OAAO,IAAA,CAAK,OAAA;AAAA,EACd;AAAA;AAAA,EAGA,UAAU,MAAA,EAA6B;AACrC,IAAA,IAAA,CAAK,MAAA,GAAS,MAAA;AACd,IAAA,IAAA,CAAK,SAAS,KAAK,CAAA;AAAA,EACrB;AAAA;AAAA,EAGA,MAAA,GAAe;AACb,IAAA,IAAA,CAAK,GAAA,CAAI,GAAA,CAAI,MAAA,EAAQ,IAAA,CAAK,OAAO,CAAA;AACjC,IAAA,IAAA,CAAK,GAAA,CAAI,GAAA,CAAI,MAAA,EAAQ,IAAA,CAAK,OAAO,CAAA;AAAA,EACnC;AACF","file":"chunk-Y3JNDOHU.js","sourcesContent":["/**\n * LOD 控制器(T7 / F-1.7,纯逻辑)。\n *\n * 按地图 zoom 自动切换数据密度(Level of Detail):\n * - 低 zoom(全国/省)显示聚合/简化数据,避免要素过密;\n * - 高 zoom(市/区)显示全量/精细数据。\n *\n * 设计:本模块**不依赖 maplibre / WebGL / DOM**,纯函数可在 Node 单测。\n * 控制器 `LodController` 监听 zoom 变化并回调当前应激活的 LOD 等级,\n * 调用方据此 `setData` / 切换 source(具体渲染动作由调用方决定)。\n */\n\nexport interface LodLevel<T = unknown> {\n /** 该等级名称(如 'country' | 'province' | 'city' | 'detail') */\n id: string;\n /** 进入该等级的最小 zoom(含) */\n minZoom: number;\n /** 进入该等级的最大 zoom(含);省略表示 +∞ */\n maxZoom?: number;\n /** 该等级承载的数据/配置(任意类型,调用方解释) */\n payload?: T;\n}\n\nexport interface LodChangeEvent<T> {\n /** 当前激活等级 */\n level: LodLevel<T>;\n /** 是否相较上一次发生了等级切换 */\n changed: boolean;\n /** 当前 zoom */\n zoom: number;\n}\n\n/**\n * 根据 zoom 解析应激活的 LOD 等级。\n * 规则:取满足 minZoom<=zoom<=(maxZoom??∞) 的等级;\n * 若存在多个命中(区间重叠),取 minZoom 最大者(更精细优先)。\n * 无命中返回 null(调用方可保留上一等级)。\n */\nexport function resolveLod<T>(zoom: number, levels: LodLevel<T>[]): LodLevel<T> | null {\n let best: LodLevel<T> | null = null;\n for (const lv of levels) {\n const max = lv.maxZoom ?? Infinity;\n if (zoom >= lv.minZoom && zoom <= max) {\n if (!best || lv.minZoom > best.minZoom) best = lv;\n }\n }\n return best;\n}\n\n/**\n * 给定可视范围的\"要素密度\"建议(用于调试/UI 提示)。\n * 返回每屏建议最大要素数(随 zoom 平方增长,模拟瓦片面积)。\n */\nexport function suggestDensity(zoom: number, basePerTile = 200, tileCountFactor = 1): number {\n const z = Math.max(0, zoom);\n return Math.round(basePerTile * Math.pow(2, Math.max(0, z - 8)) * tileCountFactor);\n}\n\n/**\n * LOD 控制器(薄状态机)。监听 zoom,触发 onLod 回调。\n */\nexport class LodController<T = unknown> {\n private levels: LodLevel<T>[];\n private current: LodLevel<T> | null = null;\n private onChange: (e: LodChangeEvent<T>) => void;\n private map: { getZoom: () => number; on: (e: string, h: (ev: unknown) => void) => void; off: (e: string, h: (ev: unknown) => void) => void };\n\n constructor(\n map: LodController['map'],\n levels: LodLevel<T>[],\n onChange: (e: LodChangeEvent<T>) => void\n ) {\n this.map = map;\n this.levels = levels;\n this.onChange = onChange;\n this.handler = () => this.evaluate(false);\n map.on('zoom', this.handler);\n map.on('move', this.handler);\n }\n\n private handler: (ev: unknown) => void;\n\n /** 立即评估当前 zoom 并(按需)触发回调 */\n evaluate(force = false): LodLevel<T> | null {\n const zoom = this.map.getZoom();\n const next = resolveLod(zoom, this.levels);\n if (!next) return this.current;\n const changed = force || next.id !== this.current?.id;\n if (changed || force) {\n this.current = next;\n this.onChange({ level: next, changed, zoom });\n }\n return next;\n }\n\n /** 当前等级 */\n getLevel(): LodLevel<T> | null {\n return this.current;\n }\n\n /** 更新分级配置(如权限/数据就绪后) */\n setLevels(levels: LodLevel<T>[]): void {\n this.levels = levels;\n this.evaluate(false);\n }\n\n /** 卸载监听 */\n remove(): void {\n this.map.off('zoom', this.handler);\n this.map.off('move', this.handler);\n }\n}\n"]}
@@ -0,0 +1,227 @@
1
+ 'use strict';
2
+
3
+ var theme = require('@caoguo/theme');
4
+
5
+ // src/controls/ScaleControl.ts
6
+ var EARTH_RADIUS = 6378137;
7
+ function computeScaleBar(latitude, zoom, opts = {}) {
8
+ const dpiScale = opts.dpiScale ?? 1;
9
+ const maxWidth = opts.maxWidth ?? 100;
10
+ const tileSize = opts.tileSize ?? 512;
11
+ const latRad = latitude * Math.PI / 180;
12
+ const metersPerPixel = Math.cos(latRad) * 2 * Math.PI * EARTH_RADIUS / (tileSize * Math.pow(2, zoom) * dpiScale);
13
+ let meters = metersPerPixel * maxWidth;
14
+ const pow = Math.pow(10, Math.floor(Math.log10(meters)));
15
+ const base = meters / pow;
16
+ const step = base >= 5 ? 5 : base >= 2 ? 2 : 1;
17
+ meters = step * pow;
18
+ const pixels = meters / metersPerPixel;
19
+ const label = meters >= 1e3 ? `${(meters / 1e3).toLocaleString()} km` : `${Math.round(meters)} m`;
20
+ return { meters, pixels, label };
21
+ }
22
+ var ScaleControl = class {
23
+ constructor(map, options = {}) {
24
+ this.onMove = (ev) => this.handleMove(ev);
25
+ this.onZoom = () => this.update();
26
+ this.map = map;
27
+ this.opts = {
28
+ showCoordinate: options.showCoordinate ?? true,
29
+ maxWidth: options.maxWidth ?? 100
30
+ };
31
+ this.el = options.container ?? document.createElement("div");
32
+ this.el.className = "caoguo-scale-control";
33
+ this.el.style.cssText = "position:absolute;left:10px;bottom:24px;padding:4px 8px;background:rgba(10,15,30,.7);color:#cfe;border-radius:4px;font:12px/1.4 system-ui,sans-serif;pointer-events:none;z-index:2;";
34
+ this.barEl = document.createElement("div");
35
+ this.barEl.style.cssText = "height:6px;border:1px solid #cfe;border-top:none;margin-bottom:2px;";
36
+ this.labelEl = document.createElement("div");
37
+ if (this.opts.showCoordinate) {
38
+ this.coordEl = document.createElement("div");
39
+ this.coordEl.style.cssText = "opacity:.85;margin-top:2px;";
40
+ this.el.append(this.barEl, this.labelEl, this.coordEl);
41
+ } else {
42
+ this.el.append(this.barEl, this.labelEl);
43
+ }
44
+ map.on("zoom", this.onZoom);
45
+ map.on("move", this.onZoom);
46
+ const canvas = map.getCanvas?.();
47
+ canvas?.addEventListener("mousemove", this.onMove);
48
+ }
49
+ /** 把控件挂到地图容器(无预设容器时调用) */
50
+ addTo(container) {
51
+ if (!this.el.parentElement) container.appendChild(this.el);
52
+ this.update();
53
+ return this;
54
+ }
55
+ /** 计算并刷新显示 */
56
+ update() {
57
+ const center = this.map.getCenter();
58
+ const bar = computeScaleBar(center.lat, this.map.getZoom(), { maxWidth: this.opts.maxWidth });
59
+ this.barEl.style.width = `${Math.round(bar.pixels)}px`;
60
+ this.labelEl.textContent = bar.label;
61
+ }
62
+ handleMove(ev) {
63
+ if (!this.coordEl) return;
64
+ const e = ev;
65
+ const ll = e.lngLat;
66
+ if (!ll) return;
67
+ this.coordEl.textContent = `${ll.lng.toFixed(5)}, ${ll.lat.toFixed(5)}`;
68
+ }
69
+ /** 移除控件与事件监听 */
70
+ remove() {
71
+ this.map.off("zoom", this.onZoom);
72
+ this.map.off("move", this.onZoom);
73
+ const canvas = this.map.getCanvas?.();
74
+ canvas?.removeEventListener("mousemove", this.onMove);
75
+ this.el.remove();
76
+ }
77
+ };
78
+ function caoguoStyle(theme$1 = "caoguo-dark") {
79
+ return theme.buildStyle(theme$1);
80
+ }
81
+
82
+ // src/controls/ThemeSwitcher.ts
83
+ function oppositeTheme(theme) {
84
+ return theme === "caoguo-dark" ? "caoguo-light" : "caoguo-dark";
85
+ }
86
+ function themeFromStyle(style, fallback = "caoguo-dark") {
87
+ if (!style) return fallback;
88
+ const name = typeof style === "string" ? style : style.name;
89
+ if (name === "caoguo-dark" || name === "caoguo-light") return name;
90
+ return fallback;
91
+ }
92
+ var ThemeSwitcher = class {
93
+ constructor(map, options = {}) {
94
+ this.map = map;
95
+ this.current = options.initial ?? themeFromStyle(map.getStyle?.(), "caoguo-dark");
96
+ this.el = options.container ?? document.createElement("div");
97
+ this.el.className = "caoguo-theme-switcher";
98
+ this.el.style.cssText = "position:absolute;right:10px;top:10px;z-index:3;";
99
+ this.btn = document.createElement("button");
100
+ this.btn.type = "button";
101
+ this.applyLabel();
102
+ this.btn.style.cssText = "cursor:pointer;padding:4px 10px;border:1px solid #2a3550;border-radius:6px;background:rgba(10,15,30,.7);color:#cfe;font:12px system-ui,sans-serif;";
103
+ this.btn.addEventListener("click", () => this.toggle());
104
+ this.el.append(this.btn);
105
+ }
106
+ applyLabel() {
107
+ this.btn.textContent = this.current === "caoguo-dark" ? "\u{1F319} \u6697\u8272" : "\u2600 \u4EAE\u8272";
108
+ }
109
+ /** 当前主题 */
110
+ getTheme() {
111
+ return this.current;
112
+ }
113
+ /** 切换到指定主题(保留视图,diff 模式避免闪烁) */
114
+ setTheme(theme) {
115
+ if (theme === this.current) return;
116
+ this.current = theme;
117
+ this.applyLabel();
118
+ this.map.setStyle(caoguoStyle(theme), { diff: true });
119
+ }
120
+ /** 在明暗之间切换 */
121
+ toggle() {
122
+ this.setTheme(oppositeTheme(this.current));
123
+ }
124
+ /** 挂到容器 */
125
+ addTo(container) {
126
+ if (!this.el.parentElement) container.appendChild(this.el);
127
+ return this;
128
+ }
129
+ /** 移除 */
130
+ remove() {
131
+ this.btn.removeEventListener("click", () => this.toggle());
132
+ this.el.remove();
133
+ }
134
+ };
135
+
136
+ // src/controls/LegendControl.ts
137
+ function escapeHtml(s) {
138
+ return s.replace(
139
+ /[&<>"']/g,
140
+ (c) => c === "&" ? "&amp;" : c === "<" ? "&lt;" : c === ">" ? "&gt;" : c === '"' ? "&quot;" : "&#39;"
141
+ );
142
+ }
143
+ function renderLegendHtml(items, title) {
144
+ const titleHtml = title ? `<div class="cg-legend-title">${escapeHtml(title)}</div>` : "";
145
+ const itemsHtml = items.map((it) => {
146
+ const swatch = it.shape === "line" ? `<span class="cg-legend-swatch cg-legend-swatch--line" style="background:${it.color}"></span>` : `<span class="cg-legend-swatch" style="background:${it.color}"></span>`;
147
+ return `<div class="cg-legend-item">${swatch}<span>${escapeHtml(it.label)}</span></div>`;
148
+ }).join("");
149
+ return `<div class="cg-legend-body">${titleHtml}${itemsHtml}</div>`;
150
+ }
151
+ var LegendControl = class {
152
+ constructor(options) {
153
+ this.opts = { title: options.title ?? "", items: options.items };
154
+ this.el = options.container ?? document.createElement("div");
155
+ this.el.className = "caoguo-legend-control";
156
+ this.el.style.cssText = "position:absolute;right:10px;top:10px;padding:8px 10px;background:rgba(10,15,30,.72);color:#cfe;border-radius:6px;font:12px/1.5 system-ui,sans-serif;z-index:2;max-width:220px;";
157
+ this.bodyEl = document.createElement("div");
158
+ this.el.appendChild(this.bodyEl);
159
+ this.render();
160
+ }
161
+ render() {
162
+ this.bodyEl.innerHTML = renderLegendHtml(this.opts.items, this.opts.title || void 0);
163
+ }
164
+ /** 运行时更新图例项(随图层切换重新渲染) */
165
+ setItems(items, title) {
166
+ if (title !== void 0) this.opts.title = title;
167
+ this.opts.items = items;
168
+ this.render();
169
+ }
170
+ /** 挂载到容器 */
171
+ addTo(container) {
172
+ if (!this.el.parentElement) container.appendChild(this.el);
173
+ return this;
174
+ }
175
+ remove() {
176
+ this.el.remove();
177
+ }
178
+ };
179
+
180
+ // src/controls/ExportControl.ts
181
+ function triggerDownload(dataUrl, filename) {
182
+ const a = document.createElement("a");
183
+ a.href = dataUrl;
184
+ a.download = filename.endsWith(".png") ? filename : `${filename}.png`;
185
+ document.body.appendChild(a);
186
+ a.click();
187
+ a.remove();
188
+ }
189
+ var ExportControl = class {
190
+ constructor(map, options = {}) {
191
+ this.onClick = () => this.export();
192
+ this.map = map;
193
+ this.filename = options.filename ?? "caoguo-map";
194
+ this.el = options.container ?? document.createElement("button");
195
+ this.el.className = "caoguo-export-control";
196
+ this.el.textContent = options.buttonText ?? "\u5BFC\u51FA PNG";
197
+ this.el.style.cssText = "position:absolute;right:10px;top:10px;padding:5px 10px;background:rgba(10,15,30,.72);color:#cfe;border:1px solid #2a3550;border-radius:4px;font:12px system-ui,sans-serif;cursor:pointer;z-index:3;";
198
+ this.el.addEventListener("click", this.onClick);
199
+ }
200
+ /** 导出当前地图视图为 PNG 并下载 */
201
+ export() {
202
+ const canvas = this.map.getCanvas();
203
+ const url = canvas.toDataURL("image/png");
204
+ triggerDownload(url, this.filename);
205
+ }
206
+ /** 挂载到容器 */
207
+ addTo(container) {
208
+ if (!this.el.parentElement) container.appendChild(this.el);
209
+ return this;
210
+ }
211
+ remove() {
212
+ this.el.removeEventListener("click", this.onClick);
213
+ this.el.remove();
214
+ }
215
+ };
216
+
217
+ exports.ExportControl = ExportControl;
218
+ exports.LegendControl = LegendControl;
219
+ exports.ScaleControl = ScaleControl;
220
+ exports.ThemeSwitcher = ThemeSwitcher;
221
+ exports.computeScaleBar = computeScaleBar;
222
+ exports.oppositeTheme = oppositeTheme;
223
+ exports.renderLegendHtml = renderLegendHtml;
224
+ exports.themeFromStyle = themeFromStyle;
225
+ exports.triggerDownload = triggerDownload;
226
+ //# sourceMappingURL=index.cjs.map
227
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/controls/ScaleControl.ts","../../src/styles.ts","../../src/controls/ThemeSwitcher.ts","../../src/controls/LegendControl.ts","../../src/controls/ExportControl.ts"],"names":["theme","buildStyle"],"mappings":"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","file":"index.cjs","sourcesContent":["/**\n * 比例尺 + 坐标显示控件(T8 / F-1.8)。\n *\n * - 比例尺:基于 Web Mercator 在给定纬度下的地面分辨率(m/像素),\n * 自动选择「米 / 公里」并吸附到 1/2/5 的整数档(标准地图比例尺行为)。\n * - 坐标:鼠标在图上移动时,实时显示当前光标处的经纬坐标(WGS84 渲染基准)。\n *\n * 设计为「纯函数 core + 薄 DOM 绑定」:比例计算 `computeScaleBar` 不依赖浏览器,\n * 可独立单测;`ScaleControl` 类负责把结果渲染进容器并监听地图事件。\n */\n\nexport type LengthUnit = 'metric';\n\nexport interface ScaleBar {\n /** 比例尺条代表的真实距离(米) */\n meters: number;\n /** 比例尺条在屏幕上的像素宽度 */\n pixels: number;\n /** 展示文案,如 \"500 m\" / \"2 km\" */\n label: string;\n}\n\nconst EARTH_RADIUS = 6378137; // WGS84 赤道半径(米)\n\n/**\n * 计算比例尺。\n * @param latitude 当前视图中心纬度(度)\n * @param zoom MapLibre zoom(Web Mercator)\n * @param dpiScale 设备像素比(默认 1;Retina 设为 window.devicePixelRatio)\n * @param maxWidth 比例尺条最大像素宽度(默认 100)\n * @param tileSize 瓦片像素尺寸(默认 512,MapLibre v4 默认)\n */\nexport function computeScaleBar(\n latitude: number,\n zoom: number,\n opts: { dpiScale?: number; maxWidth?: number; tileSize?: number } = {}\n): ScaleBar {\n const dpiScale = opts.dpiScale ?? 1;\n const maxWidth = opts.maxWidth ?? 100;\n const tileSize = opts.tileSize ?? 512;\n\n // 每像素的地面距离(米/像素),在给定纬度下\n const latRad = (latitude * Math.PI) / 180;\n const metersPerPixel =\n (Math.cos(latRad) * 2 * Math.PI * EARTH_RADIUS) /\n (tileSize * Math.pow(2, zoom) * dpiScale);\n\n // 最大像素宽度对应的真实距离\n let meters = metersPerPixel * maxWidth;\n\n // 吸附到 1/2/5 档\n const pow = Math.pow(10, Math.floor(Math.log10(meters)));\n const base = meters / pow; // 1..10\n const step = base >= 5 ? 5 : base >= 2 ? 2 : 1;\n meters = step * pow;\n\n const pixels = meters / metersPerPixel;\n const label = meters >= 1000 ? `${(meters / 1000).toLocaleString()} km` : `${Math.round(meters)} m`;\n\n return { meters, pixels, label };\n}\n\nexport interface ScaleControlOptions {\n /** 挂载容器(可选;不传则在地图容器内自动创建) */\n container?: HTMLElement;\n /** 是否显示实时坐标(默认 true) */\n showCoordinate?: boolean;\n /** 最大比例尺像素宽度 */\n maxWidth?: number;\n}\n\n/**\n * 比例尺控件。需传入 maplibre Map 实例(直接用 maplibre-gl 的 Map)。\n */\nexport class ScaleControl {\n private el: HTMLElement;\n private barEl: HTMLElement;\n private labelEl: HTMLElement;\n private coordEl?: HTMLElement;\n private map: { getZoom: () => number; getCenter: () => { lat: number }; on: (e: string, h: (ev: unknown) => void) => void; off: (e: string, h: (ev: unknown) => void) => void; getCanvas?: () => { addEventListener: (t: string, h: (ev: unknown) => void) => void; removeEventListener: (t: string, h: (ev: unknown) => void) => void } };\n private opts: Required<Omit<ScaleControlOptions, 'container'>>;\n private onMove = (ev: unknown) => this.handleMove(ev);\n private onZoom = () => this.update();\n\n constructor(\n map: ScaleControl['map'],\n options: ScaleControlOptions = {}\n ) {\n this.map = map;\n this.opts = {\n showCoordinate: options.showCoordinate ?? true,\n maxWidth: options.maxWidth ?? 100,\n };\n this.el = options.container ?? document.createElement('div');\n this.el.className = 'caoguo-scale-control';\n this.el.style.cssText =\n 'position:absolute;left:10px;bottom:24px;padding:4px 8px;background:rgba(10,15,30,.7);color:#cfe;border-radius:4px;font:12px/1.4 system-ui,sans-serif;pointer-events:none;z-index:2;';\n\n this.barEl = document.createElement('div');\n this.barEl.style.cssText =\n 'height:6px;border:1px solid #cfe;border-top:none;margin-bottom:2px;';\n this.labelEl = document.createElement('div');\n if (this.opts.showCoordinate) {\n this.coordEl = document.createElement('div');\n this.coordEl.style.cssText = 'opacity:.85;margin-top:2px;';\n this.el.append(this.barEl, this.labelEl, this.coordEl);\n } else {\n this.el.append(this.barEl, this.labelEl);\n }\n\n map.on('zoom', this.onZoom);\n map.on('move', this.onZoom);\n const canvas = (map as any).getCanvas?.();\n canvas?.addEventListener('mousemove', this.onMove as never);\n }\n\n /** 把控件挂到地图容器(无预设容器时调用) */\n addTo(container: HTMLElement): this {\n if (!this.el.parentElement) container.appendChild(this.el);\n this.update();\n return this;\n }\n\n /** 计算并刷新显示 */\n update(): void {\n const center = this.map.getCenter();\n const bar = computeScaleBar(center.lat, this.map.getZoom(), { maxWidth: this.opts.maxWidth });\n this.barEl.style.width = `${Math.round(bar.pixels)}px`;\n this.labelEl.textContent = bar.label;\n }\n\n private handleMove(ev: unknown): void {\n if (!this.coordEl) return;\n const e = ev as { lngLat?: { lng: number; lat: number } };\n const ll = e.lngLat;\n if (!ll) return;\n this.coordEl.textContent = `${ll.lng.toFixed(5)}, ${ll.lat.toFixed(5)}`;\n }\n\n /** 移除控件与事件监听 */\n remove(): void {\n this.map.off('zoom', this.onZoom);\n this.map.off('move', this.onZoom);\n const canvas = (this.map as any).getCanvas?.();\n canvas?.removeEventListener('mousemove', this.onMove as never);\n this.el.remove();\n }\n}\n","/**\n * 草果地图内置样式。\n *\n * - `caoguoStyle()`:官方矢量主题(caoguo-dark / caoguo-light),\n * 由 @caoguo/theme 生成,底层使用 MapLibre 免费 demo 矢量瓦片,无需 token。\n * - `geoqRasterStyle()` / `osmRasterStyle()`:国内可达的栅格兜底底图\n * (智图 GeoQ 公共服务,免 key,适配国内网络)。`osmRasterStyle` 保留为别名。\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/**\n * 国内可达的栅格兜底底图(智图 GeoQ 公共服务,免 key)。\n * 使用 ChinaOnlineStreetPurplishBlue(蓝紫街道,贴合暗色演示主题)。\n * 替代原 OSM 兜底:OSM 在国内网络普遍不可达,导致部署在国内访问的\n * 演示中心底图空白。GeoQ 为 Esri 中国公共服务,长期稳定免 key。\n */\nexport function geoqRasterStyle(): StyleSpecification {\n return {\n version: 8,\n glyphs: 'https://demotiles.maplibre.org/font/{fontstack}/{range}.pbf',\n sources: {\n 'geoq-raster': {\n type: 'raster',\n tiles: [\n 'https://map.geoq.cn/arcgis/rest/services/ChinaOnlineStreetPurplishBlue/MapServer/tile/{z}/{y}/{x}',\n ],\n tileSize: 256,\n attribution: '© GeoQ 智图',\n },\n },\n layers: [\n {\n id: 'background',\n type: 'background',\n paint: { 'background-color': '#0a0f1e' },\n },\n {\n id: 'geoq-raster',\n type: 'raster',\n source: 'geoq-raster',\n paint: {\n 'raster-opacity': 0.9,\n 'raster-saturation': -0.2,\n 'raster-contrast': 0.05,\n },\n },\n ],\n };\n}\n\n/** 兼容别名:国内可达栅格兜底底图。 */\nexport const osmRasterStyle = geoqRasterStyle;\n\n/**\n * 纯本地底图(零外部依赖,完全离线)。\n * 暗色背景 + 全球经纬网,无需任何在线瓦片 / 字体服务。\n * 用于:国内网络无法访问 OSM / GeoQ 等境外或受限瓦片源时,\n * 保证演示中心地图永远可渲染(业务图层为自有 GeoJSON,叠加其上即可见)。\n */\nfunction buildGraticule(step = 10): GeoJSON.FeatureCollection {\n const features: GeoJSON.Feature[] = [];\n for (let lon = -180; lon <= 180; lon += step) {\n features.push({\n type: 'Feature',\n properties: { kind: 'meridian' },\n geometry: { type: 'LineString', coordinates: [[lon, -85], [lon, 85]] },\n });\n }\n for (let lat = -80; lat <= 80; lat += step) {\n features.push({\n type: 'Feature',\n properties: { kind: 'parallel' },\n geometry: { type: 'LineString', coordinates: [[-180, lat], [180, lat]] },\n });\n }\n return { type: 'FeatureCollection', features };\n}\n\nexport function localBasemapStyle(): StyleSpecification {\n return {\n version: 8,\n sources: {\n graticule: { type: 'geojson', data: buildGraticule() },\n },\n layers: [\n { id: 'bg', type: 'background', paint: { 'background-color': '#0a0f1e' } },\n {\n id: 'graticule',\n type: 'line',\n source: 'graticule',\n paint: { 'line-color': '#1e293b', 'line-width': 0.6, 'line-opacity': 0.6 },\n },\n ],\n };\n}\n\n\n","/**\n * 主题切换控件(T8 / F-1.9)。\n *\n * 在暗色(caoguo-dark)与亮色(caoguo-light)官方矢量主题间切换,\n * 调用 `map.setStyle` 并保留当前视图(center/zoom)与现有 GeoJSON 源/图层。\n *\n * 设计为「纯函数 core + 薄 DOM 绑定」:\n * - `toggleTheme` / `themeFromStyle` 为纯逻辑,可独立单测;\n * - `ThemeSwitcher` 类负责渲染按钮并绑定点击事件。\n */\n\nimport { caoguoStyle } from '../styles';\nimport type { ThemeName } from '@caoguo/theme';\n\nexport interface ThemeSwitcherOptions {\n /** 挂载容器(可选) */\n container?: HTMLElement;\n /** 初始主题 */\n initial?: ThemeName;\n}\n\nexport function oppositeTheme(theme: ThemeName): ThemeName {\n return theme === 'caoguo-dark' ? 'caoguo-light' : 'caoguo-dark';\n}\n\n/**\n * 从当前 style 对象推断主题名(依据其 JSON 标识)。\n * 非草果主题时回退到传入的 fallback。\n */\nexport function themeFromStyle(\n style: { name?: string } | string | null | undefined,\n fallback: ThemeName = 'caoguo-dark'\n): ThemeName {\n if (!style) return fallback;\n const name = typeof style === 'string' ? style : style.name;\n if (name === 'caoguo-dark' || name === 'caoguo-light') return name;\n return fallback;\n}\n\nexport class ThemeSwitcher {\n private el: HTMLElement;\n private btn: HTMLButtonElement;\n private current: ThemeName;\n private map: {\n getStyle: () => { name?: string } | undefined;\n setStyle: (s: unknown, opts?: { diff?: boolean }) => void;\n };\n\n constructor(map: ThemeSwitcher['map'], options: ThemeSwitcherOptions = {}) {\n this.map = map;\n this.current = options.initial ?? themeFromStyle(map.getStyle?.(), 'caoguo-dark');\n\n this.el = options.container ?? document.createElement('div');\n this.el.className = 'caoguo-theme-switcher';\n this.el.style.cssText =\n 'position:absolute;right:10px;top:10px;z-index:3;';\n\n this.btn = document.createElement('button');\n this.btn.type = 'button';\n this.applyLabel();\n this.btn.style.cssText =\n 'cursor:pointer;padding:4px 10px;border:1px solid #2a3550;border-radius:6px;' +\n 'background:rgba(10,15,30,.7);color:#cfe;font:12px system-ui,sans-serif;';\n this.btn.addEventListener('click', () => this.toggle());\n this.el.append(this.btn);\n }\n\n private applyLabel(): void {\n this.btn.textContent = this.current === 'caoguo-dark' ? '🌙 暗色' : '☀ 亮色';\n }\n\n /** 当前主题 */\n getTheme(): ThemeName {\n return this.current;\n }\n\n /** 切换到指定主题(保留视图,diff 模式避免闪烁) */\n setTheme(theme: ThemeName): void {\n if (theme === this.current) return;\n this.current = theme;\n this.applyLabel();\n this.map.setStyle(caoguoStyle(theme), { diff: true });\n }\n\n /** 在明暗之间切换 */\n toggle(): void {\n this.setTheme(oppositeTheme(this.current));\n }\n\n /** 挂到容器 */\n addTo(container: HTMLElement): this {\n if (!this.el.parentElement) container.appendChild(this.el);\n return this;\n }\n\n /** 移除 */\n remove(): void {\n this.btn.removeEventListener('click', () => this.toggle());\n this.el.remove();\n }\n}\n","/**\n * 图例控件(通用)。\n *\n * 行业专题图(水深色阶 / 光缆利用率 / 闸站状态 / 故障告警等)渲染后,\n * 需要用户可读的图例说明。本控件接收数据驱动的图例项(label + color),\n * 以「色块 + 文案」形式渲染;支持运行时 setItems 随图层切换更新。\n *\n * 设计为「纯函数 renderLegendHtml + 薄 DOM 绑定」:renderLegendHtml 不依赖浏览器,可独立单测。\n */\n\nexport interface LegendItem {\n /** 文案,如「水深 > 3m」「利用率高」 */\n label: string;\n /** 色值(CSS color,可与渲染层 paint 配色保持一致) */\n color: string;\n /** 标记形状:color 色块(默认)/ line 线段 */\n shape?: 'color' | 'line';\n}\n\nexport interface LegendOptions {\n /** 图例标题(可选) */\n title?: string;\n /** 图例项 */\n items: LegendItem[];\n /** 挂载容器(可选;不传则自动创建) */\n container?: HTMLElement;\n}\n\nfunction escapeHtml(s: string): string {\n return s.replace(/[&<>\"']/g, (c) =>\n c === '&' ? '&amp;' : c === '<' ? '&lt;' : c === '>' ? '&gt;' : c === '\"' ? '&quot;' : '&#39;',\n );\n}\n\n/** 纯函数:根据图例项生成 HTML 字符串(可单测) */\nexport function renderLegendHtml(items: LegendItem[], title?: string): string {\n const titleHtml = title ? `<div class=\"cg-legend-title\">${escapeHtml(title)}</div>` : '';\n const itemsHtml = items\n .map((it) => {\n const swatch =\n it.shape === 'line'\n ? `<span class=\"cg-legend-swatch cg-legend-swatch--line\" style=\"background:${it.color}\"></span>`\n : `<span class=\"cg-legend-swatch\" style=\"background:${it.color}\"></span>`;\n return `<div class=\"cg-legend-item\">${swatch}<span>${escapeHtml(it.label)}</span></div>`;\n })\n .join('');\n return `<div class=\"cg-legend-body\">${titleHtml}${itemsHtml}</div>`;\n}\n\n/** 图例控件 */\nexport class LegendControl {\n private el: HTMLElement;\n private bodyEl: HTMLElement;\n private opts: Required<Pick<LegendOptions, 'title'>> & { items: LegendItem[] };\n\n constructor(options: LegendOptions) {\n this.opts = { title: options.title ?? '', items: options.items };\n this.el = options.container ?? document.createElement('div');\n this.el.className = 'caoguo-legend-control';\n this.el.style.cssText =\n 'position:absolute;right:10px;top:10px;padding:8px 10px;background:rgba(10,15,30,.72);color:#cfe;border-radius:6px;font:12px/1.5 system-ui,sans-serif;z-index:2;max-width:220px;';\n this.bodyEl = document.createElement('div');\n this.el.appendChild(this.bodyEl);\n this.render();\n }\n\n private render(): void {\n this.bodyEl.innerHTML = renderLegendHtml(this.opts.items, this.opts.title || undefined);\n }\n\n /** 运行时更新图例项(随图层切换重新渲染) */\n setItems(items: LegendItem[], title?: string): void {\n if (title !== undefined) this.opts.title = title;\n this.opts.items = items;\n this.render();\n }\n\n /** 挂载到容器 */\n addTo(container: HTMLElement): this {\n if (!this.el.parentElement) container.appendChild(this.el);\n return this;\n }\n\n remove(): void {\n this.el.remove();\n }\n}\n","/**\n * 地图导出控件(通用)。\n *\n * 行业专题图渲染后,常需导出当前视图为成果图(PNG)。\n * 基于 maplibre-gl 画布 `toDataURL('image/png')` 生成截图并触发浏览器下载。\n *\n * 设计为「纯函数 triggerDownload + 薄 DOM 绑定」:triggerDownload 不依赖地图,可独立单测。\n */\n\nexport interface ExportOptions {\n /** 下载文件名(不含扩展名) */\n filename?: string;\n /** 挂载容器(可选;不传则自动创建) */\n container?: HTMLElement;\n /** 按钮文案 */\n buttonText?: string;\n}\n\n/** 纯函数:触发浏览器下载(data URL) */\nexport function triggerDownload(dataUrl: string, filename: string): void {\n const a = document.createElement('a');\n a.href = dataUrl;\n a.download = filename.endsWith('.png') ? filename : `${filename}.png`;\n document.body.appendChild(a);\n a.click();\n a.remove();\n}\n\n/** 导出控件 */\nexport class ExportControl {\n private el: HTMLElement;\n private map: {\n getCanvas: () => { toDataURL: (type?: string) => string };\n };\n private filename: string;\n private onClick = () => this.export();\n\n constructor(map: ExportControl['map'], options: ExportOptions = {}) {\n this.map = map;\n this.filename = options.filename ?? 'caoguo-map';\n this.el = options.container ?? document.createElement('button');\n this.el.className = 'caoguo-export-control';\n this.el.textContent = options.buttonText ?? '导出 PNG';\n this.el.style.cssText =\n 'position:absolute;right:10px;top:10px;padding:5px 10px;background:rgba(10,15,30,.72);color:#cfe;border:1px solid #2a3550;border-radius:4px;font:12px system-ui,sans-serif;cursor:pointer;z-index:3;';\n this.el.addEventListener('click', this.onClick);\n }\n\n /** 导出当前地图视图为 PNG 并下载 */\n export(): void {\n const canvas = this.map.getCanvas();\n const url = canvas.toDataURL('image/png');\n triggerDownload(url, this.filename);\n }\n\n /** 挂载到容器 */\n addTo(container: HTMLElement): this {\n if (!this.el.parentElement) container.appendChild(this.el);\n return this;\n }\n\n remove(): void {\n this.el.removeEventListener('click', this.onClick);\n this.el.remove();\n }\n}\n"]}