前沿
地图瓦片指将一定范围内的地图按照一定的尺寸和格式,按缩放级别或者比例尺,切成若干行和列的正方形栅格图片,对切片后的正方形栅格图片被形象的称为瓦片[。瓦片通常应用于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