TerraExplorer二次开发C#实战:WinForm图层、建模与拾取
简介:面向Skyline TerraExplorer二次开发者的C#入门与查阅型示例包,适合已具备C#基础、希望快速理解三维地球平台API调用方式的GIS开发人员。压缩包共21个文件、约42KB,以cs源码和resx资源文件为核心,辅以Shapefile矢量数据、飞行路径文件、解决方案与项目配置及少量位图资源,便于在Visual Studio中直接打开解决方案,对照窗体逻辑、地图对象操作和数据加载流程进行学习。已有484人学习下载,可作为搭建最小可运行工程的参考。内容覆盖事件响应、图层与标记控制、空间数据集成、三维模型加载及自定义界面等常见开发环节,示例结构紧凑,适合按模块拆解并迁移到实际项目,也能帮助排查SDK引用、资源配置和基础交互中的常见问题。
1. 拿到一份 TerraExplorer 二次开发 C# 示例,先判断它值不值得跑
很多人拿到 TerraExplorer 二次开发 C# 示例代码的第一反应是双击 .sln 编译运行,结果不是卡在 Interop 引用上,就是窗口出来了但三维球是一片灰。这份示例的价值不在于代码多精美,而在于它把 Skyline 体系里最绕的几条链路——控件宿主、SGWorld 入口、图层与要素、屏幕坐标拾取——用一段可运行的最小闭环串起来了。它适合两类人:一类是要在 WinForm 里嵌一个三维场景、做点位标注和飞行定位的桌面端开发者;另一类是被 Com 组件搞得头大、想知道哪个对象该在哪一层释放的维护者。下面按我实际拆包的顺序写,先跑通,再改,最后讲哪里会翻车。
2. 环境与引用:把 COM 控件装进 WinForm 的第一步
示例能不能跑,八成取决于这一步有没有做对。TerraExplorer 的二次开发本质是 COM 调用,C# 只是套了一层 Interop 壳,壳没套好,后面写再多业务代码都是空的。
2.1 先分清三套接口各自的职责
打开示例工程,引用列表里通常能看到几个名字相近的东西:AxTerraExplorerX、TerraExplorerX、SGWorld56。新手最容易在这里犯迷糊,觉得它们是一个东西的三种写法,其实层级完全不同。
AxTerraExplorerX是 ActiveX 宿主控件,负责把三维渲染窗口嵌进 WinForm,它管的是“显示”和“鼠标消息”;TerraExplorerX是控件本身的类型库,提供事件和属性;SGWorld(示例里常见SGWorld56Class)是脚本侧的总入口对象,管的是“场景里有什么”——建对象、开图层、做定位,都从这里进。三者的关系是:控件负责画,SGWorld 负责改,改完控件自动刷新。
理解这一点之后,很多报错就顺了。比如“球出来了但对象不显示”,说明控件没问题,问题在 SGWorld 这一侧的对象参数或分组;反过来“代码全对但窗口一片灰”,那多半是控件没初始化或者工程没打开。我一般会把初始化逻辑拆成“控件就绪”和“世界就绪”两个状态位,出问题时先看哪个是 false,比盲猜快得多。
还有一点:接口名后面的数字后缀(56、60 之类)是版本标识,不是随便起的。示例里写SGWorld56Class,你的机器上导入的 Interop 就必须有同名类型,否则编译直接过不去。遇到这种情况不要去改代码里的类名,而是回到引用上,确认导入的是本机客户端自带的那个类型库。
2.2 两条引用路线:tlbimp 导入 vs 直接引 dll
示例工程一般会附带生成好的 Interop 程序集,直接引用能跑;但换一台机器、换一个客户端小版本,就得自己重新生成。命令行方式最稳,也最容易复现。
# 1) 把 ActiveX 控件导成 .NET 可用的宿主控件(生成 AxXxx 包装类) aximp "C:\Program Files\TerraExplorer\TerraExplorerX.ocx" # 2) 把类型库导成 Interop 程序集,命名空间按项目习惯指定 tlbimp "C:\Program Files\TerraExplorer\TerraExplorerX.dll" /out:TerraExplorerX.Interop.dll /namespace:TE.Interop # 3) 注册控件(32/64 位要用对应位数的 regsvr32,别拿错) regsvr32 "C:\Program Files\TerraExplorer\TerraExplorerX.ocx"三条命令分别解决三个问题:第一条生成 WinForm 设计器能拖的控件包装;第二条生成代码里new SGWorld56Class()所需的类型定义;第三条让系统知道这个 COM 组件装在哪。
参数上有两个地方要留意。/namespace建议固定成一个项目内统一的名字,示例里如果用的是别的命名空间,编译前统一改掉,别一半using TE.Interop一半using TerraExplorerX,后面维护会很难受。regsvr32的位数必须和客户端一致,64 位系统上装 32 位客户端却用System32\regsvr32.exe注册,会得到一个看起来像“文件损坏”的报错,实际是位数错配,换成SysWOW64下的那个就好了。
另外,Visual Studio 里直接“添加引用 → COM”也能生成 Interop,但生成的程序集名字带随机后缀(如Interop.TerraExplorerX.1.0.dll),换机器容易对不上。我更倾向用命令行生成固定名字的 dll,然后把 dll 一起放进代码仓库的libs目录,团队里谁拉下来都能编。
2.3 一个能跑起来的窗体:初始化、打开工程、释放
引用搞定之后,最小可运行代码其实只有三十来行。关键在于顺序:先建 SGWorld 实例,再把它挂到宿主控件上,最后才是打开工程。
using System; using System.Windows.Forms; using System.Runtime.InteropServices; using TE.Interop; // 与 tlbimp 的 /namespace 保持一致 public partial class MainForm : Form { private SGWorld56Class sgWorld; // 场景总入口 private bool worldReady = false; // 世界是否就绪,排错时先看这个 public MainForm() { InitializeComponent(); } private void MainForm_Load(object sender, EventArgs e) { try { sgWorld = new SGWorld56Class(); // 把 SGWorld 挂到宿主控件上;第二个参数 0 表示不抢占控件控制权 // 若本机 Interop 里没有 AttachTo,直接省略此行,后续 Open 仍可用 sgWorld.AttachTo(axTerraExplorerX1, 0); worldReady = true; } catch (Exception ex) { // COM 报错信息通常很含糊,把 HRESULT 一起打出来 MessageBox.Show($"初始化失败:0x{ex.HResult:X8} {ex.Message}"); } } private void MainForm_FormClosing(object sender, FormClosingEventArgs e) { if (!worldReady) return; // 不释放的后果:同一进程里二次打开窗体必崩,这是血泪经验 Marshal.ReleaseComObject(sgWorld); sgWorld = null; worldReady = false; } }逻辑上分三段。第一段new SGWorld56Class()创建的是脚本侧的世界对象,此时它还没绑定任何渲染窗口;第二段AttachTo把世界和控件接在一起,之后场景里的任何变化才会反映到界面上;第三段在关窗体时释放 COM 引用,这一步很多人省掉,单次运行看不出问题,一旦你的主界面是“关闭再打开”的 MDI 结构,第二次进来就是异常。
参数里最容易被忽略的是AttachTo的第二个参数。传 0 表示只挂接、不接管,工程由你自己调Open打开;传 1 在某些版本里会尝试接管已有工程,适合宿主已经被别人初始化过的场景。示例代码如果直接写死 1,你嵌到自己项目里可能出现“工程打开两次”的怪现象,先改成 0 试一次。
另外,worldReady这个布尔量不是凑数的。我踩过的坑是:初始化抛异常被 catch 吞了,后面每一行业务代码都对 null 调用,报错点全在业务层,真正的原因在第一行。加个状态位,日志里一眼能看出是“没起来”还是“起来了但参数错”。
3. 工程与图层:shp 数据怎么真正落到球上
初始化只是把舞台搭好,接下来是往舞台上放东西。三维项目里 90% 的数据是矢量,shp 又是最常见的载体,这一章把工程打开、图层接入、属性读取三条链路走一遍。
3.1 打开 FLY 工程与拿到主控对象
FLY 是这套体系的工程文件,里面存了场景配置、图层组织、视角书签。示例代码一般会硬编码一个路径,直接改成配置文件读取更实际。
/// <summary> /// 打开本地 FLY 工程;失败时返回 false,由调用方决定是否继续 /// </summary> private bool OpenFly(string flyPath) { if (string.IsNullOrWhiteSpace(flyPath)) return false; if (!System.IO.File.Exists(flyPath)) return false; // 先判存在,报错更直观 try { // 路径必须是绝对路径;网络映射盘在服务端运行时经常拿不到句柄 sgWorld.Open(flyPath); return true; } catch (Exception ex) { MessageBox.Show($"打开工程失败:0x{ex.HResult:X8} {ex.Message}"); return false; } }这段没有复杂逻辑,但有两个参数要盯住。一是flyPath必须是绝对路径,且不要用~或环境变量,COM 层不认;二是工程文件如果带密码保护,Open的重载里要补密码参数,示例里通常没写,你直接抄过去会得到一个“无权限”之类的模糊错误。
工程打开成功的标志不是方法没抛异常,而是sgWorld.Project.Name之类能取到值。我一般会在打开后立刻读一次工程名做断言,读不到就认为失败。这比等后面建对象时报错要早得多。
还有一种情况:工程能打开,但球是黑的。这通常是工程里引用的地形或影像服务连不上,属于数据源问题,不是代码问题。判断方法是打开客户端本身跑一遍同一个 FLY,如果客户端也黑,就别在代码里找了。
3.2 shp 图层接入与字段读取
图层接入的核心是CreateFeatureLayer,参数看着多,实际常用的就三个:名字、路径、挂到哪个组。
/// <summary> /// 把本地 shp 加载成一个要素图层 /// </summary> private IFeatureLayer56 LoadShp(string shpPath, string layerName, int groupId = 0) { if (!System.IO.File.Exists(shpPath)) throw new System.IO.FileNotFoundException("shp 不存在", shpPath); // 参数依次为:图层显示名、文件路径、连接方式、SQL 过滤、所属组、密码 // 形参个数与顺序在不同版本 Interop 里有差异,以 IDE 补全为准,不要盲抄 IFeatureLayer56 layer = sgWorld.Creator.CreateFeatureLayer( layerName, // 图层树里显示的名字 shpPath, // 绝对路径 "FileName", // 本地文件型数据源 "", // SQL 过滤,空串表示全量加载 groupId, // 0 表示挂到根组 ""); // 密码,本地文件留空 return layer; }groupId传 0 挂根组最保险。示例里如果传了某个固定数字,而你的工程里那个组恰好是隐藏的,就会出现“图层创建成功但看不见”的玄学现象。组 ID 可以通过sgWorld.ProjectTree相关接口查,也可以先用 0 跑通再挪。
要素读取是下一步。字段名必须和 dbf 表头完全一致,包括大小写和空格。
// 遍历图层要素,读出业务字段;方法名 GetFeatures/FeatureIDs 依版本而定 foreach (IFeature56 feature in layer.GetFeatures()) { // 字段名写错不会抛异常,只会返回空串,这是最难查的一类问题 string material = feature.GetAttribute("MATERIAL"); string code = feature.GetAttribute("PIPE_CODE"); if (string.IsNullOrEmpty(code)) continue; // 空值要素直接跳过 // 这里接业务逻辑:把 code 和 material 写进你的表格或着色规则 }这里有个容易翻车的细节:shp 的 dbf 字段名有 10 字符长度限制,很多中文项目在导出时字段被截断成MATERIA_1这种,你按设计文档里的字段名去读,永远是空串。稳妥做法是先遍历一遍字段列表打印出来,确认实际名字再写死。
还有编码问题。中文 dbf 常见 GBK,某些版本读出来是乱码。处理方式是在建图层时用带编码参数的重载,或者在数据准备阶段就转成 UTF-8 的 gdb/geojson 再入库。不要在代码里做二次转码,容易越转越乱。
3.3 图层样式与分组管理
图层加载进来默认是白色细线,投在大场景里基本看不见,得设样式。
// 图层级样式:线色、填充、线宽;颜色字节序各版本有差异 layer.LineColor = 0x0000FF; // 先按 BGR 试,若显示成蓝色说明要反转成 RGB layer.FillColor = 0x0000FF; layer.LineWidth = 2; layer.Show = true; // 显式打开,避免被工程默认配置关掉颜色这块我要多说一句:不同版本的 Interop 对颜色整数的解析顺序不完全一致,0x0000FF有的版本是红、有的是蓝。第一次接的时候不要猜,写个红蓝对比的两个对象放屏幕上,看一眼就知道该不该反转。这个坑不解决,后面所有按属性着色都会错位,而且错得很隐蔽——你会以为是业务字段映射错了。
分组管理是工程化的关键。真实项目里图层几十个,全挂根组会乱成一锅粥。常见做法是按业务域建组,再按组挂图层:
// 建组:组名 + 父组 ID,返回组对象 IProjectTreeItem56 group = sgWorld.ProjectTree.CreateGroup("管网数据", 0); // 后续加载都挂到这个组下 IFeatureLayer56 layer = LoadShp(shpPath, "给水主管", group.ID);挂组之后,图层的显隐就可以整组控制,界面上做个复选框,勾一下整组开关,比逐个图层去设Show干净得多。示例代码里如果没做分组,建议自己补上,这是从“能跑”到“能用”的分水岭。
4. 三维对象与交互:几何、姿态与鼠标拾取
数据上球之后,下一步是“看得懂”和“点得到”。这一章的三块内容——几何构建、姿态参数、屏幕拾取——是交互类需求绕不开的。
4.1 用 WKT 建几何:折线和多边形的稳妥走法
直接调CreatePolyline需要先有一个几何对象,而几何对象最省事的来源是 WKT。这条路径的好处是:你从 PostGIS、从文件、从接口拿到的坐标串,稍微拼一下就能用。
/// <summary> /// 由经纬度点列生成一条三维折线 /// </summary> private ITerrainPolyline56 DrawLine(double[][] points, uint color, string name) { // WKT 坐标顺序:经度 纬度 高程,逗号分隔,空格分隔维度 var sb = new System.Text.StringBuilder("LINESTRING ("); for (int i = 0; i < points.Length; i++) { if (i > 0) sb.Append(", "); sb.AppendFormat("{0:F6} {1:F6} {2:F2}", points[i][0], points[i][1], points[i][2]); } sb.Append(")"); // 由 WKT 生成几何,坐标系按 WGS84 经纬度理解 IGeometry56 geom = sgWorld.Creator.GeometryCreator.CreateGeometryFromWKT(sb.ToString()); // 参数:几何、颜色、所属组、描述名 return sgWorld.Creator.CreatePolyline(geom, color, 0, name); }三个参数要盯。F6的精度对经纬度足够(约 0.1 米),F2对高程足够;精度写太高没有意义,反而让 WKT 字符串变长、解析变慢。高程如果是相对地面,要用带贴地属性的重载,否则线会悬在半空或者埋到地下。
color的字节序问题跟图层样式一样,先用一条对比线校准。name不是随便起的,它是后续GetObject(name)反查对象的唯一凭据,命名规则要提前定好,比如“业务类型_主键”,避免重复。
多边形同理,把LINESTRING换成POLYGON,注意首尾点闭合。闭合这点很坑:不闭合的 POLYGON 在某些版本里会被自动补一条边,看起来正常但面积算出来是错的。涉及面积统计的项目,一定要在建完之后读一次几何校验。
4.2 CreatePlane 与文字标签的姿态参数
标牌、摄像头图标、告警点,基本都用CreatePlane做。它的难点不在创建,在姿态。
// 位置:经度、纬度、高程、高程类型(相对地面/绝对/贴地)、偏航、俯仰、翻滚 IPosition56 pos = sgWorld.Creator.CreatePosition( lon, lat, height, AltitudeTypeCode.ATC_TERRAIN_ABSOLUTE_OFFSET, // 相对地面抬高,最常用 yaw, // 偏航角,绕垂直轴,0 为北 pitch, // 俯仰角,正负决定抬头低头 roll); // 翻滚角,标牌一般给 0 // 宽高单位是米;名字用于反查;组 ID 决定归属 ITerrainModel56 model = sgWorld.Creator.CreatePlane(width, height, pos, 0, name);四个参数组合起来决定“牌子立不立得住”。ATC_TERRAIN_ABSOLUTE_OFFSET表示相对地形抬高,摄像机装 8 米就写 8,地形起伏时它会跟着走;ATC_ABSOLUTE是绝对海拔,标定过的固定设施才用。混用这两种会导致同一批点位有的贴地有的飘空,看起来像数据错了,其实是高程类型没统一。
角度参数的单位是度,不是弧度。我见过有同行按弧度传,结果标牌全躺平了。另外pitch给 -90 大致是垂直向下看,做俯视图标时常用;给 0 是竖直站立。文字标签的参数更简单,位置加文本即可,但字号在不同分辨率下观感差异很大,务必在目标机器的分辨率下调一次,别只在开发机上看着合适。
批量建对象时还有一个性能问题:一次循环建几千个,界面会明显卡顿。常见做法是先关掉控件刷新,批量建完再统一刷新,或者分批次加进度条。这个优化在示例代码里通常没有,属于落地时要自己补的。
4.3 鼠标拾取:PixelToWorld 里那几个参数的真实含义
点击拾取是交互的核心。思路很简单:拿到鼠标的屏幕坐标,转成世界坐标,从中取出对象 ID,再反查属性。
// 事件签名:标志位、屏幕 X、屏幕 Y;绑定在 Ax 控件上而不是 SGWorld 上 private void axTerraExplorerX1_OnLButtonClicked(int Flags, int X, int Y) { // 第三个参数是拾取标志,0 表示默认全部类型;不同版本枚举名不同,先用 0 IWorldPointInfo56 info = sgWorld.Window.PixelToWorld(X, Y, 0); if (info == null || string.IsNullOrEmpty(info.ObjectID)) { // 点在天空或空白处,正常返回,不要弹错误框 return; } string objId = info.ObjectID; // 对象 ID,可用于 GetObjectByID double lon = info.Position.X; // 命中点的经度 double lat = info.Position.Y; // 命中点的纬度 double alt = info.Position.Z; // 命中点的高程 // 用 ID 反查对象,比用屏幕坐标和列表比对稳得多 ITerraform56 obj = sgWorld.ProjectTree.GetObject(objId) as ITerraform56; if (obj != null) obj.Show = false; // 示例:点一下隐藏 }这里最关键的一句是“用 ID 反查,不要用屏幕坐标和列表比”。后者在缩放、视角变化之后立刻失效,而且几乎无法调试。PixelToWorld返回的Position是命中点的世界坐标,不是对象中心,做标注吸附时要注意这个区别。
Flags参数决定能不能穿过地形、能不能命中模型,具体枚举值各版本有差异,先用 0 拿到默认行为,再按需要调整。如果发现点模型点不中、点地面能中,八成就是这个标志位没开。
事件绑定还有一个易错点:事件要挂在 Ax 控件实例上,不是挂在sgWorld上。写成sgWorld.OnLButtonClicked += ...编译能过、运行没反应,查起来很费时间。判断方法是加个断点,点一下看进不进来,进不来就是绑错对象了。
4.4 FlyTo:把视角稳定地送到指定位置
定位看起来只是一个方法调用,实际参数没给对,镜头会翻跟头。
/// <summary> /// 飞到指定点并保持固定视角 /// </summary> private void FlyTo(double lon, double lat, double alt, double yaw, double pitch) { IPosition56 target = sgWorld.Creator.CreatePosition( lon, lat, alt, AltitudeTypeCode.ATC_TERRAIN_ABSOLUTE, // 定位用绝对高度,行为可预期 yaw, pitch, 0); // 第二个参数是动作类型,AC_FLYTO 为平滑飞行,AC_JUMPTO 为瞬移 sgWorld.Navigate.FlyTo(target, ActionCode.AC_FLYTO); }两个要点。一是定位建议用ATC_TERRAIN_ABSOLUTE绝对值,不要用相对地形,否则地形数据一换,同一个点位的高度就变了,看起来像“定位不准”。二是yaw和pitch决定视线方向,只给位置不给角度,镜头会保留上一次的方向,做“视角书签”功能时这就是 bug。
批量巡检类需求常用的是先AC_JUMPTO再微调,因为平滑飞行在连续调用时会互相打断,最后停在哪全看运气。有动画演示需求才用AC_FLYTO,并且要控制调用频率,两次飞行之间留出足够时间。
5. 避坑清单:这几处报错我踩过不止一次
下面五条按“现象 → 原因 → 解决”写,都是实际项目里反复出现的,跟代码写得好不好没关系,纯粹是这套 COM 体系的使用习惯。
5.1 启动与引用类问题
现象一:编译通过,运行时报“检索 COM 类工厂失败”,HRESULT 是 0x80040154。原因:目标机器没装客户端,或者装的是 32 位客户端而程序编译成 AnyCPU 在 64 位下跑,位数对不上。 解决:先确认客户端装没装、是哪个位数;然后把项目的目标平台从 AnyCPU 明确改成 x86 或 x64,与客户端保持一致。这一步改完必须重新生成,光改配置不重新编译是不生效的。
现象二:本机跑得好好的,拷到同事机器上提示找不到 Interop 程序集。原因:引用指向的是生成目录或 GAC 里的绝对路径,拷过去就断了。 解决:把tlbimp生成的 dll 复制到项目的libs目录,引用改成相对路径,并把“复制到输出目录”设为 true。别依赖 GAC,GAC 里的东西版本一多就乱。
5.2 运行时与部署类问题
现象三:主界面关掉再打开,第二次初始化直接崩,异常信息看不出所以然。原因:上一次的 COM 对象没释放,进程里的引用计数没归零,控件重新初始化时状态冲突。 解决:在窗体关闭事件里对sgWorld调Marshal.ReleaseComObject,并把字段置 null。如果窗体里还持有图层、模型等其他 COM 对象,也要一并释放,只释放顶层是不够的。
现象四:图层创建成功,但球上什么都不显示。原因:集中在三处——shp 缺.prj导致坐标系认不出;图层挂到了隐藏组;图层Show属性默认为 false。 解决:先检查 shp 同目录下有没有.prj,没有就用工具补一份 WGS84 的;再把groupId改成 0 挂根组复测;最后显式写layer.Show = true。三步走完还不显示,就看工程本身有没有叠加上限或图层数限制。
现象五:点击事件死活不触发,打断点进不去。原因:事件绑在了sgWorld上而不是 Ax 控件上,或者控件被上层 Panel 完全遮挡、拿不到鼠标消息。 解决:把事件重新绑到 Ax 控件实例;检查控件的 Dock 属性和 Z 序,被遮住时鼠标消息到不了。还有一种情况是控件还没初始化完就绑事件,挪到Load之后绑一次即可。
现象六:部署到客户机上,程序启动就闪退,日志里只有一串 HRESULT。原因:注册表里登记的控件路径和实际安装路径不一致,或者注册的是旧版本。 解决:用对应位数的regsvr32重新注册一次,注册前先确认客户端版本一致。打包安装程序时把注册动作写进安装脚本,别让现场人员手工注册,这活儿十次有八次会出错。
6. 进阶:把示例改成能交付的小工具
示例跑通之后,真正有价值的是把它改成一个能给别人用的东西。我常用的是一个“批量落点 + 越界校验”的小窗体:左边读 CSV,右边在球上批量建模,异常点位标红并不上图。核心是把输入校验做在前面,避免脏数据把场景搞乱。
/// <summary> /// 批量落点:先校验、再建模、最后统一刷新 /// </summary> private int BatchPlace(string csvPath, int groupId) { int ok = 0, bad = 0; var lines = System.IO.File.ReadAllLines(csvPath, System.Text.Encoding.UTF8); // 约定格式:id,name,lon,lat,height,首行为表头 for (int i = 1; i < lines.Length; i++) { var cols = lines[i].Split(','); if (cols.Length < 5) { bad++; continue; } if (!double.TryParse(cols[2], out double lon) || !double.TryParse(cols[3], out double lat) || !double.TryParse(cols[4], out double alt)) { bad++; continue; } // 越界校验:经度 ±180,纬度 ±90,高度 0~9000 米 if (lon < -180 || lon > 180 || lat < -90 || lat > 90 || alt < 0 || alt > 9000) { bad++; continue; } var pos = sgWorld.Creator.CreatePosition( lon, lat, alt, AltitudeTypeCode.ATC_TERRAIN_ABSOLUTE_OFFSET, 0, 0, 0); sgWorld.Creator.CreatePlane(6, 6, pos, groupId, cols[1]); ok++; } return ok; // 调用方拿 ok/bad 做提示 }参数上就三处值得调:CreatePlane的宽高按场景尺度给,城市级项目 6 米合适,园区级要缩到 1 米以内;ATC_TERRAIN_ABSOLUTE_OFFSET后面可以跟一个抬高值,设备图标抬 0.5 米能避免和地面 Z 冲突闪烁;groupId一定传具体组,别传 0,交付时图层树干净比什么都重要。
| 参数 | 建议值 | 说明 |
|---|---|---|
| 经度范围 | -180 ~ 180 | 超范围直接丢弃,不做纠正 |
| 纬度范围 | -90 ~ 90 | 同上 |
| 高程范围 | 0 ~ 9000 | 按项目实际场景收窄 |
| 图标尺寸 | 1 ~ 6 米 | 随场景尺度调整 |
| 批量上限 | 单批 500 个 | 超过分批处理,避免卡顿 |
做校验时我习惯把坏数据单独导出一个bad.csv,带上原因列,交给数据提供方去改。这比在界面上弹一句“有 37 条数据异常”有用得多,对方能直接拿去修。
验证环节也别省。批量落点之后,我一般随机抽三个点,用FlyTo飞过去目视核对一遍——看图标是不是站在正确的地物上,而不是飘在半空。这个动作花不了一分钟,但能发现坐标系、高程类型这类系统性问题,比在日志里翻半天快。
说个我自己的教训。早年交付一个巡检点位工具,示例代码里的坐标校验我直接删了,觉得输入都是自己人整理的不会错。结果现场导进来一批把经纬度写反的数据,一半点位落到了几百公里外,演示当场翻车。从那以后我每次接手这类示例,都强制把“范围校验 + 抽样目视”这两步写进流程,哪怕需求里没提。希望帮到你。
本文还有配套的精品资源,点击获取