【BIM】基于BIMFACE的空间拆分与合并
BIMFACE中矩形空间拆分与合并
应用场景
在BIM运维场景中,空间同设备一样,作为一种资产被纳入运维管理体系,典型的应用场景例如商铺、防火分区等,这就涉及到空间的拆分和合并,在bimface中,已经实现了空间的动态调整,但是距离自定义的,较为直观的空间拆分与合并,目前的处理方式还不能够满足业务场景的需求,于是自行完成了基于bimface的矩形空间的拆分与合并的实现过程。
先说合并
合并矩形空间的前提条件,是有两个及以上的且相邻的矩形空间,如果两个空间不相邻,也就失去了合并的意义,即使合并也不能够表达出真实物理世界的空间结构。空间合并相对来说比较简单,每个空间都是有一系列的有序的点围起来的二维封闭平面,这一系列的点集暂且称之为边界信息点集,加上高度参数就形成了三维立体空间。相邻的矩形空间必然会有近似重合的点,如下图的黄圈部分,如果把这些点去掉,只保留最外围的点(极值点,如下图的白圈部分),就形成了一个新的有序的点集,构成了新的边界信息,再加上合理的高度,被合并的空间就产生了。

以下是空间合并的核心代码:
/**
 * 空间合并处理管道,适用于多个规则且相邻的矩形空间
 * @param {Array} boundaryArray 空间边界数据数组(必填)
 * @param {String} id 空间唯一标识(非必填)
 * @param {Number} height 空间高度(非必填)
 * @param {Glodon.Web.Graphics.Color} faceColor 空间表面颜色(非必填)
 * @param {Glodon.Web.Graphics.Color} frameColor 空间轮廓颜色(非必填)
 * @returns {Object} 新构造的空间边界
 * @requires WebUtils
 * @public
 */
mergeBoundaryPipeline: function (boundaryArray, id, height, faceColor, frameColor) {
	if (!boundaryArray || !boundaryArray.length) {
		console.warn("boundaryArray is empty!");
		return;
	}
	const vertical = 1;
	for (let n = 0, len = boundaryArray.length; n < len; n++) {
		//第一步:整理数据,去除小数部分
		let cleanData = this.cleanBoundaryData(boundaryArray[n]);
		//第二步:将所有的点数据存储至一维数组
		this.storePointArray(cleanData);
	}
	//第三步:筛选极值点
	let extremum = this.extremumBoundaryPoint(this.pointCollection, vertical);
	//第四步:根据极值点构造新边界
	let newBoundary = this.buildBoundary(extremum);
	this.viewer.createRoom(newBoundary, height || 5500, id || webUtils.guid(), faceColor || webUtils.fromHexColor('#ff0000', 0.25), frameColor || webUtils.fromHexColor('#ff0000'));
	return newBoundary;
},
/**
 * 通过顶点集合获取极值点,以便构造新的空间边界
 * @param {Array} pointCollection 被合并前的多个空间的顶点集合
 * @param {Number} direction 原空间的分隔方向 1:纵向 2:横向
 * @returns {Array} 从一系列顶点中筛选出的顶点集合
 */
extremumBoundaryPoint: function (pointCollection, direction) {
	const vertical = 1, horizontal = 2;
	let extremumPoint = [];
	minX = maxX = pointCollection[0].x;
	minY = maxY = pointCollection[0].y;
	for (let n = 1, len = pointCollection.length; n < len; n++) {
		pointCollection[n].x > maxX ? maxX = pointCollection[n].x : null;
		pointCollection[n].x < minX ? minX = pointCollection[n].x : null;
		pointCollection[n].y > maxY ? maxY = pointCollection[n].y : null;
		pointCollection[n].y < minY ? minY = pointCollection[n].y : null;
	}
	for (let k = 0, len = pointCollection.length; k < len; k++) {
		let currentPoint = pointCollection[k];
		if (direction === 1) {
			if (!(currentPoint.x > minX && currentPoint.x < maxX)) {
				let exist = extremumPoint.some(item => {
					if (item.x == currentPoint.x && item.y == currentPoint.y) {
						return true;
					}
					return false;
				})
				if (!exist) {
					extremumPoint.push(currentPoint);
				}
			} else {
				// console.log("分割方向:纵向");
			}
		}
		if (direction === 2) {
			if (!(currentPoint.y > minY && currentPoint.y < maxY)) {
				let exist = extremumPoint.some(item => {
					if (item.x == currentPoint.x && item.y == currentPoint.y) {
						return true;
					}
					return false;
				})
				if (!exist) {
					extremumPoint.push(currentPoint);
				}
			}
		}
	}
	//对符合条件的点集进行顺时针排序,思路是找到最大和最小占1、3索引,剩余的两个点随机
	return extremumPoint;
}
蓝色代表原始的分离的空间,红色代表合并后的空间效果

