ArcGIS Server 服务对象扩展(SOE)开发解析

SOE应用场景

在ArcGIS 10.1中ArcGIS Server不再支持DCOM方式的连接,也就意味着用户不能通过本地方式的连接使用ArcObjects提供的更多功能,不少开发者抱怨目前版本的API并不能完全满足自己的需要,这时越来越多的用户使用服务对象扩展(Server Object Extension,简称SOE)来实现对现有服务能力的扩展。

GP服务和SOE的区别

也许会有读者认为也可以使用GP服务来实现对ArcGIS Server服务的扩展,接下来就对比一下GP服务与SOE各自的缺点,也为广大读者在开发过程中选择哪种服务扩展会更加方便和实用地完善用户的功能需求提供参考。

 用户可以使用模型构建器(Model Builder)来创建、编辑和管理模型,这些模型将一系列地理处理工具(ArcToolbox)串联在一起来完成一个GIS的工作流程,用户无须编程即可实现一个复杂的地理处理任务需求。如果现有的工具并不能完全满足用户的需求,用户也可以通过Python脚本来实现扩展,通过自定义GP工具调用ArcObjects丰富的功能。这时即可将创建好的GP工具发布为GP服务来实现ArcGIS Server服务能力的扩展,而且GP服务支持异步模式来完成极为复杂的任务需求。

SOE比GP服务稍微难一点,需要对WebAPI非常了解。目前只支持同步调用。

SOE开发与部署

新生成的REST模板已经包含相关的SOE类和函数,用户无须编写任何代码即可生成一个SOE文件,不过在生成该文件之前,用户需要选择SOE所要扩展的服务类型,模板代码默认为空,SOE目前只支持地图服务和影像服务,用户需要输入所要支持的服务类型才可以进行SOE文件的生成,如图5.2所示,本例选择地图服务。

SOE部署

ArcGIS Server添加SOE出现token异常:https://blog.csdn.net/weixin_30817749/article/details/95883694   https://blog.csdn.net/qq_33777040/article/details/84587389

SOE开发调试方法

为了更好地深入理解SOE的工作原理,我们需要对SOE的生命周期及相关函数进行学习。如果我们掌握了SOE的调试方法,就可以根据代码的断点跟踪来详细了解SOE的生命周期进而更好地学习SOE。

如果是Java SOE,用户只需要设置【站点】->【GIS服务器】->【扩展】->【调试设置】即可;本例使用的.NET SOE,用户只需要在默认生成的SOE模板的构造函数中添加Debugger.Lauch()启动调试器并将其连接到进程即可。

以应急行业为例扩展SOE功能介绍

假设某一个学校发生了自来水爆管,需要搜索附近500米的受影响街区的数据。当然这个小功能当然可以通过Web API实现,但是这里不会介绍太难的地理算法,仅是以此来介绍SOE的原理、方法以及Web API的调用扩展。否则会分散学习者的注意力,如果把大量篇幅放在实现某个Web API实现不了的功能(如克里金插值)。

SOE工作原理

前面已经介绍了在.NET环境下调试SOE程序的方法,下面我们就根据这个方法来跟踪系统自动生成SOE的REST模板(RestSOE),学习SOE的原理,然后结合应急行业的功能需求,来实现扩展地图服务的爆管事件的影响分析功能。

 通过该构造函数获得相关的SOE名称,启动Server日志搜集对象,以及获得REST请求的处理对象(IRESTRequestHandler),该对象可以获得资源在实例级别的描述。

接下来,当Server启动的时候,会调用SOE初始化方法(Init),并将IServerObjectHelper对象传入,通过IServerObjectHelper.ServerObject获得服务器对象。如果扩展的是地图服务即为地图服务对象,如果扩展的是影像服务即为影像服务对象。与初始化相对应的有一个关闭方法(Shutdown),也就是在服务停止时调用该方法释放所有SOE使用的资源。

接下来会调用一个SOE对象构造方法,该方法在初始化后执行,如果SOE有配置的属性,就可以通过该方法参数得到。该方法只调用一次,这也是为什么SOE比GP服务性能高的原因之一。用户可以将SOE中比较消耗资源的逻辑写在该方法中,例如获得操作地图图层对象的代码等。

