Qt 插件综合编程-基于插件的OpenStreetMap瓦片查看器client(1)-墨卡托投影与坐标控制

(相关的代码可以从https://github.com/goldenhawking/mercator.qtviewer.git直接克隆)
git

     咱们现在是准备作一个C/S架构的地图显示控件。就一定牵扯到坐标系和UI的界面控制。github

一、墨卡托投影

    眼下osm採用墨卡托投影,这个投影的原理可以用一个假想实验解释。windows

     若是地球是一个透明的球体。在球体的球心有一个光源。咱们把一张幕布沿着赤道卷起来。使之与地球内切,地球上的一个点在这块幕布上的投影就是其墨卡托投影位置。架构

上图中,地球半径是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度。函数

二、墨卡托投影下的栅格化

     上面所说的墨卡托投影完毕了从地球上的一点到虚拟圆柱上一点的映射。然而,为了使用计算机存储、訪问地图,就必须引入採样。所谓的採样,即便用离散的栅格像素表示连续的地理空间数据。咱们眼下所见的OpenStreetMap採用了19层比例尺,标号为 0 ~ 18.post

     在0级,整个世界地图被缩略为一块 256x256 的位图。在1级。咱们把分辨率提升一倍。地图由4块256x256的瓦片组成;在二级,规模扩到 16块,以此类推。下图显示的是这样的层次关系:this


    可以简单推算一下各级比例尺下,完整图幅的大小。栅格化后的坐标左上角是0,0,右下角是 size-1, size-1
spa

级别 瓦片行/列数 图幅长/宽(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 查看页面元素,得到使用的瓦片真实地址。

三、为视图控制而准备的坐标系统

     视图在这里可简单理解为一个窗体,具备有限的像素大小。

视图控制包含显示、漫游、缩放等操做。这些操做的关键是从全局坐标(瓦片墨卡托地图)到视图坐标(通常左上角是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的比值,记录了当前中心实际偏离全图中心的的比例,实质是归一化。


设 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 时的情形。

四、视图显示

     有了上述几种坐标系,咱们可以为用户给定的 中心百分比坐标 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, 其功能是把给定的百分比坐标换算为世界像素坐标。

五、拖动、漫游、缩放

     拖动、漫游相应的是鼠标消息。鼠标消息中的坐标全部都是视窗像素。咱们仅仅要把视窗像素换算为百分比,把音响施加到中心坐标下,就能够完毕动做。

     缩放是指改变比例尺 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();
		}

然后,在鼠标弹起时。记录结束位置并换算, 见 bool layer_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便是拖动的屏幕像素距离。这个拖动參数被传给了 void tilesviewer :: DragView ( int nOffsetX , int nOffsetY )
	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插件的图层架构设计。

相关文章
相关标签/搜索