空间索引 S2 学习指南及Java工具类实践

本文介绍了Google S2空间索引相对于Geohash的优势,包括更精细的层级和多边形覆盖能力。提供了S2RegionCoverer的Java工具类,用于处理圆形、矩形和多边形的S2Region,并通过实例展示了其在多边形和圆形覆盖中的应用。此外,列举了相关学习资源和可视化工具。
摘要由CSDN通过智能技术生成

geohash对于大区域查询表现极不良好,经调研测试,改用google的s2。因为涉及的资料、工具较多,特此记录,以备后用。

一 学习指南

0 介绍说明

班门不弄斧,这里推荐 halfrost 大神的空间搜索系列文章,推荐先浏览一遍。
这一篇是对S2的概念介绍:高效的多维空间点索引算法 — Geohash 和 Google S2
这一篇是对S2里面的各个组件的介绍:Google S2 是如何解决空间覆盖最优解问题的?

1 s2 对比 geohash 的优点

  1. s2有30级,geohash只有12级。s2的层级变化较平缓,方便选择。
  2. s2功能强大,解决了向量计算,面积计算,多边形覆盖,距离计算等问题,减少了开发工作量。
  3. s2解决了多边形覆盖问题。个人认为这是其与geohash功能上最本质的不同。给定不规则范围,s2可以计算出一个多边形近似覆盖这个范围。其覆盖用的格子数量根据精确度可控。geohash在这方面十分不友好,划定一个大一点的区域,其格子数可能达到数千,若减少格子数则丢失精度,查询区域过大。
    如下,在min level和max level不变的情况下,只需设置可接受的max cells数值,即可控制覆盖精度。而且其cell的region大小自动适配。geohash要在如此大范围实现高精度覆盖则会产生极为庞大的网格数。
    另外需要注意的是,在minLevel,maxLevel,maxCells这3个参数中,不一定能完全满足.一般而言是优先满足maxLevel即最精细的网格大小,再尽可能控制cell数量在maxCells里面.而minLevel由于会合并网格,所以很难满足(在查询大区域的时候可能会出现一个大网格和很多个小网格,导致木桶效应.这个时候可能将大网格划分为指定等级的小网格,即最终效果为,严格遵循minLevel和maxLevel,为此牺牲maxCells,后面有代码)
    max cells 为10
    max cells 为45

2 精度表

levelmin areamax areaaverage areaunitsRandom cell 1 (UK) min edge lengthRandom cell 1 (UK) max edge lengthRandom cell 2 (US) min edge lengthRandom cell 2 (US) max edge lengthNumber of cells
0085011012.1985011012.1985011012.19km27842 km7842 km7842 km7842 km6
0121252753.0521252753.0521252753.05km23921 km5004 km3921 km5004 km24
024919708.236026521.165313188.26km21825 km2489 km1825 km2489 km96
031055377.481646455.501328297.07km2840 km1167 km1130 km1310 km384
04231564.06413918.15332074.27km2432 km609 km579 km636 km1536
0553798.67104297.9183018.57km2210 km298 km287 km315 km6K
0612948.8126113.3020754.64km2108 km151 km143 km156 km24K
073175.446529.095188.66km254 km76 km72 km78 km98K
08786.201632.451297.17km227 km38 km36 km39 km393K
09195.59408.12324.29km214 km19 km18 km20 km1573K
1048.78102.0381.07km27 km9 km9 km10 km6M
1112.1825.5120.27km23 km5 km4 km5 km25M
123.046.385.07km21699 m2 km2 km2 km100M
130.761.591.27km2850 m1185 m1123 m1225 m402M
140.190.400.32km2425 m593 m562 m613 m1610M
1547520.3099638.9379172.67m2212 m296 m281 m306 m6B
1611880.0824909.7319793.17m2106 m148 m140 m153 m25B
172970.026227.434948.29m253 m74 m70 m77 m103B
18742.501556.861237.07m227 m37 m35 m38 m412B
19185.63389.21309.27m213 m19 m18 m19 m1649B
2046.4197.3077.32m27 m9 m9 m10 m7T
2111.6024.3319.33m23 m5 m4 m5 m26T
222.906.084.83m2166 cm2 m2 m2 m105T
230.731.521.21m283 cm116 cm110 cm120 cm422T
240.180.380.30m241 cm58 cm55 cm60 cm1689T
25453.19950.23755.05cm221 cm29 cm27 cm30 cm7e15
26113.30237.56188.76cm210 cm14 cm14 cm15 cm27e15
2728.3259.3947.19cm25 cm7 cm7 cm7 cm108e15
287.0814.8511.80cm22 cm4 cm3 cm4 cm432e15
291.773.712.95cm212 mm18 mm17 mm18 mm1729e15
300.440.930.74cm26 mm9 mm8 mm9 mm7e18