另外还有一个接口是可选的,如果客户端有频繁发起请求和结束请求服务器上下文的操作,则可以实现IObjectActive接口。其中包含两个方法Activate和Deactivate。当客户端调用CreateServerContext()时activate()方法被调用,当客户端释放服务器上下文对象时deactivate()方法被调用。

关于Schema的理解

在SOE类的构造函数中,获得相关IRESTRequestHandler时调用了一个CreateRestSchema()函数,该函数用于构建REST的资源(Resource)与操作(Operations)的层次结构。

资源主要是从服务器端返回的信息,比如图层列表、缓存服务的比例尺信息、服务描述信息、符号信息等。例如,本例REST模板中就是添加了一个hello:world的资源描述信息。

在本例中,用户在资源函数中添加对该扩展功能的描述即可。实现的REST页面如图5.7所示。

一个资源可以包括多个子资源,每个资源也可以对应一个或者多个操作,用户可以不对资源做太多的关注,但是一定要注意操作,操作就是使用服务器提供的资源来实现某种功能,它才是SOE功能的核心。

例如,本例以学校的名称字段为查询字段,以街坊为查询图层,获得某个属性条件下学校的点要素对象,然后再以该要素获得缓冲区对象与街坊图层,进行叠加分析后获得影响结果。那么只需要为该功能暴露出查询字段名称属性以及缓冲区半径即可,用户可以自定义不同的属性来获得不同的功能结果。从该示例可以看出,用户只需要完成DoQueryBuffer()函数的功能即可,而且这个函数的编写就是使用了ArcObjects。

 单击支持的功能QueryBuffer按钮,如图5.8所示。

SOE的属性(Property)设置

在应急场景的功能扩展需求下,用户需要添加一些对客户端进行控制的属性参数。通过这些参数不同的SOE,管理员只需要设置不同的值就可以实现相关的扩展功能。例如本例以学校的名称字段为查询字段,以街坊为查询图层,获得某个属性条件下学校的点要素对象,然后再以该要素来获得缓冲区对象,再与街坊图层进行叠加分析来获得影响结果。假如另外一个需求是某个医院出现了问题,当设置完属性参数后,其他用户只需要设置医院的图层名称、查询字段就可以完成同样功能的操作。

如果用户不希望添加通用的属性设置,只是将该属性写在SOE代码中,则设置REST模板的配置即可。

[ServerObjectExtension("MapServer",
  AllCapabilities="",
  DefaultCapabilities="",
  Description="Query Buffer SOE",
  DisplayName="RestSOE1",
  Properties="QueryFieldName=name;QueryLayerName=学校;DesLayerName=街坊",
  SupportsREST=true,
  SupportsSOAP=false)]

如果用户希望添加通用的属性设置,就需要用户像添加资源一样,编辑相关的属性添加代码,然后添加到Schema中即可。

