丽水市汽车运输集团股份有限公司信息中心苟安廷
当我们兴匆匆地把GPS设备传来的经纬度坐标标记到电子地图上时,发现地图上的位置和实际位置相差甚远,这就是天朝上国特有的地图加偏,以安全之名,行掩耳盗铃之事,给我们的开发带来了不必要的麻烦,至于加偏的原因,大家百度一下,网上一大堆,这里就不废话了。除GPS设备本身加偏外,电子地图上的坐标也不是真实的,要想正确显示到地图上,还必须将收到的加偏坐标(俗称火星坐标)换算到地图对应的坐标,也就是我们常说的纠偏,而国内用的比较多的地图主要是谷歌和百度,前者有具体的控件,比较好解决,后者就麻烦了,本人这段时间为此事折腾得不轻,但功夫不费有心人,总算比较完美地解决了,故整理一下,给还在或将要受此折腾的人参考。
和大多数人一样,碰到这个问题时,首先想到了百度,总结了百度结果,主要有以下三个方法:
1.使用控件
如果和谷歌地图一样,有一个控件,直接传入GPS坐标得到地图坐标,那就省事了,通过正常途径获取控件的话,我们这种小公司还没有那个实力,而非正常途经的控件或算法貌似还没有泄漏出来,即使泄漏了,你也不敢放心使用,万一有一天,被以泄漏“郭嘉咪咪”的名义请你去喝茶就得不偿失了,因此,本方法基本放弃。
2.百度地图接口
百度网站提供了接口,只要通过http传入GPS坐标参数就可以获得对应的地图坐标,该方法优点是方便准确,不足也很明显,受网速、百度服务器等影响,处理大量并发业务时力不从心。
3.数据库
所谓的加偏,就是将真实坐标加上一定的偏移量,而这个偏移量又不是线性的,不同地区偏移不一样,但同一地区偏移量却差不多,因此,有人就使用了个暴力破解的方法,将全国按GPS坐标分成很多小块,然后查出每个小块的偏移量,并保存到数据库里面,需要纠偏时,先根据GPS坐标取出对应区域的偏移量,反算出地图坐标。优点:本地执行,速度快,缺点:中国太大了,存放区域的记录有几千万条,不仅占用了大量存储空间,检索速度也大受影响,更要命的是,网上也很难找到一个完整的数据库,有些网站说有完整的,但要Money,提到Money就不亲热了,毕竟我们是个人或小公司嘛,钱不是问题,问题是没钱。
貌似进入了一个死胡同,但我们仔细一想,数据库方法不错,但有点浪费,因为我们面向的客户大多是某区域的,而且,车辆行驶的路线相对是固定的(尤其是客运班车),塔克拉玛干沙漠或者居民小区楼顶的坐标对于我们来说,貌似没有多少意义,理论上,我们需要的仅仅是客户车辆行驶区域的坐标就可以了,车辆能到达的区域(通常是公路)相对整个国土面积来说,太小了,因此,有这么一个轻量级的数据库是不是就完美了呢?本文正是基于这一思路,将方法2和方法3结合起来,巧妙解决了百度地图纠偏问题。
基本原理:收到GPS坐标后,首先计算出该坐标所属的区域,然后再从本地数据库查询该区域的偏移量,如果查询到,直接和该偏移相加得到地图坐标,如果没有查询到,则从百度网站接口查询,并和GPS坐标相减得到偏移,将本偏移存放到本地数据库,然后直接返回从百度接口得到的地图坐标。换句话说,我们自己根据车辆使用过的坐标,构建了一个轻量级的数据库,使用一段时间后,我们会发现,绝大部分数据都是从本地数据库获取的,数据量也就区区几十万条而已。
我们分步骤完成这一思路。
第一步:划分区域的算法
GPS收到的坐标(经度和纬度两个方向)是百万分之一度,整数,如果每百万分之一度作为一个区域,那么,基本上所有的GPS坐标都位于不同的区域了,这么小的区域没有什么意义。我们知道,一度等于60分,一分等于60秒,那么,一度就是3600秒了,而经纬度中,一秒对应的距离大约31米,如果我们按秒划分区域,应该是可以接受的,也就是说,用大约31米边长的正方形表示一个区域,落在该区域的坐标我们认为偏移量是一样的。因此,我们首先要把GPS坐标转换一下,用一个精确到“秒”的数字来标记所属的区域。假如某经度为120.123456度,我们将小数部分转换成“秒”:0.123456×3600=444.4416,精确到秒,就是444秒,后面的小数部分扔掉了,因此,该GPS经度位于经度为120度444秒的某区域中,该区域的纬度算法一样。在数据库检索中,整数速度远远大于浮点数,我们想办法将120度444秒用一个整数特征值来表示,我们知道,GPS返回的坐标为百万分之一度,因此,我们将“度”乘以一百万,再把秒加上去就可以表示了,120.123456度转换成区域经度的数字为120000444,这个数字看起来怪怪的,前面部分表示度,后面部分表示秒,其实也无所谓,反正就是一个区域的唯一标记的特征值而已。为此,我们写一个方法来计算(注意,参数都是百万分之一度)。
参考代码如下:
publicint GetAreaPostion(int GpsCoordinate)
{
//计算"度"的部分
int nDegree = GpsCoordinate / 1000000 * 1000000;
//计算度后面小数部分
int nSecond = (int)(0.000001 * (GpsCoordinate - nDegree) * 3600);
//两者重新相加
return nDegree + nSecond;
}
第二步:创建数据库表
我们需要在数据库里面创建一个表,记录每个区域的偏移量,一个区域包含精确到秒的经度和纬度两列,是唯一的,因此,为直观,我们不妨用中文的“秒经度”、“秒纬度”两列表示,并设置为主键、聚族索引,再增加“经度偏移”、“纬度偏移”、“创建时间”三列,之所以要增加“创建时间”字段,是考虑到时间久了,最好更新一下,比如,半年前的数据最好重新从百度网站取,考虑到速度问题,该字段也需要创建索引。数据库设计界面如下:
第三步:将接收到的GPS坐标转换成对应的区域
坐标包含经度、纬度两部分,因此,我们用Point表示,转换办法参考如下:
MapCorrect map=newMapCorrect();
Point ptArea = new Point(0, 0);
ptArea.X = map.GetAreaPostion(Lng);
ptArea.Y = map.GetAreaPostion(Lat);
第四步:创建数据库访问的方法
既然要访问数据库,必然要提供操作数据库的方法,数据库操作不是本文的重点,大家也都会操作,这里我们创建一个名称为DB的类用于操作数据库,该类主要提供两个静态方法:
public staticDataRow GetValueDataRow(string p_strSql)
该方法执行传入的SQL语句,并将数据库返回的数据集第一行返回,如果数据库没有数据返回,该方法返回null。
public staticbool ExecSql(string p_strSql)
该方法执行指定的SQL语句,如果执行错误,返回false,执行成功,返回true。
第五步:从数据库查询偏移
为了加快速度和代码更清晰,建议使用存储过程,存储过程代码如下(忽略半年前的数据):
CREATE PROCEDUREGetOffset
@Lng INT, --精确到秒的经度
@Lat INT --精确到秒的纬度
AS
BEGIN
SET NOCOUNT ON;
SELECT 经度偏移,纬度偏移
FROM dbo.纠偏区域 WITH(NOLOCK)
WHERE 秒经度=@Lng AND 秒纬度=@LatAND创建时间>DATEADD(dd,-180,GETDATE())
END
第六步:根据查询结果进行处理
如果查询结果中已经有偏移了,直接将偏移和GPS坐标相加,否则,从百度网站查询,并计算偏移然后保存到数据库。
public Point GetMapCoordinate(Point GpsCoordinate)
{
string strSql = string.Format("EXEC GetOffset @Lng ={0},@Lat={1}",
GetAreaPostion(GpsCoordinate.X),
GetAreaPostion(GpsCoordinate.Y));
DataRow row = DB.GetValueDataRow(strSql);
Point ptMap = new Point(0, 0);
if (row == null) //从百度网站查询
{
ptMap = GetFromBaidu(GpsCoordinate);
}
else//直接返回
{
ptMap.X = GpsCoordinate.X + (int)row["经度偏移"];
ptMap.Y = GpsCoordinate.Y + (int)row["纬度偏移"];
}
return ptMap;
}
这里,我们需要完成从百度查询坐标的方法,百度网站返回的是JSON数据,JSON很强大,操作方法很多,但这里得到的结果却是简单的一句话:
{"error":0,"x":"MTE5LjE1NzE5NzEwMDc5","y":"MjguMzI0NzUzMTI4NDEz"}
显然,error表示错误信息,0表是无错误,后面的两个是地图坐标的经度和纬度,进行了Base64编码,格式是固定的,为了使用这么简单的一个JSON而引入其他项目显然没必要,我们直接用简单的字符串分解一下就可以了,代码较多,这里就不列出源代码了(文末会附下载地址),得到地图坐标后,一定要计算偏移,并更新到数据库中,更新数据库的存储过程参考如下:
CREATE PROCEDUREUpdateOffset
@Lng INT, --区域秒经度
@Lat INT, --区域秒纬度
@OffLng INT, --经度偏移
@OffLat INT --纬度偏移
AS
BEGIN
SET NOCOUNT ON;
UPDATE dbo.纠偏区域 SET 经度偏移=@OffLng,纬度偏移=@OffLat,创建时间=GETDATE()
WHERE 秒经度=@Lng AND 秒纬度=@OffLat
IF @@ROWCOUNT=0
BEGIN
INSERT INTO dbo.纠偏区域(秒经度,秒纬度,经度偏移,纬度偏移,创建时间)
VALUES (@Lng,@Lat,@OffLng,@OffLat,GETDATE())
END
END
使用存储过程,不仅提高了速度,减少了网络流量,还使客户端代码更加清晰简洁。以上仅仅是实现的基本原理,实际使用时,应该考虑多线程、异常处理等等实际情况。
题外话:
对于一个GPS软件,除了能在地图上正确显示车辆位置外,还应该显示当前位置对应的中文描述,如“浙江省杭州市沪杭高速”,也就是根据坐标查询对应的地理位置信息描述,即反向地理编码查询,百度地图提供了相应的接口(接口地址:http://developer.baidu.com/map/carapi-2.htm),我们可以用上面同样的办法,在本地建立区域缓存,避免每次都从网站读取,另外,谷歌地图提供的查询更详细和准确,二者均有使用限制,百度每个密钥每天限制5000次查询,而注册一个账号,可以申请最多20个密钥,账号注册是免费的,其余你懂的,后者按IP限制,每个IP每天最多查询2500次,而GPS服务器往往都是固定IP,对于业务初期,2500次杯水车薪。为解决这一问题,我们可以在缓存百度地理位置信息时,将GPS原始坐标也保存起来,然后另外开一个程序,通过从谷歌查询地理位置信息来对缓存中的位置信息描述进行更新,也就是说,下次再使用这一区域的地理信息名称时,得到的就是谷歌解析的名称了,如何突破谷歌的限制呢?你可以把软件COPY到一个拨号上网的地方(如家里),超过限制后,重新拨号,当然,你可以手工断线和重拨,也可以写一段代码实现自动断线和重拨,等过一段时间,客户车辆大部分位置信息都缓存到本地后,就可以直接运行在服务器上了。
附源代码下载地址(演示时,为方便使用,可以输入120.123456,也可以输入120123456):