Skip to content

属性操作(编辑 / 定位 / 过滤高亮)

除几何编辑外,要素的另一半是属性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 字段;用 setFilterscope 聚焦高亮、表单按所选要素的类别自行裁剪 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 暴露内部状态,方便整体替换样式。

下一步