前沿
地圖瓦片指將一定范圍內的地圖按照一定的尺寸和格式,按縮放級別或者比例尺,切成若干行和列的正方形柵格圖片,對切片后的正方形柵格圖片被形象的稱為瓦片[。瓦片通常應用于B/S軟件架構下,瀏覽器從服務器獲取地圖數據,由于瓦片相比正射影像底圖數據量小,查詢檢索塊,因此是一種改善地圖瀏覽用戶體驗的優化策略。
地圖瓦片是一種標準,只要按照標準執行,每個人都能從遙感影像或矢量制作地圖瓦片。也可以從公開的地圖服務中獲取瓦片。目前多家GIS地圖廠商都有自己的在線地圖瓦片服務。例如天地圖的在線服務(天地圖API)谷歌的地圖瓦片服務(https://developers.google.com/maps,需科學上網),Esri的在線地圖服務(https://www.arcgis.com/home/webmap/viewer.html)等等。也可以從專門的地圖瓦片服務提供商獲取,例如水經注,91衛圖。不過后者也都是從GIS地圖廠商下載數據,然后緩存到本地,提供區域定制,時間定制等個性化服務。
本文從純技術的角度粗淺談一下技術原理和C++編碼實現。
需求分析
序號 | 需求描述 |
1 | 支持TIF、jpeg、PNG三種瓦片格式。 |
2 | 支持瓦片存儲到MongoDB數據庫、亞馬遜S3、SQLite、Mbtiles文件以及直接存儲到磁盤文件。 |
3 | 生成指定位置的瓦片集。 |
4 | 瓦片多時態索引加載。 |
5 | 瓦片多時態索引更新。 |
6 | 支持從遙感正射影像生成瓦片。 |
7 | 支持從GIS廠商下載瓦片。支持百度地圖,高德地圖,天地圖(需要token),google earth,ESRI下載。類型涵蓋衛星圖、帶標簽的衛星圖、矢量底圖、矢量注記等多種類型瓦片。支持設置代理。支持多線程下載。支持從上次任務繼續下載。支持下載失敗的瓦片多次下載確保完整性。 |
8 | 如果將瓦片輸出到磁盤文件,支持并行化下載,支持多種瓦片命名。例如z/x/y.png, z/y/x.png, z/z_x_y.png, z/z_y_x.png, z/x_y.png,z/y_x.png |
9 | 遙感正射影像裁切成瓦片,支持支持WGS1984、CGCS2000、WebMercator、百度坐標系等多種坐標系定義。 |
10 | 遙感正射影像裁切成瓦片支持8位,16位格式切片。支持動態拉伸。支持無效值填充或替換。支持邊切邊計算以實現類似NDVI類產品。支持自定義波段。 |
11 | 下載的磁盤文件形式瓦片,支持按行政區劃,自定義范圍打包壓縮。 |
12 | MBtile形式的瓦片支持按自定義等級劃分多個存儲文件。例如下載全國1-18級瓦片,支持按照6級劃分為多個MBtiles文件存儲。 |
性能需求
- 支持并行化影像切片,支持集群模式調用。具備全國1-18級瓦片24小時完成的能力(需要硬件支持)。
- 支持并行化瓦片下載。支持自定義線程數目,支持集群模式調用。支持設置下載格式。
?原理實現
瓦片數據是為了提高地圖服務的響應速度,將配好效果的電子地圖按照一定的規則渲染和切割成的 地圖圖片數據。 (1)瓦片切圖范圍和規則:地圖投影數據范圍為 (-20037508.34米 , -20037508.34米), (20037508.34米, 20037508.34米)的正方形范圍;利用金字塔規則映射成不同顯示比 例的像素范圍,之后利用瓦片編號生成規則(見 3)從西北(-20037508.34 米, 20037508.34 米)向東南陸續把數據分成瓦片。(瓦片大小為 256*256 像素)。
(2)金字塔規則:采用四叉樹規則構建金字塔,各層的顯示比例(即分辨率)固定。顯示比例計算方法如下: 由此確定金字塔各層瓦片如下表 1 所示。 zoom 表示縮放的級別,最小為 0; resolution(分辨率):為投影距離與像素距離之比,單位是米/像素 width、height為每一級別數據的像素寬高,乘號左邊為每一級別的寬或高的瓦片數,乘號右邊的 為瓦片的像素寬高。
(3)瓦片編號生成規則:生成的像素范圍左上角為最小值(0,0)右下角為最大值,如圖:
金字塔生成 0-18 級的像素范圍后,利用 256*256 像素大小把像素范圍劃分成一張張正方形瓦片,即用像 素范圍的寬高除以瓦片寬高。瓦片編號與像素位置的關系為: 橫/縱瓦片編號與像素位置整除256。
二、柵格取圖規則
(1)最直觀的取圖規則: x=橫向的瓦片編號 y=縱向的瓦片編號 z=zoom 級別 唯一定位一張瓦片圖。
(2)高德取圖規則: 將切割好的瓦片數據按照一定的目錄規則存放,并將每張瓦片圖片命名,目錄存放規則:
x=(橫向瓦片編號/10)取整 y=(縱向瓦片編號/10)取整 z=zoom 級別唯一定位一張瓦片圖。
?核心代碼設計
地圖瓦片類型定義
/**
* @brief 地圖瓦片類型。
* @note 1、每個類型的值不可隨意變動。枚舉值用4位16進制表示,左起第一位表示廠商,后三位表示
* 瓦片類型。并且瓦片類型值支持組合,以便于后續程序支持多類型同時下載。
* 2、谷歌地圖瓦片需要設置代理翻墻。已經是無偏移的瓦片。
*/
enum class TileType
{unknown = 0x0000,///<未知類型,未初始化類型google_image = 0x0001,///<google衛星圖google_image_mark = 0x0002,///<google帶標簽的衛星圖google_terrain = 0x0004,///<google地形圖google_terrain_mark = 0x0008,///<google帶標簽的地形圖google_route = 0x0010,///<google路線圖google_label = 0x0020,///<google標簽層(路名、地名等)tianditu_vector = 0x1000,///<天地圖矢量底圖tianditu_vector_mark = 0x1001,///<天地圖矢量注記tianditu_image = 0x1002,///<天地圖影像底圖tianditu_image_mark = 0x1004,///<天地圖影像注記tianditu_shading_terrain = 0x1008,///<天地圖地形暈渲tianditu_label_terrain = 0x1010,///<天地圖地形注記tianditu_boundary = 0x1020,///<天地圖全球境界tianditu_vector_mark_en = 0x1040,///<天地圖矢量英文注記tianditu_image_mark_en = 0x1080,///<天地圖影像英文注記esri_imagery = 0x2000 ///<Arcgis衛星地圖};
文件組織方式
/**
* @brief 輸出瓦片文件組織方式。枚舉類型中的d代表directory,n代表name
*/
enum class SaveOrder {zd_xd_yn, ///> z/x/y.pngzd_yd_xn, ///> z/y/x.pngzd_z_x_yn, ///> z/z_x_y.pngzd_z_y_xn, ///> z/z_y_x.pngzd_x_yn, ///> z/x_y.pngzd_y_xn ///> z/y_x.png
};
?瓦片計算器
class LIB_WIMBASE TileCalculator
{
public:TileCalculator(size_t t_size);virtual ~TileCalculator();TileYDirection GetYDirection() const { return ydirect; };TileResolutionUnit GetUnit() const { return tile_unit; };//瓦片實際邊長virtual double GetTileXSize(int level) = 0;virtual double GetTileYSize(int level) = 0;virtual int GetLevelByXResolution(double res) = 0;virtual int GetLevelByYResolution(double res) = 0;virtual double GetLevelXResolution(int level) = 0;virtual double GetLevelYResolution(int level) = 0;virtual int GetLevelByXSize(double res) = 0;virtual int GetLevelByYSize(double res) = 0;virtual int GetColByX(int level, double x) = 0;virtual int GetRowByY(int level, double y) = 0;virtual wim::Envelope GetExtentByLevelRowCol(int level, int row, int col) = 0;virtual wim::Point2D GetLTByLevelRowCol(int level, int row, int col) = 0; /*** @brief 根據總的Envelope,獲取一個最高尺度,該尺度及以下才會更新瓦片。更高尺度的瓦片更新將因為更新區域太小而變得無意義。* @param dst_env 是指瓦片空間參考下的范圍。*/virtual void GetProperTopPos(const wim::Envelope& extent, TilePos&) = 0;/*** @brief 根據extent,獲取一個最高尺度,該最高尺度的寬和高是大于extent范圍的最接近的一個尺度。* @param extent 是指瓦片空間參考下的范圍。* @return 滿足條件的最高尺度;*/virtual int GetTopLevel(const wim::Envelope& extent) = 0;/*** @brief 根據給定尺度level,獲取所有位于空間范圍extent之內的瓦片位置。* @param extent 是指瓦片空間參考下的范圍。* @param level 尺度* @param v 位于空間范圍之內的瓦片位置集合;*/virtual void GetLevelPos(const wim::Envelope& extent, int level, std::vector<TilePos> &v) = 0;/*** @brief 根據給定尺度level,獲取所有位于空間范圍extent之內的瓦片位置。* @param extent 是指瓦片空間參考下的范圍。* @param count 數量上限,即:v的大小不超過此數* @param level 符合條件下的level等級,是輸出值。* @param v 位于空間范圍之內的瓦片位置集合;*/virtual void GetSizedPos(const wim::Envelope& extent, int count, int& level, std::vector<TilePos> &v) = 0;protected:TileYDirection ydirect;size_t tile_size;//邊長TileResolutionUnit tile_unit;
};
typedef boost::shared_ptr<TileCalculator> TileCalculatorPtr;
瓦片坐標系
//定義瓦片坐標系
enum class TileProjection{TileProjWGS1984, //EPSG:4326,[-180.0, 180.0], [-85.0511288, 85.0511288]TileProjCGCS2000, //EPSG:4490, [-180.0, 180.0], [-90, 90]TileProjWebMercator,//EPSG:3857, [-20037508.3427892, 20037508.3427892], [-20037508.3427892, 20037508.3427892] //TileProjTianDiTu, //EPSG:4490, [-180.0, 180.0], [-90, 90],屏蔽掉,直接用TileProjCGCS2000替代TileProjArcGIS, //EPSG:3857, [-20037508.342787, 20037508.342787], [-19971868.88040859, 19971868.88040859]TileProjBaidu //參考橢球:Clarke_1866,坐標系:NAD27, 投影:Mercator
};
瓦片切片上下文環境
/*** @brief 切片處理的上下文環境。* @note 該類定義了切片的執行參數和必備的環境信息,需要在切片之前就被構建和設置。*/
class LIB_IMGTILE TileContext
{
public:TileContext(const TileSetting &tilesetting, TileFormat fmt, Date timestamp, const std::string& theme, bool updating);virtual ~TileContext() { }virtual void Cleanup() const;TileProjection tile_projection; //投影類型SpatialRefPtr tile_srs; //瓦片空間參考對象TileFormat tile_format; //輸出瓦片格式Timestamp timestamp; //時間戳std::string theme_name; //對應的主題名稱int top_level; //切片的最高級別,如0級int bottom_level; //切片的最低級別,例如18級int tile_size; //瓦片大小,現在必須是256size_t jpeg_quality; //瓦片壓縮質量,僅用于JPG格式,取值在50-100之間bool is_initial; //是否是對應主題的初次瓦片生成(即忽略歷史瓦片數據,默認false)bool is_updating; //是否更新模式(默認false,即增加新時間戳數據),注意is_initial和is_updating的區別,前者負責是否讀取歷史數據,后者負責是否覆蓋歷史數據.bool force_stretch_8bit; //切片時是否拉伸波段類型為8位字節型的影像(默認是不拉伸),如果衛星影像不是8位的,該參數無效,算法內部將強制拉伸.bool use_global_minmax; //影像像素值拉伸時使用全局相同的最值(raster_min_value,raster_max_value)int raster_min_value[TILE_NUM_BANDS]; //影像集的全局最小值,該參數僅在需要對原始圖像數據進行拉伸處理時生效。int raster_max_value[TILE_NUM_BANDS]; //影像集的全局最大值,該參數僅在需要對原始圖像數據進行拉伸處理時生效。StretchType stretch_type; //拉伸類型.如果遙感影像是8位字節型的影像,僅當force_stretch_8bit為true有效.如果不是8位影像,僅當use_global_minmax為false時候有效int tile_min_value; //瓦片數據最小值,該參數僅在需要對原始圖像數據進行拉伸處理時生效。int tile_max_value; //瓦片數據最大值,該參數僅在需要對原始圖像數據進行拉伸處理時生效。double no_data_value; //影像集的默認無數據值,當影像未設置無效值時,該參數有效。bool is_full_range_8bit; //輸入的8bit影像中0~255都是有效值。如果該值為true,將算法內部將會把0值變成1,把255變成254。如果該值為false,算法內部不做處理。該參數僅對8bit數據有效。int band_order_rgb[3]; //設置輸出瓦片RGB對應的輸入遙感影像哪3個波段,波段號最小是1.例如可以是{1,2,3},{3,2,1},{1,1,1}等,用戶需要確保波段號真實存在于每個輸入數據.如果用戶沒有設置,內部將按照{1,2,3}多光譜和{1,1,1}全色來處理.uint8_t no_data_rgb[3]; //瓦片無效區域的顏色填充值SpatialRefPtr default_srs; //對于不包含參考系信息的數據使用此默認參考系ResamplingMode resampling; //生成瓦片時的重采樣方式,默認最鄰近。該參數控制遙感影像生成瓦片的采樣方式。std::string storage_uri; //瓦片存儲信息,可以為數據庫地址或磁盤路徑std::string backup_storage; //寫失敗時瓦片的備份路徑TileCalculatorPtr calculator; //瓦片信息計算對象// 讀取瓦片回調函數typedef boost::function<DataArrayPtr(const TileContext& ctx, int level, int col, int row, size_t& len)> ReadTileProxy;ReadTileProxy ReadTile;// 寫出瓦片回調函數typedef boost::function<bool(const TileContext& ctx, int level, int col, int row, DataArrayPtr, size_t len)> WriteTileProxy;WriteTileProxy WriteTile;
}
?單張瓦片定義
/*** @brief 單張瓦片的原始圖像數據。*/
struct LIB_IMGTILE TileImage
{/*** @param nodata RGB三通道的無效值因此nodata為長度是3的數組。*/TileImage(int w, int h, const uint8_t* nodata_rgb);~TileImage();int width;int height;DataArrayPtr img_data; // RRRGGGBBBDataArrayPtr alpha_data; //單波段數組,該數組最終作為輸出圖像的透明度參考.但是在中間處理過程中,用一些特殊值來標記像素位置的特殊的處理過程.//并且約定取值只可能是幾種情況//0代表未處理的區域. 該位置的值填充的是默認值(往往是無效值)。//255代表處理過的區域,該位置包含有效像素值.//1代表該位置在當前過程中被處理.//2代表該位置在當前過程中被進行了無效值替換處理.
};
瓦片索引定義
class LIB_IMGTILE TileIndex
{
public://高度自定義瓦片類型的初始化TileIndex(const TileSetting& ts, int num_levels);~TileIndex();// 增加一個Patch到索引中void AddTilePatch(Timestamp timestamp, int top_level, int bottom_level, const char* wkt);// 更新一個已有的Patch索引bool UpdateTilePatch(Timestamp timestamp, const char* wkt, int top_level, int bottom_level);/*** @brief 查詢指定瓦片的有效時間。* @param timestamp 瓦片的查詢時間,同時用以返回有效時間* @param pos 瓦片的位置* @param overall 不存在時是否返回最接近的瓦片時間* @return 是否查詢到有效的時間*/bool QueryTile(Timestamp& timestamp, TilePos& pos, bool overall);// 查詢指定時間點Patch在其最底層的WKT表示bool QueryTilePatch(Timestamp timestamp, std::string& patch);struct TilePatch;struct TilePatchTree;typedef std::unordered_map<int, TilePatch*> TilePatchMap;private:int total_levels;TileCalculatorPtr calculator;TilePatchMap tile_patches;TilePatchTree* tile_tree;
};
瓦片存儲相關
/**
* @brief 簡單文件存儲上下文環境
* @note 簡單文件存儲將瓦片按照指定的文件布局保存,不考慮時間戳,因此多次切片的結果總是合并為一個整體,
* 便于瀏覽和檢查切片結果正確性。
*/
class LIB_IMGTILE TileFileContext : public TileContext
{
public:TileFileContext(TileProjection pj, TileFormat fmt, Date timestamp, const std::string& theme, bool updating);TileFileContext(const TileSetting &tilesetting, TileFormat fmt, Date timestamp, const std::string& theme, bool updating);virtual ~TileFileContext() { }virtual bool SetTilePatch(const std::string& wkt);//對于JPG、PNG這種格式,是否創建.tfw坐標文件bool make_world_file;//如果是輸出tiff切片數據,是否將空間參考和坐標信息寫入tiff文件,寫入操作會占用一定的時間。默認寫入!bool write_coodinate_to_tiff;std::string tile_layout;
};class LIB_IMGTILE TileFileStorage
{
public:static DataArrayPtr Read(const TileContext& ctx, int level, int col, int row, size_t& len);static bool Write(const TileContext& ctx, int level, int col, int row, DataArrayPtr data, size_t len);private:TileFileStorage() { };
};class LIB_IMGTILE TileMongoStorage
{
public:TileMongoStorage();~TileMongoStorage();DataArrayPtr Read(const TileContext& ctx, int level, int col, int row, size_t& len);bool Write(const TileContext& ctx, int level, int col, int row, DataArrayPtr data, size_t len);bool Flush(const TileContext& ctx, int index = -1);struct TileData{TileData(int level, int col, int row, DataArrayPtr data, size_t len): tile_pos(level, row, col), tile_data(data), tile_len(len) { }TilePos tile_pos;size_t tile_len;DataArrayPtr tile_data;};typedef std::list<TileData> TileList;std::vector<TileList> tile_cache; // 每一個collection的分片都有一個緩存列表int tile_threshold;
};class LIB_IMGTILE TileS3Storage
{
public:static DataArrayPtr Read(const TileContext& ctx, int level, int col, int row, size_t& len);static bool Write(const TileContext& ctx, int level, int col, int row, DataArrayPtr data, size_t len);private:TileS3Storage() { };
};/**
* @brief 存儲瓦片數據到SQLite文件。
*/
class LIB_IMGTILE TileSQLiteStorage
{
public:TileSQLiteStorage(std::string db_path, int watershed_level, Envelope env, TileCalculatorPtr);~TileSQLiteStorage();DataArrayPtr Read(int level, int col, int row, size_t& len);bool Write(int level, int col, int row, DataArrayPtr data, size_t len);bool Flush(std::string dbname);inline std::string GetDBName(int level, int col, int row);int cache_size;//每個DB文件的緩存大小,直白點就是攢夠多少個瓦片寫入一次。struct TileCache{TileCache(int level, int col, int row, DataArrayPtr data, size_t len): tile_pos(level, row, col), tile_data(data), tile_len(len) { }TilePos tile_pos;size_t tile_len;DataArrayPtr tile_data;};typedef std::list<TileCache> TileList;//tile_caches容器大小會根據切片任務的總范圍,確定6級瓦片個數,根據6級瓦片個數確定。std::map<std::string, TileList> tile_caches; //<dbname, TileList>//數據庫DB文件所在路徑,注意是文件夾地址,例如“D:\ImageData\Tile\tiledb”std::string db_path;int watershed_level;Envelope env;//TileSQLiteContextPtr context;
};/**
* @brief 存儲瓦片數據到Mbtiles文件。
*/class LIB_IMGTILE TileMbtilesStorage
{
public:TileMbtilesStorage(std::string db_path);~TileMbtilesStorage();DataArrayPtr Read(int level, int col, int row, size_t& len);bool Write(int level, int col, int row, DataArrayPtr data, size_t len);/*** @brief 將緩沖區內的數據寫到Mbtiles。注意調用者在析構該對象前一定要顯式調用一遍Flush以確保數據完全被保存。*/bool Flush();struct TileCache{TileCache(int level, int col, int row, DataArrayPtr data, size_t len): tile_pos(level, row, col), tile_data(data), tile_len(len) { }TilePos tile_pos;size_t tile_len;DataArrayPtr tile_data;};typedef std::list<TileCache> TileList;std::list<TileCache> tile_caches;std::string db_file; //Mbtiles數據庫文件路徑int cache_size; //每個瓦片文件的緩存大小,直白點就是攢夠多少個瓦片寫入一次。std::mutex storemutex; //數據庫鎖
};
typedef std::shared_ptr<TileMbtilesStorage> TileMbtilesStoragePtr;
?案例
根據提供的矢量范圍,下載Google Earth瓦片
【這里CSDN不讓展示矢量范圍.jpg】
下載參數設置
std::string path = R"(D:\ImageData\tiledownload\Tile)";std::string shp = R"(D:\ImageData\tiledownload\shp\range.shp)";CommonTileDownloader tiler(path, TileSaveType::file, TileType::google_image);tiler.SetProxy("127.0.0.1:33210");tiler.SetDvideLevel(6);tiler.SetTryTimes(6);tiler.Download(shp, 18, 18, progress);
下載成果
將瓦片合并到TIF(帶地理坐標)?
std::string tile_path = wim::String::UTF8ToLocal(R"(D:\ImageData\tiledownload\Tile)");std::string dest_tiff = wim::String::UTF8ToLocal(R"(D:\ImageData\tiledownload\Tile\img_tile18.tif)");TileMosaicFromFiles(tile_path, dest_tiff, SaveOrder::zd_z_x_yn, TileProjection::TileProjWebMercator, 18, progress);
tif圖像展示(實際范圍很大,分辨率0.5米)
地圖瓦片下載器V1.1說明
程序參見“WimTileDownloader”工程
參數說明
參數 | 說明 |
--help | 打印幫助信息 |
-s [ --shppath ] arg | 矢量文件所在目錄,將遍歷目錄下的shp或者geojson進行下載 ???????????????????????? 瓦片或者解壓 |
-r [ --tilepath ] arg | 存儲瓦片的根目錄.程序將在該根目錄下按照:z/x/y順序進行 ???????????????????????? 組織 |
-z [ --zippath ] arg | 瓦片壓縮包保存目錄. |
-t [ --tiletype ] arg | 瓦片類型:img:影像圖片 cia:影像注記? vec:矢量圖片 ???????????????????????? cva:矢量注記 |
-m [ --mode ] arg (=0) | 程序運行模式,1:下載瓦片,2:壓縮瓦片 |
下載瓦片舉例:
?-s "D:\ImageData\Tile\測試可刪\2024年省級行政區劃數據" -r "D:\ImageData\Tile\測試可刪\Tileroot" -m 1 -t "cia"
壓縮瓦片舉例:
?-s "D:\ImageData\Tile\測試可刪\2024年省級行政區劃數據" -r "D:\ImageData\Tile\測試可刪\Tileroot" -z "D:\ImageData\Tile\測試可刪\ziproot" -m 2 -t "cia"
注意事項:
1、每種瓦片類型需要單獨設置瓦片根目錄,否則會重名。
2、如果有token失效,瓦片下載會結束,下次重新執行相同的命令可繼續下載。
3、下載完的矢量會被挪到tilepath文件夾下。壓縮完的矢量會被挪到zippath,以防止重復勞動。
本文章僅供技術交流。更多技術交流請聯系作者:hanbing6174@163.com? ?V:hanbing6174