2025-10-16 21:32:16 +08:00
|
|
|
|
// 地图服务 - 处理地图相关的功能,包括定位、路线规划、地理编码等
|
2025-10-18 22:21:04 +08:00
|
|
|
|
|
2025-10-16 21:32:16 +08:00
|
|
|
|
import { AMapRegeoResponse, RoutePlanResult, SearchResult, LocationData } from '../types';
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 地图服务类
|
|
|
|
|
* 提供地图相关功能,包括定位、路线规划、地理编码等
|
|
|
|
|
*/
|
|
|
|
|
class MapService {
|
|
|
|
|
// 引入高德地图小程序SDK
|
|
|
|
|
private amapFile: any;
|
|
|
|
|
|
|
|
|
|
// 高德地图API密钥
|
|
|
|
|
private readonly MAP_KEY: string;
|
|
|
|
|
|
|
|
|
|
// 昆明中心点坐标
|
|
|
|
|
private readonly KUNMING_CENTER: { longitude: number; latitude: number };
|
|
|
|
|
|
|
|
|
|
// 高德地图实例
|
|
|
|
|
private amapInstance: any;
|
|
|
|
|
|
2025-10-18 22:21:04 +08:00
|
|
|
|
|
2025-10-16 21:32:16 +08:00
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 构造函数,初始化地图实例
|
|
|
|
|
*/
|
|
|
|
|
constructor() {
|
|
|
|
|
this.amapFile = require('../libs/amap-wx.js');
|
|
|
|
|
this.MAP_KEY = '1fc5cafd570d9fbfd14c39359d41823d';
|
|
|
|
|
this.KUNMING_CENTER = {
|
|
|
|
|
longitude: 102.833722,
|
|
|
|
|
latitude: 24.880095
|
|
|
|
|
};
|
|
|
|
|
this.amapInstance = new this.amapFile.AMapWX({ key: this.MAP_KEY });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 获取用户位置信息
|
|
|
|
|
* @returns 用户位置坐标
|
|
|
|
|
*/
|
|
|
|
|
async getLocation(): Promise<{ longitude: number; latitude: number }> {
|
|
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
|
console.log('开始调用高德地图SDK获取位置...');
|
|
|
|
|
|
|
|
|
|
// 添加超时机制
|
|
|
|
|
const timeout = setTimeout(() => {
|
|
|
|
|
console.error('高德地图SDK调用超时,使用模拟位置');
|
|
|
|
|
resolve({
|
|
|
|
|
longitude: 102.7123,
|
|
|
|
|
latitude: 25.0409
|
|
|
|
|
});
|
|
|
|
|
}, 10000); // 10秒超时
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
// 高德地图SDK的getWxLocation方法需要两个参数:配置对象和回调函数
|
|
|
|
|
this.amapInstance.getWxLocation({
|
|
|
|
|
type: 'gcj02', // 使用国测局坐标系
|
|
|
|
|
success: (res: { longitude: number; latitude: number }) => {
|
|
|
|
|
clearTimeout(timeout);
|
|
|
|
|
console.log('高德地图SDK定位成功:', res);
|
|
|
|
|
resolve(res);
|
|
|
|
|
},
|
|
|
|
|
fail: (err: any) => {
|
|
|
|
|
clearTimeout(timeout);
|
|
|
|
|
console.error('高德地图SDK定位失败:', err);
|
|
|
|
|
|
|
|
|
|
// 如果高德SDK失败,尝试使用微信原生API
|
|
|
|
|
console.log('尝试使用微信原生getLocation API...');
|
|
|
|
|
wx.getLocation({
|
|
|
|
|
type: 'gcj02',
|
|
|
|
|
success: (wxRes) => {
|
|
|
|
|
console.log('微信原生API定位成功:', wxRes);
|
|
|
|
|
resolve({
|
|
|
|
|
longitude: wxRes.longitude,
|
|
|
|
|
latitude: wxRes.latitude
|
|
|
|
|
});
|
|
|
|
|
},
|
|
|
|
|
fail: (wxErr) => {
|
|
|
|
|
console.error('微信原生API定位失败:', wxErr);
|
|
|
|
|
// 所有方法都失败,使用模拟位置
|
|
|
|
|
console.log('所有定位方法失败,使用模拟位置');
|
|
|
|
|
resolve({
|
|
|
|
|
longitude: 102.7123,
|
|
|
|
|
latitude: 25.0409
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}, (locationString: string) => {
|
|
|
|
|
// 这是高德地图SDK的回调函数,参数是格式化的位置字符串
|
|
|
|
|
console.log('高德地图SDK格式化位置:', locationString);
|
|
|
|
|
|
|
|
|
|
// 如果格式化位置回调被调用,说明SDK已经获取到位置
|
|
|
|
|
// 尝试从格式化字符串中解析经纬度
|
|
|
|
|
if (locationString) {
|
|
|
|
|
const coords = locationString.split(',');
|
|
|
|
|
if (coords.length === 2) {
|
|
|
|
|
const longitude = parseFloat(coords[0]);
|
|
|
|
|
const latitude = parseFloat(coords[1]);
|
|
|
|
|
|
|
|
|
|
if (!isNaN(longitude) && !isNaN(latitude)) {
|
|
|
|
|
clearTimeout(timeout);
|
|
|
|
|
console.log('从格式化位置解析成功,使用解析的位置:', { longitude, latitude });
|
|
|
|
|
resolve({ longitude, latitude });
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 如果无法解析,继续等待success回调
|
|
|
|
|
console.log('格式化位置回调被调用,但无法解析位置,继续等待success回调...');
|
|
|
|
|
});
|
|
|
|
|
} catch (error) {
|
|
|
|
|
clearTimeout(timeout);
|
|
|
|
|
console.error('高德地图SDK调用异常:', error);
|
|
|
|
|
reject(error);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 获取当前位置信息(兼容格式)
|
|
|
|
|
* @returns 当前位置的详细信息
|
|
|
|
|
*/
|
|
|
|
|
async getCurrentLocation(): Promise<{
|
|
|
|
|
latitude: number;
|
|
|
|
|
longitude: number;
|
|
|
|
|
accuracy?: number;
|
|
|
|
|
speed?: number;
|
|
|
|
|
altitude?: number;
|
|
|
|
|
}> {
|
|
|
|
|
const location = await this.getLocation();
|
|
|
|
|
return {
|
|
|
|
|
latitude: location.latitude,
|
|
|
|
|
longitude: location.longitude,
|
|
|
|
|
accuracy: 50,
|
|
|
|
|
speed: 0,
|
|
|
|
|
altitude: 1891
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 获取位置详细信息(逆地理编码)
|
|
|
|
|
* @param longitude 经度
|
|
|
|
|
* @param latitude 纬度
|
|
|
|
|
* @returns 逆地理编码结果
|
|
|
|
|
*/
|
|
|
|
|
async getLocationInfo(longitude: number, latitude: number): Promise<AMapRegeoResponse> {
|
|
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
|
this.amapInstance.getRegeo({
|
|
|
|
|
location: `${longitude.toFixed(6)},${latitude.toFixed(6)}`,
|
|
|
|
|
success: (res: AMapRegeoResponse) => {
|
|
|
|
|
console.log('逆地理编码成功:', res);
|
|
|
|
|
resolve(res);
|
|
|
|
|
},
|
|
|
|
|
fail: (err: any) => {
|
|
|
|
|
console.log('逆地理编码失败:', err);
|
|
|
|
|
reject(err);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 获取昆明中心点坐标
|
|
|
|
|
* @returns 昆明中心点坐标
|
|
|
|
|
*/
|
|
|
|
|
getKunmingCenter(): { longitude: number; latitude: number } {
|
|
|
|
|
return this.KUNMING_CENTER;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 搜索地点
|
|
|
|
|
* @param keyword 搜索关键词
|
|
|
|
|
* @param city 城市名称
|
|
|
|
|
* @returns 搜索结果列表
|
|
|
|
|
*/
|
|
|
|
|
async searchPoi(keyword: string, city: string): Promise<SearchResult[]> {
|
|
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
|
this.amapInstance.getPoiAround({
|
|
|
|
|
querykeywords: keyword,
|
|
|
|
|
city: city,
|
|
|
|
|
radius: 50000, // 搜索半径50公里
|
|
|
|
|
offset: 20, // 返回20条结果
|
|
|
|
|
success: (res: any) => {
|
|
|
|
|
console.log('搜索POI成功:', res);
|
|
|
|
|
|
|
|
|
|
if (res.pois && res.pois.length > 0) {
|
|
|
|
|
const results: SearchResult[] = res.pois.map((poi: any) => ({
|
|
|
|
|
id: poi.id,
|
|
|
|
|
name: poi.name,
|
|
|
|
|
address: poi.address,
|
|
|
|
|
longitude: parseFloat(poi.location.split(',')[0]),
|
|
|
|
|
latitude: parseFloat(poi.location.split(',')[1]),
|
|
|
|
|
phone: poi.tel || ''
|
|
|
|
|
}));
|
|
|
|
|
resolve(results);
|
|
|
|
|
} else {
|
|
|
|
|
resolve([]);
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
fail: (err: any) => {
|
|
|
|
|
console.error('搜索POI失败:', err);
|
|
|
|
|
reject(err);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 规划驾车路线
|
|
|
|
|
* @param origin 起点坐标字符串 (经度,纬度)
|
|
|
|
|
* @param destination 终点坐标字符串 (经度,纬度)
|
|
|
|
|
* @returns 路线规划结果
|
|
|
|
|
*/
|
|
|
|
|
async getDrivingRoute(origin: string, destination: string): Promise<RoutePlanResult> {
|
|
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
|
this.amapInstance.getDrivingRoute({
|
|
|
|
|
origin,
|
|
|
|
|
destination,
|
|
|
|
|
strategy: '0', // 推荐路线策略
|
|
|
|
|
showtraffic: false, // 不显示交通状况
|
|
|
|
|
success: (res: any) => {
|
|
|
|
|
console.log('高德地图路线规划API调用成功');
|
|
|
|
|
|
|
|
|
|
if (res.paths && res.paths.length > 0) {
|
|
|
|
|
const path = res.paths[0];
|
|
|
|
|
const result: RoutePlanResult = {
|
|
|
|
|
polyline: path.polyline || '',
|
|
|
|
|
distance: path.distance || 0,
|
|
|
|
|
duration: path.duration || 0
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
console.log(`路线规划完成: 距离${result.distance}米, 预计${result.duration}秒`);
|
|
|
|
|
resolve(result);
|
|
|
|
|
} else {
|
|
|
|
|
resolve({
|
|
|
|
|
polyline: '',
|
|
|
|
|
distance: 0,
|
|
|
|
|
duration: 0
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
fail: (err: any) => {
|
|
|
|
|
console.error('高德地图路线规划API调用失败:', err);
|
|
|
|
|
reject(err);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 计算两点之间的距离(米)
|
|
|
|
|
* @param lat1 第一个点的纬度
|
|
|
|
|
* @param lng1 第一个点的经度
|
|
|
|
|
* @param lat2 第二个点的纬度
|
|
|
|
|
* @param lng2 第二个点的经度
|
|
|
|
|
* @returns 两点之间的距离(米)
|
|
|
|
|
*/
|
|
|
|
|
calculateDistance(
|
|
|
|
|
lat1: number,
|
|
|
|
|
lng1: number,
|
|
|
|
|
lat2: number,
|
|
|
|
|
lng2: number
|
|
|
|
|
): number {
|
|
|
|
|
const R = 6371000; // 地球半径(米)
|
|
|
|
|
const dLat = (lat2 - lat1) * Math.PI / 180;
|
|
|
|
|
const dLng = (lng2 - lng1) * Math.PI / 180;
|
|
|
|
|
const a =
|
|
|
|
|
Math.sin(dLat / 2) * Math.sin(dLat / 2) +
|
|
|
|
|
Math.cos(lat1 * Math.PI / 180) * Math.cos(lat2 * Math.PI / 180) *
|
|
|
|
|
Math.sin(dLng / 2) * Math.sin(dLng / 2);
|
|
|
|
|
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
|
|
|
|
|
return R * c;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 获取路线规划(通用版本)
|
|
|
|
|
* @param origin 起点坐标
|
|
|
|
|
* @param destination 终点坐标
|
|
|
|
|
* @returns 路线规划结果
|
|
|
|
|
*/
|
|
|
|
|
async getRoute(
|
|
|
|
|
origin: { latitude: number; longitude: number },
|
2025-10-18 22:21:04 +08:00
|
|
|
|
destination: { latitude: number; longitude: number }
|
2025-10-16 21:32:16 +08:00
|
|
|
|
): Promise<{
|
|
|
|
|
distance: number; // 距离(米)
|
|
|
|
|
duration: number; // 时间(秒)
|
|
|
|
|
polyline: { latitude: number; longitude: number }[];
|
|
|
|
|
}> {
|
|
|
|
|
// 对于真实模式,目前只支持驾车路线
|
|
|
|
|
const originStr = `${origin.longitude},${origin.latitude}`;
|
|
|
|
|
const destStr = `${destination.longitude},${destination.latitude}`;
|
|
|
|
|
|
|
|
|
|
const result = await this.getDrivingRoute(originStr, destStr);
|
|
|
|
|
|
|
|
|
|
// 转换polyline格式
|
|
|
|
|
const polylinePoints: { latitude: number; longitude: number }[] = [];
|
|
|
|
|
if (result.polyline) {
|
|
|
|
|
const points = result.polyline.split(';');
|
|
|
|
|
for (const point of points) {
|
|
|
|
|
const [lng, lat] = point.split(',');
|
|
|
|
|
if (lng && lat) {
|
|
|
|
|
polylinePoints.push({
|
|
|
|
|
latitude: parseFloat(lat),
|
|
|
|
|
longitude: parseFloat(lng)
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
distance: result.distance,
|
|
|
|
|
duration: result.duration,
|
|
|
|
|
polyline: polylinePoints
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 地理编码 - 地址转坐标
|
|
|
|
|
* @param address 地址字符串
|
|
|
|
|
* @returns 坐标信息
|
|
|
|
|
*/
|
|
|
|
|
async geocode(address: string): Promise<{
|
|
|
|
|
latitude: number;
|
|
|
|
|
longitude: number;
|
|
|
|
|
formattedAddress: string;
|
|
|
|
|
}> {
|
|
|
|
|
// 对于真实模式,使用搜索API来模拟地理编码
|
|
|
|
|
const results = await this.searchPoi(address, '昆明');
|
|
|
|
|
if (results.length > 0) {
|
|
|
|
|
return {
|
|
|
|
|
latitude: results[0].latitude,
|
|
|
|
|
longitude: results[0].longitude,
|
|
|
|
|
formattedAddress: results[0].address
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 如果没有找到结果,返回默认位置
|
|
|
|
|
return {
|
|
|
|
|
latitude: 25.0409,
|
|
|
|
|
longitude: 102.7123,
|
|
|
|
|
formattedAddress: '云南省昆明市'
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 逆地理编码 - 坐标转地址
|
|
|
|
|
* @param latitude 纬度
|
|
|
|
|
* @param longitude 经度
|
|
|
|
|
* @returns 地址信息
|
|
|
|
|
*/
|
|
|
|
|
async reverseGeocode(latitude: number, longitude: number): Promise<{
|
|
|
|
|
formattedAddress: string;
|
|
|
|
|
country: string;
|
|
|
|
|
province: string;
|
|
|
|
|
city: string;
|
|
|
|
|
district: string;
|
|
|
|
|
street: string;
|
|
|
|
|
}> {
|
|
|
|
|
const result = await this.getLocationInfo(longitude, latitude);
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
// 修复:不再使用不存在的formatted_address字段,而是根据已有的addressComponent字段构建格式化地址
|
|
|
|
|
formattedAddress: `${(result.regeocode && result.regeocode.addressComponent && result.regeocode.addressComponent.province) || '云南省'}${(result.regeocode && result.regeocode.addressComponent && result.regeocode.addressComponent.city) || '昆明市'}${(result.regeocode && result.regeocode.addressComponent && result.regeocode.addressComponent.district) || '五华区'}` || '未知地址',
|
|
|
|
|
country: '中国',
|
|
|
|
|
province: (result.regeocode && result.regeocode.addressComponent && result.regeocode.addressComponent.province) || '云南省',
|
|
|
|
|
city: (result.regeocode && result.regeocode.addressComponent && result.regeocode.addressComponent.city) || '昆明市',
|
|
|
|
|
district: (result.regeocode && result.regeocode.addressComponent && result.regeocode.addressComponent.district) || '五华区',
|
|
|
|
|
street: (result.regeocode && result.regeocode.addressComponent && result.regeocode.addressComponent.township) || ''
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 获取附近的地点
|
|
|
|
|
* @param latitude 纬度
|
|
|
|
|
* @param longitude 经度
|
|
|
|
|
* @param radius 搜索半径
|
|
|
|
|
* @param type 地点类型
|
|
|
|
|
* @returns 附近地点列表
|
|
|
|
|
*/
|
|
|
|
|
async getNearbyPlaces(
|
|
|
|
|
latitude: number,
|
|
|
|
|
longitude: number,
|
|
|
|
|
radius: number,
|
|
|
|
|
type?: string
|
|
|
|
|
): Promise<Array<{
|
|
|
|
|
id: string;
|
|
|
|
|
name: string;
|
|
|
|
|
latitude: number;
|
|
|
|
|
longitude: number;
|
|
|
|
|
address: string;
|
|
|
|
|
distance: number;
|
|
|
|
|
}>> {
|
|
|
|
|
// 对于真实模式,使用搜索API
|
|
|
|
|
const keyword = type ? type : '';
|
|
|
|
|
const results = await this.searchPoi(keyword, '昆明');
|
|
|
|
|
|
|
|
|
|
return results.map(place => ({
|
|
|
|
|
id: place.id,
|
|
|
|
|
name: place.name,
|
|
|
|
|
latitude: place.latitude,
|
|
|
|
|
longitude: place.longitude,
|
|
|
|
|
address: place.address,
|
|
|
|
|
distance: this.calculateDistance(latitude, longitude, place.latitude, place.longitude)
|
|
|
|
|
})).filter(place => place.distance <= radius);
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-18 22:21:04 +08:00
|
|
|
|
|
2025-10-16 21:32:16 +08:00
|
|
|
|
|
2025-10-18 22:21:04 +08:00
|
|
|
|
|
2025-10-16 21:32:16 +08:00
|
|
|
|
|
2025-10-18 22:21:04 +08:00
|
|
|
|
|
2025-10-16 21:32:16 +08:00
|
|
|
|
|
2025-10-18 22:21:04 +08:00
|
|
|
|
|
2025-10-16 21:32:16 +08:00
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 格式化路线距离
|
|
|
|
|
* @param distance 距离(米)
|
|
|
|
|
* @returns 格式化后的距离字符串
|
|
|
|
|
*/
|
|
|
|
|
formatRouteDistance(distance: number): string {
|
|
|
|
|
if (distance < 1000) {
|
|
|
|
|
return `${distance}米`;
|
|
|
|
|
} else {
|
|
|
|
|
return `${(distance / 1000).toFixed(1)}公里`;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 格式化路线时间
|
|
|
|
|
* @param duration 时间(秒)
|
|
|
|
|
* @returns 格式化后的时间字符串
|
|
|
|
|
*/
|
|
|
|
|
formatRouteDuration(duration: number): string {
|
|
|
|
|
if (duration < 60) {
|
|
|
|
|
return `${Math.round(duration)} 秒`;
|
|
|
|
|
} else if (duration < 3600) {
|
|
|
|
|
return `${Math.round(duration / 60)} 分钟`;
|
|
|
|
|
} else {
|
|
|
|
|
const hours = Math.floor(duration / 3600);
|
|
|
|
|
const minutes = Math.round((duration % 3600) / 60);
|
|
|
|
|
return `${hours}小时${minutes}分钟`;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 地图服务单例实例
|
|
|
|
|
* 导出供应用程序全局使用
|
|
|
|
|
*/
|
|
|
|
|
export default new MapService();
|