Tag Archives: tutorial

在ArcGIS中应用脚本实现批处理

Zhuotong Nan(南卓铜) [email protected]

ArcGIS 9.2提供了脚本功能,可以方便地实现批处理功能。可用脚本包括JS, VBS和Python。以下示例利用Python实现批处理ASCII grid文件到ESRI Binary grid转换的功能。

问题

ASCII grid由matlab代码生成,存储于 LDAS.data 目录。ASCII grid内容如下:

ncols         32
nrows         32
xllcorner     -88
yllcorner     37.75
cellsize      0.125
NODATA_value  -9999
0.000000    0.000000 …

各ASCII grids命名有以下规律:

nbwi3.[yyyy].[mm].[dd].[hh]h.txt

其中[yyyy]是4位年份,[mm]是月份,[dd]是2位日期,[hh]是24制的小时。比如,nbwi3.2001.07.08.15h.txt等。

生成的ESRI Binary grid有以下命名规律:n[yy][mm][dd][hh],其中[yy]是2位年份,[mm]是月份,[dd]是2位日期,[hh]是24制的小时。如n01070814。注意,ESRI grid的命名和长度是有一定要求限制的,所以我们必须要选择合适的,并能区别数据的命名方式。

我们需要做的是将数百个ASCII grids转换成对应的ESRI binary grids。

实现

脚本编写环境我们这里采用免费的PyDev/Eclipse。Python脚本存储在与LDAS.data并列的src目录下。事实上,可以使用任何文本编辑软件来用脚本,但Eclipse提供很多编程增强功能,如智能提示等。

import arcgisscripting
import os

datadir=os.path.abspath(os.path.join(os.getcwd(),"../LDAS.data"))
#print datadir
children=os.listdir(datadir)

#only ".txt" files
txtfiles=[]
grdnames=[]
for child in children:
    root,ext=os.path.splitext(child)
    if (ext==’.txt’):
        txtfiles.append(child);
        grdname=’n’+root[8:19]
        grdname=grdname.replace(‘.’,”)
        grdnames.append(grdname)
# grid names now ready       

gp=arcgisscripting.create()
gp.workspace=datadir

# begin iteration
for i in range(len(grdnames)):
    gp.ASCIIToRaster(txtfiles[i], grdnames[i], "FLOAT")
    print txtfiles[i]

首先,需要通过import arcgisscripting,将arcgisscripting导入Python。arcgisscripting事实上是位于C:Program Filesarcgisbin目录下的arcgisscripting.dll。如果在运行时,提示找不到arcgisscripting库,则需要配置Python库路径。

库导入成功后,gp=arcgisscripting.create() 创建script对象,此后通过“gp.命令”的方式调用script支持的命令。一般来讲,可以在arcgis command window里使用的命令,都会有对象的script调用方式。比如,

ASCIIToRaster的命令语法是ASCIIToRaster_conversion <in_ascii_file> <out_raster> {INTEGER | FLOAT},对应的脚本语法是ASCIIToRaster_conversion (in_ascii_file, out_raster, data_type)。我们可以在ArcGIS Desktop帮助文档里找到。

此外,gp还有一些专用的script命令,比如ListRasters、GetMessage等,大家可以在ArcGIS Desktop帮助文档查询Geoprocessor object。这些命令对于进行批处理迭代时十分有用。

在以上代码中,与geoprocessor有关的代码有,

gp=arcgisscripting.create()
gp.workspace=datadir
gp.ASCIIToRaster(txtfiles[i], grdnames[i], "FLOAT")

workspace设置了当前的工作目录,相当于在command window里执行 “workspace 目录”。

ASCIIToRaster执行实质的转换工作。如果此处我们需要执行位于ArcGIS扩展模块的命令,则需要在调用命令前,先对扩展模块(如spatial analysis)进行checkout。使用以下命令:

gp.CheckOutExtension("Spatial")
gp.CellStatistics_sa(…)

否则CellStatistic_sa会抱怨说没有License可用。

代码其余部分是常规Python代码。以下取得指定目录下的txt扩展名的全部文件名,并生成对应的grid文件名。

