地图上叠加自定义图片(CAD图纸或高精度局部地图等)

0 阅读5分钟

在地图上叠加自定义底图:CAD 图纸与特殊区域地图的实战方案

背景

在一次项目交付中,客户提出了一个特殊需求:他们有厂区的 CAD 图纸,希望直接叠加在我们的地图系统上,而不是重新测绘。还有的客户在某些区域有更高精度的专用地图(比如园区内部地图),需要作为底图使用。

这类需求的核心问题是:如何让一张静态图片与地图坐标系统对齐?

本文分享我们在 AntV L7 上的实现方案——通过可视化交互让用户"拖拽对齐"图片与地图。

效果图

2026-05-14-17-34-43_高压缩.awebp

整体思路

实现自定义底图叠加,需要解决三个核心问题:

  1. 图片如何叠加到地图上:使用绝对定位的 <img> 元素,通过 CSS 控制位置和大小
  2. 如何确定图片的地理范围:用户通过拖拽、缩放操作,可视化地确定图片覆盖的经纬度范围
  3. 地图操作时图片如何跟随:监听地图事件,实时同步图片位置

核心实现

1. 图片叠加层组件

我们封装了一个 PictureMap 组件,核心结构如下:

<template>
  <div class="pictureMap">
    <!-- L7 地图 -->
    <L7MapRender
      ref="l7MapRenderRef"
      :render-name="[]"
      @scene-ready="onSceneReady"
    />

    <!-- 图片叠加层,使用 TranslateBox 实现可拖拽 -->
    <TranslateBox
      ref="translateBoxObj"
      :class-name="'custom-map-layer'"
      :width="0"
      :height="0"
      :close-rotate="true"
      :open-wheel-spread="true"
      @endMove="translateEndHandle"
    >
      <img
        :style="{ opacity: props.opacity ?? 1 }"
        :src="props.imgUrl"
      >
    </TranslateBox>
  </div>
</template>

