@caoguo/maplibre 0.0.3 → 0.0.6

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/index.js CHANGED
@@ -1,7 +1,7 @@
1
- import { caoguoStyle, WUHAN_ZOOM, WUHAN_CENTER, osmRasterStyle } from './chunk-DNEF4DHG.js';
1
+ import { caoguoStyle, osmRasterStyle, WUHAN_ZOOM, WUHAN_CENTER } from './chunk-DNEF4DHG.js';
2
2
  export { WUHAN_CENTER, WUHAN_ZOOM, osmRasterStyle } from './chunk-DNEF4DHG.js';
3
- import { tiandituStyle, addTiandituBaseMap } from './chunk-EZOV5O2M.js';
4
- export { MissingTokenError, addTiandituBaseMap, buildTiandituSources, tiandituStyle, tiandituTileUrls } from './chunk-EZOV5O2M.js';
3
+ import { MissingTokenError, tiandituStyle, addTiandituBaseMap } from './chunk-QGVXYR2I.js';
4
+ export { MissingTokenError, addTiandituBaseMap, buildTiandituSources, tiandituStyle, tiandituTileUrls } from './chunk-QGVXYR2I.js';
5
5
  import { createDefaultStore, registerOfflineProtocol, offlineSourceTiles, packGeoJSONToStore } from './chunk-MNUQZUQA.js';
6
6
  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 } from './chunk-MNUQZUQA.js';
7
7
  import maplibregl from 'maplibre-gl';
@@ -258,15 +258,96 @@ var ThemeSwitcher = class {
258
258
  }
259
259
  };
260
260
 
