空間索引 S2 學習指南及Java工具類實踐

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要在如此大範圍實現高精度覆蓋則會產生極爲龐大的網格數。
    max cells 爲10
    max cells 爲45

2 相關資料

  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實踐

以下是個人使用的Java工具類,持有對象S2RegionCoverer(用於獲取給定區域的cellId),用於操作3種常見區域類型(圓,矩形,多邊形)。支持多種傳參(ch.hsr.geohash的WGS84Point傳遞經緯度,或者Tuple工具類傳遞經緯度)
主要包含3類方法:

  1. getS2RegionByXXX
    獲取給定經緯度座標對應的S2Region,該region可用於獲取cellId,或用於判斷包含關係
  2. getCellIdListByXXX
    獲取給定經緯度座標對應的region的cellId
  3. contains
    對於指定S2Region,判斷經緯度或CellToken是否在其範圍內

注意事項:

  1. 該S2RegionCoverer不確定是否線程安全,待測試,不建議動態修改其配置參數
  2. 原生的矩形Rect在某些參數下表現不正常,待確認,這裏將其轉爲多邊形對待。
  3. 其圓形獲取方式使用了魔數,來自參考文章,未確認原理。
public enum S2Util {
    /**
     * 實例
     */
    INSTANCE;

    private static int minLevel = 4;
    private static int maxLevel = 16;
    private static int maxCells = 30;

    private static final S2RegionCoverer coverer = new S2RegionCoverer();

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

    public static void main(String[] args) {
        StringBuilder sb1 = new StringBuilder();
        List<S2CellId> cellIdListByCircle = getCellIdListByCircle(22.6, 113.6, 1000);
        cellIdListByCircle.forEach(s2CellId -> {
            System.out.println("Level:" + s2CellId.level() + ",ID:" + s2CellId.toToken() + ",Min:" + s2CellId.rangeMin().toToken() + ",Max:" + s2CellId.rangeMax().toToken());
            sb1.append(",").append(s2CellId.toToken());
        });
        System.out.println(sb1.substring(1));
        StringBuilder sb2=new StringBuilder();
        List<S2CellId> cellIdListByRect = getCellIdListByRect(22.6, 113.6, 22.3, 113.83);
        cellIdListByRect.forEach(s2CellId -> {
            System.out.println("Level:" + s2CellId.level() + ",ID:" + s2CellId.toToken() + ",Min:" + s2CellId.rangeMin().toToken() + ",Max:" + s2CellId.rangeMax().toToken());
            sb2.append(",").append(s2CellId.toToken());
        });
        System.out.println(sb2.substring(1));
        StringBuilder sb3=new StringBuilder();
        S2Region s2Region = getS2RegionByPolygon(Lists.newArrayList(Tuple2.tuple(20.1, 110.0),  Tuple2.tuple(20.1, 112.0),Tuple2.tuple(21.1, 112.0), Tuple2.tuple(21.1, 110.0)));
        List<S2CellId> cellIdListByPolygon = 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());
        });
        System.out.println(sb3.substring(1));
        System.out.println(contains(s2Region,"315220f936c825dd"));
        Tuple2<Double, Double> latLon = toLatLon("315220f936c825dd");
        System.out.println("lat:"+latLon.getVal1());
        System.out.println("lon:"+latLon.getVal2());
        System.out.println("cellId:" + toCellId(latLon.getVal1(),latLon.getVal2()));

    }

    public static Tuple2<Double,Double> toLatLon(String token){
        S2LatLng latLng = new S2LatLng(S2CellId.fromToken(token).toPoint());
        return Tuple2.tuple(latLng.latDegrees(),latLng.lngDegrees());
    }

    public static S2CellId toCellId(double lat,double lon){
        return S2CellId.fromLatLng(S2LatLng.fromDegrees(lat, lon));
    }

    public static boolean contains(S2Region region, String token) {
        return region.contains(new S2Cell(S2CellId.fromToken(token)));
    }

    public static boolean contains(S2Region region, double lat, double lon) {
        S2LatLng s2LatLng = S2LatLng.fromDegrees(lat, lon);
        return region.contains(new S2Cell(s2LatLng));
    }

    public static List<S2CellId> getCellIdList(S2Region region) {
        return coverer.getCovering(region).cellIds();
    }


    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);
    }

    public static List<S2CellId> getCellIdListByCircle(WGS84Point point, double radius) {
        return getCellIdListByCircle(point.getLatitude(), point.getLongitude(), radius);
    }

    public static List<S2CellId> getCellIdListByCircle(double lat, double lon, double radius) {
        return getCellIdList(getS2RegionByCircle(lat, lon, radius));
    }

    public static List<S2CellId> getCellIdListByRect(WGS84Point point1, WGS84Point point2) {
        return getCellIdListByRect(point1.getLatitude(), point1.getLongitude(), point2.getLatitude(), point2.getLongitude());
    }

    public static List<S2CellId> getCellIdListByRect(Tuple2<Double, Double> point1, Tuple2<Double, Double> point2) {
        return getCellIdListByRect(point1.getVal1(), point1.getVal2(), point2.getVal1(), point2.getVal2());
    }

    public static List<S2CellId> getCellIdListByRect(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 getCellIdListByPolygon(latLonTuple2List);
    }

    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();
    }

    public static List<S2CellId> getCellIdListByPolygon(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 getCellIdListByPolygon(latLonTuple2List);
    }

    public static List<S2CellId> getCellIdListByPolygon(Tuple2<Double, Double>[] tuple2Array) {
        return getCellIdListByPolygon(Lists.newArrayList(tuple2Array));
    }

    /**
     * 注意需要以逆時針方向添加座標點
     */
    public static List<S2CellId> getCellIdListByPolygon(List<Tuple2<Double, Double>> latLonTuple2List) {
        return getCellIdList(getS2RegionByPolygon(latLonTuple2List));
    }


    /////////////     配置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);
    }
}

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章