springboot基础(80):redis geospatial的应用

文章目录

前言

基于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 服务器

引用的依赖

xml 复制代码
		<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

java 复制代码
private String positionKey = "parking";

GEOADD 添加位置

官方文档: https://redis.io/commands/geoadd/

bash 复制代码
GEOADD key [NX | XX] [CH] longitude latitude member [longitude
  latitude member ...]

注意经纬度需要在范围内

  • 经度 -180到180
  • 纬度 -85.05112878到85.05112878

如果你添加的经纬度超出范围会报错

XX: 仅更新已存在的元素。永远不要添加元素。

NX:不要更新已经存在的元素。始终添加新元素。

CH:将返回值从添加的新元素数量修改为更改的元素总数(CH是changed的缩写)。已更改的图元是添加的新元素和已更新坐标的现有图元。因此,命令行中指定的分数与过去相同的元素将不被计算在内。注意:通常情况下,GEOADD的返回值只计算添加的新元素数量。

java 复制代码
 @Test
    void 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替换它。

bash 复制代码
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 英尺
java 复制代码
  @Test
    void 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());
        }
    }

运行结果

bash 复制代码
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)

java 复制代码
 @Test
    void 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());
        }
    }

执行结果

bash 复制代码
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开始。

bash 复制代码
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]
java 复制代码
    @Test
    void 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);
        }
    }

执行结果

bash 复制代码
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 搜索指定成员附件的停车场

java 复制代码
 @Test
    void 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);
        }
    }

执行结果

bash 复制代码
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/

bash 复制代码
GEOPOS key [member [member ...]]
java 复制代码
    @Test
    void geoGetPoints(@Autowired RedisTemplate redisTemplate) {
        List<Point> points = redisTemplate.opsForGeo().position(positionKey, "10003:科学中心", "10004:博物馆");
        System.out.println(points);
    }

执行结果

bash 复制代码
[Point [x=-122.246984, y=37.810404], Point [x=-122.262509, y=37.793512]]

GEODIST获取两点之间的距离

官方文档: https://redis.io/commands/geodist/

命令

bash 复制代码
GEODIST key member1 member2 [M | KM | FT | MI]

单位

  • m 米
  • km 千米
  • mi 英里
  • ft 英尺
bash 复制代码
    @Test
    public void testDist(@Autowired RedisTemplate redisTemplate) {
        Distance distance = redisTemplate.opsForGeo()
                .distance(positionKey, "10003:科学中心", "10004:博物馆", RedisGeoCommands.DistanceUnit.KILOMETERS);
        System.out.println(distance);
    }

执行结果

bash 复制代码
2.3219 KILOMETERS

GEOHASH获取坐标的hash

官方文档: https://redis.io/commands/geohash/

bash 复制代码
GEOHASH key [member [member ...]]

这里给定的10005不存在

bash 复制代码
    @Test
    public void testGeoHash(@Autowired RedisTemplate redisTemplate) {
        List<String> results = redisTemplate.opsForGeo()
                .hash(positionKey, "10004:博物馆", "10002:百货大楼", "10005:欢乐海岸");
        System.out.println(results);
    }

执行结果

bash 复制代码
[9q9p1b55jj0, 9q9p1drt380, null]

ZREM 移除位置

官方没有GEODEL,需要使用ZREM命令

官方文档: https://redis.io/commands/zrem/

java 复制代码
 @Test
    public 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百货大楼已经被移除了。

java 复制代码
[9q9p1b55jj0, null, null]

注意: redis geo没有GEODEL命令,需要使用ZREM命令删除元素

数学球面计算两点距离

java 复制代码
/**
 * 通过球面计算距离
 */
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√sin²(a/2)+cos(lat1)*cos(lat2)*sin²(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;//由于地球不是标准的球体,中间粗,两头细,这里計算平均半徑r
        double 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示例中目标到科学中心的距离

java 复制代码
  @Test
    void 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返回的结果基本吻合。

bash 复制代码
经纬度(-122.2612767,37.793684)到(-122.2469854,37.8104049)的距离为: 2246.057783614703 米
相关推荐
SimonKing26 分钟前
FreeLLMAPI:24 家厂商、128 个模型、1 个 Key智能切换所有可用的模型
java·后端·程序员
IT_陈寒1 小时前
React useEffect里面写异步,我的状态怎么老丢?
前端·人工智能·后端
程序员爱钓鱼2 小时前
第一个 Rust 程序 Hello World
后端·rust
卷福同学8 小时前
不用服务器,不用配环境,我10分钟上线了一个AI Agent
人工智能·后端·算法
小明bishe1811 小时前
计算机毕业设计之基于JAVA的植物科普网站
java·spring boot·spring·架构·课程设计
GetcharZp11 小时前
只需 10 分钟,轻松实现异地组网!Netmaker 保姆级部署教程来了
后端
前端兰博11 小时前
03-Spring Boot
后端
前端兰博11 小时前
01-Java开发基础语言
后端
前端兰博11 小时前
02-Servlet、JDBC、Maven、Mybatis
后端