一步步DIY: OSM-Web服务器(六) C/S架构客户端开发中的细节问题

版权声明:本文为博主原创文章,转载时请注明 http://blog.csdn.net/goldenhawking https://blog.csdn.net/goldenhawking/article/details/7980862

        虽然Ajax的Web应用功能强大,但是,很多时候还是需要 C/S模式的客户端程序。最为典型的应用是为现有产品添加新的OSM地图支持(比如替换掉MapX)。很多现有GIS应用都是Native C++的。这些CLient 与网页最大的不同,就是需要即时以及复杂的交互。以OSM为底图,其上需要进行复杂的科学计算,呈现一些网页不容易表现的功能。因此,在NATIVE C++上做一个地图控件是最合适的。

       <1> 坐标系统

       地图控件本质上是一个窗口(Widget),设计这种控件,最细节、最关键的问题就是坐标转换。对摩卡托投影系的OSM地图而言,由于其比例尺是成倍阶跃的,不存在无级缩放、无缝漫游的要求,设计起来相对简单。

       控制当前视图的要素有两个就够了, 一个是比例尺(0-18,整形),一个是视图中心点相对全图的行、列百分比(0.0-1.0)。有了这两个要素,立刻可以计算出需要哪些瓦片来填补背景。

     首先,对比例尺 n 来说,图幅长、宽都是   2^(n+8) 像素, n=0 时,就是 256 *256, n=1为  512 * 512。当然,瓦片的行列容积都是 2^n,即 n=0 时就是 1x1,n=1时为 2x2,n=2时为 4*4,以此类推。通过百分比中心点,即可知道中心点位于当前图幅的像素位置:

    x = cx * (2^(n+8))

    y = cy * (2^(n+8))

    同时,知道了中心点的瓦片编号。由于瓦片都是 256 *256 的,则

   nx = floor(x /256)

   ny = floor(y/256)

    而贴图的偏移为

   ox = x mod 256

   oy = y mod 256

   当然了,具体的贴图还要看窗口坐标的轴方向、窗口坐标的原点。但原理是一样的。按照瓦片的坐标偏移,把略微大于视图范围的各个瓦片顺序读出来,表在底图的缓存里,就完成拼接了。

   其次,对用户拖动来说,屏幕上像素的拖拽偏移  dx, dy 要换算到归一化的 0-1 全图坐标上。这一步原理很简单。由于比例尺已知,图幅大小已知,比例尺n下,用户拖拽了 dx,dy 像素,相当于整个视图中心移动了

  dcx =  dx /  (2^(n+8))

  dcy = dy /  (2^(n+8))

  至于说符号问题,就是向左为正还是向右为正,还要看屏幕坐标系的朝向。

   <2> 与 WGS 84 的转换

  第一步里,所有坐标均是与摩卡托线形相关的。但是,与外部程序接口,我们一般用经纬度,这样,需要转换。摩卡托与经纬度的转换,可以看看wiki,里给出转换的类:

#include <math.h>

class cProjectionMercator
{
public:
	double m_lat,m_lon;
	double m_x,m_y;
	static const double R;
	static const double pi;

	cProjectionMercator(double v_cood=0,double h_cood=0)
		:m_lat(v_cood),
		m_lon(h_cood),
		m_x(h_cood),
		m_y(v_cood)
	{
	}

	virtual ~cProjectionMercator(void)
	{
	}

	cProjectionMercator & ToLatLon()
	{
		m_lon = 180.0 * (m_x / cProjectionMercator::R) /cProjectionMercator::pi;
		m_lat = (atan(exp(m_y / cProjectionMercator::R))-cProjectionMercator::pi/4)*2.0/cProjectionMercator::pi*180.0;
		return *this;
	}

	cProjectionMercator & ToMercator()
	{
		m_x = cProjectionMercator::R * m_lon* cProjectionMercator::pi /180.0;
		m_y = 
			cProjectionMercator::R * 
			log
			(
				tan
				(
					m_lat/180.0* cProjectionMercator::pi/2.0 + cProjectionMercator::pi/4
				)
			);
		return *this;
	}

};
const double cProjectionMercator::R=6378137;
const double cProjectionMercator::pi=3.1415926535897932384626433832795;

调用:

cProjectionMercator m = cProjectionMercator (31,121).ToMercator();
cProjectionMercator w = cProjectionMercator (-1828374,283726).ToLatLon();


<3> 异步拼接与本地缓存

由于瓦片渲染是要花费时间的,如果界面线程老等待下载完毕,当然会导致访问很卡。所以,我们使用独立的线程来下载数据,并异步的返回到当前视图。为了确保视图的有效性,下载任务需要记录瓦片的比例尺、索引,以及请求这个瓦片的视图的版本。如果用户在尚未下载完毕时就拖动、漫游、缩放,需要通知下载器删除旧版本的任务。

