以前寫過MySQL空間索引簡單使用,測試也是可用的,當時沒有測試效率問題,由於存儲的矢量數據都只是一個四點的多邊形而已。此次使用mongoDB來作一個行政區劃檢索的功能,記錄一下使用的過程。ios
參考資料:mongodb
MongoDB存儲的數據是bson
結構,因此只要你的數據符合這個結構都是能夠存儲的,可是要支持空間索引,就必須按照它的規定來。
早期版本的(2.6以前)僅僅支持簡單的點數據的索引,也就是filed:[x,y]
這樣的結構,這個適用範圍太有限了。如今的版本支持GeoJSON
形式的數據類型,且支持OCG
的空間數據查詢模型,使用上很是方便。shell
MongoDB要求把GeoJSON
格式的數據以子文檔的形式存入,但實際上並非存入一個完整GeoJSON
對象,只須要其中的type
和coordinates
兩個字段就能夠了。數據庫
下面以存入一個含有地理空間數據的文檔爲例,把全部支持的GeoJSON
對象類型都作個示例。
這裏假設存儲一個縣的信息,數據都以json格式表示。json
{ "xian":"潛山縣", "sheng":"安徽" }
如今假設爲這個文檔添加上中心點位置,那麼這個文檔就變成了以下樣子:api
{ "xian":"潛山縣", "sheng":"安徽" "center":{ "type":"Point","coordinates":[116.45,30.72]} }
如今加上一個到省會合肥的路徑連線,那麼文檔就變成了以下樣子:數組
{ "xian":"潛山縣", "sheng":"安徽" "center":{ "type":"Point","coordinates":[116.45,30.72]}, "toShengHui":{ "type":"LineString"," coordinates":[[116.55944824218749,30.58827267102698], [116.87667846679689,30.791396195188927],[116.96594238281249,31.038815104128687], [117.18292236328124,31.264465555752835],[117.22412109375,31.819230730326613]]} }
多邊形是當前地理信息領域應用的比較多的數據類型。
多邊形描述的是一個面對象,其由兩部分組成,一個外殼shell
和0或多個內洞holes
。
外殼和內洞的表示都是一個閉合的線環(LinearRing),表示一個閉合曲面。外殼包括的區域表示在多邊形內的區域,內洞表示的區域則是從外殼表示的區域中排除的區域,以下圖所示的樣子,藍色的是外殼,綠色部分的是內洞。
bash
由於行政邊界的涉及到的點太多,因此這裏就只添加一個外包框做爲示例:
由於GeoJSON中使用bbox字段(四個double值的數組)來描述範圍外包框,因此這裏不能使用bbox來存儲一個多邊形,不然將報錯error inserting documents: location object expected, location array not in correct format
。但奇怪的是,這個bbox
用於搜索的時候倒是無效的。
這裏就不使用內洞了,有內洞的狀況就是coordinates
數組中後面加上線環便可。session
{ "xian":"潛山縣", "sheng":"安徽" "center":{ "type":"Point","coordinates":[116.45,30.72]}, "toShengHui":{ "type":"LineString"," coordinates":[[116.55944824218749,30.58827267102698], [116.87667846679689,30.791396195188927],[116.96594238281249,31.038815104128687], [117.18292236328124,31.264465555752835],[117.22412109375,31.819230730326613]]}, "box":{ "type":"Polygon", "coordinates":[[[116.40701293945311,30.454001045389525], [116.77505493164062,30.454001045389525],[116.77505493164062,30.76248901825541], [116.40701293945311,30.76248901825541],[116.40701293945311,30.454001045389525]]]} }
對於多點、多線和多多邊形,與單個的區別,也就是將coordinates
成員改成一個數組形式,存入多個單個形式的座標數據。
幾何集合就是多個幾何對象的集合,就是一個數組裏面放多個幾何對象。
一、首先下載全國的性質邊界矢量數據,這個能夠從https://www.gadm.org/download_country_v3.html下載。由於中國的矢量數據中沒有臺灣和香港澳門的數據,能夠下在後合成一個。這份數據還有一些其餘的小問題,這裏就不提了。這也是我能找到的免費數據中較好的一份。
二、下載的數據可使用GDAL或QGIS工具將其轉換爲geojson
格式文檔,也能夠轉換,直接寫程序來讀取。我把轉換後的程序再通過了一次簡化,由於全部的邊界線都是MutilPolygon
,而大多數邊界是僅僅一個Polygon
的,因此我把能轉換的都轉換成了Polygon
。
三、由於MongoDB
中存儲字段bbox
爲GeoJSON中的數組形式在查詢的時候會有問題,因此我把它改成了多邊形。但bbox
字段又不支持多邊形,因此改成以box
字段來存儲。由於縣級行政區劃的邊界都比較複雜,點比較多,在查詢的時候會比較慢,因此使用$and
來先查詢box
在查詢geometry
會比較快。
處理好的數據能夠在這裏下載 連接: https://pan.baidu.com/s/1f2c9FEQhkfDzC1dZHn6XmA 密碼:5a2u
四、處理完成以後把全部的json
對象按行寫入了到了(縣級邊界.json.txt)文件中,由於每行都是一個json對象,可使用mongoimport
將其導入數據庫中。可是直接導入會有一個問題,就是數據量太大,沒法一次寫入。因此我先拆分紅了幾個小文件。
split -l 512 縣級邊界.json.txt -d -a 1 中國縣級行政邊界_
執行上面命令後生成了5個小文件,而後逐個導入到mongodb便可。
mongoimport --host 192.168.0.28 --db us --collection xzbj --file 中國縣級行政邊界_0 2018-10-12T14:14:45.165+0800 connected to: 192.168.0.28 2018-10-12T14:14:47.386+0800 imported 512 documents mongoimport --host 192.168.0.28 --db us --collection xzbj --file 中國縣級行政邊界_1 mongoimport --host 192.168.0.28 --db us --collection xzbj --file 中國縣級行政邊界_2 mongoimport --host 192.168.0.28 --db us --collection xzbj --file 中國縣級行政邊界_3 mongoimport --host 192.168.0.28 --db us --collection xzbj --file 中國縣級行政邊界_4
bbox只能以數組的形式存在,如圖所示,我在處理的時候已經改用box多邊形表示了。
MongoDB的空間索引有三種,2dsphere
、2d
、geoHaystack
。
對於某些地理空間查詢操做,必須有相應的索引才行。
官網文檔:https://docs.mongodb.com/manual/core/2dsphere/
2dsphere索引支持查詢球面幾何實體對象。2dsphere是MongoDB地理空間索引支持全部查詢:用於查詢、交點和接近。地理空間查詢的更多信息,參見地理空間查詢。
2dsphere
索引支持點數據(包括傳統點數據方式和GeoJSON的Point方式)。2dsphere
索引因爲是球面索引,因此僅僅支持經緯度數據,座標系爲WGS84
。
版本2之後的MongoDB支持附加GeoJSON對象包括MultiPoint、MultiLineString、MultiPolygon和 GeometryCollection,具體的參考官方文檔。
建立一個2dsphere索引的語句以下:
db.collection.createIndex( { <location field> : "2dsphere" } )
這裏有一個問題,就是建立的時候,有MultiPolygon
等不支持的幾何類型的時候會出現錯誤"errmsg" : "Can't extract geo keys...
,但看官網文檔,這個是能夠附加的類型,但目前沒有找到相關的文檔。
官網文檔:https://docs.mongodb.com/manual/core/2d/
2d索引對存儲爲二維平面上的點的數據使用。 2d索引適用於MongoDB 2.2及更早版本中使用的舊座標對。
應對僅在下列狀況下使用2d索引 您的數據庫具備MongoDB 2.2或更早版本的遺留遺留座標對,以及 您不打算將任何位置數據存儲爲GeoJSON對象。
建立一個2d索引的語句以下:
db.collection.createIndex( { <location field> : "2d" } )
要在具備非簡單排序規則的集合上建立2d索引,必須在建立索引時顯式指定{collation:{locale:「simple」}}
。
官網文檔:https://docs.mongodb.com/manual/core/geohaystack/
geoHaystack
索引是一種特殊索引,優化小面積內的返回結果。geoHaystack
索引可提升在平面進行幾何(geometry)查詢的性能。
對於使用球面的幾何查詢,2dsphere
索引是一個比geoHaystack
索引更好的選擇。2dsphere
索引容許字段從新排序。
geoHaystack
索引要求第一個字段爲location
字段。此外,geoHaystack
索引僅可經過命令使用,所以始終一次返回全部結果。
檢索這個也是看官方的文檔比較快,這裏只是一個簡單的介紹。
官方文檔:Geospatial Query Operators
MongoDB的地理空間查詢能夠實現球面或平面幾何實體對象的查詢。
2dsphere
索引僅僅支持球面查詢(即把點座標數據當作球面經緯度處理)。2d
索引支持平面查詢(即將點座標數據當作平面直角座標系點座標處理)和一些球面查詢,雖然2d
索引支持一些球面查詢,可是對這些球面查詢使用2d
索引可能會致使錯誤,這樣的數據儘可能優先使用2dsphere
索引。下面列出每一個地理空間操做使用的地理空間查詢運算符,支持的查詢和相關說明:
操做符 | 參數類型 | 索引 | 支持查詢 |
---|---|---|---|
$near 鄰近查詢 |
GeoJSON質心點在這個line和下一個line, | 2dsphere | 球面 |
$near | legacy coordinates | 2d | 平面 |
$nearSphere | GeoJSON點 | 2dsphere | 球面 |
$nearSphere | legacy coordinates | 2d | 球面 |
$geoWithin 內部查詢 |
GeoJSON幾何對象 { $geometry: … } |
球面 | |
$geoWithin | { $box: … } | 2d | 平面 |
$geoWithin | { $polygon: … } | 2d | 平面 |
$geoWithin | { $center: … } | 2d | 平面 |
$geoWithin | { $centerSphere: … } | 2d/2dsphere | 球面 |
$geoIntersects 相交查詢 |
{ $geometry: … } 多邊形或多多邊形 |
球面 |
還有$geoNear
這個操做符,這裏就不摘錄了,直接去官網看好了。
由於這些操做符都比較簡單,這裏只單獨介紹一下最有用的$geoIntersects
操做符。
$geoIntersects操做符使用
選擇地理空間數據與指定GeoJSON對象相交的文檔; 即數據和指定對象的交集是非空的。
$geoIntersects
運算符使用$geometry
運算符來指定一個GeoJSON對象做爲參數,使用默認座標系(CRS)指定GeoJSON多邊形或多邊形的使用語法以下:
{ 空間數據字段名: { $geoIntersects: { $geometry: { type: "<GeoJSON對象類型>" , coordinates: [ <coordinates> ] } } } }
對於$geoIntersects
查詢,當指定的GeoJSON的幾何對象大於半個球面時,使用默認的座標系(CRS)會致使互補的幾何對象在查詢結果中。
3.0版中的新功能:要指定單環的GeoJSON多邊形使用自定義MongoDB CRS,使用如下語法在$geometry
表達式中指定自定義MongoDB CRS:
{ <location field>: { $geoIntersects: { $geometry: { type: "Polygon" , coordinates: [ <coordinates> ], crs: { type: "name", properties: { name: "urn:x-mongodb:crs:strictwinding:EPSG:4326" } } } } } }
自定義MongoDB CRS使用逆時針順序包覆,並容許$geoIntersects
支持具備單環GeoJSON多邊形的查詢,該多邊形的面積大於或等於單個半球。若是指定的多邊形小於單個半球,則帶有MongoDB CRS的$ geoIntersects
的行爲與默認的CRS相同。「Big」 Polygons參考
若是指定緯度和經度座標,請先列出經度而後列出緯度: 有效經度值介於-180和180之間(包括二者)。 有效緯度值介於-90和90之間(包括二者)。
上面的數據導入以後,寫幾個查詢的例子來測試一下。
$geoIntersects
用於查詢與給定參數有相交區域的記錄,只能查詢geojson
形式表示的位置字段,有沒有索引均可以查,速度與幾何對象複雜程度有關。
查詢代碼以下
db.getCollection('xzbj').find({ "geometry": { "$geoIntersects": { "$geometry": { "type": "Polygon", "coordinates": [[[116.24633789062499, 40.168380093142446], [116.17492675781251, 40.15998434802335], [116.1199951171875, 40.057052221322], [116.09527587890624, 40.002371935876475], [116.1474609375, 39.890772566959534], [116.10626220703124, 39.70929962338767], [116.3177490234375, 39.69662085337441], [116.57592773437499, 39.7642140375156], [116.6912841796875, 39.86969567045658], [116.69677734375, 39.99605985169435], [116.62261962890624, 40.094882122321145], [116.6143798828125, 40.13899044275822], [116.43310546875, 40.15788524950653], [116.28753662109375, 40.18097176388719], [116.24633789062499, 40.168380093142446]]] } } } })
這些點是沿着北京六環線畫的一個多邊形,查詢的速度比較慢,耗時達到7.12
秒。
下面添加外包框過濾,在進行相交比較,加快查詢速度。這裏要注意我是要的box
字段內存的也是一個Polygon
而不是GeoJSON中的bbox。
db.getCollection('xzbj').find({ "$and": [{ "box": { "$geoIntersects": { "$geometry": { "type": "Polygon", "coordinates": [[[116.0870361328125, 39.69873414348139], [116.71600341796874, 39.69873414348139], [116.71600341796874, 40.17257757632168], [116.0870361328125, 40.17257757632168], [116.0870361328125, 39.69873414348139]]] } } } }, { "geometry": { "$geoIntersects": { "$geometry": { "type": "Polygon", "coordinates": [[[116.24633789062499, 40.168380093142446], [116.17492675781251, 40.15998434802335], [116.1199951171875, 40.057052221322], [116.09527587890624, 40.002371935876475], [116.1474609375, 39.890772566959534], [116.10626220703124, 39.70929962338767], [116.3177490234375, 39.69662085337441], [116.57592773437499, 39.7642140375156], [116.6912841796875, 39.86969567045658], [116.69677734375, 39.99605985169435], [116.62261962890624, 40.094882122321145], [116.6143798828125, 40.13899044275822], [116.43310546875, 40.15788524950653], [116.28753662109375, 40.18097176388719], [116.24633789062499, 40.168380093142446]]] } } } }] })
這樣查詢的速度就大大提高了,僅耗時41毫秒就完成了檢索。通過測試,$and
數組中的元素順序對檢索速度沒有影響,不知道是否是與字段名的排序有關係。
使用上和$geoIntersects
差很少,查詢速度上也差很少。
db.getCollection('xzbj').find({ "geometry": { "$geoWithin": { "$geometry": { "type": "Polygon", "coordinates": [[[93.69140625, 28.76765910569123], [113.37890625, 28.76765910569123], [113.37890625, 39.30029918615029], [93.69140625, 39.30029918615029], [93.69140625, 28.76765910569123]]] } } } })
能夠指定參數幾何對象的座標系。
db.getCollection('xzbj').find({ "box": { "$geoWithin": { "$geometry": { "type": "Polygon", "coordinates": [[[93.69140625, 28.76765910569123], [113.37890625, 28.76765910569123], [113.37890625, 39.30029918615029], [93.69140625, 39.30029918615029], [93.69140625, 28.76765910569123]]], "crs": { "type": "name", "properties": { "name": "urn:x-mongodb:crs:strictwinding:EPSG:4326" } } } } } })
上面使用Polygon
做爲查詢參數,沒有索引也能夠查。若是使用$center
(中心點加半徑)和$box
查詢,則僅有存在2d
索引的狀況才能查詢。
使用$centerSphere
做爲查詢參數的時候,有沒有索引均可以查,並且速度很快。能夠查詢GeoJSON表示的字段,沒有類型限制。
$centerShpere
的參數也是一個點
(先經度後緯度)加一個弧度
,示例以下:
db.getCollection('xzbj').find({ "box": { "$geoWithin": { "$centerSphere": [ [116.3623809,39.9013085] ,0.008 ] } } })
上的很快就搜索到了,由於我對box
字段建了2dSphere
索引。把搜索的字段換爲geometry
就比較慢了,由於沒有建索引。
其餘的這裏就不記錄了,須要的時候查詢便可。
下面是我對QGIS轉出的geojson進行提取的代碼,放在這裏作個存檔。由於這是簡單的使用一次,沒有處理各類錯誤異常等。
#include <iostream> #include <rapidjson/filereadstream.h> #include <rapidjson/rapidjson.h> #include <rapidjson/document.h> #include <rapidjson/stringbuffer.h> #include <rapidjson/writer.h> #include <Poco/Net/HTTPClientSession.h> #include <Poco/Net/HTTPRequest.h> #include <Poco/Net/HTTPResponse.h> #include <Poco/URI.h> #include <Poco/StreamCopier.h> int main() { FILE* fp = fopen("./xianjie.geojson","rb"); char buffer[65536]; rapidjson::FileReadStream is(fp,buffer,sizeof(buffer)); rapidjson::Document doc; doc.ParseStream(is); if(doc.HasParseError()){ std::cout<<"parse error:"<< doc.GetParseError()<<std::endl; return 0; } rapidjson::Value::ConstMemberIterator iter_features = doc.FindMember("features"); if(iter_features == doc.MemberEnd()){ std::cerr<<"沒有找到 iter_features"<<std::endl; return 0; } const auto& features = iter_features->value.GetArray(); // std::cout<<"features size = "<< features.Size()<<std::endl; for(size_t i =0;i<features.Size();++i){ auto& obj = features[i]; rapidjson::Value::ConstMemberIterator iter_properties = obj.FindMember("properties"); if(iter_properties == obj.MemberEnd()){ std::cout<<"features["<<i<<"] format error"<<std::endl; continue; } std::string sheng = iter_properties->value["NL_NAME_1"].GetString(); size_t pos = sheng.rfind('|'); if(pos != std::string::npos){ sheng = sheng.substr(pos+1); } std::string di = iter_properties->value["NL_NAME_2"].GetString(); pos = di.find('|'); if(pos != std::string::npos){ di = di.substr(0,pos); } std::string xian = di; if(iter_properties->value.HasMember("NL_NAME_3") && iter_properties->value["NL_NAME_3"].IsString()){ xian = iter_properties->value["NL_NAME_3"].GetString(); pos = xian.find('|'); if(pos != std::string::npos){ xian = xian.substr(0,pos); } } std::string fullname = sheng + "-" + di + "-" + x2; rapidjson::Value::ConstMemberIterator iter_geometry = obj.FindMember("geometry"); if(iter_geometry == obj.MemberEnd()){ std::cout<<"Failed "<<fullname<<std::endl; continue; } std::string geometry; { rapidjson::StringBuffer buffer; rapidjson::Writer<rapidjson::StringBuffer> w(buffer); if(iter_geometry->value["coordinates"].GetArray().Size() == 1){ iter_geometry->value["coordinates"].GetArray()[0].Accept(w); geometry.append("{\"type\":\"Polygon\",\"coordinates\":"); geometry.append(buffer.GetString(),buffer.GetSize()); geometry.append("}"); } else{ obj.Accept(w); geometry.assign(buffer.GetString(),buffer.GetSize()); } } double bbox[4]; { bbox[0] = obj["bbox"].GetArray()[0].GetDouble(); bbox[1] = obj["bbox"].GetArray()[1].GetDouble(); bbox[2] = obj["bbox"].GetArray()[2].GetDouble(); bbox[3] = obj["bbox"].GetArray()[3].GetDouble(); } std::cout<<"{\"name\":\""<<fullname<<"\",\"type\":\"Feature\"," <<"\"properties\":{\"sheng\":\""<<sheng<<"\",\"di\":\""<<di<<"\",\"xian\":\""<<xian <<"\"},\"box\":{\"type\":\"Polygon\",\"coordinates\":[[" <<"["<<bbox[0]<<","<<bbox[1]<<"]," <<"["<<bbox[2]<<","<<bbox[1]<<"]," <<"["<<bbox[2]<<","<<bbox[3]<<"]," <<"["<<bbox[0]<<","<<bbox[3]<<"]," <<"["<<bbox[0]<<","<<bbox[1]<<"]]]}" /*<<"\"},\"bbox\":["<<bbox[0]<<","<<bbox[1]<<","<<bbox[2]<<","<<bbox[3]<<"]"*/ <<",\"geometry\":"<<geometry<<"}\n"; #ifdef CouchDB_Insert Poco::Net::HTTPClientSession session("192.168.0.28",5984); Poco::Net::HTTPRequest request(Poco::Net::HTTPRequest::HTTP_PUT ,"/xzbj/" + fullname); request.setContentType("application/json"); request.setContentLength((int)geometry.size()); request.set("Authorization","Basic 用戶名密碼base64"); std::ostream& ss = session.sendRequest(request); ss.write(geometry.data(),geometry.size()); Poco::Net::HTTPResponse response; std::istream& rs = session.receiveResponse(response); std::cout<<"\n\n"<<request.getURI()<<"\t\t"<<response.getStatus()<<std::endl; Poco::StreamCopier copier; copier.copyStream(rs,std::cout); #endif } return 0; }