概述
本文演示了一个基于STK +C# + Cesium联合编程的应用实例。关于STK和Cesium编程网上在线资料丰富,本文主要解决了如果配置IIS服务以使得远程客户端能访问、初始化、以及执行服务器端STK的接口服务。
请参考本作者之前关于STK、Cesium(CZML)、C#构建Web服务等相关文章,若有疑问,欢迎留言或评论。
C#:微软发布的编程语言,本例没有考虑跨平台支持,C#仍基于.NETFramework框架,而非.NET Core。
STK:处于领先地位的航空航天领域专业分析软件。
Cesium:基于JavaScript的前端三维和二维地球可视化编程开源库。
环境及版本
STK 11.6
VS2017(C#, .NETFramework 4.8)
Cesium-1.99
IIS 10.0(Win系统默认)
相关在线资源
C#在线学习及参考:https://learn.microsoft.com/zh-cn/dotnet/csharp/
STK 11 Online Help:https://help.agi.com/stk/11.7.1/
STK 12 Online Help:https://help.agi.com/stk/
STK Programming Help:https://help.agi.com/stkdevkit/index.htm
Cesium在线文档:https://cesium.com/learn/cesiumjs/ref-doc/
Cesium在线实例:https://sandcastle.cesium.com/
目标任务
通过STK提供的C#编程接口,计算地球表面任意两点间的(球面)距离,并同步在Cesium前端显示。
具体流程:
客户端(基于Cesium的可视化客户端)发送计算请求,参数为球面两个点的经纬度,本例中为北京(116.391, 39.915)和上海(121.4, 31.2)。
服务端接收到客户端的请求,启动STK实例,创建场景(scenario),执行计算,然后返回结果。
客户端接收到服务端的计算结果,在Cesium可视化场景中绘制北京和上海之间的连线,并标识(显示)计算结果(两地之间的距离)。
实现过程
STK部分
STK编程接口提供三种用户应用类型:
Extend STK:STK桌面应用的扩展,作为桌面应用的一部分运行,有两种方式:
在STK桌面应用中以HTM页面的方式嵌入;
UI插件,例如开发一个新的STK桌面应用UI插件。
AutomateSTK:控制STK桌面应用(Controls the STK desktopapplication)。此模式下,用户应用可连接到已有STK实例,或者创建一个STK实例,通常情形是用户应用首先尝试连接到已有STK实例,如果没有,则创建一个新STK实例。此模式下的用户应用通常为基于控制台的应用,相关的计算完成之后即退出STK(断开与STK应用的连接)。如Matlab + STK为此类型的典型应用模式实例。
Develop acustom application:独立于STK桌面应用运行(Runsindependently from the STK desktop application),此类应用亦称为STK Engine。此模式下,用户应用将STK分析和可视化引擎集成到自己的应用中,应用通常通过‘new AGI.STKObjects.AgStkObjectRoot();’方法创建一个AgStkObjectRoot对象,然后基于该对象展开计算和操作。应用也可以选择是否集成ActiveX控件(典型的如2D和3D控件),例如通过‘new AGI.STKX.Controls.AxAgUiAx2DCntrl();’生成一个2D视图插件,通过‘newAGI.STKX.Controls.AxAgUiAxVOCntrl();’生成一个3D视图插件,用户应用可以生成多个2D或3D插件,并可以进行独立的显示设置和控制。
一句话概括:模式1)用于开发STK应用组件,并集成到STK中运行;模式2)与一个已在运行的STK进程连接,通过STK提供的接口库执行计算,完成计算后断开与STK进程的通信;模式3)开发一个STK应用,本身是一个STK进程(Engine)。
本例以及本系列的应用开发中,通过Web服务器端应用与STK交互,适用于Automate STK应用开发模式。即,当客户端计算请求的到达,服务端通过与外部STK应用程序交互完成计算,然后将计算结果返回给客户端,服务端(Web服务器)无需集成STK应用及其功能。
当然,也可以开发一个同时提供Web服务的第3)种模式的集成STK应用功能的独立STK桌面应用,此模式不在本系列的讨论之中。
服务端需要安装相应版本的STK,并能正常运行。
C#服务端
参照作者之前两篇博文配置基于C#的Web服务器:
C#构建Web服务项目实战(一):https://blog.csdn.net/wangyulj/article/details/128567095
C#构建Web服务项目实战(二):https://blog.csdn.net/wangyulj/article/details/128676847
前提条件:
一个C# Web Service服务端(本例中用的VS2017 + C# + .NETFramework 4.8)
本例基于JSON格式在客户端和服务端传递数据,C# Web Service项目需引入System.Text.Json包(程序集)。
示例代码:
using System; using System.IO; using System.Collections.Generic; using System.Linq; using System.Web; using System.Web.Services; using System.Runtime.InteropServices; using AGI.STKObjects; using AGI.STKUtil; using AGI.Ui.Application; using System.Text.Json;
namespace MyWebApp { /// <summary> /// WebService1 的摘要说明 /// </summary> [WebService(Namespace = "http://www.xxx.com/")] [WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)] [System.ComponentModel.ToolboxItem(false)] // 若要允许使用 ASP.NET AJAX 从脚本中调用此 Web 服务,请取消注释以下行。 [System.Web.Script.Services.ScriptService] public class WebService1 : System.Web.Services.WebService {
public AgUiApplication m_STKApp { get; set; } public IAgStkObjectRoot m_STKRoot { get; set; } // 保留HelloWorld方法用于示例 [WebMethod] public string HelloWorld() { return "Hello World"; } [WebMethod(Description = "计算两地之间的距离")] //参数说明: // StartLat, StartLon:起点的维度与经度 // EndLat, EndLon:终点的维度与经度 public void MeasureSurfaceDistance(string StartLat, string StartLon, string EndLat, string EndLon) { string distance = ""; // 计算结果:两地之间的距离 string description = ""; // 计算过程、结果或异常等的描述 this.m_STKApp = null; this.m_STKRoot = null; try { // 获取已有STK运行实例 this.m_STKApp = Marshal.GetActiveObject("STK11.Application") as AGI.Ui.Application.AgUiApplication; } catch (System.Runtime.InteropServices.COMException ex1) { // 获取已有STK实例失败,创建一个新的STK实例 Guid clsID = typeof(AgUiApplicationClass).GUID; Type oType = Type.GetTypeFromCLSID(clsID); try { this.m_STKApp = Activator.CreateInstance(oType) as AGI.Ui.Application.AgUiApplication; // 获取用户设置(此过程应该是执行了有关license的检测) this.m_STKApp.LoadPersonality("STK"); } catch (System.Runtime.InteropServices.COMException ex2) { this.m_STKApp = null; description = "获取或创建STK应用实例失败1。\n" + ex2.Message; packAndResponse(-1, description, ""); return; } }
if (!(this.m_STKApp is null)) { try { this.m_STKRoot = (IAgStkObjectRoot)this.m_STKApp.Personality2; description = "成功创建STK用户应用。"; } catch (Exception ex3) { description = "获取或创建STK应用实例失败2。\n" + ex3.Message; packAndResponse(-1, description, ""); return; } }
if ((this.m_STKApp is null) || (this.m_STKRoot is null)) { // 初始化STK应用失败 if (!(this.m_STKRoot is null)) { Marshal.ReleaseComObject(this.m_STKRoot); } if (!(this.m_STKApp is null)) { Marshal.ReleaseComObject(this.m_STKApp); } packAndResponse(-1, "初始化STK应用失败。", ""); }
// --------------------------------------------- // 创建场景 // 关闭当前(可能有的)场景 this.m_STKRoot.CloseScenario(); // 新建场景 this.m_STKRoot.NewScenario("Demo");
// 执行计算 string strCmd = $"MeasureSurfaceDistance * {StartLat} {StartLon} {EndLat} {EndLon} Earth"; try { // 执行Connect命令 IAgExecCmdResult result = m_STKRoot.ExecuteCommand(strCmd); if(result.IsSucceeded) { description = "成功执行计算。"; distance = Convert.ToString(result[0]); } else { // 未能正确获得计算结果 this.m_STKRoot.CloseScenario(); packAndResponse(-1, "未能正确获得计算结果,请检查输入(计算)参数正确性。", ""); return; } } catch { // 执行Connect命令异常 this.m_STKRoot.CloseScenario(); packAndResponse(-1, "STK执行相关计算出现异常,请检查输入(计算)参数正确性。", ""); return; }
// 关闭场景 this.m_STKRoot.CloseScenario(); // 退出并关闭STK应用(可选) Marshal.ReleaseComObject(this.m_STKRoot); Marshal.ReleaseComObject(this.m_STKApp); this.m_STKApp = null; this.m_STKRoot = null;
packAndResponse(0, description, distance); } // 将计算结果作为JSON格式数据返回给客户端 private void packAndResponse(int b, string des, string rslt) { var calRslt = new STKServiceCalculateRslt(b, des, rslt); string jsonString = JsonSerializer.Serialize(calRslt); this.Context.Response.Clear(); this.Context.Response.ContentType = "application/json; charset=utf-8"; this.Context.Response.Write(jsonString); this.Context.Response.Flush(); this.Context.Response.End(); } }
// 定义用于描述计算结果的数据结构 public struct STKServiceCalculateRslt { public STKServiceCalculateRslt(string rslt) { isSuccess = 0; description = string.Empty; calResult = rslt; } public STKServiceCalculateRslt(string dspt, string rslt) { isSuccess = 0; description = dspt; calResult = rslt; } public STKServiceCalculateRslt(int bSuccess, string dspt, string rslt) { isSuccess = bSuccess; description = dspt; calResult = rslt; } // 计算是否成功标记:0 - success;^0 - failed public int isSuccess { get; set; } // 计算(结果)的描述 public string description { get; set; } // 计算结果:Json字符串 public string calResult { get; set; } } } |
代码说明
一、关于C# [WebMethod]的返回结果
C# Web Service方法默认会以XML格式封装数据然后返回给客户端。以上述HelloWorld方法为例,方法返回一个字符串(“Hello World”),C# Web服务会将返回结果封装为XML格式再返回给请求的客户端。
客户端的Ajax(本例客户端用Ajax包请求服务端数据)接收到数据后,会从XML数据中提取出“Hello World”字符串,并重新封装为“{“d” : “Hello World”}”格式的JSON对象,其中只有一个数据字段,数据字段名为“d”(由ajax自动生成),字段值为服务端返回的(字符串)数据。
经测试,假设服务端HelloWorld方法返回例如“{“result” : “Hello World}”形式的JSON格式字符串,客户端ajax代码生成的JSON对象为“{“d” : “{“result” : “HelloWorld}”}”。
如果需要服务端返回真正的JSON数据,参考示例代码中的packAndResponse方法,该方法返回的JSON数据会以所期望的形式被ajax解析。(具体细节不在此赘述,有疑问欢迎留言)此方式需要将C#的[WebMethod]方法的返回值设置为void,然后通过packAndResponse方法返回结果。
二、关于本例中的返回结果
本例中定义了一个通用的JSON格式的返回结果数据(参见上述结构STKServiceCalculateRslt的定义),如下:
返回结果包含三个字段:
isSuccess:指示有关STK计算的执行是否成功,0-成功,非0-失败;
description:关于计算任务、过程或结果的描述;
calResult:计算的结果,结算的结果本身可以是一个嵌套的JSON数据。本例中,计算结果为(数值转换的)字符串。
三、核心代码
本例中的核心Web Service方法为MeasureSurfaceDistance,该方法接收四个参数,分别代表计算起点的维度、经度,以及计算终点的维度、经度。(注意:维度在前,经度在后)
核心方法中关于STK应用和STK Root Object的初始化参见STK安装后随同安装的例子代码,本文中不展开解释(有问题欢迎留言)
IIS服务配置
编译生成项目,发布项目,若有疑问,可参照本系列之前的文章:
C#构建Web服务项目实战(一):https://blog.csdn.net/wangyulj/article/details/128567095
通过客户端访问,当然,是不可能成功的!!!
其间主要涉及的是IIS的用户及授权、STK的用户配置及授权(STK应用的运行需要验证服务器本地的用户配置文件),网络搜索没有答案,自己在黑暗中摸索,最终解决问题。
本例中,发布的IIS网站名称为:MyWebApp
本例中操作涉及的IIS版本为10.0
关于IIS的用户
在IIS 7及更高版本中,默认的用户名为IUSR,该用户属于IIS_IUSRS组。IUSR 帐户不再需要密码,因为它是内置帐户。本文不深入讨论IUSR。
从 IIS 7.5 开始,添加了名为“应用程序池标识”的新安全功能。此功能允许在唯一帐户下运行应用程序池,而无需创建和管理域或本地帐户。
本例中通过配置IIS服务的“应用程序池标识”达成所期望的目的,虽然通过应用程序池标识无需创建和管理域或本地账户,但应用程序池还提供了通过指定域或本地账户的方式访问服务器的应用、资源、及数据!
启动IIS管理器(可通过运行inetmgr命令启动),点击‘应用程序池’,界面如下,可以看到所有网站默认的‘标识’属性均为‘ApplicationPoolIdentify’。

关于此处的‘标识’字段,英文原文为‘Identify’,不知道是谁翻译的(应该是机器自动匹配翻译),个人感觉翻译的严重不正确,应当翻译为‘身份标识/鉴定/验证/识别’为妥,尤其是不能省略‘身份’关键字,如此则可一目了然知道该字段是用于(Web用户)身份识别的信息。
点击最右侧窗口中的‘高级设置’,可以看到‘标识’字段有如下四个选项:
ApplicationPoolIdentity(默认):远程用户访问时,用户身份为一个IIS分配的Windows系统内部账号(通过用户管理也看不见,但确实存在,网上有资料介绍在文件及目录权限设置中可通过搜索可以找到)。此账户对于本地应用程序来说是最安全的。
LocalSystem:所有标识中权限较高,使用HKEY_USERS/.Default账户配置,不能访问其他账户的配置,隶属于本地Administrators组。在系统权限设置中(例如在设置文件/文件夹安全属性时)通过搜索‘IISAppPool\DefaultAppPool’可以看到。
LocalService:拥有最小权限的本地账户,在网络凭证中具有匿名身份。
NetworkService:权限高于LocalService,低于LocalSystem。
通过测试,各类型‘标识’执行时IIS进程(进程名w3wp)的实际用户名如下表:
应用程序池标识 |
Web用户在本地执行时的用户名 |
ApplicationPoolIdentity |
用户名为MyWebApp,即IIS发布的网站名称。 |
LocalSystem |
用户名为SYSTEM,根据程序代码,返回用户名SYSTEM有可能是获取进程用户名时发生异常(系统匿名用户)。 |
LocalService |
用户名为LOCAL SERVICE |
NetworkService |
用户名为NETWORK SERVICE |
VS调试模式下 |
为当前活动的本地账户,即正在运行VS的当前登录主机的账户。 |
实际的试错过程非常耗时,本文不再继续深入讨论,感兴趣或有疑问的可以留言讨论。
下面直接给出解决方案。
一、创建一个本地用户,专用于STK服务计算
在服务器本地,通过‘计算机管理’->‘本地用户和组’创建一个本地用户账号,假设用户名为stkRemote(当然可以根据应用需要设定其他的用户名)。
新用户默认的组为‘Users’,权限不够执行STK应用。
将用户加入‘PowerUsers’组(如果调试过程中仍然不能正确执行服务端STK进程,可尝试加入‘Administrators’组)。
以新用户账号登录系统,运行STK应用,根据STK的提示完成相关配置,确认STK应用成功并正确运行,然后退出。
提示:在实际的应用(IIS服务)运行过程中,无需所创建的用户登录系统。
二、配置IIS的‘应用程序池’
启动IIS管理器,如上图,选择‘应用程序池’,在应用程序池界面中选择要配置的网站,然后点击右侧窗口的‘高级设置’,在弹出的‘高级设置’窗口中滚动鼠标定位到‘进程模型’,如下图。

找到‘标识’字段,鼠标点击默认设置‘ApplicationPoolIdentity’,然后点击字段右侧出现的小按钮,弹出如下的‘应用程序池标识’设置窗口。

点击‘自定义账户’,然后点击‘设置’,在弹出的‘设置凭据’窗口中输入为STK远程Web用户访问创建的本地用户账号,并确认用户密码,如下图。

点击确定保存设置,重新启动网站,远程测试,OK!!!
提示:
远程Web用户访问服务器,STK应用运行时,服务端无界面显示,但不影响STK计算的执行难。(猜测应该是.NETFramework和IIS服务权限设置);
完成各项配置后,建议均通过IIS管理器重新启动Web网站;
对服务器本地用户进行配置更改后,建议以该本地用户账户重新登录一次系统,并确认用户账户运行STK应用无误。
客户端Cesium
关于Cesium的提示:
默认Cesium客户端显示的GIS数据来自Cesium官网,如果你自己没有GIS服务器其他外部GIS服务,需要登录到Cesium官网获取一个用户Token,并在初始化中指定Token(参见下述代码)。
示例代码中所用到的Cesium有关的js脚本均已下载到本地。
完整源码:
<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="utf-8" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, minimum-scale=1, user-scalable=no" /> <meta name="description" content="Use Viewer to start building new applications or easily embed Cesium into existing applications."> <meta name="cesium-sandcastle-labels" content="Beginner, Showcases"> <title>C#+STK+Cesium Demo</title> <!-- Cesium相关的包 --> <script type="text/javascript" src="/Sandcastle/Sandcastle-header.js"></script> <script src="/CesiumUnminified/Cesium.js"></script> <script>window.CESIUM_BASE_URL = "/CesiumUnminified/";</script> <!-- JQuery包 --> <script type="text/javascript" src="/lib/jquery-3.6.0.min.js"></script> </head> <body class="sandcastle-loading" data-sandcastle-bucket="/CommonFiles/bucket-requirejs.html"> <style> @import url(/templates/bucket.css); </style> <div id="cesiumContainer" class="fullSize"></div> <div id="loadingOverlay"><h1>Loading...</h1></div> <div id="toolbar"></div> <script id="cesium_sandcastle_script"> window.startup = function (Cesium) { 'use strict'; Cesium.Ion.defaultAccessToken = 'your token retrived from Cesium online'; //Sandcastle_Begin const viewer = new Cesium.Viewer("cesiumContainer"); // 计算状态提示 const statusDisplay = document.createElement("div"); statusDisplay.innerHTML = "点击按钮执行计算……"; // 添加一个按钮 Sandcastle.addToolbarButtonWy("计算两地之间的距离", function () { statusDisplay.innerHTML = "正在执行计算,请耐心等待……"; var params = { "StartLat" : "39.915", "StartLon" : "116.391", "EndLat" : "31.2", "EndLon" : "121.4" }; $.ajax({ type : "POST", contentType : "application/json; charset:utf-8", url : "http://localhost:xx/WebService1.asmx/MeasureSurfaceDistance", data : JSON.stringify(params), dataType : "json", success : function (data) { // 注:直接返回的是ajax解析生成的JSON对象 if(data["isSuccess"] == 0) { statusDisplay.innerHTML = "成功执行计算,计算结果:" + data["calResult"] + " m"; var entity = viewer.entities.getById("BJ_to_SH"); entity.label.text = data["calResult"] + " m"; } else { statusDisplay.innerHTML = "计算失败,原因:" + data["description"]; } }, error : function(xhr) { // for test statusDisplay.innerHTML = xhr.responseText; } }); }); // 初始化场景,添加‘北京’和‘上海’两个地面标识,并在两地之间连线 const cityBeijing = viewer.entities.add({ name: "beijing", position: Cesium.Cartesian3.fromDegrees(116.391, 39.915), point: { pixelSize: 8, color: Cesium.Color.RED, outlineColor: Cesium.Color.LIME, outlineWidth: 1, }, label: { text: "北京", font: "14pt 宋体 bold", fillColor: Cesium.Color.YELLOW, style: Cesium.LabelStyle.FILL_AND_OUTLINE, outlineWidth: 2, verticalOrigin: Cesium.VerticalOrigin.BOTTOM, pixelOffset: new Cesium.Cartesian2(0, -9), }, }); const cityShanghai = viewer.entities.add({ name: "shanghai", position: Cesium.Cartesian3.fromDegrees(121.4, 31.2), point: { pixelSize: 8, color: Cesium.Color.RED, outlineColor: Cesium.Color.LIME, outlineWidth: 1, }, label: { text: "上海", font: "14pt 宋体 bold", fillColor: Cesium.Color.YELLOW, style: Cesium.LabelStyle.FILL_AND_OUTLINE, outlineWidth: 2, verticalOrigin: Cesium.VerticalOrigin.BOTTOM, pixelOffset: new Cesium.Cartesian2(0, 25), }, }); // 两地之间的连线 viewer.entities.add({ polyline: { positions: Cesium.Cartesian3.fromDegreesArray([116.391, 39.915, 121.4, 31.2]), width: 5, material: Cesium.Color.RED, }, }); // 两地之间的距离(Label) viewer.entities.add({ id : "BJ_to_SH", position: Cesium.Cartesian3.fromDegrees(118.8955, 35.5575), label: { text: "???? m", font: "16pt 宋体 bold", fillColor: Cesium.Color.YELLOW, style: Cesium.LabelStyle.FILL_AND_OUTLINE, outlineWidth: 1, verticalOrigin: Cesium.VerticalOrigin.BOTTOM, pixelOffset: new Cesium.Cartesian2(70, 0), }, });
// 计算提示信息 statusDisplay.style.background = "rgba(42, 42, 42, 0.8)"; statusDisplay.style.color = "rgba(192, 192, 42, 1)"; statusDisplay.style.padding = "5px 10px"; document.getElementById("toolbar").appendChild(statusDisplay);
// 设置场景视点 viewer.zoomTo(viewer.entities); // viewer.camera.flyHome(0); //Sandcastle_End Sandcastle.finishedLoading(); }; if (typeof Cesium !== 'undefined') { window.startupCalled = true; window.startup(Cesium); } </script> </body> </html>
|
说明:
本示例代码基于Cesium的Hello World例子(https://sandcastle.cesium.com)扩展,基本流程:
创建场景;
在场景中添加北京和上海两个位置(Point);
在两个点之间连线(Polyline);
添加一个Label用于标识两地之间的距离(Label);
添加一个按钮以启动异步Ajax请求,从服务器获取STK的计算结果,并更新界面中显示的两地之间距离
在Cesium.Ion.defaultAccessToken=’’代码行中赋值你从Cesium官网获取的Token。
示例代码中相关数据(结构)的定义需结合前述C#服务端部分示例代码理解。
客户端运行界面如下:
一、初始界面

二、点击按钮后,页面提示正在执行计算。由于是Ajax异步请求操作,故客户端的其他操作不会受影响。

三、计算结束,服务端返回结果,客户端更新界面显示,STK默认计算结果单位为米(m),计算结果显示两地之间直线(球面)距离约1068km。

关于Cesium相关的编程本文不继续深入,欢迎留言讨论。
拓展:需要解决多个客户端的并发访问冲突,需提供服务器端STK应用实例(启动、关闭、进程数量等)的运行控制及维护。
转载:https://blog.csdn.net/wangyulj/article/details/128914391