闽公网安备 35020302035485号

按 “我” 与别人距离的远近排序,筛选出离我最近的用户或者商店等

| geohash码长度 | 宽度 | 高度 |
|---|---|---|
| 1 | 5,009.4km | 4,992.6km |
| 2 | 1,252.3km | 624.1km |
| 3 | 156.5km | 156km |
| 4 | 39.1km | 19.5km |
| 5 | 4.9km | 4.9km |
| 6 | 1.2km | 609.4m |
| 7 | 152.9m | 152.4m |
| 8 | 38.2m | 19m |
| 9 | 4.8m | 4.8m |
| 10 | 1.2m | 59.5cm |
| 11 | 14.9cm | 14.9cm |
| 12 | 3.7cm | 1.9cm |
| 商户 | 经纬度 | Geohash字符串 |
|---|---|---|
| 串串香 | 116.402843,39.999375 | wx4er9v |
| 火锅 | 116.3967,39.99932 | wx4ertk |
| 烤肉 | 116.40382,39.918118 | wx4erfe |
CREATE TABLE `nearby_user` ( `id` int(11) NOT NULL AUTO_INCREMENT, `name` varchar(255) DEFAULT NULL COMMENT '名称', `longitude` double DEFAULT NULL COMMENT '经度', `latitude` double DEFAULT NULL COMMENT '纬度', `create_time` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '创建时间', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;计算两个点之间的距离,用了一个三方的类库,毕竟自己造的轮子不是特别圆,还有可能是方的,啊哈哈哈~
<dependency>
<groupId>com.spatial4j</groupId>
<artifactId>spatial4j</artifactId>
<version>0.5</version>
</dependency>
获取到外接正方形后,以正方形的最大最小经、纬度值搜索正方形区域内的用户,再剔除超过指定距离的用户,就是最终的附近的人。 private SpatialContext spatialContext = SpatialContext.GEO;
/**
* 获取附近 x 米的人
* 堆代码 duidaima.com
* @param distance 搜索距离范围 单位km
* @param userLng 当前用户的经度
* @param userLat 当前用户的纬度
*/
@GetMapping("/nearby")
public String nearBySearch(@RequestParam("distance") double distance,
@RequestParam("userLng") double userLng,
@RequestParam("userLat") double userLat) {
//1.获取外接正方形
Rectangle rectangle = getRectangle(distance, userLng, userLat);
//2.获取位置在正方形内的所有用户
List<User> users = userMapper.selectUser(rectangle.getMinX(), rectangle.getMaxX(), rectangle.getMinY(), rectangle.getMaxY());
//3.剔除半径超过指定距离的多余用户
users = users.stream()
.filter(a -> getDistance(a.getLongitude(), a.getLatitude(), userLng, userLat) <= distance)
.collect(Collectors.toList());
return JSON.toJSONString(users);
}
private Rectangle getRectangle(double distance, double userLng, double userLat) {
return spatialContext.getDistCalc()
.calcBoxByDistFromPt(spatialContext.makePoint(userLng, userLat),
distance * DistanceUtils.KM_TO_DEG, spatialContext, null);
}
由于用户间距离的排序是在业务代码中实现的,可以看到SQL语句也非常的简单。 <select id="selectUser" resultMap="BaseResultMap">
SELECT * FROM user
WHERE 1=1
and (longitude BETWEEN ${minlng} AND ${maxlng})
and (latitude BETWEEN ${minlat} AND ${maxlat})
</select>
四、Mysql + GeoHashCREATE TABLE `nearby_user_geohash` ( `id` int(11) NOT NULL AUTO_INCREMENT, `name` varchar(255) DEFAULT NULL COMMENT '名称', `longitude` double DEFAULT NULL COMMENT '经度', `latitude` double DEFAULT NULL COMMENT '纬度', `geo_code` varchar(64) DEFAULT NULL COMMENT '经纬度所计算的geohash码', `create_time` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '创建时间', PRIMARY KEY (`id`), KEY `index_geo_hash` (`geo_code`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;首先根据用户经、纬度信息,在指定精度后计算用户坐标的geoHash码,再获取到用户周边8个方位的geoHash码在数据库中搜索用户,最后过滤掉超出给定距离(500米内)的用户。
private SpatialContext spatialContext = SpatialContext.GEO;
/***
* 添加用户
* @return
*/
@PostMapping("/addUser")
public boolean add(@RequestBody UserGeohash user) {
//默认精度12位
String geoHashCode = GeohashUtils.encodeLatLon(user.getLatitude(),user.getLongitude());
return userGeohashService.save(user.setGeoCode(geoHashCode).setCreateTime(LocalDateTime.now()));
}
/**
* 获取附近指定范围的人
*
* @param distance 距离范围(附近多远的用户) 单位km
* @param len geoHash的精度(几位的字符串)
* @param userLng 当前用户的经度
* @param userLat 当前用户的纬度
* @return json
*/
@GetMapping("/nearby")
public String nearBySearch(@RequestParam("distance") double distance,
@RequestParam("len") int len,
@RequestParam("userLng") double userLng,
@RequestParam("userLat") double userLat) {
//1.根据要求的范围,确定geoHash码的精度,获取到当前用户坐标的geoHash码
GeoHash geoHash = GeoHash.withCharacterPrecision(userLat, userLng, len);
//2.获取到用户周边8个方位的geoHash码
GeoHash[] adjacent = geoHash.getAdjacent();
QueryWrapper<UserGeohash> queryWrapper = new QueryWrapper<UserGeohash>()
.likeRight("geo_code",geoHash.toBase32());
Stream.of(adjacent).forEach(a -> queryWrapper.or().likeRight("geo_code",a.toBase32()));
//3.匹配指定精度的geoHash码
List<UserGeohash> users = userGeohashService.list(queryWrapper);
//4.过滤超出距离的
users = users.stream()
.filter(a ->getDistance(a.getLongitude(),a.getLatitude(),userLng,userLat)<= distance)
.collect(Collectors.toList());
return JSON.toJSONString(users);
}
/***
* 球面中,两点间的距离
* @param longitude 经度1
* @param latitude 纬度1
* @param userLng 经度2
* @param userLat 纬度2
* @return 返回距离,单位km
*/
private double getDistance(Double longitude, Double latitude, double userLng, double userLat) {
return spatialContext.calcDistance(spatialContext.makePoint(userLng, userLat),
spatialContext.makePoint(longitude, latitude)) * DistanceUtils.DEG_TO_KM;
}
五、Redis + GeoHashGEOADD key longitude latitude member [longitude latitude member ...]其中,key为集合名称,member为该经纬度所对应的对象。
GEORADIUS hotel 119.98866180732716 30.27465803229662 500 m WITHCOORDRedis内部使用有序集合(zset)保存用户的位置信息,zset中每个元素都是一个带位置的对象,元素的score值为通过经、纬度计算出的52位geohash值。
@Autowired
private RedisTemplate<String, Object> redisTemplate;
//GEO相关命令用到的KEY
private final static String KEY = "user_info";
public boolean save(User user) {
Long flag = redisTemplate.opsForGeo().add(KEY, new RedisGeoCommands.GeoLocation<>(
user.getName(),
new Point(user.getLongitude(), user.getLatitude()))
);
return flag != null && flag > 0;
}
/**
* 根据当前位置获取附近指定范围内的用户
* @param distance 指定范围 单位km ,可根据{@link org.springframework.data.geo.Metrics} 进行设置
* @param userLng 用户经度
* @param userLat 用户纬度
* @return
*/
public String nearBySearch(double distance, double userLng, double userLat) {
List<User> users = new ArrayList<>();
// 1.GEORADIUS获取附近范围内的信息
GeoResults<RedisGeoCommands.GeoLocation<Object>> reslut =
redisTemplate.opsForGeo().radius(KEY,
new Circle(new Point(userLng, userLat), new Distance(distance, Metrics.KILOMETERS)),
RedisGeoCommands.GeoRadiusCommandArgs.newGeoRadiusArgs()
.includeDistance()
.includeCoordinates().sortAscending());
//2.收集信息,存入list
List<GeoResult<RedisGeoCommands.GeoLocation<Object>>> content = reslut.getContent();
//3.过滤掉超过距离的数据
content.forEach(a-> users.add(
new User().setDistance(a.getDistance().getValue())
.setLatitude(a.getContent().getPoint().getX())
.setLongitude(a.getContent().getPoint().getY())));
return JSON.toJSONString(users);
}
六、MongoDB + 2d索引db.hotel.insertMany([
{'name':'hotel1', location:[115.993121,28.676436]},
{'name':'hotel2', location:[116.000093,28.679402]},
{'name':'hotel3', location:[115.999967,28.679743]},
{'name':'hotel4', location:[115.995593,28.681632]},
{'name':'hotel5', location:[115.975543,28.679509]},
{'name':'hotel6', location:[115.968428,28.669368]},
{'name':'hotel7', location:[116.035262,28.677037]},
{'name':'hotel8', location:[116.024770,28.68667]},
{'name':'hotel9', location:[116.002384,28.683865]},
{'name':'hotel10', location:[116.000821,28.68129]},
])
接下来我们给 location 字段创建一个2d索引,索引的精度通过bits来指定,bits越大,索引的精度就越高。db.coll.createIndex({'location':"2d"}, {"bits":11111})
用geoNear 命令测试一下, near 当前坐标(经、纬度),spherical 是否计算球面距离,distanceMultiplier地球半径,单位是米,默认6378137, maxDistance 过滤条件(指定距离内的用户),开启弧度需除distanceMultiplier,distanceField 计算出的两点间距离,字段别名(随意取名)。db.hotel.aggregate({
$geoNear:{
near: [115.999567,28.681813], // 当前坐标
spherical: true, // 计算球面距离
distanceMultiplier: 6378137, // 地球半径,单位是米,那么的除的记录也是米
maxDistance: 2000/6378137, // 过滤条件2000米内,需要弧度
distanceField: "distance" // 距离字段别名
}
})
看到结果中有符合条件的数据,还多出一个字段distance 刚才设置的别名,代表两点间的距离。{ "_id" : ObjectId("5e96a5c91b8d4ce765381e58"), "name" : "hotel10", "location" : [ 116.000821, 28.68129 ], "distance" : 135.60095397487655 }
{ "_id" : ObjectId("5e96a5c91b8d4ce765381e51"), "name" : "hotel3", "location" : [ 115.999967, 28.679743 ], "distance" : 233.71915803517447 }
{ "_id" : ObjectId("5e96a5c91b8d4ce765381e50"), "name" : "hotel2", "location" : [ 116.000093, 28.679402 ], "distance" : 273.26317035334176 }
{ "_id" : ObjectId("5e96a5c91b8d4ce765381e57"), "name" : "hotel9", "location" : [ 116.002384, 28.683865 ], "distance" : 357.5791936927476 }
{ "_id" : ObjectId("5e96a5c91b8d4ce765381e52"), "name" : "hotel4", "location" : [ 115.995593, 28.681632 ], "distance" : 388.62555058249967 }
{ "_id" : ObjectId("5e96a5c91b8d4ce765381e4f"), "name" : "hotel1", "location" : [ 115.993121, 28.676436 ], "distance" : 868.6740526419927 }
总结