gis-view-webgl2 0.1.11 → 0.1.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +0 -24
- package/dist/gis-view.css +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.d.ts +524 -33
- package/dist/index.js +1 -1
- package/docs/AI/345/257/271/350/257/235/346/261/207/346/200/273.md +156 -8
- package/docs/GisView.md +6 -6
- package/docs/ai/06-/344/274/232/350/257/235/347/264/242/345/274/225/344/270/216/345/244/215/346/237/245/346/226/271/346/263/225.md +157 -7
- package/docs/demos/BackgroundDemo.vue +600 -0
- package/docs/demos/ChannelDemo.vue +145 -3
- package/docs/demos/StyleDemo.vue +280 -69
- package/docs/demos/api-index.js +35 -19
- package/docs/demos/demos.js +26 -18
- package/docs/demos/gis-view-demo/demo.css +76 -0
- package/docs/demos/gis-view-demo/panels/StylePanels.vue +24 -8
- package/docs/demos/gis-view-demo/panels/ViewPanels.vue +33 -3
- package/docs/demos/gis-view-demo/useGisViewDemo.js +236 -22
- package/docs/format/02-/350/212/202/347/202/271/344/270/216/347/272/277/350/267/257.md +1 -1
- package/docs/format/03-/346/213/220/347/202/271/350/243/205/351/245/260/344/270/216/346/240/267/345/274/217.md +3 -3
- package/docs/from0/06-/344/273/216/346/225/260/346/215/256/345/210/260/345/256/236/344/276/213-/350/247/243/346/236/220/345/261/202.md +13 -11
- package/docs/from0/15-/351/231/204/345/275/225-/345/270/270/351/207/217/351/200/237/346/237/245/344/270/216/350/270/251/345/235/221/346/270/205/345/215/225.md +1 -1
- package/docs/index.html +2 -2
- package/docs/internals/10-/350/247/204/345/210/231/346/235/241/344/273/266/344/270/216/347/212/266/346/200/201/346/240/267/345/274/217.md +3 -3
- package/docs/internals/13-/351/271/260/347/234/274/345/233/276.md +1 -1
- package/docs/internals/14-/350/203/214/346/231/257/345/261/202.md +1 -0
- package/docs/internals/15-03-/351/200/220/350/275/256/350/256/260/345/275/225-/346/200/247/350/203/275/344/270/216/344/277/256/345/244/215-/344/270/213.md +199 -0
- package/docs/internals/17-/350/260/203/350/257/225/344/270/216/346/211/251/345/261/225/345/273/272/350/256/256.md +1 -1
- package/docs/internals/18-/346/200/247/350/203/275/345/256/236/346/265/213/344/270/216WebGPU/350/257/204/344/274/260.md +32 -0
- package/docs/manual/02-/345/235/220/346/240/207/344/270/216/346/225/260/346/215/256.md +1 -1
- package/docs/manual/03-/351/205/215/347/275/256/351/241/271.md +217 -26
- package/docs/manual/07-/346/214/207/351/222/210/344/270/216/350/246/201/347/264/240/347/274/226/350/276/221.md +2 -2
- package/docs/manual/08-/351/200/211/344/270/255/344/270/216/346/213/220/347/202/271.md +37 -4
- package/docs/manual/09-/351/200/232/351/201/223/344/270/216/345/244/232/345/233/236/347/272/277.md +58 -1
- package/docs/manual/11-/346/240/267/345/274/217/344/270/216/350/247/204/345/210/231.md +59 -8
- package/docs/manual/12-/346/240/207/347/255/276/344/270/216/350/243/205/351/245/260.md +1 -1
- package/docs/manual/13-/345/212/250/346/225/210/344/270/216/345/233/276/345/205/203.md +220 -2
- package/docs/manual/14-/350/247/206/345/233/276/344/270/216/347/255/233/351/200/211.md +77 -16
- package/docs/manual/15-/345/257/274/345/207/272/344/270/216/351/233/206/346/210/220.md +30 -17
- package/docs/manual/16-/345/256/236/344/276/213/346/226/271/346/263/225/346/200/273/350/241/250.md +11 -4
- package/docs/manual/17-/345/217/202/346/225/260/351/200/237/346/237/245.md +17 -8
- package/docs/manual/18-/351/200/220/346/226/271/346/263/225/347/244/272/344/276/213.md +128 -4
- package/docs/manual/19-/350/257/273/346/225/260/344/270/216/346/213/226/346/213/275/351/242/204/350/247/210.md +2 -2
- package/docs/manual/20-/346/200/247/350/203/275/344/270/216/345/244/247/346/225/260/346/215/256.md +13 -1
- package/docs/manual/21-/350/257/264/346/230/216/344/270/216/345/270/270/350/247/201/351/227/256/351/242/230.md +4 -1
- package/docs/steps/index.html +1 -1
- package/docs/steps/selftest-demo.html +2 -2
- package/docs/steps/selftest-demos.html +626 -11
- package/docs/steps/selftest-docs.html +8 -4
- package/docs/steps/selftest-features.html +1428 -2
- package/docs/steps/selftest-perf.html +296 -25
- package/docs/steps/selftest-scale.html +61 -3
- package/docs/steps/selftest-select.html +303 -1
- package/docs/steps/step-06-style.js +3 -2
- package/package.json +2 -2
|
@@ -663,7 +663,7 @@
|
|
|
663
663
|
for (let index = 0; index < anchorLines.length; index += 1) {
|
|
664
664
|
anchorLines[index] = { id: `ax${index}`, start: [anchorRandom() * 4000, anchorRandom() * 2500], end: [anchorRandom() * 4000, anchorRandom() * 2500], name: `长线 ${index}` }
|
|
665
665
|
}
|
|
666
|
-
const anchorView = createGisView(anchorHost, { points: anchorPoints, lines: anchorLines, width: 900, height: 560, label: { content: '{name}', maxCount: 200 }, lineLabel: { maxCount: 60 } })
|
|
666
|
+
const anchorView = createGisView(anchorHost, { points: anchorPoints, lines: anchorLines, width: 900, height: 560, progressiveInit: false, labelOptimize: true, label: { content: '{name}', maxCount: 200 }, lineLabel: { maxCount: 60 } })
|
|
667
667
|
await sleep(200)
|
|
668
668
|
for (let step = 0; step < 6; step += 1) {
|
|
669
669
|
anchorView.setCamera({ center: [1000 + step * 120, 700 + step * 60], scale: 1.4 })
|
|
@@ -673,15 +673,28 @@
|
|
|
673
673
|
// 程序化相机运动还有 160ms 的轻量画法窗口,停稳后会补一张全质量帧并再收一次候选。
|
|
674
674
|
// 先让这一帧落地,再开始计量,后面测到的才是稳态重排,而不是收尾那一帧的噪声。
|
|
675
675
|
await sleep(800)
|
|
676
|
+
// 四万点 + 四万线的发散渲染可能比上面那一段更慢:没铺完时 refreshLabels 会主动等下一帧,
|
|
677
|
+
// 这时六轮 updateOptions 的读数都是 0,随后 updatePoint 正好撞上第一次真实排版,
|
|
678
|
+
// 会把整屏标签都算成「第一次出现」。先等到 isLayoutReady 再做稳态断言。
|
|
679
|
+
const anchorReadyDeadline = now() + 8000
|
|
680
|
+
while (!anchorView.isLayoutReady() && now() < anchorReadyDeadline) {
|
|
681
|
+
await sleep(16)
|
|
682
|
+
}
|
|
683
|
+
await new Promise(resolve => requestAnimationFrame(() => resolve()))
|
|
684
|
+
// 字体加载完成会推进 labelFontEpoch,让所有标签尺寸缓存按新字体重量一次。
|
|
685
|
+
// 这是真实且必要的一次失效,不能算进「稳态重排」;先等字体集合稳定,避免冷启动把
|
|
686
|
+
// 最后一轮定向更新的读数抬成整屏标签数。
|
|
687
|
+
await document.fonts?.ready
|
|
688
|
+
await sleep(50)
|
|
676
689
|
const anchorStats = { reads: 0, duplicates: 0 }
|
|
677
|
-
const anchorSeen = new Set()
|
|
678
|
-
const anchorReads = []
|
|
679
|
-
const anchorDuplicates = []
|
|
680
|
-
const nativeAnchorRect = Element.prototype.getBoundingClientRect
|
|
681
|
-
Element.prototype.getBoundingClientRect = function (...args) {
|
|
682
|
-
if (this.classList?.contains('gis-view__label')) {
|
|
683
|
-
anchorStats.reads += 1
|
|
684
|
-
// 按标签身份(类型 + 行号 + 标签名)去重:标签节点是循环复用的,按节点去重会把「同一格换了条标签」误判成重复量
|
|
690
|
+
const anchorSeen = new Set()
|
|
691
|
+
const anchorReads = []
|
|
692
|
+
const anchorDuplicates = []
|
|
693
|
+
const nativeAnchorRect = Element.prototype.getBoundingClientRect
|
|
694
|
+
Element.prototype.getBoundingClientRect = function (...args) {
|
|
695
|
+
if (this.classList?.contains('gis-view__label')) {
|
|
696
|
+
anchorStats.reads += 1
|
|
697
|
+
// 按标签身份(类型 + 行号 + 标签名)去重:标签节点是循环复用的,按节点去重会把「同一格换了条标签」误判成重复量
|
|
685
698
|
const anchorKey = `${this.dataset.labelKind}#${this.dataset.labelIndex}#${this.dataset.labelLabel}`
|
|
686
699
|
if (anchorSeen.has(anchorKey)) {
|
|
687
700
|
anchorStats.duplicates += 1
|
|
@@ -690,11 +703,11 @@
|
|
|
690
703
|
}
|
|
691
704
|
return nativeAnchorRect.apply(this, args)
|
|
692
705
|
}
|
|
693
|
-
const anchorRound = () => {
|
|
694
|
-
anchorStats.reads = 0
|
|
695
|
-
anchorStats.duplicates = 0
|
|
696
|
-
anchorSeen.clear()
|
|
697
|
-
}
|
|
706
|
+
const anchorRound = () => {
|
|
707
|
+
anchorStats.reads = 0
|
|
708
|
+
anchorStats.duplicates = 0
|
|
709
|
+
anchorSeen.clear()
|
|
710
|
+
}
|
|
698
711
|
try {
|
|
699
712
|
// 跑 6 轮:换锚点(anchorFallback)/ 沿线滑动(slideFallback)会让某一轮补画几十条
|
|
700
713
|
// 从没出现过的标签 —— 第一次出现必须量一次尺寸,这一笔有限、下一轮起回零;
|
|
@@ -702,8 +715,8 @@
|
|
|
702
715
|
for (let round = 0; round < 6; round += 1) {
|
|
703
716
|
anchorRound()
|
|
704
717
|
anchorView.updateOptions({ label: { content: '{name}', maxCount: 200 }, lineLabel: { maxCount: 60 } })
|
|
705
|
-
anchorReads.push(anchorStats.reads)
|
|
706
|
-
anchorDuplicates.push(anchorStats.duplicates)
|
|
718
|
+
anchorReads.push(anchorStats.reads)
|
|
719
|
+
anchorDuplicates.push(anchorStats.duplicates)
|
|
707
720
|
}
|
|
708
721
|
// 改一条视口里的标签(下标 = 76 行 × 400 列 + 150 → 世界坐标 1500, 760):这一轮同样不该整层重量
|
|
709
722
|
anchorRound()
|
|
@@ -714,13 +727,13 @@
|
|
|
714
727
|
finally {
|
|
715
728
|
Element.prototype.getBoundingClientRect = nativeAnchorRect
|
|
716
729
|
}
|
|
717
|
-
diag('放大后连续整层重排 · 标签量测读数', `逐轮 ${anchorReads.join(' / ')} 次 · 同一轮里重复量 ${anchorDuplicates.join(' / ')} 条(按标签身份去重;旧写法第 1 轮 2662 次里有 1235 次是把同一条标签又量了一遍)`)
|
|
730
|
+
diag('放大后连续整层重排 · 标签量测读数', `逐轮 ${anchorReads.join(' / ')} 次 · 同一轮里重复量 ${anchorDuplicates.join(' / ')} 条(按标签身份去重;旧写法第 1 轮 2662 次里有 1235 次是把同一条标签又量了一遍)`)
|
|
718
731
|
check('稳态重排(后 3 轮)一次布局都不读(避让盒全走算术)', anchorReads.slice(3, 6).every(value => value === 0), `第 4~6 轮 ${anchorReads.slice(3, 6).join(' / ')} 次`)
|
|
719
732
|
// 换落点那条路补画的新标签都是「第一次出现」,各量一次尺寸就进缓存;整段合计要有限,
|
|
720
733
|
// 真退回「每轮把所有标签都真量一遍」的老写法时这里会立刻爆掉(旧写法一轮 1299 ~ 2662 次)
|
|
721
734
|
check('稳态重排里补画的新标签只量第一次出现的那一次(合计有限、之后回零)', anchorReads.slice(1, 6).reduce((sum, value) => sum + value, 0) <= 400, `第 2~6 轮合计 ${anchorReads.slice(1, 6).reduce((sum, value) => sum + value, 0)} 次(逐轮 ${anchorReads.slice(1, 6).join(' / ')})`)
|
|
722
735
|
check('整层重排里没有哪条标签(按身份)被量第二次(避让盒不再退回真量一次)', anchorDuplicates.every(value => value === 0), `逐轮重复 ${anchorDuplicates.join(' / ')} 条`)
|
|
723
|
-
check('改一条可见标签之后的定向重排也不读布局、不重复量', anchorDuplicates[6] === 0 && anchorReads[6] === 0, `读数 ${anchorReads[6]} 次 · 重复 ${anchorDuplicates[6]} 条`)
|
|
736
|
+
check('改一条可见标签之后的定向重排也不读布局、不重复量', anchorDuplicates[6] === 0 && anchorReads[6] === 0, `读数 ${anchorReads[6]} 次 · 重复 ${anchorDuplicates[6]} 条`)
|
|
724
737
|
anchorView.destroy()
|
|
725
738
|
anchorHost.remove()
|
|
726
739
|
labelView.destroy()
|
|
@@ -1447,10 +1460,225 @@
|
|
|
1447
1460
|
const densityAfterPoint = densityView.getStats().heatDensity
|
|
1448
1461
|
check('热力图密度场:定向 updatePoint 改要素值后场跟着重烤(不再停在旧数据上)',
|
|
1449
1462
|
densityAfterPoint !== densityAfterColor && densityAfterPoint.samples === densityAfterColor.samples)
|
|
1450
|
-
diag('热力图密度场更新', `纯透明度 ${opacityUpdateMs.toFixed(2)} ms / 换色带 ${colorUpdateMs.toFixed(2)} ms;烘焙 ${densityAfterColor.ms.toFixed(1)} ms`)
|
|
1451
|
-
densityView.destroy()
|
|
1452
|
-
densityHost.remove()
|
|
1453
|
-
|
|
1463
|
+
diag('热力图密度场更新', `纯透明度 ${opacityUpdateMs.toFixed(2)} ms / 换色带 ${colorUpdateMs.toFixed(2)} ms;烘焙 ${densityAfterColor.ms.toFixed(1)} ms`)
|
|
1464
|
+
densityView.destroy()
|
|
1465
|
+
densityHost.remove()
|
|
1466
|
+
|
|
1467
|
+
/* 密度场不能复用背景图片的四边形:图片之外的要素也必须按自己的世界范围铺场 */
|
|
1468
|
+
const densityBackgroundHost = document.createElement('div')
|
|
1469
|
+
densityBackgroundHost.style.cssText = 'width:640px;height:380px'
|
|
1470
|
+
document.body.append(densityBackgroundHost)
|
|
1471
|
+
const densityBackgroundSvg = `data:image/svg+xml,${encodeURIComponent('<svg xmlns="http://www.w3.org/2000/svg" width="8" height="8"><rect width="8" height="8" fill="#ffffff"/></svg>')}`
|
|
1472
|
+
const densityBackgroundView = createGisView(densityBackgroundHost, {
|
|
1473
|
+
points: [
|
|
1474
|
+
{ id: 'dbg-a', x: 400, y: 200, value: 100 },
|
|
1475
|
+
{ id: 'dbg-b', x: 460, y: 200, value: 100 }
|
|
1476
|
+
],
|
|
1477
|
+
lines: [],
|
|
1478
|
+
pointStyle: { opacity: 0 },
|
|
1479
|
+
heatmap: {
|
|
1480
|
+
enabled: true,
|
|
1481
|
+
target: 'point',
|
|
1482
|
+
field: 'value',
|
|
1483
|
+
domain: [0, 100],
|
|
1484
|
+
colors: ['#ff0000', '#ff0000'],
|
|
1485
|
+
opacity: 1,
|
|
1486
|
+
glow: false,
|
|
1487
|
+
density: { style: 'smooth', resolution: 96, radiusRatio: 0.12, threshold: 0, opacity: 1 }
|
|
1488
|
+
},
|
|
1489
|
+
background: { color: '#000000', image: { src: densityBackgroundSvg, bounds: [0, 0, 80, 80] } },
|
|
1490
|
+
width: 640,
|
|
1491
|
+
height: 380,
|
|
1492
|
+
quality: 'high',
|
|
1493
|
+
showGrid: false
|
|
1494
|
+
})
|
|
1495
|
+
densityBackgroundView.setCamera({ center: [430, 200], scale: 1 })
|
|
1496
|
+
await densityBackgroundView.whenRendered()
|
|
1497
|
+
// 等背景图片解码完成后再显式补一帧;静止画面不会自己继续画,不能先 sleep 再等 whenRendered
|
|
1498
|
+
await sleep(300)
|
|
1499
|
+
densityBackgroundView.refresh()
|
|
1500
|
+
await densityBackgroundView.whenRendered()
|
|
1501
|
+
const densityBackgroundCanvas = densityBackgroundHost.querySelector('canvas')
|
|
1502
|
+
const densityBackgroundGl = densityBackgroundCanvas.getContext('webgl2')
|
|
1503
|
+
const densityBackgroundPixels = new Uint8Array(densityBackgroundCanvas.width * densityBackgroundCanvas.height * 4)
|
|
1504
|
+
densityBackgroundGl.readPixels(0, 0, densityBackgroundCanvas.width, densityBackgroundCanvas.height, densityBackgroundGl.RGBA, densityBackgroundGl.UNSIGNED_BYTE, densityBackgroundPixels)
|
|
1505
|
+
let densityBackgroundWarm = 0
|
|
1506
|
+
for (let index = 0; index < densityBackgroundPixels.length; index += 4) {
|
|
1507
|
+
const red = densityBackgroundPixels[index]
|
|
1508
|
+
if (red > 20 && red > Math.max(densityBackgroundPixels[index + 1], densityBackgroundPixels[index + 2]) + 5) {
|
|
1509
|
+
densityBackgroundWarm += 1
|
|
1510
|
+
}
|
|
1511
|
+
}
|
|
1512
|
+
check('热力图密度场:有图片背景时仍按数据范围铺场,图片范围外不漏画',
|
|
1513
|
+
Boolean(densityBackgroundView.snapshotScene().backgroundImage) && densityBackgroundWarm > 20 && densityBackgroundView.getStats().heatDensity?.samples > 0,
|
|
1514
|
+
JSON.stringify({ warm: densityBackgroundWarm, density: densityBackgroundView.getStats().heatDensity, image: Boolean(densityBackgroundView.snapshotScene().backgroundImage) }))
|
|
1515
|
+
densityBackgroundView.destroy()
|
|
1516
|
+
densityBackgroundHost.remove()
|
|
1517
|
+
|
|
1518
|
+
/* 热力交互帧:线路光晕不消失,且合并成一笔绘制(不再退回「无光晕 + 细线」) */
|
|
1519
|
+
const heatMotionHost = document.createElement('div')
|
|
1520
|
+
heatMotionHost.style.cssText = 'width:640px;height:380px'
|
|
1521
|
+
document.body.append(heatMotionHost)
|
|
1522
|
+
heatMotionHost.scrollIntoView({ block: 'center', inline: 'nearest' })
|
|
1523
|
+
const heatMotionPoints = Array.from({ length: 12 }, (unused, index) => ({
|
|
1524
|
+
id: `hm-p${index}`,
|
|
1525
|
+
x: (index % 4) * 80,
|
|
1526
|
+
y: Math.floor(index / 4) * 90
|
|
1527
|
+
}))
|
|
1528
|
+
const heatMotionLines = Array.from({ length: 18 }, (unused, index) => ({
|
|
1529
|
+
id: `hm-l${index}`,
|
|
1530
|
+
from: heatMotionPoints[index % heatMotionPoints.length].id,
|
|
1531
|
+
to: heatMotionPoints[(index * 5 + 3) % heatMotionPoints.length].id,
|
|
1532
|
+
value: 30 + (index % 5) * 15
|
|
1533
|
+
}))
|
|
1534
|
+
const heatMotionGlow = { strength: 1, size: [20, 3], blend: 'normal' }
|
|
1535
|
+
const heatMotionHeatmap = {
|
|
1536
|
+
enabled: true,
|
|
1537
|
+
target: 'line',
|
|
1538
|
+
field: 'value',
|
|
1539
|
+
domain: [0, 100],
|
|
1540
|
+
colors: ['#ff2d2d', '#ff2d2d'],
|
|
1541
|
+
opacity: 1,
|
|
1542
|
+
density: false,
|
|
1543
|
+
glow: heatMotionGlow
|
|
1544
|
+
}
|
|
1545
|
+
const heatMotionView = createGisView(heatMotionHost, {
|
|
1546
|
+
points: heatMotionPoints,
|
|
1547
|
+
lines: heatMotionLines,
|
|
1548
|
+
lineStyle: { width: 1, opacity: 1 },
|
|
1549
|
+
heatmap: heatMotionHeatmap,
|
|
1550
|
+
background: '#05070b',
|
|
1551
|
+
width: 640,
|
|
1552
|
+
height: 380,
|
|
1553
|
+
quality: 'high',
|
|
1554
|
+
interactionNoMsaa: false,
|
|
1555
|
+
showGrid: false
|
|
1556
|
+
})
|
|
1557
|
+
await heatMotionView.whenRendered()
|
|
1558
|
+
await sleep(80)
|
|
1559
|
+
const heatMotionCanvas = heatMotionHost.querySelector('canvas')
|
|
1560
|
+
const heatMotionGl = heatMotionCanvas.getContext('webgl2')
|
|
1561
|
+
const readHeatMotionPixels = () => {
|
|
1562
|
+
const pixels = new Uint8Array(heatMotionCanvas.width * heatMotionCanvas.height * 4)
|
|
1563
|
+
heatMotionGl.readPixels(0, 0, heatMotionCanvas.width, heatMotionCanvas.height, heatMotionGl.RGBA, heatMotionGl.UNSIGNED_BYTE, pixels)
|
|
1564
|
+
return pixels
|
|
1565
|
+
}
|
|
1566
|
+
const summarizeHeatMotionPixels = (pixels) => {
|
|
1567
|
+
let warm = 0
|
|
1568
|
+
let intensity = 0
|
|
1569
|
+
for (let index = 0; index < pixels.length; index += 4) {
|
|
1570
|
+
const red = pixels[index]
|
|
1571
|
+
const green = pixels[index + 1]
|
|
1572
|
+
const blue = pixels[index + 2]
|
|
1573
|
+
const redness = Math.max(0, red - Math.max(green, blue))
|
|
1574
|
+
intensity += redness
|
|
1575
|
+
if (red > 10 && redness > 3) {
|
|
1576
|
+
warm += 1
|
|
1577
|
+
}
|
|
1578
|
+
}
|
|
1579
|
+
return { warm, intensity }
|
|
1580
|
+
}
|
|
1581
|
+
const readHeatMotionWarmPixels = () => summarizeHeatMotionPixels(readHeatMotionPixels())
|
|
1582
|
+
const heatMotionPointById = new Map(heatMotionPoints.map(point => [point.id, point]))
|
|
1583
|
+
const heatMotionMidpoints = heatMotionLines.map((line) => {
|
|
1584
|
+
const from = heatMotionPointById.get(line.from)
|
|
1585
|
+
const to = heatMotionPointById.get(line.to)
|
|
1586
|
+
return [(from.x + to.x) / 2, (from.y + to.y) / 2]
|
|
1587
|
+
})
|
|
1588
|
+
const heatMotionWarmAt = (pixels, screen) => {
|
|
1589
|
+
if (!screen) {
|
|
1590
|
+
return false
|
|
1591
|
+
}
|
|
1592
|
+
const scaleX = heatMotionCanvas.width / Math.max(heatMotionHost.clientWidth, 1)
|
|
1593
|
+
const scaleY = heatMotionCanvas.height / Math.max(heatMotionHost.clientHeight, 1)
|
|
1594
|
+
const centerX = Math.round(screen[0] * scaleX)
|
|
1595
|
+
const centerY = Math.round((heatMotionHost.clientHeight - screen[1]) * scaleY)
|
|
1596
|
+
for (let offsetY = -5; offsetY <= 5; offsetY += 1) {
|
|
1597
|
+
const y = centerY + offsetY
|
|
1598
|
+
if (y < 0 || y >= heatMotionCanvas.height) {
|
|
1599
|
+
continue
|
|
1600
|
+
}
|
|
1601
|
+
for (let offsetX = -5; offsetX <= 5; offsetX += 1) {
|
|
1602
|
+
const x = centerX + offsetX
|
|
1603
|
+
if (x < 0 || x >= heatMotionCanvas.width) {
|
|
1604
|
+
continue
|
|
1605
|
+
}
|
|
1606
|
+
const index = (y * heatMotionCanvas.width + x) * 4
|
|
1607
|
+
const red = pixels[index]
|
|
1608
|
+
if (red > 8 && red > Math.max(pixels[index + 1], pixels[index + 2]) + 2) {
|
|
1609
|
+
return true
|
|
1610
|
+
}
|
|
1611
|
+
}
|
|
1612
|
+
}
|
|
1613
|
+
return false
|
|
1614
|
+
}
|
|
1615
|
+
const heatMotionMissingLines = pixels => heatMotionMidpoints
|
|
1616
|
+
.map((midpoint, index) => (heatMotionWarmAt(pixels, heatMotionView.project(midpoint[0], midpoint[1])) ? null : index))
|
|
1617
|
+
.filter(index => index !== null)
|
|
1618
|
+
const heatMotionStart = heatMotionView.getCamera()
|
|
1619
|
+
heatMotionView.setCamera({ center: [heatMotionStart.center[0] + 2, heatMotionStart.center[1] + 1], scale: heatMotionStart.scale * 1.15 })
|
|
1620
|
+
await new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve)))
|
|
1621
|
+
heatMotionView.refresh({ reflow: false })
|
|
1622
|
+
const heatMotionMovingPixels = readHeatMotionPixels()
|
|
1623
|
+
const heatMotionMovingWarm = summarizeHeatMotionPixels(heatMotionMovingPixels)
|
|
1624
|
+
const heatMotionMovingStats = heatMotionView.getStats()
|
|
1625
|
+
const heatMotionMovingMissing = heatMotionMissingLines(heatMotionMovingPixels)
|
|
1626
|
+
await sleep(360)
|
|
1627
|
+
heatMotionView.refresh({ reflow: false })
|
|
1628
|
+
const heatMotionStaticPixels = readHeatMotionPixels()
|
|
1629
|
+
const heatMotionStaticWarm = summarizeHeatMotionPixels(heatMotionStaticPixels)
|
|
1630
|
+
const heatMotionStaticStats = heatMotionView.getStats()
|
|
1631
|
+
const heatMotionStaticMissing = heatMotionMissingLines(heatMotionStaticPixels)
|
|
1632
|
+
check('热力图交互帧:逐条线路在相机运动与停稳帧都有热力(不是只统计整屏暖色)',
|
|
1633
|
+
heatMotionMovingMissing.length === 0 && heatMotionStaticMissing.length === 0,
|
|
1634
|
+
`运动缺失 ${heatMotionMovingMissing.join(',') || '无'} / 停稳缺失 ${heatMotionStaticMissing.join(',') || '无'}`)
|
|
1635
|
+
check('热力图交互帧:相机运动时线路光晕仍在(暖色像素与停稳帧相当)',
|
|
1636
|
+
heatMotionStaticWarm.warm > 0 && heatMotionMovingWarm.warm > 0 && heatMotionMovingWarm.warm / heatMotionStaticWarm.warm > 0.55 && heatMotionMovingWarm.warm / heatMotionStaticWarm.warm < 1.8 && heatMotionMovingWarm.intensity / heatMotionStaticWarm.intensity > 0.65 && heatMotionMovingWarm.intensity / heatMotionStaticWarm.intensity < 1.5,
|
|
1637
|
+
`停稳 ${heatMotionStaticWarm.warm} 像素 / 强度 ${heatMotionStaticWarm.intensity};运动 ${heatMotionMovingWarm.warm} 像素 / 强度 ${heatMotionMovingWarm.intensity}`)
|
|
1638
|
+
check('热力图交互帧:合并管线接管线路绘制,不再走会丢光晕的细线快路径,也不多出绘制批次',
|
|
1639
|
+
heatMotionMovingStats.thinLines === false && heatMotionMovingStats.drawCalls <= heatMotionStaticStats.drawCalls,
|
|
1640
|
+
`thinLines=${heatMotionMovingStats.thinLines} drawCalls ${heatMotionStaticStats.drawCalls} -> ${heatMotionMovingStats.drawCalls}`)
|
|
1641
|
+
heatMotionView.updateOptions({ heatmap: { ...heatMotionHeatmap, glow: false } })
|
|
1642
|
+
await sleep(40)
|
|
1643
|
+
heatMotionView.refresh({ reflow: false })
|
|
1644
|
+
const heatMotionNoGlowWarm = readHeatMotionWarmPixels()
|
|
1645
|
+
check('热力图交互帧:关掉光晕后暖色覆盖明显收窄(像素断言确实量到光晕)',
|
|
1646
|
+
heatMotionNoGlowWarm.warm > 0 && heatMotionNoGlowWarm.warm < heatMotionStaticWarm.warm * 0.8 && heatMotionNoGlowWarm.intensity < heatMotionStaticWarm.intensity * 0.8,
|
|
1647
|
+
`有光晕 ${heatMotionStaticWarm.warm} 像素 / 强度 ${heatMotionStaticWarm.intensity};无光晕 ${heatMotionNoGlowWarm.warm} 像素 / 强度 ${heatMotionNoGlowWarm.intensity}`)
|
|
1648
|
+
const heatMotionNoGlowStart = heatMotionView.getCamera()
|
|
1649
|
+
heatMotionView.setCamera({ center: [heatMotionNoGlowStart.center[0] + 2, heatMotionNoGlowStart.center[1] - 1], scale: heatMotionNoGlowStart.scale * 1.02 })
|
|
1650
|
+
await new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve)))
|
|
1651
|
+
heatMotionView.refresh({ reflow: false })
|
|
1652
|
+
const heatMotionNoGlowMovingWarm = readHeatMotionWarmPixels()
|
|
1653
|
+
const heatMotionNoGlowMovingStats = heatMotionView.getStats()
|
|
1654
|
+
check('热力图交互帧:关光晕时细线快路径照常生效(说明上面确实走的是合并分支)',
|
|
1655
|
+
heatMotionNoGlowMovingStats.thinLines === true,
|
|
1656
|
+
`thinLines=${heatMotionNoGlowMovingStats.thinLines}`)
|
|
1657
|
+
// 深底色最常用的加法混合也走合并管线:glow.blend 一变,混合状态与预乘输出要同时切过去
|
|
1658
|
+
heatMotionView.updateOptions({ heatmap: { ...heatMotionHeatmap, glow: { ...heatMotionGlow, blend: 'add' } } })
|
|
1659
|
+
await sleep(360)
|
|
1660
|
+
heatMotionView.refresh({ reflow: false })
|
|
1661
|
+
const heatMotionAddStaticPixels = readHeatMotionPixels()
|
|
1662
|
+
const heatMotionAddStaticWarm = summarizeHeatMotionPixels(heatMotionAddStaticPixels)
|
|
1663
|
+
const heatMotionAddStart = heatMotionView.getCamera()
|
|
1664
|
+
heatMotionView.setCamera({ center: [heatMotionAddStart.center[0] - 2, heatMotionAddStart.center[1] + 1], scale: heatMotionAddStart.scale * 1.02 })
|
|
1665
|
+
await new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve)))
|
|
1666
|
+
heatMotionView.refresh({ reflow: false })
|
|
1667
|
+
const heatMotionAddMovingPixels = readHeatMotionPixels()
|
|
1668
|
+
const heatMotionAddMovingWarm = summarizeHeatMotionPixels(heatMotionAddMovingPixels)
|
|
1669
|
+
const heatMotionAddMovingStats = heatMotionView.getStats()
|
|
1670
|
+
const heatMotionAddStaticMissing = heatMotionMissingLines(heatMotionAddStaticPixels)
|
|
1671
|
+
const heatMotionAddMovingMissing = heatMotionMissingLines(heatMotionAddMovingPixels)
|
|
1672
|
+
check('热力图交互帧:配置加法混合时仍走两遍光晕管线,逐条线路在停稳与运动帧都不缺失',
|
|
1673
|
+
heatMotionAddStaticWarm.warm > 0 && heatMotionAddMovingWarm.warm > 0 &&
|
|
1674
|
+
heatMotionAddStaticMissing.length === 0 && heatMotionAddMovingMissing.length === 0 &&
|
|
1675
|
+
heatMotionAddMovingStats.thinLines === false,
|
|
1676
|
+
`停稳缺失 ${heatMotionAddStaticMissing.join(',') || '无'} / 运动缺失 ${heatMotionAddMovingMissing.join(',') || '无'};停稳 ${heatMotionAddStaticWarm.warm} 像素,运动 ${heatMotionAddMovingWarm.warm} 像素;thinLines=${heatMotionAddMovingStats.thinLines}`)
|
|
1677
|
+
await sleep(360)
|
|
1678
|
+
heatMotionView.destroy()
|
|
1679
|
+
heatMotionHost.remove()
|
|
1680
|
+
|
|
1681
|
+
heatFreshView.destroy()
|
|
1454
1682
|
heatFreshHost.remove()
|
|
1455
1683
|
heatView.destroy()
|
|
1456
1684
|
heatHost.remove()
|
|
@@ -2448,9 +2676,52 @@
|
|
|
2448
2676
|
check('同一批点线回灌:换通道骨架时线路那一半不能沿用旧几何',
|
|
2449
2677
|
JSON.stringify(guardChanged?.lines?.map(line => line.waypoints)) !== JSON.stringify(guardAfter?.lines?.map(line => line.waypoints)),
|
|
2450
2678
|
JSON.stringify({ before: guardAfter?.lines?.map(line => line.waypoints), after: guardChanged?.lines?.map(line => line.waypoints) }))
|
|
2451
|
-
big.updateOptions({ channels: [] })
|
|
2452
|
-
|
|
2453
|
-
|
|
2679
|
+
big.updateOptions({ channels: [] })
|
|
2680
|
+
|
|
2681
|
+
/* ---------------- visualMap:图例更新快路径 / 同签名零 DOM 写入 / 自动值域 ---------------- */
|
|
2682
|
+
const visualPerfPointCount = 12000
|
|
2683
|
+
const visualPerfPoints = new Array(visualPerfPointCount)
|
|
2684
|
+
for (let index = 0; index < visualPerfPointCount; index += 1) {
|
|
2685
|
+
visualPerfPoints[index] = { id: `vp${index}`, x: (index % 120) * 12, y: Math.floor(index / 120) * 12, value: index % 1000 }
|
|
2686
|
+
}
|
|
2687
|
+
const visualPerfHost = document.createElement('div')
|
|
2688
|
+
visualPerfHost.style.cssText = 'width:640px;height:380px'
|
|
2689
|
+
document.querySelector('#map').append(visualPerfHost)
|
|
2690
|
+
const visualPerfView = createGisView(visualPerfHost, {
|
|
2691
|
+
points: visualPerfPoints,
|
|
2692
|
+
heatmap: { field: 'value', target: 'point', density: false, glow: false, opacity: 1 },
|
|
2693
|
+
visualMap: { target: 'point', title: '性能图例', unit: 'kW', tickCount: 5 },
|
|
2694
|
+
width: 640,
|
|
2695
|
+
height: 380,
|
|
2696
|
+
quality: 'high',
|
|
2697
|
+
showGrid: false
|
|
2698
|
+
})
|
|
2699
|
+
await visualPerfView.whenRendered()
|
|
2700
|
+
await sleep(40)
|
|
2701
|
+
const visualPerfDomain = visualPerfView.getVisualMap()?.domain
|
|
2702
|
+
check('visualMap:一万二千点按当前数据自动取值域', visualPerfDomain?.[0] === 0 && visualPerfDomain?.[1] === 999, JSON.stringify(visualPerfDomain))
|
|
2703
|
+
const visualPerfElement = visualPerfView.getVisualMapElement()
|
|
2704
|
+
let visualPerfMutations = 0
|
|
2705
|
+
const visualPerfObserver = new MutationObserver(records => { visualPerfMutations += records.length })
|
|
2706
|
+
visualPerfObserver.observe(visualPerfElement, { subtree: true, childList: true, attributes: true, characterData: true })
|
|
2707
|
+
const visualPerfPatch = { target: 'point', title: '性能图例', unit: 'kW', tickCount: 5 }
|
|
2708
|
+
const visualPerfStarted = now()
|
|
2709
|
+
for (let round = 0; round < 50; round += 1) {
|
|
2710
|
+
visualPerfView.updateOptions({ visualMap: { ...visualPerfPatch } })
|
|
2711
|
+
}
|
|
2712
|
+
const visualPerfSyncMs = (now() - visualPerfStarted) / 50
|
|
2713
|
+
await sleep(80)
|
|
2714
|
+
visualPerfObserver.disconnect()
|
|
2715
|
+
check('visualMap:50 次同签名更新只合帧一次,DOM 写入为 0', visualPerfMutations === 0, `${visualPerfMutations} 条 mutation`)
|
|
2716
|
+
diag('visualMap:只改图例的同步开销(1.2 万点热力图)', `${visualPerfSyncMs.toFixed(4)} ms / 次(50 次)`)
|
|
2717
|
+
visualPerfView.updatePoint('vp9999', { value: 5000 })
|
|
2718
|
+
const visualPerfUpdatedDomain = visualPerfView.getVisualMap()?.domain
|
|
2719
|
+
check('visualMap:定向改数据后自动值域跟着当前数据走', visualPerfUpdatedDomain?.[1] === 5000, JSON.stringify(visualPerfUpdatedDomain))
|
|
2720
|
+
visualPerfView.updateOptions({ visualMap: null })
|
|
2721
|
+
visualPerfView.destroy()
|
|
2722
|
+
visualPerfHost.remove()
|
|
2723
|
+
|
|
2724
|
+
thinView.destroy()
|
|
2454
2725
|
thinHost.remove()
|
|
2455
2726
|
big.destroy()
|
|
2456
2727
|
bigHost.remove()
|
|
@@ -907,16 +907,60 @@
|
|
|
907
907
|
const spacingScreen1 = spacingView.project(60, 0)
|
|
908
908
|
const spacingScreen = Math.hypot(spacingScreen1[0] - spacingScreen0[0], spacingScreen1[1] - spacingScreen0[1])
|
|
909
909
|
check('适应节点间距:最近节点中心距达到 minDistance(CSS 像素)', Math.abs(spacingScreen - 24) < 0.1, String(spacingScreen))
|
|
910
|
+
const exactSpacingView = createGisView(host(640, 320), {
|
|
911
|
+
points: [{ id: 'e0', x: 0, y: 0, size: 10 }, { id: 'e1', x: 60, y: 0, size: 10 }],
|
|
912
|
+
fitSpacing: { minDistance: 24, maxZoom: 0 },
|
|
913
|
+
showGrid: false
|
|
914
|
+
})
|
|
915
|
+
await settle(100)
|
|
916
|
+
const exactGap = (() => {
|
|
917
|
+
const first = exactSpacingView.project(0, 0)
|
|
918
|
+
const second = exactSpacingView.project(60, 0)
|
|
919
|
+
return Math.hypot(second[0] - first[0], second[1] - first[1])
|
|
920
|
+
})()
|
|
921
|
+
check('适应节点间距:普通适应得到的间距更大时也会缩到指定 minDistance', Math.abs(exactGap - 24) < 0.1, JSON.stringify({ scale: exactSpacingView.getScale(), gap: exactGap }))
|
|
922
|
+
exactSpacingView.updateOptions({ fitSpacing: { minDistance: 12, maxZoom: 2 } })
|
|
923
|
+
exactSpacingView.fitView()
|
|
924
|
+
await settle(40)
|
|
925
|
+
const exactSmallGap = (() => {
|
|
926
|
+
const first = exactSpacingView.project(0, 0)
|
|
927
|
+
const second = exactSpacingView.project(60, 0)
|
|
928
|
+
return Math.hypot(second[0] - first[0], second[1] - first[1])
|
|
929
|
+
})()
|
|
930
|
+
check('适应节点间距:改小 minDistance 会实际缩小比例,maxZoom 不再截断', Math.abs(exactSmallGap - 12) < 0.1, JSON.stringify({ scale: exactSpacingView.getScale(), gap: exactSmallGap }))
|
|
931
|
+
exactSpacingView.destroy()
|
|
910
932
|
spacingView.updateOptions({ fitSpacing: { minDistance: 24, maxZoom: 2 } })
|
|
911
933
|
spacingView.fitView()
|
|
912
934
|
await settle(60)
|
|
913
|
-
const
|
|
914
|
-
check('适应节点间距:maxZoom
|
|
935
|
+
const spacingMaxZoomIgnored = spacingView.getCamera().scale
|
|
936
|
+
check('适应节点间距:maxZoom 兼容保留,不截断精确的 minDistance', Math.abs(spacingMaxZoomIgnored - spacingFit) < 1e-9, `${spacingFit} → ${spacingMaxZoomIgnored}`)
|
|
915
937
|
spacingView.updateOptions({ fitSpacing: false })
|
|
916
938
|
spacingView.fitView()
|
|
917
939
|
await settle(60)
|
|
918
940
|
const spacingOff = spacingView.getCamera()
|
|
919
|
-
check('适应节点间距:关闭后恢复普通适应比例与包围盒中心', Math.abs(spacingOff.scale - plainFit) < 1e-9 && Math.abs(spacingOff.center[0] - 5000) < 0.5, `${
|
|
941
|
+
check('适应节点间距:关闭后恢复普通适应比例与包围盒中心', Math.abs(spacingOff.scale - plainFit) < 1e-9 && Math.abs(spacingOff.center[0] - 5000) < 0.5, `${spacingMaxZoomIgnored} → ${spacingOff.scale} / ${JSON.stringify(spacingOff.center)}`)
|
|
942
|
+
const selectedSpacing = fitSpacingBaseView.getPointsSpacing(['f1', { index: 3 }, 'f1'])
|
|
943
|
+
const selectedSpacingPx = 140 * spacingScale
|
|
944
|
+
check('getPointsSpacing:只算指定节点,引用去重并按当前比例返回 CSS 像素',
|
|
945
|
+
selectedSpacing.count === 2 && Math.abs(selectedSpacing.minDistance - selectedSpacingPx) < 1e-6 && Math.abs(selectedSpacing.maxDistance - selectedSpacingPx) < 1e-6 && selectedSpacing.nearest?.from.id === 'f1' && selectedSpacing.nearest?.to.id === 'f2',
|
|
946
|
+
JSON.stringify(selectedSpacing))
|
|
947
|
+
const emptySpacing = fitSpacingBaseView.getPointsSpacing([])
|
|
948
|
+
const singleSpacing = fitSpacingBaseView.getPointsSpacing('f1')
|
|
949
|
+
check('getPointsSpacing:空引用与单节点返回稳定空读数', emptySpacing.count === 0 && emptySpacing.minDistance === null && emptySpacing.maxDistance === null && singleSpacing.count === 1 && singleSpacing.minDistance === null && singleSpacing.maxDistance === null,
|
|
950
|
+
JSON.stringify({ emptySpacing, singleSpacing }))
|
|
951
|
+
const hiddenGroupSpacingView = createGisView(host(640, 320), {
|
|
952
|
+
points: [{ id: 'h0', x: 0, y: 0, size: 10 }, { id: 'h1', x: 40, y: 0, size: 10 }],
|
|
953
|
+
showGrid: false
|
|
954
|
+
})
|
|
955
|
+
await settle(100)
|
|
956
|
+
hiddenGroupSpacingView.hideFeatures('point', 'h1')
|
|
957
|
+
await settle(40)
|
|
958
|
+
const hiddenVisibleSpacing = hiddenGroupSpacingView.getPointSpacing()
|
|
959
|
+
const hiddenGroupSpacing = hiddenGroupSpacingView.getPointsSpacing(['h0', 'h1'])
|
|
960
|
+
check('getPointsSpacing:显式传入的隐藏节点仍参与统计(与 getPointSpacing 的可见集合分开)',
|
|
961
|
+
hiddenVisibleSpacing.count === 1 && hiddenGroupSpacing.count === 2 && Math.abs(hiddenGroupSpacing.minDistance - 40 * hiddenGroupSpacingView.getScale()) < 1e-6,
|
|
962
|
+
JSON.stringify({ visible: hiddenVisibleSpacing.count, group: hiddenGroupSpacing }))
|
|
963
|
+
hiddenGroupSpacingView.destroy()
|
|
920
964
|
/* maxDistance 反过来压住稀疏数据:最远节点在屏幕上不超过给定像素,避免少量场站回显时整幅放大 */
|
|
921
965
|
const sparseSpacingView = createGisView(host(640, 320), {
|
|
922
966
|
points: [{ id: 's0', x: 0, y: 0, size: 10 }, { id: 's1', x: 100, y: 0, size: 10 }, { id: 's2', x: 0, y: 80, size: 10 }],
|
|
@@ -958,6 +1002,20 @@
|
|
|
958
1002
|
Math.abs(gridCenter[0] - 5000) <= 0.5 && Math.abs(gridCenter[1] - 5000) <= 0.5,
|
|
959
1003
|
JSON.stringify(gridCenter))
|
|
960
1004
|
gridSpacingView.destroy()
|
|
1005
|
+
/* 默认 maxScale 不能提前截断 minDistance:经纬度这类小量纲数据需要的比例会超过旧缺省 20000 */
|
|
1006
|
+
const fineSpacingView = createGisView(host(640, 320), {
|
|
1007
|
+
points: [{ id: 'fine0', x: 0, y: 0, size: 10 }, { id: 'fine1', x: 0.01, y: 0, size: 10 }],
|
|
1008
|
+
fitSpacing: { minDistance: 1000 },
|
|
1009
|
+
showGrid: false
|
|
1010
|
+
})
|
|
1011
|
+
await settle(140)
|
|
1012
|
+
const fineScreen0 = fineSpacingView.project(0, 0)
|
|
1013
|
+
const fineScreen1 = fineSpacingView.project(0.01, 0)
|
|
1014
|
+
const fineGap = Math.hypot(fineScreen1[0] - fineScreen0[0], fineScreen1[1] - fineScreen0[1])
|
|
1015
|
+
check('适应节点间距:默认 maxScale 不再把 minDistance 截在旧上限,目标 1000px 能落到位',
|
|
1016
|
+
fineSpacingView.getScale() > 20000 && Math.abs(fineGap - 1000) < 0.2,
|
|
1017
|
+
JSON.stringify({ scale: fineSpacingView.getScale(), gap: fineGap }))
|
|
1018
|
+
fineSpacingView.destroy()
|
|
961
1019
|
fitSpacingBaseView.destroy()
|
|
962
1020
|
spacingView.destroy()
|
|
963
1021
|
}
|