概述

用 uniapp 开发APP,版本 vue3,HBuildX 4.75
map组件在 APP 平台(vue版)上,地图组件的点击事件无法直接返回经纬度。我们通过屏幕坐标和地图视野范围,计算出点击位置的经纬度。

示例

核心思路

将屏幕上的点击位置,按照比例映射到地图的地理坐标上。

实现步骤

第一步:获取屏幕点击坐标

当用户点击地图时,我们从触摸事件中获取点击位置在屏幕上的坐标:

  • clickX:点击位置距离屏幕左边的像素距离
  • clickY:点击位置距离屏幕顶部的像素距离

第二步:转换为地图容器内的相对坐标

地图组件在屏幕上有一个固定的位置和大小。我们需要计算点击位置相对于地图容器的坐标:

1
2
相对X = 屏幕X - 地图容器左边距离
相对Y = 屏幕Y - 地图容器顶部距离

第三步:计算比例

将相对坐标转换为比例值(0 到 1 之间):

  • ratioX = 相对X / 地图容器宽度(0 = 最左边,1 = 最右边)
  • ratioY = 相对Y / 地图容器高度(0 = 最上边,1 = 最下边)

第四步:获取地图视野范围

通过地图 API 获取当前地图显示的地理范围:

  • 西南角(左下角):{ latitude: 39.91, longitude: 116.39 }
  • 东北角(右上角):{ latitude: 39.92, longitude: 116.41 }

第五步:按比例计算经纬度

使用线性插值,根据点击位置的比例,在地图视野范围内计算对应的经纬度:

经度计算(从左到右):

1
经度 = 西边经度 + (东边经度 - 西边经度) × ratioX

纬度计算(从下到上,注意屏幕Y轴和纬度方向相反):

1
纬度 = 南边纬度 + (北边纬度 - 南边纬度) × (1 - ratioY)

举例说明

假设:

  • 屏幕点击位置:(375, 500) 像素
  • 地图容器位置:左边 32px,顶部 128px,宽度 686px,高度 822px
  • 地图视野:西南角 (39.91, 116.39),东北角 (39.92, 116.41)

计算过程:

  1. 相对坐标

    • relativeX = 375 - 32 = 343
    • relativeY = 500 - 128 = 372
  2. 比例

    • ratioX = 343 / 686 = 0.5(水平中间)
    • ratioY = 372 / 822 = 0.453(垂直中偏上)
  3. 经纬度

    • 经度 = 116.39 + (116.41 - 116.39) × 0.5 = 116.40
    • 纬度 = 39.91 + (39.92 - 39.91) × (1 - 0.453) = 39.91547

最终结果:点击位置的经纬度是 (39.91547, 116.40)

注意事项

  1. 精度:这种方法在小范围视野内(几公里)精度较好,误差通常在 10-50 米左右,对于围栏设置等场景足够使用。

  2. 纬度反转:屏幕坐标系 Y 轴向下增大,但地图纬度向上增大,所以计算纬度时需要使用 (1 - ratioY) 来反转。

  3. 线性插值:我们假设地图上两点之间的坐标变化是均匀的(线性),这在较小视野范围内是合理的近似。

总结

通过”屏幕坐标 → 相对坐标 → 比例 → 经纬度”的转换过程,我们实现了在 APP 平台上获取地图点击位置经纬度的功能。核心思想就是按比例映射:点击位置在地图上的相对位置,对应到地理坐标上的相对位置。

