OpenGIS 规范致力于为地理信息系统间的数据和服务互操作提供统一,提供了很多在线的 GIS 数据,包括Web Map Service (WMS),Web Feature Service(WFS),Web Coverage Service(WCS)等在线地图服务。为了能够方便使用这样的在线地图数据,QGis专门做了支持在线地图数据的功能,只要电脑联网,就可以轻松访问 OGC 的各种地图服务器,并轻易的将所需要的地图数据下载下来。本文就来与大家探讨一下如何在QGis二次开发时添加这些在线地图图层。
WMS 图层属于图像图层,也就是栅格图层。既然是栅格,先来看一看 QgsRasterLayer 有没有相关的支持。在 API 文档中,QgsRasterLayer 的构造函数有三个,如下图
其中,第三个构造函数,正是我们需要的根据地图文件位置、地图图层名称以及地图数据提供者来构造栅格图像的方式。
看到这里似乎就很简单了,要添加 WMS 图层,与之前添加本地栅格图层一个道理,只不过将原来的本地路径改为地图服务所在的网址就行了。于是,有了下面的代码。
// **这个函数的定义见下文**
addOpenSourceRasterLayer("contextualWMSLegend=0&crs=EPSG:4326&dpiMode=all&featureCount=10&format=image/gif&layers=DC&styles=&url=http://wms.lizardtech.com/lizardtech/iserv/ows",
"DC",
"wms" );
只要触发上面这行代码,就能够添加一个基本的 WMS 图层,效果如下。
如果你的需求仅仅是为了打开某个特定的在线地图数据,到这里应该就能实现了。
然而,有没有办法如同 QGis 那样可以查询可用的地图服务,实时下载显示呢?
为了解决上面的问题,实际上,我们需要模仿QGis的做法,于是还是从源码上面找答案。打开源码,找到添加 WMS 图层的相应代码段,如下
void QgisApp::addWmsLayer()
{
// Fudge for now
QgsDebugMsg( "about to addRasterLayer" );
// TODO: QDialog for now, switch to QWidget in future
QDialog *wmss = dynamic_cast( QgsProviderRegistry::instance()->selectWidget( QString( "wms" ), this ) );
if ( !wmss )
{
QMessageBox::warning( this, tr( "WMS" ), tr( "Cannot get WMS select dialog from provider." ) );
return;
}
connect( wmss, SIGNAL( addRasterLayer( QString const &, QString const &, QString const & ) ),
this, SLOT( addRasterLayer( QString const &, QString const &, QString const & ) ) );
wmss->exec();
delete wmss;
}
其中,重点关注这一句
QDialog *wmss = dynamic_cast( QgsProviderRegistry::instance()->selectWidget( QString( "wms" ), this ) );
这个返回了一个 Dialog类,这个Dialog就是长下图这个造型的
如果有这个东西,岂不是就能直接查找可用的在线地图服务了?是的,实现起来就这么简单,只需要用 QgsProviderRegistry 这个类的实例,通过传入类似 “wms”这种服务源的字符串,就能构造出相应的在线地图查询窗口。这也是为什么我用 WMS 图层作为例子的原因,因为 WCS 和 WFS 图层只是服务源不同而已,传入不同的字符串就行了。
瞬间觉得 so easy。
首先,在应用的菜单栏添加一个功能按钮,添加 WMS 图层,然后将这个按钮的事件方法定义如下:
void qgis_dev::addWMSLayers()
{
QDialog *wms = dynamic_cast( QgsProviderRegistry::instance()->selectWidget( QString( "wms" ), this ) );
if ( !wms )
{
statusBar()->showMessage( tr( "cannot add wms layer." ), 10 );
}
connect( wms, SIGNAL( addRasterLayer( QString const &, QString const &, QString const & ) ),
this, SLOT( addOpenSourceRasterLayer( QString const &, QString const &, QString const & ) ) );
wms->exec();
delete wms;
}
唯一需要注意的就是 connect 这个对话框的方法到相应的添加图层方法上,这个添加图层的方法实现如下
void qgis_dev::addOpenSourceRasterLayer( const QString& url, const QString& basename, const QString& providerKey )
{
QgsRasterLayer *rasterLayer = 0;
if ( providerKey.isEmpty() )
{
rasterLayer = new QgsRasterLayer( url, basename );
}
else
{
rasterLayer = new QgsRasterLayer( url, basename, providerKey );
}
if ( !rasterLayer->isValid() )
{
QMessageBox::critical( this, "error", "layer is invalid" );
return;
}
QgsMapLayerRegistry::instance()->addMapLayer( rasterLayer );
mapCanvasLayerSet.append( rasterLayer );
m_mapCanvas->setExtent( rasterLayer->extent() );
m_mapCanvas->setLayerSet( mapCanvasLayerSet );
m_mapCanvas->setVisible( true );
m_mapCanvas->freeze( false );
m_mapCanvas->refresh();
}
其实跟添加栅格图层的方式没什么特别的不同。
可用服务列表里面一个也没有,点击 add default servers 也依然没用。为何?
这个问题实际上刚开始也困扰了我很久,按照原理来说,这个接口已经非常简单了,不明白哪里没有做。
还是回到源代码,一路追踪到 add default servers 这个方法的代码段,如下
void QgsOWSSourceSelect::addDefaultServers()
{
QMap exampleServers;
exampleServers["DM Solutions GMap"] = "http://www2.dmsolutions.ca/cgi-bin/mswms_gmap";
exampleServers["Lizardtech server"] = "http://wms.lizardtech.com/lizardtech/iserv/ows";
// Nice to have the qgis users map, but I'm not sure of the URL at the moment.
// exampleServers["Qgis users map"] = "http://qgis.org/wms.cgi";
QSettings settings; // 这里其实才是问题的关键
settings.beginGroup( "/Qgis/connections-" + mService.toLower() );
QMap::const_iterator i = exampleServers.constBegin();
for ( ; i != exampleServers.constEnd(); ++i )
{
// Only do a server if it's name doesn't already exist.
QStringList keys = settings.childGroups();
if ( !keys.contains( i.key() ) )
{
QString path = i.key();
settings.setValue( path + "/url", i.value() );
}
}
settings.endGroup();
populateConnectionList();
QMessageBox::information( this, tr( "WMS proxies" ), ""
+ tr( "Several WMS servers have "
"been added to the server list. Note that if "
"you access the internet via a web proxy, you will "
"need to set the proxy settings in the QGIS options dialog." ) + "" );
}
看到这个代码段里面对默认服务器的配置采用了 QSettings 这个类,这个类可能很多 Qt 的初学者不太熟悉,找源代码也不容易将问题定位到这个类这里。关于这个类的使用方法,我截取 Qt 的官方帮助的一段话
关于这个类的使用方法,我这里就不展开了,大家可以自己去查查官方的帮助。这里关注一个点,上面的文字描述中说的很清楚,如果想要全局使用这个类来做为配置选项的辅助,需要实现设置好全局应用的名称和单位的名称。而我们的代码中使用到了 QSettings 这个类,却只用到了默认的无参数的构造函数,程序无法定位到相应的配置上,所以导致了默认的服务器添加不到 comboBox 上去(这个需要一点理解,看似添加图层对话框的使用与 QSettings 没什么关系,但实际上是就是这个小细节在作怪)。
通过以上的描述,我们要做的就很明白了,在 main 函数里面按照 Qt 官方帮助的描述,对 QSettings 做相应的配置。我的配置如下
// 为了使用 QSettings
QCoreApplication::setOrganizationName( "Jacory" );
QCoreApplication::setOrganizationDomain( "jacory.com" ); // 域名好像是可以不用加的
QCoreApplication::setApplicationName( "QGis_Dev" );
点击 connect 按钮,就能连接到相应服务器了,选择好需要添加的图层数据,点击 Add 按钮,稍等片刻,图层就加载到地图窗口中了。
通过上面的描述,添加 WCS 图层就很明白了。只需要在构造服务器选择窗口的时候,将服务器源字符串替换成 “wcs”就好了。完整代码如下
void qgis_dev::addWCSLayers()
{
QDialog *wcs = dynamic_cast( QgsProviderRegistry::instance()->selectWidget( QString( "wcs" ), this ) );
if ( !wcs )
{
statusBar()->showMessage( tr( "cannot add wcs layer." ), 10 );
}
connect( wcs, SIGNAL( addRasterLayer( QString const &, QString const &, QString const & ) ),
this, SLOT( addOpenSourceRasterLayer( QString const &, QString const &, QString const & ) ) );
wcs->exec();
delete wcs;
}
需要说明的是,这个是没有默认的地图服务器的,需要自己用 New 按钮来新建。
这个其实也和上面的两种图层添加方式差不多,需要注意的就是,WFS 图层是矢量图层,因此,添加的时候需要使用矢量的添加方式。
void qgis_dev::addWFSLayers()
{
if ( !m_mapCanvas ) {return;}
QDialog *wfs = dynamic_cast( QgsProviderRegistry::instance()->selectWidget( QString( "WFS" ), this ) );
if ( !wfs )
{
QMessageBox::warning( this, tr( "WFS" ), tr( "Cannot get WFS select dialog from provider." ) );
return;
}
connect( wfs, SIGNAL( addWfsLayer( QString, QString ) ),
this, SLOT( addWfsLayer( const QString, const QString ) ) );
//re-enable wfs with extent setting: pass canvas info to source select
wfs->setProperty( "MapExtent", m_mapCanvas->extent().toString() );
if ( m_mapCanvas->mapSettings().hasCrsTransformEnabled() )
{
//if "on the fly" reprojection is active, pass canvas CRS
wfs->setProperty( "MapCRS", m_mapCanvas->mapSettings().destinationCrs().authid() );
}
bool bkRenderFlag = m_mapCanvas->renderFlag();
m_mapCanvas->setRenderFlag( false );
wfs->exec();
m_mapCanvas->setRenderFlag( bkRenderFlag );
delete wfs;
}
// 这个是添加矢量图层的方式
void qgis_dev::addWFSLayer( const QString& url, const QString& typeName )
{
QgsVectorLayer* vecLayer = new QgsVectorLayer( url, typeName, "WFS", false );
if ( !vecLayer->isValid() )
{
QMessageBox::critical( this, "error", "layer is invalid" );
return;
}
QgsMapLayerRegistry::instance()->addMapLayer( vecLayer );
mapCanvasLayerSet.append( vecLayer );
m_mapCanvas->setExtent( vecLayer->extent() );
m_mapCanvas->setLayerSet( mapCanvasLayerSet );
m_mapCanvas->setVisible( true );
m_mapCanvas->freeze( false );
m_mapCanvas->refresh();
}
WFS 图层添加窗口也是没有默认服务器的,需要自己使用 New 按钮新建服务器。
在 QGis 源码学习过程中,有很多小细节往往是功能的关键,但又不是那么容易发现(如本例中的 QSettings)。由于 QGis 工程的庞大,很多功能的实现隐藏的比较深,需要耐心的挖掘、思考。
希望本文能够帮助到正在困惑的朋友,我的 QGis 二次开发博客系列,也是希望可以帮到更多的朋友,提供一个基础知识的文档,让更多的开发者将 QGis 这个强大的工具用起来,并最终学习到 QGis 这样优秀的开源软件的开发思想,为我们的软件开发服务。
如本文有遗漏的地方,还请指出,有错误的地方,请不吝指正!谢谢!