在地图上叠加自定义底图:CAD 图纸与特殊区域地图的实战方案
背景
在一次项目交付中,客户提出了一个特殊需求:他们有厂区的 CAD 图纸,希望直接叠加在我们的地图系统上,而不是重新测绘。还有的客户在某些区域有更高精度的专用地图(比如园区内部地图),需要作为底图使用。
这类需求的核心问题是:如何让一张静态图片与地图坐标系统对齐?
本文分享我们在 AntV L7 上的实现方案——通过可视化交互让用户"拖拽对齐"图片与地图。
效果图
整体思路
实现自定义底图叠加,需要解决三个核心问题:
- 图片如何叠加到地图上:使用绝对定位的
<img>元素,通过 CSS 控制位置和大小 - 如何确定图片的地理范围:用户通过拖拽、缩放操作,可视化地确定图片覆盖的经纬度范围
- 地图操作时图片如何跟随:监听地图事件,实时同步图片位置
核心实现
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 事件中重新计算位置会有性能问题——拖动时图片会闪烁。
我们的解决方案是:
- 缩放时:隐藏图片,缩放结束后重新计算位置并显示
- 平移时:通过 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]; // 默认中心点
}
用户可以:
- 上传图片(支持预览)
- 调整图片透明度
- 隐藏底图(纯图片模式)
- 设置默认的地图中心点和缩放等级
实际效果
用户操作流程:
- 上传 CAD 图纸或区域地图
- 在预览地图中拖动、缩放图片,使其与底图对齐
- 系统自动计算并保存图片的地理范围
- 后续访问时,图片会自动定位到正确的地理位置
小结
这个方案的核心价值在于:让非专业用户也能完成图片与地图的对齐工作。用户不需要了解坐标系统,只需要像操作普通图片一样拖拽即可。
技术上,我们通过以下手段实现了流畅的交互体验:
- 三种坐标系统的双向转换
- TranslateBox 组件封装拖拽交互
- 缩放时隐藏、平移时 CSS 偏移的同步策略
- 双击快速定位的便捷操作
完整代码已开源:GitHub - topo-l7map