STK + C # + Cesium joint programming (1): technical route verification

overview

This article demonstrates an application example based on STK + C # + Cesium joint programming. There is a wealth of online information about STK and Cesium programming online. This article mainly solves how to configure IIS services so that remote clients can access, initialize, and execute server-side STK interface services.

Please refer to the author's previous articles about STK, Cesium (CZML), and C# building Web services. If you have any questions, please leave a message or comment.

  • C#: A programming language released by Microsoft. This example does not consider cross-platform support. C# is still based on the .NETFramework framework rather than .NET Core.

  • STK: A leading professional analysis software for the aerospace industry.

  • Cesium: JavaScript-based front-end 3D and 2D earth visualization programming open source library.

Environment and version

  • STK 11.6

  • VS2017(C#, .NETFramework 4.8)

  • Cesium-1.99

  • IIS 10.0 (Win system default)

Related Online Resources

C# online learning and reference: 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 online documentation: https://cesium.com/learn/cesiumjs/ref-doc/

Cesium online instance: https://sandcastle.cesium.com/

target task

Through the C# programming interface provided by STK, calculate the (spherical) distance between any two points on the earth's surface, and display it synchronously on the front end of Cesium.

specific process:

  1. The client (a Cesium-based visualization client) sends a calculation request, and the parameters are the latitude and longitude of two points on the sphere, in this case, Beijing (116.391, 39.915) and Shanghai (121.4, 31.2).

  1. The server receives the client's request, starts the STK instance, creates the scenario (scenario), executes the calculation, and returns the result.

  1. The client receives the calculation results from the server, draws the connection line between Beijing and Shanghai in the Cesium visualization scene, and identifies (displays) the calculation results (the distance between the two places).

Implementation process

STK part

The STK programming interface provides three user application types:

  1. Extend STK : The extension of the STK desktop application, running as part of the desktop application, there are two ways:

  1. Embed in the STK desktop application as an HTM page;

  1. UI plug-ins, such as developing a new STK desktop application UI plug-in.

  1. AutomateSTK : Controls the STK desktop application (Controls the STK desktop application). In this mode, the user application can connect to an existing STK instance, or create an STK instance. Usually, the user application first tries to connect to an existing STK instance, and if not, a new STK instance is created. User applications in this mode are usually console-based applications, and exit STK (disconnect from the STK application) after the relevant calculations are completed. For example, Matlab + STK is an example of a typical application mode of this type.

  1. Develop acustom application : run independently from the STK desktop application (Runs independently from the STK desktop application), this type of application is also called STK Engine. In this mode, the user application integrates the STK analysis and visualization engine into its own application. The application usually creates an AgStkObjectRoot object through the 'new AGI.STKObjects.AgStkObjectRoot();' method, and then performs calculations and operations based on this object. The application can also choose whether to integrate ActiveX controls (typically such as 2D and 3D controls), for example, through 'new AGI.STKX.Controls.AxAgUiAx2DCntrl();' to generate a 2D view plug-in, through 'newAGI.STKX.Controls.AxAgUiAxVOCntrl() ;'Generate a 3D view plug-in, the user application can generate multiple 2D or 3D plug-ins, and can perform independent display settings and controls.

One sentence summary: mode 1) is used to develop STK application components and integrate them into STK to run; mode 2) connect to an already running STK process, perform calculations through the interface library provided by STK, and disconnect from STK after the calculation is completed Process communication; Mode 3) Develop an STK application, which itself is an STK process (Engine).

In this example and the application development of this series, the interaction with STK through the web server-side application is applicable to the Automate STK application development mode . That is, when the client calculates a request, the server completes the calculation by interacting with the external STK application, and then returns the calculation result to the client. The server (Web server) does not need to integrate the STK application and its functions.

Of course, it is also possible to develop an independent STK desktop application that integrates STK application functions in the third) mode that provides Web services at the same time, and this mode is not discussed in this series.

The server needs to install the corresponding version of STK, and it can run normally.

C# server

Refer to the author's previous two blog posts to configure the C#-based Web server:

C# builds Web service project combat (1): https://blog.csdn.net/wangyulj/article/details/128567095

C# builds Web service project actual combat (2): https://blog.csdn.net/wangyulj/article/details/128676847

prerequisites:

  • A C# Web Service server (VS2017 + C# + .NETFramework 4.8 used in this example)

  • In this example, data is transferred between the client and server based on the JSON format. The C# Web Service project needs to introduce the System.Text.Json package (assembly).

Sample code:

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>

/// Summary description of WebService1

/// </summary>

[WebService(Namespace = "http://www.xxx.com/")]

[WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]

[System.ComponentModel.ToolboxItem(false)]

// To allow this web service to be called from script using ASP.NET AJAX, uncomment the following line.

[System.Web.Script.Services.ScriptService]

public class WebService1 : System.Web.Services.WebService

{

public AgUiApplication m_STKApp { get; set; }

public IAgStkObjectRoot m_STKRoot { get; set; }

// keep HelloWorld method for example

[WebMethod]

public string HelloWorld()

{

return "Hello World";

}

[WebMethod(Description = "Calculate the distance between two places")]

//Parameter Description:

// StartLat, StartLon: the latitude and longitude of the starting point

// EndLat, EndLon: the latitude and longitude of the end point

public void MeasureSurfaceDistance(string StartLat, string StartLon, string EndLat, string EndLon)

{

string distance = ""; // calculation result: distance between two places

string description = ""; // description of calculation process, result or exception, etc.

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>

说明:

  • 创建场景;

  • 在场景中添加北京和上海两个位置(Point);

  • 在两个点之间连线(Polyline);

  • 添加一个Label用于标识两地之间的距离(Label);

  • 添加一个按钮以启动异步Ajax请求,从服务器获取STK的计算结果,并更新界面中显示的两地之间距离

  • 在Cesium.Ion.defaultAccessToken=’’代码行中赋值你从Cesium官网获取的Token。

  • 示例代码中相关数据(结构)的定义需结合前述C#服务端部分示例代码理解。

客户端运行界面如下:

一、初始界面

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

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

关于Cesium相关的编程本文不继续深入,欢迎留言讨论。

拓展:需要解决多个客户端的并发访问冲突,需提供服务器端STK应用实例(启动、关闭、进程数量等)的运行控制及维护。

Guess you like

Origin blog.csdn.net/wangyulj/article/details/128914391