(相关的代码能够从https://github.com/goldenhawking/mercator.qtviewer.git直接克隆)
我们如今是准备做一个C/S架构的地图显示控件。就必定牵扯到坐标系和UI的界面控制。
1、墨卡托投影
眼下osm採用墨卡托投影,这个投影的原理能够用一个假想实验解释。
如果地球是一个透明的球体。在球体的球心有一个光源。我们把一张幕布沿着赤道卷起来。使之与地球内切,地球上的一个点在这块幕布上的投影就是其墨卡托投影位置。
上图中,地球半径是R=6378137米,可想而知,圆柱顶面周长为 2 pi R。我们以0度经线投影为中轴,用剪刀沿着180度经线投影剪开,就可以展开形成地图面。这个地图面的中心与地理位置 (0,0)重合;X轴是赤道,长度为 2 * Pi * R,取值范围 -pi R 到 pi R。即 -20037508 ~ + 20037508 米。Y轴是本初子午线投影。点光源直接照耀球迷质点形成的影子具有拉伸特性,高纬度地区拉伸很严重。其拉伸效果是
y = R ln (tan (pi/4 + lat/2)),在南极、北极存在奇点。
对一般的瓦片地图而言,为了方便计算机处理。一般y的取值范围也是 -20037508 ~ + 20037508 米,反推回去,相应纬度范围仅仅能表示到 -85度~85度。
2、墨卡托投影下的栅格化
上面所说的墨卡托投影完毕了从地球上的一点到虚拟圆柱上一点的映射。然而,为了使用计算机存储、訪问地图,就必须引入採样。所谓的採样,即使用离散的栅格像素表示连续的地理空间数据。我们眼下所见的OpenStreetMap採用了19层比例尺,标号为 0 ~ 18.
在0级,整个世界地图被缩略为一块 256x256 的位图。在1级。我们把分辨率提高一倍。地图由4块256x256的瓦片组成;在二级,规模扩到 16块,以此类推。下图显示的是这样的层次关系:
watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQv/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/Center" alt="">
能够简单推算一下各级比例尺下,完整图幅的大小。栅格化后的坐标左上角是0,0,右下角是 size-1, size-1
级别 | 瓦片行/列数 | 图幅长/宽(size) | 粗略像素分辨率 |
0 | 1 | 256 | 156公里 |
1 | 2 | 512 | 78公里 |
2 | 4 | 1024 | 39公里 |
3 | 8 | 2048 | 19公里 |
4 | 16 | 4096 | 9公里 |
5 | 32 | 8192 | 5公里 |
6 | 64 | 16384 | 2.5公里 |
7 | 128 | 32768 | 1.3公里 |
8 | 256 | 65536 | 611米 |
9 | 512 | 131072 | 305米 |
10 | 1024 | 262144 | 152米 |
11 | 2048 | 524288 | 76米 |
12 | 4096 | 1048576 | 38米 |
13 | 8192 | 2097152 | 19米 |
14 | 16384 | 4194304 | 9米 |
15 | 32768 | 8388608 | 4.5米 |
16 | 65536 | 16777216 | 2.2米 |
17 | 131072 | 33554432 | 1.1米 |
18 | 262144 | 67108864 | 0.5米 |
这些瓦片被编号为行、列,加上比例尺,一个瓦片的索引即为 (level, x, y)。即比例尺、所在列号、所在行号。我们仅仅要这三个參数,就可以从openstreetmap瓦片server上下载瓦片位图。
如:
http://c.tile.openstreetmap.org/0/0/0.png
http://c.tile.openstreetmap.org/2/2/1.png
须要注意的是。OSM瓦片server速度非常慢,当中国的镜像位置有不少,比方
http://120.52.72.79/c.tile.openstreetmap.org/c3pr90ntcsf0/2/2/1.png
建议使用 FireFox 查看页面元素,获得使用的瓦片真实地址。
3、为视图控制而准备的坐标系统
视图在这里可简单理解为一个窗体,具有有限的像素大小。
视图控制包含显示、漫游、缩放等操作。这些操作的关键是从全局坐标(瓦片墨卡托地图)到视图坐标(一般左上角是0,0,右下角是 width-1。height-1) 的相互映射。
我们能够记录当前窗体左上角、右下角的全局坐标,从而实现窗体像素和全局像素的换算。然而,考虑到对于各个比例尺而言,图幅是不断变化的。且记录左上角、右下角坐标在比例尺变化后。相应的全局坐标必须刷新。我们决定不这么做。
能够採用更简单的方式——记录中心相对百分比坐标和当前比例尺来实现同样功能,进而,百分比作为第一种全局坐标系被建立起来,最好还是称之为百分比坐标 。
3.1 全局百分比坐标
百分比坐标是一个等效的尺度无关坐标。记录了当前视图中心位置相应的摩卡托坐标百分比。
//Center Lat,Lon
double m_dCenterX; //percentage, -0.5~0.5
double m_dCenterY; //percentage, -0.5~0.5
int m_nLevel; //0-18
在第一章的投影坐标中。X.Y坐标定义域均为 [-piR , piR],而百分比坐标即为摩卡托坐标与2piR的比值,记录了当前中心实际偏离全图中心的的比例,实质是归一化。
watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQv/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/Center" alt="">
设 px,py为百分比坐标, mx,my为摩卡托投影坐标。二者关系为
px = mx / 2piR
py = - my / 2piR
百分比坐标的优点是尺度无关。在各种比例尺下,一个固定的地理位置相应的百分比坐标不变。
须要注意的是,百分比坐标的Y轴取反。以便在兴许转换中与设备坐标在度量、坐标方向上取得一致。
百分比坐标是一个浮点值。还无法相应到当前比例尺图幅上去。
我们在第二章已经介绍了另外一种全局坐标系,即全局像素坐标系。
3.2全局像素坐标
全局像素坐标即当前比例尺下。一个位置相应的像素位置。第二章的表格里。记录了每一个比例尺下的图幅大小。这个坐标就是地理位置相应当前比例尺图幅上的像素点位置。当前图幅左上角为(0,0),右下角为 (size-1, size-1)。 有了全局像素坐标。就可以计算须要的像素位于哪个瓦片上。由于全部的瓦片都是256x256大小。瓦片位置直接等于 Xw / 256, Yw/256。同一时候。基于3.1, 3.2的工作,依据当前窗体的尺寸,就可以立马计算窗体中随意一点的全局像素坐标。代码是这种:
计算窗体位置(dX,dY)相应的全局像素坐标(px,py)
bool tilesviewer::CV_DP2World(qint32 dX, qint32 dY, double * px, double * py)
{
if (!px||!py) return false;
//!1.Current World Pixel Size, connected to nLevel
int nCurrImgSize = (1<<m_nLevel)*256;
//!2.current DP according to center
double dx = dX-(width()/2.0);
double dy = dY-(height()/2.0);
//!3.Percentage -0.5 ~ 0.5 coord
double dImgX = dx/nCurrImgSize+m_dCenterX;
double dImgY = dy/nCurrImgSize+m_dCenterY;
//!4.Calculat the World pixel coordinats
*px = dImgX * nCurrImgSize + nCurrImgSize/2;
*py = dImgY * nCurrImgSize + nCurrImgSize/2;
return true; }
上图中,黑色为全局像素坐标,红色为百分比坐标,绿色为窗体像素坐标。
3.3 瓦片索引坐标
全局像素是由瓦片拼接而成的。我们用3.2节的世界坐标系可方便求取瓦片像素坐标。
瓦片行 = wy /256, 瓦片列 = wx /256
瓦片像素: (wx % 256, wy %256)
上图中。蓝色为瓦片坐标与瓦片分割线。相应8x8。为比例尺 3 时的情形。
4、视图显示
有了上述几种坐标系,我们能够为用户给定的 中心百分比坐标 m_dCenterX, m_dCenterY,结合窗体大小。直接获得须要的瓦片索引。以及他们粘贴在当前视窗上的像素偏移。
/*!
\brief When the tileviewer enter its paint_event function, this callback will be called. \fn layer_tiles::cb_paintEvent
\param pImage the In-mem image for paint .
*/
void layer_tiles::cb_paintEvent( QPainter * pPainter )
{
if (!m_pViewer || m_bVisible==false) return;
//!1,We should first calculate current windows' position, centerx,centery, in pixcel.
double nCenter_X ,nCenter_Y;
//!2,if the CV_PercentageToPixel returns true, painting will begin.
if (true==m_pViewer->CV_Pct2World(
m_pViewer->centerX(),
m_pViewer->centerY(),
&nCenter_X,&nCenter_Y))
{
int sz_whole_idx = 1<<m_pViewer->level();
//!2.1 get current center tile idx, in tile count.(tile is 256x256)
int nCenX = nCenter_X/256;
int nCenY = nCenter_Y/256;
//!2.2 calculate current left top tile idx
int nCurrLeftX = floor((nCenter_X-m_pViewer->width()/2)/256.0);
int nCurrTopY = floor((nCenter_Y-m_pViewer->height()/2)/256.0);
//!2.3 calculate current right bottom idx
int nCurrRightX = ceil((nCenter_X+m_pViewer->width()/2)/256.0);
int nCurrBottomY = ceil((nCenter_Y+m_pViewer->height()/2)/256.0); //!2.4 a repeat from tileindx left to right.
for (int col = nCurrLeftX;col<=nCurrRightX;col++)
{
//!2.4.1 a repeat from tileindx top to bottom.
for (int row = nCurrTopY;row<=nCurrBottomY;row++)
{
QImage image_source;
int req_row = row, req_col = col;
if (row<0 || row>=sz_whole_idx)
continue;
if (col>=sz_whole_idx)
req_col = col % sz_whole_idx;
if (col<0)
req_col = (col + (1-col/sz_whole_idx)*sz_whole_idx) % sz_whole_idx;
//!2.4.2 call getTileImage to query the image .
if (true==this->getTileImage(m_pViewer->level(),req_col,req_row,image_source))
{
//bitblt
int nTileOffX = (col-nCenX)*256;
int nTileOffY = (row-nCenY)*256;
//0,0 lefttop offset
int zero_offX = int(nCenter_X+0.5) % 256;
int zero_offY = int(nCenter_Y+0.5) % 256;
//bitblt cood
int tar_x = m_pViewer->width()/2-zero_offX+nTileOffX;
int tar_y = m_pViewer->height()/2-zero_offY+nTileOffY;
//bitblt
pPainter->drawImage(tar_x,tar_y,image_source);
}
}
}
} }
这里一个关键的坐标转换函数是CV_Pct2World, 其功能是把给定的百分比坐标换算为世界像素坐标。
5、拖动、漫游、缩放
拖动、漫游相应的是鼠标消息。鼠标消息中的坐标所有都是视窗像素。我们仅仅要把视窗像素换算为百分比,把音响施加到中心坐标下,就可以完毕动作。
缩放是指改变比例尺 m_nLevel,无需别的操作。m_nLevel改变后,立马重绘窗体,一切皆自己主动计算——这得益于我们控制视图的參数是尺度无关的归一化坐标。
我们以拖动为例, 首先,在鼠标按键按下时。记录起始位置:
见bool
layer_tiles::cb_mousePressEvent(QMouseEvent*event)
if (event->button()==Qt::LeftButton)
{
this->m_nStartPosX = event->pos().x();
this->m_nStartPosY = event->pos().y();
}
而后,在鼠标弹起时。记录结束位置并换算, 见boollayer_tiles::cb_mouseReleaseEvent(QMouseEvent*event):
if (event->button()==Qt::LeftButton)
{
int nOffsetX = event->pos().x()-this->m_nStartPosX;
int nOffsetY = event->pos().y()-this->m_nStartPosY;
if (!(nOffsetX ==0 && nOffsetY==0))
{
m_pViewer->DragView(nOffsetX,nOffsetY);
this->m_nStartPosX = this->m_nStartPosY = -1;
res = true;
}
}
上面代码中的 nOffsetX,nOffsetY即是拖动的屏幕像素距离。这个拖动參数被传给了voidtilesviewer::DragView(intnOffsetX,intnOffsetY)
void tilesviewer::DragView(int nOffsetX,int nOffsetY)
{
if (nOffsetX==0 && nOffsetY == 0)
return; int sz_whole_idx = 1<<m_nLevel;
int sz_whole_size = sz_whole_idx*256; double dx = nOffsetX*1.0/sz_whole_size;
double dy = nOffsetY*1.0/sz_whole_size; this->m_dCenterX -= dx;
this->m_dCenterY -= dy;
终于,当前中心的百分比被刷新。
小结
本章介绍了视图的控制。为了简单方便,我们建立了一个百分比坐标系。归一化的參数避免在缩放过程中改动视窗的全局坐标,且很便于计算。
当然,上述坐标系仅仅是显示瓦片须要的坐标系。假设还要和经纬度打交道,那就必须引入经纬度坐标、墨卡托坐标。作为一个插件化的project,我们希望这些坐标转化所有由主框架公布功能,供插件使用,在下一章节,我们就介绍基于Qt插件的图层架构设计。