空间坐标测距检索
当时项目提出的具体项目需求, 后续实现之后感觉挺有趣的就把其单独剔除出来总结起来;
具体需求就是商家在我们自己商户平台 注册并绑定当前地址, 然后用户需要在平台检索最近的注册商家.
这里实现方式其实也十分简单, 只需要在注册的时候使用 MySQL|MariaDB 的 Geometry 类型保存位置经纬度.
注意: MySQL版本必须在 5.7 之后才能支持该类型.
Geometry 类型是专门用于空间存储的类型, 具体支持以下细分类型:
Point(110.3 44.0): 点, 保存坐标点位置, 也就是位置标识经纬度LineString(80.07 23.45, 99.23 34.56): 线, 两个点联系的直线, 用于标识两点直线Polygon((93.30 35.301 90.332 30.341)): 面, 多点连接采用的多边形面MultiPoint: 多点, 用于多个点记录, 一般是复杂的多点位置记录MultiLineString: 多线, 用于多条路线记录, 一般是复杂的多路线记录MultiPolygon: 多面, 用于多个面对象, 一般是复杂的多面记录GeometryCollection: 集合, 这是很高级的经纬度混合集合, 实际上我个人很少去用到
回归需求上面, 实际上要求客户端|移动端在商户平台需要对接第三方SDK平台来提交坐标系, 这里以 百度地图开放平台SDK 做样例;
假设安卓客户端已经接入 百度安卓定位SDK, 如下官方文档说明:
/**
* 实现定位回调
*/
public class MyLocationListener extends BDAbstractLocationListener {
@Override
public void onReceiveLocation(BDLocation location) {
//此处的BDLocation为定位结果信息类,通过它的各种get方法可获取定位相关的全部结果
//以下只列举部分获取经纬度相关(常用)的结果信息
//更多结果信息获取说明,请参照类参考中BDLocation类中的说明
//获取纬度信息 - 关键参数
double latitude = location.getLatitude();
//获取经度信息 - 关键参数
double longitude = location.getLongitude();
//获取定位精度,默认值为0.0f
float radius = location.getRadius();
//获取经纬度坐标类型,以LocationClientOption中设置过的坐标类型为准
String coorType = location.getCoorType();
//获取定位类型、定位错误返回码,具体信息可参照类参考中BDLocation类中的说明
int errorCode = location.getLocType();
}
}
上面最关键的参数 latitude(纬度) 和 longitude(精度) 且坐标定位也能支持海外地址, 我们需要做的是将移动端商户经纬度提交到服务端.
数据库构建
这里构建个初始化的数据表来构建测试处理:
CREATE TABLE `bussiess_info`
(
`id` BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键标识',
`name` VARCHAR(255) NOT NULL DEFAULT '' COMMENT '商户名称' COLLATE 'utf8mb4_unicode_ci',
`position` POINT NULL DEFAULT NULL COMMENT '位置经纬, 允许NULL用于商户可能暂时不绑定地址',
PRIMARY KEY (`id`) USING BTREE
)
COMMENT ='商户位置绑定'
COLLATE = 'utf8mb4_unicode_ci'
ENGINE = InnoDB
;
这里推荐 百度官方坐标拾取器 用来测试定位
这里假设目前处于广州特色景点 广州塔(经度: 113.318977N, 纬度: 23.114155E), 目前假定有些商户平台在附近绑定:
XXX酒家: (23.113781E,113.316372N)YYY庄园: (23.114113E,113.314297N)ZZZ店: (23.113698E,113.311741N)远的店: (23.120557E,113.318151N)很远的旅馆: (23.126079E,113.324079N)超远的广场: (23.209921E,113.2792N)
假设添加数据库追加上这几个商户对象:
# 插入对应的数据, geomfromtext 是转换经纬度函数, 首位代表经度,次位为纬度
# XXX酒家
INSERT INTO `bussiess_info`(`id`, `name`, `position`)
VALUES (default, 'XXX酒家', geomfromtext('point(113.316372 23.113781)'));
# YYY庄园
INSERT INTO `bussiess_info`(`id`, `name`, `position`)
VALUES (default, 'YYY庄园', geomfromtext('point(113.314297 23.114113)'));
# ZZZ店
INSERT INTO `bussiess_info`(`id`, `name`, `position`)
VALUES (default, 'ZZZ店', geomfromtext('point(113.311741 23.113698)'));
# 远的店
INSERT INTO `bussiess_info`(`id`, `name`, `position`)
VALUES (default, '远的店', geomfromtext('point(113.318151 23.120557)'));
# 很远的旅馆
INSERT INTO `bussiess_info`(`id`, `name`, `position`)
VALUES (default, '很远的旅馆', geomfromtext('point(113.324079 23.126079)'));
# 超远的广场
INSERT INTO `bussiess_info`(`id`, `name`, `position`)
VALUES (default, '超远的广场', geomfromtext('point(113.2792 23.209921)'));
如果你用GUI会看到新增的数据在GUI内部都是乱码没办法看到坐标信息, 这里可以通过 astext 转化为文本信息:
# 查看坐标详情
SELECT `id`, `name`, astext(`position`) as `gis`
FROM `bussiess_info`;
这里假设目前你处于 广州塔(经度: 113.318977N, 纬度: 23.114155E) 需要测量出和这些绑定商户具体的两点距离:
# 查看所有相差的的距离位置
# 当前广州塔的位置: geomfromtext('point(113.318977 23.114155)')
SELECT `id`,
`name`,
floor(st_distance_sphere(`position`, geomfromtext('point(113.318977 23.114155)'))) as `distance`
FROM `bussiess_info`;
这里查询出来的信息如下, 这里 distance 单位为 米:
| id | name | distance |
|---|---|---|
| 1 | XXX酒家 | 269 |
| 2 | YYY庄园 | 478 |
| 3 | ZZZ店 | 741 |
| 4 | 远的店 | 716 |
| 5 | 很远的旅馆 | 1424 |
| 6 | 超远的广场 | 11398 |
这时候就能直接做匹配附近的商家从而推送给客户端处理:
# 附近商户检索, 只需要 500 米之内的商家
# 当前广州塔的位置: geomfromtext('point(113.318977 23.114155)')
# 注意这里采用函数处理, 所以不能使用 `WHERE` 查询而是要采用 `HAVING` 后查询
SELECT `id`,
`name`,
floor(st_distance_sphere(`position`, geomfromtext('point(113.318977 23.114155)'))) as `distance`
FROM `bussiess_info`
HAVING `distance` <= 500;
这里只作为简单思路开发, 因为这种直接查询性能上面可能有所损失, 后续可以采用 GeoHash 算法做模糊匹配,
将通过经纬度检索分号模糊匹配保存到 Redis 查询,
其实就是将 (113.31897, 23.11415) 这种高分结果以丢失精度方式 (113.32, 23.11) 归并结果集之后缓存 Redis 提供给用户结果;
好处就是性能效率上面能够提升很大效率, 但是坏处就是出现定位精度丢失问题.