这里主要分析 Mapbox Native GL 移动端sdk 影像瓦片加载到渲染的整个流程
有关mapbox矢量瓦片切分 帖子可见:http://qiancy.com/2016/12/14/mapbox-vector-tiles/
一.地图投影
地图投影有很多,常用的有Web墨卡托(国际组织称3857或900913投影),谷歌、高德、百度、mapbox都是用的墨卡托
投影,其次经纬度投影(4326投影)
地理坐标系是使用三维球面表示地理表面位置,是一种三维球面坐标
投影坐标系是一个以X/Y表示位置的二维平面坐标系,
二者的关系是:
投影坐标系在二维平面上有恒定的长度、面积、角度
投影坐标是地理坐标投影到二维平面上形成的,所以投影坐标总是基于地理坐标,地理坐标基于球体或者椭球体
web墨卡托投影:把球模拟成正球体,全球范围取经度(-180 ~ 180) 维度(-85.05112877980659 ~ 85.05112877980659).其余
区域忽略;
web墨卡托投影坐标系:赤道为标准维线,本初子午线为中央经线,两线的交点为坐标原点向东向北为正,
所以投影坐标系的范围是 -(2*PI*Earth_Radius) * 0.5 ~ (2*PI*Earth_Radius)*0.5 (-20037508.3427892 ~ 20037508.3427892)
墨卡托、经纬度相互转换算法:
public Vector2D lonLat2Mercator(Vector2D lonLat)
{
Vector2D mercator = new Vector2D();
double x = lonLat.X * 20037508.34 / 180;
double y = Math.Log(Math.Tan((90 + lonLat.Y) * Math.PI / 360)) / (Math.PI / 180);
y = y * 20037508.34 / 180;
mercator.X = x;
mercator.Y = y;
return mercator;
}
//墨卡托转经纬度
public Vector2D Mercator2lonLat(Vector2D mercator)
{
Vector2D lonLat = new Vector2D();
double x = mercator.X / 20037508.34 * 180;
double y = mercator.Y / 20037508.34 * 180;
y = 180 / Math.PI * (2 * Math.Atan(Math.Exp(y * Math.PI / 180)) - Math.PI / 2);
lonLat.X = x;
lonLat.Y = y;
return lonLat;
}
Ground Resolution(地面分辨率, piexl/米) ZoomLevel决定像素,latitude决定地面距离
(cos(latitude * pi/180) * 2 * pi * 6378137) / (256 * Pow(2,level))
Map Scale(地图分辨率,cm : 米) 和屏幕dpi有关map scale = 256 * 2level / screen dpi / 0.0254 / (cos(latitude * pi/180) * 2 * pi * 6378137) = 1 : (cos(latitude * pi/180) * 2 * pi * 6378137 * screen dpi) / (256 * 2level * 0.0254)
两者关系:map scale = 1 : ground resolution * screen dpi / 0.0254 meters/inch二.瓦片金字塔
关于瓦片金字塔构建可见链接:http://blog.youkuaiyun.com/wudiazu/article/details/76597294
mapbox中每个数据文件对象 source中都有一个瓦片对象TilePyamid对瓦片进行组织
三.Mapbox 瓦片加载
四.Mapbox瓦片绘制
绘制流程开始先介绍一下mapbox,瓦片坐标转换的几个关键函数,集中在projection.h 和 transform_state 里面
三点中介绍了瓦片加载流程,里面最重要的一点就是根据屏幕四个端点计算经纬度,关于经纬度转屏幕坐标,就借助了之前
介绍的墨卡托投影公式
static Point<double> project(const LatLng& latLng, double scale) {
return Point<double> {
util::LONGITUDE_MAX + latLng.longitude(),
util::LONGITUDE_MAX - util::RAD2DEG * std::log(std::tan(M_PI / 4 + latLng.latitude() * M_PI / util::DEGREES_MAX))
} * worldSize(scale) / util::DEGREES_MAX;
}
static LatLng unproject(const Point<double>& p, double scale, LatLng::WrapMode wrapMode = LatLng::Unwrapped) {
auto p2 = p * util::DEGREES_MAX / worldSize(scale);
return LatLng {
util::DEGREES_MAX / M_PI * std::atan(std::exp((util::LONGITUDE_MAX - p2.y) * util::DEG2RAD)) - 90.0,
p2.x - util::LONGITUDE_MAX,
wrapMode
};
}
但是Mapbox的计算公式与墨卡托投影坐标系又有不同的是,并没有借助地球周长。
由于墨卡托瓦片切分规则 x 与 y 瓦片数量 都是 2^zoom 个,mapbox 取单瓦片(tilesize = 512)的大小,然后根据x/y瓦片的行列号
摆放瓦片位置.所以x.y的取值范围就是 0 ~ 2^zoom * tilesize ,下述代码则是mapbox 构建瓦片变换矩阵的,根据瓦片行列号
void TransformState::matrixFor(mat4& matrix, const UnwrappedTileID& tileID) const {
const uint64_t tileScale = 1ull << tileID.canonical.z;
const double s = Projection::worldSize(scale) / tileScale;
matrix::identity(matrix);
matrix::translate(matrix, matrix,
int64_t(tileID.canonical.x + tileID.wrap * tileScale) * s,
// chg by niurg for wmts
int64_t(tileID.canonical.y) * s, 0);
// end
matrix::scale(matrix, matrix, s / util::EXTENT, s / util::EXTENT, 1);
}
project函数的含义就是就是将经纬度映射到上述的坐标系中,unproject则是将上述坐标系的点映射到对于的经纬度上
经过这个投影变换以后,还需要经过透视投影变换以及视口变换来完全对应上屏幕坐标与经纬度坐标的相互转换
详细内容不再赘述有兴趣的可见 TransformState::latLngToScreenCoordinate以及TransformState::screenCoordinateToLatLng
两个函数的实现
通过上述介绍基本可以将三种的第一步穿起来了,大致流程如下
左边先调用,再生成好RenderTiles以后才到右边的绘制流程,在startRender里面就会调用tile的matrixFor(四中所示函数)放置每个
瓦片的变换矩阵,真正绘制及调用es2.0函数的地方在最后Painter.renderRaster,会根据之前流程中生成的变换矩阵,以及纹理
信息,顶点数据(使用了顶点缓冲区),最终输入到ES2.0的绘制函数进行绘制