再说拆分
空间的拆分相对于合并就比较麻烦,因为合并只有一种方式,单拆分却有很多种。例如,沿着相对于空间水平方向或者垂直方向切割、沿着对角线切割、斜方向切割等,要考虑多种可能性。大体的思路是,首先监听鼠标单击事件,获取单击的两个点位置作为参数,可以计算出过该两点的直线,有了直线方程,再分别与空间边界的四条边计算交点,如果交点不在边界信息围成的区域内则丢弃,只保留在边界信息内的交点,如果与矩形区域相交,必然是两个交点(与矩形顶点相交没有意义,排除一个交点的可能),再按照拆分的类型分别计算拆分后的点集并排序,计算出两个新的边界点集,最终绘制出两个新的空间。

空间拆分的核心算法如下:
/**
 * 根据二维坐标点集和求解二元一次方程直线
 * @param {Array} pointArray 二维坐标点集合 [{x:100,y:200},{x:200,y:400}]
 * @returns {Object} 返回直线【Y = Ax + b】的斜率【A】和截距【b】
 */
resolveEquation: function (pointArray) {
	let result = {
		A: 0, b: 0
	};
	if (!pointArray || !pointArray.length) {
		console.warn("parameter pointArray invalidate!");
		return;
	}
	//解方程 Y = Ax + b 核心算法,此处考虑要不要四舍五入
	let A, b
	//不存在斜率
	if (Math.round(pointArray[1].y) === Math.round(pointArray[0].y)) {
		A = 0;
		b = pointArray[0].y;
		console.log("点集" + JSON.stringify(pointArray) + "对应的二元一次方程为:Y = " + b);
	} else if (Math.round(pointArray[0].x) === Math.round(pointArray[1].x)) {
		A = 0;
		b = pointArray[0].x;
		console.log("点集" + JSON.stringify(pointArray) + "对应的二元一次方程为:X = " + b);
	}
	//存在斜率
	else {
		A = (pointArray[1].y - pointArray[0].y) / (pointArray[1].x - pointArray[0].x);
		b = pointArray[0].y - pointArray[0].x * (pointArray[0].y - pointArray[1].y) / (pointArray[0].x - pointArray[1].x);
		console.log("点集" + JSON.stringify(pointArray) + "对应的二元一次方程为:Y = " + A + "*x + " + b);
	}
	result.A = A;
	result.b = b;
	return result;
},
/**
 * 根据点集合与边界计算交点
 * @param {Object} boundary 空间边界数据
 * @param {Array} pointArray 分割点集合
 * @param {Number} height 高度
 * @requires RoomUtils
 * @returns {Array} crossPointArray 直线与边界交点集合
 */
