WebGL 地图上做精准编辑?这套分层方案搞定管网拖拽 / 连接

在 L7 地图上实现拓扑编辑:SVG 叠加层与坐标同步的实战

如何在 WebGL 地图上叠加 SVG 编辑层?如何实现地图移动时图形跟随?这篇文章分享我们项目中 L7 + draw2d 的集成方案。

效果图

背景

智慧供热系统不仅要展示管网,还要支持编辑------拖拽设备、连接管道、修改属性。

L7 是基于 WebGL 的地图引擎,擅长大规模数据渲染。但 WebGL 不适合交互编辑------我们需要的是 SVG 级别的精确控制。

解决方案:在 L7 地图上叠加一个 SVG 层,使用 draw2d.js 处理编辑逻辑。

架构设计

bash 复制代码
draw/
├── l7AndDraw2d.ts           # SVG 叠加层创建 + L7 事件同步
├── createDrawCanvas.ts      # 画布初始化
├── draw2dManager.ts         # 编辑管理器
├── bindEventOfEquipment.ts  # 设备事件绑定
├── coordHelper.ts           # 坐标转换
├── loadDraw2d.ts            # draw2d.js 延迟加载
└── topologyDrawL7/          # 拓扑数据渲染
    ├── index.ts
    ├── device.ts
    └── line.ts

核心思路

  1. L7 负责地图渲染(底图 + 管网展示)
  2. SVG 叠加层负责编辑交互(拖拽、连接)
  3. 两层通过坐标转换保持同步

核心实现

1. 创建 SVG 叠加层

在 L7 地图容器上叠加一个 div,draw2d.js 在这个 div 上渲染 SVG:

typescript 复制代码
export default function createL7DrawLayer(scene: Scene): Promise<{ canvas: any; overlay: HTMLDivElement }> {
  return new Promise((resolve, reject) => {
    const container = scene.getContainer();
    
    // 创建叠加层
    const overlayEl = document.createElement('div');
    overlayEl.id = 'l7-draw2d-overlay';
    overlayEl.style.cssText = `
      position: absolute;
      top: 0; left: 0;
      width: 100%; height: 100%;
      z-index: 10;
      pointer-events: none;  // 默认穿透,让地图响应事件
    `;
    container.appendChild(overlayEl);
    
    // 初始化 draw2d Canvas
    const canvas = new window.draw2d.Canvas(overlayEl.id);
    
    // 监听 L7 地图事件
    scene.on('zoomstart', onZoomStart);
    scene.on('zoomend', onZoomEnd);
    scene.on('mapmove', onMapMove);
    scene.on('moveend', onMoveEnd);
    
    resolve({ canvas, overlay: overlayEl });
  });
}

关键点pointer-events: none 让空白区域穿透到地图,只有 SVG 图形响应鼠标。

2. 地图移动时的坐标同步

用户拖动地图时,SVG 元素需要跟随移动。有两种策略:

策略一:实时偏移(移动中)

移动过程中,通过 CSS transform 偏移整个 SVG 层,避免频繁重绘:

typescript 复制代码
function onMapMove() {
  if (!isMoving) {
    // 记录起始坐标
    moveStartLngLat = [scene.getCenter().lng, scene.getCenter().lat];
    moveStartScreen = scene.lngLatToContainer(moveStartLngLat);
    isMoving = true;
    return;
  }

  // 计算偏移量
  const currentScreen = scene.lngLatToContainer(moveStartLngLat);
  const deltaX = currentScreen.x - moveStartScreen.x;
  const deltaY = currentScreen.y - moveStartScreen.y;

  // CSS transform 偏移
  overlayEl.style.transform = `translate(${deltaX}px, ${deltaY}px)`;
}

策略二:精确重绘(移动结束)

移动结束后,重新计算每个元素的精确屏幕坐标:

typescript 复制代码
function onMoveEnd() {
  isMoving = false;
  updatePositions();
}

function updatePositions() {
  const figures = canvas.getFigures().data;
  const lines = canvas.getLines().data;

  // 重置 transform
  overlayEl.style.transform = '';

  // 更新每个元素的屏幕坐标
  [...figures, ...lines].forEach((item) => {
    if (item.cssClass !== 'draw2d_Connection') {
      // 设备:根据地理坐标重新定位
      const coord = item.userData?.writer?.coord;  // Web Mercator 坐标
      if (coord) {
        const screen = webMercatorToScreen(scene, coord);
        item.setPosition(screen.x - OFFSET, screen.y - OFFSET);
      }
    } else {
      // 管道:根据坐标数组重新定位
      const coords = item.userData?.writer?.coord;
      if (coords) {
        const vertices = coords.map(c => {
          const screen = webMercatorToScreen(scene, c);
          return { x: screen.x, y: screen.y };
        });
        item.setVertices(vertices);
      }
    }
  });
}

3. 坐标转换

L7 使用经纬度,draw2d 使用屏幕像素。需要双向转换:

typescript 复制代码
import { webMercatorToCoord, coordToWebMercator } from '@/tools/tool/tool';

/** Web Mercator → 屏幕坐标 */
export function webMercatorToScreen(scene: Scene, coord: [number, number]) {
  const [lng, lat] = webMercatorToCoord(coord);  // 转经纬度
  return scene.lngLatToContainer([lng, lat]);    // 转屏幕坐标
}

/** 屏幕坐标 → Web Mercator */
export function screenToWebMercator(scene: Scene, x: number, y: number) {
  const lngLat = scene.containerToLngLat([x, y]);  // 屏幕转经纬度
  return coordToWebMercator([lngLat.lng, lngLat.lat]);  // 转 Web Mercator
}

为什么用 Web Mercator 而不是经纬度?

  • 项目中拓扑数据统一使用 Web Mercator 坐标系
  • 避免经纬度计算时的精度问题

