做地图相关的小程序时,很多团队都会遇到一个具体需求:让用户自己在地图上点两个点,马上显示出它们之间的实际地面距离,并且界面上要有一条随缩放变化的比例尺。微信小程序自带的map组件虽然能显示地图和标记,但没有开放测距接口,我们只能拿到经纬度,所有距离与比例尺的计算都得自己写。这篇文章从坐标转换、画布标尺、交互设计三个层面,把自定义测距工具的完整思路拆开讲。

经纬度到实际距离:Haversine公式的落地
地球不是一个平面,两点间最短距离要走球面大圆弧。很多初学者会直接用勾股定理算经纬度差,这种方法在赤道附近小范围勉强可用,纬度稍高或距离拉长就会偏差巨大。正确的做法是用Haversine公式,它基于球面三角函数,输入两个点的纬度和经度(十进制度数),输出球面上弧长对应的实际距离。
在微信小程序里,我们通常在js逻辑层拿到marker的经纬度。注意小程序map组件的markers属性里的经纬度是Number类型,不需要额外解析。把角度转成弧度后套公式即可。下面这段代码演示了核心计算函数,其中地球半径取6371000米,这是国际通用的平均半径。
// 将角度转为弧度
function toRad(d) {
return d * Math.PI / 180;
}
// 计算两点间距离,单位米
function getDistance(lat1, lon1, lat2, lon2) {
var R = 6371000;
var dLat = toRad(lat2 - lat1);
var dLon = toRad(lon2 - lon1);
var a = Math.sin(dLat / 2) * Math.sin(dLat / 2) +
Math.cos(toRad(lat1)) * Math.cos(toRad(lat2)) *
Math.sin(dLon / 2) * Math.sin(dLon / 2);
var c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
return R * c;
}
// 示例:北京到上海大致距离
var dist = getDistance(39.9042, 116.4074, 31.2304, 121.4737);
console.log('距离约为', dist, '米');
这个函数返回的是米,我们在页面上可以根据数值大小自动切换成千米展示。比如小于1000米时显示“米”,大于则显示“千米”并保留两位小数点。实际项目中建议把这个函数放到独立的util模块里,方便测距、围栏判断等多个功能复用。
有一点容易忽略:如果两点距离非常近(比如几米内),浮点误差可能让结果跳动。此时可以加一个阈值,小于1米直接显示“0米”或“重合”,避免用户看到奇怪的小数。另外地图坐标系必须是GCJ-02,微信小程序默认就是这个,不要混用GPS的WGS-84,否则城市级误差能到几百米。
比例尺换算:从视野跨度反推每像素米数
比例尺的本质是“屏幕上的长度对应现实中多长”。小程序map组件本身不直接告诉你当前缩放级别下每像素多少米,但我们可以通过地图可视区域四个角的经纬度来算。调用mapContext.getRegion()能拿到 northeast 和 southwest 坐标,二者纬度差对应的实际南北距离,除以地图在屏幕上渲染的高度像素,就是纵向每像素米数。
由于墨卡托投影下不同纬度横向压缩不同,严格来说横向和纵向比例不完全一致,但普通测距工具在中低缩放级别用纵向近似已经够用。下面代码展示如何在小程序里取区域并算比例尺:
// 假设已创建 mapContext
var mapContext = wx.createMapContext('myMap');
mapContext.getRegion({
success: function(res) {
var ne = res.northeast;
var sw = res.southwest;
// 南北实际距离
var realMeters = getDistance(sw.latitude, sw.longitude, ne.latitude, sw.longitude);
// 假设地图组件在wxml里高度固定为 300px
var pxHeight = 300;
var metersPerPx = realMeters / pxHeight;
console.log('每像素约', metersPerPx, '米');
}
});
拿到metersPerPx之后,我们就可以在canvas上画一条100像素的线段,标注“100 × metersPerPx 米”来当动态比例尺。用户双指缩放地图,bindregionchange事件触发后重新算一遍,标尺数字就跟着变了。相比写死不同缩放级别的对照表,这种实时反推的方式适配所有机型分辨率。
如果追求更严谨,可以分别算横向:用 northeast 和 northwest(同纬度西边界,需自行按经度差补点)距离除以组件宽度。但在手机竖屏窄视图里,纵向算法简单且误差可接受,多数自研测距插件都采用纵向近似。需要提醒,getRegion是异步回调,不要在onLoad里立刻读,要在地图渲染完成或首次regionchange后取。
交互与绘制:canvas覆盖与拖拽点设计
算出了距离和比例,下一步是让用户能点地图选点。小程序map组件上可以直接放markers,但我们还需要一条连线和一个数据面板。由于map原生层级极高,普通view盖不住它,必须用cover-view和cover-image,或者用一个绝对定位的canvas通过type="2d"接口绘制连线,再配合map的bindtap拿点击经纬度。
具体交互这样设计:用户单击地图,逻辑层记录该点经纬度并推入数组,同时在markers里加一个图标;当数组有两个点,就调用前面写的getDistance算距,并把结果写进cover-view显示的文本框。如果要在两点间画直线,由于map上不能画矢量线(polyline只能沿路网或直线但样式有限),我们可以用canvas 2d把两个点的屏幕坐标连起来,屏幕坐标由mapContext.getMapPoints或自己经纬度转像素的辅助函数得出。
// 简化:记录点并刷新测距
Page({
data: {
points: [],
distanceText: ''
},
onMapTap: function(e) {
var lat = e.detail.latitude;
var lon = e.detail.longitude;
var pts = this.data.points.concat([{ latitude: lat, longitude: lon }]);
this.setData({ points: pts });
if (pts.length >= 2) {
var a = pts[pts.length - 2];
var b = pts[pts.length - 1];
var d = getDistance(a.latitude, a.longitude, b.latitude, b.longitude);
this.setData({ distanceText: '两段距:' + (d / 1000).toFixed(2) + '千米' });
}
}
});
为了体验更好,可以允许长按marker拖拽,拖的时候在bindmarkertap和bindtouchemove里更新对应点坐标并重算距离,canvas线也跟着重绘。这种自定义测距工具比调起第三方地图App轻便,数据全在小程序内,还能和后端轨迹功能打通。整体来看,只要把经纬度转距离、区域反推比例、cover层绘制这三块打通,一个可用的测距模块就成型了。