文章目录
- 1. 介绍
- 2. 分析
- 2.1. 场景加载
- 2.2. 地形平压
- 2.3. 模型放置
- 2.4. 绘制区域
- 2.5. 查看方案
- 2.6. 数据库的字段
- 2.7. 接口
- 3. 难点
- 3.1. 调整模型的位置
- 3.2. 旋转
1. 介绍
这是一个涉及Cesium.js(一个用于Web的3D地球和地图的JavaScript库)和前后端交互的复杂项目。项目的主要功能包括地形平压、模型放置和调整、方案保存和查看等。
2. 分析
2.1. 场景加载
加载Cesium的Melbourne Photogrammetry
的倾斜摄影作为底图,本身是贴地的,使用Cesium的primitives功能加载特定ID的数据集。
2.2. 地形平压
- 激活画笔工具,用户可在地图上绘制多边形区域。
- 绘制完成后,所选区域的地形将被“平压”,即该区域的地形高度将被统一至某一水平面,视觉效果变为类似水泥的平坦表面。
- 平压操作完成后,系统将显示平压区域的坐标范围以及平压后的高度信息。
2.3. 模型放置
- 预设三维模型库,包括购物商城、L型办公楼、政务大楼、教学楼等,模型默认尺寸用0.001。
- 用户选择模型后,激活画笔工具,在地图上绘制放置点。
- 绘制完成后,系统根据点的坐标将模型放置到对应位置。
- 提供调整按钮,允许用户对已放置的模型进行旋转、位置移动、放大缩小等操作。
- 调整完成后,用户可提交方案,输入方案名称并保存至数据库。
2.4. 绘制区域
- 用户可随时激活画笔工具,重新绘制或修改已存在的平压区域。
- 系统将实时更新绘制区域的视觉效果和相关信息。
2.5. 查看方案
- 用户可通过请求获取已添加的方案列表。
- 点击列表中的方案,系统将跳转到该方案的视角,并移除其他已展示的方案。
- 用户可方案列表中进行删除操作。
- 删除方案时,系统将发送删除请求至服务器,并实时更新方案列表。
2.6. 数据库的字段
id:方案的唯一标识符,自增
name:方案名称
src:模型的存放路径,用于在还原方案时获取模型文件
position:模型的坐标信息,支持存储经纬度+高度或笛卡尔坐标。提交方案时存储的坐标格式,在还原时将保持不变,并不会改变你的数据源
tileHeight:压平高度,不是真实模型的高度,是模型坐标系的高度
heading:模型的角度,用于控制模型的旋转方向
scale:模型的尺寸缩放比例
polygon:压平区域的坐标数据,以GeoJSON格式存储
cameraPosition:相机的笛卡尔坐标,记录提交方案时相机的位置信息
cameraOrt:相机视角,提交方案时候的视角
如果你觉得设计的不是很好,可以手动点击获取相机的视角,放到信息卡片上,调整后再进行提交
create_at:方案的创建时间,自动记录方案添加到数据库的时间
delete_at:方案的删除时间,当方案被删除时自动记录时间戳
2.7. 接口
增加
/api/v1/addScheme
传3个number类型,其他都是字符串。
获取(不需要传id)
/api/v1/getScheme
删除(需要传id)
/api/v1/delScheme
3. 难点
3.1. 调整模型的位置
在3D空间中,调整模型位置时容易改变其高度,导致模型悬浮在空中。改变笛卡尔坐标不好调,不管改了哪个轴,高度会发生改变。
原则:无论怎么调整位置,高度都不能发生改变,否则模型就会悬浮在空中。
解决:将笛卡尔坐标转为经纬度,可以按照上北下南左西右东调整,再转为笛卡尔坐标。
另一个bug:当视角刚好是上北下南左西右东时,是没有问题的,如果视角转了一个角度的话,方位就会打乱。
解决:应该是按照屏幕坐标的像素移动,不管哪个方位,都可以实现上下左右移动。
笛卡尔坐标转屏幕坐标,屏幕坐标再转笛卡尔坐标的实现代码:
// 屏幕转世界坐标
let pick1 = new Cesium.Cartesian2(0,0)
let cartesian = viewer.scene.globe.pick(viewer.camera.getPickRay(pick1),viewer.scene);
// 注意这里的屏幕坐标一定要在球上,否则生成的cartesian对象是undefined
// 世界坐标转屏幕坐标 (笛卡尔坐标高度为0)
Cesium.SceneTransforms.wgs84ToWindowCoordinates(scene,Cartesian3)
// 结果是Cartesian2对象,取出X,Y即为屏幕坐标
3.2. 旋转
鼠标按下去不动一直触发事件,鼠标抬起、按下并释放鼠标和松开鼠标都会取消事件。
实现代码:
app.directive("longpress", {
mounted: function (el, binding, vNode) {
// Make sure expression provided is a function
if (typeof binding.value !== "function" && vNode.context !== undefined) {
// pass warning to console
let warn = `[longpress:] provided expression '${binding.expression}' is not a function, but has to be`;
console.warn(warn);
}
// Define variable
let pressTimer = null;
// Define funtion handlers
// Create timeout ( run function after 1s )
let start = (e) => {
if (e instanceof MouseEvent && e.type === "click" && e.button !== 0) {
return;
}
handler(e);
};
// Cancel Timeout
let cancel = () => {
// Check if timer has a value or not
if (pressTimer !== null) {
clearTimeout(pressTimer);
pressTimer = null;
}
};
// Run Function 按下60毫秒触发
const handler = (e) => {
pressTimer = setTimeout(() => {
binding.value(e);
// 执行递归 调用自身
handler(e);
}, 60);
};
// Add Event listeners
el.addEventListener("mousedown", start);
// Cancel timeouts if this events happen
el.addEventListener("click", cancel);
el.addEventListener("mouseup", cancel);
el.addEventListener("mouseout", cancel);
},
});
参考文档:在 Vue 中定义一个「长按事件」(in TypeScript)