findCrossPoint: function (boundary, pointArray, height) {
	let roomUtils = new RoomUtils();
	//整理边界数据
	boundary = roomUtils.cleanBoundaryData(boundary);
	//计算分割点集所在的直线方程 Y = Ax + b
	let { A, b } = this.resolveEquation(pointArray);
	let pointList = boundary.loops[0];
	//直线与边界的交点集合,N条边N个点,最终会保留两个交点
	let pointCollection = [];
	let crossObjectArray = [];
	for (let n = 0, len = pointList.length; n < len; n++) {
		//item => 标识线段的两端点集合 [{x:x,y:y},{x:x,y:y}]
		let item = pointList[n];
		let roundX0 = Math.round(item[0].x), roundX1 = Math.round(item[1].x);
		let roundY0 = Math.round(item[0].y), roundY1 = Math.round(item[1].y);
		let crossObject = { item: item, cross: false, crossBy: undefined };
		//当边界线是垂直直线
		if (roundX0 === roundX1) {
			let y = this.calculateCoordinate(A, b, item[0].x, 0);
			let point = { x: item[0].x, y: y, z: height };
			//如果交点Y坐标在线段两端之间则加入到集合
			if (Math.min(item[0].y, item[1].y) < y && Math.max(item[0].y, item[1].y) > y) {
				pointCollection.push(new THREE.Vector3(point.x, point.y, point.z));
				crossObject.cross = true;
				crossObject.crossBy = new THREE.Vector3(point.x, point.y, point.z);
			}
		}
		//当边界线是水平直线
		if (roundY0 === roundY1) {
			let x = this.calculateCoordinate(A, b, 0, item[0].y);
			let point = { x: x, y: item[0].y, z: height };
			//如果交点X坐标在线段两端之间则加入到集合
			if (Math.min(item[0].x, item[1].x) < x && Math.max(item[0].x, item[1].x) > x) {
				pointCollection.push(new THREE.Vector3(point.x, point.y, point.z));
				crossObject.cross = true;
				crossObject.crossBy = new THREE.Vector3(point.x, point.y, point.z);
			}
		}
		crossObjectArray.push(crossObject);
		//其他情形暂不考虑,先验证可行性与准确性
	}
	return { pointCollection: pointCollection, crossObjectArray: crossObjectArray };
},
/**
 * 创建拆分后的空间
 * @param {Array} crossObjectArray 用于拆分空间的点集合
 * @requires WebUtils
 * @requires ModelHelper
 * @returns {Array} 拆分后的空间边界集合
 */
