最近项目中遇到一个功能需求,让用户可以在地图上画出一个圆,然后我们将圆中已经录入的地点显示出来。这个可以理解为大众点评或者美团的店铺搜索,用户当前所在的位置就是那个圆心。考虑到这是一个非常成熟的功能点,我就去网上找了一些实现方法,在这里算是个梳理总结,理清一下思路。
本文部分内容引用转载了一些学习过程中看到的其他博主的博客内容,因为用图和描述都很容易理解,而我想不到更好的表达了,借用一下非常感谢.......如果侵权请与我联系,我会删除引用的部分。
参考引用的博客链接:
https://www.cnblogs.com/feiquan/p/11380461.html
https://blog.csdn.net/u011497262/article/details/81807622
https://blog.csdn.net/kangle0228/article/details/80876795
https://blog.csdn.net/gaojingyuan/article/details/79004990
Geohash算法:
一.Geohash算法原理:
Geohash是一种用于将任意精度的纬度和经度坐标编码为文本字符串,从而使二维数据变成一维数据。Geohash其实就是将整个地图或者某个分割所得的区域进行一次划分,由于采用的是base32编码方式,即Geohash中的每一个字母或者数字(如wx4g0e中的w)都是由5bits组成(2^5 = 32,base32),这5bits可以有32中不同的组合(0~31),这样我们可以将整个地图区域分为32个区域,通过00000 ~ 11111来标识这32个区域。第一次对地图划分后的情况如下图所示(每个区域中的编号对应于该区域所对应的编码):
Geohash的0、1串序列是经度0、1序列和纬度0、1序列中的数字交替进行排列的,偶数位对应的序列为经度序列,奇数位对应的序列为纬度序列,在进行第一次划分时,Geohash0、1序列中的前5个bits(11100),那么这5bits中有3bits是表示经度,2bits表示纬度,所以第一次划分时,是将经度划分成8个区段(2^3 = 8),将纬度划分为4个区段(2^2 = 4),这样就形成了32个区域。如下图:
同理,可以按照第一次划分所采用的方式对第一次划分所得的32个区域各自再次划分。
那么我们为什么要把经纬度进行混合编码呢?
如图所示,我们将二进制编码的结果填写到空间中,当将空间划分为四块时候,编码的顺序分别是左下角00,左上角01,右下脚10,右上角11,也就 是类似于Z的曲线,当我们递归的将各个块分解成更小的子块时,编码的顺序是自相似的(分形),每一个子快也形成Z曲线,这种类型的曲线被称为Peano空 间填充曲线。
这种类型的空间填充曲线的优点是将二维空间转换成一维曲线(事实上是分形维),对大部分而言,编码相似的距离也相近, 但Peano空间填充曲线最大的缺点就是突变性,有些编码相邻但距离却相差很远,比如0111与1000,编码是相邻的,但距离相差很大。
二.举个Geohash编码过程的栗子:
下面以北海公园为例介绍GeoHash算法的计算步骤
2.1. 根据经纬度计算GeoHash二进制编码
地球纬度区间是[-90,90], 北海公园的纬度是39.928167,可以通过下面算法对纬度39.928167进行逼近编码:
1)区间[-90,90]进行二分为[-90,0),[0,90],称为左右区间,可以确定39.928167属于右区间[0,90],给标记为1;
2)接着将区间[0,90]进行二分为 [0,45),[45,90],可以确定39.928167属于左区间 [0,45),给标记为0;
3)递归上述过程39.928167总是属于某个区间[a,b]。随着每次迭代区间[a,b]总在缩小,并越来越逼近39.928167;
4)如果给定的纬度x(39.928167)属于左区间,则记录0,如果属于右区间则记录1,这样随着算法的进行会产生一个序列1011100,序列的长度跟给定的区间划分次数有关。
根据纬度算编码
bit | min | mid | max |
1 | -90.000 | 0.000 | 90.000 |
0 | 0.000 | 45.000 | 90.000 |
1 | 0.000 | 22.500 | 45.000 |
1 | 22.500 | 33.750 | 45.000 |
1 | 33.7500 | 39.375 | 45.000 |
0 | 39.375 | 42.188 | 45.000 |
0 | 39.375 | 40.7815 | 42.188 |
0 | 39.375 | 40.07825 | 40.7815 |
1 | 39.375 | 39.726625 | 40.07825 |
1 | 39.726625 | 39.9024375 | 40.07825 |
同理,地球经度区间是[-180,180],可以对经度116.389550进行编码。
根据经度算编码
bit | min | mid | max |
1 | -180 | 0.000 | 180 |
1 | 0.000 | 90 | 180 |
0 | 90 | 135 | 180 |
1 | 90 | 112.5 | 135 |
0 | 112.5 | 123.75 | 135 |
0 | 112.5 | 118.125 | 123.75 |
1 | 112.5 | 115.3125 | 118.125 |
0 | 115.3125 | 116.71875 | 118.125 |
1 | 115.3125 | 116.015625 | 116.71875 |
1 | 116.015625 | 116.3671875 | 116.71875 |
2.2. 组码
通过上述计算,纬度产生的编码为10111 00011,经度产生的编码为11010 01011。偶数位放经度,奇数位放纬度,把2串编码组合生成新串:11100 11101 00100 01111。
最后使用用0-9、b-z(去掉a, i, l, o)这32个字母进行base32编码,首先将11100 11101 00100 01111转成十进制,对应着28、29、4、15,十进制对应的编码就是wx4g。
三.Geohash编码长度与精度:
通过了解Geohash算法的原理,我们也可以想象出Geohash编码的长度和具体的精度是正相关的。编码的长度越长,所表达的精度就越高。参考图一,长度为1的编码6,把大半个南美洲都给包进去了。那么具体编码长度和精度是怎么样一个对照关系呢,我们参照下图,这个精度非常重要。
Sptial Indexing空间索引:
空间索引是指依据空间对象的位置和形状或空间对象之间的某种空间关系按一定的顺序排列的一种数据结构,其中包含空间对象的概要信息,如对象的标识、外接矩形及指向空间对象实体的指针。因为我们项目中用的数据库是MySQL,索引本文主要针对MySQL的空间索引进行阐述。
MySQL中有一个类型Point,可以用来存储每个位置对应的经纬度,在这一列上建立空间索引。对于InnoDB
和MyISAM
表,MySQL可以使用类似于创建常规索引的语法创建空间索引,但是使用 SPATIAL
关键字。必须声明空间索引中的列NOT NULL
。下面的SQL语句是创建一个空间坐标点信息的表。其中location字段就是用来存储经纬度信息的。
CREATE TABLE `points` (
`name` varchar(20) NOT NULL DEFAULT '',
`location` point NOT NULL,
`description` varchar(200) DEFAULT NULL,
PRIMARY KEY (`name`),
SPATIAL KEY `sp_index` (`location`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
那么如何插入坐标数据呢?
INSERT INTO points (name, location) VALUES ( 'data1' , POINT(116.273106,39.992524));
INSERT INTO points (name, location) VALUES ( 'data2' , POINT(116.397279,39.908149));
INSERT INTO points (name, location) VALUES ( 'data3' , POINT(116.397389,39.918149));
GeomFromText()是我们在接触空间数据之前从未见过的函数。他接受任何几何类型的WKT(文本)值作为其第一个参数。其他功能提供了特定于类型的构造功能,用于构造每种几何类型的几何值。
既然现在已经插入了数据,我们应该如何检索想要的数据呢?
# 定义多边形
SET @bbox = CONCAT('POLYGON((116.373871 39.915786,116.417645 39.916444,116.41816 39.900841,116.374214 39.900182,116.373871 39.915786))');
# 使用变量
select name,X(location),Y(location),Astext(location) from points where ST_INTERSECTS( location, GeomFromText(@bbox));
在这条语句中呢,又出现了陌生的数据结构和函数。Polygon(简单面)用来表示多边形平面,在地图中可以代表一片区域。并且我们可以看到组成多边形的入参中,第一个点和最后一个点是完全重合的,这样就完成了连接。来看一下mysql官方对Polygon的断言。
重点!!!ST_INTERSECTS,返回1或0以指示g1
空间上是否 相交 g2
。这里我们需要注意一下,官方还提供了一个函数INTERSECTS,和我们例子中用的函数非常相像。大家可以试一下,在有大量数据时,使用三角形作为多边形,这两个函数所得到的结果是不一样的。使用INTERSECTS的话,和矩形范围内检索的结果完全相同,并没有减少结果数。这也是我之前在测试这些函数的时候,好奇把测试的矩形改成三角形会有什么样的结果的时候发现的。那么是为什么呢?这里要提到一个概念叫MBR,最小边界矩阵,其实很多空间函数都在命名时写明了使用最小边界矩阵来进行计算,比如MBRContains,
返回1或0以指示的最小边界矩形是否g1
包含的最小边界矩形g2。
我们来看下查询的结果。
通过结果我们也可以观察出来,X(location)和Y(location)分别代表的是Point类型的X轴值和Y轴值。我们也如愿所偿的得到了想要的点。
其他空间函数
因为对MySQL所提供的空间函数比较感兴趣,大概翻阅了一下官方文档(主要是想知道三角形怎么查询的目的),也看到了几个比较有用的函数,在这里介绍一下。
(1)ST_Distance_Sphere(
g1
, g2
[, radius
])
返回球体上两个点和/或多点之间的最小球面距离(以米为单位),或者 NULL
如果任何几何参数为 NULL
或为空,则返回。
计算使用球形地球和可配置的半径。可选radius
参数应以米为单位。如果省略,则默认半径为6,370,986米。
几何参数应由指定(经度,纬度)坐标值的点组成:
-
经度和纬度分别是该点的第一和第二坐标。
-
两个坐标均以度为单位。
-
经度值必须在(-180,180]范围内。正值位于本初子午线以东。
-
纬度值必须在[-90,90]范围内。正值位于赤道以北。
mysql> SET @pt1 = ST_GeomFromText('POINT(0 0)');
mysql> SET @pt2 = ST_GeomFromText('POINT(180 0)');
mysql> SELECT ST_Distance_Sphere(@pt1, @pt2);
+--------------------------------+
| ST_Distance_Sphere(@pt1, @pt2) |
+--------------------------------+
| 20015042.813723423 |
+--------------------------------+
(2)ST_GeoHash(
, longitude
, latitude
, max_length
)ST_GeoHash(
point
, max_length
)
好,在这里见到老朋友了,call back,我们见到了MySQL对于我们前面那部分GeoHash的实现函数。
返回连接字符集和排序规则中的geohash字符串。
如果有任何参数NULL
,则返回值为NULL
。如果任何参数无效,则会发生错误。
对于第一种语法,longitude
必须为[-180,180]范围内的数字,并且 latitude
必须为[-90,90]范围内的数字。对于第二种语法,POINT
需要一个 值,其中X和Y坐标分别在经度和纬度的有效范围内。
生成的字符串不得超过 max_length
字符(上限为100个字符)。该字符串可能短于 max_length
字符,这是因为创建geohash值的算法会继续进行,直到创建的字符串可以精确表示位置或max_length
字符, 以先到者为准。
mysql> SELECT ST_GeoHash(180,0,10), ST_GeoHash(-180,-90,15);
+----------------------+-------------------------+
| ST_GeoHash(180,0,10) | ST_GeoHash(-180,-90,15) |
+----------------------+-------------------------+
| xbpbpbpbpb | 000000000000000 |
+----------------------+-------------------------+
关于性能
Mysql的空间索引是通过R树实现的,R树用来做空间数据存储的树状数据结构。例如给地理位置,矩形和多边形这类多维数据建立索引,理解起来其实和Geohash的实现思想非常相像。感兴趣的同学可以去了解一下,在这里就不详细描述了。
我造了10W的单表测试数据,执行下面这条测试语句用时是0.205s。
SET @center = GeomFromText('POINT(0 0)');
SET @radius = 10;
SET @bbox = CONCAT('POLYGON((',
X(@center) - @radius, ' ', Y(@center) - @radius, ',',
X(@center) + @radius, ' ', Y(@center) - @radius, ',',
X(@center) + @radius, ' ', Y(@center) + @radius, ',',
X(@center) - @radius, ' ', Y(@center) + @radius, ',',
X(@center) - @radius, ' ', Y(@center) - @radius, '))'
);
SELECT name, AsText(location), @bbox, ST_Distance_Sphere(location, @center) as dis
FROM points
WHERE ST_INTERSECTS( location, GeomFromText(@bbox) ) order by dis;
好了,到这里,我想明白如何实现该功能了,希望也能够给到大家启发。