为了防止重复下载瓦片浪费时间和带宽,我们本地需要一个以 n, row, col 为联合 hash 的瓦片索引map,以及一个数据文件。每次请求前,先看看本地的 hash_map里面有木有对应瓦片的偏移,有的话,直接 fseek到本地缓存的位置读取数据,木有的话,要下载,并存在缓存。

<4>动手操练

这里,用QT制作的简单的查看器

4.1 视图控制

其主要的控制变量为三个

protected:
    //This is the main para for display
    double m_dCenterX;   //percentage, -0.5~0.5
    double m_dCenterY;   //percentage, -0.5~0.5
    int m_nLevel;        //0-18
初始化为

	this->m_dCenterX = this->m_dCenterY = 0;
	this->m_nLevel = 0;


在每次刷新的时候,即paintEvent里,我们直接刷新存储背景的 m_image 到屏幕。

void tilesviewer::paintEvent( QPaintEvent * /*event*/ )
{
	QPainter painter(this);
	//bitblt
	if (m_image.isNull()==false)
		painter.drawImage(0,0,m_image);
        //...
}

而m_image 是用瓦片拼接产生的。当试图初始化、用户缩放、视图Size变化等事件都会触发重新制作image,看看比例尺变化的曹

//public slots for resolution changed events
void tilesviewer::on_level_changed(int n)
{
	this->m_nLevel = n;
	//force update
	generateBackImage(true);
	update();
}


<4.2> 背景图像拼接

制作m_image 的函数主要分为以下几步。首先是计算需要显示的Image 究竟由哪些瓦片组成,即左右上下的瓦片编号都是多少。而后,是计算偏移,就是这些瓦片表到底图上时,相对左上角偏移的像素数。最后,是进行拼接。这个方法的代码:

//make a new background image
void tilesviewer::generateBackImage(bool need_gen)
{
	m_bNeedReqimage = false;
	//the boolean mask for generate
	if (need_gen == false)
	{
		if (m_image.isNull()==true)
			need_gen = true;
		if (m_image.width()!=this->width()||m_image.height()!=this->height())
			need_gen = true;
	}
	if (need_gen == false)
		return;
	QImage image(this->width(),this->height(),QImage::Format_ARGB32);
	//then, draw tiles in the image
	QPainter imagePainter(&image);
	imagePainter.initFrom(this);
	imagePainter.setRenderHint(QPainter::Antialiasing, true);
	imagePainter.setRenderHint(QPainter::TextAntialiasing, true);
	imagePainter.eraseRect(rect());

	//calculate current position
	int nCenter_X ,nCenter_Y;
	if (true==this->CV_PercentageToPixel(m_nLevel,m_dCenterX,m_dCenterY,&nCenter_X,&nCenter_Y))
	{
		int sz_whole_idx = 1<<m_nLevel;
		//current center
		int nCenX = nCenter_X/256;
		int nCenY = nCenter_Y/256;
		//current left top tile idx
		int nCurrLeftX = floor((nCenter_X-width()/2)/256.0);
		int nCurrTopY = floor((nCenter_Y-height()/2)/256.0);
		//current right btm
		int nCurrRightX = ceil((nCenter_X+width()/2)/256.0);
		int nCurrBottomY = ceil((nCenter_Y+height()/2)/256.0);

		//draw images
		for (int col = nCurrLeftX;col<=nCurrRightX;col++)
		{
			for (int row = nCurrTopY;row<=nCurrBottomY;row++)
			{
				//generate a image
				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;
				//query
				this->getTileImage(m_nLevel,req_col,req_row,image_source);
                //bitblt
				int nTileOffX = (col-nCenX)*256;
				int nTileOffY = (row-nCenY)*256;
				//0,0 lefttop offset
				int zero_offX = nCenter_X % 256;
				int zero_offY = nCenter_Y % 256;
				//bitblt cood
				int tar_x = width()/2-zero_offX+nTileOffX;
				int tar_y = height()/2-zero_offY+nTileOffY;
				//bitblt
				imagePainter.drawImage(tar_x,tar_y,image_source);
			}
		}
		//Draw center mark
		QPen pen(Qt::DotLine);
		pen.setColor(QColor(0,0,255,128));
		imagePainter.setPen(pen);
		imagePainter.drawLine(
			width()/2+.5,height()/2+.5-32,
			width()/2+.5,height()/2+.5+32
			);
		imagePainter.drawLine(
			width()/2+.5-32,height()/2+.5,
			width()/2+.5+32,height()/2+.5
			);

	}

	imagePainter.end();

	m_image = image;
	return;

}

其关键代码是计算各个瓦片的行列,送给getTileImage下载瓦片。上文调用的CV_PercentageToPixel 方法把 -0.5 ~ 0.5 的中心坐标(与0-1类似)换算到当前比例尺的全图像素坐标下,这个函数主要代码

