Appearance
属性操作(编辑 / 定位 / 过滤高亮)
除几何编辑外,要素的另一半是属性(feature.properties)。GeoVerse 编辑器把「改字段值、按字段查/定位、按条件筛一批高亮」纳入编辑引擎一等公民——属性编辑与几何编辑共享同一套撤销栈与后端同步契约(RFC-0007 / ADR-0009)。
三项能力 + 一套字段 Schema:
| 能力 | 说明 | 落点 |
|---|---|---|
| 属性编辑 | 改单个 / 批量要素的字段值,可撤销、可提交后端 | core 命令 |
| 定位 | 按要素 id / 过滤结果,把视野 fit 到目标要素 | core 算 extent,适配器 fit |
| 过滤高亮 | 可插拔谓词筛已加载要素,命中高亮 / 其余淡出 | core 谓词 + 适配器高亮 |
| 字段 Schema | 字段定义 + 类型,驱动表单与(仅 UX 的)校验 | core schema |
属性编辑
SetPropertiesCommand 浅合并 patch 到 properties,产出含 propertyChanges 的 ChangeSet(加法式,不动几何语义)。撤销 / 重做 / 同步与几何走同一机制。
ts
import { EditSession, SetPropertiesCommand } from '@geoverse/editor-core';
const session = new EditSession({
features: [
/* … */
],
});
// 单要素
session.setProperties('f1', { name: '新名称', level: 3 });
// 批量:统一 patch
session.setPropertiesBatch({ ids: ['f1', 'f2'], patch: { status: 'done' } });
// 批量:按 id 各自 patch
session.setPropertiesBatch({ perId: { f1: { level: 1 }, f2: { level: 2 } } });
session.engine.undo(); // 属性变更可撤销before/after 为该要素改动前后的完整属性快照,undo 整体还原,round-trip 精确。
字段 Schema(声明 ∪ 推断)
类型系统支持 string / int / float / boolean / date。Schema = 混合:显式声明优先、未声明从现有 properties 自动推断。
ts
import { inferSchema, mergeSchema } from '@geoverse/editor-core';
// 纯推断
const schema = session.resolveSchema();
// 声明优先、推断补缺
const schema2 = session.resolveSchema([
{
name: 'level',
type: 'int',
label: '等级',
required: true,
enum: [1, 2, 3],
},
]);字段级强转与校验(仅 UX,后端权威,ADR-0004):
ts
import { coerceValue, validateValue } from '@geoverse/editor-core';
coerceValue('int', '42'); // → 42
validateValue({ name: 'level', type: 'int', required: true }, undefined);
// → [{ ruleId: 'field:level:required', severity: 'error', … }]过滤高亮
谓词即 (feature) => boolean,配套构建器为语法糖。在内存中已加载的要素上求值,命中集仅驱动适配器重着色——不改数据、不入撤销栈。
ts
import { gt, and, eq, queryFeatures } from '@geoverse/editor-core';
// 直接查询命中 id
queryFeatures(features, and(gt('pop', 1000), eq('type', 'city')));
// 经 session 下发给适配器(高亮命中 / 淡出其余)
session.setFilter(gt('pop', 1000));
session.onFilterChange(({ matchedIds, scopedIds }) => {
adapter.applyFilter(matchedIds, scopedIds); // OL / libre 对称 API
});
// 按图层分类控制:scope 限定参与过滤的子集
session.setFilter(gt('pop', 1000), { scope: eq('layer', 'cities') });
session.clearFilter();构建器:eq / neq / gt / lt / range / oneOf / contains / and / or / not。
后端下推过滤(predicate → WFS filter)为预留扩展点,本期不实现。
定位
core 算包围盒,适配器 fit 视野(坐标按各自工作 CRS)。
ts
const bbox = session.extentOf(['f1', 'f2']); // BBox | null
adapter.locate(bbox); // OL view.fit / libre fitBounds作用范围:一个 EditSession = 一个扁平要素集合
属性操作的粒度是整个 EditSession,不是「图层」也不是「类别」。编辑引擎内部只持有一个扁平的要素表(Map<FeatureId, EditableFeature>),适配器也只渲染一个要素源——editor-core 没有原生的图层 / 分组 / 类别概念。因此:
setProperties/resolveSchema/setFilter默认作用于会话内全部要素,不分几何类型、不分业务类别。resolveSchema()在要素异构时返回字段并集(声明 ∪ 推断;推断类型冲突的字段退化为string),无关字段留空。- 编辑不受过滤 / scope 门控:
setProperties(id, patch)按 id 改任意要素,没有「当前活动图层」的限制。 setFilter(pred, { scope })里的scope只缩小前端高亮 / 淡出范围,不约束编辑、不改数据、不入撤销栈。
要表达「按图层 / 按类别操作」,有两条路:
| 方案 | 做法 | 适用 |
|---|---|---|
| A. 多个 EditSession | 一图层 / 一类别一个 session,各自独立撤销栈、Schema、同步 | 需要真正隔离(最接近 QGIS「每图层一张属性表」) |
| B. 单 session + 判别字段 | 要素加 category / layer 字段;用 setFilter 的 scope 聚焦高亮、表单按所选要素的类别自行裁剪 schema | 共享撤销 / 同步,轻量 |
在线演示见 Playground「属性表 + 编辑」预设:单集合内混合点 / 线 / 面共 4 个类别,用类别下拉聚焦「图层」、表单按类别裁剪字段、
WHERE过滤叠加类别scope——正是方案 B 的落地。
后端同步与字段级合并
属性变更随 EditSubmission 提交;409 冲突走字段级三路合并——本地改 A 字段、服务端改 B 字段可并存,仅同字段双改才算真分歧(交 resolver 裁决,默认采纳服务端)。详见核心概念 · 后端同步。
ts
import { SyncClient, remoteWinsResolver } from '@geoverse/editor-core';
const sync = new SyncClient(engine, backend, {
conflictResolver: remoteWinsResolver,
});
// 属性编辑与几何编辑共用同一 commit / rebase 流程框架封装(Vue / React)
headless composable / hook(零样式)+ 一套无样式参考组件(功能完整、可整体替换)。
tsx
import {
useFeatureAttributes,
useAttributeFilter,
AttributeForm,
AttributeFilter,
FeatureList,
} from '@geoverse/react';
// headless
const { values, dirty, setField, save, reset } = useFeatureAttributes(session, id);
const { matchedIds, apply, clear } = useAttributeFilter(session);
// 参考组件(render-prop 可整体接管渲染)
<AttributeForm session={session} featureId={id} declared={schema} />
<AttributeFilter session={session} declared={schema} />
<FeatureList session={session} locator={adapter} />vue
<script setup lang="ts">
import {
useFeatureAttributes,
useAttributeFilter,
GvAttributeForm,
GvAttributeFilter,
GvFeatureList,
} from '@geoverse/vue';
const { values, dirty, setField, save, reset } = useFeatureAttributes(
session,
() => id,
);
const { matchedIds, apply, clear } = useAttributeFilter(session);
</script>
<template>
<GvAttributeForm :session="session" :feature-id="id" :declared="schema" />
<GvAttributeFilter :session="session" :declared="schema" />
<GvFeatureList :session="session" :locator="adapter" @pick="onPick" />
</template>composables / hooks:useFieldSchema · useFeatureAttributes · useAttributeFilter · useFeatureLocate。参考组件默认插槽 / render-prop 暴露内部状态,方便整体替换样式。
下一步
- 核心概念 —— 撤销重做 / 类型校验 / 后端同步(含三路合并)
- OpenLayers 适配器 · MapLibre 适配器
- 编辑器 API 参考
