gis-view-webgl2 0.1.3 → 0.1.4

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 (34) hide show
  1. package/README.md +12 -9
  2. package/dist/index.cjs +1 -1
  3. package/dist/index.d.ts +112 -17
  4. package/dist/index.js +1 -1
  5. package/docs/AI/345/257/271/350/257/235/346/261/207/346/200/273.md +34 -0
  6. package/docs/GisView.md +3 -3
  7. 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 +30 -0
  8. package/docs/api/index.html +22 -8
  9. package/docs/demos/ApiCoverageDemo.vue +1 -1
  10. package/docs/demos/ExtensionDemo.vue +69 -12
  11. package/docs/demos/SearchFilterDemo.vue +13 -11
  12. package/docs/demos/api-index.js +13 -9
  13. package/docs/demos/demos.js +1 -1
  14. package/docs/demos/gis-view-demo/GisViewDemoCardHeader.vue +1 -0
  15. package/docs/demos/gis-view-demo/panels/OpsPanels.vue +2 -2
  16. package/docs/demos/gis-view-demo/useGisViewDemo.js +9 -9
  17. package/docs/index.html +41 -4
  18. package/docs/manual/03-/351/205/215/347/275/256/351/241/271.md +18 -3
  19. package/docs/manual/04-/344/272/213/344/273/266/344/270/216/345/233/236/350/260/203.md +7 -6
  20. package/docs/manual/08-/351/200/211/344/270/255/344/270/216/346/213/220/347/202/271.md +24 -3
  21. package/docs/manual/14-/350/247/206/345/233/276/344/270/216/347/255/233/351/200/211.md +90 -34
  22. package/docs/manual/15-/345/257/274/345/207/272/344/270/216/351/233/206/346/210/220.md +32 -3
  23. package/docs/manual/16-/345/256/236/344/276/213/346/226/271/346/263/225/346/200/273/350/241/250.md +7 -4
  24. package/docs/manual/17-/345/217/202/346/225/260/351/200/237/346/237/245.md +9 -6
  25. package/docs/manual/18-/351/200/220/346/226/271/346/263/225/347/244/272/344/276/213.md +39 -7
  26. 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 +3 -3
  27. 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 +6 -3
  28. package/docs/steps/README.md +7 -7
  29. package/docs/steps/selftest-demos.html +26 -9
  30. package/docs/steps/selftest-docs.html +33 -4
  31. package/docs/steps/selftest-features.html +91 -10
  32. package/docs/steps/selftest-scale.html +98 -0
  33. package/docs/steps/selftest-select.html +90 -0
  34. package/package.json +1 -1
@@ -4,13 +4,13 @@
4
4
 
5
5
  ## 筛选
6
6
 
7
- `setFilter` 按属性过滤点与线,点被过滤掉时绑在它身上的线路按 `cascade` 联动消失;这个默认联动还受 `lineFollowsNode` 影响(见下):
7
+ `setFilter` 按属性过滤点与线,点被过滤掉时绑在它身上的线路按 `cascade` 联动消失;这个默认联动还受 `lineFollowsNode` 影响(见下)。`mode` 决定不命中的要素怎么处理:缺省 `'hide'` 只改变**显示集合**,原始数据与选中集都不动,被隐藏的选中项仍留在 `getSelections()` 里,条件放宽 / 清空后原样恢复高亮;`'remove'` 则把筛选结果直接从数据里删掉,走一条 `dataChange('filterRemove')`,开了历史时可以撤销。
8
8
 
9
9
  ```js
10
- // 只看 525kV 以上的节点:两端节点没了的线路(以及通道回线)一起隐藏
10
+ // 默认 hide:只看 525kV 以上的节点,两端节点没了的线路(以及通道回线)一起隐藏
11
11
  view.setFilter({ points: { vn_kv: { gte: 525 } } })
12
12
 
13
- // 只筛节点时可以写简写:与上一行完全等价(顶层出现 points / lines / cascade / worker 任一键才按完整格式解释)
13
+ // 只筛节点时可以写简写:与上一行完全等价(顶层出现 points / lines / cascade / mode / worker 任一键才按完整格式解释)
14
14
  view.setFilter({ vn_kv: { gte: 525 } })
15
15
 
16
16
  // 点条件 + 线条件 + 联动方式
@@ -20,6 +20,9 @@ view.setFilter({
20
20
  cascade: 'binding'
21
21
  })
22
22
 
23
+ // remove:不命中的点 / 线从数据里删掉(先删线路、再删节点,只发一条 filterRemove)
24
+ view.setFilter({ points: { station_type: 'coal' }, mode: 'remove' })
25
+
23
26
  // 也可以创建时给初始条件
24
27
  createGisView(container, {
25
28
  points,
@@ -48,12 +51,50 @@ normalizeFilter({ points: {}, cascade: 'both', nope: 1 }) // unknownKeys: ['nop
48
51
  | `points` | `GisViewConditionInput<GisViewPoint>` | 无 | 点条件,缺省表示不过滤点 |
49
52
  | `lines` | `GisViewConditionInput<GisViewLine>` | 无 | 线条件,缺省表示不过滤线 |
50
53
  | `cascade` | `false \| 'binding' \| 'coordinate' \| 'both'` | `lineFollowsNode` 开启时 `'binding'`,关闭时 `false` | 点被过滤后哪些线路一起消失:按 `from` / `to` 绑定、按端点坐标(端点正好落在被过滤的节点上)、两者都算、或完全不联动;显式写出的值始终优先 |
54
+ | `mode` | `'hide' \| 'remove'` | `'hide'` | 不命中的要素怎么处理:`hide` 只隐藏;`remove` 从数据里删除 `hiddenPoints` / `hiddenLines`,只发一条 `dataChange('filterRemove')` |
51
55
  | `worker` | `boolean` | 跟随 `filterWorker` | 这一次筛选是否允许放进 Worker |
52
56
 
53
- 联动只影响表现,数据本身不动:`updateData` / `updateOptions` 换了数据之后按同一个条件重算。通道回线也是普通线路,跟着同一个 `cascade` 走:`binding` / `coordinate` / `both` 时绑着被过滤节点的通道回线一起收起来,`false` 时照旧画 —— 被过滤的节点仍然当锚点用(几何不跟着筛选断,回线端点还钉在它原来的坐标上),只是节点自己不显示。`cascade: false` 时仍显示的通道宽度也按被过滤节点原本的尺寸链计算,不会因为节点不画了就改按另一端尺寸或退回默认 `channelWidth`。只有节点真的不在数据里(删掉、或者写了 `visible: false`)时,两端锚不住的通道与它的回线才整条不显示(和「两端没有节点就不显示」是同一条规则)。
57
+ `hide` 模式下的联动只影响表现,数据本身不动:`updateData` / `updateOptions` 换了数据之后按同一个条件重算。通道回线也是普通线路,跟着同一个 `cascade` 走:`binding` / `coordinate` / `both` 时绑着被过滤节点的通道回线一起收起来,`false` 时照旧画 —— 被过滤的节点仍然当锚点用(几何不跟着筛选断,回线端点还钉在它原来的坐标上),只是节点自己不显示。`cascade: false` 时仍显示的通道宽度也按被过滤节点原本的尺寸链计算,不会因为节点不画了就改按另一端尺寸或退回默认 `channelWidth`。只有节点真的不在数据里(删掉、或者写了 `visible: false`)时,两端锚不住的通道与它的回线才整条不显示(和「两端没有节点就不显示」是同一条规则)。
54
58
 
55
59
  `lineFollowsNode: false` 把「节点退场」与「线路 / 通道退场」拆开:筛选未显式写 `cascade` 时默认不再级联,隐藏、删除节点也只动节点;线路与通道继续停在最后一次解析出的坐标、骨架与宽度。条件里显式写了 `cascade` 时仍按显式值级联。筛选条件放宽或清空、被筛掉的回线重新出现后,引擎会在线路线宽解析完成后重新排一次通道偏移,通道与回线按当前宽度回到正确车道,不会保留筛选期间的旧间距。拖拽节点松手后,按端点坐标联动的筛选会按新坐标重算一次。需要「把某个要素找出来并居中」而不是隐藏其它要素时用 `search`,见「搜索与定位」。
56
60
 
61
+ ### 删除型筛选(mode: 'remove')
62
+
63
+ `mode: 'remove'` 把同一套判定算出的 `hiddenPoints` / `hiddenLines` 从数据里删掉,而不是只交给引擎隐藏。删除按「先线路、再节点」执行(先删点会让线路下标挪位),因此筛选结果里没有 id 的线路也能按删除前的下标删掉。`cascade` 仍决定哪些线路算进 `hiddenLines`,但删除集合就是筛选结果本身,不会在删节点时再按 `lineFollowsNode` 额外扩一圈。
64
+
65
+ ```js
66
+ createGisView(container, {
67
+ points, lines, channels,
68
+ filter: { points: { station_type: 'coal' }, mode: 'remove' },
69
+ onDataChange: payload => {
70
+ if (payload.type !== 'filterRemove') return
71
+ console.log(payload.pointIndexes, payload.removedPoints)
72
+ console.log(payload.removedLineIndexes, payload.removedLineItems)
73
+ }
74
+ })
75
+
76
+ view.setFilter({ lines: { vn_kv: { lt: 220 } }, mode: 'remove' })
77
+ view.getFilter() // { lines: { … }, mode: 'remove' }
78
+ view.getFilterResult() // 删除完成后 hiddenPoints / hiddenLines 都是空数组
79
+ ```
80
+
81
+ 一次删除只发一条 `dataChange('filterRemove')`:删除期间中间的 `removePoints` / `removeLines` 数据变更与 `onFilter` 都被抑制,最后按删完的数据重算一次筛选并正常派发一条 `onFilter`。载荷里与删除项相关的字段:
82
+
83
+ | 字段 | 说明 |
84
+ | --- | --- |
85
+ | `pointIndexes` | 删掉的节点下标(删除前它们在 `points` 里的位置,升序) |
86
+ | `removedPoints` | 删掉的节点数据,顺序与 `pointIndexes` 对应 |
87
+ | `lineIndexes` / `removedLineIndexes` | 删掉的线路下标(删除前它们在 `lines` 里的位置,升序,二者同一份) |
88
+ | `removedLineItems` | 删掉的线路数据,顺序与 `removedLineIndexes` 对应 |
89
+ | `lineIds` | 删掉的线路 id(没有 id 的线路不会出现在这里) |
90
+ | `count` / `removedLines` / `removedChannels` | 删掉的节点数 / 线路数 / 整条消失的通道声明数 |
91
+
92
+ 载荷里的 `points` / `lines` / `channels` 与其它数据变更一样是**删除之后的最新数据**,不是删除项;删除项看 `removedPoints` / `removedLineItems`。被删掉的要素会从选中集里移除,没被删的选中项保持原样。
93
+
94
+ 开了 `history` 时整次删除只落一步 `filterRemove`。撤销会恢复数据,但当前 `remove` 条件仍保留,历史回放本身不会再触发删除(恢复出来的不命中项会先按筛选结果隐藏);重做恢复删除结果,也不会补发第二条 `filterRemove`。历史关着时删除不可撤消。
95
+
96
+ 初始筛选在同步路径下(`filterWorker: false`、数据量低于阈值,或条件里带函数)会在 `ready` 之前完成删除;如果初始筛选走了 Worker,结果异步回来,删除与 `onFilter` 会发生在 `ready` 之后,结果回来之前先按隐藏集合画首帧。运行时 `setFilter({ … mode: 'remove' })` 同理。
97
+
57
98
  ### 大数据下的 Worker 筛选
58
99
 
59
100
  数据量大时把判定放进 Web Worker,主线程只负责应用隐藏下标:
@@ -72,28 +113,39 @@ Worker 按投影版本缓存字段投影,条件变化时只传条件本身;
72
113
 
73
114
  ## 搜索与定位
74
115
 
75
- 搜索和筛选是两件事:**筛选**(`setFilter`)决定哪些要素显示,会隐藏数据;**搜索**(`search`)只把要素找出来,不隐藏任何东西,**也不动视角**:命中连 `bounds` 一起给回来,要不要聚焦、聚焦谁由调用方自己调 `focusOn`(组件只给结果,不再替使用方决定相机):
116
+ 搜索和筛选是两件事:**筛选**(`setFilter`)默认只隐藏显示,不删数据、也不改选中集,显式写 `mode: 'remove'` 才会删;**搜索**(`search`)只把要素找出来,不隐藏任何东西,**也不动视角**:命中连 `bounds` 一起给回来,要不要聚焦、聚焦谁由调用方自己调 `focusOn`(组件只给结果,不再替使用方决定相机):
76
117
 
77
118
  ```js
