Qt 插件综合编程-基于插件的OpenStreetMap瓦片查看器客户端(4) 插件绘图

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

   我们在前面的叙述中,介绍了插件的运作管理机制。在本章,将介绍插件具体实现过程中,绘图、交互的要点。

  1、经度边界处理

      地球是一个圆球,从格林威治皇家天文台所在的本初子午线开始,向西为负,向东为正,计量经度。到了太平洋上日期变更线附近时,正180度与负180度交汇,而墨卡托投影的圆柱面也恰好从这里切开。对于一个平面图而言,-180度位于地图最左侧,+180度位于最右侧。如果一个人恰好从 +179度走到 -179度,其实他只行驶了几百公里而已,但在地图上,这两个点像素距离却很远。无论做什么插件,几乎都要遇到这个问题。不处理好这个问题,会导致诡异的结果,比如绘制的直线跨越整个地球,或者多边形无法闭合等等。

     为了解决这个问题,我们可以对地理物件的坐标进行约束:

     点对象,坐标没有限制;

     线对象,至少一个顶点位于 -180~+180之间,另一个顶点与本顶点在球面上构成劣弧, 简单近似处理经度差 < 180度 。

     多边形,   首顶点位于 -180~+180之间,各边在球面上均构成劣弧,简单近似处理经度差 < 180度。

     经过这样处理的坐标,能够避免边界问题,但某些顶点可能位于 -180~+180之外。为了绘制连续、不间断的图形,只要在 paintEvent中,绘制从  -540 ~ + 540 共三个周期的图形即可。绘制的效果如下图所示:

   Center

其对应代码如下:

 

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,绘制三个周期,解决跨界绘制的问题。

 

2、引入模型-视图架构

     Qt 的项视图2D绘图很有特色,把MVC的思想搬到了绘图上面来,在处理事件、消息、碰撞检测方面很有特色。我们在设计插件的时候,很自然的就想到把Qt的 QGraphicsScene挪到插件中助力绘图。然而,在实际设计时,有坐标系的因素需要考虑。

    如果保持QGraphicsScene的尺寸不变,则需要采用尺度无关坐标系。尺度无关坐标系类似墨卡托坐标或者百分比坐标(见系列文章(1)),采用尺度无关坐标系的好处是地图比例尺变化后,无需更新场景大小;坏处是,由于瓦片背景的尺寸不同,需要维护一个动态的坐标映射,把尺度无关坐标映射到全局像素坐标去。这种映射很简单,但会造成一个副作用,即图元线型(主要是粗细)、大小随映射一起变形:

     Center

      这种形变是不好的,因为线应该是固定宽度(比如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 框架在地图上绘制视图项了。下图是不同级别比例尺下,几个视图项的显示情况:

Center

可以看到,线宽、图标大小、字体均保持一致。

 

下一篇,我们将试图阐述一种较为通用的插件化软件架构设计理念,希望其能够帮助后续的各类软件开发需求。

 

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

丁劲犇

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

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

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

打赏作者

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

抵扣说明:

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

余额充值