3 相关资料

  1. halfrost 的 git 仓库,包含空间搜索系列文章:https://github.com/halfrost/Halfrost-Field
  2. s2 官网:https://s2geometry.io
  3. s2 地图/可视化工具(功能强大,强烈推荐): http://s2.sidewalklabs.com/regioncoverer/
  4. 经纬度画圆/画矩形 地图/可视化工具 :https://www.mapdevelopers.com/draw-circle-tool.php
  5. 经纬度画多边形 地图/可视化工具 :http://apps.headwallphotonics.com
  6. csdn参考文章:Google S2 常用操作 :https://blog.csdn.net/deng0515001/article/details/88031153

二 工具类及测试

工具类

说明

以下是个人使用的Java工具类,持有对象S2RegionCoverer(用于获取给定区域的cellId),用于操作3种常见区域类型(圆,矩形,多边形)。支持多种传参(ch.hsr.geohash的WGS84Point传递经纬度,或者Tuple工具类传递经纬度)
主要包含3类方法:

  1. getS2RegionByXXX
    获取给定经纬度坐标对应的S2Region,该region可用于获取cellId,或用于判断包含关系
  2. getCellIdList
    获取给定region的cellId,并通过childrenCellId方法控制其严格遵守minLevel
  3. contains
    对于指定S2Region,判断经纬度或CellToken是否在其范围内

注意事项:

  1. 该S2RegionCoverer不确定是否线程安全,待测试,不建议动态修改其配置参数
  2. 原生的矩形Rect在某些参数下表现不正常,待确认,这里将其转为多边形对待。
代码
public enum S2Util {
    /**
     * 实例
     */
    INSTANCE;

    private static int minLevel = 11;
    private static int maxLevel = 16;
    private static int maxCells = 100;

    private static final S2RegionCoverer COVERER = new S2RegionCoverer();

    static {
        COVERER.setMinLevel(minLevel);
        COVERER.setMaxLevel(maxLevel);
        COVERER.setMaxCells(maxCells);
    }

    /**
     * 将单个cellId转换为多个指定level的cellId
     * @param s2CellId
     * @param desLevel
     * @return
     */
    public static List<S2CellId> childrenCellId(S2CellId s2CellId, Integer desLevel) {
        return childrenCellId(s2CellId, s2CellId.level(), desLevel);
    }

    private static List<S2CellId> childrenCellId(S2CellId s2CellId, Integer curLevel, Integer desLevel) {
        if (curLevel < desLevel) {
            long interval = (s2CellId.childEnd().id() - s2CellId.childBegin().id()) / 4;
            List<S2CellId> s2CellIds = Lists.newArrayList();
            for (int i = 0; i < 4; i++) {
                long id = s2CellId.childBegin().id() + interval * i;
                s2CellIds.addAll(childrenCellId(new S2CellId(id), curLevel + 1, desLevel));
            }
            return s2CellIds;
        } else {
            return Lists.newArrayList(s2CellId);
        }
    }

    /**
     * 将cellToken转换为经纬度
     * @param token
     * @return
     */
    public static Tuple2<Double, Double> toLatLon(String token) {
        S2LatLng latLng = new S2LatLng(S2CellId.fromToken(token).toPoint());
        return Tuple2.tuple(latLng.latDegrees(), latLng.lngDegrees());
    }

    /**
     * 将经纬度转换为cellId
     * @param lat
     * @param lon
     * @return
     */
    public static S2CellId toCellId(double lat, double lon) {
        return S2CellId.fromLatLng(S2LatLng.fromDegrees(lat, lon));
    }

    /**
     * 判断region是否包含指定cellToken
     * @param region
     * @param cellToken
     * @return
     */
    public static boolean contains(S2Region region, String cellToken) {
        return region.contains(new S2Cell(S2CellId.fromToken(cellToken)));
    }

