项目源码效果
前阵子测评Mapmost产品,他们赠送了2个项目源码,起初我并没有在意,想着赠品能有多好,因为我在他们官网上看到项目的演示视频,第一感觉一眼假,又拿设计稿渲染视频骗人这是:
官网宣传视频:www.mapmost.com/#/double12/
![]()
但我打开编辑器跑起来后,整体的渲染效果和功能交互做的确实很好,和视频里介绍的一模一样,关键还是基于WebGL技渲染
的,我先放几张图大家看看:
1.初始页面-园区总览
2.园区态势
3.安防管理-分层分户
4.安防管理-室内查看
5.安防管理-安防预警(三维特效)
6.安防管理-逃生路径
7.安防管理-人员聚集(3D热力图)
8.设备管理-监控摄像头
9.设备管理-路灯
今天就带着大家,一点点分析上面项目的实现过程,至于产这个SDK本身怎么用,大家可以去看文档,或者看我之前写的文章。
Mapmost官网 www.mapmost.com/#/
实现过程
我以这个智慧园区
为示例,和大家分析一下实现过程。
一、SDK获取
这里可以参他们考官网上的教程:
他们项目源码里用的是CDN:
二、地图初始化
这块代码在src/components/Map.vue
中,这也是入口文件:
//自定义透明矢量地图样式文件
const style_opacity = {
version: 8,
sources: {},
layers: [
{
id: "land",
type: "background",
paint: {
"background-color": {
stops: [
[15, "rgba(9, 30, 55, 0.6)"],
[16, "rgba(9, 30, 55, 0.6)"],
],
},
},
},
],
};
//初始化地图实例
let map = new mapmost.Map({
container: "map-container",
style: style_opacity,
doubleClickZoom: false,
center: [120.72929672369003, 31.288619767132104],
zoom: 17.88998700147244,
sky: "light", //天空颜色
pitch: 64.42598133276567,
bearing: -37.87271910936988,
userId: "*****", //SDK 授权码
env3D: {
defaultLights: false, //关闭默认光源
envMap: "./assets/data/yun(17).hdr", //环境贴图文件
exposure: 2.64, //场景曝光度
},
});
此时的效果是这样的,黑乎乎一片,只有一块空地图
三、加载场景模型
这里注意的是,整个场景是一个大模型,没有做拆分,包括建筑、道路、树木、路灯等。
这块代码在
src/components/Map.vue
中
// 园区模型
let models_obj = ["yq"].map((item) => ({
type: "glb",
url: "./assets/models/" + item + ".mm",
decryptWasm:
"https://delivery.mapmost.com/cdn/b3dm_codec/0.0.2-alpha/sdk_b3dm_codec_wasm_bg_opt.wasm",
}));
// 模型图层参数
let options = {
id: "model_id124",
models: models_obj,
outline: true, // 允许轮廓高亮
type: "model",
funcRender: function (gl, matrix) {
if (Layer) {
Layer.renderMarker(gl, matrix);
}
},
center: [120.73014920373011, 31.287414975761724, 0.1],
callback: function (group, layer) {
Layer = layer;
// 初始化场景
//new InitApi(map, layer, group);
// 道路行驶车辆
//let car = new CarApi(map);
//car.Car();
//let count = 0;
//setInterval(function () {
//每隔6秒放一次车,放5次
//if (count < 5) {
// car.Car();
// count++;
//}
//}, 10000);
// 获取MapApi接口
window.mapApi = new MapApi(map, layer, group);
},
};
// 添加模型
map.addLayer(options);
现在效果是这样的,整体来看,模型本身做了PBR
处理,金属度挺高的。
四、初始化场景
其实这部分在加载模型的回调函数里已经完成了,上面为了做区分我注释掉了。主要场景灯光和太空盒样式设置,另外把车辆模型单独加载到图层上,让其运动起来
这块代码在
src/components/Map.vue
中
// 初始化场景
new InitApi(map, layer, group);
// 道路行驶车辆
let car = new CarApi(map);
car.Car();
let count = 0;
setInterval(function () {
//每隔6秒放一次车,放5次
if (count < 5) {
car.Car();
count++;
}
}, 10000);
初始化场景的代码在另一个单独的js中,这里是一个类,加了2个平行光
以及设置了天空和大气的样式
这块代码在
src/api/InitApi.js
中
class InitApi {
constructor(map, layer, group) {
this._map = map;
this._layer = layer || null;
this._group = group || null;
this.Sky();
this.Light();
this.allFlash = [];
}
// 灯光
Light() {
// 场景灯光1
let light1 = new mapmost.DirectionalLight({
color: '#484b4b',
intensity: 3.7,
position: [291218.1880671615, -359034.8940693505, 220067.97081694918],
});
// 场景灯光2
let light2 = new mapmost.DirectionalLight({
color: '#767676',
intensity: 0.91,
position: [-291218.1880671615, 359034.8940693505, 220067.97081694918],
});
this._map.addLight(light1);
this._map.addLight(light2);
}
// 天空盒
Sky() {
this._map.addLayer({
id: 'sky',
type: 'mapmost_sky',
enableCloud: true,
paint: {
'sky-url': './assets/images/CubeRT_Capture_Tex_2048.png',
'sky-angle': 0,
'sky-exposure': 1,
'sky-opacity': [
'interpolate',
['linear'],
['zoom'],
0,
0,
5,
0.3,
8,
1,
],
'sky-type': 'atmosphere',
'sky-atmosphere-sun': [227.02725700614292, 110.95561210040023],
'sky-atmosphere-sun-intensity': 5,
},
});
}
}
export default InitApi;
另外车辆的加载一个在一个单图的js文件中处理的,这里代码比较多,我就直接截图了。
车辆运动的实现方式比较简单,这里是预置的固定路线,然后通过匹配不同车辆的模型,让其按照路线运动。
这块代码在
src/api/CarApi.js
中
然后再看一下,场景整体明亮了很多,天空也不再那么昏暗,另外车辆也出现了
五、交互功能
交互功能整体代码都在src/api/MapApi.js
中,这里基本都是用静态数据做的,功能写的比较简单,不过影响不大,我们这里主要看实现方式和代码逻辑,后面自己可以替换为动态数据,以及扩展其他业务功能。
安防预警特效
这里我们注意到,这个预警特效是由3部分组成红色的圆圈
、黄色的扩散圆环
、黄色的旋转扇形
这部分代码在MapApi.js
可以找到,由radarWarning()
、securityWarning()
这2个方法实现
黄色的扩散圆环
和黄色的旋转扇形
一起组成雷达图:
//安防预警雷达
radarWarning() {
let circle_radar = this._layer.addCircle({
type: 'radar',
color: 'yellow',
radius: 45,
segment: 256,
isCW: true,
speed: 3,
opacity: 0.8,
center: [120.7293385335865, 31.288814045160496, 52.85],
});
this.polygonData.push(circle_radar);
let waterRipple = this._layer.addCircle({
type: 'ripple',
color: 'yellow',
radius: 45,
turns: 5,
speed: 0.05,
opacity: 1.0,
center: [120.7293385335865, 31.288814045160496, 52.75],
});
this.polygonData.push(waterRipple);
}
红色的圆圈
单独实现:
// 安防预警
securityWarning() {
let model = this._group.children[0].children[2];
const tween = new TWEEN.Tween({ z: 0 }, false)
.to({ z: 30 }, 1000)
.easing(TWEEN.Easing.Quadratic.InOut)
.onUpdate((val) => {
model.translateY(val.z);
})
.start();
function animate(time) {
tween.update(time);
requestAnimationFrame(animate);
}
animate();
let circle_spread = this._layer.addCircle({
type: 'spread',
color: 'red',
radius: 45,
segment: 256,
speed: 3,
opacity: 0.8,
center: [120.7293385335865, 31.288814045160496, 52.85],
});
this.polygonData.push(circle_spread);
}
这里我看到有个叫addCircle
的API被反复用到,我去他们文档上看了一下,这是模型图层实例下面的方法,根据参数type
的类型来渲染不同的效果。
至于预警通知弹窗的内容,这里用的是图片,没有真实数据,明显是偷懒了
//预警图片
warnPicture() {
let that = this;
let warnPoints = [
{
element: this.createDeviceDom(
'yj',
'./assets/images/UI/弹窗_预警通知.png',
'265.2px',
'289.2px'
),
position: [120.72994108596296, 31.28834145014008, 52.75],
},
];
let pictrue2 = this._layer.addMarker({
id: 'warnSevice',
data: warnPoints,
});
that.markerData.push(pictrue2);
}
我看他们封装了一个专用的createDeviceDom()
方法,用来创建设备弹窗, 参数接受的就是图片:
createDeviceDom(
id,
imageUrl = './assets/images/device.png',
w = '50px',
h = '70px',
name
) {
let container = document.createElement('div');
container.setAttribute('id', id);
container.className = name || 'markerDevice';
container.style.width = w;
container.style.height = h;
container.style.backgroundImage = 'url(' + imageUrl + ')';
container.style.backgroundRepeat = 'no-repeat';
container.style.backgroundSize = '100% 100%';
container.style.margin = '0px';
container.style.backgroundPosition = 'center 0';
return container;
}
其实在这里我们可以修改createDeviceDom
的传参,从服务请求后,以参数的形式传进去:
//获取服务端数据
const data = getDadaFromServer('xxxxx')
//增加参数 data
createDeviceDom(id,imageUrl,w,name,data) {
//....
//创建DOM渲染数据
container.innerHTML = data.userName + data.userPhone
//....
}
建筑模型分层
这里代码比较多,我大致说一下逻辑:
- 先改变相机视角,调用
changeViewers()
方法 - 选中要分层的模型,定义TWEEN动画,沿着Z轴平移
- 通过定时器,展示楼层的注记图片,更改楼层模型材质
//楼层平移
floorPanning() {
let locations = [
{
center: [120.72893418497381, 31.289093070022545],
zoom: 18.47807755785181,
bearing: -43.624945530783634,
pitch: 49.98152219360618,
speed: 0.2,
curve: 1,
},
];
this.changeViewers(locations);
let self = this;
let model2 = this._group.children[0].children[3];
const tween2 = new TWEEN.Tween({ z: 0 }, false)
.to({ z: 1 }, 1000)
.easing(TWEEN.Easing.Quadratic.InOut)
.onUpdate((val) => {
model2.translateZ(val.z);
})
.start();
function animate(time) {
tween2.update(time);
requestAnimationFrame(animate);
}
animate();
setTimeout(() => {
self.floorPicture();
self.selectedObj = model2;
model2.traverse(function (child) {
if (child instanceof THREE.Mesh) {
// 将原有材质存储到自定义属性
child.baseMaterial = child.material;
// 复制原有的材质并修改颜色
child.material = child.material.clone();
child.material.color = new THREE.Color(0xffff00);
}
});
}, 2000);
}
模型选中与高亮
这里我们以查看路灯设备为例,包含3个过程
- 移动地图视角到路灯附近,选中路灯模型,
- 高亮模型
- 弹窗三维面板,查看路灯信息
这里代码比较多,但是对应上面我说的三个步骤
// 路灯设备查看
lampDeviceMarker() {
let self = this;
let locations = [
{
center: [120.72972234736437, 31.28788940180162],
zoom: 20.791587499232204,
bearing: 15.996875838980145,
pitch: 71.92451688228758,
speed: 0.5, // 速度
curve: 1, // 运动方式
},
];
this.changeViewers(locations);
let modelLayer = this._layer;
let modelGroup = this._group;
this._map.on('click', (e) => {
if (modelLayer) {
selectObj(e.point);
}
});
function selectObj(point) {
let intersect = modelLayer.selectModel(point)[0];
// 计算与摘取射线相交的物体
if (intersect) {
let nearestObject = intersect.object;
let name = nearestObject.parent.name;
if (name === 'lamp') {
// 模型轮廓效果
modelLayer.outlineModel([modelGroup.children[0].children[0]], {
scope: 'default', // 模型轮廓范围:layer、model、default
edgeStrength: 3.0, // 轮廓强度系数
edgeGlow: 0.0, // 轮廓发光稀释
edgeColor: '#00C1FF', // 轮廓颜色
enableFillColor: true, // 轮廓内部是否填充
fillColorOpacity: 0.2, // 轮廓内部填充颜色不透明度
});
// 标签
let datas = [
{
id: 1,
element: self.createDeviceDom(
'lampInfo',
'./assets/images/device.png',
'50px',
'70px'
),
position: [
120.72962054029763, 31.287636866542304, 11.556247058023184,
],
},
{
id: 2,
element: self.createDeviceDom(
'lamp_popup',
'./assets/images/UI/弹窗_设备信息2.png',
'186px',
'174px'
),
position: [
120.72960254553067, 31.28765832716174, 7.068647141749621,
],
},
];
let lampMarker = modelLayer.addMarker({
id: 'marker_lamp',
data: datas,
});
self.markerData.push(lampMarker);
}
}
}
}
移动视角依旧是用changeViewers()
方法,这里主要是如何选中模型,我看这里定义了一个selectObj()
方法,里面用到了射线检测
,通过鼠标点击的模型信息,来判断是不是选中了目标路灯模型。
function selectObj(point) {
let intersect = modelLayer.selectModel(point)[0];
// 计算与摘取射线相交的物体
if (intersect) {
let nearestObject = intersect.object;
let name = nearestObject.parent.name;
//...
如果是目标模型,就高亮它,这里也是用到他们文档上的模型图层接口outlineModel
if (name === 'lamp') {
// 模型轮廓效果
modelLayer.outlineModel([modelGroup.children[0].children[0]], {
scope: 'default', // 模型轮廓范围:layer、model、default
edgeStrength: 3.0, // 轮廓强度系数
edgeGlow: 0.0, // 轮廓发光稀释
edgeColor: '#00C1FF', // 轮廓颜色
enableFillColor: true, // 轮廓内部是否填充
fillColorOpacity: 0.2, // 轮廓内部填充颜色不透明度
});
//...
另外弹窗依旧是图片,和上面的安防预警一样的实现方式:
// 标签
let datas = [
{
id: 1,
element: self.createDeviceDom(
'lampInfo',
'./assets/images/device.png',
'50px',
'70px'
),
position: [
120.72962054029763, 31.287636866542304, 11.556247058023184,
],
},
设备点位与弹窗面板
这里我们以查看监控视频为例:
- 先添加设备图标
- 点击图标打开视频弹窗
- 点击x号关闭视频弹窗
这里代码很长我们分开看:
// 添加监控标签
addMonitorMarker(MonitorMarkeDatas) {
let self = this;
let modelLayer = this._layer;
// 标签
let datas = new Array(MonitorMarkeDatas.length);
var i;
for (i = 0; i < MonitorMarkeDatas.length; i++) {
datas[i] = {
id: i,
element: self.createDeviceDom(
'cameraInfo',
'./assets/images/camera4.png',
'30px',
'50px',
'cameraClass'
),
position: MonitorMarkeDatas[i],
};
}
self.monitorMarker = modelLayer.addMarker({
id: 'marker_monitor',
data: datas,
});
self.markerData.push(self.monitorMarker);
self.monitorMarker.element.children.forEach((dom, index) => {
dom.element.addEventListener('click', (e) => {
if (self.videoMarkers.length > 0) {
self.videoMarkers.forEach((m) => {
if (m.remove) m.remove();
m = null;
});
self.videoMarkers = [];
}
let dom = self.createDeviceDom(
'videopop',
'./assets/images/UI/弹窗_实时视频2.png',
'320px',
'160px'
);
dom.style.paddingTop = '40px';
dom.style.paddingBottom = '15px';
dom.style.backgroundColor = 'rgba(0,0,0,0.5)';
let closeIcon = document.createElement('div');
closeIcon.className = 'closeVideo';
const video = document.createElement('video');
video.src = './assets/images/UI/videoexample.mp4';
video.controls = false;
video.autoplay = true;
video.muted = true;
video.loop = true;
video.height = 160;
video.width = 320;
dom.appendChild(video);
dom.appendChild(closeIcon);
video.style.opacity = 1.0;
let cameraMarker = modelLayer.addMarker({
id: 'marker_video',
data: [
{
name: 'a',
element: dom,
position: datas[index].position,
},
],
});
self.videoMarkers.push(cameraMarker);
cameraMarker.element.children[0].element.addEventListener(
'click',
(e) => {
let target = e.target; // 获取当前点击的目标子元素
if (target.className == 'closeVideo') {
if (self.videoMarkers.length > 0) {
self.videoMarkers.forEach((m) => {
if (m.remove) m.remove();
m = null;
});
self.videoMarkers = [];
}
}
}
);
});
});
}
首先添加监控设备的小图标
// 标签
let datas = new Array(MonitorMarkeDatas.length);
var i;
for (i = 0; i < MonitorMarkeDatas.length; i++) {
datas[i] = {
id: i,
element: self.createDeviceDom(
'cameraInfo',
'./assets/images/camera4.png',
'30px',
'50px',
'cameraClass'
),
position: MonitorMarkeDatas[i],
};
}
self.monitorMarker = modelLayer.addMarker({
id: 'marker_monitor',
data: datas,
});
self.markerData.push(self.monitorMarker);
这里是批量添加的,调用的是他们文档里面的添加三维标签的APIaddMarker
然后就是添加视频的部分,这里写的有些复杂,完全用原生js的DOM实现的:
- 遍历每个三维标签
self.monitorMarker.element.children.forEach((dom, index) => {
//...
- 提前创建好弹窗和视频元素
let dom = self.createDeviceDom(
'videopop',
'./assets/images/UI/弹窗_实时视频2.png',
'320px',
'160px'
);
dom.style.paddingTop = '40px';
dom.style.paddingBottom = '15px';
dom.style.backgroundColor = 'rgba(0,0,0,0.5)';
//...
const video = document.createElement('video');
video.src = './assets/images/UI/videoexample.mp4';
video.controls = false;
video.autoplay = true;
video.muted = true;
video.loop = true;
video.height = 160;
video.width = 320;
dom.appendChild(video);
dom.appendChild(closeIcon);
video.style.opacity = 1.0;
//...
- 给每个标签绑定点击事件,根据鼠标点击的元素,选择开启或者关闭视频弹窗
let cameraMarker = modelLayer.addMarker({
id: 'marker_video',
data: [
{
name: 'a',
element: dom,
position: datas[index].position,
},
],
});
self.videoMarkers.push(cameraMarker);
cameraMarker.element.children[0].element.addEventListener(
'click',
(e) => {
let target = e.target; // 获取当前点击的目标子元素
if (target.className == 'closeVideo') {
if (self.videoMarkers.length > 0) {
self.videoMarkers.forEach((m) => {
if (m.remove) m.remove();
m = null;
});
self.videoMarkers = [];
}
}
}
);
只不过他们这里视频不是实时监控,而是用MP4文件代替的,不过没关系,我们也可替换为hls或者flv
格式的实时视频流。
屏幕适配
这里我看他们CSS单位用的是vw
和百分比
,当然也可以用,至于其他的适配方式,可以参考我之前的文章:
Vue+Echarts企业级大屏项目适配方案 juejin.cn/post/700908…
.top-bg{
position: relative;
width: 100%;
height: 100%;
background: url(../assets/img/人口数据@2x.png) no-repeat;
background-size: 100% 100%;
}
.top-content{
position: relative;
top: 7vh;
width: 100%;
height: 100%;
left: 50%;
transform: translate(-45%);
background: url(../assets/img/人口数据内容@2x.png) no-repeat;
background-size: 90% 72%;
}
UI面板
UI都在src/layout
里面,这里按照结构分了上下左右部分
但是里面没有图表,基本都是图片,主要是做示意吧,不过问题不大,我们自己可以加
这部分如果想替换真实的图表,也可以参考我之前的文章:juejin.cn/post/700908…
其他功能
其他功能相对比较简单,项目都是基于他们已有的产品 Mapmost SDK for WebGL做的,对应的接口在文档上都有
面板的功能触发统一都写在了LyBottom.vue
里
六、智慧工厂项目源码
这个和园区的实现逻辑是一样的,这里我就不展开了,大家可以自己看
七、源码如何获取
目前是购买产品送的,我是买了一年教育版体验的,可以找同学或者朋友借一个邮箱,先不说产品本身,就这2个项目源码就远超99元的价值