geohash對於大區域查詢表現極不良好,經調研測試,改用google的s2。因爲涉及的資料、工具較多,特此記錄,以備後用。
一 學習指南
0 介紹說明
班門不弄斧,這裏推薦 halfrost 大神的空間搜索系列文章,推薦先瀏覽一遍。
這一篇是對S2的概念介紹:高效的多維空間點索引算法 — Geohash 和 Google S2
這一篇是對S2裏面的各個組件的介紹:Google S2 是如何解決空間覆蓋最優解問題的?
1 s2 對比 geohash 的優點
- s2有30級,geohash只有12級。s2的層級變化較平緩,方便選擇。
- s2功能強大,解決了向量計算,面積計算,多邊形覆蓋,距離計算等問題,減少了開發工作量。
- s2解決了多邊形覆蓋問題。個人認爲這是其與geohash功能上最本質的不同。給定不規則範圍,s2可以計算出一個多邊形近似覆蓋這個範圍。其覆蓋用的格子數量根據精確度可控。geohash在這方面十分不友好,劃定一個大一點的區域,其格子數可能達到數千,若減少格子數則丟失精度,查詢區域過大。
如下,在min level和max level不變的情況下,只需設置可接受的max cells數值,即可控制覆蓋精度。而且其cell的region大小自動適配。geohash要在如此大範圍實現高精度覆蓋則會產生極爲龐大的網格數。
2 相關資料
- halfrost 的 git 倉庫,包含空間搜索系列文章:https://github.com/halfrost/Halfrost-Field
- s2 官網:https://s2geometry.io
- s2 地圖/可視化工具(功能強大,強烈推薦): http://s2.sidewalklabs.com/regioncoverer/
- 經緯度畫圓/畫矩形 地圖/可視化工具 :https://www.mapdevelopers.com/draw-circle-tool.php
- 經緯度畫多邊形 地圖/可視化工具 :http://apps.headwallphotonics.com
- csdn參考文章:Google S2 常用操作 :https://blog.csdn.net/deng0515001/article/details/88031153
二 Java實踐
以下是個人使用的Java工具類,持有對象S2RegionCoverer(用於獲取給定區域的cellId),用於操作3種常見區域類型(圓,矩形,多邊形)。支持多種傳參(ch.hsr.geohash的WGS84Point傳遞經緯度,或者Tuple工具類傳遞經緯度)
主要包含3類方法:
- getS2RegionByXXX
獲取給定經緯度座標對應的S2Region,該region可用於獲取cellId,或用於判斷包含關係 - getCellIdListByXXX
獲取給定經緯度座標對應的region的cellId - contains
對於指定S2Region,判斷經緯度或CellToken是否在其範圍內
注意事項:
- 該S2RegionCoverer不確定是否線程安全,待測試,不建議動態修改其配置參數
- 原生的矩形Rect在某些參數下表現不正常,待確認,這裏將其轉爲多邊形對待。
- 其圓形獲取方式使用了魔數,來自參考文章,未確認原理。
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);
}
}