基于OpenLayers的等值线编辑功能实现

功能概述

接上一篇等值线编辑,这次是选择任意部分进行编辑,用于对生成的等值线进行部分微调,包括:

  • 手动添加锚点进行局部编辑
  • 曲线平滑度调节
  • 编辑线段与原始线段连接处的平滑过渡

实现效果

在这里插入图片描述

在这里插入图片描述

核心功能

1. 编辑模式切换

点击编辑按钮进入编辑模式,此时可以通过 Ctrl + 点击 在等值线上添加锚点。

2. 锚点管理

  • 添加锚点Ctrl + 点击 等值线上的位置,系统会找到最近的线段并在该位置添加锚点
  • 删除锚点:选中锚点后按 Delete 键删除
  • 拖拽锚点:直接拖拽锚点修改线段形状

3. 平滑度调节

提供平滑度滑块(0-10),使用 Chaikin 曲线平滑算法对编辑线段进行平滑处理。

4. 连接处平滑过渡

提供连接平滑范围滑块(0-100),确保编辑后的线段与原始线段在连接处平滑过渡。

技术实现

数据结构

// 编辑状态
const isEditMode = ref(false);                    // 是否处于编辑模式
const smoothness = ref(0);                        // 曲线平滑度
const transitionRange = ref(4);                   // 连接处平滑影响范围
const currentEditFeature = ref<Feature | null>(); // 当前编辑的原始要素
const anchorPoints = ref<number[][]>([]);         // 用户添加的锚点
const originalCoords = ref<number[][]>([]);       // 原始线段坐标(用于取消)
const editSegmentFeature = ref<Feature | null>(); // 编辑线段要素

图层管理

使用两个独立的图层:

  1. 原始图层 (vectorLayer):显示原始等值线(红色)
  2. 编辑图层 (editLayer):显示编辑线段(蓝色)和锚点(黄色圆点)
// 原始图层
vectorLayer = new VectorLayer({
  source: vectorSource,
  style: (feature) => createLineStyle(feature as Feature),
});

// 编辑图层
editLayer = new VectorLayer({
  source: editSource,
  zIndex: 100, // 确保在最上层
});

添加锚点流程

用户 Ctrl + 点击
    ↓
找到最近的等值线要素
    ↓
计算点击位置在等值线上的最近点
    ↓
添加到 anchorPoints 数组
    ↓
按在线段上的顺序排序
    ↓
更新编辑图层显示

关键代码:

function addAnchorPoint(coordinate: number[]) {
  // 遍历所有等值线要素,找到最近的线段
  for (const feature of features) {
    const geometry = feature.getGeometry();
    const closestPoint = geometry.getClosestPoint(coordinate);
    // 计算距离并找到最近的目标
  }

  // 添加锚点并排序
  anchorPoints.value.push(nearestPoint);
  sortAnchorPoints(); // 按在线段上的位置排序
  updateEditDisplay();
}

锚点排序算法

确保锚点按照在线段上的实际顺序排列:

function sortAnchorPoints() {
  // 计算每个锚点在线段上的位置比例
  const anchorPositions = anchorPoints.value.map((anchor) => {
    // 遍历线段,找到锚点所在位置的比例值
    let position = 计算锚点在线段上的位置比例(0-1);
    return { anchor, position };
  });

  // 按位置排序
  anchorPositions.sort((a, b) => a.position - b.position);
  anchorPoints.value = anchorPositions.map(item => item.anchor);
}

Chaikin 曲线平滑算法

Chaikin 算法是一种简单高效的曲线平滑方法,通过在每条线段上创建两个新点来生成更平滑的曲线。

function smoothLine(coords: number[][], iterations: number): number[][] {
  let result = [...coords];
  const iter = Math.ceil(iterations);

  for (let i = 0; i < iter; i++) {
    const newCoords = [];

    // 保留起点
    newCoords.push(result[0]);

    for (let j = 0; j < result.length - 1; j++) {
      const p0 = result[j];
      const p1 = result[j + 1];

      const factor = 0.1 + (iterations / 10) * 0.15; // 0.1 - 0.25

      // 在线段上创建两个新点
      const q = [p0[0] + factor * (p1[0] - p0[0]), p0[1] + factor * (p1[1] - p0[1])];
      const r = [p0[0] + (1 - factor) * (p1[0] - p0[0]), p0[1] + (1 - factor) * (p1[1] - p0[1])];

      newCoords.push(q, r);
    }

    // 保留终点
    newCoords.push(result[result.length - 1]);

    result = newCoords;
  }

  return result;
}

连接处平滑过渡

这是本功能的核心难点。编辑后的线段两端需要与原始线段平滑连接。

问题分析

直接拼接会导致:

  1. 位置不连续:编辑线段端点与原始线段连接点位置不一致
  2. 方向不连续:两端线段方向突变,形成尖角
解决方案

原线段过渡点参与平滑法

核心思想:在原线段上以连接点为中心,向两端各取指定数量的点,这些点与编辑锚点一起参与平滑计算,确保过渡区域平滑。