这里的关键是 TranslateBox 组件——这是我们之前封装的通用变换盒子,支持拖拽移动和滚轮缩放。用户可以通过它直观地调整图片的位置和大小。(TranslateBox 组件介绍

2. 坐标系统转换

要让图片与地图对齐,需要在三种坐标系统之间转换:

  • 经纬度坐标(LngLat):地理坐标,如 [116.4, 39.9]
  • Web Mercator 坐标:投影坐标,适合计算距离和面积
  • 屏幕坐标(Screen):像素坐标,用于 DOM 元素定位

转换流程:

经纬度 → Web Mercator → 屏幕坐标 → DOM 元素位置

核心转换函数:

// 经纬度转 Web Mercator
export function coordToWebMercator(coord: [number, number]): [number, number] {
  const [lng, lat] = coord;
  const x = lng * 20037508.34 / 180;
  const y = Math.log(Math.tan((90 + lat) * Math.PI / 360)) / (Math.PI / 180) * 20037508.34 / 180;
  return [x, y];
}

// Web Mercator 转经纬度
export function webMercatorToCoord(web: [number, number]): [number, number] {
  const [x, y] = web;
  const lng = x * 180 / 20037508.34;
  const lat = Math.atan(Math.exp(y * Math.PI / 20037508.34)) * 360 / Math.PI - 90;
  return [lng, lat];
}

// Web Mercator 转屏幕坐标(依赖 L7 Scene)
export function webMercatorToScreen(
  scene: Scene,
  webX: number,
  webY: number
): { x: number; y: number } {
  const lngLat = webMercatorToCoord([webX, webY]);
  const pixel = scene.lngLatToContainer({ lng: lngLat[0], lat: lngLat[1] });
  return { x: pixel.x, y: pixel.y };
}

3. 图片位置计算

图片的地理范围用四个边界值表示:

interface ImgCoord {
  xmin: number; // 西边界经度
  xmax: number; // 东边界经度
  ymin: number; // 南边界纬度
  ymax: number; // 北边界纬度
}

根据这个范围,计算图片在屏幕上的位置:

export function getTranslateValue(
  pictureWeb: {
    leftTop: [number, number],      // 左上角墨卡托坐标
    rightBottom: [number, number]   // 右下角墨卡托坐标
  },
  scene: Scene
) {
  const leftTopPoint = webMercatorToScreen(scene, pictureWeb.leftTop);
  const rightBottomPoint = webMercatorToScreen(scene, pictureWeb.rightBottom);

  return {
    left: leftTopPoint.x,
    top: leftTopPoint.y,
    width: rightBottomPoint.x - leftTopPoint.x,
    height: rightBottomPoint.y - leftTopPoint.y,
  };
}

4. 用户拖拽后的坐标更新

当用户拖动或缩放图片后,需要反向计算新的地理范围:

function translateEndHandle(res: EmitMoveObj) {
  if (!scene.value) return;

  // 屏幕坐标转墨卡托坐标
  const leftTopPoint: [number, number] = [res.left, res.top];
  const rightBottomPoint: [number, number] = [res.left + res.width, res.top + res.height];

  pictureWeb.leftTop = screenToWebMercator(scene.value, leftTopPoint[0], leftTopPoint[1]);
  pictureWeb.rightBottom = screenToWebMercator(scene.value, rightBottomPoint[0], rightBottomPoint[1]);

  // 墨卡托转经纬度,保存结果
  const pictureLngLat = {
    leftTop: webMercatorToCoord(pictureWeb.leftTop),
    rightBottom: webMercatorToCoord(pictureWeb.rightBottom),
  };

  const extent = {
    xmin: pictureLngLat.leftTop[0],
    xmax: pictureLngLat.rightBottom[0],
    ymin: pictureLngLat.rightBottom[1],
    ymax: pictureLngLat.leftTop[1],
  };

  emit('update:pictureEdge', extent);
}

5. 地图操作时的图层同步

这是最棘手的部分。当用户缩放或平移地图时,图片需要跟随移动。但直接在每次 mapmove 事件中重新计算位置会有性能问题——拖动时图片会闪烁。

我们的解决方案是:

  1. 缩放时:隐藏图片,缩放结束后重新计算位置并显示
  2. 平移时:通过 CSS left/top 偏移实现"伪跟随",平移结束后重置偏移并重新计算
// customMapLayerSync.ts 核心逻辑

function onZoomStart(selector: string) {
  isZooming = true;
  hideAll(selector);  // 缩放开始时隐藏
}

function onZoomEnd(selector: string) {
  setTimeout(() => {
    isZooming = false;
    resetOffsetAll(selector);
    showAll(selector);  // 缩放结束后显示
  }, 200);
}

function onMapMove(scene: Scene, selector: string) {
  if (!isMoving) {
    // 记录起始点
    const center = scene.getCenter();
    moveStartLngLat = [center.lng, center.lat];
    moveStartScreen = scene.lngLatToContainer(moveStartLngLat);
    return;
  }

  // 计算偏移量:同一点在屏幕上的位置变化 = 视口偏移
  const currentScreen = scene.lngLatToContainer(moveStartLngLat);
  const deltaX = currentScreen.x - moveStartScreen.x;
  const deltaY = currentScreen.y - moveStartScreen.y;

  setOffsetAll(selector, deltaX, deltaY);  // CSS 偏移
}

function onMoveEnd(selector: string) {
  isMoving = false;
  if (isZooming) return;  // 缩放期间等 zoomend 处理
  resetOffsetAll(selector);
}

使用时只需注册:

import { registerCustomMapLayerSync, unregisterCustomMapLayerSync } from './customMapLayerSync';

function onSceneReady(instance: L7MapInstance) {
  scene.value = instance.scene;
  registerCustomMapLayerSync(scene.value);  // 注册同步
}

onBeforeUnmount(() => {
  unregisterCustomMapLayerSync(scene.value);  // 销毁时注销
});

6. 双击快速定位

为了方便用户操作,我们支持双击地图将图片中心快速移动到该位置:

function onDblClick(e: MouseEvent) {
  if (!scene.value) return;

  // 获取双击点的经纬度
  const lngLat = scene.value.containerToLngLat({
    x: e.clientX,
    y: e.clientY
  });

  // 计算偏移量并更新图片位置
  const newCenter = coordToWebMercator([lngLat.lng, lngLat.lat]);
  const oldCenter = [
    (pictureWeb.rightBottom[0] + pictureWeb.leftTop[0]) / 2,
    (pictureWeb.rightBottom[1] + pictureWeb.leftTop[1]) / 2
  ];

  const addValueX = newCenter[0] - oldCenter[0];
  const addValueY = newCenter[1] - oldCenter[1];

  pictureWeb.leftTop[0] += addValueX;
  pictureWeb.rightBottom[0] += addValueX;
  pictureWeb.leftTop[1] += addValueY;
  pictureWeb.rightBottom[1] += addValueY;

  forceUpdate();
  emit('update:pictureEdge', newExtent);
}

配置界面

完整的配置界面支持以下功能:

interface OtherJsonObjType {
  isCustomImage: boolean;      // 是否启用自定义底图
  imgUrl: string;              // 图片 URL
  imgCoord: ImgCoord;          // 图片地理范围
  hideMap: boolean;            // 是否隐藏底图(只显示自定义图片)
  opacity: number;             // 图片透明度
  isCustomMapInit: boolean;    // 是否自定义地图初始状态
  defaultMapGrade: number;     // 默认缩放等级
  defaultMapCenter: [number, number];  // 默认中心点
}

用户可以:

  • 上传图片(支持预览)
  • 调整图片透明度
  • 隐藏底图(纯图片模式)
  • 设置默认的地图中心点和缩放等级

实际效果

用户操作流程:

  1. 上传 CAD 图纸或区域地图
  2. 在预览地图中拖动、缩放图片,使其与底图对齐
  3. 系统自动计算并保存图片的地理范围
  4. 后续访问时,图片会自动定位到正确的地理位置

小结

这个方案的核心价值在于:让非专业用户也能完成图片与地图的对齐工作。用户不需要了解坐标系统,只需要像操作普通图片一样拖拽即可。

技术上,我们通过以下手段实现了流畅的交互体验:

  • 三种坐标系统的双向转换
  • TranslateBox 组件封装拖拽交互
  • 缩放时隐藏、平移时 CSS 偏移的同步策略
  • 双击快速定位的便捷操作

完整代码已开源:GitHub - topo-l7map