MongoDB地理空間數據存儲及檢索

以前寫過MySQL空間索引簡單使用,測試也是可用的,當時沒有測試效率問題,由於存儲的矢量數據都只是一個四點的多邊形而已。此次使用mongoDB來作一個行政區劃檢索的功能,記錄一下使用的過程。ios

參考資料:mongodb

一、存入地理數據

MongoDB存儲的數據是bson結構,因此只要你的數據符合這個結構都是能夠存儲的,可是要支持空間索引,就必須按照它的規定來。
早期版本的(2.6以前)僅僅支持簡單的點數據的索引,也就是filed:[x,y]這樣的結構,這個適用範圍太有限了。如今的版本支持GeoJSON形式的數據類型,且支持OCG的空間數據查詢模型,使用上很是方便。shell

GeoJSON數據存入

MongoDB要求把GeoJSON格式的數據以子文檔的形式存入,但實際上並非存入一個完整GeoJSON對象,只須要其中的typecoordinates兩個字段就能夠了。數據庫

下面以存入一個含有地理空間數據的文檔爲例,把全部支持的GeoJSON對象類型都作個示例。
這裏假設存儲一個縣的信息,數據都以json格式表示。json

{
      "xian":"潛山縣",
      "sheng":"安徽"
}

一、Ponit 點數據

如今假設爲這個文檔添加上中心點位置,那麼這個文檔就變成了以下樣子:api

{
      "xian":"潛山縣",
      "sheng":"安徽"
      "center":{ "type":"Point","coordinates":[116.45,30.72]}
}

二、LineString 線數據(多段線)

如今加上一個到省會合肥的路徑連線,那麼文檔就變成了以下樣子:數組

{
      "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]]}
}

三、 Polygon 多邊形數據

多邊形是當前地理信息領域應用的比較多的數據類型。
多邊形描述的是一個面對象,其由兩部分組成,一個外殼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]]]}
}

四、MultiPoint多點、MultiLineString多線、MultiPolygon多多邊形

對於多點、多線和多多邊形,與單個的區別,也就是將coordinates成員改成一個數組形式,存入多個單個形式的座標數據。

五、GeometryCollection 幾何集合

幾何集合就是多個幾何對象的集合,就是一個數組裏面放多個幾何對象。

六、全國區縣行政區劃入庫示例

一、首先下載全國的性質邊界矢量數據,這個能夠從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多邊形表示了。
bbox存在的形式

二、建立地理索引

MongoDB的空間索引有三種,2dsphere2dgeoHaystack

對於某些地理空間查詢操做,必須有相應的索引才行

2.一、2dsphere索引

官網文檔: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...,但看官網文檔,這個是能夠附加的類型,但目前沒有找到相關的文檔。

2.二、2d索引

官網文檔: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」}}

2.三、geoHaystacks索引

官網文檔:https://docs.mongodb.com/manual/core/geohaystack/

geoHaystack索引是一種特殊索引,優化小面積內的返回結果。geoHaystack索引可提升在平面進行幾何(geometry)查詢的性能。
對於使用球面的幾何查詢,2dsphere索引是一個比geoHaystack索引更好的選擇。2dsphere索引容許字段從新排序。
geoHaystack索引要求第一個字段爲location字段。此外,geoHaystack索引僅可經過命令使用,所以始終一次返回全部結果。

三、檢索地理數據

檢索這個也是看官方的文檔比較快,這裏只是一個簡單的介紹。
官方文檔:Geospatial Query Operators

3.1地理空間模型

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之間(包括二者)。

3.二、查詢示例(使用全國縣級行政邊界數據)

上面的數據導入以後,寫幾個查詢的例子來測試一下。

3.2.一、使用$geoIntersects查詢相交的區域

$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數組中的元素順序對檢索速度沒有影響,不知道是否是與字段名的排序有關係。

3.2.3 使用$geoWithin查詢

使用上和$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就比較慢了,由於沒有建索引。

3.2.2 其餘的

其餘的這裏就不記錄了,須要的時候查詢便可。


下面是我對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;
}
相關文章
相關標籤/搜索