4. 拖拽时禁用地图交互

用户拖拽设备时,不能让地图跟着移动:

typescript 复制代码
item.installEditPolicy(
  new window.draw2d.policy.figure.DragDropEditPolicy({
    onDragStart: function () {
      // 禁用地图交互
      scene.setMapStatus({ dragEnable: false, zoomEnable: false });
      // SVG 层全区域接收鼠标,防止快速拖出导致事件丢失
      overlayEl.style.pointerEvents = 'auto';
    },
    
    onDragEnd: function (canvas, figure, x, y) {
      // 恢复地图交互
      scene.setMapStatus({ dragEnable: true, zoomEnable: true });
      overlayEl.style.pointerEvents = 'none';
      
      // 更新设备地理坐标
      const device = findDevice(figure.id);
      const newCoord = screenToWebMercator(scene, x, y);
      device.latLng = newCoord;
      
      // 更新画布元素存储的坐标(供后续移动同步使用)
      figure.userData.writer.coord = newCoord;
    },
  })
);

5. 缩放时的优化

缩放时如果实时更新位置,会非常卡顿。优化策略:

typescript 复制代码
function onZoomStart() {
  isZooming = true;
  hideDrawLayer();  // 隐藏 SVG 层
}

function onZoomEnd() {
  // 防抖:用户停止缩放 200ms 后才恢复
  setTimeout(() => {
    isZooming = false;
    updatePositions();  // 重新计算位置
    showDrawLayer();    // 显示 SVG 层
  }, 200);
}

6. draw2d.js 的延迟加载

draw2d.js 使用 AMD 模块系统,可能与项目中其他 AMD loader(如 dojo)冲突:

typescript 复制代码
export function loadDraw2d(): Promise<void> {
  if (window.draw2d) return Promise.resolve();

  return new Promise((resolve, reject) => {
    // 临时屏蔽全局 AMD
    const _define = window.define;
    const _require = window.require;
    delete window.define;
    delete window.require;

    const script = document.createElement('script');
    script.src = '/static/js/draw2d.js';
    script.onload = () => {
      // 恢复 AMD 环境
      if (_define) window.define = _define;
      if (_require) window.require = _require;
      resolve();
    };
    script.onerror = reject;
    document.body.append(script);
  });
}

完整流程

css 复制代码
用户进入编辑模式
    ↓
加载 draw2d.js(延迟加载)
    ↓
创建 SVG 叠加层
    ↓
初始化 draw2d Canvas
    ↓
将拓扑数据转为 draw2d 格式渲染
    ↓
绑定设备事件(拖拽、连接、删除)
    ↓
监听 L7 地图事件(移动、缩放)
    ↓
坐标同步更新

使用示例

typescript 复制代码
import CreateDrawCanvas, { getDraw2dManager } from './draw/createDrawCanvas';
import { DrawDataL7 } from './draw/topologyDrawL7';

// 1. 创建编辑画布
const [canvas, overlay] = await CreateDrawCanvas(scene);

// 2. 渲染拓扑数据到画布
DrawDataL7(scene, writeArea, canvas);

// 3. 获取管理器进行操作
const manager = getDraw2dManager();
manager.rotateEquipmentFromOutside('device-001', 45);  // 旋转设备
manager.removeEquipmentFromOutside({ type: 'device', tpId: 'device-001' });  // 删除设备

踩过的坑

坑一:pointer-events 导致地图无法响应

SVG 叠加层默认会拦截所有鼠标事件。解决:设置 pointer-events: none,只让 SVG 图形响应。

坑二:快速拖拽导致事件丢失

用户快速拖拽时,鼠标可能移出 SVG 图形范围。解决:拖拽开始时将整个叠加层设为 pointer-events: auto

坑三:缩放时 SVG 闪烁

缩放过程中频繁更新位置导致闪烁。解决:缩放时隐藏 SVG 层,结束后再显示。

坑四:AMD loader 冲突

draw2d.js 的 AMD loader 与 dojo 冲突。解决:加载前临时屏蔽全局 define/require。

总结

这套方案的核心是分层架构

  • L7 负责地图渲染(WebGL)
  • draw2d 负责编辑交互(SVG)
  • 通过坐标转换保持同步

优点:

  • 充分利用 WebGL 的渲染性能
  • SVG 编辑交互更精确、更灵活
  • 两层解耦,可独立优化

缺点:

  • 需要维护两层同步
  • 坐标转换有性能开销
  • AMD loader 兼容性问题

相关链接

相关推荐
山海鲸可视化1 天前
数字孪生项目案例 | 物流园区可视化
webgl·可视化·数据可视化·数据表格·搜索框
图扑软件2 天前
50ms 级实时数字孪生|汽车先进制造车间工艺流程
3d·数据采集·webgl·数字孪生·可视化·opc ua·汽车制造
子兮曰2 天前
SuperSplat 深度解析:7.6K Stars 的浏览器端 3D 高斯泼溅编辑器 — 在 Web 上编辑现实
前端·javascript·webgl
丷丩2 天前
我正用AI Agent重构传统GIS 核心功能,说大白话做空间分析
人工智能·gis·geoai
丷丩4 天前
策略模式实战:GeoAI-UP中MVT发布器的可扩展架构设计
人工智能·架构·gis·策略模式·空间分析·geoai
WebGIS开发5 天前
地理学硕士转行GIS开发经历分享
gis·webgis·地理学
supermapsupport5 天前
SuperMap iClient3D for WebGL 根据实体高度进行差异化颜色渲染
webgl
HYCS6 天前
用pixijs实现fabricjs(一):FakeCanvasRenderingContext2D
javascript·webgl·canvas
qiao若huan喜7 天前
14、webgl 基本概念 + 图形变换
webgl