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

本文介绍了一款C/S架构地图显示控件的设计思路,重点讲解了墨卡托投影原理及其栅格化过程,并详细阐述了视图控制中涉及的不同坐标系转换方法,包括百分比坐标、全局像素坐标及瓦片索引坐标。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

(相关的代码可以从colorEagleStdio / qplanetosm · GitCode直接克隆)

     我们现在是准备做一个C/S架构的地图显示控件,就必然牵扯到坐标系和UI的界面控制。

1、墨卡托投影

    目前osm采用墨卡托投影,这个投影的原理可以用一个假想实验解释。

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

Center

上图中,地球半径是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块,以此类推。下图显示的是这种层次关系:

Center

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

级别瓦片行/列数图幅长/宽(size)粗略像素分辨率
01256156公里
1251278公里
24102439公里
38204819公里
41640969公里
53281925公里
664163842.5公里
7128327681.3公里
825665536611米
9512131072305米
101024262144152米
11204852428876米
124096104857638米
138192209715219米
141638441943049米
153276883886084.5米
1665536167772162.2米
17131072335544321.1米
18262144671088640.5米

 

这些瓦片被编号为行、列,加上比例尺,一个瓦片的索引即为 (level,  x, y),即比例尺、所在列号、所在行号。我们只要这三个参数,即可从openstreetmap瓦片服务器上下载瓦片位图。

 

如:

http://c.tile.openstreetmap.org/0/0/0.png

http://c.tile.openstreetmap.org/2/2/1.png

需要注意的是,OSM瓦片服务器速度很慢,其中国的镜像位置有不少,比如

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的比值,记录了当前中心实际偏离全图中心的的比例,实质是归一化。

 

Center

设 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;

	}

Center

上图中,黑色为全局像素坐标,红色为百分比坐标,绿色为窗口像素坐标。

 

3.3 瓦片索引坐标

   全局像素是由瓦片拼接而成的,我们用3.2节的世界坐标系可方便求取瓦片像素坐标。

  瓦片行 = wy /256, 瓦片列 = wx /256

 瓦片像素: (wx % 256, wy %256)

Center

上图中,蓝色为瓦片坐标与瓦片切割线,对应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;

	


最终,当前中心的百分比被刷新。

 

小结

     本章介绍了视图的控制。为了简单方便,我们建立了一个百分比坐标系,归一化的参数避免在缩放过程中修改视窗的全局坐标,且非常便于计算。当然,上述坐标系只是显示瓦片需要的坐标系。如果还要和经纬度打交道,那就必须引入经纬度坐标、墨卡托坐标。作为一个插件化的工程,我们希望这些坐标转化全部由主框架发布功能,供插件使用,在下一章节,我们就介绍基于Qt插件的图层架构设计。

 

 

离线地图_openstreetmap_postgresql_瓦片 离线地图_openstreetmap_postgresql_postgis_mapnik_osm2pgsql_osm数据 写于20150414 关于软件地址 事先说明这其实就是我全部放到百度网盘空间里了。 所以万一一不小心我手抖删了,请mail我。 haibinzhagncn@qq.com 软件包括 leaflet osm里面中国和台湾的数据 openlayers geoserver mabox_studio mapnik 和生成瓦片工具需要的前置包等 postgresql osm2pgsql postgis python 一次只能上传一份那我就少点多几份吧: 介绍(免积分) http://download.youkuaiyun.com/detail/a137015127302/8594877 如果懒得自己慢慢找,我想你不介意花点积分的吧。 1.postgreSql_1.&postgis_install http://download.youkuaiyun.com/detail/a137015127302/8594903 2.postgreSql_2.mapnik&python_install http://download.youkuaiyun.com/detail/a137015127302/8594915 3.postgreSql_3.环境变量配置_osm数据导入 http://download.youkuaiyun.com/detail/a137015127302/8594919 4.postgreSql_4.生成图片瓦片byMapnik http://download.youkuaiyun.com/detail/a137015127302/8594921 其他:postgreSql_psql_乱码问题 http://download.youkuaiyun.com/detail/a137015127302/8594937 上传什么的好烦-- 我再试一次要是还是不能上传我就不玩了。切~ 核心内容(英文版公开资料):http://wiki.openstreetmap.org/wiki/Creating_your_own_tiles 以下本人写的中文版本的核心:只要注意这个基本上就没什么大问题了。 当然你要是懒得自己一步一步走,我想你应该也不介意多花点积分的。 摘录 首先版本请用 postgresql-9.3.6-2-windows.exe + postgis-bundle-pg93x32-setup-2.1.5-1.exe 因为至少如果是 postgresql-9.4.1-3-windows.exe + postgis-bundle-pg94x32-setup-2.1.7-1.exe 存在sample数据库无法创建问题。 同时32位下中文客户端提示信息异常问题,你只能改为英文显示,但是又会有warn信息提示你本地不符(很烦不是么) 所以结论最新的未必就是最好的。 而且貌似他们已经开始放弃32位了。 还有这个2.1.7貌似是赶工出来的,因为提示信息写的是支援9.3而事实是否定的。 以上是我重装了n多遍的结论。
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

丁劲犇

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值