React-Native之定位实践
当我们谈定位功能时,我们想要的是什么?
- WGS-84经纬度(即gps经纬度)或者GCJ-02经纬度(中国坐标偏移标准,高德、腾讯地图都适用这个)或者BD-09经纬度(百度坐标偏移标准,百度地图使用),看自己项目需要
- 有了经纬度,我们还想要的是该经纬度对应的地理位置信息,毕竟我们很难从一串数字中看出什么意思
- 有时候我们还需要得到两个经纬度(同一坐标系下)的距离远近
对于定位这样的公共模块,我们想要的是一个公共的模块,最好调用一个函数就能得到我们想要的一切定位相关信息,可以先写个结果类型定义来定义我们的输出:
export interface ILocationPosition {
longitude: number,//gps精度
latitude: number,//gps纬度
address: String,//详细地理位置信息
currentTime: String,//当前时间
realErrorDistance?: number,//与另一个经纬度的实际误差距离
gdLongitude?: number,//高德经度
gdLatitude?: number,//高德纬度
}
或
export interface ILocationPosition {
longitude: number,//高德精度
latitude: number,//高德纬度
address: String,//详细地理位置信息
currentTime: String,//当前时间
realErrorDistance?: number,//与另一个经纬度的实际误差距离
gpsLongitude?: number,//高德经度
gpsLatitude?: number,//高德纬度
}
一、获取定位权限
在开始写定位方法前,我们需要获取应用的定位权限:
1.1 对于android端来说
首先要在/android/app/src/main/AndroidManifest.xml文件中,写上我们需要的定位权限:
<!--用于访问GPS定位-->
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
<!--用于进行网络定位-->
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
<!--用于访问网络,网络定位需要上网-->
<uses-permission android:name="android.permission.INTERNET" />
然后,android6.0以后对于危险权限需要进行动态获取,我们需要在代码中写上动态获取定位权限的代码,可以使用PermissionsAndroid模块来进行获取,附上通用获取android动态权限的方法:
export function getPositionInit() {
// //如果使用高德定位,则设置高德key与需要逆地理编码的属性;如果不使用高德定位则注释掉下面代码
// await init({
// ios: '[你的高德ios key]',
// android: "[你的高德android key]"// 传入AMAP_KEY
// });
// // android 需要逆地理编码
// setNeedAddress(true);
// // ios 需要逆地理编码
// setLocatingWithReGeocode(true);
if (Platform.OS === "android") {
//获取gps位置是否打开
return LocationServicesDialogBox.checkLocationServicesIsEnabled({
message: "<h2>开启位置服务</h2>开启位置服务,获取精准定位<br/>",
ok: "去开启",
cancel: "取消",
enableHighAccuracy: true,
showDialog: true,
openLocationServices: true,
preventOutSideTouch: false,
preventBackClick: false,
providerListener: true
}).then(async function (success) {
console.log("获取gps成功", success);
const permissions = [
PermissionsAndroid.PERMISSIONS.ACCESS_FINE_LOCATION,
PermissionsAndroid.PERMISSIONS.ACCESS_COARSE_LOCATION
];
const granteds = await PermissionsAndroid.requestMultiple(permissions);
console.log("granteds====", granteds);
if (granteds['android.permission.ACCESS_FINE_LOCATION'] === 'granted' && granteds['android.permission.ACCESS_COARSE_LOCATION'] === 'granted') {
return true;
} else {
Modal.alert("请开启定位权限", "请开启获取手机位置服务,否则系统部分功能将无法使用", [
{
text: "开启", onPress: () => {
console.log("点击开启按钮");
if (
granteds['android.permission.ACCESS_FINE_LOCATION'] === 'never_ask_again' && granteds['android.permission.ACCESS_COARSE_LOCATION'] === 'never_ask_again'
) {
Alert.alert("警告", "您将应用获取手机定位的权限设为拒绝且不再询问,功能无法使用!" +
"想要重新打开权限,请到手机-设置-权限管理中允许[你的应用名称]app对该权限的获取");
return false;
} else {
//短时间第二次可以唤醒再次请求权限框,但是选项会从拒绝变为拒绝且不再询,如果选择该项则无法再唤起请求权限框
getPositionInit();
}
}
},
{
text: "拒绝授权", onPress: () => {
return false;
}
}
])
}
})
}
}
通过最终返回的true或者false来判断是否获取到定位权限。
1.2 对于ios端来说
需要在info.plist中写明申请权限的具体原因
二、定位方法
明确了想要的东西之后,我们有几种经过实践有效的定位方式:
2.1、react-native自带的Geolocation定位模块
Geolocation模块原本是在react-native项目中包含的定位模块,后来为了精简rn的项目架构,rn只保留了核心架构,像定位、拍照之类的功能都被放到了react-native-community社区,Geolocation库详情可见https://github.com/react-native-geolocation/react-native-geolocation。
使用Geolocation.getCurrentPosition方法可以获取到当前位置的gps经纬度,要想获取详细的地理位置信息还需要借用高德等地图服务商的逆地理编码服务,因为使用高德的逆地理编码服务,所以还需要将gps经纬度转为高德经纬度。
gps转高德与逆地理编码服务高德均有提供,去高德开放平台即可查阅,下面直接放代码
const GaoDe_Key = '[你的高德web服务key]';
function requestGetGeoPosition(setLocation?:Function,setAddress?:Function,targetCoordinate?:coordinate,setCurrentError?:Function,setGDLocation?:Function,isToGD?:Boolean){
return new Promise((resolve, reject) => {
Geolocation.getCurrentPosition(async (position) => {
console.log('原生获取经纬度=======', position);
if (setLocation && typeof setLocation === 'function'){
console.log("set GPS经纬度")
setLocation({
longitude: getBit(position.coords.longitude, 6),
latitude: getBit(position.coords.latitude, 6),
})
}
let serverTime = '';
serverTime = await getServerTime().catch((err) => {
console.log('获取服务器时间接口捕获错误:', err);
});
let time = dateFormat("YYYY-mm-dd HH:MM:SS", new Date());
let result:IGeoPosition;
result = {
longitude: getBit(position.coords.longitude, 6),
latitude: getBit(position.coords.latitude, 6),
currentTime: serverTime ? serverTime : time,
};
if (isToGD){
let data = await getGDLocation({
latitude: position.coords.latitude,
longitude: position.coords.longitude,
});
console.log('gps转为高德坐标系===========',data);
if (setGDLocation && typeof setGDLocation === 'function'){
setGDLocation(data);
}
result = {
...result,
gdLongitude:data.longitude,
gdLatitude:data.latitude,
};
if (setAddress && typeof setAddress === 'function'){
let addressStr = '';
addressStr = await getAddressService(data);
console.log('address----------',addressStr);
setAddress(addressStr);
result = {
...result,
address:addressStr
};
}
}
if (targetCoordinate && setCurrentError && typeof setCurrentError === "function"){
let distance = 0;
distance = getDistance(targetCoordinate, {
longitude:position.coords.longitude,
latitude:position.coords.latitude,
}, 1);
console.log('目标经纬度与当前gps经纬度的对比距离为',distance);
result = {
...result,
realErrorDistance: distance,
}
}
//最终返回数据
console.log('原生获取经纬度最后所得结果:',result);
resolve(result);
}, (error) => {
console.log('原生获取错误', error);
}, {
enableHighAccuracy: false,
timeout: 2000,
maximumAge: 1000,
});
})
}
const getNetData = url => {
return new Promise((resolve, reject) => {
fetch(url)
.then(response => {
console.log("response",response)
return response.json()
})
.then(responseData => {
console.log("responseData",responseData)
resolve(responseData);
})
.catch(error => {
reject(error);
})
});
};
//获取城市定位信息,逆地理编码
const getAddressService = (locationObj) => {
const GaoDe_URL = `https://restapi.amap.com/v3/geocode/regeo?key=${GaoDe_Key}&radius=1000&extensions=all&poitype=商务写字楼&location=`;
return new Promise((resolve, reject) => {
if (locationObj && locationObj !== {}) {
let longitude = locationObj.longitude;
let latitude = locationObj.latitude;
//高德逆地理编码接口
const requestUrl = GaoDe_URL + longitude + ',' + latitude;
console.log('请求API', requestUrl);
getNetData(requestUrl)
.then(data => {
console.log("高德地图:", data);
//高德地图获取数据
if (data.status == 1) {
resolve(data.regeocode.formatted_address);
} else {
reject(data.code);
}
})
.catch(data => {
reject(data.code);
});
}
});
};
//将gps坐标转为高德坐标
const getGDLocation = (gpsLocation) => {
return new Promise((resolve, reject) => {
let url = `https://restapi.amap.com/v3/assistant/coordinate/convert?key=${GaoDe_Key}&locations=${gpsLocation.longitude},${gpsLocation.latitude}&coordsys=gps`;
getNetData(url)
.then((data) => {
if (data && data.locations) {
const gdLocation = {
latitude: parseFloat(data.locations.slice(data.locations.indexOf(",") + 1)),
longitude: parseFloat(data.locations.slice(0, data.locations.indexOf(",")))
};
resolve(gdLocation);
}else{
console.error("gps转高德请求接口报错");
}
})
.catch((err)=>{
reject(err);
})
})
};
2.2,高德定位模块 react-native-amap-geolocation之单次定位
react-native-amap-geolocation是高德地图定位模块的rn库,地址在https://github.com/qiuxiang/react-native-amap-geolocation,可以搭配作者的高德地图组件库react-native-amap3d[https://github.com/qiuxiang/react-native-amap3d]来使用。
这里介绍该库的单次定位模式:
function requestFetchGetList(SetLocation?, SetAddress?, targetCoordinate?: coordinate, SetCurrentError?: Function,SetGpsLocation?:Function,isToGps?:boolean) {
//设为单次定位
// setOnceLocation(true);
// //设置首次定位是否等待卫星定位结果
// // 只有在单次定位高精度定位模式下有效,设置为 true 时,会等待卫星定位结果返回, 最多等待 30 秒,
// // 若 30 秒后仍无卫星定位结果返回,返回网络定位结果。 等待卫星定位结果返回的时间可以通过
// // setGpsFirstTimeout 进行设置。
// setGpsFirst(true);
// //设置优先返回卫星定位信息时等待卫星定位结果的超时时间(毫秒)
// // 只有在 setGpsFirst(true) 时才有效。
// setGpsFirstTimeout(3000);
// // 设置是否使用设备传感器
// setSensorEnable(true);
// 仅设备模式在室内直接报错,无法使用。故使用高精度模式,在这种定位模式下,将同时使用高德网络定位和卫星定位,优先返回精度高的定位
// setLocationMode(<LocationMode>"Battery_Saving");
return new Promise((resolve, reject) => {
Geolocation.getCurrentPosition(
async res => {
if (SetLocation && typeof SetLocation === 'function' && res.location) {
SetLocation({
longitude: getBit(res.location.longitude, 6),
latitude: getBit(res.location.latitude, 6),
});
}
console.log('SetAddress========', typeof SetAddress);
if (SetAddress && typeof SetAddress === 'function') {
SetAddress(res.location.address);
}
//后台写的获取服务器时间接口
let serverTime = '';
serverTime = await getServerTime().catch((err) => {
console.log('获取服务器时间接口捕获错误:', err);
});
//后台写的将高德经纬度转为gps经纬度的方法
let gpsLocation = {};
if (isToGps !== false) {
gpsLocation = await transformGdToGps({
wgLon: res.location.longitude,
wgLat: res.location.latitude,
}).catch((err) => {
console.log('经纬度转换接口捕获错误:', err);
});
console.log("gpsLocation======", gpsLocation);
if (SetGpsLocation && typeof SetGpsLocation === 'function' && gpsLocation) {
SetGpsLocation({
gpsLongitude: getBit(gpsLocation.wgLon, 6),
gpsLatitude: getBit(gpsLocation.wgLat, 6),
})
}
}
let time = dateFormat("YYYY-mm-dd HH:MM:SS", new Date());
let result: ILocationPosition;
result = {
longitude: getBit(res.location.longitude, 6),//高德精度
latitude: getBit(res.location.latitude, 6),//高德纬度
address: res.location.address,
currentTime: serverTime ? serverTime : time,
gpsLongitude: gpsLocation && gpsLocation.wgLon ? getBit(gpsLocation.wgLon, 6) : undefined,
gpsLatitude: gpsLocation && gpsLocation.wgLat ? getBit(gpsLocation.wgLat, 6) : undefined,
};
console.log('SetCurrentError类型为========', typeof SetCurrentError);
//计算两个坐标点之间的距离
if (targetCoordinate && SetCurrentError && typeof SetCurrentError === "function") {
let newError = 0;
newError = getDistance(targetCoordinate, {
longitude: res.location.longitude,
latitude: res.location.latitude
}, 1);
console.log("实际误差=================", newError);
SetCurrentError(newError);
result = {
...result,
realErrorDistance: newError,
}
}
console.log(result);
resolve(result);
},
(error) => {
let errorInfo = error.message.substring(0, error.message.indexOf(" "));
console.log(error);
Toast.fail(`定位报错:${errorInfo}`, 0.5);
},
// 此处修改enableHighAccuracy为false 6689
{ enableHighAccuracy: false, timeout: 3000, maximumAge: 3000 })
})
}
单次定位在城市中是很好用的,但是去了山区以后会因为基站减少、信号减弱等原因导致精度误差很大,要想克服山区定位不准的问题需要使用下面的监听定位方法。
2.3,高德定位模块 react-native-amap-geolocation之监听定位
监听定位是调用定位的监听器来实现的定位方式,它要注意的地方是定位完成后要及时移除监听器,避免监听器持续运行(如果是持续运动的定位类型那么可以持续监听直到运动完成再移除监听器)。
//供外面调用的监听定位方法
export function getListenerPosition(SetLocation?, SetAddress?, targetCoordinate?, SetCurrentError?: Function,SetGpsLocation?:Function,isToGps?:boolean) {
return new Promise((resolve, reject) => {
getPositionInit().then((permission) => {
if (permission) {
start();
addLocationListener((location) => {
console.log(location, 111);
let result = loadFunction(location, SetLocation, SetAddress, targetCoordinate, SetCurrentError,SetGpsLocation,isToGps);
resolve(result);
});
} else {
Alert.alert("应用获取手机定位权限失败,无法获得当前经纬度信息!")
}
})
})
}
const loadFunction = async (location, SetLocation?, SetAddress?, targetCoordinate?, SetCurrentError?: Function,SetGpsLocation?:Function,isToGps?:boolean) => {
if (location) {
console.log('监听到的运动距离', location.accuracy);
// 获取数据之后进行两个条件的筛选
// locationType 0:GPS定位 1:网络定位 类型为6的情况不进行统计
// accuracy 例如精度大于10米的点不进行业务运算。
if (location.accuracy * 1 <= 500 && location.locationType * 1 != 6) {
console.log('小于10', location);
let time = dateFormat("YYYY-mm-dd HH:MM:SS", new Date());
if (SetLocation && typeof SetLocation === "function") {
SetLocation({
longitude: getBit(location.longitude, 6),
latitude: getBit(location.latitude, 6),
});
}
if (SetAddress && typeof SetAddress === "function") {
SetAddress(location.address);
}
let serverTime = '';
serverTime = await getServerTime().catch((err) => {
console.log('获取服务器时间接口捕获错误:', err);
});
let gpsLocation = {};
if (isToGps !== false) {
gpsLocation = await transformGdToGps({
wgLon: location.longitude,
wgLat: location.latitude,
}).catch((err) => {
console.log('经纬度转换接口捕获错误:', err);
});
console.log("gpsLocation======", gpsLocation);
if (SetGpsLocation && typeof SetGpsLocation === 'function' && gpsLocation) {
SetGpsLocation({
gpsLongitude: getBit(gpsLocation.wgLon, 6),
gpsLatitude: getBit(gpsLocation.wgLat, 6),
})
}
}
let time = dateFormat("YYYY-mm-dd HH:MM:SS", new Date());
let result: ILocationPosition;
result = {
longitude: getBit(location.longitude, 6),
latitude: getBit(location.latitude, 6),
address: location.address,
currentTime: serverTime ? serverTime : time,
gpsLongitude: gpsLocation && gpsLocation.wgLon ? getBit(gpsLocation.wgLon, 6) : undefined,
gpsLatitude: gpsLocation && gpsLocation.wgLat ? getBit(gpsLocation.wgLat, 6) : undefined,
};
//计算两个坐标点之间的距离
if (targetCoordinate && SetCurrentError && typeof SetCurrentError === "function") {
let newError = 0;
newError = getDistance(targetCoordinate, {
longitude: location.longitude,
latitude: location.latitude
}, 1);
console.log("实际误差=================", newError);
SetCurrentError(newError);
result = {
...result,
realErrorDistance: newError,
}
}
console.log('新版打点结果=======', result);
removeLocationListener((location) => loadFunction(location, SetLocation, SetAddress, targetCoordinate, SetCurrentError));
stop();
return result;
}
}
};
这里的移除监听removeLocationListener方法是库里没有的,手动在库文件amap-geolocation.js中添加了该方法:
/**我的修改
* 移除定位监听函数
*
* @param listener
*/
function removeLocationListener(listener) {
console.log("000000000");
return eventEmitter.removeAllListeners("AMapGeolocation");
}
exports.removeLocationListener = removeLocationListener;
同时在库文件amap-geolocation.d.ts中导出它:
/**
* 移除定位监听函数
*
* @param listener
*/
export declare function removeLocationListener(listener: (location: Location & ReGeocode) => void): import("react-native").EmitterSubscription;
就可以在自己的代码中这样引用它了:
import {addLocationListener, removeLocationListener, start, stop} from "react-native-amap-geolocation/lib/js";
监听定位最大的好处就是在山区也可以比较精准,它筛掉了定位精度在500m以上的定位结果和定位类型为6也就是基站定位的结果,因为基站定位纯粹依赖移动、联通、电信等移动网络定位,定位精度在500米-5000米之间,是误差很大的数据。这样就可以得到较为精准和波动较小的结果。
三、定位对比
总结一下:
定位方式 | 获取的坐标系 | 逆地理编码 | 城市准确度 | 山区准确度 |
---|---|---|---|---|
rn自带的Geolocation定位 | gps坐标 | 需要接口转换 | 高 | 高 |
react-native-amap-geolocation的单次定位 | 高德坐标 | 结果本身包含不需要转换 | 高 | 低 |
react-native-amap-geolocation的监听定位 | 高德坐标 | 结果本身包含不需要转换 | 高 | 高 |
结尾
最后,是一凡的公众号,用来记录学习、工作中的技术思考,大家一起努力!