    /**
     * 判断region是否包含指定经纬度坐标
     * @param region
     * @param lat
     * @param lon
     * @return
     */
    public static boolean contains(S2Region region, double lat, double lon) {
        S2LatLng s2LatLng = S2LatLng.fromDegrees(lat, lon);
        try {
            boolean contains = region.contains(new S2Cell(s2LatLng));
            return contains;
        } catch (NullPointerException e) {
            e.printStackTrace();
            return false;
        }
    }


    /**
     * 根据region获取cellId列表
     * @param region
     * @return
     */
    public static List<S2CellId> getCellIdList(S2Region region) {
        List<S2CellId> primeS2CellIdList = COVERER.getCovering(region).cellIds();
        return primeS2CellIdList.stream().flatMap(s2CellId -> S2Util.childrenCellId(s2CellId, S2Util.minLevel).stream()).collect(Collectors.toList());
    }

    /**
     * 根据region获取合并后的cellId列表
     * @param region
     * @return
     */
    public static List<S2CellId> getCompactCellIdList(S2Region region) {
        List<S2CellId> primeS2CellIdList = COVERER.getCovering(region).cellIds();
        return primeS2CellIdList;
    }

    /     获取圆形region       ///

    public static S2Region getS2RegionByCircle(double lat, double lon, double radius) {
        double capHeight = (2 * S2.M_PI) * (radius / 40075017);
        S2Cap cap = S2Cap.fromAxisHeight(S2LatLng.fromDegrees(lat, lon).toPoint(), capHeight * capHeight / 2);
        S2CellUnion s2CellUnion = COVERER.getCovering(cap);
        return cap;
    }

    public static S2Region getS2RegionByCircle(WGS84Point point, double radius) {
        return getS2RegionByCircle(point.getLatitude(), point.getLongitude(), radius);
    }

    /     获取矩形region       ///


    public static S2Region geS2RegionByRect(WGS84Point point1, WGS84Point point2) {
        return getS2RegionByRect(point1.getLatitude(), point1.getLongitude(), point2.getLatitude(), point2.getLongitude());
    }

    public static S2Region getS2RegionByRect(Tuple2<Double, Double> point1, Tuple2<Double, Double> point2) {
        return getS2RegionByRect(point1.getVal1(), point1.getVal2(), point2.getVal1(), point2.getVal2());
    }

    public static S2Region getS2RegionByRect(double lat1, double lon1, double lat2, double lon2) {
        List<Tuple2<Double, Double>> latLonTuple2List = Lists.newArrayList(Tuple2.tuple(lat1, lon1), Tuple2.tuple(lat1, lon2), Tuple2.tuple(lat2, lon2), Tuple2.tuple(lat2, lon1));
        return getS2RegionByPolygon(latLonTuple2List);
    }

    /     获取多边形region       ///

    public static S2Region getS2RegionByPolygon(WGS84Point[] pointArray) {
        List<Tuple2<Double, Double>> latLonTuple2List = Lists.newArrayListWithExpectedSize(pointArray.length);
        for (int i = 0; i < pointArray.length; ++i) {
            latLonTuple2List.add(Tuple2.tuple(pointArray[i].getLatitude(), pointArray[i].getLongitude()));
        }
        return getS2RegionByPolygon(latLonTuple2List);
    }

    public static S2Region getS2RegionByPolygon(Tuple2<Double, Double>[] tuple2Array) {
        return getS2RegionByPolygon(Lists.newArrayList(tuple2Array));
    }

    /**
     * 注意需要以逆时针方向添加坐标点
     */
    public static S2Region getS2RegionByPolygon(List<Tuple2<Double, Double>> latLonTuple2List) {
        List<S2Point> pointList = Lists.newArrayList();
        for (Tuple2<Double, Double> latlonTuple2 : latLonTuple2List) {
            pointList.add(S2LatLng.fromDegrees(latlonTuple2.getVal1(), latlonTuple2.getVal2()).toPoint());

        }
        S2Loop s2Loop = new S2Loop(pointList);
        S2PolygonBuilder builder = new S2PolygonBuilder(S2PolygonBuilder.Options.DIRECTED_XOR);
        builder.addLoop(s2Loop);
        return builder.assemblePolygon();
    }


    /     配置coverer参数       ///

    public static int getMinLevel() {
        return minLevel;
    }