buildSplitAreas: function (crossObjectArray) {
	if (!crossObjectArray) return;
	console.log(crossObjectArray);
	var webUtils = new WebUtils();
	var modelHelper = new ModelHelper();
	//标识切割边是否相邻
	let isAdjacent = false;
	let boundaryCollection = [];
	//区分邻边还是对边
	for (let i = 0, len = crossObjectArray.length; i < len; i++) {
		if (i !== len - 1 && crossObjectArray[i].cross && crossObjectArray[i + 1].cross) {
			isAdjacent = true;
		}
	};
	//首尾相接时
	if (crossObjectArray[0].cross && crossObjectArray[crossObjectArray.length - 1].cross) {
		isAdjacent = true;
	}
	console.log(isAdjacent);
	//如果交点相邻
	if (isAdjacent) {
		//找到切割点的公共点作为中间点构件边界
		let boundaryPoints = [];
		let boundary = crossObjectArray.filter(p => { return p.cross });
		//找到公共点,如果不是首尾相接,取中间,否则取两边
		let commonPoint = webUtils.isObjectEqual(boundary[0].item[0], boundary[1].item[1]) ? boundary[0].item[0] : boundary[0].item[1];
		//寻找相交线中非公共点
		let leftPoint = [];
		webUtils.isObjectEqual(boundary[0].item[0], boundary[1].item[1]) ? leftPoint.push(boundary[0].item[1], boundary[1].item[0]) : leftPoint.push(boundary[0].item[0], boundary[1].item[1]);
		for (let k = 0, len = boundary.length; k < len; k++) {
			boundary[k].crossBy.z = 0;
			boundaryPoints.push(boundary[k].crossBy);
		}
		boundaryPoints.splice(1, 0, commonPoint);
		//获取三角侧边界对象
		var boundarys = modelHelper.buildAreaBoundary(boundaryPoints);
		boundaryCollection.push(boundarys);
		//开始寻找另一侧点集
		let oppositeBoundary = crossObjectArray.filter(p => { return !p.cross });
		let oppositePoint = webUtils.isObjectEqual(oppositeBoundary[0].item[0], oppositeBoundary[1].item[1]) ? oppositeBoundary[0].item[0] : oppositeBoundary[0].item[1];
		//组装另一侧空间边界
		leftPoint.splice(1, 0, oppositePoint);
		//点集排序
		if (leftPoint[0].x === boundary[0].crossBy.x || leftPoint[0].y === boundary[0].crossBy.y) {
			leftPoint.splice(0, 0, boundary[0].crossBy);
			leftPoint.splice(leftPoint.length, 0, boundary[1].crossBy);
		} else {
			leftPoint.splice(0, 0, boundary[1].crossBy);
			leftPoint.splice(leftPoint.length, 0, boundary[0].crossBy);
		}
		//获取非三角侧边界对象
		console.log("leftPoint", leftPoint);
		var boundarys2 = modelHelper.buildAreaBoundary(leftPoint);
		boundaryCollection.push(boundarys2);
	} else {
		let points = [];
		//如果交点非相邻(对边)
		if (crossObjectArray[0].cross) {
			crossObjectArray[0].crossBy.z = crossObjectArray[2].crossBy.z = 0;
			points.push(crossObjectArray[3].item[0], crossObjectArray[3].item[1], crossObjectArray[0].crossBy, crossObjectArray[2].crossBy);
			boundaryCollection.push(modelHelper.buildAreaBoundary(points));
			points = [];
			points.push(crossObjectArray[0].crossBy, crossObjectArray[1].item[0], crossObjectArray[1].item[1], crossObjectArray[2].crossBy);
			boundaryCollection.push(modelHelper.buildAreaBoundary(points));
		} else {
			crossObjectArray[1].crossBy.z = crossObjectArray[3].crossBy.z = 0;
			points.push(crossObjectArray[0].item[0], crossObjectArray[0].item[1], crossObjectArray[1].crossBy, crossObjectArray[3].crossBy);
			boundaryCollection.push(modelHelper.buildAreaBoundary(points));
			points = [];
			points.push(crossObjectArray[1].crossBy, crossObjectArray[2].item[0], crossObjectArray[2].item[1], crossObjectArray[3].crossBy);
			boundaryCollection.push(modelHelper.buildAreaBoundary(points));
		}
	}
	return boundaryCollection;
}
总体效果

目前的空间拆分仅限于矩形空间,因为矩形的空间在BIM运维中相对来说是比较多的,而且算法相对简单一些,后续我们会逐渐探索非矩形空间,甚至是不规则多边形的空间拆分与合并算法,并应用到空间资产管理与运维场景中。
地址:https://www.cnblogs.com/xhb-bky-blog/p/13500295.html
声明:本博客原创文字只代表本人工作中在某一时间内总结的观点或结论,与本人所在单位没有直接利益关系。非商业,未授权贴子请以现状保留,转载时必须保留此段声明,且在文章页面明显位置给出原文连接。
【BIM】基于BIMFACE的空间拆分与合并的更多相关文章
- NDK学习笔记-文件的拆分与合并
		
文件的拆分与合并在开发中经常会用到,上传或是下载的时候都有这样的运用 文件拆分的思路 将文件大小拆分为n个文件 那么,每个文件的大小就是等大小的 如果文件大小被n除不尽,那么就使用n+1个文件来拆分 ...
 - (数据科学学习手札84)基于geopandas的空间数据分析——空间计算篇(上)
		
本文示例代码.数据及文件已上传至我的Github仓库https://github.com/CNFeffery/DataScienceStudyNotes 1 简介 在本系列之前的文章中我们主要讨论了g ...
 - (数据科学学习手札88)基于geopandas的空间数据分析——空间计算篇(下)
		
本文示例代码及数据已上传至我的Github仓库https://github.com/CNFeffery/DataScienceStudyNotes 1 简介 在基于geopandas的空间数据分析系列 ...
 - 基于Solr的空间搜索
		