1
2
3
4
5
6
7
8
9
<view 
class="outgoing-map-container"
@tap="handleContainerTap"
>
<map :longitude="currentLongitude" :latitude="currentLatitude" :scale="currentScale" :markers="markers"
:show-location="showLocation" :include-points="includePoints" :circles="circles" :polyline="polyline"
:polygons="polygons" class="map" @tap="handleMapTap"
@markertap="handleMarkerTap" @poitap="handleMapTap" :enable-zoom="true" :enable-scroll="true" id="map"></map>
</view>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
handleContainerTap(e) {
console.log('xxxx',e)
// 如果未启用该功能,直接返回
if (!this.enableAppTapLocation) return

if (!this.mapContext) {
console.warn('地图上下文未初始化')
return
}

// 尝试从事件对象获取坐标
// 注意:APP 平台的 @tap 事件可能没有坐标信息,需要从 touches/changedTouches 获取
let clickX, clickY

// 方法1:尝试从 detail 中获取(小程序可能有)
if (e.detail && typeof e.detail.x === 'number' && typeof e.detail.y === 'number') {
clickX = e.detail.x
clickY = e.detail.y
}
// 方法2:尝试从 changedTouches 获取(触摸结束时可能有)
else if (e.changedTouches && e.changedTouches.length > 0) {
const touch = e.changedTouches[0]
clickX = touch.clientX || touch.pageX
clickY = touch.clientY || touch.pageY
}

// 如果成功获取坐标,处理点击
if (typeof clickX === 'number' && typeof clickY === 'number') {
this.processTapLocation(clickX, clickY)
} else {
console.warn('无法获取点击坐标,@tap 事件对象:', e)
}
},
// 处理点击位置(计算经纬度并触发事件)
processTapLocation(clickX, clickY) {
// 获取地图视野范围
this.mapContext.getRegion({
success: async (res) => {
try {
// 计算点击位置的经纬度
const coords = await this.screenToLatLng(
clickX,
clickY,
res
)

if (coords) {
console.log('=== 地图点击坐标计算 ===')
console.log('屏幕坐标:', { x: clickX, y: clickY })
console.log('地图视野范围:', res)
console.log('计算的经纬度:', coords)

// 触发点击事件,格式与小程序保持一致
this.$emit('map-tap', {
detail: {
latitude: coords.latitude,
longitude: coords.longitude
}
})
} else {
console.warn('坐标转换失败')
}
} catch (err) {
console.error('处理地图点击失败', err)
}
},
fail: (err) => {
console.error('获取地图视野范围失败', err)
}
})
},
// 将屏幕坐标转换为经纬度
screenToLatLng(screenX, screenY, region) {
return new Promise((resolve) => {
if (!region || !region.northeast || !region.southwest) {
console.warn('地图视野范围数据无效', region)
resolve(null)
return
}

// 获取地图容器的位置和尺寸
const query = uni.createSelectorQuery().in(this)
query.select('.map').boundingClientRect((rect) => {
if (!rect) {
console.warn('获取地图容器位置失败')
resolve(null)
return
}

// 计算相对于地图容器的坐标
const relativeX = screenX - rect.left
const relativeY = screenY - rect.top

// 验证坐标是否在地图范围内
if (relativeX < 0 || relativeX > rect.width || relativeY < 0 || relativeY > rect.height) {
console.warn('点击坐标超出地图范围', { relativeX, relativeY, width: rect.width, height: rect.height })
resolve(null)
return
}

// 计算相对比例(0-1)
const ratioX = relativeX / rect.width
const ratioY = relativeY / rect.height

// 获取边界坐标
const sw = region.southwest // 西南角(左下角)
const ne = region.northeast // 东北角(右上角)

// 线性插值计算经纬度
// 纬度:从下往上(sw.latitude -> ne.latitude),所以是 1 - ratioY
const lat = sw.latitude + (ne.latitude - sw.latitude) * (1 - ratioY)

// 经度:从左往右(sw.longitude -> ne.longitude),所以是 ratioX
const lng = sw.longitude + (ne.longitude - sw.longitude) * ratioX

// 验证坐标有效性
if (typeof lat !== 'number' || typeof lng !== 'number' ||
isNaN(lat) || isNaN(lng) ||
lat < -90 || lat > 90 || lng < -180 || lng > 180) {
console.warn('计算的坐标无效', { lat, lng })
resolve(null)
return
}

resolve({ latitude: lat, longitude: lng })
}).exec()
})
},