bool tilesviewer::CV_PercentageToPixel(int nLevel,double px,double py,int * nx,int * ny)
{
	if (!nx || !ny || nLevel<0 || nLevel>18)
		return false;
	if (px<-0.5 || px>0.5 || py<-0.5 || py>0.5)
		return false;
	//calculate the region we need
	//first, determine whole map size in current level
	int sz_whole_idx = 1<<nLevel;
	int sz_whole_size = sz_whole_idx*256;

	//calculate pix coodinats
	int nCenter_X = px * sz_whole_size+sz_whole_size/2+.5;
	if (nCenter_X<0)
		nCenter_X = 0;
	if (nCenter_X>=sz_whole_size)
		nCenter_X = sz_whole_size-1;

	int nCenter_Y = py * sz_whole_size+sz_whole_size/2+.5;
	if (nCenter_Y<0)
		nCenter_Y = 0;
	if (nCenter_Y>=sz_whole_size)
		nCenter_Y = sz_whole_size-1;

	*nx = nCenter_X;
	*ny = nCenter_Y;
	return true;
}

下载瓦片的代码getTileImage与采用的下载工具高度相关,这里就不赘述拉.

<4.3> 经纬度到视图的互换
为了实现和经纬度的坐标转换,视图坐标首先被转换为摩卡托,摩卡托接着转换为经纬度。反之亦然。这个功能决定了能否按照经纬度在地图上裱画额外的东东。

bool tilesviewer::oTVP_LLA2DP(double lat,double lon,qint32 * pX,qint32 *pY)
{
	if (!pX||!pY)
		return false;

	//到墨卡托投影
	double dMx = cProjectionMercator(lat,lon).ToMercator().m_x;
	double dMy = cProjectionMercator(lat,lon).ToMercator().m_y;

	//计算巨幅图片内的百分比
	double dperx = dMx/(cProjectionMercator::pi*cProjectionMercator::R*2);
	double dpery = -dMy/(cProjectionMercator::pi*cProjectionMercator::R*2);

	double dCurrImgSize = pow(2.0,m_nLevel)*256;
	//计算要转换的点的巨幅图像坐标
	double dTarX = dperx * dCurrImgSize + dCurrImgSize/2;
	double dTarY = dpery * dCurrImgSize + dCurrImgSize/2;
	//计算当前中心点的巨幅图像坐标
	double dCurrX = dCurrImgSize*m_dCenterX+dCurrImgSize/2;
	double dCurrY = dCurrImgSize*m_dCenterY+dCurrImgSize/2;
	//计算当前中心的全局坐标
	double nOffsetLT_x = (dCurrX-width()/2.0);
	double nOffsetLT_y = (dCurrY-height()/2.0);

	//判断是否在视点内
	*pX = dTarX - nOffsetLT_x+.5;
	*pY = dTarY - nOffsetLT_y+.5;

	if (*pX>=0 && *pX<width()&&*pY>=0&&*pY<height())
		return true;

	return false;
}

bool tilesviewer::oTVP_DP2LLA(qint32 X,qint32 Y,double  * plat,double * plon)
{
	if (!plat||!plon)
		return false;
	//显示经纬度
	//当前缩放图幅的像素数
	double dCurrImgSize = pow(2.0,m_nLevel)*256;
	int dx = X-(width()/2+.5);
	int dy = Y-(height()/2+.5);
	double dImgX = dx/dCurrImgSize+m_dCenterX;
	double dImgY = dy/dCurrImgSize+m_dCenterY;
	double Mercator_x = cProjectionMercator::pi*cProjectionMercator::R*2*dImgX;
	double Mercator_y = -cProjectionMercator::pi*cProjectionMercator::R*2*dImgY;
	double dLat = cProjectionMercator(Mercator_y,Mercator_x).ToLatLon().m_lat;
	double dLon = cProjectionMercator(Mercator_y,Mercator_x).ToLatLon().m_lon;
	if (dLat>=-90 && dLat<=90 && dLon>=-180 && dLon<=180)
	{
		*plat = dLat;
		*plon = dLon;
		return true;
	}
	return false;
}

<5> 后续功能

上述实现的是最简单的单线程客户端。在局域网上,问题不大,如果拿到因特网,就要按照更高要求写多线程、本地缓存了。另外,一个底图并不是目的,目的是让其上的各类应用能够方便的搭建。这要求要向用户提供二次开发的支持。可以采用的比如 QT的插件、ActiveX控件等等。这些东西都有设计模式可以参考,大可以自由发挥啦!

------------------------------------

后记--

2008年,偶然机会接触OSM到现在,其在相关的专业领域发挥了非常大的作用。OSM 作为完全开放的地理信息解决方案,还没有形成ArcGIS那样方便的成套的二次开发环境,但是其丰富的数据本身就是最强大的优势,不断更新的数据使他充满活力。相信大家都期待它的进步,开放的力量是无穷的!今后还会继续跟进OSM的应用。


阅读更多

没有更多推荐了,返回首页