文章目錄
- 前言
- redis geospatial
- 如何從地圖上獲取經緯度
- springboot 的相關方法調用
- 準備redis服務器
- 引用的依賴
- 預設位置的key
- GEOADD 添加位置
- GEORADIUS 獲取指定經緯度附件的停車場(deprecated)
- GEORADIUS 獲取指定成員附件的停車場(deprecated)
- GEOSEARCH 搜索指定經緯度附件的停車場
- GEOSEARCH 搜索指定成員附件的停車場
- GEOPOS獲取指定成員經緯度
- GEODIST獲取兩點之間的距離
- GEOHASH獲取坐標的hash
- ZREM 移除位置
- 數學球面計算兩點距離
前言
基于redis geospatial的應用比較廣泛,比如需要獲取附件5公里的停車場。
官方文檔:https://redis.io/docs/data-types/geospatial/
代碼已分享至Gitee:https://gitee.com/lengcz/redisgeo
redis geospatial
用于地理位置服務的計算,它能做哪些?
- 我周邊的共享單車的信息,可以按距離輸出
- 兩坐標、車輛之間的距離
如何從地圖上獲取經緯度
高德地圖:
https://lbs.amap.com/demo/javascript-api/example/map/click-to-get-lnglat/
百度地圖:
https://api.map.baidu.com/lbsapi/getpoint/index.html
springboot 的相關方法調用
準備redis服務器
需要安裝redis 服務器
引用的依賴
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId></dependency><dependency><groupId>junit</groupId><artifactId>junit</artifactId><scope>test</scope></dependency>
注意本demo中的springboot版本為2.7.15,如果你的版本較低,可能部分GEOSEARCH 的API不可用。
如果遇到demo中的示例出現報紅,請考慮是否為依賴版本過低。
預設位置的key
private String positionKey = "parking";
GEOADD 添加位置
官方文檔: https://redis.io/commands/geoadd/
GEOADD key [NX | XX] [CH] longitude latitude member [longitudelatitude member ...]
注意經緯度需要在范圍內
- 經度 -180到180
- 緯度 -85.05112878到85.05112878
如果你添加的經緯度超出范圍會報錯
XX: 僅更新已存在的元素。永遠不要添加元素。
NX:不要更新已經存在的元素。始終添加新元素。
CH:將返回值從添加的新元素數量修改為更改的元素總數(CH是changed的縮寫)。已更改的圖元是添加的新元素和已更新坐標的現有圖元。因此,命令行中指定的分數與過去相同的元素將不被計算在內。注意:通常情況下,GEOADD的返回值只計算添加的新元素數量。
@Testvoid geoAdd(@Autowired RedisTemplate redisTemplate) {System.out.println("--------添加位置-----");GeoOperations geoOperations = redisTemplate.opsForGeo();{Long addedNum = geoOperations.add(positionKey, new Point(-122.27652, 37.805186), "10001:市圖書館");System.out.println(addedNum);}{Long addedNum = geoOperations.add(positionKey, new Point(-122.2674626, 37.8062344), "10002:百貨大樓");System.out.println(addedNum);}{Long addedNum = geoOperations.add(positionKey, new Point(-122.2469854, 37.8104049), "10003:科學中心");System.out.println(addedNum);}{Long addedNum = geoOperations.add(positionKey, new Point(-122.2625112, 37.793513), "10004:博物館");System.out.println(addedNum);}System.out.println("--------添加位置END-----");}
GEORADIUS 獲取指定經緯度附件的停車場(deprecated)
官方文檔: https://redis.io/commands/georadius/
從Redis版本6.2.0開始,此命令被視為已棄用。
在遷移或編寫新代碼時,可以用帶有BYRADIUS參數的GEOSEARCH和GEOSEARCHSTORE替換它。
GEORADIUS key longitude latitude radius <M | KM | FT | MI>[WITHCOORD] [WITHDIST] [WITHHASH] [COUNT count [ANY]] [ASC | DESC][STORE key | STOREDIST key]
單位
- m 米
- km 千米
- mi 英里
- ft 英尺
@Testvoid geoRadius(@Autowired RedisTemplate redisTemplate) {// 指定經緯度附近5公里的停車場Circle circle = new Circle(new Point(-122.2612767, 37.793684), RedisGeoCommands.DistanceUnit.KILOMETERS.getMultiplier());RedisGeoCommands.GeoRadiusCommandArgs args = RedisGeoCommands.GeoRadiusCommandArgs.newGeoRadiusArgs().includeCoordinates().includeDistance().sortAscending().limit(5);GeoResults<RedisGeoCommands.GeoLocation<String>> results = redisTemplate.opsForGeo().radius(positionKey, circle, args);System.out.println(results);for (GeoResult<RedisGeoCommands.GeoLocation<String>> g : results) {System.out.println(g);String addr = g.getContent().getName();System.out.println("addr:" + addr + ",distance:" + g.getDistance().getValue());}}
運行結果
GeoResult [content: RedisGeoCommands.GeoLocation(name=10004:博物館, point=Point [x=-122.262509, y=37.793512]), distance: 109.9417 METERS, ]
addr:10004:博物館,distance:109.9417
GeoResult [content: RedisGeoCommands.GeoLocation(name=10002:百貨大樓, point=Point [x=-122.267460, y=37.806234]), distance: 1497.9608 METERS, ]
addr:10002:百貨大樓,distance:1497.9608
GeoResult [content: RedisGeoCommands.GeoLocation(name=10001:市圖書館, point=Point [x=-122.276520, y=37.805185]), distance: 1852.3499 METERS, ]
addr:10001:市圖書館,distance:1852.3499
GeoResult [content: RedisGeoCommands.GeoLocation(name=10003:科學中心, point=Point [x=-122.246984, y=37.810404]), distance: 2244.1523 METERS, ]
addr:10003:科學中心,distance:2244.1523
GEORADIUS 獲取指定成員附件的停車場(deprecated)
@Testvoid geoRadius2(@Autowired RedisTemplate redisTemplate) {// 指定地址附近5公里的停車場RedisGeoCommands.GeoRadiusCommandArgs args = RedisGeoCommands.GeoRadiusCommandArgs.newGeoRadiusArgs().includeCoordinates().includeDistance().sortAscending().limit(5);GeoResults<RedisGeoCommands.GeoLocation<String>> results = redisTemplate.opsForGeo().radius(positionKey, "10001:市圖書館", new Distance(5, Metrics.KILOMETERS), args);System.out.println(results);for (GeoResult<RedisGeoCommands.GeoLocation<String>> g : results) {System.out.println(g);String addr = g.getContent().getName();System.out.println("addr:" + addr + ",distance:" + g.getDistance().getValue());}}
執行結果
GeoResult [content: RedisGeoCommands.GeoLocation(name=10001:市圖書館, point=Point [x=-122.276520, y=37.805185]), distance: 0.0 KILOMETERS, ]
addr:10001:市圖書館,distance:0.0
GeoResult [content: RedisGeoCommands.GeoLocation(name=10002:百貨大樓, point=Point [x=-122.267460, y=37.806234]), distance: 0.8047 KILOMETERS, ]
addr:10002:百貨大樓,distance:0.8047
GeoResult [content: RedisGeoCommands.GeoLocation(name=10004:博物館, point=Point [x=-122.262509, y=37.793512]), distance: 1.7894 KILOMETERS, ]
addr:10004:博物館,distance:1.7894
GeoResult [content: RedisGeoCommands.GeoLocation(name=10003:科學中心, point=Point [x=-122.246984, y=37.810404]), distance: 2.6597 KILOMETERS, ]
addr:10003:科學中心,distance:2.6597
GEOSEARCH 搜索指定經緯度附件的停車場
官方文檔: https://redis.io/commands/geosearch/
注意該命令從redis 6.0.2開始。
GEOSEARCH key <FROMMEMBER member | FROMLONLAT longitude latitude><BYRADIUS radius <M | KM | FT | MI> | BYBOX width height <M | KM |FT | MI>> [ASC | DESC] [COUNT count [ANY]] [WITHCOORD] [WITHDIST][WITHHASH]
@Testvoid geoSearch(@Autowired RedisTemplate redisTemplate) {// 指定經緯度附近5公里的停車場RedisGeoCommands.GeoSearchCommandArgs geoSearchCommandArgs = RedisGeoCommands.GeoSearchCommandArgs.newGeoSearchArgs().includeCoordinates().includeDistance().sortAscending();GeoResults<RedisGeoCommands.GeoLocation> searchResult = redisTemplate.opsForGeo().search(positionKey, new GeoReference.GeoCoordinateReference(-122.2612767, 37.793684), new Distance(5, Metrics.KILOMETERS), geoSearchCommandArgs);
// System.out.println(searchResult);List<GeoResult<RedisGeoCommands.GeoLocation>> content = searchResult.getContent();for (GeoResult<RedisGeoCommands.GeoLocation> g : content) {
// System.out.println(g);RedisGeoCommands.GeoLocation content1 = g.getContent();Distance distance = g.getDistance();System.out.println("content1:" + content1 + ",distance:" + distance);}}
執行結果
content1:RedisGeoCommands.GeoLocation(name=10004:博物館, point=Point [x=-122.262509, y=37.793512]),distance:0.1099 KILOMETERS
content1:RedisGeoCommands.GeoLocation(name=10001:市圖書館, point=Point [x=-122.276520, y=37.805185]),distance:1.8523 KILOMETERS
content1:RedisGeoCommands.GeoLocation(name=10003:科學中心, point=Point [x=-122.246984, y=37.810404]),distance:2.2442 KILOMETERS
GEOSEARCH 搜索指定成員附件的停車場
@Testvoid geoSearch2(@Autowired RedisTemplate redisTemplate) {// 指定成員附近5公里的停車場RedisGeoCommands.GeoSearchCommandArgs geoSearchCommandArgs = RedisGeoCommands.GeoSearchCommandArgs.newGeoSearchArgs().includeCoordinates().includeDistance().sortAscending();GeoResults<RedisGeoCommands.GeoLocation> searchResult = redisTemplate.opsForGeo().search(positionKey, new GeoReference.GeoMemberReference<>("10004:博物館"), new Distance(5, Metrics.KILOMETERS), geoSearchCommandArgs);
// System.out.println(searchResult);List<GeoResult<RedisGeoCommands.GeoLocation>> content = searchResult.getContent();for (GeoResult<RedisGeoCommands.GeoLocation> g : content) {
// System.out.println(g);RedisGeoCommands.GeoLocation content1 = g.getContent();Distance distance = g.getDistance();System.out.println("content1:" + content1 + ",distance:" + distance);}}
執行結果
content1:RedisGeoCommands.GeoLocation(name=10004:博物館, point=Point [x=-122.262509, y=37.793512]),distance:0.0 KILOMETERS
content1:RedisGeoCommands.GeoLocation(name=10001:市圖書館, point=Point [x=-122.276520, y=37.805185]),distance:1.7894 KILOMETERS
content1:RedisGeoCommands.GeoLocation(name=10003:科學中心, point=Point [x=-122.246984, y=37.810404]),distance:2.3219 KILOMETERS
GEOPOS獲取指定成員經緯度
官方文檔 https://redis.io/commands/geopos/
GEOPOS key [member [member ...]]
@Testvoid geoGetPoints(@Autowired RedisTemplate redisTemplate) {List<Point> points = redisTemplate.opsForGeo().position(positionKey, "10003:科學中心", "10004:博物館");System.out.println(points);}
執行結果
[Point [x=-122.246984, y=37.810404], Point [x=-122.262509, y=37.793512]]
GEODIST獲取兩點之間的距離
官方文檔: https://redis.io/commands/geodist/
命令
GEODIST key member1 member2 [M | KM | FT | MI]
單位
- m 米
- km 千米
- mi 英里
- ft 英尺
@Testpublic void testDist(@Autowired RedisTemplate redisTemplate) {Distance distance = redisTemplate.opsForGeo().distance(positionKey, "10003:科學中心", "10004:博物館", RedisGeoCommands.DistanceUnit.KILOMETERS);System.out.println(distance);}
執行結果
2.3219 KILOMETERS
GEOHASH獲取坐標的hash
官方文檔: https://redis.io/commands/geohash/
GEOHASH key [member [member ...]]
這里給定的10005不存在
@Testpublic void testGeoHash(@Autowired RedisTemplate redisTemplate) {List<String> results = redisTemplate.opsForGeo().hash(positionKey, "10004:博物館", "10002:百貨大樓", "10005:歡樂海岸");System.out.println(results);}
執行結果
[9q9p1b55jj0, 9q9p1drt380, null]
ZREM 移除位置
官方沒有GEODEL,需要使用ZREM命令
官方文檔: https://redis.io/commands/zrem/
@Testpublic void testGeoRemove(@Autowired RedisTemplate redisTemplate) {Long remove = redisTemplate.opsForGeo().remove(positionKey, "10002:百貨大樓");List<String> results = redisTemplate.opsForGeo().hash(positionKey, "10004:博物館", "10002:百貨大樓", "10005:歡樂海岸");System.out.println(results);}
執行結果,可以看到10002百貨大樓已經被移除了。
[9q9p1b55jj0, null, null]
注意: redis geo沒有GEODEL命令,需要使用ZREM命令刪除元素
數學球面計算兩點距離
/*** 通過球面計算距離*/
public class DistanceUtil {/*** 赤道半徑(m)*/public final static double RC = 6378137;/*** 極半徑(m)*/public final static double RJ = 6356725;/*** 將角度轉化為弧度*/public static double radians(double d) {return d * Math.PI / 180.0;}/*** 根據兩點經緯度((longitude and latitude))坐標計算直線距離* <p>* S = 2arcsin√sin2(a/2)+cos(lat1)*cos(lat2)*sin2(b/2) ̄*6378137* <p>* 1. lng1 lat1 表示A點經緯度,lng2 lat2 表示B點經緯度;<br>* 2. a=lat1 – lat2 為兩點緯度之差 b=lng1 -lng2 為兩點經度之差;<br>* 3. 6378137為地球赤道半徑,單位為米;** @param longitude1 點1經度* @param latitude1 點1緯度* @param longitude2 點2經度* @param latitude2 點2緯度* @return 距離,單位米(M)* @see <a href=* "https://zh.wikipedia.org/wiki/%E5%8D%8A%E6%AD%A3%E7%9F%A2%E5%85%AC%E5%BC%8F">* 半正矢(Haversine)公式</a>*/public static double getDistanceFromToLngLat(double longitude1, double latitude1, double longitude2, double latitude2) {// 將角度轉化為弧度double radLongitude1 = radians(longitude1);double radLatitude1 = radians(latitude1);double radLongitude2 = radians(longitude2);double radLatitude2 = radians(latitude2);double a = radLatitude1 - radLatitude2;double b = radLongitude1 - radLongitude2;return 2 * Math.asin(Math.sqrt(Math.sin(a / 2) * Math.sin(a / 2)+ Math.cos(radLatitude1) * Math.cos(radLatitude2) * Math.sin(b / 2) * Math.sin(b / 2))) * (RC);}/*** 獲取指定角度方向上的經緯度(longitude and latitude)<br>* 以原始點的經緯度為基點,構建XY二維坐標線,則angle角度則從x軸起算。<br>* 說明:LBS查找,東西南北四個方向的最遠點,構建矩形,在矩形框方位內, 才有可能是該距離方位內的坐標,然后利用距離公式從小范圍內找坐標** @param origLongitude 基點經度* @param origLatitude 基點緯度* @param distance 距離(m)* @param angle 角度(0~360)* @param 經緯度(該角度指定距離的經緯度)*/public static Coordinate getCoordinate(double origLongitude, double origLatitude, double distance, double angle) {double x = distance * Math.sin(angle * Math.PI / 180.0);double y = distance * Math.cos(angle * Math.PI / 180.0);double r = RJ + (RC - RJ) * (90.0 - origLongitude) / 90.0;//由于地球不是標準的球體,中間粗,兩頭細,這里計算平均半徑rdouble d = r * Math.cos(origLongitude * Math.PI / 180);double newLongitude = (y / r + origLongitude * Math.PI / 180.0) * 180.0 / Math.PI;double newLatitude = (x / d + origLatitude * Math.PI / 180.0) * 180.0 / Math.PI;return new Coordinate(newLongitude, newLatitude);}
}
測試用例,用例的經緯度為GEOSEARCH示例中目標到科學中心的距離
@Testvoid getDist() {double longitude1 = -122.2612767;double latitude1 = 37.793684;double longitude2 = -122.2469854;double latitude2 = 37.8104049;double distance = DistanceUtil.getDistanceFromToLngLat(longitude1, latitude1, longitude2, latitude2);System.out.println("經緯度(" + longitude1 + "," + latitude1 + ")到(" + longitude2 + "," + latitude2 + ")的距離為: " + distance + " 米");}
運行測試用例,返回結果與GEO返回的結果基本吻合。
經緯度(-122.2612767,37.793684)到(-122.2469854,37.8104049)的距離為: 2246.057783614703 米