# Ciyon Web3D 应用开发指南
## 概述
本文档为AI开发3D地图应用提供指导,重点讲解如何基于 `cemap.html` 进行二次开发,实现资产点击交互和统计图表展示等功能。
## 技术栈
### 前端
- **核心引擎**: Cesium.js (基于WebGL的3D地球可视化引擎)
- **UI框架**: 自定义框架 (ciy.js, ciycmp.js)
- **图表库**: ECharts (可选,用于统计图表)
### 数据库
- MySQL
- **主要数据表**:
- `zc_cemap_data` - 地图配置数据
- `zc_cemap_bill` - 指示牌资源
- `zc_cemap_glb` - GLB模型资源
---
## 资产管理
### 资产说明
系统提供以下资产类型,可通过后台管理界面进行管理:
1. **GLB模型资源** - 3D建筑、设施等模型
- 管理界面: `glb.html`
- 数据表: `zc_cemap_glb`
- 支持路径分类管理
2. **指示牌资源** - 平面图片、标识牌等
- 管理界面: `bill.html`
- 数据表: `zc_cemap_bill`
- 支持路径分类管理
3. **3D Tile资源** - 实景航拍、大规模3D场景
- 管理界面: `ceeditor.html`(地编器)
- 可直接在编辑器中添加和配置
### 地图配置
地图配置存储在 `zc_cemap_data` 表的 `mapjson` 字段中,包含:
- 地图源设置(天地图、高德、腾讯、百度等)
- 3D Tile列表
- Entity列表(GLB模型、指示牌等)
- 飞行点列表
- 地形、遮罩等设置
---
## 核心API - ciyearth4.js
### 1. 初始化地图
```javascript
var ce = new ciyearth();
ce.init({
domid: 'ceContainer', // DOM容器ID或DOM对象
personheight: 1.8, // 眼睛到地面高度
timeline: 0, // 1显示时间轴
ion: 'your-ion-token', // Cesium Ion授权token
sunhourmin: '12:12', // 太阳时间
setin: 3, // 1地球飞入, 2中国飞入, 3直接进入
wmts_source: 1, // 地图源: 1默认, 2纯色, 3天地图, 4高德, 5腾讯, 6百度
wmts_style: 1, // 样式: 1卫星, 2卫星(标注), 3矢量
tiles: [], // 3DTile数组
entitys: [], // Entity数组
flys: [] // 快捷定位点数组
});
```
### 3. Entity管理
#### Entity类型
- `model` - 3D模型(GLB/GLTF)
- `billboard` - 永正面(始终朝向相机)
- `plane` - 墙面(平面贴图)
#### 添加Entity
```javascript
ce.addentity({
type: 'model', // 类型: model/billboard/plane
name: '建筑名称',
bind: 'building_123', // 资产编号,用于关联业务数据
lng: 116.39, // 经度
lat: 39.9, // 纬度
height: 10, // 高度
url: '/path/to/model.glb', // 模型URL
w: 10, // 宽度(米) - plane/billboard
h: 10, // 高度(米) - plane/billboard
sc: 1 // 缩放比例
});
```
#### 添加标记
```javascript
ce.addmarker({
lng: 116.39,
lat: 39.9,
img: '/path/to/icon.png',
width: 20,
height: 27,
autoheight: true
});
```
### 4. 相机控制
#### 飞行到指定位置
```javascript
ce.flyTo({
fd: {x, y, z}, // 目标位置
fo: {h, p, r} // h:heading, p:pitch, r:roll
}, 3); // 时间(秒)
```
#### 设置相机位置
```javascript
ce.setView({
fd: {x, y, z},
fo: {h, p, r}
});
```
#### 获取相机位置
```javascript
var camera = ce.getView();
// 返回: {fd: {x, y, z}, fo: {h, p, r}}
```
### 5. 事件系统
```javascript
ce.ciyevent = function(type, act, data, data2) {
if (type == 'entity') {
if (act == 'leftclick') {
// 左键点击Entity
var entity = data.entity;
var bindId = entity.ciydata.bind; // 获取资产编号
console.log('点击了资产:', bindId);
// 在这里处理资产点击事件
showAssetInfo(bindId);
}
} else if (type == 'tile') {
if (act == 'leftclick') {
// 左键点击3D Tile
var primitive = data.primitive;
console.log('点击了Tile');
}
} else if (type == 'control') {
if (act == 'op') {
// 控制操作中(飞行/行走)
// data: Cartographic相机位置
updateCameraInfo(data);
}
}
};
```
### 6. 飞行/行走模式
```javascript
// 开启飞行模式
ce.control_fly_open(false); // 飞行
ce.control_fly_open(true); // 行走
// 关闭飞行模式
ce.control_fly_close();
```
**键盘控制**:
- `W` - 前进
- `S` - 后退
- `A` - 左移
- `D` - 右移
- `Q` - 上升
- `E` - 下降
- `Z` - 左转
- `X` - 右转
- `空格` - 跳跃(行走模式)
- `F` - 减速
- `R` - 加速
- `ESC` - 退出飞行模式
### 7. 地形管理
```javascript
// 添加地形
ce.add_terrain('url', {
url: 'https://example.com/terrain',
requestVertexNormals: true,
requestWaterMask: true
});
// 获取高度
var height = await ce.getposheight(cartesian3);
var floorHeight = ce.getfloorheight();
```
---
## 地图应用开发
### 基础页面结构
```html
3D地图应用
```
---
## 事件交互
### Entity点击事件
```javascript
ce.ciyevent = function(type, act, data, data2) {
if (type == 'entity' && act == 'leftclick') {
var entity = data.entity;
// 获取Entity信息
var entityInfo = {
id: entity.id,
name: entity.name,
type: entity.ciytype,
bind: entity.ciydata.bind, // 资产编号
position: entity.position._value,
customData: entity.ciydata // 自定义数据
};
console.log('点击Entity:', entityInfo);
// 处理点击事件
handleEntityClick(entityInfo);
}
};
```
### Tile点击事件
```javascript
if (type == 'tile' && act == 'leftclick') {
var primitive = data.primitive;
console.log('点击3D Tile:', primitive);
// 可以通过拾取获取Tile中的具体建筑信息
// 这需要3D Tile数据中包含属性信息
}
```
### 自定义点击事件
```javascript
// 添加自定义点击处理器
var handler = new Cesium.ScreenSpaceEventHandler(ce.viewer.canvas);
handler.setInputAction(function(click) {
var pickedObject = ce.viewer.scene.pick(click.position);
if (Cesium.defined(pickedObject)) {
if (pickedObject.id) {
// 点击了Entity
var entity = pickedObject.id;
console.log('Entity:', entity.name, entity.ciydata);
} else if (pickedObject.primitive) {
// 点击了Primitive(如3D Tile)
console.log('Primitive:', pickedObject.primitive);
}
} else {
// 点击了空白处
console.log('点击空白处');
// 可以关闭浮动面板
$5('#assetPanel').hide();
}
}, Cesium.ScreenSpaceEventType.LEFT_CLICK);
```
---
## 浮动面板
### 创建浮动面板
```javascript
// 左侧面板 - 资产信息
var leftPanel = document.createElement('div');
leftPanel.className = 'float-panel left';
leftPanel.style.cssText = 'top: 80px; left: 20px; width: 320px;';
leftPanel.innerHTML = '资产信息
';
document.body.appendChild(leftPanel);
// 右侧面板 - 统计图表
var rightPanel = document.createElement('div');
rightPanel.className = 'float-panel right';
rightPanel.style.cssText = 'top: 80px; right: 20px; width: 380px;';
rightPanel.innerHTML = '统计数据
';
document.body.appendChild(rightPanel);
// 顶部面板 - 搜索/筛选
var topPanel = document.createElement('div');
topPanel.className = 'float-panel top';
topPanel.innerHTML = '';
document.body.appendChild(topPanel);
// 底部面板 - 快捷操作
var bottomPanel = document.createElement('div');
bottomPanel.className = 'float-panel bottom';
bottomPanel.innerHTML = '';
document.body.appendChild(bottomPanel);
```
### 面板样式
```css
.float-panel {
position: absolute;
background: rgba(255, 255, 255, 0.95);
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
padding: 15px;
z-index: 1000;
transition: all 0.3s ease;
}
.float-panel.top {
top: 20px;
left: 50%;
transform: translateX(-50%);
}
.float-panel.left {
top: 80px;
left: 20px;
width: 320px;
max-height: calc(100vh - 100px);
overflow-y: auto;
}
.float-panel.right {
top: 80px;
right: 20px;
width: 380px;
max-height: calc(100vh - 100px);
overflow-y: auto;
}
.float-panel.bottom {
bottom: 20px;
left: 50%;
transform: translateX(-50%);
}
/* 面板折叠状态 */
.float-panel.collapsed {
transform: scale(0);
opacity: 0;
pointer-events: none;
}
```
### 面板显示/隐藏
```javascript
// 显示面板
function showPanel(selector) {
$5(selector).removeClass('collapsed');
}
// 隐藏面板
function hidePanel(selector) {
$5(selector).addClass('collapsed');
}
// 切换面板
function togglePanel(selector) {
$5(selector).toggleClass('collapsed');
}
```
---
## 统计图表
### 使用ECharts渲染图表
```javascript
// 渲染柱状图
function renderBarChart(containerId, data) {
var chart = echarts.init(document.getElementById(containerId));
var option = {
title: { text: data.title || '柱状图', left: 'center', textStyle: {fontSize: 12} },
tooltip: { trigger: 'axis' },
xAxis: { type: 'category', data: data.categories || [] },
yAxis: { type: 'value' },
series: [{
name: data.seriesName || '数值',
type: 'bar',
data: data.values || [],
itemStyle: { color: '#5470c6' }
}]
};
chart.setOption(option);
return chart;
}
// 渲染折线图
function renderLineChart(containerId, data) {
var chart = echarts.init(document.getElementById(containerId));
var option = {
title: { text: data.title || '折线图', left: 'center', textStyle: {fontSize: 12} },
tooltip: { trigger: 'axis' },
xAxis: { type: 'category', data: data.categories || [] },
yAxis: { type: 'value' },
series: [{
name: data.seriesName || '数值',
type: 'line',
data: data.values || [],
smooth: true,
areaStyle: { opacity: 0.3 }
}]
};
chart.setOption(option);
return chart;
}
// 渲染饼图
function renderPieChart(containerId, data) {
var chart = echarts.init(document.getElementById(containerId));
var option = {
title: { text: data.title || '饼图', left: 'center', textStyle: {fontSize: 12} },
tooltip: { trigger: 'item' },
legend: { bottom: 0 },
series: [{
type: 'pie',
radius: '50%',
data: data.values || [],
emphasis: {
itemStyle: {
shadowBlur: 10,
shadowOffsetX: 0,
shadowColor: 'rgba(0, 0, 0, 0.5)'
}
}
}]
};
chart.setOption(option);
return chart;
}
```
### 响应式图表
```javascript
// 监听窗口大小变化
var charts = [];
function addChart(chart) {
charts.push(chart);
}
window.addEventListener('resize', function() {
charts.forEach(function(chart) {
chart.resize();
});
});
```
---
## 完整示例
### 示例:智慧园区管理
```javascript
// 资产数据映射
var assetDataMap = {
'building_001': {
name: 'A栋办公楼',
type: '办公楼',
floors: 18,
area: 12000,
occupancy: 85,
status: '正常',
energy: { today: 4500, month: 120000 },
visits: [120, 200, 150, 80, 70, 110, 130],
timeDist: [20, 150, 280, 90]
},
'building_002': {
name: 'B栋研发中心',
type: '研发中心',
floors: 12,
area: 8000,
occupancy: 92,
status: '正常',
energy: { today: 3800, month: 98000 },
visits: [90, 150, 180, 100, 60, 130, 140],
timeDist: [15, 120, 250, 80]
}
};
// 处理Entity点击
function handleEntityClick(entityInfo) {
var assetId = entityInfo.bind;
if (!assetId || !assetDataMap[assetId]) {
console.log('未知资产:', assetId);
return;
}
var asset = assetDataMap[assetId];
// 显示资产信息面板
showAssetPanel(asset);
// 更新统计图表
updateStatCharts(asset);
// 飞行到资产位置
if (entityInfo.position) {
ce.viewer.flyTo(ce.viewer.entities.getById(entityInfo.id), {
offset: new Cesium.HeadingPitchRange(1.57, -0.6, 50)
});
}
}
// 显示资产信息面板
function showAssetPanel(asset) {
var html = '';
html += '
' + asset.name + '
';
html += '
';
html += '
类型:' + asset.type + '
';
html += '
楼层:' + asset.floors + '层
';
html += '
面积:' + asset.area + '㎡
';
html += '
入住率:' + asset.occupancy + '%
';
html += '
状态:' + asset.status + '
';
html += '
';
html += '
';
html += '
今日能耗:' + asset.energy.today + ' kWh
';
html += '
本月能耗:' + asset.energy.month + ' kWh
';
html += '
';
html += '
';
$5('#assetContent').html(html);
}
// 更新统计图表
function updateStatCharts(asset) {
// 更新访问量趋势图
var visitsChart = echarts.init(document.getElementById('visitsChart'));
visitsChart.setOption({
series: [{ data: asset.visits }]
});
// 更新时段分布图
var timeDistChart = echarts.init(document.getElementById('timeDistChart'));
timeDistChart.setOption({
series: [{ data: asset.timeDist }]
});
}
// 初始化页面
function initPage() {
// 初始化地图
ce.init({
domid: 'ceContainer',
wmts_source: 4,
wmts_style: 3,
setin: 3,
entitys: [
{
type: 'model',
name: 'A栋办公楼',
bind: 'building_001',
lng: 116.39,
lat: 39.9,
height: 0,
url: '/models/building_a.glb'
},
{
type: 'model',
name: 'B栋研发中心',
bind: 'building_002',
lng: 116.40,
lat: 39.9,
height: 0,
url: '/models/building_b.glb'
}
]
});
// 设置事件回调
ce.ciyevent = function(type, act, data) {
if (type == 'entity' && act == 'leftclick') {
var entity = data.entity;
handleEntityClick({
id: entity.id,
bind: entity.ciydata.bind,
position: entity.position._value
});
}
};
}
// 页面加载完成后执行
ciyfn.pageload(initPage);
```
---
## 最佳实践
### 1. 性能优化
```javascript
// 使用requestRenderMode按需渲染
ce.init({
domid: 'ceContainer',
requestRenderMode: true,
maximumRenderTimeChange: Infinity
});
// 距离优化:只加载视野内的Entity
ce.reloadentity(); // 自动优化加载
// 减少Entity数量
// 对于大量相似Entity,考虑使用3D Tile或PrimitiveCollection
```
### 2. 事件处理
```javascript
// 使用节流防止频繁触发
function throttle(func, delay) {
var lastTime = 0;
return function() {
var now = Date.now();
if (now - lastTime >= delay) {
lastTime = now;
func.apply(this, arguments);
}
};
}
// 应用节流
ce.ciyevent = throttle(function(type, act, data) {
// 处理事件
}, 100);
```
### 3. 内存管理
```javascript
// 销毁图表实例
function disposeCharts() {
charts.forEach(function(chart) {
chart.dispose();
});
charts = [];
}
// 页面卸载时清理
window.addEventListener('beforeunload', function() {
disposeCharts();
ce.destroy();
});
```
### 4. 响应式设计
```javascript
// 监听窗口大小变化
window.addEventListener('resize', function() {
// 调整图表大小
charts.forEach(function(chart) {
chart.resize();
});
// 调整面板位置
updatePanelPositions();
});
// 移动端适配
if (ciyfn.inmobile()) {
// 隐藏部分面板
$5('.control-panel').hide();
// 调整面板样式
$5('.float-panel.left').css('width', '80%');
}
```
---
## 常见问题
### 1. 如何获取点击的Entity详细信息?
```javascript
ce.ciyevent = function(type, act, data) {
if (type == 'entity' && act == 'leftclick') {
var entity = data.entity;
console.log('Entity ID:', entity.id);
console.log('Entity Name:', entity.name);
console.log('Entity Type:', entity.ciytype);
console.log('Custom Data:', entity.ciydata);
console.log('Position:', entity.position._value);
}
};
```
### 2. 如何在点击时高亮显示Entity?
```javascript
// 使用轮廓高亮
ce.silhouette.selected = [entity];
// 使用颜色高亮
entity.model.color = Cesium.Color.fromCssColorString('#ffff00');
// 重置高亮
ce.silhouette.selected = [];
entity.model.color = Cesium.Color.WHITE;
```
### 3. 如何加载大量Entity而不影响性能?
```javascript
// 分批加载
var batchSize = 100;
var currentIndex = 0;
function loadBatch() {
var batch = entities.slice(currentIndex, currentIndex + batchSize);
batch.forEach(function(entityData) {
ce.addentity(entityData);
});
currentIndex += batchSize;
if (currentIndex < entities.length) {
setTimeout(loadBatch, 100);
}
}
loadBatch();
```
### 4. 如何实现图层的显示/隐藏?
```javascript
// 给Entity添加分组属性
ce.addentity({
type: 'model',
name: '建筑1',
category: 'building', // 分类
// ...
});
// 控制图层显示
function toggleLayer(category, visible) {
for (var id in ce.entitys) {
var entity = ce.entitys[id];
if (entity.ciydata.category === category) {
entity.show = visible;
}
}
}
// 使用
toggleLayer('building', false); // 隐藏建筑层
toggleLayer('building', true); // 显示建筑层
```
### 5. 如何实现搜索定位功能?
```javascript
// 搜索Entity
function searchEntity(keyword) {
for (var id in ce.entitys) {
var entity = ce.entitys[id];
if (entity.name && entity.name.indexOf(keyword) > -1) {
// 找到匹配的Entity
flyToEntity(id);
return;
}
}
}
// 飞行到Entity
function flyToEntity(entityId) {
var entity = ce.entitys[entityId];
ce.viewer.flyTo(entity, {
offset: new Cesium.HeadingPitchRange(1.57, -0.6, 50)
});
// 高亮显示
ce.silhouette.selected = [entity];
}
```
---
## 参考资料
- Cesium官方文档: https://cesium.com/docs/
- Cesium Ion: https://cesium.com/ion/
- ECharts文档: https://echarts.apache.org/zh/index.html
- 开源作者: 众产®
- 官网: http://ciy.cn/code