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

来源:互联网 发布:为淘宝店添加背景音乐 编辑:程序博客网 时间:2024/05/16 08:49

        虽然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);//bitbltif (m_image.isNull()==false)painter.drawImage(0,0,m_image);        //...}

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

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


<4.2> 背景图像拼接

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

//make a new background imagevoid tilesviewer::generateBackImage(bool need_gen){m_bNeedReqimage = false;//the boolean mask for generateif (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 imageQPainter imagePainter(&image);imagePainter.initFrom(this);imagePainter.setRenderHint(QPainter::Antialiasing, true);imagePainter.setRenderHint(QPainter::TextAntialiasing, true);imagePainter.eraseRect(rect());//calculate current positionint 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 centerint nCenX = nCenter_X/256;int nCenY = nCenter_Y/256;//current left top tile idxint nCurrLeftX = floor((nCenter_X-width()/2)/256.0);int nCurrTopY = floor((nCenter_Y-height()/2)/256.0);//current right btmint nCurrRightX = ceil((nCenter_X+width()/2)/256.0);int nCurrBottomY = ceil((nCenter_Y+height()/2)/256.0);//draw imagesfor (int col = nCurrLeftX;col<=nCurrRightX;col++){for (int row = nCurrTopY;row<=nCurrBottomY;row++){//generate a imageQImage 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;//querythis->getTileImage(m_nLevel,req_col,req_row,image_source);                //bitbltint nTileOffX = (col-nCenX)*256;int nTileOffY = (row-nCenY)*256;//0,0 lefttop offsetint zero_offX = nCenter_X % 256;int zero_offY = nCenter_Y % 256;//bitblt coodint tar_x = width()/2-zero_offX+nTileOffX;int tar_y = height()/2-zero_offY+nTileOffY;//bitbltimagePainter.drawImage(tar_x,tar_y,image_source);}}//Draw center markQPen 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 levelint sz_whole_idx = 1<<nLevel;int sz_whole_size = sz_whole_idx*256;//calculate pix coodinatsint 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的应用。