261
+ // src/controls/LegendControl.ts
262
+ function escapeHtml(s) {
263
+ return s.replace(
264
+ /[&<>"']/g,
265
+ (c) => c === "&" ? "&amp;" : c === "<" ? "&lt;" : c === ">" ? "&gt;" : c === '"' ? "&quot;" : "&#39;"
266
+ );
267
+ }
268
+ function renderLegendHtml(items, title) {
269
+ const titleHtml = title ? `<div class="cg-legend-title">${escapeHtml(title)}</div>` : "";
270
+ const itemsHtml = items.map((it) => {
271
+ 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>`;
272
+ return `<div class="cg-legend-item">${swatch}<span>${escapeHtml(it.label)}</span></div>`;
273
+ }).join("");
274
+ return `<div class="cg-legend-body">${titleHtml}${itemsHtml}</div>`;
275
+ }
276
+ var LegendControl = class {
277
+ constructor(options) {
278
+ this.opts = { title: options.title ?? "", items: options.items };
279
+ this.el = options.container ?? document.createElement("div");
280
+ this.el.className = "caoguo-legend-control";
281
+ 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;";
282
+ this.bodyEl = document.createElement("div");
283
+ this.el.appendChild(this.bodyEl);
284
+ this.render();
285
+ }
286
+ render() {
287
+ this.bodyEl.innerHTML = renderLegendHtml(this.opts.items, this.opts.title || void 0);
288
+ }
289
+ /** 运行时更新图例项(随图层切换重新渲染) */
290
+ setItems(items, title) {
291
+ if (title !== void 0) this.opts.title = title;
292
+ this.opts.items = items;
293
+ this.render();
294
+ }
295
+ /** 挂载到容器 */
296
+ addTo(container) {
297
+ if (!this.el.parentElement) container.appendChild(this.el);
298
+ return this;
299
+ }
300
+ remove() {
301
+ this.el.remove();
302
+ }
303
+ };
304
+
305
+ // src/controls/ExportControl.ts
306
+ function triggerDownload(dataUrl, filename) {
307
+ const a = document.createElement("a");
308
+ a.href = dataUrl;
309
+ a.download = filename.endsWith(".png") ? filename : `${filename}.png`;
310
+ document.body.appendChild(a);
311
+ a.click();
312
+ a.remove();
313
+ }
314
+ var ExportControl = class {
315
+ constructor(map, options = {}) {
316
+ this.onClick = () => this.export();
317
+ this.map = map;
318
+ this.filename = options.filename ?? "caoguo-map";
319
+ this.el = options.container ?? document.createElement("button");
320
+ this.el.className = "caoguo-export-control";
321
+ this.el.textContent = options.buttonText ?? "\u5BFC\u51FA PNG";
322
+ 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;";
323
+ this.el.addEventListener("click", this.onClick);
324
+ }
325
+ /** 导出当前地图视图为 PNG 并下载 */
326
+ export() {
327
+ const canvas = this.map.getCanvas();
328
+ const url = canvas.toDataURL("image/png");
329
+ triggerDownload(url, this.filename);
330
+ }
331
+ /** 挂载到容器 */
332
+ addTo(container) {
333
+ if (!this.el.parentElement) container.appendChild(this.el);
334
+ return this;
335
+ }
336
+ remove() {
337
+ this.el.removeEventListener("click", this.onClick);
338
+ this.el.remove();
339
+ }
340
+ };
341
+
261
342
  // src/shaders/glowGeometry.ts
262
- function glowPasses(passes = 4, coreOpacity = 1) {
343
+ function glowPasses(passes = 4, baseWidth = 3) {
263
344
  if (passes < 1) passes = 1;
264
345
  const out = [];
265
346
  for (let i = 0; i < passes; i++) {
266
347
  const t = passes === 1 ? 1 : 1 - i / (passes - 1);
267
348
  out.push({
268
- widthScale: 1 + (1 - t) * (passes - 1) * 1.5,
269
- opacity: coreOpacity * (0.12 + 0.88 * t)
349
+ width: baseWidth * (1 + (1 - t) * (passes - 1) * 1.8),
350
+ opacity: 0.1 + 0.9 * t * t
270
351
  });
271
352
  }
272
353
  return out;
@@ -274,20 +355,58 @@ function glowPasses(passes = 4, coreOpacity = 1) {
274
355
  function projectSimple(lng, lat) {
275
356
  const x = lng / 180;
276
357
  const y = Math.log(Math.tan(Math.PI / 4 + lat * Math.PI / 360)) / Math.PI;
277
- return [x, Math.max(-1, Math.min(1, y))];
358
+ return [x, Math.max(-1, Math.min(1, y / Math.PI))];
278
359
  }
279
360
  function buildGlowGeometry(lines, opts = {}) {
280
- const passes = glowPasses(opts.passes ?? 4, opts.coreOpacity ?? 1);
281
- let vertexCount = 0;
282
- const projected = lines.map((l) => {
283
- const points = l.coordinates.map(([lng, lat]) => projectSimple(lng, lat));
284
- vertexCount += Math.max(0, points.length - 1) * 2;
285
- return { points, group: l.group };
286
- });
287
- return { lines: projected, passes, vertexCount };
361
+ const passes = glowPasses(opts.passes ?? 4, opts.baseWidth ?? 3);
362
+ const stride = 6;
363
+ const verts = [];
364
+ const passRanges = [];
365
+ const renderGroups = [];
366
+ const groups = [];
367
+ for (const line of lines) {
368
+ const g = line.group ?? "default";
369
+ if (!groups.includes(g)) groups.push(g);
370
+ }
371
+ for (let p = 0; p < passes.length; p++) {
372
+ passes[p];
373
+ const start = verts.length / stride;
374
+ for (const group of groups) {
375
+ const groupStart = verts.length / stride;
376
+ const groupIndex = groups.indexOf(group);
377
+ for (const line of lines) {
378
+ if ((line.group ?? "default") !== group) continue;
379
+ const pts = line.coordinates.map(([lng, lat]) => projectSimple(lng, lat));
380
+ if (pts.length < 2) continue;
381
+ for (let i = 0; i < pts.length - 1; i++) {
382
+ const a = pts[i];
383
+ const b = pts[i + 1];
384
+ const dx = b[0] - a[0];
385
+ const dy = b[1] - a[1];
386
+ const quad = [
387
+ [a[0], a[1], dx, dy, 1, groupIndex],
388
+ [a[0], a[1], dx, dy, -1, groupIndex],
389
+ [b[0], b[1], dx, dy, 1, groupIndex],
390
+ [b[0], b[1], dx, dy, -1, groupIndex]
391
+ ];
392
+ const idx = [0, 1, 2, 2, 1, 3];
393
+ for (const k of idx) {
394
+ const v = quad[k];
395
+ verts.push(v[0], v[1], v[2], v[3], v[4], v[5]);
396
+ }
397
+ }
398
+ }
399
+ const count2 = verts.length / stride - groupStart;
400
+ if (count2 > 0) renderGroups.push({ passIndex: p, group, start: groupStart, count: count2 });
401
+ }
402
+ const count = verts.length / stride - start;
403
+ passRanges.push({ start, count });
404
+ }
405
+ return { vertices: new Float32Array(verts), passRanges, passes, stride, groups, renderGroups };
288
406
  }
289
407
 
290
408
  // src/shaders/CustomLineLayer.ts
409
+ var FALLBACK_COLOR = [0.6, 0.7, 0.9];
291
410
  var DEFAULT_COLORS = {
292
411
  pipe: [0.2, 0.85, 1],
293
412
  // 青蓝:管线
@@ -298,17 +417,21 @@ var DEFAULT_COLORS = {
298
417
  };
299
418
  var VERT = `
300
419
  attribute vec2 aPos;
301
- attribute float aMiter;
420
+ attribute vec2 aDir;
421
+ attribute float aSide;
302
422
  uniform mat4 uMatrix;
303
423
  uniform float uWidth;
304
424
  uniform vec2 uResolution;
305
425
  void main() {
306
- // \u7B80\u5316\uFF1A\u6CBF\u6CD5\u7EBF\u65B9\u5411\u504F\u79FB width\uFF08\u5C4F\u5E55\u7A7A\u95F4\uFF09\uFF0C\u8FD9\u91CC\u4EC5\u6F14\u793A\u6838\u5FC3\u6295\u5F71
307
426
  vec4 clip = uMatrix * vec4(aPos, 0.0, 1.0);
308
- vec2 screen = clip.xy / clip.w;
309
- // \u6CE8\uFF1A\u7CBE\u786E\u7EBF\u5BBD\u9700\u6CD5\u7EBF\u8BA1\u7B97\uFF0C\u793A\u8303\u5C42\u7528\u70B9\u5BBD\u8FD1\u4F3C
310
- vec2 offset = vec2(aMiter * uWidth / uResolution.x * 2.0, 0.0);
311
- gl_Position = vec4(screen + offset * clip.w, clip.z, clip.w);
427
+ // \u6BB5\u65B9\u5411\u4E5F\u6295\u5F71\uFF0C\u6C42\u5C4F\u5E55\u7A7A\u95F4\u65B9\u5411
428
+ vec4 clipDir = uMatrix * vec4(aPos + aDir, 0.0, 1.0);
429
+ vec2 s0 = clip.xy / clip.w;
430
+ vec2 s1 = clipDir.xy / clipDir.w;
431
+ vec2 dir = normalize(s1 - s0 + vec2(1e-6));
432
+ vec2 normal = vec2(-dir.y, dir.x);
433
+ vec2 offset = normal * aSide * (uWidth / uResolution * 2.0);
434
+ gl_Position = vec4(s0 + offset * clip.w, clip.z, clip.w);
312
435
  }
313
436
  `;
314
437
  var FRAG = `
@@ -323,6 +446,9 @@ function compile(gl, type, src) {
323
446
  const sh = gl.createShader(type);
324
447
  gl.shaderSource(sh, src);
325
448
  gl.compileShader(sh);
449
+ if (!gl.getShaderParameter(sh, gl.COMPILE_STATUS)) {
450
+ console.error("[CustomLineLayer] shader \u7F16\u8BD1\u5931\u8D25:", gl.getShaderInfoLog(sh));
451
+ }
326
452
  return sh;
327
453
  }
328
454
  var CustomLineLayer = class {
@@ -333,40 +459,48 @@ var CustomLineLayer = class {
333
459
  this.lines = opts.lines;
334
460
  this.colors = { ...DEFAULT_COLORS, ...opts.colors };
335
461
  this.baseWidth = opts.baseWidth ?? 3;
336
- this.passes = opts.passes ?? 4;
337
- this.geometry = buildGlowGeometry(this.lines, { passes: this.passes });
462
+ this.geometry = buildGlowGeometry(this.lines, { passes: opts.passes ?? 4, baseWidth: this.baseWidth });
463
+ for (const g of this.geometry.groups) {
464
+ if (!(g in this.colors)) this.colors[g] = FALLBACK_COLOR;
465
+ }
338
466
  }
339
467
  onAdd(map, gl) {
340
468
  this.map = map;
469
+ this.gl = gl;
341
470
  const vs = compile(gl, gl.VERTEX_SHADER, VERT);
342
471
  const fs = compile(gl, gl.FRAGMENT_SHADER, FRAG);
343
472
  const prog = gl.createProgram();
344
473
  gl.attachShader(prog, vs);
345
474
  gl.attachShader(prog, fs);
346
475
  gl.linkProgram(prog);
347
- this.program = prog;
348
- const verts = [];
349
- for (const line of this.geometry.lines) {
350
- const pts = line.points;
351
- for (let i = 0; i < pts.length - 1; i++) {
352
- verts.push(pts[i][0], pts[i][1], 0, pts[i + 1][0], pts[i + 1][1], 0);
353
- }
476
+ if (!gl.getProgramParameter(prog, gl.LINK_STATUS)) {
477
+ console.error("[CustomLineLayer] program \u94FE\u63A5\u5931\u8D25:", gl.getProgramInfoLog(prog));
354
478
  }
479
+ this.program = prog;
355
480
  const buf = gl.createBuffer();
356
481
  gl.bindBuffer(gl.ARRAY_BUFFER, buf);
357
- gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(verts), gl.STATIC_DRAW);
482
+ gl.bufferData(gl.ARRAY_BUFFER, this.geometry.vertices, gl.STATIC_DRAW);
358
483
  this.buffer = buf;
359
484
  }
360
485
  render(gl, matrix) {
361
486
  if (!this.program || !this.buffer) return;
362
487
  gl.useProgram(this.program);
363
488
  gl.bindBuffer(gl.ARRAY_BUFFER, this.buffer);
489
+ const stride = this.geometry.stride * 4;
364
490
  const aPos = gl.getAttribLocation(this.program, "aPos");
365
- const aMiter = gl.getAttribLocation(this.program, "aMiter");
491
+ const aDir = gl.getAttribLocation(this.program, "aDir");
492
+ const aSide = gl.getAttribLocation(this.program, "aSide");
493
+ const aGroup = gl.getAttribLocation(this.program, "aGroup");
366
494
  gl.enableVertexAttribArray(aPos);
367
- gl.vertexAttribPointer(aPos, 2, gl.FLOAT, false, 24, 0);
368
- gl.enableVertexAttribArray(aMiter);
369
- gl.vertexAttribPointer(aMiter, 1, gl.FLOAT, false, 24, 16);
495
+ gl.vertexAttribPointer(aPos, 2, gl.FLOAT, false, stride, 0);
496
+ gl.enableVertexAttribArray(aDir);
497
+ gl.vertexAttribPointer(aDir, 2, gl.FLOAT, false, stride, 8);
498
+ gl.enableVertexAttribArray(aSide);
499
+ gl.vertexAttribPointer(aSide, 1, gl.FLOAT, false, stride, 16);
500
+ if (aGroup >= 0) {
501
+ gl.enableVertexAttribArray(aGroup);
502
+ gl.vertexAttribPointer(aGroup, 1, gl.FLOAT, false, stride, 20);
503
+ }
370
504
  const uMatrix = gl.getUniformLocation(this.program, "uMatrix");
371
505
  gl.uniformMatrix4fv(uMatrix, false, new Float32Array(matrix));
372
506
  const uWidth = gl.getUniformLocation(this.program, "uWidth");
@@ -377,12 +511,31 @@ var CustomLineLayer = class {
377
511
  gl.uniform2f(uRes, canvas?.width ?? 1024, canvas?.height ?? 768);
378
512
  gl.enable(gl.BLEND);
379
513
  gl.blendFunc(gl.SRC_ALPHA, gl.ONE);
380
- const color = this.colors[this.lines[0]?.group ?? "pipe"] ?? this.colors.pipe;
381
- for (const pass of this.geometry.passes) {
382
- gl.uniform1f(uWidth, this.baseWidth * pass.widthScale);
514
+ for (const rg of this.geometry.renderGroups) {
515
+ const pass = this.geometry.passes[rg.passIndex];
516
+ const color = this.colors[rg.group] ?? FALLBACK_COLOR;
517
+ gl.uniform1f(uWidth, pass.width);
383
518
  gl.uniform3f(uColor, color[0], color[1], color[2]);
384
519
  gl.uniform1f(uOpacity, pass.opacity);
385
- gl.drawArrays(gl.LINES, 0, this.geometry.vertexCount);
520
+ gl.drawArrays(gl.TRIANGLES, rg.start, rg.count);
521
+ }
522
+ }
523
+ /**
524
+ * 动态更新线集合(高亮选中、切换数据等无需重建图层)。
525
+ * 浏览器环境会刷新 GPU 缓冲;纯逻辑/测试环境仅更新几何与数据。
526
+ */
527
+ setLines(lines) {
528
+ this.lines = lines;
529
+ this.geometry = buildGlowGeometry(this.lines, {
530
+ passes: this.geometry.passes.length,
531
+ baseWidth: this.baseWidth
532
+ });
533
+ for (const g of this.geometry.groups) {
534
+ if (!(g in this.colors)) this.colors[g] = FALLBACK_COLOR;
535
+ }
536
+ if (this.buffer && this.gl) {
537
+ this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.buffer);
538
+ this.gl.bufferData(this.gl.ARRAY_BUFFER, this.geometry.vertices, this.gl.STATIC_DRAW);
386
539
  }
387
540
  }
388
541
  onRemove(_map, gl) {
@@ -446,17 +599,119 @@ var LodController = class {
446
599
  }
447
600
  };
448
601
 
602
+ // src/terrain.ts
603
+ var DEFAULT_TERRAIN_TILES = [
604
+ "https://elevation-tiles-prod.s3.amazonaws.com/terrarium/{z}/{x}/{y}.png"
605
+ ];
606
+ function applyTerrain(mlMap, opts = {}) {
607
+ const sourceId = opts.sourceId ?? "cg-dem";
608
+ if (!mlMap.getSource(sourceId)) {
609
+ mlMap.addSource(sourceId, {
610
+ type: "raster-dem",
611
+ tiles: opts.tiles ?? DEFAULT_TERRAIN_TILES,
612
+ encoding: opts.encoding ?? "terrarium",
613
+ tileSize: 256,
614
+ maxzoom: opts.maxzoom ?? 15
615
+ });
616
+ }
617
+ mlMap.setTerrain({ source: sourceId, exaggeration: opts.exaggeration ?? 1.5 });
618
+ }
619
+ function removeTerrain(mlMap, sourceId = "cg-dem") {
620
+ mlMap.setTerrain(null);
621
+ if (mlMap.getSource(sourceId)) {
622
+ mlMap.removeSource(sourceId);
623
+ }
624
+ }
625
+
626
+ // src/sourceUtils.ts
627
+ function upsertSource(mlMap, id, data) {
628
+ if (mlMap.getSource(id)) {
629
+ if (mlMap.setData) {
630
+ mlMap.setData(id, data);
631
+ } else if (mlMap.addSource) {
632
+ mlMap.addSource(id, data);
633
+ }
634
+ } else if (mlMap.addSource) {
635
+ mlMap.addSource(id, data);
636
+ }
637
+ }
638
+ function removeSourceSafe(mlMap, id) {
639
+ if (mlMap.getSource(id) && mlMap.removeSource) {
640
+ mlMap.removeSource(id);
641
+ }
642
+ }
643
+ function removeSourcesSafe(mlMap, ids) {
644
+ for (const id of ids) {
645
+ removeSourceSafe(mlMap, id);
646
+ }
647
+ }
648
+
449
649
  // src/index.ts
650
+ var WebGLUnavailableError = class extends Error {
651
+ constructor(message = "\u5F53\u524D\u73AF\u5883\u4E0D\u652F\u6301 WebGL\uFF0C\u65E0\u6CD5\u6E32\u67D3\u5730\u56FE") {
652
+ super(message);
653
+ this.name = "WebGLUnavailableError";
654
+ }
655
+ };
656
+ function isWebGLAvailable() {
657
+ try {
658
+ const canvas = document.createElement("canvas");
659
+ let failed = false;
660
+ const onErr = () => {
661
+ failed = true;
662
+ };
663
+ canvas.addEventListener("webglcontextcreationerror", onErr, { once: true });
664
+ const gl = canvas.getContext("webgl2") || canvas.getContext("webgl") || canvas.getContext("experimental-webgl");
665
+ canvas.removeEventListener("webglcontextcreationerror", onErr);
666
+ if (failed || !gl) return false;
667
+ const vendor = gl.getParameter(gl.VERSION);
668
+ if (!vendor) return false;
669
+ const vs = gl.createShader(gl.VERTEX_SHADER);
670
+ if (!vs) return false;
671
+ gl.shaderSource(vs, "attribute vec2 p; void main(){ gl_Position = vec4(p,0.0,1.0); }");
672
+ gl.compileShader(vs);
673
+ if (!gl.getShaderParameter(vs, gl.COMPILE_STATUS)) return false;
674
+ return true;
675
+ } catch {
676
+ return false;
677
+ }
678
+ }
679
+ var GLOBAL_KEY = "__caoguoMapConfig__";
680
+ function setGlobalConfig(cfg) {
681
+ const prev = getGlobalConfig();
682
+ globalThis[GLOBAL_KEY] = { ...prev, ...cfg };
683
+ }
684
+ function getGlobalConfig() {
685
+ return globalThis[GLOBAL_KEY] ?? {};
686
+ }
450
687
  var Map = class {
451
688
  constructor(options) {
452
689
  /** 离线瓦片后端(默认浏览器 IndexedDB / Node 内存) */
453
690
  this._store = createDefaultStore();
454
691
  this._dataCRS = options.dataCRS ?? "WGS84";
692
+ if (!isWebGLAvailable()) {
693
+ throw new WebGLUnavailableError();
694
+ }
695
+ let style = osmRasterStyle();
696
+ if (options.style) {
697
+ style = options.style;
698
+ } else if (options.tianditu) {
699
+ if (!options.tianditu.token) throw new MissingTokenError();
700
+ style = tiandituStyle(
701
+ options.tianditu.type ?? "vector",
702
+ options.tianditu
703
+ );
704
+ } else {
705
+ const g = getGlobalConfig();
706
+ if (g.tiandituToken) {
707
+ style = tiandituStyle("vector", { token: g.tiandituToken });
708
+ }
709
+ }
455
710
  this._map = new maplibregl.Map({
456
711
  container: options.container,
457
712
  center: options.center ?? WUHAN_CENTER,
458
713
  zoom: options.zoom ?? WUHAN_ZOOM,
459
- style: options.style ?? osmRasterStyle(),
714
+ style,
460
715
  pitch: options.pitch ?? 0,
461
716
  bearing: options.bearing ?? 0,
462
717
  attributionControl: { compact: true }
@@ -525,8 +780,23 @@ var Map = class {
525
780
  else this._map.on(event, cb);
526
781
  }
527
782
  addSource(id, source) {
783
+ if (this._map.getSource(id)) {
784
+ try {
785
+ this._map.removeSource(id);
786
+ } catch {
787
+ }
788
+ }
528
789
  this._map.addSource(id, source);
529
790
  }
791
+ /** 安全移除 source(不存在时静默忽略) */
792
+ removeSource(id) {
793
+ if (this._map.getSource(id)) {
794
+ try {
795
+ this._map.removeSource(id);
796
+ } catch {
797
+ }
798
+ }
799
+ }
530
800
  getSource(id) {
531
801
  return this._map.getSource(id);
532
802
  }
@@ -556,6 +826,26 @@ var Map = class {
556
826
  ctrl.addTo(container);
557
827
  return ctrl;
558
828
  }
829
+ /**
830
+ * 挂载图例控件(通用):渲染数据驱动的色块/线段图例,说明专题图层语义。
831
+ * 返回控件实例,可调用 .setItems() 随图层切换更新、.remove() 卸载。
832
+ */
833
+ addLegendControl(options) {
834
+ const ctrl = new LegendControl(options);
835
+ const container = this._map.getContainer();
836
+ ctrl.addTo(container);
837
+ return ctrl;
838
+ }
839
+ /**
840
+ * 挂载地图导出控件(通用):点击按钮将当前视图导出为 PNG 下载。
841
+ * 返回控件实例,可调用 .remove() 卸载。
842
+ */
843
+ addExportControl(options = {}) {
844
+ const ctrl = new ExportControl(this._map, options);
845
+ const container = this._map.getContainer();
846
+ ctrl.addTo(container);
847
+ return ctrl;
848
+ }
559
849
  /**
560
850
  * 挂载辉光管线 Custom Layer(T6 / F-1.3)。
561
851
  * 传入 GeoJSON 线集合,叠加渲染管线/路网/水系辉光效果。
@@ -564,7 +854,10 @@ var Map = class {
564
854
  addGlowLayer(opts) {
565
855
  const layer = new CustomLineLayer(opts);
566
856
  this._map.addLayer(layer);
567
- return layer.id;
857
+ return {
858
+ id: layer.id,
859
+ setLines: (lines) => layer.setLines(lines)
860
+ };
568
861
  }
569
862
  /**
570
863
  * 挂载 LOD 控制器(T7 / F-1.7)。
@@ -576,12 +869,30 @@ var Map = class {
576
869
  ctrl.evaluate(true);
577
870
  return ctrl;
578
871
  }
872
+ /**
873
+ * 启用 3D 地形渲染(F-1.5)。
874
+ *
875
+ * 注入 raster-dem 高程源并调用 maplibre-gl 原生 `setTerrain`,使地图呈现地形起伏。
876
+ * DEM 瓦片默认使用公共 Terrarium 源(elevation-tiles-prod,Terrain-RGB 编码,支持 CORS),
877
+ * 可在 opts.tiles 覆盖为私有瓦片服务(如 MinIO / 本地 MBTiles 服务)。
878
+ *
879
+ * @param opts.exaggeration 地形夸张系数(默认 1.5)
880
+ * @param opts.tiles DEM 瓦片模板,{z}/{x}/{y} 占位
881
+ * @param opts.encoding 'terrarium'(默认,Mapzen)| 'mapbox'
882
+ */
883
+ enableTerrain(opts = {}) {
884
+ applyTerrain(this._map, opts);
885
+ }
886
+ /** 关闭 3D 地形渲染(F-1.5),并移除 DEM 源 */
887
+ disableTerrain(sourceId = "cg-dem") {
888
+ removeTerrain(this._map, sourceId);
889
+ }
579
890
  get instance() {
580
891
  return this._map;
581
892
  }
582
893
  };
583
894
  var src_default = Map;
584
895
 
585
- export { CustomLineLayer, LodController, Map, ScaleControl, ThemeSwitcher, buildGlowGeometry, cgcs2000ToWgs84, computeScaleBar, createTransformer, src_default as default, fromWgs84, gcj02ToWgs84, glowPasses, isInChina, oppositeTheme, projectSimple, resolveLod, setCgcs2000GridShift, suggestDensity, themeFromStyle, toWgs84, transformBounds, transformPoint, wgs84ToCgcs2000, wgs84ToGcj02 };
896
+ export { CustomLineLayer, ExportControl, LegendControl, LodController, Map, ScaleControl, ThemeSwitcher, WebGLUnavailableError, buildGlowGeometry, cgcs2000ToWgs84, computeScaleBar, createTransformer, src_default as default, fromWgs84, gcj02ToWgs84, getGlobalConfig, glowPasses, isInChina, isWebGLAvailable, oppositeTheme, projectSimple, removeSourceSafe, removeSourcesSafe, renderLegendHtml, resolveLod, setCgcs2000GridShift, setGlobalConfig, suggestDensity, themeFromStyle, toWgs84, transformBounds, transformPoint, triggerDownload, upsertSource, wgs84ToCgcs2000, wgs84ToGcj02 };
586
897
  //# sourceMappingURL=index.js.map
587
898
  //# sourceMappingURL=index.js.map