EarthSDK3实战 在 WebGL 三维可视化领域CesiumJS 是当之无愧的王者但其庞大的 API 和复杂的坐标系让许多开发者望而却步。EarthSDK 地球可视化二次开发框架一套代码实现 Cesium、UnrealEngine、OpenLayers 多引擎可视化。本文记录了基于 EarthSDK 3版本的二次开发实战过程涵盖环境搭建、常用功能实现。一、 搭建一个地球1.访问官方网站下载模板2.个人从0搭建 vuevite项目Node.js ≥ 20.x推荐 LTSVue 3 Vite 已不再支持 Node 18 及以下包管理器 pnpm / npm / yarn / bun推荐 pnpm速度快、磁盘占用小IDE VS Code 必装插件Vue - Official原 Volar#环境检查 node -v # 输出 v20.x 或更高 pnpm -v # 或 npm -v项目初始化安装earthsdk基础包pnpm add earthsdk3 --save # pnpm add cesium pnpm add earthsdk3-ue --save # pnpm add cesium pnpm add earthsdk3-cesium --save #静态资源提取库 pnpm add earthsdk3-assets --save #配置一个资源copy pnpm add vite-plugin-static-copy #配置插件 pnpm add vite-plugin-cesium配置main.tsimport { createApp } from vue import App from ./App.vue import { ESObjectsManager } from earthsdk3; import { ESCesiumViewer } from earthsdk3-cesium; import { ESUeViewer } from earthsdk3-ue; const objm new ESObjectsManager(ESUeViewer, ESCesiumViewer); createApp(App,{ objm }).mount(#app) objm.sceneTree.createSceneObjectTreeItemFromJson({ id: ae103185-08c7-4ed0-b6d4-15ad77bbbf66, type: ESImageryLayer, url: https://server.arcgisonline.com/arcgis/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}, maximumLevel: 18, name: 全球影像, allowPicking: true })vite.config.tsimport vue from vitejs/plugin-vue; import { defineConfig, normalizePath } from vite; import cesium from vite-plugin-cesium; import { viteStaticCopy } from vite-plugin-static-copy; import path from path; export default defineConfig({ // Use relative asset paths so dist can be hosted under subdirectories. base: ./, server: { proxy: { /pfsc-api: { target: https://pfsc.agri.cn, changeOrigin: true, rewrite: (p) p.replace(/^\/pfsc-api/, ) } } }, resolve: { alias: { : path.resolve(__dirname, src), // 配置Cesium的访问 cesium: path.resolve(__dirname, node_modules/cesium/Source/Cesium) } }, plugins: [ vue(), cesium(), // 运行和构建时copy viteStaticCopy({ targets: [ { src: normalizePath(path.resolve(__dirname, ./node_modules/earthsdk3-assets)), dest: ./js } ] }) ] })getobjm.tsimport { ESObjectsManager } from earthsdk3; import { inject } from vue; /** * 获取ESObjectsManager * returns {ESObjectsManager} */ export function getobjm() { const objm injectESObjectsManager(objm); if (!objm) throw new Error(ESObjectsManager not found); return objm; }MapContainer.vuescript setup langts import {getobjm} from /scripts/getobjm.ts; import {nextTick, ref} from vue; import type { ESObjectsManager } from earthsdk3; const container refHTMLDivElement | null(null); const objm: ESObjectsManager getobjm() as ESObjectsManager; console.log(objm,objm); objm.viewerCreatedEvent.don(async (viewer: any) { viewer.clickEvent.don((e: any) { viewer.pick(e.screenPosition, customPick).then((res: unknown) { console.log(全局点击事件); }); }); }); nextTick(() { if (!container.value) return; objm.createCesiumViewer(container.value); }) /script template div classcontainer refcontainer/div /template style scoped langscss /styleapp.vuescript setup langts import MapContainer from /views/MapContainer.vue; import {provide} from vue; const props defineProps([objm]); provide(objm, props.objm); /script template MapContainer / /template style scoped/style成功界面二、 实战核心功能模块1、引擎切换#创建Cesium视口 const viewer objm.createCesiumViewer(container.value); #创建UE视口 const options { type: ESUeViewer, container.value, options: { https://earth3d.alink.link:30002, earthsdk3}, } const viewer objm.createUeViewer(options); #引擎切换 const switchEngine (engine) { if (engine cesium) { objm.switchToCesiumViewer({ container: container.value }) } else { objm.switchToUEViewer({ container: container.value, uri: https://earth3d.alink.link:30002, app: earthsdk3 }) } }2、获取ESS里场景的数据# 获取ess登陆凭证 async function fetchEssLoginAuth() { let authorization localStorage.getItem(essAuthorization); if (authorization) return authorization; const urlESS登陆地址; //例如https://earth3d:1111/login try { const response await fetch(url, { method: POST, headers: { Content-Type: application/json, }, body: JSON.stringify({ username: 登陆账号, password: 登陆密码 }) }); const data await response.json(); if (data.status ! ok) { throw new Error(登录失败原因${data.status}); } localStorage.setItem(essAuthorization, data.data); return data.data; } catch (error) { console.error(ESS认证失败:, error); throw error; } } #获取内容 async function fetchEssContent(id) { const authorization await fetchEssLoginAuth(); const urlhttps://earth3d:1111/staticscene/get?id${id} try { const response await fetch(url, { method: GET, headers: { Authorization: ${authorization} } }); const data await response.json(); if (data.status ! ok) { throw new Error(获取场景内容失败原因${data.status}); } const rootChildren data?.data?.content?.sceneTree?.root?.children; if (rootChildren Array.isArray(rootChildren)) { return rootChildren; } } catch (error) { console.error(获取场景内容失败:, error); throw error; } } #返回数据结构 const response[ { children: [], name: 全球影像, sceneObj: { maximumLevel: 18, name: 全球影像, id: 08838491-acba-451e-bf1f-ed4d458f7e38, type: ESImageryLayer, url: https://server.arcgisonline.com/arcgis/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x} } }, { children: [], collapsed: true, name: boundary, sceneObj: { strokeStyle: { color: [ 0.21568627450980393, 0.9294117647058824, 0.03529411764705882, 1 ], material: , width: 5, ground: false, materialParams: {}, widthType: screen }, flyInParam: { flyDuration: 1, rotation: [ 359.99999999999886, -37.20170635557474, 359.99999999999903 ], position: [ 101.18400820868538, 30.28480013892119, 137990.32151619304 ] }, name: boundary, filled: false, id: 7a0ca06f-6054-44c6-848f-51855b2b8cb4, type: ESGeoJson, url: https://gismap.alink.link/3dmapfiles/rangtang/rangtangxian.json } }, { children: [], name: road, sceneObj: { name: road, id: 5c3cc102-a8bc-4bf6-a1c2-fdc9c77e19c6, type: ESImageryLayer, url: https://mapdata.alink.link:6001/indexCia/tms/tilemapresource.xml, zIndex: 1 } }, { children: [], collapsed: true, name: terrain, sceneObj: { name: terrain, id: d11ddc26-7a25-4351-a100-ee0d22283276, type: ESImageryLayer, url: https://mapdata.alink.link:6001/pak/layer.json, zIndex: 2 } }, { children: [ { children: [], collapsed: true, name: 蒲西乡, sceneObj: { name: 蒲西乡, fontSize: 18, id: 36c9a5b1-ba60-427c-9efd-8ad394967c1b, position: [ 101.31096988518799, 31.758069533041372, -0.3554368269705526 ], text: 蒲西乡, type: ESTextLabel } }, { children: [], name: 宗科乡, sceneObj: { name: 宗科乡, fontSize: 18, id: 6bdf556e-ec0b-4d53-9dca-8e514b6a764b, position: [ 101.06397423616474, 31.735457402324926, 0.07002222182983407 ], text: 宗科乡, type: ESTextLabel } }, { children: [], name: 石里乡, sceneObj: { name: 石里乡, fontSize: 18, id: 69e75356-3a34-4b20-9175-6d00ba6cf9e3, position: [ 101.11188855211684, 31.894306648044548, -0.1276350955737183 ], text: 石里乡, type: ESTextLabel } }, { children: [], collapsed: true, name: 吾伊乡, sceneObj: { name: 吾伊乡, fontSize: 18, id: 50e3f588-de57-4b40-b341-3e9d3af1a067, position: [ 101.00075510206388, 32.049643388354454, -0.395072220384877 ], text: 吾伊乡, type: ESTextLabel } }, { children: [], collapsed: true, name: 中壤塘镇, sceneObj: { name: 中壤塘镇, fontSize: 18, id: 65bfbfbe-53bc-47e7-8ab1-0ea63e26fa03, position: [ 101.22475720742355, 32.18666584333736, 0.04699996893255173 ], text: 中壤塘镇, type: ESTextLabel } }, { children: [], name: 上壤塘乡, sceneObj: { name: 上壤塘乡, fontSize: 18, id: f96054b5-40ae-4f06-810a-a484bbd2a468, position: [ 101.3462915036391, 32.24639772361728, 0.14415805983716212 ], text: 上壤塘乡, type: ESTextLabel } }, { children: [], collapsed: true, name: 尕多乡, sceneObj: { name: 尕多乡, fontSize: 18, id: 4fd1ba95-41f9-4c64-add2-0f63e9f9a3ac, position: [ 101.14077716155053, 32.39338163805662, -0.15134801435379808 ], text: 尕多乡, type: ESTextLabel } }, { children: [], name: 南木达镇, sceneObj: { name: 南木达镇, fontSize: 18, id: facf3d08-f1bb-4d0c-a75b-6be49850cfa2, position: [ 101.02613744401968, 32.410023040030836, -0.22438337926771446 ], text: 南木达镇, type: ESTextLabel } }, { children: [], name: 茸木达乡, sceneObj: { name: 茸木达乡, fontSize: 18, id: b523be1e-be6d-4417-a505-efcdbf155825, position: [ 101.04638705887756, 32.51416480265879, 0.045464148752334864 ], text: 茸木达乡, type: ESTextLabel } }, { children: [], collapsed: true, name: 上杜柯乡, sceneObj: { name: 上杜柯乡, fontSize: 18, id: d074f6cd-cb95-4b22-a95f-95fb0823ad63, position: [ 100.74553605818096, 32.453110335930056, -0.2785384109580931 ], text: 上杜柯乡, type: ESTextLabel } }, { children: [], collapsed: true, name: 岗木达镇, sceneObj: { name: 岗木达镇, fontSize: 18, id: b538b18b-d8aa-4632-bc78-496b9e3ce4a6, position: [ 100.88045514041491, 32.23970992447721, -0.10215226380875761 ], text: 岗木达镇, type: ESTextLabel } } ], collapsed: true, name: boundaryName }, { children: [ { children: [], collapsed: true, name: 测试, sceneObj: { mode: SquareV03, socketName: 站点1, name: 测试, style: { textBoxAlign: start, textOffset: [ 10, 2 ], textBoxShow: true, fontSize: 20, textBoxMode: default, imageSize: [ 60, 60 ], textBackgroundSize: [ 120, 30 ], textBackgroundColor: [ 1, 1, 1, 0 ], textColor: [ 1, 1, 1, 1 ], textBoxOffset: [ 40, 0 ] }, id: e79ece31-eebb-4eff-877f-211e3a7e7070, allowPicking: true, position: [ 100.78260332965166, 32.1923681024918, -0.0021763699037819872 ], actorTag: 61f09fa0-2820-11f1-a1d0-a736ecedf777, type: ESPoi2D } } ], name: sites } ]3、场景对象创建与销毁他们有官方示例大家可自行参考我是根据实际开发需求总结的,无论是添加影像、地形、3DTileset以及各种类型的标记等等#创建方式一、拿到数据里的sceneObj根据createSceneObjectFromJson创建对象 const createSceneObjectFromJson (name, data) { if (data.length 0) return; if (!objm.savedSceenObjects[name]) { objm.savedSceenObjects[name] []; } for (let datum of data) { const sceneObject objm.createSceneObjectFromJson(datum.sceneObj); sceneObject.allowPicking true; if (!objm.savedSceenObjects[name]) { objm.savedSceenObjects[name] []; } objm.savedSceenObjects[name].push(sceneObject); sceneObject.pickedEvent.don((e) { console.log(FromJson, e?.sceneObject.json); }) } } 创建方式二、拿到的数据转为GeoJson,使用矢量的方式创建 const formatGeoJsonData (type, data) { let result { type: FeatureCollection, features: [] } data.forEach(item { const sceneObj item?.sceneObj; if (!sceneObj) return; const {position, flyToParam} sceneObj; sceneObj.type type; if (position flyToParam) { sceneObj.flyInParam objm.activeViewer.transformFlyParam(position, flyToParam) } result.features.push({ type: Feature, geometry: { type: Point, coordinates: position }, properties: { sceneObj: sceneObj, name: sceneObj.name, type: type } }) }); return result; } const createSceneObjectFromGeoJson (name, data) { const sceneObject1 objm.createSceneObject(ESGeoJson); sceneObject1.url data; sceneObject1.textProperty name; sceneObject1.allowPicking true; sceneObject1.textFontSize 26; sceneObject1.imageSize [53, 57]; sceneObject1.textOffset [50, -70] sceneObject1.textAnchor [0.5, 1] if (name boundaryName) { sceneObject1.url https://earth3d.alink.link:30002/defaultIcon.png } if (!objm.savedSceenObjects[name]) { objm.savedSceenObjects[name] []; } objm.savedSceenObjects[name].push(sceneObject1); sceneObject1.pickedEvent.don((e) { console.log(FromGeo, e.geojsonPickInfo) }) } #销毁对象 objm.destroySceneObject(sceneObject) #对象显示隐藏 const visibletrue/false; sceneObject.show visible;三、 高频“踩坑”与解决方案