[https://www.cnblogs.com/2008nmj/p/14505720.html]

        private byte[] PropertiesResHandler(NameValueCollection boundVariables, string outputFormat, string requestProperties, out string responseProperties)
        {
            responseProperties = "{"Content-Type":"application/json"}";
            JsonObject result = new JsonObject();
            result.AddString("QueryFieldName",this.m_QueryFieldName);
            result.AddString("QueryLayerName", this.m_QueryLayerName);
            result.AddString("DesLayerName", this.m_DesLayerName);
            return Encoding.UTF8.GetBytes(result.ToJson());
        }
            //在Schema中添加属性
            RestResource propertiesResource = new RestResource("properties", false, PropertiesResHandler);
            rootRes.resources.Add(propertiesResource);

获取数据源

前面提到了,用户可以在SOE对象构造方法中实现比较复杂的逻辑,以达到一次构造,随时调用的目的,那么获取ServerObject的数据源对象,就可以在该方法中实现。在SOE中,用户并不可能像ArcObjects编程一样获得工作空间,进而获得要素类对象,用户需要将前面获得IServerObjectHelper.ServerObjects服务对象转换为IMapServer3对象,通过该对象的GetServerInfo方法获得服务器对象信息,进而获得地图图层信息(MapLayerInfos),然后通过循环来获得所需图层的ID信息。

接下来通过IMapServer3对象QI到地图服务数据访问对象IMapServerDataAccess,通过该对象提供的GetDataSource方法,传入查询图层的ID信息,进而获得相关图层的IFeatureClass对象,看到这个对象以后ArcObjects的开发者就会“拨云见日”了,因为对他们来说该对象再熟悉不过了,剩下的就是对这些图层进行属性查询、缓冲区分析、空间查询的代码实现,这些代码就不在书中进行赘述了。

        public void Construct(IPropertySet props)//复杂操作放在构造函数里只执行一次
        {
            configProps = props;
            //从属性资源中获得查询字段的名称,也就是学校点图层的名称字段(Name)
            if(props.GetProperty("QueryFieldName")!=null)
            {
                this.m_QueryFieldName = props.GetProperty("QueryFieldName") as string;
            }
            //从属性资源中获得查询图层名称,因为测试数据设置了中文别名,所以使用中文(学校)
            if(props.GetProperty("QueryLayerName")!=null)
            {
                this.m_QueryLayerName = props.GetProperty("QueryLayerName") as string;
            }
            //从属性资源中获得分析图层别称(街坊)
            if (props.GetProperty("DesLayerName") != null)
            {
                this.m_DesLayerName = props.GetProperty("DesLayerName") as string;
            }
            try 
            { 
                //获得数据
                IMapServer3 mapServer = (IMapServer3)serverObjectHelper.ServerObject;
                IMapLayerInfo layerInfo;
                IMapLayerInfos layerInfos = mapServer.GetServerInfo(mapServer.DefaultMapName).MapLayerInfos;
                //获取查询图层id
                int layercount = layerInfos.Count;
                int querylayerIndex = 0;
                int deslayerIndex = 0;
                string sName = "";
                for (int i = 0; i < layercount; i++)
                {
                    layerInfo = layerInfos.get_Element(i);
                    sName = layerInfo.Name;
                    if(sName == this.m_QueryLayerName)
                    {
                        querylayerIndex = i;
                    }
                    if(sName == this.m_DesLayerName)
                    {
                        deslayerIndex = i;
                    }
                }
                IMapServerDataAccess dataAccess = (IMapServerDataAccess)mapServer;
                //获得相关的IFeatureClass对象
                this.m_QueryFeatureClass = (IFeatureClass)dataAccess.GetDataSource(mapServer.DefaultMapName, querylayerIndex);
                this.m_DesFeatureClass = (IFeatureClass)dataAccess.GetDataSource(mapServer.DefaultMapName, deslayerIndex);
            }catch(Exception e){
            }
        }

打开SOE资源链接

当SOE已经成功添加到地图服务中,单击SOE扩展,可以查看相关的资源链接信息,用户只需要打开这个URL就可以看到该SOE相关的Schema,包括描述资源、属性、功能操作等信息。

        byte[] IRESTRequestHandler.HandleRESTRequest(string Capabilities,
            string resourceName,
            string operationName,
            string operationInput,
            string outputFormat,
            string requestProperties,
            out string responseProperties)
        {
            return _reqHandler.HandleRESTRequest(Capabilities, resourceName, operationName, operationInput, outputFormat, requestProperties, out responseProperties);
        }

其中参数分别为,Capabilities为被资源授权的操作,resourceName为资源名称,operationName为操作名称,operationInput为操作的输入参数值,outputFormat为客户端请求输出格式,requestProperties为请求的属性信息,responseProperties为响应的属性信息。

关于SOE的处理流程

下图为一个标准的SOE请求流程,客户端会将用户的请求以JSON的形式发送给服务器端,例如本示例的属性条件和缓冲区半径大小,那么有时候也会传入一个带有空间对象描述的JSON对象,服务器会将这些JSON对象进行反序列化为SOE业务处理所需要的格式。SOE的功能实现就是ArcObjects,将JSON反序列化为ArcObjects所识别的IGeometry、IPoint、double等对象,如图5.12所示。

常用函数为ToString()、TryGetArray、TryGetAsBoolean、TryGetAsDate、TryGetAsDouble、TryGetAsLong、TryGetJsonObject、TryGetObject、TryGetString

然后,就是ArcObjects开发的范畴了。。

最后需要将ArcObjects生成的对象再序列化为Json对象,通过HTTP协议传送给客户端。

用户可以通过SOE提供的Conversion类获得ArcObjects对象转换为JSON对象的函数,如图5.15所示。

有Equals()、ReferenceEquals、ToGeometry、ToJson、ToJsonObject、ToSpatialReference。

执行SOE实现的功能

了解了SOE的处理流程,我们通过调试REST模板调试一下执行SOE核心功能的具体代码。首先进入模板默认的sampleOperation操作中,输入暴露给用户两个参数值即可。

private byte[] DoQueryBuffer(NameValueCollection boundVariables,
                                                  JsonObject operationInput,
                                                      string outputFormat,
                                                      string requestProperties,
                                                  out string responseProperties)//
        {
            responseProperties = null;

            //反序列属性名称
            string WhereClause;
            bool found = operationInput.TryGetString("WhereClause", out WhereClause);
            if (!found || string.IsNullOrEmpty(WhereClause))
                throw new ArgumentNullException("WhereClause");
            //反序列查询半径
            double? BufferRadius;
            found = operationInput.TryGetAsDouble("BufferRadius", out BufferRadius);
            if (!found || !BufferRadius.HasValue)
                throw new ArgumentNullException("BufferRadius");
            //根据属性名称、查询半径等条件获得结果列表
            //这里的代码就不再进行罗列了,非常简单,本书也有统一的源代码输出口
            List<IFeature> pList = GetQueryBufferGeometryList(this.m_QueryFieldName,WhereClause,BufferRadius);
            //将查询结果的AO对象序列化为JSON对象
            return GetResultJson(pList);

            //string parm1Value;
            //bool found = operationInput.TryGetString("parm1", out parm1Value);
            //if (!found || string.IsNullOrEmpty(parm1Value))
            //    throw new ArgumentNullException("parm1");

            //string parm2Value;
            //found = operationInput.TryGetString("parm2", out parm2Value);
            //if (!found || string.IsNullOrEmpty(parm2Value))
            //    throw new ArgumentNullException("parm2");

            //JsonObject result = new JsonObject();
            //result.AddString("parm1", parm1Value);
            //result.AddString("parm2", parm2Value);

            //return Encoding.UTF8.GetBytes(result.ToJson());
        }

        private List<IFeature> GetQueryBufferGeometryList(string p, string WhereClause, double? BufferRadius)//查询缓冲区,将查找到的范围内的点返回,查询字段信息p="Name",WhereClause="黄村小学",BufferRadius="100"
        {//核心业务代码
            //Name=黄村小学,根据"Name=黄村小学"计算querylayer"学校"图层下"Name=黄村小学"并且方圆为100的缓冲区,然后利用该缓冲区与deslayer"街坊"图层叠加计算得到结果
            //缓冲区函数
            //叠加函数

            //先查询Name=黄村小学的要素
            IFeature f = this.m_QueryFeatureClass.Search(IQueryFilter , false);

            //计算Name=黄村小学的要素周围的缓冲区
            ITopologicalOperator pTopo = null; //缓冲区接口
            IGeometry pBuffer = pTopo.Buffer(100);
            IElement pElement = null;
            
            IGeometry pGeo = null;
            ISelection pSelection = null;
            IEnumFeatureSetup pEnumFeatureSetup = null;
            IEnumFeature pEnumFeature = null;
            IFillSymbol pFillSymbol = null;
            IRgbColor pRgbColor = null;

            //将上面计算得到的缓冲区与街坊图层进行叠加分析

            //将结果返回

            IFeature pFea = null;
            IFeature pFeature = null;
            IFeatureLayer pFeaLayer = null;
            IFeatureClass pFeaClass = null;
            IFeatureCursor pFeaCursor = null;

            ISpatialFilter pSpatialfilter = null;



            throw new NotImplementedException();
        }

        private byte[] GetResultJson(List<IFeature> pList)
        {
            List<JsonObject> pJoList = null;
            if (pList != null)
            {
                IFeature pfea = null;
                pJoList = new List<JsonObject>();
                for (int i = 0; i < pList.Count; i++)
                {
                    pfea = pList[i];
                    //将查询结果的Geometry转化为JSON对象
                    pJoList.Add(Conversion.ToJsonObject(pfea.ShapeCopy));
                }
            }
            JsonObject resultJson = new JsonObject();
            //设置分析结果名称"geometries"
            resultJson.AddArray("geometries", pJoList.ToArray());
            byte[] result = Encoding.UTF8.GetBytes(resultJson.ToJson());
            return result;
        }

利用java开发soe:http://blog.sina.com.cn/s/blog_a9be5a470102uz6o.html

原文地址:https://www.cnblogs.com/2008nmj/p/13896020.html