MapObjects开发技术 (VC++)
MapObjects是一组基于COMTRANBBS技术的地图应用组件,它由一个称为Map的ActiveX控件(OCX)和约45个自动化对象组成,在标准的Windows编程环境下,能够与其他图形、多媒体、数据库开发技术组成完全独立的综合性应用软件,是基于前端应用业务的良好的地图开发环境。
MapObjects是全球最大的GIS软件供应商ESRI公司在业界最早推出的GIS软件组件,它起点高、功能强、结构优雅。ESRI在推出其每一个版本时,都采取了非常严谨慎重的态度。从1.0版本算起,MapObjects正式问世已经5年,才发展到2.1版本,可见每个版本质量控制之严格。事实上,MapObjects是全球范围内使用最广的GIS组件,也是潜在错误被最充分暴露并得以纠正的软件组件,其稳定可靠性无以置疑。有经验的程序员都知道,在软件开发过程中,稳定性压倒一切,而这正是MapObjects能被成功应用的重要前提和保证。
作为ESRI公司GIS软件族的重要成员,凭借ESRI公司在GIS领域的领先技术和市场地位,MapObjects操作的数据资源与ESRI的旗舰产品ArcGIS完全兼容,从结构简练的桌面数据格式(Shape文件)到以拓扑关系为基础的经典ArcInfo Coverage,以至基于数据库(DBMS)和ArcSDE基础之上的Geodatabase,都能够被MapObjects读取。除了矢量数据以外,MapObjects还能够读取多种格式的栅格数据,如BMP、TIF、JPEG、ArcInfo Grid、Erdas Image等。MapObjects本身也能够生成Shape文件格式的GIS数据,该格式已经成为GIS业界事实上的基于桌面应用的标准。采用MapObjects,能够最大限度地与主流GIS技术融合,保护用户在数据生产、功能开发、以及人员培训上所作的一切投资。
1、 MapObjects的体系结构
MapObjects可以说得上是最优雅简洁的GIS软件组件。我们这么说,是因为它以最少的接口提供了常用的GIS功能,甚至GPS的动态特性,同时做到了结构合理,简单明了,容易理解和扩展。与之相比,其他的GIS组件,要不是由于提供的功能过多而破坏了其内在的结构美并影响了用户扩展的灵活性,就是结构过于简单而使功能大大弱化。可以说,MapObjects在功能和结构两者之间,取得了完美平衡,体现出了软件的艺术性,给人带来愉悦的感受。
1.1 数据结构
从MapObjects对地图数据的组织方式来看,概念清晰,易于理解。它认为一个综合性的地图由多个图层构成,图层数据来源广泛,既可以是GIS矢量图层,也可以是CAD图层,甚至影像数据。对于GIS和CAD的矢量图层,其内部统一用记录集(Recordset)来表达,这样就抹平了各种不同格式数据之间在内存中的表达鸿沟,简化了程序员的数据观点。记录集正象关系数据库中的二维表,行表示每个要素、列表示每个属性。如果含有名称为 “Shape”的列,则该记录集表示的是地图要素,否则表示的是普通的数据库表格。这样,在数据结构内部就消除了GIS和MIS数据之间的差异,使程序员开发GMIS综合应用系统易如反掌。
Shape字段以面向对象的技术封装了要素的图形部分。它把图形划分为三种类型,即点、线、面。这些几何类型既简单又复杂,说它简单,是因为很容易理解,所有的图形归根到底都是由点、线、面构成的;说它复杂,是因为这些点、线、面并不是简单的点、线、面,实际上它引入了一个部件(Part)的概念,由点构成部件,部件由复合点构成、线和面则由部件构成。
如果只有一个部件,则它是简单的线或面,如果有两个以上的部件,则构成复合线或面。例如夏威夷群岛,由很多小岛屿构成,但它是一个整体,其中的每个小岛都可以用Part来表示,多个Part构成的多边形放到一个Shape字段中。
至于注记,则被描述为属性字段的自动标注。标注的位置由Shape字段中的图形要素决定,它可以是点、线或面,通常是线。标注的内容由普通属性表示,既可以是字符型,也可以是整型或浮点型,通常是字符型。这样,一切要素既可以按图形表示,也可以按其属性内容标注显示,甚至可以两者同时显示,地图显示和制图的方式是极为灵活的。
1.2 功能接口
再从MapObjects所提供的功能接口来看,常用的地图应用基本上都能实现,例如:
* 创建新的Shape文件
* 更新属性或图形数据
* 绘制点、线、椭圆、矩形和多边形等图形要素
* 绘制描述性的文本注记
* 地图简单点取查询、空间查询、相对位置查询、SQL逻辑条件查询等
* 空间统计
* 地图缩放和漫游
* 丰富的绘图方式,如按值润色、分类显示、绘制密度图、产生含各类图表的专题图等。
* 属性自动标注
* 显示航空和卫星遥感影像
* 动态显示实时或顺时数据,如GPS动态监测
* 地址匹配
* 投影变换
* Buffer、Union、Intersect等空间分析算子
通过调用这些接口,能够开发从简单的电子地图应用,至复杂的基于GIS/GPS/RS的3S应用。利用空间分析算子,甚至可以构造出具有一定复杂度的空间分析模型。由于提供了数据更新接口,用户能够扩展出自己的编辑工具,富融公司便基于MapObjects 2.0/2.0a/2.1扩展了近50种的编辑工具,使之能够满足更为广泛的应用要求。
2、 MapObjects的开发过程
MapObjects的使用和开发过程与其他的ActiveX控件没有两样,在Visual Basic、Delphi、Visual C++等能够支持控件开发的编程环境下,一旦把控件插入到编程项目中,就可以通过接口使用控件所提供的各种方法。
由于MapObjects联机帮助和随机手册中的大部分编程说明都采用Visual Basic作为代码示例,而Visual C++的说明相对较少,为了方便Visual C++程序员的工作,以下的开发过程和代码示例使用Visual C++,在其他环境下的编程过程大致类似。
以下说明均假设在Windows下已成功地安装了MapObjects控件。我们将构造一个简单的电子地图应用程序,它可以增加一个新图层,实现地图的放大、缩小,并且可以实现要素的定位功能。
2.1 生成Visual C++编程项目
使用Visual C++“File | New”打开新建工程对话框,使用MFC AppWizard(exe)产生一个新的MFC应用程序,我们把工程名称取为“mmap”,该向导一共有6步,前5步都按缺省选项,第6步把Basic Class由CView改为CFormView,这样完成后产生的应用程序将有一个表单模板,其标识号为IDD_MMAP_FROM,初始状态是一行静态文本,内容为“TODO:在这个对话框里设置表格控制”(如果是英文版或选择的语言为英文,则提示换成英文)。把这行字删掉,以便在此表单中加入地图控件。
2.2 加入MapObjects地图控件
使用“Project | Add To Project | Components and Controls...”菜单功能打开“Components and Controls Gallery”对话框,从其“Registered ActiveX Controls”文件夹下的已登记控件列表中找到“MapObjects 2.1 Map Control”并按“Insert”按钮,系统将生成该控件及其自动化对象的一系列包裹类(Wrapper Class),把第一个类名称由CMap1改为CMoMap,实现文件保持不变,即map.h和map.cpp。结束该对话框后,在控制条中会增加一个地图控件图标,把该图标插入到IDD_MMAP_FORM表单中。然后选中刚插入的控件图标,在用左手按住键盘的“Ctrl”键的情况下,右手双击鼠标左键,将弹出一个“Add Member Variable”对话框,在成员变量名称栏输入“m_map”,下面两栏保持不变,即Category为Control,Variable Type为CMoMap。做完这些工作后,回到ClassView中观察CMmapView类,将发现增加了一个类型为CMoMap的m_map对象,利用它就可以操作地图了。
2.3 使用Map对象增加图层
这时编译程序,应该不会出错,但在运行时,发现除了在表单视图中增加了一个空白的代表地图控件的小图标外,应用程序与刚生成时的情况并没有什么太大的区别。为了增加图层数据,首先要把地图控件的尺寸放大到与表单视图一样大,然后把图层数据加入到地图对象中。
第一步、找到CMmapView的OnInitialUpdate()方法,作如下操作:
(1)如果原来有“ResizeParentToFit();”语句,注释掉或删除它;
(2)在返回语句之前增加如下语句:
// 将显示尺寸调整到整个客户区
CRect client;
GetClientRect(&client);
int cx=client.Width();
int cy=client.Height();
m_map.SetWindowPos(0, 0, 0, cx, cy, SWP_NOZORDER);
第二步、找到CChildFrame,重载其OnClientCreate()方法,该方法将在创建表单客户区时被调用,在其返回语句之前增加语句:“MDIMaximine();”。
完成上面两步后,再次编译程序并运行,在原来表单客户区应该出现一个空白的地图,接下来往这个空白图上增加图层。为了简单起见,我们假设图层数据放在C:\data目录下,图层格式为Shape文件,其文件名为test.shp。
第三步、在CMmapView中创建一个增加shape文件的方法AddShpLayer(),其实现为:
增加SHP图层,返回图层内部名称,为空表示不成功。
CString CMmapView::AddShpLayer(const CString & path, COLORREF color, short symbolSize, short symbolStyle)
{
CMoDataConnection conn;
if (!conn.CreateDispatch(TEXT("MapObjects2.DataConnection"))) return "";
conn.SetDatabase(GetFileDirectory(path));
if (!conn.Connect()) return "";
// Add layer specified by path
CMoLayers layers=m_map.GetLayers();
CMoMapLayer layer;
if (!layer.CreateDispatch(TEXT("MapObjects2.MapLayer"))) return "";
CString LayerName = GetFileTitle(path);
CMoGeoDataset geoDataset=conn.FindGeoDataset(LayerName);
if(!geoDataset) return "";
layer.SetGeoDataset(geoDataset);
CMoSymbol layerSymbol(layer.GetSymbol());
if (color != -1) layerSymbol.SetColor(color); // Set color if specified
layerSymbol.SetSize(symbolSize);
layerSymbol.SetStyle(symbolStyle);
layers.Add(layer);
return(layer.GetName());
}
为了使这段代码能够被顺利编译,还要在mmapview.cpp的文件开始处增加如下include语句:
#include "modataconnection.h"
#include "molayers.h"
#include "momaplayer.h"
#include "mogeodataset.h"
#include "mosymbol.h"
另外,在CMmapView中增加几个辅助函数,用于分析图层文件路径中的文件名、目录名:
CString CMmapView::GetFileDirectory(const CString& path)
{
int pos = path.ReverseFind('\\');
if (pos >= 0) return path.Left(pos);
return "";
}
CString CMmapView::GetFileTitle(const CString& path)
{
CString strResult = GetFileName(path);
int pos = strResult.ReverseFind('.');
if (pos >= 0) return strResult.Left(pos);
return strResult;
}
CString CMmapView::GetFileName(const CString& path)
{
int pos = path.ReverseFind('\\');
if (pos >= 0) return path.Right(path.GetLength() - pos - 1);
return path;
}
第四步,回到CMmapView的OnInitialUpdate(),在其返回之前加上如下语句:
AddShpLayer("c:\\data\\test.shp", RGB(125,125,125), 0, 0);
再次编译后运行(运行前确保C:\data\test.shp图层文件存在,即至少包括c:\data\test.dbf、c:\data\test.shp、c:\data\test.shx三个文件),应该能够观察到test图层被加入到了地图中。
2.4 使用map对象操作地图
接下来我们完成对地图的放大缩小操作,当点击鼠标左键时,地图放大1倍,当点击鼠标左键时,地图回到全图显示。
第一步,使用类向导(Class Wizzard)在CMmapView中增加一个地图消息响应函数。即选中CMmapView中的IDC_MAP1,在Message列表框中双击MouseDown,将生成一个
OnMouseDownMap1()消息函数。
第二步,在OnMouseDownMap1()中加入如下语句:
if(Button==1)
{
CMoRectangle rect=m_map.GetExtent();
rect.ScaleRectangle(0.5);
m_map.SetExtent(rect);
}
else if(Button==2)
{
CMoRectangle rect=m_map.GetFullExtent();
m_map.SetExtent(rect);
}
为了使用CMoRectangle,还需要在mmapview.cpp的开始部分加一个include语句,即:
#include "morectangle.h"
按下鼠标左键时,在消息响应函数中的Button参数记录按下的是哪个键,1表示左键,2表示右键。编译后运行,分别点击鼠标左右键,应该观察到地图放大和缩回到全图的效果。
2.5 使用Recordset对象检索数据
接下来我们想找到图层中的第一个要素,即其FeatureId为1(在MapObjects要素图层中,FeatureId是其固有的字段,用于记录每个要素在图层中的序号),找到这个要素后,把它放在视图窗口的中央显示,这就类似于一个条件定位的功能。
第一步,在IDR_MMAPTYPE菜单中增加一个“定位”菜单项至“查看”菜单下,设其ID号为ID_FEATURE_LOCATE。
第二步,使用类向导(Class Wizzard)产生该菜单项在CMmapView中的消息响应函数,即与COMMAND和UPDATA_COMMAND_UI两个消息对应的OnFeatureLocate()和OnUpdateFeatureLocate(),在OnUpdateFeatureLocate()的实现中增加下行语句,使该菜单总是处于激活状态:
pCmdUI->Enable();
第三步,在OnFeatureLocate()中增加如下语句:
void CMmapView::OnFeatureLocate()
{
CMoLayers layers=m_map.GetLayers();
CMoMapLayer layer=layers.Item(COleVariant(TEXT("test")));
if(layer)
{
CMoRecordset recs=layer.SearchExpression(_T("FeatureId = 1"));
recs.MoveFirst();
if(!recs.GetEof())
{
CMoFields fields=recs.GetFields();
CMoField shapeField=fields.Item(COleVariant(TEXT("Shape")));
if(shapeField)
{
switch(shapeField.GetType())
{
case 21:
{
CMoPoint point=shapeField.GetValue().pdispVal;
if(point) m_map.CenterAt(point.GetX(), point.GetY());
}
break;
case 22:
{
CMoLine line=shapeField.GetValue().pdispVal;
if(line)
{
CMoRectangle rect=line.GetExtent();
rect.ScaleRectangle(1.5);
m_map.SetExtent(rect);
}
}
break;
case 23:
{
CMoPolygon polygon=shapeField.GetValue().pdispVal;
if(polygon)
{
CMoRectangle rect=polygon.GetExtent();
rect.ScaleRectangle(1.5);
m_map.SetExtent(rect);
}
}
break;
}
}
}
}
}
在这里使用到了记录集、字段、点、线、面等对象,因此在文件头部还要增加如下include文件:
#include "morecordset.h"
#include "mofields.h"
#include "mofield.h"
#include "mopoint.h"
#include "moline.h"
#include "mopolygon.h"
在switch/case语句中的21表示要素的几何类型为点,22表示线,23表示面。编译后运行,并选择“定位”菜单,程序将找到test图层中的第一个要素,并把它放在窗口的中央显示出来。如果加入的图层数据是点层,我们建议把其symbolSize设为10,以便观察到点位。即把OnInitialUpdate()中的AddShpLayer方法调用换作:
AddShpLayer("c:\\data\test.shp", RGB(255,0,0), 10, 0);
从这个例子中我们看到了如何通过图层(Layer)执行一个SQL查询语句,获得记录集后如何对其进行检索,并提取出具体字段内容。对于图形图层,“Shape”字段也是固定存在的,其中存放了该要素的图形几何部分,通过使用字段的GetType()方法可以获得该图层是点层、线层或面层,并作出相应的定位处理。字段的GetValue()方法返回的是一个VARIANT类型值,其中封装了各种各样的数据类型,在Shape字段中,它封装的是一个图形要素,可以通过pdispVal取得它的真正内容,并根据图层类型转换为相应的图形要素,作为计算地图显示范围的依据。