78
119
  // 关键字可以是字符串,也可以是配置对象
79
120
  const result = view.search('望京')
80
- view.search({ keyword: '525 变电站', scope: 'channels', limit: 50 })
121
+ view.search({ keyword: '525 变电站', scope: ['channels', 'groups'], limit: 50 })
122
+ view.search({ keyword: 'channel-a', scope: 'group' }) // 单数别名与 focusOn 的目标类型一致
81
123
 
82
124
  result.total // 命中总数
83
- result.points / result.lines / result.channels // 三类各自的命中
84
- result.hits // 三类合集,顺序是节点、线路、通道
125
+ result.points / result.lines / result.channels / result.groups // 四类各自的命中
126
+ result.hits // 四类合集,顺序是节点、线路、通道、分组
85
127
  result.truncated // 命中数达到 limit 被截断
86
128
  result.durationMs
87
129
 
88
130
  // 一条命中带命中的字段、值与范围,用来回写界面或自己定位(这份最小场景里搜不到时 hit 是 undefined)
89
131
  const hit = result.hits[0]
90
- hit?.kind // 'point' | 'line' | 'channel'
91
- hit?.index // 点 / 线在数据里的下标,通道是通道下标
92
- hit?.id / hit?.item / hit?.bounds // 范围可以直接交给 zoomToBounds
132
+ hit?.kind // 'point' | 'line' | 'channel' | 'group'
133
+ hit?.index // 点 / 线在数据里的下标,通道是通道下标,分组是 groups 结果里的下标
134
+ hit?.id / hit?.item / hit?.bounds // 范围可以直接交给 zoomToBounds / focusOn
93
135
  hit?.field / hit?.value // 命中的是哪个字段、字段值是什么(如 'props.vn_kv')
136
+ hit?.channelIndex / hit?.memberIndex // 线路 / 通道命中的所属通道与第几回(通道命中自身字段时没有 memberIndex)
137
+ hit?.memberLineIndex / hit?.memberLineId // 通道因某条成员回线命中时,那条回线在 lines 里的下标与 id
138
+ hit?.groupId / hit?.groupIndex // 分组命中:分组 id 与它在所属通道里的序号(从 0 起)
139
+
140
+ // 分组命中可以直接交给 focusOn / selectFeatures,与手写 { type: 'group', channelIndex, groupId } 等价
141
+ const grouped = view.search({ keyword: 'group-2', scope: 'group' }).groups[0]
142
+ view.focusOn(grouped) // 只把中心移到这一组,比例不动
143
+ view.focusOn(grouped, { zoom: true }) // 显式按这一组的真实 bounds 适配
144
+ view.selectFeatures(grouped) // 选中这一组的全部成员回线
94
145
 
95
146
  // focusOn:把视角移到目标上 —— 单条、一批、只居中都由它一条搞定
96
- view.focusOn(hit) // 一条命中:按它的范围适配视图
147
+ view.focusOn(hit) // 一条命中:缺省只居中,比例一点不动
148
+ view.focusOn(hit, { zoom: true }) // 显式按命中范围适配视图
97
149
  view.focusOn('station-1024', { scale: 12 }) // 直接给比例
98
150
  view.focusOn({ type: 'point', id: 'station-1024' }) // 节点 / 线路同 id 时显式消歧
99
151
  view.focusOn({ type: 'line', id: 'station-1024' })
@@ -102,10 +154,10 @@ view.focusOn({ type: 'channel', channelId: 'channel-a' }) // 通道也可以直
102
154
  view.focusOn({ type: 'group', channelId: 'channel-a', groupId: 'group-2' })
103
155
  view.focusOn({ type: 'group', groupId: 'group-2' }) // groupId 全局唯一时可省略通道
104
156
  view.focusOn([116.4, 39.9]) // 一个坐标点:只居中,比例不动
105
- view.focusOn(['station-1', 'station-2', 'station-3']) // 一批:取整体包围盒 —— 这三个点的中心当屏幕中心
106
- view.focusOn(['station-1', 'station-2'], { select: true }) // 顺带把它们选上(与接口选中同一份 state)
107
- view.focusOn('station-1', { maxZoom: 8 }) // 单个节点这种零面积目标也能放大到 8 倍
108
- view.focusOn({ minX: 116, minY: 39, maxX: 117, maxY: 40 }, { padding: 60 })
157
+ view.focusOn(['station-1', 'station-2', 'station-3']) // 一批:整体中心当屏幕中心,比例不动
158
+ view.focusOn(['station-1', 'station-2'], { select: true, zoom: true }) // 适配范围并顺手选上
159
+ view.focusOn('station-1', { zoom: true, maxZoom: 8 }) // 零面积目标也能放大到 8 倍
160
+ view.focusOn({ minX: 116, minY: 39, maxX: 117, maxY: 40 }, { zoom: true, padding: 60 })
109
161
 
110
162
  // 只挪中心、比例一点都不动:focusOn(target, { center: true })
111
163
  view.focusOn([116.4, 39.9], { center: true }) // 世界坐标(数组 / { x, y } 都认)
@@ -115,36 +167,39 @@ view.focusOn({ lngLat: [116.4, 39.9] }, { center: true }) // 配了 GeoJSON 底
115
167
  view.focusOn({ minX: 116, minY: 39, maxX: 117, maxY: 40 }, { center: true }) // 一段范围:取中心
