功能解析
在水利、水务数字孪生三维项目中,可能会遇到前端需要实现闸口开闸放水的功能需求。那我一同来看看如何实现开闸放水这个功能吧~~~
数据准备层面:
- 需要将水闸与闸门进行分离,分开两个模型进行发布(便于后续闸门的位移)
前端实现层面:
- 闸门向上升起移动;
- 创建粒子效果(更好模拟水流冲击效果);
- 水流体仿真实现放水。
我这里提前准备好了对应的工程以及代码,大家可以下载下来实践一下噢
❝
专题资源链接(包含数据、代码)下载:
该代码为html,需要将lib/aircity下的SDK替换成本地Cloud SDK就可以直接在浏览器中进行预览啦~~~
(代码学习省流版)- 查看完整代码tab
替换SDK步骤:
- Cloud SDK获取方式:将SDK文件夹的ac.min.js进行复制;
- 将代码/开闸放水/lib/aircity的ac.min.js替换成SDK的ac.min.js。
实现步骤
我们根据上面流程进行详细拆解剖析。
闸门向上升起移动
获取闸门信息,绑定闸门对应信息。(涉及对象infoTree)
图层名称与id绑定;
记录闸门的初始位置,方便后续重置闸门。
记录闸门的开启位置,方便后续移动闸门。
闸门进行运动动画(涉及对象TileLayer)
- 让闸门可以从起点位移到终点,其中位移速度可控。(涉及对象tileLayer)
创建粒子效果
创建粒子效果方式有两种:explorer提前创建好通过控制图层显隐、通过代码使用对象CustomObject进行创建。 这里呢,给大家减少负担~通过控制图层显隐来创建。
- explorer主页-偏好设置-本地资源目录挂载pak;
- ‘资源库-粒子-水’选择合适粒子将其拖入至场景中;
- 提前在explorer将水花粒子提前创建好,以文件夹将每个闸门水花粒子进行管理,并给文件夹命名进行命名;
- 控制水花粒子的显示。(涉及对象infoTree)
水流体仿真实现放水
- 拾取出水点位置(ps:该点不要被模型遮挡);
- 添加水流体出水点配置信息(涉及对象fluid);
- 激活出水点(涉及对象fluid)
粒子需要在Explorer提前加载好:
从以上步骤可以分析需要的全局参数
-
状态参数
- 闸门是否正在位移-moveState-Boolean:如果闸门正在位移,就禁止其他时间,防止交互阻塞
-
效果参数
- 动画帧数-fps-Number:唯一的动画需要多少帧完成,用于控制动画的平滑度,dts都是异步操作,直接用速度会把握不准,所以使用帧来代替速度
- 闸门升起的目标高度-risingHeight-Number:用于表示闸门需要升起的高度。
-
对象
- infotreeObj:用于存储图层的必要信息
- gateLocation:用于存储闸门位置
- controlGate:控制打开闸门的序列
- fluidStyle:流体样式,共28种水样式,取值范围:[0~27]
-
对象配置
-
gate:闸门名称
-
fluidSources:出水点点位对象,用于存储出水点相关信息
- coordinate:出水点位置,用于计算出水点范围(出水点包围盒);
- velocity: 出水点流速、流向方位设置;
- bbox_length: 用于设置出水点包围盒宽高范围(此值大小影响水流体速度,值越大流速越大);
- bbox_height: 用于设置出水点包围盒高度范围(此值大小影响水流体速度,值越大流速越大);
- shape:出水点形状,0矩形出水点,1圆形出水点;
- duration:出水点仿真执行时间。单位:秒,即一直执行,大于0则按时间执行
-
particle:闸门对应粒子文件夹名称
-
具体实现
开闸放水步骤拆解
1. 前置:定义全局参数,绑定图层信息
使用飞渡api的时候,传参都是用的id,但图层的id普遍都是随机生成的,没有实际的概念。所以我们一般会用图层名称去绑定id,一是让代码的可读性更高,二是方便后续维护。需要数组,存储对应的图层名称,这里的图层名称【gate】要和Explorer工程的名称对应,并且要从低到高排列,方便后续判断。
再创建一个infotreeObj对象,用于存储名称与id的对应关系,方便接口调用。
配置项存储了相关对应的变量数据。
const infotreeObj = {},
fps = 8, // 动画帧数:控制动画的平滑度
risingHeight = 2 // 闸门升起的目标高度
let moveState = false // 闸门是否正在位移
/**
* 系统配置
*/
const CONFIG = [
{
gate: '闸门01',
fluidSource: {
coordinate: [-69, 52.987934570312504, -3.84681884765625],
velocity: [-1, 0], // 可选参数 uv流向
bbox_length: 2, // 可选参数 出水点包围盒宽高
bbox_height: 2, // 可选参数 出水点高度
shape: 0, // 可选参数 取值范围:[0,1],0矩形出水点,1圆形出水点
duration: -1 // 可选参数 出水点仿真执行时间,单位:秒,默认值:-1,即一直执行,大于0则按时间执行
},
particle: '闸门01_粒子'
},
{
gate: '闸门02',
fluidSource: {
coordinate: [-69, 45.470498046875, -3.8468170166015625],
velocity: [-1, 0]
},
particle: '闸门02_粒子'
},
{
gate: '闸门03',
fluidSource: {
coordinate: [-69, 37.9300830078125, -3.8468157958984377],
velocity: [-1, 0]
},
particle: '闸门03_粒子'
},
{
gate: '闸门04',
fluidSource: {
coordinate: [-69, 28.81292724609375, -3.8468145751953124],
velocity: [-1, 0]
},
particle: '闸门04_粒子'
}
]
let fluidStyle = 6 // 水流体样式,共28种水样式
const gateLocation = {} // 闸门位置
const controlGate = CONFIG.map((item, index) => index) // 控制打开的闸门序列,默认全开
在场景初始化后,为了清除上个用户执行的操作,需要使用fdapi.reset()
重置一下视频流。再通过fdapi.infoTree.get()
获取图层树信息。获取的信息如下:
{
"infotree":[
{"iD":"5D6728F44BEAA533268D6EA04ADC368F","index":119,"parentIndex":118,"name":"L1-4","visiblity":true,"type":"EPT_Scene"},
{"iD":"70EEA6304541BFB27E6B618878888B40","index":120,"parentIndex":118,"name":"L5","visiblity":true,"type":"EPT_Scene"},{"iD":"261F27DE488D97B44A02269B1822B71D","index":121,"parentIndex":118,"name":"L6","visiblity":true,"type":"EPT_Scene"}]
}
在初始化的过程中,使用fdapi.tileLayer.get()
通过循环检索闸门相关信息,包括获取当前位置以及存储位移后位置。
/**
* 初始化场景
*/
function initScene() {
new DigitalTwinPlayer(HostConfig.Player, {
domId: 'player',
apiOptions: {
onReady: async () => {
fdapi.reset(1 | 2 | 4)
const { infotree } = await fdapi.infoTree.get()
infotree.forEach(item => {
infotreeObj[item.name] = item.iD
})
// 获取闸门详细信息。主要获取当前位置
const gateIdList = CONFIG.map(item => infotreeObj[item.gate])
const { data: gates } = await fdapi.tileLayer.get(gateIdList)
// 存储每个图层对应的初始化位置与位移后位置
gates.forEach((item, index) => {
const name = CONFIG[index].gate
if (!gateLocation[name]) gateLocation[name] = {}
gateLocation[name].init = item.location
gateLocation[name].open = [ item.location[0],
item.location[1],
item.location[2] + risingHeight
]
})
}
}
})
}
2. 闸门位移
在DTS中实现图层的位移需要使用fdapi.tileLayer.setLocation()
,但是只能瞬间移动,不能做到平滑移动。 这里引用一个方法,通过两个点的坐标以及分段数,计算两个点之间的的点位坐标。有了这个方法我们就在两个点之间有了非常多的点位,可以通过fdapi.tileLayer.setLocation()
多次移动,做到平滑位移了。如下所示:
// 传参示例
getLineSegmentPoint([[0,0,20],[100,0,20]],20);
/**
* 线段分点
*/
function getLineSegmentPoint(lineSegment, interval) {
try {
if (interval && lineSegment && lineSegment.length === 2) {
const point1 = lineSegment[0]
const point2 = lineSegment[1]
const a = point2[1] - point1[1]
const b = point2[0] - point1[0]
const c = point2[2] - point1[2]
const o = Math.sqrt(
Math.pow(a, 2) + Math.pow(b, 2) + Math.pow(c, 2)
)
const n = o / interval
const p = []
for (let i = 1; i <= interval; i++) {
const x = (b / o) * (n * i) + point1[0]
const y = (a / o) * (n * i) + point1[1]
const z = (c / o) * (n * i) + point1[2]
p.push([x, y, z])
}
return p
} else {
console.error('线段取点失败', lineSegment, interval)
}
} catch (error) {
console.error('线段取点失败', error)
}
}
平常用其他系统大多数都是同步逻辑,在dts中api操作都是异步的,所以这里移动不需要写定时器,直接循环调用就可以做到逐帧调用代码。
具体代码如下:
const fps = 8, // 动画帧数:控制动画的平滑度
// 传参示例
setLocation({id:'123456',location:[100,0,20]});
/**
* 模型位移,从模型当前位置位移到指定位置
* pamras arr {id:string,location:number[]}
*/
const setLocation = async arr => {
// 设置移动状态为true
moveState = true
// 获取模型当前位置-通过tileLayer.get 获取模型信息
const idList = []
for (let index in arr) idList.push(arr[index].id)
const { data } = await fdapi.tileLayer.get(idList)
// 获取根据起点和终点,计算路径点
for (let index in arr) {
const startLocation = data[index].location
const endLocation = JSON.parse(JSON.stringify(arr[index].location))
arr[index].pathPoints = getLineSegmentPoint(
[startLocation, endLocation],
fps
)
}
// 每一帧根据路径点移动一次模型
for (let f = 0; f <= fps - 1; f++) {
for (let index in arr)
fdapi.tileLayer.setLocation(
arr[index].id,
arr[index].pathPoints[f],
null
)
if (f == fps - 1) moveState = false
}
}
从上图看到四个闸门还是有依次到达,实际想要的效果是让这几个图层,在上升的时候,是同步进行上升的。这时候我们可以用到updateBegin()
与updateEnd()
方法。在开始修改之前调用fdapi.tileLayer.updateBegin()
,然后可以多次调用fdapi.tileLayer.setLocation()
方法,最后调用fdapi.tileLayer.updateEnd()
提交修改更新数据。这样可以让两个方法中间的位移操作变成同步。
最终改造后的代码如下:
/**
* 模型位移,从模型当前位置位移到指定位置
* pamras arr {id:string,location:number[]}
*/
const setLocation = async arr => {
// 设置移动状态为true
moveState = true
// 获取模型当前位置-通过tileLayer.get 获取模型信息
const idList = []
for (let index in arr) idList.push(arr[index].id)
const { data } = await fdapi.tileLayer.get(idList)
// 获取根据起点和终点,计算路径点
for (let index in arr) {
const startLocation = data[index].location
const endLocation = JSON.parse(JSON.stringify(arr[index].location))
arr[index].pathPoints = getLineSegmentPoint(
[startLocation, endLocation],
fps
)
}
// 每一帧根据路径点移动一次模型
for (let f = 0; f <= fps - 1; f++) {
fdapi.tileLayer.updateBegin()
for (let index in arr)
fdapi.tileLayer.setLocation(
arr[index].id,
arr[index].pathPoints[f],
null
)
await fdapi.tileLayer.updateEnd()
if (f == fps - 1) moveState = false
}
}
3. 触发闸门位移
根据传入的对应的闸门的序列进行实现对应闸门序列的闸门位移,其中位移数组在初始化中已经对对应的位移数组进行储存。
async function startWaterRelease(arr = controlGate) {
if (moveState) return
// 构造 setLocationList id,对应结束的位置
const setLocationList = [] // 闸门位移数组
arr.forEach(index => {
const item = CONFIG[index]
// 闸门位移数组
setLocationList.push({
id: infotreeObj[item.gate],
location: gateLocation[item.gate].open
})
})
// 闸门开始位移
setLocation(setLocationList)
}
4. 粒子的显示
水花粒子是在explorer提前加载的,使用fdapi.infoTree
进行控制。在开闸门之后,对水花粒子使用fdapi.infoTree.show()
进行显示。
// 显示所有的粒子效果
await fdapi.infoTree.show(arr.map(index => infotreeObj[CONFIG[index].particle]))
将上述代码,写入到触发闸门位移方法中。如下所示:
async function startWaterRelease(arr = controlGate) {
if (moveState) return
// 构造 setLocationList id,对应结束的位置
const setLocationList = [] // 闸门位移数组
arr.forEach(index => {
const item = CONFIG[index]
// 闸门位移数组
setLocationList.push({
id: infotreeObj[item.gate],
location: gateLocation[item.gate].open
})
})
// 闸门开始位移
setLocation(setLocationList)
await fdapi.infoTree.show(arr.map(index => infotreeObj[CONFIG[index].particle]))
}
5. 水流体仿真
添加了水花粒子之后,需要使用fluid
进行模拟流体仿真。首先我们需要在场景中进行拾取出水点位,推荐闸门前位置,且位置上面无模型遮挡。item.fluidSource
用于存储对应出水点相关信息,其中coordinate
是出水点位置,使用出水点位置主要用于计算出水点的包围盒范围,velocity
代表水流体的流速流向,duration
代表水点仿真执行时间,这些参数根据实际项目进行调节。
onst fluidSources = [] // 水流体出水口
arr.forEach(index => {
const item = CONFIG[index]
// 构造出水点信息对象
const [x, y, h] = item.fluidSource.coordinate
let length = item.fluidSource.bbox_length || 2
let height = item.fluidSource.bbox_height || 2
fluidSources.push({
id: `fluid_${item.gate}`,
bbox: [x - length, y - length, h, x + length, y + length, h + height],
velocity: item.fluidSource.velocity || [0, 0],
shape: item.fluidSource.shape || 0,
duration: item.fluidSource.duration || -1
})
})
获取了对应的出水点信息,我们这里就需要进行创建对应的流体仿真对象。
在创建前我们需要获取整个出水点的包围盒,首先需要在多个出水点中计算一个中间值,使用getPointsCenter()
进行计算;然后根据这个中间值来设置对应的包围盒,使用getBboxByPoints()
进行获取。
/**
* 计算水流体中点
* pamras points 点坐标数组
*/
function getPointsCenter(points) {
var point_num = points.length // 坐标点个数
if (point_num === 0) return null // 如果没有点,返回null
// 初始化累加器
var X = 0,
Y = 0,
Z = 0,
H = 0 // X, Y, Z 用于计算经纬度中心,H 用于累加高度
for (let i = 0; i < point_num; i++) {
let [lat, lng, height] = points[i] // 解构数组元素,得到纬度、经度和高度
lat = (parseFloat(lat) * Math.PI) / 180 // 将纬度转换为弧度
lng = (parseFloat(lng) * Math.PI) / 180 // 将经度转换为弧度
// 将经纬度转换为三维笛卡尔坐标
var x = Math.cos(lat) * Math.cos(lng)
var y = Math.cos(lat) * Math.sin(lng)
var z = Math.sin(lat)
// 累加每个点的 x, y, z 值
X += x
Y += y
Z += z
// 累加高度值(注意:这里只是简单地累加,并没有进行平均或其他处理)
H += parseFloat(height)
}
// 计算经纬度的平均值
X = X / point_num
Y = Y / point_num
Z = Z / point_num
// 反转换回经纬度
var centerLng = Math.atan2(Y, X)
var centerLat = Math.atan2(Z, Math.sqrt(X * X + Y * Y))
// 将弧度转换回度数
centerLng = (centerLng * 180) / Math.PI
centerLat = (centerLat * 180) / Math.PI
// 计算平均高度(如果需要的话)
var averageHeight = H / point_num
// 返回结果:中心点的经纬度(度数)和平均高度(如果需要)
// 注意:这里的平均高度可能并没有实际意义,因为它没有考虑地形等因素
return [centerLat, centerLng, averageHeight]
}
/**
* 计算水流体bbox
* pamras points 点坐标数组
* pamras length bbox长宽
* pamras height 垂直高度
*/
function getBboxByPoints(points, length = 500, height = 100) {
const point = getPointsCenter(points)
return [
point[0] - length,
point[1] - length,
-height,
point[0] + length,
point[1] + length,
height
]
}
通过上述两个方法就可以实现获取所有出水点的包围盒,结合之前的出水点信息就可以通过fdapi.fluid.add()
实现水流体的添加
await fdapi.fluid.add({
id: 'fluid',
bbox: getBboxByPoints(CONFIG.map(item => item.ffiud_coordinate)),
style: fluidStyle,
sources: fluidSources
})
添加完水流体之后,还需要对水流体实现激活,才可以实现水流效果,通过fdapi.fluid.continueSource()
实现。
// 激活出水点
await fdapi.fluid.continueSource({
id: 'fluid',
sources: fluidSources.map(s => {
return { id: s.id, active: true }
})
})
完整触发开闸放水
完整的触发开闸放水代码剖析完了,放上对应的触发开闸放水功能的代码。
/**
* 开闸放水
*/
async function startWaterRelease(arr = controlGate) {
if (moveState) return
// 构造 setLocationList id,对应结束的位置
const setLocationList = [], // 闸门位移数组
fluidSources = [] // 水流体出水口
arr.forEach(index => {
const item = CONFIG[index]
// 闸门位移数组
setLocationList.push({
id: infotreeObj[item.gate],
location: gateLocation[item.gate].open
})
// 构造出水点信息对象
const [x, y, h] = item.fluidSource.coordinate
let length = item.fluidSource.bbox_length || 2
let height = item.fluidSource.bbox_height || 2
fluidSources.push({
id: `fluid_${item.gate}`,
bbox: [
x - length,
y - length,
h,
x + length,
y + length,
h + height
],
velocity: item.fluidSource.velocity || [0, 0],
shape: item.fluidSource.shape || 0,
duration: item.fluidSource.duration || -1
})
console.log(fluidSources)
})
// 闸门开始位移
setLocation(setLocationList)
// 显示所有的粒子效果
await fdapi.infoTree.show(
arr.map(index => infotreeObj[CONFIG[index].particle])
)
// 添加水流体
await fdapi.fluid.add({
id: 'fluid',
bbox: getBboxByPoints(
CONFIG.map(item => item.fluidSource.coordinate)
),
style: fluidStyle,
sources: fluidSources
})
// 激活出水点
await fdapi.fluid.continueSource({
id: 'fluid',
sources: fluidSources.map(s => {
return { id: s.id, active: true }
})
})
}
关闸
- 将闸门恢复到原始位置;
- 水花粒子隐藏,使用fdapi.infoTree.hide()实现水花粒子的隐藏;
- 流体进行停止,使用fdapi.fluid.stopSource()将水流体进行停止。
/**
* 关闸
*/
async function closeGates(arr = controlGate) {
if (moveState) return
const setLocationList = [], // 闸门位移数组
activateSources = [] // 暂停出水点
arr.forEach(index => {
const item = CONFIG[index]
// 闸门位移数组
setLocationList.push({
id: infotreeObj[item.gate],
location: gateLocation[item.gate].init
})
activateSources.push({
id: `fluid_${item.gate}`,
active: false
})
})
setLocation(setLocationList)
fdapi.fluid.stopSource({
id: 'fluid',
sources: activateSources
})
// 隐藏粒子效果
fdapi.infoTree.hide(
arr.map(index => infotreeObj[CONFIG[index].particle])
)
}
完整代码
该代码是完整的代码,包含开闸放水、关闸、重置。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>开闸放水</title>
<link rel="stylesheet" href="./lib/styles.css" />
<script src="./lib/aircity/ac_conf.js"></script>
<script src="./lib/aircity/ac.min.js"></script>
</head>
<body>
<div id="player"></div>
<div class="btn-list">
<div onclick="startWaterRelease()" class="btn">开闸放水</div>
<div onclick="closeGates()" class="btn">关闸</div>
<div onclick="resetAll()" class="btn">重置</div>
</div>
<script>
const infotreeObj = {},
fps = 8, // 动画帧数:控制动画的平滑度
risingHeight = 2 // 闸门升起的目标高度
let moveState = false // 闸门是否正在位移
/**
* 系统配置
*/
const CONFIG = [
{
gate: '闸门01',
fluidSource: {
coordinate: [-69, 52.987934570312504, -3.84681884765625],
velocity: [-1, 0], // 可选参数 uv流向
bbox_length: 2, // 可选参数 出水点包围盒宽高
bbox_height: 2, // 可选参数 出水点高度
shape: 0, // 可选参数 取值范围:[0,1],0矩形出水点,1圆形出水点
duration: -1 // 可选参数 出水点仿真执行时间,单位:秒,默认值:-1,即一直执行,大于0则按时间执行
},
particle: '闸门01_粒子'
},
{
gate: '闸门02',
fluidSource: {
coordinate: [-69, 45.470498046875, -3.8468170166015625],
velocity: [-1, 0]
},
particle: '闸门02_粒子'
},
{
gate: '闸门03',
fluidSource: {
coordinate: [-69, 37.9300830078125, -3.8468157958984377],
velocity: [-1, 0]
},
particle: '闸门03_粒子'
},
{
gate: '闸门04',
fluidSource: {
coordinate: [-69, 28.81292724609375, -3.8468145751953124],
velocity: [-1, 0]
},
particle: '闸门04_粒子'
}
]
let fluidStyle = 6 // 水流体样式,共28种水样式
const gateLocation = {} // 闸门位置
const controlGate = CONFIG.map((item, index) => index) // 控制打开的闸门序列,默认全开
/**
* 初始化场景
*/
function initScene() {
new DigitalTwinPlayer(HostConfig.Player, {
domId: 'player',
apiOptions: {
onReady: async () => {
fdapi.reset(1 | 2 | 4)
const { infotree } = await fdapi.infoTree.get()
infotree.forEach(item => {
infotreeObj[item.name] = item.iD
})
// 获取闸门详细信息。主要获取当前位置
const gateIdList = CONFIG.map(item => infotreeObj[item.gate])
const { data: gates } = await fdapi.tileLayer.get(gateIdList)
// 存储每个图层对应的初始化位置与位移后位置
gates.forEach((item, index) => {
const name = CONFIG[index].gate
if (!gateLocation[name]) gateLocation[name] = {}
gateLocation[name].init = item.location
gateLocation[name].open = [
item.location[0],
item.location[1],
item.location[2] + risingHeight
]
})
}
}
})
}
/**
* 线段分点
*/
function getLineSegmentPoint(lineSegment, interval) {
try {
if (interval && lineSegment && lineSegment.length === 2) {
const point1 = lineSegment[0]
const point2 = lineSegment[1]
const a = point2[1] - point1[1]
const b = point2[0] - point1[0]
const c = point2[2] - point1[2]
const o = Math.sqrt(
Math.pow(a, 2) + Math.pow(b, 2) + Math.pow(c, 2)
)
const n = o / interval
const p = []
for (let i = 1; i <= interval; i++) {
const x = (b / o) * (n * i) + point1[0]
const y = (a / o) * (n * i) + point1[1]
const z = (c / o) * (n * i) + point1[2]
p.push([x, y, z])
}
return p
} else {
console.error('线段取点失败', lineSegment, interval)
}
} catch (error) {
console.error('线段取点失败', error)
}
}
/**
* 模型位移,从模型当前位置位移到指定位置
* pamras arr {id:string,location:number[]}
*/
const setLocation = async arr => {
// 设置移动状态为true
moveState = true
// 获取模型当前位置-通过tileLayer.get 获取模型信息
const idList = []
for (let index in arr) idList.push(arr[index].id)
const { data } = await fdapi.tileLayer.get(idList)
// 获取根据起点和终点,计算路径点
for (let index in arr) {
const startLocation = data[index].location
const endLocation = JSON.parse(JSON.stringify(arr[index].location))
arr[index].pathPoints = getLineSegmentPoint(
[startLocation, endLocation],
fps
)
}
// 每一帧根据路径点移动一次模型
for (let f = 0; f <= fps - 1; f++) {
fdapi.tileLayer.updateBegin()
for (let index in arr)
fdapi.tileLayer.setLocation(
arr[index].id,
arr[index].pathPoints[f],
null
)
await fdapi.tileLayer.updateEnd()
if (f == fps - 1) moveState = false
}
}
/**
* 计算水流体中点
* pamras points 点坐标数组
*/
function getPointsCenter(points) {
var point_num = points.length // 坐标点个数
if (point_num === 0) return null // 如果没有点,返回null
// 初始化累加器
var X = 0,
Y = 0,
Z = 0,
H = 0 // X, Y, Z 用于计算经纬度中心,H 用于累加高度
for (let i = 0; i < point_num; i++) {
let [lat, lng, height] = points[i] // 解构数组元素,得到纬度、经度和高度
lat = (parseFloat(lat) * Math.PI) / 180 // 将纬度转换为弧度
lng = (parseFloat(lng) * Math.PI) / 180 // 将经度转换为弧度
// 将经纬度转换为三维笛卡尔坐标
var x = Math.cos(lat) * Math.cos(lng)
var y = Math.cos(lat) * Math.sin(lng)
var z = Math.sin(lat)
// 累加每个点的 x, y, z 值
X += x
Y += y
Z += z
// 累加高度值(注意:这里只是简单地累加,并没有进行平均或其他处理)
H += parseFloat(height)
}
// 计算经纬度的平均值
X = X / point_num
Y = Y / point_num
Z = Z / point_num
// 反转换回经纬度
var centerLng = Math.atan2(Y, X)
var centerLat = Math.atan2(Z, Math.sqrt(X * X + Y * Y))
// 将弧度转换回度数
centerLng = (centerLng * 180) / Math.PI
centerLat = (centerLat * 180) / Math.PI
// 计算平均高度(如果需要的话)
var averageHeight = H / point_num
// 返回结果:中心点的经纬度(度数)和平均高度(如果需要)
// 注意:这里的平均高度可能并没有实际意义,因为它没有考虑地形等因素
return [centerLat, centerLng, averageHeight]
}
/**
* 计算水流体bbox
* pamras points 点坐标数组
* pamras length bbox长宽
* pamras height 垂直高度
*/
function getBboxByPoints(points, length = 500, height = 100) {
const point = getPointsCenter(points)
return [
point[0] - length,
point[1] - length,
-height,
point[0] + length,
point[1] + length,
height
]
}
/**
* 开闸放水
*/
async function startWaterRelease(arr = controlGate) {
if (moveState) return
// 构造 setLocationList id,对应结束的位置
const setLocationList = [], // 闸门位移数组
fluidSources = [] // 水流体出水口
arr.forEach(index => {
const item = CONFIG[index]
// 闸门位移数组
setLocationList.push({
id: infotreeObj[item.gate],
location: gateLocation[item.gate].open
})
// 构造出水点信息对象
const [x, y, h] = item.fluidSource.coordinate
let length = item.fluidSource.bbox_length || 2
let height = item.fluidSource.bbox_height || 2
fluidSources.push({
id: `fluid_${item.gate}`,
bbox: [
x - length,
y - length,
h,
x + length,
y + length,
h + height
],
velocity: item.fluidSource.velocity || [0, 0],
shape: item.fluidSource.shape || 0,
duration: item.fluidSource.duration || -1
})
console.log(fluidSources)
})
// 闸门开始位移
setLocation(setLocationList)
// 显示所有的粒子效果
await fdapi.infoTree.show(
arr.map(index => infotreeObj[CONFIG[index].particle])
)
// 添加水流体
await fdapi.fluid.add({
id: 'fluid',
bbox: getBboxByPoints(
CONFIG.map(item => item.fluidSource.coordinate)
),
style: fluidStyle,
sources: fluidSources
})
// 激活出水点
await fdapi.fluid.continueSource({
id: 'fluid',
sources: fluidSources.map(s => {
return { id: s.id, active: true }
})
})
}
/**
* 关闸
*/
async function closeGates(arr = controlGate) {
if (moveState) return
const setLocationList = [], // 闸门位移数组
activateSources = [] // 暂停出水点
arr.forEach(index => {
const item = CONFIG[index]
// 闸门位移数组
setLocationList.push({
id: infotreeObj[item.gate],
location: gateLocation[item.gate].init
})
activateSources.push({
id: `fluid_${item.gate}`,
active: false
})
})
setLocation(setLocationList)
fdapi.fluid.stopSource({
id: 'fluid',
sources: activateSources
})
// 隐藏粒子效果
fdapi.infoTree.hide(
arr.map(index => infotreeObj[CONFIG[index].particle])
)
}
/**
* 重置所有状态
*/
async function resetAll() {
if (moveState) return
await fdapi.infoTree.hide(
CONFIG.map(item => infotreeObj[item.particle])
)
fdapi.fluid.delete('fluid')
// 立即重置闸门位置
fdapi.tileLayer.updateBegin()
for (let index in CONFIG) {
fdapi.tileLayer.setLocation(
infotreeObj[CONFIG[index].gate],
gateLocation[CONFIG[index].gate].init
)
}
fdapi.tileLayer.updateEnd()
}
// 窗口自适应
function onResize() {
const player = document.getElementById('player')
player.style.width = window.innerWidth + 'px'
player.style.height = window.innerHeight + 'px'
}
// 事件监听
window.addEventListener('load', initScene)
window.addEventListener('resize', onResize)
</script>
</body>
</html>
开发小tip
在项目开发中,可以直接复用代码哟~只需要修改对应配置项,就可以轻松实现前端解决开闸放水难题啦~~~