feat(急救服务平台): 附近资源聚合展示与路径规划优化

- 新增 Map.createClusterOverlay:30m 内点位合并为带数字徽标的聚合
  marker,聚合点与独立 marker 按距离用户统一编号 1, 2, 3...
- 聚合徽标使用纯色圆 + 数量 N + 左上角位置编号角标,hover 显示簇内
  数量与最近资源名称
- 点击聚合 marker 弹 isCustom InfoWindow 列表,自带关闭按钮 + 选中
  触发原有路径规划回调
- 独立 marker 用 typeToIcon 纯 url 同步渲染,canvas 角标异步叠加
  失败不影响主渲染
- 修复 removeOverlayGroup 未关闭簇列表弹窗的 bug
- confirmHelp 加 createOverlay1 fallback,聚合层异常时回退原渲染
This commit is contained in:
2026-09-16 09:52:47 +08:00
parent 9431553951
commit 4c30187435
3 changed files with 639 additions and 31 deletions
+327
View File
@@ -122,3 +122,330 @@ export function getCenter(list = []) {
let centerObj = turf.center(features);
return centerObj?.geometry?.coordinates;
}
/**
* @desc 球面距离(haversine),返回米
* @param lng1 经度1
* @param lat1 纬度1
* @param lng2 经度2
* @param lat2 纬度2
*/
export function haversine(lng1: number, lat1: number, lng2: number, lat2: number): number {
const toRad = (d: number) => (d * Math.PI) / 180;
const R = 6371008.8; // 地球平均半径,单位米
const dLat = toRad(lat2 - lat1);
const dLng = toRad(lng2 - lng1);
const a = Math.sin(dLat / 2) ** 2 + Math.cos(toRad(lat1)) * Math.cos(toRad(lat2)) * Math.sin(dLng / 2) ** 2;
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
return R * c;
}
/**
* @desc 单链聚合:距离阈值(米)内的点归为同一簇;簇内不足2点返回 null
* @param items 待聚合的点位(必须含 lon/lat 经纬度)
* @param radiusMeters 聚合半径,单位米,默认 30
*/
export function clusterByRadius(items: any[] = [], radiusMeters = 30): any[][] {
const clusters: any[][] = [];
const visited = new Array(items.length).fill(false);
for (let i = 0; i < items.length; i++) {
if (visited[i]) continue;
const seed = items[i];
const seedLng = Number(seed.lon ?? seed.longitude ?? seed.lng);
const seedLat = Number(seed.lat ?? seed.latitude);
if (!seedLng || !seedLat) continue;
const cluster: any[] = [seed];
visited[i] = true;
for (let j = i + 1; j < items.length; j++) {
if (visited[j]) continue;
const cur = items[j];
const curLng = Number(cur.lon ?? cur.longitude ?? cur.lng);
const curLat = Number(cur.lat ?? cur.latitude);
if (!curLng || !curLat) continue;
const d = haversine(seedLng, seedLat, curLng, curLat);
if (d <= radiusMeters) {
cluster.push(cur);
visited[j] = true;
}
}
// 不足 2 个点不形成聚合(保留为独立 marker)
if (cluster.length >= 2) {
clusters.push(cluster);
}
}
return clusters;
}
/**
* @desc 计算簇的几何中心(取所有点的经纬度平均)
*/
export function getClusterCenter(cluster: any[] = []): [number, number] {
if (!cluster.length) return [0, 0];
let sumLng = 0;
let sumLat = 0;
let count = 0;
for (const item of cluster) {
const lng = Number(item.lon ?? item.longitude ?? item.lng);
const lat = Number(item.lat ?? item.latitude);
if (!lng || !lat) continue;
sumLng += lng;
sumLat += lat;
count++;
}
if (!count) return [0, 0];
return [sumLng / count, sumLat / count];
}
/**
* @desc 按数量返回聚合徽标的颜色
* @param count 簇内点数
*/
export function getClusterColor(count: number): { bg: string; border: string } {
if (count >= 10) return { bg: '#E74C3C', border: '#B83A2D' };
if (count >= 5) return { bg: '#F5A623', border: '#C77F12' };
return { bg: '#3FA9F5', border: '#2380C7' };
}
/**
* @desc 生成聚合 marker 的 HTML 内容(纯色圆形 + 数量 N + 左上角位置编号角标)
* @param count 簇内点数(主圈中心数字)
* @param items 簇内所有点位,用于挑最近资源并生成 title 提示
* @param userLocation [lng, lat] 用户位置,用于挑最近资源
* @param clusterNumber 聚合点本身的位置编号(按距离用户升序),左上角小角标显示;不传则不显示
*/
export function buildClusterIcon(
count: number,
items: any[] = [],
userLocation?: [number, number],
clusterNumber?: number
): string {
// 挑选最近的资源(仅用于 title 提示)
let nearest: any = items[0] || {};
if (userLocation && items.length > 1) {
let minDist = Infinity;
for (const it of items) {
const lng = Number(it.lon ?? it.longitude ?? it.lng);
const lat = Number(it.lat ?? it.latitude);
if (!lng || !lat) continue;
const d = haversine(userLocation[0], userLocation[1], lng, lat);
if (d < minDist) {
minDist = d;
nearest = it;
}
}
}
const { bg, border } = getClusterColor(count);
const nearestName = nearest.name || typeToLabel(nearest.type);
const tip = `附近 ${count} 个资源,最近:${nearestName}`;
// 左上角位置编号角标(仅当传了 clusterNumber 才显示)
const badgeHtml =
clusterNumber !== undefined && clusterNumber !== null
? `<div style="
position: absolute;
top: -6px;
left: -6px;
min-width: 22px;
height: 22px;
padding: 0 6px;
line-height: 22px;
border-radius: 11px;
background: #E74C3C;
border: 2px solid #fff;
color: #fff;
text-align: center;
font-weight: 700;
font-size: 12px;
box-shadow: 0 1px 3px rgba(0,0,0,0.4);
">${clusterNumber}</div>`
: '';
// 主圈:纯色圆 + 数量 N 在中心
return `
<div class="cluster-marker" title="${tip}" style="
position: relative;
width: 44px;
height: 44px;
cursor: pointer;
user-select: none;
">
<div style="
position: absolute;
left: 0;
top: 0;
width: 44px;
height: 44px;
line-height: 44px;
text-align: center;
border-radius: 50%;
background: ${bg};
border: 3px solid ${border};
color: #fff;
font-weight: 700;
font-size: 18px;
box-shadow: 0 2px 6px rgba(0,0,0,0.35);
">${count}</div>
${badgeHtml}
</div>`;
}
/**
* @desc 异步创建带数字角标的图标:图片加载完成后绘制到 canvas,再叠加红色数字角标
* @param originalIconUrl 原始图标 url(来自 typeToIcon
* @param text 角标文字(数字 1/2/3/...
* @param type 类型:1 医院 / 3 健康小屋 / 4 AED / 5 救护车,影响 canvas 尺寸
*/
export function createNumberedMarkerIcon(originalIconUrl: string, text: string | number, type: string | number): Promise<string> {
return new Promise((resolve) => {
// 异常兜底:url 为空直接返回空串,让调用方决定
if (!originalIconUrl) return resolve('');
// 兜底:不在浏览器环境(SSR / 测试),返回原 url
if (typeof window === 'undefined' || typeof document === 'undefined') {
return resolve(originalIconUrl);
}
const width = type == 1 ? 40 : 28;
const height = type == 1 ? 69 : 49;
const canvas = document.createElement('canvas');
canvas.width = width;
canvas.height = height + 15;
const ctx = canvas.getContext('2d');
if (!ctx) return resolve(originalIconUrl);
const img = new Image();
// 不设置 crossOriginVite 的本地资源是 blob URL,无需 CORS
// 设置 crossOrigin=anonymous 在某些环境下反而触发 onerror。
img.onload = () => {
try {
ctx.drawImage(img, 0, 10, width, height);
const textX = type == 1 ? 32 : 22;
const textY = 11;
const textStr = String(text);
const textWidth = ctx.measureText(textStr).width;
const circleRadius = Math.max(textWidth / 2 + 1, 6);
ctx.beginPath();
ctx.arc(textX, textY, circleRadius, 0, Math.PI * 2);
ctx.fillStyle = 'red';
ctx.fill();
ctx.font = 'bold 10px Arial';
ctx.textAlign = 'right';
ctx.textBaseline = 'middle';
ctx.fillStyle = '#ffffff';
ctx.fillText(textStr, type == 1 ? 35 : 24.5, 10.8);
resolve(canvas.toDataURL());
} catch (e) {
// canvas 污染等异常:降级到原 url
resolve(originalIconUrl);
}
};
img.onerror = () => resolve(originalIconUrl);
img.src = originalIconUrl;
// 兜底:5 秒未触发 onload/onerror 也降级
setTimeout(() => resolve(originalIconUrl), 5000);
});
}
/**
* @desc 类型编号 → 中文标签(用于簇列表)
* @param type 1 医院 / 3 健康小屋 / 4 AED / 5 救护车
*/
export function typeToLabel(type: string | number): string {
const map: Record<string, string> = {
'1': '医院',
'3': '健康小屋',
'4': 'AED',
'5': '救护车',
};
return map[String(type)] || '资源';
}
/**
* @desc 生成簇列表 InfoWindow 的 HTML 内容
* @param items 簇内点位
* @param userLocation [lng, lat] 用户位置,用于计算直线距离并排序
*/
export function buildClusterInfoContent(items: any[] = [], userLocation?: [number, number]): string {
// 计算直线距离并按距离升序
const list = items.map((it) => {
const lng = Number(it.lon ?? it.longitude ?? it.lng);
const lat = Number(it.lat ?? it.latitude);
let distance = Number(it.distance);
if (!distance && userLocation) {
distance = haversine(userLocation[0], userLocation[1], lng, lat);
}
return { ...it, _distance: distance || 0, _lng: lng, _lat: lat };
});
list.sort((a, b) => a._distance - b._distance);
const formatDist = (m: number) => (m >= 1000 ? `${(m / 1000).toFixed(2)} 公里` : `${Math.round(m)}`);
const rows = list
.map(
(it, idx) => `
<div class="cluster-row" data-idx="${idx}" style="
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 10px;
cursor: pointer;
border-bottom: 1px solid rgba(255,255,255,0.1);
transition: background 0.2s;
">
<div style="display: flex; align-items: center; gap: 8px;">
<span style="
display: inline-block;
width: 10px;
height: 10px;
border-radius: 50%;
background: #3FA9F5;
flex-shrink: 0;
"></span>
<span style="color: #fff; font-size: 14px;">${it.name || typeToLabel(it.type)}</span>
<span style="color: rgba(255,255,255,0.6); font-size: 12px;">${typeToLabel(it.type)}</span>
</div>
<span style="color: rgba(255,255,255,0.85); font-size: 13px;">${formatDist(it._distance)}</span>
</div>`
)
.join('');
return `
<div class="cluster-info-window" style="
position: relative;
width: 260px;
background: rgba(15, 32, 60, 0.95);
border-radius: 8px;
padding: 12px 0;
color: #fff;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
box-shadow: 0 4px 16px rgba(0,0,0,0.35);
box-sizing: border-box;
">
<button class="cluster-info-close" type="button" style="
position: absolute;
top: 6px;
right: 6px;
width: 22px;
height: 22px;
line-height: 18px;
text-align: center;
border-radius: 50%;
background: rgba(255,255,255,0.15);
border: 0;
color: #fff;
font-size: 16px;
cursor: pointer;
padding: 0;
font-family: inherit;
">×</button>
<div style="
padding: 0 36px 8px 14px;
font-size: 14px;
font-weight: 700;
border-bottom: 1px solid rgba(255,255,255,0.15);
">附近 ${items.length} 个资源</div>
<div class="cluster-rows">${rows}</div>
<div style="
padding: 6px 14px 0;
font-size: 12px;
color: rgba(255,255,255,0.5);
">点击列表项查看详情并规划路线</div>
</div>`;
}
+243 -1
View File
@@ -1,7 +1,20 @@
import AMapLoader from '@amap/amap-jsapi-loader';
import china from '/@/assets/lngLat/china.ts';
import chinaInner from '/@/assets/lngLat/chinaInner.ts';
import { createCircle, createIcon, getCenter, getLonLat, typeToIcon, typeToIcon1 } from '/@/assets/mapUtils/commonFun.ts';
import {
buildClusterIcon,
buildClusterInfoContent,
clusterByRadius,
createCircle,
createIcon,
createNumberedMarkerIcon,
getCenter,
getClusterCenter,
getLonLat,
haversine,
typeToIcon,
typeToIcon1,
} from '/@/assets/mapUtils/commonFun.ts';
import { aircraftContent, alarmIcon, female, male, watchIconR } from '/@/components/chinaMap/chinaHooks.ts';
import { isFunction } from '/@/utils/is.ts';
import { Callback, ExtDataType, InitMap, MarkerOption, ResultType } from '/@/assets/mapUtils/mapFunTypes.ts';
@@ -27,6 +40,9 @@ export const Map = {
mapDriverOrWalkLoading: false,
layerGroup: null,
layerData: null,
clusterInfoWindow: null, // 簇列表弹窗(聚合 marker 点击后展示)
clusterRadius: 30, // 聚合半径,单位米
clusterUserLocation: null, // 用户位置 [lng, lat],用于列表显示距离
/**
* @desc 地图舒适化
* @param el dom元素 id选择器
@@ -146,6 +162,11 @@ export const Map = {
}
this.clearActiveHospital();
this.clearInfoModal();
// 关闭簇列表弹窗(聚合 marker 列表弹窗)
if (this.clusterInfoWindow) {
this.clusterInfoWindow.close();
this.clusterInfoWindow = null;
}
// this.map.remove(this.airMarkerList);
}, //
/**
@@ -322,6 +343,227 @@ export const Map = {
});
return arr;
},
/**
* @desc 创建带聚合的覆盖物组:距离阈值内的点位合并为聚合 marker(最近资源图标+数字角标),
* 点击聚合 marker 弹出 InfoWindow 展示簇内点位列表;簇外点位独立渲染。
* @param result 待渲染的点位(医院/医疗点/AED 等)
* @param extData 与 createOverlay1 一致,content/extOption 用于 label 等
* @param callback 单个点位被选中时的回调(被聚合列表选中也会调用,签名相同)
* @param computedCenter 是否计算视野中心
* @param iconType 同 createOverlay1(保留参数占位)
* @param radius 聚合半径,单位米,默认 30
* @param userLocation 用户位置 [lng, lat],用于挑选最近资源 + 簇列表显示距离
*/
async createClusterOverlay(
result: ResultType[],
extData?: ExtDataType,
callback?: Callback,
computedCenter?: boolean,
iconType?: string,
radius?: number,
userLocation?: [number, number]
) {
this.removeOverlayGroup();
this.overlayGroup = new AMap.OverlayGroup();
// 关闭上一次的簇列表弹窗
if (this.clusterInfoWindow) {
this.clusterInfoWindow.close();
this.clusterInfoWindow = null;
}
const r = radius || this.clusterRadius || 30;
if (userLocation) {
this.clusterUserLocation = userLocation;
}
// 过滤掉缺少经纬度或类型的点
const validItems: any[] = [];
for (const item of result || []) {
const lon = Number(item.lon ?? item.longitude ?? item.lng);
const lat = Number(item.lat ?? item.latitude);
const type = item.type;
if (!lon || !lat || !type) continue;
validItems.push({ ...item, lon, lat });
}
// 单链聚类(30m 内合并)
const clusters = clusterByRadius(validItems, r);
const clusteredSet = new Set();
for (const c of clusters) {
for (const item of c) clusteredSet.add(item);
}
const singles = validItems.filter((it) => !clusteredSet.has(it));
const allMarkers: any[] = [];
// 收集所有"位置点":每个聚合点(按 cluster center)和每个独立点都是一个位置点
// 统一按距离用户位置升序排序后连续编号 1, 2, 3, ...
// 这样聚合点和独立 marker 不会各自从 #1 开始,地图上编号全局唯一
const positionPoints: any[] = [];
for (const cluster of clusters) {
const [cLng, cLat] = getClusterCenter(cluster);
positionPoints.push({ kind: 'cluster', cluster, cLng, cLat });
}
for (const item of singles) {
positionPoints.push({ kind: 'single', item });
}
if (userLocation) {
positionPoints.sort((a, b) => {
const da =
a.kind === 'cluster'
? haversine(userLocation[0], userLocation[1], a.cLng, a.cLat)
: haversine(userLocation[0], userLocation[1], a.item.lon, a.item.lat);
const db =
b.kind === 'cluster'
? haversine(userLocation[0], userLocation[1], b.cLng, b.cLat)
: haversine(userLocation[0], userLocation[1], b.item.lon, b.item.lat);
return da - db;
});
}
// 同步渲染聚合 marker(HTML,无异步依赖)
const singleMarkerRefs: Array<{ marker: any; item: any; number: number }> = [];
positionPoints.forEach((p, idx) => {
const number = idx + 1;
if (p.kind === 'cluster') {
// 聚合 marker:纯色圆 + 数量 N + 左上角位置编号角标
const clusterMarker: any = new AMap.Marker({
position: [p.cLng, p.cLat],
content: buildClusterIcon(p.cluster.length, p.cluster, userLocation, number),
clickable: true,
offset: new AMap.Pixel(-22, -22), // 主圈 44x44,中心对准坐标
extData: { __isCluster: true, items: p.cluster, clusterNumber: number },
zIndex: 110,
});
// 点击聚合 → 打开 InfoWindow
clusterMarker.on('click', () => {
this.openClusterInfoWindow(p.cluster, [p.cLng, p.cLat], callback);
});
this.overlayGroup.setMap(this.map);
this.overlayGroup.addOverlay(clusterMarker);
allMarkers.push(clusterMarker);
} else {
// 同步渲染独立 marker:先用 typeToIcon 的纯 url(保证渲染成功,无角标也行)
// 后续异步优化:用 canvas 画带角标的图标,setIcon 替换
const iconUrl: string = typeToIcon(p.item.type);
const startMarker: any = new AMap.Marker({
position: [p.item.lon, p.item.lat],
icon: iconUrl,
clickable: true,
offset: [-18, -32],
maxZoom: 14,
extData: p.item,
label: {
content: isFunction(extData?.content) && extData?.content(p.item),
},
zIndex: 99,
...extData?.extOption,
});
this.overlayGroup.setMap(this.map);
this.overlayGroup.addOverlay(startMarker);
allMarkers.push(startMarker);
// 记录独立 marker 引用,后续异步替换为带角标图标
singleMarkerRefs.push({ marker: startMarker, item: p.item, number });
}
});
// 异步优化:为独立 marker 叠加数字角标(canvas 失败也不影响主渲染流程)
// 主渲染已经完成(marker 已上地图),这里只是 setIcon 替换图标
singleMarkerRefs.forEach(({ marker, item, number }) => {
createNumberedMarkerIcon(typeToIcon(item.type), number, item.type)
.then((numberedUrl) => {
if (numberedUrl && marker && typeof marker.setIcon === 'function') {
try {
marker.setIcon(numberedUrl);
} catch (e) {
// 静默:setIcon 失败不影响
}
}
})
.catch(() => {
// 静默:canvas 失败保留原 url 图标
});
});
// 独立 marker 点击 → 直接走原回调
this.overlayGroup.on('click', (e: any) => {
const ext = e?.target?.getExtData?.();
if (!ext || ext.__isCluster) return; // 聚合已自行处理
if (!isFunction(callback)) return;
callback(ext);
});
if (computedCenter && allMarkers.length) {
try {
this.map && this.map.setFitView(allMarkers, false, [50, 50, 0, 50]);
} catch (e) {
console.warn('[createClusterOverlay] setFitView failed:', e);
}
}
return allMarkers;
},
/**
* @desc 打开簇列表 InfoWindow,点击列表项触发与单点点击相同的回调
*/
openClusterInfoWindow(cluster: any[], position: [number, number], callback?: Callback) {
if (!this.map || !AMap) return;
// 关闭上一次的
if (this.clusterInfoWindow) {
this.clusterInfoWindow.close();
this.clusterInfoWindow = null;
}
// 把 HTML 转成 DOM 元素直接传给 InfoWindow,避免 setTimeout + querySelector 的时序问题
// 事件直接绑在元素上,AMap 插入到地图 DOM 后 listener 依然生效(事件 listener 跟元素绑定,不跟 DOM 位置绑定)
const wrapper = document.createElement('div');
wrapper.innerHTML = buildClusterInfoContent(cluster, this.clusterUserLocation);
const root = wrapper.querySelector('.cluster-info-window') as HTMLElement | null;
const closeWindow = () => {
if (this.clusterInfoWindow) {
this.clusterInfoWindow.close();
this.clusterInfoWindow = null;
}
};
if (root) {
// 列表行点击
const rows = root.querySelectorAll('.cluster-row');
rows.forEach((row) => {
row.addEventListener('click', (e) => {
e.stopPropagation();
const idx = Number((row as HTMLElement).getAttribute('data-idx') || 0);
const target = cluster[idx];
if (!target) return;
closeWindow();
if (isFunction(callback)) callback(target);
});
row.addEventListener('mouseenter', () => {
(row as HTMLElement).style.background = 'rgba(255,255,255,0.08)';
});
row.addEventListener('mouseleave', () => {
(row as HTMLElement).style.background = 'transparent';
});
});
// 关闭按钮
const closeBtn = root.querySelector('.cluster-info-close');
if (closeBtn) {
closeBtn.addEventListener('click', (e) => {
e.stopPropagation();
closeWindow();
});
}
}
this.clusterInfoWindow = new AMap.InfoWindow({
position,
offset: new AMap.Pixel(0, -10),
content: wrapper,
isCustom: true,
autoMove: true,
closeWhenClickMap: true,
});
this.clusterInfoWindow.open(this.map, position);
},
clearOverlay() {
if (this.overlayGroup) {
this.map.remove(this.overlayGroup);
+69 -30
View File
@@ -436,37 +436,76 @@
centerMapRef.value.setStartPosition(data);
const a = ((await getHospitalList(data.hospitalList, point)) as any) || [];
hospitalList.value = a.sort((a: any, b: any) => a.distance - b.distance);
const arr = await Map.createOverlay1(
hospitalList.value.concat([{ latitude: data.lat, longitude: data.lon }]),
{
content: (val: any) => {
return `<div style="
width: 21px;
height: 44px;
position: absolute;
left: -20px;
top: -20px;" title="${val?.name}">
</div>`;
// 优先走聚合层;如抛错则回退到原 createOverlay1,保证至少能渲染
let arr: any[];
try {
arr = await Map.createClusterOverlay(
hospitalList.value.concat([{ latitude: data.lat, longitude: data.lon }]),
{
content: (val: any) => {
return `<div style="
width: 21px;
height: 44px;
position: absolute;
left: -20px;
top: -20px;" title="${val?.name}">
</div>`;
},
},
},
async (val) => {
if (Map.mapDriverOrWalkLoading) return;
Map.createInfoModal({ option: val, content: infoContentSpecial(val) }, false);
const end = await Map.getAddressByPoint([val.lon, val.lat]);
if (!data?.address) {
data['address'] = await Map.getAddressByPoint([data.lon, data.lat]);
}
centerMapRef.value.getSearch({
startPosition: [data.lon, data.lat],
endPosition: [val.lon, val.lat],
start: data['address'],
end,
});
('');
},
true,
'1'
);
async (val) => {
if (Map.mapDriverOrWalkLoading) return;
Map.createInfoModal({ option: val, content: infoContentSpecial(val) }, false);
const end = await Map.getAddressByPoint([val.lon, val.lat]);
if (!data?.address) {
data['address'] = await Map.getAddressByPoint([data.lon, data.lat]);
}
centerMapRef.value.getSearch({
startPosition: [data.lon, data.lat],
endPosition: [val.lon, val.lat],
start: data['address'],
end,
});
('');
},
true,
'1',
30,
[data.lon, data.lat]
);
} catch (e) {
console.error('[confirmHelp] createClusterOverlay 失败,回退到 createOverlay1:', e);
arr = await Map.createOverlay1(
hospitalList.value.concat([{ latitude: data.lat, longitude: data.lon }]),
{
content: (val: any) => {
return `<div style="
width: 21px;
height: 44px;
position: absolute;
left: -20px;
top: -20px;" title="${val?.name}">
</div>`;
},
},
async (val) => {
if (Map.mapDriverOrWalkLoading) return;
Map.createInfoModal({ option: val, content: infoContentSpecial(val) }, false);
const end = await Map.getAddressByPoint([val.lon, val.lat]);
if (!data?.address) {
data['address'] = await Map.getAddressByPoint([data.lon, data.lat]);
}
centerMapRef.value.getSearch({
startPosition: [data.lon, data.lat],
endPosition: [val.lon, val.lat],
start: data['address'],
end,
});
('');
},
true,
'1'
);
}
Map.getAddressByPoint(point)
.then((res) => {