function buildSmoothSegment(
  lineCoords: number[][],
  firstAnchorIndex: number,
  lastAnchorIndex: number,
  range: number  // 用户可调节的过渡点数量
): { coords: number[][]; startOffset: number; endOffset: number } {

  // 起始端:从firstAnchorIndex向起点方向取range个点
  const startTransitionStart = Math.max(0, firstAnchorIndex - range);
  const startTransition = lineCoords.slice(startTransitionStart, firstAnchorIndex);

  // 终止端:从lastAnchorIndex向终点方向取range个点
  const endTransitionEnd = Math.min(lineCoords.length, lastAnchorIndex + range + 1);
  const endTransition = lineCoords.slice(lastAnchorIndex + 1, endTransitionEnd);

  // 构建完整线段:起始过渡点 + 编辑锚点 + 终止过渡点
  const coords = [
    ...startTransition,
    ...anchorPoints.value,
    ...endTransition
  ];

  return {
    coords,
    startOffset: startTransition.length,  // 编辑部分在平滑后数组中的起始索引
    endOffset: endTransition.length       // 编辑部分在平滑后数组中的结束偏移
  };
}
算法图解
原始线段:  P[0] --- P[1] --- P[2] --- P[3] === P[4] --- P[5] --- P[6] === P[7] --- P[8] --- P[9]
                             ↑                ↑                      ↑
                        过渡点起点         第一个锚点              最后一个锚点
                             |<-- range -->|<---- 编辑区域 ---->|<-- range -->|

参与平滑:  P[2] --- P[3] === A[0] --- A[1] --- A[2] === P[6] --- P[7]
          过渡段        编辑锚点部分              过渡段

平滑后:    P[2]' --- P[3]' === A[0]' --- A[1]' --- A[2]' === P[6]' --- P[7]'
最终拼接

保存时将平滑后的完整线段替换原线段中对应的部分:

function saveEdit() {
  // 构建新的线段坐标
  const newCoords = [];

  // 1. 添加过渡点之前的部分
  if (startTransitionStart > 0) {
    newCoords.push(...lineCoords.slice(0, startTransitionStart));
  }

  // 2. 添加平滑后的完整线段(包含过渡点 + 编辑部分 + 过渡点)
  newCoords.push(...smoothedCoords);

  // 3. 添加过渡点之后的部分
  if (endTransitionEnd < lineCoords.length) {
    newCoords.push(...lineCoords.slice(endTransitionEnd));
  }

  // 更新原始线段
  geometry.setCoordinates(newCoords);
}
用户界面
  • 平滑范围滑块(0-100):控制从连接点向两端取多少个点参与平滑

    • 值越大,过渡区域越长,连接越平滑
    • 值为0时,不进行过渡处理
  • 显示效果

    • 蓝色线段:编辑锚点部分(包含平滑后的结果)

保存修改

function saveEdit() {
  // 1. 获取原始线段坐标
  const lineCoords = geometry.getCoordinates();

  // 2. 找到锚点在原始线段上的索引位置
  const anchorIndices = findAnchorIndices(lineCoords);

  // 3. 构建用于平滑的完整线段(包含过渡点)
  const smoothSegment = buildSmoothSegment(lineCoords, firstIndex, lastIndex, range);

  // 4. 应用曲线平滑
  const smoothedCoords = smoothLine(smoothSegment.coords, smoothness.value);

  // 5. 构建新的线段坐标
  const newCoords = [
    ...lineCoords.slice(0, startTransitionStart),  // 过渡点之前的部分
    ...smoothedCoords,                              // 平滑后的完整线段
    ...lineCoords.slice(endTransitionEnd)           // 过渡点之后的部分
  ];

  // 6. 更新原始要素
  geometry.setCoordinates(newCoords);
}

实时预览

两个滑块变化时都会触发预览更新:

function applySmoothnessToEditSegment() {
  // 1. 构建用于平滑的完整线段
  const smoothSegment = buildSmoothSegment(lineCoords, firstIndex, lastIndex, range);

  // 2. 应用曲线平滑
  const smoothedCoords = smoothLine(smoothSegment.coords, smoothness.value);

  // 3. 提取编辑部分的坐标显示为蓝色线段
  const editCoords = smoothedCoords.slice(startOffset, -endOffset);
  editGeometry.setCoordinates(editCoords);
}

用户交互

操作流程

  1. 点击「编辑」按钮进入编辑模式
  2. Ctrl + 点击 等值线添加锚点(至少 2 个)
  3. 拖拽锚点修改线段形状
  4. 调节平滑度滑块预览曲线效果
  5. 调节连接平滑范围滑块预览连接效果
  6. 点击「保存修改」应用更改
  7. 或点击「取消编辑」放弃更改

快捷键

操作 快捷键
添加锚点 Ctrl + 点击
删除锚点 选中后按 Delete
拖拽锚点 直接拖拽

样式设计

元素 颜色 说明
原始等值线 红色 不可编辑状态
编辑线段 蓝色 当前编辑区域(包含平滑效果)
锚点 黄色 用户添加的控制点
拖拽中的锚点 粉红色 正在操作的锚点
拖拽中的锚点 粉红色 正在操作的锚点

性能优化

  1. 编辑图层独立:编辑操作不影响原始图层,只在保存时更新
  2. 局部更新:只更新编辑线段部分,不影响其他等值线
  3. 防抖处理:滑块拖动时实时预览,但计算量可控

扩展功能

可选实现

  1. 撤销/重做:记录操作历史,支持 Ctrl+Z / Ctrl+Y
  2. 多线段同时编辑:选中多条等值线同时编辑
  3. 锚点类型:区分平滑锚点和尖角锚点
  4. 精确输入:输入坐标值精确定位锚点

注意事项

  1. 坐标系统:确保所有坐标使用相同的投影(本实现使用 EPSG:4326)
  2. 最小锚点数:至少需要 2 个锚点才能形成线段
  3. 连接点索引:正确计算锚点在原始线段上的索引位置
  4. 取消编辑:需要恢复原始坐标,所以编辑前要保存

参考资料

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