children=os.listdir(datadir)

#only ".txt" files
txtfiles=[]
grdnames=[]
for child in children:
    root,ext=os.path.splitext(child)
    if (ext==’.txt’):
        txtfiles.append(child);
        grdname=’n’+root[8:19]
        grdname=grdname.replace(‘.’,”)
        grdnames.append(grdname)

Python十分有趣,如果有其它语言基础,学习起来并不难。对我们写ArcGIS脚本,有一般的Python知识就足够了。尽管效率不如传统的arcinfo AML高,但在功能上则不逊色。如果需要实现十分复杂的GIS功能,则建议使用c++, vb或.net语言操作ArcObjects。

Extending ArcGIS Command: A case tutorial (II)

ArcGIS command扩展:ArcGIS 9.2/C# 2的实现
Zhuotong Nan (南卓铜) ([email protected])

(上节介绍了如何使用VS 2005 C# express来帮助我们生成ArcGIS Command扩展的代码框架。)

下面我们来介绍框架代码各部分的具体含义。

/// <summary>
/// Command that works in ArcMap/Map/PageLayout
/// </summary>
[Guid("eb9b3424-41d4-44e9-8ab0-c4740339db95")]
[ClassInterface(ClassInterfaceType.None)]
[ProgId("MyArcGISClassLibrary.BatchedChangeDatasource")]
public sealed class BatchedChangeDatasource : BaseCommand

ArcGIS是基于COM构建的,它只支持同样符合COM规范的动态链接库(Class library)。Guid必须不与任何一个COM组件相同。默认会生成一个新的全局标识符。我们也可以使用ArcGIS菜单里Developer Tools下面的Guid Generator来获取新的标识符(图10)。

image
图10,ArcGIS为我们提供了生成Guid的工具

ClassInterface和ProgId仍然是作为将.net类库导出成COM需要的一些属性。seal关键词指定了本命令不能再被继承。需要注意的是此类从BaseCommand上继承过来,BaseCommand实现了ICommand接口。9.2里BaseCommand放到ADF.BaseClasses命名空间下面,在之前版本是放在Utilities.BaseClasses下面。同时一些BaseCommand有关的辅助命令现在也放到了ADF.CATIDs空间。Utilities命名空间仍然存在,但当使用该空间下的BaseCommand在编译时会有deprecated的警告。

COM Registration Function(s)折叠起来的代码用于实现DLL到COM的自动注册和卸载过程。对于一般的应用,使用其生成代码就足够。

public BatchedChangeDatasource()构造函数里初始化了一些Command必要的信息,比如该Command所在的类别,这是以后在使用命令的时候存放的类别(如图11),同时也连接了一个命令表示的图标(m_bitmap)。这些变量是其基类BaseCommand里继承过来的。

string bitmapResourceName = GetType().Name + ".bmp";

如果不使用默认图标,则需要更改这里的名称。

Overriden Class Methods区域是需要开发者具体完成实现的地方。OnCreate在命令附加到ArcGIS时调用,一般这里传入的Object就是具体的Application,这里将Application对象存储到m_hookHelper.Hook属性里。通过Hook属性可以与ArcGIS Application进行必要的互访问。默认的OnCreate功能十分简单,只是判断了Application是否打开,Application是否有活动的View,如果有则使本命令可用。更多的判断则需要开发者来实现。

OnClick()是用户在点击Command后进行的操作。本案例中进行批量更改DataSource等功能便需要在这里实现。

此外,框架代码还将Project Build Properties设置成 Register for COM interop。对于手工生成的项目,必须记得设置此项。

image
图11,Project Build Properties设置

在具体添加代码前,我们先编译一下,看是否有问题。不幸的是默认的框架编译时会提示缺少引用。我们添加引用后(图13)编译通过。

image
图12,缺少引用错误

image 
图13,增加ESRI.ArcGIS.Display引用

在类最前面添加私有变量声明。

public sealed class BatchedChangeDatasource : BaseCommand
{
    private ESRI.ArcGIS.Framework.IApplication _app;
    private string _layerNameToBeReplaced = "nwbi3";
   

_app接管Hook对象保管Application。_layerNameToBeReplaced指定要被替换Data Source的Layer名。同名的Layer会存在于不同的Data Frame里面。

因为我们需要实现的功能只对ArcMap有用,所以需要判断宿主应用程序是否MxApplication,如否,则禁用本命令。将OnCreate里的下面代码:

if (m_hookHelper == null)
        base.m_enabled = false;
else
        base.m_enabled = true;

更改成:

if (m_hookHelper == null)
    base.m_enabled = false;
else
{
    _app = hook as ESRI.ArcGIS.Framework.IApplication;
    if (_app is ESRI.ArcGIS.ArcMapUI.IMxApplication)
        base.m_enabled = true;
}

现在主要的功能是在OnClick里实现。要完成批量替代Data Source的功能,一般的思路是这样:在当前的Document(一个.Mxd文件即Document)里找到各个Data Frame(在ArcObjects,每个Data Frame就是一个Map),迭代各Map。在每个Map里迭代各Layer,判断此Layer是否具备指定的_layerNameToBeReplaced的名称,如是,将该Layer的Data Source代换成指定的。为了实现时间序列的代换,则还需要先形成目标Dataset Name的List,然后逐一代换。以下代码实现了将一个给定Layer Name的Feature层替换成固定的某个存储在File Geodatabase里的FeatureClass。targetGDBpath指定File Geodatabase的路径,具体的FeatureClass由 fcn.Name指定。如果此FeatureClass是存储在Geodatabase里的某个Feature Dataset里,可由fcn.FeatureDatasetName来指定。最终的Data Source的更换由IDataLayer.Connect来实现。

public override void OnClick()
{
    ESRI.ArcGIS.ArcMapUI.IMxDocument pDoc = (ESRI.ArcGIS.ArcMapUI.IMxDocument)_app.Document;
    if (pDoc == null) return;

    //_layerNameToBeReplaced ="nwbi3";
    string targetGDBpath = "c:tempfileGDB.gdb";

    ESRI.ArcGIS.Carto.IMaps maps = pDoc.Maps;
    ESRI.ArcGIS.Carto.IMap map;
    ESRI.ArcGIS.Carto.ILayer layer;
    ESRI.ArcGIS.Carto.IDataLayer dLayer;
    ESRI.ArcGIS.Geodatabase.FeatureClassNameClass fcn = new ESRI.ArcGIS.Geodatabase.FeatureClassNameClass();

    string conTofGDB = @"DATABASE= " + targetGDBpath;

    ESRI.ArcGIS.Geodatabase.IDataset ds = fGDBWorkspaceFromString(conTofGDB) as ESRI.ArcGIS.Geodatabase.IDataset;

    ESRI.ArcGIS.Geodatabase.IWorkspaceName wsName = ds.FullName as ESRI.ArcGIS.Geodatabase.IWorkspaceName;
    fcn.WorkspaceName = wsName;
    //IDatasetName dsName = ds.Subsets.Next().FullName as IDatasetName;
    //fcn.FeatureDatasetName = dsName;

    fcn.Name = "NWBI3";

    for (int i = 0; i < maps.Count; i++)
    {
        //each data frame
        map = maps.get_Item(i);
        for (int j = 0; j < map.LayerCount; j++)
        {
            layer = map.get_Layer(j);
            if (layer.Name == _layerNameToBeReplaced)
            {
                dLayer = layer as ESRI.ArcGIS.Carto.IDataLayer;
                if (dLayer != null)
                {

                    bool rev = dLayer.Connect(fcn);
                }
            }

        }
    }

    pDoc.ActiveView.Refresh();
}

COM风格的编程经常会看到很多的接口,不同的接口间不停的转换。其实接口一般是按功能进行分类,一个类通常实现了多个接口。比如FeatureLayer会实现不同的例如IDataLayer, IFeatureLayer, IDataset等,IDataLayer负责与数据源有关的操作,IFeatureLayer负责跟具备FeatureClass有关的操作,而IDataset实现了FeatureLayer作为Dataset有关的一些操作。

对Interface的熟悉应用建立在对相关类熟知的基础上。.net help十分全面,查找某接口,可以回溯到应用该接口的全面Class,而每个Class也会给出其实现的各个接口。通过这种方法,我们可以在一定程度上知道哪些接口是可以相互转换的。比如,IMap.get_Layer()查找帮助我们知道是返回ILayer,再结合我们期望是找到一个FeatureLayer,所以寻找FeatureLayerClass的实现的接口,发现其与数据源有关的操作封装在IDataLayer和IDataLayer2里。于是我们将layer cast成IDataLayer,再进行Connect操作。

以上代码中,fGDBWorkspaceFromString方法实现如下,作用是通过构建连接字符串来生成IWorkspace。

public ESRI.ArcGIS.Geodatabase.IWorkspace fGDBWorkspaceFromString(String connectionString)
       {
           ESRI.ArcGIS.Geodatabase.IWorkspaceFactory2 workspaceFactory;
           workspaceFactory = (ESRI.ArcGIS.Geodatabase.IWorkspaceFactory2)new ESRI.ArcGIS.DataSourcesGDB.FileGDBWorkspaceFactoryClass();
           return workspaceFactory.OpenFromString(connectionString, 0);
       }

至此,我们完成了一个很简单的可运行的Command扩展。回到构造函数,将m_category赋为“tong’s Extensions”。编译后,打开ArcMap,打开tools菜单下的Customize…,弹出窗口如图14。左边的Categories可以找到tong’s Extensions,右边的Commands可以看到我们的BatchedChangeDatasource。选定该命令,可以拖拉到任何工具栏。Close此Customize窗口后,命令就可以如内置一样运行。

 image
图14,ArcMap的定制窗口

剩余的问题

对前面设定的期望目标,我们可以通过以下方法形成一个时间序列Dataset的名称。

private List<string> TargetRasterNames()
{
    //DateTime start = new DateTime(1997, 1, 27, 0, 0, 0);
    DateTime start = new DateTime(2002, 8, 19, 0, 0, 0);
    //DateTime end = new DateTime(1997, 1, 29, 0, 0, 0);
    DateTime end = new DateTime(2002, 8, 20, 0, 0, 0);
    List<string> rev = new List<string>();
    for (DateTime dt = start; dt < end; dt = dt.AddHours(1) )
    {
        //string dtStr = string.Format("c{0:MMddHH}c_3hm", dt);
        string dtStr = string.Format("{0:yyyyMMddHH}.asc", dt);
        rev.Add(dtStr);
    }
    return rev;

}

在Connect的时候,根据次序可以赋给不同的RasterName。在更换数据源时,如果确保都在同一Workspace下,则可以通过下面代码简单代换。

if (layer.Name==_layerNameToBeReplaced)
{
          dLayer= layer as IDataLayer;
          if (dLayer!=null )
          {
                 IRasterDatasetName pName = dLayer.DataSourceName as IRasterDatasetName;
                 IDatasetName dsName = pName as IDatasetName;
                 ii++;
                 dsName.Name = targets[ii];
            }
}

如果不在同一Workspace,IName还需要指定IWorkspaceName。

结论

本文结合实际工作的一个案例,演示了如何应用Visual Studio 2005 C# Express来开发扩展ArcGIS Command。以上演示还有很多需要改进的地方,比如要代换的Layer名,不能内置在代码里,我们可以通过生成一个Dialog让使用者来决定替换哪一层,再比如,我们可以设置更漂亮的图标以区别于已有的图标。但这些都已经不是本tutorial的内容。本tutorial的目的是希望大家通过阅读本文可以初步掌握如何使用C#进行ArcGIS扩展,大家更多需要关注的是OnClick里的Business logic。

有什么问题有与我联系或留言。转载请保留帖子全部信息。

Extending ArcGIS command: a case tutorial

ArcGIS command扩展:ArcGIS 9.2/C# 2的实现
Zhuotong Nan (南卓铜) ([email protected])

为什么要做扩展?

很简单,ArcGIS提供了通用的功能,但不可能提供每个大家需要的功能。而且,对一些专业(如水文)也许最实用的功能都没有。这时候便需要对现有的ArcGIS进行扩展。

如何扩展?

有很多方法了,比如AML来扩展ArcInfo Workstation——我以前写过一个AML的简要指南,Google应该能找到。在Desktop下,可以用VBA扩展,或者用任何支持COM的现代语言来扩展。这里用Visual Studio C# 2005 Express来开发。对于独立应用程序,应当购买ArcEngine。ArcEngine可以认为是ArcObjects的一个子集。

准备工作

需要ArcGIS desktop .net developer kit。如果最初安装ArcGIS 9.2的时候没有安装上,那么到安装/删除程序里去重新修改安装。注意事先机器上得已经存在.net framework SDK,如果没有,安装ArcGIS看不到.net developer kit选项。.net SDK不同于平时大家安装的redistribution .net framework runtime。请到微软官方网站下载。

安装Visual Studio C# 2005 Express,建议在安装ArcGIS desktop .net developer kit之前进行,ArcGIS desktop .net developer kit将在Visual Studio C# 2005 Express上生成一些项目模板(如ArcMap class library),应用这些模板进行开发会节省很多时间。

我们的工作

如图1所示,要在一个Document里生成多个Map(多个Data Framework),Map里的内容是时间序列的小时降水量(不同的Raster Dataset)。右图仅演示了1天24小时的图。我们的挑战是要做10天,而且必须各个图采用同样的图例。

 
图1, 小时降水量图,我们希望实现的目标

解决方法

先设计好一个Data Frame,设置好Legend(不能是sketch,否则系列图就没有意义)。然后复制24份Data Frame,应用ArcMap的页面排版功能进行排版。然后将此文档复制10份(10天)。现在的问题是如何去更改每个Data Frame里的Data Source。我们需要的是实现一个批量处理的方法。采用ArcObjects进行二次开发,生成一个Command来完成这个事情,是个不错的方案。

建立一个新项目,点击File>New Project…(图2)。因为我们只想进行基于ArcMap的扩展,这里选择ArcMap Class Library(图3),敲入项目名称“MyArcGISClassLibrary”(不包括引号)。一般而言,项目按功能进行归类,一个可以实现很多的Command或者Tools扩展。所以没有必要对每个Command或者Tool都建项目。

image
图2,新建项目

 image
图3,新建ArcMap Class Library

在随后出现的ArcGIS Project Wizard(图4),添加必要的引用。这里选择ESRI.ArcGIS.ArcMap,ESRI.ArcGIS.ArcMapUI,ESRI.ArcGIS.Carto,ESRI.ArcGIS.DataSourceGDB,ESRI.ArcGIS.DataSourceRaster,ESRI.ArcGIS.Framework,ESRI.ArcGIS.Geodatabase,ESRI.ArcGIS.System。这些都是常用的库,添加的这些库,会出现在VS项目的Reference里。如果这里不添加,也可以以后根据需要再行添加(如图4.1)。需要注意的是,这里用的ESRI.ArcGIS.DataSourceGDB是9.2新出来的File Geodatabase,据称比Personal Geodatabase性能要好,而且有更大的存储许可。点击Finish结束。

image 
图4,ArcGIS Project Wizard

image 
图4.1 可以从Solution的这个位置添加引用

该向导帮助我们添加了必要的Reference,并新建了Class1.cs。但我们不需要它。如图5所示,删除。

 image
图5,删除Class1.cs

移动Solution Explorer窗口的项目上,如图6,添加一个新项,点击New Item…。在新弹出的窗口中选择Base Command(图7)。并设置名称为"BatchedChangeDatasource.cs"。在New Item Wizard Options里选择ArcMap Command(如图8)。

image 
图6,新建Command类

image 
图7,选择Base Command

image 
图8,选择ArcMap Command

点击OK,Wizard生成足够多的东西(如图9),我们在此基础上再写一些代码以完成我们的预期功能。

image 
图9,Wizard生成的代码框架按Ctrl+S保存此Solution。Solution名称设成MyArcGISClassLibrary.sln。

(下一节我们将介绍代码的作用,和如何添加自己的功能以实现既定目标。)