116
168
  ```
117
169
 
118
- 字符串 / 数字仍然兼容旧写法:先按节点 id 找,再按线路 id 找;同一个 id 同时出现在节点与线路里时用 `{ type: 'point' | 'line', id }` 消歧。裸字符串不表示通道;整条通道可以写 `{ channelId }`,要和通用 `id` / `index` 写法区分时再用 `{ type: 'channel', ... }`。分组必须写 `{ type: 'group', channelId?, groupId }`,`groupId` 在全部通道里唯一时可以省略通道。节点、线路、通道与分组都先解析成世界坐标范围再交给同一条 `zoomToBounds` 路径,聚焦比例口径一致,没有为某类目标额外乘放大倍率。
170
+ 字符串 / 数字仍然兼容旧写法:先按节点 id 找,再按线路 id 找;同一个 id 同时出现在节点与线路里时用 `{ type: 'point' | 'line', id }` 消歧。裸字符串不表示通道;整条通道可以写 `{ channelId }`,要和通用 `id` / `index` 写法区分时再用 `{ type: 'channel', ... }`。分组必须写 `{ type: 'group', channelId?, groupId }`,`groupId` 在全部通道里唯一时可以省略通道。节点、线路、通道与分组都先解析成世界坐标范围;**缺省只按这个范围的中心调 `centerOn`,比例一点不动**,要适配就显式写 `zoom: true`,这时才交给 `zoomToBounds` 按解析出的真实 `bounds` 计算,没有为某类目标额外乘放大倍率。`scale` 的优先级高于 `zoom`;`maxZoom` 只给单点 / 单线这类零面积目标补最小范围。
119
171
 
120
172
  | `focusOn` 的选项 | 说明 |
121
173
  | --- | --- |
122
- | `scale` | 直接设成这个比例,不再按范围适配 |
123
- | `center: true` | 只把中心挪过去、比例一点不动(原来 `centerOn` 的能力并到了这里) |
124
- | `padding` | 按范围适配时四周留白(CSS 像素,缺省 40) |
125
- | `maxZoom` | 适配时的最大比例:单个节点 / 一条直线没有面积,靠它撑出一个最小范围 |
174
+ | `zoom: true` | 显式要求按目标范围适配视图;不写就只居中、比例不动 |
175
+ | `scale` | 直接设成这个比例,不再按范围适配(优先级高于 `zoom`);线路 / 通道 / 分组不会在此基础上再自动放大 |
176
+ | `center: true` | 显式只把中心挪过去、比例一点不动(与缺省行为一致,保留旧写法) |
177
+ | `padding` | 按范围适配(`zoom: true`)时四周留白(CSS 像素,缺省 40) |
178
+ | `maxZoom` | 适配(`zoom: true`)时的最大比例:单个节点 / 一条直线没有面积,靠它撑出一个最小范围 |
126
179
  | `select: true`(或另给一批写法) | 把这些被聚焦的要素同时变成选中状态(与接口选中同一份 state) |
127
180
  | `mode` | 配合 `select`:这一批怎么并进当前选中集(`replace` 缺省 / `add` / `toggle` / `remove`) |
128
181
 
129
182
  | 字段 | 类型 | 默认值 | 说明 |
130
183
  | --- | --- | --- | --- |
131
184
  | `keyword` | `string` | 无 | 关键字:空格分隔多个词时要在同一个字段里全部出现才命中,不区分大小写 |
132
- | `scope` | `'points' \| 'lines' \| 'channels'` 或它们的数组 | 三类都搜 | 搜索范围 |
185
+ | `scope` | `'points' \| 'lines' \| 'channels' \| 'groups'` 或它们的数组;也认单数别名 `'point'` / `'line'` / `'channel'` / `'group'` | 四类都搜 | 搜索范围 |
133
186
  | `fields` | `string[]` | 要素的全部字段 | 限定参与匹配的字段路径,如 `['name', 'props.vn_kv']` |
134
187
  | `limit` | `number` | `200` | 每类的命中上限,`0` 表示不限 |
135
188
 
136
189
  补充口径:
137
190
 
138
- - `limit` 按「节点 / 线路 / 通道」三类分别计数;省略或写成非数字 / `NaN` 时回落到默认 `200`,不会退化成不限。`0` 与负数都表示不限。
191
+ - `limit` 按「节点 / 线路 / 通道 / 分组」四类分别计数;省略或写成非数字 / `NaN` 时回落到默认 `200`,不会退化成不限。`0` 与负数都表示不限。
139
192
  - `fields` 会忽略空项并去重;数组为空或没有有效字段路径时,与不写 `fields` 一样按全部字段处理。
140
193
  - 默认全字段模式会跳过 `symbol` / `style` / `tooltip` 这类外观结构;但 `fields` 显式点名时按路径直接读取,不受这条跳过规则限制。例如 `fields: ['symbol']`、`fields: ['style.color']` 能命中对应值。
141
- - 搜索始终基于全量数据,不受当前 `setFilter` 影响:被筛选隐藏的要素仍然可以被搜到。`scope` 写错或传空数组时会回落到三类全搜;重复线路 id 按数据里的第一条解析(见 [ID 解析口径](02-坐标与数据.md#id-解析口径))。
194
+ - 搜索基于当前数据:`hide` 模式下被隐藏的要素仍在数据里,因此仍然可以被搜到;`remove` 模式下已经删掉的要素搜不到。`scope` 写错或传空数组时会回落到四类全搜;重复线路 id 按数据里的第一条解析(见 [ID 解析口径](02-坐标与数据.md#id-解析口径))。
195
+
196
+ 搜索**不动视角**:命中里带着 `bounds`,要定位就自己调 `focusOn(result.hits, …)` —— 缺省只把一批命中的整体中心当屏幕中心;要按范围适配就写 `{ zoom: true }`,`{ select: true }` 还能顺带把它们选上。`zoom` / `scale` / `padding` / `maxZoom` 属于 `focusOn` 的选项,搜索本身没有这些参数。
142
197
 
143
- 搜索**不动视角**(`focus` / `scale` / `padding` 三个选项随之取消):命中里带着 `bounds`,要定位就自己调 `focusOn(result.hits, …)` —— 一批命中的整体中心就是屏幕中心,`{ select: true }` 还能顺带把它们选上。
198
+ 匹配的是要素自己的字段:`id`、`name`、`from` / `to`,以及任意业务字段——`props` 这类普通对象会再往里摊一层,所以 `props.vn_kv` 里的值也能搜到;样式与符号字段不参与匹配。**通道连成员回线一起搜**:命中的是通道里的某条回线时,命中里会带上 `channelIndex`、`memberIndex`(第几回,0 开始)与 `memberLineIndex` / `memberLineId`,`bounds` 也换成那条回线的范围,`focusOn(hit)` 会把中心移到通道里的这条线路上,`focusOn(hit, { zoom: true })` 才按它的范围适配;反过来搜线路时,属于通道的线路也会带上 `channelIndex` / `memberIndex`。
144
199
 
145
- 匹配的是要素自己的字段:`id`、`name`、`from` / `to`,以及任意业务字段——`props` 这类普通对象会再往里摊一层,所以 `props.vn_kv` 里的值也能搜到;样式与符号字段不参与匹配。**通道连成员回线一起搜**:命中的是通道里的某条回线时,命中里会带上 `channelIndex`、`memberIndex`(第几回,0 开始)与 `memberLineIndex` / `memberLineId`,`bounds` 也换成那条回线的范围,`focusOn(hit)` 因此会直接定位到通道里的这条线路;反过来搜线路时,属于通道的线路也会带上 `channelIndex` / `memberIndex`。
200
+ **分组按分组自身字段匹配**(`id` / `channelId` / `channelIndex` / `groupIndex` / `lineCount` / `anchored` / `edited` 等)。命中项的 `item` 是轻量快照(含成员下标 `lineIndexes` 与成员 id `lineIds`,不含骨架 / 回线几何),同时带上 `channelIndex`、`groupId`、`groupIndex`;`bounds` 由这一组的骨架与成员线算出,所以 `focusOn(hit, { zoom: true })` 会按整组范围适配,`selectFeatures(hit)` 会选中组内全部回线。分组搜索结果在 `result.groups`,也包含在 `result.hits` 里。
146
201
 
147
- 搜索是同步、只读的:不改数据、不改筛选、不产生额外的渲染状态,按关键字顺序返回。要「隐藏不符合条件的要素」用 `setFilter`;要「找出来」用 `search`,要「居中 / 放大过去」自己调 `focusOn`。
202
+ 搜索是同步、只读的:不改数据、不改筛选、不产生额外的渲染状态,按关键字顺序返回。要「只隐藏不符合条件的要素」用默认的 `setFilter`(`mode: 'hide'`),要「从数据里删掉」显式写 `mode: 'remove'`;要「找出来」用 `search`,要「居中 / 放大过去」自己调 `focusOn`。
148
203
 
149
204
  ### 异步搜索:`searchAsync`
150
205
 
@@ -153,13 +208,14 @@ view.focusOn({ minX: 116, minY: 39, maxX: 117, maxY: 40 }, { center: true }) //
153
208
  ```js
154
209
  // 每次调用都会 resolve 自己那一次的命中;worker 说明这一份是在哪算的
155
210
  const result = await view.searchAsync('望京')
156
- view.searchAsync({ keyword: '525 变电站', scope: 'channels' })
211
+ view.searchAsync({ keyword: '525 变电站', scope: ['channels', 'groups'] })
212
+ view.searchAsync({ keyword: 'channel-a', scope: 'group' })
157
213
 
158
214
  // 连着敲字:先回来的那几份带着 stale,界面上该丢掉(视角也不会跟着它动)
159
215
  let latest = 0
160
216
  const search = async keyword => {
161
217
  const token = ++latest
162
- const result = await view.searchAsync({ keyword, focus: false })
218
+ const result = await view.searchAsync({ keyword })
163
219
  if (result.stale || token !== latest) return
164
220
  render(result)
165
221
  }