如果需要对带经纬度的数据进行检索,比如查找当前所在位置附近1000米的酒店,一种简单的方法就是:获取数据库中的所有酒店数据,按经纬度计算距离,返回距离小于1000米的数据. 这种方式在数据量小的时候比 ...
 - pdf拆分与合并
		
1.引用iTextSharp,用于拆分和合并pdf文件 using iTextSharp.text; using iTextSharp.text.pdf; 2.合并pdf //outMergeFile ...
 - fasta文件拆分与合并
		
Linux中fasta文件的拆分与合并 FASTA文件的拆分: (1)如果从一个文件a提取第11至20个序列存到另一个文件b: awk -v RS='>' 'NR>1{i++}i>= ...
 - Goldengate进程的拆分与合并
		
Goldengate的拆分与合并分类: ORACLE GoldenGate 2013-10-10 15:22 721人阅读 评论(0) 收藏 举报在使用Goldengate作为复制解决方案时,随着负载 ...
 - 基于AutoCAD的空间数据共享平台雏形
		
好久没有更新博客了,今天先透露一个新的产品——AutoMap.我自己对于这个产品的定位是“基于AutoCAD的空间数据共享平台”.用一句话来概括AutoMap的功能:为用户提供一个在AutoCAD下访 ...
 - C#文件的拆分与合并操作示例
		
C#文件的拆分与合并操作示例代码. 全局变量定义 ;//文件大小 //拆分.合并的文件数 int count; FileInfo splitFile; string splitFliePath; Fi ...
 
随机推荐
- SpringBoot整合Shiro自定义Redis存储
			
Shiro Shiro 主要分为 安全认证 和 接口授权 两个部分,其中的核心组件为 Subject. SecurityManager. Realms,公共部分 Shiro 都已经为我们封装好了,我们 ...
 - JS 留言板案例
			
css代码 ul { list-style: none; } ul li { background-color: pink; line-height: 40px; margin: 10px; widt ...
 - 史上最简单操作!!!!!!!Window Server2012 修改远程桌面端口号
			
Window Server2012 修改远程桌面端口号 Win + R 输入 regedit 打开注册表编辑器 在注册表编辑器中找到 PortNumber 双击 PortNumber,选择10进制 ...
 - Vue 修饰符sync的应用
			
官方链接 https://cn.vuejs.org/v2/guide/components-custom-events.html#sync-修饰符 这个解释有点不太直观,用代码解释一下 父组件 v-b ...
 - 一个在交流群里讨论过两轮的问题,答案竟然跟一个 PEP 有关
			
Python 中有没有办法通过类方法找到其所属的类? 这个问题看起来不容易理解,我可以给出一个例子: class Test: @xxx def foo(self): pass 现在有一个类和一个类方法 ...
 - c/c++ 感悟  2008-10-03 02:08
			
许久没有坐在电脑前写东西了.除了密密麻麻的英文小虫子,还是英文小虫子.今天不是解决bug,明天就是在创造bug,一句话不在bug中沉默就在bug中爆发.或许喜欢小宇宙爆发的样子吧,那样的感觉总是让人热 ...
 - SSH 加固指南
			
本文翻译自:A Guide to Securing the SSH Daemon SSH(Secure Shell)是一种能够让用户安全访问远程系统的网络协议,它为不安全网络中的两台主机提供了一个强加 ...
 - 推荐一看的blog
			
不同专题: 个人blog cnblogs.com/MiLog cnblogs.com/Dway (指DeepWay) cnblogs.com/muly 建议一看,主要发布在cnblogs.com/dl ...
 - 目录扫描、Nmap
			
一.基本定义 1.目录扫描: 扫描站点的目录,寻找敏感文件(目录名.探针文件.后台.robots.txt.备份文件等). 2.目录:站点结构,权限控制不严格. 3.探针文件:服务器配置信息,例:php ...
 - Jenkins配置总结
			
1.配置全局 系统管理->全局工具配置 2.配置 自己安装安装jdk,git,以及maven 3.系统管理->系统配置 3.1配置Jenkins URL 3.2 配置SSH Servers ...