现代工业化的推进在极大加速现代化进程的同时也带来的相应的安全隐患,在传统的监控领域,一般都是基于Web前端技术来实现 2D 可视化监控,本文采用ThingJS来构造轻量化的 3D 可视化场景,该3D场景展示了一个现代化商场的数字孪生可视化场景,包括人员的实时位置、电子围栏的范围、现场的安全情况等等,帮助直观的了解当前人员的安全状况。
电子围栏 又称周界防盗报警系统,监控防区工作状态,实景中的电子围栏系统用于农业、畜牧业,以及监狱、军事设施等安全敏感地区。ThingJS平台上,电子围栏指的是一个区域,使用PolygonRegion属性。创建物体对象或模型并开启移动功能,即可开始检测目标点是否进入电子围栏区域,判断true或false显示告警反应。
本篇文章通过对数字孪生可视化场景的搭建和模型的加载,人物实时定位代码的实现、电子围栏和轨迹图的实现进行阐述,了解如何通过使用ThingJS实现一个简单的3D电子围栏可视化。
// 添加电子围栏
new THING.widget.Button('添加电子围栏', function() {
// 构成多边形的点(取世界坐标系下的坐标)
points = [
[81, 0.5, 63],
[81, 0.5, 52],
[72, 0.5, 52],
[72, 0.5, 63]
];
if (polygonMarker) { return; }
// 创建电子围栏(区域)
polygonMarker = app.create({
type: 'PolygonRegion',
points: points, // 传入世界坐标系下点坐标
style: {
regionOpacity: .6,
regionColor: '#3CF9DF', // 区域颜色
lineColor: '#3CF9DF' // 线框颜色
}
});
// 设置永远在最上层显示
polygonMarker.style.alwaysOnTop = false;
})
当人物或物体对象出发警报时,有2种方式提醒注意,一是踏足的禁区围栏颜色发生改变;二是展示面板显示报警信息,可视化监控目标点的移动范围。
完整代码如下:
// 添加图片标注
new THING.widget.Button('添加图片标注', function() {
var coord = [83, 0.5, 61];
if (marker1) { return; }
// 创建目标点(marker)
marker1 = app.create({
type: "Marker",
id: "marker1",
url: "/guide/examples/images/navigation/user.png",
position: coord,
size: 1
})
})
var point = [
[81, 63],
[81, 52],
[72, 52],
[72, 63]
];
// 移动图片标注
new THING.widget.Button('移动图片标注', function() {
var markerEndPoint = [68, 0.5, 55];
if (marker1 != null) {
var moveState = marker1.getAttribute('moveState');
if (moveState == 'complete') {
marker1.off('update', null, '监控图片标注');
return;
}
// 目标点移动
marker1.moveTo({
position: markerEndPoint, // 移动到终点位置
time: 2 * 1000,
orientToPath: true, // 沿路径方向
complete: function(ev) {
marker1.off('update', null, '监控图片标注');
$('.warninfo1').css('display', 'none');
$('.warninfo2').css('display', 'block');
$('.warninfo3').css('display', 'none');
marker1.setAttribute('moveState', 'complete');
}
})
}
if (points != null) {
// 监控图片标注是否进入电子围栏区域
if (marker1 != null) {
marker1.on('update', function() {
if (polygonMarker != null) {
var intoPolygonMarker = isInPolygon([marker1.position[0], marker1.position[2]], point);
if (intoPolygonMarker) {
polygonMarker.regionColor = '#a94442';
polygonMarker.lineColor = '#a94442'
$('.warninfo1').css('display', 'block');
$('.warninfo2').css('display', 'none');
$('.warninfo3').css('display', 'none');
} else {
polygonMarker.regionColor = '#3CF9DF';
polygonMarker.lineColor = '#3CF9DF'
$('.warninfo1').css('display', 'none');
$('.warninfo2').css('display', 'none');
$('.warninfo3').css('display', 'block');
}
}
}, '监控图片标注')
}
}
})
// 添加模型标注
new THING.widget.Button('添加模型标注', function() {
//创建目标点(Obj)
people = app.query('#worker')[0];
people.position = [83, 0.1, 56];
people.visible = true;
people.scale = [1.5, 1.5, 1.5];
})
// 移动模型标注
new THING.widget.Button('移动模型标注', function() {
var objEndPoint = [70, 0.1, 60];
if (people != null) {
var moveState = people.getAttribute('moveState');
if (moveState == 'complete') {
people.off('update', null, '监控图片标注');
return;
}
// 播放模型动画
people.playAnimation({
name: '走',
speed: 1,
loopType: THING.LoopType.Repeat,
});
// 模型移动
people.moveTo({
position: objEndPoint, // 移动到终点位置
orientToPath: true, // 沿路径方向
time: 8 * 1000,
complete: function(ev) {
people.stopAnimation('走');
people.off('update', null, '监控模型标注');
$('.warninfo1').css('display', 'none');
$('.warninfo2').css('display', 'block');
$('.warninfo3').css('display', 'none');
people.setAttribute('moveState', 'complete');
}
})
}
if (points != null) {
// 监控模型标注是否进入电子围栏区域
if (people != null) {
people.on('update', function() {
if (polygonMarker != null) {
var intoPolygonMarker = isInPolygon([people.position[0], people.position[2]], point);
if (intoPolygonMarker) {
polygonMarker.regionColor = '#a94442';
polygonMarker.lineColor = '#a94442'
$('.warninfo1').css('display', 'block');
$('.warninfo2').css('display', 'none');
$('.warninfo3').css('display', 'none');
} else {
polygonMarker.regionColor = '#3CF9DF';
polygonMarker.lineColor = '#3CF9DF'
$('.warninfo1').css('display', 'none');
$('.warninfo2').css('display', 'none');
$('.warninfo3').css('display', 'block');
}
}
}, '监控模型标注')
}
}
})
// 重置
new THING.widget.Button('重置', function() {
if (polygonMarker) {
polygonMarker.destroy();
polygonMarker = null;
}
if (marker1) {
marker1.destroy();
marker1 = null;
}
if (people) {
people.visible = false;
people.setAttribute('moveState', null);
}
$('.warninfo1').css('display', 'none');
$('.warninfo1').css('display', 'none');
$('.warninfo1').css('display', 'block');
})
createTip(); // 创建提示面板
});
/**
* 创建提示面板
*/
function createTip() {
var html =
`<div class="fencing" style="width:200px;position: absolute;top: 50px;left: 50%;transform: translateX(-50%);z-index: 999;">
<div class="alert alert-danger warninfo1" role="alert" style="padding: 15px;margin-bottom: 20px;color: #a94442;background-color: #f2dede;border-color: #ebccd1;border-radius: 4px;display:none;">目标已进入围栏</div>
<div class="alert alert-info warninfo2" role="alert" style="padding: 15px;margin-bottom: 20px;color: #31708f;background-color: #d9edf7;border-color: #bce8f1;border-radius: 4px;display:none;">到达目的地</div>
<div class="alert alert-warning warninfo3" role="alert" style="padding: 15px;margin-bottom: 20px;color: #8a6d3b;background-color: #fcf8e3;border-color: #faebcc;border-radius: 4px;">目标未进入围栏</div>
<div onclick="fenClose()" style="cursor: pointer;position: absolute;top: -7px;right: -8px;width: 16px;height: 16px;border-radius: 50%;background-color: #777777;border: 3px solid #ffffff;">
<div style="position: absolute;width: 10px;height: 2px;background-color: #fff;transform: rotate(45deg);top: 7px;left: 3px;"></div>
<div style="position: absolute;width: 10px;height: 2px;background-color: #fff;transform: rotate(-45deg);top: 7px;left: 3px;"></div>
</div>
</div>`;
$('#div2d').append($(html));
}
/**
* 关闭提示面板
*/
function fenClose() {
$(".fencing").hide();
}
/**
* 检测目标点是否进入电子围栏区域
* @param {Array} checkPoint - 校验坐标
* @param {Array} polygonPoints - 形成电子围栏的坐标
* @returns {Boolean} true 或 false
* @description 此方法仅判断处于同一个平面的目标点是否在区域内(只判断坐标x和z值),
* 不考虑两者当前离地高度(坐标的y值)
*/
function isInPolygon(checkPoint, polygonPoints) {
var counter = 0;
var i;
var xinters;
var p1, p2;
var pointCount = polygonPoints.length;
p1 = polygonPoints[0];
for (i = 1; i <= pointCount; i++) {
p2 = polygonPoints[i % pointCount];
if (checkPoint[0] > Math.min(p1[0], p2[0]) && checkPoint[0] <= Math.max(p1[0], p2[0])) {
if (checkPoint[1] <= Math.max(p1[1], p2[1])) {
if (p1[0] != p2[0]) {
xinters = (checkPoint[0] - p1[0]) * (p2[1] - p1[1]) / (p2[0] - p1[0]) + p1[1];
if (p1[1] == p2[1] || checkPoint[1] <= xinters) {
counter++;
}
}
}
}
p1 = p2;
}
if (counter % 2 == 0) {
return false;
} else {
return true;
}
}