@@ -172,7 +228,7 @@ const search = async keyword => {
172
228
  | `worker` | `boolean` | 这一份是在 Worker 里算的(`false` 表示这条路上退回了同步计算) |
173
229
  | `stale` | `boolean` | 结果回来时数据已经改过、或者又发过一次搜索:下标与内容都可能不作数了 |
174
230
 
175
- 同步的 `search` 与 `searchAsync` 在**同一个数据版本 + 同一个请求**上结果逐字段一致(命中、命中的字段与值、`item` 引用、`bounds`)。环境不支持 Worker、数据量不到阈值或 Worker 报错时,`searchAsync` 会退回同步计算再 resolve,调用方不用写两条路。
231
+ 同步的 `search` 与 `searchAsync` 在**同一个数据版本 + 同一个请求**上结果逐字段一致(四类命中、命中的字段与值、`item` 引用、`bounds`);分组范围的当前轻量快照会随请求交给 Worker,分组字段或分组 id 改过之后也不会拿旧快照匹配。环境不支持 Worker、数据量不到阈值或 Worker 报错时,`searchAsync` 会退回同步计算再 resolve,调用方不用写两条路。
176
232
 
177
233
  ## 初始视图
178
234
 
@@ -209,9 +265,9 @@ createGisView(container, {
209
265
  「本来铺得好好的,新加一个远处的节点,整幅视图缩了一圈、两点之间的可视距离变短」——根因是**自动适应读的是数据包围盒**:新节点把包围盒撑大,`fitView()` 按更大的范围重算 `scale`,比例一变小,节点之间的屏幕距离就跟着短(节点自己的屏幕尺寸由 `pointSizeScale` 那一对系数决定,没跟着缩,看着就更挤)。先想清楚「视野由谁说了算」,再按场景选:
210
266
 
211
267
  1. **视野只由业务范围决定(推荐)**:配 `initialView: { bounds, padding }`(或 `mode: 'center'` + `scale`)把首屏钉在一块业务范围上 —— `bounds` 不跟着数据长,新节点进来包围盒变大也不影响相机;数据分布本来就广时,业务范围与数据边界分开正是想要的。
212
- 2. **新节点不进自动适应**:`fitView()` 只手动调(首屏一次、切数据集一次),`addPoint` / `addPoints` 追加之后组件不会自动适应;要展示新节点就 `focusOn(id)` 或高亮 / `setDropPreview`,别整幅重适应。
268
+ 2. **新节点不进自动适应**:`fitView()` 只手动调(首屏一次、切数据集一次),`addPoint` / `addPoints` 追加之后组件不会自动适应;要展示新节点就 `focusOn(id, { zoom: true })` 或高亮 / `setDropPreview`,别整幅重适应。
213
269
  3. **只平移不改比例**:`setCamera({ center })` 或 `focusOn(id, { center: true })` —— 中心挪过去、`scale` 一点不动,两点之间的屏幕距离就不变。
214
- 4. **按「节点间距」而不是「包围盒」算适应**:`focusOn([...几个代表性要素])` / `zoomToBounds(业务范围)`;也可以给 `minScale` 一个下界,缩到这条线就停住。
270
+ 4. **按「节点间距」而不是「包围盒」算适应**:`focusOn([...几个代表性要素], { zoom: true })` / `zoomToBounds(业务范围)`;也可以给 `minScale` 一个下界,缩到这条线就停住。
215
271
  5. **想让节点本身不跟着缩**:`pointZoomScale` / `lineZoomScale` 调小(`0` = 完全不跟随地图缩放),尺寸主要由 `pointSizeScale` / `lineWidthScale` 的常数倍数决定 —— 比例被带跑时至少节点不会一起变小。
216
272
 
217
273
  6. **给节点最小间距兜底(先试它)**:打开 `fitSpacing` —— 适应视图时按可见节点最近中心距反推比例,远处孤立点撑大包围盒也不会再把主体压成一团;孤立点允许落到视口外,必要时用 `maxZoom` 限制放大倍数,见「适应视图的节点间距(fitSpacing)」。
@@ -257,7 +313,7 @@ view.updateOptions({ fitSpacing: false }) // 关掉,下一次 fitView 恢复
257
313
 
258
314
  几个约定:
259
315
 
260
- - **默认关**,打开后只影响「默认 fit 模式下的首次适应」与手动 `fitView()`;`zoomToBounds()` / `focusOn()` 仍按指定范围算,不会强行套节点间距。
316
+ - **默认关**,打开后只影响「默认 fit 模式下的首次适应」与手动 `fitView()`;`zoomToBounds()` / `focusOn(..., { zoom: true })` 仍按指定范围算,不会强行套节点间距。
261
317
  - 只统计**筛选 / 隐藏之后仍然可见**的节点。完全重合的坐标不算有效间距(否则两个重叠点会让整套兜底失效);少于两个不同位置时不调整。
262
318
  - **只放大、不跳视点**:`fitSpacing` 不重算中心,也不挑一对节点居中。需要改变中心时用 `initialView.center` / `initialView.bounds`、`focusOn()` 或 `setCamera({ center })`。
263
319
  - `minDistance` 是节点**中心之间**的屏幕距离(CSS 像素),默认 `24`。想给节点本身和描边留更多空隙就调大。
@@ -482,7 +538,7 @@ createGisView(container, {
482
538
  | `addChannel` 顶层 `channelId` / `groupId` / `groupIndex` ↔ `resolution: { type: 'joinGroup', … }` | 同一件事的两种写法(与 `updateLine` 的分组指令同款):并进已有通道时指定「哪条通道、哪一组」;只给 `channelId` 按这条通道自己的推荐顺序挑(新分组 → 第一个能并的分组)。结果里的 `channelLineIds` 是更新后的完整成员表,见「新增通道」 |
483
539
  | `project(x, y)` / `unproject(x, y)` ↔ `clientToScreen(clientX, clientY)` / `screenToClient(x, y)` ↔ `clientToWorld(clientX, clientY)` / `worldToClient(x, y)` | 同一套换算链的三档入口:`project` / `unproject` 收**画布 CSS 像素**,`clientTo*` 这一对吃鼠标事件里的 `clientX` / `clientY`(自动减 canvas rect、除宿主缩放)只换到画布像素,再接一层就是世界坐标(`clientToWorld` / `worldToClient`);经纬度那一对是 `lngLatToScreen` / `screenToLngLat` |
484
540
  | `fitView(padding?)` / `applyInitialView()` / `resetView()` / `zoomToBounds(bounds, padding?)` | 视角的四个语义:适应**数据范围**(并记下首次落位)/ 按 `options.initialView` 落位(可反复调用)/ 回到**首次落位那一刻**的相机 / 按给定的世界坐标范围适应 |
485
- | `focusOn(ref, options?)` ↔ `zoomToBounds(bounds, padding?)` | `focusOn` 认要素(id / 下标 / 搜索命中 / 坐标 / 矩形 / 一批),内部算出范围再走适应那一条,另带 `center` / `scale` / `maxZoom` / `select` 这些选项 |
541
+ | `focusOn(ref, options?)` ↔ `zoomToBounds(bounds, padding?)` | `focusOn` 认要素(id / 下标 / 搜索命中 / 坐标 / 矩形 / 一批),缺省只按范围中心居中;写 `{ zoom: true }` 才走适应那一条,另带 `center` / `scale` / `maxZoom` / `select` 这些选项 |
486
542
  | `pick(x, y)` ↔ `pickAt(clientX, clientY)` | 坐标口径不同:`pick` 收画布 CSS 像素,`pickAt` 直接吃鼠标事件里的客户端坐标(结果里另加 `target` 与 `lngLat`) |
487
543
  | `hoverAt(clientX, clientY)` / `clearHover()` / `getHover()` | 三个动作:程序化悬停 / 撤掉并补发 `onHover(null)` / 读当前那一份(`hoverAt` 的返回与 `onHover` 载荷同一份) |
488
544
  | `getData()` / `getVisibleData()` / `getOptions()` | 读的层不同:全量数据副本(含拖拽后的最新坐标)/ 筛掉隐藏之后的点与线 / 整份配置与数据 |
@@ -40,7 +40,7 @@ await view.exportImage({ type: 'svg', background: 'none', download: '线路图.s
40
40
 
41
41
  导出用的是当前这一帧的实际画面:位图内部先同步重画一帧再取像素 —— 透明底与换底色这两种写法出的都是「只给导出用」的那一帧,读完像素立刻补画一帧正常画面的把画布还原,屏幕上不会停在导出那一版。整个过程不需要开 `preserveDrawingBuffer`,所以也没有常驻的额外显存开销。
42
42
 
43
- `type: 'svg'` 走的是矢量:线(含虚线图案与相位)、点(内置圆 / 方与 SVG 符号)、线路装饰、拐点、GeoJSON 区域、扩展图形与标签都是真正的 SVG 元素,颜色 / 线宽 / 线型取的就是当前那一帧解析出来的值(悬停、选中样式同样在内),放大到任意倍都不糊。PNG / JPEG / WebP 这一支先画引擎画布,再按「扩展图形 → 标签」叠上去,所以 `graphics` 里的 SVG 路径、图片与 Canvas 都在合成结果里。两点差异:热力密度场是烘焙出来的贴图、不进 SVG;位图符号(`symbol.type: 'image'`)、自定义图元(`registerSymbol` 的画笔)与图片 / Canvas 图形都以 `<image href>` 出现(前者引用原地址,后两者是原图或 data URL),把导出的 svg 当图片内嵌到别的页面时,跨域图片与外部字体会被浏览器挡掉(直接打开 svg 文件没问题)。需要那一份矢量数据自己拼别的格式时,用 `view.snapshotScene({ background, labels })`(它已经带上 `graphics`),或用导出的 `buildSceneSvg(scene, { scale })`;`svgDataUrl(markup)` 是把一段 SVG 标记转成 `data:` URL 的小工具(组件内部导 svg 图片时走它,自己拿到标记要拼进 `<img src>` 或 CSS 时也可以直接用)。
43
+ `type: 'svg'` 走的是矢量:线(含虚线图案与相位)、点(内置圆 / 方与 SVG 符号)、线路装饰、拐点、GeoJSON 区域、扩展图形与标签都是真正的 SVG 元素,颜色 / 线宽 / 线型取的就是当前那一帧解析出来的值(悬停、选中样式同样在内),放大到任意倍都不糊。SVG 符号里的 `currentStrokeColor` 取该节点当前解析出的描边色(逐点 / 规则 / 状态样式都算),不会再回落到白色;`currentColor` 仍取填充色。PNG / JPEG / WebP 这一支先画引擎画布,再按「扩展图形 → 标签」叠上去,所以 `graphics` 里的 SVG 路径、图片与 Canvas 都在合成结果里。两点差异:热力密度场是烘焙出来的贴图、不进 SVG;位图符号(`symbol.type: 'image'`)、自定义图元(`registerSymbol` 的画笔)与图片 / Canvas 图形都以 `<image href>` 出现(前者引用原地址,后两者是原图或 data URL),把导出的 svg 当图片内嵌到别的页面时,跨域图片与外部字体会被浏览器挡掉(直接打开 svg 文件没问题)。需要那一份矢量数据自己拼别的格式时,用 `view.snapshotScene({ background, labels })`(它已经带上 `graphics`),或用导出的 `buildSceneSvg(scene, { scale })`;`svgDataUrl(markup)` 是把一段 SVG 标记转成 `data:` URL 的小工具(组件内部导 svg 图片时走它,自己拿到标记要拼进 `<img src>` 或 CSS 时也可以直接用)。
44
44
 
45
45
  ## 导出选中的节点
46
46
 
@@ -352,7 +352,7 @@ const focusSelectionPlugin = view => {
352
352
  }
353
353
  const selection = view.getSelections()
354
354
  if (selection.length) {
355
- view.focusOn(selection, { padding: 80 })
355
+ view.focusOn(selection, { zoom: true, padding: 80 })
356
356
  }
357
357
  }
358
358
  window.addEventListener('keydown', onKeydown)
@@ -385,7 +385,35 @@ dispose() // 卸载:跑上面那个 destroy,从实例的清理列表里摘
385
385
  - 与插件并列的另一条扩展点是**自定义图元**:插件加的是「视图级行为」(快捷键、面板、URL 同步…),自定义图元加的是「点怎么画」—— 注册一支画笔 `view.registerSymbol('三角', painter)`,或在数据里写 `{ type: 'custom', draw }`,见「自定义图元」一节。
386
386
  - 第三条扩展点是**自定义条件运算符**:插件加行为、图元加画法,运算符加的是「条件语言里的判定动词」—— `view.registerOperator('mod2', value => current => …)` 之后规则 / 筛选 / 标签的 `when` 都能写 `{ vn_kv: { mod2: true } }`,见「条件语言 → 自定义运算符」。
387
387
 
388
- ### 五类扩展点怎么选
388
+ ## 实例命令
389
+
390
+ `registerCommand` 把「业务动作」注册到当前视图实例上,适合面板按钮、快捷键和外接工具栏共用同一个入口。命令只在当前实例有效,不进全局注册表;handler 同步执行,拿到原样传入的 payload 与冻结的 `{ view, name }` 上下文:
391
+
392
+ ```js
393
+ // 业务动作注册一次,按钮 / 快捷键 / 宿主脚本都走 executeCommand
394
+ const dispose = view.registerCommand('focusHighestLoad', (payload = {}, context) => {
395
+ const threshold = Number(payload.threshold ?? 0.8)
396
+ const hit = view.search({ keyword: String(threshold), scope: 'points' }).points[0]
397
+ if (!hit) {
398
+ return { focused: false, threshold }
399
+ }
400
+ context.view.focusOn(hit.id, { select: true, zoom: true, maxZoom: 12 })
401
+ return { focused: true, id: hit.id, command: context.name }
402
+ })
403
+
404
+ view.listCommands() // ['focusHighestLoad']
405
+ const result = view.executeCommand('focusHighestLoad', { threshold: 0.85 })
406
+ // 成功:{ ok: true, name: 'focusHighestLoad', value: { focused: true, ... } }
407
+ // 未注册 / 名字非法 / handler 抛错:{ ok: false, name, reason }
408
+ dispose() // 幂等注销;同名已被后来者覆盖时不会误删
409
+ ```
410
+
411
+ - 名字会 `trim`;同名再注册以后一次为准,覆盖不会改变它在 `listCommands()` 里的位置。返回的注销函数只摘掉自己那一份注册。
412
+ - 名字为空 / handler 不是函数只报一条 `onWarning`,本次注册不生效;执行未注册的命令同样走 warning 并返回 `ok: false`。
413
+ - handler 抛出的异常由组件收成 `onError`,`executeCommand` 返回 `{ ok: false, name, reason }`,不会把异常甩给调用方。
414
+ - 命令是同步调用;要串异步流程就让 handler 自己启动 Promise,再用事件 / 状态读数回报结果。`view.destroy()` 会清空实例命令表。
415
+
416
+ ### 六类扩展点怎么选
389
417
 
390
418
  | 扩展点 | 适合做什么 | 作用域 | 清理方式 |
391
419
  | --- | --- | --- | --- |
@@ -394,6 +422,7 @@ dispose() // 卸载:跑上面那个 destroy,从实例的清理列表里摘
394
422
  | `view.registerOperator(name, test)` | 给规则 / 筛选 / 标签增加条件动词,例如 `mod2`、`evenIndex` | 模块级条件运算符表,同页所有实例共享 | 返回幂等注销函数,只删除自己这次注册;注销后旧条件按「不成立」处理 |
395
423
  | `view.addWebGLLayer(layer)` | 直接拿组件的 WebGL2 上下文画自定义标记、特效或调试几何;层画在内置内容之后,不参与拾取与数据导出 | 单个视图实例 | 返回卸载函数;也可用 `updateWebGLLayer` / `removeWebGLLayer` / `clearWebGLLayers` 维护,画布外还可通过 `view.gl` 直接取上下文 |
396
424
  | `defineExportFormat(name, adapter)` | 把标准文档映射成后端 DTO、业务表列名或项目自己的嵌套结构(同步导出器) | 模块级导出格式表,所有实例的 `exportData` 共享 | 返回幂等清理函数,只删除自己这次注册;需要按名字全局摘除时用 `unregisterExportFormat(name)` |
425
+ | `view.registerCommand(name, handler)` | 把业务动作注册成命令:面板按钮、快捷键与宿主脚本共用 `executeCommand` 入口,handler 里可继续调任意实例 API | 单个视图实例 | 返回幂等注销函数;同名覆盖时旧卸载函数不会误删新命令,`view.destroy()` 会清空命令表 |
397
426
 
398
427
  ## 自建界面
399
428
 
@@ -35,8 +35,8 @@
35
35
  | `zoomIn(step?)` / `zoomOut(step?)` / `zoomBy(factor, origin?)` | 相对缩放,`origin` 为画布 CSS 像素锚点(缺省视口中心) |
36
36
  | `zoomTo(scale, origin?)` | 直接设成指定比例(像素 / 世界单位) |
37
37
  | `zoomToBounds(bounds, padding?)` | 把一段世界坐标矩形适应到视图内,退化成一点时只居中不改比例 |
38
- | `focusOn(target, options?)` | 把视角移到目标上:单条(搜索命中 / 要素 id / `{ type: 'point' \| 'line' \| 'channel', id \| index }` / `{ channelId }` 整条通道 / `{ type: 'group', channelId?, groupId }` / 坐标 / 矩形 / `{ lngLat }`)或**一批**(数组取整体包围盒 —— 搜到三个节点就是这三个点的中心当屏幕中心);字符串先节点后线路,裸字符串不表示通道,同 id 用 `type` 消歧,分组必须显式写 `type: 'group'`(全局唯一 `groupId` 可省略通道);节点 / 线路 / 通道 / 分组共用同一套范围适配,不额外放大。`options` 认 `scale`、`center: true`(只居中、比例一点不动)、`padding`、`maxZoom`、`select`(顺带选中这批,`mode` 决定怎么并进当前选中集) |
39
- | `search(keyword \| options)` | 按关键字搜索节点 / 线路 / 通道并返回命中(**不动视角**:要不要聚焦由调用方自己调 `focusOn`),见「搜索与定位」 |
38
+ | `focusOn(target, options?)` | 把视角移到目标上:单条(搜索命中 / 要素 id / `{ type: 'point' \| 'line' \| 'channel', id \| index }` / `{ channelId }` 整条通道 / `{ type: 'group', channelId?, groupId }` / 坐标 / 矩形 / `{ lngLat }`)或**一批**(数组取整体包围盒 —— 搜到三个节点就是这三个点的中心当屏幕中心);字符串先节点后线路,裸字符串不表示通道,同 id 用 `type` 消歧,分组必须显式写 `type: 'group'`(全局唯一 `groupId` 可省略通道)。**缺省只居中、比例一点不动**;`options.zoom: true` 才按解析出的真实范围适配(节点 / 线路 / 通道 / 分组共用这一条,不额外放大),`scale` 优先级高于 `zoom`。`options` 还认 `center: true`(显式只居中,与缺省一致)、`padding`、`maxZoom`、`select`(顺带选中这批,`mode` 决定怎么并进当前选中集) |
39
+ | `search(keyword \| options)` | 按关键字搜索节点 / 线路 / 通道 / 通道分组并返回命中:`scope` 认复数的 `points` / `lines` / `channels` / `groups`,也认与 `focusOn` 一致的单数别名 `point` / `line` / `channel` / `group`;结果分 `points` / `lines` / `channels` / `groups` 四类,分组命中带 `channelIndex` / `groupId` / `groupIndex`,可直接交给 `focusOn` / `selectFeatures`。**不动视角**:要不要聚焦由调用方自己调 `focusOn`,见「搜索与定位」 |
40
40
  | `searchAsync(keyword \| options)` | 同一份口径的异步版:数据量大时把扫描放进 Worker(10 万点 / 10 万线下同步要占主线程 40 ms 上下的那一段),返回 `Promise<GisViewSearchResult>`;结果带 `worker` 与 `stale` 两个字段,见「异步搜索」 |
41
41
  | `setCamera(camera)` / `getCamera()` | 读写 `{ center, scale }` |
42
42
  | `getZoomBaseScale()` / `setZoomBaseScale(scale?)` | 读 / 重定**缩放联动的基准比例**(缺省是首次适应视图时的比例):不传参数就取当下相机的比例 —— 把现在这一屏当成「原样大小」,之后放大缩小都从它算倍数;配合 `pointZoomScale` / `lineZoomScale` 使用 |
@@ -152,7 +152,7 @@
152
152
  | `beginDecorationDraft(options?)` / `getDecorationDraft()` / `updateDecorationDraft(patch?)` / `commitDecorationDraft(overrides?)` / `cancelDecorationDraft(options?)` | 动态装饰器草稿:`target` 取 `'line'`(缺省)/ `'point'` / `'both'`,指针移到线路 / 回线或节点上就画临时装饰,移出立即收掉;`both` 按屏幕距离取更近者、等距优先节点,最近目标被白名单或上限拦住时尝试另一类。线路按 `scope`(`line` 单条 / `group` 同组 / `channel` 整条通道,非通道线退回单条)展开;节点侧 `placement` 只选位置来源(`offset` 相对偏移 / `fixed` 固定坐标 / `pointer` 鼠标位置),`anchor` 再选落地关系(`follow` 保存相对节点的屏幕偏移 / `fixed` 保存世界坐标),两者可自由组合,缺省按 `offset → follow`、`fixed / pointer → fixed` 推导。`allowedLines` / `allowedChannels` / `allowedGroups` / `allowedPoints` 与 `maxPerLine` / `maxPerPoint` 在悬浮与提交两处拦截,`allowDuplicate: false` 时按 `duplicateDistance` 检测同位置重复,`duplicateStrategy` 选 `offset` 自动避让或 `reject` 直接拦住。左键在目标上松开发 `ready`,空白处不发;`get` 取当前载荷,`update` 边交互边改图标 / 大小 / 方向 / 范围 / 位置来源 / 落地关系;`commit` 把线路与节点目标放在同一个 `batch` 里写入(一次 `dataChange`、一条历史),`cancel` 只清假装饰(Esc / 右键等价),数据与历史一个字节不动 |
153
153
  | `setChannelLineWidth(id, width)` / `setChannelColors(id, colors)` / `setChannelPath(id, coordinates)` | 只改一条通道的回线线宽 / 逐回线颜色 / 骨架拐点:前两个把**成员回线连同通道声明**一起改(一次 `batch`,只发一条数据变更、只落一步历史),颜色不够时按顺序循环取、只给一个就整条通道一个色(改**单条回线**的颜色用 `updateLine(id, { color })`,不必走整条通道的批量写法);`colors` 传字符串或字符串数组,`setChannelPath` 只写中间拐点(两端由节点坐标补齐);分过组的通道写第一组的声明(`groups[].path` 优先于顶层 `path`;「第一组」跳过 `lineIds` 空的分组声明 —— 引擎会把这种组整组丢掉),要改别的组用 `updateChannel(id, { groups })` |
154
154
  | `getParallelGroups()` | 新增拐点集合:被加过 / 拖过拐点的通道与线路,结构与 `test.json` 的 `parallel_groups` 一致 |
155
- | `setFilter(filter \| null)` | 设置或清空筛选条件(传 `null` 清空):完整格式 `{ points, lines, cascade, worker }`,只筛节点时可以简写成节点条件本身;见「筛选」 |
155
+ | `setFilter(filter \| null)` | 设置或清空筛选条件(传 `null` 清空):完整格式 `{ points, lines, cascade, mode, worker }`,只筛节点时可以简写成节点条件本身。`mode: 'hide'`(缺省)只隐藏、不删数据、也不清空选中集;`mode: 'remove'` 把筛选结果从数据里删掉,只发一条 `filterRemove` 数据变更与最终一条 `onFilter`,见「筛选」 |
156
156
  | `getFilter()` / `getFilterResult()` | 当前条件与最近一次结果:隐藏的点 / 线下标、可见数量、是否用了 Worker、耗时 |
157
157
  | `getVisibleData()` | 过滤之后的数据副本(`points` / `lines`) |
158
158
  | `getMovedPoints()` | 位置对账:相对上次对账(建视图 / 整份换数据 / 上一次 `clearMovedPoints`)位置变过的节点,按 id 去重、一趟扫出 `[{ index, id, x, y, item }]` —— 拖动 / `updatePoint` / `patchPoints` / 撤销都算,`removePoints` 删掉的记录顺手清掉,见「节点位置对账」 |
@@ -173,6 +173,9 @@
173
173
  | `unregisterOperator(name)` | 注销一个自定义运算符(返回它之前是否注册着):还在用这个名字的条件按认不出来处理(不成立,一个要素都不命中) |
174
174
  | `listOperators()` | 现在能用哪些运算符:内置 14 个在前,自定义按注册顺序跟在后面 |
175
175
  | `use(plugin)` | 运行时挂一个插件(写法与 `options.plugins` 里的一项一致):调用即执行,返回幂等的卸载函数(跑插件自己的 `destroy`),见「插件」 |
176
+ | `registerCommand(name, handler)` | 注册一个实例命令:`handler(payload, context)` 同步执行,`context` 是冻结的 `{ view, name }`;同名覆盖,返回幂等的注销函数,旧卸载函数不会误删后来注册的同名命令,见「实例命令」 |
177
+ | `executeCommand(name, payload?)` | 执行实例命令:成功给 `{ ok: true, name, value }`;名字非法 / 未注册 / handler 抛错给 `{ ok: false, name, reason }`,见「实例命令」 |
178
+ | `listCommands()` | 当前注册着哪些实例命令(按注册顺序;同名覆盖不改变原来的位置) |
176
179
  | `getFeatures()` / `setFeatures(next)` | 读当前生效的功能开关(跟随详细配置的也解析成 `true` / `false`)/ 换一份(预设名 / 开关表 / `true` / `false`),换完立刻生效并发一条 `onFeatureChange` |
177
180
  | `undo()` / `redo()` | 撤销 / 重做一步:返回 `{ type, label, canUndo, canRedo }`,历史没开或没得撤销 / 重做时返回 `null` |
178
181
  | `canUndo()` / `canRedo()` | 能不能撤销 / 重做(界面据此灰掉按钮) |
@@ -192,7 +195,7 @@
192
195
 
193
196
  返回 `this` 的方法都可以链式调用;返回其它值的方法(`get*` / `list*` / `export*` / `search` / `pick*` / `add*` / `remove*` / `copy*` / `merge*` / `locate*` 这些)在下面的「参数速查」里都标了返回类型。
194
197
 
195
- **怎么确认「组件到底有哪些属性和方法」**:上面那张表按功能分组介绍,下面的「参数速查」把实例的**每个成员逐个列全**(当前 253 个:250 个方法 + `el` / `canvas` / `gl` 三个属性),`options` 表列全 95 个键,数据字段表列全 249 个字段。这三份清单不靠手抄 —— 它们直接对着源码与类型对账:
198
+ **怎么确认「组件到底有哪些属性和方法」**:上面那张表按功能分组介绍,下面的「参数速查」把实例的**每个成员逐个列全**(当前 256 个:253 个方法 + `el` / `canvas` / `gl` 三个属性),`options` 表列全 96 个键,数据字段表列全 250 个字段。这三份清单不靠手抄 —— 它们直接对着源码与类型对账:
196
199
 
197
200
  - `node docs/steps/gen-api-index.mjs`:从 `create.js` 的实例定义、本文档的两张表与 `types.d.ts` 生成 `docs/demos/api-index.js`(`API_SUMMARY` / `API_SECTIONS` / `API_OPTIONS`),demo 的「实例 API」面板读的就是它;带 `--check` 时只校验有没有过期。
198
201
  - `node docs/steps/check-doc-coverage.mjs`:10 项双向对账 —— 实例成员 × 文档 / × 参数速查 / × 类型、参数速查与实例方法主表各自反查过期条目、类型反查过期声明、数据字段 × 文档、options 键 × 文档 / options 表反查过期字段、demo 清单是否最新。加一个实例成员、改一个 options 键而没同步文档时,这个脚本会直接报出来。
@@ -1,6 +1,6 @@
1
1
  # 17 参数速查(逐个方法)
2
2
 
3
- > 253 个成员逐个列全:参数、默认值与返回。
3
+ > 256 个成员逐个列全:参数、默认值与返回。
4
4
 
5
5
  ### 参数速查(逐个方法)
6
6
 
@@ -15,7 +15,7 @@
15
15
  - **同步方法返回 `this` 时可链式调用**:所有 `update*` / `set*` / `add*` / `remove*`、选中与视角方法都先完成本地状态更新再返回实例;需要结果时用相邻的 `get*` / `list*` 读回。少数返回数组、对象或 `null` 的方法在「返回」列单独标出。
16
16
  - **数据写入只发一次通知**:`updatePoint`、`updateLine`、`patch*`、`add*`、`remove*` 等每次成功改数据都会发一条对应的 `dataChange`(底层统一为 `updatePoints` / `updateLines` / `updateChannels` 等类型)并落一步历史;在 `batch()` 里连续调用会合并成一条。`emit: false` 只写数据、不发通知,适合宿主自己在保存成功后统一刷新。
17
17
  - **选中方法会同步选中集**:`selectFeatures` / `selectWaypoint` / `selectInBox` 先更新引擎选中态,再同步 `getSelections()`、`getSelectionData()` 与 `isSelected()`;一次有效变更会发 `onSelect`(`cause` 标明是 `api`、`box` 或指针来源)和 `onSelectChange`。没有命中任何目标时不会凭空选中,`selectWaypoint(null)` 只清拐点、不动要素选中集。
18
- - **视角方法会更新相机**:`focusOn` / `zoomTo` / `setCamera` / `fitView` 成功落到目标范围后更新 `getCamera()`,并触发 `onCameraChange`;目标认不出来、视口尚未量出或参数非法时保持当前相机。`focusOn` 的 `scale` / `center` / `padding` / `maxZoom` 只影响这一次调用,不会改写持久配置。
18
+ - **视角方法会更新相机**:`focusOn` / `zoomTo` / `setCamera` / `fitView` 成功落到目标范围后更新 `getCamera()`,并触发 `onCameraChange`;目标认不出来、视口尚未量出或参数非法时保持当前相机。`focusOn` 缺省只居中、不改比例;`zoom` / `scale` / `center` / `padding` / `maxZoom` 只影响这一次调用,不会改写持久配置。
19
19
  - **只读方法返回快照或副本**:`getSelectionData()`、`getFilter()`、`getStats()`、`snapshotScene()`、`listPoints()` / `listLines()` 返回的是调用当时的读数,不会随后续数据变更自动更新;需要最新值就再调一次。对象型字段的含义以 `types.d.ts` 同名接口为准。
20
20
  - **失败是可控的**:找不到 id / 下标越界 / 不支持的写法会跳过这一条,能返回 `null` 或 `{ ok: false, reason }` 的就按对应签名返回;需要排查时订阅 `onWarning`。异步筛选 / 搜索通过完成事件、`getFilterResult()` / 搜索结果或返回的 Promise 取结果,不要在调用下一行假定 Worker 已经跑完。
21
21
 
@@ -58,9 +58,9 @@
58
58
  | `zoomBy(factor, origin?)` | `factor: number` 缩放倍数(`> 1` 放大、`< 1` 缩小);`origin?: [number, number]` 缩放中心(画布 CSS 像素,缺省画布中心) | `this` |
59
59
  | `zoomTo(scale, origin?)` | `scale: number` 目标缩放值(世界单位 → CSS 像素的比例);`origin?: [x, y]` 缩放中心 | `this` |
60
60
  | `zoomToBounds(bounds, padding?)` | `bounds: GisViewBounds` 要装进画面的世界矩形 `[minX, minY, maxX, maxY]`(也认 `{ minX, minY, maxX, maxY }`);`padding?: number` 四周留白 | `this` |
61
- | `focusOn(target, options?)` | `target: GisViewFocusTargets` 要对准的东西:id / `{ type: 'point' \| 'line' \| 'channel' \| 'group', id \| index }` / `{ channelId }`(整条通道)/ `{ type: 'group', channelId?, groupId }` / `[x, y]` 坐标 / 矩形 / `{ lngLat }` / `{ center }`,或者**一批**(数组取整体包围盒)。字符串按「先节点、再线路」,裸字符串不能表示通道;同 id 用显式 `type` 消歧,分组必须用 `type: 'group'`(`groupId` 全局唯一时可省略 `channelId`)。四种显式类型共用同一套包围盒适配,没有额外放大倍率;`maxZoom` 只给单点 / 单线这类零面积目标补最小范围。`options?: GisViewFocusOptions`:`scale` 直接给比例、`center: true` 只居中且不动比例、`padding` 要四周留白(CSS 像素,缺省 40)、`maxZoom` 限制这次适配的最大比例、`select` 顺带选中(`true` 选这次目标,也可另给一批写法)、`mode` 决定怎么并进当前选中集。目标认不出来或参数非法时什么都不做 | `this`(相机更新后发 `onCameraChange`;`select` 生效时另发 `onSelect` / `onSelectChange`) |
62
- | `search(input)` | `input: string \| GisViewSearchOptions` 关键字,或 `{ keyword, scope, fields, limit }` 这类选项(`scope` 可选 `'points'` / `'lines'` / `'channels'`,也认它们的数组)—— **只返回结果、不动视角**,要定位就自己调 `focusOn(result.hits, …)` | `GisViewSearchResult` |
63
- | `searchAsync(input)` | 同上 | `Promise<GisViewSearchResult>`:多一个 `worker`(是不是在 Worker 里算的)与 `stale`(回来时数据已经改过、或又搜过一次,这一份不作数了) |
61
+ | `focusOn(target, options?)` | `target: GisViewFocusTargets` 要对准的东西:id / `{ type: 'point' \| 'line' \| 'channel' \| 'group', id \| index }` / `{ channelId }`(整条通道)/ `{ type: 'group', channelId?, groupId }` / `[x, y]` 坐标 / 矩形 / `{ lngLat }` / `{ center }`,或者**一批**(数组取整体包围盒)。字符串按「先节点、再线路」,裸字符串不能表示通道;同 id 用显式 `type` 消歧,分组必须用 `type: 'group'`(`groupId` 全局唯一时可省略 `channelId`)。**缺省只居中、比例一点不动**;`options.zoom: true` 才按解析出的真实范围适配,四类目标共用这一条、没有额外放大倍率;`scale` 优先级高于 `zoom`,`maxZoom` 只给单点 / 单线这类零面积目标补最小范围。`options?: GisViewFocusOptions`:`zoom: true` 显式适配、`scale` 直接给比例、`center: true` 显式只居中(与缺省一致)、`padding` 要四周留白(CSS 像素,缺省 40)、`maxZoom` 限制这次适配的最大比例、`select` 顺带选中(`true` 选这次目标,也可另给一批写法)、`mode` 决定怎么并进当前选中集。目标认不出来或参数非法时什么都不做 | `this`(相机更新后发 `onCameraChange`;`select` 生效时另发 `onSelect` / `onSelectChange`) |
62
+ | `search(input)` | `input: string \| GisViewSearchOptions` 关键字,或 `{ keyword, scope, fields, limit }` 这类选项。`scope` 可选 `'points'` / `'lines'` / `'channels'` / `'groups'`,也认与 `focusOn` 一致的单数别名 `'point'` / `'line'` / `'channel'` / `'group'` 及数组;省略或写错时四类全搜。`limit` 对每一类分别计数。返回的 `groups` / `hits` 带分组命中(`channelIndex` / `groupId` / `groupIndex`,`item` 是轻量快照)—— **只返回结果、不动视角**,要定位就自己调 `focusOn(result.hits, …)` | `GisViewSearchResult`:`keyword` / `scope` / `points` / `lines` / `channels` / `groups` / `hits` / `total` / `truncated` / `durationMs` |
63
+ | `searchAsync(input)` | 同上;分组范围的当前轻量快照会随请求交给 Worker | `Promise<GisViewSearchResult>`:多一个 `worker`(是不是在 Worker 里算的)与 `stale`(回来时数据已经改过、或又搜过一次,这一份不作数了) |
64
64
  | `setCamera(camera)` | `camera: Partial<GisViewCamera>` 要改的相机字段 `{ x, y, scale }`(只写一个也行) | `this` |
65
65
  | `getCamera()` | 无 | `GisViewCamera` |
66
66
  | `getZoomBaseScale()` | 无 | `number` 缩放联动的基准比例(`pointZoomScale` / `lineZoomScale` 共用那一份;缺省是首次适应视图时的 `camera.scale`) |
@@ -306,7 +306,7 @@
306
306
 
307
307
  | 方法 | 参数(名字: 类型 = 默认值) | 返回 |
308
308
  | --- | --- | --- |
309
- | `setFilter(filter)` | `filter: GisViewFilterInput \| null` 筛选条件:完整格式 `{ points, lines, cascade, worker }`;只筛节点时可以简写成节点条件本身;传 `null` 等于清空 | `this` |
309
+ | `setFilter(filter)` | `filter: GisViewFilterInput \| null` 筛选条件:完整格式 `{ points, lines, cascade, mode, worker }`;只筛节点时可以简写成节点条件本身;传 `null` 等于清空。`mode: 'hide'`(缺省)只隐藏、不删数据、不清空选中集;`mode: 'remove'` 删除筛选结果,只发一条 `filterRemove` 与最终一条 `onFilter` | `this` |
310
310
  | `getFilter()` | 无 | `GisViewFilter \| null` |
311
311
  | `getFilterResult()` | 无 | `GisViewFilterResult \| null` |
312
312
 
@@ -318,6 +318,9 @@
318
318
  | `once(name, handler)` | 与 `on` 同一套事件名与载荷;只触发一次,回调跑完自动摘掉。返回的取消函数可提前撤销;即使提前取消,重复调用也安全 | `() => void` 取消订阅 |
319
319
  | `off(name, handler?)` | `name` 事件名(也认 `'*'`);`handler?` 只摘掉这一个回调,不传 = 摘掉这个事件的全部订阅 | `number` 摘掉几条(没摘到返回 `0`) |
320
320
  | `use(plugin)` | `plugin: GisViewPlugin`(`(view, context) => hook`,与 `options.plugins` 的写法一致):立即执行一次,返回卸载函数 —— 调它跑插件自己的 `destroy`(重复调用只清一次,实例销毁之后也安全) | `() => void` 卸载 |
321
+ | `registerCommand(name, handler)` | `name: string` 命令名(会 `trim`,同名覆盖);`handler: GisViewCommandHandler<Payload, Value>` 同步处理函数 `(payload, context) => Value`,`context` 是冻结的 `{ view, name }`。名字为空 / handler 不是函数只报 warning、不注册;视图销毁后注册不生效 | `() => void` 幂等注销;同名已被后来者覆盖时不会误删新命令 |
322
+ | `executeCommand(name, payload?)` | `name: string` 命令名;`payload?: unknown` 原样交给 handler | `GisViewCommandResult<Value>`:`{ ok: true, name, value }` / `{ ok: false, name, reason }`(未注册 / 名字非法 / handler 抛错) |
323
+ | `listCommands()` | 无 | `string[]` 当前注册的命令名(按注册顺序;同名覆盖不改变位置) |
321
324
  | `getFeatures()` | 无 | `GisViewFeatureState` |
322
325
  | `setFeatures(next)` | `next: GisViewFeaturesInput` 预设名(`'edit'` / `'view'` / `'static'`)、开关表或布尔值 | `this` |
323
326
  | `undo()` / `redo()` | 无:从撤销 / 重做栈回放一步;会同步数据、发 `history` 与对应的 `dataChange`,并更新相机以外的运行时读数 | `GisViewHistoryStepResult \| null`:`{ type, label, canUndo, canRedo }`;历史没开或栈空时为 `null` |
@@ -26,6 +26,7 @@ view.updateOptions({ points: nextPoints, lines: nextLines }, true)
26
26
  // denseMode:大数据展示模式('auto' 缺省 / 'on' / 'off',或对象写法换三档阈值)
27
27
  view.updateOptions({ denseMode: 'on' }) // 临时强制进入密集档(标签避让 / 截断,动效暂停)
28
28
  view.updateOptions({ denseMode: { mode: 'auto', visibleThreshold: 3000, densityThreshold: 2500, pointThreshold: 60000 } })
29
+ view.updateOptions({ selectTrigger: 'dblClick' }) // 改成双击选中(缺省 'click');第二下同时派发 onClick 与 onDblClick
29
30
  const dense = view.getStats().dense // { mode, active, reason, visiblePoints, visibleThreshold, densityThreshold, pointThreshold }
30
31
 
31
32
  // updatePointStyle(patch):按字段合并全局点样式;写 null 清掉整份
@@ -169,20 +170,23 @@ view.zoomToBounds([0, 0, 400, 200], 40)
169
170
  view.zoomToBounds({ minX: 116, minY: 39, maxX: 117, maxY: 40 })
170
171
 
171
172
  // focusOn(target, options?):对准一个或多个目标(id / 显式类型 / 坐标 / 矩形 / 搜索命中 / 一批)
172
- view.focusOn('a')
173
- view.focusOn(['a', 'b'], { padding: 60, maxZoom: 8, select: true })
173
+ view.focusOn('a') // 缺省只居中,比例不动
174
+ view.focusOn('a', { zoom: true, maxZoom: 8 }) // 显式按范围适配
175
+ view.focusOn(['a', 'b'], { zoom: true, padding: 60, select: true })
174
176
  view.focusOn({ type: 'point', id: 'a' }) // 节点与线路同 id 时显式消歧
175
177
  view.focusOn({ type: 'line', id: 'a' })
176
178
  view.focusOn({ type: 'channel', channelId: 'c1' }) // 通道也可以直接写 { channelId: 'c1' }
177
179
  view.focusOn({ type: 'group', channelId: 'c1', groupId: 'c1::2' })
178
180
  view.focusOn({ type: 'group', groupId: 'global-group' }) // groupId 全局唯一时可省略通道
179
- view.focusOn([120, 80], { center: true }) // 只居中,比例不动
181
+ view.focusOn([120, 80], { center: true }) // 显式只居中(与缺省一致)
180
182
  view.focusOn({ lngLat: [116.4, 39.9] })
181
183
 
182
184
  // search(input) / searchAsync(input):搜索(只返回结果、不动视角)
183
185
  const hits = view.search('甲站')
184
- const scoped = view.search({ keyword: '甲', scope: ['points', 'channels'], limit: 10 })
185
- view.focusOn(hits.hits, { maxZoom: 8 }) // 命中集直接丢给 focusOn
186
+ const scoped = view.search({ keyword: '甲', scope: ['points', 'channels', 'groups'], limit: 10 })
187
+ const groupHits = view.search({ keyword: 'c1::2', scope: 'group' }) // 单数别名与 focusOn 一致
188
+ view.focusOn(groupHits.groups[0], { zoom: true, select: true }) // 按整组范围适配并选中成员回线
189
+ view.focusOn(hits.hits, { zoom: true, maxZoom: 8 }) // 命中集直接丢给 focusOn
186
190
  const asyncHits = await view.searchAsync('甲站') // 大表走 Worker;stale 说明这一份不作数了
187
191
 
188
192
  // setCamera(camera) / getCamera():读写相机(只写一个字段也行)
@@ -832,15 +836,29 @@ const selectionData = view.exportData({ scope: 'selection', parts: ['station', '
832
836
  #### 筛选
833
837
 
834
838
  ```js
835
- // setFilter(filter):设筛选条件(只筛节点可以写简写);传 null 等于清空
839
+ // setFilter(filter):设筛选条件(只筛节点可以写简写);传 null 等于清空;默认只隐藏
836
840
  view.setFilter({ vn_kv: { gte: 525 } })
837
841
  view.setFilter({ points: { station_type: 'coal' }, lines: { vn_kv: { gte: 525 } }, cascade: 'binding' })
838
842
  view.setFilter(null)
839
843
 
844
+ // 默认 hide:只隐藏显示,不删数据、也不清空选中集;被筛掉的选中项在条件放宽后原样恢复
845
+ view.selectFeatures(['a', 'a-b'])
846
+ view.setFilter({ points: { station_type: 'coal' }, cascade: false })
847
+ view.getSelections().map(item => item.item.id) // 仍然包含被隐藏的 'a' / 'a-b'
848
+ view.setFilter(null)
849
+
850
+ // remove:不命中的点 / 线直接从数据里删掉;先删线路再删节点,只发一条 filterRemove
851
+ view.on('dataChange', payload => {
852
+ if (payload.type === 'filterRemove') console.log(payload.pointIndexes, payload.removedPoints, payload.removedLineItems)
853
+ })
854
+ view.setFilter({ points: { station_type: 'coal' }, mode: 'remove' })
855
+ view.getFilterResult() // 删除完成后 hiddenPoints / hiddenLines 为空
856
+ view.undo() // 开了 history 时恢复数据;回放本身不会再触发一次删除
857
+
840
858
  // getFilter() / getFilterResult():当前条件(副本)/ 最近一次结果
841
859
  view.setFilter(null)
842
860
  const filter = view.getFilter()
843
- const filterResult = view.getFilterResult() // { hiddenPoints, hiddenLines, visiblePoints, visibleLines, worker, durationMs }
861
+ const filterResult = view.getFilterResult() // hide 模式是隐藏下标;remove 完成后 hidden 为空
844
862
  ```
845
863
 
846
864
  #### 事件、开关与历史
@@ -857,6 +875,20 @@ view.off('dataChange', sync) // 只摘掉这一个(返回摘掉几条
857
875
  const dispose = view.use((view, context) => ({ destroy() { /* 插件自己的清理;context.apiVersion 当前为 1 */ } }))
858
876
  dispose()
859
877
 
878
+ // registerCommand(name, handler) / executeCommand(name, payload?) / listCommands():实例级业务命令
879
+ const disposeCommand = view.registerCommand('focusHighestLoad', (payload = {}, context) => {
880
+ const threshold = Number(payload.threshold ?? 0.8)
881
+ const target = view.getData().points
882
+ .filter(point => Number(point.max_loading_rate ?? 0) >= threshold)
883
+ .sort((left, right) => Number(right.max_loading_rate ?? 0) - Number(left.max_loading_rate ?? 0))[0]
884
+ if (!target) return { focused: false, threshold }
885
+ context.view.focusOn(target.id, { select: true, zoom: true, maxZoom: 12 })
886
+ return { focused: true, id: target.id, command: context.name }
887
+ })
888
+ const commandResult = view.executeCommand('focusHighestLoad', { threshold: 0.85 })
889
+ const commandNames = view.listCommands() // ['focusHighestLoad']
890
+ disposeCommand() // 幂等卸载;同名覆盖时不会误删新命令
891
+
860
892
  // getFeatures() / setFeatures(next):功能开关的读 / 写(预设名 / 开关表 / 布尔值)
861
893
  const features = view.getFeatures()
862
894
  view.setFeatures('view') // 'edit' / 'view' / 'static'
@@ -20,8 +20,8 @@ view.listLines({ inView: true, limit: 200 }) // 视口里看得见的线
20
20
  view.getElementBounds(view.getSelections()) // { minX, minY, maxX, maxY }
21
21
 
22
22
  // 把视角挪过去 / 一次取全读数
23
- view.focusOn(view.getSelections(), { maxZoom: 8 }) // 选中集整体居中;单个节点也会放大到 8 倍
24
- view.focusOn([{ type: 'point', index: 3 }, { type: 'line', index: 2 }], { padding: 60 })
23
+ view.focusOn(view.getSelections(), { zoom: true, maxZoom: 8 }) // 选中集按整体范围适配;单个节点最多放大到 8 倍
24
+ view.focusOn([{ type: 'point', index: 3 }, { type: 'line', index: 2 }], { zoom: true, padding: 60 })
25
25
  const state = view.getViewState() // camera / viewport / layoutReady / stats …
26
26
 
27
27
  // 自定义右键菜单:一个屏幕点上的东西都在这里
@@ -34,7 +34,7 @@ canvas.addEventListener('contextmenu', (event) => {
34
34
 
35
35
  `GisViewListOptions` 的几个筛选条件是「与」的关系:`indexes`(只看这几个下标)、`inView`(`true` 按当前视口,或者直接给一份世界矩形 `{ minX, minY, maxX, maxY }`)、`limit`(最多几条)、`visibleOnly`(缺省 `true`:被筛选 / `visible: false` 隐藏掉的不算)、`withScreen`(补一份画布 CSS 像素坐标)。都不给就是「全部可见的」;十万级数据下这套方法适合面板读数、导出与校验,别逐帧调。
36
36
 
37
- `listPoints()` 用的就是引擎画点那一份可见判定(旧版单独暴露的 `getVisiblePoints()` 并进了这一条):`radius` / 图层号 / 屏幕坐标这些几何读数都在它给的条目里。`focusOn()` 只移动相机、不改数据:`{ center: true }` 只居中、比例不动,`{ select: true }` 顺带把这批选上,传一批目标(数组)就是整体居中。
37
+ `listPoints()` 用的就是引擎画点那一份可见判定(旧版单独暴露的 `getVisiblePoints()` 并进了这一条):`radius` / 图层号 / 屏幕坐标这些几何读数都在它给的条目里。`focusOn()` 只移动相机、不改数据:缺省(以及显式 `{ center: true }`)只居中、比例不动,`{ zoom: true }` 才按目标范围适配,`{ select: true }` 顺带把这批选上,传一批目标(数组)就是整体居中。
38
38
 
39
39
  只要「屏幕里这一片」的节点时用 `forEachPointInView()`:它按数据顺序逐个回调 `(index, item, x, y, radius)`,不建中间数组,回调返回 `false` 可以提前结束 —— 放大看一小片时不会去碰整份数据(十万级数据下,面板读数、导出与排序用上面那两条,按视口画自己的东西用这一条):
40
40