(相关的代码能够从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插件的图层架构设计。

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

  1. Qt 插件综合编程-基于插件的OpenStreetMap瓦片查看器client(5) 小结

    经过不断试用与改动,这个查看器终于还是完毕了设计.实现.查看器,顾名思义,没有编辑功能:说的白一点,仅仅是一个以OpenStreetMap为底图的显示装置罢了.和专业GIS相比,这款基于插件的Open ...

  2. jQuery 插件 Magnify 开发简介(仿 Windows 照片查看器)

    前言 因为一些特殊的业务需求,经过一个多月的蛰伏及思考,我开发了这款 jQuery 图片查看器插件 Magnify,它实现了 Windows 照片查看器的所有功能,比如模态窗的拖拽.调整大小.最大化, ...

  3. jQuery 图片查看插件 Magnify 开发简介(仿 Windows 照片查看器)

    前言 因为一些特殊的业务需求,经过一个多月的蛰伏及思考,我开发了这款 jQuery 图片查看器插件 Magnify,它实现了 Windows 照片查看器的所有功能,比如模态窗的拖拽.调整大小.最大化, ...

  4. 用Qt写软件系列二:QCookieViewer(浏览器Cookie查看器)

    预备 继上篇<浏览器缓存查看器QCacheViewer>之后,本篇开始QCookieViewer的编写.Cookie技术作为网站收集用户隐私信息.分析用户偏好的一种手段,广泛应用于各大网站 ...

  5. 用Qt写软件系列一:QCacheViewer(浏览器缓存查看器)

    介绍 Cache技术广泛应用于计算机行业的软硬件领域.该技术既是人们对新技术探讨的结果,也是对当前软硬件计算能力的一种妥协.在浏览器中使用cache技术,可以大幅度提高web页面的响应速度,降低数据传 ...

  6. jQuery插件综合应用(一)注册

    一.介绍 注册和登录是每个稍微有点规模的网站就应该有的功能.登陆功能与注册功能类似,也比注册功能要简单些.所以本文就以注册来说明jQuery插件的应用. jQuery插件的使用非常简单,如果只按照jQ ...

  7. jQuery插件综合应用(三)发布文章页面

    一.使用的插件 一个折叠的功能导航,由Akordeon插件实现.Nanoscroller插件与Tagit插件主要用于美化页面.这里只是测试,其实还可以综合使用其它的插件,例如将Akordeon插件换成 ...

  8. jQuery插件综合应用(二)文字为主的页面

    一.介绍 文字内容是每个网站都有的内容,网站在展示文字内容时,总是比图片.视频等富媒体内容要难一些,因为富媒体容易被用户接受.尤其是越多的文字内容越难以被用户通篇的阅读,跳跃式阅读往往是阅读的主要方式 ...

  9. 移动端下拉刷新、加载更多插件dropload.js(基于jQuery/Zepto)

    移动端下拉刷新.加载更多插件dropload.js(基于jQuery/Zepto) 原文:http://www.grycheng.com/?p=1869 废话不多说,先让大家看一下案例效果: DEMO ...

随机推荐

  1. 查找链表中倒数第k个结点

    题目:输入一个单向链表,输出该链表中倒数第k个结点.链表的倒数第0个结点为链表的尾指针.链表结点定义如下: struct ListNode { int m_nKey; ListNode* m_pNex ...

  2. EF 不允许启动新事务,因为有其他线程正在该会话中运行。

    引起原因:在查询中提交了更改.如在遍历的时候,调用了savechanges(): 解决:把savechange()提到循环外.             IOrderedQueryable<TOH ...

  3. poj 3258 River Hopscotch 题解

    [题意] 牛要到河对岸,在与河岸垂直的一条线上,河中有N块石头,给定河岸宽度L,以及每一块石头离牛所在河岸的距离, 现在去掉M块石头,要求去掉M块石头后,剩下的石头之间以及石头与河岸的最小距离的最大值 ...

  4. 《Android Studio有用指南》4.27 使用演示模式

    本文节选自<Android Studio有用指南> 第4章第27节 作者: 毕小朋 眼下本书已上传到百度阅读, 在百度中搜索[Anroid Studio有用指南]便能够找到本书. 什么是演 ...

  5. 权限项目总结(四) shiro 授权

    概述 Authorization(授权):不难理解,授权就是用来控制当前訪问用户在訪问系统资源权限. 这个词也做证书的解释,从证书这个角度来讲,推断是否拥有对资源訪问的权限时.当前用户须要提供证书. ...

  6. OpenMP 中的线程任务调度

    OpenMP中任务调度主要针对并行的for循环,当循环中每次迭代的计算量不相等时,如果简单地给各个线程分配相同次数的迭代,则可能会造成各个线程计算负载的不平衡,影响程序的整体性能. 如下面的代码中,如 ...

  7. go语言基础之goto的用法

    1.goto的用法 示例: package main //必须有一个main包 import "fmt" func main() { //break //break is not ...

  8. 如何在SharePoint的列表中使用通配符来filter出ListItem?

    一个朋友问我这样一个问题, 他想要快速从SharePoint的文档库中filter出来名字中先带有一个Q, 接着一些其他的字符, 后面再跟着有一个数字20这样的文件.   第一个想法就是修改Share ...

  9. 再谈Cognos利用FM模型来做同比环比

    很早之前已经讲过 <Cognos利用DMR模型开发同比环比>这篇文章里说的是不利用过滤器,而是采用 except (lastPeriods (-9000,[订单数据分析].[日期维度].[ ...

  10. SVG Viewer 3.0安装发现SVG Viewer License.txt无法介入写入,安装失败

    这几天研究SVG,发现"SVG Viewer 3.0安装发现SVG Viewer License.txt无法介入写入,安装失败"这个问题,晚上没找到解答的答案,后来被我们项目经理搞 ...