(相关的代码可以从https://github.com/goldenhawking/mercator.qtviewer.git直接克隆)
我们在前面的叙述中,介绍了插件的运作管理机制。在本章,将介绍插件具体实现过程中,绘图、交互的要点。
地球是一个圆球,从格林威治皇家天文台所在的本初子午线开始,向西为负,向东为正,计量经度。到了太平洋上日期变更线附近时,正180度与负180度交汇,而墨卡托投影的圆柱面也恰好从这里切开。对于一个平面图而言,-180度位于地图最左侧,+180度位于最右侧。如果一个人恰好从 +179度走到 -179度,其实他只行驶了几百公里而已,但在地图上,这两个点像素距离却很远。无论做什么插件,几乎都要遇到这个问题。不处理好这个问题,会导致诡异的结果,比如绘制的直线跨越整个地球,或者多边形无法闭合等等。
为了解决这个问题,我们可以对地理物件的坐标进行约束:
点对象,坐标没有限制;
线对象,至少一个顶点位于 -180~+180之间,另一个顶点与本顶点在球面上构成劣弧, 简单近似处理经度差 < 180度 。
多边形, 首顶点位于 -180~+180之间,各边在球面上均构成劣弧,简单近似处理经度差 < 180度。
经过这样处理的坐标,能够避免边界问题,但某些顶点可能位于 -180~+180之外。为了绘制连续、不间断的图形,只要在 paintEvent中,绘制从 -540 ~ + 540 共三个周期的图形即可。绘制的效果如下图所示:
其对应代码如下:
void qtvplugin_geomarker::cb_paintEvent(QPainter * pImage) { if (!m_pVi || m_bVisible==false) return ; QRect rect = m_pVi->windowRect(); // Calc current viewport in world double leftcenx, topceny, rightcenx, bottomceny; m_pVi->CV_DP2World(0,0,&leftcenx,&topceny); m_pVi->CV_DP2World(rect.width()-1,rect.height()-1,&rightcenx,&bottomceny); int winsz = 256 * (1<<m_pVi->level()); QRectF destin( 0, 0, rect.width(), rect.height() ); //Warpping 180, -180. because longitude +180 and -180 is the same point, // but the map is plat, -180 and + 180 is quite different positions, we // should draw 3 times, to slove cross 180 drawing problems. for (int p = -1; p<=1 ;++p) { QRectF source( leftcenx + p * winsz, topceny, (rightcenx - leftcenx), (bottomceny - topceny) ); m_pScene->render(pImage,destin,source); } }
上面的代码中,p从-1到1,绘制三个周期,解决跨界绘制的问题。
Qt 的项视图2D绘图很有特色,把MVC的思想搬到了绘图上面来,在处理事件、消息、碰撞检测方面很有特色。我们在设计插件的时候,很自然的就想到把Qt的 QGraphicsScene挪到插件中助力绘图。然而,在实际设计时,有坐标系的因素需要考虑。
如果保持QGraphicsScene的尺寸不变,则需要采用尺度无关坐标系。尺度无关坐标系类似墨卡托坐标或者百分比坐标(见系列文章(1)),采用尺度无关坐标系的好处是地图比例尺变化后,无需更新场景大小;坏处是,由于瓦片背景的尺寸不同,需要维护一个动态的坐标映射,把尺度无关坐标映射到全局像素坐标去。这种映射很简单,但会造成一个副作用,即图元线型(主要是粗细)、大小随映射一起变形:
这种形变是不好的,因为线应该是固定宽度(比如3个像素粗),不随着地图缩放而缩放。同样的问题还出现在图标上,图标也会随着映射变化,放大导致模糊失真,或者缩小导致什么也看不着了。为此,我们需要考虑一种全新的方法。
我们响应比例尺变化消息,采用全局像素坐标作为QGraphicsScene场景坐标。这样做的好处,是无论在哪级比例尺下,图标、线条都能保持原有的像素大小,不发生形变。这样做的坏处,是必须在比例尺变化时,重新计算各个视图项的坐标——好在这种计算非常简单:
由于比例尺放大一层,图幅宽度增大一倍,因此,对应的位置点坐标也恰好是乘二的关系;比例尺缩小一层,图幅宽度缩小一倍,因此,对应的位置点坐标也恰好是乘以0.5的关系。解决了这个问题,就解决了视图项随比例尺同步挪动的效果。
为了解决坐标变换的问题,可以从QGraphicsScene类派生子类geoGraphicsScene,我们看一下插件的比例尺变化回调函数中geoGraphicsScene的方法调用:
void qtvplugin_geomarker::cb_levelChanged(intlevel) { if (!m_pVi) return ; //Adjust new Scene rect QRectF rect(0,0,256*(1<<level),256*(1<<level)); m_pScene->adjust_item_coords(level); m_pScene->setSceneRect(rect); }
在这个回调函数中,调用了场景类的voidgeoGraphicsScene::adjust_item_coords(intnewLevel)方法。在该方法中,会枚举所有视图项,并调用视图项的同名函数。视图项必须从子类geoItemBase派生,并重载方法adjust_coords 。我们看一下多边形视图项的定义:
voidgeoGraphicsPolygonItem::adjust_coords(intnNewLevel) { if (vi() && nNewLevel != level()) { /** Since the map is zooming from level() to current level, * the map size zoom ratio can be calculated using pow below. * We can get new coord for current zoom level by multiplicative. */ double ratio = pow(2.0,(nNewLevel - level())); QPolygonF p = this->polygon(); int sz = p.size(); for (int i=0;i<sz;++i) { qreal x = p[i].x() * ratio; qreal y = p[i].y() * ratio; p[i].setX(x); p[i].setY(y); } this->setPolygon(p); } }
在坐标变换方法中,会得到现有的多边形,而后计算尺度差造成的坐标缩放比率 ratio,并逐一施加该因子到每个顶点。最终,把新的多边形设置为当前视图项的多边形。
有了这个方法,就可以方便的使用 MVC 框架在地图上绘制视图项了。下图是不同级别比例尺下,几个视图项的显示情况:
可以看到,线宽、图标大小、字体均保持一致。
下一篇,我们将试图阐述一种较为通用的插件化软件架构设计理念,希望其能够帮助后续的各类软件开发需求。