    public static void setMinLevel(int minLevel) {
        S2Util.minLevel = minLevel;
        COVERER.setMinLevel(minLevel);
    }

    public static int getMaxLevel() {
        return maxLevel;
    }

    public static void setMaxLevel(int maxLevel) {
        S2Util.maxLevel = maxLevel;
        COVERER.setMaxLevel(maxLevel);
    }

    public static int getMaxCells() {
        return maxCells;
    }

    public static void setMaxCells(int maxCells) {
        S2Util.maxCells = maxCells;
        COVERER.setMaxCells(maxCells);
    }
}
	
测试
1. (不规则)多边形
  1. 去http://apps.headwallphotonics.com/画个多边形,这里我画了个有棱有角的爱心标志,如下.
    爱心标志
  2. 将左下角的坐标信息作为参数,调用,测试代码如下
        @Test
        public void getCellIdListByPolygon() {
            Map<Integer,Integer> sizeCountMap= Maps.newHashMap();
            StringBuilder sb3=new StringBuilder();
            S2Region s2Region = S2Util.getS2RegionByPolygon(Lists.newArrayList(Tuple2.tuple(23.851458634747043, 113.66432546548037),  Tuple2.tuple(21.60205563594303, 114.82887624673037),Tuple2.tuple(23.771049234941454, 116.18019460610537),Tuple2.tuple(23.16640234327511, 114.94423269204286)));
            List<S2CellId> cellIdListByPolygon = S2Util.getCellIdList(s2Region);
            cellIdListByPolygon.forEach(s2CellId -> {
                System.out.println("Level:" + s2CellId.level() + ",ID:" + s2CellId.toToken() + ",Min:" + s2CellId.rangeMin().toToken() + ",Max:" + s2CellId.rangeMax().toToken());
                sb3.append(",").append(s2CellId.toToken());
                sizeCountMap.put(s2CellId.level(),sizeCountMap.getOrDefault(s2CellId.level(),0)+1);
            });
            System.out.println(sb3.substring(1));
            System.out.println("totalSize:"+cellIdListByPolygon.size());
            sizeCountMap.entrySet().forEach(integerIntegerEntry -> {
                System.out.printf("level:%d,size:%d\n",integerIntegerEntry.getKey(),integerIntegerEntry.getValue());
            });
        }
    
  3. 执行结果如下
    在这里插入图片描述
    可以看到网格数远远超出了设定的100,不过网格大小严格控制在了11到16之间.
  4. 将cellToken列表(逗号分隔)复制到 http://s2.sidewalklabs.com/regioncoverer/ ,点击那个网格(data)标志即可,如下
    在这里插入图片描述
  5. 如果调用的是getCompactCellIdList,则结果如下,其cell数从1000多压缩到200多.
    在这里插入图片描述
2 圆形
  1. 这次我们再用 https://www.mapdevelopers.com/draw-circle-tool.php 到台湾省上面画个圈圈,如下
    圈圈
  2. 然后将其经纬度和坐标信息作为参数,调用,测试方法如下,使用的是getCompactCellIdList
        public void getCellIdListByCircle() {
            Map<Integer,Integer> sizeCountMap= Maps.newHashMap();
            StringBuilder sb3=new StringBuilder();
            S2Region s2Region = S2Util.getS2RegionByCircle(23.753954,120.749615,193511.10);
            List<S2CellId> cellIdListByPolygon = S2Util.getCompactCellIdList(s2Region);
            cellIdListByPolygon.forEach(s2CellId -> {
                System.out.println("Level:" + s2CellId.level() + ",ID:" + s2CellId.toToken() + ",Min:" + s2CellId.rangeMin().toToken() + ",Max:" + s2CellId.rangeMax().toToken());
                sb3.append(",").append(s2CellId.toToken());
                sizeCountMap.put(s2CellId.level(),sizeCountMap.getOrDefault(s2CellId.level(),0)+1);
            });
            System.out.println(sb3.substring(1));
            System.out.println("totalSize:"+cellIdListByPolygon.size());
            sizeCountMap.entrySet().forEach(integerIntegerEntry -> {
                System.out.printf("level:%d,size:%d\n",integerIntegerEntry.getKey(),integerIntegerEntry.getValue());
            });
        }
    
  3. 结果如下
    在这里插入图片描述
评论 9
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值