clxx 3.0.1 → 3.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +49 -3
- package/README.md +6 -3
- package/build/Alert/style.js +15 -14
- package/build/CarouselNotice/style.js +2 -1
- package/build/CitySelect/data.d.ts +1 -1
- package/build/CitySelect/data.js +2277 -1674
- package/build/CitySelect/index.d.ts +2 -1
- package/build/CitySelect/index.js +35 -9
- package/build/CitySelect/style.js +52 -48
- package/build/Container/index.js +21 -1
- package/build/DatePicker/style.d.ts +1 -1
- package/build/DatePicker/style.js +39 -35
- package/build/Indicator/index.js +2 -1
- package/build/Loading/Wrapper.js +3 -2
- package/build/Loading/style.js +7 -6
- package/build/MapLocationSelection/buildSelectedLocation.d.ts +16 -0
- package/build/MapLocationSelection/buildSelectedLocation.js +123 -0
- package/build/MapLocationSelection/createProvider.d.ts +8 -0
- package/build/MapLocationSelection/createProvider.js +33 -0
- package/build/MapLocationSelection/getLocation.d.ts +8 -0
- package/build/MapLocationSelection/getLocation.js +112 -0
- package/build/MapLocationSelection/index.d.ts +16 -0
- package/build/MapLocationSelection/index.js +985 -0
- package/build/MapLocationSelection/loader.amap.d.ts +48 -0
- package/build/MapLocationSelection/loader.amap.js +125 -0
- package/build/MapLocationSelection/loader.bmap.d.ts +8 -0
- package/build/MapLocationSelection/loader.bmap.js +60 -0
- package/build/MapLocationSelection/provider.amap.d.ts +38 -0
- package/build/MapLocationSelection/provider.amap.js +659 -0
- package/build/MapLocationSelection/provider.bmap.d.ts +36 -0
- package/build/MapLocationSelection/provider.bmap.js +837 -0
- package/build/MapLocationSelection/provider.d.ts +45 -0
- package/build/MapLocationSelection/provider.js +10 -0
- package/build/MapLocationSelection/style.d.ts +4 -0
- package/build/MapLocationSelection/style.js +442 -0
- package/build/MapLocationSelection/types.d.ts +29 -0
- package/build/MapLocationSelection/types.js +22 -0
- package/build/MapLocationSelection/userMarker.d.ts +2 -0
- package/build/MapLocationSelection/userMarker.js +95 -0
- package/build/RegionPicker/data.js +974 -992
- package/build/RegionPicker/index.d.ts +3 -2
- package/build/RegionPicker/index.js +29 -10
- package/build/RegionPicker/style.js +54 -49
- package/build/ScrollView/style.js +5 -4
- package/build/Toast/style.js +6 -5
- package/build/index.d.ts +3 -0
- package/build/index.js +8 -2
- package/build/utils/rem.d.ts +1 -0
- package/build/utils/rem.js +48 -0
- package/package.json +2 -2
- package/test/src/city-select/index.jsx +28 -15
- package/test/src/index/index.jsx +5 -0
- package/test/src/index.jsx +1 -0
- package/test/src/map-location-selection/index.jsx +192 -0
- package/test/src/region-picker/index.jsx +29 -21
|
@@ -0,0 +1,837 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// 百度地图 BMapGL Provider 实现(v5.3 — 反查切到 webservice + 内置 Geolocation)。
|
|
3
|
+
//
|
|
4
|
+
// 设计目标:
|
|
5
|
+
// - 完全依赖百度 JSAPI(BMap.LocalSearch + BMap.Geocoder),不再调用任何
|
|
6
|
+
// Web 服务 API(reverse_geocoding/v3、place/v2/search);
|
|
7
|
+
// - 不需要 referer 白名单之外的额外鉴权(共用 JSAPI 的 ak),不需要同域代理;
|
|
8
|
+
// - 代码量大幅缩减(从 ~1180 行降到 ~700 行),无 jsonp、proxy、跨源 fetch 链路;
|
|
9
|
+
// - 与高德 PlaceSearch.searchNearBy 全大类召回的实现风格对齐:周边搜索走
|
|
10
|
+
// "多关键字数组覆盖 + 距离 dedupe",关键字搜索走单次 LocalSearch.search
|
|
11
|
+
// + forceLocal: false(同城精确命中 + 本城无命中时 SDK 自动回退全国)。
|
|
12
|
+
//
|
|
13
|
+
// 关键差异(与历史版本):
|
|
14
|
+
// - 弃用 reverse_geocoding/v3:JSAPI Geocoder.getLocation 已经覆盖反查 + 周边
|
|
15
|
+
// POI(surroundingPois)。后者精度低于 Web API 但与 LocalSearch fan-out 互补,
|
|
16
|
+
// 不再单独使用;
|
|
17
|
+
// - 弃用 place/v2/search 的多关键字 / 全国通道:使用 LocalSearch.search +
|
|
18
|
+
// forceLocal: false 替代——纯 JSAPI 同样能拿到"上海搜北京天安门"的跨城命中,
|
|
19
|
+
// 与高德 / 微信"发送位置"的搜索体感对齐;
|
|
20
|
+
// - **v5.2 重构**:利用百度官方文档明确支持的 LocalSearch.searchNearby
|
|
21
|
+
// **keyword 数组形式**(一次最多 10 个关键字),把原来"21 个独立 LocalSearch
|
|
22
|
+
// 实例并发"压成"3 组多关键字 LocalSearch 并发"——HTTP 请求数从 21 → 3,
|
|
23
|
+
// 绕开浏览器单域 6 个并发连接的限制(之前 21 并发会被分批排队,部分串行化)。
|
|
24
|
+
// onSearchComplete 在多关键字时返回 LocalResult[] 数组,runLocalSearch
|
|
25
|
+
// 已统一兼容单 / 多关键字两种回调格式。
|
|
26
|
+
//
|
|
27
|
+
// **v5.3 追加变更**(解决"百度地图不返回省市区 code"):
|
|
28
|
+
// - **reverseGeocode 主路径切到 webservice `reverse_geocoding/v3`** + jsonp:
|
|
29
|
+
// BMapGL JSAPI 的 AddressComponent 类参考文档明确只有 streetNumber / street /
|
|
30
|
+
// district / city / province 5 个字段,**不含 adcode**——这与高德 JSAPI
|
|
31
|
+
// 端能直接拿到 adcode 形成跛脚。webservice `reverse_geocoding/v3` 的
|
|
32
|
+
// addressComponent 包含 country / province / city / district / town /
|
|
33
|
+
// adcode(int 类型)等完整字段,且与 JSAPI 共用同一个 ak(referer 白
|
|
34
|
+
// 名单一致),用 jsonp 在浏览器直调即可,无需服务端代理;
|
|
35
|
+
// - **geolocate 切到 BMapGL.Geolocation**:原 navigator.geolocation +
|
|
36
|
+
// BMapGL.Convertor 两步式被官方内置 Geolocation 类一次替代——内部融合
|
|
37
|
+
// 浏览器 H5 定位 + IP 定位 + 安卓 SDK 辅助定位,自动产出 BD09,不再需要
|
|
38
|
+
// COORDINATES_WGS84/BD09 常量与 Convertor 实例。
|
|
39
|
+
//
|
|
40
|
+
// 与高德相比的关键差异(仍然适用):
|
|
41
|
+
// - 没有"无关键字周边检索" JSAPI:百度 LocalSearch.searchNearby 必须给 keyword,
|
|
42
|
+
// 所以 searchAround 必须 fan-out 多个关键字;
|
|
43
|
+
// - click 事件携带的是 e.latlng / e.pixel,e.point 在部分版本下是 EarthMC 米数。
|
|
44
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
45
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
46
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
47
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
48
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
49
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
50
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
51
|
+
});
|
|
52
|
+
};
|
|
53
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
54
|
+
exports.BMapProvider = void 0;
|
|
55
|
+
const loader_bmap_1 = require("./loader.bmap");
|
|
56
|
+
const jsonp_1 = require("../utils/jsonp");
|
|
57
|
+
// 用户当前位置 marker 的静态 SVG。
|
|
58
|
+
//
|
|
59
|
+
// 之所以在百度端不用 DOM + CSS keyframes / 涟漪(高德端 createUserMarkerDom 那套):
|
|
60
|
+
// - BMapGL.CustomOverlay 在当前 SDK 版本下渲染锚点行为不稳定(实测 anchors / offsetX /
|
|
61
|
+
// offsetY 都被静默忽略,与 map.centerAndZoom 同一坐标对不齐),所以必须用
|
|
62
|
+
// BMapGL.Marker + BMapGL.Icon —— 它跟 map.setCenter / centerAndZoom 共享同一套坐标
|
|
63
|
+
// 转换,能保证 marker 中心与 centerPin 针尖(地图几何中心)严格重合;
|
|
64
|
+
// - BMapGL.Icon 通过 dataURL 加载 SVG 时走的是 <img> 静态资源路径,浏览器把 SVG 当
|
|
65
|
+
// 普通图像,里面的 <animate>(SMIL)不会播放 —— 涟漪做不出来,故直接做静态点。
|
|
66
|
+
//
|
|
67
|
+
// 视觉规格:相对高德端 .mls-user-loc__dot(外径 12 px = 蓝色 9 + 白边 1.5)整体放大约 33%,
|
|
68
|
+
// 因为百度端没有涟漪辅助识别,单靠蓝点显眼度略弱,所以略大于高德但不至于喧宾夺主。
|
|
69
|
+
//
|
|
70
|
+
// 在 viewBox=64 中等比设计:r=24 + stroke=8(描边居中,半内半外)→ 外半径 28;
|
|
71
|
+
// CSS size=16 时 scale = 16/64 = 0.25:
|
|
72
|
+
// 蓝点直径(fill 区) = 2*(24-4)*0.25 = 10 CSS px
|
|
73
|
+
// 外径(含 stroke) = 2*(24+4)*0.25 = 14 CSS px
|
|
74
|
+
// 对比原始 12 px 外径略大一圈,刚好够辨识又不显眼。
|
|
75
|
+
const USER_MARKER_SVG = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="64" height="64"><circle cx="32" cy="32" r="24" fill="#4575F6" stroke="#ffffff" stroke-width="8"/></svg>`;
|
|
76
|
+
const USER_MARKER_ICON_URL = "data:image/svg+xml;charset=UTF-8," + encodeURIComponent(USER_MARKER_SVG);
|
|
77
|
+
const USER_MARKER_ICON_SIZE = 16;
|
|
78
|
+
const DEFAULT_FALLBACK_CENTER_BD09 = [116.404, 39.915]; // 北京(BD09)
|
|
79
|
+
// 百度 BMAP_STATUS_SUCCESS = 0;这里直接用字面量,避免 SDK 加载完成前读全局常量
|
|
80
|
+
const BMAP_STATUS_SUCCESS = 0;
|
|
81
|
+
// IP 定位判定阈值:accuracy ≥ 此值视为 IP 兜底(城市级精度)。
|
|
82
|
+
// 与高德端 IP_LOCATION_ACCURACY_THRESHOLD_M 保持一致——5km 远超室内 H5
|
|
83
|
+
// 定位误差上限(最差也罕见 > 2km),完全是城市级 IP 才有的精度。
|
|
84
|
+
//
|
|
85
|
+
// 百度 BMapGL.GeolocationResult 文档只暴露 accuracy / address / point 三个
|
|
86
|
+
// 字段,**没有 location_type**——只能靠 accuracy 阈值识别。
|
|
87
|
+
const IP_LOCATION_ACCURACY_THRESHOLD_M = 5000;
|
|
88
|
+
// 周边搜索的关键字集合:覆盖打车 / 网约车「上车点」场景的细粒度 POI。
|
|
89
|
+
//
|
|
90
|
+
// 百度 LocalSearch.searchNearby 必须给 keyword,没法用空 keyword 拉所有 POI
|
|
91
|
+
// (高德 PlaceSearch.searchNearBy("", ..., type=NEARBY_POI_TYPE) 那套不可用)。
|
|
92
|
+
// 而且百度 LocalSearch **不返回 child_pois**——大型 POI 的子 POI(小区楼栋 /
|
|
93
|
+
// 商场店铺 / 加油站厕所等)只能靠**手工 fan-out 细粒度关键字**召回,没有别的
|
|
94
|
+
// 路径。每个关键字独立发起一次 LocalSearch 并发拉取,最后合并 dedupe,效果上
|
|
95
|
+
// 接近高德全大类 + child_pois 展开的召回粒度。
|
|
96
|
+
//
|
|
97
|
+
// 关键字分组(按打车场景上车点频次排序):
|
|
98
|
+
// ① 一级大类(高频上车点母 POI,对应百度 17 大类的代表关键字):
|
|
99
|
+
// 房地产 → 小区 交通设施 → 地铁站、公交站、停车场
|
|
100
|
+
// 公司企业 → 公司 教育培训 → 学校
|
|
101
|
+
// 酒店 → 酒店 医疗 → 医院
|
|
102
|
+
// 购物 → 商场、超市 金融 → 银行
|
|
103
|
+
// 美食 → 餐厅 旅游景点 → 景点、公园
|
|
104
|
+
// 汽车服务 → 加油站
|
|
105
|
+
// ② 细粒度补充(**用户明确要求"粒度细到极致"**——加油站厕所 / 楼栋出入口
|
|
106
|
+
// 这种独立 POI 在大类下不会被召回,必须独立 fan-out):
|
|
107
|
+
// 便利店 ← 罗森 / 全家 / 7-11 等便利店连锁("超市"召回不全)
|
|
108
|
+
// 快餐 ← KFC / 麦当劳 / 汉堡王("餐厅"主要命中正餐)
|
|
109
|
+
// 咖啡 ← 星巴克 / 瑞幸(高频上车点)
|
|
110
|
+
// 充电站 ← 新能源车补给("加油站"不命中)
|
|
111
|
+
// 厕所 ← 公共厕所 / 加油站厕所 / 商场厕所(独立 POI 类型)
|
|
112
|
+
// 出入口 ← 地铁出入口 / 商场出入口 / 小区南门东门(楼栋粒度的入口)
|
|
113
|
+
//
|
|
114
|
+
// 总请求量:**仅 3 个 LocalSearch.searchNearby**(21 个关键字按每组 10 个切片)。
|
|
115
|
+
// 利用百度官方 keyword: String | Array 的多关键字 API(详见 searchAround 内
|
|
116
|
+
// 的注释),把"每个关键字一次独立请求"压成"一次请求带多关键字数组"——总耗时
|
|
117
|
+
// 由 max(单次 LocalSearch) 决定,绕开浏览器单域 6 个并发连接的限制。
|
|
118
|
+
const NEARBY_KEYWORDS = [
|
|
119
|
+
"公司",
|
|
120
|
+
"小区",
|
|
121
|
+
"酒店",
|
|
122
|
+
"商场",
|
|
123
|
+
"超市",
|
|
124
|
+
"便利店",
|
|
125
|
+
"餐厅",
|
|
126
|
+
"快餐",
|
|
127
|
+
"咖啡",
|
|
128
|
+
"地铁站",
|
|
129
|
+
"公交站",
|
|
130
|
+
"停车场",
|
|
131
|
+
"学校",
|
|
132
|
+
"医院",
|
|
133
|
+
"银行",
|
|
134
|
+
"景点",
|
|
135
|
+
"公园",
|
|
136
|
+
"加油站",
|
|
137
|
+
"充电站",
|
|
138
|
+
"厕所",
|
|
139
|
+
"出入口",
|
|
140
|
+
];
|
|
141
|
+
// 把百度 LocalSearch / Geocoder.surroundingPois 返回的 POI 字段标准化为 POIItem。
|
|
142
|
+
//
|
|
143
|
+
// 字段命名差异:百度用 title(不是 name)、point.lng/lat(不是 location)、uid(不是 id)。
|
|
144
|
+
// SDK 没有给"POI ↔ 查询点"的距离字段,distance 留 undefined,统一交给 UI 层
|
|
145
|
+
// rewriteDistanceFromCenter 用 haversine 算(球面距离精度,与高德端口径完全一致)。
|
|
146
|
+
function normalizeBMapPOI(poi) {
|
|
147
|
+
var _a, _b;
|
|
148
|
+
if (!poi || !poi.point)
|
|
149
|
+
return null;
|
|
150
|
+
const lng = typeof poi.point.lng === "number" ? poi.point.lng : NaN;
|
|
151
|
+
const lat = typeof poi.point.lat === "number" ? poi.point.lat : NaN;
|
|
152
|
+
if (!Number.isFinite(lng) || !Number.isFinite(lat))
|
|
153
|
+
return null;
|
|
154
|
+
const name = ((_a = poi.title) !== null && _a !== void 0 ? _a : "").toString();
|
|
155
|
+
const address = ((_b = poi.address) !== null && _b !== void 0 ? _b : "").toString();
|
|
156
|
+
return {
|
|
157
|
+
id: poi.uid || `${lng},${lat},${name}`,
|
|
158
|
+
name,
|
|
159
|
+
address,
|
|
160
|
+
location: { lng, lat },
|
|
161
|
+
cityname: poi.city,
|
|
162
|
+
pname: poi.province,
|
|
163
|
+
adname: poi.district,
|
|
164
|
+
raw: poi,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
class BMapProvider {
|
|
168
|
+
constructor(opts) {
|
|
169
|
+
this.BMap = null;
|
|
170
|
+
this.map = null;
|
|
171
|
+
this.userMarker = null;
|
|
172
|
+
this.geocoder = null;
|
|
173
|
+
// BMapGL.Geolocation 单例:getCurrentPosition 内部融合浏览器 H5 定位 +
|
|
174
|
+
// IP 定位 + 安卓 SDK 辅助定位,自动返回 BD09 坐标。比 navigator.geolocation
|
|
175
|
+
// + Convertor 两步式更可靠(H5 定位失败时自动降级到 IP,能拿到城市级位置
|
|
176
|
+
// 而不是直接 null)。
|
|
177
|
+
this.geolocation = null;
|
|
178
|
+
this.pendingGeolocate = null;
|
|
179
|
+
this.aroundSeq = 0;
|
|
180
|
+
this.keywordSeq = 0;
|
|
181
|
+
this.opts = opts;
|
|
182
|
+
}
|
|
183
|
+
// 加载 SDK + 创建所有 service 类(Geocoder / Geolocation 单例)。
|
|
184
|
+
// BMapGL.LocalSearch 不在这里创建——它每次 runLocalSearch 内部都会 new
|
|
185
|
+
// 一份并配独立的 onSearchComplete 回调,**且第一参数就是 BMap.Point 而
|
|
186
|
+
// 非 Map 实例**(详见 v5.x 头部注释中"LocalSearch 构造第一个参数传
|
|
187
|
+
// BMap.Point 而非 this.map"),所以 LocalSearch **本就独立于 Map**。
|
|
188
|
+
// 抽出来共用:组件 init 与 headless initHeadless 的"加载 SDK + service"
|
|
189
|
+
// 部分完全相同,分别写两份是重复代码。
|
|
190
|
+
initServices() {
|
|
191
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
192
|
+
const BMap = yield (0, loader_bmap_1.loadBMap)({ ak: this.opts.ak });
|
|
193
|
+
this.BMap = BMap;
|
|
194
|
+
this.geocoder = new BMap.Geocoder();
|
|
195
|
+
// 创建 Geolocation 单例:内部首次 getCurrentPosition 时才会真正发起定位,
|
|
196
|
+
// 这里仅创建对象本身(无网络 / 权限副作用)。
|
|
197
|
+
if (typeof BMap.Geolocation === "function") {
|
|
198
|
+
this.geolocation = new BMap.Geolocation();
|
|
199
|
+
}
|
|
200
|
+
return BMap;
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
init(o) {
|
|
204
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
205
|
+
var _a, _b;
|
|
206
|
+
const BMap = yield this.initServices();
|
|
207
|
+
const center = (_a = o.initialCenter) !== null && _a !== void 0 ? _a : DEFAULT_FALLBACK_CENTER_BD09;
|
|
208
|
+
const zoom = (_b = o.initialZoom) !== null && _b !== void 0 ? _b : 16;
|
|
209
|
+
this.map = new BMap.Map(o.container, {
|
|
210
|
+
// 关闭底图 POI 点击,避免与组件 click 处理冲突
|
|
211
|
+
enableIconClick: false,
|
|
212
|
+
});
|
|
213
|
+
// BMapGL 必须显式调用 centerAndZoom 才能开始渲染瓦片
|
|
214
|
+
this.map.centerAndZoom(new BMap.Point(center[0], center[1]), zoom);
|
|
215
|
+
// BMapGL 默认关闭双指缩放与滚轮缩放(仅默认开启拖拽 + 双击放大),
|
|
216
|
+
// 与高德 Map 默认体感不一致 —— 必须显式启用,否则 H5 端用户「捏不动」、
|
|
217
|
+
// PC 端用户「滚不动」,看起来就像地图不能缩放。
|
|
218
|
+
if (typeof this.map.enablePinchToZoom === "function") {
|
|
219
|
+
this.map.enablePinchToZoom(true);
|
|
220
|
+
}
|
|
221
|
+
if (typeof this.map.enableScrollWheelZoom === "function") {
|
|
222
|
+
this.map.enableScrollWheelZoom(true);
|
|
223
|
+
}
|
|
224
|
+
// 双击放大默认开启,这里冗余声明一次,防止某些 SDK 版本默认值被改动
|
|
225
|
+
if (typeof this.map.enableDoubleClickZoom === "function") {
|
|
226
|
+
this.map.enableDoubleClickZoom(true);
|
|
227
|
+
}
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
// headless:仅 service 类,不创建 Map 实例 → 不挂容器 / 不下载瓦片 /
|
|
231
|
+
// 不渲染。详见 MapProvider.initHeadless 接口注释。
|
|
232
|
+
// opts.initialCity:百度 LocalSearch 以 Point 为作用域,此处忽略(仅为接口对齐)。
|
|
233
|
+
initHeadless(_opts) {
|
|
234
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
235
|
+
yield this.initServices();
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
destroy() {
|
|
239
|
+
try {
|
|
240
|
+
if (this.map && typeof this.map.destroy === "function") {
|
|
241
|
+
this.map.destroy();
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
catch (_a) {
|
|
245
|
+
// ignore
|
|
246
|
+
}
|
|
247
|
+
this.BMap = null;
|
|
248
|
+
this.map = null;
|
|
249
|
+
this.userMarker = null;
|
|
250
|
+
this.geocoder = null;
|
|
251
|
+
this.geolocation = null;
|
|
252
|
+
this.pendingGeolocate = null;
|
|
253
|
+
}
|
|
254
|
+
getCenter() {
|
|
255
|
+
const c = this.map.getCenter();
|
|
256
|
+
return [c.lng, c.lat];
|
|
257
|
+
}
|
|
258
|
+
setCenter(center, zoom) {
|
|
259
|
+
const BMap = this.BMap;
|
|
260
|
+
const point = new BMap.Point(center[0], center[1]);
|
|
261
|
+
// 视图过渡策略(参见 SKILL map-init.md):
|
|
262
|
+
// - 统一用 flyTo(point, zoom):SDK 一次性完成中心 + zoom 的平滑过渡,
|
|
263
|
+
// 体感与高德 AMap.Map.setCenter / setZoomAndCenter 默认动画一致。
|
|
264
|
+
// - 不带 zoom 时也走 flyTo(point, currentZoom):因为 BMapGL.panTo 在目标点距离
|
|
265
|
+
// 当前视野较远时会被 SDK 自己降级为瞬变 setCenter(实测列表点击较远 POI 即触发),
|
|
266
|
+
// 而 flyTo 没有这个距离阈值,能保证任何距离都是丝滑动画——与高德端表现拉齐。
|
|
267
|
+
// - 极端兜底:flyTo 不存在时用 setCenter + setZoom 拆开调(禁用 centerAndZoom,
|
|
268
|
+
// 部分 BMapGL 版本下二次调用 centerAndZoom 会清空所有覆盖物,把蓝点 marker 也带走)。
|
|
269
|
+
const targetZoom = typeof zoom === "number" ? zoom : this.map.getZoom();
|
|
270
|
+
if (typeof this.map.flyTo === "function") {
|
|
271
|
+
this.map.flyTo(point, targetZoom);
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
this.map.setCenter(point);
|
|
275
|
+
if (typeof zoom === "number" && this.map.getZoom() !== zoom) {
|
|
276
|
+
this.map.setZoom(zoom);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
upsertUserMarker(center) {
|
|
280
|
+
const BMap = this.BMap;
|
|
281
|
+
if (!BMap || !this.map)
|
|
282
|
+
return;
|
|
283
|
+
const point = new BMap.Point(center[0], center[1]);
|
|
284
|
+
// 更新策略:每次都 removeOverlay + addOverlay 重建,而不是 marker.setPoint。
|
|
285
|
+
//
|
|
286
|
+
// 原因:
|
|
287
|
+
// - 实测部分 BMapGL 版本下,已添加的 marker 调用 setPoint(point) 后内部坐标
|
|
288
|
+
// 更新了,但屏幕渲染位置没刷新,肉眼看起来就是"蓝色 marker 消失";
|
|
289
|
+
// - removeOverlay + addOverlay 重新走完整渲染管线,对齐和显示都最稳;
|
|
290
|
+
// - 用户触发的频率低(GPS 定位、点击「回到当前位置」),重建成本可忽略。
|
|
291
|
+
if (this.userMarker) {
|
|
292
|
+
try {
|
|
293
|
+
this.map.removeOverlay(this.userMarker);
|
|
294
|
+
}
|
|
295
|
+
catch (_a) {
|
|
296
|
+
// 极端情况下 marker 已被 SDK 内部清除(例如旧版本 centerAndZoom 副作用),
|
|
297
|
+
// 这里 remove 失败不影响后续 add,吞掉异常即可。
|
|
298
|
+
}
|
|
299
|
+
this.userMarker = null;
|
|
300
|
+
}
|
|
301
|
+
// 用 BMapGL.Marker + Icon 替代 CustomOverlay:SDK 内置 marker 与地图本身共享同一套
|
|
302
|
+
// 坐标转换,同一个 BMapGL.Point 喂给 map 和喂给 marker,屏幕落点严格一致。
|
|
303
|
+
// 设置 icon.anchor = (size/2, size/2) 让 SVG 几何中心对齐 point。
|
|
304
|
+
const icon = new BMap.Icon(USER_MARKER_ICON_URL, new BMap.Size(USER_MARKER_ICON_SIZE, USER_MARKER_ICON_SIZE), {
|
|
305
|
+
anchor: new BMap.Size(USER_MARKER_ICON_SIZE / 2, USER_MARKER_ICON_SIZE / 2),
|
|
306
|
+
});
|
|
307
|
+
const marker = new BMap.Marker(point, {
|
|
308
|
+
icon,
|
|
309
|
+
enableDragging: false,
|
|
310
|
+
enableMassClear: false,
|
|
311
|
+
});
|
|
312
|
+
this.map.addOverlay(marker);
|
|
313
|
+
this.userMarker = marker;
|
|
314
|
+
}
|
|
315
|
+
// 周边搜索:纯 JSAPI 多关键字数组检索(v5.2 重构)。
|
|
316
|
+
//
|
|
317
|
+
// 实现策略:
|
|
318
|
+
// 1) 把 NEARBY_KEYWORDS(21 个关键字)按 KEYWORDS_PER_REQUEST=10 切成
|
|
319
|
+
// 多组(10 / 10 / 1);
|
|
320
|
+
// 2) 每组 new 一个独立 BMap.LocalSearch 实例,并发发起
|
|
321
|
+
// searchNearby(keyword[], point, radius)——百度官方支持 keyword 数组形式,
|
|
322
|
+
// 一次最多 10 个关键字(自 1.2 版本起,详见 LocalSearch 文档);
|
|
323
|
+
// 3) 单实例 setPageCapacity(50) 拿首页满量,不翻页(关键字数组已保证候选数);
|
|
324
|
+
// 4) Promise.all 等所有完成后合并 by uid 去重,UI 层 sortByDistance 排序。
|
|
325
|
+
//
|
|
326
|
+
// 为何用多关键字数组(不是 21 次独立请求):
|
|
327
|
+
// - **HTTP 请求数从 21 → 3**——浏览器单域并发连接限制(一般 6 个)下,21 个
|
|
328
|
+
// 独立请求会被分批排队,前 6 个先发后 15 个等待,部分串行化拖慢总耗时;
|
|
329
|
+
// 3 个请求一次发完,每个请求内部由百度后端并发处理多关键字;
|
|
330
|
+
// - 百度后端处理多关键字应该是合并查询,相比 21 次独立查询省 RTT 与服务端
|
|
331
|
+
// 重复的"中心点 + 半径"运算;
|
|
332
|
+
// - onSearchComplete 在多关键字时回调 LocalResult[],runLocalSearch 已经
|
|
333
|
+
// 统一归一化(见 runLocalSearch 注释)。
|
|
334
|
+
//
|
|
335
|
+
// 为何独立实例不共享:BMap.LocalSearch 单实例同时只能跑一个 search 请求
|
|
336
|
+
// (onSearchComplete 回调只有一份,复用会被互踩)。所以 3 组之间必须 3 个
|
|
337
|
+
// 独立实例。
|
|
338
|
+
//
|
|
339
|
+
// 总请求量:3 个并发 LocalSearch.searchNearby(每组 ≤10 关键字)。与高德 1 个
|
|
340
|
+
// PlaceSearch.searchNearBy 全 type 召回相比仍是 3:1 劣势,但这是百度 SDK
|
|
341
|
+
// 不提供"无关键字全大类周边"接口、且 LocalSearch 不返回 child_pois 的客观
|
|
342
|
+
// 限制——必须靠多关键字覆盖(含厕所 / 充电站 / 出入口等细粒度)才能兜出与
|
|
343
|
+
// 高德全大类 + child_pois 展开等价的召回粒度。
|
|
344
|
+
//
|
|
345
|
+
// 不支持翻页:百度 LocalSearch 翻页时 SDK 按相关度(不是按距离)排,远端 POI
|
|
346
|
+
// 也会被翻上来,对周边场景反而是噪声。UI 层 hasMore=false 让翻页按钮隐藏。
|
|
347
|
+
searchAround(center, options) {
|
|
348
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
349
|
+
var _a;
|
|
350
|
+
// 仅检查 SDK 加载完成——不依赖 this.map(headless 模式下可用)
|
|
351
|
+
if (!this.BMap) {
|
|
352
|
+
return { pois: [], hasMore: false };
|
|
353
|
+
}
|
|
354
|
+
const page = Math.max(1, (_a = options.page) !== null && _a !== void 0 ? _a : 1);
|
|
355
|
+
if (page > 1) {
|
|
356
|
+
// 不支持翻页:UI 在 hasMore=false 时不会再触发翻页,这里仅作冗余兜底
|
|
357
|
+
return { pois: [], hasMore: false };
|
|
358
|
+
}
|
|
359
|
+
const seq = ++this.aroundSeq;
|
|
360
|
+
// radius 钳制:1000-5000m。下限 1000 是因为低密度区域(郊区 / 工业园)
|
|
361
|
+
// 200m 半径常常返回空;上限 5000 兼顾响应速度与召回总量。
|
|
362
|
+
const radius = Math.min(Math.max(options.radius, 1000), 5000);
|
|
363
|
+
const pageCapacity = Math.min(50, Math.max(20, options.pageSize));
|
|
364
|
+
// 百度官方上限:LocalSearch.searchNearby 的 keyword 数组最多 10 个。
|
|
365
|
+
// 21 个关键字按 10/10/1 切成 3 组并发——每个分组在 SDK 内部一次请求带多
|
|
366
|
+
// 关键字(百度后端合并查询),3 个分组在浏览器层并发发出。
|
|
367
|
+
const KEYWORDS_PER_REQUEST = 10;
|
|
368
|
+
const groups = [];
|
|
369
|
+
for (let i = 0; i < NEARBY_KEYWORDS.length; i += KEYWORDS_PER_REQUEST) {
|
|
370
|
+
groups.push(NEARBY_KEYWORDS.slice(i, i + KEYWORDS_PER_REQUEST));
|
|
371
|
+
}
|
|
372
|
+
const tasks = groups.map((group) => this.runLocalSearchNearby(group, center, radius, pageCapacity));
|
|
373
|
+
const results = yield Promise.all(tasks);
|
|
374
|
+
if (seq !== this.aroundSeq)
|
|
375
|
+
return { pois: [], hasMore: false };
|
|
376
|
+
// 跨关键字 dedupe by uid:同一 POI 可能命中多个关键字(如"罗森便利店"既是
|
|
377
|
+
// "超市"也是"便利店"),保留先到的版本即可——字段完全一致。
|
|
378
|
+
const seen = new Map();
|
|
379
|
+
for (const list of results) {
|
|
380
|
+
for (const p of list) {
|
|
381
|
+
if (!seen.has(p.id))
|
|
382
|
+
seen.set(p.id, p);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
return { pois: Array.from(seen.values()), hasMore: false };
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
// 关键字搜索:LocalSearch.search(keyword) + forceLocal: false。
|
|
389
|
+
//
|
|
390
|
+
// search vs searchNearby 的关键区别:
|
|
391
|
+
// - searchNearby(keyword, point, radius):以 point 为圆心 radius 半径的圆形
|
|
392
|
+
// 检索,受 radius 强约束(SDK 上限 100km),不会跨城;
|
|
393
|
+
// - search(keyword):在 LocalSearch 构造时配置的 location 范围内检索
|
|
394
|
+
// (location = Point 时取 Point 所在城市),按相关度返回。配合
|
|
395
|
+
// forceLocal: false,本城无命中时 SDK 会自动扩展到全国。
|
|
396
|
+
//
|
|
397
|
+
// forceLocal: false 的命中行为(百度 JSAPI 官方约定):
|
|
398
|
+
// - "虹桥火车站"在上海有精确命中 → 返回上海命中(同城精度不降级);
|
|
399
|
+
// - "天安门"在上海无真正"天安门"地标命中 → SDK 自动扩展全国 → 返回北京天安门。
|
|
400
|
+
// 这正是高德 / 微信"发送位置"的搜索体感。
|
|
401
|
+
//
|
|
402
|
+
// 与 UI 层的配合:
|
|
403
|
+
// - search() 不像 searchNearby() 严格按距离排序,SDK 给的顺序是相关度;
|
|
404
|
+
// - UI 端 sortByKeywordRelevance 仍负责"名称命中度分级 + 同档距离升序"排序,
|
|
405
|
+
// 跨城远距离命中会排在同城精确命中之后;
|
|
406
|
+
// - 距离字段在 UI 端用 haversineMeters 重算(rewriteDistanceFromCenter),
|
|
407
|
+
// 与 SDK 给的口径解耦,跨城距离显示也准确。
|
|
408
|
+
searchByKeyword(center, keyword, options) {
|
|
409
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
410
|
+
if (!this.BMap)
|
|
411
|
+
return [];
|
|
412
|
+
const seq = ++this.keywordSeq;
|
|
413
|
+
const pageCapacity = Math.min(50, Math.max(20, options.pageSize));
|
|
414
|
+
const list = yield this.runLocalSearchKeyword(keyword, center, pageCapacity);
|
|
415
|
+
if (seq !== this.keywordSeq)
|
|
416
|
+
return [];
|
|
417
|
+
return list;
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
// 单次 LocalSearch 调用 → POIItem[] 的统一封装。
|
|
421
|
+
//
|
|
422
|
+
// 关键约定(与历史踩坑教训):
|
|
423
|
+
// * **LocalSearch 构造第一个参数传 BMap.Point 而非 this.map**——这是修
|
|
424
|
+
// "首次进入列表跑到 1000+ km 之外"的关键。BMap.LocalSearch(location, opts)
|
|
425
|
+
// 的 location 决定 SDK 内部"检索作用域":
|
|
426
|
+
// - 传 BMap.Map:SDK 把 map 当前所在城市当作检索作用域,**会忽略**
|
|
427
|
+
// searchNearby(keyword, point, radius) 第二个参数指定的 point 城市——
|
|
428
|
+
// 典型反例:map 在北京(fallback 中心未被 setCenter 切到上海前),
|
|
429
|
+
// 即便 searchNearby 传 上海 point + 5km,SDK 仍然把 keyword 限制在
|
|
430
|
+
// 北京全市检索 → 列表全是北京 POI;
|
|
431
|
+
// - 传 BMap.Point:SDK 直接以 point 所在城市作为检索作用域,与
|
|
432
|
+
// searchNearby 的 location 参数完全对齐。
|
|
433
|
+
// 这是 BMapGL JSAPI 的反直觉行为,文档没明说但实测必踩。统一用 Point
|
|
434
|
+
// 构造彻底切断对 map 当前 city 状态的依赖。
|
|
435
|
+
// * **每次 new 一个 LocalSearch 实例**:避免共享实例的 mutex 串行化——
|
|
436
|
+
// fan-out 必须并发,串行总耗时 ≈ 15 × 单次 ≈ 6-12s 不可接受;
|
|
437
|
+
// * **status 非 SUCCESS 时返回空数组**(不 reject):让上层 Promise.all
|
|
438
|
+
// 对单关键字失败容忍,其它关键字仍能贡献候选;
|
|
439
|
+
// * **跨城回退命中**(forceLocal: false 时)部分版本 SDK 会返回 status = 4
|
|
440
|
+
// (CITY_LIST,建议用户从城市列表选择)。这里只接受 SUCCESS = 0,让 UI
|
|
441
|
+
// 表现稳定不需要弹城市选择列表——业务方只关心 POI 列表。
|
|
442
|
+
//
|
|
443
|
+
// 调用方提供 invoke 回调来决定具体调 search() 还是 searchNearby(...),避免
|
|
444
|
+
// 把两条路径的样板代码各写一份。
|
|
445
|
+
//
|
|
446
|
+
// 多关键字回调兼容:百度官方文档明确 search / searchInBounds / searchNearby
|
|
447
|
+
// 的 keyword 都支持 String | Array(最多 10 个关键字,自 1.2 版本起)。
|
|
448
|
+
// 调用方给单关键字时 onSearchComplete 收到的是单个 LocalResult;给数组时
|
|
449
|
+
// 收到的是 LocalResult[]——这里统一归一化成数组迭代,让上层 invoke 回调
|
|
450
|
+
// 决定传单还是数组,runLocalSearch 不需要分两条路径。
|
|
451
|
+
//
|
|
452
|
+
// 文档参考:https://lbsyun.baidu.com/cms/jsapi/reference/jsapi_webgl_1_0.html
|
|
453
|
+
// 中 LocalSearch 章节:"如果是多关键字范围检索,则返回一个 LocalResult 的数组"。
|
|
454
|
+
runLocalSearch(point, options, invoke) {
|
|
455
|
+
const BMap = this.BMap;
|
|
456
|
+
// LocalSearch 第一参数本身是 Point(不是 Map 实例),所以 service 路径
|
|
457
|
+
// 完全不依赖 this.map——headless 模式下 this.map 为 null 仍然能用。
|
|
458
|
+
if (!BMap)
|
|
459
|
+
return Promise.resolve([]);
|
|
460
|
+
return new Promise((resolve) => {
|
|
461
|
+
const search = new BMap.LocalSearch(point, Object.assign(Object.assign({}, options), { onSearchComplete: (results) => {
|
|
462
|
+
if (!results) {
|
|
463
|
+
resolve([]);
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
466
|
+
const status = typeof search.getStatus === "function"
|
|
467
|
+
? search.getStatus()
|
|
468
|
+
: BMAP_STATUS_SUCCESS;
|
|
469
|
+
if (status !== BMAP_STATUS_SUCCESS) {
|
|
470
|
+
resolve([]);
|
|
471
|
+
return;
|
|
472
|
+
}
|
|
473
|
+
// 多关键字时 results = LocalResult[],单关键字时 results = LocalResult。
|
|
474
|
+
// 统一归一化成数组迭代——单 / 多关键字共用同一份 POI 提取逻辑。
|
|
475
|
+
const localResults = Array.isArray(results)
|
|
476
|
+
? results
|
|
477
|
+
: [results];
|
|
478
|
+
const list = [];
|
|
479
|
+
for (const r of localResults) {
|
|
480
|
+
if (!r)
|
|
481
|
+
continue;
|
|
482
|
+
const num = typeof r.getCurrentNumPois === "function"
|
|
483
|
+
? r.getCurrentNumPois()
|
|
484
|
+
: 0;
|
|
485
|
+
for (let i = 0; i < num; i++) {
|
|
486
|
+
const item = normalizeBMapPOI(r.getPoi(i));
|
|
487
|
+
if (item)
|
|
488
|
+
list.push(item);
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
resolve(list);
|
|
492
|
+
} }));
|
|
493
|
+
invoke(search);
|
|
494
|
+
});
|
|
495
|
+
}
|
|
496
|
+
// 跨城关键字检索:LocalSearch.search() + forceLocal: false。本城无命中时
|
|
497
|
+
// SDK 自动扩展到全国(参见 searchByKeyword 头部注释)。
|
|
498
|
+
runLocalSearchKeyword(keyword, center, pageCapacity) {
|
|
499
|
+
const BMap = this.BMap;
|
|
500
|
+
if (!BMap)
|
|
501
|
+
return Promise.resolve([]);
|
|
502
|
+
const point = new BMap.Point(center[0], center[1]);
|
|
503
|
+
return this.runLocalSearch(point, { forceLocal: false, pageCapacity }, (search) => search.search(keyword));
|
|
504
|
+
}
|
|
505
|
+
// 周边检索:LocalSearch.searchNearby(keyword, point, radius)。受 radius 强约束,
|
|
506
|
+
// 不会跨城(与 runLocalSearchKeyword 互补)。供 searchAround 的关键字分组调用。
|
|
507
|
+
//
|
|
508
|
+
// keyword 类型 string | string[]:
|
|
509
|
+
// - string:单关键字,对应 onSearchComplete(LocalResult);
|
|
510
|
+
// - string[]:多关键字数组(**最多 10 个**,百度官方上限),对应
|
|
511
|
+
// onSearchComplete(LocalResult[])。runLocalSearch 已统一兼容两种回调。
|
|
512
|
+
runLocalSearchNearby(keyword, center, radius, pageCapacity) {
|
|
513
|
+
const BMap = this.BMap;
|
|
514
|
+
if (!BMap)
|
|
515
|
+
return Promise.resolve([]);
|
|
516
|
+
const point = new BMap.Point(center[0], center[1]);
|
|
517
|
+
return this.runLocalSearch(point, { pageCapacity }, (search) => search.searchNearby(keyword, point, radius));
|
|
518
|
+
}
|
|
519
|
+
// 定位:BMapGL.Geolocation.getCurrentPosition(百度官方推荐方式)。
|
|
520
|
+
//
|
|
521
|
+
// 与原 `navigator.geolocation + BMapGL.Convertor` 两步式相比的优势:
|
|
522
|
+
// - **直接 BD09**:内部已做 WGS84 → BD09 转换,不需要再调 Convertor.translate,
|
|
523
|
+
// 省一次异步 SDK 调用 + 配额;
|
|
524
|
+
// - **多源融合**:浏览器 H5 定位失败时自动 fallback 到 IP 定位(城市级),
|
|
525
|
+
// 再失败才回 null——比 navigator.geolocation 的"成功 / 失败"二元结果
|
|
526
|
+
// 更友好,用户拒绝 GPS 时仍能拿到城市级位置;
|
|
527
|
+
// - **状态码统一**:getStatus() 返回 BMAP_STATUS_SUCCESS / TIMEOUT /
|
|
528
|
+
// PERMISSION_DENIED / UNKNOWN_LOCATION,与 SDK 其他模块(LocalSearch
|
|
529
|
+
// 等)的状态码语义一致。
|
|
530
|
+
//
|
|
531
|
+
// in-flight Promise 复用:连续调用共享同一个进行中的请求,避免重复弹权限框。
|
|
532
|
+
// SDK 加载或实例创建失败的极端情况:返回 null(业务方按缺省定位处理)。
|
|
533
|
+
//
|
|
534
|
+
// 文档参考:
|
|
535
|
+
// https://lbs.baidu.com/docs/jsapi?title=jspopularGL/guide/geoloaction
|
|
536
|
+
geolocate(options) {
|
|
537
|
+
if (this.pendingGeolocate)
|
|
538
|
+
return this.pendingGeolocate;
|
|
539
|
+
const geolocation = this.geolocation;
|
|
540
|
+
if (!geolocation || typeof geolocation.getCurrentPosition !== "function") {
|
|
541
|
+
return Promise.resolve(null);
|
|
542
|
+
}
|
|
543
|
+
const allowIp = (options === null || options === void 0 ? void 0 : options.allowIpFallback) === true;
|
|
544
|
+
const promise = new Promise((resolve) => {
|
|
545
|
+
geolocation.getCurrentPosition(function onComplete(r) {
|
|
546
|
+
// SDK 设计:getStatus() 是 Geolocation 实例的方法(this 绑定到该实例)。
|
|
547
|
+
// 用普通 function(非箭头函数)保留 this 引用,与百度官方示例一致。
|
|
548
|
+
const status = typeof (this === null || this === void 0 ? void 0 : this.getStatus) === "function"
|
|
549
|
+
? this.getStatus()
|
|
550
|
+
: BMAP_STATUS_SUCCESS;
|
|
551
|
+
if (status !== BMAP_STATUS_SUCCESS || !(r === null || r === void 0 ? void 0 : r.point)) {
|
|
552
|
+
console.warn("[MapLocationSelection] BMapGL.Geolocation 失败。status=", status, "(6=PERMISSION_DENIED 2=POSITION_UNAVAILABLE 8=TIMEOUT)");
|
|
553
|
+
resolve(null);
|
|
554
|
+
return;
|
|
555
|
+
}
|
|
556
|
+
const lng = Number(r.point.lng);
|
|
557
|
+
const lat = Number(r.point.lat);
|
|
558
|
+
if (!Number.isFinite(lng) || !Number.isFinite(lat)) {
|
|
559
|
+
resolve(null);
|
|
560
|
+
return;
|
|
561
|
+
}
|
|
562
|
+
// IP 兜底事后判定:百度 GeolocationResult 不暴露 location_type,
|
|
563
|
+
// 只能靠 accuracy 阈值识别。详见类内 IP_LOCATION_ACCURACY_THRESHOLD_M
|
|
564
|
+
// 注释。
|
|
565
|
+
const acc = Number(r.accuracy);
|
|
566
|
+
const ipLike = Number.isFinite(acc) && acc >= IP_LOCATION_ACCURACY_THRESHOLD_M;
|
|
567
|
+
if (!allowIp && ipLike) {
|
|
568
|
+
console.warn("[MapLocationSelection] BMapGL.Geolocation 命中 IP 兜底(accuracy=", r.accuracy, "m ≥", IP_LOCATION_ACCURACY_THRESHOLD_M, "m)但 allowIpFallback=false,已丢弃返回 null。");
|
|
569
|
+
resolve(null);
|
|
570
|
+
return;
|
|
571
|
+
}
|
|
572
|
+
resolve([lng, lat]);
|
|
573
|
+
}, {
|
|
574
|
+
enableHighAccuracy: true,
|
|
575
|
+
timeout: 10000,
|
|
576
|
+
});
|
|
577
|
+
}).finally(() => {
|
|
578
|
+
this.pendingGeolocate = null;
|
|
579
|
+
});
|
|
580
|
+
this.pendingGeolocate = promise;
|
|
581
|
+
return promise;
|
|
582
|
+
}
|
|
583
|
+
// 反查:webservice `reverse_geocoding/v3` + jsonp 主路径,JSAPI fallback。
|
|
584
|
+
//
|
|
585
|
+
// 为何主路径不用 JSAPI(v5.3 切换原因):
|
|
586
|
+
// - BMapGL JSAPI 类参考文档明确 `AddressComponent` 仅含 streetNumber /
|
|
587
|
+
// street / district / city / province 5 个字段,**不含 adcode**——所以
|
|
588
|
+
// 业务方拿不到 6 位国标行政区划码。这与高德 JSAPI 端能直接拿到 adcode
|
|
589
|
+
// 形成跛脚体验;
|
|
590
|
+
// - webservice `reverse_geocoding/v3` 的 `addressComponent` 包含 country /
|
|
591
|
+
// province / city / district / town / **adcode** (int) / street / 等
|
|
592
|
+
// 完整字段,且与 JSAPI 共用同一个 ak(依赖 referer 白名单一致),用
|
|
593
|
+
// jsonp 在浏览器直调即可,**无需服务端代理**;
|
|
594
|
+
// - 每次只一发请求,比 JSAPI 主路径 + 二次补 adcode 更简洁、更省总耗时。
|
|
595
|
+
//
|
|
596
|
+
// JSAPI fallback 的价值:webservice 失败(断网 / 限额 / referer 拒绝)时
|
|
597
|
+
// 仍能拿到 name / address / 省市区名称——只是 adcode 为 undefined,
|
|
598
|
+
// splitAdcode 会让三个 *Code 同步置 undefined,业务方按需兜底。
|
|
599
|
+
//
|
|
600
|
+
// 文档参考:
|
|
601
|
+
// https://lbs.baidu.com/faq/api?title=webapi/guide/webservice-geocoding-abroad-base
|
|
602
|
+
reverseGeocode(center) {
|
|
603
|
+
return this.reverseGeocodeViaWebApi(center).then((webResult) => {
|
|
604
|
+
if (webResult)
|
|
605
|
+
return webResult;
|
|
606
|
+
return this.reverseGeocodeViaJsapi(center);
|
|
607
|
+
});
|
|
608
|
+
}
|
|
609
|
+
// webservice `reverse_geocoding/v3` 主路径。
|
|
610
|
+
//
|
|
611
|
+
// 字段映射(webservice → ReverseGeocodeResult):
|
|
612
|
+
// * name 候选:sematic_description("XX商圈附近")→ business(商圈)→
|
|
613
|
+
// addressComponent.town(街道乡镇)→ addressComponent.street。**不掺
|
|
614
|
+
// POI**——POI 借用由 commitMapCenter 用 searchAround 的 LocalSearch
|
|
615
|
+
// fan-out 结果统一处理,与 JSAPI 路径口径一致;
|
|
616
|
+
// * address:formatted_address("四川省成都市青羊区人民中路一段",最规范);
|
|
617
|
+
// * province / city / district:addressComponent 同名字段直拿;
|
|
618
|
+
// * adcode:addressComponent.adcode 是 **int**——toString() 后用 6 位 regex
|
|
619
|
+
// 校验,非法返回 undefined。
|
|
620
|
+
//
|
|
621
|
+
// 失败语义(接口 status !== 0 或网络异常):返回 null 让外层 fallback 到 JSAPI。
|
|
622
|
+
reverseGeocodeViaWebApi(center) {
|
|
623
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
624
|
+
var _a, _b, _c, _d, _e, _f, _g;
|
|
625
|
+
const ak = this.opts.ak;
|
|
626
|
+
if (!ak)
|
|
627
|
+
return null;
|
|
628
|
+
// location 参数顺序是 lat,lng(与 JSAPI 的 lng,lat 相反,百度文档明确);
|
|
629
|
+
// coordtype=bd09ll:与 BMapGL 全局坐标系对齐,避免内部再做 GCJ02→BD09 转换。
|
|
630
|
+
const url = "https://api.map.baidu.com/reverse_geocoding/v3/?" +
|
|
631
|
+
`ak=${encodeURIComponent(ak)}` +
|
|
632
|
+
`&location=${center[1]},${center[0]}` +
|
|
633
|
+
"&output=json" +
|
|
634
|
+
"&coordtype=bd09ll" +
|
|
635
|
+
"&extensions_poi=0";
|
|
636
|
+
let raw;
|
|
637
|
+
try {
|
|
638
|
+
raw = yield (0, jsonp_1.jsonp)(url);
|
|
639
|
+
}
|
|
640
|
+
catch (_h) {
|
|
641
|
+
// jsonp script onerror —— 在百度 webservice 的语义下几乎必然是 ak 鉴权
|
|
642
|
+
// 失败:百度此时返回**未包装 callback 的纯 JSON 错误体**(如
|
|
643
|
+
// `{"status":240,"message":"该 AK 不具备对应接口的权限..."}`),浏览器把
|
|
644
|
+
// 它当 JS 解析触发 SyntaxError → script.onerror 抛 `Event{type:'error'}`。
|
|
645
|
+
//
|
|
646
|
+
// 给业务方一个**能直接定位问题**的诊断输出:把请求 url 打出来——开发者
|
|
647
|
+
// 在浏览器新 tab 直接访问该 url,就能看到百度返回的真实 status/message。
|
|
648
|
+
// 常见 status 码与对应处理:
|
|
649
|
+
// 240 → 控制台「我的应用 → 设置 → 启用服务」勾选 "**Geocoding API**"
|
|
650
|
+
// 210 → 「Referer 白名单」加当前域名(或填 `*` 临时放开)
|
|
651
|
+
// 200 → ak 不存在 / 输入有误,复查 ak
|
|
652
|
+
// 302 → 配额已用完(需付费或换 ak)
|
|
653
|
+
console.warn("[MapLocationSelection] BMap reverse_geocoding/v3 调用失败(adcode 取不到)。\n" +
|
|
654
|
+
"原因通常是 ak 未启用 Geocoding API 服务、或 referer 白名单不含当前域名。\n" +
|
|
655
|
+
"诊断步骤:\n" +
|
|
656
|
+
" 1) 把下面这条 URL 复制到浏览器新 tab 打开,看百度返回的 status / message:\n" +
|
|
657
|
+
" " +
|
|
658
|
+
url +
|
|
659
|
+
"\n" +
|
|
660
|
+
" 2) 去百度地图开放平台「我的应用 → 设置」给当前 ak 勾选 'Geocoding API'\n" +
|
|
661
|
+
" 并把 Referer 白名单加上当前域名(或临时填 `*` 放开)。\n" +
|
|
662
|
+
"组件已自动 fallback 到 JSAPI 路径,name/address/省市区名仍可用,仅 adcode=undefined。");
|
|
663
|
+
return null;
|
|
664
|
+
}
|
|
665
|
+
if (!raw || raw.status !== 0 || !raw.result) {
|
|
666
|
+
// 这个分支:百度返回了**已包装 callback 的错误体**(少数情况),jsonp
|
|
667
|
+
// 能正常解析为 JS 对象,但业务 status != 0。打印完整 status + message
|
|
668
|
+
// 让业务方能直接看到原因,比 onerror 分支更"会说话"。
|
|
669
|
+
if ((raw === null || raw === void 0 ? void 0 : raw.status) !== undefined) {
|
|
670
|
+
console.warn("[MapLocationSelection] reverse_geocoding/v3 业务错误(adcode 取不到,已 fallback 到 JSAPI)。\n" +
|
|
671
|
+
"status=" +
|
|
672
|
+
raw.status +
|
|
673
|
+
" message=" +
|
|
674
|
+
(raw.message || "") +
|
|
675
|
+
"\n请求 URL:" +
|
|
676
|
+
url);
|
|
677
|
+
}
|
|
678
|
+
return null;
|
|
679
|
+
}
|
|
680
|
+
const result = raw.result;
|
|
681
|
+
const ac = result.addressComponent || {};
|
|
682
|
+
const business = ((_a = result.business) !== null && _a !== void 0 ? _a : "").toString().trim();
|
|
683
|
+
const firstBusiness = business
|
|
684
|
+
? (_b = business.split(/,|,/)[0]) === null || _b === void 0 ? void 0 : _b.trim()
|
|
685
|
+
: "";
|
|
686
|
+
const sematicRaw = ((_c = result.sematic_description) !== null && _c !== void 0 ? _c : "").toString().trim();
|
|
687
|
+
const town = ((_d = ac.town) !== null && _d !== void 0 ? _d : "").toString().trim();
|
|
688
|
+
const street = ((_e = ac.street) !== null && _e !== void 0 ? _e : "").toString().trim();
|
|
689
|
+
const nameCandidates = [
|
|
690
|
+
sematicRaw,
|
|
691
|
+
firstBusiness,
|
|
692
|
+
town,
|
|
693
|
+
street,
|
|
694
|
+
];
|
|
695
|
+
const name = (_f = nameCandidates
|
|
696
|
+
.map((s) => (s !== null && s !== void 0 ? s : "").toString().trim())
|
|
697
|
+
.find(Boolean)) !== null && _f !== void 0 ? _f : "";
|
|
698
|
+
const fullAddr = ((_g = result.formatted_address) !== null && _g !== void 0 ? _g : "").toString().trim();
|
|
699
|
+
// name / address 偶尔会一致(如同时 fallback 到 town),让 address 留空,
|
|
700
|
+
// 与周边 POI 项「address 为空只渲染距离」的展示分支保持一致。
|
|
701
|
+
const address = fullAddr === name ? "" : fullAddr;
|
|
702
|
+
// adcode 是 int 类型(如 510105,不是字符串)。toString 后正则校验为 6 位
|
|
703
|
+
// 数字才采纳,非法(极少数边境地区可能给 0 / 空)置 undefined 由上层兜底。
|
|
704
|
+
const adcodeRaw = ac.adcode !== undefined ? String(ac.adcode).trim() : "";
|
|
705
|
+
const adcode = /^\d{6}$/.test(adcodeRaw) ? adcodeRaw : undefined;
|
|
706
|
+
return {
|
|
707
|
+
name,
|
|
708
|
+
address,
|
|
709
|
+
province: ac.province,
|
|
710
|
+
city: ac.city || ac.province,
|
|
711
|
+
district: ac.district,
|
|
712
|
+
adcode,
|
|
713
|
+
};
|
|
714
|
+
});
|
|
715
|
+
}
|
|
716
|
+
// JSAPI fallback:webservice 失败时仍能拿到 name / address / 省市区名称。
|
|
717
|
+
//
|
|
718
|
+
// 关于 adcode:BMapGL JSAPI 类参考文档明确 AddressComponent 不含 adcode
|
|
719
|
+
// 字段(详见头部 v5.3 注释),但**百度文档历来滞后于实现**,新版 SDK 的
|
|
720
|
+
// result 上**可能**藏着 adcode(无法通过文档证实,只能运行时探测)。这里
|
|
721
|
+
// 容错读取以下几个可能位置:
|
|
722
|
+
// 1) result.addressComponents.adcode (文档没列但最可能存在的位置)
|
|
723
|
+
// 2) result.adcode (顶级,参考百度其他接口习惯)
|
|
724
|
+
// 3) result.addressComponent.adcode (单数形式 component,对齐 webservice)
|
|
725
|
+
// 任意一个能拿到合法 6 位数字就采纳——比"硬编码 undefined"更稳健。
|
|
726
|
+
//
|
|
727
|
+
// 调试日志:webservice 失败时这条路径才会执行,**默认打印完整 result**——
|
|
728
|
+
// 业务方在浏览器 console 能直接看到 BMapGL 返回的原始字段结构,协助识别
|
|
729
|
+
// 是否真的有暗藏 adcode(如果发现某新版 SDK 真有,可在此处补字段名)。
|
|
730
|
+
//
|
|
731
|
+
// name 候选优先级:商圈[0] → 镇 → 街道(覆盖中心点的具体地名)。
|
|
732
|
+
// 不掺 POI 名——POI 借用由 commitMapCenter 用 searchAround 的结果统一处理,
|
|
733
|
+
// 跟高德端的对称(reverseGeocode 只负责"地名",POI 列表走另一条路径)。
|
|
734
|
+
reverseGeocodeViaJsapi(center) {
|
|
735
|
+
const BMap = this.BMap;
|
|
736
|
+
const geocoder = this.geocoder;
|
|
737
|
+
if (!BMap || !geocoder)
|
|
738
|
+
return Promise.resolve(null);
|
|
739
|
+
return new Promise((resolve) => {
|
|
740
|
+
geocoder.getLocation(new BMap.Point(center[0], center[1]), (result) => {
|
|
741
|
+
var _a, _b, _c, _d;
|
|
742
|
+
if (!result) {
|
|
743
|
+
resolve(null);
|
|
744
|
+
return;
|
|
745
|
+
}
|
|
746
|
+
// 把完整 result 打出来,便于用户排查 BMapGL 是否藏了 adcode 字段。
|
|
747
|
+
// 这条日志只在 webservice 主路径失败时才出现,不会污染正常调用。
|
|
748
|
+
console.info("[MapLocationSelection] BMapGL.Geocoder result(用于排查 adcode 字段):", result);
|
|
749
|
+
const ac = result.addressComponents || {};
|
|
750
|
+
const business = ((_a = result.business) !== null && _a !== void 0 ? _a : "").toString().trim();
|
|
751
|
+
const firstBusiness = business
|
|
752
|
+
? (_b = business.split(/,|,/)[0]) === null || _b === void 0 ? void 0 : _b.trim()
|
|
753
|
+
: "";
|
|
754
|
+
const nameCandidates = [firstBusiness, ac.town, ac.street];
|
|
755
|
+
const name = (_c = nameCandidates
|
|
756
|
+
.map((s) => (s !== null && s !== void 0 ? s : "").toString().trim())
|
|
757
|
+
.find(Boolean)) !== null && _c !== void 0 ? _c : "";
|
|
758
|
+
const fullAddr = ((_d = result.address) !== null && _d !== void 0 ? _d : "").toString().trim();
|
|
759
|
+
// name / address 偶尔会完全相同(如同时 fallback 到 town),让 address
|
|
760
|
+
// 留空,跟周边 POI 项「address 为空只渲染距离」的分支保持一致。
|
|
761
|
+
const address = fullAddr === name ? "" : fullAddr;
|
|
762
|
+
// 多位置探测 adcode:万一新版 BMapGL 实际返回了,但文档没列。
|
|
763
|
+
const adcodeCandidates = [
|
|
764
|
+
ac.adcode,
|
|
765
|
+
result.adcode,
|
|
766
|
+
(result.addressComponent || {}).adcode,
|
|
767
|
+
];
|
|
768
|
+
let adcode;
|
|
769
|
+
for (const c of adcodeCandidates) {
|
|
770
|
+
if (c === undefined || c === null)
|
|
771
|
+
continue;
|
|
772
|
+
const s = String(c).trim();
|
|
773
|
+
if (/^\d{6}$/.test(s)) {
|
|
774
|
+
adcode = s;
|
|
775
|
+
break;
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
resolve({
|
|
779
|
+
name,
|
|
780
|
+
address,
|
|
781
|
+
province: ac.province,
|
|
782
|
+
city: ac.city || ac.province,
|
|
783
|
+
district: ac.district,
|
|
784
|
+
adcode,
|
|
785
|
+
});
|
|
786
|
+
});
|
|
787
|
+
});
|
|
788
|
+
}
|
|
789
|
+
on(event, handler) {
|
|
790
|
+
if (!this.map)
|
|
791
|
+
return;
|
|
792
|
+
if (event === "click") {
|
|
793
|
+
this.map.on("click", (e) => {
|
|
794
|
+
// BMapGL 官方 SKILL 内部对经纬度字段的描述自相矛盾:
|
|
795
|
+
// - base-classes.md 示例:经纬度在 e.latlng(GL 版本主流写法);
|
|
796
|
+
// - map-events.md 表格:经纬度在 e.point(与老 BMap 一致)。
|
|
797
|
+
// 实测部分版本下 e.point 是 EarthMC 地理墨卡托坐标(百万级米),当经纬度用
|
|
798
|
+
// 会让地图跳到地球另一角落 —— 这正是"百度地图一点就偏到不知道哪里"的根因。
|
|
799
|
+
// 取值优先级(从最稳到兜底):
|
|
800
|
+
// 1) e.latlng:GL 主流字段,明确是 BD09 经纬度;
|
|
801
|
+
// 2) e.pixel + map.pixelToPoint(pixel):屏幕像素→经纬度,API 跨版本稳定;
|
|
802
|
+
// 3) e.point:仅当前两者都不存在时才用,并校验数值落在合法经纬度区间,
|
|
803
|
+
// 避免把 EarthMC 米数喂出去。
|
|
804
|
+
const isValidLngLat = (lng, lat) => lng >= -180 && lng <= 180 && lat >= -90 && lat <= 90;
|
|
805
|
+
let lng;
|
|
806
|
+
let lat;
|
|
807
|
+
if ((e === null || e === void 0 ? void 0 : e.latlng) &&
|
|
808
|
+
typeof e.latlng.lng === "number" &&
|
|
809
|
+
typeof e.latlng.lat === "number") {
|
|
810
|
+
lng = e.latlng.lng;
|
|
811
|
+
lat = e.latlng.lat;
|
|
812
|
+
}
|
|
813
|
+
else if ((e === null || e === void 0 ? void 0 : e.pixel) && typeof this.map.pixelToPoint === "function") {
|
|
814
|
+
const p = this.map.pixelToPoint(e.pixel);
|
|
815
|
+
if (p && typeof p.lng === "number" && typeof p.lat === "number") {
|
|
816
|
+
lng = p.lng;
|
|
817
|
+
lat = p.lat;
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
else if ((e === null || e === void 0 ? void 0 : e.point) &&
|
|
821
|
+
typeof e.point.lng === "number" &&
|
|
822
|
+
typeof e.point.lat === "number" &&
|
|
823
|
+
isValidLngLat(e.point.lng, e.point.lat)) {
|
|
824
|
+
lng = e.point.lng;
|
|
825
|
+
lat = e.point.lat;
|
|
826
|
+
}
|
|
827
|
+
if (typeof lng === "number" && typeof lat === "number") {
|
|
828
|
+
handler(lng, lat);
|
|
829
|
+
}
|
|
830
|
+
});
|
|
831
|
+
}
|
|
832
|
+
else if (event === "movestart" || event === "moveend") {
|
|
833
|
+
this.map.on(event, () => handler());
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
exports.BMapProvider = BMapProvider;
|